diff --git a/docs/ledger-exchange.md b/docs/ledger-exchange.md index 717c26d..9ebc1ee 100644 --- a/docs/ledger-exchange.md +++ b/docs/ledger-exchange.md @@ -69,7 +69,7 @@ replace 只接受已验证完整范围的 full 快照;不能只信任包自报 - 来源从业务行汇总,`factory` 档案不参与合成/用户提供的汇总。已有同 ID 记录的来源、识别模式及原图引用不可通过替换改写;人工确认保留 mock 与 synthetic。 - 空格式模板依旧不可执行。需要真实字段骨架时先导出当前工作区;新增申领/拜访还必须满足事件、历史操作者与资产规则,不能只填页面主表。 -客户端领域与 HTTP 契约已有针对性测试。个人试用所选 Supabase 项目已应用 `20261014171026` 交换迁移,并通过 8 组云端回滚事务与权限检查;具体范围见 [案例验证记录](../examples/saas/mes/factory-operations/evidence/verification.md)。这些数据库检查不等于真实用户的云端 UI 验收,首次应用注册尚待使用者完成。Sites 所有者私有试用站已发布,Supabase 已连接;真实用户注册后的 UI 验收待完成。本机静态预览已验收,当前网络访问线上站点被托管边缘拦截,未进入工作台;其他提供方仍须独立接入与验证。 +客户端领域与 HTTP 契约已有针对性测试。个人试用所选 Supabase 项目已应用 `20261014171026` 交换迁移,并通过 8 组云端回滚事务与权限检查;具体范围见 [案例验证记录](../examples/saas/mes/factory-operations/evidence/verification.md)。当前千问托管试用站已发布,沿用现有 Supabase;静态资源、未登录入口及邮箱确认回跳已验证。真实用户登录后的 UI,以及上传、登记、预览/下载、刷新与重新登录回读仍待按 [案例验收手册](../examples/saas/mes/factory-operations/acceptance.md#真实存储与登录后验收操作) 完成。此前 Sites 403 保留为历史事实。整包 JSON 只用于同工作区业务恢复,不含附件正文,不能作为全量备份;其他提供方仍须独立接入与验证。 ## 最小验收 diff --git a/examples/saas/mes/factory-operations/README.md b/examples/saas/mes/factory-operations/README.md index 0baee00..18bc83b 100644 --- a/examples/saas/mes/factory-operations/README.md +++ b/examples/saas/mes/factory-operations/README.md @@ -75,7 +75,7 @@ VITE_API_BASE_URL=https://your-platform.example/api/workbench ## JSON 与拍照台账 -个人试用所选 Supabase 项目已应用交换迁移,并完成 8 组云端事务与权限验证;首次真实用户注册及云端 UI 验收尚未进行。其他部署仍需应用迁移并独立验证,选择提供方不会自动部署接口。详见 [集成说明](integrations.md) 与 [验证记录](evidence/verification.md)。 +个人试用所选 Supabase 项目已应用交换迁移,并完成 8 组云端事务与权限验证;真实用户登录后的云端 UI 与文件存取验收尚未完成。其他部署仍需应用迁移并独立验证,选择提供方不会自动部署接口。详见 [集成说明](integrations.md) 与 [验证记录](evidence/verification.md)。 1. 所有者在“数据接入”点击“导出完整快照”,得到当前工作区四张业务表及资产清单。先保存当前快照,再处理待恢复的文件。 2. “下载格式模板”只说明包结构,不能提交。填写数据时以当前导出包为基准,对照 [字段与流程约束](data-model.md),保留工厂 ID、完整事件、历史操作者、来源和资产引用。 @@ -90,8 +90,8 @@ VITE_API_BASE_URL=https://your-platform.example/api/workbench ## 个人试用部署进度(2026-10-15) -现有 Supabase 项目已准备好新交换能力,迁移版本为 `20261014171026`。云端验证采用合成输入并在事务中回滚;结束后业务数据、Storage 对象和应用用户均为零。首次应用账号由使用者自行注册,尚未记录真实用户的云端 UI 验收。 +当前千问托管试用站已发布,沿用现有 Supabase 身份、数据库及私有资产存储。静态资源、未登录工厂入口和邮箱确认回跳已验证;邮箱确认保持开启,默认与显式目标的 303 回跳均匹配当前入口,精确重定向允许列表已同步。四个数据库迁移已齐备,交换迁移版本为 `20261014171026`。 -Sites 所有者私有试用站已发布,Supabase 已连接;发布工具返回 `succeeded`。邮箱确认保持开启,默认回跳地址与精确重定向允许列表均指向在线工厂入口,默认和显式目标的 303 回跳复测均匹配。真实用户注册后的 UI 验收待完成。 +真实用户登录后的 UI,以及上传、登记、预览/下载、刷新和重新登录后的持久化回读,仍待按 [验收手册](acceptance.md#真实存储与登录后验收操作) 完成。整包 JSON 只支持同工作区业务恢复,不含附件正文,不能作为完整文件备份。 -本机静态预览已验收;当前网络访问线上站点被 Cloudflare 托管边缘拦截,未进入工作台,因此不声明在线 UI 已验收。公开文档不保存私有地址、项目标识或密钥。案例保持 `integrated`、`customerConfirmed: false`;Supabase 是本次个人部署选择,其他用户仍可采用自己的提供方。 +此前 Sites 站点发布成功但访问遇到 Cloudflare 403,保留为 [历史验证记录](evidence/verification.md),不作为当前千问入口的访问状态。公开文档不保存私人地址、项目标识或密钥。案例保持 `integrated`、`customerConfirmed: false`;Supabase 是本次个人部署选择,其他用户仍可采用自己的提供方。 diff --git a/examples/saas/mes/factory-operations/acceptance.md b/examples/saas/mes/factory-operations/acceptance.md index 1f03b7b..d84da5c 100644 --- a/examples/saas/mes/factory-operations/acceptance.md +++ b/examples/saas/mes/factory-operations/acceptance.md @@ -35,4 +35,15 @@ | AC-22 | 只有服务端发布成功后刷新来源及版本;失败保留原正式数据,切换工作区/连接后旧响应不串入新页面,模拟记录往返后继续标注。 | 浏览器竞态和失败注入 | | AC-23 | 超过 5 MiB、40 层或业务注册上限的输入明确拒绝;HTTP 保留原文、同幂等键,不自动重试 403/409/服务或网络失败。 | 解析边界与 HTTP 契约测试 | -新增功能的本地验证、云端回滚事务与真实用户 UI 验收分开记录。个人试用 Supabase 交换迁移已部署并完成数据库与权限检查,所有者私有试用站已发布。邮箱确认开启,默认/显式确认目标的 303 回跳与精确允许列表匹配;首次用户注册后的云端 UI 验收仍待完成,当前网络访问被托管边缘拦截,不能据发布成功判定在线 UI 已验收。其他提供方不能继承此 `connected` 状态。通用 schema、领域测试和客户端契约都不能替代数据库事务与真实提供方验证。 +新增功能的本地验证、云端回滚事务与真实用户 UI 验收分开记录。个人试用 Supabase 交换迁移已部署并完成数据库与权限检查,当前千问托管试用站已发布;静态资源、未登录入口和邮箱确认回跳已验证。邮箱确认开启,默认/显式目标的 303 回跳与精确允许列表匹配;真实用户登录后的 UI 和文件存取仍待按下列步骤验收。此前 Sites 403 属于历史访问记录。其他提供方不能继承此 `connected` 状态;通用 schema、领域测试和客户端契约不能替代数据库事务与真实提供方验证。 + +## 真实存储与登录后验收操作 + +以下是待执行的验收手册;结果应记录在 [验证记录](evidence/verification.md),不能把操作步骤本身记为通过。 + +1. 使用应用账号完成所需的邮箱确认并登录,创建名称明确的专用合成验收工作区。后续操作始终确认当前工作区,使用合成文件与业务字段。 +2. 准备一张小于 20 MiB 的合成 JPEG、PNG 或 WebP 图片,保留原文件并记录文件名、字节大小与 SHA-256。通过资产或申领上传入口上传,确认资产登记完成且列表出现对应记录;仅文件传输成功不能代替登记成功。断针申领必须包含照片,PDF 不能代替现场照片。 +3. 将该资产关联到一条合成申领,保存后检查列表与详情只出现预期单据和附件引用。若提示失败或回包不确定,先刷新回读确认是否已保存,再决定是否重试,避免重复创建。 +4. 从已登记资产取得预览/下载入口,确认可用,并将下载文件的大小与 SHA-256 和原文件比较。授权地址有时效,保留资产引用即可,不把临时签名地址放入公开验收记录。 +5. 刷新页面,然后退出并重新登录,重新选择同一工作区;确认单据、附件登记与关联仍存在,并重新获取下载入口再次读取相同文件。预览、下载及重新登录回读全部成功,才能记录本轮真实存储链路通过。 +6. 由工作区所有者导出当前 JSON,保存后在该专用工作区内进行一次受控修改,再预览并恢复原快照。核对变更数量、业务记录与附件引用,确认附件仍可读取。此恢复只针对同工作区业务 JSON;包中只有资产元数据与引用,没有文件正文,不能恢复已丢失的文件,也不能作为跨提供方迁移或全量备份。 diff --git a/examples/saas/mes/factory-operations/evidence/verification.md b/examples/saas/mes/factory-operations/evidence/verification.md index 81ae6a9..180d05f 100644 --- a/examples/saas/mes/factory-operations/evidence/verification.md +++ b/examples/saas/mes/factory-operations/evidence/verification.md @@ -101,16 +101,34 @@ npm run test:sql --workspace factory-operations-workbench 检查结束后业务数据行与 Storage 对象均为零,检查前后数据指纹一致;没有留下合成验收数据。额外匿名 RPC 检查返回 HTTP 401 / SQLSTATE 42501。安全 advisor 没有 WARN 或 ERROR;这是本次检查结果,不将其反写成早期检查已经通过。 -当前 `auth.users` 为零,首次应用账号由使用者自行注册。上述检查验证已部署数据库的事务和授权行为,**没有进行真实用户注册、登录后的云端 UI 端到端验收**,也不声明本轮重新执行了资产文件传输或多个数据库连接的并发实测。图片识别继续为 mock,真实 OCR 未接入。 +该次云端事务检查结束时 `auth.users` 为零,首次应用账号由使用者自行注册。上述检查验证已部署数据库的事务和授权行为,**没有进行真实用户注册、登录后的云端 UI 端到端验收**,也不声明本轮重新执行了资产文件传输或多个数据库连接的并发实测。图片识别继续为 mock,真实 OCR 未接入。 -案例保持 `integrated`、`customerConfirmed: false`。Supabase 交换服务新增实际部署证据,HTTP/MySQL 等其他提供方仍按各自验证范围声明。Sites 所有者私有试用站已发布,Supabase 已连接;发布与浏览器验收的具体范围见下节。本记录不包含私有站点地址、项目标识、连接密钥或账号信息。 +案例保持 `integrated`、`customerConfirmed: false`。Supabase 交换服务新增实际部署证据,HTTP/MySQL 等其他提供方仍按各自验证范围声明。随后 Sites 试用站与千问托管入口的发布及浏览器验证范围分别记录于下文。本记录不包含私有站点地址、项目标识、连接密钥或账号信息。 -## 私有试用站发布与回跳验证(2026-10-15) +## Sites 私有试用站阶段(历史,2026-10-15) Sites 远端发布工具正式返回 **`succeeded`**,并返回已部署地址;站点保持所有者私有。工厂前端使用已部署的 Supabase 组合,未增加或替换应用账号体系,也未改变多提供方契约。此处仅记录发布结果,不保存具体私有地址、项目 ID 或密钥。 邮箱确认保持开启;默认确认回跳地址和精确重定向允许列表已配置为在线工厂入口。对默认目标及显式指定目标重新检查 HTTP 303,两个回跳位置均匹配预期工厂入口。该结果证明回跳配置正确,真实用户注册、收取确认邮件及登录后的 UI 验收仍待完成。 -本机静态预览已完成验收,4318 端口的工厂构建可打开且 Supabase 已配置。当前网络访问线上私有站点被托管边缘拦截:应用内浏览器返回 Cloudflare security blocked,未进入工作台页面;普通 HTTPS HEAD 请求站点根路径同样返回 Cloudflare 403。因此本轮证明远端发布工具成功和本机静态页面可用,**没有完成线上浏览器或真实用户云端 UI 验收**。 +本机静态预览已完成验收,4318 端口的工厂构建可打开且 Supabase 已配置。该阶段网络访问线上私有站点被托管边缘拦截:应用内浏览器返回 Cloudflare security blocked,未进入工作台页面;普通 HTTPS HEAD 请求站点根路径同样返回 Cloudflare 403。因此本轮证明远端发布工具成功和本机静态页面可用,**没有完成线上浏览器或真实用户云端 UI 验收**。 本轮未改应用代码,重新运行的 **239 项测试全部通过**(24 项仓库测试、164 项案例单元测试、8 项 POC、43 项 SQL 回归)。前述 34 项浏览器回归保留其原有本地执行范围,不作为此次线上访问成功的证据。 + +## 千问托管入口与真实存储复核(2026-10-15) + +当前千问托管试用站已发布,目录及应用静态资源、未登录工厂入口均已验证可访问。发布产物中的 JavaScript、CSS、JSON、字体与媒体文件逐项比对 SHA-256 一致;HTML 保留原始业务内容,增加托管平台的品牌展示与页面元信息。此静态检查没有读取或修改真实业务数据,也不代替登录后的业务验收。 + +邮箱确认仍开启,默认回跳地址和精确重定向允许列表已更新到当前工厂入口;默认与显式目标的 303 回跳均匹配。此前 Sites 403 是另一托管入口的历史访问结果,不代表当前千问入口仍被拦截。 + +云端只读审计确认四个迁移齐备;私有资产桶上限为 20 MiB,允许 JPEG、PNG、WebP、PDF、MP4 与 MP3。检查时资产记录及 Storage 对象均为零;匿名 RPC 返回 HTTP 401,新托管来源的 CORS 检查返回 200,安全 advisor 没有 WARN 或 ERROR。本次未新增文件、业务记录或应用账号。 + +**真实用户登录后 UI,以及上传 → 登记 → 预览/下载 → 刷新和重新登录回读,尚未完成验收。** 待按 [公开验收手册](../acceptance.md#真实存储与登录后验收操作) 在专用合成工作区执行并补充结果。已验证的静态资源、未登录入口、回跳配置及数据库权限各有独立范围,不合并宣称真实用户端到端验收通过。案例继续保持 `integrated`、`customerConfirmed: false`。 + +## 申领保存确认修复(2026-10-15) + +复现到创建申领 RPC 已成功返回,但其后的展示用资产查询失败,导致界面将已经保存的单据报告为提交失败。将资产读取与断针照片校验移到写入前,RPC 成功后直接映射返回单据与预读资产,不再以附属查询失败覆盖保存结果。数据库权限与 RPC 校验保持不变。 + +新增三项回归分别覆盖资产预读失败时不写入、成功写入后网络不可用仍返回单据和附件,以及非图片资产不能作为断针照片。独立代码复核未发现阻塞问题;`npm ci` 完成且 audit 为零,`npm run check` 通过 **242 项测试**(24 项仓库、167 项案例单元、8 项 POC、43 项 SQL)及三个构建;工厂浏览器回归 **9 项通过**。这些测试使用合成夹具,没有创建当前云端用户或业务数据。 + +本修复针对已经收到成功回包后的错误复读;创建 RPC 自身响应丢失仍需先刷新确认结果,不声明所有创建请求已具备服务端幂等。 diff --git a/examples/saas/mes/factory-operations/integrations.md b/examples/saas/mes/factory-operations/integrations.md index 8fd31ca..2c1a2c9 100644 --- a/examples/saas/mes/factory-operations/integrations.md +++ b/examples/saas/mes/factory-operations/integrations.md @@ -100,10 +100,8 @@ npm run test:sql --workspace factory-operations-workbench ## 个人试用环境当前状态 -2026-10-15,交换迁移部署及云端检查完成后,业务数据行与 Storage 对象均为零,检查前后数据指纹一致,`auth.users` 为零;匿名 RPC 返回 401 / 42501。安全 advisor 未返回 WARN 或 ERROR。此结果不证明外部邮箱投递、真实用户端到端操作或生产运维已验收,首次应用注册由使用者自行完成。 +2026-10-15,当前千问托管试用站已发布,静态资源、未登录工厂入口与邮箱确认回跳已验证;业务身份、数据及附件仍由现有 Supabase 提供。静态站点访问与工作台应用登录分别管理,多提供方契约不变。此前 Sites 发布成功但访问返回 Cloudflare 403 的情况保留在 [历史验证记录](evidence/verification.md)。 -Sites 所有者私有静态试用站已发布,远端发布工具返回 `succeeded`,Supabase 已连接。静态托管承载目录、前端与公开材料;业务身份、数据及资产仍由当前选择的提供方负责,站点访问权限与工作台应用登录分别管理。 +本次云端只读审计确认:四个迁移均已应用;资产桶为私有桶,上限 20 MiB,允许 JPEG、PNG、WebP、PDF、MP4 与 MP3;资产记录及 Storage 对象均为零。匿名 RPC 返回 HTTP 401,新托管来源的 CORS 检查返回 200,安全 advisor 没有 WARN 或 ERROR。先前回滚事务验证及其前后数据指纹另记于验证记录,这次只读检查未执行上传或写入业务数据。 -Supabase 邮箱确认保持开启。默认确认回跳地址与精确重定向允许列表已指向在线工厂入口,默认和显式目标的 HTTP 303 回跳复测均匹配;这验证了回跳配置,首次真实用户注册、邮件收取与登录后 UI 验收仍由使用者完成。 - -本机静态预览已验收。当前网络访问发布站被 Cloudflare 托管边缘拦截,应用内浏览器未进入页面,普通 HTTPS HEAD 请求亦返回 403;因此远端发布成功与在线 UI 验收分别记录,当前不声明后者已完成。 +Supabase 邮箱确认保持开启。默认确认回跳地址与精确重定向允许列表已指向当前在线工厂入口,默认和显式目标的 HTTP 303 回跳复测均匹配。真实用户的邮件收取、登录后 UI,以及文件上传、登记、下载、刷新与重新登录回读仍待验收,不能由静态入口可访问或 CORS 成功推定通过。具体操作见 [真实存储与登录后验收操作](acceptance.md#真实存储与登录后验收操作)。 diff --git a/examples/saas/mes/factory-operations/workbench/src/data/supabase.ts b/examples/saas/mes/factory-operations/workbench/src/data/supabase.ts index 67ff984..40e8834 100644 --- a/examples/saas/mes/factory-operations/workbench/src/data/supabase.ts +++ b/examples/saas/mes/factory-operations/workbench/src/data/supabase.ts @@ -399,14 +399,15 @@ export function createSupabaseServices( }, async createReport(id, input) { validateReport(input); + const assets = await listAssets(id); if (input.kind === "broken_needle") - validateReportAttachments(input, await listAssets(id)); - return loadReport( - await rpc("fo_create_report", { - p_workspace: id, - p_input: input, - }), - ); + validateReportAttachments(input, assets); + const row = await rpc("fo_create_report", { + p_workspace: id, + p_input: input, + }); + // 保存成功后不再追加可能失败的展示查询,避免把已落库申领误报为提交失败。 + return reportFromRow(row, assets); }, async transitionReport(input) { return loadReport( diff --git a/examples/saas/mes/factory-operations/workbench/tests/supabase-contract.test.ts b/examples/saas/mes/factory-operations/workbench/tests/supabase-contract.test.ts index 56d5ec1..4ea3ab8 100644 --- a/examples/saas/mes/factory-operations/workbench/tests/supabase-contract.test.ts +++ b/examples/saas/mes/factory-operations/workbench/tests/supabase-contract.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { createClient } from "@supabase/supabase-js"; import { createSupabaseServices } from "../src/data/supabase"; +import type { ReportInput } from "../src/data/contracts"; vi.mock("@supabase/supabase-js", () => ({ createClient: vi.fn() })); describe("Supabase 业务冲突", () => { it("PT409立即显示刷新提示,禁用SDK自动重试,并对所有网络请求设置超时", async () => { @@ -33,3 +34,129 @@ describe("Supabase 业务冲突", () => { ); }); }); + +describe("Supabase 申领保存确认", () => { + const workspaceId = "workspace-1"; + const input: ReportInput = { + kind: "broken_needle", + contractNumber: "QA-CONTRACT", + materialNumber: "QA-NEEDLE", + station: "合成测试工位", + reason: "合成测试:更换断针", + quantity: 1, + assetIds: ["asset-1"], + }; + const asset = { + id: "asset-1", + workspace_id: workspaceId, + name: "现场照片.png", + media_type: "image/png", + size: 4, + storage_path: `${workspaceId}/user-1/asset-1.png`, + created_at: "2026-10-15T00:00:00Z", + sha256: "a".repeat(64), + }; + const row = { + id: "report-1", + workspace_id: workspaceId, + kind: input.kind, + contract_number: input.contractNumber, + material_number: input.materialNumber, + station: input.station, + reason: input.reason, + quantity: input.quantity, + asset_ids: input.assetIds, + status: "submitted", + fragments_recovered: false, + version: 1, + created_by: "user-1", + created_at: "2026-10-15T00:00:00Z", + updated_at: "2026-10-15T00:00:00Z", + provenance: { contentOrigin: "user-imported" }, + }; + function setup( + range: ReturnType, + rpc: ReturnType, + ) { + const query = { + select: () => query, + order: () => query, + eq: () => query, + range, + }; + vi.mocked(createClient).mockReturnValue({ + from: vi.fn().mockReturnValue(query), + rpc, + } as never); + return createSupabaseServices( + "https://example.supabase.co", + "sb_publishable_example", + ); + } + + it("附件预读失败时不写申领,避免落库后假失败", async () => { + const range = vi.fn().mockResolvedValue({ + data: null, + count: null, + error: { message: "资产读取暂时失败" }, + }); + const rpc = vi.fn().mockResolvedValue({ data: row, error: null }); + const services = setup(range, rpc); + + await expect( + services.operations.createReport(workspaceId, { + ...input, + kind: "consumable", + assetIds: [], + }), + ).rejects.toThrow("资产读取暂时失败"); + + expect(range).toHaveBeenCalledOnce(); + expect(rpc).not.toHaveBeenCalled(); + }); + + it("写入成功后不再读资产,网络中断仍返回已保存单据及照片", async () => { + const calls: string[] = []; + const range = vi.fn().mockImplementation(async () => { + calls.push("read-assets"); + return { data: [asset], count: 1, error: null }; + }); + const rpc = vi.fn().mockImplementation(async () => { + calls.push("create-report"); + range.mockRejectedValue(new Error("写入后网络中断")); + return { data: row, error: null }; + }); + const services = setup(range, rpc); + + const saved = await services.operations.createReport(workspaceId, input); + + expect(saved).toMatchObject({ + id: row.id, + status: "submitted", + assets: [{ id: asset.id, name: asset.name, sha256: asset.sha256 }], + }); + expect(calls).toEqual(["read-assets", "create-report"]); + expect(range).toHaveBeenCalledOnce(); + expect(rpc).toHaveBeenCalledExactlyOnceWith("fo_create_report", { + p_workspace: workspaceId, + p_input: input, + }); + }); + + it("断针引用的资产不是照片时仍在写入前拒绝", async () => { + const range = vi.fn().mockResolvedValue({ + data: [{ ...asset, name: "说明.pdf", media_type: "application/pdf" }], + count: 1, + error: null, + }); + const rpc = vi.fn(); + const services = setup(range, rpc); + + await expect( + services.operations.createReport(workspaceId, input), + ).rejects.toThrow("断针上报至少需要一张照片"); + + expect(range).toHaveBeenCalledOnce(); + expect(rpc).not.toHaveBeenCalled(); + }); +});