This commit is contained in:
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
validate := validator.New()
|
||||
personalCustomerDeviceStore := postgres.NewPersonalCustomerDeviceStore(deps.DB)
|
||||
assetWalletStore := postgres.NewAssetWalletStore(deps.DB, deps.Redis)
|
||||
packageStore := postgres.NewPackageStore(deps.DB)
|
||||
shopPackageAllocationStore := postgres.NewShopPackageAllocationStore(deps.DB)
|
||||
@@ -56,7 +55,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
rechargeOrderStore,
|
||||
paymentStore,
|
||||
assetWalletStore,
|
||||
personalCustomerDeviceStore,
|
||||
svc.CustomerBinding,
|
||||
personalCustomerOpenIDStore,
|
||||
personalCustomerStore,
|
||||
personalCustomerPhoneStore,
|
||||
@@ -78,12 +77,12 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
Permission: admin.NewPermissionHandler(svc.Permission),
|
||||
PersonalCustomer: app.NewPersonalCustomerHandler(svc.PersonalCustomer, deps.Logger),
|
||||
ClientAuth: app.NewClientAuthHandler(svc.ClientAuth, deps.Logger),
|
||||
ClientAsset: app.NewClientAssetHandler(svc.Asset, personalCustomerDeviceStore, 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),
|
||||
ClientAsset: app.NewClientAssetHandler(svc.Asset, svc.CustomerBinding, assetWalletStore, packageStore, shopPackageAllocationStore, iotCardStore, deviceStore, deps.DB, deps.Logger),
|
||||
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),
|
||||
ClientExchange: app.NewClientExchangeHandler(svc.Exchange),
|
||||
ClientRealname: app.NewClientRealnameHandler(svc.Asset, personalCustomerDeviceStore, iotCardStore, deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger, svc.PollingManualTrigger),
|
||||
ClientDevice: app.NewClientDeviceHandler(svc.Asset, personalCustomerDeviceStore, deviceStore, deviceSimBindingStore, iotCardStore, deps.GatewayClient, deps.Logger),
|
||||
ClientRealname: app.NewClientRealnameHandler(svc.Asset, svc.CustomerBinding, iotCardStore, deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger, svc.PollingManualTrigger),
|
||||
ClientDevice: app.NewClientDeviceHandler(svc.Asset, svc.CustomerBinding, deviceStore, deviceSimBindingStore, iotCardStore, deps.GatewayClient, deps.Logger),
|
||||
ClientRechargeOrder: app.NewClientRechargeOrderHandler(rechargeOrderStore, paymentStore, deps.Logger),
|
||||
Shop: admin.NewShopHandler(svc.Shop),
|
||||
ShopRole: admin.NewShopRoleHandler(svc.Shop),
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
|
||||
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"
|
||||
commissionStatsSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
||||
commissionWithdrawalSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
|
||||
@@ -109,9 +110,13 @@ type services struct {
|
||||
TrafficQuery *trafficSvc.QueryService
|
||||
OperationPassword *operationPasswordSvc.Service
|
||||
AgentOpenAPI *agentOpenAPISvc.Service
|
||||
CustomerBinding *customerBindingSvc.Service
|
||||
}
|
||||
|
||||
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)
|
||||
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
|
||||
assetAudit := assetAuditSvc.NewService(s.AssetOperationLog, deps.DB)
|
||||
@@ -210,7 +215,6 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
deps.DB,
|
||||
s.PersonalCustomerOpenID,
|
||||
s.PersonalCustomer,
|
||||
s.PersonalCustomerDevice,
|
||||
s.PersonalCustomerPhone,
|
||||
s.IotCard,
|
||||
s.Device,
|
||||
@@ -219,6 +223,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
deps.JWTManager,
|
||||
deps.Redis,
|
||||
deps.Logger,
|
||||
customerBinding,
|
||||
),
|
||||
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),
|
||||
@@ -264,7 +269,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
PurchaseValidation: purchaseValidation,
|
||||
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),
|
||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig, deps.Redis, deps.Logger),
|
||||
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
|
||||
@@ -308,5 +313,6 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
s.AssetWallet,
|
||||
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/dto"
|
||||
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"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
|
||||
type ClientAssetHandler struct {
|
||||
assetService *asset.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
customerBinding *customerBinding.Service
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
packageStore *postgres.PackageStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
@@ -40,7 +41,7 @@ type ClientAssetHandler struct {
|
||||
// NewClientAssetHandler 创建 C 端资产信息处理器
|
||||
func NewClientAssetHandler(
|
||||
assetService *asset.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
binding *customerBinding.Service,
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
@@ -51,7 +52,7 @@ func NewClientAssetHandler(
|
||||
) *ClientAssetHandler {
|
||||
return &ClientAssetHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
customerBinding: binding,
|
||||
assetWalletStore: assetWalletStore,
|
||||
packageStore: packageStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
@@ -102,7 +103,7 @@ func (h *ClientAssetHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier
|
||||
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 {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
@@ -422,21 +423,6 @@ func (h *ClientAssetHandler) RefreshAsset(c *fiber.Ctx) error {
|
||||
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) {
|
||||
switch assetType {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
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/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
@@ -27,7 +28,7 @@ type deviceAssetInfo struct {
|
||||
// 提供设备卡列表、重启、恢复出厂、WiFi 配置、切卡等操作
|
||||
type ClientDeviceHandler struct {
|
||||
assetService *assetSvc.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
customerBinding *customerBinding.Service
|
||||
deviceStore *postgres.DeviceStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
@@ -38,7 +39,7 @@ type ClientDeviceHandler struct {
|
||||
// NewClientDeviceHandler 创建 C 端设备能力处理器
|
||||
func NewClientDeviceHandler(
|
||||
assetService *assetSvc.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
binding *customerBinding.Service,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
@@ -47,7 +48,7 @@ func NewClientDeviceHandler(
|
||||
) *ClientDeviceHandler {
|
||||
return &ClientDeviceHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
customerBinding: binding,
|
||||
deviceStore: deviceStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
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 {
|
||||
h.logger.Error("校验设备所有权失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("virtual_no", asset.VirtualNo),
|
||||
zap.Uint("asset_id", asset.AssetID),
|
||||
zap.Error(err))
|
||||
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/dto"
|
||||
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"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
@@ -29,7 +30,7 @@ var clientRealnameValidator = validator.New()
|
||||
// ClientRealnameHandler C 端实名认证处理器
|
||||
type ClientRealnameHandler struct {
|
||||
assetService *assetService.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
customerBinding *customerBinding.Service
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
carrierStore *postgres.CarrierStore
|
||||
@@ -41,7 +42,7 @@ type ClientRealnameHandler struct {
|
||||
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
||||
func NewClientRealnameHandler(
|
||||
assetSvc *assetService.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
binding *customerBinding.Service,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
carrierStore *postgres.CarrierStore,
|
||||
@@ -51,7 +52,7 @@ func NewClientRealnameHandler(
|
||||
) *ClientRealnameHandler {
|
||||
return &ClientRealnameHandler{
|
||||
assetService: assetSvc,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
customerBinding: binding,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
carrierStore: carrierStore,
|
||||
@@ -89,11 +90,11 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 4. 验证资产归属(个人客户必须绑定过该资产)
|
||||
owned, err := h.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, asset.VirtualNo)
|
||||
owned, err := h.customerBinding.OwnsAsset(ctx, customerID, asset.AssetType, asset.AssetID)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Error("查询资产归属失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("virtual_no", asset.VirtualNo),
|
||||
zap.Uint("asset_id", asset.AssetID),
|
||||
zap.Error(err))
|
||||
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/dto"
|
||||
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"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -31,7 +32,7 @@ import (
|
||||
// 提供 C1~C5 钱包详情、流水、充值前校验、充值下单、充值记录接口
|
||||
type ClientWalletHandler struct {
|
||||
assetService *asset.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
customerBinding *customerBinding.Service
|
||||
walletStore *postgres.AssetWalletStore
|
||||
transactionStore *postgres.AssetWalletTransactionStore
|
||||
rechargeOrderStore *postgres.RechargeOrderStore
|
||||
@@ -49,7 +50,7 @@ type ClientWalletHandler struct {
|
||||
// NewClientWalletHandler 创建 C 端钱包处理器
|
||||
func NewClientWalletHandler(
|
||||
assetService *asset.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
binding *customerBinding.Service,
|
||||
walletStore *postgres.AssetWalletStore,
|
||||
transactionStore *postgres.AssetWalletTransactionStore,
|
||||
rechargeOrderStore *postgres.RechargeOrderStore,
|
||||
@@ -65,7 +66,7 @@ func NewClientWalletHandler(
|
||||
) *ClientWalletHandler {
|
||||
return &ClientWalletHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
customerBinding: binding,
|
||||
walletStore: walletStore,
|
||||
transactionStore: transactionStore,
|
||||
rechargeOrderStore: rechargeOrderStore,
|
||||
@@ -544,7 +545,7 @@ func (h *ClientWalletHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifie
|
||||
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 {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
@@ -572,21 +573,6 @@ func (h *ClientWalletHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifie
|
||||
}, 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) {
|
||||
switch assetType {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -41,7 +42,6 @@ type Service struct {
|
||||
db *gorm.DB
|
||||
openidStore *postgres.PersonalCustomerOpenIDStore
|
||||
customerStore *postgres.PersonalCustomerStore
|
||||
deviceBindStore *postgres.PersonalCustomerDeviceStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
@@ -51,6 +51,7 @@ type Service struct {
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
wechatCache kernel.CacheInterface
|
||||
customerBinding *customerBinding.Service
|
||||
}
|
||||
|
||||
// New 创建 C 端认证服务实例
|
||||
@@ -58,7 +59,6 @@ func New(
|
||||
db *gorm.DB,
|
||||
openidStore *postgres.PersonalCustomerOpenIDStore,
|
||||
customerStore *postgres.PersonalCustomerStore,
|
||||
deviceBindStore *postgres.PersonalCustomerDeviceStore,
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
@@ -67,12 +67,12 @@ func New(
|
||||
jwtManager *auth.JWTManager,
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
binding *customerBinding.Service,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
openidStore: openidStore,
|
||||
customerStore: customerStore,
|
||||
deviceBindStore: deviceBindStore,
|
||||
phoneStore: phoneStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
@@ -82,6 +82,7 @@ func New(
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
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
|
||||
}
|
||||
|
||||
// bindAsset 绑定客户与资产关系
|
||||
// bindAsset 委托 CustomerBinding 模块处理客户与资产的绑定关系
|
||||
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)
|
||||
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)
|
||||
return s.customerBinding.Bind(ctx, tx, customerID, assetType, assetID)
|
||||
}
|
||||
|
||||
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/dto"
|
||||
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/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||
@@ -55,7 +56,7 @@ type Service struct {
|
||||
rechargeOrderStore *postgres.RechargeOrderStore
|
||||
paymentStore *postgres.PaymentStore
|
||||
walletStore *postgres.AssetWalletStore
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
customerBinding *customerBinding.Service
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
personalCustomerStore *postgres.PersonalCustomerStore
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||
@@ -78,7 +79,7 @@ func New(
|
||||
rechargeOrderStore *postgres.RechargeOrderStore,
|
||||
paymentStore *postgres.PaymentStore,
|
||||
walletStore *postgres.AssetWalletStore,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
binding *customerBinding.Service,
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||
personalCustomerStore *postgres.PersonalCustomerStore,
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
@@ -99,7 +100,7 @@ func New(
|
||||
rechargeOrderStore: rechargeOrderStore,
|
||||
paymentStore: paymentStore,
|
||||
walletStore: walletStore,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
customerBinding: binding,
|
||||
openIDStore: openIDStore,
|
||||
personalCustomerStore: personalCustomerStore,
|
||||
personalCustomerPhoneStore: personalCustomerPhoneStore,
|
||||
@@ -132,8 +133,10 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil {
|
||||
return nil, err
|
||||
if owned, err := s.customerBinding.OwnsAsset(skipCtx, customerID, assetInfo.AssetType, assetInfo.AssetID); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
||||
} else if !owned {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *Service) checkAssetOwnership(ctx context.Context, customerID uint, virtualNo string) error {
|
||||
owned, err := s.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, virtualNo)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
||||
// getOrderAssetInfo 从订单中提取资产类型和 ID,无需额外 DB 查询
|
||||
func getOrderAssetInfo(order *model.Order) (string, uint, error) {
|
||||
if order == nil {
|
||||
return "", 0, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
if owned {
|
||||
return nil
|
||||
}
|
||||
|
||||
records, err := s.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
|
||||
}
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
switch order.OrderType {
|
||||
case model.OrderTypeSingleCard:
|
||||
if order.IotCardID == nil || *order.IotCardID == 0 {
|
||||
return "", 0, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
||||
return nil
|
||||
return "iot_card", *order.IotCardID, 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) {
|
||||
@@ -1066,8 +1065,10 @@ func (s *Service) ListOrders(ctx context.Context, customerID uint, req *dto.Clie
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := s.checkAssetOwnership(skipCtx, customerID, assetInfo.VirtualNo); err != nil {
|
||||
return nil, 0, err
|
||||
if owned, err := s.customerBinding.OwnsAsset(skipCtx, customerID, assetInfo.AssetType, assetInfo.AssetID); err != nil {
|
||||
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)
|
||||
@@ -1150,13 +1151,14 @@ func (s *Service) GetOrderDetail(ctx context.Context, customerID uint, orderID u
|
||||
}
|
||||
|
||||
skipCtx := context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
virtualNo, err := s.getOrderVirtualNo(skipCtx, order)
|
||||
orderAssetType, orderAssetID, err := getOrderAssetInfo(order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.checkAssetOwnership(skipCtx, customerID, virtualNo); err != nil {
|
||||
return nil, err
|
||||
if owned, ownErr := s.customerBinding.OwnsAsset(skipCtx, customerID, orderAssetType, orderAssetID); ownErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
} else if !owned {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
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"
|
||||
"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/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -26,7 +27,7 @@ type Service struct {
|
||||
packageUsageStore *postgres.PackageUsageStore
|
||||
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore
|
||||
resourceTagStore *postgres.ResourceTagStore
|
||||
personalCustomerDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
customerBinding *customerBindingSvc.Service
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ func New(
|
||||
packageUsageStore *postgres.PackageUsageStore,
|
||||
packageUsageDailyRecordStore *postgres.PackageUsageDailyRecordStore,
|
||||
resourceTagStore *postgres.ResourceTagStore,
|
||||
personalCustomerDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
customerBinding *customerBindingSvc.Service,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
@@ -53,7 +54,7 @@ func New(
|
||||
packageUsageStore: packageUsageStore,
|
||||
packageUsageDailyRecordStore: packageUsageDailyRecordStore,
|
||||
resourceTagStore: resourceTagStore,
|
||||
personalCustomerDeviceStore: personalCustomerDeviceStore,
|
||||
customerBinding: customerBinding,
|
||||
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 {
|
||||
oldKey := exchangeAssetBindingKey(oldAsset)
|
||||
newKey := exchangeAssetBindingKey(newAsset)
|
||||
oldBindCount := int64(0)
|
||||
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, "新资产无法承接客户绑定")
|
||||
}
|
||||
// 新资产无虚拟号时不再拦截:Migrate() 已能正确处理无绑定跳过、有绑定迁移到 pci 的情形
|
||||
if newKey == "" {
|
||||
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 {
|
||||
oldKey := exchangeAssetBindingKey(oldAsset)
|
||||
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
|
||||
return s.customerBinding.Migrate(ctx, tx, oldAsset.AssetType, oldAsset.AssetID, newAsset.AssetType, newAsset.AssetID)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 软删除绑定记录
|
||||
func (s *PersonalCustomerDeviceStore) Delete(ctx context.Context, id uint) 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
|
||||
}
|
||||
|
||||
// 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 创建或更新绑定记录的最后使用时间
|
||||
// 如果绑定记录存在,更新最后使用时间;如果不存在,创建新记录
|
||||
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
|
||||
}
|
||||
|
||||
// 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 创建或更新绑定记录的最后使用时间
|
||||
// 如果绑定记录存在,更新最后使用时间;如果不存在,创建新记录
|
||||
func (s *PersonalCustomerICCIDStore) CreateOrUpdateLastUsed(ctx context.Context, customerID uint, iccid string) error {
|
||||
|
||||
Reference in New Issue
Block a user