node-resource-utils 0.0.0-stage → 1.1.8
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.
- package/LICENSE +21 -0
- package/README.md +35 -2
- package/lib/sync.js +121 -0
- package/package.json +14 -4
- package/resources/.cache/ref.b64 +1 -0
- package/resources/README.md +97 -0
- package/resources/_.env +6 -0
- package/resources/_.env.example +25 -0
- package/resources/_.gitignore +18 -0
- package/resources/_.lintstagedrc.json +9 -0
- package/resources/_.npmrc +5 -0
- package/resources/_.prettierrc +7 -0
- package/resources/commitlint.config.mjs +1 -0
- package/resources/content/docs/README.md +63 -0
- package/resources/content/docs/faq.md +107 -0
- package/resources/content/docs/features/_meta.json +5 -0
- package/resources/content/docs/features/ai.md +71 -0
- package/resources/content/docs/features/appearance.md +47 -0
- package/resources/content/docs/features/feedback.md +79 -0
- package/resources/content/docs/features/files.md +74 -0
- package/resources/content/docs/features/graph.md +66 -0
- package/resources/content/docs/features/inbox.md +61 -0
- package/resources/content/docs/guide/_meta.json +5 -0
- package/resources/content/docs/guide/account.md +76 -0
- package/resources/content/docs/guide/data.md +87 -0
- package/resources/content/docs/guide/navigation.md +81 -0
- package/resources/content/docs/guide/permissions.md +67 -0
- package/resources/content/docs/guide/system.md +92 -0
- package/resources/content/docs/quick-start.md +70 -0
- package/resources/content/docs/shortcuts.md +69 -0
- package/resources/eslint.config.mjs +17 -0
- package/resources/next-env.d.ts +6 -0
- package/resources/next.config.ts +13 -0
- package/resources/postcss.config.mjs +7 -0
- package/resources/prisma/org.config.ts +98 -0
- package/resources/prisma/schema.prisma +336 -0
- package/resources/prisma/seed.ts +721 -0
- package/resources/prisma/seedSafe.ts +347 -0
- package/resources/prisma.config.ts +14 -0
- package/resources/scripts/check-config.mjs +773 -0
- package/resources/scripts/lib/schema-graph.mjs +410 -0
- package/resources/scripts/patch-next-trace.mjs +86 -0
- package/resources/scripts/suggest-relations.mjs +105 -0
- package/resources/src/app/(admin)/ai/page.tsx +469 -0
- package/resources/src/app/(admin)/dashboard/page.tsx +260 -0
- package/resources/src/app/(admin)/demo/employees/page.tsx +508 -0
- package/resources/src/app/(admin)/demo/files/page.tsx +451 -0
- package/resources/src/app/(admin)/demo/loading/page.tsx +513 -0
- package/resources/src/app/(admin)/error.tsx +96 -0
- package/resources/src/app/(admin)/feedback/page.tsx +573 -0
- package/resources/src/app/(admin)/graph/page.tsx +1224 -0
- package/resources/src/app/(admin)/inbox/page.tsx +31 -0
- package/resources/src/app/(admin)/layout.tsx +58 -0
- package/resources/src/app/(admin)/loading.tsx +31 -0
- package/resources/src/app/(admin)/system/depts/page.tsx +415 -0
- package/resources/src/app/(admin)/system/logs/page.tsx +215 -0
- package/resources/src/app/(admin)/system/menus/page.tsx +403 -0
- package/resources/src/app/(admin)/system/roles/page.tsx +433 -0
- package/resources/src/app/(admin)/system/users/page.tsx +620 -0
- package/resources/src/app/api/account-recovery/route.ts +132 -0
- package/resources/src/app/api/ai/act/route.ts +70 -0
- package/resources/src/app/api/ai/ask/route.ts +82 -0
- package/resources/src/app/api/ai/chat/route.ts +404 -0
- package/resources/src/app/api/ai/report/route.ts +164 -0
- package/resources/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/resources/src/app/api/auth/change-password/route.ts +57 -0
- package/resources/src/app/api/auth/forgot/route.ts +98 -0
- package/resources/src/app/api/auth/register/route.ts +95 -0
- package/resources/src/app/api/auth/register-config/route.ts +51 -0
- package/resources/src/app/api/badges/route.ts +39 -0
- package/resources/src/app/api/depts/[id]/route.ts +145 -0
- package/resources/src/app/api/depts/route.ts +126 -0
- package/resources/src/app/api/docs/export/route.ts +87 -0
- package/resources/src/app/api/docs/parse/route.ts +57 -0
- package/resources/src/app/api/docs/search/route.ts +22 -0
- package/resources/src/app/api/employees/[id]/route.ts +147 -0
- package/resources/src/app/api/employees/export/route.ts +115 -0
- package/resources/src/app/api/employees/import/route.ts +205 -0
- package/resources/src/app/api/employees/route.ts +149 -0
- package/resources/src/app/api/export/route.ts +254 -0
- package/resources/src/app/api/favorites/route.ts +56 -0
- package/resources/src/app/api/feedback/ai/route.ts +31 -0
- package/resources/src/app/api/feedback/approve/route.ts +67 -0
- package/resources/src/app/api/feedback/report/route.ts +213 -0
- package/resources/src/app/api/feedback/route.ts +246 -0
- package/resources/src/app/api/files/[name]/route.ts +82 -0
- package/resources/src/app/api/graph/focus/route.ts +75 -0
- package/resources/src/app/api/graph/node/move/route.ts +48 -0
- package/resources/src/app/api/graph/node/route.ts +274 -0
- package/resources/src/app/api/graph/route.ts +272 -0
- package/resources/src/app/api/health/route.ts +11 -0
- package/resources/src/app/api/inbox/read/route.ts +36 -0
- package/resources/src/app/api/inbox/summary/route.ts +15 -0
- package/resources/src/app/api/logs/route.ts +82 -0
- package/resources/src/app/api/menus/[id]/route.ts +132 -0
- package/resources/src/app/api/menus/mine/route.ts +23 -0
- package/resources/src/app/api/menus/route.ts +89 -0
- package/resources/src/app/api/roles/[id]/menus/route.ts +73 -0
- package/resources/src/app/api/roles/[id]/route.ts +96 -0
- package/resources/src/app/api/roles/route.ts +114 -0
- package/resources/src/app/api/search/route.ts +22 -0
- package/resources/src/app/api/stats/route.ts +92 -0
- package/resources/src/app/api/upload/route.ts +38 -0
- package/resources/src/app/api/users/[id]/password/route.ts +55 -0
- package/resources/src/app/api/users/[id]/route.ts +145 -0
- package/resources/src/app/api/users/route.ts +153 -0
- package/resources/src/app/api/visits/route.ts +48 -0
- package/resources/src/app/change-password/ChangePasswordForm.tsx +174 -0
- package/resources/src/app/change-password/page.tsx +19 -0
- package/resources/src/app/docs/[[...slug]]/page.tsx +56 -0
- package/resources/src/app/docs/layout.tsx +34 -0
- package/resources/src/app/docs/loading.tsx +19 -0
- package/resources/src/app/docs/not-found.tsx +25 -0
- package/resources/src/app/forgot/page.tsx +392 -0
- package/resources/src/app/global-error.tsx +75 -0
- package/resources/src/app/globals.css +235 -0
- package/resources/src/app/layout.tsx +44 -0
- package/resources/src/app/login/page.tsx +267 -0
- package/resources/src/app/page.tsx +14 -0
- package/resources/src/app/register/page.tsx +237 -0
- package/resources/src/auth.ts +215 -0
- package/resources/src/components/AiPanel.tsx +970 -0
- package/resources/src/components/AuthContext.tsx +82 -0
- package/resources/src/components/DataTable.tsx +459 -0
- package/resources/src/components/EChart.tsx +140 -0
- package/resources/src/components/FeedbackAiModal.tsx +476 -0
- package/resources/src/components/FeedbackReportModal.tsx +479 -0
- package/resources/src/components/Markdown.tsx +532 -0
- package/resources/src/components/OntologyGraph.tsx +1196 -0
- package/resources/src/components/Providers.tsx +99 -0
- package/resources/src/components/VisitTracker.tsx +89 -0
- package/resources/src/components/admin/AdminShell.tsx +749 -0
- package/resources/src/components/admin/CountBadge.tsx +67 -0
- package/resources/src/components/admin/GlobalSearch.tsx +509 -0
- package/resources/src/components/admin/InboxBell.tsx +78 -0
- package/resources/src/components/admin/VisitStats.tsx +145 -0
- package/resources/src/components/admin/menu-icons.tsx +56 -0
- package/resources/src/components/ai/AiInputForm.tsx +351 -0
- package/resources/src/components/appearance/AppearanceDrawer.tsx +268 -0
- package/resources/src/components/appearance/AppearanceProvider.tsx +160 -0
- package/resources/src/components/docs/DocArticle.tsx +106 -0
- package/resources/src/components/docs/DocToc.tsx +100 -0
- package/resources/src/components/docs/DocsChrome.tsx +259 -0
- package/resources/src/components/docs/DocsHome.tsx +209 -0
- package/resources/src/components/docs/DocsSearch.tsx +365 -0
- package/resources/src/components/docs/docs.css +1458 -0
- package/resources/src/components/feedback/AsyncButton.tsx +108 -0
- package/resources/src/components/feedback/LoadingOverlay.tsx +125 -0
- package/resources/src/components/feedback/PressableCard.tsx +92 -0
- package/resources/src/components/feedback/README.md +97 -0
- package/resources/src/components/feedback/RouteProgress.tsx +108 -0
- package/resources/src/components/feedback/Skeleton.tsx +249 -0
- package/resources/src/components/feedback/StatCard.tsx +70 -0
- package/resources/src/components/feedback/TaskHint.tsx +153 -0
- package/resources/src/components/feedback/index.ts +93 -0
- package/resources/src/components/feedback/motion.css +204 -0
- package/resources/src/components/feedback/motion.ts +63 -0
- package/resources/src/components/feedback/useCountUp.ts +110 -0
- package/resources/src/components/feedback/usePending.ts +209 -0
- package/resources/src/components/feedback/useRowFlash.ts +40 -0
- package/resources/src/components/inbox/InboxView.tsx +409 -0
- package/resources/src/components/ui/button.tsx +35 -0
- package/resources/src/components/ui/card.tsx +37 -0
- package/resources/src/components/ui/input.tsx +19 -0
- package/resources/src/lib/ai.ts +310 -0
- package/resources/src/lib/aiContext.ts +357 -0
- package/resources/src/lib/aiForm.test.ts +191 -0
- package/resources/src/lib/aiForm.ts +532 -0
- package/resources/src/lib/aiFormTypes.ts +83 -0
- package/resources/src/lib/aiToken.ts +72 -0
- package/resources/src/lib/aiTools.ts +823 -0
- package/resources/src/lib/api.ts +160 -0
- package/resources/src/lib/appInfo.ts +128 -0
- package/resources/src/lib/appearance.ts +643 -0
- package/resources/src/lib/badge-count.ts +146 -0
- package/resources/src/lib/badges.test.ts +58 -0
- package/resources/src/lib/badges.ts +79 -0
- package/resources/src/lib/brand-mark.test.ts +87 -0
- package/resources/src/lib/brand-mark.ts +45 -0
- package/resources/src/lib/docs-shared.test.ts +148 -0
- package/resources/src/lib/docs-shared.ts +422 -0
- package/resources/src/lib/docs.ts +381 -0
- package/resources/src/lib/err-text.ts +21 -0
- package/resources/src/lib/excel.ts +152 -0
- package/resources/src/lib/favorite-path.ts +60 -0
- package/resources/src/lib/favorites.ts +59 -0
- package/resources/src/lib/features.ts +69 -0
- package/resources/src/lib/feedback-access.ts +17 -0
- package/resources/src/lib/feedbackAi.ts +1621 -0
- package/resources/src/lib/formRules.ts +76 -0
- package/resources/src/lib/format.ts +52 -0
- package/resources/src/lib/graph-cascade.ts +304 -0
- package/resources/src/lib/graph-focus.ts +301 -0
- package/resources/src/lib/graph-link.ts +17 -0
- package/resources/src/lib/graph-node-write.ts +150 -0
- package/resources/src/lib/graph-schema.ts +475 -0
- package/resources/src/lib/graph-scope.test.ts +46 -0
- package/resources/src/lib/graph-scope.ts +28 -0
- package/resources/src/lib/http.ts +157 -0
- package/resources/src/lib/inbox-types.ts +57 -0
- package/resources/src/lib/inbox.ts +55 -0
- package/resources/src/lib/menu-service.ts +204 -0
- package/resources/src/lib/notifications.ts +113 -0
- package/resources/src/lib/office.ts +531 -0
- package/resources/src/lib/ontology.ts +277 -0
- package/resources/src/lib/openaiCompat.ts +257 -0
- package/resources/src/lib/pending.ts +98 -0
- package/resources/src/lib/prisma.ts +30 -0
- package/resources/src/lib/rbac.test.ts +106 -0
- package/resources/src/lib/rbac.ts +185 -0
- package/resources/src/lib/search-data.ts +158 -0
- package/resources/src/lib/security-questions.ts +20 -0
- package/resources/src/lib/skin-theme.ts +155 -0
- package/resources/src/lib/system-config.ts +55 -0
- package/resources/src/lib/theme.ts +96 -0
- package/resources/src/lib/todos/feedback.ts +86 -0
- package/resources/src/lib/todos/index.ts +73 -0
- package/resources/src/lib/todos/types.ts +33 -0
- package/resources/src/lib/upload.ts +106 -0
- package/resources/src/lib/utils.test.ts +9 -0
- package/resources/src/lib/utils.ts +6 -0
- package/resources/src/lib/validation.test.ts +297 -0
- package/resources/src/lib/validation.ts +658 -0
- package/resources/src/lib/visit-stats.ts +109 -0
- package/resources/src/lib/visits.ts +111 -0
- package/resources/src/lib/zodField.ts +44 -0
- package/resources/tsconfig.json +41 -0
- package/resources/vitest.config.ts +16 -0
- package/resources/vitest.setup.ts +1 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 演示数据安全生成器 —— 彻底杜绝乱码。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
*
|
|
6
|
+
* 实测:SQLite / Prisma / 模型输出 / 浏览器 **每一环单独测都能正确处理 emoji**。
|
|
7
|
+
* 但 AI 生成 seed 数据时仍会偶发乱码,典型表现是
|
|
8
|
+
*
|
|
9
|
+
* 机场接送 🚗 → 🚗ðŸš"
|
|
10
|
+
*
|
|
11
|
+
* 这是「UTF-8 字节被当成 Latin-1 解码」的教科书症状(mojibake)。
|
|
12
|
+
* 偶发意味着你没法靠"修某一环"根治,只能靠**约束输入 + 事后检测**。
|
|
13
|
+
*
|
|
14
|
+
* ## 所以这里做三件事
|
|
15
|
+
*
|
|
16
|
+
* 1. **安全字符白名单**:只允许 中文 + ASCII 可见字符 + 少量中文标点。
|
|
17
|
+
* 这覆盖了 100% 的业务文案需求,同时把风险字符(emoji、私用区、
|
|
18
|
+
* Unicode 变体选择符、零宽字符)全部挡在外面。
|
|
19
|
+
* 2. **安全词库**:演示数据从这里取,不手写字符串。
|
|
20
|
+
* 3. **断言**:`assertNoMojibake()` 在 seed 结束时扫描全库,
|
|
21
|
+
* 发现乱码立刻抛错终止,让问题在构建期暴露,而不是留给评委看到。
|
|
22
|
+
*
|
|
23
|
+
* ## 用法(在 prisma/seed.ts 里)
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { text, personName, deptName, bizReason, assertNoMojibake } from './seedSafe';
|
|
27
|
+
*
|
|
28
|
+
* const rows = REASONS.map((r) => ({ reason: text(r) }));
|
|
29
|
+
* // ... 全部 seed 完之后 ...
|
|
30
|
+
* await assertNoMojibake(prisma);
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* ## 闭环造数(重要)
|
|
34
|
+
*
|
|
35
|
+
* 业务单据里的"人"(申请人 / 审批人 / 处理人 …)必须来自 `PERSON_ROSTER` 名册,
|
|
36
|
+
* 且名册里的人都要在用户表里有账号 —— 顺序是**先建用户,再让单据引用其 id**。
|
|
37
|
+
* 不要对引用型人员字段用 `personName()` 现编姓名,那样"单据里十几个人、
|
|
38
|
+
* 用户管理里两三个账号",评委一眼看穿(见 PERSON_ROSTER 注释)。
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/* ============================ 1. 安全字符白名单 ============================ */
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 允许的字符:
|
|
45
|
+
* - ASCII 可见字符(0x20-0x7E)
|
|
46
|
+
* - 中日韩统一表意文字(4E00-9FFF)与扩展 A(3400-4DBF)
|
|
47
|
+
* - 中文标点与常用全角符号:,。、;:?!“”‘’()《》【】…—·
|
|
48
|
+
* - 数字全角(0-9)与空格
|
|
49
|
+
*
|
|
50
|
+
* 刻意**排除**:
|
|
51
|
+
* - emoji(U+1F300–U+1FAFF、U+2600–U+27BF)——乱码高发区
|
|
52
|
+
* - 私用区(U+E000–U+F8FF)
|
|
53
|
+
* - 变体选择符(U+FE00–U+FE0F)与零宽字符(U+200B–U+200F、U+FEFF)
|
|
54
|
+
* - 拉丁扩展(U+00A0–U+024F)——`ð` `Ÿ` `Â` 都在这,是 mojibake 的副产物
|
|
55
|
+
*/
|
|
56
|
+
const ALLOWED_EXTRA = ',。、;:?!“”‘’()《》【】…—· 0123456789';
|
|
57
|
+
const SAFE_CHAR = new RegExp(
|
|
58
|
+
`^[\\x20-\\x7E\\u3400-\\u4DBF\\u4E00-\\u9FFF${ALLOWED_EXTRA}]*$`
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** 判空/判非法用 */
|
|
62
|
+
export function isSafeText(s: string): boolean {
|
|
63
|
+
return SAFE_CHAR.test(s);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 清洗任意字符串到安全字符集。
|
|
68
|
+
*
|
|
69
|
+
* 注意:**不做"智能修复"**(比如把 🚗 还原成 emoji)。
|
|
70
|
+
* 修复是不可靠的猜测,宁可删掉风险字符,也不要猜。
|
|
71
|
+
*/
|
|
72
|
+
export function text(input: unknown, fallback = ''): string {
|
|
73
|
+
if (input === null || input === undefined) return fallback;
|
|
74
|
+
let s = String(input);
|
|
75
|
+
// 去掉零宽与变体选择符(它们不可见,但会破坏匹配与显示)
|
|
76
|
+
s = s.replace(/[\u200B-\u200F\uFEFF\uFE00-\uFE0F]/g, '');
|
|
77
|
+
// 去掉所有非白名单字符(含 emoji、私用区、Latin-1 扩展)
|
|
78
|
+
s = Array.from(s)
|
|
79
|
+
.filter((ch) => SAFE_CHAR.test(ch))
|
|
80
|
+
.join('');
|
|
81
|
+
return s.length ? s : fallback;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ============================ 2. 安全词库 ============================ */
|
|
85
|
+
|
|
86
|
+
/** 常见中文姓名(纯汉字,无生僻字) */
|
|
87
|
+
export const SURNAMES = [
|
|
88
|
+
'张', '王', '李', '赵', '刘', '陈', '杨', '黄', '周', '吴',
|
|
89
|
+
'徐', '孙', '马', '朱', '胡', '郭', '何', '高', '林', '罗',
|
|
90
|
+
];
|
|
91
|
+
export const GIVEN_NAMES = [
|
|
92
|
+
'伟', '芳', '娜', '敏', '静', '强', '磊', '洋', '艳', '勇',
|
|
93
|
+
'军', '杰', '娟', '涛', '明', '超', '秀英', '建华', '文博', '雨欣',
|
|
94
|
+
'志强', '晓东', '国栋', '海燕', '春梅', '玉兰', '建华', '立新', '雅琴', '浩然',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 闭环人员名册 —— 业务单据里的"人"从这里取。
|
|
99
|
+
*
|
|
100
|
+
* ★★ 这是"数据闭环"的关键:请假 / 用车 / 值班这些单据里出现的每个人,
|
|
101
|
+
* 都必须在**用户管理**(User 表)里有同名账号。顺序永远是:
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* const roster = makeRoster(20); // 确定性 20 人名册
|
|
105
|
+
* // ① 先把名册建成用户(username 用名册里的 username,name 用名册里的 name)
|
|
106
|
+
* const users = [];
|
|
107
|
+
* for (const p of roster) {
|
|
108
|
+
* users.push(await prisma.user.upsert({ where: { username: p.username }, ... }));
|
|
109
|
+
* }
|
|
110
|
+
* // ② 单据再引用落库后的 userId / 或填 name:text(u.name)
|
|
111
|
+
* const rows = users.map((u) => ({ userId: u.id, ... }));
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* 反例(评委一眼看穿):请假记录里申请人有"李雷、韩梅梅、王强"十几个人,
|
|
115
|
+
* 打开用户管理却只有 admin / zhangsan 两三个人 —— 单据里的人全是凭空编的。
|
|
116
|
+
* **单据里出现多少人,用户表里就要有多少个对应账号**,两边人名必须一致。
|
|
117
|
+
*/
|
|
118
|
+
export interface RosterPerson {
|
|
119
|
+
/** 建议用作 User.username(全库唯一) */
|
|
120
|
+
username: string;
|
|
121
|
+
/** 人名(User.name / 单据里的申请人姓名都用它) */
|
|
122
|
+
name: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 名册用的固定人名表(无生僻字、无重名、纯中文)。
|
|
127
|
+
*
|
|
128
|
+
* ★ 姓名可以随便编 —— 但**只能从这张表里取**,不要在 seed 里手写人名。
|
|
129
|
+
* 这里保证:① 每个字都过白名单(不会出乱码)② 无重名(业务单据里"张伟"出现两次
|
|
130
|
+
* 会被当成同一个人)③ 前 24 个顺序固定,历史数据重跑 seed 不会错位。
|
|
131
|
+
*
|
|
132
|
+
* 人数不够就往后面加,加完记得保持无重名。
|
|
133
|
+
*/
|
|
134
|
+
export const PERSON_NAMES = [
|
|
135
|
+
// 1-24:原有顺序,勿动
|
|
136
|
+
'李雷', '韩梅梅', '王强', '刘洋', '陈静', '杨帆', '赵敏', '周杰',
|
|
137
|
+
'吴迪', '郑爽', '孙悦', '马丽', '朱伟', '胡军', '林峰', '高圆',
|
|
138
|
+
'何洁', '郭涛', '罗宾', '梁静', '许诺', '宋佳', '唐宁', '冯雷',
|
|
139
|
+
// 25-64:扩充,供多部门 / 大名单题目使用
|
|
140
|
+
'邓超', '曹颖', '彭勇', '曾毅', '田野', '董倩', '袁泉', '潘石',
|
|
141
|
+
'蒋雯', '蔡明', '余华', '杜鹏', '叶青', '程亮', '苏晴', '魏然',
|
|
142
|
+
'吕刚', '丁宁', '任杰', '沈冰', '姚远', '卢俊', '姜涛', '崔健',
|
|
143
|
+
'钟楚', '谭松', '陆航', '汪洋', '范冰', '金石', '廖凡', '贾玲',
|
|
144
|
+
'夏雨', '韦唯', '傅明', '方岩', '白露', '邹凯', '孟然', '秦岚',
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 从 PERSON_NAMES 生成确定性名册:同 count 每次结果完全一致。
|
|
149
|
+
*
|
|
150
|
+
* username 固定为 user01 / user02 ……(受控命名空间,绝不会撞题目里的人)。
|
|
151
|
+
*/
|
|
152
|
+
export function makeRoster(count: number): RosterPerson[] {
|
|
153
|
+
if (count > PERSON_NAMES.length) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`名册人数 ${count} 超过姓名池上限 ${PERSON_NAMES.length}。` +
|
|
156
|
+
`请调小 prisma/org.config.ts 的 PEOPLE_TOTAL,或往 seedSafe.ts 的 PERSON_NAMES 里补名字(注意不要重名)。`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const n = Math.max(0, count);
|
|
160
|
+
return PERSON_NAMES.slice(0, n).map((name, i) => ({
|
|
161
|
+
username: `user${String(i + 1).padStart(2, '0')}`,
|
|
162
|
+
name: text(name),
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 部门名 */
|
|
167
|
+
export const DEPT_NAMES = [
|
|
168
|
+
'综合办公室', '技术部', '市场部', '销售一部', '销售二部',
|
|
169
|
+
'人力资源部', '财务部', '运维部', '客户服务部', '研发中心',
|
|
170
|
+
'质量管理部', '采购部', '行政部', '法务部', '战略发展部',
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/** 用车事由(车辆派遣类题目) */
|
|
174
|
+
export const DISPATCH_REASONS = [
|
|
175
|
+
'机场接送', '客户接待', '项目出差', '会议通勤', '物料运送',
|
|
176
|
+
'设备巡检', '外出培训', '商务洽谈', '跨部门协作', '紧急抢修',
|
|
177
|
+
'文件递送', '现场勘察', '供应商走访', '行业展会', '内部调拨',
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
/** 请假事由(请销假类题目) */
|
|
181
|
+
export const LEAVE_REASONS = [
|
|
182
|
+
'个人事务', '探亲', '就医', '婚假', '产假', '陪产假',
|
|
183
|
+
'年假', '调休', '子女入学', '家中急事', '外出考试', '身体不适',
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/** 值班事项(值班管理类题目) */
|
|
187
|
+
export const DUTY_ITEMS = [
|
|
188
|
+
'机房巡检完成,设备运行正常', '接待来访人员三名并登记',
|
|
189
|
+
'处理服务器告警一次,已恢复', '巡查消防通道,无异常',
|
|
190
|
+
'接收并分发快递十二件', '配合维保单位完成设备检修',
|
|
191
|
+
'夜间巡查两次,未发现异常', '记录并上报停电通知',
|
|
192
|
+
'协助处理门禁故障', '完成交接班记录',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
/** 地点 */
|
|
196
|
+
export const PLACES = [
|
|
197
|
+
'市政务中心', '滨江路办公区', '火车站东广场', '国际会展中心',
|
|
198
|
+
'高新产业园', '客户现场', '分公司', '仓储中心', '培训基地', '机场T2航站楼',
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
/** 车牌前缀(真实格式,纯 ASCII + 汉字) */
|
|
202
|
+
export const PLATE_CITIES = ['京A', '沪B', '粤B', '浙A', '苏E', '川A', '鲁B', '津A'];
|
|
203
|
+
|
|
204
|
+
/** 车型 */
|
|
205
|
+
export const VEHICLE_MODELS = ['帕萨特', '别克GL8', '卡罗拉', '汉兰达', '考斯特', '依维柯', '迈腾', '奥德赛'];
|
|
206
|
+
|
|
207
|
+
/* ============================ 3. 生成器(带确定性伪随机) ============================ */
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 确定性伪随机:同一个 seed 值永远产出同一批数据。
|
|
211
|
+
* 好处:每次 seed 结果一致,便于复现问题;也避免 Math.random 导致的"看不出来"的变化。
|
|
212
|
+
*/
|
|
213
|
+
export function makeRng(seed = 20260101) {
|
|
214
|
+
let s = seed >>> 0;
|
|
215
|
+
return () => {
|
|
216
|
+
// xorshift32
|
|
217
|
+
s ^= s << 13; s >>>= 0;
|
|
218
|
+
s ^= s >>> 17;
|
|
219
|
+
s ^= s << 5; s >>>= 0;
|
|
220
|
+
return s / 0x100000000;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function makePicker<T>(list: readonly T[], rng: () => number) {
|
|
225
|
+
return () => list[Math.floor(rng() * list.length) % list.length];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 生成中文姓名。
|
|
230
|
+
*
|
|
231
|
+
* ★ 只用于**独立姓名字段**(联系人、家属姓名这类不需要账号的人)。
|
|
232
|
+
* 申请人 / 审批人 / 处理人这类**要和用户管理对账**的字段,
|
|
233
|
+
* 一律用 `makeRoster()` 建名册、先建用户再引用 —— 不要拿 personName 现编,
|
|
234
|
+
* 否则"单据里十几个人、用户管理里两三个账号",数据不闭环。
|
|
235
|
+
*/
|
|
236
|
+
export function personName(rng: () => number): string {
|
|
237
|
+
const s = makePicker(SURNAMES, rng)();
|
|
238
|
+
const g = makePicker(GIVEN_NAMES, rng)();
|
|
239
|
+
return text(s + g);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ============================ 4. 乱码检测(关键) ============================ */
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* mojibake 特征字符。
|
|
246
|
+
*
|
|
247
|
+
* 这些字符**绝不应该**出现在中文业务数据里:
|
|
248
|
+
* ð Ÿ Â Ã ¢ â € Æ 等 —— 它们是 UTF-8 字节被 Latin-1 解码后的残留
|
|
249
|
+
* U+FFFD —— 解码失败的替换符
|
|
250
|
+
*/
|
|
251
|
+
const MOJIBAKE_RE = /[ðŸÂââ€Ã©Ã¨Ã¤Ã¶Ã¼ÃŸ\uFFFD]/;
|
|
252
|
+
/** 连续 2 个以上 Latin-1 扩展字符,也高度可疑 */
|
|
253
|
+
const MOJIBAKE_RUN_RE = /[\u00A0-\u024F]{2,}/;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 确定不该出现在业务数据里的字符:emoji、私用区、变体选择符、零宽连接符。
|
|
257
|
+
*
|
|
258
|
+
* ★ 这里刻意用**黑名单**而不是"白名单放行"。
|
|
259
|
+
* 旧写法是「不在允许列表里的字符一律报错」,实测会误杀正常中文标点:
|
|
260
|
+
* 「」『』(U+300C..300F)、×、①、㎡ 都被判成乱码,
|
|
261
|
+
* 于是自检一旦接进 seed,`npm run db:seed` 直接失败 —— 这也是它长期没被接线的真实原因。
|
|
262
|
+
* 黑名单只拦"肯定是错的"那一类,不会因为作者用了书名号就把整库数据判死。
|
|
263
|
+
*
|
|
264
|
+
* 2600–27BF(★☆✓ 这类常用符号)故意不拦:中文文案里出现它们是正常的。
|
|
265
|
+
*/
|
|
266
|
+
const EMOJI_RE = /[\u{1F000}-\u{1FAFF}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}\u{E000}-\u{F8FF}]/u;
|
|
267
|
+
|
|
268
|
+
export interface MojibakeHit {
|
|
269
|
+
model: string;
|
|
270
|
+
field: string;
|
|
271
|
+
value: string;
|
|
272
|
+
reasons: string[];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 扫描所有指定的表,找出乱码字段。
|
|
277
|
+
*
|
|
278
|
+
* 用法:seed 末尾调用 `await assertNoMojibake(prisma)`,有乱码直接抛错,
|
|
279
|
+
* 让 `npm run db:seed` 失败 —— 问题在构建期暴露,而不是等评委点开列表。
|
|
280
|
+
*/
|
|
281
|
+
export async function findMojibake(
|
|
282
|
+
// 参数故意用 unknown:调用方传的是 PrismaClient,具体类型随 schema 变化,
|
|
283
|
+
// 这里只需要鸭子类型。收窄写死成某个形状会让调用方被迫 `as unknown as ...` 双跳。
|
|
284
|
+
prismaLike: unknown,
|
|
285
|
+
models: string[] = []
|
|
286
|
+
): Promise<MojibakeHit[]> {
|
|
287
|
+
const hits: MojibakeHit[] = [];
|
|
288
|
+
const targets = models.length ? models : await listModels(prismaLike);
|
|
289
|
+
|
|
290
|
+
for (const model of targets) {
|
|
291
|
+
const delegate = (prismaLike as Record<string, unknown>)[model] as
|
|
292
|
+
| { findMany: (a: unknown) => Promise<Record<string, unknown>[]> }
|
|
293
|
+
| undefined;
|
|
294
|
+
if (!delegate?.findMany) continue;
|
|
295
|
+
|
|
296
|
+
let rows: Record<string, unknown>[];
|
|
297
|
+
try {
|
|
298
|
+
rows = await delegate.findMany({ take: 500 });
|
|
299
|
+
} catch {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
for (const [field, val] of Object.entries(row)) {
|
|
305
|
+
if (typeof val !== 'string') continue;
|
|
306
|
+
const reasons: string[] = [];
|
|
307
|
+
if (MOJIBAKE_RE.test(val)) reasons.push('含 mojibake 特征字符');
|
|
308
|
+
if (MOJIBAKE_RUN_RE.test(val)) reasons.push('含连续 Latin-1 扩展字符');
|
|
309
|
+
if (val.includes('\uFFFD')) reasons.push('含解码替换符 U+FFFD');
|
|
310
|
+
if (EMOJI_RE.test(val)) reasons.push('含 emoji / 私用区字符');
|
|
311
|
+
if (reasons.length) {
|
|
312
|
+
hits.push({ model, field, value: val.slice(0, 60), reasons });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return hits;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 从 prisma 实例上列出所有模型名 */
|
|
321
|
+
async function listModels(prismaLike: unknown): Promise<string[]> {
|
|
322
|
+
const rt = (prismaLike as {
|
|
323
|
+
_runtimeDataModel?: { models?: Record<string, unknown> };
|
|
324
|
+
})._runtimeDataModel;
|
|
325
|
+
if (rt?.models) return Object.keys(rt.models);
|
|
326
|
+
// 兜底:常见模型名
|
|
327
|
+
return ['User', 'Department', 'Employee', 'OperationLog', 'Menu', 'Role'];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 断言全库无乱码。有就抛错并打印明细 —— 让 seed 直接失败。
|
|
332
|
+
*/
|
|
333
|
+
export async function assertNoMojibake(prismaLike: unknown, models: string[] = []): Promise<void> {
|
|
334
|
+
const hits = await findMojibake(prismaLike, models);
|
|
335
|
+
if (!hits.length) return;
|
|
336
|
+
|
|
337
|
+
const lines = hits
|
|
338
|
+
.slice(0, 20)
|
|
339
|
+
.map((h) => ` ${h.model}.${h.field} = ${JSON.stringify(h.value)} ← ${h.reasons.join(';')}`);
|
|
340
|
+
throw new Error(
|
|
341
|
+
`检测到 ${hits.length} 处乱码/非法字符,seed 已中止:\n` +
|
|
342
|
+
lines.join('\n') +
|
|
343
|
+
`\n\n修复方法:演示数据一律用 prisma/seedSafe.ts 里的 text() 清洗,` +
|
|
344
|
+
`或直接从安全词库(DISPATCH_REASONS / LEAVE_REASONS / DEPT_NAMES 等)取值。` +
|
|
345
|
+
`\n禁止在 seed 里手写 emoji 或非中文特殊符号。`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { defineConfig } from 'prisma/config';
|
|
3
|
+
|
|
4
|
+
// Prisma 7 起,schema 里不再允许写 datasource.url(写了会报 P1012)。
|
|
5
|
+
// 连接串统一放到这里,供 migrate / db push / studio 等 CLI 命令使用。
|
|
6
|
+
//
|
|
7
|
+
// 运行时(PrismaClient 构造函数)在 Prisma 7 里改用 driver adapter,
|
|
8
|
+
// 例如:new PrismaClient({ adapter: new PrismaLibSql({ url: ... }) })
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
schema: path.join('prisma', 'schema.prisma'),
|
|
11
|
+
datasource: {
|
|
12
|
+
url: process.env.DATABASE_URL ?? 'file:./dev.db',
|
|
13
|
+
},
|
|
14
|
+
});
|