From 5b17f47141fcca406d4fc074e3eba39fa874bd38 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Fri, 28 Aug 2026 10:51:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code-review-issues.md | 28 ++++++++++----------- internal/biz/system/api.go | 11 +++++--- internal/biz/system/authentication.go | 7 +++++- internal/biz/system/authentication_test.go | 17 +++++++++++++ internal/biz/system/export.go | 8 +++--- internal/config/runtime.go | 4 ++- internal/data/system/api.go | 2 +- internal/data/system/export.go | 2 +- internal/integration/runtimeconfig/store.go | 3 +++ internal/service/dto/authority.go | 2 +- internal/service/system/api.go | 6 ++--- internal/service/system/export.go | 4 +-- 12 files changed, 63 insertions(+), 31 deletions(-) diff --git a/docs/code-review-issues.md b/docs/code-review-issues.md index 7110345..72147a0 100644 --- a/docs/code-review-issues.md +++ b/docs/code-review-issues.md @@ -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 | 二轮 | diff --git a/internal/biz/system/api.go b/internal/biz/system/api.go index c52202f..193a52c 100644 --- a/internal/biz/system/api.go +++ b/internal/biz/system/api.go @@ -13,9 +13,12 @@ type API struct { Description string APIGroup string Method string - OrderKey string - Desc bool - StrictAll bool +} + +type APIFilter struct { + Path, Description, APIGroup, Method string + OrderKey string + 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 diff --git a/internal/biz/system/authentication.go b/internal/biz/system/authentication.go index e64c3c8..abbf240 100644 --- a/internal/biz/system/authentication.go +++ b/internal/biz/system/authentication.go @@ -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 { diff --git a/internal/biz/system/authentication_test.go b/internal/biz/system/authentication_test.go index d2887c6..2dd9eda 100644 --- a/internal/biz/system/authentication_test.go +++ b/internal/biz/system/authentication_test.go @@ -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) diff --git a/internal/biz/system/export.go b/internal/biz/system/export.go index 454ee14..7560377 100644 --- a/internal/biz/system/export.go +++ b/internal/biz/system/export.go @@ -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 diff --git a/internal/config/runtime.go b/internal/config/runtime.go index 503253f..59d85a1 100644 --- a/internal/config/runtime.go +++ b/internal/config/runtime.go @@ -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 { diff --git a/internal/data/system/api.go b/internal/data/system/api.go index 073d459..409f4e4 100644 --- a/internal/data/system/api.go +++ b/internal/data/system/api.go @@ -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() diff --git a/internal/data/system/export.go b/internal/data/system/export.go index 3c00e2c..ca48ac8 100644 --- a/internal/data/system/export.go +++ b/internal/data/system/export.go @@ -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 { diff --git a/internal/integration/runtimeconfig/store.go b/internal/integration/runtimeconfig/store.go index 6e5fef9..57a7b2b 100644 --- a/internal/integration/runtimeconfig/store.go +++ b/internal/integration/runtimeconfig/store.go @@ -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 diff --git a/internal/service/dto/authority.go b/internal/service/dto/authority.go index 1353057..0e4d4c8 100644 --- a/internal/service/dto/authority.go +++ b/internal/service/dto/authority.go @@ -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"` diff --git a/internal/service/system/api.go b/internal/service/system/api.go index 9e5b0dd..620a52b 100644 --- a/internal/service/system/api.go +++ b/internal/service/system/api.go @@ -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 } diff --git a/internal/service/system/export.go b/internal/service/system/export.go index 1ac54c9..444413c 100644 --- a/internal/service/system/export.go +++ b/internal/service/system/export.go @@ -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