package iot import ( "context" "database/sql" "errors" "fmt" "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/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) const ( renewPaymentModeOffline = "offline" renewPaymentModeBalance = "balance" renewFinanceIncomeExpenseIncome = 0 renewFinanceIncomeExpenseExpense = 1 renewFinanceTransactionTypeConsume = 1 renewFinanceTransactionChannelBalance = 3 renewFinanceTransactionStatusPaid = 1 renewFinanceAccountStatusNormal = 0 ) func renewTotalAmount(snapshot *model.TSimCardRenewPriceSnapshot, durationValue, quantity int) float64 { if snapshot == nil || durationValue <= 0 || quantity <= 0 { return 0 } return roundAmount(snapshot.RenewUnitPrice * float64(durationValue) * float64(quantity)) } func renewDefaultPaymentMode(isTopDept bool) string { if isTopDept { return renewPaymentModeOffline } return renewPaymentModeBalance } func normalizeRenewPaymentMode(value string, isTopDept bool) (string, error) { mode := strings.TrimSpace(value) if mode == "" { mode = renewDefaultPaymentMode(isTopDept) } switch mode { case renewPaymentModeOffline: if !isTopDept { return "", gerror.New("当前组织续费必须使用余额支付") } return mode, nil case renewPaymentModeBalance: return mode, nil default: return "", gerror.New("续费付款方式不正确") } } func (s *sIotTSimCardRenew) fillRenewPaymentView(ctx context.Context, res *model.TSimCardRenewBatchPrepareRes) { if res == nil { return } isTop, _, err := service.AgentDept().IsTopDept(ctx) if err != nil { g.Log().Warningf(ctx, "load renew payment view top dept failed: %+v", err) } res.IsTopDept = isTop res.DefaultPaymentMode = renewDefaultPaymentMode(isTop) balance, err := s.currentAgentBalance(ctx, res.CustomerAgentId) if err != nil { g.Log().Warningf(ctx, "load renew payment view balance failed: %+v", err) return } res.AvailableBalance = balance } func (s *sIotTSimCardRenew) fillRenewPrepareResultPaymentView(ctx context.Context, res *model.TSimCardRenewPrepareResultRes) { if res == nil { return } if len(res.RenewablePackages) == 0 { s.fillRenewPrepareResultPackages(ctx, res) } isTop, _, err := service.AgentDept().IsTopDept(ctx) if err != nil { g.Log().Warningf(ctx, "load renew prepare result top dept failed: %+v", err) } res.IsTopDept = isTop res.DefaultPaymentMode = renewDefaultPaymentMode(isTop) balance, err := s.currentAgentBalance(ctx, res.CustomerAgentId) if err != nil { g.Log().Warningf(ctx, "load renew prepare result balance failed: %+v", err) return } res.AvailableBalance = balance } func (s *sIotTSimCardRenew) fillRenewPrepareResultPackages(ctx context.Context, res *model.TSimCardRenewPrepareResultRes) { if res == nil || strings.TrimSpace(res.PackageCode) == "" || res.CustomerAgentId <= 0 { return } err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { codes, packages, err := s.loadRenewablePackageOptions(ctx, tx, res.CustomerAgentId, res.PackageCode) if err != nil { return err } res.RenewablePackageCodes = codes res.RenewablePackages = packages if len(res.RenewableDurationOptions) == 0 { for _, item := range packages { if strings.TrimSpace(item.PackageCode) == strings.TrimSpace(res.PackageCode) { res.RenewableDurationOptions = item.RenewableDurationOptions break } } } return nil }) if err != nil { g.Log().Warningf(ctx, "load renew prepare result packages failed, batchNo=%s, packageCode=%s, err=%+v", res.BatchNo, res.PackageCode, err) } } func (s *sIotTSimCardRenew) currentAgentBalance(ctx context.Context, deptId int64) (float64, error) { if deptId <= 0 { deptId = contexts.GetDeptId(ctx) } tenantId := contexts.GetTenantId(ctx) if deptId <= 0 || tenantId == "" { return 0, gerror.New("当前用户未绑定组织或租户") } account := &entity.TAgentBalance{} if err := dao.TAgentBalance.Ctx(ctx). Fields(dao.TAgentBalance.Columns().Id, dao.TAgentBalance.Columns().Balance). Where(dao.TAgentBalance.Columns().DeptId, deptId). Where(dao.TAgentBalance.Columns().TenantId, tenantId). Scan(account); err != nil { return 0, gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return 0, nil } return roundAmount(account.Balance), nil } func (s *sIotTSimCardRenew) buildRenewPaymentSnapshot( ctx context.Context, tx gdb.TX, batchNo string, snapshot *model.TSimCardRenewPriceSnapshot, quantity int, in *model.TSimCardRenewBatchConfirmParam, ) (*model.TSimCardRenewPaymentSnapshot, error) { if in == nil { return nil, gerror.New("续费确认参数不能为空") } isTop, _, err := service.AgentDept().IsTopDept(ctx) if err != nil { return nil, gerror.Wrap(err, "判断当前组织类型失败") } mode, err := normalizeRenewPaymentMode(in.PaymentMode, isTop) if err != nil { return nil, err } durationValue := in.DurationYears if durationValue <= 0 { durationValue = normalizeLegacyRenewYears(in.DurationMonths) } amount := renewTotalAmount(snapshot, durationValue, quantity) if amount < 0 { return nil, gerror.New("续费金额不正确") } paymentTime := gtime.Now() switch mode { case renewPaymentModeOffline: if !in.OfflineContractCompleted { return nil, gerror.New("请确认线下合同、销售和走账已完成") } return &model.TSimCardRenewPaymentSnapshot{ PaymentMode: renewPaymentModeOffline, PayType: consts.PayTypeOffline, PayStatus: consts.PayStatusPaid, PayAmount: amount, OfflinePaidAmount: amount, PaymentTime: paymentTime, PayerDeptId: renewPaymentPayerDeptID(snapshot, contexts.GetDeptId(ctx)), PayerUserId: contexts.GetUserId(ctx), IsTopDept: isTop, }, nil case renewPaymentModeBalance: payerDeptID := renewPaymentPayerDeptID(snapshot, contexts.GetDeptId(ctx)) if err := s.ensureRenewBalanceAccountUsable(ctx, tx, payerDeptID); err != nil { return nil, err } balance, err := s.currentAgentBalanceForRenew(ctx, tx, payerDeptID) if err != nil { return nil, err } if balance+0.0001 < amount { return nil, gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, balance) } return &model.TSimCardRenewPaymentSnapshot{ PaymentMode: renewPaymentModeBalance, PayType: consts.PayTypeBalance, PayAmount: amount, BalanceAmount: amount, BalanceBefore: balance, BalanceAfter: balance, PayerDeptId: payerDeptID, PayerUserId: contexts.GetUserId(ctx), IsTopDept: isTop, }, nil default: return nil, gerror.New("续费付款方式不正确") } } func (s *sIotTSimCardRenew) currentAgentBalanceForRenew(ctx context.Context, tx gdb.TX, deptId int64) (float64, error) { if deptId <= 0 { deptId = contexts.GetDeptId(ctx) } tenantId := contexts.GetTenantId(ctx) if deptId <= 0 || tenantId == "" { return 0, gerror.New("当前用户未绑定组织或租户") } account := &entity.TAgentBalance{} mod := dao.TAgentBalance.Ctx(ctx) if tx != nil { mod = tx.Ctx(ctx).Model(dao.TAgentBalance.Table()) } if err := mod.Fields(dao.TAgentBalance.Columns().Id, dao.TAgentBalance.Columns().Balance). Where(dao.TAgentBalance.Columns().DeptId, deptId). Where(dao.TAgentBalance.Columns().TenantId, tenantId). Scan(account); err != nil { return 0, gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return 0, nil } return roundAmount(account.Balance), nil } func (s *sIotTSimCardRenew) ensureRenewBalanceAccountUsable(ctx context.Context, tx gdb.TX, deptId int64) error { if deptId <= 0 { deptId = contexts.GetDeptId(ctx) } tenantId := contexts.GetTenantId(ctx) if deptId <= 0 || tenantId == "" { return gerror.New("当前用户未绑定组织或租户") } account := &entity.TAgentBalance{} mod := dao.TAgentBalance.Ctx(ctx) if tx != nil { mod = tx.Ctx(ctx).Model(dao.TAgentBalance.Table()) } if err := mod.Fields(dao.TAgentBalance.Columns().Id, dao.TAgentBalance.Columns().Status). Where(dao.TAgentBalance.Columns().DeptId, deptId). Where(dao.TAgentBalance.Columns().TenantId, tenantId). Scan(account); err != nil { return gerror.Wrap(err, "查询账户余额失败") } if account.Id == 0 { return nil } if account.Status != renewFinanceAccountStatusNormal { return gerror.New("账户余额已冻结,不能扣余额") } return nil } func renewPaymentPayerDeptID(snapshot *model.TSimCardRenewPriceSnapshot, fallbackDeptID int64) int64 { if snapshot != nil && snapshot.CustomerAgentId > 0 { return snapshot.CustomerAgentId } return fallbackDeptID } func (s *sIotTSimCardRenew) deductRenewBalance(ctx context.Context, tx gdb.TX, batchNo string, amount float64, paymentTime *gtime.Time, payerDeptID int64, isTop bool) (*model.TSimCardRenewPaymentSnapshot, error) { if amount < 0 { return nil, gerror.New("续费金额不正确") } if tx == nil { return nil, gerror.New("续费余额支付必须在事务中执行") } deptId := contexts.GetDeptId(ctx) tenantId := contexts.GetTenantId(ctx) userId := contexts.GetUserId(ctx) if deptId <= 0 || tenantId == "" || userId <= 0 { return nil, gerror.New("当前用户未绑定组织、租户或用户信息") } if payerDeptID <= 0 { payerDeptID = deptId } if amount == 0 { balance, _ := s.currentAgentBalance(ctx, 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: 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, 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, 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 := roundAmount(account.Balance) if before+0.0001 < amount { return nil, gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, before) } after := roundAmount(before - amount) now := gtime.Now() if paymentTime == nil { paymentTime = 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: 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: tenantId, dao.TBalanceDetail.Columns().CreatedDept: deptId, dao.TBalanceDetail.Columns().CreatedBy: 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: userId, IsTopDept: isTop, }, nil } func renewBalanceSerialNo(batchNo string) string { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return "" } return "RENPAY" + batchNo }