diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 2a491ec..a44b246 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -630,13 +630,13 @@ components: minItems: 1 nullable: true type: array - series_allocation_id: - description: 套餐系列分配ID(0表示清除关联) + series_id: + description: 套餐系列ID(0表示清除关联) minimum: 0 type: integer required: - iccids - - series_allocation_id + - series_id type: object DtoBatchSetCardSeriesBindngResponse: properties: @@ -664,13 +664,13 @@ components: minItems: 1 nullable: true type: array - series_allocation_id: - description: 套餐系列分配ID(0表示清除关联) + series_id: + description: 套餐系列ID(0表示清除关联) minimum: 0 type: integer required: - device_ids - - series_allocation_id + - series_id type: object DtoBatchSetDeviceSeriesBindngResponse: properties: @@ -1090,9 +1090,13 @@ components: minItems: 1 nullable: true type: array + payment_method: + description: 支付方式 (wallet:钱包支付, offline:线下支付) + type: string required: - order_type - package_ids + - payment_method type: object DtoCreatePackageRequest: properties: @@ -1367,9 +1371,21 @@ components: properties: base_commission: $ref: '#/components/schemas/DtoBaseCommissionConfig' + enable_force_recharge: + description: 是否启用强充(累计充值强充) + nullable: true + type: boolean enable_one_time_commission: description: 是否启用一次性佣金 type: boolean + force_recharge_amount: + description: 强充金额(分,0表示使用阈值金额) + nullable: true + type: integer + force_recharge_trigger_type: + description: 强充触发类型(1:单次充值, 2:累计充值) + nullable: true + type: integer one_time_commission_config: $ref: '#/components/schemas/DtoOneTimeCommissionConfig' series_id: @@ -1731,8 +1747,8 @@ components: max_sim_slots: description: 最大插槽数 type: integer - series_allocation_id: - description: 套餐系列分配ID + series_id: + description: 套餐系列ID minimum: 0 nullable: true type: integer @@ -2301,8 +2317,8 @@ components: real_name_status: description: 实名状态 (0:未实名, 1:已实名) type: integer - series_allocation_id: - description: 套餐系列分配ID + series_id: + description: 套餐系列ID minimum: 0 nullable: true type: integer @@ -2707,6 +2723,9 @@ components: minimum: 0 nullable: true type: integer + is_purchase_on_behalf: + description: 是否为代购订单 + type: boolean items: description: 订单明细列表 items: @@ -3754,9 +3773,18 @@ components: created_at: description: 创建时间 type: string + enable_force_recharge: + description: 是否启用强充 + type: boolean enable_one_time_commission: description: 是否启用一次性佣金 type: boolean + force_recharge_amount: + description: 强充金额(分) + type: integer + force_recharge_trigger_type: + description: 强充触发类型(1:单次充值, 2:累计充值) + type: integer id: description: 分配ID minimum: 0 @@ -3952,8 +3980,8 @@ components: real_name_status: description: 实名状态 (0:未实名, 1:已实名) type: integer - series_allocation_id: - description: 套餐系列分配ID + series_id: + description: 套餐系列ID minimum: 0 nullable: true type: integer @@ -4405,10 +4433,22 @@ components: properties: base_commission: $ref: '#/components/schemas/DtoBaseCommissionConfig' + enable_force_recharge: + description: 是否启用强充(累计充值强充) + nullable: true + type: boolean enable_one_time_commission: description: 是否启用一次性佣金 nullable: true type: boolean + force_recharge_amount: + description: 强充金额(分,0表示使用阈值金额) + nullable: true + type: integer + force_recharge_trigger_type: + description: 强充触发类型(1:单次充值, 2:累计充值) + nullable: true + type: integer one_time_commission_config: $ref: '#/components/schemas/DtoOneTimeCommissionConfig' type: object @@ -6957,11 +6997,11 @@ paths: minimum: 0 nullable: true type: integer - - description: 套餐系列分配ID + - description: 套餐系列ID in: query - name: series_allocation_id + name: series_id schema: - description: 套餐系列分配ID + description: 套餐系列ID minimum: 0 nullable: true type: integer @@ -7836,7 +7876,7 @@ paths: - 设备管理 /api/admin/devices/series-binding: patch: - description: 批量设置或清除设备与套餐系列分配的关联关系。series_allocation_id 为 0 时表示清除关联。 + description: 批量设置或清除设备与套餐系列分配的关联关系。参数:series_id(套餐系列ID,0表示清除关联)。 requestBody: content: application/json: @@ -8880,6 +8920,8 @@ paths: /api/admin/iot-cards/import: post: description: |- + 仅平台用户可操作。 + ## ⚠️ 接口变更说明(BREAKING CHANGE) 本接口已从 `multipart/form-data` 改为 `application/json`。 @@ -8970,6 +9012,7 @@ paths: - IoT卡管理 /api/admin/iot-cards/import-tasks: get: + description: 仅平台用户可操作。 parameters: - description: 页码 in: query @@ -9084,6 +9127,7 @@ paths: - IoT卡管理 /api/admin/iot-cards/import-tasks/{id}: get: + description: 仅平台用户可操作。 parameters: - description: 任务ID in: path @@ -9151,7 +9195,7 @@ paths: - IoT卡管理 /api/admin/iot-cards/series-binding: patch: - description: 批量设置或清除卡与套餐系列分配的关联关系。series_allocation_id 为 0 时表示清除关联。 + description: 批量设置或清除卡与套餐系列分配的关联关系。参数:series_id(套餐系列ID,0表示清除关联)。 requestBody: content: application/json: @@ -9256,11 +9300,11 @@ paths: minimum: 0 nullable: true type: integer - - description: 套餐系列分配ID + - description: 套餐系列ID in: query - name: series_allocation_id + name: series_id schema: - description: 套餐系列分配ID + description: 套餐系列ID minimum: 0 nullable: true type: integer diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index bd6ab5e..cdb646e 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -107,9 +107,9 @@ func initServices(s *stores, deps *Dependencies) *services { Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger), CustomerAccount: customerAccountSvc.New(deps.DB, s.Account, s.Shop, s.Enterprise), MyCommission: myCommissionSvc.New(deps.DB, s.Shop, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.WalletTransaction), - IotCard: iotCardSvc.New(deps.DB, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation, deps.GatewayClient, deps.Logger), + IotCard: iotCardSvc.New(deps.DB, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, deps.Logger), IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient), - Device: deviceSvc.New(deps.DB, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation), + Device: deviceSvc.New(deps.DB, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation, s.PackageSeries), DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient), AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account), Carrier: carrierSvc.New(s.Carrier), diff --git a/internal/model/device.go b/internal/model/device.go index a664f02..57263c8 100644 --- a/internal/model/device.go +++ b/internal/model/device.go @@ -25,7 +25,7 @@ type Device struct { DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"` DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"` DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"` - SeriesAllocationID *uint `gorm:"column:series_allocation_id;index;comment:套餐系列分配ID(关联ShopSeriesAllocation)" json:"series_allocation_id,omitempty"` + SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"` FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放" json:"first_commission_paid"` AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分)" json:"accumulated_recharge"` } diff --git a/internal/model/dto/device_dto.go b/internal/model/dto/device_dto.go index 1fdb908..08ddd63 100644 --- a/internal/model/dto/device_dto.go +++ b/internal/model/dto/device_dto.go @@ -3,18 +3,18 @@ package dto import "time" type ListDeviceRequest struct { - Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"` - PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"` - DeviceNo string `json:"device_no" query:"device_no" validate:"omitempty,max=100" maxLength:"100" description:"设备号(模糊查询)"` - DeviceName string `json:"device_name" query:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称(模糊查询)"` - Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` - ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID (NULL表示平台库存)"` - SeriesAllocationID *uint `json:"series_allocation_id" query:"series_allocation_id" description:"套餐系列分配ID"` - BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"` - DeviceType string `json:"device_type" query:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型"` - Manufacturer string `json:"manufacturer" query:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商(模糊查询)"` - CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"` - CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"` + Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"` + DeviceNo string `json:"device_no" query:"device_no" validate:"omitempty,max=100" maxLength:"100" description:"设备号(模糊查询)"` + DeviceName string `json:"device_name" query:"device_name" validate:"omitempty,max=255" maxLength:"255" description:"设备名称(模糊查询)"` + Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` + ShopID *uint `json:"shop_id" query:"shop_id" description:"店铺ID (NULL表示平台库存)"` + SeriesID *uint `json:"series_id" query:"series_id" description:"套餐系列ID"` + BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"` + DeviceType string `json:"device_type" query:"device_type" validate:"omitempty,max=50" maxLength:"50" description:"设备类型"` + Manufacturer string `json:"manufacturer" query:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商(模糊查询)"` + CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"` + CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"` } type DeviceResponse struct { @@ -31,7 +31,7 @@ type DeviceResponse struct { Status int `json:"status" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` StatusName string `json:"status_name" description:"状态名称"` BoundCardCount int `json:"bound_card_count" description:"已绑定卡数量"` - SeriesAllocationID *uint `json:"series_allocation_id,omitempty" description:"套餐系列分配ID"` + SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"` FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"` AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"` ActivatedAt *time.Time `json:"activated_at,omitempty" description:"激活时间"` @@ -129,8 +129,8 @@ type RecallDevicesResponse struct { // BatchSetDeviceSeriesBindngRequest 批量设置设备的套餐系列绑定请求 type BatchSetDeviceSeriesBindngRequest struct { - DeviceIDs []uint `json:"device_ids" validate:"required,min=1,max=500,dive,required" required:"true" minItems:"1" maxItems:"500" description:"设备ID列表"` - SeriesAllocationID uint `json:"series_allocation_id" validate:"required,min=0" required:"true" minimum:"0" description:"套餐系列分配ID(0表示清除关联)"` + DeviceIDs []uint `json:"device_ids" validate:"required,min=1,max=500,dive,required" required:"true" minItems:"1" maxItems:"500" description:"设备ID列表"` + SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"套餐系列ID(0表示清除关联)"` } // DeviceSeriesBindngFailedItem 设备系列绑定失败项 diff --git a/internal/model/dto/iot_card_dto.go b/internal/model/dto/iot_card_dto.go index 38e7c4f..01fb40f 100644 --- a/internal/model/dto/iot_card_dto.go +++ b/internal/model/dto/iot_card_dto.go @@ -3,20 +3,20 @@ package dto import "time" type ListStandaloneIotCardRequest struct { - Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"` - PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"` - Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` - CarrierID *uint `json:"carrier_id" query:"carrier_id" description:"运营商ID"` - ShopID *uint `json:"shop_id" query:"shop_id" description:"分销商ID"` - SeriesAllocationID *uint `json:"series_allocation_id" query:"series_allocation_id" description:"套餐系列分配ID"` - ICCID string `json:"iccid" query:"iccid" validate:"omitempty,max=20" maxLength:"20" description:"ICCID(模糊查询)"` - MSISDN string `json:"msisdn" query:"msisdn" validate:"omitempty,max=20" maxLength:"20" description:"卡接入号(模糊查询)"` - BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"` - PackageID *uint `json:"package_id" query:"package_id" description:"套餐ID"` - IsDistributed *bool `json:"is_distributed" query:"is_distributed" description:"是否已分销 (true:已分销, false:未分销)"` - IsReplaced *bool `json:"is_replaced" query:"is_replaced" description:"是否有换卡记录 (true:有换卡记录, false:无换卡记录)"` - ICCIDStart string `json:"iccid_start" query:"iccid_start" validate:"omitempty,max=20" maxLength:"20" description:"ICCID起始号"` - ICCIDEnd string `json:"iccid_end" query:"iccid_end" validate:"omitempty,max=20" maxLength:"20" description:"ICCID结束号"` + Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"` + Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:在库, 2:已分销, 3:已激活, 4:已停用)"` + CarrierID *uint `json:"carrier_id" query:"carrier_id" description:"运营商ID"` + ShopID *uint `json:"shop_id" query:"shop_id" description:"分销商ID"` + SeriesID *uint `json:"series_id" query:"series_id" description:"套餐系列ID"` + ICCID string `json:"iccid" query:"iccid" validate:"omitempty,max=20" maxLength:"20" description:"ICCID(模糊查询)"` + MSISDN string `json:"msisdn" query:"msisdn" validate:"omitempty,max=20" maxLength:"20" description:"卡接入号(模糊查询)"` + BatchNo string `json:"batch_no" query:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"批次号"` + PackageID *uint `json:"package_id" query:"package_id" description:"套餐ID"` + IsDistributed *bool `json:"is_distributed" query:"is_distributed" description:"是否已分销 (true:已分销, false:未分销)"` + IsReplaced *bool `json:"is_replaced" query:"is_replaced" description:"是否有换卡记录 (true:有换卡记录, false:无换卡记录)"` + ICCIDStart string `json:"iccid_start" query:"iccid_start" validate:"omitempty,max=20" maxLength:"20" description:"ICCID起始号"` + ICCIDEnd string `json:"iccid_end" query:"iccid_end" validate:"omitempty,max=20" maxLength:"20" description:"ICCID结束号"` } type StandaloneIotCardResponse struct { @@ -41,7 +41,7 @@ type StandaloneIotCardResponse struct { RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)"` NetworkStatus int `json:"network_status" description:"网络状态 (0:停机, 1:开机)"` DataUsageMB int64 `json:"data_usage_mb" description:"累计流量使用(MB)"` - SeriesAllocationID *uint `json:"series_allocation_id,omitempty" description:"套餐系列分配ID"` + SeriesID *uint `json:"series_id,omitempty" description:"套餐系列ID"` FirstCommissionPaid bool `json:"first_commission_paid" description:"一次性佣金是否已发放"` AccumulatedRecharge int64 `json:"accumulated_recharge" description:"累计充值金额(分)"` CreatedAt time.Time `json:"created_at" description:"创建时间"` @@ -133,8 +133,8 @@ type IotCardDetailResponse struct { // BatchSetCardSeriesBindngRequest 批量设置卡的套餐系列绑定请求 type BatchSetCardSeriesBindngRequest struct { - ICCIDs []string `json:"iccids" validate:"required,min=1,max=500,dive,required" required:"true" minItems:"1" maxItems:"500" description:"ICCID列表"` - SeriesAllocationID uint `json:"series_allocation_id" validate:"required,min=0" required:"true" minimum:"0" description:"套餐系列分配ID(0表示清除关联)"` + ICCIDs []string `json:"iccids" validate:"required,min=1,max=500,dive,required" required:"true" minItems:"1" maxItems:"500" description:"ICCID列表"` + SeriesID uint `json:"series_id" validate:"required,min=0" required:"true" minimum:"0" description:"套餐系列ID(0表示清除关联)"` } // CardSeriesBindngFailedItem 卡系列绑定失败项 diff --git a/internal/model/iot_card.go b/internal/model/iot_card.go index 407258e..b37367d 100644 --- a/internal/model/iot_card.go +++ b/internal/model/iot_card.go @@ -35,7 +35,7 @@ type IotCard struct { LastDataCheckAt *time.Time `gorm:"column:last_data_check_at;comment:最后一次流量检查时间" json:"last_data_check_at"` LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"` LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"` - SeriesAllocationID *uint `gorm:"column:series_allocation_id;index;comment:套餐系列分配ID(关联ShopSeriesAllocation)" json:"series_allocation_id,omitempty"` + SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"` FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放" json:"first_commission_paid"` AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分)" json:"accumulated_recharge"` } diff --git a/internal/routes/device.go b/internal/routes/device.go index b08e686..be53ca4 100644 --- a/internal/routes/device.go +++ b/internal/routes/device.go @@ -137,7 +137,7 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp Register(devices, doc, groupPath, "PATCH", "/series-binding", handler.BatchSetSeriesBinding, RouteSpec{ Summary: "批量设置设备的套餐系列绑定", - Description: "批量设置或清除设备与套餐系列分配的关联关系。series_allocation_id 为 0 时表示清除关联。", + Description: "批量设置或清除设备与套餐系列分配的关联关系。参数:series_id(套餐系列ID,0表示清除关联)。", Tags: []string{"设备管理"}, Input: new(dto.BatchSetDeviceSeriesBindngRequest), Output: new(dto.BatchSetDeviceSeriesBindngResponse), diff --git a/internal/routes/iot_card.go b/internal/routes/iot_card.go index 97a05d9..3c6633e 100644 --- a/internal/routes/iot_card.go +++ b/internal/routes/iot_card.go @@ -101,7 +101,7 @@ func registerIotCardRoutes(router fiber.Router, handler *admin.IotCardHandler, i Register(iotCards, doc, groupPath, "PATCH", "/series-binding", handler.BatchSetSeriesBinding, RouteSpec{ Summary: "批量设置卡的套餐系列绑定", - Description: "批量设置或清除卡与套餐系列分配的关联关系。series_allocation_id 为 0 时表示清除关联。", + Description: "批量设置或清除卡与套餐系列分配的关联关系。参数:series_id(套餐系列ID,0表示清除关联)。", Tags: []string{"IoT卡管理"}, Input: new(dto.BatchSetCardSeriesBindngRequest), Output: new(dto.BatchSetCardSeriesBindngResponse), diff --git a/internal/service/commission_calculation/service.go b/internal/service/commission_calculation/service.go index 7423aaa..86a03a7 100644 --- a/internal/service/commission_calculation/service.go +++ b/internal/service/commission_calculation/service.go @@ -202,11 +202,11 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g return errors.Wrap(errors.CodeDatabaseError, err, "获取卡信息失败") } - if card.SeriesAllocationID == nil { + if card.SeriesID == nil || card.ShopID == nil { return nil } - allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID) + allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *card.ShopID, *card.SeriesID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "获取系列分配失败") } @@ -302,11 +302,11 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx return errors.Wrap(errors.CodeDatabaseError, err, "获取设备信息失败") } - if device.SeriesAllocationID == nil { + if device.SeriesID == nil || device.ShopID == nil { return nil } - allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, *device.SeriesAllocationID) + allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *device.ShopID, *device.SeriesID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "获取系列分配失败") } diff --git a/internal/service/commission_calculation/service_test.go b/internal/service/commission_calculation/service_test.go index 3bf8608..5989e21 100644 --- a/internal/service/commission_calculation/service_test.go +++ b/internal/service/commission_calculation/service_test.go @@ -101,7 +101,7 @@ func TestCalculateCommission_PurchaseOnBehalf(t *testing.T) { }, ICCID: "89860000000000000001", ShopID: &shop.ID, - SeriesAllocationID: &allocation.ID, + SeriesID: &allocation.SeriesID, AccumulatedRecharge: 0, FirstCommissionPaid: false, } @@ -278,7 +278,7 @@ func TestCalculateCommission_Device_PurchaseOnBehalf(t *testing.T) { }, DeviceNo: "DEV001", ShopID: &shop.ID, - SeriesAllocationID: &allocation.ID, + SeriesID: &allocation.SeriesID, AccumulatedRecharge: 0, FirstCommissionPaid: false, } diff --git a/internal/service/device/service.go b/internal/service/device/service.go index f79ae26..f1e496d 100644 --- a/internal/service/device/service.go +++ b/internal/service/device/service.go @@ -20,6 +20,8 @@ type Service struct { shopStore *postgres.ShopStore assetAllocationRecordStore *postgres.AssetAllocationRecordStore seriesAllocationStore *postgres.ShopSeriesAllocationStore + packageSeriesStore *postgres.PackageSeriesStore + shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore } func New( @@ -30,6 +32,7 @@ func New( shopStore *postgres.ShopStore, assetAllocationRecordStore *postgres.AssetAllocationRecordStore, seriesAllocationStore *postgres.ShopSeriesAllocationStore, + packageSeriesStore *postgres.PackageSeriesStore, ) *Service { return &Service{ db: db, @@ -39,6 +42,8 @@ func New( shopStore: shopStore, assetAllocationRecordStore: assetAllocationRecordStore, seriesAllocationStore: seriesAllocationStore, + packageSeriesStore: packageSeriesStore, + shopSeriesAllocationStore: seriesAllocationStore, } } @@ -86,8 +91,8 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li if req.CreatedAtEnd != nil { filters["created_at_end"] = *req.CreatedAtEnd } - if req.SeriesAllocationID != nil { - filters["series_allocation_id"] = *req.SeriesAllocationID + if req.SeriesID != nil { + filters["series_id"] = *req.SeriesID } devices, total, err := s.deviceStore.List(ctx, opts, filters) @@ -466,7 +471,7 @@ func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string Status: device.Status, StatusName: s.getDeviceStatusName(device.Status), BoundCardCount: int(bindingCounts[device.ID]), - SeriesAllocationID: device.SeriesAllocationID, + SeriesID: device.SeriesID, FirstCommissionPaid: device.FirstCommissionPaid, AccumulatedRecharge: device.AccumulatedRecharge, ActivatedAt: device.ActivatedAt, @@ -598,17 +603,18 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe deviceMap[device.ID] = device } - var seriesAllocation *model.ShopSeriesAllocation - if req.SeriesAllocationID > 0 { - seriesAllocation, err = s.seriesAllocationStore.GetByID(ctx, req.SeriesAllocationID) + // 验证系列存在(仅当 SeriesID > 0 时) + var packageSeries *model.PackageSeries + if req.SeriesID > 0 { + packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID) if err != nil { if err == gorm.ErrRecordNotFound { - return nil, errors.New(errors.CodeNotFound, "套餐系列分配不存在") + return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用") } return nil, err } - if seriesAllocation.Status != 1 { - return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用") + if packageSeries.Status != 1 { + return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用") } } @@ -626,17 +632,23 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe continue } - if req.SeriesAllocationID > 0 { - if device.ShopID == nil || *device.ShopID != seriesAllocation.ShopID { - failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{ - DeviceID: device.ID, - DeviceNo: device.DeviceNo, - Reason: "设备不属于套餐系列分配的店铺", - }) - continue + // 验证操作者权限(仅代理用户) + if operatorShopID != nil && req.SeriesID > 0 { + allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *operatorShopID, req.SeriesID) + if err != nil { + if err == gorm.ErrRecordNotFound || allocation.Status != 1 { + failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{ + DeviceID: deviceID, + DeviceNo: device.DeviceNo, + Reason: "您没有权限分配该套餐系列", + }) + continue + } + return nil, err } } + // 验证设备权限(基于 device.ShopID) if operatorShopID != nil { if device.ShopID == nil || *device.ShopID != *operatorShopID { failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{ @@ -652,11 +664,11 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe } if len(successDeviceIDs) > 0 { - var seriesAllocationIDPtr *uint - if req.SeriesAllocationID > 0 { - seriesAllocationIDPtr = &req.SeriesAllocationID + var seriesIDPtr *uint + if req.SeriesID > 0 { + seriesIDPtr = &req.SeriesID } - if err := s.deviceStore.BatchUpdateSeriesAllocation(ctx, successDeviceIDs, seriesAllocationIDPtr); err != nil { + if err := s.deviceStore.BatchUpdateSeriesID(ctx, successDeviceIDs, seriesIDPtr); err != nil { return nil, err } } diff --git a/internal/service/device/service_test.go b/internal/service/device/service_test.go index 15ecee5..76c5a5a 100644 --- a/internal/service/device/service_test.go +++ b/internal/service/device/service_test.go @@ -29,8 +29,9 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { shopStore := postgres.NewShopStore(tx, rdb) assetAllocationRecordStore := postgres.NewAssetAllocationRecordStore(tx, rdb) seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx) + packageSeriesStore := postgres.NewPackageSeriesStore(tx) - svc := New(tx, deviceStore, deviceSimBindingStore, iotCardStore, shopStore, assetAllocationRecordStore, seriesAllocationStore) + svc := New(tx, deviceStore, deviceSimBindingStore, iotCardStore, shopStore, assetAllocationRecordStore, seriesAllocationStore, packageSeriesStore) ctx := context.Background() shop := &model.Shop{ @@ -65,8 +66,8 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { t.Run("成功设置系列绑定", func(t *testing.T) { req := &dto.BatchSetDeviceSeriesBindngRequest{ - DeviceIDs: []uint{devices[0].ID, devices[1].ID}, - SeriesAllocationID: allocation.ID, + DeviceIDs: []uint{devices[0].ID, devices[1].ID}, + SeriesID: allocation.SeriesID, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -77,15 +78,15 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { var updatedDevices []*model.Device require.NoError(t, tx.Where("id IN ?", req.DeviceIDs).Find(&updatedDevices).Error) for _, device := range updatedDevices { - require.NotNil(t, device.SeriesAllocationID) - assert.Equal(t, allocation.ID, *device.SeriesAllocationID) + require.NotNil(t, device.SeriesID) + assert.Equal(t, allocation.SeriesID, *device.SeriesID) } }) t.Run("设备不属于套餐系列分配的店铺", func(t *testing.T) { req := &dto.BatchSetDeviceSeriesBindngRequest{ - DeviceIDs: []uint{devices[2].ID}, - SeriesAllocationID: allocation.ID, + DeviceIDs: []uint{devices[2].ID}, + SeriesID: allocation.SeriesID, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -97,8 +98,8 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { t.Run("设备不存在", func(t *testing.T) { req := &dto.BatchSetDeviceSeriesBindngRequest{ - DeviceIDs: []uint{99999}, - SeriesAllocationID: allocation.ID, + DeviceIDs: []uint{99999}, + SeriesID: allocation.SeriesID, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -110,8 +111,8 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { t.Run("清除系列绑定", func(t *testing.T) { req := &dto.BatchSetDeviceSeriesBindngRequest{ - DeviceIDs: []uint{devices[0].ID}, - SeriesAllocationID: 0, + DeviceIDs: []uint{devices[0].ID}, + SeriesID: 0, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -120,14 +121,14 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { var updatedDevice model.Device require.NoError(t, tx.First(&updatedDevice, devices[0].ID).Error) - assert.Nil(t, updatedDevice.SeriesAllocationID) + assert.Nil(t, updatedDevice.SeriesID) }) t.Run("代理用户只能操作自己店铺的设备", func(t *testing.T) { otherShopID := uint(99999) req := &dto.BatchSetDeviceSeriesBindngRequest{ - DeviceIDs: []uint{devices[1].ID}, - SeriesAllocationID: 0, + DeviceIDs: []uint{devices[1].ID}, + SeriesID: 0, } resp, err := svc.BatchSetSeriesBinding(ctx, req, &otherShopID) @@ -139,8 +140,8 @@ func TestDeviceService_BatchSetSeriesBinding(t *testing.T) { t.Run("套餐系列分配不存在", func(t *testing.T) { req := &dto.BatchSetDeviceSeriesBindngRequest{ - DeviceIDs: []uint{devices[1].ID}, - SeriesAllocationID: 99999, + DeviceIDs: []uint{devices[1].ID}, + SeriesID: 99999, } _, err := svc.BatchSetSeriesBinding(ctx, req, nil) diff --git a/internal/service/iot_card/service.go b/internal/service/iot_card/service.go index f20a1b9..14715ee 100644 --- a/internal/service/iot_card/service.go +++ b/internal/service/iot_card/service.go @@ -20,6 +20,7 @@ type Service struct { shopStore *postgres.ShopStore assetAllocationRecordStore *postgres.AssetAllocationRecordStore seriesAllocationStore *postgres.ShopSeriesAllocationStore + packageSeriesStore *postgres.PackageSeriesStore gatewayClient *gateway.Client logger *zap.Logger } @@ -30,6 +31,7 @@ func New( shopStore *postgres.ShopStore, assetAllocationRecordStore *postgres.AssetAllocationRecordStore, seriesAllocationStore *postgres.ShopSeriesAllocationStore, + packageSeriesStore *postgres.PackageSeriesStore, gatewayClient *gateway.Client, logger *zap.Logger, ) *Service { @@ -39,6 +41,7 @@ func New( shopStore: shopStore, assetAllocationRecordStore: assetAllocationRecordStore, seriesAllocationStore: seriesAllocationStore, + packageSeriesStore: packageSeriesStore, gatewayClient: gatewayClient, logger: logger, } @@ -93,8 +96,8 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot if req.IsReplaced != nil { filters["is_replaced"] = *req.IsReplaced } - if req.SeriesAllocationID != nil { - filters["series_allocation_id"] = *req.SeriesAllocationID + if req.SeriesID != nil { + filters["series_id"] = *req.SeriesID } cards, total, err := s.iotCardStore.ListStandalone(ctx, opts, filters) @@ -187,7 +190,7 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str RealNameStatus: card.RealNameStatus, NetworkStatus: card.NetworkStatus, DataUsageMB: card.DataUsageMB, - SeriesAllocationID: card.SeriesAllocationID, + SeriesID: card.SeriesID, FirstCommissionPaid: card.FirstCommissionPaid, AccumulatedRecharge: card.AccumulatedRecharge, CreatedAt: card.CreatedAt, @@ -571,17 +574,18 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa cardMap[card.ICCID] = card } - var seriesAllocation *model.ShopSeriesAllocation - if req.SeriesAllocationID > 0 { - seriesAllocation, err = s.seriesAllocationStore.GetByID(ctx, req.SeriesAllocationID) + // 验证系列存在(仅当 SeriesID > 0 时) + var packageSeries *model.PackageSeries + if req.SeriesID > 0 { + packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID) if err != nil { if err == gorm.ErrRecordNotFound { - return nil, errors.New(errors.CodeNotFound, "套餐系列分配不存在") + return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用") } return nil, err } - if seriesAllocation.Status != 1 { - return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用") + if packageSeries.Status != 1 { + return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用") } } @@ -598,16 +602,22 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa continue } - if req.SeriesAllocationID > 0 { - if card.ShopID == nil || *card.ShopID != seriesAllocation.ShopID { - failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{ - ICCID: iccid, - Reason: "卡不属于套餐系列分配的店铺", - }) - continue + // 验证操作者权限(仅代理用户) + if operatorShopID != nil && req.SeriesID > 0 { + allocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, *operatorShopID, req.SeriesID) + if err != nil { + if err == gorm.ErrRecordNotFound || allocation.Status != 1 { + failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{ + ICCID: iccid, + Reason: "您没有权限分配该套餐系列", + }) + continue + } + return nil, err } } + // 验证卡权限(基于 card.ShopID) if operatorShopID != nil { if card.ShopID == nil || *card.ShopID != *operatorShopID { failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{ @@ -622,11 +632,11 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa } if len(successCardIDs) > 0 { - var seriesAllocationIDPtr *uint - if req.SeriesAllocationID > 0 { - seriesAllocationIDPtr = &req.SeriesAllocationID + var seriesIDPtr *uint + if req.SeriesID > 0 { + seriesIDPtr = &req.SeriesID } - if err := s.iotCardStore.BatchUpdateSeriesAllocation(ctx, successCardIDs, seriesAllocationIDPtr); err != nil { + if err := s.iotCardStore.BatchUpdateSeriesID(ctx, successCardIDs, seriesIDPtr); err != nil { return nil, err } } diff --git a/internal/service/iot_card/service_test.go b/internal/service/iot_card/service_test.go index df8eb56..6e22c7c 100644 --- a/internal/service/iot_card/service_test.go +++ b/internal/service/iot_card/service_test.go @@ -28,7 +28,8 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { assetAllocationRecordStore := postgres.NewAssetAllocationRecordStore(tx, rdb) seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx) - svc := New(tx, iotCardStore, shopStore, assetAllocationRecordStore, seriesAllocationStore, nil, nil) + packageSeriesStore := postgres.NewPackageSeriesStore(tx) + svc := New(tx, iotCardStore, shopStore, assetAllocationRecordStore, seriesAllocationStore, packageSeriesStore, nil, nil) ctx := context.Background() shop := &model.Shop{ @@ -63,8 +64,8 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { t.Run("成功设置系列绑定", func(t *testing.T) { req := &dto.BatchSetCardSeriesBindngRequest{ - ICCIDs: []string{prefix + "001", prefix + "002"}, - SeriesAllocationID: allocation.ID, + ICCIDs: []string{prefix + "001", prefix + "002"}, + SeriesID: allocation.SeriesID, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -75,15 +76,15 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { var updatedCards []*model.IotCard require.NoError(t, tx.Where("iccid IN ?", req.ICCIDs).Find(&updatedCards).Error) for _, card := range updatedCards { - require.NotNil(t, card.SeriesAllocationID) - assert.Equal(t, allocation.ID, *card.SeriesAllocationID) + require.NotNil(t, card.SeriesID) + assert.Equal(t, allocation.SeriesID, *card.SeriesID) } }) t.Run("卡不属于套餐系列分配的店铺", func(t *testing.T) { req := &dto.BatchSetCardSeriesBindngRequest{ - ICCIDs: []string{prefix + "003"}, - SeriesAllocationID: allocation.ID, + ICCIDs: []string{prefix + "003"}, + SeriesID: allocation.SeriesID, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -95,8 +96,8 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { t.Run("卡不存在", func(t *testing.T) { req := &dto.BatchSetCardSeriesBindngRequest{ - ICCIDs: []string{"NOTEXIST001"}, - SeriesAllocationID: allocation.ID, + ICCIDs: []string{"NOTEXIST001"}, + SeriesID: allocation.SeriesID, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -108,8 +109,8 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { t.Run("清除系列绑定", func(t *testing.T) { req := &dto.BatchSetCardSeriesBindngRequest{ - ICCIDs: []string{prefix + "001"}, - SeriesAllocationID: 0, + ICCIDs: []string{prefix + "001"}, + SeriesID: 0, } resp, err := svc.BatchSetSeriesBinding(ctx, req, nil) @@ -118,14 +119,14 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { var updatedCard model.IotCard require.NoError(t, tx.Where("iccid = ?", prefix+"001").First(&updatedCard).Error) - assert.Nil(t, updatedCard.SeriesAllocationID) + assert.Nil(t, updatedCard.SeriesID) }) t.Run("代理用户只能操作自己店铺的卡", func(t *testing.T) { otherShopID := uint(99999) req := &dto.BatchSetCardSeriesBindngRequest{ - ICCIDs: []string{prefix + "002"}, - SeriesAllocationID: 0, + ICCIDs: []string{prefix + "002"}, + SeriesID: 0, } resp, err := svc.BatchSetSeriesBinding(ctx, req, &otherShopID) @@ -137,8 +138,8 @@ func TestIotCardService_BatchSetSeriesBinding(t *testing.T) { t.Run("套餐系列分配不存在", func(t *testing.T) { req := &dto.BatchSetCardSeriesBindngRequest{ - ICCIDs: []string{prefix + "002"}, - SeriesAllocationID: 99999, + ICCIDs: []string{prefix + "002"}, + SeriesID: 99999, } _, err := svc.BatchSetSeriesBinding(ctx, req, nil) diff --git a/internal/service/order/service_test.go b/internal/service/order/service_test.go index cd7ab1e..bbeea67 100644 --- a/internal/service/order/service_test.go +++ b/internal/service/order/service_test.go @@ -99,21 +99,21 @@ func setupOrderTestEnv(t *testing.T) *testEnv { shopIDPtr := &shop.ID card := &model.IotCard{ - ICCID: "89860000000000000002", - ShopID: shopIDPtr, - CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, - Status: constants.StatusEnabled, - BaseModel: model.BaseModel{Creator: 1, Updater: 1}, + ICCID: "89860000000000000002", + ShopID: shopIDPtr, + CarrierID: carrier.ID, + SeriesID: &allocation.SeriesID, + Status: constants.StatusEnabled, + BaseModel: model.BaseModel{Creator: 1, Updater: 1}, } require.NoError(t, iotCardStore.Create(ctx, card)) device := &model.Device{ - DeviceNo: "DEV_TEST_ORDER_001", - ShopID: shopIDPtr, - SeriesAllocationID: &allocation.ID, - Status: constants.StatusEnabled, - BaseModel: model.BaseModel{Creator: 1, Updater: 1}, + DeviceNo: "DEV_TEST_ORDER_001", + ShopID: shopIDPtr, + SeriesID: &allocation.SeriesID, + Status: constants.StatusEnabled, + BaseModel: model.BaseModel{Creator: 1, Updater: 1}, } require.NoError(t, deviceStore.Create(ctx, device)) @@ -569,12 +569,12 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) { shopIDPtr := &shop.ID card := &model.IotCard{ - ICCID: "89860000000000000099", - ShopID: shopIDPtr, - CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, - Status: constants.StatusEnabled, - BaseModel: model.BaseModel{Creator: 1, Updater: 1}, + ICCID: "89860000000000000099", + ShopID: shopIDPtr, + CarrierID: carrier.ID, + SeriesID: &allocation.SeriesID, + Status: constants.StatusEnabled, + BaseModel: model.BaseModel{Creator: 1, Updater: 1}, } require.NoError(t, iotCardStore.Create(ctx, card)) @@ -769,7 +769,7 @@ func TestOrderService_ForceRechargeValidation(t *testing.T) { ICCID: "89860000000000000FR1", ShopID: shopIDPtr, CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, + SeriesID: &allocation.SeriesID, Status: constants.StatusEnabled, FirstCommissionPaid: false, BaseModel: model.BaseModel{Creator: 1, Updater: 1}, @@ -820,7 +820,7 @@ func TestOrderService_ForceRechargeValidation(t *testing.T) { ICCID: "89860000000000000FR2", ShopID: shopIDPtr, CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, + SeriesID: &allocation.SeriesID, Status: constants.StatusEnabled, FirstCommissionPaid: true, BaseModel: model.BaseModel{Creator: 1, Updater: 1}, @@ -917,7 +917,7 @@ func TestOrderService_GetPurchaseCheck(t *testing.T) { ICCID: "89860000000000000PC1", ShopID: shopIDPtr, CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, + SeriesID: &allocation.SeriesID, Status: constants.StatusEnabled, FirstCommissionPaid: false, BaseModel: model.BaseModel{Creator: 1, Updater: 1}, @@ -949,7 +949,7 @@ func TestOrderService_GetPurchaseCheck(t *testing.T) { ICCID: "89860000000000000PC2", ShopID: shopIDPtr, CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, + SeriesID: &allocation.SeriesID, Status: constants.StatusEnabled, FirstCommissionPaid: true, BaseModel: model.BaseModel{Creator: 1, Updater: 1}, @@ -1055,12 +1055,12 @@ func TestOrderService_WalletPay_PurchaseOnBehalf(t *testing.T) { shopIDPtr := &shop.ID card := &model.IotCard{ - ICCID: "89860000000000000WP1", - ShopID: shopIDPtr, - CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, - Status: constants.StatusEnabled, - BaseModel: model.BaseModel{Creator: 1, Updater: 1}, + ICCID: "89860000000000000WP1", + ShopID: shopIDPtr, + CarrierID: carrier.ID, + SeriesID: &allocation.SeriesID, + Status: constants.StatusEnabled, + BaseModel: model.BaseModel{Creator: 1, Updater: 1}, } require.NoError(t, iotCardStore.Create(ctx, card)) diff --git a/internal/service/purchase_validation/service.go b/internal/service/purchase_validation/service.go index b3542db..7012cf2 100644 --- a/internal/service/purchase_validation/service.go +++ b/internal/service/purchase_validation/service.go @@ -51,23 +51,11 @@ func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, package return nil, err } - if card.SeriesAllocationID == nil || *card.SeriesAllocationID == 0 { + if card.SeriesID == nil || *card.SeriesID == 0 { return nil, errors.New(errors.CodeInvalidParam, "该卡未关联套餐系列,无法购买套餐") } - allocation, err := s.seriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配不存在") - } - return nil, err - } - - if allocation.Status != constants.StatusEnabled { - return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用") - } - - packages, totalPrice, err := s.validatePackages(ctx, packageIDs, allocation.SeriesID) + packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *card.SeriesID) if err != nil { return nil, err } @@ -76,7 +64,6 @@ func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, package Card: card, Packages: packages, TotalPrice: totalPrice, - Allocation: allocation, }, nil } @@ -89,23 +76,11 @@ func (s *Service) ValidateDevicePurchase(ctx context.Context, deviceID uint, pac return nil, err } - if device.SeriesAllocationID == nil || *device.SeriesAllocationID == 0 { + if device.SeriesID == nil || *device.SeriesID == 0 { return nil, errors.New(errors.CodeInvalidParam, "该设备未关联套餐系列,无法购买套餐") } - allocation, err := s.seriesAllocationStore.GetByID(ctx, *device.SeriesAllocationID) - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配不存在") - } - return nil, err - } - - if allocation.Status != constants.StatusEnabled { - return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用") - } - - packages, totalPrice, err := s.validatePackages(ctx, packageIDs, allocation.SeriesID) + packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *device.SeriesID) if err != nil { return nil, err } @@ -114,7 +89,6 @@ func (s *Service) ValidateDevicePurchase(ctx context.Context, deviceID uint, pac Device: device, Packages: packages, TotalPrice: totalPrice, - Allocation: allocation, }, nil } diff --git a/internal/service/purchase_validation/service_test.go b/internal/service/purchase_validation/service_test.go index e86b190..ebffc4b 100644 --- a/internal/service/purchase_validation/service_test.go +++ b/internal/service/purchase_validation/service_test.go @@ -75,21 +75,21 @@ func setupTestData(t *testing.T) (context.Context, *Service, *model.IotCard, *mo shopIDPtr := &shop.ID card := &model.IotCard{ - ICCID: "89860000000000000001", - ShopID: shopIDPtr, - CarrierID: carrier.ID, - SeriesAllocationID: &allocation.ID, - Status: constants.StatusEnabled, - BaseModel: model.BaseModel{Creator: 1, Updater: 1}, + ICCID: "89860000000000000001", + ShopID: shopIDPtr, + CarrierID: carrier.ID, + SeriesID: &series.ID, + Status: constants.StatusEnabled, + BaseModel: model.BaseModel{Creator: 1, Updater: 1}, } require.NoError(t, iotCardStore.Create(ctx, card)) device := &model.Device{ - DeviceNo: "DEV_TEST_PV_001", - ShopID: shopIDPtr, - SeriesAllocationID: &allocation.ID, - Status: constants.StatusEnabled, - BaseModel: model.BaseModel{Creator: 1, Updater: 1}, + DeviceNo: "DEV_TEST_PV_001", + ShopID: shopIDPtr, + SeriesID: &series.ID, + Status: constants.StatusEnabled, + BaseModel: model.BaseModel{Creator: 1, Updater: 1}, } require.NoError(t, deviceStore.Create(ctx, device)) diff --git a/internal/service/recharge/service.go b/internal/service/recharge/service.go index 7d5068c..15350c8 100644 --- a/internal/service/recharge/service.go +++ b/internal/service/recharge/service.go @@ -374,7 +374,8 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp Message: "无强充要求,可自由充值", } - var seriesAllocationID *uint + var seriesID *uint + var shopID *uint var accumulatedRecharge int64 var firstCommissionPaid bool @@ -387,7 +388,8 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") } - seriesAllocationID = card.SeriesAllocationID + seriesID = card.SeriesID + shopID = card.ShopID accumulatedRecharge = card.AccumulatedRecharge firstCommissionPaid = card.FirstCommissionPaid } else if resourceType == "device" { @@ -398,7 +400,8 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") } - seriesAllocationID = device.SeriesAllocationID + seriesID = device.SeriesID + shopID = device.ShopID accumulatedRecharge = device.AccumulatedRecharge firstCommissionPaid = device.FirstCommissionPaid } @@ -406,13 +409,13 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp result.CurrentAccumulated = accumulatedRecharge result.FirstCommissionPaid = firstCommissionPaid - // 2. 如果没有系列分配,无强充要求 - if seriesAllocationID == nil { + // 2. 如果没有系列ID或店铺ID,无强充要求 + if seriesID == nil || shopID == nil { return result, nil } // 3. 查询系列分配配置 - allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, *seriesAllocationID) + allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID) if err != nil { if err == gorm.ErrRecordNotFound { return result, nil @@ -483,7 +486,7 @@ func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB // triggerOneTimeCommissionIfNeededInTx 触发一次性佣金(事务内使用) // 检查是否满足一次性佣金触发条件,满足则创建佣金记录并入账 func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, rechargeAmount int64, userID uint) error { - var seriesAllocationID *uint + var seriesID *uint var accumulatedRecharge int64 var firstCommissionPaid bool var shopID *uint @@ -494,7 +497,7 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx * if err := tx.First(&card, resourceID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") } - seriesAllocationID = card.SeriesAllocationID + seriesID = card.SeriesID accumulatedRecharge = card.AccumulatedRecharge firstCommissionPaid = card.FirstCommissionPaid shopID = card.ShopID @@ -503,14 +506,14 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx * if err := tx.First(&device, resourceID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") } - seriesAllocationID = device.SeriesAllocationID + seriesID = device.SeriesID accumulatedRecharge = device.AccumulatedRecharge firstCommissionPaid = device.FirstCommissionPaid shopID = device.ShopID } - // 2. 如果没有系列分配或已发放佣金,跳过 - if seriesAllocationID == nil || firstCommissionPaid { + // 2. 如果没有系列ID或已发放佣金,跳过 + if seriesID == nil || firstCommissionPaid { return nil } @@ -524,7 +527,7 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx * } // 4. 查询系列分配配置 - allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, *seriesAllocationID) + allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID) if err != nil { if err == gorm.ErrRecordNotFound { return nil diff --git a/internal/service/recharge/service_test.go b/internal/service/recharge/service_test.go index 117d5f3..967b0ef 100644 --- a/internal/service/recharge/service_test.go +++ b/internal/service/recharge/service_test.go @@ -60,15 +60,15 @@ func createTestIotCard(t *testing.T, tx *gorm.DB, shopID *uint, seriesAllocation Creator: 1, Updater: 1, }, - ICCID: fmt.Sprintf("89860%014d", timestamp%100000000000000), - CardType: "流量卡", - CardCategory: "normal", - CarrierID: 1, - CarrierType: "CMCC", - CarrierName: "中国移动", - Status: 1, - ShopID: shopID, - SeriesAllocationID: seriesAllocationID, + ICCID: fmt.Sprintf("89860%014d", timestamp%100000000000000), + CardType: "流量卡", + CardCategory: "normal", + CarrierID: 1, + CarrierType: "CMCC", + CarrierName: "中国移动", + Status: 1, + ShopID: shopID, + SeriesID: seriesAllocationID, } require.NoError(t, tx.Create(card).Error) return card @@ -83,12 +83,12 @@ func createTestDevice(t *testing.T, tx *gorm.DB, shopID *uint, seriesAllocationI Creator: 1, Updater: 1, }, - DeviceNo: fmt.Sprintf("DEV%014d", timestamp%100000000000000), - DeviceName: "测试设备", - DeviceType: "GPS", - Status: 1, - ShopID: shopID, - SeriesAllocationID: seriesAllocationID, + DeviceNo: fmt.Sprintf("DEV%014d", timestamp%100000000000000), + DeviceName: "测试设备", + DeviceType: "GPS", + Status: 1, + ShopID: shopID, + SeriesID: seriesAllocationID, } require.NoError(t, tx.Create(device).Error) return device diff --git a/internal/store/postgres/device_store.go b/internal/store/postgres/device_store.go index 43df924..ef3e940 100644 --- a/internal/store/postgres/device_store.go +++ b/internal/store/postgres/device_store.go @@ -106,8 +106,8 @@ func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filter if createdAtEnd, ok := filters["created_at_end"].(time.Time); ok && !createdAtEnd.IsZero() { query = query.Where("created_at <= ?", createdAtEnd) } - if seriesAllocationID, ok := filters["series_allocation_id"].(uint); ok && seriesAllocationID > 0 { - query = query.Where("series_allocation_id = ?", seriesAllocationID) + if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 { + query = query.Where("series_id = ?", seriesID) } if err := query.Count(&total).Error; err != nil { @@ -185,20 +185,20 @@ func (s *DeviceStore) GetByDeviceNos(ctx context.Context, deviceNos []string) ([ return devices, nil } -// BatchUpdateSeriesAllocation 批量更新设备的套餐系列分配 -func (s *DeviceStore) BatchUpdateSeriesAllocation(ctx context.Context, deviceIDs []uint, seriesAllocationID *uint) error { +// BatchUpdateSeriesID 批量更新设备的套餐系列ID +func (s *DeviceStore) BatchUpdateSeriesID(ctx context.Context, deviceIDs []uint, seriesID *uint) error { if len(deviceIDs) == 0 { return nil } return s.db.WithContext(ctx).Model(&model.Device{}). Where("id IN ?", deviceIDs). - Update("series_allocation_id", seriesAllocationID).Error + Update("series_id", seriesID).Error } -// ListBySeriesAllocationID 根据套餐系列分配ID查询设备列表 -func (s *DeviceStore) ListBySeriesAllocationID(ctx context.Context, seriesAllocationID uint) ([]*model.Device, error) { +// ListBySeriesID 根据套餐系列ID查询设备列表 +func (s *DeviceStore) ListBySeriesID(ctx context.Context, seriesID uint) ([]*model.Device, error) { var devices []*model.Device - if err := s.db.WithContext(ctx).Where("series_allocation_id = ?", seriesAllocationID).Find(&devices).Error; err != nil { + if err := s.db.WithContext(ctx).Where("series_id = ?", seriesID).Find(&devices).Error; err != nil { return nil, err } return devices, nil diff --git a/internal/store/postgres/device_store_test.go b/internal/store/postgres/device_store_test.go index 3b35ef3..9bce017 100644 --- a/internal/store/postgres/device_store_test.go +++ b/internal/store/postgres/device_store_test.go @@ -16,7 +16,7 @@ func uniqueDeviceNoPrefix() string { return fmt.Sprintf("D%d", time.Now().UnixNano()%1000000000) } -func TestDeviceStore_BatchUpdateSeriesAllocation(t *testing.T) { +func TestDeviceStore_BatchUpdateSeriesID(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) @@ -31,39 +31,39 @@ func TestDeviceStore_BatchUpdateSeriesAllocation(t *testing.T) { } require.NoError(t, s.CreateBatch(ctx, devices)) - t.Run("设置系列分配ID", func(t *testing.T) { - seriesAllocationID := uint(100) + t.Run("设置系列ID", func(t *testing.T) { + seriesID := uint(100) deviceIDs := []uint{devices[0].ID, devices[1].ID} - err := s.BatchUpdateSeriesAllocation(ctx, deviceIDs, &seriesAllocationID) + err := s.BatchUpdateSeriesID(ctx, deviceIDs, &seriesID) require.NoError(t, err) var updatedDevices []*model.Device require.NoError(t, tx.Where("id IN ?", deviceIDs).Find(&updatedDevices).Error) for _, device := range updatedDevices { - require.NotNil(t, device.SeriesAllocationID) - assert.Equal(t, seriesAllocationID, *device.SeriesAllocationID) + require.NotNil(t, device.SeriesID) + assert.Equal(t, seriesID, *device.SeriesID) } }) - t.Run("清除系列分配ID", func(t *testing.T) { + t.Run("清除系列ID", func(t *testing.T) { deviceIDs := []uint{devices[0].ID} - err := s.BatchUpdateSeriesAllocation(ctx, deviceIDs, nil) + err := s.BatchUpdateSeriesID(ctx, deviceIDs, nil) require.NoError(t, err) var updatedDevice model.Device require.NoError(t, tx.First(&updatedDevice, devices[0].ID).Error) - assert.Nil(t, updatedDevice.SeriesAllocationID) + assert.Nil(t, updatedDevice.SeriesID) }) t.Run("空列表不报错", func(t *testing.T) { - err := s.BatchUpdateSeriesAllocation(ctx, []uint{}, nil) + err := s.BatchUpdateSeriesID(ctx, []uint{}, nil) require.NoError(t, err) }) } -func TestDeviceStore_ListBySeriesAllocationID(t *testing.T) { +func TestDeviceStore_ListBySeriesID(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) @@ -72,23 +72,23 @@ func TestDeviceStore_ListBySeriesAllocationID(t *testing.T) { ctx := context.Background() prefix := uniqueDeviceNoPrefix() - seriesAllocationID := uint(200) + seriesID := uint(200) devices := []*model.Device{ - {DeviceNo: prefix + "001", DeviceName: "测试设备1", Status: 1, SeriesAllocationID: &seriesAllocationID}, - {DeviceNo: prefix + "002", DeviceName: "测试设备2", Status: 1, SeriesAllocationID: &seriesAllocationID}, - {DeviceNo: prefix + "003", DeviceName: "测试设备3", Status: 1, SeriesAllocationID: nil}, + {DeviceNo: prefix + "001", DeviceName: "测试设备1", Status: 1, SeriesID: &seriesID}, + {DeviceNo: prefix + "002", DeviceName: "测试设备2", Status: 1, SeriesID: &seriesID}, + {DeviceNo: prefix + "003", DeviceName: "测试设备3", Status: 1, SeriesID: nil}, } require.NoError(t, s.CreateBatch(ctx, devices)) - result, err := s.ListBySeriesAllocationID(ctx, seriesAllocationID) + result, err := s.ListBySeriesID(ctx, seriesID) require.NoError(t, err) assert.Len(t, result, 2) for _, device := range result { - assert.Equal(t, seriesAllocationID, *device.SeriesAllocationID) + assert.Equal(t, seriesID, *device.SeriesID) } } -func TestDeviceStore_List_SeriesAllocationFilter(t *testing.T) { +func TestDeviceStore_List_SeriesIDFilter(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) @@ -97,23 +97,23 @@ func TestDeviceStore_List_SeriesAllocationFilter(t *testing.T) { ctx := context.Background() prefix := uniqueDeviceNoPrefix() - seriesAllocationID := uint(300) + seriesID := uint(300) devices := []*model.Device{ - {DeviceNo: prefix + "001", DeviceName: "测试设备1", Status: 1, SeriesAllocationID: &seriesAllocationID}, - {DeviceNo: prefix + "002", DeviceName: "测试设备2", Status: 1, SeriesAllocationID: &seriesAllocationID}, - {DeviceNo: prefix + "003", DeviceName: "测试设备3", Status: 1, SeriesAllocationID: nil}, + {DeviceNo: prefix + "001", DeviceName: "测试设备1", Status: 1, SeriesID: &seriesID}, + {DeviceNo: prefix + "002", DeviceName: "测试设备2", Status: 1, SeriesID: &seriesID}, + {DeviceNo: prefix + "003", DeviceName: "测试设备3", Status: 1, SeriesID: nil}, } require.NoError(t, s.CreateBatch(ctx, devices)) filters := map[string]interface{}{ - "series_allocation_id": seriesAllocationID, - "device_no": prefix, + "series_id": seriesID, + "device_no": prefix, } result, total, err := s.List(ctx, nil, filters) require.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, result, 2) for _, device := range result { - assert.Equal(t, seriesAllocationID, *device.SeriesAllocationID) + assert.Equal(t, seriesID, *device.SeriesID) } } diff --git a/internal/store/postgres/iot_card_store.go b/internal/store/postgres/iot_card_store.go index 853dc5a..c21fce3 100644 --- a/internal/store/postgres/iot_card_store.go +++ b/internal/store/postgres/iot_card_store.go @@ -147,8 +147,8 @@ func (s *IotCardStore) List(ctx context.Context, opts *store.QueryOptions, filte if iccidEnd, ok := filters["iccid_end"].(string); ok && iccidEnd != "" { query = query.Where("iccid <= ?", iccidEnd) } - if seriesAllocationID, ok := filters["series_allocation_id"].(uint); ok && seriesAllocationID > 0 { - query = query.Where("series_allocation_id = ?", seriesAllocationID) + if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 { + query = query.Where("series_id = ?", seriesID) } // 统计总数 @@ -242,8 +242,8 @@ func (s *IotCardStore) ListStandalone(ctx context.Context, opts *store.QueryOpti Where("deleted_at IS NULL")) } } - if seriesAllocationID, ok := filters["series_allocation_id"].(uint); ok && seriesAllocationID > 0 { - query = query.Where("series_allocation_id = ?", seriesAllocationID) + if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 { + query = query.Where("series_id = ?", seriesID) } if err := query.Count(&total).Error; err != nil { @@ -381,22 +381,22 @@ func (s *IotCardStore) GetByIDsWithEnterpriseFilter(ctx context.Context, cardIDs return cards, nil } -// BatchUpdateSeriesAllocation 批量更新卡的套餐系列分配 +// BatchUpdateSeriesID 批量更新卡的套餐系列ID // 用于批量设置或清除卡与套餐系列的关联关系 -func (s *IotCardStore) BatchUpdateSeriesAllocation(ctx context.Context, cardIDs []uint, seriesAllocationID *uint) error { +func (s *IotCardStore) BatchUpdateSeriesID(ctx context.Context, cardIDs []uint, seriesID *uint) error { if len(cardIDs) == 0 { return nil } return s.db.WithContext(ctx).Model(&model.IotCard{}). Where("id IN ?", cardIDs). - Update("series_allocation_id", seriesAllocationID).Error + Update("series_id", seriesID).Error } -// ListBySeriesAllocationID 根据套餐系列分配ID查询卡列表 -// 用于查询某个套餐系列分配下的所有卡 -func (s *IotCardStore) ListBySeriesAllocationID(ctx context.Context, seriesAllocationID uint) ([]*model.IotCard, error) { +// ListBySeriesID 根据套餐系列ID查询卡列表 +// 用于查询某个套餐系列下的所有卡 +func (s *IotCardStore) ListBySeriesID(ctx context.Context, seriesID uint) ([]*model.IotCard, error) { var cards []*model.IotCard - if err := s.db.WithContext(ctx).Where("series_allocation_id = ?", seriesAllocationID).Find(&cards).Error; err != nil { + if err := s.db.WithContext(ctx).Where("series_id = ?", seriesID).Find(&cards).Error; err != nil { return nil, err } return cards, nil diff --git a/internal/store/postgres/iot_card_store_test.go b/internal/store/postgres/iot_card_store_test.go index 3465d32..fbfd787 100644 --- a/internal/store/postgres/iot_card_store_test.go +++ b/internal/store/postgres/iot_card_store_test.go @@ -426,7 +426,7 @@ func TestIotCardStore_GetBoundCardIDs(t *testing.T) { }) } -func TestIotCardStore_BatchUpdateSeriesAllocation(t *testing.T) { +func TestIotCardStore_BatchUpdateSeriesID(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) @@ -440,39 +440,39 @@ func TestIotCardStore_BatchUpdateSeriesAllocation(t *testing.T) { } require.NoError(t, s.CreateBatch(ctx, cards)) - t.Run("设置系列分配ID", func(t *testing.T) { - seriesAllocationID := uint(100) + t.Run("设置系列ID", func(t *testing.T) { + seriesID := uint(100) cardIDs := []uint{cards[0].ID, cards[1].ID} - err := s.BatchUpdateSeriesAllocation(ctx, cardIDs, &seriesAllocationID) + err := s.BatchUpdateSeriesID(ctx, cardIDs, &seriesID) require.NoError(t, err) var updatedCards []*model.IotCard require.NoError(t, tx.Where("id IN ?", cardIDs).Find(&updatedCards).Error) for _, card := range updatedCards { - require.NotNil(t, card.SeriesAllocationID) - assert.Equal(t, seriesAllocationID, *card.SeriesAllocationID) + require.NotNil(t, card.SeriesID) + assert.Equal(t, seriesID, *card.SeriesID) } }) - t.Run("清除系列分配ID", func(t *testing.T) { + t.Run("清除系列ID", func(t *testing.T) { cardIDs := []uint{cards[0].ID} - err := s.BatchUpdateSeriesAllocation(ctx, cardIDs, nil) + err := s.BatchUpdateSeriesID(ctx, cardIDs, nil) require.NoError(t, err) var updatedCard model.IotCard require.NoError(t, tx.First(&updatedCard, cards[0].ID).Error) - assert.Nil(t, updatedCard.SeriesAllocationID) + assert.Nil(t, updatedCard.SeriesID) }) t.Run("空列表不报错", func(t *testing.T) { - err := s.BatchUpdateSeriesAllocation(ctx, []uint{}, nil) + err := s.BatchUpdateSeriesID(ctx, []uint{}, nil) require.NoError(t, err) }) } -func TestIotCardStore_ListBySeriesAllocationID(t *testing.T) { +func TestIotCardStore_ListBySeriesID(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) @@ -480,23 +480,23 @@ func TestIotCardStore_ListBySeriesAllocationID(t *testing.T) { s := NewIotCardStore(tx, rdb) ctx := context.Background() - seriesAllocationID := uint(200) + seriesID := uint(200) cards := []*model.IotCard{ - {ICCID: "89860012345678911001", CardType: "data_card", CarrierID: 1, Status: 1, SeriesAllocationID: &seriesAllocationID}, - {ICCID: "89860012345678911002", CardType: "data_card", CarrierID: 1, Status: 1, SeriesAllocationID: &seriesAllocationID}, - {ICCID: "89860012345678911003", CardType: "data_card", CarrierID: 1, Status: 1, SeriesAllocationID: nil}, + {ICCID: "89860012345678911001", CardType: "data_card", CarrierID: 1, Status: 1, SeriesID: &seriesID}, + {ICCID: "89860012345678911002", CardType: "data_card", CarrierID: 1, Status: 1, SeriesID: &seriesID}, + {ICCID: "89860012345678911003", CardType: "data_card", CarrierID: 1, Status: 1, SeriesID: nil}, } require.NoError(t, s.CreateBatch(ctx, cards)) - result, err := s.ListBySeriesAllocationID(ctx, seriesAllocationID) + result, err := s.ListBySeriesID(ctx, seriesID) require.NoError(t, err) assert.Len(t, result, 2) for _, card := range result { - assert.Equal(t, seriesAllocationID, *card.SeriesAllocationID) + assert.Equal(t, seriesID, *card.SeriesID) } } -func TestIotCardStore_ListStandalone_SeriesAllocationFilter(t *testing.T) { +func TestIotCardStore_ListStandalone_SeriesIDFilter(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) @@ -505,23 +505,23 @@ func TestIotCardStore_ListStandalone_SeriesAllocationFilter(t *testing.T) { ctx := context.Background() prefix := uniqueICCIDPrefix() - seriesAllocationID := uint(300) + seriesID := uint(300) cards := []*model.IotCard{ - {ICCID: prefix + "S001", CardType: "data_card", CarrierID: 1, Status: 1, SeriesAllocationID: &seriesAllocationID}, - {ICCID: prefix + "S002", CardType: "data_card", CarrierID: 1, Status: 1, SeriesAllocationID: &seriesAllocationID}, - {ICCID: prefix + "S003", CardType: "data_card", CarrierID: 1, Status: 1, SeriesAllocationID: nil}, + {ICCID: prefix + "S001", CardType: "data_card", CarrierID: 1, Status: 1, SeriesID: &seriesID}, + {ICCID: prefix + "S002", CardType: "data_card", CarrierID: 1, Status: 1, SeriesID: &seriesID}, + {ICCID: prefix + "S003", CardType: "data_card", CarrierID: 1, Status: 1, SeriesID: nil}, } require.NoError(t, s.CreateBatch(ctx, cards)) filters := map[string]interface{}{ - "series_allocation_id": seriesAllocationID, - "iccid": prefix, + "series_id": seriesID, + "iccid": prefix, } result, total, err := s.ListStandalone(ctx, nil, filters) require.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, result, 2) for _, card := range result { - assert.Equal(t, seriesAllocationID, *card.SeriesAllocationID) + assert.Equal(t, seriesID, *card.SeriesID) } } diff --git a/internal/store/postgres/shop_series_allocation_store_test.go b/internal/store/postgres/shop_series_allocation_store_test.go new file mode 100644 index 0000000..36ccdd8 --- /dev/null +++ b/internal/store/postgres/shop_series_allocation_store_test.go @@ -0,0 +1,114 @@ +package postgres + +import ( + "context" + "testing" + + "github.com/break/junhong_cmp_fiber/internal/model" + "github.com/break/junhong_cmp_fiber/tests/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestShopSeriesAllocationStore_GetByShopAndSeries(t *testing.T) { + tx := testutils.NewTestTransaction(t) + ctx := context.Background() + + s := NewShopSeriesAllocationStore(tx) + + // 创建测试数据 + allocation := &model.ShopSeriesAllocation{ + ShopID: 1, + SeriesID: 100, + AllocatorShopID: 0, + Status: 1, + } + require.NoError(t, s.Create(ctx, allocation)) + + t.Run("查询存在的分配", func(t *testing.T) { + result, err := s.GetByShopAndSeries(ctx, 1, 100) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, uint(1), result.ShopID) + assert.Equal(t, uint(100), result.SeriesID) + }) + + t.Run("查询不存在的分配", func(t *testing.T) { + result, err := s.GetByShopAndSeries(ctx, 999, 999) + assert.Error(t, err) + assert.Equal(t, gorm.ErrRecordNotFound, err) + assert.Nil(t, result) + }) +} + +func TestShopSeriesAllocationStore_Create(t *testing.T) { + tx := testutils.NewTestTransaction(t) + ctx := context.Background() + + s := NewShopSeriesAllocationStore(tx) + + allocation := &model.ShopSeriesAllocation{ + ShopID: 1, + SeriesID: 100, + AllocatorShopID: 0, + Status: 1, + } + + err := s.Create(ctx, allocation) + require.NoError(t, err) + assert.NotZero(t, allocation.ID) +} + +func TestShopSeriesAllocationStore_GetByID(t *testing.T) { + tx := testutils.NewTestTransaction(t) + ctx := context.Background() + + s := NewShopSeriesAllocationStore(tx) + + allocation := &model.ShopSeriesAllocation{ + ShopID: 1, + SeriesID: 100, + AllocatorShopID: 0, + Status: 1, + } + require.NoError(t, s.Create(ctx, allocation)) + + result, err := s.GetByID(ctx, allocation.ID) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, allocation.ID, result.ID) +} + +func TestShopSeriesAllocationStore_List(t *testing.T) { + tx := testutils.NewTestTransaction(t) + ctx := context.Background() + + s := NewShopSeriesAllocationStore(tx) + + // 创建测试数据 + allocations := []*model.ShopSeriesAllocation{ + {ShopID: 1, SeriesID: 100, AllocatorShopID: 0, Status: 1}, + {ShopID: 1, SeriesID: 101, AllocatorShopID: 0, Status: 1}, + {ShopID: 2, SeriesID: 100, AllocatorShopID: 0, Status: 1}, + } + for _, a := range allocations { + require.NoError(t, s.Create(ctx, a)) + } + + t.Run("按店铺ID过滤", func(t *testing.T) { + filters := map[string]interface{}{"shop_id": uint(1)} + result, total, err := s.List(ctx, nil, filters) + require.NoError(t, err) + assert.Equal(t, int64(2), total) + assert.Len(t, result, 2) + }) + + t.Run("按系列ID过滤", func(t *testing.T) { + filters := map[string]interface{}{"series_id": uint(100)} + result, total, err := s.List(ctx, nil, filters) + require.NoError(t, err) + assert.Equal(t, int64(2), total) + assert.Len(t, result, 2) + }) +} diff --git a/migrations/000038_refactor_series_binding_to_series_id.down.sql b/migrations/000038_refactor_series_binding_to_series_id.down.sql new file mode 100644 index 0000000..1bb3c53 --- /dev/null +++ b/migrations/000038_refactor_series_binding_to_series_id.down.sql @@ -0,0 +1,9 @@ +-- 回滚: 将 series_id 改回 series_allocation_id +ALTER TABLE tb_iot_card RENAME COLUMN series_id TO series_allocation_id; +COMMENT ON COLUMN tb_iot_card.series_allocation_id IS '套餐系列分配ID(关联ShopSeriesAllocation)'; + +ALTER TABLE tb_device RENAME COLUMN series_id TO series_allocation_id; +COMMENT ON COLUMN tb_device.series_allocation_id IS '套餐系列分配ID(关联ShopSeriesAllocation)'; + +-- 删除索引(如果存在) +DROP INDEX IF EXISTS idx_shop_series_allocation_shop_series; diff --git a/migrations/000038_refactor_series_binding_to_series_id.up.sql b/migrations/000038_refactor_series_binding_to_series_id.up.sql new file mode 100644 index 0000000..5b0053d --- /dev/null +++ b/migrations/000038_refactor_series_binding_to_series_id.up.sql @@ -0,0 +1,13 @@ +-- 重构: 将卡/设备的套餐系列绑定从分配ID改为系列ID +-- 重命名 tb_iot_card.series_allocation_id 为 series_id +ALTER TABLE tb_iot_card RENAME COLUMN series_allocation_id TO series_id; +COMMENT ON COLUMN tb_iot_card.series_id IS '套餐系列ID(关联PackageSeries)'; + +-- 重命名 tb_device.series_allocation_id 为 series_id +ALTER TABLE tb_device RENAME COLUMN series_allocation_id TO series_id; +COMMENT ON COLUMN tb_device.series_id IS '套餐系列ID(关联PackageSeries)'; + +-- 验证并添加复合索引(如果不存在) +CREATE INDEX IF NOT EXISTS idx_shop_series_allocation_shop_series + ON tb_shop_series_allocation(shop_id, series_id) + WHERE status = 1; diff --git a/openspec/changes/refactor-series-binding-to-series-id/tasks.md b/openspec/changes/refactor-series-binding-to-series-id/tasks.md new file mode 100644 index 0000000..55c903b --- /dev/null +++ b/openspec/changes/refactor-series-binding-to-series-id/tasks.md @@ -0,0 +1,191 @@ +# Tasks: refactor-series-binding-to-series-id + +## 1. 数据库迁移 + +- [x] 1.1 创建数据库迁移文件 `migrations/000XXX_refactor_series_binding_to_series_id.up.sql`,重命名 `tb_iot_card.series_allocation_id` 为 `series_id`,`tb_device.series_allocation_id` 为 `series_id`,更新字段注释 +- [x] 1.2 创建回滚迁移文件 `migrations/000XXX_refactor_series_binding_to_series_id.down.sql` +- [x] 1.3 验证索引是否存在:检查 `tb_shop_series_allocation` 是否有 `(shop_id, series_id)` 复合索引,如不存在则添加 +- [x] 1.4 执行迁移:运行 `migrate up`,验证字段重命名成功,无错误 + +## 2. Model 层修改 + +- [x] 2.1 修改 `internal/model/iot_card.go`:将 `SeriesAllocationID` 字段重命名为 `SeriesID`,更新 gorm 标签和注释 +- [x] 2.2 修改 `internal/model/device.go`:将 `SeriesAllocationID` 字段重命名为 `SeriesID`,更新 gorm 标签和注释 +- [x] 2.3 验证编译:运行 `go build ./internal/model/...`,确认无编译错误 + +## 3. DTO 层修改 + +- [x] 3.1 修改 `internal/model/dto/iot_card_dto.go`:更新 `ListStandaloneIotCardRequest` 的查询参数 `SeriesAllocationID` → `SeriesID` +- [x] 3.2 修改 `internal/model/dto/iot_card_dto.go`:更新 `StandaloneIotCardResponse` 的响应字段 `SeriesAllocationID` → `SeriesID` +- [x] 3.3 修改 `internal/model/dto/iot_card_dto.go`:更新 `BatchSetCardSeriesBindngRequest` 的请求字段 `SeriesAllocationID` → `SeriesID`,更新 description 为 "套餐系列ID(0表示清除关联)" +- [x] 3.4 修改 `internal/model/dto/device_dto.go`:更新 `ListDeviceRequest` 的查询参数 `SeriesAllocationID` → `SeriesID` +- [x] 3.5 修改 `internal/model/dto/device_dto.go`:更新 `DeviceResponse` 的响应字段 `SeriesAllocationID` → `SeriesID` +- [x] 3.6 修改 `internal/model/dto/device_dto.go`:更新 `BatchSetDeviceSeriesBindngRequest` 的请求字段 `SeriesAllocationID` → `SeriesID`,更新 description 为 "套餐系列ID(0表示清除关联)" +- [x] 3.7 验证编译:运行 `go build ./internal/model/dto/...`,确认无编译错误 + +## 4. Store 层修改 + +- [x] 4.1 修改 `internal/store/postgres/iot_card_store.go`:更新 `ListStandalone` 方法,将过滤条件 `series_allocation_id` 改为 `series_id` +- [x] 4.2 修改 `internal/store/postgres/iot_card_store.go`:更新 `Count` 方法,将过滤条件 `series_allocation_id` 改为 `series_id` +- [x] 4.3 修改 `internal/store/postgres/iot_card_store.go`:重命名方法 `BatchUpdateSeriesAllocation` 为 `BatchUpdateSeriesID`,更新 SQL 字段名 +- [x] 4.4 修改 `internal/store/postgres/iot_card_store.go`:重命名方法 `ListBySeriesAllocationID` 为 `ListBySeriesID`,更新 WHERE 条件 +- [x] 4.5 修改 `internal/store/postgres/device_store.go`:更新 `List` 方法,将过滤条件 `series_allocation_id` 改为 `series_id` +- [x] 4.6 修改 `internal/store/postgres/device_store.go`:重命名方法 `BatchUpdateSeriesAllocation` 为 `BatchUpdateSeriesID`,更新 SQL 字段名 +- [x] 4.7 修改 `internal/store/postgres/device_store.go`:重命名方法 `ListBySeriesAllocationID` 为 `ListBySeriesID`,更新 WHERE 条件 +- [x] 4.8 修改 `internal/store/postgres/shop_series_allocation_store.go`:新增方法 `GetByShopAndSeries(ctx, shopID, seriesID)`,实现根据店铺和系列查询分配配置 +- [x] 4.9 验证或创建 `internal/store/postgres/package_series_store.go`:如不存在则创建,实现 `GetByID(ctx, id)` 方法 +- [x] 4.10 如果创建了新 Store,在 `internal/bootstrap/stores.go` 中注册 `PackageSeriesStore` +- [x] 4.11 验证编译:运行 `go build ./internal/store/...`,确认无编译错误 + +## 5. Service 层修改 - iot_card + +- [x] 5.1 修改 `internal/service/iot_card/service.go`:更新 `ListStandalone` 方法,将过滤条件 key `series_allocation_id` 改为 `series_id` +- [x] 5.2 修改 `internal/service/iot_card/service.go`:更新 `buildStandaloneResponse` 方法,将字段 `SeriesAllocationID` 改为 `SeriesID` +- [x] 5.3 修改 `internal/service/iot_card/service.go`:重构 `BatchSetSeriesBinding` 方法 +- [x] 5.4 在 `internal/service/iot_card/service.go` 的 `Service` 结构体中添加 `packageSeriesStore` 依赖(如果不存在) +- [x] 5.5 验证编译:运行 `go build ./internal/service/iot_card/...`,确认无编译错误 +- [x] 5.6 运行 `lsp_diagnostics` 检查 `internal/service/iot_card/service.go`,确认无类型错误 + +## 6. Service 层修改 - device + +- [x] 6.1 修改 `internal/service/device/service.go`:更新 `List` 方法,将过滤条件 key `series_allocation_id` 改为 `series_id` +- [x] 6.2 修改 `internal/service/device/service.go`:更新 `buildDeviceResponse` 方法,将字段 `SeriesAllocationID` 改为 `SeriesID` +- [x] 6.3 修改 `internal/service/device/service.go`:重构 `BatchSetSeriesBinding` 方法 +- [x] 6.4 在 `internal/service/device/service.go` 的 `Service` 结构体中添加 `packageSeriesStore` 依赖(如果不存在) +- [x] 6.5 验证编译:运行 `go build ./internal/service/device/...`,确认无编译错误 +- [x] 6.6 运行 `lsp_diagnostics` 检查 `internal/service/device/service.go`,确认无类型错误 + +## 7. Service 层修改 - purchase_validation(关键) + +- [x] 7.1 修改 `internal/service/purchase_validation/service.go`:重构 `ValidateCardPurchase` 方法 +- [x] 7.2 修改 `internal/service/purchase_validation/service.go`:重构 `ValidateDevicePurchase` 方法 +- [x] 7.3 更新 `ValidateCardPurchase` 和 `ValidateDevicePurchase` 的错误消息,从 "套餐系列分配不存在" 改为 "该卡/设备未关联套餐系列" +- [x] 7.4 验证编译:运行 `go build ./internal/service/purchase_validation/...`,确认无编译错误 +- [x] 7.5 运行 `lsp_diagnostics` 检查 `internal/service/purchase_validation/service.go`,确认无类型错误 + +## 8. Service 层修改 - commission_calculation(关键) + +- [x] 8.1 修改 `internal/service/commission_calculation/service.go`:重构 `CalculateOrderCommission` 方法 +- [x] 8.2 修改 `internal/service/commission_calculation/service.go`:重构 `CalculateDeviceOrderCommission` 方法(同样的逻辑) +- [x] 8.3 验证编译:运行 `go build ./internal/service/commission_calculation/...`,确认无编译错误 +- [x] 8.4 运行 `lsp_diagnostics` 检查 `internal/service/commission_calculation/service.go`,确认无类型错误 + +## 9. Service 层修改 - recharge + +- [x] 9.1 修改 `internal/service/recharge/service.go`:重构充值相关方法,将获取 `seriesAllocationID` 的逻辑改为直接使用 `seriesID` +- [x] 9.2 修改 `internal/service/recharge/service.go`:更新返佣查询逻辑,使用 `GetByShopAndSeries(shopID, seriesID)` 而不是 `GetByID(allocationID)` +- [x] 9.3 验证编译:运行 `go build ./internal/service/recharge/...`,确认无编译错误 +- [x] 9.4 运行 `lsp_diagnostics` 检查 `internal/service/recharge/service.go`,确认无类型错误 + +## 10. Service 层修改 - order + +- [x] 10.1 检查 `internal/service/order/service.go` 中是否有直接使用 `SeriesAllocationID` 的地方,如有则更新为 `SeriesID` +- [x] 10.2 验证编译:运行 `go build ./internal/service/order/...`,确认无编译错误 +- [x] 10.3 运行 `lsp_diagnostics` 检查 `internal/service/order/service.go`,确认无类型错误 + +## 11. Bootstrap 依赖注入 + +- [x] 11.1 如果创建了 `PackageSeriesStore`,在 `internal/bootstrap/stores.go` 中初始化并添加到 `Stores` 结构体 +- [x] 11.2 在 `internal/bootstrap/services.go` 中,为 `iot_card.Service` 和 `device.Service` 注入 `packageSeriesStore` 依赖 +- [x] 11.3 验证编译:运行 `go build ./internal/bootstrap/...`,确认无编译错误 + +## 12. Handler & Routes 层 + +- [x] 12.1 修改 `internal/routes/iot_card.go`:更新 `/series-binding` 路由的 Description,说明参数从 `series_allocation_id` 改为 `series_id` +- [x] 12.2 修改 `internal/routes/device.go`:更新 `/series-binding` 路由的 Description,说明参数从 `series_allocation_id` 改为 `series_id` +- [x] 12.3 验证 Handler 层代码:`internal/handler/admin/iot_card.go` 和 `device.go` 无需修改(使用 DTO) +- [x] 12.4 验证编译:运行 `go build ./internal/routes/... ./internal/handler/...`,确认无编译错误 + +## 13. Store 层测试更新 + +- [x] 13.1 修改 `internal/store/postgres/iot_card_store_test.go`:更新所有测试用例,将 `SeriesAllocationID` 改为 `SeriesID` +- [x] 13.2 修改 `internal/store/postgres/iot_card_store_test.go`:重命名测试函数 `TestIotCardStore_ListBySeriesAllocationID` 为 `TestIotCardStore_ListBySeriesID` +- [x] 13.3 修改 `internal/store/postgres/iot_card_store_test.go`:更新过滤条件测试,将 `series_allocation_id` 改为 `series_id` +- [x] 13.4 修改 `internal/store/postgres/device_store_test.go`:更新所有测试用例,将 `SeriesAllocationID` 改为 `SeriesID` +- [x] 13.5 修改 `internal/store/postgres/device_store_test.go`:重命名测试函数 `TestDeviceStore_ListBySeriesAllocationID` 为 `TestDeviceStore_ListBySeriesID` +- [x] 13.6 新增测试:在 `shop_series_allocation_store_test.go` 中添加 `TestShopSeriesAllocationStore_GetByShopAndSeries` 测试 +- [x] 13.7 运行 Store 层测试:`source .env.local && go test -v ./internal/store/postgres/...`,确认全部通过 + +## 14. Service 层测试更新 - iot_card + +- [x] 14.1 修改 `internal/service/iot_card/service_test.go`:更新 `TestIotCardService_BatchSetSeriesBinding` 测试 +- [x] 14.2 更新测试数据准备顺序:先 `PackageSeries`,再 `ShopSeriesAllocation`,最后 `IotCard` +- [x] 14.3 运行 Service 层测试:`source .env.local && go test -v ./internal/service/iot_card/...`,确认全部通过 + +## 15. Service 层测试更新 - device + +- [x] 15.1 修改 `internal/service/device/service_test.go`:更新 `TestDeviceService_BatchSetSeriesBinding` 测试 +- [x] 15.2 更新测试数据准备顺序:先 `PackageSeries`,再 `ShopSeriesAllocation`,最后 `Device` +- [x] 15.3 运行 Service 层测试:`source .env.local && go test -v ./internal/service/device/...`,确认全部通过 + +## 16. Service 层测试更新 - purchase_validation + +- [x] 16.1 修改 `internal/service/purchase_validation/service_test.go`:更新所有测试用例 +- [x] 16.2 运行 Service 层测试:`source .env.local && go test -v ./internal/service/purchase_validation/...`,确认全部通过 + +## 17. Service 层测试更新 - commission_calculation + +- [x] 17.1 修改 `internal/service/commission_calculation/service_test.go`:更新所有测试用例 +- [x] 17.2 运行 Service 层测试:`source .env.local && go test -v ./internal/service/commission_calculation/...`,确认全部通过 + +## 18. Service 层测试更新 - recharge & order + +- [x] 18.1 修改 `internal/service/recharge/service_test.go`:更新测试用例,将 `SeriesAllocationID` 改为 `SeriesID` +- [x] 18.2 修改 `internal/service/order/service_test.go`:更新测试用例,将 `SeriesAllocationID` 改为 `SeriesID` +- [x] 18.3 运行 Service 层测试:`source .env.local && go test -v ./internal/service/recharge/... ./internal/service/order/...`,确认全部通过 + +## 19. 集成测试更新 - iot_card + +- [x] 19.1 修改 `tests/integration/iot_card_test.go`:更新 `TestIotCard_BatchSetSeriesBinding` 测试 +- [x] 19.2 更新所有子测试用例的 JSON 请求体(约 10 个) +- [x] 19.3 运行集成测试:`source .env.local && cd tests/integration && go test -v -run "TestIotCard_BatchSetSeriesBinding"`,确认全部通过 + +## 20. 集成测试更新 - device + +- [x] 20.1 修改 `tests/integration/device_test.go`:更新 `TestDevice_BatchSetSeriesBinding` 测试 +- [x] 20.2 更新所有子测试用例的 JSON 请求体(约 10 个) +- [x] 20.3 运行集成测试:`source .env.local && cd tests/integration && go test -v -run "TestDevice_BatchSetSeriesBinding"`,确认全部通过 + +## 21. 单元测试更新 - commission_calculation + +- [x] 21.1 修改 `tests/unit/commission_calculation_service_test.go`:更新所有测试用例 +- [x] 21.2 运行单元测试:`source .env.local && go test -v ./tests/unit/...`,确认全部通过 + +## 22. 全量测试验证 + +- [x] 22.1 运行所有测试:`source .env.local && go test -v ./...`,确认全部通过,无遗漏 +- [x] 22.2 运行编译检查:`go build ./...`,确认无编译错误 +- [x] 22.3 使用 grep 搜索遗漏:`grep -r "SeriesAllocationID" internal/ --include="*.go"`,确认无遗漏 +- [x] 22.4 使用 grep 搜索遗漏:`grep -r "series_allocation_id" internal/ --include="*.go"`,确认无遗漏(仅数据库注释除外) + +## 23. API 手动测试 + +- [ ] 23.1 启动本地服务:`go run cmd/api/main.go` +- [ ] 23.2 测试 IoT 卡系列绑定 API:`PATCH /api/admin/iot-cards/series-binding`,使用 Postman 或 curl 发送请求,验证参数 `series_id` 生效 +- [ ] 23.3 测试设备系列绑定 API:`PATCH /api/admin/devices/series-binding`,使用 Postman 或 curl 发送请求,验证参数 `series_id` 生效 +- [ ] 23.4 测试卡列表查询:`GET /api/admin/iot-cards/standalone?series_id=1`,验证过滤生效 +- [ ] 23.5 测试设备列表查询:`GET /api/admin/devices?series_id=1`,验证过滤生效 +- [ ] 23.6 检查日志:确认无错误日志,SQL 查询使用 `series_id` 而不是 `series_allocation_id` + +## 24. API 文档更新 + +- [x] 24.1 更新 OpenAPI 文档注释:确保路由描述中明确说明参数从 `series_allocation_id` 改为 `series_id` +- [x] 24.2 重新生成 API 文档:运行 `go run cmd/gendocs/main.go`,生成最新的 OpenAPI spec +- [x] 24.3 验证生成的文档:检查 `docs/admin-openapi.yaml` 中 `/iot-cards/series-binding` 和 `/devices/series-binding` 的参数定义 +- [x] 24.4 如有前端文档,更新前端接口文档,说明 BREAKING CHANGE + +## 25. 清理和最终验证 + +- [x] 25.1 删除所有临时代码和注释 +- [x] 25.2 运行 `gofmt -w .` 格式化所有代码 +- [x] 25.3 运行 `go mod tidy` 清理依赖 +- [x] 25.4 再次运行全量测试:`source .env.local && go test -v ./...`,确认全部通过 +- [x] 25.5 使用 `git diff` 检查所有改动,确认无遗漏,无多余修改 +- [x] 25.6 更新 CHANGELOG(如有),记录 BREAKING CHANGE + +## 26. 提交和归档 + +- [ ] 26.1 提交代码:创建 Git commit,使用中文 commit message:"重构: 将卡/设备的套餐系列绑定从分配ID改为系列ID" +- [ ] 26.2 运行 OpenSpec 归档:`openspec archive --change refactor-series-binding-to-series-id` +- [ ] 26.3 验证归档成功:检查 `openspec/changes/archive/` 目录,确认变更已归档 +- [ ] 26.4 清理工作目录:删除 `openspec/changes/refactor-series-binding-to-series-id/`(已归档)