package iot import ( "bytes" "context" "fmt" "io" "strconv" "strings" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/allocationunits" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/allocate" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/packagecustomer" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gtime" "github.com/xuri/excelize/v2" ) const ( allocatePrepareBatchType = consts.BatchTypePrepare allocateEventQueue = "iotlcm.app.business" allocateEventType = "card_allocate_completed" ) type sIotTSimCardAllocate struct{} type allocateRow struct { DetailID int64 RowNo int Iccid string Msisdn string Imsi string TargetDeptId int64 TargetDeptKey string TargetDeptName string Target *entity.SysDept Remark string Card *entity.TSimCardInfo Reason string } func NewIotTSimCardAllocate() *sIotTSimCardAllocate { return &sIotTSimCardAllocate{} } func init() { service.RegisterIotTSimCardAllocate(NewIotTSimCardAllocate()) } func (s *sIotTSimCardAllocate) DownloadExcelTemplate(ctx context.Context) error { book := excelize.NewFile() defer func() { _ = book.Close() }() headers := allocate.NewProcessor().Config().TemplateHeaders if err := book.SetSheetRow("Sheet1", "A1", &headers); err != nil { return err } widths := map[string]float64{ "A": 22, "B": 20, "C": 20, "D": 28, } for column, width := range widths { if err := book.SetColWidth("Sheet1", column, column, width); err != nil { return err } } if err := iot_excelize.ApplyHeaderColumnsTextStyle(book); err != nil { return err } return iot_excelize.WriteHTTP(ctx, book, "sim_card_allocate_template") } func (s *sIotTSimCardAllocate) ListTargets(ctx context.Context, in *model.TSimCardAllocateTargetListParam) (items []*model.TSimCardAllocateTargetModel, err error) { operatorDeptID := contexts.GetDeptId(ctx) if operatorDeptID <= 0 { return nil, gerror.New("当前账号未绑定部门,不能使用卡划拨") } columns := dao.SysDept.Columns() mod := handler.Model(dao.SysDept.Ctx(ctx), &handler.Option{ FilterTenant: true, FilterAuth: true, }).Where(columns.ParentId, operatorDeptID). Where(columns.Status, "0"). WhereNull(columns.DeletedAt). OrderAsc(columns.OrderNum). OrderAsc(columns.DeptId) if in != nil { if keyword := strings.TrimSpace(in.Keyword); keyword != "" { mod = mod.Where( "("+columns.DeptName+" LIKE ? OR "+columns.DeptKey+" LIKE ? OR "+columns.DeptId+" = ?)", "%"+keyword+"%", "%"+keyword+"%", keyword, ) } } var depts []*entity.SysDept if err = mod.Scan(&depts); err != nil { return nil, gerror.Wrap(err, "查询划拨目标客户失败") } items = make([]*model.TSimCardAllocateTargetModel, 0, len(depts)) for _, dept := range depts { if dept == nil || dept.DeptId <= 0 { continue } items = append(items, &model.TSimCardAllocateTargetModel{ DeptId: dept.DeptId, ParentId: dept.ParentId, DeptKey: strings.TrimSpace(dept.DeptKey), DeptName: strings.TrimSpace(dept.DeptName), DeptCategory: strings.TrimSpace(dept.DeptCategory), }) } return items, nil } func (s *sIotTSimCardAllocate) PrepareInput(ctx context.Context, in *model.TSimCardAllocatePrepareParam) (*model.TSimCardAllocateBatchModel, error) { if in == nil { in = &model.TSimCardAllocatePrepareParam{} } result, err := allocate.CreatePendingInput(ctx, in) if err != nil { return nil, err } return allocatePendingResultToModel(result), nil } func (s *sIotTSimCardAllocate) PrepareExcel(ctx context.Context, in *model.TSimCardAllocateExcelSubmitParam) (*model.TSimCardAllocateBatchModel, error) { if in == nil { in = &model.TSimCardAllocateExcelSubmitParam{} } file, err := allocateUploadFile(ctx) if err != nil { return nil, err } result, err := allocate.CreatePendingExcel(ctx, file, in) if err != nil { return nil, err } return allocatePendingResultToModel(result), nil } func allocatePendingResultToModel(result *allocate.PendingResult) *model.TSimCardAllocateBatchModel { res := &model.TSimCardAllocateBatchModel{ BatchType: consts.BatchTypeAllocate, Status: "pending", Details: []*model.TSimCardAllocateBatchDetailModel{}, BatchAsyncStatus: consts.BatchAsyncStatusPending, } if result == nil { return res } res.BatchNo = result.BatchNo res.BatchType = result.BatchType res.BatchAsyncStatus = result.BatchAsyncStatus res.TotalCount = result.Total res.ValidCount = result.ValidCount res.FailCount = result.FailCount return res } func (s *sIotTSimCardAllocate) prepare(ctx context.Context, rows []*allocateRow, targetDeptID int64, source string) *model.TSimCardAllocateBatchModel { result, err := s.createPrepareBatch(ctx, rows, targetDeptID, source) if err != nil { return &model.TSimCardAllocateBatchModel{ BatchType: allocatePrepareBatchType, Status: "failed", FailReason: err.Error(), } } return result } func (s *sIotTSimCardAllocate) createPrepareBatch(ctx context.Context, rows []*allocateRow, targetDeptID int64, source string) (*model.TSimCardAllocateBatchModel, error) { operator := allocateOperator(ctx) if operator.DeptId <= 0 { return nil, gerror.New("当前账号未绑定部门,不能使用卡划拨") } if targetDeptID <= 0 && source != "excel" { return nil, gerror.New("请选择划拨目标客户") } var target *entity.SysDept if targetDeptID > 0 { var err error target, err = s.loadTarget(ctx, operator.TenantId, operator.DeptId, targetDeptID) if err != nil { return nil, err } } if len(rows) == 0 { return nil, gerror.New("划拨卡片不能为空") } if len(rows) > 1000 { return nil, gerror.New("单次最多划拨1000张卡") } s.validateRows(ctx, operator, rows, target) batchNo, err := nextAllocateBatchNo(ctx) if err != nil { return nil, err } now := gtime.Now() validCount, failCount := allocateCounts(rows) failReason := "" if failCount > 0 { failReason = "存在校验失败数据,本次未执行" } units, err := allocationunits.Snapshot(ctx, operator.TenantId, operator.DeptId, allocateTargetDeptID(target)) if err != nil { return nil, err } batchInfo := gjson.New(g.Map{ "source": source, "targetBatchType": consts.BatchTypeAllocate, "targetBatchName": "卡划拨", "targetDeptId": allocateTargetDeptID(target), "targetDeptName": allocateTargetDeptName(target), "targetMode": allocateTargetMode(target, rows), "quantity": validCount, "totalCount": len(rows), "validCount": validCount, "failCount": failCount, "failReason": failReason, }) batchInfo = allocationunits.Merge(batchInfo, units) batchStatus := consts.BatchAsyncStatusSuccess runResult := consts.BatchRunResultSuccess if failCount > 0 { batchStatus = consts.BatchAsyncStatusFailed runResult = consts.BatchRunResultFailed } err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Data(do.BatchMain{ BatchNo: batchNo, BatchType: allocatePrepareBatchType, BatchAsyncStatus: batchStatus, BatchStatus: consts.BatchStatusNormal, SourceType: allocateSourceType(source), SourceChannel: batchmetadata.Console, ExecutionMode: batchmetadata.Bulk, TargetDeptId: allocateTargetDeptID(target), DeptId: operator.DeptId, TotalCount: len(rows), ValidCount: validCount, SuccessCount: 0, FailCount: failCount, RunResult: runResult, FailReason: failReason, BatchInfo: batchInfo, StartTime: now, EndTime: now, TenantId: operator.TenantId, CreatedDept: operator.DeptId, CreatedBy: operator.UserId, CreatedAt: now, UpdatedBy: operator.UserId, UpdatedAt: now, }).Insert() if err != nil { return gerror.Wrap(err, "写入划拨预处理批次失败") } details := make([]do.BatchDetail, 0, len(rows)) for _, row := range rows { status := consts.BatchDetailStatusSuccess if row.Reason != "" { status = consts.BatchDetailStatusFailed } details = append(details, do.BatchDetail{ BatchNo: batchNo, BatchType: allocatePrepareBatchType, DetailStatus: status, BlankOutStatus: consts.BlankOutStatusNo, RowNo: row.RowNo, ItemId: row.Iccid, ErrorReason: row.Reason, Snapshot: allocationunits.Merge(allocateRowSnapshot(row, target), gjson.New(map[string]any{"sourceDeptName": units.Get("sourceDeptName").String(), "sourceDeptKey": units.Get("sourceDeptKey").String()})), TenantId: operator.TenantId, CreatedDept: operator.DeptId, CreatedBy: operator.UserId, CreatedAt: now, UpdatedBy: operator.UserId, UpdatedAt: now, }) } _, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()).Data(details).Insert() return gerror.Wrap(err, "写入划拨预处理明细失败") }) if err != nil { return nil, err } return buildAllocateBatchModel(batchNo, allocatePrepareBatchType, target, rows, "prepared", failReason), nil } func (s *sIotTSimCardAllocate) SubmitBatch(ctx context.Context, in *model.TSimCardAllocateBatchSubmitParam) (*model.TSimCardAllocateBatchModel, error) { if in == nil { in = &model.TSimCardAllocateBatchSubmitParam{} } operator := allocateOperator(ctx) batchNo := strings.TrimSpace(in.BatchNo) if batchNo == "" { return nil, gerror.New("预处理批次号不能为空") } var result *model.TSimCardAllocateBatchModel var eventTarget *entity.SysDept var syncDeptIDs []int64 err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var err error var batch *entity.BatchMain if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(dao.BatchMain.Columns().BatchNo, batchNo). Where(dao.BatchMain.Columns().BatchType, allocatePrepareBatchType). Where(dao.BatchMain.Columns().TenantId, operator.TenantId). Where(dao.BatchMain.Columns().CreatedBy, operator.UserId). LockUpdate().Scan(&batch); err != nil { return gerror.Wrap(err, "查询划拨预处理批次失败") } if batch == nil || batch.Id == 0 { return gerror.New("划拨预处理批次不存在或无权限确认") } if batch.BatchInfo == nil || batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeAllocate { return gerror.New("批次不是卡划拨预处理批次") } if batch.BatchAsyncStatus != consts.BatchAsyncStatusSuccess || batch.FailCount > 0 || batch.RunResult != consts.BatchRunResultSuccess { return gerror.New("划拨预处理批次存在校验失败数据,请重新校验") } if batch.TargetDeptId > 0 && in.TargetDeptId > 0 && in.TargetDeptId != batch.TargetDeptId { return gerror.New("提交目标客户与预处理批次不一致,请重新校验") } var details []*entity.BatchDetail if err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, allocatePrepareBatchType). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess). OrderAsc(dao.BatchDetail.Columns().RowNo). Scan(&details); err != nil { return gerror.Wrap(err, "查询划拨预处理明细失败") } if len(details) == 0 { return gerror.New("划拨预处理批次没有可确认的卡片") } rows := make([]*allocateRow, 0, len(details)) targets := make(map[int64]*entity.SysDept) for _, detail := range details { if detail == nil || strings.TrimSpace(detail.ItemId) == "" { return gerror.New("划拨预处理明细缺少ICCID") } if detail.Snapshot == nil { return gerror.Newf("卡%s缺少预校验快照,请重新校验", detail.ItemId) } cardID := detail.Snapshot.Get("cardId").Int64() if cardID <= 0 { return gerror.Newf("卡%s缺少有效卡片ID,请重新校验", detail.ItemId) } targetDeptID := detail.Snapshot.Get("targetDeptId").Int64() if targetDeptID <= 0 { targetDeptID = batch.TargetDeptId } if targetDeptID <= 0 { return gerror.Newf("卡%s缺少目标客户快照,请重新校验", detail.ItemId) } target := targets[targetDeptID] if target == nil { target, err = s.loadTargetTx(ctx, tx, operator.TenantId, operator.DeptId, targetDeptID) if err != nil { return err } targets[targetDeptID] = target } var card *entity.TSimCardInfo if err = tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()). Where(dao.TSimCardInfo.Columns().Id, cardID). Where(dao.TSimCardInfo.Columns().TenantId, operator.TenantId). LockUpdate().Scan(&card); err != nil { return err } if card == nil || card.Id == 0 { return gerror.Newf("卡%s不存在,不能划拨", detail.ItemId) } if card.DeptId != operator.DeptId { return gerror.Newf("卡%s归属已变更,请重新校验", card.Iccid) } if reason := validateAllocateCard(card, target.DeptId); reason != "" { return gerror.New(reason) } rows = append(rows, &allocateRow{ DetailID: detail.Id, RowNo: detail.RowNo, Iccid: strings.TrimSpace(card.Iccid), Msisdn: strings.TrimSpace(card.Msisdn), Imsi: strings.TrimSpace(card.Imsi), TargetDeptId: target.DeptId, TargetDeptKey: strings.TrimSpace(target.DeptKey), TargetDeptName: strings.TrimSpace(target.DeptName), Target: target, Card: card, }) } eventTarget = singleAllocateTarget(rows) syncDeptIDs = allocatePackageSyncDeptIDs(operator, rows, eventTarget) now := gtime.Now() for _, row := range rows { target := row.Target if target == nil || target.DeptId <= 0 { return gerror.Newf("卡%s缺少目标客户快照,请重新校验", row.Iccid) } affected, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()). Where(dao.TSimCardInfo.Columns().Id, row.Card.Id). Where(dao.TSimCardInfo.Columns().DeptId, operator.DeptId). Data(do.TSimCardInfo{ DeptId: target.DeptId, OutboundBatchNo: batchNo, AllocationBatchNo: batchNo, UpdatedBy: operator.UserId, UpdatedAt: now, }).Update() if err != nil { return gerror.Wrapf(err, "更新卡%s归属失败", row.Iccid) } if count, err := affected.RowsAffected(); err != nil || count != 1 { return gerror.Newf("卡%s归属已变更,请重新校验", row.Iccid) } if _, err = tx.Ctx(ctx).Model(dao.BatchDetail.Table()). Where(dao.BatchDetail.Columns().Id, row.DetailID). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Data(do.BatchDetail{ BatchType: consts.BatchTypeAllocate, Snapshot: allocateSubmitSnapshot(row, target, batchNo), UpdatedBy: operator.UserId, UpdatedAt: now, }).Update(); err != nil { return gerror.Wrap(err, "更新划拨批次明细失败") } } batchInfo := gjson.New(map[string]any{ "source": "console", "targetBatchType": consts.BatchTypeAllocate, "targetBatchName": "卡划拨", "targetDeptId": allocateTargetDeptID(eventTarget), "targetDeptName": allocateTargetDeptName(eventTarget), "targetMode": allocateTargetMode(eventTarget, rows), "quantity": len(rows), "remark": strings.TrimSpace(in.Remark), "allocateTime": now, }) if _, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(dao.BatchMain.Columns().Id, batch.Id). Where(dao.BatchMain.Columns().BatchType, allocatePrepareBatchType). Data(do.BatchMain{ BatchType: consts.BatchTypeAllocate, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, TargetDeptId: allocateTargetDeptID(eventTarget), ValidCount: len(rows), SuccessCount: len(rows), FailCount: 0, RunResult: consts.BatchRunResultSuccess, FailReason: "", BatchInfo: batchInfo, EndTime: now, UpdatedBy: operator.UserId, UpdatedAt: now, }).Update(); err != nil { return gerror.Wrap(err, "更新划拨批次失败") } result = buildAllocateBatchModel(batchNo, consts.BatchTypeAllocate, eventTarget, rows, "success", "") return nil }) if err != nil { return nil, err } publishAllocateEvent(ctx, operator, batchNo, eventTarget) packagecustomer.PublishSyncForDeptsSafe(ctx, operator.TenantId, operator.UserId, syncDeptIDs, "allocate", batchNo) return result, nil } func (s *sIotTSimCardAllocate) loadTarget(ctx context.Context, tenantID string, sourceDeptID, targetDeptID int64) (*entity.SysDept, error) { return s.loadTargetTx(ctx, nil, tenantID, sourceDeptID, targetDeptID) } func (s *sIotTSimCardAllocate) loadTargetTx(ctx context.Context, tx gdb.TX, tenantID string, sourceDeptID, targetDeptID int64) (*entity.SysDept, error) { mod := dao.SysDept.Ctx(ctx) if tx != nil { mod = tx.Ctx(ctx).Model(dao.SysDept.Table()) } columns := dao.SysDept.Columns() var target *entity.SysDept err := mod.Where(columns.TenantId, tenantID). Where(columns.DeptId, targetDeptID). Where(columns.ParentId, sourceDeptID). Where(columns.Status, "0"). WhereNull(columns.DeletedAt). Limit(1).Scan(&target) if err != nil { return nil, gerror.Wrap(err, "查询划拨目标客户失败") } if target == nil || target.DeptId == 0 { return nil, gerror.New("划拨目标客户不存在或不在当前部门的下级范围内") } return target, nil } func (s *sIotTSimCardAllocate) validateRows(ctx context.Context, operator allocateOperatorContext, rows []*allocateRow, fixedTarget *entity.SysDept) { seen := make(map[string]struct{}, len(rows)) columns := dao.TSimCardInfo.Columns() for _, row := range rows { if fixedTarget != nil { row.TargetDeptId = fixedTarget.DeptId row.TargetDeptKey = strings.TrimSpace(fixedTarget.DeptKey) row.TargetDeptName = strings.TrimSpace(fixedTarget.DeptName) row.Target = fixedTarget } row.Iccid = strings.TrimSpace(row.Iccid) if row.Iccid == "" { row.Reason = appendAllocateReason(row.Reason, "ICCID不能为空") continue } if _, exists := seen[row.Iccid]; exists { row.Reason = appendAllocateReason(row.Reason, "ICCID重复") continue } seen[row.Iccid] = struct{}{} var card *entity.TSimCardInfo err := handler.Model(dao.TSimCardInfo.Ctx(ctx), &handler.Option{ FilterTenant: true, FilterAuth: true, }).Where(columns.Iccid, row.Iccid).Limit(1).Scan(&card) if err != nil || card == nil || card.Id == 0 { row.Reason = appendAllocateReason(row.Reason, "ICCID不存在或不在当前操作员授权范围内") continue } row.Card = card row.Msisdn = strings.TrimSpace(card.Msisdn) row.Imsi = strings.TrimSpace(card.Imsi) if card.DeptId != operator.DeptId { row.Reason = appendAllocateReason(row.Reason, "卡片当前归属不在当前部门,不能划拨") continue } if row.TargetDeptId <= 0 { row.Reason = appendAllocateReason(row.Reason, "目标客户不能为空") continue } if reason := validateAllocateCard(card, row.TargetDeptId); reason != "" { row.Reason = appendAllocateReason(row.Reason, reason) } } } func validateAllocateCard(card *entity.TSimCardInfo, targetDeptID int64) string { if card == nil { return "卡片信息不存在" } label := strings.TrimSpace(card.Iccid) if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped { return fmt.Sprintf("卡%s当前为停机状态,不能划拨", label) } if card.PackageStatus == consts.PackageStatusExpired || (card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() && card.FlowPkgEndAt.Before(gtime.Now())) { return fmt.Sprintf("卡%s套餐已过期,不能划拨", label) } if card.DeptId == targetDeptID { return fmt.Sprintf("卡%s已属于目标客户,无需重复划拨", label) } return "" } func allocateCounts(rows []*allocateRow) (valid, failed int) { for _, row := range rows { if row != nil && row.Reason == "" { valid++ } else { failed++ } } return } func buildAllocateBatchModel(batchNo string, batchType int, target *entity.SysDept, rows []*allocateRow, status, failReason string) *model.TSimCardAllocateBatchModel { valid, failed := allocateCounts(rows) result := &model.TSimCardAllocateBatchModel{ BatchNo: batchNo, BatchType: batchType, TotalCount: len(rows), ValidCount: valid, FailCount: failed, FailReason: failReason, Status: status, Details: make([]*model.TSimCardAllocateBatchDetailModel, 0, len(rows)), } if target != nil { result.Target = &model.TSimCardAllocateTargetModel{ DeptId: target.DeptId, ParentId: target.ParentId, DeptKey: strings.TrimSpace(target.DeptKey), DeptName: strings.TrimSpace(target.DeptName), DeptCategory: strings.TrimSpace(target.DeptCategory), } } for _, row := range rows { if row == nil { continue } itemStatus := "success" if row.Reason != "" { itemStatus = "failed" } result.Details = append(result.Details, &model.TSimCardAllocateBatchDetailModel{ RowNo: row.RowNo, ItemId: row.Iccid, TargetDeptId: row.TargetDeptId, TargetDeptKey: row.TargetDeptKey, TargetDeptName: row.TargetDeptName, Status: itemStatus, ErrorReason: row.Reason, }) } return result } func allocateRowSnapshot(row *allocateRow, target *entity.SysDept) *gjson.Json { targetDeptID := int64(0) targetDeptKey := "" targetDeptName := "" if row != nil { targetDeptID = row.TargetDeptId targetDeptKey = strings.TrimSpace(row.TargetDeptKey) targetDeptName = strings.TrimSpace(row.TargetDeptName) } if targetDeptID <= 0 && target != nil { targetDeptID = target.DeptId targetDeptKey = strings.TrimSpace(target.DeptKey) targetDeptName = strings.TrimSpace(target.DeptName) } return gjson.New(g.Map{ "ICCID": row.Iccid, "MSISDN": row.Msisdn, "IMSI": row.Imsi, "cardId": allocateCardID(row), "sourceDeptId": allocateCardDeptID(row), "targetDeptId": targetDeptID, "targetDeptKey": targetDeptKey, "targetDeptName": targetDeptName, "remark": row.Remark, "errorReason": row.Reason, }) } func allocateSubmitSnapshot(row *allocateRow, target *entity.SysDept, batchNo string) *gjson.Json { snapshot := allocateRowSnapshot(row, target).Map() snapshot["allocateBatchNo"] = batchNo return gjson.New(snapshot) } func allocateCardID(row *allocateRow) int64 { if row != nil && row.Card != nil { return row.Card.Id } return 0 } func allocateCardDeptID(row *allocateRow) int64 { if row != nil && row.Card != nil { return row.Card.DeptId } return 0 } func allocateTargetDeptID(target *entity.SysDept) int64 { if target == nil { return 0 } return target.DeptId } func allocateTargetDeptName(target *entity.SysDept) string { if target == nil { return "" } return strings.TrimSpace(target.DeptName) } func allocateTargetMode(target *entity.SysDept, rows []*allocateRow) string { if target != nil { return "single" } seen := make(map[int64]struct{}) for _, row := range rows { if row == nil || row.TargetDeptId <= 0 { continue } seen[row.TargetDeptId] = struct{}{} } if len(seen) > 1 { return "multiple" } return "row" } func singleAllocateTarget(rows []*allocateRow) *entity.SysDept { var target *entity.SysDept for _, row := range rows { if row == nil || row.Target == nil || row.Target.DeptId <= 0 { continue } if target == nil { target = row.Target continue } if target.DeptId != row.Target.DeptId { return nil } } return target } type allocateOperatorContext struct { TenantId string DeptId int64 UserId int64 } func allocateOperator(ctx context.Context) allocateOperatorContext { return allocateOperatorContext{ TenantId: contexts.GetTenantId(ctx), DeptId: contexts.GetDeptId(ctx), UserId: contexts.GetUserId(ctx), } } func nextAllocateBatchNo(ctx context.Context) (string, error) { return batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixAllocate) } func publishAllocateEvent(ctx context.Context, operator allocateOperatorContext, batchNo string, target *entity.SysDept) { payload := g.Map{ "eventType": allocateEventType, "batchNo": batchNo, "batchType": consts.BatchTypeAllocate, "tenantId": operator.TenantId, "userId": operator.UserId, "deptId": operator.DeptId, "trigger": "allocate_submit", "createdAt": gtime.Now().Format("Y-m-d H:i:s"), } if target != nil { payload["targetDeptId"] = target.DeptId payload["targetDeptName"] = target.DeptName } if err := rabbitmq.Publish(ctx, allocateEventQueue, payload); err != nil { g.Log().Warningf(ctx, "卡划拨业务事件发布失败,batchNo:%s, queue:%s, err:%+v", batchNo, allocateEventQueue, err) return } g.Log().Infof(ctx, "卡划拨业务事件发布成功,batchNo:%s, queue:%s", batchNo, allocateEventQueue) } func allocatePackageSyncDeptIDs(operator allocateOperatorContext, rows []*allocateRow, target *entity.SysDept) []int64 { deptIds := []int64{operator.DeptId} if target != nil && target.DeptId > 0 { deptIds = append(deptIds, target.DeptId) } for _, row := range rows { if row == nil { continue } if row.TargetDeptId > 0 { deptIds = append(deptIds, row.TargetDeptId) } } return deptIds } func parseAllocateInput(input string) ([]*allocateRow, error) { input = strings.TrimSpace(input) if input == "" { return nil, gerror.New("批量输入内容不能为空") } lines := strings.FieldsFunc(input, func(r rune) bool { return r == '\n' || r == '\r' }) rows := make([]*allocateRow, 0, len(lines)) for index, line := range lines { fields := strings.FieldsFunc(line, func(r rune) bool { return r == '\t' || r == ',' || r == ',' || r == ';' || r == ';' || r == ' ' }) row := &allocateRow{RowNo: index + 1} switch len(fields) { case 1: row.Iccid = strings.TrimSpace(fields[0]) case 3: row.Iccid = strings.TrimSpace(fields[0]) row.Msisdn = strings.TrimSpace(fields[1]) row.Imsi = strings.TrimSpace(fields[2]) default: row.Reason = "格式错误,应为 ICCID 或 ICCID MSISDN IMSI" } rows = append(rows, row) } return rows, nil } func (s *sIotTSimCardAllocate) resolveExcelTargets(ctx context.Context, rows []*allocateRow, fallbackTargetDeptID int64) (int64, error) { operator := allocateOperator(ctx) if operator.DeptId <= 0 { return 0, gerror.New("当前账号未绑定部门,不能使用卡划拨") } var fallbackTarget *entity.SysDept var err error if fallbackTargetDeptID > 0 { fallbackTarget, err = s.loadTarget(ctx, operator.TenantId, operator.DeptId, fallbackTargetDeptID) if err != nil { return 0, err } } targets := make(map[int64]*entity.SysDept) for _, row := range rows { if row == nil { continue } if row.TargetDeptId <= 0 && row.TargetDeptKey == "" && row.TargetDeptName == "" && fallbackTarget != nil { row.TargetDeptId = fallbackTarget.DeptId row.TargetDeptKey = strings.TrimSpace(fallbackTarget.DeptKey) row.TargetDeptName = strings.TrimSpace(fallbackTarget.DeptName) row.Target = fallbackTarget targets[fallbackTarget.DeptId] = fallbackTarget continue } target, err := s.resolveExcelTarget(ctx, operator, row) if err != nil { row.Reason = appendAllocateReason(row.Reason, err.Error()) continue } row.TargetDeptId = target.DeptId row.TargetDeptKey = strings.TrimSpace(target.DeptKey) row.TargetDeptName = strings.TrimSpace(target.DeptName) row.Target = target targets[target.DeptId] = target } if len(targets) == 1 { for targetDeptID := range targets { return targetDeptID, nil } } return 0, nil } func (s *sIotTSimCardAllocate) resolveExcelTarget(ctx context.Context, operator allocateOperatorContext, row *allocateRow) (*entity.SysDept, error) { if row == nil { return nil, gerror.New("划拨Excel行为空") } if row.TargetDeptId > 0 { return s.loadTarget(ctx, operator.TenantId, operator.DeptId, row.TargetDeptId) } if row.TargetDeptKey != "" { return s.loadTargetByColumn(ctx, nil, operator.TenantId, operator.DeptId, dao.SysDept.Columns().DeptKey, row.TargetDeptKey) } if row.TargetDeptName != "" { return s.loadTargetByColumn(ctx, nil, operator.TenantId, operator.DeptId, dao.SysDept.Columns().DeptName, row.TargetDeptName) } return nil, gerror.New("目标客户不能为空") } func (s *sIotTSimCardAllocate) loadTargetByColumn(ctx context.Context, tx gdb.TX, tenantID string, sourceDeptID int64, column string, value string) (*entity.SysDept, error) { value = strings.TrimSpace(value) if value == "" { return nil, gerror.New("目标客户不能为空") } mod := dao.SysDept.Ctx(ctx) if tx != nil { mod = tx.Ctx(ctx).Model(dao.SysDept.Table()) } columns := dao.SysDept.Columns() var targets []*entity.SysDept err := mod.Where(columns.TenantId, tenantID). Where(columns.ParentId, sourceDeptID). Where(columns.Status, "0"). WhereNull(columns.DeletedAt). Where(column, value). OrderAsc(columns.DeptId). Limit(2). Scan(&targets) if err != nil { return nil, gerror.Wrap(err, "查询划拨目标客户失败") } if len(targets) == 0 { return nil, gerror.New("划拨目标客户不存在或不在当前部门的下级范围内") } if len(targets) > 1 { return nil, gerror.New("目标客户名称不唯一,请改用批量输入方式选择目标客户") } return targets[0], nil } func parseAllocateExcel(file *ghttp.UploadFile) ([]*allocateRow, error) { reader, err := file.Open() if err != nil { return nil, gerror.Wrap(err, "打开划拨Excel失败") } defer reader.Close() content, err := io.ReadAll(reader) if err != nil { return nil, gerror.Wrap(err, "读取划拨Excel失败") } book, err := excelize.OpenReader(bytes.NewReader(content)) if err != nil { return nil, gerror.Wrap(err, "解析划拨Excel失败") } defer book.Close() sheet := book.GetSheetName(0) rows, err := book.GetRows(sheet) if err != nil { return nil, gerror.Wrap(err, "读取划拨Excel行失败") } if len(rows) < 2 { return nil, gerror.New("划拨Excel没有可处理的数据") } headers := make(map[string]int) for index, header := range rows[0] { headers[normalizeAllocateHeader(header)] = index } iccidIndex, ok := headers[normalizeAllocateHeader("ICCID")] if !ok { return nil, gerror.New("划拨Excel必须包含ICCID列") } if !hasAllocateHeader(headers, "目标客户名称", "TARGETDEPTNAME", "TARGET_CUSTOMER_NAME") { return nil, gerror.New("划拨Excel必须包含目标客户名称列") } result := make([]*allocateRow, 0, len(rows)-1) for index, values := range rows[1:] { row := &allocateRow{ RowNo: index + 2, Iccid: allocateHeaderValue(headers, values, "ICCID"), Msisdn: allocateHeaderValue(headers, values, "MSISDN"), Imsi: allocateHeaderValue(headers, values, "IMSI"), TargetDeptName: allocateHeaderValue(headers, values, "目标客户名称", "客户名称", "TARGETDEPTNAME", "TARGET_CUSTOMER_NAME"), Remark: allocateHeaderValue(headers, values, "划拨备注", "备注", "REMARK"), } if iccidIndex >= len(values) { row.Reason = appendAllocateReason(row.Reason, "ICCID不能为空") result = append(result, row) continue } if row.TargetDeptId <= 0 && row.TargetDeptKey == "" && row.TargetDeptName == "" { row.Reason = appendAllocateReason(row.Reason, "目标客户不能为空") } result = append(result, row) } return result, nil } func normalizeAllocateHeader(value string) string { value = strings.ToUpper(strings.TrimSpace(value)) replacer := strings.NewReplacer(" ", "", "_", "", "-", "", " ", "") return replacer.Replace(value) } func hasAllocateHeader(headers map[string]int, aliases ...string) bool { for _, alias := range aliases { if _, ok := headers[normalizeAllocateHeader(alias)]; ok { return true } } return false } func allocateHeaderValue(headers map[string]int, values []string, aliases ...string) string { for _, alias := range aliases { column, exists := headers[normalizeAllocateHeader(alias)] if !exists || column >= len(values) { continue } if value := strings.TrimSpace(values[column]); value != "" { return value } } return "" } func parseAllocateTargetDeptID(value string) (int64, error) { value = strings.TrimSpace(value) if value == "" { return 0, nil } parsed, err := strconv.ParseInt(value, 10, 64) if err != nil || parsed <= 0 { return 0, gerror.New("目标客户ID格式错误") } return parsed, nil } func appendAllocateReason(current string, reason string) string { current = strings.TrimSpace(current) reason = strings.TrimSpace(reason) if current == "" { return reason } if reason == "" { return current } return current + ";" + reason } func allocateUploadFile(ctx context.Context) (*ghttp.UploadFile, error) { request := g.RequestFromCtx(ctx) if request == nil { return nil, gerror.New("当前上下文不是HTTP请求") } file := request.GetUploadFile("file") if file == nil { return nil, gerror.New("请选择上传文件") } return file, nil } func allocateSourceType(source string) int { if strings.TrimSpace(source) == "excel" { return consts.BatchSourceTypeFile } return consts.BatchSourceTypeBatchInput }