feat(edit): AI套图生成、图片切七牛

This commit is contained in:
2026-10-04 20:47:47 +08:00
parent 696112e74d
commit e20cd7547f
60 changed files with 6241 additions and 312 deletions
+3
View File
@@ -49,6 +49,7 @@
- 包装输入是编辑草稿合并后的有效 g/mm 值,必须已确认;接口为兼容历史值允许最多三位小数,重量至少 1 g,当前编辑页与试算界面只接受正整数。保存时从实际持久化草稿重新合并并核对,不能仅信任客户端摘要。
- `mode=target_markup`:目标利润=采购成本×加价率;求解销售价。`fixed_sale`:selling_price 必填,按该售价核算利润。
- 百分比费用以销售额为基数;除加价率外每项小于 1、最多六位小数。引擎拒绝总扣费率≥1。划线价=销售价÷(1−折扣率)。
- `selling_price` 在编辑页显示为「实际售价」,利润按实际售价计算。`list_price` 显示为「划线价」,两价在试算结果中同行展示;首次上架 Ozon 的 `price` 使用划线价,不发送 `old_price`,上线后用户再在 Ozon 调整实际售价(2026-10-04 维护者要求)。人民币摘要按报价快照换算两价,不使用当前汇率改写历史报价。缺失或低于实际售价的划线价须重新确认报价后发布。
- 枚举运输方式×产品类型×佣金档,按最终币种舍入后回验货值、佣金、重量和体积重边界。默认运输不可用时 recommended_option_id=null,不替换为另一方式。
- `fx.mode=system` 读取 PostgreSQL 最新 CBR;`policy_fixed` 必须绑定有固定汇率的方案;`one_time_override` 必须明确 rate 和 reason。固定汇率取方案值,不信任客户端覆盖。采用的汇率固定在本次预览中,15 分钟内确认不重新拉取外部汇率。
- 只有不存在有效正式佣金规则且无方案或方案允许时,才能 temporary_commission_rate。已有正式规则禁止覆盖。
@@ -95,6 +96,8 @@ context 保存规则版本、实际汇率与来源,以及系统汇率记录 ID
写入 `{name,note,parameters,allow_commission_override}`。只保存费用、加价、折扣、固定/系统汇率与运输偏好;采购价、包装、手动售价及临时佣金不属于方案持久字段。
方案的默认和比较运输必须属于同一可用费率卡;正式环境默认允许有效草稿卡;可通过 `PRICING_ALLOW_DRAFT_RATES=false` 仅使用已审核卡。历史快照不会被方案修改;尚未确认的预览因方案版本改变要求重算。
2026-10-04 维护者确认:SKU「重新试算」与「按方案计算全部」主动重读目标店铺 `/pricing/catalog`,使用最新 `is_default=true` 方案的完整参数(包含加价、折扣、费用、运输与汇率偏好),替换 SKU 的旧定价参数再生成预览;沿用当前采购成本和有效包装,不自动保存商品。无默认方案或其费率卡不可用时给出明确提示,不静默使用折扣率为 0 的临时参数。已导入 SKU 不重算。自动试算、手工实际售价调整和试算详情内的重新计算继续使用当前 SKU/详情参数。默认方案折扣率为 50% 时,划线价=实际售价÷(1−50%)=实际售价×2。
## 5. 数据启用与错误
迁移中的 CEL 目录为 `draft`,覆盖六种产品类型、三种运输方式,香港空运不纳入。平台类目佣金映射仍是正式试算所需数据;草稿物流费率可按上述配置参与报价,页面保留费率来源标识,接口不会回退到未选定的固定佣金。物流费率审核用于形成有证据的已审核版本,不再是草稿报价上架预检的前置条件。
+9 -3
View File
@@ -94,7 +94,7 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
### 单张 AI 图片编辑(SKU 选图弹窗)
商品编辑页固定任务入口使用 `GET /api/products/{product_id}/image-tasks?limit=20&cursor=...`。`limit` 默认 20、最大 100,按进行中优先、创建时间倒序、ID 倒序稳定分页,返回 `{ data: ProductImageTask[], meta: { next_cursor, has_more } }`。接口只返回当前工作空间、当前商品的图片 AI 任务;首期包含单张编辑,后续套图任务按同一结构加入。每项包含 `kind: single_ai_edit | ai_suite`、`status`、`phase`、已完成数/总数、创建/完成时间、稳定错误码、`failure_source: pcore | atlas | null`、`failure_stage: task_setup | input_preparation | provider_submission | provider_polling | result_download | result_processing | result_storage | null`、`failure_detail: string | null`、`provider_error_raw: string | null` 与结果素材 ID 数组。失败来源表示 Pcore 接口返回/调用失败或 Atlas 自身处理失败;步骤指出实际出错的位置。`failure_detail` 是最长 500 字符的脱敏诊断摘要,不包含密钥、对象键或预签名 URL。Pcore 失败时 `provider_error_raw` 保留供应商原始错误响应文本(JSON 响应保存错误对象,最多 32,768 字符),便于在当前工作空间的任务列表中查看;没有供应商响应时为 `null`。历史任务未记录的字段返回 `null`,不得按通用错误码猜测失败来源。`GET /api/media-ai-edits/{task_id}` 的单张编辑响应也返回三个诊断字段,供未关闭的编辑弹窗显示。不返回供应商任务 ID、对象键或预览 URL。任务入口每 3 秒刷新运行中的任务,其余时间低频刷新;任务提交后即时刷新。查看结果时再用素材访问接口签发短时预览 URL。单张编辑任务取消沿用 `POST /api/media-ai-edits/{task_id}/cancel`。
商品编辑页固定任务入口使用 `GET /api/products/{product_id}/image-tasks?limit=20&cursor=...`。`limit` 默认 20、最大 100,按进行中优先、创建时间倒序、ID 倒序稳定分页,返回 `{ data: ProductImageTask[], meta: { next_cursor, has_more } }`。接口只返回当前工作空间、当前商品的图片 AI 任务;包含单张编辑和套图任务;套图汇总为整套任务,内部子任务不重复展示。每项包含 `kind: single_ai_edit | ai_suite`、`status`、`phase`、已完成数/总数、创建/完成时间、稳定错误码、`failure_source: pcore | atlas | null`、`failure_stage: task_setup | input_preparation | provider_submission | provider_polling | result_download | result_processing | result_storage | null`、`failure_detail: string | null`、`provider_error_raw: string | null` 与结果素材 ID 数组。失败来源表示 Pcore 接口返回/调用失败或 Atlas 自身处理失败;步骤指出实际出错的位置。`failure_detail` 是最长 500 字符的脱敏诊断摘要,不包含密钥、对象键或预签名 URL。Pcore 失败时 `provider_error_raw` 保留供应商原始错误响应文本(JSON 响应保存错误对象,最多 32,768 字符),便于在当前工作空间的任务列表中查看;没有供应商响应时为 `null`。历史任务未记录的字段返回 `null`,不得按通用错误码猜测失败来源。`GET /api/media-ai-edits/{task_id}` 的单张编辑响应也返回三个诊断字段,供未关闭的编辑弹窗显示。不返回供应商任务 ID、对象键或预览 URL。任务入口每 3 秒刷新运行中的任务,其余时间低频刷新;任务提交后即时刷新。查看结果时再用素材访问接口签发短时预览 URL。单张编辑任务取消沿用 `POST /api/media-ai-edits/{task_id}/cancel`;套图取消与失败重试见 [商品 AI 套图](./product-image-suites.md)。
`POST /api/media-assets/{asset_id}/ai-edits`:必须带 `Idempotency-Key`,请求体带当前工作空间 `product_id`,图片字段只使用正式素材 ID,不接受图片 URL。`asset_id` 为固定主图;`reference_asset_ids` 最多两项,仅作辅助参考。请求支持 `prompt`、`mode: edit | outpaint`、`ratio: 3:4 | 1:1 | 4:5`、图中文字翻译与目标语言、背景模式与描述、五种可改写的视觉风格、商品长宽高及尺寸标注图。`outpaint` 只允许 3:4、单主图,不生成尺寸标注。成功返回 HTTP 202、任务 ID 和 `queued` 状态。
@@ -283,12 +283,12 @@ Headers:`Idempotency-Key` 必填。Body:
AI 填报接口只创建异步任务并返回 `202` + `ai_task_id`,不直接改写已保存草稿或发布数据。任务完成后,编辑页自动把校验合格的字段回填到**未保存表单**,再由用户使用现有 `editor-draft` 保存。详细字段规则、冲突策略与中文对照存储见[商品 AI 填报方案](../product/ai-autofill-plan.md)。
创建 AI 任务前,前端必须先保存当前编辑草稿。服务端从已持久化的 `ai_input` 组装输入,不接收客户端自行拼装的规格。填报任务读取全部 SKU 的规格摘要,并固化商品、profile、采集和类目目录版本;套图和尺寸图任务只读取目标 SKU 的 `effective_physical_spec` / `effective_package_spec`。因此 AI 使用的是用户在编辑页保存后的资料,而不是浏览器临时拼接的采集载荷。
创建填报、文案或专用尺寸图 AI 任务前,前端必须先保存当前编辑草稿,服务端从已持久化的 `ai_input` 组装输入。填报任务读取全部 SKU 的规格摘要,并固化商品、profile、采集和类目目录版本。2026-10-03 经维护者确认的套图抽屉采用独立可编辑资料:目标 SKU 必须已经保存;名称、颜色、实物尺寸/重量、采集信息、卖点和逐张方案作为本次套图输入快照保存,不自动改写商品字段或发布规格。此行为及正式接口以 [商品 AI 套图](./product-image-suites.md) 为准;替换此前尚未实现的 profile 级套图规划接口。
| 接口 | body 核心字段 | 成功结果 |
|---|---|---|
| `POST /api/product-platform-profiles/{profile_id}/ai/autofills` | 可选 `hint` 与策略开关;Header 必填 `Idempotency-Key` | 一次创建类目、型号、未导入 SKU 货号、可证包装规格及中俄文案的填报任务。Ozon 来源先运行确定性类目匹配;其他来源使用受限类目树候选。 |
| `POST /api/product-platform-profiles/{profile_id}/ai/suite-plans` | `variant_ids` 必填、`prompt` 可选 | 每个目标 SKU 明确绑定,创建套图规划/生成任务。 |
| `POST /api/products/{product_id}/ai/suite-plans` | `variant_id`、底图、套图资料与配置 | 创建显式绑定一个目标 SKU 的异步逐张规划;生成由 `POST /api/products/{product_id}/ai/image-suites` 提交。详见 [套图契约](./product-image-suites.md)。 |
| `POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/ai/dimension-images` | 可选样式 | 固化该 SKU 合并后的**实物**规格快照;实物规格缺失/待确认返回 `422`。 |
`GET /api/product-ai-tasks/{ai_task_id}` 返回任务状态、输入版本、脱敏结果或错误;全局填报成功时返回类目或候选、型号、逐 SKU 货号、可证包装建议及 SPU 中俄文案,不生成 SKU 文案。单 SKU 文案由 `POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/ai/copy` 单独创建任务,在弹窗预览并由用户应用。Ozon 动态类目属性不属于当前 AI 填报范围,仍通过类目确认后加载的 schema 由用户填写。歧义类目仍需用户选择。中文对照与俄文在下一次 `editor-draft` 中一同保存,读取编辑上下文时一同返回。
@@ -309,6 +309,12 @@ Headers:`X-Atlas-Platform-Id`、`Idempotency-Key`、`If-Match: "product_revisi
同一事务创建 `product_publish_jobs` / `product_publish_job_items`、将 listing 置为 `submitting`,写 Outbox(只含 job ID)。冻结商品/profile 版本、店铺、货号、Ozon 类目和属性、十进制价格、逐 SKU 标题/标签/描述、有效包装规格、素材 ID / checksum / 对象身份;签名 URL 不持久化。标题按 `title_mode` 选择,标签/描述按 SKU 覆盖回退。货号最多 50 字符,发布标题最多 500 字符,每 SKU 1–30 张图。首期仅 rFBS,不传 SPU 共用条码、库存、仓库、顶层 description、视频或富内容;中国主体跨境 rFBS 首期省略 `vat`,实际导入与商品信息税率核对后再决定。
经维护者 2026-10-04 要求,首次上架的 Ozon `price` 使用已确认报价的 `listing.list_price`(划线价),保留十进制金额字符串精度,不发送 `old_price`。`selling_price` 在 Web 显示为「实际售价」,表示后续折扣后的目标售价,利润仍按此金额试算;划线价为初始上架预留折扣空间,商品上线后由用户在 Ozon 调整实际售价。预检与任务创建要求划线价大于 0 且不低于实际售价,缺失或不合法时返回 `LIST_PRICE_REQUIRED` / `LIST_PRICE_INVALID`,要求重新确认报价,不回退为实际售价。折扣率为 0 时两价相同,正常发布。上架确认页同时展示实际售价和划线价(本次上报价)。已创建任务继续使用冻结的价格快照,已导入 SKU 不因编辑草稿而更新远端价格。
主题标签在编辑与草稿中仍为数组,支持最多 30 个标签。生成 Ozon 属性 `23171` 时,按 SKU 覆盖或 SPU 继承取得有效标签,去除空项并用空格拼成一个 `values[].value`,例如 `tags: ["#boho", "#loft"]` 映射为 `values: [{ "value": "#boho #loft" }]`;无标签时不提交该属性。schema 的 `max_value_count=1` 限制的是属性值条目数,不是其中的标签个数,不得据此拒绝多标签草稿。
每个数组项表示一个标签,必须以单个 `#` 开头,正文只允许 Unicode 字母、数字和下划线,标签内不得包含空格或其他 `#`;每项最多 30 个字符(含 `#`)。Web 对手工输入显示具体格式提示,草稿仍可保存供后续修改;发布预检及创建任务按所选 SKU 的有效标签(SKU 覆盖或 SPU 继承)检查,不合规时返回 `OZON_HASHTAG_INVALID` 字段问题并阻止发布。未被选中或被 SKU 覆盖的通用标签不阻断当前发布。AI 全局填报和单 SKU 文案提示词采用同一格式要求;输出回填前补齐缺失的 `#`,将词间空白、连字符转为下划线,过滤仍含非法字符、重复或超长的标签,并保留对应中文对照。不自动改写手工及历史草稿标签。
```json
{ "data": { "publish_job": { "id": "uuid", "profile_id": "uuid", "store_id": "uuid", "status": "queued", "item_count": 2, "created_at": "ISO8601", "completed_at": null, "items": [] } } }
```
+41
View File
@@ -0,0 +1,41 @@
# 商品 AI 套图
本次交付负责人为当前集成会话,范围为 Web、API、Worker、contracts、database。经维护者确认三列右侧抽屉设计,新增契约而不改写已有选图/保存接口。依赖商品、SKU、正式素材、operations、outbox 与既有单图生图任务。验收:SKU 归属校验、最多 8 张底图、默认 4 张与可编辑逐张方案、异步智能规划、后台生成/取消/失败重试、结果回填表单及跨租户隔离。
所有接口要求当前工作空间鉴权。POST 均要求 UUID `Idempotency-Key`;异步创建返回 202 `{ data: ImageSuite }`,包含 `operation`,Outbox 仅保存任务 ID。GET 返回 200 同一资源,预览地址按次签发。接口不自动修改商品草稿。
- `POST /api/products/:productId/ai/suite-plans`:请求 `ImageSuitePlanInput`;异步调用 DeepSeek,生成 `shots`。失败显式返回任务错误,不将默认方案冒充 AI 规划。
智能规划请求在 `ImageSuiteInput` 上增加可选 `image_count`(整数 1–20,默认 4),以当前方案总张数作为预算;生成接口不接收此字段。Worker 使用 `.env` 的 `DEEPSEEK_API_KEY`、`DEEPSEEK_MODEL_NAME` 调用 DeepSeek,关闭 thinking 并要求 JSON。规划按当前 SKU 的商品资料、核心卖点/要求、实物参数、风格、比例及文字语言重新分配用途,必须精确返回预算张数,每类最多 5 张。每张承担一个不同的信息重点;首张为白底主图,优先仅 1 张(大预算可增加不同白底角度),其他用途按品类和可信资料选择,不强制插入场景、尺寸或材质图。底图 ID 不代表图像内容已被识别,不能据此编造细节;尺寸图必须有带单位的实物参数。方案经过共享契约和预算/主题校验,无效结果重新请求一次;取消后不回写结果,失败保留前端当前方案。不自动开始生图,用户核对后显式点击开始生成。
- `POST /api/products/:productId/ai/image-suites`:请求 `ImageSuiteInput` 加 `shots`;固化资料、规格、风格、配置、逐张方案并创建生图子任务。每张使用不超过 3 张底图,第一张是主体图。
- `GET /api/products/:productId/ai/image-suites`:`limit` 默认 20、最大 100、`cursor` 按创建时间及 ID 倒序分页,供重开抽屉恢复任务。
- `GET /api/products/:productId/ai/image-suites/:suiteId`:任务详情,包括逐张状态、脱敏失败诊断及结果正式素材。
- `POST /api/products/:productId/ai/image-suites/:suiteId/cancel`:取消规划或未完成的生图子任务;成功素材保留,Worker 不再回写取消项。
- `POST /api/products/:productId/ai/image-suites/:suiteId/retry`:只重试失败/取消的子任务,保留成功项;运行中的整套返回 409。规划失败重新创建规划任务。
`ImageSuiteInput`:`variant_id` 必填,必须属于当前商品;`reference_asset_ids` 1–8 个不重复正式图片;`product_name` 必填;`color`、`dimensions`(实物文字)、`weight`(实物文字)、`collected_info`、`selling_points` 可编辑。Web 尺寸由长/宽/高三个 `InputNumber` 组成,默认 CM,可切换 MM/M 并同步换算;提交时仍以带轴名称和单位的字符串保存到 `dimensions`,空轴省略。实物重量同样使用 `InputNumber`,默认 g,可切换 kg 并同步换算;提交到 `weight` 的仍是带所选单位的字符串(例如 `1.25 kg`),为空时不生成重量标注,已填写数值必须大于 0。`collected_info` 展示为「采集信息/描述」,`selling_points` 展示为「核心卖点/要求」;`ratio` 为 3:4(默认)、1:1、4:3;`language` 为 ru(默认)、zh、en、ar;`style_set` 1–5、`style_prompt` 为可选风格描述。
每个 `shot` 包含唯一 `id`、`category: white | scene | selling | other`、`type`、`theme`、`description`、`evidence` 与可选 `reference_asset_ids`(1–3,必须属于套图底图库)。每类最多 5 张,总共 1–20 张;缺少实物尺寸时不接受尺寸图。默认 4 张:1 白底主图 + 1 场景展示图 + 1 核心卖点图 + 1 细节特写图(分类数量为 1/1/2/0);缺资料时以真实外观/细节取代无依据的尺寸、材质、对比或配件图。
`ImageSuite` 包含 `id`、`product_id`、`variant_id`、`purpose: plan | generate`、`status: queued | running | succeeded | partial | failed | cancelled`、`input`、`shots`、`items`、`error_code`、`created_at`、`completed_at`、`operation`。`items` 为逐张 `shot_id` + `edit_id` + 既有 `MediaAiEdit`。生成任务已完成但部分失败返回 partial,可保留成功图并重试失败项。统一图片任务入口将整套作为一项 `kind: ai_suite` 展示,子任务不重复出现;成功子图自然进入当前商品 AI 生图记录。
取消和重试使用整套资源锁;数据库复合外键强制 SKU/商品/工作空间与子任务同商品归属。底图库关联表强制素材工作空间归属;所有表 FORCE RLS。触发器依据子任务事实汇总整套进度及 operation,队列不是事实来源。
规划请求示例(UUID 为示例值):
```json
{
"variant_id": "11111111-1111-4111-8111-111111111111",
"reference_asset_ids": ["22222222-2222-4222-8222-222222222222"],
"product_name": "可调角度台灯",
"collected_info": "灯头可调节角度",
"selling_points": "突出角度调节,画面简洁",
"image_count": 4,
"ratio": "3:4",
"language": "ru"
}
```
返回 `202 { data: ImageSuite }`,`purpose=plan`、`status=queued`、`input.image_count=4`,不创建生图子任务。轮询完成后 `status=succeeded`、`shots.length=4`;每项包含主题、画面描述、资料依据及参考图 ID。若配置缺失,任务为 failed,错误码为 `DEEPSEEK_NOT_CONFIGURED`,诊断提示检查 Worker 的 API Key 和模型名称。
规划校验失败分别使用 `AI_SUITE_PLAN_SCHEMA_INVALID`、`AI_SUITE_PLAN_COUNT_MISMATCH`、`AI_SUITE_PLAN_MAIN_IMAGE_INVALID`、`AI_SUITE_PLAN_DUPLICATE_THEME`、`AI_SUITE_PLAN_DIMENSIONS_UNVERIFIED`;结果保存失败使用 `AI_SUITE_PLAN_SAVE_FAILED`,未知执行异常使用 `AI_SUITE_PLAN_FAILED`。Worker 对每次校验失败及最终失败打印安全的结构化日志,包含任务 ID、阶段、尝试序号和字段校验路径;最终诊断保存至 operation.result.failure(内部诊断,不新增 HTTP 字段)。不保存商品资料、模型原文或凭据。第二次规划携带具体校验规则及路径,仍须通过完整校验,不能截断或用默认方案替换。
@@ -2,7 +2,7 @@
> 文档日期:2026-09-30。操作示例:腾讯云 Linux 服务器、Ubuntu 24.04、DNSPod 中国站、七牛 Kodo 私有空间与 CDN 加速域名、Let's Encrypt、acme.sh 3.1.6。
>
> 状态:操作手册;文档中的服务器安装、DNS 修改、证书申请和七牛部署尚未在你的账号执行。操作系统、DNS 托管商和实际域名需要按现场情况替换。本次仅更新操作文档,不变更应用代码、存储配置或已有素材。
> 状态:2026-10-03 已完成 Let's Encrypt staging 与正式证书签发,正式证书的颁发者、有效期和 SAN 已由维护者提供的 OpenSSL 输出确认;七牛 hook 对两个图片域名返回部署成功,两个图片域名的网络检查均返回本次正式证书的颁发者、有效期,且指纹一致。服务器使用 1Panel/OpenResty,Atlas 生产、开发应用尚未部署。定时自动续期仍待验收;网站 HTTPS 接入留待应用部署时完成。现场进度见第 6.2 节、第 7 节、第 8.2 节、第 9.2 节和第 10.2 节。操作系统、DNS 托管商和实际域名需要按现场情况核对。
## 1. 要完成的事情
@@ -338,6 +338,14 @@ install -d -m 700 /var/lib/atlas-cert/acme-staging
RSA 2048 用作兼容性示例,避免后续文件目录和 `--ecc` 参数混用。acme.sh 会创建临时 TXT、等待 DNS 验证并清理;首次可能需几分钟。成功后在 DNSPod 检查本次 TXT 已清理,且两个图片 CNAME 保持正确。
### 2026-10-03 现场执行记录
- 服务器上的 `atlas-cert` 账号运行上述 staging `--issue` 命令,于北京时间 2026-10-03 11:02:09 收到证书文件路径及续期窗口输出;这确认测试环境签发已完成,不能视为正式证书或七牛部署验收。
- 测试证书位于 `/var/lib/atlas-cert/acme-staging/atlas.joeyone.cn/`:叶证书 `atlas.joeyone.cn.cer`、私钥 `atlas.joeyone.cn.key`、中间证书 `ca.cer`、完整证书链 `fullchain.cer`。这里只记录路径,不记录证书或私钥内容。
- 本次 staging 输出的 ARI 建议窗口为 `2026-12-01T04:47:36Z`~`2026-12-02T23:58:26Z`,所选下次续期时间为 `2026-12-01T20:38:56Z`;这些是**测试证书**的输出,不能用作正式证书续期日程。
- DNSPod 自动验证已使签发完成;临时 TXT 是否清理、两个图片域名 CNAME 是否保持正确仍需在 DNSPod 复核。随后按第 7 节申请正式证书,并用 `openssl` 检查正式证书的 SAN 和有效期。
- 正式签发和部署前,确认此前截图中暴露的腾讯云及七牛凭据均已停用、更换,并核对 `secrets.env` 与 acme.sh 缓存中的凭据。不要在项目文档或排错日志中保存真实密钥。
如果有 CAA 记录,确保它允许 Let's Encrypt 的 `letsencrypt.org` 签发;不要为排错直接删除业务已有 CAA。DNS-01 验证原理参见[Let's Encrypt 官方说明](https://letsencrypt.org/docs/challenge-types/)。
## 7. 申请正式证书
@@ -369,6 +377,21 @@ openssl x509 \
证书私钥仅用于证书部署,不是七牛 SK,也不是 Ozon 店铺 API Key。
### 2026-10-03 正式签发记录
维护者在北京时间 2026-10-03 11:08:13 收到正式证书签发成功输出,证书目录为 `/var/lib/atlas-cert/.acme.sh/atlas.joeyone.cn/`。随后运行本节的 OpenSSL 检查,结果如下;尚未据此确认七牛或网站已使用新证书。
| 项目 | 本次结果 |
|---|---|
| 颁发者 | `C = US, O = Let's Encrypt, CN = YR2` |
| SAN | `*.atlas.joeyone.cn`、`atlas.joeyone.cn` |
| 生效时间 | UTC `2026-10-03 02:09:40`;北京时间 `2026-10-03 10:09:40` |
| 到期时间 | UTC `2027-01-01 02:09:39`;北京时间 `2027-01-01 10:09:39` |
| ARI 建议窗口 | UTC `2026-12-01 04:53:44`~`2026-12-03 00:04:34` |
| acme.sh 所选下次续期时间 | UTC `2026-12-01 20:51:08`;北京时间 `2026-12-02 04:51:08` |
ARI 时间是客户端本次选择的续期时间,实际自动执行仍依赖第 9 节的定时任务。下一步为第 8.1 节首次上传和启用七牛 HTTPS。
## 8. 首次开启七牛 HTTPS 并配置自动部署
### 8.1 首次开启 HTTPS
@@ -381,6 +404,13 @@ openssl x509 \
4. 如需 HTTP 强制跳转 HTTPS,保持与 `QINIU_FORCE_HTTPS='true'` 一致。
5. 等待控制台显示部署完成,检查两个域名的证书;临时下载到本机的私钥按秘密文件管理,传递结束后清理临时副本。
取回文件时可复用已有管理员 SSH 连接的 SFTP 文件面板。本次服务器使用已有的 `root` 管理连接访问证书目录;终端内的 `sudo -iu atlas-cert` 不会改变文件面板的登录身份。不要为下载文件给维护账号新增 SSH 密码或放宽证书目录权限。
1. 打开客户端的服务器文件面板,将远端路径设为 `/var/lib/atlas-cert/.acme.sh/atlas.joeyone.cn/`。`.acme.sh` 是隐藏目录,优先直接输入完整路径,或启用“显示隐藏文件”。
2. 只下载 `fullchain.cer` 和 `atlas.joeyone.cn.key` 到本机专用临时目录,保留服务器上的原文件。不要下载整个 `.acme.sh`,其中包含账户与 DNS/API 凭据。
3. 在七牛上传这两个文件;若页面要求填写 PEM 文本,在本机文本编辑器打开对应文件,完整复制各自的 BEGIN/END 区块。私钥不发送到聊天、项目仓库或公共日志。
4. 两个域名证书验收完成后,清理本机临时副本;服务器原文件继续供后续自动续期与部署使用。
如果两个域名原本就已开启 HTTPS,先上传并切换到新证书即可。不要先删除旧证书。
### 8.2 执行一次自动部署
@@ -398,6 +428,8 @@ set +a
该步骤会上传证书,并逐一更新 `QINIU_CDN_DOMAIN` 中的两个域名,同时保存部署 hook 供续期时调用。确认输出包含两个域名的部署成功记录,并在七牛控制台复核。首次手工上传和 hook 上传可能留下两条证书记录,这是正常的;验收后按引用情况清理旧条目。
2026-10-03 现场记录:维护者以 `atlas-cert` 执行上述部署命令,于北京时间 11:26:12 收到 `img.atlas.joeyone.cn` 和 `img-dev.atlas.joeyone.cn` 各自的 `certificate has been deployed successfully`,最终输出 `Success`。随后两个图片域名的网络证书检查结果已记录于第 10.2 节;自动续期执行仍需完成第 9 节的 systemd timer。日志中的 `updating domain atlas.joeyone.cn` 使用证书主域名标识本次证书,不能据此判断网站 Nginx 已更新。
参考:[acme.sh 七牛部署实现](https://github.com/acmesh-official/acme.sh/blob/3.1.6/deploy/qiniu.sh)、[七牛证书管理与部署 FAQ](https://developer.qiniu.com/fusion/3905/ssl-certificate-faq)。
收紧本地文件权限:
@@ -425,6 +457,13 @@ chmod 600 /var/lib/atlas-cert/nginx-export/privkey.pem \
`--install-cert` 会保存导出路径,续期时自动更新这两个文件。Nginx 不直接引用 acme.sh 内部文件;本流程由第 9.2 节的 root 管理脚本复制到 `/etc/nginx/tls/atlas/current/`、执行 `nginx -t` 并重载,因此这里没有设置 `--reloadcmd`。必须完成第 9 节,只有导出证书不能让 Nginx 使用新证书。[acme.sh 证书安装说明](https://github.com/acmesh-official/acme.sh#3-install-the-cert-to-apachenginx-etc)
2026-10-03 现场记录:北京时间 11:27:26,维护者收到两项 `Installing` 输出,证书已导出到以下路径;Nginx 接入仍需完成第 9.2 节。
| 文件用途 | 实际导出路径 |
|---|---|
| 证书私钥 | `/var/lib/atlas-cert/nginx-export/privkey.pem` |
| 完整证书链 | `/var/lib/atlas-cert/nginx-export/fullchain.pem` |
维护账号不获得通用 sudo 权限。证书申请和七牛 API 调用以 `atlas-cert` 执行;复制到系统证书目录与 Nginx 重载由 systemd 的 root 步骤完成。root 脚本不加载 `secrets.env`,只读取固定路径的证书文件。
## 9. 配置 systemd 定时续期
@@ -463,7 +502,43 @@ exit
### 9.2 创建 Nginx 部署脚本并完成首次接入
本节默认服务器已经安装并运行 Nginx。如果使用 Docker、宝塔、其他 Web 服务或开发网站位于另一台机器,应按实际部署方式替换证书分发与重载步骤;不要直接运行这份面向本机 systemd Nginx 的脚本。
本节按网站服务选择接入方式:当前现场使用 **9.2.1 的 1Panel 分支**;9.2.2 仅适用于已经安装并运行宿主机 systemd Nginx 的服务器。
> 2026-10-03 现场环境确认:服务器为 1Panel `v2.0.11 stable`,网站服务容器为 `1Panel-openresty-C1B1`,镜像为 `1panel/openresty:1.27.1.2-2-3-focal`。维护者执行本节原脚本时,证书检查与宿主机 `nginx -t` 通过,但 `systemctl reload nginx` 报 `nginx.service is not active, cannot reload`;本节接入未完成。应用部署后应使用 1Panel 配置网站 HTTPS,证书续期后的同步与容器重载需按实际网站配置适配。当前可先启用第 9.3 节不含网站部署步骤的基础定时任务,待适配验收后补上 `ExecStartPost`。
>
> 1Panel v2.0.11 从服务器路径导入证书时会读取并保存内容,不能把导入操作视为持续引用源文件;acme.sh 更新 `nginx-export` 后仍需主动同步到 1Panel 网站使用的证书。依据:[v2.0.11 证书导入实现](https://github.com/1Panel-dev/1Panel/blob/v2.0.11/agent/app/service/website_ssl.go)。
#### 9.2.1 当前现场:1Panel/OpenResty,应用尚未部署
2026-10-03,维护者通过 `docker inspect` 与容器内 `nginx -T` 确认以下挂载关系:
| 宿主机路径 | OpenResty 容器路径 | 用途 |
|---|---|---|
| `/opt/1panel/www` | `/www` | 网站目录及各网站证书文件 |
| `/opt/1panel/www/conf.d` | `/usr/local/openresty/nginx/conf/conf.d` | 网站配置 |
| `/opt/1panel/apps/openresty/openresty/conf/ssl` | `/usr/local/openresty/nginx/conf/ssl` | 默认站点证书 |
现有网站包括 `git.joeyone.cn`、`pipi-assets.joeyone.cn`、`test.joeyone.cn`、`wmd.joeyone.cn`,分别使用 `/www/sites/<网站域名>/ssl/fullchain.pem` 和 `privkey.pem`。输出中没有 `atlas.joeyone.cn` 或 `dev.atlas.joeyone.cn` 的网站配置;维护者确认两套 Atlas 应用尚未部署。
**当前可先完成图片域名验收及第 9.3 节的七牛续期任务。** 网站应用未部署前,无需创建占位反向代理,也不把项目本地开发端口当作云端应用地址。现有网站及默认站点证书继续由各自配置维护。
应用部署时按以下顺序接入:
1. 部署生产、开发应用,确认各自实际访问地址、监听端口及 OpenResty 容器可达性。
2. 在 1Panel 的“网站 → 证书”上传正式证书,使用“本地导入/服务器路径”时填写以下**宿主机路径**;若界面要求文件内容,则在受控终端读取后粘贴,不将私钥发送到聊天或提交到仓库。
- 证书:`/var/lib/atlas-cert/nginx-export/fullchain.pem`
- 私钥:`/var/lib/atlas-cert/nginx-export/privkey.pem`
3. 在 1Panel 新建 `atlas.joeyone.cn` 和 `dev.atlas.joeyone.cn` 网站,配置各自真实应用的反向代理,并在 HTTPS 设置中选择导入的同一张 Atlas 证书。
4. 再次检查网站配置中的实际证书路径,并从网络验证两个网站证书和环境路由。文件目录可能由网站创建方式决定,不预先假设一定为 `/www/sites/atlas.joeyone.cn`。
5. 接入续期后同步:优先通过与 1Panel 版本匹配的证书更新接口更新已导入证书,使面板记录与关联网站一起更新;验收后再接入第 9.3 节的 `ExecStartPost`。当前未确认面板接口配置、证书 ID 和新网站实际路径,自动同步尚未完成。
仅手工导入证书不会完成网站自动续期部署。以后只替换网站目录里的 PEM 文件也可能造成面板保存的证书内容与实际文件不一致,后续面板操作可能重新写入旧内容,应完成上述同步适配。
#### 9.2.2 宿主机 systemd Nginx 分支
以下示例仅用于宿主机 Nginx。当前 1Panel 现场跳过本小节;不要为使旧脚本成功而启动另一个宿主机 Nginx。
以下由管理账号执行。脚本及系统证书目录由 root 管理,维护账号不能修改部署脚本。每次部署使用独立版本目录,只有配置检查和重载成功后才记录成功指纹;失败则恢复原目录链接,供下次重试:
@@ -566,7 +641,9 @@ sudo systemctl reload nginx
### 9.3 配置定时任务
以下由管理账号执行,创建 systemd 单元:
当前 1Panel 现场尚未部署 Atlas 网站,先创建**证书续期、七牛部署及 PEM 导出**的定时任务。以下基础服务不调用第 9.2.2 节的宿主机 Nginx 脚本,因此不会因 `nginx.service` 未运行而报错;网站自动同步仍须在应用部署后单独接入。
以下由管理账号执行,创建 systemd 单元。如果先前已创建含 `ExecStartPost=/usr/local/sbin/atlas-cert-install-nginx` 的服务,使用此基础单元覆盖后重新加载:
```bash
sudo tee /etc/systemd/system/atlas-qiniu-cert-renew.service > /dev/null <<'UNIT'
@@ -580,7 +657,6 @@ Type=oneshot
WorkingDirectory=/var/lib/atlas-cert
UMask=0077
ExecStart=/usr/sbin/runuser -u atlas-cert -- /var/lib/atlas-cert/renew-qiniu.sh
ExecStartPost=/usr/local/sbin/atlas-cert-install-nginx
TimeoutStartSec=30min
NoNewPrivileges=true
PrivateTmp=true
@@ -613,9 +689,11 @@ sudo systemctl show atlas-qiniu-cert-renew.service \
sudo journalctl -u atlas-qiniu-cert-renew.service -n 50 --no-pager
```
该服务由 root 启动,`ExecStart` 通过 `runuser` 降权执行 acme.sh;只有申请/续期及七牛部署返回成功,才执行 root 的 `ExecStartPost` 部署 Nginx。维护账号仍没有 sudo 权限。Nginx 部署失败会使整个 service 失败;acme.sh 的邮件通知只覆盖其自身的申请及七牛步骤,因此还须监控 service 失败和四个域名实际证书。
该基础服务由 root 启动,`ExecStart` 通过 `runuser` 降权执行 acme.sh,维护账号仍没有 sudo 权限。基础服务成功只证明当前续期检查流程完成,不证明网站证书已同步;还须监控 service 失败和两个图片域名实际证书。
每天北京时间 03:20 后随机延迟最多 30 分钟执行。每天检查不等于每天重新申请;acme.sh 按证书续期窗口决定是否申请,并更新保存的证书导出路径、调用七牛部署 hook。随后 Nginx 脚本核验证书,指纹变化才重载;若七牛失败则本轮不执行 Nginx 部署,排错后重新运行 service。服务器关机时不会运行,`Persistent=true` 会在恢复后补跑错过的定时检查。
每天北京时间 03:20 后随机延迟最多 30 分钟执行。每天检查不等于每天重新申请;acme.sh 按证书续期窗口决定是否申请,并更新保存的证书导出路径、调用七牛部署 hook。服务器关机时不会运行,`Persistent=true` 会在恢复后补跑错过的定时检查。
**已有宿主机 Nginx 网站的服务器**:先完成第 9.2.2 节验收,再在服务 `[Service]` 中添加 `ExecStartPost=/usr/local/sbin/atlas-cert-install-nginx`,重新执行单元校验、`daemon-reload` 和手工触发 service。**1Panel 现场**:完成第 9.2.1 节的网站及证书同步适配后,才添加对应的部署脚本;不能使用宿主机 Nginx 脚本替代。带 `ExecStartPost` 时,只有前置续期流程成功才执行网站部署,网站部署失败会使整个 service 失败;应同时监控网站证书和任务状态。
刚申请的证书显示跳过续期是正常的。`Type=oneshot` 完成后显示 inactive/dead 也是正常的,检查 timer 为 active、任务 `Result=success`、`ExecMainStatus=0`。日志只在受控终端查看,不导出带凭据的调试日志。
@@ -651,35 +729,51 @@ exit
### 10.2 从网络检查四个域名实际证书
本地证书更新成功不等于所有 CDN 节点完成更新。使用 OpenSSL 从网络检查四个域名实际提供的证书:
本地证书更新成功不等于所有 CDN 节点完成更新。当前两个网站尚未部署,先使用 OpenSSL 检查两个图片域名。应用上线且网站 HTTPS 配置完成后,将循环域名扩展为 `atlas.joeyone.cn dev.atlas.joeyone.cn img.atlas.joeyone.cn img-dev.atlas.joeyone.cn`,再验收四个域名。
仅复制代码块内的命令,不复制 Markdown 的三个反引号。若终端显示连续的 `>` 续行提示,先按 `Ctrl+C` 返回正常提示符,再重新粘贴命令。
```bash
set -o pipefail
for cert_domain in atlas.joeyone.cn dev.atlas.joeyone.cn img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
for cert_domain in img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
printf '%s\n' "$cert_domain"
timeout 20 openssl s_client \
-connect "$cert_domain:443" -servername "$cert_domain" \
-verify_hostname "$cert_domain" -verify_return_error </dev/null 2>/dev/null |
-verify_hostname "$cert_domain" -verify_return_error </dev/null |
openssl x509 -noout -issuer -dates -fingerprint -sha256
done
```
四个域名的网络证书指纹应在部署传播完成后与正式 `fullchain.cer` 的叶证书指纹一致。私有图片返回 401/403 不能据此判断 TLS 失败;TLS 握手验证可以独立于图片授权完成。
2026-10-03 现场网络检查记录(维护者从服务器执行,检查的具体时刻未提供):
| 项目 | 两个图片域名的结果 |
|---|---|
| 域名 | `img.atlas.joeyone.cn`、`img-dev.atlas.joeyone.cn` |
| 颁发者 | `C = US, O = Let's Encrypt, CN = YR2` |
| 有效期开始 | `2026-10-03 02:09:40 GMT`(北京时间 10:09:40) |
| 有效期结束 | `2027-01-01 02:09:39 GMT`(北京时间 10:09:39) |
| SHA-256 指纹 | `E0:EB:B3:0B:A7:3A:B0:12:10:5F:EC:8D:F0:1E:99:F5:0D:09:9C:CA:29:27:BF:8A:B0:4C:74:48:04:44:D0:00` |
两域名返回的指纹相同,颁发者和有效期与第 7 节本次正式签发结果一致,确认本次探测到的 CDN 节点已提供正式证书;这不代表所有地区节点都已完成传播,也不代表定时续期任务已验收。
同次检查中,`atlas.joeyone.cn` 与 `dev.atlas.joeyone.cn` 返回 `Could not read certificate from <stdin>` / `Unable to load certificate`,表示管道没有获得可解析的证书。由于当时隐藏了 `s_client` 的错误输出,无法仅凭这一结果区分 DNS、连接或 TLS 失败;结合应用尚未部署的现场状态,两个网站暂列待部署后验收,无需重新签发证书。
### 10.3 给四个域名分别加独立监控
在现有外部监控系统或支持证书检查的云监控产品中,把四个完整域名分别纳入 TLS 到期监控。同一张证书也可能在某个部署目标更新失败:
在现有外部监控系统或支持证书检查的云监控产品中,当前先把两个图片域名纳入 TLS 到期监控;网站上线后补齐两个网站域名。同一张证书也可能在某个部署目标更新失败:
- 证书剩余不足 21 天告警,不足 7 天升级处理。
- TLS 握手、证书链或域名匹配失败立即告警。
- 日常任务长期没有运行,单独按任务执行时间监控。
- 私有资源 HTTP 探测需使用受控授权;普通无签名 403 不是资源故障。
首次手工检查到期阈值可使用以下命令;它只检查当前网络节点,不能代替多地区监控:
首次手工检查到期阈值可使用以下命令;它只检查当前网络节点,不能代替多地区监控。当前仅检查图片域名,网站上线后再扩展域名列表:
```bash
set -o pipefail
for cert_domain in atlas.joeyone.cn dev.atlas.joeyone.cn img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
for cert_domain in img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
if timeout 20 openssl s_client \
-connect "$cert_domain:443" -servername "$cert_domain" \
-verify_hostname "$cert_domain" -verify_return_error </dev/null 2>/dev/null |
@@ -765,6 +859,7 @@ PY
```dotenv
OBJECT_STORAGE_PROVIDER=qiniu_kodo
OBJECT_STORAGE_ENDPOINT=https://s3.cn-east-1.qiniucs.com
OBJECT_STORAGE_REGION=cn-east-1
OBJECT_STORAGE_BUCKET=atlas-prod-assets
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_ACCESS_KEY=填写生产对象存储凭据
@@ -777,16 +872,16 @@ QINIU_SECRET_KEY=填写生产私有下载签名凭据
OZON_PUBLISH_ENABLED=auto
```
开发联调配置替换为开发空间、开发凭据和 `https://img-dev.atlas.joeyone.cn`。数据库素材身份继续记录 `storage_provider + bucket + object_key`,不保存签名 URL 作为素材主数据。
开发联调配置替换为开发空间、开发凭据和 `https://img-dev.atlas.joeyone.cn`。`OZON_IMAGE_DOWNLOAD_ORIGIN` 同时用于 API 的图片预览、附件下载和 Worker 向 Ozon 交付图片;七牛私有空间的链接由服务端按用途签发短期下载凭证。上传仍使用 `OBJECT_STORAGE_ENDPOINT`。数据库素材身份继续记录 `storage_provider + bucket + object_key`,不保存签名 URL 作为素材主数据。
`OBJECT_STORAGE_BUCKET` 当前用于 S3 操作,因此应填七牛显示的 **S3 空间名**;如它与普通空间名不同,不可混用。自定义 CDN 域名绑定相应的实际空间。[七牛 S3 服务域名说明](https://developer.qiniu.com/kodo/4088/s3-access-domainname)
### 12.2 当前代码接入前置事项
### 12.2 接入校验事项
证书配置完成不代表项目已完成七牛接入。本文编写时:
证书配置完成后,还需确认应用与存储空间的联通性:
1. `apps/api/src/media/media.service.ts` 和 `apps/worker/src/main.ts` 的 S3 region 为硬编码 `us-east-1`。需要后续代码任务添加配置,并与七牛 S3 区域匹配;目前没有已生效的 `OBJECT_STORAGE_REGION` 配置项,不能只在 `.env` 填一个新变量就认为完成。
2. 需验证现有 S3 上传、HEAD/GET、预签名预览和浏览器直传的七牛兼容性;上传 endpoint 的 CORS 要允许实际 Atlas 开发/生产网站 Origin、PUT/GET/HEAD 及预检请求中实际使用的头,不能拿 CDN 配置代替上传 CORS。
1. API 和 Worker 的 S3 客户端读取 `OBJECT_STORAGE_REGION`;它必须与七牛空间的区域及 endpoint 匹配。API 的七牛预览与附件下载使用绑定的 HTTPS 图片域名签发私有下载链接。
2. 需验证现有 S3 上传、HEAD/GET、图片域名私有下载和浏览器直传的七牛兼容性;上传 endpoint 的 CORS 要允许实际 Atlas 开发/生产网站 Origin、PUT/GET/HEAD 及预检请求中实际使用的头,不能拿 CDN 配置代替上传 CORS。
3. 发布代码拒绝 `local_minio` 或与当前 bucket 不一致的素材。原 MinIO 图片需要复制、校验并更新素材定位,或重新导入,不能仅切换配置。
4. 原有架构文件仍以普通本地开发使用 MinIO 为基线。本手册描述待实施的七牛联调操作,不自行宣布该基线已被替换;正式切换时应同步更新上游架构和交接文档。
5. 生产 `OZON_PUBLISH_ENABLED=auto` 默认不启用发布。完成七牛延迟抓图及真实店铺其他前置验收后,另行显式启用,不在证书维护步骤中顺便开启生产上架。
@@ -873,7 +968,7 @@ sudo systemctl start atlas-qiniu-cert-renew.timer
| systemd 执行后 inactive | oneshot 已正常结束 | 看 timer、Result 和退出码 |
| 没收到邮件 | SMTP 授权码、From、端口出站、收件箱 | 重做通知配置测试,检查邮箱服务商要求 |
| 图片签名链接 404 | 对象是否在正确空间、key 编码、是否被提前清理 | 检查对象身份和保留策略 |
| 应用 S3 访问失败 | region、endpoint、S3 空间名、对象权限 | 按第 12 节处理应用配置与代码前置事项 |
| 应用 S3 访问失败 | region、endpoint、S3 空间名、对象权限 | 按第 12 节核对应用配置和存储空间权限 |
排错时只提供脱敏后的错误码、域名、时间和请求 ID;不提供 SK、证书私钥、完整授权 URL 或完整账号配置文件。
@@ -907,15 +1002,18 @@ sudo systemctl start atlas-qiniu-cert-renew.timer
- [ ] 两个空间都是私有,生产与开发数据/应用凭据隔离。
- [ ] 两个下载域名是七牛 CDN 域名,分别指向正确空间,CNAME 已生效。
- [ ] 两个域名均启用私有空间回源鉴权,未添加与现有签名协议冲突的限制。
- [ ] DNS-01 staging 验证成功,临时 TXT 清理正常。
- [ ] 正式 SAN 含 `atlas.joeyone.cn` 与 `*.atlas.joeyone.cn`,覆盖四个域名;未误用更深一层域名。
- [x] DNS-01 staging 验证及测试证书签发成功(2026-10-03)。
- [ ] 临时 TXT 已清理,两个图片域名 CNAME 保持正确。
- [x] 正式 SAN 含 `atlas.joeyone.cn` 与 `*.atlas.joeyone.cn`,覆盖四个域名;颁发者及有效期已检查(2026-10-03)。
- [ ] 两个网站 A 记录正确,Nginx 分别路由到对应环境,证书导出、配置检查和重载成功。
- [ ] 七牛 hook 首次部署成功,两域名实际证书均已更新。
- [x] 七牛 hook 首次部署对两个图片域名均返回成功(2026-10-03 11:26:12,北京时间)。
- [x] 两个图片域名的实际网络证书已核验更新(2026-10-03,当前探测节点的颁发者及有效期与本次正式证书一致,两个域名指纹相同)。
- [ ] systemd timer 启用,手工触发 service 成功,七牛及 Nginx 均完成部署;没有另一个 cron/服务器并发维护。
- [ ] 当前 1Panel 阶段:先启用仅续期、七牛部署及 PEM 导出的基础 timer,手工触发成功;应用上线后补齐网站同步,再验收上一项完整链路。
- [ ] 故障/实际续期通知已收到测试消息,四个域名已纳入独立到期监控。
- [ ] 两空间测试图片均通过有效签名、无签名、缓存后无签名、过期签名测试。
- [ ] 权限受控的加密备份和旧有效证书回滚方式已准备。
- [ ] 应用 S3 region 等前置问题另行解决,未把证书部署成功当作 Ozon 上架验收成功。
- [ ] 应用 S3 region、上传与图片域名的私有下载均已验证,未把证书部署成功当作 Ozon 上架验收成功。
## 17. 项目与官方参考
+9 -2
View File
@@ -69,12 +69,19 @@ SKU 图片选择弹窗左侧是该 SKU 的已选图片(排序/移除),右
图片「编辑 → AI 编辑」打开标题为「AI编辑」的单张编辑抽屉,主图固定,可选最多两张辅助图。用户可设置编辑要求、图中文字翻译、背景、五种视觉风格、尺寸标注和画面比例;抽屉不展示生成方式,提交时固定使用 `edit` 模式。Worker 调用 Pcore `gpt-image-2` 异步接口;普通编辑参照旧项目以 JSON `images[].image_url` 发送原始图片的 data URI,仅扩图发送预处理后的 PNG 画布和蒙版 data URI。生成图独立保存。提交和生图期间都可关闭抽屉,任务在后台继续,可从底部操作栏的图片任务入口查看;失败任务同时展示通用提示和已取得的 Pcore 原始错误内容。用户预览后点击「添加到当前 SKU」,结果插在原图后方;已完成结果也保留在当前商品的「AI 生图记录」,可在重开弹窗后选择。选图弹窗确认和页面保存仍分别控制表单回填与持久化。
商品编辑页底部固定操作栏在「AI 套图生成」后展示「图片任务」入口;有运行中任务时显示「任务处理中」和数量。展开后按进行中和最近任务显示当前商品的图片 AI 任务、状态、完成数、失败信息和结果预览;单张 AI 编辑可在此取消。提交后立即刷新,进行中每 3 秒刷新,其余时间低频刷新。当前接入单张编辑;后续 AI 套图沿用商品图片任务结构并增加任务来源,入口无需重新设计。
商品编辑页底部固定操作栏在「AI 套图生成」后展示「图片任务」入口;有运行中任务时显示「任务处理中」和数量。展开后按进行中和最近任务显示当前商品的图片 AI 任务、状态、完成数、失败信息和结果预览,支持取消单张及套图的未完成图片。提交后立即刷新,进行中每 3 秒刷新,其余时间低频刷新。套图作为整套任务展示,内部逐张任务不重复出现在列表。
2026-10-03:AI 套图使用从右侧滑出的三列抽屉:商品资料、生图策略、生成结果。资料含目标 SKU、最多 8 张底图(建议 3–6)、商品名/颜色/实物尺寸重量、采集信息及核心卖点文本域。选图来源顺序为采集图片、AI 生图记录、图片处理记录、上传图片、网络图片;底图首张决定商品主体,每张生成图使用最多 3 张相关参考图。策略保留白底、场景、卖点、其他四类,默认 4 张(1/1/2/0):白底主图、场景展示图、核心卖点图、细节特写图各 1 张,展开后可逐张修改类型、主题、描述、依据或删除;智能规划使用 DeepSeek,按当前总张数预算和 SKU 资料重新分配用途,不强制凑齐图类;完成后全部展开供确认,不自动生图。五种视觉风格放在策略列,可修改描述并恢复默认;比例为 3:4、1:1、4:3,文字语言为俄/中/英/阿拉伯语。尺寸图只使用有依据的实物尺寸;默认方案不含尺寸图,可手动添加或由智能规划选择。Pcore 逐张生成,可关闭后后台运行、保留部分成功结果、取消及重试未完成项。结果加入目标 SKU 只回填表单,保存商品后持久化。契约见 [商品 AI 套图](../api/product-image-suites.md)。
打开「SKU 图片选择」时静默读取当前工作空间默认水印。点击单张或批量「添加水印」时,有默认模板便直接生成带水印的派生素材;没有默认模板或读取失败才打开每行最多三张卡片的选模板弹窗,水印管理在新标签页打开。原图和其他 SKU 的选择不被改写;生成结果替换当前图片位置,并可在同一弹窗撤销。弹窗确认只更新编辑表单,页面保存后才持久化选图;发布时不会自动加水印。
首期素材规则:发布图片仅接受 JPG、PNG、WebP;单文件最大 20 MB,最短边低于 700 px 给出警告且不得作为主图,超过 10,000 px 的边长在上传时拒绝;建议 1:1 或 3:4,允许其他比例但在预览中提示裁切风险。采集候选素材可保留原始文件供编辑选择,实际发布时始终按目标平台的图片数、格式和可访问性校验。Ozon 发布图片必须是可由 Ozon 拉取的 HTTPS 地址。
“AI 填报”针对当前平台一次生成类目、型号、SKU 货号、证据充分的包装规格以及 **SPU 共用的**标题、标签与简介,并在任务完成后直接填入当前页面表单,等待用户保存。SKU 文案默认继承 SPU;用户可从当前 SKU 的 AI 图标单独生成并在弹窗明确应用。SPU 和每个 SKU 的三项字段旁均有「中文对照」图标,打开同一个弹窗查看当前作用域左俄右中的三项对照;继承字段标明来源,俄文被手改后该字段对照标为待更新。首期不生成或编辑富内容。AI 套图由左侧抽屉承载,必须显式选择目标 SKU;尺寸图一 SKU 一任务,并保存实物规格快照。
“AI 填报”针对当前平台一次生成类目、型号、SKU 货号、证据充分的包装规格以及 **SPU 共用的**标题、标签与简介,并在任务完成后直接填入当前页面表单,等待用户保存。SKU 文案默认继承 SPU;用户可从当前 SKU 的 AI 图标单独生成并在弹窗明确应用。SPU 和每个 SKU 的三项字段旁均有「中文对照」图标,打开同一个弹窗查看当前作用域左俄右中的三项对照;继承字段标明来源,俄文被手改后该字段对照标为待更新。首期不生成或编辑富内容。AI 套图由右侧三列抽屉承载,必须显式选择目标 SKU;每套任务绑定一个 SKU,并保存资料与实物规格快照。
类目推荐由“AI 填报”主动触发:系统将采集证据与当前平台可发布的类目候选交给 AI。Ozon 来源优先沿用现有确定性匹配;1688、淘宝、天猫等来源由 AI 评估目标类目。服务端验证候选 UUID 后可将明确结果填入**未保存的页面表单**;用户点击保存后才写入平台发布配置。歧义结果留给用户选择。AI 不得跳过类目属性校验;本期 AI 填报不处理动态类目属性。
### 2026-10-04 主题标签格式
SPU 和 SKU 发布主题标签以 `#` 开头,正文只允许字母、数字和下划线,多词用下划线连接;最多 30 个标签,每个含 `#` 最多 30 字符。编辑页用空格拆分不同标签,显示具体格式问题;上架按所选 SKU 的有效标签校验,草稿允许保留原值继续修改。AI 全局填报和 SKU 文案采用同一格式规则,回填前整理缺少 `#`、词间空格或连字符,过滤无法合规的结果;俄文和中文对照保持成对。多个标签发布时用空格拼在 Ozon 属性 `23171` 的同一个值里。
+45 -1
View File
@@ -2,6 +2,44 @@
> 更新:2026-09-30。此文是新 AI 对话、架构讨论与开发工作的首读入口。开始开发前,必须先阅读并遵守 [`development-collaboration-contract.md`](./development-collaboration-contract.md) 和仓库 `AGENTS.md`。下方按日期记录的旧方案仅供追溯;当前状态以本节、2026-09-29 类目佣金记录及最新的 1.47 节为准。
## 2026-10-04 SKU 主动重算使用最新默认方案
根因:`SkuPricing.calculate` 优先使用 SKU 已保存的 `pricing_draft`,「重新试算」和批量按钮只强制重发相同参数,旧零折扣因此使实际售价与划线价相同。当前集成会话仅调整 Web `SkuPricing`、补充定价引擎公式回归测试及 API/交接说明,不修改服务端业务、HTTP 字段、定价引擎或数据库。
两个主动入口重读目标店铺定价目录,使用最新默认方案的完整参数替换旧 SKU 参数,沿用当前成本与有效包装;无默认方案/费率不可用/读取失败时明确提示,不回退零折扣。自动试算、手工售价输入和试算详情继续使用当前参数。批量跳过已导入 SKU,试算结果仍需保存草稿确认。表单监听保留未注册字段,返回报价直接核对实时表单,避免参数回填的渲染时序误丢弃报价;建立同一计算上下文时不取消已经发出的匹配请求。
验证:ego-browser 隔离真实组件复现旧零折扣两价相同,确认单 SKU 与批量使用最新 50% 折扣、划线价为实际售价两倍,保留成本和包装继承,跳过已上架 SKU;页面打开后将默认方案改为 25% 再重算能读取新值;手工实际售价不覆盖当前折扣;无默认方案、费率不可用、目录失败均不生成回退报价。定价引擎 16 项(新增 CNY/RUB 的 50% 折扣)和定价保存 12 项通过;Web/API 源码与测试类型检查、构建及差异检查通过。临时页面已清理,无真实商品保存或平台发布,未手动启停原有服务。
## 2026-10-04 实际售价、划线价展示与首次发布价格
本轮由当前集成会话负责,范围为 Web `SkuPricing`、`PricingCalculator`、`ProductPublishActions`、SKU 样式,API 发布映射/校验、发布回归测试和相关文档。按维护者要求,首次上架的 Ozon `price` 改为已确认 `listing.list_price`,不发送 `old_price`;保持 HTTP 字段、定价公式、数据库和 Worker 冻结快照执行方式。实际售价继续用于目标利润计算,用户上线后在 Ozon 调整实际售价;已创建任务沿用原价格快照,已导入 SKU 不自动更新。
SKU 收起摘要和展开定价区显示采购价、实际售价、划线价、预估净利润;试算结果实际售价与划线价同行。非人民币报价按同一报价快照比例换算摘要,不读取当前汇率改写历史价格。确认上架表格同时显示两价,划线价标为「本次上报价」。服务端预检与创建要求划线价有效且不低于实际售价,缺失/零值返回 `LIST_PRICE_REQUIRED`,低于实际售价返回 `LIST_PRICE_INVALID`;零折扣两价相同可发布。
验证:发布 11 项、商品编辑 13 项、定价引擎 15 项、定价保存 12 项全部通过,覆盖十进制划线价冻结/实际模拟 Ozon 请求、实际售价保留、零折扣、缺失/零划线价阻断、幂等和租户隔离。API 源码/测试类型检查及构建、Web 类型检查及构建通过。ego-browser 隔离真实组件检查 721px/390px 下的两价同行与无页面横向溢出、展开区、上架确认和 RUB 报价的人民币摘要;未向真实 Ozon 发布,未修改真实商品,原有服务未手动启停。临时验证页面已清理。
## 2026-10-04 主题标签格式与 AI 输出校验
本轮由当前集成会话负责,范围为共享 `ozon-hashtags.ts` 规则、Web SPU/SKU 标签提示、API 发布预检/创建校验、Worker AI 全局及 SKU 文案提示词与输出整理,以及相关测试和文档;不修改 HTTP 请求结构、数据库或标签覆盖继承语义。标签逐项以单个 # 开头,正文只允许字母、数字和下划线,无内部空格;每项含 # 最多 30 字符,最多 30 项,发布继续拼为一个 Ozon 属性值。AI 补齐缺失 #,词间空白及连字符转下划线,过滤非法、超长及重复项,并保留同项中文对照。
历史与手工草稿仍允许保存,Web 给出具体格式提示;预检和任务创建按所选 SKU 实际使用的标签拦截,返回 `OZON_HASHTAG_INVALID`,合法 SKU 独立覆盖不受未使用的通用标签阻断。Web 支持以空格拆分多个输入标签,不自动改写已有内容。新规则及示例见商品编辑 API 文档。
验证:Worker 11 项、发布 HTTP/数据库/模拟 Ozon 10 项、商品编辑 13 项通过;contracts/API/Worker/Web 类型检查与构建通过。ego-browser 隔离真实组件验证缺少 #、内部空格、非法标点、合法标签清除提示、空格拆分及 SKU 覆盖独立提示;临时页面已清理。未调用收费 AI 或向真实 Ozon 店铺发布,API/Worker 仍沿用原有开发监听进程,无手动启停。
## 2026-10-04 Ozon 多主题标签发布修复
本轮由当前集成会话负责,范围为 API 发布映射、发布集成测试和 API/交接文档,无 Web、Worker、数据库迁移或共享类型变更。原代码把每个主题标签分别映射到属性 `23171` 的 `values[]`,被实际 Ozon schema 的单值限制误阻断。读取本地缓存的 Ozon 原始说明确认:支持最多 30 个标签,用空格分隔放在同一个属性值中。草稿继续保存标签数组;发布时先按 SKU 覆盖或 SPU 继承解析标签,再过滤空项、去除首尾空白并拼成一个值,无标签则不提交属性。
先用新增回归复现相同的「#主题标签最多 1 个值」错误,修复后发布 HTTP/数据库/Worker 模拟平台测试全部 8 项通过,覆盖 SKU 覆盖、SPU 继承、30 个标签及空标签,并核对实际组装的 `/v3/product/import` 模拟请求。API 源码/测试类型检查与构建通过,未向真实店铺发布商品。原有 API 开发监听进程已自动重载并继续监听 8200,无手动启停;测试使用临时监听端口并已退出。
## 2026-10-04 套图生成固定操作区
本轮由当前集成会话负责,仅修改 Web 套图抽屉、底部操作区组件、样式与交接记录,复用现有 API,无契约或数据模型变更。维护者已确认「并列操作」设计:策略内容独立滚动,底部固定「本次生成」,生成/排队时并列显示取消和状态,智能规划有独立取消文案;取消确认及重复操作保护已接入,失败/取消后在同一区域只重试未完成图片。任务摘要使用执行快照,成功图片保留,结果列移除重复进度和重试入口。使用真实组件及模拟任务验证桌面/窄屏、滚动固定、取消和重试状态,Web 类型检查与构建通过;未调用收费模型,未保存商品,临时页面已清理,原有开发服务未手动重启。具体前端交接见 [`../../apps/web/HANDOFF.md`](../../apps/web/HANDOFF.md)。
## 2026-10-03 智能规划失败诊断
当前集成会话修复范围为 Worker、共享错误码及相关文档,无请求字段、数据库迁移或生图流程变更。用户授权的两次真实 DeepSeek 诊断均在四张图片的 `type` 字段发生枚举校验错误;提示词改用有效示例和明确英文枚举,重试携带具体字段校验规则。Worker 每次校验失败和最终失败打印安全结构化日志,最终诊断写入 operation.result.failure;张数、字段、主体图、主题、尺寸及保存失败使用不同错误码。历史记录不回填推测结果,不保存模型原文、商品资料或凭据。离线测试验证错误路径、纠正反馈和诊断持久化;修复后的真实模型规划尚未再次调用。维护者 API/Worker 仍使用原有 watch 进程,无手动重启。
## 1. 项目定位
这是一个全新的云端产品,不是 `ozon-seller-kit` 的本地版重构。目标用户可拥有多个门店,每个门店只能绑定一个平台;平台自身可支持多个市场、国家、语言和币种。因此语言、币种、类目和发布规则不能写死为 Ozon 或中文/俄文。
@@ -12,9 +50,15 @@ pnpm monorepo 包含 Web、NestJS API、BullMQ Worker、WXT 浏览器扩展、
## 0. 交接结论(先看这里)
### 2026-10-03:AI 套图生成
智能规划已重新设计为按当前总张数预算规划(`image_count` 1–20、默认 4),不沿用旧项目固定图类配额。使用 Worker `.env` 中的 DeepSeek 配置,按 SKU 文字资料/要求和出图配置安排各张用途;底图 ID 不当作视觉识别证据,资料不足时不编造卖点或参数。严格校验预算、分类、主题、底图与实物单位;业务方案无效时重试一次,失败返回固定脱敏诊断。影响 Web/API/Worker/contracts,数据库结构不变。
已按维护者确认的三列右侧抽屉接入 Web、API、Worker、contracts 与数据库。默认 4 张(白底/场景/核心卖点/细节各 1)、四大类及逐张方案编辑,最多 8 张底图,逐张选用最多 3 张;明确绑定已保存 SKU。DeepSeek 异步规划,Pcore 复用单图链路并汇总为一个套图任务。支持历史结果、后台生成、取消、保留部分成功图与重试;重试代次避免旧轮询/迟到回写污染新任务。结果只回填目标 SKU 表单。迁移 056–059;具体契约与验收范围见 [套图 API](../api/product-image-suites.md)。API/数据库/Worker 自动测试不发起收费模型调用;真实套图质量仍需业务样本验证。
已完成“采集 → 采集箱 → 商品编辑草稿”的主要数据闭环、平台级类目/属性/字典目录与发布预检。编辑页已接入异步「AI 填报」:基于来源资料与 Atlas 类目树生成类目建议、型号/SKU 货号、证据充分的包装建议及 SPU 中俄文案;单 SKU 文案由用户单独生成并确认应用。AI 结果只回填未保存表单,保存草稿仍是持久化边界。Ozon 动态类目属性可按已选类目加载并手工填写,**当前不由 AI 自动填报**。SKU 图片选择现支持采集候选图、工作空间上传图和网络导入图;在弹窗确认后回填草稿,页面保存后持久化。
SKU 定价已打通试算、表单应用与原子保存确认,在线商品同步已有 API/Worker。首次发布 job/import 与结果轮询已接入;生产启用仍需完成 VAT 与七牛图片交付验收;正式定价还需完成 CEL 费率及类目佣金映射审核。AI 类目推荐和文案已能运行,但不能等同于跨来源样本验收;尤其应继续用 1688/淘宝/天猫样本核验 Atlas 类目候选质量,以及俄文文案、包装证据和 SKU 继承行为。AI 套图生图仍是占位入口;图片「编辑」菜单中的一键 3:4、添加水印和单张 AI 编辑已接入异步素材任务。
SKU 定价已打通试算、表单应用与原子保存确认,在线商品同步已有 API/Worker。首次发布 job/import 与结果轮询已接入;生产启用仍需完成 VAT 与七牛图片交付验收;正式定价还需完成 CEL 费率及类目佣金映射审核。AI 类目推荐和文案已能运行,但不能等同于跨来源样本验收;尤其应继续用 1688/淘宝/天猫样本核验 Atlas 类目候选质量,以及俄文文案、包装证据和 SKU 继承行为。AI 套图已接入规划、逐张生成、取消和失败重试;图片「编辑」菜单中的一键 3:4、添加水印和单张 AI 编辑已接入异步素材任务。
当前优先事项:用真实来源样本回归 AI 类目推荐、文案与图片选择,补做真实图片上传/网络导入联调;审核正式物流费率与佣金映射,并用实际结算账单核对 Ozon 平台服务费、收款费和其他扣费;完成首次发布在中国主体跨境 rFBS 测试店铺的 VAT 省略与七牛延迟抓图验收。平台参考目录目前由平台级参考源和同步任务维护,不能把 9 月 10 日的门店级缓存描述当作现行架构。