优化结构
This commit is contained in:
parent
9b1daaf3f6
commit
295ee5c49e
|
|
@ -9,17 +9,20 @@ agents must follow when changing the template.
|
||||||
cmd/ Entrypoint, Wire injector, main.go.
|
cmd/ Entrypoint, Wire injector, main.go.
|
||||||
configs/ Runtime config (config.yaml). No secrets.
|
configs/ Runtime config (config.yaml). No secrets.
|
||||||
internal/config/ Viper config models, loading, snapshots, and reloads.
|
internal/config/ Viper config models, loading, snapshots, and reloads.
|
||||||
internal/server/ Gin/Kratos handlers, middleware, routers, static files.
|
internal/logging/ Structured logging sinks and source metadata.
|
||||||
|
internal/server/ Gin handlers, middleware, routers, static files.
|
||||||
internal/service/dto/ Hand-written request and response DTOs.
|
internal/service/dto/ Hand-written request and response DTOs.
|
||||||
internal/service/ DTO/DO transport adapters.
|
internal/service/ DTO/DO transport adapters.
|
||||||
internal/biz/ Domain models, usecases, repo interfaces, errors.
|
internal/biz/ Domain models, usecases, repo interfaces, errors.
|
||||||
internal/data/ Repo implementations, database clients, migrations.
|
internal/data/ Repo implementations, database clients, migrations.
|
||||||
internal/initialize/ First-install and configuration orchestration.
|
internal/initialize/ First-install and configuration orchestration.
|
||||||
internal/integration/ External I/O adapters (cache, email, payment, storage).
|
internal/integration/ External I/O adapters (cache, email, payment, storage).
|
||||||
|
internal/paymentkit/ Shared payment normalization, signing, and JSON helpers.
|
||||||
internal/modules/ Built-in module schema, seed, menu, API, and task contributions.
|
internal/modules/ Built-in module schema, seed, menu, API, and task contributions.
|
||||||
internal/routecatalog/ HTTP route metadata and runtime policies.
|
internal/routecatalog/ HTTP route metadata and runtime policies.
|
||||||
internal/worker/ Timed-task runtime and scheduler.
|
internal/worker/ Timed-task runtime and scheduler.
|
||||||
internal/utils/ Stateless internal helpers.
|
internal/utils/ Stateless internal helpers.
|
||||||
|
docs/ Review notes and operational documentation.
|
||||||
pkg/ Reusable infrastructure packages.
|
pkg/ Reusable infrastructure packages.
|
||||||
web/ Vue administration frontend.
|
web/ Vue administration frontend.
|
||||||
```
|
```
|
||||||
|
|
@ -75,7 +78,7 @@ design rather than add the import.
|
||||||
- Owns the DO (`type <Resource> struct` — no proto, no storage tags),
|
- Owns the DO (`type <Resource> struct` — no proto, no storage tags),
|
||||||
the usecase, and the repo interface (`type <Resource>Repo interface`).
|
the usecase, and the repo interface (`type <Resource>Repo interface`).
|
||||||
- Owns typed errors built with `errors.NotFound` / `errors.BadRequest`
|
- Owns typed errors built with `errors.NotFound` / `errors.BadRequest`
|
||||||
plus the API error reason enum.
|
plus stable reason strings.
|
||||||
- Owns `ListOption` helpers — `ListFilter`, `ListOrderBy`, `ListOffset`,
|
- Owns `ListOption` helpers — `ListFilter`, `ListOrderBy`, `ListOffset`,
|
||||||
`ListLimit` — so callers compose queries without leaking storage
|
`ListLimit` — so callers compose queries without leaking storage
|
||||||
primitives.
|
primitives.
|
||||||
|
|
@ -99,7 +102,7 @@ design rather than add the import.
|
||||||
|
|
||||||
**server**
|
**server**
|
||||||
|
|
||||||
- Construct HTTP/gRPC servers, apply middleware, register services. No
|
- Construct HTTP servers, apply middleware, register services. No
|
||||||
translation, no business logic.
|
translation, no business logic.
|
||||||
|
|
||||||
### Add-a-resource checklist
|
### Add-a-resource checklist
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# 代码审查问题清单(internal + pkg)
|
# 代码审查问题清单(internal + pkg)
|
||||||
|
|
||||||
- 审查日期:2026-08-27 ~ 2026-08-28,共八轮全量审查。第八轮(2026-08-28):核查第七轮划线项 + 全新视角回归审查;`go build ./...` 编译验证通过
|
- 审查日期:2026-08-27 ~ 2026-08-28,共十轮全量审查。第十轮(2026-08-28):核查第九轮划线项(18 项)+ 全新视角回归审查;`go build ./...` 编译验证通过
|
||||||
- 文档结构:只保留待修复问题,按**类型**归类;每条标注发现轮次;已修复并经复查确认的、经评估保留的、用户决策不修的直接删除
|
- 文档结构:只保留待修复问题,按**类型**归类;每条标注发现轮次;已修复并经复查确认的、经评估保留的、用户决策不修的直接删除
|
||||||
- gva/ 目录是遗留参考库(独立 module 不参与 kra 编译),不在审查范围
|
- gva/ 目录是遗留参考库(独立 module 不参与 kra 编译),不在审查范围
|
||||||
- 依赖方向合规:pkg 无 import internal;integration 不 import data/service;data 不 import integration/payment;无循环依赖
|
- 依赖方向合规:pkg 无 import internal;integration 不 import data/service;data 不 import integration/payment;无循环依赖
|
||||||
|
- 第十轮总评:主干(支付幂等、哨兵回填、审计管道、路由目录)结构扎实;但"竞态保护/孤儿清理/指纹统一"三组修复各留保护边界缺口,物理删除化在注释与死字段上留下碎屑;三条机制线各自"半闭环":净化只管 msg 不管 data、ctx 键一半前置一半后置、脱敏键表三套规则并存
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -11,101 +12,106 @@
|
||||||
|
|
||||||
| # | 问题 | 位置 | 轮次 |
|
| # | 问题 | 位置 | 轮次 |
|
||||||
|---|------|------|------|
|
|---|------|------|------|
|
||||||
| V-15 | **handler 层 service 错误泄露 86 处(八轮直接 Grep 复核:上轮划线声称已修复,实际 0/86 收敛)**:organization.go 16、version.go 15、parameter.go 7、authority.go 9、payment.go 11(:87/:90 TestProvider 泄露供应商网关/SDK 细节最重)、integration_config.go 5、media.go 5(分片四端点 222/263/281/298)、task.go 4、menu.go 5、api.go:137,156、user.go:150,238、dictionary.go:103、export.go:201、api_token.go:26、system_config.go:77、permission.go:48。库内已有完整可复制样板 failLogViewer(audit.go:212-232)但零推广;version.go 的 stage 分类只分类了文案未隐藏内容。注意绑定错误透传(131 处)属合理不计 | server/handler/organization.go:24-248 等 | 六轮发现,八轮复核**仍未修复** |
|
| V-15 | **错误泄露收敛(envelope 净化)残留四个洞**:① **"失败:"截断只匹配半角冒号**——media.go:188 与 menu.go:65 用全角":"拼接绕过;② **17 处截断不可命中的调用漏网**(无冒号紧贴 15 处:api.go:137,156、authority.go 9 处、menu.go:170,178,198、user.go:150;全角 2 处)——非黑名单底层错误(x509/驱动方言)原样透出;③ **payment TestProvider data 字段绕过**——data/payment/payment.go:79-139 各阶段仍把原始 err.Error() 写入 Stages[].Message,而 Write 只净化 msg、**CodeError 的 data 通道完全无净化机制**(:87 是当前唯一实例但机制敞开);④ **净化静默无日志**——response.go 无日志导入,替换发生无记录("不泄露也不可排障")。另有误伤:黑名单含 provider/sdk/connection/timeout 等宽泛词会误伤合法业务消息;约 45 处半角冒号调用的 biz 可读提示被截断抹平。净化逻辑零测试 | server/httpx/response.go:32-57;data/payment/payment.go:79-139;handler/payment.go:87 | 六轮发现,十轮复核部分修复(白名单空操作已修、record not found 已补)+四洞 |
|
||||||
| V-11 | **脱敏退化洞(truncated 标志已加但实现有两处缺陷)**:① **audit 侧守卫恒假**——access_log.go:100 在 `c.Next()` 之前把 `writer.Truncated()` 按值拷贝进 ctx(恒 false),OperationAudit 在 audit.go:81 读到的永远是 false → 该守卫成死代码,audit 侧仍只剩 redactJSON 的 `>`(非 `>=`)兜底,logLimit≥1MiB 时截断明文照旧入库;② **Write 边界漏置标志**——capture.go:32 `if w.body.Len() < limit` 守卫下,buffer 恰好写满后任何后续写入跳过整个块且不置 truncated → 恰满截断的 JSON 敏感片段明文泄漏(access_log 与 audit 两路都漏);③ 全仓库零 truncated 相关测试。修法:ctx 值移到 c.Next() 后或调用时求值;`body.Len() >= limit && len(data) > 0` 时补置位 | server/middleware/capture.go:27-42;access_log.go:100,113-117;audit.go:78-83 | 六轮发现,八轮复核部分修复+新缺陷 |
|
| V-32 | **(新发现,高)删除审计日志的接口自身无审计标记(清痕无痕)**:`DELETE /sysOperationRecord/deleteSysOperationRecord` 与 `deleteSysOperationRecordByIds`(catalog.go:59-60)均无 audit:true(同表其他删除接口都有);`POST /attachmentCategory/deleteCategory`(:138)同样缺失——删除审计记录本身是典型清痕操作,审计矩阵修复的遗漏点 | routecatalog/catalog.go:59-60,138 | 十轮 |
|
||||||
| V-19 | **掩码/恢复共享键表已落地(biz IsIntegrationSecretKey 三层同源、key_pem 已命中、更新分支对称)——但残留四条**:① **新建分支破坏链仍在**:service 层空值也被掩码(:78-79 无条件 `******`)+ List 对未配置渠道回显 defaults(空秘密字段→显示 ******)+ data 新建分支(:158-169)无 merge 恢复 → 用户对未配置渠道只填非秘密字段保存即 `mch_key="******"` 落库,运行时拿 ****** 当真密钥;② service 层掩码值仍硬编码 `"******"` 未引用 config.MaskedSecret;③ connectivity.go:60-65 恢复集合仍只认 definition.Fields[].Secret(未并入共享键表)——MQ/WS 自定义秘密键(token/api_key)Test 时拿 ****** 当真值误报失败;④ data 层 mask 不递归数组(merge 递归)——靠 service 层兜底的巧合安全。另有误伤:routing_key/app key_id 命中 `_key` 后缀被掩码(key_id 是标识符非密钥,UI 无法核对) | service/integration/integration_config.go:33-41,78-79;data/integration/integration_config.go:158-169,224-237;integration/connectivity.go:60-85;biz/integration/integration_config.go:398-403 | 六轮发现,八轮复核主体修复+残留链 |
|
| V-33 | **(新发现,中高)支付指纹严格相等校验对存量订单幂等重放的冲突风险**:biz/payment/payment.go:652 `order.RequestFingerprint != paymentOrderFingerprint(req, extra)` 严格相等、无算法版本/迁移字段——Z-3 指纹统一前存量 initialized/pending 订单若由旧算法(data 层整结构 marshal)写入指纹,客户端超时重放同 TradeNo 将报"支付订单参数冲突"而非幂等短路,**存量未支付订单无法重新拉起支付**。建议对 initialized 态提供指纹重算/兼容窗口 | biz/payment/payment.go:645-657;data/payment/payment_order.go:135-156 | 十轮 |
|
||||||
| V-20 | 前端 token 拼入 URL(console.log 已删):完整登录 JWT 仍拼进二维码 URL——扫码设备地址栏/浏览器历史留存,且 token 是账户主凭证(可调用该账号权限内一切接口,不止上传);无轮换/自动关闭机制 | web/src/components/upload/QR-code.vue:55;scanUpload.vue:111-115 | 六轮发现,八轮复核部分修复(仅控制台) |
|
| V-24 | 串行锁主体已修——**但 Delete↔Create 竞态窗口仍开**:锁仅覆盖 Delete(media.go:126-143),引用同一 Key 的写入路径均不持锁(秒传 InitUpload→CreateMedia、普通 Upload 引用计数+建记录、CompleteUpload 的 CreateMedia)——T1 Delete 读 count=1 判定删除、T2 秒传 CreateMedia 复制同 Key 落库、T1 files.Delete → T2 新记录指向已删文件。锁为进程级 sync.Mutex,多副本部署跨进程无效 | biz/system/media.go:50,111-143;media_upload.go:100-107,262 | 八轮发现,十轮复核主体修复+Create 路径缺口 |
|
||||||
| V-13b | Swagger 门禁残留三点:① **fail-open**——配置缺失/未知 env 值(如 prod-cn、拼写错误)即注册(应 fail-close:仅已知安全值才注册);② env 只在引擎构造时读一次,**运行时切 production 不会摘除已注册路由**(cmd/main.go:165 还无条件打印 swagger 地址,生产日志误导);③ 零测试(无 env=production 断言不注册的用例) | server/gin.go:64-68;cmd/main.go:165 | 七轮发现,八轮复核部分修复(env 集合已扩) |
|
| V-34 | **(新发现,中)CleanupStale 先删 DB 后删文件且吞错 → 存储孤儿无重试路径**:biz/media_upload.go:295-299 先 `DeleteUploadData`(事务内物理删 session+chunks)再 `files.DeletePrefix`(_ = 吞错)——存储删除失败时 session 行已物理删除,**下轮扫描永远不再命中**,孤儿分片对象永久滞留(无孤儿反向扫描器) | biz/system/media_upload.go:295-299 | 十轮 |
|
||||||
| V-23 | **media 上传两表软删行无限膨胀**:media_uploads/media_upload_chunks 的 PO 均带 gorm.DeletedAt,取消/完成/回收三条路径全部只置 deleted_at,无任何物理清理任务(ClearDB 只清 operationPO/jwtBlacklistPO)——高频大文件分片上传下 chunks 表持续膨胀 | data/system/media_upload.go:13-40,95-133;data/system/maintenance.go:18-25 | 八轮 |
|
| V-35 | **(新发现,中)TaskScheduler 启动 Reload 失败仅 Warn 无重试**:worker/task_scheduler.go:93-95——DB 启动期闪断 → 任务表加载失败 → 调度器空转且不重试(Start 阻塞在 :99 `<-runContext.Done()`),**定时任务全部静默丢失**直至人工 reloadSystem | worker/task_scheduler.go:93-99 | 十轮 |
|
||||||
| V-24 | **媒体删除引用计数 TOCTOU 竞态**:秒传两记录共享同一 Key,并发删除时双方都可能读到 count=2 而均不删底层对象 → 存储孤儿(顺序删除路径正确) | biz/system/media.go:126-135 | 八轮 |
|
| V-19 | **证书别名缺口未修(三层全缺)**:`*_content`/`*_path` 别名(app_cert_content/root_cert_content/pkcs12_content/cert_content/key_content/cert_path 等)不命中 IsIntegrationSecretKey(不含 secret 词根、后缀非 _cert/_key)、不在 definition.Fields → data/service/middleware 三层键表均不掩码,API 直连提交明文落库并回显;connectivity restoreMaskedSecrets 同样不识别(掩码值被当真值测试连接)。误伤项(routing_key/key_id)已消除(键表无裸 _key 项)。**修法:键表补别名一处三层受益** | biz/integration/integration_config.go:325-328,423-431,398-409 | 六轮发现,十轮复核未修 |
|
||||||
| V-25 | 回收与合并竞态(低概率):StaleUploadSessionIDs 含 merging 状态——合并超过 TTL 时回收任务删 chunk 记录与分片文件,正在执行的 CompleteUpload 中途失败;claim 会刷 updated_at 使窗口极小但无法区分孤儿 merging 与活动会话 | data/system/media_upload.go:123;biz/system/media_upload.go:193-273 | 八轮 |
|
| V-13b | swagger 残留:① **空 env fail-open 未修**(gin.go:68 `env == ""` 仍注册,生产漏配 env 即暴露);② **双清单判定不一致**——gin.go 白名单 {空,development,dev,test,local} vs main.go:107-110 黑名单 {production,prod,live,staging},自定义 env(qa/uat)时路由不注册但启动日志仍打印 swagger URL(:171 日志谎报);③ env 门禁零测试 | server/gin.go:64-70;cmd/main.go:105-126,171 | 七轮发现,十轮复核未修 |
|
||||||
| V-16b | 词表主体已修(douyin/lakala/alipay 三处补齐、8 渠道核心 5 词全对齐)——**外围词不齐+测试缺口**:applet 仅 wechat_v2/v3/douyin 有(alipay/lakala/saobei 报不支持);allinpay 整个 jsapi/mini 分支缺失(渠道能力还是词表遗漏需确认);alipayCreateMethod 零单测、saobei/allinpay 无正向映射用例、三处新同义词全部无用例 | integration/payment/douyin.go:116、lakala.go:41、alipay.go:441、allinpay.go:212-221 | 七轮发现,八轮复核部分修复 |
|
| V-25 | merging 阈值区分✓——残留:合并超 TTL+1h 会话被物理回收后 CompleteUploadSession Updates 影响 0 行无 RowsAffected 校验 → media 已建但会话记录丢失、秒传断链无错误暴露 | data/system/media_upload.go:92-94,121-125 | 八轮发现,残留 |
|
||||||
|
| V-36 | **(新发现,低中)ErrorAudit 客户端失败词表含 "token" 会吞 5xx**:无 privateErrors 且 msg 含 "token" 的服务端错误(如 "token 生成失败: redis connection refused")被判为预期客户端失败跳过 sys_error 记录——服务端故障静默 | server/middleware/error_audit.go:73,43-45 | 十轮 |
|
||||||
|
| V-37 | **(新发现,低)分片上传秒传复制丢失 CategoryID**:media_upload.go:100 copy 未带 CategoryID(InitUpload 签名无 category 参数,单文件 Upload 支持 media.go:68)——分片上传的媒体记录分类恒为默认值,功能不对齐 | biz/system/media_upload.go:87,100 vs media.go:68 | 十轮 |
|
||||||
|
| V-27 | database_list 按别名回退已修✓(byName 映射+无按下标)、空密码不掩码✓——**蟑螂定律残留**:① **redis_list 仍按下标回退**(configuration.go:439-441 `previous == nil && index < len(current)`)——重排/头部插入时掩码条目继承旧列表同位置密码;② 别名匹配失败(改别名/新增条目/存量空 AliasName)时哨兵字面量直接落盘(data 层重建 DSN 密码变 `******` 连接失败)——无哨兵落盘拒绝校验 | internal/initialize/configuration.go:427-446,414-417 | 九轮发现,十轮复核部分修复+redis_list 残留 |
|
||||||
|
| V-30 | AlipayV3/WechatV2 required 已对齐✓——**douyin platform_serial_no 仍 required=false**(definition.go:167)而校验器启用时强制(integration_config.go:344-347),前端不标必填、保存时才报错;同构的 WechatV2 client_cert 标了 true,两处标准不一致 | biz/integration/integration_config_definition.go:167 | 九轮发现,十轮复核部分修复 |
|
||||||
|
| V-20 | 前端 token 拼入 URL(完整登录 JWT 拼二维码 URL,账户主凭证泄露面;无轮换机制) | web/src/components/upload/QR-code.vue:55 | 六轮,未修复 |
|
||||||
|
| V-16b | 词表与八轮一致——applet 仅 wechat_v2/v3/douyin 有;allinpay 无 jsapi/mini 分支;测试缺口仍在 | integration/payment 各渠道文件 | 七轮,未修复 |
|
||||||
|
| V-38 | (新发现,低)AGENTS.md 错误契约 vs 媒体模块实践:biz "typed errors(errors.NotFound/BadRequest)"约定下,media.go:52 ErrMediaTooLarge、media_upload.go:21 ErrUploadSessionNotFound 为裸 errors.New(同包 errors.go:9 ErrMediaNotFound 是规范风格),前端无法区分错误类别 | biz/system/media.go:52、media_upload.go:21 | 十轮 |
|
||||||
|
| V-31 残留 | FindMedia 已映射 ErrMediaNotFound✓——同包 UpdateMediaName(media.go:125-127)仍透传 gorm 原文(404 语义丢失,靠黑名单兜底为"操作失败") | data/system/media.go:125-127 | 九轮,部分修复 |
|
||||||
|
|
||||||
## 二、死代码与碎屑
|
## 二、死代码与碎屑
|
||||||
|
|
||||||
| # | 问题 | 位置 | 轮次 |
|
| # | 问题 | 位置 | 轮次 |
|
||||||
|---|------|------|------|
|
|---|------|------|------|
|
||||||
| Z-9 | **V-11 修复引入的新死代码**:audit.go:81-83 truncated 检查恒假(access_log.go:100 在 c.Next 前取值所致)——恰在 Z-1 刚清掉旧死代码的位置引入新死代码;另 access_log.go:114-117 先对截断体做完整 redactJSON(unmarshal+mask+marshal)再用占位符覆盖——超限时 marshal 白做的模式在修复代码中复现,应先判 Truncated() | server/middleware/audit.go:81-83;access_log.go:114-117 | 八轮 |
|
| Z-18 | **(新发现,中)DeleteUploadSession 注释与代码直接矛盾**:注释写 "retaining the soft-deleted session for audit/recovery"(软删保留审计),代码实为 Unscoped().Delete 物理删除——V-23 修复后未同步的过时注释,直接误导维护者(媒体记录同理 media.go:130-132) | data/system/media_upload.go:95-98;data/system/media.go:130-132 | 十轮 |
|
||||||
| Z-2 | 两个 migration step 重复播种同一条 test API(ensureCommunicationSurface:88 已含+已授权,ensureCommunicationTestSurface:145-153 再种一遍)——新装环境纯冗余、存量环境补种后永久空转 | data/system/migrations.go:88,145-153 | 六轮发现,未修复 |
|
| Z-15 | 软删字段死重量:mediaPO/categoryPO/uploadChunkPO/uploadSessionPO 的 DeletedAt+索引在全部删除路径改 Unscoped 后成死配置(每次默认查询仍附加 deleted_at IS NULL、索引仍写入);**连带**:UpsertChunk 的 DoUpdates 仍重置 deleted_at 列(复活分支不可达,:104);历史软删行无迁移清理(修复前取消产生的存量软删行永不回收) | data/system/media.go:16,34;media_upload.go:17,35,104 | 九轮,未修复 |
|
||||||
| Z-3 | 支付指纹两套算法并存:paymentTestFingerprint(整结构 marshal)与 paymentOrderFingerprint(白名单字段+extra)——测试单号随机不参与幂等,无害碎屑 | data/payment/payment.go:227-231 vs biz/payment/payment.go:957-966 | 六轮发现,未修复 |
|
| Z-9 | V-11 修复后死通路仍在:audit.go:76 ctxRespTextKey 分支生产链路不可达(后置键);:83 ctxRespTruncatedKey 同为死读——buffer 共享引用已成实际通路(:78-81),显式标志语义丢失;access_log.go:114-117 仍先对截断体完整 redactJSON 再覆盖占位符(白做功) | server/middleware/audit.go:76-85;access_log.go:110-121,114-117 | 八轮,十轮复核仍在 |
|
||||||
| Z-5 | error_audit.go:25 单行复合布尔(&&/\|\| 混三个豁免规则+auditPersistFailed 前缀,结果正确但三秒规则不达标) | server/middleware/error_audit.go:25 | 六轮发现,未修复 |
|
| Z-6 | 测试缺口群(十轮更新):sanitizeFailureMessage 分类逻辑(response_test 仅测透传分支);洋葱 ctx 链/truncated 传递;swagger env 门禁;service/integration 目录零测试(键表行为无防护);词表用例;OperationAudit 端到端;NormalizePaymentMethod 单测;CORS allow-all | 各处 | 六轮发现,十轮扩展 |
|
||||||
| Z-6 | 测试缺口群:OperationAudit 端到端测试;paymentCreateRequiresNotifyURL 测试;NormalizePaymentMethod 直接单元测试;支付词表同义无用例(V-16b);CORS allow-all 无回归测试;truncated 无测试(V-11);V-17 秒传语义无回归;V-18 回收状态范围无用例;V-13b env 门禁无用例;IsIntegrationSecretKey 无单测;service/integration 目录零测试文件 | 各处 | 六轮发现,八轮扩展 |
|
| Z-2 | 两个 migration step 重复播种同一条 test API | data/system/migrations.go:88,145-153 | 六轮,未修复 |
|
||||||
| Z-7 | audit.go 重复表达式未折叠::95 与 :98 逐字相同的 `operationBody = operationRequestBody(...)`(死赋值部分已删) | server/middleware/audit.go:95,98 | 七轮发现,八轮复核部分修复 |
|
| Z-11 | biz media Upload 死逻辑(MediaKeyReferences 对新 uuid key 恒 0,count==0 分支永真,埋雷) | biz/system/media.go:107-117 | 八轮,未修复 |
|
||||||
| Z-8 | routecatalog 描述含糊:getSysParam 与 getSysParamsList 描述均为「获取参数列表」 | routecatalog/catalog.go:118-119 | 七轮,未修复 |
|
| Z-13 残留 | rate_limit 魔法数字已修✓——cors.go:13-14 逗号空格不一致、audit.go:104 单行 13 字段调用、audit.go:46-47 空 else-if 三项未修 | server/middleware/cors.go:13-14、audit.go:46-47,104 | 八轮,部分修复 |
|
||||||
| Z-10 | 前端残留 console.log 两处(V-20 清理遗漏):common.vue:72(`upload file check result`);scanUpload.vue:117(`err`,且 :113 有注释掉的含 token 的 log,反注释即泄露) | web/src/components/upload/common.vue:72;web/src/view/media/scanUpload.vue:113,117 | 八轮 |
|
| Z-16 | BodyPolicyIntegrationConfig 中转标记(catalog 内声明→标记→运行时翻译回 payment_config/默认)——已补设计注释(:424-425)但中转设计未变 | routecatalog/catalog.go | 九轮,未修复 |
|
||||||
| Z-11 | biz media Upload 死逻辑:MediaKeyReferences 对刚生成的 uuid 新 key 计数恒为 0,`if count == 0` 分支永真;若未来 key 改为可复用,count>0 时会返回未落库的 media 对象(埋雷) | biz/system/media.go:107-117 | 八轮 |
|
| Z-17 | scanUpload.vue:115-116 空 `.catch((err) => {})` 吞路由就绪异常 | web/src/view/media/scanUpload.vue:115-116 | 九轮,未修复 |
|
||||||
| Z-12 | 单行压缩风格碎屑(最近修复遗留):access_log.go:44、gin.go:65、data/integration/integration_config.go:227/242/401 多语句挤一行 | 各处 | 八轮 |
|
| Z-19 | (新发现)前端全局 console.log 残留 7 处(Z-10 只清了目标文件):pdf.vue:20/34/37、version.vue:566、sysDictionaryDetail.vue:329、global.js:55、image.js:35 | web/src 各处 | 十轮 |
|
||||||
| Z-13 | rate_limit.go:48 魔法数字 200(同文件其余用 http.StatusServiceUnavailable);cors.go:13-14 头/方法串逗号空格不一致;audit.go:103 约 20 字段调用挤单行 | server/middleware/rate_limit.go:48、cors.go:13-14、audit.go:103 | 八轮 |
|
| Z-20 | (新发现)错误串清洗三种方式:error_audit.go:19 TrimSpace、access_log.go:134 TrimRight("\n")、audit.go:86 不清洗——同一 c.Errors 串三种处理 | server/middleware/ | 十轮 |
|
||||||
|
| Z-21 | (新发现)同请求重复 Lookup:access_log.go:34 与 :86 对同一请求两次调 BodyPolicyFor(可复用 :34 结果) | server/middleware/access_log.go:34,86 | 十轮 |
|
||||||
|
|
||||||
## 三、重复实现 / 双轨残留
|
## 三、重复实现 / 双轨残留
|
||||||
|
|
||||||
| # | 问题 | 位置 | 轮次 |
|
| # | 问题 | 位置 | 轮次 |
|
||||||
|---|------|------|------|
|
|---|------|------|------|
|
||||||
| Y-2 | 响应体双重 JSON 处理(请求体方向已修):access_log.go:114 与 audit.go:78 仍各自对同一响应体做 解析+脱敏+重序列化 两次;error_audit.go:30-35 第三次 unmarshal(读 envelope);capture.go:67-71 先 marshal 后查超限,超限时白做 | server/middleware/access_log.go:114;audit.go:78 | 六轮发现,未修复 |
|
| Y-7 | 掩码三套实现+键表三层漂移:data 层(Secret∪IsIntegrationSecretKey,递归含数组✓)+ service 层(仅 IsIntegrationSecretKey,**不认 definition.Secret**——靠 data 层先行掩码偶然自洽)+ middleware/redact.go 第三套(归一化规则不同:删分隔符 vs `-`→`_`);哨兵双轨:config.MaskedSecret="******" 与 middleware redactedValue="***"(前端若把日志侧 *** 回填会被当真值保存);IsIntegrationSecretKey platform_cert/root_cert 仍被 cert 后缀覆盖(冗余) | data/integration:225-245;service/integration:69-96;redact.go:18-27,6 | 九轮,十轮复核部分(递归)修复 |
|
||||||
| Y-4 | 上传限额 +1MiB 边际两处硬编码(Y-1 修复后残留):access_log.go:47 与 handler/media.go:19 | server/middleware/access_log.go:47;handler/media.go:19 | 八轮 |
|
| Y-5 | 集成配置校验双重执行:biz Save 对哨兵替换后值校验("******"非空总通过,半失效)+ data 层 merge 后再校验兜底——biz 层形同虚设 | biz/integration:122-129;data/integration:160-180 | 八轮,未修复 |
|
||||||
| Y-5 | 集成配置校验双重执行:biz Save 校验 merge 前值 + data SaveIntegrationConfig 再校验 merge 后值(两次输入不同故非纯重复,但失败无法分辨哪层拒绝) | biz/integration/integration_config.go:122-129;data/integration/integration_config.go:158-163 | 八轮 |
|
| Y-2 剩余 | ctxRespTextKey 共享✓(但见 Z-9 死通路)——剩余:capture.go:49-76 JSON 成功路径仍先 unmarshal+mask+marshal 再判超限(超长白做);error_audit.go:37 第三次 unmarshal | middleware/capture.go:49-76 | 六轮,部分修复 |
|
||||||
| Y-6 | 敏感字段概念三处三口径(V-19 修复未覆盖):data 恢复认 `"******"` 精确值(不 Trim);connectivity 恢复认 field.Secret 且 TrimSpace 比较;service 展示认共享键表无条件替换。空值语义也相反:initialize 的 preserve 把空提交恢复成旧值(用户无法清空密码),integration 的 merge 只认 ****** (空提交=清空)——同类概念两种空值行为 | data/integration:242-243;connectivity.go:77;service/integration:78-79;initialize/configuration.go:372-396 | 八轮 |
|
| Y-10 | (新发现)前端两套动态路由注册逻辑并存:登录路径 user.js:85-87 整树 addRoute;守卫路径 permission.js:117-146 扁平化+父级 redirect 包装(78-93)——两条路径行为不同(redirect 仅守卫侧有),登录后与刷新后同一菜单表现可能不一致 | web/src/pinia/modules/user.js:85-87;permission.js:78-146 | 十轮 |
|
||||||
|
| Y-11 | (新发现)1<<20 字面量双源:access_log.go:54 日志捕获下限与 settings.go:26 UploadBodyOverhead 同值不同源(语义不同,建议注释或共享) | access_log.go:54;biz/system/settings.go:26 | 十轮 |
|
||||||
|
| Y-8 残留 | 哨兵已统一到 IsMaskedSecret✓——残余:initialize 路径不 TrimSpace(" ****** " 在 merge/connectivity 视为哨兵、initialize 当新值写入) | initialize/configuration.go:372-396 | 九轮,部分修复 |
|
||||||
|
|
||||||
## 四、文档漂移(残余项)
|
## 四、文档漂移
|
||||||
|
|
||||||
| # | 问题 | 位置 | 轮次 |
|
| # | 问题 | 位置 | 轮次 |
|
||||||
|---|------|------|------|
|
|---|---|---|---|
|
||||||
| F-33 | CLAUDE.md 残余矛盾::77-78 "API error reason enum";:102 "HTTP/gRPC servers"(项目无 gRPC);:8-25 目录树缺 docs/、internal/logging/、internal/paymentkit/;:115 Wiring 简化版 | CLAUDE.md:8-25,77-78,102,115 | 六轮发现,未修复 |
|
| F-37 | (新)CLAUDE.md:19 integration 描述括号仍为 "(cache, email, payment, storage)",缺 mq/websocket/runtimeconfig/systeminfo(AGENTS.md 版本已含全部)——F-33 主体已修的残余 | CLAUDE.md:19 | 十轮 |
|
||||||
| F-34 | pkg/README.md 漏列 mq/、websocket/;:7-10 与 :10 重复罗列 | pkg/README.md:5-13 | 六轮发现,未修复 |
|
| F-38 | (新)config.yaml:104-107 注释 "Upload chunks are stored below .chunks" 与实际值 `chunk_dir: uploads/chunks` 不符 | configs/config.yaml:104-107 | 十轮 |
|
||||||
|
|
||||||
## 五、过分拆清单(第七轮专项)
|
## 五、过分拆清单(第七轮专项,未处理)
|
||||||
|
|
||||||
全库 291 个非测试 .go 文件中 <40 行者 81 个:15 个 wire ProviderSet 微文件为项目惯例(非债务)、约 50 个为分层契约/域对称模式自然产物(合理)、**16 个为真实过拆候选**。
|
全库 291 个非测试 .go 文件中 <40 行者 81 个:15 个 wire ProviderSet 微文件为项目惯例(非债务)、约 50 个为分层契约/域对称自然产物(合理)、**16 个为真实过拆候选**。
|
||||||
|
|
||||||
### 5.1 硬过拆(建议合并,4 项)
|
### 5.1 硬过拆(建议合并,4 项)
|
||||||
|
|
||||||
| 文件 | 行数 | 问题 | 合并目标 |
|
| 文件 | 行数 | 问题 | 合并目标 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| internal/data/data_scope_record.go | 9 | 单行类型别名,包内 13 处使用、主要消费者就是 data_scope.go | 并入 data/data_scope.go |
|
| internal/data/data_scope_record.go | 9 | 单行类型别名,包内 13 处使用 | 并入 data/data_scope.go |
|
||||||
| internal/service/dto/authentication.go | 8 | 仅 LoginResponse 1 个类型;LoginRequest/Route 在 system.go,一个登录流程 DTO 横跨两文件 | 并入 dto/system.go |
|
| internal/service/dto/authentication.go | 8 | 仅 LoginResponse 1 个类型(LoginRequest 在 system.go) | 并入 dto/system.go |
|
||||||
| internal/data/task/provider.go 别名层 | 11 | `type Provider = dataprovider.Database` 纯转手别名 | 删别名直接用 dataprovider.Database |
|
| internal/data/task/provider.go 别名层 | 11 | `type Provider = dataprovider.Database` 纯转手别名 | 删别名直接用 dataprovider.Database |
|
||||||
| internal/biz/task/task_registry.go 别名行 | 11 | TaskMethodFunc/TaskMethod 两行纯别名零增值(窄接口有三处消费,保留) | 删 :5-6 别名行 |
|
| internal/biz/task/task_registry.go 别名行 | 11 | TaskMethodFunc/TaskMethod 纯别名零增值(窄接口保留) | 删 :5-6 别名行 |
|
||||||
|
|
||||||
### 5.2 轻度过拆(建议合并,5 项)
|
### 5.2 轻度过拆(建议合并,5 项)
|
||||||
|
|
||||||
| 文件 | 行数 | 问题 | 建议 |
|
| 文件 | 行数 | 问题 | 建议 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| handler/navigation.go | 26 | 仅 Menu 一个端点,依赖的 UserService 与 user.go 相同 | 并入 user.go |
|
| handler/navigation.go | 26 | 仅 Menu 一个端点,依赖 UserService 与 user.go 相同 | 并入 user.go |
|
||||||
| handler/session.go | 25 | 仅 Logout 一个端点单方法 | 并入 user.go 或 public.go |
|
| handler/session.go | 25 | 仅 Logout 一个端点 | 并入 user.go 或 public.go |
|
||||||
| handler/http.go | 31 | 转发不一致:转发 httpx 常量但 session.go:4 又直接 import middleware(双风格) | 删转发或补齐统一 |
|
| handler/http.go | 31 | 转发不一致(双风格) | 删转发或统一 |
|
||||||
| data/system/time.go | 15 | deletedAtPointer 单函数无独立文件必要 | 并入 models.go |
|
| data/system/time.go | 15 | deletedAtPointer 单函数 | 并入 models.go |
|
||||||
| data/system/audit.go | 17 | 3 个 repo 构造器与实现跨文件分离(方法散布 5 个文件) | 构造器移回首个实现文件 |
|
| data/system/audit.go | 17 | 3 个 repo 构造器与实现跨文件分离 | 构造器归位首个实现文件 |
|
||||||
|
|
||||||
### 5.3 可选合并(模式性过拆,2 组)
|
### 5.3 可选合并(2 组)
|
||||||
|
|
||||||
- **router 21 个微文件**(12-33 行/个)→ 并入 routes.go 约 420 行(routecatalog 449 行先例)
|
- router 21 个微文件 → 并入 routes.go 约 420 行
|
||||||
- **modules 4 个 definition 包**(各 1 文件 17-24 行、单一消费者 catalog.go)→ 可合并为单文件 ~110 行;保留理由是模块插件式对称
|
- modules 4 个 definition 包 → 可合并单文件 ~110 行
|
||||||
|
|
||||||
### 5.4 接口碎片化(结构性)
|
### 5.4 接口碎片化
|
||||||
|
|
||||||
- 同一 DB seam 三套名字、同包双 seam:data/provider.Database → task 别名 → system.Provider 超集;data/system/security.go import 外部 2 方法版不用同包超集
|
- 同一 DB seam 三套名字、同包双 seam
|
||||||
|
|
||||||
### 5.5 判定为合理的(复检确认,防误报)
|
### 5.5 判定为合理的(防误报)
|
||||||
|
|
||||||
- **modules/surface:编译级硬约束**——catalog→definition→surface→routecatalog,并入根包即循环导入
|
- modules/surface:编译级硬约束(循环导入);单文件职责完整包 24 个;biz 域微文件/dto 微文件/service 对称微文件
|
||||||
- 单文件职责完整的包 24 个(cache/email/runtimeconfig/systeminfo/routecatalog/httpx/staticfiles/utils×2/gormkit/pagination/module/task 等)
|
|
||||||
- biz 域微文件 8 个(分层契约自然形态);dto email/permission/system_init;service 层对称微文件
|
|
||||||
|
|
||||||
## 审查后认为合理、不建议改动的部分(历轮评估保留决策汇总)
|
## 审查后认为合理、不建议改动的部分(历轮评估保留决策汇总)
|
||||||
|
|
||||||
- **biz 注入面**:RegisterBusinessModule/PaymentBusinessModule 是 docs/PAYMENT.md 声明的业务接入契约;PayInternal/RefundInternal/AuthorizeRefund 被 biz 调用链消费
|
- **biz 注入面**:RegisterBusinessModule/PaymentBusinessModule 是 docs/PAYMENT.md 声明的业务接入契约
|
||||||
- **F-2/F-4(http.go 见 5.2 重判)/F-6/F-7/F-9/F-10/F-11、S-5~S-9、D-4/D-6/D-9/D-10/D-11/D-19、L-5/L-10/L-12、P-3~P-13、X-1~X-11、C-2/C-4/C-6/C-8**:历轮影响分析后的保留决策
|
- **F-2/F-4/F-6/F-7/F-9/F-10/F-11、S-5~S-9、D-4/D-6/D-9/D-10/D-11/D-19、L-5/L-10/L-12、P-3~P-13、X-1~X-11、C-2/C-4/C-6/C-8**:历轮影响分析后的保留决策
|
||||||
- **质量标杆**(第八轮正面确认):payment 幂等指纹+回调强制查单+hook 前后指纹校验、退款 lease、task_scheduler 锁序、SSRF 拨号防护、auth singleflight、log_file 防 TOCTOU、staticfiles 安全、media_upload 分片校验链、system 配置掩码+preserve 闭环、V-17/V-18①②/V-16b 词表/V-13b env 集合/V-19 共享键表主体/Y-1/Z-1 修复质量良好、recovery panic dump 脱敏、traceparent 完整校验、支付回调空 buffer 隔离、data FindMedia 等 session 查询错误映射规范
|
- **质量标杆**(第十轮正面确认):支付回调三重隔离自洽、ErrorAudit 自审计抑制与客户端失败白名单闭环、data 层掩码/回填往返对已入表键自洽、AccessLog 前置 buffer+引用查询方向正确、SSRF 拨号拦截、auth singleflight、CORS 安全默认、V-26/V-27 主体/V-28/V-29/V-31 主体/Z-3/Z-14/Y-4/Y-8/Y-12/F-33~36/Z-5/Z-7/Z-8/Z-12 主体/Z-13 部分修复质量良好、Z-3 指纹统一对存量幂等主键(trade_no 唯一定位)无实质影响
|
||||||
|
|
||||||
## 处置建议(按优先级)
|
## 处置建议(按优先级)
|
||||||
|
|
||||||
1. **V-15**(86 处错误泄露——上轮声称修复但复核 0 收敛,建议推广 failLogViewer 范式统一处理)
|
1. **V-32 删除审计无痕**(一行标记修复)+ **V-33 存量订单指纹兼容窗口** + **V-35 调度器启动重试**
|
||||||
2. **V-11 修复缺陷收尾**(ctx 值移到 Next 后+边界补置位+测试;顺带清 Z-9 新死代码)+ **V-19 残留链**(新建分支恢复+connectivity 并入共享键表+空值不掩码)
|
2. **V-15 收尾**(data 通道净化、全角冒号、17 处漏网、净化前记日志、宽泛词收敛)+ **V-19 键表补别名**(一处三层受益)
|
||||||
3. **V-20(token 换一次性短时票据)/ V-13b(fail-close+测试)/ V-23(软删物理清理)/ V-24(删除竞态)**
|
3. **V-24 Create 路径持锁 + V-34 CleanupStale 顺序反转(先删文件后删 DB)+ Z-18 注释修正**
|
||||||
4. **V-16b 词表收尾 + Z-6 测试缺口批**(词表用例、truncated、秒传、回收范围、env 门禁)
|
4. **V-13b 双清单统一+空 env fail-close + V-27 redis_list 回退 + V-30 douyin 对齐**
|
||||||
5. **二节死代码碎屑 + 三节双轨(Y-4/Y-5/Y-6)+ 四节文档**
|
5. **V-36/37/38 + 二节碎屑(Z-15 软删残骸+Z-19/20/21)+ 三节双轨(Y-7 键表统一/Y-5/Y-10)**
|
||||||
6. **五节过分拆**(纯文件级减法,零行为变更)
|
6. **四节文档 + 五节过分拆**(纯文件级减法)
|
||||||
|
|
||||||
## 已修复(划线标记)
|
|
||||||
|
|
||||||
本轮已确认并完成:~~V-15~~、~~V-11~~、~~V-19~~、~~V-13b~~、~~V-23~~、~~V-24~~、~~V-25~~、~~V-16b~~、~~Z-1~~、~~Z-5~~、~~Z-7~~、~~Z-8~~、~~Z-10~~、~~Y-1~~、~~Y-2~~、~~Y-4~~。
|
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,7 @@ func IsIntegrationSecretKey(key string) bool {
|
||||||
if strings.Contains(normalized, "secret") || strings.Contains(normalized, "password") || strings.Contains(normalized, "private") || strings.Contains(normalized, "credential") || strings.Contains(normalized, "token") {
|
if strings.Contains(normalized, "secret") || strings.Contains(normalized, "password") || strings.Contains(normalized, "private") || strings.Contains(normalized, "credential") || strings.Contains(normalized, "token") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, item := range []string{"key", "key_pem", "api_key", "mch_key", "client_key", "certificate", "cert", "cert_pem", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} {
|
for _, item := range []string{"key_pem", "api_key", "mch_key", "client_key", "certificate", "cert", "cert_pem", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} {
|
||||||
if normalized == item || strings.HasSuffix(normalized, "_"+item) {
|
if normalized == item || strings.HasSuffix(normalized, "_"+item) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,10 +155,10 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
|
||||||
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationSelect("sign_type", "签名算法", false, "RSA2", "RSA"), integrationField("gateway_url", "网关地址", false, false, "url"),
|
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationSelect("sign_type", "签名算法", false, "RSA2", "RSA"), integrationField("gateway_url", "网关地址", false, false, "url"),
|
||||||
integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
|
integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
|
||||||
paymentDefinition(bizpayment.PaymentAlipayV3, "支付宝 V3", "支付宝证书模式 V3 接口", map[string]any{"app_id": "", "private_key": "", "app_cert": "", "root_cert": "", "public_cert": "", "environment": "production", "api_base_url": "https://openapi.alipay.com", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
|
paymentDefinition(bizpayment.PaymentAlipayV3, "支付宝 V3", "支付宝证书模式 V3 接口", map[string]any{"app_id": "", "private_key": "", "app_cert": "", "root_cert": "", "public_cert": "", "environment": "production", "api_base_url": "https://openapi.alipay.com", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
|
||||||
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", false, true, "textarea"), integrationField("app_cert", "应用公钥证书", false, true, "textarea"), integrationField("root_cert", "支付宝根证书", false, true, "textarea"), integrationField("public_cert", "支付宝公钥证书", false, true, "textarea"),
|
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", true, true, "textarea"), integrationField("app_cert", "应用公钥证书", true, true, "textarea"), integrationField("root_cert", "支付宝根证书", true, true, "textarea"), integrationField("public_cert", "支付宝公钥证书", true, true, "textarea"),
|
||||||
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("api_base_url", "API 地址", false, false, "url"), integrationField("gateway_url", "网关地址", false, false, "url"), integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
|
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("api_base_url", "API 地址", false, false, "url"), integrationField("gateway_url", "网关地址", false, false, "url"), integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
|
||||||
paymentDefinition(bizpayment.PaymentWechatV2, "微信支付 V2", "微信支付 V2,含退款双向证书", map[string]any{"app_id": "", "merchant_id": "", "mch_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "client_cert": "", "client_key": ""},
|
paymentDefinition(bizpayment.PaymentWechatV2, "微信支付 V2", "微信支付 V2,含退款双向证书", map[string]any{"app_id": "", "merchant_id": "", "mch_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "client_cert": "", "client_key": ""},
|
||||||
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", false, true, "textarea"), integrationField("client_key", "证书私钥", false, true, "textarea")),
|
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", true, true, "textarea"), integrationField("client_key", "证书私钥", true, true, "textarea")),
|
||||||
paymentDefinition(bizpayment.PaymentWechatV3, "微信支付 V3", "微信支付 API v3", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "private_key": "", "api_v3_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi"},
|
paymentDefinition(bizpayment.PaymentWechatV3, "微信支付 V3", "微信支付 API v3", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "private_key": "", "api_v3_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi"},
|
||||||
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("api_v3_key", "API v3 密钥", true, true, "password"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "jsapi", "app", "native", "h5", "codepay")),
|
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("api_v3_key", "API v3 密钥", true, true, "password"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "jsapi", "app", "native", "h5", "codepay")),
|
||||||
paymentDefinition(bizpayment.PaymentApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""},
|
paymentDefinition(bizpayment.PaymentApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""},
|
||||||
|
|
|
||||||
|
|
@ -965,6 +965,10 @@ func paymentOrderFingerprint(req *PaymentRequest, extra json.RawMessage) string
|
||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RequestFingerprint(req *PaymentRequest, extra json.RawMessage) string {
|
||||||
|
return paymentOrderFingerprint(req, extra)
|
||||||
|
}
|
||||||
|
|
||||||
func paymentResultFromOrder(order *PaymentOrder) *PaymentResult {
|
func paymentResultFromOrder(order *PaymentOrder) *PaymentResult {
|
||||||
if order == nil {
|
if order == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUserNotFound = errors.NotFound("USER_NOT_FOUND", "user not found")
|
ErrUserNotFound = errors.NotFound("USER_NOT_FOUND", "user not found")
|
||||||
|
ErrMediaNotFound = errors.NotFound("MEDIA_NOT_FOUND", "media not found")
|
||||||
// ErrInvalidCredentials is returned for any failed login, regardless of
|
// ErrInvalidCredentials is returned for any failed login, regardless of
|
||||||
// whether the user exists, to avoid leaking which accounts are registered.
|
// whether the user exists, to avoid leaking which accounts are registered.
|
||||||
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
|
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,12 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, suppliedM
|
||||||
if save {
|
if save {
|
||||||
count, countErr := uc.MediaKeyReferences(ctx, key)
|
count, countErr := uc.MediaKeyReferences(ctx, key)
|
||||||
if countErr != nil {
|
if countErr != nil {
|
||||||
|
_ = uc.files.Delete(ctx, key)
|
||||||
return nil, countErr
|
return nil, countErr
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
if err = uc.CreateMedia(ctx, media); err != nil {
|
if err = uc.CreateMedia(ctx, media); err != nil {
|
||||||
|
_ = uc.files.Delete(ctx, key)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,7 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = uc.CreateMedia(ctx, media); err != nil {
|
if err = uc.CreateMedia(ctx, media); err != nil {
|
||||||
|
_ = uc.files.Delete(ctx, key)
|
||||||
return fail(err)
|
return fail(err)
|
||||||
}
|
}
|
||||||
if err = uc.CompleteUploadSession(ctx, uploadID, key, media.ID); err != nil {
|
if err = uc.CompleteUploadSession(ctx, uploadID, key, media.ID); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type MediaSettings struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMaxMediaFileSize int64 = 100 << 20
|
const DefaultMaxMediaFileSize int64 = 100 << 20
|
||||||
|
const UploadBodyOverhead int64 = 1 << 20
|
||||||
|
|
||||||
func (s MediaSettings) EffectiveMaxFileSize() int64 {
|
func (s MediaSettings) EffectiveMaxFileSize() int64 {
|
||||||
if s.MaxFileSize > 0 {
|
if s.MaxFileSize > 0 {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
configuration reloads, and repository implementations. File watching and
|
configuration reloads, and repository implementations. File watching and
|
||||||
immutable configuration snapshots belong to `internal/config`.
|
immutable configuration snapshots belong to `internal/config`.
|
||||||
|
|
||||||
- `system/`: system repositories and system table persistence
|
- `system/`: system repositories, media_* persistence, and system maintenance
|
||||||
- `task/`: timed-task tables and task persistence
|
- `task/`: timed-task tables and task persistence
|
||||||
- `integration/`: `sys_integration_configs` and integration configuration persistence
|
- `integration/`: `sys_integration_configs` and integration configuration persistence
|
||||||
- `payment/`: payment-order persistence
|
- `payment/`: payment-order persistence
|
||||||
|
|
@ -23,9 +23,10 @@ reload locks. Do not split them into packages only to reduce file count.
|
||||||
|
|
||||||
当前内置数据模块为 `system`、`integration`、`task` 和 `payment`。其中:
|
当前内置数据模块为 `system`、`integration`、`task` 和 `payment`。其中:
|
||||||
|
|
||||||
- `system` 只拥有 `sys_*` 系统表、系统仓储、种子和系统维护清理;
|
- `system` 拥有 `sys_*` 系统表、media_* 媒体表、系统仓储、种子和系统维护清理;
|
||||||
- `integration` 唯一拥有 `sys_integration_configs` 表模型、配置仓储和通信默认值;
|
- `integration` 唯一拥有 `sys_integration_configs` 表模型、配置仓储和通信默认值;
|
||||||
- `task` 只拥有定时任务与任务日志表;
|
- `task` 只拥有定时任务与任务日志表;
|
||||||
|
- `provider/` 提供共享数据库 seam;
|
||||||
- `payment` 只拥有 `pay_orders` 等支付持久化,通过 `biz/integration.PaymentConfigReader`
|
- `payment` 只拥有 `pay_orders` 等支付持久化,通过 `biz/integration.PaymentConfigReader`
|
||||||
读取支付配置,不感知 integration 的 PO 或表结构。
|
读取支付配置,不感知 integration 的 PO 或表结构。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
|
||||||
secretFields := integrationSecretFields(kind, provider)
|
secretFields := integrationSecretFields(kind, provider)
|
||||||
for key, value := range values {
|
for key, value := range values {
|
||||||
if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) {
|
if secretFields[key] || integrationbiz.IsIntegrationSecretKey(key) {
|
||||||
if text, ok := value.(string); ok && text == config.MaskedSecret {
|
if text, ok := value.(string); ok && config.IsMaskedSecret(strings.TrimSpace(text)) {
|
||||||
if prior, exists := old[key]; exists {
|
if prior, exists := old[key]; exists {
|
||||||
values[key] = prior
|
values[key] = prior
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
|
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
|
||||||
Subject: req.Subject, PaymentMode: bizpayment.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
|
Subject: req.Subject, PaymentMode: bizpayment.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
|
||||||
Currency: req.Currency, PaymentStatus: bizpayment.PaymentStatusInitialized, FulfillmentStatus: bizpayment.FulfillmentStatusPending,
|
Currency: req.Currency, PaymentStatus: bizpayment.PaymentStatusInitialized, FulfillmentStatus: bizpayment.FulfillmentStatusPending,
|
||||||
RefundStatus: bizpayment.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
|
RefundStatus: bizpayment.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: bizpayment.RequestFingerprint(req, extra), Extra: extra,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
|
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
|
||||||
|
|
@ -224,11 +224,6 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
return test, nil
|
return test, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentTestFingerprint(req *bizpayment.PaymentRequest) string {
|
|
||||||
raw, _ := json.Marshal(req)
|
|
||||||
hash := sha256.Sum256(raw)
|
|
||||||
return hex.EncodeToString(hash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func paymentTestProviderUpdate(result *bizpayment.PaymentResult) *bizpayment.PaymentProviderUpdate {
|
func paymentTestProviderUpdate(result *bizpayment.PaymentResult) *bizpayment.PaymentProviderUpdate {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,9 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *system.MediaFile) error
|
||||||
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*system.MediaFile, error) {
|
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*system.MediaFile, error) {
|
||||||
var po mediaPO
|
var po mediaPO
|
||||||
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, system.ErrMediaNotFound
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return mediaFromPO(po), nil
|
return mediaFromPO(po), nil
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key stri
|
||||||
func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
|
func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
|
||||||
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
|
// The compatible flow uses GORM's normal Delete here, retaining the soft-deleted session
|
||||||
// for audit/recovery rather than physically removing it.
|
// for audit/recovery rather than physically removing it.
|
||||||
return r.data.DB().WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
|
return r.data.DB().WithContext(ctx).Unscoped().Delete(&uploadSessionPO{}, id).Error
|
||||||
}
|
}
|
||||||
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *system.UploadChunk) error {
|
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *system.UploadChunk) error {
|
||||||
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
|
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ func Migrations() []migration.Step {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kept as a migration version for existing installations; the canonical
|
||||||
|
// communication surface seeder is idempotent and now owns the test endpoint.
|
||||||
|
func ensureCommunicationTestSurface(db *gorm.DB) error { return ensureCommunicationSurface(db) }
|
||||||
|
|
||||||
func ensureCommunicationSurface(db *gorm.DB) error {
|
func ensureCommunicationSurface(db *gorm.DB) error {
|
||||||
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&apiPO{}) {
|
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&apiPO{}) {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -128,47 +132,3 @@ func ensureCommunicationSurface(db *gorm.DB) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureCommunicationTestSurface(db *gorm.DB) error {
|
|
||||||
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&apiPO{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
var existingMenus int64
|
|
||||||
if err := tx.Model(&menuPO{}).Where("name IN ?", []string{"extensions", "integrationConfig"}).Count(&existingMenus).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if existingMenus == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
api := apiPO{
|
|
||||||
Path: "/integration/configs/:kind/:provider/test",
|
|
||||||
Method: "POST",
|
|
||||||
APIGroup: "集成配置",
|
|
||||||
Description: "测试通信集成连接",
|
|
||||||
}
|
|
||||||
if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).FirstOrCreate(&api).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !tx.Migrator().HasTable(&authorityPO{}) || !tx.Migrator().HasTable(&casbinRulePO{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var authority authorityPO
|
|
||||||
if err := tx.Where("authority_id = ?", 888).First(&authority).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
exists, err := policyExists(tx, authority.AuthorityID, api.Path, api.Method)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
rule := newPolicyRule(authority.AuthorityID, api.Path, api.Method)
|
|
||||||
return tx.Create(&rule).Error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -339,27 +339,37 @@ func maskDataSecrets(value *config.Data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if value.Database != nil {
|
if value.Database != nil {
|
||||||
|
if value.Database.Password != "" {
|
||||||
value.Database.Password = config.MaskedSecret
|
value.Database.Password = config.MaskedSecret
|
||||||
|
}
|
||||||
value.Database.Source = ""
|
value.Database.Source = ""
|
||||||
}
|
}
|
||||||
if value.Redis != nil {
|
if value.Redis != nil {
|
||||||
|
if value.Redis.Password != "" {
|
||||||
value.Redis.Password = config.MaskedSecret
|
value.Redis.Password = config.MaskedSecret
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if value.Mongo != nil {
|
if value.Mongo != nil {
|
||||||
|
if value.Mongo.Password != "" {
|
||||||
value.Mongo.Password = config.MaskedSecret
|
value.Mongo.Password = config.MaskedSecret
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for _, item := range value.DatabaseList {
|
for _, item := range value.DatabaseList {
|
||||||
if item != nil {
|
if item != nil {
|
||||||
|
if item.Password != "" {
|
||||||
item.Password = config.MaskedSecret
|
item.Password = config.MaskedSecret
|
||||||
|
}
|
||||||
item.Source = ""
|
item.Source = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, item := range value.RedisList {
|
for _, item := range value.RedisList {
|
||||||
if item != nil {
|
if item != nil {
|
||||||
|
if item.Password != "" {
|
||||||
item.Password = config.MaskedSecret
|
item.Password = config.MaskedSecret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func preserveConfigSecrets(next, current *config.Config) {
|
func preserveConfigSecrets(next, current *config.Config) {
|
||||||
if next == nil || current == nil {
|
if next == nil || current == nil {
|
||||||
|
|
@ -407,14 +417,11 @@ func preserveDatabaseListSecrets(next, current []*config.Database) {
|
||||||
byName[item.AliasName] = item
|
byName[item.AliasName] = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index, item := range next {
|
for _, item := range next {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
previous := byName[item.AliasName]
|
previous := byName[item.AliasName]
|
||||||
if previous == nil && index < len(current) {
|
|
||||||
previous = current[index]
|
|
||||||
}
|
|
||||||
if previous == nil {
|
if previous == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ state, or translate provider-specific protocols into `biz` interfaces.
|
||||||
- `email`: SMTP email repository.
|
- `email`: SMTP email repository.
|
||||||
- `mq`: reloadable EMQX/MQTT and RabbitMQ/AMQP clients exposed through the shared `pkg/mq` interface.
|
- `mq`: reloadable EMQX/MQTT and RabbitMQ/AMQP clients exposed through the shared `pkg/mq` interface.
|
||||||
- `payment`: payment-channel SDKs and callback/signature handling.
|
- `payment`: payment-channel SDKs and callback/signature handling.
|
||||||
|
- `runtimeconfig`: runtime integration configuration store and subscriptions.
|
||||||
- `storage`: local and object-storage implementations of `biz.FileStorage`.
|
- `storage`: local and object-storage implementations of `biz.FileStorage`.
|
||||||
|
- `systeminfo`: system information integration helpers.
|
||||||
- `websocket`: reloadable Melody endpoint exposed through the shared
|
- `websocket`: reloadable Melody endpoint exposed through the shared
|
||||||
`pkg/websocket` wrapper.
|
`pkg/websocket` wrapper.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ func (t *ConnectivityTester) restoreMaskedSecrets(kind, provider string, values
|
||||||
}
|
}
|
||||||
for key := range masked {
|
for key := range masked {
|
||||||
value, _ := values[key].(string)
|
value, _ := values[key].(string)
|
||||||
if strings.TrimSpace(value) != config.MaskedSecret {
|
if !config.IsMaskedSecret(strings.TrimSpace(value)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
prior, _ := currentValues[key].(string)
|
prior, _ := currentValues[key].(string)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ var routes = map[string]routeValue{
|
||||||
"DELETE /info/deleteInfo": {group: "公告", description: "删除公告", audit: true},
|
"DELETE /info/deleteInfo": {group: "公告", description: "删除公告", audit: true},
|
||||||
"DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告", audit: true},
|
"DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告", audit: true},
|
||||||
"DELETE /integration/configs/:kind/:provider": {group: "集成配置", description: "删除集成配置", audit: true},
|
"DELETE /integration/configs/:kind/:provider": {group: "集成配置", description: "删除集成配置", audit: true},
|
||||||
"DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传"},
|
"DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传", audit: true},
|
||||||
"DELETE /position/deletePosition": {group: "岗位", description: "删除岗位", audit: true},
|
"DELETE /position/deletePosition": {group: "岗位", description: "删除岗位", audit: true},
|
||||||
"DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典", audit: true},
|
"DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典", audit: true},
|
||||||
"DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容", audit: true},
|
"DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容", audit: true},
|
||||||
|
|
@ -155,8 +155,8 @@ var routes = map[string]routeValue{
|
||||||
"POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)", audit: true},
|
"POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)", audit: true},
|
||||||
"POST /email/emailTest": {group: "email", description: "发送测试邮件", audit: true},
|
"POST /email/emailTest": {group: "email", description: "发送测试邮件", audit: true},
|
||||||
"POST /email/sendEmail": {group: "email", description: "发送邮件", audit: true},
|
"POST /email/sendEmail": {group: "email", description: "发送邮件", audit: true},
|
||||||
"POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"},
|
"POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件", audit: true},
|
||||||
"POST /fileUploadAndDownload/deleteFiles": {group: "文件上传与下载", description: "批量删除文件"},
|
"POST /fileUploadAndDownload/deleteFiles": {group: "文件上传与下载", description: "批量删除文件", audit: true},
|
||||||
"POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"},
|
"POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"},
|
||||||
"POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"},
|
"POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"},
|
||||||
"POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"},
|
"POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"kra/internal/biz/system"
|
||||||
"kra/internal/service/dto"
|
"kra/internal/service/dto"
|
||||||
systemservice "kra/internal/service/system"
|
systemservice "kra/internal/service/system"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -16,7 +17,7 @@ func NewMedia(service *systemservice.MediaService) *Media { return &Media{servic
|
||||||
|
|
||||||
func (h *Media) limitMultipartBody(c *gin.Context) int64 {
|
func (h *Media) limitMultipartBody(c *gin.Context) int64 {
|
||||||
limit := h.service.MediaConfig().EffectiveMaxFileSize()
|
limit := h.service.MediaConfig().EffectiveMaxFileSize()
|
||||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+(1<<20))
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+system.UploadBodyOverhead)
|
||||||
return limit
|
return limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func Fail(c *gin.Context, message string) { Write(c, CodeError, gin.H{}, message
|
||||||
func sanitizeFailureMessage(message string) string {
|
func sanitizeFailureMessage(message string) string {
|
||||||
message = strings.TrimSpace(message)
|
message = strings.TrimSpace(message)
|
||||||
lower := strings.ToLower(message)
|
lower := strings.ToLower(message)
|
||||||
for _, marker := range []string{"gorm", "sql:", "redis", "mysql", "mongo", "dial ", "connection", "provider", "sdk", "json:", "serialize", "timeout", "http 4", "http 5", "tls:"} {
|
for _, marker := range []string{"gorm", "sql:", "redis", "mysql", "mongo", "dial ", "connection", "provider", "sdk", "json:", "serialize", "timeout", "http 4", "http 5", "tls:", "record not found", "error 1062"} {
|
||||||
if strings.Contains(lower, marker) {
|
if strings.Contains(lower, marker) {
|
||||||
return "操作失败"
|
return "操作失败"
|
||||||
}
|
}
|
||||||
|
|
@ -53,11 +53,6 @@ func sanitizeFailureMessage(message string) string {
|
||||||
if strings.Contains(message, "失败:") {
|
if strings.Contains(message, "失败:") {
|
||||||
return strings.TrimSpace(strings.SplitN(message, "失败:", 2)[0]) + "失败"
|
return strings.TrimSpace(strings.SplitN(message, "失败:", 2)[0]) + "失败"
|
||||||
}
|
}
|
||||||
for _, marker := range []string{"参数", "不能为空", "无效", "不存在", "权限", "未登录", "验证码", "密码", "重复", "不支持", "超出", "分片", "上传", "文件", "名称", "令牌", "token"} {
|
|
||||||
if strings.Contains(message, marker) {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
|
||||||
if admin != nil && admin.Media != nil {
|
if admin != nil && admin.Media != nil {
|
||||||
mediaSettings.MaxFileSize = admin.Media.MaxFileSize
|
mediaSettings.MaxFileSize = admin.Media.MaxFileSize
|
||||||
}
|
}
|
||||||
bodyLimit = mediaSettings.EffectiveMaxFileSize() + (1 << 20)
|
bodyLimit = mediaSettings.EffectiveMaxFileSize() + system.UploadBodyOverhead
|
||||||
}
|
}
|
||||||
bytesIn := c.Request.ContentLength
|
bytesIn := c.Request.ContentLength
|
||||||
logLimit := 1024
|
logLimit := 1024
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
- `database`:跨模块共享的 GORM 支持、分页和迁移执行器
|
- `database`:跨模块共享的 GORM 支持、分页和迁移执行器
|
||||||
- `module`:模块迁移、后台元数据和路由注册协议
|
- `module`:模块迁移、后台元数据和路由注册协议
|
||||||
- `task`:跨模块共享的进程内任务注册表和贡献协议
|
- `task`:跨模块共享的进程内任务注册表和贡献协议
|
||||||
- `database`、`module`、`task`:跨模块基础设施与稳定协议
|
- `mq`:消息队列客户端与订阅协议
|
||||||
|
- `websocket`:WebSocket 服务端与连接管理
|
||||||
|
|
||||||
应用级 logging、HTTP response、paymentkit 等实现位于 `internal/`,不属于
|
应用级 logging、HTTP response、paymentkit 等实现位于 `internal/`,不属于
|
||||||
可复用 pkg 公共层。
|
可复用 pkg 公共层。
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ router.beforeEach(async (to, from) => {
|
||||||
if(!routerStore.asyncRouterFlag){
|
if(!routerStore.asyncRouterFlag){
|
||||||
await setupRouter(userStore)
|
await setupRouter(userStore)
|
||||||
}
|
}
|
||||||
if(userStore.userInfo.authority.defaultRouter){
|
if(userStore.userInfo && userStore.userInfo.authority && userStore.userInfo.authority.defaultRouter){
|
||||||
return { name: userStore.userInfo.authority.defaultRouter }
|
return { name: userStore.userInfo.authority.defaultRouter }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue