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 != "" { 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 }