package renew import ( "context" "sort" "strconv" "strings" "time" "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/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/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/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) type Processor struct { repo *Repository config batchimport.ProcessorConfig } func NewProcessor() *Processor { return &Processor{ repo: NewRepository(), config: batchimport.ProcessorConfig{ Code: "sim_card_renew", BatchType: consts.BatchTypeRenew, BatchNoPrefix: consts.BatchNoPrefixRenew, TaskName: consts.ResourceBatchDownloadTaskSimCardRenew, ExportScene: consts.ResourceExportSceneRenew, ImportDir: consts.SimCardRenewSubDirImportExcel, ResultDir: consts.SimCardRenewSubDirResultExcel, MaxFileSize: consts.SimCardRenewMaxFileSize, TemplateHeaders: ExcelTemplateHeaders(), RequiredHeaders: []string{"ICCID"}, }, } } func (p *Processor) Config() batchimport.ProcessorConfig { return p.config } func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) { return nil, gerror.New("卡续费不支持文件导入") } func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error { renewRows := make([]*Row, 0, len(rows)) for _, item := range rows { row, ok := item.(*Row) if !ok { item.AddErrorReason("续费行类型错误") continue } renewRows = append(renewRows, row) } if err := normalizeRenewIdentityRows(ctx, renewRows); err != nil { return err } return validateRows(renewRows) } func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error { if req == nil { return gerror.New("续费请求不能为空") } in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam) if !ok || in == nil { return gerror.New("续费参数类型错误") } renewRows := make([]*Row, 0, len(rows)) for _, item := range rows { row, ok := item.(*Row) if !ok { return gerror.New("续费行类型错误") } renewRows = append(renewRows, row) } options, err := loadExecutionOptions(ctx, tx, req.BatchNo) if err != nil { return err } applyExecutionOptions(in, options) prepared, err := p.validateRenewContext(ctx, tx, in, req.Operator, renewRows, req.BatchNo, false) if err != nil { return err } priceSnapshot, err := ensurePriceSnapshot(ctx, tx, req.Operator, prepared, options.PriceSnapshot) if err != nil { return err } durationValue := renewDisplayDurationValue(in) if durationValue <= 0 { return gerror.New("续费时长必须大于0") } if !containsRenewDurationOption(priceSnapshot.RenewableDurationOptions, durationValue) { return gerror.New("续费时长不在当前套餐可续费范围内") } output, err := p.Complete(ctx, tx, &CompleteInput{ SubmitParam: in, BatchNo: req.BatchNo, Rows: renewRows, Operator: req.Operator, PriceSnapshot: priceSnapshot, }) if err != nil { return err } req.ExecutionResult = output return nil } func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json { info := g.Map{} if result != nil && result.ImportFileOssId > 0 { info["importFileOssId"] = result.ImportFileOssId } else if importOss != nil && importOss.OssId > 0 { info["importFileOssId"] = importOss.OssId } if resultOss != nil { info["resultFileOssId"] = resultOss.OssId } if downloadId > 0 { info["resultDownloadId"] = downloadId } if in, ok := req.BizParams.(*model.TSimCardRenewBatchSubmitParam); ok && in != nil { durationValue := renewDisplayDurationValue(in) if durationValue > 0 { actualDurationMonths := renewActualDurationMonths(nil, durationValue) info["durationYears"] = durationValue info["durationMonths"] = actualDurationMonths info["durationValue"] = durationValue info["actualDurationMonths"] = actualDurationMonths } info["giftDurationMonths"] = in.GiftDurationMonths info["amount"] = strings.TrimSpace(in.Amount) if in.PaymentSnapshot != nil { appendRenewPaymentSnapshot(info, in.PaymentSnapshot) } else if paymentMode := strings.TrimSpace(in.PaymentMode); paymentMode != "" { info["paymentMode"] = paymentMode } if packageCode := strings.TrimSpace(in.PackageCode); packageCode != "" { info["packageCode"] = packageCode } info["offlineContractCompleted"] = in.OfflineContractCompleted if sourceBatchNo := strings.TrimSpace(in.SourceBatchNo); sourceBatchNo != "" { info["sourceBatchNo"] = sourceBatchNo } if buyerNote := strings.TrimSpace(in.BuyerNote); buyerNote != "" { info["buyerNote"] = buyerNote } if adminNote := strings.TrimSpace(in.AdminNote); adminNote != "" { info["adminNote"] = adminNote } if remark := strings.TrimSpace(in.Remark); remark != "" { info["remark"] = remark } } if snapshot, ok := req.ExecutionResult.(*model.TSimCardRenewPriceSnapshot); ok && snapshot != nil { if result != nil && result.BatchType == consts.BatchTypePrepare { info["targetBatchType"] = consts.BatchTypeRenew info["targetBatchName"] = "卡续费" } info["customerAgentId"] = snapshot.CustomerAgentId info["packageCode"] = snapshot.PackageCode info["packageName"] = snapshot.PackageName info["billingMethod"] = snapshot.BillingMethod info["renewDurationUnit"] = snapshot.RenewDurationUnit info["renewDurationUnitText"] = snapshot.RenewDurationUnitText if durationValue := gconv.Int(info["durationValue"]); durationValue > 0 { actualDurationMonths := renewActualDurationMonths(snapshot, durationValue) info["durationMonths"] = actualDurationMonths info["actualDurationMonths"] = actualDurationMonths } info["quantity"] = result.ValidRows info["priceSource"] = snapshot.PriceSource info["priceOwnerDeptId"] = snapshot.PriceOwnerDeptId info["agentDefaultPriceId"] = snapshot.AgentDefaultPriceId info["targetPriceId"] = snapshot.TargetPriceId info["renewUnitPrice"] = snapshot.RenewUnitPrice info["renewableDurationOptions"] = snapshot.RenewableDurationOptions info["renewablePackageCodes"] = snapshot.RenewablePackageCodes info["renewablePackages"] = snapshot.RenewablePackages info["renewBeginDate"] = snapshot.RenewBeginDate info["renewBeginDateMin"] = snapshot.RenewBeginDateMin info["renewBeginDateMax"] = snapshot.RenewBeginDateMax info["sameRenewBeginDate"] = snapshot.SameRenewBeginDate } if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil { info["quantity"] = output.Quantity info["packageCode"] = output.PackageCode info["orderSn"] = output.OrderSn info["renewTime"] = output.RenewTime if sourceBatchNo := strings.TrimSpace(output.SourceBatchNo); sourceBatchNo != "" { info["sourceBatchNo"] = sourceBatchNo } } return gjson.New(info) } func appendRenewPaymentSnapshot(info g.Map, snapshot *model.TSimCardRenewPaymentSnapshot) { if info == nil || snapshot == nil { return } info["paymentMode"] = strings.TrimSpace(snapshot.PaymentMode) info["payType"] = snapshot.PayType info["payStatus"] = snapshot.PayStatus info["payAmount"] = snapshot.PayAmount info["balanceAmount"] = snapshot.BalanceAmount info["offlinePaidAmount"] = snapshot.OfflinePaidAmount info["balanceBefore"] = snapshot.BalanceBefore info["balanceAfter"] = snapshot.BalanceAfter info["balanceDetailSerialNo"] = strings.TrimSpace(snapshot.BalanceDetailSerialNo) info["payerDeptId"] = snapshot.PayerDeptId info["payerUserId"] = snapshot.PayerUserId info["isTopDept"] = snapshot.IsTopDept if snapshot.PaymentTime != nil { info["paymentTime"] = snapshot.PaymentTime.Format("Y-m-d H:i:s") } info["paymentSnapshot"] = snapshot } func (p *Processor) ValidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row) (*PrepareContext, error) { return p.validateRenewContext(ctx, tx, in, operator, rows, "", false) } func (p *Processor) RevalidatePrepareContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row, currentBatchNo string) (*PrepareContext, bool, error) { prepared, err := p.validateRenewContext(ctx, tx, in, operator, rows, currentBatchNo, true) return prepared, hasInvalidRows(rows), err } func (p *Processor) validateRenewContext(ctx context.Context, tx gdb.TX, in *model.TSimCardRenewBatchSubmitParam, operator batchimport.OperatorContext, rows []*Row, currentBatchNo string, allowPartial bool) (*PrepareContext, error) { iccids := rowsToIccids(rows) cardsByIccid, err := p.repo.LockCardsByIccids(ctx, tx, iccids) if err != nil { return nil, gerror.Wrap(err, "校验续费卡信息失败") } bindsByIccid, err := p.repo.LockLatestBindsByIccids(ctx, tx, iccids) if err != nil { return nil, gerror.Wrap(err, "校验续费订购记录失败") } conflictingBatches, err := p.repo.UnfinishedRenewBatchNosByIccids( ctx, tx, operator.TenantId, iccids, currentBatchNo, ) if err != nil { return nil, gerror.Wrap(err, "校验未完成续费批次失败") } sourceBatchNo := strings.TrimSpace(in.SourceBatchNo) packageCodes := make(map[string]struct{}) customerAgentIds := make(map[int64]struct{}) currentPackagePeriods := make(map[string]struct{}) renewBeginDates := make([]string, 0, len(rows)) packageCode := "" customerAgentId := int64(0) for _, row := range rows { if row == nil || row.Reason != "" { continue } card := cardsByIccid[row.Iccid] bind := bindsByIccid[row.Iccid] if strings.TrimSpace(conflictingBatches[row.Iccid]) != "" { row.AddReason("存在未完成续费批次,不能重复续费") continue } reason := validateRenewRow(sourceBatchNo, operator, card, bind) if reason != "" { row.AddReason(reason) continue } baseExpireAt := renewBaseExpireTime(card, bind) periodKey := currentPackagePeriodKeyWithExpire(bind, baseExpireAt) if periodKey == "" { row.AddReason("不是统一批次") continue } currentPackageCode := strings.TrimSpace(card.CurrentPackageCode) packageCodes[currentPackageCode] = struct{}{} customerAgentIds[card.DeptId] = struct{}{} currentPackagePeriods[periodKey] = struct{}{} packageCode = currentPackageCode customerAgentId = card.DeptId renewBeginAt := renewBeginDateFromExpire(baseExpireAt) if renewBeginAt != nil { renewBeginDates = append(renewBeginDates, renewBeginAt.Format("Y-m-d")) } } if len(packageCodes) > 1 { markRowsWithoutReason(rows, "当前批次下存在多个套餐,不能统一续费") } if len(customerAgentIds) > 1 { markRowsWithoutReason(rows, "当前批次下存在多个客户,不能统一续费") } if len(currentPackagePeriods) > 1 { markRowsWithoutReason(rows, "不是统一批次") } if len(customerAgentIds) == 1 && customerAgentId > 0 { if err := validateRenewCustomerDeptScope(ctx, tx, operator, customerAgentId); err != nil { markRowsWithoutReason(rows, err.Error()) } } if hasInvalidRows(rows) && !allowPartial { markRemainingRowsSkipped(rows) return nil, gerror.New("存在不满足续费条件的卡,本次未执行") } validCount := countValidRows(rows) if validCount == 0 { return &PrepareContext{}, nil } renewBeginDate, renewBeginDateMin, renewBeginDateMax, sameRenewBeginDate := summarizeRenewBeginDates(renewBeginDates) return &PrepareContext{ CustomerAgentId: customerAgentId, PackageCode: packageCode, Quantity: validCount, RenewBeginDate: renewBeginDate, RenewBeginDateMin: renewBeginDateMin, RenewBeginDateMax: renewBeginDateMax, SameRenewBeginDate: sameRenewBeginDate, }, nil } func currentPackagePeriodKey(bind *entity.TSimCardBindPkg) string { if bind == nil { return "" } return currentPackagePeriodKeyWithExpire(bind, bind.ExpectedExpireTime) } func currentPackagePeriodKeyWithExpire(bind *entity.TSimCardBindPkg, expireAt *gtime.Time) string { if bind == nil || bind.EffectiveTime == nil { return "" } if expireAt == nil { expireAt = bind.ExpectedExpireTime } if expireAt == nil { return "" } return bind.EffectiveTime.Format("Y-m-d") + "|" + expireAt.Format("Y-m-d") } func summarizeRenewBeginDates(dates []string) (sameDate string, minDate string, maxDate string, same bool) { cleaned := make([]string, 0, len(dates)) for _, date := range dates { date = strings.TrimSpace(date) if date != "" { cleaned = append(cleaned, date) } } if len(cleaned) == 0 { return "", "", "", false } sort.Strings(cleaned) minDate = cleaned[0] maxDate = cleaned[len(cleaned)-1] same = minDate == maxDate if same { sameDate = minDate } return sameDate, minDate, maxDate, same } func renewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time { if expireAt == nil { return nil } return normalizeRenewDateAtDayEnd(expireAt) } func normalizeRenewDateAtDayEnd(expireAt *gtime.Time) *gtime.Time { if expireAt == nil || expireAt.IsZero() { return nil } t := expireAt.Time return gtime.NewFromTime( time.Date( t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location(), ), ) } func validateRenewRow(sourceBatchNo string, operator batchimport.OperatorContext, card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) string { if card == nil { return "ICCID不存在正式卡记录,不能续费" } if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped { return "ICCID当前为停机状态,不能续费" } if bind == nil { return "ICCID没有订购记录,不能续费" } if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone { return "ICCID没有当前套餐,不能续费" } if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) { return "ICCID当前套餐与最新订购记录不一致,不能续费" } baseExpireAt := renewBaseExpireTime(card, bind) if baseExpireAt == nil { return "ICCID最新订购没有预计到期时间,不能续费" } if renewExpireAfterAllowedWindow(baseExpireAt) { return "距离到期超过6个月,不能续费" } if sourceBatchNo != "" && strings.TrimSpace(card.OutboundBatchNo) != sourceBatchNo { return "ICCID不属于来源出库批次,不能续费" } return "" } func renewBaseExpireTime(card *entity.TSimCardInfo, bind *entity.TSimCardBindPkg) *gtime.Time { var expireAt *gtime.Time if bind != nil { expireAt = laterRenewExpireTime(expireAt, bind.ExpectedExpireTime) } if card != nil { expireAt = laterRenewExpireTime(expireAt, card.FlowPkgEndAt) expireAt = laterRenewExpireTime(expireAt, card.FlowPkgPresentAt) expireAt = laterRenewExpireTime(expireAt, card.FlowPkgRealPresentAt) } return expireAt } func laterRenewExpireTime(current, candidate *gtime.Time) *gtime.Time { if candidate == nil || candidate.IsZero() { return current } if current == nil || current.IsZero() || candidate.After(current) { return candidate } return current } func renewExpireAfterAllowedWindow(expireAt *gtime.Time) bool { if expireAt == nil { return false } return expireAt.After(gtime.Now().AddDate(0, 6, 0)) } func validateRenewCustomerDeptScope(ctx context.Context, tx gdb.TX, operator batchimport.OperatorContext, customerDeptID int64) error { if operator.DeptId <= 0 || operator.TenantId == "" { return gerror.New("当前用户未绑定组织或租户") } if customerDeptID <= 0 { return gerror.New("ICCID不属于当前机构,不能续费") } if customerDeptID == operator.DeptId { return nil } dept := &entity.SysDept{} if err := tx.Ctx(ctx).Model(dao.SysDept.Table()). Fields(dao.SysDept.Columns().DeptId, dao.SysDept.Columns().ParentId, dao.SysDept.Columns().Ancestors). Where(dao.SysDept.Columns().TenantId, operator.TenantId). Where(dao.SysDept.Columns().DeptId, customerDeptID). WhereNull(dao.SysDept.Columns().DeletedAt). Scan(dept); err != nil { return gerror.Wrap(err, "校验ICCID归属失败") } if dept.DeptId == 0 { return gerror.New("ICCID不属于当前机构,不能续费") } if dept.ParentId == operator.DeptId || renewDeptAncestorsContain(dept.Ancestors, operator.DeptId) { return nil } return gerror.New("ICCID不属于当前机构,不能续费") } func renewDeptAncestorsContain(ancestors string, deptID int64) bool { if deptID <= 0 { return false } target := strconv.FormatInt(deptID, 10) for _, item := range strings.Split(strings.Trim(ancestors, ","), ",") { if strings.TrimSpace(item) == target { return true } } return false } func hasInvalidRows(rows []*Row) bool { for _, row := range rows { if row != nil && row.Reason != "" { return true } } return false } func countValidRows(rows []*Row) int { count := 0 for _, row := range rows { if row != nil && row.Valid() { count++ } } return count } func markRowsWithoutReason(rows []*Row, reason string) { for _, row := range rows { if row != nil && row.Reason == "" { row.AddReason(reason) } } } func markRemainingRowsSkipped(rows []*Row) { for _, row := range rows { if row != nil && row.Reason == "" { row.AddReason("同批存在不满足续费条件的卡,本行未执行") } } } func validateRows(rows []*Row) error { firstByIccid := make(map[string]*Row, len(rows)) for _, row := range rows { row.Iccid = strings.TrimSpace(row.Iccid) if row.Iccid == "" { row.AddReason("ICCID不能为空") continue } if first, ok := firstByIccid[row.Iccid]; ok { row.AddReason("ICCID重复") first.AddReason("ICCID重复") continue } firstByIccid[row.Iccid] = row } return nil }