topfans/docs/specs/2026-07-21-backend-remediation-plan.md
2026-07-21 21:14:22 +08:00

25 KiB
Raw Blame History

后端问题修复方案2026-07-21

配套审计报告:docs/backend-audit-2026-07-21.md 本文把审计发现的问题转化为可执行的修复批次,每条含:问题引用 → 根因 → 修复方案 → 涉及文件 → migration/SQL → 验证 → 风险 → 工作量。


一、方案概述(必读)

要解决的问题

安全类

  • 生产密钥OSS/SMS/OpenAI/Dify已提交进 git任何人 clone 即可拿到。
  • 4/5 服务信任请求体/attachment 里的 user_id/reporter_id,可越权与刷量。
  • JWT 全局可变密钥、弱默认值、data race。

财务/数据一致性类

  • 展品收益结算非幂等,实测超发 2,525,254 水晶且已被领取(exhibition_revenue_records 无唯一约束 + 多结算入口)。
  • 铸造扣费事务内嵌跨服务 gRPC可扣费成功但无藏品、连接池耗尽。
  • 累计时长累加无幂等(AddExhibitionHours 不用 sourceID时长虚高→提前升级→多发奖励。
  • exhibition_revenue_records.created_at 单位混乱(秒/毫秒混存)。

稳定性类

  • bcrypt 在 DB 事务内执行,注册高峰连接池耗尽。
  • Redis-Streams 消费者重启即丢消息,无 DLQ/重试。

架构/边界类

  • 全部服务共享一个 Postgres + 一套 migration + 一个 pkg/models;服务间直接 import 对方 Go 包49 条跨域外键。
  • 网关直连 DB、import 服务内部包;UserSocialService 契约混合。
  • 服务端口默认值四层不一致;starbookService 孤儿但仍在部署链路。

整体实现路径(按风险/收益排序,非架构驱动)

批次 主题 目标 工作量估算
批次 0 安全紧急 轮换密钥、清历史、密钥出库 0.51 天(+ 运维轮换)
批次 1 财务正确性 结算幂等、mint 事务/随机数、doMint 限流、时长幂等、存量校正 46 天
批次 2 鉴权边界 provider 统一从 attachment 取身份、social 隐私/正确性 34 天
批次 3 稳定性 bcrypt、Login 限流、MQ DLQ、JWT 密钥、aiChat 健壮性、事件可靠性、网关聚合 46 天
批次 4 配置/部署 端口对齐、starbook 决断、健康探针、.env 对齐、P2 清理 23 天
批次 5 架构治理(路线图) 去分布式单体(不在本次落地 独立项目,数周

关键决策

  1. 不在本次拆库/拆 pkg/modelsMVP 先行原则)。当前业务规模下,拆库是数周级独立项目,收益/风险比不划算。本次只做加固:加 lint 禁止新增跨服务 import、加唯一约束堵幂等漏洞、明确表 owner 注释。架构级拆分列入批次 5 路线图。参见 §批次5
  2. 财务批次优先于边界批次:已实证资损(超发水晶已被领取)>理论越权,先止血。
  3. 存量数据修复走 dry-run + 备份 + 序列同步,遵循 CLAUDE.md PostgreSQL 序列规范。参见 §存量数据修复

核心修复优先级图TL;DR

先做 ──────────────────────────────────────────────────────→ 后做
 批次0        批次1              批次2         批次3            批次4            批次5
 安全紧急      财务正确性          鉴权边界       稳定性            配置/部署         架构治理(路线图)
 密钥轮换   结算幂等/mint/时长   身份透传     bcrypt/MQ/JWT     端口/starbook     去分布式单体
 (P0)      /doMint (P0+P1)     /social(P0+P1) /aiChat/网关(P1)  /探针/.env/P2    (本次不落地)

文档说明

  • 适用范围backend 全部服务 + gateway + pkg + 配置/部署。
  • 工作量:批次 04 合计约 1420 人天(不含架构批次 5
  • 前置:审计报告 docs/backend-audit-2026-07-21.md;基线 commit b7f8f1b724e5
  • 目标读者后端负责人、运维、DBA。

二、修复批次详解

批次 0安全紧急密钥

对应审计 P0-1。先做,且需运维配合轮换

根因backend/deploy/envs/*.envbackend/.env.exampledocker/.env.localdocker/.env.prod 含真实密钥且被 git 追踪。

修复步骤

  1. 立即轮换(运维):阿里云 OSS AccessKeyLTAI5t6QcdJHpYbCPxM8SXYE 及第二套)、复用的 SMS key、OpenAIsk-proj-.../微达、Difyapp-...。轮换是首要动作——git 历史清理无法撤销已泄露的凭证。
  2. 密钥出库
    • .gitignore 增加:backend/deploy/envs/*.env(保留 *.env.example 占位模板)。
    • 从 git 索引移除:git rm --cached backend/deploy/envs/*.env docker/.env.local docker/.env.prod
    • .env.example 里所有真实密钥替换为 <REPLACE_ME> 占位符。
  3. 清理 git 历史git filter-repo --path backend/deploy/envs --path docker/.env.prod --invert-paths(需团队协调强推 + 重新 clone
  4. 改由部署时注入docker-compose 走宿主机环境变量 / k8s 走 Secretvalues-prod.yaml 已 gitignore作为注入源

验证git ls-files | grep -E 'deploy/envs/.*\.env$' 应为空;git log -p -- backend/.env.example | grep -c 'sk-proj' 为 0。

风险:轮换 OSS key 需同步更新所有消费方asset/user/gateway历史清理需全员重新 clone。


批次 1财务正确性

1.1 展品收益结算幂等(最高优先,已实测资损

对应审计 §七.1。

根因exhibition_revenue_records(exhibition_id, cycle_start_time) 唯一约束;cleanup_worker.go 的 ZSET 路径L137、DB 兜底路径L239、MQ 消费路径三者可对同一展品-周期重复调 OnExhibitionCompletedCreateRevenueRecordrevenue_service.go:476)无条件插入。实测 3850 个展品同周期重复结算。

修复方案

  1. 加唯一约束migration见下UNIQUE(exhibition_id, cycle_start_time)
  2. 写入改幂等revenue_service.go:476CreateRevenueRecordclause.OnConflict{DoNothing: true}
    // repository 层
    tx.Clauses(clause.OnConflict{
        Columns:   []clause.Column{{Name: "exhibition_id"}, {Name: "cycle_start_time"}},
        DoNothing: true,
    }).Create(record)
    
  3. 收敛结算入口:保留 MQ 消费路径(已有 isAlreadyProcessed),删除 cleanup_worker.go:137:239 的直连 RPC 调用(OnExhibitionCompleted 已标注"本期内废弃");或让三路共用同一 isAlreadyProcessed 判重。
  4. 审计列:新增 settled_at bigint,弃用把 is_processed 当 settled原 §四 P2

涉及文件

  • backend/services/taskService/service/revenue_service.go:461-482(写入幂等)
  • backend/services/taskService/repository/revenueRepo.CreateRevenueRecord
  • backend/services/galleryService/service/cleanup_worker.go:137,239(删直连入口)
  • 新 migration

migrationbackend/migrations/2026_07_21_001_exhibition_revenue_idempotent.sql

-- 先去重历史(保留每组最早一条),再加约束
DELETE FROM exhibition_revenue_records a
USING exhibition_revenue_records b
WHERE a.exhibition_id = b.exhibition_id
  AND a.cycle_start_time = b.cycle_start_time
  AND a.id > b.id;
ALTER TABLE exhibition_revenue_records
  ADD CONSTRAINT uk_exhibition_revenue_cycle UNIQUE (exhibition_id, cycle_start_time);
ALTER TABLE exhibitions ADD COLUMN IF NOT EXISTS settled_at bigint;
-- 序列同步(本表 BIGSERIAL
SELECT setval('exhibition_revenue_records_id_seq', (SELECT COALESCE(MAX(id),1) FROM exhibition_revenue_records));

验证:迁移后 select count(*)-count(distinct (exhibition_id,cycle_start_time)) from exhibition_revenue_records = 0重放 worker 两次不新增记录。

1.2 累计时长幂等

对应审计 §七.2。

根因fan_profile_repository.go:535-558AddExhibitionHourssourceID 但不判重;revenue_service.go:513 资产级累加不传 sourceID。

修复方案

  1. 新增去重表 exhibition_hours_log(source_id VARCHAR UNIQUE, user_id, star_id, hours, created_at)AddExhibitionHours 事务内先 INSERT ... ON CONFLICT DO NOTHINGRowsAffected==0 则跳过累加。
  2. 资产级 assetLevelService.AddExhibitionHourssourceID 参数并同样判重。
  3. 存量:按 distinct exhibition 重算 total_exhibition_hoursseason_exhibition_hours,回滚因虚高误发的升级奖励(见 §四)。

涉及文件

  • backend/services/userService/repository/fan_profile_repository.go:535-558
  • backend/services/taskService/service/revenue_service.go:487-524
  • backend/services/assetService/...assetLevelService.AddExhibitionHours 签名)

1.3 created_at 单位统一

对应审计 §七.1 附带发现。

根因exhibition_revenue_records.created_at 6013 条秒、45836 条毫秒混存。

修复:代码统一 time.Now().UnixMilli()migration 回填:

UPDATE exhibition_revenue_records SET created_at = created_at * 1000
WHERE created_at < 100000000000;  -- 10位秒 → 毫秒

排查同类 bigint 时间列(like_bet_revenue_recordscrystal_transaction_records)是否同样混用。

1.4 铸造扣费事务重构

对应审计 P0-2。

根因mint_service.go:309-318db.Transaction 内调 userClient.UpdateCrystalBalance(跨服务 gRPCCreateMintOrder 非幂等。

修复方案

  1. RPC 移出事务:先在事务内落"扣费意图/占位订单",提交后再调 userService 扣费,失败走补偿/回滚订单状态。
  2. 幂等键mint_orders 已有 order_idCreateMintOrder 入口先按 order_id 查已存在的成功订单直接返回,而非重复扣费。
  3. 用户侧 UpdateCrystalBalance 已带 source_id,确认 userService 侧按 (source_id, change_type) 幂等。

涉及文件backend/services/assetService/service/mint_service.go:209-516

1.5 铸造随机性可预测

对应审计 §三 P1mint_service.go:323,349)。

  • 保底概率mint_service.go:323time.Now().UnixNano()%100,并发同纳秒结果相同、可被脚本卡点操纵。改用 crypto/rand
  • mockTxHashmint_service.go:349orderID/userID/starID/时间戳 全可观测输入拼 sha256可被预测伪造"已上链"。改为不可预测来源或明确标注非链上哈希、不作为凭证。

1.6 doMint 限流 TOCTOU

对应审计 §三 P1peripheral_service.go:217-319)。

CountRecentMintL233检查与 insertL271不在同一事务并发可绕过 10/24h 限制。把 count 检查并入同一事务,或用 Redis Lua 原子自增。


批次 2鉴权边界

对应审计 P0-3、P1trust boundary

根因moderation_provider.go:35asset_provider.go:483share_service.go 等直接用 req.UserId/ReporterId/SharerUserId4/5 服务信任 attachment 不再校验。

修复方案

  1. 统一身份提取中间件/拦截器:所有 provider 从 Dubbo attachment 提取 user_id/star_id覆盖 req 内同名字段,禁止业务读 req.UserId。抽到 pkg/ 公共拦截器,避免每服务各写一份。
  2. 内部 RPC 可信边界:内部服务间 RPC 走内网 + mTLS 或签名;ValidateTokenrouter.go:151)移出公开 /auth 组。
  3. 逐个 provider reviewasset/social/moderation/gallery/task列出所有读 req.*Id 的点改为读 attachment。

涉及文件:各服务 provider/*.gopkg/ 新增拦截器;gateway/router/router.go:151

验证:构造伪造 reporter_id/user_id 的 RPC应被拦截器覆盖为真实身份。

2.1 socialService 隐私/正确性

对应审计 §三 P1social 三条)。

  • friend_service.go:685CheckFriendship 硬编码 starID=0TODO 未做)→ 结果错误 + 好友关系隐私预言机。改为从 attachment 取真实 starID。
  • social_repository.go:648,672,903,923OR 子句括号优先级问题,可能让已删除资产漏进点赞列表。用显式 db.Where(...).Or(...) 或单条带括号的 SQL 分组。
  • social_repository.go:461GetRandomUsersByStarOFFSET rand + LIMIT 取连续段,非随机且可预测。改 TABLESAMPLEORDER BY random()(小表)。

批次 3稳定性

3.1 bcrypt 移出事务

P1。auth_service.go:161,566HashPassword 移到 db.Transaction 之前。仅需调整语句顺序,风险低。

3.2 Login 限流 + 消除用户枚举

P1。auth_service.go:300 vs 315 统一错误码("账号或密码错误"Login 加与 SMS 同款 Redis 限流。

3.3 MQ 可靠性

P1。pkg/mq/streams/adapter.goconsumer 名改为稳定标识(服务名+实例名,去掉 UnixNano)以便重启认领 PELXAUTOCLAIM 回收 + DLQ。若短期不投入明确停用 streams adapter当前 EventProducer/Stream* 常量 0 引用),二选一,避免半吊子。

3.4 material_type 收敛

P1。asset_like_service.go:267CONCAT 改为集合去重写入(或独立标签表),防无限增长。当前实测最多 3 值未爆,属预防。

3.5 JWT 密钥治理

P1 安全。pkg/jwt/jwt.go:18 包级可变全局 jwtSecret,弱默认值 "your-secret-key-change-in-production"SetSecret/ParseToken 无锁 data race。修复启动时强制从 env 注入,缺失/为默认值即 fail-fast;密钥用 sync.Once 或初始化时一次性设定,运行期只读,消除 racegateway 与 userService 用同一密钥源KMS/Secret避免漂移导致全量 token 失效。

3.6 aiChatService 健壮性

对应审计 §三 P1aiChat 三条)。

  • ai_chat_provider.go:250:保存上下文用请求里的 personaID 而非解析后的 persona.ID → 用 persona.ID
  • ai_chat_provider.go:138,141Redis 出错静默吞掉(记忆/历史丢失无日志)→ 记 Error 日志 + 降级提示,不静默。
  • main.go:190-211 / provider:188Dify/LLM 启动时二选一、无 fallback/熔断/重试Dify 挂即硬故障,且原样回传 err.Error() → 加熔断/超时/降级,错误映射为稳定用户文案,原始 err 仅记服务端。

3.7 事件/统计可靠性

对应审计 §三 P1statistic、队列名

  • pkg/statistic/client.go:53-70TrackEvent fire-and-forget失败只 log → 至少加本地重试/落盘补偿,或明确接受丢失并在文档标注。
  • 队列名("gallery""default")硬编码在 producer/consumer 两侧 → 抽为共享常量,消除改名漂移。

3.8 网关聚合与双写

对应审计 §三 P1gateway

  • asset_controller.go:414-436:铸造后本地写激光卡与 assetService 无事务串联 → 实例卡 mining。改为由 assetService 统一落状态 + 事件驱动,或加对账补偿任务。
  • auth_controller.go:69-97 等 6 处Register/Login/Me/Profile 链式调 GetFanIdentities(每次拉全量明星目录)→ 让 userService 单 RPC 返回带 star 信息的实体,减少往返并加下游失败补偿。
  • user_controller.go:647-748DeleteAccountdatabase.GetDB() 自开事务直接软删 users+fan_profiles绕过 userServiceuserService 无从感知、无法执行下游失效)。近期止血:改调 userService 的删除 RPC彻底修复随批次 5「网关瘦身」移除网关直连 DB。
  • gateway/repository/laser_card_repository.goapp_download_repository.go:网关持有 DB repo非纯 BFF→ 归批次 5「网关瘦身」DB 写回落 owner 服务。

批次 4配置/部署

4.1 服务端口对齐

P0-6。修 gateway/config/config.go:162-165 默认端口gallery 20001 / activity 20004 / starbook 20005galleryService/main.go:44 taskService 默认改 20006userService/main.go:283 去掉硬编码 WithPort(20000) 或删死 flag。加配置测试断言"网关默认端口 == 服务绑定端口"。

4.2 starbookService 决断

P0-7。二选一

  • 正式化:补 go.mod、加入 go.work、加 systemd/env
  • 删除:移除目录 + assetService 对它的 importmain.go:32+ compose/helm/dev.sh/gateway config 引用,代码并入 assetService。 推荐后者(生产未部署它)。

4.3 健康探针 & 大二进制

P1。k8s notificationservice 探针 /healthz/healthmoderationservice 补探针策略。.gitignorebackend/{assetService,gateway-fixed,test,cleanup-orphan-avatars}git rm --cached

4.4 序列规范

P1。backend/scripts/create_gallery_test_users.go 生成的 SQL 末尾补 setval(...)(见 CLAUDE.md 强制规则)。

4.5 .env 漂移

P1。backend/.envWS_AI_CHAT_PATH.env.example 未文档化;.env.example 有一批 .env 缺失的必需键(DIFY_TIMEOUT_SEC/DIFY_WORKFLOW/LANDING_BASE_URL/PUSH_*/SEGMENT_* 等)。对齐两份,.env.example 只留占位符 + 完整键清单。


批次 4.9P2 择机清理(随相关批次一起做)

对应审计 §四。不单独排期,改动到对应文件时顺手清。

P2 项 文件:行 动作
错误码字符串解析 gateway/controller/asset_controller.go:160-194 改用 status.FromError
原始 err 泄露给前端 auth_controller.go:222user_controller.go:139 统一错误码 + 友好文案err 仅记日志
ResetPassword 不失效 JWT userService/service/user_service.go:681 校验 JWT 与 users.access_token,或引入版本号/黑名单
头像 URL 不校验同源 user_service.go:1023 限本站 OSS bucket 或只接受 asset id
PII 打进 INFO 日志 ai_chat_provider.go:107mint_service.go/user_service.go 多处 手机号/JWT/聊天内容脱敏或降级
pkg/mq 死代码 EventProducer/Stream*/asynq.GetInfo/Delete 删除或补真实现(与 3.3 一并决断)
直连 Redis Pub/Sub activityService/service/activity_service.go:217,1579 走 broker 抽象 adapter.EventProducer
周边密钥兜底 pkg/peripheral/sign.go:33-44 去掉 JWT_SECRET/dev 默认兜底,SECRET_KEY 缺失即 fail-fast

批次 5架构治理路线图仅规划,本次不落地

⚠️ MVP 先行:以下为长期方向,当前不实施,避免为未来规模写用不上的抽象。

  • 去分布式单体:按域拆库或至少拆 schema + 明确表 owner跨服务读写改 RPC + service-local DTO。
  • pkg/models:改为各服务本地 model + proto DTO消除 7 服务共享 model 的编译耦合。
  • 消除跨服务 Go importtaskService↔assetService↔starbookServicegateway→asset/moderation 改走 RPC加 lint 规则禁止新增。
  • 拆分 UserSocialService:公开 API 与内部 RPC 分为两个契约(参考 task.proto 的 Mobile/Internal 范式)。
  • 网关瘦身:移除 gateway/repository/DB 写回落到 owner 服务。
  • 事件契约proto/event.proto{event_id, schema_version, produced_at} envelope。

加固动作(本次可做,低成本):加 CI lint 禁止 services/*/ 之间、gateway→services/*/{repository,service} 的新增 import防止耦合继续恶化。


三、问题 → 修复映射表

审计条目 严重度 批次
P0-1 密钥泄露 P0 批次 0
§七.1 结算超发(已实测资损) P0/财务 批次 1.1
§七.2 累计时长幂等 P1/财务 批次 1.2
§七.1 created_at 单位 P1 批次 1.3
P0-2 mint 事务嵌 RPC P0 批次 1.4
§三 P1 mint 随机数/txHash P1 批次 1.5
§三 P1 doMint 限流 TOCTOU P1 批次 1.6
P0-3 身份伪造/越权 P0 批次 2
§三 P1 social 隐私/正确性CheckFriendship/OR/随机) P1 批次 2.1
P1 bcrypt 事务内 P1 批次 3.1
P1 Login 枚举/限流 P1 批次 3.2
P1 MQ 丢消息/无 DLQ P1 批次 3.3
P1 material_type 增长 P1 批次 3.4
P1 JWT 全局密钥/data race P1 批次 3.5
§三 P1 aiChatpersonaID/Redis/Dify P1 批次 3.6
§三 P1 TrackEvent 丢事件/队列名漂移 P1 批次 3.7
§三 P1 网关双写/链式调用/DeleteAccount 直连 DB P1 批次 3.8(止血)/ 批次 5彻底
P0-6 端口不一致 P0 批次 4.1
P0-7 starbook 孤儿 P0 批次 4.2
P1 健康探针/大二进制/序列 P1 批次 4.3/4.4
P1 .env 漂移 P1 批次 4.5
§四 P2 全部8 项) P2 批次 4.9
P0-4/5 分布式单体、共享库、跨域 FK P0/架构 批次 5路线图
P1 UserSocialService / social.proto 契约混合 P1/架构 批次 5
P1 event.proto 无 version envelope P1/架构 批次 5事件契约
P1 网关持有 DB repolaser_card/app_download P1/架构 批次 5网关瘦身

四、存量数据修复脚本规范

遵循 CLAUDE.md:手动 INSERT 指定 id 必须 setval;破坏性操作前备份 + dry-run。

统一要求

  1. 备份pg_dump -t exhibition_revenue_records -t exhibitions -t user_exhibition_hours -t asset_level_records ... 先落盘。
  2. dry-run:所有清理脚本先跑 SELECT 版本输出将影响的行数/金额,人工确认后再跑 DELETE/UPDATE
  3. 事务包裹BEGIN; ... ; COMMIT;,异常回滚。
  4. 序列同步:任何删改后对相关表执行 SELECT setval('<table>_id_seq', (SELECT MAX(id) FROM <table>));
  5. 回收超发:批次 1.1 去重后,统计已 claimed 的重复水晶(本库 2,525,254与运营确认回收/核销口径(本库为测试数据,可直接清账;生产需谨慎)。
  6. 时长重算 + 等级/奖励回滚(批次 1.2 存量)
    • distinct exhibition 重算每个 (user_id,star_id)total_exhibition_hours 与每个资产的 season_exhibition_hours(以去重后的结算记录为准)。
    • 用重算后的时长经 CalculateLevelFromExhibitionHours 复算等级;对因虚高时长误升的等级下修,并冲销对应的升级奖励水晶(crystal_transaction_recordschange_type='level_up_bonus' 且 source 关联误升的记录)。
    • 全程 dry-run 先出"将下修的用户/资产 + 冲销水晶总额"清单,人工确认后执行;执行后同步相关表序列。

存量清理顺序created_at 单位回填1.3)→ 收益记录去重 + 加约束1.1)→ 回收重复水晶 → 重算时长 + 回滚误发奖励1.2)。


五、回归验证清单(每批次完成后)

  • go build ./...(含 go.work 全部模块)通过。
  • 改动函数的所有调用方query_graph callers_of)不受影响。
  • 相关单测通过;财务/鉴权补新用例。
  • migration 在本地 top-fans 全新库跑通,序列健康检查通过。
  • 端口配置测试通过(批次 4
  • 伪造身份 RPC 被拦截(批次 2
  • 结算重放不产生重复记录(批次 1
  • git ls-files 无密钥/大二进制(批次 0/4

六、备注

  • 本文按 CLAUDE.md 接口开发规范分层、DTO、错误码、日志、migration、测试落地各修复。
  • 架构级拆分(批次 5建议单独立项本次仅做加固与止血。
  • 本库多为测试期脏数据,存量脚本以"可清账"为前提;生产执行前需 DBA 复核并按生产数据量重估。

六.1 跨 plan 执行协调(全局自审 2026-07-21 发现)

各批次已拆成独立 plandocs/superpowers/plans/2026-07-21-*.md)。以下为多 plan 触碰同一资源的协调约定,执行时按序,第二个改的人先 rebase

共享资源 涉及 plan 约定
migration 编号 settlement/mint/hours 固定顺序 001 收益幂等 → 002 crystal_tx 唯一索引 → 003 时长幂等,按号执行
galleryService/mq/consumer.go isSettled/markSettled + settled_at settlement(Task3) owns / config(4.9-G 仅校验) 只由 settlement plan 改config plan 不重复编辑
galleryService/mq/consumer.go 队列名常量(RegisterHandlers) stability(3.7) 与上者不同区域;建议 settlement 先落、stability 后落
taskService/service/revenue_service.go settlement(只读调用方) / hours(Task4 改 :513) 无同行冲突hours 后落即可
userService/repository/fan_profile_repository.go hours(AddExhibitionHours) / mint(UpdateCrystalBalance) 不同方法,任意顺序;第二个先 go build 校验
crystal_transaction_records 幂等 mint(加 uk_crystal_tx_source_change) 唯一约束只由 mint plan 加hours 的 level_up_bonus 流水用不同 change_type,不冲突
userService/service/user_service.go ResetPassword stability(3.1 bcrypt 出事务) / config(4.9 P2 JWT 失效) 不同关注点同一函数,串行改、后者 rebase