Go从入门到精通(20)-一个简单web项目-服务搭建


文章目录

  • Go从入门到精通(20)-一个简单web项目-服务搭建
  • 前言
  • 前期准备
  • 为API 添加 Swagger 文档
    • 1.安装依赖
    • 2.添加 Swagger 注释
      • main.go
      • app.go
      • api.go
      • public_handler.go
      • auth_handler.go
      • common_constant.go
      • common_dto.go
      • token_utils.go
    • 3.生成swagger文档
      • swgger.go
    • 4.访问 Swagger UI
  • swagger注释说明
    • 1.全局信息
    • 2 API 操作注释
    • 3.模型定义
    • 4.认证说明
  • swagger2openapi3
  • 注意事项


前言

Api比较多,没有文档不能清晰的知道每个文档的参数。下面我们引入swagger文档来解决这一个问题


前期准备

上一版我们所有的文件都在main文的main包下,这期我们简单分一下包,这样项目结构更加清晰。

go-web-demo
├───app
│ ├───api
│ │ └───handler
│ ├───constant
│ ├───dto
│ └───utils
├───config
├───discovery
├───docs
├───global
├───logger
└───tracing

简单说明一下

  • app:业务模块
    • api: Api的入口,类似controller层
      • handler Api实现,类似service层
    • dto 定义请求响应dto
    • constants 常量和枚举类
    • utils 工具类
  • config:配置模块
  • discovery:服务注册发现模块
  • logger:日志模块
  • tracing:链路追踪模块
  • docs:文档比如swagger文档
  • global:读取一些全局配置参数

大概项目结构如下
项目结构

为API 添加 Swagger 文档

我将使用swaggo/swag自动生成文档,并通过swaggo/gin-swagger在浏览器中展示。

1.安装依赖

go get -u github.com/swaggo/swag/cmd/swag
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

2.添加 Swagger 注释

main.go

这里看main()函数本身已经很简单了,主要的工作都是在下面的包完成的

package mainimport ("fmt""go-web-demo/app"
)// @title          Gin API Example
// @version        1.0
// @description    This is a sample server for a web application.
// @termsOfService http://swagger.io/terms/// @contact.name  API Support
// @contact.url   http://www.swagger.io/support
// @contact.email support@swagger.io// @license.name Apache 2.0
// @license.url  http://www.apache.org/licenses/LICENSE-2.0.html// @host     localhost:8082
// @BasePath /api
func main() {err := app.StartApp()if err != nil {fmt.Println("Failed to start app:", err)return}
}

app.go

这个里包主要完成gin的初始化和启动
注意这里添加的swagger配置

// Swagger文档路由
docs.Init(“localhost:8082”)
router.GET(“/swagger/*any”,ginSwagger.WrapHandler(swaggerFiles.Handler))

以及引入的swagger包

import swaggerFiles “github.com/swaggo/files”
import ginSwagger “github.com/swaggo/gin-swagger”

package appimport ("fmt""github.com/gin-contrib/cors""github.com/gin-gonic/gin"swaggerFiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger""go-web-demo/app/api""go-web-demo/docs"
)func StartApp() error {// 设置为生产模式// gin.SetMode(gin.ReleaseMode)// 创建默认引擎,包含日志和恢复中间件router := gin.Default()// 配置CORSrouter.Use(cors.Default())// Swagger文档路由docs.Init("localhost:8082")router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))err := api.InitRouters(router)if err != nil {return err}// 启动服务器fmt.Println("Server started at :8082")if err := router.Run(":8082"); err != nil {fmt.Println("Failed to start server:", err)}return nil
}

api.go

这里主要是分包,不需要额外的注释

package apiimport ("github.com/gin-gonic/gin""go-web-demo/app/api/handler""go-web-demo/app/utils"
)// http接口映射
func InitRouters(router *gin.Engine) error {// 公共路由public := router.Group("/api/public"){public.POST("/register", handler.RegisterHandler)public.POST("/login", handler.LoginHandler)public.GET("/health", handler.HealthHandler)}// 认证路由auth := router.Group("/api/v1/auth")auth.Use(utils.AuthMiddleware()){auth.GET("/users/me", handler.GetCurrentUserHandler)auth.GET("/users", handler.GetUsersHandler)auth.GET("/users/:id", handler.GetUserHandler)auth.PUT("/users/:id", handler.UpdateUserHandler)auth.DELETE("/users/:id", handler.DeleteUserHandler)}return nil
}

public_handler.go

这里主要拆分Api实现,注意函数上面的注释就是生成swagger的文档的来源
// @Param user body dto.RegisterRequest true "用户注册信息" 引用参数如果在包下面也必须带上包,比如这里要用 dto.RegisterReques,直接使用RegisterRequest会提示找不到

package handlerimport ("fmt""github.com/gin-gonic/gin""go-web-demo/app/dto""go-web-demo/app/utils""golang.org/x/crypto/bcrypt""net/http"
)// 模拟数据库
var users = make(map[string]dto.User)
var nextUserID = 1// 健康检查
// HealthHandler 健康检查
// @Summary      测试API连通性
// @Description  简单的测试接口,success
// @Tags         通用
// @Produce      json
// @Success      200  {object}  map[string]string  "success"
// @Router       /ping [get]
func HealthHandler(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "success"})
}// 注册处理
// RegisterHandler 注册新用户
// @Summary      注册新用户
// @Description  创建一个新用户账户
// @Tags         用户
// @Accept       json
// @Produce      json
// @Param        user  body      dto.RegisterRequest  true  "用户注册信息"
// @Success      201   {object}  dto.TokenResponse    "注册成功,返回JWT令牌"
// @Failure      400   {object}  dto.ErrorResponse    "参数错误"
// @Failure      409   {object}  dto.ErrorResponse    "用户名已存在"
// @Failure      500   {object}  dto.ErrorResponse    "服务器内部错误"
// @Router       /register [post]
func RegisterHandler(c *gin.Context) {var request dto.RegisterRequest// 绑定并验证请求if err := c.ShouldBindJSON(&request); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 检查用户名是否已存在for _, user := range users {if user.Username == request.Username {c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})return}}// 哈希密码hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})return}// 创建新用户userID := fmt.Sprintf("%d", nextUserID)nextUserID++user := dto.User{ID:       userID,Username: request.Username,Password: string(hashedPassword),Email:    request.Email,}// 保存用户users[userID] = user// 生成令牌token, err := utils.GenerateToken(userID)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})return}c.JSON(http.StatusCreated, dto.TokenResponse{Token: token})
}// 登录处理
// LoginHandler 用户登录
// @Summary      用户登录
// @Description  使用用户名和密码进行登录
// @Tags         用户
// @Accept       json
// @Produce      json
// @Param        credentials  body      dto.LoginRequest  true  "登录凭证"
// @Success      200          {object}  dto.TokenResponse "登录成功,返回JWT令牌"
// @Failure      400          {object}  dto.ErrorResponse "参数错误"
// @Failure      401          {object}  dto.ErrorResponse "认证失败"
// @Router       /login [post]
func LoginHandler(c *gin.Context) {var request dto.LoginRequest// 绑定并验证请求if err := c.ShouldBindJSON(&request); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 查找用户var user dto.Userfor _, u := range users {if u.Username == request.Username {user = ubreak}}// 验证用户if user.ID == "" {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})return}// 验证密码if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)); err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})return}// 生成令牌token, err := utils.GenerateToken(user.ID)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})return}c.JSON(http.StatusOK, dto.TokenResponse{Token: token})
}

auth_handler.go

package handlerimport ("github.com/gin-gonic/gin""go-web-demo/app/dto""golang.org/x/crypto/bcrypt""net/http"
)// GetCurrentUserHandler 获取当前用户信息
// @Summary      获取当前用户信息
// @Description  获取已登录用户的详细信息
// @Tags         用户
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Success      200  {object}  dto.User        "成功返回用户信息"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Failure      404  {object}  dto.ErrorResponse "用户不存在"
// @Router       /users/me [get]
func GetCurrentUserHandler(c *gin.Context) {userID := c.MustGet("user_id").(string)user, exists := users[userID]if !exists {c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})return}// 不返回密码user.Password = ""c.JSON(http.StatusOK, user)
}// GetUsersHandler 获取所有用户
// @Summary      获取所有用户列表
// @Description  获取系统中所有用户的信息(需管理员权限)
// @Tags         用户
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Success      200  {array}   dto.User        "成功返回用户列表"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Router       /users [get]
func GetUsersHandler(c *gin.Context) {var userList []dto.Userfor _, user := range users {// 不返回密码user.Password = ""userList = append(userList, user)}c.JSON(http.StatusOK, userList)
}// GetUserHandler 获取单个用户
// @Summary      获取单个用户信息
// @Description  根据用户ID获取用户详细信息
// @Tags         用户
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  true  "用户ID"
// @Success      200  {object}  dto.User    "成功返回用户信息"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Failure      404  {object}  dto.ErrorResponse "用户不存在"
// @Router       /users/{id} [get]
func GetUserHandler(c *gin.Context) {userID := c.Param("id")user, exists := users[userID]if !exists {c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})return}// 不返回密码user.Password = ""c.JSON(http.StatusOK, user)
}// UpdateUserHandler 更新用户信息
// @Summary      更新用户信息
// @Description  更新当前用户的信息
// @Tags         用户
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id    path      string  true  "用户ID"
// @Param        user  body      dto.User    true  "要更新的用户信息"
// @Success      200   {object}  dto.User    "成功返回更新后的用户信息"
// @Failure      400   {object}  dto.ErrorResponse "参数错误"
// @Failure      401   {object}  dto.ErrorResponse "未授权"
// @Failure      403   {object}  dto.ErrorResponse "权限不足"
// @Failure      404   {object}  dto.ErrorResponse "用户不存在"
// @Router       /users/{id} [put]
func UpdateUserHandler(c *gin.Context) {userID := c.Param("id")currentUserID := c.MustGet("user_id").(string)// 只能更新自己的信息if userID != currentUserID {c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})return}user, exists := users[userID]if !exists {c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})return}var updateData struct {Username string `json:"username"`Email    string `json:"email"`Password string `json:"password"`}if err := c.ShouldBindJSON(&updateData); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 更新字段if updateData.Username != "" {user.Username = updateData.Username}if updateData.Email != "" {user.Email = updateData.Email}if updateData.Password != "" {// 哈希新密码hashedPassword, err := bcrypt.GenerateFromPassword([]byte(updateData.Password), bcrypt.DefaultCost)if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})return}user.Password = string(hashedPassword)}// 保存更新users[userID] = user// 不返回密码user.Password = ""c.JSON(http.StatusOK, user)
}// DeleteUserHandler 删除用户
// @Summary      删除用户
// @Description  删除当前用户账户
// @Tags         用户
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  true  "用户ID"
// @Success      204           "成功删除"
// @Failure      401  {object}  dto.ErrorResponse "未授权"
// @Failure      403  {object}  dto.ErrorResponse "权限不足"
// @Failure      404  {object}  dto.ErrorResponse "用户不存在"
// @Router       /users/{id} [delete]
func DeleteUserHandler(c *gin.Context) {userID := c.Param("id")currentUserID := c.MustGet("user_id").(string)// 只能删除自己的账户if userID != currentUserID {c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})return}_, exists := users[userID]if !exists {c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})return}// 删除用户delete(users, userID)c.JSON(http.StatusNoContent, nil)
}

common_constant.go

package constantimport "time"// 配置信息
const (SecretKey       = "your-secret-key"TokenExpiration = 24 * time.Hour
)

common_dto.go

这里其实应该按业务拆分为多个go文件,数量不多就先放一起吧

package dto// User 用户模型
// @Description 用户信息的完整表示
type User struct {// 用户唯一标识,系统自动生成的UUIDID string `json:"id" binding:"required" example:"5f8d4e1c-3b9d-4c9d-8e1c-3b9d4c9d8e1c"`// 用户名,用于登录,必须全局唯一Username string `json:"username" binding:"required,min=3,max=20" example:"john_doe"`// 用户密码(哈希后),登录时验证,不返回给客户端Password string `json:"password,omitempty" example:"$2a$10$Z1JzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJzJ"`// 用户邮箱地址,用于接收通知和密码重置Email string `json:"email" binding:"required,email" example:"john@example.com"`
}// LoginRequest 登录请求
// @Description 用户登录时提交的凭证
type LoginRequest struct {// 登录用户名Username string `json:"username" binding:"required" example:"john_doe"`// 登录密码Password string `json:"password" binding:"required" example:"SecurePass123"`
}// RegisterRequest 注册请求
// @Description 新用户注册时提交的信息
type RegisterRequest struct {// 注册用户名,3-20个字符,只能包含字母、数字和下划线Username string `json:"username" binding:"required,min=3,max=20,alphanum" example:"new_user123"`// 注册密码,至少6个字符,需包含大小写字母和数字Password string `json:"password" binding:"required,min=6" example:"Passw0rd!"`// 注册邮箱,必须为有效的邮箱格式Email string `json:"email" binding:"required,email" example:"user@example.com"`
}// TokenResponse 令牌响应
// @Description 登录或注册成功后返回的认证令牌
type TokenResponse struct {// JWT认证令牌,用于后续请求的Authorization头Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImV4cCI6MTY5MzkxMDQwMH0._..."`
}// ErrorResponse 错误响应
// @Description API请求失败时返回的错误信息
type ErrorResponse struct {// 错误代码,用于客户端识别具体错误类型Code int `json:"code" example:"400"`// 错误消息,简要描述错误原因Message string `json:"message" example:"Invalid request parameters"`// 错误详情,包含具体字段的错误信息(可选)Details map[string]string `json:"details,omitempty" example:"{'username': 'Username already exists'}"`
}

token_utils.go

package utilsimport ("fmt""github.com/dgrijalva/jwt-go""github.com/gin-gonic/gin""go-web-demo/app/constant""net/http""time"
)// 生成JWT令牌
func GenerateToken(userID string) (string, error) {// 创建令牌token := jwt.New(jwt.SigningMethodHS256)// 设置声明claims := token.Claims.(jwt.MapClaims)claims["id"] = userIDclaims["exp"] = time.Now().Add(constant.TokenExpiration).Unix()// 生成签名字符串return token.SignedString([]byte(constant.SecretKey))
}// 认证中间件
func AuthMiddleware() gin.HandlerFunc {return func(c *gin.Context) {// 获取授权头authHeader := c.GetHeader("Authorization")if authHeader == "" {c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})c.Abort()return}// 验证授权头格式if len(authHeader) < 7 || authHeader[:7] != "Bearer " {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})c.Abort()return}// 提取令牌tokenString := authHeader[7:]// 解析令牌token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {// 验证签名方法if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])}return []byte(constant.SecretKey), nil})if err != nil {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})c.Abort()return}// 验证令牌有效性if !token.Valid {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})c.Abort()return}// 提取用户IDclaims, ok := token.Claims.(jwt.MapClaims)if !ok {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})c.Abort()return}userID, ok := claims["id"].(string)if !ok {c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID in token"})c.Abort()return}// 将用户ID添加到上下文c.Set("user_id", userID)// 继续处理请求c.Next()}
}

3.生成swagger文档

在项目根目录下执行:

swag init

这将自动解析代码中的注释,生成docs目录,包含docs.go、swagger.json和swagger.yaml文件。
在这里插入图片描述

swgger.go

这段代码读取文件内容,放入swaggerDoc 变量

//go:embed swagger.json
var swaggerDoc []byte

因为生成的swagger文档是静态内容,下面的代码展示怎么动态注入变更swagger文档内容

package docsimport (_ "embed""encoding/json""fmt"
)//go:embed swagger.json
var swaggerDoc []bytefunc Init(swaggerHost string) {SwaggerInfo.Host = swaggerHostswaggerMap := make(map[string]interface{})err := json.Unmarshal(swaggerDoc, &swaggerMap)if err != nil {return}swaggerMapServer := make([]map[string]string, 0)swaggerMapServer = append(swaggerMapServer, map[string]string{"url": fmt.Sprintf("http://%s/api/**", swaggerHost),})swaggerMap["servers"] = swaggerMapServerswaggerJson, err := json.Marshal(swaggerMap)if err != nil {return}swaggerJsonString := string(swaggerJson)SwaggerInfo.SwaggerTemplate = swaggerJsonString
}

4.访问 Swagger UI

启动服务器后,访问:

http://localhost:8082/swagger/index.html

就能看到类似下面的页面了
swagger文档

swagger注释说明

1.全局信息

// @title Gin API Example
// @version 1.0
// @description This is a sample server for a web application.
// @host localhost:8080
// @BasePath /api

2 API 操作注释

// @Summary 注册新用户
// @Description 创建一个新用户账户
// @Tags 用户
// @Accept json
// @Produce json
// @Param user body RegisterRequest true “用户注册信息”
// @Success 201 {object} TokenResponse “注册成功,返回JWT令牌”
// @Failure 400 {object} ErrorResponse “参数错误”
// @Router /register [post]

3.模型定义

// User 用户模型
// @Description 用户信息
type User struct {
ID string json:"id" binding:"required"
Username string json:"username" binding:"required"
Password string json:"password,omitempty"
Email string json:"email" binding:"required,email"
}

4.认证说明

Security

swagger2openapi3

部分Api网关可能要去OpenAPI 3.0.1格式。Swagger2openapi3 提供了一个包,可以将Swagger 2.0规范的JSON和YAML转换为OpenAPI 3.0.1。它还提供了一个工具叫做swag2op,它集成了Swagger 2.0的生成,并支持转换为OpenAPI 3.0.3。

安装

go install github.com/zxmfke/swagger2openapi3/cmd/swag2op@latest

执行

swag2op init

生成的swagger.json和swagger.yaml文档替换前面的即可

注意事项

  • 每次修改 API 注释后,需要重新运行swag init生成文档
  • 生产环境中建议限制 Swagger UI 的访问权限
  • 可以通过swag init -g main.go指定入口文件

更多 Swagger 注释选项,请参考: swag官网

现在你的 API 已经有了完整的文档,前端开发人员或 API 使用者可以通过 Swagger UI 直观地了解和测试所有 API 端点。`

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88365.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88365.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/88365.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动驾驶环境感知:天气数据采集与融合技术实战

天气与我们日常各类生活场景密不可分&#xff0c;在驾驶场景里当车主发动汽车准备驶向目的地时&#xff0c;窗外的阴晴或许只是直观感受&#xff0c;而真正影响驾驶安全与行程效率的&#xff0c;可能是几公里外的突发暴雨、桥面的结冰预警&#xff0c;或是前方路段的强侧风等级…

基于svga+uniapp的微信小程序动画组件开发指南

lottie动画指南 效果 概述 本项目使用 svgaplayer.weapp.js 库来实现 SVGA 动画播放功能&#xff0c;支持在微信小程序、H5 等多端环境下播放高质量的矢量动画。SVGA 是一种跨平台的开源动画格式&#xff0c;具有文件小、渲染性能高的特点。 技术栈 核心库: svgaplayer.wea…

数据结构与算法——计算直线的交点数

前言&#xff1a; 这是之前做的一道笔试题&#xff0c;当时没写出来烦恼很久&#xff0c;这次记录一下。 题目链接&#xff1a; Dotcpp--题目 1174: 计算直线的交点数 参考文章&#xff1a; CSDN--槐阳7--计算直线的交点数 题目&#xff1a; 解题思考&#xff1a; 在当时…

大模型及agent开发6 OpenAI Assistant API 高阶应用 - 流式输出功能

1.Assistant API 的主要优点&#xff1a; 减少编码工作量、自动管理上下文窗口、安全的访问控制、工具和文档的轻松集成 本节讲应用设计和性能流式输出&#xff1a;借助流式输出&#xff0c;可以让应用程序实时处理和响应用户输入。具体来说&#xff0c;这种技术允许数据在生成…

React Native安卓刘海屏适配终极方案:仅需修改 AndroidManifest.xml!

&#x1f4cc; 问题背景在 React Native 开发中&#xff0c;我们经常会遇到安卓设备刘海屏&#xff08;Notch&#xff09;适配问题。即使正确使用了 react-native-safe-area-context 和 react-navigation&#xff0c;在一些安卓设备&#xff08;如小米、华为、OPPO 等&#xff…

Spring Boot整合MyBatis+MySQL实战指南(Java 1.8 + 单元测试)

一、环境准备 开发工具&#xff1a;IntelliJ IDEA 2023.1 JDK 1.8.0_382 Maven3.6.3数据库&#xff1a;MySQL 8.0.21依赖版本&#xff1a;<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifact…

游戏开发日记

如何用数据表来储存&#xff0c;位置坐标&#xff08;XYZ&#xff09;&#xff1a;决定了对象在世界中的摆放资源ID / 图片URL&#xff1a;决定了使用什么模型或贴图事件ID / 特效&#xff1a;是否触发某些事件&#xff08;例如点击、交互&#xff09;逻辑索引&#xff08;Grid…

如何使用xmind编写测试用例

如何使用xmind编写测试用例为什么要使用xmind&#xff1f;使用xmind编写测试用例是为了梳理我们的思路。使用xmind编写测试用例的思路是什么&#xff1f;先进行分析再提取测试用例。 例如下面的注册功能的测试用例的分析&#xff1a; 分析&#xff1a; 先提取出需要测试的功能点…

使用LLaMA-Factory微调Qwen2.5-VL-3B 的目标检测任务-数据集格式转换(voc 转 ShareGPT)

一、LLaMA-Factory Qwen2.5-VL ShareGPT 格式要求ShareGPT 格式就是多轮对话的 list&#xff0c;每条数据如下&#xff1a;[{"conversations": [{"from": "user", "value": "<image>\n请标注图片中的所有目标及其类别和位…

【SkyWalking】服务端部署与微服务无侵入接入实战指南

【SkyWalking】服务端部署与微服务无侵入接入实战指南 &#x1f4a1; SkyWalking 系列总引导 在微服务架构快速演进的今天&#xff0c;如何有效实现服务链路追踪、性能分析、日志采集与自动化告警&#xff0c;成为系统稳定性的关键保障手段。 SkyWalking&#xff0c;作为 Apa…

LVDS系列20:Xilinx 7系ISERDESE2原语(一)

Xilinx 7系FPGA bank的io单元如下&#xff1a;Hr bank比hp bank少odelaye2组件&#xff0c;两者的idelaye2组件后面&#xff0c;都有iserdese2组件&#xff1b; iserdese2组件是一种专用的串并转换器或称解串器&#xff0c;用于高速源同步应用&#xff0c;如大部分LVDS信号解析…

【U-Boot】Shell指令

目录 U-Boot 三个Shell U-Boot Shell Linux Shell shell脚本 总结 U-Boot Shell命令 帮助命令 部分命令分类与功能说明 一、基础操作与信息查询 二、内存操作 三、启动管理 四、文件系统操作 五、设备与分区管理 六、环境变量 七、诊断与调试 八、特殊功能 九…

《Revisiting Generative Replay for Class Incremental Object Detection》阅读笔记

摘要Abstract部分 原文 Generative replay has gained significant attention in class-incremental learning; however, its application to Class Incremental Object Detection (CIOD) remains limited due to the challenges in generating complex images with precise …

Mysql: Bin log原理以及三种格式

目录 一、什么是 Binlog&#xff1f; 二、Binlog 的应用场景与案例 1. 数据恢复 (Point-in-Time Recovery) 2. 主从复制 (Master-Slave Replication) 3. 数据审计 三、Binlog 的三种格式 1. STATEMENT 模式 (Statement-Based Logging - SBL) 2. ROW 模式 (Row-Based Log…

LiteHub之文件下载与视频播放

文件下载 前端请求 箭头函数 //这个箭头函数可以形象理解为&#xff0c;x流入&#xff08;>&#xff09;x*x, //自然而然>前面的就是传入参数,>表示函数体 x > x * x//相当于 function (x) {return x * x; }//如果参数不是一个&#xff0c;就需要用括号()括起来…

QT5使用cmakelists引入Qt5Xlsx库并使用

1、首先需要已经有了Qt5Xlsx的头文件和库&#xff0c;并拷贝到程序exe路径下&#xff08;以xxx.exe/3rdparty/qtxlsx路径为例&#xff0c;Qt5Xlsx版本为0.3.0&#xff09;&#xff1b; 2、cmakelist中&#xff1a; # 设置 QtXlsx 路径 set(QTXLSX_ROOT_DIR ${CMAKE_CURRENT_SOU…

醋酸镨:闪亮的稀土宝藏,掀开科技应用新篇章

一、什么是醋酸镨醋酸镨是一种镨的有机盐&#xff0c;镨是稀土金属元素之一。作为一种重要的稀土化合物&#xff0c;醋酸镨通常以水合物的形式存在&#xff0c;呈现淡黄色或无色结晶。镨元素本身因其独特的物理化学特性&#xff0c;在工业和科技领域有着广泛应用&#xff0c;而…

深入解析JVM内存结构与垃圾回收机制

java是强类型高级语言JVM&#xff08;Java Virtual Machine&#xff0c;Java虚拟机&#xff09;是Java平台的核心组件&#xff0c;它是一个虚拟的计算机&#xff0c;能够执行Java字节码&#xff08;bytecode&#xff09;。1、区域划分JVM对Java内存的管理也是分区分块进行&…

Java 流程控制详解:从顺序执行到跳转语句,掌握程序逻辑设计

作为一名Java开发工程师&#xff0c;你一定知道&#xff0c;流程控制&#xff08;Flow Control&#xff09; 是编写任何程序的核心。它决定了代码的执行路径、分支走向和循环次数。本文将带你系统梳理 Java中的所有常用流程控制结构&#xff0c;包括&#xff1a;顺序结构分支结…

面试150 环形链表

思路 采用双指针法,slow指针每次走一步,fast指针每次走两步&#xff0c;如果相遇的情况下&#xff0c;slow指针回到开始的位置,此时快慢指针各走一步&#xff0c;当相遇的时候也就是说明链表中有环。 # Definition for singly-linked list. # class ListNode: # def __init…