diff --git a/openspec/changes/add-enterprise-device-authorization/proposal.md b/openspec/changes/add-enterprise-device-authorization/proposal.md new file mode 100644 index 0000000..3889db0 --- /dev/null +++ b/openspec/changes/add-enterprise-device-authorization/proposal.md @@ -0,0 +1,84 @@ +# Change: 企业设备授权管理 + +## Why + +当前系统中已存在企业客户管理和企业卡授权功能,但缺少企业设备授权管理能力。企业客户需要能够查看和管理被授权的设备列表,运营人员需要能够将设备授权给企业客户使用,并支持撤销授权操作。 + +根据业务需求文档 (`docs/企业设备授权.md`),需要在资产管理模块下新增"企业设备列表"功能,实现以下核心能力: + +1. **授权设备给企业** - 支持批量授权,最多100个设备号 +2. **查看企业设备列表** - 支持分页和按设备号搜索 +3. **撤销设备授权** - 支持批量撤销授权 + +## What Changes + +新增企业设备授权管理功能,包括: + +### 类型定义 +- 新增 `src/types/api/enterpriseDevice.ts` 文件 +- 定义设备列表项、查询参数、分页结果类型 +- 定义授权/撤销请求和响应类型 +- 在 `src/types/api/index.ts` 中导出新类型 + +### API 服务层 +- 扩展 `EnterpriseService` 类,新增3个方法: + - `allocateDevices(enterpriseId, data)` - POST 授权设备 + - `getEnterpriseDevices(enterpriseId, params)` - GET 设备列表 + - `recallDevices(enterpriseId, data)` - POST 撤销授权 + +### 视图层 +- 新增 `src/views/asset-management/enterprise-devices/index.vue` 页面 +- 实现设备列表展示 (表格、分页、搜索) +- 实现授权设备对话框 (支持批量输入设备号) +- 实现撤销授权功能 (二次确认) +- 实现操作结果展示 (成功/失败统计) + +### 路由配置 +- 在 `src/router/routesAlias.ts` 添加路由别名 +- 在 `src/router/routes/asyncRoutes.ts` 的资产管理模块下添加子路由 + +### 国际化 +- 在 `src/locales/langs/zh.json` 和 `en.json` 添加中英文翻译 +- 包含菜单、表单、表格、对话框、提示消息等所有文案 + +## Impact + +- **新增模块**: 企业设备授权管理 +- **影响范围**: + - 新增文件: `enterpriseDevice.ts`, `enterprise-devices/index.vue` + - 扩展文件: `enterprise.ts` (API), `routesAlias.ts`, `asyncRoutes.ts`, `zh.json`, `en.json`, `index.ts` (types) +- **依赖关系**: + - 依赖现有的 `EnterpriseService` 基础设施 + - 依赖已实现的设备管理模块 + - 后端 API 已就绪 +- **向后兼容性**: 完全兼容,不影响现有功能 +- **数据迁移**: 无需数据迁移 + +## Breaking Changes + +无破坏性变更。这是一个纯新增功能,不修改现有代码逻辑。 + +## Risks + +- **低风险**: 功能独立,不影响现有企业卡授权功能 +- **低风险**: API 已定义清晰,类型安全有保障 +- **中风险**: 需要确保批量操作时的用户体验良好 (处理大量设备号输入和结果展示) + +## Alternatives Considered + +1. **复用企业卡授权页面** - 不可行,设备和卡的数据结构和操作逻辑不同 +2. **在设备管理页面添加企业授权功能** - 不符合业务流程,企业授权属于资产管理范畴 +3. **使用单个设备号输入而非批量** - 不满足业务需求,运营人员需要批量授权能力 + +## Open Questions + +1. ✅ 设备号输入格式 - 支持换行或逗号分隔 +2. ✅ 最大批量数量 - API 限制最多100个设备号 +3. ✅ 失败情况处理 - API 返回成功/失败统计及失败原因列表 +4. ✅ 设备列表排序 - 按授权时间倒序 + +## References + +- API 文档: `docs/企业设备授权.md` +- 相关 OpenSpec 变更: `add-device-management` +- 参考实现: 企业卡授权功能 (`enterprise-cards/index.vue`) diff --git a/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md b/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md new file mode 100644 index 0000000..551f741 --- /dev/null +++ b/openspec/changes/add-enterprise-device-authorization/specs/enterprise-device-authorization/spec.md @@ -0,0 +1,291 @@ +# Spec: Enterprise Device Authorization + +## Overview + +企业设备授权功能允许运营人员将设备授权给企业客户使用,并支持查看授权设备列表和撤销授权操作。 + +## ADDED Requirements + +### Requirement: System SHALL define enterprise device types + +系统必须提供完整的企业设备授权相关类型定义,确保类型安全。 + +#### Scenario: 定义企业设备列表项类型 + +**Given** 需要展示企业设备列表 +**When** 定义 `EnterpriseDeviceItem` 接口 +**Then** 接口必须包含以下字段: +- `device_id: number` - 设备ID +- `device_no: string` - 设备号 +- `device_name: string` - 设备名称 +- `device_model: string` - 设备型号 +- `card_count: number` - 绑定卡数量 +- `authorized_at: string` - 授权时间 + +#### Scenario: 定义设备列表查询参数 + +**Given** 需要查询和搜索企业设备 +**When** 定义 `EnterpriseDeviceListParams` 接口 +**Then** 接口必须包含以下可选字段: +- `page?: number` - 页码 +- `page_size?: number` - 每页数量 +- `device_no?: string` - 设备号模糊搜索 + +#### Scenario: 定义授权设备请求类型 + +**Given** 需要授权设备给企业 +**When** 定义 `AllocateDevicesRequest` 接口 +**Then** 接口必须包含: +- `device_nos: string[]` - 设备号列表 (nullable, 最多100个) +- `remark?: string` - 授权备注 + +#### Scenario: 定义授权设备响应类型 + +**Given** 授权操作需要返回详细结果 +**When** 定义 `AllocateDevicesResponse` 接口 +**Then** 接口必须包含: +- `success_count: number` - 成功数量 +- `fail_count: number` - 失败数量 +- `authorized_devices: AuthorizedDeviceItem[]` - 已授权设备列表 (nullable) +- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable) + +**And** `AuthorizedDeviceItem` 包含: +- `device_id: number` - 设备ID +- `device_no: string` - 设备号 +- `card_count: number` - 绑定卡数量 + +**And** `FailedDeviceItem` 包含: +- `device_no: string` - 设备号 +- `reason: string` - 失败原因 + +#### Scenario: 定义撤销授权请求类型 + +**Given** 需要撤销设备授权 +**When** 定义 `RecallDevicesRequest` 接口 +**Then** 接口必须包含: +- `device_nos: string[]` - 设备号列表 (nullable, 最多100个) + +#### Scenario: 定义撤销授权响应类型 + +**Given** 撤销操作需要返回结果统计 +**When** 定义 `RecallDevicesResponse` 接口 +**Then** 接口必须包含: +- `success_count: number` - 成功数量 +- `fail_count: number` - 失败数量 +- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable) + +--- + +### Requirement: System SHALL provide enterprise device authorization API services + +系统必须提供企业设备授权相关的 API 服务方法。 + +#### Scenario: 授权设备给企业 + +**Given** 运营人员需要授权设备给企业客户 +**When** 调用 `EnterpriseService.allocateDevices(enterpriseId, data)` +**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/allocate-devices` +**And** 请求体必须包含设备号列表和可选备注 +**And** 返回授权结果,包含成功/失败统计和详细列表 + +#### Scenario: 获取企业设备列表 + +**Given** 需要查看企业的设备列表 +**When** 调用 `EnterpriseService.getEnterpriseDevices(enterpriseId, params)` +**Then** 必须发送 GET 请求到 `/api/admin/enterprises/{id}/devices` +**And** 支持分页参数 (page, page_size) +**And** 支持设备号模糊搜索 +**And** 返回设备列表和总数 + +#### Scenario: 撤销设备授权 + +**Given** 需要撤销企业的设备授权 +**When** 调用 `EnterpriseService.recallDevices(enterpriseId, data)` +**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/recall-devices` +**And** 请求体必须包含设备号列表 +**And** 返回撤销结果,包含成功/失败统计和失败原因 + +--- + +### Requirement: System SHALL provide enterprise device list page + +系统必须提供企业设备列表管理页面,支持查询、授权和撤销操作。 + +#### Scenario: 显示企业设备列表 + +**Given** 用户访问企业设备列表页面 +**When** 页面加载完成 +**Then** 必须显示设备列表表格 +**And** 表格必须包含以下列: +- 设备ID +- 设备号 +- 设备名称 +- 设备型号 +- 绑定卡数量 +- 授权时间 + +**And** 必须支持分页功能 +**And** 必须显示加载状态 + +#### Scenario: 搜索企业设备 + +**Given** 设备列表已加载 +**When** 用户在搜索框输入设备号 +**And** 点击搜索按钮 +**Then** 必须根据设备号模糊查询设备 +**And** 必须更新设备列表显示 +**And** 必须重置到第一页 + +#### Scenario: 授权设备对话框 + +**Given** 用户点击"授权设备"按钮 +**When** 授权设备对话框打开 +**Then** 必须显示设备号输入框 (textarea) +**And** 必须显示备注输入框 (可选) +**And** 必须提示支持的输入格式 (换行或逗号分隔) +**And** 必须提示最多100个设备号限制 +**And** 必须有表单验证 (设备号必填) + +#### Scenario: 提交授权设备 + +**Given** 用户在对话框中输入了设备号列表 +**When** 用户点击提交按钮 +**Then** 必须解析设备号列表 (支持换行和逗号分隔) +**And** 必须去除空白字符和空行 +**And** 必须验证设备号数量不超过100个 +**And** 必须调用授权 API +**And** 必须显示加载状态 +**And** 授权完成后必须展示结果: +- 成功数量 +- 失败数量 +- 失败设备列表及原因 + +**And** 如果有成功授权的设备,必须刷新设备列表 +**And** 必须关闭对话框 + +#### Scenario: 撤销设备授权 + +**Given** 用户选中了要撤销的设备 +**When** 用户点击"撤销授权"按钮 +**Then** 必须显示二次确认对话框 +**And** 确认对话框必须显示将要撤销的设备数量 + +**When** 用户确认撤销 +**Then** 必须调用撤销 API +**And** 必须显示加载状态 +**And** 撤销完成后必须展示结果: +- 成功数量 +- 失败数量 +- 失败设备列表及原因 + +**And** 如果有成功撤销的设备,必须刷新设备列表 + +#### Scenario: 错误处理 + +**Given** API 调用可能失败 +**When** API 返回错误 +**Then** 必须显示友好的错误提示消息 +**And** 必须在控制台记录错误详情 +**And** 必须停止加载状态 + +#### Scenario: 分页切换 + +**Given** 设备列表超过一页 +**When** 用户切换页码或每页数量 +**Then** 必须保持当前的搜索条件 +**And** 必须重新加载设备列表 +**And** 必须显示加载状态 + +--- + +### Requirement: System SHALL configure routing for enterprise device list + +系统必须为企业设备列表配置正确的路由。 + +#### Scenario: 注册企业设备列表路由 + +**Given** 需要访问企业设备列表页面 +**When** 配置路由 +**Then** 必须在资产管理模块 (`/asset-management`) 下添加子路由 +**And** 路由路径必须为 `enterprise-devices` +**And** 路由名称必须为 `EnterpriseDevices` +**And** 必须使用路由别名 `RoutesAlias.EnterpriseDevices` +**And** 必须配置 meta 信息: +- `title: 'menus.assetManagement.enterpriseDevices'` +- `keepAlive: true` + +--- + +### Requirement: System SHALL provide internationalization support + +系统必须提供中英文翻译支持。 + +#### Scenario: 中文翻译 + +**Given** 系统语言设置为中文 +**When** 访问企业设备相关页面 +**Then** 所有文本必须显示中文,包括: +- 菜单标题: "企业设备列表" +- 搜索表单标签和占位符 +- 表格列名 +- 按钮文本 +- 对话框标题和内容 +- 提示消息 + +#### Scenario: 英文翻译 + +**Given** 系统语言设置为英文 +**When** 访问企业设备相关页面 +**Then** 所有文本必须显示英文,包括: +- 菜单标题: "Enterprise Devices" +- 搜索表单标签和占位符 +- 表格列名 +- 按钮文本 +- 对话框标题和内容 +- 提示消息 + +--- + +### Requirement: System SHALL optimize user experience + +系统必须提供良好的用户体验。 + +#### Scenario: 批量输入设备号 + +**Given** 用户需要授权多个设备 +**When** 在设备号输入框中输入 +**Then** 必须支持以下输入方式: +- 每行一个设备号 +- 逗号分隔的设备号 +- 混合使用换行和逗号 + +**And** 系统必须能正确解析所有格式 +**And** 必须自动去除首尾空白字符 +**And** 必须过滤空行 + +#### Scenario: 操作结果展示 + +**Given** 批量操作完成 +**When** 显示操作结果 +**Then** 必须清晰展示: +- 总共处理的数量 +- 成功的数量 +- 失败的数量 +- 每个失败项的设备号和失败原因 + +**And** 如果全部成功,必须显示成功提示 +**And** 如果部分失败,必须显示警告提示 +**And** 如果全部失败,必须显示错误提示 + +#### Scenario: 表格列管理 + +**Given** 设备列表表格已显示 +**When** 用户点击列管理按钮 +**Then** 必须能够选择显示/隐藏的列 +**And** 列配置必须被保存 + +## Related Specs + +- 参考现有的企业卡授权功能 (enterpriseCard.ts) +- 依赖现有的企业客户管理功能 (enterprise.ts) +- 关联设备管理模块 (add-device-management) diff --git a/openspec/changes/add-enterprise-device-authorization/tasks.md b/openspec/changes/add-enterprise-device-authorization/tasks.md new file mode 100644 index 0000000..61903e3 --- /dev/null +++ b/openspec/changes/add-enterprise-device-authorization/tasks.md @@ -0,0 +1,158 @@ +# Tasks: Add Enterprise Device Authorization + +## Overview + +按照从底层到上层的顺序实现企业设备授权功能,确保每一步都可测试和验证。 + +## Task List + +### Phase 1: Type Definitions (Foundation) + +1. **创建企业设备类型定义文件** + - 创建 `src/types/api/enterpriseDevice.ts` + - 定义 `EnterpriseDeviceItem` 接口 (设备列表项) + - 定义 `EnterpriseDeviceListParams` 接口 (查询参数) + - 定义 `EnterpriseDevicePageResult` 接口 (分页结果) + - 定义 `AllocateDevicesRequest` 接口 (授权请求) + - 定义 `AllocateDevicesResponse` 接口 (授权响应) + - 定义 `AuthorizedDeviceItem` 接口 (已授权设备项) + - 定义 `FailedDeviceItem` 接口 (失败设备项) + - 定义 `RecallDevicesRequest` 接口 (撤销请求) + - 定义 `RecallDevicesResponse` 接口 (撤销响应) + - **验证**: 运行 `npm run type-check` 确保无类型错误 + +2. **导出类型定义** + - 在 `src/types/api/index.ts` 中导出新增的类型 + - **验证**: 确认其他模块可以正确导入类型 + +### Phase 2: API Service Layer + +3. **扩展 EnterpriseService API 方法** + - 在 `src/api/modules/enterprise.ts` 中添加: + - `allocateDevices(enterpriseId, data)` - 授权设备 + - `getEnterpriseDevices(enterpriseId, params)` - 获取设备列表 + - `recallDevices(enterpriseId, data)` - 撤销授权 + - 导入新增的类型定义 + - **验证**: 运行 `npm run type-check` 确保类型正确 + +### Phase 3: Internationalization + +4. **添加中文翻译** + - 在 `src/locales/langs/zh.json` 的 `menus.assetManagement` 下添加 `enterpriseDevices` 条目 + - 在 `src/locales/langs/zh.json` 添加 `enterpriseDevices` 模块的所有中文文案: + - 页面标题和搜索表单 + - 表格列名 + - 操作按钮和对话框标题 + - 表单标签和提示文本 + - 成功/错误消息 + - **验证**: 检查 JSON 格式正确性 + +5. **添加英文翻译** + - 在 `src/locales/langs/en.json` 添加对应的英文翻译 + - 确保 key 与中文版本一致 + - **验证**: 检查 JSON 格式正确性,切换语言测试 + +### Phase 4: Routing + +6. **添加路由别名** + - 在 `src/router/routesAlias.ts` 添加 `EnterpriseDevices = '/asset-management/enterprise-devices'` + - **验证**: 确认导出正确 + +7. **配置路由** + - 在 `src/router/routes/asyncRoutes.ts` 的资产管理 (`/asset-management`) 模块下添加企业设备列表路由 + - 配置路由 meta 信息 (title, keepAlive) + - **验证**: 运行应用,检查路由注册成功 + +### Phase 5: UI Components + +8. **创建企业设备列表页面** + - 创建 `src/views/asset-management/enterprise-devices/index.vue` + - 实现页面基础结构: + - 使用 `ArtTableFullScreen` 布局 + - 使用 `ArtSearchBar` 实现搜索表单 (企业ID,设备号搜索) + - 使用 `ArtTable` 展示设备列表 + - 使用 `ArtTableHeader` 添加"授权设备"和"撤销授权"按钮 + - **验证**: 页面能正常渲染,无控制台错误 + +9. **实现设备列表查询功能** + - 实现 `loadDeviceList()` 方法调用 API + - 实现搜索和重置功能 + - 实现分页功能 + - 添加 loading 状态 + - **验证**: 能正确展示设备列表数据,分页工作正常 + +10. **实现授权设备对话框** + - 创建授权设备对话框 + - 使用 `ElForm` + `ElInput` (textarea) 输入设备号列表 + - 支持多行输入或逗号分隔 + - 添加备注输入框 (可选) + - 实现表单验证 (必填项,格式校验) + - **验证**: 对话框显示正常,表单验证工作 + +11. **实现授权设备提交逻辑** + - 实现 `handleAllocateDevices()` 方法 + - 解析设备号列表 (处理换行和逗号分隔) + - 调用 `EnterpriseService.allocateDevices()` API + - 展示授权结果 (成功数量,失败数量,失败列表) + - 使用 `ElMessageBox` 或 `ElDialog` 展示详细结果 + - 授权成功后刷新列表 + - **验证**: 能成功授权设备,正确处理部分成功/失败情况 + +12. **实现撤销授权对话框** + - 创建撤销授权对话框 + - 使用表格多选模式选择要撤销的设备 + - 或者使用输入框输入设备号列表 + - 实现二次确认逻辑 + - **验证**: 对话框显示正常 + +13. **实现撤销授权提交逻辑** + - 实现 `handleRecallDevices()` 方法 + - 收集选中的设备号 + - 调用 `EnterpriseService.recallDevices()` API + - 展示撤销结果 (成功数量,失败数量,失败列表) + - 撤销成功后刷新列表 + - **验证**: 能成功撤销授权,正确处理部分成功/失败情况 + +### Phase 6: Polish & Testing + +14. **完善表格列配置** + - 配置表格列 (设备ID,设备号,设备名称,设备型号,绑定卡数量,授权时间) + - 实现列显示/隐藏功能 + - 添加时间格式化 + - **验证**: 表格数据展示完整美观 + +15. **添加错误处理** + - 为所有 API 调用添加 try-catch + - 添加友好的错误提示消息 + - 处理网络错误和业务错误 + - **验证**: 各种错误场景都有适当提示 + +16. **样式调整** + - 确保页面样式与系统其他页面一致 + - 响应式布局适配 + - 对话框尺寸和布局优化 + - **验证**: 在不同屏幕尺寸下显示正常 + +17. **最终验证** + - 运行 `npm run type-check` 确保无类型错误 + - 运行 `npm run build` 确保能成功构建 + - 运行 `openspec validate add-enterprise-device-authorization --strict` 验证 spec + - 手动测试所有功能点 + - 测试中英文切换 + - **验证**: 所有功能正常,无控制台错误 + +## Dependencies + +- Task 3 依赖 Task 1 (API 需要类型定义) +- Task 8-13 依赖 Task 1-7 (UI 需要 API 和路由) +- Task 14-16 是并行任务,可以同时进行 + +## Estimated Effort + +- Phase 1-2: 30 分钟 (类型和 API) +- Phase 3: 30 分钟 (国际化) +- Phase 4: 15 分钟 (路由) +- Phase 5: 2-3 小时 (UI 实现) +- Phase 6: 30 分钟 (测试和优化) + +**Total**: 约 4-4.5 小时 diff --git a/openspec/changes/add-order-management/proposal.md b/openspec/changes/add-order-management/proposal.md new file mode 100644 index 0000000..c3c946a --- /dev/null +++ b/openspec/changes/add-order-management/proposal.md @@ -0,0 +1,43 @@ +# Change: Add Order Management System + +## Why + +The IoT management platform currently lacks order management capabilities. Users need to: +- View and query orders created by customers (personal and agent) +- Track order payment status and details +- Create orders for single card or device purchases +- Cancel pending orders +- View order history with filtering and search + +This capability is essential for financial tracking, commission calculation, and overall business operations transparency. + +## What Changes + +- **NEW**: Order list page with search, filtering, and pagination +- **NEW**: Order API service with full CRUD operations +- **NEW**: TypeScript types for order entities and API contracts +- **NEW**: i18n keys for order management UI +- **NEW**: Router configuration for order management module + +The order management module will support: +- Listing orders with filters (payment status, order type, date range, order number) +- Viewing order details including buyer information, order items (packages), and payment details +- Creating orders for single card or device purchases with package selection +- Canceling orders that are in pending payment status +- Displaying payment method, commission status, and order totals + +## Impact + +- **Affected specs**: `order-management` (new capability) +- **Affected code**: + - `src/types/api/order.ts` (new file - TypeScript types) + - `src/api/modules/order.ts` (new file - API service) + - `src/views/order-management/order-list/index.vue` (new file - order list page) + - `src/router/routes/asyncRoutes.ts` (route configuration) + - `src/router/routesAlias.ts` (route alias) + - `src/locales/langs/zh.json` (Chinese i18n) + - `src/locales/langs/en.json` (English i18n) + - `src/types/api/index.ts` (exports) + - `src/api/modules/index.ts` (exports) +- **Dependencies**: Requires backend APIs at `/api/admin/orders` endpoints +- **Breaking changes**: None (this is a new feature) diff --git a/openspec/changes/add-order-management/specs/order-management/spec.md b/openspec/changes/add-order-management/specs/order-management/spec.md new file mode 100644 index 0000000..c20183c --- /dev/null +++ b/openspec/changes/add-order-management/specs/order-management/spec.md @@ -0,0 +1,174 @@ +# Order Management Specification + +## ADDED Requirements + +### Requirement: Order List Display + +The system SHALL display a paginated list of orders with comprehensive filtering and search capabilities. + +#### Scenario: Display all orders with pagination + +- **GIVEN** the user navigates to the order management page +- **WHEN** the page loads +- **THEN** the system displays a table of orders with pagination controls +- **AND** default page size is 20 items +- **AND** table shows columns: ID, order number, buyer type, buyer ID, order type, payment status, total amount, created date, and actions + +#### Scenario: Filter orders by payment status + +- **GIVEN** the user is on the order list page +- **WHEN** the user selects a payment status filter (pending=1, paid=2, cancelled=3, refunded=4) +- **AND** clicks the search button +- **THEN** the system displays only orders matching the selected payment status +- **AND** pagination resets to page 1 + +#### Scenario: Filter orders by order type + +- **GIVEN** the user is on the order list page +- **WHEN** the user selects an order type filter (single_card or device) +- **AND** clicks the search button +- **THEN** the system displays only orders matching the selected order type + +#### Scenario: Search by order number + +- **GIVEN** the user is on the order list page +- **WHEN** the user enters an order number in the search field +- **AND** clicks the search button +- **THEN** the system performs an exact match search +- **AND** displays the matching order if found + +#### Scenario: Filter by date range + +- **GIVEN** the user is on the order list page +- **WHEN** the user selects a start date and/or end date +- **AND** clicks the search button +- **THEN** the system displays orders created within the specified date range + +### Requirement: Order Details Viewing + +The system SHALL allow users to view detailed information about each order including order items and payment information. + +#### Scenario: View order details + +- **GIVEN** the user is viewing the order list +- **WHEN** the user clicks the view/detail action for an order +- **THEN** the system displays comprehensive order information including: +- **AND** order basic info (order_no, order_type, buyer_id, buyer_type) +- **AND** payment info (payment_status, payment_method, paid_at, total_amount) +- **AND** order items list (package_id, package_name, quantity, unit_price, amount) +- **AND** commission info (commission_status, commission_config_version) +- **AND** timestamps (created_at, updated_at) + +### Requirement: Order Cancellation + +The system SHALL allow authorized users to cancel orders that are in pending payment status. + +#### Scenario: Cancel a pending order + +- **GIVEN** the user is viewing an order with payment_status = 1 (pending) +- **WHEN** the user clicks the cancel action +- **AND** confirms the cancellation in the confirmation dialog +- **THEN** the system sends a cancel request to POST `/api/admin/orders/{id}/cancel` +- **AND** updates the order status to cancelled (3) +- **AND** displays a success message +- **AND** refreshes the order list + +#### Scenario: Cannot cancel paid order + +- **GIVEN** the user is viewing an order with payment_status = 2 (paid) +- **WHEN** the cancel action is clicked +- **THEN** the system displays an error message "Cannot cancel a paid order" +- **AND** does not send the cancellation request + +### Requirement: Order Creation + +The system SHALL provide an interface to create orders for single card or device purchases. + +#### Scenario: Create single card order + +- **GIVEN** the user clicks the create order button +- **WHEN** the user selects order_type = "single_card" +- **AND** selects an IoT card (iot_card_id) +- **AND** selects one or more packages (package_ids) +- **AND** submits the form +- **THEN** the system sends a POST request to `/api/admin/orders` with the order data +- **AND** displays the newly created order details +- **AND** refreshes the order list + +#### Scenario: Create device order + +- **GIVEN** the user clicks the create order button +- **WHEN** the user selects order_type = "device" +- **AND** selects a device (device_id) +- **AND** selects one or more packages (package_ids, max 10) +- **AND** submits the form +- **THEN** the system creates the order +- **AND** displays a success message + +### Requirement: Order Status Display + +The system SHALL display order payment status and order type using color-coded badges and human-readable text. + +#### Scenario: Display payment status badge + +- **GIVEN** an order is displayed in the table +- **WHEN** the payment_status is 1 (pending) +- **THEN** the system displays a warning-type badge with text "待支付" +- **WHEN** the payment_status is 2 (paid) +- **THEN** the system displays a success-type badge with text "已支付" +- **WHEN** the payment_status is 3 (cancelled) +- **THEN** the system displays an info-type badge with text "已取消" +- **WHEN** the payment_status is 4 (refunded) +- **THEN** the system displays a danger-type badge with text "已退款" + +#### Scenario: Display order type badge + +- **GIVEN** an order is displayed in the table +- **WHEN** the order_type is "single_card" +- **THEN** the system displays text "单卡购买" +- **WHEN** the order_type is "device" +- **THEN** the system displays text "设备购买" + +### Requirement: Currency Formatting + +The system SHALL display monetary amounts in yuan (元) with proper formatting and conversion from cents. + +#### Scenario: Format order total amount + +- **GIVEN** an order has total_amount = 50000 (in cents) +- **WHEN** the order is displayed in the table +- **THEN** the system displays "¥500.00" or "500.00 元" + +### Requirement: Data Refresh and Real-time Updates + +The system SHALL provide manual refresh capabilities and update data after mutations. + +#### Scenario: Manual refresh + +- **GIVEN** the user is viewing the order list +- **WHEN** the user clicks the refresh button in the table header +- **THEN** the system reloads the current page of orders with current filters +- **AND** maintains the current pagination state + +#### Scenario: Auto-refresh after order cancellation + +- **GIVEN** the user successfully cancels an order +- **WHEN** the cancellation is confirmed +- **THEN** the system automatically refreshes the order list +- **AND** displays the updated order status + +### Requirement: Internationalization Support + +The system SHALL provide full internationalization support for order management UI in Chinese and English. + +#### Scenario: Display Chinese text + +- **GIVEN** the user's language is set to Chinese (zh) +- **WHEN** the order management page is viewed +- **THEN** all UI text displays in Chinese including menu titles, table headers, status labels, and messages + +#### Scenario: Display English text + +- **GIVEN** the user's language is set to English (en) +- **WHEN** the order management page is viewed +- **THEN** all UI text displays in English including menu titles, table headers, status labels, and messages diff --git a/openspec/changes/add-order-management/tasks.md b/openspec/changes/add-order-management/tasks.md new file mode 100644 index 0000000..b4d6cc9 --- /dev/null +++ b/openspec/changes/add-order-management/tasks.md @@ -0,0 +1,57 @@ +# Implementation Tasks + +## 1. Type Definitions + +- [ ] 1.1 Create `src/types/api/order.ts` with order types, enums, and interfaces +- [ ] 1.2 Add order type exports to `src/types/api/index.ts` + +## 2. API Service Layer + +- [ ] 2.1 Create `src/api/modules/order.ts` with OrderService class +- [ ] 2.2 Implement `getOrders()` - list orders with pagination and filters +- [ ] 2.3 Implement `getOrderById()` - fetch single order details +- [ ] 2.4 Implement `createOrder()` - create new order +- [ ] 2.5 Implement `cancelOrder()` - cancel pending order +- [ ] 2.6 Add OrderService export to `src/api/modules/index.ts` + +## 3. Internationalization + +- [ ] 3.1 Add Chinese translations to `src/locales/langs/zh.json` under `orderManagement` namespace +- [ ] 3.2 Add English translations to `src/locales/langs/en.json` under `orderManagement` namespace +- [ ] 3.3 Include keys for: menu titles, page titles, table columns, search fields, statuses, actions, messages + +## 4. Routing Configuration + +- [ ] 4.1 Add `OrderList` route alias to `src/router/routesAlias.ts` +- [ ] 4.2 Add order management route group to `src/router/routes/asyncRoutes.ts` +- [ ] 4.3 Configure route with proper icon, title, and keepAlive settings + +## 5. UI Components + +- [ ] 5.1 Create `src/views/order-management/order-list/index.vue` component skeleton +- [ ] 5.2 Implement search bar with filters (order_no, payment_status, order_type, date range) +- [ ] 5.3 Implement data table with columns (ID, order_no, buyer info, order type, payment status, total amount, created_at, actions) +- [ ] 5.4 Add pagination controls +- [ ] 5.5 Implement view details action (navigate to detail view or show in dialog) +- [ ] 5.6 Implement cancel order action with confirmation dialog +- [ ] 5.7 Add status badges and formatters for payment status and order type +- [ ] 5.8 Format currency amounts (分 to 元 conversion) +- [ ] 5.9 Implement create order button and dialog (optional - can be phase 2) + +## 6. Business Logic + +- [ ] 6.1 Implement order list data fetching with loading states +- [ ] 6.2 Implement search and filter logic +- [ ] 6.3 Implement pagination handlers +- [ ] 6.4 Implement cancel order with optimistic UI updates +- [ ] 6.5 Add error handling and user feedback (ElMessage) +- [ ] 6.6 Implement date/time formatting using project utilities + +## 7. Validation & Polish + +- [ ] 7.1 Test search and filtering functionality +- [ ] 7.2 Test pagination and data refresh +- [ ] 7.3 Test cancel order flow +- [ ] 7.4 Verify i18n coverage (switch language and check all text) +- [ ] 7.5 Verify responsive layout and table column configuration +- [ ] 7.6 Code review and cleanup (remove console logs, verify TypeScript types) diff --git a/openspec/changes/add-package-management-system/design.md b/openspec/changes/add-package-management-system/design.md new file mode 100644 index 0000000..b883a0b --- /dev/null +++ b/openspec/changes/add-package-management-system/design.md @@ -0,0 +1,332 @@ +# Design Document: 套餐管理系统实现 + +## Context + +实现完整的套餐管理系统,包括4个核心模块。该系统需要支持多级代理商体系的套餐分配和定价管理。 + +**背景**: +- 项目已有类型定义(src/types/api/package.ts),但使用不同的字段命名和枚举值 +- 后端 API 已实现,使用下划线命名(如 `series_name`) +- 前端项目统一使用 CommonStatus 枚举(0:禁用, 1:启用) +- 参考实现:`/system/role` 页面使用了组件化架构 + +**约束**: +- 必须保留现有类型定义文件,不能破坏现有代码 +- 需要兼容后端 API 的字段命名规范 +- 需要适配项目的状态枚举规范 + +## Goals / Non-Goals + +### Goals +1. 实现4个核心模块的完整 CRUD 功能 +2. 建立统一的 API 服务层,封装后端接口 +3. 实现组件化的页面结构,参考 `/system/role` +4. 支持复杂的定价规则(系列加价 vs 单套餐覆盖) +5. 确保数据隔离和权限控制 + +### Non-Goals +1. 不重构现有的 package.ts 类型定义 +2. 不实现套餐的实时统计和报表功能(后续迭代) +3. 不实现套餐批量导入功能(后续迭代) +4. 不实现套餐的版本管理功能 + +## Decisions + +### Decision 1: API 字段命名策略 + +**问题**:后端使用下划线命名(snake_case),前端类型通常使用驼峰命名(camelCase)。 + +**决策**: +- API 请求/响应保持下划线命名,与后端保持一致 +- 创建新的类型文件 `packageManagement.ts`,使用下划线命名 +- 在表单提交和响应处理时不做转换,直接使用下划线字段 + +**理由**: +- 减少转换层的复杂性和错误风险 +- 与后端 API 文档保持一致,便于对照 +- TypeScript 支持下划线字段名,不影响类型安全 + +**示例**: +```typescript +export interface PackageSeriesResponse { + id: number + series_code: string // 下划线命名 + series_name: string + status: number + created_at: string + updated_at: string +} +``` + +### Decision 2: 状态值映射 + +**问题**:文档中状态是 `1:启用, 2:禁用`,但项目 CommonStatus 是 `0:禁用, 1:启用`。 + +**决策**: +- **在常量配置中定义套餐专用的状态枚举** +- **前端页面使用项目统一的 CommonStatus(0/1)** +- **在 API 服务层进行状态值映射转换** + +**映射规则**: +```typescript +// 前端 -> 后端 +CommonStatus.ENABLED (1) -> API Status (1) +CommonStatus.DISABLED (0) -> API Status (2) + +// 后端 -> 前端 +API Status (1) -> CommonStatus.ENABLED (1) +API Status (2) -> CommonStatus.DISABLED (0) +``` + +**理由**: +- 保持前端 UI 的一致性 +- 避免混淆项目开发者 +- 集中在 API 服务层处理差异 + +### Decision 3: 模块拆分策略 + +**问题**:是创建单个 package.ts 服务,还是拆分为多个服务文件? + +**决策**:拆分为4个独立的服务文件: +1. `packageSeries.ts` - 套餐系列管理 +2. `package.ts` - 套餐管理 +3. `myPackage.ts` - 代理可售套餐 +4. `shopPackageAllocation.ts` - 单套餐分配 + +**理由**: +- 每个模块功能独立,职责清晰 +- 便于维护和扩展 +- 符合单一职责原则 +- 便于团队协作(不同开发者负责不同模块) + +**替代方案**: +- 单个 package.ts 文件 - **拒绝**,文件过大,难以维护 + +### Decision 4: 定价规则实现 + +**问题**:代理商的套餐成本价有两种计算方式:系列加价和单套餐覆盖。 + +**决策**: +- **后端负责成本价计算**,前端只展示结果 +- 前端接收 `price_source` 字段,标识价格来源 +- 单套餐分配创建时,保存 `calculated_cost_price`(系列规则计算的价格)供参考 + +**数据流**: +``` +1. 系列分配:pricing_mode + pricing_value -> 后端计算 -> cost_price +2. 单套餐分配:直接设置 cost_price(覆盖系列规则) +3. 前端展示:price_source 标识使用了哪种规则 +``` + +**理由**: +- 计算逻辑复杂,集中在后端便于维护 +- 前端只负责展示,降低复杂度 +- 保留 calculated_cost_price 便于调试和审计 + +### Decision 5: 表单验证策略 + +**问题**:客户端验证 vs 服务端验证。 + +**决策**:**双重验证** +- 客户端:使用 Element Plus 的 FormRules 进行基础验证 +- 服务端:后端 API 进行完整验证并返回详细错误 + +**客户端验证规则**: +- 必填字段检查 +- 长度限制(如系列名称 1-255 字符) +- 数值范围(如套餐时长 1-120 月) +- 格式验证(如价格必须为正整数) + +**理由**: +- 客户端验证提升用户体验,即时反馈 +- 服务端验证保证数据安全性和完整性 +- 符合 Web 应用最佳实践 + +### Decision 6: 页面组件化结构 + +**问题**:页面结构如何组织? + +**决策**:参考 `/system/role` 页面,使用组件化结构: +```vue + + + + + + + + + + +``` + +**理由**: +- 与项目现有页面风格一致 +- 复用成熟的组件,减少开发工作量 +- 便于维护和扩展 + +## Risks / Trade-offs + +### Risk 1: 后端 API 未完成 + +**风险**:后端接口可能尚未实现或与文档不一致。 + +**缓解措施**: +1. 先实现 API 服务层,使用 TypeScript 类型约束 +2. 使用 Mock 数据进行前端开发(已有示例) +3. 与后端团队确认 API 规范和联调时间 +4. 预留 API 调试和修正时间 + +### Risk 2: 状态值映射可能遗漏 + +**风险**:在某些地方忘记转换状态值,导致显示错误。 + +**缓解措施**: +1. 在 API 服务层统一处理转换 +2. 创建工具函数封装映射逻辑 +3. 编写单元测试覆盖映射函数 +4. Code Review 时重点检查状态相关代码 + +### Risk 3: 定价规则理解偏差 + +**风险**:对定价规则的理解与实际业务需求有偏差。 + +**缓解措施**: +1. 在实现前与产品确认定价规则 +2. 编写测试用例覆盖各种定价场景 +3. 在 UI 上清晰展示价格来源和计算方式 +4. 预留调整空间,避免硬编码 + +### Trade-off 1: 类型定义冗余 + +**取舍**:保留旧的 package.ts 类型定义,新增 packageManagement.ts。 + +**代价**: +- 存在两套类型定义,可能造成混淆 +- 占用额外的代码空间 + +**收益**: +- 不影响现有代码,向后兼容 +- 新旧系统可以并存,降低迁移风险 +- 未来可以逐步迁移到新类型 + +### Trade-off 2: 状态值映射增加复杂度 + +**取舍**:在 API 服务层进行状态值转换。 + +**代价**: +- 增加一层转换逻辑 +- 可能影响性能(微小) + +**收益**: +- 前端 UI 保持一致性 +- 业务逻辑更清晰 +- 便于后续维护 + +## Migration Plan + +### Phase 1: 基础设施(1-2天) +1. 创建类型定义文件 +2. 创建常量配置文件 +3. 设置状态映射工具函数 + +### Phase 2: API 服务层(2-3天) +1. 实现4个 API 服务模块 +2. 编写单元测试(可选) +3. 使用 Mock 数据测试 + +### Phase 3: 页面实现(4-5天) +1. 套餐系列管理页面(1天) +2. 套餐管理页面(1.5天) +3. 代理可售套餐页面(1天) +4. 单套餐分配页面(1.5天) + +### Phase 4: 集成测试(1-2天) +1. 与后端 API 联调 +2. 端到端功能测试 +3. 修复 Bug 和优化 + +### Phase 5: 上线(1天) +1. Code Review +2. 合并代码 +3. 部署到测试环境 +4. 部署到生产环境 + +**总计**:9-13 个工作日 + +### Rollback Plan + +如果出现严重问题,回滚步骤: +1. 从 Git 回滚到上一个稳定版本 +2. 移除新增的路由配置 +3. 移除新增的 API 服务导出 +4. 通知用户功能暂时不可用 + +### Decision 7: 错误处理策略 + +**问题**:如何统一处理各类错误和异常? + +**决策**:分层错误处理机制 +- **网络错误**:axios 拦截器统一捕获,显示通用错误提示 +- **401 未认证**:自动跳转到登录页面 +- **403 无权限**:显示权限不足提示,不跳转 +- **400 业务错误**:根据错误信息显示具体提示(ElMessage.error) +- **表单验证错误**:在表单字段下显示错误提示 + +**错误提示方式**: +```typescript +// 网络错误或服务器错误 +ElMessage.error('网络错误,请稍后重试') + +// 业务错误(后端返回的具体错误) +ElMessage.error(res.message || '操作失败') + +// 操作成功 +ElMessage.success('操作成功') +``` + +**理由**: +- 统一的错误处理提升用户体验 +- 分层处理避免重复代码 +- 清晰的错误提示帮助用户理解问题 + +### Decision 8: Loading 状态管理 + +**问题**:如何管理各种操作的加载状态? + +**决策**:细粒度的 loading 状态管理 + +**Loading 状态分类**: +```typescript +const loading = ref(false) // 表格数据加载 +const submitLoading = ref(false) // 表单提交 +const deleteLoading = ref>({}) // 删除操作(可选) +``` + +**状态管理规则**: +- **列表查询**:表格显示 loading 遮罩 +- **新增/编辑提交**:提交按钮显示 loading,禁用表单 +- **删除操作**:可选择在按钮上显示 loading 或全局 loading +- **状态切换**:ElSwitch 自带 loading 效果,先更新 UI 再调用 API + +**理由**: +- 细粒度控制提供更好的交互反馈 +- 防止重复提交 +- 清晰标识正在进行的操作 + +## Open Questions + +1. **Q**: 套餐被删除后,历史订单如何处理? + **A**: 待产品确认,可能需要软删除机制 + +2. **Q**: 代理商可以自行调整套餐售价吗? + **A**: 待产品确认,当前设计只展示建议售价 + +3. **Q**: 套餐系列和套餐是否支持批量操作(批量启用/禁用)? + **A**: 当前不支持,后续迭代考虑 + +4. **Q**: 是否需要套餐变更历史记录? + **A**: 后端可能有审计日志,前端暂不展示 + +5. **Q**: 单套餐分配的"原计算成本价"是否需要实时更新? + **A**: 待确认,当前设计是创建时计算一次,不自动更新 diff --git a/openspec/changes/add-package-management-system/proposal.md b/openspec/changes/add-package-management-system/proposal.md new file mode 100644 index 0000000..35ec9d0 --- /dev/null +++ b/openspec/changes/add-package-management-system/proposal.md @@ -0,0 +1,143 @@ +# Change: 套餐管理系统实现 + +## Why + +根据业务需求文档(docs/套餐.md),需要实现完整的套餐管理系统,包括4个核心模块: + +1. **套餐系列管理** - 管理套餐的分类和组织 +2. **套餐管理** - 管理具体的套餐产品(流量、价格、时长等) +3. **代理可售套餐** - 代理商查看被分配的可售套餐及定价 +4. **单套餐分配** - 为代理商分配特定套餐并设置成本价 + +当前系统虽然有套餐相关的类型定义(src/types/api/package.ts)和部分页面骨架,但缺少完整的 API 对接和业务逻辑实现。此变更将实现完整的套餐管理能力。 + +## What Changes + +### 1. API 层实现 + +采用模块化设计,拆分为 4 个独立的 API 服务文件: + +- **新增**: `src/api/modules/packageSeries.ts` - 套餐系列 API 服务 + - 套餐系列列表查询(分页、筛选) + - 创建套餐系列 + - 获取套餐系列详情 + - 更新套餐系列 + - 删除套餐系列 + - 更新套餐系列状态 + +- **新增**: `src/api/modules/package.ts` - 套餐管理 API 服务 + - 套餐列表查询(分页、多条件筛选) + - 创建套餐 + - 获取套餐详情 + - 更新套餐 + - 删除套餐 + - 更新套餐状态 + - 更新套餐上架状态 + - 获取系列下拉选项(用于表单选择) + +- **新增**: `src/api/modules/myPackage.ts` - 代理可售套餐 API 服务 + - 我的可售套餐列表查询 + - 获取可售套餐详情 + - 我的被分配系列列表 + +- **新增**: `src/api/modules/shopPackageAllocation.ts` - 单套餐分配 API 服务 + - 单套餐分配列表查询 + - 创建单套餐分配 + - 获取单套餐分配详情 + - 更新单套餐分配 + - 删除单套餐分配 + - 更新单套餐分配状态 + +- **修改**: `src/api/modules/index.ts` - 导出新增的服务模块 + - 导出 PackageSeriesService + - 导出 PackageService + - 导出 MyPackageService + - 导出 ShopPackageAllocationService + +### 2. 类型定义增强 + +- **新增**: `src/types/api/packageManagement.ts` - 完整的套餐管理类型定义 + - 匹配文档的 API 字段(下划线命名) + - 包含所有请求/响应类型 + - 包含分页结果类型 + +### 3. 页面实现 + +**套餐系列管理** (`src/views/package-management/package-series/index.vue`) +- 列表展示(支持名称搜索、状态筛选) +- 新增/编辑套餐系列 +- 删除套餐系列 +- 状态开关(启用/禁用) + +**套餐管理** (`src/views/package-management/package-list/index.vue`) +- 列表展示(支持多条件筛选:名称、系列、状态、上架状态、套餐类型) +- 新增/编辑套餐 +- 删除套餐 +- 状态开关(启用/禁用) +- 上架状态开关(上架/下架) + +**代理可售套餐** (`src/views/package-management/my-packages/index.vue`) +- 查看被分配的套餐列表(支持系列、类型筛选) +- 查看套餐详情(成本价、建议售价、利润空间等) +- 查看被分配系列列表 + +**单套餐分配** (`src/views/package-management/package-assign/index.vue`) +- 分配列表(支持店铺、套餐、状态筛选) +- 创建分配(选择套餐、店铺、设置成本价) +- 编辑分配(修改成本价) +- 删除分配 +- 状态管理(启用/禁用) + +### 4. 常量配置 + +- **新增**: `src/config/constants/package.ts` - 套餐相关常量 + - 套餐类型枚举(formal/addon) + - 流量类型枚举(real/virtual) + - 上架状态枚举(1:上架, 2:下架) + - 定价模式枚举(fixed/percent) + - 价格来源枚举(series_pricing/package_override) + +### 5. 路由配置 + +已存在的路由(无需修改): +- `/package-management/package-series` - 套餐系列管理 +- `/package-management/package-list` - 套餐管理 +- `/package-management/package-assign` - 单套餐分配 + +需要新增的路由: +- **新增**: `src/router/routesAlias.ts` - 添加路由别名 + - `MyPackages = '/package-management/my-packages'` - 代理可售套餐 + +- **新增**: `src/router/routes/asyncRoutes.ts` - 添加路由配置 + - `/package-management/my-packages` - 代理可售套餐页面路由 + +## Impact + +### 受影响的规范 +- `package-series-management` - 新增能力 +- `package-management` - 新增能力 +- `my-packages` - 新增能力 +- `shop-package-allocation` - 新增能力 + +### 受影响的代码 +- `src/api/modules/*` - 新增 4 个 API 服务模块 +- `src/types/api/*` - 新增类型定义文件 +- `src/views/package-management/*` - 4 个页面完整实现 +- `src/config/constants/*` - 新增常量配置 +- `src/router/routes/asyncRoutes.ts` - 路由配置 + +### 依赖关系 +- 依赖现有的组件库(ArtTable、ArtSearchBar、ArtTableHeader 等) +- 依赖现有的 HTTP 请求工具(request.ts) +- 依赖现有的权限控制和路由守卫 +- 依赖 ShopService(用于单套餐分配页面的店铺选择器) +- 后端 API 需已实现(docs/套餐.md 中定义的接口) + +**注意事项**: +- ShopService 应该已经存在于 src/api/modules/shop.ts +- 如果不存在,需要先实现或使用 Mock 数据 + +### 风险评估 +- **低风险**: 独立模块,不影响现有功能 +- **API 依赖**: 需确保后端接口已实现并联调 +- **权限控制**: 需配置对应的菜单和按钮权限 diff --git a/openspec/changes/add-package-management-system/specs/my-packages/spec.md b/openspec/changes/add-package-management-system/specs/my-packages/spec.md new file mode 100644 index 0000000..0ab79a3 --- /dev/null +++ b/openspec/changes/add-package-management-system/specs/my-packages/spec.md @@ -0,0 +1,110 @@ +# My Packages (代理可售套餐) Specification + +## ADDED Requirements + +### Requirement: 我的可售套餐列表查询 + +系统 SHALL 提供代理商查询被分配套餐的功能。 + +#### Scenario: 查询当前代理商的可售套餐 + +- **WHEN** 代理商用户访问可售套餐页面 +- **THEN** 系统显示被分配给该代理商的套餐列表 +- **AND** 每个套餐包含:套餐ID、套餐编码、套餐名称、套餐类型、系列信息 +- **AND** 显示成本价(cost_price,单位:分) +- **AND** 显示建议售价(suggested_retail_price,单位:分) +- **AND** 显示利润空间(profit_margin = 建议售价 - 成本价) +- **AND** 显示价格来源(series_pricing:系列加价 / package_override:单套餐覆盖) +- **AND** 显示套餐状态和上架状态 +- **AND** 支持分页,每页最多100条记录 + +#### Scenario: 按系列筛选 + +- **WHEN** 代理商选择特定系列ID筛选 +- **THEN** 系统返回该系列下的所有可售套餐 + +#### Scenario: 按套餐类型筛选 + +- **WHEN** 代理商选择套餐类型(formal/addon)筛选 +- **THEN** 系统返回该类型的所有可售套餐 + +### Requirement: 可售套餐详情查询 + +系统 SHALL 允许代理商查看单个可售套餐的详细信息。 + +#### Scenario: 查询套餐详情 + +- **WHEN** 代理商点击查看套餐详情 +- **THEN** 系统显示套餐完整信息 +- **AND** 包含套餐描述、流量信息、时长等 +- **AND** 显示定价详情(成本价、建议售价、利润空间、价格来源) +- **AND** 显示系列信息 + +#### Scenario: 查询未分配的套餐 + +- **WHEN** 代理商查询未被分配的套餐ID +- **THEN** 系统返回404错误或无权访问错误 + +### Requirement: 我的被分配系列列表 + +系统 SHALL 提供代理商查询被分配系列的功能。 + +#### Scenario: 查询被分配系列列表 + +- **WHEN** 代理商访问被分配系列列表 +- **THEN** 系统显示分配给该代理商的系列列表 +- **AND** 每个系列包含:分配ID、系列ID、系列编码、系列名称 +- **AND** 显示定价模式(fixed:固定金额 / percent:百分比) +- **AND** 显示定价值(pricing_value) +- **AND** 显示分配者店铺名称 +- **AND** 显示可售套餐数量 +- **AND** 显示状态 +- **AND** 支持分页 + +### Requirement: 成本价计算规则 + +系统 SHALL 根据价格来源计算代理商的成本价。 + +#### Scenario: 系列加价模式(series_pricing) + +- **WHEN** 套餐通过系列分配获得定价 +- **AND** 定价模式为 fixed(固定金额) +- **THEN** 成本价 = 套餐价格 + 固定加价金额 + +- **WHEN** 定价模式为 percent(百分比) +- **THEN** 成本价 = 套餐价格 × (1 + 加价百分比) + +#### Scenario: 单套餐覆盖模式(package_override) + +- **WHEN** 套餐被单独分配并设置了成本价 +- **THEN** 成本价 = 单套餐分配中设置的成本价 +- **AND** 价格来源显示为 package_override + +### Requirement: 数据隔离 + +系统 SHALL 确保代理商只能查看被分配给自己的套餐。 + +#### Scenario: 数据访问隔离 + +- **WHEN** 代理商查询可售套餐列表 +- **THEN** 系统仅返回分配给该代理商的套餐 +- **AND** 不显示其他代理商的套餐信息 + +#### Scenario: 跨代理商访问保护 + +- **WHEN** 代理商尝试访问未分配给自己的套餐详情 +- **THEN** 系统返回403无权访问错误 + +### Requirement: 权限控制 + +系统 SHALL 对可售套餐查询功能实施权限控制。 + +#### Scenario: 仅代理商可访问 + +- **WHEN** 非代理商用户访问可售套餐接口 +- **THEN** 系统返回403无权访问错误 + +#### Scenario: 未认证用户访问 + +- **WHEN** 未登录用户访问可售套餐接口 +- **THEN** 系统返回401未认证错误 diff --git a/openspec/changes/add-package-management-system/specs/package-management/spec.md b/openspec/changes/add-package-management-system/specs/package-management/spec.md new file mode 100644 index 0000000..5e3049d --- /dev/null +++ b/openspec/changes/add-package-management-system/specs/package-management/spec.md @@ -0,0 +1,159 @@ +# Package Management Specification + +## ADDED Requirements + +### Requirement: 套餐列表查询 + +系统 SHALL 提供套餐列表查询功能,支持分页和多条件筛选。 + +#### Scenario: 查询所有套餐 + +- **WHEN** 用户访问套餐管理页面 +- **THEN** 系统显示套餐列表,包含套餐编码、套餐名称、系列ID、套餐类型、流量、价格、状态、上架状态等 +- **AND** 支持按套餐名称模糊搜索 +- **AND** 支持按系列ID筛选 +- **AND** 支持按状态筛选(启用/禁用) +- **AND** 支持按上架状态筛选(上架/下架) +- **AND** 支持按套餐类型筛选(formal/addon) +- **AND** 支持分页,每页最多100条记录 + +#### Scenario: 多条件组合查询 + +- **WHEN** 用户同时使用多个筛选条件 +- **THEN** 系统返回满足所有条件的套餐列表 + +### Requirement: 创建套餐 + +系统 SHALL 允许管理员创建新的套餐。 + +#### Scenario: 成功创建套餐 + +- **WHEN** 用户填写必填字段(套餐编码、套餐名称、套餐类型、套餐时长、套餐价格) +- **AND** 可选填写系列ID、流量信息、成本价、建议售价等 +- **AND** 提交表单 +- **THEN** 系统创建套餐,默认状态为启用,默认上架状态为下架 +- **AND** 返回创建的套餐详情 + +#### Scenario: 套餐编码唯一性验证 + +- **WHEN** 用户使用已存在的套餐编码创建套餐 +- **THEN** 系统返回错误提示"套餐编码已存在" + +#### Scenario: 验证套餐时长范围 + +- **WHEN** 套餐时长小于1个月或大于120个月 +- **THEN** 系统返回验证错误"套餐时长必须在1-120个月之间" + +#### Scenario: 流量类型与流量额度关系 + +- **WHEN** 流量类型为真流量(real) +- **THEN** 真流量额度(real_data_mb)必须大于0 +- **AND** 总流量额度(data_amount_mb)等于真流量额度 + +- **WHEN** 流量类型为虚流量(virtual) +- **THEN** 虚流量额度(virtual_data_mb)必须大于0 +- **AND** 真流量额度和虚流量额度之和等于总流量额度 + +### Requirement: 查看套餐详情 + +系统 SHALL 允许用户查看单个套餐的详细信息。 + +#### Scenario: 查询存在的套餐 + +- **WHEN** 用户通过套餐ID查询详情 +- **THEN** 系统返回该套餐的完整信息,包括所有字段 + +#### Scenario: 查询不存在的套餐 + +- **WHEN** 用户查询不存在的套餐ID +- **THEN** 系统返回404错误 + +### Requirement: 更新套餐 + +系统 SHALL 允许管理员更新套餐信息。 + +#### Scenario: 成功更新套餐 + +- **WHEN** 用户修改套餐的可变字段(名称、时长、价格、流量等) +- **AND** 提交更新 +- **THEN** 系统更新套餐信息 +- **AND** 返回更新后的套餐详情 + +#### Scenario: 套餐编码不可修改 + +- **WHEN** 用户尝试修改套餐编码 +- **THEN** 系统忽略该字段,不允许修改 + +### Requirement: 删除套餐 + +系统 SHALL 允许管理员删除套餐。 + +#### Scenario: 成功删除套餐 + +- **WHEN** 用户删除未被使用的套餐 +- **THEN** 系统删除该套餐 +- **AND** 返回成功状态 + +#### Scenario: 删除被分配的套餐 + +- **WHEN** 用户删除已被分配给代理商的套餐 +- **THEN** 系统返回错误提示"该套餐已被分配,无法删除" + +### Requirement: 套餐状态管理 + +系统 SHALL 支持套餐启用/禁用状态管理。 + +#### Scenario: 启用套餐 + +- **WHEN** 用户将禁用状态的套餐切换为启用 +- **THEN** 系统更新状态为启用(status=1) + +#### Scenario: 禁用套餐 + +- **WHEN** 用户将启用状态的套餐切换为禁用 +- **THEN** 系统更新状态为禁用(status=2) +- **AND** 该套餐将不可用于新的分配或充值 + +### Requirement: 套餐上架状态管理 + +系统 SHALL 支持套餐上架/下架状态管理。 + +#### Scenario: 上架套餐 + +- **WHEN** 用户将下架状态的套餐切换为上架 +- **THEN** 系统更新上架状态为上架(shelf_status=1) +- **AND** 该套餐将对代理商可见 + +#### Scenario: 下架套餐 + +- **WHEN** 用户将上架状态的套餐切换为下架 +- **THEN** 系统更新上架状态为下架(shelf_status=2) +- **AND** 该套餐将对代理商不可见 + +### Requirement: 套餐类型支持 + +系统 SHALL 支持两种套餐类型。 + +#### Scenario: 正式套餐 + +- **WHEN** 创建套餐类型为 formal 的套餐 +- **THEN** 系统记录为正式套餐 + +#### Scenario: 附加套餐 + +- **WHEN** 创建套餐类型为 addon 的套餐 +- **THEN** 系统记录为附加套餐 + +### Requirement: 权限控制 + +系统 SHALL 对套餐管理功能实施权限控制。 + +#### Scenario: 未认证用户访问 + +- **WHEN** 未登录用户访问套餐管理接口 +- **THEN** 系统返回401未认证错误 + +#### Scenario: 无权限用户访问 + +- **WHEN** 已登录但无权限的用户访问套餐管理接口 +- **THEN** 系统返回403无权访问错误 diff --git a/openspec/changes/add-package-management-system/specs/package-series-management/spec.md b/openspec/changes/add-package-management-system/specs/package-series-management/spec.md new file mode 100644 index 0000000..9c1aa1b --- /dev/null +++ b/openspec/changes/add-package-management-system/specs/package-series-management/spec.md @@ -0,0 +1,116 @@ +# Package Series Management Specification + +## ADDED Requirements + +### Requirement: 套餐系列列表查询 + +系统 SHALL 提供套餐系列列表查询功能,支持分页和条件筛选。 + +#### Scenario: 查询所有套餐系列 + +- **WHEN** 用户访问套餐系列管理页面 +- **THEN** 系统显示套餐系列列表,包含系列名称、系列编码、描述、状态、创建时间、更新时间 +- **AND** 支持按系列名称模糊搜索 +- **AND** 支持按状态筛选(启用/禁用) +- **AND** 支持分页,每页最多100条记录 + +#### Scenario: 空列表处理 + +- **WHEN** 没有符合条件的套餐系列 +- **THEN** 系统显示空状态提示 + +### Requirement: 创建套餐系列 + +系统 SHALL 允许管理员创建新的套餐系列。 + +#### Scenario: 成功创建套餐系列 + +- **WHEN** 用户填写系列编码、系列名称(必填) +- **AND** 可选填写描述(最大500字符) +- **AND** 提交表单 +- **THEN** 系统创建套餐系列,默认状态为启用 +- **AND** 返回创建的套餐系列详情 + +#### Scenario: 系列编码重复 + +- **WHEN** 用户使用已存在的系列编码创建套餐系列 +- **THEN** 系统返回错误提示"系列编码已存在" + +#### Scenario: 验证系列名称长度 + +- **WHEN** 系列名称长度小于1或大于255个字符 +- **THEN** 系统返回验证错误 + +### Requirement: 查看套餐系列详情 + +系统 SHALL 允许用户查看单个套餐系列的详细信息。 + +#### Scenario: 查询存在的套餐系列 + +- **WHEN** 用户通过系列ID查询详情 +- **THEN** 系统返回该套餐系列的完整信息 + +#### Scenario: 查询不存在的套餐系列 + +- **WHEN** 用户查询不存在的系列ID +- **THEN** 系统返回404错误 + +### Requirement: 更新套餐系列 + +系统 SHALL 允许管理员更新套餐系列信息。 + +#### Scenario: 成功更新套餐系列 + +- **WHEN** 用户修改系列名称或描述 +- **AND** 提交更新 +- **THEN** 系统更新套餐系列信息 +- **AND** 返回更新后的套餐系列详情 + +#### Scenario: 系列编码不可修改 + +- **WHEN** 用户尝试修改系列编码 +- **THEN** 系统忽略该字段,不允许修改 + +### Requirement: 删除套餐系列 + +系统 SHALL 允许管理员删除套餐系列。 + +#### Scenario: 成功删除套餐系列 + +- **WHEN** 用户删除未被套餐使用的系列 +- **THEN** 系统删除该套餐系列 +- **AND** 返回成功状态 + +#### Scenario: 删除被使用的套餐系列 + +- **WHEN** 用户删除已被套餐关联的系列 +- **THEN** 系统返回错误提示"该系列下存在套餐,无法删除" + +### Requirement: 套餐系列状态管理 + +系统 SHALL 支持套餐系列状态的开关管理。 + +#### Scenario: 启用套餐系列 + +- **WHEN** 用户将禁用状态的系列切换为启用 +- **THEN** 系统更新状态为启用(status=1) + +#### Scenario: 禁用套餐系列 + +- **WHEN** 用户将启用状态的系列切换为禁用 +- **THEN** 系统更新状态为禁用(status=2) +- **AND** 该系列下的套餐可能受到影响(业务规则) + +### Requirement: 权限控制 + +系统 SHALL 对套餐系列管理功能实施权限控制。 + +#### Scenario: 未认证用户访问 + +- **WHEN** 未登录用户访问套餐系列管理接口 +- **THEN** 系统返回401未认证错误 + +#### Scenario: 无权限用户访问 + +- **WHEN** 已登录但无权限的用户访问套餐系列管理接口 +- **THEN** 系统返回403无权访问错误 diff --git a/openspec/changes/add-package-management-system/specs/shop-package-allocation/spec.md b/openspec/changes/add-package-management-system/specs/shop-package-allocation/spec.md new file mode 100644 index 0000000..25b0878 --- /dev/null +++ b/openspec/changes/add-package-management-system/specs/shop-package-allocation/spec.md @@ -0,0 +1,164 @@ +# Shop Package Allocation (单套餐分配) Specification + +## ADDED Requirements + +### Requirement: 单套餐分配列表查询 + +系统 SHALL 提供单套餐分配记录的查询功能。 + +#### Scenario: 查询所有单套餐分配 + +- **WHEN** 管理员访问单套餐分配管理页面 +- **THEN** 系统显示单套餐分配列表 +- **AND** 每条记录包含:分配ID、套餐ID、套餐编码、套餐名称 +- **AND** 显示被分配的店铺ID和店铺名称 +- **AND** 显示覆盖的成本价(cost_price,单位:分) +- **AND** 显示原计算成本价(calculated_cost_price,供参考) +- **AND** 显示关联的系列分配ID +- **AND** 显示状态、创建时间、更新时间 +- **AND** 支持分页,每页最多100条记录 + +#### Scenario: 按店铺筛选 + +- **WHEN** 管理员选择特定店铺ID筛选 +- **THEN** 系统返回该店铺的所有单套餐分配记录 + +#### Scenario: 按套餐筛选 + +- **WHEN** 管理员选择特定套餐ID筛选 +- **THEN** 系统返回该套餐的所有分配记录 + +#### Scenario: 按状态筛选 + +- **WHEN** 管理员选择状态(启用/禁用)筛选 +- **THEN** 系统返回符合状态条件的分配记录 + +### Requirement: 创建单套餐分配 + +系统 SHALL 允许管理员为店铺分配特定套餐并设置成本价。 + +#### Scenario: 成功创建单套餐分配 + +- **WHEN** 管理员选择套餐ID、店铺ID +- **AND** 设置覆盖的成本价(必填,最小值为0) +- **AND** 提交分配 +- **THEN** 系统创建单套餐分配记录 +- **AND** 计算并保存原计算成本价(基于系列分配规则) +- **AND** 返回创建的分配详情,包括关联的系列分配ID + +#### Scenario: 重复分配检查 + +- **WHEN** 管理员为同一店铺分配已分配过的套餐 +- **THEN** 系统返回错误提示"该套餐已分配给此店铺" + +#### Scenario: 店铺和套餐验证 + +- **WHEN** 管理员使用不存在的店铺ID或套餐ID +- **THEN** 系统返回错误提示"店铺或套餐不存在" + +### Requirement: 查看单套餐分配详情 + +系统 SHALL 允许管理员查看单个分配记录的详细信息。 + +#### Scenario: 查询存在的分配记录 + +- **WHEN** 管理员通过分配ID查询详情 +- **THEN** 系统返回该分配记录的完整信息 +- **AND** 包含套餐完整信息、店铺信息、定价信息 + +#### Scenario: 查询不存在的分配记录 + +- **WHEN** 管理员查询不存在的分配ID +- **THEN** 系统返回404错误 + +### Requirement: 更新单套餐分配 + +系统 SHALL 允许管理员更新单套餐分配的成本价。 + +#### Scenario: 成功更新成本价 + +- **WHEN** 管理员修改覆盖的成本价 +- **AND** 提交更新 +- **THEN** 系统更新成本价 +- **AND** 返回更新后的分配详情 + +#### Scenario: 成本价验证 + +- **WHEN** 管理员设置成本价小于0 +- **THEN** 系统返回验证错误"成本价必须大于等于0" + +### Requirement: 删除单套餐分配 + +系统 SHALL 允许管理员删除单套餐分配记录。 + +#### Scenario: 成功删除分配 + +- **WHEN** 管理员删除单套餐分配记录 +- **THEN** 系统删除该分配记录 +- **AND** 该店铺将无法再以此定价售卖该套餐 +- **AND** 返回成功状态 + +#### Scenario: 删除正在使用的分配 + +- **WHEN** 管理员删除正在被订单使用的分配记录 +- **THEN** 系统可能返回警告或阻止删除(根据业务规则) + +### Requirement: 单套餐分配状态管理 + +系统 SHALL 支持单套餐分配的启用/禁用状态管理。 + +#### Scenario: 启用分配 + +- **WHEN** 管理员将禁用状态的分配切换为启用 +- **THEN** 系统更新状态为启用(status=1) +- **AND** 该店铺可以使用此定价售卖套餐 + +#### Scenario: 禁用分配 + +- **WHEN** 管理员将启用状态的分配切换为禁用 +- **THEN** 系统更新状态为禁用(status=2) +- **AND** 该店铺将无法使用此定价售卖套餐 + +### Requirement: 成本价优先级规则 + +系统 SHALL 实现单套餐分配成本价覆盖系列分配规则。 + +#### Scenario: 单套餐分配优先 + +- **WHEN** 店铺同时拥有系列分配和单套餐分配 +- **THEN** 系统使用单套餐分配的成本价 +- **AND** 原计算成本价(calculated_cost_price)保存系列分配规则计算的价格,供参考 + +#### Scenario: 仅系列分配 + +- **WHEN** 店铺只有系列分配,没有单套餐分配 +- **THEN** 系统使用系列分配规则计算成本价 + +### Requirement: 关联系列分配追踪 + +系统 SHALL 追踪单套餐分配与系列分配的关联关系。 + +#### Scenario: 记录关联的系列分配 + +- **WHEN** 创建单套餐分配时 +- **THEN** 系统记录关联的系列分配ID(allocation_id) +- **AND** 用于追溯定价来源 + +### Requirement: 权限控制 + +系统 SHALL 对单套餐分配管理功能实施权限控制。 + +#### Scenario: 未认证用户访问 + +- **WHEN** 未登录用户访问单套餐分配接口 +- **THEN** 系统返回401未认证错误 + +#### Scenario: 无权限用户访问 + +- **WHEN** 已登录但无权限的用户访问单套餐分配接口 +- **THEN** 系统返回403无权访问错误 + +#### Scenario: 仅管理员可操作 + +- **WHEN** 非管理员用户尝试创建、更新或删除单套餐分配 +- **THEN** 系统返回403无权访问错误 diff --git a/openspec/changes/add-package-management-system/tasks.md b/openspec/changes/add-package-management-system/tasks.md new file mode 100644 index 0000000..37ae01a --- /dev/null +++ b/openspec/changes/add-package-management-system/tasks.md @@ -0,0 +1,156 @@ +# Implementation Tasks + +## 1. 基础设施准备 + +- [ ] 1.1 创建套餐管理类型定义文件(src/types/api/packageManagement.ts) +- [ ] 1.2 创建套餐常量配置文件(src/config/constants/package.ts) +- [ ] 1.3 导出常量配置到 constants/index.ts + +## 2. API 服务层实现 + +### 2.1 套餐系列 API(packageSeries.ts) +- [ ] 2.1.1 实现 getPackageSeries(套餐系列列表) +- [ ] 2.1.2 实现 createPackageSeries(创建套餐系列) +- [ ] 2.1.3 实现 getPackageSeriesDetail(获取套餐系列详情) +- [ ] 2.1.4 实现 updatePackageSeries(更新套餐系列) +- [ ] 2.1.5 实现 deletePackageSeries(删除套餐系列) +- [ ] 2.1.6 实现 updatePackageSeriesStatus(更新套餐系列状态) + +### 2.2 套餐管理 API(package.ts) +- [ ] 2.2.1 实现 getPackages(套餐列表) +- [ ] 2.2.2 实现 createPackage(创建套餐) +- [ ] 2.2.3 实现 getPackageDetail(获取套餐详情) +- [ ] 2.2.4 实现 updatePackage(更新套餐) +- [ ] 2.2.5 实现 deletePackage(删除套餐) +- [ ] 2.2.6 实现 updatePackageStatus(更新套餐状态) +- [ ] 2.2.7 实现 updatePackageShelfStatus(更新套餐上架状态) + +### 2.3 代理可售套餐 API(myPackage.ts) +- [ ] 2.3.1 实现 getMyPackages(我的可售套餐列表) +- [ ] 2.3.2 实现 getMyPackageDetail(获取可售套餐详情) +- [ ] 2.3.3 实现 getMySeriesAllocations(我的被分配系列列表) + +### 2.4 单套餐分配 API(shopPackageAllocation.ts) +- [ ] 2.4.1 实现 getShopPackageAllocations(单套餐分配列表) +- [ ] 2.4.2 实现 createShopPackageAllocation(创建单套餐分配) +- [ ] 2.4.3 实现 getShopPackageAllocationDetail(获取单套餐分配详情) +- [ ] 2.4.4 实现 updateShopPackageAllocation(更新单套餐分配) +- [ ] 2.4.5 实现 deleteShopPackageAllocation(删除单套餐分配) +- [ ] 2.4.6 实现 updateShopPackageAllocationStatus(更新单套餐分配状态) + +- [ ] 2.5 在 src/api/modules/index.ts 中导出所有新服务 + +## 3. 页面实现 + +### 3.1 套餐系列管理页面(package-series/index.vue) +- [ ] 3.1.1 实现列表展示(表格、分页) +- [ ] 3.1.2 实现搜索栏(系列名称、状态筛选) +- [ ] 3.1.3 实现新增对话框(表单验证) +- [ ] 3.1.4 实现编辑功能(复用新增对话框,根据 dialogType 区分新增/编辑) +- [ ] 3.1.5 实现删除功能(二次确认) +- [ ] 3.1.6 实现状态开关(启用/禁用) +- [ ] 3.1.7 集成 API 服务并处理加载状态 + +### 3.2 套餐管理页面(package-list/index.vue) +- [ ] 3.2.1 实现列表展示(表格、分页) +- [ ] 3.2.2 实现搜索栏(名称、系列、状态、上架状态、类型筛选) +- [ ] 3.2.3 实现系列下拉选择器(加载套餐系列列表,只显示启用状态) +- [ ] 3.2.4 实现新增对话框(表单验证、系列选择) +- [ ] 3.2.5 实现编辑功能(复用新增对话框,根据 dialogType 区分新增/编辑) +- [ ] 3.2.6 实现删除功能(二次确认) +- [ ] 3.2.7 实现状态开关(启用/禁用) +- [ ] 3.2.8 实现上架状态开关(上架/下架) +- [ ] 3.2.9 集成 API 服务并处理加载状态 + +### 3.3 代理可售套餐页面(my-packages/index.vue) +- [ ] 3.3.1 创建页面文件和基本结构 +- [ ] 3.3.2 实现列表展示(表格、分页) +- [ ] 3.3.3 实现搜索栏(系列、类型筛选) +- [ ] 3.3.4 实现详情对话框(显示成本价、建议售价、利润空间) +- [ ] 3.3.5 实现被分配系列列表Tab(可选) +- [ ] 3.3.6 集成 API 服务并处理加载状态 + +### 3.4 单套餐分配页面(package-assign/index.vue) +- [ ] 3.4.1 创建页面文件和基本结构 +- [ ] 3.4.2 实现列表展示(表格、分页) +- [ ] 3.4.3 实现搜索栏(店铺、套餐、状态筛选) +- [ ] 3.4.4 实现套餐下拉选择器(加载套餐列表,只显示启用且上架的套餐) +- [ ] 3.4.5 实现店铺下拉选择器(使用 ShopService 加载店铺列表) +- [ ] 3.4.6 实现新增对话框(套餐选择、店铺选择、成本价输入) +- [ ] 3.4.7 实现编辑功能(单独对话框或复用新增对话框,只允许修改成本价) +- [ ] 3.4.8 实现删除功能(二次确认) +- [ ] 3.4.9 实现状态管理(启用/禁用开关) +- [ ] 3.4.10 集成 API 服务并处理加载状态 + +## 4. 路由配置 + +- [ ] 4.1 在 asyncRoutes.ts 中添加 my-packages 路由配置 +- [ ] 4.2 验证路由权限配置正确 + +## 5. 集成测试 + +### 5.1 套餐系列管理测试 +- [ ] 5.1.1 测试列表查询(空列表、有数据、分页) +- [ ] 5.1.2 测试搜索功能(名称模糊搜索、状态筛选) +- [ ] 5.1.3 测试新增功能(成功、编码重复、字段验证) +- [ ] 5.1.4 测试编辑功能(成功、字段验证) +- [ ] 5.1.5 测试删除功能(成功、有关联套餐时禁止删除) +- [ ] 5.1.6 测试状态切换(启用→禁用、禁用→启用) +- [ ] 5.1.7 测试权限控制(未登录、无权限) + +### 5.2 套餐管理测试 +- [ ] 5.2.1 测试列表查询(空列表、有数据、分页) +- [ ] 5.2.2 测试多条件筛选(名称、系列、状态、上架状态、类型) +- [ ] 5.2.3 测试系列下拉选择器(只显示启用状态的系列) +- [ ] 5.2.4 测试新增功能(成功、编码重复、时长验证、流量验证) +- [ ] 5.2.5 测试编辑功能(成功、字段验证) +- [ ] 5.2.6 测试删除功能(成功、已分配时禁止删除) +- [ ] 5.2.7 测试状态切换(启用→禁用、禁用→启用) +- [ ] 5.2.8 测试上架状态切换(上架→下架、下架→上架) +- [ ] 5.2.9 测试权限控制(未登录、无权限) + +### 5.3 代理可售套餐测试 +- [ ] 5.3.1 测试列表查询(空列表、有数据、分页) +- [ ] 5.3.2 测试筛选功能(按系列、按类型) +- [ ] 5.3.3 测试详情查询(显示成本价、建议售价、利润空间、价格来源) +- [ ] 5.3.4 测试数据隔离(只能看到分配给自己的套餐) +- [ ] 5.3.5 测试被分配系列列表(如果实现) +- [ ] 5.3.6 测试权限控制(非代理商用户无法访问) + +### 5.4 单套餐分配测试 +- [ ] 5.4.1 测试列表查询(空列表、有数据、分页) +- [ ] 5.4.2 测试筛选功能(按店铺、按套餐、按状态) +- [ ] 5.4.3 测试套餐下拉选择器(只显示启用且上架的套餐) +- [ ] 5.4.4 测试店铺下拉选择器(加载店铺列表) +- [ ] 5.4.5 测试新增功能(成功、重复分配、成本价验证) +- [ ] 5.4.6 测试编辑功能(修改成本价) +- [ ] 5.4.7 测试删除功能(成功、有订单时的处理) +- [ ] 5.4.8 测试状态切换(启用→禁用、禁用→启用) +- [ ] 5.4.9 测试价格覆盖规则(单套餐分配优先于系列分配) +- [ ] 5.4.10 测试权限控制(仅管理员可操作) + +### 5.5 通用功能测试 +- [ ] 5.5.1 测试所有页面的表单验证(必填、长度、格式) +- [ ] 5.5.2 测试所有页面的 loading 状态(列表、提交、删除) +- [ ] 5.5.3 测试所有页面的错误处理(网络错误、业务错误) +- [ ] 5.5.4 测试所有页面的二次确认(删除操作) +- [ ] 5.5.5 测试分页功能(换页、改变每页数量) +- [ ] 5.5.6 测试刷新功能(列表刷新) +- [ ] 5.5.7 测试列显示/隐藏功能 +- [ ] 5.5.8 测试状态值映射(前端0/1与后端1/2的转换) + +## 6. 代码优化和文档 + +- [ ] 6.1 代码格式化和 ESLint 检查 +- [ ] 6.2 添加必要的注释 +- [ ] 6.3 更新 API 文档(如需要) +- [ ] 6.4 提交代码并创建 PR + +## 注意事项 + +- 所有页面需参考 `/system/role` 页面的组件化结构 +- 使用统一的 `CommonStatus` 常量(需要注意文档中的状态值映射) +- API 字段使用下划线命名(如 `series_name`),前端类型使用驼峰命名 +- 所有删除操作需要二次确认 +- 所有表单需要完整的验证规则 +- 统一使用 Element Plus 的 Message 和 MessageBox 组件 diff --git a/openspec/changes/update-series-allocation-commission/design.md b/openspec/changes/update-series-allocation-commission/design.md new file mode 100644 index 0000000..7a9c795 --- /dev/null +++ b/openspec/changes/update-series-allocation-commission/design.md @@ -0,0 +1,190 @@ +# Design: 套餐系列分配佣金系统重构 + +## Context + +当前系统使用简单的定价模式(pricing_mode/pricing_value)和一次性佣金(one_time_commission_*)来管理套餐系列分配。随着业务发展,需要更复杂的佣金系统: +- 支持基础返佣(固定金额或百分比) +- 支持梯度返佣(根据销量或销售额分档返佣) +- 更清晰的数据模型和API接口 + +**背景约束**: +- 前后端需要同步部署(Breaking Change) +- 需要数据迁移方案 +- 影响现有的套餐系列分配功能 + +## Goals / Non-Goals + +**Goals**: +- 实现新的佣金配置模型,支持基础返佣和梯度返佣 +- 提供清晰的UI界面让用户配置复杂的返佣规则 +- 保证数据一致性和类型安全 +- 提供良好的用户体验(表单验证、错误提示) + +**Non-Goals**: +- 不处理历史数据的完整性验证(由后端负责) +- 不实现佣金计算逻辑(由后端负责) +- 不处理佣金结算流程(属于其他模块) + +## Decisions + +### 1. Data Model Design + +**决策**: 采用嵌套对象结构表示佣金配置 + +**理由**: +- `base_commission: { mode, value }` - 清晰表达基础返佣的两个维度 +- `tier_config: { period_type, tier_type, tiers[] }` - 梯度配置与基础配置分离,可选性强 +- `tiers: [{ threshold, mode, value }]` - 每个档位都有独立的返佣模式和值 + +**替代方案考虑**: +- ❌ 平铺所有字段 - 会导致字段过多,语义不清晰 +- ❌ 使用JSON字符串存储配置 - 失去类型安全,不利于表单编辑 + +### 2. UI Design Pattern + +**决策**: 使用渐进式表单设计 + +**表单结构**: +``` +1. 基础返佣配置 (必填) + - 返佣模式: 单选 (固定金额/百分比) + - 返佣值: 数字输入 + +2. 梯度返佣设置 (可选) + - 启用开关: 是/否 + - [如果启用]: + - 周期类型: 下拉 (月度/季度/年度) + - 梯度类型: 下拉 (销量/销售额) + - 档位列表: 动态表单 + * 阈值 + * 返佣模式 + * 返佣值 + * [添加]/[删除] 按钮 +``` + +**理由**: +- 渐进式设计降低初始复杂度 +- 只有启用梯度返佣时才显示相关配置 +- 动态档位列表提供灵活性 + +**替代方案考虑**: +- ❌ 全部平铺展示 - 对不需要梯度返佣的用户造成困扰 +- ❌ 使用向导模式 - 增加操作步骤,不适合编辑场景 + +### 3. Form Validation Strategy + +**决策**: 分层验证 + 条件验证 + +**验证规则**: +1. 基础返佣配置: + - mode: 必选 + - value: 必填,>= 0 + +2. 梯度返佣配置(当启用时): + - period_type: 必选 + - tier_type: 必选 + - tiers: 至少一个档位 + - 每个档位: + - threshold: 必填,> 0 + - mode: 必选 + - value: 必填,> 0 + - 档位阈值必须递增 + +**实现方式**: +- 使用 Element Plus 的表单验证 +- 自定义validator处理档位阈值递增验证 +- 使用 computed 动态生成验证规则 + +### 4. API Response Handling + +**决策**: 统一使用 `list` 字段名,添加适配层 + +**理由**: +- 后端统一规范使用 `list` 而非 `items` +- 添加 `total_pages` 字段提供更完整的分页信息 +- 保持前端代码与后端规范一致 + +**迁移策略**: +```typescript +// Before +const res = await getShopSeriesAllocations(params) +allocationList.value = res.data.items || [] + +// After +const res = await getShopSeriesAllocations(params) +allocationList.value = res.data.list || [] +``` + +## Risks / Trade-offs + +### Risk 1: Breaking Change导致部署协调困难 + +**风险**: 前后端必须同时部署,否则会出现接口不兼容 + +**缓解措施**: +- 与后端团队协调部署窗口 +- 准备回退方案(前端代码分支) +- 在测试环境充分验证 + +### Risk 2: 复杂表单导致用户体验问题 + +**风险**: 梯度返佣配置较复杂,用户可能不理解 + +**缓解措施**: +- 提供清晰的字段说明 +- 添加示例或帮助文档链接 +- 使用默认值简化初次配置 + +### Risk 3: 数据迁移可能失败 + +**风险**: 旧数据无法完全转换为新模型 + +**缓解措施**: +- 要求后端提供数据迁移脚本和验证 +- 在迁移后检查数据完整性 +- 保留旧数据备份 + +## Migration Plan + +### Phase 1: 准备阶段 +1. 与后端确认API变更细节和时间表 +2. 在开发环境实现前端变更 +3. 与后端在测试环境联调 + +### Phase 2: 测试阶段 +1. 功能测试: 创建、编辑、删除、列表展示 +2. 集成测试: 与后端API集成 +3. 用户验收测试: 业务人员验证 + +### Phase 3: 部署阶段 +1. 准备回退方案 +2. 与后端协调部署窗口 +3. 同步部署前后端 +4. 验证生产环境功能 + +### Phase 4: 监控阶段 +1. 监控API错误率 +2. 收集用户反馈 +3. 修复遗留问题 + +### Rollback Plan + +如果部署后发现严重问题: +1. 前端回退到上一版本 +2. 后端回退API(如果可能) +3. 通知用户暂时不可用 +4. 修复问题后重新部署 + +## Open Questions + +1. **Q**: 梯度返佣的档位数量是否有上限? + - **A**: 待后端确认,前端可以先不限制或设置合理上限(如10个) + +2. **Q**: 返佣值的单位和精度如何处理? + - **A**: 固定金额使用"分"为单位,百分比使用千分比(如200=20%),待后端确认 + +3. **Q**: 是否需要在列表页显示梯度返佣信息? + - **A**: 暂时只显示基础返佣,梯度信息在详情或编辑时查看 + +4. **Q**: 旧数据如何映射到新模型? + - **A**: 待后端提供迁移方案,前端需要能够正确显示迁移后的数据 diff --git a/openspec/changes/update-series-allocation-commission/proposal.md b/openspec/changes/update-series-allocation-commission/proposal.md new file mode 100644 index 0000000..f2daa82 --- /dev/null +++ b/openspec/changes/update-series-allocation-commission/proposal.md @@ -0,0 +1,46 @@ +# Change: 更新套餐系列分配佣金系统 + +## Why + +当前套餐系列分配使用简单的定价模式(固定加价/百分比加价)和一次性佣金配置,不支持复杂的返佣规则。新的业务需求要求支持: +- 基础返佣配置(固定金额或百分比) +- 梯度返佣系统(根据销量或销售额分档返佣) +- 更灵活的佣金计算模型 + +## What Changes + +**BREAKING** - 完全重构套餐系列分配的数据模型和API接口: + +- 移除旧的定价字段: `pricing_mode`, `pricing_value`, `calculated_cost_price` +- 移除旧的一次性佣金字段: `one_time_commission_trigger`, `one_time_commission_threshold`, `one_time_commission_amount` +- 新增基础返佣配置: `base_commission` (包含 mode 和 value) +- 新增梯度返佣开关: `enable_tier_commission` +- 新增梯度返佣配置: `tier_config` (可选,当启用梯度返佣时需要) +- 响应数据结构从 `items` 改为 `list` (符合后端统一规范) +- 更新所有相关的创建/更新接口以支持新的佣金模型 + +## Impact + +- **影响模块**: 套餐系列分配 (`/package-management/series-assign`) +- **受影响文件**: + - `src/types/api/packageManagement.ts` - TypeScript类型定义 + - `src/api/modules/shopSeriesAllocation.ts` - API服务层 + - `src/views/package-management/series-assign/index.vue` - 前端页面 +- **数据迁移**: 需要后端提供数据迁移方案,将旧的定价数据转换为新的佣金配置 +- **向后兼容性**: **不兼容**,需要前后端同时部署 + +## Breaking Changes + +1. **API响应结构变更**: + - 列表接口响应从 `{ items, page, page_size, total }` 改为 `{ list, page, page_size, total, total_pages }` + - 移除 `pricing_mode`, `pricing_value`, `calculated_cost_price` 字段 + - 移除一次性佣金相关字段 + - 新增 `base_commission`, `enable_tier_commission` 字段 + +2. **API请求结构变更**: + - 创建/更新接口需要新的 `base_commission` 对象 + - 支持可选的 `enable_tier_commission` 和 `tier_config` + +3. **前端组件变更**: + - 表单需要支持基础佣金配置和梯度返佣配置 + - 表格列需要显示新的佣金信息 diff --git a/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md b/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md new file mode 100644 index 0000000..d554d2d --- /dev/null +++ b/openspec/changes/update-series-allocation-commission/specs/package-series-allocation/spec.md @@ -0,0 +1,225 @@ +# Package Series Allocation Spec Delta + +## MODIFIED Requirements + +### Requirement: 套餐系列分配列表查询 + +系统 SHALL 提供套餐系列分配列表查询功能,支持按店铺、系列和状态筛选,返回包含新佣金配置的分配信息。 + +#### Scenario: 成功获取分配列表 + +- **WHEN** 用户请求套餐系列分配列表 +- **THEN** 系统返回分配列表,每项包含: + - 基本信息: id, series_id, series_name, shop_id, shop_name + - 分配者信息: allocator_shop_id, allocator_shop_name + - 佣金配置: base_commission (mode, value), enable_tier_commission + - 状态和时间: status, created_at, updated_at +- **AND** 响应结构为: `{ list, page, page_size, total, total_pages }` + +#### Scenario: 按条件筛选分配列表 + +- **WHEN** 用户提供筛选条件 (shop_id, series_id, status) +- **THEN** 系统返回符合条件的分配列表 +- **AND** 支持分页参数 (page, page_size) + +### Requirement: 创建套餐系列分配 + +系统 SHALL 允许用户创建套餐系列分配,配置基础返佣和可选的梯度返佣规则。 + +#### Scenario: 创建基础返佣分配 + +- **WHEN** 用户提交创建请求,包含: + - series_id: 套餐系列ID + - shop_id: 被分配的店铺ID + - base_commission: { mode: "fixed"/"percent", value: number } +- **THEN** 系统创建分配记录 +- **AND** 返回完整的分配信息包含 allocator_shop_id 和 allocator_shop_name + +#### Scenario: 创建带梯度返佣的分配 + +- **WHEN** 用户提交创建请求,包含: + - series_id, shop_id, base_commission (同上) + - enable_tier_commission: true + - tier_config: { period_type, tier_type, tiers[] } + - period_type: "monthly"/"quarterly"/"yearly" + - tier_type: "sales_count"/"sales_amount" + - tiers: [{ threshold, mode, value }] +- **THEN** 系统创建分配记录并保存梯度配置 +- **AND** 梯度档位按阈值升序存储 + +#### Scenario: 验证梯度档位阈值递增 + +- **WHEN** 用户提交的梯度档位阈值不是递增的 +- **THEN** 系统返回400错误 +- **AND** 错误消息说明阈值必须递增 + +### Requirement: 更新套餐系列分配 + +系统 SHALL 允许用户更新现有分配的佣金配置,包括基础返佣和梯度返佣。 + +#### Scenario: 更新基础返佣配置 + +- **WHEN** 用户提交更新请求,包含新的 base_commission +- **THEN** 系统更新分配记录 +- **AND** 返回更新后的完整信息 + +#### Scenario: 启用或禁用梯度返佣 + +- **WHEN** 用户更新 enable_tier_commission 标志 +- **THEN** 系统更新配置 +- **AND** 如果启用,必须提供有效的 tier_config +- **AND** 如果禁用,tier_config 可以为空 + +#### Scenario: 更新梯度返佣配置 + +- **WHEN** 用户提交包含新 tier_config 的更新请求 +- **THEN** 系统替换整个梯度配置 +- **AND** 验证新档位阈值递增 +- **AND** 返回更新后的信息 + +### Requirement: 删除套餐系列分配 + +系统 SHALL 允许用户删除套餐系列分配记录。 + +#### Scenario: 成功删除分配 + +- **WHEN** 用户请求删除指定ID的分配 +- **THEN** 系统删除该分配记录 +- **AND** 返回成功响应 (200) + +#### Scenario: 删除不存在的分配 + +- **WHEN** 用户请求删除不存在的分配ID +- **THEN** 系统返回404错误 +- **AND** 错误消息说明分配不存在 + +### Requirement: 获取套餐系列分配详情 + +系统 SHALL 提供单个分配记录的详细信息查询。 + +#### Scenario: 成功获取详情 + +- **WHEN** 用户请求指定ID的分配详情 +- **THEN** 系统返回完整的分配信息,包括: + - 所有基本字段 + - base_commission 对象 + - enable_tier_commission 标志 + - tier_config (如果启用了梯度返佣) + +### Requirement: 更新套餐系列分配状态 + +系统 SHALL 允许用户启用或禁用套餐系列分配。 + +#### Scenario: 切换分配状态 + +- **WHEN** 用户提交状态更新请求 (status: 1启用/2禁用) +- **THEN** 系统更新分配状态 +- **AND** 返回成功响应 + +#### Scenario: 禁用分配后的行为 + +- **WHEN** 分配被禁用 (status: 2) +- **THEN** 该分配不再参与佣金计算 (由后端业务逻辑保证) +- **AND** 前端列表中状态显示为"禁用" + +## REMOVED Requirements + +### Requirement: 定价模式配置 + +**Reason**: 旧的定价模式 (pricing_mode, pricing_value) 已被新的佣金模型替代 + +**Migration**: 旧数据通过后端迁移脚本转换为基础返佣配置: +- pricing_mode="fixed" → base_commission.mode="fixed" +- pricing_mode="percentage" → base_commission.mode="percent" +- pricing_value → base_commission.value (需要单位转换) + +### Requirement: 一次性佣金配置 + +**Reason**: 一次性佣金配置 (one_time_commission_*) 已被梯度返佣系统替代 + +**Migration**: 旧的一次性佣金通过以下方式迁移: +- 如果设置了一次性佣金,转换为单档位的梯度返佣 +- trigger → tier_type mapping (first_activation → sales_count, cumulative_recharge → sales_amount) +- threshold → tiers[0].threshold +- amount → tiers[0].value (mode设为fixed) + +### Requirement: 计算成本价字段 + +**Reason**: calculated_cost_price 字段在新模型中不再使用 + +**Migration**: 该字段不再返回,成本计算由后端业务逻辑内部处理 + +## ADDED Requirements + +### Requirement: 基础返佣配置 + +系统 SHALL 为每个分配提供基础返佣配置,包含返佣模式和返佣值。 + +#### Scenario: 固定金额返佣 + +- **WHEN** base_commission.mode = "fixed" +- **THEN** base_commission.value 表示固定返佣金额(单位:分) +- **AND** 每笔交易返佣该固定金额 + +#### Scenario: 百分比返佣 + +- **WHEN** base_commission.mode = "percent" +- **THEN** base_commission.value 表示返佣百分比的千分比 (如200表示20%) +- **AND** 每笔交易返佣 = 交易金额 * (value / 1000) + +### Requirement: 梯度返佣配置 + +系统 SHALL 支持可选的梯度返佣配置,根据周期内的销量或销售额分档返佣。 + +#### Scenario: 按销量分档返佣 + +- **WHEN** tier_config.tier_type = "sales_count" +- **THEN** 系统根据周期内的销售数量匹配档位 +- **AND** 达到档位阈值后,该档位的返佣规则生效 + +#### Scenario: 按销售额分档返佣 + +- **WHEN** tier_config.tier_type = "sales_amount" +- **THEN** 系统根据周期内的销售金额(分)匹配档位 +- **AND** 达到档位阈值后,该档位的返佣规则生效 + +#### Scenario: 月度返佣周期 + +- **WHEN** tier_config.period_type = "monthly" +- **THEN** 系统按自然月统计销量或销售额 +- **AND** 每月初重置计数 + +#### Scenario: 季度返佣周期 + +- **WHEN** tier_config.period_type = "quarterly" +- **THEN** 系统按季度统计销量或销售额 +- **AND** 每季度初重置计数 + +#### Scenario: 年度返佣周期 + +- **WHEN** tier_config.period_type = "yearly" +- **THEN** 系统按年度统计销量或销售额 +- **AND** 每年初重置计数 + +### Requirement: 梯度档位管理 + +系统 SHALL 支持多个返佣档位,每个档位有独立的阈值和返佣规则。 + +#### Scenario: 多档位返佣 + +- **WHEN** 配置了多个档位 (tiers数组) +- **THEN** 系统按阈值从低到高排列档位 +- **AND** 当销量/销售额超过某档位阈值时,使用该档位的返佣规则 + +#### Scenario: 档位阈值唯一性 + +- **WHEN** 用户提交包含重复阈值的档位 +- **THEN** 系统返回400错误 +- **AND** 错误消息说明阈值必须唯一 + +#### Scenario: 档位返佣模式 + +- **WHEN** 档位的 mode = "fixed" +- **THEN** value 表示固定返佣金额(分) +- **WHEN** 档位的 mode = "percent" +- **THEN** value 表示返佣百分比的千分比 diff --git a/openspec/changes/update-series-allocation-commission/tasks.md b/openspec/changes/update-series-allocation-commission/tasks.md new file mode 100644 index 0000000..f81619d --- /dev/null +++ b/openspec/changes/update-series-allocation-commission/tasks.md @@ -0,0 +1,59 @@ +# Implementation Tasks + +## 1. Type Definitions Update + +- [x] 1.1 更新 `ShopSeriesAllocationResponse` 接口,移除旧字段,添加新的佣金字段 +- [x] 1.2 创建 `BaseCommissionConfig` 类型定义 (mode, value) +- [x] 1.3 创建 `TierCommissionConfig` 类型定义 (period_type, tier_type, tiers) +- [x] 1.4 创建 `TierEntry` 类型定义 (threshold, mode, value) +- [x] 1.5 更新 `CreateShopSeriesAllocationRequest` 接口以支持新的佣金字段 +- [x] 1.6 更新 `UpdateShopSeriesAllocationRequest` 接口以支持新的佣金字段 +- [x] 1.7 更新分页响应类型,从 `items` 改为 `list`,添加 `total_pages` + +## 2. API Service Layer Update + +- [x] 2.1 更新 `getShopSeriesAllocations` 方法以处理新的响应结构 (list 而非 items) +- [x] 2.2 更新 `createShopSeriesAllocation` 方法以发送新的佣金配置 +- [x] 2.3 更新 `updateShopSeriesAllocation` 方法以发送新的佣金配置 +- [x] 2.4 确保所有API方法正确处理新的类型定义 + +## 3. Frontend Page Refactoring + +- [x] 3.1 移除旧的定价模式相关代码 (pricing_mode, pricing_value) +- [x] 3.2 移除旧的一次性佣金配置相关代码 +- [x] 3.3 更新表格列定义,移除 `calculated_cost_price` 等旧列 +- [x] 3.4 添加基础佣金配置表单字段 (mode: fixed/percent, value) +- [x] 3.5 添加梯度返佣开关字段 (enable_tier_commission) +- [x] 3.6 添加梯度返佣配置表单 (period_type, tier_type, tiers数组) +- [x] 3.7 实现梯度档位的动态添加/删除功能 +- [x] 3.8 更新表格列以显示新的佣金信息 (base_commission) +- [x] 3.9 更新表单验证规则以适配新的字段要求 +- [x] 3.10 更新数据列表获取逻辑,从 `res.data.items` 改为 `res.data.list` +- [x] 3.11 更新 `showDialog` 函数以正确填充新的佣金字段 +- [x] 3.12 更新 `handleDialogClosed` 函数以重置新的佣金字段 +- [x] 3.13 更新 `handleSubmit` 函数以正确提交新的佣金配置 + +## 4. UI/UX Enhancement + +- [x] 4.1 设计并实现基础佣金配置UI (单选模式 + 数值输入) +- [x] 4.2 设计并实现梯度返佣配置UI (周期类型、梯度类型、档位列表) +- [x] 4.3 添加梯度档位的表单验证 (阈值递增、必填字段等) +- [x] 4.4 优化对话框布局以容纳新增的配置项 +- [x] 4.5 添加梯度返佣的帮助提示或说明文档链接 + +## 5. Data Migration Support + +- [x] 5.1 与后端确认数据迁移方案和时间表 +- [x] 5.2 准备回退方案(如果部署失败) +- [x] 5.3 准备测试数据以验证迁移正确性 + +## 6. Testing and Validation + +- [x] 6.1 测试基础佣金配置的创建和编辑 +- [x] 6.2 测试梯度返佣的创建和编辑 +- [x] 6.3 测试档位添加/删除功能 +- [x] 6.4 测试表单验证规则 +- [x] 6.5 测试数据列表的显示和分页 +- [x] 6.6 测试状态切换功能 +- [x] 6.7 测试删除功能 +- [x] 6.8 验证与后端API的集成 diff --git a/src/api/BaseService.ts b/src/api/BaseService.ts index e5e5ccd..5a414b1 100644 --- a/src/api/BaseService.ts +++ b/src/api/BaseService.ts @@ -61,6 +61,24 @@ export class BaseService { }) } + /** + * PATCH 请求 + * @param url 请求URL + * @param data 请求数据 + * @param config 额外配置 + */ + protected static patch( + url: string, + data?: Record, + config?: Record + ): Promise { + return request.patch({ + url, + data, + ...config + }) + } + /** * DELETE 请求 * @param url 请求URL diff --git a/src/api/modules/card.ts b/src/api/modules/card.ts index 53431b9..12e2b63 100644 --- a/src/api/modules/card.ts +++ b/src/api/modules/card.ts @@ -354,4 +354,17 @@ export class CardService extends BaseService { static getAssetAllocationRecordDetail(id: number): Promise> { return this.getOne(`/api/admin/asset-allocation-records/${id}`) } + + // ========== 批量设置卡的套餐系列绑定相关 ========== + + /** + * 批量设置卡的套餐系列绑定 + * @param data 请求参数 + */ + static batchSetCardSeriesBinding(data: { + iccids: string[] + series_allocation_id: number + }): Promise> { + return this.patch('/api/admin/iot-cards/series-binding', data) + } } diff --git a/src/api/modules/commission.ts b/src/api/modules/commission.ts index 9d30a19..e069bd1 100644 --- a/src/api/modules/commission.ts +++ b/src/api/modules/commission.ts @@ -18,7 +18,11 @@ import type { SubmitWithdrawalParams, ShopCommissionRecordPageResult, ShopCommissionSummaryQueryParams, - ShopCommissionSummaryPageResult + ShopCommissionSummaryPageResult, + MyCommissionStatsQueryParams, + MyCommissionStatsResponse, + MyDailyCommissionStatsQueryParams, + DailyCommissionStatsItem } from '@/types/api/commission' export class CommissionService extends BaseService { @@ -130,6 +134,32 @@ export class CommissionService extends BaseService { return this.post('/api/admin/my/withdrawal-requests', params) } + /** + * 获取我的佣金统计 + * GET /api/admin/my/commission-stats + */ + static getMyCommissionStats( + params?: MyCommissionStatsQueryParams + ): Promise> { + return this.get>( + '/api/admin/my/commission-stats', + params + ) + } + + /** + * 获取我的每日佣金统计 + * GET /api/admin/my/commission-daily-stats + */ + static getMyDailyCommissionStats( + params?: MyDailyCommissionStatsQueryParams + ): Promise> { + return this.get>( + '/api/admin/my/commission-daily-stats', + params + ) + } + // ==================== 代理商佣金管理 ==================== /** diff --git a/src/api/modules/device.ts b/src/api/modules/device.ts index 10af425..f12f666 100644 --- a/src/api/modules/device.ts +++ b/src/api/modules/device.ts @@ -154,4 +154,17 @@ export class DeviceService extends BaseService { static getImportTaskDetail(id: number): Promise> { return this.getOne(`/api/admin/devices/import/tasks/${id}`) } + + // ========== 批量设置设备的套餐系列绑定相关 ========== + + /** + * 批量设置设备的套餐系列绑定 + * @param data 请求参数 + */ + static batchSetDeviceSeriesBinding(data: { + device_ids: number[] + series_allocation_id: number + }): Promise> { + return this.patch('/api/admin/devices/series-binding', data) + } } diff --git a/src/api/modules/enterprise.ts b/src/api/modules/enterprise.ts index 196e85a..199c302 100644 --- a/src/api/modules/enterprise.ts +++ b/src/api/modules/enterprise.ts @@ -24,6 +24,14 @@ import type { RecallCardsRequest, RecallCardsResponse } from '@/types/api/enterpriseCard' +import type { + EnterpriseDeviceListParams, + EnterpriseDevicePageResult, + AllocateDevicesRequest, + AllocateDevicesResponse, + RecallDevicesRequest, + RecallDevicesResponse +} from '@/types/api/enterpriseDevice' export class EnterpriseService extends BaseService { /** @@ -157,4 +165,51 @@ export class EnterpriseService extends BaseService { data ) } + + // ========== 企业设备授权相关 ========== + + /** + * 授权设备给企业 + * @param enterpriseId 企业ID + * @param data 授权请求数据 + */ + static allocateDevices( + enterpriseId: number, + data: AllocateDevicesRequest + ): Promise> { + return this.post>( + `/api/admin/enterprises/${enterpriseId}/allocate-devices`, + data + ) + } + + /** + * 获取企业设备列表 + * @param enterpriseId 企业ID + * @param params 查询参数 + */ + static getEnterpriseDevices( + enterpriseId: number, + params?: EnterpriseDeviceListParams + ): Promise> { + return this.get>( + `/api/admin/enterprises/${enterpriseId}/devices`, + params + ) + } + + /** + * 撤销设备授权 + * @param enterpriseId 企业ID + * @param data 撤销请求数据 + */ + static recallDevices( + enterpriseId: number, + data: RecallDevicesRequest + ): Promise> { + return this.post>( + `/api/admin/enterprises/${enterpriseId}/recall-devices`, + data + ) + } } diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts index d97805f..a229e8c 100644 --- a/src/api/modules/index.ts +++ b/src/api/modules/index.ts @@ -21,7 +21,12 @@ export { StorageService } from './storage' export { AuthorizationService } from './authorization' export { DeviceService } from './device' export { CarrierService } from './carrier' +export { PackageSeriesService } from './packageSeries' +export { PackageManageService } from './packageManage' +export { MyPackageService } from './myPackage' +export { ShopPackageAllocationService } from './shopPackageAllocation' +export { ShopSeriesAllocationService } from './shopSeriesAllocation' +export { OrderService } from './order' // TODO: 按需添加其他业务模块 -// export { PackageService } from './package' // export { SettingService } from './setting' diff --git a/src/api/modules/myPackage.ts b/src/api/modules/myPackage.ts new file mode 100644 index 0000000..8d465f4 --- /dev/null +++ b/src/api/modules/myPackage.ts @@ -0,0 +1,45 @@ +/** + * 代理可售套餐 API 服务 + */ + +import { BaseService } from '../BaseService' +import type { + MyPackageResponse, + MyPackageQueryParams, + MySeriesAllocationResponse, + BaseResponse, + PaginationResponse +} from '@/types/api' + +export class MyPackageService extends BaseService { + /** + * 获取我的可售套餐列表 + * GET /api/admin/my-packages + * @param params 查询参数 + */ + static getMyPackages( + params?: MyPackageQueryParams + ): Promise> { + return this.getPage('/api/admin/my-packages', params) + } + + /** + * 获取我的可售套餐详情 + * GET /api/admin/my-packages/{id} + * @param id 套餐ID + */ + static getMyPackageDetail(id: number): Promise> { + return this.getOne(`/api/admin/my-packages/${id}`) + } + + /** + * 获取我的被分配系列列表 + * GET /api/admin/my-series-allocations + * @param params 查询参数(支持分页) + */ + static getMySeriesAllocations( + params?: Record + ): Promise> { + return this.getPage('/api/admin/my-series-allocations', params) + } +} diff --git a/src/api/modules/order.ts b/src/api/modules/order.ts new file mode 100644 index 0000000..aff9c23 --- /dev/null +++ b/src/api/modules/order.ts @@ -0,0 +1,47 @@ +/** + * 订单管理相关 API + */ + +import { BaseService } from '../BaseService' +import type { + Order, + OrderQueryParams, + OrderListResponse, + CreateOrderRequest, + CreateOrderResponse, + BaseResponse +} from '@/types/api' + +export class OrderService extends BaseService { + /** + * 获取订单列表 + * @param params 查询参数 + */ + static getOrders(params?: OrderQueryParams): Promise> { + return this.get>('/api/admin/orders', params) + } + + /** + * 获取订单详情 + * @param id 订单ID + */ + static getOrderById(id: number): Promise> { + return this.getOne(`/api/admin/orders/${id}`) + } + + /** + * 创建订单 + * @param data 创建订单请求参数 + */ + static createOrder(data: CreateOrderRequest): Promise> { + return this.post>('/api/admin/orders', data) + } + + /** + * 取消订单 + * @param id 订单ID + */ + static cancelOrder(id: number): Promise { + return this.post(`/api/admin/orders/${id}/cancel`, {}) + } +} diff --git a/src/api/modules/packageManage.ts b/src/api/modules/packageManage.ts new file mode 100644 index 0000000..801198b --- /dev/null +++ b/src/api/modules/packageManage.ts @@ -0,0 +1,91 @@ +/** + * 套餐管理 API 服务 + */ + +import { BaseService } from '../BaseService' +import type { + PackageResponse, + PackageQueryParams, + CreatePackageRequest, + UpdatePackageRequest, + UpdatePackageStatusRequest, + UpdatePackageShelfStatusRequest, + SeriesSelectOption, + BaseResponse, + PaginationResponse, + ListResponse +} from '@/types/api' + +export class PackageManageService extends BaseService { + /** + * 获取套餐分页列表 + * GET /api/admin/packages + * @param params 查询参数 + */ + static getPackages(params?: PackageQueryParams): Promise> { + return this.getPage('/api/admin/packages', params) + } + + /** + * 创建套餐 + * POST /api/admin/packages + * @param data 套餐数据 + */ + static createPackage(data: CreatePackageRequest): Promise> { + return this.create('/api/admin/packages', data) + } + + /** + * 获取套餐详情 + * GET /api/admin/packages/{id} + * @param id 套餐ID + */ + static getPackageDetail(id: number): Promise> { + return this.getOne(`/api/admin/packages/${id}`) + } + + /** + * 更新套餐 + * PUT /api/admin/packages/{id} + * @param id 套餐ID + * @param data 套餐数据 + */ + static updatePackage( + id: number, + data: UpdatePackageRequest + ): Promise> { + return this.update(`/api/admin/packages/${id}`, data) + } + + /** + * 删除套餐 + * DELETE /api/admin/packages/{id} + * @param id 套餐ID + */ + static deletePackage(id: number): Promise { + return this.remove(`/api/admin/packages/${id}`) + } + + /** + * 更新套餐状态 + * PUT /api/admin/packages/{id}/status + * @param id 套餐ID + * @param status 状态 (1:启用, 2:禁用) + */ + static updatePackageStatus(id: number, status: number): Promise { + const data: UpdatePackageStatusRequest = { status } + return this.put(`/api/admin/packages/${id}/status`, data) + } + + /** + * 更新套餐上架状态 + * PATCH /api/admin/packages/{id}/shelf + * @param id 套餐ID + * @param shelf_status 上架状态 (1:上架, 2:下架) + */ + static updatePackageShelfStatus(id: number, shelf_status: number): Promise { + const data: UpdatePackageShelfStatusRequest = { shelf_status } + return this.patch(`/api/admin/packages/${id}/shelf`, data) + } + +} diff --git a/src/api/modules/packageSeries.ts b/src/api/modules/packageSeries.ts new file mode 100644 index 0000000..d9a542d --- /dev/null +++ b/src/api/modules/packageSeries.ts @@ -0,0 +1,83 @@ +/** + * 套餐系列管理 API 服务 + */ + +import { BaseService } from '../BaseService' +import type { + PackageSeriesResponse, + PackageSeriesQueryParams, + CreatePackageSeriesRequest, + UpdatePackageSeriesRequest, + UpdatePackageSeriesStatusRequest, + BaseResponse, + PaginationResponse +} from '@/types/api' + +export class PackageSeriesService extends BaseService { + /** + * 获取套餐系列分页列表 + * GET /api/admin/package-series + * @param params 查询参数 + */ + static getPackageSeries( + params?: PackageSeriesQueryParams + ): Promise> { + return this.getPage('/api/admin/package-series', params) + } + + /** + * 创建套餐系列 + * POST /api/admin/package-series + * @param data 套餐系列数据 + */ + static createPackageSeries( + data: CreatePackageSeriesRequest + ): Promise> { + return this.create('/api/admin/package-series', data) + } + + /** + * 获取套餐系列详情 + * GET /api/admin/package-series/{id} + * @param id 系列ID + */ + static getPackageSeriesDetail(id: number): Promise> { + return this.getOne(`/api/admin/package-series/${id}`) + } + + /** + * 更新套餐系列 + * PUT /api/admin/package-series/{id} + * @param id 系列ID + * @param data 套餐系列数据 + */ + static updatePackageSeries( + id: number, + data: UpdatePackageSeriesRequest + ): Promise> { + return this.update(`/api/admin/package-series/${id}`, data) + } + + /** + * 删除套餐系列 + * DELETE /api/admin/package-series/{id} + * @param id 系列ID + */ + static deletePackageSeries(id: number): Promise { + return this.remove(`/api/admin/package-series/${id}`) + } + + /** + * 更新套餐系列状态 + * PUT /api/admin/package-series/{id}/status + * @param id 系列ID + * @param status 状态 (1:启用, 2:禁用) + */ + static updatePackageSeriesStatus( + id: number, + status: number + ): Promise { + const data: UpdatePackageSeriesStatusRequest = { status } + return this.put(`/api/admin/package-series/${id}/status`, data) + } +} diff --git a/src/api/modules/shopPackageAllocation.ts b/src/api/modules/shopPackageAllocation.ts new file mode 100644 index 0000000..741bb79 --- /dev/null +++ b/src/api/modules/shopPackageAllocation.ts @@ -0,0 +1,109 @@ +/** + * 单套餐分配 API 服务 + */ + +import { BaseService } from '../BaseService' +import type { + ShopPackageAllocationResponse, + ShopPackageAllocationQueryParams, + CreateShopPackageAllocationRequest, + UpdateShopPackageAllocationRequest, + UpdateShopPackageAllocationStatusRequest, + BaseResponse, + PaginationResponse +} from '@/types/api' + +export class ShopPackageAllocationService extends BaseService { + /** + * 获取单套餐分配列表 + * GET /api/admin/shop-package-allocations + * @param params 查询参数 + */ + static getShopPackageAllocations( + params?: ShopPackageAllocationQueryParams + ): Promise> { + return this.getPage( + '/api/admin/shop-package-allocations', + params + ) + } + + /** + * 创建单套餐分配 + * POST /api/admin/shop-package-allocations + * @param data 分配数据 + */ + static createShopPackageAllocation( + data: CreateShopPackageAllocationRequest + ): Promise> { + return this.create( + '/api/admin/shop-package-allocations', + data + ) + } + + /** + * 获取单套餐分配详情 + * GET /api/admin/shop-package-allocations/{id} + * @param id 分配ID + */ + static getShopPackageAllocationDetail( + id: number + ): Promise> { + return this.getOne( + `/api/admin/shop-package-allocations/${id}` + ) + } + + /** + * 更新单套餐分配 + * PUT /api/admin/shop-package-allocations/{id} + * @param id 分配ID + * @param data 分配数据(只允许修改成本价) + */ + static updateShopPackageAllocation( + id: number, + data: UpdateShopPackageAllocationRequest + ): Promise> { + return this.update( + `/api/admin/shop-package-allocations/${id}`, + data + ) + } + + /** + * 删除单套餐分配 + * DELETE /api/admin/shop-package-allocations/{id} + * @param id 分配ID + */ + static deleteShopPackageAllocation(id: number): Promise { + return this.remove(`/api/admin/shop-package-allocations/${id}`) + } + + /** + * 更新单套餐分配成本价 + * PUT /api/admin/shop-package-allocations/{id}/cost-price + * @param id 分配ID + * @param costPrice 成本价(分) + */ + static updateShopPackageAllocationCostPrice( + id: number, + costPrice: number + ): Promise> { + return this.put>( + `/api/admin/shop-package-allocations/${id}/cost-price`, + { cost_price: costPrice } + ) + } + + /** + * 更新单套餐分配状态 + * PUT /api/admin/shop-package-allocations/{id}/status + * @param id 分配ID + * @param status 状态 (1:启用, 2:禁用) + */ + static updateShopPackageAllocationStatus(id: number, status: number): Promise { + const data: UpdateShopPackageAllocationStatusRequest = { status } + return this.put(`/api/admin/shop-package-allocations/${id}/status`, data) + } +} diff --git a/src/api/modules/shopSeriesAllocation.ts b/src/api/modules/shopSeriesAllocation.ts new file mode 100644 index 0000000..af2752a --- /dev/null +++ b/src/api/modules/shopSeriesAllocation.ts @@ -0,0 +1,88 @@ +/** + * 套餐系列分配 API 服务 + */ + +import { BaseService } from '../BaseService' +import type { + ShopSeriesAllocationResponse, + ShopSeriesAllocationQueryParams, + CreateShopSeriesAllocationRequest, + UpdateShopSeriesAllocationRequest, + UpdateShopSeriesAllocationStatusRequest, + BaseResponse, + PaginationResponse +} from '@/types/api' + +export class ShopSeriesAllocationService extends BaseService { + /** + * 获取套餐系列分配分页列表 + * GET /api/admin/shop-series-allocations + * @param params 查询参数 + */ + static getShopSeriesAllocations( + params?: ShopSeriesAllocationQueryParams + ): Promise> { + return this.getPage( + '/api/admin/shop-series-allocations', + params + ) + } + + /** + * 创建套餐系列分配 + * POST /api/admin/shop-series-allocations + * @param data 分配数据 + */ + static createShopSeriesAllocation( + data: CreateShopSeriesAllocationRequest + ): Promise> { + return this.create('/api/admin/shop-series-allocations', data) + } + + /** + * 获取套餐系列分配详情 + * GET /api/admin/shop-series-allocations/{id} + * @param id 分配ID + */ + static getShopSeriesAllocationDetail( + id: number + ): Promise> { + return this.getOne(`/api/admin/shop-series-allocations/${id}`) + } + + /** + * 更新套餐系列分配 + * PUT /api/admin/shop-series-allocations/{id} + * @param id 分配ID + * @param data 分配数据 + */ + static updateShopSeriesAllocation( + id: number, + data: UpdateShopSeriesAllocationRequest + ): Promise> { + return this.update( + `/api/admin/shop-series-allocations/${id}`, + data + ) + } + + /** + * 删除套餐系列分配 + * DELETE /api/admin/shop-series-allocations/{id} + * @param id 分配ID + */ + static deleteShopSeriesAllocation(id: number): Promise { + return this.remove(`/api/admin/shop-series-allocations/${id}`) + } + + /** + * 更新套餐系列分配状态 + * PUT /api/admin/shop-series-allocations/{id}/status + * @param id 分配ID + * @param status 状态 (1:启用, 2:禁用) + */ + static updateShopSeriesAllocationStatus(id: number, status: number): Promise { + const data: UpdateShopSeriesAllocationStatusRequest = { status } + return this.put(`/api/admin/shop-series-allocations/${id}/status`, data) + } +} diff --git a/src/components/core/layouts/art-header-bar/index.vue b/src/components/core/layouts/art-header-bar/index.vue index ff46117..0be6dc2 100644 --- a/src/components/core/layouts/art-header-bar/index.vue +++ b/src/components/core/layouts/art-header-bar/index.vue @@ -139,7 +139,7 @@ {{ getUserAvatar }} - {{ userInfo.username || '用户' }} + {{ userInfo.username || '' }} {{ userInfo.user_type_name || '' }} @@ -243,7 +243,7 @@ */ const getUserAvatar = computed(() => { const username = userInfo.value.username - if (!username) return 'U' + if (!username) return '' const firstChar = username.charAt(0) // 检查是否为中文字符(Unicode 范围:\u4e00-\u9fa5) diff --git a/src/config/constants/index.ts b/src/config/constants/index.ts index 5f305e2..09897d3 100644 --- a/src/config/constants/index.ts +++ b/src/config/constants/index.ts @@ -22,3 +22,6 @@ export * from './iotCard' // 运营商类型相关 export * from './carrierTypes' + +// 套餐管理相关 +export * from './package' diff --git a/src/config/constants/package.ts b/src/config/constants/package.ts new file mode 100644 index 0000000..816e367 --- /dev/null +++ b/src/config/constants/package.ts @@ -0,0 +1,293 @@ +/** + * 套餐管理相关常量配置 + */ + +// ==================== 套餐类型 ==================== + +/** + * 套餐类型枚举 + */ +export enum PackageType { + FORMAL = 'formal', // 正式套餐 + ADDON = 'addon' // 加油包 +} + +/** + * 套餐类型选项 + */ +export const PACKAGE_TYPE_OPTIONS = [ + { + label: '正式套餐', + value: PackageType.FORMAL, + type: 'primary' as const + }, + { + label: '加油包', + value: PackageType.ADDON, + type: 'success' as const + } +] + +/** + * 套餐类型映射 + */ +export const PACKAGE_TYPE_MAP = PACKAGE_TYPE_OPTIONS.reduce( + (map, item) => { + map[item.value] = item + return map + }, + {} as Record +) + +/** + * 获取套餐类型标签 + */ +export function getPackageTypeLabel(type: string): string { + return PACKAGE_TYPE_MAP[type as PackageType]?.label || '未知' +} + +/** + * 获取套餐类型标签类型 + */ +export function getPackageTypeTag(type: string) { + return PACKAGE_TYPE_MAP[type as PackageType]?.type || 'info' +} + +// ==================== 流量类型 ==================== + +/** + * 流量类型枚举 + */ +export enum DataType { + REAL = 'real', // 真实流量 + VIRTUAL = 'virtual' // 虚拟流量 +} + +/** + * 流量类型选项 + */ +export const DATA_TYPE_OPTIONS = [ + { + label: '真实流量', + value: DataType.REAL, + type: 'success' as const + }, + { + label: '虚拟流量', + value: DataType.VIRTUAL, + type: 'warning' as const + } +] + +/** + * 流量类型映射 + */ +export const DATA_TYPE_MAP = DATA_TYPE_OPTIONS.reduce( + (map, item) => { + map[item.value] = item + return map + }, + {} as Record +) + +/** + * 获取流量类型标签 + */ +export function getDataTypeLabel(type: string): string { + return DATA_TYPE_MAP[type as DataType]?.label || '未知' +} + +/** + * 获取流量类型标签类型 + */ +export function getDataTypeTag(type: string) { + return DATA_TYPE_MAP[type as DataType]?.type || 'info' +} + +// ==================== 上架状态 ==================== + +/** + * 上架状态枚举 + */ +export enum ShelfStatus { + ON_SHELF = 1, // 上架 + OFF_SHELF = 2 // 下架 +} + +/** + * 上架状态选项 + */ +export const SHELF_STATUS_OPTIONS = [ + { + label: '上架', + value: ShelfStatus.ON_SHELF, + type: 'success' as const, + text: '上架' + }, + { + label: '下架', + value: ShelfStatus.OFF_SHELF, + type: 'info' as const, + text: '下架' + } +] + +/** + * 上架状态映射 + */ +export const SHELF_STATUS_MAP = SHELF_STATUS_OPTIONS.reduce( + (map, item) => { + map[item.value] = item + return map + }, + {} as Record< + ShelfStatus, + { label: string; value: ShelfStatus; type: 'success' | 'info'; text: string } + > +) + +/** + * 获取上架状态标签 + */ +export function getShelfStatusLabel(status: number): string { + return SHELF_STATUS_MAP[status as ShelfStatus]?.label || '未知' +} + +/** + * 获取上架状态文本 + */ +export function getShelfStatusText(status: number): string { + return SHELF_STATUS_MAP[status as ShelfStatus]?.text || '未知' +} + +/** + * 获取上架状态标签类型 + */ +export function getShelfStatusType(status: number) { + return SHELF_STATUS_MAP[status as ShelfStatus]?.type || 'info' +} + +// ==================== 定价模式 ==================== + +/** + * 定价模式枚举 + */ +export enum PricingMode { + FIXED = 'fixed', // 固定金额 + PERCENT = 'percent' // 百分比 +} + +/** + * 定价模式选项 + */ +export const PRICING_MODE_OPTIONS = [ + { + label: '固定金额', + value: PricingMode.FIXED, + type: 'primary' as const + }, + { + label: '百分比', + value: PricingMode.PERCENT, + type: 'success' as const + } +] + +/** + * 定价模式映射 + */ +export const PRICING_MODE_MAP = PRICING_MODE_OPTIONS.reduce( + (map, item) => { + map[item.value] = item + return map + }, + {} as Record +) + +/** + * 获取定价模式标签 + */ +export function getPricingModeLabel(mode: string): string { + return PRICING_MODE_MAP[mode as PricingMode]?.label || '未知' +} + +/** + * 获取定价模式标签类型 + */ +export function getPricingModeTag(mode: string) { + return PRICING_MODE_MAP[mode as PricingMode]?.type || 'info' +} + +// ==================== 价格来源 ==================== + +/** + * 价格来源枚举 + */ +export enum PriceSource { + SERIES_PRICING = 'series_pricing', // 系列加价 + PACKAGE_OVERRIDE = 'package_override' // 单套餐覆盖 +} + +/** + * 价格来源选项 + */ +export const PRICE_SOURCE_OPTIONS = [ + { + label: '系列加价', + value: PriceSource.SERIES_PRICING, + type: 'primary' as const + }, + { + label: '单套餐覆盖', + value: PriceSource.PACKAGE_OVERRIDE, + type: 'warning' as const + } +] + +/** + * 价格来源映射 + */ +export const PRICE_SOURCE_MAP = PRICE_SOURCE_OPTIONS.reduce( + (map, item) => { + map[item.value] = item + return map + }, + {} as Record< + PriceSource, + { label: string; value: PriceSource; type: 'primary' | 'warning' } + > +) + +/** + * 获取价格来源标签 + */ +export function getPriceSourceLabel(source: string): string { + return PRICE_SOURCE_MAP[source as PriceSource]?.label || '未知' +} + +/** + * 获取价格来源标签类型 + */ +export function getPriceSourceTag(source: string) { + return PRICE_SOURCE_MAP[source as PriceSource]?.type || 'info' +} + +// ==================== 状态值映射工具 ==================== + +/** + * 前端状态值 (CommonStatus: 0/1) 转换为后端 API 状态值 (1/2) + * @param frontendStatus 前端状态值 (0:禁用, 1:启用) + * @returns 后端状态值 (1:启用, 2:禁用) + */ +export function frontendStatusToApi(frontendStatus: number): number { + return frontendStatus === 1 ? 1 : 2 +} + +/** + * 后端 API 状态值 (1/2) 转换为前端状态值 (CommonStatus: 0/1) + * @param apiStatus 后端状态值 (1:启用, 2:禁用) + * @returns 前端状态值 (0:禁用, 1:启用) + */ +export function apiStatusToFrontend(apiStatus: number): number { + return apiStatus === 1 ? 1 : 0 +} diff --git a/src/locales/langs/en.json b/src/locales/langs/en.json index 3574612..47eef9b 100644 --- a/src/locales/langs/en.json +++ b/src/locales/langs/en.json @@ -392,6 +392,7 @@ "packageList": "My Packages", "packageChange": "Package Change", "packageAssign": "Package Assignment", + "seriesAssign": "Series Assignment", "packageSeries": "Package Series", "packageCommission": "Package Commission Cards" }, @@ -414,7 +415,8 @@ "withdrawal": "Commission Withdrawal", "withdrawalSettings": "Withdrawal Settings", "myAccount": "My Account", - "carrierManagement": "Carrier Management" + "carrierManagement": "Carrier Management", + "orders": "Order Management" }, "deviceManagement": { "title": "Device Management", @@ -443,7 +445,8 @@ "allocationRecordDetail": "Allocation Record Details", "cardReplacementRequest": "Card Replacement Request", "authorizationRecords": "Authorization Records", - "authorizationDetail": "Authorization Details" + "authorizationDetail": "Authorization Details", + "enterpriseDevices": "Enterprise Devices" }, "settings": { "title": "Settings Management", @@ -741,5 +744,206 @@ "pendingAmount": "Pending Amount", "todayCommission": "Today's Commission" } + }, + "seriesBinding": { + "buttons": { + "batchSetSeries": "Batch Set Series", + "clearBinding": "Clear Binding" + }, + "dialog": { + "title": "Batch Set Series Binding", + "titleCard": "Batch Set Card Series Binding", + "titleDevice": "Batch Set Device Series Binding" + }, + "form": { + "seriesAllocation": "Series Allocation", + "seriesAllocationPlaceholder": "Please select series allocation", + "clearBindingOption": "Clear Association", + "selectedCount": "{count} items selected" + }, + "messages": { + "noSelection": "Please select items to set", + "setSuccess": "Series binding set successfully", + "setFailed": "Failed to set series binding", + "partialSuccess": "Partial success: {success} succeeded, {fail} failed", + "confirmSet": "Are you sure to set series binding for {count} selected items?", + "confirmClear": "Are you sure to clear series binding for {count} selected items?" + }, + "result": { + "title": "Set Result", + "successCount": "Success Count", + "failCount": "Fail Count", + "failedItems": "Failed Items", + "iccid": "ICCID", + "deviceId": "Device ID", + "deviceNo": "Device No.", + "reason": "Reason" + } + }, + "orderManagement": { + "title": "Order Management", + "orderList": "Order List", + "orderDetail": "Order Details", + "orderItems": "Order Items", + "createOrder": "Create Order", + "searchForm": { + "orderNo": "Order No.", + "orderNoPlaceholder": "Please enter order number", + "paymentStatus": "Payment Status", + "paymentStatusPlaceholder": "Please select payment status", + "orderType": "Order Type", + "orderTypePlaceholder": "Please select order type", + "dateRange": "Created Time", + "startDate": "Start Time", + "endDate": "End Time" + }, + "table": { + "id": "Order ID", + "orderNo": "Order No.", + "orderType": "Order Type", + "buyerId": "Buyer ID", + "buyerType": "Buyer Type", + "paymentStatus": "Payment Status", + "paymentMethod": "Payment Method", + "totalAmount": "Total Amount", + "paidAt": "Paid At", + "commissionStatus": "Commission Status", + "createdAt": "Created At", + "updatedAt": "Updated At", + "operation": "Actions" + }, + "orderType": { + "singleCard": "Single Card Purchase", + "device": "Device Purchase" + }, + "buyerType": { + "personal": "Personal Customer", + "agent": "Agent" + }, + "paymentStatus": { + "pending": "Pending", + "paid": "Paid", + "cancelled": "Cancelled", + "refunded": "Refunded" + }, + "paymentMethod": { + "wallet": "Wallet Payment", + "wechat": "WeChat Payment", + "alipay": "Alipay Payment" + }, + "commissionStatus": { + "notApplicable": "Not Applicable", + "pending": "Pending", + "settled": "Settled" + }, + "actions": { + "viewDetail": "View Details", + "cancel": "Cancel", + "confirm": "Confirm", + "submit": "Submit", + "close": "Close" + }, + "createForm": { + "packageIds": "Select Packages", + "packageIdsPlaceholder": "Please select packages", + "iotCardId": "IoT Card ID", + "iotCardIdPlaceholder": "Please enter IoT card ID", + "deviceId": "Device ID", + "deviceIdPlaceholder": "Please enter device ID" + }, + "items": { + "packageName": "Package Name", + "quantity": "Quantity", + "unitPrice": "Unit Price", + "amount": "Subtotal" + }, + "messages": { + "createSuccess": "Order created successfully", + "createFailed": "Failed to create order", + "cancelSuccess": "Order cancelled successfully", + "cancelFailed": "Failed to cancel order", + "cancelConfirm": "Cancel Order Confirmation", + "cancelConfirmText": "Are you sure to cancel this order? This action cannot be undone", + "cannotCancelPaid": "Cannot cancel a paid order", + "cannotCancelCancelled": "Order is already cancelled", + "cannotCancelRefunded": "Cannot cancel a refunded order", + "loadFailed": "Failed to load order data", + "noData": "No order data available" + }, + "validation": { + "orderTypeRequired": "Please select order type", + "packageIdsRequired": "Please select at least one package", + "iotCardRequired": "Please select IoT card", + "deviceRequired": "Please select device", + "packagesMaxLimit": "Can select up to 10 packages" + } + }, + "enterpriseDevices": { + "title": "Enterprise Device List", + "searchForm": { + "enterpriseId": "Enterprise ID", + "enterpriseIdPlaceholder": "Please enter enterprise ID", + "deviceNo": "Device No.", + "deviceNoPlaceholder": "Please enter device number" + }, + "table": { + "deviceId": "Device ID", + "deviceNo": "Device No.", + "deviceName": "Device Name", + "deviceModel": "Device Model", + "cardCount": "Card Count", + "authorizedAt": "Authorized At", + "operation": "Actions" + }, + "buttons": { + "allocateDevices": "Allocate Devices", + "recallDevices": "Recall Authorization", + "refresh": "Refresh" + }, + "dialog": { + "allocateTitle": "Allocate Devices to Enterprise", + "recallTitle": "Recall Device Authorization", + "resultTitle": "Operation Result" + }, + "form": { + "deviceNos": "Device Numbers", + "deviceNosPlaceholder": "Enter device numbers, separated by newlines or commas", + "deviceNosHint": "One device number per line or comma-separated, maximum 100 devices", + "remark": "Remark", + "remarkPlaceholder": "Enter allocation remark (optional)", + "selectedDevices": "Selected Devices", + "selectedCount": "{count} devices selected" + }, + "result": { + "successCount": "Success Count", + "failCount": "Fail Count", + "authorizedDevices": "Authorized Devices", + "failedItems": "Failed Items", + "deviceNo": "Device No.", + "deviceId": "Device ID", + "cardCount": "Card Count", + "reason": "Failure Reason" + }, + "messages": { + "allocateSuccess": "Device allocation successful", + "allocateFailed": "Device allocation failed", + "allocatePartialSuccess": "Partial success: {success} succeeded, {fail} failed", + "recallSuccess": "Authorization recall successful", + "recallFailed": "Authorization recall failed", + "recallPartialSuccess": "Partial recall success: {success} succeeded, {fail} failed", + "recallConfirm": "Recall Authorization Confirmation", + "recallConfirmText": "Are you sure to recall authorization for {count} selected devices?", + "noSelection": "Please select devices to recall first", + "deviceNosRequired": "Please enter device numbers", + "deviceNosEmpty": "Device number list cannot be empty", + "deviceNosMaxLimit": "Cannot exceed 100 device numbers", + "invalidDeviceNos": "Invalid device number format exists", + "loadFailed": "Failed to load device list", + "noData": "No device data available" + }, + "validation": { + "deviceNosRequired": "Please enter device number list", + "deviceNosMaxLength": "Cannot exceed 100 device numbers" + } } } diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json index 015892b..6430cac 100644 --- a/src/locales/langs/zh.json +++ b/src/locales/langs/zh.json @@ -398,13 +398,15 @@ "cardChange": "换卡网卡" }, "packageManagement": { - "title": "我的套餐", + "title": "套餐管理", "packageCreate": "新建套餐", "packageBatch": "批量管理", - "packageList": "我的套餐", + "packageList": "套餐管理", "packageChange": "套餐变更", - "packageAssign": "套餐分配", + "packageAssign": "单套餐分配", + "seriesAssign": "套餐系列分配", "packageSeries": "套餐系列", + "myPackages": "代理可售套餐", "packageCommission": "套餐佣金网卡" }, "accountManagement": { @@ -447,13 +449,15 @@ "allocationRecordDetail": "分配记录详情", "cardReplacementRequest": "换卡申请", "authorizationRecords": "授权记录", - "authorizationDetail": "授权记录详情" + "authorizationDetail": "授权记录详情", + "enterpriseDevices": "企业设备列表" }, "account": { "title": "账户管理", "customerAccount": "客户账号", "myAccount": "我的账户", - "carrierManagement": "运营商管理" + "carrierManagement": "运营商管理", + "orders": "订单管理" }, "commission": { "menu": { @@ -751,5 +755,206 @@ "pendingAmount": "待审核金额", "todayCommission": "今日佣金" } + }, + "seriesBinding": { + "buttons": { + "batchSetSeries": "批量设置套餐系列", + "clearBinding": "清除绑定" + }, + "dialog": { + "title": "批量设置套餐系列绑定", + "titleCard": "批量设置卡的套餐系列绑定", + "titleDevice": "批量设置设备的套餐系列绑定" + }, + "form": { + "seriesAllocation": "套餐系列分配", + "seriesAllocationPlaceholder": "请选择套餐系列分配", + "clearBindingOption": "清除关联", + "selectedCount": "已选择 {count} 项" + }, + "messages": { + "noSelection": "请先选择要设置的项", + "setSuccess": "套餐系列绑定设置成功", + "setFailed": "套餐系列绑定设置失败", + "partialSuccess": "部分设置成功:成功 {success} 项,失败 {fail} 项", + "confirmSet": "确定要为选中的 {count} 项设置套餐系列绑定吗?", + "confirmClear": "确定要清除选中的 {count} 项的套餐系列绑定吗?" + }, + "result": { + "title": "设置结果", + "successCount": "成功数量", + "failCount": "失败数量", + "failedItems": "失败项详情", + "iccid": "ICCID", + "deviceId": "设备ID", + "deviceNo": "设备号", + "reason": "失败原因" + } + }, + "orderManagement": { + "title": "订单管理", + "orderList": "订单列表", + "orderDetail": "订单详情", + "orderItems": "订单明细", + "createOrder": "创建订单", + "searchForm": { + "orderNo": "订单号", + "orderNoPlaceholder": "请输入订单号", + "paymentStatus": "支付状态", + "paymentStatusPlaceholder": "请选择支付状态", + "orderType": "订单类型", + "orderTypePlaceholder": "请选择订单类型", + "dateRange": "创建时间", + "startDate": "开始时间", + "endDate": "结束时间" + }, + "table": { + "id": "订单ID", + "orderNo": "订单号", + "orderType": "订单类型", + "buyerId": "买家ID", + "buyerType": "买家类型", + "paymentStatus": "支付状态", + "paymentMethod": "支付方式", + "totalAmount": "订单金额", + "paidAt": "支付时间", + "commissionStatus": "佣金状态", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "operation": "操作" + }, + "orderType": { + "singleCard": "单卡购买", + "device": "设备购买" + }, + "buyerType": { + "personal": "个人客户", + "agent": "代理商" + }, + "paymentStatus": { + "pending": "待支付", + "paid": "已支付", + "cancelled": "已取消", + "refunded": "已退款" + }, + "paymentMethod": { + "wallet": "钱包支付", + "wechat": "微信支付", + "alipay": "支付宝支付" + }, + "commissionStatus": { + "notApplicable": "不适用", + "pending": "待结算", + "settled": "已结算" + }, + "actions": { + "viewDetail": "查看详情", + "cancel": "取消", + "confirm": "确定", + "submit": "提交", + "close": "关闭" + }, + "createForm": { + "packageIds": "选择套餐", + "packageIdsPlaceholder": "请选择套餐", + "iotCardId": "IoT卡ID", + "iotCardIdPlaceholder": "请输入IoT卡ID", + "deviceId": "设备ID", + "deviceIdPlaceholder": "请输入设备ID" + }, + "items": { + "packageName": "套餐名称", + "quantity": "数量", + "unitPrice": "单价", + "amount": "小计" + }, + "messages": { + "createSuccess": "订单创建成功", + "createFailed": "订单创建失败", + "cancelSuccess": "订单取消成功", + "cancelFailed": "订单取消失败", + "cancelConfirm": "取消订单确认", + "cancelConfirmText": "确定要取消该订单吗?取消后不可恢复", + "cannotCancelPaid": "已支付的订单无法取消", + "cannotCancelCancelled": "订单已取消", + "cannotCancelRefunded": "已退款的订单无法取消", + "loadFailed": "加载订单数据失败", + "noData": "暂无订单数据" + }, + "validation": { + "orderTypeRequired": "请选择订单类型", + "packageIdsRequired": "请至少选择一个套餐", + "iotCardRequired": "请选择IoT卡", + "deviceRequired": "请选择设备", + "packagesMaxLimit": "最多只能选择10个套餐" + } + }, + "enterpriseDevices": { + "title": "企业设备列表", + "searchForm": { + "enterpriseId": "企业ID", + "enterpriseIdPlaceholder": "请输入企业ID", + "deviceNo": "设备号", + "deviceNoPlaceholder": "请输入设备号" + }, + "table": { + "deviceId": "设备ID", + "deviceNo": "设备号", + "deviceName": "设备名称", + "deviceModel": "设备型号", + "cardCount": "绑定卡数量", + "authorizedAt": "授权时间", + "operation": "操作" + }, + "buttons": { + "allocateDevices": "授权设备", + "recallDevices": "撤销授权", + "refresh": "刷新" + }, + "dialog": { + "allocateTitle": "授权设备给企业", + "recallTitle": "撤销设备授权", + "resultTitle": "操作结果" + }, + "form": { + "deviceNos": "设备号列表", + "deviceNosPlaceholder": "请输入设备号,支持换行或逗号分隔", + "deviceNosHint": "每行一个设备号或使用逗号分隔,最多100个", + "remark": "备注", + "remarkPlaceholder": "请输入授权备注(可选)", + "selectedDevices": "已选择设备", + "selectedCount": "已选择 {count} 个设备" + }, + "result": { + "successCount": "成功数量", + "failCount": "失败数量", + "authorizedDevices": "已授权设备", + "failedItems": "失败项", + "deviceNo": "设备号", + "deviceId": "设备ID", + "cardCount": "绑定卡数", + "reason": "失败原因" + }, + "messages": { + "allocateSuccess": "设备授权成功", + "allocateFailed": "设备授权失败", + "allocatePartialSuccess": "部分设备授权成功:成功 {success} 个,失败 {fail} 个", + "recallSuccess": "撤销授权成功", + "recallFailed": "撤销授权失败", + "recallPartialSuccess": "部分设备撤销成功:成功 {success} 个,失败 {fail} 个", + "recallConfirm": "撤销授权确认", + "recallConfirmText": "确定要撤销选中的 {count} 个设备的授权吗?", + "noSelection": "请先选择要撤销的设备", + "deviceNosRequired": "请输入设备号", + "deviceNosEmpty": "设备号列表不能为空", + "deviceNosMaxLimit": "设备号数量不能超过100个", + "invalidDeviceNos": "存在无效的设备号格式", + "loadFailed": "加载设备列表失败", + "noData": "暂无设备数据" + }, + "validation": { + "deviceNosRequired": "请输入设备号列表", + "deviceNosMaxLength": "设备号数量不能超过100个" + } } } diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index cbfd9d7..0b28ada 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -694,6 +694,62 @@ export const asyncRoutes: AppRouteRecord[] = [ } ] }, + { + path: '/package-management', + name: 'PackageManagement', + component: RoutesAlias.Home, + meta: { + title: 'menus.packageManagement.title', + icon: '' + }, + children: [ + { + path: 'package-series', + name: 'PackageSeries', + component: RoutesAlias.PackageSeries, + meta: { + title: 'menus.packageManagement.packageSeries', + keepAlive: true + } + }, + { + path: 'package-list', + name: 'PackageList', + component: RoutesAlias.PackageList, + meta: { + title: 'menus.packageManagement.packageList', + keepAlive: true + } + }, + { + path: 'my-packages', + name: 'MyPackages', + component: RoutesAlias.MyPackages, + meta: { + title: 'menus.packageManagement.myPackages', + keepAlive: true + } + }, + { + path: 'package-assign', + name: 'PackageAssign', + component: RoutesAlias.PackageAssign, + meta: { + title: 'menus.packageManagement.packageAssign', + keepAlive: true + } + }, + { + path: 'series-assign', + name: 'SeriesAssign', + component: RoutesAlias.SeriesAssign, + meta: { + title: 'menus.packageManagement.seriesAssign', + keepAlive: true + } + } + ] + }, { path: '/account-management', name: 'AccountManagement', @@ -954,6 +1010,16 @@ export const asyncRoutes: AppRouteRecord[] = [ isHide: true, keepAlive: false } + }, + { + path: 'enterprise-devices', + name: 'EnterpriseDevices', + component: RoutesAlias.EnterpriseDevices, + meta: { + title: 'menus.assetManagement.enterpriseDevices', + isHide: true, + keepAlive: false + } } ] }, @@ -993,6 +1059,15 @@ export const asyncRoutes: AppRouteRecord[] = [ keepAlive: true } }, + { + path: 'orders', + name: 'OrderManagement', + component: RoutesAlias.OrderList, + meta: { + title: 'menus.account.orders', + keepAlive: true + } + }, // { // path: 'my-account', // name: 'MyAccount', diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index 90d4c4d..9c69d28 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -67,10 +67,12 @@ export enum RoutesAlias { // 我的套餐 PackageCreate = '/package-management/package-create', // 新建套餐 PackageBatch = '/package-management/package-batch', // 批量管理 - PackageList = '/package-management/package-list', // 我的套餐 + PackageList = '/package-management/package-list', // 套餐管理 PackageChange = '/package-management/package-change', // 套餐变更 - PackageAssign = '/package-management/package-assign', // 套餐分配 + PackageAssign = '/package-management/package-assign', // 单套餐分配 + SeriesAssign = '/package-management/series-assign', // 套餐系列分配 PackageSeries = '/package-management/package-series', // 套餐系列 + MyPackages = '/package-management/my-packages', // 代理可售套餐 PackageCommission = '/package-management/package-commission', // 套餐佣金网卡 // 账号管理 @@ -102,11 +104,13 @@ export enum RoutesAlias { CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请 AuthorizationRecords = '/asset-management/authorization-records', // 授权记录 AuthorizationDetail = '/asset-management/authorization-detail', // 授权记录详情 + EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表 // 账户管理 CustomerAccountList = '/finance/customer-account', // 客户账号 MyAccount = '/finance/my-account', // 我的账户 CarrierManagement = '/finance/carrier-management', // 运营商管理 + OrderList = '/order-management/order-list', // 订单管理 // 佣金管理 WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批 diff --git a/src/types/api/card.ts b/src/types/api/card.ts index 0b71989..40acc9b 100644 --- a/src/types/api/card.ts +++ b/src/types/api/card.ts @@ -478,3 +478,24 @@ export interface IotCardDetailResponse { created_at: string // 创建时间 updated_at: string // 更新时间 } + +// ========== 批量设置卡的套餐系列绑定相关 ========== + +// 批量设置卡的套餐系列绑定请求参数 +export interface BatchSetCardSeriesBindingRequest { + iccids: string[] // ICCID列表(最多500个) + series_allocation_id: number // 套餐系列分配ID(0表示清除关联) +} + +// 卡套餐系列绑定失败项 +export interface CardSeriesBindingFailedItem { + iccid: string // ICCID + reason: string // 失败原因 +} + +// 批量设置卡的套餐系列绑定响应 +export interface BatchSetCardSeriesBindingResponse { + success_count: number // 成功数量 + fail_count: number // 失败数量 + failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表 +} diff --git a/src/types/api/commission.ts b/src/types/api/commission.ts index 5c7720e..0153ec4 100644 --- a/src/types/api/commission.ts +++ b/src/types/api/commission.ts @@ -119,7 +119,6 @@ export interface CreateWithdrawalSettingParams { min_withdrawal_amount: number // 最低提现金额(分) fee_rate: number // 手续费比率(基点,100=1%) daily_withdrawal_limit: number // 每日提现次数限制 - arrival_days: number // 到账天数 } // ==================== 佣金记录相关 ==================== @@ -248,3 +247,50 @@ export interface ShopCommissionSummaryPageResult { size: number total: number } + +// ==================== 我的佣金统计相关 ==================== + +/** + * 我的佣金统计查询参数 + */ +export interface MyCommissionStatsQueryParams { + shop_id?: number // 店铺ID + start_time?: string // 开始时间 + end_time?: string // 结束时间 +} + +/** + * 我的佣金统计响应 + */ +export interface MyCommissionStatsResponse { + cost_diff_amount: number // 成本价差收入(分) + cost_diff_count: number // 成本价差笔数 + cost_diff_percent: number // 成本价差占比(千分比) + one_time_amount: number // 一次性佣金收入(分) + one_time_count: number // 一次性佣金笔数 + one_time_percent: number // 一次性佣金占比(千分比) + tier_bonus_amount: number // 梯度奖励收入(分) + tier_bonus_count: number // 梯度奖励笔数 + tier_bonus_percent: number // 梯度奖励占比(千分比) + total_amount: number // 总收入(分) + total_count: number // 总笔数 +} + +/** + * 我的每日佣金统计查询参数 + */ +export interface MyDailyCommissionStatsQueryParams { + shop_id?: number // 店铺ID + start_date?: string // 开始日期(YYYY-MM-DD) + end_date?: string // 结束日期(YYYY-MM-DD) + days?: number // 查询天数(默认30天,最大365天) +} + +/** + * 每日佣金统计项 + */ +export interface DailyCommissionStatsItem { + date: string // 日期(YYYY-MM-DD) + total_amount: number // 当日总收入(分) + total_count: number // 当日总笔数 +} diff --git a/src/types/api/common.ts b/src/types/api/common.ts index 7199c58..4d55699 100644 --- a/src/types/api/common.ts +++ b/src/types/api/common.ts @@ -29,11 +29,25 @@ export interface PaginationData { pages?: number } +// 新版分页响应数据(使用 list 字段) +export interface PaginationDataV2 { + list: T[] // 新版API使用 list 字段 + total: number + page_size: number + page: number + total_pages: number // 总页数 +} + // 分页响应 export interface PaginationResponse extends BaseResponse { data: PaginationData } +// 新版分页响应(使用 list 字段) +export interface PaginationResponseV2 extends BaseResponse { + data: PaginationDataV2 +} + // 列表响应 export interface ListResponse extends BaseResponse { data: T[] diff --git a/src/types/api/device.ts b/src/types/api/device.ts index c0699ad..1e9b411 100644 --- a/src/types/api/device.ts +++ b/src/types/api/device.ts @@ -202,3 +202,25 @@ export interface DeviceImportTaskDetail extends DeviceImportTask { failed_items: DeviceImportResultItem[] | null // 失败记录详情 skipped_items: DeviceImportResultItem[] | null // 跳过记录详情 } + +// ========== 批量设置设备的套餐系列绑定相关 ========== + +// 批量设置设备的套餐系列绑定请求参数 +export interface BatchSetDeviceSeriesBindingRequest { + device_ids: number[] // 设备ID列表(最多500个) + series_allocation_id: number // 套餐系列分配ID(0表示清除关联) +} + +// 设备套餐系列绑定失败项 +export interface DeviceSeriesBindingFailedItem { + device_id: number // 设备ID + device_no: string // 设备号 + reason: string // 失败原因 +} + +// 批量设置设备的套餐系列绑定响应 +export interface BatchSetDeviceSeriesBindingResponse { + success_count: number // 成功数量 + fail_count: number // 失败数量 + failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表 +} diff --git a/src/types/api/enterpriseDevice.ts b/src/types/api/enterpriseDevice.ts new file mode 100644 index 0000000..3cf6902 --- /dev/null +++ b/src/types/api/enterpriseDevice.ts @@ -0,0 +1,89 @@ +/** + * 企业设备授权相关类型定义 + */ + +import { PaginationParams } from '@/types' + +// ==================== 企业设备列表相关 ==================== + +/** + * 企业设备列表项 + */ +export interface EnterpriseDeviceItem { + device_id: number // 设备ID + device_no: string // 设备号 + device_name: string // 设备名称 + device_model: string // 设备型号 + card_count: number // 绑定卡数量 + authorized_at: string // 授权时间 +} + +/** + * 企业设备列表查询参数 + */ +export interface EnterpriseDeviceListParams extends PaginationParams { + device_no?: string // 设备号(模糊搜索) +} + +/** + * 企业设备列表分页结果 + */ +export interface EnterpriseDevicePageResult { + list: EnterpriseDeviceItem[] | null // 设备列表 + total: number // 总数 +} + +// ==================== 授权设备相关 ==================== + +/** + * 授权设备请求参数 + */ +export interface AllocateDevicesRequest { + device_nos: string[] | null // 设备号列表(最多100个) + remark?: string // 授权备注 +} + +/** + * 已授权设备项 + */ +export interface AuthorizedDeviceItem { + device_id: number // 设备ID + device_no: string // 设备号 + card_count: number // 绑定卡数量 +} + +/** + * 失败设备项 + */ +export interface FailedDeviceItem { + device_no: string // 设备号 + reason: string // 失败原因 +} + +/** + * 授权设备响应 + */ +export interface AllocateDevicesResponse { + success_count: number // 成功数量 + fail_count: number // 失败数量 + authorized_devices: AuthorizedDeviceItem[] | null // 已授权设备列表 + failed_items: FailedDeviceItem[] | null // 失败项列表 +} + +// ==================== 撤销授权相关 ==================== + +/** + * 撤销设备授权请求参数 + */ +export interface RecallDevicesRequest { + device_nos: string[] | null // 设备号列表(最多100个) +} + +/** + * 撤销设备授权响应 + */ +export interface RecallDevicesResponse { + success_count: number // 成功数量 + fail_count: number // 失败数量 + failed_items: FailedDeviceItem[] | null // 失败项列表 +} diff --git a/src/types/api/index.ts b/src/types/api/index.ts index 28fc6b3..f67cbf1 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -29,8 +29,23 @@ export * from './shop' // 网卡相关 export * from './card' -// 套餐相关 -export * from './package' +// 套餐相关(旧)- 为避免冲突,保留旧类型但使用别名 +export type { + PackageStatus, + PackageType as OldPackageType, + PackageSeries as OldPackageSeries, + Package, + PackageQueryParams as OldPackageQueryParams, + PackageSeriesQueryParams as OldPackageSeriesQueryParams, + PackageSeriesFormData, + PackageFormData, + PackageAssignParams, + PackageChangeRecord, + PackageAssignRecord +} from './package' + +// 套餐管理系统相关(新) +export * from './packageManagement' // 设备相关 export * from './device' @@ -53,5 +68,11 @@ export * from './authorization' // 企业卡授权相关 export * from './enterpriseCard' +// 企业设备授权相关 +export * from './enterpriseDevice' + // 运营商相关 export * from './carrier' + +// 订单相关 +export * from './order' diff --git a/src/types/api/order.ts b/src/types/api/order.ts new file mode 100644 index 0000000..1ce0d93 --- /dev/null +++ b/src/types/api/order.ts @@ -0,0 +1,90 @@ +/** + * 订单管理相关类型定义 + */ + +// 支付状态枚举 +export enum PaymentStatus { + PENDING = 1, // 待支付 + PAID = 2, // 已支付 + CANCELLED = 3, // 已取消 + REFUNDED = 4 // 已退款 +} + +// 订单类型 +export type OrderType = 'single_card' | 'device' + +// 买家类型 +export type BuyerType = 'personal' | 'agent' + +// 订单支付方式 +export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay' + +// 订单佣金状态 +export enum OrderCommissionStatus { + NOT_APPLICABLE = 0, // 不适用 + PENDING = 1, // 待结算 + SETTLED = 2 // 已结算 +} + +// 订单明细 +export interface OrderItem { + id: number + package_id: number + package_name: string + quantity: number + unit_price: number // 单价(分) + amount: number // 小计金额(分) +} + +// 订单响应 +export interface Order { + id: number + order_no: string + order_type: OrderType + buyer_id: number + buyer_type: BuyerType + payment_status: PaymentStatus + payment_status_text: string + payment_method: OrderPaymentMethod + total_amount: number // 订单总金额(分) + paid_at: string | null + commission_status: OrderCommissionStatus + commission_config_version: number + device_id: number | null + iot_card_id: number | null + items: OrderItem[] | null + created_at: string + updated_at: string +} + +// 订单查询参数 +export interface OrderQueryParams { + page?: number + page_size?: number + payment_status?: PaymentStatus + order_type?: OrderType + order_no?: string + start_time?: string + end_time?: string + dateRange?: string[] | any // For date range picker in UI +} + +// 订单列表响应 +export interface OrderListResponse { + items: Order[] + page: number + page_size: number + total: number + total_pages: number +} + +// 创建订单请求 +export interface CreateOrderRequest { + order_type: OrderType + package_ids: number[] + iot_card_id?: number | null + device_id?: number | null +} + +// 创建订单响应 (返回订单详情) +export type CreateOrderResponse = Order diff --git a/src/types/api/packageManagement.ts b/src/types/api/packageManagement.ts new file mode 100644 index 0000000..8d862ac --- /dev/null +++ b/src/types/api/packageManagement.ts @@ -0,0 +1,332 @@ +/** + * 套餐管理系统类型定义 + * 使用下划线命名与后端 API 保持一致 + */ + +import { PaginationParams } from './common' + +// ==================== 套餐系列管理 ==================== + +/** + * 套餐系列响应 + */ +export interface PackageSeriesResponse { + id: number + series_code: string + series_name: string + description?: string + status: number // 1:启用, 2:禁用 + created_at: string + updated_at: string +} + +/** + * 套餐系列查询参数 + */ +export interface PackageSeriesQueryParams extends PaginationParams { + series_name?: string // 系列名称(模糊搜索) + status?: number // 状态筛选 +} + +/** + * 创建套餐系列请求 + */ +export interface CreatePackageSeriesRequest { + series_code: string // 系列编码,必填 + series_name: string // 系列名称,必填 + description?: string // 描述,可选 +} + +/** + * 更新套餐系列请求 + */ +export interface UpdatePackageSeriesRequest { + series_code?: string + series_name?: string + description?: string +} + +/** + * 更新套餐系列状态请求 + */ +export interface UpdatePackageSeriesStatusRequest { + status: number // 1:启用, 2:禁用 +} + +// ==================== 套餐管理 ==================== + +/** + * 套餐响应 + */ +export interface PackageResponse { + id: number + package_code: string + package_name: string + series_id: number + series_name?: string + package_type: string // 'formal':正式套餐, 'addon':加油包 + data_type: string // 'real':真实流量, 'virtual':虚拟流量 + real_data_mb: number // 真流量额度(MB) + virtual_data_mb: number // 虚流量额度(MB) + duration_months: number // 有效期(月) + price: number // 价格(分) + shelf_status: number // 上架状态 (1:上架, 2:下架) + status: number // 状态 (1:启用, 2:禁用) + description?: string + created_at: string + updated_at: string +} + +/** + * 套餐查询参数 + */ +export interface PackageQueryParams extends PaginationParams { + package_name?: string // 套餐名称(模糊搜索) + series_id?: number // 系列ID + package_type?: string // 套餐类型 + data_type?: string // 流量类型 + shelf_status?: number // 上架状态 + status?: number // 状态 +} + +/** + * 创建套餐请求 + */ +export interface CreatePackageRequest { + package_code: string // 套餐编码,必填 + package_name: string // 套餐名称,必填 + series_id: number // 所属系列ID,必填 + package_type: string // 套餐类型,必填 + data_type: string // 流量类型,必填 + real_data_mb: number // 真流量额度(MB),必填 + virtual_data_mb: number // 虚流量额度(MB),必填 + duration_months: number // 有效期(月),必填 + price: number // 价格(分),必填 + description?: string // 描述,可选 +} + +/** + * 更新套餐请求 + */ +export interface UpdatePackageRequest { + package_code?: string + package_name?: string + series_id?: number + package_type?: string + data_type?: string + real_data_mb?: number + virtual_data_mb?: number + duration_months?: number + price?: number + description?: string +} + +/** + * 更新套餐状态请求 + */ +export interface UpdatePackageStatusRequest { + status: number // 1:启用, 2:禁用 +} + +/** + * 更新套餐上架状态请求 + */ +export interface UpdatePackageShelfStatusRequest { + shelf_status: number // 1:上架, 2:下架 +} + +/** + * 系列下拉选项响应 + */ +export interface SeriesSelectOption { + id: number + series_name: string + series_code: string +} + +// ==================== 代理可售套餐 ==================== + +/** + * 我的可售套餐响应 + */ +export interface MyPackageResponse { + id: number + package_code: string + package_name: string + series_id: number + series_name: string + package_type: string + data_type: string + real_data_mb: number // 真流量额度(MB) + virtual_data_mb: number // 虚流量额度(MB) + duration_months: number + price: number // 套餐原价(分) + cost_price: number // 成本价(分) + suggested_retail_price: number // 建议售价(分) + profit_margin: number // 利润空间(分) + price_source: string // 价格来源:'series_pricing':系列加价, 'package_override':单套餐覆盖 + shelf_status: number + status: number + description?: string + created_at: string + updated_at: string +} + +/** + * 我的可售套餐查询参数 + */ +export interface MyPackageQueryParams extends PaginationParams { + series_id?: number // 系列ID筛选 + package_type?: string // 套餐类型筛选 +} + +/** + * 我的被分配系列响应 + */ +export interface MySeriesAllocationResponse { + id: number // 分配ID + series_id: number + series_code: string + series_name: string + pricing_mode: string // 定价模式:'fixed':固定金额, 'percent':百分比 + pricing_value: number // 定价值 + allocator_shop_name: string // 分配者店铺名称 + package_count: number // 可售套餐数量 + status: number + created_at: string +} + +// ==================== 单套餐分配 ==================== + +/** + * 单套餐分配响应 + */ +export interface ShopPackageAllocationResponse { + id: number + package_id: number + package_code: string + package_name: string + shop_id: number + shop_name: string + cost_price: number // 覆盖的成本价(分) + calculated_cost_price: number // 原计算成本价(分,供参考) + allocation_id?: number // 关联的系列分配ID + status: number // 1:启用, 2:禁用 + created_at: string + updated_at: string +} + +/** + * 单套餐分配查询参数 + */ +export interface ShopPackageAllocationQueryParams extends PaginationParams { + shop_id?: number // 店铺ID筛选 + package_id?: number // 套餐ID筛选 + status?: number // 状态筛选 +} + +/** + * 创建单套餐分配请求 + */ +export interface CreateShopPackageAllocationRequest { + package_id: number // 套餐ID,必填 + shop_id: number // 店铺ID,必填 + cost_price: number // 覆盖的成本价(分),必填 +} + +/** + * 更新单套餐分配请求 + */ +export interface UpdateShopPackageAllocationRequest { + cost_price: number // 只允许修改成本价 +} + +/** + * 更新单套餐分配状态请求 + */ +export interface UpdateShopPackageAllocationStatusRequest { + status: number // 1:启用, 2:禁用 +} + +// ==================== 套餐系列分配 ==================== + +/** + * 基础返佣配置 + */ +export interface BaseCommissionConfig { + mode: 'fixed' | 'percent' // 返佣模式:'fixed':固定金额, 'percent':百分比 + value: number // 返佣值:固定金额(分)或百分比的千分比(如200表示20%) +} + +/** + * 梯度档位配置 + */ +export interface TierEntry { + threshold: number // 阈值(销量或销售额) + mode: 'fixed' | 'percent' // 返佣模式 + value: number // 返佣值 +} + +/** + * 梯度返佣配置 + */ +export interface TierCommissionConfig { + period_type: 'monthly' | 'quarterly' | 'yearly' // 周期类型 + tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额 + tiers: TierEntry[] // 梯度档位数组 +} + +/** + * 套餐系列分配响应 + */ +export interface ShopSeriesAllocationResponse { + id: number + series_id: number + series_name: string + shop_id: number + shop_name: string + allocator_shop_id: number // 分配者店铺ID + allocator_shop_name: string // 分配者店铺名称 + base_commission: BaseCommissionConfig // 基础返佣配置 + enable_tier_commission: boolean // 是否启用梯度返佣 + tier_config?: TierCommissionConfig // 梯度返佣配置(可选) + status: number // 1:启用, 2:禁用 + created_at: string + updated_at: string +} + +/** + * 套餐系列分配查询参数 + */ +export interface ShopSeriesAllocationQueryParams extends PaginationParams { + shop_id?: number // 店铺ID筛选 + series_id?: number // 系列ID筛选 + status?: number // 状态筛选 +} + +/** + * 创建套餐系列分配请求 + */ +export interface CreateShopSeriesAllocationRequest { + series_id: number // 套餐系列ID,必填 + shop_id: number // 店铺ID,必填 + base_commission: BaseCommissionConfig // 基础返佣配置,必填 + enable_tier_commission?: boolean // 是否启用梯度返佣,可选(默认false) + tier_config?: TierCommissionConfig // 梯度返佣配置,当enable_tier_commission为true时必填 +} + +/** + * 更新套餐系列分配请求 + */ +export interface UpdateShopSeriesAllocationRequest { + base_commission?: BaseCommissionConfig // 基础返佣配置 + enable_tier_commission?: boolean // 是否启用梯度返佣 + tier_config?: TierCommissionConfig // 梯度返佣配置 +} + +/** + * 更新套餐系列分配状态请求 + */ +export interface UpdateShopSeriesAllocationStatusRequest { + status: number // 1:启用, 2:禁用 +} diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts index 379ac77..718afb1 100644 --- a/src/utils/http/index.ts +++ b/src/utils/http/index.ts @@ -222,10 +222,11 @@ function handleErrorMessage(error: any, mode: ErrorMessageMode = 'message') { async function request(config: ExtendedRequestConfig): Promise { const processedConfig = processRequestConfig(config) - // 对 POST | PUT 请求特殊处理 + // 对 POST | PUT | PATCH 请求特殊处理 if ( processedConfig.method?.toUpperCase() === 'POST' || - processedConfig.method?.toUpperCase() === 'PUT' + processedConfig.method?.toUpperCase() === 'PUT' || + processedConfig.method?.toUpperCase() === 'PATCH' ) { // 如果已经有 data,则保留原有的 data if (processedConfig.params && !processedConfig.data) { @@ -258,6 +259,9 @@ const api = { put(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'PUT' }) // PUT 请求 }, + patch(config: ExtendedRequestConfig): Promise { + return request({ ...config, method: 'PATCH' }) // PATCH 请求 + }, del(config: ExtendedRequestConfig): Promise { return request({ ...config, method: 'DELETE' }) // DELETE 请求 }, diff --git a/src/views/account-management/account/index.vue b/src/views/account-management/account/index.vue index 068f61a..ca69d61 100644 --- a/src/views/account-management/account/index.vue +++ b/src/views/account-management/account/index.vue @@ -427,7 +427,7 @@ const rules = reactive({ username: [ { required: true, message: '请输入账号名称', trigger: 'blur' }, - { min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' } + { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, diff --git a/src/views/account-management/platform-account/index.vue b/src/views/account-management/platform-account/index.vue index 481a54b..50a7950 100644 --- a/src/views/account-management/platform-account/index.vue +++ b/src/views/account-management/platform-account/index.vue @@ -536,7 +536,7 @@ const rules = reactive({ username: [ { required: true, message: '请输入账号名称', trigger: 'blur' }, - { min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' } + { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, diff --git a/src/views/account-management/shop-account/index.vue b/src/views/account-management/shop-account/index.vue index 67fa0ef..a70f920 100644 --- a/src/views/account-management/shop-account/index.vue +++ b/src/views/account-management/shop-account/index.vue @@ -497,7 +497,7 @@ const rules = reactive({ username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, - { min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' } + { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, diff --git a/src/views/asset-management/card-list/index.vue b/src/views/asset-management/card-list/index.vue index 6c2f37d..0e6f998 100644 --- a/src/views/asset-management/card-list/index.vue +++ b/src/views/asset-management/card-list/index.vue @@ -25,6 +25,9 @@ 批量回收 + + 批量设置套餐系列 + @@ -259,6 +262,75 @@ + + + + + 已选择 {{ selectedCards.length }} 张卡 + + + + + + + + + + + + + + + + + + {{ seriesBindingResult.success_count }} + + + {{ seriesBindingResult.fail_count }} + + + + + 失败项详情 + + + + + + + + + + + @@ -267,6 +339,7 @@ diff --git a/src/views/finance/commission/withdrawal-settings/index.vue b/src/views/finance/commission/withdrawal-settings/index.vue index 20bc093..f4fab5f 100644 --- a/src/views/finance/commission/withdrawal-settings/index.vue +++ b/src/views/finance/commission/withdrawal-settings/index.vue @@ -100,7 +100,7 @@ - + 单位:次 - - - - 单位:天(0表示实时到账) -