This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user