sync: 同步 v1.0.4 开源代码并脱敏
This commit is contained in:
@@ -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", " ")
|
||||
}
|
||||
Reference in New Issue
Block a user