From 14a8ea5a2ca068234df30079134596ddb28c470f Mon Sep 17 00:00:00 2001 From: huang Date: Wed, 8 Apr 2026 10:45:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8D=95=E5=8D=A1=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E5=8F=B7=E6=94=B9=E4=B8=BA=E5=8F=AF=E9=80=89?= =?UTF-8?q?=EF=BC=8C=E9=9D=9E=E7=A9=BA=E6=97=B6=E4=BF=9D=E8=AF=81=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=94=AF=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/model/iot_card.go | 2 +- internal/task/iot_card_import.go | 22 +++++++------------ ...0108_iot_card_virtual_no_optional.down.sql | 13 +++++++++++ ...000108_iot_card_virtual_no_optional.up.sql | 13 +++++++++++ pkg/utils/excel.go | 16 -------------- 5 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 migrations/000108_iot_card_virtual_no_optional.down.sql create mode 100644 migrations/000108_iot_card_virtual_no_optional.up.sql diff --git a/internal/model/iot_card.go b/internal/model/iot_card.go index 352d6e4..b4d77d0 100644 --- a/internal/model/iot_card.go +++ b/internal/model/iot_card.go @@ -52,7 +52,7 @@ type IotCard struct { AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"` Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"` IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"` - VirtualNo string `gorm:"column:virtual_no;type:varchar(50);not null;uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL;comment:虚拟号(必填,全局唯一)" json:"virtual_no"` + VirtualNo string `gorm:"column:virtual_no;type:varchar(50);uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';comment:虚拟号(可选,非空时全局唯一)" json:"virtual_no,omitempty"` LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"` } diff --git a/internal/task/iot_card_import.go b/internal/task/iot_card_import.go index 571942a..d5f56ba 100644 --- a/internal/task/iot_card_import.go +++ b/internal/task/iot_card_import.go @@ -305,7 +305,9 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot virtualNos := make([]string, 0, len(newCards)) for _, card := range newCards { - virtualNos = append(virtualNos, card.VirtualNo) + if card.VirtualNo != "" { + virtualNos = append(virtualNos, card.VirtualNo) + } } existingVirtualNos := make(map[string]bool) existingVirtualNos, err = h.iotCardStore.ExistsByVirtualNoBatch(ctx, virtualNos) @@ -320,17 +322,7 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot finalCards := make([]model.CardItem, 0, len(newCards)) for _, card := range newCards { meta := cardMetaMap[card.ICCID] - if card.VirtualNo == "" { - result.failedItems = append(result.failedItems, model.ImportResultItem{ - Line: meta.line, - ICCID: card.ICCID, - MSISDN: meta.msisdn, - Reason: "虚拟号(virtual_no)不能为空", - }) - result.failCount++ - continue - } - if existingVirtualNos[card.VirtualNo] || batchUsedVirtualNos[card.VirtualNo] { + if card.VirtualNo != "" && (existingVirtualNos[card.VirtualNo] || batchUsedVirtualNos[card.VirtualNo]) { result.failedItems = append(result.failedItems, model.ImportResultItem{ Line: meta.line, ICCID: card.ICCID, @@ -378,8 +370,10 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot if err := txIdentifierStore.Register(ctx, card.ICCID, model.AssetTypeIotCard, iotCard.ID); err != nil { return fmt.Errorf("ICCID 已被占用: %s", card.ICCID) } - if err := txIdentifierStore.Register(ctx, card.VirtualNo, model.AssetTypeIotCard, iotCard.ID); err != nil { - return fmt.Errorf("虚拟号已被占用: %s", card.VirtualNo) + if card.VirtualNo != "" { + if err := txIdentifierStore.Register(ctx, card.VirtualNo, model.AssetTypeIotCard, iotCard.ID); err != nil { + return fmt.Errorf("虚拟号已被占用: %s", card.VirtualNo) + } } return nil }) diff --git a/migrations/000108_iot_card_virtual_no_optional.down.sql b/migrations/000108_iot_card_virtual_no_optional.down.sql new file mode 100644 index 0000000..f94f63f --- /dev/null +++ b/migrations/000108_iot_card_virtual_no_optional.down.sql @@ -0,0 +1,13 @@ +-- 回滚:将 tb_iot_card.virtual_no 恢复为 NOT NULL,并重建无条件唯一索引 + +-- 清理空值记录(如有) +DELETE FROM tb_iot_card WHERE virtual_no IS NULL OR virtual_no = ''; + +-- 删除条件唯一索引 +DROP INDEX IF EXISTS idx_iot_card_virtual_no; + +-- 将 virtual_no 列改为 NOT NULL +ALTER TABLE tb_iot_card ALTER COLUMN virtual_no SET NOT NULL; + +-- 重建无条件唯一索引 +CREATE UNIQUE INDEX idx_iot_card_virtual_no ON tb_iot_card(virtual_no) WHERE deleted_at IS NULL; diff --git a/migrations/000108_iot_card_virtual_no_optional.up.sql b/migrations/000108_iot_card_virtual_no_optional.up.sql new file mode 100644 index 0000000..97e9a7d --- /dev/null +++ b/migrations/000108_iot_card_virtual_no_optional.up.sql @@ -0,0 +1,13 @@ +-- 将 tb_iot_card.virtual_no 恢复为可选字段(可为空) +-- 背景:业务确认单卡导入时虚拟号非必填,不能强制要求 +-- 步骤:1. 删除无条件唯一索引,2. 移除 NOT NULL 约束,3. 重建条件唯一索引(允许多条空值共存) + +-- 删除当前无条件唯一索引 +DROP INDEX IF EXISTS idx_iot_card_virtual_no; + +-- 将 virtual_no 列改回允许 NULL +ALTER TABLE tb_iot_card ALTER COLUMN virtual_no DROP NOT NULL; + +-- 重建条件唯一索引(仅对非空、非空字符串的值保证唯一;允许多条空值/NULL 共存) +CREATE UNIQUE INDEX idx_iot_card_virtual_no ON tb_iot_card(virtual_no) + WHERE deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> ''; diff --git a/pkg/utils/excel.go b/pkg/utils/excel.go index b7521e2..c83db90 100644 --- a/pkg/utils/excel.go +++ b/pkg/utils/excel.go @@ -7,8 +7,6 @@ import ( "strings" "github.com/xuri/excelize/v2" - - pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors" ) // CardInfo 卡信息(ICCID + MSISDN + VirtualNo) @@ -228,10 +226,6 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) { } } - if virtualNoCol == -1 { - return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Excel 文件缺少 virtual_no 列,请使用最新模板") - } - startLine := 0 if headerSkipped { startLine = 1 @@ -292,16 +286,6 @@ func parseCardRows(rows [][]string) (*CSVParseResult, error) { continue } - if virtualNo == "" { - result.ParseErrors = append(result.ParseErrors, CSVParseError{ - Line: lineNum, - ICCID: iccid, - MSISDN: msisdn, - Reason: "虚拟号(virtual_no)不能为空", - }) - continue - } - result.Cards = append(result.Cards, CardInfo{ ICCID: iccid, MSISDN: msisdn,