文档提交
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s

This commit is contained in:
2026-04-11 11:27:43 +08:00
parent e9ff14df0e
commit 8f66ca7bcb
15 changed files with 3775 additions and 3 deletions

View File

@@ -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` 的常见问题

250
docs/ORDER_STATUS_INDEX.md Normal file
View File

@@ -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 行CreateH5OrderH5订单创建
└─ 第 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.1K29个章节45+个代码位置
---
## 🚀 快速开始
### 第一次接触系统?
1. 阅读 order_status_search_summary.md5分钟
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
**维护者**:开发团队

244
docs/PACKAGE_DOCS_INDEX.md Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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
```

View File

@@ -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元 ✓
```

View File

@@ -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) - 可视化的流程图

View File

@@ -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 // 待人工修正
```

View File

@@ -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 |

View File

@@ -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
- 实际到账 = 提现金额 - 手续费

View File

@@ -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`
- 提现流程中使用事务确保数据一致性

View File

@@ -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
```

View File

@@ -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` |

View File

@@ -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 和数据库中都有定义,用于按天计算套餐有效期。

147
docs/quick_navigation.md Normal file
View File

@@ -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` |