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,269 @@
package upstreamimpl
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"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"
"github.com/gogf/gf/v2/frame/g"
)
// 真实移动 OneLink 上游功能验证(默认跳过)。
//
// 仅在显式设置 APN_REAL_CARRIER_TEST=1 时执行,并且只做本文件声明的动作,
// 用于小范围真实卡的功能验证。凭证从文件读取(APN_REAL_CREDS_FILE),
// 不出现在命令行或代码里。
//
// 环境变量:
//
// 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_CARDS=<iccid:msisdn:imsi;...> 卡清单
// APN_REAL_ACTION=apninfo|submit|query
// APN_REAL_OPERTYPE=0|1 submit 时使用:0=开 1=停
// APN_REAL_APN=CMIOT submit 时使用
// APN_REAL_JOBID=<jobId> query 时使用
func TestRealCarrierApnFunctional(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"
}
keys, err := loadRealCarrierKeys()
if err != nil {
t.Fatalf("解析卡清单失败:%v", err)
}
channel := &sSimYD{
channelConfig: &model.SimChannelConfigItemCache{Channel: &entity.TSimChannels{ChanKey: "real-carrier-verify", AppKey: appID}},
tokenProvider: &realCarrierToken{appID: appID, appSecret: appSecret, baseURL: baseURL},
apiConfig: &oneLinkAPIConfig{BaseURL: baseURL, Timeout: 30 * time.Second},
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
switch strings.TrimSpace(os.Getenv("APN_REAL_ACTION")) {
case "apninfo":
apnInfoProbe(t, ctx, channel, keys)
case "submit":
submitProbe(t, ctx, channel, keys)
case "query":
queryProbe(t, ctx, channel, keys)
default:
t.Fatalf("APN_REAL_ACTION 必须为 apninfo|submit|query")
}
}
// apninfoProbe 只读:查询每张卡当前已开通 APN 列表及状态。
func apnInfoProbe(t *testing.T, ctx context.Context, channel *sSimYD, keys []*model.SimKey) {
for _, key := range keys {
params := g.Map{"transid": channel.getTransId(ctx), "iccid": key.ICCID}
resp, err := channel.requestYd(ctx, "QuerySimApnInfo", channel.getUrl(ctx, "ec/query/apn-info"), params)
if err != nil {
t.Errorf("apn-info %s 请求失败:%v", key.ICCID, err)
continue
}
body := resp.ReadAllString()
fmt.Printf("APNINFO iccid=%s msisdn=%s body=%s\n", key.ICCID, key.MSISDN, oneLine(body))
}
}
// submitProbe 真实提交一次批量开/停,只取受理结果(SubmitOnly),不轮询。
func submitProbe(t *testing.T, ctx context.Context, channel *sSimYD, keys []*model.SimKey) {
operType := strings.TrimSpace(os.Getenv("APN_REAL_OPERTYPE"))
if operType == "" {
t.Fatalf("submit 必须指定 APN_REAL_OPERTYPE(0开/1停)")
}
apn := strings.TrimSpace(os.Getenv("APN_REAL_APN"))
if apn == "" {
apn = "CMIOT"
}
checkpointed := make(model.SimCardOperateChangeApnOutput, 0)
ctx = context.WithValue(ctx, model.ApnOperationCheckpointKey{}, func(partial model.SimCardOperateChangeApnOutput) error {
checkpointed = append(checkpointed, partial...)
return nil
})
items, err := changeSimApnFunctionYD(ctx, channel, &model.SimCardOperateChangeApnInput{
SimKeys: keys,
ReqData: model.SimCardOperateChangeApnInputItem{OperType: operType, ApnName: apn, SubmitOnly: true, OperationID: "real-verify-" + fmt.Sprint(time.Now().Unix())},
})
if err != nil {
t.Fatalf("提交失败:%v", err)
}
fmt.Printf("SUBMIT operType=%s apn=%s cards=%d checkpointed=%d\n", operType, apn, len(items), len(checkpointed))
jobIDs := map[string]int{}
for _, item := range items {
if item == nil || item.SimKey == nil {
t.Errorf("返回项缺少卡关联")
continue
}
jobIDs[item.ResData.OrderNum]++
fmt.Printf("SUBMIT_ITEM iccid=%s code=%d pending=%v uncertain=%v jobId=%s message=%s\n",
item.SimKey.ICCID, item.Code, item.ResData.Pending, item.ResData.Uncertain, item.ResData.OrderNum, item.Message)
}
fmt.Printf("SUBMIT_JOBS %v\n", jobIDs)
}
// queryProbe 真实查询一次批量任务结果(单次,不轮询)。
func queryProbe(t *testing.T, ctx context.Context, channel *sSimYD, keys []*model.SimKey) {
jobID := strings.TrimSpace(os.Getenv("APN_REAL_JOBID"))
if jobID == "" {
t.Fatalf("query 必须指定 APN_REAL_JOBID")
}
items, err := changeSimApnFunctionYD(ctx, channel, &model.SimCardOperateChangeApnInput{
SimKeys: keys,
ReqData: model.SimCardOperateChangeApnInputItem{QueryJobID: jobID},
})
if err != nil {
t.Fatalf("查询失败:%v", err)
}
fmt.Printf("QUERY jobId=%s cards=%d\n", jobID, len(items))
for _, item := range items {
if item == nil || item.SimKey == nil {
t.Errorf("返回项缺少卡关联")
continue
}
fmt.Printf("QUERY_ITEM iccid=%s msisdn=%s code=%d pending=%v uncertain=%v jobId=%s message=%s\n",
item.SimKey.ICCID, item.SimKey.MSISDN, item.Code, item.ResData.Pending, item.ResData.Uncertain, item.ResData.OrderNum, item.Message)
}
}
// realCarrierToken 直接从 OneLink 换 token,不落库、不依赖数据库。
type realCarrierToken struct {
appID string
appSecret string
baseURL string
mu sync.Mutex
token string
expireAt time.Time
seq int64
}
func (p *realCarrierToken) GetToken(ctx context.Context) (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.token != "" && time.Now().Before(p.expireAt.Add(-2*time.Minute)) {
return p.token, nil
}
return p.fetch(ctx)
}
func (p *realCarrierToken) RecoverToken(ctx context.Context, rejected string) (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.token != "" && p.token != rejected {
return p.token, nil
}
p.token = ""
return p.fetch(ctx)
}
func (p *realCarrierToken) fetch(ctx context.Context) (string, error) {
p.seq++
transid := p.appID + time.Now().Format("20060102150405") + fmt.Sprintf("%08d", p.seq)
endpoint := strings.TrimRight(p.baseURL, "/") + "/ec/get/token?" + url.Values{
"appid": {p.appID},
"password": {p.appSecret},
"transid": {transid},
"refresh": {"0"},
}.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var parsed struct {
Status string `json:"status"`
Message string `json:"message"`
Result []struct {
Token string `json:"token"`
Ttl int `json:"ttl"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return "", err
}
if parsed.Status != "0" || len(parsed.Result) == 0 || parsed.Result[0].Token == "" {
return "", fmt.Errorf("换 token 失败 status=%s message=%s", parsed.Status, parsed.Message)
}
p.token = parsed.Result[0].Token
ttl := parsed.Result[0].Ttl
if ttl <= 0 {
ttl = 3600
}
p.expireAt = time.Now().Add(time.Duration(ttl) * time.Second)
return p.token, nil
}
func loadRealCarrierCreds() (string, string, error) {
path := strings.TrimSpace(os.Getenv("APN_REAL_CREDS_FILE"))
if path == "" {
return "", "", fmt.Errorf("未设置 APN_REAL_CREDS_FILE")
}
raw, err := os.ReadFile(path)
if err != nil {
return "", "", err
}
lines := make([]string, 0, 2)
for _, line := range strings.Split(string(raw), "\n") {
if line = strings.TrimSpace(line); line != "" {
lines = append(lines, line)
}
}
if len(lines) < 2 {
return "", "", fmt.Errorf("凭证文件需要两行:appid / appsecret")
}
return lines[0], lines[1], nil
}
func loadRealCarrierKeys() ([]*model.SimKey, error) {
raw := strings.TrimSpace(os.Getenv("APN_REAL_CARDS"))
if raw == "" {
return nil, fmt.Errorf("未设置 APN_REAL_CARDS")
}
keys := make([]*model.SimKey, 0)
for _, entry := range strings.Split(raw, ";") {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
parts := strings.Split(entry, ":")
if len(parts) < 2 {
return nil, fmt.Errorf("卡项格式应为 iccid:msisdn[:imsi]:%q", entry)
}
key := &model.SimKey{ChanKey: "real-carrier-verify", ICCID: strings.TrimSpace(parts[0]), MSISDN: strings.TrimSpace(parts[1])}
if len(parts) > 2 {
key.IMSI = strings.TrimSpace(parts[2])
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, fmt.Errorf("卡清单为空")
}
return keys, nil
}
func oneLine(body string) string {
return strings.ReplaceAll(strings.ReplaceAll(body, "\n", " "), "\r", " ")
}