From ec0b78c550b80f702d5d0e3c5fecee14eef315d0 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Tue, 28 Jul 2026 02:33:09 +0800 Subject: [PATCH] =?UTF-8?q?UI=E2=91=A1:=E6=95=B0=E6=8D=AE=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E5=85=A8=E7=94=A8=E4=B8=AD=E6=96=87=E5=90=8D=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20label=20=E9=9A=8F=E7=89=A9=E5=8C=96?= =?UTF-8?q?=E8=90=BD=20PG=20=E6=B3=A8=E9=87=8A,=E5=85=A8=E9=93=BE=E8=AF=BB?= =?UTF-8?q?=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前 label 在建表那一步就丢了,治理台只剩 project__intake_record 这种键给人看。 用表/列注释而非另建元数据表:名字与表同生共死,不会对不上。 设计台/运营台/对象详情/触发器下拉一律中文名在前、键在后。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../apps/api/src/modules/ledger/contract.d.ts | 5 +- .../apps/api/src/modules/ledger/port.d.ts | 5 +- .../contracts/src/api/governance.d.ts | 4 ++ apps/api/src/app/routes/governance-ops.ts | 7 ++- apps/api/src/modules/ledger/CONTRACT.md | 7 +++ apps/api/src/modules/ledger/contract.ts | 3 +- .../ledger/internal/adapter/ledger-store.ts | 57 ++++++++++++++++--- .../ledger/internal/domain/schema-merge.ts | 4 ++ apps/api/src/modules/ledger/port.ts | 3 +- .../src/features/gov-data/internal/panel.tsx | 32 +++++++---- .../features/gov-trigger/internal/panel.tsx | 4 +- packages/contracts/src/api/governance.ts | 8 ++- 12 files changed, 113 insertions(+), 26 deletions(-) diff --git a/api-report/apps/api/src/modules/ledger/contract.d.ts b/api-report/apps/api/src/modules/ledger/contract.d.ts index 6b8f7c7..6dc50b7 100644 --- a/api-report/apps/api/src/modules/ledger/contract.d.ts +++ b/api-report/apps/api/src/modules/ledger/contract.d.ts @@ -72,7 +72,10 @@ export type ReleaseWriteResult = { export type LedgerService = { applyDeclarations(declarations: readonly ObjectDeclaration[]): Promise; describe(address: ObjectAddress): Promise; - listObjects(): Promise; + /** 出参带人读名(表注释);没登记就没有 label,呈现退回对象键 */ + listObjects(): Promise>; systemWrite(write: SystemWrite): Promise; draftWrite(write: DraftWrite): Promise; releaseWrite(taskId: TaskId): Promise; diff --git a/api-report/apps/api/src/modules/ledger/port.d.ts b/api-report/apps/api/src/modules/ledger/port.d.ts index dd0211f..6bbe988 100644 --- a/api-report/apps/api/src/modules/ledger/port.d.ts +++ b/api-report/apps/api/src/modules/ledger/port.d.ts @@ -25,7 +25,10 @@ export type ObjectRef = { export type LedgerStore = { /** 已物化对象的列与主键;未物化返回 undefined */ describe(ref: ObjectRef): Promise; - listObjects(): Promise; + /** 出参带人读名(表注释);没登记就没有 label,呈现退回对象键 */ + listObjects(): Promise>; /** 执行「只增不破坏」的变更计划 */ applyChanges(ref: ObjectRef, changes: readonly SchemaChange[]): Promise; select(query: LedgerQuery): Promise<{ diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts index 56d66c8..84b7a61 100644 --- a/api-report/packages/contracts/src/api/governance.d.ts +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -1626,10 +1626,12 @@ export declare const listToolsRoute: { export declare const listLedgerObjectsRoute: { readonly method: "GET"; readonly path: "/api/gov/data/objects"; + /** `label` 是人读名(落在表注释上);缺省即没登记,呈现退回对象键 */ readonly output: z.ZodObject<{ objects: z.ZodArray; }, z.core.$strip>>; }, z.core.$strip>; }; @@ -1641,9 +1643,11 @@ export declare const browseLedgerRoute: { object: z.ZodString; }, z.core.$strip>; readonly output: z.ZodObject<{ + label: z.ZodOptional; columns: z.ZodArray; }, z.core.$strip>>; primaryKey: z.ZodArray; rows: z.ZodArray>; diff --git a/apps/api/src/app/routes/governance-ops.ts b/apps/api/src/app/routes/governance-ops.ts index 023c7ac..98d63f8 100644 --- a/apps/api/src/app/routes/governance-ops.ts +++ b/apps/api/src/app/routes/governance-ops.ts @@ -441,7 +441,12 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai // 记录浏览只读正式版 —— 跨任务只见过门的事实 const page = await container.ledger.query({ address, where: {}, orderBy: [], limit: 50, offset: 0 }); return c.json({ - columns: described.columns.map((col) => ({ name: col.name, type: col.type })), + ...(described.label === undefined ? {} : { label: described.label }), + columns: described.columns.map((col) => ({ + name: col.name, + type: col.type, + ...(col.label === undefined ? {} : { label: col.label }), + })), primaryKey: [...described.primaryKey], rows: page.rows, total: page.total, diff --git a/apps/api/src/modules/ledger/CONTRACT.md b/apps/api/src/modules/ledger/CONTRACT.md index f69ebc8..51fe0f4 100644 --- a/apps/api/src/modules/ledger/CONTRACT.md +++ b/apps/api/src/modules/ledger/CONTRACT.md @@ -35,3 +35,10 @@ 标识符过 `IDENT` 正则 + `quoteIdent` 影响半径: index/contract/port 未变 → 对外零影响 + +近变(2026-07-28):**人读名落库** —— 物化时把声明里的 `label` 写成 PG 的表/列注释, + `describe` 与 `listObjects` 一并读回(`MaterializedObject.label` / + `MaterializedColumn.label` / `listObjects()` 出参加 `label?`)。 + 此前 label 在建表那一步就丢了,治理台只剩 `project__intake_record` 这种键给人看。 + 用注释而非另建元数据表:名字与表同生共死,不会对不上。 + `comment on` 是工具语句不吃占位符,字面量在 adapter 内转义单引号。 diff --git a/apps/api/src/modules/ledger/contract.ts b/apps/api/src/modules/ledger/contract.ts index e6797f6..b01cf29 100644 --- a/apps/api/src/modules/ledger/contract.ts +++ b/apps/api/src/modules/ledger/contract.ts @@ -91,7 +91,8 @@ export type ReleaseWriteResult = { export type LedgerService = { applyDeclarations(declarations: readonly ObjectDeclaration[]): Promise; describe(address: ObjectAddress): Promise; - listObjects(): Promise; + /** 出参带人读名(表注释);没登记就没有 label,呈现退回对象键 */ + listObjects(): Promise>; systemWrite(write: SystemWrite): Promise; draftWrite(write: DraftWrite): Promise; diff --git a/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts b/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts index 3b6182f..cefe31e 100644 --- a/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts +++ b/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts @@ -63,13 +63,21 @@ function buildStore(runner: Queryable, pool: Pool): LedgerStore { const store: LedgerStore = { async describe(ref) { const table = tableNameOf(ref); - const cols = await runner.query<{ column_name: string; data_type: string }>( - `select column_name, data_type from information_schema.columns - where table_schema = $1 and table_name = $2 order by ordinal_position`, + // 列注释即人读名:与列定义同一次查询取回,不多跑一趟 + const cols = await runner.query<{ column_name: string; data_type: string; label: string | null }>( + `select c.column_name, c.data_type, + col_description(($1 || '.' || quote_ident($2))::regclass, c.ordinal_position) as label + from information_schema.columns c + where c.table_schema = $1 and c.table_name = $2 order by c.ordinal_position`, [LEDGER_SCHEMA, table], ); if (cols.rows.length === 0) return undefined; + const tableLabel = await runner.query<{ label: string | null }>( + `select obj_description(($1 || '.' || quote_ident($2))::regclass) as label`, + [LEDGER_SCHEMA, table], + ); + const pk = await runner.query<{ attname: string }>( `select a.attname from pg_index i join pg_attribute a on a.attrelid = i.indrelid and a.attnum = any(i.indkey) @@ -77,30 +85,53 @@ function buildStore(runner: Queryable, pool: Pool): LedgerStore { [`${LEDGER_SCHEMA}.${quoteIdent(table)}`], ); + const label = tableLabel.rows[0]?.label ?? undefined; const materialized: MaterializedObject = { domain: ref.domain, object: ref.object, - columns: cols.rows.map((c) => ({ name: c.column_name, type: FROM_PG[c.data_type] ?? 'text' })), + ...(label === null || label === undefined ? {} : { label }), + columns: cols.rows.map((c) => ({ + name: c.column_name, + type: FROM_PG[c.data_type] ?? 'text', + ...(c.label === null ? {} : { label: c.label }), + })), primaryKey: pk.rows.map((p) => p.attname), }; return materialized; }, async listObjects() { - const res = await runner.query<{ table_name: string }>( - `select table_name from information_schema.tables - where table_schema = $1 and table_name not like '\\_\\_%' order by table_name`, + // 表注释即人读名 —— 与表名同一次查询取回,治理台不必逐个 describe 才知道叫什么 + const res = await runner.query<{ table_name: string; label: string | null }>( + `select t.table_name, + obj_description(($1 || '.' || quote_ident(t.table_name))::regclass) as label + from information_schema.tables t + where t.table_schema = $1 and t.table_name not like '\\_\\_%' order by t.table_name`, [LEDGER_SCHEMA], ); return res.rows.flatMap((r) => { const idx = r.table_name.indexOf('__'); if (idx <= 0) return []; - return [{ domain: r.table_name.slice(0, idx), object: r.table_name.slice(idx + 2) }]; + return [ + { + domain: r.table_name.slice(0, idx), + object: r.table_name.slice(idx + 2), + ...(r.label === null ? {} : { label: r.label }), + }, + ]; }); }, async applyChanges(ref, changes) { const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`; + /* + * 注释即人读名;没有 label 就不写,读回来是 null,呈现自然退回键名。 + * `comment on` 是**工具语句**,不吃 $1 占位符 —— 只能拼字面量,故自己转义单引号。 + */ + const comment = async (target: string, label: string | undefined) => { + if (label === undefined || label.trim() === '') return; + await runner.query(`comment on ${target} is '${label.replaceAll("'", "''")}'`); + }; for (const change of changes) { switch (change.kind) { case 'create-table': { @@ -110,12 +141,22 @@ function buildStore(runner: Queryable, pool: Pool): LedgerStore { ? `, primary key (${change.object.primaryKey.map(quoteIdent).join(', ')})` : ''; await runner.query(`create table if not exists ${table} (${cols.join(', ')}${pk})`); + /* + * 人读名落**表/列注释** —— 声明里带的 label 不能在建表这一步丢掉: + * 丢了,治理台就只剩 `project__intake_record` 这种键给人看。 + * 用注释而不是另建元数据表:名字与表同生共死,不会对不上。 + */ + await comment(`table ${table}`, change.object.label); + for (const f of change.object.fields) { + await comment(`column ${table}.${quoteIdent(f.name)}`, f.label); + } break; } case 'add-column': await runner.query( `alter table ${table} add column if not exists ${quoteIdent(change.field.name)} ${PG_TYPE[change.field.type]}`, ); + await comment(`column ${table}.${quoteIdent(change.field.name)}`, change.field.label); break; case 'widen-column': await runner.query( diff --git a/apps/api/src/modules/ledger/internal/domain/schema-merge.ts b/apps/api/src/modules/ledger/internal/domain/schema-merge.ts index e9923ab..e3f3aec 100644 --- a/apps/api/src/modules/ledger/internal/domain/schema-merge.ts +++ b/apps/api/src/modules/ledger/internal/domain/schema-merge.ts @@ -185,11 +185,15 @@ export function mergeDeclarations(declarations: readonly ObjectDeclaration[]): M export type MaterializedColumn = { readonly name: string; readonly type: LedgerFieldType; + /** 人读名 —— 落在 PG 的列注释上,建表/加列时随声明写下 */ + readonly label?: string | undefined; }; export type MaterializedObject = { readonly domain: string; readonly object: string; + /** 人读名 —— 落在 PG 的表注释上;没有就退回对象键 */ + readonly label?: string | undefined; readonly columns: readonly MaterializedColumn[]; readonly primaryKey: readonly string[]; }; diff --git a/apps/api/src/modules/ledger/port.ts b/apps/api/src/modules/ledger/port.ts index 8f0e310..603d481 100644 --- a/apps/api/src/modules/ledger/port.ts +++ b/apps/api/src/modules/ledger/port.ts @@ -20,7 +20,8 @@ export type ObjectRef = { readonly domain: string; readonly object: string }; export type LedgerStore = { /** 已物化对象的列与主键;未物化返回 undefined */ describe(ref: ObjectRef): Promise; - listObjects(): Promise; + /** 出参带人读名(表注释);没登记就没有 label,呈现退回对象键 */ + listObjects(): Promise>; /** 执行「只增不破坏」的变更计划 */ applyChanges(ref: ObjectRef, changes: readonly SchemaChange[]): Promise; diff --git a/apps/web/src/features/gov-data/internal/panel.tsx b/apps/web/src/features/gov-data/internal/panel.tsx index c63eeee..fcc5738 100644 --- a/apps/web/src/features/gov-data/internal/panel.tsx +++ b/apps/web/src/features/gov-data/internal/panel.tsx @@ -55,8 +55,9 @@ export function DataHome({ nav }: HomeProps) { nav.push({ k: 'obj', id: `${o.domain}.${o.object}` })} /> ))} @@ -95,7 +96,13 @@ export function LedgersHome({ tab, nav }: HomeProps) { ) : ( {inDomain.map((o) => ( - + ))} )} @@ -105,14 +112,14 @@ export function LedgersHome({ tab, nav }: HomeProps) { } /** 一个数据对象一行:行数是运营侧最想先看到的那个数 */ -function LedgerObjectRow({ domain, object, nav }: { domain: string; object: string; nav: Nav }) { +function LedgerObjectRow({ domain, object, label, nav }: { domain: string; object: string; label?: string | undefined; nav: Nav }) { const page = useGovLedger(domain, object); const total = page.data?.total; return ( 0 ? 'brand' : 'mute'} ico="table" - title={object} + title={label ?? page.data?.label ?? object} sub={`${domain}.${object}`} cols={[ { k: '行数', v: total === undefined ? '…' : String(total) }, @@ -137,8 +144,8 @@ export function ObjPage({ id }: { id: string }) { <> {columns.length} 列 @@ -152,8 +159,11 @@ export function ObjPage({ id }: { id: string }) {
{columns.map((c) => (
- {c.name} - {c.type} + {/* 人读名在前、字段键在后:填表的人看名字,写映射的人看键 */} + {c.label ?? c.name} + + {c.name} · {c.type} +
))}
@@ -167,7 +177,9 @@ export function ObjPage({ id }: { id: string }) { {columns.map((c) => ( - {c.name} + + {c.label ?? c.name} + ))} diff --git a/apps/web/src/features/gov-trigger/internal/panel.tsx b/apps/web/src/features/gov-trigger/internal/panel.tsx index 1921960..3a05626 100644 --- a/apps/web/src/features/gov-trigger/internal/panel.tsx +++ b/apps/web/src/features/gov-trigger/internal/panel.tsx @@ -41,6 +41,7 @@ import { } from '@web-shared/gov/primitives.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { useGovObjects, useGovPackages, useGovTriggers } from '@web-shared/gov/gov-api.ts'; +import { domainShort } from '@web-shared/gov/domains.ts'; export type HomeProps = { tab: string; nav: Nav }; @@ -294,7 +295,8 @@ function TriggerForm({ > {(objects.data?.objects ?? []).map((o) => ( ))} diff --git a/packages/contracts/src/api/governance.ts b/packages/contracts/src/api/governance.ts index bfc6dfd..7f4aac9 100644 --- a/packages/contracts/src/api/governance.ts +++ b/packages/contracts/src/api/governance.ts @@ -361,7 +361,10 @@ export const listToolsRoute = { export const listLedgerObjectsRoute = { method: 'GET', path: '/api/gov/data/objects', - output: z.object({ objects: z.array(z.object({ domain: z.string(), object: z.string() })) }), + /** `label` 是人读名(落在表注释上);缺省即没登记,呈现退回对象键 */ + output: z.object({ + objects: z.array(z.object({ domain: z.string(), object: z.string(), label: z.string().optional() })), + }), } as const satisfies RouteContract; export const browseLedgerRoute = { @@ -369,7 +372,8 @@ export const browseLedgerRoute = { path: '/api/gov/data/:domain/:object', params: z.object({ domain: z.string(), object: z.string() }), output: z.object({ - columns: z.array(z.object({ name: z.string(), type: z.string() })), + label: z.string().optional(), + columns: z.array(z.object({ name: z.string(), type: z.string(), label: z.string().optional() })), primaryKey: z.array(z.string()), rows: z.array(z.record(z.string(), z.unknown())), total: z.number().int(),