package app import ( "context" "fmt" "strings" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "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/os/gtime" ) type appAllocateBatchContext struct { Target *entity.SysDept Cards []*appAllocateCardContext TotalCount int ValidCount int FailCount int FailReason string } type appAllocateCardContext struct { Card *appCardQueryRow ItemId string RowNo int SourceDeptId int64 ErrorReason string } // AllocateTargets 查询当前企业用户可划拨的目标客户。 func (s *sAppCard) AllocateTargets(ctx context.Context, param *model.AppCardAllocateTargetListParam) (items []*model.AppCardAllocateTargetModel, total int, err error) { if param == nil { param = &model.AppCardAllocateTargetListParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, 0, err } if s.isTerminalUser(ctx, user) { return nil, 0, gerror.New("终端用户不能使用卡划拨功能") } if user.DeptId <= 0 { return nil, 0, gerror.New("当前账号未绑定企业,不能使用卡划拨功能") } if param.Page <= 0 { param.Page = 1 } if param.PageSize <= 0 { param.PageSize = 50 } if param.PageSize > 1000 { param.PageSize = 1000 } var rows []*entity.SysDept rows, total, err = s.loadAllocateTargetDepts(ctx, user, strings.TrimSpace(param.Keyword), param.Page, param.PageSize) if err != nil { return nil, 0, gerror.Wrap(err, "查询可划拨目标客户失败") } items = make([]*model.AppCardAllocateTargetModel, 0, len(rows)) for _, row := range rows { items = append(items, appAllocateTargetModel(row, user.DeptId)) } return items, total, nil } func (s *sAppCard) loadAllocateTargetDepts(ctx context.Context, user *entity.SysUser, keyword string, page int, pageSize int) ([]*entity.SysDept, int, error) { if user == nil || user.DeptId <= 0 { return []*entity.SysDept{}, 0, nil } if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 50 } var children []*entity.SysDept err := dao.SysDept.Ctx(ctx). Where(dao.SysDept.Columns().TenantId, user.TenantId). Where(dao.SysDept.Columns().ParentId, user.DeptId). WhereNull(dao.SysDept.Columns().DeletedAt). OrderAsc(dao.SysDept.Columns().OrderNum). OrderAsc(dao.SysDept.Columns().DeptId). Scan(&children) if err != nil { return nil, 0, err } rows := make([]*entity.SysDept, 0, len(children)) for _, child := range children { if child == nil || child.DeptId <= 0 || !appAllocateDeptMatchesKeyword(child, keyword) { continue } rows = append(rows, child) } rows, err = appFilterDeptsByEnterpriseDataScope(ctx, user, rows) if err != nil { return nil, 0, err } total := len(rows) start := (page - 1) * pageSize if start >= total { return []*entity.SysDept{}, total, nil } end := start + pageSize if end > total { end = total } return rows[start:end], total, nil } func appAllocateDeptMatchesKeyword(dept *entity.SysDept, keyword string) bool { keyword = strings.TrimSpace(keyword) if keyword == "" { return true } if dept == nil { return false } return strings.Contains(strings.TrimSpace(dept.DeptName), keyword) || strings.Contains(strings.TrimSpace(dept.DeptKey), keyword) || strings.Contains(fmt.Sprintf("%d", dept.DeptId), keyword) } // AllocateBatchPrepare 校验待划拨卡片,并写入划拨预处理批次。 func (s *sAppCard) AllocateBatchPrepare(ctx context.Context, param *model.AppCardAllocateBatchPrepareParam) (res *model.AppCardAllocateBatchModel, err error) { if param == nil { param = &model.AppCardAllocateBatchPrepareParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } batch, err := s.buildAllocateBatchContext(ctx, user, param.CardValues, param.TargetDeptId) if err != nil { return nil, err } ctx = appContextWithUser(ctx, user) processor := newAppAllocateBatchProcessor(batch) rows := appAllocateBatchRows(batch) req := batchimport.SubmitRequest{ BizParams: param, SourceType: consts.BatchSourceTypeBatchSelect, BatchType: consts.BatchTypePrepare, FailureBatchType: consts.BatchTypeAllocate, SkipExecute: true, TargetDeptId: batch.Target.DeptId, Operator: batchimport.OperatorContext{ TenantId: user.TenantId, DeptId: user.DeptId, UserId: user.UserId, }, } result, err := batchimport.RunRows(ctx, req, processor, rows, "miniapp-allocate-prepare") if err != nil { return nil, gerror.Wrap(err, "写入划拨预处理批次失败") } status := "prepared" batchNo := "" if result != nil { batchNo = result.BatchNo } if batch.FailCount > 0 || result == nil || result.ImportStatus != consts.ImportStatusSuccess { status = "failed" } return s.buildAllocateBatchModel(ctx, user, batch, batchNo, "", consts.BatchTypePrepare, status), nil } // AllocateBatchSubmit 确认划拨预处理批次,并更新卡片归属;划拨后原企业的可见和办理权限仍保留。 func (s *sAppCard) AllocateBatchSubmit(ctx context.Context, param *model.AppCardAllocateBatchSubmitParam) (res *model.AppCardAllocateBatchModel, err error) { if param == nil { param = &model.AppCardAllocateBatchSubmitParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } if s.isTerminalUser(ctx, user) { return nil, gerror.New("终端用户不能使用卡划拨功能") } now := gtime.Now() var batch *appAllocateBatchContext var sourceBatchNo string batchNo := strings.TrimSpace(param.BatchNo) err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { prepareBatch, err := s.lockAppAllocatePrepareBatch(ctx, tx, user, batchNo) if err != nil { return err } sourceBatchNo = appAllocateSourceBatchNoFromBatch(prepareBatch) targetDeptId := prepareBatch.TargetDeptId if param.TargetDeptId > 0 && param.TargetDeptId != targetDeptId { return gerror.New("提交目标客户与预处理批次不一致,请重新校验") } target, err := s.loadAllocateTarget(ctx, user, targetDeptId) if err != nil { return err } details, err := s.loadAppAllocatePrepareBatchDetails(ctx, tx, prepareBatch.BatchNo) if err != nil { return err } if len(details) == 0 { return gerror.New("划拨预处理批次没有可确认的卡") } cardValues := make([]string, 0, len(details)) for _, detail := range details { cardValues = append(cardValues, strings.TrimSpace(detail.ItemId)) } if len(param.CardValues) > 0 && !appSameStringSet(appNormalizeAllocateCardValues(param.CardValues), cardValues) { return gerror.New("提交卡片与预处理批次不一致,请重新校验") } batch = &appAllocateBatchContext{ Target: target, Cards: make([]*appAllocateCardContext, 0, len(details)), } for _, detail := range details { card, err := s.lockAllocateCardForSubmit(ctx, tx, user, detail, target.DeptId) if err != nil { return err } sourceDeptId := card.DeptId if detail.Snapshot != nil && detail.Snapshot.Get("sourceDeptId").Int64() > 0 { sourceDeptId = detail.Snapshot.Get("sourceDeptId").Int64() } batch.Cards = append(batch.Cards, &appAllocateCardContext{ Card: card, ItemId: card.Iccid, RowNo: detail.RowNo, SourceDeptId: sourceDeptId, }) } batch.refreshCounts() if err = s.updateAllocateCards(ctx, tx, user, batch, prepareBatch.BatchNo, now); err != nil { return err } if err = s.markAppAllocatePrepareBatchConfirmed(ctx, tx, user, prepareBatch, batch, sourceBatchNo, strings.TrimSpace(param.Remark), now); err != nil { return err } return nil }) if err != nil { return nil, err } if batch == nil { return nil, gerror.New("划拨提交结果不完整") } res = s.buildAllocateBatchModel(ctx, user, batch, batchNo, sourceBatchNo, consts.BatchTypeAllocate, "success") _ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{ EventType: appAsyncEventAllocateCompleted, BatchNo: batchNo, BatchType: consts.BatchTypeAllocate, TenantId: user.TenantId, UserId: user.UserId, DeptId: user.DeptId, Trigger: "allocate_submit", }) return res, nil } func (s *sAppCard) buildAllocateBatchContext(ctx context.Context, user *entity.SysUser, cardValues []string, targetDeptId int64) (*appAllocateBatchContext, error) { if s.isTerminalUser(ctx, user) { return nil, gerror.New("终端用户不能使用卡划拨功能") } if user.DeptId <= 0 { return nil, gerror.New("当前账号未绑定企业,不能使用卡划拨功能") } values := appNormalizeAllocateCardValues(cardValues) if len(values) == 0 { return nil, gerror.New("请选择需要划拨的卡") } if len(values) > 100 { return nil, gerror.New("单次批量划拨最多支持100张卡") } target, err := s.loadAllocateTarget(ctx, user, targetDeptId) if err != nil { return nil, err } batch := &appAllocateBatchContext{ Target: target, Cards: make([]*appAllocateCardContext, 0, len(values)), TotalCount: len(values), } seenIccid := make(map[string]struct{}, len(values)) for index, value := range values { item := &appAllocateCardContext{ ItemId: strings.TrimSpace(value), RowNo: index + 2, } card, err := s.findVisibleCard(ctx, user, value) if err != nil { return nil, err } if card == nil { item.ErrorReason = fmt.Sprintf("第%d张卡不存在或无权划拨:%s", index+1, value) batch.Cards = append(batch.Cards, item) continue } item.Card = card item.ItemId = strings.TrimSpace(card.Iccid) item.SourceDeptId = card.DeptId iccid := strings.TrimSpace(card.Iccid) if _, exists := seenIccid[iccid]; exists { item.ErrorReason = fmt.Sprintf("批量划拨卡重复:%s", iccid) batch.Cards = append(batch.Cards, item) continue } seenIccid[iccid] = struct{}{} if err = s.ensureAllocateCardOwnedByOperator(ctx, nil, user, card); err != nil { item.ErrorReason = err.Error() } else if err = s.validateAllocateCardBusinessRules(ctx, nil, card); err != nil { item.ErrorReason = err.Error() } else if card.DeptId == target.DeptId { item.ErrorReason = fmt.Sprintf("卡%s已属于目标客户,无需重复划拨", appFirstNotEmpty(card.Iccid, value)) } batch.Cards = append(batch.Cards, item) } batch.refreshCounts() return batch, nil } func (batch *appAllocateBatchContext) refreshCounts() { if batch == nil { return } batch.TotalCount = len(batch.Cards) batch.ValidCount = 0 batch.FailCount = 0 batch.FailReason = "" for _, item := range batch.Cards { if item == nil { batch.FailCount++ continue } if strings.TrimSpace(item.ErrorReason) != "" { batch.FailCount++ continue } batch.ValidCount++ } if batch.FailCount > 0 { batch.FailReason = "存在校验失败数据,本次未写入" } } func (item *appAllocateCardContext) allocateItemId() string { if item == nil { return "" } if item.Card != nil && strings.TrimSpace(item.Card.Iccid) != "" { return strings.TrimSpace(item.Card.Iccid) } return strings.TrimSpace(item.ItemId) } func (s *sAppCard) insertAppAllocatePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, now *gtime.Time) error { batch.refreshCounts() batchStatus := consts.BatchAsyncStatusSuccess runResult := consts.BatchRunResultSuccess successCount := batch.ValidCount if batch.FailCount > 0 { batchStatus = consts.BatchAsyncStatusFailed runResult = consts.BatchRunResultFailed successCount = 0 } _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{ BatchNo: batchNo, BatchType: consts.BatchTypePrepare, BatchStatus: batchStatus, BatchAsyncStatus: batchStatus, SourceType: consts.BatchSourceTypeBatchSelect, SourceBatchNo: batchNo, IsCrossDept: 0, TargetDeptId: batch.Target.DeptId, DeptId: user.DeptId, TotalCount: batch.TotalCount, ValidCount: batch.ValidCount, SuccessCount: successCount, FailCount: batch.FailCount, RunResult: runResult, FailReason: batch.FailReason, BatchInfo: appAllocatePrepareBatchInfo(batch, batchNo), StartTime: now, EndTime: now, TenantId: user.TenantId, CreatedDept: user.DeptId, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).Insert() if err != nil { return gerror.Wrap(err, "写入划拨预处理主表失败") } details := make([]do.BatchDetail, 0, len(batch.Cards)) for _, item := range batch.Cards { detailStatus := consts.BatchDetailStatusSuccess if strings.TrimSpace(item.ErrorReason) != "" { detailStatus = consts.BatchDetailStatusFailed } details = append(details, do.BatchDetail{ BatchNo: batchNo, BatchType: consts.BatchTypePrepare, DetailStatus: detailStatus, BlankOutStatus: consts.BlankOutStatusNo, RowNo: item.RowNo, ItemId: item.allocateItemId(), ErrorReason: strings.TrimSpace(item.ErrorReason), Snapshot: appAllocatePrepareDetailSnapshot(batch, item), TenantId: user.TenantId, CreatedDept: user.DeptId, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }) } if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil { return gerror.Wrap(err, "写入划拨预处理明细失败") } return nil } func (s *sAppCard) lockAppAllocatePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batchNo string) (*entity.BatchMain, error) { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return nil, gerror.New("预处理批次号不能为空") } var batch *entity.BatchMain columns := dao.BatchMain.Columns() err := dao.BatchMain.Ctx(ctx).TX(tx). Where(columns.BatchNo, batchNo). Where(columns.BatchType, consts.BatchTypePrepare). Where(columns.TenantId, user.TenantId). Where(columns.CreatedBy, user.UserId). LockUpdate(). Limit(1). Scan(&batch) if err != nil { return nil, gerror.Wrap(err, "查询划拨预处理批次失败") } if batch == nil || batch.Id == 0 { return nil, gerror.New("划拨预处理批次不存在或无权确认") } if batch.BatchInfo == nil || batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeAllocate { return nil, gerror.New("批次不是卡划拨预处理批次") } if batch.RunResult != consts.BatchRunResultSuccess || batch.FailCount > 0 { return nil, gerror.New("划拨预处理批次存在校验失败数据,请重新校验后再提交") } return batch, nil } func (s *sAppCard) loadAppAllocatePrepareBatchDetails(ctx context.Context, tx gdb.TX, batchNo string) ([]*entity.BatchDetail, error) { var details []*entity.BatchDetail columns := dao.BatchDetail.Columns() err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(columns.BatchNo, strings.TrimSpace(batchNo)). Where(columns.BatchType, consts.BatchTypePrepare). Where(columns.DetailStatus, consts.BatchDetailStatusSuccess). Where(columns.BlankOutStatus, consts.BlankOutStatusNo). OrderAsc(columns.RowNo). Scan(&details) if err != nil { return nil, gerror.Wrap(err, "查询划拨预处理明细失败") } return details, nil } func (s *sAppCard) lockAllocateCardForSubmit(ctx context.Context, tx gdb.TX, user *entity.SysUser, detail *entity.BatchDetail, targetDeptId int64) (*appCardQueryRow, error) { if detail == nil || strings.TrimSpace(detail.ItemId) == "" { return nil, gerror.New("划拨批次明细缺少卡号") } expectedSourceDeptId := int64(0) if detail.Snapshot != nil { expectedSourceDeptId = detail.Snapshot.Get("sourceDeptId").Int64() } var card *appCardQueryRow err := s.cardQueryBase(ctx, false).TX(tx). Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().TenantId, user.TenantId). Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Iccid, strings.TrimSpace(detail.ItemId)). LockUpdate(). Limit(1). Scan(&card) if err != nil { return nil, gerror.Wrapf(err, "锁定划拨卡%s失败", detail.ItemId) } if card == nil || card.Id == 0 { return nil, gerror.Newf("卡%s不存在,不能划拨", detail.ItemId) } if err = s.attachCardChannelInfo(ctx, card); err != nil { return nil, err } if expectedSourceDeptId > 0 && card.DeptId != expectedSourceDeptId { return nil, gerror.Newf("卡%s归属已变化,请重新校验", card.Iccid) } if err = s.ensureAllocateCardVisible(ctx, tx, user, card); err != nil { return nil, err } if err = s.ensureAllocateCardOwnedByOperator(ctx, tx, user, card); err != nil { return nil, err } if err = s.validateAllocateCardBusinessRules(ctx, tx, card); err != nil { return nil, err } if card.DeptId == targetDeptId { return nil, gerror.Newf("卡%s已属于目标客户,无需重复划拨", card.Iccid) } return card, nil } func (s *sAppCard) validateAllocateCardBusinessRules(ctx context.Context, tx gdb.TX, card *appCardQueryRow) error { if card == nil || strings.TrimSpace(card.Iccid) == "" { return gerror.New("划拨卡信息不完整") } cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei) if card.BossCardStatus == 4 { return gerror.Newf("卡%s当前为停机状态,不能划拨", cardLabel) } if card.PackageStatus == consts.PackageStatusExpired || (card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() && card.FlowPkgEndAt.Before(gtime.Now())) { return gerror.Newf("卡%s套餐已过期,不能划拨", cardLabel) } return nil } func (s *sAppCard) updateAllocateCards(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, now *gtime.Time) error { for _, item := range batch.Cards { result, err := dao.TSimCardInfo.Ctx(ctx).TX(tx). Where(dao.TSimCardInfo.Columns().Id, item.Card.Id). Where(dao.TSimCardInfo.Columns().DeptId, item.Card.DeptId). Data(do.TSimCardInfo{ DeptId: batch.Target.DeptId, CreatedDept: user.DeptId, OutboundBatchNo: batchNo, AllocationBatchNo: batchNo, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrapf(err, "更新卡%s归属失败", item.Card.Iccid) } if affected, err := result.RowsAffected(); err != nil { return gerror.Wrap(err, "读取划拨更新数量失败") } else if affected != 1 { return gerror.Newf("卡%s归属已变化,请重新校验", item.Card.Iccid) } if item.SourceDeptId <= 0 { item.SourceDeptId = item.Card.DeptId } item.Card.DeptId = batch.Target.DeptId item.Card.CreatedDept = user.DeptId item.Card.OutboundBatchNo = batchNo item.Card.AllocationBatchNo = batchNo } return nil } func (s *sAppCard) markAppAllocatePrepareBatchConfirmed(ctx context.Context, tx gdb.TX, user *entity.SysUser, prepareBatch *entity.BatchMain, batch *appAllocateBatchContext, sourceBatchNo string, remark string, now *gtime.Time) error { batchNo := strings.TrimSpace(prepareBatch.BatchNo) for _, item := range batch.Cards { _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess). Where(dao.BatchDetail.Columns().ItemId, item.Card.Iccid). Data(do.BatchDetail{ BatchType: consts.BatchTypeAllocate, Snapshot: appAllocateSubmitDetailSnapshot(batch, item, batchNo, sourceBatchNo), UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新划拨批次明细失败") } } if _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare). Data(do.BatchDetail{ BatchType: consts.BatchTypeAllocate, UpdatedBy: user.UserId, UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新划拨批次明细类型失败") } logFiles, err := s.ensureAppAllocateBatchLogFiles(ctx, tx, user, batch, prepareBatch.BatchInfo, batchNo, sourceBatchNo, remark, now) if err != nil { return err } batchInfo := appAllocateBatchInfoWithLogFiles( appAllocateSubmitBatchInfo(batch, batchNo, sourceBatchNo, remark, now), logFiles, ) _, err = dao.BatchMain.Ctx(ctx).TX(tx). Where(dao.BatchMain.Columns().Id, prepareBatch.Id). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypePrepare). Data(do.BatchMain{ BatchType: consts.BatchTypeAllocate, BatchStatus: consts.BatchStatusSuccess, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, TargetDeptId: batch.Target.DeptId, TotalCount: len(batch.Cards), ValidCount: len(batch.Cards), SuccessCount: len(batch.Cards), FailCount: 0, RunResult: consts.BatchRunResultSuccess, BatchInfo: batchInfo, EndTime: now, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新划拨批次主表失败") } return nil } func (s *sAppCard) loadAllocateTarget(ctx context.Context, user *entity.SysUser, targetDeptId int64) (*entity.SysDept, error) { if targetDeptId <= 0 { return nil, gerror.New("请选择划拨目标客户") } target, err := s.loadDeptById(ctx, user.TenantId, targetDeptId) if err != nil { return nil, err } if target == nil || target.DeptId == 0 { return nil, gerror.New("划拨目标客户不存在") } if target.DeptId == user.DeptId { return nil, gerror.New("划拨目标不能选择当前企业") } if target.ParentId != user.DeptId { return nil, gerror.New("划拨目标客户不在当前企业直属下级范围内") } visible, err := appEnterpriseDeptVisibleByDataScope(ctx, user, target.DeptId) if err != nil { return nil, err } if !visible { return nil, gerror.New("划拨目标客户不在当前角色可操作范围内") } return target, nil } func (s *sAppCard) ensureAllocateCardVisible(ctx context.Context, tx gdb.TX, user *entity.SysUser, card *appCardQueryRow) error { if card == nil || card.Id <= 0 || strings.TrimSpace(card.Iccid) == "" { return gerror.New("划拨卡信息不完整") } var visible *appCardQueryRow if err := s.visibleCardModel(ctx, user).TX(tx). Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Id, card.Id). Where(dao.TSimCardInfo.Table()+"."+dao.TSimCardInfo.Columns().Iccid, card.Iccid). Limit(1). Scan(&visible); err != nil { return gerror.Wrapf(err, "校验卡%s可操作范围失败", card.Iccid) } if visible == nil || visible.Id == 0 { return gerror.Newf("卡%s不在当前企业可操作范围内", card.Iccid) } return nil } func (s *sAppCard) ensureAllocateCardOwnedByOperator(ctx context.Context, tx gdb.TX, user *entity.SysUser, card *appCardQueryRow) error { if user == nil || user.DeptId <= 0 { return gerror.New("当前用户未绑定机构,不能划拨卡片") } if card == nil || strings.TrimSpace(card.Iccid) == "" { return gerror.New("划拨卡信息不完整") } if card.DeptId != user.DeptId { cardLabel := appFirstNotEmpty(card.Iccid, card.Msisdn, card.Imsi, card.Imei) return gerror.Newf("卡%s当前归属不在本企业名下,划拨权已归当前归属客户", cardLabel) } return nil } func (s *sAppCard) loadDeptById(ctx context.Context, tenantId string, deptId int64) (*entity.SysDept, error) { var dept *entity.SysDept err := dao.SysDept.Ctx(ctx). Where(dao.SysDept.Columns().TenantId, tenantId). Where(dao.SysDept.Columns().DeptId, deptId). WhereNull(dao.SysDept.Columns().DeletedAt). Limit(1). Scan(&dept) if err != nil { return nil, gerror.Wrap(err, "查询客户信息失败") } return dept, nil } func (s *sAppCard) buildAllocateBatchModel(ctx context.Context, user *entity.SysUser, batch *appAllocateBatchContext, batchNo string, sourceBatchNo string, batchType int, status string) *model.AppCardAllocateBatchModel { batch.refreshCounts() cards := make([]*model.AppCardModel, 0, len(batch.Cards)) details := make([]*model.AppCardAllocateBatchDetailModel, 0, len(batch.Cards)) for _, item := range batch.Cards { if item == nil { continue } if item.Card != nil && strings.TrimSpace(item.ErrorReason) == "" { cards = append(cards, s.cardRowToModel(ctx, item.Card, user, appCardOwnerAgent, true)) } details = append(details, &model.AppCardAllocateBatchDetailModel{ RowNo: item.RowNo, ItemId: item.allocateItemId(), Status: appAllocateDetailStatusText(item.ErrorReason), ErrorReason: strings.TrimSpace(item.ErrorReason), }) } return &model.AppCardAllocateBatchModel{ BatchNo: strings.TrimSpace(batchNo), SourceBatchNo: strings.TrimSpace(sourceBatchNo), BatchType: batchType, Quantity: batch.ValidCount, TotalCount: batch.TotalCount, ValidCount: batch.ValidCount, FailCount: batch.FailCount, FailReason: batch.FailReason, Target: appAllocateTargetModel(batch.Target, user.DeptId), Cards: cards, Details: details, Status: status, } } func appAllocateTargetModel(dept *entity.SysDept, currentDeptId int64) *model.AppCardAllocateTargetModel { if dept == nil { return nil } label := strings.TrimSpace(dept.DeptName) if strings.TrimSpace(dept.DeptKey) != "" { label = fmt.Sprintf("%s(%s)", label, strings.TrimSpace(dept.DeptKey)) } scope := "self" scopeName := "当前机构" if currentDeptId > 0 && dept.DeptId != currentDeptId && dept.ParentId == currentDeptId { scope = "direct_child" scopeName = "直属下级" } return &model.AppCardAllocateTargetModel{ DeptId: dept.DeptId, ParentId: dept.ParentId, DeptKey: strings.TrimSpace(dept.DeptKey), DeptName: strings.TrimSpace(dept.DeptName), DeptCategory: strings.TrimSpace(dept.DeptCategory), TargetScope: scope, TargetScopeName: scopeName, Label: label, } } func appAllocatePrepareBatchInfo(batch *appAllocateBatchContext, batchNo string) *gjson.Json { return gjson.New(map[string]any{ "source": "app", "batchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(batchNo), "targetBatchType": consts.BatchTypeAllocate, "targetBatchName": "卡划拨", "targetDeptId": batch.Target.DeptId, "targetDeptName": batch.Target.DeptName, "quantity": batch.ValidCount, "totalCount": batch.TotalCount, "validCount": batch.ValidCount, "failCount": batch.FailCount, "failReason": batch.FailReason, "cards": appAllocateCardSnapshotList(batch), }) } func appAllocateSubmitBatchInfo(batch *appAllocateBatchContext, batchNo string, sourceBatchNo string, remark string, now *gtime.Time) *gjson.Json { return gjson.New(map[string]any{ "source": "app", "batchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(sourceBatchNo), "targetBatchType": consts.BatchTypeAllocate, "targetBatchName": "卡划拨", "targetDeptId": batch.Target.DeptId, "targetDeptName": batch.Target.DeptName, "quantity": len(batch.Cards), "remark": strings.TrimSpace(remark), "allocateTime": now, "cards": appAllocateCardSnapshotList(batch), }) } func appAllocatePrepareDetailSnapshot(batch *appAllocateBatchContext, item *appAllocateCardContext) *gjson.Json { if item == nil { return gjson.New(map[string]any{}) } card := item.Card cardId := int64(0) if card != nil { cardId = card.Id } return gjson.New(map[string]any{ "ICCID": appAllocateSnapshotICCID(item), "MSISDN": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Msisdn }), "IMSI": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Imsi }), "IMEI": appAllocateSnapshotField(card, func(c *appCardQueryRow) string { return c.Imei }), "rowNo": item.RowNo, "cardId": cardId, "sourceDeptId": item.SourceDeptId, "targetDeptId": batch.Target.DeptId, "targetDeptName": batch.Target.DeptName, "currentPackageCode": appCardCurrentPackageCode(card), "packageName": appCardPackageName(card), "errorReason": strings.TrimSpace(item.ErrorReason), }) } func appAllocateSnapshotICCID(item *appAllocateCardContext) string { if item == nil { return "" } if item.Card != nil && strings.TrimSpace(item.Card.Iccid) != "" { return strings.TrimSpace(item.Card.Iccid) } return strings.TrimSpace(item.ItemId) } func appAllocateSnapshotField(card *appCardQueryRow, getter func(*appCardQueryRow) string) string { if card == nil || getter == nil { return "" } return strings.TrimSpace(getter(card)) } func appAllocateDetailStatusText(reason string) string { if strings.TrimSpace(reason) != "" { return "failed" } return "success" } func appAllocateSubmitDetailSnapshot(batch *appAllocateBatchContext, item *appAllocateCardContext, batchNo string, sourceBatchNo string) *gjson.Json { card := item.Card return gjson.New(map[string]any{ "ICCID": card.Iccid, "MSISDN": card.Msisdn, "IMSI": card.Imsi, "IMEI": card.Imei, "rowNo": item.RowNo, "cardId": card.Id, "allocateBatchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(sourceBatchNo), "sourceDeptId": item.SourceDeptId, "targetDeptId": batch.Target.DeptId, "targetDeptName": batch.Target.DeptName, "currentPackageCode": appCardCurrentPackageCode(card), "packageName": appCardPackageName(card), }) } func appAllocateCardSnapshotList(batch *appAllocateBatchContext) []map[string]any { cards := make([]map[string]any, 0, len(batch.Cards)) for _, item := range batch.Cards { if item == nil { continue } cardId := int64(0) iccid := item.allocateItemId() msisdn := "" currentPackageCode := "" if item.Card != nil { cardId = item.Card.Id msisdn = item.Card.Msisdn currentPackageCode = appCardCurrentPackageCode(item.Card) } cards = append(cards, map[string]any{ "cardId": cardId, "iccid": iccid, "msisdn": msisdn, "sourceDeptId": item.SourceDeptId, "targetDeptId": batch.Target.DeptId, "currentPackageCode": currentPackageCode, "errorReason": strings.TrimSpace(item.ErrorReason), }) } return cards } func appAllocateSourceBatchNoFromBatch(batch *entity.BatchMain) string { if batch == nil { return "" } if batch.BatchInfo != nil { if sourceBatchNo := strings.TrimSpace(batch.BatchInfo.Get("sourceBatchNo").String()); sourceBatchNo != "" { return sourceBatchNo } } return strings.TrimSpace(batch.SourceBatchNo) } func appNormalizeAllocateCardValues(values []string) []string { seen := map[string]struct{}{} result := make([]string, 0, len(values)) for _, raw := range values { parts := strings.FieldsFunc(raw, func(r rune) bool { switch r { case ',', ',', ';', ';', '\n', '\r', '\t', ' ': return true default: return false } }) for _, part := range parts { value := strings.TrimSpace(part) if value == "" { continue } if _, exists := seen[value]; exists { continue } seen[value] = struct{}{} result = append(result, value) } } return result }