96b1edff45
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
274 lines
8.4 KiB
Go
274 lines
8.4 KiB
Go
package mingqian
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
)
|
|
|
|
const TemplateKey = "sim_mingqian_v3"
|
|
const statePath = "/simlist/get-state-by-number"
|
|
const usagePath = "/simlist/get-flow-use-by-number"
|
|
const packageUsagePath = "/use/get-card-use-by-number"
|
|
|
|
type Config struct {
|
|
API struct {
|
|
BaseURL string `json:"baseUrl"`
|
|
Timeout string `json:"timeout"`
|
|
ResumePath string `json:"resumePath"`
|
|
} `json:"api"`
|
|
}
|
|
|
|
type Adapter struct {
|
|
client *Client
|
|
resumePath string
|
|
}
|
|
|
|
var _ model.SimCardChannelI = (*Adapter)(nil)
|
|
|
|
func New(cfg *model.SimChannelConfigItemCache) (*Adapter, error) {
|
|
if cfg == nil || cfg.Template == nil || cfg.Channel == nil {
|
|
return nil, gerror.New("mingqian: channel and template required")
|
|
}
|
|
config := Config{}
|
|
config.API.BaseURL, config.API.Timeout = DefaultBaseURL, "10s"
|
|
if strings.TrimSpace(cfg.Template.TempConfig) != "" {
|
|
if err := json.Unmarshal([]byte(cfg.Template.TempConfig), &config); err != nil {
|
|
return nil, gerror.New("mingqian: invalid template JSON")
|
|
}
|
|
}
|
|
timeout, err := time.ParseDuration(config.API.Timeout)
|
|
if err != nil || timeout <= 0 {
|
|
return nil, gerror.New("mingqian: invalid api.timeout")
|
|
}
|
|
resume := strings.TrimSpace(config.API.ResumePath)
|
|
if resume != "" && (!strings.HasPrefix(resume, "/simlist/") || strings.ContainsAny(resume, "?#") || strings.Contains(resume, "..") || resume == "/simlist/get-sim-switchoff-by-number") {
|
|
return nil, gerror.New("mingqian: resumePath must be a confirmed distinct /simlist/ path")
|
|
}
|
|
client, err := NewClient(config.API.BaseURL, cfg.Channel.AppKey, cfg.Channel.AppSecret, timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Adapter{client: client, resumePath: resume}, nil
|
|
}
|
|
|
|
type number float64
|
|
|
|
func (value *number) UnmarshalJSON(raw []byte) error {
|
|
text := strings.TrimSpace(string(raw))
|
|
if text == "null" {
|
|
return gerror.New("mingqian: null numeric field")
|
|
}
|
|
if strings.HasPrefix(text, `"`) {
|
|
if err := json.Unmarshal(raw, &text); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
parsed, err := strconv.ParseFloat(text, 64)
|
|
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) {
|
|
return gerror.New("mingqian: invalid numeric field")
|
|
}
|
|
*value = number(parsed)
|
|
return nil
|
|
}
|
|
|
|
type card struct {
|
|
ICCID string `json:"iccid"`
|
|
MSISDN string `json:"msisdn"`
|
|
State *int `json:"state"`
|
|
StateName string `json:"statename"`
|
|
Usage *number `json:"use"`
|
|
PackageTotal *number `json:"totalpackage"`
|
|
PackageUsage *number `json:"targetpackageusage"`
|
|
Success *int `json:"issuccess"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func identifier(key *model.SimKey) string {
|
|
if key == nil {
|
|
return ""
|
|
}
|
|
if value := strings.TrimSpace(key.ICCID); value != "" {
|
|
return value
|
|
}
|
|
return strings.TrimSpace(key.MSISDN)
|
|
}
|
|
|
|
func query[T any](ctx context.Context, adapter *Adapter, keys []*model.SimKey, path string, limit int, convert func(card) (T, error)) []*model.SimBaseRes[T] {
|
|
output := make([]*model.SimBaseRes[T], len(keys))
|
|
for index, key := range keys {
|
|
output[index] = &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: ICCID or MSISDN required (IMSI-only unsupported)"}
|
|
}
|
|
for start := 0; start < len(keys); start += limit {
|
|
end := start + limit
|
|
if end > len(keys) {
|
|
end = len(keys)
|
|
}
|
|
numbers := make([]string, 0, end-start)
|
|
seen := map[string]bool{}
|
|
for _, key := range keys[start:end] {
|
|
value := identifier(key)
|
|
if value != "" && !seen[value] {
|
|
numbers = append(numbers, value)
|
|
seen[value] = true
|
|
}
|
|
}
|
|
if len(numbers) == 0 {
|
|
continue
|
|
}
|
|
var cards []card
|
|
err := adapter.client.post(ctx, path, map[string]any{"numberArray": numbers}, &cards)
|
|
for index := start; index < end; index++ {
|
|
requested := identifier(keys[index])
|
|
if requested == "" {
|
|
continue
|
|
}
|
|
item := output[index]
|
|
if err != nil {
|
|
item.Message = err.Error()
|
|
continue
|
|
}
|
|
var matched *card
|
|
duplicate := false
|
|
for cardIndex := range cards {
|
|
candidate := &cards[cardIndex]
|
|
matches := false
|
|
if strings.TrimSpace(keys[index].ICCID) != "" {
|
|
matches = strings.TrimSpace(candidate.ICCID) == requested
|
|
} else {
|
|
matches = strings.TrimSpace(candidate.MSISDN) == requested
|
|
}
|
|
if matches {
|
|
if matched != nil {
|
|
duplicate = true
|
|
break
|
|
}
|
|
matched = candidate
|
|
}
|
|
}
|
|
if duplicate {
|
|
item.Message = "mingqian: duplicate card in response"
|
|
continue
|
|
}
|
|
if matched == nil {
|
|
item.Message = "mingqian: requested card missing from response"
|
|
continue
|
|
}
|
|
result, mapErr := convert(*matched)
|
|
if mapErr != nil {
|
|
item.Message = mapErr.Error()
|
|
continue
|
|
}
|
|
item.Code = consts.ApiStatusSuccess
|
|
item.Message = ""
|
|
item.ResData = result
|
|
}
|
|
}
|
|
return output
|
|
}
|
|
|
|
func cardStatus(state *int) (int, error) {
|
|
if state == nil {
|
|
return 0, gerror.New("mingqian: state missing")
|
|
}
|
|
switch *state {
|
|
case 1:
|
|
return 1, nil
|
|
case 2:
|
|
return 2, nil
|
|
case 3:
|
|
return 4, nil
|
|
case 4:
|
|
return 6, nil
|
|
case 5:
|
|
return 7, nil
|
|
case 6:
|
|
return 9, nil
|
|
default:
|
|
return 0, gerror.Newf("mingqian: unmapped card state %d", *state)
|
|
}
|
|
}
|
|
|
|
func (adapter *Adapter) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
|
return query(ctx, adapter, input.SimKeys, statePath, 50, func(item card) (model.SimBasicInfoItem, error) {
|
|
state, err := cardStatus(item.State)
|
|
if err != nil {
|
|
return model.SimBasicInfoItem{}, err
|
|
}
|
|
return model.SimBasicInfoItem{ICCID: item.ICCID, MSISDN: item.MSISDN, SimStatus: strconv.Itoa(state), Remark: item.StateName}, nil
|
|
}), nil
|
|
}
|
|
|
|
func (adapter *Adapter) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) (model.SimCardStatusOutput, error) {
|
|
return query(ctx, adapter, input.SimKeys, statePath, 50, func(item card) (model.SimCardStatusItem, error) {
|
|
state, err := cardStatus(item.State)
|
|
return model.SimCardStatusItem{CardStatus: state}, err
|
|
}), nil
|
|
}
|
|
|
|
func (adapter *Adapter) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) {
|
|
if input == nil {
|
|
return nil, gerror.New("mingqian: input required")
|
|
}
|
|
output := query(ctx, adapter, input.SimKeys, usagePath, 50, func(item card) (model.SimDataUsageItem, error) {
|
|
if item.Usage == nil || *item.Usage < 0 {
|
|
return model.SimDataUsageItem{}, gerror.New("mingqian: current-month usage missing or unknown")
|
|
}
|
|
return model.SimDataUsageItem{DataUsage: float64(*item.Usage) / 1024}, nil
|
|
})
|
|
packages, err := adapter.QueryPackageFlow(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for index, item := range output {
|
|
if packages[index].Code != consts.ApiStatusSuccess {
|
|
item.ResData.PackageFlowError = packages[index].Message
|
|
if item.Code == consts.ApiStatusSuccess {
|
|
item.Message = "mingqian: current-month usage available; package flow unavailable (see packageFlowError)"
|
|
}
|
|
continue
|
|
}
|
|
item.ResData.PackageFlow = &packages[index].ResData
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func (adapter *Adapter) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) (model.SimCardStatusChangeOutput, error) {
|
|
if input == nil {
|
|
return nil, gerror.New("mingqian: input required")
|
|
}
|
|
path := ""
|
|
switch strings.TrimSpace(input.ReqData.OperType) {
|
|
case consts.CardStatusChangeOperTypeApplyStop:
|
|
path = "/simlist/get-sim-switchoff-by-number"
|
|
case consts.CardStatusChangeOperTypeApplyResume:
|
|
path = adapter.resumePath
|
|
default:
|
|
return unsupported[model.SimCardStatusChangeItem](input.SimKeys, "invalid operType"), nil
|
|
}
|
|
if path == "" {
|
|
return unsupported[model.SimCardStatusChangeItem](input.SimKeys, "resume endpoint unconfirmed; configure api.resumePath"), nil
|
|
}
|
|
return query(ctx, adapter, input.SimKeys, path, 20, func(item card) (model.SimCardStatusChangeItem, error) {
|
|
if item.Success == nil || *item.Success != 1 {
|
|
return model.SimCardStatusChangeItem{}, gerror.New("mingqian: card operation failed or success flag missing")
|
|
}
|
|
return model.SimCardStatusChangeItem{}, nil
|
|
}), nil
|
|
}
|
|
|
|
func unsupported[T any](keys []*model.SimKey, capability string) []*model.SimBaseRes[T] {
|
|
output := make([]*model.SimBaseRes[T], 0, len(keys))
|
|
for _, key := range keys {
|
|
output = append(output, &model.SimBaseRes[T]{SimKey: key, Code: consts.ApiStatusError, Message: "mingqian: " + capability})
|
|
}
|
|
return output
|
|
}
|