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:
2026-03-19 10:56:50 +08:00
parent 817d0d6e04
commit ec86dbf463
70 changed files with 1438 additions and 1188 deletions

View File

@@ -0,0 +1,107 @@
## 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`,记录所有变更内容