9.9 KiB
Design Document: 套餐管理系统实现
Context
实现完整的套餐管理系统,包括4个核心模块。该系统需要支持多级代理商体系的套餐分配和定价管理。
背景:
- 项目已有类型定义(src/types/api/package.ts),但使用不同的字段命名和枚举值
- 后端 API 已实现,使用下划线命名(如
series_name) - 前端项目统一使用 CommonStatus 枚举(0:禁用, 1:启用)
- 参考实现:
/system/role页面使用了组件化架构
约束:
- 必须保留现有类型定义文件,不能破坏现有代码
- 需要兼容后端 API 的字段命名规范
- 需要适配项目的状态枚举规范
Goals / Non-Goals
Goals
- 实现4个核心模块的完整 CRUD 功能
- 建立统一的 API 服务层,封装后端接口
- 实现组件化的页面结构,参考
/system/role - 支持复杂的定价规则(系列加价 vs 单套餐覆盖)
- 确保数据隔离和权限控制
Non-Goals
- 不重构现有的 package.ts 类型定义
- 不实现套餐的实时统计和报表功能(后续迭代)
- 不实现套餐批量导入功能(后续迭代)
- 不实现套餐的版本管理功能
Decisions
Decision 1: API 字段命名策略
问题:后端使用下划线命名(snake_case),前端类型通常使用驼峰命名(camelCase)。
决策:
- API 请求/响应保持下划线命名,与后端保持一致
- 创建新的类型文件
packageManagement.ts,使用下划线命名 - 在表单提交和响应处理时不做转换,直接使用下划线字段
理由:
- 减少转换层的复杂性和错误风险
- 与后端 API 文档保持一致,便于对照
- 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 服务层进行状态值映射转换
映射规则:
// 前端 -> 后端
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个独立的服务文件:
packageSeries.ts- 套餐系列管理package.ts- 套餐管理myPackage.ts- 代理可售套餐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 页面,使用组件化结构:
<template>
<ArtTableFullScreen>
<ArtSearchBar />
<!-- 搜索栏 -->
<ElCard>
<ArtTableHeader />
<!-- 表格头部:刷新、列设置、操作按钮 -->
<ArtTable />
<!-- 数据表格 -->
<ElDialog />
<!-- 新增/编辑对话框 -->
</ElCard>
</ArtTableFullScreen>
</template>
理由:
- 与项目现有页面风格一致
- 复用成熟的组件,减少开发工作量
- 便于维护和扩展
Risks / Trade-offs
Risk 1: 后端 API 未完成
风险:后端接口可能尚未实现或与文档不一致。
缓解措施:
- 先实现 API 服务层,使用 TypeScript 类型约束
- 使用 Mock 数据进行前端开发(已有示例)
- 与后端团队确认 API 规范和联调时间
- 预留 API 调试和修正时间
Risk 2: 状态值映射可能遗漏
风险:在某些地方忘记转换状态值,导致显示错误。
缓解措施:
- 在 API 服务层统一处理转换
- 创建工具函数封装映射逻辑
- 编写单元测试覆盖映射函数
- Code Review 时重点检查状态相关代码
Risk 3: 定价规则理解偏差
风险:对定价规则的理解与实际业务需求有偏差。
缓解措施:
- 在实现前与产品确认定价规则
- 编写测试用例覆盖各种定价场景
- 在 UI 上清晰展示价格来源和计算方式
- 预留调整空间,避免硬编码
Trade-off 1: 类型定义冗余
取舍:保留旧的 package.ts 类型定义,新增 packageManagement.ts。
代价:
- 存在两套类型定义,可能造成混淆
- 占用额外的代码空间
收益:
- 不影响现有代码,向后兼容
- 新旧系统可以并存,降低迁移风险
- 未来可以逐步迁移到新类型
Trade-off 2: 状态值映射增加复杂度
取舍:在 API 服务层进行状态值转换。
代价:
- 增加一层转换逻辑
- 可能影响性能(微小)
收益:
- 前端 UI 保持一致性
- 业务逻辑更清晰
- 便于后续维护
Migration Plan
Phase 1: 基础设施(1-2天)
- 创建类型定义文件
- 创建常量配置文件
- 设置状态映射工具函数
Phase 2: API 服务层(2-3天)
- 实现4个 API 服务模块
- 编写单元测试(可选)
- 使用 Mock 数据测试
Phase 3: 页面实现(4-5天)
- 套餐系列管理页面(1天)
- 套餐管理页面(1.5天)
- 代理可售套餐页面(1天)
- 单套餐分配页面(1.5天)
Phase 4: 集成测试(1-2天)
- 与后端 API 联调
- 端到端功能测试
- 修复 Bug 和优化
Phase 5: 上线(1天)
- Code Review
- 合并代码
- 部署到测试环境
- 部署到生产环境
总计:9-13 个工作日
Rollback Plan
如果出现严重问题,回滚步骤:
- 从 Git 回滚到上一个稳定版本
- 移除新增的路由配置
- 移除新增的 API 服务导出
- 通知用户功能暂时不可用
Decision 7: 错误处理策略
问题:如何统一处理各类错误和异常?
决策:分层错误处理机制
- 网络错误:axios 拦截器统一捕获,显示通用错误提示
- 401 未认证:自动跳转到登录页面
- 403 无权限:显示权限不足提示,不跳转
- 400 业务错误:根据错误信息显示具体提示(ElMessage.error)
- 表单验证错误:在表单字段下显示错误提示
错误提示方式:
// 网络错误或服务器错误
ElMessage.error('网络错误,请稍后重试')
// 业务错误(后端返回的具体错误)
ElMessage.error(res.message || '操作失败')
// 操作成功
ElMessage.success('操作成功')
理由:
- 统一的错误处理提升用户体验
- 分层处理避免重复代码
- 清晰的错误提示帮助用户理解问题
Decision 8: Loading 状态管理
问题:如何管理各种操作的加载状态?
决策:细粒度的 loading 状态管理
Loading 状态分类:
const loading = ref(false) // 表格数据加载
const submitLoading = ref(false) // 表单提交
const deleteLoading = ref<Record<number, boolean>>({}) // 删除操作(可选)
状态管理规则:
- 列表查询:表格显示 loading 遮罩
- 新增/编辑提交:提交按钮显示 loading,禁用表单
- 删除操作:可选择在按钮上显示 loading 或全局 loading
- 状态切换:ElSwitch 自带 loading 效果,先更新 UI 再调用 API
理由:
- 细粒度控制提供更好的交互反馈
- 防止重复提交
- 清晰标识正在进行的操作
Open Questions
-
Q: 套餐被删除后,历史订单如何处理? A: 待产品确认,可能需要软删除机制
-
Q: 代理商可以自行调整套餐售价吗? A: 待产品确认,当前设计只展示建议售价
-
Q: 套餐系列和套餐是否支持批量操作(批量启用/禁用)? A: 当前不支持,后续迭代考虑
-
Q: 是否需要套餐变更历史记录? A: 后端可能有审计日志,前端暂不展示
-
Q: 单套餐分配的"原计算成本价"是否需要实时更新? A: 待确认,当前设计是创建时计算一次,不自动更新