## 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` 字段,批量调价接口仅保留成本价路径 - [x] 5.2 修改 `internal/service/shop_package_batch_pricing/service.go` 的 `BatchUpdatePricing()` 方法:删除 `retail_price` 分支与默认分流逻辑,仅保留 `cost_price` 调整(包含 4.5 的锁定检查) - [x] 5.3 在 `internal/model/dto/package_dto.go` 新增 `UpdateRetailPriceRequest` 与 `UpdateRetailPriceParams`,用于代理修改自己零售价 - [x] 5.4 在 `internal/store/postgres/shop_package_allocation_store.go` 新增 `UpdateRetailPrice(ctx context.Context, id uint, retailPrice int64, updater uint) error` - [x] 5.5 在 `internal/service/package/service.go` 新增 `UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) error`:仅代理可调用、校验 `retail_price >= cost_price` - [x] 5.6 在 `internal/handler/admin/package.go` 新增 `UpdateRetailPrice`,并在 `internal/routes/package.go` 注册 `PATCH /api/admin/packages/:id/retail-price` - [x] 5.7 修改 `internal/model/dto/package_dto.go` 的 `PackageResponse` 结构体:新增 `RetailPrice *int64` 字段(`json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`) - [x] 5.8 修改 `internal/service/package/service.go` 的 `toResponse()` 方法(约第 530-541 行):代理用户查询时,从 allocation 读取 `RetailPrice` 设入 `resp.RetailPrice`;同时修正 `ProfitMargin` 计算:从 `pkg.SuggestedRetailPrice - allocation.CostPrice` 改为 `allocation.RetailPrice - allocation.CostPrice` - [x] 5.9 修改 `internal/service/package/service.go` 的 `toResponseWithAllocation()` 方法(约第 595-603 行):同 5.8,从 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` 接口仅支持成本价调整;并验证 `PATCH /api/admin/packages/:id/retail-price` 可供代理修改自己的零售价 - [x] 13.6 验证代理套餐列表:确认 `PackageResponse` 包含 `retail_price` 字段,`profit_margin` 计算基于 `retail_price - cost_price` - [x] 13.7 撰写功能总结文档 `docs/client-api-data-model-fixes/功能总结.md`,记录所有变更内容