diff --git a/AGENTS.md b/AGENTS.md index 44e049e..a4df84b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,21 +96,23 @@ backend-go/ - **Employee Permission Codes**: `POST /api/employee-serials/generate`, `POST /api/employee-serials/:serialNumber/qrcode`, `GET /api/employee-serials/:serialNumber/query`, `GET /api/employee-serials`, `PATCH /api/employee-serials/:serialNumber`, `PUT /api/employee-serials/:serialNumber`, `POST /api/employee-serials/:serialNumber/revoke`, `DELETE /api/employee-serials/:serialNumber` - **Product Traces**: `GET /api/product-traces/:serialNumber/query`, `POST /api/product-traces`, `GET /api/product-traces`, `GET /api/product-traces/:serialNumber`, `PATCH /api/product-traces/:serialNumber`, `POST /api/product-traces/:serialNumber/qrcode`, `POST /api/product-traces/:serialNumber/wechat-qrcode`, `POST /api/product-traces/:serialNumber/revoke`, `DELETE /api/product-traces/:serialNumber` - **Aftersales** (公开): `GET /api/aftersales/:serialNumber/query`, `POST /api/aftersales/:serialNumber/confirm` -- **Aftersales** (技术员+管理员): `POST /api/aftersales`, `GET /api/aftersales`, `GET /api/aftersales/:serialNumber`, `PATCH /api/aftersales/:serialNumber`, `POST /api/aftersales/:serialNumber/qrcode`, `POST /api/aftersales/:serialNumber/submit` -- **Aftersales** (仅管理员): `POST /api/aftersales/:serialNumber/reassign`, `POST /api/aftersales/:serialNumber/force-close`, `DELETE /api/aftersales/:serialNumber` +- **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`, `POST /api/aftersales/:serialNumber/reassign`, `POST /api/aftersales/:serialNumber/force-close`, `DELETE /api/aftersales/:serialNumber` - **Project Orders** (公开): `GET /api/project-orders/:serialNumber/query`, `POST /api/project-orders/:serialNumber/site-images`, `POST /api/project-orders/:serialNumber/complete` -- **Project Orders** (技术员+管理员): `POST /api/project-orders`, `GET /api/project-orders`, `GET /api/project-orders/:serialNumber`, `PATCH /api/project-orders/:serialNumber`, `POST /api/project-orders/:serialNumber/qrcode`, `POST /api/project-orders/:serialNumber/submit` -- **Project Orders** (仅管理员): `POST /api/project-orders/:serialNumber/reassign`, `POST /api/project-orders/:serialNumber/force-close`, `DELETE /api/project-orders/:serialNumber` -- **Users** (技术员+管理员): `GET /api/users/assignable` +- **Project Orders** (工单角色+管理员): `GET /api/project-orders`, `GET /api/project-orders/:serialNumber`, `PATCH /api/project-orders/:serialNumber`, `POST /api/project-orders/:serialNumber/qrcode`, `POST /api/project-orders/:serialNumber/submit` +- **Project Orders** (仅管理员): `POST /api/project-orders`, `POST /api/project-orders/:serialNumber/reassign`, `POST /api/project-orders/:serialNumber/force-close`, `DELETE /api/project-orders/:serialNumber` +- **Users** (仅管理员): `GET /api/users/assignable` - **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. +- `admin` is the system administrator role and has full backend access. +- Permission management may create/edit only four equal work-order roles: `software_engineer`, `hardware_engineer`, `business_manager`, `project_manager`. +- The four work-order roles can log in only to process aftersales/project orders assigned to themselves. +- The four work-order roles must not access dashboard, permission management, product traces, assignable user lists, creation/deletion/reassign/force-close endpoints, or other admin-only resources. +- `technician` is legacy-compatible and may keep work-order access, but must not be offered as a new role. +- `employee` is legacy/no backend login access and must not be offered as a new role. - Creating an employee through `/api/employees` creates employee master data and automatically generates one employee permission code bound by `employeeId`. -- `admin` / `technician` creation requires an initial password; `employee` creation must not require one. +- Creating managed work-order roles requires an initial password; creating `admin` or `employee` through permission management is forbidden. ### Business Boundaries - Enterprise/company-code management was removed. Do not reintroduce `/api/companies`, `/api/serials`, `CompaniesService`, `SerialsService`, or a company-management UI. @@ -248,7 +250,7 @@ After modifying Swagger annotations, run `make swagger`. ### Middleware - **JWTAuthMiddleware**: Validates JWT tokens, sets user in context - **AdminMiddleware**: Checks if user has admin role -- **TechnicianMiddleware**: Allows admin and technician roles (used for aftersales endpoints) +- **TechnicianMiddleware**: Allows admin, the four managed work-order roles, and legacy `technician` (used for work-order processing endpoints) - Access current user: `user, ok := GetCurrentUser(ctx)` ### Git Hooks diff --git a/README.md b/README.md index 1ee38f1..00a19a0 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ backend-go/ ├── logger/ # 日志管理 │ └── logger.go # 结构化日志(使用 Zap) ├── middleware/ # 中间件层 -│ └── auth.go # JWT 认证、管理员/技术员权限检查 +│ └── auth.go # JWT 认证、管理员/工单角色权限检查 ├── models/ # 数据模型和 DTO │ └── models.go # User、Company、EmployeeSerial、ProductTrace、AftersalesOrder、ProjectOrder 等模型定义 ├── routes/ # 路由配置 @@ -298,12 +298,12 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go | GET | `/api/project-orders/:serialNumber/query` | 公开查询项目工单 | 否 | 任何 | | POST | `/api/project-orders/:serialNumber/site-images` | 上传现场图片 | 否 | 任何 | | POST | `/api/project-orders/:serialNumber/complete` | 工程师提交完成 | 否 | 任何 | -| POST | `/api/project-orders` | 创建项目工单 | 是 | 管理员/技术员 | -| GET | `/api/project-orders` | 项目工单列表 | 是 | 管理员/技术员 | -| GET | `/api/project-orders/:serialNumber` | 项目工单详情 | 是 | 管理员/技术员 | -| PATCH | `/api/project-orders/:serialNumber` | 更新项目工单 | 是 | 管理员/技术员 | -| POST | `/api/project-orders/:serialNumber/qrcode` | 生成项目工单二维码 | 是 | 管理员/技术员 | -| POST | `/api/project-orders/:serialNumber/submit` | 后台提交项目完成 | 是 | 管理员/技术员 | +| POST | `/api/project-orders` | 创建项目工单 | 是 | 管理员 | +| GET | `/api/project-orders` | 项目工单列表 | 是 | 管理员/工单角色 | +| GET | `/api/project-orders/:serialNumber` | 项目工单详情 | 是 | 管理员/工单角色 | +| PATCH | `/api/project-orders/:serialNumber` | 更新项目工单 | 是 | 管理员/工单角色 | +| POST | `/api/project-orders/:serialNumber/qrcode` | 生成项目工单二维码 | 是 | 管理员/工单角色 | +| POST | `/api/project-orders/:serialNumber/submit` | 后台提交项目完成 | 是 | 管理员/工单角色 | | POST | `/api/project-orders/:serialNumber/reassign` | 工单分配 | 是 | 管理员 | | POST | `/api/project-orders/:serialNumber/force-close` | 强制完成 | 是 | 管理员 | | DELETE | `/api/project-orders/:serialNumber` | 删除项目工单 | 是 | 管理员 | @@ -313,6 +313,7 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go - 工单号格式: `zjbf-xm-YYMMDDNN` - 现场图片最多 18 张 - 仅需要工程师签名,无客户签字环节 +- 软件工程师、硬件工程师、商务经理、项目经理只能处理管理员分配给自己的项目工单 - 完成状态使用 `已完成` ### 售后工单 @@ -321,13 +322,13 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go | ------ | --------------------------------------------- | -------------------------- | -------- | --------------- | | GET | `/api/aftersales/:serialNumber/query` | 公开查询工单(脱敏) | 否 | 任何 | | POST | `/api/aftersales/:serialNumber/confirm` | 客户授权/未授权确认 | 否 | 任何 | -| POST | `/api/aftersales` | 创建售后工单 | 是 | 管理员/技术员 | -| GET | `/api/aftersales` | 工单列表(支持筛选) | 是 | 管理员/技术员 | -| GET | `/api/aftersales/:serialNumber` | 工单详情 | 是 | 管理员/技术员 | -| PATCH | `/api/aftersales/:serialNumber` | 更新工单(仅负责人或管理员)| 是 | 管理员/技术员 | -| POST | `/api/aftersales/:serialNumber/qrcode` | 生成工单二维码 | 是 | 管理员/技术员 | -| POST | `/api/aftersales/:serialNumber/submit` | 提交客户确认 | 是 | 管理员/技术员 | -| POST | `/api/aftersales/:serialNumber/reassign` | 工单分配(重新分配技术员) | 是 | 管理员 | +| POST | `/api/aftersales` | 创建售后工单 | 是 | 管理员 | +| GET | `/api/aftersales` | 工单列表(支持筛选) | 是 | 管理员/工单角色 | +| GET | `/api/aftersales/:serialNumber` | 工单详情 | 是 | 管理员/工单角色 | +| PATCH | `/api/aftersales/:serialNumber` | 更新工单(仅负责人或管理员)| 是 | 管理员/工单角色 | +| POST | `/api/aftersales/:serialNumber/qrcode` | 生成工单二维码 | 是 | 管理员/工单角色 | +| POST | `/api/aftersales/:serialNumber/submit` | 提交客户确认 | 是 | 管理员/工单角色 | +| POST | `/api/aftersales/:serialNumber/reassign` | 工单分配(重新分配工单负责人) | 是 | 管理员 | | POST | `/api/aftersales/:serialNumber/force-close` | 强制关闭工单 | 是 | 管理员 | | DELETE | `/api/aftersales/:serialNumber` | 删除工单 | 是 | 管理员 | @@ -336,6 +337,7 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go - 工单号格式: `zjbf-sh-YYMMDDNN`(年份后 2 位 + 月份 2 位 + 日期 2 位 + 当天序号至少 2 位,例:`zjbf-sh-26052801`) - 序号按天重置,软删除工单不释放编号(避免回收造成混淆) - 工单里的企业名称是售后客户信息,只保存在工单中 +- 软件工程师、硬件工程师、商务经理、项目经理只能处理管理员分配给自己的售后工单 - 二维码扫码后客户在网页签名(canvas)后点「已授权」确认;选择「未授权」需填写退回原因 - 签名以 PNG dataURL 形式持久化到工单(`signature` 字段),管理员详情页可查看留底 - 签名校验:必须为 `data:image/png;base64,` 或 `data:image/jpeg;base64,` 前缀,解码后 200B–500KB @@ -347,22 +349,24 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go | 方法 | 路径 | 描述 | 需要认证 | 角色 | | ------ | ----------------------------------- | -------------------------- | -------- | ------------- | -| GET | `/api/users/assignable` | 可分配用户列表(用于售后) | 是 | 管理员/技术员 | +| GET | `/api/users/assignable` | 可分配工单负责人列表 | 是 | 管理员 | | POST | `/api/employees` | 创建员工并自动生成员工码 | 是 | 管理员 | | GET | `/api/employees` | 员工列表(分页+筛选) | 是 | 管理员 | | PATCH | `/api/employees/:id` | 更新员工姓名/电话/工号/岗位/角色 | 是 | 管理员 | | POST | `/api/employees/:id/reset-password` | 重置后台账号密码 | 是 | 管理员 | | DELETE | `/api/employees/:id` | 删除员工 | 是 | 管理员 | -**员工角色**: -- `admin`:管理员,拥有全部后台权限,包括权限管理、产品溯源、工单分配、强制关闭和删除工单 -- `technician`:技术员,仅拥有工单模块权限,可创建/处理工单,可使用 `assignable` 查询可分配同事 -- `employee`:员工,无后台登录权限,不需要密码,仅用于员工主档和员工码查询 +**角色规则**: +- `admin`:系统管理员,拥有全部后台权限,包括权限管理、产品溯源、创建工单、工单分配、强制关闭和删除工单 +- 权限管理只能创建/编辑四个对等工单角色:`software_engineer`(软件工程师)、`hardware_engineer`(硬件工程师)、`business_manager`(商务经理)、`project_manager`(项目经理) +- 四个工单角色只能登录后台处理管理员分配给自己的项目工单和售后工单,不能访问控制台、权限管理、产品溯源等其他模块 +- `technician` 是旧数据兼容角色,不再作为新建选项 +- `employee` 是旧普通员工角色,无后台登录权限,不再作为新建选项 **保护规则**: - 不能删除自己;不能将自己的 admin 角色降级;不能删除最后一个 admin -- 创建 `admin` / `technician` 必须设置初始密码,密码 bcrypt 加密存储 -- 创建 `employee` 不要求密码,且不能登录后台 +- 创建四个工单角色必须设置初始密码,密码 bcrypt 加密存储 +- 不允许通过权限管理创建 `admin` 或 `employee` ## 测试 diff --git a/controllers/aftersales_controller.go b/controllers/aftersales_controller.go index 108fa59..c2c6e18 100644 --- a/controllers/aftersales_controller.go +++ b/controllers/aftersales_controller.go @@ -59,7 +59,7 @@ func (c *AftersalesController) Create(ctx *gin.Context) { // FindAll 获取售后工单列表 // @Summary 获取售后工单列表 -// @Description 支持分页、搜索、按状态/服务类型/技术员筛选 +// @Description 支持分页、搜索、按状态/服务类型/工单负责人筛选 // @Tags 售后工单 // @Produce json // @Security BearerAuth @@ -68,7 +68,7 @@ func (c *AftersalesController) Create(ctx *gin.Context) { // @Param search query string false "搜索关键词" // @Param workOrderStatus query string false "工单状态" // @Param serviceType query string false "服务类型" -// @Param technicianId query int false "技术员 ID" +// @Param technicianId query int false "工单负责人 ID" // @Param mine query bool false "仅查看自己负责的工单" // @Success 200 {object} models.PaginationResponse // @Failure 401 {object} models.ErrorResponse @@ -179,9 +179,9 @@ func (c *AftersalesController) Update(ctx *gin.Context) { }) } -// SubmitForConfirmation 技术员提交客户确认 +// SubmitForConfirmation 工单负责人提交客户确认 // @Summary 提交客户确认 -// @Description 技术员填写处理结果后提交,工单进入"待客户确认"状态 +// @Description 工单负责人填写处理结果后提交,工单进入"待客户确认"状态 // @Tags 售后工单 // @Accept json // @Produce json @@ -257,14 +257,14 @@ func (c *AftersalesController) GenerateQRCode(ctx *gin.Context) { }) } -// Reassign 重新分配技术员(仅管理员) -// @Summary 重新分配技术员 +// Reassign 重新分配工单负责人(仅管理员) +// @Summary 重新分配工单负责人 // @Tags 售后工单 // @Accept json // @Produce json // @Security BearerAuth // @Param serialNumber path string true "工单号" -// @Param data body models.ReassignAftersalesDTO true "新技术员 ID" +// @Param data body models.ReassignAftersalesDTO true "新工单负责人 ID" // @Success 200 {object} models.DataResponse // @Failure 400 {object} models.ErrorResponse // @Failure 401 {object} models.ErrorResponse diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go index 4a084b9..37f86ae 100644 --- a/controllers/auth_controller.go +++ b/controllers/auth_controller.go @@ -43,8 +43,8 @@ func (c *AuthController) Login(ctx *gin.Context) { ErrorResponse(ctx, http.StatusUnauthorized, err.Error()) return } - if user.Role == "employee" { - ErrorResponse(ctx, http.StatusForbidden, "员工账号无后台登录权限") + if !models.HasBackendAccess(user.Role) { + ErrorResponse(ctx, http.StatusForbidden, "该账号无后台登录权限") return } diff --git a/controllers/project_orders_controller.go b/controllers/project_orders_controller.go index 40a72c9..c756686 100644 --- a/controllers/project_orders_controller.go +++ b/controllers/project_orders_controller.go @@ -59,7 +59,7 @@ func (c *ProjectOrdersController) Create(ctx *gin.Context) { // FindAll 获取项目工单列表 // @Summary 获取项目工单列表 -// @Description 支持分页、搜索、按状态/服务类型/技术员筛选 +// @Description 支持分页、搜索、按状态/服务类型/工单负责人筛选 // @Tags 项目工单 // @Produce json // @Security BearerAuth @@ -68,7 +68,7 @@ func (c *ProjectOrdersController) Create(ctx *gin.Context) { // @Param search query string false "搜索关键词" // @Param workOrderStatus query string false "工单状态" // @Param serviceType query string false "服务类型" -// @Param technicianId query int false "技术员 ID" +// @Param technicianId query int false "工单负责人 ID" // @Param mine query bool false "仅查看自己负责的工单" // @Success 200 {object} models.PaginationResponse // @Failure 401 {object} models.ErrorResponse @@ -179,9 +179,9 @@ func (c *ProjectOrdersController) Update(ctx *gin.Context) { }) } -// SubmitCompletion 技术员提交完成资料 +// SubmitCompletion 工单负责人提交完成资料 // @Summary 提交完成确认 -// @Description 技术员填写处理结果后提交,工单进入"待完成确认"状态 +// @Description 工单负责人填写处理结果后提交,工单进入"待完成确认"状态 // @Tags 项目工单 // @Accept json // @Produce json @@ -257,14 +257,14 @@ func (c *ProjectOrdersController) GenerateQRCode(ctx *gin.Context) { }) } -// Reassign 重新分配技术员(仅管理员) -// @Summary 重新分配技术员 +// Reassign 重新分配工单负责人(仅管理员) +// @Summary 重新分配工单负责人 // @Tags 项目工单 // @Accept json // @Produce json // @Security BearerAuth // @Param serialNumber path string true "工单号" -// @Param data body models.ReassignProjectOrderDTO true "新技术员 ID" +// @Param data body models.ReassignProjectOrderDTO true "新工单负责人 ID" // @Success 200 {object} models.DataResponse // @Failure 400 {object} models.ErrorResponse // @Failure 401 {object} models.ErrorResponse diff --git a/controllers/users_controller.go b/controllers/users_controller.go index 1983dc3..e646a49 100644 --- a/controllers/users_controller.go +++ b/controllers/users_controller.go @@ -87,7 +87,7 @@ func (c *UsersController) FindAll(ctx *gin.Context) { // FindAssignable 获取可分配的用户(admin + technician) // @Summary 获取可分配用户列表 -// @Description 用于售后工单分配选择技术员/管理员,无需分页 +// @Description 用于工单分配选择可派单人员,无需分页 // @Tags 用户管理 // @Produce json // @Security BearerAuth diff --git a/docs/docs.go b/docs/docs.go index 4c9270a..2f98681 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -31,7 +31,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "支持分页、搜索、按状态/服务类型/技术员筛选", + "description": "支持分页、搜索、按状态/服务类型/工单负责人筛选", "produces": [ "application/json" ], @@ -72,7 +72,7 @@ const docTemplate = `{ }, { "type": "integer", - "description": "技术员 ID", + "description": "工单负责人 ID", "name": "technicianId", "in": "query" }, @@ -519,7 +519,7 @@ const docTemplate = `{ "tags": [ "售后工单" ], - "summary": "重新分配技术员", + "summary": "重新分配工单负责人", "parameters": [ { "type": "string", @@ -529,7 +529,7 @@ const docTemplate = `{ "required": true }, { - "description": "新技术员 ID", + "description": "新工单负责人 ID", "name": "data", "in": "body", "required": true, @@ -617,7 +617,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "技术员填写处理结果后提交,工单进入\"待客户确认\"状态", + "description": "工单负责人填写处理结果后提交,工单进入\"待客户确认\"状态", "consumes": [ "application/json" ], @@ -1558,7 +1558,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "支持分页、搜索、按状态/服务类型/技术员筛选", + "description": "支持分页、搜索、按状态/服务类型/工单负责人筛选", "produces": [ "application/json" ], @@ -1599,7 +1599,7 @@ const docTemplate = `{ }, { "type": "integer", - "description": "技术员 ID", + "description": "工单负责人 ID", "name": "technicianId", "in": "query" }, @@ -2046,7 +2046,7 @@ const docTemplate = `{ "tags": [ "项目工单" ], - "summary": "重新分配技术员", + "summary": "重新分配工单负责人", "parameters": [ { "type": "string", @@ -2056,7 +2056,7 @@ const docTemplate = `{ "required": true }, { - "description": "新技术员 ID", + "description": "新工单负责人 ID", "name": "data", "in": "body", "required": true, @@ -2144,7 +2144,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "技术员填写处理结果后提交,工单进入\"待完成确认\"状态", + "description": "工单负责人填写处理结果后提交,工单进入\"待完成确认\"状态", "consumes": [ "application/json" ], @@ -2202,7 +2202,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "用于售后工单分配选择技术员/管理员,无需分页", + "description": "用于工单分配选择可派单人员,无需分页", "produces": [ "application/json" ], @@ -2394,9 +2394,10 @@ const docTemplate = `{ "role": { "type": "string", "enum": [ - "admin", - "technician", - "employee" + "software_engineer", + "hardware_engineer", + "business_manager", + "project_manager" ] }, "username": { @@ -2774,9 +2775,10 @@ const docTemplate = `{ "role": { "type": "string", "enum": [ - "admin", - "technician", - "employee" + "software_engineer", + "hardware_engineer", + "business_manager", + "project_manager" ] } } diff --git a/docs/swagger.json b/docs/swagger.json index 8e6609d..cb718d6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -25,7 +25,7 @@ "BearerAuth": [] } ], - "description": "支持分页、搜索、按状态/服务类型/技术员筛选", + "description": "支持分页、搜索、按状态/服务类型/工单负责人筛选", "produces": [ "application/json" ], @@ -66,7 +66,7 @@ }, { "type": "integer", - "description": "技术员 ID", + "description": "工单负责人 ID", "name": "technicianId", "in": "query" }, @@ -513,7 +513,7 @@ "tags": [ "售后工单" ], - "summary": "重新分配技术员", + "summary": "重新分配工单负责人", "parameters": [ { "type": "string", @@ -523,7 +523,7 @@ "required": true }, { - "description": "新技术员 ID", + "description": "新工单负责人 ID", "name": "data", "in": "body", "required": true, @@ -611,7 +611,7 @@ "BearerAuth": [] } ], - "description": "技术员填写处理结果后提交,工单进入\"待客户确认\"状态", + "description": "工单负责人填写处理结果后提交,工单进入\"待客户确认\"状态", "consumes": [ "application/json" ], @@ -1552,7 +1552,7 @@ "BearerAuth": [] } ], - "description": "支持分页、搜索、按状态/服务类型/技术员筛选", + "description": "支持分页、搜索、按状态/服务类型/工单负责人筛选", "produces": [ "application/json" ], @@ -1593,7 +1593,7 @@ }, { "type": "integer", - "description": "技术员 ID", + "description": "工单负责人 ID", "name": "technicianId", "in": "query" }, @@ -2040,7 +2040,7 @@ "tags": [ "项目工单" ], - "summary": "重新分配技术员", + "summary": "重新分配工单负责人", "parameters": [ { "type": "string", @@ -2050,7 +2050,7 @@ "required": true }, { - "description": "新技术员 ID", + "description": "新工单负责人 ID", "name": "data", "in": "body", "required": true, @@ -2138,7 +2138,7 @@ "BearerAuth": [] } ], - "description": "技术员填写处理结果后提交,工单进入\"待完成确认\"状态", + "description": "工单负责人填写处理结果后提交,工单进入\"待完成确认\"状态", "consumes": [ "application/json" ], @@ -2196,7 +2196,7 @@ "BearerAuth": [] } ], - "description": "用于售后工单分配选择技术员/管理员,无需分页", + "description": "用于工单分配选择可派单人员,无需分页", "produces": [ "application/json" ], @@ -2388,9 +2388,10 @@ "role": { "type": "string", "enum": [ - "admin", - "technician", - "employee" + "software_engineer", + "hardware_engineer", + "business_manager", + "project_manager" ] }, "username": { @@ -2768,9 +2769,10 @@ "role": { "type": "string", "enum": [ - "admin", - "technician", - "employee" + "software_engineer", + "hardware_engineer", + "business_manager", + "project_manager" ] } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6d785c3..ef220c0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -110,9 +110,10 @@ definitions: type: string role: enum: - - admin - - technician - - employee + - software_engineer + - hardware_engineer + - business_manager + - project_manager type: string username: type: string @@ -368,9 +369,10 @@ definitions: type: string role: enum: - - admin - - technician - - employee + - software_engineer + - hardware_engineer + - business_manager + - project_manager type: string type: object models.User: @@ -441,7 +443,7 @@ info: paths: /aftersales: get: - description: 支持分页、搜索、按状态/服务类型/技术员筛选 + description: 支持分页、搜索、按状态/服务类型/工单负责人筛选 parameters: - description: 页码 in: query @@ -463,7 +465,7 @@ paths: in: query name: serviceType type: string - - description: 技术员 ID + - description: 工单负责人 ID in: query name: technicianId type: integer @@ -754,7 +756,7 @@ paths: name: serialNumber required: true type: string - - description: 新技术员 ID + - description: 新工单负责人 ID in: body name: data required: true @@ -777,7 +779,7 @@ paths: $ref: '#/definitions/models.ErrorResponse' security: - BearerAuth: [] - summary: 重新分配技术员 + summary: 重新分配工单负责人 tags: - 售后工单 /aftersales/{serialNumber}/site-images: @@ -817,7 +819,7 @@ paths: post: consumes: - application/json - description: 技术员填写处理结果后提交,工单进入"待客户确认"状态 + description: 工单负责人填写处理结果后提交,工单进入"待客户确认"状态 parameters: - description: 工单号 in: path @@ -1412,7 +1414,7 @@ paths: - 员工管理 /project-orders: get: - description: 支持分页、搜索、按状态/服务类型/技术员筛选 + description: 支持分页、搜索、按状态/服务类型/工单负责人筛选 parameters: - description: 页码 in: query @@ -1434,7 +1436,7 @@ paths: in: query name: serviceType type: string - - description: 技术员 ID + - description: 工单负责人 ID in: query name: technicianId type: integer @@ -1725,7 +1727,7 @@ paths: name: serialNumber required: true type: string - - description: 新技术员 ID + - description: 新工单负责人 ID in: body name: data required: true @@ -1748,7 +1750,7 @@ paths: $ref: '#/definitions/models.ErrorResponse' security: - BearerAuth: [] - summary: 重新分配技术员 + summary: 重新分配工单负责人 tags: - 项目工单 /project-orders/{serialNumber}/site-images: @@ -1788,7 +1790,7 @@ paths: post: consumes: - application/json - description: 技术员填写处理结果后提交,工单进入"待完成确认"状态 + description: 工单负责人填写处理结果后提交,工单进入"待完成确认"状态 parameters: - description: 工单号 in: path @@ -1823,7 +1825,7 @@ paths: - 项目工单 /users/assignable: get: - description: 用于售后工单分配选择技术员/管理员,无需分页 + description: 用于工单分配选择可派单人员,无需分页 produces: - application/json responses: diff --git a/middleware/auth.go b/middleware/auth.go index d71b644..fc6137d 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -111,7 +111,7 @@ func AdminMiddleware() gin.HandlerFunc { } } -// TechnicianMiddleware 技术员权限中间件(放行 admin 和 technician) +// TechnicianMiddleware 工单处理权限中间件(放行 admin 和可派单角色) func TechnicianMiddleware() gin.HandlerFunc { return func(c *gin.Context) { user, exists := c.Get("user") @@ -123,7 +123,7 @@ func TechnicianMiddleware() gin.HandlerFunc { } userModel := user.(models.User) - if userModel.Role != "admin" && userModel.Role != "technician" { + if !models.HasWorkOrderAccess(userModel.Role) { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "message": "无权限访问此资源", }) diff --git a/models/models.go b/models/models.go index 9ce4d52..f142142 100644 --- a/models/models.go +++ b/models/models.go @@ -6,6 +6,53 @@ import ( "gorm.io/gorm" ) +const ( + RoleAdmin = "admin" + RoleTechnicianLegacy = "technician" + RoleEmployee = "employee" + RoleSoftwareEngineer = "software_engineer" + RoleHardwareEngineer = "hardware_engineer" + RoleBusinessManager = "business_manager" + RoleProjectManager = "project_manager" +) + +// WorkOrderRoles 是权限管理中可创建/编辑的四个对等角色。 +var WorkOrderRoles = []string{ + RoleSoftwareEngineer, + RoleHardwareEngineer, + RoleBusinessManager, + RoleProjectManager, +} + +// AssignableWorkOrderRoles 是可被派单的角色,包含旧 technician 数据兼容。 +var AssignableWorkOrderRoles = append([]string{}, append(WorkOrderRoles, RoleTechnicianLegacy)...) + +func IsWorkOrderRole(role string) bool { + for _, item := range WorkOrderRoles { + if role == item { + return true + } + } + return false +} + +func IsAssignableWorkOrderRole(role string) bool { + for _, item := range AssignableWorkOrderRoles { + if role == item { + return true + } + } + return false +} + +func HasBackendAccess(role string) bool { + return role == RoleAdmin || IsAssignableWorkOrderRole(role) +} + +func HasWorkOrderAccess(role string) bool { + return role == RoleAdmin || IsAssignableWorkOrderRole(role) +} + // User 模型 type User struct { ID uint `gorm:"primaryKey" json:"id"` @@ -94,7 +141,7 @@ type CreateUserDTO struct { 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"` + Role string `json:"role" validate:"required,oneof=software_engineer hardware_engineer business_manager project_manager"` } // UpdateUserDTO 管理员更新用户信息请求 @@ -104,7 +151,7 @@ type UpdateUserDTO struct { 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"` + Role string `json:"role,omitempty" validate:"omitempty,oneof=software_engineer hardware_engineer business_manager project_manager"` } // AdminResetPasswordDTO 管理员重置用户密码 @@ -295,7 +342,7 @@ type CustomerConfirmDTO struct { RejectReason string `json:"rejectReason,omitempty" validate:"required_if=Action reject"` } -// ReassignAftersalesDTO 重新分配技术员请求 +// ReassignAftersalesDTO 重新分配工单负责人请求 type ReassignAftersalesDTO struct { TechnicianID uint `json:"technicianId" validate:"required"` } diff --git a/routes/routes.go b/routes/routes.go index 93a6d61..6e252c8 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -80,7 +80,7 @@ func SetupAPIRoutes(r *gin.RouterGroup) { usersRoutes := r.Group("/users") { - usersRoutes.GET("/assignable", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), usersController.FindAssignable) + usersRoutes.GET("/assignable", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), usersController.FindAssignable) } // 售后工单路由 @@ -93,7 +93,7 @@ func SetupAPIRoutes(r *gin.RouterGroup) { aftersalesRoutes.POST("/:serialNumber/confirm", aftersalesController.CustomerConfirm) // 技术员 + 管理员 - aftersalesRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), aftersalesController.Create) + aftersalesRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), aftersalesController.Create) aftersalesRoutes.GET("", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), aftersalesController.FindAll) aftersalesRoutes.GET("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), aftersalesController.FindOne) aftersalesRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), aftersalesController.Update) @@ -116,7 +116,7 @@ func SetupAPIRoutes(r *gin.RouterGroup) { projectOrdersRoutes.POST("/:serialNumber/complete", projectOrdersController.EngineerComplete) // 技术员 + 管理员 - projectOrdersRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.Create) + projectOrdersRoutes.POST("", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), projectOrdersController.Create) projectOrdersRoutes.GET("", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.FindAll) projectOrdersRoutes.GET("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.FindOne) projectOrdersRoutes.PATCH("/:serialNumber", middleware.JWTAuthMiddleware(), middleware.TechnicianMiddleware(), projectOrdersController.Update) diff --git a/services/aftersales_service.go b/services/aftersales_service.go index ca64a03..987786f 100644 --- a/services/aftersales_service.go +++ b/services/aftersales_service.go @@ -334,7 +334,7 @@ func (s *AftersalesService) Update( return &order, nil } -// SubmitForConfirmation 技术员提交客户确认 +// SubmitForConfirmation 工单负责人提交客户确认 func (s *AftersalesService) SubmitForConfirmation( serialNumber string, dto models.SubmitForConfirmationDTO, @@ -539,7 +539,7 @@ func (s *AftersalesService) CustomerConfirm(serialNumber string, dto models.Cust return s.PublicQuery(normalized) } -// Reassign 重新分配技术员(仅管理员) +// Reassign 重新分配工单负责人(仅管理员) func (s *AftersalesService) Reassign(serialNumber string, technicianID uint) (*models.AftersalesOrder, error) { var order models.AftersalesOrder result := database.DB.Where("serial_number = ?", normalizeAftersalesSerial(serialNumber)).First(&order) @@ -553,15 +553,15 @@ func (s *AftersalesService) Reassign(serialNumber string, technicianID uint) (*m var technician models.User if err := database.DB.First(&technician, technicianID).Error; err != nil { - return nil, errors.New("指定的技术员不存在") + return nil, errors.New("指定的工单负责人不存在") } - if technician.Role != "admin" && technician.Role != "technician" { - return nil, errors.New("指定的用户不是技术员或管理员") + if !models.IsAssignableWorkOrderRole(technician.Role) { + return nil, errors.New("指定的用户不是可派单人员") } order.TechnicianID = &technicianID if err := database.DB.Save(&order).Error; err != nil { - return nil, fmt.Errorf("重新分配技术员失败: %w", err) + return nil, fmt.Errorf("重新分配工单负责人失败: %w", err) } _ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order) diff --git a/services/auth_service.go b/services/auth_service.go index 0cd3338..8385130 100644 --- a/services/auth_service.go +++ b/services/auth_service.go @@ -23,7 +23,7 @@ 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" { + if !models.HasBackendAccess(user.Role) { return nil, errors.New("用户名或密码不正确") } if user.Password == "" { diff --git a/services/project_orders_service.go b/services/project_orders_service.go index b5cc63e..1cd63ab 100644 --- a/services/project_orders_service.go +++ b/services/project_orders_service.go @@ -213,7 +213,7 @@ func (s *ProjectOrdersService) Update( return &order, nil } -// SubmitCompletion 技术员提交完成资料,进入待完成确认状态 +// SubmitCompletion 工单负责人提交完成资料,进入待完成确认状态 func (s *ProjectOrdersService) SubmitCompletion( serialNumber string, dto models.SubmitProjectCompletionDTO, @@ -395,7 +395,7 @@ func (s *ProjectOrdersService) EngineerComplete(serialNumber string, dto models. return s.PublicQuery(normalized) } -// Reassign 重新分配技术员(仅管理员) +// Reassign 重新分配工单负责人(仅管理员) func (s *ProjectOrdersService) Reassign(serialNumber string, technicianID uint) (*models.ProjectOrder, error) { var order models.ProjectOrder result := database.DB.Where("serial_number = ?", normalizeProjectOrderSerial(serialNumber)).First(&order) @@ -409,15 +409,15 @@ func (s *ProjectOrdersService) Reassign(serialNumber string, technicianID uint) var technician models.User if err := database.DB.First(&technician, technicianID).Error; err != nil { - return nil, errors.New("指定的技术员不存在") + return nil, errors.New("指定的工单负责人不存在") } - if technician.Role != "admin" && technician.Role != "technician" { - return nil, errors.New("指定的用户不是技术员或管理员") + if !models.IsAssignableWorkOrderRole(technician.Role) { + return nil, errors.New("指定的用户不是可派单人员") } order.TechnicianID = &technicianID if err := database.DB.Save(&order).Error; err != nil { - return nil, fmt.Errorf("重新分配技术员失败: %w", err) + return nil, fmt.Errorf("重新分配工单负责人失败: %w", err) } _ = database.DB.Preload("Technician").Preload("Creator").Where("serial_number = ?", order.SerialNumber).First(&order) diff --git a/services/users_service.go b/services/users_service.go index d93b201..1bc61df 100644 --- a/services/users_service.go +++ b/services/users_service.go @@ -31,11 +31,11 @@ func toUserDTO(user models.User) models.UserDTO { } func hasBackendAccess(role string) bool { - return role == "admin" || role == "technician" + return models.HasBackendAccess(role) } -func isValidEmployeeRole(role string) bool { - return role == "admin" || role == "technician" || role == "employee" +func isValidManagedRole(role string) bool { + return models.IsWorkOrderRole(role) } // Create 创建用户 @@ -62,11 +62,11 @@ func (s *UsersService) Create(dto models.CreateUserDTO) (*models.UserDTO, error) if position == "" { return nil, errors.New("岗位不能为空") } - if !isValidEmployeeRole(role) { - return nil, errors.New("角色不正确") + if !isValidManagedRole(role) { + return nil, errors.New("权限管理只能创建软件工程师、硬件工程师、商务经理、项目经理") } if hasBackendAccess(role) && len(dto.Password) < 6 { - return nil, errors.New("管理员和技术员必须设置至少 6 位初始密码") + return nil, errors.New("工单处理账号必须设置至少 6 位初始密码") } var existing models.User @@ -133,7 +133,12 @@ func (s *UsersService) FindAll(page int, limit int, role string, search string) db := database.DB.Model(&models.User{}).Preload("EmployeeSerials") if role != "" { + if !models.IsAssignableWorkOrderRole(role) { + return []models.UserDTO{}, 0, 0, nil + } db = db.Where("role = ?", role) + } else { + db = db.Where("role IN ?", models.AssignableWorkOrderRoles) } if search != "" { pattern := "%" + search + "%" @@ -159,11 +164,11 @@ func (s *UsersService) FindAll(page int, limit int, role string, search string) return result, int(total), totalPages, nil } -// FindAssignable 获取可分配的用户(admin + technician),用于售后工单分配 +// FindAssignable 获取可分配的工单处理人员,用于工单分配 func (s *UsersService) FindAssignable() ([]models.UserDTO, error) { var users []models.User - if err := database.DB.Where("role IN ?", []string{"admin", "technician"}). - Order("role DESC, created_at ASC").Find(&users).Error; err != nil { + if err := database.DB.Where("role IN ?", models.AssignableWorkOrderRoles). + Order("created_at ASC").Find(&users).Error; err != nil { return nil, fmt.Errorf("查询可分配用户失败: %w", err) } result := make([]models.UserDTO, 0, len(users)) @@ -206,8 +211,11 @@ func (s *UsersService) Update(userId uint, dto models.UpdateUserDTO, currentUser user.Position = strings.TrimSpace(dto.Position) } if dto.Role != "" { - if !isValidEmployeeRole(dto.Role) { - return nil, errors.New("角色不正确") + if !isValidManagedRole(dto.Role) { + return nil, errors.New("权限管理只能设置软件工程师、硬件工程师、商务经理、项目经理") + } + if user.Role == "admin" { + return nil, errors.New("不能通过权限管理修改管理员角色") } // 防止管理员把自己降级 if user.ID == currentUserId && user.Role == "admin" && dto.Role != "admin" { @@ -231,7 +239,7 @@ func (s *UsersService) ResetPassword(userId uint, newPassword string) error { return errors.New("用户不存在") } if !hasBackendAccess(user.Role) { - return errors.New("员工无后台登录权限,不能设置密码") + return errors.New("该账号无后台登录权限,不能设置密码") } hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) diff --git a/services/users_service_test.go b/services/users_service_test.go index adec33b..f9ed484 100644 --- a/services/users_service_test.go +++ b/services/users_service_test.go @@ -18,13 +18,13 @@ func TestUsersService_Create_Success(t *testing.T) { Email: "new@example.com", Phone: "13800000001", EmployeeNo: "users_create_ok", - Position: "技术员", - Role: "technician", + Position: "软件工程师", + Role: "software_engineer", }) 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, "software_engineer", dto.Role) assert.Equal(t, "13800000001", dto.Phone) assert.Equal(t, "users_create_ok", dto.EmployeeNo) assert.Len(t, dto.EmployeeSerials, 1) @@ -33,35 +33,17 @@ func TestUsersService_Create_Success(t *testing.T) { database.DB.Unscoped().Where("employee_name = ?", "新技术员").Delete(&models.EmployeeSerial{}) } -func TestUsersService_Create_EmployeeWithoutPasswordGeneratesSerial(t *testing.T) { +func TestUsersService_Create_BlocksEmployeeRole(t *testing.T) { svc := UsersService{} - dto, err := svc.Create(models.CreateUserDTO{ + _, 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) - - serialService := EmployeeSerialsService{} - serial, err := serialService.Query(dto.EmployeeSerials[0].SerialNumber) - assert.NoError(t, err) - assert.NotNil(t, serial.Employee) - assert.Equal(t, "普通员工", serial.Employee.Name) - assert.Equal(t, "13800000002", serial.Employee.Phone) - assert.Equal(t, "employee_no_pwd", serial.Employee.EmployeeNo) - assert.Equal(t, "生产员工", serial.Employee.Position) - - database.DB.Unscoped().Where("employee_id = ?", user.ID).Delete(&models.EmployeeSerial{}) - database.DB.Unscoped().Delete(&user) + assert.Error(t, err) + assert.Contains(t, err.Error(), "权限管理只能创建") } func TestUsersService_Create_BackendRoleRequiresPassword(t *testing.T) { @@ -70,8 +52,8 @@ func TestUsersService_Create_BackendRoleRequiresPassword(t *testing.T) { Name: "无密码技术员", Phone: "13800000003", EmployeeNo: "tech_no_pwd", - Position: "技术员", - Role: "technician", + Position: "软件工程师", + Role: "software_engineer", }) assert.Error(t, err) assert.Contains(t, err.Error(), "必须设置") @@ -82,7 +64,7 @@ func TestUsersService_Create_DuplicateUsername(t *testing.T) { Username: "users_create_dup", Password: "x", Name: "existing", - Role: "technician", + Role: "software_engineer", } database.DB.Create(&user) defer database.DB.Unscoped().Delete(&user) @@ -94,8 +76,8 @@ func TestUsersService_Create_DuplicateUsername(t *testing.T) { Name: "duplicate", Phone: "13800000004", EmployeeNo: "users_create_dup_2", - Position: "技术员", - Role: "technician", + Position: "软件工程师", + Role: "software_engineer", }) assert.Error(t, err) assert.Contains(t, err.Error(), "用户名已存在") @@ -113,10 +95,10 @@ func TestUsersService_Update_BlocksSelfDemotion(t *testing.T) { svc := UsersService{} _, err := svc.Update(admin.ID, models.UpdateUserDTO{ - Role: "technician", + Role: "software_engineer", }, admin.ID) assert.Error(t, err) - assert.Contains(t, err.Error(), "不能修改自己的管理员角色") + assert.Contains(t, err.Error(), "不能通过权限管理修改管理员角色") } func TestUsersService_Update_AllowsRoleChangeForOthers(t *testing.T) { @@ -130,7 +112,7 @@ func TestUsersService_Update_AllowsRoleChangeForOthers(t *testing.T) { Username: "users_update_target", Password: "x", Name: "target", - Role: "technician", + Role: "software_engineer", } database.DB.Create(¤tAdmin) database.DB.Create(&target) @@ -140,11 +122,11 @@ func TestUsersService_Update_AllowsRoleChangeForOthers(t *testing.T) { svc := UsersService{} updated, err := svc.Update(target.ID, models.UpdateUserDTO{ Name: "新名字", - Role: "admin", + Role: "project_manager", }, currentAdmin.ID) assert.NoError(t, err) assert.Equal(t, "新名字", updated.Name) - assert.Equal(t, "admin", updated.Role) + assert.Equal(t, "project_manager", updated.Role) } func TestUsersService_ResetPassword_ChangesHash(t *testing.T) { @@ -153,7 +135,7 @@ func TestUsersService_ResetPassword_ChangesHash(t *testing.T) { Username: "users_reset_pwd", Password: string(hashed), Name: "reset", - Role: "technician", + Role: "software_engineer", } database.DB.Create(&user) defer database.DB.Unscoped().Delete(&user) @@ -214,15 +196,18 @@ func TestUsersService_Delete_BlocksLastAdmin(t *testing.T) { database.DB.Unscoped().Delete(&last) } -func TestUsersService_FindAssignable_ReturnsAdminAndTechnician(t *testing.T) { +func TestUsersService_FindAssignable_ReturnsWorkOrderRoles(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"} + software := models.User{Username: "assignable_software", Password: "x", Name: "S", Role: "software_engineer"} plain := models.User{Username: "assignable_user", Password: "x", Name: "U", Role: "employee"} database.DB.Create(&a) database.DB.Create(&tech) + database.DB.Create(&software) database.DB.Create(&plain) defer database.DB.Unscoped().Delete(&a) defer database.DB.Unscoped().Delete(&tech) + defer database.DB.Unscoped().Delete(&software) defer database.DB.Unscoped().Delete(&plain) svc := UsersService{} @@ -233,15 +218,17 @@ func TestUsersService_FindAssignable_ReturnsAdminAndTechnician(t *testing.T) { for _, u := range users { usernames[u.Username] = u.Role } - assert.Equal(t, "admin", usernames["assignable_admin"]) assert.Equal(t, "technician", usernames["assignable_tech"]) + assert.Equal(t, "software_engineer", usernames["assignable_software"]) + _, hasAdmin := usernames["assignable_admin"] + assert.False(t, hasAdmin, "admin should not be assignable") _, hasPlain := usernames["assignable_user"] assert.False(t, hasPlain, "plain user should not be assignable") } 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"} + tech1 := models.User{Username: "findall_tech1", Password: "x", Name: "T1", Role: "software_engineer"} + tech2 := models.User{Username: "findall_tech2", Password: "x", Name: "T2", Role: "software_engineer"} user1 := models.User{Username: "findall_user1", Password: "x", Name: "U1", Role: "employee"} database.DB.Create(&tech1) database.DB.Create(&tech2) @@ -251,9 +238,9 @@ func TestUsersService_FindAll_FilterByRole(t *testing.T) { defer database.DB.Unscoped().Delete(&user1) svc := UsersService{} - results, _, _, err := svc.FindAll(1, 50, "technician", "") + results, _, _, err := svc.FindAll(1, 50, "software_engineer", "") assert.NoError(t, err) for _, u := range results { - assert.Equal(t, "technician", u.Role) + assert.Equal(t, "software_engineer", u.Role) } }