# 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**: 待确认,当前设计是创建时计算一次,不自动更新