From d3ee215f61a34b787eb9c462274d691aae0a8e92 Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Thu, 28 May 2026 10:05:59 +0800 Subject: [PATCH] Refactor employee management --- AGENTS.md | 16 +- Makefile | 4 +- README.md | 52 +- controllers/auth_controller.go | 15 +- controllers/users_controller.go | 52 +- database/database.go | 13 +- docs/docs.go | 1881 ++++++++++++++++++++++++++++++- docs/swagger.json | 1881 ++++++++++++++++++++++++++++++- docs/swagger.yaml | 1210 +++++++++++++++++++- models/models.go | 90 +- routes/routes.go | 22 +- services/auth_service.go | 36 +- services/employees_service.go | 56 +- services/services_test.go | 33 +- services/users_service.go | 130 ++- services/users_service_test.go | 66 +- tests/main_test.go | 8 +- 17 files changed, 5391 insertions(+), 174 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0e38764..765524b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ backend-go/ │ ├── employees_controller.go # Employee serials: generate, query, update, revoke, qrcode │ ├── helper.go # Helper functions (GetCurrentUser, BindJSON, Response) │ ├── serials_controller.go # Company serials: generate, query, update, revoke, qrcode -│ └── users_controller.go # User management (admin): create, list, update, reset password, delete +│ └── users_controller.go # Employee master data / backend account management ├── database/ # Database connection and migrations │ └── database.go # GORM init, AutoMigrate ├── docs/ # Swagger documentation (auto-generated) @@ -70,8 +70,8 @@ backend-go/ │ ├── serials_service.go # Company serials: generate, query, update, revoke, qrcode │ ├── aftersales_service_test.go # Aftersales unit tests │ ├── services_test.go # Auth / Serials / Employees / Companies unit tests -│ ├── users_service.go # User CRUD, role management, password reset (admin) -│ └── users_service_test.go # Users unit tests +│ ├── users_service.go # Employee CRUD, role management, password reset (admin) +│ └── users_service_test.go # Employee/user account unit tests ├── tests/ # Integration tests │ └── main_test.go # End-to-end tests ├── data/ # SQLite data directory @@ -97,7 +97,15 @@ backend-go/ - **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` - **Users** (技术员+管理员): `GET /api/users/assignable` -- **Users** (仅管理员): `POST /api/users`, `GET /api/users`, `PATCH /api/users/:id`, `POST /api/users/:id/reset-password`, `DELETE /api/users/:id` +- **Employees** (仅管理员): `POST /api/employees`, `GET /api/employees`, `PATCH /api/employees/:id`, `POST /api/employees/:id/reset-password`, `DELETE /api/employees/:id` + +### Roles and permissions +- Roles are limited to `admin`, `technician`, and `employee`. +- `admin` has full backend access. +- `technician` only has aftersales/work-order module access. +- `employee` has no backend login access and does not require a password. +- Creating an employee through `/api/employees` creates employee master data and automatically generates one employee serial bound by `employeeId`. +- `admin` / `technician` creation requires an initial password; `employee` creation must not require one. ### Import Organization Standard imports followed by third-party imports, then project imports (sorted alphabetically): diff --git a/Makefile b/Makefile index e3fd31b..c7bc85a 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ deps: # 生成 Swagger 文档 swagger: @echo "生成 Swagger 文档..." - @swag init -g main.go + @go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go @echo "Swagger 文档已生成在 docs/ 目录" # 初始化数据库 @@ -84,4 +84,4 @@ build-mac: @set CGO_ENABLED=0 @set GOOS=darwin @set GOARCH=arm64 - @go build -o $(NAME) main.go \ No newline at end of file + @go build -o $(NAME) main.go diff --git a/README.md b/README.md index b7e930c..93d3e46 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ backend-go/ │ ├── employees_controller.go # 员工赋码接口 │ ├── helper.go # 控制器通用辅助函数 │ ├── serials_controller.go # 序列号管理接口 -│ └── users_controller.go # 用户管理接口(仅管理员) +│ └── users_controller.go # 员工主档管理接口(仅管理员) ├── database/ # 数据库连接和操作 │ └── database.go # 数据库初始化、连接池配置 ├── docs/ # Swagger API 文档(自动生成) @@ -58,8 +58,8 @@ backend-go/ │ ├── serials_service.go # 序列号业务逻辑 │ ├── aftersales_service_test.go # 售后工单单元测试 │ ├── services_test.go # 认证/序列号/员工/企业单元测试 -│ ├── users_service.go # 用户管理业务逻辑 -│ └── users_service_test.go # 用户管理单元测试 +│ ├── users_service.go # 员工主档/后台账号业务逻辑 +│ └── users_service_test.go # 员工主档/后台账号单元测试 ├── tests/ # 集成测试 │ └── main_test.go # 端到端测试 ├── data/ # 数据目录(SQLite 数据库存储位置) @@ -197,7 +197,7 @@ curl -X GET http://localhost:3000/api/health ```bash curl -X POST http://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"password123"}' + -d '{"username":"admin","password":"Beifan@2026"}' ``` 首次启动且用户表为空时,系统会自动创建默认管理员账号(请在生产环境立即修改密码): @@ -227,11 +227,11 @@ http://localhost:3000/swagger/index.html 如果修改了代码中的 API 注解,需要重新生成 Swagger 文档: ```bash -# 确保已安装 swag 工具 -go install github.com/swaggo/swag/cmd/swag@latest - # 生成文档 -swag init -g main.go +make swagger + +# 或直接执行 +go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go # 文档将生成在 docs/ 目录下 ``` @@ -254,9 +254,9 @@ swag init -g main.go | ---- | ----------------------------------- | ---------------- | -------- | ------ | | POST | `/api/serials/generate` | 生成序列号 | 是 | 管理员 | | POST | `/api/serials/generate-with-prefix` | 带前缀生成序列号 | 是 | 管理员 | -| POST | `/api/serials/:serialNumber/qrcode` | 生成序列号二维码 | 是 | 任何 | +| POST | `/api/serials/:serialNumber/qrcode` | 生成序列号二维码 | 是 | 管理员 | | GET | `/api/serials/:serialNumber/query` | 查询序列号信息 | 否 | 任何 | -| GET | `/api/serials` | 获取序列号列表 | 是 | 任何 | +| GET | `/api/serials` | 获取序列号列表 | 是 | 管理员 | | PATCH | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 | | PUT | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 | | POST | `/api/serials/:serialNumber/revoke` | 吊销序列号 | 是 | 管理员 | @@ -280,9 +280,9 @@ swag init -g main.go | 方法 | 路径 | 描述 | 需要认证 | 角色 | | ---- | -------------------------------------- | ------------------ | -------- | ------ | | POST | `/api/employee-serials/generate` | 生成员工序列号 | 是 | 管理员 | -| GET | `/api/employee-serials` | 获取员工序列号列表 | 是 | 任何 | +| GET | `/api/employee-serials` | 获取员工序列号列表 | 是 | 管理员 | | GET | `/api/employee-serials/:serialNumber/query` | 查询员工序列号信息 | 否 | 任何 | -| POST | `/api/employee-serials/:serialNumber/qrcode` | 生成员工二维码 | 是 | 任何 | +| POST | `/api/employee-serials/:serialNumber/qrcode` | 生成员工二维码 | 是 | 管理员 | | PATCH | `/api/employee-serials/:serialNumber` | 更新员工序列号信息 | 是 | 管理员 | | PUT | `/api/employee-serials/:serialNumber` | 更新员工序列号信息 | 是 | 管理员 | | POST | `/api/employee-serials/:serialNumber/revoke` | 吊销员工序列号 | 是 | 管理员 | @@ -295,7 +295,8 @@ swag init -g main.go **员工序列号特点**: - 无有效期限制(与企业赋码不同) -- 包含部门(department)和员工姓名(employeeName)信息 +- 创建员工主档时会自动生成 1 个员工码,并通过 `employeeId` 绑定员工 +- 包含岗位(position)和员工姓名(employeeName)信息 - 序列号格式: `EMP26xxxxxx`(EMP + 年份后两位 + 6位随机字符) ### 售后工单 @@ -325,25 +326,26 @@ swag init -g main.go - 工单状态机: `created` → `pending_confirmation` → `closed` / `rejected`,被退回后可重新提交 - 公开查询不返回手机号(脱敏) -### 用户管理(仅管理员) +### 员工管理(仅管理员) | 方法 | 路径 | 描述 | 需要认证 | 角色 | | ------ | ----------------------------------- | -------------------------- | -------- | ------------- | | GET | `/api/users/assignable` | 可分配用户列表(用于售后) | 是 | 管理员/技术员 | -| POST | `/api/users` | 创建用户 | 是 | 管理员 | -| GET | `/api/users` | 用户列表(分页+筛选) | 是 | 管理员 | -| PATCH | `/api/users/:id` | 更新用户姓名/邮箱/角色 | 是 | 管理员 | -| POST | `/api/users/:id/reset-password` | 重置用户密码 | 是 | 管理员 | -| DELETE | `/api/users/:id` | 删除用户 | 是 | 管理员 | +| POST | `/api/employees` | 创建员工并自动生成员工码 | 是 | 管理员 | +| GET | `/api/employees` | 员工列表(分页+筛选) | 是 | 管理员 | +| PATCH | `/api/employees/:id` | 更新员工姓名/电话/工号/岗位/角色 | 是 | 管理员 | +| POST | `/api/employees/:id/reset-password` | 重置后台账号密码 | 是 | 管理员 | +| DELETE | `/api/employees/:id` | 删除员工 | 是 | 管理员 | -**用户角色**: -- `admin`:完整权限,包括用户管理、强制关闭工单、工单分配(重新分配技术员)、删除工单 -- `technician`:可创建/编辑自己负责的售后工单,可使用 `assignable` 查询同事 -- `user`:保留角色(暂未实际启用) +**员工角色**: +- `admin`:管理员,拥有全部后台权限,包括企业管理、员工管理、工单分配、强制关闭和删除工单 +- `technician`:技术员,仅拥有工单模块权限,可创建/处理工单,可使用 `assignable` 查询可分配同事 +- `employee`:员工,无后台登录权限,不需要密码,仅用于员工主档和员工码查询 **保护规则**: - 不能删除自己;不能将自己的 admin 角色降级;不能删除最后一个 admin -- 默认创建用户密码 bcrypt 加密存储 +- 创建 `admin` / `technician` 必须设置初始密码,密码 bcrypt 加密存储 +- 创建 `employee` 不要求密码,且不能登录后台 ## 测试 @@ -376,7 +378,7 @@ go tool cover -html=coverage.out - 员工赋码测试(生成、查询、更新、吊销、二维码生成) - 企业统计测试(统计概览) - 售后工单测试(YYMMDDNN 序号生成、状态机、客户确认手机号校验、强制关闭) - - 用户管理测试(重复用户名、自降级保护、最后管理员保护、密码重置) + - 员工管理测试(创建员工自动生成员工码、重复工号、自降级保护、最后管理员保护、密码重置) - **tests/**: 集成测试(健康检查、登录流程) ## 代码检查 diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go index 154e83e..4a084b9 100644 --- a/controllers/auth_controller.go +++ b/controllers/auth_controller.go @@ -58,12 +58,15 @@ func (c *AuthController) Login(ctx *gin.Context) { "message": "登录成功", "accessToken": token, "user": models.UserDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Email: user.Email, - Role: user.Role, - CreatedAt: user.CreatedAt, + ID: user.ID, + Username: user.Username, + Name: user.Name, + Email: user.Email, + Phone: user.Phone, + EmployeeNo: user.EmployeeNo, + Position: user.Position, + Role: user.Role, + CreatedAt: user.CreatedAt, }, }) } diff --git a/controllers/users_controller.go b/controllers/users_controller.go index e52ac1e..1983dc3 100644 --- a/controllers/users_controller.go +++ b/controllers/users_controller.go @@ -10,7 +10,7 @@ import ( "git.beifan.cn/trace-system/backend-go/services" ) -// UsersController 用户管理控制器 +// UsersController 员工管理控制器 type UsersController struct { usersService services.UsersService } @@ -22,17 +22,17 @@ func NewUsersController() *UsersController { } } -// Create 创建用户(管理员) -// @Summary 创建用户 -// @Tags 用户管理 +// Create 创建员工(管理员) +// @Summary 创建员工 +// @Tags 员工管理 // @Accept json // @Produce json // @Security BearerAuth -// @Param data body models.CreateUserDTO true "用户数据" +// @Param data body models.CreateUserDTO true "员工数据" // @Success 200 {object} models.DataResponse // @Failure 400 {object} models.ErrorResponse // @Failure 401 {object} models.ErrorResponse -// @Router /users [post] +// @Router /employees [post] func (c *UsersController) Create(ctx *gin.Context) { var dto models.CreateUserDTO if !BindJSON(ctx, &dto) { @@ -45,14 +45,14 @@ func (c *UsersController) Create(ctx *gin.Context) { return } - SuccessResponse(ctx, "用户创建成功", gin.H{ - "user": user, + SuccessResponse(ctx, "员工创建成功", gin.H{ + "employee": user, }) } -// FindAll 用户列表 -// @Summary 用户列表 -// @Tags 用户管理 +// FindAll 员工列表 +// @Summary 员工列表 +// @Tags 员工管理 // @Produce json // @Security BearerAuth // @Param page query int false "页码" @@ -61,7 +61,7 @@ func (c *UsersController) Create(ctx *gin.Context) { // @Param search query string false "搜索" // @Success 200 {object} models.PaginationResponse // @Failure 401 {object} models.ErrorResponse -// @Router /users [get] +// @Router /employees [get] func (c *UsersController) FindAll(ctx *gin.Context) { page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20")) @@ -74,7 +74,7 @@ func (c *UsersController) FindAll(ctx *gin.Context) { return } - SuccessResponse(ctx, "获取用户列表成功", gin.H{ + SuccessResponse(ctx, "获取员工列表成功", gin.H{ "data": users, "pagination": gin.H{ "page": page, @@ -106,9 +106,9 @@ func (c *UsersController) FindAssignable(ctx *gin.Context) { }) } -// Update 更新用户信息 -// @Summary 更新用户 -// @Tags 用户管理 +// Update 更新员工信息 +// @Summary 更新员工 +// @Tags 员工管理 // @Accept json // @Produce json // @Security BearerAuth @@ -117,7 +117,7 @@ func (c *UsersController) FindAssignable(ctx *gin.Context) { // @Success 200 {object} models.DataResponse // @Failure 400 {object} models.ErrorResponse // @Failure 401 {object} models.ErrorResponse -// @Router /users/{id} [patch] +// @Router /employees/{id} [patch] func (c *UsersController) Update(ctx *gin.Context) { userModel, ok := GetCurrentUser(ctx) if !ok { @@ -141,14 +141,14 @@ func (c *UsersController) Update(ctx *gin.Context) { return } - SuccessResponse(ctx, "用户更新成功", gin.H{ - "user": user, + SuccessResponse(ctx, "员工更新成功", gin.H{ + "employee": user, }) } // ResetPassword 重置用户密码 // @Summary 重置密码 -// @Tags 用户管理 +// @Tags 员工管理 // @Accept json // @Produce json // @Security BearerAuth @@ -157,7 +157,7 @@ func (c *UsersController) Update(ctx *gin.Context) { // @Success 200 {object} models.BaseResponse // @Failure 400 {object} models.ErrorResponse // @Failure 401 {object} models.ErrorResponse -// @Router /users/{id}/reset-password [post] +// @Router /employees/{id}/reset-password [post] func (c *UsersController) ResetPassword(ctx *gin.Context) { id, err := strconv.ParseUint(ctx.Param("id"), 10, 32) if err != nil { @@ -178,16 +178,16 @@ func (c *UsersController) ResetPassword(ctx *gin.Context) { SuccessResponse(ctx, "密码重置成功") } -// Delete 删除用户 -// @Summary 删除用户 -// @Tags 用户管理 +// Delete 删除员工 +// @Summary 删除员工 +// @Tags 员工管理 // @Produce json // @Security BearerAuth // @Param id path int true "用户 ID" // @Success 200 {object} models.BaseResponse // @Failure 400 {object} models.ErrorResponse // @Failure 401 {object} models.ErrorResponse -// @Router /users/{id} [delete] +// @Router /employees/{id} [delete] func (c *UsersController) Delete(ctx *gin.Context) { userModel, ok := GetCurrentUser(ctx) if !ok { @@ -205,5 +205,5 @@ func (c *UsersController) Delete(ctx *gin.Context) { return } - SuccessResponse(ctx, "用户删除成功") + SuccessResponse(ctx, "员工删除成功") } diff --git a/database/database.go b/database/database.go index 3b425b7..4652aef 100644 --- a/database/database.go +++ b/database/database.go @@ -148,11 +148,14 @@ func seedAdminUser() { } admin := models.User{ - Username: adminUsername, - Password: string(hashedPassword), - Name: adminName, - Email: adminEmail, - Role: "admin", + Username: adminUsername, + Password: string(hashedPassword), + Name: adminName, + Email: adminEmail, + Phone: "00000000000", + EmployeeNo: adminUsername, + Position: "管理员", + Role: "admin", } if err := DB.Create(&admin).Error; err != nil { diff --git a/docs/docs.go b/docs/docs.go index 2861856..0075b3c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -24,6 +24,600 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/aftersales": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "支持分页、搜索、按状态/服务类型/技术员筛选", + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "获取售后工单列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "搜索关键词", + "name": "search", + "in": "query" + }, + { + "type": "string", + "description": "工单状态", + "name": "workOrderStatus", + "in": "query" + }, + { + "type": "string", + "description": "服务类型", + "name": "serviceType", + "in": "query" + }, + { + "type": "integer", + "description": "技术员 ID", + "name": "technicianId", + "in": "query" + }, + { + "type": "boolean", + "description": "仅查看自己负责的工单", + "name": "mine", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的售后工单并分配编号", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "创建售后工单", + "parameters": [ + { + "description": "工单数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateAftersalesOrderDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "获取售后工单详情", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "删除售后工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "更新售后工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "更新数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateAftersalesOrderDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/confirm": { + "post": { + "description": "客户输入手机号后四位后选择已授权或未授权", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单查询" + ], + "summary": "客户授权确认", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "确认数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CustomerConfirmDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/force-close": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "强制关闭工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/qrcode": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "生成售后工单二维码", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "二维码参数", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/models.QRCodeDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.QRCodeResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/query": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "售后工单查询" + ], + "summary": "公开查询售后工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/reassign": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "重新分配技术员", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "新技术员 ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ReassignAftersalesDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/submit": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "技术员填写处理结果后提交,工单进入\"待客户确认\"状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "提交客户确认", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "处理结果", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SubmitForConfirmationDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/auth/change-password": { "post": { "security": [ @@ -121,6 +715,37 @@ const docTemplate = `{ } } }, + "/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "用户登出(JWT 无状态,前端清理令牌即可)", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登出", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/auth/profile": { "get": { "security": [ @@ -319,7 +944,106 @@ const docTemplate = `{ } } }, + "/companies/stats/overview": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取企业、序列号统计数据", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "获取企业统计概览", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/companies/{companyName}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定企业详情(含序列号分页)", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "获取企业详情", + "parameters": [ + { + "type": "string", + "description": "企业名称", + "name": "companyName", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, "put": { "security": [ { @@ -400,7 +1124,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "删除企业", + "description": "删除企业及其关联序列号", "produces": [ "application/json" ], @@ -451,6 +1175,792 @@ const docTemplate = `{ } } }, + "/companies/{companyName}/revoke": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "吊销企业及其关联序列号", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "吊销企业", + "parameters": [ + { + "type": "string", + "description": "企业名称", + "name": "companyName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/companies/{companyName}/serials/{serialNumber}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定企业下的序列号", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "删除企业序列号", + "parameters": [ + { + "type": "string", + "description": "企业名称", + "name": "companyName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取员工序列号列表,支持分页和搜索", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "获取员工序列号列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "搜索关键词", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/generate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "生成指定数量的员工序列号(无有效期)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "生成员工序列号", + "parameters": [ + { + "description": "生成数据", + "name": "generateData", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.GenerateEmployeeSerialDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新指定员工序列号的信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "更新员工序列号信息", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "更新数据", + "name": "updateData", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateEmployeeSerialDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定员工序列号(物理删除)", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "删除员工序列号", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}/qrcode": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为指定员工序列号生成查询二维码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "生成员工二维码", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "二维码数据", + "name": "qrCodeData", + "in": "body", + "schema": { + "$ref": "#/definitions/models.QRCodeDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.QRCodeResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}/query": { + "get": { + "description": "查询指定员工序列号的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码查询" + ], + "summary": "查询员工序列号信息", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}/revoke": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "吊销指定员工序列号", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "吊销员工序列号", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employees": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "员工列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "角色筛选", + "name": "role", + "in": "query" + }, + { + "type": "string", + "description": "搜索", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "创建员工", + "parameters": [ + { + "description": "员工数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateUserDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employees/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "删除员工", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "更新员工", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateUserDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employees/{id}/reset-password": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "重置密码", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新密码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.AdminResetPasswordDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/serials": { "get": { "security": [ @@ -865,9 +2375,52 @@ const docTemplate = `{ } } } + }, + "/users/assignable": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "用于售后工单分配选择技术员/管理员,无需分页", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取可分配用户列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } } }, "definitions": { + "models.AdminResetPasswordDTO": { + "type": "object", + "required": [ + "newPassword" + ], + "properties": { + "newPassword": { + "type": "string", + "minLength": 6 + } + } + }, "models.BaseResponse": { "type": "object", "properties": { @@ -945,6 +2498,107 @@ const docTemplate = `{ } } }, + "models.CreateAftersalesOrderDTO": { + "type": "object", + "required": [ + "companyAddress", + "companyName", + "contactName", + "contactPhone", + "issueDescription", + "serviceType" + ], + "properties": { + "companyAddress": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "issueDescription": { + "type": "string" + }, + "serviceType": { + "type": "string", + "enum": [ + "software", + "hardware", + "maintenance" + ] + }, + "technicianId": { + "type": "integer" + } + } + }, + "models.CreateUserDTO": { + "type": "object", + "required": [ + "employeeNo", + "name", + "phone", + "position", + "role" + ], + "properties": { + "email": { + "type": "string" + }, + "employeeNo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "technician", + "employee" + ] + }, + "username": { + "type": "string" + } + } + }, + "models.CustomerConfirmDTO": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "authorize", + "reject" + ] + }, + "rejectReason": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, "models.DataResponse": { "type": "object", "properties": { @@ -954,6 +2608,50 @@ const docTemplate = `{ } } }, + "models.EmployeeSerial": { + "type": "object", + "properties": { + "company": { + "$ref": "#/definitions/models.Company" + }, + "companyName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdBy": { + "type": "integer" + }, + "employee": { + "$ref": "#/definitions/models.User" + }, + "employeeId": { + "type": "integer" + }, + "employeeName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isActive": { + "type": "boolean" + }, + "position": { + "type": "string" + }, + "serialNumber": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/models.User" + } + } + }, "models.ErrorResponse": { "type": "object", "properties": { @@ -965,6 +2663,33 @@ const docTemplate = `{ } } }, + "models.GenerateEmployeeSerialDTO": { + "type": "object", + "required": [ + "companyName", + "employeeName", + "position" + ], + "properties": { + "companyName": { + "type": "string" + }, + "employeeName": { + "type": "string" + }, + "position": { + "type": "string" + }, + "quantity": { + "type": "integer", + "maximum": 1000, + "minimum": 1 + }, + "serialPrefix": { + "type": "string" + } + } + }, "models.GenerateSerialDTO": { "type": "object", "required": [ @@ -1091,6 +2816,76 @@ const docTemplate = `{ } } }, + "models.ReassignAftersalesDTO": { + "type": "object", + "required": [ + "technicianId" + ], + "properties": { + "technicianId": { + "type": "integer" + } + } + }, + "models.SubmitForConfirmationDTO": { + "type": "object", + "required": [ + "resolutionNote" + ], + "properties": { + "resolutionNote": { + "type": "string" + } + } + }, + "models.UpdateAftersalesOrderDTO": { + "type": "object", + "properties": { + "companyAddress": { + "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "issueDescription": { + "type": "string" + }, + "resolutionNote": { + "type": "string" + }, + "serviceType": { + "type": "string", + "enum": [ + "software", + "hardware", + "maintenance" + ] + }, + "technicianId": { + "type": "integer" + } + } + }, + "models.UpdateEmployeeSerialDTO": { + "type": "object", + "properties": { + "companyName": { + "type": "string" + }, + "employeeName": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "position": { + "type": "string" + } + } + }, "models.UpdateProfileDTO": { "type": "object", "required": [ @@ -1120,6 +2915,75 @@ const docTemplate = `{ } } }, + "models.UpdateUserDTO": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "employeeNo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "technician", + "employee" + ] + } + } + }, + "models.User": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employeeNo": { + "type": "string" + }, + "employeeSerials": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EmployeeSerial" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, + "role": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "models.UserDTO": { "type": "object", "properties": { @@ -1129,12 +2993,27 @@ const docTemplate = `{ "email": { "type": "string" }, + "employeeNo": { + "type": "string" + }, + "employeeSerials": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EmployeeSerial" + } + }, "id": { "type": "integer" }, "name": { "type": "string" }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, "role": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 358a3b2..64d2464 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -18,6 +18,600 @@ "host": "localhost:8080", "basePath": "/api", "paths": { + "/aftersales": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "支持分页、搜索、按状态/服务类型/技术员筛选", + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "获取售后工单列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "搜索关键词", + "name": "search", + "in": "query" + }, + { + "type": "string", + "description": "工单状态", + "name": "workOrderStatus", + "in": "query" + }, + { + "type": "string", + "description": "服务类型", + "name": "serviceType", + "in": "query" + }, + { + "type": "integer", + "description": "技术员 ID", + "name": "technicianId", + "in": "query" + }, + { + "type": "boolean", + "description": "仅查看自己负责的工单", + "name": "mine", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的售后工单并分配编号", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "创建售后工单", + "parameters": [ + { + "description": "工单数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateAftersalesOrderDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "获取售后工单详情", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "删除售后工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "更新售后工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "更新数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateAftersalesOrderDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/confirm": { + "post": { + "description": "客户输入手机号后四位后选择已授权或未授权", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单查询" + ], + "summary": "客户授权确认", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "确认数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CustomerConfirmDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/force-close": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "强制关闭工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/qrcode": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "生成售后工单二维码", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "二维码参数", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/models.QRCodeDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.QRCodeResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/query": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "售后工单查询" + ], + "summary": "公开查询售后工单", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/reassign": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "重新分配技术员", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "新技术员 ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ReassignAftersalesDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/aftersales/{serialNumber}/submit": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "技术员填写处理结果后提交,工单进入\"待客户确认\"状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单" + ], + "summary": "提交客户确认", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "处理结果", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SubmitForConfirmationDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/auth/change-password": { "post": { "security": [ @@ -115,6 +709,37 @@ } } }, + "/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "用户登出(JWT 无状态,前端清理令牌即可)", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登出", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/auth/profile": { "get": { "security": [ @@ -313,7 +938,106 @@ } } }, + "/companies/stats/overview": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取企业、序列号统计数据", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "获取企业统计概览", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/companies/{companyName}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定企业详情(含序列号分页)", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "获取企业详情", + "parameters": [ + { + "type": "string", + "description": "企业名称", + "name": "companyName", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, "put": { "security": [ { @@ -394,7 +1118,7 @@ "BearerAuth": [] } ], - "description": "删除企业", + "description": "删除企业及其关联序列号", "produces": [ "application/json" ], @@ -445,6 +1169,792 @@ } } }, + "/companies/{companyName}/revoke": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "吊销企业及其关联序列号", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "吊销企业", + "parameters": [ + { + "type": "string", + "description": "企业名称", + "name": "companyName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/companies/{companyName}/serials/{serialNumber}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定企业下的序列号", + "produces": [ + "application/json" + ], + "tags": [ + "企业管理" + ], + "summary": "删除企业序列号", + "parameters": [ + { + "type": "string", + "description": "企业名称", + "name": "companyName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取员工序列号列表,支持分页和搜索", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "获取员工序列号列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "搜索关键词", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/generate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "生成指定数量的员工序列号(无有效期)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "生成员工序列号", + "parameters": [ + { + "description": "生成数据", + "name": "generateData", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.GenerateEmployeeSerialDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新指定员工序列号的信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "更新员工序列号信息", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "更新数据", + "name": "updateData", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateEmployeeSerialDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定员工序列号(物理删除)", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "删除员工序列号", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}/qrcode": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为指定员工序列号生成查询二维码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "生成员工二维码", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "description": "二维码数据", + "name": "qrCodeData", + "in": "body", + "schema": { + "$ref": "#/definitions/models.QRCodeDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.QRCodeResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}/query": { + "get": { + "description": "查询指定员工序列号的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码查询" + ], + "summary": "查询员工序列号信息", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employee-serials/{serialNumber}/revoke": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "吊销指定员工序列号", + "produces": [ + "application/json" + ], + "tags": [ + "员工赋码管理" + ], + "summary": "吊销员工序列号", + "parameters": [ + { + "type": "string", + "description": "序列号", + "name": "serialNumber", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employees": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "员工列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "角色筛选", + "name": "role", + "in": "query" + }, + { + "type": "string", + "description": "搜索", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginationResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "创建员工", + "parameters": [ + { + "description": "员工数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateUserDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employees/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "删除员工", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "更新员工", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateUserDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, + "/employees/{id}/reset-password": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "员工管理" + ], + "summary": "重置密码", + "parameters": [ + { + "type": "integer", + "description": "用户 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "新密码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.AdminResetPasswordDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } + }, "/serials": { "get": { "security": [ @@ -859,9 +2369,52 @@ } } } + }, + "/users/assignable": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "用于售后工单分配选择技术员/管理员,无需分页", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取可分配用户列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DataResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } + } } }, "definitions": { + "models.AdminResetPasswordDTO": { + "type": "object", + "required": [ + "newPassword" + ], + "properties": { + "newPassword": { + "type": "string", + "minLength": 6 + } + } + }, "models.BaseResponse": { "type": "object", "properties": { @@ -939,6 +2492,107 @@ } } }, + "models.CreateAftersalesOrderDTO": { + "type": "object", + "required": [ + "companyAddress", + "companyName", + "contactName", + "contactPhone", + "issueDescription", + "serviceType" + ], + "properties": { + "companyAddress": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "issueDescription": { + "type": "string" + }, + "serviceType": { + "type": "string", + "enum": [ + "software", + "hardware", + "maintenance" + ] + }, + "technicianId": { + "type": "integer" + } + } + }, + "models.CreateUserDTO": { + "type": "object", + "required": [ + "employeeNo", + "name", + "phone", + "position", + "role" + ], + "properties": { + "email": { + "type": "string" + }, + "employeeNo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "technician", + "employee" + ] + }, + "username": { + "type": "string" + } + } + }, + "models.CustomerConfirmDTO": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "authorize", + "reject" + ] + }, + "rejectReason": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, "models.DataResponse": { "type": "object", "properties": { @@ -948,6 +2602,50 @@ } } }, + "models.EmployeeSerial": { + "type": "object", + "properties": { + "company": { + "$ref": "#/definitions/models.Company" + }, + "companyName": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "createdBy": { + "type": "integer" + }, + "employee": { + "$ref": "#/definitions/models.User" + }, + "employeeId": { + "type": "integer" + }, + "employeeName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isActive": { + "type": "boolean" + }, + "position": { + "type": "string" + }, + "serialNumber": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/models.User" + } + } + }, "models.ErrorResponse": { "type": "object", "properties": { @@ -959,6 +2657,33 @@ } } }, + "models.GenerateEmployeeSerialDTO": { + "type": "object", + "required": [ + "companyName", + "employeeName", + "position" + ], + "properties": { + "companyName": { + "type": "string" + }, + "employeeName": { + "type": "string" + }, + "position": { + "type": "string" + }, + "quantity": { + "type": "integer", + "maximum": 1000, + "minimum": 1 + }, + "serialPrefix": { + "type": "string" + } + } + }, "models.GenerateSerialDTO": { "type": "object", "required": [ @@ -1085,6 +2810,76 @@ } } }, + "models.ReassignAftersalesDTO": { + "type": "object", + "required": [ + "technicianId" + ], + "properties": { + "technicianId": { + "type": "integer" + } + } + }, + "models.SubmitForConfirmationDTO": { + "type": "object", + "required": [ + "resolutionNote" + ], + "properties": { + "resolutionNote": { + "type": "string" + } + } + }, + "models.UpdateAftersalesOrderDTO": { + "type": "object", + "properties": { + "companyAddress": { + "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "issueDescription": { + "type": "string" + }, + "resolutionNote": { + "type": "string" + }, + "serviceType": { + "type": "string", + "enum": [ + "software", + "hardware", + "maintenance" + ] + }, + "technicianId": { + "type": "integer" + } + } + }, + "models.UpdateEmployeeSerialDTO": { + "type": "object", + "properties": { + "companyName": { + "type": "string" + }, + "employeeName": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "position": { + "type": "string" + } + } + }, "models.UpdateProfileDTO": { "type": "object", "required": [ @@ -1114,6 +2909,75 @@ } } }, + "models.UpdateUserDTO": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "employeeNo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "admin", + "technician", + "employee" + ] + } + } + }, + "models.User": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employeeNo": { + "type": "string" + }, + "employeeSerials": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EmployeeSerial" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, + "role": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "models.UserDTO": { "type": "object", "properties": { @@ -1123,12 +2987,27 @@ "email": { "type": "string" }, + "employeeNo": { + "type": "string" + }, + "employeeSerials": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EmployeeSerial" + } + }, "id": { "type": "integer" }, "name": { "type": "string" }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + }, "role": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 18a338e..e3f89b5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,13 @@ basePath: /api definitions: + models.AdminResetPasswordDTO: + properties: + newPassword: + minLength: 6 + type: string + required: + - newPassword + type: object models.BaseResponse: properties: message: @@ -50,12 +58,112 @@ definitions: isActive: type: boolean type: object + models.CreateAftersalesOrderDTO: + properties: + companyAddress: + type: string + companyName: + type: string + contactName: + type: string + contactPhone: + type: string + issueDescription: + type: string + serviceType: + enum: + - software + - hardware + - maintenance + type: string + technicianId: + type: integer + required: + - companyAddress + - companyName + - contactName + - contactPhone + - issueDescription + - serviceType + type: object + models.CreateUserDTO: + properties: + email: + type: string + employeeNo: + type: string + name: + type: string + password: + type: string + phone: + type: string + position: + type: string + role: + enum: + - admin + - technician + - employee + type: string + username: + type: string + required: + - employeeNo + - name + - phone + - position + - role + type: object + models.CustomerConfirmDTO: + properties: + action: + enum: + - authorize + - reject + type: string + rejectReason: + type: string + signature: + type: string + required: + - action + type: object models.DataResponse: properties: data: {} message: type: string type: object + models.EmployeeSerial: + properties: + company: + $ref: '#/definitions/models.Company' + companyName: + type: string + createdAt: + type: string + createdBy: + type: integer + employee: + $ref: '#/definitions/models.User' + employeeId: + type: integer + employeeName: + type: string + id: + type: integer + isActive: + type: boolean + position: + type: string + serialNumber: + type: string + updatedAt: + type: string + user: + $ref: '#/definitions/models.User' + type: object models.ErrorResponse: properties: error: @@ -63,6 +171,25 @@ definitions: message: type: string type: object + models.GenerateEmployeeSerialDTO: + properties: + companyName: + type: string + employeeName: + type: string + position: + type: string + quantity: + maximum: 1000 + minimum: 1 + type: integer + serialPrefix: + type: string + required: + - companyName + - employeeName + - position + type: object models.GenerateSerialDTO: properties: companyName: @@ -148,6 +275,52 @@ definitions: queryUrl: type: string type: object + models.ReassignAftersalesDTO: + properties: + technicianId: + type: integer + required: + - technicianId + type: object + models.SubmitForConfirmationDTO: + properties: + resolutionNote: + type: string + required: + - resolutionNote + type: object + models.UpdateAftersalesOrderDTO: + properties: + companyAddress: + type: string + contactName: + type: string + contactPhone: + type: string + issueDescription: + type: string + resolutionNote: + type: string + serviceType: + enum: + - software + - hardware + - maintenance + type: string + technicianId: + type: integer + type: object + models.UpdateEmployeeSerialDTO: + properties: + companyName: + type: string + employeeName: + type: string + isActive: + type: boolean + position: + type: string + type: object models.UpdateProfileDTO: properties: email: @@ -167,16 +340,72 @@ definitions: validUntil: type: string type: object + models.UpdateUserDTO: + properties: + email: + type: string + employeeNo: + type: string + name: + type: string + phone: + type: string + position: + type: string + role: + enum: + - admin + - technician + - employee + type: string + type: object + models.User: + properties: + createdAt: + type: string + email: + type: string + employeeNo: + type: string + employeeSerials: + items: + $ref: '#/definitions/models.EmployeeSerial' + type: array + id: + type: integer + name: + type: string + phone: + type: string + position: + type: string + role: + type: string + updatedAt: + type: string + username: + type: string + type: object models.UserDTO: properties: createdAt: type: string email: type: string + employeeNo: + type: string + employeeSerials: + items: + $ref: '#/definitions/models.EmployeeSerial' + type: array id: type: integer name: type: string + phone: + type: string + position: + type: string role: type: string username: @@ -196,6 +425,384 @@ info: title: Trace System API version: "1.0" paths: + /aftersales: + get: + description: 支持分页、搜索、按状态/服务类型/技术员筛选 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: limit + type: integer + - description: 搜索关键词 + in: query + name: search + type: string + - description: 工单状态 + in: query + name: workOrderStatus + type: string + - description: 服务类型 + in: query + name: serviceType + type: string + - description: 技术员 ID + in: query + name: technicianId + type: integer + - description: 仅查看自己负责的工单 + in: query + name: mine + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PaginationResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取售后工单列表 + tags: + - 售后工单 + post: + consumes: + - application/json + description: 创建一个新的售后工单并分配编号 + parameters: + - description: 工单数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.CreateAftersalesOrderDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建售后工单 + tags: + - 售后工单 + /aftersales/{serialNumber}: + delete: + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除售后工单 + tags: + - 售后工单 + get: + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取售后工单详情 + tags: + - 售后工单 + patch: + consumes: + - application/json + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 更新数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.UpdateAftersalesOrderDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新售后工单 + tags: + - 售后工单 + /aftersales/{serialNumber}/confirm: + post: + consumes: + - application/json + description: 客户输入手机号后四位后选择已授权或未授权 + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 确认数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.CustomerConfirmDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 客户授权确认 + tags: + - 售后工单查询 + /aftersales/{serialNumber}/force-close: + post: + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 强制关闭工单 + tags: + - 售后工单 + /aftersales/{serialNumber}/qrcode: + post: + consumes: + - application/json + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 二维码参数 + in: body + name: data + schema: + $ref: '#/definitions/models.QRCodeDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.QRCodeResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 生成售后工单二维码 + tags: + - 售后工单 + /aftersales/{serialNumber}/query: + get: + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 公开查询售后工单 + tags: + - 售后工单查询 + /aftersales/{serialNumber}/reassign: + post: + consumes: + - application/json + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 新技术员 ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.ReassignAftersalesDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 重新分配技术员 + tags: + - 售后工单 + /aftersales/{serialNumber}/submit: + post: + consumes: + - application/json + description: 技术员填写处理结果后提交,工单进入"待客户确认"状态 + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 处理结果 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.SubmitForConfirmationDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 提交客户确认 + tags: + - 售后工单 /auth/change-password: post: consumes: @@ -258,6 +865,25 @@ paths: summary: 用户登录 tags: - 认证 + /auth/logout: + post: + description: 用户登出(JWT 无状态,前端清理令牌即可) + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 用户登出 + tags: + - 认证 /auth/profile: get: description: 获取当前登录用户的个人信息 @@ -385,7 +1011,7 @@ paths: - 企业管理 /companies/{companyName}: delete: - description: 删除企业 + description: 删除企业及其关联序列号 parameters: - description: 企业名称 in: path @@ -420,6 +1046,46 @@ paths: summary: 删除企业 tags: - 企业管理 + get: + description: 获取指定企业详情(含序列号分页) + parameters: + - description: 企业名称 + in: path + name: companyName + required: true + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取企业详情 + tags: + - 企业管理 put: consumes: - application/json @@ -468,6 +1134,529 @@ paths: summary: 更新企业信息 tags: - 企业管理 + /companies/{companyName}/revoke: + post: + description: 吊销企业及其关联序列号 + parameters: + - description: 企业名称 + in: path + name: companyName + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 吊销企业 + tags: + - 企业管理 + /companies/{companyName}/serials/{serialNumber}: + delete: + description: 删除指定企业下的序列号 + parameters: + - description: 企业名称 + in: path + name: companyName + required: true + type: string + - description: 序列号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除企业序列号 + tags: + - 企业管理 + /companies/stats/overview: + get: + description: 获取企业、序列号统计数据 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取企业统计概览 + tags: + - 企业管理 + /employee-serials: + get: + description: 获取员工序列号列表,支持分页和搜索 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: limit + type: integer + - description: 搜索关键词 + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PaginationResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取员工序列号列表 + tags: + - 员工赋码管理 + /employee-serials/{serialNumber}: + delete: + description: 删除指定员工序列号(物理删除) + parameters: + - description: 序列号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除员工序列号 + tags: + - 员工赋码管理 + put: + consumes: + - application/json + description: 更新指定员工序列号的信息 + parameters: + - description: 序列号 + in: path + name: serialNumber + required: true + type: string + - description: 更新数据 + in: body + name: updateData + required: true + schema: + $ref: '#/definitions/models.UpdateEmployeeSerialDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新员工序列号信息 + tags: + - 员工赋码管理 + /employee-serials/{serialNumber}/qrcode: + post: + consumes: + - application/json + description: 为指定员工序列号生成查询二维码 + parameters: + - description: 序列号 + in: path + name: serialNumber + required: true + type: string + - description: 二维码数据 + in: body + name: qrCodeData + schema: + $ref: '#/definitions/models.QRCodeDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.QRCodeResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 生成员工二维码 + tags: + - 员工赋码管理 + /employee-serials/{serialNumber}/query: + get: + description: 查询指定员工序列号的详细信息 + parameters: + - description: 序列号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 查询员工序列号信息 + tags: + - 员工赋码查询 + /employee-serials/{serialNumber}/revoke: + post: + description: 吊销指定员工序列号 + parameters: + - description: 序列号 + in: path + name: serialNumber + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 吊销员工序列号 + tags: + - 员工赋码管理 + /employee-serials/generate: + post: + consumes: + - application/json + description: 生成指定数量的员工序列号(无有效期) + parameters: + - description: 生成数据 + in: body + name: generateData + required: true + schema: + $ref: '#/definitions/models.GenerateEmployeeSerialDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 生成员工序列号 + tags: + - 员工赋码管理 + /employees: + get: + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: limit + type: integer + - description: 角色筛选 + in: query + name: role + type: string + - description: 搜索 + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PaginationResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 员工列表 + tags: + - 员工管理 + post: + consumes: + - application/json + parameters: + - description: 员工数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.CreateUserDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建员工 + tags: + - 员工管理 + /employees/{id}: + delete: + parameters: + - description: 用户 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除员工 + tags: + - 员工管理 + patch: + consumes: + - application/json + parameters: + - description: 用户 ID + in: path + name: id + required: true + type: integer + - description: 更新数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.UpdateUserDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新员工 + tags: + - 员工管理 + /employees/{id}/reset-password: + post: + consumes: + - application/json + parameters: + - description: 用户 ID + in: path + name: id + required: true + type: integer + - description: 新密码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/models.AdminResetPasswordDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 重置密码 + tags: + - 员工管理 /serials: get: description: 获取序列号列表,支持分页和搜索 @@ -733,6 +1922,25 @@ paths: summary: 带前缀生成序列号 tags: - 序列号管理 + /users/assignable: + get: + description: 用于售后工单分配选择技术员/管理员,无需分页 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DataResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取可分配用户列表 + tags: + - 用户管理 securityDefinitions: BearerAuth: description: Bearer token 认证 diff --git a/models/models.go b/models/models.go index 3200d4a..4a09f48 100644 --- a/models/models.go +++ b/models/models.go @@ -8,16 +8,20 @@ import ( // User 模型 type User struct { - ID uint `gorm:"primaryKey" json:"id"` - Username string `gorm:"uniqueIndex;size:255" json:"username"` - Password string `gorm:"size:255" json:"-"` - Name string `gorm:"size:255" json:"name"` - Email string `gorm:"size:255" json:"email"` - Role string `gorm:"size:50;default:'user'" json:"role"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Serials []Serial `gorm:"foreignKey:CreatedBy" json:"-"` + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"uniqueIndex;size:255" json:"username"` + Password string `gorm:"size:255" json:"-"` + Name string `gorm:"size:255" json:"name"` + Email string `gorm:"size:255" json:"email"` + Phone string `gorm:"size:50" json:"phone"` + EmployeeNo string `gorm:"uniqueIndex;size:100" json:"employeeNo"` + Position string `gorm:"size:255" json:"position"` + Role string `gorm:"size:50;default:'employee'" json:"role"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Serials []Serial `gorm:"foreignKey:CreatedBy" json:"-"` + EmployeeSerials []EmployeeSerial `gorm:"foreignKey:EmployeeID" json:"employeeSerials,omitempty"` } // Company 模型 @@ -48,12 +52,16 @@ type Serial struct { // UserDTO 数据传输对象 type UserDTO struct { - ID uint `json:"id"` - Username string `json:"username"` - Name string `json:"name"` - Email string `json:"email"` - Role string `json:"role"` - CreatedAt time.Time `json:"createdAt"` + ID uint `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + EmployeeNo string `json:"employeeNo"` + Position string `json:"position"` + Role string `json:"role"` + CreatedAt time.Time `json:"createdAt"` + EmployeeSerials []EmployeeSerial `json:"employeeSerials,omitempty"` } // LoginDTO 登录请求数据 @@ -76,18 +84,24 @@ type UpdateProfileDTO struct { // CreateUserDTO 管理员创建用户请求 type CreateUserDTO struct { - Username string `json:"username" validate:"required,min=3,max=50"` - Password string `json:"password" validate:"required,min=6"` - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"omitempty,email"` - Role string `json:"role" validate:"required,oneof=admin technician employee user"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"omitempty,email"` + Phone string `json:"phone" validate:"required"` + EmployeeNo string `json:"employeeNo" validate:"required"` + Position string `json:"position" validate:"required"` + Role string `json:"role" validate:"required,oneof=admin technician employee"` } // UpdateUserDTO 管理员更新用户信息请求 type UpdateUserDTO struct { - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty" validate:"omitempty,email"` - Role string `json:"role,omitempty" validate:"omitempty,oneof=admin technician employee user"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty" validate:"omitempty,email"` + Phone string `json:"phone,omitempty"` + EmployeeNo string `json:"employeeNo,omitempty"` + Position string `json:"position,omitempty"` + Role string `json:"role,omitempty" validate:"omitempty,oneof=admin technician employee"` } // AdminResetPasswordDTO 管理员重置用户密码 @@ -187,19 +201,19 @@ type CompanyUpdateRequest struct { // CompanyStatsOverviewDTO 企业统计概览 type CompanyStatsOverviewDTO struct { - TotalCompanies int64 `json:"totalCompanies"` - ActiveCompanies int64 `json:"activeCompanies"` - InactiveCompanies int64 `json:"inactiveCompanies"` - TotalSerials int64 `json:"totalSerials"` - ActiveSerials int64 `json:"activeSerials"` - RevokedSerials int64 `json:"revokedSerials"` - TotalEmployeeSerials int64 `json:"totalEmployeeSerials"` - ActiveEmployeeSerials int64 `json:"activeEmployeeSerials"` - RevokedEmployeeSerials int64 `json:"revokedEmployeeSerials"` - TotalAftersales int64 `json:"totalAftersales"` - PendingConfirmation int64 `json:"pendingConfirmation"` - ClosedAftersales int64 `json:"closedAftersales"` - RejectedAftersales int64 `json:"rejectedAftersales"` + TotalCompanies int64 `json:"totalCompanies"` + ActiveCompanies int64 `json:"activeCompanies"` + InactiveCompanies int64 `json:"inactiveCompanies"` + TotalSerials int64 `json:"totalSerials"` + ActiveSerials int64 `json:"activeSerials"` + RevokedSerials int64 `json:"revokedSerials"` + TotalEmployeeSerials int64 `json:"totalEmployeeSerials"` + ActiveEmployeeSerials int64 `json:"activeEmployeeSerials"` + RevokedEmployeeSerials int64 `json:"revokedEmployeeSerials"` + TotalAftersales int64 `json:"totalAftersales"` + PendingConfirmation int64 `json:"pendingConfirmation"` + ClosedAftersales int64 `json:"closedAftersales"` + RejectedAftersales int64 `json:"rejectedAftersales"` } // EmployeeSerial 员工序列号模型 @@ -209,12 +223,14 @@ type EmployeeSerial struct { CompanyName string `gorm:"index;size:255" json:"companyName"` Position string `gorm:"size:255" json:"position"` EmployeeName string `gorm:"size:255" json:"employeeName"` + EmployeeID *uint `gorm:"index" json:"employeeId,omitempty"` IsActive bool `gorm:"default:true" json:"isActive"` CreatedBy *uint `json:"createdBy"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` User *User `gorm:"foreignKey:CreatedBy" json:"user,omitempty"` + Employee *User `gorm:"foreignKey:EmployeeID" json:"employee,omitempty"` Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName" json:"company,omitempty"` } diff --git a/routes/routes.go b/routes/routes.go index 02258d0..60ff37c 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -38,9 +38,9 @@ func SetupAPIRoutes(r *gin.RouterGroup) { serialsRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Update) serialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Generate) 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(), middleware.AdminMiddleware(), serialsController.GenerateQRCode) serialsRoutes.GET("/:serialNumber/query", serialsController.Query) - serialsRoutes.GET("/", middleware.JWTAuthMiddleware(), serialsController.FindAll) + serialsRoutes.GET("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.FindAll) serialsRoutes.PUT("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Update) serialsRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), serialsController.Revoke) } @@ -66,9 +66,9 @@ func SetupAPIRoutes(r *gin.RouterGroup) { { employeeSerialsRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Update) employeeSerialsRoutes.POST("/generate", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Generate) - employeeSerialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), employeeSerialsController.GenerateQRCode) + employeeSerialsRoutes.POST("/:serialNumber/qrcode", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.GenerateQRCode) employeeSerialsRoutes.GET("/:serialNumber/query", employeeSerialsController.Query) - employeeSerialsRoutes.GET("/", middleware.JWTAuthMiddleware(), employeeSerialsController.FindAll) + employeeSerialsRoutes.GET("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.FindAll) employeeSerialsRoutes.PUT("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Update) employeeSerialsRoutes.POST("/:serialNumber/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Revoke) employeeSerialsRoutes.DELETE("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), employeeSerialsController.Delete) @@ -76,14 +76,18 @@ func SetupAPIRoutes(r *gin.RouterGroup) { // 用户管理路由(仅管理员) usersController := controllers.NewUsersController() + employeesRoutes := r.Group("/employees") + { + employeesRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.Create) + employeesRoutes.GET("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.FindAll) + employeesRoutes.PATCH("/:id", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.Update) + employeesRoutes.POST("/:id/reset-password", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.ResetPassword) + employeesRoutes.DELETE("/:id", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.Delete) + } + usersRoutes := r.Group("/users") { usersRoutes.GET("/assignable", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), usersController.FindAssignable) - usersRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.Create) - usersRoutes.GET("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.FindAll) - usersRoutes.PATCH("/:id", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.Update) - usersRoutes.POST("/:id/reset-password", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.ResetPassword) - usersRoutes.DELETE("/:id", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.Delete) } // 售后工单路由 diff --git a/services/auth_service.go b/services/auth_service.go index c57a3de..0cd3338 100644 --- a/services/auth_service.go +++ b/services/auth_service.go @@ -23,6 +23,12 @@ func (s *AuthService) ValidateUser(username string, password string) (*models.Us if result.Error != nil { return nil, errors.New("用户名或密码不正确") } + if user.Role != "admin" && user.Role != "technician" { + return nil, errors.New("用户名或密码不正确") + } + if user.Password == "" { + return nil, errors.New("用户名或密码不正确") + } err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) if err != nil { @@ -56,12 +62,15 @@ func (s *AuthService) GetProfile(userId uint) (*models.UserDTO, error) { } return &models.UserDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Email: user.Email, - Role: user.Role, - CreatedAt: user.CreatedAt, + ID: user.ID, + Username: user.Username, + Name: user.Name, + Email: user.Email, + Phone: user.Phone, + EmployeeNo: user.EmployeeNo, + Position: user.Position, + Role: user.Role, + CreatedAt: user.CreatedAt, }, nil } @@ -109,11 +118,14 @@ func (s *AuthService) UpdateProfile(userId uint, name string, email string) (*mo } return &models.UserDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Email: user.Email, - Role: user.Role, - CreatedAt: user.CreatedAt, + ID: user.ID, + Username: user.Username, + Name: user.Name, + Email: user.Email, + Phone: user.Phone, + EmployeeNo: user.EmployeeNo, + Position: user.Position, + Role: user.Role, + CreatedAt: user.CreatedAt, }, nil } diff --git a/services/employees_service.go b/services/employees_service.go index 5cd5826..981b5e9 100644 --- a/services/employees_service.go +++ b/services/employees_service.go @@ -16,11 +16,14 @@ import ( "git.beifan.cn/trace-system/backend-go/database" "git.beifan.cn/trace-system/backend-go/models" + "gorm.io/gorm" ) // EmployeeSerialsService 员工序列号服务 type EmployeeSerialsService struct{} +const defaultEmployeeCompanyName = "内部员工" + // Generate 生成员工序列号 func (s *EmployeeSerialsService) Generate( companyName string, @@ -29,18 +32,62 @@ func (s *EmployeeSerialsService) Generate( quantity int, userId uint, serialPrefix string, +) ([]models.EmployeeSerial, error) { + return s.generateWithDB(database.DB, companyName, position, employeeName, quantity, userId, nil, serialPrefix) +} + +// GenerateForEmployee 为员工主档生成一个员工码 +func (s *EmployeeSerialsService) GenerateForEmployee( + tx *gorm.DB, + employee models.User, + creatorID uint, + serialPrefix string, +) (*models.EmployeeSerial, error) { + db := tx + if db == nil { + db = database.DB + } + + serials, err := s.generateWithDB( + db, + defaultEmployeeCompanyName, + employee.Position, + employee.Name, + 1, + creatorID, + &employee.ID, + serialPrefix, + ) + if err != nil { + return nil, err + } + if len(serials) == 0 { + return nil, errors.New("员工码生成失败") + } + return &serials[0], nil +} + +func (s *EmployeeSerialsService) generateWithDB( + db *gorm.DB, + companyName string, + position string, + employeeName string, + quantity int, + userId uint, + employeeID *uint, + serialPrefix string, ) ([]models.EmployeeSerial, error) { var serials []models.EmployeeSerial // 检查公司是否存在,不存在则创建 var company models.Company - result := database.DB.Where("company_name = ?", companyName).First(&company) + result := db.Where("company_name = ?", companyName).First(&company) if result.Error != nil { company = models.Company{ CompanyName: companyName, IsActive: true, } - result = database.DB.Create(&company) + result = db.Create(&company) if result.Error != nil { return nil, fmt.Errorf("创建公司失败: %w", result.Error) } @@ -68,7 +115,7 @@ func (s *EmployeeSerialsService) Generate( } var existingSerial models.EmployeeSerial - checkResult := database.DB.Where("serial_number = ?", serialNumber).First(&existingSerial) + checkResult := db.Where("serial_number = ?", serialNumber).First(&existingSerial) if checkResult.Error != nil { serialNumbers[serialNumber] = true i++ @@ -81,6 +128,7 @@ func (s *EmployeeSerialsService) Generate( CompanyName: companyName, Position: position, EmployeeName: employeeName, + EmployeeID: employeeID, CreatedBy: &userId, IsActive: true, } @@ -88,7 +136,7 @@ func (s *EmployeeSerialsService) Generate( } // 保存到数据库 - result = database.DB.Create(&serials) + result = db.Create(&serials) if result.Error != nil { return nil, fmt.Errorf("保存员工序列号失败: %w", result.Error) } diff --git a/services/services_test.go b/services/services_test.go index a90a6d5..91cc4f5 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -53,7 +53,7 @@ func TestAuthService_ValidateUser_Success(t *testing.T) { Password: string(password), Name: "测试用户", Email: "test@example.com", - Role: "user", + Role: "technician", } database.DB.Create(&user) @@ -75,7 +75,7 @@ func TestAuthService_ValidateUser_WrongPassword(t *testing.T) { Password: string(password), Name: "测试用户2", Email: "test2@example.com", - Role: "user", + Role: "technician", } database.DB.Create(&user) @@ -87,6 +87,25 @@ func TestAuthService_ValidateUser_WrongPassword(t *testing.T) { database.DB.Unscoped().Delete(&user) } +func TestAuthService_ValidateUser_EmployeeCannotLogin(t *testing.T) { + password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) + user := models.User{ + Username: "employee_no_login", + Password: string(password), + Name: "普通员工", + Role: "employee", + } + database.DB.Create(&user) + + authService := AuthService{} + _, err := authService.ValidateUser("employee_no_login", "password123") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "用户名或密码不正确") + + database.DB.Unscoped().Delete(&user) +} + func TestAuthService_ValidateUser_UserNotFound(t *testing.T) { authService := AuthService{} _, err := authService.ValidateUser("nonexistent", "password") @@ -98,7 +117,7 @@ func TestAuthService_GenerateToken_Success(t *testing.T) { user := &models.User{ ID: 1, Username: "testuser", - Role: "user", + Role: "technician", } authService := AuthService{} @@ -117,7 +136,7 @@ func TestAuthService_GetProfile_Success(t *testing.T) { Password: string(password), Name: "测试用户3", Email: "test3@example.com", - Role: "user", + Role: "technician", } database.DB.Create(&user) @@ -140,7 +159,7 @@ func TestAuthService_ChangePassword_Success(t *testing.T) { Password: string(password), Name: "测试用户4", Email: "test4@example.com", - Role: "user", + Role: "technician", } database.DB.Create(&user) @@ -165,7 +184,7 @@ func TestAuthService_ChangePassword_WrongCurrentPassword(t *testing.T) { Password: string(password), Name: "测试用户5", Email: "test5@example.com", - Role: "user", + Role: "technician", } database.DB.Create(&user) @@ -185,7 +204,7 @@ func TestAuthService_UpdateProfile_Success(t *testing.T) { Password: string(password), Name: "测试用户6", Email: "test6@example.com", - Role: "user", + Role: "technician", } database.DB.Create(&user) diff --git a/services/users_service.go b/services/users_service.go index a2d0bce..d93b201 100644 --- a/services/users_service.go +++ b/services/users_service.go @@ -17,18 +17,57 @@ type UsersService struct{} func toUserDTO(user models.User) models.UserDTO { return models.UserDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Email: user.Email, - Role: user.Role, - CreatedAt: user.CreatedAt, + ID: user.ID, + Username: user.Username, + Name: user.Name, + Email: user.Email, + Phone: user.Phone, + EmployeeNo: user.EmployeeNo, + Position: user.Position, + Role: user.Role, + CreatedAt: user.CreatedAt, + EmployeeSerials: user.EmployeeSerials, } } +func hasBackendAccess(role string) bool { + return role == "admin" || role == "technician" +} + +func isValidEmployeeRole(role string) bool { + return role == "admin" || role == "technician" || role == "employee" +} + // Create 创建用户 func (s *UsersService) Create(dto models.CreateUserDTO) (*models.UserDTO, error) { + name := strings.TrimSpace(dto.Name) + phone := strings.TrimSpace(dto.Phone) + employeeNo := strings.TrimSpace(dto.EmployeeNo) + position := strings.TrimSpace(dto.Position) + role := strings.TrimSpace(dto.Role) username := strings.TrimSpace(dto.Username) + if username == "" { + username = employeeNo + } + + if name == "" { + return nil, errors.New("姓名不能为空") + } + if phone == "" { + return nil, errors.New("电话不能为空") + } + if employeeNo == "" { + return nil, errors.New("工号不能为空") + } + if position == "" { + return nil, errors.New("岗位不能为空") + } + if !isValidEmployeeRole(role) { + return nil, errors.New("角色不正确") + } + if hasBackendAccess(role) && len(dto.Password) < 6 { + return nil, errors.New("管理员和技术员必须设置至少 6 位初始密码") + } var existing models.User err := database.DB.Where("username = ?", username).First(&existing).Error @@ -39,21 +78,48 @@ func (s *UsersService) Create(dto models.CreateUserDTO) (*models.UserDTO, error) return nil, fmt.Errorf("查询用户失败: %w", err) } - hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("密码加密失败: %w", err) + err = database.DB.Where("employee_no = ?", employeeNo).First(&existing).Error + if err == nil { + return nil, errors.New("工号已存在") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("查询员工工号失败: %w", err) + } + + hashed := "" + if hasBackendAccess(role) { + hashBytes, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("密码加密失败: %w", err) + } + hashed = string(hashBytes) } user := models.User{ - Username: username, - Password: string(hashed), - Name: dto.Name, - Email: dto.Email, - Role: dto.Role, + Username: username, + Password: hashed, + Name: name, + Email: dto.Email, + Phone: phone, + EmployeeNo: employeeNo, + Position: position, + Role: role, } - if err := database.DB.Create(&user).Error; err != nil { - return nil, fmt.Errorf("创建用户失败: %w", err) + err = database.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&user).Error; err != nil { + return fmt.Errorf("创建员工失败: %w", err) + } + + serialService := EmployeeSerialsService{} + if _, err := serialService.GenerateForEmployee(tx, user, user.ID, ""); err != nil { + return err + } + + return tx.Preload("EmployeeSerials").First(&user, user.ID).Error + }) + if err != nil { + return nil, err } dtoOut := toUserDTO(user) @@ -65,13 +131,14 @@ func (s *UsersService) FindAll(page int, limit int, role string, search string) var users []models.User var total int64 - db := database.DB.Model(&models.User{}) + db := database.DB.Model(&models.User{}).Preload("EmployeeSerials") if role != "" { db = db.Where("role = ?", role) } if search != "" { pattern := "%" + search + "%" - db = db.Where("username LIKE ? OR name LIKE ? OR email LIKE ?", pattern, pattern, pattern) + db = db.Where("username LIKE ? OR name LIKE ? OR email LIKE ? OR phone LIKE ? OR employee_no LIKE ? OR position LIKE ?", + pattern, pattern, pattern, pattern, pattern, pattern) } if err := db.Count(&total).Error; err != nil { @@ -114,12 +181,34 @@ func (s *UsersService) Update(userId uint, dto models.UpdateUserDTO, currentUser } if dto.Name != "" { - user.Name = dto.Name + user.Name = strings.TrimSpace(dto.Name) } if dto.Email != "" { user.Email = dto.Email } + if dto.Phone != "" { + user.Phone = strings.TrimSpace(dto.Phone) + } + if dto.EmployeeNo != "" { + employeeNo := strings.TrimSpace(dto.EmployeeNo) + var existing models.User + err := database.DB.Where("employee_no = ? AND id <> ?", employeeNo, userId).First(&existing).Error + if err == nil { + return nil, errors.New("工号已存在") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("查询员工工号失败: %w", err) + } + user.EmployeeNo = employeeNo + user.Username = employeeNo + } + if dto.Position != "" { + user.Position = strings.TrimSpace(dto.Position) + } if dto.Role != "" { + if !isValidEmployeeRole(dto.Role) { + return nil, errors.New("角色不正确") + } // 防止管理员把自己降级 if user.ID == currentUserId && user.Role == "admin" && dto.Role != "admin" { return nil, errors.New("不能修改自己的管理员角色") @@ -141,6 +230,9 @@ func (s *UsersService) ResetPassword(userId uint, newPassword string) error { if err := database.DB.First(&user, userId).Error; err != nil { return errors.New("用户不存在") } + if !hasBackendAccess(user.Role) { + return errors.New("员工无后台登录权限,不能设置密码") + } hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { diff --git a/services/users_service_test.go b/services/users_service_test.go index ebee5ee..5578e8e 100644 --- a/services/users_service_test.go +++ b/services/users_service_test.go @@ -13,18 +13,59 @@ import ( func TestUsersService_Create_Success(t *testing.T) { svc := UsersService{} dto, err := svc.Create(models.CreateUserDTO{ - Username: "users_create_ok", - Password: "password123", - Name: "新技术员", - Email: "new@example.com", - Role: "technician", + Password: "password123", + Name: "新技术员", + Email: "new@example.com", + Phone: "13800000001", + EmployeeNo: "users_create_ok", + Position: "技术员", + Role: "technician", }) assert.NoError(t, err) assert.NotNil(t, dto) assert.Equal(t, "users_create_ok", dto.Username) assert.Equal(t, "technician", dto.Role) + assert.Equal(t, "13800000001", dto.Phone) + assert.Equal(t, "users_create_ok", dto.EmployeeNo) + assert.Len(t, dto.EmployeeSerials, 1) database.DB.Unscoped().Where("username = ?", "users_create_ok").Delete(&models.User{}) + database.DB.Unscoped().Where("employee_name = ?", "新技术员").Delete(&models.EmployeeSerial{}) +} + +func TestUsersService_Create_EmployeeWithoutPasswordGeneratesSerial(t *testing.T) { + svc := UsersService{} + dto, err := svc.Create(models.CreateUserDTO{ + Name: "普通员工", + Phone: "13800000002", + EmployeeNo: "employee_no_pwd", + Position: "生产员工", + Role: "employee", + }) + assert.NoError(t, err) + assert.NotNil(t, dto) + assert.Equal(t, "employee", dto.Role) + assert.Len(t, dto.EmployeeSerials, 1) + + var user models.User + assert.NoError(t, database.DB.Where("employee_no = ?", "employee_no_pwd").First(&user).Error) + assert.Empty(t, user.Password) + + database.DB.Unscoped().Where("employee_id = ?", user.ID).Delete(&models.EmployeeSerial{}) + database.DB.Unscoped().Delete(&user) +} + +func TestUsersService_Create_BackendRoleRequiresPassword(t *testing.T) { + svc := UsersService{} + _, err := svc.Create(models.CreateUserDTO{ + Name: "无密码技术员", + Phone: "13800000003", + EmployeeNo: "tech_no_pwd", + Position: "技术员", + Role: "technician", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "必须设置") } func TestUsersService_Create_DuplicateUsername(t *testing.T) { @@ -39,10 +80,13 @@ func TestUsersService_Create_DuplicateUsername(t *testing.T) { svc := UsersService{} _, err := svc.Create(models.CreateUserDTO{ - Username: "users_create_dup", - Password: "password123", - Name: "duplicate", - Role: "technician", + Username: "users_create_dup", + Password: "password123", + Name: "duplicate", + Phone: "13800000004", + EmployeeNo: "users_create_dup_2", + Position: "技术员", + Role: "technician", }) assert.Error(t, err) assert.Contains(t, err.Error(), "用户名已存在") @@ -164,7 +208,7 @@ func TestUsersService_Delete_BlocksLastAdmin(t *testing.T) { func TestUsersService_FindAssignable_ReturnsAdminAndTechnician(t *testing.T) { a := models.User{Username: "assignable_admin", Password: "x", Name: "A", Role: "admin"} tech := models.User{Username: "assignable_tech", Password: "x", Name: "T", Role: "technician"} - plain := models.User{Username: "assignable_user", Password: "x", Name: "U", Role: "user"} + plain := models.User{Username: "assignable_user", Password: "x", Name: "U", Role: "employee"} database.DB.Create(&a) database.DB.Create(&tech) database.DB.Create(&plain) @@ -189,7 +233,7 @@ func TestUsersService_FindAssignable_ReturnsAdminAndTechnician(t *testing.T) { func TestUsersService_FindAll_FilterByRole(t *testing.T) { tech1 := models.User{Username: "findall_tech1", Password: "x", Name: "T1", Role: "technician"} tech2 := models.User{Username: "findall_tech2", Password: "x", Name: "T2", Role: "technician"} - user1 := models.User{Username: "findall_user1", Password: "x", Name: "U1", Role: "user"} + user1 := models.User{Username: "findall_user1", Password: "x", Name: "U1", Role: "employee"} database.DB.Create(&tech1) database.DB.Create(&tech2) database.DB.Create(&user1) diff --git a/tests/main_test.go b/tests/main_test.go index c4f40a5..ea6f014 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -73,11 +73,11 @@ func createTestUsers() { Role: "admin", }, { - Username: "user1", + Username: "employee1", Password: string(testPassword), - Name: "普通用户", - Email: "user1@example.com", - Role: "user", + Name: "普通员工", + Email: "employee1@example.com", + Role: "employee", }, }