Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
a9248e61
Commit
a9248e61
authored
Oct 22, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
Sync archived operational and design records into test
parents
70f95af3
15d08d44
Pipeline
#3862
failed in 0 seconds
Changes
15
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
1189 additions
and
0 deletions
+1189
-0
docs/README.md
+2
-0
docs/_archive/2026-10-operations-and-design/2026-10-14-push-latency-local.md
+22
-0
docs/_archive/2026-10-operations-and-design/2026-10-14-return-visit-push-fix-test.md
+37
-0
docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review-document.html
+194
-0
docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review.html
+214
-0
docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review.md
+146
-0
docs/_archive/2026-10-operations-and-design/2026-10-18-business-date-shanghai-test-validation.md
+40
-0
docs/_archive/2026-10-operations-and-design/2026-10-18-jvs-complaint-ingestion-prod-validation.md
+68
-0
docs/_archive/2026-10-operations-and-design/2026-10-18-jvs-complaint-ingestion-test-validation.md
+77
-0
docs/_archive/2026-10-operations-and-design/2026-10-19-meyer-friday-pac-imaging-review-document.html
+93
-0
docs/_archive/2026-10-operations-and-design/2026-10-19-meyer-friday-pac-imaging-review.md
+85
-0
docs/_archive/2026-10-operations-and-design/2026-10-20-data-warehouse-and-pac-positioning.md
+84
-0
docs/_archive/2026-10-operations-and-design/README.md
+37
-0
docs/_archive/2026-10-operations-and-design/manifest.json
+83
-0
docs/_archive/README.md
+7
-0
No files found.
docs/README.md
View file @
a9248e61
...
@@ -22,6 +22,8 @@ PAC 客户召回与 FridayAI 的正式产品、原理和接口文档统一维护
...
@@ -22,6 +22,8 @@ PAC 客户召回与 FridayAI 的正式产品、原理和接口文档统一维护
临时规划、旧流程、环境快照、验收报告和重复销售稿移入
[
_archive/
](
_archive/README.md
)
,保留原文件名、引用与移动清单。
临时规划、旧流程、环境快照、验收报告和重复销售稿移入
[
_archive/
](
_archive/README.md
)
,保留原文件名、引用与移动清单。
2026 年 10 月新增归档:
[
美亚与患者事实开发记录
](
_archive/2026-10-meyer-and-patient-facts/README.md
)
、
[
运维验收与架构讨论
](
_archive/2026-10-operations-and-design/README.md
)
。
设计准则、行为埋点和架构决策已全部归档至
[
_archive/design/
](
_archive/design/
)
与
[
_archive/adr/
](
_archive/adr/
)
。
设计准则、行为埋点和架构决策已全部归档至
[
_archive/design/
](
_archive/design/
)
与
[
_archive/adr/
](
_archive/adr/
)
。
Claude Code 的编号资料见
[
FridayAI 开发档案
](
_archive/friday-ai/docs/README.md
)
。未结事项见
[
跟进清单
](
qa/follow-ups.md
)
,归档不表示解决。环境现状需要重新查询;当前行为以代码和正式文档为准。
Claude Code 的编号资料见
[
FridayAI 开发档案
](
_archive/friday-ai/docs/README.md
)
。未结事项见
[
跟进清单
](
qa/follow-ups.md
)
,归档不表示解决。环境现状需要重新查询;当前行为以代码和正式文档为准。
docs/
qa
/2026-10-14-push-latency-local.md
→
docs/
_archive/2026-10-operations-and-design
/2026-10-14-push-latency-local.md
View file @
a9248e61
# Push 关键路径优化与排障(本地)
# Push 关键路径优化与排障(本地)
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。
分支
`codex/push-latency-fix`
,基于 main
`99fb2648cacdafcd0f26ec8540d04c8465ecbb87`
。独立 worktree,主目录及正在运行的本地服务未变。已提交并部署测试;生产未改动。
分支
`codex/push-latency-fix`
,基于 main
`99fb2648cacdafcd0f26ec8540d04c8465ecbb87`
。独立 worktree,主目录及正在运行的本地服务未变。已提交并部署测试;生产未改动。
## 最终边界
## 最终边界
...
@@ -60,3 +62,23 @@
...
@@ -60,3 +62,23 @@
-
历史字段审计在后台完成 20ms,不阻塞响应。同期主档请求 195ms。健康 JSON status=ok。
-
历史字段审计在后台完成 20ms,不阻塞响应。同期主档请求 195ms。健康 JSON status=ok。
-
本条真实预约没有慢环节,确认业务落库后毫秒级返回;它不能证明此前分钟级超时的具体根因,也不是宿主建预约到消费响应的端到端计时。
-
本条真实预约没有慢环节,确认业务落库后毫秒级返回;它不能证明此前分钟级超时的具体根因,也不是宿主建预约到消费响应的端到端计时。
-
已获得真实预约样本,暂停本轮定时跟踪。生产未修改。
-
已获得真实预约样本,暂停本轮定时跟踪。生产未修改。
## 生产发布准备(2026-10-14)
-
用户验收测试并授权合 main、部署生产;检查发现 4 个召回主查询及其并行进程运行中,用户进一步明确允许中断本轮后部署。
-
main 快进到
`584bd3dd34c2ed20f313d31c476b7f14220f4a88`
;相比测试的业务代码相同,增加测试验收记录。没有将 test 的其他提交合入 main。
-
生产原版本
`99fb264`
,可用内存约 11GiB、/data 可用134GiB,旧服务健康。队列为空但召回SQL活跃,不能据队列断言空闲。
-
有一条13:03:41开始的病历 push 仍标 running,早于原容器13:03:42启动;未将其状态改成功/失败。
-
回退目录
`/data/backups/pac-release/584bd3d-20261014`
:原源码SHA、PAC旧镜像及回退标签、私密配置副本、Compose校验值、迁移元数据、AI/ingress原镜像和启动时间、构建/部署日志与退出标记。配置/镜像备份不是业务数据备份。
-
生产镜像构建退出0。使用 COMPOSE_MANAGED=1、固定SHA、标准发布脚本 --no-pull,只发布PAC;本次唯一数据库变更为并发索引,不重放数据。
## 生产发布验收
-
发布时间北京时间 19:02:51—19:10:29,固定 main@584bd3d,标准部署脚本退出0。
-
并发索引扫描/校验完成后等待旧快照;按用户本轮明确授权,仅取消了4条已核对SQL形态的旧召回主查询(1540/1556/1621/1554),随后迁移成功。没有取消其他查询或手工标记业务任务成功。
-
索引迁移19:10:12完成,ready/valid均true,索引350MB;Prisma无pending。
-
实际运行镜像与构建产物一致:PAC service
`271291ef75ee`
,web
`b88c9fbdf169`
,docs
`2fb22de20fd0`
。
-
公网 https://pac.friday.tech/health 返回JSON status=ok;公网/plans为200;loopback及私网health/web通过;docs
`/docs/integration/channel-push`
本机和私网200。
-
service/web env逐字未变,Compose校验值一致;FridayAI service/web及ingress镜像和启动时间均与发布前一致。
-
首次验收期间生产Friday尚无新push,未声称生产真实推送性能已验证;测试真实预约167ms的结果见上节。
-
主目录原有未跟踪AGENTS.md/output保持原样,本地服务未操作。
docs/_archive/2026-10-operations-and-design/2026-10-14-return-visit-push-fix-test.md
0 → 100644
View file @
a9248e61
# 测试回访推送修复验收
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。
-
功能提交:dca8663;test 合并并部署:a627d29cbf75321c88f2a58b89ea174119ac62ff。
-
仅测试环境;未合入 main、未部署生产。主目录已有文件未改。
-
根因:回访/患者关系仍加载整个租户患者索引,138772 条查询结果通过 push(...rows) 展开触发 Maximum call stack size exceeded。
-
修复:两条链路按本批引用患者、tenant、sourceUnit 查询(关系包含本人和关系人),每块1000个ID;逐条合并结果,索引查询禁止省略引用范围。
-
同类检查:摄入模块其余查询展开为固定50条;CLI患者查询已有20000条分块;未发现另一处相同的无界租户查询展开。
-
本地:5 suites / 46 tests 通过;service 两组 type-check 通过。新工作区先构建 types/utils/host-connector 和 Prisma client。
-
部署退出0,迁移无pending,运行镜像校验通过,/health JSON status=ok。
-
回退资料:测试机 /root/pac-ops/return-visit-fix-20261014,旧service/web镜像已打回退标签。
## 单条原文重放
-
原失败 syncLog:5e6bcd20-2edc-4847-8d31-f54f348ef829。
-
原证据:afc4d32c-721c-4493-9443-6c494f272a30。
-
原文保持不变,经 HMAC 签名调用正常 /pac/v1/push/rows;严格限定 Friday、customer_return_visit、1条、回访267076、患者700687。
-
成功 syncLog:2edf876a-adad-419f-83cf-534107223b07。
-
HTTP200 / code0 / hasFailures=false / failed=0 / factsFailed=0 / stubs=0。
-
请求396ms;lookup-transform9ms、upsert34ms、touched8ms。
-
该回访已落 patient_return_visits:患者与祥云小镇诊所匹配,已回访,执行人5992。
-
直接调用部署版本 AgentRosterService 验证:5992在该诊所名册,recentVisits=1,inRoster=true。
-
回访为独立资源,正常 transactionsWritten=0、personaEnqueued=0;未修改事实或重算召回。
-
未重放其他回访,未读写5i5ya业务数据。
## 生产发布(用户测试确认后授权)
-
main 合并版本:fa09e5ff75aa4e9414e679166b492ad98a533375。
-
一并发布 main 原有未部署改动:FridayAI侧栏导航保持会话和诊所状态(a33528b,合并efc2f64)。该修复专项9项测试及ai-web类型检查通过。
-
发布前:同步日志无running,5个业务队列active/waiting/delayed均0;召回筛选查询pid12383/12411/12412已运行约33分钟,用户明确允许中断本轮后发布。
-
PAC和FridayAI串行发布均exit0,运行镜像校验通过,无待执行迁移;PAC /health、AI /health/ready、私网页面与AI到PAC连通性通过。
-
发布后旧召回查询已不在pg_stat_activity;本轮未手动重算召回。
-
截至2026-10-14 23:25(上海)未看到发布后新push,不能将测试重放结果当作生产真实回访验收;已确认生产编译代码包含scopedPatientIndexLoader。
-
检查启动后日志未见ERROR/栈溢出;未向生产重放测试回访。
-
回退目录:/data/backups/pac-release/return-visit-20261014(旧镜像标签、环境配置、compose、发布日志和退出码)。
-
main主目录未提交文件及本地服务未修改;QA记录保留在隔离工作区,尚未提交。
docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review-document.html
0 → 100644
View file @
a9248e61
<!doctype html>
<html
lang=
"zh-CN"
><head><meta
charset=
"utf-8"
><meta
name=
"viewport"
content=
"width=device-width,initial-scale=1"
><meta
name=
"color-scheme"
content=
"light dark"
><meta
name=
"description"
content=
"美亚、FridaySaaS 与 PAC 的影像及分析数据方案,供内部评审。"
><title>
影像与分析数据方案设计 · 美亚 × FridaySaaS × PAC
</title><style>
:root
{
color-scheme
:
light
;
--ground
:
#f4f6fb
;
--surface
:
#fff
;
--sunken
:
#eaeef6
;
--ink
:
#0f1b33
;
--ink-2
:
#44506a
;
--ink-3
:
#6f7a91
;
--brand
:
#0032a0
;
--brand-soft
:
#e3eafa
;
--rule
:
#dde3ee
;
--note
:
#f5f7fc
;
--warn-bg
:
#fff9ed
;
--warn-rule
:
#d6ae63
;
--serif
:
"Noto Serif SC"
,
"Songti SC"
,
"STSong"
,
serif
;
--sans
:
"Noto Sans SC"
,
"PingFang SC"
,
"Microsoft YaHei"
,
system-ui
,
sans-serif
;
--mono
:
"IBM Plex Mono"
,
ui-monospace
,
"SFMono-Regular"
,
Menlo
,
monospace
}
:root
[
data-theme
=
dark
]
{
color-scheme
:
dark
;
--ground
:
#0a1020
;
--surface
:
#121a2d
;
--sunken
:
#1a2440
;
--ink
:
#e7ecf6
;
--ink-2
:
#b3bdd2
;
--ink-3
:
#8994ab
;
--brand
:
#86a7ff
;
--brand-soft
:
#1d2b52
;
--rule
:
#2a3550
;
--note
:
#172037
;
--warn-bg
:
#292518
;
--warn-rule
:
#a38a55
}
@media
(
prefers-color-scheme
:
dark
){
:root:not
([
data-theme
=
light
])
{
color-scheme
:
dark
;
--ground
:
#0a1020
;
--surface
:
#121a2d
;
--sunken
:
#1a2440
;
--ink
:
#e7ecf6
;
--ink-2
:
#b3bdd2
;
--ink-3
:
#8994ab
;
--brand
:
#86a7ff
;
--brand-soft
:
#1d2b52
;
--rule
:
#2a3550
;
--note
:
#172037
;
--warn-bg
:
#292518
;
--warn-rule
:
#a38a55
}}
*
{
box-sizing
:
border-box
}
html
{
scroll-behavior
:
smooth
}
body
{
margin
:
0
;
background
:
var
(
--ground
);
color
:
var
(
--ink
);
font-family
:
var
(
--sans
);
font-size
:
16px
;
line-height
:
1.9
;
-webkit-font-smoothing
:
antialiased
}
::selection
{
background
:
var
(
--brand-soft
);
color
:
var
(
--ink
)}
a
{
color
:
var
(
--brand
);
text-decoration-color
:
color-mix
(
in
srgb
,
var
(
--brand
)
35%
,
transparent
);
text-underline-offset
:
4px
}
a
:hover
{
text-decoration-color
:
var
(
--brand
)}
button
{
font
:
inherit
}
a
:focus-visible
,
button
:focus-visible
{
outline
:
2px
solid
var
(
--brand
);
outline-offset
:
4px
}
.wrap
{
max-width
:
1040px
;
margin
:
auto
;
padding
:
40px
26px
48px
}
.document-header
{
padding
:
0
8px
28px
}
.topline
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
16px
;
align-items
:
center
;
margin-bottom
:
16px
}
.eyebrow
{
font-size
:
12px
;
letter-spacing
:
.10em
;
color
:
var
(
--brand
);
font-weight
:
700
}
.tools
{
display
:
flex
;
gap
:
4px
}
.tools
button
{
border
:
0
;
color
:
var
(
--ink-3
);
background
:
transparent
;
padding
:
5px
8px
;
cursor
:
pointer
;
font-size
:
12px
;
border-radius
:
5px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
.tools
button
:hover
{
background
:
var
(
--brand-soft
);
color
:
var
(
--brand
)}
.tools
svg
{
width
:
15px
;
height
:
15px
}
h1
{
font-family
:
var
(
--serif
);
font-weight
:
700
;
font-size
:
clamp
(
30px
,
3.8vw
,
42px
);
line-height
:
1.35
;
letter-spacing
:
.015em
;
margin
:
0
0
14px
}
.meta
{
display
:
flex
;
align-items
:
center
;
flex-wrap
:
wrap
;
gap
:
9px
;
color
:
var
(
--ink-3
);
font-size
:
12px
;
margin-bottom
:
14px
}
.meta
.version
{
background
:
var
(
--brand-soft
);
color
:
var
(
--brand
);
border-radius
:
99px
;
padding
:
1px
10px
;
font-weight
:
500
}
.meta
time
{
font-family
:
var
(
--mono
);
font-size
:
12px
}
.scope
{
max-width
:
48em
;
color
:
var
(
--ink-2
);
font-size
:
14px
;
line-height
:
1.8
;
margin
:
0
}
main
{
background
:
var
(
--surface
);
border-radius
:
14px
;
padding
:
42px
58px
40px
}
p
{
margin
:
0
0
18px
;
color
:
var
(
--ink-2
)}
strong
{
color
:
var
(
--ink
);
font-weight
:
600
}
p
:last-child
{
margin-bottom
:
0
}
.boundary
{
padding
:
0
0
0
20px
;
border-left
:
3px
solid
var
(
--brand
);
font-size
:
18px
;
line-height
:
1.85
;
margin-bottom
:
24px
}
.boundary
strong
{
font-weight
:
600
}
.principle
{
font-size
:
15.5px
;
line-height
:
1.9
;
margin-bottom
:
30px
}
.contents
{
border-top
:
1px
solid
var
(
--rule
);
border-bottom
:
1px
solid
var
(
--rule
);
padding
:
17px
0
19px
;
margin
:
32px
0
0
}
.contents-label
{
color
:
var
(
--ink-3
);
font-size
:
11px
;
letter-spacing
:
.14em
;
display
:
block
;
margin-bottom
:
9px
}
.contents-links
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
gap
:
7px
18px
}
.contents
a
{
text-decoration
:
none
;
color
:
var
(
--ink-2
);
font-size
:
13px
;
white-space
:
nowrap
}
.contents
a
:hover
{
color
:
var
(
--brand
)}
.contents
a
span
{
font-family
:
var
(
--mono
);
display
:
inline-block
;
width
:
29px
;
color
:
var
(
--brand
);
font-size
:
11px
}
.chapter
{
padding-top
:
46px
;
scroll-margin-top
:
28px
}
.chapter
+
.chapter
{
margin-top
:
42px
;
border-top
:
1px
solid
var
(
--rule
)}
.chapter-head
{
display
:
flex
;
align-items
:
baseline
;
gap
:
15px
;
margin-bottom
:
24px
}
.chapter-num
{
color
:
var
(
--brand
);
font-family
:
var
(
--mono
);
font-size
:
15px
;
line-height
:
1.3
;
flex
:
none
}
.chapter
h2
{
font-family
:
var
(
--serif
);
font-size
:
26px
;
line-height
:
1.45
;
font-weight
:
700
;
margin
:
0
;
text-wrap
:
pretty
}
.chapter
h3
{
font-size
:
18px
;
line-height
:
1.6
;
color
:
var
(
--ink
);
margin
:
32px
0
14px
;
font-weight
:
600
}
.chapter-head
+
h3
{
margin-top
:
0
}
.step-heading
{
display
:
flex
;
gap
:
8px
}
.step-heading
span
{
color
:
var
(
--brand
);
font-weight
:
500
}
.figure-intro
{
font-size
:
14px
;
margin-bottom
:
12px
}
ul
,
ol
{
padding-left
:
23px
;
color
:
var
(
--ink-2
);
margin
:
14px
0
22px
}
li
{
padding-left
:
3px
;
margin
:
7px
0
}
li
::marker
{
color
:
var
(
--brand
)}
blockquote
{
margin
:
24px
0
;
padding
:
20px
24px
;
background
:
var
(
--brand-soft
);
border-left
:
3px
solid
var
(
--brand
)}
blockquote
p
{
color
:
var
(
--ink
);
font-family
:
var
(
--serif
);
font-size
:
20px
;
line-height
:
1.75
;
font-weight
:
600
}
.consequence
{
border-left
:
3px
solid
var
(
--warn-rule
);
background
:
var
(
--warn-bg
);
padding
:
17px
21px
;
margin
:
22px
0
}
.consequence
strong
{
font-weight
:
500
}
.own-business
{
border-top
:
1px
dashed
var
(
--rule
);
padding-top
:
20px
;
margin-top
:
24px
}
.future
{
margin
:
28px
0
0
;
padding
:
18px
23px
;
background
:
var
(
--note
);
font-size
:
14px
;
border-left
:
2px
solid
var
(
--rule
)}
.future
p
{
margin-bottom
:
8px
}
.future
ul
{
margin
:
7px
0
0
}
.future
li
{
margin
:
4px
0
}
.duties
{
list-style
:
none
;
padding
:
0
;
counter-reset
:
duty
;
margin
:
20px
0
24px
}
.duties
li
{
counter-increment
:
duty
;
position
:
relative
;
padding
:
15px
0
15px
42px
;
margin
:
0
;
border-top
:
1px
solid
var
(
--rule
)}
.duties
li
:last-child
{
border-bottom
:
1px
solid
var
(
--rule
)}
.duties
li
::before
{
content
:
'0'
counter
(
duty
);
position
:
absolute
;
left
:
0
;
color
:
var
(
--brand
);
font
:
14px
var
(
--mono
);
top
:
21px
}
.questions
{
padding-left
:
23px
}
.questions
li
{
margin
:
12px
0
}
.questions
strong
{
color
:
var
(
--brand
)}
.table-wrap
{
margin
:
22px
0
26px
}
table
{
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
14px
;
line-height
:
1.8
;
text-align
:
left
}
th
{
font-weight
:
600
;
color
:
var
(
--ink
);
background
:
var
(
--sunken
);
padding
:
12px
16px
;
border-bottom
:
1px
solid
var
(
--rule
);
vertical-align
:
top
}
td
{
padding
:
13px
16px
;
border-bottom
:
1px
solid
var
(
--rule
);
vertical-align
:
top
;
color
:
var
(
--ink-2
)}
tbody
td
:first-child
{
font-weight
:
500
;
color
:
var
(
--ink
)}
.two-col
td
:first-child
{
width
:
37%
}
.three-col
th
:nth-child
(
1
)
{
width
:
33%
}
.three-col
th
:nth-child
(
2
)
{
width
:
28%
}
.three-col
th
:nth-child
(
3
)
{
width
:
39%
}
#review
td
:first-child
{
width
:
22%
;
color
:
var
(
--brand
);
font-weight
:
600
}
figure
{
margin
:
0
}
.flow
{
padding
:
20px
24px
22px
;
border
:
1px
solid
var
(
--rule
);
border-radius
:
9px
;
margin
:
14px
0
22px
;
background
:
var
(
--note
)}
figcaption
{
font-size
:
13px
;
font-weight
:
600
;
color
:
var
(
--ink
);
display
:
flex
;
gap
:
10px
;
align-items
:
center
;
flex-wrap
:
wrap
;
padding-bottom
:
18px
}
figcaption
>
span
{
font-size
:
11px
;
font-family
:
var
(
--mono
);
color
:
var
(
--brand
);
font-weight
:
400
}
figcaption
>
small
{
font-size
:
11px
;
font-weight
:
400
;
color
:
var
(
--ink-3
);
margin-left
:
auto
}
.flow-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
)
104px
minmax
(
0
,
1.1
fr
)
65px
minmax
(
0
,
1
fr
);
grid-template-rows
:
auto
auto
auto
;
align-items
:
center
}
.capture
{
grid-column
:
1
;
grid-row
:
1
;
text-align
:
center
;
font-size
:
11px
;
color
:
var
(
--ink-3
);
line-height
:
1.7
;
align-self
:
end
;
padding
:
0
0
7px
}
.capture
>
span
{
display
:
block
;
color
:
var
(
--ink-3
);
font-size
:
20px
;
line-height
:
1.3
}
.system
{
grid-row
:
2
;
background
:
var
(
--surface
);
border
:
1px
solid
var
(
--rule
);
border-top
:
2px
solid
var
(
--brand
);
padding
:
16px
8px
;
text-align
:
center
;
align-self
:
stretch
;
display
:
flex
;
flex-direction
:
column
;
justify-content
:
center
;
min-width
:
0
}
.meyer
{
grid-column
:
1
}
.saas
{
grid-column
:
3
;
background
:
var
(
--brand-soft
)}
.pac
{
grid-column
:
5
}
.system-label
{
color
:
var
(
--ink-3
);
font-size
:
10px
;
letter-spacing
:
.10em
;
line-height
:
1.5
;
margin-bottom
:
6px
}
.system
>
strong
{
font-size
:
18px
;
line-height
:
1.5
;
font-weight
:
600
;
white-space
:
nowrap
}
.system
>
small
{
font-size
:
11px
;
color
:
var
(
--ink-2
);
margin-top
:
6px
;
line-height
:
1.7
}
.connector
{
grid-row
:
2
;
text-align
:
center
;
color
:
var
(
--brand
);
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
8px
;
padding
:
0
5px
}
.connector.first
{
grid-column
:
2
}
.connector.second
{
grid-column
:
4
}
.connector
>
span
{
font-size
:
11px
;
line-height
:
1.6
}
.connector
svg
{
width
:
38px
;
height
:
12px
;
flex
:
none
}
.connector
small
{
font-size
:
10px
;
color
:
var
(
--ink-3
);
line-height
:
1.5
}
.storage
{
grid-column
:
1
;
grid-row
:
3
;
text-align
:
center
;
color
:
var
(
--ink-3
);
font-size
:
13px
}
.storage
>
span
{
display
:
block
;
line-height
:
1.4
;
font-size
:
20px
}
.storage
>
div
{
border
:
1px
dashed
var
(
--rule
);
background
:
var
(
--surface
);
padding
:
4px
10px
}
.storage
small
{
font-size
:
11px
;
margin-left
:
5px
}
.mobile-only
{
display
:
none
}
.view-path
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
padding
:
19px
20px
;
border-top
:
1px
solid
var
(
--rule
);
border-bottom
:
1px
solid
var
(
--rule
);
margin
:
18px
0
;
color
:
var
(
--ink
);
font-size
:
14px
;
font-weight
:
500
}
.view-path
b
{
color
:
var
(
--brand
);
font-size
:
18px
;
font-weight
:
400
;
line-height
:
1
}
.sources
{
margin-top
:
44px
;
padding-top
:
25px
;
border-top
:
1px
solid
var
(
--rule
);
scroll-margin-top
:
28px
;
font-size
:
12.5px
;
color
:
var
(
--ink-3
);
line-height
:
1.85
}
.sources
h2
{
font-size
:
14px
;
font-weight
:
600
;
margin
:
0
0
12px
}
.sources
p
{
color
:
var
(
--ink-3
);
margin-bottom
:
13px
}
.sources
ul
{
padding-left
:
19px
;
margin
:
14px
0
17px
}
.sources
li
{
padding-left
:
1px
;
margin
:
8px
0
;
color
:
var
(
--ink-3
)}
.sources
a
{
font-weight
:
500
}
.page-footer
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
12px
;
color
:
var
(
--ink-3
);
font-size
:
11px
;
padding
:
19px
8px
0
}
.page-footer
a
{
text-decoration
:
none
;
color
:
var
(
--ink-3
)}
.back-top
{
position
:
fixed
;
right
:
24px
;
bottom
:
22px
;
z-index
:
10
;
border
:
1px
solid
var
(
--rule
);
border-radius
:
99px
;
padding
:
8px
14px
;
background
:
var
(
--surface
);
color
:
var
(
--brand
);
font-size
:
12px
;
text-decoration
:
none
;
box-shadow
:
0
2px
16px
#00000006
;
opacity
:
0
;
pointer-events
:
none
;
transition
:
opacity
.2s
}
.back-top.visible
{
opacity
:
1
;
pointer-events
:
auto
}
.skip-link
{
position
:
absolute
;
top
:
-80px
;
left
:
20px
;
background
:
var
(
--surface
);
padding
:
10px
;
z-index
:
20
}
.skip-link
:focus
{
top
:
10px
}
@media
(
min-width
:
1440px
){
.wrap
{
padding-top
:
48px
}}
@media
(
max-width
:
900px
){
.wrap
{
padding
:
26px
22px
32px
}
main
{
padding
:
34px
35px
}
.chapter
h2
{
font-size
:
24px
}
.flow
{
padding
:
18px
16px
}
.flow-grid
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
90px
minmax
(
0
,
1.1
fr
)
56px
minmax
(
0
,
1
fr
)}
.system
>
strong
{
font-size
:
16px
}
.system
>
small
{
font-size
:
9px
}
.system-label
{
font-size
:
9px
}
.connector
>
span
{
font-size
:
9px
}
.connector
small
{
font-size
:
8px
}}
@media
(
max-width
:
660px
){
body
{
font-size
:
15px
;
line-height
:
1.85
}
.wrap
{
padding
:
22px
15px
28px
}
.document-header
{
padding
:
0
4px
23px
}
.topline
{
align-items
:
flex-start
;
gap
:
8px
;
margin-bottom
:
13px
}
.eyebrow
{
font-size
:
10px
;
letter-spacing
:
.04em
;
padding-top
:
7px
}
.tools
{
gap
:
0
;
flex
:
none
}
.tools
button
{
padding
:
7px
}
.tools
button
span
{
display
:
none
}
.tools
svg
{
width
:
16px
;
height
:
16px
}
h1
span
{
display
:
block
}
h1
{
font-size
:
31px
;
line-height
:
1.4
;
max-width
:
11em
;
text-wrap
:
balance
;
margin-bottom
:
13px
}
.meta
{
font-size
:
11px
;
margin-bottom
:
12px
}
.meta
time
{
font-size
:
11px
}
.scope
{
font-size
:
13px
;
line-height
:
1.8
}
main
{
padding
:
27px
22px
29px
;
border-radius
:
10px
}
.boundary
{
font-size
:
17px
;
line-height
:
1.85
;
padding-left
:
15px
;
margin-bottom
:
21px
}
.principle
{
font-size
:
14px
}
.contents
{
margin-top
:
25px
;
padding
:
15px
0
17px
}
.contents-links
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
gap
:
7px
12px
}
.contents
a
{
font-size
:
12px
}
.contents
a
span
{
width
:
25px
}
.chapter
{
padding-top
:
32px
;
scroll-margin-top
:
18px
}
.chapter
+
.chapter
{
margin-top
:
30px
}
.chapter-head
{
gap
:
10px
;
margin-bottom
:
21px
}
.chapter-num
{
font-size
:
12px
}
.chapter
h2
{
font-size
:
22px
;
line-height
:
1.5
}
.chapter
h3
{
font-size
:
16px
;
margin-top
:
28px
}
.chapter-head
+
h3
{
margin-top
:
0
}
.consequence
{
padding
:
14px
16px
}
.chapter
blockquote
{
padding
:
16px
18px
;
margin
:
22px
0
}
.chapter
blockquote
p
{
font-size
:
18px
;
line-height
:
1.8
}
.table-wrap
{
margin
:
22px
0
}
.table-wrap
thead
{
display
:
none
}
.table-wrap
table
,
.table-wrap
tbody
{
display
:
block
}
.table-wrap
tr
{
display
:
block
;
padding
:
13px
0
;
border-bottom
:
1px
solid
var
(
--rule
)}
.table-wrap
tr
:first-child
{
border-top
:
2px
solid
var
(
--brand
)}
.table-wrap
td
{
display
:
block
;
padding
:
3px
0
;
border
:
0
;
width
:
auto
!important
;
line-height
:
1.85
;
font-size
:
13px
}
.table-wrap
td
:first-child
{
font-size
:
14px
;
font-weight
:
600
;
padding
:
0
0
6px
;
color
:
var
(
--ink
)}
.three-col
td
:not
(
:first-child
)
{
display
:
grid
;
grid-template-columns
:
6.4em
1
fr
;
gap
:
10px
}
.three-col
td
:not
(
:first-child
)
::before
{
content
:
attr
(
data-label
);
font-size
:
11px
;
color
:
var
(
--ink-3
);
padding-top
:
2px
}
#review
td
:first-child
{
color
:
var
(
--brand
)}
.flow
{
padding
:
16px
;
margin
:
13px
0
20px
}
figcaption
{
font-size
:
12px
;
gap
:
8px
;
padding-bottom
:
18px
}
figcaption
>
small
{
display
:
block
;
flex-basis
:
100%
;
font-size
:
10px
;
margin
:
0
}
.flow-grid
{
display
:
flex
;
flex-direction
:
column
;
gap
:
0
}
.capture
{
font-size
:
11px
;
align-self
:
center
;
padding-bottom
:
4px
}
.system
{
align-self
:
stretch
;
padding
:
14px
;
min-height
:
95px
}
.system
>
strong
{
font-size
:
19px
}
.system
>
small
{
font-size
:
11px
}
.system-label
{
font-size
:
10px
;
margin-bottom
:
3px
}
.connector
{
padding
:
10px
0
;
gap
:
4px
}
.connector
svg
{
transform
:
rotate
(
90deg
);
width
:
28px
;
height
:
12px
;
margin
:
6px
0
}
.connector
>
span
{
font-size
:
11px
}
.connector
>
span
br
{
display
:
none
}
.connector
small
{
font-size
:
10px
}
.storage
{
margin-top
:
16px
;
align-self
:
stretch
;
font-size
:
12px
}
.storage
>
span
{
display
:
none
}
.storage
>
div
{
padding
:
7px
5px
;
background
:
transparent
}
.storage
small
{
font-size
:
11px
}
.mobile-only
{
display
:
inline
;
font-weight
:
400
}
.view-path
{
flex-direction
:
column
;
gap
:
7px
;
padding
:
16px
;
font-size
:
13px
;
align-items
:
flex-start
}
.view-path
b
{
transform
:
rotate
(
90deg
);
margin-left
:
7px
;
font-size
:
17px
;
line-height
:
1.1
}
.duties
li
{
padding-left
:
33px
}
.duties
li
::before
{
font-size
:
12px
;
top
:
20px
}
.future
{
font-size
:
13px
;
padding
:
15px
17px
}
.sources
{
margin-top
:
32px
;
font-size
:
11.5px
}
.sources
h2
{
font-size
:
13px
}
.page-footer
{
font-size
:
10px
;
flex-direction
:
column
;
gap
:
3px
;
padding
:
15px
4px
0
}
.back-top
{
right
:
12px
;
bottom
:
12px
;
font-size
:
11px
;
padding
:
6px
11px
}}
@media
(
max-width
:
350px
){
.wrap
{
padding-inline
:
10px
}
main
{
padding-inline
:
17px
}
h1
{
font-size
:
29px
}
.eyebrow
{
font-size
:
9px
}
.boundary
{
font-size
:
16px
}
.chapter
h2
{
font-size
:
20px
}
.flow
{
padding-inline
:
12px
}}
@media
(
prefers-reduced-motion
:
reduce
){
html
{
scroll-behavior
:
auto
}
*
{
transition
:
none
!important
}}
@page
{
size
:
A4
;
margin
:
16mm
17mm
17mm
}
@media
print
{
html
{
scroll-behavior
:
auto
}
:root
,
:root
[
data-theme
=
dark
]
{
color-scheme
:
light
;
--ground
:
#fff
;
--surface
:
#fff
;
--sunken
:
#edf0f6
;
--ink
:
#0f1b33
;
--ink-2
:
#33415a
;
--ink-3
:
#69758a
;
--brand
:
#0032a0
;
--brand-soft
:
#edf1fa
;
--rule
:
#d5dce9
;
--note
:
#f8f9fc
;
--warn-bg
:
#fffaf0
;
--warn-rule
:
#c1a26b
}
body
{
background
:
white
;
font-size
:
10pt
;
line-height
:
1.75
;
-webkit-print-color-adjust
:
exact
;
print-color-adjust
:
exact
}
.wrap
{
padding
:
0
;
max-width
:
none
}
.document-header
{
padding
:
0
0
17pt
}
.topline
{
margin-bottom
:
9pt
}
.eyebrow
{
font-size
:
8pt
}
h1
{
font-size
:
25pt
;
margin-bottom
:
8pt
}
.meta
{
margin-bottom
:
7pt
;
font-size
:
8pt
}
.meta
time
{
font-size
:
8pt
}
.scope
{
font-size
:
9pt
}
.tools
,
.back-top
,
.skip-link
{
display
:
none
!important
}
main
{
padding
:
0
;
border-radius
:
0
}
.boundary
{
font-size
:
11.5pt
;
margin-bottom
:
13pt
;
padding-left
:
12pt
}
.principle
{
font-size
:
9.5pt
;
margin-bottom
:
17pt
}
.contents
{
margin-top
:
17pt
;
padding
:
10pt
0
}
.contents-links
{
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
4pt
10pt
}
.contents
a
{
font-size
:
8pt
}
.contents-label
{
font-size
:
7pt
;
margin-bottom
:
5pt
}
.chapter
{
padding-top
:
24pt
}
.chapter
+
.chapter
{
margin-top
:
23pt
}
.chapter-head
{
gap
:
10pt
;
margin-bottom
:
13pt
;
break-after
:
avoid
}
.chapter
h2
{
font-size
:
17pt
;
line-height
:
1.45
}
.chapter-num
{
font-size
:
10pt
}
.chapter
h3
{
font-size
:
11.5pt
;
margin
:
19pt
0
9pt
;
break-after
:
avoid
}
.chapter-head
+
h3
{
margin-top
:
0
}
p
{
margin-bottom
:
10pt
;
orphans
:
3
;
widows
:
3
}
li
{
margin
:
4pt
0
}
ul
,
ol
{
margin
:
9pt
0
13pt
}
blockquote
{
margin
:
13pt
0
;
padding
:
12pt
16pt
;
break-inside
:
avoid
}
blockquote
p
{
font-size
:
12pt
}
.consequence
{
padding
:
10pt
14pt
;
margin
:
12pt
0
;
break-inside
:
avoid
}
.figure-intro
{
font-size
:
9pt
;
break-after
:
avoid
;
margin-bottom
:
7pt
}
.flow
{
margin
:
8pt
0
13pt
;
padding
:
12pt
14pt
;
break-inside
:
avoid
}
.flow-grid
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
85px
minmax
(
0
,
1.1
fr
)
55px
minmax
(
0
,
1
fr
)}
figcaption
{
font-size
:
8pt
;
padding-bottom
:
12pt
}
figcaption
>
span
{
font-size
:
7pt
}
figcaption
>
small
{
font-size
:
7pt
}
.capture
{
font-size
:
7pt
}
.system
{
padding
:
10pt
5pt
}
.system-label
{
font-size
:
6.5pt
}
.system
>
strong
{
font-size
:
12pt
}
.system
>
small
{
font-size
:
6.5pt
}
.connector
>
span
{
font-size
:
6.5pt
}
.connector
small
{
font-size
:
5.5pt
}
.storage
{
font-size
:
8pt
}
.storage
small
{
font-size
:
7pt
}
.view-path
{
font-size
:
8.5pt
;
padding
:
12pt
10pt
;
break-inside
:
avoid
;
margin
:
11pt
0
}
.table-wrap
{
margin
:
13pt
0
16pt
;
break-inside
:
avoid
}
table
{
font-size
:
8.5pt
;
line-height
:
1.65
}
th
{
padding
:
8pt
10pt
}
td
{
padding
:
8pt
10pt
}
tr
{
break-inside
:
avoid
}
.duties
{
break-inside
:
avoid
}
.duties
li
{
padding
:
9pt
0
9pt
29pt
}
.duties
li
::before
{
font-size
:
9pt
;
top
:
13pt
}
.future
{
font-size
:
8.5pt
;
break-inside
:
avoid
;
padding
:
12pt
15pt
;
margin-top
:
17pt
}
.own-business
{
padding-top
:
12pt
;
margin-top
:
15pt
}
.sources
{
font-size
:
8pt
;
line-height
:
1.75
;
margin-top
:
25pt
;
padding-top
:
17pt
}
.sources
h2
{
font-size
:
9.5pt
;
break-after
:
avoid
}
.sources
li
{
margin
:
6pt
0
}
.sources
p
{
margin-bottom
:
9pt
}
.page-footer
{
font-size
:
7pt
;
padding-top
:
15pt
}
a
{
color
:
inherit
;
text-decoration
:
none
}
.sources
a
{
text-decoration
:
underline
;
text-decoration-color
:
#b9c3d8
}
.questions
{
break-inside
:
avoid
;
break-after
:
avoid
}
.review-conclusion
{
break-before
:
avoid
}
.table-wrap
:has
(+
p
)
{
break-after
:
auto
}
.keep-next
{
break-after
:
avoid
}}
</style></head>
<body>
<aside
role=
"note"
style=
"padding:12px 20px;background:#fff4da;color:#594100;border-bottom:1px solid #e9cf8d;font:14px/1.7 sans-serif"
>
历史评审稿,2026-10-22 归档。事件推送方案未被采用;现行实现按诊所分页轮询并批量查询分析。
<a
href=
"../../../apps/pac-service/src/integrations/meyer/README.md"
>
查看当前采集说明
</a></aside><a
class=
"skip-link"
href=
"#main"
>
跳到正文
</a><div
class=
"wrap"
id=
"top"
>
<header
class=
"document-header"
><div
class=
"topline"
><div
class=
"eyebrow"
>
美亚 × FridaySaaS × PAC
</div><div
class=
"tools"
aria-label=
"文档工具"
><button
type=
"button"
id=
"theme-button"
aria-label=
"切换深浅色"
aria-pressed=
"false"
title=
"切换深浅色"
><svg
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
aria-hidden=
"true"
><circle
cx=
"12"
cy=
"12"
r=
"8"
/><path
d=
"M12 4a8 8 0 0 1 0 16Z"
fill=
"currentColor"
stroke=
"none"
/></svg><span>
深浅色
</span></button><button
type=
"button"
id=
"print-button"
aria-label=
"打印或另存为 PDF"
title=
"打印或另存为 PDF"
><svg
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.5"
aria-hidden=
"true"
><path
d=
"M6 9V3h12v6M6 18H3v-8h18v8h-3M6 14h12v7H6Z"
/></svg><span>
打印
</span></button></div></div><h1><span>
影像与分析数据
</span><span>
方案设计
</span></h1><div
class=
"meta"
><span
class=
"version"
>
内部评审稿
</span><span>
v0.2
</span><span
aria-hidden=
"true"
>
·
</span><time
datetime=
"2026-10-16"
>
2026-10-16
</time></div><p
class=
"scope"
>
本轮先确认设计方向与系统边界;字段、表结构、调度周期和开发任务在方案采纳后细化。
</p></header>
<main
id=
"main"
><div
class=
"opening"
><p
class=
"boundary"
><strong>
建议边界:美亚管理影像资产,FridaySaaS 管理业务归属并负责对外同步,PAC 消费宿主提供的事实。PAC 核心不直接接美亚。
</strong></p>
<p
class=
"principle"
>
判断依据是:
<strong>
三个系统可以保存和使用同一份患者与影像关系,但应明确各自数据的权威来源,并让关联及其变更沿统一链路传递。
</strong>
FridaySaaS 提供业务患者身份,美亚维护影像资产及其患者映射;SaaS 将两者对应关系校验、留存后提供给 PAC,PAC 据此形成事实和诊断信号。
</p></div><nav
class=
"contents"
id=
"contents"
aria-label=
"文档目录"
><span
class=
"contents-label"
>
目录
</span><div
class=
"contents-links"
><a
href=
"#relationship"
><span>
01
</span>
整体关系
</a><a
href=
"#meyer"
><span>
02
</span>
美亚
</a><a
href=
"#saas"
><span>
03
</span>
FridaySaaS
</a><a
href=
"#pac"
><span>
04
</span>
PAC
</a><a
href=
"#review"
><span>
05
</span>
评审与确认
</a><a
href=
"#sources"
><span>
附
</span>
资料依据
</a></div></nav>
<section
class=
"chapter"
id=
"relationship"
aria-labelledby=
"heading-1"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
01
</span><h2
id=
"heading-1"
>
整体关系:数据供给与查看原片分开
</h2></div><h3
class=
"step-heading"
><span>
①
</span>
关联基础:先明确影像属于谁
</h3>
<p>
FridaySaaS 向美亚同步患者、诊所和就诊信息,美亚据此关联影像;SaaS 校验并留存对应关系,再向 PAC 提供。
</p>
<h3
class=
"step-heading"
><span>
②
</span>
数据供给:同步影像信息与分析结果
</h3>
<p
class=
"figure-intro"
>
以下以启用美亚新影像为例,表示目标链路:
</p>
<figure
class=
"flow"
aria-labelledby=
"flow-caption"
>
<figcaption
id=
"flow-caption"
><span>
图 01
</span>
影像与分析的数据供给
<small>
目标链路 · 新影像启用时
</small></figcaption>
<div
class=
"flow-grid"
>
<div
class=
"capture"
>
诊所设备与前置机
<span
aria-hidden=
"true"
>
↓
</span></div>
<div
class=
"system meyer"
><span
class=
"system-label"
>
影像资产
</span><strong>
美亚 PACS
</strong><small>
采集接入 · 影像与分析
</small></div>
<div
class=
"connector first"
><span>
影像信息
<br>
分析结果及变更
</span><svg
viewBox=
"0 0 40 12"
fill=
"none"
aria-hidden=
"true"
><path
d=
"M1 6h36m-5-4 5 4-5 4"
stroke=
"currentColor"
stroke-width=
"1.3"
></path></svg><small>
获取方式待确认
</small></div>
<div
class=
"system saas"
><span
class=
"system-label"
>
业务归属
</span><strong>
FridaySaaS
</strong><small>
校验归属 · 留存 · 同步
</small></div>
<div
class=
"connector second"
><span>
宿主事实
</span><svg
viewBox=
"0 0 40 12"
fill=
"none"
aria-hidden=
"true"
><path
d=
"M1 6h36m-5-4 5 4-5 4"
stroke=
"currentColor"
stroke-width=
"1.3"
></path></svg></div>
<div
class=
"system pac"
><span
class=
"system-label"
>
信号应用
</span><strong>
PAC
</strong><small>
诊断信号 · 画像与召回
</small></div>
<div
class=
"storage"
><span
aria-hidden=
"true"
>
↓
</span><div><b
class=
"mobile-only"
>
美亚 →
</b>
OSS
<small>
原片存储
</small></div></div>
</div>
</figure>
<p>
原片由美亚管理,SaaS 向 PAC 提供影像业务信息与分析结果。美亚到 SaaS 如何获取变更,仍需确认。
</p>
<p>
新老影像功能可以保留,实际使用时择一启用;选用旧影像时,也由 SaaS 统一向 PAC 提供数据。
</p>
<h3
class=
"step-heading"
><span>
③
</span>
查看原片:用户按需打开宿主入口
</h3>
<div
class=
"view-path"
role=
"group"
aria-label=
"查看原片路径"
><span>
PAC 宿主槽位
</span><b
aria-hidden=
"true"
>
→
</b><span>
FridaySaaS 影像页面
</span><b
aria-hidden=
"true"
>
→
</b><span>
当前选用的影像系统
</span></div>
<p>
SaaS 负责权限和入口;启用新影像时,由美亚提供阅片服务。
<strong>
数据同步持续进行,查看原片由用户按需发起。
</strong></p>
</section>
<section
class=
"chapter"
id=
"meyer"
aria-labelledby=
"heading-2"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
02
</span><h2
id=
"heading-2"
>
美亚:最重要的承诺是“变化可获取”
</h2></div><p>
美亚负责采集接入、原片存储、影像关联、阅片,以及其提供的分析。对外不仅要能查询当前数据,还要让 SaaS 知道数据发生了什么变化。
</p>
<p
class=
"consequence"
><strong>
如果只能查新影像列表,SaaS 可以知道“多了一张片”,却未必知道这张片后来分析完成、被改绑到另一位患者,或被撤销。下游就可能继续使用过期甚至归属错误的依据。
</strong></p>
<p>
因此,对美亚真正要提的要求是:
</p>
<blockquote>
<p>
提供可追溯、可补拉的影像及分析变更机制,包含新增、修改、撤销和归属变化。
</p>
</blockquote>
<div
class=
"table-wrap"
><table
class=
"three-col"
>
<thead>
<tr>
<th
scope=
"col"
>
美亚提供的能力
</th>
<th
scope=
"col"
>
FridaySaaS 如何获取
</th>
<th
scope=
"col"
>
意味着什么
</th>
</tr>
</thead>
<tbody><tr>
<td
data-label=
"美亚提供的能力"
>
有事件通知,并能补查变更
</td>
<td
data-label=
"FridaySaaS 如何获取"
>
通知后同步,遗漏时补拉
</td>
<td
data-label=
"意味着什么"
>
及时性较好,也能恢复遗漏
</td>
</tr>
<tr>
<td
data-label=
"美亚提供的能力"
>
没有通知,有完整变更查询
</td>
<td
data-label=
"FridaySaaS 如何获取"
>
定时拉取
</td>
<td
data-label=
"意味着什么"
>
可以可靠同步,延迟取决于拉取周期
</td>
</tr>
<tr>
<td
data-label=
"美亚提供的能力"
>
只有入库时间列表、逐张查询
</td>
<td
data-label=
"FridaySaaS 如何获取"
>
定时发现新片,反复查询分析
</td>
<td
data-label=
"意味着什么"
>
能做部分同步,但后续修订、改绑、撤销的完整性没有保障
</td>
</tr>
</tbody></table></div>
</section>
<section
class=
"chapter"
id=
"saas"
aria-labelledby=
"heading-3"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
03
</span><h2
id=
"heading-3"
>
FridaySaaS:留业务记录,统一新老来源
</h2></div><h3>
3.1 为什么需要留新影像数据
</h3>
<p><strong>
如果 SaaS 只在用户打开页面时代理查询美亚,它承担的是展示功能,无法承担稳定的下游数据供给。
</strong>
没人打开页面时,PAC 仍需要知道患者有了新的影像或分析。
</p>
<p
class=
"keep-next"
>
既然 PAC 从 SaaS 接收数据,SaaS 就应保存一份能够重试、重推、核对的影像业务记录:
</p>
<div
class=
"table-wrap"
><table
class=
"two-col"
>
<thead>
<tr>
<th
scope=
"col"
>
应保存的信息
</th>
<th
scope=
"col"
>
用途
</th>
</tr>
</thead>
<tbody><tr>
<td
data-label=
"应保存的信息"
>
患者、租户与影像采集诊所
</td>
<td
data-label=
"用途"
>
明确业务归属;采集诊所不随患者后续转店而改变
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
影像来源、影像与检查标识
</td>
<td
data-label=
"用途"
>
稳定识别、去重和追溯
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
类型、拍摄时间、绑定及有效状态
</td>
<td
data-label=
"用途"
>
解释这份影像的业务含义
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
稳定文件引用,如 OSS ID
</td>
<td
data-label=
"用途"
>
后续获取原片;临时链接过期后仍可定位
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
分析结果、来源及版本
</td>
<td
data-label=
"用途"
>
保留当时依据,支持补推和重放
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
同步进度与失败记录
</td>
<td
data-label=
"用途"
>
知道哪些已交付、哪些还需要补齐
</td>
</tr>
</tbody></table></div>
<p>
原片继续由美亚管理。SaaS 保存索引和结果,不重复建设影像存储;临时签名 URL 不能作为永久地址。
</p>
<p><strong>
这样即使美亚暂时不可用,SaaS 和 PAC 仍可查询已留存的影像信息和分析结果;原片访问和最新变更同步则可能受影响。
</strong>
分析结果会影响后续业务判断,值得留存,而不应仅在查看页面时临时取一次。
</p>
<p>
美亚接入、获取变更和归属校验由 SaaS 对接层负责;open-connector 从 SaaS 已保存的业务记录向 PAC 推送。供应商协议与下游数据传输各自负责自己的部分。
</p>
<p
class=
"own-business"
>
即使不接 PAC,留存业务记录也能支撑 SaaS 自身业务:将影像与就诊、病历、收费关联,用于检查进度跟进、复诊查阅、经营分析,以及追溯当时使用的分析结果。如果不留存,这些业务就需要反复查询美亚并临时关联数据,受其接口可用性和查询能力限制;分析结果更新或撤销后,若美亚不提供历史版本,SaaS 就难以还原当时使用的依据。
</p>
<h3>
3.2 新老影像怎么归一
</h3>
<p
class=
"keep-next"
><strong>
业务语义归一,来源实现分开。
</strong>
新老功能可以保留,实际择一使用;对下游都应能回答同一组问题:
</p>
<ul>
<li>
影像属于谁、在哪里拍摄,是什么类型、什么时候拍摄;
</li>
<li>
当前是否有效,怎样查看,有哪些分析结果。
</li>
</ul>
<p><strong>
如果下游直接依赖“老影像的一套字段”和“美亚的另一套字段”,每切换一次影像系统,下游都要跟着改。
</strong>
统一表达后,来源切换主要由 SaaS 处理,PAC 继续按宿主事实消费。
</p>
<p>
但统一不等于强行写入旧表。美亚有检查、切片、绑定状态和外部文件标识;如果旧结构表达不了,不能为了兼容丢掉它们,也不能把临时链接塞进旧文件地址就算完成。是否复用旧表,是方案采纳后的存储设计选择。
</p>
<p>
如果未来把历史影像迁入美亚,应保留新旧标识的对应关系。
<strong>
同一张片换了存储位置,不应被当成一次新的拍摄或重复的分析依据。
</strong></p>
</section>
<section
class=
"chapter"
id=
"pac"
aria-labelledby=
"heading-4"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
04
</span><h2
id=
"heading-4"
>
PAC:用分析形成诊断信号,通过宿主查看影像
</h2></div><p
class=
"keep-next"
>
PAC 在这条链路中的职责保持两项:
</p>
<ol
class=
"duties"
>
<li><strong>
消费影像分析。
</strong>
将宿主提供的有效分析结果作为诊断信号源之一,与其他患者事实共同用于画像与召回,并保留来源依据。
</li>
<li><strong>
提供影像查看入口。
</strong>
沿用宿主槽位打开 FridaySaaS,由宿主处理新老影像入口、权限和阅片授权。
</li>
</ol>
<p>
美亚生成的分析,由 SaaS 接收、留底后推给 PAC。
<strong>
如果 PAC 每次处理事实都临时向美亚取结果,重放同一份数据时可能拿到不同内容,就无法解释当时为什么得出那个判断。
</strong></p>
<p>
PAC 可以知道数据来源是美亚,但美亚不应成为 PAC 核心的直接依赖。
<strong>
否则接入下一家影像供应商时,PAC 又要接一套鉴权、患者映射和同步机制,还可能与 SaaS 的权限及归属规则形成两套判断。
</strong>
这些差异应由宿主消化。
</p>
<aside
class=
"future"
aria-label=
"后续能力边界"
><p
class=
"keep-next"
>
如果将来是 PAC 自己发起分析,那是另一项能力:
</p><ul>
<li>
PAC 通过宿主获得授权的文件;
</li>
<li>
PAC 调用自己选择的分析服务;
</li>
<li>
结果标明由 PAC 发起,保留实际分析服务来源及结果版本,并关联原始影像。
</li>
</ul></aside>
</section>
<section
class=
"chapter"
id=
"review"
aria-labelledby=
"heading-5"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
05
</span><h2
id=
"heading-5"
>
评审要确定什么,哪些仍需外部确认
</h2></div><p
class=
"keep-next"
>
本轮评审确认的是职责和数据留存方向:
</p>
<div
class=
"table-wrap"
><table
class=
"two-col"
>
<thead>
<tr>
<th
scope=
"col"
>
系统
</th>
<th
scope=
"col"
>
对下游承担的承诺
</th>
</tr>
</thead>
<tbody><tr>
<td
data-label=
"系统"
>
美亚
</td>
<td
data-label=
"对下游承担的承诺"
>
影像与分析可查询,变更可获取,原片可授权访问
</td>
</tr>
<tr>
<td
data-label=
"系统"
>
FridaySaaS
</td>
<td
data-label=
"对下游承担的承诺"
>
患者归属正确,新老影像统一表达,业务数据可留底、补推和核对
</td>
</tr>
<tr>
<td
data-label=
"系统"
>
PAC
</td>
<td
data-label=
"对下游承担的承诺"
>
消费宿主分析作为诊断信号,通过宿主入口查看影像
</td>
</tr>
</tbody></table></div>
<p
class=
"keep-next"
>
方案采纳前,优先向美亚确认两件事:
</p>
<ul
class=
"questions"
>
<li><strong>
变更怎样获取
</strong>
:有无通知;没有通知时,能否完整拉到新增、修订、撤销和归属变化,并补查遗漏。
</li>
<li><strong>
分析怎样产生和识别
</strong>
:何时触发,怎样区分未分析、处理中、失败、完成零发现,以及结果后来是否修改。
</li>
</ul>
<p
class=
"review-conclusion"
>
这两项决定后续采用通知还是轮询,以及分析能否可靠成为诊断信号。具体字段、存储结构、调度周期和任务拆分,在方向采纳、能力确认后再制定。
</p>
</section>
<aside
class=
"sources"
id=
"sources"
aria-labelledby=
"sources-title"
><h2
id=
"sources-title"
>
依据与核对范围
</h2><p
class=
"keep-next"
>
本稿依据 2026-10-16 已核对的本地代码、SDK 和以下资料;未验证美亚实际部署能力。接口细节存在版本差异,正式对接仍需双方确认。
</p>
<ul>
<li><span
title=
"本机参考文件,未随归档分发"
>
美亚系统集成方案
</span>
:第 2–3 章职责及信息同步、第 4–5 章设备与前置机、第 6–7 章阅片与患者映射。
</li>
<li><span
title=
"本机参考文件,未随归档分发"
>
美亚私有化部署方案
</span>
:明确规划阿里云 OSS 和算法资源;规划不代表当前能力已启用。
</li>
<li><span
title=
"本机参考文件,未随归档分发"
>
美亚接口文档
</span>
:§6.3 分析查询及空结果含义、§6.4/6.8 文件引用与临时 URL。
</li>
<li>
SaaS 代码:
<span
title=
"本机参考文件,未随归档分发"
>
PACS 接口
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
同步与查询
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
新版影像中心
</span>
。
</li>
<li>
PAC 代码:
<span
title=
"本机参考文件,未随归档分发"
>
影像分析事实
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
宿主影像入口
</span>
。
</li>
</ul>
<p>
资料链接指向本机原件;内部分享时可随报告提供原 PDF。
</p>
</aside></main>
<footer
class=
"page-footer"
><span>
美亚 × FridaySaaS × PAC · 影像与分析数据方案设计
</span><a
href=
"#top"
>
内部评审稿 v0.2 · 返回页首 ↑
</a></footer></div><a
class=
"back-top"
href=
"#contents"
aria-label=
"返回文档目录"
>
目录 ↑
</a>
<script>
(()
=>
{
const
root
=
document
.
documentElement
,
themeButton
=
document
.
getElementById
(
'theme-button'
),
key
=
'imaging-document-theme'
;
const
scheme
=
matchMedia
(
'(prefers-color-scheme: dark)'
);
const
isDark
=
()
=>
root
.
dataset
.
theme
?
root
.
dataset
.
theme
===
'dark'
:
scheme
.
matches
;
const
update
=
()
=>
themeButton
.
setAttribute
(
'aria-pressed'
,
String
(
isDark
()));
try
{
const
saved
=
localStorage
.
getItem
(
key
);
if
(
saved
===
'dark'
||
saved
===
'light'
)
root
.
dataset
.
theme
=
saved
;}
catch
{}
update
();
scheme
.
addEventListener
(
'change'
,
update
);
themeButton
.
addEventListener
(
'click'
,()
=>
{
const
theme
=
isDark
()?
'light'
:
'dark'
;
root
.
dataset
.
theme
=
theme
;
try
{
localStorage
.
setItem
(
key
,
theme
)}
catch
{}
update
()});
document
.
getElementById
(
'print-button'
).
addEventListener
(
'click'
,()
=>
window
.
print
());
const
back
=
document
.
querySelector
(
'.back-top'
);
const
check
=
()
=>
{
const
visible
=
scrollY
>
700
;
back
.
classList
.
toggle
(
'visible'
,
visible
);
back
.
tabIndex
=
visible
?
0
:
-
1
;
back
.
setAttribute
(
'aria-hidden'
,
String
(
!
visible
))};
addEventListener
(
'scroll'
,
check
,{
passive
:
true
});
check
();
})();
</script></body></html>
docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review.html
0 → 100644
View file @
a9248e61
<!doctype html>
<html
lang=
"zh-CN"
>
<head>
<meta
charset=
"utf-8"
><meta
name=
"viewport"
content=
"width=device-width,initial-scale=1"
><meta
name=
"color-scheme"
content=
"light dark"
>
<title>
影像与分析数据方案设计 · 美亚 × FridaySaaS × PAC
</title>
<meta
name=
"description"
content=
"影像资产、业务归属与诊断信号的三方协作方案。明确美亚、FridaySaaS 与 PAC 的数据流、留存责任与评审事项。"
>
<script>
try
{
let
t
=
localStorage
.
getItem
(
'imaging-report-theme'
);
if
(
t
===
'dark'
||
t
===
'light'
)
document
.
documentElement
.
dataset
.
theme
=
t
;
let
f
=
localStorage
.
getItem
(
'imaging-report-size'
);
if
(
f
===
'large'
)
document
.
documentElement
.
dataset
.
size
=
f
}
catch
{}
</script>
<style>
:root
{
--bg
:
#f2f1ec
;
--paper
:
#fffefb
;
--ink
:
#22312f
;
--heading
:
#172e2b
;
--muted
:
#5c6b66
;
--subtle
:
#6e7b75
;
--line
:
#dedfd6
;
--soft
:
#f5f5ef
;
--teal
:
#17665b
;
--teal-soft
:
#eaf3ed
;
--teal-line
:
#bad3c4
;
--amber
:
#80591f
;
--amber-soft
:
#faf3e6
;
--amber-line
:
#e1cfaa
;
--blue
:
#355d80
;
--blue-soft
:
#eef3f7
;
--shadow
:
0
12px
50px
#243c2b06
;
--font
:
-apple-system
,
BlinkMacSystemFont
,
"PingFang SC"
,
"Hiragino Sans GB"
,
"Microsoft YaHei"
,
sans-serif
;
--serif
:
"Songti SC"
,
"Noto Serif CJK SC"
,
"STSong"
,
"SimSun"
,
serif
;
color-scheme
:
light
}
@media
(
prefers-color-scheme
:
dark
){
:root:not
([
data-theme
=
"light"
])
{
--bg
:
#151c1b
;
--paper
:
#1c2623
;
--ink
:
#d6ded6
;
--heading
:
#edf3e9
;
--muted
:
#a4b3ab
;
--subtle
:
#96a79e
;
--line
:
#36433c
;
--soft
:
#222e28
;
--teal
:
#96cbb6
;
--teal-soft
:
#243b31
;
--teal-line
:
#466b57
;
--amber
:
#dec08a
;
--amber-soft
:
#382f21
;
--amber-line
:
#645235
;
--blue
:
#a6c4e0
;
--blue-soft
:
#253340
;
--shadow
:
none
;
color-scheme
:
dark
}}
:root
[
data-theme
=
"dark"
]
{
--bg
:
#151c1b
;
--paper
:
#1c2623
;
--ink
:
#d6ded6
;
--heading
:
#edf3e9
;
--muted
:
#a4b3ab
;
--subtle
:
#96a79e
;
--line
:
#36433c
;
--soft
:
#222e28
;
--teal
:
#96cbb6
;
--teal-soft
:
#243b31
;
--teal-line
:
#466b57
;
--amber
:
#dec08a
;
--amber-soft
:
#382f21
;
--amber-line
:
#645235
;
--blue
:
#a6c4e0
;
--blue-soft
:
#253340
;
--shadow
:
none
;
color-scheme
:
dark
}
*
{
box-sizing
:
border-box
}
html
{
scroll-behavior
:
smooth
;
scroll-padding-top
:
100px
}
body
{
margin
:
0
;
background
:
var
(
--bg
);
color
:
var
(
--ink
);
font-family
:
var
(
--font
);
font-size
:
16px
;
line-height
:
1.95
;
-webkit-font-smoothing
:
antialiased
}
a
{
color
:
var
(
--teal
);
text-decoration
:
none
}
button
,
a
,
summary
{
-webkit-tap-highlight-color
:
transparent
}
button
{
font
:
inherit
;
cursor
:
pointer
}
a
:focus-visible
,
button
:focus-visible
,
summary
:focus-visible
{
outline
:
2px
solid
var
(
--teal
);
outline-offset
:
5px
}
button
:hover
{
background
:
var
(
--soft
)}
a
:hover
{
text-decoration
:
underline
;
text-underline-offset
:
4px
}
::selection
{
background
:
var
(
--teal-line
);
color
:
var
(
--heading
)}
h1
,
h2
,
h3
,
p
,
figure
,
blockquote
{
margin
:
0
}
strong
{
font-weight
:
650
;
color
:
var
(
--heading
)}
h1
,
h2
,
h3
{
color
:
var
(
--heading
);
text-wrap
:
pretty
}
h1
{
font-family
:
var
(
--serif
);
font-size
:
52px
;
line-height
:
1.28
;
letter-spacing
:
-.025em
;
font-weight
:
700
}
h2
{
font-size
:
27px
;
line-height
:
1.6
;
font-weight
:
650
;
letter-spacing
:
-.025em
}
h3
{
font-size
:
20px
;
line-height
:
1.65
;
font-weight
:
600
;
margin
:
30px
0
17px
}
h3
:first-child
{
margin-top
:
0
}
.icon
{
width
:
20px
;
height
:
20px
;
flex
:
none
;
vertical-align
:
middle
}
.skip-link
{
position
:
fixed
;
left
:
20px
;
top
:
-70px
;
background
:
var
(
--paper
);
z-index
:
99
;
padding
:
10px
18px
;
border
:
1px
solid
var
(
--teal
)}
.skip-link
:focus
{
top
:
12px
}
.page-progress
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100%
;
height
:
3px
;
z-index
:
100
;
transform
:
scaleX
(
0
);
transform-origin
:
left
;
background
:
var
(
--teal
)}
.topbar
{
height
:
70px
;
position
:
sticky
;
top
:
0
;
z-index
:
30
;
background
:
var
(
--bg
);
border-bottom
:
1px
solid
var
(
--line
)}
.topbar-inner
{
max-width
:
1300px
;
height
:
100%
;
padding
:
0
38px
;
margin
:
auto
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
20px
}
.doc-mark
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
color
:
var
(
--heading
);
font-size
:
13px
;
font-weight
:
650
;
letter-spacing
:
.025em
}
.mark-icon
{
display
:
grid
;
place-items
:
center
;
width
:
30px
;
height
:
30px
;
color
:
var
(
--teal
);
border
:
1px
solid
var
(
--teal-line
);
border-radius
:
7px
}
.toolbar
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
.tool
{
border
:
0
;
background
:
transparent
;
color
:
var
(
--muted
);
border-radius
:
6px
;
min-width
:
36px
;
min-height
:
36px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
7px
;
font-size
:
12px
;
padding
:
7px
9px
}
.tool
[
data-active
=
"true"
]
{
background
:
var
(
--teal-soft
);
color
:
var
(
--teal
)}
.print-button
{
border
:
1px
solid
var
(
--line
);
margin-left
:
5px
}
.version
{
font-size
:
11px
;
font-variant-numeric
:
tabular-nums
;
color
:
var
(
--subtle
);
margin-right
:
17px
;
letter-spacing
:
.04em
}
.size-icon
{
font-family
:
Georgia
,
serif
;
font-size
:
17px
}
.shell
{
max-width
:
1300px
;
margin
:
0
auto
;
padding
:
42px
38px
70px
;
display
:
grid
;
grid-template-columns
:
206px
minmax
(
0
,
1
fr
);
gap
:
40px
;
align-items
:
start
}
.sidebar
{
position
:
sticky
;
top
:
109px
;
padding
:
19px
0
;
max-height
:
calc
(
100vh
-
135px
);
overflow
:
auto
;
scrollbar-width
:
thin
}
.toc-label
{
font-size
:
11px
;
color
:
var
(
--subtle
);
letter-spacing
:
.15em
;
margin
:
0
0
20px
12px
}
.toc
{
display
:
grid
;
gap
:
5px
}
.toc
a
{
display
:
flex
;
gap
:
12px
;
padding
:
12px
12px
;
border-left
:
2px
solid
transparent
;
border-radius
:
0
6px
6px
0
;
font-size
:
13px
;
line-height
:
1.5
;
color
:
var
(
--muted
);
text-decoration
:
none
}
.toc
small
{
font-size
:
11px
;
font-weight
:
400
;
display
:
block
;
margin-top
:
4px
;
color
:
var
(
--subtle
)}
.toc-number
{
font-size
:
10px
;
font-variant-numeric
:
tabular-nums
;
padding-top
:
3px
;
color
:
var
(
--subtle
);
min-width
:
17px
}
.toc
a
.active
{
background
:
var
(
--teal-soft
);
color
:
var
(
--teal
);
border-color
:
var
(
--teal
);
font-weight
:
650
}
.toc
a
:hover
{
background
:
var
(
--soft
)}
.rail-note
{
border-top
:
1px
solid
var
(
--line
);
margin
:
27px
12px
0
;
padding-top
:
19px
;
color
:
var
(
--subtle
);
font-size
:
11px
;
line-height
:
1.9
}
.rail-note
b
{
display
:
block
;
font-weight
:
550
;
color
:
var
(
--muted
);
margin-bottom
:
5px
}
.reading-status
{
display
:
flex
;
justify-content
:
space-between
;
margin-top
:
20px
;
font-variant-numeric
:
tabular-nums
}
.reading-track
{
height
:
2px
;
background
:
var
(
--line
);
margin-top
:
7px
;
overflow
:
hidden
}
.reading-track
span
{
display
:
block
;
height
:
100%
;
width
:
0
;
background
:
var
(
--teal
)}
.document
{
min-width
:
0
;
background
:
var
(
--paper
);
border
:
1px
solid
var
(
--line
);
box-shadow
:
var
(
--shadow
);
border-radius
:
4px
;
padding
:
60px
58px
26px
;
position
:
relative
}
.document
:before
{
content
:
""
;
position
:
absolute
;
top
:
-1px
;
left
:
58px
;
width
:
62px
;
height
:
3px
;
background
:
var
(
--teal
)}
.hero
{
padding-bottom
:
44px
}
.eyebrow
{
display
:
flex
;
align-items
:
center
;
gap
:
9px
;
font-size
:
12px
;
color
:
var
(
--teal
);
letter-spacing
:
.09em
;
margin-bottom
:
23px
}
.eyebrow
.sep
{
color
:
var
(
--line
)}
.hero
h1
span
{
display
:
block
}
.hero-deck
{
font-family
:
var
(
--serif
);
font-size
:
21px
;
color
:
var
(
--muted
);
margin-top
:
17px
;
letter-spacing
:
.02em
}
.meta
{
display
:
flex
;
gap
:
15px
;
align-items
:
center
;
flex-wrap
:
wrap
;
font-size
:
11px
;
color
:
var
(
--subtle
);
margin
:
24px
0
0
}
.meta
.draft
{
border
:
1px
solid
var
(
--teal-line
);
padding
:
2px
8px
;
border-radius
:
3px
;
color
:
var
(
--teal
);
background
:
var
(
--teal-soft
)}
.hero-scope
{
color
:
var
(
--muted
);
font-size
:
13px
;
line-height
:
1.9
;
max-width
:
650px
;
margin-top
:
15px
}
.boundary
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
margin-top
:
36px
;
padding
:
25px
0
;
border-top
:
1px
solid
var
(
--line
);
border-bottom
:
1px
solid
var
(
--line
);
gap
:
25px
}
.boundary
>
div
+
div
{
border-left
:
1px
solid
var
(
--line
);
padding-left
:
25px
}
.boundary
.role-name
{
font-size
:
13px
;
font-weight
:
650
;
color
:
var
(
--teal
)}
.boundary
h2
{
font-family
:
var
(
--serif
);
font-size
:
23px
;
margin-top
:
6px
;
font-weight
:
650
;
line-height
:
1.5
}
.boundary
p
{
font-size
:
12px
;
color
:
var
(
--muted
);
line-height
:
1.8
;
margin-top
:
5px
}
.core-boundary
{
font-size
:
12px
;
margin-top
:
13px
;
color
:
var
(
--teal
);
display
:
flex
;
gap
:
7px
;
align-items
:
center
}
.core-boundary
.icon
{
width
:
15px
;
height
:
15px
}
.principle
{
padding
:
24px
0
0
;
color
:
var
(
--muted
);
font-size
:
14px
;
line-height
:
2
}
.principle
strong
{
font-family
:
var
(
--serif
);
font-size
:
20px
;
line-height
:
1.85
;
font-weight
:
600
}
.principle
p
{
margin
:
0
}
.principle
strong
{
display
:
block
;
margin
:
8px
0
13px
}
.principle
.note-label
{
margin-bottom
:
0
}
.chapter
{
padding
:
40px
0
42px
;
border-top
:
1px
solid
var
(
--line
);
scroll-margin-top
:
0
}
.chapter-head
{
display
:
flex
;
gap
:
20px
;
align-items
:
flex-start
;
margin-bottom
:
26px
}
.chapter-num
{
font-family
:
Georgia
,
serif
;
font-size
:
42px
;
line-height
:
1.3
;
color
:
var
(
--teal-line
);
font-variant-numeric
:
tabular-nums
;
font-weight
:
400
;
min-width
:
47px
;
padding-top
:
10px
}
.chapter-kicker
{
font-size
:
10px
;
letter-spacing
:
.16em
;
line-height
:
1.6
;
color
:
var
(
--teal
);
margin-bottom
:
4px
}
.prose
p
{
margin
:
16px
0
}
.prose
>
p
:first-child
{
margin-top
:
0
}
.prose
p
:last-child
{
margin-bottom
:
0
}
.prose
ul
,
.prose
ol
{
padding-left
:
24px
;
margin
:
18px
0
}
.prose
li
{
padding-left
:
3px
;
margin
:
7px
0
}
.prose
li
::marker
{
color
:
var
(
--teal
)}
.prose
>
h3
:not
(
:first-child
)
{
padding-top
:
15px
;
margin-top
:
34px
;
border-top
:
1px
dashed
var
(
--line
)}
.step-heading
{
font-size
:
16px
;
font-weight
:
600
;
color
:
var
(
--teal
);
margin-top
:
28px
!important
}
.step-heading
strong
{
color
:
var
(
--teal
)}
.figure-intro
{
font-size
:
13px
;
color
:
var
(
--muted
);
margin-bottom
:
13px
!important
}
.emphasis
{
border-left
:
3px
solid
var
(
--teal
);
padding
:
8px
0
8px
19px
;
font-size
:
16px
}
.consequence
{
background
:
var
(
--amber-soft
);
border
:
1px
solid
var
(
--amber-line
);
border-radius
:
4px
;
padding
:
19px
22px
;
font-size
:
15px
;
line-height
:
1.95
}
.consequence
strong
{
font-weight
:
500
;
color
:
var
(
--ink
)}
.note-label
{
display
:
block
;
font-size
:
11px
;
font-weight
:
600
;
letter-spacing
:
.06em
;
line-height
:
1.5
;
margin-bottom
:
10px
;
color
:
var
(
--teal
)}
.consequence
.note-label
{
color
:
var
(
--amber
)}
.requirement
{
margin
:
20px
0
25px
;
padding
:
21px
25px
;
border-left
:
3px
solid
var
(
--teal
);
background
:
var
(
--teal-soft
);
font-family
:
var
(
--serif
);
font-size
:
22px
;
line-height
:
1.8
;
color
:
var
(
--heading
)}
.requirement
p
{
margin
:
0
}
.continuity
{
border-left
:
2px
solid
var
(
--teal-line
);
padding-left
:
19px
;
font-size
:
15px
}
.continuity
strong
{
font-weight
:
600
}
.own-business
{
background
:
var
(
--soft
);
padding
:
21px
23px
;
border-radius
:
4px
;
font-size
:
14px
;
color
:
var
(
--muted
);
line-height
:
2
}
.footnote-callout
{
border-top
:
1px
solid
var
(
--line
);
padding-top
:
17px
;
font-size
:
14px
;
color
:
var
(
--muted
)}
.flow-figure
{
margin
:
17px
0
23px
;
padding
:
22px
22px
24px
;
background-color
:
var
(
--soft
);
background-image
:
radial-gradient
(
var
(
--line
)
.7px
,
transparent
.7px
);
background-size
:
15px
15px
;
border
:
1px
solid
var
(
--line
);
border-radius
:
5px
}
.flow-figure
figcaption
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
flex-wrap
:
wrap
;
font-size
:
12px
;
line-height
:
1.7
;
margin-bottom
:
21px
}
.figure-number
{
font-size
:
9px
;
letter-spacing
:
.09em
;
color
:
var
(
--subtle
)}
.state-badge
{
font-size
:
10px
;
color
:
var
(
--muted
);
margin-left
:
auto
}
.flow-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
)
85px
minmax
(
0
,
1
fr
)
65px
minmax
(
0
,
1
fr
);
align-items
:
stretch
}
.system
{
background
:
var
(
--paper
);
border
:
1px
solid
var
(
--line
);
border-radius
:
5px
;
min-width
:
0
;
display
:
flex
;
flex-direction
:
column
;
justify-content
:
center
;
align-items
:
center
;
text-align
:
center
;
padding
:
19px
7px
;
min-height
:
125px
}
.system.saas
{
background
:
var
(
--teal-soft
);
border-color
:
var
(
--teal-line
)}
.system-kicker
{
font-size
:
10px
;
color
:
var
(
--subtle
);
letter-spacing
:
.08em
;
margin-bottom
:
5px
}
.system
strong
{
font-size
:
17px
;
color
:
var
(
--heading
);
font-weight
:
650
;
line-height
:
1.5
}
.system
p
{
font-size
:
10px
!important
;
line-height
:
1.8
;
color
:
var
(
--muted
);
margin
:
8px
0
0
!important
}
.connector
{
position
:
relative
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
text-align
:
center
;
color
:
var
(
--muted
);
gap
:
5px
;
padding
:
2px
5px
;
font-size
:
10px
;
line-height
:
1.65
}
.connector
.icon
{
height
:
20px
;
width
:
34px
;
color
:
var
(
--teal
)}
.connector
small
{
font-size
:
9px
;
color
:
var
(
--amber
)}
.flow-top
{
width
:
calc
((
100%
-
150px
)/
3
);
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
3px
;
margin-bottom
:
3px
}
.capture
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
font-size
:
10px
;
line-height
:
1.65
;
white-space
:
nowrap
;
color
:
var
(
--muted
)}
.capture
.icon
{
width
:
14px
;
height
:
14px
}
.capture-arrow
{
font-size
:
20px
;
color
:
var
(
--subtle
);
line-height
:
1.1
}
.flow-bottom
{
width
:
calc
((
100%
-
150px
)/
3
);
display
:
flex
;
justify-content
:
center
}
.storage
{
position
:
relative
;
padding-top
:
23px
;
display
:
flex
;
align-items
:
center
;
gap
:
7px
;
color
:
var
(
--muted
);
font-size
:
13px
}
.storage
.icon
{
width
:
16px
;
height
:
16px
}
.storage
small
{
font-size
:
10px
;
margin-left
:
4px
}
.storage-line
{
height
:
17px
;
border-left
:
1px
solid
var
(
--subtle
);
position
:
absolute
;
top
:
0
;
left
:
50%
}
.storage-line
:after
{
content
:
""
;
position
:
absolute
;
bottom
:
0
;
left
:
-3px
;
border
:
solid
var
(
--subtle
);
border-width
:
0
1px
1px
0
;
padding
:
2px
;
transform
:
rotate
(
45deg
)}
.view-path
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
7px
;
padding
:
18px
17px
;
background
:
var
(
--blue-soft
);
border-radius
:
4px
;
font-size
:
12px
;
font-weight
:
550
;
color
:
var
(
--blue
);
margin
:
16px
0
}
.view-path
>
span
{
display
:
flex
;
gap
:
7px
;
align-items
:
center
}
.view-path
>
.icon
{
width
:
17px
;
height
:
17px
;
opacity
:
.7
}
.view-path
>
span
>
.icon
{
width
:
17px
;
height
:
17px
}
.table-wrap
{
margin
:
23px
0
;
overflow
:
hidden
;
border
:
1px
solid
var
(
--line
);
border-radius
:
4px
}
table
{
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
14px
;
line-height
:
1.85
;
text-align
:
left
}
th
{
font-weight
:
600
;
font-size
:
12px
;
color
:
var
(
--muted
);
background
:
var
(
--soft
);
padding
:
12px
16px
;
border-bottom
:
1px
solid
var
(
--line
)}
td
{
padding
:
15px
16px
;
border-bottom
:
1px
solid
var
(
--line
);
vertical-align
:
top
}
tbody
tr
:last-child
td
{
border-bottom
:
0
}
td
:first-child
{
font-weight
:
550
;
color
:
var
(
--heading
);
width
:
37%
}
#meyer
td
:first-child
{
width
:
31%
}
#meyer
td
:nth-child
(
2
)
{
width
:
26%
}
#review
td
:first-child
{
width
:
23%
}
tbody
tr
:hover
{
background
:
var
(
--soft
)}
.responsibilities
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
20px
;
list-style
:
none
;
padding
:
0
!important
;
margin
:
22px
0
!important
}
.responsibilities
li
{
position
:
relative
;
margin
:
0
;
padding
:
22px
22px
20px
;
background
:
var
(
--teal-soft
);
border
:
1px
solid
var
(
--teal-line
);
border-radius
:
4px
;
font-size
:
14px
;
line-height
:
1.95
}
.responsibilities
li
>
strong
{
display
:
block
;
font-size
:
18px
;
margin
:
12px
0
5px
}
.responsibility-icon
{
display
:
inline-flex
;
color
:
var
(
--teal
)}
.responsibility-icon
.icon
{
width
:
24px
;
height
:
24px
}
.future
{
margin-top
:
27px
;
border-top
:
1px
solid
var
(
--line
);
border-bottom
:
1px
solid
var
(
--line
)}
summary
{
list-style
:
none
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
15px
;
padding
:
20px
0
;
font-size
:
15px
;
font-weight
:
550
;
color
:
var
(
--heading
)}
summary
::-webkit-details-marker
{
display
:
none
}
summary
.note-label
{
font-size
:
10px
;
margin-bottom
:
4px
;
color
:
var
(
--subtle
)}
summary
>
.icon
{
width
:
17px
;
height
:
17px
;
transition
:
transform
.18s
}
details
[
open
]
summary
>
.icon
{
transform
:
rotate
(
90deg
)}
.detail-body
{
font-size
:
14px
;
color
:
var
(
--muted
);
padding-bottom
:
17px
}
.detail-body
>
p
:first-child
{
margin-top
:
0
}
.detail-body
>
ul
:last-child
{
margin-bottom
:
0
}
.open-questions
{
list-style
:
none
;
padding
:
0
!important
;
display
:
grid
;
gap
:
12px
;
margin
:
24px
0
!important
}
.open-questions
li
{
position
:
relative
;
margin
:
0
;
padding
:
19px
22px
19px
62px
;
background
:
var
(
--amber-soft
);
border
:
1px
solid
var
(
--amber-line
);
border-radius
:
4px
;
font-size
:
14px
;
line-height
:
1.9
}
.open-questions
strong
{
display
:
block
;
font-size
:
17px
;
margin-bottom
:
4px
}
.question-no
{
position
:
absolute
;
left
:
20px
;
top
:
22px
;
font-size
:
14px
;
font-family
:
Georgia
,
serif
;
color
:
var
(
--amber
)}
.sources-section
{
padding
:
29px
0
18px
;
border-top
:
1px
solid
var
(
--line
);
scroll-margin-top
:
0
}
.sources-section
>
details
>
summary
{
padding
:
5px
0
16px
}
.sources-title
{
display
:
block
;
font-size
:
20px
;
line-height
:
1.6
}
.sources-intro
{
display
:
block
;
font-size
:
12px
;
color
:
var
(
--subtle
);
margin-top
:
4px
}
.sources-content
{
font-size
:
13px
;
color
:
var
(
--muted
);
line-height
:
1.95
}
.sources-content
p
{
margin
:
14px
0
}
.sources-content
ul
{
padding-left
:
18px
}
.sources-content
li
{
margin
:
12px
0
;
padding-left
:
3px
}
.sources-content
a
{
text-decoration
:
underline
;
text-decoration-color
:
var
(
--teal-line
);
text-underline-offset
:
4px
}
.document-footer
{
border-top
:
1px
solid
var
(
--line
);
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
20px
;
padding-top
:
22px
;
font-size
:
10px
;
color
:
var
(
--subtle
);
letter-spacing
:
.02em
}
.document-footer
a
{
white-space
:
nowrap
}
.toast
{
position
:
fixed
;
bottom
:
24px
;
left
:
50%
;
transform
:
translateX
(
-50%
);
z-index
:
60
;
background
:
var
(
--heading
);
color
:
var
(
--paper
);
padding
:
9px
17px
;
border-radius
:
6px
;
font-size
:
12px
;
box-shadow
:
0
5px
20px
#0001
;
max-width
:
90vw
;
text-align
:
center
;
pointer-events
:
none
}
.toast
[
hidden
]
{
display
:
none
}
.source-note
{
font-size
:
10px
!important
;
color
:
var
(
--subtle
)}
:root
[
data-size
=
"large"
]
.prose
{
font-size
:
18px
}
:root
[
data-size
=
"large"
]
.prose
table
{
font-size
:
16px
}
:root
[
data-size
=
"large"
]
.prose
.own-business
,
:root
[
data-size
=
"large"
]
.prose
.continuity
,
:root
[
data-size
=
"large"
]
.prose
.consequence
,
:root
[
data-size
=
"large"
]
.prose
.responsibilities
li
{
font-size
:
16px
}
@media
(
min-width
:
1500px
){
.shell
{
gap
:
52px
;
grid-template-columns
:
218px
minmax
(
0
,
1
fr
)}
.document
{
padding-left
:
68px
;
padding-right
:
68px
}}
@media
(
max-width
:
1180px
){
.shell
{
padding
:
26px
24px
50px
;
grid-template-columns
:
170px
minmax
(
0
,
1
fr
);
gap
:
25px
}
.document
{
padding
:
45px
35px
25px
}
.topbar-inner
{
padding
:
0
24px
}
.sidebar
{
top
:
95px
}
.toc
a
{
padding-left
:
8px
;
gap
:
8px
;
font-size
:
12px
}
.toc
small
{
font-size
:
10px
}
.flow-grid
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
70px
minmax
(
0
,
1
fr
)
55px
minmax
(
0
,
1
fr
)}
.flow-top
,
.flow-bottom
{
width
:
calc
((
100%
-
125px
)/
3
)}
.system
strong
{
font-size
:
15px
}
.system
p
{
font-size
:
9px
!important
}
.flow-figure
{
padding
:
18px
16px
}
.capture
{
font-size
:
9px
}
.boundary
{
gap
:
18px
}
.boundary
>
div
+
div
{
padding-left
:
18px
}
.boundary
h2
{
font-size
:
21px
}
h1
{
font-size
:
46px
}
h2
{
font-size
:
25px
}}
@media
(
max-width
:
960px
){
.shell
{
display
:
block
;
padding
:
0
24px
45px
}
.sidebar
{
position
:
sticky
;
top
:
70px
;
z-index
:
20
;
max-height
:
none
;
background
:
var
(
--bg
);
padding
:
10px
0
;
border-bottom
:
1px
solid
var
(
--line
);
margin-bottom
:
25px
;
overflow
:
visible
}
.toc-label
,
.rail-note
{
display
:
none
}
.toc
{
display
:
flex
;
overflow
:
auto
;
scrollbar-width
:
none
;
gap
:
5px
}
.toc
::-webkit-scrollbar
{
display
:
none
}
.toc
a
{
flex
:
none
;
align-items
:
center
;
padding
:
8px
13px
;
border
:
0
;
border-bottom
:
2px
solid
transparent
;
border-radius
:
5px
;
font-size
:
12px
;
white-space
:
nowrap
;
gap
:
7px
}
.toc
a
.active
{
border-bottom-color
:
var
(
--teal
)}
.toc
small
{
display
:
none
}
.toc-number
{
padding
:
0
;
font-size
:
9px
}
.document
{
padding
:
45px
48px
25px
;
max-width
:
830px
;
margin
:
auto
}
.document
:before
{
left
:
48px
}
html
{
scroll-padding-top
:
145px
}
.chapter
,
.sources-section
{
scroll-margin-top
:
0
}
.flow-grid
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
85px
minmax
(
0
,
1
fr
)
65px
minmax
(
0
,
1
fr
)}
.flow-top
,
.flow-bottom
{
width
:
calc
((
100%
-
150px
)/
3
)}
.system
strong
{
font-size
:
17px
}
.system
p
{
font-size
:
10px
!important
}
.capture
{
font-size
:
10px
}
.chapter-head
{
gap
:
17px
}}
@media
(
max-width
:
640px
){
body
{
font-size
:
15px
;
line-height
:
1.95
}
.topbar
{
height
:
59px
}
.topbar-inner
{
padding
:
0
16px
;
gap
:
6px
}
.doc-mark
{
font-size
:
11px
;
gap
:
7px
}
.mark-icon
{
width
:
27px
;
height
:
27px
}
.mark-icon
.icon
{
width
:
17px
;
height
:
17px
}
.version
,
.print-label
{
display
:
none
}
.toolbar
{
gap
:
0
}
.tool
{
min-width
:
33px
;
min-height
:
34px
;
padding
:
6px
}
.print-button
{
margin-left
:
2px
;
border
:
0
}
.shell
{
padding
:
0
13px
22px
}
.sidebar
{
top
:
59px
;
padding
:
8px
0
;
margin-bottom
:
15px
}
.toc
a
{
font-size
:
11px
;
padding
:
7px
11px
}
.document
{
padding
:
34px
22px
22px
;
border-radius
:
3px
}
.document
:before
{
left
:
22px
;
width
:
46px
}
.eyebrow
{
font-size
:
10px
;
gap
:
6px
;
margin-bottom
:
20px
;
letter-spacing
:
.025em
}
h1
{
font-size
:
37px
;
line-height
:
1.35
}
.hero-deck
{
font-size
:
17px
;
margin-top
:
13px
}
.meta
{
font-size
:
10px
;
gap
:
10px
;
margin-top
:
20px
}
.hero-scope
{
font-size
:
12px
;
margin-top
:
13px
}
.hero
{
padding-bottom
:
29px
}
.boundary
{
margin-top
:
28px
;
gap
:
11px
;
padding
:
20px
0
}
.boundary
>
div
+
div
{
padding-left
:
11px
}
.boundary
.role-name
{
font-size
:
10px
}
.boundary
h2
{
font-size
:
17px
;
line-height
:
1.7
;
margin-top
:
5px
}
.boundary
p
{
font-size
:
10px
}
.core-boundary
{
font-size
:
10px
}
.principle
{
font-size
:
13px
;
padding-top
:
22px
;
line-height
:
1.95
}
.principle
strong
{
font-size
:
18px
}
.chapter
{
padding
:
28px
0
30px
;
scroll-margin-top
:
0
}
.chapter-head
{
gap
:
12px
;
margin-bottom
:
21px
}
.chapter-num
{
font-size
:
30px
;
min-width
:
34px
;
padding-top
:
13px
}
h2
{
font-size
:
22px
;
line-height
:
1.6
}
.chapter-kicker
{
font-size
:
9px
}
.prose
p
{
margin
:
15px
0
}
h3
{
font-size
:
18px
}
.step-heading
{
font-size
:
15px
}
.flow-figure
{
padding
:
16px
;
margin
:
17px
-3px
21px
}
.flow-figure
figcaption
{
font-size
:
11px
;
gap
:
6px
;
margin-bottom
:
20px
}
.figure-number
{
font-size
:
8px
}
.state-badge
{
display
:
block
;
flex-basis
:
100%
;
margin-left
:
0
;
font-size
:
10px
}
.flow-grid
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
stretch
;
gap
:
0
}
.flow-top
{
width
:
100%
;
margin-bottom
:
7px
}
.capture
{
font-size
:
11px
}
.system
{
min-height
:
101px
;
padding
:
16px
10px
}
.system
strong
{
font-size
:
19px
}
.system
p
{
font-size
:
11px
!important
;
margin-top
:
5px
!important
}
.system-kicker
{
font-size
:
10px
;
margin-bottom
:
2px
}
.connector
{
padding
:
12px
0
;
min-height
:
78px
;
display
:
grid
;
grid-template-columns
:
1
fr
22px
1
fr
;
gap
:
7px
;
text-align
:
right
;
font-size
:
11px
}
.connector
.icon
{
transform
:
rotate
(
90deg
);
width
:
23px
;
height
:
23px
}
.connector
small
{
font-size
:
9px
;
text-align
:
left
;
max-width
:
70px
}
.connector
:not
(
:has
(
small
))
:after
{
content
:
""
}
.connector
br
{
display
:
none
}
.flow-bottom
{
width
:
100%
;
justify-content
:
flex-start
;
margin-top
:
14px
}
.storage
{
padding
:
0
;
font-size
:
11px
}
.storage-line
{
display
:
none
}
.storage
:before
{
content
:
"美亚 →"
;
font-size
:
10px
;
color
:
var
(
--subtle
);
margin-right
:
2px
}
.storage
small
{
font-size
:
10px
}
.view-path
{
flex-direction
:
column
;
align-items
:
flex-start
;
gap
:
8px
;
padding
:
18px
20px
;
font-size
:
12px
}
.view-path
>
.icon
{
transform
:
rotate
(
90deg
);
margin-left
:
2px
}
.consequence
{
padding
:
17px
18px
;
font-size
:
14px
}
.requirement
{
padding
:
17px
19px
;
font-size
:
19px
;
margin
:
19px
0
}
.emphasis
{
font-size
:
15px
;
padding-left
:
15px
}
.table-wrap
{
border
:
0
;
border-top
:
1px
solid
var
(
--line
);
border-radius
:
0
;
margin
:
20px
0
}
table
,
tbody
,
tr
,
td
{
display
:
block
}
thead
{
display
:
none
}
tr
{
padding
:
15px
0
;
border-bottom
:
1px
solid
var
(
--line
)}
td
{
padding
:
4px
0
;
border
:
0
;
font-size
:
13px
;
line-height
:
1.9
;
width
:
auto
!important
}
td
:first-child
{
font-size
:
14px
;
font-weight
:
650
;
margin-bottom
:
5px
}
td
:not
(
:first-child
)
:before
{
content
:
attr
(
data-label
);
color
:
var
(
--subtle
);
font-size
:
10px
;
display
:
block
;
letter-spacing
:
.025em
}
#meyer
td
:nth-child
(
2
)
{
width
:
auto
}
tbody
tr
:hover
{
background
:
transparent
}
.own-business
{
font-size
:
13px
;
padding
:
18px
}
.continuity
{
font-size
:
14px
;
padding-left
:
15px
}
.responsibilities
{
grid-template-columns
:
1
fr
;
gap
:
12px
}
.responsibilities
li
{
font-size
:
14px
;
padding
:
20px
}
.responsibilities
li
>
strong
{
font-size
:
17px
;
margin-top
:
9px
}
.open-questions
li
{
padding
:
16px
17px
16px
49px
;
font-size
:
13px
}
.question-no
{
left
:
16px
;
top
:
19px
;
font-size
:
12px
}
.open-questions
strong
{
font-size
:
16px
}
.sources-section
{
scroll-margin-top
:
0
}
.sources-content
{
font-size
:
12px
;
overflow-wrap
:
anywhere
}
.document-footer
{
font-size
:
9px
;
gap
:
12px
;
align-items
:
flex-start
}
.document-footer
span
{
max-width
:
220px
}
.source-note
{
font-size
:
9px
!important
}
html
{
scroll-padding-top
:
126px
}}
@media
(
max-width
:
640px
){
.boundary
{
grid-template-columns
:
1
fr
;
gap
:
13px
;
padding
:
19px
0
}
.boundary
>
div
{
display
:
grid
;
grid-template-columns
:
92px
1
fr
;
column-gap
:
12px
;
align-items
:
baseline
}
.boundary
>
div
+
div
{
border-left
:
0
;
border-top
:
1px
solid
var
(
--line
);
padding
:
12px
0
0
}
.boundary
.role-name
{
grid-row
:
1
/
3
;
font-size
:
11px
}
.boundary
h2
{
font-size
:
18px
;
margin
:
0
;
line-height
:
1.5
}
.boundary
p
{
grid-column
:
2
;
margin-top
:
3px
;
font-size
:
11px
}
.principle
strong
{
margin-bottom
:
10px
}}
@media
(
prefers-reduced-motion
:
reduce
){
html
{
scroll-behavior
:
auto
}
*,*
:before
,*
:after
{
transition
:
none
!important
;
animation
:
none
!important
}}
@media
print
{
html
{
scroll-behavior
:
auto
;
color-scheme
:
light
!important
}
:root
,
:root
[
data-theme
=
"dark"
],
:root:not
([
data-theme
=
"light"
])
{
--bg
:
#fff
;
--paper
:
#fff
;
--ink
:
#25322e
;
--heading
:
#172e2b
;
--muted
:
#53645c
;
--subtle
:
#65736c
;
--line
:
#d5dbd3
;
--soft
:
#f4f6f0
;
--teal
:
#17665b
;
--teal-soft
:
#edf4ed
;
--teal-line
:
#bad3c4
;
--amber
:
#80591f
;
--amber-soft
:
#faf4e8
;
--amber-line
:
#e1cfaa
;
--blue
:
#355d80
;
--blue-soft
:
#eef3f7
}
body
{
background
:
white
;
font-size
:
10pt
;
line-height
:
1.75
;
-webkit-print-color-adjust
:
exact
;
print-color-adjust
:
exact
}
.topbar
,
.sidebar
,
.page-progress
,
.toast
,
.skip-link
{
display
:
none
!important
}
.shell
{
display
:
block
;
max-width
:
none
;
margin
:
0
;
padding
:
0
}
.document
{
padding
:
0
;
border
:
0
;
border-radius
:
0
;
box-shadow
:
none
;
max-width
:
none
}
.document
:before
{
display
:
none
}
.hero
{
padding
:
0
0
22px
}
h1
{
font-size
:
32pt
}
.hero-deck
{
font-size
:
15pt
}
.eyebrow
{
font-size
:
9pt
}
.meta
,
.hero-scope
{
font-size
:
8pt
}
.boundary
{
margin-top
:
20px
;
padding
:
15px
0
;
break-inside
:
avoid
}
.boundary
h2
{
font-size
:
16pt
}
.boundary
.role-name
{
font-size
:
9pt
}
.boundary
p
{
font-size
:
8pt
}
.principle
{
font-size
:
9pt
}
.principle
strong
{
font-size
:
13pt
}
.chapter
{
padding
:
24px
0
;
break-before
:
auto
}
.chapter-head
{
break-after
:
avoid
;
margin-bottom
:
15px
}
.chapter-num
{
font-size
:
28pt
}
h2
{
font-size
:
18pt
}
h3
{
font-size
:
13pt
;
break-after
:
avoid
}
.prose
p
{
margin
:
12px
0
;
orphans
:
3
;
widows
:
3
}
.prose
ul
,
.prose
ol
{
margin
:
12px
0
}
.flow-figure
{
break-inside
:
avoid
;
background-image
:
none
;
padding
:
18px
}
.system
{
min-height
:
100px
}
.flow-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
)
85px
minmax
(
0
,
1
fr
)
65px
minmax
(
0
,
1
fr
)}
.flow-top
,
.flow-bottom
{
width
:
calc
((
100%
-
150px
)/
3
)}
.system
strong
{
font-size
:
12pt
}
.system
p
{
font-size
:
7pt
!important
}
.table-wrap
{
overflow
:
visible
;
break-inside
:
avoid
}
table
{
font-size
:
9pt
}
th
{
font-size
:
8pt
}
td
,
th
{
padding
:
9px
12px
}
tr
{
break-inside
:
avoid
}
thead
{
display
:
table-header-group
}
.consequence
,
.requirement
,
.own-business
,
.continuity
,
.responsibilities
li
,
.open-questions
li
{
break-inside
:
avoid
}
.consequence
,
.emphasis
,
.continuity
{
font-size
:
10pt
}
.requirement
{
font-size
:
14pt
}
.own-business
,
.footnote-callout
{
font-size
:
9pt
}
.responsibilities
li
{
font-size
:
9pt
}
.responsibilities
li
>
strong
{
font-size
:
12pt
}
.future
{
break-inside
:
avoid
}
details
>
.detail-body
,
details
>
.sources-content
{
display
:
block
!important
}
details
::details-content
{
content-visibility
:
visible
!important
;
height
:
auto
!important
}
summary
.icon
{
display
:
none
}
.open-questions
li
{
font-size
:
9pt
}
.open-questions
strong
{
font-size
:
12pt
}
.sources-content
{
font-size
:
8pt
}
.sources-section
{
break-inside
:
avoid
}
.sources-content
a
{
color
:
var
(
--ink
)}
.document-footer
{
font-size
:
7pt
;
margin-top
:
15px
}
.document-footer
a
{
display
:
none
}
a
{
text-decoration
:
none
}
.step-heading
,
.figure-intro
,
.prose
>
p
:first-child
,
.prose
>
p
:has
(+
.table-wrap
),
.prose
>
p
:has
(+
blockquote
),
.prose
>
p
:has
(+
.responsibilities
),
.prose
>
p
:has
(+
.open-questions
)
{
break-after
:
avoid
}
.sources-title
{
font-size
:
13pt
}
@page
{
size
:
A4
;
margin
:
17mm
16mm
18mm
}}
</style>
</head>
<body>
<aside
role=
"note"
style=
"padding:12px 20px;background:#fff4da;color:#594100;border-bottom:1px solid #e9cf8d;font:14px/1.7 sans-serif"
>
历史评审稿,2026-10-22 归档。事件推送方案未被采用;现行实现按诊所分页轮询并批量查询分析。
<a
href=
"../../../apps/pac-service/src/integrations/meyer/README.md"
>
查看当前采集说明
</a></aside>
<a
class=
"skip-link"
href=
"#document"
>
跳到正文
</a><div
class=
"page-progress"
aria-hidden=
"true"
></div>
<header
class=
"topbar"
><div
class=
"topbar-inner"
><a
class=
"doc-mark"
href=
"#document"
><span
class=
"mark-icon"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m12 3 10 5-10 5L2 8l10-5ZM2 12l10 5 10-5M2 16l10 5 10-5"
/></svg></span><span>
影像与分析 · 方案设计
</span></a><div
class=
"toolbar"
><span
class=
"version"
>
内部评审 · v0.2
</span><button
class=
"tool"
id=
"font-button"
aria-label=
"切换较大字号"
aria-pressed=
"false"
title=
"切换字号"
><span
class=
"size-icon"
aria-hidden=
"true"
>
Aa
</span></button><button
class=
"tool"
id=
"theme-button"
aria-label=
"切换深色模式"
title=
"切换深浅色"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><circle
cx=
"12"
cy=
"12"
r=
"4"
/><path
d=
"M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5"
/></svg></button><button
class=
"tool print-button"
id=
"print-button"
aria-label=
"打印或保存为 PDF"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"M6 9V3h12v6M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
/><path
d=
"M6 14h12v8H6zM18 12h.01"
/></svg><span
class=
"print-label"
>
打印 / PDF
</span></button></div></div></header>
<div
class=
"shell"
>
<aside
class=
"sidebar"
aria-label=
"文档目录"
><p
class=
"toc-label"
>
阅读导航
</p><nav
class=
"toc"
><a
href=
"#relationship"
class=
"active"
aria-current=
"location"
><span
class=
"toc-number"
>
01
</span><span>
整体关系
<small>
数据供给与原片查看
</small></span></a><a
href=
"#meyer"
><span
class=
"toc-number"
>
02
</span><span>
美亚 PACS
<small>
让变化可获取
</small></span></a><a
href=
"#saas"
><span
class=
"toc-number"
>
03
</span><span>
FridaySaaS
<small>
留业务记录,统一来源
</small></span></a><a
href=
"#pac"
><span
class=
"toc-number"
>
04
</span><span>
PAC
<small>
消费信号,打开影像
</small></span></a><a
href=
"#review"
><span
class=
"toc-number"
>
05
</span><span>
评审与确认
<small>
方向与待确认能力
</small></span></a><a
href=
"#sources"
><span
class=
"toc-number"
>
↗
</span><span>
资料依据
<small>
范围与原始出处
</small></span></a></nav><div
class=
"rail-note"
><b>
先确定边界,再细化实现
</b>
本稿讨论设计方向。接口、存储和开发任务在方案采纳后展开。
<div
class=
"reading-status"
><span>
阅读进度
</span><span
id=
"progress-value"
>
0%
</span></div><div
class=
"reading-track"
aria-hidden=
"true"
><span></span></div></div></aside>
<main
class=
"document"
id=
"document"
>
<header
class=
"hero"
><p
class=
"eyebrow"
>
美亚 PACS
<span
class=
"sep"
>
×
</span>
FridaySaaS
<span
class=
"sep"
>
×
</span>
PAC
</p><h1><span>
影像与分析数据
</span><span>
方案设计
</span></h1><p
class=
"hero-deck"
>
让影像资产、业务归属与诊断信号各有承接。
</p><div
class=
"meta"
><span
class=
"draft"
>
内部评审稿
</span><span>
v0.2
</span><time
datetime=
"2026-10-16"
>
2026.10.16
</time></div><p
class=
"hero-scope"
>
本轮先确认设计方向与系统边界;字段、表结构、调度周期和开发任务在方案采纳后细化。
</p>
<div
class=
"boundary"
aria-label=
"建议的三方职责"
><div><span
class=
"role-name"
>
美亚 PACS
</span><h2>
管理影像资产
</h2><p>
影像与分析 · 原片访问
</p></div><div><span
class=
"role-name"
>
FridaySaaS
</span><h2>
管理业务归属
</h2><p>
关系校验 · 留存与同步
</p></div><div><span
class=
"role-name"
>
PAC
</span><h2>
消费宿主事实
</h2><p>
诊断信号 · 画像与召回
</p></div></div><p
class=
"core-boundary"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m5 12 4 4L19 6"
/></svg>
PAC 核心不直接接美亚。
</p><div
class=
"principle"
><p><span
class=
"note-label"
>
判断依据
</span><strong>
三个系统可以保存和使用同一份患者与影像关系,但应明确各自数据的权威来源,并让关联及其变更沿统一链路传递。
</strong>
FridaySaaS 提供业务患者身份,美亚维护影像资产及其患者映射;SaaS 将两者对应关系校验、留存后提供给 PAC,PAC 据此形成事实和诊断信号。
</p>
</div></header>
<section
class=
"chapter"
id=
"relationship"
aria-labelledby=
"heading-relationship"
><header
class=
"chapter-head"
><span
class=
"chapter-num"
aria-hidden=
"true"
>
01
</span><div><p
class=
"chapter-kicker"
>
RELATIONSHIP
</p><h2
id=
"heading-relationship"
>
整体关系:数据供给与查看原片分开
</h2></div></header><div
class=
"prose"
><p
class=
"step-heading"
><strong>
① 关联基础:先明确影像属于谁
</strong></p>
<p>
FridaySaaS 向美亚同步患者、诊所和就诊信息,美亚据此关联影像;SaaS 校验并留存对应关系,再向 PAC 提供。
</p>
<p
class=
"step-heading"
><strong>
② 数据供给:同步影像信息与分析结果
</strong></p>
<p
class=
"figure-intro"
>
以下以启用美亚新影像为例,表示目标链路:
</p>
<figure
class=
"flow-figure"
aria-labelledby=
"flow-caption"
>
<figcaption
id=
"flow-caption"
><span
class=
"figure-number"
>
FIG. 01
</span><strong>
影像与分析的数据供给
</strong><span
class=
"state-badge"
>
目标链路 · 新影像启用时
</span></figcaption>
<div
class=
"flow-top"
><span
class=
"capture"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m12 3 10 5-10 5L2 8l10-5ZM2 12l10 5 10-5M2 16l10 5 10-5"
></path></svg>
诊所设备与前置机
</span><span
class=
"capture-arrow"
aria-hidden=
"true"
>
↓
</span></div>
<div
class=
"flow-grid"
>
<div
class=
"system meyer"
><span
class=
"system-kicker"
>
影像资产
</span><strong>
美亚 PACS
</strong><p>
采集接入 · 影像与分析
</p></div>
<div
class=
"connector"
><span>
影像信息
<br>
分析结果及变更
</span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"M4 12h16m-6-6 6 6-6 6"
></path></svg><small>
获取方式待确认
</small></div>
<div
class=
"system saas"
><span
class=
"system-kicker"
>
业务归属
</span><strong>
FridaySaaS
</strong><p>
校验归属 · 留存 · 同步
</p></div>
<div
class=
"connector"
><span>
宿主事实
</span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"M4 12h16m-6-6 6 6-6 6"
></path></svg></div>
<div
class=
"system pac"
><span
class=
"system-kicker"
>
信号应用
</span><strong>
PAC
</strong><p>
诊断信号 · 画像与召回
</p></div>
</div>
<div
class=
"flow-bottom"
><div
class=
"storage"
><span
class=
"storage-line"
aria-hidden=
"true"
></span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m12 3 10 5-10 5L2 8l10-5ZM2 12l10 5 10-5M2 16l10 5 10-5"
></path></svg><span>
OSS
<small>
原片存储
</small></span></div></div>
</figure>
<p>
原片由美亚管理,SaaS 向 PAC 提供影像业务信息与分析结果。美亚到 SaaS 如何获取变更,仍需确认。
</p>
<p>
新老影像功能可以保留,实际使用时择一启用;选用旧影像时,也由 SaaS 统一向 PAC 提供数据。
</p>
<p
class=
"step-heading"
><strong>
③ 查看原片:用户按需打开宿主入口
</strong></p>
<div
class=
"view-path"
role=
"group"
aria-label=
"查看原片路径"
><span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m10 13 4-4m-6 6-2 2a4 4 0 0 1-6-6l5-5a4 4 0 0 1 6 0m2 3 2-2a4 4 0 0 1 6 6l-5 5a4 4 0 0 1-6 0"
transform=
"translate(1 1) scale(.9)"
></path></svg>
PAC 宿主槽位
</span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"M4 12h16m-6-6 6 6-6 6"
></path></svg><span>
FridaySaaS 影像页面
</span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"M4 12h16m-6-6 6 6-6 6"
></path></svg><span>
当前选用的影像系统
</span></div>
<p>
SaaS 负责权限和入口;启用新影像时,由美亚提供阅片服务。
<strong>
数据同步持续进行,查看原片由用户按需发起。
</strong></p>
</div></section>
<section
class=
"chapter"
id=
"meyer"
aria-labelledby=
"heading-meyer"
><header
class=
"chapter-head"
><span
class=
"chapter-num"
aria-hidden=
"true"
>
02
</span><div><p
class=
"chapter-kicker"
>
MEYER PACS
</p><h2
id=
"heading-meyer"
>
美亚:最重要的承诺是“变化可获取”
</h2></div></header><div
class=
"prose"
><p>
美亚负责采集接入、原片存储、影像关联、阅片,以及其提供的分析。对外不仅要能查询当前数据,还要让 SaaS 知道数据发生了什么变化。
</p>
<p
class=
"consequence"
><span
class=
"note-label"
>
只发现新增,会漏掉什么
</span><strong>
如果只能查新影像列表,SaaS 可以知道“多了一张片”,却未必知道这张片后来分析完成、被改绑到另一位患者,或被撤销。下游就可能继续使用过期甚至归属错误的依据。
</strong></p>
<p>
因此,对美亚真正要提的要求是:
</p>
<blockquote
class=
"requirement"
>
<p>
提供可追溯、可补拉的影像及分析变更机制,包含新增、修改、撤销和归属变化。
</p>
</blockquote>
<div
class=
"table-wrap"
><table>
<thead>
<tr>
<th
scope=
"col"
>
美亚提供的能力
</th>
<th
scope=
"col"
>
FridaySaaS 如何获取
</th>
<th
scope=
"col"
>
意味着什么
</th>
</tr>
</thead>
<tbody><tr>
<td
data-label=
"美亚提供的能力"
>
有事件通知,并能补查变更
</td>
<td
data-label=
"FridaySaaS 如何获取"
>
通知后同步,遗漏时补拉
</td>
<td
data-label=
"意味着什么"
>
及时性较好,也能恢复遗漏
</td>
</tr>
<tr>
<td
data-label=
"美亚提供的能力"
>
没有通知,有完整变更查询
</td>
<td
data-label=
"FridaySaaS 如何获取"
>
定时拉取
</td>
<td
data-label=
"意味着什么"
>
可以可靠同步,延迟取决于拉取周期
</td>
</tr>
<tr>
<td
data-label=
"美亚提供的能力"
>
只有入库时间列表、逐张查询
</td>
<td
data-label=
"FridaySaaS 如何获取"
>
定时发现新片,反复查询分析
</td>
<td
data-label=
"意味着什么"
>
能做部分同步,但后续修订、改绑、撤销的完整性没有保障
</td>
</tr>
</tbody></table></div>
</div></section>
<section
class=
"chapter"
id=
"saas"
aria-labelledby=
"heading-saas"
><header
class=
"chapter-head"
><span
class=
"chapter-num"
aria-hidden=
"true"
>
03
</span><div><p
class=
"chapter-kicker"
>
FRIDAY SAAS
</p><h2
id=
"heading-saas"
>
FridaySaaS:留业务记录,统一新老来源
</h2></div></header><div
class=
"prose"
><h3>
3.1 为什么需要留新影像数据
</h3>
<p
class=
"emphasis"
><strong>
如果 SaaS 只在用户打开页面时代理查询美亚,它承担的是展示功能,无法承担稳定的下游数据供给。
</strong>
没人打开页面时,PAC 仍需要知道患者有了新的影像或分析。
</p>
<p>
既然 PAC 从 SaaS 接收数据,SaaS 就应保存一份能够重试、重推、核对的影像业务记录:
</p>
<div
class=
"table-wrap"
><table>
<thead>
<tr>
<th
scope=
"col"
>
应保存的信息
</th>
<th
scope=
"col"
>
用途
</th>
</tr>
</thead>
<tbody><tr>
<td
data-label=
"应保存的信息"
>
患者、租户与影像采集诊所
</td>
<td
data-label=
"用途"
>
明确业务归属;采集诊所不随患者后续转店而改变
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
影像来源、影像与检查标识
</td>
<td
data-label=
"用途"
>
稳定识别、去重和追溯
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
类型、拍摄时间、绑定及有效状态
</td>
<td
data-label=
"用途"
>
解释这份影像的业务含义
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
稳定文件引用,如 OSS ID
</td>
<td
data-label=
"用途"
>
后续获取原片;临时链接过期后仍可定位
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
分析结果、来源及版本
</td>
<td
data-label=
"用途"
>
保留当时依据,支持补推和重放
</td>
</tr>
<tr>
<td
data-label=
"应保存的信息"
>
同步进度与失败记录
</td>
<td
data-label=
"用途"
>
知道哪些已交付、哪些还需要补齐
</td>
</tr>
</tbody></table></div>
<p>
原片继续由美亚管理。SaaS 保存索引和结果,不重复建设影像存储;临时签名 URL 不能作为永久地址。
</p>
<p
class=
"continuity"
><strong>
这样即使美亚暂时不可用,SaaS 和 PAC 仍可查询已留存的影像信息和分析结果;原片访问和最新变更同步则可能受影响。
</strong>
分析结果会影响后续业务判断,值得留存,而不应仅在查看页面时临时取一次。
</p>
<p>
美亚接入、获取变更和归属校验由 SaaS 对接层负责;open-connector 从 SaaS 已保存的业务记录向 PAC 推送。供应商协议与下游数据传输各自负责自己的部分。
</p>
<p
class=
"own-business"
><span
class=
"note-label"
>
SaaS 自身业务同样受益
</span>
即使不接 PAC,留存业务记录也能支撑 SaaS 自身业务:将影像与就诊、病历、收费关联,用于检查进度跟进、复诊查阅、经营分析,以及追溯当时使用的分析结果。如果不留存,这些业务就需要反复查询美亚并临时关联数据,受其接口可用性和查询能力限制;分析结果更新或撤销后,若美亚不提供历史版本,SaaS 就难以还原当时使用的依据。
</p>
<h3>
3.2 新老影像怎么归一
</h3>
<p><strong>
业务语义归一,来源实现分开。
</strong>
新老功能可以保留,实际择一使用;对下游都应能回答同一组问题:
</p>
<ul>
<li>
影像属于谁、在哪里拍摄,是什么类型、什么时候拍摄;
</li>
<li>
当前是否有效,怎样查看,有哪些分析结果。
</li>
</ul>
<p><strong>
如果下游直接依赖“老影像的一套字段”和“美亚的另一套字段”,每切换一次影像系统,下游都要跟着改。
</strong>
统一表达后,来源切换主要由 SaaS 处理,PAC 继续按宿主事实消费。
</p>
<p>
但统一不等于强行写入旧表。美亚有检查、切片、绑定状态和外部文件标识;如果旧结构表达不了,不能为了兼容丢掉它们,也不能把临时链接塞进旧文件地址就算完成。是否复用旧表,是方案采纳后的存储设计选择。
</p>
<p
class=
"footnote-callout"
>
如果未来把历史影像迁入美亚,应保留新旧标识的对应关系。
<strong>
同一张片换了存储位置,不应被当成一次新的拍摄或重复的分析依据。
</strong></p>
</div></section>
<section
class=
"chapter"
id=
"pac"
aria-labelledby=
"heading-pac"
><header
class=
"chapter-head"
><span
class=
"chapter-num"
aria-hidden=
"true"
>
04
</span><div><p
class=
"chapter-kicker"
>
PAC
</p><h2
id=
"heading-pac"
>
PAC:用分析形成诊断信号,通过宿主查看影像
</h2></div></header><div
class=
"prose"
><p>
PAC 在这条链路中的职责保持两项:
</p>
<ol
class=
"responsibilities"
>
<li><span
class=
"responsibility-icon"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m12 3 10 5-10 5L2 8l10-5ZM2 12l10 5 10-5M2 16l10 5 10-5"
></path></svg></span><strong>
消费影像分析。
</strong>
将宿主提供的有效分析结果作为诊断信号源之一,与其他患者事实共同用于画像与召回,并保留来源依据。
</li>
<li><span
class=
"responsibility-icon"
><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m10 13 4-4m-6 6-2 2a4 4 0 0 1-6-6l5-5a4 4 0 0 1 6 0m2 3 2-2a4 4 0 0 1 6 6l-5 5a4 4 0 0 1-6 0"
transform=
"translate(1 1) scale(.9)"
></path></svg></span><strong>
提供影像查看入口。
</strong>
沿用宿主槽位打开 FridaySaaS,由宿主处理新老影像入口、权限和阅片授权。
</li>
</ol>
<p>
美亚生成的分析,由 SaaS 接收、留底后推给 PAC。
<strong>
如果 PAC 每次处理事实都临时向美亚取结果,重放同一份数据时可能拿到不同内容,就无法解释当时为什么得出那个判断。
</strong></p>
<p>
PAC 可以知道数据来源是美亚,但美亚不应成为 PAC 核心的直接依赖。
<strong>
否则接入下一家影像供应商时,PAC 又要接一套鉴权、患者映射和同步机制,还可能与 SaaS 的权限及归属规则形成两套判断。
</strong>
这些差异应由宿主消化。
</p>
<details
class=
"future"
><summary><span><span
class=
"note-label"
>
未来扩展
</span>
如果由 PAC 主动发起分析
</span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m9 5 7 7-7 7"
></path></svg></summary><div
class=
"detail-body"
><p>
如果将来是 PAC 自己发起分析,那是另一项能力:
</p><ul>
<li>
PAC 通过宿主获得授权的文件;
</li>
<li>
PAC 调用自己选择的分析服务;
</li>
<li>
结果标明由 PAC 发起,保留实际分析服务来源及结果版本,并关联原始影像。
</li>
</ul></div></details>
</div></section>
<section
class=
"chapter"
id=
"review"
aria-labelledby=
"heading-review"
><header
class=
"chapter-head"
><span
class=
"chapter-num"
aria-hidden=
"true"
>
05
</span><div><p
class=
"chapter-kicker"
>
REVIEW
</p><h2
id=
"heading-review"
>
评审要确定什么,哪些仍需外部确认
</h2></div></header><div
class=
"prose"
><p>
本轮评审确认的是职责和数据留存方向:
</p>
<div
class=
"table-wrap"
><table>
<thead>
<tr>
<th
scope=
"col"
>
系统
</th>
<th
scope=
"col"
>
对下游承担的承诺
</th>
</tr>
</thead>
<tbody><tr>
<td
data-label=
"系统"
>
美亚
</td>
<td
data-label=
"对下游承担的承诺"
>
影像与分析可查询,变更可获取,原片可授权访问
</td>
</tr>
<tr>
<td
data-label=
"系统"
>
FridaySaaS
</td>
<td
data-label=
"对下游承担的承诺"
>
患者归属正确,新老影像统一表达,业务数据可留底、补推和核对
</td>
</tr>
<tr>
<td
data-label=
"系统"
>
PAC
</td>
<td
data-label=
"对下游承担的承诺"
>
消费宿主分析作为诊断信号,通过宿主入口查看影像
</td>
</tr>
</tbody></table></div>
<p>
方案采纳前,优先向美亚确认两件事:
</p>
<ul
class=
"open-questions"
>
<li><span
class=
"question-no"
>
01
</span><strong>
变更怎样获取
</strong>
有无通知;没有通知时,能否完整拉到新增、修订、撤销和归属变化,并补查遗漏。
</li>
<li><span
class=
"question-no"
>
02
</span><strong>
分析怎样产生和识别
</strong>
何时触发,怎样区分未分析、处理中、失败、完成零发现,以及结果后来是否修改。
</li>
</ul>
<p>
这两项决定后续采用通知还是轮询,以及分析能否可靠成为诊断信号。具体字段、存储结构、调度周期和任务拆分,在方向采纳、能力确认后再制定。
</p>
</div></section>
<section
class=
"sources-section"
id=
"sources"
aria-labelledby=
"heading-sources"
><details
open
><summary><span><span
class=
"sources-title"
id=
"heading-sources"
>
依据与核对范围
</span><span
class=
"sources-intro"
>
本地代码、SDK 与三份美亚资料
</span></span><svg
class=
"icon "
viewBox=
"0 0 24 24"
aria-hidden=
"true"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
stroke-linecap=
"round"
stroke-linejoin=
"round"
><path
d=
"m9 5 7 7-7 7"
/></svg></summary><div
class=
"sources-content"
><p>
本稿依据 2026-10-16 已核对的本地代码、SDK 和以下资料;未验证美亚实际部署能力。接口细节存在版本差异,正式对接仍需双方确认。
</p>
<ul>
<li><span
title=
"本机参考文件,未随归档分发"
>
美亚系统集成方案
</span>
:第 2–3 章职责及信息同步、第 4–5 章设备与前置机、第 6–7 章阅片与患者映射。
</li>
<li><span
title=
"本机参考文件,未随归档分发"
>
美亚私有化部署方案
</span>
:明确规划阿里云 OSS 和算法资源;规划不代表当前能力已启用。
</li>
<li><span
title=
"本机参考文件,未随归档分发"
>
美亚接口文档
</span>
:§6.3 分析查询及空结果含义、§6.4/6.8 文件引用与临时 URL。
</li>
<li>
SaaS 代码:
<span
title=
"本机参考文件,未随归档分发"
>
PACS 接口
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
同步与查询
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
新版影像中心
</span>
。
</li>
<li>
PAC 代码:
<span
title=
"本机参考文件,未随归档分发"
>
影像分析事实
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
宿主影像入口
</span>
。
</li>
</ul>
<p>
资料链接指向本机原件;内部分享时可随报告提供原 PDF。
</p>
</div></details></section>
<footer
class=
"document-footer"
><span>
美亚 × FridaySaaS × PAC
<br>
影像与分析数据方案设计 · 内部评审稿
</span><a
href=
"#document"
>
回到开头 ↑
</a></footer>
</main></div><div
class=
"toast"
id=
"toast"
role=
"status"
aria-live=
"polite"
hidden
></div>
<script>
(()
=>
{
const
root
=
document
.
documentElement
,
media
=
window
.
matchMedia
(
'(prefers-color-scheme:dark)'
);
const
toast
=
document
.
getElementById
(
'toast'
);
let
toastTimer
;
function
say
(
text
){
toast
.
textContent
=
text
;
toast
.
hidden
=
false
;
clearTimeout
(
toastTimer
);
toastTimer
=
setTimeout
(()
=>
toast
.
hidden
=
true
,
1800
)}
function
theme
(){
return
root
.
dataset
.
theme
||
(
media
.
matches
?
'dark'
:
'light'
)}
function
themeLabel
(){
document
.
getElementById
(
'theme-button'
).
setAttribute
(
'aria-label'
,
theme
()
===
'dark'
?
'切换浅色模式'
:
'切换深色模式'
)}
document
.
getElementById
(
'theme-button'
).
addEventListener
(
'click'
,()
=>
{
const
next
=
theme
()
===
'dark'
?
'light'
:
'dark'
;
root
.
dataset
.
theme
=
next
;
try
{
localStorage
.
setItem
(
'imaging-report-theme'
,
next
)}
catch
{}
themeLabel
();
say
(
next
===
'dark'
?
'已切换深色阅读'
:
'已切换浅色阅读'
)});
themeLabel
();
media
.
addEventListener
?.(
'change'
,
themeLabel
);
const
font
=
document
.
getElementById
(
'font-button'
);
function
fontState
(){
const
large
=
root
.
dataset
.
size
===
'large'
;
font
.
setAttribute
(
'aria-pressed'
,
String
(
large
));
font
.
dataset
.
active
=
String
(
large
);
font
.
setAttribute
(
'aria-label'
,
large
?
'切换标准字号'
:
'切换较大字号'
)};
fontState
();
font
.
addEventListener
(
'click'
,()
=>
{
const
large
=
root
.
dataset
.
size
!==
'large'
;
root
.
dataset
.
size
=
large
?
'large'
:
'normal'
;
try
{
localStorage
.
setItem
(
'imaging-report-size'
,
root
.
dataset
.
size
)}
catch
{}
fontState
();
update
();
say
(
large
?
'已使用较大字号'
:
'已恢复标准字号'
)});
document
.
getElementById
(
'print-button'
).
addEventListener
(
'click'
,()
=>
window
.
print
());
const
printStates
=
new
Map
();
window
.
addEventListener
(
'beforeprint'
,()
=>
{
document
.
querySelectorAll
(
'details'
).
forEach
(
d
=>
{
printStates
.
set
(
d
,
d
.
open
);
d
.
open
=
true
})});
window
.
addEventListener
(
'afterprint'
,()
=>
{
printStates
.
forEach
((
open
,
d
)
=>
d
.
open
=
open
);
printStates
.
clear
()});
const
links
=
[...
document
.
querySelectorAll
(
'.toc a'
)],
sections
=
links
.
map
(
a
=>
document
.
querySelector
(
a
.
getAttribute
(
'href'
)));
let
queued
=
false
;
function
update
(){
const
max
=
document
.
documentElement
.
scrollHeight
-
window
.
innerHeight
;
const
progress
=
max
>
0
?
Math
.
min
(
1
,
Math
.
max
(
0
,
window
.
scrollY
/
max
)):
0
;
document
.
querySelector
(
'.page-progress'
).
style
.
transform
=
'scaleX('
+
progress
+
')'
;
document
.
querySelector
(
'.reading-track span'
).
style
.
width
=
Math
.
round
(
progress
*
100
)
+
'%'
;
document
.
getElementById
(
'progress-value'
).
textContent
=
Math
.
round
(
progress
*
100
)
+
'%'
;
let
index
=
0
;
sections
.
forEach
((
s
,
i
)
=>
{
if
(
s
.
getBoundingClientRect
().
top
<
190
)
index
=
i
});
if
(
progress
>
.
99
)
index
=
links
.
length
-
1
;
links
.
forEach
((
a
,
i
)
=>
{
a
.
classList
.
toggle
(
'active'
,
i
===
index
);
if
(
i
===
index
)
a
.
setAttribute
(
'aria-current'
,
'location'
);
else
a
.
removeAttribute
(
'aria-current'
)});
queued
=
false
}
const
queue
=
()
=>
{
if
(
!
queued
){
queued
=
true
;
requestAnimationFrame
(
update
)}};
window
.
addEventListener
(
'scroll'
,
queue
,{
passive
:
true
});
window
.
addEventListener
(
'resize'
,
queue
);
document
.
querySelectorAll
(
'details'
).
forEach
(
d
=>
d
.
addEventListener
(
'toggle'
,
queue
));
update
();
})();
</script>
</body>
</html>
docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review.md
0 → 100644
View file @
a9248e61
# 美亚 × FridaySaaS × PAC:影像与分析数据方案设计
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。 原方案中的影像事件推送未被采用,当前实现按诊所分页轮询并批量查询分析。
> 内部评审稿 v0.2 · 2026-10-16
> 本轮先确认设计方向与系统边界;字段、表结构、调度周期和开发任务在方案采纳后细化。
**建议边界:美亚管理影像资产,FridaySaaS 管理业务归属并负责对外同步,PAC 消费宿主提供的事实。PAC 核心不直接接美亚。**
判断依据是:
**三个系统可以保存和使用同一份患者与影像关系,但应明确各自数据的权威来源,并让关联及其变更沿统一链路传递。**
FridaySaaS 提供业务患者身份,美亚维护影像资产及其患者映射;SaaS 将两者对应关系校验、留存后提供给 PAC,PAC 据此形成事实和诊断信号。
## 1. 整体关系:数据供给与查看原片分开
**① 关联基础:先明确影像属于谁**
FridaySaaS 向美亚同步患者、诊所和就诊信息,美亚据此关联影像;SaaS 校验并留存对应关系,再向 PAC 提供。
**② 数据供给:同步影像信息与分析结果**
以下以启用美亚新影像为例,表示目标链路:
```
mermaid
flowchart TB
E[诊所设备与前置机] --> M[美亚 PACS]
M -->|存储原片| O[OSS]
M -->|影像信息、分析结果及变更| S["FridaySaaS<br/>校验归属、留存业务记录、对外同步"]
S -->|宿主事实| P["PAC<br/>诊断信号、画像与召回"]
```
原片由美亚管理,SaaS 向 PAC 提供影像业务信息与分析结果。美亚到 SaaS 如何获取变更,仍需确认。
新老影像功能可以保留,实际使用时择一启用;选用旧影像时,也由 SaaS 统一向 PAC 提供数据。
**③ 查看原片:用户按需打开宿主入口**
PAC 宿主槽位 → FridaySaaS 影像页面 → 当前选用的影像系统。
SaaS 负责权限和入口;启用新影像时,由美亚提供阅片服务。
**数据同步持续进行,查看原片由用户按需发起。**
## 2. 美亚:最重要的承诺是“变化可获取”
美亚负责采集接入、原片存储、影像关联、阅片,以及其提供的分析。对外不仅要能查询当前数据,还要让 SaaS 知道数据发生了什么变化。
**如果只能查新影像列表,SaaS 可以知道“多了一张片”,却未必知道这张片后来分析完成、被改绑到另一位患者,或被撤销。下游就可能继续使用过期甚至归属错误的依据。**
因此,对美亚真正要提的要求是:
> 提供可追溯、可补拉的影像及分析变更机制,包含新增、修改、撤销和归属变化。
| 美亚提供的能力 | FridaySaaS 如何获取 | 意味着什么 |
|---|---|---|
| 有事件通知,并能补查变更 | 通知后同步,遗漏时补拉 | 及时性较好,也能恢复遗漏 |
| 没有通知,有完整变更查询 | 定时拉取 | 可以可靠同步,延迟取决于拉取周期 |
| 只有入库时间列表、逐张查询 | 定时发现新片,反复查询分析 | 能做部分同步,但后续修订、改绑、撤销的完整性没有保障 |
## 3. FridaySaaS:留业务记录,统一新老来源
### 3.1 为什么需要留新影像数据
**如果 SaaS 只在用户打开页面时代理查询美亚,它承担的是展示功能,无法承担稳定的下游数据供给。**
没人打开页面时,PAC 仍需要知道患者有了新的影像或分析。
既然 PAC 从 SaaS 接收数据,SaaS 就应保存一份能够重试、重推、核对的影像业务记录:
| 应保存的信息 | 用途 |
|---|---|
| 患者、租户与影像采集诊所 | 明确业务归属;采集诊所不随患者后续转店而改变 |
| 影像来源、影像与检查标识 | 稳定识别、去重和追溯 |
| 类型、拍摄时间、绑定及有效状态 | 解释这份影像的业务含义 |
| 稳定文件引用,如 OSS ID | 后续获取原片;临时链接过期后仍可定位 |
| 分析结果、来源及版本 | 保留当时依据,支持补推和重放 |
| 同步进度与失败记录 | 知道哪些已交付、哪些还需要补齐 |
原片继续由美亚管理。SaaS 保存索引和结果,不重复建设影像存储;临时签名 URL 不能作为永久地址。
**这样即使美亚暂时不可用,SaaS 和 PAC 仍可查询已留存的影像信息和分析结果;原片访问和最新变更同步则可能受影响。**
分析结果会影响后续业务判断,值得留存,而不应仅在查看页面时临时取一次。
美亚接入、获取变更和归属校验由 SaaS 对接层负责;open-connector 从 SaaS 已保存的业务记录向 PAC 推送。供应商协议与下游数据传输各自负责自己的部分。
即使不接 PAC,留存业务记录也能支撑 SaaS 自身业务:将影像与就诊、病历、收费关联,用于检查进度跟进、复诊查阅、经营分析,以及追溯当时使用的分析结果。如果不留存,这些业务就需要反复查询美亚并临时关联数据,受其接口可用性和查询能力限制;分析结果更新或撤销后,若美亚不提供历史版本,SaaS 就难以还原当时使用的依据。
### 3.2 新老影像怎么归一
**业务语义归一,来源实现分开。**
新老功能可以保留,实际择一使用;对下游都应能回答同一组问题:
-
影像属于谁、在哪里拍摄,是什么类型、什么时候拍摄;
-
当前是否有效,怎样查看,有哪些分析结果。
**如果下游直接依赖“老影像的一套字段”和“美亚的另一套字段”,每切换一次影像系统,下游都要跟着改。**
统一表达后,来源切换主要由 SaaS 处理,PAC 继续按宿主事实消费。
但统一不等于强行写入旧表。美亚有检查、切片、绑定状态和外部文件标识;如果旧结构表达不了,不能为了兼容丢掉它们,也不能把临时链接塞进旧文件地址就算完成。是否复用旧表,是方案采纳后的存储设计选择。
如果未来把历史影像迁入美亚,应保留新旧标识的对应关系。
**同一张片换了存储位置,不应被当成一次新的拍摄或重复的分析依据。**
## 4. PAC:用分析形成诊断信号,通过宿主查看影像
PAC 在这条链路中的职责保持两项:
1.
**消费影像分析。**
将宿主提供的有效分析结果作为诊断信号源之一,与其他患者事实共同用于画像与召回,并保留来源依据。
2.
**提供影像查看入口。**
沿用宿主槽位打开 FridaySaaS,由宿主处理新老影像入口、权限和阅片授权。
美亚生成的分析,由 SaaS 接收、留底后推给 PAC。
**如果 PAC 每次处理事实都临时向美亚取结果,重放同一份数据时可能拿到不同内容,就无法解释当时为什么得出那个判断。**
PAC 可以知道数据来源是美亚,但美亚不应成为 PAC 核心的直接依赖。
**否则接入下一家影像供应商时,PAC 又要接一套鉴权、患者映射和同步机制,还可能与 SaaS 的权限及归属规则形成两套判断。**
这些差异应由宿主消化。
如果将来是 PAC 自己发起分析,那是另一项能力:
-
PAC 通过宿主获得授权的文件;
-
PAC 调用自己选择的分析服务;
-
结果标明由 PAC 发起,保留实际分析服务来源及结果版本,并关联原始影像。
## 5. 评审要确定什么,哪些仍需外部确认
本轮评审确认的是职责和数据留存方向:
| 系统 | 对下游承担的承诺 |
|---|---|
| 美亚 | 影像与分析可查询,变更可获取,原片可授权访问 |
| FridaySaaS | 患者归属正确,新老影像统一表达,业务数据可留底、补推和核对 |
| PAC | 消费宿主分析作为诊断信号,通过宿主入口查看影像 |
方案采纳前,优先向美亚确认两件事:
-
**变更怎样获取**
:有无通知;没有通知时,能否完整拉到新增、修订、撤销和归属变化,并补查遗漏。
-
**分析怎样产生和识别**
:何时触发,怎样区分未分析、处理中、失败、完成零发现,以及结果后来是否修改。
这两项决定后续采用通知还是轮询,以及分析能否可靠成为诊断信号。具体字段、存储结构、调度周期和任务拆分,在方向采纳、能力确认后再制定。
## 6. 兜底方案(低优先级)
若 FridaySaaS 暂不留存和同步新影像业务数据,可由
**PAC 宿主适配层直接拉取美亚现有接口**
。采用
**主动轮询、快照对账、幂等更新和采集留底**
:按入库时间发现新增,周期复查历史影像及分析,再转换为 PAC 标准事实。患者归属与授权沿用 SaaS,查看原片仍走宿主入口。
这条路线无需等待美亚推送或 SaaS 新增业务存储,但采集维护成本转至 PAC 适配层;时效受轮询周期限制,无法保证捕获所有中间变更,也无法仅凭现有空结果或查询不到准确判断分析状态及删除。
---
### 依据与核对范围
本稿依据 2026-10-16 已核对的本地代码、SDK 和以下资料;未验证美亚实际部署能力。接口细节存在版本差异,正式对接仍需双方确认。
-
美亚系统集成方案(本机参考文件
`瑞尔口腔影像中心PACS项目系统集成方案v0.4.pdf`
,未随归档分发):第 2–3 章职责及信息同步、第 4–5 章设备与前置机、第 6–7 章阅片与患者映射。
-
美亚私有化部署方案(本机参考文件
`瑞尔PACS影像中心私有化部署方案v0.1.pdf`
,未随归档分发):明确规划阿里云 OSS 和算法资源;规划不代表当前能力已启用。
-
美亚接口文档(本机参考文件
`PACS_瑞尔接口对接文档.pdf`
,未随归档分发):§6.3 分析查询及空结果含义、§6.4/6.8 文件引用与临时 URL。
-
SaaS 代码:PACS 接口(外部仓库:
`friday-saas/emr/src/main/java/com/emr/project/medical/controller/PacsController.java`
)、同步与查询(外部仓库:
`friday-saas/emr/src/main/java/com/emr/project/medical/service/impl/PacsSyncServiceImpl.java`
)、新版影像中心(外部仓库:
`friday-saas-v2/apps/web/features/patient/components/pacs-image-center.tsx`
)。
-
PAC 代码:
[
影像分析事实
](
../../../apps/pac-service/src/modules/sync/pipeline/parsers/image-analysis.parser.ts
)
、
[
宿主影像入口
](
../../../apps/pac-web/src/lib/image-action-url.ts
)
。
资料链接指向本机原件;内部分享时可随报告提供原 PDF。
docs/_archive/2026-10-operations-and-design/2026-10-18-business-date-shanghai-test-validation.md
0 → 100644
View file @
a9248e61
# 下游北京业务日期修复验收(2026-10-18)
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。
## 边界与改动
-
时刻继续以 UTC 保存;下游按
`Asia/Shanghai`
解释业务日期。
-
生日、回访任务日、
`@db.Date`
保持纯日期语义,不将数据库的 UTC 零点载体当成事件时刻。
-
修复上次到诊派生日期、同日就诊计数、同日病历/医嘱关联、未来回访排除、外院治疗回访日期比较、周岁及生日边界、AI 日期话术、患者详情与批次日期展示。
-
到诊分档按北京自然日稳定划分;原有天数阈值不变。下次回访表单按北京时间提交。
-
摄入、源时区转换、事实字段、数据库结构、召回阈值与按实际时长计算的冷却窗口均未修改。
## 本地验证
-
后端完整回归:145 套、2429 项通过;3 套、24 项按原有条件跳过。
-
前端:5 套、77 项通过;前后端 TypeScript 检查通过。
-
实际 PostgreSQL 在 UTC、北京、洛杉矶会话时区分别验证:同日/前日/次日回访、未来回访、生日年龄均一致;事务只读且回滚。
-
Node 三种进程时区验证一致;覆盖北京零点、跨月年、闰日、纯日期、摄入零点锚、到诊 DATE 派生及同日医生关联。
-
两处旧禁忌边界测试改为完整时刻断言:北京 2026-10-28 零点对应
`2026-10-27T16:00:00Z`
。
## 存量与发布范围
-
读取时计算的日期随发布生效;已持久化的上次到诊、画像特征和已生成话术不会因代码发布全部自动改写。
-
历史画像须走已有患者定向强制重算;未运行全库重算、未重摄入事实。部署测试后用真实患者定向核验。
-
本次顺序:功能分支 → test → 测试发布与验证 → 同一功能分支独立合入 main。生产不发布。
## 已完成发布与验收
-
功能分支:
`codex/business-date-shanghai`
,提交
`2e5e22e`
。
-
test 合并及实际部署:
`daff13e`
。
-
main 独立合并:
`b7901c9`
;已确认功能提交同时属于 main/test 历史,未将 test 合入 main。
-
测试发布退出码 0,57 个已有迁移无待执行项;实际容器镜像与新构建镜像一致。
-
测试健康接口返回
`status: ok`
,网页 HTTP 200;前后端容器运行且重启计数为 0。
-
在部署镜像内验证北京零点、周岁边界通过。
-
选取 Friday 测试宿主 3 位真实患者,调用实际到诊派生字段刷新逻辑;日期分别为 2026-10-16、2026-10-13、2026-09-21,与 PostgreSQL 北京日期计算一致。同日去重计数分别为 1、2、1。
-
3 个样本刷新前后事实行摘要一致,未改摄入数据;这些样本原有末诊日期本就正确,不能据此声称修复了全部历史数据。跨日纠错由本地边界用例验证。
-
仅刷新所选患者的到诊派生字段,未运行全量画像或召回重算;已生成话术未批量重建。
-
生产未部署。主目录已有未提交文件、本地服务和美亚工作区未操作。
服务器验收与回退证据保留于测试机
`/root/pac-release-backups/business-date-20261018/`
;本文未包含患者身份与凭据。
docs/_archive/2026-10-operations-and-design/2026-10-18-jvs-complaint-ingestion-prod-validation.md
0 → 100644
View file @
a9248e61
# 5i5ya 投诉摄入:生产发布与真实数据验收
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。
日期:2026-10-18(Asia/Shanghai)
**结论:已合入 main 并部署生产;34 个真实患者身份对应的 75 张反馈单、184 条跟进,落库与重复摄入核验通过。**
## 版本与发布范围
-
功能提交:
`3d29ada`
,分支
`codex/jvs-complaint-ingestion`
。
-
main 合并及生产版本:
`1793c1bcdd43ea70d420a5d00c549d1c9503fd59`
。
-
test 保持已验收版本
`c9360bcb853c3cd7d20ad1201e5e82c1e243a96b`
。
-
从功能分支合入 main,没有将 test 上的其他功能带入生产。
-
FridaySaaS 投诉未接入,召回规则未修改;本次没有新增数据库迁移。
-
沿用此前同一功能提交的本地测试结果:143 组、2,411 项通过,另行真实样本及隔离 PostgreSQL 验证 26 项通过。本轮执行生产构建及真实数据库验收。
## 发布前任务与部署
发布准备时摄入已结束,队列没有执行中的任务,但数据库仍有召回筛选长查询。不能仅凭摄入账本和队列为空判断系统空闲。用户明确允许中断本轮后,执行应用重建。
构建最初因 Dockerfile 外部解析器镜像下载失败而中止;使用删除首行解析器指令的临时 Dockerfile 副本,交由 Docker 内置解析器完成构建。其余构建指令、源码及生产构建参数保持一致,未修改仓库 Dockerfile。发布采用既有脚本的迁移、强制重建及验收流程,构建步骤替换为校验已构建镜像 ID 和固定提交。
生产约 17:21 完成应用替换:
-
57 份现有迁移,无待执行迁移。
-
service 镜像:
`1a258da2c107d5c2c2320a8075f27f5feb306791352f454ab3c1f5fbdfb830c3`
。
-
web 镜像:
`96824485987118fba9bac734b1c943134ec64e1b2018f820f5920361358f8283`
。
-
两容器实际镜像均与构建结果一致,检查时重启次数均为 0。
-
`/health`
返回
`status: ok`
;前端本机、私网及生产公网
`/plans`
均为 HTTP 200。
-
使用 managed 配置,只替换 PAC 应用容器,未重启托管 PostgreSQL/Redis。
-
旧版本
`76e7d5b`
和旧镜像保留,私有发布证据:
`/data/pac-release-backups/complaint-20261018/`
。
## 真实数仓到生产 PAC
发布后通过生产环境连接重新只读查询
`dw_group.fact_complain_out`
,使用正式
`sync-incremental`
流程摄入。患者范围限定为 34 个品牌+患者身份,单批 50、并发 1,使用
`--no-recompute`
,未另启全量画像或召回计算。
| 验收项 | 结果 |
|---|---:|
| 源投诉及跟进行 | 194 |
| 聚合反馈单 | 75 |
| 有效未完成投诉 | 4 |
| 有效已完成投诉 | 20 |
| 非有效快照 | 51 |
| 跟进记录 | 184 |
| 逐条落库一致 | 75 / 75 |
| 投诉事务 / 事实版本 | 75 / 75 |
| 归属、内容、状态、时间差异 | 0 |
51 张非有效快照包括表扬、中性、未知或草稿等,保留为
`invalidated`
,不作为有效投诉信号。
首轮于 17:22:45 完成,cohort 处理 9.392 秒,运行 ID
`5c06e2f9-89e4-4712-b923-08ddff41c848`
。正式 cohort 会同时处理这些患者的其他已有资源:整批新增 360 条事务、80 条事实,其中投诉为 75 条;整批及事实失败数均为 0。
逐条核对患者外部 ID、租户、品牌、诊所、状态、正文与标准字段、发生时间、来源版本时间和完整跟进;另以显式
`+08:00`
独立转换源时间,75 张反馈单及 184 条跟进均无差异。
第二轮于 17:23:12 完成,cohort 处理 7.333 秒,运行 ID
`1a30e0f6-f96f-45ef-8db8-61f72064e969`
。整批新增事务为 0、事实新增及替换均为 0;投诉仍为 75 条事务和 75 个事实版本,事实指纹不变,幂等验收通过。
两轮均确认全局增量游标未因抽样验收而推进。原始样本、患者清单和核对证据仅留在私有验收目录,不进入 Git。
## 后续自动摄入与边界
生产已注册
`jvs-dw`
常规调度:
`15 0,8-22/2 * * *`
,时区 Asia/Shanghai;验收结束后的下一次计划为当日 18:15。投诉历史发现由常规调度继续执行,本次 34 人验收不代表全量投诉已补齐。
最终检查无执行中的摄入、画像或召回账本记录,无活动数据库查询;画像队列另有 1 条延迟任务。未手动启动被中断召回轮次的全量重算。
源端没有 ETL 入仓时间及删除标记,窗口外迟到、时间字段完全不变的原地修订和物理删除仍属于既有采集边界。本次未增加对账机制。
主目录未提交文档、本地服务和美亚工作区未改动。本验收报告留在投诉独立工作区,暂未提交。
docs/_archive/2026-10-operations-and-design/2026-10-18-jvs-complaint-ingestion-test-validation.md
0 → 100644
View file @
a9248e61
# 5i5ya 投诉摄入:测试发布与真实数据验收
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。
日期:2026-10-18(Asia/Shanghai)
**结论:已合入 test 并部署测试环境;34 个真实患者身份、75 张反馈单的两轮摄入与逐条核验通过。main 和生产未变更。**
## 发布版本与范围
-
功能分支:
`codex/jvs-complaint-ingestion`
。
-
功能提交:
`3d29ada`
。
-
test 合并提交及服务器运行版本:
`c9360bcb853c3cd7d20ad1201e5e82c1e243a96b`
。
-
main 保持:
`76e7d5bc69e98d9f61d4a967c131c4a58d3d0207`
。
-
FridaySaaS 投诉未接入;召回规则、画像提取器、免打扰规则未修改。
-
主目录未提交文件和本地服务未改动。美亚工作区未改动。
复查重点:投诉按配置命名空间聚合,沿用宿主患者身份;变化发现与完整跟进读取分开;新资源首轮发现历史患者;北京时间统一转 UTC;非负面、草稿和未知数据保存无效快照;重复和旧快照重放不恢复已撤销的投诉效力。
## 本次本地验证
-
回归测试:143 组、2,411 项通过;24 项按配置跳过。
-
另行显式启用真实源样本和独立 PostgreSQL 验证:2 组、26 项通过;SQL 导出辅助项未启用。
-
`@pac/types`
构建通过。
-
后端源码与测试的类型检查通过。
-
提交前差异检查通过。
真实样本及数据库专项验证覆盖同时间分类修订、旧版本重放、跨品牌同号、身份冲突、完整跟进和 UTC 转换。测试库与本地应用数据库隔离。
## 测试发布检查
发布前测试服务器在 test 分支,工作目录干净;健康接口正常,磁盘剩余约 111 GB;摄入账本无 running 任务,上轮摄入已于 16:34:36 完成。
使用现有发布脚本完成镜像构建、迁移检查和应用容器替换:
-
57 份现有迁移,无待执行迁移;本改动无新表和迁移。
-
service 镜像:
`c5932b8708e0c39d85e5b230435c267745a66f3119d6c96a69c6524f063ba8bd`
。
-
web 镜像:
`15746e42828714c27e8866ba2b6446f778452fff2bb7c0ce561c975c9b8c60ff`
。
-
容器实际镜像与构建结果一致;最终检查两容器重启次数均为 0。
-
`/health`
返回
`status: ok`
,前端 HTTP 200。
-
旧提交和镜像已留存,测试服务器发布证据位于
`/root/pac-release-backups/complaint-20261018/`
(私有目录)。
## 真实数仓 → 测试 PAC
从测试容器重新只读查询数仓;不是将本地造数写入测试库。
| 验收内容 | 实际结果 |
|---|---:|
| 患者范围(品牌+患者 ID) | 34 |
| 原始投诉及跟进行 | 194 |
| 聚合后的反馈单 | 75 |
| 有效、未完成投诉 | 4 |
| 有效、已完成投诉 | 20 |
| 非有效投诉快照 | 51 |
| 保留的跟进 | 184 |
| 逐条事实核验一致 | 75 / 75 |
| 投诉事务 | 75 |
| 投诉事实版本 | 75 |
| 核验差异 | 0 |
非有效快照包括表扬、中性、未知或草稿等,均为
`invalidated`
、
`signal_eligible=false`
,不计入有效投诉。有效投诉合计 24 张。
通过正式
`sync-incremental`
CLI,使用限定的患者清单、单批 50 人、并发 1、
`--no-recompute`
运行两轮。沿用完整宿主 cohort 管线,因此同批患者其他资源也按已有机制正常摄入;没有另开全量画像或召回计算。
首轮:16:47:20 完成,cohort 处理约 9.5 秒;整批失败数和事实失败数均为 0。首轮运行 ID:
`d4414cca-4495-42df-9dfc-51d47777471d`
。
核对包括:患者外部 ID、租户、品牌、投诉诊所、状态、正文及标准字段、发生时间、版本时间和完整跟进内容。另用显式
`+08:00`
独立转换源时间,75 张反馈单与 184 条跟进全部通过,未依赖服务器默认时区。
第二轮:投诉事务仍为 75,事实版本仍为 75,全部事实的内容指纹与第一轮一致,无重复新增或版本抖动。
两轮均保持原全局增量游标不变,投诉首轮全量发现水位仍未被抽样验收提前推进。其余患者的存量发现由后续常规增量调度处理;本次验收不代表全量投诉已经补齐。
## 已知边界
源端没有 ETL 入仓时间或删除标记。现有回看机制仍无法保证发现窗口外的迟到回填、全部时间字段不变的原地修订或物理删除。本次未扩展为对账任务,也未把投诉信号加入召回决策。
原始数据、患者清单及逐条比对证据保存在私有验收目录,不进入 Git;本报告仅记录汇总和运行证据。
docs/_archive/2026-10-operations-and-design/2026-10-19-meyer-friday-pac-imaging-review-document.html
0 → 100644
View file @
a9248e61
<!doctype html>
<html
lang=
"zh-CN"
><head><meta
charset=
"utf-8"
><meta
name=
"viewport"
content=
"width=device-width,initial-scale=1"
><meta
name=
"color-scheme"
content=
"light dark"
><meta
name=
"description"
content=
"美亚、FridaySaaS 与 PAC 的影像及患者分析方案,评审修订稿 v0.4。"
><title>
影像与分析数据方案设计 · v0.4
</title><style>
:root
{
color-scheme
:
light
;
--ground
:
#f4f6fb
;
--surface
:
#fff
;
--sunken
:
#eaeef6
;
--ink
:
#0f1b33
;
--ink-2
:
#44506a
;
--ink-3
:
#6f7a91
;
--brand
:
#0032a0
;
--brand-soft
:
#e3eafa
;
--rule
:
#dde3ee
;
--note
:
#f5f7fc
;
--warn-bg
:
#fff9ed
;
--warn-rule
:
#d6ae63
;
--serif
:
"Noto Serif SC"
,
"Songti SC"
,
"STSong"
,
serif
;
--sans
:
"Noto Sans SC"
,
"PingFang SC"
,
"Microsoft YaHei"
,
system-ui
,
sans-serif
;
--mono
:
"IBM Plex Mono"
,
ui-monospace
,
"SFMono-Regular"
,
Menlo
,
monospace
}
:root
[
data-theme
=
dark
]
{
color-scheme
:
dark
;
--ground
:
#0a1020
;
--surface
:
#121a2d
;
--sunken
:
#1a2440
;
--ink
:
#e7ecf6
;
--ink-2
:
#b3bdd2
;
--ink-3
:
#8994ab
;
--brand
:
#86a7ff
;
--brand-soft
:
#1d2b52
;
--rule
:
#2a3550
;
--note
:
#172037
;
--warn-bg
:
#292518
;
--warn-rule
:
#a38a55
}
@media
(
prefers-color-scheme
:
dark
){
:root:not
([
data-theme
=
light
])
{
color-scheme
:
dark
;
--ground
:
#0a1020
;
--surface
:
#121a2d
;
--sunken
:
#1a2440
;
--ink
:
#e7ecf6
;
--ink-2
:
#b3bdd2
;
--ink-3
:
#8994ab
;
--brand
:
#86a7ff
;
--brand-soft
:
#1d2b52
;
--rule
:
#2a3550
;
--note
:
#172037
;
--warn-bg
:
#292518
;
--warn-rule
:
#a38a55
}}
*
{
box-sizing
:
border-box
}
html
{
scroll-behavior
:
smooth
}
body
{
margin
:
0
;
background
:
var
(
--ground
);
color
:
var
(
--ink
);
font-family
:
var
(
--sans
);
font-size
:
16px
;
line-height
:
1.9
;
-webkit-font-smoothing
:
antialiased
}
::selection
{
background
:
var
(
--brand-soft
);
color
:
var
(
--ink
)}
a
{
color
:
var
(
--brand
);
text-decoration-color
:
color-mix
(
in
srgb
,
var
(
--brand
)
35%
,
transparent
);
text-underline-offset
:
4px
}
a
:hover
{
text-decoration-color
:
var
(
--brand
)}
button
{
font
:
inherit
}
a
:focus-visible
,
button
:focus-visible
{
outline
:
2px
solid
var
(
--brand
);
outline-offset
:
4px
}
.wrap
{
max-width
:
1040px
;
margin
:
auto
;
padding
:
40px
26px
48px
}
.document-header
{
padding
:
0
8px
28px
}
.topline
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
16px
;
align-items
:
center
;
margin-bottom
:
16px
}
.eyebrow
{
font-size
:
12px
;
letter-spacing
:
.10em
;
color
:
var
(
--brand
);
font-weight
:
700
}
.tools
{
display
:
flex
;
gap
:
4px
}
.tools
button
{
border
:
0
;
color
:
var
(
--ink-3
);
background
:
transparent
;
padding
:
5px
8px
;
cursor
:
pointer
;
font-size
:
12px
;
border-radius
:
5px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
.tools
button
:hover
{
background
:
var
(
--brand-soft
);
color
:
var
(
--brand
)}
.tools
svg
{
width
:
15px
;
height
:
15px
}
h1
{
font-family
:
var
(
--serif
);
font-weight
:
700
;
font-size
:
clamp
(
30px
,
3.8vw
,
42px
);
line-height
:
1.35
;
letter-spacing
:
.015em
;
margin
:
0
0
14px
}
.meta
{
display
:
flex
;
align-items
:
center
;
flex-wrap
:
wrap
;
gap
:
9px
;
color
:
var
(
--ink-3
);
font-size
:
12px
;
margin-bottom
:
14px
}
.meta
.version
{
background
:
var
(
--brand-soft
);
color
:
var
(
--brand
);
border-radius
:
99px
;
padding
:
1px
10px
;
font-weight
:
500
}
.meta
time
{
font-family
:
var
(
--mono
);
font-size
:
12px
}
.scope
{
max-width
:
48em
;
color
:
var
(
--ink-2
);
font-size
:
14px
;
line-height
:
1.8
;
margin
:
0
}
main
{
background
:
var
(
--surface
);
border-radius
:
14px
;
padding
:
42px
58px
40px
}
p
{
margin
:
0
0
18px
;
color
:
var
(
--ink-2
)}
strong
{
color
:
var
(
--ink
);
font-weight
:
600
}
p
:last-child
{
margin-bottom
:
0
}
.boundary
{
padding
:
0
0
0
20px
;
border-left
:
3px
solid
var
(
--brand
);
font-size
:
18px
;
line-height
:
1.85
;
margin-bottom
:
24px
}
.boundary
strong
{
font-weight
:
600
}
.principle
{
font-size
:
15.5px
;
line-height
:
1.9
;
margin-bottom
:
30px
}
.contents
{
border-top
:
1px
solid
var
(
--rule
);
border-bottom
:
1px
solid
var
(
--rule
);
padding
:
17px
0
19px
;
margin
:
32px
0
0
}
.contents-label
{
color
:
var
(
--ink-3
);
font-size
:
11px
;
letter-spacing
:
.14em
;
display
:
block
;
margin-bottom
:
9px
}
.contents-links
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
gap
:
7px
18px
}
.contents
a
{
text-decoration
:
none
;
color
:
var
(
--ink-2
);
font-size
:
13px
;
white-space
:
nowrap
}
.contents
a
:hover
{
color
:
var
(
--brand
)}
.contents
a
span
{
font-family
:
var
(
--mono
);
display
:
inline-block
;
width
:
29px
;
color
:
var
(
--brand
);
font-size
:
11px
}
.chapter
{
padding-top
:
46px
;
scroll-margin-top
:
28px
}
.chapter
+
.chapter
{
margin-top
:
42px
;
border-top
:
1px
solid
var
(
--rule
)}
.chapter-head
{
display
:
flex
;
align-items
:
baseline
;
gap
:
15px
;
margin-bottom
:
24px
}
.chapter-num
{
color
:
var
(
--brand
);
font-family
:
var
(
--mono
);
font-size
:
15px
;
line-height
:
1.3
;
flex
:
none
}
.chapter
h2
{
font-family
:
var
(
--serif
);
font-size
:
26px
;
line-height
:
1.45
;
font-weight
:
700
;
margin
:
0
;
text-wrap
:
pretty
}
.chapter
h3
{
font-size
:
18px
;
line-height
:
1.6
;
color
:
var
(
--ink
);
margin
:
32px
0
14px
;
font-weight
:
600
}
.chapter-head
+
h3
{
margin-top
:
0
}
.step-heading
{
display
:
flex
;
gap
:
8px
}
.step-heading
span
{
color
:
var
(
--brand
);
font-weight
:
500
}
.figure-intro
{
font-size
:
14px
;
margin-bottom
:
12px
}
ul
,
ol
{
padding-left
:
23px
;
color
:
var
(
--ink-2
);
margin
:
14px
0
22px
}
li
{
padding-left
:
3px
;
margin
:
7px
0
}
li
::marker
{
color
:
var
(
--brand
)}
blockquote
{
margin
:
24px
0
;
padding
:
20px
24px
;
background
:
var
(
--brand-soft
);
border-left
:
3px
solid
var
(
--brand
)}
blockquote
p
{
color
:
var
(
--ink
);
font-family
:
var
(
--serif
);
font-size
:
20px
;
line-height
:
1.75
;
font-weight
:
600
}
.consequence
{
border-left
:
3px
solid
var
(
--warn-rule
);
background
:
var
(
--warn-bg
);
padding
:
17px
21px
;
margin
:
22px
0
}
.consequence
strong
{
font-weight
:
500
}
.own-business
{
border-top
:
1px
dashed
var
(
--rule
);
padding-top
:
20px
;
margin-top
:
24px
}
.future
{
margin
:
28px
0
0
;
padding
:
18px
23px
;
background
:
var
(
--note
);
font-size
:
14px
;
border-left
:
2px
solid
var
(
--rule
)}
.future
p
{
margin-bottom
:
8px
}
.future
ul
{
margin
:
7px
0
0
}
.future
li
{
margin
:
4px
0
}
.duties
{
list-style
:
none
;
padding
:
0
;
counter-reset
:
duty
;
margin
:
20px
0
24px
}
.duties
li
{
counter-increment
:
duty
;
position
:
relative
;
padding
:
15px
0
15px
42px
;
margin
:
0
;
border-top
:
1px
solid
var
(
--rule
)}
.duties
li
:last-child
{
border-bottom
:
1px
solid
var
(
--rule
)}
.duties
li
::before
{
content
:
'0'
counter
(
duty
);
position
:
absolute
;
left
:
0
;
color
:
var
(
--brand
);
font
:
14px
var
(
--mono
);
top
:
21px
}
.questions
{
padding-left
:
23px
}
.questions
li
{
margin
:
12px
0
}
.questions
strong
{
color
:
var
(
--brand
)}
.table-wrap
{
margin
:
22px
0
26px
}
table
{
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
14px
;
line-height
:
1.8
;
text-align
:
left
}
th
{
font-weight
:
600
;
color
:
var
(
--ink
);
background
:
var
(
--sunken
);
padding
:
12px
16px
;
border-bottom
:
1px
solid
var
(
--rule
);
vertical-align
:
top
}
td
{
padding
:
13px
16px
;
border-bottom
:
1px
solid
var
(
--rule
);
vertical-align
:
top
;
color
:
var
(
--ink-2
)}
tbody
td
:first-child
{
font-weight
:
500
;
color
:
var
(
--ink
)}
.two-col
td
:first-child
{
width
:
37%
}
.three-col
th
:nth-child
(
1
)
{
width
:
33%
}
.three-col
th
:nth-child
(
2
)
{
width
:
28%
}
.three-col
th
:nth-child
(
3
)
{
width
:
39%
}
#review
td
:first-child
{
width
:
22%
;
color
:
var
(
--brand
);
font-weight
:
600
}
figure
{
margin
:
0
}
.flow
{
padding
:
20px
24px
22px
;
border
:
1px
solid
var
(
--rule
);
border-radius
:
9px
;
margin
:
14px
0
22px
;
background
:
var
(
--note
)}
figcaption
{
font-size
:
13px
;
font-weight
:
600
;
color
:
var
(
--ink
);
display
:
flex
;
gap
:
10px
;
align-items
:
center
;
flex-wrap
:
wrap
;
padding-bottom
:
18px
}
figcaption
>
span
{
font-size
:
11px
;
font-family
:
var
(
--mono
);
color
:
var
(
--brand
);
font-weight
:
400
}
figcaption
>
small
{
font-size
:
11px
;
font-weight
:
400
;
color
:
var
(
--ink-3
);
margin-left
:
auto
}
.flow-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
)
104px
minmax
(
0
,
1.1
fr
)
65px
minmax
(
0
,
1
fr
);
grid-template-rows
:
auto
auto
auto
;
align-items
:
center
}
.capture
{
grid-column
:
1
;
grid-row
:
1
;
text-align
:
center
;
font-size
:
11px
;
color
:
var
(
--ink-3
);
line-height
:
1.7
;
align-self
:
end
;
padding
:
0
0
7px
}
.capture
>
span
{
display
:
block
;
color
:
var
(
--ink-3
);
font-size
:
20px
;
line-height
:
1.3
}
.system
{
grid-row
:
2
;
background
:
var
(
--surface
);
border
:
1px
solid
var
(
--rule
);
border-top
:
2px
solid
var
(
--brand
);
padding
:
16px
8px
;
text-align
:
center
;
align-self
:
stretch
;
display
:
flex
;
flex-direction
:
column
;
justify-content
:
center
;
min-width
:
0
}
.meyer
{
grid-column
:
1
}
.saas
{
grid-column
:
3
;
background
:
var
(
--brand-soft
)}
.pac
{
grid-column
:
5
}
.system-label
{
color
:
var
(
--ink-3
);
font-size
:
10px
;
letter-spacing
:
.10em
;
line-height
:
1.5
;
margin-bottom
:
6px
}
.system
>
strong
{
font-size
:
18px
;
line-height
:
1.5
;
font-weight
:
600
;
white-space
:
nowrap
}
.system
>
small
{
font-size
:
11px
;
color
:
var
(
--ink-2
);
margin-top
:
6px
;
line-height
:
1.7
}
.connector
{
grid-row
:
2
;
text-align
:
center
;
color
:
var
(
--brand
);
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
8px
;
padding
:
0
5px
}
.connector.first
{
grid-column
:
2
}
.connector.second
{
grid-column
:
4
}
.connector
>
span
{
font-size
:
11px
;
line-height
:
1.6
}
.connector
svg
{
width
:
38px
;
height
:
12px
;
flex
:
none
}
.connector
small
{
font-size
:
10px
;
color
:
var
(
--ink-3
);
line-height
:
1.5
}
.storage
{
grid-column
:
1
;
grid-row
:
3
;
text-align
:
center
;
color
:
var
(
--ink-3
);
font-size
:
13px
}
.storage
>
span
{
display
:
block
;
line-height
:
1.4
;
font-size
:
20px
}
.storage
>
div
{
border
:
1px
dashed
var
(
--rule
);
background
:
var
(
--surface
);
padding
:
4px
10px
}
.storage
small
{
font-size
:
11px
;
margin-left
:
5px
}
.mobile-only
{
display
:
none
}
.view-path
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
12px
;
padding
:
19px
20px
;
border-top
:
1px
solid
var
(
--rule
);
border-bottom
:
1px
solid
var
(
--rule
);
margin
:
18px
0
;
color
:
var
(
--ink
);
font-size
:
14px
;
font-weight
:
500
}
.view-path
b
{
color
:
var
(
--brand
);
font-size
:
18px
;
font-weight
:
400
;
line-height
:
1
}
.sources
{
margin-top
:
44px
;
padding-top
:
25px
;
border-top
:
1px
solid
var
(
--rule
);
scroll-margin-top
:
28px
;
font-size
:
12.5px
;
color
:
var
(
--ink-3
);
line-height
:
1.85
}
.sources
h2
{
font-size
:
14px
;
font-weight
:
600
;
margin
:
0
0
12px
}
.sources
p
{
color
:
var
(
--ink-3
);
margin-bottom
:
13px
}
.sources
ul
{
padding-left
:
19px
;
margin
:
14px
0
17px
}
.sources
li
{
padding-left
:
1px
;
margin
:
8px
0
;
color
:
var
(
--ink-3
)}
.sources
a
{
font-weight
:
500
}
.page-footer
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
12px
;
color
:
var
(
--ink-3
);
font-size
:
11px
;
padding
:
19px
8px
0
}
.page-footer
a
{
text-decoration
:
none
;
color
:
var
(
--ink-3
)}
.back-top
{
position
:
fixed
;
right
:
24px
;
bottom
:
22px
;
z-index
:
10
;
border
:
1px
solid
var
(
--rule
);
border-radius
:
99px
;
padding
:
8px
14px
;
background
:
var
(
--surface
);
color
:
var
(
--brand
);
font-size
:
12px
;
text-decoration
:
none
;
box-shadow
:
0
2px
16px
#00000006
;
opacity
:
0
;
pointer-events
:
none
;
transition
:
opacity
.2s
}
.back-top.visible
{
opacity
:
1
;
pointer-events
:
auto
}
.skip-link
{
position
:
absolute
;
top
:
-80px
;
left
:
20px
;
background
:
var
(
--surface
);
padding
:
10px
;
z-index
:
20
}
.skip-link
:focus
{
top
:
10px
}
@media
(
min-width
:
1440px
){
.wrap
{
padding-top
:
48px
}}
@media
(
max-width
:
900px
){
.wrap
{
padding
:
26px
22px
32px
}
main
{
padding
:
34px
35px
}
.chapter
h2
{
font-size
:
24px
}
.flow
{
padding
:
18px
16px
}
.flow-grid
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
90px
minmax
(
0
,
1.1
fr
)
56px
minmax
(
0
,
1
fr
)}
.system
>
strong
{
font-size
:
16px
}
.system
>
small
{
font-size
:
9px
}
.system-label
{
font-size
:
9px
}
.connector
>
span
{
font-size
:
9px
}
.connector
small
{
font-size
:
8px
}}
@media
(
max-width
:
660px
){
body
{
font-size
:
15px
;
line-height
:
1.85
}
.wrap
{
padding
:
22px
15px
28px
}
.document-header
{
padding
:
0
4px
23px
}
.topline
{
align-items
:
flex-start
;
gap
:
8px
;
margin-bottom
:
13px
}
.eyebrow
{
font-size
:
10px
;
letter-spacing
:
.04em
;
padding-top
:
7px
}
.tools
{
gap
:
0
;
flex
:
none
}
.tools
button
{
padding
:
7px
}
.tools
button
span
{
display
:
none
}
.tools
svg
{
width
:
16px
;
height
:
16px
}
h1
span
{
display
:
block
}
h1
{
font-size
:
31px
;
line-height
:
1.4
;
max-width
:
11em
;
text-wrap
:
balance
;
margin-bottom
:
13px
}
.meta
{
font-size
:
11px
;
margin-bottom
:
12px
}
.meta
time
{
font-size
:
11px
}
.scope
{
font-size
:
13px
;
line-height
:
1.8
}
main
{
padding
:
27px
22px
29px
;
border-radius
:
10px
}
.boundary
{
font-size
:
17px
;
line-height
:
1.85
;
padding-left
:
15px
;
margin-bottom
:
21px
}
.principle
{
font-size
:
14px
}
.contents
{
margin-top
:
25px
;
padding
:
15px
0
17px
}
.contents-links
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
gap
:
7px
12px
}
.contents
a
{
font-size
:
12px
}
.contents
a
span
{
width
:
25px
}
.chapter
{
padding-top
:
32px
;
scroll-margin-top
:
18px
}
.chapter
+
.chapter
{
margin-top
:
30px
}
.chapter-head
{
gap
:
10px
;
margin-bottom
:
21px
}
.chapter-num
{
font-size
:
12px
}
.chapter
h2
{
font-size
:
22px
;
line-height
:
1.5
}
.chapter
h3
{
font-size
:
16px
;
margin-top
:
28px
}
.chapter-head
+
h3
{
margin-top
:
0
}
.consequence
{
padding
:
14px
16px
}
.chapter
blockquote
{
padding
:
16px
18px
;
margin
:
22px
0
}
.chapter
blockquote
p
{
font-size
:
18px
;
line-height
:
1.8
}
.table-wrap
{
margin
:
22px
0
}
.table-wrap
thead
{
display
:
none
}
.table-wrap
table
,
.table-wrap
tbody
{
display
:
block
}
.table-wrap
tr
{
display
:
block
;
padding
:
13px
0
;
border-bottom
:
1px
solid
var
(
--rule
)}
.table-wrap
tr
:first-child
{
border-top
:
2px
solid
var
(
--brand
)}
.table-wrap
td
{
display
:
block
;
padding
:
3px
0
;
border
:
0
;
width
:
auto
!important
;
line-height
:
1.85
;
font-size
:
13px
}
.table-wrap
td
:first-child
{
font-size
:
14px
;
font-weight
:
600
;
padding
:
0
0
6px
;
color
:
var
(
--ink
)}
.three-col
td
:not
(
:first-child
)
{
display
:
grid
;
grid-template-columns
:
6.4em
1
fr
;
gap
:
10px
}
.three-col
td
:not
(
:first-child
)
::before
{
content
:
attr
(
data-label
);
font-size
:
11px
;
color
:
var
(
--ink-3
);
padding-top
:
2px
}
#review
td
:first-child
{
color
:
var
(
--brand
)}
.flow
{
padding
:
16px
;
margin
:
13px
0
20px
}
figcaption
{
font-size
:
12px
;
gap
:
8px
;
padding-bottom
:
18px
}
figcaption
>
small
{
display
:
block
;
flex-basis
:
100%
;
font-size
:
10px
;
margin
:
0
}
.flow-grid
{
display
:
flex
;
flex-direction
:
column
;
gap
:
0
}
.capture
{
font-size
:
11px
;
align-self
:
center
;
padding-bottom
:
4px
}
.system
{
align-self
:
stretch
;
padding
:
14px
;
min-height
:
95px
}
.system
>
strong
{
font-size
:
19px
}
.system
>
small
{
font-size
:
11px
}
.system-label
{
font-size
:
10px
;
margin-bottom
:
3px
}
.connector
{
padding
:
10px
0
;
gap
:
4px
}
.connector
svg
{
transform
:
rotate
(
90deg
);
width
:
28px
;
height
:
12px
;
margin
:
6px
0
}
.connector
>
span
{
font-size
:
11px
}
.connector
>
span
br
{
display
:
none
}
.connector
small
{
font-size
:
10px
}
.storage
{
margin-top
:
16px
;
align-self
:
stretch
;
font-size
:
12px
}
.storage
>
span
{
display
:
none
}
.storage
>
div
{
padding
:
7px
5px
;
background
:
transparent
}
.storage
small
{
font-size
:
11px
}
.mobile-only
{
display
:
inline
;
font-weight
:
400
}
.view-path
{
flex-direction
:
column
;
gap
:
7px
;
padding
:
16px
;
font-size
:
13px
;
align-items
:
flex-start
}
.view-path
b
{
transform
:
rotate
(
90deg
);
margin-left
:
7px
;
font-size
:
17px
;
line-height
:
1.1
}
.duties
li
{
padding-left
:
33px
}
.duties
li
::before
{
font-size
:
12px
;
top
:
20px
}
.future
{
font-size
:
13px
;
padding
:
15px
17px
}
.sources
{
margin-top
:
32px
;
font-size
:
11.5px
}
.sources
h2
{
font-size
:
13px
}
.page-footer
{
font-size
:
10px
;
flex-direction
:
column
;
gap
:
3px
;
padding
:
15px
4px
0
}
.back-top
{
right
:
12px
;
bottom
:
12px
;
font-size
:
11px
;
padding
:
6px
11px
}}
@media
(
max-width
:
350px
){
.wrap
{
padding-inline
:
10px
}
main
{
padding-inline
:
17px
}
h1
{
font-size
:
29px
}
.eyebrow
{
font-size
:
9px
}
.boundary
{
font-size
:
16px
}
.chapter
h2
{
font-size
:
20px
}
.flow
{
padding-inline
:
12px
}}
@media
(
prefers-reduced-motion
:
reduce
){
html
{
scroll-behavior
:
auto
}
*
{
transition
:
none
!important
}}
@page
{
size
:
A4
;
margin
:
16mm
17mm
17mm
}
@media
print
{
html
{
scroll-behavior
:
auto
}
:root
,
:root
[
data-theme
=
dark
]
{
color-scheme
:
light
;
--ground
:
#fff
;
--surface
:
#fff
;
--sunken
:
#edf0f6
;
--ink
:
#0f1b33
;
--ink-2
:
#33415a
;
--ink-3
:
#69758a
;
--brand
:
#0032a0
;
--brand-soft
:
#edf1fa
;
--rule
:
#d5dce9
;
--note
:
#f8f9fc
;
--warn-bg
:
#fffaf0
;
--warn-rule
:
#c1a26b
}
body
{
background
:
white
;
font-size
:
10pt
;
line-height
:
1.75
;
-webkit-print-color-adjust
:
exact
;
print-color-adjust
:
exact
}
.wrap
{
padding
:
0
;
max-width
:
none
}
.document-header
{
padding
:
0
0
17pt
}
.topline
{
margin-bottom
:
9pt
}
.eyebrow
{
font-size
:
8pt
}
h1
{
font-size
:
25pt
;
margin-bottom
:
8pt
}
.meta
{
margin-bottom
:
7pt
;
font-size
:
8pt
}
.meta
time
{
font-size
:
8pt
}
.scope
{
font-size
:
9pt
}
.tools
,
.back-top
,
.skip-link
{
display
:
none
!important
}
main
{
padding
:
0
;
border-radius
:
0
}
.boundary
{
font-size
:
11.5pt
;
margin-bottom
:
13pt
;
padding-left
:
12pt
}
.principle
{
font-size
:
9.5pt
;
margin-bottom
:
17pt
}
.contents
{
margin-top
:
17pt
;
padding
:
10pt
0
}
.contents-links
{
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
4pt
10pt
}
.contents
a
{
font-size
:
8pt
}
.contents-label
{
font-size
:
7pt
;
margin-bottom
:
5pt
}
.chapter
{
padding-top
:
24pt
}
.chapter
+
.chapter
{
margin-top
:
23pt
}
.chapter-head
{
gap
:
10pt
;
margin-bottom
:
13pt
;
break-after
:
avoid
}
.chapter
h2
{
font-size
:
17pt
;
line-height
:
1.45
}
.chapter-num
{
font-size
:
10pt
}
.chapter
h3
{
font-size
:
11.5pt
;
margin
:
19pt
0
9pt
;
break-after
:
avoid
}
.chapter-head
+
h3
{
margin-top
:
0
}
p
{
margin-bottom
:
10pt
;
orphans
:
3
;
widows
:
3
}
li
{
margin
:
4pt
0
}
ul
,
ol
{
margin
:
9pt
0
13pt
}
blockquote
{
margin
:
13pt
0
;
padding
:
12pt
16pt
;
break-inside
:
avoid
}
blockquote
p
{
font-size
:
12pt
}
.consequence
{
padding
:
10pt
14pt
;
margin
:
12pt
0
;
break-inside
:
avoid
}
.figure-intro
{
font-size
:
9pt
;
break-after
:
avoid
;
margin-bottom
:
7pt
}
.flow
{
margin
:
8pt
0
13pt
;
padding
:
12pt
14pt
;
break-inside
:
avoid
}
.flow-grid
{
grid-template-columns
:
minmax
(
0
,
1
fr
)
85px
minmax
(
0
,
1.1
fr
)
55px
minmax
(
0
,
1
fr
)}
figcaption
{
font-size
:
8pt
;
padding-bottom
:
12pt
}
figcaption
>
span
{
font-size
:
7pt
}
figcaption
>
small
{
font-size
:
7pt
}
.capture
{
font-size
:
7pt
}
.system
{
padding
:
10pt
5pt
}
.system-label
{
font-size
:
6.5pt
}
.system
>
strong
{
font-size
:
12pt
}
.system
>
small
{
font-size
:
6.5pt
}
.connector
>
span
{
font-size
:
6.5pt
}
.connector
small
{
font-size
:
5.5pt
}
.storage
{
font-size
:
8pt
}
.storage
small
{
font-size
:
7pt
}
.view-path
{
font-size
:
8.5pt
;
padding
:
12pt
10pt
;
break-inside
:
avoid
;
margin
:
11pt
0
}
.table-wrap
{
margin
:
13pt
0
16pt
;
break-inside
:
avoid
}
table
{
font-size
:
8.5pt
;
line-height
:
1.65
}
th
{
padding
:
8pt
10pt
}
td
{
padding
:
8pt
10pt
}
tr
{
break-inside
:
avoid
}
.duties
{
break-inside
:
avoid
}
.duties
li
{
padding
:
9pt
0
9pt
29pt
}
.duties
li
::before
{
font-size
:
9pt
;
top
:
13pt
}
.future
{
font-size
:
8.5pt
;
break-inside
:
avoid
;
padding
:
12pt
15pt
;
margin-top
:
17pt
}
.own-business
{
padding-top
:
12pt
;
margin-top
:
15pt
}
.sources
{
font-size
:
8pt
;
line-height
:
1.75
;
margin-top
:
25pt
;
padding-top
:
17pt
}
.sources
h2
{
font-size
:
9.5pt
;
break-after
:
avoid
}
.sources
li
{
margin
:
6pt
0
}
.sources
p
{
margin-bottom
:
9pt
}
.page-footer
{
font-size
:
7pt
;
padding-top
:
15pt
}
a
{
color
:
inherit
;
text-decoration
:
none
}
.sources
a
{
text-decoration
:
underline
;
text-decoration-color
:
#b9c3d8
}
.questions
{
break-inside
:
avoid
;
break-after
:
avoid
}
.review-conclusion
{
break-before
:
avoid
}
.table-wrap
:has
(+
p
)
{
break-after
:
auto
}
.keep-next
{
break-after
:
avoid
}}
.diagram-desktop
{
display
:
block
;
width
:
100%
;
height
:
auto
;
color
:
var
(
--ink
);
font-family
:
var
(
--sans
)}
.diagram-desktop
.node
{
fill
:
var
(
--surface
);
stroke
:
var
(
--rule
);
stroke-width
:
1.3
}
.diagram-desktop
.hub
{
fill
:
var
(
--brand-soft
);
stroke
:
var
(
--brand
)}
.diagram-desktop
.edge
{
stroke
:
var
(
--brand
);
stroke-width
:
1.5
;
fill
:
none
;
marker-end
:
url(#arrow)
}
.diagram-desktop
.title
{
fill
:
var
(
--ink
);
font-size
:
19px
;
font-weight
:
600
}
.diagram-desktop
.sub
{
fill
:
var
(
--ink-2
);
font-size
:
12px
}
.diagram-desktop
.label
{
fill
:
var
(
--ink-2
);
font-size
:
13px
}
.diagram-mobile
{
display
:
none
}
.contents
a
{
white-space
:
normal
}
.sources
a
{
overflow-wrap
:
anywhere
}
@media
screen
and
(
max-width
:
660px
){
.diagram-desktop
{
display
:
none
}
.diagram-mobile
{
display
:
block
}
.route
{
padding
:
13px
0
;
border-top
:
1px
solid
var
(
--rule
);
font-size
:
13px
;
line-height
:
1.85
}
.route
:first-child
{
padding-top
:
0
;
border-top
:
0
}
.route
:last-child
{
padding-bottom
:
0
}
.route
strong
{
display
:
block
;
color
:
var
(
--brand
);
font-size
:
14px
;
margin-bottom
:
3px
}
.route
span
{
color
:
var
(
--ink-2
)}
.table-wrap
td
+
td
:before
{
content
:
attr
(
data-label
)
":"
;
color
:
var
(
--ink-3
);
font-size
:
11px
}
.chapter
h2
{
font-size
:
21px
}}
@media
print
{
body
{
font-size
:
9.5pt
;
line-height
:
1.7
}
.chapter
{
padding-top
:
16pt
}
.chapter
+
.chapter
{
margin-top
:
14pt
}
.sources
{
margin-top
:
14pt
;
padding-top
:
10pt
}
.sources
ul
{
margin
:
7pt
0
}
.sources
li
{
margin
:
3pt
0
}
.sources
p
{
margin-bottom
:
6pt
}
.chapter
p
{
margin-bottom
:
8pt
}
.chapter
h3
{
margin-top
:
14pt
}
.chapter-head
{
margin-bottom
:
10pt
}
.diagram-desktop
{
display
:
block
!important
;
max-height
:
190pt
}
.diagram-mobile
{
display
:
none
!important
}
.contents
{
break-inside
:
avoid
}
.sources
{
break-inside
:
avoid
}
.chapter-head
,
h3
{
break-after
:
avoid
}
.chapter
h2
{
font-size
:
16pt
}
.flow
{
break-inside
:
avoid
}
.two-col
tr
{
break-inside
:
avoid
}}
</style></head><body>
<aside
role=
"note"
style=
"padding:12px 20px;background:#fff4da;color:#594100;border-bottom:1px solid #e9cf8d;font:14px/1.7 sans-serif"
>
历史评审稿,2026-10-22 归档。事件推送方案未被采用;现行实现按诊所分页轮询并批量查询分析。
<a
href=
"../../../apps/pac-service/src/integrations/meyer/README.md"
>
查看当前采集说明
</a></aside><a
class=
"skip-link"
href=
"#main"
>
跳到正文
</a><div
class=
"wrap"
id=
"top"
>
<header
class=
"document-header"
><div
class=
"topline"
><div
class=
"eyebrow"
>
美亚 × FridaySaaS × PAC
</div><div
class=
"tools"
aria-label=
"文档工具"
><button
type=
"button"
id=
"theme-button"
aria-label=
"切换深浅色"
aria-pressed=
"false"
title=
"切换深浅色"
><svg
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.6"
aria-hidden=
"true"
><circle
cx=
"12"
cy=
"12"
r=
"8"
/><path
d=
"M12 4a8 8 0 0 1 0 16Z"
fill=
"currentColor"
stroke=
"none"
/></svg><span>
深浅色
</span></button><button
type=
"button"
id=
"print-button"
aria-label=
"打印或另存为 PDF"
title=
"打印或另存为 PDF"
><svg
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
stroke-width=
"1.5"
aria-hidden=
"true"
><path
d=
"M6 9V3h12v6M6 18H3v-8h18v8h-3M6 14h12v7H6Z"
/></svg><span>
打印
</span></button></div></div><h1><span>
影像与分析数据
</span><span>
方案设计
</span></h1><div
class=
"meta"
><span
class=
"version"
>
评审修订稿
</span><span>
v0.4
</span><span
aria-hidden=
"true"
>
·
</span><time
datetime=
"2026-10-19"
>
2026-10-19
</time></div><p
class=
"scope"
>
本稿确认方案方向与系统边界,具体实现另行设计。
</p></header>
<main
id=
"main"
><div
class=
"opening"
><p
class=
"boundary"
><strong>
整体边界:美亚管理影像资产并提供影像算法能力,PAC 承担患者分析层,FridaySaaS 承担应用消费层。PAC 直接对接美亚的授权和数据接口,向 FridaySaaS 开放统一的患者分析能力。
</strong></p><p
class=
"principle"
>
判断依据是:
<strong>
数据由各自的权威系统负责,分析由 PAC 统一组织,应用围绕结果开展业务。
</strong>
FridaySaaS 定义业务患者和组织归属,美亚维护影像资产及绑定关系,PAC 校验对应关系、留存依据并形成分析。三个系统可以使用同一份关联,但不能各自猜测患者身份或独立修正归属。
</p></div><nav
class=
"contents"
id=
"contents"
aria-label=
"文档目录"
><span
class=
"contents-label"
>
目录
</span><div
class=
"contents-links"
><a
href=
"#relationship"
><span>
01
</span>
整体关系
</a><a
href=
"#meyer"
><span>
02
</span>
美亚
</a><a
href=
"#saas"
><span>
03
</span>
FridaySaaS
</a><a
href=
"#pac"
><span>
04
</span>
PAC
</a><a
href=
"#review"
><span>
05
</span>
当前方案与范围
</a><a
href=
"#sources"
><span>
附
</span>
依据与范围
</a></div></nav><section
class=
"chapter"
id=
"relationship"
aria-labelledby=
"heading-1"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
01
</span><h2
id=
"heading-1"
>
整体关系:采集呈现、患者分析、业务应用分工
</h2></div><p>
FridaySaaS 沿用现有美亚接入,同步患者、诊所和就诊信息,配合影像采集并提供阅片入口。PAC 直接获取美亚影像与分析,结合病历、治疗等患者事实,向 SaaS 输出分析能力。
</p><figure
aria-labelledby=
"flow-caption"
class=
"flow"
><figcaption
id=
"flow-caption"
><span>
图 01
</span>
采集、分析与应用的关系
<small>
目标链路 · 美亚新影像启用时
</small></figcaption>
<svg
aria-labelledby=
"diagram-title diagram-desc"
class=
"diagram-desktop"
role=
"img"
viewbox=
"0 45 840 380"
>
<title
id=
"diagram-title"
>
美亚、FridaySaaS 与 PAC 的职责及数据流
</title><desc
id=
"diagram-desc"
>
诊所设备向美亚采集影像,美亚使用 OSS 存储原片。FridaySaaS 向美亚同步患者、诊所与就诊,向 PAC 提供患者身份与业务数据。美亚向 PAC 实时推送新增影像通知。PAC 直接授权访问美亚影像、批量查询分析,并向 SaaS 输出分析结果、依据与状态。
</desc>
<defs><marker
id=
"arrow"
markerheight=
"6"
markerwidth=
"6"
orient=
"auto-start-reverse"
refx=
"9"
refy=
"5"
viewbox=
"0 0 10 10"
><path
d=
"M1 1L9 5L1 9"
fill=
"none"
stroke=
"var(--brand)"
stroke-width=
"1.5"
></path></marker></defs>
<path
class=
"edge"
d=
"M180 115H325"
></path><path
class=
"edge"
d=
"M515 115H660"
></path>
<text
class=
"label"
text-anchor=
"middle"
x=
"252"
y=
"101"
>
影像采集
</text><text
class=
"label"
text-anchor=
"middle"
x=
"588"
y=
"101"
>
原片存储
</text>
<path
class=
"edge"
d=
"M175 300L365 160"
></path><path
class=
"edge"
d=
"M625 300L435 160"
></path><path
class=
"edge"
d=
"M510 160L700 300"
></path>
<text
class=
"label"
text-anchor=
"middle"
x=
"183"
y=
"218"
><tspan
x=
"183"
>
同步患者、诊所
</tspan><tspan
dy=
"20"
x=
"183"
>
与就诊
</tspan></text>
<text
class=
"label"
text-anchor=
"middle"
x=
"430"
y=
"240"
><tspan
x=
"430"
>
授权访问影像
</tspan><tspan
dy=
"20"
x=
"430"
>
批量查询分析
</tspan></text><text
class=
"label"
text-anchor=
"middle"
x=
"716"
y=
"208"
><tspan
x=
"716"
>
实时推送
</tspan><tspan
dy=
"20"
x=
"716"
>
新增影像通知
</tspan></text>
<path
class=
"edge"
d=
"M290 325H550"
></path><path
class=
"edge"
d=
"M550 382H290"
></path>
<text
class=
"label"
text-anchor=
"middle"
x=
"420"
y=
"311"
>
患者身份与业务数据
</text><text
class=
"label"
text-anchor=
"middle"
x=
"420"
y=
"368"
>
分析结果、依据与状态
</text>
<rect
class=
"node"
height=
"90"
rx=
"6"
width=
"160"
x=
"20"
y=
"70"
></rect><text
class=
"title"
style=
"font-size:16px"
text-anchor=
"middle"
x=
"100"
y=
"110"
>
诊所设备
</text><text
class=
"sub"
text-anchor=
"middle"
x=
"100"
y=
"135"
>
与前置机
</text>
<rect
class=
"node"
height=
"90"
rx=
"6"
width=
"190"
x=
"325"
y=
"70"
></rect><text
class=
"title"
text-anchor=
"middle"
x=
"420"
y=
"110"
>
美亚 PACS
</text><text
class=
"sub"
text-anchor=
"middle"
x=
"420"
y=
"135"
>
影像资产 · 阅片 · 影像算法
</text>
<rect
class=
"node"
height=
"90"
rx=
"6"
width=
"160"
x=
"660"
y=
"70"
></rect><text
class=
"title"
text-anchor=
"middle"
x=
"740"
y=
"110"
>
OSS
</text><text
class=
"sub"
text-anchor=
"middle"
x=
"740"
y=
"135"
>
原片存储
</text>
<rect
class=
"node"
height=
"102"
rx=
"6"
width=
"230"
x=
"60"
y=
"300"
></rect><text
class=
"title"
text-anchor=
"middle"
x=
"175"
y=
"344"
>
FridaySaaS
</text><text
class=
"sub"
text-anchor=
"middle"
x=
"175"
y=
"372"
>
业务管理 · 影像呈现 · 应用消费
</text>
<rect
class=
"node hub"
height=
"102"
rx=
"6"
width=
"230"
x=
"550"
y=
"300"
></rect><text
class=
"title"
text-anchor=
"middle"
x=
"665"
y=
"344"
>
PAC
</text><text
class=
"sub"
text-anchor=
"middle"
x=
"665"
y=
"372"
>
数据接入 · 患者分析 · 开放输出
</text>
</svg>
<div
aria-label=
"系统关系"
class=
"diagram-mobile"
><div
class=
"route"
><strong>
诊所设备 → 美亚 → OSS
</strong><span>
采集影像,管理与存储原片
</span></div><div
class=
"route"
><strong>
FridaySaaS → 美亚
</strong><span>
同步患者、诊所与就诊
</span></div><div
class=
"route"
><strong>
FridaySaaS → PAC
</strong><span>
提供患者身份与业务数据
</span></div><div
class=
"route"
><strong>
美亚 → PAC
</strong><span>
实时推送新增影像通知
</span></div><div
class=
"route"
><strong>
PAC → 美亚
</strong><span>
授权访问影像,批量查询分析
</span></div><div
class=
"route"
><strong>
PAC → FridaySaaS
</strong><span>
输出分析结果、依据与状态
</span></div></div></figure><p>
FridaySaaS 在患者详情、接诊、客服等场景使用 PAC 的分析。
<strong>
召回只是其中一个场景,患者是否进入召回计划不应决定其能否获得分析。
</strong></p><p>
查看原片继续走 SaaS 阅片入口,PAC 工作台可沿用宿主槽位;PAC 为分析取片则直接访问美亚。
<strong>
如果分析依赖用户打开页面,没人查看的影像就可能被漏掉;如果每次打开页面才取数并计算,响应又会受外部服务和算法耗时影响。
</strong>
因此,后台持续获取和分析,页面按需读取结果、查看原片。
</p></section><section
class=
"chapter"
id=
"meyer"
aria-labelledby=
"heading-2"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
02
</span><h2
id=
"heading-2"
>
美亚:实时通知新增影像,分析结果延后获取
</h2></div><p>
根据美亚联调回复及后续沟通,已确认:
</p><div
class=
"table-wrap"
><table
class=
"two-col"
>
<thead>
<tr>
<th
scope=
"col"
>
已确认信息
</th>
<th
scope=
"col"
>
对方案的影响
</th>
</tr>
</thead>
<tbody>
<tr>
<td
data-label=
"已确认信息"
>
影像数据只创建,不修改、不删除
</td>
<td
data-label=
"对方案的影响"
>
按新增采集与去重,不设计影像修改、删除的同步链路
</td>
</tr>
<tr>
<td
data-label=
"已确认信息"
>
影像生成后实时推送,接收方提供回调地址
</td>
<td
data-label=
"对方案的影响"
>
PAC 提供回调入口,直接接收新增影像通知
</td>
</tr>
<tr>
<td
data-label=
"已确认信息"
>
上传后后台识别病症,通常约 3–5 分钟,高峰预计仍为分钟级
</td>
<td
data-label=
"对方案的影响"
>
影像通知与分析结果并非同时到达;该时间不是完成时限承诺
</td>
</tr>
<tr>
<td
data-label=
"已确认信息"
>
分析接口将支持批量查询;正常查询无数据时,美亚说明算法仍在处理中
</td>
<td
data-label=
"对方案的影响"
>
批量获取结果,未取得结果的后续补拉,不要求完整分析状态接口
</td>
</tr>
<tr>
<td
data-label=
"已确认信息"
>
病症分析目前仅支持全景片,CBCT 暂不支持
</td>
<td
data-label=
"对方案的影响"
>
对不支持的类型不反复补拉病症分析
</td>
</tr>
</tbody>
</table></div><p><strong>
实时通知解决“发现新影像”,延后补拉解决“取得晚到分析”。
</strong>
如果收到通知时只查一次,尚未生成的分析就可能被漏掉;没有取得结果,也不能解释为没有发现问题。
</p></section><section
class=
"chapter"
id=
"saas"
aria-labelledby=
"heading-3"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
03
</span><h2
id=
"heading-3"
>
FridaySaaS:做好业务身份、影像呈现和分析消费
</h2></div><p>
SaaS 负责采集接入和呈现,向 PAC 提供患者业务数据及授权上下文,再消费分析结果。
<strong>
无需为了转交 PAC,再建设一套美亚分析采集和重推链路。
</strong>
否则同一份分析要经过两套同步过程,既重复维护,也增加故障定位的环节。
</p><p>
SaaS 保留自身业务所需的关联和操作记录。医生采纳建议、客服据此跟进时,应记录引用的分析版本及必要内容。
<strong>
否则分析更新后,就难以还原当时为什么做出那次业务操作。
</strong>
分析依据和版本由 PAC 管理,业务执行由 SaaS 管理。
</p><p>
新老影像功能可以保留,实际择一启用:旧影像沿用 SaaS 数据通路,美亚影像由 PAC 直连获取,来源差异由 PAC 接入层统一。
<strong>
应用不应随影像来源切换而重新适配;同一张片迁入美亚,也不应被当成两份独立依据。
</strong></p></section><section
class=
"chapter"
id=
"pac"
aria-labelledby=
"heading-4"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
04
</span><h2
id=
"heading-4"
>
PAC:统一分析,留存依据,对应用负责
</h2></div><h3>
① 接入归一,身份与权限沿用业务边界
</h3><p>
美亚接入由 PAC 独立适配层负责,转换为 PAC 标准数据后用于分析。患者对应关系以 SaaS 业务身份和美亚绑定为依据,缺失或冲突时不猜测匹配。
</p><p>
PAC 负责美亚服务授权;用户能查看哪些患者和分析,仍依据 SaaS 的业务授权,由 PAC 校验执行。
<strong>
服务能读取多家诊所,不代表每个调用方都有权访问这些患者。
</strong></p><h3>
② 留存分析依据,原片继续由美亚管理
</h3><p>
PAC 保存影像关联、原始分析依据和自身分析结果,并保留来源、版本及数据时效。
<strong>
如果每次都临时向美亚取结果,同一份业务数据重放时可能取得不同内容,就无法解释当时为什么得出那个判断。
</strong></p><p>
原片仍存于美亚/OSS,由 PAC 按需授权访问。美亚暂时不可用时,PAC 仍能提供已留存的结果及依据,并说明数据截至时间;原片访问和最新分析获取则可能受影响。
</p><h3>
③ 区分来源,开放统一的患者分析能力
</h3><p>
PAC 可以使用美亚已有的算法结果,也可按需要获取原片、调用其他分析服务,再结合患者事实形成综合分析。承担分析层不要求底层影像算法全部自研。
</p><p><strong>
美亚发现了什么、PAC 综合判断了什么、医生确认了什么,应分别标明来源和依据。
</strong>
否则算法发现容易被误当成医生诊断,原始结果变化时也难以追溯受影响的结论。
</p><p>
对已接入且获授权的患者,PAC 向应用提供分析结论、依据、版本和状态。
<strong>
未取得分析结果不代表没有发现问题;PAC 只表达已知的结果可用性与能力范围,不推断美亚内部处理状态。
</strong>
SaaS 使用这些结果开展业务;结果回流时也应保留来源,避免同一结论被重复当成独立依据。
</p></section><section
class=
"chapter"
id=
"review"
aria-labelledby=
"heading-5"
><div
class=
"chapter-head"
><span
class=
"chapter-num"
>
05
</span><h2
id=
"heading-5"
>
当前方案与范围
</h2></div><p><strong>
新增影像实时通知 PAC → 留存影像记录 → 批量查询分析 → 有结果保存,无结果延后补拉。
</strong>
回调确认以通知可靠落库为界,不等待分析完成。接入前的历史影像另做首次采集。
</p></section><aside
class=
"sources"
id=
"sources"
aria-labelledby=
"sources-title"
><h2
id=
"sources-title"
>
依据与范围
</h2><p>
本稿依据本轮评审原则、2026-10-19 美亚联调回复及后续确认,以及既有代码和本地联调记录。直连取数已有验证基础,完整分析服务仍需后续设计与实现。
</p><p>
参考资料:
<span
title=
"本机参考文件,未随归档分发"
>
系统集成方案
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
私有化部署方案
</span>
、
<span
title=
"本机参考文件,未随归档分发"
>
接口对接文档
</span>
。
</p><p>
参考资料链接指向本机原件;分享本文时可一并提供原 PDF。
</p></aside></main><footer
class=
"page-footer"
><span>
美亚 × FridaySaaS × PAC · 影像与分析数据方案设计
</span><a
href=
"#top"
>
评审修订稿 v0.4 · 返回页首 ↑
</a></footer></div><a
class=
"back-top"
href=
"#contents"
aria-label=
"返回文档目录"
>
目录 ↑
</a>
<script>
(()
=>
{
const
root
=
document
.
documentElement
,
themeButton
=
document
.
getElementById
(
'theme-button'
),
key
=
'imaging-document-theme'
;
const
scheme
=
matchMedia
(
'(prefers-color-scheme: dark)'
);
const
isDark
=
()
=>
root
.
dataset
.
theme
?
root
.
dataset
.
theme
===
'dark'
:
scheme
.
matches
;
const
update
=
()
=>
themeButton
.
setAttribute
(
'aria-pressed'
,
String
(
isDark
()));
try
{
const
saved
=
localStorage
.
getItem
(
key
);
if
(
saved
===
'dark'
||
saved
===
'light'
)
root
.
dataset
.
theme
=
saved
;}
catch
{}
update
();
scheme
.
addEventListener
(
'change'
,
update
);
themeButton
.
addEventListener
(
'click'
,()
=>
{
const
theme
=
isDark
()?
'light'
:
'dark'
;
root
.
dataset
.
theme
=
theme
;
try
{
localStorage
.
setItem
(
key
,
theme
)}
catch
{}
update
()});
document
.
getElementById
(
'print-button'
).
addEventListener
(
'click'
,()
=>
window
.
print
());
const
back
=
document
.
querySelector
(
'.back-top'
);
const
check
=
()
=>
{
const
visible
=
scrollY
>
700
;
back
.
classList
.
toggle
(
'visible'
,
visible
);
back
.
tabIndex
=
visible
?
0
:
-
1
;
back
.
setAttribute
(
'aria-hidden'
,
String
(
!
visible
))};
addEventListener
(
'scroll'
,
check
,{
passive
:
true
});
check
();
})();
</script></body></html>
docs/_archive/2026-10-operations-and-design/2026-10-19-meyer-friday-pac-imaging-review.md
0 → 100644
View file @
a9248e61
# 美亚 × FridaySaaS × PAC:影像与分析数据方案设计
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。 原方案中的影像事件推送未被采用,当前实现按诊所分页轮询并批量查询分析。
> 评审修订稿 v0.4 · 2026-10-19
> 本稿确认方案方向与系统边界,具体实现另行设计。
**整体边界:美亚管理影像资产并提供影像算法能力,PAC 承担患者分析层,FridaySaaS 承担应用消费层。PAC 直接对接美亚的授权和数据接口,向 FridaySaaS 开放统一的患者分析能力。**
判断依据是:
**数据由各自的权威系统负责,分析由 PAC 统一组织,应用围绕结果开展业务。**
FridaySaaS 定义业务患者和组织归属,美亚维护影像资产及绑定关系,PAC 校验对应关系、留存依据并形成分析。三个系统可以使用同一份关联,但不能各自猜测患者身份或独立修正归属。
## 1. 整体关系:采集呈现、患者分析、业务应用分工
FridaySaaS 沿用现有美亚接入,同步患者、诊所和就诊信息,配合影像采集并提供阅片入口。PAC 直接获取美亚影像与分析,结合病历、治疗等患者事实,向 SaaS 输出分析能力。
```
mermaid
flowchart TB
E[诊所设备与前置机] --> M["美亚 PACS<br/>影像资产、阅片、影像算法"]
M --> O[OSS 原片存储]
S["FridaySaaS<br/>业务管理、影像呈现、应用消费"] -->|同步患者、诊所与就诊| M
S -->|患者身份与业务数据| P["PAC<br/>数据接入、患者分析、开放输出"]
M -->|实时推送新增影像通知| P
P -->|直接授权访问影像、批量查询分析| M
P -->|分析结果、依据与状态| S
```
FridaySaaS 在患者详情、接诊、客服等场景使用 PAC 的分析。
**召回只是其中一个场景,患者是否进入召回计划不应决定其能否获得分析。**
查看原片继续走 SaaS 阅片入口,PAC 工作台可沿用宿主槽位;PAC 为分析取片则直接访问美亚。
**如果分析依赖用户打开页面,没人查看的影像就可能被漏掉;如果每次打开页面才取数并计算,响应又会受外部服务和算法耗时影响。**
因此,后台持续获取和分析,页面按需读取结果、查看原片。
## 2. 美亚:实时通知新增影像,分析结果延后获取
根据美亚联调回复及后续沟通,已确认:
| 已确认信息 | 对方案的影响 |
|---|---|
| 影像数据只创建,不修改、不删除 | 按新增采集与去重,不设计影像修改、删除的同步链路 |
| 影像生成后实时推送,接收方提供回调地址 | PAC 提供回调入口,直接接收新增影像通知 |
| 上传后后台识别病症,通常约 3–5 分钟,高峰预计仍为分钟级 | 影像通知与分析结果并非同时到达;该时间不是完成时限承诺 |
| 分析接口将支持批量查询;正常查询无数据时,美亚说明算法仍在处理中 | 批量获取结果,未取得结果的后续补拉,不要求完整分析状态接口 |
| 病症分析目前仅支持全景片,CBCT 暂不支持 | 对不支持的类型不反复补拉病症分析 |
**实时通知解决“发现新影像”,延后补拉解决“取得晚到分析”。**
如果收到通知时只查一次,尚未生成的分析就可能被漏掉;没有取得结果,也不能解释为没有发现问题。
## 3. FridaySaaS:做好业务身份、影像呈现和分析消费
SaaS 负责采集接入和呈现,向 PAC 提供患者业务数据及授权上下文,再消费分析结果。
**无需为了转交 PAC,再建设一套美亚分析采集和重推链路。**
否则同一份分析要经过两套同步过程,既重复维护,也增加故障定位的环节。
SaaS 保留自身业务所需的关联和操作记录。医生采纳建议、客服据此跟进时,应记录引用的分析版本及必要内容。
**否则分析更新后,就难以还原当时为什么做出那次业务操作。**
分析依据和版本由 PAC 管理,业务执行由 SaaS 管理。
新老影像功能可以保留,实际择一启用:旧影像沿用 SaaS 数据通路,美亚影像由 PAC 直连获取,来源差异由 PAC 接入层统一。
**应用不应随影像来源切换而重新适配;同一张片迁入美亚,也不应被当成两份独立依据。**
## 4. PAC:统一分析,留存依据,对应用负责
**① 接入归一,身份与权限沿用业务边界**
美亚接入由 PAC 独立适配层负责,转换为 PAC 标准数据后用于分析。患者对应关系以 SaaS 业务身份和美亚绑定为依据,缺失或冲突时不猜测匹配。
PAC 负责美亚服务授权;用户能查看哪些患者和分析,仍依据 SaaS 的业务授权,由 PAC 校验执行。
**服务能读取多家诊所,不代表每个调用方都有权访问这些患者。**
**② 留存分析依据,原片继续由美亚管理**
PAC 保存影像关联、原始分析依据和自身分析结果,并保留来源、版本及数据时效。
**如果每次都临时向美亚取结果,同一份业务数据重放时可能取得不同内容,就无法解释当时为什么得出那个判断。**
原片仍存于美亚/OSS,由 PAC 按需授权访问。美亚暂时不可用时,PAC 仍能提供已留存的结果及依据,并说明数据截至时间;原片访问和最新分析获取则可能受影响。
**③ 区分来源,开放统一的患者分析能力**
PAC 可以使用美亚已有的算法结果,也可按需要获取原片、调用其他分析服务,再结合患者事实形成综合分析。承担分析层不要求底层影像算法全部自研。
**美亚发现了什么、PAC 综合判断了什么、医生确认了什么,应分别标明来源和依据。**
否则算法发现容易被误当成医生诊断,原始结果变化时也难以追溯受影响的结论。
对已接入且获授权的患者,PAC 向应用提供分析结论、依据、版本和状态。
**未取得分析结果不代表没有发现问题;PAC 只表达已知的结果可用性与能力范围,不推断美亚内部处理状态。**
SaaS 使用这些结果开展业务;结果回流时也应保留来源,避免同一结论被重复当成独立依据。
## 5. 当前方案与范围
**新增影像实时通知 PAC → 留存影像记录 → 批量查询分析 → 有结果保存,无结果延后补拉。**
回调确认以通知可靠落库为界,不等待分析完成。接入前的历史影像另做首次采集。
---
### 依据与范围
本稿依据本轮评审原则、2026-10-19 美亚联调回复及后续确认,以及既有代码和本地联调记录。直连取数已有验证基础,完整分析服务仍需后续设计与实现。
参考资料:系统集成方案(本机参考文件
`瑞尔口腔影像中心PACS项目系统集成方案v0.4.pdf`
,未随归档分发)、私有化部署方案(本机参考文件
`瑞尔PACS影像中心私有化部署方案v0.1.pdf`
,未随归档分发)、接口对接文档(本机参考文件
`PACS_瑞尔接口对接文档.pdf`
,未随归档分发)。
docs/_archive/2026-10-operations-and-design/2026-10-20-data-warehouse-and-pac-positioning.md
0 → 100644
View file @
a9248e61
# 公司数仓的定位与 PAC 的职责边界
> 历史归档(2026-10-22):保留原记录日期的方案、样本和验收结论,不代表当前运行状态;现行入口见本目录索引。
> 架构讨论稿 · 2026-10-20
**公司需要独立、可复用的数据能力。以 PAC 兼任公司级数据底座,是满足当前业务的过渡方案,不能作为长期的公司数仓定位。**
过渡的是 PAC 代为承担的公共数据职责;患者分析、行动判断与执行协作,仍然是 PAC 可以长期发展的产品能力。
## 1. 数仓是什么:公司的公共数据基础
数仓持续整合不同系统、不同业务的数据,形成
**定义一致、来源可追溯、质量可核对、能够重复使用**
的公共数据,服务经营分析、报表、算法和业务应用。
它的价值体现在三个方面:
-
**整合**
:患者、诊疗、收费、营销、人员、门店等数据能够关联使用。
-
**一致**
:身份、时间、状态和指标有明确口径,减少各部门各算一套。
-
**复用**
:一次可靠接入和公共加工,可以支持多个消费者。
数仓需要独立的数据模型和治理责任。把业务库集中复制到另一个数据库,只完成了数据汇集;还需要整合语义、明确质量和使用规则。
业务系统继续维护自己的记录并办理业务。数仓通常通过同步取得数据,不要求预约、收费、写病历等日常操作经过数仓。
## 2. 公司为什么需要:让数据服务多个业务
公司需要回答的问题已经超出单一患者场景:渠道带来的患者是否到诊、治疗和收费如何衔接、不同门店和人员的表现是否可比。这些问题需要跨系统的数据和共同口径。
如果每个产品独立取数、清洗和解释,同一份数据就会产生多套加工逻辑。新增应用、源系统变更和历史补数,都会重复付出成本;结果不一致时,也难以确定谁负责。
**建设时机取决于复用和治理需求,不只取决于数据量。**
多个消费者反复使用同一批数据、口径冲突持续出现、跨系统分析成为日常工作时,就应明确公共数据能力的建设责任。
这不意味着立即建设覆盖全公司的大型平台。可以从已有重复需求开始,逐步形成公共能力;前提是有人持续负责定义、质量和交付。
## 3. PAC 当前承担了什么:为患者应用补齐数据条件
PAC 已包含多源接入、字段与语义归一、来源留存、事实版本、画像及计划更新等能力,具备部分数据仓储与治理特征。当前主要业务落点是客户召回。
这些能力首先服务于 PAC:把已接入的患者记录转化为可解释的事实,支持画像、机会判断、工作分配和跟进。
**有数据加工和事实存储,不等于具备公司级数仓的覆盖范围与服务责任。**
PAC 中某条记录可供召回判断,也不意味着它已经满足财务统计、经营分析或其他产品的全部要求。
## 4. 为什么只能过渡:应用边界不能替代公司数据边界
| 局限 | 对公司的影响 |
|---|---|
| 数据覆盖随患者应用需求选择 | PAC 已接入的数据,不能代表全公司的业务数据已完整覆盖 |
| 数据语义和加工围绕患者判断组织 | 其他业务仍需核对粒度、状态与统计口径,不能直接照用应用结果 |
| 原文与版本留存在已采集范围内 | 重放可以重处理已有记录,无法恢复从未接入的数据 |
| 数据交付与应用运行相互耦合 | 多个消费者长期依赖后,公共补数、质量和变更管理会挤占产品迭代,并扩大故障影响 |
继续无边界扩充 PAC,会使其同时承担公共数据供应和患者业务交付。两类责任的消费者、覆盖范围、质量要求和变更节奏不同,需要分别管理。
## 5. 长期边界:记录、公共数据与业务判断分别负责
| 角色 | 核心责任 |
|---|---|
|
**源业务系统**
| 维护权威业务记录,办理业务操作,明确数据及其变更含义 |
|
**公司数仓/公共数据能力**
| 可靠整合、公共身份与口径、历史留存、来源追溯、质量核对及共享数据交付 |
|
**PAC**
| 患者事实的领域解释、画像、机会与优先级判断、行动计划、分配和反馈 |
“单一可信源”应落实为明确的权威分工:业务事实以对应源系统为准,公共分析使用经过治理的数据和指标,PAC 的判断与行动状态由 PAC 管理。无需把所有读写集中到同一个数据库。
公共数据模型应表达可复用的业务含义;源系统差异在接入与加工中处理,应用特有判断留在应用边界内。公共模型可以演进,但不应随某个来源的表结构或某个应用的临时需求任意变化。
数据完整性和正确性也需要分工:源系统负责提供明确、可靠的记录,公共数据能力负责同步与加工质量,PAC 负责自身判断及证据使用。数仓无法自动消除源端缺失或含义不明的问题。
## 6. 过渡原则:沉淀公共能力,保留产品价值
-
**当前可继续交付**
:PAC 在明确数据范围和质量条件下提供患者分析与召回能力,不必等待完整数仓建成。
-
**公共能力逐步独立**
:优先沉淀被多个消费者重复需要的接入、归一、追溯和质量核对,明确责任人与交付约定。
-
**迁移按数据范围推进**
:公共数据具备可验证的完整性、时效和语义后,PAC 再逐步切换,避免同一数据长期存在相互冲突的加工路径。
-
**产品职责持续保留**
:数仓完善后,PAC 仍维护自己的分析、计划和执行状态,并对患者相关判断负责。
外部公司的同类数据也按此原则接入:主要供 PAC 使用时,可以直接进入其接入流程;需要跨产品复用时,应纳入公共数据能力,并保留来源、授权范围及组织隔离。
**目标是让公司获得稳定的数据资产,让 PAC 集中解决患者相关的判断与行动问题。是否独立部署、采用何种数据库及如何建表,属于后续设计。**
---
### 依据
-
当前 PAC 的
[
架构总览
](
../../../apps/pac-docs/content/docs/architecture/overview.mdx
)
、
[
三层模型
](
../../../apps/pac-docs/content/docs/architecture/three-layer-model.mdx
)
与
[
产品概述
](
../../../apps/pac-docs/content/docs/start/what-is-pac.mdx
)
。
-
[
DataWorks 数仓分层
](
https://www.alibabacloud.com/help/en/dataworks/user-guide/data-warehouse-layering
)
:源数据、公共加工与应用服务的职责区分。
docs/_archive/2026-10-operations-and-design/README.md
0 → 100644
View file @
a9248e61
# 2026 年 10 月运维验收与架构讨论归档
2026-10-22 整理。保存 11 份独有文档,包含历史发布记录、数仓定位讨论及影像方案的不同评审版本。
## 现行入口
-
[
美亚采集器
](
../../../apps/pac-service/src/integrations/meyer/README.md
)
-
[
患者事实查询 API
](
../../../apps/pac-docs/content/docs/api/open/PatientFactsLookupController_lookup.mdx
)
-
[
PAC 产品概述
](
../../../apps/pac-docs/content/docs/start/what-is-pac.mdx
)
-
[
运维说明
](
../../operations/README.md
)
-
[
美亚与患者事实开发历史
](
../2026-10-meyer-and-patient-facts/README.md
)
## 归档清单
| 文件 | 内容 |
| --- | --- |
|
[
2026-10-14-push-latency-local.md
](
2026-10-14-push-latency-local.md
)
| Push 优化及测试、生产验收;含此前未提交的生产验收补充 |
|
[
2026-10-14-return-visit-push-fix-test.md
](
2026-10-14-return-visit-push-fix-test.md
)
| 回访推送修复、单条重放和发布验收 |
|
[
2026-10-18-business-date-shanghai-test-validation.md
](
2026-10-18-business-date-shanghai-test-validation.md
)
| 北京业务日期修复及跨时区验证 |
|
[
2026-10-18-jvs-complaint-ingestion-test-validation.md
](
2026-10-18-jvs-complaint-ingestion-test-validation.md
)
| 投诉摄入测试发布与真实样本验收 |
|
[
2026-10-18-jvs-complaint-ingestion-prod-validation.md
](
2026-10-18-jvs-complaint-ingestion-prod-validation.md
)
| 投诉摄入生产发布与真实样本验收 |
|
[
2026-10-20-data-warehouse-and-pac-positioning.md
](
2026-10-20-data-warehouse-and-pac-positioning.md
)
| 数仓与 PAC 的职责边界讨论 |
|
[
2026-10-16-meyer-friday-pac-imaging-review.md
](
2026-10-16-meyer-friday-pac-imaging-review.md
)
| 影像早期评审稿 v0.2 |
|
[
2026-10-16-meyer-friday-pac-imaging-review-document.html
](
2026-10-16-meyer-friday-pac-imaging-review-document.html
)
| v0.2 独立文档版 HTML |
|
[
2026-10-16-meyer-friday-pac-imaging-review.html
](
2026-10-16-meyer-friday-pac-imaging-review.html
)
| v0.2 展示版 HTML |
|
[
2026-10-19-meyer-friday-pac-imaging-review.md
](
2026-10-19-meyer-friday-pac-imaging-review.md
)
| 影像修订评审稿 v0.4 |
|
[
2026-10-19-meyer-friday-pac-imaging-review-document.html
](
2026-10-19-meyer-friday-pac-imaging-review-document.html
)
| v0.4 独立文档版 HTML |
## 阅读边界
-
环境状态、版本、数据量、耗时和发布结果均为原记录日期的证据;本次没有重新查询服务器。
-
两版影像方案保存了不同阶段的职责划分,早期事件推送建议未被采用。当前按诊所分页轮询影像、批量查询分析,具体行为以现行代码为准。
-
数仓文档是架构讨论,不表示公司数仓已经建成,也不改变 PAC 现有业务职责。
-
不在此复制患者查询原文、临时访问地址、生产备份或运行凭据。内部联调资料单独备份,不纳入 Git。
-
原本机 PDF 及外部仓库源码仅保留引用说明,未作为附件发布;本仓库链接已按新位置修正。
[
移动与校验清单
](
manifest.json
)
记录原路径、新路径及整理前后摘要。原始文档另有完整可恢复备份。
docs/_archive/2026-10-operations-and-design/manifest.json
0 → 100644
View file @
a9248e61
{
"archived_on"
:
"2026-10-22"
,
"files"
:
[
{
"from"
:
"docs/qa/2026-10-14-push-latency-local.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-14-push-latency-local.md"
,
"original_sha256"
:
"9dbe13cb926a54834dcb35a4d013f6bb4297e46668cd40d91beb47cc83a7eb73"
,
"archived_sha256"
:
"ad369f52632c4f7b118429f78864a6e19594f864072297cd4c8c0ef5e59f63a5"
,
"category"
:
"historical_validation"
},
{
"from"
:
"docs/qa/2026-10-14-return-visit-push-fix-test.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-14-return-visit-push-fix-test.md"
,
"original_sha256"
:
"6e92dfeeade2ce23c28b0a5e09a0429c38453ba6efadb6d5c27c8381dd82a162"
,
"archived_sha256"
:
"fa0d03f1009093bd744810c38adb9c5c2c3911cc93ba030007a582d071b18bb5"
,
"category"
:
"historical_validation"
},
{
"from"
:
"docs/qa/2026-10-16-meyer-friday-pac-imaging-review-document.html"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review-document.html"
,
"original_sha256"
:
"794cd8b72fb6fd75259f4cb63715d1f6f95e9f3e31ea555f43b1c6766a1373de"
,
"archived_sha256"
:
"a3387fdf95b8156838b06a875d4c35cd2a5138010b6f44bb7087999a0f0cdf40"
,
"category"
:
"historical_design"
},
{
"from"
:
"docs/qa/2026-10-16-meyer-friday-pac-imaging-review.html"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review.html"
,
"original_sha256"
:
"0adb5e5adfe6674ca7cd15b0f62e12407b57e606628a1074c139e21c4c250765"
,
"archived_sha256"
:
"b9bcbb7d3fff38d3bd22c33191d3106653e37f128a5d8e8c242c7fd41fa6c467"
,
"category"
:
"historical_design"
},
{
"from"
:
"docs/qa/2026-10-16-meyer-friday-pac-imaging-review.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-16-meyer-friday-pac-imaging-review.md"
,
"original_sha256"
:
"2679e80c6546802bc9d29d130a684e2b3696e1b86c79a85c5ccff92808f2332d"
,
"archived_sha256"
:
"e659a0769012faa8c6e090e0dc622b0a5f86dd9adcab9c4fe6c206474b759879"
,
"category"
:
"historical_design"
},
{
"from"
:
"docs/qa/2026-10-18-business-date-shanghai-test-validation.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-18-business-date-shanghai-test-validation.md"
,
"original_sha256"
:
"fff7acd4aea01673487cc8b2e96b299d877081412f3e619bdd85d69ab0a26b32"
,
"archived_sha256"
:
"99be312617b0445866ea5e193d9708658940a862adf4db4d9af6c113eb98ade7"
,
"category"
:
"historical_validation"
},
{
"from"
:
"docs/qa/2026-10-18-jvs-complaint-ingestion-prod-validation.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-18-jvs-complaint-ingestion-prod-validation.md"
,
"original_sha256"
:
"ddee388593323a72b30d75388f534767db88370824b362f40d7946f7d0302edc"
,
"archived_sha256"
:
"64af94eb97d52140f76113c28be8193a34a9236cf930e228b0ebf9822611ecc5"
,
"category"
:
"historical_validation"
},
{
"from"
:
"docs/qa/2026-10-18-jvs-complaint-ingestion-test-validation.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-18-jvs-complaint-ingestion-test-validation.md"
,
"original_sha256"
:
"ba7149068b9b7e9fe7e9dde3e40d47c464007e156e72d542da3b95dee09cc64f"
,
"archived_sha256"
:
"8c3116cddafd0d03464183beccd0841a062ceda2f44d763a2afd44a18baac42f"
,
"category"
:
"historical_validation"
},
{
"from"
:
"docs/qa/2026-10-19-meyer-friday-pac-imaging-review-document.html"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-19-meyer-friday-pac-imaging-review-document.html"
,
"original_sha256"
:
"aebf50c50fd912e51c4aeea1d1dd78aa1b80658d61f1c73fe4889794ea7a78a3"
,
"archived_sha256"
:
"c9541bf669f483732ed9ae7bec8fd597c1aae9087954dad17ff5a803256fd64b"
,
"category"
:
"historical_design"
},
{
"from"
:
"docs/qa/2026-10-19-meyer-friday-pac-imaging-review.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-19-meyer-friday-pac-imaging-review.md"
,
"original_sha256"
:
"b85aaa9b8fc67efea031156935ede484e8975af30bfee0de481c88cfb4d57e47"
,
"archived_sha256"
:
"4cef9b11cfdd1ddaf40c5526016d1f427c5fb867c561acf9610fc94f019c348b"
,
"category"
:
"historical_design"
},
{
"from"
:
"docs/qa/2026-10-20-data-warehouse-and-pac-positioning.md"
,
"to"
:
"docs/_archive/2026-10-operations-and-design/2026-10-20-data-warehouse-and-pac-positioning.md"
,
"original_sha256"
:
"c3acd20f181483a052d66306bc007290440c6e9769bced5c62c3abfa115895fa"
,
"archived_sha256"
:
"597afabd5ecfc0a7b87babf8a428cb408101d9a051e0ef546145bcc62991cbce"
,
"category"
:
"architecture_discussion"
}
],
"changes"
:
"Add historical notices, repair repository links, label unavailable local references; preserve original content in private backup."
}
docs/_archive/README.md
View file @
a9248e61
...
@@ -4,6 +4,13 @@
...
@@ -4,6 +4,13 @@
归档保存历史设计、实验、排查与交付证据。文件名与原文保留;移动时修正引用。归档不等于问题关闭,不以历史环境快照判断当前运行状态。
归档保存历史设计、实验、排查与交付证据。文件名与原文保留;移动时修正引用。归档不等于问题关闭,不以历史环境快照判断当前运行状态。
## 2026-10-22 整理
-
[
美亚与患者事实开发历史
](
2026-10-meyer-and-patient-facts/README.md
)
:8 份实现、字段审核与验证记录。
-
[
运维验收与架构讨论
](
2026-10-operations-and-design/README.md
)
:11 份 Push、日期、投诉验收记录及数仓、影像方案讨论,含不同版本 HTML。
内部联调原文与一次性检查产物另行备份,不作为正式产品文档发布。
## 2026-10-14 整理
## 2026-10-14 整理
| 分类 | 入口 | 移入文件 | 原位置与归档依据 |
| 分类 | 入口 | 移入文件 | 原位置与归档依据 |
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment