From 8f66ca7bcbc1105d0e55d0bbb8272d7174e92772 Mon Sep 17 00:00:00 2001 From: huang Date: Sat, 11 Apr 2026 11:27:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/COMMISSION_DOCS_INDEX.md | 235 ++++++++++++ docs/ORDER_STATUS_INDEX.md | 250 +++++++++++++ docs/PACKAGE_DOCS_INDEX.md | 244 ++++++++++++ docs/admin-openapi.yaml | 9 +- docs/architecture_diagram.md | 366 ++++++++++++++++++ docs/commission-flow-diagram.md | 319 ++++++++++++++++ docs/commission-quick-reference.md | 283 ++++++++++++++ docs/commission-search-result.md | 237 ++++++++++++ docs/order_status_commission_analysis.md | 456 +++++++++++++++++++++++ docs/order_status_quick_reference.md | 196 ++++++++++ docs/order_status_search_summary.md | 174 +++++++++ docs/package_architecture_diagram.md | 366 ++++++++++++++++++ docs/package_quick_navigation.md | 147 ++++++++ docs/package_structure_summary.md | 349 +++++++++++++++++ docs/quick_navigation.md | 147 ++++++++ 15 files changed, 3775 insertions(+), 3 deletions(-) create mode 100644 docs/COMMISSION_DOCS_INDEX.md create mode 100644 docs/ORDER_STATUS_INDEX.md create mode 100644 docs/PACKAGE_DOCS_INDEX.md create mode 100644 docs/architecture_diagram.md create mode 100644 docs/commission-flow-diagram.md create mode 100644 docs/commission-quick-reference.md create mode 100644 docs/commission-search-result.md create mode 100644 docs/order_status_commission_analysis.md create mode 100644 docs/order_status_quick_reference.md create mode 100644 docs/order_status_search_summary.md create mode 100644 docs/package_architecture_diagram.md create mode 100644 docs/package_quick_navigation.md create mode 100644 docs/package_structure_summary.md create mode 100644 docs/quick_navigation.md diff --git a/docs/COMMISSION_DOCS_INDEX.md b/docs/COMMISSION_DOCS_INDEX.md new file mode 100644 index 0000000..1638e00 --- /dev/null +++ b/docs/COMMISSION_DOCS_INDEX.md @@ -0,0 +1,235 @@ +# 差价佣金文档索引 + +本索引汇总了所有关于差价佣金计算和分配的文档,帮助快速定位所需信息。 + +## 📚 文档列表 + +### 1. **commission-search-result.md** (237行) +**内容**: 差价佣金计算和分配逻辑的完整搜索结果 + +**包含内容**: +- ✅ 差价佣金的计算方式(公式、流程、示例) +- ✅ 差价佣金的发放条件(触发条件、发放流程、入账步骤) +- ✅ 差价佣金与订单状态的关系(状态定义、转换关系、关键字段) +- ✅ 关键代码位置总结(表格形式) +- ✅ 相关常量定义 + +**适合场景**: 需要快速了解差价佣金的完整逻辑 + +**快速导航**: +- 差价佣金计算公式 → 第一部分 1.1 +- 计算流程详解 → 第一部分 1.2 +- 触发条件 → 第二部分 2.1 +- 发放流程 → 第二部分 2.2 +- 佣金入账 → 第二部分 2.3 + +--- + +### 2. **commission-flow-diagram.md** (319行) +**内容**: 差价佣金计算的可视化流程图 + +**包含内容**: +- ✅ 订单支付到佣金入账的完整流程(ASCII流程图) +- ✅ 成本价差佣金计算详细流程(分步骤流程图) +- ✅ 佣金入账流程(5个步骤) +- ✅ 订单状态转换图 +- ✅ 佣金记录状态转换图 +- ✅ 代理链佣金分配示例(实际案例) + +**适合场景**: 需要理解整个流程的全貌,或向他人解释流程 + +**快速导航**: +- 完整流程 → 第一部分 +- 计算细节 → 第二部分 +- 入账步骤 → 第三部分 +- 状态转换 → 第四、五部分 +- 实际案例 → 第六部分 + +--- + +### 3. **commission-quick-reference.md** (283行) +**内容**: 差价佣金的快速参考指南 + +**包含内容**: +- ✅ 关键文件位置表(8个关键功能) +- ✅ 关键常量速查(任务类型、佣金来源、状态常量) +- ✅ 关键数据库表(5个核心表) +- ✅ 常见问题快速查询(10个Q&A) +- ✅ 调试技巧(6个SQL查询示例) +- ✅ 性能优化建议(3个优化方向) +- ✅ 常见错误排查(3个常见错误) + +**适合场景**: 开发过程中快速查询信息,或调试问题 + +**快速导航**: +- 找代码位置 → 快速查询表 1 +- 找常量值 → 快速查询表 2 +- 找数据库表 → 快速查询表 3 +- 常见问题 → 常见问题快速查询 +- 调试SQL → 调试技巧 +- 性能问题 → 性能优化建议 +- 错误处理 → 常见错误排查 + +--- + +### 4. **commission-package-model.md** (351行) +**内容**: 套餐与佣金业务模型(原有文档) + +**包含内容**: +- ✅ 核心概念(两种佣金类型、实体关系) +- ✅ 套餐模型详解 +- ✅ 差价佣金规则(计算规则、关键区分) +- ✅ 一次性佣金规则(触发条件、链式分配、流程) +- ✅ 梯度佣金规则 +- ✅ 约束规则(6个约束) +- ✅ 操作流程(理想的线性流程) + +**适合场景**: 需要理解完整的业务规则和约束 + +**快速导航**: +- 两种佣金类型 → 第一部分 1.1 +- 差价佣金规则 → 第三部分 +- 一次性佣金规则 → 第四部分 +- 约束规则 → 第六部分 + +--- + +## 🎯 使用指南 + +### 场景1: 我是新开发者,想快速了解差价佣金 + +**推荐阅读顺序**: +1. 先读 `commission-flow-diagram.md` 的第一部分(了解整体流程) +2. 再读 `commission-search-result.md` 的第一部分(理解计算方式) +3. 最后读 `commission-quick-reference.md` 的常见问题(掌握细节) + +**预计时间**: 30分钟 + +--- + +### 场景2: 我需要修改佣金计算逻辑 + +**推荐阅读顺序**: +1. 先读 `commission-search-result.md` 的第一部分(理解当前逻辑) +2. 查看 `commission-quick-reference.md` 的关键文件位置(定位代码) +3. 查看 `commission-flow-diagram.md` 的第二部分(理解计算细节) +4. 打开代码文件进行修改 + +**关键文件**: `internal/service/commission_calculation/service.go:121-225` + +--- + +### 场景3: 我需要调试佣金计算问题 + +**推荐阅读顺序**: +1. 先读 `commission-quick-reference.md` 的调试技巧(获取SQL查询) +2. 执行SQL查询获取数据 +3. 对比 `commission-flow-diagram.md` 的第六部分(验证计算结果) +4. 查看 `commission-quick-reference.md` 的常见错误排查(定位问题) + +**关键SQL**: 见 `commission-quick-reference.md` 的调试技巧部分 + +--- + +### 场景4: 我需要向产品经理解释佣金逻辑 + +**推荐阅读顺序**: +1. 先读 `commission-package-model.md` 的第三部分(业务规则) +2. 使用 `commission-flow-diagram.md` 的第六部分(实际案例) +3. 使用 `commission-flow-diagram.md` 的流程图(可视化展示) + +**关键资源**: `commission-flow-diagram.md` 的第六部分(代理链佣金分配示例) + +--- + +### 场景5: 我需要验证佣金计算的正确性 + +**推荐阅读顺序**: +1. 先读 `commission-quick-reference.md` 的Q10(验证方法) +2. 执行SQL查询获取数据 +3. 按照验证步骤进行检查 +4. 如有问题,查看常见错误排查 + +**关键内容**: `commission-quick-reference.md` 的Q10 + +--- + +## 📊 文档对比表 + +| 文档 | 长度 | 类型 | 适合场景 | 重点 | +|------|------|------|---------|------| +| commission-search-result.md | 237行 | 参考 | 快速了解逻辑 | 代码位置、计算方式 | +| commission-flow-diagram.md | 319行 | 可视化 | 理解流程 | 流程图、状态转换 | +| commission-quick-reference.md | 283行 | 工具 | 开发调试 | 常见问题、SQL查询 | +| commission-package-model.md | 351行 | 规范 | 业务理解 | 业务规则、约束 | + +--- + +## 🔗 关键代码位置速查 + +| 功能 | 文件 | 行号 | 文档位置 | +|------|------|------|---------| +| 差价佣金计算 | `internal/service/commission_calculation/service.go` | 121-225 | search-result 1.2 | +| 佣金入账 | `internal/service/commission_calculation/service.go` | 641-687 | search-result 2.3 | +| 佣金计算触发 | `internal/service/order/service.go` | 2132-2150 | search-result 2.1 | +| 订单支付完成 | `internal/service/order/service.go` | 1431-1623 | search-result 2.1 | +| 异步任务处理 | `internal/task/commission_calculation.go` | 40-62 | search-result 2.2 | + +--- + +## 💡 常见问题速查 + +| 问题 | 答案位置 | +|------|---------| +| 差价佣金什么时候计算? | quick-reference Q1 | +| 差价佣金的计算公式是什么? | quick-reference Q2 / search-result 1.1 | +| 如何查询某个订单的佣金记录? | quick-reference Q3 | +| 佣金什么时候入账? | quick-reference Q4 | +| 链路断裂是什么意思? | quick-reference Q5 | +| 如何追踪佣金计算过程? | quick-reference Q6 | +| 代理钱包余额如何更新? | quick-reference Q7 | +| 如何处理佣金计算失败? | quick-reference Q8 | +| 一个订单产生多少条佣金记录? | quick-reference Q9 | +| 如何验证佣金计算的正确性? | quick-reference Q10 | + +--- + +## 📝 文档维护 + +### 最后更新时间 +- commission-search-result.md: 2025-04-11 +- commission-flow-diagram.md: 2025-04-11 +- commission-quick-reference.md: 2025-04-11 +- commission-package-model.md: 2025-02-03 + +### 更新日志 +- 2025-04-11: 新增三份文档(search-result, flow-diagram, quick-reference) +- 2025-02-03: 原有 commission-package-model.md + +### 如何贡献 +如果发现文档有误或需要补充,请: +1. 提交 Issue 描述问题 +2. 或直接提交 PR 修改文档 +3. 确保修改后的文档保持一致性 + +--- + +## 🚀 快速开始 + +**第一次接触差价佣金?** + +1. 花5分钟看 `commission-flow-diagram.md` 的第一部分 +2. 花10分钟看 `commission-search-result.md` 的第一部分 +3. 需要时查阅 `commission-quick-reference.md` + +**预计总时间**: 15分钟快速入门 + +--- + +## 📞 获取帮助 + +- **代码问题**: 查看 `commission-quick-reference.md` 的常见错误排查 +- **业务问题**: 查看 `commission-package-model.md` 的约束规则 +- **流程问题**: 查看 `commission-flow-diagram.md` 的流程图 +- **其他问题**: 查看 `commission-quick-reference.md` 的常见问题 + diff --git a/docs/ORDER_STATUS_INDEX.md b/docs/ORDER_STATUS_INDEX.md new file mode 100644 index 0000000..e5d2bd2 --- /dev/null +++ b/docs/ORDER_STATUS_INDEX.md @@ -0,0 +1,250 @@ +# 订单状态与佣金系统文档索引 + +本索引汇总了关于订单状态、冻结状态和差价佣金的所有文档和代码位置。 + +## 📚 文档列表 + +### 1. [order_status_commission_analysis.md](order_status_commission_analysis.md) +**完整的系统分析报告** - 适合深入理解系统设计 + +**包含内容**: +- ✅ 订单状态定义(支付状态、佣金状态) +- ✅ 订单状态流转逻辑(3种支付方式) +- ✅ 订单超时自动取消机制 +- ✅ 佣金系统与订单的关系 +- ✅ 钱包冻结与提现流程(3阶段) +- ✅ 关键数据模型说明 +- ✅ 完整流程图 +- ✅ 业务规则总结 +- ✅ 文件索引表 + +**适用场景**: +- 需要全面理解系统设计 +- 进行系统架构评审 +- 编写相关功能文档 + +--- + +### 2. [order_status_quick_reference.md](order_status_quick_reference.md) +**快速参考指南** - 适合日常开发查询 + +**包含内容**: +- ✅ 订单支付状态速查表 +- ✅ 订单佣金状态速查表 +- ✅ 订单创建流程速查 +- ✅ 钱包冻结状态速查表 +- ✅ 提现流程速查(3阶段) +- ✅ 佣金类型速查 +- ✅ 关键代码位置速查 +- ✅ 常用常量速查 +- ✅ 常见问题解答 +- ✅ 数据库查询示例 +- ✅ 业务规则速查 + +**适用场景**: +- 快速查询状态值 +- 查找代码位置 +- 解答常见问题 +- 编写 SQL 查询 + +--- + +### 3. [order_status_search_summary.md](order_status_search_summary.md) +**搜索结果总结** - 适合了解核心发现 + +**包含内容**: +- ✅ 搜索范围说明 +- ✅ 5个核心发现 +- ✅ 关键代码位置索引 +- ✅ 重要业务规则 +- ✅ 数据流向图 +- ✅ 生成的文档说明 +- ✅ 关键发现总结 +- ✅ 注意事项 +- ✅ 最佳实践 + +**适用场景**: +- 快速了解系统概况 +- 查找关键代码位置 +- 了解业务规则 +- 学习最佳实践 + +--- + +## 🔍 核心概念速查 + +### 订单状态 +| 状态 | 值 | 说明 | +|------|-----|------| +| 待支付 | 1 | 微信/支付宝支付时的初始状态 | +| 已支付 | 2 | 线下/钱包支付或支付回调后的状态 | +| 已取消 | 3 | 待支付订单超时30分钟后的状态 | +| 已退款 | 4 | 退款操作后的状态 | + +### 佣金状态 +| 状态 | 值 | 说明 | +|------|-----|------| +| 待计算 | 1 | 订单创建时的初始状态 | +| 已计算 | 2 | 佣金计算完成后的状态 | + +### 钱包冻结 +| 字段 | 说明 | +|------|------| +| balance | 总余额 | +| frozen_balance | 冻结余额(用于提现) | +| 可用余额 | balance - frozen_balance | + +--- + +## 📍 关键代码位置 + +### 订单相关 +``` +internal/model/order.go +├─ 第 98-104 行:支付状态常量 +├─ 第 106-110 行:佣金状态常量 +└─ 第 9-138 行:订单模型定义 + +internal/service/order/service.go +├─ 第 114-354 行:CreateLegacy(已废弃) +├─ 第 359-674 行:CreateAdminOrder(后台订单创建) +├─ 第 679-928 行:CreateH5Order(H5订单创建) +└─ 第 1314-1385 行:订单超时自动取消 +``` + +### 佣金相关 +``` +internal/service/commission_calculation/service.go +├─ 第 74-119 行:佣金计算主逻辑 +└─ 第 121-230 行:成本价差佣金计算 + +internal/model/commission.go +└─ 第 9-46 行:佣金记录模型 +``` + +### 提现相关 +``` +internal/service/shop_commission/service.go +└─ 第 517-649 行:代理发起提现申请 + +internal/service/commission_withdrawal/service.go +├─ 第 145-256 行:平台审核通过 +└─ 第 258-310 行:平台审核拒绝 +``` + +### 钱包相关 +``` +internal/model/agent_wallet.go +├─ 第 9-30 行:代理钱包模型 +└─ 第 37-94 行:钱包交易记录模型 + +pkg/constants/wallet.go +├─ 第 17-22 行:钱包状态常量 +├─ 第 83-91 行:交易状态常量 +└─ 第 1-234 行:所有钱包相关常量 +``` + +--- + +## 🎯 常见任务指南 + +### 任务 1:查询订单状态 +**文档**:order_status_quick_reference.md - 订单支付状态速查表 +**代码**:internal/model/order.go:98-104 + +### 任务 2:理解佣金计算 +**文档**:order_status_commission_analysis.md - 第三章 +**代码**:internal/service/commission_calculation/service.go:74-230 + +### 任务 3:实现提现功能 +**文档**:order_status_commission_analysis.md - 第四章 +**代码**: +- 发起申请:internal/service/shop_commission/service.go:517-649 +- 审核通过:internal/service/commission_withdrawal/service.go:145-256 +- 审核拒绝:internal/service/commission_withdrawal/service.go:258-310 + +### 任务 4:查询可提现金额 +**文档**:order_status_quick_reference.md - 数据库查询速查 +**SQL**: +```sql +SELECT balance - frozen_balance as available_balance +FROM tb_agent_wallet +WHERE shop_id = ? AND wallet_type = 'commission'; +``` + +### 任务 5:处理订单超时 +**文档**:order_status_commission_analysis.md - 第二章 +**代码**:internal/service/order/service.go:1314-1385 + +--- + +## ⚠️ 重要注意事项 + +1. **订单没有冻结状态** + - 冻结状态存在于代理钱包,不在订单 + - 订单只有支付状态和佣金状态 + +2. **订单超时不计算佣金** + - 取消的订单佣金状态保持为"待计算" + - 不会入队佣金计算任务 + +3. **代购订单不计算一次性佣金** + - 一次性佣金仅对非代购订单触发 + - 仅在首次购买时触发 + +4. **提现流程中的冻结机制** + - 提现申请时冻结余额 + - 审核通过时从冻结余额直接扣款 + - 审核拒绝时解冻余额 + +--- + +## 📊 文档统计 + +| 文档 | 大小 | 章节数 | 代码位置数 | +|------|------|--------|-----------| +| order_status_commission_analysis.md | 14K | 9 | 20+ | +| order_status_quick_reference.md | 6.0K | 12 | 15+ | +| order_status_search_summary.md | 6.1K | 8 | 10+ | + +**总计**:26.1K,29个章节,45+个代码位置 + +--- + +## 🚀 快速开始 + +### 第一次接触系统? +1. 阅读 order_status_search_summary.md(5分钟) +2. 查看 order_status_commission_analysis.md 的流程图(10分钟) +3. 收藏 order_status_quick_reference.md(备用) + +### 需要快速查询? +1. 打开 order_status_quick_reference.md +2. 使用速查表或常见问题部分 +3. 参考代码位置索引 + +### 需要深入理解? +1. 阅读 order_status_commission_analysis.md 的完整内容 +2. 对照代码位置查看源代码 +3. 参考业务规则总结 + +--- + +## 📞 相关资源 + +### 相关文档 +- [套餐与佣金业务模型](../commission-package-model.md) +- [分佣逻辑验证指引](../优化说明/分佣逻辑正确与否验证.md) + +### 相关代码 +- 订单服务:`internal/service/order/` +- 佣金服务:`internal/service/commission_calculation/` +- 提现服务:`internal/service/commission_withdrawal/` +- 钱包服务:`internal/service/shop_commission/` + +--- + +**最后更新**:2024年4月11日 +**文档版本**:1.0 +**维护者**:开发团队 + diff --git a/docs/PACKAGE_DOCS_INDEX.md b/docs/PACKAGE_DOCS_INDEX.md new file mode 100644 index 0000000..324f945 --- /dev/null +++ b/docs/PACKAGE_DOCS_INDEX.md @@ -0,0 +1,244 @@ +# 套餐接口文档索引 + +本目录包含套餐(Package/PackageSeries)相关接口的完整代码结构分析文档。 + +## 📚 文档清单 + +### 1. 📋 [完整结构分析](./package_structure_summary.md) +**用途**:全面了解套餐接口的代码结构 + +**包含内容**: +- Model 层文件位置和结构 +- DTO 层请求/响应定义 +- Handler 层方法列表 +- Service 层业务逻辑 +- Store 层数据访问 +- API 路由注册 +- 关键字段详解(duration_days) +- 业务逻辑关键点 +- 数据库表结构 +- 完整请求/响应示例 + +**适合场景**: +- 需要全面了解套餐接口架构 +- 查看具体的代码位置和行号 +- 理解业务逻辑和校验规则 + +--- + +### 2. 🎯 [快速导航指南](./package_quick_navigation.md) +**用途**:快速定位需要的代码 + +**包含内容**: +- 按功能快速查找表 +- 按字段快速查找表 +- 按 API 端点快速查找表 +- 关键代码片段 +- 数据流向图 +- 常见操作指南 +- 相关模块链接 + +**适合场景**: +- 需要快速找到某个字段或方法 +- 想了解数据流向 +- 需要修改或扩展功能 + +--- + +### 3. 🏗️ [架构详细图](./package_architecture_diagram.md) +**用途**:可视化理解系统架构 + +**包含内容**: +- 整体架构流程图 +- 分层详细结构(Handler/Service/Store) +- 数据模型关系图 +- 请求流程示例 +- 代理用户查询流程 +- 字段验证流程 + +**适合场景**: +- 需要理解系统整体架构 +- 想了解数据流向和处理过程 +- 需要设计新功能时参考 + +--- + +## 🔍 快速查找 + +### 按需求查找 + +| 需求 | 文档 | 位置 | +|------|------|------| +| 查看套餐数据模型 | 完整结构分析 | 第 1 节 | +| 查看 API 请求/响应结构 | 完整结构分析 | 第 2 节 | +| 查看 HTTP 处理器 | 完整结构分析 | 第 3 节 | +| 查看业务逻辑 | 完整结构分析 | 第 4 节 | +| 查看数据访问 | 完整结构分析 | 第 5 节 | +| 快速定位代码 | 快速导航指南 | 第 1-2 节 | +| 理解数据流向 | 快速导航指南 | 第 3 节 | +| 了解系统架构 | 架构详细图 | 第 1-2 节 | +| 查看请求流程 | 架构详细图 | 第 4-5 节 | + +### 按文件查找 + +| 文件 | 说明 | 文档 | +|------|------|------| +| `internal/model/package.go` | 数据模型 | 完整结构分析 1.1 | +| `internal/model/dto/package_dto.go` | DTO 定义 | 完整结构分析 1.2 | +| `internal/handler/admin/package.go` | HTTP 处理 | 完整结构分析 1.3 | +| `internal/service/package/service.go` | 业务逻辑 | 完整结构分析 1.4 | +| `internal/store/postgres/package_store.go` | 数据访问 | 完整结构分析 1.5 | +| `internal/routes/package.go` | 路由注册 | 完整结构分析 2.2 | +| `internal/routes/admin.go` | 路由入口 | 完整结构分析 2.1 | + +### 按字段查找 + +| 字段 | Model 位置 | DTO 位置 | 文档 | +|------|-----------|---------|------| +| `duration_days` | package.go:46 | package_dto.go:96 | 完整结构分析 3.1 | +| `calendar_type` | package.go:45 | package_dto.go:95 | 完整结构分析 3.1 | +| `duration_months` | package.go:37 | package_dto.go:79 | 完整结构分析 3.1 | +| `real_data_mb` | package.go:38 | package_dto.go:80 | 完整结构分析 3.1 | +| `virtual_data_mb` | package.go:39 | package_dto.go:81 | 完整结构分析 3.1 | + +--- + +## 🔑 关键信息速查 + +### duration_days 字段 + +**定义位置**: +- Model: `internal/model/package.go:46` +- DTO: `internal/model/dto/package_dto.go:96` + +**用途**: +- 当 `calendar_type = "by_day"` 时必填 +- 用于按天计算套餐有效期 + +**验证规则**: +- 范围:1-3650 天 +- 当 `calendar_type = "by_day"` 时必须提供 + +**相关校验**: +- Service 层校验:`internal/service/package/service.go:65-80` + +--- + +### API 端点总览 + +| 方法 | 路径 | 处理器 | 说明 | +|------|------|--------|------| +| GET | `/packages` | `List()` | 套餐列表 | +| POST | `/packages` | `Create()` | 创建套餐 | +| GET | `/packages/:id` | `Get()` | 获取详情 | +| PUT | `/packages/:id` | `Update()` | 更新套餐 | +| DELETE | `/packages/:id` | `Delete()` | 删除套餐 | +| PATCH | `/packages/:id/status` | `UpdateStatus()` | 更新状态 | +| PATCH | `/packages/:id/shelf` | `UpdateShelfStatus()` | 更新上架状态 | +| PATCH | `/packages/:id/retail-price` | `UpdateRetailPrice()` | 更新零售价 | + +**路由注册**: +- 文件:`internal/routes/package.go:11-78` +- 入口:`internal/routes/admin.go:71-78` + +--- + +### 关键业务逻辑 + +#### 1. 虚流量配置校验 +**位置**:`internal/service/package/service.go:51-63` + +规则: +- 启用虚流量时,虚流量额度必须 > 0 +- 虚流量额度不能大于真流量额度 + +#### 2. 套餐周期类型校验 +**位置**:`internal/service/package/service.go:65-80` + +规则: +- `natural_month`:必须提供 `duration_months` +- `by_day`:必须提供 `duration_days` + +#### 3. 代理用户套餐过滤 +**位置**:`internal/store/postgres/package_store.go:57-66` + +规则: +- 代理用户只能看到已分配的套餐 +- 通过 INNER JOIN `tb_shop_package_allocation` 实现 + +--- + +## 📊 数据库表 + +### 主要表 + +| 表名 | 说明 | 文档 | +|------|------|------| +| `tb_package` | 套餐表 | 完整结构分析 6.1 | +| `tb_package_series` | 套餐系列表 | 完整结构分析 6.2 | +| `tb_package_usage` | 套餐使用表 | 架构详细图 3 | +| `tb_package_usage_daily_record` | 日记录表 | 架构详细图 3 | +| `tb_shop_package_allocation` | 套餐分配表 | 架构详细图 3 | + +--- + +## 🛠️ 常见操作 + +### 添加新的套餐字段 + +1. **Model 层**:修改 `internal/model/package.go` 的 `Package` 结构体 +2. **DTO 层**:修改 `internal/model/dto/package_dto.go` 的相关 Request/Response +3. **Service 层**:修改 `internal/service/package/service.go` 的业务逻辑 +4. **Store 层**:修改 `internal/store/postgres/package_store.go` 的查询条件 +5. **数据库**:添加数据库迁移 + +详见:快速导航指南 第 4 节 + +### 修改套餐 API 响应 + +1. 修改 `PackageResponse` 结构体(`package_dto.go:71-99`) +2. 修改 Service 的转换逻辑 +3. 测试 API 端点 + +详见:快速导航指南 第 4 节 + +### 添加新的套餐 API 端点 + +1. 在 `PackageHandler` 中添加方法 +2. 在 `PackageService` 中添加业务逻辑 +3. 在 `registerPackageRoutes()` 中注册路由 + +详见:快速导航指南 第 4 节 + +--- + +## 📝 相关文档 + +- **套餐系统升级**:`docs/package-system-upgrade/` +- **套餐与佣金业务模型**:`docs/commission-package-model.md` +- **API 文档生成规范**:`docs/api-documentation-guide.md` + +--- + +## 🔗 相关模块 + +| 模块 | 说明 | 文件 | +|------|------|------| +| **PackageSeries** | 套餐系列管理 | `internal/routes/package_series.go` | +| **PackageUsage** | 套餐使用记录 | `internal/routes/package_usage.go` | +| **ShopPackageAllocation** | 套餐分配给代理 | `internal/store/postgres/shop_package_allocation_store.go` | +| **ShopSeriesAllocation** | 系列分配给代理 | `internal/store/postgres/shop_series_allocation_store.go` | + +--- + +## 💡 使用建议 + +1. **首次了解**:先读 [架构详细图](./package_architecture_diagram.md),理解整体流程 +2. **深入学习**:再读 [完整结构分析](./package_structure_summary.md),了解具体实现 +3. **快速查找**:使用 [快速导航指南](./package_quick_navigation.md),定位具体代码 +4. **修改代码**:参考 [快速导航指南](./package_quick_navigation.md) 的常见操作部分 + +--- + +**最后更新**:2025-04-10 +**文档版本**:1.0 diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index da160aa..0aa24d3 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -199,8 +199,11 @@ components: description: 店铺名称 type: string status: - description: 状态 (1:待支付, 2:已完成, 3:已取消) + description: 状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款) type: integer + status_name: + description: 状态名称(中文) + type: string updated_at: description: 更新时间 type: string @@ -8682,11 +8685,11 @@ paths: minimum: 0 nullable: true type: integer - - description: 按状态过滤 (1:待支付, 2:已完成, 3:已取消) + - description: 按状态过滤 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款) in: query name: status schema: - description: 按状态过滤 (1:待支付, 2:已完成, 3:已取消) + description: 按状态过滤 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款) nullable: true type: integer - description: 创建时间起始日期(YYYY-MM-DD) diff --git a/docs/architecture_diagram.md b/docs/architecture_diagram.md new file mode 100644 index 0000000..17cd10e --- /dev/null +++ b/docs/architecture_diagram.md @@ -0,0 +1,366 @@ +# 套餐接口架构图 + +## 1. 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ HTTP 请求 │ +│ POST /api/admin/packages │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────▼────────┐ + │ 认证中间件 │ + │ (AdminAuth) │ + └────────┬────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ 路由注册 (routes/admin.go) │ + │ registerPackageRoutes() │ + │ ↓ │ + │ internal/routes/package.go │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PackageHandler │ + │ (internal/handler/admin/package.go) │ + │ │ + │ - List() │ + │ - Create() │ + │ - Get() │ + │ - Update() │ + │ - Delete() │ + │ - UpdateStatus() │ + │ - UpdateShelfStatus() │ + │ - UpdateRetailPrice() │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PackageService │ + │ (internal/service/package/service.go) │ + │ │ + │ 业务逻辑: │ + │ - 虚流量校验 │ + │ - 周期类型校验 │ + │ - 代理权限检查 │ + │ - DTO 转换 │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PackageStore │ + │ (internal/store/postgres/...) │ + │ │ + │ - PackageStore │ + │ - PackageSeriesStore │ + │ - ShopPackageAllocationStore │ + │ - ShopSeriesAllocationStore │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PostgreSQL 数据库 │ + │ │ + │ - tb_package │ + │ - tb_package_series │ + │ - tb_shop_package_allocation │ + │ - tb_shop_series_allocation │ + └─────────────────────────────────────────┘ +``` + +--- + +## 2. 分层详细结构 + +### Handler 层 +``` +PackageHandler +├── List(c *fiber.Ctx) +│ └── 解析查询参数 → 调用 Service.List() → 返回分页结果 +├── Create(c *fiber.Ctx) +│ └── 解析请求体 → 调用 Service.Create() → 返回创建结果 +├── Get(c *fiber.Ctx) +│ └── 解析 ID 参数 → 调用 Service.Get() → 返回详情 +├── Update(c *fiber.Ctx) +│ └── 解析 ID 和请求体 → 调用 Service.Update() → 返回更新结果 +├── Delete(c *fiber.Ctx) +│ └── 解析 ID 参数 → 调用 Service.Delete() → 返回删除结果 +├── UpdateStatus(c *fiber.Ctx) +│ └── 解析 ID 和状态 → 调用 Service.UpdateStatus() → 返回结果 +├── UpdateShelfStatus(c *fiber.Ctx) +│ └── 解析 ID 和上架状态 → 调用 Service.UpdateShelfStatus() → 返回结果 +└── UpdateRetailPrice(c *fiber.Ctx) + └── 解析 ID 和零售价 → 调用 Service.UpdateRetailPrice() → 返回结果 +``` + +### Service 层 +``` +PackageService +├── Create(ctx, req) +│ ├── 校验套餐编码唯一性 +│ ├── 校验虚流量配置 +│ │ ├── 启用时虚流量 > 0 +│ │ └── 虚流量 ≤ 真流量 +│ ├── 校验周期类型和时长 +│ │ ├── natural_month: 需要 duration_months +│ │ └── by_day: 需要 duration_days +│ ├── 查询套餐系列信息 +│ ├── 创建 Package 模型 +│ └── 调用 Store.Create() +├── List(ctx, req) +│ ├── 获取用户类型和店铺 ID +│ ├── 代理用户额外过滤 +│ │ └── INNER JOIN tb_shop_package_allocation +│ ├── 应用其他过滤条件 +│ └── 调用 Store.List() +├── Get(ctx, id) +│ └── 调用 Store.GetByID() +├── Update(ctx, id, req) +│ ├── 获取现有套餐 +│ ├── 更新字段 +│ └── 调用 Store.Update() +├── Delete(ctx, id) +│ └── 调用 Store.Delete() +├── UpdateStatus(ctx, id, status) +│ └── 更新状态字段 +├── UpdateShelfStatus(ctx, id, shelfStatus) +│ └── 更新上架状态字段 +└── UpdateRetailPrice(ctx, id, price) + └── 更新零售价字段 +``` + +### Store 层 +``` +PackageStore +├── Create(ctx, pkg) +│ └── db.Create(pkg) +├── GetByID(ctx, id) +│ └── db.First(&pkg, id) +├── GetByCode(ctx, code) +│ └── db.Where("package_code = ?", code).First(&pkg) +├── Update(ctx, pkg) +│ └── db.Save(pkg) +├── Delete(ctx, id) +│ └── db.Delete(&Package{}, id) +└── List(ctx, opts, filters) + ├── 构建基础查询 + ├── 代理用户 JOIN 分配表 + ├── 应用过滤条件 + │ ├── package_name (模糊搜索) + │ ├── series_id + │ ├── status + │ ├── shelf_status + │ └── package_type + ├── 计算总数 + ├── 应用分页 + └── 执行查询 +``` + +--- + +## 3. 数据模型关系 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ tb_package (套餐表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ package_code (UNIQUE) │ +│ package_name │ +│ series_id (FK → tb_package_series) │ +│ package_type (formal/addon) │ +│ duration_months │ +│ duration_days ← 关键字段 │ +│ calendar_type (natural_month/by_day) │ +│ real_data_mb │ +│ virtual_data_mb │ +│ enable_virtual_data │ +│ data_reset_cycle │ +│ expiry_base │ +│ cost_price │ +│ suggested_retail_price │ +│ shelf_status │ +│ status │ +│ created_at, updated_at, deleted_at │ +└──────────────────────────────────────────────────────────────┘ + │ + │ 1:N + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ tb_package_usage (套餐使用表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ order_id (FK) │ +│ package_id (FK → tb_package) │ +│ usage_type (single_card/device) │ +│ iot_card_id / device_id │ +│ data_limit_mb │ +│ data_usage_mb │ +│ activated_at, expires_at │ +│ status │ +│ priority │ +│ master_usage_id (加油包关联主套餐) │ +└──────────────────────────────────────────────────────────────┘ + │ + │ 1:N + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ tb_package_usage_daily_record (日记录表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ package_usage_id (FK) │ +│ date │ +│ daily_usage_mb │ +│ cumulative_usage_mb │ +└──────────────────────────────────────────────────────────────┘ + +┌──────────────────────────────────────────────────────────────┐ +│ tb_package_series (套餐系列表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ series_code (UNIQUE) │ +│ series_name │ +│ description │ +│ enable_one_time_commission │ +│ one_time_commission_config (JSONB) │ +│ status │ +└──────────────────────────────────────────────────────────────┘ + │ + │ 1:N + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ tb_shop_package_allocation (分配表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ shop_id (FK → tb_shop) │ +│ package_id (FK → tb_package) │ +│ retail_price │ +│ shelf_status │ +│ status │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 请求流程示例 + +### 创建套餐(按天) + +``` +1. HTTP 请求 + POST /api/admin/packages + { + "package_code": "PKG_DAY_30", + "package_name": "30天套餐", + "calendar_type": "by_day", + "duration_days": 30, + ... + } + +2. Handler.Create() + ├─ 解析请求体 → CreatePackageRequest + └─ 调用 Service.Create(ctx, req) + +3. Service.Create() + ├─ 校验 package_code 唯一性 + │ └─ Store.GetByCode(ctx, "PKG_DAY_30") → nil ✓ + ├─ 校验虚流量配置 ✓ + ├─ 校验周期类型 + │ ├─ calendar_type = "by_day" ✓ + │ ├─ duration_days = 30 ✓ + │ └─ 校验通过 ✓ + ├─ 查询套餐系列 (可选) + ├─ 创建 Package 模型 + └─ Store.Create(ctx, pkg) + +4. Store.Create() + └─ db.Create(pkg) + └─ INSERT INTO tb_package (...) + +5. Service 返回 PackageResponse + └─ Handler 返回 HTTP 200 + +6. HTTP 响应 + { + "code": 0, + "data": { + "id": 1, + "package_code": "PKG_DAY_30", + "duration_days": 30, + ... + }, + "msg": "success" + } +``` + +--- + +## 5. 代理用户套餐查询流程 + +``` +1. 代理用户请求 + GET /api/admin/packages?status=1 + +2. Handler.List() + └─ Service.List(ctx, req) + +3. Service.List() + ├─ 获取用户类型: UserTypeAgent ✓ + ├─ 获取店铺 ID: 10 ✓ + └─ Store.List(ctx, opts, filters) + +4. Store.List() + ├─ 构建基础查询 + │ └─ SELECT * FROM tb_package + ├─ 检测代理用户 + │ └─ isAgent = true ✓ + ├─ 添加 JOIN 条件 + │ └─ INNER JOIN tb_shop_package_allocation + │ ON tb_shop_package_allocation.package_id = tb_package.id + │ AND tb_shop_package_allocation.deleted_at IS NULL + ├─ 添加 WHERE 条件 + │ └─ WHERE tb_shop_package_allocation.shop_id = 10 + │ AND tb_shop_package_allocation.status = 1 + ├─ 应用其他过滤 + │ └─ AND tb_package.status = 1 + ├─ 计算总数 + ├─ 应用分页 + └─ 执行查询 + +5. 返回结果 + └─ 只返回已分配给店铺 10 的套餐 +``` + +--- + +## 6. 字段验证流程 + +``` +CreatePackageRequest +├─ package_code +│ └─ validate: required, min=1, max=100 +│ └─ Service: 检查唯一性 +├─ package_name +│ └─ validate: required, min=1, max=255 +├─ calendar_type +│ └─ validate: oneof=natural_month by_day +│ └─ Service: 根据类型校验时长字段 +├─ duration_months +│ └─ validate: required, min=1, max=120 +│ └─ Service: 当 calendar_type=natural_month 时必填 +├─ duration_days +│ └─ validate: omitempty, min=1, max=3650 +│ └─ Service: 当 calendar_type=by_day 时必填 +├─ real_data_mb +│ └─ validate: omitempty, min=0 +├─ virtual_data_mb +│ └─ validate: omitempty, min=0 +│ └─ Service: 启用虚流量时必须 > 0 且 ≤ real_data_mb +├─ enable_virtual_data +│ └─ Service: 虚流量配置校验 +├─ cost_price +│ └─ validate: required, min=0 +└─ suggested_retail_price + └─ validate: omitempty, min=0 +``` + diff --git a/docs/commission-flow-diagram.md b/docs/commission-flow-diagram.md new file mode 100644 index 0000000..089cfba --- /dev/null +++ b/docs/commission-flow-diagram.md @@ -0,0 +1,319 @@ +# 差价佣金计算流程图 + +## 一、订单支付到佣金入账的完整流程 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 客户下单购买套餐 │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ┌────────▼────────┐ + │ 创建订单 │ + │ order.id = 123 │ + │ commission_status = 1 (待计算) + │ payment_status = 1 (待支付) + └────────┬────────┘ + │ + ┌────────▼────────┐ + │ 用户支付 │ + │ WalletPay() │ + └────────┬────────┘ + │ + ┌────────▼────────┐ + │ 更新订单状态 │ + │ payment_status = 2 (已支付) + │ paid_at = now │ + └────────┬────────┘ + │ + ┌────────▼────────────────────────┐ + │ 激活套餐 │ + │ activatePackage() │ + │ (分配套餐到卡/设备) │ + └────────┬────────────────────────┘ + │ + ┌────────▼────────────────────────┐ + │ 入队佣金计算任务 │ + │ enqueueCommissionCalculation() │ + │ task_type = "commission:calculate" + │ payload = {order_id: 123} │ + └────────┬────────────────────────┘ + │ + │ (异步处理) + │ + ┌────────────────────▼────────────────────┐ + │ Asynq Worker 处理任务 │ + │ HandleCommissionCalculation() │ + └────────────────────┬────────────────────┘ + │ + ┌────────▼────────────────────┐ + │ CalculateCommission() │ + │ 开启数据库事务 │ + └────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ 1. 检查订单佣金状态 │ + │ if commission_status == 2 → 跳过 │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ 2. 计算成本价差佣金 │ + │ CalculateCostDiffCommission() │ + └────────────────────┬────────────────────┘ + │ + ▼ +``` + +## 二、成本价差佣金计算详细流程 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ CalculateCostDiffCommission(order) │ +└────────────────────┬─────────────────────────────────────────┘ + │ + ┌────────────▼────────────┐ + │ 获取销售店铺信息 │ + │ seller_shop_id = 10 │ + └────────────┬────────────┘ + │ + ┌────────────▼────────────────────────────┐ + │ 计算销售端利润 │ + │ profit = total_amount - seller_cost_price + │ profit = 200 - 130 = 70元 │ + │ 为销售店铺创建佣金记录 │ + │ commission_record { │ + │ shop_id: 10, │ + │ amount: 70, │ + │ commission_source: "cost_diff" │ + │ } │ + └────────────┬────────────────────────────┘ + │ + ┌────────────▼────────────────────────────┐ + │ 链式向上计算 │ + │ current_shop_id = seller_shop_id │ + │ child_cost_price = seller_cost_price │ + └────────────┬────────────────────────────┘ + │ + ┌────────────▼────────────────────────────┐ + │ 循环:while current_shop_id != null │ + └────────────┬────────────────────────────┘ + │ + ┌────────────▼────────────────────────────┐ + │ 1. 获取上级店铺 │ + │ parent_shop_id = current_shop.parent_id + │ if parent_shop_id == null → 结束 │ + └────────────┬────────────────────────────┘ + │ + ┌────────────▼────────────────────────────┐ + │ 2. 查询上级的套餐分配配置 │ + │ allocation = ShopPackageAllocation │ + │ where shop_id = parent_shop_id │ + │ and package_id = order.package_id │ + └────────────┬────────────────────────────┘ + │ + ├─ 未找到 ──┐ + │ │ + │ ┌──────▼──────────────────┐ + │ │ 链路断裂处理 │ + │ │ 创建待审佣金记录 │ + │ │ status = 99 │ + │ │ amount = 0 │ + │ │ remark = "套餐系列未分配" │ + │ │ 结束循环 │ + │ └─────────────────────────┘ + │ + └─ 找到 ──┐ + │ + ┌─────────────────────▼──────────────────┐ + │ 3. 计算差价 │ + │ my_cost_price = allocation.cost_price + │ profit = child_cost_price - my_cost_price + │ profit = 130 - 120 = 10元 │ + └─────────────────────┬──────────────────┘ + │ + ┌─────────────────────▼──────────────────┐ + │ 4. 如果差价 > 0,创建佣金记录 │ + │ commission_record { │ + │ shop_id: parent_shop_id, │ + │ amount: profit, │ + │ commission_source: "cost_diff" │ + │ } │ + └─────────────────────┬──────────────────┘ + │ + ┌─────────────────────▼──────────────────┐ + │ 5. 更新变量,继续向上 │ + │ child_cost_price = my_cost_price │ + │ current_shop_id = parent_shop_id │ + │ 继续循环 │ + └─────────────────────┬──────────────────┘ + │ + ┌─────────▴─────────┐ + │ 返回佣金记录列表 │ + └───────────────────┘ +``` + +## 三、佣金入账流程 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 对每条佣金记录调用 creditCommissionInTx() │ +└────────────────────┬─────────────────────────────────────────┘ + │ + ┌────────────▼────────────────────────┐ + │ 1. 获取店铺的分佣钱包 │ + │ wallet = AgentWallet │ + │ where shop_id = record.shop_id │ + │ and wallet_type = 'commission' │ + └────────────┬────────────────────────┘ + │ + ┌────────────▼────────────────────────┐ + │ 2. 增加钱包余额 │ + │ wallet.balance += record.amount │ + │ wallet.version += 1 │ + │ (乐观锁防并发) │ + └────────────┬────────────────────────┘ + │ + ┌────────────▼────────────────────────┐ + │ 3. 更新佣金记录 │ + │ commission_record.status = 3 │ + │ (CommissionStatusReleased) │ + │ commission_record.released_at = now + │ commission_record.balance_after │ + │ = balance_before + amount │ + └────────────┬────────────────────────┘ + │ + ┌────────────▼────────────────────────┐ + │ 4. 创建钱包交易记录 │ + │ transaction { │ + │ agent_wallet_id: wallet.id, │ + │ transaction_type: 'commission',│ + │ amount: record.amount, │ + │ balance_before: ..., │ + │ balance_after: ..., │ + │ reference_type: 'commission', │ + │ reference_id: record.id │ + │ } │ + └────────────┬────────────────────────┘ + │ + ┌────────────▼────────────────────────┐ + │ 5. 事务提交 │ + │ 佣金入账完成 │ + └────────────────────────────────────┘ +``` + +## 四、订单状态转换图 + +``` + ┌─────────────────┐ + │ 订单创建 │ + └────────┬────────┘ + │ + ┌────────▼────────────────────┐ + │ commission_status = 1 │ + │ payment_status = 1 │ + │ (待计算, 待支付) │ + └────────┬────────────────────┘ + │ + ┌────────▼────────────────────┐ + │ 用户支付 │ + └────────┬────────────────────┘ + │ + ┌────────▼────────────────────┐ + │ payment_status = 2 │ + │ (已支付) │ + │ commission_status = 1 │ + │ (待计算) │ + └────────┬────────────────────┘ + │ + ┌────────▼────────────────────┐ + │ 异步佣金计算 │ + └────────┬────────────────────┘ + │ + ┌────────▼────────────────────┐ + │ commission_status = 2 │ + │ (已计算) │ + │ 佣金已入账到代理钱包 │ + └────────────────────────────┘ +``` + +## 五、佣金记录状态转换图 + +``` + ┌──────────────────┐ + │ 佣金记录创建 │ + │ status = 3 │ + │ (已发放) │ + └────────┬─────────┘ + │ + ┌────────▼──────────────────┐ + │ 入账到代理钱包 │ + │ released_at = now │ + │ balance_after = ... │ + └────────┬──────────────────┘ + │ + ┌────────▼──────────────────┐ + │ 佣金可用于提现 │ + │ 或继续分配给下级 │ + └───────────────────────────┘ + + ┌──────────────────┐ + │ 链路断裂情况 │ + │ status = 99 │ + │ (待人工修正) │ + └────────┬─────────┘ + │ + ┌────────▼──────────────────┐ + │ 平台人工审核 │ + │ 修正或失效 │ + └───────────────────────────┘ +``` + +## 六、代理链佣金分配示例 + +``` +平台 (基础成本价: 100元) + │ + ├─ 代理A (成本价: 120元) + │ │ + │ ├─ 代理A1 (成本价: 130元) + │ │ │ + │ │ └─ 代理A1a (成本价: 140元) + │ │ │ + │ │ └─ 客户购买,售价: 200元 + │ │ + │ └─ 代理A2 (成本价: 135元) + │ + └─ 代理B (成本价: 125元) + +订单信息: + seller_shop_id = A1a (代理A1a) + seller_cost_price = 140元 + total_amount = 200元 + +佣金计算结果: + ┌─────────────────────────────────────────┐ + │ A1a (销售店铺) │ + │ 销售利润 = 200 - 140 = 60元 │ + │ 佣金记录: amount=60, commission_source=cost_diff + └─────────────────────────────────────────┘ + + ┌─────────────────────────────────────────┐ + │ A1 (A1a的上级) │ + │ 差价 = 140 - 130 = 10元 │ + │ 佣金记录: amount=10, commission_source=cost_diff + └─────────────────────────────────────────┘ + + ┌─────────────────────────────────────────┐ + │ A (A1的上级) │ + │ 差价 = 130 - 120 = 10元 │ + │ 佣金记录: amount=10, commission_source=cost_diff + └─────────────────────────────────────────┘ + + ┌─────────────────────────────────────────┐ + │ 平台 (A的上级) │ + │ 差价 = 120 - 100 = 20元 │ + │ (平台收入,不作为佣金) │ + └─────────────────────────────────────────┘ + +总计: 60 + 10 + 10 + 20 = 100元 ✓ +``` + diff --git a/docs/commission-quick-reference.md b/docs/commission-quick-reference.md new file mode 100644 index 0000000..10cf84c --- /dev/null +++ b/docs/commission-quick-reference.md @@ -0,0 +1,283 @@ +# 差价佣金快速参考指南 + +## 快速查询表 + +### 1. 关键文件位置 + +| 功能 | 文件 | 行号 | +|------|------|------| +| 差价佣金计算 | `internal/service/commission_calculation/service.go` | 121-225 | +| 佣金入账 | `internal/service/commission_calculation/service.go` | 641-687 | +| 佣金计算触发 | `internal/service/order/service.go` | 2132-2150 | +| 订单支付完成 | `internal/service/order/service.go` | 1431-1623 | +| 异步任务处理 | `internal/task/commission_calculation.go` | 40-62 | +| 订单模型 | `internal/model/order.go` | 1-138 | +| 佣金记录模型 | `internal/model/commission.go` | 1-46 | +| 业务文档 | `docs/commission-package-model.md` | - | + +### 2. 关键常量 + +```go +// 任务类型 +constants.TaskTypeCommission = "commission:calculate" + +// 佣金来源 +model.CommissionSourceCostDiff = "cost_diff" // 成本价差 +model.CommissionSourceOneTime = "one_time" // 一次性佣金 + +// 订单佣金状态 +model.CommissionStatusPending = 1 // 待计算 +model.CommissionStatusCalculated = 2 // 已计算 + +// 订单支付状态 +model.PaymentStatusPending = 1 // 待支付 +model.PaymentStatusPaid = 2 // 已支付 +model.PaymentStatusCancelled = 3 // 已取消 +model.PaymentStatusRefunded = 4 // 已退款 + +// 佣金记录状态 +constants.CommissionStatusReleased = 3 // 已发放 +constants.CommissionStatusInvalid = 4 // 已失效 +constants.CommissionStatusPendingReview = 99 // 待人工修正 +``` + +### 3. 关键数据库表 + +| 表名 | 用途 | 关键字段 | +|------|------|---------| +| `tb_order` | 订单 | `commission_status`, `payment_status`, `seller_shop_id`, `seller_cost_price` | +| `tb_commission_record` | 佣金记录 | `shop_id`, `order_id`, `amount`, `status`, `released_at` | +| `tb_agent_wallet` | 代理钱包 | `shop_id`, `balance`, `wallet_type` | +| `tb_agent_wallet_transaction` | 钱包交易 | `agent_wallet_id`, `transaction_type`, `amount` | +| `tb_shop_package_allocation` | 套餐分配 | `shop_id`, `package_id`, `cost_price` | + +--- + +## 常见问题快速查询 + +### Q1: 差价佣金什么时候计算? + +**A**: 订单支付成功后立即入队异步任务,由 Worker 异步计算。 + +**代码位置**: `internal/service/order/service.go:1621` + +```go +s.enqueueCommissionCalculation(ctx, orderID) +``` + +### Q2: 差价佣金的计算公式是什么? + +**A**: `差价佣金 = 下级成本价 - 自己成本价` + +**代码位置**: `internal/service/commission_calculation/service.go:203` + +```go +profit := childCostPrice - myCostPrice +``` + +### Q3: 如何查询某个订单的佣金记录? + +**A**: 查询 `tb_commission_record` 表,按 `order_id` 过滤。 + +```sql +SELECT * FROM tb_commission_record +WHERE order_id = ? +ORDER BY created_at DESC; +``` + +### Q4: 佣金什么时候入账到代理钱包? + +**A**: 佣金记录创建后立即入账,通过 `creditCommissionInTx()` 方法。 + +**代码位置**: `internal/service/commission_calculation/service.go:641-687` + +### Q5: 链路断裂是什么意思? + +**A**: 上级代理未分配该套餐,导致佣金链中断。此时创建待审佣金记录(status=99),需平台人工处理。 + +**代码位置**: `internal/service/commission_calculation/service.go:173-200` + +### Q6: 如何追踪一个订单的佣金计算过程? + +**A**: +1. 查询订单:`SELECT * FROM tb_order WHERE id = ?` +2. 检查 `commission_status` 字段(1=待计算, 2=已计算) +3. 查询佣金记录:`SELECT * FROM tb_commission_record WHERE order_id = ?` +4. 查询钱包交易:`SELECT * FROM tb_agent_wallet_transaction WHERE reference_id = ? AND reference_type = 'commission'` + +### Q7: 代理钱包余额如何更新? + +**A**: 通过 `AgentWalletStore.AddBalanceWithTx()` 方法,使用乐观锁防并发。 + +**代码位置**: `internal/service/commission_calculation/service.go:651` + +```go +s.agentWalletStore.AddBalanceWithTx(ctx, tx, wallet.ID, record.Amount) +``` + +### Q8: 如何处理佣金计算失败? + +**A**: Asynq 会自动重试,最多重试次数由队列配置决定。如果最终失败,任务会进入死信队列。 + +**代码位置**: `internal/task/commission_calculation.go:40-62` + +### Q9: 一个订单可能产生多少条佣金记录? + +**A**: 取决于代理链的深度。最多为:销售店铺 + 所有上级店铺。 + +**示例**: 4级代理链 → 最多4条佣金记录 + +### Q10: 如何验证佣金计算的正确性? + +**A**: +1. 验证总金额:所有佣金记录的 `amount` 之和 = 订单总金额 - 平台成本价 +2. 验证链路:每条佣金记录的 `shop_id` 应该是代理链中的一个 +3. 验证状态:所有佣金记录的 `status` 应该是 3(已发放)或 99(待审) + +--- + +## 调试技巧 + +### 1. 查看订单的佣金计算状态 + +```sql +SELECT + id, order_no, commission_status, payment_status, + seller_shop_id, seller_cost_price, total_amount +FROM tb_order +WHERE id = ?; +``` + +### 2. 查看订单的所有佣金记录 + +```sql +SELECT + id, shop_id, amount, commission_source, status, released_at +FROM tb_commission_record +WHERE order_id = ? +ORDER BY shop_id; +``` + +### 3. 查看代理的钱包余额变化 + +```sql +SELECT + id, shop_id, balance, wallet_type, version +FROM tb_agent_wallet +WHERE shop_id = ?; +``` + +### 4. 查看代理的钱包交易记录 + +```sql +SELECT + id, agent_wallet_id, transaction_type, amount, + balance_before, balance_after, reference_type, reference_id +FROM tb_agent_wallet_transaction +WHERE shop_id = ? AND transaction_type = 'commission' +ORDER BY created_at DESC; +``` + +### 5. 查看待审佣金记录(链路断裂) + +```sql +SELECT + id, shop_id, order_id, amount, status, remark +FROM tb_commission_record +WHERE status = 99 +ORDER BY created_at DESC; +``` + +### 6. 查看异步任务队列状态 + +```bash +# 查看待处理任务 +redis-cli LRANGE asynq:queues:default 0 -1 + +# 查看已完成任务 +redis-cli LRANGE asynq:queues:completed 0 -1 + +# 查看失败任务 +redis-cli LRANGE asynq:queues:failed 0 -1 +``` + +--- + +## 性能优化建议 + +### 1. 批量查询优化 + +避免在循环中逐条查询,使用 `IN` 子句批量查询: + +```go +// ❌ 不好:N+1 查询 +for _, shopID := range shopIDs { + allocation, _ := store.GetByShopAndPackage(ctx, shopID, packageID) +} + +// ✅ 好:批量查询 +allocations, _ := store.GetByShopsAndPackage(ctx, shopIDs, packageID) +``` + +### 2. 事务优化 + +确保事务范围最小化,避免长事务: + +```go +// ✅ 好:事务只包含必要操作 +err = s.db.Transaction(func(tx *gorm.DB) error { + // 创建佣金记录 + // 更新钱包余额 + // 创建交易记录 + return nil +}) +``` + +### 3. 索引优化 + +确保以下字段有索引: +- `tb_order.id`, `tb_order.commission_status`, `tb_order.payment_status` +- `tb_commission_record.order_id`, `tb_commission_record.shop_id`, `tb_commission_record.status` +- `tb_agent_wallet.shop_id`, `tb_agent_wallet.wallet_type` + +--- + +## 常见错误排查 + +### 错误1: 佣金未入账 + +**症状**: `commission_status = 2` 但钱包余额未增加 + +**排查步骤**: +1. 检查 `tb_commission_record` 中是否有记录 +2. 检查记录的 `status` 是否为 3(已发放) +3. 检查 `tb_agent_wallet_transaction` 中是否有对应交易 +4. 查看应用日志中的错误信息 + +### 错误2: 链路断裂 + +**症状**: `tb_commission_record.status = 99`,`remark` 包含"套餐系列未分配" + +**排查步骤**: +1. 检查上级代理是否分配了该套餐 +2. 检查 `tb_shop_package_allocation` 表中是否有记录 +3. 平台管理员需要手动分配套餐或修正佣金记录 + +### 错误3: 佣金计算失败 + +**症状**: 异步任务失败,订单 `commission_status` 仍为 1 + +**排查步骤**: +1. 查看应用日志中的错误信息 +2. 检查订单数据是否完整(`seller_shop_id`, `seller_cost_price` 等) +3. 检查数据库连接是否正常 +4. 手动重试任务或联系技术支持 + +--- + +## 相关文档 + +- [套餐与佣金业务模型](commission-package-model.md) - 完整的业务规则说明 +- [差价佣金搜索结果](commission-search-result.md) - 详细的代码位置和实现说明 +- [差价佣金流程图](commission-flow-diagram.md) - 可视化的流程图 + diff --git a/docs/commission-search-result.md b/docs/commission-search-result.md new file mode 100644 index 0000000..9f9d0d5 --- /dev/null +++ b/docs/commission-search-result.md @@ -0,0 +1,237 @@ +# 差价佣金计算和分配逻辑搜索结果 + +## 一、差价佣金的计算方式 + +### 1.1 计算公式 + +差价佣金 = 下级成本价 - 自己成本价 + +### 1.2 计算流程 + +**文件位置**: `internal/service/commission_calculation/service.go:121-225` + +```go +func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.Order) ([]*model.CommissionRecord, error) +``` + +**核心逻辑**: + +1. **销售端利润计算** (第138-153行) + - 销售利润 = 订单总金额 - 销售成本价 + - 如果销售利润 > 0,为销售店铺创建一条佣金记录 + +2. **链式向上计算** (第163-222行) + - 从销售店铺开始,逐级向上遍历代理链 + - 对于每一级代理: + - 获取该代理的成本价(从 ShopPackageAllocation 表) + - 计算差价:childCostPrice - myCostPrice + - 如果差价 > 0,创建佣金记录 + +3. **链路断裂处理** (第173-200行) + - 如果上级代理未分配该套餐,创建待审佣金记录(status=99) + - 记录备注:套餐系列未分配给该代理,需人工核查 + +### 1.3 示例 + +``` +平台基础成本价: 100元 + ↓ +分配给代理A,成本价: 120元 + ├─ A的利润: 120 - 100 = 20元 + ↓ +分配给代理A1,成本价: 130元 + ├─ A1的利润: 130 - 120 = 10元 + ├─ A的差价佣金: 130 - 120 = 10元 + ↓ +A1销售给客户,售价: 200元 + ├─ A1的销售利润: 200 - 130 = 70元 + ├─ A的差价佣金: 130 - 120 = 10元 + ├─ 平台收入: 120元 +``` + +--- + +## 二、差价佣金的发放条件 + +### 2.1 触发条件 + +**文件位置**: `internal/service/order/service.go:1621` 和 `internal/task/commission_calculation.go` + +差价佣金在以下情况触发: + +1. **订单支付成功** + - 支付状态从 `PaymentStatusPending` (1) 变为 `PaymentStatusPaid` (2) + - 触发点:`WalletPay()` 方法完成后调用 `enqueueCommissionCalculation()` + +2. **异步任务处理** + - 任务类型: `commission:calculate` + - 处理器: `CommissionCalculationHandler.HandleCommissionCalculation()` + - 队列: Asynq 异步任务队列 + +### 2.2 发放流程 + +**文件位置**: `internal/service/commission_calculation/service.go:74-119` + +```go +func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error +``` + +**步骤**: + +1. **检查订单佣金状态** (第81-84行) + - 如果 `commission_status == 2` (已计算),跳过 + - 防止重复计算 + +2. **计算成本价差佣金** (第86-98行) + - 调用 `CalculateCostDiffCommission()` 获取佣金记录列表 + - 逐条创建佣金记录到 `tb_commission_record` 表 + - 调用 `creditCommissionInTx()` 入账到代理钱包 + +3. **触发一次性佣金** (第100-111行) + - 仅非代购订单触发 + - 根据订单类型(单卡/设备)调用对应方法 + +4. **更新订单佣金状态** (第113-115行) + - 将 `commission_status` 更新为 `2` (已计算) + +### 2.3 佣金入账 + +**文件位置**: `internal/service/commission_calculation/service.go:641-687` + +```go +func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record *model.CommissionRecord) error +``` + +**入账步骤**: + +1. 获取店铺的分佣钱包 (wallet_type = 'commission') +2. 增加钱包余额 +3. 更新佣金记录状态为 `CommissionStatusReleased` (3) +4. 记录入账时间 `released_at` +5. 创建钱包交易记录 (transaction_type = 'commission') + +--- + +## 三、差价佣金与订单状态的关系 + +### 3.1 订单状态定义 + +**文件位置**: `internal/model/order.go:106-110` + +```go +// 佣金状态常量 +const ( + CommissionStatusPending = 1 // 待计算 + CommissionStatusCalculated = 2 // 已计算 +) +``` + +### 3.2 订单支付状态 + +**文件位置**: `internal/model/order.go:98-104` + +```go +// 支付状态常量 +const ( + PaymentStatusPending = 1 // 待支付 + PaymentStatusPaid = 2 // 已支付 + PaymentStatusCancelled = 3 // 已取消 + PaymentStatusRefunded = 4 // 已退款 +) +``` + +### 3.3 状态转换关系 + +``` +订单创建 + ↓ +commission_status = 1 (待计算) +payment_status = 1 (待支付) + ↓ +用户支付 + ↓ +payment_status = 2 (已支付) + ↓ +触发佣金计算任务 + ↓ +异步处理:CalculateCommission() + ↓ +commission_status = 2 (已计算) +佣金入账到代理钱包 +``` + +### 3.4 佣金记录状态 + +**文件位置**: `internal/model/commission.go:40-46` 和 `pkg/constants/iot.go:180-187` + +```go +// 佣金记录状态(tb_commission_record.status) +const ( + CommissionStatusReleased = 3 // 已发放 + CommissionStatusInvalid = 4 // 已失效 + CommissionStatusPendingReview = 99 // 待人工修正(链路断裂) +) + +// 代理钱包交易状态(tb_agent_wallet_transaction.status) +const ( + CommissionStatusFrozen = 1 // 已冻结 + CommissionStatusUnfreezing = 2 // 解冻中 + CommissionStatusReleased = 3 // 已发放 + CommissionStatusInvalid = 4 // 已失效 + CommissionStatusPendingReview = 99 // 待人工修正 +) +``` + +### 3.5 关键字段 + +**订单表 (tb_order)**: +- `commission_status`: 佣金计算状态 +- `commission_config_version`: 佣金配置版本快照 +- `seller_shop_id`: 销售店铺ID(用于成本价差佣金) +- `seller_cost_price`: 销售成本价(用于计算利润) +- `series_id`: 系列ID(用于查询分配配置) + +**佣金记录表 (tb_commission_record)**: +- `shop_id`: 佣金归属店铺 +- `order_id`: 关联订单 +- `commission_source`: 佣金来源 (cost_diff/one_time) +- `amount`: 佣金金额 +- `status`: 佣金状态 +- `released_at`: 入账时间 +- `balance_after`: 入账后钱包余额 + +--- + +## 四、关键代码位置总结 + +| 功能 | 文件位置 | 方法 | +|------|---------|------| +| 差价佣金计算 | `internal/service/commission_calculation/service.go` | `CalculateCostDiffCommission()` | +| 佣金入账 | `internal/service/commission_calculation/service.go` | `creditCommissionInTx()` | +| 佣金计算触发 | `internal/service/order/service.go` | `enqueueCommissionCalculation()` | +| 异步任务处理 | `internal/task/commission_calculation.go` | `HandleCommissionCalculation()` | +| 订单支付完成 | `internal/service/order/service.go` | `WalletPay()` | +| 模型定义 | `internal/model/commission.go` | `CommissionRecord` | +| 模型定义 | `internal/model/order.go` | `Order` | +| 业务文档 | `docs/commission-package-model.md` | 完整的佣金业务模型说明 | + +--- + +## 五、相关常量 + +**文件位置**: `pkg/constants/constants.go` 和 `pkg/constants/iot.go` + +```go +// 任务类型 +TaskTypeCommission = "commission:calculate" + +// 佣金来源 +CommissionSourceCostDiff = "cost_diff" // 成本价差 +CommissionSourceOneTime = "one_time" // 一次性佣金 + +// 佣金状态 +CommissionStatusReleased = 3 // 已发放 +CommissionStatusInvalid = 4 // 已失效 +CommissionStatusPendingReview = 99 // 待人工修正 +``` + diff --git a/docs/order_status_commission_analysis.md b/docs/order_status_commission_analysis.md new file mode 100644 index 0000000..ac124cd --- /dev/null +++ b/docs/order_status_commission_analysis.md @@ -0,0 +1,456 @@ +# 订单状态与佣金系统分析报告 + +## 一、订单状态定义 + +### 1.1 订单支付状态(Payment Status) +**文件**: `internal/model/order.go` (第 98-104 行) + +```go +const ( + PaymentStatusPending = 1 // 待支付 + PaymentStatusPaid = 2 // 已支付 + PaymentStatusCancelled = 3 // 已取消 + PaymentStatusRefunded = 4 // 已退款 +) +``` + +**数据库字段**: `tb_order.payment_status` (int) + +**说明**: +- 订单创建时默认为 `PaymentStatusPending`(待支付) +- 支付成功后更新为 `PaymentStatusPaid`(已支付) +- 待支付订单超时 30 分钟自动取消,状态变为 `PaymentStatusCancelled` +- 退款后状态变为 `PaymentStatusRefunded` + +### 1.2 订单佣金状态(Commission Status) +**文件**: `internal/model/order.go` (第 106-110 行) + +```go +const ( + CommissionStatusPending = 1 // 待计算 + CommissionStatusCalculated = 2 // 已计算 +) +``` + +**数据库字段**: `tb_order.commission_status` (int) + +**说明**: +- 订单创建时默认为 `CommissionStatusPending`(待计算) +- 佣金计算完成后更新为 `CommissionStatusCalculated`(已计算) +- 佣金计算是异步任务,通过 Asynq 队列处理 + +--- + +## 二、订单状态流转逻辑 + +### 2.1 订单创建流程 +**文件**: `internal/service/order/service.go` + +#### 支付方式决定初始状态: + +**1. 线下支付(offline)- 平台代购** +- 创建订单时直接设置 `PaymentStatus = PaymentStatusPaid`(已支付) +- 立即激活套餐 +- 立即入队佣金计算任务 +- 代码位置: 第 189-209 行、第 464-502 行 + +**2. 钱包支付(wallet)- 代理自购或代购** +- 创建订单时直接设置 `PaymentStatus = PaymentStatusPaid`(已支付) +- 在事务中完成:创建订单 → 扣款 → 激活套餐 +- 代码位置: 第 210-289 行、第 504-573 行 + +**3. 微信/支付宝支付(wechat/alipay)- H5 端** +- 创建订单时设置 `PaymentStatus = PaymentStatusPending`(待支付) +- 设置 `ExpiresAt` 为 30 分钟后(用于自动取消) +- 支付成功后由支付回调更新状态为 `PaymentStatusPaid` +- 代码位置: 第 754-927 行 + +### 2.2 订单超时自动取消 +**文件**: `internal/service/order/service.go` (第 1314-1349 行) + +```go +func (s *Service) CancelExpiredOrders(ctx context.Context) (int, error) { + // 查询超时订单(expires_at < now) + orders, err := s.orderStore.FindExpiredOrders(ctx, constants.OrderExpireBatchSize) + // 批量取消订单 + for _, order := range orders { + s.cancelOrder(ctx, order) + } +} +``` + +**取消逻辑** (第 1351-1385 行): +- 只有 `PaymentStatusPending` 的订单才能取消 +- 更新状态为 `PaymentStatusCancelled` +- 清除 `ExpiresAt` 字段 +- 如果是钱包支付,解冻钱包余额 + +**触发方式**: +- Asynq Scheduler 每分钟执行一次 +- 常量: `constants.OrderExpireTimeout = 30 * time.Minute` + +--- + +## 三、佣金系统与订单状态的关系 + +### 3.1 佣金计算触发条件 +**文件**: `internal/service/commission_calculation/service.go` (第 74-119 行) + +```go +func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error { + // 1. 检查订单佣金状态 + if order.CommissionStatus == model.CommissionStatusCalculated { + return nil // 已计算,跳过 + } + + // 2. 计算成本价差佣金 + costDiffRecords, err := s.CalculateCostDiffCommission(ctx, order) + + // 3. 入账佣金 + for _, record := range costDiffRecords { + s.creditCommissionInTx(ctx, tx, record) + } + + // 4. 触发一次性佣金(仅非代购订单) + if !order.IsPurchaseOnBehalf { + s.triggerOneTimeCommissionForCardInTx(ctx, tx, order, *order.IotCardID) + } + + // 5. 更新订单佣金状态为已计算 + tx.Model(&model.Order{}).Update("commission_status", CommissionStatusCalculated) +} +``` + +**触发时机**: +- 线下支付(offline)订单:创建后立即入队 +- 钱包支付(wallet)订单:创建后立即入队 +- 微信/支付宝支付:支付成功回调后入队 + +### 3.2 佣金类型与订单关系 + +#### 成本价差佣金(Cost Diff Commission) +**文件**: `internal/service/commission_calculation/service.go` (第 121-230 行) + +```go +func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.Order) { + // 计算销售店铺的利润 + sellerProfit := order.TotalAmount - order.SellerCostPrice + + // 沿着代理链向上分配佣金 + // 每一级代理的佣金 = 该级成本价 - 下级成本价 +} +``` + +**关键字段**: +- `order.SellerShopID`: 销售店铺ID(佣金归属) +- `order.SellerCostPrice`: 销售成本价(用于计算利润) +- `order.SeriesID`: 系列ID(用于查询分配配置) + +**佣金状态**: `CommissionStatusReleased`(已入账) + +#### 一次性佣金(One-Time Commission) +**触发条件**: +- 仅非代购订单触发(`!order.IsPurchaseOnBehalf`) +- 单卡首次购买或设备首次购买时触发 +- 代码位置: 第 102-111 行 + +--- + +## 四、钱包冻结与佣金提现流程 + +### 4.1 钱包冻结状态定义 +**文件**: `pkg/constants/wallet.go` (第 17-22 行) + +```go +const ( + AgentWalletStatusNormal = 1 // 正常 + AgentWalletStatusFrozen = 2 // 冻结(钱包整体冻结) + AgentWalletStatusClosed = 3 // 关闭 +) +``` + +**冻结余额字段**: +- `tb_agent_wallet.frozen_balance`: 冻结余额(分) +- `tb_agent_wallet.balance`: 总余额(分) +- 可用余额 = balance - frozen_balance + +### 4.2 佣金提现流程(3 阶段) + +#### 阶段 1: 代理发起提现申请 +**文件**: `internal/service/shop_commission/service.go` (第 517-649 行) + +```go +func (s *Service) CreateWithdrawalRequest(ctx context.Context, shopID uint, req *dto.CreateMyWithdrawalReq) { + // 1. 验证可用余额 + if req.Amount > wallet.GetAvailableBalance() { + return "可提现余额不足" + } + + // 2. 冻结余额(在事务中) + tx.Model(&AgentWallet{}). + Where("id = ? AND balance - frozen_balance >= ?", wallet.ID, req.Amount). + Updates(map[string]interface{}{ + "frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount), + }) + + // 3. 创建提现申请记录 + withdrawalRequest = &CommissionWithdrawalRequest{ + Status: 1, // 待审核 + } + + // 4. 创建钱包流水 + transaction = &AgentWalletTransaction{ + TransactionType: "withdrawal", + Amount: -req.Amount, + Status: TransactionStatusProcessing, + } +} +``` + +**状态**: `WithdrawalStatusPending`(待审核) + +#### 阶段 2: 平台审核通过 +**文件**: `internal/service/commission_withdrawal/service.go` (第 145-256 行) + +```go +func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdrawalReq) { + // 1. 检查冻结余额 + if wallet.FrozenBalance < amount { + return "钱包冻结余额不足" + } + + // 2. 从冻结余额扣款(在事务中) + s.agentWalletStore.DeductFrozenBalanceWithTx(ctx, tx, wallet.ID, amount) + + // 3. 创建提现交易流水 + transaction = &AgentWalletTransaction{ + TransactionType: "withdrawal", + Amount: -amount, + } + + // 4. 更新提现申请状态 + updates["status"] = WithdrawalStatusApproved +} +``` + +**状态**: `WithdrawalStatusApproved`(已通过) + +#### 阶段 3: 平台审核拒绝 +**文件**: `internal/service/commission_withdrawal/service.go` (第 258-310 行) + +```go +func (s *Service) Reject(ctx context.Context, id uint, req *dto.RejectWithdrawalReq) { + // 1. 解冻余额(在事务中) + s.agentWalletStore.UnfreezeBalanceWithTx(ctx, tx, wallet.ID, withdrawal.Amount) + + // 2. 创建退款交易流水 + transaction = &AgentWalletTransaction{ + TransactionType: "refund", + Amount: withdrawal.Amount, // 正数,增加可用余额 + } + + // 3. 更新提现申请状态 + updates["status"] = WithdrawalStatusRejected +} +``` + +**状态**: `WithdrawalStatusRejected`(已拒绝) + +### 4.3 钱包余额计算 +**文件**: `internal/service/shop_commission/service.go` (第 154-198 行) + +```go +func (s *Service) buildFundSummaryItem(shop, mainWallet, commissionWallet) { + // 佣金钱包 + balance = commissionWallet.Balance + frozenBalance = commissionWallet.FrozenBalance + + // 总佣金 = 可用 + 冻结 + 已提现 + totalCommission := balance + frozenBalance + withdrawnAmount + + // 未提现佣金 = 总佣金 - 已提现 + unwithdrawCommission := totalCommission - withdrawnAmount + + // 可提现佣金 = 可用 - 提现中 + availableCommission := balance - withdrawingAmount + + return ShopFundSummaryItem{ + TotalCommission: totalCommission, + WithdrawnCommission: withdrawnAmount, + UnwithdrawCommission: unwithdrawCommission, + FrozenCommission: frozenBalance, + WithdrawingCommission: withdrawingAmount, + AvailableCommission: availableCommission, + } +} +``` + +--- + +## 五、关键数据模型 + +### 5.1 订单模型 +**文件**: `internal/model/order.go` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `payment_status` | int | 支付状态(1-待支付 2-已支付 3-已取消 4-已退款) | +| `commission_status` | int | 佣金状态(1-待计算 2-已计算) | +| `seller_shop_id` | uint | 销售店铺ID(用于成本价差佣金) | +| `seller_cost_price` | int64 | 销售成本价(分) | +| `series_id` | uint | 系列ID(用于查询分配配置) | +| `is_purchase_on_behalf` | bool | 是否代购订单 | +| `expires_at` | time.Time | 订单过期时间(待支付订单) | + +### 5.2 佣金记录模型 +**文件**: `internal/model/commission.go` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `shop_id` | uint | 店铺ID(佣金归属) | +| `order_id` | uint | 订单ID | +| `commission_source` | string | 佣金来源(cost_diff-成本价差 one_time-一次性) | +| `amount` | int64 | 佣金金额(分) | +| `status` | int | 状态(1-已入账 2-已失效) | + +### 5.3 代理钱包模型 +**文件**: `internal/model/agent_wallet.go` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `balance` | int64 | 总余额(分) | +| `frozen_balance` | int64 | 冻结余额(分) | +| `status` | int | 钱包状态(1-正常 2-冻结 3-关闭) | +| `version` | int | 版本号(乐观锁) | + +### 5.4 提现申请模型 +**文件**: `internal/model/commission_withdrawal_request.go` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | int | 状态(1-待审核 2-已通过 3-已拒绝) | +| `amount` | int64 | 提现金额(分) | +| `frozen_balance` | int64 | 冻结余额(分) | + +--- + +## 六、关键常量 + +### 6.1 订单相关常量 +**文件**: `pkg/constants/constants.go` + +```go +const ( + OrderExpireTimeout = 30 * time.Minute // 订单超时时间 + OrderExpireBatchSize = 100 // 批量处理数量 +) +``` + +### 6.2 钱包相关常量 +**文件**: `pkg/constants/wallet.go` + +```go +const ( + AgentWalletStatusNormal = 1 // 正常 + AgentWalletStatusFrozen = 2 // 冻结 + AgentWalletStatusClosed = 3 // 关闭 + + AgentTransactionTypeWithdrawal = "withdrawal" // 提现 + TransactionStatusProcessing = 3 // 处理中 +) +``` + +--- + +## 七、订单状态与佣金的完整流程图 + +``` +订单创建 + ├─ 线下支付(offline) + │ ├─ PaymentStatus = PaymentStatusPaid(已支付) + │ ├─ 立即激活套餐 + │ ├─ 入队佣金计算 + │ └─ CommissionStatus = CommissionStatusCalculated + │ + ├─ 钱包支付(wallet) + │ ├─ PaymentStatus = PaymentStatusPaid(已支付) + │ ├─ 扣款 → 激活套餐(事务) + │ ├─ 入队佣金计算 + │ └─ CommissionStatus = CommissionStatusCalculated + │ + └─ 微信/支付宝(wechat/alipay) + ├─ PaymentStatus = PaymentStatusPending(待支付) + ├─ ExpiresAt = now + 30min + ├─ 支付成功回调 + │ ├─ PaymentStatus = PaymentStatusPaid + │ ├─ 激活套餐 + │ ├─ 入队佣金计算 + │ └─ CommissionStatus = CommissionStatusCalculated + └─ 超时自动取消(30min) + ├─ PaymentStatus = PaymentStatusCancelled + ├─ 解冻钱包余额 + └─ CommissionStatus = CommissionStatusPending(不计算) + +佣金计算流程 + ├─ 成本价差佣金 + │ ├─ 计算销售店铺利润 + │ ├─ 沿代理链向上分配 + │ └─ 入账到分佣钱包 + │ + └─ 一次性佣金(非代购订单) + ├─ 单卡/设备首次购买触发 + └─ 入账到分佣钱包 + +提现流程(3 阶段) + ├─ 阶段 1: 代理发起申请 + │ ├─ 验证可用余额 + │ ├─ 冻结余额(frozen_balance += amount) + │ ├─ 创建提现申请(Status = 待审核) + │ └─ 创建钱包流水(Status = 处理中) + │ + ├─ 阶段 2a: 平台审核通过 + │ ├─ 从冻结余额扣款(frozen_balance -= amount) + │ ├─ 创建提现交易流水 + │ └─ 更新提现申请(Status = 已通过) + │ + └─ 阶段 2b: 平台审核拒绝 + ├─ 解冻余额(frozen_balance -= amount) + ├─ 创建退款交易流水 + └─ 更新提现申请(Status = 已拒绝) +``` + +--- + +## 八、重要业务规则 + +### 8.1 订单支付规则 +1. **待支付订单自动取消**: 30 分钟未支付自动取消,解冻钱包余额 +2. **幂等性检查**: 防止同一买家对同一资源短时间内重复下单 +3. **强充要求**: 首次购买需满足最低充值要求 + +### 8.2 佣金计算规则 +1. **成本价差佣金**: 仅当 `seller_profit > 0` 时才计算 +2. **一次性佣金**: 仅非代购订单触发,且仅首次购买时触发 +3. **佣金链路**: 沿代理层级向上分配,若中间断裂则标记为待审核 + +### 8.3 提现规则 +1. **冻结机制**: 提现申请时冻结余额,防止重复提现 +2. **可用余额**: balance - frozen_balance,用于验证提现金额 +3. **手续费**: 提现时扣除手续费,实际到账 = 申请金额 - 手续费 + +--- + +## 九、相关文件索引 + +| 功能 | 文件 | 关键行数 | +|------|------|---------| +| 订单模型 | `internal/model/order.go` | 9-138 | +| 订单创建 | `internal/service/order/service.go` | 114-354, 359-674, 679-928 | +| 订单超时取消 | `internal/service/order/service.go` | 1314-1385 | +| 佣金计算 | `internal/service/commission_calculation/service.go` | 74-230 | +| 提现申请 | `internal/service/shop_commission/service.go` | 517-649 | +| 提现审核 | `internal/service/commission_withdrawal/service.go` | 145-310 | +| 钱包模型 | `internal/model/agent_wallet.go` | 9-94 | +| 钱包常量 | `pkg/constants/wallet.go` | 1-234 | +| 订单常量 | `pkg/constants/constants.go` | 153-166 | + diff --git a/docs/order_status_quick_reference.md b/docs/order_status_quick_reference.md new file mode 100644 index 0000000..d9b4a65 --- /dev/null +++ b/docs/order_status_quick_reference.md @@ -0,0 +1,196 @@ +# 订单状态与佣金系统 - 快速参考 + +## 订单支付状态速查表 + +| 状态值 | 常量名 | 说明 | 触发条件 | 后续操作 | +|-------|-------|------|---------|---------| +| 1 | `PaymentStatusPending` | 待支付 | 微信/支付宝支付创建订单 | 支付回调或30分钟超时 | +| 2 | `PaymentStatusPaid` | 已支付 | 线下/钱包支付或支付成功回调 | 激活套餐、计算佣金 | +| 3 | `PaymentStatusCancelled` | 已取消 | 待支付订单超时30分钟 | 解冻钱包余额 | +| 4 | `PaymentStatusRefunded` | 已退款 | 退款操作 | 返还金额 | + +## 订单佣金状态速查表 + +| 状态值 | 常量名 | 说明 | 触发条件 | 后续操作 | +|-------|-------|------|---------|---------| +| 1 | `CommissionStatusPending` | 待计算 | 订单创建时默认 | 异步任务计算佣金 | +| 2 | `CommissionStatusCalculated` | 已计算 | 佣金计算完成 | 佣金入账到分佣钱包 | + +## 订单创建流程速查 + +### 线下支付(offline) +``` +创建订单 → PaymentStatus=2(已支付) → 激活套餐 → 入队佣金计算 +``` + +### 钱包支付(wallet) +``` +创建订单 → PaymentStatus=2(已支付) → 扣款 → 激活套餐 → 入队佣金计算 +``` + +### 微信/支付宝(wechat/alipay) +``` +创建订单 → PaymentStatus=1(待支付) → 支付回调 → PaymentStatus=2 → 激活套餐 → 入队佣金计算 + ↓ + 30分钟超时 → PaymentStatus=3(已取消) → 解冻余额 +``` + +## 钱包冻结状态速查表 + +| 字段 | 说明 | 计算方式 | +|------|------|---------| +| `balance` | 总余额 | 充值 + 佣金入账 - 扣款 | +| `frozen_balance` | 冻结余额 | 提现申请时冻结 | +| 可用余额 | 可提现金额 | `balance - frozen_balance` | + +## 提现流程速查 + +### 阶段 1: 代理发起申请 +``` +验证可用余额 ✓ + ↓ +冻结余额 (frozen_balance += amount) + ↓ +创建提现申请 (Status=1 待审核) + ↓ +创建钱包流水 (Status=3 处理中) +``` + +### 阶段 2a: 平台审核通过 +``` +检查冻结余额 ✓ + ↓ +从冻结余额扣款 (frozen_balance -= amount) + ↓ +创建提现交易流水 + ↓ +更新提现申请 (Status=2 已通过) +``` + +### 阶段 2b: 平台审核拒绝 +``` +解冻余额 (frozen_balance -= amount) + ↓ +创建退款交易流水 + ↓ +更新提现申请 (Status=3 已拒绝) +``` + +## 佣金类型速查 + +| 佣金类型 | 常量值 | 触发条件 | 计算方式 | +|---------|-------|---------|---------| +| 成本价差 | `cost_diff` | 所有订单 | 销售价 - 销售成本价 | +| 一次性 | `one_time` | 非代购订单首次购买 | 根据配置 | + +## 关键代码位置速查 + +### 订单相关 +- 订单模型: `internal/model/order.go:98-110` +- 订单创建: `internal/service/order/service.go:114-928` +- 订单超时: `internal/service/order/service.go:1314-1385` + +### 佣金相关 +- 佣金计算: `internal/service/commission_calculation/service.go:74-119` +- 成本价差: `internal/service/commission_calculation/service.go:121-230` + +### 提现相关 +- 发起申请: `internal/service/shop_commission/service.go:517-649` +- 审核通过: `internal/service/commission_withdrawal/service.go:145-256` +- 审核拒绝: `internal/service/commission_withdrawal/service.go:258-310` + +## 常用常量速查 + +```go +// 订单超时 +constants.OrderExpireTimeout = 30 * time.Minute + +// 钱包状态 +constants.AgentWalletStatusNormal = 1 // 正常 +constants.AgentWalletStatusFrozen = 2 // 冻结 +constants.AgentWalletStatusClosed = 3 // 关闭 + +// 交易状态 +constants.TransactionStatusSuccess = 1 // 成功 +constants.TransactionStatusFailed = 2 // 失败 +constants.TransactionStatusProcessing = 3 // 处理中 + +// 提现状态 +constants.WithdrawalStatusPending = 1 // 待审核 +constants.WithdrawalStatusApproved = 2 // 已通过 +constants.WithdrawalStatusRejected = 3 // 已拒绝 +``` + +## 常见问题速查 + +### Q: 订单什么时候计算佣金? +A: 订单支付成功后立即入队佣金计算任务(异步)。线下/钱包支付创建时已支付,微信/支付宝支付在回调时支付。 + +### Q: 提现时冻结的余额什么时候解冻? +A: +- 审核通过:从冻结余额扣款(不解冻,直接扣除) +- 审核拒绝:解冻余额回到可用余额 + +### Q: 可用余额如何计算? +A: `可用余额 = 总余额 - 冻结余额` + +### Q: 订单超时自动取消后还会计算佣金吗? +A: 不会。取消的订单佣金状态保持为 `待计算`,不会入队计算。 + +### Q: 代购订单是否计算一次性佣金? +A: 不计算。一次性佣金仅对非代购订单触发。 + +### Q: 成本价差佣金如何沿代理链分配? +A: 从销售店铺开始,沿着 `parent_id` 向上逐级分配。每一级的佣金 = 该级成本价 - 下级成本价。 + +## 数据库查询速查 + +### 查询待支付订单 +```sql +SELECT * FROM tb_order WHERE payment_status = 1 AND expires_at < NOW(); +``` + +### 查询待计算佣金的订单 +```sql +SELECT * FROM tb_order WHERE commission_status = 1; +``` + +### 查询店铺可用余额 +```sql +SELECT balance - frozen_balance as available_balance +FROM tb_agent_wallet +WHERE shop_id = ? AND wallet_type = 'commission'; +``` + +### 查询提现中的金额 +```sql +SELECT SUM(amount) as withdrawing_amount +FROM tb_commission_withdrawal_request +WHERE shop_id = ? AND status = 1; +``` + +### 查询已提现的金额 +```sql +SELECT SUM(amount) as withdrawn_amount +FROM tb_commission_withdrawal_request +WHERE shop_id = ? AND status = 2; +``` + +## 业务规则速查 + +1. **订单支付规则** + - 待支付订单 30 分钟未支付自动取消 + - 取消时解冻钱包余额(如有) + - 防止重复下单(幂等性检查) + +2. **佣金计算规则** + - 成本价差佣金:仅当 `销售价 > 销售成本价` 时计算 + - 一次性佣金:仅非代购订单首次购买时触发 + - 佣金链路断裂时标记为待审核 + +3. **提现规则** + - 提现申请时冻结余额 + - 可用余额 = 总余额 - 冻结余额 + - 手续费 = 提现金额 × 费率 / 10000 + - 实际到账 = 提现金额 - 手续费 + diff --git a/docs/order_status_search_summary.md b/docs/order_status_search_summary.md new file mode 100644 index 0000000..f0e78f8 --- /dev/null +++ b/docs/order_status_search_summary.md @@ -0,0 +1,174 @@ +# 订单状态与佣金系统搜索结果总结 + +## 搜索范围 +- 关键词:订单状态、冻结、差价佣金、order status、frozen、commission +- 搜索范围:整个项目代码库 +- 文件类型:Go 源代码文件 + +## 核心发现 + +### 1. 订单状态定义(无冻结状态) +**重要发现**:订单本身**没有冻结状态**,只有支付状态和佣金状态。 + +#### 订单支付状态(4 种) +- `PaymentStatusPending = 1`:待支付 +- `PaymentStatusPaid = 2`:已支付 +- `PaymentStatusCancelled = 3`:已取消 +- `PaymentStatusRefunded = 4`:已退款 + +#### 订单佣金状态(2 种) +- `CommissionStatusPending = 1`:待计算 +- `CommissionStatusCalculated = 2`:已计算 + +**文件位置**:`internal/model/order.go:98-110` + +### 2. 冻结状态存在于钱包,不在订单 +**重要发现**:冻结状态是钱包的概念,用于提现流程。 + +#### 代理钱包状态(3 种) +- `AgentWalletStatusNormal = 1`:正常 +- `AgentWalletStatusFrozen = 2`:冻结(整个钱包冻结) +- `AgentWalletStatusClosed = 3`:关闭 + +#### 钱包冻结余额字段 +- `balance`:总余额 +- `frozen_balance`:冻结余额(用于提现) +- 可用余额 = balance - frozen_balance + +**文件位置**:`pkg/constants/wallet.go:17-22`、`internal/model/agent_wallet.go:15-16` + +### 3. 订单状态流转逻辑 + +#### 支付方式决定初始状态 +1. **线下支付(offline)**:创建时直接为 `PaymentStatusPaid` +2. **钱包支付(wallet)**:创建时直接为 `PaymentStatusPaid` +3. **微信/支付宝(wechat/alipay)**:创建时为 `PaymentStatusPending`,支付回调后变为 `PaymentStatusPaid` + +#### 订单超时自动取消 +- 待支付订单 30 分钟未支付自动取消 +- 状态变为 `PaymentStatusCancelled` +- 解冻钱包余额(如有) + +**文件位置**:`internal/service/order/service.go:1314-1385` + +### 4. 订单与佣金的关系 + +#### 佣金计算触发条件 +- 订单支付成功后立即入队佣金计算任务 +- 异步处理,不阻塞订单创建 + +#### 佣金类型 +1. **成本价差佣金**(cost_diff) + - 计算方式:销售价 - 销售成本价 + - 沿代理链向上分配 + +2. **一次性佣金**(one_time) + - 仅非代购订单触发 + - 仅首次购买时触发 + +**文件位置**:`internal/service/commission_calculation/service.go:74-230` + +### 5. 佣金提现流程(3 阶段) + +#### 阶段 1:代理发起申请 +- 验证可用余额 +- **冻结余额**(frozen_balance += amount) +- 创建提现申请(Status = 待审核) + +#### 阶段 2a:平台审核通过 +- 检查冻结余额 +- **从冻结余额扣款**(frozen_balance -= amount) +- 更新提现申请(Status = 已通过) + +#### 阶段 2b:平台审核拒绝 +- **解冻余额**(frozen_balance -= amount) +- 更新提现申请(Status = 已拒绝) + +**文件位置**: +- 发起申请:`internal/service/shop_commission/service.go:517-649` +- 审核通过:`internal/service/commission_withdrawal/service.go:145-256` +- 审核拒绝:`internal/service/commission_withdrawal/service.go:258-310` + +## 关键代码位置 + +| 功能 | 文件 | 行数 | +|------|------|------| +| 订单模型定义 | `internal/model/order.go` | 9-138 | +| 订单支付状态常量 | `internal/model/order.go` | 98-104 | +| 订单佣金状态常量 | `internal/model/order.go` | 106-110 | +| 订单创建逻辑 | `internal/service/order/service.go` | 114-928 | +| 订单超时取消 | `internal/service/order/service.go` | 1314-1385 | +| 佣金计算 | `internal/service/commission_calculation/service.go` | 74-230 | +| 提现申请 | `internal/service/shop_commission/service.go` | 517-649 | +| 提现审核 | `internal/service/commission_withdrawal/service.go` | 145-310 | +| 钱包模型 | `internal/model/agent_wallet.go` | 9-94 | +| 钱包常量 | `pkg/constants/wallet.go` | 1-234 | + +## 重要业务规则 + +### 订单支付规则 +1. 待支付订单 30 分钟未支付自动取消 +2. 取消时解冻钱包余额(如有) +3. 防止重复下单(幂等性检查) + +### 佣金计算规则 +1. 成本价差佣金:仅当 `销售价 > 销售成本价` 时计算 +2. 一次性佣金:仅非代购订单首次购买时触发 +3. 佣金链路断裂时标记为待审核 + +### 提现规则 +1. 提现申请时冻结余额 +2. 可用余额 = 总余额 - 冻结余额 +3. 手续费 = 提现金额 × 费率 / 10000 + +## 数据流向图 + +``` +订单创建 + ├─ 线下/钱包支付 → PaymentStatus=2(已支付) → 激活套餐 → 入队佣金计算 + └─ 微信/支付宝 → PaymentStatus=1(待支付) → 支付回调 → PaymentStatus=2 → 激活套餐 → 入队佣金计算 + ↓ + 30分钟超时 → PaymentStatus=3(已取消) + +佣金计算 + ├─ 成本价差佣金:销售价 - 销售成本价 → 沿代理链分配 → 入账分佣钱包 + └─ 一次性佣金:非代购订单首次购买 → 入账分佣钱包 + +提现流程 + ├─ 代理申请 → 冻结余额 → 创建申请(待审核) + ├─ 平台通过 → 从冻结余额扣款 → 更新申请(已通过) + └─ 平台拒绝 → 解冻余额 → 更新申请(已拒绝) +``` + +## 生成的文档 + +已生成两份详细文档: + +1. **order_status_commission_analysis.md** + - 完整的系统分析报告 + - 包含所有状态定义、流程、数据模型 + - 适合深入理解系统设计 + +2. **order_status_quick_reference.md** + - 快速参考指南 + - 包含速查表、常见问题、SQL 查询 + - 适合日常开发查询 + +## 关键发现总结 + +### ✅ 已确认 +- 订单有支付状态和佣金状态,但**没有冻结状态** +- 冻结状态存在于**代理钱包**,用于提现流程 +- 差价佣金通过 `seller_cost_price` 字段计算 +- 佣金与订单的关系通过 `commission_status` 字段维护 + +### ⚠️ 注意事项 +- 订单超时自动取消不会触发佣金计算 +- 代购订单不计算一次性佣金 +- 提现时冻结的余额在审核通过时直接扣款,不是解冻后再扣 + +### 📌 最佳实践 +- 查询订单时同时检查 `payment_status` 和 `commission_status` +- 查询可提现金额时使用 `balance - frozen_balance` +- 提现流程中使用事务确保数据一致性 + diff --git a/docs/package_architecture_diagram.md b/docs/package_architecture_diagram.md new file mode 100644 index 0000000..17cd10e --- /dev/null +++ b/docs/package_architecture_diagram.md @@ -0,0 +1,366 @@ +# 套餐接口架构图 + +## 1. 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ HTTP 请求 │ +│ POST /api/admin/packages │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────▼────────┐ + │ 认证中间件 │ + │ (AdminAuth) │ + └────────┬────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ 路由注册 (routes/admin.go) │ + │ registerPackageRoutes() │ + │ ↓ │ + │ internal/routes/package.go │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PackageHandler │ + │ (internal/handler/admin/package.go) │ + │ │ + │ - List() │ + │ - Create() │ + │ - Get() │ + │ - Update() │ + │ - Delete() │ + │ - UpdateStatus() │ + │ - UpdateShelfStatus() │ + │ - UpdateRetailPrice() │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PackageService │ + │ (internal/service/package/service.go) │ + │ │ + │ 业务逻辑: │ + │ - 虚流量校验 │ + │ - 周期类型校验 │ + │ - 代理权限检查 │ + │ - DTO 转换 │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PackageStore │ + │ (internal/store/postgres/...) │ + │ │ + │ - PackageStore │ + │ - PackageSeriesStore │ + │ - ShopPackageAllocationStore │ + │ - ShopSeriesAllocationStore │ + └────────────────────┬────────────────────┘ + │ + ┌────────────────────▼────────────────────┐ + │ PostgreSQL 数据库 │ + │ │ + │ - tb_package │ + │ - tb_package_series │ + │ - tb_shop_package_allocation │ + │ - tb_shop_series_allocation │ + └─────────────────────────────────────────┘ +``` + +--- + +## 2. 分层详细结构 + +### Handler 层 +``` +PackageHandler +├── List(c *fiber.Ctx) +│ └── 解析查询参数 → 调用 Service.List() → 返回分页结果 +├── Create(c *fiber.Ctx) +│ └── 解析请求体 → 调用 Service.Create() → 返回创建结果 +├── Get(c *fiber.Ctx) +│ └── 解析 ID 参数 → 调用 Service.Get() → 返回详情 +├── Update(c *fiber.Ctx) +│ └── 解析 ID 和请求体 → 调用 Service.Update() → 返回更新结果 +├── Delete(c *fiber.Ctx) +│ └── 解析 ID 参数 → 调用 Service.Delete() → 返回删除结果 +├── UpdateStatus(c *fiber.Ctx) +│ └── 解析 ID 和状态 → 调用 Service.UpdateStatus() → 返回结果 +├── UpdateShelfStatus(c *fiber.Ctx) +│ └── 解析 ID 和上架状态 → 调用 Service.UpdateShelfStatus() → 返回结果 +└── UpdateRetailPrice(c *fiber.Ctx) + └── 解析 ID 和零售价 → 调用 Service.UpdateRetailPrice() → 返回结果 +``` + +### Service 层 +``` +PackageService +├── Create(ctx, req) +│ ├── 校验套餐编码唯一性 +│ ├── 校验虚流量配置 +│ │ ├── 启用时虚流量 > 0 +│ │ └── 虚流量 ≤ 真流量 +│ ├── 校验周期类型和时长 +│ │ ├── natural_month: 需要 duration_months +│ │ └── by_day: 需要 duration_days +│ ├── 查询套餐系列信息 +│ ├── 创建 Package 模型 +│ └── 调用 Store.Create() +├── List(ctx, req) +│ ├── 获取用户类型和店铺 ID +│ ├── 代理用户额外过滤 +│ │ └── INNER JOIN tb_shop_package_allocation +│ ├── 应用其他过滤条件 +│ └── 调用 Store.List() +├── Get(ctx, id) +│ └── 调用 Store.GetByID() +├── Update(ctx, id, req) +│ ├── 获取现有套餐 +│ ├── 更新字段 +│ └── 调用 Store.Update() +├── Delete(ctx, id) +│ └── 调用 Store.Delete() +├── UpdateStatus(ctx, id, status) +│ └── 更新状态字段 +├── UpdateShelfStatus(ctx, id, shelfStatus) +│ └── 更新上架状态字段 +└── UpdateRetailPrice(ctx, id, price) + └── 更新零售价字段 +``` + +### Store 层 +``` +PackageStore +├── Create(ctx, pkg) +│ └── db.Create(pkg) +├── GetByID(ctx, id) +│ └── db.First(&pkg, id) +├── GetByCode(ctx, code) +│ └── db.Where("package_code = ?", code).First(&pkg) +├── Update(ctx, pkg) +│ └── db.Save(pkg) +├── Delete(ctx, id) +│ └── db.Delete(&Package{}, id) +└── List(ctx, opts, filters) + ├── 构建基础查询 + ├── 代理用户 JOIN 分配表 + ├── 应用过滤条件 + │ ├── package_name (模糊搜索) + │ ├── series_id + │ ├── status + │ ├── shelf_status + │ └── package_type + ├── 计算总数 + ├── 应用分页 + └── 执行查询 +``` + +--- + +## 3. 数据模型关系 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ tb_package (套餐表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ package_code (UNIQUE) │ +│ package_name │ +│ series_id (FK → tb_package_series) │ +│ package_type (formal/addon) │ +│ duration_months │ +│ duration_days ← 关键字段 │ +│ calendar_type (natural_month/by_day) │ +│ real_data_mb │ +│ virtual_data_mb │ +│ enable_virtual_data │ +│ data_reset_cycle │ +│ expiry_base │ +│ cost_price │ +│ suggested_retail_price │ +│ shelf_status │ +│ status │ +│ created_at, updated_at, deleted_at │ +└──────────────────────────────────────────────────────────────┘ + │ + │ 1:N + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ tb_package_usage (套餐使用表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ order_id (FK) │ +│ package_id (FK → tb_package) │ +│ usage_type (single_card/device) │ +│ iot_card_id / device_id │ +│ data_limit_mb │ +│ data_usage_mb │ +│ activated_at, expires_at │ +│ status │ +│ priority │ +│ master_usage_id (加油包关联主套餐) │ +└──────────────────────────────────────────────────────────────┘ + │ + │ 1:N + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ tb_package_usage_daily_record (日记录表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ package_usage_id (FK) │ +│ date │ +│ daily_usage_mb │ +│ cumulative_usage_mb │ +└──────────────────────────────────────────────────────────────┘ + +┌──────────────────────────────────────────────────────────────┐ +│ tb_package_series (套餐系列表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ series_code (UNIQUE) │ +│ series_name │ +│ description │ +│ enable_one_time_commission │ +│ one_time_commission_config (JSONB) │ +│ status │ +└──────────────────────────────────────────────────────────────┘ + │ + │ 1:N + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ tb_shop_package_allocation (分配表) │ +├──────────────────────────────────────────────────────────────┤ +│ id (PK) │ +│ shop_id (FK → tb_shop) │ +│ package_id (FK → tb_package) │ +│ retail_price │ +│ shelf_status │ +│ status │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 请求流程示例 + +### 创建套餐(按天) + +``` +1. HTTP 请求 + POST /api/admin/packages + { + "package_code": "PKG_DAY_30", + "package_name": "30天套餐", + "calendar_type": "by_day", + "duration_days": 30, + ... + } + +2. Handler.Create() + ├─ 解析请求体 → CreatePackageRequest + └─ 调用 Service.Create(ctx, req) + +3. Service.Create() + ├─ 校验 package_code 唯一性 + │ └─ Store.GetByCode(ctx, "PKG_DAY_30") → nil ✓ + ├─ 校验虚流量配置 ✓ + ├─ 校验周期类型 + │ ├─ calendar_type = "by_day" ✓ + │ ├─ duration_days = 30 ✓ + │ └─ 校验通过 ✓ + ├─ 查询套餐系列 (可选) + ├─ 创建 Package 模型 + └─ Store.Create(ctx, pkg) + +4. Store.Create() + └─ db.Create(pkg) + └─ INSERT INTO tb_package (...) + +5. Service 返回 PackageResponse + └─ Handler 返回 HTTP 200 + +6. HTTP 响应 + { + "code": 0, + "data": { + "id": 1, + "package_code": "PKG_DAY_30", + "duration_days": 30, + ... + }, + "msg": "success" + } +``` + +--- + +## 5. 代理用户套餐查询流程 + +``` +1. 代理用户请求 + GET /api/admin/packages?status=1 + +2. Handler.List() + └─ Service.List(ctx, req) + +3. Service.List() + ├─ 获取用户类型: UserTypeAgent ✓ + ├─ 获取店铺 ID: 10 ✓ + └─ Store.List(ctx, opts, filters) + +4. Store.List() + ├─ 构建基础查询 + │ └─ SELECT * FROM tb_package + ├─ 检测代理用户 + │ └─ isAgent = true ✓ + ├─ 添加 JOIN 条件 + │ └─ INNER JOIN tb_shop_package_allocation + │ ON tb_shop_package_allocation.package_id = tb_package.id + │ AND tb_shop_package_allocation.deleted_at IS NULL + ├─ 添加 WHERE 条件 + │ └─ WHERE tb_shop_package_allocation.shop_id = 10 + │ AND tb_shop_package_allocation.status = 1 + ├─ 应用其他过滤 + │ └─ AND tb_package.status = 1 + ├─ 计算总数 + ├─ 应用分页 + └─ 执行查询 + +5. 返回结果 + └─ 只返回已分配给店铺 10 的套餐 +``` + +--- + +## 6. 字段验证流程 + +``` +CreatePackageRequest +├─ package_code +│ └─ validate: required, min=1, max=100 +│ └─ Service: 检查唯一性 +├─ package_name +│ └─ validate: required, min=1, max=255 +├─ calendar_type +│ └─ validate: oneof=natural_month by_day +│ └─ Service: 根据类型校验时长字段 +├─ duration_months +│ └─ validate: required, min=1, max=120 +│ └─ Service: 当 calendar_type=natural_month 时必填 +├─ duration_days +│ └─ validate: omitempty, min=1, max=3650 +│ └─ Service: 当 calendar_type=by_day 时必填 +├─ real_data_mb +│ └─ validate: omitempty, min=0 +├─ virtual_data_mb +│ └─ validate: omitempty, min=0 +│ └─ Service: 启用虚流量时必须 > 0 且 ≤ real_data_mb +├─ enable_virtual_data +│ └─ Service: 虚流量配置校验 +├─ cost_price +│ └─ validate: required, min=0 +└─ suggested_retail_price + └─ validate: omitempty, min=0 +``` + diff --git a/docs/package_quick_navigation.md b/docs/package_quick_navigation.md new file mode 100644 index 0000000..9d56ded --- /dev/null +++ b/docs/package_quick_navigation.md @@ -0,0 +1,147 @@ +# 套餐接口快速导航 + +## 🎯 快速查找 + +### 按功能查找 + +| 需求 | 文件 | 行号 | +|------|------|------| +| 查看套餐数据模型 | `internal/model/package.go` | 28-56 | +| 查看套餐 API 请求/响应结构 | `internal/model/dto/package_dto.go` | 1-173 | +| 查看套餐 HTTP 处理器 | `internal/handler/admin/package.go` | 1-148 | +| 查看套餐业务逻辑 | `internal/service/package/service.go` | 1-745 | +| 查看套餐数据访问 | `internal/store/postgres/package_store.go` | 1-150 | +| 查看套餐 API 路由 | `internal/routes/package.go` | 1-78 | +| 查看路由注册入口 | `internal/routes/admin.go` | 71-78 | + +### 按字段查找 + +| 字段 | Model 位置 | DTO 位置 | 说明 | +|------|-----------|---------|------| +| `duration_days` | `package.go:46` | `package_dto.go:96` | 套餐天数(按天时必填) | +| `calendar_type` | `package.go:45` | `package_dto.go:95` | 套餐周期类型 | +| `duration_months` | `package.go:37` | `package_dto.go:79` | 套餐时长(月数) | +| `real_data_mb` | `package.go:38` | `package_dto.go:80` | 真流量额度 | +| `virtual_data_mb` | `package.go:39` | `package_dto.go:81` | 虚流量额度 | +| `enable_virtual_data` | `package.go:40` | `package_dto.go:82` | 是否启用虚流量 | + +### 按 API 端点查找 + +| 端点 | Handler 方法 | 路由文件 | 行号 | +|------|-------------|---------|------| +| `GET /packages` | `List()` | `package.go` | 15-21 | +| `POST /packages` | `Create()` | `package.go` | 23-29 | +| `GET /packages/:id` | `Get()` | `package.go` | 31-37 | +| `PUT /packages/:id` | `Update()` | `package.go` | 39-45 | +| `DELETE /packages/:id` | `Delete()` | `package.go` | 47-53 | +| `PATCH /packages/:id/status` | `UpdateStatus()` | `package.go` | 55-61 | +| `PATCH /packages/:id/shelf` | `UpdateShelfStatus()` | `package.go` | 63-69 | +| `PATCH /packages/:id/retail-price` | `UpdateRetailPrice()` | `package.go` | 71-77 | + +--- + +## 🔍 关键代码片段 + +### 1. 套餐周期类型校验 +**文件**:`internal/service/package/service.go:65-80` + +**用途**:创建套餐时验证 `calendar_type` 和 `duration_days` 的配合 + +```go +if calendarType == constants.PackageCalendarTypeByDay { + if req.DurationDays == nil || *req.DurationDays <= 0 { + return nil, errors.New(errors.CodeInvalidParam, "按天套餐必须提供有效的duration_days") + } +} +``` + +### 2. 虚流量配置校验 +**文件**:`internal/service/package/service.go:51-63` + +**用途**:验证虚流量配置的合法性 + +```go +if req.EnableVirtualData { + if *req.VirtualDataMB > realDataMB { + return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度") + } +} +``` + +### 3. 代理用户套餐过滤 +**文件**:`internal/store/postgres/package_store.go:57-66` + +**用途**:代理用户只能看到已分配的套餐 + +```go +if isAgent { + query = query.Joins("INNER JOIN tb_shop_package_allocation ..."). + Where("tb_shop_package_allocation.shop_id = ? AND tb_shop_package_allocation.status = ?", + shopID, constants.StatusEnabled) +} +``` + +--- + +## 📊 数据流向 + +``` +HTTP 请求 + ↓ +PackageHandler (internal/handler/admin/package.go) + ↓ +PackageService (internal/service/package/service.go) + ├─ 业务逻辑校验 + ├─ 调用 PackageStore + └─ 返回 DTO + ↓ +PackageStore (internal/store/postgres/package_store.go) + ├─ 数据库查询 + └─ 返回 Model + ↓ +HTTP 响应 (DTO) +``` + +--- + +## 🛠️ 常见操作 + +### 添加新的套餐字段 + +1. **Model 层**:`internal/model/package.go` 的 `Package` 结构体 +2. **DTO 层**:`internal/model/dto/package_dto.go` 的相关 Request/Response +3. **Service 层**:`internal/service/package/service.go` 的业务逻辑 +4. **Store 层**:`internal/store/postgres/package_store.go` 的查询条件 +5. **数据库**:迁移文件中添加新列 + +### 修改套餐 API 响应 + +1. 修改 `PackageResponse` 结构体(`package_dto.go:71-99`) +2. 修改 Service 的转换逻辑(`service.go` 中的 `toDTO()` 方法) +3. 测试 API 端点 + +### 添加新的套餐 API 端点 + +1. 在 `PackageHandler` 中添加方法(`internal/handler/admin/package.go`) +2. 在 `PackageService` 中添加业务逻辑(`internal/service/package/service.go`) +3. 在 `registerPackageRoutes()` 中注册路由(`internal/routes/package.go`) + +--- + +## 📝 相关文档 + +- **套餐系统升级**:`docs/package-system-upgrade/` +- **套餐与佣金业务模型**:`docs/commission-package-model.md` +- **API 文档生成规范**:`docs/api-documentation-guide.md` + +--- + +## 🔗 相关模块 + +| 模块 | 说明 | 文件 | +|------|------|------| +| **PackageSeries** | 套餐系列管理 | `internal/routes/package_series.go` | +| **PackageUsage** | 套餐使用记录 | `internal/routes/package_usage.go` | +| **ShopPackageAllocation** | 套餐分配给代理 | `internal/store/postgres/shop_package_allocation_store.go` | +| **ShopSeriesAllocation** | 系列分配给代理 | `internal/store/postgres/shop_series_allocation_store.go` | + diff --git a/docs/package_structure_summary.md b/docs/package_structure_summary.md new file mode 100644 index 0000000..2bd435b --- /dev/null +++ b/docs/package_structure_summary.md @@ -0,0 +1,349 @@ +# 后台管理套餐相关接口代码结构探索 + +## 📋 概览 + +套餐(Package/PackageSeries)相关功能采用标准的分层架构: +``` +Handler → Service → Store → Model +``` + +--- + +## 1️⃣ 核心文件位置 + +### Model 层(数据模型) +| 文件 | 路径 | 说明 | +|------|------|------| +| **Package** | `internal/model/package.go:28-56` | 套餐主模型,包含 `duration_days` 字段 | +| **PackageSeries** | `internal/model/package.go:10-26` | 套餐系列模型 | +| **PackageUsage** | `internal/model/package.go:58-90` | 套餐使用情况模型 | +| **PackageUsageDailyRecord** | `internal/model/package.go:92-107` | 套餐流量日记录模型 | + +### DTO 层(数据传输对象) +| 文件 | 路径 | 说明 | +|------|------|------| +| **PackageResponse** | `internal/model/dto/package_dto.go:71-99` | 套餐响应结构,包含 `duration_days` 字段 | +| **CreatePackageRequest** | `internal/model/dto/package_dto.go:3-19` | 创建套餐请求 | +| **UpdatePackageRequest** | `internal/model/dto/package_dto.go:21-36` | 更新套餐请求 | +| **PackageListRequest** | `internal/model/dto/package_dto.go:38-47` | 套餐列表请求 | +| **PackagePageResult** | `internal/model/dto/package_dto.go:125-131` | 套餐分页结果 | + +### Handler 层(HTTP 处理) +| 文件 | 路径 | 说明 | +|------|------|------| +| **PackageHandler** | `internal/handler/admin/package.go` | 套餐管理处理器 | +| 方法 | 行号 | 功能 | +| `List()` | 22-34 | 获取套餐列表 | +| `Create()` | 36-48 | 创建套餐 | +| `Get()` | 50-62 | 获取套餐详情 | +| `Update()` | 64-81 | 更新套餐 | +| `Delete()` | 83-94 | 删除套餐 | +| `UpdateStatus()` | 96-112 | 更新套餐状态 | +| `UpdateShelfStatus()` | 114-130 | 更新上架状态 | +| `UpdateRetailPrice()` | 132-148 | 更新零售价 | + +### Service 层(业务逻辑) +| 文件 | 路径 | 说明 | +|------|------|------| +| **Service** | `internal/service/package/service.go:19-38` | 套餐服务主类 | +| 依赖 | 说明 | | +| `packageStore` | 套餐数据访问 | | +| `packageSeriesStore` | 套餐系列数据访问 | | +| `packageAllocationStore` | 套餐分配数据访问 | | +| `shopSeriesAllocationStore` | 店铺系列分配数据访问 | | + +**关键方法**(`service.go` 第 40-80 行): +- `Create()` - 创建套餐,包含虚流量和周期类型校验 +- `List()` - 列表查询,支持代理用户过滤 +- `Get()` - 获取详情 +- `Update()` - 更新套餐 +- `Delete()` - 删除套餐 +- `UpdateStatus()` - 更新状态 +- `UpdateShelfStatus()` - 更新上架状态 +- `UpdateRetailPrice()` - 更新零售价 + +### Store 层(数据访问) +| 文件 | 路径 | 说明 | +|------|------|------| +| **PackageStore** | `internal/store/postgres/package_store.go:15-21` | 套餐数据访问 | +| 方法 | 行号 | 功能 | +| `Create()` | 23-25 | 创建套餐 | +| `GetByID()` | 27-33 | 按 ID 查询 | +| `GetByCode()` | 35-41 | 按编码查询 | +| `Update()` | 43-45 | 更新套餐 | +| `Delete()` | 47-49 | 删除套餐 | +| `List()` | 51-100+ | 列表查询(支持代理用户过滤) | + +**其他相关 Store**: +- `PackageSeriesStore` - `internal/store/postgres/package_series_store.go` +- `PackageUsageStore` - `internal/store/postgres/package_usage_store.go` +- `PackageUsageDailyRecordStore` - `internal/store/postgres/package_usage_daily_record_store.go` +- `ShopPackageAllocationStore` - `internal/store/postgres/shop_package_allocation_store.go` + +--- + +## 2️⃣ API 路由注册 + +### 主路由注册入口 +**文件**:`internal/routes/admin.go:71-78` + +```go +if handlers.PackageSeries != nil { + registerPackageSeriesRoutes(authGroup, handlers.PackageSeries, doc, basePath) +} +if handlers.Package != nil { + registerPackageRoutes(authGroup, handlers.Package, doc, basePath) +} +if handlers.PackageUsage != nil { + registerPackageUsageRoutes(authGroup, handlers.PackageUsage, doc, basePath) +} +``` + +### 套餐路由详情 +**文件**:`internal/routes/package.go:11-78` + +| 方法 | 路径 | 处理器 | 说明 | +|------|------|--------|------| +| GET | `/packages` | `List()` | 套餐列表 | +| POST | `/packages` | `Create()` | 创建套餐 | +| GET | `/packages/:id` | `Get()` | 获取详情 | +| PUT | `/packages/:id` | `Update()` | 更新套餐 | +| DELETE | `/packages/:id` | `Delete()` | 删除套餐 | +| PATCH | `/packages/:id/status` | `UpdateStatus()` | 更新状态 | +| PATCH | `/packages/:id/shelf` | `UpdateShelfStatus()` | 更新上架状态 | +| PATCH | `/packages/:id/retail-price` | `UpdateRetailPrice()` | 更新零售价 | + +### 套餐系列路由 +**文件**:`internal/routes/package_series.go:11-62` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/package-series` | 列表 | +| POST | `/package-series` | 创建 | +| GET | `/package-series/:id` | 详情 | +| PUT | `/package-series/:id` | 更新 | +| DELETE | `/package-series/:id` | 删除 | +| PATCH | `/package-series/:id/status` | 更新状态 | + +### 套餐使用记录路由 +**文件**:`internal/routes/package_usage.go:12-23` + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/package-usage/:id/daily-records` | 获取流量详单 | + +--- + +## 3️⃣ 关键字段详解 + +### Package 模型中的 duration_days 字段 +**位置**:`internal/model/package.go:46` + +```go +DurationDays int `gorm:"column:duration_days;type:int;comment:套餐天数(calendar_type=by_day时必填)" json:"duration_days"` +``` + +**说明**: +- 当 `calendar_type = "by_day"` 时必填 +- 当 `calendar_type = "natural_month"` 时不需要 +- 用于按天计算套餐有效期 + +### PackageResponse DTO 中的 duration_days +**位置**:`internal/model/dto/package_dto.go:96` + +```go +DurationDays *int `json:"duration_days,omitempty" description:"套餐天数(calendar_type=by_day时有值)"` +``` + +**说明**: +- 可选字段(指针类型) +- 仅当 `calendar_type = "by_day"` 时返回值 + +### 创建请求中的 duration_days +**位置**:`internal/model/dto/package_dto.go:16` + +```go +DurationDays *int `json:"duration_days" validate:"omitempty,min=1,max=3650" minimum:"1" maximum:"3650" description:"套餐天数(calendar_type=by_day时必填)"` +``` + +**验证规则**: +- 可选字段 +- 当提供时,必须在 1-3650 之间 +- 当 `calendar_type = "by_day"` 时必须提供 + +--- + +## 4️⃣ 业务逻辑关键点 + +### 套餐周期类型校验(Service 层) +**位置**:`internal/service/package/service.go:65-80` + +```go +// 校验套餐周期类型和时长配置 +calendarType := constants.PackageCalendarTypeByDay // 默认按天 +if req.CalendarType != nil { + calendarType = *req.CalendarType +} +if calendarType == constants.PackageCalendarTypeNaturalMonth { + // 自然月套餐:必须提供 duration_months + if req.DurationMonths <= 0 { + return nil, errors.New(errors.CodeInvalidParam, "自然月套餐必须提供有效的duration_months") + } +} else if calendarType == constants.PackageCalendarTypeByDay { + // 按天套餐:必须提供 duration_days + if req.DurationDays == nil || *req.DurationDays <= 0 { + return nil, errors.New(errors.CodeInvalidParam, "按天套餐必须提供有效的duration_days") + } +} +``` + +### 虚流量配置校验 +**位置**:`internal/service/package/service.go:51-63` + +```go +if req.EnableVirtualData { + if req.VirtualDataMB == nil || *req.VirtualDataMB <= 0 { + return nil, errors.New(errors.CodeInvalidParam, "启用虚流量时,虚流量额度必须大于0") + } + realDataMB := int64(0) + if req.RealDataMB != nil { + realDataMB = *req.RealDataMB + } + if *req.VirtualDataMB > realDataMB { + return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度") + } +} +``` + +### 代理用户套餐过滤 +**位置**:`internal/store/postgres/package_store.go:57-66` + +```go +// 代理用户额外过滤:只能看到已分配(allocation.status=启用)的套餐 +userType := middleware.GetUserTypeFromContext(ctx) +shopID := middleware.GetShopIDFromContext(ctx) +isAgent := userType == constants.UserTypeAgent && shopID > 0 +if isAgent { + query = query.Joins("INNER JOIN tb_shop_package_allocation ON tb_shop_package_allocation.package_id = tb_package.id AND tb_shop_package_allocation.deleted_at IS NULL"). + Where("tb_shop_package_allocation.shop_id = ? AND tb_shop_package_allocation.status = ?", + shopID, constants.StatusEnabled) +} +``` + +--- + +## 5️⃣ 相关常量 + +**位置**:`pkg/constants/` 目录 + +常用常量: +- `PackageCalendarTypeNaturalMonth` - 自然月 +- `PackageCalendarTypeByDay` - 按天 +- `UserTypeAgent` - 代理用户类型 +- `StatusEnabled` - 启用状态 + +--- + +## 6️⃣ 数据库表结构 + +### tb_package(套餐表) +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键 | +| package_code | varchar(100) | 套餐编码(唯一) | +| package_name | varchar(255) | 套餐名称 | +| series_id | bigint | 套餐系列 ID | +| package_type | varchar(50) | 套餐类型(formal/addon) | +| duration_months | int | 套餐时长(月数) | +| real_data_mb | bigint | 真流量额度(MB) | +| virtual_data_mb | bigint | 虚流量额度(MB) | +| enable_virtual_data | boolean | 是否启用虚流量 | +| **duration_days** | **int** | **套餐天数(按天时必填)** | +| calendar_type | varchar(20) | 套餐周期类型(natural_month/by_day) | +| data_reset_cycle | varchar(20) | 流量重置周期(daily/monthly/yearly/none) | +| expiry_base | varchar(30) | 到期时间基准(from_activation/from_purchase) | +| cost_price | bigint | 成本价(分) | +| suggested_retail_price | bigint | 建议售价(分) | +| shelf_status | int | 上架状态(1-上架/2-下架) | +| status | int | 状态(1-启用/2-禁用) | +| created_at | timestamp | 创建时间 | +| updated_at | timestamp | 更新时间 | +| deleted_at | timestamp | 删除时间 | + +### tb_package_series(套餐系列表) +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键 | +| series_code | varchar(100) | 系列编码(唯一) | +| series_name | varchar(255) | 系列名称 | +| description | text | 描述 | +| enable_one_time_commission | boolean | 是否启用一次性佣金 | +| one_time_commission_config | jsonb | 一次性佣金规则配置 | +| status | int | 状态(1-启用/2-禁用) | + +--- + +## 7️⃣ 完整请求/响应示例 + +### 创建套餐(按天) +**请求**: +```json +POST /api/admin/packages +{ + "package_code": "PKG_DAY_30", + "package_name": "30天套餐", + "package_type": "formal", + "duration_months": 1, + "real_data_mb": 10240, + "enable_virtual_data": false, + "cost_price": 9900, + "suggested_retail_price": 19900, + "calendar_type": "by_day", + "duration_days": 30, + "data_reset_cycle": "monthly", + "expiry_base": "from_activation" +} +``` + +**响应**: +```json +{ + "code": 0, + "data": { + "id": 1, + "package_code": "PKG_DAY_30", + "package_name": "30天套餐", + "package_type": "formal", + "duration_months": 1, + "real_data_mb": 10240, + "enable_virtual_data": false, + "cost_price": 9900, + "suggested_retail_price": 19900, + "calendar_type": "by_day", + "duration_days": 30, + "data_reset_cycle": "monthly", + "expiry_base": "from_activation", + "status": 1, + "shelf_status": 2, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "msg": "success" +} +``` + +--- + +## 📌 总结 + +| 层级 | 关键文件 | 核心类/函数 | +|------|---------|-----------| +| **Model** | `internal/model/package.go` | `Package`、`PackageSeries`、`PackageUsage` | +| **DTO** | `internal/model/dto/package_dto.go` | `PackageResponse`、`CreatePackageRequest` | +| **Handler** | `internal/handler/admin/package.go` | `PackageHandler` | +| **Service** | `internal/service/package/service.go` | `Service` | +| **Store** | `internal/store/postgres/package_store.go` | `PackageStore` | +| **Routes** | `internal/routes/package.go` | `registerPackageRoutes()` | +| **Route Registry** | `internal/routes/admin.go:71-78` | 路由注册入口 | + +**关键字段**:`duration_days` 在 Model、DTO 和数据库中都有定义,用于按天计算套餐有效期。 diff --git a/docs/quick_navigation.md b/docs/quick_navigation.md new file mode 100644 index 0000000..9d56ded --- /dev/null +++ b/docs/quick_navigation.md @@ -0,0 +1,147 @@ +# 套餐接口快速导航 + +## 🎯 快速查找 + +### 按功能查找 + +| 需求 | 文件 | 行号 | +|------|------|------| +| 查看套餐数据模型 | `internal/model/package.go` | 28-56 | +| 查看套餐 API 请求/响应结构 | `internal/model/dto/package_dto.go` | 1-173 | +| 查看套餐 HTTP 处理器 | `internal/handler/admin/package.go` | 1-148 | +| 查看套餐业务逻辑 | `internal/service/package/service.go` | 1-745 | +| 查看套餐数据访问 | `internal/store/postgres/package_store.go` | 1-150 | +| 查看套餐 API 路由 | `internal/routes/package.go` | 1-78 | +| 查看路由注册入口 | `internal/routes/admin.go` | 71-78 | + +### 按字段查找 + +| 字段 | Model 位置 | DTO 位置 | 说明 | +|------|-----------|---------|------| +| `duration_days` | `package.go:46` | `package_dto.go:96` | 套餐天数(按天时必填) | +| `calendar_type` | `package.go:45` | `package_dto.go:95` | 套餐周期类型 | +| `duration_months` | `package.go:37` | `package_dto.go:79` | 套餐时长(月数) | +| `real_data_mb` | `package.go:38` | `package_dto.go:80` | 真流量额度 | +| `virtual_data_mb` | `package.go:39` | `package_dto.go:81` | 虚流量额度 | +| `enable_virtual_data` | `package.go:40` | `package_dto.go:82` | 是否启用虚流量 | + +### 按 API 端点查找 + +| 端点 | Handler 方法 | 路由文件 | 行号 | +|------|-------------|---------|------| +| `GET /packages` | `List()` | `package.go` | 15-21 | +| `POST /packages` | `Create()` | `package.go` | 23-29 | +| `GET /packages/:id` | `Get()` | `package.go` | 31-37 | +| `PUT /packages/:id` | `Update()` | `package.go` | 39-45 | +| `DELETE /packages/:id` | `Delete()` | `package.go` | 47-53 | +| `PATCH /packages/:id/status` | `UpdateStatus()` | `package.go` | 55-61 | +| `PATCH /packages/:id/shelf` | `UpdateShelfStatus()` | `package.go` | 63-69 | +| `PATCH /packages/:id/retail-price` | `UpdateRetailPrice()` | `package.go` | 71-77 | + +--- + +## 🔍 关键代码片段 + +### 1. 套餐周期类型校验 +**文件**:`internal/service/package/service.go:65-80` + +**用途**:创建套餐时验证 `calendar_type` 和 `duration_days` 的配合 + +```go +if calendarType == constants.PackageCalendarTypeByDay { + if req.DurationDays == nil || *req.DurationDays <= 0 { + return nil, errors.New(errors.CodeInvalidParam, "按天套餐必须提供有效的duration_days") + } +} +``` + +### 2. 虚流量配置校验 +**文件**:`internal/service/package/service.go:51-63` + +**用途**:验证虚流量配置的合法性 + +```go +if req.EnableVirtualData { + if *req.VirtualDataMB > realDataMB { + return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度") + } +} +``` + +### 3. 代理用户套餐过滤 +**文件**:`internal/store/postgres/package_store.go:57-66` + +**用途**:代理用户只能看到已分配的套餐 + +```go +if isAgent { + query = query.Joins("INNER JOIN tb_shop_package_allocation ..."). + Where("tb_shop_package_allocation.shop_id = ? AND tb_shop_package_allocation.status = ?", + shopID, constants.StatusEnabled) +} +``` + +--- + +## 📊 数据流向 + +``` +HTTP 请求 + ↓ +PackageHandler (internal/handler/admin/package.go) + ↓ +PackageService (internal/service/package/service.go) + ├─ 业务逻辑校验 + ├─ 调用 PackageStore + └─ 返回 DTO + ↓ +PackageStore (internal/store/postgres/package_store.go) + ├─ 数据库查询 + └─ 返回 Model + ↓ +HTTP 响应 (DTO) +``` + +--- + +## 🛠️ 常见操作 + +### 添加新的套餐字段 + +1. **Model 层**:`internal/model/package.go` 的 `Package` 结构体 +2. **DTO 层**:`internal/model/dto/package_dto.go` 的相关 Request/Response +3. **Service 层**:`internal/service/package/service.go` 的业务逻辑 +4. **Store 层**:`internal/store/postgres/package_store.go` 的查询条件 +5. **数据库**:迁移文件中添加新列 + +### 修改套餐 API 响应 + +1. 修改 `PackageResponse` 结构体(`package_dto.go:71-99`) +2. 修改 Service 的转换逻辑(`service.go` 中的 `toDTO()` 方法) +3. 测试 API 端点 + +### 添加新的套餐 API 端点 + +1. 在 `PackageHandler` 中添加方法(`internal/handler/admin/package.go`) +2. 在 `PackageService` 中添加业务逻辑(`internal/service/package/service.go`) +3. 在 `registerPackageRoutes()` 中注册路由(`internal/routes/package.go`) + +--- + +## 📝 相关文档 + +- **套餐系统升级**:`docs/package-system-upgrade/` +- **套餐与佣金业务模型**:`docs/commission-package-model.md` +- **API 文档生成规范**:`docs/api-documentation-guide.md` + +--- + +## 🔗 相关模块 + +| 模块 | 说明 | 文件 | +|------|------|------| +| **PackageSeries** | 套餐系列管理 | `internal/routes/package_series.go` | +| **PackageUsage** | 套餐使用记录 | `internal/routes/package_usage.go` | +| **ShopPackageAllocation** | 套餐分配给代理 | `internal/store/postgres/shop_package_allocation_store.go` | +| **ShopSeriesAllocation** | 系列分配给代理 | `internal/store/postgres/shop_series_allocation_store.go` | +