diff --git a/AGENTS.md b/AGENTS.md index 4532c0c..44e049e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ make test # Run all integration tests (./test go test -v ./services/... # Run service layer unit tests go test -v ./tests/... # Run integration tests go test -v -run TestAuthService_ValidateUser_Success ./services/... # Single test -go test -v ./services/... -run TestSerialsService # Run test prefix +go test -v ./services/... -run TestEmployeeSerialsService # Run test prefix make test-coverage # Generate coverage report ``` @@ -46,10 +46,11 @@ backend-go/ ├── controllers/ # HTTP request handlers │ ├── aftersales_controller.go # Aftersales orders: create, query, submit, confirm, qrcode │ ├── auth_controller.go # Auth: login, profile, password change -│ ├── companies_controller.go # Company CRUD -│ ├── employees_controller.go # Employee serials: generate, query, update, revoke, qrcode +│ ├── dashboard_controller.go # Dashboard work-order statistics +│ ├── employees_controller.go # Employee permission codes: generate, query, update, revoke, qrcode │ ├── helper.go # Helper functions (GetCurrentUser, BindJSON, Response) -│ ├── serials_controller.go # Company serials: generate, query, update, revoke, qrcode +│ ├── product_traces_controller.go # Product traceability +│ ├── project_orders_controller.go # Project work orders │ └── users_controller.go # Employee master data / backend account management ├── database/ # Database connection and migrations │ └── database.go # GORM init, AutoMigrate @@ -59,17 +60,18 @@ backend-go/ ├── middleware/ # Middleware │ └── auth.go # JWT auth, Admin / Technician permission checks ├── models/ # Data models and DTOs -│ └── models.go # User, Company, Serial, EmployeeSerial, AftersalesOrder and DTOs +│ └── models.go # User, Company, EmployeeSerial, ProductTrace, AftersalesOrder, ProjectOrder and DTOs ├── routes/ # Route configuration │ └── routes.go # API route registration ├── services/ # Business logic layer │ ├── aftersales_service.go # Aftersales orders: create, list, update, submit, customer confirm, qrcode │ ├── auth_service.go # Auth: validate user, generate token, password management -│ ├── companies_service.go # Company CRUD -│ ├── employees_service.go # Employee serials: generate, query, update, revoke, qrcode -│ ├── serials_service.go # Company serials: generate, query, update, revoke, qrcode +│ ├── dashboard_service.go # Dashboard work-order statistics +│ ├── employees_service.go # Employee permission codes: generate, query, update, revoke, qrcode +│ ├── product_traces_service.go # Product traceability +│ ├── project_orders_service.go # Project work orders │ ├── aftersales_service_test.go # Aftersales unit tests -│ ├── services_test.go # Auth / Serials / Employees / Companies unit tests +│ ├── services_test.go # Auth / employee permission-code unit tests │ ├── users_service.go # Employee CRUD, role management, password reset (admin) │ └── users_service_test.go # Employee/user account unit tests ├── tests/ # Integration tests @@ -90,12 +92,15 @@ backend-go/ ### API Surface (Current) - **Auth**: `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/profile`, `PUT /api/auth/profile`, `POST /api/auth/change-password` -- **Serials**: `POST /api/serials/generate`, `POST /api/serials/generate-with-prefix`, `POST /api/serials/:serialNumber/qrcode`, `GET /api/serials/:serialNumber/query`, `GET /api/serials`, `PATCH /api/serials/:serialNumber`, `PUT /api/serials/:serialNumber`, `POST /api/serials/:serialNumber/revoke` -- **Companies**: `GET /api/companies/stats/overview`, `GET /api/companies`, `GET /api/companies/:companyName`, `POST /api/companies`, `PATCH /api/companies/:companyName`, `PUT /api/companies/:companyName`, `POST /api/companies/:companyName/revoke`, `DELETE /api/companies/:companyName/serials/:serialNumber`, `DELETE /api/companies/:companyName` -- **Employee Serials**: `POST /api/employee-serials/generate`, `POST /api/employee-serials/:serialNumber/qrcode`, `GET /api/employee-serials/:serialNumber/query`, `GET /api/employee-serials`, `PATCH /api/employee-serials/:serialNumber`, `PUT /api/employee-serials/:serialNumber`, `POST /api/employee-serials/:serialNumber/revoke` +- **Dashboard**: `GET /api/dashboard/stats` +- **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` +- **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` - **Employees** (仅管理员): `POST /api/employees`, `GET /api/employees`, `PATCH /api/employees/:id`, `POST /api/employees/:id/reset-password`, `DELETE /api/employees/:id` @@ -104,12 +109,13 @@ backend-go/ - `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`. +- 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. ### Business Boundaries -- Company management (`Company`, `/api/companies`) is for authorized agents/company-code verification. -- Aftersales order `companyName` is customer information stored on the order only; creating or updating an aftersales order must not create or link a `Company` record. +- Enterprise/company-code management was removed. Do not reintroduce `/api/companies`, `/api/serials`, `CompaniesService`, `SerialsService`, or a company-management UI. +- The `Company` model remains only as internal compatibility for employee permission-code ownership; it is not an exposed management feature. +- Aftersales/project order `companyName` is customer information stored on the order only; creating or updating a work order must not create a managed-company feature. ### Import Organization Standard imports followed by third-party imports, then project imports (sorted alphabetically): @@ -127,9 +133,9 @@ import ( ``` ### Naming Conventions -- **Controllers**: `AuthController`, `SerialsController`, `CompaniesController`, `EmployeeSerialsController` -- **Services**: `AuthService`, `SerialsService`, `CompaniesService`, `EmployeeSerialsService` -- **Models**: `User`, `Company`, `Serial` (use PascalCase for exported structs) +- **Controllers**: `AuthController`, `DashboardController`, `EmployeeSerialsController`, `ProductTracesController`, `ProjectOrdersController` +- **Services**: `AuthService`, `DashboardService`, `EmployeeSerialsService`, `ProductTracesService`, `ProjectOrdersService` +- **Models**: `User`, `Company`, `EmployeeSerial`, `ProductTrace`, `AftersalesOrder`, `ProjectOrder` (use PascalCase for exported structs) - **DTOs**: `LoginDTO`, `ChangePasswordDTO`, `UpdateProfileDTO` (DTO suffix) - **Functions**: `ValidateUser`, `Generate`, `Query` (PascalCase for exported) - **Variables**: camelCase for local variables, PascalCase for exported diff --git a/README.md b/README.md index 46a4b8d..512f980 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 浙江贝凡溯源管理平台 - 后端服务 (Go 版本) +# 浙江贝凡溯源赋码平台 - 后端服务 (Go 版本) -这是一个使用 Go 语言开发的溯源管理平台后端服务,提供序列号生成、查询、管理等功能。 +这是一个使用 Go 语言开发的溯源赋码平台后端服务,提供权限下发、产品溯源、项目工单、售后工单等功能。 ## 技术栈 @@ -31,10 +31,11 @@ backend-go/ ├── controllers/ # 控制器层,处理 HTTP 请求 │ ├── aftersales_controller.go # 售后工单接口 │ ├── auth_controller.go # 认证相关接口 -│ ├── companies_controller.go # 企业管理接口 +│ ├── dashboard_controller.go # 控制台统计接口 │ ├── employees_controller.go # 员工赋码接口 │ ├── helper.go # 控制器通用辅助函数 -│ ├── serials_controller.go # 序列号管理接口 +│ ├── product_traces_controller.go # 产品溯源接口 +│ ├── project_orders_controller.go # 项目工单接口 │ └── users_controller.go # 员工主档管理接口(仅管理员) ├── database/ # 数据库连接和操作 │ └── database.go # 数据库初始化、连接池配置 @@ -47,17 +48,18 @@ backend-go/ ├── middleware/ # 中间件层 │ └── auth.go # JWT 认证、管理员/技术员权限检查 ├── models/ # 数据模型和 DTO -│ └── models.go # User、Company、Serial、AftersalesOrder 等模型定义 +│ └── models.go # User、Company、EmployeeSerial、ProductTrace、AftersalesOrder、ProjectOrder 等模型定义 ├── routes/ # 路由配置 │ └── routes.go # API 路由注册 ├── services/ # 业务逻辑层 │ ├── aftersales_service.go # 售后工单业务逻辑 │ ├── auth_service.go # 认证业务逻辑 -│ ├── companies_service.go # 企业管理业务逻辑 +│ ├── dashboard_service.go # 控制台统计业务逻辑 │ ├── employees_service.go # 员工赋码业务逻辑 -│ ├── serials_service.go # 序列号业务逻辑 +│ ├── product_traces_service.go # 产品溯源业务逻辑 +│ ├── project_orders_service.go # 项目工单业务逻辑 │ ├── aftersales_service_test.go # 售后工单单元测试 -│ ├── services_test.go # 认证/序列号/员工/企业单元测试 +│ ├── services_test.go # 认证/员工赋码单元测试 │ ├── users_service.go # 员工主档/后台账号业务逻辑 │ └── users_service_test.go # 员工主档/后台账号单元测试 ├── tests/ # 集成测试 @@ -220,7 +222,7 @@ http://localhost:3000/swagger/index.html - **交互式测试**: 直接在浏览器中测试 API 端点 - **请求/响应示例**: 查看每个接口的请求参数和响应格式 - **认证支持**: 支持 Bearer Token 认证,可以输入 JWT 令牌进行测试 -- **按分组浏览**: API 按功能模块分组(认证、序列号管理、企业管理等) +- **按分组浏览**: API 按功能模块分组(认证、控制台、权限下发、产品溯源、项目工单、售后工单等) ### 重新生成 Swagger 文档 @@ -248,59 +250,71 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go > 错误提示已做用户友好化,例如登录失败统一返回:`用户名或密码不正确`。 -### 序列号管理 +### 控制台 -| 方法 | 路径 | 描述 | 需要认证 | 角色 | -| ---- | ----------------------------------- | ---------------- | -------- | ------ | -| POST | `/api/serials/generate` | 生成序列号 | 是 | 管理员 | -| POST | `/api/serials/generate-with-prefix` | 带前缀生成序列号 | 是 | 管理员 | -| POST | `/api/serials/:serialNumber/qrcode` | 生成序列号二维码 | 是 | 管理员 | -| GET | `/api/serials/:serialNumber/query` | 查询序列号信息 | 否 | 任何 | -| GET | `/api/serials` | 获取序列号列表 | 是 | 管理员 | -| PATCH | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 | -| PUT | `/api/serials/:serialNumber` | 更新序列号信息 | 是 | 管理员 | -| POST | `/api/serials/:serialNumber/revoke` | 吊销序列号 | 是 | 管理员 | +| 方法 | 路径 | 描述 | 需要认证 | 角色 | +| ---- | ---- | ---- | -------- | ---- | +| GET | `/api/dashboard/stats` | 获取工单统计和最近售后工单 | 是 | 管理员 | -### 企业管理 - -企业管理用于维护授权代理商/企业码主体,供客户扫码查询代理商授权状态;它不是售后客户名录。 - -| 方法 | 路径 | 描述 | 需要认证 | 角色 | -| ------ | ----------------------------- | ------------ | -------- | ------ | -| GET | `/api/companies/stats/overview` | 获取企业统计概览 | 是 | 管理员 | -| GET | `/api/companies` | 获取企业列表 | 是 | 管理员 | -| GET | `/api/companies/:companyName` | 获取企业详情 | 是 | 管理员 | -| POST | `/api/companies` | 创建新企业 | 是 | 管理员 | -| PATCH | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 | -| PUT | `/api/companies/:companyName` | 更新企业信息 | 是 | 管理员 | -| POST | `/api/companies/:companyName/revoke` | 吊销企业及序列号 | 是 | 管理员 | -| DELETE | `/api/companies/:companyName/serials/:serialNumber` | 删除企业下序列号 | 是 | 管理员 | -| DELETE | `/api/companies/:companyName` | 删除企业 | 是 | 管理员 | - -### 员工赋码 +### 权限下发(员工赋码) | 方法 | 路径 | 描述 | 需要认证 | 角色 | | ---- | -------------------------------------- | ------------------ | -------- | ------ | -| POST | `/api/employee-serials/generate` | 生成员工序列号 | 是 | 管理员 | -| GET | `/api/employee-serials` | 获取员工序列号列表 | 是 | 管理员 | -| GET | `/api/employee-serials/:serialNumber/query` | 查询员工序列号信息 | 否 | 任何 | +| POST | `/api/employee-serials/generate` | 生成员工权限码 | 是 | 管理员 | +| GET | `/api/employee-serials` | 获取员工权限码列表 | 是 | 管理员 | +| GET | `/api/employee-serials/:serialNumber/query` | 查询员工权限码信息 | 否 | 任何 | | POST | `/api/employee-serials/:serialNumber/qrcode` | 生成员工二维码 | 是 | 管理员 | -| PATCH | `/api/employee-serials/:serialNumber` | 更新员工序列号信息 | 是 | 管理员 | -| PUT | `/api/employee-serials/:serialNumber` | 更新员工序列号信息 | 是 | 管理员 | -| POST | `/api/employee-serials/:serialNumber/revoke` | 吊销员工序列号 | 是 | 管理员 | +| PATCH | `/api/employee-serials/:serialNumber` | 更新员工权限码信息 | 是 | 管理员 | +| PUT | `/api/employee-serials/:serialNumber` | 更新员工权限码信息 | 是 | 管理员 | +| POST | `/api/employee-serials/:serialNumber/revoke` | 吊销员工权限码 | 是 | 管理员 | -### Node 对齐说明 - -- Go 版路由已与 `backend-node` 对齐(含 `PATCH` 更新接口、企业详情、企业吊销、删除企业下单个序列号)。 -- 企业统计与企业详情接口统一返回 `{ message, data }` 结构。 -- 序列号相关返回字段已对齐前端使用(如 `serialNumber`、`validUntil`、`createdBy`、`status`)。 - -**员工序列号特点**: -- 无有效期限制(与企业赋码不同) +**员工权限码特点**: +- 无有效期限制 - 创建员工主档时会自动生成 1 个员工码,并通过 `employeeId` 绑定员工 - 包含岗位(position)和员工姓名(employeeName)信息 - 序列号格式: `EMP26xxxxxx`(EMP + 年份后两位 + 6位随机字符) +### 产品溯源 + +| 方法 | 路径 | 描述 | 需要认证 | 角色 | +| ---- | ---- | ---- | -------- | ---- | +| 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` | 删除产品溯源 | 是 | 管理员 | + +**产品溯源字段顺序**: +企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选)。 + +### 项目工单 + +| 方法 | 路径 | 描述 | 需要认证 | 角色 | +| ---- | ---- | ---- | -------- | ---- | +| 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/:serialNumber/reassign` | 工单分配 | 是 | 管理员 | +| POST | `/api/project-orders/:serialNumber/force-close` | 强制完成 | 是 | 管理员 | +| DELETE | `/api/project-orders/:serialNumber` | 删除项目工单 | 是 | 管理员 | + +**项目工单特点**: +- 用于现场勘查、现场实施等项目任务 +- 工单号格式: `zjbf-xm-YYMMDDNN` +- 现场图片最多 18 张 +- 仅需要工程师签名,无客户签字环节 +- 完成状态使用 `已完成` + ### 售后工单 | 方法 | 路径 | 描述 | 需要认证 | 角色 | @@ -321,7 +335,7 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go - 服务类型枚举:`software`(软件故障)、`hardware`(硬件故障)、`maintenance`(售后维保) - 工单号格式: `zjbf-sh-YYMMDDNN`(年份后 2 位 + 月份 2 位 + 日期 2 位 + 当天序号至少 2 位,例:`zjbf-sh-26052801`) - 序号按天重置,软删除工单不释放编号(避免回收造成混淆) -- 工单里的企业名称是售后客户信息,只保存在工单中,不会自动创建或关联企业管理记录 +- 工单里的企业名称是售后客户信息,只保存在工单中 - 二维码扫码后客户在网页签名(canvas)后点「已授权」确认;选择「未授权」需填写退回原因 - 签名以 PNG dataURL 形式持久化到工单(`signature` 字段),管理员详情页可查看留底 - 签名校验:必须为 `data:image/png;base64,` 或 `data:image/jpeg;base64,` 前缀,解码后 200B–500KB @@ -329,7 +343,7 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go - 工单状态机: `created` → `pending_confirmation` → `closed` / `rejected`,被退回后可重新提交 - 公开查询不返回手机号(脱敏) -### 员工管理(仅管理员) +### 权限下发(仅管理员) | 方法 | 路径 | 描述 | 需要认证 | 角色 | | ------ | ----------------------------------- | -------------------------- | -------- | ------------- | @@ -341,7 +355,7 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g main.go | DELETE | `/api/employees/:id` | 删除员工 | 是 | 管理员 | **员工角色**: -- `admin`:管理员,拥有全部后台权限,包括企业管理、员工管理、工单分配、强制关闭和删除工单 +- `admin`:管理员,拥有全部后台权限,包括权限下发、产品溯源、工单分配、强制关闭和删除工单 - `technician`:技术员,仅拥有工单模块权限,可创建/处理工单,可使用 `assignable` 查询可分配同事 - `employee`:员工,无后台登录权限,不需要密码,仅用于员工主档和员工码查询 @@ -375,13 +389,13 @@ go tool cover -html=coverage.out ### 当前测试覆盖 -- **services/**: 包含 AuthService、SerialsService、EmployeeSerialsService、CompaniesService、AftersalesService 和 UsersService 的完整单元测试 +- **services/**: 包含 AuthService、EmployeeSerialsService、AftersalesService、ProjectOrdersService、ProductTracesService 和 UsersService 的单元测试 - 用户认证测试(登录、获取用户信息、修改密码、更新资料) - - 序列号管理测试(生成、查询、更新、吊销、分页列表) - 员工赋码测试(生成、查询、更新、吊销、二维码生成) - - 企业统计测试(统计概览) - 售后工单测试(YYMMDDNN 序号生成、状态机、客户确认手机号校验、强制关闭) - - 员工管理测试(创建员工自动生成员工码、重复工号、自降级保护、最后管理员保护、密码重置) + - 项目工单测试(创建、完成、现场图片、工程师签名) + - 产品溯源测试(创建、查询、二维码、公众号二维码) + - 权限下发测试(创建员工自动生成员工码、重复工号、自降级保护、最后管理员保护、密码重置) - **tests/**: 集成测试(健康检查、登录流程) ## 代码检查 diff --git a/controllers/companies_controller.go b/controllers/companies_controller.go deleted file mode 100644 index edc5254..0000000 --- a/controllers/companies_controller.go +++ /dev/null @@ -1,320 +0,0 @@ -package controllers - -import ( - "net/http" - "net/url" - "strconv" - "time" - - "github.com/gin-gonic/gin" - - "git.beifan.cn/trace-system/backend-go/services" -) - -// CompaniesController 企业管理控制器 -type CompaniesController struct { - companiesService services.CompaniesService -} - -// NewCompaniesController 创建企业管理控制器实例 -func NewCompaniesController() *CompaniesController { - return &CompaniesController{ - companiesService: services.CompaniesService{}, - } -} - -// FindAll 获取企业列表 -// @Summary 获取企业列表 -// @Description 获取企业列表,支持分页和搜索 -// @Tags 企业管理 -// @Produce json -// @Security BearerAuth -// @Param page query int false "页码" -// @Param limit query int false "每页数量" -// @Param search query string false "搜索关键词" -// @Success 200 {object} models.PaginationResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies [get] -func (c *CompaniesController) FindAll(ctx *gin.Context) { - page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) - limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20")) - search := ctx.DefaultQuery("search", "") - - companies, total, totalPages, err := c.companiesService.FindAll(page, limit, search) - if err != nil { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - items := make([]gin.H, 0, len(companies)) - now := time.Now() - for _, company := range companies { - serialCount := len(company.Serials) - activeCount := 0 - for _, serial := range company.Serials { - if serial.IsActive && (serial.ValidUntil == nil || serial.ValidUntil.After(now)) { - activeCount++ - } - } - - items = append(items, gin.H{ - "companyName": company.CompanyName, - "firstCreated": company.CreatedAt, - "lastCreated": company.UpdatedAt, - "serialCount": serialCount, - "activeCount": activeCount, - "status": map[bool]string{ - true: "active", - false: "disabled", - }[company.IsActive], - }) - } - - SuccessResponse(ctx, "获取企业列表成功", gin.H{ - "data": items, - "pagination": gin.H{ - "page": page, - "limit": limit, - "total": total, - "totalPages": totalPages, - }, - }) -} - -// FindOne 获取企业详情 -// @Summary 获取企业详情 -// @Description 获取指定企业详情(含序列号分页) -// @Tags 企业管理 -// @Produce json -// @Security BearerAuth -// @Param companyName path string true "企业名称" -// @Param page query int false "页码" -// @Param limit query int false "每页数量" -// @Success 200 {object} models.DataResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies/{companyName} [get] -func (c *CompaniesController) FindOne(ctx *gin.Context) { - companyName, _ := url.PathUnescape(ctx.Param("companyName")) - page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) - limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20")) - - data, err := c.companiesService.FindOne(companyName, page, limit) - if err != nil { - if err.Error() == "企业不存在" { - ErrorResponse(ctx, http.StatusNotFound, err.Error()) - return - } - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "获取企业详情成功", - "data": data, - }) -} - -// Create 创建企业 -// @Summary 创建企业 -// @Description 创建新的企业 -// @Tags 企业管理 -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param companyData body models.CompanyDataRequest true "企业数据" -// @Success 201 {object} models.CompanyResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 409 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies [post] -func (c *CompaniesController) Create(ctx *gin.Context) { - var companyData struct { - CompanyName string `json:"companyName" validate:"required"` - } - if !BindJSON(ctx, &companyData) { - return - } - - company, err := c.companiesService.Create(companyData.CompanyName) - if err != nil { - if err.Error() == "企业名称已存在" { - ErrorResponse(ctx, http.StatusConflict, err.Error()) - } else { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - } - return - } - - ctx.JSON(http.StatusCreated, gin.H{ - "message": "企业创建成功", - "company": company, - }) -} - -// Update 更新企业信息 -// @Summary 更新企业信息 -// @Description 更新企业信息 -// @Tags 企业管理 -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param companyName path string true "企业名称" -// @Param companyData body models.CompanyUpdateRequest true "企业数据" -// @Success 200 {object} models.CompanyResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 409 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies/{companyName} [put] -func (c *CompaniesController) Update(ctx *gin.Context) { - companyName, _ := url.PathUnescape(ctx.Param("companyName")) - - var companyData struct { - CompanyName string `json:"companyName"` - NewCompanyName string `json:"newCompanyName"` - IsActive *bool `json:"isActive"` - } - if !BindJSON(ctx, &companyData) { - return - } - - newName := companyData.NewCompanyName - if newName == "" { - newName = companyData.CompanyName - } - - company, err := c.companiesService.Update(companyName, newName, companyData.IsActive) - if err != nil { - switch err.Error() { - case "企业不存在": - ErrorResponse(ctx, http.StatusNotFound, err.Error()) - case "企业名称已存在": - ErrorResponse(ctx, http.StatusConflict, err.Error()) - default: - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - } - return - } - - SuccessResponse(ctx, "企业信息更新成功", gin.H{"company": company}) -} - -// Delete 删除企业 -// @Summary 删除企业 -// @Description 删除企业及其关联序列号 -// @Tags 企业管理 -// @Produce json -// @Security BearerAuth -// @Param companyName path string true "企业名称" -// @Success 200 {object} models.BaseResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies/{companyName} [delete] -func (c *CompaniesController) Delete(ctx *gin.Context) { - companyName, _ := url.PathUnescape(ctx.Param("companyName")) - - err := c.companiesService.Delete(companyName) - if err != nil { - if err.Error() == "企业不存在" { - ErrorResponse(ctx, http.StatusNotFound, err.Error()) - } else { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - } - return - } - - SuccessResponse(ctx, "企业已完全删除,所有相关序列号已删除") -} - -// DeleteSerial 删除企业下指定序列号 -// @Summary 删除企业序列号 -// @Description 删除指定企业下的序列号 -// @Tags 企业管理 -// @Produce json -// @Security BearerAuth -// @Param companyName path string true "企业名称" -// @Param serialNumber path string true "序列号" -// @Success 200 {object} models.BaseResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies/{companyName}/serials/{serialNumber} [delete] -func (c *CompaniesController) DeleteSerial(ctx *gin.Context) { - companyName, _ := url.PathUnescape(ctx.Param("companyName")) - serialNumber := ctx.Param("serialNumber") - - err := c.companiesService.DeleteSerial(companyName, serialNumber) - if err != nil { - if err.Error() == "序列号不存在或不属于该企业" { - ErrorResponse(ctx, http.StatusNotFound, err.Error()) - } else { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - } - return - } - - SuccessResponse(ctx, "序列号已成功删除", gin.H{ - "serialNumber": serialNumber, - "companyName": companyName, - }) -} - -// Revoke 吊销企业 -// @Summary 吊销企业 -// @Description 吊销企业及其关联序列号 -// @Tags 企业管理 -// @Produce json -// @Security BearerAuth -// @Param companyName path string true "企业名称" -// @Success 200 {object} models.BaseResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies/{companyName}/revoke [post] -func (c *CompaniesController) Revoke(ctx *gin.Context) { - companyName, _ := url.PathUnescape(ctx.Param("companyName")) - - err := c.companiesService.Revoke(companyName) - if err != nil { - if err.Error() == "企业不存在" { - ErrorResponse(ctx, http.StatusNotFound, err.Error()) - } else { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - } - return - } - - SuccessResponse(ctx, "企业已吊销,所有序列号已失效", gin.H{ - "companyName": companyName, - }) -} - -// StatsOverview 获取企业统计概览 -// @Summary 获取企业统计概览 -// @Description 获取企业、序列号统计数据 -// @Tags 企业管理 -// @Produce json -// @Security BearerAuth -// @Success 200 {object} models.DataResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /companies/stats/overview [get] -func (c *CompaniesController) StatsOverview(ctx *gin.Context) { - stats, err := c.companiesService.GetStats() - if err != nil { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - ctx.JSON(http.StatusOK, gin.H{ - "message": "获取统计数据成功", - "data": stats, - }) -} diff --git a/controllers/dashboard_controller.go b/controllers/dashboard_controller.go new file mode 100644 index 0000000..b9e45d6 --- /dev/null +++ b/controllers/dashboard_controller.go @@ -0,0 +1,32 @@ +package controllers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "git.beifan.cn/trace-system/backend-go/services" +) + +// DashboardController 控制台统计控制器 +type DashboardController struct { + dashboardService services.DashboardService +} + +// NewDashboardController 创建控制台统计控制器实例 +func NewDashboardController() *DashboardController { + return &DashboardController{ + dashboardService: services.DashboardService{}, + } +} + +// Stats 获取控制台统计 +func (c *DashboardController) Stats(ctx *gin.Context) { + stats, err := c.dashboardService.GetStats() + if err != nil { + ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) + return + } + + SuccessResponse(ctx, "获取统计数据成功", stats) +} diff --git a/controllers/serials_controller.go b/controllers/serials_controller.go deleted file mode 100644 index e69def3..0000000 --- a/controllers/serials_controller.go +++ /dev/null @@ -1,345 +0,0 @@ -package controllers - -import ( - "net/http" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - - "git.beifan.cn/trace-system/backend-go/models" - "git.beifan.cn/trace-system/backend-go/services" -) - -// SerialsController 序列号控制器 -type SerialsController struct { - serialsService services.SerialsService -} - -// NewSerialsController 创建序列号控制器实例 -func NewSerialsController() *SerialsController { - return &SerialsController{ - serialsService: services.SerialsService{}, - } -} - -// Generate 生成序列号 -// @Summary 生成序列号 -// @Description 生成指定数量的序列号 -// @Tags 序列号管理 -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param generateData body models.GenerateSerialDTO true "生成数据" -// @Success 200 {object} models.DataResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials/generate [post] -func (c *SerialsController) Generate(ctx *gin.Context) { - userModel, ok := GetCurrentUser(ctx) - if !ok { - return - } - - var generateData models.GenerateSerialDTO - if !BindJSON(ctx, &generateData) { - return - } - - serials, err := c.serialsService.Generate( - generateData.CompanyName, - generateData.Quantity, - generateData.ValidDays, - userModel.ID, - ) - if err != nil { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - items := make([]gin.H, 0, len(serials)) - for _, serial := range serials { - items = append(items, gin.H{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "validUntil": serial.ValidUntil, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "createdBy": userModel.Name, - }) - } - - SuccessResponse(ctx, "成功生成"+strconv.Itoa(len(serials))+"个序列号", gin.H{ - "serials": items, - }) -} - -// GenerateWithPrefix 带前缀生成序列号 -// @Summary 带前缀生成序列号 -// @Description 生成带有指定前缀的序列号 -// @Tags 序列号管理 -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param generateData body models.GenerateWithPrefixDTO true "生成数据" -// @Success 200 {object} models.DataResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials/generate-with-prefix [post] -func (c *SerialsController) GenerateWithPrefix(ctx *gin.Context) { - userModel, ok := GetCurrentUser(ctx) - if !ok { - return - } - - var generateData models.GenerateWithPrefixDTO - if !BindJSON(ctx, &generateData) { - return - } - - serials, err := c.serialsService.Generate( - generateData.CompanyName, - generateData.Quantity, - generateData.ValidDays, - userModel.ID, - generateData.SerialPrefix, - ) - if err != nil { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - items := make([]gin.H, 0, len(serials)) - for _, serial := range serials { - items = append(items, gin.H{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "validUntil": serial.ValidUntil, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "createdBy": userModel.Name, - }) - } - - SuccessResponse(ctx, "成功生成"+strconv.Itoa(len(serials))+"个序列号", gin.H{ - "serials": items, - }) -} - -// GenerateQRCode 生成二维码 -// @Summary 生成二维码 -// @Description 为指定序列号生成查询二维码 -// @Tags 序列号管理 -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param serialNumber path string true "序列号" -// @Param qrCodeData body models.QRCodeDTO false "二维码数据" -// @Success 200 {object} models.QRCodeResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials/{serialNumber}/qrcode [post] -func (c *SerialsController) GenerateQRCode(ctx *gin.Context) { - serialNumber := ctx.Param("serialNumber") - - var qrCodeData models.QRCodeDTO - if !BindJSON(ctx, &qrCodeData) { - return - } - - protocol := "http" - if ctx.Request.TLS != nil { - protocol = "https" - } - - qrCodeBase64, queryUrl, err := c.serialsService.GenerateQRCode( - serialNumber, - qrCodeData.BaseUrl, - ctx.Request.Host, - protocol, - ) - if err != nil { - ErrorResponse(ctx, http.StatusBadRequest, err.Error()) - return - } - - SuccessResponse(ctx, "二维码生成成功", gin.H{ - "qrCodeData": qrCodeBase64, - "queryUrl": queryUrl, - }) -} - -// Query 查询序列号信息 -// @Summary 查询序列号信息 -// @Description 查询指定序列号的详细信息 -// @Tags 序列号查询 -// @Produce json -// @Param serialNumber path string true "序列号" -// @Success 200 {object} models.DataResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials/{serialNumber}/query [get] -func (c *SerialsController) Query(ctx *gin.Context) { - serialNumber := ctx.Param("serialNumber") - - serial, err := c.serialsService.Query(serialNumber) - if err != nil { - ErrorResponse(ctx, http.StatusBadRequest, err.Error()) - return - } - - createdBy := "" - if serial.User != nil { - createdBy = serial.User.Name - } - - status := "active" - if !serial.IsActive { - status = "disabled" - } - - SuccessResponse(ctx, "查询成功", gin.H{ - "serial": gin.H{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "validUntil": serial.ValidUntil, - "status": status, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "createdBy": createdBy, - }, - }) -} - -// FindAll 获取序列号列表 -// @Summary 获取序列号列表 -// @Description 获取序列号列表,支持分页和搜索 -// @Tags 序列号管理 -// @Produce json -// @Security BearerAuth -// @Param page query int false "页码" -// @Param limit query int false " "每页数量" -// @Param search query string false "搜索关键词" -// @Success 200 {object} models.PaginationResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials [get] -func (c *SerialsController) FindAll(ctx *gin.Context) { - page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) - limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20")) - search := ctx.DefaultQuery("search", "") - - serials, total, totalPages, err := c.serialsService.FindAll(page, limit, search) - if err != nil { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - items := make([]gin.H, 0, len(serials)) - for _, serial := range serials { - createdBy := "" - if serial.User != nil { - createdBy = serial.User.Name - } - - items = append(items, gin.H{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "validUntil": serial.ValidUntil, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "createdBy": createdBy, - }) - } - - SuccessResponse(ctx, "获取序列号列表成功", gin.H{ - "data": items, - "pagination": gin.H{ - "page": page, - "limit": limit, - "total": total, - "totalPages": totalPages, - }, - }) -} - -// Update 更新序列号信息 -// @Summary 更新序列号信息 -// @Description 更新指定序列号的信息 -// @Tags 序列号管理 -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param serialNumber path string true "序列号" -// @Param updateData body models.UpdateSerialDTO true "更新数据" -// @Success 200 {object} models.DataResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials/{serialNumber} [put] -func (c *SerialsController) Update(ctx *gin.Context) { - serialNumber := ctx.Param("serialNumber") - - var updateData models.UpdateSerialDTO - if !BindJSON(ctx, &updateData) { - return - } - - serial, err := c.serialsService.Update(serialNumber, updateData) - if err != nil { - ErrorResponse(ctx, http.StatusInternalServerError, err.Error()) - return - } - - createdBy := "" - if serial.User != nil { - createdBy = serial.User.Name - } - - SuccessResponse(ctx, "序列号更新成功", gin.H{ - "serial": gin.H{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "validUntil": serial.ValidUntil, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "updatedAt": serial.UpdatedAt, - "createdBy": createdBy, - }, - }) -} - -// Revoke 吊销序列号 -// @Summary 吊销序列号 -// @Description 吊销指定序列号 -// @Tags 序列号管理 -// @Produce json -// @Security BearerAuth -// @Param serialNumber path string true "序列号" -// @Success 200 {object} models.BaseResponse -// @Failure 400 {object} models.ErrorResponse -// @Failure 401 {object} models.ErrorResponse -// @Failure 404 {object} models.ErrorResponse -// @Failure 500 {object} models.ErrorResponse -// @Router /serials/{serialNumber}/revoke [post] -func (c *SerialsController) Revoke(ctx *gin.Context) { - serialNumber := ctx.Param("serialNumber") - - err := c.serialsService.Revoke(serialNumber) - if err != nil { - ErrorResponse(ctx, http.StatusBadRequest, err.Error()) - return - } - - SuccessResponse(ctx, "序列号已吊销", gin.H{ - "data": gin.H{ - "serialNumber": strings.ToUpper(serialNumber), - }, - }) -} diff --git a/database/database.go b/database/database.go index 6e1004d..9c1f87a 100644 --- a/database/database.go +++ b/database/database.go @@ -112,7 +112,6 @@ func AutoMigrate() { if err := DB.AutoMigrate( &models.User{}, &models.Company{}, - &models.Serial{}, &models.ProductTrace{}, &models.EmployeeSerial{}, &models.AftersalesOrder{}, diff --git a/docs/docs.go b/docs/docs.go index 0075b3c..4c9270a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -560,6 +560,56 @@ const docTemplate = `{ } } }, + "/aftersales/{serialNumber}/site-images": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单查询" + ], + "summary": "上传售后现场图片", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "现场图片", + "name": "files", + "in": "formData", + "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" + } + } + } + } + }, "/aftersales/{serialNumber}/submit": { "post": { "security": [ @@ -826,466 +876,6 @@ const docTemplate = `{ } } }, - "/companies": { - "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" - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "创建新的企业", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "企业管理" - ], - "summary": "创建企业", - "parameters": [ - { - "description": "企业数据", - "name": "companyData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CompanyDataRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.CompanyResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "409": { - "description": "Conflict", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/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": [ - { - "BearerAuth": [] - } - ], - "description": "更新企业信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "企业管理" - ], - "summary": "更新企业信息", - "parameters": [ - { - "type": "string", - "description": "企业名称", - "name": "companyName", - "in": "path", - "required": true - }, - { - "description": "企业数据", - "name": "companyData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CompanyUpdateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.CompanyResponse" - } - }, - "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" - } - }, - "409": { - "description": "Conflict", - "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": "companyName", - "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" - } - } - } - } - }, - "/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": [ @@ -1961,21 +1551,21 @@ const docTemplate = `{ } } }, - "/serials": { + "/project-orders": { "get": { "security": [ { "BearerAuth": [] } ], - "description": "获取序列号列表,支持分页和搜索", + "description": "支持分页、搜索、按状态/服务类型/技术员筛选", "produces": [ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "获取序列号列表", + "summary": "获取项目工单列表", "parameters": [ { "type": "integer", @@ -1985,7 +1575,7 @@ const docTemplate = `{ }, { "type": "integer", - "description": " ", + "description": "每页数量", "name": "limit", "in": "query" }, @@ -1994,6 +1584,30 @@ const docTemplate = `{ "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": { @@ -2016,16 +1630,14 @@ const docTemplate = `{ } } } - } - }, - "/serials/generate": { + }, "post": { "security": [ { "BearerAuth": [] } ], - "description": "生成指定数量的序列号", + "description": "创建一个新的项目工单并分配编号", "consumes": [ "application/json" ], @@ -2033,17 +1645,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "生成序列号", + "summary": "创建项目工单", "parameters": [ { - "description": "生成数据", - "name": "generateData", + "description": "工单数据", + "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.GenerateSerialDTO" + "$ref": "#/definitions/models.CreateProjectOrderDTO" } } ], @@ -2075,97 +1687,27 @@ const docTemplate = `{ } } }, - "/serials/generate-with-prefix": { - "post": { + "/project-orders/{serialNumber}": { + "get": { "security": [ { "BearerAuth": [] } ], - "description": "生成带有指定前缀的序列号", - "consumes": [ - "application/json" - ], "produces": [ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "带前缀生成序列号", - "parameters": [ - { - "description": "生成数据", - "name": "generateData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.GenerateWithPrefixDTO" - } - } - ], - "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" - } - } - } - } - }, - "/serials/{serialNumber}": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "更新指定序列号的信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "序列号管理" - ], - "summary": "更新序列号信息", + "summary": "获取项目工单详情", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true - }, - { - "description": "更新数据", - "name": "updateData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.UpdateSerialDTO" - } } ], "responses": { @@ -2175,12 +1717,6 @@ const docTemplate = `{ "$ref": "#/definitions/models.DataResponse" } }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, "401": { "description": "Unauthorized", "schema": { @@ -2192,24 +1728,58 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/models.ErrorResponse" } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } - } - }, - "/serials/{serialNumber}/qrcode": { - "post": { + }, + "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": [] } ], - "description": "为指定序列号生成查询二维码", "consumes": [ "application/json" ], @@ -2217,20 +1787,187 @@ const docTemplate = `{ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "生成二维码", + "summary": "更新项目工单", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true }, { - "description": "二维码数据", - "name": "qrCodeData", + "description": "更新数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateProjectOrderDTO" + } + } + ], + "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" + } + } + } + } + }, + "/project-orders/{serialNumber}/complete": { + "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.ProjectEngineerCompleteDTO" + } + } + ], + "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" + } + } + } + } + }, + "/project-orders/{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" + } + } + } + } + }, + "/project-orders/{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" @@ -2255,39 +1992,127 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/models.ErrorResponse" } + } + } + } + }, + "/project-orders/{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" } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } } }, - "/serials/{serialNumber}/query": { - "get": { - "description": "查询指定序列号的详细信息", + "/project-orders/{serialNumber}/reassign": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "序列号查询" + "项目工单" ], - "summary": "查询序列号信息", + "summary": "重新分配技术员", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true + }, + { + "description": "新技术员 ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ReassignProjectOrderDTO" + } + } + ], + "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" + } + } + } + } + }, + "/project-orders/{serialNumber}/site-images": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "项目工单查询" + ], + "summary": "上传项目现场图片", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "现场图片", + "name": "files", + "in": "formData", + "required": true } ], "responses": { @@ -2308,45 +2133,51 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/models.ErrorResponse" } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } } }, - "/serials/{serialNumber}/revoke": { + "/project-orders/{serialNumber}/submit": { "post": { "security": [ { "BearerAuth": [] } ], - "description": "吊销指定序列号", + "description": "技术员填写处理结果后提交,工单进入\"待完成确认\"状态", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "吊销序列号", + "summary": "提交完成确认", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true + }, + { + "description": "处理结果", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SubmitProjectCompletionDTO" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.BaseResponse" + "$ref": "#/definitions/models.DataResponse" } }, "400": { @@ -2360,18 +2191,6 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/models.ErrorResponse" } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } } @@ -2465,39 +2284,6 @@ const docTemplate = `{ } } }, - "models.CompanyDataRequest": { - "type": "object", - "required": [ - "companyName" - ], - "properties": { - "companyName": { - "type": "string" - } - } - }, - "models.CompanyResponse": { - "type": "object", - "properties": { - "company": { - "$ref": "#/definitions/models.Company" - }, - "message": { - "type": "string" - } - } - }, - "models.CompanyUpdateRequest": { - "type": "object", - "properties": { - "companyName": { - "type": "string" - }, - "isActive": { - "type": "boolean" - } - } - }, "models.CreateAftersalesOrderDTO": { "type": "object", "required": [ @@ -2537,6 +2323,46 @@ const docTemplate = `{ } } }, + "models.CreateProjectOrderDTO": { + "type": "object", + "required": [ + "companyAddress", + "companyName", + "contactName", + "contactPhone", + "projectType", + "siteDescription" + ], + "properties": { + "companyAddress": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "projectType": { + "type": "string", + "enum": [ + "survey", + "implementation", + "maintenance", + "other" + ] + }, + "siteDescription": { + "type": "string" + }, + "technicianId": { + "type": "integer" + } + } + }, "models.CreateUserDTO": { "type": "object", "required": [ @@ -2594,6 +2420,9 @@ const docTemplate = `{ "rejectReason": { "type": "string" }, + "responsibleSignature": { + "type": "string" + }, "signature": { "type": "string" } @@ -2690,51 +2519,6 @@ const docTemplate = `{ } } }, - "models.GenerateSerialDTO": { - "type": "object", - "required": [ - "companyName" - ], - "properties": { - "companyName": { - "type": "string" - }, - "quantity": { - "type": "integer", - "maximum": 1000, - "minimum": 1 - }, - "validDays": { - "type": "integer", - "maximum": 3650, - "minimum": 1 - } - } - }, - "models.GenerateWithPrefixDTO": { - "type": "object", - "required": [ - "companyName" - ], - "properties": { - "companyName": { - "type": "string" - }, - "quantity": { - "type": "integer", - "maximum": 1000, - "minimum": 1 - }, - "serialPrefix": { - "type": "string" - }, - "validDays": { - "type": "integer", - "maximum": 3650, - "minimum": 1 - } - } - }, "models.LoginDTO": { "type": "object", "required": [ @@ -2794,6 +2578,20 @@ const docTemplate = `{ } } }, + "models.ProjectEngineerCompleteDTO": { + "type": "object", + "required": [ + "engineerSignature" + ], + "properties": { + "completionNote": { + "type": "string" + }, + "engineerSignature": { + "type": "string" + } + } + }, "models.QRCodeDTO": { "type": "object", "properties": { @@ -2827,6 +2625,17 @@ const docTemplate = `{ } } }, + "models.ReassignProjectOrderDTO": { + "type": "object", + "required": [ + "technicianId" + ], + "properties": { + "technicianId": { + "type": "integer" + } + } + }, "models.SubmitForConfirmationDTO": { "type": "object", "required": [ @@ -2838,6 +2647,17 @@ const docTemplate = `{ } } }, + "models.SubmitProjectCompletionDTO": { + "type": "object", + "required": [ + "completionNote" + ], + "properties": { + "completionNote": { + "type": "string" + } + } + }, "models.UpdateAftersalesOrderDTO": { "type": "object", "properties": { @@ -2901,17 +2721,35 @@ const docTemplate = `{ } } }, - "models.UpdateSerialDTO": { + "models.UpdateProjectOrderDTO": { "type": "object", "properties": { - "companyName": { + "companyAddress": { "type": "string" }, - "isActive": { - "type": "boolean" - }, - "validUntil": { + "completionNote": { "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "projectType": { + "type": "string", + "enum": [ + "survey", + "implementation", + "maintenance", + "other" + ] + }, + "siteDescription": { + "type": "string" + }, + "technicianId": { + "type": "integer" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 64d2464..8e6609d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -554,6 +554,56 @@ } } }, + "/aftersales/{serialNumber}/site-images": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "售后工单查询" + ], + "summary": "上传售后现场图片", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "现场图片", + "name": "files", + "in": "formData", + "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" + } + } + } + } + }, "/aftersales/{serialNumber}/submit": { "post": { "security": [ @@ -820,466 +870,6 @@ } } }, - "/companies": { - "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" - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "创建新的企业", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "企业管理" - ], - "summary": "创建企业", - "parameters": [ - { - "description": "企业数据", - "name": "companyData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CompanyDataRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.CompanyResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "409": { - "description": "Conflict", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/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": [ - { - "BearerAuth": [] - } - ], - "description": "更新企业信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "企业管理" - ], - "summary": "更新企业信息", - "parameters": [ - { - "type": "string", - "description": "企业名称", - "name": "companyName", - "in": "path", - "required": true - }, - { - "description": "企业数据", - "name": "companyData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.CompanyUpdateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.CompanyResponse" - } - }, - "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" - } - }, - "409": { - "description": "Conflict", - "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": "companyName", - "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" - } - } - } - } - }, - "/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": [ @@ -1955,21 +1545,21 @@ } } }, - "/serials": { + "/project-orders": { "get": { "security": [ { "BearerAuth": [] } ], - "description": "获取序列号列表,支持分页和搜索", + "description": "支持分页、搜索、按状态/服务类型/技术员筛选", "produces": [ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "获取序列号列表", + "summary": "获取项目工单列表", "parameters": [ { "type": "integer", @@ -1979,7 +1569,7 @@ }, { "type": "integer", - "description": " ", + "description": "每页数量", "name": "limit", "in": "query" }, @@ -1988,6 +1578,30 @@ "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": { @@ -2010,16 +1624,14 @@ } } } - } - }, - "/serials/generate": { + }, "post": { "security": [ { "BearerAuth": [] } ], - "description": "生成指定数量的序列号", + "description": "创建一个新的项目工单并分配编号", "consumes": [ "application/json" ], @@ -2027,17 +1639,17 @@ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "生成序列号", + "summary": "创建项目工单", "parameters": [ { - "description": "生成数据", - "name": "generateData", + "description": "工单数据", + "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.GenerateSerialDTO" + "$ref": "#/definitions/models.CreateProjectOrderDTO" } } ], @@ -2069,97 +1681,27 @@ } } }, - "/serials/generate-with-prefix": { - "post": { + "/project-orders/{serialNumber}": { + "get": { "security": [ { "BearerAuth": [] } ], - "description": "生成带有指定前缀的序列号", - "consumes": [ - "application/json" - ], "produces": [ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "带前缀生成序列号", - "parameters": [ - { - "description": "生成数据", - "name": "generateData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.GenerateWithPrefixDTO" - } - } - ], - "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" - } - } - } - } - }, - "/serials/{serialNumber}": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "更新指定序列号的信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "序列号管理" - ], - "summary": "更新序列号信息", + "summary": "获取项目工单详情", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true - }, - { - "description": "更新数据", - "name": "updateData", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.UpdateSerialDTO" - } } ], "responses": { @@ -2169,12 +1711,6 @@ "$ref": "#/definitions/models.DataResponse" } }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, "401": { "description": "Unauthorized", "schema": { @@ -2186,24 +1722,58 @@ "schema": { "$ref": "#/definitions/models.ErrorResponse" } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } - } - }, - "/serials/{serialNumber}/qrcode": { - "post": { + }, + "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": [] } ], - "description": "为指定序列号生成查询二维码", "consumes": [ "application/json" ], @@ -2211,20 +1781,187 @@ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "生成二维码", + "summary": "更新项目工单", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true }, { - "description": "二维码数据", - "name": "qrCodeData", + "description": "更新数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdateProjectOrderDTO" + } + } + ], + "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" + } + } + } + } + }, + "/project-orders/{serialNumber}/complete": { + "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.ProjectEngineerCompleteDTO" + } + } + ], + "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" + } + } + } + } + }, + "/project-orders/{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" + } + } + } + } + }, + "/project-orders/{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" @@ -2249,39 +1986,127 @@ "schema": { "$ref": "#/definitions/models.ErrorResponse" } + } + } + } + }, + "/project-orders/{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" } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } } }, - "/serials/{serialNumber}/query": { - "get": { - "description": "查询指定序列号的详细信息", + "/project-orders/{serialNumber}/reassign": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "序列号查询" + "项目工单" ], - "summary": "查询序列号信息", + "summary": "重新分配技术员", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true + }, + { + "description": "新技术员 ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ReassignProjectOrderDTO" + } + } + ], + "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" + } + } + } + } + }, + "/project-orders/{serialNumber}/site-images": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "项目工单查询" + ], + "summary": "上传项目现场图片", + "parameters": [ + { + "type": "string", + "description": "工单号", + "name": "serialNumber", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "现场图片", + "name": "files", + "in": "formData", + "required": true } ], "responses": { @@ -2302,45 +2127,51 @@ "schema": { "$ref": "#/definitions/models.ErrorResponse" } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } } }, - "/serials/{serialNumber}/revoke": { + "/project-orders/{serialNumber}/submit": { "post": { "security": [ { "BearerAuth": [] } ], - "description": "吊销指定序列号", + "description": "技术员填写处理结果后提交,工单进入\"待完成确认\"状态", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "序列号管理" + "项目工单" ], - "summary": "吊销序列号", + "summary": "提交完成确认", "parameters": [ { "type": "string", - "description": "序列号", + "description": "工单号", "name": "serialNumber", "in": "path", "required": true + }, + { + "description": "处理结果", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SubmitProjectCompletionDTO" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.BaseResponse" + "$ref": "#/definitions/models.DataResponse" } }, "400": { @@ -2354,18 +2185,6 @@ "schema": { "$ref": "#/definitions/models.ErrorResponse" } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } } } } @@ -2459,39 +2278,6 @@ } } }, - "models.CompanyDataRequest": { - "type": "object", - "required": [ - "companyName" - ], - "properties": { - "companyName": { - "type": "string" - } - } - }, - "models.CompanyResponse": { - "type": "object", - "properties": { - "company": { - "$ref": "#/definitions/models.Company" - }, - "message": { - "type": "string" - } - } - }, - "models.CompanyUpdateRequest": { - "type": "object", - "properties": { - "companyName": { - "type": "string" - }, - "isActive": { - "type": "boolean" - } - } - }, "models.CreateAftersalesOrderDTO": { "type": "object", "required": [ @@ -2531,6 +2317,46 @@ } } }, + "models.CreateProjectOrderDTO": { + "type": "object", + "required": [ + "companyAddress", + "companyName", + "contactName", + "contactPhone", + "projectType", + "siteDescription" + ], + "properties": { + "companyAddress": { + "type": "string" + }, + "companyName": { + "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "projectType": { + "type": "string", + "enum": [ + "survey", + "implementation", + "maintenance", + "other" + ] + }, + "siteDescription": { + "type": "string" + }, + "technicianId": { + "type": "integer" + } + } + }, "models.CreateUserDTO": { "type": "object", "required": [ @@ -2588,6 +2414,9 @@ "rejectReason": { "type": "string" }, + "responsibleSignature": { + "type": "string" + }, "signature": { "type": "string" } @@ -2684,51 +2513,6 @@ } } }, - "models.GenerateSerialDTO": { - "type": "object", - "required": [ - "companyName" - ], - "properties": { - "companyName": { - "type": "string" - }, - "quantity": { - "type": "integer", - "maximum": 1000, - "minimum": 1 - }, - "validDays": { - "type": "integer", - "maximum": 3650, - "minimum": 1 - } - } - }, - "models.GenerateWithPrefixDTO": { - "type": "object", - "required": [ - "companyName" - ], - "properties": { - "companyName": { - "type": "string" - }, - "quantity": { - "type": "integer", - "maximum": 1000, - "minimum": 1 - }, - "serialPrefix": { - "type": "string" - }, - "validDays": { - "type": "integer", - "maximum": 3650, - "minimum": 1 - } - } - }, "models.LoginDTO": { "type": "object", "required": [ @@ -2788,6 +2572,20 @@ } } }, + "models.ProjectEngineerCompleteDTO": { + "type": "object", + "required": [ + "engineerSignature" + ], + "properties": { + "completionNote": { + "type": "string" + }, + "engineerSignature": { + "type": "string" + } + } + }, "models.QRCodeDTO": { "type": "object", "properties": { @@ -2821,6 +2619,17 @@ } } }, + "models.ReassignProjectOrderDTO": { + "type": "object", + "required": [ + "technicianId" + ], + "properties": { + "technicianId": { + "type": "integer" + } + } + }, "models.SubmitForConfirmationDTO": { "type": "object", "required": [ @@ -2832,6 +2641,17 @@ } } }, + "models.SubmitProjectCompletionDTO": { + "type": "object", + "required": [ + "completionNote" + ], + "properties": { + "completionNote": { + "type": "string" + } + } + }, "models.UpdateAftersalesOrderDTO": { "type": "object", "properties": { @@ -2895,17 +2715,35 @@ } } }, - "models.UpdateSerialDTO": { + "models.UpdateProjectOrderDTO": { "type": "object", "properties": { - "companyName": { + "companyAddress": { "type": "string" }, - "isActive": { - "type": "boolean" - }, - "validUntil": { + "completionNote": { "type": "string" + }, + "contactName": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "projectType": { + "type": "string", + "enum": [ + "survey", + "implementation", + "maintenance", + "other" + ] + }, + "siteDescription": { + "type": "string" + }, + "technicianId": { + "type": "integer" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e3f89b5..6d785c3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -37,27 +37,6 @@ definitions: updatedAt: type: string type: object - models.CompanyDataRequest: - properties: - companyName: - type: string - required: - - companyName - type: object - models.CompanyResponse: - properties: - company: - $ref: '#/definitions/models.Company' - message: - type: string - type: object - models.CompanyUpdateRequest: - properties: - companyName: - type: string - isActive: - type: boolean - type: object models.CreateAftersalesOrderDTO: properties: companyAddress: @@ -86,6 +65,35 @@ definitions: - issueDescription - serviceType type: object + models.CreateProjectOrderDTO: + properties: + companyAddress: + type: string + companyName: + type: string + contactName: + type: string + contactPhone: + type: string + projectType: + enum: + - survey + - implementation + - maintenance + - other + type: string + siteDescription: + type: string + technicianId: + type: integer + required: + - companyAddress + - companyName + - contactName + - contactPhone + - projectType + - siteDescription + type: object models.CreateUserDTO: properties: email: @@ -124,6 +132,8 @@ definitions: type: string rejectReason: type: string + responsibleSignature: + type: string signature: type: string required: @@ -190,38 +200,6 @@ definitions: - employeeName - position type: object - models.GenerateSerialDTO: - properties: - companyName: - type: string - quantity: - maximum: 1000 - minimum: 1 - type: integer - validDays: - maximum: 3650 - minimum: 1 - type: integer - required: - - companyName - type: object - models.GenerateWithPrefixDTO: - properties: - companyName: - type: string - quantity: - maximum: 1000 - minimum: 1 - type: integer - serialPrefix: - type: string - validDays: - maximum: 3650 - minimum: 1 - type: integer - required: - - companyName - type: object models.LoginDTO: properties: password: @@ -261,6 +239,15 @@ definitions: pagination: $ref: '#/definitions/models.Pagination' type: object + models.ProjectEngineerCompleteDTO: + properties: + completionNote: + type: string + engineerSignature: + type: string + required: + - engineerSignature + type: object models.QRCodeDTO: properties: baseUrl: @@ -282,6 +269,13 @@ definitions: required: - technicianId type: object + models.ReassignProjectOrderDTO: + properties: + technicianId: + type: integer + required: + - technicianId + type: object models.SubmitForConfirmationDTO: properties: resolutionNote: @@ -289,6 +283,13 @@ definitions: required: - resolutionNote type: object + models.SubmitProjectCompletionDTO: + properties: + completionNote: + type: string + required: + - completionNote + type: object models.UpdateAftersalesOrderDTO: properties: companyAddress: @@ -331,14 +332,27 @@ definitions: - email - name type: object - models.UpdateSerialDTO: + models.UpdateProjectOrderDTO: properties: - companyName: + companyAddress: type: string - isActive: - type: boolean - validUntil: + completionNote: type: string + contactName: + type: string + contactPhone: + type: string + projectType: + enum: + - survey + - implementation + - maintenance + - other + type: string + siteDescription: + type: string + technicianId: + type: integer type: object models.UpdateUserDTO: properties: @@ -766,6 +780,39 @@ paths: summary: 重新分配技术员 tags: - 售后工单 + /aftersales/{serialNumber}/site-images: + post: + consumes: + - multipart/form-data + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 现场图片 + in: formData + name: files + required: true + type: file + 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' + summary: 上传售后现场图片 + tags: + - 售后工单查询 /aftersales/{serialNumber}/submit: post: consumes: @@ -934,300 +981,6 @@ paths: summary: 更新用户信息 tags: - 认证 - /companies: - 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: - - 企业管理 - post: - consumes: - - application/json - description: 创建新的企业 - parameters: - - description: 企业数据 - in: body - name: companyData - required: true - schema: - $ref: '#/definitions/models.CompanyDataRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.CompanyResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/models.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/models.ErrorResponse' - "409": - description: Conflict - schema: - $ref: '#/definitions/models.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/models.ErrorResponse' - security: - - BearerAuth: [] - summary: 创建企业 - tags: - - 企业管理 - /companies/{companyName}: - delete: - description: 删除企业及其关联序列号 - parameters: - - description: 企业名称 - in: path - name: companyName - 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: - - 企业管理 - 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 - description: 更新企业信息 - parameters: - - description: 企业名称 - in: path - name: companyName - required: true - type: string - - description: 企业数据 - in: body - name: companyData - required: true - schema: - $ref: '#/definitions/models.CompanyUpdateRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.CompanyResponse' - "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' - "409": - description: Conflict - schema: - $ref: '#/definitions/models.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/models.ErrorResponse' - security: - - BearerAuth: [] - 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: 获取员工序列号列表,支持分页和搜索 @@ -1657,15 +1410,15 @@ paths: summary: 重置密码 tags: - 员工管理 - /serials: + /project-orders: get: - description: 获取序列号列表,支持分页和搜索 + description: 支持分页、搜索、按状态/服务类型/技术员筛选 parameters: - description: 页码 in: query name: page type: integer - - description: ' ' + - description: 每页数量 in: query name: limit type: integer @@ -1673,6 +1426,22 @@ paths: 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: @@ -1690,26 +1459,20 @@ paths: $ref: '#/definitions/models.ErrorResponse' security: - BearerAuth: [] - summary: 获取序列号列表 + summary: 获取项目工单列表 tags: - - 序列号管理 - /serials/{serialNumber}: - put: + - 项目工单 + post: consumes: - application/json - description: 更新指定序列号的信息 + description: 创建一个新的项目工单并分配编号 parameters: - - description: 序列号 - in: path - name: serialNumber - required: true - type: string - - description: 更新数据 + - description: 工单数据 in: body - name: updateData + name: data required: true schema: - $ref: '#/definitions/models.UpdateSerialDTO' + $ref: '#/definitions/models.CreateProjectOrderDTO' produces: - application/json responses: @@ -1725,99 +1488,19 @@ paths: 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: 更新序列号信息 + summary: 创建项目工单 tags: - - 序列号管理 - /serials/{serialNumber}/qrcode: - post: - consumes: - - application/json - description: 为指定序列号生成查询二维码 + - 项目工单 + /project-orders/{serialNumber}: + delete: 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: - - 序列号管理 - /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: - - 序列号查询 - /serials/{serialNumber}/revoke: - post: - description: 吊销指定序列号 - parameters: - - description: 序列号 + - description: 工单号 in: path name: serialNumber required: true @@ -1837,31 +1520,53 @@ paths: 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' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/models.ErrorResponse' security: - BearerAuth: [] - summary: 吊销序列号 + summary: 获取项目工单详情 tags: - - 序列号管理 - /serials/generate: - post: + - 项目工单 + patch: consumes: - application/json - description: 生成指定数量的序列号 parameters: - - description: 生成数据 + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 更新数据 in: body - name: generateData + name: data required: true schema: - $ref: '#/definitions/models.GenerateSerialDTO' + $ref: '#/definitions/models.UpdateProjectOrderDTO' produces: - application/json responses: @@ -1877,27 +1582,32 @@ paths: description: Unauthorized schema: $ref: '#/definitions/models.ErrorResponse' - "500": - description: Internal Server Error + "403": + description: Forbidden schema: $ref: '#/definitions/models.ErrorResponse' security: - BearerAuth: [] - summary: 生成序列号 + summary: 更新项目工单 tags: - - 序列号管理 - /serials/generate-with-prefix: + - 项目工单 + /project-orders/{serialNumber}/complete: post: consumes: - application/json - description: 生成带有指定前缀的序列号 + description: 工程师上传现场图片后签字提交,工单进入已完成状态 parameters: - - description: 生成数据 + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 确认数据 in: body - name: generateData + name: data required: true schema: - $ref: '#/definitions/models.GenerateWithPrefixDTO' + $ref: '#/definitions/models.ProjectEngineerCompleteDTO' produces: - application/json responses: @@ -1913,15 +1623,204 @@ paths: description: Unauthorized schema: $ref: '#/definitions/models.ErrorResponse' - "500": - description: Internal Server Error + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/models.ErrorResponse' + summary: 工程师提交完成 + tags: + - 项目工单查询 + /project-orders/{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: 带前缀生成序列号 + summary: 强制关闭工单 tags: - - 序列号管理 + - 项目工单 + /project-orders/{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: + - 项目工单 + /project-orders/{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: + - 项目工单查询 + /project-orders/{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.ReassignProjectOrderDTO' + 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: + - 项目工单 + /project-orders/{serialNumber}/site-images: + post: + consumes: + - multipart/form-data + parameters: + - description: 工单号 + in: path + name: serialNumber + required: true + type: string + - description: 现场图片 + in: formData + name: files + required: true + type: file + 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' + summary: 上传项目现场图片 + tags: + - 项目工单查询 + /project-orders/{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.SubmitProjectCompletionDTO' + 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: + - 项目工单 /users/assignable: get: description: 用于售后工单分配选择技术员/管理员,无需分页 diff --git a/models/models.go b/models/models.go index 4ea7665..9ce4d52 100644 --- a/models/models.go +++ b/models/models.go @@ -20,7 +20,6 @@ type User struct { 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"` } @@ -32,22 +31,6 @@ type Company struct { CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Serials []Serial `gorm:"foreignKey:CompanyName;references:CompanyName" json:"-"` -} - -// Serial 模型 -type Serial struct { - ID uint `gorm:"primaryKey" json:"id"` - SerialNumber string `gorm:"uniqueIndex;size:255" json:"serialNumber"` - CompanyName string `gorm:"index;size:255" json:"companyName"` - ValidUntil *time.Time `json:"validUntil"` - 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"` - Company *Company `gorm:"foreignKey:CompanyName;references:CompanyName" json:"company,omitempty"` } // ProductTrace 产品溯源模型 @@ -129,28 +112,6 @@ type AdminResetPasswordDTO struct { NewPassword string `json:"newPassword" validate:"required,min=6"` } -// GenerateSerialDTO 生成序列号请求数据 -type GenerateSerialDTO struct { - CompanyName string `json:"companyName" validate:"required"` - Quantity int `json:"quantity" validate:"min=1,max=1000"` - ValidDays int `json:"validDays" validate:"min=1,max=3650"` -} - -// GenerateWithPrefixDTO 带前缀生成序列号请求数据 -type GenerateWithPrefixDTO struct { - CompanyName string `json:"companyName" validate:"required"` - Quantity int `json:"quantity" validate:"min=1,max=1000"` - ValidDays int `json:"validDays" validate:"min=1,max=3650"` - SerialPrefix string `json:"serialPrefix" validate:"omitempty,alphanum"` -} - -// UpdateSerialDTO 序列号更新请求数据 -type UpdateSerialDTO struct { - CompanyName string `json:"companyName,omitempty" validate:"omitempty"` - ValidUntil *time.Time `json:"validUntil,omitempty"` - IsActive *bool `json:"isActive,omitempty"` -} - // CreateProductTraceDTO 创建产品溯源请求数据 type CreateProductTraceDTO struct { CompanyName string `json:"companyName" validate:"required"` @@ -228,40 +189,6 @@ type QRCodeResponse struct { QueryURL string `json:"queryUrl"` } -// CompanyResponse 企业响应 -type CompanyResponse struct { - Message string `json:"message"` - Company Company `json:"company"` -} - -// CompanyDataRequest 企业数据请求 -type CompanyDataRequest struct { - CompanyName string `json:"companyName" validate:"required"` -} - -// CompanyUpdateRequest 企业更新请求 -type CompanyUpdateRequest struct { - CompanyName string `json:"companyName"` - IsActive *bool `json:"isActive"` -} - -// 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"` -} - // EmployeeSerial 员工序列号模型 type EmployeeSerial struct { ID uint `gorm:"primaryKey" json:"id"` diff --git a/routes/routes.go b/routes/routes.go index e404ff3..93a6d61 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -31,33 +31,11 @@ func SetupAPIRoutes(r *gin.RouterGroup) { authRoutes.POST("/change-password", middleware.JWTAuthMiddleware(), authController.ChangePassword) } - // 序列号路由 - serialsController := controllers.NewSerialsController() - serialsRoutes := r.Group("/serials") + // 控制台统计路由 + dashboardController := controllers.NewDashboardController() + dashboardRoutes := r.Group("/dashboard") { - 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(), middleware.AdminMiddleware(), serialsController.GenerateQRCode) - serialsRoutes.GET("/:serialNumber/query", serialsController.Query) - 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) - } - - // 企业管理路由 - companiesController := controllers.NewCompaniesController() - companiesRoutes := r.Group("/companies") - { - companiesRoutes.GET("/stats/overview", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.StatsOverview) - companiesRoutes.GET("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.FindAll) - companiesRoutes.GET("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.FindOne) - companiesRoutes.POST("/", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Create) - companiesRoutes.PATCH("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update) - companiesRoutes.PUT("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Update) - companiesRoutes.DELETE("/:companyName/serials/:serialNumber", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.DeleteSerial) - companiesRoutes.POST("/:companyName/revoke", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Revoke) - companiesRoutes.DELETE("/:companyName", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), companiesController.Delete) + dashboardRoutes.GET("/stats", middleware.JWTAuthMiddleware(), middleware.AdminMiddleware(), dashboardController.Stats) } // 产品溯源路由 diff --git a/services/companies_service.go b/services/companies_service.go deleted file mode 100644 index d2c81ac..0000000 --- a/services/companies_service.go +++ /dev/null @@ -1,502 +0,0 @@ -package services - -import ( - "errors" - "fmt" - "sort" - "time" - - "gorm.io/gorm" - - "git.beifan.cn/trace-system/backend-go/database" - "git.beifan.cn/trace-system/backend-go/models" -) - -// CompaniesService 企业管理服务 -type CompaniesService struct{} - -// FindAll 获取所有企业列表 -func (s *CompaniesService) FindAll(page int, limit int, search string) ([]models.Company, int, int, error) { - var companies []models.Company - var total int64 - - if page < 1 { - page = 1 - } - if limit < 1 { - limit = 20 - } - - offset := (page - 1) * limit - db := database.DB.Model(&models.Company{}) - - if search != "" { - db = db.Where("company_name LIKE ?", "%"+search+"%") - } - - if err := db.Count(&total).Error; err != nil { - return nil, 0, 0, errors.New("查询企业总数失败") - } - - result := db.Preload("Serials").Order("updated_at DESC").Offset(offset).Limit(limit).Find(&companies) - if result.Error != nil { - return nil, 0, 0, errors.New("查询企业列表失败") - } - - totalPages := 0 - if total > 0 { - totalPages = (int(total) + limit - 1) / limit - } - - return companies, int(total), totalPages, nil -} - -// FindOne 获取单个企业详情(含分页序列号) -func (s *CompaniesService) FindOne(companyName string, page int, limit int) (map[string]any, error) { - if page < 1 { - page = 1 - } - if limit < 1 { - limit = 20 - } - - var company models.Company - if err := database.DB.Where("company_name = ?", companyName).First(&company).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("企业不存在") - } - return nil, errors.New("查询企业失败") - } - - var allSerials []models.Serial - if err := database.DB.Preload("User").Where("company_name = ?", companyName).Order("created_at DESC").Find(&allSerials).Error; err != nil { - return nil, errors.New("查询企业序列号失败") - } - - now := time.Now() - serialCount := len(allSerials) - activeCount := 0 - disabledCount := 0 - expiredCount := 0 - - for _, serial := range allSerials { - if !serial.IsActive { - disabledCount++ - continue - } - if serial.ValidUntil != nil && serial.ValidUntil.Before(now) { - expiredCount++ - continue - } - activeCount++ - } - - offset := (page - 1) * limit - end := offset + limit - if offset > len(allSerials) { - offset = len(allSerials) - } - if end > len(allSerials) { - end = len(allSerials) - } - paginatedSerials := allSerials[offset:end] - - serialItems := make([]map[string]any, 0, len(paginatedSerials)) - for _, serial := range paginatedSerials { - createdBy := "" - if serial.User != nil { - createdBy = serial.User.Name - } - serialItems = append(serialItems, map[string]any{ - "serialNumber": serial.SerialNumber, - "validUntil": serial.ValidUntil, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "createdBy": createdBy, - }) - } - - monthlyStatsMap := map[string]int{} - for i := 11; i >= 0; i-- { - date := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, time.Local) - monthKey := date.Format("2006-01") - monthlyStatsMap[monthKey] = 0 - } - for _, serial := range allSerials { - monthKey := serial.CreatedAt.Format("2006-01") - if _, ok := monthlyStatsMap[monthKey]; ok { - monthlyStatsMap[monthKey]++ - } - } - - monthlyStats := make([]map[string]any, 0) - for i := 11; i >= 0; i-- { - date := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, time.Local) - monthKey := date.Format("2006-01") - count := monthlyStatsMap[monthKey] - if count > 0 { - monthlyStats = append(monthlyStats, map[string]any{"month": monthKey, "count": count}) - } - } - - return map[string]any{ - "companyName": company.CompanyName, - "serialCount": serialCount, - "activeCount": activeCount, - "disabledCount": disabledCount, - "expiredCount": expiredCount, - "firstCreated": company.CreatedAt, - "lastCreated": company.UpdatedAt, - "status": map[bool]string{true: "active", false: "disabled"}[company.IsActive], - "serials": serialItems, - "monthlyStats": monthlyStats, - "pagination": map[string]any{ - "page": page, - "limit": limit, - "total": serialCount, - "totalPages": func() int { - if serialCount == 0 { - return 0 - } - return (serialCount + limit - 1) / limit - }(), - }, - }, nil -} - -// Create 创建企业 -func (s *CompaniesService) Create(companyName string) (*models.Company, error) { - var existingCompany models.Company - result := database.DB.Where("company_name = ?", companyName).First(&existingCompany) - if result.Error == nil { - return nil, errors.New("企业名称已存在") - } - - company := models.Company{ - CompanyName: companyName, - IsActive: true, - } - - result = database.DB.Create(&company) - if result.Error != nil { - return nil, errors.New("创建企业失败") - } - - return &company, nil -} - -// Update 更新企业信息 -func (s *CompaniesService) Update(companyName string, newCompanyName string, isActive *bool) (*models.Company, error) { - var company models.Company - result := database.DB.Where("company_name = ?", companyName).First(&company) - if result.Error != nil { - return nil, errors.New("企业不存在") - } - - if newCompanyName == "" { - newCompanyName = companyName - } - - if newCompanyName != companyName { - var existingCompany models.Company - checkResult := database.DB.Where("company_name = ?", newCompanyName).First(&existingCompany) - if checkResult.Error == nil { - return nil, errors.New("企业名称已存在") - } - } - - err := database.DB.Transaction(func(tx *gorm.DB) error { - if newCompanyName != companyName { - if err := tx.Model(&models.Serial{}).Where("company_name = ?", companyName).Update("company_name", newCompanyName).Error; err != nil { - return fmt.Errorf("更新企业赋码企业名称失败: %w", err) - } - if err := tx.Model(&models.EmployeeSerial{}).Where("company_name = ?", companyName).Update("company_name", newCompanyName).Error; err != nil { - return fmt.Errorf("更新员工赋码企业名称失败: %w", err) - } - company.CompanyName = newCompanyName - } - - if isActive != nil { - company.IsActive = *isActive - } - - if err := tx.Save(&company).Error; err != nil { - return fmt.Errorf("更新企业信息失败: %w", err) - } - - return nil - }) - if err != nil { - return nil, errors.New(err.Error()) - } - - return &company, nil -} - -// Delete 删除企业(同时删除关联序列号) -func (s *CompaniesService) Delete(companyName string) error { - var company models.Company - if err := database.DB.Where("company_name = ?", companyName).First(&company).Error; err != nil { - return errors.New("企业不存在") - } - - if err := database.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Where("company_name = ?", companyName).Delete(&models.Serial{}).Error; err != nil { - return err - } - if err := tx.Where("company_name = ?", companyName).Delete(&models.EmployeeSerial{}).Error; err != nil { - return err - } - if err := tx.Delete(&company).Error; err != nil { - return err - } - return nil - }); err != nil { - return errors.New("删除企业失败") - } - - return nil -} - -// DeleteSerial 删除企业下指定企业赋码序列号 -func (s *CompaniesService) DeleteSerial(companyName string, serialNumber string) error { - var serial models.Serial - err := database.DB.Where("serial_number = ? AND company_name = ?", serialNumber, companyName).First(&serial).Error - if err != nil { - return errors.New("序列号不存在或不属于该企业") - } - - if err := database.DB.Delete(&serial).Error; err != nil { - return errors.New("删除序列号失败") - } - - return nil -} - -// Revoke 吊销企业(吊销所有企业赋码与员工赋码) -func (s *CompaniesService) Revoke(companyName string) error { - var company models.Company - if err := database.DB.Where("company_name = ?", companyName).First(&company).Error; err != nil { - return errors.New("企业不存在") - } - - if err := database.DB.Transaction(func(tx *gorm.DB) error { - if err := tx.Model(&models.Serial{}).Where("company_name = ?", companyName).Update("is_active", false).Error; err != nil { - return err - } - if err := tx.Model(&models.EmployeeSerial{}).Where("company_name = ?", companyName).Update("is_active", false).Error; err != nil { - return err - } - if err := tx.Model(&company).Update("is_active", false).Error; err != nil { - return err - } - return nil - }); err != nil { - return errors.New("吊销企业失败") - } - - return nil -} - -// GetStats 获取企业统计(兼容 Node 返回结构) -func (s *CompaniesService) GetStats() (map[string]any, error) { - now := time.Now() - - var companies []models.Company - if err := database.DB.Order("updated_at DESC").Find(&companies).Error; err != nil { - return nil, errors.New("查询企业统计失败") - } - - var serials []models.Serial - if err := database.DB.Order("created_at DESC").Find(&serials).Error; err != nil { - return nil, errors.New("查询序列号统计失败") - } - - var employeeSerials []models.EmployeeSerial - if err := database.DB.Order("created_at DESC").Find(&employeeSerials).Error; err != nil { - return nil, errors.New("查询员工序列号统计失败") - } - - var aftersales []models.AftersalesOrder - if err := database.DB.Preload("Technician").Order("created_at DESC").Find(&aftersales).Error; err != nil { - return nil, errors.New("查询售后工单统计失败") - } - - companyCount := len(companies) - serialCount := len(serials) - employeeSerialCount := len(employeeSerials) - activeCount := 0 - for _, serial := range serials { - if serial.IsActive && (serial.ValidUntil == nil || serial.ValidUntil.After(now)) { - activeCount++ - } - } - inactiveCount := serialCount - activeCount - - monthlyItems := make([]map[string]any, 0) - for i := 11; i >= 0; i-- { - date := time.Date(now.Year(), now.Month()-time.Month(i), 1, 0, 0, 0, 0, time.Local) - monthStr := date.Format("2006-01") - monthSerialCount := 0 - companySet := map[string]bool{} - for _, serial := range serials { - if serial.CreatedAt.Year() == date.Year() && serial.CreatedAt.Month() == date.Month() { - monthSerialCount++ - companySet[serial.CompanyName] = true - } - } - if monthSerialCount > 0 { - monthlyItems = append(monthlyItems, map[string]any{ - "month": monthStr, - "company_count": len(companySet), - "serial_count": monthSerialCount, - }) - } - } - - recentCompanies := make([]map[string]any, 0) - for i, company := range companies { - if i >= 10 { - break - } - recentCompanies = append(recentCompanies, map[string]any{ - "companyName": company.CompanyName, - "lastCreated": company.UpdatedAt, - "status": map[bool]string{true: "active", false: "disabled"}[company.IsActive], - }) - } - - recentSerials := make([]map[string]any, 0) - // 添加企业序列号 - for _, serial := range serials { - recentSerials = append(recentSerials, map[string]any{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "type": "company", - }) - } - // 添加员工序列号 - for _, serial := range employeeSerials { - recentSerials = append(recentSerials, map[string]any{ - "serialNumber": serial.SerialNumber, - "companyName": serial.CompanyName, - "isActive": serial.IsActive, - "createdAt": serial.CreatedAt, - "type": "employee", - "position": serial.Position, - "employeeName": serial.EmployeeName, - }) - } - // 按创建时间排序,保留最新的10条 - sort.Slice(recentSerials, func(i, j int) bool { - return recentSerials[i]["createdAt"].(time.Time).After(recentSerials[j]["createdAt"].(time.Time)) - }) - if len(recentSerials) > 10 { - recentSerials = recentSerials[:10] - } - - aftersalesTotal := len(aftersales) - aftersalesPending := 0 - aftersalesClosed := 0 - aftersalesRejected := 0 - for _, o := range aftersales { - switch o.WorkOrderStatus { - case "pending_confirmation": - aftersalesPending++ - case "closed": - aftersalesClosed++ - case "rejected": - aftersalesRejected++ - } - } - - recentAftersales := make([]map[string]any, 0) - for i, order := range aftersales { - if i >= 10 { - break - } - technicianName := "" - if order.Technician != nil { - technicianName = order.Technician.Name - } - recentAftersales = append(recentAftersales, map[string]any{ - "serialNumber": order.SerialNumber, - "companyName": order.CompanyName, - "serviceType": order.ServiceType, - "workOrderStatus": order.WorkOrderStatus, - "authorizationStatus": order.AuthorizationStatus, - "technicianName": technicianName, - "createdAt": order.CreatedAt, - }) - } - - return map[string]any{ - "overview": map[string]any{ - "totalCompanies": companyCount, - "totalSerials": serialCount, - "totalEmployeeSerials": employeeSerialCount, - "activeSerials": activeCount, - "inactiveSerials": inactiveCount, - "totalAftersales": aftersalesTotal, - "pendingConfirmation": aftersalesPending, - "closedAftersales": aftersalesClosed, - "rejectedAftersales": aftersalesRejected, - }, - "monthlyStats": monthlyItems, - "recentCompanies": recentCompanies, - "recentSerials": recentSerials, - "recentAftersales": recentAftersales, - }, nil -} - -// GetStatsOverview 获取企业统计概览 -func (s *CompaniesService) GetStatsOverview() (*models.CompanyStatsOverviewDTO, error) { - stats := &models.CompanyStatsOverviewDTO{} - - if err := database.DB.Model(&models.Company{}).Count(&stats.TotalCompanies).Error; err != nil { - return nil, errors.New("统计企业总数失败") - } - - if err := database.DB.Model(&models.Company{}).Where("is_active = ?", true).Count(&stats.ActiveCompanies).Error; err != nil { - return nil, errors.New("统计启用企业数量失败") - } - - stats.InactiveCompanies = stats.TotalCompanies - stats.ActiveCompanies - - if err := database.DB.Model(&models.Serial{}).Count(&stats.TotalSerials).Error; err != nil { - return nil, errors.New("统计企业赋码总数失败") - } - - if err := database.DB.Model(&models.Serial{}).Where("is_active = ?", true).Count(&stats.ActiveSerials).Error; err != nil { - return nil, errors.New("统计有效企业赋码数量失败") - } - - stats.RevokedSerials = stats.TotalSerials - stats.ActiveSerials - - if err := database.DB.Model(&models.EmployeeSerial{}).Count(&stats.TotalEmployeeSerials).Error; err != nil { - return nil, errors.New("统计员工赋码总数失败") - } - - if err := database.DB.Model(&models.EmployeeSerial{}).Where("is_active = ?", true).Count(&stats.ActiveEmployeeSerials).Error; err != nil { - return nil, errors.New("统计有效员工赋码数量失败") - } - - stats.RevokedEmployeeSerials = stats.TotalEmployeeSerials - stats.ActiveEmployeeSerials - - if err := database.DB.Model(&models.AftersalesOrder{}).Count(&stats.TotalAftersales).Error; err != nil { - return nil, errors.New("统计售后工单总数失败") - } - if err := database.DB.Model(&models.AftersalesOrder{}).Where("work_order_status = ?", "pending_confirmation").Count(&stats.PendingConfirmation).Error; err != nil { - return nil, errors.New("统计待客户确认工单失败") - } - if err := database.DB.Model(&models.AftersalesOrder{}).Where("work_order_status = ?", "closed").Count(&stats.ClosedAftersales).Error; err != nil { - return nil, errors.New("统计已完成工单失败") - } - if err := database.DB.Model(&models.AftersalesOrder{}).Where("work_order_status = ?", "rejected").Count(&stats.RejectedAftersales).Error; err != nil { - return nil, errors.New("统计已退回工单失败") - } - - return stats, nil -} diff --git a/services/dashboard_service.go b/services/dashboard_service.go new file mode 100644 index 0000000..6c68606 --- /dev/null +++ b/services/dashboard_service.go @@ -0,0 +1,64 @@ +package services + +import ( + "errors" + + "git.beifan.cn/trace-system/backend-go/database" + "git.beifan.cn/trace-system/backend-go/models" +) + +// DashboardService 控制台统计服务 +type DashboardService struct{} + +// GetStats 获取控制台工单统计 +func (s *DashboardService) GetStats() (map[string]any, error) { + var aftersales []models.AftersalesOrder + if err := database.DB.Preload("Technician").Order("created_at DESC").Find(&aftersales).Error; err != nil { + return nil, errors.New("查询售后工单统计失败") + } + + aftersalesTotal := len(aftersales) + aftersalesPending := 0 + aftersalesClosed := 0 + aftersalesRejected := 0 + for _, order := range aftersales { + switch order.WorkOrderStatus { + case "pending_confirmation": + aftersalesPending++ + case "closed": + aftersalesClosed++ + case "rejected": + aftersalesRejected++ + } + } + + recentAftersales := make([]map[string]any, 0) + for i, order := range aftersales { + if i >= 10 { + break + } + technicianName := "" + if order.Technician != nil { + technicianName = order.Technician.Name + } + recentAftersales = append(recentAftersales, map[string]any{ + "serialNumber": order.SerialNumber, + "companyName": order.CompanyName, + "serviceType": order.ServiceType, + "workOrderStatus": order.WorkOrderStatus, + "authorizationStatus": order.AuthorizationStatus, + "technicianName": technicianName, + "createdAt": order.CreatedAt, + }) + } + + return map[string]any{ + "overview": map[string]any{ + "totalAftersales": aftersalesTotal, + "pendingConfirmation": aftersalesPending, + "closedAftersales": aftersalesClosed, + "rejectedAftersales": aftersalesRejected, + }, + "recentAftersales": recentAftersales, + }, nil +} diff --git a/services/serials_service.go b/services/serials_service.go deleted file mode 100644 index 2c30833..0000000 --- a/services/serials_service.go +++ /dev/null @@ -1,271 +0,0 @@ -package services - -import ( - "crypto/rand" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "os" - "strings" - "time" - - "github.com/google/uuid" - qr "github.com/yeqown/go-qrcode/v2" - "github.com/yeqown/go-qrcode/writer/standard" - - "git.beifan.cn/trace-system/backend-go/database" - "git.beifan.cn/trace-system/backend-go/models" -) - -// SerialsService 序列号服务 -type SerialsService struct{} - -// Generate 生成序列号 -func (s *SerialsService) Generate( - companyName string, - quantity int, - validDays int, - userId uint, - prefix ...string, -) ([]models.Serial, error) { - var serials []models.Serial - validUntil := time.Now().AddDate(0, 0, validDays) - - // 检查公司是否存在,不存在则创建 - var company models.Company - result := database.DB.Where("company_name = ?", companyName).First(&company) - if result.Error != nil { - company = models.Company{ - CompanyName: companyName, - IsActive: true, - } - result = database.DB.Create(&company) - if result.Error != nil { - return nil, fmt.Errorf("创建公司失败: %w", result.Error) - } - } - - // 生成序列号前缀 - var serialPrefix string - if len(prefix) > 0 && prefix[0] != "" { - serialPrefix = strings.ToUpper(strings.ReplaceAll(prefix[0], "[^A-Z0-9]", "")) - } else { - serialPrefix = fmt.Sprintf("BF%d", time.Now().Year()%100) - } - - // 预生成所有序列号 - serialNumbers := make(map[string]bool) - for i := 0; i < quantity; { - randomBytes := make([]byte, 3) - if _, err := rand.Read(randomBytes); err != nil { - return nil, fmt.Errorf("生成随机数失败: %w", err) - } - randomPart := hex.EncodeToString(randomBytes)[:6] - serialNumber := fmt.Sprintf("%s%s", serialPrefix, randomPart) - - if serialNumbers[serialNumber] { - continue - } - - var existingSerial models.Serial - checkResult := database.DB.Where("serial_number = ?", serialNumber).First(&existingSerial) - if checkResult.Error != nil { - serialNumbers[serialNumber] = true - i++ - } - } - - for serialNumber := range serialNumbers { - serial := models.Serial{ - SerialNumber: strings.ToUpper(serialNumber), - CompanyName: companyName, - ValidUntil: &validUntil, - CreatedBy: &userId, - IsActive: true, - } - serials = append(serials, serial) - } - - // 保存到数据库 - result = database.DB.Create(&serials) - if result.Error != nil { - return nil, fmt.Errorf("保存序列号失败: %w", result.Error) - } - - return serials, nil -} - -// GenerateQRCode 生成二维码 -func (s *SerialsService) GenerateQRCode( - serialNumber string, - baseUrl string, - requestHost string, - protocol string, -) (string, string, error) { - var serial models.Serial - result := database.DB.Preload("User").Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) - if result.Error != nil { - return "", "", fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在")) - } - - if !serial.IsActive { - return "", "", fmt.Errorf("序列号状态无效: %w", errors.New("序列号已被禁用")) - } - - if serial.ValidUntil != nil && serial.ValidUntil.Before(time.Now()) { - return "", "", fmt.Errorf("序列号已过期") - } - - // 确定查询 URL - if baseUrl == "" { - baseUrl = fmt.Sprintf("%s://%s/query.html", protocol, requestHost) - } - - var queryUrl string - if strings.Contains(baseUrl, "?") { - queryUrl = fmt.Sprintf("%s&serial=%s", baseUrl, serial.SerialNumber) - } else { - queryUrl = fmt.Sprintf("%s?serial=%s", baseUrl, serial.SerialNumber) - } - - // 生成二维码到临时文件 - filePath := fmt.Sprintf("temp_qr_%s.png", uuid.New().String()) - writer, err := standard.New(filePath, standard.WithQRWidth(6)) - if err != nil { - return "", "", fmt.Errorf("二维码写入器创建失败: %w", err) - } - - qrc, errCode := qr.New(queryUrl) - if errCode != nil { - os.Remove(filePath) - return "", "", fmt.Errorf("二维码创建失败: %w", errCode) - } - - if errSave := qrc.Save(writer); errSave != nil { - os.Remove(filePath) - return "", "", fmt.Errorf("二维码保存失败: %w", errSave) - } - - // 读取文件内容 - fileContent, errRead := os.ReadFile(filePath) - if errRead != nil { - os.Remove(filePath) - return "", "", fmt.Errorf("二维码文件读取失败: %w", errRead) - } - - // 删除临时文件 - os.Remove(filePath) - - // 转换为 base64 - qrCodeBase64 := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(fileContent)) - return qrCodeBase64, queryUrl, nil -} - -// Query 查询序列号信息 -func (s *SerialsService) Query(serialNumber string) (*models.Serial, error) { - var serial models.Serial - result := database.DB.Preload("User").Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) - if result.Error != nil { - return nil, fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在")) - } - - if serial.ValidUntil != nil && serial.ValidUntil.Before(time.Now()) { - return nil, fmt.Errorf("序列号已过期") - } - - return &serial, nil -} - -// FindAll 获取序列号列表 -func (s *SerialsService) FindAll(page int, limit int, search string) ([]models.Serial, int, int, error) { - var serials []models.Serial - var total int64 - - offset := (page - 1) * limit - db := database.DB.Preload("User") - - // 搜索条件 - if search != "" { - db = db.Where("serial_number LIKE ? OR company_name LIKE ?", "%"+search+"%", "%"+search+"%") - } - - // 获取总数 - countQuery := db.Model(&models.Serial{}) - if search != "" { - countQuery = countQuery.Where("serial_number LIKE ? OR company_name LIKE ?", "%"+search+"%", "%"+search+"%") - } - countQuery.Count(&total) - - // 分页查询 - result := db.Model(&models.Serial{}).Order("created_at DESC").Offset(offset).Limit(limit).Find(&serials) - if result.Error != nil { - return nil, 0, 0, fmt.Errorf("查询序列号列表失败: %w", result.Error) - } - - totalPages := (int(total) + limit - 1) / limit - - return serials, int(total), totalPages, nil -} - -// Update 更新序列号信息 -func (s *SerialsService) Update(serialNumber string, updateData models.UpdateSerialDTO) (*models.Serial, error) { - var serial models.Serial - result := database.DB.Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) - if result.Error != nil { - return nil, fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在")) - } - - if updateData.CompanyName != "" { - // 检查公司是否存在 - var company models.Company - companyResult := database.DB.Where("company_name = ?", updateData.CompanyName).First(&company) - if companyResult.Error != nil { - company = models.Company{ - CompanyName: updateData.CompanyName, - IsActive: true, - } - database.DB.Create(&company) - } - - serial.CompanyName = updateData.CompanyName - } - - if updateData.ValidUntil != nil { - serial.ValidUntil = updateData.ValidUntil - } - - if updateData.IsActive != nil { - serial.IsActive = *updateData.IsActive - } - - result = database.DB.Save(&serial) - if result.Error != nil { - return nil, fmt.Errorf("更新序列号失败: %w", result.Error) - } - - _ = database.DB.Preload("User").Where("serial_number = ?", serial.SerialNumber).First(&serial) - - return &serial, nil -} - -// Revoke 吊销序列号 -func (s *SerialsService) Revoke(serialNumber string) error { - var serial models.Serial - result := database.DB.Where("serial_number = ?", strings.ToUpper(serialNumber)).First(&serial) - if result.Error != nil { - return fmt.Errorf("查询序列号失败: %w", errors.New("序列号不存在")) - } - - if !serial.IsActive { - return fmt.Errorf("序列号状态无效: %w", errors.New("序列号已被吊销")) - } - - serial.IsActive = false - result = database.DB.Save(&serial) - if result.Error != nil { - return fmt.Errorf("吊销序列号失败: %w", result.Error) - } - - return nil -} diff --git a/services/services_test.go b/services/services_test.go index 91cc4f5..ec8eb1f 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -5,7 +5,6 @@ import ( "os" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "golang.org/x/crypto/bcrypt" @@ -30,7 +29,6 @@ func TestMain(m *testing.M) { database.DB.Unscoped().Where("1 = 1").Delete(&models.User{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{}) - database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.EmployeeSerial{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.AftersalesOrder{}) @@ -38,7 +36,6 @@ func TestMain(m *testing.M) { database.DB.Unscoped().Where("1 = 1").Delete(&models.User{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{}) - database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.EmployeeSerial{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.AftersalesOrder{}) @@ -219,222 +216,6 @@ func TestAuthService_UpdateProfile_Success(t *testing.T) { database.DB.Unscoped().Delete(&user) } -func TestSerialsService_Generate_Success(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser", - Password: string(password), - Name: "管理员", - Email: "admin@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, err := serialService.Generate("TestCompany", 5, 30, user.ID) - - assert.NoError(t, err) - assert.Len(t, serials, 5) - assert.Equal(t, "TestCompany", serials[0].CompanyName) - assert.True(t, serials[0].IsActive) - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "TestCompany").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - -func TestSerialsService_Generate_WithPrefix(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser2", - Password: string(password), - Name: "管理员2", - Email: "admin2@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, err := serialService.Generate("TestCompany2", 3, 30, user.ID, "TEST") - - assert.NoError(t, err) - assert.Len(t, serials, 3) - assert.True(t, len(serials[0].SerialNumber) > 0) - assert.Contains(t, serials[0].SerialNumber, "TEST") - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "TestCompany2").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - -func TestSerialsService_Query_QuerySuccess(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser3", - Password: string(password), - Name: "管理员3", - Email: "admin3@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, _ := serialService.Generate("TestCompany3", 1, 30, user.ID, "QR") - - serialNumber := strings.ToUpper(serials[0].SerialNumber) - result, err := serialService.Query(serialNumber) - - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, serialNumber, strings.ToUpper(result.SerialNumber)) - assert.True(t, result.IsActive) - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "TestCompany3").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - -func TestSerialsService_Query_SerialNotFound(t *testing.T) { - serialService := SerialsService{} - _, err := serialService.Query("NONEXISTENT") - - assert.Error(t, err) -} - -func TestSerialsService_FindAll_Success(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser4", - Password: string(password), - Name: "管理员4", - Email: "admin4@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, _ := serialService.Generate("TestCompany4", 10, 30, user.ID, "LIST") - - result, total, totalPages, err := serialService.FindAll(1, 5, "") - - assert.NoError(t, err) - assert.Len(t, result, 5) - assert.GreaterOrEqual(t, total, 10) - assert.Greater(t, totalPages, 0) - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "TestCompany4").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - -func TestSerialsService_FindAll_WithSearch(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser5", - Password: string(password), - Name: "管理员5", - Email: "admin5@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, _ := serialService.Generate("SearchCompany", 5, 30, user.ID, "SEARCH") - - result, _, _, err := serialService.FindAll(1, 10, "SearchCompany") - - assert.NoError(t, err) - assert.Greater(t, len(result), 0) - assert.Equal(t, "SearchCompany", result[0].CompanyName) - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "SearchCompany").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - -func TestSerialsService_Revoke_Success(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser6", - Password: string(password), - Name: "管理员6", - Email: "admin6@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, _ := serialService.Generate("RevokeCompany", 1, 30, user.ID, "REVOKE") - - serialNumber := serials[0].SerialNumber - err := serialService.Revoke(serialNumber) - - assert.NoError(t, err) - - var revokedSerial models.Serial - database.DB.Where("serial_number = ?", serialNumber).First(&revokedSerial) - assert.False(t, revokedSerial.IsActive) - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "RevokeCompany").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - -func TestSerialsService_Update_Success(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "adminuser7", - Password: string(password), - Name: "管理员7", - Email: "admin7@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - serials, _ := serialService.Generate("UpdateCompany", 1, 30, user.ID, "UPDATE") - - serialNumber := serials[0].SerialNumber - newValidUntil := time.Now().AddDate(0, 0, 60) - isActive := false - - updateData := models.UpdateSerialDTO{ - ValidUntil: &newValidUntil, - IsActive: &isActive, - } - - result, err := serialService.Update(serialNumber, updateData) - - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsActive) - - for _, serial := range serials { - database.DB.Unscoped().Delete(&serial) - } - database.DB.Unscoped().Where("company_name = ?", "UpdateCompany").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} - // ==================== EmployeeSerialsService 测试 ==================== func TestEmployeeSerialsService_Generate_Success(t *testing.T) { @@ -794,47 +575,3 @@ func TestEmployeeSerialsService_GenerateQRCode_Inactive(t *testing.T) { database.DB.Unscoped().Where("company_name = ?", "QREmpCompany2").Delete(&models.Company{}) database.DB.Unscoped().Delete(&user) } - -func TestCompaniesService_GetStatsOverview_Success(t *testing.T) { - var user models.User - password, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) - user = models.User{ - Username: "statsadmin", - Password: string(password), - Name: "统计管理员", - Email: "statsadmin@example.com", - Role: "admin", - } - database.DB.Create(&user) - - serialService := SerialsService{} - companySerials, _ := serialService.Generate("StatsCompany", 2, 30, user.ID, "STAT") - - employeeSerialsService := EmployeeSerialsService{} - employeeSerials, _ := employeeSerialsService.Generate("StatsCompany", "技术部", "测试员工", 2, user.ID, "") - - _ = serialService.Revoke(companySerials[0].SerialNumber) - _ = employeeSerialsService.Revoke(employeeSerials[0].SerialNumber) - - companiesService := CompaniesService{} - stats, err := companiesService.GetStatsOverview() - - assert.NoError(t, err) - assert.NotNil(t, stats) - assert.GreaterOrEqual(t, stats.TotalCompanies, int64(1)) - assert.GreaterOrEqual(t, stats.TotalSerials, int64(2)) - assert.GreaterOrEqual(t, stats.TotalEmployeeSerials, int64(2)) - assert.GreaterOrEqual(t, stats.RevokedSerials, int64(1)) - assert.GreaterOrEqual(t, stats.RevokedEmployeeSerials, int64(1)) - - for _, serial := range companySerials { - database.DB.Unscoped().Delete(&serial) - } - - for _, serial := range employeeSerials { - database.DB.Unscoped().Delete(&serial) - } - - database.DB.Unscoped().Where("company_name = ?", "StatsCompany").Delete(&models.Company{}) - database.DB.Unscoped().Delete(&user) -} diff --git a/tests/main_test.go b/tests/main_test.go index ea6f014..8d00864 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -56,7 +56,6 @@ func cleanupTestData() { // 删除所有测试数据 database.DB.Unscoped().Where("1 = 1").Delete(&models.User{}) database.DB.Unscoped().Where("1 = 1").Delete(&models.Company{}) - database.DB.Unscoped().Where("1 = 1").Delete(&models.Serial{}) } func createTestUsers() {