chore: 并入 iotlcm-worker-svr 历史到 worker-svr/(原提交 96f0f4e)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:03 +08:00
422 changed files with 80993 additions and 0 deletions
@@ -0,0 +1,214 @@
package warningconfig
import (
"context"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"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"
)
const warningConfigProcessingRetryAfter = 2 * time.Minute
type executor struct {
repository *Repository
}
func NewExecutor() *executor {
return &executor{repository: NewRepository()}
}
func (e *executor) CanExecuteProcessing(_ context.Context, main *entity.BatchMain) (bool, error) {
if main == nil || main.BatchType != consts.BatchTypeSimWarningConfig || main.BatchAsyncStatus != consts.BatchAsyncStatusProcessing {
return false, nil
}
if main.StartTime == nil || main.StartTime.IsZero() {
return true, nil
}
return gtime.Now().Sub(main.StartTime) >= warningConfigProcessingRetryAfter, nil
}
func (e *executor) Execute(ctx context.Context, batchNo string) error {
task, err := e.repository.Load(ctx, strings.TrimSpace(batchNo))
if err != nil {
return gerror.Wrap(err, "load warning config batch failed")
}
validateTask(task)
return gerror.Wrap(e.repository.Commit(ctx, task), "commit warning config batch failed")
}
func validateTask(task *Task) {
if task == nil || len(task.Details) == 0 {
if task != nil {
task.FailureReason = "预警配置批次没有可处理明细"
}
return
}
for _, detail := range task.Details {
validateDetail(task, detail)
}
_, failCount := countResult(task)
if failCount > 0 {
markAtomicFailure(task, "预警配置批次存在失败明细,整批未执行")
}
}
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 = "本批次存在失败明细,整批未执行"
}
}
}
func validateDetail(task *Task, detail *Detail) {
if detail == nil || detail.Reason != "" {
return
}
config := detail.Config
if config.ItemType == warnItemTypeFlowPool {
validateFlowPoolDetail(config, detail)
return
}
validateCardDetail(config, detail)
}
func validateCardDetail(config Config, detail *Detail) {
detail.ICCID = strings.TrimSpace(detail.ICCID)
detail.MSISDN = strings.TrimSpace(detail.MSISDN)
detail.IMSI = strings.TrimSpace(detail.IMSI)
if detail.ICCID == "" {
detail.Reason = "ICCID不能为空"
return
}
if !detail.Authorized {
detail.Reason = "ICCID不在当前操作员授权范围内"
return
}
if detail.Card == nil {
detail.Reason = "ICCID不存在或不属于当前租户"
return
}
if detail.MSISDN != "" && detail.MSISDN != detail.Card.MSISDN {
detail.Reason = "MSISDN与卡档案不一致"
return
}
if detail.IMSI != "" && detail.IMSI != detail.Card.IMSI {
detail.Reason = "IMSI与卡档案不一致"
return
}
if detail.Card.BossCardStatus == consts.SimBossCardStatusStopped {
detail.Reason = "卡已停机,不能配置预警"
return
}
if detail.Card.BossCardStatus != consts.SimBossCardStatusActivated {
detail.Reason = "当前卡状态为" + bossCardStatusText(detail.Card.BossCardStatus) + ",只有已激活卡可以配置预警"
return
}
if detail.Card.PackageStatus != packageStatusEffective {
detail.Reason = "套餐未生效,不能配置预警"
return
}
if detail.Card.IsFlowPool != 0 {
detail.Reason = "流量池卡暂不支持单卡预警配置"
return
}
if strings.TrimSpace(detail.Card.CurrentPackageCode) == "" {
detail.Reason = "当前套餐编号为空,不能配置预警"
return
}
switch config.WarnType {
case warnTypeCardTraffic:
validateTrafficDetail(config, detail)
case warnTypeExpireDay, warnTypeExpireMonth:
if detail.Card.FlowPkgEndAt == nil || detail.Card.FlowPkgEndAt.IsZero() {
detail.Reason = "套餐到期时间为空,不能配置到期预警"
}
default:
detail.Reason = "预警类型无效"
}
}
func validateFlowPoolDetail(config Config, detail *Detail) {
detail.FlowPoolCode = strings.TrimSpace(detail.FlowPoolCode)
if detail.FlowPoolCode == "" {
detail.Reason = "流量池编号不能为空"
return
}
if !detail.Authorized {
detail.Reason = "流量池不在当前操作员授权范围内"
return
}
if detail.Pool == nil {
detail.Reason = "流量池不存在或不属于当前租户"
return
}
if detail.Pool.PoolStatus != flowPoolStatusNormal {
detail.Reason = "流量池状态不是正常,不能配置预警"
return
}
if detail.Pool.TotalFlowMb <= 0 {
detail.Reason = "流量池总流量为空,不能配置预警"
return
}
if config.WarnType != warnTypeFlowPoolPercent || config.WarnUnit != warnUnitPercent {
detail.Reason = "流量池预警仅支持百分比配置"
return
}
if config.AutoAction != 0 {
detail.Reason = "流量池预警暂不支持第三次关停"
}
}
func validateTrafficDetail(config Config, detail *Detail) {
if detail.Card.PackageTotalFlowMb <= 0 {
detail.Reason = "套餐总流量为空,不能配置流量预警"
return
}
if config.WarnUnit != warnUnitMb {
return
}
total := detail.Card.PackageTotalFlowMb
if config.FirstWarn > total || config.SecondWarn > total || config.ThirdWarn > total {
detail.Reason = "流量阈值不能大于套餐总流量"
}
}
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 bossCardStatusText(status int) string {
switch status {
case consts.SimBossCardStatusActivated:
return "已激活"
case consts.SimBossCardStatusStopped:
return "已停机"
case 1:
return "待激活"
default:
return "未知状态"
}
}
@@ -0,0 +1,124 @@
package warningconfig
import (
"context"
"testing"
"time"
"github.com/gogf/gf/v2/encoding/gjson"
"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"
)
func TestCanExecuteProcessingAllowsStaleWarningConfigBatch(t *testing.T) {
executor := &executor{}
main := &entity.BatchMain{
BatchType: consts.BatchTypeSimWarningConfig,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
StartTime: gtime.Now().Add(-warningConfigProcessingRetryAfter - time.Second),
}
canExecute, err := executor.CanExecuteProcessing(context.Background(), main)
if err != nil || !canExecute {
t.Fatalf("CanExecuteProcessing() = %t, %v; want true, nil", canExecute, err)
}
}
func TestConfigFromDetailSnapshotUsesRowValues(t *testing.T) {
fallback := Config{
ItemType: warnItemTypeCard,
WarnUnit: warnUnitMb,
WarnMethods: warnDefaultMethod,
IsEnabled: 1,
}
snapshot := gjson.New(map[string]any{
"configSet": true,
"configItemType": warnItemTypeCard,
"warnUnit": warnUnitMb,
"firstWarn": 100,
"secondWarn": 500,
"thirdWarn": 900,
"warnMethods": "3,1,0",
"warnPhones": "13800008000",
"warnEmails": "xxx@example.com",
"autoAction": warnAutoActionSuspend,
"isEnabled": 1,
"remark": "row config",
})
got := configFromDetailSnapshot(snapshot, fallback)
if got.FirstWarn != 100 || got.SecondWarn != 500 || got.ThirdWarn != 900 {
t.Fatalf("unexpected thresholds: %#v", got)
}
if got.WarnMethods != "3,1,0" || got.AutoAction != warnAutoActionSuspend {
t.Fatalf("unexpected row notification config: %#v", got)
}
}
func TestCanExecuteProcessingSkipsFreshOrDifferentBatch(t *testing.T) {
executor := &executor{}
tests := []struct {
name string
main *entity.BatchMain
}{
{
name: "fresh processing batch",
main: &entity.BatchMain{
BatchType: consts.BatchTypeSimWarningConfig,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
StartTime: gtime.Now(),
},
},
{
name: "different batch type",
main: &entity.BatchMain{
BatchType: consts.BatchTypeSimCardStart,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
StartTime: gtime.Now().Add(-warningConfigProcessingRetryAfter - time.Second),
},
},
{
name: "pending warning config batch",
main: &entity.BatchMain{
BatchType: consts.BatchTypeSimWarningConfig,
BatchAsyncStatus: 1,
StartTime: gtime.Now().Add(-warningConfigProcessingRetryAfter - time.Second),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
canExecute, err := executor.CanExecuteProcessing(context.Background(), tt.main)
if err != nil || canExecute {
t.Fatalf("CanExecuteProcessing() = %t, %v; want false, nil", canExecute, err)
}
})
}
}
func TestValidateTaskMarksWholeBatchFailed(t *testing.T) {
task := &Task{
Details: []*Detail{
{ICCID: "8986000000000000001"},
{ICCID: "8986000000000000002", Reason: "卡不存在"},
},
}
validateTask(task)
for _, detail := range task.Details {
if detail.Success() {
t.Fatalf("detail %s remained successful after atomic failure", detail.ICCID)
}
}
if task.FailureReason == "" {
t.Fatal("expected batch failure reason")
}
status, result := batchResult(countResult(task))
if status != consts.BatchAsyncStatusFailed || result != consts.BatchRunResultFailed {
t.Fatalf("batchResult() = %d, %d; want failed, failed", status, result)
}
}
@@ -0,0 +1,37 @@
package warningconfig
import (
"github.com/gogf/gf/v2/encoding/gjson"
"testing"
)
func TestNotificationFieldPresenceSurvivesSnapshot(t *testing.T) {
for _, supplied := range []bool{false, true} {
data := map[string]any{"itemType": 1, "warnUnit": 1, "firstWarn": 100, "isEnabled": 1}
if supplied {
data["appNotifyEnabled"] = 0
data["wxTemplateId"] = ""
}
config, err := configFromBatchInfo(gjson.New(data))
if err != nil {
t.Fatal(err)
}
if config.AppNotifyProvided != supplied || config.WxTemplateProvided != supplied {
t.Fatalf("presence lost: %+v", config)
}
detail := &Detail{Config: config}
snapshot := detail.Snapshot(config)
restored := configFromDetailSnapshot(snapshot, Config{})
if restored.AppNotifyProvided != supplied || restored.WxTemplateProvided != supplied {
t.Fatalf("snapshot presence lost: %+v", restored)
}
}
}
func TestNotificationSnapshotPreservesFallbackWhenAbsent(t *testing.T) {
fallback := Config{AppNotifyEnabled: 1, AppNotifyProvided: true, WxTemplateID: "existing", WxTemplateProvided: true}
restored := configFromDetailSnapshot(gjson.New(map[string]any{"configSet": true, "warnUnit": 1}), fallback)
if restored.AppNotifyEnabled != 1 || restored.WxTemplateID != "existing" {
t.Fatalf("fallback overwritten: %+v", restored)
}
}
@@ -0,0 +1,714 @@
package warningconfig
import (
"context"
"strings"
"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"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
)
const packageStatusEffective = consts.PackageStatusEffective
type Repository struct {
db gdb.DB
results batchresult.Generator
}
func NewRepository() *Repository {
return &Repository{results: batchresult.New()}
}
func (r *Repository) database() gdb.DB {
if r.db != nil {
return r.db
}
return g.DB()
}
func (r *Repository) Load(ctx context.Context, batchNo string) (*Task, error) {
if batchNo == "" {
return nil, gerror.New("预警配置批次号不能为空")
}
var main *entity.BatchMain
if err := r.database().Model(dao.BatchMain.Table()).Safe().Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimWarningConfig).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Scan(&main); err != nil {
return nil, gerror.Wrap(err, "查询预警配置批次主表失败")
}
if main == nil || main.Id == 0 {
return nil, gerror.New("处理中预警配置批次不存在")
}
config, err := configFromBatchInfo(main.BatchInfo)
if err != nil {
return nil, err
}
var batchDetails []*entity.BatchDetail
if err = r.database().Model(dao.BatchDetail.Table()).Safe().Ctx(ctx).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypeSimWarningConfig).
OrderAsc(dao.BatchDetail.Columns().RowNo).
Scan(&batchDetails); err != nil {
return nil, gerror.Wrap(err, "查询预警配置批次明细失败")
}
if len(batchDetails) == 0 {
return nil, gerror.New("预警配置批次没有运行中明细")
}
task := &Task{
Batch: Batch{
ID: main.Id,
BatchNo: main.BatchNo,
BatchType: main.BatchType,
TenantID: main.TenantId,
CreatedDept: main.CreatedDept,
CreatedBy: main.CreatedBy,
StartTime: main.StartTime,
BatchInfo: main.BatchInfo,
},
Config: config,
Details: make([]*Detail, 0, len(batchDetails)),
}
for _, batchDetail := range batchDetails {
if batchDetail == nil {
continue
}
detail := &Detail{
ID: batchDetail.Id,
RowNo: batchDetail.RowNo,
DetailStatus: batchDetail.DetailStatus,
ItemType: config.ItemType,
Reason: strings.TrimSpace(batchDetail.ErrorReason),
Config: config,
}
if detail.DetailStatus != consts.BatchDetailStatusPending &&
detail.DetailStatus != consts.BatchDetailStatusRunning &&
detail.Reason == "" {
detail.Reason = "批次明细已处理,不能重复执行"
}
if config.ItemType == warnItemTypeFlowPool {
detail.FlowPoolCode = strings.TrimSpace(batchDetail.ItemId)
} else {
detail.ICCID = strings.TrimSpace(batchDetail.ItemId)
}
if batchDetail.Snapshot != nil {
if value := batchDetail.Snapshot.Get("itemType").Int(); value > 0 {
detail.ItemType = value
}
if value := strings.TrimSpace(batchDetail.Snapshot.Get("ICCID").String()); value != "" {
detail.ICCID = value
}
detail.MSISDN = strings.TrimSpace(batchDetail.Snapshot.Get("MSISDN").String())
detail.IMSI = strings.TrimSpace(batchDetail.Snapshot.Get("IMSI").String())
if value := strings.TrimSpace(batchDetail.Snapshot.Get("flowPoolCode").String()); value != "" {
detail.FlowPoolCode = value
}
detail.Authorized = batchDetail.Snapshot.Get("authorized").Bool()
detail.Config = configFromDetailSnapshot(batchDetail.Snapshot, config)
}
task.Details = append(task.Details, detail)
}
if len(task.Details) == 0 {
return nil, gerror.New("预警配置批次没有可读取明细")
}
if err = r.loadItems(ctx, task); err != nil {
return nil, err
}
return task, nil
}
func (r *Repository) loadItems(ctx context.Context, task *Task) error {
if task == nil {
return nil
}
if task.Config.ItemType == warnItemTypeFlowPool {
return r.loadFlowPools(ctx, task)
}
return r.loadCards(ctx, task)
}
func (r *Repository) loadCards(ctx context.Context, task *Task) error {
iccids := make([]string, 0, len(task.Details))
for _, detail := range task.Details {
if detail != nil && strings.TrimSpace(detail.ICCID) != "" {
iccids = append(iccids, strings.TrimSpace(detail.ICCID))
}
}
if len(iccids) == 0 {
return nil
}
var cards []*entity.TSimCardInfo
columns := dao.TSimCardInfo.Columns()
if err := r.database().Model(dao.TSimCardInfo.Table()).Safe().Ctx(ctx).
Where(columns.TenantId, task.Batch.TenantID).
WhereIn(columns.Iccid, iccids).
Scan(&cards); err != nil {
return gerror.Wrap(err, "查询SIM卡档案失败")
}
byICCID := make(map[string]*entity.TSimCardInfo, len(cards))
for _, card := range cards {
if card != nil {
byICCID[strings.TrimSpace(card.Iccid)] = card
}
}
for _, detail := range task.Details {
if detail == nil {
continue
}
card := byICCID[strings.TrimSpace(detail.ICCID)]
if card == nil {
continue
}
detail.Card = &Card{
ID: card.Id,
ICCID: strings.TrimSpace(card.Iccid),
MSISDN: strings.TrimSpace(card.Msisdn),
IMSI: strings.TrimSpace(card.Imsi),
CurrentPackageCode: strings.TrimSpace(card.CurrentPackageCode),
PackageStatus: card.PackageStatus,
BossCardStatus: card.BossCardStatus,
IsFlowPool: card.IsFlowPool,
FlowPkgEndAt: card.FlowPkgEndAt,
PackageTotalFlowMb: card.PackageTotalFlowMb,
}
}
return nil
}
func (r *Repository) loadFlowPools(ctx context.Context, task *Task) error {
codes := make([]string, 0, len(task.Details))
for _, detail := range task.Details {
if detail != nil && strings.TrimSpace(detail.FlowPoolCode) != "" {
codes = append(codes, strings.TrimSpace(detail.FlowPoolCode))
}
}
if len(codes) == 0 {
return nil
}
var pools []*entity.TSimFlowPool
columns := dao.TSimFlowPool.Columns()
if err := r.database().Model(dao.TSimFlowPool.Table()).Safe().Ctx(ctx).
Fields(
columns.Id,
columns.FlowPoolCode,
columns.TotalFlowMb,
columns.UsedFlowMb,
columns.RemainFlowMb,
columns.PoolStatus,
).
Where(columns.TenantId, task.Batch.TenantID).
WhereIn(columns.FlowPoolCode, codes).
Scan(&pools); err != nil {
return gerror.Wrap(err, "查询流量池档案失败")
}
byCode := make(map[string]*entity.TSimFlowPool, len(pools))
for _, pool := range pools {
if pool != nil {
byCode[strings.TrimSpace(pool.FlowPoolCode)] = pool
}
}
for _, detail := range task.Details {
if detail == nil {
continue
}
pool := byCode[strings.TrimSpace(detail.FlowPoolCode)]
if pool == nil {
continue
}
detail.Pool = &Pool{
ID: pool.Id,
FlowPoolCode: strings.TrimSpace(pool.FlowPoolCode),
TotalFlowMb: pool.TotalFlowMb,
UsedFlowMb: pool.UsedFlowMb,
RemainFlowMb: pool.RemainFlowMb,
PoolStatus: pool.PoolStatus,
}
}
return nil
}
func (r *Repository) Commit(ctx context.Context, task *Task) error {
if task == nil {
return gerror.New("预警配置任务不能为空")
}
if _, failCount := countResult(task); failCount > 0 {
markAtomicFailure(task, "预警配置批次存在失败明细,整批未执行")
}
successCount, failCount := countResult(task)
artifact, artifactErr := r.createResultFile(ctx, task)
if artifactErr != nil {
g.Log().Warningf(ctx, "warning config result file generation failed, batchNo:%s, err:%+v", task.Batch.BatchNo, artifactErr)
}
now := gtime.Now()
err := r.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, detail := range task.Details {
if detail == nil {
continue
}
if detail.Success() {
if err := r.upsertWarningConfig(ctx, tx, task, detail, now); err != nil {
return err
}
}
if err := r.updateDetail(ctx, tx, task, detail, now); err != nil {
return err
}
}
var ossID, downloadID int64
if artifactErr == nil {
var err error
ossID, downloadID, err = r.persistArtifact(ctx, tx, task, artifact, successCount, failCount, now)
if err != nil {
return err
}
}
return r.finalizeBatch(ctx, tx, task, successCount, failCount, ossID, downloadID, now)
})
if err != nil && artifactErr == nil && artifact.Path != "" {
batchresult.RemoveLocalFile(artifact)
}
return gerror.Wrap(err, "保存预警配置批次结果失败")
}
func (r *Repository) upsertWarningConfig(ctx context.Context, tx gdb.TX, task *Task, detail *Detail, now *gtime.Time) error {
itemID, itemCode, err := warningConfigItemSnapshot(task, detail)
if err != nil {
return err
}
config := detail.Config
columns := dao.TWarnConfig.Columns()
excludeFields := "id,tenant_id,item_type,item_id,warn_unit,created_dept,created_by,created_at"
if !config.AppNotifyProvided {
excludeFields += ",app_notify_enabled"
}
if !config.WxTemplateProvided {
excludeFields += ",wx_template_id"
}
_, err = tx.Ctx(ctx).Model(dao.TWarnConfig.Table()).Safe().
Data(do.TWarnConfig{
TenantId: task.Batch.TenantID,
BatchNo: task.Batch.BatchNo,
ItemType: config.ItemType,
ItemId: itemID,
ItemCode: itemCode,
WarnUnit: config.WarnUnit,
FirstWarn: config.FirstWarn,
SecondWarn: config.SecondWarn,
ThirdWarn: config.ThirdWarn,
WarnMethods: config.WarnMethods,
WarnPhones: config.WarnPhones,
WarnEmails: config.WarnEmails,
AutoAction: config.AutoAction,
IsEnabled: config.IsEnabled,
WxTemplateId: config.WxTemplateID,
AppNotifyEnabled: config.AppNotifyEnabled,
Remark: config.Remark,
CreatedDept: task.Batch.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
CreatedAt: now,
UpdatedBy: task.Batch.CreatedBy,
UpdatedAt: now,
}).
OnConflict(columns.TenantId, columns.ItemType, columns.ItemId, columns.WarnUnit).
OnDuplicateEx(excludeFields).
Save()
return gerror.Wrap(err, "写入预警配置失败")
}
func warningConfigItemSnapshot(task *Task, detail *Detail) (int64, string, error) {
if task == nil || detail == nil {
return 0, "", gerror.New("预警配置明细为空")
}
if detail.Config.ItemType == warnItemTypeFlowPool {
if detail.Pool == nil {
return 0, "", gerror.New("预警配置明细缺少流量池档案")
}
itemCode := strings.TrimSpace(detail.Pool.FlowPoolCode)
if itemCode == "" {
itemCode = strings.TrimSpace(detail.FlowPoolCode)
}
return detail.Pool.ID, itemCode, nil
}
if detail.Card == nil {
return 0, "", gerror.New("预警配置明细缺少SIM卡档案")
}
itemCode := strings.TrimSpace(detail.Card.ICCID)
if itemCode == "" {
itemCode = strings.TrimSpace(detail.ICCID)
}
return detail.Card.ID, itemCode, nil
}
func (r *Repository) updateDetail(ctx context.Context, tx gdb.TX, task *Task, detail *Detail, now *gtime.Time) error {
if detail == nil ||
(detail.DetailStatus != consts.BatchDetailStatusPending &&
detail.DetailStatus != consts.BatchDetailStatusRunning) {
return nil
}
status := consts.BatchDetailStatusFailed
if detail.Success() {
status = consts.BatchDetailStatusSuccess
}
result, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().Id, detail.ID).
Where(dao.BatchDetail.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypeSimWarningConfig).
WhereIn(dao.BatchDetail.Columns().DetailStatus, []int{
consts.BatchDetailStatusPending,
consts.BatchDetailStatusRunning,
}).
Data(do.BatchDetail{
DetailStatus: status,
ErrorReason: detail.Reason,
Snapshot: detail.Snapshot(detail.Config),
UpdatedBy: task.Batch.CreatedBy,
UpdatedAt: now,
}).Update()
if err != nil {
return gerror.Wrap(err, "更新预警配置批次明细失败")
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return gerror.Newf("预警配置批次明细%d已不是运行中状态", detail.ID)
}
return nil
}
func (r *Repository) finalizeBatch(ctx context.Context, tx gdb.TX, task *Task, successCount, failCount int, ossID, downloadID int64, now *gtime.Time) error {
batchAsyncStatus, runResult := batchResult(successCount, failCount)
result, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, task.Batch.BatchNo).
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeSimWarningConfig).
Where(dao.BatchMain.Columns().BatchAsyncStatus, consts.BatchAsyncStatusProcessing).
Data(do.BatchMain{
BatchAsyncStatus: batchAsyncStatus,
RunResult: runResult,
TotalCount: len(task.Details),
ValidCount: successCount,
SuccessCount: successCount,
FailCount: failCount,
FailReason: task.FailureReason,
BatchInfo: mergeBatchInfo(task.Batch.BatchInfo, ossID, downloadID),
EndTime: now,
UpdatedBy: task.Batch.CreatedBy,
}).Update()
if err != nil {
return gerror.Wrap(err, "更新预警配置批次主表失败")
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return gerror.New("预警配置批次已不是处理中状态")
}
return nil
}
func (r *Repository) createResultFile(ctx context.Context, task *Task) (batchresult.Artifact, error) {
if r.results == nil {
return batchresult.Artifact{}, gerror.New("result generator is not configured")
}
return r.results.Create(ctx, batchresult.Request{
BatchNo: task.Batch.BatchNo,
BatchType: task.Batch.BatchType,
TenantID: task.Batch.TenantID,
Rows: task.ResultRows(),
})
}
func (r *Repository) persistArtifact(ctx context.Context, tx gdb.TX, task *Task, artifact batchresult.Artifact, successCount, failCount int, now *gtime.Time) (int64, int64, error) {
ossID, err := tx.Ctx(ctx).Model(dao.SysOss.Table()).Data(do.SysOss{
TenantId: task.Batch.TenantID,
FileName: artifact.FileName,
OriginalName: artifact.OriginalName,
FileSuffix: artifact.FileSuffix,
Path: artifact.Path,
Url: artifact.URL,
CreatedDept: task.Batch.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
Service: "local",
Md5: artifact.MD5,
FileSize: artifact.Size,
}).InsertAndGetId()
if err != nil {
return 0, 0, gerror.Wrap(err, "写入预警配置结果文件OSS失败")
}
downloadID, err := tx.Ctx(ctx).Model(dao.TResourceBatchDownload.Table()).Data(do.TResourceBatchDownload{
BatchNo: task.Batch.BatchNo,
TaskName: batchresult.TaskName(task.Batch.BatchType),
ResourceType: consts.ResourceTypeExcel,
ExportScene: consts.BatchTypeSimWarningConfig,
TotalCount: len(task.Details),
SuccessCount: successCount,
FailCount: failCount,
DownloadStatus: consts.DownloadStatusGenerated,
OssId: ossID,
FileName: artifact.FileName,
FileExt: artifact.FileSuffix,
FileUrl: artifact.URL,
FilePath: artifact.Path,
FileSize: artifact.Size,
FileMd5: artifact.MD5,
DownloadCount: 0,
MaxDownloadCount: 0,
DeptId: task.Batch.CreatedDept,
StartTime: task.Batch.StartTime,
FinishTime: now,
TenantId: task.Batch.TenantID,
CreatedDept: task.Batch.CreatedDept,
CreatedBy: task.Batch.CreatedBy,
UpdatedBy: task.Batch.CreatedBy,
}).InsertAndGetId()
if err != nil {
return 0, 0, gerror.Wrap(err, "写入预警配置结果下载记录失败")
}
return ossID, downloadID, nil
}
func configFromBatchInfo(info *gjson.Json) (Config, error) {
if info == nil {
return Config{}, gerror.New("预警配置批次缺少批次参数")
}
config := Config{
ItemType: info.Get("itemType").Int(),
WarnUnit: info.Get("warnUnit").Int(),
FirstWarn: info.Get("firstWarn").Float64(),
SecondWarn: info.Get("secondWarn").Float64(),
ThirdWarn: info.Get("thirdWarn").Float64(),
WarnMethods: strings.TrimSpace(info.Get("warnMethods").String()),
WarnPhones: strings.TrimSpace(info.Get("warnPhones").String()),
WarnEmails: strings.TrimSpace(info.Get("warnEmails").String()),
AutoAction: info.Get("autoAction").Int(),
IsEnabled: info.Get("isEnabled").Int(),
WxTemplateID: strings.TrimSpace(info.Get("wxTemplateId").String()),
AppNotifyEnabled: info.Get("appNotifyEnabled").Int(),
AppNotifyProvided: !info.Get("appNotifyEnabled").IsNil(),
WxTemplateProvided: !info.Get("wxTemplateId").IsNil(),
Remark: strings.TrimSpace(info.Get("remark").String()),
}
if config.WarnMethods == "" {
config.WarnMethods = warnDefaultMethod
}
if config.ItemType == 0 {
config.ItemType = warnItemTypeCard
}
config.WarnType = deriveWarnType(config.ItemType, config.WarnUnit)
if config.ItemType != warnItemTypeCard && config.ItemType != warnItemTypeFlowPool {
return Config{}, gerror.New("预警配置对象类型无效")
}
if config.WarnUnit == warnUnitDay || config.WarnUnit == warnUnitMonth {
return Config{}, gerror.New("日期预警由系统默认规则处理,不支持手动配置")
}
if config.AppNotifyEnabled != 0 && config.AppNotifyEnabled != 1 {
return Config{}, gerror.New("微信提醒启停状态无效")
}
if info.Get("rowConfig").Bool() {
if err := validateWarnTypeUnit(config.ItemType, config.WarnType, config.WarnUnit); err != nil {
return Config{}, err
}
return config, nil
}
if config.IsEnabled != 0 && config.IsEnabled != 1 {
return Config{}, gerror.New("预警配置启停状态无效")
}
if config.AutoAction != 0 && config.AutoAction != warnAutoActionSuspend {
return Config{}, gerror.New("预警配置自动动作无效")
}
if err := validateWarnTypeUnit(config.ItemType, config.WarnType, config.WarnUnit); err != nil {
return Config{}, err
}
if config.ItemType == warnItemTypeFlowPool && config.AutoAction != 0 {
return Config{}, gerror.New("流量池预警暂不支持第三次关停")
}
if err := validateConfigThresholds(&config); err != nil {
return Config{}, err
}
return config, nil
}
func configFromDetailSnapshot(snapshot *gjson.Json, fallback Config) Config {
if snapshot == nil {
return fallback
}
if !snapshot.Get("configSet").Bool() &&
snapshot.Get("configItemType").Int() == 0 &&
snapshot.Get("warnUnit").Int() == 0 {
return fallback
}
config := fallback
if value := snapshot.Get("configItemType").Int(); value > 0 {
config.ItemType = value
}
if value := snapshot.Get("warnUnit").Int(); value > 0 {
config.WarnUnit = value
}
config.FirstWarn = snapshot.Get("firstWarn").Float64()
config.SecondWarn = snapshot.Get("secondWarn").Float64()
config.ThirdWarn = snapshot.Get("thirdWarn").Float64()
if value := strings.TrimSpace(snapshot.Get("warnMethods").String()); value != "" {
config.WarnMethods = value
}
config.WarnPhones = strings.TrimSpace(snapshot.Get("warnPhones").String())
config.WarnEmails = strings.TrimSpace(snapshot.Get("warnEmails").String())
config.AutoAction = snapshot.Get("autoAction").Int()
if snapshot.Get("isEnabled").Val() != nil {
config.IsEnabled = snapshot.Get("isEnabled").Int()
}
if !snapshot.Get("wxTemplateId").IsNil() {
config.WxTemplateID = strings.TrimSpace(snapshot.Get("wxTemplateId").String())
config.WxTemplateProvided = true
}
if snapshot.Get("appNotifyEnabled").Val() != nil {
config.AppNotifyEnabled = snapshot.Get("appNotifyEnabled").Int()
config.AppNotifyProvided = true
}
config.Remark = strings.TrimSpace(snapshot.Get("remark").String())
config.WarnType = deriveWarnType(config.ItemType, config.WarnUnit)
return config
}
func validateConfigThresholds(config *Config) error {
if config == nil {
return gerror.New("预警配置参数不能为空")
}
if config.FirstWarn <= 0 {
return gerror.New("第一档阈值必须大于0")
}
if config.SecondWarn < 0 || config.ThirdWarn < 0 {
return gerror.New("预警阈值不能小于0")
}
if config.AutoAction == warnAutoActionSuspend && config.ThirdWarn <= 0 {
return gerror.New("开启第三次自动停机时必须配置第三档阈值")
}
switch config.WarnType {
case warnTypeCardTraffic:
if config.WarnUnit == warnUnitPercent {
config.FirstWarn = normalizePercentThreshold(config.FirstWarn)
config.SecondWarn = normalizePercentThreshold(config.SecondWarn)
config.ThirdWarn = normalizePercentThreshold(config.ThirdWarn)
if config.FirstWarn > 100 || config.SecondWarn > 100 || config.ThirdWarn > 100 {
return gerror.New("百分比阈值不能大于100")
}
}
return validateGreaterThresholds(config)
case warnTypeFlowPoolPercent:
config.FirstWarn = normalizePercentThreshold(config.FirstWarn)
config.SecondWarn = normalizePercentThreshold(config.SecondWarn)
config.ThirdWarn = normalizePercentThreshold(config.ThirdWarn)
if config.FirstWarn > 100 || config.SecondWarn > 100 || config.ThirdWarn > 100 {
return gerror.New("百分比阈值不能大于100")
}
return validateGreaterThresholds(config)
default:
return gerror.New("预警类型无效")
}
}
func validateGreaterThresholds(config *Config) error {
if config.SecondWarn > 0 && config.SecondWarn <= config.FirstWarn {
return gerror.New("流量预警第二档阈值必须大于第一档")
}
if config.ThirdWarn > 0 {
if config.SecondWarn <= 0 {
return gerror.New("配置第三档时必须先配置第二档")
}
if config.ThirdWarn <= config.SecondWarn {
return gerror.New("流量预警第三档阈值必须大于第二档")
}
}
return nil
}
func normalizePercentThreshold(value float64) float64 {
if value > 0 && value <= 1 {
return value * 100
}
return value
}
func deriveWarnType(itemType, warnUnit int) int {
if itemType == warnItemTypeFlowPool {
return warnTypeFlowPoolPercent
}
switch warnUnit {
case warnUnitMb, warnUnitPercent:
return warnTypeCardTraffic
case warnUnitDay:
return warnTypeExpireDay
case warnUnitMonth:
return warnTypeExpireMonth
default:
return 0
}
}
func validateWarnTypeUnit(itemType, warnType, warnUnit int) error {
switch warnType {
case warnTypeCardTraffic:
if itemType != warnItemTypeCard {
return gerror.New("单卡流量预警只能配置单卡对象")
}
if warnUnit != warnUnitMb && warnUnit != warnUnitPercent {
return gerror.New("单卡流量预警仅支持MB或百分比单位")
}
case warnTypeExpireDay:
return gerror.New("日期预警由系统默认规则处理,不支持手动配置")
case warnTypeExpireMonth:
return gerror.New("日期预警由系统默认规则处理,不支持手动配置")
case warnTypeFlowPoolPercent:
if itemType != warnItemTypeFlowPool {
return gerror.New("流量池预警只能配置流量池对象")
}
if warnUnit != warnUnitPercent {
return gerror.New("流量池预警仅支持百分比单位")
}
default:
return gerror.New("预警类型无效")
}
return nil
}
func mergeBatchInfo(info *gjson.Json, ossID, downloadID int64) *gjson.Json {
data := make(map[string]any)
if info != nil {
for key, value := range info.Map() {
data[key] = value
}
}
if ossID > 0 {
data["resultFileOssId"] = ossID
}
if downloadID > 0 {
data["resultDownloadId"] = downloadID
data["resultFile"] = map[string]any{"status": "generated"}
} else {
data["resultFile"] = map[string]any{"status": "skipped"}
}
return gjson.New(data)
}
func batchResult(successCount, failCount int) (int, int) {
if failCount > 0 || successCount == 0 {
return consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed
}
return consts.BatchAsyncStatusSuccess, consts.BatchRunResultSuccess
}
@@ -0,0 +1,201 @@
package warningconfig
import (
"strings"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gtime"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
)
const (
warnItemTypeCard = 1
warnItemTypeFlowPool = 2
warnUnitMb = 1
warnUnitPercent = 2
warnUnitDay = 3
warnUnitMonth = 4
warnTypeCardTraffic = 1
warnTypeExpireDay = 2
warnTypeExpireMonth = 3
warnTypeFlowPoolPercent = 4
warnAutoActionSuspend = 3
warnDefaultMethod = "3"
flowPoolStatusNormal = 0
)
type Batch struct {
ID int64
BatchNo string
BatchType int
TenantID string
CreatedDept int64
CreatedBy int64
StartTime *gtime.Time
BatchInfo *gjson.Json
}
type Config struct {
ItemType int
WarnType int
WarnUnit int
FirstWarn float64
SecondWarn float64
ThirdWarn float64
WarnMethods string
WarnPhones string
WarnEmails string
AutoAction int
IsEnabled int
WxTemplateID string
AppNotifyEnabled int
AppNotifyProvided bool
WxTemplateProvided bool
Remark string
}
type Card struct {
ID int64
ICCID string
MSISDN string
IMSI string
CurrentPackageCode string
PackageStatus int
BossCardStatus int
IsFlowPool int
FlowPkgEndAt *gtime.Time
PackageTotalFlowMb float64
}
type Pool struct {
ID int64
FlowPoolCode string
TotalFlowMb float64
UsedFlowMb float64
RemainFlowMb float64
PoolStatus int
}
type Detail struct {
ID int64
RowNo int
DetailStatus int
ItemType int
ICCID string
MSISDN string
IMSI string
FlowPoolCode string
Authorized bool
Card *Card
Pool *Pool
Reason string
Config Config
}
type Task struct {
Batch Batch
Config Config
Details []*Detail
FailureReason string
}
func (d *Detail) Success() bool {
return d != nil && strings.TrimSpace(d.Reason) == ""
}
func (d *Detail) DisplayCode() string {
if d == nil {
return ""
}
if d.ItemType == warnItemTypeFlowPool {
if d.Pool != nil && strings.TrimSpace(d.Pool.FlowPoolCode) != "" {
return strings.TrimSpace(d.Pool.FlowPoolCode)
}
return strings.TrimSpace(d.FlowPoolCode)
}
if d.Card != nil && strings.TrimSpace(d.Card.ICCID) != "" {
return strings.TrimSpace(d.Card.ICCID)
}
return strings.TrimSpace(d.ICCID)
}
func (d *Detail) Snapshot(config Config) *gjson.Json {
if d == nil {
return gjson.New(nil)
}
data := map[string]any{
"rowNo": d.RowNo,
"itemType": d.ItemType,
"ICCID": d.ICCID,
"MSISDN": d.MSISDN,
"IMSI": d.IMSI,
"flowPoolCode": d.FlowPoolCode,
"authorized": d.Authorized,
"configItemType": config.ItemType,
"configSet": true,
"warnUnit": config.WarnUnit,
"firstWarn": config.FirstWarn,
"secondWarn": config.SecondWarn,
"thirdWarn": config.ThirdWarn,
"warnMethods": config.WarnMethods,
"warnPhones": config.WarnPhones,
"warnEmails": config.WarnEmails,
"autoAction": config.AutoAction,
"isEnabled": config.IsEnabled,
"remark": config.Remark,
"errorReason": d.Reason,
}
if config.WxTemplateProvided {
data["wxTemplateId"] = config.WxTemplateID
}
if config.AppNotifyProvided {
data["appNotifyEnabled"] = config.AppNotifyEnabled
}
if d.Card != nil {
data["cardId"] = d.Card.ID
data["cardIccid"] = d.Card.ICCID
data["cardMsisdn"] = d.Card.MSISDN
data["cardImsi"] = d.Card.IMSI
data["currentPackageCode"] = d.Card.CurrentPackageCode
data["packageStatus"] = d.Card.PackageStatus
data["bossCardStatus"] = d.Card.BossCardStatus
data["isFlowPool"] = d.Card.IsFlowPool
data["packageTotalFlowMb"] = d.Card.PackageTotalFlowMb
if d.Card.FlowPkgEndAt != nil {
data["flowPkgEndAt"] = d.Card.FlowPkgEndAt.String()
}
}
if d.Pool != nil {
data["poolId"] = d.Pool.ID
data["poolFlowPoolCode"] = d.Pool.FlowPoolCode
data["poolTotalFlowMb"] = d.Pool.TotalFlowMb
data["poolUsedFlowMb"] = d.Pool.UsedFlowMb
data["poolRemainFlowMb"] = d.Pool.RemainFlowMb
data["poolStatus"] = d.Pool.PoolStatus
}
return gjson.New(data)
}
func (t *Task) ResultRows() []batchresult.Row {
rows := make([]batchresult.Row, 0, len(t.Details))
for _, detail := range t.Details {
if detail == nil {
continue
}
rows = append(rows, batchresult.Row{
RowNo: detail.RowNo,
ObjectCode: detail.DisplayCode(),
ICCID: detail.ICCID,
MSISDN: detail.MSISDN,
IMSI: detail.IMSI,
Success: detail.Success(),
Reason: detail.Reason,
})
}
return rows
}