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:
MiFaZhan
2026-10-22 15:42:29 +08:00
commit 96b1edff45
191 changed files with 30805 additions and 0 deletions
@@ -0,0 +1,205 @@
package upstreamimpl
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/upstreamimpl/dx"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
)
func TestDXBatchDayAvailable(t *testing.T) {
now := time.Date(2026, 9, 20, 12, 0, 0, 0, dxBatchLocation)
tests := map[string]bool{
"20260913": true,
"20260919": true,
"20260912": false,
"20260920": false,
"20260921": false,
}
for queryDate, want := range tests {
if got := dxBatchDayAvailable(queryDate, now); got != want {
t.Fatalf("dxBatchDayAvailable(%q) = %v, want %v", queryDate, got, want)
}
}
}
func TestDXBatchMonthAvailable(t *testing.T) {
now := time.Date(2026, 9, 20, 12, 0, 0, 0, dxBatchLocation)
if !dxBatchMonthAvailable("202609", now) {
t.Fatal("current month should use telecom batch API")
}
if dxBatchMonthAvailable("202608", now) || dxBatchMonthAvailable("invalid", now) {
t.Fatal("historical or invalid month must not use telecom current-month batch API")
}
}
func TestQuerySimDataUsageMonthlyUsesBatchForCurrentMonth(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
if r.URL.Path != "/api/v1/batchQry/batchQryFlowByMonth" {
t.Fatalf("path = %q", r.URL.Path)
}
var request dxBatchRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
if len(request.AccessNumbers) != 2 || request.CustNumber != "cust-1" {
t.Fatalf("request = %+v", request)
}
_, _ = w.Write([]byte(`{"code":0,"msg":"查询成功","data":[{"accessNumber":"138****8001","result":"1","unit":"MB","flowAmount":"12.5"},{"accessNumber":"138****8002","result":"1","unit":"MB","flowAmount":"7.5"}]}`))
}))
defer server.Close()
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "secret"}), custNumber: "cust-1"}
queryMonth := time.Now().In(dxBatchLocation).Format("200601")
output, err := sim.QuerySimDataUsageMonthly(context.Background(), &model.SimDataUsageMonthlyInput{
SimKeys: []*model.SimKey{{MSISDN: "138****8001"}, {MSISDN: "138****8002"}},
ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: queryMonth},
})
if err != nil {
t.Fatal(err)
}
if calls.Load() != 1 || len(output) != 2 || output[0].ResData.DataAmount != 12.5 || output[1].ResData.DataAmount != 7.5 {
t.Fatalf("calls=%d output=%+v", calls.Load(), output)
}
}
func TestQuerySimDataUsageDailyLegacyFallbackIsBounded(t *testing.T) {
var active atomic.Int32
var maximum atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
current := active.Add(1)
defer active.Add(-1)
for {
previous := maximum.Load()
if current <= previous || maximum.CompareAndSwap(previous, current) {
break
}
}
time.Sleep(30 * time.Millisecond)
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(`<root><NEW_DATA_TICKET_QRsp><IRESULT>0</IRESULT><SMSG>OK</SMSG><TOTAL_BYTES_CNT>1MB</TOTAL_BYTES_CNT></NEW_DATA_TICKET_QRsp></root>`))
}))
defer server.Close()
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "secret", Timeout: time.Second})}
keys := make([]*model.SimKey, 12)
for index := range keys {
keys[index] = &model.SimKey{MSISDN: "138****8003"}
}
output, err := sim.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{
SimKeys: keys,
ReqData: model.SimDataUsageDailyInputItem{QueryDate: "20260101"},
})
if err != nil {
t.Fatal(err)
}
if len(output) != len(keys) {
t.Fatalf("results = %d, want %d", len(output), len(keys))
}
if maximum.Load() > dxLegacyUsageConcurrency || maximum.Load() < 2 {
t.Fatalf("maximum concurrency = %d, want 2..%d", maximum.Load(), dxLegacyUsageConcurrency)
}
}
func TestQuerySimDataUsageDailyRejectsMalformedXML(t *testing.T) {
cases := []struct {
name string
body string
}{
{name: "missing business node", body: `<root><ERROR>denied</ERROR></root>`},
{name: "missing result", body: `<root><NEW_DATA_TICKET_QRsp><TOTAL_BYTES_CNT>12MB</TOTAL_BYTES_CNT></NEW_DATA_TICKET_QRsp></root>`},
{name: "missing amount", body: `<root><NEW_DATA_TICKET_QRsp><IRESULT>0</IRESULT></NEW_DATA_TICKET_QRsp></root>`},
{name: "invalid amount", body: `<root><NEW_DATA_TICKET_QRsp><IRESULT>0</IRESULT><TOTAL_BYTES_CNT>invalidMB</TOTAL_BYTES_CNT></NEW_DATA_TICKET_QRsp></root>`},
{name: "non numeric result", body: `<root><NEW_DATA_TICKET_QRsp><IRESULT>ERROR</IRESULT></NEW_DATA_TICKET_QRsp></root>`},
}
for _, item := range cases {
t.Run(item.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/xml")
_, _ = writer.Write([]byte(item.body))
}))
defer server.Close()
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "test"})}
output, err := sim.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{
SimKeys: []*model.SimKey{{ICCID: "audit-card", MSISDN: "138****8001"}},
ReqData: model.SimDataUsageDailyInputItem{QueryDate: "20260101"},
})
if err != nil {
t.Fatal(err)
}
if len(output) != 1 || output[0].Code == 0 {
t.Fatalf("malformed response accepted: %+v", output)
}
})
}
}
func TestQuerySimDataUsageDailyBatchFailureFallsBackToLegacy(t *testing.T) {
var batchCalls atomic.Int32
var singleCalls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path == "/api/v1/batchQry/batchQryFlowByDate" {
batchCalls.Add(1)
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write([]byte(`{"code":403,"msg":"denied"}`))
return
}
singleCalls.Add(1)
writer.Header().Set("Content-Type", "application/xml")
_, _ = writer.Write([]byte(`<root><NEW_DATA_TICKET_QRsp><IRESULT>0</IRESULT><TOTAL_BYTES_CNT>12MB</TOTAL_BYTES_CNT></NEW_DATA_TICKET_QRsp></root>`))
}))
defer server.Close()
sim := &sSimDX{client: dx.NewClient(dx.Config{BaseURL: server.URL, AppKey: "test", AppSecret: "test"}), custNumber: "audit-customer"}
queryDate := time.Now().In(dxBatchLocation).AddDate(0, 0, -1).Format("20060102")
output, err := sim.QuerySimDataUsageDaily(context.Background(), &model.SimDataUsageDailyInput{
SimKeys: []*model.SimKey{{MSISDN: "138****8001"}},
ReqData: model.SimDataUsageDailyInputItem{QueryDate: queryDate},
})
if err != nil {
t.Fatal(err)
}
if batchCalls.Load() != 1 || singleCalls.Load() != 1 {
t.Fatalf("batch calls=%d, single calls=%d, want 1 each", batchCalls.Load(), singleCalls.Load())
}
if len(output) != 1 || output[0].Code != 0 || output[0].ResData.DataAmount != 12 {
t.Fatalf("unexpected fallback output: %+v", output)
}
}
func TestParseDXDataUsage(t *testing.T) {
cases := []struct {
value string
want float64
invalid bool
}{
{value: "0MB", want: 0},
{value: "12.5MB", want: 12.5},
{value: " 1 gb ", want: 1024},
{value: "1024KB", want: 1},
{value: "12", want: 12},
{value: "", invalid: true},
{value: "invalidMB", invalid: true},
{value: "-1MB", invalid: true},
{value: "NaNMB", invalid: true},
{value: "InfMB", invalid: true},
{value: "1e308GB", invalid: true},
}
for _, item := range cases {
t.Run(item.value, func(t *testing.T) {
amount, err := parseDXDataUsage(item.value)
if (err != nil) != item.invalid || (!item.invalid && amount != item.want) {
t.Fatalf("parseDXDataUsage(%q) = %v, %v, want %v, invalid=%v", item.value, amount, err, item.want, item.invalid)
}
})
}
}