chore: 初始开源发布
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),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
@@ -0,0 +1,831 @@
|
||||
package addpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"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"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
|
||||
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
|
||||
renewchangeservice "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/renewchange"
|
||||
)
|
||||
|
||||
const addPackagePrepareProcessingRetryAfter = 2 * time.Minute
|
||||
|
||||
type PrepareExecutor struct {
|
||||
repo *renewchangeservice.Repository
|
||||
inner *executor
|
||||
targetBatchType int
|
||||
nowFunc NowFunc
|
||||
}
|
||||
|
||||
func NewCardPrepareExecutor(repo *renewchangeservice.Repository) *PrepareExecutor {
|
||||
if repo == nil {
|
||||
repo = renewchangeservice.NewRepository()
|
||||
}
|
||||
return &PrepareExecutor{
|
||||
repo: repo,
|
||||
inner: &executor{repo: repo, batchType: consts.BatchTypePrepare, nowFunc: timeNow},
|
||||
targetBatchType: consts.BatchTypeCardAddPackage,
|
||||
nowFunc: timeNow,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPoolPrepareExecutor(repo *renewchangeservice.Repository) *PrepareExecutor {
|
||||
if repo == nil {
|
||||
repo = renewchangeservice.NewRepository()
|
||||
}
|
||||
return &PrepareExecutor{
|
||||
repo: repo,
|
||||
inner: &executor{repo: repo, batchType: consts.BatchTypePrepare, poolMode: true, nowFunc: timeNow},
|
||||
targetBatchType: consts.BatchTypePoolAddPackage,
|
||||
nowFunc: timeNow,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) CanExecuteProcessing(_ context.Context, main *entity.BatchMain) (bool, error) {
|
||||
if main == nil || main.BatchType != consts.BatchTypePrepare || main.BatchAsyncStatus != consts.BatchAsyncStatusProcessing {
|
||||
return false, nil
|
||||
}
|
||||
if targetBatchTypeFromInfo(main.BatchInfo) != p.targetBatchType {
|
||||
return false, nil
|
||||
}
|
||||
if main.StartTime == nil || main.StartTime.IsZero() {
|
||||
return true, nil
|
||||
}
|
||||
return p.now().Sub(main.StartTime.Time) >= addPackagePrepareProcessingRetryAfter, nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) Execute(ctx context.Context, batchNo string) error {
|
||||
batchNo = strings.TrimSpace(batchNo)
|
||||
if batchNo == "" {
|
||||
return gerror.New("add package prepare batch number is empty")
|
||||
}
|
||||
main, err := p.repo.LoadProcessingBatch(ctx, batchNo, consts.BatchTypePrepare)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if targetBatchTypeFromInfo(main.BatchInfo) != p.targetBatchType {
|
||||
return p.finalizePrepareFailure(ctx, main, "加油包预处理批次类型不匹配", nil)
|
||||
}
|
||||
details, err := p.repo.LoadRunningDetails(ctx, batchNo, consts.BatchTypePrepare)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "load add package prepare details failed")
|
||||
}
|
||||
task := &batchTask{
|
||||
main: main,
|
||||
total: len(details),
|
||||
plans: make([]*detailPlan, 0, len(details)),
|
||||
paymentIntent: readPaymentIntent(main.BatchInfo),
|
||||
paymentSnapshot: make(map[int64]*PaymentSnapshot),
|
||||
prepareOnly: true,
|
||||
}
|
||||
if len(details) == 0 {
|
||||
return p.finalizePrepareFailure(ctx, main, "加油包预处理批次没有可处理明细", task)
|
||||
}
|
||||
p.inner.nowFunc = p.now
|
||||
if err := p.inner.buildTask(ctx, task, details); err != nil {
|
||||
return p.finalizePrepareFailure(ctx, main, err.Error(), task)
|
||||
}
|
||||
if task.fail > 0 || task.valid != len(details) {
|
||||
reason := strings.TrimSpace(task.reason)
|
||||
if reason == "" {
|
||||
reason = "加油包预处理存在不满足条件的明细"
|
||||
}
|
||||
return p.finalizePrepareFailure(ctx, main, reason, task)
|
||||
}
|
||||
return p.finalizePrepareSuccess(ctx, task)
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) finalizePrepareFailure(ctx context.Context, main *rc.BatchMainRow, reason string, task *batchTask) error {
|
||||
if main == nil {
|
||||
return gerror.New("add package prepare batch is empty")
|
||||
}
|
||||
if isAutoConfirmPrepare(main) {
|
||||
return p.finalizeAutoPrepareFailure(ctx, main, reason, task)
|
||||
}
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
reason = "加油包预处理失败"
|
||||
}
|
||||
now := p.now()
|
||||
failCount := main.TotalCount
|
||||
err := p.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if task != nil {
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.detail == nil {
|
||||
continue
|
||||
}
|
||||
rowReason := strings.TrimSpace(plan.reason)
|
||||
if rowReason == "" {
|
||||
rowReason = reason
|
||||
}
|
||||
if err := p.repo.Tables().UpdateBatchDetailSnapshot(ctx, tx, main.BatchNo, main.BatchType, plan.detail.RowNo, consts.BatchDetailStatusFailed, buildDetailSnapshot(plan, ""), rowReason, main.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := p.repo.MarkAllDetailsFailed(ctx, tx, main.BatchNo, main.BatchType, reason, main.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.repo.FinalizeBatchMain(ctx, tx, main.BatchNo, main.BatchType, &rc.BatchMainRow{
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
|
||||
RunResult: consts.BatchRunResultFailed,
|
||||
FailReason: reason,
|
||||
TotalCount: main.TotalCount,
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: failCount,
|
||||
EndTime: gtime.NewFromTime(now),
|
||||
UpdatedBy: main.CreatedBy,
|
||||
})
|
||||
})
|
||||
return gerror.Wrap(err, "finalize add package prepare failure failed")
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) finalizePrepareSuccess(ctx context.Context, task *batchTask) error {
|
||||
if task == nil || task.main == nil {
|
||||
return gerror.New("add package prepare task is empty")
|
||||
}
|
||||
now := p.now()
|
||||
info, err := p.buildPrepareInfo(ctx, task)
|
||||
if err != nil {
|
||||
if isAutoConfirmPrepare(task.main) {
|
||||
return p.finalizeAutoPrepareFailure(ctx, task.main, err.Error(), task)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if isAutoConfirmPrepare(task.main) {
|
||||
return p.finalizeAutoPrepareSuccess(ctx, task, info, now)
|
||||
}
|
||||
err = p.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.detail == nil {
|
||||
continue
|
||||
}
|
||||
if err := p.repo.Tables().UpdateBatchDetailSnapshot(ctx, tx, task.main.BatchNo, task.main.BatchType, plan.detail.RowNo, consts.BatchDetailStatusSuccess, buildDetailSnapshot(plan, ""), "", task.main.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := p.repo.MarkAllDetailsSuccess(ctx, tx, task.main.BatchNo, task.main.BatchType, task.main.CreatedBy, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.repo.UpdateBatchMainInfo(ctx, tx, task.main.BatchNo, task.main.BatchType, &rc.BatchMainRow{
|
||||
BatchType: task.main.BatchType,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
|
||||
RunResult: consts.BatchRunResultSuccess,
|
||||
TotalCount: task.total,
|
||||
ValidCount: task.valid,
|
||||
SuccessCount: task.valid,
|
||||
FailCount: 0,
|
||||
FailReason: "",
|
||||
BatchInfo: gjson.New(info),
|
||||
EndTime: gtime.NewFromTime(now),
|
||||
UpdatedBy: task.main.CreatedBy,
|
||||
})
|
||||
})
|
||||
return gerror.Wrap(err, "finalize add package prepare success failed")
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) finalizeAutoPrepareSuccess(ctx context.Context, task *batchTask, info map[string]any, now time.Time) error {
|
||||
if task == nil || task.main == nil {
|
||||
return gerror.New("add package auto prepare task is empty")
|
||||
}
|
||||
targetBatchType := p.targetBatchType
|
||||
info["autoConfirmed"] = true
|
||||
info["prepareBatchType"] = consts.BatchTypePrepare
|
||||
info["status"] = "pending_execute"
|
||||
err := p.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.detail == nil {
|
||||
continue
|
||||
}
|
||||
if err := p.repo.Tables().UpdateBatchDetailSnapshot(ctx, tx, task.main.BatchNo, task.main.BatchType, plan.detail.RowNo, consts.BatchDetailStatusSuccess, buildDetailSnapshot(plan, ""), "", task.main.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := convertAddPackagePrepareDetails(ctx, tx, task.main.BatchNo, targetBatchType, consts.BatchDetailStatusPending, "", task.main.CreatedBy, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return convertAddPackagePrepareMain(ctx, tx, task.main.BatchNo, targetBatchType, &rc.BatchMainRow{
|
||||
BatchAsyncStatus: batch.AsyncStatusPending,
|
||||
RunResult: 0,
|
||||
TotalCount: task.total,
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
FailReason: "",
|
||||
BatchInfo: gjson.New(info),
|
||||
UpdatedBy: task.main.CreatedBy,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "auto confirm add package prepare failed")
|
||||
}
|
||||
p.publishExecution(ctx, task.main.BatchNo, targetBatchType)
|
||||
g.Log().Infof(ctx, "add package prepare auto confirmed, batchNo:%s, targetType:%d", task.main.BatchNo, targetBatchType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) finalizeAutoPrepareFailure(ctx context.Context, main *rc.BatchMainRow, reason string, task *batchTask) error {
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
reason = "加油包预处理失败"
|
||||
}
|
||||
now := p.now()
|
||||
targetBatchType := p.targetBatchType
|
||||
info := mergeBatchInfo(main.BatchInfo, map[string]any{
|
||||
"targetBatchType": targetBatchType,
|
||||
"prepareBatchType": consts.BatchTypePrepare,
|
||||
"autoConfirm": true,
|
||||
"autoConfirmed": false,
|
||||
"status": "failed",
|
||||
})
|
||||
err := p.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if task != nil {
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.detail == nil {
|
||||
continue
|
||||
}
|
||||
rowReason := strings.TrimSpace(plan.reason)
|
||||
if rowReason == "" {
|
||||
rowReason = reason
|
||||
}
|
||||
if err := p.repo.Tables().UpdateBatchDetailSnapshot(ctx, tx, main.BatchNo, main.BatchType, plan.detail.RowNo, consts.BatchDetailStatusFailed, buildDetailSnapshot(plan, ""), rowReason, main.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := p.repo.MarkAllDetailsFailed(ctx, tx, main.BatchNo, main.BatchType, reason, main.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
detailReason := reason
|
||||
if task != nil && len(task.plans) > 0 {
|
||||
detailReason = ""
|
||||
}
|
||||
if err := convertAddPackagePrepareDetails(ctx, tx, main.BatchNo, targetBatchType, consts.BatchDetailStatusFailed, detailReason, main.CreatedBy, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return convertAddPackagePrepareMain(ctx, tx, main.BatchNo, targetBatchType, &rc.BatchMainRow{
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
|
||||
RunResult: consts.BatchRunResultFailed,
|
||||
FailReason: reason,
|
||||
TotalCount: main.TotalCount,
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: main.TotalCount,
|
||||
BatchInfo: gjson.New(info),
|
||||
EndTime: gtime.NewFromTime(now),
|
||||
UpdatedBy: main.CreatedBy,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "finalize auto add package prepare failure failed")
|
||||
}
|
||||
rows, _ := p.repo.ResolveResultSnapshot(ctx, main.BatchNo, targetBatchType)
|
||||
_ = p.repo.WriteResultFileSafe(ctx, main.BatchNo, targetBatchType, main.TenantId, main.CreatedDept, main.CreatedBy, rows)
|
||||
g.Log().Warningf(ctx, "add package prepare auto failed, batchNo:%s, targetType:%d, reason:%s", main.BatchNo, targetBatchType, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) publishExecution(ctx context.Context, batchNo string, batchType int) {
|
||||
if err := rabbitmq.Publish(ctx, consts.QueueBatchExecute, batch.Message{BatchNo: batchNo, BatchType: batchType}); err != nil {
|
||||
g.Log().Errorf(ctx, "publish add package execution after prepare failed, batchNo:%s, batchType:%d, err:%+v", batchNo, batchType, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) buildPrepareInfo(ctx context.Context, task *batchTask) (map[string]any, error) {
|
||||
info := mergeBatchInfo(task.main.BatchInfo, map[string]any{
|
||||
"targetBatchType": p.targetBatchType,
|
||||
"batchMode": map[bool]string{true: "pool", false: "card"}[p.inner.poolMode],
|
||||
})
|
||||
quantity := 0
|
||||
totalAmount := 0.0
|
||||
var sample *detailPlan
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.reason != "" {
|
||||
continue
|
||||
}
|
||||
if sample == nil {
|
||||
sample = plan
|
||||
}
|
||||
quantity++
|
||||
if plan.price == nil {
|
||||
continue
|
||||
}
|
||||
totalAmount = roundAmount(totalAmount + plan.amount)
|
||||
}
|
||||
if task.prepareOnly && sample != nil && strings.TrimSpace(sample.packageCode) == "" {
|
||||
options, err := p.prepareAvailablePackages(ctx, task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(options) == 0 {
|
||||
return nil, gerror.New("当前没有共同可购的加油包套餐")
|
||||
}
|
||||
info["quantity"] = quantity
|
||||
info["basePackageKey"] = sample.basePackageKey
|
||||
info["availablePackages"] = options
|
||||
info["packageOptions"] = options
|
||||
if _, err := p.fillPreparePaymentView(ctx, task, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info["prepareTime"] = p.now().Format("2006-01-02 15:04:05")
|
||||
info["status"] = "success"
|
||||
return info, nil
|
||||
}
|
||||
if sample == nil || sample.price == nil {
|
||||
return nil, gerror.New("加油包预处理没有可确认的明细")
|
||||
}
|
||||
info["quantity"] = quantity
|
||||
info["basePackageKey"] = sample.basePackageKey
|
||||
info["packageCode"] = sample.packageCode
|
||||
info["packageName"] = sample.price.PackageName
|
||||
info["flowMb"] = sample.price.FlowMb
|
||||
info["unitPrice"] = sample.price.UnitPrice
|
||||
info["totalAmount"] = totalAmount
|
||||
info["effectiveMode"] = sample.effectiveMode
|
||||
info["effectiveModeText"] = effectiveModeText(sample.effectiveMode)
|
||||
info["effectiveTime"] = gtime.NewFromTime(sample.effectiveTime).Format("Y-m-d H:i:s")
|
||||
info["expectedExpireTime"] = gtime.NewFromTime(sample.expireTime).Format("Y-m-d H:i:s")
|
||||
info["priceSnapshot"] = sample.price
|
||||
accounts, err := p.preparePaymentAccounts(ctx, task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info["paymentAccounts"] = accounts
|
||||
isTop, err := p.fillPreparePaymentView(ctx, task, info)
|
||||
if isAutoConfirmPrepare(task.main) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
intent, err := normalizePaymentIntent(task.paymentIntent, isTop)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task.paymentIntent = intent
|
||||
info["paymentMode"] = intent.PaymentMode
|
||||
info["offlineContractCompleted"] = intent.OfflineContractCompleted
|
||||
if intent.PaymentMode == "balance" {
|
||||
if err := validatePrepareBalanceEnough(accounts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
info["prepareTime"] = p.now().Format("2006-01-02 15:04:05")
|
||||
info["status"] = "success"
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) fillPreparePaymentView(ctx context.Context, task *batchTask, info map[string]any) (bool, error) {
|
||||
isTop, err := p.inner.isTopDept(ctx, task.main.TenantId, task.main.CreatedDept)
|
||||
if err == nil {
|
||||
info["isTopDept"] = isTop
|
||||
info["defaultPaymentMode"] = "balance"
|
||||
}
|
||||
balance, balanceErr := p.availableBalance(ctx, task.main.TenantId, task.main.CreatedDept)
|
||||
if balanceErr != nil {
|
||||
return isTop, balanceErr
|
||||
}
|
||||
info["availableBalance"] = balance
|
||||
return isTop, err
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) prepareAvailablePackages(ctx context.Context, task *batchTask) ([]map[string]any, error) {
|
||||
sample := firstValidAddPackagePlan(task)
|
||||
if sample == nil {
|
||||
return nil, gerror.New("加油包预处理没有可确认的明细")
|
||||
}
|
||||
codes, err := p.availablePackageCodes(ctx, task.main.TenantId, sample)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options := make([]map[string]any, 0, len(codes))
|
||||
for _, code := range codes {
|
||||
option, err := p.buildAvailablePackageOption(ctx, task, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if option != nil {
|
||||
options = append(options, option)
|
||||
}
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func firstValidAddPackagePlan(task *batchTask) *detailPlan {
|
||||
if task == nil {
|
||||
return nil
|
||||
}
|
||||
for _, plan := range task.plans {
|
||||
if plan != nil && plan.reason == "" {
|
||||
return plan
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) availablePackageCodes(ctx context.Context, tenantID string, sample *detailPlan) ([]string, error) {
|
||||
if p.inner.poolMode {
|
||||
return p.availablePoolPackageCodes(ctx, tenantID, sample)
|
||||
}
|
||||
basePkg, err := p.repo.Tables().LoadPackageByCode(ctx, tenantID, sample.basePackageKey)
|
||||
if err != nil || basePkg == nil {
|
||||
return nil, gerror.New("当前卡基础套餐不存在")
|
||||
}
|
||||
codes := packageCodesFromJSON(basePkg.TopupPackageCodes)
|
||||
if len(codes) == 0 {
|
||||
return nil, gerror.New("当前卡基础套餐未配置可购加油包")
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) availablePoolPackageCodes(ctx context.Context, tenantID string, sample *detailPlan) ([]string, error) {
|
||||
if sample.pool == nil || sample.pool.PoolPackageCode <= 0 {
|
||||
return nil, gerror.New("流量池未配置基础套餐")
|
||||
}
|
||||
var packages []*rc.FlowPackageInfo
|
||||
if err := p.repo.DB().Model(rc.TableSimFlowPackageInfo).Safe().Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Where("pool_package_code", sample.pool.PoolPackageCode).
|
||||
Where("package_type", consts.PackageTypePoolAddPackage).
|
||||
Where("enabled", 1).
|
||||
OrderAsc("sort_no").
|
||||
Scan(&packages); err != nil {
|
||||
return nil, gerror.Wrap(err, "查询流量池加油包套餐失败")
|
||||
}
|
||||
codes := make([]string, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
if code := strings.TrimSpace(pkg.PackageCode); code != "" {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
}
|
||||
if len(codes) == 0 {
|
||||
return nil, gerror.New("所选流量池没有共同可购的加油包套餐")
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) buildAvailablePackageOption(ctx context.Context, task *batchTask, packageCode string) (map[string]any, error) {
|
||||
packageCode = strings.TrimSpace(packageCode)
|
||||
if packageCode == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var (
|
||||
samplePrice *PriceSnapshot
|
||||
totalAmount float64
|
||||
unitPrice float64
|
||||
priceAvailable = true
|
||||
priceReason string
|
||||
accounts = make(map[int64]*prepareAccount)
|
||||
deptIDs = make([]int64, 0)
|
||||
)
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.reason != "" {
|
||||
continue
|
||||
}
|
||||
pkg, price, err := p.inner.resolvePrice(ctx, task.main.TenantId, plan.customerDeptID, packageCode)
|
||||
if err != nil {
|
||||
priceAvailable = false
|
||||
if priceReason == "" {
|
||||
priceReason = err.Error()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if p.inner.poolMode {
|
||||
if pkg.PackageType != consts.PackageTypePoolAddPackage || plan.pool == nil || pkg.PoolPackageCode != plan.pool.PoolPackageCode {
|
||||
priceAvailable = false
|
||||
priceReason = "加油包不属于当前流量池套餐"
|
||||
continue
|
||||
}
|
||||
} else if pkg.PackageType != consts.PackageTypeCardAddPackage {
|
||||
priceAvailable = false
|
||||
priceReason = "目标套餐类型不是单卡加油包"
|
||||
continue
|
||||
}
|
||||
if samplePrice == nil {
|
||||
samplePrice = price
|
||||
unitPrice = price.UnitPrice
|
||||
} else if !sameAddPackageAmount(unitPrice, price.UnitPrice) {
|
||||
priceAvailable = false
|
||||
priceReason = addPackageSamePriceFailureReason
|
||||
}
|
||||
totalAmount = roundAmount(totalAmount + price.UnitPrice)
|
||||
account := accounts[plan.customerDeptID]
|
||||
if account == nil {
|
||||
account = &prepareAccount{payerDeptID: plan.customerDeptID}
|
||||
accounts[plan.customerDeptID] = account
|
||||
deptIDs = append(deptIDs, plan.customerDeptID)
|
||||
}
|
||||
account.count++
|
||||
account.required = roundAmount(account.required + price.UnitPrice)
|
||||
}
|
||||
if samplePrice == nil {
|
||||
return nil, nil
|
||||
}
|
||||
paymentAccounts, err := p.prepareAccountItems(ctx, task.main.TenantId, accounts, deptIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
option := map[string]any{
|
||||
"packageCode": samplePrice.PackageCode,
|
||||
"packageName": samplePrice.PackageName,
|
||||
"flowMb": samplePrice.FlowMb,
|
||||
"unitPrice": unitPrice,
|
||||
"totalAmount": totalAmount,
|
||||
"priceAvailable": priceAvailable,
|
||||
"paymentAccounts": paymentAccounts,
|
||||
}
|
||||
if priceReason != "" {
|
||||
option["priceReason"] = priceReason
|
||||
}
|
||||
return option, nil
|
||||
}
|
||||
|
||||
type prepareAccount struct {
|
||||
payerDeptID int64
|
||||
count int
|
||||
required float64
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) prepareAccountItems(ctx context.Context, tenantID string, accounts map[int64]*prepareAccount, deptIDs []int64) ([]map[string]any, error) {
|
||||
sort.Slice(deptIDs, func(i, j int) bool { return deptIDs[i] < deptIDs[j] })
|
||||
out := make([]map[string]any, 0, len(deptIDs))
|
||||
for _, deptID := range deptIDs {
|
||||
item := accounts[deptID]
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
balance, err := p.availableBalance(ctx, tenantID, deptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"payerDeptId": item.payerDeptID,
|
||||
"targetCount": item.count,
|
||||
"requiredAmount": item.required,
|
||||
"availableBalance": balance,
|
||||
"balanceEnough": balance+0.0001 >= item.required,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func validatePrepareBalanceEnough(accounts []map[string]any) error {
|
||||
for _, account := range accounts {
|
||||
if account == nil {
|
||||
continue
|
||||
}
|
||||
if enough, _ := account["balanceEnough"].(bool); enough {
|
||||
continue
|
||||
}
|
||||
return gerror.Newf(
|
||||
"账户余额不足,付款部门%v需支付%.2f元,当前余额%.2f元",
|
||||
account["payerDeptId"],
|
||||
floatFromAny(account["requiredAmount"]),
|
||||
floatFromAny(account["availableBalance"]),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func floatFromAny(value any) float64 {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return v
|
||||
case float32:
|
||||
return float64(v)
|
||||
case int:
|
||||
return float64(v)
|
||||
case int64:
|
||||
return float64(v)
|
||||
case int32:
|
||||
return float64(v)
|
||||
case uint:
|
||||
return float64(v)
|
||||
case uint64:
|
||||
return float64(v)
|
||||
case uint32:
|
||||
return float64(v)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) preparePaymentAccounts(ctx context.Context, task *batchTask) ([]map[string]any, error) {
|
||||
type account struct {
|
||||
payerDeptID int64
|
||||
count int
|
||||
required float64
|
||||
}
|
||||
byDept := make(map[int64]*account)
|
||||
deptIDs := make([]int64, 0)
|
||||
for _, plan := range task.plans {
|
||||
if plan == nil || plan.reason != "" {
|
||||
continue
|
||||
}
|
||||
payerDeptID := plan.customerDeptID
|
||||
if payerDeptID <= 0 {
|
||||
return nil, gerror.New("加油包付款部门不能为空")
|
||||
}
|
||||
item := byDept[payerDeptID]
|
||||
if item == nil {
|
||||
item = &account{payerDeptID: payerDeptID}
|
||||
byDept[payerDeptID] = item
|
||||
deptIDs = append(deptIDs, payerDeptID)
|
||||
}
|
||||
item.count++
|
||||
item.required = roundAmount(item.required + plan.amount)
|
||||
}
|
||||
sort.Slice(deptIDs, func(i, j int) bool { return deptIDs[i] < deptIDs[j] })
|
||||
out := make([]map[string]any, 0, len(deptIDs))
|
||||
for _, deptID := range deptIDs {
|
||||
item := byDept[deptID]
|
||||
balance, err := p.availableBalance(ctx, task.main.TenantId, deptID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"payerDeptId": item.payerDeptID,
|
||||
"targetCount": item.count,
|
||||
"requiredAmount": item.required,
|
||||
"availableBalance": balance,
|
||||
"balanceEnough": balance+0.0001 >= item.required,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) availableBalance(ctx context.Context, tenantID string, deptID int64) (float64, error) {
|
||||
if deptID <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var row agentBalanceRow
|
||||
if err := p.repo.DB().Model("t_agent_balance").Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Where("dept_id", deptID).
|
||||
Scan(&row); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, gerror.Wrap(err, "query add package balance failed")
|
||||
}
|
||||
if row.Id == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return roundAmount(row.Balance), nil
|
||||
}
|
||||
|
||||
func (p *PrepareExecutor) now() time.Time {
|
||||
if p == nil || p.nowFunc == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return p.nowFunc()
|
||||
}
|
||||
|
||||
type PrepareMux struct {
|
||||
repo *renewchangeservice.Repository
|
||||
renew batch.Executor
|
||||
card *PrepareExecutor
|
||||
pool *PrepareExecutor
|
||||
}
|
||||
|
||||
func NewPrepareMux(repo *renewchangeservice.Repository, renew batch.Executor) *PrepareMux {
|
||||
if repo == nil {
|
||||
repo = renewchangeservice.NewRepository()
|
||||
}
|
||||
return &PrepareMux{
|
||||
repo: repo,
|
||||
renew: renew,
|
||||
card: NewCardPrepareExecutor(repo),
|
||||
pool: NewPoolPrepareExecutor(repo),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PrepareMux) Execute(ctx context.Context, batchNo string) error {
|
||||
main, err := m.repo.LoadProcessingBatch(ctx, strings.TrimSpace(batchNo), consts.BatchTypePrepare)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch targetBatchTypeFromInfo(main.BatchInfo) {
|
||||
case consts.BatchTypeCardAddPackage:
|
||||
return m.card.Execute(ctx, batchNo)
|
||||
case consts.BatchTypePoolAddPackage:
|
||||
return m.pool.Execute(ctx, batchNo)
|
||||
default:
|
||||
if m.renew != nil {
|
||||
return m.renew.Execute(ctx, batchNo)
|
||||
}
|
||||
return gerror.Newf("unsupported prepare target batch type: %d", targetBatchTypeFromInfo(main.BatchInfo))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PrepareMux) CanExecuteProcessing(ctx context.Context, main *entity.BatchMain) (bool, error) {
|
||||
if main == nil || main.BatchType != consts.BatchTypePrepare || main.BatchAsyncStatus != consts.BatchAsyncStatusProcessing {
|
||||
return false, nil
|
||||
}
|
||||
switch targetBatchTypeFromInfo(main.BatchInfo) {
|
||||
case consts.BatchTypeCardAddPackage:
|
||||
return m.card.CanExecuteProcessing(ctx, main)
|
||||
case consts.BatchTypePoolAddPackage:
|
||||
return m.pool.CanExecuteProcessing(ctx, main)
|
||||
default:
|
||||
if processing, ok := m.renew.(batch.ProcessingExecutor); ok {
|
||||
return processing.CanExecuteProcessing(ctx, main)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func targetBatchTypeFromInfo(info *gjson.Json) int {
|
||||
if info == nil || info.IsNil() {
|
||||
return 0
|
||||
}
|
||||
if v := info.Get("targetBatchType"); v != nil && !v.IsNil() {
|
||||
return v.Int()
|
||||
}
|
||||
if v := info.Get("target_batch_type"); v != nil && !v.IsNil() {
|
||||
return v.Int()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func isAutoConfirmPrepare(main *rc.BatchMainRow) bool {
|
||||
if main == nil || main.SourceType != consts.BatchSourceTypeFile || main.BatchInfo == nil || main.BatchInfo.IsNil() {
|
||||
return false
|
||||
}
|
||||
return main.BatchInfo.Get("autoConfirm").Bool()
|
||||
}
|
||||
|
||||
func convertAddPackagePrepareDetails(ctx context.Context, tx gdb.TX, batchNo string, targetBatchType int, detailStatus int, reason string, userID int64, now time.Time) error {
|
||||
data := map[string]any{
|
||||
"batch_type": targetBatchType,
|
||||
"detail_status": detailStatus,
|
||||
"updated_by": userID,
|
||||
"updated_at": gtime.NewFromTime(now),
|
||||
}
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
data["error_reason"] = reason
|
||||
}
|
||||
_, err := tx.Ctx(ctx).Model(rc.TableBatchDetail).
|
||||
Where("batch_no", batchNo).
|
||||
Where("batch_type", consts.BatchTypePrepare).
|
||||
Data(data).
|
||||
Update()
|
||||
return gerror.Wrap(err, "convert add package prepare details failed")
|
||||
}
|
||||
|
||||
func convertAddPackagePrepareMain(ctx context.Context, tx gdb.TX, batchNo string, targetBatchType int, data *rc.BatchMainRow) error {
|
||||
if data == nil {
|
||||
return gerror.New("add package batch conversion data is empty")
|
||||
}
|
||||
update := map[string]any{
|
||||
"batch_type": targetBatchType,
|
||||
"batch_async_status": data.BatchAsyncStatus,
|
||||
"run_result": data.RunResult,
|
||||
"total_count": data.TotalCount,
|
||||
"valid_count": data.ValidCount,
|
||||
"success_count": data.SuccessCount,
|
||||
"fail_count": data.FailCount,
|
||||
"fail_reason": data.FailReason,
|
||||
"batch_info": data.BatchInfo,
|
||||
"start_time": nil,
|
||||
"end_time": data.EndTime,
|
||||
"updated_by": data.UpdatedBy,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
if data.EndTime == nil {
|
||||
update["end_time"] = nil
|
||||
}
|
||||
result, err := tx.Ctx(ctx).Model(rc.TableBatchMain).
|
||||
Where("batch_no", batchNo).
|
||||
Where("batch_type", consts.BatchTypePrepare).
|
||||
Data(update).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "convert add package prepare main failed")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "read add package prepare main conversion result failed")
|
||||
}
|
||||
if affected == 0 {
|
||||
return gerror.New("加油包预处理批次已变化,请重试")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user