node-resource-utils 1.1.11 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/resources/README.md +10 -11
  3. package/resources/_.npmrc +1 -3
  4. package/resources/prisma/org.config.ts +11 -11
  5. package/resources/prisma/schema.prisma +1 -1
  6. package/resources/prisma/seed.ts +17 -17
  7. package/resources/prisma/seedSafe.ts +10 -10
  8. package/resources/scripts/check-config.mjs +13 -13
  9. package/resources/scripts/suggest-relations.mjs +3 -3
  10. package/resources/src/app/(admin)/ai/page.tsx +1 -1
  11. package/resources/src/app/(admin)/dashboard/page.tsx +3 -3
  12. package/resources/src/app/(admin)/demo/loading/page.tsx +1 -1
  13. package/resources/src/app/(admin)/error.tsx +1 -1
  14. package/resources/src/app/(admin)/feedback/page.tsx +1 -1
  15. package/resources/src/app/(admin)/system/depts/page.tsx +1 -1
  16. package/resources/src/app/api/ai/ask/route.ts +1 -1
  17. package/resources/src/app/api/ai/report/route.ts +1 -1
  18. package/resources/src/app/api/badges/route.ts +1 -1
  19. package/resources/src/app/api/docs/parse/route.ts +1 -1
  20. package/resources/src/app/api/feedback/ai/route.ts +1 -1
  21. package/resources/src/auth.ts +1 -1
  22. package/resources/src/components/FeedbackReportModal.tsx +1 -1
  23. package/resources/src/components/Markdown.tsx +1 -1
  24. package/resources/src/components/feedback/AsyncButton.tsx +1 -1
  25. package/resources/src/components/feedback/README.md +1 -1
  26. package/resources/src/lib/aiContext.ts +1 -1
  27. package/resources/src/lib/api.ts +1 -1
  28. package/resources/src/lib/appInfo.ts +8 -8
  29. package/resources/src/lib/appearance.ts +1 -1
  30. package/resources/src/lib/badges.test.ts +1 -1
  31. package/resources/src/lib/docs-shared.ts +1 -1
  32. package/resources/src/lib/docs.ts +1 -1
  33. package/resources/src/lib/err-text.ts +1 -1
  34. package/resources/src/lib/excel.ts +1 -1
  35. package/resources/src/lib/features.ts +3 -3
  36. package/resources/src/lib/http.ts +2 -2
  37. package/resources/src/lib/office.ts +1 -1
  38. package/resources/src/lib/ontology.ts +2 -2
  39. package/resources/src/lib/pending.ts +1 -1
  40. package/resources/src/lib/search-data.ts +1 -1
  41. package/resources/src/lib/system-config.ts +1 -1
  42. package/resources/src/lib/validation.ts +1 -1
  43. package/resources/src/lib/visit-stats.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-resource-utils",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
4
  "description": "Utilities for synchronizing reusable Node.js project resources.",
5
5
  "main": "lib/sync.js",
6
6
  "license": "MIT",
@@ -21,8 +21,8 @@ and a knowledge graph.
21
21
  ## Getting started
22
22
 
23
23
  ```bash
24
- npm install
25
- npm run db:reset # create the database and seed demo data
24
+ # dependencies are already installed (node_modules ships with the project)
25
+ npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts
26
26
  npm run dev
27
27
  ```
28
28
 
@@ -31,29 +31,28 @@ The dev server listens on http://localhost:3000.
31
31
  Default accounts are provisioned by the seed script — see `prisma/seed.ts`.
32
32
 
33
33
  Organization structure is **not** hardcoded in the seed: edit `prisma/org.config.ts`
34
- (`ORG_TREE`) to match the departments required by your brief. The seed then distributes
34
+ (`ORG_TREE`) to match the departments required by your project. The seed then distributes
35
35
  roster users evenly across all *leaf* departments (`PEOPLE_TOTAL` / `PEOPLE_MIN_PER_DEPT`),
36
36
  so no department ends up empty. Set `ORG_SOURCE = 'existing'` to keep departments that were
37
- created through the UI instead. After changing the tree, run `npm run db:reset` — plain
38
- `db:seed` only upserts and will leave the previous departments behind.
37
+ created through the UI instead. After changing the tree, drop and recreate the database as
38
+ shown above — seeding alone only upserts and will leave the previous departments behind.
39
39
 
40
40
  ## Scripts
41
41
 
42
42
  | Command | Description |
43
43
  | --- | --- |
44
44
  | `npm run dev` | Start the development server |
45
- | `npm run build` | Production build (runs config validation first) |
45
+ | `npm run build` | Production build |
46
46
  | `npm start` | Serve the production build |
47
47
  | `npm run typecheck` | TypeScript type check, no emit |
48
48
  | `npm run lint` | ESLint |
49
49
  | `npm test` | Unit tests (Vitest) |
50
50
  | `npm run db:push` | Sync the Prisma schema to the database |
51
- | `npm run db:seed` | Seed demo data |
52
- | `npm run db:reset` | Drop, recreate, and reseed the database |
53
51
  | `npm run db:studio` | Open Prisma Studio |
54
- | `npm run check:config` | Validate system identity and ontology configuration |
55
- | `npm run relations` | Inspect schema relations before editing the knowledge-graph ontology |
56
- | `npm run verify` | Full gate: config check + type check + production build |
52
+ | `npx tsx prisma/seed.ts` | Seed demo data |
53
+ | `npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts` | Drop, recreate, and reseed the database |
54
+ | `node scripts/check-config.mjs` | Validate system identity and ontology configuration |
55
+ | `node scripts/suggest-relations.mjs` | Inspect schema relations before editing the knowledge-graph ontology |
57
56
 
58
57
  ## Project structure
59
58
 
package/resources/_.npmrc CHANGED
@@ -1,5 +1,3 @@
1
- # 内网离线:统一走本地私服,禁止直连外网
2
- registry=http://127.0.0.1:4873/
3
- strict-ssl=false
1
+ # 依赖已随项目提供(node_modules 预装);确需安装新包时走默认 registry
4
2
  audit=false
5
3
  fund=false
@@ -1,12 +1,12 @@
1
1
  /**
2
- * 组织架构配置 —— 比赛时**只改这个文件**。
2
+ * 组织架构配置 —— **只在这个文件里改**。
3
3
  *
4
4
  * ## 为什么单独抽出来
5
5
  *
6
- * 题目里的**部门由题意决定,一个字都不能编**;而人员姓名可以随便造。
6
+ * 需求里的**部门由需求决定,一个字都不能编**;而人员姓名可以随便造。
7
7
  * 以前部门写死在 seed.ts 里(HQ / 技术部 / 市场部 / 人力资源部),
8
- * 一跑 seed 就把这 4 个题目外部门塞进「部门管理」,人员也全被分到这三个部门,
9
- * 题目要的部门里一个人都没有 —— 评委点开就能看出是模板数据。
8
+ * 一跑 seed 就把这 4 个范围外部门塞进「部门管理」,人员也全被分到这三个部门,
9
+ * 需求要的部门里一个人都没有 —— 用户点开就能看出是模板数据。
10
10
  *
11
11
  * 现在把「部门」和「人员」解耦:
12
12
  * - 部门树在本文件 ORG_TREE 里声明(唯一来源,改了重跑 seed 即生效)
@@ -16,18 +16,18 @@
16
16
  * ## 两种工作流,二选一
17
17
  *
18
18
  * ### ① 配置驱动(默认,推荐)—— 部门写在下面
19
- * 把 ORG_TREE 换成题目给的部门树,然后 `npm run db:reset`。
19
+ * 把 ORG_TREE 换成需求给的部门树,然后 `npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts`。
20
20
  * code 是 upsert 主键,自己定,全库唯一、之后别改(业务表按它找部门)。
21
21
  *
22
22
  * ### ② 库里现有(ORG_SOURCE = 'existing')—— 部门已在界面里录好
23
- * 如果你已经在「系统管理 → 部门管理」把题目部门手工建完了,
23
+ * 如果你已经在「系统管理 → 部门管理」把需求部门手工建完了,
24
24
  * 把 ORG_SOURCE 改成 'existing':seed **完全不碰部门表**,
25
25
  * 只按库里现有的叶子部门铺人员。适合「先录组织、再补数据」的节奏。
26
26
  *
27
27
  * ## 注意
28
28
  *
29
- * - 改了部门树之后要 `npm run db:reset`(或先清空部门表)。
30
- * `db:seed` 只 upsert、不删旧数据,旧的 HQ/TECH/SALES/HR 会残留在库里。
29
+ * - 改了部门树之后要 `npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts`(或先清空部门表)。
30
+ * 只跑 seed 只 upsert、不删旧数据,旧的 HQ/TECH/SALES/HR 会残留在库里。
31
31
  * - 业务单据里的部门字段一律引用这里建出来的部门 id,不要再手写部门名。
32
32
  */
33
33
 
@@ -45,16 +45,16 @@ export const ORG_SOURCE: 'config' | 'existing' = 'config';
45
45
  export interface OrgNode {
46
46
  /** 部门编码,upsert 主键。建议用拼音首字母或短英文,如 BGS / CWK / CGK */
47
47
  code: string;
48
- /** 部门名称,必须与题意完全一致 */
48
+ /** 部门名称,必须与需求完全一致 */
49
49
  name: string;
50
50
  children?: OrgNode[];
51
51
  }
52
52
 
53
53
  /**
54
- * ★★ 比赛时改这里 ★★
54
+ * ★★ 按需改这里 ★★
55
55
  *
56
56
  * 下面是骨架自带的通用组织(4 个部门),只作开箱演示。
57
- * 题目给了部门清单就整段替换掉,层级照题目写:
57
+ * 需求给了部门清单就整段替换掉,层级照需求写:
58
58
  *
59
59
  * ```ts
60
60
  * export const ORG_TREE: OrgNode[] = [
@@ -134,7 +134,7 @@ model Menu {
134
134
  keywords String? // 搜索别名:空格或逗号分隔的口语词,全局搜索用(如「请假 销假」)
135
135
  /// 侧边栏红色数字徽标的计数源(如 feedback:attention)。
136
136
  /// 为空 = 该菜单不显示徽标。取值必须是 src/lib/badges.ts 里 BADGE_SOURCES 的 key,
137
- /// 且 badge-count.ts 里有同名计数器 —— npm run check:config 会拦不一致的情况。
137
+ /// 且 badge-count.ts 里有同名计数器 —— scripts/check-config.mjs 会拦不一致的情况。
138
138
  badgeKey String?
139
139
  type String @default("menu") // dir 目录 | menu 菜单 | button 按钮
140
140
  permCode String? @unique // 权限标识,如 system:user:add
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * 种子数据。
3
3
  *
4
- * 跑法: npm run db:seed (首次先 npm run db:push)
5
- * 想连数据一起重置: npm run db:reset
4
+ * 跑法: npx tsx prisma/seed.ts (首次先 npm run db:push)
5
+ * 想连数据一起重置: npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts
6
6
  *
7
7
  * 这里刻意自带一个 PrismaClient 实例,不走 src/lib/prisma.ts ——
8
8
  * 那个文件用了 `@/` 路径别名,tsx 直接跑脚本时不一定能解析,
@@ -11,7 +11,7 @@
11
11
  * 幂等:全部用 upsert,重复跑不会产生重复数据。
12
12
  *
13
13
  * ★ 部门不在本文件里写死 —— 见 prisma/org.config.ts。
14
- * 题目给了部门清单,改那一个文件即可;人员会按叶子部门自动铺满。
14
+ * 需求给了部门清单,改那一个文件即可;人员会按叶子部门自动铺满。
15
15
  */
16
16
  import { PrismaClient } from '@prisma/client';
17
17
  import { PrismaLibSql } from '@prisma/adapter-libsql';
@@ -46,7 +46,7 @@ type MenuSeed = {
46
46
 
47
47
  const MENUS: MenuSeed[] = [
48
48
  // ============================================================
49
- // ★★ 业务模块放在最前面!把题目的菜单插在这段注释下面(在所有系统菜单之前)
49
+ // ★★ 业务模块放在最前面!把需求的菜单插在这段注释下面(在所有系统菜单之前)
50
50
  // 脚手架自带的「工作台 / AI 能力 / 问题反馈 / 系统管理」都排在业务模块之后。
51
51
  // keywords = 全局搜索别名(Ctrl K):用户口头怎么叫这个功能就写什么。
52
52
  // 示例(请销假系统):
@@ -281,7 +281,7 @@ function pickLeaves(rows: DeptRow[]): DeptRow[] {
281
281
  * 人员 → 部门的分配计划。
282
282
  *
283
283
  * 交错轮询:先给每个叶子部门 1 人,再给每个部门第 2 人……天然均衡,
284
- * 且部门增删后不用改代码(老版本写死 `rosterDepts[i % 3]`,题目部门一换就全错)。
284
+ * 且部门增删后不用改代码(老版本写死 `rosterDepts[i % 3]`,需求部门一换就全错)。
285
285
  * 总数不足时以「叶子数 × minPerDept」为准扩容,保证没有空部门。
286
286
  */
287
287
  function planDeptOf(leafIds: string[], total: number, minPerDept: number): string[] {
@@ -302,15 +302,15 @@ async function main() {
302
302
  console.log('开始初始化种子数据 ...');
303
303
 
304
304
  // ---- 部门(唯一来源:prisma/org.config.ts)----
305
- // ★ 题目部门必须原样落地,一个题目外部门都不能混进来。
305
+ // ★ 需求部门必须原样落地,一个范围外部门都不能混进来。
306
306
  // 老版本把 HQ/技术部/市场部/人力资源部 写死在这里:一跑 seed 组织架构就被污染,
307
- // 名册人员也全被分到这三个部门,题目要的部门点进去一个人都没有。
307
+ // 名册人员也全被分到这三个部门,需求要的部门点进去一个人都没有。
308
308
  if (ORG_SOURCE === 'existing') {
309
309
  const has = await prisma.department.count();
310
310
  if (!has) {
311
311
  throw new Error(
312
312
  'ORG_SOURCE = existing,但部门表是空的。\n' +
313
- ' 要么先在「系统管理 → 部门管理」把题目部门建好再跑 seed,\n' +
313
+ ' 要么先在「系统管理 → 部门管理」把需求部门建好再跑 seed,\n' +
314
314
  ' 要么把 prisma/org.config.ts 改回 ORG_SOURCE = "config" 并在 ORG_TREE 里声明部门树。'
315
315
  );
316
316
  }
@@ -326,8 +326,8 @@ async function main() {
326
326
 
327
327
  // ★ 配置驱动模式下,人员只在**配置树内**分配。
328
328
  // 不这么收口的话,库里残留的旧部门(换题后没 reset 的 HQ/TECH/SALES/HR)
329
- // 会一起参与"叶子部门"计算,把名册人员抢走一部分 —— 实测 6 个题目部门时
330
- // 有 7 个人被分到旧部门去了,题目部门反而没人。
329
+ // 会一起参与"叶子部门"计算,把名册人员抢走一部分 —— 实测 6 个需求部门时
330
+ // 有 7 个人被分到旧部门去了,需求部门反而没人。
331
331
  const scopedDepts =
332
332
  ORG_SOURCE === 'config' ? allDepts.filter((d) => cfgCodes.includes(d.code)) : allDepts;
333
333
  const leaves = pickLeaves(scopedDepts);
@@ -421,11 +421,11 @@ async function main() {
421
421
  // ---- 用户 ----
422
422
  // ★ 人头闭环:业务单据(请假 / 用车 / 值班 …)里出现的每个人,
423
423
  // 都必须在这里有同名账号 —— 顺序是**先建齐用户,再建业务单据**。
424
- // 反例(评委一眼看穿):请假记录里申请人有十几个人,用户管理里却只有两三个账号。
424
+ // 反例(用户一眼看穿):请假记录里申请人有十几个人,用户管理里却只有两三个账号。
425
425
  // 名册由 makeRoster 从固定姓名池生成(与员工演示名单同源),
426
426
  // 业务演示数据直接引用他们的 username/name,不要再用 personName() 现编姓名往单据里塞。
427
427
  //
428
- // ★ 部门分配见 planDeptOf:按叶子部门交错铺满,题目部门增删都不用改代码。
428
+ // ★ 部门分配见 planDeptOf:按叶子部门交错铺满,需求部门增删都不用改代码。
429
429
  const plan = planDeptOf(
430
430
  deptPool.map((d) => d.id),
431
431
  PEOPLE_TOTAL,
@@ -522,14 +522,14 @@ async function main() {
522
522
  deptPool.map((d) => `${d.name} ${plan.filter((id) => id === d.id).length}`).join(' ')
523
523
  );
524
524
 
525
- // 改了部门树再跑 db:seed,旧部门会残留(seed 只 upsert、不删数据)。
526
- // 这里只提示、不自动删 —— 万一题目部门是通过界面录的,自动删就闯祸了。
525
+ // 改了部门树再只跑 seed,旧部门会残留(seed 只 upsert、不删数据)。
526
+ // 这里只提示、不自动删 —— 万一需求部门是通过界面录的,自动删就闯祸了。
527
527
  if (ORG_SOURCE === 'config') {
528
528
  const extra = allDepts.filter((d) => !cfgCodes.includes(d.code));
529
529
  if (extra.length) {
530
530
  console.log(
531
531
  ` 注意:库里有 ${extra.length} 个部门不在 org.config 里(${extra.map((d) => d.name).join(' / ')})。` +
532
- `\n 本次没有往这些部门分人。换过部门树的话,跑 npm run db:reset 清干净最稳妥。`
532
+ `\n 本次没有往这些部门分人。换过部门树的话,跑 npx prisma db push --force-reset --accept-data-loss && npx tsx prisma/seed.ts 清干净最稳妥。`
533
533
  );
534
534
  }
535
535
  }
@@ -579,7 +579,7 @@ async function main() {
579
579
 
580
580
  // ---- 操作日志演示数据 ----
581
581
  // 为什么要有:审计是骨架的卖点之一,但日志只能由真实写操作产生。
582
- // 刚跑完 seed 的库是"干净"的,评委第一眼看到的会是空表格 + 首页空的「最近动态」,
582
+ // 刚跑完 seed 的库是"干净"的,用户第一眼看到的会是空表格 + 首页空的「最近动态」,
583
583
  // 看起来像审计没接上。这里补一批与演示数据同源的历史流水(只补一次,重跑 seed 不重复灌)。
584
584
  if ((await prisma.operationLog.count()) === 0) {
585
585
  const seedUsers = [admin, zhangsan, ...rosterUsers.slice(0, 6)];
@@ -700,7 +700,7 @@ async function main() {
700
700
 
701
701
  // ---- 乱码自检 ----
702
702
  // 扫全库字符串字段:emoji / 私用区 / U+FFFD / Latin-1 乱码特征。
703
- // 命中就抛错让 seed 失败 —— 宁可在这里红一次,也不要让评委在列表里看到 🚗
703
+ // 命中就抛错让 seed 失败 —— 宁可在这里红一次,也不要让用户在列表里看到 🚗
704
704
  // 只读操作,不写库;单表取 500 行,SQLite 上是毫秒级。
705
705
  await assertNoMojibake(prisma);
706
706
  console.log(' 乱码自检 OK(全库无 emoji / 无编码残留)');
@@ -18,7 +18,7 @@
18
18
  * Unicode 变体选择符、零宽字符)全部挡在外面。
19
19
  * 2. **安全词库**:演示数据从这里取,不手写字符串。
20
20
  * 3. **断言**:`assertNoMojibake()` 在 seed 结束时扫描全库,
21
- * 发现乱码立刻抛错终止,让问题在构建期暴露,而不是留给评委看到。
21
+ * 发现乱码立刻抛错终止,让问题在构建期暴露,而不是留给用户看到。
22
22
  *
23
23
  * ## 用法(在 prisma/seed.ts 里)
24
24
  *
@@ -35,7 +35,7 @@
35
35
  * 业务单据里的"人"(申请人 / 审批人 / 处理人 …)必须来自 `PERSON_ROSTER` 名册,
36
36
  * 且名册里的人都要在用户表里有账号 —— 顺序是**先建用户,再让单据引用其 id**。
37
37
  * 不要对引用型人员字段用 `personName()` 现编姓名,那样"单据里十几个人、
38
- * 用户管理里两三个账号",评委一眼看穿(见 PERSON_ROSTER 注释)。
38
+ * 用户管理里两三个账号",用户一眼看穿(见 PERSON_ROSTER 注释)。
39
39
  */
40
40
 
41
41
  /* ============================ 1. 安全字符白名单 ============================ */
@@ -111,7 +111,7 @@ export const GIVEN_NAMES = [
111
111
  * const rows = users.map((u) => ({ userId: u.id, ... }));
112
112
  * ```
113
113
  *
114
- * 反例(评委一眼看穿):请假记录里申请人有"李雷、韩梅梅、王强"十几个人,
114
+ * 反例(用户一眼看穿):请假记录里申请人有"李雷、韩梅梅、王强"十几个人,
115
115
  * 打开用户管理却只有 admin / zhangsan 两三个人 —— 单据里的人全是凭空编的。
116
116
  * **单据里出现多少人,用户表里就要有多少个对应账号**,两边人名必须一致。
117
117
  */
@@ -136,7 +136,7 @@ export const PERSON_NAMES = [
136
136
  '李雷', '韩梅梅', '王强', '刘洋', '陈静', '杨帆', '赵敏', '周杰',
137
137
  '吴迪', '郑爽', '孙悦', '马丽', '朱伟', '胡军', '林峰', '高圆',
138
138
  '何洁', '郭涛', '罗宾', '梁静', '许诺', '宋佳', '唐宁', '冯雷',
139
- // 25-64:扩充,供多部门 / 大名单题目使用
139
+ // 25-64:扩充,供多部门 / 大名单场景使用
140
140
  '邓超', '曹颖', '彭勇', '曾毅', '田野', '董倩', '袁泉', '潘石',
141
141
  '蒋雯', '蔡明', '余华', '杜鹏', '叶青', '程亮', '苏晴', '魏然',
142
142
  '吕刚', '丁宁', '任杰', '沈冰', '姚远', '卢俊', '姜涛', '崔健',
@@ -147,7 +147,7 @@ export const PERSON_NAMES = [
147
147
  /**
148
148
  * 从 PERSON_NAMES 生成确定性名册:同 count 每次结果完全一致。
149
149
  *
150
- * username 固定为 user01 / user02 ……(受控命名空间,绝不会撞题目里的人)。
150
+ * username 固定为 user01 / user02 ……(受控命名空间,绝不会撞需求里的人)。
151
151
  */
152
152
  export function makeRoster(count: number): RosterPerson[] {
153
153
  if (count > PERSON_NAMES.length) {
@@ -170,20 +170,20 @@ export const DEPT_NAMES = [
170
170
  '质量管理部', '采购部', '行政部', '法务部', '战略发展部',
171
171
  ];
172
172
 
173
- /** 用车事由(车辆派遣类题目) */
173
+ /** 用车事由(车辆派遣类业务) */
174
174
  export const DISPATCH_REASONS = [
175
175
  '机场接送', '客户接待', '项目出差', '会议通勤', '物料运送',
176
176
  '设备巡检', '外出培训', '商务洽谈', '跨部门协作', '紧急抢修',
177
177
  '文件递送', '现场勘察', '供应商走访', '行业展会', '内部调拨',
178
178
  ];
179
179
 
180
- /** 请假事由(请销假类题目) */
180
+ /** 请假事由(请销假类业务) */
181
181
  export const LEAVE_REASONS = [
182
182
  '个人事务', '探亲', '就医', '婚假', '产假', '陪产假',
183
183
  '年假', '调休', '子女入学', '家中急事', '外出考试', '身体不适',
184
184
  ];
185
185
 
186
- /** 值班事项(值班管理类题目) */
186
+ /** 值班事项(值班管理类业务) */
187
187
  export const DUTY_ITEMS = [
188
188
  '机房巡检完成,设备运行正常', '接待来访人员三名并登记',
189
189
  '处理服务器告警一次,已恢复', '巡查消防通道,无异常',
@@ -258,7 +258,7 @@ const MOJIBAKE_RUN_RE = /[\u00A0-\u024F]{2,}/;
258
258
  * ★ 这里刻意用**黑名单**而不是"白名单放行"。
259
259
  * 旧写法是「不在允许列表里的字符一律报错」,实测会误杀正常中文标点:
260
260
  * 「」『』(U+300C..300F)、×、①、㎡ 都被判成乱码,
261
- * 于是自检一旦接进 seed,`npm run db:seed` 直接失败 —— 这也是它长期没被接线的真实原因。
261
+ * 于是自检一旦接进 seed,`npx tsx prisma/seed.ts` 直接失败 —— 这也是它长期没被接线的真实原因。
262
262
  * 黑名单只拦"肯定是错的"那一类,不会因为作者用了书名号就把整库数据判死。
263
263
  *
264
264
  * 2600–27BF(★☆✓ 这类常用符号)故意不拦:中文文案里出现它们是正常的。
@@ -276,7 +276,7 @@ export interface MojibakeHit {
276
276
  * 扫描所有指定的表,找出乱码字段。
277
277
  *
278
278
  * 用法:seed 末尾调用 `await assertNoMojibake(prisma)`,有乱码直接抛错,
279
- * 让 `npm run db:seed` 失败 —— 问题在构建期暴露,而不是等评委点开列表。
279
+ * 让 `npx tsx prisma/seed.ts` 失败 —— 问题在构建期暴露,而不是等用户点开列表。
280
280
  */
281
281
  export async function findMojibake(
282
282
  // 参数故意用 unknown:调用方传的是 PrismaClient,具体类型随 schema 变化,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 为什么要有这个:只靠口头提醒 AI 改并不可靠,AI 会忘、会跳步、
5
5
  * 会在别的地方写一堆演示文案。这里在构建前硬检查,
6
- * 让"交上去还是个演示系统"变得不可能。
6
+ * 让"上线后还是个演示系统"变得不可能。
7
7
  *
8
8
  * 检查五类:
9
9
  * 系统身份:APP_NAME / APP_DESC / SHOW_DEMO_ACCOUNT_HINT / HOME_STATS
@@ -17,14 +17,14 @@
17
17
  * 功能权限只决定"能不能进页面",它才决定"进去能看哪些行"
18
18
  * 关系覆盖:视图里同时出现的两个本体,如果 schema 里有外键/桥接表连接,
19
19
  * 图上却没有任何路径,直接报错 —— 专治"考试和人员都在图里、却没配关系"。
20
- * 全局漏配(本体不在视图里)只提示不报错,见 npm run relations。
20
+ * 全局漏配(本体不在视图里)只提示不报错,见 node scripts/suggest-relations.mjs。
21
21
  *
22
22
  * 为什么连引用闭合也要查:图谱是"配置驱动"的,ontology.ts 里写错一个 id
23
23
  * (视图引用了不存在的本体、root 不在视图里、关系两端拼错)不会报编译错误,
24
24
  * 只会在打开 /graph 时变成红色「图谱加载失败」,而 tsc 完全看不出来。
25
25
  *
26
26
  * 用法:
27
- * npm run check:config 手动检查
27
+ * node scripts/check-config.mjs 手动检查
28
28
  * npm run build 自动检查(prebuild 钩子)
29
29
  */
30
30
  import { readFileSync, readdirSync } from 'node:fs';
@@ -66,7 +66,7 @@ const appName = grab(appSrc, 'APP_NAME');
66
66
  if (!appName || appName === "'业务管理系统'") {
67
67
  errors.push(
68
68
  'APP_NAME 还是项目默认值「业务管理系统」。' + NL +
69
- ' → 改成题目里的系统名,例如 \'车辆派遣管理系统\''
69
+ ' → 改成需求里的系统名,例如 \'车辆派遣管理系统\''
70
70
  );
71
71
  }
72
72
 
@@ -97,7 +97,7 @@ const demoHint = grab(appSrc, 'SHOW_DEMO_ACCOUNT_HINT');
97
97
  if (demoHint === 'true') {
98
98
  errors.push(
99
99
  'SHOW_DEMO_ACCOUNT_HINT 还是 true,登录页会显示「演示账号:admin / 123456」。' + NL +
100
- ' → 改成 false。这是评委一眼认出"这是演示系统"的最强信号'
100
+ ' → 改成 false。这是用户一眼认出"这是演示系统"的最强信号'
101
101
  );
102
102
  }
103
103
 
@@ -172,8 +172,8 @@ if (ontoSrc) {
172
172
 
173
173
  if (isDefault) {
174
174
  errors.push(
175
- 'ontology.ts 还是基础项目默认的「部门-员工-角色-日志」本体,跟题目无关 —— 知识图谱会显示一堆跟业务不相关的东西。' + NL +
176
- ' → 改成题目的业务本体,例如车辆派遣:部门 / 车辆 / 驾驶员 / 派遣申请' + NL +
175
+ 'ontology.ts 还是基础项目默认的「部门-员工-角色-日志」本体,跟需求无关 —— 知识图谱会显示一堆跟业务不相关的东西。' + NL +
176
+ ' → 改成需求的业务本体,例如车辆派遣:部门 / 车辆 / 驾驶员 / 派遣申请' + NL +
177
177
  ' → 每个 entity 都要给 layer: 0/1/2(数值小的在左)'
178
178
  );
179
179
  }
@@ -390,7 +390,7 @@ if (panelSrc && !/\/api\/ai\/act/.test(panelSrc)) {
390
390
  /**
391
391
  * 视图内的"该连未连"是硬错误:
392
392
  * 两个本体都摆进图里了,schema 也有外键或桥接表,图上却连不起来 ——
393
- * 评委一眼就能看出"考试和人员没关系?不对吧"。
393
+ * 用户一眼就能看出"考试和人员没关系?不对吧"。
394
394
  * 允许两跳可达(A—C—B),因为那至少是连通的;完全不通才报错。
395
395
  */
396
396
  const schemaForRel = readOrNull(SCHEMA);
@@ -419,7 +419,7 @@ if (ontoRaw && schemaForRel) {
419
419
  if (reported.has(key) || reported.has(`${m.bEntity}|${m.aEntity}|${m.kind}|${m.bridgeModel ?? ''}`)) continue;
420
420
  const a = rel.entities.find((e) => e.id === m.aEntity)?.label ?? m.aEntity;
421
421
  const b = rel.entities.find((e) => e.id === m.bEntity)?.label ?? m.bEntity;
422
- warnings.push(`${a} ↔ ${b}(${describe(m)})未在 relations 里声明 —— 跑 npm run relations 看建议片段`);
422
+ warnings.push(`${a} ↔ ${b}(${describe(m)})未在 relations 里声明 —— 跑 node scripts/suggest-relations.mjs 看建议片段`);
423
423
  }
424
424
  }
425
425
 
@@ -699,7 +699,7 @@ if (!badgesSrc || !badgeCountSrc) {
699
699
  * `lib/appearance.ts`(token)、`lib/skin-theme.ts`(antd 主题)、
700
700
  * `components/appearance/*`(面板与状态)三块撑起来。
701
701
  *
702
- * 为什么放在硬检查里:换题目后 AI 重写业务页面完全正常,但很容易顺手把
702
+ * 为什么放在硬检查里:换需求后 AI 重写业务页面完全正常,但很容易顺手把
703
703
  * 「看起来没用」的外观面板、顶栏入口、根布局里的样式注入删掉 ——
704
704
  * 删了不会报编译错误,只会让"外观设置"这个功能凭空消失,
705
705
  * 而且换皮肤后页面还在用写死的 #fff / 13px,看起来像做了一半。
@@ -752,9 +752,9 @@ if (layoutSrc && !/buildAppearanceCss/.test(layoutSrc)) {
752
752
  /* ============================ 4. 输出 ============================ */
753
753
 
754
754
  if (errors.length) {
755
- console.error('\n✗ 系统还没按题目配置(构建已中止)\n');
755
+ console.error('\n✗ 系统还没按需求配置(构建已中止)\n');
756
756
  errors.forEach((e, i) => console.error(` ${i + 1}) ${e}\n`));
757
- console.error(' 改完再跑一次:npm run check:config');
757
+ console.error(' 改完再跑一次:node scripts/check-config.mjs');
758
758
  console.error(` 涉及文件:${APPINFO} / ${ONTOLOGY} / ${SCHEMA} / ${DEPT_PAGE} / ${SEED} / ${FEATURES_FILE}\n`);
759
759
  if (warnings.length) {
760
760
  console.error(' 另外还有几条"可能漏配"的提示(不阻塞构建):');
@@ -767,7 +767,7 @@ if (errors.length) {
767
767
  if (warnings.length) {
768
768
  console.log('\n提示:还有几条配置不完整(不影响构建,但会缺能力):');
769
769
  warnings.forEach((w) => console.log(` · ${w}`));
770
- console.log(' 关系类问题跑 npm run relations 看完整报告与可粘贴片段\n');
770
+ console.log(' 关系类问题跑 node scripts/suggest-relations.mjs 看完整报告与可粘贴片段\n');
771
771
  }
772
772
 
773
773
  console.log('✓ 系统配置检查通过(系统身份 + 知识图谱本体 + 部门管理 + AI 工具层 + 全局搜索 + 数据权限 + 视图内关系覆盖 + 侧边栏徽标 + 外观底座)');
@@ -3,8 +3,8 @@
3
3
  * 关系挖掘器 —— 把 schema 里"物理存在但 ontology 没写"的关系找出来。
4
4
  *
5
5
  * 用法:
6
- * npm run relations 列出候选关系、漏配项、桥接表,并给出可粘贴的 TS 片段
7
- * npm run relations -- --strict 只要有"视图内该连未连"就退出码 1(CI 用)
6
+ * node scripts/suggest-relations.mjs 列出候选关系、漏配项、桥接表,并给出可粘贴的 TS 片段
7
+ * node scripts/suggest-relations.mjs -- --strict 只要有"视图内该连未连"就退出码 1(CI 用)
8
8
  *
9
9
  * 为什么需要它:
10
10
  * 图谱关系是手写的,人只会写"想得到的"。最典型的漏配是
@@ -82,7 +82,7 @@ if (indirectNote.length) {
82
82
  console.log('── 只能间接到达(两跳内可连通,不报错)──');
83
83
  for (const n of indirectNote) {
84
84
  console.log(` · 视图「${n.view}」:${n.a} ↔ ${n.b} 隔着 1 个节点,${n.hops} 跳可达`);
85
- console.log(` 如果评委要想看它们的关系,建议补一条 via.model 直连`);
85
+ console.log(` 如果要看它们的关系,建议补一条 via.model 直连`);
86
86
  }
87
87
  console.log('');
88
88
  }
@@ -24,7 +24,7 @@ const { RangePicker } = DatePicker;
24
24
  * 功能没变:左边生成报告,右边一个统一助手(数据问答 + 使用答疑)。
25
25
  */
26
26
 
27
- /** 助手空白态的引导问题。接业务时换成题目里的真实问题(数据类 / 使用类 / 办事类各一条) */
27
+ /** 助手空白态的引导问题。接业务时换成需求里的真实问题(数据类 / 使用类 / 办事类各一条) */
28
28
  const ASK_SAMPLES = [
29
29
  '这个月新增了哪些数据?分别是什么类型',
30
30
  '当前数据里有哪些明显的问题或风险?',
@@ -188,7 +188,7 @@ export default function DashboardPage() {
188
188
  </div>
189
189
  <div style={{ fontSize: fs(12), color: MILITARY.textMuted }}>统计口径:当前数据库全量数据</div>
190
190
  </div>
191
- {/* ★ 未配置警告:APP_NAME 还是默认值,说明还没按题目配置。
191
+ {/* ★ 未配置警告:APP_NAME 还是默认值,说明还没按需求配置。
192
192
  目的是让"忘了改"不可能被忽略。改完 appInfo.ts 后自动消失。 */}
193
193
  {!IS_CONFIGURED && (
194
194
  <Alert
@@ -199,11 +199,11 @@ export default function DashboardPage() {
199
199
  description={
200
200
  <div>
201
201
  修改 <Typography.Text code>src/lib/appInfo.ts</Typography.Text>:
202
- 把 <Typography.Text code>APP_NAME</Typography.Text> 改成题目里的系统名、
202
+ 把 <Typography.Text code>APP_NAME</Typography.Text> 改成需求里的系统名、
203
203
  <Typography.Text code>HOME_STATS</Typography.Text> 换成业务指标、
204
204
  把本页图表换成业务相关的内容、
205
205
  把 <Typography.Text code>SHOW_DEMO_ACCOUNT_HINT</Typography.Text> 设为 false。
206
- 然后运行 <Typography.Text code>npm run check:config</Typography.Text> 校验。
206
+ 然后运行 <Typography.Text code>node scripts/check-config.mjs</Typography.Text> 校验。
207
207
  </div>
208
208
  }
209
209
  />
@@ -25,7 +25,7 @@ const { Paragraph, Text, Title } = Typography;
25
25
  /**
26
26
  * 「等待与反馈」样板间。
27
27
  *
28
- * 这个页面不解决业务问题,它解决的是**比赛现场的一个固定问题**:
28
+ * 这个页面不解决业务问题,它解决的是**演示现场的一个固定问题**:
29
29
  * 出的题不确定,但"点了一下要等几秒"这件事一定会出现。
30
30
  * 所以把各种等待形态集中在这里,新题来了照抄对应的一块就行。
31
31
  *
@@ -13,7 +13,7 @@ import { fs, MILITARY } from '@/lib/theme';
13
13
  * 明确说清"没打开"、给出可能的两个原因、并且**保留重试按钮**。
14
14
  *
15
15
  * 注意:错误信息做脱敏 —— 生产环境只给摘要(digest),
16
- * 开发环境才显示原始 message,避免把堆栈/路径泄漏给评委看到。
16
+ * 开发环境才显示原始 message,避免把堆栈/路径泄漏给用户看到。
17
17
  */
18
18
  export default function AdminError({
19
19
  error,
@@ -24,7 +24,7 @@ import { formRules } from '@/lib/formRules';
24
24
  * 问题反馈页(骨架内置)。
25
25
  *
26
26
  * ★ 为什么必须有这个模块:
27
- * 比赛演示时评委最常问的一句是「如果有问题怎么反馈」。
27
+ * 演示时用户最常问的一句是「如果有问题怎么反馈」。
28
28
  * 有一个能提交、能跟踪状态、管理员能回复的反馈闭环,
29
29
  * 比任何功能都更能体现"这是个能上线的系统"。
30
30
  *
@@ -19,7 +19,7 @@ import { fs, MILITARY } from '@/lib/theme';
19
19
  * 为什么它不能省:
20
20
  * 用户、员工、以及绝大多数业务单据都要挂部门。
21
21
  * 只做用户管理不做部门管理,等于把"组织结构"这个地基留空,
22
- * 评委只需要问一句"部门在哪维护"就露馅。
22
+ * 用户只需要问一句"部门在哪维护"就露馅。
23
23
  *
24
24
  * 实现要点:
25
25
  * - 树形表格(不是分页列表):组织结构必须能一眼看出层级
@@ -37,7 +37,7 @@ export const POST = withRoute(async (req) => {
37
37
  });
38
38
  if (!ctx.snapshot.length) {
39
39
  throw new BizError(
40
- '库里没有可查询的业务数据。请先补充演示数据(npm run db:seed),或在 ontology.ts 里声明业务表。',
40
+ '库里没有可查询的业务数据。请先补充演示数据(npx tsx prisma/seed.ts),或在 ontology.ts 里声明业务表。',
41
41
  CODE.BAD_REQUEST
42
42
  );
43
43
  }
@@ -7,7 +7,7 @@ import { aiChat, cleanAiText } from '@/lib/ai';
7
7
  * 「数据 → 报告」生成器 —— 这是整套骨架里最有差异化的一块。
8
8
  *
9
9
  * 别的队伍在用大模型写代码,你把**大模型用进了产品里**。
10
- * 评委打开系统看到一个「一键生成报告」,印象分直接拉开差距。
10
+ * 用户打开系统看到一个「一键生成报告」,印象分直接拉开差距。
11
11
  *
12
12
  * 用法(模型接业务时只要调这一个接口):
13
13
  * POST /api/ai/report
@@ -15,7 +15,7 @@ export const dynamic = 'force-dynamic';
15
15
  * 为什么不建 SSE / WebSocket 推送:
16
16
  * 待办数字的变化频率是以"分钟"计的,不是聊天消息。60 秒轮询 + 回到前台刷新,
17
17
  * 代价是一个几乎不耗资源的 count 查询,换来的是"断线、多标签页、休眠唤醒"
18
- * 全都不用额外处理。这套骨架的定位是比赛演示与内网部署,简单可靠优先。
18
+ * 全都不用额外处理。这套骨架的定位是业务演示与内网部署,简单可靠优先。
19
19
  *
20
20
  * 为什么 path 而不是 menuId:菜单重跑 seed 会换 id,path 稳定;
21
21
  * 而且前端菜单树里天然有 path,不用为徽标额外传一份菜单 id 映射。
@@ -14,7 +14,7 @@ const ALLOWED = ['.doc', '.docx', '.xls', '.xlsx', '.pdf', '.txt', '.md', '.csv'
14
14
  /**
15
15
  * POST /api/docs/parse —— 上传并解析文档。
16
16
  *
17
- * 这是给评委看的核心亮点,所以策略是「宁可降级也不报错」:
17
+ * 这是给用户看的核心亮点,所以策略是「宁可降级也不报错」:
18
18
  * LibreOffice 缺失、pdf-parse 缺失都只影响对应格式的深度解析,
19
19
  * 接口本身永远返回结构化结果 + 明确的提示文本。
20
20
  */
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic';
14
14
  * ★ 这个接口**永远不返回 404 / 500**:
15
15
  * 反馈不存在、模型不可用、业务规则冲突,全部转成
16
16
  * `code: 0 + outcome` 的结构化结果,由前端在弹窗里展示分析与提示。
17
- * 反馈页因此不会出现红色报错弹窗,评委点任何一条都能得到回应。
17
+ * 反馈页因此不会出现红色报错弹窗,用户点任何一条都能得到回应。
18
18
  */
19
19
  export const POST = withRoute(async (req: NextRequest) => {
20
20
  const user = await currentUser();
@@ -10,7 +10,7 @@ import { prisma } from '@/lib/prisma';
10
10
  * - JWT 会话(非数据库会话):内网单机、无横向扩容需求,JWT 省一次库往返,
11
11
  * 刷新页面也不掉登录态。
12
12
  * - 权限集合(permCodes)在登录时一次算好塞进 JWT —— 之后每次判权限不用再查库。
13
- * 代价是改权限要重新登录才生效;演示/比赛场景完全够用。
13
+ * 代价是改权限要重新登录才生效;演示场景完全够用。
14
14
  * - 密码一律 bcrypt 比对,绝不存明文。
15
15
  */
16
16
 
@@ -22,7 +22,7 @@ import { fs, MILITARY } from '@/lib/theme';
22
22
  * 深军绿封面 + 统计条 + 白纸正文 + 底部操作栏。
23
23
  *
24
24
  * 为什么不用普通 Modal 直接塞一段文字:
25
- * 这是比赛演示的亮点功能,评委第一眼看到的是排版。
25
+ * 这是演示的亮点功能,用户第一眼看到的是排版。
26
26
  * 封面、统计卡、纸张层次能把「AI 真的读了数据」这件事讲清楚。
27
27
  */
28
28
 
@@ -9,7 +9,7 @@ import { headingId } from '@/lib/docs-shared';
9
9
  * 轻量 Markdown 渲染器(无第三方依赖)。
10
10
  *
11
11
  * 为什么自己写:
12
- * 比赛环境是离线内网,装不了 react-markdown / marked;
12
+ * 运行环境是离线内网,装不了 react-markdown / marked;
13
13
  * 而模型的回答天然是 Markdown,直接当纯文本显示会看到一堆 ** 和 | 表格。
14
14
  *
15
15
  * 支持:标题 / 段落 / 有序无序列表 / 代码块 / 行内代码 / 粗斜体 / 删除线 /
@@ -9,7 +9,7 @@ import { fs } from '@/lib/theme';
9
9
  /**
10
10
  * 会自己交代进度的按钮。
11
11
  *
12
- * 比赛里最常写的一行代码是:
12
+ * 业务页面里最常写的一行代码是:
13
13
  * <Button onClick={async () => { await axios.post(...) }}>提交</Button>
14
14
  * 它有三个隐患:不防重复点击、慢的时候没有任何反馈、失败只报 console。
15
15
  *
@@ -1,6 +1,6 @@
1
1
  # 反馈底座(feedback)
2
2
 
3
- 比赛每道新题都要重做一遍页面,但"用户点了之后有没有反应"这件事是**通用**的。
3
+ 每做一个新模块都要重做一遍页面,但"用户点了之后有没有反应"这件事是**通用**的。
4
4
  这个目录把它做成了一层能力:换题时不用重新设计动效,照着下面的样板接就行。
5
5
 
6
6
  ## 两条铁律
@@ -32,7 +32,7 @@ const MAX_FIELD_LEN = 40;
32
32
  /** 快照总字符上限 */
33
33
  const MAX_SNAPSHOT_CHARS = 7000;
34
34
 
35
- /** 北京时间固定 UTC+8(比赛机器在国内,不依赖服务器时区设置) */
35
+ /** 北京时间固定 UTC+8(部署机器在国内,不依赖服务器时区设置) */
36
36
  const TZ_OFFSET_MS = 8 * 3600_000;
37
37
  const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
38
38
 
@@ -9,7 +9,7 @@ import { ZodError } from 'zod';
9
9
  * { code: 400xx, data: null, message: '...' } 失败
10
10
  *
11
11
  * 这样做的直接好处:任何接口忘记 try/catch 都不会把 500 堆栈泄漏给前端,
12
- * 评委点到的永远是干净的提示语,而不是一屏红字。
12
+ * 用户点到的永远是干净的提示语,而不是一屏红字。
13
13
  */
14
14
  export type ApiOk<T> = { code: 0; data: T; message: string };
15
15
  export type ApiErr = { code: number; data: null; message: string };
@@ -4,12 +4,12 @@
4
4
  * ★ 为什么要有这个文件
5
5
  *
6
6
  * 之前这些值是硬编码在 layout / 登录页 / 侧边栏三个地方的,
7
- * AI 建站时经常只改一处(甚至一处都不改),结果交上去的系统标题还叫
7
+ * AI 建站时经常只改一处(甚至一处都不改),结果上线后的系统标题还叫
8
8
  * "业务管理系统",一眼就是演示品,专业度直接掉分。
9
9
  *
10
10
  * ★ 用法(AI 建站时第 0 步就要做)
11
11
  *
12
- * 把 APP_NAME 改成题目里的系统名,SUBTITLE 写一句业务定位,
12
+ * 把 APP_NAME 改成需求里的系统名,SUBTITLE 写一句业务定位,
13
13
  * 再把 HOME_STATS 换成这个业务最该看的 3~4 个指标。
14
14
  */
15
15
 
@@ -22,13 +22,13 @@
22
22
  */
23
23
 
24
24
  /**
25
- * 系统全称。**必须和题目的系统名一致**。
25
+ * 系统全称。**必须和需求的系统名一致**。
26
26
  * 例:车辆派遣管理系统 / 请销假管理系统 / 值班管理系统
27
27
  *
28
28
  * ★ 长名字不用自己截断:侧栏那 154px 的文字列只放得下约 9 个字,
29
29
  * 超过就折成 2 行(`globals.css` 的 `.wb-brand-title`,行高给足、品牌行自己长高),
30
30
  * 再长才用省略号,完整名字挂在 `title` 属性上(悬停可见)。
31
- * 所以题目名字长也不必改短,写全称即可。
31
+ * 所以需求名字长也不必改短,写全称即可。
32
32
  */
33
33
  export const APP_NAME: string = '业务管理系统';
34
34
 
@@ -62,7 +62,7 @@ export const APP_DESC: string = '内置 RBAC 权限、操作审计与数据导
62
62
  * 登录页是否显示「演示账号」提示。
63
63
  *
64
64
  * ★ 交付前必须改成 false。
65
- * 「演示账号:admin / 123456」是评委一眼就认出这是演示系统的最强信号。
65
+ * 「演示账号:admin / 123456」是用户一眼就认出这是演示系统的最强信号。
66
66
  * 自己答辩时记在纸上就行,不要写在页面上。
67
67
  */
68
68
  export const SHOW_DEMO_ACCOUNT_HINT: boolean = true;
@@ -71,10 +71,10 @@ export const SHOW_DEMO_ACCOUNT_HINT: boolean = true;
71
71
  export const DEMO_ACCOUNT_TEXT: string = '演示账号:admin / 123456(超管)· zhangsan / 123456(普通用户)';
72
72
 
73
73
  /**
74
- * 是否已经按题目配置过。
74
+ * 是否已经按需求配置过。
75
75
  *
76
76
  * 判据:APP_NAME 不再是脚手架默认值。
77
- * 首页会用它决定要不要显示「待配置」警告,`npm run check:config` 也用它卡构建。
77
+ * 首页会用它决定要不要显示「待配置」警告,`node scripts/check-config.mjs` 也用它卡构建。
78
78
  */
79
79
  export const IS_CONFIGURED = APP_NAME !== '业务管理系统';
80
80
 
@@ -103,7 +103,7 @@ export type HomeStat = {
103
103
  * 所以指标换掉之后记得同步改 `src/app/api/stats/route.ts` 返回同名字段。
104
104
  *
105
105
  * ★ 默认给的是骨架自带的三个(用户/角色/员工),
106
- * 接业务后应该全部替换掉 —— 首页是评委第一眼看的地方,
106
+ * 接业务后应该全部替换掉 —— 首页是用户第一眼看的地方,
107
107
  * 摆"用户总数"这种系统自身指标,一眼就是脚手架没改。
108
108
  *
109
109
  * 例(车辆派遣):
@@ -2,7 +2,7 @@
2
2
  * 外观设置 —— 皮肤(配色 + 材质)与字体大小的唯一来源。
3
3
  *
4
4
  * 为什么要有这一层:
5
- * 题目一换,页面全是新写的,但"换个外观"这种全站能力不能跟着重写。
5
+ * 需求一换,页面全是新写的,但"换个外观"这种全站能力不能跟着重写。
6
6
  * 所以这里只描述 **token**(颜色、阴影、侧栏材质、品牌区渐变),
7
7
  * 页面永远只引用语义(MILITARY.primary / var(--wb-text)),
8
8
  * 皮肤切换就是把另一套 token 换上去,页面代码一行都不用动。
@@ -6,7 +6,7 @@ import { BADGE_MAX, BADGE_SOURCES, badgeText, sumBadges } from './badges';
6
6
  *
7
7
  * 这几条看着琐碎,但都是侧边栏每天要渲染几十次的分支:
8
8
  * 0 要不要显示、三位数会不会把菜单挤变形、后端返回 null 时会不会渲染出 "NaN"。
9
- * NaN 尤其致命 —— 它会原样出现在侧边栏上,是评委一眼就能看见的那种 bug。
9
+ * NaN 尤其致命 —— 它会原样出现在侧边栏上,是用户一眼就能看见的那种 bug。
10
10
  */
11
11
  describe('badgeText', () => {
12
12
  it('0 和负数不显示', () => {
@@ -128,7 +128,7 @@ function parseScalar(raw: string): FrontMatterValue {
128
128
  /**
129
129
  * 解析 `---` 包围的简易 front matter。
130
130
  *
131
- * 刻意不引入 yaml 依赖:比赛环境离线,而文档只需要 title / description /
131
+ * 刻意不引入 yaml 依赖:运行环境离线,而文档只需要 title / description /
132
132
  * order / tags / updated / hidden 这几个标量。解析失败一律降级为空元数据,
133
133
  * 正文照常渲染 —— 写错一个冒号不该让整篇文档打不开。
134
134
  */
@@ -28,7 +28,7 @@ import {
28
28
  *
29
29
  * ★ 内容即目录:`content/docs` 下的 Markdown 文件自动成为文档页,
30
30
  * 子目录自动成为分组;新增一篇文档 = 丢一个 .md 进去,不需要改任何页面代码。
31
- * 这正是「通用底座」的关键 —— 比赛换题、加模块时,只需要补内容,不用改框架。
31
+ * 这正是「通用底座」的关键 —— 换业务、加模块时,只需要补内容,不用改框架。
32
32
  *
33
33
  * 约定:
34
34
  * - 页面:`content/docs/<路径>.md`,URL 为 `/docs/<路径>`
@@ -5,7 +5,7 @@ import axios from 'axios';
5
5
  *
6
6
  * 约定:后端业务失败也是 HTTP 200 + `{ code, message }`(见 lib/api.ts),
7
7
  * 所以 axios 不报错、但 body 里有 message 的情况很常见 —— 两层都要看。
8
- * 最后才回落到 JS 的原始 message,避免把一屏英文堆栈甩给评委。
8
+ * 最后才回落到 JS 的原始 message,避免把一屏英文堆栈甩给用户。
9
9
  */
10
10
  export function errText(e: unknown, fallback = '操作失败'): string {
11
11
  if (axios.isAxiosError(e)) {
@@ -7,7 +7,7 @@ import dayjs from 'dayjs';
7
7
  *
8
8
  * 选 exceljs 而不是 xlsx(SheetJS):exceljs 是纯 JS + 自带类型,
9
9
  * 内网离线装完即用;而且它对「样式」支持完整,
10
- * 导出的表格能带表头底色、冻结行、自动筛选,评委一眼能看出是「认真做的」。
10
+ * 导出的表格能带表头底色、冻结行、自动筛选,用户一眼能看出是「认真做的」。
11
11
  */
12
12
 
13
13
  export type ExcelColumn = {
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * ## 默认策略:业务能力默认关,反馈通道默认开
5
5
  *
6
- * AI 中心、知识图谱、文件中心、部门管理默认 `false`。题目的组织维度、要不要 AI、
7
- * 要不要图谱,只有读过题目的人才知道;默认关着,写业务代码前必须**明确改一次**,
6
+ * AI 中心、知识图谱、文件中心、部门管理默认 `false`。需求的组织维度、要不要 AI、
7
+ * 要不要图谱,只有读过需求的人才知道;默认关着,写业务代码前必须**明确改一次**,
8
8
  * 于是"这个模块到底要不要"变成一次有意识的决定,而不是默认跟着模板走。
9
9
  *
10
10
  * 问题反馈是例外:它是任何系统都成立的兜底通道,也是待办与消息中心的示例来源,
11
- * 所以默认 `true`。题目明确不需要反馈时再关掉。
11
+ * 所以默认 `true`。需求明确不需要反馈时再关掉。
12
12
  *
13
13
  * ## 关掉的影响(改之前先知道)
14
14
  *
@@ -8,7 +8,7 @@ import { beginTask, endTask } from './pending';
8
8
  *
9
9
  * ★ 这是「任何耗时场景都有提示」的**兜底层**:
10
10
  * 在 Providers 里装一次拦截器,全站所有 axios 请求(现有 34 个接口 +
11
- * 比赛新加的接口)都会自动登记到全局任务表,
11
+ * 新加的接口)都会自动登记到全局任务表,
12
12
  * 超过 3 秒没有结束,右下角就会浮出一张「正在处理…已等待 N 秒」的小卡片。
13
13
  *
14
14
  * 也就是说:哪怕新写的页面忘了加 loading,用户也不会对着静止的界面猜。
@@ -32,7 +32,7 @@ declare module 'axios' {
32
32
  * 路径 → 人话文案。
33
33
  *
34
34
  * 命中不到不算错,会回退成「正在处理数据…」。
35
- * 比赛出了新题、加了新接口,想让提示更具体,在这里补一行正则即可 ——
35
+ * 换了业务、加了新接口,想让提示更具体,在这里补一行正则即可 ——
36
36
  * 不需要改页面、不需要改组件。
37
37
  */
38
38
  const LABELS: { test: RegExp; text: string }[] = [
@@ -23,7 +23,7 @@ import { BizError } from '@/lib/api';
23
23
  /**
24
24
  * 文档处理:解析(Word / Excel / PDF / 纯文本)+ 生成(Word / PDF)。
25
25
  *
26
- * 设计取舍说明(很重要,评委一定会问「为什么不自己解析二进制」):
26
+ * 设计取舍说明(很重要,用户一定会问「为什么不自己解析二进制」):
27
27
  *
28
28
  * 1. .docx 是 zip + OOXML,交给 mammoth 最稳;mammoth 还能顺手给出 HTML,
29
29
  * 这样前端可以「左边纯文本、右边富文本」双视图预览。
@@ -2,7 +2,7 @@
2
2
  * 本体论(Ontology)配置 —— 知识图谱的"通用引擎"。
3
3
  *
4
4
  * ★ 这是整套骨架里第二个差异化亮点(第一个是 AI 能力)。
5
- * 评委打开看到一个能点的知识图谱,基本就记住你了。
5
+ * 用户打开看到一个能点的知识图谱,基本就记住你了。
6
6
  *
7
7
  * 设计思路:
8
8
  * 图谱画什么,由这个文件里的声明式配置决定,跟代码完全解耦。
@@ -16,7 +16,7 @@
16
16
  * ★ views[0] 就是 /graph 默认打开的视图。页面会自动读取 views 并渲染视图下拉,
17
17
  * **不需要改 graph/page.tsx** —— 换业务时只改这个文件就够了。
18
18
  * views 里引用的 entities / relations / root 必须都是已声明的 id,
19
- * `npm run check:config` 会在 1 秒内把这些引用错误报出来。
19
+ * `node scripts/check-config.mjs` 会在 1 秒内把这些引用错误报出来。
20
20
  *
21
21
  * 换系统的例子(请销假):
22
22
  * entities: Person(人) / LeaveRequest(请假申请) / LeavePlan(休假计划) / LeaveChange(变更申请)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 全局等待任务登记表(等待反馈体系的「大脑」)。
3
3
  *
4
- * 解决的问题:比赛出的题不可预知,页面里任何一个耗时动作都可能让人干瞪眼。
4
+ * 解决的问题:业务需求不可预知,页面里任何一个耗时动作都可能让人干瞪眼。
5
5
  * 与其给每个按钮/每个页面各写一套 loading,不如让**所有耗时动作都往这里登记一条**,
6
6
  * 由全局浮层统一兜底 —— 即使写页面的人忘了加 spinner,用户也不会看到"点了没反应"。
7
7
  *
@@ -117,7 +117,7 @@ export async function searchData(user: SessionUser, rawQuery: string): Promise<D
117
117
  * 有 deptId → 套部门范围;
118
118
  * 没有 deptId → 非「全部数据」时只看自己名下的行(userId = 自己)。
119
119
  * 少了第二条,列表上明明过滤了、Ctrl K 一搜就把别人的单子全列出来 ——
120
- * 这是最容易被评委顺手试出来的漏洞。
120
+ * 这是最容易被用户顺手试出来的漏洞。
121
121
  */
122
122
  const scopeIsAll = !('deptId' in scope);
123
123
  if (!scopeIsAll) {
@@ -13,7 +13,7 @@ import { prisma } from '@/lib/prisma';
13
13
  *
14
14
  * - 布尔统一存 '1' / '0';
15
15
  * - **缺行 = 默认值**,不往 seed 里塞行 —— 默认值以代码为唯一来源,
16
- * 重跑 db:reset 也不会把管理员的开关悄悄打回默认之外的状态;
16
+ * 重跑建库流程也不会把管理员的开关悄悄打回默认之外的状态;
17
17
  * - 读多写少(登录页每次加载都读一次),SQLite 点查主键,不值得上缓存,
18
18
  * 加了缓存反而要处理「管理员改了、登录页还是旧值」的失效问题。
19
19
  */
@@ -2,7 +2,7 @@
2
2
  * 输入合理性校验底座 —— 前端表单、后端接口、AI 动态表单共用同一套规则。
3
3
  *
4
4
  * ★ 为什么单独抽一层,而不是在每个 Form.Item 里写正则:
5
- * 比赛会换题/加字段,如果校验规则散落在页面和接口里,每加一个「身份证号」
5
+ * 业务会换/加字段,如果校验规则散落在页面和接口里,每加一个「身份证号」
6
6
  * 就要前端写一遍 required、后端再写一遍 zod,提示语还对不上。
7
7
  * 这里把「字段类型 → 规整方式 + 校验逻辑 + 友好提示」收敛成注册表:
8
8
  * - 新题加字段:优先复用内置类型;不够用时 registerFieldType() 注册一个即可
@@ -92,7 +92,7 @@ export async function trackVisit(input: TrackVisitInput, now: Date = new Date())
92
92
  *
93
93
  * 三条 count 并行发:在线数走 Visitor.lastSeenAt 索引,今日 / 总量走 Visit.createdAt 索引,
94
94
  * 在 SQLite 上都是毫秒级。演示规模不引入缓存层 —— 数字要是"缓存过的旧值",
95
- * 反而会让评委怀疑系统没在实时统计。
95
+ * 反而会让用户怀疑系统没在实时统计。
96
96
  */
97
97
  export async function getVisitStats(now: Date = new Date()): Promise<VisitStats> {
98
98
  const [online, today, total] = await Promise.all([