Compare commits
5 Commits
51025195a5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e820b858bf
|
|||
|
4f18028a7b
|
|||
|
6070df659a
|
|||
|
1cc3097d9b
|
|||
|
f80f2b43ce
|
18
AGENTS.md
18
AGENTS.md
@@ -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
|
||||||
@@ -121,7 +125,7 @@ func (s *AuthService) ValidateUser(username, password string) (models.User, erro
|
|||||||
var user models.User
|
var user models.User
|
||||||
err := database.DB.Where("username = ?", username).First(&user).Error
|
err := database.DB.Where("username = ?", username).First(&user).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.User{}, errors.New("用户名或密码错误")
|
return models.User{}, errors.New("用户名或密码不正确")
|
||||||
}
|
}
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
@@ -145,6 +149,9 @@ func (c *AuthController) Login(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Use `ErrorResponse` for API errors so messages are user-friendly and consistent.
|
||||||
|
- Do not leak raw DB/stack errors to clients; log internal details and return safe messages.
|
||||||
|
|
||||||
### Response Format
|
### Response Format
|
||||||
**Success response**:
|
**Success response**:
|
||||||
```json
|
```json
|
||||||
@@ -154,6 +161,8 @@ func (c *AuthController) Login(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Some endpoints also return Node-compatible top-level keys (e.g. `serial`, `serials`, `pagination`) to preserve frontend compatibility.
|
||||||
|
|
||||||
**Error response**:
|
**Error response**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -175,6 +184,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 +213,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
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -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,10 +235,13 @@ 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 +251,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 +259,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 +275,17 @@ 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` | 吊销员工序列号 | 是 | 管理员 |
|
||||||
|
|
||||||
|
### Node 对齐说明
|
||||||
|
|
||||||
|
- Go 版路由已与 `backend-node` 对齐(含 `PATCH` 更新接口、企业详情、企业吊销、删除企业下单个序列号)。
|
||||||
|
- 企业统计与企业详情接口统一返回 `{ message, data }` 结构。
|
||||||
|
- 序列号相关返回字段已对齐前端使用(如 `serialNumber`、`validUntil`、`createdBy`、`status`)。
|
||||||
|
|
||||||
**员工序列号特点**:
|
**员工序列号特点**:
|
||||||
- 无有效期限制(与企业赋码不同)
|
- 无有效期限制(与企业赋码不同)
|
||||||
@@ -296,10 +317,11 @@ go tool cover -html=coverage.out
|
|||||||
|
|
||||||
### 当前测试覆盖
|
### 当前测试覆盖
|
||||||
|
|
||||||
- **services/**: 包含 AuthService、SerialsService 和 EmployeeSerialsService 的完整单元测试
|
- **services/**: 包含 AuthService、SerialsService、EmployeeSerialsService 和 CompaniesService 的完整单元测试
|
||||||
- 用户认证测试(登录、获取用户信息、修改密码、更新资料)
|
- 用户认证测试(登录、获取用户信息、修改密码、更新资料)
|
||||||
- 序列号管理测试(生成、查询、更新、吊销、分页列表)
|
- 序列号管理测试(生成、查询、更新、吊销、分页列表)
|
||||||
- 员工赋码测试(生成、查询、更新、吊销、二维码生成)
|
- 员工赋码测试(生成、查询、更新、吊销、二维码生成)
|
||||||
|
- 企业统计测试(统计概览)
|
||||||
- **tests/**: 集成测试(健康检查、登录流程)
|
- **tests/**: 集成测试(健康检查、登录流程)
|
||||||
|
|
||||||
## 代码检查
|
## 代码检查
|
||||||
|
|||||||
@@ -34,27 +34,19 @@ func NewAuthController() *AuthController {
|
|||||||
// @Router /auth/login [post]
|
// @Router /auth/login [post]
|
||||||
func (c *AuthController) Login(ctx *gin.Context) {
|
func (c *AuthController) Login(ctx *gin.Context) {
|
||||||
var loginData models.LoginDTO
|
var loginData models.LoginDTO
|
||||||
if err := ctx.ShouldBindJSON(&loginData); err != nil {
|
if !BindJSON(ctx, &loginData) {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"message": "无效的请求数据",
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := c.authService.ValidateUser(loginData.Username, loginData.Password)
|
user, err := c.authService.ValidateUser(loginData.Username, loginData.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusUnauthorized, gin.H{
|
ErrorResponse(ctx, http.StatusUnauthorized, err.Error())
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := c.authService.GenerateToken(user)
|
token, err := c.authService.GenerateToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
ErrorResponse(ctx, http.StatusInternalServerError, "令牌生成失败")
|
||||||
"message": "令牌生成失败",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@@ -41,15 +43,36 @@ 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": "获取企业列表成功",
|
now := time.Now()
|
||||||
"data": companies,
|
for _, company := range companies {
|
||||||
|
serialCount := len(company.Serials)
|
||||||
|
activeCount := 0
|
||||||
|
for _, serial := range company.Serials {
|
||||||
|
if serial.IsActive && (serial.ValidUntil == nil || serial.ValidUntil.After(now)) {
|
||||||
|
activeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, gin.H{
|
||||||
|
"companyName": company.CompanyName,
|
||||||
|
"firstCreated": company.CreatedAt,
|
||||||
|
"lastCreated": company.UpdatedAt,
|
||||||
|
"serialCount": serialCount,
|
||||||
|
"activeCount": activeCount,
|
||||||
|
"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 +82,41 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "获取企业详情成功",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Create 创建企业
|
// Create 创建企业
|
||||||
// @Summary 创建企业
|
// @Summary 创建企业
|
||||||
// @Description 创建新的企业
|
// @Description 创建新的企业
|
||||||
@@ -77,24 +135,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 +172,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 +218,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 +307,14 @@ 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{
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
"overview": stats,
|
"message": "获取统计数据成功",
|
||||||
|
"data": stats,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ func (c *EmployeeSerialsController) Generate(ctx *gin.Context) {
|
|||||||
|
|
||||||
serials, err := c.employeeSerialsService.Generate(
|
serials, err := c.employeeSerialsService.Generate(
|
||||||
generateData.CompanyName,
|
generateData.CompanyName,
|
||||||
generateData.Department,
|
generateData.Position,
|
||||||
generateData.EmployeeName,
|
generateData.EmployeeName,
|
||||||
generateData.Quantity,
|
generateData.Quantity,
|
||||||
userModel.ID,
|
userModel.ID,
|
||||||
|
generateData.SerialPrefix,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
ErrorResponse(ctx, http.StatusInternalServerError, err.Error())
|
||||||
@@ -226,3 +227,28 @@ func (c *EmployeeSerialsController) Revoke(ctx *gin.Context) {
|
|||||||
|
|
||||||
SuccessResponse(ctx, "员工序列号吊销成功")
|
SuccessResponse(ctx, "员工序列号吊销成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete 删除员工序列号
|
||||||
|
// @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} [delete]
|
||||||
|
func (c *EmployeeSerialsController) Delete(ctx *gin.Context) {
|
||||||
|
serialNumber := ctx.Param("serialNumber")
|
||||||
|
|
||||||
|
err := c.employeeSerialsService.Delete(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "员工序列号删除成功")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,52 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"git.beifan.cn/trace-system/backend-go/models"
|
"git.beifan.cn/trace-system/backend-go/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func friendlyMessage(message string) string {
|
||||||
|
if message == "" {
|
||||||
|
return "请求失败,请稍后重试"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := strings.TrimSpace(message)
|
||||||
|
|
||||||
|
friendlyMap := []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
}{
|
||||||
|
{"用户名或密码错误", "用户名或密码不正确"},
|
||||||
|
{"当前密码错误", "当前密码不正确"},
|
||||||
|
{"令牌生成失败", "登录失败,请稍后重试"},
|
||||||
|
{"未认证", "请先登录"},
|
||||||
|
{"用户信息无效", "登录状态无效,请重新登录"},
|
||||||
|
{"序列号不存在", "序列号不存在"},
|
||||||
|
{"企业不存在", "企业不存在"},
|
||||||
|
{"序列号已过期", "序列号已过期"},
|
||||||
|
{"序列号已被禁用", "序列号已禁用"},
|
||||||
|
{"序列号已被吊销", "序列号已吊销"},
|
||||||
|
{"企业名称已存在", "企业名称已存在"},
|
||||||
|
{"无效的请求数据", "提交数据格式不正确"},
|
||||||
|
{"无效的查询参数", "查询参数不正确"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range friendlyMap {
|
||||||
|
if strings.Contains(msg, item.key) {
|
||||||
|
return item.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(msg, "record not found") {
|
||||||
|
return "数据不存在"
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
// GetCurrentUser 从上下文中获取当前用户
|
// GetCurrentUser 从上下文中获取当前用户
|
||||||
// 如果用户未认证,返回 false 并返回 401 错误
|
// 如果用户未认证,返回 false 并返回 401 错误
|
||||||
func GetCurrentUser(ctx *gin.Context) (models.User, bool) {
|
func GetCurrentUser(ctx *gin.Context) (models.User, bool) {
|
||||||
@@ -35,8 +75,7 @@ func GetCurrentUser(ctx *gin.Context) (models.User, bool) {
|
|||||||
func BindJSON(ctx *gin.Context, obj interface{}) bool {
|
func BindJSON(ctx *gin.Context, obj interface{}) bool {
|
||||||
if err := ctx.ShouldBindJSON(obj); err != nil {
|
if err := ctx.ShouldBindJSON(obj); err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||||
"message": "无效的请求数据",
|
"message": "提交数据格式不正确",
|
||||||
"error": err.Error(),
|
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -48,8 +87,7 @@ func BindJSON(ctx *gin.Context, obj interface{}) bool {
|
|||||||
func BindQuery(ctx *gin.Context, obj interface{}) bool {
|
func BindQuery(ctx *gin.Context, obj interface{}) bool {
|
||||||
if err := ctx.ShouldBindQuery(obj); err != nil {
|
if err := ctx.ShouldBindQuery(obj); err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||||
"message": "无效的查询参数",
|
"message": "查询参数不正确",
|
||||||
"error": err.Error(),
|
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -58,11 +96,12 @@ func BindQuery(ctx *gin.Context, obj interface{}) bool {
|
|||||||
|
|
||||||
// ErrorResponse 返回错误响应
|
// ErrorResponse 返回错误响应
|
||||||
func ErrorResponse(ctx *gin.Context, status int, message string, err ...error) {
|
func ErrorResponse(ctx *gin.Context, status int, message string, err ...error) {
|
||||||
|
friendly := friendlyMessage(message)
|
||||||
response := gin.H{
|
response := gin.H{
|
||||||
"message": message,
|
"message": friendly,
|
||||||
}
|
}
|
||||||
if len(err) > 0 && err[0] != nil {
|
if len(err) > 0 && err[0] != nil {
|
||||||
response["error"] = err[0].Error()
|
response["error"] = friendlyMessage(err[0].Error())
|
||||||
}
|
}
|
||||||
ctx.JSON(status, response)
|
ctx.JSON(status, response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@@ -57,8 +58,20 @@ func (c *SerialsController) Generate(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessResponse(ctx, "成功生成 "+strconv.Itoa(len(serials))+" 个序列号", gin.H{
|
items := make([]gin.H, 0, len(serials))
|
||||||
"serials": serials,
|
for _, serial := range serials {
|
||||||
|
items = append(items, gin.H{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"validUntil": serial.ValidUntil,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"createdBy": userModel.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "成功生成"+strconv.Itoa(len(serials))+"个序列号", gin.H{
|
||||||
|
"serials": items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +111,20 @@ func (c *SerialsController) GenerateWithPrefix(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessResponse(ctx, "成功生成 "+strconv.Itoa(len(serials))+" 个序列号", gin.H{
|
items := make([]gin.H, 0, len(serials))
|
||||||
"serials": serials,
|
for _, serial := range serials {
|
||||||
|
items = append(items, gin.H{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"validUntil": serial.ValidUntil,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"createdBy": userModel.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "成功生成"+strconv.Itoa(len(serials))+"个序列号", gin.H{
|
||||||
|
"serials": items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +193,26 @@ func (c *SerialsController) Query(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdBy := ""
|
||||||
|
if serial.User != nil {
|
||||||
|
createdBy = serial.User.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "active"
|
||||||
|
if !serial.IsActive {
|
||||||
|
status = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
SuccessResponse(ctx, "查询成功", gin.H{
|
SuccessResponse(ctx, "查询成功", gin.H{
|
||||||
"serial": serial,
|
"serial": gin.H{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"validUntil": serial.ValidUntil,
|
||||||
|
"status": status,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"createdBy": createdBy,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,8 +240,25 @@ func (c *SerialsController) FindAll(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items := make([]gin.H, 0, len(serials))
|
||||||
|
for _, serial := range serials {
|
||||||
|
createdBy := ""
|
||||||
|
if serial.User != nil {
|
||||||
|
createdBy = serial.User.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, gin.H{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"validUntil": serial.ValidUntil,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"createdBy": createdBy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
SuccessResponse(ctx, "获取序列号列表成功", gin.H{
|
SuccessResponse(ctx, "获取序列号列表成功", gin.H{
|
||||||
"data": serials,
|
"data": items,
|
||||||
"pagination": gin.H{
|
"pagination": gin.H{
|
||||||
"page": page,
|
"page": page,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
@@ -237,8 +297,21 @@ func (c *SerialsController) Update(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessResponse(ctx, "序列号信息更新成功", gin.H{
|
createdBy := ""
|
||||||
"serial": serial,
|
if serial.User != nil {
|
||||||
|
createdBy = serial.User.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessResponse(ctx, "序列号更新成功", gin.H{
|
||||||
|
"serial": gin.H{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"validUntil": serial.ValidUntil,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"updatedAt": serial.UpdatedAt,
|
||||||
|
"createdBy": createdBy,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,5 +337,9 @@ func (c *SerialsController) Revoke(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessResponse(ctx, "序列号吊销成功")
|
SuccessResponse(ctx, "序列号已吊销", gin.H{
|
||||||
|
"data": gin.H{
|
||||||
|
"serialNumber": strings.ToUpper(serialNumber),
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,17 +33,17 @@ type Company struct {
|
|||||||
|
|
||||||
// Serial 模型
|
// Serial 模型
|
||||||
type Serial struct {
|
type Serial struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
SerialNumber string `gorm:"uniqueIndex;size:255"`
|
SerialNumber string `gorm:"uniqueIndex;size:255" json:"serialNumber"`
|
||||||
CompanyName string `gorm:"index;size:255"`
|
CompanyName string `gorm:"index;size:255" json:"companyName"`
|
||||||
ValidUntil *time.Time
|
ValidUntil *time.Time `json:"validUntil"`
|
||||||
IsActive bool `gorm:"default:true"`
|
IsActive bool `gorm:"default:true" json:"isActive"`
|
||||||
CreatedBy *uint
|
CreatedBy *uint `json:"createdBy"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
User *User `gorm:"foreignKey:CreatedBy"`
|
User *User `gorm:"foreignKey:CreatedBy" json:"user,omitempty"`
|
||||||
Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName"`
|
Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName" json:"company,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserDTO 数据传输对象
|
// UserDTO 数据传输对象
|
||||||
@@ -182,7 +182,7 @@ type EmployeeSerial struct {
|
|||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
SerialNumber string `gorm:"uniqueIndex;size:255" json:"serialNumber"`
|
SerialNumber string `gorm:"uniqueIndex;size:255" json:"serialNumber"`
|
||||||
CompanyName string `gorm:"index;size:255" json:"companyName"`
|
CompanyName string `gorm:"index;size:255" json:"companyName"`
|
||||||
Department string `gorm:"size:255" json:"department"`
|
Position string `gorm:"size:255" json:"position"`
|
||||||
EmployeeName string `gorm:"size:255" json:"employeeName"`
|
EmployeeName string `gorm:"size:255" json:"employeeName"`
|
||||||
IsActive bool `gorm:"default:true" json:"isActive"`
|
IsActive bool `gorm:"default:true" json:"isActive"`
|
||||||
CreatedBy *uint `json:"createdBy"`
|
CreatedBy *uint `json:"createdBy"`
|
||||||
@@ -196,15 +196,16 @@ type EmployeeSerial struct {
|
|||||||
// GenerateEmployeeSerialDTO 生成员工序列号请求数据
|
// GenerateEmployeeSerialDTO 生成员工序列号请求数据
|
||||||
type GenerateEmployeeSerialDTO struct {
|
type GenerateEmployeeSerialDTO struct {
|
||||||
CompanyName string `json:"companyName" validate:"required"`
|
CompanyName string `json:"companyName" validate:"required"`
|
||||||
Department string `json:"department" validate:"required"`
|
Position string `json:"position" validate:"required"`
|
||||||
EmployeeName string `json:"employeeName" validate:"required"`
|
EmployeeName string `json:"employeeName" validate:"required"`
|
||||||
Quantity int `json:"quantity" validate:"min=1,max=1000"`
|
Quantity int `json:"quantity" validate:"min=1,max=1000"`
|
||||||
|
SerialPrefix string `json:"serialPrefix,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEmployeeSerialDTO 员工序列号更新请求数据
|
// UpdateEmployeeSerialDTO 员工序列号更新请求数据
|
||||||
type UpdateEmployeeSerialDTO struct {
|
type UpdateEmployeeSerialDTO struct {
|
||||||
CompanyName string `json:"companyName,omitempty" validate:"omitempty"`
|
CompanyName string `json:"companyName,omitempty" validate:"omitempty"`
|
||||||
Department string `json:"department,omitempty" validate:"omitempty"`
|
Position string `json:"position,omitempty" validate:"omitempty"`
|
||||||
EmployeeName string `json:"employeeName,omitempty" validate:"omitempty"`
|
EmployeeName string `json:"employeeName,omitempty" validate:"omitempty"`
|
||||||
IsActive *bool `json:"isActive,omitempty"`
|
IsActive *bool `json:"isActive,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +64,13 @@ 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)
|
||||||
employeeSerialsRoutes.GET("/", middleware.JWTAuthMiddleware(), employeeSerialsController.FindAll)
|
employeeSerialsRoutes.GET("/", middleware.JWTAuthMiddleware(), employeeSerialsController.FindAll)
|
||||||
employeeSerialsRoutes.PUT("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Update)
|
employeeSerialsRoutes.PUT("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Update)
|
||||||
employeeSerialsRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Revoke)
|
employeeSerialsRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Revoke)
|
||||||
|
employeeSerialsRoutes.DELETE("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ func (s *AuthService) ValidateUser(username string, password string) (*models.Us
|
|||||||
var user models.User
|
var user models.User
|
||||||
result := database.DB.Where("username = ?", username).First(&user)
|
result := database.DB.Where("username = ?", username).First(&user)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return nil, fmt.Errorf("验证用户失败: %w", errors.New("用户名或密码错误"))
|
return nil, errors.New("用户名或密码不正确")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("密码验证失败: %w", errors.New("用户名或密码错误"))
|
return nil, errors.New("用户名或密码不正确")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"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 +20,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.Preload("Serials").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 +186,226 @@ 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("企业名称已存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
company.CompanyName = newCompanyName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
company.IsActive = isActive
|
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
|
||||||
|
}
|
||||||
|
|
||||||
result = database.DB.Save(&company)
|
if isActive != nil {
|
||||||
if result.Error != nil {
|
company.IsActive = *isActive
|
||||||
return nil, errors.New("更新企业信息失败")
|
}
|
||||||
|
|
||||||
|
if err := tx.Save(&company).Error; err != nil {
|
||||||
|
return fmt.Errorf("更新企业信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
result = database.DB.Delete(&company)
|
if err := tx.Delete(&company).Error; err != nil {
|
||||||
if result.Error != 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("查询序列号统计失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
var employeeSerials []models.EmployeeSerial
|
||||||
|
if err := database.DB.Order("created_at DESC").Find(&employeeSerials).Error; err != nil {
|
||||||
|
return nil, errors.New("查询员工序列号统计失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
companyCount := len(companies)
|
||||||
|
serialCount := len(serials)
|
||||||
|
employeeSerialCount := len(employeeSerials)
|
||||||
|
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 _, serial := range serials {
|
||||||
|
recentSerials = append(recentSerials, map[string]any{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"type": "company",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 添加员工序列号
|
||||||
|
for _, serial := range employeeSerials {
|
||||||
|
recentSerials = append(recentSerials, map[string]any{
|
||||||
|
"serialNumber": serial.SerialNumber,
|
||||||
|
"companyName": serial.CompanyName,
|
||||||
|
"isActive": serial.IsActive,
|
||||||
|
"createdAt": serial.CreatedAt,
|
||||||
|
"type": "employee",
|
||||||
|
"position": serial.Position,
|
||||||
|
"employeeName": serial.EmployeeName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 按创建时间排序,保留最新的10条
|
||||||
|
sort.Slice(recentSerials, func(i, j int) bool {
|
||||||
|
return recentSerials[i]["createdAt"].(time.Time).After(recentSerials[j]["createdAt"].(time.Time))
|
||||||
|
})
|
||||||
|
if len(recentSerials) > 10 {
|
||||||
|
recentSerials = recentSerials[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"overview": map[string]any{
|
||||||
|
"totalCompanies": companyCount,
|
||||||
|
"totalSerials": serialCount,
|
||||||
|
"totalEmployeeSerials": employeeSerialCount,
|
||||||
|
"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{}
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ type EmployeeSerialsService struct{}
|
|||||||
// Generate 生成员工序列号
|
// Generate 生成员工序列号
|
||||||
func (s *EmployeeSerialsService) Generate(
|
func (s *EmployeeSerialsService) Generate(
|
||||||
companyName string,
|
companyName string,
|
||||||
department string,
|
position string,
|
||||||
employeeName string,
|
employeeName string,
|
||||||
quantity int,
|
quantity int,
|
||||||
userId uint,
|
userId uint,
|
||||||
|
serialPrefix string,
|
||||||
) ([]models.EmployeeSerial, error) {
|
) ([]models.EmployeeSerial, error) {
|
||||||
var serials []models.EmployeeSerial
|
var serials []models.EmployeeSerial
|
||||||
|
|
||||||
@@ -45,8 +46,12 @@ func (s *EmployeeSerialsService) Generate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成序列号前缀 (EMP + 年份后两位)
|
// 生成序列号前缀
|
||||||
serialPrefix := fmt.Sprintf("EMP%d", time.Now().Year()%100)
|
if serialPrefix == "" {
|
||||||
|
serialPrefix = fmt.Sprintf("EMP%d", time.Now().Year()%100)
|
||||||
|
}
|
||||||
|
// 标准化前缀(转大写,去除空格)
|
||||||
|
serialPrefix = strings.ToUpper(strings.TrimSpace(serialPrefix))
|
||||||
|
|
||||||
// 预生成所有序列号
|
// 预生成所有序列号
|
||||||
serialNumbers := make(map[string]bool)
|
serialNumbers := make(map[string]bool)
|
||||||
@@ -74,7 +79,7 @@ func (s *EmployeeSerialsService) Generate(
|
|||||||
serial := models.EmployeeSerial{
|
serial := models.EmployeeSerial{
|
||||||
SerialNumber: strings.ToUpper(serialNumber),
|
SerialNumber: strings.ToUpper(serialNumber),
|
||||||
CompanyName: companyName,
|
CompanyName: companyName,
|
||||||
Department: department,
|
Position: position,
|
||||||
EmployeeName: employeeName,
|
EmployeeName: employeeName,
|
||||||
CreatedBy: &userId,
|
CreatedBy: &userId,
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
@@ -158,8 +163,8 @@ func (s *EmployeeSerialsService) Update(serialNumber string, updateData models.U
|
|||||||
serial.CompanyName = updateData.CompanyName
|
serial.CompanyName = updateData.CompanyName
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateData.Department != "" {
|
if updateData.Position != "" {
|
||||||
serial.Department = updateData.Department
|
serial.Position = updateData.Position
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateData.EmployeeName != "" {
|
if updateData.EmployeeName != "" {
|
||||||
@@ -199,6 +204,22 @@ func (s *EmployeeSerialsService) Revoke(serialNumber string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete 删除员工序列号(物理删除)
|
||||||
|
func (s *EmployeeSerialsService) Delete(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("序列号不存在"))
|
||||||
|
}
|
||||||
|
|
||||||
|
result = database.DB.Delete(&serial)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("删除员工序列号失败: %w", result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateQRCode 生成员工二维码
|
// GenerateQRCode 生成员工二维码
|
||||||
func (s *EmployeeSerialsService) GenerateQRCode(
|
func (s *EmployeeSerialsService) GenerateQRCode(
|
||||||
serialNumber string,
|
serialNumber string,
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ func (s *SerialsService) Update(serialNumber string, updateData models.UpdateSer
|
|||||||
return nil, fmt.Errorf("更新序列号失败: %w", result.Error)
|
return nil, fmt.Errorf("更新序列号失败: %w", result.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = database.DB.Preload("User").Where("serial_number = ?", serial.SerialNumber).First(&serial)
|
||||||
|
|
||||||
return &serial, nil
|
return &serial, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{
|
||||||
@@ -158,5 +160,5 @@ func TestLoginFailed(t *testing.T) {
|
|||||||
var response map[string]interface{}
|
var response map[string]interface{}
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, response["message"], "用户名或密码错误")
|
assert.Contains(t, response["message"], "用户名或密码不正确")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user