sync: 同步 v1.0.4 开源代码并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 14:59:32 +08:00
parent 14f4bb64e1
commit 3e5a7453b8
74 changed files with 3297 additions and 248 deletions
@@ -0,0 +1,255 @@
package upstreamimpl
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
)
// 真实上游时延测量(默认跳过)。
//
// 目的:实测移动 OneLink「批量提交返回 jobId」与「按 jobId 查到终态」各自的耗时,
// 用于校准平台超时与补查节奏。
//
// 仅在 APN_REAL_CARRIER_TEST=1 时执行。
//
// APN_REAL_CREDS_FILE=<path> 两行:appid / appsecret
// APN_REAL_BASE_URL=<url> 默认 https://api.iot.10086.cn/v5
// APN_REAL_APN=CMIOT
// APN_REAL_BATCHES=<b1>|<b2>|... 每批:operType,iccid:msisdn,iccid:msisdn,...
// APN_REAL_POLL_INTERVAL=15s 查询间隔
// APN_REAL_MAX_WAIT=15m 最长等待终态
func TestRealCarrierApnLatencyMeasure(t *testing.T) {
if strings.TrimSpace(os.Getenv("APN_REAL_CARRIER_TEST")) != "1" {
t.Skip("真实上游测量未启用:设置 APN_REAL_CARRIER_TEST=1 才执行")
}
appID, appSecret, err := loadRealCarrierCreds()
if err != nil {
t.Fatalf("读取凭证失败:%v", err)
}
baseURL := strings.TrimSpace(os.Getenv("APN_REAL_BASE_URL"))
if baseURL == "" {
baseURL = "https://api.iot.10086.cn/v5"
}
apn := strings.TrimSpace(os.Getenv("APN_REAL_APN"))
if apn == "" {
apn = "CMIOT"
}
batches, operTypes, err := parseRealBatches(os.Getenv("APN_REAL_BATCHES"))
if err != nil {
t.Fatalf("解析批次失败:%v", err)
}
pollInterval := 15 * time.Second
if raw := strings.TrimSpace(os.Getenv("APN_REAL_POLL_INTERVAL")); raw != "" {
if d, derr := time.ParseDuration(raw); derr == nil && d > 0 {
pollInterval = d
}
}
maxWait := 15 * time.Minute
if raw := strings.TrimSpace(os.Getenv("APN_REAL_MAX_WAIT")); raw != "" {
if d, derr := time.ParseDuration(raw); derr == nil && d > 0 {
maxWait = d
}
}
channel := &sSimYD{
channelConfig: &model.SimChannelConfigItemCache{Channel: &entity.TSimChannels{ChanKey: "real-carrier-measure", AppKey: appID}},
tokenProvider: &realCarrierToken{appID: appID, appSecret: appSecret, baseURL: baseURL},
apiConfig: &oneLinkAPIConfig{BaseURL: baseURL, Timeout: 60 * time.Second},
}
type batchOutcome struct {
index int
operType string
cards int
elapsed time.Duration
jobID string
failed string
}
start := time.Now()
ctx := context.Background()
results := make([]batchOutcome, len(batches))
var wg sync.WaitGroup
for i := range batches {
wg.Add(1)
go func(i int) {
defer wg.Done()
outcome := batchOutcome{index: i, operType: operTypes[i], cards: len(batches[i])}
begin := time.Now()
items, callErr := changeSimApnFunctionYD(ctx, channel, &model.SimCardOperateChangeApnInput{
SimKeys: batches[i],
ReqData: model.SimCardOperateChangeApnInputItem{
OperType: operTypes[i],
ApnName: apn,
SubmitOnly: true,
OperationID: fmt.Sprintf("measure-%d-%d", i, begin.UnixNano()),
},
})
outcome.elapsed = time.Since(begin)
switch {
case callErr != nil:
outcome.failed = callErr.Error()
case len(items) == 0:
outcome.failed = "空返回"
default:
outcome.jobID = items[0].ResData.OrderNum
for _, item := range items {
if item == nil {
continue
}
if item.ResData.OrderNum != "" && item.ResData.OrderNum != outcome.jobID {
outcome.failed = "同批出现多个jobId"
}
if !item.ResData.Pending && !item.ResData.Uncertain {
outcome.failed = fmt.Sprintf("提交即终态 code=%d msg=%s", item.Code, item.Message)
}
}
}
results[i] = outcome
}(i)
}
wg.Wait()
submitWall := time.Since(start)
fmt.Printf("MEASURE_SUBMIT batches=%d cards=%d wall=%s\n", len(batches), totalRealCards(batches), submitWall)
for _, r := range results {
fmt.Printf("MEASURE_SUBMIT_BATCH idx=%d operType=%s cards=%d elapsed=%s jobId=%s failed=%s\n",
r.index, r.operType, r.cards, r.elapsed, r.jobID, r.failed)
}
// 汇总待查任务
pendingJobs := map[string][]*model.SimKey{}
for _, r := range results {
if r.jobID != "" {
pendingJobs[r.jobID] = batches[r.index]
}
}
terminalAt := map[string]time.Duration{}
fmt.Printf("MEASURE_JOBS distinct=%d %v\n", len(pendingJobs), sortedJobIDs(pendingJobs))
deadline := start.Add(maxWait)
for len(terminalAt) < len(pendingJobs) && time.Now().Before(deadline) {
time.Sleep(pollInterval)
elapsed := time.Since(start)
for jobID, keys := range pendingJobs {
if _, done := terminalAt[jobID]; done {
continue
}
items, err := changeSimApnFunctionYD(ctx, channel, &model.SimCardOperateChangeApnInput{
SimKeys: keys,
ReqData: model.SimCardOperateChangeApnInputItem{QueryJobID: jobID},
})
if err != nil {
fmt.Printf("MEASURE_QUERY_ERR jobId=%s at=%s err=%v\n", jobID, elapsed.Round(time.Second), err)
continue
}
allTerminal := len(items) > 0
for _, item := range items {
if item == nil || item.ResData.Pending {
allTerminal = false
break
}
}
if allTerminal {
terminalAt[jobID] = elapsed
fmt.Printf("MEASURE_TERMINAL jobId=%s at=%s\n", jobID, elapsed.Round(time.Second))
for _, item := range items {
fmt.Printf("MEASURE_CARD jobId=%s iccid=%s msisdn=%s code=%d uncertain=%v message=%s\n",
jobID, item.SimKey.ICCID, item.SimKey.MSISDN, item.Code, item.ResData.Uncertain, item.Message)
}
} else {
fmt.Printf("MEASURE_PENDING jobId=%s at=%s note=%s\n", jobID, elapsed.Round(time.Second), firstMessage(items))
}
}
}
fmt.Printf("MEASURE_SUMMARY\n")
for _, jobID := range sortedJobIDs(pendingJobs) {
if at, ok := terminalAt[jobID]; ok {
fmt.Printf("MEASURE_RESULT jobId=%s terminalAt=%s\n", jobID, at.Round(time.Second))
} else {
fmt.Printf("MEASURE_RESULT jobId=%s terminalAt=NONE within %s\n", jobID, maxWait)
}
}
}
func totalRealCards(batches [][]*model.SimKey) int {
total := 0
for _, b := range batches {
total += len(b)
}
return total
}
func sortedJobIDs(jobs map[string][]*model.SimKey) []string {
ids := make([]string, 0, len(jobs))
for id := range jobs {
ids = append(ids, id)
}
sort.Strings(ids)
return ids
}
func firstMessage(items model.SimCardOperateChangeApnOutput) string {
for _, item := range items {
if item != nil {
return item.Message
}
}
return ""
}
// parseRealBatches 解析 APN_REAL_BATCHES:批之间用 | 分隔,每批首项为 operType,其余为 iccid:msisdn。
func parseRealBatches(raw string) ([][]*model.SimKey, []string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil, fmt.Errorf("未设置 APN_REAL_BATCHES")
}
batches := make([][]*model.SimKey, 0)
operTypes := make([]string, 0)
for _, batchRaw := range strings.Split(raw, "|") {
batchRaw = strings.TrimSpace(batchRaw)
if batchRaw == "" {
continue
}
fields := strings.Split(batchRaw, ",")
operType := strings.TrimSpace(fields[0])
if _, err := strconv.Atoi(operType); err != nil {
return nil, nil, fmt.Errorf("批次首项应为 operType:%q", batchRaw)
}
keys := make([]*model.SimKey, 0, len(fields)-1)
for _, card := range fields[1:] {
card = strings.TrimSpace(card)
if card == "" {
continue
}
parts := strings.Split(card, ":")
if len(parts) < 2 {
return nil, nil, fmt.Errorf("卡项格式应为 iccid:msisdn:%q", card)
}
keys = append(keys, &model.SimKey{
ChanKey: "real-carrier-measure",
ICCID: strings.TrimSpace(parts[0]),
MSISDN: strings.TrimSpace(parts[1]),
})
}
if len(keys) == 0 {
continue
}
batches = append(batches, keys)
operTypes = append(operTypes, operType)
}
if len(batches) == 0 {
return nil, nil, fmt.Errorf("批次为空")
}
return batches, operTypes, nil
}