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,396 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user