refactor: remove company management APIs

This commit is contained in:
Frudrax Cheng
2026-06-05 18:00:14 +08:00
parent efdde0ab28
commit 698c22cd08
16 changed files with 1631 additions and 3738 deletions
+24 -18
View File
@@ -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
+72 -58
View File
@@ -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,` 前缀,解码后 200B500KB
@@ -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/**: 集成测试(健康检查、登录流程)
## 代码检查
-320
View File
@@ -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,
})
}
+32
View File
@@ -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)
}
-345
View File
@@ -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),
},
})
}
-1
View File
@@ -112,7 +112,6 @@ func AutoMigrate() {
if err := DB.AutoMigrate(
&models.User{},
&models.Company{},
&models.Serial{},
&models.ProductTrace{},
&models.EmployeeSerial{},
&models.AftersalesOrder{},
+529 -691
View File
File diff suppressed because it is too large Load Diff
+529 -691
View File
File diff suppressed because it is too large Load Diff
+377 -478
View File
File diff suppressed because it is too large Load Diff
-73
View File
@@ -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
View File
@@ -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)
}
// 产品溯源路由
-502
View File
@@ -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
}
+64
View File
@@ -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
}
-271
View File
@@ -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
}
-263
View File
@@ -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)
}
-1
View File
@@ -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() {