导入清单的「是否认证」界面照记下的说:登录身份 / 未认证署名

为什么:服务端开始如实记「是否认证」之后(导入清单那个提交),界面上的清单、导入记录、接受理由还一律写「署名(未认证)」,
开了登录、署名就是登录的人时说错了(05-已知遗留 (c))。

做了什么:导入记录接口带上 signed_by_verified(按这一期导入清单里记下的;旧清单记的是 false,照记下的说,不改写;简单导入
没有清单按未认证)。ManifestBody 的文件署名、接受理由的署名,VersionsDialog 的导入记录、快照里各期、接受理由的署名,都按记下
的值写「登录身份:x」或「署名(未认证):x」,和确认框里那一行(signerLine)同一套说法。接受理由由提交这一期的人写:署名和
清单(导入记录)的一样时才算认证。统计期、修改记录的署名是暂存时记的,没有记是否认证,照旧写「署名(未认证)」。

测试:test_signer_verified 加一句:开登录导入后导入记录的 signed_by_verified 为真。
This commit is contained in:
ThearchyHelios
2026-10-16 10:54:30 +08:00
parent fbb671a96b
commit b334f58009
5 changed files with 49 additions and 11 deletions
+12
View File
@@ -110,6 +110,16 @@ def load_manifest(artifact_id: str | None) -> dict[str, Any] | None:
return doc if isinstance(doc, dict) else None
def _signer_verified(artifact_id: str | None) -> bool:
"""导入清单里记下的「署名是否认证」;没有清单(简单导入)、读不出来的照实算未认证。"""
try:
doc = load_manifest(artifact_id)
except ValueError:
return False
signer = (doc or {}).get("signed_by")
return bool(signer.get("verified")) if isinstance(signer, dict) else False
def _rows_of(report: dict[str, Any] | None) -> dict[str, int]:
"""构建回执里各表的行数(执行器回执、期 1 的解析回执都是 tables: [{name, rows}])。"""
out: dict[str, int] = {}
@@ -1055,6 +1065,8 @@ async def import_records(session: AsyncSession, source: DataSource, imps: list[T
# 按配方导入才有(简单导入为空):配方、统计期、署名,接受的条数
"recipe_id": imp.recipe_id, "period_start": imp.period_start, "period_end": imp.period_end,
"signed_by": imp.signed_by, "overrides": len(imp.overrides or []), "waivers": len(imp.waivers or []),
# 署名是不是认证过的身份:按这一期导入清单里记下的(05-已知遗留 (c);旧清单写的是 False,照记下的说,不改写)
"signed_by_verified": _signer_verified(imp.manifest_artifact),
# 期 3(7.6)
"acceptances": _acceptance_rows(imp), "revoked": imp.revoked, "manifest_artifact": imp.manifest_artifact,
"recipe_seq": seqs.get(imp.recipe_id) if imp.recipe_id else None, "in_current": in_current,
+2
View File
@@ -56,6 +56,8 @@ async def test_login_on_import_manifest_and_purge_record_say_verified(admin):
assert imp.signed_by == "root"
source_id = out["source"]["id"]
records = (await admin.get(f"/api/datasources/{source_id}/imports")).json()
assert [(r["signed_by"], r["signed_by_verified"]) for r in records] == [("root", True)], "导入记录按清单里记下的说"
resp = await admin.post(f"/api/datasources/{source_id}/imports/{imp.id}/purge-raw",
json={"reason": "含有不应保留的内容", "signed_by": "随手填的名字"})
assert resp.status_code == 200, resp.text
+17 -3
View File
@@ -3,7 +3,7 @@ import clsx from 'clsx'
import type { CheckResult, ImportAcceptance, ManifestOut } from '../../types'
import { CopyButton, Notice } from '../../components/ui'
import { formatBytes, formatCost, formatDateTime, formatTokens } from '../../lib/format'
import { RAW_STATE_LABEL, RECIPE_ORIGIN_LABEL, RECIPE_TEXT, VERSIONS_TEXT as VT } from '../../lib/terms'
import { RAW_STATE_LABEL, RECIPE_ORIGIN_LABEL, RECIPE_TEXT, SIGNER_TEXT, VERSIONS_TEXT as VT } from '../../lib/terms'
import { CheckStatusChip } from './ImportReceipt'
import { ReceiptSummary } from './ReceiptBlocks'
@@ -79,7 +79,11 @@ export function ManifestBody({ m, fileName, rawState, recipeSeq }: {
{short(c.file?.raw_sha256) && <span className="mono text-dim">{VT.sha(short(c.file?.raw_sha256))}</span>}
{rawState != null && <span className="chip">{RAW_STATE_LABEL[rawState] ?? rawState}</span>}
</div>
{c.signed_by?.name && <div className="text-2xs text-faint">{VT.signedBy(c.signed_by.name)}</div>}
{c.signed_by?.name && (
<div className="text-2xs text-faint" data-manifest-signer={c.signed_by.verified ? 'verified' : 'self'}>
{signerText(c.signed_by.name, !!c.signed_by.verified)}
</div>
)}
</Block>
<Block id="recipe" title={B.recipe}>
@@ -146,7 +150,9 @@ export function ManifestBody({ m, fileName, rawState, recipeSeq }: {
</span>
</div>
<div className="flex flex-wrap gap-x-2 text-2xs text-faint">
<span>{VT.signedBy(a.signed_by || VT.unsigned)}</span>
{/* 接受理由由提交这一期的人写,署名同清单的 signed_by:是不是登录身份按清单里记下的说 */}
<span>{a.signed_by ? signerText(a.signed_by, !!c.signed_by?.verified && a.signed_by === c.signed_by.name)
: VT.signedBy(VT.unsigned)}</span>
{a.at && <span className="tnum">{formatDateTime(a.at)}</span>}
</div>
</li>
@@ -235,3 +241,11 @@ function RawJson({ summary, value, attr }: { summary: string; value: unknown; at
</details>
)
}
/**
* 留痕里的署名怎么说:按记下的「是否认证」写「登录身份:x」或「署名(未认证):x」(05-已知遗留 (c)),和确认框里那一行
* (VersionsDialog 的 signerLine)同一套说法。旧清单记的是未认证,照记下的说
*/
export function signerText(name: string, verified: boolean): string {
return verified ? SIGNER_TEXT.signedIn(name) : VT.signedBy(name)
}
+16 -8
View File
@@ -14,7 +14,7 @@ import { signerNow } from '../../store/auth'
import { humanizeError } from '../../lib/errors'
import { formatBytes, formatDateTime, formatTime, parseServerTime } from '../../lib/format'
import { RAW_STATE_LABEL, SIGNER_TEXT, VERSIONS_TEXT as VT } from '../../lib/terms'
import { ManifestBody } from './ManifestBody'
import { ManifestBody, signerText } from './ManifestBody'
// ===========================================================================
// 版本页(数据源卡片上的「版本」,P3-SPEC 7.8):当前版本的各期、历史版本、全部导入记录三个页签,
@@ -447,10 +447,10 @@ function CurrentTab({ cur, recOf, busy, onManifest, onRemove, onRevoke, onPurge
{acceptN > 0 && (
<details className="text-xs" data-part-acceptances={acceptN}>
<summary className="cursor-pointer select-none text-dim hover:text-fg">{VT.acceptCount(acceptN)}</summary>
<AcceptanceList items={accepted} className="mt-1.5" />
<AcceptanceList items={accepted} className="mt-1.5" signer={rec} />
</details>
)}
{rec?.signed_by && <div className="text-2xs text-faint">{VT.signedBy(rec.signed_by)}</div>}
{rec?.signed_by && <div className="text-2xs text-faint">{signerText(rec.signed_by, !!rec.signed_by_verified)}</div>}
<div className="flex flex-wrap items-center gap-1.5">
<button type="button" className="btn btn-xs" data-manifest-open={p.import_id} disabled={!rec}
onClick={() => rec && onManifest(rec)}>
@@ -522,8 +522,12 @@ function TableRows({ rows, label, attr }: { rows: Record<string, number> | null
)
}
/** 接受逐条:种类、核对、理由、署名(未认证)、时间 */
function AcceptanceList({ items, className }: { items: ImportAcceptance[]; className?: string }) {
/**
* 接受逐条:种类、核对、理由、署名、时间。理由由提交这一期的人写:署名和导入记录的一样时,是不是登录身份按记录(清单里记下的)说
*/
function AcceptanceList({ items, className, signer }: {
items: ImportAcceptance[]; className?: string; signer?: { signed_by: string | null; signed_by_verified?: boolean } | null
}) {
if (!items.length) return null
return (
<ul className={clsx('space-y-1', className)}>
@@ -534,7 +538,9 @@ function AcceptanceList({ items, className }: { items: ImportAcceptance[]; class
<span className="min-w-0 flex-1 break-words">{VT.acceptRow(a.check_id, a.reason)}</span>
</div>
<div className="flex flex-wrap gap-x-2 text-2xs text-faint">
<span>{VT.signedBy(a.signed_by || VT.unsigned)}</span>
<span>{a.signed_by
? signerText(a.signed_by, !!signer?.signed_by_verified && a.signed_by === signer.signed_by)
: VT.signedBy(VT.unsigned)}</span>
{a.at && <span className="tnum">{formatDateTime(a.at)}</span>}
</div>
</li>
@@ -654,10 +660,12 @@ function ImportsTab({ imports, busy, onManifest, onPurge }: {
<div className="flex flex-wrap gap-x-3 gap-y-1 text-2xs text-faint">
<span>{r.recipe_seq != null ? VT.recipeSeq(r.recipe_seq) : r.recipe_id ? '' : VT.simpleImport}</span>
{r.created_at && <span className="tnum">{formatDateTime(r.created_at)}</span>}
{r.signed_by && <span>{VT.signedBy(r.signed_by)}</span>}
{r.signed_by && <span data-record-signer={r.signed_by_verified ? 'verified' : 'self'}>
{signerText(r.signed_by, !!r.signed_by_verified)}
</span>}
</div>
<TableRows rows={r.rows} attr="data-record-rows" />
<AcceptanceList items={r.acceptances ?? []} />
<AcceptanceList items={r.acceptances ?? []} signer={r} />
<div className="flex flex-wrap items-center gap-1.5">
<button type="button" className="btn btn-xs" data-manifest-open={r.id} onClick={() => onManifest(r)}>
<FileText size={11} aria-hidden /> {VT.manifest}
+2
View File
@@ -1260,6 +1260,8 @@ export interface ImportRecord {
period_start: string | null
period_end: string | null
signed_by: string | null
/** 署名是不是认证过的身份:按这一期导入清单里记下的(05-已知遗留 (c)),旧清单记的是 false。老后端不给 */
signed_by_verified?: boolean
/** 接受的条数 */
overrides: number
waivers: number