Files
iotlcm/internal/service/allocate/executor.go
T
2026-10-29 14:52:43 +08:00

326 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package allocate
import (
"context"
"strings"
"time"
"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"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/packagecustomer"
)
const defaultAllocateProcessingRetryAfter = 30 * time.Minute
const allocateParentJobRole = "allocate_parent"
type executor struct {
repository *Repository
}
func NewExecutor() *executor {
return &executor{repository: NewRepository()}
}
func (e *executor) CanExecuteProcessing(ctx context.Context, main *entity.BatchMain) (bool, error) {
if main == nil || main.BatchAsyncStatus != consts.BatchAsyncStatusProcessing {
return false, nil
}
if isAllocateParent(main) {
return true, nil
}
if main.BatchType != consts.BatchTypeAllocate {
return false, nil
}
if main.StartTime == nil || main.StartTime.IsZero() {
return true, nil
}
return gtime.Now().Sub(main.StartTime) >= allocateProcessingRetryAfter(ctx), nil
}
func (e *executor) HandleExecutionFailure(ctx context.Context, batchNo string, batchType int, _ string) error {
if batchType != consts.BatchTypeAllocate {
return nil
}
main, err := e.repository.LoadMain(ctx, batchNo)
if err != nil {
return err
}
if main == nil {
return nil
}
if strings.TrimSpace(main.SourceBatchNo) == "" {
return nil
}
return e.repository.AggregateParent(ctx, main.SourceBatchNo)
}
func allocateProcessingRetryAfter(ctx context.Context) time.Duration {
duration := g.Cfg().MustGet(ctx, "batch.allocate.processingRetryAfter", defaultAllocateProcessingRetryAfter).Duration()
if duration <= 0 {
return defaultAllocateProcessingRetryAfter
}
return duration
}
func (e *executor) Execute(ctx context.Context, batchNo string) error {
main, err := e.repository.LoadMain(ctx, strings.TrimSpace(batchNo))
if err != nil {
return gerror.Wrap(err, "load allocate batch main failed")
}
if isAllocateParent(main) {
return e.repository.AggregateParent(ctx, main.BatchNo)
}
task, err := e.repository.Load(ctx, strings.TrimSpace(batchNo))
if err != nil {
return gerror.Wrap(err, "load allocate batch failed")
}
validateTask(task)
if err = e.repository.Commit(ctx, task); err != nil {
return gerror.Wrap(err, "commit allocate batch failed")
}
if strings.TrimSpace(task.Batch.ParentBatchNo) == "" {
publishPackageCustomerSync(ctx, task)
}
return nil
}
func isAllocateParent(main *entity.BatchMain) bool {
if main == nil || main.BatchType != consts.BatchTypeAllocate || strings.TrimSpace(main.SourceBatchNo) != "" {
return false
}
return main.BatchInfo != nil && main.BatchInfo.Get("jobRole").String() == allocateParentJobRole
}
func validateTask(task *Task) {
if task == nil || len(task.Details) == 0 {
if task != nil {
task.FailureReason = "划拨批次没有可处理明细"
}
return
}
iccidCounts := make(map[string]int, len(task.Details))
for _, detail := range task.Details {
if detail == nil {
continue
}
detail.ICCID = strings.TrimSpace(detail.ICCID)
detail.MSISDN = strings.TrimSpace(detail.MSISDN)
detail.IMSI = strings.TrimSpace(detail.IMSI)
detail.TargetDeptKey = strings.TrimSpace(detail.TargetDeptKey)
detail.TargetDeptName = strings.TrimSpace(detail.TargetDeptName)
if detail.ICCID != "" {
iccidCounts[detail.ICCID]++
}
}
for _, detail := range task.Details {
if detail == nil {
continue
}
if detail.Reason == "" && iccidCounts[detail.ICCID] > 1 {
detail.Reason = "ICCID在批次内重复"
}
validateDetail(task, detail)
}
_, failCount := countResult(task)
if failCount > 0 {
markAtomicFailure(task, "划拨批次存在失败明细,整批未执行")
}
}
func publishPackageCustomerSync(ctx context.Context, task *Task) {
if task == nil {
return
}
deptIds := []int64{task.Batch.CreatedDept}
hasSuccess := false
for _, detail := range task.Details {
if detail == nil || !detail.Success() {
continue
}
hasSuccess = true
if detail.TargetDeptID > 0 {
deptIds = append(deptIds, detail.TargetDeptID)
}
}
if !hasSuccess {
return
}
packagecustomer.PublishSyncForDeptsSafe(ctx, task.Batch.TenantID, task.Batch.CreatedBy, deptIds, "allocate", task.Batch.BatchNo)
}
func markAtomicFailure(task *Task, reason string) {
if task == nil {
return
}
reason = strings.TrimSpace(reason)
if reason == "" {
reason = "本批次存在失败明细,整批未执行"
}
task.FailureReason = reason
for _, detail := range task.Details {
if detail != nil && strings.TrimSpace(detail.Reason) == "" {
detail.Reason = atomicNotExecutedReason
}
}
}
const atomicNotExecutedReason = "校验通过,但本批次存在失败明细,整批未执行划拨"
func allocationDetailStatus(detail *Detail) int {
if detail.Reason == atomicNotExecutedReason {
return consts.BatchDetailStatusNotExecuted
}
if detail.Success() {
return consts.BatchDetailStatusSuccess
}
return consts.BatchDetailStatusFailed
}
func countValidationFailures(task *Task) int {
count := 0
for _, detail := range task.Details {
if detail != nil && !detail.Success() && detail.Reason != atomicNotExecutedReason {
count++
}
}
return count
}
func persistedAllocationDetailStatus(task *Task, detail *Detail) int {
status := allocationDetailStatus(detail)
if strings.TrimSpace(task.Batch.ParentBatchNo) != "" && status == consts.BatchDetailStatusSuccess {
return consts.BatchDetailStatusRunning
}
return status
}
func validateDetail(task *Task, detail *Detail) {
if task == nil || detail == nil {
return
}
var reasons []string
if reason := strings.TrimSpace(detail.Reason); reason != "" {
reasons = append(reasons, reason)
}
defer func() { detail.Reason = strings.Join(reasons, ";") }()
if detail.ICCID == "" {
reasons = append(reasons, "ICCID不能为空")
return
}
detail.Reason = ""
target := resolveTarget(task, detail)
if target == nil {
reasons = append(reasons, detail.Reason)
} else {
detail.Target = target
detail.TargetDeptID = target.DeptID
if detail.TargetDeptKey == "" {
detail.TargetDeptKey = strings.TrimSpace(target.DeptKey)
}
if detail.TargetDeptName == "" {
detail.TargetDeptName = strings.TrimSpace(target.DeptName)
}
}
if detail.Card == nil {
reasons = append(reasons, "ICCID不存在或不属于当前租户")
return
}
if strings.TrimSpace(detail.ICCID) != strings.TrimSpace(detail.Card.ICCID) {
reasons = append(reasons, "ICCID与卡档案不一致")
return
}
detail.MSISDN = coalesce(detail.MSISDN, detail.Card.MSISDN)
detail.IMSI = coalesce(detail.IMSI, detail.Card.IMSI)
if detail.Card.DeptID != task.Batch.CreatedDept {
reasons = append(reasons, "ICCID不在当前操作员授权范围内")
}
if strings.TrimSpace(detail.MSISDN) != "" && strings.TrimSpace(detail.MSISDN) != detail.Card.MSISDN {
reasons = append(reasons, "MSISDN与卡档案不一致")
}
if strings.TrimSpace(detail.IMSI) != "" && strings.TrimSpace(detail.IMSI) != detail.Card.IMSI {
reasons = append(reasons, "IMSI与卡档案不一致")
}
if detail.Card.BossCardStatus == consts.SimBossCardStatusStopped {
reasons = append(reasons, "卡已停机,不能划拨")
}
if detail.Card.BossCardStatus == consts.SimBossCardStatusCanceled {
reasons = append(reasons, "卡已销户,不能划拨")
}
if detail.Card.PackageStatus == consts.PackageStatusExpired ||
(detail.Card.FlowPkgEndAt != nil && !detail.Card.FlowPkgEndAt.IsZero() && detail.Card.FlowPkgEndAt.Before(gtime.Now())) {
reasons = append(reasons, "套餐已过期,不能划拨")
}
if target != nil && detail.Card.DeptID == target.DeptID {
reasons = append(reasons, "卡已属于目标客户,无需重复划拨")
}
}
func resolveTarget(task *Task, detail *Detail) *Target {
if task == nil || detail == nil {
return nil
}
targetDeptID := detail.TargetDeptID
if targetDeptID <= 0 {
targetDeptID = task.Batch.TargetDeptID
}
if targetDeptID > 0 {
target := task.TargetsByID[targetDeptID]
if target == nil {
detail.Reason = "划拨目标客户不存在或不在当前部门的下级范围内"
return nil
}
return target
}
if detail.TargetDeptKey != "" {
target := task.TargetsByKey[detail.TargetDeptKey]
if target == nil {
detail.Reason = "划拨目标客户编码不存在或不在当前部门的下级范围内"
return nil
}
return target
}
if detail.TargetDeptName != "" {
targets := task.TargetsByName[detail.TargetDeptName]
if len(targets) == 0 {
detail.Reason = "划拨目标客户名称不存在或不在当前部门的下级范围内"
return nil
}
if len(targets) > 1 {
detail.Reason = "划拨目标客户名称不唯一,请使用目标客户编码"
return nil
}
return targets[0]
}
detail.Reason = "目标客户不能为空"
return nil
}
func countResult(task *Task) (successCount, failCount int) {
if task == nil {
return 0, 0
}
for _, detail := range task.Details {
if detail != nil && detail.Success() {
successCount++
} else {
failCount++
}
}
return successCount, failCount
}
func coalesce(value, fallback string) string {
value = strings.TrimSpace(value)
if value != "" {
return value
}
return strings.TrimSpace(fallback)
}