package cardpackage import ( "context" "fmt" "strings" "time" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageduration" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" ) func (s *Service) RenewCurrentPackage(ctx context.Context, tx gdb.TX, in *RenewCurrentPackageInput) (*RenewCurrentPackageOutput, error) { if err := validateRenewInput(in); err != nil { return nil, err } iccids := uniqueStrings(in.Iccids) cardsByIccid, err := s.repo.LockCardsByIccids(ctx, tx, iccids) if err != nil { return nil, gerror.Wrap(err, "锁定卡信息失败") } if len(cardsByIccid) != len(iccids) { return nil, gerror.New("部分卡信息已变化,请刷新后重试") } latestBindsByIccid, err := s.repo.LockLatestBindsByIccids(ctx, tx, iccids) if err != nil { return nil, gerror.Wrap(err, "锁定最新订购失败") } if len(latestBindsByIccid) != len(iccids) { return nil, gerror.New("部分卡没有订购记录,不能续费") } now := gtime.Now() sourceBatchNo := strings.TrimSpace(in.SourceBatchNo) currentPackageCode := "" customerAgentId := int64(0) var renewBeginAt *gtime.Time for _, iccid := range iccids { card := cardsByIccid[iccid] bind := latestBindsByIccid[iccid] if err := validateRenewCardStatus(iccid, card); err != nil { return nil, err } if bind == nil { return nil, gerror.Newf("ICCID没有订购记录:%s", iccid) } if strings.TrimSpace(card.CurrentPackageCode) == "" || card.PackageStatus == consts.PackageStatusNone { return nil, gerror.Newf("ICCID没有可续费套餐:%s", iccid) } if strings.TrimSpace(card.CurrentPackageCode) != strings.TrimSpace(bind.PackageCode) { return nil, gerror.Newf("ICCID当前套餐与最新订购记录不一致:%s", iccid) } baseExpireAt := renewBaseExpireTime(card, bind) if baseExpireAt == nil { return nil, gerror.Newf("ICCID最新订购没有预计到期时间:%s", iccid) } periodBegin := renewPeriodBeginAt(baseExpireAt, in.PackageBeginTime) if renewExpireAfterAllowedWindow(baseExpireAt, now) { return nil, gerror.Newf("距离到期超过6个月,不能续费:%s", iccid) } if renewBeginAt == nil { renewBeginAt = periodBegin } else if !sameRenewTime(renewBeginAt, periodBegin) { return nil, gerror.New("当前批次下卡不是统一续费批次") } if sourceBatchNo != "" && strings.TrimSpace(card.OutboundBatchNo) != sourceBatchNo { return nil, gerror.Newf("ICCID不属于来源出库批次:%s", iccid) } if currentPackageCode == "" { currentPackageCode = strings.TrimSpace(card.CurrentPackageCode) } if currentPackageCode != strings.TrimSpace(card.CurrentPackageCode) { return nil, gerror.New("当前批次下存在多个套餐,不能统一续费") } if customerAgentId == 0 { customerAgentId = card.DeptId } if customerAgentId != card.DeptId { return nil, gerror.New("当前批次下存在多个客户,不能统一续费") } } customer, err := s.loadTargetDept(ctx, tx, customerAgentId, in.Operator, true) if err != nil { return nil, err } targetPackageCode := strings.TrimSpace(in.PackageCode) if targetPackageCode == "" { targetPackageCode = currentPackageCode } currentPkg, err := s.loadPackage(ctx, tx, currentPackageCode) if err != nil { return nil, err } if !renewPackageAllowed(currentPkg, targetPackageCode) { return nil, gerror.New("续费套餐不在当前套餐可续费范围内") } pkg := currentPkg if targetPackageCode != currentPackageCode { pkg, err = s.loadPackage(ctx, tx, targetPackageCode) } if err != nil { return nil, err } durationUnit, durationValue, durationMonths, giftDurationValue, giftDurationMonths := normalizeRenewPackageDuration(in) if durationMonths <= 0 { return nil, gerror.New("续费时长必须大于0") } amount, err := parseAmount(in.Amount) if err != nil { return nil, err } quantity := len(iccids) unitPrice := 0.0 if quantity > 0 { unitPrice = roundAmount(amount / float64(quantity)) } firstBaseExpireAt := renewBaseExpireTime(cardsByIccid[iccids[0]], latestBindsByIccid[iccids[0]]) minBeginAt := renewPeriodBeginAt(firstBaseExpireAt, in.PackageBeginTime) maxEndAt := firstBaseExpireAt for _, iccid := range iccids { baseExpireAt := renewBaseExpireTime(cardsByIccid[iccid], latestBindsByIccid[iccid]) periodBegin := renewPeriodBeginAt(baseExpireAt, in.PackageBeginTime) if minBeginAt == nil || (periodBegin != nil && periodBegin.Before(minBeginAt)) { minBeginAt = periodBegin } endAt := addMonths(periodBegin, durationMonths+giftDurationMonths) if maxEndAt == nil || (endAt != nil && endAt.After(maxEndAt)) { maxEndAt = endAt } } if in.PackageEndTime != nil && !sameRenewTime(maxEndAt, in.PackageEndTime) { return nil, gerror.New("续费结束日期与续费时长不一致") } orderSn, err := batchimport.NextRandomNo(orderNoPrefix) if err != nil { return nil, err } orderInput := &CreatePackageSubscriptionInput{ BatchNo: in.BatchNo, CustomerAgentId: customerAgentId, PackageCode: targetPackageCode, PackageBeginTime: minBeginAt, DurationMonths: durationMonths, GiftDurationMonths: giftDurationMonths, DurationDisplayValue: durationValue, GiftDurationDisplayValue: giftDurationValue, DurationUnit: durationUnit, Amount: in.Amount, PayType: in.PayType, PayStatus: in.PayStatus, BalanceAmount: in.BalanceAmount, OfflinePaidAmount: in.OfflinePaidAmount, PayTime: in.PayTime, OfflineContractCompleted: in.OfflineContractCompleted, BuyerNote: in.BuyerNote, AdminNote: in.AdminNote, BusinessType: consts.PackageBusinessTypeRenew, OrderRemark: chooseString(in.OrderRemark, "卡续费业务订单载体"), BindRemark: in.BindRemark, Operator: in.Operator, } orderId, err := s.insertOrder(ctx, tx, orderInput, orderSn, amount, now) if err != nil { return nil, err } if err = s.insertOrderItem(ctx, tx, orderInput, orderId, orderSn, pkg, amount, unitPrice, quantity, minBeginAt, maxEndAt, now); err != nil { return nil, err } bindRows := make([]do.TSimCardBindPkg, 0, quantity) newBindsByIccid := make(map[string]*entity.TSimCardBindPkg, quantity) for _, iccid := range iccids { card := cardsByIccid[iccid] latestBind := latestBindsByIccid[iccid] periodBegin := renewPeriodBeginAt(renewBaseExpireTime(card, latestBind), in.PackageBeginTime) periodEnd := addMonths(periodBegin, durationMonths+giftDurationMonths) _, effectiveStatus := calcPackageStatuses(now, periodBegin, periodEnd) subscriptionNo, err := batchimport.NextRandomNo(subscriptionNoPrefix) if err != nil { return nil, err } row := do.TSimCardBindPkg{ SubscriptionNo: subscriptionNo, BatchNo: in.BatchNo, Iccid: iccid, Msisdn: strings.TrimSpace(card.Msisdn), DeptId: fmt.Sprintf("%d", customerAgentId), PackageType: pkg.PackageType, PackageCode: pkg.PackageCode, PreviousPackageCode: currentPackageCode, BusinessType: consts.PackageBusinessTypeRenew, EffectiveStatus: effectiveStatus, HandleTime: now, EffectiveTime: periodBegin, ExpectedExpireTime: periodEnd, DurationMonths: durationMonths, GiftDurationMonths: giftDurationMonths, OrderCode: orderSn, Remark: strings.TrimSpace(in.BindRemark), TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, } bindRows = append(bindRows, row) newBindsByIccid[iccid] = &entity.TSimCardBindPkg{ SubscriptionNo: subscriptionNo, BatchNo: in.BatchNo, Iccid: iccid, Msisdn: strings.TrimSpace(card.Msisdn), DeptId: fmt.Sprintf("%d", customerAgentId), PackageType: pkg.PackageType, PackageCode: pkg.PackageCode, PreviousPackageCode: currentPackageCode, BusinessType: consts.PackageBusinessTypeRenew, EffectiveStatus: effectiveStatus, HandleTime: now, EffectiveTime: periodBegin, ExpectedExpireTime: periodEnd, DurationMonths: durationMonths, GiftDurationMonths: giftDurationMonths, OrderCode: orderSn, Remark: strings.TrimSpace(in.BindRemark), TenantId: in.Operator.TenantId, CreatedDept: in.Operator.DeptId, CreatedBy: in.Operator.UserId, CreatedAt: now, UpdatedBy: in.Operator.UserId, UpdatedAt: now, } } if err = s.InsertCardPackageBinds(ctx, tx, bindRows); err != nil { return nil, err } out := &RenewCurrentPackageOutput{ Iccids: iccids, OrderId: orderId, OrderSn: orderSn, Customer: customer, Package: pkg, CardsByIccid: cardsByIccid, LatestBindsByIccid: latestBindsByIccid, NewBindsByIccid: newBindsByIccid, BindRows: bindRows, RenewTime: now, PackageCode: targetPackageCode, Amount: amount, UnitPrice: unitPrice, Quantity: quantity, } if err = s.UpdateCardsAfterRenew(ctx, tx, out, in.Operator.UserId); err != nil { return nil, err } return out, nil } func renewExpireAfterAllowedWindow(expireAt, now *gtime.Time) bool { if expireAt == nil { return false } if now == nil { now = gtime.Now() } return expireAt.After(now.AddDate(0, 6, 0)) } func sameRenewTime(left, right *gtime.Time) bool { switch { case left == nil && right == nil: return true case left == nil || right == nil: return false default: return !left.Before(right) && !left.After(right) } } func renewPeriodBeginAt(latest, requested *gtime.Time) *gtime.Time { if requested != nil { return renewDayEndAt(requested) } return renewBeginDateFromExpire(latest) } func renewBeginDateFromExpire(expireAt *gtime.Time) *gtime.Time { if expireAt == nil { return nil } return renewDayEndAt(expireAt) } 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 renewDayEndAt(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 renewDayEndAt(t *gtime.Time) *gtime.Time { if t == nil || t.IsZero() { return nil } tt := t.Time return gtime.NewFromTime(time.Date(tt.Year(), tt.Month(), tt.Day(), 23, 59, 59, 0, tt.Location())) } func renewPackageAllowed(currentPkg *entity.TSimFlowPackageInfo, targetPackageCode string) bool { targetPackageCode = strings.TrimSpace(targetPackageCode) if currentPkg == nil || targetPackageCode == "" { return false } var codes []string if currentPkg.RenewablePackageCodes != nil { _ = currentPkg.RenewablePackageCodes.Scan(&codes) } for _, code := range uniqueStrings(codes) { if strings.TrimSpace(code) == targetPackageCode { return true } } return false } func normalizeRenewPackageDuration(in *RenewCurrentPackageInput) (durationUnit string, durationValue int, durationMonths int, giftDurationValue int, giftDurationMonths int) { durationUnit = consts.RenewDurationUnitYear if in == nil { return } durationValue = in.DurationDisplayValue durationMonths = in.DurationMonths if durationValue > 0 { // Renewal duration is always expressed in years. The database keeps // the resulting subscription duration in months for lifecycle logic. durationMonths = packageduration.RenewMonths(durationValue) } giftDurationMonths = in.GiftDurationMonths giftDurationValue = in.GiftDurationDisplayValue if giftDurationValue <= 0 { giftDurationValue = giftDurationMonths } return } func validateRenewCardStatus(iccid string, card *entity.TSimCardInfo) error { if card == nil { return gerror.Newf("ICCID不存在正式卡记录:%s", iccid) } if card.BossCardStatus == consts.V3ApiSimBossCardStatusStopped { return gerror.Newf("ICCID当前为停机状态,不能续费:%s", iccid) } return nil } func validateRenewInput(in *RenewCurrentPackageInput) error { if in == nil { return gerror.New("续费参数不能为空") } if strings.TrimSpace(in.BatchNo) == "" { return gerror.New("续费批次号不能为空") } if len(in.Iccids) == 0 { return gerror.New("续费卡不能为空") } if in.DurationMonths <= 0 && in.DurationDisplayValue <= 0 { return gerror.New("续费时长必须大于0") } if in.GiftDurationMonths < 0 { return gerror.New("赠送时长不能小于0") } payType := in.PayType if payType == 0 { payType = consts.PayTypeOffline } if payType == consts.PayTypeOffline && !in.OfflineContractCompleted { return gerror.New("请确认线下合同、销售和走账已完成") } return nil } func chooseString(primary, fallback string) string { primary = strings.TrimSpace(primary) if primary != "" { return primary } return strings.TrimSpace(fallback) }