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
50f342b3
Commit
50f342b3
authored
Aug 17, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
merge: 圈人侧也改读预存标签 —— 603ms → 57ms + 堵上两侧口径会漂的口子
parents
b9caf5ce
7bbe9a1b
Pipeline
#3577
failed in 0 seconds
Changes
5
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
78 additions
and
22 deletions
+78
-22
apps/pac-service/src/modules/plan/reason-temperature.sql.ts
+2
-8
apps/pac-service/tests/cohort-filter.spec.ts
+15
-6
apps/pac-service/tests/plan-label.spec.ts
+60
-1
apps/pac-web/src/components/plans/pool-matrix.tsx
+1
-2
apps/pac-web/src/components/supervisor/new-batch.tsx
+0
-5
No files found.
apps/pac-service/src/modules/plan/reason-temperature.sql.ts
View file @
50f342b3
...
@@ -179,10 +179,8 @@ export function labelExistsSql(label: string): Prisma.Sql {
...
@@ -179,10 +179,8 @@ export function labelExistsSql(label: string): Prisma.Sql {
AND EXISTS (
AND EXISTS (
SELECT 1
SELECT 1
FROM plan_reasons pr
FROM plan_reasons pr
CROSS JOIN LATERAL jsonb_array_elements_text(pr.evidence->'factIds') fid
JOIN patient_facts f ON f.id = fid::uuid AND f.status = 'active'
WHERE pr.plan_id = fp.id
WHERE pr.plan_id = fp.id
AND
${
labelCaseSql
(
'f'
,
AGE_YEARS_SQL_P
)}
=
${
label
}
AND
pr.potential_labels @> ARRAY[
${
label
}
]::text[]
)`
;
)`
;
}
}
...
@@ -203,13 +201,9 @@ export function labelTemperatureExistsSql(
...
@@ -203,13 +201,9 @@ export function labelTemperatureExistsSql(
AND EXISTS (
AND EXISTS (
SELECT 1
SELECT 1
FROM plan_reasons pr
FROM plan_reasons pr
CROSS JOIN LATERAL jsonb_array_elements_text(pr.evidence->'factIds') fid
JOIN patient_facts f ON f.id = fid::uuid AND f.status = 'active'
CROSS JOIN LATERAL (SELECT COALESCE(f.occurred_at, f.planned_for) AS at) anc
${
LAST_VISIT_JOIN
}
${
LAST_VISIT_JOIN
}
WHERE pr.plan_id = fp.id
WHERE pr.plan_id = fp.id
AND
${
labelCaseSql
(
'f'
,
AGE_YEARS_SQL_P
)}
=
${
label
}
AND pr.potential_labels @> ARRAY[
${
label
}
]::text[]
AND anc.at IS NOT NULL
HAVING
${
temperatureBucketCaseSql
(
BOUNDS
.
hot
,
BOUNDS
.
warm
,
BOUNDS
.
anchor
)}
IN (
${
Prisma
.
join
(
HAVING
${
temperatureBucketCaseSql
(
BOUNDS
.
hot
,
BOUNDS
.
warm
,
BOUNDS
.
anchor
)}
IN (
${
Prisma
.
join
(
buckets
.
map
((
t
)
=>
Prisma
.
sql
`
${
t
}
`
),
buckets
.
map
((
t
)
=>
Prisma
.
sql
`
${
t
}
`
),
', '
,
', '
,
...
...
apps/pac-service/tests/cohort-filter.spec.ts
View file @
50f342b3
...
@@ -66,17 +66,24 @@ describe('人群取数 —— 两根轴走召回单证据(2026-08 换源)', () =
...
@@ -66,17 +66,24 @@ describe('人群取数 —— 两根轴走召回单证据(2026-08 换源)', () =
).
not
.
toThrow
();
).
not
.
toThrow
();
});
});
test
(
'⭐⭐ 两根轴
从 plan_reasons → patient_facts 取
,⛔ 不再读画像的 types / temperature'
,
()
=>
{
test
(
'⭐⭐ 两根轴
仍走召回单证据(plan_reasons)
,⛔ 不再读画像的 types / temperature'
,
()
=>
{
// 画像回答「这个人有哪些潜在治疗」,矩阵要回答「引擎为什么召回他」。实测 161 个格位
// 画像回答「这个人有哪些潜在治疗」,矩阵要回答「引擎为什么召回他」。实测 161 个格位
// 画像有而召回单没有 —— 主管点那一格捞到的人,plan 讲的是别的病。
// 画像有而召回单没有 —— 主管点那一格捞到的人,plan 讲的是别的病。
expect
(
hot
).
toContain
(
'plan_reasons'
);
expect
(
hot
).
toContain
(
'plan_reasons'
);
expect
(
hot
).
toContain
(
'patient_facts'
);
expect
(
hot
).
not
.
toContain
(
"pf.data #> '{types}'"
);
expect
(
hot
).
not
.
toContain
(
"pf.data #> '{types}'"
);
expect
(
hot
).
not
.
toContain
(
'pf.data #>>'
);
expect
(
hot
).
not
.
toContain
(
'pf.data #>>'
);
});
});
test
(
'⭐⭐ 只认**未治疗**的证据 —— 治完是 fulfilled,拿它当锚就是念一个做完的诊断'
,
()
=>
{
/**
expect
(
hot
).
toContain
(
"f.status = 'active'"
);
* ⚠️ 2026-08-17 改造:标签不再在查询里现算,改读预存的 `plan_reasons.potential_labels`
* (初选矩阵 958ms → 110ms,圈人侧 603ms → 57ms)。
* 🔴 下面两条不变量**没有消失,是搬家了** —— 现在由回填 SQL(`plan-label.sql.ts`)保证。
* ⛔ 别因为查询里看不到就把断言删掉:删了这两条,谁把"治完的诊断"或"没有日期的证据"
* 算进标签都不会有人发现。断言跟着搬到 `plan-label.spec.ts`。
*/
test
(
'⭐⭐ 查询侧改读预存标签 —— ⛔ 不再回查 patient_facts'
,
()
=>
{
expect
(
hot
).
toContain
(
'potential_labels'
);
expect
(
hot
).
not
.
toContain
(
'patient_facts'
);
});
});
test
(
'🔴 关联片段⛔ 不许自己 SELECT followup_plans —— 内层 fp 会遮蔽外层,关联条件恒真且不报错'
,
()
=>
{
test
(
'🔴 关联片段⛔ 不许自己 SELECT followup_plans —— 内层 fp 会遮蔽外层,关联条件恒真且不报错'
,
()
=>
{
...
@@ -149,9 +156,11 @@ describe('人群取数 —— 锚点 = 末诊,只此一版', () => {
...
@@ -149,9 +156,11 @@ describe('人群取数 —— 锚点 = 末诊,只此一版', () => {
expect
(
t
).
toContain
(
'last_visit_at'
);
expect
(
t
).
toContain
(
'last_visit_at'
);
});
});
test
(
'⭐⭐ ⛔ 不许再拿诊断日当锚点 ——
但 anc.at 的非空守卫要留着(它决定行集)
'
,
()
=>
{
test
(
'⭐⭐ ⛔ 不许再拿诊断日当锚点 ——
锚点只能是末诊
'
,
()
=>
{
const
t
=
sql
().
strings
.
join
(
'?'
);
const
t
=
sql
().
strings
.
join
(
'?'
);
expect
(
t
).
toContain
(
'anc.at IS NOT NULL'
);
// ⚠️ 「anc.at 非空」这条守卫 2026-08-17 随标签预计算搬到了回填 SQL
// (见 plan-label.spec.ts 的「日期非空」一条)——⛔ 别在这里断言它还在查询里。
expect
(
t
).
toContain
(
'patient_profiles'
);
// 末诊锚点还在
// 锚点表达式只能是末诊:⛔ 不许出现 `max(anc.at`(那是旧口径的指纹)
// 锚点表达式只能是末诊:⛔ 不许出现 `max(anc.at`(那是旧口径的指纹)
expect
(
t
).
not
.
toMatch
(
/max
\(\s
*anc
\.
at/
);
expect
(
t
).
not
.
toMatch
(
/max
\(\s
*anc
\.
at/
);
});
});
...
...
apps/pac-service/tests/plan-label.spec.ts
View file @
50f342b3
import
{
refreshLabelsSql
,
countMissingLabelsSql
,
LABEL_REFRESH_BATCH
}
from
'../src/modules/plan/plan-label.sql'
;
import
{
refreshLabelsSql
,
countMissingLabelsSql
,
LABEL_REFRESH_BATCH
}
from
'../src/modules/plan/plan-label.sql'
;
import
{
planLabelAnchorsSql
}
from
'../src/modules/plan/reason-temperature.sql'
;
import
{
planLabelAnchorsSql
,
labelExistsSql
,
labelTemperatureExistsSql
,
}
from
'../src/modules/plan/reason-temperature.sql'
;
import
{
poolBaseSql
}
from
'../src/modules/plan/cohort-filter'
;
import
{
poolBaseSql
}
from
'../src/modules/plan/cohort-filter'
;
import
{
Prisma
}
from
'@prisma/client'
;
import
{
Prisma
}
from
'@prisma/client'
;
...
@@ -89,6 +93,46 @@ describe('poolBaseSql 用到的别名,查询里必须都在 FROM 里', () => {
...
@@ -89,6 +93,46 @@ describe('poolBaseSql 用到的别名,查询里必须都在 FROM 里', () => {
});
});
});
});
/**
* 🔴 **看侧与圈人侧必须同源** —— `reason-temperature.sql.ts` 里那段注释写着:
* 「曾经它们各自带一个 mode 参数,一旦调用方漏传就是主管看到 87 人、
* 确认单捞出另一批,而且全程不报错(T14)」。
*
* ⚠️ 2026-08-17 差点重演:看侧(矩阵)改用了预存的 `potential_labels`,
* 而圈人侧仍在实时回查 `patient_facts` 算标签 ⇒ 过生日跨年龄档的人会两边不一致。
* (标签规则里有三条带年龄:K08>18 / K07 3~12 / K07 13~40。)
* ⇒ 这组测试把"两侧读同一个来源"钉死。
*/
describe
(
'看侧与圈人侧同源'
,
()
=>
{
const
view
=
planLabelAnchorsSql
(
Prisma
.
sql
`fp.status = 'active'`
).
sql
;
const
pick
=
labelExistsSql
(
'implant'
).
sql
;
const
pickTemp
=
labelTemperatureExistsSql
(
'implant'
,
[
'cold_3y'
]).
sql
;
it
(
'🔴 三处都读 potential_labels'
,
()
=>
{
expect
(
view
).
toMatch
(
/potential_labels/
);
expect
(
pick
).
toMatch
(
/potential_labels/
);
expect
(
pickTemp
).
toMatch
(
/potential_labels/
);
});
it
(
'🔴 ⛔ 三处都不再回查 patient_facts —— 那是两边会漂的根源'
,
()
=>
{
for
(
const
sql
of
[
view
,
pick
,
pickTemp
])
{
expect
(
sql
).
not
.
toMatch
(
/patient_facts/
);
expect
(
sql
).
not
.
toMatch
(
/jsonb_array_elements_text/
);
}
});
it
(
'⛔ 圈人侧也不再按年龄现算标签(年龄已烘进那一列)'
,
()
=>
{
for
(
const
sql
of
[
pick
,
pickTemp
])
{
expect
(
sql
).
not
.
toMatch
(
/date_part
\(
'year'/
);
}
});
it
(
'温度那支仍保留末诊锚点与 HAVING 分档语义'
,
()
=>
{
expect
(
pickTemp
).
toMatch
(
/patient_profiles/
);
expect
(
pickTemp
).
toMatch
(
/HAVING/
);
});
});
describe
(
'刷新 SQL 的几条结构前提'
,
()
=>
{
describe
(
'刷新 SQL 的几条结构前提'
,
()
=>
{
it
(
'🔴 带 id 游标 —— 否则全量重算每次取同一批,原地打转且不报错'
,
()
=>
{
it
(
'🔴 带 id 游标 —— 否则全量重算每次取同一批,原地打转且不报错'
,
()
=>
{
const
first
=
text
(
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
false
}));
const
first
=
text
(
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
false
}));
...
@@ -119,6 +163,21 @@ describe('刷新 SQL 的几条结构前提', () => {
...
@@ -119,6 +163,21 @@ describe('刷新 SQL 的几条结构前提', () => {
expect
(
s
).
toMatch
(
/'
\{\}
'::text
\[\]
/
);
expect
(
s
).
toMatch
(
/'
\{\}
'::text
\[\]
/
);
});
});
/**
* 🔴 这两条原本长在 `cohort-filter.spec.ts` 的查询断言里,2026-08-17 随标签预计算搬到这儿。
* ⛔ 别因为"查询里看不到了"就当它们没了 —— 删掉之后,谁把「治完的诊断」或
* 「没有日期的证据」算进标签都不会有人发现,而那会直接改变召回谁。
*/
it
(
'⭐⭐ 只认**未治疗**的证据 —— 治完是 fulfilled,拿它当锚就是念一个做完的诊断'
,
()
=>
{
const
s
=
text
(
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
true
}));
expect
(
s
).
toMatch
(
/f
\.
status = 'active'/
);
});
it
(
'⭐⭐ 证据必须有日期 —— 没有 occurred_at / planned_for 的不算进标签'
,
()
=>
{
const
s
=
text
(
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
true
}));
expect
(
s
).
toMatch
(
/COALESCE
\(
f
\.
occurred_at, f
\.
planned_for
\)
IS NOT NULL/
);
});
it
(
'⛔ 不在这里另写一份标签 CASE —— 复用矩阵那份(规则表是单一真源)'
,
()
=>
{
it
(
'⛔ 不在这里另写一份标签 CASE —— 复用矩阵那份(规则表是单一真源)'
,
()
=>
{
const
q
=
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
true
});
const
q
=
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
true
});
// ⚠️ 标签值走参数化占位,在 `values` 里而不在 `sql` 里 —— 断言写在 sql 上会假绿。
// ⚠️ 标签值走参数化占位,在 `values` 里而不在 `sql` 里 —— 断言写在 sql 上会假绿。
...
...
apps/pac-web/src/components/plans/pool-matrix.tsx
View file @
50f342b3
...
@@ -199,8 +199,7 @@ export function PoolMatrix({
...
@@ -199,8 +199,7 @@ export function PoolMatrix({
不说清楚,主管会以为"这人三个机会都刚诊断"。⛔ 别省。
不说清楚,主管会以为"这人三个机会都刚诊断"。⛔ 别省。
*/
}
*/
}
<
p
className=
"mt-2 text-[10.5px] leading-relaxed text-muted-foreground"
>
<
p
className=
"mt-2 text-[10.5px] leading-relaxed text-muted-foreground"
>
按
<
span
className=
"font-medium"
>
{
TEMPERATURE_AXIS_ZH
}
距今多久
</
span
>
分档。
按
<
span
className=
"font-medium"
>
{
TEMPERATURE_AXIS_ZH
}
距今多久
</
span
>
分档
末诊按人算,同一个人的几个治疗项都在同一档。
</
p
>
</
p
>
{
data
.
note
&&
(
{
data
.
note
&&
(
...
...
apps/pac-web/src/components/supervisor/new-batch.tsx
View file @
50f342b3
...
@@ -109,11 +109,6 @@ export function NewBatchPanel({ clinicId }: { clinicId: string | null }) {
...
@@ -109,11 +109,6 @@ export function NewBatchPanel({ clinicId }: { clinicId: string | null }) {
<
span
className=
"text-[11px] text-slate-400"
>
<
span
className=
"text-[11px] text-slate-400"
>
点一格 = 把这批人交给助手,助手出确认单
点一格 = 把这批人交给助手,助手出确认单
</
span
>
</
span
>
{
/* ⚠️ 这句不是装饰:列头写的是「三个月内」,而"什么的三个月"只有这一处说得清。
⛔ 别为了省空间把它收进 tooltip。 */
}
<
span
className=
"ml-auto hidden text-[11px] text-slate-400 sm:inline"
>
按
{
TEMPERATURE_AXIS_ZH
}
距今多久分档
</
span
>
</
div
>
</
div
>
{
/* ⚠️ 小屏兜底:矩阵有六列,窄了就横向滚,⛔ 别让它压缩到看不清数字 */
}
{
/* ⚠️ 小屏兜底:矩阵有六列,窄了就横向滚,⛔ 别让它压缩到看不清数字 */
}
<
div
className=
"overflow-x-auto"
>
<
div
className=
"overflow-x-auto"
>
...
...
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