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:
MiFaZhan
2026-10-22 16:37:56 +08:00
commit 2bec257289
361 changed files with 71524 additions and 0 deletions
+396
View File
@@ -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)
}