chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
)
|
||||
|
||||
const inboundBasicInfoClientNameEnv = "UPSTREAM_INBOUND_BASIC_INFO_CLIENT_NAME"
|
||||
const upstreamProxyMaxTimeoutConfigKey = "upstream.proxy.maxTimeout"
|
||||
|
||||
type Client struct {
|
||||
currentUsageClientName string
|
||||
currentUsageTimeout time.Duration
|
||||
inboundBasicInfoClientName string
|
||||
imeiClientName string
|
||||
imeiTimeout time.Duration
|
||||
}
|
||||
|
||||
type response[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []model.SimBaseRes[T] `json:"data"`
|
||||
}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{}
|
||||
}
|
||||
|
||||
func (c *Client) QuerySimBasicInfo(ctx context.Context, input *model.SimBasicInfoInput) ([]model.SimBaseRes[model.SimBasicInfoItem], error) {
|
||||
address, err := c.inboundBasicInfoAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return requestAllWithAddress[model.SimBasicInfoItem](ctx, "/sim/query/sim-basic-info", input, len(input.SimKeys), address)
|
||||
}
|
||||
|
||||
// NewWithCurrentUsageClient keeps the full-sync current-usage call independent
|
||||
// from the configured upstream.proxy endpoint.
|
||||
func NewWithCurrentUsageClient(address string, timeout ...time.Duration) *Client {
|
||||
var currentUsageTimeout time.Duration
|
||||
if len(timeout) > 0 {
|
||||
currentUsageTimeout = timeout[0]
|
||||
}
|
||||
return &Client{currentUsageClientName: address, currentUsageTimeout: currentUsageTimeout}
|
||||
}
|
||||
|
||||
func NewWithInboundBasicInfoClient(address string) *Client {
|
||||
return &Client{inboundBasicInfoClientName: address}
|
||||
}
|
||||
|
||||
// NewWithImeiClient pins the IMEI full-sync path to an explicit chan endpoint
|
||||
// and timeout. An empty address falls back to the shared upstream.proxy
|
||||
// configuration, which is only intended for local runs.
|
||||
func NewWithImeiClient(address string, timeout ...time.Duration) *Client {
|
||||
var imeiTimeout time.Duration
|
||||
if len(timeout) > 0 {
|
||||
imTimeout := timeout[0]
|
||||
if imTimeout > 0 {
|
||||
imeiTimeout = imTimeout
|
||||
}
|
||||
}
|
||||
return &Client{imeiClientName: address, imeiTimeout: imeiTimeout}
|
||||
}
|
||||
|
||||
func (c *Client) inboundBasicInfoAddress(ctx context.Context) (string, error) {
|
||||
if strings.TrimSpace(c.inboundBasicInfoClientName) != "" {
|
||||
return strings.TrimSpace(c.inboundBasicInfoClientName), nil
|
||||
}
|
||||
if address := strings.TrimSpace(os.Getenv(inboundBasicInfoClientNameEnv)); address != "" {
|
||||
return address, nil
|
||||
}
|
||||
config, ok, err := optionalUpstreamConfig(ctx, "upstream.gateway")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok || strings.TrimSpace(config.Address) == "" {
|
||||
return "", gerror.Newf("upstream gateway address is empty, configure upstream.gateway.address or %s", inboundBasicInfoClientNameEnv)
|
||||
}
|
||||
return strings.TrimSpace(config.Address), nil
|
||||
}
|
||||
|
||||
// QuerySimDataUsage calls Chan's current-month real-time usage endpoint. The
|
||||
// response keeps one item per requested card so the caller can classify
|
||||
// per-card errors without reissuing successful cards.
|
||||
func (c *Client) QuerySimDataUsage(ctx context.Context, input *model.SimDataUsageInput) ([]model.SimBaseRes[model.SimDataUsageItem], error) {
|
||||
return requestAllWithAddressTimeout[model.SimDataUsageItem](ctx, "/sim/query/sim-datausage", input, len(input.SimKeys), c.currentUsageClientName, c.currentUsageTimeout)
|
||||
}
|
||||
|
||||
// QuerySimCardStatus queries the carrier status for the supplied cards. The
|
||||
// status queue currently sends one card per request, while retaining the
|
||||
// response shape needed for future batch support.
|
||||
func (c *Client) QuerySimCardStatus(ctx context.Context, input *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error) {
|
||||
return requestAll[model.SimCardStatusItem](ctx, "/sim/query/sim-status", input, len(input.SimKeys))
|
||||
}
|
||||
|
||||
// QuerySimImei queries the terminal IMEI for the supplied cards. The IMEI queue
|
||||
// sends one card per request.
|
||||
//
|
||||
// It deliberately uses requestAll rather than request: a per-item carrier
|
||||
// failure (unsupported template, missing value) must reach the handler as an
|
||||
// item with a non-zero Code, not be silently dropped from the slice.
|
||||
func (c *Client) QuerySimImei(ctx context.Context, input *model.SimImeiInput) ([]model.SimBaseRes[model.SimImeiItem], error) {
|
||||
return requestAllWithAddressTimeout[model.SimImeiItem](ctx, "/sim/query/sim-imei", input, len(input.SimKeys), c.imeiClientName, c.imeiTimeout)
|
||||
}
|
||||
|
||||
func (c *Client) QuerySimDatausageDaily(ctx context.Context, input *model.SimDataUsageDailyInput) ([]model.SimBaseRes[model.SimDataUsageDailyOutputItem], error) {
|
||||
return requestAll[model.SimDataUsageDailyOutputItem](ctx, "/sim/query/sim-datausage-daily", input, len(input.SimKeys))
|
||||
}
|
||||
|
||||
func (c *Client) QuerySimDatausageMonthly(ctx context.Context, input *model.SimDataUsageMonthlyInput) ([]model.SimBaseRes[model.SimDataUsageMonthlyOutputItem], error) {
|
||||
return requestAll[model.SimDataUsageMonthlyOutputItem](ctx, "/sim/query/sim-datausage-monthly", input, len(input.SimKeys))
|
||||
}
|
||||
|
||||
// ChangeSimStatus returns one result for every upstream item, including failed items.
|
||||
// Lifecycle batch execution needs those failures to be persisted to the matching detail.
|
||||
func (c *Client) ChangeSimStatus(ctx context.Context, input *model.SimCardStatusChangeInput) ([]model.SimBaseRes[model.SimCardStatusChangeOutputItem], error) {
|
||||
return requestAll[model.SimCardStatusChangeOutputItem](ctx, "/sim/change/sim-status", input, len(input.SimKeys))
|
||||
}
|
||||
|
||||
// ChangeSimApnFunction returns one result for every upstream item, including failed items.
|
||||
// Data service batch execution needs those failures to be persisted to the matching detail.
|
||||
func (c *Client) ChangeSimApnFunction(ctx context.Context, input *model.SimCardApnChangeInput) ([]model.SimBaseRes[model.SimCardApnChangeOutputItem], error) {
|
||||
return requestAll[model.SimCardApnChangeOutputItem](ctx, "/sim/change/sim-apn", input, len(input.SimKeys))
|
||||
}
|
||||
|
||||
func request[T any](ctx context.Context, path string, input any, itemCount int) ([]model.SimBaseRes[T], error) {
|
||||
items, err := requestAll[T](ctx, path, input, itemCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
successItems := make([]model.SimBaseRes[T], 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Code == 0 {
|
||||
successItems = append(successItems, item)
|
||||
}
|
||||
}
|
||||
return successItems, nil
|
||||
}
|
||||
|
||||
func requestAll[T any](ctx context.Context, path string, input any, itemCount int) ([]model.SimBaseRes[T], error) {
|
||||
return requestAllWithAddress[T](ctx, path, input, itemCount, "")
|
||||
}
|
||||
|
||||
func requestAllWithAddress[T any](ctx context.Context, path string, input any, itemCount int, address string) ([]model.SimBaseRes[T], error) {
|
||||
return requestAllWithAddressTimeout[T](ctx, path, input, itemCount, address, 0)
|
||||
}
|
||||
|
||||
func requestAllWithAddressTimeout[T any](ctx context.Context, path string, input any, itemCount int, address string, timeoutOverride time.Duration) ([]model.SimBaseRes[T], error) {
|
||||
config, err := upstreamConfig(ctx, "upstream.proxy")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if address == "" {
|
||||
address = config.Address
|
||||
}
|
||||
if address == "" {
|
||||
return nil, gerror.New("upstream proxy address is empty")
|
||||
}
|
||||
if itemCount < 1 {
|
||||
itemCount = 1
|
||||
}
|
||||
|
||||
client := g.Client()
|
||||
timeout := timeoutOverride
|
||||
if timeout <= 0 {
|
||||
timeout = config.Timeout
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
if itemCount > 1 {
|
||||
timeout *= time.Duration(itemCount)
|
||||
}
|
||||
if maxTimeout := g.Cfg().MustGet(ctx, upstreamProxyMaxTimeoutConfigKey, 2*time.Minute).Duration(); maxTimeout > 0 && timeout > maxTimeout {
|
||||
timeout = maxTimeout
|
||||
}
|
||||
setClientTimeout(client, timeout)
|
||||
clientResponse, err := client.Post(ctx, fmt.Sprintf("http://%s%s", address, path), input)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "request upstream failed")
|
||||
}
|
||||
defer clientResponse.Close()
|
||||
|
||||
return decodeResponse[T](clientResponse.ReadAll())
|
||||
}
|
||||
|
||||
func optionalUpstreamConfig(ctx context.Context, key string) (model.UpstreamChanConfig, bool, error) {
|
||||
configValue, err := g.Cfg().Get(ctx, key)
|
||||
if err != nil {
|
||||
return model.UpstreamChanConfig{}, false, gerror.Wrapf(err, "read upstream configuration failed: %s", key)
|
||||
}
|
||||
if configValue == nil || configValue.IsNil() {
|
||||
return model.UpstreamChanConfig{}, false, nil
|
||||
}
|
||||
config := model.UpstreamChanConfig{}
|
||||
if err = configValue.Scan(&config); err != nil {
|
||||
return model.UpstreamChanConfig{}, false, gerror.Wrapf(err, "parse upstream configuration failed: %s", key)
|
||||
}
|
||||
return config, true, nil
|
||||
}
|
||||
|
||||
func upstreamConfig(ctx context.Context, key string) (model.UpstreamChanConfig, error) {
|
||||
config, ok, err := optionalUpstreamConfig(ctx, key)
|
||||
if err != nil {
|
||||
return model.UpstreamChanConfig{}, err
|
||||
}
|
||||
if !ok {
|
||||
return model.UpstreamChanConfig{}, gerror.Newf("upstream configuration is missing: %s", key)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func decodeResponse[T any](body []byte) ([]model.SimBaseRes[T], error) {
|
||||
output := response[T]{}
|
||||
if err := json.Unmarshal(body, &output); err != nil {
|
||||
return nil, gerror.Wrap(err, "parse upstream response failed")
|
||||
}
|
||||
if output.Code != 0 {
|
||||
return nil, gerror.New(output.Message)
|
||||
}
|
||||
return output.Data, nil
|
||||
}
|
||||
|
||||
func setClientTimeout(client *gclient.Client, timeout time.Duration) {
|
||||
client.SetTimeout(timeout)
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model"
|
||||
)
|
||||
|
||||
func TestSimCardStatusChangeProtocol(t *testing.T) {
|
||||
input := model.SimCardStatusChangeInput{
|
||||
SimKeys: []*model.SimKey{{ChanKey: "channel-a", ICCID: "89860001"}},
|
||||
ReqData: model.SimCardStatusChangeInputItem{OperType: "1"},
|
||||
}
|
||||
body, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const want = `{"simKeys":[{"chanKey":"channel-a","iccid":"89860001","imsi":"","msisdn":""}],"reqData":{"operType":"1"}}`
|
||||
if got := string(body); got != want {
|
||||
t.Fatalf("request body = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUsageConsumerProtocol(t *testing.T) {
|
||||
input := model.SimDataUsageInput{
|
||||
SimKeys: []*model.SimKey{{ChanKey: "channel-a", ICCID: "89860001", IMSI: "460001", MSISDN: "13800000000"}},
|
||||
}
|
||||
body, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const want = `{"simKeys":[{"chanKey":"channel-a","iccid":"89860001","imsi":"460001","msisdn":"13800000000"}],"reqData":{}}`
|
||||
if got := string(body); got != want {
|
||||
t.Fatalf("request body = %s, want %s", got, want)
|
||||
}
|
||||
client := NewWithCurrentUsageClient("iotlcmupstreamchan-http-svc:8110")
|
||||
if client.currentUsageClientName != "iotlcmupstreamchan-http-svc:8110" {
|
||||
t.Fatalf("current usage client = %q", client.currentUsageClientName)
|
||||
}
|
||||
if client.currentUsageTimeout != 0 {
|
||||
t.Fatalf("default current usage timeout = %s, want unset", client.currentUsageTimeout)
|
||||
}
|
||||
configured := NewWithCurrentUsageClient("iotlcmupstreamchan-http-svc:8110", 90*time.Second)
|
||||
if configured.currentUsageTimeout != 90*time.Second {
|
||||
t.Fatalf("configured current usage timeout = %s, want 90s", configured.currentUsageTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInboundBasicInfoClientCanUseDedicatedEndpoint(t *testing.T) {
|
||||
client := NewWithInboundBasicInfoClient("iotlcm.example.com:8110")
|
||||
clientName, err := client.inboundBasicInfoAddress(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if clientName != "iotlcm.example.com:8110" {
|
||||
t.Fatalf("inbound basic info client = %q, want dedicated chan endpoint", clientName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInboundBasicInfoClientCanUseEnvEndpoint(t *testing.T) {
|
||||
const endpoint = "iotlcm.example.com:8110"
|
||||
t.Setenv(inboundBasicInfoClientNameEnv, endpoint)
|
||||
client := New()
|
||||
clientName, err := client.inboundBasicInfoAddress(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if clientName != endpoint {
|
||||
t.Fatalf("inbound basic info client = %q, want env endpoint", clientName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredInboundBasicInfoClientUsesDedicatedEndpoint(t *testing.T) {
|
||||
if value := os.Getenv(inboundBasicInfoClientNameEnv); value != "" {
|
||||
t.Setenv(inboundBasicInfoClientNameEnv, "")
|
||||
}
|
||||
client := New()
|
||||
clientName, err := client.inboundBasicInfoAddress(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(clientName, ":8110") {
|
||||
t.Fatalf("inbound basic info client = %q, want dedicated chan endpoint", clientName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimCardApnChangeProtocol(t *testing.T) {
|
||||
input := model.SimCardApnChangeInput{
|
||||
SimKeys: []*model.SimKey{{ChanKey: "channel-a", ICCID: "89860001"}},
|
||||
ReqData: model.SimCardApnChangeInputItem{OperType: "0", ApnName: "CMIOT"},
|
||||
}
|
||||
body, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const want = `{"simKeys":[{"chanKey":"channel-a","iccid":"89860001","imsi":"","msisdn":""}],"reqData":{"operType":"0","apnName":"CMIOT"}}`
|
||||
if got := string(body); got != want {
|
||||
t.Fatalf("request body = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSimCardApnChangeResponseKeepsFailedItems(t *testing.T) {
|
||||
body := []byte(`{"code":0,"message":"ok","data":[{"code":0,"message":"success","simKey":{"iccid":"89860001"},"resData":{"orderNum":"order-1"}},{"code":4001,"message":"denied","simKey":{"iccid":"89860002"},"resData":{"orderNum":"order-2"}}]}`)
|
||||
items, err := decodeResponse[model.SimCardApnChangeOutputItem](body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("item count = %d, want 2", len(items))
|
||||
}
|
||||
if items[1].Code != 4001 || items[1].ResData.OrderNum != "order-2" {
|
||||
t.Fatalf("failed item was not preserved: %+v", items[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSimCardStatusChangeResponseKeepsFailedItems(t *testing.T) {
|
||||
body := []byte(`{"code":0,"message":"ok","data":[{"code":0,"message":"success","simKey":{"iccid":"89860001"},"resData":{"orderNum":"order-1"}},{"code":4001,"message":"denied","simKey":{"iccid":"89860002"},"resData":{"orderNum":"order-2"}}]}`)
|
||||
items, err := decodeResponse[model.SimCardStatusChangeOutputItem](body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("item count = %d, want 2", len(items))
|
||||
}
|
||||
if items[1].Code != 4001 || items[1].ResData.OrderNum != "order-2" {
|
||||
t.Fatalf("failed item was not preserved: %+v", items[1])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user