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,273 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user