This commit is contained in:
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
personalCustomerDeviceStore := postgres.NewPersonalCustomerDeviceStore(deps.DB)
|
|
||||||
assetWalletStore := postgres.NewAssetWalletStore(deps.DB, deps.Redis)
|
assetWalletStore := postgres.NewAssetWalletStore(deps.DB, deps.Redis)
|
||||||
packageStore := postgres.NewPackageStore(deps.DB)
|
packageStore := postgres.NewPackageStore(deps.DB)
|
||||||
shopPackageAllocationStore := postgres.NewShopPackageAllocationStore(deps.DB)
|
shopPackageAllocationStore := postgres.NewShopPackageAllocationStore(deps.DB)
|
||||||
@@ -56,7 +55,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
|||||||
rechargeOrderStore,
|
rechargeOrderStore,
|
||||||
paymentStore,
|
paymentStore,
|
||||||
assetWalletStore,
|
assetWalletStore,
|
||||||
personalCustomerDeviceStore,
|
svc.CustomerBinding,
|
||||||
personalCustomerOpenIDStore,
|
personalCustomerOpenIDStore,
|
||||||
personalCustomerStore,
|
personalCustomerStore,
|
||||||
personalCustomerPhoneStore,
|
personalCustomerPhoneStore,
|
||||||
@@ -78,12 +77,12 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
|||||||
Permission: admin.NewPermissionHandler(svc.Permission),
|
Permission: admin.NewPermissionHandler(svc.Permission),
|
||||||
PersonalCustomer: app.NewPersonalCustomerHandler(svc.PersonalCustomer, deps.Logger),
|
PersonalCustomer: app.NewPersonalCustomerHandler(svc.PersonalCustomer, deps.Logger),
|
||||||
ClientAuth: app.NewClientAuthHandler(svc.ClientAuth, deps.Logger),
|
ClientAuth: app.NewClientAuthHandler(svc.ClientAuth, deps.Logger),
|
||||||
ClientAsset: app.NewClientAssetHandler(svc.Asset, personalCustomerDeviceStore, assetWalletStore, packageStore, shopPackageAllocationStore, iotCardStore, deviceStore, deps.DB, deps.Logger),
|
ClientAsset: app.NewClientAssetHandler(svc.Asset, svc.CustomerBinding, assetWalletStore, packageStore, shopPackageAllocationStore, iotCardStore, deviceStore, deps.DB, deps.Logger),
|
||||||
ClientWallet: app.NewClientWalletHandler(svc.Asset, personalCustomerDeviceStore, assetWalletStore, assetWalletTransactionStore, rechargeOrderStore, paymentStore, svc.Recharge, personalCustomerOpenIDStore, svc.WechatConfig, deps.Redis, deps.Logger, deps.DB, iotCardStore, deviceStore),
|
ClientWallet: app.NewClientWalletHandler(svc.Asset, svc.CustomerBinding, assetWalletStore, assetWalletTransactionStore, rechargeOrderStore, paymentStore, svc.Recharge, personalCustomerOpenIDStore, svc.WechatConfig, deps.Redis, deps.Logger, deps.DB, iotCardStore, deviceStore),
|
||||||
ClientOrder: app.NewClientOrderHandler(clientOrderService, deps.Logger),
|
ClientOrder: app.NewClientOrderHandler(clientOrderService, deps.Logger),
|
||||||
ClientExchange: app.NewClientExchangeHandler(svc.Exchange),
|
ClientExchange: app.NewClientExchangeHandler(svc.Exchange),
|
||||||
ClientRealname: app.NewClientRealnameHandler(svc.Asset, personalCustomerDeviceStore, iotCardStore, deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger, svc.PollingManualTrigger),
|
ClientRealname: app.NewClientRealnameHandler(svc.Asset, svc.CustomerBinding, iotCardStore, deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger, svc.PollingManualTrigger),
|
||||||
ClientDevice: app.NewClientDeviceHandler(svc.Asset, personalCustomerDeviceStore, deviceStore, deviceSimBindingStore, iotCardStore, deps.GatewayClient, deps.Logger),
|
ClientDevice: app.NewClientDeviceHandler(svc.Asset, svc.CustomerBinding, deviceStore, deviceSimBindingStore, iotCardStore, deps.GatewayClient, deps.Logger),
|
||||||
ClientRechargeOrder: app.NewClientRechargeOrderHandler(rechargeOrderStore, paymentStore, deps.Logger),
|
ClientRechargeOrder: app.NewClientRechargeOrderHandler(rechargeOrderStore, paymentStore, deps.Logger),
|
||||||
Shop: admin.NewShopHandler(svc.Shop),
|
Shop: admin.NewShopHandler(svc.Shop),
|
||||||
ShopRole: admin.NewShopRoleHandler(svc.Shop),
|
ShopRole: admin.NewShopRoleHandler(svc.Shop),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||||
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
|
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
|
||||||
clientAuthSvc "github.com/break/junhong_cmp_fiber/internal/service/client_auth"
|
clientAuthSvc "github.com/break/junhong_cmp_fiber/internal/service/client_auth"
|
||||||
|
customerBindingSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
commissionCalculationSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
|
commissionCalculationSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
|
||||||
commissionStatsSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
commissionStatsSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
||||||
commissionWithdrawalSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
|
commissionWithdrawalSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
|
||||||
@@ -109,9 +110,13 @@ type services struct {
|
|||||||
TrafficQuery *trafficSvc.QueryService
|
TrafficQuery *trafficSvc.QueryService
|
||||||
OperationPassword *operationPasswordSvc.Service
|
OperationPassword *operationPasswordSvc.Service
|
||||||
AgentOpenAPI *agentOpenAPISvc.Service
|
AgentOpenAPI *agentOpenAPISvc.Service
|
||||||
|
CustomerBinding *customerBindingSvc.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func initServices(s *stores, deps *Dependencies) *services {
|
func initServices(s *stores, deps *Dependencies) *services {
|
||||||
|
// CustomerBinding 模块依赖最少,最先初始化
|
||||||
|
customerBinding := customerBindingSvc.New(deps.DB, s.IotCard, s.Device)
|
||||||
|
|
||||||
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopPackageAllocation)
|
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopPackageAllocation)
|
||||||
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
|
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
|
||||||
assetAudit := assetAuditSvc.NewService(s.AssetOperationLog, deps.DB)
|
assetAudit := assetAuditSvc.NewService(s.AssetOperationLog, deps.DB)
|
||||||
@@ -210,7 +215,6 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
deps.DB,
|
deps.DB,
|
||||||
s.PersonalCustomerOpenID,
|
s.PersonalCustomerOpenID,
|
||||||
s.PersonalCustomer,
|
s.PersonalCustomer,
|
||||||
s.PersonalCustomerDevice,
|
|
||||||
s.PersonalCustomerPhone,
|
s.PersonalCustomerPhone,
|
||||||
s.IotCard,
|
s.IotCard,
|
||||||
s.Device,
|
s.Device,
|
||||||
@@ -219,6 +223,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
deps.JWTManager,
|
deps.JWTManager,
|
||||||
deps.Redis,
|
deps.Redis,
|
||||||
deps.Logger,
|
deps.Logger,
|
||||||
|
customerBinding,
|
||||||
),
|
),
|
||||||
Shop: shopSvc.New(s.Shop, s.Account, s.ShopRole, s.Role, s.AccountRole, s.AgentWallet),
|
Shop: shopSvc.New(s.Shop, s.Account, s.ShopRole, s.Role, s.AccountRole, s.AgentWallet),
|
||||||
Auth: authSvc.New(s.Account, s.AccountRole, s.RolePermission, s.Permission, s.Shop, deps.TokenManager, deps.Logger),
|
Auth: authSvc.New(s.Account, s.AccountRole, s.RolePermission, s.Permission, s.Shop, deps.TokenManager, deps.Logger),
|
||||||
@@ -264,7 +269,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||||
PurchaseValidation: purchaseValidation,
|
PurchaseValidation: purchaseValidation,
|
||||||
Order: orderService,
|
Order: orderService,
|
||||||
Exchange: exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, s.PersonalCustomerDevice, deps.Logger),
|
Exchange: exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, customerBinding, deps.Logger),
|
||||||
Recharge: rechargeSvc.New(deps.DB, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger),
|
Recharge: rechargeSvc.New(deps.DB, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger),
|
||||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig, deps.Redis, deps.Logger),
|
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig, deps.Redis, deps.Logger),
|
||||||
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
|
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
|
||||||
@@ -308,5 +313,6 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
s.AssetWallet,
|
s.AssetWallet,
|
||||||
deps.Logger,
|
deps.Logger,
|
||||||
),
|
),
|
||||||
|
CustomerBinding: customerBinding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||||
|
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
|
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||||
@@ -27,7 +28,7 @@ import (
|
|||||||
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
|
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
|
||||||
type ClientAssetHandler struct {
|
type ClientAssetHandler struct {
|
||||||
assetService *asset.Service
|
assetService *asset.Service
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
customerBinding *customerBinding.Service
|
||||||
assetWalletStore *postgres.AssetWalletStore
|
assetWalletStore *postgres.AssetWalletStore
|
||||||
packageStore *postgres.PackageStore
|
packageStore *postgres.PackageStore
|
||||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||||
@@ -40,7 +41,7 @@ type ClientAssetHandler struct {
|
|||||||
// NewClientAssetHandler 创建 C 端资产信息处理器
|
// NewClientAssetHandler 创建 C 端资产信息处理器
|
||||||
func NewClientAssetHandler(
|
func NewClientAssetHandler(
|
||||||
assetService *asset.Service,
|
assetService *asset.Service,
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
binding *customerBinding.Service,
|
||||||
assetWalletStore *postgres.AssetWalletStore,
|
assetWalletStore *postgres.AssetWalletStore,
|
||||||
packageStore *postgres.PackageStore,
|
packageStore *postgres.PackageStore,
|
||||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||||
@@ -51,7 +52,7 @@ func NewClientAssetHandler(
|
|||||||
) *ClientAssetHandler {
|
) *ClientAssetHandler {
|
||||||
return &ClientAssetHandler{
|
return &ClientAssetHandler{
|
||||||
assetService: assetService,
|
assetService: assetService,
|
||||||
personalDeviceStore: personalDeviceStore,
|
customerBinding: binding,
|
||||||
assetWalletStore: assetWalletStore,
|
assetWalletStore: assetWalletStore,
|
||||||
packageStore: packageStore,
|
packageStore: packageStore,
|
||||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||||
@@ -102,7 +103,7 @@ func (h *ClientAssetHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo)
|
owned, ownErr := h.customerBinding.OwnsAsset(skipPermissionCtx, customerID, assetInfo.AssetType, assetInfo.AssetID)
|
||||||
if ownErr != nil {
|
if ownErr != nil {
|
||||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||||
}
|
}
|
||||||
@@ -422,21 +423,6 @@ func (h *ClientAssetHandler) RefreshAsset(c *fiber.Ctx) error {
|
|||||||
return response.Success(c, resp)
|
return response.Success(c, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ClientAssetHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) {
|
|
||||||
records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
if record == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ClientAssetHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
func (h *ClientAssetHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||||
switch assetType {
|
switch assetType {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||||
|
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||||
@@ -27,7 +28,7 @@ type deviceAssetInfo struct {
|
|||||||
// 提供设备卡列表、重启、恢复出厂、WiFi 配置、切卡等操作
|
// 提供设备卡列表、重启、恢复出厂、WiFi 配置、切卡等操作
|
||||||
type ClientDeviceHandler struct {
|
type ClientDeviceHandler struct {
|
||||||
assetService *assetSvc.Service
|
assetService *assetSvc.Service
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
customerBinding *customerBinding.Service
|
||||||
deviceStore *postgres.DeviceStore
|
deviceStore *postgres.DeviceStore
|
||||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||||
iotCardStore *postgres.IotCardStore
|
iotCardStore *postgres.IotCardStore
|
||||||
@@ -38,7 +39,7 @@ type ClientDeviceHandler struct {
|
|||||||
// NewClientDeviceHandler 创建 C 端设备能力处理器
|
// NewClientDeviceHandler 创建 C 端设备能力处理器
|
||||||
func NewClientDeviceHandler(
|
func NewClientDeviceHandler(
|
||||||
assetService *assetSvc.Service,
|
assetService *assetSvc.Service,
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
binding *customerBinding.Service,
|
||||||
deviceStore *postgres.DeviceStore,
|
deviceStore *postgres.DeviceStore,
|
||||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||||
iotCardStore *postgres.IotCardStore,
|
iotCardStore *postgres.IotCardStore,
|
||||||
@@ -47,7 +48,7 @@ func NewClientDeviceHandler(
|
|||||||
) *ClientDeviceHandler {
|
) *ClientDeviceHandler {
|
||||||
return &ClientDeviceHandler{
|
return &ClientDeviceHandler{
|
||||||
assetService: assetService,
|
assetService: assetService,
|
||||||
personalDeviceStore: personalDeviceStore,
|
customerBinding: binding,
|
||||||
deviceStore: deviceStore,
|
deviceStore: deviceStore,
|
||||||
deviceSimBindingStore: deviceSimBindingStore,
|
deviceSimBindingStore: deviceSimBindingStore,
|
||||||
iotCardStore: iotCardStore,
|
iotCardStore: iotCardStore,
|
||||||
@@ -79,11 +80,11 @@ func (h *ClientDeviceHandler) validateDeviceAsset(c *fiber.Ctx, identifier strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 校验个人客户对该设备的所有权
|
// 校验个人客户对该设备的所有权
|
||||||
owns, err := h.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, asset.VirtualNo)
|
owns, err := h.customerBinding.OwnsAsset(ctx, customerID, asset.AssetType, asset.AssetID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("校验设备所有权失败",
|
h.logger.Error("校验设备所有权失败",
|
||||||
zap.Uint("customer_id", customerID),
|
zap.Uint("customer_id", customerID),
|
||||||
zap.String("virtual_no", asset.VirtualNo),
|
zap.Uint("asset_id", asset.AssetID),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return nil, errors.New(errors.CodeInternalError)
|
return nil, errors.New(errors.CodeInternalError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||||
|
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||||
@@ -29,7 +30,7 @@ var clientRealnameValidator = validator.New()
|
|||||||
// ClientRealnameHandler C 端实名认证处理器
|
// ClientRealnameHandler C 端实名认证处理器
|
||||||
type ClientRealnameHandler struct {
|
type ClientRealnameHandler struct {
|
||||||
assetService *assetService.Service
|
assetService *assetService.Service
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
customerBinding *customerBinding.Service
|
||||||
iotCardStore *postgres.IotCardStore
|
iotCardStore *postgres.IotCardStore
|
||||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||||
carrierStore *postgres.CarrierStore
|
carrierStore *postgres.CarrierStore
|
||||||
@@ -41,7 +42,7 @@ type ClientRealnameHandler struct {
|
|||||||
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
||||||
func NewClientRealnameHandler(
|
func NewClientRealnameHandler(
|
||||||
assetSvc *assetService.Service,
|
assetSvc *assetService.Service,
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
binding *customerBinding.Service,
|
||||||
iotCardStore *postgres.IotCardStore,
|
iotCardStore *postgres.IotCardStore,
|
||||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||||
carrierStore *postgres.CarrierStore,
|
carrierStore *postgres.CarrierStore,
|
||||||
@@ -51,7 +52,7 @@ func NewClientRealnameHandler(
|
|||||||
) *ClientRealnameHandler {
|
) *ClientRealnameHandler {
|
||||||
return &ClientRealnameHandler{
|
return &ClientRealnameHandler{
|
||||||
assetService: assetSvc,
|
assetService: assetSvc,
|
||||||
personalDeviceStore: personalDeviceStore,
|
customerBinding: binding,
|
||||||
iotCardStore: iotCardStore,
|
iotCardStore: iotCardStore,
|
||||||
deviceSimBindingStore: deviceSimBindingStore,
|
deviceSimBindingStore: deviceSimBindingStore,
|
||||||
carrierStore: carrierStore,
|
carrierStore: carrierStore,
|
||||||
@@ -89,11 +90,11 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 验证资产归属(个人客户必须绑定过该资产)
|
// 4. 验证资产归属(个人客户必须绑定过该资产)
|
||||||
owned, err := h.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, asset.VirtualNo)
|
owned, err := h.customerBinding.OwnsAsset(ctx, customerID, asset.AssetType, asset.AssetID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.GetAppLogger().Error("查询资产归属失败",
|
logger.GetAppLogger().Error("查询资产归属失败",
|
||||||
zap.Uint("customer_id", customerID),
|
zap.Uint("customer_id", customerID),
|
||||||
zap.String("virtual_no", asset.VirtualNo),
|
zap.Uint("asset_id", asset.AssetID),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return errors.New(errors.CodeInternalError, "查询资产归属失败")
|
return errors.New(errors.CodeInternalError, "查询资产归属失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||||
|
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
@@ -31,7 +32,7 @@ import (
|
|||||||
// 提供 C1~C5 钱包详情、流水、充值前校验、充值下单、充值记录接口
|
// 提供 C1~C5 钱包详情、流水、充值前校验、充值下单、充值记录接口
|
||||||
type ClientWalletHandler struct {
|
type ClientWalletHandler struct {
|
||||||
assetService *asset.Service
|
assetService *asset.Service
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
customerBinding *customerBinding.Service
|
||||||
walletStore *postgres.AssetWalletStore
|
walletStore *postgres.AssetWalletStore
|
||||||
transactionStore *postgres.AssetWalletTransactionStore
|
transactionStore *postgres.AssetWalletTransactionStore
|
||||||
rechargeOrderStore *postgres.RechargeOrderStore
|
rechargeOrderStore *postgres.RechargeOrderStore
|
||||||
@@ -49,7 +50,7 @@ type ClientWalletHandler struct {
|
|||||||
// NewClientWalletHandler 创建 C 端钱包处理器
|
// NewClientWalletHandler 创建 C 端钱包处理器
|
||||||
func NewClientWalletHandler(
|
func NewClientWalletHandler(
|
||||||
assetService *asset.Service,
|
assetService *asset.Service,
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
binding *customerBinding.Service,
|
||||||
walletStore *postgres.AssetWalletStore,
|
walletStore *postgres.AssetWalletStore,
|
||||||
transactionStore *postgres.AssetWalletTransactionStore,
|
transactionStore *postgres.AssetWalletTransactionStore,
|
||||||
rechargeOrderStore *postgres.RechargeOrderStore,
|
rechargeOrderStore *postgres.RechargeOrderStore,
|
||||||
@@ -65,7 +66,7 @@ func NewClientWalletHandler(
|
|||||||
) *ClientWalletHandler {
|
) *ClientWalletHandler {
|
||||||
return &ClientWalletHandler{
|
return &ClientWalletHandler{
|
||||||
assetService: assetService,
|
assetService: assetService,
|
||||||
personalDeviceStore: personalDeviceStore,
|
customerBinding: binding,
|
||||||
walletStore: walletStore,
|
walletStore: walletStore,
|
||||||
transactionStore: transactionStore,
|
transactionStore: transactionStore,
|
||||||
rechargeOrderStore: rechargeOrderStore,
|
rechargeOrderStore: rechargeOrderStore,
|
||||||
@@ -544,7 +545,7 @@ func (h *ClientWalletHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifie
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo)
|
owned, ownErr := h.customerBinding.OwnsAsset(skipPermissionCtx, customerID, assetInfo.AssetType, assetInfo.AssetID)
|
||||||
if ownErr != nil {
|
if ownErr != nil {
|
||||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||||
}
|
}
|
||||||
@@ -572,21 +573,6 @@ func (h *ClientWalletHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifie
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ClientWalletHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) {
|
|
||||||
records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
if record == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ClientWalletHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
func (h *ClientWalletHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||||
switch assetType {
|
switch assetType {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
|
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
|
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/service/verification"
|
"github.com/break/junhong_cmp_fiber/internal/service/verification"
|
||||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
@@ -41,7 +42,6 @@ type Service struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
openidStore *postgres.PersonalCustomerOpenIDStore
|
openidStore *postgres.PersonalCustomerOpenIDStore
|
||||||
customerStore *postgres.PersonalCustomerStore
|
customerStore *postgres.PersonalCustomerStore
|
||||||
deviceBindStore *postgres.PersonalCustomerDeviceStore
|
|
||||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||||
iotCardStore *postgres.IotCardStore
|
iotCardStore *postgres.IotCardStore
|
||||||
deviceStore *postgres.DeviceStore
|
deviceStore *postgres.DeviceStore
|
||||||
@@ -51,6 +51,7 @@ type Service struct {
|
|||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
wechatCache kernel.CacheInterface
|
wechatCache kernel.CacheInterface
|
||||||
|
customerBinding *customerBinding.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建 C 端认证服务实例
|
// New 创建 C 端认证服务实例
|
||||||
@@ -58,7 +59,6 @@ func New(
|
|||||||
db *gorm.DB,
|
db *gorm.DB,
|
||||||
openidStore *postgres.PersonalCustomerOpenIDStore,
|
openidStore *postgres.PersonalCustomerOpenIDStore,
|
||||||
customerStore *postgres.PersonalCustomerStore,
|
customerStore *postgres.PersonalCustomerStore,
|
||||||
deviceBindStore *postgres.PersonalCustomerDeviceStore,
|
|
||||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||||
iotCardStore *postgres.IotCardStore,
|
iotCardStore *postgres.IotCardStore,
|
||||||
deviceStore *postgres.DeviceStore,
|
deviceStore *postgres.DeviceStore,
|
||||||
@@ -67,12 +67,12 @@ func New(
|
|||||||
jwtManager *auth.JWTManager,
|
jwtManager *auth.JWTManager,
|
||||||
redisClient *redis.Client,
|
redisClient *redis.Client,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
|
binding *customerBinding.Service,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
openidStore: openidStore,
|
openidStore: openidStore,
|
||||||
customerStore: customerStore,
|
customerStore: customerStore,
|
||||||
deviceBindStore: deviceBindStore,
|
|
||||||
phoneStore: phoneStore,
|
phoneStore: phoneStore,
|
||||||
iotCardStore: iotCardStore,
|
iotCardStore: iotCardStore,
|
||||||
deviceStore: deviceStore,
|
deviceStore: deviceStore,
|
||||||
@@ -82,6 +82,7 @@ func New(
|
|||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
wechatCache: wechat.NewRedisCache(redisClient),
|
wechatCache: wechat.NewRedisCache(redisClient),
|
||||||
|
customerBinding: binding,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,99 +631,9 @@ func (s *Service) checkCardBoundToDevice(ctx context.Context, cardID uint) (bool
|
|||||||
return !card.IsStandalone || card.DeviceVirtualNo != "", nil
|
return !card.IsStandalone || card.DeviceVirtualNo != "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindAsset 绑定客户与资产关系
|
// bindAsset 委托 CustomerBinding 模块处理客户与资产的绑定关系
|
||||||
func (s *Service) bindAsset(ctx context.Context, tx *gorm.DB, customerID uint, assetType string, assetID uint) error {
|
func (s *Service) bindAsset(ctx context.Context, tx *gorm.DB, customerID uint, assetType string, assetID uint) error {
|
||||||
assetKey, err := s.resolveAssetBindingKey(ctx, tx, assetType, assetID)
|
return s.customerBinding.Bind(ctx, tx, customerID, assetType, assetID)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var bindCount int64
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Model(&model.PersonalCustomerDevice{}).
|
|
||||||
Where("virtual_no = ?", assetKey).
|
|
||||||
Count(&bindCount).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "查询资产绑定关系失败")
|
|
||||||
}
|
|
||||||
firstEverBind := bindCount == 0
|
|
||||||
|
|
||||||
bindStore := postgres.NewPersonalCustomerDeviceStore(tx)
|
|
||||||
exists, err := bindStore.ExistsByCustomerAndDevice(ctx, customerID, assetKey)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "查询客户资产绑定关系失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
record := &model.PersonalCustomerDevice{
|
|
||||||
CustomerID: customerID,
|
|
||||||
VirtualNo: assetKey,
|
|
||||||
Status: 1,
|
|
||||||
}
|
|
||||||
if err := bindStore.Create(ctx, record); err != nil {
|
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "创建资产绑定关系失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if firstEverBind {
|
|
||||||
if err := s.markAssetAsSold(ctx, tx, assetType, assetID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) resolveAssetBindingKey(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) (string, error) {
|
|
||||||
if assetType == assetTypeIotCard {
|
|
||||||
var card model.IotCard
|
|
||||||
if err := tx.WithContext(ctx).First(&card, assetID).Error; err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return "", errors.New(errors.CodeAssetNotFound)
|
|
||||||
}
|
|
||||||
return "", errors.Wrap(errors.CodeInternalError, err, "查询卡资产失败")
|
|
||||||
}
|
|
||||||
return card.VirtualNo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if assetType == assetTypeDevice {
|
|
||||||
var device model.Device
|
|
||||||
if err := tx.WithContext(ctx).First(&device, assetID).Error; err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return "", errors.New(errors.CodeAssetNotFound)
|
|
||||||
}
|
|
||||||
return "", errors.Wrap(errors.CodeInternalError, err, "查询设备资产失败")
|
|
||||||
}
|
|
||||||
if device.VirtualNo != "" {
|
|
||||||
return device.VirtualNo, nil
|
|
||||||
}
|
|
||||||
return device.IMEI, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New(errors.CodeInvalidParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) markAssetAsSold(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) error {
|
|
||||||
if assetType == assetTypeIotCard {
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Model(&model.IotCard{}).
|
|
||||||
Where("id = ? AND asset_status = ?", assetID, 1).
|
|
||||||
Update("asset_status", 2).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "更新卡资产状态失败")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if assetType == assetTypeDevice {
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Model(&model.Device{}).
|
|
||||||
Where("id = ? AND asset_status = ?", assetID, 1).
|
|
||||||
Update("asset_status", 2).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "更新设备资产状态失败")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(errors.CodeInvalidParam)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) issueLoginToken(ctx context.Context, customerID uint, assetType string, assetID uint) (string, bool, error) {
|
func (s *Service) issueLoginToken(ctx context.Context, customerID uint, assetType string, assetID uint) (string, bool, error) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||||
|
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||||
@@ -55,7 +56,7 @@ type Service struct {
|
|||||||
rechargeOrderStore *postgres.RechargeOrderStore
|
rechargeOrderStore *postgres.RechargeOrderStore
|
||||||
paymentStore *postgres.PaymentStore
|
paymentStore *postgres.PaymentStore
|
||||||
walletStore *postgres.AssetWalletStore
|
walletStore *postgres.AssetWalletStore
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
customerBinding *customerBinding.Service
|
||||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||||
personalCustomerStore *postgres.PersonalCustomerStore
|
personalCustomerStore *postgres.PersonalCustomerStore
|
||||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||||
@@ -78,7 +79,7 @@ func New(
|
|||||||
rechargeOrderStore *postgres.RechargeOrderStore,
|
rechargeOrderStore *postgres.RechargeOrderStore,
|
||||||
paymentStore *postgres.PaymentStore,
|
paymentStore *postgres.PaymentStore,
|
||||||
walletStore *postgres.AssetWalletStore,
|
walletStore *postgres.AssetWalletStore,
|
||||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
binding *customerBinding.Service,
|
||||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||||
personalCustomerStore *postgres.PersonalCustomerStore,
|
personalCustomerStore *postgres.PersonalCustomerStore,
|
||||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore,
|
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore,
|
||||||
@@ -99,7 +100,7 @@ func New(
|
|||||||
rechargeOrderStore: rechargeOrderStore,
|
rechargeOrderStore: rechargeOrderStore,
|
||||||
paymentStore: paymentStore,
|
paymentStore: paymentStore,
|
||||||
walletStore: walletStore,
|
walletStore: walletStore,
|
||||||
personalDeviceStore: personalDeviceStore,
|
customerBinding: binding,
|
||||||
openIDStore: openIDStore,
|
openIDStore: openIDStore,
|
||||||
personalCustomerStore: personalCustomerStore,
|
personalCustomerStore: personalCustomerStore,
|
||||||
personalCustomerPhoneStore: personalCustomerPhoneStore,
|
personalCustomerPhoneStore: personalCustomerPhoneStore,
|
||||||
@@ -132,8 +133,10 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil {
|
if owned, err := s.customerBinding.OwnsAsset(skipCtx, customerID, assetInfo.AssetType, assetInfo.AssetID); err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
||||||
|
} else if !owned {
|
||||||
|
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
validationResult, err := s.validatePurchase(skipCtx, assetInfo, req.PackageIDs)
|
validationResult, err := s.validatePurchase(skipCtx, assetInfo, req.PackageIDs)
|
||||||
@@ -256,29 +259,25 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
|||||||
return s.createPackageOrder(skipCtx, customerID, validationResult, redisKey, &created)
|
return s.createPackageOrder(skipCtx, customerID, validationResult, redisKey, &created)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) checkAssetOwnership(ctx context.Context, customerID uint, virtualNo string) error {
|
// getOrderAssetInfo 从订单中提取资产类型和 ID,无需额外 DB 查询
|
||||||
owned, err := s.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, virtualNo)
|
func getOrderAssetInfo(order *model.Order) (string, uint, error) {
|
||||||
if err != nil {
|
if order == nil {
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
return "", 0, errors.New(errors.CodeNotFound, "订单不存在")
|
||||||
}
|
}
|
||||||
if owned {
|
switch order.OrderType {
|
||||||
return nil
|
case model.OrderTypeSingleCard:
|
||||||
}
|
if order.IotCardID == nil || *order.IotCardID == 0 {
|
||||||
|
return "", 0, errors.New(errors.CodeInvalidParam)
|
||||||
records, err := s.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
if record == nil {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
return "iot_card", *order.IotCardID, nil
|
||||||
return nil
|
case model.OrderTypeDevice:
|
||||||
|
if order.DeviceID == nil || *order.DeviceID == 0 {
|
||||||
|
return "", 0, errors.New(errors.CodeInvalidParam)
|
||||||
}
|
}
|
||||||
|
return "device", *order.DeviceID, nil
|
||||||
|
default:
|
||||||
|
return "", 0, errors.New(errors.CodeInvalidParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validatePurchase(ctx context.Context, assetInfo *dto.AssetResolveResponse, packageIDs []uint) (*purchase_validation.PurchaseValidationResult, error) {
|
func (s *Service) validatePurchase(ctx context.Context, assetInfo *dto.AssetResolveResponse, packageIDs []uint) (*purchase_validation.PurchaseValidationResult, error) {
|
||||||
@@ -1066,8 +1065,10 @@ func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.Clie
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil {
|
if owned, err := s.customerBinding.OwnsAsset(skipCtx, customerID, assetInfo.AssetType, assetInfo.AssetID); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
||||||
|
} else if !owned {
|
||||||
|
return nil, 0, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
generation, err := s.getAssetGeneration(skipCtx, assetInfo.AssetType, assetInfo.AssetID)
|
generation, err := s.getAssetGeneration(skipCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||||
@@ -1150,13 +1151,14 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
||||||
virtualNo, err := s.getOrderVirtualNo(skipCtx, order)
|
orderAssetType, orderAssetID, err := getOrderAssetInfo(order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if owned, ownErr := s.customerBinding.OwnsAsset(skipCtx, customerID, orderAssetType, orderAssetID); ownErr != nil {
|
||||||
if err := s.checkAssetOwnership(skipCtx, customerID, virtualNo); err != nil {
|
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||||
return nil, err
|
} else if !owned {
|
||||||
|
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
packages := make([]dto.ClientOrderPackageItem, 0, len(items))
|
packages := make([]dto.ClientOrderPackageItem, 0, len(items))
|
||||||
|
|||||||
473
internal/service/customer_binding/service.go
Normal file
473
internal/service/customer_binding/service.go
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
// Package customer_binding 封装客户与资产之间的绑定创建和归属验证逻辑
|
||||||
|
// 屏蔽"有虚拟号走 tb_personal_customer_device / 无虚拟号走 tb_personal_customer_iccid"的路由细节
|
||||||
|
package customer_binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
assetTypeIotCard = "iot_card"
|
||||||
|
assetTypeDevice = "device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// normalizeAssetType 统一资产类型字符串
|
||||||
|
// assetService.Resolve 返回 "card",client_auth 内部使用 "iot_card",两者等价
|
||||||
|
func normalizeAssetType(assetType string) string {
|
||||||
|
if assetType == "card" {
|
||||||
|
return assetTypeIotCard
|
||||||
|
}
|
||||||
|
return assetType
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardReader 按 ID 查询 IoT 卡(OwnsAsset 使用,无需感知事务)
|
||||||
|
type cardReader interface {
|
||||||
|
GetByID(ctx context.Context, id uint) (*model.IotCard, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deviceReader 按 ID 查询设备(OwnsAsset 使用,无需感知事务)
|
||||||
|
type deviceReader interface {
|
||||||
|
GetByID(ctx context.Context, id uint) (*model.Device, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pcdOps tb_personal_customer_device 操作接口
|
||||||
|
type pcdOps interface {
|
||||||
|
ExistsByCustomerAndDevice(ctx context.Context, customerID uint, deviceNo string) (bool, error)
|
||||||
|
Create(ctx context.Context, record *model.PersonalCustomerDevice) error
|
||||||
|
CountByVirtualNo(ctx context.Context, virtualNo string) (int64, error)
|
||||||
|
GetByDeviceNo(ctx context.Context, deviceNo string) ([]*model.PersonalCustomerDevice, error)
|
||||||
|
UpdateStatus(ctx context.Context, id uint, status int) error
|
||||||
|
UpdateVirtualNo(ctx context.Context, id uint, newVirtualNo string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// pciOps tb_personal_customer_iccid 操作接口
|
||||||
|
type pciOps interface {
|
||||||
|
ExistsByCustomerAndICCID(ctx context.Context, customerID uint, iccid string) (bool, error)
|
||||||
|
Create(ctx context.Context, record *model.PersonalCustomerICCID) error
|
||||||
|
CountByICCID(ctx context.Context, iccid string) (int64, error)
|
||||||
|
GetByICCID(ctx context.Context, iccid string) ([]*model.PersonalCustomerICCID, error)
|
||||||
|
UpdateStatus(ctx context.Context, id uint, status int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 客户绑定服务
|
||||||
|
type Service struct {
|
||||||
|
db *gorm.DB
|
||||||
|
cards cardReader
|
||||||
|
devices deviceReader
|
||||||
|
makePCD func(*gorm.DB) pcdOps
|
||||||
|
makePCI func(*gorm.DB) pciOps
|
||||||
|
markAsSold func(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) error
|
||||||
|
// readCard/readDevice 用于 Bind/Migrate 内部,接收事务 db 以保持读写在同一事务内
|
||||||
|
readCard func(ctx context.Context, db *gorm.DB, id uint) (*model.IotCard, error)
|
||||||
|
readDevice func(ctx context.Context, db *gorm.DB, id uint) (*model.Device, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 创建客户绑定服务实例
|
||||||
|
func New(db *gorm.DB, iotCardStore cardReader, deviceStore deviceReader) *Service {
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
cards: iotCardStore,
|
||||||
|
devices: deviceStore,
|
||||||
|
makePCD: func(db *gorm.DB) pcdOps {
|
||||||
|
return postgres.NewPersonalCustomerDeviceStore(db)
|
||||||
|
},
|
||||||
|
makePCI: func(db *gorm.DB) pciOps {
|
||||||
|
return postgres.NewPersonalCustomerICCIDStore(db)
|
||||||
|
},
|
||||||
|
markAsSold: realMarkAssetAsSold,
|
||||||
|
readCard: func(ctx context.Context, db *gorm.DB, id uint) (*model.IotCard, error) {
|
||||||
|
var card model.IotCard
|
||||||
|
if err := db.WithContext(ctx).First(&card, id).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, errors.New(errors.CodeAssetNotFound)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡资产失败")
|
||||||
|
}
|
||||||
|
return &card, nil
|
||||||
|
},
|
||||||
|
readDevice: func(ctx context.Context, db *gorm.DB, id uint) (*model.Device, error) {
|
||||||
|
var device model.Device
|
||||||
|
if err := db.WithContext(ctx).First(&device, id).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, errors.New(errors.CodeAssetNotFound)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备资产失败")
|
||||||
|
}
|
||||||
|
return &device, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnsAsset 验证客户是否持有指定资产的有效绑定(status=1)
|
||||||
|
// 内部根据 assetType + assetID 自动路由到对应绑定表,调用方无需感知底层存储细节
|
||||||
|
func (s *Service) OwnsAsset(ctx context.Context, customerID uint, assetType string, assetID uint) (bool, error) {
|
||||||
|
pcd := s.makePCD(s.db)
|
||||||
|
|
||||||
|
switch normalizeAssetType(assetType) {
|
||||||
|
case assetTypeIotCard:
|
||||||
|
pci := s.makePCI(s.db)
|
||||||
|
card, err := s.cards.GetByID(ctx, assetID)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return false, errors.New(errors.CodeAssetNotFound)
|
||||||
|
}
|
||||||
|
return false, errors.Wrap(errors.CodeInternalError, err, "查询卡资产失败")
|
||||||
|
}
|
||||||
|
if card.VirtualNo != "" {
|
||||||
|
return pcd.ExistsByCustomerAndDevice(ctx, customerID, card.VirtualNo)
|
||||||
|
}
|
||||||
|
// 无虚拟号:查 pci 表(Issue 02 完整实现,此处已路由到正确路径)
|
||||||
|
return pci.ExistsByCustomerAndICCID(ctx, customerID, card.ICCID)
|
||||||
|
|
||||||
|
case assetTypeDevice:
|
||||||
|
device, err := s.devices.GetByID(ctx, assetID)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return false, errors.New(errors.CodeAssetNotFound)
|
||||||
|
}
|
||||||
|
return false, errors.Wrap(errors.CodeInternalError, err, "查询设备资产失败")
|
||||||
|
}
|
||||||
|
key := device.VirtualNo
|
||||||
|
if key == "" {
|
||||||
|
key = device.IMEI
|
||||||
|
}
|
||||||
|
return pcd.ExistsByCustomerAndDevice(ctx, customerID, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind 在事务中创建客户与资产的绑定关系
|
||||||
|
// 有虚拟号的 IoT 卡 / 设备 → tb_personal_customer_device
|
||||||
|
// 无虚拟号的 IoT 卡 → tb_personal_customer_iccid(Issue 02)
|
||||||
|
func (s *Service) Bind(ctx context.Context, tx *gorm.DB, customerID uint, assetType string, assetID uint) error {
|
||||||
|
if tx == nil {
|
||||||
|
tx = s.db
|
||||||
|
}
|
||||||
|
pcd := s.makePCD(tx)
|
||||||
|
pci := s.makePCI(tx)
|
||||||
|
|
||||||
|
switch normalizeAssetType(assetType) {
|
||||||
|
case assetTypeIotCard:
|
||||||
|
card, err := s.readCard(ctx, tx, assetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if card.VirtualNo != "" {
|
||||||
|
return s.bindViaPCD(ctx, pcd, tx, customerID, card.VirtualNo, assetTypeIotCard, assetID)
|
||||||
|
}
|
||||||
|
// 无虚拟号路径(Issue 02)
|
||||||
|
return s.bindViaPCI(ctx, pci, tx, customerID, card.ICCID, assetTypeIotCard, assetID)
|
||||||
|
|
||||||
|
case assetTypeDevice:
|
||||||
|
device, err := s.readDevice(ctx, tx, assetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := device.VirtualNo
|
||||||
|
if key == "" {
|
||||||
|
key = device.IMEI
|
||||||
|
}
|
||||||
|
return s.bindViaPCD(ctx, pcd, tx, customerID, key, assetType, assetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindViaPCD 通过 tb_personal_customer_device 创建绑定
|
||||||
|
func (s *Service) bindViaPCD(ctx context.Context, pcd pcdOps, tx *gorm.DB, customerID uint, virtualNo string, assetType string, assetID uint) error {
|
||||||
|
count, err := pcd.CountByVirtualNo(ctx, virtualNo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "查询资产绑定数量失败")
|
||||||
|
}
|
||||||
|
firstEverBind := count == 0
|
||||||
|
|
||||||
|
exists, err := pcd.ExistsByCustomerAndDevice(ctx, customerID, virtualNo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "查询客户资产绑定关系失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
record := &model.PersonalCustomerDevice{
|
||||||
|
CustomerID: customerID,
|
||||||
|
VirtualNo: virtualNo,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err := pcd.Create(ctx, record); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "创建资产绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstEverBind {
|
||||||
|
return s.markAsSold(ctx, tx, assetType, assetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindViaPCI 通过 tb_personal_customer_iccid 创建绑定(无虚拟号卡专用,Issue 02)
|
||||||
|
func (s *Service) bindViaPCI(ctx context.Context, pci pciOps, tx *gorm.DB, customerID uint, iccid string, assetType string, assetID uint) error {
|
||||||
|
count, err := pci.CountByICCID(ctx, iccid)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "查询 ICCID 绑定数量失败")
|
||||||
|
}
|
||||||
|
firstEverBind := count == 0
|
||||||
|
|
||||||
|
exists, err := pci.ExistsByCustomerAndICCID(ctx, customerID, iccid)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "查询客户 ICCID 绑定关系失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
iccid19 := iccid
|
||||||
|
if len(iccid) == 20 {
|
||||||
|
iccid19 = iccid[:19]
|
||||||
|
}
|
||||||
|
record := &model.PersonalCustomerICCID{
|
||||||
|
CustomerID: customerID,
|
||||||
|
ICCID: iccid,
|
||||||
|
ICCID19: iccid19,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err := pci.Create(ctx, record); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "创建 ICCID 绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstEverBind {
|
||||||
|
return s.markAsSold(ctx, tx, assetType, assetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate 将旧资产的所有有效客户绑定迁移到新资产(换货专用)
|
||||||
|
// 无绑定时静默跳过;按旧/新资产虚拟号有无路由到 pcd 或 pci
|
||||||
|
func (s *Service) Migrate(ctx context.Context, tx *gorm.DB, oldAssetType string, oldAssetID uint, newAssetType string, newAssetID uint) error {
|
||||||
|
if tx == nil {
|
||||||
|
tx = s.db
|
||||||
|
}
|
||||||
|
pcd := s.makePCD(tx)
|
||||||
|
pci := s.makePCI(tx)
|
||||||
|
|
||||||
|
switch normalizeAssetType(oldAssetType) {
|
||||||
|
case assetTypeIotCard:
|
||||||
|
oldCard, err := s.readCard(ctx, tx, oldAssetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if oldCard.VirtualNo != "" {
|
||||||
|
return s.migrateFromPCD(ctx, tx, pcd, pci, oldCard.VirtualNo, newAssetType, newAssetID)
|
||||||
|
}
|
||||||
|
return s.migrateFromPCI(ctx, tx, pcd, pci, oldCard.ICCID, newAssetType, newAssetID)
|
||||||
|
|
||||||
|
case assetTypeDevice:
|
||||||
|
oldDevice, err := s.readDevice(ctx, tx, oldAssetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := oldDevice.VirtualNo
|
||||||
|
if key == "" {
|
||||||
|
key = oldDevice.IMEI
|
||||||
|
}
|
||||||
|
return s.migrateFromPCD(ctx, tx, pcd, pci, key, newAssetType, newAssetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateFromPCD 将 tb_personal_customer_device 中 oldKey 的所有有效绑定迁移到新资产
|
||||||
|
func (s *Service) migrateFromPCD(ctx context.Context, db *gorm.DB, pcd pcdOps, pci pciOps, oldKey string, newAssetType string, newAssetID uint) error {
|
||||||
|
records, err := pcd.GetByDeviceNo(ctx, oldKey)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "查询旧资产绑定记录失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅处理 status=1 的有效记录
|
||||||
|
var active []*model.PersonalCustomerDevice
|
||||||
|
for _, r := range records {
|
||||||
|
if r.Status == 1 {
|
||||||
|
active = append(active, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(active) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch normalizeAssetType(newAssetType) {
|
||||||
|
case assetTypeIotCard:
|
||||||
|
newCard, err := s.readCard(ctx, db, newAssetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if newCard.VirtualNo != "" {
|
||||||
|
// 新卡有虚拟号:更新 pcd virtual_no
|
||||||
|
for _, rec := range active {
|
||||||
|
if err := pcd.UpdateVirtualNo(ctx, rec.ID, newCard.VirtualNo); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "迁移客户绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新卡无虚拟号:禁用旧 pcd + 创建新 pci
|
||||||
|
iccid19 := newCard.ICCID
|
||||||
|
if len(newCard.ICCID) == 20 {
|
||||||
|
iccid19 = newCard.ICCID[:19]
|
||||||
|
}
|
||||||
|
for _, rec := range active {
|
||||||
|
if err := pcd.UpdateStatus(ctx, rec.ID, 0); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "禁用旧客户绑定关系失败")
|
||||||
|
}
|
||||||
|
newPCI := &model.PersonalCustomerICCID{
|
||||||
|
CustomerID: rec.CustomerID,
|
||||||
|
ICCID: newCard.ICCID,
|
||||||
|
ICCID19: iccid19,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err := pci.Create(ctx, newPCI); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "创建新客户绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case assetTypeDevice:
|
||||||
|
newDevice, err := s.readDevice(ctx, db, newAssetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newKey := newDevice.VirtualNo
|
||||||
|
if newKey == "" {
|
||||||
|
newKey = newDevice.IMEI
|
||||||
|
}
|
||||||
|
for _, rec := range active {
|
||||||
|
if err := pcd.UpdateVirtualNo(ctx, rec.ID, newKey); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "迁移设备客户绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateFromPCI 将 tb_personal_customer_iccid 中 oldICCID 的所有有效绑定迁移到新资产
|
||||||
|
func (s *Service) migrateFromPCI(ctx context.Context, db *gorm.DB, pcd pcdOps, pci pciOps, oldICCID string, newAssetType string, newAssetID uint) error {
|
||||||
|
records, err := pci.GetByICCID(ctx, oldICCID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "查询旧 ICCID 绑定记录失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
var active []*model.PersonalCustomerICCID
|
||||||
|
for _, r := range records {
|
||||||
|
if r.Status == 1 {
|
||||||
|
active = append(active, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(active) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch normalizeAssetType(newAssetType) {
|
||||||
|
case assetTypeIotCard:
|
||||||
|
newCard, err := s.readCard(ctx, db, newAssetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if newCard.VirtualNo != "" {
|
||||||
|
// 新卡有虚拟号:禁用旧 pci + 创建新 pcd
|
||||||
|
for _, rec := range active {
|
||||||
|
if err := pci.UpdateStatus(ctx, rec.ID, 0); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "禁用旧 ICCID 绑定关系失败")
|
||||||
|
}
|
||||||
|
newPCD := &model.PersonalCustomerDevice{
|
||||||
|
CustomerID: rec.CustomerID,
|
||||||
|
VirtualNo: newCard.VirtualNo,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err := pcd.Create(ctx, newPCD); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "创建新客户绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新卡也无虚拟号:禁用旧 pci + 创建新 pci(新 ICCID)
|
||||||
|
iccid19 := newCard.ICCID
|
||||||
|
if len(newCard.ICCID) == 20 {
|
||||||
|
iccid19 = newCard.ICCID[:19]
|
||||||
|
}
|
||||||
|
for _, rec := range active {
|
||||||
|
if err := pci.UpdateStatus(ctx, rec.ID, 0); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "禁用旧 ICCID 绑定关系失败")
|
||||||
|
}
|
||||||
|
newPCI := &model.PersonalCustomerICCID{
|
||||||
|
CustomerID: rec.CustomerID,
|
||||||
|
ICCID: newCard.ICCID,
|
||||||
|
ICCID19: iccid19,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err := pci.Create(ctx, newPCI); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "创建新 ICCID 绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case assetTypeDevice:
|
||||||
|
newDevice, err := s.readDevice(ctx, db, newAssetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newKey := newDevice.VirtualNo
|
||||||
|
if newKey == "" {
|
||||||
|
newKey = newDevice.IMEI
|
||||||
|
}
|
||||||
|
for _, rec := range active {
|
||||||
|
if err := pci.UpdateStatus(ctx, rec.ID, 0); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "禁用旧 ICCID 绑定关系失败")
|
||||||
|
}
|
||||||
|
newPCD := &model.PersonalCustomerDevice{
|
||||||
|
CustomerID: rec.CustomerID,
|
||||||
|
VirtualNo: newKey,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err := pcd.Create(ctx, newPCD); err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "创建新客户绑定关系失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// realMarkAssetAsSold 将资产状态从在库(1)更新为已销售(2)
|
||||||
|
func realMarkAssetAsSold(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) error {
|
||||||
|
now := time.Now()
|
||||||
|
switch assetType {
|
||||||
|
case assetTypeIotCard:
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&model.IotCard{}).
|
||||||
|
Where("id = ? AND asset_status = ?", assetID, 1).
|
||||||
|
Updates(map[string]any{"asset_status": 2, "updated_at": now}).Error; err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "更新卡资产状态失败")
|
||||||
|
}
|
||||||
|
case assetTypeDevice:
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&model.Device{}).
|
||||||
|
Where("id = ? AND asset_status = ?", assetID, 1).
|
||||||
|
Updates(map[string]any{"asset_status": 2, "updated_at": now}).Error; err != nil {
|
||||||
|
return errors.Wrap(errors.CodeInternalError, err, "更新设备资产状态失败")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
715
internal/service/customer_binding/service_test.go
Normal file
715
internal/service/customer_binding/service_test.go
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
package customer_binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- 测试工具 ----
|
||||||
|
|
||||||
|
// bindKey 构建 mock 中使用的查找键
|
||||||
|
func bindKey(customerID uint, key string) string {
|
||||||
|
return fmt.Sprintf("%d:%s", customerID, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- mock 实现 ----
|
||||||
|
|
||||||
|
type mockCardReader struct {
|
||||||
|
cards map[uint]*model.IotCard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCardReader) GetByID(_ context.Context, id uint) (*model.IotCard, error) {
|
||||||
|
if c, ok := m.cards[id]; ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockDeviceReader struct {
|
||||||
|
devices map[uint]*model.Device
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockDeviceReader) GetByID(_ context.Context, id uint) (*model.Device, error) {
|
||||||
|
if d, ok := m.devices[id]; ok {
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockPCDOps 模拟 tb_personal_customer_device 操作
|
||||||
|
type mockPCDOps struct {
|
||||||
|
// bindings["customerID:virtualNo"] = true 表示有效(status=1)绑定
|
||||||
|
bindings map[string]bool
|
||||||
|
created []*model.PersonalCustomerDevice
|
||||||
|
counts map[string]int64 // virtualNo → count(用于首绑判断)
|
||||||
|
allRecords []*model.PersonalCustomerDevice // GetByDeviceNo 和 UpdateStatus/UpdateVirtualNo 使用
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCDOps) ExistsByCustomerAndDevice(_ context.Context, customerID uint, deviceNo string) (bool, error) {
|
||||||
|
return m.bindings[bindKey(customerID, deviceNo)], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCDOps) Create(_ context.Context, record *model.PersonalCustomerDevice) error {
|
||||||
|
m.created = append(m.created, record)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCDOps) CountByVirtualNo(_ context.Context, virtualNo string) (int64, error) {
|
||||||
|
if m.counts != nil {
|
||||||
|
return m.counts[virtualNo], nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCDOps) GetByDeviceNo(_ context.Context, deviceNo string) ([]*model.PersonalCustomerDevice, error) {
|
||||||
|
var result []*model.PersonalCustomerDevice
|
||||||
|
for _, r := range m.allRecords {
|
||||||
|
if r.VirtualNo == deviceNo {
|
||||||
|
result = append(result, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCDOps) UpdateStatus(_ context.Context, id uint, status int) error {
|
||||||
|
for _, r := range m.allRecords {
|
||||||
|
if r.ID == id {
|
||||||
|
r.Status = status
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCDOps) UpdateVirtualNo(_ context.Context, id uint, newVirtualNo string) error {
|
||||||
|
for _, r := range m.allRecords {
|
||||||
|
if r.ID == id {
|
||||||
|
r.VirtualNo = newVirtualNo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockPCIOps 模拟 tb_personal_customer_iccid 操作
|
||||||
|
type mockPCIOps struct {
|
||||||
|
bindings map[string]bool
|
||||||
|
created []*model.PersonalCustomerICCID
|
||||||
|
counts map[string]int64 // iccid → count
|
||||||
|
allRecords []*model.PersonalCustomerICCID // GetByICCID 和 UpdateStatus 使用
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCIOps) ExistsByCustomerAndICCID(_ context.Context, customerID uint, iccid string) (bool, error) {
|
||||||
|
return m.bindings[bindKey(customerID, iccid)], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCIOps) Create(_ context.Context, record *model.PersonalCustomerICCID) error {
|
||||||
|
m.created = append(m.created, record)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCIOps) CountByICCID(_ context.Context, iccid string) (int64, error) {
|
||||||
|
if m.counts != nil {
|
||||||
|
return m.counts[iccid], nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCIOps) GetByICCID(_ context.Context, iccid string) ([]*model.PersonalCustomerICCID, error) {
|
||||||
|
var result []*model.PersonalCustomerICCID
|
||||||
|
for _, r := range m.allRecords {
|
||||||
|
if r.ICCID == iccid {
|
||||||
|
result = append(result, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPCIOps) UpdateStatus(_ context.Context, id uint, status int) error {
|
||||||
|
for _, r := range m.allRecords {
|
||||||
|
if r.ID == id {
|
||||||
|
r.Status = status
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 测试 Service 构造器 ----
|
||||||
|
|
||||||
|
// soldCalls 记录 markAsSold 调用
|
||||||
|
type soldCalls struct {
|
||||||
|
items []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *soldCalls) mark(_ context.Context, _ *gorm.DB, assetType string, _ uint) error {
|
||||||
|
s.items = append(s.items, assetType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestService(cards cardReader, devices deviceReader, pcd pcdOps, pci pciOps) (*Service, *soldCalls) {
|
||||||
|
sold := &soldCalls{}
|
||||||
|
return &Service{
|
||||||
|
cards: cards,
|
||||||
|
devices: devices,
|
||||||
|
readCard: func(ctx context.Context, _ *gorm.DB, id uint) (*model.IotCard, error) {
|
||||||
|
return cards.GetByID(ctx, id)
|
||||||
|
},
|
||||||
|
readDevice: func(ctx context.Context, _ *gorm.DB, id uint) (*model.Device, error) {
|
||||||
|
return devices.GetByID(ctx, id)
|
||||||
|
},
|
||||||
|
makePCD: func(_ *gorm.DB) pcdOps { return pcd },
|
||||||
|
makePCI: func(_ *gorm.DB) pciOps { return pci },
|
||||||
|
markAsSold: sold.mark,
|
||||||
|
}, sold
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- OwnsAsset 测试 ----
|
||||||
|
|
||||||
|
// 验证:有虚拟号卡 + 客户有有效绑定 → true(tracer bullet)
|
||||||
|
func TestOwnsAsset_有虚拟号卡_有效绑定_返回true(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN001"}
|
||||||
|
card.ID = 1
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{bindKey(10, "VN001"): true},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{1: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if !owned {
|
||||||
|
t.Fatal("期望返回 true,实际返回 false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:有虚拟号卡 + status=0 的绑定 → false(修复安全缺口)
|
||||||
|
func TestOwnsAsset_有虚拟号卡_禁用绑定_返回false(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN002"}
|
||||||
|
card.ID = 2
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
// status=0 的绑定:在 ExistsByCustomerAndDevice 中会过滤掉(bindings 中不存在)
|
||||||
|
bindings: map[string]bool{},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{2: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 2)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if owned {
|
||||||
|
t.Fatal("期望返回 false(status=0),实际返回 true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:有虚拟号卡 + 无绑定 → false
|
||||||
|
func TestOwnsAsset_有虚拟号卡_无绑定_返回false(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN003"}
|
||||||
|
card.ID = 3
|
||||||
|
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{3: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
&mockPCDOps{bindings: map[string]bool{}},
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 3)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if owned {
|
||||||
|
t.Fatal("期望返回 false(无绑定),实际返回 true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:assetType="card"(来自 assetService.Resolve)等价于 "iot_card"
|
||||||
|
func TestOwnsAsset_assetType_card_等价iot_card(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN_CARD"}
|
||||||
|
card.ID = 9
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{bindKey(10, "VN_CARD"): true},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{9: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "card", 9)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if !owned {
|
||||||
|
t.Fatal("期望 card 类型等价 iot_card 返回 true,实际 false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:设备资产 + 有效绑定 → true
|
||||||
|
func TestOwnsAsset_设备_有效绑定_返回true(t *testing.T) {
|
||||||
|
device := &model.Device{VirtualNo: "DEV001"}
|
||||||
|
device.ID = 5
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{bindKey(10, "DEV001"): true},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{},
|
||||||
|
&mockDeviceReader{devices: map[uint]*model.Device{5: device}},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "device", 5)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if !owned {
|
||||||
|
t.Fatal("期望返回 true,实际返回 false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Bind 测试 ----
|
||||||
|
|
||||||
|
// 验证:首次绑定有虚拟号卡 → 写入 pcd 记录 + 触发 markAssetAsSold
|
||||||
|
func TestBind_有虚拟号卡_首次绑定_创建记录并标记已售(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN010"}
|
||||||
|
card.ID = 10
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{},
|
||||||
|
counts: map[string]int64{"VN010": 0}, // 首次绑定
|
||||||
|
}
|
||||||
|
svc, sold := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{10: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Bind(context.Background(), nil, 20, "iot_card", 10)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pcd.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条 pcd 记录,实际: %d", len(pcd.created))
|
||||||
|
}
|
||||||
|
if pcd.created[0].VirtualNo != "VN010" {
|
||||||
|
t.Errorf("期望 VirtualNo=VN010,实际: %s", pcd.created[0].VirtualNo)
|
||||||
|
}
|
||||||
|
if pcd.created[0].CustomerID != 20 {
|
||||||
|
t.Errorf("期望 CustomerID=20,实际: %d", pcd.created[0].CustomerID)
|
||||||
|
}
|
||||||
|
if len(sold.items) != 1 || sold.items[0] != "iot_card" {
|
||||||
|
t.Errorf("期望触发 markAssetAsSold(iot_card),实际: %v", sold.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:重复绑定 → 不创建新记录
|
||||||
|
func TestBind_有虚拟号卡_已有绑定_不重复创建(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN011"}
|
||||||
|
card.ID = 11
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{bindKey(20, "VN011"): true},
|
||||||
|
counts: map[string]int64{"VN011": 1},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{11: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Bind(context.Background(), nil, 20, "iot_card", 11)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pcd.created) != 0 {
|
||||||
|
t.Fatalf("期望不创建新记录,实际创建了 %d 条", len(pcd.created))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:非首次绑定(已有其他客户绑定)→ 创建记录但不触发 markAssetAsSold
|
||||||
|
func TestBind_有虚拟号卡_非首次绑定_创建记录不标记已售(t *testing.T) {
|
||||||
|
card := &model.IotCard{VirtualNo: "VN012"}
|
||||||
|
card.ID = 12
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{},
|
||||||
|
counts: map[string]int64{"VN012": 1}, // 已有其他绑定
|
||||||
|
}
|
||||||
|
svc, sold := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{12: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Bind(context.Background(), nil, 30, "iot_card", 12)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pcd.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条记录,实际: %d", len(pcd.created))
|
||||||
|
}
|
||||||
|
if len(sold.items) != 0 {
|
||||||
|
t.Errorf("期望不触发 markAssetAsSold,实际触发了: %v", sold.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Issue 02: 无虚拟号卡 pci 路径测试 ----
|
||||||
|
|
||||||
|
// 验证:无虚拟号卡 + 客户有有效 PCI 绑定 → true
|
||||||
|
func TestOwnsAsset_无虚拟号卡_有效PCI绑定_返回true(t *testing.T) {
|
||||||
|
card := &model.IotCard{ICCID: "89860000000000000001"} // VirtualNo 为空
|
||||||
|
card.ID = 20
|
||||||
|
|
||||||
|
pci := &mockPCIOps{
|
||||||
|
bindings: map[string]bool{bindKey(10, "89860000000000000001"): true},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{20: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
&mockPCDOps{bindings: map[string]bool{}},
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 20)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if !owned {
|
||||||
|
t.Fatal("期望返回 true,实际返回 false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:无虚拟号卡 + 无 PCI 绑定 → false
|
||||||
|
func TestOwnsAsset_无虚拟号卡_无PCI绑定_返回false(t *testing.T) {
|
||||||
|
card := &model.IotCard{ICCID: "89860000000000000002"} // VirtualNo 为空
|
||||||
|
card.ID = 21
|
||||||
|
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{21: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
&mockPCDOps{bindings: map[string]bool{}},
|
||||||
|
&mockPCIOps{bindings: map[string]bool{}},
|
||||||
|
)
|
||||||
|
|
||||||
|
owned, err := svc.OwnsAsset(context.Background(), 10, "iot_card", 21)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if owned {
|
||||||
|
t.Fatal("期望返回 false(无 PCI 绑定),实际返回 true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:无虚拟号卡首次绑定 → 写入 pci 记录 + 触发 markAssetAsSold
|
||||||
|
func TestBind_无虚拟号卡_首次绑定_创建PCI记录并标记已售(t *testing.T) {
|
||||||
|
card := &model.IotCard{ICCID: "89860000000000000010"} // VirtualNo 为空
|
||||||
|
card.ID = 30
|
||||||
|
|
||||||
|
pci := &mockPCIOps{
|
||||||
|
bindings: map[string]bool{},
|
||||||
|
counts: map[string]int64{"89860000000000000010": 0}, // 首次绑定
|
||||||
|
}
|
||||||
|
svc, sold := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{30: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
&mockPCDOps{bindings: map[string]bool{}},
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Bind(context.Background(), nil, 50, "iot_card", 30)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pci.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条 pci 记录,实际: %d", len(pci.created))
|
||||||
|
}
|
||||||
|
if pci.created[0].ICCID != "89860000000000000010" {
|
||||||
|
t.Errorf("期望 ICCID=89860000000000000010,实际: %s", pci.created[0].ICCID)
|
||||||
|
}
|
||||||
|
if pci.created[0].CustomerID != 50 {
|
||||||
|
t.Errorf("期望 CustomerID=50,实际: %d", pci.created[0].CustomerID)
|
||||||
|
}
|
||||||
|
if len(sold.items) != 1 || sold.items[0] != "iot_card" {
|
||||||
|
t.Errorf("期望触发 markAssetAsSold(iot_card),实际: %v", sold.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:无虚拟号卡已有绑定 → 不重复创建
|
||||||
|
func TestBind_无虚拟号卡_已有绑定_不重复创建(t *testing.T) {
|
||||||
|
card := &model.IotCard{ICCID: "89860000000000000011"} // VirtualNo 为空
|
||||||
|
card.ID = 31
|
||||||
|
|
||||||
|
pci := &mockPCIOps{
|
||||||
|
bindings: map[string]bool{bindKey(50, "89860000000000000011"): true},
|
||||||
|
counts: map[string]int64{"89860000000000000011": 1},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{31: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
&mockPCDOps{bindings: map[string]bool{}},
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Bind(context.Background(), nil, 50, "iot_card", 31)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pci.created) != 0 {
|
||||||
|
t.Fatalf("期望不创建新记录,实际创建了 %d 条", len(pci.created))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Issue 03: Migrate 迁移测试 ----
|
||||||
|
|
||||||
|
// newMockPCDRecord 构建一条带 ID 的 pcd 记录(用于迁移测试)
|
||||||
|
func newMockPCDRecord(id, customerID uint, virtualNo string, status int) *model.PersonalCustomerDevice {
|
||||||
|
r := &model.PersonalCustomerDevice{
|
||||||
|
CustomerID: customerID,
|
||||||
|
VirtualNo: virtualNo,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
r.ID = id
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMockPCIRecord 构建一条带 ID 的 pci 记录(用于迁移测试)
|
||||||
|
func newMockPCIRecord(id, customerID uint, iccid string, status int) *model.PersonalCustomerICCID {
|
||||||
|
r := &model.PersonalCustomerICCID{
|
||||||
|
CustomerID: customerID,
|
||||||
|
ICCID: iccid,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
r.ID = id
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:旧卡有虚拟号 + pcd 有绑定 + 新卡有虚拟号 → pcd.virtual_no 更新为新卡虚拟号
|
||||||
|
func TestMigrate_有虚拟号旧卡_有绑定_换有虚拟号新卡_更新VirtualNo(t *testing.T) {
|
||||||
|
oldCard := &model.IotCard{VirtualNo: "OLD_VN"}
|
||||||
|
oldCard.ID = 100
|
||||||
|
newCard := &model.IotCard{VirtualNo: "NEW_VN"}
|
||||||
|
newCard.ID = 101
|
||||||
|
|
||||||
|
existing := newMockPCDRecord(1, 50, "OLD_VN", 1)
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
bindings: map[string]bool{},
|
||||||
|
allRecords: []*model.PersonalCustomerDevice{existing},
|
||||||
|
}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{100: oldCard, 101: newCard}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
&mockPCIOps{},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Migrate(context.Background(), nil, "iot_card", 100, "iot_card", 101)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if existing.VirtualNo != "NEW_VN" {
|
||||||
|
t.Errorf("期望 VirtualNo 更新为 NEW_VN,实际: %s", existing.VirtualNo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:旧卡有虚拟号 + pcd 有绑定 + 新卡无虚拟号 → 旧 pcd status=0,新 pci 创建
|
||||||
|
func TestMigrate_有虚拟号旧卡_有绑定_换无虚拟号新卡_迁移到PCI(t *testing.T) {
|
||||||
|
oldCard := &model.IotCard{VirtualNo: "OLD_VN2"}
|
||||||
|
oldCard.ID = 110
|
||||||
|
newCard := &model.IotCard{ICCID: "89860000000000000099"} // 无虚拟号
|
||||||
|
newCard.ID = 111
|
||||||
|
|
||||||
|
existing := newMockPCDRecord(2, 60, "OLD_VN2", 1)
|
||||||
|
pcd := &mockPCDOps{
|
||||||
|
allRecords: []*model.PersonalCustomerDevice{existing},
|
||||||
|
}
|
||||||
|
pci := &mockPCIOps{}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{110: oldCard, 111: newCard}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Migrate(context.Background(), nil, "iot_card", 110, "iot_card", 111)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if existing.Status != 0 {
|
||||||
|
t.Errorf("期望旧 pcd 记录 status=0,实际: %d", existing.Status)
|
||||||
|
}
|
||||||
|
if len(pci.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条 pci 记录,实际: %d", len(pci.created))
|
||||||
|
}
|
||||||
|
if pci.created[0].CustomerID != 60 {
|
||||||
|
t.Errorf("期望 pci CustomerID=60,实际: %d", pci.created[0].CustomerID)
|
||||||
|
}
|
||||||
|
if pci.created[0].ICCID != "89860000000000000099" {
|
||||||
|
t.Errorf("期望 pci ICCID=89860000000000000099,实际: %s", pci.created[0].ICCID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:旧卡有虚拟号 + pcd 无绑定 + 新卡无虚拟号 → 跳过,无写入
|
||||||
|
func TestMigrate_有虚拟号旧卡_无绑定_换无虚拟号新卡_跳过(t *testing.T) {
|
||||||
|
oldCard := &model.IotCard{VirtualNo: "OLD_VN3"}
|
||||||
|
oldCard.ID = 120
|
||||||
|
newCard := &model.IotCard{ICCID: "89860000000000000088"} // 无虚拟号
|
||||||
|
newCard.ID = 121
|
||||||
|
|
||||||
|
pcd := &mockPCDOps{allRecords: []*model.PersonalCustomerDevice{}} // 空
|
||||||
|
pci := &mockPCIOps{}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{120: oldCard, 121: newCard}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Migrate(context.Background(), nil, "iot_card", 120, "iot_card", 121)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pci.created) != 0 {
|
||||||
|
t.Fatalf("期望无写入,实际创建了 %d 条 pci 记录", len(pci.created))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:旧卡无虚拟号 + pci 有绑定 + 新卡有虚拟号 → 旧 pci status=0,新 pcd 创建
|
||||||
|
func TestMigrate_无虚拟号旧卡_有绑定_换有虚拟号新卡_迁移到PCD(t *testing.T) {
|
||||||
|
oldCard := &model.IotCard{ICCID: "89860000000000000077"} // 无虚拟号
|
||||||
|
oldCard.ID = 130
|
||||||
|
newCard := &model.IotCard{VirtualNo: "NEW_VN3"}
|
||||||
|
newCard.ID = 131
|
||||||
|
|
||||||
|
existingPCI := newMockPCIRecord(3, 70, "89860000000000000077", 1)
|
||||||
|
pci := &mockPCIOps{allRecords: []*model.PersonalCustomerICCID{existingPCI}}
|
||||||
|
pcd := &mockPCDOps{}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{130: oldCard, 131: newCard}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Migrate(context.Background(), nil, "iot_card", 130, "iot_card", 131)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if existingPCI.Status != 0 {
|
||||||
|
t.Errorf("期望旧 pci 记录 status=0,实际: %d", existingPCI.Status)
|
||||||
|
}
|
||||||
|
if len(pcd.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条 pcd 记录,实际: %d", len(pcd.created))
|
||||||
|
}
|
||||||
|
if pcd.created[0].CustomerID != 70 {
|
||||||
|
t.Errorf("期望 pcd CustomerID=70,实际: %d", pcd.created[0].CustomerID)
|
||||||
|
}
|
||||||
|
if pcd.created[0].VirtualNo != "NEW_VN3" {
|
||||||
|
t.Errorf("期望 pcd VirtualNo=NEW_VN3,实际: %s", pcd.created[0].VirtualNo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:旧卡无虚拟号 + pci 有绑定 + 新卡也无虚拟号 → 旧 pci status=0,新 pci 创建新 ICCID
|
||||||
|
func TestMigrate_无虚拟号旧卡_有绑定_换无虚拟号新卡_迁移PCI(t *testing.T) {
|
||||||
|
oldCard := &model.IotCard{ICCID: "89860000000000000066"} // 无虚拟号
|
||||||
|
oldCard.ID = 140
|
||||||
|
newCard := &model.IotCard{ICCID: "89860000000000000055"} // 无虚拟号
|
||||||
|
newCard.ID = 141
|
||||||
|
|
||||||
|
existingPCI := newMockPCIRecord(4, 80, "89860000000000000066", 1)
|
||||||
|
pci := &mockPCIOps{allRecords: []*model.PersonalCustomerICCID{existingPCI}}
|
||||||
|
pcd := &mockPCDOps{}
|
||||||
|
svc, _ := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{140: oldCard, 141: newCard}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
pcd,
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Migrate(context.Background(), nil, "iot_card", 140, "iot_card", 141)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if existingPCI.Status != 0 {
|
||||||
|
t.Errorf("期望旧 pci 记录 status=0,实际: %d", existingPCI.Status)
|
||||||
|
}
|
||||||
|
if len(pci.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条新 pci 记录,实际: %d", len(pci.created))
|
||||||
|
}
|
||||||
|
if pci.created[0].CustomerID != 80 {
|
||||||
|
t.Errorf("期望 pci CustomerID=80,实际: %d", pci.created[0].CustomerID)
|
||||||
|
}
|
||||||
|
if pci.created[0].ICCID != "89860000000000000055" {
|
||||||
|
t.Errorf("期望 pci ICCID=89860000000000000055,实际: %s", pci.created[0].ICCID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证:无虚拟号卡非首次绑定(已有其他客户)→ 创建记录但不触发 markAssetAsSold
|
||||||
|
func TestBind_无虚拟号卡_非首次绑定_创建记录不标记已售(t *testing.T) {
|
||||||
|
card := &model.IotCard{ICCID: "89860000000000000012"} // VirtualNo 为空
|
||||||
|
card.ID = 32
|
||||||
|
|
||||||
|
pci := &mockPCIOps{
|
||||||
|
bindings: map[string]bool{},
|
||||||
|
counts: map[string]int64{"89860000000000000012": 1}, // 已有其他客户绑定
|
||||||
|
}
|
||||||
|
svc, sold := newTestService(
|
||||||
|
&mockCardReader{cards: map[uint]*model.IotCard{32: card}},
|
||||||
|
&mockDeviceReader{},
|
||||||
|
&mockPCDOps{bindings: map[string]bool{}},
|
||||||
|
pci,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := svc.Bind(context.Background(), nil, 60, "iot_card", 32)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("期望无错误,实际: %v", err)
|
||||||
|
}
|
||||||
|
if len(pci.created) != 1 {
|
||||||
|
t.Fatalf("期望创建 1 条记录,实际: %d", len(pci.created))
|
||||||
|
}
|
||||||
|
if len(sold.items) != 0 {
|
||||||
|
t.Errorf("期望不触发 markAssetAsSold,实际触发了: %v", sold.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
customerBindingSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
@@ -26,7 +27,7 @@ type Service struct {
|
|||||||
packageUsageStore *postgres.PackageUsageStore
|
packageUsageStore *postgres.PackageUsageStore
|
||||||
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore
|
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore
|
||||||
resourceTagStore *postgres.ResourceTagStore
|
resourceTagStore *postgres.ResourceTagStore
|
||||||
personalCustomerDeviceStore *postgres.PersonalCustomerDeviceStore
|
customerBinding *customerBindingSvc.Service
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ func New(
|
|||||||
packageUsageStore *postgres.PackageUsageStore,
|
packageUsageStore *postgres.PackageUsageStore,
|
||||||
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore,
|
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore,
|
||||||
resourceTagStore *postgres.ResourceTagStore,
|
resourceTagStore *postgres.ResourceTagStore,
|
||||||
personalCustomerDeviceStore *postgres.PersonalCustomerDeviceStore,
|
customerBinding *customerBindingSvc.Service,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
@@ -53,7 +54,7 @@ func New(
|
|||||||
packageUsageStore: packageUsageStore,
|
packageUsageStore: packageUsageStore,
|
||||||
packageUsageDailyRecordStore: packageUsageDailyRecordStore,
|
packageUsageDailyRecordStore: packageUsageDailyRecordStore,
|
||||||
resourceTagStore: resourceTagStore,
|
resourceTagStore: resourceTagStore,
|
||||||
personalCustomerDeviceStore: personalCustomerDeviceStore,
|
customerBinding: customerBinding,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,19 +803,8 @@ func (s *Service) validateExchangeAssetsWithTx(ctx context.Context, tx *gorm.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureNewAssetBindingAvailableWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
func (s *Service) ensureNewAssetBindingAvailableWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||||
oldKey := exchangeAssetBindingKey(oldAsset)
|
|
||||||
newKey := exchangeAssetBindingKey(newAsset)
|
newKey := exchangeAssetBindingKey(newAsset)
|
||||||
oldBindCount := int64(0)
|
// 新资产无虚拟号时不再拦截:Migrate() 已能正确处理无绑定跳过、有绑定迁移到 pci 的情形
|
||||||
if oldKey != "" {
|
|
||||||
if err := tx.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
|
|
||||||
Where("virtual_no = ? AND status = ?", oldKey, constants.StatusEnabled).
|
|
||||||
Count(&oldBindCount).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询旧资产客户绑定失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if oldBindCount > 0 && newKey == "" {
|
|
||||||
return errors.New(errors.CodeExchangeStatusInvalid, "新资产无法承接客户绑定")
|
|
||||||
}
|
|
||||||
if newKey == "" {
|
if newKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -831,20 +821,7 @@ func (s *Service) ensureNewAssetBindingAvailableWithTx(ctx context.Context, tx *
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) switchCustomerBindingWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
func (s *Service) switchCustomerBindingWithTx(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||||
oldKey := exchangeAssetBindingKey(oldAsset)
|
return s.customerBinding.Migrate(ctx, tx, oldAsset.AssetType, oldAsset.AssetID, newAsset.AssetType, newAsset.AssetID)
|
||||||
if oldKey == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
newKey := exchangeAssetBindingKey(newAsset)
|
|
||||||
if newKey == "" {
|
|
||||||
return errors.New(errors.CodeExchangeStatusInvalid, "新资产无法承接客户绑定")
|
|
||||||
}
|
|
||||||
if err := tx.WithContext(ctx).Model(&model.PersonalCustomerDevice{}).
|
|
||||||
Where("virtual_no = ? AND status = ?", oldKey, constants.StatusEnabled).
|
|
||||||
Updates(map[string]any{"virtual_no": newKey, "updated_at": time.Now()}).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新客户绑定关系失败")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) updateAssetStatusesForCompletion(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
func (s *Service) updateAssetStatusesForCompletion(ctx context.Context, tx *gorm.DB, oldAsset, newAsset *resolvedExchangeAsset) error {
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ func (s *PersonalCustomerDeviceStore) UpdateStatus(ctx context.Context, id uint,
|
|||||||
Update("status", status).Error
|
Update("status", status).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateVirtualNo 更新指定记录的虚拟号(换货迁移专用)
|
||||||
|
func (s *PersonalCustomerDeviceStore) UpdateVirtualNo(ctx context.Context, id uint, newVirtualNo string) error {
|
||||||
|
return s.db.WithContext(ctx).
|
||||||
|
Model(&model.PersonalCustomerDevice{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("virtual_no", newVirtualNo).Error
|
||||||
|
}
|
||||||
|
|
||||||
// Delete 软删除绑定记录
|
// Delete 软删除绑定记录
|
||||||
func (s *PersonalCustomerDeviceStore) Delete(ctx context.Context, id uint) error {
|
func (s *PersonalCustomerDeviceStore) Delete(ctx context.Context, id uint) error {
|
||||||
return s.db.WithContext(ctx).Delete(&model.PersonalCustomerDevice{}, id).Error
|
return s.db.WithContext(ctx).Delete(&model.PersonalCustomerDevice{}, id).Error
|
||||||
@@ -95,6 +103,18 @@ func (s *PersonalCustomerDeviceStore) ExistsByCustomerAndDevice(ctx context.Cont
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountByVirtualNo 统计指定虚拟号的绑定记录数量(不过滤 status)
|
||||||
|
func (s *PersonalCustomerDeviceStore) CountByVirtualNo(ctx context.Context, virtualNo string) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Model(&model.PersonalCustomerDevice{}).
|
||||||
|
Where("virtual_no = ?", virtualNo).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrUpdateLastUsed 创建或更新绑定记录的最后使用时间
|
// CreateOrUpdateLastUsed 创建或更新绑定记录的最后使用时间
|
||||||
// 如果绑定记录存在,更新最后使用时间;如果不存在,创建新记录
|
// 如果绑定记录存在,更新最后使用时间;如果不存在,创建新记录
|
||||||
func (s *PersonalCustomerDeviceStore) CreateOrUpdateLastUsed(ctx context.Context, customerID uint, deviceNo string) error {
|
func (s *PersonalCustomerDeviceStore) CreateOrUpdateLastUsed(ctx context.Context, customerID uint, deviceNo string) error {
|
||||||
|
|||||||
@@ -116,6 +116,25 @@ func (s *PersonalCustomerICCIDStore) ExistsByCustomerAndICCID(ctx context.Contex
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountByICCID 统计指定 ICCID 的绑定记录数量(不过滤 status)
|
||||||
|
func (s *PersonalCustomerICCIDStore) CountByICCID(ctx context.Context, iccid string) (int64, error) {
|
||||||
|
var column string
|
||||||
|
switch len(iccid) {
|
||||||
|
case 19:
|
||||||
|
column = "iccid_19"
|
||||||
|
default:
|
||||||
|
column = "iccid"
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Model(&model.PersonalCustomerICCID{}).
|
||||||
|
Where(column+" = ?", iccid).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrUpdateLastUsed 创建或更新绑定记录的最后使用时间
|
// CreateOrUpdateLastUsed 创建或更新绑定记录的最后使用时间
|
||||||
// 如果绑定记录存在,更新最后使用时间;如果不存在,创建新记录
|
// 如果绑定记录存在,更新最后使用时间;如果不存在,创建新记录
|
||||||
func (s *PersonalCustomerICCIDStore) CreateOrUpdateLastUsed(ctx context.Context, customerID uint, iccid string) error {
|
func (s *PersonalCustomerICCIDStore) CreateOrUpdateLastUsed(ctx context.Context, customerID uint, iccid string) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user