优化结构
This commit is contained in:
parent
fec3820464
commit
5b17f47141
|
|
@ -54,15 +54,15 @@
|
|||
| # | 问题 | 位置 | 轮次 |
|
||||
|---|------|------|------|
|
||||
| ~~D-2(部分完成)~~ | ~~支付方式归一化骨架已统一到 `paymentkit.NormalizePaymentMethod`;各渠道状态词表与退款身份校验因语义不同保留。~~ | ~~integration/payment;internal/paymentkit~~ | ~~部分完成~~ |
|
||||
| D-4 | handler 四段式样板约 70 处(ShouldBindJSON→Fail→service→Write) | server/handler/* | 二轮 |
|
||||
| ~~D-4~~ | ~~经复核样板虽多,但各 handler 的绑定方式、错误文案、响应 envelope 和鉴权上下文差异明显;抽统一门面会隐藏 transport 语义并扩大回归面,暂不改动。~~ | ~~server/handler/*~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
| D-6 | mq/websocket 两包各写一套 map 解码 helper 且逐字符相同;TestConfig 探测骨架三处同构【属 2.2 排除范围交叉项】 | emqx.go:226-260 vs websocket/server.go:189-240 | 二轮 |
|
||||
|
||||
### 3.2 配置/数据不变式双份维护
|
||||
|
||||
| # | 问题 | 位置 | 轮次 |
|
||||
|---|------|------|------|
|
||||
| D-9 | config.Store 与 runtimeconfig.Store 各写一套同构 listener/通知/克隆机制【既定不采用原建议,保持分离——待写决策注释固化】 | config/runtime.go:29,150-196;runtimeconfig/store.go:62-167 | 一轮 |
|
||||
| D-10 | "storage/email 不落盘"不变式三重执行(persistConfigValues 置 nil+Delete、persistDatabaseConfig 再 Delete、removeIntegrationConfigFromFile 启动时又删) | config_store.go:45-47,102,106-122;data.go:279-283 | 一轮 |
|
||||
| ~~D-9~~ | ~~`config.Store` 与 `runtimeconfig.Store` 的同构 listener/通知/克隆机制经复核不合并:前者负责文件快照与 fsnotify 全量替换,后者负责数据库集成配置按 provider key 通知;已补决策注释固化边界。~~ | ~~config/runtime.go;integration/runtimeconfig/store.go~~ | ~~评估完成,保留分离(2026-08-28)~~ |
|
||||
| ~~D-10~~ | ~~经复核保留三处清理:`persistConfigValues` 覆盖完整运行时保存,`persistDatabaseConfig` 覆盖初始化页的局部写入,`removeIntegrationConfigFromFile` 覆盖启动时对旧模板的兼容清理;入口不同且各自可独立触发,合并会削弱不落盘不变式。~~ | ~~internal/data/config_store.go;data.go~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
|
||||
### 3.3 中小重复
|
||||
|
||||
|
|
@ -99,11 +99,11 @@
|
|||
| ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ |
|
||||
| ~~S-3(第一批)~~ | ~~routes.go 已改为有序注册表;各领域路由文件仍保留。~~ | ~~internal/server/router/routes.go~~ | ~~部分完成~~ |
|
||||
| ~~S-4~~ | ~~四个 data 子包的 provider 文件已合并。~~ | ~~internal/data/*/provider.go~~ | ~~已完成~~ |
|
||||
| S-5 | 巨微两极:biz/payment/payment.go 1150 行 vs 同域微文件;dto 超小文件 vs settings.go 170 行跨四域 | biz/payment、service/dto | 三轮 |
|
||||
| S-6 | dto 包组织混乱:system.go 混装三域;settings.go 横跨四域 | service/dto/system.go、settings.go | 一轮 |
|
||||
| S-7 | data 层组织纪律:转换函数命名四种风格;PO 分布无规则;audit.go 名不副实;runtime.go 拼盘 | data/system/* | 二轮 |
|
||||
| S-8 | media 域同域四文件 | biz/system/media* | 三轮 |
|
||||
| S-9 | 单方法 handler 各占结构体+Set 23 字段 | server/handler/session.go、navigation.go、set.go | 一轮 |
|
||||
| ~~S-5~~ | ~~经复核保留:payment 大文件承载跨供应商编排与契约,微文件分别对应独立边界;dto 合并会重新混装领域,收益不足。~~ | ~~biz/payment、service/dto~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
| ~~S-6~~ | ~~经复核保留:`system.go`/`settings.go` 虽跨域,但移动类型会放大 service DTO 导入与生成契约变化;本批不做机械拆分。~~ | ~~service/dto/system.go、settings.go~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
| ~~S-7~~ | ~~经复核保留:data 层转换与 PO 命名差异来自不同存储关系和历史兼容,统一命名需全域迁移,当前无安全局部收益。~~ | ~~data/system/*~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
| ~~S-8~~ | ~~经复核保留:media 四文件分别覆盖资源、元数据、上传会话与上传流程,职责边界清晰,不为减少文件合并。~~ | ~~biz/system/media*~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
| ~~S-9~~ | ~~经复核保留:单方法 handler 结构体由 Wire/路由注入约束形成,合并会改变构造与注册契约,暂不改动。~~ | ~~server/handler/session.go、navigation.go、set.go~~ | ~~评估完成,保留(2026-08-28)~~ |
|
||||
|
||||
## 六、包归属问题
|
||||
|
||||
|
|
@ -127,12 +127,12 @@
|
|||
| # | 问题 | 位置 | 轮次 |
|
||||
|---|------|------|------|
|
||||
| ~~L-3(部分完成)~~ | ~~PaymentResult/PaymentTestResult 的死 JSON 标签已移除;PaymentRequest 字段与指纹语义保留,Definition 家族因仍被 service 消费暂不迁移。~~ | ~~biz/payment/payment.go;biz/integration~~ | ~~部分完成~~ |
|
||||
| ~~L-4(部分完成)~~ | ~~`SystemParameter` 的查询字段与时间区间已拆为 `SystemParameterFilter`;API/Export 过滤字段仍待独立迁移。~~ | ~~biz/service/data system parameter~~ | ~~部分完成~~ |
|
||||
| ~~L-4~~ | ~~`SystemParameter` 的查询字段与时间区间已拆为 `SystemParameterFilter`;API/Export 过滤字段已分别迁移至独立 `APIFilter`、`ExportTemplateFilter`,实体 DO 不再承载列表过滤/排序字段。~~ | ~~biz/service/data system parameter、api、export~~ | ~~已完成(2026-08-28;针对性测试通过)~~ |
|
||||
| L-5 | middleware 硬编码业务语义:中文消息黑名单判断审计(改文案即改审计行为);业务路径硬编码;支付回调专用逻辑内嵌通用中间件;限流策略内联 | server/middleware/* | 二轮 |
|
||||
| L-6 | biz 契约泄漏存储/表现原语:QueryExport 返回 []map[string]any;export DO 携带 SQL 片段;UserOptions UI 形状;AuthenticationResult 携带密码哈希 | biz/system/* | 二轮 |
|
||||
| ~~L-6(部分完成)~~ | ~~`AuthenticationResult` 已在返回 service 前清空密码哈希并补回归测试。~~ `QueryExport` 动态行、export DO 的兼容 SQL 字段、`UserOptions` 选项形状仍保留:前两项涉及公开导入导出契约与存量数据兼容,后者虽命名偏 UI,但实际是稳定的 label/value 投影;当前直接迁移收益不足以覆盖契约风险。 | biz/system/* | ~~部分完成(2026-08-28;针对性测试通过)~~ |
|
||||
| L-7 | data 层纪律:Table("字符串") 绕过 PO;saveRelations 回写入参 DO;OriginSetting 裸转换 | data/system/* | 二轮 |
|
||||
| L-8 | 编排类文件过重:seedSystem 126 行 10 类职责;authority.go 四类职责(权限引擎应独立 accessGuard);BuildVersionBundle 五职责 | data/system/seed.go、authority.go、version.go | 二轮 |
|
||||
| L-9 | dto 契约问题:ID 类型三处分叉;AuthorityResponse.DeletedAt 泄漏;ErrorRecordMutationRequest 指针/值不自洽;DTO 反向依赖 biz 类型 | service/dto/* | 二轮 |
|
||||
| ~~L-9(部分完成)~~ | ~~`AuthorityResponse.DeletedAt` 已改为 `json:"-"`,不再泄漏软删字段。~~ ID 类型分叉涉及现有 handler/usecase/数据库键类型的兼容迁移;`ErrorRecordMutationRequest` 的指针字段用于区分省略与显式空值且已有测试,原建议不成立,保留;DTO 对 integration biz 字段定义的反向依赖真实存在,但迁移会改变配置元数据契约,留待独立处理。 | service/dto/* | ~~部分完成(2026-08-28;针对性测试通过)~~ |
|
||||
| L-10 | 错误体系双轨:errors.go 仅 3 个 kratos 类型错误其余 stdlib 散落 13+ 文件;Error+Unwrap 与 Error+Is 混用 | biz/system/errors.go 等 | 二轮 |
|
||||
| L-12 | 校验双轨制:handler 手工 if 与 dto binding 标签混用 | server/handler/* + service/dto/* | 一/三轮 |
|
||||
|
||||
|
|
@ -153,14 +153,14 @@
|
|||
|
||||
| # | 问题 | 位置 | 轮次 |
|
||||
|---|------|------|------|
|
||||
| X-1 | router 与 routecatalog 双声明(表驱动合并为单一声明源) | server/router/*;routecatalog/catalog.go | 一轮 |
|
||||
| ~~X-1~~ | ~~已完成影响分析,暂不实施单一声明源迁移:`routecatalog` 承载审计/Swagger/模块同步元数据,`router` 负责 Gin handler 绑定;当前 catalog 还无法表达 handler 注入与注册顺序,强行合并会扩大启动与路由回归面。~~ | ~~server/router/*;routecatalog/catalog.go~~ | ~~评估完成,保留分离(2026-08-28)~~ |
|
||||
| X-2 | 新增资源触碰 7 处 | — | 一轮 |
|
||||
| X-3 | 集成配置三形状两通道(storage/email 走 config.Store,mq/websocket 走 runtimeconfig) | data/integration_config.go 等 | 一轮 |
|
||||
| ~~X-3~~ | ~~已完成影响分析,暂不合并三种配置形状:storage/email 需要强类型 `config.Store` 快照与文件兼容,mq/websocket 需要按 provider 的 `runtimeconfig.Store` 热通知;统一形状会牺牲强类型校验或通知粒度。~~ | ~~data/integration_config.go 等~~ | ~~评估完成,保留分离(2026-08-28)~~ |
|
||||
| X-4 | swagger 运行时文档手拼仍在 server 根包 | server/swagger.go:21-159 | 一轮 |
|
||||
| X-6 | local 存储两套入口【既定不采用,保留】 | server/staticfiles、integration/storage/local.go | 一轮 |
|
||||
| X-7 | gopay_helpers.go 杂物间(渠道专属谓词/状态机应下沉各渠道文件) | integration/payment/gopay_helpers.go | 二轮 |
|
||||
| X-8 | vendor.go 14 键金额 DSL 投机通用性;18 字段全 Required【既定不采用,保留】 | integration/payment/vendor.go | 二轮 |
|
||||
| X-9 | config 热重载双通道:watchLoop 只换快照不重建客户端,全量重载需手动触发 | config/runtime.go:263-287 | 三轮 |
|
||||
| ~~X-9~~ | ~~已完成影响分析,现状并非“只换快照”:`watchLoop` 负责发布合并快照,`Data.reloadConfig` 显式重建数据库、Redis、Mongo、storage 与 integration runtime;两者分工避免文件 watcher 直接持有基础设施生命周期。合并为单通道需重做锁、回滚与连接退休策略,暂不改动。~~ | ~~config/runtime.go:263-287;data/config_store.go:124-252~~ | ~~评估完成,保留分离(2026-08-28)~~ |
|
||||
| X-10 | payment 新增渠道需改 4 处散弹式修改 | biz/payment 常量+adapter 工厂+配置定义 | 三轮 |
|
||||
| X-11 | TaskScheduler 多锁【真实并发需求,仅记录不改】 | worker/task_scheduler.go | 二轮 |
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ type API struct {
|
|||
Description string
|
||||
APIGroup string
|
||||
Method string
|
||||
}
|
||||
|
||||
type APIFilter struct {
|
||||
Path, Description, APIGroup, Method string
|
||||
OrderKey string
|
||||
Desc bool
|
||||
StrictAll bool
|
||||
Desc, StrictAll bool
|
||||
}
|
||||
|
||||
type APIRepo interface {
|
||||
|
|
@ -23,7 +26,7 @@ type APIRepo interface {
|
|||
UpdateAPI(context.Context, *API) error
|
||||
DeleteAPIs(context.Context, []uint) error
|
||||
FindAPI(context.Context, uint) (*API, error)
|
||||
ListAPIs(context.Context, int, int, *API) ([]*API, int64, error)
|
||||
ListAPIs(context.Context, int, int, *APIFilter) ([]*API, int64, error)
|
||||
APIRoleIDs(context.Context, string, string) ([]uint, error)
|
||||
SetAPIRoles(context.Context, string, string, []uint) error
|
||||
CheckPolicyStore(context.Context) error
|
||||
|
|
|
|||
|
|
@ -152,7 +152,12 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
|||
return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
|
||||
}
|
||||
}
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil
|
||||
// AuthenticationResult crosses the biz/service boundary; never retain the
|
||||
// credential hash in the user object returned to transport code. Keep the
|
||||
// repository-owned value untouched in case another caller shares it.
|
||||
resultUser := *user
|
||||
resultUser.Password = ""
|
||||
return &AuthenticationResult{User: &resultUser, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: resultUser.MustChangePassword}, nil
|
||||
}
|
||||
|
||||
func userHasAuthority(user *User, authorityID uint) bool {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,23 @@ func TestLoginRecordsSuccessBeforeMultipointCacheFailure(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoginResultDoesNotExposePasswordHash(t *testing.T) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user := &User{ID: 1, Username: "admin", Password: string(hash), AuthorityID: 888, Enable: 1}
|
||||
security := NewSecurityUsecase(&authenticationSecurityRepo{}, &authenticationCache{}, &authenticationSettings{}, nil)
|
||||
uc := NewAuthenticationUsecase(NewUserUsecase(&authenticationUserRepo{user: user}), security, &authenticationIssuer{}, nil)
|
||||
result, err := uc.Login(context.Background(), &LoginAttempt{Username: "admin", Password: "secret", IP: "127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.User.Password != "" {
|
||||
t.Fatalf("authentication result retained password hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordLoginAllowsMissingAuditRecorder(t *testing.T) {
|
||||
uc := &AuthenticationUsecase{}
|
||||
uc.recordLogin(context.Background(), &LoginAttempt{Username: "admin"}, false, "failed", 0)
|
||||
|
|
|
|||
|
|
@ -39,15 +39,17 @@ type ExportTemplate struct {
|
|||
Order string
|
||||
Conditions []ExportCondition
|
||||
Joins []ExportJoin
|
||||
StartCreatedAt *time.Time
|
||||
EndCreatedAt *time.Time
|
||||
}
|
||||
type ExportTemplateFilter struct {
|
||||
Name, TableName, TemplateID string
|
||||
StartCreatedAt, EndCreatedAt *time.Time
|
||||
}
|
||||
type ExportRepo interface {
|
||||
CreateExportTemplate(context.Context, *ExportTemplate) error
|
||||
UpdateExportTemplate(context.Context, *ExportTemplate) error
|
||||
DeleteExportTemplates(context.Context, []uint) error
|
||||
FindExportTemplate(context.Context, uint, string) (*ExportTemplate, error)
|
||||
ListExportTemplates(context.Context, int, int, *ExportTemplate) ([]*ExportTemplate, int64, error)
|
||||
ListExportTemplates(context.Context, int, int, *ExportTemplateFilter) ([]*ExportTemplate, int64, error)
|
||||
QueryExport(context.Context, *ExportTemplate, map[string]string) ([]map[string]any, string, error)
|
||||
PreviewExport(context.Context, *ExportTemplate, map[string]string) (string, error)
|
||||
ImportExportRows(context.Context, *ExportTemplate, []map[string]any) error
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import (
|
|||
var ErrConfigPathRequired = errors.New("configuration path is required")
|
||||
|
||||
// Store owns an immutable configuration snapshot and its Viper-backed file
|
||||
// watcher.
|
||||
// watcher. It intentionally remains separate from integration/runtimeconfig.Store:
|
||||
// this store replaces the whole file-backed application snapshot, while the
|
||||
// integration store publishes database-backed provider changes by key.
|
||||
// Every accessor returns a deep copy, so readers can safely use it without
|
||||
// coordinating with a reload in another goroutine.
|
||||
type Store struct {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*system.API, error) {
|
|||
}
|
||||
return apiFromPO(po), nil
|
||||
}
|
||||
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *system.API) ([]*system.API, int64, error) {
|
||||
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *system.APIFilter) ([]*system.API, int64, error) {
|
||||
db := r.data.DB().WithContext(ctx).Model(&apiPO{})
|
||||
if q != nil && q.StrictAll {
|
||||
config := r.data.Runtime().Admin()
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
|
|||
}
|
||||
return exportFromPO(po, conditions, joins), nil
|
||||
}
|
||||
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *system.ExportTemplate) ([]*system.ExportTemplate, int64, error) {
|
||||
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *system.ExportTemplateFilter) ([]*system.ExportTemplate, int64, error) {
|
||||
db := r.data.DB().WithContext(ctx).Model(&exportTemplatePO{})
|
||||
if q != nil {
|
||||
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ type listener struct {
|
|||
}
|
||||
|
||||
type Store struct {
|
||||
// This store is intentionally not merged with config.Store. Integration
|
||||
// settings are database-backed and listeners subscribe by provider key,
|
||||
// whereas config.Store owns file snapshots and fsnotify lifecycle.
|
||||
mu sync.RWMutex
|
||||
values map[string]Config
|
||||
listeners map[uint64]listener
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type SetDataScopeRequest struct {
|
|||
type AuthorityResponse struct {
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt *time.Time `json:"DeletedAt"`
|
||||
DeletedAt *time.Time `json:"-"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
AuthorityName string `json:"authorityName"`
|
||||
ParentID *uint `json:"parentId"`
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func apiResponses(items []*system.API) []*dto.APIResponse {
|
|||
}
|
||||
|
||||
func (s *APIService) ListAPI(ctx context.Context, req *dto.APIListRequest) ([]*dto.APIResponse, int64, error) {
|
||||
items, total, err := s.uc.ListAPIs(ctx, req.Page, req.PageSize, &system.API{Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method, OrderKey: req.OrderKey, Desc: req.Desc})
|
||||
items, total, err := s.uc.ListAPIs(ctx, req.Page, req.PageSize, &system.APIFilter{Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method, OrderKey: req.OrderKey, Desc: req.Desc})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ func (s *APIService) ListAPI(ctx context.Context, req *dto.APIListRequest) ([]*d
|
|||
}
|
||||
|
||||
func (s *APIService) AllAPI(ctx context.Context, strict bool) ([]*dto.APIResponse, error) {
|
||||
items, _, err := s.uc.ListAPIs(ctx, 0, 0, &system.API{StrictAll: strict})
|
||||
items, _, err := s.uc.ListAPIs(ctx, 0, 0, &system.APIFilter{StrictAll: strict})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ func (s *APIService) Groups(ctx context.Context) ([]string, map[string]string, e
|
|||
// The compatible group endpoint uses an unqualified Find, whose natural order is
|
||||
// the primary-key ascending order. GetAllApis intentionally uses id DESC,
|
||||
// so do not reuse it here or the group/dropdown order changes.
|
||||
items, _, err := s.uc.ListAPIs(ctx, 0, 0, &system.API{OrderKey: "id"})
|
||||
items, _, err := s.uc.ListAPIs(ctx, 0, 0, &system.APIFilter{OrderKey: "id"})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ func (s *ExportService) UpdateRequest(ctx context.Context, req *dto.ExportTempla
|
|||
return s.Update(ctx, exportTemplateDomain(req))
|
||||
}
|
||||
func (s *ExportService) TemplatesFilter(ctx context.Context, page, size int, name, tableName, templateID string, start, end *time.Time) ([]*dto.ExportTemplateResponse, int64, error) {
|
||||
return s.Templates(ctx, page, size, &system.ExportTemplate{Name: name, TableName: tableName, TemplateID: templateID, StartCreatedAt: start, EndCreatedAt: end})
|
||||
return s.Templates(ctx, page, size, &system.ExportTemplateFilter{Name: name, TableName: tableName, TemplateID: templateID, StartCreatedAt: start, EndCreatedAt: end})
|
||||
}
|
||||
func exportDTO(v *system.ExportTemplate) *dto.ExportTemplateResponse {
|
||||
var conditions []dto.ExportConditionResponse
|
||||
|
|
@ -119,7 +119,7 @@ func (s *ExportService) Template(ctx context.Context, id uint, tid string) (*dto
|
|||
}
|
||||
return exportDTO(v), nil
|
||||
}
|
||||
func (s *ExportService) Templates(ctx context.Context, page, size int, q *system.ExportTemplate) ([]*dto.ExportTemplateResponse, int64, error) {
|
||||
func (s *ExportService) Templates(ctx context.Context, page, size int, q *system.ExportTemplateFilter) ([]*dto.ExportTemplateResponse, int64, error) {
|
||||
items, total, err := s.uc.ListExportTemplates(ctx, page, size, q)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
|
|
|||
Loading…
Reference in New Issue