1429 lines
45 KiB
Go
1429 lines
45 KiB
Go
package addpackage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"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/batch"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
|
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
|
|
renewchangeservice "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/renewchange"
|
|
)
|
|
|
|
type executor struct {
|
|
repo *renewchangeservice.Repository
|
|
batchType int
|
|
poolMode bool
|
|
nowFunc NowFunc
|
|
}
|
|
|
|
type flowPoolRow struct {
|
|
Id int64 `orm:"id"`
|
|
FlowPoolCode string `orm:"flow_pool_code"`
|
|
DeptId int64 `orm:"dept_id"`
|
|
PoolPackageCode int64 `orm:"pool_package_code"`
|
|
AddOnFlowMb float64 `orm:"add_on_flow_mb"`
|
|
TotalFlowMb float64 `orm:"total_flow_mb"`
|
|
RemainFlowMb float64 `orm:"remain_flow_mb"`
|
|
PoolStatus int `orm:"pool_status"`
|
|
TenantId string `orm:"tenant_id"`
|
|
CreatedDept int64 `orm:"created_dept"`
|
|
CreatedBy int64 `orm:"created_by"`
|
|
UpdatedBy int64 `orm:"updated_by"`
|
|
UpdatedAt *gtime.Time `orm:"updated_at"`
|
|
}
|
|
|
|
type sysDeptRow struct {
|
|
DeptId int64 `orm:"dept_id"`
|
|
ParentId int64 `orm:"parent_id"`
|
|
TenantId string `orm:"tenant_id"`
|
|
DeletedAt *gtime.Time `orm:"deleted_at"`
|
|
}
|
|
|
|
type agentBalanceRow struct {
|
|
Id int64 `orm:"id"`
|
|
DeptId int64 `orm:"dept_id"`
|
|
Balance float64 `orm:"balance"`
|
|
Status int `orm:"status"`
|
|
TenantId string `orm:"tenant_id"`
|
|
}
|
|
|
|
type balanceDetailRow struct {
|
|
DeptId int64 `orm:"dept_id"`
|
|
SerialNo string `orm:"serial_no"`
|
|
TransactionTime *gtime.Time `orm:"transaction_time"`
|
|
IncomeExpenseType int `orm:"income_expense_type"`
|
|
TransactionType int `orm:"transaction_type"`
|
|
TransactionChannel int `orm:"transaction_channel"`
|
|
TransactionStatus int `orm:"transaction_status"`
|
|
Amount float64 `orm:"amount"`
|
|
BeforeBalance float64 `orm:"before_balance"`
|
|
BalanceAfter float64 `orm:"balance_after"`
|
|
ChannelSerialNo string `orm:"channel_serial_no"`
|
|
OrderSn string `orm:"order_sn"`
|
|
Remark string `orm:"remark"`
|
|
TenantId string `orm:"tenant_id"`
|
|
CreatedDept int64 `orm:"created_dept"`
|
|
CreatedBy int64 `orm:"created_by"`
|
|
CreatedAt *gtime.Time `orm:"created_at"`
|
|
}
|
|
|
|
const (
|
|
addPackageBizType = 3
|
|
addPackageTransactionType = 1
|
|
addPackageTransactionChan = 3
|
|
addPackageTransactionPaid = 1
|
|
addPackageAccountStatusOK = 0
|
|
)
|
|
|
|
const (
|
|
addPackageSameBaseFailureReason = "同一批加油包目标当前基础套餐必须一致"
|
|
addPackageSamePackageFailureReason = "同一批加油包套餐必须一致"
|
|
addPackageSameEffectiveFailureReason = "同一批加油包生效方式必须一致"
|
|
addPackageSamePriceFailureReason = "同一批加油包单价必须一致"
|
|
)
|
|
|
|
func NewCardExecutor(repo *renewchangeservice.Repository) batch.Executor {
|
|
if repo == nil {
|
|
repo = renewchangeservice.NewRepository()
|
|
}
|
|
return &executor{repo: repo, batchType: consts.BatchTypeCardAddPackage, nowFunc: timeNow}
|
|
}
|
|
|
|
func NewPoolExecutor(repo *renewchangeservice.Repository) batch.Executor {
|
|
if repo == nil {
|
|
repo = renewchangeservice.NewRepository()
|
|
}
|
|
return &executor{repo: repo, batchType: consts.BatchTypePoolAddPackage, poolMode: true, nowFunc: timeNow}
|
|
}
|
|
|
|
func (e *executor) Execute(ctx context.Context, batchNo string) error {
|
|
batchNo = strings.TrimSpace(batchNo)
|
|
if batchNo == "" {
|
|
return gerror.New("add package batch number is empty")
|
|
}
|
|
main, err := e.repo.LoadProcessingBatch(ctx, batchNo, e.batchType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
details, err := e.repo.LoadRunningDetails(ctx, batchNo, e.batchType)
|
|
if err != nil {
|
|
return gerror.Wrap(err, "load add package details failed")
|
|
}
|
|
task := &batchTask{
|
|
main: main,
|
|
total: len(details),
|
|
plans: make([]*detailPlan, 0, len(details)),
|
|
paymentIntent: readPaymentIntent(main.BatchInfo),
|
|
paymentSnapshot: make(map[int64]*PaymentSnapshot),
|
|
}
|
|
if len(details) == 0 {
|
|
return e.finalizeFailure(ctx, task, "加油包批次没有可处理明细")
|
|
}
|
|
if err := e.buildTask(ctx, task, details); err != nil {
|
|
return e.finalizeFailure(ctx, task, err.Error())
|
|
}
|
|
if task.valid == 0 {
|
|
if task.reason == "" {
|
|
task.reason = "加油包批次没有可处理明细"
|
|
}
|
|
return e.finalizeFailure(ctx, task, task.reason)
|
|
}
|
|
orderSN, err := e.applyBusiness(ctx, task)
|
|
if err != nil {
|
|
return e.finalizeFailure(ctx, task, err.Error())
|
|
}
|
|
if err = e.finalizeSuccess(ctx, task, orderSN); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *executor) buildTask(ctx context.Context, task *batchTask, details []*rc.BatchDetailRow) error {
|
|
seen := make(map[string]struct{}, len(details))
|
|
now := e.nowTime()
|
|
for _, detail := range details {
|
|
if detail == nil {
|
|
continue
|
|
}
|
|
plan := &detailPlan{detail: detail}
|
|
snapshot := readDetailSnapshot(detail.Snapshot)
|
|
plan.targetCode = strings.TrimSpace(detail.ItemId)
|
|
if snapshot != nil {
|
|
if v := strings.TrimSpace(snapshot.TargetCode); v != "" {
|
|
plan.targetCode = v
|
|
}
|
|
plan.packageCode = strings.TrimSpace(snapshot.PackageCode)
|
|
plan.remark = strings.TrimSpace(snapshot.Remark)
|
|
}
|
|
if plan.packageCode == "" && task.main.BatchInfo != nil {
|
|
plan.packageCode = strings.TrimSpace(task.main.BatchInfo.Get("packageCode").String())
|
|
}
|
|
mode, modeErr := effectiveModeFrom(task.main.BatchInfo, snapshot)
|
|
if modeErr != nil {
|
|
plan.reason = modeErr.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
plan.effectiveMode = mode
|
|
if plan.targetCode == "" {
|
|
plan.reason = "目标编码不能为空"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
hasPackageCode := plan.packageCode != ""
|
|
if !hasPackageCode && !task.prepareOnly {
|
|
plan.reason = "加油包套餐编码不能为空"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
dupKey := strings.Join([]string{plan.targetCode, plan.packageCode}, "|")
|
|
if _, ok := seen[dupKey]; ok {
|
|
plan.reason = "批次内重复的目标编码 + 套餐编码"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
seen[dupKey] = struct{}{}
|
|
|
|
if e.poolMode {
|
|
pool, err := e.loadPool(ctx, task.main.TenantId, plan.targetCode)
|
|
if err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if pool == nil || pool.Id == 0 {
|
|
plan.reason = "流量池不存在"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
plan.pool = pool
|
|
plan.customerDeptID = pool.DeptId
|
|
plan.basePackageKey = fmt.Sprintf("%d", pool.PoolPackageCode)
|
|
if pool.PoolStatus != 0 {
|
|
plan.reason = "流量池状态异常,不能订购加油包"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if err := e.ensureNoActiveTopup(ctx, nil, task.main.TenantId, plan.targetCode, now); err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if !hasPackageCode {
|
|
task.valid++
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
pkg, price, err := e.resolvePrice(ctx, task.main.TenantId, pool.DeptId, plan.packageCode)
|
|
if err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if pkg.PackageType != 104 {
|
|
plan.reason = "目标套餐类型不是流量池加油包"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if pkg.PoolPackageCode != pool.PoolPackageCode {
|
|
plan.reason = "加油包不属于当前流量池套餐"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
plan.price = price
|
|
plan.unitAmount = price.UnitPrice
|
|
plan.amount = roundAmount(price.UnitPrice)
|
|
plan.effectiveTime, plan.expireTime, plan.effectiveNow, err = calcMonthWindow(plan.effectiveMode, now)
|
|
if err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
task.valid++
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
|
|
cardMap, err := e.repo.Tables().LoadCardsByIccids(ctx, []string{plan.targetCode})
|
|
if err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
card := cardMap[plan.targetCode]
|
|
if card == nil || card.Id == 0 {
|
|
plan.reason = "ICCID不存在"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
plan.card = card
|
|
plan.customerDeptID = card.DeptId
|
|
plan.basePackageKey = strings.TrimSpace(card.CurrentPackageCode)
|
|
if card.PackageStatus != consts.PackageStatusEffective {
|
|
plan.reason = "当前卡套餐未生效,不能订购加油包"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if err := e.ensureNoActiveTopup(ctx, nil, task.main.TenantId, plan.targetCode, now); err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
basePkg, err := e.repo.Tables().LoadPackageByCode(ctx, task.main.TenantId, card.CurrentPackageCode)
|
|
if err != nil || basePkg == nil {
|
|
plan.reason = "当前卡基础套餐不存在"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if !containsString(packageCodesFromJSON(basePkg.TopupPackageCodes), plan.packageCode) {
|
|
if hasPackageCode {
|
|
plan.reason = "当前套餐不允许订购该加油包"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
}
|
|
if !hasPackageCode {
|
|
task.valid++
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
pkg, price, err := e.resolvePrice(ctx, task.main.TenantId, card.DeptId, plan.packageCode)
|
|
if err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
if pkg.PackageType != 103 {
|
|
plan.reason = "目标套餐类型不是单卡加油包"
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
plan.price = price
|
|
plan.unitAmount = price.UnitPrice
|
|
plan.amount = roundAmount(price.UnitPrice)
|
|
plan.effectiveTime, plan.expireTime, plan.effectiveNow, err = calcMonthWindow(plan.effectiveMode, now)
|
|
if err != nil {
|
|
plan.reason = err.Error()
|
|
task.plans = append(task.plans, plan)
|
|
continue
|
|
}
|
|
task.valid++
|
|
task.plans = append(task.plans, plan)
|
|
}
|
|
enforceAddPackageBatchUniformity(task)
|
|
task.valid = 0
|
|
task.success = 0
|
|
task.fail = 0
|
|
task.reason = ""
|
|
for _, plan := range task.plans {
|
|
if plan == nil {
|
|
continue
|
|
}
|
|
if plan.reason != "" {
|
|
task.fail++
|
|
if task.reason == "" {
|
|
task.reason = plan.reason
|
|
}
|
|
continue
|
|
}
|
|
task.success++
|
|
task.valid++
|
|
}
|
|
if task.reason == "" && task.valid == 0 {
|
|
task.reason = "加油包批次没有可处理明细"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func enforceAddPackageBatchUniformity(task *batchTask) {
|
|
if task == nil {
|
|
return
|
|
}
|
|
var first *detailPlan
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.reason != "" {
|
|
continue
|
|
}
|
|
if first == nil {
|
|
first = plan
|
|
continue
|
|
}
|
|
switch {
|
|
case strings.TrimSpace(plan.basePackageKey) != strings.TrimSpace(first.basePackageKey):
|
|
plan.reason = addPackageSameBaseFailureReason
|
|
case strings.TrimSpace(plan.packageCode) != strings.TrimSpace(first.packageCode):
|
|
plan.reason = addPackageSamePackageFailureReason
|
|
case strings.TrimSpace(plan.effectiveMode) != strings.TrimSpace(first.effectiveMode):
|
|
plan.reason = addPackageSameEffectiveFailureReason
|
|
case !sameAddPackageAmount(plan.unitAmount, first.unitAmount):
|
|
plan.reason = addPackageSamePriceFailureReason
|
|
}
|
|
}
|
|
}
|
|
|
|
func sameAddPackageAmount(left, right float64) bool {
|
|
return amountCents(left) == amountCents(right)
|
|
}
|
|
|
|
func amountCents(value float64) int64 {
|
|
if value < 0 {
|
|
value = 0
|
|
}
|
|
return int64(value*100 + 0.5)
|
|
}
|
|
|
|
func (e *executor) applyBusiness(ctx context.Context, task *batchTask) (string, error) {
|
|
now := e.nowTime()
|
|
tenantID := task.main.TenantId
|
|
userID := task.main.CreatedBy
|
|
deptID := task.main.CreatedDept
|
|
isTopDept, err := e.isTopDept(ctx, tenantID, deptID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
intent, err := normalizePaymentIntent(task.paymentIntent, isTopDept)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
task.paymentIntent = intent
|
|
if orderSN, recovered, err := e.recoverAppliedBusiness(ctx, task); err != nil {
|
|
return "", err
|
|
} else if recovered {
|
|
return orderSN, nil
|
|
}
|
|
planGroups := make(map[int64][]*detailPlan)
|
|
orderedDeptIDs := make([]int64, 0, len(task.plans))
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.reason != "" {
|
|
continue
|
|
}
|
|
payerDeptID := plan.customerDeptID
|
|
if payerDeptID <= 0 {
|
|
return "", gerror.New("加油包归属部门不能为空")
|
|
}
|
|
if _, ok := planGroups[payerDeptID]; !ok {
|
|
orderedDeptIDs = append(orderedDeptIDs, payerDeptID)
|
|
}
|
|
planGroups[payerDeptID] = append(planGroups[payerDeptID], plan)
|
|
}
|
|
if len(planGroups) == 0 {
|
|
return "", gerror.New("加油包订单金额不能为空")
|
|
}
|
|
sort.Slice(orderedDeptIDs, func(i, j int) bool { return orderedDeptIDs[i] < orderedDeptIDs[j] })
|
|
orderSNs := make([]string, 0, len(orderedDeptIDs))
|
|
if err := e.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.reason != "" {
|
|
continue
|
|
}
|
|
if err := e.ensureNoActiveTopup(ctx, tx, tenantID, plan.targetCode, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, payerDeptID := range orderedDeptIDs {
|
|
groupPlans := planGroups[payerDeptID]
|
|
if len(groupPlans) == 0 {
|
|
continue
|
|
}
|
|
groupAmount := 0.0
|
|
for _, plan := range groupPlans {
|
|
groupAmount = roundAmount(groupAmount + plan.amount)
|
|
}
|
|
if groupAmount <= 0 {
|
|
return gerror.New("加油包订单金额不能为空")
|
|
}
|
|
groupOrderSN := rc.GenerateOrderSn(&now)
|
|
payment, err := e.executePayment(
|
|
ctx,
|
|
tx,
|
|
task.main,
|
|
intent,
|
|
payerDeptID,
|
|
groupAmount,
|
|
groupOrderSN,
|
|
now,
|
|
isTopDept,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
task.paymentSnapshot[payerDeptID] = payment
|
|
orderRow := buildAddPackageOrderDO(groupOrderSN, task.main, payerDeptID, groupAmount, payment, now)
|
|
orderID, err := e.repo.Tables().InsertContractOrder(ctx, tx, orderRow)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, plan := range groupPlans {
|
|
subscriptionNo := rc.GenerateSubscriptionNo("ADP")
|
|
plan.orderSN = groupOrderSN
|
|
if e.poolMode {
|
|
if err := e.insertPoolBind(ctx, tx, plan, task.main, groupOrderSN, subscriptionNo, now); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := e.insertCardBind(ctx, tx, plan, task.main, groupOrderSN, subscriptionNo, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
item := buildAddPackageOrderItemDO(orderID, groupOrderSN, task.main, payerDeptID, deptID, plan, now)
|
|
if err := e.repo.Tables().InsertContractOrderItem(ctx, tx, item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
orderSNs = append(orderSNs, groupOrderSN)
|
|
}
|
|
if e.poolMode {
|
|
if err := e.updateFlowPool(ctx, tx, task.plans, tenantID, userID, now); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := e.updateCardTopupFlow(ctx, tx, task.plans, tenantID, userID, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return "", err
|
|
}
|
|
if len(orderSNs) == 0 {
|
|
return "", nil
|
|
}
|
|
return orderSNs[0], nil
|
|
}
|
|
|
|
func (e *executor) finalizeSuccess(ctx context.Context, task *batchTask, orderSN string) error {
|
|
now := e.nowTime()
|
|
tenantID := task.main.TenantId
|
|
userID := task.main.CreatedBy
|
|
deptID := task.main.CreatedDept
|
|
successCount := task.valid
|
|
failCount := task.total - task.valid
|
|
runResult := consts.BatchRunResultSuccess
|
|
asyncStatus := consts.BatchAsyncStatusSuccess
|
|
failReason := ""
|
|
if failCount > 0 && successCount > 0 {
|
|
runResult = consts.BatchRunResultPartialSuccess
|
|
asyncStatus = consts.BatchAsyncStatusPartialSuccess
|
|
failReason = firstFailureReason(task.plans)
|
|
}
|
|
if failCount > 0 && successCount == 0 {
|
|
runResult = consts.BatchRunResultFailed
|
|
asyncStatus = consts.BatchAsyncStatusFailed
|
|
failReason = firstFailureReason(task.plans)
|
|
}
|
|
if err := e.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.detail == nil {
|
|
continue
|
|
}
|
|
snapshot := buildDetailSnapshot(plan, plan.orderSN)
|
|
status := consts.BatchDetailStatusSuccess
|
|
reason := ""
|
|
if plan.reason != "" {
|
|
status = consts.BatchDetailStatusFailed
|
|
reason = plan.reason
|
|
}
|
|
if err := e.repo.Tables().UpdateBatchDetailSnapshot(ctx, tx, task.main.BatchNo, task.main.BatchType, plan.detail.RowNo, status, snapshot, reason, userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
info := mergeBatchInfo(task.main.BatchInfo, map[string]any{
|
|
"orderSn": orderSN,
|
|
"orderSnList": collectOrderSNs(task.plans),
|
|
"priceSnapshot": buildBatchPriceSnapshot(task),
|
|
"paymentMode": task.paymentIntent.PaymentMode,
|
|
"offlineContractCompleted": task.paymentIntent.OfflineContractCompleted,
|
|
"paymentSnapshotByPayerDept": task.paymentSnapshot,
|
|
"batchMode": map[bool]string{true: "pool", false: "card"}[e.poolMode],
|
|
})
|
|
return e.repo.UpdateBatchMainInfo(ctx, tx, task.main.BatchNo, task.main.BatchType, &rc.BatchMainRow{
|
|
BatchType: task.main.BatchType,
|
|
BatchAsyncStatus: asyncStatus,
|
|
RunResult: runResult,
|
|
TotalCount: task.total,
|
|
ValidCount: task.valid,
|
|
SuccessCount: successCount,
|
|
FailCount: failCount,
|
|
FailReason: failReason,
|
|
BatchInfo: gjson.New(info),
|
|
OrderSn: orderSN,
|
|
EndTime: gtime.NewFromTime(now),
|
|
UpdatedBy: userID,
|
|
})
|
|
}); err != nil {
|
|
return gerror.Wrap(err, "finalize add package success failed")
|
|
}
|
|
rows, _ := e.repo.ResolveResultSnapshot(ctx, task.main.BatchNo, task.main.BatchType)
|
|
_ = e.repo.WriteResultFileSafe(ctx, task.main.BatchNo, task.main.BatchType, tenantID, deptID, userID, rows)
|
|
g.Log().Infof(ctx, "add package batch success, batchNo:%s, orderSn:%s, batchType:%d", task.main.BatchNo, orderSN, task.main.BatchType)
|
|
return nil
|
|
}
|
|
|
|
func (e *executor) finalizeFailure(ctx context.Context, task *batchTask, reason string) error {
|
|
now := e.nowTime()
|
|
tenantID := task.main.TenantId
|
|
userID := task.main.CreatedBy
|
|
deptID := task.main.CreatedDept
|
|
if err := e.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.detail == nil {
|
|
continue
|
|
}
|
|
snapshot := buildDetailSnapshot(plan, "")
|
|
rowReason := plan.reason
|
|
if rowReason == "" {
|
|
rowReason = reason
|
|
}
|
|
if err := e.repo.Tables().UpdateBatchDetailSnapshot(ctx, tx, task.main.BatchNo, task.main.BatchType, plan.detail.RowNo, consts.BatchDetailStatusFailed, snapshot, rowReason, userID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return e.repo.FinalizeBatchMain(ctx, tx, task.main.BatchNo, task.main.BatchType, &rc.BatchMainRow{
|
|
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
|
|
RunResult: consts.BatchRunResultFailed,
|
|
FailReason: reason,
|
|
TotalCount: task.total,
|
|
ValidCount: task.valid,
|
|
SuccessCount: 0,
|
|
FailCount: task.total,
|
|
EndTime: gtime.NewFromTime(now),
|
|
UpdatedBy: userID,
|
|
})
|
|
}); err != nil {
|
|
return gerror.Wrap(err, "finalize add package failure failed")
|
|
}
|
|
rows, _ := e.repo.ResolveResultSnapshot(ctx, task.main.BatchNo, task.main.BatchType)
|
|
_ = e.repo.WriteResultFileSafe(ctx, task.main.BatchNo, task.main.BatchType, tenantID, deptID, userID, rows)
|
|
g.Log().Warningf(ctx, "add package batch failed, batchNo:%s, batchType:%d, reason:%s", task.main.BatchNo, task.main.BatchType, reason)
|
|
return nil
|
|
}
|
|
|
|
func buildAddPackageOrderDO(orderSN string, main *rc.BatchMainRow, payerDeptID int64, totalAmount float64, payment *PaymentSnapshot, now time.Time) *rc.OrderDO {
|
|
payTime := gtime.NewFromTime(now)
|
|
createdDept := main.CreatedDept
|
|
if createdDept <= 0 {
|
|
createdDept = payerDeptID
|
|
}
|
|
payType := consts.PayTypeBalance
|
|
balanceAmount := totalAmount
|
|
offlinePaidAmount := 0.0
|
|
payOrderSN := orderSN
|
|
if payment != nil {
|
|
payType = payment.PayType
|
|
balanceAmount = payment.BalanceAmount
|
|
offlinePaidAmount = payment.OfflinePaidAmount
|
|
if payment.BalanceDetailSerialNo != "" {
|
|
payOrderSN = payment.BalanceDetailSerialNo
|
|
}
|
|
if !payment.PaymentTime.IsZero() {
|
|
payTime = gtime.NewFromTime(payment.PaymentTime)
|
|
}
|
|
}
|
|
return &rc.OrderDO{
|
|
OrderSn: orderSN,
|
|
ParentOrderId: int64(0),
|
|
ParentOrderSn: "",
|
|
CustomerType: consts.CustomerTypeAgent,
|
|
CustomerAgentId: payerDeptID,
|
|
EndUserId: int64(0),
|
|
AgentId: payerDeptID,
|
|
OrderType: consts.OrderTypeCardSubscribe,
|
|
OrderStatus: consts.OrderStatusCompleted,
|
|
ShippingStatus: 0,
|
|
ReturnStatus: consts.OrderReturnStatusNone,
|
|
Consignee: "",
|
|
Mobile: "",
|
|
Email: "",
|
|
Address: "",
|
|
AreaCode: "",
|
|
BuyerNote: "加油包自动生成订单",
|
|
AdminNote: "",
|
|
Remark: "加油包自动生成订单",
|
|
ShippingMethod: consts.ShippingMethodNone,
|
|
LogisticsId: int64(0),
|
|
LogisticsName: "",
|
|
ShippingTypeId: int64(0),
|
|
ShippingTypeName: "",
|
|
TrackingNo: "",
|
|
PayType: payType,
|
|
PayStatus: consts.PayStatusPaid,
|
|
PayOrderSn: payOrderSN,
|
|
PayTime: payTime,
|
|
TotalAmount: totalAmount,
|
|
ProductAmount: totalAmount,
|
|
ShippingFee: 0,
|
|
ServiceFee: 0,
|
|
InvoiceFee: 0,
|
|
CouponAmount: 0,
|
|
DiscountAmount: 0,
|
|
Balance: balanceAmount,
|
|
PaidAmount: totalAmount,
|
|
OnlinePaidAmount: 0,
|
|
OfflinePaidAmount: offlinePaidAmount,
|
|
UnpaidAmount: 0,
|
|
UnrefundAmount: 0,
|
|
IsNeedCommission: 0,
|
|
DistributionStatus: 0,
|
|
TenantId: main.TenantId,
|
|
CreatedDept: createdDept,
|
|
CreatedBy: main.CreatedBy,
|
|
CreatedAt: payTime,
|
|
UpdatedBy: main.CreatedBy,
|
|
UpdatedAt: payTime,
|
|
}
|
|
}
|
|
|
|
func buildAddPackageOrderItemDO(orderID int64, orderSN string, main *rc.BatchMainRow, payerDeptID, createdDeptID int64, plan *detailPlan, now time.Time) *rc.OrderItemDO {
|
|
if plan == nil || plan.price == nil {
|
|
return nil
|
|
}
|
|
createdDept := createdDeptID
|
|
if createdDept <= 0 {
|
|
createdDept = payerDeptID
|
|
}
|
|
skuData, _ := json.Marshal([]map[string]any{
|
|
{"skuName": "targetCode", "skuValue": plan.targetCode},
|
|
{"skuName": "package", "skuValue": plan.price.PackageName, "packageCode": plan.price.PackageCode, "packageType": plan.price.PackageType},
|
|
{"skuName": "effectiveMode", "skuValue": effectiveModeText(plan.effectiveMode)},
|
|
})
|
|
extraSKUData, _ := json.Marshal(map[string]any{
|
|
"businessBatchNo": main.BatchNo,
|
|
"targetCode": plan.targetCode,
|
|
"packageCode": plan.price.PackageCode,
|
|
"effectiveMode": plan.effectiveMode,
|
|
"effectiveModeText": effectiveModeText(plan.effectiveMode),
|
|
"remark": plan.remark,
|
|
})
|
|
return &rc.OrderItemDO{
|
|
OrderId: orderID,
|
|
OrderSn: orderSN,
|
|
CustomerType: consts.CustomerTypeAgent,
|
|
CustomerAgentId: payerDeptID,
|
|
EndUserId: int64(0),
|
|
Price: plan.amount,
|
|
Quantity: 1,
|
|
ProductId: plan.price.PackageID,
|
|
ProductName: plan.price.PackageName,
|
|
ProductSn: plan.price.PackageCode,
|
|
PicThumb: "",
|
|
SkuId: int64(0),
|
|
SkuData: string(skuData),
|
|
DeliveryQuantity: 0,
|
|
ProductType: consts.ProductTypeCard,
|
|
IsGift: 0,
|
|
AgentId: payerDeptID,
|
|
PrepayPrice: 0,
|
|
Commission: "",
|
|
OriginPrice: plan.amount,
|
|
PromotionData: "",
|
|
ExtraSkuData: string(extraSKUData),
|
|
PayMode: consts.PayModeYearMonth,
|
|
ServiceBeginTime: gtime.NewFromTime(plan.effectiveTime),
|
|
ServiceEndTime: gtime.NewFromTime(plan.expireTime),
|
|
TenantId: main.TenantId,
|
|
CreatedDept: createdDept,
|
|
CreatedBy: main.CreatedBy,
|
|
CreatedAt: gtime.NewFromTime(now),
|
|
UpdatedBy: main.CreatedBy,
|
|
UpdatedAt: gtime.NewFromTime(now),
|
|
}
|
|
}
|
|
|
|
func collectOrderSNs(plans []*detailPlan) []string {
|
|
if len(plans) == 0 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(plans))
|
|
out := make([]string, 0, len(plans))
|
|
for _, plan := range plans {
|
|
if plan == nil || strings.TrimSpace(plan.orderSN) == "" {
|
|
continue
|
|
}
|
|
orderSN := strings.TrimSpace(plan.orderSN)
|
|
if _, ok := seen[orderSN]; ok {
|
|
continue
|
|
}
|
|
seen[orderSN] = struct{}{}
|
|
out = append(out, orderSN)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func (e *executor) insertCardBind(ctx context.Context, tx gdb.TX, plan *detailPlan, main *rc.BatchMainRow, orderSN, subscriptionNo string, now time.Time) error {
|
|
nowGT := gtime.NewFromTime(now)
|
|
bind := &rc.SimCardBindPkg{
|
|
BatchNo: main.BatchNo,
|
|
Iccid: plan.targetCode,
|
|
Msisdn: "",
|
|
DeptId: plan.customerDeptID,
|
|
PackageType: plan.price.PackageType,
|
|
PackageCode: plan.packageCode,
|
|
PreviousPackageCode: "",
|
|
BusinessType: addPackageBizType,
|
|
EffectiveStatus: map[bool]int{true: consts.EffectiveStatusEffective, false: consts.EffectiveStatusPending}[plan.effectiveNow],
|
|
HandleTime: nowGT,
|
|
EffectiveTime: gtime.NewFromTime(plan.effectiveTime),
|
|
ExpectedExpireTime: gtime.NewFromTime(plan.expireTime),
|
|
ActualExpireTime: nil,
|
|
DurationMonths: 1,
|
|
GiftDurationMonths: 0,
|
|
OrderCode: orderSN,
|
|
SubscriptionNo: subscriptionNo,
|
|
Remark: plan.remark,
|
|
TenantId: main.TenantId,
|
|
CreatedDept: main.CreatedDept,
|
|
CreatedBy: main.CreatedBy,
|
|
CreatedAt: nowGT,
|
|
}
|
|
if err := e.repo.Tables().InsertBind(ctx, tx, bind); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *executor) insertPoolBind(ctx context.Context, tx gdb.TX, plan *detailPlan, main *rc.BatchMainRow, orderSN, subscriptionNo string, now time.Time) error {
|
|
nowGT := gtime.NewFromTime(now)
|
|
_, err := tx.Ctx(ctx).Model("t_sim_pool_bind_package").Data(map[string]any{
|
|
"subscription_no": subscriptionNo,
|
|
"batch_no": main.BatchNo,
|
|
"pool_no": plan.targetCode,
|
|
"dept_id": plan.customerDeptID,
|
|
"package_type": plan.price.PackageType,
|
|
"package_code": plan.packageCode,
|
|
"previous_package_code": "",
|
|
"business_type": addPackageBizType,
|
|
"effective_status": map[bool]int{true: consts.EffectiveStatusEffective, false: consts.EffectiveStatusPending}[plan.effectiveNow],
|
|
"handle_time": nowGT,
|
|
"effective_time": gtime.NewFromTime(plan.effectiveTime),
|
|
"expected_expire_time": gtime.NewFromTime(plan.expireTime),
|
|
"actual_expire_time": nil,
|
|
"duration_months": 1,
|
|
"gift_duration_months": 0,
|
|
"order_code": orderSN,
|
|
"remark": plan.remark,
|
|
"tenant_id": main.TenantId,
|
|
"created_dept": main.CreatedDept,
|
|
"created_by": main.CreatedBy,
|
|
"created_at": nowGT,
|
|
}).Insert()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "insert flow pool add package failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *executor) updateFlowPool(ctx context.Context, tx gdb.TX, plans []*detailPlan, tenantID string, userID int64, now time.Time) error {
|
|
type poolDelta struct {
|
|
pool *flowPoolRow
|
|
flow float64
|
|
}
|
|
pools := make(map[int64]*poolDelta)
|
|
for _, plan := range plans {
|
|
if plan == nil || !plan.effectiveNow || plan.price == nil || plan.pool == nil {
|
|
continue
|
|
}
|
|
delta, ok := pools[plan.pool.Id]
|
|
if !ok {
|
|
delta = &poolDelta{pool: plan.pool}
|
|
pools[plan.pool.Id] = delta
|
|
}
|
|
delta.flow = roundAmount(delta.flow + plan.price.FlowMb)
|
|
}
|
|
if len(pools) == 0 {
|
|
return nil
|
|
}
|
|
for _, delta := range pools {
|
|
if delta == nil || delta.pool == nil || delta.flow <= 0 {
|
|
continue
|
|
}
|
|
_, err := tx.Ctx(ctx).Model("t_sim_flow_pool").
|
|
Where("id", delta.pool.Id).
|
|
Data(map[string]any{
|
|
"add_on_flow_mb": roundAmount(delta.pool.AddOnFlowMb + delta.flow),
|
|
"total_flow_mb": roundAmount(delta.pool.TotalFlowMb + delta.flow),
|
|
"remain_flow_mb": roundAmount(delta.pool.RemainFlowMb + delta.flow),
|
|
"updated_by": userID,
|
|
"updated_at": gtime.NewFromTime(now),
|
|
}).
|
|
Update()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "update flow pool add-on flow failed")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *executor) updateCardTopupFlow(ctx context.Context, tx gdb.TX, plans []*detailPlan, tenantID string, userID int64, now time.Time) error {
|
|
type cardDelta struct {
|
|
card *rc.SimCardInfo
|
|
flow float64
|
|
}
|
|
cards := make(map[int64]*cardDelta)
|
|
for _, plan := range plans {
|
|
if plan == nil || !plan.effectiveNow || plan.price == nil || plan.card == nil {
|
|
continue
|
|
}
|
|
delta, ok := cards[plan.card.Id]
|
|
if !ok {
|
|
delta = &cardDelta{card: plan.card}
|
|
cards[plan.card.Id] = delta
|
|
}
|
|
delta.flow = roundAmount(delta.flow + plan.price.FlowMb)
|
|
}
|
|
if len(cards) == 0 {
|
|
return nil
|
|
}
|
|
for _, delta := range cards {
|
|
if delta == nil || delta.card == nil || delta.card.Id <= 0 || delta.flow <= 0 {
|
|
continue
|
|
}
|
|
update := map[string]any{
|
|
"package_total_flow_mb": roundAmount(delta.card.PackageTotalFlowMb + delta.flow),
|
|
"updated_by": userID,
|
|
"updated_at": gtime.NewFromTime(now),
|
|
}
|
|
mod := tx.Ctx(ctx).Model("t_sim_card_info").Where("id", delta.card.Id)
|
|
if strings.TrimSpace(tenantID) != "" {
|
|
mod = mod.Where("tenant_id", tenantID)
|
|
}
|
|
if _, err := mod.Data(update).Update(); err != nil {
|
|
return gerror.Wrap(err, "update card add-on flow failed")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizePaymentIntent(intent PaymentIntent, isTopDept bool) (PaymentIntent, error) {
|
|
intent.PaymentMode = strings.TrimSpace(intent.PaymentMode)
|
|
if intent.PaymentMode == "" {
|
|
intent.PaymentMode = "balance"
|
|
}
|
|
switch intent.PaymentMode {
|
|
case "balance":
|
|
intent.OfflineContractCompleted = false
|
|
return intent, nil
|
|
case "offline":
|
|
if !isTopDept {
|
|
return PaymentIntent{}, gerror.New("当前组织加油包必须使用余额支付")
|
|
}
|
|
if !intent.OfflineContractCompleted {
|
|
return PaymentIntent{}, gerror.New("请确认线下合同、销售和走账已完成")
|
|
}
|
|
return intent, nil
|
|
default:
|
|
return PaymentIntent{}, gerror.New("加油包付款方式不正确")
|
|
}
|
|
}
|
|
|
|
func (e *executor) executePayment(
|
|
ctx context.Context,
|
|
tx gdb.TX,
|
|
main *rc.BatchMainRow,
|
|
intent PaymentIntent,
|
|
payerDeptID int64,
|
|
amount float64,
|
|
orderSN string,
|
|
now time.Time,
|
|
isTopDept bool,
|
|
) (*PaymentSnapshot, error) {
|
|
if main == nil {
|
|
return nil, gerror.New("加油包批次不能为空")
|
|
}
|
|
snapshot := &PaymentSnapshot{
|
|
PaymentMode: intent.PaymentMode,
|
|
PayStatus: consts.PayStatusPaid,
|
|
PayAmount: amount,
|
|
PaymentTime: now,
|
|
PayerDeptID: payerDeptID,
|
|
PayerUserID: main.CreatedBy,
|
|
IsTopDept: isTopDept,
|
|
}
|
|
if intent.PaymentMode == "offline" {
|
|
snapshot.PayType = consts.PayTypeOffline
|
|
snapshot.OfflinePaidAmount = amount
|
|
return snapshot, nil
|
|
}
|
|
serialNo := addPackageBalanceSerialNo(main.BatchNo, payerDeptID)
|
|
before, after, err := e.debitBalance(
|
|
ctx,
|
|
tx,
|
|
main.TenantId,
|
|
payerDeptID,
|
|
amount,
|
|
main.CreatedBy,
|
|
orderSN,
|
|
serialNo,
|
|
now,
|
|
main.CreatedDept,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot.PayType = consts.PayTypeBalance
|
|
snapshot.BalanceAmount = amount
|
|
snapshot.BalanceBefore = before
|
|
snapshot.BalanceAfter = after
|
|
snapshot.BalanceDetailSerialNo = serialNo
|
|
return snapshot, nil
|
|
}
|
|
|
|
func addPackageBalanceSerialNo(batchNo string, payerDeptID int64) string {
|
|
batchNo = strings.TrimSpace(batchNo)
|
|
if batchNo == "" || payerDeptID <= 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("ADPPAY%sD%d", batchNo, payerDeptID)
|
|
}
|
|
|
|
func (e *executor) isTopDept(ctx context.Context, tenantID string, deptID int64) (bool, error) {
|
|
if strings.TrimSpace(tenantID) == "" || deptID <= 0 {
|
|
return false, gerror.New("当前用户未绑定组织或租户")
|
|
}
|
|
var dept sysDeptRow
|
|
if err := e.repo.DB().Model("sys_dept").Ctx(ctx).
|
|
Where("tenant_id", tenantID).
|
|
Where("dept_id", deptID).
|
|
WhereNull("deleted_at").
|
|
Scan(&dept); err != nil {
|
|
return false, gerror.Wrap(err, "query current department failed")
|
|
}
|
|
if dept.DeptId == 0 {
|
|
return false, gerror.New("当前组织不存在")
|
|
}
|
|
return dept.ParentId == 0, nil
|
|
}
|
|
|
|
func (e *executor) recoverAppliedBusiness(ctx context.Context, task *batchTask) (string, bool, error) {
|
|
if task == nil || task.main == nil {
|
|
return "", false, nil
|
|
}
|
|
type existingOrderItem struct {
|
|
OrderSN string `orm:"order_sn"`
|
|
ExtraSKUData string `orm:"extra_sku_data"`
|
|
}
|
|
var existingItems []existingOrderItem
|
|
if err := e.repo.DB().Model("t_order_item oi").Ctx(ctx).
|
|
Fields("oi.order_sn, oi.extra_sku_data").
|
|
LeftJoin("t_order o", "o.id = oi.order_id").
|
|
Where("o.tenant_id", task.main.TenantId).
|
|
Where("oi.extra_sku_data LIKE ?", "%\"businessBatchNo\":\""+task.main.BatchNo+"\"%").
|
|
OrderAsc("oi.item_id").
|
|
Scan(&existingItems); err != nil {
|
|
return "", false, gerror.Wrap(err, "query existing add package orders failed")
|
|
}
|
|
if len(existingItems) == 0 {
|
|
return "", false, nil
|
|
}
|
|
|
|
orderByPlan := make(map[string]string, len(existingItems))
|
|
for _, item := range existingItems {
|
|
orderSN := strings.TrimSpace(item.OrderSN)
|
|
if orderSN == "" || strings.TrimSpace(item.ExtraSKUData) == "" {
|
|
continue
|
|
}
|
|
var extra struct {
|
|
BusinessBatchNo string `json:"businessBatchNo"`
|
|
TargetCode string `json:"targetCode"`
|
|
PackageCode string `json:"packageCode"`
|
|
}
|
|
if err := json.Unmarshal([]byte(item.ExtraSKUData), &extra); err != nil {
|
|
return "", false, gerror.Wrap(err, "decode existing add package order item failed")
|
|
}
|
|
if extra.BusinessBatchNo != task.main.BatchNo {
|
|
continue
|
|
}
|
|
key := addPackageRecoveryKey(extra.TargetCode, extra.PackageCode)
|
|
if key == "" {
|
|
return "", false, gerror.New("existing add package order item is missing target or package")
|
|
}
|
|
if previous, ok := orderByPlan[key]; ok && previous != orderSN {
|
|
return "", false, gerror.New("existing add package orders contain duplicate target records")
|
|
}
|
|
orderByPlan[key] = orderSN
|
|
}
|
|
|
|
matched := 0
|
|
firstOrderSN := ""
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.reason != "" {
|
|
continue
|
|
}
|
|
key := addPackageRecoveryKey(plan.targetCode, plan.packageCode)
|
|
orderSN, ok := orderByPlan[key]
|
|
if !ok {
|
|
return "", false, gerror.New("existing add package orders are incomplete; refusing to retry business writes")
|
|
}
|
|
plan.orderSN = orderSN
|
|
if firstOrderSN == "" {
|
|
firstOrderSN = orderSN
|
|
}
|
|
matched++
|
|
}
|
|
if matched == 0 {
|
|
return "", false, nil
|
|
}
|
|
return firstOrderSN, true, nil
|
|
}
|
|
|
|
func addPackageRecoveryKey(targetCode, packageCode string) string {
|
|
targetCode = strings.TrimSpace(targetCode)
|
|
packageCode = strings.TrimSpace(packageCode)
|
|
if targetCode == "" || packageCode == "" {
|
|
return ""
|
|
}
|
|
return targetCode + "\\x00" + packageCode
|
|
}
|
|
|
|
func (e *executor) ensureNoActiveTopup(ctx context.Context, tx gdb.TX, tenantID, targetCode string, now time.Time) error {
|
|
targetCode = strings.TrimSpace(targetCode)
|
|
tenantID = strings.TrimSpace(tenantID)
|
|
if targetCode == "" || tenantID == "" {
|
|
return nil
|
|
}
|
|
if e.poolMode {
|
|
return ensureNoActiveTopupRecord(ctx, tx, rc.TableSimPoolBindPackage, "pool_no", consts.PackageTypePoolAddPackage, tenantID, targetCode, now, "当前流量池存在未过期加油包,不能重复订购")
|
|
}
|
|
return ensureNoActiveTopupRecord(ctx, tx, rc.TableSimCardBindPkg, "iccid", consts.PackageTypeCardAddPackage, tenantID, targetCode, now, "当前卡存在未过期加油包,不能重复订购")
|
|
}
|
|
|
|
func ensureNoActiveTopupRecord(ctx context.Context, tx gdb.TX, table, targetColumn string, packageType int, tenantID, targetCode string, now time.Time, message string) error {
|
|
model := g.DB().Model(table).Ctx(ctx)
|
|
if tx != nil {
|
|
model = tx.Ctx(ctx).Model(table)
|
|
}
|
|
model = model.
|
|
Where("tenant_id", tenantID).
|
|
Where(targetColumn, targetCode).
|
|
Where("package_type", packageType).
|
|
WhereIn("effective_status", []int{consts.EffectiveStatusPending, consts.EffectiveStatusEffective}).
|
|
Where("(expected_expire_time IS NULL OR expected_expire_time > ?)", gtime.NewFromTime(now))
|
|
if tx != nil {
|
|
model = model.LockUpdate()
|
|
}
|
|
var row struct {
|
|
Id int64 `orm:"id"`
|
|
}
|
|
err := model.Fields("id").Limit(1).Scan(&row)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return gerror.Wrap(err, "查询未过期加油包失败")
|
|
}
|
|
if row.Id > 0 {
|
|
return gerror.New(message)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *executor) debitBalance(ctx context.Context, tx gdb.TX, tenantID string, deptID int64, amount float64, userID int64, orderSN, serialNo string, now time.Time, createdDept int64) (float64, float64, error) {
|
|
serialNo = strings.TrimSpace(serialNo)
|
|
if serialNo == "" {
|
|
return 0, 0, gerror.New("加油包余额流水号不能为空")
|
|
}
|
|
var existing balanceDetailRow
|
|
if err := tx.Ctx(ctx).Model("t_balance_detail").
|
|
Where("tenant_id", tenantID).
|
|
Where("serial_no", serialNo).
|
|
LockUpdate().
|
|
Scan(&existing); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return 0, 0, gerror.Wrap(err, "query add package balance detail failed")
|
|
}
|
|
if strings.TrimSpace(existing.SerialNo) != "" {
|
|
return 0, 0, gerror.New("当前加油包批次已扣款,请勿重复提交")
|
|
}
|
|
var account agentBalanceRow
|
|
if err := tx.Ctx(ctx).Model("t_agent_balance").
|
|
Where("dept_id", deptID).
|
|
Where("tenant_id", tenantID).
|
|
LockUpdate().
|
|
Scan(&account); err != nil {
|
|
return 0, 0, gerror.Wrap(err, "lock balance account failed")
|
|
}
|
|
if account.Id == 0 {
|
|
return 0, 0, gerror.New("账户余额不存在")
|
|
}
|
|
if account.Status != addPackageAccountStatusOK {
|
|
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)
|
|
nowGT := gtime.NewFromTime(now)
|
|
if _, err := tx.Ctx(ctx).Model("t_agent_balance").
|
|
Where("id", account.Id).
|
|
Data(map[string]any{
|
|
"balance": after,
|
|
"updated_by": userID,
|
|
"updated_at": nowGT,
|
|
}).
|
|
Update(); err != nil {
|
|
return 0, 0, gerror.Wrap(err, "update agent balance failed")
|
|
}
|
|
if _, err := tx.Ctx(ctx).Model("t_balance_detail").Data(balanceDetailRow{
|
|
DeptId: deptID,
|
|
SerialNo: serialNo,
|
|
TransactionTime: nowGT,
|
|
IncomeExpenseType: 1,
|
|
TransactionType: addPackageTransactionType,
|
|
TransactionChannel: addPackageTransactionChan,
|
|
TransactionStatus: addPackageTransactionPaid,
|
|
Amount: amount,
|
|
BeforeBalance: before,
|
|
BalanceAfter: after,
|
|
ChannelSerialNo: serialNo,
|
|
OrderSn: orderSN,
|
|
Remark: fmt.Sprintf("加油包余额支付,批次号:%s", serialNo),
|
|
TenantId: tenantID,
|
|
CreatedDept: createdDept,
|
|
CreatedBy: userID,
|
|
CreatedAt: nowGT,
|
|
}).Insert(); err != nil {
|
|
return 0, 0, gerror.Wrap(err, "insert balance detail failed")
|
|
}
|
|
return before, after, nil
|
|
}
|
|
|
|
func (e *executor) resolvePrice(ctx context.Context, tenantID string, customerDeptID int64, packageCode string) (*rc.FlowPackageInfo, *PriceSnapshot, error) {
|
|
pkg, err := e.repo.Tables().LoadPackageByCode(ctx, tenantID, packageCode)
|
|
if err != nil || pkg == nil {
|
|
return nil, nil, gerror.New("加油包套餐不存在")
|
|
}
|
|
if pkg.Enabled != 1 {
|
|
return nil, nil, gerror.New("加油包套餐未启用")
|
|
}
|
|
parentDeptID, err := e.loadParentDeptID(ctx, tenantID, customerDeptID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
snapshot := &PriceSnapshot{
|
|
PackageID: pkg.Id,
|
|
PackageCode: packageCode,
|
|
PackageName: pkg.PackageName,
|
|
PackageType: pkg.PackageType,
|
|
PriceSource: "system",
|
|
FlowMb: pkg.FlowMb,
|
|
}
|
|
salePrice, err := renewchangeservice.ResolveEffectiveSalePrice(ctx, e.repo.Tables(), tenantID, parentDeptID, customerDeptID, pkg)
|
|
if err != nil {
|
|
return nil, nil, gerror.Wrap(err, "calculate add package effective price failed")
|
|
}
|
|
snapshot.PriceSource = salePrice.Rule.PriceSource
|
|
snapshot.AgentDefaultPriceID = salePrice.Rule.AgentDefaultPriceId
|
|
snapshot.TargetPriceID = salePrice.Rule.TargetPriceId
|
|
snapshot.UnitPrice = salePrice.AgentShipmentPrice
|
|
return pkg, snapshot, nil
|
|
}
|
|
|
|
func (e *executor) loadParentDeptID(ctx context.Context, tenantID string, deptID int64) (int64, error) {
|
|
if deptID <= 0 {
|
|
return 0, nil
|
|
}
|
|
var dept sysDeptRow
|
|
if err := e.repo.DB().Model("sys_dept").Ctx(ctx).
|
|
Where("tenant_id", tenantID).
|
|
Where("dept_id", deptID).
|
|
WhereNull("deleted_at").
|
|
Scan(&dept); err != nil {
|
|
return 0, gerror.Wrap(err, "query parent dept failed")
|
|
}
|
|
if dept.DeptId == 0 {
|
|
return 0, nil
|
|
}
|
|
return dept.ParentId, nil
|
|
}
|
|
|
|
func (e *executor) loadPool(ctx context.Context, tenantID, flowPoolCode string) (*flowPoolRow, error) {
|
|
var row flowPoolRow
|
|
if err := e.repo.DB().Model("t_sim_flow_pool").Ctx(ctx).
|
|
Where("tenant_id", tenantID).
|
|
Where("flow_pool_code", flowPoolCode).
|
|
Scan(&row); err != nil {
|
|
return nil, gerror.Wrap(err, "query flow pool failed")
|
|
}
|
|
if row.Id == 0 {
|
|
return nil, nil
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func (e *executor) nowTime() time.Time {
|
|
if e.nowFunc == nil {
|
|
return time.Now()
|
|
}
|
|
return e.nowFunc()
|
|
}
|
|
|
|
func effectiveModeFrom(info *gjson.Json, snapshot *DetailSnapshot) (string, error) {
|
|
if snapshot != nil && strings.TrimSpace(snapshot.EffectiveMode) != "" {
|
|
return normalizeEffectiveMode(snapshot.EffectiveMode)
|
|
}
|
|
if info != nil && !info.IsNil() {
|
|
if mode := strings.TrimSpace(info.Get("effectiveMode").String()); mode != "" {
|
|
return normalizeEffectiveMode(mode)
|
|
}
|
|
}
|
|
return effectiveModeCurrentMonth, nil
|
|
}
|
|
|
|
func normalizeEffectiveMode(mode string) (string, error) {
|
|
switch strings.TrimSpace(mode) {
|
|
case "", "immediate", "current_month", "this_month", "本月生效", "本月", "当月", "当月生效":
|
|
return effectiveModeCurrentMonth, nil
|
|
case "next_month", "nextMonth", "次月生效", "次月", "下月", "下月生效":
|
|
return "", gerror.New("加油包只支持本月生效")
|
|
default:
|
|
return "", gerror.New("加油包生效方式不正确")
|
|
}
|
|
}
|
|
|
|
func calcMonthWindow(mode string, now time.Time) (time.Time, time.Time, bool, error) {
|
|
if _, err := normalizeEffectiveMode(mode); err != nil {
|
|
return time.Time{}, time.Time{}, false, err
|
|
}
|
|
start := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location())
|
|
next := time.Date(start.Year(), start.Month()+1, 1, 0, 0, 0, 0, start.Location())
|
|
return start, next.Add(-time.Second), !start.After(now), nil
|
|
}
|
|
|
|
func effectiveModeText(mode string) string {
|
|
_, _ = normalizeEffectiveMode(mode)
|
|
return effectiveModeCurrentText
|
|
}
|
|
|
|
func buildDetailSnapshot(plan *detailPlan, orderSN string) *gjson.Json {
|
|
if plan == nil {
|
|
return nil
|
|
}
|
|
data := map[string]any{
|
|
"targetCode": plan.targetCode,
|
|
"packageCode": plan.packageCode,
|
|
"basePackageKey": plan.basePackageKey,
|
|
"effectiveMode": plan.effectiveMode,
|
|
"effectiveModeText": effectiveModeText(plan.effectiveMode),
|
|
"remark": plan.remark,
|
|
"orderSn": orderSN,
|
|
}
|
|
if plan.price != nil {
|
|
data["priceSnapshot"] = plan.price
|
|
data["packageName"] = plan.price.PackageName
|
|
data["packageType"] = plan.price.PackageType
|
|
data["unitPrice"] = plan.price.UnitPrice
|
|
data["flowMb"] = plan.price.FlowMb
|
|
}
|
|
if !plan.effectiveTime.IsZero() {
|
|
data["effectiveTime"] = gtime.NewFromTime(plan.effectiveTime).Format("Y-m-d H:i:s")
|
|
}
|
|
if !plan.expireTime.IsZero() {
|
|
data["expectedExpireTime"] = gtime.NewFromTime(plan.expireTime).Format("Y-m-d H:i:s")
|
|
}
|
|
if plan.effectiveNow {
|
|
data["effectiveStatus"] = consts.EffectiveStatusEffective
|
|
} else {
|
|
data["effectiveStatus"] = consts.EffectiveStatusPending
|
|
}
|
|
return gjson.New(data)
|
|
}
|
|
|
|
func buildBatchPriceSnapshot(task *batchTask) map[string]any {
|
|
if task == nil {
|
|
return nil
|
|
}
|
|
rows := make([]map[string]any, 0, len(task.plans))
|
|
for _, plan := range task.plans {
|
|
if plan == nil || plan.price == nil {
|
|
continue
|
|
}
|
|
rows = append(rows, map[string]any{
|
|
"targetCode": plan.targetCode,
|
|
"basePackageKey": plan.basePackageKey,
|
|
"packageCode": plan.packageCode,
|
|
"effectiveMode": plan.effectiveMode,
|
|
"effectiveModeText": effectiveModeText(plan.effectiveMode),
|
|
"priceSource": plan.price.PriceSource,
|
|
"unitPrice": plan.price.UnitPrice,
|
|
})
|
|
}
|
|
return map[string]any{"rows": rows}
|
|
}
|
|
|
|
func mergeBatchInfo(prev *gjson.Json, addition map[string]any) map[string]any {
|
|
out := map[string]any{}
|
|
if prev != nil {
|
|
for k, v := range prev.Map() {
|
|
out[k] = v
|
|
}
|
|
}
|
|
for k, v := range addition {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func firstFailureReason(plans []*detailPlan) string {
|
|
for _, plan := range plans {
|
|
if plan != nil && plan.reason != "" {
|
|
return plan.reason
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func packageCodesFromJSON(jsonValue *gjson.Json) []string {
|
|
if jsonValue == nil || jsonValue.IsNil() {
|
|
return nil
|
|
}
|
|
values := jsonValue.Interfaces()
|
|
out := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, raw := range values {
|
|
code := strings.TrimSpace(fmt.Sprint(raw))
|
|
if code == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[code]; ok {
|
|
continue
|
|
}
|
|
seen[code] = struct{}{}
|
|
out = append(out, code)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
target = strings.TrimSpace(target)
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func normalizePriceRatio(ratio float64) float64 {
|
|
if ratio > 10 {
|
|
return ratio / 100
|
|
}
|
|
return ratio
|
|
}
|
|
|
|
func roundAmount(value float64) float64 {
|
|
if value < 0 {
|
|
return 0
|
|
}
|
|
return float64(int(value*100+0.5)) / 100
|
|
}
|