chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,880 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
qianxunTempKey = "sim_qianxun_v3"
|
||||
qianxunSuccessCode = "000000"
|
||||
qianxunTimeLayout = "20060102150405"
|
||||
qianxunBatchLimit = 200
|
||||
)
|
||||
|
||||
var qianxunEast8 = time.FixedZone("UTC+8", 8*60*60)
|
||||
|
||||
type sSimQianxun struct {
|
||||
channelConfig *model.SimChannelConfigItemCache
|
||||
baseURL string
|
||||
appKey string
|
||||
appSecret string
|
||||
userName string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
var _ model.SimCardChannelI = (*sSimQianxun)(nil)
|
||||
var _ model.SimCardBatchQueryI = (*sSimQianxun)(nil)
|
||||
|
||||
type qianxunResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
type qianxunCardInfo struct {
|
||||
ICCID string `json:"iccid"`
|
||||
IMSI string `json:"imsi"`
|
||||
IMEI string `json:"imei"`
|
||||
MSISDN string `json:"msisdn"`
|
||||
AccessNumber string `json:"accessNumber"`
|
||||
CardNo string `json:"cardNo"`
|
||||
CardStatus any `json:"cardStatus"`
|
||||
Status any `json:"status"`
|
||||
SimStatus any `json:"simStatus"`
|
||||
RealStatus any `json:"realStatus"`
|
||||
StatusName string `json:"statusName"`
|
||||
OpenDate string `json:"openDate"`
|
||||
OpenTime string `json:"openTime"`
|
||||
ActiveDate string `json:"activeDate"`
|
||||
ActiveTime string `json:"activeTime"`
|
||||
ActivateTime string `json:"activateTime"`
|
||||
PackageName string `json:"packageName"`
|
||||
MainOfferName string `json:"mainOfferName"`
|
||||
ProdName string `json:"prodName"`
|
||||
Remark string `json:"remark"`
|
||||
UsageTotal any `json:"usageTotal"`
|
||||
UsedGprs any `json:"usedGprs"`
|
||||
UsedFlow any `json:"usedFlow"`
|
||||
UseFlow any `json:"useFlow"`
|
||||
CardFlow any `json:"cardFlow"`
|
||||
Flow any `json:"flow"`
|
||||
TotalFlow any `json:"totalFlow"`
|
||||
ProdTotal any `json:"prodTotal"`
|
||||
PackageFlow any `json:"packageFlow"`
|
||||
LeftFlow any `json:"leftFlow"`
|
||||
LastChangeDate string `json:"lastChangeDate"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
type qianxunFlowHistory struct {
|
||||
ICCID string `json:"iccid"`
|
||||
BillingMonth string `json:"billingMonth"`
|
||||
UsageTotal any `json:"usageTotal"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type qianxunBatchCardResult struct {
|
||||
ICCID string `json:"iccid"`
|
||||
ICCIDData json.RawMessage `json:"iccidData"`
|
||||
ICCIDResult json.RawMessage `json:"iccidResult"`
|
||||
}
|
||||
|
||||
func NewSimQianxun() *sSimQianxun {
|
||||
return &sSimQianxun{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.UpstreamAPI().RegisterTemp(qianxunTempKey, NewSimQianxun())
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) NewTemp(ctx context.Context, channelConfig *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
baseURL := ""
|
||||
if channelConfig != nil && channelConfig.Template != nil {
|
||||
var templateConfig struct {
|
||||
API struct {
|
||||
BaseURL string `json:"baseUrl"`
|
||||
} `json:"api"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(channelConfig.Template.TempConfig), &templateConfig); err != nil {
|
||||
g.Log().Warningf(ctx, "parse Qianxun template config failed: %v", err)
|
||||
} else {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(templateConfig.API.BaseURL), "/")
|
||||
}
|
||||
}
|
||||
|
||||
var channelExtra struct {
|
||||
UserName string `json:"userName"`
|
||||
}
|
||||
if channelConfig != nil && channelConfig.Channel != nil {
|
||||
if err := json.Unmarshal([]byte(channelConfig.Channel.ChanConfig), &channelExtra); err != nil {
|
||||
g.Log().Warningf(ctx, "parse Qianxun channel config failed, chanKey=%s err=%v", channelConfig.Channel.ChanKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
appKey := ""
|
||||
appSecret := ""
|
||||
if channelConfig != nil && channelConfig.Channel != nil {
|
||||
appKey = strings.TrimSpace(channelConfig.Channel.AppKey)
|
||||
appSecret = strings.TrimSpace(channelConfig.Channel.AppSecret)
|
||||
}
|
||||
|
||||
return &sSimQianxun{
|
||||
channelConfig: channelConfig,
|
||||
baseURL: baseURL,
|
||||
appKey: appKey,
|
||||
appSecret: appSecret,
|
||||
userName: strings.TrimSpace(channelExtra.UserName),
|
||||
http: &http.Client{Timeout: resolveUpstreamHTTPTimeout(ctx, templateConfigJSON(channelConfig))},
|
||||
}
|
||||
}
|
||||
|
||||
func qianxunSign(appKey, appSecret, userName, timestamp string) string {
|
||||
plain := fmt.Sprintf("appKey=%s&timeStamp=%s&userName=%s&appSecret=%s", appKey, timestamp, userName, appSecret)
|
||||
sum := md5.Sum([]byte(plain))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) authParams() (url.Values, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("Qianxun client is nil")
|
||||
}
|
||||
if strings.TrimSpace(s.baseURL) == "" {
|
||||
return nil, fmt.Errorf("Qianxun baseUrl is not configured")
|
||||
}
|
||||
if strings.TrimSpace(s.appKey) == "" || strings.TrimSpace(s.appSecret) == "" || strings.TrimSpace(s.userName) == "" {
|
||||
return nil, fmt.Errorf("Qianxun credentials are not configured")
|
||||
}
|
||||
timestamp := time.Now().In(qianxunEast8).Format(qianxunTimeLayout)
|
||||
params := url.Values{}
|
||||
params.Set("appKey", s.appKey)
|
||||
params.Set("timeStamp", timestamp)
|
||||
params.Set("userName", s.userName)
|
||||
params.Set("sign", qianxunSign(s.appKey, s.appSecret, s.userName, timestamp))
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) post(ctx context.Context, endpoint string, params url.Values) (qianxunResponse, error) {
|
||||
authParams, err := s.authParams()
|
||||
if err != nil {
|
||||
return qianxunResponse{}, err
|
||||
}
|
||||
for key, values := range params {
|
||||
for _, value := range values {
|
||||
authParams.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
target := strings.TrimRight(s.baseURL, "/") + "/" + strings.TrimLeft(endpoint, "/")
|
||||
body := authParams.Encode()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewBufferString(body))
|
||||
if err != nil {
|
||||
return qianxunResponse{}, fmt.Errorf("create Qianxun request %s: %w", endpoint, err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := s.http
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: defaultUpstreamHTTPTimeout}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return qianxunResponse{}, fmt.Errorf("Qianxun request %s: %w", endpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return qianxunResponse{}, fmt.Errorf("read Qianxun response %s: %w", endpoint, err)
|
||||
}
|
||||
rawResponse := strings.TrimSpace(string(responseBody))
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
g.Log().Warningf(ctx, "Qianxun HTTP error endpoint=%s status=%d response=%s", endpoint, resp.StatusCode, rawResponse)
|
||||
return qianxunResponse{}, fmt.Errorf("Qianxun HTTP status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var output qianxunResponse
|
||||
if err := json.Unmarshal(responseBody, &output); err != nil {
|
||||
g.Log().Warningf(ctx, "decode Qianxun response failed endpoint=%s response=%s err=%v", endpoint, rawResponse, err)
|
||||
return qianxunResponse{}, fmt.Errorf("decode Qianxun response %s: %w", endpoint, err)
|
||||
}
|
||||
if !qianxunSuccess(output) {
|
||||
g.Log().Warningf(ctx, "Qianxun business error endpoint=%s status=%s message=%s response=%s", endpoint, output.Status, output.Message, rawResponse)
|
||||
} else {
|
||||
g.Log().Infof(ctx, "Qianxun response endpoint=%s status=%s response=%s", endpoint, output.Status, rawResponse)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func qianxunSuccess(resp qianxunResponse) bool {
|
||||
return strings.TrimSpace(resp.Status) == qianxunSuccessCode
|
||||
}
|
||||
|
||||
func qianxunBusinessError[T any](simKey *model.SimKey, resp qianxunResponse) *model.SimBaseRes[T] {
|
||||
return &model.SimBaseRes[T]{
|
||||
SimKey: simKey,
|
||||
Code: gconv.Int(resp.Status),
|
||||
ErrCode: resp.Status,
|
||||
Message: resp.Message,
|
||||
}
|
||||
}
|
||||
|
||||
func qianxunError[T any](simKey *model.SimKey, err error) *model.SimBaseRes[T] {
|
||||
return &model.SimBaseRes[T]{SimKey: simKey, Code: consts.ApiStatusError, Message: err.Error()}
|
||||
}
|
||||
|
||||
func qianxunUnsupported[T any](simKeys []*model.SimKey, message string) []*model.SimBaseRes[T] {
|
||||
items := make([]*model.SimBaseRes[T], 0, len(simKeys))
|
||||
for _, simKey := range simKeys {
|
||||
items = append(items, &model.SimBaseRes[T]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiInvokeException,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func qianxunICCID(simKey *model.SimKey) (string, error) {
|
||||
if simKey == nil || strings.TrimSpace(simKey.ICCID) == "" {
|
||||
return "", fmt.Errorf("ICCID not found")
|
||||
}
|
||||
return strings.TrimSpace(simKey.ICCID), nil
|
||||
}
|
||||
|
||||
func decodeQianxunData[T any](raw json.RawMessage, target *T) error {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return fmt.Errorf("Qianxun response result is empty")
|
||||
}
|
||||
if err := json.Unmarshal(raw, target); err != nil {
|
||||
return fmt.Errorf("decode Qianxun response result: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeQianxunStringJSON(raw json.RawMessage, target any) error {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return fmt.Errorf("Qianxun batch item result is empty")
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err == nil {
|
||||
text = strings.TrimSpace(text)
|
||||
if !strings.HasPrefix(text, "{") && !strings.HasPrefix(text, "[") {
|
||||
return errors.New(text)
|
||||
}
|
||||
return json.Unmarshal([]byte(text), target)
|
||||
}
|
||||
return json.Unmarshal(raw, target)
|
||||
}
|
||||
|
||||
func qianxunCardStatus(ctx context.Context, value any) int {
|
||||
status := strings.TrimSpace(gconv.String(value))
|
||||
switch status {
|
||||
case "":
|
||||
return 0
|
||||
case "0", "10":
|
||||
return 2
|
||||
case "1":
|
||||
return 1
|
||||
case "3", "4", "8":
|
||||
return 4
|
||||
case "6":
|
||||
return 6
|
||||
case "7":
|
||||
return 7
|
||||
case "9":
|
||||
return 9
|
||||
default:
|
||||
g.Log().Warningf(ctx, "Unknown Qianxun card status %s from upstream", status)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func qianxunUsageMB(values ...any) float64 {
|
||||
for _, value := range values {
|
||||
text := strings.TrimSpace(gconv.String(value))
|
||||
if text != "" && text != "<nil>" {
|
||||
return parseDataUsage(text)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func qianxunCardMessage(resp qianxunResponse) string {
|
||||
return firstNonEmpty(resp.Message, "成功")
|
||||
}
|
||||
|
||||
func qianxunCardToBasic(data qianxunCardInfo) model.SimBasicInfoItem {
|
||||
status := strconv.Itoa(qianxunCardStatus(context.Background(), qianxunStatusValue(data)))
|
||||
return model.SimBasicInfoItem{
|
||||
ICCID: data.ICCID,
|
||||
IMSI: data.IMSI,
|
||||
MSISDN: firstNonEmpty(data.MSISDN, data.AccessNumber, data.CardNo),
|
||||
ActiveDate: firstNonEmpty(data.ActiveDate, data.ActiveTime, data.ActivateTime),
|
||||
OpenDate: firstNonEmpty(data.OpenDate, data.OpenTime),
|
||||
Remark: data.Remark,
|
||||
SimStatus: status,
|
||||
MainOfferName: firstNonEmpty(data.MainOfferName, data.PackageName, data.ProdName),
|
||||
}
|
||||
}
|
||||
|
||||
func qianxunStatusValue(data qianxunCardInfo) string {
|
||||
return firstNonEmpty(gconv.String(data.SimStatus), gconv.String(data.CardStatus), gconv.String(data.Status), gconv.String(data.RealStatus))
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) queryCardInfo(ctx context.Context, simKey *model.SimKey) (qianxunCardInfo, qianxunResponse, error) {
|
||||
iccid, err := qianxunICCID(simKey)
|
||||
if err != nil {
|
||||
return qianxunCardInfo{}, qianxunResponse{}, err
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("iccid", iccid)
|
||||
resp, err := s.post(ctx, "/api/v3/cardInfo", params)
|
||||
if err != nil || !qianxunSuccess(resp) {
|
||||
return qianxunCardInfo{}, resp, err
|
||||
}
|
||||
var data qianxunCardInfo
|
||||
if err := decodeQianxunData(resp.Result, &data); err != nil {
|
||||
return qianxunCardInfo{}, resp, err
|
||||
}
|
||||
return data, resp, nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimBasicInfoOutput, 0), nil
|
||||
}
|
||||
items := make(model.SimBasicInfoOutput, 0, len(input.SimKeys))
|
||||
for _, simKey := range input.SimKeys {
|
||||
data, resp, err := s.queryCardInfo(ctx, simKey)
|
||||
if err != nil {
|
||||
items = append(items, qianxunError[model.SimBasicInfoItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
if !qianxunSuccess(resp) {
|
||||
items = append(items, qianxunBusinessError[model.SimBasicInfoItem](simKey, resp))
|
||||
continue
|
||||
}
|
||||
items = append(items, &model.SimBaseRes[model.SimBasicInfoItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
Message: qianxunCardMessage(resp),
|
||||
ResData: qianxunCardToBasic(data),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimCardStatusOutput, 0), nil
|
||||
}
|
||||
items := make(model.SimCardStatusOutput, 0, len(input.SimKeys))
|
||||
for _, simKey := range input.SimKeys {
|
||||
data, resp, err := s.queryCardInfo(ctx, simKey)
|
||||
if err != nil {
|
||||
items = append(items, qianxunError[model.SimCardStatusItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
if !qianxunSuccess(resp) {
|
||||
items = append(items, qianxunBusinessError[model.SimCardStatusItem](simKey, resp))
|
||||
continue
|
||||
}
|
||||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
Message: qianxunCardMessage(resp),
|
||||
ResData: model.SimCardStatusItem{
|
||||
CardStatus: qianxunCardStatus(ctx, qianxunStatusValue(data)),
|
||||
LastChangeDate: firstNonEmpty(data.LastChangeDate, data.UpdateTime),
|
||||
},
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimDataUsageOutput, 0), nil
|
||||
}
|
||||
items := make(model.SimDataUsageOutput, 0, len(input.SimKeys))
|
||||
for _, simKey := range input.SimKeys {
|
||||
data, resp, err := s.queryCardInfo(ctx, simKey)
|
||||
if err != nil {
|
||||
items = append(items, qianxunError[model.SimDataUsageItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
if !qianxunSuccess(resp) {
|
||||
items = append(items, qianxunBusinessError[model.SimDataUsageItem](simKey, resp))
|
||||
continue
|
||||
}
|
||||
item := model.SimDataUsageItem{
|
||||
DataUsage: qianxunUsageMB(data.UsedGprs, data.UsageTotal, data.UsedFlow, data.UseFlow, data.CardFlow, data.Flow),
|
||||
}
|
||||
total := qianxunUsageMB(data.ProdTotal, data.TotalFlow, data.PackageFlow)
|
||||
if total > 0 {
|
||||
item.TotalFlow = &total
|
||||
}
|
||||
items = append(items, &model.SimBaseRes[model.SimDataUsageItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
Message: qianxunCardMessage(resp),
|
||||
ResData: item,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDataUsageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimDataUsageMonthlyOutput, 0), nil
|
||||
}
|
||||
queryMonth, err := qianxunMonth(input.ReqData.QueryDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make(model.SimDataUsageMonthlyOutput, 0, len(input.SimKeys))
|
||||
for _, simKey := range input.SimKeys {
|
||||
iccid, err := qianxunICCID(simKey)
|
||||
if err != nil {
|
||||
items = append(items, qianxunError[model.SimDataUsageMonthlyOutputItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("iccid", iccid)
|
||||
params.Set("billingMonth", queryMonth)
|
||||
resp, err := s.post(ctx, "/api/v3/queryFlowHistory", params)
|
||||
if err != nil {
|
||||
items = append(items, qianxunError[model.SimDataUsageMonthlyOutputItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
if !qianxunSuccess(resp) {
|
||||
items = append(items, qianxunBusinessError[model.SimDataUsageMonthlyOutputItem](simKey, resp))
|
||||
continue
|
||||
}
|
||||
var data qianxunFlowHistory
|
||||
if err := decodeQianxunData(resp.Result, &data); err != nil {
|
||||
items = append(items, qianxunError[model.SimDataUsageMonthlyOutputItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(data.Status), "N") {
|
||||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: "Qianxun monthly usage data not synced",
|
||||
ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: firstNonEmpty(data.BillingMonth, queryMonth)},
|
||||
})
|
||||
continue
|
||||
}
|
||||
items = append(items, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
Message: qianxunCardMessage(resp),
|
||||
ResData: model.SimDataUsageMonthlyOutputItem{
|
||||
DeadLine: firstNonEmpty(data.BillingMonth, queryMonth),
|
||||
DataAmount: qianxunUsageMB(data.UsageTotal),
|
||||
},
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func qianxunMonth(queryDate string) (string, error) {
|
||||
queryDate = strings.TrimSpace(queryDate)
|
||||
if queryDate == "" {
|
||||
return "", fmt.Errorf("queryDate is required")
|
||||
}
|
||||
for _, layout := range []string{"200601", "2006-01", "20060102", "2006-01-02"} {
|
||||
if parsed, err := time.Parse(layout, queryDate); err == nil {
|
||||
return parsed.Format("200601"), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("invalid queryDate: %s", queryDate)
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimCardStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
|
||||
if input == nil || len(input.SimKeys) == 0 {
|
||||
return make(model.SimCardStatusOutput, 0), nil
|
||||
}
|
||||
cards, errs := s.queryCardInfoBatch(ctx, input.SimKeys)
|
||||
items := make(model.SimCardStatusOutput, 0, len(input.SimKeys))
|
||||
for _, simKey := range input.SimKeys {
|
||||
iccid, err := qianxunICCID(simKey)
|
||||
if err != nil {
|
||||
items = append(items, qianxunError[model.SimCardStatusItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
if err = errs[iccid]; err != nil {
|
||||
items = append(items, qianxunError[model.SimCardStatusItem](simKey, err))
|
||||
continue
|
||||
}
|
||||
data, ok := cards[iccid]
|
||||
if !ok {
|
||||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{SimKey: simKey, Code: consts.ApiStatusError, Message: "batch response missing card"})
|
||||
continue
|
||||
}
|
||||
items = append(items, &model.SimBaseRes[model.SimCardStatusItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
ResData: model.SimCardStatusItem{
|
||||
CardStatus: qianxunCardStatus(ctx, qianxunStatusValue(data)),
|
||||
LastChangeDate: firstNonEmpty(data.LastChangeDate, data.UpdateTime),
|
||||
},
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDataUsageMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) {
|
||||
if input == nil || len(input.SimKeys) == 0 {
|
||||
return make(model.SimDataUsageMonthlyOutput, 0), nil
|
||||
}
|
||||
queryMonth, err := qianxunMonth(input.ReqData.QueryDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(model.SimDataUsageMonthlyOutput, len(input.SimKeys))
|
||||
for _, chunk := range qianxunChunkSimKeys(input.SimKeys, qianxunBatchLimit) {
|
||||
iccids := make([]string, 0, len(chunk))
|
||||
for _, item := range chunk {
|
||||
iccid, err := qianxunICCID(item.simKey)
|
||||
if err != nil {
|
||||
results[item.index] = qianxunError[model.SimDataUsageMonthlyOutputItem](item.simKey, err)
|
||||
continue
|
||||
}
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
if len(iccids) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("iccids", strings.Join(iccids, "_"))
|
||||
params.Set("billingMonth", queryMonth)
|
||||
resp, err := s.post(ctx, "/api/v3/batchQueryFlowHistory", params)
|
||||
if err != nil {
|
||||
for _, item := range chunk {
|
||||
if results[item.index] == nil {
|
||||
results[item.index] = qianxunError[model.SimDataUsageMonthlyOutputItem](item.simKey, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !qianxunSuccess(resp) {
|
||||
for _, item := range chunk {
|
||||
if results[item.index] == nil {
|
||||
results[item.index] = qianxunBusinessError[model.SimDataUsageMonthlyOutputItem](item.simKey, resp)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
history, itemErrs, err := decodeQianxunBatchHistory(resp.Result)
|
||||
if err != nil {
|
||||
for _, item := range chunk {
|
||||
if results[item.index] == nil {
|
||||
results[item.index] = qianxunError[model.SimDataUsageMonthlyOutputItem](item.simKey, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, item := range chunk {
|
||||
if results[item.index] != nil {
|
||||
continue
|
||||
}
|
||||
iccid, _ := qianxunICCID(item.simKey)
|
||||
if itemErrs[iccid] != nil {
|
||||
results[item.index] = qianxunError[model.SimDataUsageMonthlyOutputItem](item.simKey, itemErrs[iccid])
|
||||
continue
|
||||
}
|
||||
data, ok := history[iccid]
|
||||
if !ok {
|
||||
results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: item.simKey, Code: consts.ApiStatusError, Message: "batch response missing card", ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: queryMonth}}
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(data.Status), "N") {
|
||||
results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{SimKey: item.simKey, Code: consts.ApiStatusError, Message: "Qianxun monthly usage data not synced", ResData: model.SimDataUsageMonthlyOutputItem{DeadLine: firstNonEmpty(data.BillingMonth, queryMonth)}}
|
||||
continue
|
||||
}
|
||||
results[item.index] = &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||||
SimKey: item.simKey,
|
||||
Code: consts.ApiStatusSuccess,
|
||||
Message: qianxunCardMessage(resp),
|
||||
ResData: model.SimDataUsageMonthlyOutputItem{
|
||||
DeadLine: firstNonEmpty(data.BillingMonth, queryMonth),
|
||||
DataAmount: qianxunUsageMB(data.UsageTotal),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return compactMonthlyBatchResults(results), nil
|
||||
}
|
||||
|
||||
type qianxunBatchKey struct {
|
||||
index int
|
||||
simKey *model.SimKey
|
||||
}
|
||||
|
||||
func qianxunChunkSimKeys(simKeys []*model.SimKey, size int) [][]qianxunBatchKey {
|
||||
if size <= 0 {
|
||||
size = 1
|
||||
}
|
||||
items := make([]qianxunBatchKey, 0, len(simKeys))
|
||||
for i, simKey := range simKeys {
|
||||
items = append(items, qianxunBatchKey{index: i, simKey: simKey})
|
||||
}
|
||||
chunks := make([][]qianxunBatchKey, 0, (len(items)+size-1)/size)
|
||||
for start := 0; start < len(items); start += size {
|
||||
end := start + size
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
chunks = append(chunks, items[start:end])
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) queryCardInfoBatch(ctx context.Context, simKeys []*model.SimKey) (map[string]qianxunCardInfo, map[string]error) {
|
||||
cards := make(map[string]qianxunCardInfo, len(simKeys))
|
||||
errs := make(map[string]error, len(simKeys))
|
||||
for _, chunk := range qianxunChunkSimKeys(simKeys, qianxunBatchLimit) {
|
||||
iccids := make([]string, 0, len(chunk))
|
||||
for _, item := range chunk {
|
||||
iccid, err := qianxunICCID(item.simKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
iccids = append(iccids, iccid)
|
||||
}
|
||||
if len(iccids) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("iccids", strings.Join(iccids, "_"))
|
||||
resp, err := s.post(ctx, "/api/v3/cardInfos", params)
|
||||
if err != nil {
|
||||
for _, iccid := range iccids {
|
||||
errs[iccid] = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !qianxunSuccess(resp) {
|
||||
for _, iccid := range iccids {
|
||||
errs[iccid] = fmt.Errorf("Qianxun batch card info failed: code=%s message=%s", resp.Status, resp.Message)
|
||||
}
|
||||
continue
|
||||
}
|
||||
batchCards, itemErrs, err := decodeQianxunBatchCards(resp.Result)
|
||||
if err != nil {
|
||||
for _, iccid := range iccids {
|
||||
errs[iccid] = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for iccid, data := range batchCards {
|
||||
cards[iccid] = data
|
||||
}
|
||||
for iccid, itemErr := range itemErrs {
|
||||
errs[iccid] = itemErr
|
||||
}
|
||||
}
|
||||
return cards, errs
|
||||
}
|
||||
|
||||
func decodeQianxunBatchCards(raw json.RawMessage) (map[string]qianxunCardInfo, map[string]error, error) {
|
||||
cards := make(map[string]qianxunCardInfo)
|
||||
errs := make(map[string]error)
|
||||
|
||||
var wrapped []qianxunBatchCardResult
|
||||
if err := decodeQianxunData(raw, &wrapped); err == nil && qianxunHasWrappedData(wrapped) {
|
||||
for _, item := range wrapped {
|
||||
iccid := strings.TrimSpace(item.ICCID)
|
||||
var card qianxunCardInfo
|
||||
if err := decodeQianxunStringJSON(firstQianxunRaw(item.ICCIDData, item.ICCIDResult), &card); err != nil {
|
||||
errs[iccid] = err
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(card.ICCID) == "" {
|
||||
card.ICCID = iccid
|
||||
}
|
||||
cards[card.ICCID] = card
|
||||
}
|
||||
return cards, errs, nil
|
||||
}
|
||||
|
||||
var list []qianxunCardInfo
|
||||
if err := decodeQianxunData(raw, &list); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
iccid := strings.TrimSpace(item.ICCID)
|
||||
if iccid == "" {
|
||||
continue
|
||||
}
|
||||
cards[iccid] = item
|
||||
}
|
||||
return cards, errs, nil
|
||||
}
|
||||
|
||||
func decodeQianxunBatchHistory(raw json.RawMessage) (map[string]qianxunFlowHistory, map[string]error, error) {
|
||||
history := make(map[string]qianxunFlowHistory)
|
||||
errs := make(map[string]error)
|
||||
|
||||
var wrapped []qianxunBatchCardResult
|
||||
if err := decodeQianxunData(raw, &wrapped); err == nil && qianxunHasWrappedData(wrapped) {
|
||||
for _, item := range wrapped {
|
||||
iccid := strings.TrimSpace(item.ICCID)
|
||||
var data qianxunFlowHistory
|
||||
if err := decodeQianxunStringJSON(firstQianxunRaw(item.ICCIDResult, item.ICCIDData), &data); err != nil {
|
||||
errs[iccid] = err
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(data.ICCID) == "" {
|
||||
data.ICCID = iccid
|
||||
}
|
||||
history[data.ICCID] = data
|
||||
}
|
||||
return history, errs, nil
|
||||
}
|
||||
|
||||
var list []qianxunFlowHistory
|
||||
if err := decodeQianxunData(raw, &list); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
iccid := strings.TrimSpace(item.ICCID)
|
||||
if iccid == "" {
|
||||
continue
|
||||
}
|
||||
history[iccid] = item
|
||||
}
|
||||
return history, errs, nil
|
||||
}
|
||||
|
||||
func qianxunHasWrappedData(items []qianxunBatchCardResult) bool {
|
||||
for _, item := range items {
|
||||
if len(item.ICCIDData) > 0 && string(item.ICCIDData) != "null" {
|
||||
return true
|
||||
}
|
||||
if len(item.ICCIDResult) > 0 && string(item.ICCIDResult) != "null" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firstQianxunRaw(values ...json.RawMessage) json.RawMessage {
|
||||
for _, value := range values {
|
||||
if len(value) > 0 && string(value) != "null" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimSession(ctx context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimSessionOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimSessionItem](input.SimKeys, "千寻通道暂不支持在线信息查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimStopReason(ctx context.Context, input *model.SimStopReasonInput) (model.SimStopReasonOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimStopReasonOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimStopReasonItem](input.SimKeys, "千寻通道暂不支持停机原因查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimFrozenState(ctx context.Context, input *model.SimFrozenStateInput) (model.SimFrozenStateOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimFrozenStateOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimFrozenStateItem](input.SimKeys, "千寻通道暂不支持冻结状态查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimCardBindStatus(ctx context.Context, input *model.SimCardBindStatusInput) (model.SimCardBindStatusOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimCardBindStatusOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimCardBindStatusItem](input.SimKeys, "千寻通道暂不支持机卡绑定状态查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDeviceStatus(ctx context.Context, input *model.SimDeviceStatusInput) (model.SimDeviceStatusOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimDeviceStatusOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimDeviceStatusItem](input.SimKeys, "千寻通道暂不支持设备状态查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimRegionLimitStatus(ctx context.Context, input *model.SimRegionLimitStatusInput) (model.SimRegionLimitStatusOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimRegionLimitStatusOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimRegionLimitStatusItem](input.SimKeys, "千寻通道暂不支持地域限制状态查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimImei(ctx context.Context, input *model.SimImeiInput) (model.SimImeiOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimImeiOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimImeiItem](input.SimKeys, "千寻通道暂不支持IMEI查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDataDiagnosis(ctx context.Context, input *model.SimDataDiagnosisInput) (model.SimDataDiagnosisOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimDataDiagnosisOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[any](input.SimKeys, "千寻通道暂不支持达量限速档位查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDataUsageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimDataUsageDailyOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimDataUsageDailyOutputItem](input.SimKeys, "千寻通道暂不支持单日流量查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimDataUsageDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) {
|
||||
return s.QuerySimDataUsageDaily(ctx, input)
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) QuerySimApnInfo(ctx context.Context, input *model.SimApnInfoInput) (model.SimApnInfoOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimApnInfoOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimApnInfoItemList](input.SimKeys, "千寻通道暂不支持APN信息查询"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimCardStatusChangeOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimCardStatusChangeItem](input.SimKeys, "千寻通道暂不支持卡状态变更"), nil
|
||||
}
|
||||
|
||||
func (s *sSimQianxun) ChangeSimApnFunction(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimCardOperateChangeApnOutput, error) {
|
||||
if input == nil {
|
||||
return make(model.SimCardOperateChangeApnOutput, 0), nil
|
||||
}
|
||||
return qianxunUnsupported[model.SimCardOperateChangeApnItem](input.SimKeys, "千寻通道暂不支持APN功能开停"), nil
|
||||
}
|
||||
Reference in New Issue
Block a user