From b1e3adaf6a59229a40bdc95ab2e8adc063b74261 Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Tue, 26 May 2026 10:51:16 +0800 Subject: [PATCH] Change aftersales serial to YYMMNN monthly sequence User-requested format: zjbf-sh-260501 (26=year, 05=month, 01=monthly seq). Sequence resets each month and skips soft-deleted entries to avoid reuse. Also documents aftersales API and new technician role in AGENTS.md / README. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 10 ++++++++-- README.md | 30 ++++++++++++++++++++++++++++-- services/aftersales_service.go | 31 +++++++++++++++++++++---------- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 97290c9..a431a79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ backend-go/ ├── config/ # Configuration management │ └── config.go # Config loading (.env, config.yaml, env vars) ├── controllers/ # HTTP request handlers +│ ├── aftersales_controller.go # Aftersales orders: create, query, submit, confirm, qrcode │ ├── auth_controller.go # Auth: login, profile, password change │ ├── companies_controller.go # Company CRUD │ ├── employees_controller.go # Employee serials: generate, query, update, revoke, qrcode @@ -55,12 +56,13 @@ backend-go/ ├── logger/ # Structured logging │ └── logger.go # Zap logger wrapper ├── middleware/ # Middleware -│ └── auth.go # JWT auth, Admin permission check +│ └── auth.go # JWT auth, Admin / Technician permission checks ├── models/ # Data models and DTOs -│ └── models.go # User, Company, Serial, EmployeeSerial and DTOs +│ └── models.go # User, Company, Serial, EmployeeSerial, AftersalesOrder and DTOs ├── routes/ # Route configuration │ └── routes.go # API route registration ├── services/ # Business logic layer +│ ├── aftersales_service.go # Aftersales orders: create, list, update, submit, customer confirm, qrcode │ ├── auth_service.go # Auth: validate user, generate token, password management │ ├── companies_service.go # Company CRUD │ ├── employees_service.go # Employee serials: generate, query, update, revoke, qrcode @@ -87,6 +89,9 @@ backend-go/ - **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` +- **Aftersales** (公开): `GET /api/aftersales/:serialNumber/query`, `POST /api/aftersales/:serialNumber/confirm` +- **Aftersales** (技术员+管理员): `POST /api/aftersales`, `GET /api/aftersales`, `GET /api/aftersales/:serialNumber`, `PATCH /api/aftersales/:serialNumber`, `POST /api/aftersales/:serialNumber/qrcode`, `POST /api/aftersales/:serialNumber/submit` +- **Aftersales** (仅管理员): `POST /api/aftersales/:serialNumber/reassign`, `POST /api/aftersales/:serialNumber/force-close`, `DELETE /api/aftersales/:serialNumber` ### Import Organization Standard imports followed by third-party imports, then project imports (sorted alphabetically): @@ -219,6 +224,7 @@ After modifying Swagger annotations, run `make swagger`. ### Middleware - **JWTAuthMiddleware**: Validates JWT tokens, sets user in context - **AdminMiddleware**: Checks if user has admin role +- **TechnicianMiddleware**: Allows admin and technician roles (used for aftersales endpoints) - Access current user: `user, ok := GetCurrentUser(ctx)` ### Git Hooks diff --git a/README.md b/README.md index 302ff78..489da17 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ backend-go/ ├── config/ # 配置管理 │ └── config.go # 配置加载和解析(支持 .env 文件和环境变量) ├── controllers/ # 控制器层,处理 HTTP 请求 +│ ├── aftersales_controller.go # 售后工单接口 │ ├── auth_controller.go # 认证相关接口 │ ├── companies_controller.go # 企业管理接口 │ ├── employees_controller.go # 员工赋码接口 @@ -43,12 +44,13 @@ backend-go/ ├── logger/ # 日志管理 │ └── logger.go # 结构化日志(使用 Zap) ├── middleware/ # 中间件层 -│ └── auth.go # JWT 认证和权限检查 +│ └── auth.go # JWT 认证、管理员/技术员权限检查 ├── models/ # 数据模型和 DTO -│ └── models.go # User、Company、Serial 等模型定义 +│ └── models.go # User、Company、Serial、AftersalesOrder 等模型定义 ├── routes/ # 路由配置 │ └── routes.go # API 路由注册 ├── services/ # 业务逻辑层 +│ ├── aftersales_service.go # 售后工单业务逻辑 │ ├── auth_service.go # 认证业务逻辑 │ ├── companies_service.go # 企业管理业务逻辑 │ ├── employees_service.go # 员工赋码业务逻辑 @@ -292,6 +294,30 @@ swag init -g main.go - 包含部门(department)和员工姓名(employeeName)信息 - 序列号格式: `EMP26xxxxxx`(EMP + 年份后两位 + 6位随机字符) +### 售后工单 + +| 方法 | 路径 | 描述 | 需要认证 | 角色 | +| ------ | --------------------------------------------- | -------------------------- | -------- | --------------- | +| GET | `/api/aftersales/:serialNumber/query` | 公开查询工单(脱敏) | 否 | 任何 | +| POST | `/api/aftersales/:serialNumber/confirm` | 客户授权/未授权确认 | 否 | 任何 | +| POST | `/api/aftersales` | 创建售后工单 | 是 | 管理员/技术员 | +| GET | `/api/aftersales` | 工单列表(支持筛选) | 是 | 管理员/技术员 | +| GET | `/api/aftersales/:serialNumber` | 工单详情 | 是 | 管理员/技术员 | +| PATCH | `/api/aftersales/:serialNumber` | 更新工单(仅负责人或管理员)| 是 | 管理员/技术员 | +| POST | `/api/aftersales/:serialNumber/qrcode` | 生成工单二维码 | 是 | 管理员/技术员 | +| POST | `/api/aftersales/:serialNumber/submit` | 提交客户确认 | 是 | 管理员/技术员 | +| POST | `/api/aftersales/:serialNumber/reassign` | 重新分配技术员 | 是 | 管理员 | +| POST | `/api/aftersales/:serialNumber/force-close` | 强制关闭工单 | 是 | 管理员 | +| DELETE | `/api/aftersales/:serialNumber` | 删除工单 | 是 | 管理员 | + +**售后工单特点**: +- 工单号格式: `zjbf-sh-YYMMNN`(年份后 2 位 + 月份 2 位 + 当月序号至少 2 位,例:`zjbf-sh-260501`) +- 序号按月重置,软删除工单不释放编号(避免回收造成混淆) +- 二维码扫码后客户输入手机号后 4 位进行身份校验 +- 客户确认接口每分钟同一工单最多 5 次请求 +- 工单状态机: `created` → `pending_confirmation` → `closed` / `rejected`,被退回后可重新提交 +- 公开查询不返回手机号(脱敏) + ## 测试 ### 运行所有测试 diff --git a/services/aftersales_service.go b/services/aftersales_service.go index 5e3b821..b6e465d 100644 --- a/services/aftersales_service.go +++ b/services/aftersales_service.go @@ -1,9 +1,7 @@ package services import ( - "crypto/rand" "encoding/base64" - "encoding/hex" "errors" "fmt" "os" @@ -70,20 +68,33 @@ func normalizeAftersalesSerial(sn string) string { } // generateUniqueSerial 生成唯一的售后工单序列号 +// 格式:zjbf-sh-YYMMNN,YY=年份后两位,MM=月份,NN=当月第几单(至少 2 位,溢出自然加宽) func (s *AftersalesService) generateUniqueSerial() (string, error) { - for attempt := 0; attempt < 10; attempt++ { - randomBytes := make([]byte, 3) - if _, err := rand.Read(randomBytes); err != nil { - return "", fmt.Errorf("生成随机数失败: %w", err) - } - randomPart := strings.ToLower(hex.EncodeToString(randomBytes))[:6] - candidate := aftersalesSerialPrefix + randomPart + now := time.Now() + yy := now.Year() % 100 + mm := int(now.Month()) + + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + nextMonth := monthStart.AddDate(0, 1, 0) + + // 统计本月已创建工单数(含软删除,避免编号回收) + var count int64 + if err := database.DB.Unscoped().Model(&models.AftersalesOrder{}). + Where("created_at >= ? AND created_at < ?", monthStart, nextMonth). + Count(&count).Error; err != nil { + return "", fmt.Errorf("统计当月工单数失败: %w", err) + } + + seq := int(count) + 1 + for attempt := 0; attempt < 100; attempt++ { + candidate := fmt.Sprintf("%s%02d%02d%02d", aftersalesSerialPrefix, yy, mm, seq) var existing models.AftersalesOrder - result := database.DB.Where("serial_number = ?", candidate).First(&existing) + result := database.DB.Unscoped().Where("serial_number = ?", candidate).First(&existing) if result.Error != nil { return candidate, nil } + seq++ } return "", errors.New("生成唯一序列号失败,请重试") }