Files
iotlcm/server/internal/logic/iot/simcardbatch/renew/payment.go
T
MiFaZhan 59eb34976a chore: 初始开源发布
iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
2026-10-23 14:48:30 +08:00

1042 lines
36 KiB
Go

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
}