sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -8,12 +8,16 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) DeptExport(ctx context.Context, req *v1.DeptExportReq) (*v1.DeptExportRes, error) {
|
||||
return &v1.DeptExportRes{}, service.AgentDept().DeptExport(ctx, &req.AgentDeptListParam)
|
||||
}
|
||||
|
||||
func (c *ControllerV1) DeptList(ctx context.Context, req *v1.DeptListReq) (res *v1.DeptListRes, err error) {
|
||||
data, total, err := service.AgentDept().DeptList(ctx, &req.AgentDeptListParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.DeptListRes{PageResult: response.PageResult{Page: 1, PageSize: 5000, Total: total}, Data: data}, nil
|
||||
return &v1.DeptListRes{PageResult: response.PageResult{Page: req.Page, PageSize: req.PageSize, Total: total}, Data: data}, nil
|
||||
}
|
||||
|
||||
func (c *ControllerV1) DeptView(ctx context.Context, req *v1.DeptViewReq) (res *v1.DeptViewRes, err error) {
|
||||
|
||||
@@ -8,6 +8,10 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) UserExport(ctx context.Context, req *v1.UserExportReq) (*v1.UserExportRes, error) {
|
||||
return &v1.UserExportRes{}, service.AgentUser().UserExport(ctx, &req.AgentUserListParam)
|
||||
}
|
||||
|
||||
func (c *ControllerV1) UserList(ctx context.Context, req *v1.UserListReq) (res *v1.UserListRes, err error) {
|
||||
data, total, err := service.AgentUser().UserList(ctx, &req.AgentUserListParam)
|
||||
if err != nil {
|
||||
|
||||
@@ -51,3 +51,13 @@ func (c *ControllerV1) BatchExecutionLogDetailList(ctx context.Context, req *v1.
|
||||
res.PageResult.Total = totalCount
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ControllerV1) BatchExecutionLogSyncReview(ctx context.Context, req *v1.BatchExecutionLogSyncReviewReq) (res *v1.BatchExecutionLogSyncReviewRes, err error) {
|
||||
data, err := service.CommonBatchExecutionLog().SyncReview(ctx, &req.BatchExecutionLogSyncReviewParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(v1.BatchExecutionLogSyncReviewRes)
|
||||
res.BatchExecutionLogSyncReviewModel = data
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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 common
|
||||
@@ -69,6 +69,12 @@ func (c *ControllerV1) BatchMainCards(ctx context.Context, req *v1.BatchMainCard
|
||||
return
|
||||
}
|
||||
|
||||
// ExportWarehousing 导出入库批次列表(数据与入库管理页一致)
|
||||
func (c *ControllerV1) BatchMainExportWarehousing(ctx context.Context, req *v1.BatchMainExportWarehousingReq) (res *v1.BatchMainExportWarehousingRes, err error) {
|
||||
err = service.CommonBatchMain().ExportWarehousing(ctx, &req.BatchMainSuccessListParam)
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出批次主表列表
|
||||
func (c *ControllerV1) BatchMainExport(ctx context.Context, req *v1.BatchMainExportReq) (res *v1.BatchMainExportRes, err error) {
|
||||
err = service.CommonBatchMain().Export(ctx, &req.BatchMainListParam)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/common/v1"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) DownloadCenterList(ctx context.Context, req *v1.DownloadCenterListReq) (res *v1.DownloadCenterListRes, err error) {
|
||||
list, totalCount, err := service.CommonDownloadCenter().List(ctx, &req.DownloadCenterListParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*model.DownloadCenterListModel{}
|
||||
}
|
||||
res = new(v1.DownloadCenterListRes)
|
||||
res.Items = list
|
||||
res.PageResult.Page = req.Page
|
||||
res.PageResult.PageSize = req.PageSize
|
||||
res.PageResult.Total = totalCount
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ControllerV1) DownloadCenterRecent(ctx context.Context, req *v1.DownloadCenterRecentReq) (res *v1.DownloadCenterRecentRes, err error) {
|
||||
data, err := service.CommonDownloadCenter().Recent(ctx, &req.DownloadCenterRecentParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(v1.DownloadCenterRecentRes)
|
||||
res.DownloadCenterRecentModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ControllerV1) DownloadCenterDownload(ctx context.Context, req *v1.DownloadCenterDownloadReq) (res *v1.DownloadCenterDownloadRes, err error) {
|
||||
if err = service.CommonDownloadCenter().Download(ctx, &req.DownloadCenterTaskParam); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *ControllerV1) DownloadCenterRemove(ctx context.Context, req *v1.DownloadCenterRemoveReq) (res *v1.DownloadCenterRemoveRes, err error) {
|
||||
if err = service.CommonDownloadCenter().Remove(ctx, &req.DownloadCenterTaskParam); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new(v1.DownloadCenterRemoveRes), nil
|
||||
}
|
||||
@@ -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 common
|
||||
|
||||
@@ -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 common
|
||||
|
||||
@@ -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 common
|
||||
|
||||
@@ -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 common
|
||||
|
||||
@@ -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 common
|
||||
|
||||
@@ -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 common
|
||||
|
||||
@@ -55,3 +55,32 @@ func (c *ControllerV1) TMessageLogDelete(ctx context.Context, req *v1.TMessageLo
|
||||
err = service.CommonTMessageLog().Delete(ctx, &req.TMessageLogDeleteParam)
|
||||
return
|
||||
}
|
||||
|
||||
// UnreadList 获取当前用户未读消息通知
|
||||
func (c *ControllerV1) TMessageLogUnreadList(ctx context.Context, req *v1.TMessageLogUnreadListReq) (res *v1.TMessageLogUnreadListRes, err error) {
|
||||
list, totalCount, err := service.CommonTMessageLog().UnreadList(ctx, &req.TMessageLogUnreadListParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
list = []*model.TMessageLogUnreadListModel{}
|
||||
}
|
||||
|
||||
res = new(v1.TMessageLogUnreadListRes)
|
||||
res.Items = list
|
||||
res.Total = totalCount
|
||||
return
|
||||
}
|
||||
|
||||
// Read 标记当前用户指定消息已读
|
||||
func (c *ControllerV1) TMessageLogRead(ctx context.Context, req *v1.TMessageLogReadReq) (res *v1.TMessageLogReadRes, err error) {
|
||||
err = service.CommonTMessageLog().Read(ctx, &req.TMessageLogReadParam)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll 标记当前用户全部消息已读
|
||||
func (c *ControllerV1) TMessageLogReadAll(ctx context.Context, req *v1.TMessageLogReadAllReq) (res *v1.TMessageLogReadAllRes, err error) {
|
||||
err = service.CommonTMessageLog().ReadAll(ctx, &req.TMessageLogReadAllParam)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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 common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package iot
|
||||
// @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 common
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package gen
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package gen
|
||||
// @Link https://github.com/xiujiecn/xiu-admin
|
||||
// @Copyright Copyright (c) 2025 LiXiujie
|
||||
// @Author Lxj <li@xiujie.cn>
|
||||
// @Author Lxj <xxx@example.com>
|
||||
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
|
||||
// @AutoGenerate Version
|
||||
package system
|
||||
|
||||
Reference in New Issue
Block a user