2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
633 lines
21 KiB
Go
633 lines
21 KiB
Go
package renewchange
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
|
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
|
|
)
|
|
|
|
const (
|
|
renewFinanceAccountStatusNormal = 0
|
|
renewFinanceIncomeExpenseExpense = 1
|
|
)
|
|
|
|
type renewPaymentAccountRow struct {
|
|
ID int64 `orm:"id"`
|
|
Balance float64 `orm:"balance"`
|
|
Status int `orm:"status"`
|
|
}
|
|
|
|
func (r *RenewExecutor) ensureRenewExecutionContract(
|
|
ctx context.Context,
|
|
tx gdb.TX,
|
|
main *rc.BatchMainRow,
|
|
details []*rc.BatchDetailRow,
|
|
cards map[string]*rc.SimCardInfo,
|
|
binds map[string]*rc.SimCardBindPkg,
|
|
futureBinds map[string][]*rc.SimCardBindPkg,
|
|
now time.Time,
|
|
) (*ExecutionContract, error) {
|
|
if contract, err := ParseExecutionContract(main); err == nil {
|
|
if err = r.validateRenewRuntimePermissions(ctx, tx, main, contract); err != nil {
|
|
return nil, err
|
|
}
|
|
return contract, nil
|
|
}
|
|
|
|
info := main.BatchInfo
|
|
if info == nil || info.IsNil() {
|
|
return nil, gerror.New("续费批次缺少导入参数")
|
|
}
|
|
durationValue := firstInt(info, "durationValue", "durationYears")
|
|
if durationValue <= 0 {
|
|
durationValue = normalizeLegacyRenewDurationValue(firstInt(info, "durationMonths"))
|
|
}
|
|
if durationValue <= 0 {
|
|
return nil, gerror.New("续费时长不能为空")
|
|
}
|
|
giftDurationMonths := firstInt(info, "giftDurationMonths")
|
|
if giftDurationMonths < 0 {
|
|
return nil, gerror.New("赠送时长不能小于0")
|
|
}
|
|
|
|
iccids, err := strictIccidsFromDetails(details)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
customerID, packageCode, beginDates, err := runtimeRenewFacts(main, details, cards, binds, futureBinds, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkg, err := r.repo.Tables().LoadRenewPackage(ctx, tx, main.TenantId, packageCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pkg.Enabled != 1 {
|
|
return nil, gerror.New("当前套餐未启用")
|
|
}
|
|
durationUnit := renewDurationUnitForPackage(pkg)
|
|
actualDurationMonths := consts.DurationValueToMonths(durationUnit, durationValue)
|
|
actualGiftDurationMonths := consts.DurationValueToMonths(durationUnit, giftDurationMonths)
|
|
options, err := validateRuntimeRenewDuration(pkg, durationValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
customerDept, err := loadRenewDeptTx(ctx, tx, main.TenantId, customerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
priceSnap, err := loadRuntimeRenewPriceSnapshot(ctx, tx, main.TenantId, pkg, customerDept, options, beginDates)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
amount := roundAmount(priceSnap.RenewUnitPrice * float64(durationValue) * float64(len(iccids)))
|
|
|
|
mode := strings.TrimSpace(firstString(info, "paymentMode", "paymentSnapshot.paymentMode"))
|
|
payment, err := r.buildRuntimeRenewPayment(ctx, tx, main, mode, customerID, amount, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addition := map[string]any{
|
|
"customerAgentId": customerID,
|
|
"customerName": strings.TrimSpace(customerDept.DeptName),
|
|
"packageCode": priceSnap.PackageCode,
|
|
"packageName": priceSnap.PackageName,
|
|
"priceSource": priceSnap.PriceSource,
|
|
"priceOwnerDeptId": priceSnap.PriceOwnerDeptId,
|
|
"agentDefaultPriceId": priceSnap.AgentDefaultPriceId,
|
|
"targetPriceId": priceSnap.TargetPriceId,
|
|
"renewUnitPrice": priceSnap.RenewUnitPrice,
|
|
"renewableDurationOptions": priceSnap.RenewableDurationOptions,
|
|
"renewablePackageCodes": priceSnap.RenewablePackageCodes,
|
|
"renewablePackages": priceSnap.RenewablePackages,
|
|
"billingMethod": priceSnap.BillingMethod,
|
|
"renewDurationUnit": priceSnap.RenewDurationUnit,
|
|
"renewDurationUnitText": priceSnap.RenewDurationUnitText,
|
|
"renewBeginDate": priceSnap.RenewBeginDate,
|
|
"renewBeginDateMin": priceSnap.RenewBeginDateMin,
|
|
"renewBeginDateMax": priceSnap.RenewBeginDateMax,
|
|
"sameRenewBeginDate": priceSnap.SameRenewBeginDate,
|
|
"priceSnapshot": priceSnap,
|
|
"quantity": len(iccids),
|
|
"durationValue": durationValue,
|
|
"durationMonths": actualDurationMonths,
|
|
"actualDurationMonths": actualDurationMonths,
|
|
"giftDurationMonths": giftDurationMonths,
|
|
"actualGiftDurationMonths": actualGiftDurationMonths,
|
|
"amount": amount,
|
|
}
|
|
for key, value := range payment {
|
|
addition[key] = value
|
|
}
|
|
merged := mergeBatchInfo(main.BatchInfo, addition)
|
|
main.BatchInfo = gjson.New(merged)
|
|
|
|
contract, err := ParseExecutionContract(main)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = r.validateRenewRuntimePermissions(ctx, tx, main, contract); err != nil {
|
|
return nil, err
|
|
}
|
|
return contract, nil
|
|
}
|
|
|
|
func runtimeRenewFacts(
|
|
main *rc.BatchMainRow,
|
|
details []*rc.BatchDetailRow,
|
|
cards map[string]*rc.SimCardInfo,
|
|
binds map[string]*rc.SimCardBindPkg,
|
|
futureBinds map[string][]*rc.SimCardBindPkg,
|
|
now time.Time,
|
|
) (int64, string, []string, error) {
|
|
customerIDs := map[int64]struct{}{}
|
|
packageCodes := map[string]struct{}{}
|
|
beginDates := make([]string, 0, len(details))
|
|
periodRows := make([]*RenewRow, 0, len(details))
|
|
sourceBatchNo := executionSourceBatchNo(main, main.BatchInfo)
|
|
|
|
for _, detail := range details {
|
|
if detail == nil {
|
|
return 0, "", nil, gerror.New("续费明细为空")
|
|
}
|
|
iccid := strings.TrimSpace(detail.ItemId)
|
|
card := cards[iccid]
|
|
bind := binds[iccid]
|
|
if card == nil {
|
|
return 0, "", nil, gerror.Newf("ICCID不存在:%s", iccid)
|
|
}
|
|
if bind == nil {
|
|
return 0, "", nil, gerror.Newf("ICCID没有订购记录:%s", iccid)
|
|
}
|
|
expectedMSISDN, expectedIMSI := snapshotIdentity(detail.Snapshot)
|
|
if err := validateCurrentSubscription(card, bind, subscriptionValidation{
|
|
ICCID: iccid,
|
|
MSISDN: expectedMSISDN,
|
|
IMSI: expectedIMSI,
|
|
TenantID: main.TenantId,
|
|
CustomerDeptID: card.DeptId,
|
|
PackageCode: card.CurrentPackageCode,
|
|
SourceBatchNo: sourceBatchNo,
|
|
}); err != nil {
|
|
return 0, "", nil, gerror.Wrapf(err, "校验续费卡失败,ICCID:%s", iccid)
|
|
}
|
|
base := renewBaseSubscriptionForCard(card, bind, futureBinds[iccid])
|
|
if err := validateRenewBaseWindow(base, now, iccid); err != nil {
|
|
return 0, "", nil, err
|
|
}
|
|
customerIDs[card.DeptId] = struct{}{}
|
|
packageCodes[strings.TrimSpace(card.CurrentPackageCode)] = struct{}{}
|
|
periodRows = append(periodRows, &RenewRow{
|
|
RowNo: detail.RowNo,
|
|
ICCID: iccid,
|
|
OldEffTime: base.EffectiveTime,
|
|
OldExpireTime: base.ExpectedExpireTime,
|
|
})
|
|
beginAt := renewBeginDateFromExpire(base.ExpectedExpireTime)
|
|
if beginAt != nil {
|
|
beginDates = append(beginDates, beginAt.Format("Y-m-d"))
|
|
}
|
|
}
|
|
if err := validateRenewSameCurrentPeriod(periodRows, nil); err != nil {
|
|
return 0, "", nil, err
|
|
}
|
|
if len(customerIDs) != 1 {
|
|
return 0, "", nil, gerror.New("当前批次下存在多个客户,不能统一续费")
|
|
}
|
|
if len(packageCodes) != 1 {
|
|
return 0, "", nil, gerror.New("当前批次下存在多个套餐,不能统一续费")
|
|
}
|
|
var customerID int64
|
|
for id := range customerIDs {
|
|
customerID = id
|
|
}
|
|
var packageCode string
|
|
for code := range packageCodes {
|
|
packageCode = code
|
|
}
|
|
return customerID, packageCode, beginDates, nil
|
|
}
|
|
|
|
func normalizeLegacyRenewDurationValue(raw int) int {
|
|
if raw <= 0 {
|
|
return 0
|
|
}
|
|
if raw >= 12 && raw%12 == 0 {
|
|
return raw / 12
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func validateRuntimeRenewDuration(pkg *rc.FlowPackageInfo, durationValue int) ([]int, error) {
|
|
if pkg == nil {
|
|
return nil, gerror.New("续费套餐不存在")
|
|
}
|
|
options, err := renewableYearOptions(pkg.RenewableDurationConfig)
|
|
if err != nil || len(options) == 0 {
|
|
return nil, gerror.Newf("当前套餐%s未配置可续费时长", runtimeRenewPackageLabel(pkg))
|
|
}
|
|
if !rc.ContainsInt(options, durationValue) {
|
|
return nil, gerror.Newf(
|
|
"续费时长%d%s不在当前套餐%s可续费范围内,可选:%s",
|
|
durationValue,
|
|
consts.RenewDurationUnitText(renewDurationUnitForPackage(pkg)),
|
|
runtimeRenewPackageLabel(pkg),
|
|
formatRuntimeRenewDurationOptions(options, renewDurationUnitForPackage(pkg)),
|
|
)
|
|
}
|
|
return options, nil
|
|
}
|
|
|
|
func runtimeRenewPackageLabel(pkg *rc.FlowPackageInfo) string {
|
|
if pkg == nil {
|
|
return ""
|
|
}
|
|
name := strings.TrimSpace(pkg.PackageName)
|
|
code := strings.TrimSpace(pkg.PackageCode)
|
|
switch {
|
|
case name != "" && code != "":
|
|
return name + "(" + code + ")"
|
|
case name != "":
|
|
return name
|
|
default:
|
|
return code
|
|
}
|
|
}
|
|
|
|
func formatRuntimeRenewDurationOptions(options []int, unit string) string {
|
|
if len(options) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(options))
|
|
for _, option := range options {
|
|
parts = append(parts, fmt.Sprintf("%d%s", option, consts.RenewDurationUnitText(unit)))
|
|
}
|
|
return strings.Join(parts, "、")
|
|
}
|
|
|
|
func snapshotIdentity(snapshot *gjson.Json) (string, string) {
|
|
if snapshot == nil {
|
|
return "", ""
|
|
}
|
|
msisdn := strings.TrimSpace(snapshot.Get("msisdn").String())
|
|
if msisdn == "" {
|
|
msisdn = strings.TrimSpace(snapshot.Get("MSISDN").String())
|
|
}
|
|
imsi := strings.TrimSpace(snapshot.Get("imsi").String())
|
|
if imsi == "" {
|
|
imsi = strings.TrimSpace(snapshot.Get("IMSI").String())
|
|
}
|
|
return msisdn, imsi
|
|
}
|
|
|
|
func loadRuntimeRenewPriceSnapshot(
|
|
ctx context.Context,
|
|
tx gdb.TX,
|
|
tenantID string,
|
|
pkg *rc.FlowPackageInfo,
|
|
customerDept *rc.SysDeptRow,
|
|
options []int,
|
|
beginDates []string,
|
|
) (*RenewPriceSnapshot, error) {
|
|
if pkg == nil || customerDept == nil {
|
|
return nil, gerror.New("续费套餐或客户信息缺失")
|
|
}
|
|
priceOwnerDeptID := renewPriceOwnerDeptID(customerDept)
|
|
salePrice, err := resolveRenewEffectiveSalePrice(ctx, renewTxEffectivePriceStore{tx: tx}, tenantID, priceOwnerDeptID, customerDept.DeptId, pkg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sameDate, minDate, maxDate, same := summarizeRenewBeginDates(beginDates)
|
|
durationUnit := renewDurationUnitForPackage(pkg)
|
|
renewableCodes, renewablePackages, err := runtimeRenewablePackageOptions(ctx, tx, tenantID, customerDept.DeptId, priceOwnerDeptID, pkg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &RenewPriceSnapshot{
|
|
CustomerAgentId: customerDept.DeptId,
|
|
PriceOwnerDeptId: priceOwnerDeptID,
|
|
PackageCode: pkg.PackageCode,
|
|
PackageName: pkg.PackageName,
|
|
BillingMethod: pkg.BillingMethod,
|
|
RenewDurationUnit: durationUnit,
|
|
RenewDurationUnitText: consts.RenewDurationUnitText(durationUnit),
|
|
PriceSource: salePrice.Rule.PriceSource,
|
|
AgentDefaultPriceId: salePrice.Rule.AgentDefaultPriceId,
|
|
TargetPriceId: salePrice.Rule.TargetPriceId,
|
|
RenewUnitPrice: salePrice.AgentShipmentPrice,
|
|
RenewableDurationOptions: options,
|
|
RenewablePackageCodes: renewableCodes,
|
|
RenewablePackages: renewablePackages,
|
|
RenewBeginDate: sameDate,
|
|
RenewBeginDateMin: minDate,
|
|
RenewBeginDateMax: maxDate,
|
|
SameRenewBeginDate: same,
|
|
}, nil
|
|
}
|
|
|
|
func runtimeRenewablePackageOptions(ctx context.Context, tx gdb.TX, tenantID string, customerDeptID, priceOwnerDeptID int64, currentPkg *rc.FlowPackageInfo) ([]string, []RenewPackageOption, error) {
|
|
if currentPkg == nil {
|
|
return nil, nil, gerror.New("当前套餐不存在或未启用")
|
|
}
|
|
codes := renewPackageCodesFromJSON(currentPkg.RenewablePackageCodes)
|
|
if len(codes) == 0 {
|
|
return nil, nil, gerror.New("当前套餐未配置可续费套餐")
|
|
}
|
|
validCodes := make([]string, 0, len(codes))
|
|
options := make([]RenewPackageOption, 0, len(codes))
|
|
for _, code := range codes {
|
|
pkg, err := loadRenewPackageOptionTx(ctx, tx, tenantID, code)
|
|
if err != nil || pkg == nil || pkg.Enabled != 1 {
|
|
continue
|
|
}
|
|
unitPrice, err := runtimeRenewPackageUnitPrice(ctx, tx, tenantID, customerDeptID, priceOwnerDeptID, pkg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
durations, err := renewableYearOptions(pkg.RenewableDurationConfig)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
validCodes = append(validCodes, pkg.PackageCode)
|
|
options = append(options, RenewPackageOption{
|
|
PackageCode: pkg.PackageCode,
|
|
PackageName: pkg.PackageName,
|
|
RenewUnitPrice: unitPrice,
|
|
RenewableDurationOptions: durations,
|
|
})
|
|
}
|
|
if len(options) == 0 {
|
|
return nil, nil, gerror.New("当前套餐未配置可用的可续费套餐")
|
|
}
|
|
return validCodes, options, nil
|
|
}
|
|
|
|
func loadRenewPackageOptionTx(ctx context.Context, tx gdb.TX, tenantID, packageCode string) (*rc.FlowPackageInfo, error) {
|
|
var pkg rc.FlowPackageInfo
|
|
err := tx.Ctx(ctx).Model(rc.TableSimFlowPackageInfo).
|
|
Where("tenant_id", strings.TrimSpace(tenantID)).
|
|
Where("package_code", strings.TrimSpace(packageCode)).
|
|
LockShared().
|
|
Scan(&pkg)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return nil, gerror.Wrap(err, "查询可续费套餐失败")
|
|
}
|
|
if pkg.Id == 0 {
|
|
return nil, nil
|
|
}
|
|
return &pkg, nil
|
|
}
|
|
|
|
func runtimeRenewPackageUnitPrice(ctx context.Context, tx gdb.TX, tenantID string, customerDeptID, priceOwnerDeptID int64, pkg *rc.FlowPackageInfo) (float64, error) {
|
|
if pkg == nil {
|
|
return 0, gerror.New("续费套餐不存在")
|
|
}
|
|
salePrice, err := resolveRenewEffectiveSalePrice(ctx, renewTxEffectivePriceStore{tx: tx}, tenantID, priceOwnerDeptID, customerDeptID, pkg)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return salePrice.AgentShipmentPrice, nil
|
|
}
|
|
|
|
func (r *RenewExecutor) buildRuntimeRenewPayment(ctx context.Context, tx gdb.TX, main *rc.BatchMainRow, requestedMode string, payerDeptID int64, amount float64, now time.Time) (map[string]any, error) {
|
|
isTop, operatorDeptID, err := isRenewTopDeptTx(ctx, tx, main)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if payerDeptID <= 0 {
|
|
payerDeptID = operatorDeptID
|
|
}
|
|
mode := strings.TrimSpace(requestedMode)
|
|
if mode == "" {
|
|
if isTop {
|
|
mode = "offline"
|
|
} else {
|
|
mode = "balance"
|
|
}
|
|
}
|
|
switch mode {
|
|
case "offline":
|
|
if !isTop {
|
|
return nil, gerror.New("当前组织续费必须使用余额支付")
|
|
}
|
|
if main.BatchInfo != nil && !main.BatchInfo.Get("offlineContractCompleted").Bool() {
|
|
return nil, gerror.New("请确认线下业务已完成")
|
|
}
|
|
return runtimePaymentSnapshot(mode, consts.PayTypeOffline, amount, 0, amount, 0, 0, "", payerDeptID, main.CreatedBy, isTop, now), nil
|
|
case "balance":
|
|
before, after, serialNo, err := deductRuntimeRenewBalance(ctx, tx, main, payerDeptID, amount, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return runtimePaymentSnapshot(mode, consts.PayTypeBalance, amount, amount, 0, before, after, serialNo, payerDeptID, main.CreatedBy, isTop, now), nil
|
|
default:
|
|
return nil, gerror.New("续费付款方式不正确")
|
|
}
|
|
}
|
|
|
|
func runtimePaymentSnapshot(mode string, payType int, payAmount, balanceAmount, offlineAmount, before, after float64, serialNo string, payerDeptID, payerUserID int64, isTop bool, now time.Time) map[string]any {
|
|
paymentTime := now.Format("2006-01-02 15:04:05")
|
|
snapshot := map[string]any{
|
|
"paymentMode": mode,
|
|
"payType": payType,
|
|
"payStatus": consts.PayStatusPaid,
|
|
"payAmount": payAmount,
|
|
"balanceAmount": balanceAmount,
|
|
"offlinePaidAmount": offlineAmount,
|
|
"balanceBefore": before,
|
|
"balanceAfter": after,
|
|
"balanceDetailSerialNo": strings.TrimSpace(serialNo),
|
|
"paymentTime": paymentTime,
|
|
"payerDeptId": payerDeptID,
|
|
"payerUserId": payerUserID,
|
|
"isTopDept": isTop,
|
|
}
|
|
return map[string]any{
|
|
"paymentMode": mode,
|
|
"payType": payType,
|
|
"payStatus": consts.PayStatusPaid,
|
|
"payAmount": payAmount,
|
|
"balanceAmount": balanceAmount,
|
|
"offlinePaidAmount": offlineAmount,
|
|
"balanceBefore": before,
|
|
"balanceAfter": after,
|
|
"balanceDetailSerialNo": strings.TrimSpace(serialNo),
|
|
"paymentTime": paymentTime,
|
|
"payerDeptId": payerDeptID,
|
|
"payerUserId": payerUserID,
|
|
"isTopDept": isTop,
|
|
"paymentSnapshot": snapshot,
|
|
}
|
|
}
|
|
|
|
func deductRuntimeRenewBalance(ctx context.Context, tx gdb.TX, main *rc.BatchMainRow, payerDeptID int64, amount float64, now time.Time) (float64, float64, string, error) {
|
|
amount = roundAmount(amount)
|
|
if amount <= 0 {
|
|
return 0, 0, "", nil
|
|
}
|
|
serialNo := "RENPAY" + strings.TrimSpace(main.BatchNo)
|
|
if serialNo == "RENPAY" {
|
|
return 0, 0, "", gerror.New("续费余额流水号生成失败")
|
|
}
|
|
existingModel := tx.Ctx(ctx).Model(rc.TableBalanceDetail).
|
|
Where("tenant_id", main.TenantId).
|
|
Where("serial_no", serialNo)
|
|
existing, err := runtimeRenewLockForUpdate(existingModel).One()
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return 0, 0, "", gerror.Wrap(err, "查询续费余额流水失败")
|
|
}
|
|
if existing != nil && !existing.IsEmpty() {
|
|
return 0, 0, "", gerror.New("当前续费批次已扣款,请勿重复提交")
|
|
}
|
|
|
|
var account renewPaymentAccountRow
|
|
recordModel := tx.Ctx(ctx).Model(rc.TableAgentBalance).
|
|
Where("dept_id", payerDeptID).
|
|
Where("tenant_id", main.TenantId)
|
|
record, err := runtimeRenewLockForUpdate(recordModel).One()
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return 0, 0, "", gerror.Wrap(err, "查询账户余额失败")
|
|
}
|
|
if record != nil && !record.IsEmpty() {
|
|
if err := record.Struct(&account); err != nil {
|
|
return 0, 0, "", gerror.Wrap(err, "解析账户余额失败")
|
|
}
|
|
}
|
|
if account.ID == 0 {
|
|
return 0, 0, "", gerror.New("账户余额不存在,请先充值")
|
|
}
|
|
if account.Status != renewFinanceAccountStatusNormal {
|
|
return 0, 0, "", gerror.New("账户余额状态异常,不能续费")
|
|
}
|
|
before := roundAmount(account.Balance)
|
|
if before+0.0001 < amount {
|
|
return 0, 0, "", gerror.Newf("账户余额不足,需支付%.2f元,当前余额%.2f元", amount, before)
|
|
}
|
|
after := roundAmount(before - amount)
|
|
nowTime := gtime.NewFromTime(now)
|
|
if _, err := tx.Ctx(ctx).Model(rc.TableAgentBalance).Where("id", account.ID).Data(g.Map{
|
|
"balance": after,
|
|
"updated_by": main.CreatedBy,
|
|
"updated_at": nowTime,
|
|
}).Update(); err != nil {
|
|
return 0, 0, "", gerror.Wrap(err, "扣减账户余额失败")
|
|
}
|
|
if _, err := tx.Ctx(ctx).Model(rc.TableBalanceDetail).Data(g.Map{
|
|
"dept_id": payerDeptID,
|
|
"serial_no": serialNo,
|
|
"transaction_time": nowTime,
|
|
"income_expense_type": renewFinanceIncomeExpenseExpense,
|
|
"transaction_type": renewFinanceTransactionTypeConsume,
|
|
"transaction_channel": renewFinanceTransactionChannelBalance,
|
|
"transaction_status": renewFinanceTransactionStatusPaid,
|
|
"amount": amount,
|
|
"before_balance": before,
|
|
"balance_after": after,
|
|
"order_sn": main.BatchNo,
|
|
"remark": fmt.Sprintf("SIM卡续费余额支付,批次号:%s", main.BatchNo),
|
|
"tenant_id": main.TenantId,
|
|
"created_dept": payerDeptID,
|
|
"created_by": main.CreatedBy,
|
|
"created_at": nowTime,
|
|
}).Insert(); err != nil {
|
|
return 0, 0, "", gerror.Wrap(err, "写入续费余额流水失败")
|
|
}
|
|
return before, after, serialNo, nil
|
|
}
|
|
|
|
func runtimeRenewLockForUpdate(model *gdb.Model) *gdb.Model {
|
|
if g.DB().GetConfig().Type == "sqlite" {
|
|
return model
|
|
}
|
|
return model.LockUpdate()
|
|
}
|
|
|
|
func (r *RenewExecutor) validateRenewRuntimePermissions(ctx context.Context, tx gdb.TX, main *rc.BatchMainRow, contract *ExecutionContract) error {
|
|
if contract == nil {
|
|
return gerror.New("续费执行合同不能为空")
|
|
}
|
|
isTop, _, err := isRenewTopDeptTx(ctx, tx, main)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if contract.PaymentMode == "offline" && !isTop {
|
|
return gerror.New("当前组织续费必须使用余额支付")
|
|
}
|
|
if contract.PackageBeginTime != nil && !isTop {
|
|
return gerror.New("当前组织不能修改续费日期")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isRenewTopDeptTx(ctx context.Context, tx gdb.TX, main *rc.BatchMainRow) (bool, int64, error) {
|
|
deptID := main.CreatedDept
|
|
if deptID <= 0 {
|
|
deptID = main.DeptId
|
|
}
|
|
if deptID <= 0 {
|
|
return false, 0, gerror.New("当前批次缺少操作组织")
|
|
}
|
|
dept, err := loadRenewDeptTx(ctx, tx, main.TenantId, deptID)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
return dept.ParentId == 0, deptID, nil
|
|
}
|
|
|
|
func loadRenewDeptTx(ctx context.Context, tx gdb.TX, tenantID string, deptID int64) (*rc.SysDeptRow, error) {
|
|
var row rc.SysDeptRow
|
|
err := tx.Ctx(ctx).Model(rc.TableSysDept).
|
|
Where("tenant_id", tenantID).
|
|
Where("dept_id", deptID).
|
|
WhereNull("deleted_at").
|
|
LockShared().
|
|
Scan(&row)
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "查询客户部门失败")
|
|
}
|
|
if row.DeptId == 0 {
|
|
return nil, gerror.New("客户部门不存在")
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func loadRenewPriceOverrideTx(ctx context.Context, tx gdb.TX, tenantID string, deptID, targetAgentID int64, scopeType int, packageCode string) (*rc.FlowPackagePrice, error) {
|
|
var row rc.FlowPackagePrice
|
|
err := tx.Ctx(ctx).Model(rc.TableSimFlowPackagePrice).
|
|
Where("tenant_id", tenantID).
|
|
Where("dept_id", deptID).
|
|
Where("agent_scope_type", scopeType).
|
|
Where("target_agent_id", targetAgentID).
|
|
Where("package_code", strings.TrimSpace(packageCode)).
|
|
Where("enabled", 1).
|
|
LockShared().
|
|
Scan(&row)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return nil, gerror.Wrap(err, "查询代理续费定价失败")
|
|
}
|
|
if row.Id == 0 {
|
|
return nil, nil
|
|
}
|
|
return &row, nil
|
|
}
|