合并最新 master 到 BGFilter 优化分支
同步 master 的最新二十个提交。 保留当前分支的 BgFilter 抠图降级与 cross_check 策略。 解决决策日志追加冲突并保留双方记录。
This commit is contained in:
@@ -16,6 +16,22 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-12 泥点充值收敛为四档并统一资产入口
|
||||
|
||||
- 背景:主站与图片画板的泥点余额入口、余额明细和充值弹窗存在不同实现,旧充值口径仍展示六档泥点、首充双倍和会员购买 / 升级入口,容易让展示、商品资格与后端余额真相发生漂移。
|
||||
- 决策:主站与图片画板统一复用公共泥点资产入口,收起态展示总额与充值,展开态只展示不限时泥点、每日免费泥点和使用详情;充值中心 BFF 继续统一下发总额、三桶余额、限时到期时间、每日免费基础重置额及下次重置时间,前端不得自行相减推算,但会员周期限时泥点仅用于存量兼容和后端结算,当前版本不在前台展示。默认泥点商品收敛为 `60 / ¥6`、`180 + 90 / ¥18`、`300 + 150 / ¥30`、`680 + 340 / ¥68` 四档,`60` 档无赠送,后三档按现有 `user_id + product_id` 独立资格规则首次购买加赠 `50%`。当前版本关闭会员购买页签、会员商品和购买 / 升级入口。
|
||||
- 影响范围:`profile_recharge_product_config` 默认商品、充值中心 read model、共享前后端契约、主站与图片画板泥点资产入口、充值弹窗、后台充值商品默认值。
|
||||
- 验证方式:充值与统一入口定向前端测试、`npm run typecheck`、充值商品定向 Rust 测试、`cargo check -p spacetime-module -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 2026-07-12 每日免费泥点独立于任务并按北京时间日切
|
||||
|
||||
- 背景:现有“每日免费泥点”实际只是每日登录任务领取奖励,领取后进入普通永久余额,既不是独立余额桶,也不会在次日失效;同时主站仍展示每日任务卡片和任务中心入口,与新的产品口径不一致。
|
||||
- 决策:新增 `profile_daily_free_points` 作为每日免费泥点事实源,基础额度固定为 `20`,以北京时间 `day_key` 为业务日;跨日后的首次余额读取或扣费原子清除昨日剩余及退款叠加量,并把当日额度重置为 `20`,对外语义始终视为北京时间 `00:00` 已重置。扣费按“每日免费 -> 会员周期限时 -> 永久”顺序;资产退款在同一业务日恢复原每日免费额度,跨业务日时把原每日免费消费部分叠加到退款当日每日免费桶,当日允许超过 `20`,下一业务日仍统一重置为 `20`。每日任务系统和 `daily_task_reward` 保留为普通永久奖励,但主站隐藏每日任务卡片及任务中心入口。
|
||||
- 影响范围:`profile_daily_free_points`、`profile_wallet_ledger`、个人资金 read model、钱包扣费和退款 metadata、主站“我的”页、SpacetimeDB 迁移与生成绑定。
|
||||
- 验证方式:`npm run spacetime:generate`、`npm run check:spacetime-schema`、钱包定向 Rust 测试、个人中心定向前端测试、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 2026-07-11 BgFilter 交叉模型否决只用于角色形象
|
||||
|
||||
- 背景:新版 BgFilter 的 `cross_check` 默认开启,会额外运行 HR-matting 第二意见模型;角色形象需要保留发丝、镂空等复杂边缘质量,但角色动作序列帧、图标 spritesheet 和 UI 素材提取不需要承担这部分额外推理开销。
|
||||
@@ -24,6 +40,22 @@
|
||||
- 验证方式:运行 `cargo test -p api-server editor_bgfilter_cross_check --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server editor_canvas_screen_background_generation_uses_bgfilter_postprocess --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server editor_character_animation_frames_use_three_stage_matting_fallback --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 2026-07-11 SpacetimeDB 工具链统一升级到 2.6.0
|
||||
|
||||
- 背景:生产数据副本验证已使用 2.6.0 standalone,而仓库 Rust crate、本地 CLI、生成 bindings、容器与 server provision 仍锁定 2.5.0 或更早版本,继续混用会增加 BSATN / procedure 返回值与发布产物错配风险。
|
||||
- 决策:`server-rs/Cargo.toml` 的 `spacetimedb`、`spacetimedb-sdk`、`spacetimedb-lib` 精确锁定 2.6.0;本地 CLI / standalone、Rust bindings、worker smoke、容器压测镜像和生产 provision 下载根同步对齐 2.6.0。其它 crate 恰好出现的 2.4.1 / 2.5.0 不随本决策机械替换。
|
||||
- 影响范围:Rust workspace lockfile、SpacetimeDB bindings、本地 dev 版本门禁、容器 smoke / loadtest、server provision Jenkins 与项目 SpacetimeDB skills / 文档。
|
||||
- 验证方式:核对 `spacetime --version`,运行 `npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo check` / 定向测试、`npm run test -- scripts/dev.test.ts`、server provision 工具测试、production ops / encoding / diff 门禁。
|
||||
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
## 2026-07-10 外部生成任务只持久化轻量媒体引用并独立维护摘要投影
|
||||
|
||||
- 背景:编辑器 worker 化后直接把同步接口 payload 序列化进 `external_generation_job.request_payload_json`;前端又把已有 OSS `objectKey` 下载成 Data URL 再提交,导致单个任务 JSON 膨胀到数 MB,正式任务列表读取 20 条任务时同时搬运约 65 MB payload,并放大为 SpacetimeDB 与 api-server 的瞬时内存峰值。此前“禁止 Data URL 持久化”只覆盖工程、素材、图层和元数据,遗漏了正式生成任务表。
|
||||
- 决策:`external_generation_job.request_payload_json` / `result_payload_json` 同样属于正式持久化边界。对于本次事故涉及的 `source_module = editor-canvas` 任务,只允许普通业务参数和 `objectKey` / `resourceId` / `assetId` 等已登记轻量引用;任意层级 `data:` / `blob:` 与超限 JSON 必须由 api-server 和 SpacetimeDB 双重拒绝。编辑器已有媒体直接传正式引用,本地红框标记图先上传 OSS 后再入队,上传目录与文件名使用同一个强唯一 ID。其它玩法现存 Data URL 请求契约不在本次事故修复中被静默禁用,后续必须先完成各自资源化再扩大 DB 门禁。用户任务列表、单任务状态和 acknowledge 只读取不含 request/result payload 的 `external_generation_job_summary` 投影;acknowledge 只更新摘要小表并保留审计事件,不为确认通知加载 / 重写主任务 payload。提示词在入队时提前提取;错误摘要统一去除内联媒体并限制为 2048 字符;列表在单次 owner 扫描中只保留固定大小 top-N,不再收集全量历史后截断。历史终态 payload 仅允许迁移操作员通过默认 dry-run、`editor-canvas + job_id` B-tree cursor 显式分批压缩,pending / running 永不压缩;cursor 选择最多读取 `limit + 1` 行,apply 再逐条主键读取。首次发布默认 fail-closed 暂停在 Stdb 与 API 之间,保持维护模式并停止旧 API/controller/worker,完成压缩和摘要回填后才由指定审批人放行 API。
|
||||
- 影响范围:编辑器生成提交 workflow、`external_generation_job`、`external_generation_job_summary`、外部生成 procedure / typed client / BFF、SpacetimeDB bindings、历史数据维护流程和图片画布文档。
|
||||
- 验证方式:覆盖编辑器嵌套内联媒体与 payload 上限拒绝、非编辑器既有任务不被本轮门禁误伤、正式任务接口类型不含 payload、终态分批压缩不修改活动任务、已有 objectKey 不转 Data URL、本地标记图先上传再提交;运行外部生成定向 Rust / Vitest、`npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
|
||||
## 2026-07-10 BgFilter segModel 保留内部字段,不进入外部 OpenAPI
|
||||
|
||||
- 背景:`api-server` 的图片生成、图标 spritesheet 与 UI 素材提取请求仍可反序列化 `segModel`,并识别 `birefnet` / `anime-seg`,以兼容内部调用和既有任务;但 BgFilter 当前受服务进程内存与并发容量约束,不同分割模型的内存占用并非可由外部调用方自由选择的稳定契约。
|
||||
@@ -51,7 +83,7 @@
|
||||
## 2026-07-09 充值订单过期改为 SpacetimeDB scheduled 表触发
|
||||
|
||||
- 背景:旧充值过期处理使用 api-server 后台轮询 worker claim 普通 schedule 表,非 HTTP 的 external-generation-worker / controller 进程也可能启动同一过期任务;扩外部生成 worker 会意外放大微信查单 / 关单流量,并且本地过期后若微信仍可支付,容易出现“微信扣款但本地拒绝入账”的风险。
|
||||
- 决策:新建原生 scheduled 表 `profile_recharge_order_expiration_timer`,创建真实微信 pending 充值订单时写入 5 分钟 timer;scheduled reducer 到点只把仍为 `pending` 的订单改为 `expired` 并写 `expired_at`。HTTP `api-server` 订阅 `profile_recharge_order` 的 `Pending -> Expired` 更新并执行微信查单补偿;`SUCCESS` 允许 `Expired -> Paid` 入账,未支付或远端已终态只记录检查结果,本地保持 `expired`。`external-generation-worker` 和 controller 不处理充值过期。
|
||||
- 决策:新建原生 scheduled 表 `profile_recharge_order_expiration_timer`,创建真实微信 pending 充值订单时写入 5 分钟 timer;scheduled reducer 到点只把仍为 `pending` 的订单改为 `expired` 并写 `expired_at`。HTTP `api-server` 只订阅活跃 timer 表的删除事件,按 `order_id` 重新读取订单并仅对 `expired` 执行微信查单补偿;支付或主动关闭导致的 timer 删除会被状态判断忽略,断线窗口由未检查过期订单 catch-up 补齐,不订阅完整 `profile_recharge_order` 历史表。`SUCCESS` 允许 `Expired -> Paid` 入账,未支付或远端已终态只记录检查结果,本地保持 `expired`。`external-generation-worker` 和 controller 不处理充值过期。
|
||||
- 影响范围:`profile_recharge_order`、`profile_recharge_order_expiration_timer`、充值订单状态契约、`spacetime-client` bindings/facade、`api-server` 充值过期监听器、微信支付查单 / 关单、个人中心充值前端、后台表查询和运维文档。
|
||||
- 验证方式:`npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、充值过期 listener / 微信支付 / shared contracts / 前端充值定向测试、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
@@ -3941,3 +3973,43 @@
|
||||
- 性能:`api-server` 只在当前判定涉及的已启用 gate 配置了用户标签白名单时读取用户标签;不因无关 gate 或纯用户 ID / 百分比灰度触发额外标签读取。
|
||||
- 影响范围:`feature_gate_config`、`spacetime-client` runtime facade、`api-server` 创作入口配置与路由熔断、`apps/admin-web` 灰度发布页。
|
||||
- 验证方式:`npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo test -p module-runtime --manifest-path server-rs/Cargo.toml feature_gate`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml creation_entry_feature_gate`、`npm run admin-web:typecheck`、后台灰度页 Vitest、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-07-10 官网 SEO 与主站 SPA 404 边界
|
||||
|
||||
- 背景:主站 Nginx 和 Pingora 原先会把任意未知路径回退到 `index.html`,导致 soft 404;共享 `index.html` 也缺少首页 SEO head,robots 和 sitemap 请求会落入 SPA fallback。
|
||||
- 决策:新增真实 `robots.txt` 和仅首页的 `sitemap.xml`,首页共享 head 提供基础 SEO/OG/JSON-LD 文本但不使用未确认的 image/logo URL;首页 DOM 只保留一个稳定产品定位 H1。Nginx 与 Pingora 只允许当前完整 SPA 路径回退 `index.html`,同前缀未知路径必须返回 404;`/admin` 继续走独立子应用。路由变化必须同步三套 Nginx、Pingora、route parity matrix 和自动门禁。
|
||||
- 影响范围:`index.html`、`public/robots.txt`、`public/sitemap.xml`、首页组件、三套 Nginx、Pingora 网关和路由 parity 门禁。
|
||||
- 验证方式:前端定向测试与构建、`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`npm run check:pingora-gateway-smoke`、`npm run check:encoding`、`git diff --check`,部署后同时抽查根级未知路径和 `/creation/not-exist` 等同前缀未知路径。
|
||||
|
||||
## 2026-07-11 Jenkins Secret File 默认值与 dev 定时发布
|
||||
|
||||
- 背景:Stdb Build / Publish / Full Job 改用 Secret File 后,live Job UI 默认值为空且会被 SCM Jenkinsfile 覆盖;Full Job 的 04:00 timer 又与默认人工 rollout gate 冲突。dev 服务器不对外,允许定时完整发布。
|
||||
- 决策:三个 Jenkinsfile 将 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 默认固定为 `genarrative-spacetime-bootstrap-secret-dev-file`。Full Job 保留 04:00 timer,默认 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal`,按 Stdb → API → Web 完整发布 dev;三个下游 Build 都显式传 `PUBLISH_AFTER_BUILD=false`,防止提前发布和顺序漂移。人工维护窗口才选择 `pause-after-stdb` 并强制校验 approvers。
|
||||
- 凭据边界:Secret 原文以 Jenkins Secret File 为事实源;credential ID 与参数行为以仓库 Jenkinsfile 为事实源。旧 Secret Text 继续服务 Database Import / Export,不原地改类型或删除。
|
||||
- 影响范围:`jenkins/Jenkinsfile.production-full-build-and-deploy`、`jenkins/Jenkinsfile.production-stdb-module-build`、`jenkins/Jenkinsfile.production-stdb-module-publish`、生产运维门禁与 live Job 参数 schema。
|
||||
- 验证方式:`node --check scripts/check-production-ops-guardrails.mjs`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`;推送后用首阶段 fail-closed 运行刷新三个 live Job 参数,再只读核对 credential 默认值、`normal` 默认值与 timer。
|
||||
|
||||
## 2026-07-12 维护模式只拦截公网流量
|
||||
|
||||
- 背景:此前维护 marker 只对内网放行后台,内网排障或人工维护仍无法访问主站、普通 API 和 SpacetimeDB 路由;当前维护目标是隔离公网访问,不应阻断可信内网流量。
|
||||
- 决策:Nginx 与 Pingora 允许 IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local 来源在维护期间访问整站,包括主站页面与静态资源、普通 API、后台页面与 `/admin/api/**`、SpacetimeDB 路由。公网应用主站、普通 API、后台和 SpacetimeDB 路由继续维护响应,应用层鉴权不变。
|
||||
- 信任边界:Nginx 使用 TCP `$remote_addr`;Pingora 使用 TCP peer,只有同机 loopback Nginx 才可通过其强制覆盖的 `X-Real-IP` 传递原始地址,禁止使用 `X-Forwarded-For` 做维护放行判断。
|
||||
- 限制:网关放行不等于后端存活;`pause-after-stdb` 停止 api-server 时,内网普通 API 和后台 API 仍不可用。
|
||||
- 影响范围:生产 / dev Nginx 模板、维护 snippet、Pingora maintenance gate、Nginx 静态门禁与 Pingora smoke。
|
||||
- 验证方式:`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml`、`npm run check:pingora-gateway-smoke`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-07-12 Full 发布显式控制成功后维护状态
|
||||
|
||||
- 背景:Full Job 只能用 `STDB_API_ROLLOUT_MODE` 控制 Stdb 与 API 之间是否暂停,但 API Deploy 在 readiness 成功后固定执行 `maintenance-off.sh`,因此无法选择完整流水线结束后继续保留维护页。
|
||||
- 决策:Full Job 新增默认勾选的 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION`,并让 Stdb Publish、API Deploy 两个下游阶段固定保持维护;Web Deploy 成功后才由独立 `Exit Maintenance` 阶段按该参数决定是否调用 current release 随包 `maintenance-off.sh`。API Deploy Job 单独使用 `KEEP_MAINTENANCE_MODE`,将其转换为随包 `production-api-deploy.sh --keep-maintenance-mode`;默认仍退出维护,失败路径继续沿用 current 切换前后既有安全语义。
|
||||
- 参数刷新:Jenkinsfile 是参数事实源。推送后必须让 Full 与 API Deploy live Job 安全加载一次新 Jenkinsfile,再只读确认两个参数已进入 `config.xml`;只在 Jenkins UI 手工加参数不是持久修复。
|
||||
- 影响范围:Full / API Deploy Jenkinsfile、API 发布脚本、生产 API deploy fixture、生产运维门禁与 live Job 参数 schema。
|
||||
- 验证方式:`bash -n scripts/deploy/production-api-deploy.sh`、`npm run check:production-api-deploy`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-07-13 数据库冷备使用 OSS Multipart 上传并在清理前验真
|
||||
|
||||
- 背景:SpacetimeDB 冷备归档已经超过 OSS 单次 PutObject 的 5 GiB 上限,单请求上传会稳定失败并让本地归档持续积压;网络中断还可能让 CompleteMultipartUpload 的客户端结果不确定。
|
||||
- 决策:`scripts/database-backup-to-oss.mjs` 对备份归档统一使用 OSS Multipart Upload,默认按 128 MiB 顺序分片;每个分片请求重新创建文件流、时间和 V4 签名,仅对网络错误、HTTP 408 / 429 / 5xx 做有限重试。V4 canonical query 必须同时支持无等号的 `uploads` 子资源和带值的 `partNumber` / `uploadId` 参数。
|
||||
- 验真与清理边界:Complete 后必须发送签名 HEAD,并严格核对 OSS `Content-Length` 与本地归档大小;Complete 响应不确定时也先用 HEAD 判定对象是否已经完整落盘。只有验真成功后才能把 manifest 标记为 `uploaded`,并按 `keepLocal` 决定是否删除本地归档;失败时 best-effort AbortMultipartUpload,不得提前更新 manifest 或清理本地文件。
|
||||
- 影响范围:数据库备份 OSS 上传实现、备份回归门禁、release 本地归档保留与 timer 恢复流程。
|
||||
- 验证方式:`npm run check:database-backup`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`;线上先对既有归档使用 `--upload-archive ... --keep-local`,确认 OSS 对象长度和可恢复性后再清理积压并恢复 timer。
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 禁止 Data URL 持久化时不要漏掉异步任务 JSON
|
||||
|
||||
- 现象:工程、素材、图层和元数据都已禁止 Data URL 后,服务器仍在生成高峰出现 SpacetimeDB / api-server 内存急剧膨胀甚至 OOM;读取少量正式生成任务也会造成远大于响应体的瞬时内存增长。
|
||||
- 原因:同步接口 worker 化时把原请求整体序列化到 `external_generation_job.request_payload_json`,而前端又把已有 `objectKey` 下载成 Data URL 提交。任务表也是正式持久化边界;列表 procedure 若先收集完整任务行再截断,还会把 request/result 大字段在 SpacetimeDB、SDK mapper 和 BFF 多次持有。
|
||||
- 处理:先在事故涉及的编辑器持久任务 JSON 上由 api-server 与 SpacetimeDB 两层递归拒绝 `data:` / `blob:` 并限制字节数;已有媒体传 `objectKey` / `resourceId` / `assetId`,本地派生图先用强唯一 key 上传。其它玩法若仍以 Data URL 作为正式请求契约,必须先资源化,不能直接扩大门禁造成玩法回归。列表、详情和 acknowledge 只走无 payload 的摘要投影,ack 不能为了同步旧字段重写大任务行;摘要错误文本也必须清除内联媒体并设硬上限,列表只能维护有界 top-N,不能先收集 owner 全量历史再截断。历史只通过迁移操作员的 dry-run + B-tree cursor 分批 procedure 压缩 `editor-canvas` 终态任务,cursor 选择读取量必须受 limit 约束,绝不全表扫描、绝不处理 pending / running;dry-run 后 apply 同一批时保持输入 cursor 不变,最后一批即使 `has_more=false` 只要仍有命中也必须 apply,只有 apply 成功后才推进到返回 cursor。SpacetimeDB CLI 2.5 的 `Option<T>` 非空参数必须使用 SATS sum 编码;维护脚本要统一编码 `cursor_job_id`、`owner_user_id` 和 `completed_before_micros`,否则首批空 cursor 可运行,但第二批或带截止时间的调用会在写入前被拒绝。
|
||||
- 发布门禁:生产发布入口必须固定 `--delete-data=never` 与 scoped `--yes=migrate,break-clients`,普通 Jenkins 参数不得暴露清库开关;需要删数据的 schema 冲突必须直接阻断并重新检查 artifact/schema,不能靠裸 `--yes` 放行。
|
||||
- 验证:构造嵌套 Data URL、Blob URL 和超限 JSON 确认入队失败;检查正式 UI procedure / client record 不含 request/result payload;用 dry-run 和 apply 测试确认活动任务不变、终态普通提示词保留且内联媒体被替换;至少带一次非空 `--cursor-job-id` 与 `--completed-before-micros` 验证 CLI Option 编码,而不是只测首批空 cursor。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_generation_queue.rs`、`server-rs/crates/spacetime-module/src/external_generation.rs`、`server-rs/crates/api-server/src/external_generation.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## React 测试因内部状态或实现细节正常重构就碎
|
||||
|
||||
- 现象:修改组件结构、按钮排序、图标库 class、提示文案或 hook 内部状态名后,React 测试大量失败,但真实用户流程和对外契约没有变化。
|
||||
@@ -359,6 +368,14 @@
|
||||
- 验证:`npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand`;后端验证至少覆盖 `editor_image_edit_request_omits_price_mud_points` 和 `editor_image_edit_can_complete_by_replacing_target_layer`。
|
||||
- 关联:`src/services/image-editor/editorProjectClient.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`server-rs/crates/api-server/src/editor_project.rs`。
|
||||
|
||||
## 图片画布快速编辑尺寸要区分业务目标和 provider 对齐尺寸
|
||||
|
||||
- 现象:原图经过快速编辑后 Resolution 变成近似比例的 1K / 2K 预设;原图或框选标记图宽高不是 16 的倍数时,VectorEngine edits 直接拒绝请求。
|
||||
- 原因:前端已有源图精确 `originalWidth/originalHeight`,提交时却按最近常用比例和 K 档重新计算 `size`;后端又把非 16 倍数的目标尺寸和原始参考图字节直接放进 multipart,并以 provider 回图宽高落库和覆盖画布图层。
|
||||
- 处理:画布快速编辑提交源图精确尺寸作为业务目标;api-server 只在 provider 边界向右、向下复制边缘像素,把每张参考图和目标尺寸临时补齐到 16 的倍数,收到回图后裁回业务目标尺寸再持久化。若上游异常返回其他尺寸,先按目标比例裁切缩放;临时对齐尺寸不能进入 OSS 元数据、`editor_project_resource`、`editor_asset` 或画布 Resolution。前端 inline 回填也保留源图显示尺寸和 Resolution,避免旧回包再次放大图层。
|
||||
- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx` 覆盖 `1537x1025` 精确提交和源图回填;`cargo test -p api-server editor_image_edit_aligns_provider_images_and_restores_source_dimensions --manifest-path server-rs/Cargo.toml` 覆盖 provider `1552x1040`、额外参考图独立对齐和回图恢复。
|
||||
- 关联:`src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/ImageCanvasGenerationLayerModel.ts`、`server-rs/crates/api-server/src/editor_project.rs`。
|
||||
|
||||
## 图片画布快速编辑元数据必须记录原图引用
|
||||
|
||||
- 现象:快速编辑生成的新图可以替换画布,但打开图片信息时“生成输入”里看不到被修改的原图。
|
||||
@@ -2043,10 +2060,10 @@
|
||||
|
||||
- 现象:微信小程序支付下单能返回 `prepay_id`,但真实支付通知验签失败,或者本地实现误把商户 API 私钥当作回调验签 key。
|
||||
- 原因:商户私钥只用于商户请求微信支付和生成小程序 `paySign`;微信支付通知的 `Wechatpay-Signature` 需要使用微信支付平台公钥或平台证书公钥验签,并按通知头里的平台序列号匹配。
|
||||
- 处理:api-server 真实微信支付配置同时需要商户私钥与微信平台公钥:`WECHAT_PAY_PRIVATE_KEY_*` 用于签名,`WECHAT_PAY_PLATFORM_PUBLIC_KEY_*` 与 `WECHAT_PAY_PLATFORM_SERIAL_NO` 用于通知验签,`WECHAT_PAY_API_V3_KEY` 只用于解密通知 resource。支付成功后只通过通知里的 `out_trade_no` 确认本地 pending 订单,并保存 `transaction_id` 到 `profile_recharge_order.provider_transaction_id`。
|
||||
- 处理:api-server 真实微信支付配置同时需要商户私钥与微信平台公钥:`WECHAT_PAY_PRIVATE_KEY_*` 用于签名,`WECHAT_PAY_PLATFORM_PUBLIC_KEY_*` 与 `WECHAT_PAY_PLATFORM_SERIAL_NO` 用于通知验签,`WECHAT_PAY_API_V3_KEY` 只用于解密通知 resource。微信平台 `PUBLIC KEY` PEM 的 DER 内容是 SPKI `SubjectPublicKeyInfo`,初始化时必须解析并提取其中的 PKCS#1 `RSAPublicKey` DER 后再交给 `ring::RSA_PKCS1_2048_8192_SHA256`;不能把整段 SPKI DER 直接传给 `ring`。支付成功后只通过通知里的 `out_trade_no` 确认本地 pending 订单,并保存 `transaction_id` 到 `profile_recharge_order.provider_transaction_id`。
|
||||
- APIv3 通知成功应答使用 HTTP `204 No Content`,不要沿用 V2 XML 成功报文;失败仍返回 4XX/5XX 让微信重试。
|
||||
- 验证:mock 通知测试只能覆盖本地回调推进;真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
|
||||
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs`、`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`。
|
||||
- 验证:mock 通知测试只能覆盖本地回调推进;`platform-wechat` 必须用标准 SPKI `PUBLIC KEY` 和匹配私钥生成真实 RSA-SHA256 签名,覆盖 SPKI 到 PKCS#1 的解析与生产验签 helper。真实环境还需用微信支付平台公钥、真实通知头和 API v3 密钥验证签名与解密链路。
|
||||
- 关联:`server-rs/crates/platform-wechat/src/pay.rs`、`server-rs/crates/api-server/src/wechat/pay.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 微信支付 JSAPI 下单必须显式带 User-Agent
|
||||
|
||||
@@ -2084,10 +2101,17 @@
|
||||
|
||||
- 现象:外部生成 worker/controller 扩容后,微信充值过期查单和关单流量也被同步放大;排查时还会误去外部生成 worker 日志里找支付过期任务。
|
||||
- 原因:支付过期是账户资金链路,不是外部内容生成队列;旧实现把充值过期轮询 worker 挂在通用后台任务启动函数里,非 HTTP 角色也会启动。
|
||||
- 处理:充值订单过期由 SpacetimeDB 原生 `profile_recharge_order_expiration_timer` 到点把 `pending` 改为 `expired`,只有 HTTP `api-server` 订阅 `profile_recharge_order` 的 `Pending -> Expired` 更新并查微信补偿。未支付终态本地保持 `expired`,不要再改写成 `closed`;微信成功支付通知或补偿查单仍可把 `Expired -> Paid` 入账。
|
||||
- 处理:充值订单过期由 SpacetimeDB 原生 `profile_recharge_order_expiration_timer` 到点把 `pending` 改为 `expired`,只有 HTTP `api-server` 订阅活跃 timer 表的删除事件,按 `order_id` 重新读取订单并仅对 `expired` 查微信补偿;支付或主动关闭导致的删除信号会被状态判断忽略,断线窗口由未检查过期订单 catch-up 补齐。未支付终态本地保持 `expired`,不要再改写成 `closed`;微信成功支付通知或补偿查单仍可把 `Expired -> Paid` 入账。
|
||||
- 验证:确认 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` / `external-generation-controller` 不启动充值过期监听;创建 pending 充值单后只由 scheduled reducer 产生 `expired`,HTTP api-server listener 记录 `expiration_checked_at` 或补入账。
|
||||
- 关联:`server-rs/crates/api-server/src/profile_recharge_expiration_listener.rs`、`server-rs/crates/spacetime-module/src/runtime/profile.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 充值订单状态枚举不能用字符串 SQL 字面量订阅
|
||||
|
||||
- 现象:API 已 ready,但日志每 5 秒出现 `profile recharge expiration listener failed to subscribe`,并提示 `pending` 不能解析为 `profile_recharge_order.status` 的枚举类型;scheduled reducer 仍会把订单改成 `expired`,但微信查单补偿监听没有运行。
|
||||
- 原因:SpacetimeDB 2.6 不会把订阅 SQL 中的 `'pending'` / `'expired'` 字符串自动转换为生成绑定的 sum-type enum;两个按状态过滤的订阅都在应用阶段失败。
|
||||
- 处理:不要改成订阅完整 `profile_recharge_order` 历史表。后端订阅只保留活跃五分钟定时器的 `profile_recharge_order_expiration_timer`,监听 timer 删除后按 `order_id` 通过 procedure 读取订单,只处理当前状态为 `expired` 的记录;支付 / 关闭信号会被忽略,断线窗口继续由未检查过期订单 catch-up 补齐。这样既不依赖不受支持的枚举 SQL,也不会把充值历史常驻 API 客户端缓存。
|
||||
- 验证:运行 `cargo test -p spacetime-client profile_recharge_expiration --manifest-path server-rs/Cargo.toml`,发布后确认 API 日志不再出现订阅解析错误,并用真实 pending 订单验证 scheduled reducer 过期后写入 `expiration_checked_at`。
|
||||
|
||||
## 抓大鹅历史草稿外部 Rodin GLB 链接必须转存后再试玩或发布
|
||||
|
||||
- 现象:草稿页预览模型失败并报 `GL_INVALID_ENUM: Invalid cap.`,或结果页能看到历史生成记录但试玩、发布和正式运行态仍显示默认积木。
|
||||
@@ -2928,3 +2952,41 @@
|
||||
- 处理:产品 UI 不提供模型选择,应用内调用固定 `birefnet`;外部编辑器 OpenAPI 不声明 `segModel`,并保持相关请求 schema 的 `additionalProperties: false`,使外部请求携带该字段时被契约拒绝。只有维护 BgFilter 模型与容量的后端代码可使用该内部字段;若未来需要开放,先完成各模型的内存、并发和超时压测,再明确版本化外部契约。
|
||||
- 验证:检查 `docs/openapi/genarrative-external-v1.openapi.json` 的图片生成、图标 spritesheet 和 UI 素材提取请求 schema 均未包含 `segModel`,且均保持 `additionalProperties: false`。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`src/services/image-editor/editorProjectClient.ts`、`docs/project-memory/shared-memory/decision-log.md`。
|
||||
## SPA 路由白名单不能只按一级目录放行
|
||||
|
||||
- 现象:`/not-exist` 已返回 404,但 `/creation/not-exist`、`/runtime/not-exist` 或 `/puzzle/not-exist` 仍返回 200 首页,搜索引擎继续判定为 soft 404。
|
||||
- 原因:Nginx 或 Pingora 使用 `/creation/*`、`/runtime/*` 等宽前缀作为 SPA fallback,前端对未知路径又回到平台首页;只验收根级未知 URL 无法发现该问题。
|
||||
- 处理:SPA fallback 必须精确匹配当前真实完整路径,同时允许前端已有的大小写归一和尾部斜杠;最终 catch-all 只提供真实静态文件,失败返回 404。路由增删同步三套 Nginx、Pingora、route parity matrix 和路由门禁。
|
||||
- 验证:除全部真实 SPA 路径外,至少检查 `/not-exist`、`/creation/not-exist`、`/runtime/not-exist` 和 `/puzzle/not-exist` 均返回 404;维护模式仍保持页面 503 优先语义。
|
||||
- 关联:`src/routing/appRoutes.tsx`、`src/routing/appPageRoutes.ts`、`deploy/nginx/`、`deploy/container/nginx.conf`、`server-rs/crates/pingora-gateway/src/main.rs`。
|
||||
|
||||
## Jenkins Job UI 参数会被 SCM Jenkinsfile 覆盖
|
||||
|
||||
- 现象:在 Jenkins Job 页面给 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 配了默认值,下一次加载 Declarative Pipeline 后又变空或恢复旧描述;04:00 Full Job 还可能因默认选择 `pause-after-stdb` 且 approvers 为空而失败。
|
||||
- 原因:这些 Job 使用 Pipeline script from SCM,`parameters {}` 和 `triggers {}` 会作为 Job property 回写现场配置;只改 UI 不是持久修复。构建编排如果不显式关闭下游 `PUBLISH_AFTER_BUILD`,还会受下游默认值漂移影响。
|
||||
- 处理:credential ID 和参数默认值写回三个 Jenkinsfile;仅供开发使用的 dev 定时 Full Job 默认 `STDB_API_ROLLOUT_MODE=normal`,三路 Build 调用显式传 `PUBLISH_AFTER_BUILD=false`,再由 Full Job 统一按 Stdb → API → Web 发布。Secret 原文只放 Jenkins Secret File,旧 Secret Text 保留给 Import / Export。
|
||||
- 验证:推送后让 Full / Stdb Build 用不存在的源码分支在 checkout 阶段 fail-closed,让 Stdb Publish 用空构建版本在 Prepare 阶段 fail-closed,以安全刷新参数 schema;随后只读检查三个 live `config.xml` 的参数描述和默认值,确认 Full timer 仍为 `0 4 * * *`、rollout 默认值为 `normal`,并确认刷新运行未进入 publish / deploy stage。
|
||||
- 关联:`jenkins/Jenkinsfile.production-full-build-and-deploy`、`jenkins/Jenkinsfile.production-stdb-module-build`、`jenkins/Jenkinsfile.production-stdb-module-publish`、`scripts/check-production-ops-guardrails.mjs`。
|
||||
|
||||
## 维护模式内网全站放行不能信任 X-Forwarded-For
|
||||
|
||||
- 现象:维护期间希望让内网继续访问整站,如果直接按 `X-Forwarded-For: 192.168.x.x` 放行,公网请求可伪造该头绕过维护闸;如果仍按路径只放行后台,又会让内网主站和普通 API 继续返回 503。
|
||||
- 原因:XFF 是客户端可提交的普通请求头,当前 Nginx 的 `$proxy_add_x_forwarded_for` 还会保留已有前缀;维护放行属于授权判断,必须建立在不可伪造的网络来源边界上,并在路由分类前按来源统一决定是否绕过维护闸。
|
||||
- 处理:Nginx 按 TCP `$remote_addr` 判断内网;Pingora 按 TCP peer 判断,只有 peer 为 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的 `X-Real-IP`。可信内网来源绕过整站维护响应,公网应用主站、普通 API、后台和 SpacetimeDB 路由仍保持维护响应;绝不能用 `X-Forwarded-For` 做放行判断。
|
||||
- 验证:Pingora smoke 同时覆盖公网主站、普通 API、后台为 503,以及内网对应路由为 200;Rust 单测覆盖 IPv4 / IPv6 内网、公网和空来源;Nginx 静态门禁反查两份模板的内网来源定义与全局维护变量清零逻辑。
|
||||
- 限制:如果发布门禁已经停止 api-server,网关放行后普通 API 和后台 API 仍会失败;需要调用后端时应确保对应服务仍运行,不能把维护页绕过误当作服务可用性保证。
|
||||
|
||||
## Full 结束后保持维护不能只加一个 UI 参数
|
||||
|
||||
- 现象:Full Job 参数页没有“完整发布成功后是否退出维护”选项,或者补了选项后 API readiness 一通过仍自动撤掉维护。
|
||||
- 原因:维护退出发生在随 API artifact 发布的 `production-api-deploy.sh` 内;Full、API Deploy Job 和脚本任一层没有透传,最终都会回到固定执行 `maintenance-off.sh`。Declarative Pipeline 参数还要等 live Job 加载新版 Jenkinsfile 后才会刷新。
|
||||
- 处理:Full 使用 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 表达产品选择,Stdb Publish 和 API Deploy 全程固定保持维护,Web Deploy 成功后才进入独立最终退出阶段;API Deploy 的独立 `KEEP_MAINTENANCE_MODE` 再转换为脚本 `--keep-maintenance-mode`。API deploy 还必须把 `production-api-deploy.sh`、`maintenance-on.sh` 和 `maintenance-off.sh` 从同一 build artifact 复制进 current release,否则 Full 最终阶段即使有选项也找不到随包退出脚本。默认值仍在 Full 结束时退出维护,避免定时 dev 发布行为变化。
|
||||
- 验证:API deploy fixture 必须覆盖成功发布并保留 marker,还要断言 current release 中三个部署 / 维护脚本存在;生产运维静态门禁同时反查 Full 参数、下游透传、API Deploy 参数和脚本 flag。推送后用 fail-closed 首阶段运行刷新 live Job 参数,再核对 `config.xml`,不能只看仓库文件。
|
||||
|
||||
## 遮罩点击关闭必须校验完整指针序列
|
||||
|
||||
- 现象:在弹窗内容内按下鼠标,拖到弹窗外的遮罩上松开时,弹窗被误关闭。
|
||||
- 原因:只在 `click` 阶段判断 `event.target === event.currentTarget` 不足以确认用户点击了遮罩;跨弹窗边界松开时,浏览器可能把合成点击的目标归到弹窗和遮罩的共同祖先。
|
||||
- 处理:共享弹窗统一记录 `pointerdown` 与 `pointerup` 的目标,只有按下和松开都发生在遮罩自身时才允许关闭。新增弹窗优先复用 `UnifiedModal`,不要继续复制只判断最终 `click` 目标的手写遮罩逻辑。
|
||||
- 验证:回归测试同时覆盖“弹窗内按下、遮罩松开不关闭”和“遮罩按下、遮罩松开正常关闭”。
|
||||
- 关联:`src/components/common/UnifiedModal.tsx`、`src/components/common/UnifiedModal.test.tsx`、`src/components/auth/PlatformAuthModalShell.test.tsx`。
|
||||
|
||||
@@ -36,7 +36,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
|
||||
server-rs + Axum + SpacetimeDB
|
||||
```
|
||||
|
||||
当前 SpacetimeDB crate、SDK、CLI / standalone、生成 bindings 和容器压测镜像统一按 `2.5.0` 对齐;遇到版本不匹配时先升级到 `server-rs/Cargo.toml` 锁定版本,升级后重启对应 SpacetimeDB 进程再重试。
|
||||
当前 SpacetimeDB crate、SDK、CLI / standalone、生成 bindings 和容器压测镜像统一按 `2.6.0` 对齐;遇到版本不匹配时先升级到 `server-rs/Cargo.toml` 锁定版本,升级后重启对应 SpacetimeDB 进程再重试。
|
||||
|
||||
职责边界:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user