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

15 KiB
Raw Blame History

1. 常量定义

  • 1.1 在 pkg/constants/ 新增 asset_status.go,定义资产业务状态常量:AssetStatusInStock = 1(在库)、AssetStatusSold = 2(已销售)、AssetStatusExchanged = 3(已换货)、AssetStatusDeactivated = 4(已停用),每个常量必须有中文注释
  • 1.2 在 pkg/constants/ 新增 order_source.go,定义订单来源常量:OrderSourceAdmin = "admin"(后台)、OrderSourceClient = "client"(客户端),每个常量必须有中文注释
  • 1.3 在 pkg/constants/ 新增 operator_type.go,定义操作人类型常量:OperatorTypeAdminUser = "admin_user"(后台用户)、OperatorTypePersonalCustomer = "personal_customer"(个人客户),每个常量必须有中文注释
  • 1.4 在 pkg/constants/ 新增 realname_link.go,定义实名链接类型常量:RealnameLinkTypeNone = "none"RealnameLinkTypeTemplate = "template"RealnameLinkTypeGateway = "gateway",每个常量必须有中文注释

2. 模型字段新增

  • 2.1 在 internal/model/iot_card.goIotCard 结构体中新增 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:资产世代编号
  • 2.2 在 internal/model/device.goDevice 结构体中新增 AssetStatus intGeneration int 字段gorm tag 同 2.1
  • 2.3 在 internal/model/order.goOrder 结构体中新增 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:资产世代编号
  • 2.4 在 internal/model/package.goPackageUsage 结构体中新增 Generation int 字段gorm: column:generation;type:int;not null;default:1;comment:资产世代编号
  • 2.5 在 internal/model/asset_wallet.goAssetRechargeRecord 结构体中新增以下字段:OperatorType stringcolumn:operator_type;type:varchar(20);not null;default:'admin_user';comment:操作人类型)、Generation intcolumn:generation;type:int;not null;default:1;comment:资产世代编号)、LinkedPackageIDs datatypes.JSONcolumn:linked_package_ids;type:jsonb;default:'[]';comment:强充关联套餐ID列表)、LinkedOrderType stringcolumn:linked_order_type;type:varchar(20);comment:关联订单类型)、LinkedCarrierType stringcolumn:linked_carrier_type;type:varchar(20);comment:关联载体类型)、LinkedCarrierID *uintcolumn:linked_carrier_id;type:bigint;comment:关联载体ID
  • 2.6 在 internal/model/carrier.goCarrier 结构体中新增 RealnameLinkType stringcolumn:realname_link_type;type:varchar(20);not null;default:'none';comment:实名链接类型 none-不支持 template-模板URL gateway-Gateway接口)和 RealnameLinkTemplate stringcolumn:realname_link_template;type:varchar(500);default:'';comment:实名链接模板URL
  • 2.7 在 internal/model/shop_package_allocation.goShopPackageAllocation 结构体中新增 RetailPrice int64 字段gorm: column:retail_price;type:bigint;not null;default:0;comment:代理面向终端客户的零售价(分)
  • 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. 数据库迁移

  • 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
  • 3.2 在同一迁移文件中添加存量数据修复 SQLUPDATE 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
  • 3.3 在同一迁移文件中添加索引变更DROP idx_personal_customer_wx_open_id 唯一索引CREATE 普通索引 idx_personal_customer_wx_open_id ON tb_personal_customer(wx_open_id)
  • 3.4 编写 down 迁移文件(回滚用),包含对应的 DROP COLUMN 和索引恢复语句
  • 3.5 执行迁移,使用 PostgreSQL MCP 工具验证所有字段已添加、存量 retail_price 已更新、索引已变更

4. BUG-1 修复:代理零售价

  • 4.1 修改 internal/service/purchase_validation/service.goGetPurchasePrice() 方法(约第 172 行):增加渠道判断,代理渠道(card.ShopID > 0)时查询 ShopPackageAllocation 获取 RetailPrice 并返回,平台渠道继续返回 Package.SuggestedRetailPrice
  • 4.2 修改 internal/service/purchase_validation/service.govalidatePackages() 方法(约第 148 行):将 totalPrice += pkg.SuggestedRetailPrice 改为按渠道取价逻辑(复用 4.1 的渠道判断),代理渠道额外校验 allocation.RetailPrice >= allocation.CostPrice,不满足则该套餐视为不可购买
  • 4.3 修改 internal/service/shop_package_batch_allocation/service.go:创建分配记录时设置 RetailPricePackage.SuggestedRetailPrice(约第 84-105 行创建 allocation 的位置)
  • 4.4 修改 internal/service/shop_series_grant/service.go:创建/更新 ShopPackageAllocation 时同步设置 RetailPrice(约第 302-352 行和第 614-685 行)
  • 4.5 在 internal/service/shop_package_batch_pricing/service.goBatchUpdatePricing() 方法中新增 cost_price 锁定检查:更新每条 allocation 的 cost_price 前,查询是否存在下级分配记录(ShopPackageAllocation WHERE allocator_shop_id = allocation.ShopID AND package_id = allocation.PackageID),存在则跳过该条并记录到响应的 skipped 列表,附带原因"存在下级分配记录,请先回收后再修改成本价"
  • 4.6 在 internal/service/shop_series_grant/service.go 中同步添加 cost_price 锁定检查:更新现有 allocation 的 CostPrice 时(约第 634 行),查询是否存在下级分配记录,存在则拒绝修改并返回错误

5. 代理零售价后台管理

  • 5.1 修改 internal/model/dto/shop_package_batch_pricing_dto.goBatchUpdateCostPriceRequest:新增 PricingTarget string 字段(json:"pricing_target" validate:"omitempty,oneof=cost_price retail_price" description:"调价目标 cost_price-成本价(默认) retail_price-零售价"),不传时默认为 cost_price 保持向后兼容
  • 5.2 修改 internal/service/shop_package_batch_pricing/service.goBatchUpdatePricing() 方法:根据 req.PricingTarget 分流——cost_price 走现有逻辑(含 4.5 的锁定检查),retail_price 走新逻辑:计算新零售价、校验 newRetailPrice >= allocation.CostPrice(不满足则跳过并报错"零售价不能低于成本价")、更新 allocation.RetailPrice、记录价格历史(ShopPackageAllocationPriceHistory 增加 old_retail_price/new_retail_price 字段或复用 OldCostPrice/NewCostPrice 字段并新增 price_type 标识)
  • 5.3 修改 internal/model/dto/package_dto.goPackageResponse 结构体:新增 RetailPrice *int64 字段(json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"
  • 5.4 修改 internal/service/package/service.gotoResponse() 方法(约第 530-541 行):代理用户查询时,从 allocation 读取 RetailPrice 设入 resp.RetailPrice;同时修正 ProfitMargin 计算:从 pkg.SuggestedRetailPrice - allocation.CostPrice 改为 allocation.RetailPrice - allocation.CostPrice
  • 5.5 修改 internal/service/package/service.gotoResponseWithAllocation() 方法(约第 595-603 行):同 5.4,从 allocation 读取 RetailPrice、修正 ProfitMargin 计算

6. Carrier 管理 DTO 更新

  • 6.1 修改 internal/model/dto/carrier_dto.go(或 Carrier 相关 DTO 文件):在 CarrierCreateRequestCarrierUpdateRequest 中新增 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} 占位符"
  • 6.2 修改 Carrier Service 的 Create/Update 方法:将 DTO 中的 RealnameLinkTypeRealnameLinkTemplate 写入 Carrier 模型Update 时 realname_link_typetemplate 时校验 realname_link_template 非空
  • 6.3 修改 Carrier 的响应 DTOCarrierResponse):新增 RealnameLinkTypeRealnameLinkTemplate 字段,后台列表/详情可展示

7. 后台订单 generation 快照

  • 7.1 修改 internal/service/order/service.goCreateAdminOrder() 方法:创建 Order 时从资产IotCard/Device获取当前 Generation 值并设入 order.Generation,不再依赖数据库默认值 1

8. 资产手动停用

  • 8.1 在 internal/handler/admin/asset.go(或新建 internal/handler/admin/asset_lifecycle.go)新增 DeactivateAsset Handler 方法:接收资产类型和 ID调用 Service 将 asset_status 设为 constants.AssetStatusDeactivated4
  • 8.2 在 internal/service/asset/service.go(或相关 Service新增 Deactivate() 方法:校验当前 asset_status 为 1在库或 2已销售才允许停用已换货3或已停用4的拒绝操作使用条件更新 WHERE asset_status IN (1, 2) 确保幂等
  • 8.3 在 internal/routes/ 注册停用路由:PATCH /api/admin/iot-cards/:id/deactivatePATCH /api/admin/devices/:id/deactivate(或统一为 PATCH /api/admin/assets/:type/:id/deactivate
  • 8.4 更新 cmd/api/docs.gocmd/gendocs/main.go:如新增了 Handler 则同步注册到文档生成器

9. BUG-2 修复:一次性佣金触发条件

  • 9.1 修改 internal/service/commission_calculation/service.go 中的 triggerOneTimeCommissionForCardInTx 方法:将 if order.IsPurchaseOnBehalf 的跳过逻辑改为 if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient,即代购订单或非客户端来源订单均跳过一次性佣金
  • 9.2 修改 internal/service/commission_calculation/service.go 中的 triggerOneTimeCommissionForDeviceInTx 方法:同 9.1 逻辑

10. BUG-4 修复:充值回调事务一致性

  • 10.1 修改 internal/store/postgres/asset_recharge_store.goUpdateStatusWithOptimisticLock 方法:新增 tx *gorm.DB 参数(方法签名变更),内部使用传入的 tx 替代 s.db.WithContext(ctx);同时保留原方法签名的兼容版本或修改所有调用点
  • 10.2 修改 internal/store/postgres/asset_recharge_store.goUpdatePaymentInfo 方法:同 10.1,新增 tx *gorm.DB 参数
  • 10.3 修改 internal/service/recharge/service.goHandlePaymentCallback 方法(约第 308-321 行):在事务闭包内调用 Store 方法时传入事务 tx,确保 UpdateStatusWithOptimisticLockUpdatePaymentInfo 和钱包入账在同一事务内
  • 10.4 检查并更新 UpdateStatusWithOptimisticLockUpdatePaymentInfo 的其他调用点(如有),确保传入正确的 db 或 tx 参数

11. 旧 H5 接口清理

  • 11.1 删除 internal/handler/h5/ 目录下全部 5 个文件:auth.goorder.gorecharge.gopackage_usage.goenterprise_device.go
  • 11.2 删除 internal/routes/h5.gointernal/routes/h5_enterprise_device.gointernal/routes/h5_package_usage.go
  • 11.3 修改 internal/routes/routes.go:移除 /api/h5 路由组挂载(约第 31-33 行)
  • 11.4 修改 internal/routes/order.go:移除 registerH5OrderRoutes 函数(约第 56-105 行)
  • 11.5 修改 internal/routes/recharge.go:移除 registerH5RechargeRoutes 函数(约第 11-44 行)
  • 11.6 修改 internal/bootstrap/handlers.go:移除 H5 Handler 构造H5Auth、EnterpriseDeviceH5、H5PackageUsage、H5Order、H5Recharge约第 24、31、44、49、50 行)
  • 11.7 修改 internal/bootstrap/types.go:移除 Handlers 结构体中的 H5 Handler 字段(约第 22、29、42、47、48 行)
  • 11.8 修改 internal/bootstrap/middlewares.go:移除 createH5AuthMiddleware 函数和 H5 跳过路径配置(约第 72-95 行)
  • 11.9 修改 pkg/openapi/handlers.go:移除文档生成中的 H5 Handler 构造EnterpriseDeviceH5、H5PackageUsage、H5Order、H5Recharge
  • 11.10 修改 cmd/api/main.go:移除 /api/h5 限流挂载(约第 250-257 行)

12. 旧个人客户登录接口清理

  • 12.1 修改 internal/handler/app/personal_customer.go:删除 Login约第 79 行、SendCode约第 35 行、WechatOAuthLogin约第 114 行、BindWechat约第 134 行)方法。保留 UpdateProfile 和 GetProfile
  • 12.2 修改 internal/routes/personal.go移除指向已删除方法的路由注册Login、SendCode、WechatOAuthLogin、BindWechat 的路由)
  • 12.3 检查 internal/bootstrap/handlers.gointernal/bootstrap/types.go:如果 PersonalCustomer Handler 有初始化引用已删除方法的依赖,同步清理

13. 验证

  • 13.1 执行 go build ./... 确认编译通过,无任何编译错误
  • 13.2 对所有修改的文件执行 lsp_diagnostics 确认无错误和警告
  • 13.3 使用 PostgreSQL MCP 工具验证数据库:确认 7 张表的新字段存在、默认值正确、存量 retail_price 已填充、wx_open_id 索引已变更
  • 13.4 验证删除的 H5 路由不再注册:检查代码中无 /api/h5 相关路由残留
  • 13.5 验证 BatchUpdatePricing 接口扩展:确认 pricing_target=retail_price 参数可正常使用,不传时默认走 cost_price 逻辑(向后兼容)
  • 13.6 验证代理套餐列表:确认 PackageResponse 包含 retail_price 字段,profit_margin 计算基于 retail_price - cost_price
  • 13.7 撰写功能总结文档 docs/client-api-data-model-fixes/功能总结.md,记录所有变更内容