Re-migrate code

This commit is contained in:
2026-03-02 10:24:11 +08:00
parent 51025195a5
commit f80f2b43ce
6 changed files with 479 additions and 103 deletions

View File

@@ -38,8 +38,6 @@ swag init -g main.go # Alternative command
## Code Style Guidelines ## Code Style Guidelines
## Code Style Guidelines
### Project Structure ### Project Structure
``` ```
backend-go/ backend-go/
@@ -84,6 +82,12 @@ backend-go/
- **middleware/**: Authentication and authorization - **middleware/**: Authentication and authorization
- **routes/**: Route registration, connect controllers to router - **routes/**: Route registration, connect controllers to router
### API Surface (Current)
- **Auth**: `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/profile`, `PUT /api/auth/profile`, `POST /api/auth/change-password`
- **Serials**: `POST /api/serials/generate`, `POST /api/serials/generate-with-prefix`, `POST /api/serials/:serialNumber/qrcode`, `GET /api/serials/:serialNumber/query`, `GET /api/serials`, `PATCH /api/serials/:serialNumber`, `PUT /api/serials/:serialNumber`, `POST /api/serials/:serialNumber/revoke`
- **Companies**: `GET /api/companies/stats/overview`, `GET /api/companies`, `GET /api/companies/:companyName`, `POST /api/companies`, `PATCH /api/companies/:companyName`, `PUT /api/companies/:companyName`, `POST /api/companies/:companyName/revoke`, `DELETE /api/companies/:companyName/serials/:serialNumber`, `DELETE /api/companies/:companyName`
- **Employee Serials**: `POST /api/employee-serials/generate`, `POST /api/employee-serials/:serialNumber/qrcode`, `GET /api/employee-serials/:serialNumber/query`, `GET /api/employee-serials`, `PATCH /api/employee-serials/:serialNumber`, `PUT /api/employee-serials/:serialNumber`, `POST /api/employee-serials/:serialNumber/revoke`
### Import Organization ### Import Organization
Standard imports followed by third-party imports, then project imports (sorted alphabetically): Standard imports followed by third-party imports, then project imports (sorted alphabetically):
```go ```go
@@ -175,6 +179,7 @@ logger.Fatal("致命错误", logger.Err(err))
- Always check for errors: `if err != nil { ... }` - Always check for errors: `if err != nil { ... }`
- Use Unscoped for permanent deletion: `database.DB.Unscoped().Delete(...)` - Use Unscoped for permanent deletion: `database.DB.Unscoped().Delete(...)`
- Test cleanup: `database.DB.Unscoped().Where("1 = 1").Delete(&models.User{})` - Test cleanup: `database.DB.Unscoped().Where("1 = 1").Delete(&models.User{})`
- During `AutoMigrate()`, default admin is seeded only when user table is empty
### Testing ### Testing
- Use `testify/assert` for assertions - Use `testify/assert` for assertions
@@ -203,7 +208,7 @@ After modifying Swagger annotations, run `make swagger`.
### Configuration ### Configuration
- Load with `config.LoadConfig()` - Load with `config.LoadConfig()`
- Access with `config.GetAppConfig()` - Access with `config.GetAppConfig()`
- Environment variables: `APP_SERVER_PORT`, `APP_DATABASE_DRIVER`, etc. - Environment variables must use `APP_` prefix (e.g. `APP_SERVER_PORT`, `APP_DATABASE_DRIVER`, `APP_DATABASE_SQLITE_PATH`, `APP_JWT_SECRET`)
- .env file for local development (not committed) - .env file for local development (not committed)
### Middleware ### Middleware

View File

@@ -163,8 +163,8 @@ APP_JWT_SECRET=my-secret-key # 覆盖 jwt.secret
**开发环境(.env**: **开发环境(.env**:
```bash ```bash
# 使用 SQLite # 使用 SQLite
DATABASE_DRIVER=sqlite APP_DATABASE_DRIVER=sqlite
DATABASE_PATH=./data/dev.sqlite APP_DATABASE_SQLITE_PATH=./data/dev.sqlite
``` ```
**生产环境(环境变量)**: **生产环境(环境变量)**:
@@ -194,6 +194,11 @@ curl -X POST http://localhost:3000/api/auth/login \
-d '{"username":"admin","password":"password123"}' -d '{"username":"admin","password":"password123"}'
``` ```
首次启动且用户表为空时,系统会自动创建默认管理员账号(请在生产环境立即修改密码):
- username: `admin`
- password: `Beifan@2026`
## API 文档 ## API 文档
项目使用 Swagger 生成交互式 API 文档。 项目使用 Swagger 生成交互式 API 文档。
@@ -230,6 +235,7 @@ swag init -g main.go
| 方法 | 路径 | 描述 | 需要认证 | | 方法 | 路径 | 描述 | 需要认证 |
| ---- | --------------------------- | ----------------------- | -------- | | ---- | --------------------------- | ----------------------- | -------- |
| POST | `/api/auth/login` | 用户登录,返回 JWT 令牌 | 否 | | POST | `/api/auth/login` | 用户登录,返回 JWT 令牌 | 否 |
| POST | `/api/auth/logout` | 用户登出 | 是 |
| GET | `/api/auth/profile` | 获取当前用户信息 | 是 | | GET | `/api/auth/profile` | 获取当前用户信息 | 是 |
| PUT | `/api/auth/profile` | 更新用户信息 | 是 | | PUT | `/api/auth/profile` | 更新用户信息 | 是 |
| POST | `/api/auth/change-password` | 修改密码 | 是 | | POST | `/api/auth/change-password` | 修改密码 | 是 |
@@ -243,6 +249,7 @@ swag init -g main.go
| POST | `/api/serials/:serialNumber/qrcode` | 生成序列号二维码 | 是 | 任何 | | POST | `/api/serials/:serialNumber/qrcode` | 生成序列号二维码 | 是 | 任何 |
| GET | `/api/serials/:serialNumber/query` | 查询序列号信息 | 否 | 任何 | | GET | `/api/serials/:serialNumber/query` | 查询序列号信息 | 否 | 任何 |
| GET | `/api/serials` | 获取序列号列表 | 是 | 任何 | | GET | `/api/serials` | 获取序列号列表 | 是 | 任何 |
| PATCH | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 |
| PUT | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 | | PUT | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 |
| POST | `/api/serials/:serialNumber/revoke` | 吊销序列号 | 是 | 管理员 | | POST | `/api/serials/:serialNumber/revoke` | 吊销序列号 | 是 | 管理员 |
@@ -250,9 +257,14 @@ swag init -g main.go
| 方法 | 路径 | 描述 | 需要认证 | 角色 | | 方法 | 路径 | 描述 | 需要认证 | 角色 |
| ------ | ----------------------------- | ------------ | -------- | ------ | | ------ | ----------------------------- | ------------ | -------- | ------ |
| GET | `/api/companies` | 获取企业列表 | 是 | 任何 | | GET | `/api/companies/stats/overview` | 获取企业统计概览 | 是 | 管理员 |
| GET | `/api/companies` | 获取企业列表 | 是 | 管理员 |
| GET | `/api/companies/:companyName` | 获取企业详情 | 是 | 管理员 |
| POST | `/api/companies` | 创建新企业 | 是 | 管理员 | | POST | `/api/companies` | 创建新企业 | 是 | 管理员 |
| PATCH | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 |
| PUT | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 | | PUT | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 |
| POST | `/api/companies/:companyName/revoke` | 吊销企业及序列号 | 是 | 管理员 |
| DELETE | `/api/companies/:companyName/serials/:serialNumber` | 删除企业下序列号 | 是 | 管理员 |
| DELETE | `/api/companies/:companyName` | 删除企业 | 是 | 管理员 | | DELETE | `/api/companies/:companyName` | 删除企业 | 是 | 管理员 |
### 员工赋码 ### 员工赋码
@@ -261,10 +273,11 @@ swag init -g main.go
| ---- | -------------------------------------- | ------------------ | -------- | ------ | | ---- | -------------------------------------- | ------------------ | -------- | ------ |
| POST | `/api/employee-serials/generate` | 生成员工序列号 | 是 | 管理员 | | POST | `/api/employee-serials/generate` | 生成员工序列号 | 是 | 管理员 |
| GET | `/api/employee-serials` | 获取员工序列号列表 | 是 | 任何 | | GET | `/api/employee-serials` | 获取员工序列号列表 | 是 | 任何 |
| GET | `/api/employee-serials/:serial/query` | 查询员工序列号信息 | 否 | 任何 | | GET | `/api/employee-serials/:serialNumber/query` | 查询员工序列号信息 | 否 | 任何 |
| POST | `/api/employee-serials/:serial/qrcode` | 生成员工二维码 | 是 | 任何 | | POST | `/api/employee-serials/:serialNumber/qrcode` | 生成员工二维码 | 是 | 任何 |
| PUT | `/api/employee-serials/:serial` | 更新员工序列号信息 | 是 | 管理员 | | PATCH | `/api/employee-serials/:serialNumber` | 更新员工序列号信息 | 是 | 管理员 |
| POST | `/api/employee-serials/:serial/revoke` | 吊销员工序列号 | 是 | 管理员 | | PUT | `/api/employee-serials/:serialNumber` | 更新员工序列号信息 | 是 | 管理员 |
| POST | `/api/employee-serials/:serialNumber/revoke` | 吊销员工序列号 | 是 | 管理员 |
**员工序列号特点**: **员工序列号特点**:
- 无有效期限制(与企业赋码不同) - 无有效期限制(与企业赋码不同)
@@ -296,10 +309,11 @@ go tool cover -html=coverage.out
### 当前测试覆盖 ### 当前测试覆盖
- **services/**: 包含 AuthService、SerialsServiceEmployeeSerialsService 的完整单元测试 - **services/**: 包含 AuthService、SerialsServiceEmployeeSerialsService 和 CompaniesService 的完整单元测试
- 用户认证测试(登录、获取用户信息、修改密码、更新资料) - 用户认证测试(登录、获取用户信息、修改密码、更新资料)
- 序列号管理测试(生成、查询、更新、吊销、分页列表) - 序列号管理测试(生成、查询、更新、吊销、分页列表)
- 员工赋码测试(生成、查询、更新、吊销、二维码生成) - 员工赋码测试(生成、查询、更新、吊销、二维码生成)
- 企业统计测试(统计概览)
- **tests/**: 集成测试(健康检查、登录流程) - **tests/**: 集成测试(健康检查、登录流程)
## 代码检查 ## 代码检查

View File

@@ -2,6 +2,7 @@ package controllers
import ( import (
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -41,15 +42,25 @@ func (c *CompaniesController) FindAll(ctx *gin.Context) {
companies, total, totalPages, err := c.companiesService.FindAll(page, limit, search) companies, total, totalPages, err := c.companiesService.FindAll(page, limit, search)
if err != nil { if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{ ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
"message": err.Error(),
})
return return
} }
ctx.JSON(http.StatusOK, gin.H{ items := make([]gin.H, 0, len(companies))
"message": "获取企业列表成功", for _, company := range companies {
"data": companies, items = append(items, gin.H{
"companyName": company.CompanyName,
"firstCreated": company.CreatedAt,
"lastCreated": company.UpdatedAt,
"status": map[bool]string{
true: "active",
false: "disabled",
}[company.IsActive],
})
}
SuccessResponse(ctx, "获取企业列表成功", gin.H{
"data": items,
"pagination": gin.H{ "pagination": gin.H{
"page": page, "page": page,
"limit": limit, "limit": limit,
@@ -59,6 +70,38 @@ func (c *CompaniesController) FindAll(ctx *gin.Context) {
}) })
} }
// FindOne 获取企业详情
// @Summary 获取企业详情
// @Description 获取指定企业详情(含序列号分页)
// @Tags 企业管理
// @Produce json
// @Security BearerAuth
// @Param companyName path string true "企业名称"
// @Param page query int false "页码"
// @Param limit query int false "每页数量"
// @Success 200 {object} models.DataResponse
// @Failure 401 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /companies/{companyName} [get]
func (c *CompaniesController) FindOne(ctx *gin.Context) {
companyName, _ := url.PathUnescape(ctx.Param("companyName"))
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
data, err := c.companiesService.FindOne(companyName, page, limit)
if err != nil {
if err.Error() == "企业不存在" {
ErrorResponse(ctx, http.StatusNotFound, err.Error())
return
}
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
return
}
SuccessResponse(ctx, "获取企业详情成功", data)
}
// Create 创建企业 // Create 创建企业
// @Summary 创建企业 // @Summary 创建企业
// @Description 创建新的企业 // @Description 创建新的企业
@@ -77,24 +120,16 @@ func (c *CompaniesController) Create(ctx *gin.Context) {
var companyData struct { var companyData struct {
CompanyName string `json:"companyName" validate:"required"` CompanyName string `json:"companyName" validate:"required"`
} }
if err := ctx.ShouldBindJSON(&companyData); err != nil { if !BindJSON(ctx, &companyData) {
ctx.JSON(http.StatusBadRequest, gin.H{
"message": "无效的请求数据",
"error": err.Error(),
})
return return
} }
company, err := c.companiesService.Create(companyData.CompanyName) company, err := c.companiesService.Create(companyData.CompanyName)
if err != nil { if err != nil {
if err.Error() == "企业名称已存在" { if err.Error() == "企业名称已存在" {
ctx.JSON(http.StatusConflict, gin.H{ ErrorResponse(ctx, http.StatusConflict, err.Error())
"message": err.Error(),
})
} else { } else {
ctx.JSON(http.StatusInternalServerError, gin.H{ ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
"message": err.Error(),
})
} }
return return
} }
@@ -122,47 +157,41 @@ func (c *CompaniesController) Create(ctx *gin.Context) {
// @Failure 500 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse
// @Router /companies/{companyName} [put] // @Router /companies/{companyName} [put]
func (c *CompaniesController) Update(ctx *gin.Context) { func (c *CompaniesController) Update(ctx *gin.Context) {
companyName := ctx.Param("companyName") companyName, _ := url.PathUnescape(ctx.Param("companyName"))
var companyData struct { var companyData struct {
CompanyName string `json:"companyName"` CompanyName string `json:"companyName"`
IsActive bool `json:"isActive"` NewCompanyName string `json:"newCompanyName"`
IsActive *bool `json:"isActive"`
} }
if err := ctx.ShouldBindJSON(&companyData); err != nil { if !BindJSON(ctx, &companyData) {
ctx.JSON(http.StatusBadRequest, gin.H{
"message": "无效的请求数据",
"error": err.Error(),
})
return return
} }
company, err := c.companiesService.Update(companyName, companyData.CompanyName, companyData.IsActive) newName := companyData.NewCompanyName
if newName == "" {
newName = companyData.CompanyName
}
company, err := c.companiesService.Update(companyName, newName, companyData.IsActive)
if err != nil { if err != nil {
if err.Error() == "企业不存在" { switch err.Error() {
ctx.JSON(http.StatusNotFound, gin.H{ case "企业不存在":
"message": err.Error(), ErrorResponse(ctx, http.StatusNotFound, err.Error())
}) case "企业名称已存在":
} else if err.Error() == "企业名称已存在" { ErrorResponse(ctx, http.StatusConflict, err.Error())
ctx.JSON(http.StatusConflict, gin.H{ default:
"message": err.Error(), ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
})
} else {
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
} }
return return
} }
ctx.JSON(http.StatusOK, gin.H{ SuccessResponse(ctx, "企业信息更新成功", gin.H{"company": company})
"message": "企业信息更新成功",
"company": company,
})
} }
// Delete 删除企业 // Delete 删除企业
// @Summary 删除企业 // @Summary 删除企业
// @Description 删除企业 // @Description 删除企业及其关联序列号
// @Tags 企业管理 // @Tags 企业管理
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
@@ -174,34 +203,87 @@ func (c *CompaniesController) Update(ctx *gin.Context) {
// @Failure 500 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse
// @Router /companies/{companyName} [delete] // @Router /companies/{companyName} [delete]
func (c *CompaniesController) Delete(ctx *gin.Context) { func (c *CompaniesController) Delete(ctx *gin.Context) {
companyName := ctx.Param("companyName") companyName, _ := url.PathUnescape(ctx.Param("companyName"))
err := c.companiesService.Delete(companyName) err := c.companiesService.Delete(companyName)
if err != nil { if err != nil {
if err.Error() == "企业不存在" { if err.Error() == "企业不存在" {
ctx.JSON(http.StatusNotFound, gin.H{ ErrorResponse(ctx, http.StatusNotFound, err.Error())
"message": err.Error(),
})
} else if err.Error() == "企业下还有序列号,无法删除" {
ctx.JSON(http.StatusBadRequest, gin.H{
"message": err.Error(),
})
} else { } else {
ctx.JSON(http.StatusInternalServerError, gin.H{ ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
"message": err.Error(),
})
} }
return return
} }
ctx.JSON(http.StatusOK, gin.H{ SuccessResponse(ctx, "企业已完全删除,所有相关序列号已删除")
"message": "企业删除成功", }
// DeleteSerial 删除企业下指定序列号
// @Summary 删除企业序列号
// @Description 删除指定企业下的序列号
// @Tags 企业管理
// @Produce json
// @Security BearerAuth
// @Param companyName path string true "企业名称"
// @Param serialNumber path string true "序列号"
// @Success 200 {object} models.BaseResponse
// @Failure 401 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /companies/{companyName}/serials/{serialNumber} [delete]
func (c *CompaniesController) DeleteSerial(ctx *gin.Context) {
companyName, _ := url.PathUnescape(ctx.Param("companyName"))
serialNumber := ctx.Param("serialNumber")
err := c.companiesService.DeleteSerial(companyName, serialNumber)
if err != nil {
if err.Error() == "序列号不存在或不属于该企业" {
ErrorResponse(ctx, http.StatusNotFound, err.Error())
} else {
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
}
return
}
SuccessResponse(ctx, "序列号已成功删除", gin.H{
"serialNumber": serialNumber,
"companyName": companyName,
})
}
// Revoke 吊销企业
// @Summary 吊销企业
// @Description 吊销企业及其关联序列号
// @Tags 企业管理
// @Produce json
// @Security BearerAuth
// @Param companyName path string true "企业名称"
// @Success 200 {object} models.BaseResponse
// @Failure 401 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /companies/{companyName}/revoke [post]
func (c *CompaniesController) Revoke(ctx *gin.Context) {
companyName, _ := url.PathUnescape(ctx.Param("companyName"))
err := c.companiesService.Revoke(companyName)
if err != nil {
if err.Error() == "企业不存在" {
ErrorResponse(ctx, http.StatusNotFound, err.Error())
} else {
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
}
return
}
SuccessResponse(ctx, "企业已吊销,所有序列号已失效", gin.H{
"companyName": companyName,
}) })
} }
// StatsOverview 获取企业统计概览 // StatsOverview 获取企业统计概览
// @Summary 获取企业统计概览 // @Summary 获取企业统计概览
// @Description 获取企业、企业赋码、员工赋码的统计数据 // @Description 获取企业、序列号统计数据
// @Tags 企业管理 // @Tags 企业管理
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
@@ -210,13 +292,11 @@ func (c *CompaniesController) Delete(ctx *gin.Context) {
// @Failure 500 {object} models.ErrorResponse // @Failure 500 {object} models.ErrorResponse
// @Router /companies/stats/overview [get] // @Router /companies/stats/overview [get]
func (c *CompaniesController) StatsOverview(ctx *gin.Context) { func (c *CompaniesController) StatsOverview(ctx *gin.Context) {
stats, err := c.companiesService.GetStatsOverview() stats, err := c.companiesService.GetStats()
if err != nil { if err != nil {
ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
return return
} }
SuccessResponse(ctx, "获取企业统计概览成功", gin.H{ SuccessResponse(ctx, "获取统计数据成功", stats)
"overview": stats,
})
} }

View File

@@ -35,6 +35,7 @@ func SetupAPIRoutes(r *gin.RouterGroup) {
serialsController := controllers.NewSerialsController() serialsController := controllers.NewSerialsController()
serialsRoutes := r.Group("/serials") serialsRoutes := r.Group("/serials")
{ {
serialsRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Update)
serialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Generate) serialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Generate)
serialsRoutes.POST("/generate-with-prefix", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.GenerateWithPrefix) serialsRoutes.POST("/generate-with-prefix", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.GenerateWithPrefix)
serialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), serialsController.GenerateQRCode) serialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), serialsController.GenerateQRCode)
@@ -48,10 +49,14 @@ func SetupAPIRoutes(r *gin.RouterGroup) {
companiesController := controllers.NewCompaniesController() companiesController := controllers.NewCompaniesController()
companiesRoutes := r.Group("/companies") companiesRoutes := r.Group("/companies")
{ {
companiesRoutes.GET("/stats/overview", middleware.JWTAuthMiddleware(), companiesController.StatsOverview) companiesRoutes.GET("/stats/overview", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.StatsOverview)
companiesRoutes.GET("/", middleware.JWTAuthMiddleware(), companiesController.FindAll) companiesRoutes.GET("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.FindAll)
companiesRoutes.GET("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.FindOne)
companiesRoutes.POST("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Create) companiesRoutes.POST("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Create)
companiesRoutes.PATCH("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update)
companiesRoutes.PUT("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update) companiesRoutes.PUT("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update)
companiesRoutes.DELETE("/:companyName/serials/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.DeleteSerial)
companiesRoutes.POST("/:companyName/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Revoke)
companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete) companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete)
} }
@@ -59,6 +64,7 @@ func SetupAPIRoutes(r *gin.RouterGroup) {
employeeSerialsController := controllers.NewEmployeeSerialsController() employeeSerialsController := controllers.NewEmployeeSerialsController()
employeeSerialsRoutes := r.Group("/employee-serials") employeeSerialsRoutes := r.Group("/employee-serials")
{ {
employeeSerialsRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Update)
employeeSerialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Generate) employeeSerialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Generate)
employeeSerialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), employeeSerialsController.GenerateQRCode) employeeSerialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), employeeSerialsController.GenerateQRCode)
employeeSerialsRoutes.GET("/:serialNumber/query", employeeSerialsController.Query) employeeSerialsRoutes.GET("/:serialNumber/query", employeeSerialsController.Query)

View File

@@ -2,6 +2,10 @@ package services
import ( import (
"errors" "errors"
"fmt"
"time"
"gorm.io/gorm"
"git.beifan.cn/trace-system/backend-go/database" "git.beifan.cn/trace-system/backend-go/database"
"git.beifan.cn/trace-system/backend-go/models" "git.beifan.cn/trace-system/backend-go/models"
@@ -15,31 +19,152 @@ func (s *CompaniesService) FindAll(page int, limit int, search string) ([]models
var companies []models.Company var companies []models.Company
var total int64 var total int64
offset := (page - 1) * limit if page < 1 {
db := database.DB page = 1
}
if limit < 1 {
limit = 20
}
offset := (page - 1) * limit
db := database.DB.Model(&models.Company{})
// 搜索条件
if search != "" { if search != "" {
db = db.Where("company_name LIKE ?", "%"+search+"%") db = db.Where("company_name LIKE ?", "%"+search+"%")
} }
// 获取总数 if err := db.Count(&total).Error; err != nil {
db.Count(&total) return nil, 0, 0, errors.New("查询企业总数失败")
}
// 分页查询 result := db.Order("updated_at DESC").Offset(offset).Limit(limit).Find(&companies)
result := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&companies)
if result.Error != nil { if result.Error != nil {
return nil, 0, 0, errors.New("查询企业列表失败") return nil, 0, 0, errors.New("查询企业列表失败")
} }
totalPages := (int(total) + limit - 1) / limit totalPages := 0
if total > 0 {
totalPages = (int(total) + limit - 1) / limit
}
return companies, int(total), totalPages, nil return companies, int(total), totalPages, nil
} }
// FindOne 获取单个企业详情(含分页序列号)
func (s *CompaniesService) FindOne(companyName string, page int, limit int) (map[string]any, error) {
if page < 1 {
page = 1
}
if limit < 1 {
limit = 20
}
var company models.Company
if err := database.DB.Where("company_name = ?", companyName).First(&company).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("企业不存在")
}
return nil, errors.New("查询企业失败")
}
var allSerials []models.Serial
if err := database.DB.Preload("User").Where("company_name = ?", companyName).Order("created_at DESC").Find(&allSerials).Error; err != nil {
return nil, errors.New("查询企业序列号失败")
}
now := time.Now()
serialCount := len(allSerials)
activeCount := 0
disabledCount := 0
expiredCount := 0
for _, serial := range allSerials {
if !serial.IsActive {
disabledCount++
continue
}
if serial.ValidUntil != nil && serial.ValidUntil.Before(now) {
expiredCount++
continue
}
activeCount++
}
offset := (page - 1) * limit
end := offset + limit
if offset > len(allSerials) {
offset = len(allSerials)
}
if end > len(allSerials) {
end = len(allSerials)
}
paginatedSerials := allSerials[offset:end]
serialItems := make([]map[string]any, 0, len(paginatedSerials))
for _, serial := range paginatedSerials {
createdBy := ""
if serial.User != nil {
createdBy = serial.User.Name
}
serialItems = append(serialItems, map[string]any{
"serialNumber": serial.SerialNumber,
"validUntil": serial.ValidUntil,
"isActive": serial.IsActive,
"createdAt": serial.CreatedAt,
"createdBy": createdBy,
})
}
monthlyStatsMap := map[string]int{}
for i := 11; i >= 0; i-- {
date := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, time.Local)
monthKey := date.Format("2006-01")
monthlyStatsMap[monthKey] = 0
}
for _, serial := range allSerials {
monthKey := serial.CreatedAt.Format("2006-01")
if _, ok := monthlyStatsMap[monthKey]; ok {
monthlyStatsMap[monthKey]++
}
}
monthlyStats := make([]map[string]any, 0)
for i := 11; i >= 0; i-- {
date := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, time.Local)
monthKey := date.Format("2006-01")
count := monthlyStatsMap[monthKey]
if count > 0 {
monthlyStats = append(monthlyStats, map[string]any{"month": monthKey, "count": count})
}
}
return map[string]any{
"companyName": company.CompanyName,
"serialCount": serialCount,
"activeCount": activeCount,
"disabledCount": disabledCount,
"expiredCount": expiredCount,
"firstCreated": company.CreatedAt,
"lastCreated": company.UpdatedAt,
"status": map[bool]string{true: "active", false: "disabled"}[company.IsActive],
"serials": serialItems,
"monthlyStats": monthlyStats,
"pagination": map[string]any{
"page": page,
"limit": limit,
"total": serialCount,
"totalPages": func() int {
if serialCount == 0 {
return 0
}
return (serialCount + limit - 1) / limit
}(),
},
}, nil
}
// Create 创建企业 // Create 创建企业
func (s *CompaniesService) Create(companyName string) (*models.Company, error) { func (s *CompaniesService) Create(companyName string) (*models.Company, error) {
// 检查企业是否已存在
var existingCompany models.Company var existingCompany models.Company
result := database.DB.Where("company_name = ?", companyName).First(&existingCompany) result := database.DB.Where("company_name = ?", companyName).First(&existingCompany)
if result.Error == nil { if result.Error == nil {
@@ -60,57 +185,201 @@ func (s *CompaniesService) Create(companyName string) (*models.Company, error) {
} }
// Update 更新企业信息 // Update 更新企业信息
func (s *CompaniesService) Update(companyName string, newCompanyName string, isActive bool) (*models.Company, error) { func (s *CompaniesService) Update(companyName string, newCompanyName string, isActive *bool) (*models.Company, error) {
var company models.Company var company models.Company
result := database.DB.Where("company_name = ?", companyName).First(&company) result := database.DB.Where("company_name = ?", companyName).First(&company)
if result.Error != nil { if result.Error != nil {
return nil, errors.New("企业不存在") return nil, errors.New("企业不存在")
} }
// 如果企业名称已变更,检查新名称是否已存在 if newCompanyName == "" {
newCompanyName = companyName
}
if newCompanyName != companyName { if newCompanyName != companyName {
var existingCompany models.Company var existingCompany models.Company
checkResult := database.DB.Where("company_name = ?", newCompanyName).First(&existingCompany) checkResult := database.DB.Where("company_name = ?", newCompanyName).First(&existingCompany)
if checkResult.Error == nil { if checkResult.Error == nil {
return nil, errors.New("企业名称已存在") return nil, errors.New("企业名称已存在")
} }
}
err := database.DB.Transaction(func(tx *gorm.DB) error {
if newCompanyName != companyName {
if err := tx.Model(&models.Serial{}).Where("company_name = ?", companyName).Update("company_name", newCompanyName).Error; err != nil {
return fmt.Errorf("更新企业赋码企业名称失败: %w", err)
}
if err := tx.Model(&models.EmployeeSerial{}).Where("company_name = ?", companyName).Update("company_name", newCompanyName).Error; err != nil {
return fmt.Errorf("更新员工赋码企业名称失败: %w", err)
}
company.CompanyName = newCompanyName company.CompanyName = newCompanyName
} }
company.IsActive = isActive if isActive != nil {
company.IsActive = *isActive
}
result = database.DB.Save(&company) if err := tx.Save(&company).Error; err != nil {
if result.Error != nil { return fmt.Errorf("更新企业信息失败: %w", err)
return nil, errors.New("更新企业信息失败") }
return nil
})
if err != nil {
return nil, errors.New(err.Error())
} }
return &company, nil return &company, nil
} }
// Delete 删除企业 // Delete 删除企业(同时删除关联序列号)
func (s *CompaniesService) Delete(companyName string) error { func (s *CompaniesService) Delete(companyName string) error {
var company models.Company var company models.Company
result := database.DB.Where("company_name = ?", companyName).First(&company) if err := database.DB.Where("company_name = ?", companyName).First(&company).Error; err != nil {
if result.Error != nil {
return errors.New("企业不存在") return errors.New("企业不存在")
} }
// 检查企业是否有关联的序列号 if err := database.DB.Transaction(func(tx *gorm.DB) error {
var serialCount int64 if err := tx.Where("company_name = ?", companyName).Delete(&models.Serial{}).Error; err != nil {
database.DB.Model(&models.Serial{}).Where("company_name = ?", companyName).Count(&serialCount) return err
if serialCount > 0 {
return errors.New("企业下还有序列号,无法删除")
} }
if err := tx.Where("company_name = ?", companyName).Delete(&models.EmployeeSerial{}).Error; err != nil {
result = database.DB.Delete(&company) return err
if result.Error != nil { }
if err := tx.Delete(&company).Error; err != nil {
return err
}
return nil
}); err != nil {
return errors.New("删除企业失败") return errors.New("删除企业失败")
} }
return nil return nil
} }
// DeleteSerial 删除企业下指定企业赋码序列号
func (s *CompaniesService) DeleteSerial(companyName string, serialNumber string) error {
var serial models.Serial
err := database.DB.Where("serial_number = ? AND company_name = ?", serialNumber, companyName).First(&serial).Error
if err != nil {
return errors.New("序列号不存在或不属于该企业")
}
if err := database.DB.Delete(&serial).Error; err != nil {
return errors.New("删除序列号失败")
}
return nil
}
// Revoke 吊销企业(吊销所有企业赋码与员工赋码)
func (s *CompaniesService) Revoke(companyName string) error {
var company models.Company
if err := database.DB.Where("company_name = ?", companyName).First(&company).Error; err != nil {
return errors.New("企业不存在")
}
if err := database.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&models.Serial{}).Where("company_name = ?", companyName).Update("is_active", false).Error; err != nil {
return err
}
if err := tx.Model(&models.EmployeeSerial{}).Where("company_name = ?", companyName).Update("is_active", false).Error; err != nil {
return err
}
if err := tx.Model(&company).Update("is_active", false).Error; err != nil {
return err
}
return nil
}); err != nil {
return errors.New("吊销企业失败")
}
return nil
}
// GetStats 获取企业统计(兼容 Node 返回结构)
func (s *CompaniesService) GetStats() (map[string]any, error) {
now := time.Now()
var companies []models.Company
if err := database.DB.Order("updated_at DESC").Find(&companies).Error; err != nil {
return nil, errors.New("查询企业统计失败")
}
var serials []models.Serial
if err := database.DB.Order("created_at DESC").Find(&serials).Error; err != nil {
return nil, errors.New("查询序列号统计失败")
}
companyCount := len(companies)
serialCount := len(serials)
activeCount := 0
for _, serial := range serials {
if serial.IsActive && (serial.ValidUntil == nil || serial.ValidUntil.After(now)) {
activeCount++
}
}
inactiveCount := serialCount - activeCount
monthlyItems := make([]map[string]any, 0)
for i := 11; i >= 0; i-- {
date := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, time.Local)
monthStr := date.Format("2006-01")
monthSerialCount := 0
companySet := map[string]bool{}
for _, serial := range serials {
if serial.CreatedAt.Year() == date.Year() && serial.CreatedAt.Month() == date.Month() {
monthSerialCount++
companySet[serial.CompanyName] = true
}
}
if monthSerialCount > 0 {
monthlyItems = append(monthlyItems, map[string]any{
"month": monthStr,
"company_count": len(companySet),
"serial_count": monthSerialCount,
})
}
}
recentCompanies := make([]map[string]any, 0)
for i, company := range companies {
if i >= 10 {
break
}
recentCompanies = append(recentCompanies, map[string]any{
"companyName": company.CompanyName,
"lastCreated": company.UpdatedAt,
"status": map[bool]string{true: "active", false: "disabled"}[company.IsActive],
})
}
recentSerials := make([]map[string]any, 0)
for i, serial := range serials {
if i >= 10 {
break
}
recentSerials = append(recentSerials, map[string]any{
"serialNumber": serial.SerialNumber,
"companyName": serial.CompanyName,
"isActive": serial.IsActive,
"createdAt": serial.CreatedAt,
})
}
return map[string]any{
"overview": map[string]any{
"totalCompanies": companyCount,
"totalSerials": serialCount,
"activeSerials": activeCount,
"inactiveSerials": inactiveCount,
},
"monthlyStats": monthlyItems,
"recentCompanies": recentCompanies,
"recentSerials": recentSerials,
}, nil
}
// GetStatsOverview 获取企业统计概览 // GetStatsOverview 获取企业统计概览
func (s *CompaniesService) GetStatsOverview() (*models.CompanyStatsOverviewDTO, error) { func (s *CompaniesService) GetStatsOverview() (*models.CompanyStatsOverviewDTO, error) {
stats := &models.CompanyStatsOverviewDTO{} stats := &models.CompanyStatsOverviewDTO{}

View File

@@ -60,6 +60,8 @@ func cleanupTestData() {
} }
func createTestUsers() { func createTestUsers() {
database.DB.Unscoped().Where("1 = 1").Delete(&models.User{})
testPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) testPassword, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
testUsers := []models.User{ testUsers := []models.User{