544 lines
17 KiB
Go
544 lines
17 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
)
|
|
|
|
type appOneLinkMockConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
BaseURL string `json:"baseUrl"`
|
|
AppID string `json:"appid"`
|
|
Password string `json:"password"`
|
|
Timeout string `json:"timeout"`
|
|
}
|
|
|
|
type appOneLinkMockClient struct {
|
|
cfg *appOneLinkMockConfig
|
|
httpClient *http.Client
|
|
token string
|
|
}
|
|
|
|
type appOneLinkMockTokenResponse struct {
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
Result []struct {
|
|
Token string `json:"token"`
|
|
TTL string `json:"ttl"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type appOneLinkMockChangeStatusResponse struct {
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
Result []struct {
|
|
Msisdn string `json:"msisdn"`
|
|
OrderNum string `json:"orderNum"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type appOneLinkMockBasicInfoResponse struct {
|
|
Status string `json:"status"`
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Result json.RawMessage `json:"result"`
|
|
Data []struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
ResData json.RawMessage `json:"resData"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type appOneLinkMockSimSessionResponse struct {
|
|
Status string `json:"status"`
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Result json.RawMessage `json:"result"`
|
|
Data []struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
ResData appOneLinkMockSimSessionItem `json:"resData"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type appOneLinkMockSimSessionItem struct {
|
|
ApnId string `json:"apnId"`
|
|
Rat string `json:"rat"`
|
|
Imei string `json:"imei"`
|
|
Ipv4 string `json:"ipv4"`
|
|
Ipv6 string `json:"ipv6"`
|
|
Ipv6Prefix string `json:"ipv6Prefix"`
|
|
Status string `json:"status"`
|
|
CreateTime string `json:"createTime"`
|
|
}
|
|
|
|
type appOneLinkMockStatusOutcome = appSimStatusChangeOutcome
|
|
|
|
func newAppOneLinkMockClient(ctx context.Context) (*appOneLinkMockClient, error) {
|
|
cfg, err := appLoadOneLinkMockConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timeout := 10 * time.Second
|
|
if cfg.Timeout != "" {
|
|
if parsed, err := time.ParseDuration(cfg.Timeout); err == nil && parsed > 0 {
|
|
timeout = parsed
|
|
}
|
|
}
|
|
return &appOneLinkMockClient{
|
|
cfg: cfg,
|
|
httpClient: &http.Client{Timeout: timeout},
|
|
}, nil
|
|
}
|
|
|
|
func appLoadOneLinkMockConfig(ctx context.Context) (*appOneLinkMockConfig, error) {
|
|
cfg := &appOneLinkMockConfig{
|
|
BaseURL: "https://example.com/onelink-mock/v5",
|
|
Timeout: "10s",
|
|
}
|
|
if err := g.Cfg().MustGet(ctx, "upstream.onelinkMock").Scan(cfg); err != nil {
|
|
return nil, gerror.Wrap(err, "读取OneLink挡板配置失败")
|
|
}
|
|
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
|
|
cfg.AppID = strings.TrimSpace(cfg.AppID)
|
|
cfg.Password = strings.TrimSpace(cfg.Password)
|
|
cfg.Timeout = strings.TrimSpace(cfg.Timeout)
|
|
if !cfg.Enabled {
|
|
return nil, gerror.New("OneLink挡板未启用")
|
|
}
|
|
if cfg.BaseURL == "" {
|
|
return nil, gerror.New("OneLink挡板地址未配置")
|
|
}
|
|
if cfg.AppID == "" || cfg.Password == "" {
|
|
return nil, gerror.New("OneLink挡板appid或password未配置")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c *appOneLinkMockClient) ChangeSimStatus(ctx context.Context, operation appCardLifecycleOperation, row *appCardLifecycleExecutionRow) (appOneLinkMockStatusOutcome, error) {
|
|
if c == nil || c.cfg == nil {
|
|
return appOneLinkMockStatusOutcome{}, gerror.New("OneLink挡板客户端未初始化")
|
|
}
|
|
if row == nil {
|
|
return appOneLinkMockStatusOutcome{}, gerror.New("卡片操作行不能为空")
|
|
}
|
|
msisdn := strings.TrimSpace(row.Msisdn)
|
|
if msisdn == "" && row.Card != nil {
|
|
msisdn = strings.TrimSpace(row.Card.Msisdn)
|
|
}
|
|
if msisdn == "" {
|
|
return appOneLinkMockStatusOutcome{}, gerror.New("卡片缺少MSISDN,不能请求OneLink挡板")
|
|
}
|
|
operType, err := appOneLinkMockOperType(operation)
|
|
if err != nil {
|
|
return appOneLinkMockStatusOutcome{}, err
|
|
}
|
|
token, err := c.ensureToken(ctx)
|
|
if err != nil {
|
|
return appOneLinkMockStatusOutcome{}, err
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("appid", c.cfg.AppID)
|
|
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
|
params.Set("token", token)
|
|
params.Set("msisdn", msisdn)
|
|
params.Set("operType", operType)
|
|
|
|
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/change/sim-status?"+params.Encode())
|
|
if err != nil {
|
|
return appOneLinkMockStatusOutcome{}, err
|
|
}
|
|
var response appOneLinkMockChangeStatusResponse
|
|
if err = json.Unmarshal(body, &response); err != nil {
|
|
return appOneLinkMockStatusOutcome{}, gerror.Wrap(err, "解析OneLink挡板状态变更响应失败")
|
|
}
|
|
if strings.TrimSpace(response.Status) != "0" {
|
|
message := strings.TrimSpace(response.Message)
|
|
if message == "" {
|
|
message = "OneLink挡板状态变更失败"
|
|
}
|
|
return appOneLinkMockStatusOutcome{Code: -1, Message: message}, nil
|
|
}
|
|
orderNum := ""
|
|
if len(response.Result) > 0 {
|
|
orderNum = strings.TrimSpace(response.Result[0].OrderNum)
|
|
}
|
|
return appOneLinkMockStatusOutcome{
|
|
Success: true,
|
|
Code: 0,
|
|
Message: appOneLinkMockSuccessMessage(response.Message),
|
|
OrderNum: orderNum,
|
|
}, nil
|
|
}
|
|
|
|
func (c *appOneLinkMockClient) QuerySimBasicInfo(ctx context.Context, input appSimBasicInfoInput) (*appSimBasicInfoOutput, error) {
|
|
if c == nil || c.cfg == nil {
|
|
return nil, gerror.New("OneLink鎸℃澘瀹㈡埛绔湭鍒濆鍖?")
|
|
}
|
|
token, err := c.ensureToken(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("appid", c.cfg.AppID)
|
|
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
|
params.Set("token", token)
|
|
appOneLinkMockAddBasicInfoQueryParam(params, input)
|
|
if params.Get("msisdn") == "" && params.Get("iccid") == "" && params.Get("imsi") == "" && params.Get("imei") == "" {
|
|
return nil, gerror.New("鍗$墖缂哄皯鏌ヨ鏍囪瘑锛屼笉鑳借姹侽neLink鎸℃澘")
|
|
}
|
|
|
|
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/query/sim-basic-info?"+params.Encode())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var response appOneLinkMockBasicInfoResponse
|
|
if err = json.Unmarshal(body, &response); err != nil {
|
|
return nil, gerror.Wrap(err, "瑙f瀽OneLink鎸℃澘鍩烘湰淇℃伅鍝嶅簲澶辫触")
|
|
}
|
|
if status := strings.TrimSpace(response.Status); status != "" && status != "0" {
|
|
message := strings.TrimSpace(response.Message)
|
|
if message == "" {
|
|
message = "OneLink鎸℃澘鍩烘湰淇℃伅鏌ヨ澶辫触"
|
|
}
|
|
return nil, gerror.New(message)
|
|
}
|
|
if response.Code != 0 {
|
|
message := strings.TrimSpace(response.Message)
|
|
if message == "" {
|
|
message = "OneLink鎸℃澘鍩烘湰淇℃伅鏌ヨ澶辫触"
|
|
}
|
|
return nil, gerror.New(message)
|
|
}
|
|
|
|
if output := appOneLinkMockParseBasicInfoOutput(response.Result, input); output != nil {
|
|
return output, nil
|
|
}
|
|
for _, item := range response.Data {
|
|
if item.Code != 0 {
|
|
continue
|
|
}
|
|
if output := appOneLinkMockParseBasicInfoOutput(item.ResData, input); output != nil {
|
|
return output, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *appOneLinkMockClient) QuerySimSession(ctx context.Context, card *appCardQueryRow) (*appOneLinkMockSimSessionItem, error) {
|
|
if c == nil || c.cfg == nil {
|
|
return nil, gerror.New("OneLink挡板客户端未初始化")
|
|
}
|
|
if card == nil {
|
|
return nil, gerror.New("卡片信息为空,不能查询会话")
|
|
}
|
|
params := url.Values{}
|
|
params.Set("appid", c.cfg.AppID)
|
|
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
|
if token, err := c.ensureToken(ctx); err != nil {
|
|
return nil, err
|
|
} else {
|
|
params.Set("token", token)
|
|
}
|
|
if value := strings.TrimSpace(card.Msisdn); value != "" {
|
|
params.Set("msisdn", value)
|
|
}
|
|
if value := strings.TrimSpace(card.Iccid); value != "" {
|
|
params.Set("iccid", value)
|
|
}
|
|
if value := strings.TrimSpace(card.Imsi); value != "" {
|
|
params.Set("imsi", value)
|
|
}
|
|
if params.Get("msisdn") == "" && params.Get("iccid") == "" && params.Get("imsi") == "" {
|
|
return nil, gerror.New("卡片缺少查询会话所需标识")
|
|
}
|
|
|
|
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/query/sim-session?"+params.Encode())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var response appOneLinkMockSimSessionResponse
|
|
if err = json.Unmarshal(body, &response); err != nil {
|
|
return nil, gerror.Wrap(err, "解析OneLink挡板会话响应失败")
|
|
}
|
|
if strings.TrimSpace(response.Status) != "" && strings.TrimSpace(response.Status) != "0" {
|
|
message := strings.TrimSpace(response.Message)
|
|
if message == "" {
|
|
message = "OneLink挡板会话查询失败"
|
|
}
|
|
return nil, gerror.New(message)
|
|
}
|
|
if response.Code != 0 {
|
|
message := strings.TrimSpace(response.Message)
|
|
if message == "" {
|
|
message = "OneLink挡板会话查询失败"
|
|
}
|
|
return nil, gerror.New(message)
|
|
}
|
|
for _, item := range appOneLinkMockParseSessionResult(response.Result) {
|
|
if !item.isZero() {
|
|
return &item, nil
|
|
}
|
|
}
|
|
for _, item := range response.Data {
|
|
if item.Code == 0 && !item.ResData.isZero() {
|
|
session := item.ResData
|
|
return &session, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func appOneLinkMockAddBasicInfoQueryParam(params url.Values, input appSimBasicInfoInput) {
|
|
if params == nil {
|
|
return
|
|
}
|
|
if value := strings.TrimSpace(input.Msisdn); value != "" {
|
|
params.Set("msisdn", value)
|
|
}
|
|
if value := strings.TrimSpace(input.Iccid); value != "" {
|
|
params.Set("iccid", value)
|
|
}
|
|
if value := strings.TrimSpace(input.Imsi); value != "" {
|
|
params.Set("imsi", value)
|
|
}
|
|
if value := strings.TrimSpace(input.Imei); value != "" {
|
|
params.Set("imei", value)
|
|
}
|
|
if params.Get("msisdn") == "" && params.Get("iccid") == "" && params.Get("imsi") == "" && params.Get("imei") == "" {
|
|
if value := strings.TrimSpace(input.CardValue); value != "" {
|
|
params.Set("msisdn", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func appOneLinkMockParseBasicInfoOutput(raw json.RawMessage, input appSimBasicInfoInput) *appSimBasicInfoOutput {
|
|
payload := appOneLinkMockFirstPayload(raw)
|
|
if len(payload) == 0 {
|
|
return nil
|
|
}
|
|
output := &appSimBasicInfoOutput{
|
|
Provider: appUpstreamProviderMock,
|
|
Iccid: appFirstNotEmpty(appOneLinkMockMapString(payload, "iccid", "Iccid"), strings.TrimSpace(input.Iccid)),
|
|
Msisdn: appFirstNotEmpty(appOneLinkMockMapString(payload, "msisdn", "Msisdn"), strings.TrimSpace(input.Msisdn), strings.TrimSpace(input.CardValue)),
|
|
Imsi: appFirstNotEmpty(appOneLinkMockMapString(payload, "imsi", "Imsi"), strings.TrimSpace(input.Imsi)),
|
|
Imei: appFirstNotEmpty(appOneLinkMockMapString(payload, "imei", "Imei"), strings.TrimSpace(input.Imei)),
|
|
OpenDate: appOneLinkMockMapString(payload, "openDate", "open_date", "OpenDate"),
|
|
ActiveDate: appOneLinkMockMapString(payload, "activeDate", "active_date", "ActiveDate"),
|
|
Remark: appOneLinkMockMapString(payload, "remark", "Remark"),
|
|
LastChangeDate: appOneLinkMockMapString(payload, "lastChangeDate", "last_change_date", "LastChangeDate"),
|
|
}
|
|
statusText := appOneLinkMockMapString(payload, "cardStatus", "card_status", "status", "Status")
|
|
if statusText != "" {
|
|
if status, err := strconv.Atoi(statusText); err == nil {
|
|
output.CardStatus = status
|
|
}
|
|
if output.CardStatus == 0 {
|
|
switch strings.ToLower(statusText) {
|
|
case "active", "activated", "normal", "ok", "success":
|
|
output.CardStatus = consts.V3ApiSimBossCardStatusActivated
|
|
case "stop", "stopped", "disable", "disabled":
|
|
output.CardStatus = consts.V3ApiSimBossCardStatusStopped
|
|
}
|
|
}
|
|
}
|
|
if output.Iccid == "" && output.Msisdn == "" && output.Imsi == "" && output.Imei == "" && output.OpenDate == "" && output.ActiveDate == "" && output.Remark == "" && output.LastChangeDate == "" && output.CardStatus == 0 {
|
|
return nil
|
|
}
|
|
return output
|
|
}
|
|
|
|
func appOneLinkMockFirstPayload(raw json.RawMessage) map[string]any {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return nil
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(raw, &payload); err == nil && len(payload) > 0 {
|
|
if nested, ok := payload["resData"]; ok && nested != nil {
|
|
if nestedRaw, err := json.Marshal(nested); err == nil {
|
|
if nestedPayload := appOneLinkMockFirstPayload(nestedRaw); len(nestedPayload) > 0 {
|
|
return nestedPayload
|
|
}
|
|
}
|
|
}
|
|
if nested, ok := payload["data"]; ok && nested != nil {
|
|
if nestedRaw, err := json.Marshal(nested); err == nil {
|
|
if nestedPayload := appOneLinkMockFirstPayload(nestedRaw); len(nestedPayload) > 0 {
|
|
return nestedPayload
|
|
}
|
|
}
|
|
}
|
|
return payload
|
|
}
|
|
var payloadList []map[string]any
|
|
if err := json.Unmarshal(raw, &payloadList); err == nil {
|
|
for _, item := range payloadList {
|
|
if len(item) > 0 {
|
|
if nested, ok := item["resData"]; ok && nested != nil {
|
|
if nestedRaw, err := json.Marshal(nested); err == nil {
|
|
if nestedPayload := appOneLinkMockFirstPayload(nestedRaw); len(nestedPayload) > 0 {
|
|
return nestedPayload
|
|
}
|
|
}
|
|
}
|
|
return item
|
|
}
|
|
}
|
|
}
|
|
var payloadWrapper struct {
|
|
ResData json.RawMessage `json:"resData"`
|
|
}
|
|
if err := json.Unmarshal(raw, &payloadWrapper); err == nil && len(payloadWrapper.ResData) > 0 {
|
|
return appOneLinkMockFirstPayload(payloadWrapper.ResData)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func appOneLinkMockMapString(data map[string]any, keys ...string) string {
|
|
if len(data) == 0 {
|
|
return ""
|
|
}
|
|
for _, key := range keys {
|
|
if value, ok := data[key]; ok && value != nil {
|
|
text := strings.TrimSpace(fmt.Sprint(value))
|
|
if text != "" && text != "<nil>" {
|
|
return text
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *appOneLinkMockClient) ensureToken(ctx context.Context) (string, error) {
|
|
if strings.TrimSpace(c.token) != "" {
|
|
return c.token, nil
|
|
}
|
|
params := url.Values{}
|
|
params.Set("appid", c.cfg.AppID)
|
|
params.Set("password", c.cfg.Password)
|
|
params.Set("transid", appOneLinkMockTransID(c.cfg.AppID))
|
|
|
|
body, err := c.get(ctx, c.cfg.BaseURL+"/ec/get/token?"+params.Encode())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var response appOneLinkMockTokenResponse
|
|
if err = json.Unmarshal(body, &response); err != nil {
|
|
return "", gerror.Wrap(err, "解析OneLink挡板token响应失败")
|
|
}
|
|
if strings.TrimSpace(response.Status) != "0" || len(response.Result) == 0 || strings.TrimSpace(response.Result[0].Token) == "" {
|
|
message := strings.TrimSpace(response.Message)
|
|
if message == "" {
|
|
message = "获取OneLink挡板token失败"
|
|
}
|
|
return "", gerror.New(message)
|
|
}
|
|
c.token = strings.TrimSpace(response.Result[0].Token)
|
|
return c.token, nil
|
|
}
|
|
|
|
func (c *appOneLinkMockClient) get(ctx context.Context, requestURL string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "请求OneLink挡板失败")
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "读取OneLink挡板响应失败")
|
|
}
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return nil, gerror.Newf("OneLink挡板HTTP状态异常:%d", resp.StatusCode)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func appOneLinkMockParseSessionResult(raw json.RawMessage) []appOneLinkMockSimSessionItem {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return nil
|
|
}
|
|
var directList []appOneLinkMockSimSessionItem
|
|
if err := json.Unmarshal(raw, &directList); err == nil && len(directList) > 0 {
|
|
return directList
|
|
}
|
|
var direct appOneLinkMockSimSessionItem
|
|
if err := json.Unmarshal(raw, &direct); err == nil && !direct.isZero() {
|
|
return []appOneLinkMockSimSessionItem{direct}
|
|
}
|
|
var wrappedList []struct {
|
|
Code int `json:"code"`
|
|
ResData appOneLinkMockSimSessionItem `json:"resData"`
|
|
}
|
|
if err := json.Unmarshal(raw, &wrappedList); err == nil && len(wrappedList) > 0 {
|
|
items := make([]appOneLinkMockSimSessionItem, 0, len(wrappedList))
|
|
for _, wrapped := range wrappedList {
|
|
if wrapped.Code == 0 && !wrapped.ResData.isZero() {
|
|
items = append(items, wrapped.ResData)
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
var wrapped struct {
|
|
Code int `json:"code"`
|
|
ResData appOneLinkMockSimSessionItem `json:"resData"`
|
|
}
|
|
if err := json.Unmarshal(raw, &wrapped); err == nil && wrapped.Code == 0 && !wrapped.ResData.isZero() {
|
|
return []appOneLinkMockSimSessionItem{wrapped.ResData}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (item appOneLinkMockSimSessionItem) isZero() bool {
|
|
return strings.TrimSpace(item.ApnId) == "" &&
|
|
strings.TrimSpace(item.Rat) == "" &&
|
|
strings.TrimSpace(item.Imei) == "" &&
|
|
strings.TrimSpace(item.Ipv4) == "" &&
|
|
strings.TrimSpace(item.Ipv6) == "" &&
|
|
strings.TrimSpace(item.Ipv6Prefix) == "" &&
|
|
strings.TrimSpace(item.Status) == "" &&
|
|
strings.TrimSpace(item.CreateTime) == ""
|
|
}
|
|
|
|
func appOneLinkMockOperType(operation appCardLifecycleOperation) (string, error) {
|
|
switch operation.Code {
|
|
case "stop":
|
|
return consts.SimCardStatusOperTypeStop, nil
|
|
case "start":
|
|
return consts.SimCardStatusOperTypeStart, nil
|
|
default:
|
|
return "", gerror.New("OneLink挡板暂不支持该卡片操作")
|
|
}
|
|
}
|
|
|
|
func appOneLinkMockTransID(appID string) string {
|
|
random := time.Now().UnixNano() % 100000000
|
|
return fmt.Sprintf("%s%s%08d", strings.TrimSpace(appID), gtime.Now().Format("YmdHis"), random)
|
|
}
|
|
|
|
func appOneLinkMockSuccessMessage(message string) string {
|
|
message = strings.TrimSpace(message)
|
|
if message == "" {
|
|
return "OneLink挡板处理成功"
|
|
}
|
|
return message
|
|
}
|