sync: 同步 v1.0.4 并脱敏配置与镜像

This commit is contained in:
MiFaZhan
2026-10-29 14:36:49 +08:00
parent 743e8e6e6e
commit 2206a393f7
516 changed files with 18119 additions and 2253 deletions
@@ -0,0 +1,292 @@
package iot
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
v1 "git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/iot/v1"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/imeiqueue"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/simstatusqueue"
_ "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/trafficusage"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service/fullsyncretry"
)
type scopedStarter func(context.Context, *model.SyncScope) (string, error)
// periodStarter is the shape of the *At starters: a scope plus a yyyyMMdd or
// yyyyMM period.
type periodStarter func(context.Context, *model.SyncScope, string) (string, error)
type monthlyStarterStub struct {
service.ITrafficUsageMonthlySync
current scopedStarter
history scopedStarter
at periodStarter
}
func (s monthlyStarterStub) StartMonthlyRunScoped(ctx context.Context, scope *model.SyncScope) (string, error) {
return s.current(ctx, scope)
}
func (s monthlyStarterStub) StartMonthlyHistoryRunScoped(ctx context.Context, scope *model.SyncScope) (string, error) {
return s.history(ctx, scope)
}
func (s monthlyStarterStub) StartMonthlyHistoryRunScopedAt(ctx context.Context, scope *model.SyncScope, queryMonth string) (string, error) {
return s.at(ctx, scope, queryMonth)
}
type dailyStarterStub struct {
service.ITrafficUsageDailySync
start scopedStarter
at periodStarter
}
func (s dailyStarterStub) StartDailyRunScoped(ctx context.Context, scope *model.SyncScope) (string, error) {
return s.start(ctx, scope)
}
func (s dailyStarterStub) StartDailyRunScopedAt(ctx context.Context, scope *model.SyncScope, queryDate string) (string, error) {
return s.at(ctx, scope, queryDate)
}
type statusStarterStub struct {
service.ISimStatusQueue
start scopedStarter
}
func (s statusStarterStub) StartFullSyncRunScoped(ctx context.Context, scope *model.SyncScope) (string, error) {
return s.start(ctx, scope)
}
type imeiStarterStub struct {
service.IImeiQueue
start scopedStarter
}
func (s imeiStarterStub) StartImeiRunScoped(ctx context.Context, scope *model.SyncScope) (string, error) {
return s.start(ctx, scope)
}
func installFullSyncStarters(t *testing.T, start func(kind string, scope *model.SyncScope, periods ...string) (string, error)) {
t.Helper()
var (
monthly = service.TrafficUsageMonthlySync()
daily = service.TrafficUsageDailySync()
status = service.SimStatusQueue()
imei = service.ImeiQueue()
)
t.Cleanup(func() {
service.RegisterTrafficUsageMonthlySync(monthly)
service.RegisterTrafficUsageDailySync(daily)
service.RegisterSimStatusQueue(status)
service.RegisterImeiQueue(imei)
})
forKind := func(kind string) scopedStarter {
return func(_ context.Context, scope *model.SyncScope) (string, error) { return start(kind, scope) }
}
forKindAt := func(kind string) periodStarter {
return func(_ context.Context, scope *model.SyncScope, period string) (string, error) {
return start(kind, scope, period)
}
}
service.RegisterTrafficUsageMonthlySync(monthlyStarterStub{current: forKind("traffic_usage"), history: forKind("traffic_usage_monthly"), at: forKindAt("traffic_usage_monthly")})
service.RegisterTrafficUsageDailySync(dailyStarterStub{start: forKind("traffic_usage_daily"), at: forKindAt("traffic_usage_daily")})
service.RegisterSimStatusQueue(statusStarterStub{start: forKind("sim_status")})
service.RegisterImeiQueue(imeiStarterStub{start: forKind("imei")})
}
func fullSyncActorContext() context.Context {
return contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 42}}})
}
func TestFullSyncStartAndMonitorAllFiveKinds(t *testing.T) {
var started []string
var periods []string
installFullSyncStarters(t, func(kind string, scope *model.SyncScope, period ...string) (string, error) {
started = append(started, kind)
if scope.TriggerSource != model.TriggerSourceManual || !reflect.DeepEqual(scope.Channels, []string{"a", "b"}) {
t.Errorf("%s scope = %+v", kind, scope)
}
// Without a period field the *At starters must receive an empty period,
// i.e. the run keeps its historical default window.
periods = append(periods, period...)
return "run-" + kind, nil
})
var filters []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/internal/v1/full-sync-runs" || r.URL.Query().Get("limit") != "100" {
t.Errorf("unexpected active-run request: %s %s", r.Method, r.URL)
}
filters = append(filters, r.URL.Query().Get("syncKind"))
_, _ = w.Write([]byte(`{"items":[],"total":0}`))
}))
defer server.Close()
controller := &FullSyncMonitorController{client: fullsyncretry.Client{BaseURL: server.URL}}
tests := []struct{ kind, operation string }{
{"traffic_usage", "current_month_usage_full_sync"},
{"sim_status", "sim_status_queue_full_sync"},
{"imei", "imei_full_sync"},
{"traffic_usage_daily", "previous_day_usage_full_sync"},
{"traffic_usage_monthly", "previous_month_usage_full_sync"},
}
for _, tt := range tests {
t.Run(tt.kind, func(t *testing.T) {
var req v1.FullSyncRunStartReq
body, _ := json.Marshal(map[string]any{"syncKind": tt.kind, "channels": []string{"b", "a", "b"}})
if err := json.Unmarshal(body, &req); err != nil {
t.Fatal(err)
}
res, err := controller.Start(fullSyncActorContext(), &req)
if err != nil || res.RunID != "run-"+tt.kind || res.SyncKind != tt.kind || res.Status != "published" || res.ScopeChannels != 2 {
t.Fatalf("start = %+v, err = %v", res, err)
}
if res.QueryPeriod != "" {
t.Fatalf("%s queryPeriod = %q, want empty for the default period", tt.kind, res.QueryPeriod)
}
if got, name := fullSyncKindName(tt.operation); got != tt.kind || name == "" {
t.Fatalf("operation %q maps to %q, %q", tt.operation, got, name)
}
if _, err := controller.List(context.Background(), &v1.FullSyncRunsListReq{SyncKind: tt.kind, Limit: 100}); err != nil {
t.Fatal(err)
}
})
}
if len(started) != 5 || len(filters) != 10 {
t.Fatalf("starts = %v, filters = %v", started, filters)
}
// Only the daily and monthly-history kinds go through a period-aware starter.
if len(periods) != 2 || periods[0] != "" || periods[1] != "" {
t.Fatalf("periods = %v, want two empty periods", periods)
}
for i, tt := range tests {
if started[i] != tt.kind || filters[2*i] != tt.kind || filters[2*i+1] != tt.kind {
t.Fatalf("incorrect kind routing: starts = %v, filters = %v", started, filters)
}
}
}
func TestFullSyncStartRejectsInvalidAndActiveRequests(t *testing.T) {
starts, queries := 0, 0
installFullSyncStarters(t, func(string, *model.SyncScope, ...string) (string, error) {
starts++
return "unexpected", nil
})
active := "running"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
queries++
_ = json.NewEncoder(w).Encode(fullsyncretry.RunList{Items: []fullsyncretry.RunView{{Run: fullsyncretry.RunMeta{Status: active}}}})
}))
defer server.Close()
controller := &FullSyncMonitorController{client: fullsyncretry.Client{BaseURL: server.URL}}
if _, err := controller.Start(context.Background(), &v1.FullSyncRunStartReq{SyncKind: "imei"}); gerror.Code(err) != gcode.CodeNotAuthorized {
t.Fatalf("anonymous start = %v", err)
}
for _, req := range []v1.FullSyncRunStartReq{
{SyncKind: ""}, {SyncKind: "unknown"}, {SyncKind: "imei", Channels: []string{"bad/channel"}},
} {
if _, err := controller.Start(fullSyncActorContext(), &req); gerror.Code(err) != gcode.CodeInvalidParameter {
t.Fatalf("invalid request %+v = %v", req, err)
}
}
if queries != 0 {
t.Fatal("invalid request queried Worker")
}
for _, kind := range []string{"traffic_usage", "sim_status", "imei", "traffic_usage_daily", "traffic_usage_monthly"} {
for _, active = range []string{"running", "producing"} {
if _, err := controller.Start(fullSyncActorContext(), &v1.FullSyncRunStartReq{SyncKind: kind}); gerror.Code(err) != gcode.CodeInvalidParameter {
t.Fatalf("%s/%s active run = %v", kind, active, err)
}
}
}
if starts != 0 || queries != 10 {
t.Fatalf("starts = %d, queries = %d", starts, queries)
}
}
// TestFullSyncStartForwardsQueryPeriod pins the backfill wiring: the requested
// period must reach the period-aware starter, and the response must echo it so
// the operator can see which window the run covers.
func TestFullSyncStartForwardsQueryDate(t *testing.T) {
assertFullSyncPeriodForwarded(t, "traffic_usage_daily", "20261014")
}
func TestFullSyncStartForwardsQueryMonth(t *testing.T) {
assertFullSyncPeriodForwarded(t, "traffic_usage_monthly", "202604")
}
func assertFullSyncPeriodForwarded(t *testing.T, syncKind, period string) {
t.Helper()
type call struct{ kind, period string }
var calls []call
installFullSyncStarters(t, func(kind string, _ *model.SyncScope, periods ...string) (string, error) {
calls = append(calls, call{kind: kind, period: periods[0]})
return "run-" + kind, nil
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"items":[],"total":0}`))
}))
defer server.Close()
controller := &FullSyncMonitorController{client: fullsyncretry.Client{BaseURL: server.URL}}
req := &v1.FullSyncRunStartReq{SyncKind: syncKind}
if syncKind == "traffic_usage_daily" {
req.QueryDate = period
} else {
req.QueryMonth = period
}
res, err := controller.Start(fullSyncActorContext(), req)
if err != nil {
t.Fatalf("start = %v", err)
}
if res.QueryPeriod != period {
t.Fatalf("queryPeriod = %q, want %q", res.QueryPeriod, period)
}
if len(calls) != 1 || calls[0].kind != syncKind || calls[0].period != period {
t.Fatalf("starter calls = %+v, want one %s call with period %s", calls, syncKind, period)
}
}
// TestFullSyncStartRejectsMismatchedPeriodField keeps a stray period field from
// silently doing nothing: queryMonth on a daily run is a caller mistake.
func TestFullSyncStartRejectsMismatchedPeriodField(t *testing.T) {
starts := 0
installFullSyncStarters(t, func(string, *model.SyncScope, ...string) (string, error) {
starts++
return "unexpected", nil
})
queries := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
queries++
_, _ = w.Write([]byte(`{"items":[],"total":0}`))
}))
defer server.Close()
controller := &FullSyncMonitorController{client: fullsyncretry.Client{BaseURL: server.URL}}
for _, req := range []v1.FullSyncRunStartReq{
{SyncKind: "traffic_usage_daily", QueryMonth: "202604"},
{SyncKind: "traffic_usage_monthly", QueryDate: "20261014"},
{SyncKind: "imei", QueryDate: "20261014"},
{SyncKind: "sim_status", QueryMonth: "202604"},
{SyncKind: "traffic_usage", QueryMonth: "202604"},
} {
if _, err := controller.Start(fullSyncActorContext(), &req); gerror.Code(err) != gcode.CodeInvalidParameter {
t.Fatalf("mismatched request %+v = %v, want invalid parameter", req, err)
}
}
if starts != 0 {
t.Fatalf("starters called %d times for rejected requests", starts)
}
if queries != 0 {
t.Fatal("rejected request queried Worker")
}
}
@@ -0,0 +1,83 @@
package iot
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
v1 "git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/iot/v1"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service/fullsyncretry"
)
func TestFullSyncReplayRequestMaxRoundsThroughController(t *testing.T) {
var bodies []map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/internal/v1/full-sync-runs/source/replay" || r.Header.Get("Content-Type") != "application/json" {
t.Errorf("unexpected Worker request: %s %s", r.Method, r.URL)
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Error(err)
}
bodies = append(bodies, body)
w.WriteHeader(http.StatusAccepted)
_, _ = w.Write([]byte(`{"replayRunId":"replay-1","status":"accepted"}`))
}))
defer server.Close()
controller := &FullSyncReplayController{client: fullsyncretry.Client{BaseURL: server.URL}}
tests := []struct {
body string
rounds int
}{
{`{"sourceRunId":"source"}`, 0},
{`{"sourceRunId":"source","maxRounds":0}`, 0},
{`{"sourceRunId":"source","maxRounds":1}`, 1},
{`{"sourceRunId":"source","maxRounds":3}`, 3},
{`{"sourceRunId":"source","maxRounds":7}`, 7},
{`{"sourceRunId":"source","maxRounds":100}`, 100},
}
for i, tt := range tests {
var req v1.FullSyncReplayReq
if err := json.Unmarshal([]byte(tt.body), &req); err != nil {
t.Fatal(err)
}
if err := g.Validator().Data(req).Run(context.Background()); err != nil {
t.Fatalf("valid request %s: %v", tt.body, err)
}
res, err := controller.Replay(fullSyncActorContext(), &req)
if err != nil || res.ReplayRunID != "replay-1" || res.Status != "accepted" {
t.Fatalf("replay = %+v, err = %v", res, err)
}
body := bodies[i]
if body["requestedBy"] != "42" {
t.Fatalf("actor not forwarded: %+v", body)
}
if tt.rounds == 0 {
if len(body) != 1 {
t.Fatalf("legacy Worker must receive no extra field: %+v", body)
}
} else if len(body) != 2 || body["maxRounds"] != float64(tt.rounds) {
t.Fatalf("rounds not forwarded: %+v", body)
}
}
for _, rounds := range []int{-1, 101} {
req := v1.FullSyncReplayReq{SourceRunID: "source", MaxRounds: rounds}
if err := g.Validator().Data(req).Run(context.Background()); err == nil {
t.Fatalf("API accepted maxRounds=%d", rounds)
}
if _, err := controller.Replay(fullSyncActorContext(), &req); gerror.Code(err) != gcode.CodeInvalidParameter {
t.Fatalf("controller accepted maxRounds=%d: %v", rounds, err)
}
}
if _, err := controller.Replay(context.Background(), &v1.FullSyncReplayReq{SourceRunID: "source", MaxRounds: 3}); gerror.Code(err) != gcode.CodeNotAuthorized {
t.Fatalf("anonymous replay = %v", err)
}
if len(bodies) != len(tests) {
t.Fatalf("invalid/anonymous request reached Worker: %d calls", len(bodies))
}
}
@@ -4,6 +4,7 @@ import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/iot/v1"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
)
@@ -17,10 +18,13 @@ func (c *ControllerV1) TSimCardAllocateTargetList(ctx context.Context, req *v1.T
if err != nil {
return nil, err
}
return &v1.TSimCardAllocateTargetListRes{Items: items}, nil
return &v1.TSimCardAllocateTargetListRes{Items: items, MaxInputRows: model.SimCardAllocateMaxInputRows}, nil
}
func (c *ControllerV1) TSimCardAllocateInputPrepare(ctx context.Context, req *v1.TSimCardAllocateInputPrepareReq) (res *v1.TSimCardAllocateInputPrepareRes, err error) {
if err = req.ValidateInput(); err != nil {
return nil, err
}
data, err := service.IotTSimCardAllocate().PrepareInput(ctx, &req.TSimCardAllocatePrepareParam)
if err != nil {
return nil, err
@@ -0,0 +1,152 @@
package iot
import (
"bytes"
"context"
"io"
"time"
v1 "git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/iot/v1"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardquery"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/iot_excelize"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
// simCardQueryService 两个查询功能共用同一份实现,避免重复注册。
var simCardQueryService = simcardquery.New()
// SimCardBaseQueryTemplate 下载卡基础信息查询模板
func (c *ControllerV1) SimCardBaseQueryTemplate(ctx context.Context, req *v1.SimCardBaseQueryTemplateReq) (res *v1.SimCardBaseQueryTemplateRes, err error) {
file, name, err := simCardQueryService.BaseTemplate(ctx)
if err != nil {
return nil, err
}
defer file.Close()
return &v1.SimCardBaseQueryTemplateRes{}, iot_excelize.WriteHTTP(ctx, file, name)
}
// SimCardBaseQueryColumns 卡基础信息查询可选导出列
func (c *ControllerV1) SimCardBaseQueryColumns(ctx context.Context, req *v1.SimCardBaseQueryColumnsReq) (res *v1.SimCardBaseQueryColumnsRes, err error) {
return &v1.SimCardBaseQueryColumnsRes{Items: simCardQueryService.BaseColumnOptions()}, nil
}
// SimCardBaseQueryExport 提交卡基础信息查询导出
func (c *ControllerV1) SimCardBaseQueryExport(ctx context.Context, req *v1.SimCardBaseQueryExportReq) (res *v1.SimCardBaseQueryExportRes, err error) {
input, err := excelInput(ctx, req.Mode)
if err != nil {
return nil, err
}
submit, err := simCardQueryService.ExportBase(ctx, &req.SimCardQueryExportParam, input)
if err != nil {
return nil, err
}
return &v1.SimCardBaseQueryExportRes{SimCardQuerySubmitRes: toSubmitRes(submit)}, nil
}
// SimCardUsageHistoryTemplate 下载月历史用量查询模板
func (c *ControllerV1) SimCardUsageHistoryTemplate(ctx context.Context, req *v1.SimCardUsageHistoryTemplateReq) (res *v1.SimCardUsageHistoryTemplateRes, err error) {
file, name, err := simCardQueryService.UsageHistoryTemplate(ctx)
if err != nil {
return nil, err
}
defer file.Close()
return &v1.SimCardUsageHistoryTemplateRes{}, iot_excelize.WriteHTTP(ctx, file, name)
}
// SimCardUsageHistoryColumns 月历史用量查询固定列与近12个月月份清单
func (c *ControllerV1) SimCardUsageHistoryColumns(ctx context.Context, req *v1.SimCardUsageHistoryColumnsReq) (res *v1.SimCardUsageHistoryColumnsRes, err error) {
return &v1.SimCardUsageHistoryColumnsRes{
Items: simCardQueryService.UsageHistoryColumnOptions(),
Months: simCardQueryService.UsageHistoryMonths(time.Now()),
}, nil
}
// SimCardUsageHistoryDeptOptions 月历史用量查询可选公司
func (c *ControllerV1) SimCardUsageHistoryDeptOptions(ctx context.Context, req *v1.SimCardUsageHistoryDeptOptionsReq) (res *v1.SimCardUsageHistoryDeptOptionsRes, err error) {
items, err := simCardQueryService.DeptOptions(ctx)
if err != nil {
return nil, err
}
return &v1.SimCardUsageHistoryDeptOptionsRes{
Items: items,
CurrentDeptId: contexts.GetDeptId(ctx),
}, nil
}
// SimCardUsageHistoryExport 提交月历史用量查询导出
func (c *ControllerV1) SimCardUsageHistoryExport(ctx context.Context, req *v1.SimCardUsageHistoryExportReq) (res *v1.SimCardUsageHistoryExportRes, err error) {
input, err := excelInput(ctx, req.Mode)
if err != nil {
return nil, err
}
submit, err := simCardQueryService.ExportUsageHistory(ctx, &req.SimCardUsageHistoryExportParam, input)
if err != nil {
return nil, err
}
return &v1.SimCardUsageHistoryExportRes{SimCardQuerySubmitRes: toSubmitRes(submit)}, nil
}
// excelInput 只在 Excel 模式下读取上传文件。
// 一次性读进内存再交给后台解析:提交接口必须立即返回,
// 而 HTTP 请求结束后临时上传文件可能被清理,不能在后台协程里再读。
func excelInput(ctx context.Context, mode string) (simcardquery.ExcelInput, error) {
if mode != model.SimCardQueryModeExcel {
return nil, nil
}
request := g.RequestFromCtx(ctx)
if request == nil {
return nil, gerror.New("当前上下文不是HTTP请求")
}
file := request.GetUploadFile("file")
if file == nil {
return nil, gerror.New("请先上传 Excel 文件")
}
content, err := readUploadContent(file)
if err != nil {
return nil, err
}
if len(content) == 0 {
return nil, gerror.New("上传的 Excel 文件为空")
}
return func() (io.Reader, error) {
return bytes.NewReader(content), nil
}, nil
}
func toSubmitRes(submit *exportcenter.SubmitResult) *model.SimCardQuerySubmitRes {
if submit == nil {
return nil
}
return &model.SimCardQuerySubmitRes{
TaskId: submit.TaskId,
TaskNo: submit.TaskNo,
Total: submit.Total,
}
}
// maxUploadBytes Excel 上传大小上限(MB 换算后的字节数)。
// 20MB 足够容纳 20000 行三列号码,再大基本是用户传错文件。
const maxUploadBytes = 20 << 20
// readUploadContent 一次性读出上传文件内容。
func readUploadContent(file *ghttp.UploadFile) ([]byte, error) {
reader, err := file.Open()
if err != nil {
return nil, gerror.Wrap(err, "读取上传文件失败")
}
defer reader.Close()
if file.Size > maxUploadBytes {
return nil, gerror.Newf("上传文件超过 %dMB 上限", maxUploadBytes>>20)
}
content, err := io.ReadAll(reader)
if err != nil {
return nil, gerror.Wrap(err, "读取上传文件失败")
}
return content, nil
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -13,6 +13,7 @@ import (
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
)
// List 查看库存卡信息表列表
func (c *ControllerV1) TSimCardStockList(ctx context.Context, req *v1.TSimCardStockListReq) (res *v1.TSimCardStockListRes, err error) {
list, totalCount, err := service.IotTSimCardStock().List(ctx, &req.TSimCardStockListParam)
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot
@@ -1,7 +1,7 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @Author Lxj <xxx@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package iot