@oj-module/cli 0.2.6 → 0.2.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.
Files changed (105) hide show
  1. package/README.md +1 -1
  2. package/devkit/manual.md +19 -17
  3. package/devkit/sql-builder-missing-capabilities-pr.md +234 -0
  4. package/package.json +2 -2
  5. package/shell-dist/assets/runtime.js +2 -2
  6. package/shell-dist/index.html +2 -2
  7. package/shell-dist/versions.json +3 -3
  8. package/src/contract/__tests__/handler-scan.test.ts +29 -0
  9. package/src/contract/__tests__/market-client.integration.test.ts +167 -0
  10. package/src/contract/handler-scan.ts +35 -4
  11. package/src/dev.ts +10 -2
  12. package/src/index.ts +3 -3
  13. package/src/init.ts +2 -0
  14. package/src/preview-config.ts +163 -0
  15. package/src/preview.ts +105 -56
  16. package/src/usage.ts +3 -2
  17. package/templates/api/config.yaml +4 -2
  18. package/templates/api/src/_platform/README.md +31 -0
  19. package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
  20. package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
  21. package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
  22. package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
  23. package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
  24. package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
  25. package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
  26. package/templates/api/src/_platform/_lifecycle/uninstall.ts +13 -8
  27. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
  28. package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +8 -6
  29. package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
  30. package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
  31. package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +18 -17
  32. package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
  33. package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
  34. package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
  35. package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +12 -12
  36. package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
  37. package/templates/api/src/_platform/contacts/api.ts +2 -2
  38. package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
  39. package/templates/api/src/_platform/manifest.yaml +3 -1
  40. package/templates/api/src/_platform/market/api.ts +54 -16
  41. package/templates/api/src/_platform/schema.yaml +14 -0
  42. package/templates/api/src/_platform/solutions/api.ts +13 -12
  43. package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
  44. package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
  45. package/templates/api/src/auth/_shared/session.ts +25 -21
  46. package/templates/api/src/auth/api.ts +1 -1
  47. package/templates/api/src/demo/todos/api.ts +1 -0
  48. package/templates/api/src/home/line/api.ts +4 -4
  49. package/templates/api/src/home/pie/api.ts +4 -4
  50. package/templates/api/src/notification/_shared/notify.ts +40 -38
  51. package/templates/api/src/notifications/api.ts +1 -4
  52. package/templates/api/src/personal-center/upload/api.ts +1 -1
  53. package/templates/api/src/system/login-methods/api.ts +1 -1
  54. package/templates/api/src/system/oplog/api.ts +3 -3
  55. package/templates/api/src/system/permissions/api.ts +2 -2
  56. package/templates/api/src/system/roles/api.ts +2 -2
  57. package/templates/api/src/system/roles/bindings/api.ts +2 -2
  58. package/templates/api/src/system/settings/api.ts +2 -2
  59. package/templates/api/src/system/users/api.ts +2 -2
  60. package/templates/api/src/system/users/forbidden/api.ts +2 -2
  61. package/templates/api/src/system/users/password/api.ts +2 -2
  62. package/templates/api/src/system/users/roles/api.ts +2 -2
  63. package/templates/api/src/web/user-info/api.ts +2 -5
  64. package/templates/api/tests/auth-session.spec.ts +1 -1
  65. package/templates/api/tests/auth-throttle.spec.ts +1 -1
  66. package/templates/api/tests/guard.spec.ts +1 -1
  67. package/templates/api/tests/lifecycle/market-api.spec.ts +1 -1
  68. package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
  69. package/templates/api/tests/lifecycle/uninstall.spec.ts +1 -1
  70. package/templates/api/tests/query-utils.spec.ts +1 -1
  71. package/templates/api/tests/rbac-audit.spec.ts +1 -1
  72. package/templates/api/tests/rbac-contacts.spec.ts +2 -2
  73. package/templates/api/tests/rbac-guard.spec.ts +1 -1
  74. package/templates/api/tests/rbac-roles.spec.ts +2 -2
  75. package/templates/api/tests/rbac-settings.spec.ts +2 -2
  76. package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
  77. package/templates/api/tests/rbac-users.spec.ts +1 -1
  78. package/templates/api/tsconfig.json +8 -0
  79. package/templates/env.d.ts +2 -0
  80. package/templates/global.d.ts +816 -0
  81. package/templates/init.include.yaml +11 -0
  82. package/templates/logs/task-events.jsonl +0 -0
  83. package/templates/openapi.json +525 -13
  84. package/templates/package.json +2 -1
  85. package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
  86. package/templates/web/src/_platform/__client/api.ts +152 -39
  87. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
  88. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
  89. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
  90. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
  91. package/templates/web/src/oj-market/api/client.ts +5 -105
  92. package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
  93. package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
  94. package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
  95. package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
  96. package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
  97. package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
  98. package/templates/web/src/oj-market/locales/en-US.json +2 -1
  99. package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
  100. package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
  101. package/templates/web.config.ts +3 -3
  102. package/vendor/host-versions.json +3 -3
  103. /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
  104. /package/templates/api/src/_platform/_shared/service/{oplog.ts → infra/oplog.ts} +0 -0
  105. /package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  // 登录限速编排(B5/AC1-26):auth/login 与 auth/ldap-login 共用的 (IP, username) 双维度守卫 + 失败记账。
2
2
  // 状态全在 DB(sys_login_throttle/sys_rate_window,迁移 0009)——oj 每请求重建模块,进程内方案恒失效(§9.3-3)。
3
- import { auditOp } from "../../_platform/_shared/service/audit";
4
- import { loginBlocked, loginFailed, loginSucceeded, rateLimit } from "../../_platform/_shared/service/rate";
3
+ import { auditOp } from "../../_platform/_shared/service/infra/audit";
4
+ import { loginBlocked, loginFailed, loginSucceeded, rateLimit } from "../../_platform/_shared/service/infra/rate";
5
5
 
6
6
  /** 客户端 IP:优先 X-Forwarded-For(反代后),回落 X-Real-IP。 */
7
7
  export async function clientIp(): Promise<string> {
@@ -4,8 +4,8 @@
4
4
  // B4:access JWT 仅载 sub(roles 走登录响应/资料接口,不进 claims)。
5
5
  // B3:refresh = 不透明随机串 + DB 存 SHA-256 哈希(sys_refresh_token,迁移 0008);
6
6
  // 轮换作旧 + 滑动窗口(绝对 TTL 封顶 min(旧 exp, now+30d));reuse 检测全族吊销 + 审计。
7
- import { invalidateUser } from "../../_platform/_shared/service/access";
8
- import { recordOp } from "../../_platform/_shared/service/audit";
7
+ import { invalidateUser } from "../../_platform/_shared/service/infra/access";
8
+ import { recordOp } from "../../_platform/_shared/service/infra/audit";
9
9
 
10
10
  export interface SessionDeps {
11
11
  randomHex: (bytes: number) => string
@@ -59,13 +59,10 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
59
59
  const hashOf = (token: string) => deps.sha256Hex(token);
60
60
 
61
61
  async function profileOf(uid: number): Promise<SessionUser> {
62
- const rows = await db.query(
63
- "select id, username, display_name, avatar_base64, must_change from users where id = ?",
64
- [uid],
65
- ) as Array<Record<string, unknown>>;
62
+ const rows = await db.table("users").select(["id", "username", "display_name", "avatar_base64", "must_change"]).where({ field: "id", op: "eq", value: uid }).all() as Array<Record<string, unknown>>;
66
63
  if (!rows[0])
67
64
  fail(404, "user not found");
68
- const roleRows = await db.query("select role_name from sys_user_role where user_id = ?", [uid]) as Array<{ role_name: string }>;
65
+ const roleRows = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: uid }).all() as Array<{ role_name: string }>;
69
66
  return {
70
67
  id: uid,
71
68
  username: String(rows[0].username),
@@ -79,10 +76,13 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
79
76
  async function issueFor(uid: number): Promise<TokenBundle> {
80
77
  const user = await profileOf(uid);
81
78
  const refreshToken = deps.randomHex(32);
82
- await db.exec(
83
- "insert into sys_refresh_token (token_hash, user_id, expires_at, revoked, created_at) values (?, ?, ?, 0, ?)",
84
- [hashOf(refreshToken), uid, now() + deps.refreshTtlSecs, now()],
85
- );
79
+ await db.table("sys_refresh_token").insert({
80
+ token_hash: hashOf(refreshToken),
81
+ user_id: uid,
82
+ expires_at: now() + deps.refreshTtlSecs,
83
+ revoked: 0,
84
+ created_at: now(),
85
+ }).run();
86
86
  return {
87
87
  access_token: await deps.signAccess(String(uid)),
88
88
  refresh_token: refreshToken,
@@ -94,13 +94,13 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
94
94
 
95
95
  /** 吊销联动(§3.1):禁用/重置口令/改密 → 删该用户全部 refresh 记录。 */
96
96
  async function revokeAll(uid: number): Promise<void> {
97
- await db.exec("delete from sys_refresh_token where user_id = ?", [uid]);
97
+ await db.table("sys_refresh_token").where({ field: "user_id", op: "eq", value: uid }).delete().run();
98
98
  }
99
99
 
100
100
  return {
101
101
  /** B1+D14:本地登录——先验口令后查禁用;号不存在/口令错/禁用统一 401(不泄露账号状态)。 */
102
102
  async localLogin(username: string, password: string): Promise<TokenBundle> {
103
- const rows = await db.query("select id, password_hash, is_forbidden from users where username = ?", [String(username ?? "")]) as Array<Record<string, unknown>>;
103
+ const rows = await db.table("users").select(["id", "password_hash", "is_forbidden"]).where({ field: "username", op: "eq", value: String(username ?? "") }).all() as Array<Record<string, unknown>>;
104
104
  const row = rows[0];
105
105
  if (!row || !(await deps.bcryptVerify(String(password ?? ""), String(row.password_hash))))
106
106
  fail(401, "invalid credentials");
@@ -111,6 +111,7 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
111
111
 
112
112
  /** B3:刷新——轮换作旧;reuse 检测(已吊销串二次出现)全族吊销 + 审计。 */
113
113
  async refresh(token: string): Promise<TokenBundle> {
114
+ // 保留 db.query:select * 无已知列清单(规则6)
114
115
  const rows = await db.query("select * from sys_refresh_token where token_hash = ?", [hashOf(String(token ?? ""))]) as Array<Record<string, unknown>>;
115
116
  const row = rows[0];
116
117
  if (!row)
@@ -123,14 +124,17 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
123
124
  }
124
125
  if (Number(row.expires_at) <= now())
125
126
  fail(401, "invalid or expired refresh token");
126
- await db.exec("update sys_refresh_token set revoked = 1 where token_hash = ?", [row.token_hash]);
127
+ await db.table("sys_refresh_token").update({ revoked: 1 }).where({ field: "token_hash", op: "eq", value: row.token_hash }).run();
127
128
  // 滑动窗口 + 绝对 30d 封顶(§3.1「绝对 TTL + 滑动」)
128
129
  const newExp = Math.min(Number(row.expires_at), now() + deps.refreshTtlSecs);
129
130
  const refreshToken = deps.randomHex(32);
130
- await db.exec(
131
- "insert into sys_refresh_token (token_hash, user_id, expires_at, revoked, created_at) values (?, ?, ?, 0, ?)",
132
- [hashOf(refreshToken), uid, newExp, now()],
133
- );
131
+ await db.table("sys_refresh_token").insert({
132
+ token_hash: hashOf(refreshToken),
133
+ user_id: uid,
134
+ expires_at: newExp,
135
+ revoked: 0,
136
+ created_at: now(),
137
+ }).run();
134
138
  const user = await profileOf(uid);
135
139
  return {
136
140
  access_token: await deps.signAccess(String(uid)),
@@ -143,7 +147,7 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
143
147
 
144
148
  /** AC1-27:自助改密——本人 + 旧口令校验;改密清 must_change、级联删 refresh、换发正式 token。 */
145
149
  async changePassword(uid: number, oldPassword: string, newPassword: string): Promise<TokenBundle> {
146
- const rows = await db.query("select password_hash from users where id = ?", [uid]) as Array<{ password_hash: string }>;
150
+ const rows = await db.table("users").select(["password_hash"]).where({ field: "id", op: "eq", value: uid }).all() as Array<{ password_hash: string }>;
147
151
  if (!rows[0])
148
152
  fail(404, "user not found");
149
153
  if (!(await deps.bcryptVerify(String(oldPassword ?? ""), String(rows[0].password_hash))))
@@ -152,8 +156,8 @@ export function createSession(deps: SessionDeps = runtimeDeps()) {
152
156
  fail(400, "password too short (min 6)");
153
157
  const hash = await deps.bcryptHash(String(newPassword));
154
158
  await db.tx(async (t) => {
155
- await t.exec("update users set password_hash = ?, must_change = 0 where id = ?", [hash, uid]);
156
- await t.exec("delete from sys_refresh_token where user_id = ?", [uid]);
159
+ await t.table("users").update({ password_hash: hash, must_change: 0 }).where({ field: "id", op: "eq", value: uid }).run();
160
+ await t.table("sys_refresh_token").where({ field: "user_id", op: "eq", value: uid }).delete().run();
157
161
  });
158
162
  invalidateUser(uid);
159
163
  return issueFor(uid);
@@ -36,7 +36,7 @@ const logout = {
36
36
  // POST /api/auth/logout —— 吊销当前 refresh 串(access 到期前仍有效,语义同旧版)。
37
37
  async post() {
38
38
  const token = String((http.body || {}).refresh_token ?? "");
39
- await db.exec("delete from sys_refresh_token where token_hash = ?", [crypto.sha256Hex(token)]);
39
+ await db.table("sys_refresh_token").where({ field: "token_hash", op: "eq", value: crypto.sha256Hex(token) }).delete().run();
40
40
  json.ok(null);
41
41
  },
42
42
  };
@@ -9,6 +9,7 @@ const todos = {
9
9
  const where = kw ? "WHERE title LIKE ?" : "";
10
10
  const params = kw ? [`%${kw}%`] : [];
11
11
  try {
12
+ // 保留 db.query:WHERE 由 JS 字符串拼接动态生成(规则5)
12
13
  const totalRows = await db.query(`SELECT COUNT(*) AS c FROM todos ${where}`, params);
13
14
  const total = Number(totalRows[0]?.c ?? 0);
14
15
  const rows = await db.query(
@@ -12,10 +12,10 @@ const line = {
12
12
  const today = Math.floor(Date.now() / 86400000);
13
13
  const start = today - days + 1;
14
14
 
15
- const rows = await db.query(
16
- "SELECT day, SUM(value) AS v FROM home_line WHERE day >= ? AND day <= ? GROUP BY day",
17
- [start, today],
18
- );
15
+ const rows = await db.table("home_line").select([
16
+ "day",
17
+ { fn: "sum", field: "value", as: "v" },
18
+ ]).where({ field: "day", op: "gte", value: start }).where({ field: "day", op: "lte", value: today }).groupBy(["day"]).all();
19
19
  const map = new Map<number, number>();
20
20
  for (const r of rows)
21
21
  map.set(Number(r.day), Number(r.v));
@@ -6,10 +6,10 @@
6
6
  const pie = {
7
7
  async get() {
8
8
  try {
9
- const rows = await db.query(
10
- "SELECT category, SUM(value) AS v FROM home_pie GROUP BY category",
11
- [],
12
- );
9
+ const rows = await db.table("home_pie").select([
10
+ "category",
11
+ { fn: "sum", field: "value", as: "v" },
12
+ ]).groupBy(["category"]).all();
13
13
  json.ok(rows.map(r => ({ code: String(r.category), value: Number(r.v) })));
14
14
  }
15
15
  catch (e) {
@@ -51,6 +51,7 @@ interface Row {
51
51
  interface NotifyDb {
52
52
  query: (sql: string, params?: unknown[]) => Promise<Row[]>
53
53
  exec: (sql: string, params?: unknown[]) => Promise<number>
54
+ table: DBInstance["table"]
54
55
  }
55
56
  declare const db: NotifyDb;
56
57
 
@@ -83,37 +84,30 @@ function mapItem(r: Row): NoticeItem {
83
84
 
84
85
  /** 给指定用户推一条按人通知,返回命名空间 id "u:<id>" */
85
86
  export async function pushNotice(input: PushInput): Promise<string> {
86
- const ins = await db.query(
87
- `INSERT INTO notifications (user_id, avatar, date, is_read, message, title, link, category)
88
- VALUES (?, ?, ?, 0, ?, ?, ?, ?) RETURNING id`,
89
- [
90
- Number(input.userId),
91
- input.avatar ?? "",
92
- nowStr(),
93
- input.message,
94
- input.title,
95
- input.link ?? null,
96
- input.category ?? "notice",
97
- ],
98
- );
87
+ const ins = await db.table("notifications").insert({
88
+ user_id: Number(input.userId),
89
+ avatar: input.avatar ?? "",
90
+ date: nowStr(),
91
+ is_read: 0,
92
+ message: input.message,
93
+ title: input.title,
94
+ link: input.link ?? null,
95
+ category: input.category ?? "notice",
96
+ }).returning(["id"]).run() as Array<{ id: number }>;
99
97
  return `u:${Number(ins[0]!.id)}`;
100
98
  }
101
99
 
102
100
  /** 广播一条通知(全员可见,一行存储),返回命名空间 id "b:<id>" */
103
101
  export async function pushBroadcastNotice(input: PushBroadcastInput): Promise<string> {
104
- const ins = await db.query(
105
- `INSERT INTO notifications_broadcast (avatar, date, message, title, link, category, create_time)
106
- VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id`,
107
- [
108
- input.avatar ?? "",
109
- nowStr(),
110
- input.message,
111
- input.title,
112
- input.link ?? null,
113
- input.category ?? "notice",
114
- nowStr(),
115
- ],
116
- );
102
+ const ins = await db.table("notifications_broadcast").insert({
103
+ avatar: input.avatar ?? "",
104
+ date: nowStr(),
105
+ message: input.message,
106
+ title: input.title,
107
+ link: input.link ?? null,
108
+ category: input.category ?? "notice",
109
+ create_time: nowStr(),
110
+ }).returning(["id"]).run() as Array<{ id: number }>;
117
111
  return `b:${Number(ins[0]!.id)}`;
118
112
  }
119
113
 
@@ -125,16 +119,18 @@ export async function delNotice(id: string, userId: string | number): Promise<bo
125
119
  const [scope, raw] = splitId(id);
126
120
  const uid = Number(userId);
127
121
  if (scope === "b") {
128
- await db.exec(
129
- "DELETE FROM notifications_broadcast_read WHERE broadcast_id = ? AND user_id = ?",
130
- [Number(raw), uid],
131
- );
122
+ await db.table("notifications_broadcast_read")
123
+ .where({ field: "broadcast_id", op: "eq", value: Number(raw) })
124
+ .where({ field: "user_id", op: "eq", value: uid })
125
+ .delete()
126
+ .run();
132
127
  return true;
133
128
  }
134
- const n = await db.exec(
135
- "DELETE FROM notifications WHERE id = ? AND user_id = ?",
136
- [Number(raw), uid],
137
- );
129
+ const n = await db.table("notifications")
130
+ .where({ field: "id", op: "eq", value: Number(raw) })
131
+ .where({ field: "user_id", op: "eq", value: uid })
132
+ .delete()
133
+ .run() as number;
138
134
  return n > 0;
139
135
  }
140
136
 
@@ -170,13 +166,18 @@ export async function markRead(id: string, userId: string | number): Promise<boo
170
166
  const uid = Number(userId);
171
167
  const now = nowStr();
172
168
  if (scope === "b") {
169
+ // 保留 db.exec:INSERT OR IGNORE 构造器无等价(引擎按方言改写 IGNORE)
173
170
  await db.exec(
174
171
  "INSERT OR IGNORE INTO notifications_broadcast_read (broadcast_id, user_id, read_time) VALUES (?, ?, ?)",
175
172
  [Number(raw), uid, now],
176
173
  );
177
174
  return true;
178
175
  }
179
- const n = await db.exec("UPDATE notifications SET is_read = 1 WHERE id = ? AND user_id = ?", [Number(raw), uid]);
176
+ const n = await db.table("notifications")
177
+ .where({ field: "id", op: "eq", value: Number(raw) })
178
+ .where({ field: "user_id", op: "eq", value: uid })
179
+ .update({ is_read: 1 })
180
+ .run() as number;
180
181
  return n > 0;
181
182
  }
182
183
 
@@ -184,7 +185,8 @@ export async function markRead(id: string, userId: string | number): Promise<boo
184
185
  export async function markAllRead(userId: string | number): Promise<void> {
185
186
  const uid = Number(userId);
186
187
  const now = nowStr();
187
- await db.exec("UPDATE notifications SET is_read = 1 WHERE user_id = ?", [uid]);
188
+ await db.table("notifications").where({ field: "user_id", op: "eq", value: uid }).update({ is_read: 1 }).run();
189
+ // 保留 db.exec:INSERT OR IGNORE ... SELECT 构造器无法表达
188
190
  await db.exec(
189
191
  "INSERT OR IGNORE INTO notifications_broadcast_read (broadcast_id, user_id, read_time) SELECT id, ?, ? FROM notifications_broadcast",
190
192
  [uid, now],
@@ -194,6 +196,6 @@ export async function markAllRead(userId: string | number): Promise<void> {
194
196
  /** 清空:删按人项 + 删本人广播回执(列表消失) */
195
197
  export async function clearNotices(userId: string | number): Promise<void> {
196
198
  const uid = Number(userId);
197
- await db.exec("DELETE FROM notifications WHERE user_id = ?", [uid]);
198
- await db.exec("DELETE FROM notifications_broadcast_read WHERE user_id = ?", [uid]);
199
+ await db.table("notifications").where({ field: "user_id", op: "eq", value: uid }).delete().run();
200
+ await db.table("notifications_broadcast_read").where({ field: "user_id", op: "eq", value: uid }).delete().run();
199
201
  }
@@ -12,10 +12,7 @@
12
12
  export default {
13
13
  async get() {
14
14
  try {
15
- const rows = await db.query(
16
- "SELECT id, avatar, date, is_read, message, title FROM notifications ORDER BY id DESC",
17
- [],
18
- );
15
+ const rows = await db.table("notifications").select(["id", "avatar", "date", "is_read", "message", "title"]).orderBy([{ field: "id", dir: "desc" }]).all();
19
16
  json.ok(rows.map(r => ({
20
17
  id: Number(r.id),
21
18
  avatar: String(r.avatar),
@@ -41,7 +41,7 @@ const handler = {
41
41
  const bytes = meta.key ? await blob.get(meta.key) : await http.file(0);
42
42
  const b64 = toBase64(bytes);
43
43
  const dataUrl = `data:${meta.content_type || "application/octet-stream"};base64,${b64}`;
44
- await db.exec("UPDATE users SET avatar_base64 = ? WHERE id = ?", [dataUrl, Number(uid)]);
44
+ await db.table("users").update({ avatar_base64: dataUrl }).where({ field: "id", op: "eq", value: Number(uid) }).run();
45
45
  // 大文件搬运/留档用 blob(v0.1.47):搬走用 blob.move,别 get + put(字节进 V8)
46
46
  if (meta.key)
47
47
  await blob.del(meta.key);
@@ -1,7 +1,7 @@
1
1
  import { respond } from "../../_platform/_shared/respond";
2
2
  // GET /api/system/login-methods —— 登录方式发现(C5/D2;匿名端点:登录前无 token,
3
3
  // 项目 config.yaml auth.anonymous_paths 必须列 /system/login-methods,漏列则握手 401 登录页必坏)。
4
- import { listLoginMethods } from "../../_platform/_shared/service/settings";
4
+ import { listLoginMethods } from "../../_platform/_shared/service/domain/settings";
5
5
 
6
6
  async function get() {
7
7
  await respond(async () => {
@@ -1,9 +1,9 @@
1
1
  import { respond } from "../../_platform/_shared/respond";
2
2
  // GET /api/system/oplog?action=&username=&module=&limit= —— 操作日志查询(AC1-29;D13 admin 可查)。
3
3
  // POST /api/system/oplog —— 操作日志写入(C3;登录态即可,解决方案模块记录自身操作)。
4
- import { assertSystemAdmin, currentAccess } from "../../_platform/_shared/service/access";
5
- import { recordOp } from "../../_platform/_shared/service/audit";
6
- import { searchOplog } from "../../_platform/_shared/service/oplog";
4
+ import { assertSystemAdmin, currentAccess } from "../../_platform/_shared/service/infra/access";
5
+ import { recordOp } from "../../_platform/_shared/service/infra/audit";
6
+ import { searchOplog } from "../../_platform/_shared/service/infra/oplog";
7
7
 
8
8
  async function get() {
9
9
  await respond(async () => {
@@ -1,7 +1,7 @@
1
1
  import { respond } from "../../_platform/_shared/respond";
2
+ import { listPermissions } from "../../_platform/_shared/service/domain/roles";
2
3
  // GET /api/system/permissions —— 权限点全量(C2 绑定编辑器数据源,deprecated 标灰)。
3
- import { assertSystemAdmin } from "../../_platform/_shared/service/access";
4
- import { listPermissions } from "../../_platform/_shared/service/roles";
4
+ import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
5
5
 
6
6
  async function get() {
7
7
  await respond(async () => {
@@ -1,8 +1,8 @@
1
1
  import { respond } from "../../_platform/_shared/respond";
2
+ import { createRole, deleteRole, listRoles } from "../../_platform/_shared/service/domain/roles";
2
3
  // 管理域端点 /api/system/roles(设计文档 D207:/api/system/* = 管理域,assertSystemAdmin 判权)。
3
4
  // handler 层只编排:守卫 → service → json 信封;name 走 query(wiki 先例:静态段 + http.query)。
4
- import { assertSystemAdmin } from "../../_platform/_shared/service/access";
5
- import { createRole, deleteRole, listRoles } from "../../_platform/_shared/service/roles";
5
+ import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
6
6
 
7
7
  async function guard(): Promise<void> {
8
8
  await assertSystemAdmin();
@@ -1,9 +1,9 @@
1
1
  import { respond } from "../../../_platform/_shared/respond";
2
+ import { updateBindings } from "../../../_platform/_shared/service/domain/roles";
2
3
  // PUT /api/system/roles/bindings?name=xxx —— 整集替换角色绑定。
3
4
  // AC1-18 声明角色绑定可调整;AC1-19 builtin 硬拒(service 层);AC1-30 摘绑定 = 紧急吊销出口
4
5
  // (service 内写后按持有者失效判权缓存)。
5
- import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
6
- import { updateBindings } from "../../../_platform/_shared/service/roles";
6
+ import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
7
7
 
8
8
  async function put() {
9
9
  await respond(async () => {
@@ -1,7 +1,7 @@
1
1
  import { respond } from "../../_platform/_shared/respond";
2
+ import { listSettings, updateSetting } from "../../_platform/_shared/service/domain/settings";
2
3
  // /api/system/settings —— 系统设置(PRD §4.3):GET 按组列出(secret 掩码),PUT 写值(写审计)。
3
- import { assertSystemAdmin } from "../../_platform/_shared/service/access";
4
- import { listSettings, updateSetting } from "../../_platform/_shared/service/settings";
4
+ import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
5
5
 
6
6
  const settingItemSchema = {
7
7
  type: "object",
@@ -1,9 +1,9 @@
1
1
  import { respond } from "../../_platform/_shared/respond";
2
+ import { createUser, listUsers } from "../../_platform/_shared/service/domain/users";
2
3
  // 管理域端点组 /api/system/users(AC1-7~10):handler 只编排——assertSystemAdmin 守卫 → service → json。
3
4
  // 保护红线在 service 层硬 enforcement:禁止操作自己、admin 不碰成员资格、最后 superadmin 兜底。
4
5
  // 子端点:roles/password/forbidden 各为独立目录(wiki per-endpoint 先例)。
5
- import { assertSystemAdmin } from "../../_platform/_shared/service/access";
6
- import { createUser, listUsers } from "../../_platform/_shared/service/users";
6
+ import { assertSystemAdmin } from "../../_platform/_shared/service/infra/access";
7
7
 
8
8
  const bcryptDeps = { bcryptHash: (pw: string) => bcrypt.hash(pw) };
9
9
 
@@ -1,7 +1,7 @@
1
1
  import { respond } from "../../../_platform/_shared/respond";
2
+ import { setForbidden } from "../../../_platform/_shared/service/domain/users";
2
3
  // PUT /api/system/users/forbidden?uid= —— 禁用/解禁(禁用即删 refresh,撤销即强退)。
3
- import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
4
- import { setForbidden } from "../../../_platform/_shared/service/users";
4
+ import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
5
5
 
6
6
  async function put() {
7
7
  await respond(async () => {
@@ -1,7 +1,7 @@
1
1
  import { respond } from "../../../_platform/_shared/respond";
2
+ import { changePasswordByAdmin } from "../../../_platform/_shared/service/domain/users";
2
3
  // PUT /api/system/users/password?uid= —— 管理员重置口令(must_change=1 + 级联吊销 refresh)。
3
- import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
4
- import { changePasswordByAdmin } from "../../../_platform/_shared/service/users";
4
+ import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
5
5
 
6
6
  async function put() {
7
7
  await respond(async () => {
@@ -1,7 +1,7 @@
1
1
  import { respond } from "../../../_platform/_shared/respond";
2
+ import { updateUserRoles } from "../../../_platform/_shared/service/domain/users";
2
3
  // PUT /api/system/users/roles?uid= —— 整集替换用户角色(AC1-7;sys_user_role 权威源 + 写时失效)。
3
- import { assertSystemAdmin } from "../../../_platform/_shared/service/access";
4
- import { updateUserRoles } from "../../../_platform/_shared/service/users";
4
+ import { assertSystemAdmin } from "../../../_platform/_shared/service/infra/access";
5
5
 
6
6
  async function put() {
7
7
  await respond(async () => {
@@ -10,16 +10,13 @@ export default {
10
10
  return;
11
11
  }
12
12
  try {
13
- const rows = await db.query(
14
- "SELECT id, username, display_name, avatar_base64, email, must_change FROM users WHERE id = ?",
15
- [Number(uid)],
16
- );
13
+ const rows = await db.table("users").select(["id", "username", "display_name", "avatar_base64", "email", "must_change"]).where({ field: "id", op: "eq", value: Number(uid) }).all();
17
14
  const row = rows[0];
18
15
  if (!row) {
19
16
  json.fail(404, "user not found");
20
17
  return;
21
18
  }
22
- const roleRows = await db.query("SELECT role_name FROM sys_user_role WHERE user_id = ?", [Number(uid)]) as Array<{ role_name: string }>;
19
+ const roleRows = await db.table("sys_user_role").select(["role_name"]).where({ field: "user_id", op: "eq", value: Number(uid) }).all() as Array<{ role_name: string }>;
23
20
  json.ok({
24
21
  id: String(row.id),
25
22
  avatar: String(row.avatar_base64 ?? ""),
@@ -4,7 +4,7 @@
4
4
  import type { SessionDeps } from "../src/auth/_shared/session";
5
5
 
6
6
  import { afterEach, beforeAll, describe, expect, it } from "vitest";
7
- import { assertPermission } from "../src/_platform/_shared/service/access";
7
+ import { assertPermission } from "../src/_platform/_shared/service/infra/access";
8
8
  import { createSession } from "../src/auth/_shared/session";
9
9
  import { installDb } from "./fixture";
10
10
 
@@ -5,7 +5,7 @@
5
5
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
6
6
  import { afterEach, beforeAll, describe, expect, it } from "vitest";
7
7
 
8
- import { loginBlocked, loginFailed, loginSucceeded, resetLoginThrottle } from "../src/_platform/_shared/service/rate";
8
+ import { loginBlocked, loginFailed, loginSucceeded, resetLoginThrottle } from "../src/_platform/_shared/service/infra/rate";
9
9
  import { installDb } from "./fixture";
10
10
 
11
11
  let dbo: ReturnType<typeof installDb>;
@@ -3,7 +3,7 @@
3
3
  // (真实迁移建表 / 运行时全局 db 同面 / 事务往返)——S0 spike 的「一条真实判权 AC + 形态证明」。
4
4
  import { beforeAll, describe, expect, it } from "vitest";
5
5
 
6
- import { decideAccess, moduleOf } from "../src/_platform/_shared/service/guard";
6
+ import { decideAccess, moduleOf } from "../src/_platform/_shared/service/infra/guard";
7
7
  import { ADMIN, installDb } from "./fixture";
8
8
 
9
9
  const as = (roles: string[], bindings: Record<string, string[]> = {}) => ({ roles, bindings });
@@ -26,7 +26,7 @@ import { readLock, writeLock } from "../../src/_platform/_lifecycle/lock";
26
26
  import { createStateStore } from "../../src/_platform/_lifecycle/state";
27
27
  import { getTask, writeTask } from "../../src/_platform/_lifecycle/tasks";
28
28
  import { createTrustStore } from "../../src/_platform/_lifecycle/trust";
29
- import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
29
+ import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
30
30
  import { createMarketApi, marketEnv, parseVersionsJson, versionsJsonPathOf } from "../../src/_platform/market/api";
31
31
  import { installDb, MARKET_HISTORY_DDL } from "../fixture";
32
32
 
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
13
  import { createTrustStore, isValidKeyId } from "../../src/_platform/_lifecycle/trust";
14
- import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
14
+ import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
15
15
  import { createMarketApi, marketEnv } from "../../src/_platform/market/api";
16
16
  import { installDb } from "../fixture";
17
17
 
@@ -14,7 +14,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
14
14
  import { readLock, writeLock } from "../../src/_platform/_lifecycle/lock";
15
15
  import { createStateStore } from "../../src/_platform/_lifecycle/state";
16
16
  import { catalogSpec, collectRetainedTables, tablePrefixPattern } from "../../src/_platform/_lifecycle/uninstall";
17
- import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
17
+ import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
18
18
  import { createMarketApi } from "../../src/_platform/market/api";
19
19
  import { installDb, MARKET_HISTORY_DDL } from "../fixture";
20
20
 
@@ -3,7 +3,7 @@
3
3
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
4
4
  import { beforeAll, describe, expect, it } from "vitest";
5
5
 
6
- import { likePattern, paged } from "../src/_platform/_shared/service/query";
6
+ import { likePattern, paged } from "../src/_platform/_shared/service/infra/query";
7
7
  import { installDb } from "./fixture";
8
8
 
9
9
  let dbo: ReturnType<typeof installDb>;
@@ -3,7 +3,7 @@
3
3
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
4
4
  import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
5
5
 
6
- import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/_platform/_shared/service/audit";
6
+ import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/_platform/_shared/service/infra/audit";
7
7
  import { installDb } from "./fixture";
8
8
 
9
9
  let dbo: ReturnType<typeof installDb>;
@@ -4,7 +4,7 @@
4
4
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
5
5
  import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
6
6
 
7
- import { contactDetail, searchContacts } from "../src/_platform/_shared/service/contacts";
7
+ import { contactDetail, searchContacts } from "../src/_platform/_shared/service/domain/contacts";
8
8
  import { installDb } from "./fixture";
9
9
 
10
10
  let dbo: ReturnType<typeof installDb>;
@@ -74,7 +74,7 @@ describe("详情与限速", () => {
74
74
  it("接口级限速:同用户突发超限 → 429", async () => {
75
75
  const uid = mkUser("c_rate");
76
76
  g.http = { user: { id: uid } };
77
- const { rateLimit } = await import("../src/_platform/_shared/service/rate");
77
+ const { rateLimit } = await import("../src/_platform/_shared/service/infra/rate");
78
78
  // 窗口 60s / 限额 3(测试参数;生产端点各自定额)
79
79
  for (let i = 0; i < 3; i++)
80
80
  await rateLimit(`contacts:${uid}`, 3, 60_000);
@@ -4,7 +4,7 @@
4
4
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
5
5
  import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
6
6
 
7
- import { assertPermission, assertSuperAdmin, assertSystemAdmin, invalidateUser } from "../src/_platform/_shared/service/access";
7
+ import { assertPermission, assertSuperAdmin, assertSystemAdmin, invalidateUser } from "../src/_platform/_shared/service/infra/access";
8
8
  import { installDb } from "./fixture";
9
9
 
10
10
  let dbo: ReturnType<typeof installDb>;
@@ -4,8 +4,8 @@
4
4
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
5
5
  import { afterEach, beforeAll, describe, expect, it } from "vitest";
6
6
 
7
- import { assertPermission } from "../src/_platform/_shared/service/access";
8
- import { createRole, deleteRole, listPermissions, listRoles, updateBindings } from "../src/_platform/_shared/service/roles";
7
+ import { createRole, deleteRole, listPermissions, listRoles, updateBindings } from "../src/_platform/_shared/service/domain/roles";
8
+ import { assertPermission } from "../src/_platform/_shared/service/infra/access";
9
9
  import { installDb } from "./fixture";
10
10
 
11
11
  let dbo: ReturnType<typeof installDb>;
@@ -2,8 +2,8 @@
2
2
  /* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
3
3
  import { afterEach, beforeAll, describe, expect, it } from "vitest";
4
4
 
5
- import { searchOplog } from "../src/_platform/_shared/service/oplog";
6
- import { listLoginMethods, listSettings, updateSetting } from "../src/_platform/_shared/service/settings";
5
+ import { listLoginMethods, listSettings, updateSetting } from "../src/_platform/_shared/service/domain/settings";
6
+ import { searchOplog } from "../src/_platform/_shared/service/infra/oplog";
7
7
  import { installDb } from "./fixture";
8
8
 
9
9
  let dbo: ReturnType<typeof installDb>;