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,131 @@
|
||||
package simstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
zsetconsumer "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consumer/zset"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
queue "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset"
|
||||
simstatusrepo "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/simstatus"
|
||||
)
|
||||
|
||||
const (
|
||||
telecomRetryableCode = "S00004"
|
||||
telecomStatusTemplate = "sim_dx_v1"
|
||||
chanGatewayInternalError = 9999
|
||||
// tokenExpiredCode is the carrier code for an expired/invalid upstream
|
||||
// token. The gateway already invalidates and refreshes the token once on
|
||||
// this code; when it still reaches the worker, retrying in a later round
|
||||
// lets a refreshed token succeed instead of parking the card as a terminal
|
||||
// business failure.
|
||||
tokenExpiredCode = 12021
|
||||
)
|
||||
|
||||
type StatusClient interface {
|
||||
QuerySimCardStatus(context.Context, *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error)
|
||||
}
|
||||
|
||||
type StatusRepository interface {
|
||||
ApplyBatch(context.Context, []simstatusrepo.Result) error
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
client StatusClient
|
||||
repo StatusRepository
|
||||
networkRetries int
|
||||
}
|
||||
|
||||
func NewHandler(client StatusClient, repo StatusRepository, networkRetries ...int) (*Handler, error) {
|
||||
if client == nil || repo == nil {
|
||||
return nil, fmt.Errorf("sim status handler dependencies are incomplete")
|
||||
}
|
||||
retries := 0
|
||||
if len(networkRetries) > 0 && networkRetries[0] > 0 {
|
||||
retries = networkRetries[0]
|
||||
}
|
||||
return &Handler{client: client, repo: repo, networkRetries: retries}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) Query(ctx context.Context, task queue.Task) zsetconsumer.Result {
|
||||
result := zsetconsumer.Result{Task: task}
|
||||
input := &model.SimCardStatusInput{SimKeys: []*model.SimKey{{
|
||||
ChanKey: task.ChanKey,
|
||||
ICCID: task.ICCID,
|
||||
IMSI: task.IMSI,
|
||||
MSISDN: task.MSISDN,
|
||||
}}}
|
||||
var (
|
||||
output []model.SimBaseRes[model.SimCardStatusItem]
|
||||
err error
|
||||
)
|
||||
for attempt := 0; attempt <= h.networkRetries; attempt++ {
|
||||
output, err = h.client.QuerySimCardStatus(ctx, input)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
result.ErrorClass = "network"
|
||||
result.Retryable = true
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
if len(output) == 0 {
|
||||
result.ErrorClass = "protocol"
|
||||
result.Retryable = true
|
||||
result.Message = "chan returned empty status result"
|
||||
return result
|
||||
}
|
||||
item := output[0]
|
||||
if item.SimKey == nil {
|
||||
item.SimKey = &model.SimKey{ChanKey: task.ChanKey, ICCID: task.ICCID, IMSI: task.IMSI, MSISDN: task.MSISDN}
|
||||
}
|
||||
if task.ICCID != "" && item.SimKey.ICCID != task.ICCID {
|
||||
result.ErrorClass = "protocol"
|
||||
result.Retryable = true
|
||||
result.Message = "chan returned mismatched card identifier"
|
||||
return result
|
||||
}
|
||||
result.Code = item.Code
|
||||
result.Message = item.Message
|
||||
result.ErrCode = item.ErrCode
|
||||
if item.Code != 0 {
|
||||
result.ErrorClass = "business"
|
||||
if model.IsTelecomTemplate(task.TempKey) && strings.EqualFold(strings.TrimSpace(item.ErrCode), telecomRetryableCode) {
|
||||
result.ErrorClass = "carrier_retry"
|
||||
result.Retryable = true
|
||||
} else if item.Code == chanGatewayInternalError && strings.TrimSpace(item.ErrCode) == "" {
|
||||
result.ErrorClass = "gateway"
|
||||
result.Retryable = true
|
||||
} else if item.Code == tokenExpiredCode {
|
||||
result.ErrorClass = "token_expired"
|
||||
result.Retryable = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
result.Success = true
|
||||
status := item.ResData.CardStatus
|
||||
result.CardStatus = &status
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Handler) Apply(ctx context.Context, results []zsetconsumer.Result) error {
|
||||
updates := make([]simstatusrepo.Result, 0, len(results))
|
||||
for _, result := range results {
|
||||
if result.Success && result.CardStatus != nil {
|
||||
updates = append(updates, simstatusrepo.Result{CardID: result.Task.CardID, CardStatus: *result.CardStatus})
|
||||
}
|
||||
}
|
||||
return h.repo.ApplyBatch(ctx, updates)
|
||||
}
|
||||
|
||||
// ShouldRetryNextRound is the full-status-sync retry policy. It is passed to
|
||||
// the Run-aware ZSet consumer only; single-card tasks retain lease recovery.
|
||||
func ShouldRetryNextRound(result zsetconsumer.Result) bool {
|
||||
return !result.Success && result.Retryable
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package simstatus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
zsetconsumer "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consumer/zset"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
queue "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset"
|
||||
simstatusrepo "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/simstatus"
|
||||
)
|
||||
|
||||
type fakeStatusClient struct {
|
||||
responses [][]model.SimBaseRes[model.SimCardStatusItem]
|
||||
errors []error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakeStatusClient) QuerySimCardStatus(context.Context, *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error) {
|
||||
index := f.calls
|
||||
f.calls++
|
||||
var err error
|
||||
if index < len(f.errors) {
|
||||
err = f.errors[index]
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if index < len(f.responses) {
|
||||
return f.responses[index], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type fakeStatusRepository struct {
|
||||
results []simstatusrepo.Result
|
||||
}
|
||||
|
||||
func (f *fakeStatusRepository) ApplyBatch(_ context.Context, results []simstatusrepo.Result) error {
|
||||
f.results = append(f.results, results...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHandlerUsesTaskCardWhenResponseOmitsSimKey(t *testing.T) {
|
||||
client := &fakeStatusClient{responses: [][]model.SimBaseRes[model.SimCardStatusItem]{{
|
||||
{Code: 0, ResData: model.SimCardStatusItem{CardStatus: 2}},
|
||||
}}}
|
||||
repo := &fakeStatusRepository{}
|
||||
handler, err := NewHandler(client, repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
task := queue.Task{CardID: 42, ICCID: "8986000000000042", ChanKey: "mobile-a", TempKey: "sim_yd_v5"}
|
||||
result := handler.Query(context.Background(), task)
|
||||
if !result.Success || result.CardStatus == nil || *result.CardStatus != 2 {
|
||||
t.Fatalf("unexpected result: %+v", result)
|
||||
}
|
||||
if err := handler.Apply(context.Background(), []zsetconsumer.Result{result}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(repo.results) != 1 || repo.results[0].CardID != 42 || repo.results[0].CardStatus != 2 {
|
||||
t.Fatalf("unexpected updates: %+v", repo.results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRetriesNetworkErrorsOnlyForConfiguredAttempts(t *testing.T) {
|
||||
client := &fakeStatusClient{
|
||||
errors: []error{errors.New("temporary-1"), errors.New("temporary-2")},
|
||||
responses: [][]model.SimBaseRes[model.SimCardStatusItem]{nil, nil, {
|
||||
{Code: 0, ResData: model.SimCardStatusItem{CardStatus: 4}},
|
||||
}},
|
||||
}
|
||||
handler, err := NewHandler(client, &fakeStatusRepository{}, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := handler.Query(context.Background(), queue.Task{CardID: 1, ICCID: "iccid-1", ChanKey: "mobile-a", TempKey: "sim_yd_v5"})
|
||||
if !result.Success || client.calls != 3 {
|
||||
t.Fatalf("success=%v calls=%d result=%+v", result.Success, client.calls, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerClassifiesRetryableStatusFailures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
item model.SimBaseRes[model.SimCardStatusItem]
|
||||
tempKey string
|
||||
errorClass string
|
||||
retryable bool
|
||||
}{
|
||||
{
|
||||
name: "telecom S00004",
|
||||
item: model.SimBaseRes[model.SimCardStatusItem]{Code: 9999, ErrCode: telecomRetryableCode},
|
||||
tempKey: telecomStatusTemplate,
|
||||
errorClass: "carrier_retry",
|
||||
retryable: true,
|
||||
},
|
||||
{
|
||||
name: "telecom v3 S00004",
|
||||
item: model.SimBaseRes[model.SimCardStatusItem]{Code: 9999, ErrCode: telecomRetryableCode},
|
||||
tempKey: "sim_dx_v3",
|
||||
errorClass: "carrier_retry",
|
||||
retryable: true,
|
||||
},
|
||||
{
|
||||
name: "gateway error without carrier code",
|
||||
item: model.SimBaseRes[model.SimCardStatusItem]{Code: chanGatewayInternalError},
|
||||
tempKey: telecomStatusTemplate,
|
||||
errorClass: "gateway",
|
||||
retryable: true,
|
||||
},
|
||||
{
|
||||
name: "token expired is retryable",
|
||||
item: model.SimBaseRes[model.SimCardStatusItem]{Code: tokenExpiredCode},
|
||||
tempKey: "sim_yd_v5",
|
||||
errorClass: "token_expired",
|
||||
retryable: true,
|
||||
},
|
||||
{
|
||||
name: "known business error remains terminal",
|
||||
item: model.SimBaseRes[model.SimCardStatusItem]{Code: 11011, ErrCode: "11011"},
|
||||
tempKey: telecomStatusTemplate,
|
||||
errorClass: "business",
|
||||
retryable: false,
|
||||
},
|
||||
{
|
||||
name: "non telecom S00004 remains terminal",
|
||||
item: model.SimBaseRes[model.SimCardStatusItem]{Code: 9999, ErrCode: telecomRetryableCode},
|
||||
tempKey: "sim_yd_v5",
|
||||
errorClass: "business",
|
||||
retryable: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := &fakeStatusClient{responses: [][]model.SimBaseRes[model.SimCardStatusItem]{{test.item}}}
|
||||
handler, err := NewHandler(client, &fakeStatusRepository{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := handler.Query(context.Background(), queue.Task{CardID: 1, ICCID: "iccid-1", ChanKey: "telecom-a", TempKey: test.tempKey})
|
||||
if result.ErrorClass != test.errorClass || result.Retryable != test.retryable || result.BlockChannel || result.Success {
|
||||
t.Fatalf("unexpected result: %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRetryNextRoundUsesRetryableFlag(t *testing.T) {
|
||||
if !ShouldRetryNextRound(zsetconsumer.Result{Retryable: true}) {
|
||||
t.Fatal("retryable status failure should advance to the next round")
|
||||
}
|
||||
if ShouldRetryNextRound(zsetconsumer.Result{Success: true, Retryable: true}) || ShouldRetryNextRound(zsetconsumer.Result{}) {
|
||||
t.Fatal("successful and terminal status results must not advance rounds")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user