2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
397 lines
15 KiB
Go
397 lines
15 KiB
Go
package renewchange
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"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/batch"
|
|
"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"
|
|
batchrepo "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/batch"
|
|
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
|
|
)
|
|
|
|
// Repository orchestrates table access + batch status writes for renew/change
|
|
// service. It embeds the typed table repository and adds batch lifecycle
|
|
// helpers that align with the Admin side batch_main/batch_detail contract.
|
|
type Repository struct {
|
|
tables *rc.Repository
|
|
batchRepo *batchrepo.Repository
|
|
results batchresult.Generator
|
|
}
|
|
|
|
// NewRepository builds a service-level repository backed by the typed table
|
|
// repository (g.DB() default) and the shared batch repository.
|
|
func NewRepository() *Repository {
|
|
return &Repository{
|
|
tables: rc.New(),
|
|
batchRepo: batchrepo.New(),
|
|
results: batchresult.New(),
|
|
}
|
|
}
|
|
|
|
// newRepositoryWithDeps allows tests to inject custom dependencies.
|
|
func newRepositoryWithDeps(tables *rc.Repository, batchRepo *batchrepo.Repository, results batchresult.Generator) *Repository {
|
|
return &Repository{tables: tables, batchRepo: batchRepo, results: results}
|
|
}
|
|
|
|
// Tables exposes the low-level repository for tests.
|
|
func (r *Repository) Tables() *rc.Repository { return r.tables }
|
|
|
|
// Batch exposes the batch repository.
|
|
func (r *Repository) Batch() *batchrepo.Repository { return r.batchRepo }
|
|
|
|
// Results exposes the result file generator.
|
|
func (r *Repository) Results() batchresult.Generator { return r.results }
|
|
|
|
// DB exposes the underlying gdb.DB handle.
|
|
func (r *Repository) DB() gdb.DB { return r.tables.DB() }
|
|
|
|
// LoadProcessingBatch loads a processing renew/change/prepare batch by
|
|
// (batchNo, batchType). It returns an error if the batch is not in
|
|
// processing state so the caller can log and skip safely.
|
|
func (r *Repository) LoadProcessingBatch(ctx context.Context, batchNo string, batchType int) (*rc.BatchMainRow, error) {
|
|
main, err := r.tables.LoadBatchMain(ctx, batchNo, batchType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if main == nil {
|
|
return nil, gerror.Newf("batch %s (type %d) does not exist", batchNo, batchType)
|
|
}
|
|
if main.BatchAsyncStatus != batch.AsyncStatusProcessing {
|
|
return nil, gerror.Newf("batch %s (type %d) is not in processing state: %d", batchNo, batchType, main.BatchAsyncStatus)
|
|
}
|
|
return main, nil
|
|
}
|
|
|
|
// LoadRunningDetails returns the running batch details ordered by row_no.
|
|
func (r *Repository) LoadRunningDetails(ctx context.Context, batchNo string, batchType int) ([]*rc.BatchDetailRow, error) {
|
|
return r.tables.LoadBatchDetails(ctx, batchNo, batchType, []int{consts.BatchDetailStatusRunning})
|
|
}
|
|
|
|
// LoadAllDetails returns every detail regardless of status, used for the
|
|
// confirm step (renew/change) to inspect previously committed results.
|
|
func (r *Repository) LoadAllDetails(ctx context.Context, batchNo string, batchType int) ([]*rc.BatchDetailRow, error) {
|
|
return r.tables.LoadBatchDetails(ctx, batchNo, batchType, nil)
|
|
}
|
|
|
|
type ConvertPrepareResult struct {
|
|
MainAffected int64
|
|
DetailAffected int64
|
|
}
|
|
|
|
// ConvertPrepareToRenewChange switches the prepare batch (type=4) to the
|
|
// matching business type (101/102). The transaction must be the one used for
|
|
// the detail update.
|
|
func (r *Repository) ConvertPrepareToRenewChange(ctx context.Context, tx gdb.TX, batchNo string, targetType int) (*ConvertPrepareResult, error) {
|
|
detailResult, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
|
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
|
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
|
|
Data(do.BatchDetail{BatchType: targetType, UpdatedAt: gtime.Now()}).
|
|
Update()
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "convert prepare details to target type failed")
|
|
}
|
|
detailAffected, err := detailResult.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mainResult, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
|
Where(dao.BatchMain.Columns().BatchNo, batchNo).
|
|
Where(dao.BatchMain.Columns().BatchType, consts.BatchTypePrepare).
|
|
Data(do.BatchMain{BatchType: targetType, UpdatedAt: gtime.Now()}).
|
|
Update()
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "convert prepare batch type failed")
|
|
}
|
|
mainAffected, err := mainResult.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mainAffected == 0 {
|
|
return nil, gerror.New("prepare batch main already converted or missing")
|
|
}
|
|
return &ConvertPrepareResult{
|
|
MainAffected: mainAffected,
|
|
DetailAffected: detailAffected,
|
|
}, nil
|
|
}
|
|
|
|
// MarkDetailFailed writes a single detail as failed with the given reason.
|
|
func (r *Repository) MarkDetailFailed(ctx context.Context, tx gdb.TX, batchNo string, batchType, rowNo int, reason string, userID int64) error {
|
|
return r.tables.UpdateBatchDetailSnapshot(ctx, tx, batchNo, batchType, rowNo, consts.BatchDetailStatusFailed, nil, reason, userID)
|
|
}
|
|
|
|
// MarkDetailSuccess writes a single detail as success with the given snapshot.
|
|
func (r *Repository) MarkDetailSuccess(ctx context.Context, tx gdb.TX, batchNo string, batchType, rowNo int, snapshot *gjson.Json, userID int64) error {
|
|
return r.tables.UpdateBatchDetailSnapshot(ctx, tx, batchNo, batchType, rowNo, consts.BatchDetailStatusSuccess, snapshot, "", userID)
|
|
}
|
|
|
|
func (r *Repository) MarkPrepareDetailSuccess(ctx context.Context, tx gdb.TX, batchNo string, rowNo int, itemID string, snapshot *gjson.Json, userID int64) error {
|
|
data := map[string]any{
|
|
dao.BatchDetail.Columns().DetailStatus: consts.BatchDetailStatusSuccess,
|
|
dao.BatchDetail.Columns().Snapshot: snapshot,
|
|
dao.BatchDetail.Columns().UpdatedBy: userID,
|
|
dao.BatchDetail.Columns().UpdatedAt: gtime.Now(),
|
|
}
|
|
if itemID = strings.TrimSpace(itemID); itemID != "" {
|
|
data[dao.BatchDetail.Columns().ItemId] = itemID
|
|
}
|
|
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
|
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
|
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
|
|
Where(dao.BatchDetail.Columns().RowNo, rowNo).
|
|
Data(data).
|
|
Update()
|
|
return gerror.Wrap(err, "mark prepare detail success failed")
|
|
}
|
|
|
|
// MarkAllDetailsFailed marks every unfinished detail as failed in a single update.
|
|
func (r *Repository) MarkAllDetailsFailed(ctx context.Context, tx gdb.TX, batchNo string, batchType int, reason string, userID int64) error {
|
|
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
|
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
|
Where(dao.BatchDetail.Columns().BatchType, batchType).
|
|
WhereIn(dao.BatchDetail.Columns().DetailStatus, []int{consts.BatchDetailStatusPending, consts.BatchDetailStatusRunning}).
|
|
Data(do.BatchDetail{
|
|
DetailStatus: consts.BatchDetailStatusFailed,
|
|
ErrorReason: reason,
|
|
UpdatedBy: userID,
|
|
UpdatedAt: gtime.Now(),
|
|
}).
|
|
Update()
|
|
return gerror.Wrap(err, "mark all running details failed")
|
|
}
|
|
|
|
// MarkAllDetailsSuccess marks every running detail as success.
|
|
func (r *Repository) MarkAllDetailsSuccess(ctx context.Context, tx gdb.TX, batchNo string, batchType int, userID int64, extra map[string]any) error {
|
|
data := map[string]any{
|
|
"detail_status": consts.BatchDetailStatusSuccess,
|
|
"updated_by": userID,
|
|
"updated_at": gtime.Now(),
|
|
}
|
|
for k, v := range extra {
|
|
data[k] = v
|
|
}
|
|
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
|
|
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
|
|
Where(dao.BatchDetail.Columns().BatchType, batchType).
|
|
Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusRunning).
|
|
Data(data).
|
|
Update()
|
|
return gerror.Wrap(err, "mark all running details success")
|
|
}
|
|
|
|
// UpdateBatchMainInfo merges new entries into batch_main.batch_info and
|
|
// updates the lifecycle counters in a single statement.
|
|
func (r *Repository) UpdateBatchMainInfo(ctx context.Context, tx gdb.TX, batchNo string, batchType int, data *rc.BatchMainRow) error {
|
|
return r.tables.UpdateBatchMainStatus(ctx, tx, batchNo, batchType, data)
|
|
}
|
|
|
|
// FinalizeBatchMain updates only the lifecycle fields (status, counts,
|
|
// fail_reason, end_time) while keeping batch_info untouched.
|
|
func (r *Repository) FinalizeBatchMain(ctx context.Context, tx gdb.TX, batchNo string, batchType int, data *rc.BatchMainRow) error {
|
|
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
|
|
Where(dao.BatchMain.Columns().BatchNo, batchNo).
|
|
Where(dao.BatchMain.Columns().BatchType, batchType).
|
|
Data(do.BatchMain{
|
|
BatchAsyncStatus: data.BatchAsyncStatus,
|
|
RunResult: data.RunResult,
|
|
SuccessCount: data.SuccessCount,
|
|
FailCount: data.FailCount,
|
|
ValidCount: data.ValidCount,
|
|
TotalCount: data.TotalCount,
|
|
FailReason: data.FailReason,
|
|
EndTime: data.EndTime,
|
|
UpdatedBy: data.UpdatedBy,
|
|
}).
|
|
Update()
|
|
return gerror.Wrap(err, "finalize batch main failed")
|
|
}
|
|
|
|
// ResolveResultSnapshot returns the per-row data needed to render the result
|
|
// file. It always includes every detail (success and failure) for the batch.
|
|
func (r *Repository) ResolveResultSnapshot(ctx context.Context, batchNo string, batchType int) ([]ResultRow, error) {
|
|
details, err := r.tables.LoadBatchDetails(ctx, batchNo, batchType, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows := make([]ResultRow, 0, len(details))
|
|
for _, d := range details {
|
|
if d == nil {
|
|
continue
|
|
}
|
|
msisdn, imsi := "", ""
|
|
targetCode, packageCode, effectiveMode, orderNum := "", "", "", ""
|
|
if d.Snapshot != nil {
|
|
msisdn = strings.TrimSpace(d.Snapshot.Get("msisdn").String())
|
|
imsi = strings.TrimSpace(d.Snapshot.Get("imsi").String())
|
|
targetCode = strings.TrimSpace(d.Snapshot.Get("targetCode").String())
|
|
if targetCode == "" {
|
|
targetCode = strings.TrimSpace(d.ItemId)
|
|
}
|
|
packageCode = strings.TrimSpace(d.Snapshot.Get("packageCode").String())
|
|
effectiveMode = strings.TrimSpace(d.Snapshot.Get("effectiveMode").String())
|
|
orderNum = strings.TrimSpace(d.Snapshot.Get("orderSn").String())
|
|
if orderNum == "" {
|
|
orderNum = strings.TrimSpace(d.Snapshot.Get("orderNo").String())
|
|
}
|
|
}
|
|
rows = append(rows, ResultRow{
|
|
RowNo: d.RowNo,
|
|
ICCID: strings.TrimSpace(d.ItemId),
|
|
TargetCode: targetCode,
|
|
PackageCode: packageCode,
|
|
EffectiveMode: effectiveMode,
|
|
MSISDN: msisdn,
|
|
IMSI: imsi,
|
|
Success: d.DetailStatus == consts.BatchDetailStatusSuccess,
|
|
Reason: strings.TrimSpace(d.ErrorReason),
|
|
OrderNum: orderNum,
|
|
Snap: d.Snapshot,
|
|
DetailID: d.Id,
|
|
DetailCode: d.ErrorCode,
|
|
})
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// WriteResultFileSafe persists a result Excel and OSS/download records. It
|
|
// never panics on a bad batch type; missing configuration is logged and the
|
|
// business outcome is preserved. The caller must not abort the batch when
|
|
// this returns an error.
|
|
func (r *Repository) WriteResultFileSafe(ctx context.Context, batchNo string, batchType int, tenantID string, deptID, userID int64, rows []ResultRow) error {
|
|
if r.results == nil {
|
|
return nil
|
|
}
|
|
brRows := make([]batchresult.Row, 0, len(rows))
|
|
for _, row := range rows {
|
|
brRows = append(brRows, batchresult.Row{
|
|
RowNo: row.RowNo,
|
|
ObjectCode: row.TargetCode,
|
|
TargetCode: row.TargetCode,
|
|
PackageCode: row.PackageCode,
|
|
EffectiveMode: row.EffectiveMode,
|
|
ICCID: row.ICCID,
|
|
MSISDN: row.MSISDN,
|
|
IMSI: row.IMSI,
|
|
Success: row.Success,
|
|
Reason: row.Reason,
|
|
OrderNum: row.OrderNum,
|
|
})
|
|
}
|
|
request := batchresult.Request{
|
|
BatchNo: batchNo,
|
|
BatchType: batchType,
|
|
TenantID: strings.TrimSpace(tenantID),
|
|
Rows: brRows,
|
|
}
|
|
artifact, err := r.results.Create(ctx, request)
|
|
if err != nil {
|
|
g.Log().Warningf(ctx, "renew/change result file generation failed, batchNo:%s, batchType:%d, err:%+v", batchNo, batchType, err)
|
|
return err
|
|
}
|
|
if err = r.persistArtifact(ctx, artifact, batchNo, batchType, tenantID, deptID, userID, len(rows)); err != nil {
|
|
g.Log().Warningf(ctx, "renew/change result file bookkeeping failed, batchNo:%s, batchType:%d, err:%+v", batchNo, batchType, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) persistArtifact(ctx context.Context, artifact batchresult.Artifact, batchNo string, batchType int, tenantID string, deptID, userID int64, total int) error {
|
|
return r.tables.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
ossID, err := r.tables.InsertSysOss(ctx, tx, &rc.SysOssRow{
|
|
TenantId: tenantID,
|
|
FileName: artifact.FileName,
|
|
OriginalName: artifact.OriginalName,
|
|
FileSuffix: artifact.FileSuffix,
|
|
Path: artifact.Path,
|
|
Url: artifact.URL,
|
|
Md5: artifact.MD5,
|
|
FileSize: artifact.Size,
|
|
Service: "local",
|
|
CreatedDept: deptID,
|
|
CreatedBy: userID,
|
|
UpdatedBy: userID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
successCount, failCount := 0, 0
|
|
main, mErr := r.tables.LoadBatchMain(ctx, batchNo, batchType)
|
|
if mErr == nil && main != nil {
|
|
successCount = main.SuccessCount
|
|
failCount = main.FailCount
|
|
}
|
|
_, err = r.tables.InsertBatchDownload(ctx, tx, &rc.ResourceBatchDownloadRow{
|
|
BatchNo: batchNo,
|
|
TaskName: batchresult.TaskName(batchType),
|
|
ResourceType: consts.ResourceTypeExcel,
|
|
ExportScene: batchType,
|
|
TotalCount: total,
|
|
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,
|
|
DeptId: deptID,
|
|
StartTime: gtime.Now(),
|
|
FinishTime: gtime.Now(),
|
|
TenantId: tenantID,
|
|
CreatedDept: deptID,
|
|
CreatedBy: userID,
|
|
UpdatedBy: userID,
|
|
})
|
|
return err
|
|
})
|
|
}
|
|
|
|
// LoadSystemContext returns tenant id and a default user id (CreatedBy) for
|
|
// the given batch. Centralized so individual executors stay slim.
|
|
func (r *Repository) LoadSystemContext(ctx context.Context, batchNo string, batchType int) (string, int64, int64, error) {
|
|
main, err := r.tables.LoadBatchMain(ctx, batchNo, batchType)
|
|
if err != nil {
|
|
return "", 0, 0, err
|
|
}
|
|
if main == nil {
|
|
return "", 0, 0, gerror.Newf("batch %s (type %d) does not exist", batchNo, batchType)
|
|
}
|
|
return main.TenantId, main.CreatedBy, main.CreatedDept, nil
|
|
}
|
|
|
|
// ResultRow is the per-row information needed by the result file.
|
|
type ResultRow struct {
|
|
RowNo int
|
|
ICCID string
|
|
TargetCode string
|
|
PackageCode string
|
|
EffectiveMode string
|
|
MSISDN string
|
|
IMSI string
|
|
Success bool
|
|
Reason string
|
|
OrderNum string
|
|
Upstream int
|
|
Snap *gjson.Json
|
|
DetailID int64
|
|
DetailCode string
|
|
}
|
|
|
|
func tenantIDtoString(tenantID int64) string {
|
|
return strconv.FormatInt(tenantID, 10)
|
|
}
|