refactor: remove company management APIs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/**: 集成测试(健康检查、登录流程)
|
||||
|
||||
## 代码检查
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -112,7 +112,6 @@ func AutoMigrate() {
|
||||
if err := DB.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Company{},
|
||||
&models.Serial{},
|
||||
&models.ProductTrace{},
|
||||
&models.EmployeeSerial{},
|
||||
&models.AftersalesOrder{},
|
||||
|
||||
+529
-691
File diff suppressed because it is too large
Load Diff
+529
-691
File diff suppressed because it is too large
Load Diff
+377
-478
File diff suppressed because it is too large
Load Diff
@@ -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"`
|
||||
|
||||
+4
-26
@@ -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)
|
||||
}
|
||||
|
||||
// 产品溯源路由
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user