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
+122
View File
@@ -0,0 +1,122 @@
package cancel
import (
"context"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/batchresult"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/packagecustomer"
)
const (
cascadeFailureReason = "same batch has invalid cancellation rows; this row was skipped"
validationSkipReason = "batch validation failed; cancellation was skipped"
emptyICCIDReason = "ICCID cannot be empty"
duplicateICCIDReason = "duplicate ICCID in batch"
)
type executor struct {
repository Repository
}
func NewExecutor(repository Repository) Executor {
return &executor{repository: repository}
}
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 cancel batch failed")
}
if !validateTask(task) {
if err = e.repository.SaveValidationFailure(ctx, task); err != nil {
return gerror.Wrap(err, "save cancel validation failure failed")
}
e.saveResultFile(ctx, task)
return nil
}
if err = e.repository.CommitBusiness(ctx, task); err != nil {
return gerror.Wrap(err, "commit cancel business failed")
}
packagecustomer.PublishSyncForDeptsSafe(ctx, task.Batch.TenantID, task.Batch.CreatedBy, cancelPackageSyncDeptIDs(task), "cancel", task.Batch.BatchNo)
e.saveResultFile(ctx, task)
return nil
}
func cancelPackageSyncDeptIDs(task *Task) []int64 {
if task == nil {
return nil
}
deptIds := []int64{task.Batch.DeptID, task.Batch.CreatedDept}
for _, detail := range task.Details {
if detail == nil || strings.TrimSpace(detail.Reason) != "" {
continue
}
if detail.CardDeptID > 0 {
deptIds = append(deptIds, detail.CardDeptID)
}
}
return deptIds
}
func (e *executor) saveResultFile(ctx context.Context, task *Task) {
request := batchresult.Request{
BatchNo: task.Batch.BatchNo,
BatchType: task.Batch.BatchType,
TenantID: task.Batch.TenantID,
Rows: task.ResultRows(),
}
if err := e.repository.SaveResultFile(ctx, task, request); err != nil {
g.Log().Errorf(ctx, "save cancel result file status failed, batchNo:%s, err:%+v", task.Batch.BatchNo, err)
}
}
func validateTask(task *Task) bool {
if task == nil || len(task.Details) == 0 {
if task != nil {
task.FailureReason = "cancel batch has no valid details"
}
return false
}
hasFailure := false
iccids := make(map[string]int, len(task.Details))
for _, detail := range task.Details {
if detail == nil {
hasFailure = true
continue
}
if detail.Reason != "" {
hasFailure = true
continue
}
iccid := strings.TrimSpace(detail.ICCID)
if iccid == "" {
detail.Reason = emptyICCIDReason
hasFailure = true
continue
}
iccids[iccid]++
}
for _, detail := range task.Details {
if detail == nil || detail.Reason != "" {
continue
}
if iccids[strings.TrimSpace(detail.ICCID)] > 1 {
detail.Reason = duplicateICCIDReason
hasFailure = true
}
}
if !hasFailure {
return true
}
for _, detail := range task.Details {
if detail != nil && detail.Reason == "" {
detail.Reason = cascadeFailureReason
}
}
task.FailureReason = validationSkipReason
return false
}
+117
View File
@@ -0,0 +1,117 @@
package cancel
import (
"context"
"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"
)
type Batch struct {
ID int64
BatchNo string
BatchType int
TenantID string
DeptID int64
CreatedBy int64
CreatedDept int64
StartTime *gtime.Time
BatchInfo *gjson.Json
}
type Detail struct {
ID int64
RowNo int
ICCID string
MSISDN string
IMSI string
Reason string
Authorized bool
InputMSISDN string
InputIMSI string
StatusBefore int
TargetStatus int
CancelTime string
SubscriptionNo string
OrderCode string
PackageCode string
PackageType int
CardDeptID int64
}
type Task struct {
Batch Batch
Details []*Detail
FailureReason string
}
type Repository interface {
Load(ctx context.Context, batchNo string) (*Task, error)
SaveValidationFailure(ctx context.Context, task *Task) error
CommitBusiness(ctx context.Context, task *Task) error
SaveResultFile(ctx context.Context, task *Task, request batchresult.Request) error
}
type Executor interface {
Execute(ctx context.Context, batchNo string) error
}
func (d *Detail) Snapshot() *gjson.Json {
if d == nil {
return gjson.New(nil)
}
return gjson.New(map[string]any{
"rowNo": d.RowNo,
"ICCID": d.ICCID,
"MSISDN": d.MSISDN,
"IMSI": d.IMSI,
"inputMsisdn": d.InputMSISDN,
"inputImsi": d.InputIMSI,
"authorized": d.Authorized,
"statusBefore": d.StatusBefore,
"targetStatus": d.TargetStatus,
"cancelTime": d.CancelTime,
"subscriptionNo": d.SubscriptionNo,
"orderCode": d.OrderCode,
"packageCode": d.PackageCode,
"packageType": d.PackageType,
"cardDeptId": d.CardDeptID,
"errorReason": d.Reason,
})
}
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,
ICCID: detail.ICCID,
MSISDN: detail.MSISDN,
IMSI: detail.IMSI,
StatusBefore: detail.StatusBefore,
TargetStatus: detail.TargetStatus,
Success: detail.Reason == "",
Reason: detail.Reason,
})
}
return rows
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
return value
}
}
return ""
}