Files
iotlcm/internal/service/mingqian/adapter.go
T
MiFaZhan 96b1edff45 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
2026-10-22 16:08:06 +08:00

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
}