package renew import ( "context" "database/sql" "errors" "fmt" "math" "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/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageduration" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageprice" "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" ) const ( renewPaymentModeOffline = "offline" renewPaymentModeBalance = "balance" renewFinanceIncomeExpenseExpense = 1 renewFinanceTransactionTypeConsume = 1 renewFinanceTransactionChannelBalance = 3 renewFinanceTransactionStatusPaid = 1 renewFinanceAccountStatusNormal = 0 ) type executionOptions struct { PriceSnapshot *model.TSimCardRenewPriceSnapshot PaymentIntent *model.TSimCardRenewPaymentSnapshot PackageCode string DurationYears int DurationMonths int GiftDurationMonths int PaymentMode string OfflineContract bool RenewDate string PackageBeginTime string PackageEndTime string SourceBatchNo string BuyerNote string AdminNote string Remark string } type paymentExecutionInput struct { BatchNo string PriceSnapshot *model.TSimCardRenewPriceSnapshot Quantity int SubmitParam *model.TSimCardRenewBatchSubmitParam Operator batchimport.OperatorContext } func loadExecutionOptions(ctx context.Context, tx gdb.TX, batchNo string) (*executionOptions, error) { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return nil, gerror.New("续费批次号不能为空") } var batch *entity.BatchMain if err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Fields( dao.BatchMain.Columns().BatchInfo, dao.BatchMain.Columns().SourceBatchNo, ). Where(dao.BatchMain.Columns().BatchNo, batchNo). Limit(1). Scan(&batch); err != nil { return nil, gerror.Wrap(err, "查询续费批次快照失败") } if batch == nil || batch.BatchInfo == nil { return &executionOptions{}, nil } info := batch.BatchInfo priceSnapshot := priceSnapshotFromInfo(info) durationYears := jsonInt(info, "durationYears", "durationValue") durationMonths := jsonInt(info, "durationMonths") actualDurationMonths := jsonInt(info, "actualDurationMonths") if durationYears <= 0 { durationYears = renewDurationYearsFromLegacy(durationMonths, actualDurationMonths, priceSnapshot) } if durationYears > 0 { actualDurationMonths = renewActualDurationMonths(priceSnapshot, durationYears) } options := &executionOptions{ PriceSnapshot: priceSnapshot, PaymentIntent: paymentSnapshotFromInfo(info), PackageCode: jsonString(info, "packageCode", "priceSnapshot.packageCode"), DurationYears: durationYears, DurationMonths: actualDurationMonths, GiftDurationMonths: jsonInt(info, "giftDurationMonths"), PaymentMode: jsonString(info, "paymentMode", "paymentSnapshot.paymentMode"), OfflineContract: jsonBool(info, "offlineContractCompleted"), RenewDate: jsonString(info, "renewDate"), PackageBeginTime: jsonString(info, "packageBeginTime"), PackageEndTime: jsonString(info, "packageEndTime"), SourceBatchNo: jsonString(info, "sourceBatchNo"), BuyerNote: jsonString(info, "buyerNote"), AdminNote: jsonString(info, "adminNote"), Remark: jsonString(info, "remark"), } if options.SourceBatchNo == "" { options.SourceBatchNo = strings.TrimSpace(batch.SourceBatchNo) } return options, nil } func applyExecutionOptions(in *model.TSimCardRenewBatchSubmitParam, options *executionOptions) { if in == nil || options == nil { return } if in.DurationYears <= 0 && options.DurationYears > 0 { in.DurationYears = options.DurationYears } if in.DurationMonths <= 0 && options.DurationMonths > 0 { in.DurationMonths = options.DurationMonths } if in.DurationYears <= 0 && in.DurationMonths > 0 { in.DurationYears = renewDurationYearsFromLegacy(in.DurationMonths, 0, options.PriceSnapshot) } if in.GiftDurationMonths <= 0 && options.GiftDurationMonths > 0 { in.GiftDurationMonths = options.GiftDurationMonths } if strings.TrimSpace(in.PaymentMode) == "" { in.PaymentMode = strings.TrimSpace(options.PaymentMode) } if strings.TrimSpace(in.PackageCode) == "" { in.PackageCode = strings.TrimSpace(options.PackageCode) } if in.PaymentSnapshot == nil { in.PaymentSnapshot = options.PaymentIntent } if !in.OfflineContractCompleted { in.OfflineContractCompleted = options.OfflineContract } if strings.TrimSpace(in.SourceBatchNo) == "" { in.SourceBatchNo = strings.TrimSpace(options.SourceBatchNo) } if strings.TrimSpace(in.PackageBeginTime) == "" { in.PackageBeginTime = strings.TrimSpace(options.PackageBeginTime) } if strings.TrimSpace(in.PackageEndTime) == "" { in.PackageEndTime = strings.TrimSpace(options.PackageEndTime) } if strings.TrimSpace(in.BuyerNote) == "" { in.BuyerNote = strings.TrimSpace(options.BuyerNote) } if strings.TrimSpace(in.AdminNote) == "" { in.AdminNote = strings.TrimSpace(options.AdminNote) } if strings.TrimSpace(in.Remark) == "" { in.Remark = strings.TrimSpace(options.Remark) } } func ensurePriceSnapshot(ctx context.Context, tx gdb.TX, operator batchimport.OperatorContext, prepared *PrepareContext, existing *model.TSimCardRenewPriceSnapshot) (*model.TSimCardRenewPriceSnapshot, error) { if prepared == nil { return nil, gerror.New("续费预处理上下文不能为空") } snapshot := existing if snapshot != nil { if snapshot.CustomerAgentId > 0 && snapshot.CustomerAgentId != prepared.CustomerAgentId { return nil, gerror.New("卡归属客户已变化,请重新预处理") } targetPackageCode := strings.TrimSpace(snapshot.PackageCode) currentPackageCode := strings.TrimSpace(prepared.PackageCode) if targetPackageCode != "" && currentPackageCode != "" && targetPackageCode != currentPackageCode { renewableCodes, renewablePackages, err := renewablePackageOptionsTx(ctx, tx, operator.TenantId, prepared.CustomerAgentId, currentPackageCode) if err != nil { return nil, err } if !containsRenewPackageCode(renewableCodes, targetPackageCode) { return nil, gerror.New("续费套餐不在当前套餐可续费范围内") } snapshot.RenewablePackageCodes = renewableCodes snapshot.RenewablePackages = renewablePackages } } if snapshot == nil || strings.TrimSpace(snapshot.PackageCode) == "" || snapshot.RenewUnitPrice < 0 || len(snapshot.RenewableDurationOptions) == 0 || snapshot.BillingMethod == 0 || strings.TrimSpace(snapshot.RenewDurationUnit) == "" { var err error snapshot, err = buildRenewPriceSnapshotTx(ctx, tx, operator.TenantId, prepared.CustomerAgentId, prepared.PackageCode) if err != nil { return nil, err } } if snapshot.CustomerAgentId <= 0 { snapshot.CustomerAgentId = prepared.CustomerAgentId } if strings.TrimSpace(snapshot.PackageCode) == "" { snapshot.PackageCode = prepared.PackageCode } if len(snapshot.RenewablePackageCodes) == 0 { renewableCodes, renewablePackages, err := renewablePackageOptionsTx(ctx, tx, operator.TenantId, prepared.CustomerAgentId, prepared.PackageCode) if err != nil { return nil, err } snapshot.RenewablePackageCodes = renewableCodes snapshot.RenewablePackages = renewablePackages } // Keep old batch snapshots readable, but never let the legacy billing method // control renewal or flow-cycle behavior. snapshot.BillingMethod = consts.BillingMethodMonth snapshot.RenewDurationUnit = consts.RenewDurationUnitYear snapshot.RenewDurationUnitText = consts.RenewDurationUnitText(consts.RenewDurationUnitYear) snapshot.RenewBeginDate = prepared.RenewBeginDate snapshot.RenewBeginDateMin = prepared.RenewBeginDateMin snapshot.RenewBeginDateMax = prepared.RenewBeginDateMax snapshot.SameRenewBeginDate = prepared.SameRenewBeginDate return snapshot, nil } func executeRenewPayment(ctx context.Context, tx gdb.TX, in paymentExecutionInput) (*model.TSimCardRenewPaymentSnapshot, error) { if in.SubmitParam == nil { return nil, gerror.New("续费付款参数不能为空") } isTop, err := operatorIsTopDept(ctx, tx, in.Operator) if err != nil { return nil, err } mode, err := normalizePaymentMode(firstNonBlank(in.SubmitParam.PaymentMode, paymentModeOf(in.SubmitParam.PaymentSnapshot)), isTop) if err != nil { return nil, err } amount, err := resolveRenewAmount(in.PriceSnapshot, in.SubmitParam, in.Quantity) if err != nil { return nil, err } now := gtime.Now() switch mode { case renewPaymentModeOffline: if !in.SubmitParam.OfflineContractCompleted { return nil, gerror.New("请确认线下合同、销售和走账已完成") } return &model.TSimCardRenewPaymentSnapshot{ PaymentMode: renewPaymentModeOffline, PayType: consts.PayTypeOffline, PayStatus: consts.PayStatusPaid, PayAmount: amount, OfflinePaidAmount: amount, PaymentTime: now, PayerDeptId: renewPayerDeptID(in.PriceSnapshot, in.Operator.DeptId), PayerUserId: in.Operator.UserId, IsTopDept: isTop, }, nil case renewPaymentModeBalance: payerDeptID := renewPayerDeptID(in.PriceSnapshot, in.Operator.DeptId) return deductRenewBalanceTx(ctx, tx, in.BatchNo, amount, now, in.Operator, payerDeptID, isTop) default: return nil, gerror.New("续费付款方式不正确") } } func deductRenewBalanceTx(ctx context.Context, tx gdb.TX, batchNo string, amount float64, paymentTime *gtime.Time, operator batchimport.OperatorContext, payerDeptID int64, isTop bool) (*model.TSimCardRenewPaymentSnapshot, error) { if amount < 0 { return nil, gerror.New("续费金额不正确") } if operator.DeptId <= 0 || operator.TenantId == "" || operator.UserId <= 0 { return nil, gerror.New("当前用户未绑定组织或租户") } if payerDeptID <= 0 { payerDeptID = operator.DeptId } if paymentTime == nil { paymentTime = gtime.Now() } if amount == 0 { balance, _ := currentAgentBalanceTx(ctx, tx, operator, payerDeptID) return &model.TSimCardRenewPaymentSnapshot{ PaymentMode: renewPaymentModeBalance, PayType: consts.PayTypeBalance, PayStatus: consts.PayStatusPaid, PayAmount: 0, BalanceAmount: 0, BalanceBefore: balance, BalanceAfter: balance, PaymentTime: paymentTime, PayerDeptId: payerDeptID, PayerUserId: operator.UserId, IsTopDept: isTop, }, nil } serialNo := renewBalanceSerialNo(batchNo) if serialNo == "" { return nil, gerror.New("续费余额流水号生成失败") } account := &entity.TAgentBalance{} if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()). Where(dao.TAgentBalance.Columns().DeptId, payerDeptID). Where(dao.TAgentBalance.Columns().TenantId, operator.TenantId). LockUpdate(). Scan(account); err != nil { return nil, gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return nil, gerror.New("账户余额不存在,请先充值") } if account.Status != renewFinanceAccountStatusNormal { return nil, gerror.New("账户余额已冻结,不能扣余额") } record, err := tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()). Where(dao.TBalanceDetail.Columns().TenantId, operator.TenantId). Where(dao.TBalanceDetail.Columns().SerialNo, serialNo). LockUpdate(). One() if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "查询续费余额流水失败") } if record != nil && !record.IsEmpty() { return nil, gerror.New("当前续费批次已扣款,请勿重复提交") } before := roundRenewAmount(account.Balance) if before+0.0001 < amount { return nil, gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, before) } after := roundRenewAmount(before - amount) now := gtime.Now() result, err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()). WherePri(account.Id). Data(g.Map{ dao.TAgentBalance.Columns().Balance: after, dao.TAgentBalance.Columns().UpdatedBy: operator.UserId, dao.TAgentBalance.Columns().UpdatedAt: now, }). Update() if err != nil { return nil, gerror.Wrap(err, "扣减账户余额失败") } affected, err := result.RowsAffected() if err != nil { return nil, gerror.Wrap(err, "读取账户余额更新结果失败") } if affected != 1 { return nil, gerror.New("账户余额已变化,请重试") } if _, err = tx.Ctx(ctx).Model(dao.TBalanceDetail.Table()).Data(g.Map{ dao.TBalanceDetail.Columns().DeptId: payerDeptID, dao.TBalanceDetail.Columns().SerialNo: serialNo, dao.TBalanceDetail.Columns().TransactionTime: paymentTime, dao.TBalanceDetail.Columns().IncomeExpenseType: renewFinanceIncomeExpenseExpense, dao.TBalanceDetail.Columns().TransactionType: renewFinanceTransactionTypeConsume, dao.TBalanceDetail.Columns().TransactionChannel: renewFinanceTransactionChannelBalance, dao.TBalanceDetail.Columns().TransactionStatus: renewFinanceTransactionStatusPaid, dao.TBalanceDetail.Columns().Amount: amount, dao.TBalanceDetail.Columns().BeforeBalance: before, dao.TBalanceDetail.Columns().BalanceAfter: after, dao.TBalanceDetail.Columns().OrderSn: batchNo, dao.TBalanceDetail.Columns().Remark: fmt.Sprintf("SIM卡续费余额支付,批次号:%s", batchNo), dao.TBalanceDetail.Columns().TenantId: operator.TenantId, dao.TBalanceDetail.Columns().CreatedDept: operator.DeptId, dao.TBalanceDetail.Columns().CreatedBy: operator.UserId, dao.TBalanceDetail.Columns().CreatedAt: now, }).Insert(); err != nil { return nil, gerror.Wrap(err, "写入续费余额流水失败") } return &model.TSimCardRenewPaymentSnapshot{ PaymentMode: renewPaymentModeBalance, PayType: consts.PayTypeBalance, PayStatus: consts.PayStatusPaid, PayAmount: amount, BalanceAmount: amount, BalanceBefore: before, BalanceAfter: after, BalanceDetailSerialNo: serialNo, PaymentTime: paymentTime, PayerDeptId: payerDeptID, PayerUserId: operator.UserId, IsTopDept: isTop, }, nil } func buildRenewPriceSnapshotTx(ctx context.Context, tx gdb.TX, tenantID string, customerAgentID int64, packageCode string) (*model.TSimCardRenewPriceSnapshot, error) { packageCode = strings.TrimSpace(packageCode) if tenantID == "" { return nil, gerror.New("租户不能为空") } if customerAgentID <= 0 { return nil, gerror.New("客户不能为空") } if packageCode == "" { return nil, gerror.New("套餐不能为空") } pkg := &entity.TSimFlowPackageInfo{} if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()). Where(dao.TSimFlowPackageInfo.Columns().TenantId, tenantID). Where(dao.TSimFlowPackageInfo.Columns().PackageCode, packageCode). Scan(pkg); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, gerror.New("续费套餐不存在") } return nil, gerror.Wrap(err, "查询续费套餐失败") } if pkg.Id == 0 { return nil, gerror.New("续费套餐不存在") } options, err := renewableDurationOptionsTx(pkg) if err != nil { return nil, err } durationUnit := consts.RenewDurationUnitYear snapshot := &model.TSimCardRenewPriceSnapshot{ CustomerAgentId: customerAgentID, PackageCode: pkg.PackageCode, PackageName: pkg.PackageName, BillingMethod: consts.BillingMethodMonth, RenewDurationUnit: durationUnit, RenewDurationUnitText: consts.RenewDurationUnitText(durationUnit), PriceSource: "system", RenewUnitPrice: roundRenewAmount(pkg.CostPrice*normalizeRenewPriceRatio(pkg.SysDefaultRatio) + pkg.SysDefaultProfit), RenewableDurationOptions: options, } renewablePackageCodes, renewablePackages, err := renewablePackageOptionsTx(ctx, tx, tenantID, customerAgentID, packageCode) if err != nil { return nil, err } snapshot.RenewablePackageCodes = renewablePackageCodes snapshot.RenewablePackages = renewablePackages priceOwnerDeptID, err := loadRenewPriceOwnerDeptIDTx(ctx, tx, tenantID, customerAgentID) if err != nil { return nil, err } snapshot.PriceOwnerDeptId = priceOwnerDeptID salePrice, err := packageprice.ResolveEffectiveSalePrice(ctx, tx, tenantID, priceOwnerDeptID, customerAgentID, packageprice.SourceFromPackageInfo(pkg)) if err != nil { return nil, err } snapshot.PriceSource = salePrice.Rule.PriceSource snapshot.AgentDefaultPriceId = salePrice.Rule.AgentDefaultPriceId snapshot.TargetPriceId = salePrice.Rule.TargetPriceId snapshot.RenewUnitPrice = salePrice.AgentShipmentPrice return snapshot, nil } func loadRenewPriceOwnerDeptIDTx(ctx context.Context, tx gdb.TX, tenantID string, customerAgentID int64) (int64, error) { dept := &entity.SysDept{} if err := tx.Ctx(ctx).Model(dao.SysDept.Table()). Where(dao.SysDept.Columns().TenantId, tenantID). Where(dao.SysDept.Columns().DeptId, customerAgentID). WhereNull(dao.SysDept.Columns().DeletedAt). Scan(dept); err != nil { if errors.Is(err, sql.ErrNoRows) { return 0, gerror.New("客户部门不存在") } return 0, gerror.Wrap(err, "查询客户上级部门失败") } if dept.DeptId == 0 { return 0, gerror.New("客户部门不存在") } return dept.ParentId, nil } func loadRenewPriceOverrideTx(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, scopeType int, targetAgentID int64, packageCode string) (*entity.TSimFlowPackagePrice, error) { price := &entity.TSimFlowPackagePrice{} if err := tx.Ctx(ctx).Model(dao.TSimFlowPackagePrice.Table()). Where(dao.TSimFlowPackagePrice.Columns().TenantId, tenantID). Where(dao.TSimFlowPackagePrice.Columns().DeptId, deptID). Where(dao.TSimFlowPackagePrice.Columns().AgentScopeType, scopeType). Where(dao.TSimFlowPackagePrice.Columns().TargetAgentId, targetAgentID). Where(dao.TSimFlowPackagePrice.Columns().PackageCode, packageCode). Where(dao.TSimFlowPackagePrice.Columns().Enabled, 1). Scan(price); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, gerror.Wrap(err, "查询代理续费定价失败") } if price.Id == 0 { return nil, nil } return price, nil } type renewPackageEffectiveSalePrice struct { Rule renewPackageEffectivePriceRule AgentShipmentPrice float64 } type renewPackageEffectivePriceRule struct { PriceSource string AgentDefaultPriceId int64 TargetPriceId int64 AgentCostRatio float64 AgentCostProfit float64 } func resolvePackageEffectiveSalePrice(ctx context.Context, tx gdb.TX, tenantID string, ownerDeptID int64, targetDeptID int64, pkg *entity.TSimFlowPackageInfo) (renewPackageEffectiveSalePrice, error) { baseCost, err := resolvePackageEffectiveAgentBaseCost(ctx, tx, tenantID, ownerDeptID, pkg, make(map[int64]struct{})) if err != nil { return renewPackageEffectiveSalePrice{}, err } rule, err := resolvePackageEffectivePriceRule(ctx, tx, tenantID, ownerDeptID, targetDeptID, pkg) if err != nil { return renewPackageEffectiveSalePrice{}, err } return renewPackageEffectiveSalePrice{ Rule: rule, AgentShipmentPrice: roundRenewAmount(baseCost*rule.AgentCostRatio + rule.AgentCostProfit), }, nil } func resolvePackageEffectiveAgentBaseCost(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, pkg *entity.TSimFlowPackageInfo, visited map[int64]struct{}) (float64, error) { if pkg == nil || strings.TrimSpace(pkg.PackageCode) == "" { return 0, gerror.New("套餐编码不能为空") } if deptID <= 0 { return roundRenewAmount(pkg.CostPrice), nil } if _, ok := visited[deptID]; ok { return 0, gerror.New("组织定价层级存在循环,无法计算生效成本") } visited[deptID] = struct{}{} parentID, err := loadRenewPriceOwnerDeptIDTx(ctx, tx, tenantID, deptID) if err != nil { return 0, err } if parentID <= 0 { return roundRenewAmount(pkg.CostPrice), nil } parentBaseCost, err := resolvePackageEffectiveAgentBaseCost(ctx, tx, tenantID, parentID, pkg, visited) if err != nil { return 0, err } parentRule, err := resolvePackageEffectivePriceRule(ctx, tx, tenantID, parentID, deptID, pkg) if err != nil { return 0, err } return roundRenewAmount(parentBaseCost*parentRule.AgentCostRatio + parentRule.AgentCostProfit), nil } func resolvePackageEffectivePriceRule(ctx context.Context, tx gdb.TX, tenantID string, ownerDeptID int64, targetDeptID int64, pkg *entity.TSimFlowPackageInfo) (renewPackageEffectivePriceRule, error) { rule := renewPackageEffectivePriceRule{ PriceSource: "system", AgentCostRatio: normalizeRenewPriceRatio(pkg.SysDefaultRatio), AgentCostProfit: pkg.SysDefaultProfit, } if ownerDeptID <= 0 { return rule, nil } if targetDeptID > 0 { targetPrice, err := loadRenewPriceOverrideTx(ctx, tx, tenantID, ownerDeptID, 1, targetDeptID, pkg.PackageCode) if err != nil { return rule, err } if targetPrice != nil { return renewPackageEffectivePriceRule{ PriceSource: "target", TargetPriceId: targetPrice.Id, AgentCostRatio: normalizeRenewPriceRatio(targetPrice.AgentCostRatio), AgentCostProfit: targetPrice.AgentCostProfit, }, nil } } defaultPrice, err := loadRenewPriceOverrideTx(ctx, tx, tenantID, ownerDeptID, 0, 0, pkg.PackageCode) if err != nil { return rule, err } if defaultPrice != nil { return renewPackageEffectivePriceRule{ PriceSource: "agentDefault", AgentDefaultPriceId: defaultPrice.Id, AgentCostRatio: normalizeRenewPriceRatio(defaultPrice.AgentCostRatio), AgentCostProfit: defaultPrice.AgentCostProfit, }, nil } return rule, nil } func renewableDurationOptionsTx(pkg *entity.TSimFlowPackageInfo) ([]int, error) { return packageduration.RenewableYears(pkg) } func renewablePackageOptionsTx(ctx context.Context, tx gdb.TX, tenantID string, customerAgentID int64, currentPackageCode string) ([]string, []model.TSimCardRenewPackageOption, error) { currentPackageCode = strings.TrimSpace(currentPackageCode) if tenantID == "" || currentPackageCode == "" { return nil, nil, nil } current := &entity.TSimFlowPackageInfo{} if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()). Fields( dao.TSimFlowPackageInfo.Columns().PackageCode, dao.TSimFlowPackageInfo.Columns().PackageName, dao.TSimFlowPackageInfo.Columns().RenewablePackageCodes, ). Where(dao.TSimFlowPackageInfo.Columns().TenantId, tenantID). Where(dao.TSimFlowPackageInfo.Columns().PackageCode, currentPackageCode). Scan(current); err != nil { return nil, nil, gerror.Wrap(err, "查询可续费套餐配置失败") } if current.PackageCode == "" { return nil, nil, gerror.New("续费套餐不存在") } codes := renewablePackageCodesFromPackage(current) if len(codes) == 0 { return nil, nil, gerror.New("当前套餐未配置可续费套餐") } var rows []*entity.TSimFlowPackageInfo if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()). Fields( dao.TSimFlowPackageInfo.Columns().PackageCode, dao.TSimFlowPackageInfo.Columns().PackageName, dao.TSimFlowPackageInfo.Columns().CostPrice, dao.TSimFlowPackageInfo.Columns().TerminalCost, dao.TSimFlowPackageInfo.Columns().SysDefaultRatio, dao.TSimFlowPackageInfo.Columns().SysDefaultProfit, dao.TSimFlowPackageInfo.Columns().SysDefaultTerminalRatio, dao.TSimFlowPackageInfo.Columns().SysDefaultTerminalProfit, dao.TSimFlowPackageInfo.Columns().RenewableDurationConfig, ). Where(dao.TSimFlowPackageInfo.Columns().TenantId, tenantID). WhereIn(dao.TSimFlowPackageInfo.Columns().PackageCode, codes). Scan(&rows); err != nil { return nil, nil, gerror.Wrap(err, "查询可续费套餐列表失败") } nameByCode := make(map[string]string, len(rows)) packageByCode := make(map[string]*entity.TSimFlowPackageInfo, len(rows)) for _, row := range rows { if row == nil { continue } code := strings.TrimSpace(row.PackageCode) nameByCode[code] = strings.TrimSpace(row.PackageName) packageByCode[code] = row } priceOwnerDeptID, err := loadRenewPriceOwnerDeptIDTx(ctx, tx, tenantID, customerAgentID) if err != nil { return nil, nil, err } options := make([]model.TSimCardRenewPackageOption, 0, len(codes)) for _, code := range codes { name, ok := nameByCode[code] if !ok { continue } pkg := packageByCode[code] salePrice, err := packageprice.ResolveEffectiveSalePrice(ctx, tx, tenantID, priceOwnerDeptID, customerAgentID, packageprice.SourceFromPackageInfo(pkg)) if err != nil { return nil, nil, err } unitPrice := salePrice.AgentShipmentPrice durationOptions, err := renewableDurationOptionsTx(pkg) if err != nil { return nil, nil, err } options = append(options, model.TSimCardRenewPackageOption{ PackageCode: code, PackageName: name, RenewUnitPrice: unitPrice, RenewableDurationOptions: durationOptions, }) } return codes, options, nil } func renewablePackageCodesFromPackage(pkg *entity.TSimFlowPackageInfo) []string { if pkg == nil { return nil } var codes []string if pkg.RenewablePackageCodes != nil { _ = pkg.RenewablePackageCodes.Scan(&codes) } return uniqueStrings(codes) } func containsRenewPackageCode(codes []string, target string) bool { target = strings.TrimSpace(target) if target == "" { return false } for _, code := range codes { if strings.TrimSpace(code) == target { return true } } return false } func operatorIsTopDept(ctx context.Context, tx gdb.TX, operator batchimport.OperatorContext) (bool, error) { if operator.DeptId <= 0 || operator.TenantId == "" { return false, gerror.New("当前用户未绑定组织或租户") } 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, operator.DeptId). WhereNull(dao.SysDept.Columns().DeletedAt). Scan(dept); err != nil { return false, gerror.Wrap(err, "查询当前组织失败") } if dept.DeptId == 0 { return false, gerror.New("当前组织不存在") } return dept.ParentId == 0 || strings.Trim(dept.Ancestors, ",") == "0", nil } func currentAgentBalanceTx(ctx context.Context, tx gdb.TX, operator batchimport.OperatorContext, deptID int64) (float64, error) { if deptID <= 0 { deptID = operator.DeptId } account := &entity.TAgentBalance{} if err := tx.Ctx(ctx).Model(dao.TAgentBalance.Table()). Fields(dao.TAgentBalance.Columns().Id, dao.TAgentBalance.Columns().Balance). Where(dao.TAgentBalance.Columns().DeptId, deptID). Where(dao.TAgentBalance.Columns().TenantId, operator.TenantId). Scan(account); err != nil { return 0, gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return 0, nil } return roundRenewAmount(account.Balance), nil } func renewPayerDeptID(snapshot *model.TSimCardRenewPriceSnapshot, fallbackDeptID int64) int64 { if snapshot != nil && snapshot.CustomerAgentId > 0 { return snapshot.CustomerAgentId } return fallbackDeptID } func renewDurationUnitFromSnapshot(snapshot *model.TSimCardRenewPriceSnapshot) string { return consts.RenewDurationUnitYear } func renewDisplayDurationValue(in *model.TSimCardRenewBatchSubmitParam) int { if in == nil { return 0 } if in.DurationYears > 0 { return in.DurationYears } return renewDurationYearsFromLegacy(in.DurationMonths, 0, nil) } func renewActualDurationMonths(snapshot *model.TSimCardRenewPriceSnapshot, displayValue int) int { return packageduration.RenewMonths(displayValue) } func renewDurationYearsFromLegacy(durationMonths, actualDurationMonths int, snapshot *model.TSimCardRenewPriceSnapshot) int { if actualDurationMonths > 0 { return renewDurationYearsFromMonths(actualDurationMonths, snapshot) } if durationMonths > 0 { return renewDurationYearsFromMonths(durationMonths, snapshot) } return 0 } func renewDurationYearsFromMonths(months int, snapshot *model.TSimCardRenewPriceSnapshot) int { if months <= 0 { return 0 } if snapshot != nil { options := packageduration.NormalizeRenewableYears(snapshot.RenewableDurationOptions) if containsRenewDurationOption(options, months) { return months } if months%12 == 0 && containsRenewDurationOption(options, months/12) { return months / 12 } } if months%12 == 0 { return months / 12 } return months } func normalizePaymentMode(value string, isTopDept bool) (string, error) { mode := strings.TrimSpace(value) if mode == "" { if isTopDept { return renewPaymentModeOffline, nil } return renewPaymentModeBalance, nil } switch mode { case renewPaymentModeOffline: if !isTopDept { return "", gerror.New("当前组织续费必须使用余额支付") } return mode, nil case renewPaymentModeBalance: return mode, nil default: return "", gerror.New("续费付款方式不正确") } } func resolveRenewAmount(snapshot *model.TSimCardRenewPriceSnapshot, in *model.TSimCardRenewBatchSubmitParam, quantity int) (float64, error) { if in == nil { return 0, gerror.New("续费付款参数不能为空") } durationValue := renewDisplayDurationValue(in) if snapshot != nil && snapshot.RenewUnitPrice >= 0 && durationValue > 0 && quantity > 0 { return roundRenewAmount(snapshot.RenewUnitPrice * float64(durationValue) * float64(quantity)), nil } amount, err := parseRenewAmount(in.Amount) if err != nil { return 0, err } return amount, nil } func containsRenewDurationOption(values []int, target int) bool { for _, value := range values { if value == target { return true } } return false } func parseRenewAmount(value string) (float64, error) { value = strings.TrimSpace(value) if value == "" { return 0, nil } amount, err := strconv.ParseFloat(value, 64) if err != nil { return 0, gerror.New("订单金额格式不正确") } if amount < 0 { return 0, gerror.New("订单金额不能小于0") } return roundRenewAmount(amount), nil } func formatRenewAmount(amount float64) string { return fmt.Sprintf("%.2f", roundRenewAmount(amount)) } func renewBalanceSerialNo(batchNo string) string { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return "" } return "RENPAY" + batchNo } func roundRenewAmount(value float64) float64 { return math.Round(value*100) / 100 } func normalizeRenewPriceRatio(ratio float64) float64 { if ratio > 10 { return ratio / 100 } return ratio } func priceSnapshotFromInfo(info *gjson.Json) *model.TSimCardRenewPriceSnapshot { if info == nil { return nil } options := packageduration.NormalizeRenewableYears(jsonInts(info, "renewableDurationOptions", "priceSnapshot.renewableDurationOptions")) snapshot := &model.TSimCardRenewPriceSnapshot{ CustomerAgentId: jsonInt64(info, "customerAgentId", "customer.deptId", "priceSnapshot.customerAgentId"), PriceOwnerDeptId: jsonInt64(info, "priceOwnerDeptId", "priceSnapshot.priceOwnerDeptId"), PackageCode: jsonString(info, "packageCode", "priceSnapshot.packageCode"), PackageName: jsonString(info, "packageName", "priceSnapshot.packageName"), BillingMethod: consts.BillingMethodMonth, RenewDurationUnit: jsonString(info, "renewDurationUnit", "priceSnapshot.renewDurationUnit"), RenewDurationUnitText: jsonString(info, "renewDurationUnitText", "priceSnapshot.renewDurationUnitText"), PriceSource: jsonString(info, "priceSource", "priceSnapshot.priceSource"), AgentDefaultPriceId: jsonInt64(info, "agentDefaultPriceId", "priceSnapshot.agentDefaultPriceId"), TargetPriceId: jsonInt64(info, "targetPriceId", "priceSnapshot.targetPriceId"), RenewUnitPrice: roundRenewAmount(jsonFloat64(info, "renewUnitPrice", "priceSnapshot.renewUnitPrice")), RenewableDurationOptions: options, RenewablePackageCodes: jsonStrings(info, "renewablePackageCodes", "priceSnapshot.renewablePackageCodes"), RenewablePackages: renewPackageOptionsFromInfo(info, "renewablePackages", "priceSnapshot.renewablePackages"), RenewBeginDate: jsonString(info, "renewBeginDate", "priceSnapshot.renewBeginDate"), RenewBeginDateMin: jsonString(info, "renewBeginDateMin", "priceSnapshot.renewBeginDateMin"), RenewBeginDateMax: jsonString(info, "renewBeginDateMax", "priceSnapshot.renewBeginDateMax"), SameRenewBeginDate: jsonBool(info, "sameRenewBeginDate", "priceSnapshot.sameRenewBeginDate"), } if snapshot.CustomerAgentId == 0 && snapshot.PackageCode == "" && snapshot.RenewUnitPrice == 0 && len(snapshot.RenewableDurationOptions) == 0 { return nil } snapshot.RenewDurationUnit = consts.RenewDurationUnitYear snapshot.RenewDurationUnitText = consts.RenewDurationUnitText(snapshot.RenewDurationUnit) snapshot.BillingMethod = consts.BillingMethodMonth return snapshot } func paymentSnapshotFromInfo(info *gjson.Json) *model.TSimCardRenewPaymentSnapshot { if info == nil { return nil } mode := jsonString(info, "paymentMode", "paymentSnapshot.paymentMode") if mode == "" { return nil } return &model.TSimCardRenewPaymentSnapshot{ PaymentMode: mode, PayType: jsonInt(info, "payType", "paymentSnapshot.payType"), PayStatus: jsonInt(info, "payStatus", "paymentSnapshot.payStatus"), PayAmount: jsonFloat64(info, "payAmount", "paymentSnapshot.payAmount"), BalanceAmount: jsonFloat64(info, "balanceAmount", "paymentSnapshot.balanceAmount"), OfflinePaidAmount: jsonFloat64(info, "offlinePaidAmount", "paymentSnapshot.offlinePaidAmount"), BalanceBefore: jsonFloat64(info, "balanceBefore", "paymentSnapshot.balanceBefore"), BalanceAfter: jsonFloat64(info, "balanceAfter", "paymentSnapshot.balanceAfter"), BalanceDetailSerialNo: jsonString(info, "balanceDetailSerialNo", "paymentSnapshot.balanceDetailSerialNo"), PayerDeptId: jsonInt64(info, "payerDeptId", "paymentSnapshot.payerDeptId"), PayerUserId: jsonInt64(info, "payerUserId", "paymentSnapshot.payerUserId"), IsTopDept: jsonBool(info, "isTopDept", "paymentSnapshot.isTopDept"), } } func paymentModeOf(snapshot *model.TSimCardRenewPaymentSnapshot) string { if snapshot == nil { return "" } return strings.TrimSpace(snapshot.PaymentMode) } func firstNonBlank(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func jsonString(info *gjson.Json, paths ...string) string { for _, path := range paths { if value := info.Get(path); value != nil && !value.IsNil() { return strings.TrimSpace(value.String()) } } return "" } func jsonInt(info *gjson.Json, paths ...string) int { for _, path := range paths { if value := info.Get(path); value != nil && !value.IsNil() { return value.Int() } } return 0 } func jsonInt64(info *gjson.Json, paths ...string) int64 { for _, path := range paths { if value := info.Get(path); value != nil && !value.IsNil() { return value.Int64() } } return 0 } func jsonFloat64(info *gjson.Json, paths ...string) float64 { for _, path := range paths { if value := info.Get(path); value != nil && !value.IsNil() { return value.Float64() } } return 0 } func jsonBool(info *gjson.Json, paths ...string) bool { for _, path := range paths { if value := info.Get(path); value != nil && !value.IsNil() { return value.Bool() } } return false } func jsonStrings(info *gjson.Json, paths ...string) []string { if info == nil { return nil } for _, path := range paths { value := info.Get(path) if value == nil || value.IsNil() { continue } values := value.Strings() if len(values) > 0 || len(value.Interfaces()) > 0 { return uniqueStrings(values) } } return nil } func renewPackageOptionsFromInfo(info *gjson.Json, paths ...string) []model.TSimCardRenewPackageOption { if info == nil { return nil } for _, path := range paths { value := info.Get(path) if value == nil || value.IsNil() { continue } var options []model.TSimCardRenewPackageOption if err := gconv.Structs(value.Interfaces(), &options); err == nil && len(options) > 0 { return options } } return nil } func jsonInts(info *gjson.Json, paths ...string) []int { if info == nil { return nil } for _, path := range paths { value := info.Get(path) if value == nil || value.IsNil() { continue } values := value.Ints() if len(values) > 0 || len(value.Interfaces()) > 0 { return values } } return nil }