feat: 客户端接口数据模型基础准备
- 新增资产状态、订单来源、操作人类型、实名链接类型常量 - 8个模型新增字段(asset_status/generation/source/retail_price等) - 数据库迁移000082:7张表15+字段,含存量retail_price回填 - BUG-1修复:代理零售价渠道隔离,cost_price分配锁定 - BUG-2修复:一次性佣金仅客户端订单触发 - BUG-4修复:充值回调Store操作纳入事务 - 新增资产手动停用接口(PATCH /iot-cards/:id/deactivate、/devices/:id/deactivate) - Carrier管理新增实名链接配置 - 后台订单generation写时快照 - BatchUpdatePricing支持retail_price调价目标 - 清理全部H5旧接口和个人客户旧登录方法
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 分配零售价字段定义
|
||||
|
||||
系统 MUST 在 `ShopPackageAllocation` 新增 `retail_price bigint NOT NULL DEFAULT 0` 字段。
|
||||
|
||||
#### Scenario: 新字段存在且非空
|
||||
- **WHEN** 执行分配记录建表或迁移
|
||||
- **THEN** `retail_price` MUST 为非空整型字段,默认值为 `0`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 分配创建默认零售价规则
|
||||
|
||||
系统 MUST 在创建分配记录时将 `retail_price` 自动设置为对应 `Package.SuggestedRetailPrice`。
|
||||
|
||||
#### Scenario: 创建分配自动带出建议零售价
|
||||
- **WHEN** 平台给代理创建套餐分配记录
|
||||
- **THEN** 新记录的 `retail_price` MUST 等于该套餐的 `suggested_retail_price`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 零售价约束规则
|
||||
|
||||
系统 MUST 强制校验:`retail_price >= cost_price` 且 `retail_price <= suggested_retail_price * 2`。
|
||||
|
||||
#### Scenario: 零售价低于成本价
|
||||
- **WHEN** 代理设置 `retail_price < cost_price`
|
||||
- **THEN** 系统 MUST 拒绝保存并返回价格约束错误
|
||||
|
||||
#### Scenario: 零售价超过建议价两倍
|
||||
- **WHEN** 代理设置 `retail_price > suggested_retail_price * 2`
|
||||
- **THEN** 系统 MUST 拒绝保存并返回价格约束错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 成本价分配锁定规则
|
||||
|
||||
当某分配存在下级分配记录时,系统 MUST 禁止修改该分配的 `cost_price`。
|
||||
|
||||
#### Scenario: 存在下级分配时修改成本价
|
||||
- **WHEN** 上级分配记录已被继续分配到下级店铺
|
||||
- **THEN** 系统 MUST 拒绝对该记录的 `cost_price` 修改
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代理零售价可调与存量迁移
|
||||
|
||||
系统 MUST 允许代理修改自己分配记录的 `retail_price`(在约束范围内);系统 MUST 对存量数据执行迁移:将 `retail_price` 批量更新为对应套餐的 `SuggestedRetailPrice`。
|
||||
|
||||
#### Scenario: 代理调整自己的零售价
|
||||
- **WHEN** 代理修改自己分配记录的 `retail_price` 且满足价格约束
|
||||
- **THEN** 系统 MUST 允许更新
|
||||
|
||||
#### Scenario: 存量数据回填零售价
|
||||
- **WHEN** 执行本次数据迁移
|
||||
- **THEN** 系统 MUST 将历史 `ShopPackageAllocation.retail_price` 批量更新为对应套餐的 `SuggestedRetailPrice`
|
||||
@@ -0,0 +1,55 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 资产表新增代际字段
|
||||
|
||||
系统 MUST 在资产主表新增 `generation int NOT NULL DEFAULT 1` 字段,覆盖 `IotCard` 与 `Device`。
|
||||
|
||||
#### Scenario: 新资产默认代际为 1
|
||||
- **WHEN** 创建新的 IoT 卡或设备
|
||||
- **THEN** 系统 MUST 将 `generation` 初始化为 `1`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 关联业务表新增代际字段
|
||||
|
||||
系统 MUST 在以下关联业务表新增 `generation int NOT NULL DEFAULT 1` 字段:`Order`、`PackageUsage`、`AssetRechargeRecord`。
|
||||
|
||||
#### Scenario: 新关联记录默认代际为 1
|
||||
- **WHEN** 创建订单、套餐使用记录或资产充值记录
|
||||
- **THEN** 系统 MUST 将记录的 `generation` 默认为 `1`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 写时快照代际规则
|
||||
|
||||
系统 MUST 在创建关联记录时执行代际写时快照:从当前资产(IoT 卡/设备)的 `generation` 复制到新建的 `Order`、`PackageUsage`、`AssetRechargeRecord` 记录。
|
||||
|
||||
#### Scenario: 创建订单时复制资产代际
|
||||
- **WHEN** 某资产当前 `generation=3`,并基于该资产创建订单
|
||||
- **THEN** 该订单记录的 `generation` MUST 写入为 `3`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询过滤规则
|
||||
|
||||
系统 MUST 支持客户端按 `generation` 过滤历史数据;后台管理侧 MUST 不默认按 `generation` 过滤。
|
||||
|
||||
本提案阶段 MUST 仅新增字段定义,具体过滤逻辑在后续提案实现。
|
||||
|
||||
#### Scenario: 客户端按代际查看历史
|
||||
- **WHEN** 客户端请求携带指定 `generation`
|
||||
- **THEN** 系统 MUST 仅返回该代际的数据(在后续提案中实现)
|
||||
|
||||
#### Scenario: 后台查询不按代际裁剪
|
||||
- **WHEN** 管理端查询订单或充值记录且未显式指定 `generation`
|
||||
- **THEN** 系统 MUST 返回全部代际数据
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包流水不引入代际字段
|
||||
|
||||
系统 MUST NOT 在钱包流水相关表新增 `generation` 字段,因为钱包流水已通过 `wallet_id` 天然隔离。
|
||||
|
||||
#### Scenario: 钱包流水按钱包隔离
|
||||
- **WHEN** 查询某资产钱包流水
|
||||
- **THEN** 系统 MUST 仅依赖 `wallet_id` 完成数据隔离,不新增 `generation` 参与过滤
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 资产生命周期状态字段定义
|
||||
|
||||
系统 MUST 在 `IotCard` 与 `Device` 数据模型中新增 `asset_status int NOT NULL DEFAULT 1` 字段,用于表达资产生命周期状态。
|
||||
|
||||
状态值域 MUST 固定为:`1-在库`、`2-已销售`、`3-已换货`、`4-已停用`。
|
||||
|
||||
#### Scenario: 新建资产默认在库
|
||||
- **WHEN** 系统创建新的 IoT 卡或设备记录
|
||||
- **THEN** `asset_status` MUST 默认为 `1`(在库)
|
||||
|
||||
#### Scenario: 非法状态值被拒绝
|
||||
- **WHEN** 写入 `asset_status` 为 `0`、`5` 或其他非约定值
|
||||
- **THEN** 系统 MUST 拒绝该写入并提示状态值不合法
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 资产生命周期状态常量定义
|
||||
|
||||
系统 MUST 在 `pkg/constants/` 中定义资产生命周期状态常量,并统一由业务层引用,禁止在业务代码中硬编码状态值。
|
||||
|
||||
#### Scenario: 业务代码引用常量
|
||||
- **WHEN** Service 层执行资产状态判断或赋值
|
||||
- **THEN** 代码 MUST 使用 `pkg/constants/` 中定义的资产状态常量而不是硬编码数字
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 资产状态与网络状态独立
|
||||
|
||||
系统 MUST 保证 `asset_status` 与运营商侧 `network_status` 完全独立,二者不互相推导、不互相覆盖。
|
||||
|
||||
本提案阶段 MUST 仅新增字段与常量定义,状态流转逻辑(导入→在库、首次绑定/分配→已销售、换货完成→已换货、转新→在库且代际+1、手动停用→已停用)在后续提案实现。
|
||||
|
||||
#### Scenario: 网络状态变化不影响资产状态
|
||||
- **WHEN** Gateway 同步将 `network_status` 从开机改为停机
|
||||
- **THEN** 系统 MUST 保持 `asset_status` 不变
|
||||
|
||||
#### Scenario: 资产状态变化不强制修改网络状态
|
||||
- **WHEN** 管理端将资产手动停用(`asset_status=4`)
|
||||
- **THEN** 系统 MUST 不自动改写 `network_status`
|
||||
@@ -0,0 +1,24 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 资产充值表结构变更
|
||||
|
||||
系统 MUST 在 `tb_asset_recharge_record` 新增以下字段:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `operator_type` | varchar(20) | ✅ | 操作人类型,枚举 `admin_user` / `personal_customer`,默认 `admin_user` |
|
||||
| `generation` | int | ✅ | 资产代际,默认 `1` |
|
||||
| `linked_package_ids` | jsonb | ❌ | 关联套餐 ID 列表,默认 `'[]'` |
|
||||
| `linked_order_type` | varchar(20) | ❌ | 关联订单类型 |
|
||||
| `linked_carrier_type` | varchar(20) | ❌ | 关联载体类型(如 iot_card/device) |
|
||||
| `linked_carrier_id` | bigint | ❌ | 关联载体 ID |
|
||||
|
||||
#### Scenario: 新建充值记录默认字段值
|
||||
- **WHEN** 系统创建新的资产充值记录且未显式传入新增字段
|
||||
- **THEN** `operator_type` MUST 默认为 `admin_user`
|
||||
- **THEN** `generation` MUST 默认为 `1`
|
||||
- **THEN** `linked_package_ids` MUST 默认为空数组 `[]`
|
||||
|
||||
#### Scenario: 写入关联上下文信息
|
||||
- **WHEN** 充值记录由订单或套餐联动产生
|
||||
- **THEN** 系统 MUST 可写入 `linked_order_type`、`linked_carrier_type`、`linked_carrier_id` 作为关联上下文
|
||||
@@ -0,0 +1,44 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 运营商实名链接配置字段定义
|
||||
|
||||
系统 MUST 在 Carrier 模型新增以下字段:
|
||||
- `realname_link_type varchar(20) NOT NULL DEFAULT 'none'`
|
||||
- `realname_link_template varchar(500) DEFAULT ''`
|
||||
|
||||
#### Scenario: 默认配置为不支持在线实名
|
||||
- **WHEN** 创建新的运营商记录且未显式设置实名链接配置
|
||||
- **THEN** 系统 MUST 将 `realname_link_type` 设为 `none`,`realname_link_template` 设为空字符串
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 实名链接三种模式
|
||||
|
||||
系统 MUST 支持并仅支持以下实名链接模式:
|
||||
- `none`:不支持在线实名
|
||||
- `template`:使用模板 URL 生成实名链接
|
||||
- `gateway`:通过 Gateway 接口动态获取实名链接
|
||||
|
||||
#### Scenario: none 模式
|
||||
- **WHEN** `realname_link_type=none`
|
||||
- **THEN** 系统 MUST 视为不支持在线实名跳转
|
||||
|
||||
#### Scenario: template 模式
|
||||
- **WHEN** `realname_link_type=template`
|
||||
- **THEN** 系统 MUST 使用 `realname_link_template` 作为实名链接模板
|
||||
|
||||
#### Scenario: gateway 模式
|
||||
- **WHEN** `realname_link_type=gateway`
|
||||
- **THEN** 系统 MUST 通过 Gateway 能力获取实名链接
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 模板占位符规则
|
||||
|
||||
当 `realname_link_type=template` 时,系统 MUST 支持模板中的占位符 `{iccid}`、`{msisdn}`、`{virtual_no}`。
|
||||
|
||||
本提案阶段 MUST 仅新增字段,不实现实名跳转接口逻辑。
|
||||
|
||||
#### Scenario: 模板占位符可被解析
|
||||
- **WHEN** 模板 URL 包含 `{iccid}`、`{msisdn}` 或 `{virtual_no}`
|
||||
- **THEN** 系统 MUST 在后续实名跳转实现中按占位符语义进行参数替换
|
||||
@@ -0,0 +1,15 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 设备实体定义
|
||||
|
||||
系统 SHALL 在 `Device` 模型新增以下字段:
|
||||
- `asset_status int NOT NULL DEFAULT 1`
|
||||
- `generation int NOT NULL DEFAULT 1`
|
||||
|
||||
#### Scenario: 新建设备默认资产状态
|
||||
- **WHEN** 创建新的设备记录
|
||||
- **THEN** `asset_status` MUST 默认为 `1`(在库)
|
||||
|
||||
#### Scenario: 新建设备默认代际
|
||||
- **WHEN** 创建新的设备记录
|
||||
- **THEN** `generation` MUST 默认为 `1`
|
||||
@@ -0,0 +1,47 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 旧 H5 接口文件删除清单
|
||||
|
||||
系统 MUST 完整删除以下旧 H5 文件:
|
||||
- `internal/handler/h5/auth.go`
|
||||
- `internal/handler/h5/order.go`
|
||||
- `internal/handler/h5/recharge.go`
|
||||
- `internal/handler/h5/package_usage.go`
|
||||
- `internal/handler/h5/enterprise_device.go`
|
||||
- `internal/routes/h5.go`
|
||||
- `internal/routes/h5_enterprise_device.go`
|
||||
- `internal/routes/h5_package_usage.go`
|
||||
|
||||
#### Scenario: 旧 H5 文件不存在
|
||||
- **WHEN** 执行本提案改造完成后检查仓库
|
||||
- **THEN** 上述文件 MUST 全部不存在
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 旧 H5 与旧登录引用清理清单
|
||||
|
||||
系统 MUST 清理以下代码引用:
|
||||
- bootstrap:`handlers.go` 中 `H5Auth`、`EnterpriseDeviceH5`、`H5PackageUsage`、`H5Order`、`H5Recharge`
|
||||
- bootstrap:`types.go` 对应字段
|
||||
- bootstrap:`middlewares.go` 中 `createH5AuthMiddleware`
|
||||
- 路由:`routes.go` 的 `/api/h5` 挂载
|
||||
- 路由:`order.go` 的 `registerH5OrderRoutes`
|
||||
- 路由:`recharge.go` 的 `registerH5RechargeRoutes`
|
||||
- 文档:`pkg/openapi/handlers.go` 中 H5 Handler 构造
|
||||
- 限流:`cmd/api/main.go` 中 `/api/h5` 限流配置
|
||||
- 旧登录方法:`internal/handler/app/personal_customer.go` 中 `Login`、`SendCode`、`WechatOAuthLogin`、`BindWechat`
|
||||
- 旧登录路由:`internal/routes/personal.go` 中指向已删除方法的路由
|
||||
|
||||
#### Scenario: 编译期无已删除符号引用
|
||||
- **WHEN** 清理完成后执行编译
|
||||
- **THEN** 系统 MUST 不再出现对上述已删除 Handler、路由或方法的引用
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 清理后编译通过
|
||||
|
||||
系统 MUST 在完成文件删除与引用清理后保持工程可编译。
|
||||
|
||||
#### Scenario: 全量编译验证通过
|
||||
- **WHEN** 执行构建命令
|
||||
- **THEN** 工程 MUST 编译通过且无 H5 旧接口残留导致的编译错误
|
||||
@@ -0,0 +1,15 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: IoT 卡实体定义
|
||||
|
||||
系统 SHALL 在 `IotCard` 模型新增以下字段:
|
||||
- `asset_status int NOT NULL DEFAULT 1`
|
||||
- `generation int NOT NULL DEFAULT 1`
|
||||
|
||||
#### Scenario: 新建 IoT 卡默认资产状态
|
||||
- **WHEN** 创建新的 IoT 卡记录
|
||||
- **THEN** `asset_status` MUST 默认为 `1`(在库)
|
||||
|
||||
#### Scenario: 新建 IoT 卡默认代际
|
||||
- **WHEN** 创建新的 IoT 卡记录
|
||||
- **THEN** `generation` MUST 默认为 `1`
|
||||
@@ -0,0 +1,19 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单实体定义
|
||||
|
||||
系统 SHALL 定义订单(Order)实体并新增来源与代际字段:
|
||||
- `source varchar(20) NOT NULL DEFAULT 'admin'`,取值 `admin/client`
|
||||
- `generation int NOT NULL DEFAULT 1`
|
||||
|
||||
#### Scenario: 新建订单默认后台来源
|
||||
- **WHEN** 系统创建订单且未显式指定来源
|
||||
- **THEN** `source` MUST 默认为 `admin`
|
||||
|
||||
#### Scenario: 客户端下单写入客户端来源
|
||||
- **WHEN** 客户端入口创建订单
|
||||
- **THEN** `source` MUST 写入为 `client`
|
||||
|
||||
#### Scenario: 新建订单默认代际为 1
|
||||
- **WHEN** 系统创建订单且未显式指定代际
|
||||
- **THEN** `generation` MUST 默认为 `1`
|
||||
@@ -0,0 +1,19 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 一次性佣金触发条件
|
||||
|
||||
系统 SHALL 在满足一次性佣金阈值规则的前提下,仅对客户端订单触发一次性佣金。
|
||||
|
||||
完整触发判断 MUST 为:`!order.IsPurchaseOnBehalf && order.Source == "client"`。
|
||||
|
||||
#### Scenario: 客户端自购订单触发
|
||||
- **WHEN** 订单满足阈值条件,且 `order.IsPurchaseOnBehalf=false`,`order.Source="client"`
|
||||
- **THEN** 系统 SHALL 触发一次性佣金计算
|
||||
|
||||
#### Scenario: 代购订单不触发
|
||||
- **WHEN** 订单满足阈值条件,但 `order.IsPurchaseOnBehalf=true`
|
||||
- **THEN** 系统 SHALL 不触发一次性佣金
|
||||
|
||||
#### Scenario: 后台订单不触发
|
||||
- **WHEN** 订单满足阈值条件,且 `order.Source="admin"`
|
||||
- **THEN** 系统 SHALL 不触发一次性佣金
|
||||
@@ -0,0 +1,31 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 获取购买价格
|
||||
|
||||
系统 MUST 根据购买渠道返回正确的购买价格。
|
||||
|
||||
#### Scenario: 代理渠道使用分配零售价
|
||||
- **WHEN** 客户通过代理渠道购买套餐
|
||||
- **THEN** 系统 MUST 使用 `allocation.retail_price` 作为支付金额
|
||||
|
||||
#### Scenario: 平台渠道使用套餐建议零售价
|
||||
- **WHEN** 客户通过平台自营渠道购买套餐
|
||||
- **THEN** 系统 MUST 使用 `Package.SuggestedRetailPrice` 作为支付金额
|
||||
|
||||
---
|
||||
|
||||
### Requirement: validatePackages 价格累加与展示校验
|
||||
|
||||
系统 MUST 在 `validatePackages()` 中按渠道来源使用一致的价格来源进行累加计算,并在代理渠道增加价格展示可见性校验。
|
||||
|
||||
#### Scenario: 代理渠道累加使用 retail_price
|
||||
- **WHEN** `validatePackages()` 处理代理渠道的多套餐下单
|
||||
- **THEN** 总价累加 MUST 基于各套餐的 `allocation.retail_price`
|
||||
|
||||
#### Scenario: 平台渠道累加使用 SuggestedRetailPrice
|
||||
- **WHEN** `validatePackages()` 处理平台渠道的多套餐下单
|
||||
- **THEN** 总价累加 MUST 基于各套餐的 `Package.SuggestedRetailPrice`
|
||||
|
||||
#### Scenario: 代理渠道过滤异常零售价
|
||||
- **WHEN** 代理渠道某套餐存在 `retail_price < cost_price`
|
||||
- **THEN** 系统 MUST 不展示该套餐,且不允许该套餐进入下单校验
|
||||
@@ -0,0 +1,13 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 微信标识索引策略
|
||||
|
||||
系统 MUST 将 `tb_personal_customer.wx_open_id` 的索引从唯一索引调整为普通索引:删除 `uniqueIndex`,改为 `index`。
|
||||
|
||||
#### Scenario: 多条记录允许相同 wx_open_id
|
||||
- **WHEN** 数据库中写入两条具有相同 `wx_open_id` 的个人客户记录
|
||||
- **THEN** 数据库层 MUST 不再因唯一约束报错
|
||||
|
||||
#### Scenario: 查询性能仍受索引保障
|
||||
- **WHEN** 按 `wx_open_id` 执行查询
|
||||
- **THEN** 系统 MUST 继续命中普通索引以保障查询性能
|
||||
@@ -0,0 +1,26 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 充值支付回调处理
|
||||
|
||||
系统 SHALL 处理微信和支付宝的支付回调,验证签名,更新充值订单状态,增加钱包余额。
|
||||
|
||||
关键一致性修复:`HandlePaymentCallback` 内的 `UpdateStatusWithOptimisticLock` 与 `UpdatePaymentInfo` MUST 使用同一个事务内 `tx` 执行。
|
||||
|
||||
#### Scenario: 回调处理中状态更新与支付信息更新同事务
|
||||
- **WHEN** 收到支付成功回调并进入 `HandlePaymentCallback`
|
||||
- **THEN** 系统 MUST 在同一事务 `tx` 内执行 `UpdateStatusWithOptimisticLock`
|
||||
- **THEN** 系统 MUST 在同一事务 `tx` 内执行 `UpdatePaymentInfo`
|
||||
|
||||
#### Scenario: 事务失败整体回滚
|
||||
- **WHEN** 回调处理中任一步骤失败
|
||||
- **THEN** 系统 MUST 回滚该事务,保证订单状态与支付信息不出现部分成功
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Store 方法签名支持事务参数
|
||||
|
||||
系统 MUST 调整充值相关 Store 方法签名,支持显式传入 `*gorm.DB tx` 参数,以保证事务边界可控。
|
||||
|
||||
#### Scenario: Service 传入事务句柄
|
||||
- **WHEN** Service 在事务上下文调用 Store 更新充值记录
|
||||
- **THEN** Store 方法 MUST 接收并使用传入的 `tx` 执行数据库操作
|
||||
Reference in New Issue
Block a user