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

9.9 KiB
Raw Blame History

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 支持下划线字段名,不影响类型安全

示例

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 服务层进行状态值映射转换

映射规则

// 前端 -> 后端
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 页面,使用组件化结构:

<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
  • 表单验证错误:在表单字段下显示错误提示

错误提示方式

// 网络错误或服务器错误
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

  1. Q: 套餐被删除后,历史订单如何处理? A: 待产品确认,可能需要软删除机制

  2. Q: 代理商可以自行调整套餐售价吗? A: 待产品确认,当前设计只展示建议售价

  3. Q: 套餐系列和套餐是否支持批量操作(批量启用/禁用)? A: 当前不支持,后续迭代考虑

  4. Q: 是否需要套餐变更历史记录? A: 后端可能有审计日志,前端暂不展示

  5. Q: 单套餐分配的"原计算成本价"是否需要实时更新? A: 待确认,当前设计是创建时计算一次,不自动更新