Files
junhong_cmp_fiber/openspec/changes/client-api-data-model-fixes/tasks.md
huang ec86dbf463 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旧接口和个人客户旧登录方法
2026-03-19 10:56:50 +08:00

108 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 1. 常量定义
- [x] 1.1 在 `pkg/constants/` 新增 `asset_status.go`,定义资产业务状态常量:`AssetStatusInStock = 1`(在库)、`AssetStatusSold = 2`(已销售)、`AssetStatusExchanged = 3`(已换货)、`AssetStatusDeactivated = 4`(已停用),每个常量必须有中文注释
- [x] 1.2 在 `pkg/constants/` 新增 `order_source.go`,定义订单来源常量:`OrderSourceAdmin = "admin"`(后台)、`OrderSourceClient = "client"`(客户端),每个常量必须有中文注释
- [x] 1.3 在 `pkg/constants/` 新增 `operator_type.go`,定义操作人类型常量:`OperatorTypeAdminUser = "admin_user"`(后台用户)、`OperatorTypePersonalCustomer = "personal_customer"`(个人客户),每个常量必须有中文注释
- [x] 1.4 在 `pkg/constants/` 新增 `realname_link.go`,定义实名链接类型常量:`RealnameLinkTypeNone = "none"``RealnameLinkTypeTemplate = "template"``RealnameLinkTypeGateway = "gateway"`,每个常量必须有中文注释
## 2. 模型字段新增
- [x] 2.1 在 `internal/model/iot_card.go``IotCard` 结构体中新增 `AssetStatus int` 字段gorm: `column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用`)和 `Generation int` 字段gorm: `column:generation;type:int;not null;default:1;comment:资产世代编号`
- [x] 2.2 在 `internal/model/device.go``Device` 结构体中新增 `AssetStatus int``Generation int` 字段gorm tag 同 2.1
- [x] 2.3 在 `internal/model/order.go``Order` 结构体中新增 `Source string` 字段gorm: `column:source;type:varchar(20);not null;default:'admin';comment:订单来源 admin-后台 client-客户端`)和 `Generation int` 字段gorm: `column:generation;type:int;not null;default:1;comment:资产世代编号`
- [x] 2.4 在 `internal/model/package.go``PackageUsage` 结构体中新增 `Generation int` 字段gorm: `column:generation;type:int;not null;default:1;comment:资产世代编号`
- [x] 2.5 在 `internal/model/asset_wallet.go``AssetRechargeRecord` 结构体中新增以下字段:`OperatorType string``column:operator_type;type:varchar(20);not null;default:'admin_user';comment:操作人类型`)、`Generation int``column:generation;type:int;not null;default:1;comment:资产世代编号`)、`LinkedPackageIDs datatypes.JSON``column:linked_package_ids;type:jsonb;default:'[]';comment:强充关联套餐ID列表`)、`LinkedOrderType string``column:linked_order_type;type:varchar(20);comment:关联订单类型`)、`LinkedCarrierType string``column:linked_carrier_type;type:varchar(20);comment:关联载体类型`)、`LinkedCarrierID *uint``column:linked_carrier_id;type:bigint;comment:关联载体ID`
- [x] 2.6 在 `internal/model/carrier.go``Carrier` 结构体中新增 `RealnameLinkType string``column:realname_link_type;type:varchar(20);not null;default:'none';comment:实名链接类型 none-不支持 template-模板URL gateway-Gateway接口`)和 `RealnameLinkTemplate string``column:realname_link_template;type:varchar(500);default:'';comment:实名链接模板URL`
- [x] 2.7 在 `internal/model/shop_package_allocation.go``ShopPackageAllocation` 结构体中新增 `RetailPrice int64` 字段gorm: `column:retail_price;type:bigint;not null;default:0;comment:代理面向终端客户的零售价(分)`
- [x] 2.8 在 `internal/model/personal_customer.go` 中将 `WxOpenID` 字段的 gorm tag 从 `uniqueIndex:idx_personal_customer_wx_open_id,where:deleted_at IS NULL` 改为 `index:idx_personal_customer_wx_open_id`(唯一索引改为普通索引)
## 3. 数据库迁移
- [x] 3.1 创建迁移文件(使用 golang-migrate 工具),包含以下 ALTER TABLE 语句:
- `tb_iot_card` ADD `asset_status int NOT NULL DEFAULT 1`、ADD `generation int NOT NULL DEFAULT 1`
- `tb_device` ADD `asset_status int NOT NULL DEFAULT 1`、ADD `generation int NOT NULL DEFAULT 1`
- `tb_order` ADD `source varchar(20) NOT NULL DEFAULT 'admin'`、ADD `generation int NOT NULL DEFAULT 1`
- `tb_package_usage` ADD `generation int NOT NULL DEFAULT 1`
- `tb_asset_recharge_record` ADD `operator_type varchar(20) NOT NULL DEFAULT 'admin_user'`、ADD `generation int NOT NULL DEFAULT 1`、ADD `linked_package_ids jsonb DEFAULT '[]'`、ADD `linked_order_type varchar(20)`、ADD `linked_carrier_type varchar(20)`、ADD `linked_carrier_id bigint`
- `tb_carrier` ADD `realname_link_type varchar(20) NOT NULL DEFAULT 'none'`、ADD `realname_link_template varchar(500) DEFAULT ''`
- `tb_shop_package_allocation` ADD `retail_price bigint NOT NULL DEFAULT 0`
- [x] 3.2 在同一迁移文件中添加存量数据修复 SQL`UPDATE tb_shop_package_allocation spa SET retail_price = (SELECT suggested_retail_price FROM tb_package p WHERE p.id = spa.package_id) WHERE retail_price = 0`
- [x] 3.3 在同一迁移文件中添加索引变更DROP `idx_personal_customer_wx_open_id` 唯一索引CREATE 普通索引 `idx_personal_customer_wx_open_id` ON `tb_personal_customer(wx_open_id)`
- [x] 3.4 编写 down 迁移文件(回滚用),包含对应的 DROP COLUMN 和索引恢复语句
- [x] 3.5 执行迁移,使用 PostgreSQL MCP 工具验证所有字段已添加、存量 `retail_price` 已更新、索引已变更
## 4. BUG-1 修复:代理零售价
- [x] 4.1 修改 `internal/service/purchase_validation/service.go``GetPurchasePrice()` 方法(约第 172 行):增加渠道判断,代理渠道(`card.ShopID > 0`)时查询 `ShopPackageAllocation` 获取 `RetailPrice` 并返回,平台渠道继续返回 `Package.SuggestedRetailPrice`
- [x] 4.2 修改 `internal/service/purchase_validation/service.go``validatePackages()` 方法(约第 148 行):将 `totalPrice += pkg.SuggestedRetailPrice` 改为按渠道取价逻辑(复用 4.1 的渠道判断),代理渠道额外校验 `allocation.RetailPrice >= allocation.CostPrice`,不满足则该套餐视为不可购买
- [x] 4.3 修改 `internal/service/shop_package_batch_allocation/service.go`:创建分配记录时设置 `RetailPrice``Package.SuggestedRetailPrice`(约第 84-105 行创建 allocation 的位置)
- [x] 4.4 修改 `internal/service/shop_series_grant/service.go`:创建/更新 `ShopPackageAllocation` 时同步设置 `RetailPrice`(约第 302-352 行和第 614-685 行)
- [x] 4.5 在 `internal/service/shop_package_batch_pricing/service.go``BatchUpdatePricing()` 方法中新增 cost_price 锁定检查:更新每条 allocation 的 cost_price 前,查询是否存在下级分配记录(`ShopPackageAllocation WHERE allocator_shop_id = allocation.ShopID AND package_id = allocation.PackageID`),存在则跳过该条并记录到响应的 `skipped` 列表,附带原因"存在下级分配记录,请先回收后再修改成本价"
- [x] 4.6 在 `internal/service/shop_series_grant/service.go` 中同步添加 cost_price 锁定检查:更新现有 allocation 的 CostPrice 时(约第 634 行),查询是否存在下级分配记录,存在则拒绝修改并返回错误
## 5. 代理零售价后台管理
- [x] 5.1 修改 `internal/model/dto/shop_package_batch_pricing_dto.go``BatchUpdateCostPriceRequest`:新增 `PricingTarget string` 字段(`json:"pricing_target" validate:"omitempty,oneof=cost_price retail_price" description:"调价目标 cost_price-成本价(默认) retail_price-零售价"`),不传时默认为 `cost_price` 保持向后兼容
- [x] 5.2 修改 `internal/service/shop_package_batch_pricing/service.go``BatchUpdatePricing()` 方法:根据 `req.PricingTarget` 分流——`cost_price` 走现有逻辑(含 4.5 的锁定检查),`retail_price` 走新逻辑:计算新零售价、校验 `newRetailPrice >= allocation.CostPrice`(不满足则跳过并报错"零售价不能低于成本价")、更新 `allocation.RetailPrice`、记录价格历史(`ShopPackageAllocationPriceHistory` 增加 `old_retail_price`/`new_retail_price` 字段或复用 `OldCostPrice`/`NewCostPrice` 字段并新增 `price_type` 标识)
- [x] 5.3 修改 `internal/model/dto/package_dto.go``PackageResponse` 结构体:新增 `RetailPrice *int64` 字段(`json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`
- [x] 5.4 修改 `internal/service/package/service.go``toResponse()` 方法(约第 530-541 行):代理用户查询时,从 allocation 读取 `RetailPrice` 设入 `resp.RetailPrice`;同时修正 `ProfitMargin` 计算:从 `pkg.SuggestedRetailPrice - allocation.CostPrice` 改为 `allocation.RetailPrice - allocation.CostPrice`
- [x] 5.5 修改 `internal/service/package/service.go``toResponseWithAllocation()` 方法(约第 595-603 行):同 5.4,从 allocation 读取 `RetailPrice`、修正 `ProfitMargin` 计算
## 6. Carrier 管理 DTO 更新
- [x] 6.1 修改 `internal/model/dto/carrier_dto.go`(或 Carrier 相关 DTO 文件):在 `CarrierCreateRequest``CarrierUpdateRequest` 中新增 `RealnameLinkType *string` 字段(`json:"realname_link_type" validate:"omitempty,oneof=none template gateway" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`)和 `RealnameLinkTemplate *string` 字段(`json:"realname_link_template" validate:"omitempty,max=500" maxLength:"500" description:"实名链接模板URL支持 {iccid}/{msisdn}/{virtual_no} 占位符"`
- [x] 6.2 修改 Carrier Service 的 Create/Update 方法:将 DTO 中的 `RealnameLinkType``RealnameLinkTemplate` 写入 Carrier 模型Update 时 `realname_link_type``template` 时校验 `realname_link_template` 非空
- [x] 6.3 修改 Carrier 的响应 DTO`CarrierResponse`):新增 `RealnameLinkType``RealnameLinkTemplate` 字段,后台列表/详情可展示
## 7. 后台订单 generation 快照
- [x] 7.1 修改 `internal/service/order/service.go``CreateAdminOrder()` 方法:创建 Order 时从资产IotCard/Device获取当前 `Generation` 值并设入 `order.Generation`,不再依赖数据库默认值 1
## 8. 资产手动停用
- [x] 8.1 在 `internal/handler/admin/asset.go`(或新建 `internal/handler/admin/asset_lifecycle.go`)新增 `DeactivateAsset` Handler 方法:接收资产类型和 ID调用 Service 将 `asset_status` 设为 `constants.AssetStatusDeactivated`4
- [x] 8.2 在 `internal/service/asset/service.go`(或相关 Service新增 `Deactivate()` 方法:校验当前 `asset_status` 为 1在库或 2已销售才允许停用已换货3或已停用4的拒绝操作使用条件更新 `WHERE asset_status IN (1, 2)` 确保幂等
- [x] 8.3 在 `internal/routes/` 注册停用路由:`PATCH /api/admin/iot-cards/:id/deactivate``PATCH /api/admin/devices/:id/deactivate`(或统一为 `PATCH /api/admin/assets/:type/:id/deactivate`
- [x] 8.4 更新 `cmd/api/docs.go``cmd/gendocs/main.go`:如新增了 Handler 则同步注册到文档生成器
## 9. BUG-2 修复:一次性佣金触发条件
- [x] 9.1 修改 `internal/service/commission_calculation/service.go` 中的 `triggerOneTimeCommissionForCardInTx` 方法:将 `if order.IsPurchaseOnBehalf` 的跳过逻辑改为 `if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient`,即代购订单或非客户端来源订单均跳过一次性佣金
- [x] 9.2 修改 `internal/service/commission_calculation/service.go` 中的 `triggerOneTimeCommissionForDeviceInTx` 方法:同 9.1 逻辑
## 10. BUG-4 修复:充值回调事务一致性
- [x] 10.1 修改 `internal/store/postgres/asset_recharge_store.go``UpdateStatusWithOptimisticLock` 方法:新增 `tx *gorm.DB` 参数(方法签名变更),内部使用传入的 `tx` 替代 `s.db.WithContext(ctx)`;同时保留原方法签名的兼容版本或修改所有调用点
- [x] 10.2 修改 `internal/store/postgres/asset_recharge_store.go``UpdatePaymentInfo` 方法:同 10.1,新增 `tx *gorm.DB` 参数
- [x] 10.3 修改 `internal/service/recharge/service.go``HandlePaymentCallback` 方法(约第 308-321 行):在事务闭包内调用 Store 方法时传入事务 `tx`,确保 `UpdateStatusWithOptimisticLock``UpdatePaymentInfo` 和钱包入账在同一事务内
- [x] 10.4 检查并更新 `UpdateStatusWithOptimisticLock``UpdatePaymentInfo` 的其他调用点(如有),确保传入正确的 db 或 tx 参数
## 11. 旧 H5 接口清理
- [x] 11.1 删除 `internal/handler/h5/` 目录下全部 5 个文件:`auth.go``order.go``recharge.go``package_usage.go``enterprise_device.go`
- [x] 11.2 删除 `internal/routes/h5.go``internal/routes/h5_enterprise_device.go``internal/routes/h5_package_usage.go`
- [x] 11.3 修改 `internal/routes/routes.go`:移除 `/api/h5` 路由组挂载(约第 31-33 行)
- [x] 11.4 修改 `internal/routes/order.go`:移除 `registerH5OrderRoutes` 函数(约第 56-105 行)
- [x] 11.5 修改 `internal/routes/recharge.go`:移除 `registerH5RechargeRoutes` 函数(约第 11-44 行)
- [x] 11.6 修改 `internal/bootstrap/handlers.go`:移除 H5 Handler 构造H5Auth、EnterpriseDeviceH5、H5PackageUsage、H5Order、H5Recharge约第 24、31、44、49、50 行)
- [x] 11.7 修改 `internal/bootstrap/types.go`:移除 Handlers 结构体中的 H5 Handler 字段(约第 22、29、42、47、48 行)
- [x] 11.8 修改 `internal/bootstrap/middlewares.go`:移除 `createH5AuthMiddleware` 函数和 H5 跳过路径配置(约第 72-95 行)
- [x] 11.9 修改 `pkg/openapi/handlers.go`:移除文档生成中的 H5 Handler 构造EnterpriseDeviceH5、H5PackageUsage、H5Order、H5Recharge
- [x] 11.10 修改 `cmd/api/main.go`:移除 `/api/h5` 限流挂载(约第 250-257 行)
## 12. 旧个人客户登录接口清理
- [x] 12.1 修改 `internal/handler/app/personal_customer.go`:删除 Login约第 79 行、SendCode约第 35 行、WechatOAuthLogin约第 114 行、BindWechat约第 134 行)方法。保留 UpdateProfile 和 GetProfile
- [x] 12.2 修改 `internal/routes/personal.go`移除指向已删除方法的路由注册Login、SendCode、WechatOAuthLogin、BindWechat 的路由)
- [x] 12.3 检查 `internal/bootstrap/handlers.go``internal/bootstrap/types.go`:如果 PersonalCustomer Handler 有初始化引用已删除方法的依赖,同步清理
## 13. 验证
- [x] 13.1 执行 `go build ./...` 确认编译通过,无任何编译错误
- [x] 13.2 对所有修改的文件执行 `lsp_diagnostics` 确认无错误和警告
- [x] 13.3 使用 PostgreSQL MCP 工具验证数据库:确认 7 张表的新字段存在、默认值正确、存量 `retail_price` 已填充、`wx_open_id` 索引已变更
- [x] 13.4 验证删除的 H5 路由不再注册:检查代码中无 `/api/h5` 相关路由残留
- [x] 13.5 验证 `BatchUpdatePricing` 接口扩展:确认 `pricing_target=retail_price` 参数可正常使用,不传时默认走 `cost_price` 逻辑(向后兼容)
- [x] 13.6 验证代理套餐列表:确认 `PackageResponse` 包含 `retail_price` 字段,`profit_margin` 计算基于 `retail_price - cost_price`
- [x] 13.7 撰写功能总结文档 `docs/client-api-data-model-fixes/功能总结.md`,记录所有变更内容