# 开发指南 本文档提供 ZSP 后端系统的详细开发说明。 ## 开发环境设置 ### 1. 环境准备 #### 使用 Docker(推荐) ```bash # 1. 克隆项目 git clone cd zsp-backend # 2. 复制环境变量文件 cp .env.dev.example .env.dev # 3. 启动开发环境 docker-compose -f docker-compose.dev.yml up --build # 4. 查看日志 docker-compose -f docker-compose.dev.yml logs -f app ``` #### 本地开发 ```bash # 1. 安装 Go 1.25.4+ # 2. 安装 Air go install github.com/air-verse/air@latest # 3. 启动 MySQL docker run --name mysql-dev \ -e MYSQL_ROOT_PASSWORD=password \ -e MYSQL_DATABASE=myapp \ -p 3306:3306 \ -d mysql:8.0 # 4. 运行应用 air -c .air.toml ``` ### 2. 项目结构说明 ``` internal/ ├── handler/ # HTTP 请求处理器 ├── middleware/ # 中间件(认证、日志等) ├── model/ # 数据模型定义 ├── repository/ # 数据访问层 ├── service/ # 业务逻辑层 └── server/ # 服务器配置和路由 ``` ## 开发流程 ### 1. 添加新功能模块 #### 步骤 1: 创建数据模型 在 `internal/model/` 目录下创建新的模型文件: ```go // internal/model/product.go package model type Product struct { ID uint `gorm:"primarykey" json:"id"` Name string `gorm:"size:100" json:"name"` Description string `gorm:"size:255" json:"description"` Price int `json:"price"` // ... 其他字段 } ``` #### 步骤 2: 创建 Repository 在 `internal/repository/` 目录下创建数据访问层: ```go // internal/repository/product_repository.go package repository import ( "github.com/rovina/zsp-backend/internal/model" "gorm.io/gorm" ) type ProductRepository interface { Create(product *model.Product) error FindByID(id uint) (*model.Product, error) FindAll() ([]model.Product, error) Update(product *model.Product) error Delete(id uint) error } type productRepository struct { db *gorm.DB } func NewProductRepository(db *gorm.DB) ProductRepository { return &productRepository{db: db} } // 实现接口方法... ``` #### 步骤 3: 创建 Service 在 `internal/service/` 目录下创建业务逻辑层: ```go // internal/service/product_service.go package service import ( "github.com/rovina/zsp-backend/internal/model" "github.com/rovina/zsp-backend/internal/repository" ) type ProductService interface { CreateProduct(req *model.CreateProductRequest) (*model.Product, error) GetProduct(id uint) (*model.Product, error) ListProducts() ([]model.Product, error) } type productService struct { repo repository.ProductRepository } func NewProductService(repo repository.ProductRepository) ProductService { return &productService{repo: repo} } // 实现业务逻辑... ``` #### 步骤 4: 创建 Handler 在 `internal/handler/` 目录下创建 HTTP 处理器: ```go // internal/handler/product_handler.go package handler import ( "net/http" "github.com/gin-gonic/gin" "github.com/rovina/zsp-backend/internal/service" ) type ProductHandler struct { service service.ProductService } func NewProductHandler(service service.ProductService) *ProductHandler { return &ProductHandler{service: service} } func (h *ProductHandler) CreateProduct(c *gin.Context) { var req model.CreateProductRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } product, err := h.service.CreateProduct(&req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, product) } // 其他处理器方法... ``` #### 步骤 5: 注册路由 在 `internal/server/router.go` 中添加路由: ```go // internal/server/router.go func SetupRouter(userHandler *handler.UserHandler, contractHandler *handler.ContractHandler, productHandler *handler.ProductHandler, // 新增 authMiddleware gin.HandlerFunc) *gin.Engine { router := gin.Default() // 公共路由 public := router.Group("/api/v1") { public.POST("/users/register", userHandler.Register) public.POST("/users/login", userHandler.Login) } // 需要认证的路由 authorized := router.Group("/api/v1") authorized.Use(authMiddleware) { // 用户相关 authorized.GET("/users/profile", userHandler.GetProfile) // 合同相关 authorized.POST("/contracts", contractHandler.CreateContract) authorized.GET("/contracts", contractHandler.GetContracts) // 产品相关(新增) authorized.POST("/products", productHandler.CreateProduct) authorized.GET("/products", productHandler.GetProducts) authorized.GET("/products/:id", productHandler.GetProduct) authorized.PUT("/products/:id", productHandler.UpdateProduct) authorized.DELETE("/products/:id", productHandler.DeleteProduct) } return router } ``` #### 步骤 6: 在主程序中初始化 在 `cmd/api/main.go` 中初始化新的组件: ```go // cmd/api/main.go func main() { // ... 现有代码 // 初始化新的 Repository 和 Service productRepo := repository.NewProductRepository(db) productService, err := service.NewProductService(productRepo) if err != nil { log.Fatalf("error in productService Create: %s", err.Error()) return } // 创建 Handler productHandler := handler.NewProductHandler(productService) // 更新路由设置 router := server.SetupRouter(userHandler, contractHandler, productHandler, authMiddleware) // ... 剩余代码 } ``` ### 2. 数据库迁移 #### 自动迁移 在开发环境中,设置 `reset_database: true` 会自动执行 GORM 的 AutoMigrate。 #### 手动创建迁移文件 ```bash # 创建迁移目录 mkdir -p migrations # 创建迁移文件 cat > migrations/001_create_products_table.sql << 'EOF' CREATE TABLE IF NOT EXISTS products ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, description VARCHAR(255), price INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; EOF ``` ### 3. 测试开发 #### 单元测试 ```go // internal/service/product_service_test.go package service import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // 创建 Mock Repository type mockProductRepository struct { mock.Mock } func (m *mockProductRepository) Create(product *model.Product) error { args := m.Called(product) return args.Error(0) } func TestProductService_CreateProduct(t *testing.T) { // 测试用例... } ``` #### API 测试 使用 curl 或 Postman 测试 API: ```bash # 测试用户登录 curl -X POST http://localhost:8080/api/v1/users/login \ -H "Content-Type: application/json" \ -d '{"email":"test@example.com","password":"password123"}' # 测试创建合同(需要认证) curl -X POST http://localhost:8080/api/v1/contracts \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "name": "测试合同", "contract_type": "采购", "contract_code": "TEST001" }' ``` ## 代码规范 ### 1. 命名约定 - 包名:小写,单数形式 - 接口名:以 "er" 结尾(如 Repository, Handler) - 结构体名:首字母大写 - 变量名:驼峰式 ### 2. 错误处理 ```go // 正确:返回错误 func GetUser(id uint) (*model.User, error) { user, err := repo.FindByID(id) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } return user, nil } // 错误:panic 或忽略错误 func BadExample() { user, _ := repo.FindByID(1) // 不要忽略错误 // 或 panic("something went wrong") // 不要使用 panic } ``` ### 3. 日志记录 ```go import "log" // 使用结构化日志 log.Printf("User %d logged in", userID) log.Printf("Failed to create contract: %v", err) // 在生产环境中考虑使用更高级的日志库 ``` ## 调试技巧 ### 1. 使用 Air 热重载 Air 会自动检测文件变化并重新编译。修改代码后无需手动重启。 ### 2. 数据库调试 ```bash # 进入 MySQL 容器 docker exec -it zsp-backend-mysql-1 mysql -uroot -ppassword myapp # 查看表结构 DESCRIBE users; DESCRIBE contracts; # 查询数据 SELECT * FROM users; SELECT * FROM contracts; ``` ### 3. 查看应用日志 ```bash # Docker 环境 docker-compose -f docker-compose.dev.yml logs -f app # 查看特定服务的日志 docker-compose -f docker-compose.dev.yml logs app --tail=100 # 本地环境 tail -f tmp/air.log ``` ## 常见问题 ### 1. 数据库连接失败 **问题**: `Failed to connect to database` **解决**: 1. 检查 MySQL 容器是否运行: `docker ps | grep mysql` 2. 检查连接配置: `configs/config.dev.yaml` 3. 检查网络: `docker network ls` ### 2. 热重载不工作 **问题**: Air 不检测文件变化 **解决**: 1. 检查 `.air.toml` 配置 2. 确保文件在挂载的卷中 3. 重启 Air: `docker-compose restart app` ### 3. 权限问题 **问题**: 文件写入权限错误 **解决**: ```bash # 在容器内运行 docker exec -it zsp-backend-app-1 chmod -R 755 /workspace/uploads ``` ## 性能优化建议 ### 1. 数据库优化 - 为常用查询字段添加索引 - 使用连接池配置 - 避免 N+1 查询问题 ### 2. API 优化 - 实现分页查询 - 使用缓存(Redis) - 压缩响应数据 ### 3. 内存优化 - 及时关闭数据库连接 - 使用连接池 - 避免内存泄漏 ## 下一步 1. ✅ 完成基础框架搭建 2. ✅ 实现用户认证模块 3. ✅ 实现合同管理模块 4. 🔄 添加文件上传功能 5. 🔄 实现权限控制(RBAC) 6. 🔄 添加 API 文档(Swagger) 7. 🔄 实现监控和日志收集 8. 🔄 编写完整的测试套件