chore: 初始开源发布

iotlcm-openapi:对外 OpenAPI 网关(V3 接口 + 旧版兼容接口)。

- Go module:iotlcm-openapi → git.5iot.com/iotlcm-public/iotlcm-openapi(178 文件)
- 凭据与地址脱敏:JWT signingKey、MySQL DSN、Redis 口令、
  RabbitMQ DSN 全部改为 ${*} 占位;私网 IP → 文档地址段
- 7 处真实手机号 → 合法 11 位编造号
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像,移除 GOPRIVATE 内网设置
- README 重写(原为 GoFrame 模板占位文案),补 Apache-2.0 LICENSE,
  LF 行尾 + .gitattributes
This commit is contained in:
MiFaZhan
2026-10-23 14:15:47 +08:00
commit 88dfe5c640
482 changed files with 34475 additions and 0 deletions
+298
View File
@@ -0,0 +1,298 @@
package query
import (
"context"
"encoding/json"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
"github.com/gogf/gf/v2/util/gconv"
// 触发 service.RegisterIotTSimCardInfo,否则 loadSimBatchCards 会 panic
_ "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/logic/openapiiot"
)
// setupImeiBatchDB 建一张最小可用的卡表并灌入覆盖各分支的样本数据。
//
// 样本设计(对齐 §7 测试场景):
//
// CARD-A 有 imei → 正常命中
// CARD-B imei 为空 → 卡存在但上游未上报值
// CARD-C 有 imei → 正常命中(入库带的值)
// CARD-D 属于另一个部门 → 数据权限过滤后视为 notFound
func setupImeiBatchDB(t *testing.T, ctx context.Context) {
t.Helper()
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = g.DB().Close(ctx) })
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
t.Fatal(err)
}
statements := []string{
`CREATE TABLE t_sim_card_info (
id INTEGER PRIMARY KEY,
iccid TEXT, msisdn TEXT, imsi TEXT, imei TEXT,
boss_card_status INTEGER, used_flow_mb REAL,
imei_sync_time DATETIME, imei_update_time DATETIME,
dept_id INTEGER, tenant_id TEXT
)`,
// FilterAuth 的 GetDeptAndSub 会查 sys_dept(本部门及以下数据权限)
`CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, ancestors TEXT)`,
`INSERT INTO t_sim_card_info
(id, iccid, msisdn, imsi, imei, boss_card_status, used_flow_mb, imei_sync_time, imei_update_time, dept_id, tenant_id)
VALUES
(1,'CARD-A','13800000001','460000000000001','865466070817587',2,10.5,'2026-10-12 04:31:07','2026-10-12 04:31:07',7,'A'),
(2,'CARD-B','13800000002','460000000000002','', 2,20.0,'2026-10-12 04:31:09',NULL, 7,'A'),
(3,'CARD-C','13800000003','460000000000003','123456789012345',2,30.0,NULL, '2026-09-01 08:00:00',7,'A'),
(4,'CARD-D','13800000004','460000000000004','999888777666555',2,40.0,'2026-10-12 04:31:11','2026-10-12 04:31:11',8,'A')`,
}
for _, statement := range statements {
if _, err := g.DB().Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
}
// apiDataScopeRoleId 对齐 middleware.go:28 的 apiDataScopeRoleId(包私有,此处复刻值)。
const apiDataScopeRoleId int64 = 17
// imeiBatchCtx 复现 ApiCtx 中间件注入的身份:API 客户固定 RoleId=17 +
// SysRoleDataScopeDeptAndBelow(对齐 middleware.go:99),使数据权限过滤真实生效。
func imeiBatchCtx() context.Context {
ctx := contexts.Set(context.Background(), &model.Context{
User: &model.Identity{
BaseClaims: model.BaseClaims{
ID: 0,
TenantId: "A",
DeptId: 7,
Roles: []model.Role{
{RoleId: apiDataScopeRoleId, DataScope: consts.SysRoleDataScopeDeptAndBelow},
},
},
},
AgentUser: &model.BaseClaimsApi{DeptId: 7, TenantId: "A"},
})
contexts.SetDataValue(ctx, "currentRoleId", gconv.String(apiDataScopeRoleId))
contexts.SetDataValue(ctx, "currentRoleDataScope", consts.SysRoleDataScopeDeptAndBelow)
return ctx
}
// TestQuerySimImeiBatchSemantics 覆盖 §7 T11/T12/T16 的核心语义:
// - 有值的卡返回 imei
// - imei 为空但卡存在 → matched=true 且 imei="",不算 notFound(T16)
// - 不存在的卡 → matched=false + CARD_NOT_FOUND(T12)
// - 跨部门的卡 → 数据权限过滤后视为 notFound(T13)
func TestQuerySimImeiBatchSemantics(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{
{Iccid: "CARD-A"},
{Iccid: "CARD-B"},
{Iccid: "CARD-C"},
{Iccid: "CARD-D"},
{Iccid: "CARD-MISSING"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if output.Total != 5 {
t.Fatalf("total = %d, want 5", output.Total)
}
// A/B/C 命中,D(跨部门)与 MISSING 未命中
if output.MatchedCount != 3 || output.NotFoundCount != 2 {
t.Fatalf("matched=%d notFound=%d, want 3/2", output.MatchedCount, output.NotFoundCount)
}
if len(output.Items) != 5 {
t.Fatalf("items = %d, want 5 (按输入顺序逐卡返回)", len(output.Items))
}
// 顺序必须与输入一致(契约:按输入顺序逐卡返回)。
// 注意:未命中的 item 不返回 iccid(omitempty),所以必须用下标访问,
// 不能用 iccid 建索引,否则未命中项会取到零值。
ordered := []string{"CARD-A", "CARD-B", "CARD-C", "CARD-D", "CARD-MISSING"}
for i, wantIccid := range ordered {
if i == 3 || i == 4 {
continue // D 与 MISSING 未命中,不返回 iccid
}
if output.Items[i].Iccid != wantIccid {
t.Fatalf("items[%d].iccid = %q, want %q(必须保持输入顺序)", i, output.Items[i].Iccid, wantIccid)
}
}
a, b, c := output.Items[0], output.Items[1], output.Items[2]
d, missing := output.Items[3], output.Items[4]
if !a.Matched || a.Imei != "865466070817587" {
t.Fatalf("CARD-A: %+v", a)
}
// CARD-B:imei 为空但 matched=true(关键语义,不算 notFound)
if !b.Matched {
t.Fatalf("CARD-B 应为 matched=true(卡存在,只是上游没给值): %+v", b)
}
if b.Imei != "" {
t.Fatalf("CARD-B imei 应为空串: %q", b.Imei)
}
// CARD-C:入库就带值
if !c.Matched || c.Imei != "123456789012345" {
t.Fatalf("CARD-C: %+v", c)
}
// CARD-D / MISSING:未命中,且不泄露 imei
for _, tc := range []struct {
name string
item model.QuerySimImeiBatchItem
}{{"CARD-D(跳部门)", d}, {"CARD-MISSING(不存在)", missing}} {
item := tc.item
if item.Matched {
t.Fatalf("%s 不应命中: %+v", tc.name, item)
}
if item.ErrorCode != "CARD_NOT_FOUND" {
t.Fatalf("%s errorCode = %q, want CARD_NOT_FOUND", tc.name, item.ErrorCode)
}
if item.Imei != "" {
t.Fatalf("%s 未命中时不应返回 imei: %q", tc.name, item.Imei)
}
}
}
// TestQuerySimImeiBatchValidation 覆盖 §7 T14/T15 的参数校验。
func TestQuerySimImeiBatchValidation(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
svc := NewQuerySim()
cases := []struct {
name string
in *model.QuerySimImeiBatchParam
}{
{"nil 参数", nil},
{"appId 为空", &model.QuerySimImeiBatchParam{Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}}}},
{"cards 为空", &model.QuerySimImeiBatchParam{AppId: "app"}},
}
for _, tc := range cases {
if _, err := svc.QuerySimImeiBatch(ctx, tc.in); err == nil {
t.Fatalf("%s: 应返回错误", tc.name)
}
}
// 超过 1000 张(T14)
tooMany := make([]model.QuerySimBatchCard, 0, querySimBatchMaxCards+1)
for i := 0; i <= querySimBatchMaxCards; i++ {
tooMany = append(tooMany, model.QuerySimBatchCard{Iccid: "CARD-" + string(rune('0'+i%10)) + string(rune('0'+i/10%10)) + string(rune('0'+i/100%10)) + string(rune('0'+i/1000%10))})
}
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{AppId: "app", Cards: tooMany}); err == nil {
t.Fatal("超过 1000 张应被拒绝")
}
// 重复标识(parseSimBatchIdentifiers 既有逻辑)
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}, {Iccid: "CARD-A"}},
}); err == nil {
t.Fatal("重复标识应被拒绝")
}
// 单标识超长(simBatchIdentifierMaxLen)
longIccid := make([]byte, simBatchIdentifierMaxLen+1)
for i := range longIccid {
longIccid[i] = '9'
}
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: string(longIccid)}},
}); err == nil {
t.Fatal("超长标识应被拒绝")
}
// 多标识互相矛盾:ICCID 属于 CARD-A 但 MSISDN 属于 CARD-B(既有校验逻辑)
if _, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A", Msisdn: "13800000002"}},
}); err != nil {
t.Fatalf("多标识查询本身应合法: %v", err)
}
// 矛盾组合应返回 notFound 而非错误(simBatchCardMatchesIdentifier 拦下)
conflict, err := svc.QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A", Msisdn: "13800000002"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if conflict.NotFoundCount != 1 || conflict.Items[0].Matched {
t.Fatalf("矛盾的 iccid+msisdn 组合应判为 notFound: %+v", conflict.Items[0])
}
}
// TestQuerySimImeiBatchByMsisdnAndImsi 验证三种标识都能定位到卡。
func TestQuerySimImeiBatchByMsisdnAndImsi(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{
{Msisdn: "13800000001"},
{Imsi: "460000000000003"},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if output.MatchedCount != 2 {
t.Fatalf("matched = %d, want 2", output.MatchedCount)
}
for _, item := range output.Items {
if !item.Matched || item.Iccid == "" {
t.Fatalf("应通过 msisdn/imsi 定位到卡并回填 iccid: %+v", item)
}
}
}
// TestQuerySimImeiBatchItemJSONShape 锁死对外字段集:只允许
// matched / iccid / imei / errorCode / errorMessage,任何额外字段(例如
// imeiSyncTime、imeiUpdateTime)都会让该用例失败,避免内部字段泄露给客户。
func TestQuerySimImeiBatchItemJSONShape(t *testing.T) {
ctx := imeiBatchCtx()
setupImeiBatchDB(t, ctx)
output, err := NewQuerySim().QuerySimImeiBatch(ctx, &model.QuerySimImeiBatchParam{
AppId: "app",
Cards: []model.QuerySimBatchCard{{Iccid: "CARD-A"}, {Iccid: "CARD-MISSING"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
allowed := map[string]bool{
"matched": true, "iccid": true, "imei": true,
"errorCode": true, "errorMessage": true,
}
for i, item := range output.Items {
raw, marshalErr := json.Marshal(item)
if marshalErr != nil {
t.Fatalf("items[%d] 序列化失败: %v", i, marshalErr)
}
var fields map[string]any
if err := json.Unmarshal(raw, &fields); err != nil {
t.Fatalf("items[%d] 反序列化失败: %v", i, err)
}
for name := range fields {
if !allowed[name] {
t.Fatalf("items[%d] 出现非预期对外字段 %q: %s", i, name, raw)
}
}
}
}