diff --git a/AGENTS.md b/AGENTS.md index fa4f0cd..e71497d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,12 +38,51 @@ swag init -g main.go # Alternative command ## Code Style Guidelines +## Code Style Guidelines + ### Project Structure -- **controllers/**: HTTP request handlers, use helper functions (GetCurrentUser, BindJSON, ErrorResponse, SuccessResponse) -- **services/**: Business logic layer, return errors to controllers -- **models/**: Data models with JSON and GORM tags, DTOs for API -- **middleware/**: JWT authentication and admin role checks -- **database/**: SQLite/PostgreSQL connection and migrations +``` +backend-go/ +├── config/ # Configuration management +│ └── config.go # Config loading (.env, config.yaml, env vars) +├── controllers/ # HTTP request handlers +│ ├── auth_controller.go # Auth: login, profile, password change +│ ├── companies_controller.go # Company CRUD +│ ├── employees_controller.go # Employee serials: generate, query, update, revoke, qrcode +│ ├── helper.go # Helper functions (GetCurrentUser, BindJSON, Response) +│ └── serials_controller.go # Company serials: generate, query, update, revoke, qrcode +├── database/ # Database connection and migrations +│ └── database.go # GORM init, AutoMigrate +├── docs/ # Swagger documentation (auto-generated) +├── logger/ # Structured logging +│ └── logger.go # Zap logger wrapper +├── middleware/ # Middleware +│ └── auth.go # JWT auth, Admin permission check +├── models/ # Data models and DTOs +│ └── models.go # User, Company, Serial, EmployeeSerial and DTOs +├── routes/ # Route configuration +│ └── routes.go # API route registration +├── services/ # Business logic layer +│ ├── auth_service.go # Auth: validate user, generate token, password management +│ ├── companies_service.go # Company CRUD +│ ├── employees_service.go # Employee serials: generate, query, update, revoke, qrcode +│ ├── serials_service.go # Company serials: generate, query, update, revoke, qrcode +│ └── services_test.go # Unit tests +├── tests/ # Integration tests +│ └── main_test.go # End-to-end tests +├── data/ # SQLite data directory +├── main.go # Application entry point +├── go.mod # Go dependencies +├── Makefile # Build tasks +└── .env.example # Environment variable example +``` + +### Layer Responsibilities +- **controllers/**: Receive HTTP requests, validate params, call services, return responses +- **services/**: Business logic, data processing, database interaction, return results or errors +- **models/**: Data structure definitions, GORM models, API request/response DTOs +- **middleware/**: Authentication and authorization +- **routes/**: Route registration, connect controllers to router ### Import Organization Standard imports followed by third-party imports, then project imports (sorted alphabetically): @@ -61,8 +100,8 @@ import ( ``` ### Naming Conventions -- **Controllers**: `AuthController`, `SerialsController`, `CompaniesController` -- **Services**: `AuthService`, `SerialsService`, `CompaniesService` +- **Controllers**: `AuthController`, `SerialsController`, `CompaniesController`, `EmployeeSerialsController` +- **Services**: `AuthService`, `SerialsService`, `CompaniesService`, `EmployeeSerialsService` - **Models**: `User`, `Company`, `Serial` (use PascalCase for exported structs) - **DTOs**: `LoginDTO`, `ChangePasswordDTO`, `UpdateProfileDTO` (DTO suffix) - **Functions**: `ValidateUser`, `Generate`, `Query` (PascalCase for exported) diff --git a/README.md b/README.md index 7f89384..bf232fb 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ backend-go/ ├── controllers/ # 控制器层,处理 HTTP 请求 │ ├── auth_controller.go # 认证相关接口 │ ├── companies_controller.go # 企业管理接口 +│ ├── employees_controller.go # 员工赋码接口 │ ├── helper.go # 控制器通用辅助函数 │ └── serials_controller.go # 序列号管理接口 ├── database/ # 数据库连接和操作 @@ -50,6 +51,7 @@ backend-go/ ├── services/ # 业务逻辑层 │ ├── auth_service.go # 认证业务逻辑 │ ├── companies_service.go # 企业管理业务逻辑 +│ ├── employees_service.go # 员工赋码业务逻辑 │ ├── serials_service.go # 序列号业务逻辑 │ └── services_test.go # 服务层单元测试 ├── tests/ # 集成测试 @@ -253,6 +255,22 @@ swag init -g main.go | PUT | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 | | DELETE | `/api/companies/:companyName` | 删除企业 | 是 | 管理员 | +### 员工赋码 + +| 方法 | 路径 | 描述 | 需要认证 | 角色 | +| ---- | -------------------------------------- | ------------------ | -------- | ------ | +| POST | `/api/employee-serials/generate` | 生成员工序列号 | 是 | 管理员 | +| GET | `/api/employee-serials` | 获取员工序列号列表 | 是 | 任何 | +| GET | `/api/employee-serials/:serial/query` | 查询员工序列号信息 | 否 | 任何 | +| POST | `/api/employee-serials/:serial/qrcode` | 生成员工二维码 | 是 | 任何 | +| PUT | `/api/employee-serials/:serial` | 更新员工序列号信息 | 是 | 管理员 | +| POST | `/api/employee-serials/:serial/revoke` | 吊销员工序列号 | 是 | 管理员 | + +**员工序列号特点**: +- 无有效期限制(与企业赋码不同) +- 包含部门(department)和员工姓名(employeeName)信息 +- 序列号格式: `EMP26xxxxxx`(EMP + 年份后两位 + 6位随机字符) + ## 测试 ### 运行所有测试 @@ -278,9 +296,10 @@ go tool cover -html=coverage.out ### 当前测试覆盖 -- **services/**: 包含 AuthService 和 SerialsService 的完整单元测试 +- **services/**: 包含 AuthService、SerialsService 和 EmployeeSerialsService 的完整单元测试 - 用户认证测试(登录、获取用户信息、修改密码、更新资料) - 序列号管理测试(生成、查询、更新、吊销、分页列表) + - 员工赋码测试(生成、查询、更新、吊销、二维码生成) - **tests/**: 集成测试(健康检查、登录流程) ## 代码检查 diff --git a/controllers/employees_controller.go b/controllers/employees_controller.go new file mode 100644 index 0000000..c582161 --- /dev/null +++ b/controllers/employees_controller.go @@ -0,0 +1,228 @@ +package controllers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "git.beifan.cn/trace-system/backend-go/models" + "git.beifan.cn/trace-system/backend-go/services" +) + +// EmployeeSerialsController 员工序列号控制器 +type EmployeeSerialsController struct { + employeeSerialsService services.EmployeeSerialsService +} + +// NewEmployeeSerialsController 创建员工序列号控制器实例 +func NewEmployeeSerialsController() *EmployeeSerialsController { + return &EmployeeSerialsController{ + employeeSerialsService: services.EmployeeSerialsService{}, + } +} + +// Generate 生成员工序列号 +// @Summary 生成员工序列号 +// @Description 生成指定数量的员工序列号(无有效期) +// @Tags 员工赋码管理 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param generateData body models.GenerateEmployeeSerialDTO true "生成数据" +// @Success 200 {object} models.DataResponse +// @Failure 400 {object} models.ErrorResponse +// @Failure 401 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +// @Router /employee-serials/generate [post] +func (c *EmployeeSerialsController) Generate(ctx *gin.Context) { + userModel, ok := GetCurrentUser(ctx) + if !ok { + return + } + + var generateData models.GenerateEmployeeSerialDTO + if !BindJSON(ctx, &generateData) { + return + } + + serials, err := c.employeeSerialsService.Generate( + generateData.CompanyName, + generateData.Department, + generateData.EmployeeName, + generateData.Quantity, + userModel.ID, + ) + if err != nil { + ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) + return + } + + SuccessResponse(ctx, "成功生成 "+strconv.Itoa(len(serials))+" 个员工序列号", gin.H{ + "serials": serials, + }) +} + +// GenerateQRCode 生成员工二维码 +// @Summary 生成员工二维码 +// @Description 为指定员工序列号生成查询二维码 +// @Tags 员工赋码管理 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param serialNumber path string true "序列号" +// @Param qrCodeData body models.QRCodeDTO false "二维码数据" +// @Success 200 {object} models.QRCodeResponse +// @Failure 400 {object} models.ErrorResponse +// @Failure 401 {object} models.ErrorResponse +// @Failure 404 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +// @Router /employee-serials/{serialNumber}/qrcode [post] +func (c *EmployeeSerialsController) GenerateQRCode(ctx *gin.Context) { + serialNumber := ctx.Param("serialNumber") + + var qrCodeData models.QRCodeDTO + if !BindJSON(ctx, &qrCodeData) { + return + } + + protocol := "http" + if ctx.Request.TLS != nil { + protocol = "https" + } + + qrCodeBase64, queryUrl, err := c.employeeSerialsService.GenerateQRCode( + serialNumber, + qrCodeData.BaseUrl, + ctx.Request.Host, + protocol, + ) + if err != nil { + ErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + + SuccessResponse(ctx, "员工二维码生成成功", gin.H{ + "qrCodeData": qrCodeBase64, + "queryUrl": queryUrl, + }) +} + +// Query 查询员工序列号信息 +// @Summary 查询员工序列号信息 +// @Description 查询指定员工序列号的详细信息 +// @Tags 员工赋码查询 +// @Produce json +// @Param serialNumber path string true "序列号" +// @Success 200 {object} models.DataResponse +// @Failure 400 {object} models.ErrorResponse +// @Failure 404 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +// @Router /employee-serials/{serialNumber}/query [get] +func (c *EmployeeSerialsController) Query(ctx *gin.Context) { + serialNumber := ctx.Param("serialNumber") + + serial, err := c.employeeSerialsService.Query(serialNumber) + if err != nil { + ErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + + SuccessResponse(ctx, "查询成功", gin.H{ + "serial": serial, + }) +} + +// FindAll 获取员工序列号列表 +// @Summary 获取员工序列号列表 +// @Description 获取员工序列号列表,支持分页和搜索 +// @Tags 员工赋码管理 +// @Produce json +// @Security BearerAuth +// @Param page query int false "页码" +// @Param limit query int false "每页数量" +// @Param search query string false "搜索关键词" +// @Success 200 {object} models.PaginationResponse +// @Failure 401 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +// @Router /employee-serials [get] +func (c *EmployeeSerialsController) FindAll(ctx *gin.Context) { + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20")) + search := ctx.DefaultQuery("search", "") + + serials, total, totalPages, err := c.employeeSerialsService.FindAll(page, limit, search) + if err != nil { + ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) + return + } + + SuccessResponse(ctx, "获取员工序列号列表成功", gin.H{ + "data": serials, + "pagination": gin.H{ + "page": page, + "limit": limit, + "total": total, + "totalPages": totalPages, + }, + }) +} + +// Update 更新员工序列号信息 +// @Summary 更新员工序列号信息 +// @Description 更新指定员工序列号的信息 +// @Tags 员工赋码管理 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param serialNumber path string true "序列号" +// @Param updateData body models.UpdateEmployeeSerialDTO true "更新数据" +// @Success 200 {object} models.DataResponse +// @Failure 400 {object} models.ErrorResponse +// @Failure 401 {object} models.ErrorResponse +// @Failure 404 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +// @Router /employee-serials/{serialNumber} [put] +func (c *EmployeeSerialsController) Update(ctx *gin.Context) { + serialNumber := ctx.Param("serialNumber") + + var updateData models.UpdateEmployeeSerialDTO + if !BindJSON(ctx, &updateData) { + return + } + + serial, err := c.employeeSerialsService.Update(serialNumber, updateData) + if err != nil { + ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) + return + } + + SuccessResponse(ctx, "员工序列号信息更新成功", gin.H{ + "serial": serial, + }) +} + +// Revoke 吊销员工序列号 +// @Summary 吊销员工序列号 +// @Description 吊销指定员工序列号 +// @Tags 员工赋码管理 +// @Produce json +// @Security BearerAuth +// @Param serialNumber path string true "序列号" +// @Success 200 {object} models.BaseResponse +// @Failure 400 {object} models.ErrorResponse +// @Failure 401 {object} models.ErrorResponse +// @Failure 404 {object} models.ErrorResponse +// @Failure 500 {object} models.ErrorResponse +// @Router /employee-serials/{serialNumber}/revoke [post] +func (c *EmployeeSerialsController) Revoke(ctx *gin.Context) { + serialNumber := ctx.Param("serialNumber") + + err := c.employeeSerialsService.Revoke(serialNumber) + if err != nil { + ErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + + SuccessResponse(ctx, "员工序列号吊销成功") +} diff --git a/database/database.go b/database/database.go index b6111ca..8c1a31a 100644 --- a/database/database.go +++ b/database/database.go @@ -112,6 +112,7 @@ func AutoMigrate() { &models.User{}, &models.Company{}, &models.Serial{}, + &models.EmployeeSerial{}, ); err != nil { logger.Fatal("数据库迁移失败", logger.Err(err)) } diff --git a/go.mod b/go.mod index a7c9bcd..e04c1af 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.beifan.cn/trace-system/backend-go -go 1.26 +go 1.25 require ( github.com/gin-contrib/cors v1.7.6 diff --git a/models/models.go b/models/models.go index c1bf6e2..e3c9b58 100644 --- a/models/models.go +++ b/models/models.go @@ -163,3 +163,35 @@ type CompanyUpdateRequest struct { CompanyName string `json:"companyName"` IsActive *bool `json:"isActive"` } + +// EmployeeSerial 员工序列号模型 +type EmployeeSerial struct { + ID uint `gorm:"primaryKey" json:"id"` + SerialNumber string `gorm:"uniqueIndex;size:255" json:"serialNumber"` + CompanyName string `gorm:"index;size:255" json:"companyName"` + Department string `gorm:"size:255" json:"department"` + EmployeeName string `gorm:"size:255" json:"employeeName"` + IsActive bool `gorm:"default:true" json:"isActive"` + CreatedBy *uint `json:"createdBy"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + User *User `gorm:"foreignKey:CreatedBy" json:"user,omitempty"` + Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName" json:"company,omitempty"` +} + +// GenerateEmployeeSerialDTO 生成员工序列号请求数据 +type GenerateEmployeeSerialDTO struct { + CompanyName string `json:"companyName" validate:"required"` + Department string `json:"department" validate:"required"` + EmployeeName string `json:"employeeName" validate:"required"` + Quantity int `json:"quantity" validate:"min=1,max=1000"` +} + +// UpdateEmployeeSerialDTO 员工序列号更新请求数据 +type UpdateEmployeeSerialDTO struct { + CompanyName string `json:"companyName,omitempty" validate:"omitempty"` + Department string `json:"department,omitempty" validate:"omitempty"` + EmployeeName string `json:"employeeName,omitempty" validate:"omitempty"` + IsActive *bool `json:"isActive,omitempty"` +} diff --git a/routes/routes.go b/routes/routes.go index 093c9f1..8923977 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -52,4 +52,16 @@ func SetupAPIRoutes(r *gin.RouterGroup) { companiesRoutes.PUT("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update) companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete) } + + // 员工赋码路由 + employeeSerialsController := controllers.NewEmployeeSerialsController() + employeeSerialsRoutes := r.Group("/employee-serials") + { + employeeSerialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Generate) + employeeSerialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), employeeSerialsController.GenerateQRCode) + employeeSerialsRoutes.GET("/:serialNumber/query", employeeSerialsController.Query) + employeeSerialsRoutes.GET("/", middleware.JWTAuthMiddleware(), employeeSerialsController.FindAll) + employeeSerialsRoutes.PUT("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Update) + employeeSerialsRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Revoke) + } } diff --git a/services/employees_service.go b/services/employees_service.go new file mode 100644 index 0000000..7eaef27 --- /dev/null +++ b/services/employees_service.go @@ -0,0 +1,260 @@ +package services + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/google/uuid" + qr "github.com/yeqown/go-qrcode/v2" + "github.com/yeqown/go-qrcode/writer/standard" + + "git.beifan.cn/trace-system/backend-go/database" + "git.beifan.cn/trace-system/backend-go/models" +) + +// EmployeeSerialsService 员工序列号服务 +type EmployeeSerialsService struct{} + +// Generate 生成员工序列号 +func (s *EmployeeSerialsService) Generate( + companyName string, + department string, + employeeName string, + quantity int, + userId uint, +) ([]models.EmployeeSerial, error) { + var serials []models.EmployeeSerial + + // 检查公司是否存在,不存在则创建 + var company models.Company + result := database.DB.Where("company_name = ?", companyName).First(&company) + if result.Error != nil { + company = models.Company{ + CompanyName: companyName, + IsActive: true, + } + result = database.DB.Create(&company) + if result.Error != nil { + return nil, fmt.Errorf("创建公司失败: %w", result.Error) + } + } + + // 生成序列号前缀 (EMP + 年份后两位) + serialPrefix := fmt.Sprintf("EMP%d", time.Now().Year()%100) + + // 预生成所有序列号 + serialNumbers := make(map[string]bool) + for i := 0; i < quantity; { + randomBytes := make([]byte, 3) + if _, err := rand.Read(randomBytes); err != nil { + return nil, fmt.Errorf("生成随机数失败: %w", err) + } + randomPart := hex.EncodeToString(randomBytes)[:6] + serialNumber := fmt.Sprintf("%s%s", serialPrefix, randomPart) + + if serialNumbers[serialNumber] { + continue + } + + var existingSerial models.EmployeeSerial + checkResult := database.DB.Where("serial_number = ?", serialNumber).First(&existingSerial) + if checkResult.Error != nil { + serialNumbers[serialNumber] = true + i++ + } + } + + for serialNumber := range serialNumbers { + serial := models.EmployeeSerial{ + SerialNumber: strings.ToUpper(serialNumber), + CompanyName: companyName, + Department: department, + EmployeeName: employeeName, + CreatedBy: &userId, + IsActive: true, + } + serials = append(serials, serial) + } + + // 保存到数据库 + result = database.DB.Create(&serials) + if result.Error != nil { + return nil, fmt.Errorf("保存员工序列号失败: %w", result.Error) + } + + return serials, nil +} + +// Query 查询员工序列号信息 +func (s *EmployeeSerialsService) Query(serialNumber string) (*models.EmployeeSerial, error) { + var serial models.EmployeeSerial + result := database.DB.Preload("User").Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) + if result.Error != nil { + return nil, fmt.Errorf("查询员工序列号失败: %w", errors.New("序列号不存在")) + } + + return &serial, nil +} + +// FindAll 获取员工序列号列表 +func (s *EmployeeSerialsService) FindAll(page int, limit int, search string) ([]models.EmployeeSerial, int, int, error) { + var serials []models.EmployeeSerial + var total int64 + + offset := (page - 1) * limit + db := database.DB.Preload("User") + + // 搜索条件 + if search != "" { + db = db.Where("serial_number LIKE ? OR company_name LIKE ? OR department LIKE ? OR employee_name LIKE ?", + "%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%") + } + + // 获取总数 + countQuery := db.Model(&models.EmployeeSerial{}) + if search != "" { + countQuery = countQuery.Where("serial_number LIKE ? OR company_name LIKE ? OR department LIKE ? OR employee_name LIKE ?", + "%"+search+"%", "%"+search+"%", "%"+search+"%", "%"+search+"%") + } + countQuery.Count(&total) + + // 分页查询 + result := db.Model(&models.EmployeeSerial{}).Order("created_at DESC").Offset(offset).Limit(limit).Find(&serials) + if result.Error != nil { + return nil, 0, 0, fmt.Errorf("查询员工序列号列表失败: %w", result.Error) + } + + totalPages := (int(total) + limit - 1) / limit + + return serials, int(total), totalPages, nil +} + +// Update 更新员工序列号信息 +func (s *EmployeeSerialsService) Update(serialNumber string, updateData models.UpdateEmployeeSerialDTO) (*models.EmployeeSerial, error) { + var serial models.EmployeeSerial + result := database.DB.Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) + if result.Error != nil { + return nil, fmt.Errorf("查询员工序列号失败: %w", errors.New("序列号不存在")) + } + + if updateData.CompanyName != "" { + // 检查公司是否存在 + var company models.Company + companyResult := database.DB.Where("company_name = ?", updateData.CompanyName).First(&company) + if companyResult.Error != nil { + company = models.Company{ + CompanyName: updateData.CompanyName, + IsActive: true, + } + database.DB.Create(&company) + } + + serial.CompanyName = updateData.CompanyName + } + + if updateData.Department != "" { + serial.Department = updateData.Department + } + + if updateData.EmployeeName != "" { + serial.EmployeeName = updateData.EmployeeName + } + + if updateData.IsActive != nil { + serial.IsActive = *updateData.IsActive + } + + result = database.DB.Save(&serial) + if result.Error != nil { + return nil, fmt.Errorf("更新员工序列号失败: %w", result.Error) + } + + return &serial, nil +} + +// Revoke 吊销员工序列号 +func (s *EmployeeSerialsService) Revoke(serialNumber string) error { + var serial models.EmployeeSerial + result := database.DB.Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) + if result.Error != nil { + return fmt.Errorf("查询员工序列号失败: %w", errors.New("序列号不存在")) + } + + if !serial.IsActive { + return fmt.Errorf("员工序列号状态无效: %w", errors.New("序列号已被吊销")) + } + + serial.IsActive = false + result = database.DB.Save(&serial) + if result.Error != nil { + return fmt.Errorf("吊销员工序列号失败: %w", result.Error) + } + + return nil +} + +// GenerateQRCode 生成员工二维码 +func (s *EmployeeSerialsService) GenerateQRCode( + serialNumber string, + baseUrl string, + requestHost string, + protocol string, +) (string, string, error) { + var serial models.EmployeeSerial + result := database.DB.Preload("User").Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) + if result.Error != nil { + return "", "", fmt.Errorf("查询员工序列号失败: %w", errors.New("序列号不存在")) + } + + if !serial.IsActive { + return "", "", fmt.Errorf("员工序列号状态无效: %w", errors.New("序列号已被禁用")) + } + + // 确定查询 URL + if baseUrl == "" { + baseUrl = fmt.Sprintf("%s://%s/query.html", protocol, requestHost) + } + + var queryURL string + if strings.Contains(baseUrl, "?") { + queryURL = fmt.Sprintf("%s&serial=%s", baseUrl, serial.SerialNumber) + } else { + queryURL = fmt.Sprintf("%s?serial=%s", baseUrl, serial.SerialNumber) + } + + // 生成二维码到临时文件 + filePath := fmt.Sprintf("temp_qr_%s.png", uuid.New().String()) + writer, err := standard.New(filePath, standard.WithQRWidth(6)) + if err != nil { + return "", "", fmt.Errorf("二维码写入器创建失败: %w", err) + } + + qrc, errCode := qr.New(queryURL) + if errCode != nil { + os.Remove(filePath) + return "", "", fmt.Errorf("二维码创建失败: %w", errCode) + } + + if errSave := qrc.Save(writer); errSave != nil { + os.Remove(filePath) + return "", "", fmt.Errorf("二维码保存失败: %w", errSave) + } + + // 读取文件并转换为 Base64 + fileBytes, err := os.ReadFile(filePath) + if err != nil { + os.Remove(filePath) + return "", "", fmt.Errorf("读取二维码文件失败: %w", err) + } + os.Remove(filePath) + + qrCodeBase64 := base64.StdEncoding.EncodeToString(fileBytes) + + return qrCodeBase64, queryURL, nil +} diff --git a/services/services_test.go b/services/services_test.go index e080213..b4b1055 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -31,12 +31,14 @@ func TestMain(m *testing.M) { database.DB.Unscoped().Where("1 = 1").Delete(&models.User{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{}) + database.DB.Unscoped().Where("1 = 1").Delete(&models.EmployeeSerial{}) exitCode := m.Run() database.DB.Unscoped().Where("1 = 1").Delete(&models.User{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{}) + database.DB.Unscoped().Where("1 = 1").Delete(&models.EmployeeSerial{}) os.Exit(exitCode) } @@ -411,3 +413,363 @@ func TestSerialsService_Update_Success(t *testing.T) { database.DB.Unscoped().Where("company_name = ?", "UpdateCompany").Delete(&models.Company{}) database.DB.Unscoped().Delete(&user) } + +// ==================== EmployeeSerialsService 测试 ==================== + +func TestEmployeeSerialsService_Generate_Success(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin1", + Password: string(password), + Name: "员工管理员1", + Email: "empadmin1@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, err := service.Generate("EmpTestCompany", "技术部", "张三", 5, user.ID) + + assert.NoError(t, err) + assert.Len(t, serials, 5) + for _, serial := range serials { + assert.Equal(t, "EmpTestCompany", serial.CompanyName) + assert.Equal(t, "技术部", serial.Department) + assert.Equal(t, "张三", serial.EmployeeName) + assert.True(t, serial.IsActive) + assert.True(t, strings.HasPrefix(serial.SerialNumber, "EMP")) + database.DB.Unscoped().Delete(&serial) + } + + database.DB.Unscoped().Where("company_name = ?", "EmpTestCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_Generate_CreateNewCompany(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin2", + Password: string(password), + Name: "员工管理员2", + Email: "empadmin2@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, err := service.Generate("NewEmpCompany", "市场部", "李四", 3, user.ID) + + assert.NoError(t, err) + assert.Len(t, serials, 3) + + var company models.Company + result := database.DB.Where("company_name = ?", "NewEmpCompany").First(&company) + assert.NoError(t, result.Error) + assert.Equal(t, "NewEmpCompany", company.CompanyName) + assert.True(t, company.IsActive) + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "NewEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_Query_Success(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin3", + Password: string(password), + Name: "员工管理员3", + Email: "empadmin3@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("QueryEmpCompany", "财务部", "王五", 1, user.ID) + + serialNumber := serials[0].SerialNumber + result, err := service.Query(serialNumber) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, strings.ToUpper(serialNumber), strings.ToUpper(result.SerialNumber)) + assert.Equal(t, "QueryEmpCompany", result.CompanyName) + assert.Equal(t, "财务部", result.Department) + assert.Equal(t, "王五", result.EmployeeName) + assert.True(t, result.IsActive) + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "QueryEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_Query_NotFound(t *testing.T) { + service := EmployeeSerialsService{} + _, err := service.Query("NONEXISTENTEMP") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "序列号不存在") +} + +func TestEmployeeSerialsService_FindAll_Success(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin4", + Password: string(password), + Name: "员工管理员4", + Email: "empadmin4@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("ListEmpCompany", "人事部", "赵六", 10, user.ID) + + result, total, totalPages, err := service.FindAll(1, 5, "") + + assert.NoError(t, err) + assert.Len(t, result, 5) + assert.GreaterOrEqual(t, total, 10) + assert.Greater(t, totalPages, 0) + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "ListEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_FindAll_WithSearch(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin5", + Password: string(password), + Name: "员工管理员5", + Email: "empadmin5@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("SearchEmpCompany", "研发部", "钱七", 5, user.ID) + + result, _, _, err := service.FindAll(1, 10, "SearchEmpCompany") + + assert.NoError(t, err) + assert.Greater(t, len(result), 0) + assert.Equal(t, "SearchEmpCompany", result[0].CompanyName) + + result2, _, _, err2 := service.FindAll(1, 10, "研发部") + assert.NoError(t, err2) + assert.Greater(t, len(result2), 0) + + result3, _, _, err3 := service.FindAll(1, 10, "钱七") + assert.NoError(t, err3) + assert.Greater(t, len(result3), 0) + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "SearchEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_Update_Success(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin6", + Password: string(password), + Name: "员工管理员6", + Email: "empadmin6@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("UpdateEmpCompany", "运营部", "孙八", 1, user.ID) + + serialNumber := serials[0].SerialNumber + isActive := false + + updateData := models.UpdateEmployeeSerialDTO{ + CompanyName: "UpdatedEmpCompany", + Department: "新部门", + EmployeeName: "新名字", + IsActive: &isActive, + } + + result, err := service.Update(serialNumber, updateData) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "UpdatedEmpCompany", result.CompanyName) + assert.Equal(t, "新部门", result.Department) + assert.Equal(t, "新名字", result.EmployeeName) + assert.False(t, result.IsActive) + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "UpdateEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Where("company_name = ?", "UpdatedEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_Update_NotFound(t *testing.T) { + service := EmployeeSerialsService{} + updateData := models.UpdateEmployeeSerialDTO{ + CompanyName: "TestCompany", + } + + _, err := service.Update("NONEXISTENT", updateData) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "序列号不存在") +} + +func TestEmployeeSerialsService_Revoke_Success(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin7", + Password: string(password), + Name: "员工管理员7", + Email: "empadmin7@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("RevokeEmpCompany", "测试部", "周九", 1, user.ID) + + serialNumber := serials[0].SerialNumber + err := service.Revoke(serialNumber) + + assert.NoError(t, err) + + var revokedSerial models.EmployeeSerial + database.DB.Where("serial_number = ?", serialNumber).First(&revokedSerial) + assert.False(t, revokedSerial.IsActive) + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "RevokeEmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_Revoke_NotFound(t *testing.T) { + service := EmployeeSerialsService{} + err := service.Revoke("NONEXISTENT") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "序列号不存在") +} + +func TestEmployeeSerialsService_Revoke_AlreadyRevoked(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin8", + Password: string(password), + Name: "员工管理员8", + Email: "empadmin8@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("RevokeEmpCompany2", "行政部", "吴十", 1, user.ID) + + serialNumber := serials[0].SerialNumber + service.Revoke(serialNumber) + err := service.Revoke(serialNumber) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "已被吊销") + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "RevokeEmpCompany2").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_GenerateQRCode_Success(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin9", + Password: string(password), + Name: "员工管理员9", + Email: "empadmin9@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("QREmpCompany", "产品部", "郑十一", 1, user.ID) + + serialNumber := serials[0].SerialNumber + qrCodeBase64, queryUrl, err := service.GenerateQRCode(serialNumber, "", "localhost:3000", "http") + + assert.NoError(t, err) + assert.NotEmpty(t, qrCodeBase64) + assert.NotEmpty(t, queryUrl) + assert.Contains(t, queryUrl, serialNumber) + assert.Contains(t, queryUrl, "query.html") + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "QREmpCompany").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +} + +func TestEmployeeSerialsService_GenerateQRCode_NotFound(t *testing.T) { + service := EmployeeSerialsService{} + _, _, err := service.GenerateQRCode("NONEXISTENT", "", "localhost:3000", "http") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "序列号不存在") +} + +func TestEmployeeSerialsService_GenerateQRCode_Inactive(t *testing.T) { + var user models.User + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user = models.User{ + Username: "empadmin10", + Password: string(password), + Name: "员工管理员10", + Email: "empadmin10@example.com", + Role: "admin", + } + database.DB.Create(&user) + + service := EmployeeSerialsService{} + serials, _ := service.Generate("QREmpCompany2", "设计部", "王十二", 1, user.ID) + + serialNumber := serials[0].SerialNumber + service.Revoke(serialNumber) + + _, _, err := service.GenerateQRCode(serialNumber, "", "localhost:3000", "http") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "已被禁用") + + for _, serial := range serials { + database.DB.Unscoped().Delete(&serial) + } + database.DB.Unscoped().Where("company_name = ?", "QREmpCompany2").Delete(&models.Company{}) + database.DB.Unscoped().Delete(&user) +}