Files
one-pipe-system/openspec/changes/add-package-management-system/design.md
sexygoat 841cf0442b
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m30s
fetch(add): 订单管理-企业设备
2026-01-29 15:43:45 +08:00

333 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:启用`
**决策**
- **在常量配置中定义套餐专用的状态枚举**
- **前端页面使用项目统一的 CommonStatus0/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
<template>
<ArtTableFullScreen>
<ArtSearchBar /> <!-- 搜索栏 -->
<ElCard>
<ArtTableHeader /> <!-- 表格头部刷新列设置操作按钮 -->
<ArtTable /> <!-- 数据表格 -->
<ElDialog /> <!-- 新增/编辑对话框 -->
</ElCard>
</ArtTableFullScreen>
</template>
```
**理由**
- 与项目现有页面风格一致
- 复用成熟的组件,减少开发工作量
- 便于维护和扩展
## 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<Record<number, boolean>>({}) // 删除操作(可选)
```
**状态管理规则**
- **列表查询**:表格显示 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**: 待确认,当前设计是创建时计算一次,不自动更新