@oj-module/cli 0.2.6 → 0.2.14
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/README.md +1 -1
- package/devkit/README.md +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +28 -23
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +130 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +170 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +9 -3
- package/src/init.ts +24 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +177 -0
- package/src/preview.ts +106 -56
- package/src/sign.ts +21 -6
- package/src/usage.ts +22 -2
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +24 -2
- package/templates/api/src/_platform/README.md +31 -0
- package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
- package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
- package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
- package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +29 -56
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
- package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
- package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
- package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +25 -22
- package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
- package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
- package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
- package/templates/api/src/_platform/_shared/service/infra/oplog.ts +40 -0
- package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
- package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
- package/templates/api/src/_platform/contacts/api.ts +2 -2
- package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
- package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +4 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +26 -0
- package/templates/api/src/_platform/solutions/api.ts +13 -12
- package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
- package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
- package/templates/api/src/auth/_shared/session.ts +26 -22
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +8 -8
- package/templates/api/src/home/line/api.ts +4 -4
- package/templates/api/src/home/pie/api.ts +4 -4
- package/templates/api/src/notification/_shared/notify.ts +97 -57
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- package/templates/api/src/personal-center/upload/api.ts +1 -1
- package/templates/api/src/system/login-methods/api.ts +1 -1
- package/templates/api/src/system/oplog/api.ts +3 -3
- package/templates/api/src/system/permissions/api.ts +2 -2
- package/templates/api/src/system/roles/api.ts +2 -2
- package/templates/api/src/system/roles/bindings/api.ts +2 -2
- package/templates/api/src/system/settings/api.ts +2 -2
- package/templates/api/src/system/users/api.ts +2 -2
- package/templates/api/src/system/users/forbidden/api.ts +2 -2
- package/templates/api/src/system/users/password/api.ts +2 -2
- package/templates/api/src/system/users/roles/api.ts +2 -2
- package/templates/api/src/web/user-info/api.ts +4 -5
- package/templates/api/tests/auth-session.spec.ts +1 -1
- package/templates/api/tests/auth-throttle.spec.ts +1 -1
- package/templates/api/tests/fixture.ts +8 -16
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
- package/templates/api/tests/query-utils.spec.ts +85 -1
- package/templates/api/tests/rbac-audit.spec.ts +1 -1
- package/templates/api/tests/rbac-contacts.spec.ts +2 -2
- package/templates/api/tests/rbac-guard.spec.ts +1 -1
- package/templates/api/tests/rbac-roles.spec.ts +2 -2
- package/templates/api/tests/rbac-settings.spec.ts +66 -21
- package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/api/tests/rbac-users.spec.ts +1 -1
- package/templates/api/tsconfig.json +8 -0
- package/templates/cdn/README.md +48 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +850 -0
- package/templates/init.include.yaml +14 -2
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +580 -23
- package/templates/package.json +2 -1
- package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
- package/templates/web/src/_platform/__client/api.ts +152 -39
- package/templates/web/src/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
- package/templates/web/src/oj-market/api/client.ts +5 -105
- package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
- package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
- package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
- package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
- package/templates/web/src/oj-market/entry.ts +8 -2
- package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
- package/templates/web/src/oj-market/locales/en-US.json +2 -1
- package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
- package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
- package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +13 -10
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
- package/templates/api/src/notifications/api.ts +0 -32
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
|
@@ -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
|
|
|
@@ -68,12 +69,17 @@ function splitId(id: string): [scope: "u" | "b", raw: string] {
|
|
|
68
69
|
function mapItem(r: Row): NoticeItem {
|
|
69
70
|
const scope = String(r.scope);
|
|
70
71
|
const isBroadcast = scope === "broadcast";
|
|
72
|
+
// UNION 结果列名取首分支(userQ)=> 位置 4 名为 is_read。按人项此处即存储整数列;
|
|
73
|
+
// 广播项此处实际承载回执 read_time(非空即已读,与原始 SQL 的 (read_time IS NOT NULL AND <> '') 等价)。
|
|
74
|
+
const isRead = isBroadcast
|
|
75
|
+
? (r.is_read != null && String(r.is_read) !== "")
|
|
76
|
+
: Number(r.is_read) === 1;
|
|
71
77
|
return {
|
|
72
78
|
id: (isBroadcast ? "b:" : "u:") + String(r.id),
|
|
73
79
|
scope: isBroadcast ? "broadcast" : "user",
|
|
74
80
|
avatar: String(r.avatar ?? ""),
|
|
75
81
|
date: String(r.date ?? ""),
|
|
76
|
-
isRead
|
|
82
|
+
isRead,
|
|
77
83
|
message: String(r.message ?? ""),
|
|
78
84
|
title: String(r.title ?? ""),
|
|
79
85
|
link: r.link == null ? undefined : String(r.link),
|
|
@@ -83,58 +89,53 @@ function mapItem(r: Row): NoticeItem {
|
|
|
83
89
|
|
|
84
90
|
/** 给指定用户推一条按人通知,返回命名空间 id "u:<id>" */
|
|
85
91
|
export async function pushNotice(input: PushInput): Promise<string> {
|
|
86
|
-
const ins = await db.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
input.category ?? "notice",
|
|
97
|
-
],
|
|
98
|
-
);
|
|
92
|
+
const ins = await db.table("notifications").insert({
|
|
93
|
+
user_id: Number(input.userId),
|
|
94
|
+
avatar: input.avatar ?? "",
|
|
95
|
+
date: nowStr(),
|
|
96
|
+
is_read: 0,
|
|
97
|
+
message: input.message,
|
|
98
|
+
title: input.title,
|
|
99
|
+
link: input.link ?? null,
|
|
100
|
+
category: input.category ?? "notice",
|
|
101
|
+
}).returning(["id"]).run() as Array<{ id: number }>;
|
|
99
102
|
return `u:${Number(ins[0]!.id)}`;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/** 广播一条通知(全员可见,一行存储),返回命名空间 id "b:<id>" */
|
|
103
106
|
export async function pushBroadcastNotice(input: PushBroadcastInput): Promise<string> {
|
|
104
|
-
const ins = await db.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
input.category ?? "notice",
|
|
114
|
-
nowStr(),
|
|
115
|
-
],
|
|
116
|
-
);
|
|
107
|
+
const ins = await db.table("notifications_broadcast").insert({
|
|
108
|
+
avatar: input.avatar ?? "",
|
|
109
|
+
date: nowStr(),
|
|
110
|
+
message: input.message,
|
|
111
|
+
title: input.title,
|
|
112
|
+
link: input.link ?? null,
|
|
113
|
+
category: input.category ?? "notice",
|
|
114
|
+
create_time: nowStr(),
|
|
115
|
+
}).returning(["id"]).run() as Array<{ id: number }>;
|
|
117
116
|
return `b:${Number(ins[0]!.id)}`;
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
|
-
* 删除通知。按人项校验归属:非本人返回 false
|
|
120
|
+
* 删除通知。按人项校验归属:非本人返回 false;广播项是共享行,物理删除会影响
|
|
121
|
+
* 其他收件人——落「对我隐藏」标记(幂等,返回 true),列表查询过滤 hidden 命中项。
|
|
122
122
|
* 返回是否成功操作到目标(调用方据 false 判 403)。
|
|
123
123
|
*/
|
|
124
124
|
export async function delNotice(id: string, userId: string | number): Promise<boolean> {
|
|
125
125
|
const [scope, raw] = splitId(id);
|
|
126
126
|
const uid = Number(userId);
|
|
127
127
|
if (scope === "b") {
|
|
128
|
-
await db.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
await db.table("notifications_broadcast_hidden")
|
|
129
|
+
.insert({ broadcast_id: Number(raw), user_id: uid, create_time: nowStr() })
|
|
130
|
+
.orIgnore()
|
|
131
|
+
.run();
|
|
132
132
|
return true;
|
|
133
133
|
}
|
|
134
|
-
const n = await db.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const n = await db.table("notifications")
|
|
135
|
+
.where({ field: "id", op: "eq", value: Number(raw) })
|
|
136
|
+
.where({ field: "user_id", op: "eq", value: uid })
|
|
137
|
+
.delete()
|
|
138
|
+
.run() as number;
|
|
138
139
|
return n > 0;
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -146,18 +147,43 @@ export async function noticesFor(
|
|
|
146
147
|
const limit = opts.limit ?? 50;
|
|
147
148
|
const offset = opts.offset ?? 0;
|
|
148
149
|
const uid = Number(userId);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
// 构造器(v0.1.81):UNION ALL 合并「按人」与「广播(LEFT JOIN 回执)」;lit 打固定 scope 标记列,
|
|
151
|
+
// 顶层 orderBy/limit/offset 跨方言一致。
|
|
152
|
+
// 注意:UNION 结果列名取首分支(userQ),故两分支位置对齐即可——广播第 4 列取回执 read_time,
|
|
153
|
+
// 经 union 落到 is_read 列名下,是否「已读」由 mapItem 据非空推导(与原始 SQL 同源)。
|
|
154
|
+
const userQ = db.table("notifications").select([
|
|
155
|
+
"id",
|
|
156
|
+
"avatar",
|
|
157
|
+
"date",
|
|
158
|
+
"is_read",
|
|
159
|
+
"message",
|
|
160
|
+
"title",
|
|
161
|
+
"link",
|
|
162
|
+
"category",
|
|
163
|
+
{ lit: "user", as: "scope" },
|
|
164
|
+
]).where({ field: "user_id", op: "eq", value: uid });
|
|
165
|
+
const broadcastQ = db.table("notifications_broadcast").as("b").join("notifications_broadcast_read", [
|
|
166
|
+
{ left: "b.id", right: "r.broadcast_id" },
|
|
167
|
+
{ left: "r.user_id", op: "eq", value: uid },
|
|
168
|
+
], "left", "r").join("notifications_broadcast_hidden", [
|
|
169
|
+
{ left: "b.id", right: "h.broadcast_id" },
|
|
170
|
+
{ left: "h.user_id", op: "eq", value: uid },
|
|
171
|
+
], "left", "h").where({ field: "h.id", op: "isnull" }).select([
|
|
172
|
+
"b.id",
|
|
173
|
+
"b.avatar",
|
|
174
|
+
"b.date",
|
|
175
|
+
"r.read_time",
|
|
176
|
+
"b.message",
|
|
177
|
+
"b.title",
|
|
178
|
+
"b.link",
|
|
179
|
+
"b.category",
|
|
180
|
+
{ lit: "broadcast", as: "scope" },
|
|
181
|
+
]);
|
|
182
|
+
const rows = await userQ.union(broadcastQ, "all")
|
|
183
|
+
.orderBy([{ field: "date", dir: "desc" }])
|
|
184
|
+
.limit(limit)
|
|
185
|
+
.offset(offset)
|
|
186
|
+
.all();
|
|
161
187
|
return rows.map(mapItem);
|
|
162
188
|
}
|
|
163
189
|
|
|
@@ -170,13 +196,18 @@ export async function markRead(id: string, userId: string | number): Promise<boo
|
|
|
170
196
|
const uid = Number(userId);
|
|
171
197
|
const now = nowStr();
|
|
172
198
|
if (scope === "b") {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
// 构造器(v0.1.81):orIgnore 幂等写入(sqlite/pg → ON CONFLICT DO NOTHING;mysql → ON DUPLICATE KEY UPDATE pk=pk)。
|
|
200
|
+
await db.table("notifications_broadcast_read")
|
|
201
|
+
.insert({ broadcast_id: Number(raw), user_id: uid, read_time: now })
|
|
202
|
+
.orIgnore()
|
|
203
|
+
.run();
|
|
177
204
|
return true;
|
|
178
205
|
}
|
|
179
|
-
const n = await db.
|
|
206
|
+
const n = await db.table("notifications")
|
|
207
|
+
.where({ field: "id", op: "eq", value: Number(raw) })
|
|
208
|
+
.where({ field: "user_id", op: "eq", value: uid })
|
|
209
|
+
.update({ is_read: 1 })
|
|
210
|
+
.run() as number;
|
|
180
211
|
return n > 0;
|
|
181
212
|
}
|
|
182
213
|
|
|
@@ -184,16 +215,25 @@ export async function markRead(id: string, userId: string | number): Promise<boo
|
|
|
184
215
|
export async function markAllRead(userId: string | number): Promise<void> {
|
|
185
216
|
const uid = Number(userId);
|
|
186
217
|
const now = nowStr();
|
|
187
|
-
await db.
|
|
218
|
+
await db.table("notifications").where({ field: "user_id", op: "eq", value: uid }).update({ is_read: 1 }).run();
|
|
219
|
+
// 保留 db.exec:INSERT OR IGNORE ... SELECT 在 sqlite 上构造器无法表达——
|
|
220
|
+
// oj 0.1.81 的 orIgnore 仅跟随 VALUES 的 upsert 子句,不与 fromSelect(SELECT 源) 组合
|
|
221
|
+
// (实测:code 500 "orIgnore with fromSelect is not supported on sqlite")。原 SQL 三方言通用
|
|
222
|
+
// (sqlite OR IGNORE / pg ON CONFLICT DO NOTHING / mysql ON DUPLICATE KEY),退回裸 SQL。
|
|
188
223
|
await db.exec(
|
|
189
224
|
"INSERT OR IGNORE INTO notifications_broadcast_read (broadcast_id, user_id, read_time) SELECT id, ?, ? FROM notifications_broadcast",
|
|
190
225
|
[uid, now],
|
|
191
226
|
);
|
|
192
227
|
}
|
|
193
228
|
|
|
194
|
-
/** 清空:删按人项 +
|
|
229
|
+
/** 清空:删按人项 + 广播逐条落「对我隐藏」标记(共享行不可物理删,他人不受影响) */
|
|
195
230
|
export async function clearNotices(userId: string | number): Promise<void> {
|
|
196
231
|
const uid = Number(userId);
|
|
197
|
-
await db.
|
|
198
|
-
|
|
232
|
+
await db.table("notifications").where({ field: "user_id", op: "eq", value: uid }).delete().run();
|
|
233
|
+
// 保留 db.exec:INSERT OR IGNORE ... SELECT 同 markAllRead(构造器 orIgnore 不与
|
|
234
|
+
// fromSelect 组合,实测 500),原 SQL 三方言通用。
|
|
235
|
+
await db.exec(
|
|
236
|
+
"INSERT OR IGNORE INTO notifications_broadcast_hidden (broadcast_id, user_id, create_time) SELECT id, ?, ? FROM notifications_broadcast",
|
|
237
|
+
[uid, nowStr()],
|
|
238
|
+
);
|
|
199
239
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- 广播隐藏标记表(按人):清空/单条删除广播项时落标记——广播本体是共享行,
|
|
2
|
+
-- 任何人不得物理删除(会影响其他收件人),只能「对我隐藏」。
|
|
3
|
+
-- 清空 = 按人项物理删 + 广播逐条落 hidden 标记;列表查询过滤 hidden 命中项。
|
|
4
|
+
-- 三方言可移植:建表 + 索引均为标准 DDL。
|
|
5
|
+
CREATE TABLE IF NOT EXISTS notifications_broadcast_hidden (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
broadcast_id INTEGER NOT NULL,
|
|
8
|
+
user_id INTEGER NOT NULL,
|
|
9
|
+
create_time TEXT NOT NULL DEFAULT ''
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_broadcast_hidden ON notifications_broadcast_hidden (user_id, broadcast_id);
|
|
@@ -31,3 +31,10 @@ tables:
|
|
|
31
31
|
broadcast_id: {type: integer, null: false}
|
|
32
32
|
user_id: {type: integer, null: false}
|
|
33
33
|
read_time: {type: text, null: false}
|
|
34
|
+
notifications_broadcast_hidden:
|
|
35
|
+
pk: id
|
|
36
|
+
columns:
|
|
37
|
+
id: {type: integer, autoincrement: true}
|
|
38
|
+
broadcast_id: {type: integer, null: false}
|
|
39
|
+
user_id: {type: integer, null: false}
|
|
40
|
+
create_time: {type: text, null: false}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- notification 模块种子(按人真实通知)。user_id=1(admin),link/category 幂等可空/默认。
|
|
2
|
+
-- avatar 给真实 URL:铃铛按 avatar 渲染头像,空串会破图(空串仅作为运行时兜底,不作为种子值)。
|
|
2
3
|
-- 按分号朴素切分(S006):语句内不得含分号字面量。
|
|
3
|
-
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (1, 1, '', '2026-09-01 09:12', 0, '脚手架已就绪,用 admin / 123456 登录开始', '欢迎', NULL, 'notice');
|
|
4
|
-
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (2, 1, '', '2026-09-02 14:30', 0, '通知铃支持单条已读、全部已读与清空;广播经 push-broadcast 入 b: 命名空间', '通知铃', NULL, 'notice');
|
|
5
|
-
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (3, 1, '', '2026-09-03 18:45', 1, '契约改动后重跑 ojm api 生成前端 client 与文档', '契约', NULL, 'notice');
|
|
6
|
-
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (4, 1, '', '2026-09-04 08:00', 0, '编辑 api/src 下的 api.ts 保存即热更;handler 组支持四层嵌套(oj v0.1.64)', '开发提示', NULL, 'notice');
|
|
4
|
+
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (1, 1, 'https://avatar.vercel.sh/seed-welcome.svg?text=W', '2026-09-01 09:12', 0, '脚手架已就绪,用 admin / 123456 登录开始', '欢迎', NULL, 'notice');
|
|
5
|
+
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (2, 1, 'https://avatar.vercel.sh/seed-bell.svg?text=B', '2026-09-02 14:30', 0, '通知铃支持单条已读、全部已读与清空;广播经 push-broadcast 入 b: 命名空间', '通知铃', NULL, 'notice');
|
|
6
|
+
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (3, 1, 'https://avatar.vercel.sh/seed-contract.svg?text=C', '2026-09-03 18:45', 1, '契约改动后重跑 ojm api 生成前端 client 与文档', '契约', NULL, 'notice');
|
|
7
|
+
INSERT OR IGNORE INTO notifications (id, user_id, avatar, date, is_read, message, title, link, category) VALUES (4, 1, 'https://avatar.vercel.sh/seed-dev.svg?text=D', '2026-09-04 08:00', 0, '编辑 api/src 下的 api.ts 保存即热更;handler 组支持四层嵌套(oj v0.1.64)', '开发提示', NULL, 'notice');
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// POST /api/personal-center/profile —— 保存「我的资料」(个人中心表单入库端点)。
|
|
2
|
+
// 只允许改本人行(http.user.id 权威,不接受 body 传 id);username 全局唯一,
|
|
3
|
+
// 重名 400;avatar 走 upload 端点(multipart),不在此通道。
|
|
4
|
+
const handler = {
|
|
5
|
+
async post() {
|
|
6
|
+
const uid = http.user?.id;
|
|
7
|
+
if (!uid) {
|
|
8
|
+
json.fail(401, "unauthorized");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const b = (http.body ?? {}) as {
|
|
12
|
+
username?: unknown
|
|
13
|
+
email?: unknown
|
|
14
|
+
phoneNumber?: unknown
|
|
15
|
+
description?: unknown
|
|
16
|
+
};
|
|
17
|
+
const patch: Record<string, string> = {};
|
|
18
|
+
if (b.username != null && String(b.username).trim() !== "") {
|
|
19
|
+
const username = String(b.username).trim();
|
|
20
|
+
const dup = await db.table("users").select(["id"]).where({ field: "username", op: "eq", value: username }).all();
|
|
21
|
+
if (dup.some(r => Number(r.id) !== Number(uid))) {
|
|
22
|
+
json.fail(400, "username already taken");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
patch.username = username;
|
|
26
|
+
}
|
|
27
|
+
if (b.email != null)
|
|
28
|
+
patch.email = String(b.email);
|
|
29
|
+
if (b.phoneNumber != null)
|
|
30
|
+
patch.phone_number = String(b.phoneNumber);
|
|
31
|
+
if (b.description != null)
|
|
32
|
+
patch.description = String(b.description);
|
|
33
|
+
if (Object.keys(patch).length === 0) {
|
|
34
|
+
json.fail(400, "nothing to update");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await db.table("users").update(patch).where({ field: "id", op: "eq", value: Number(uid) }).run();
|
|
39
|
+
json.ok(null);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
json.fail(500, String(e));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 入参:四字段均可选(部分保存允许),类型即字符串。
|
|
48
|
+
handler.post.schema = {
|
|
49
|
+
body: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
username: { type: "string" },
|
|
53
|
+
email: { type: "string" },
|
|
54
|
+
phoneNumber: { type: "string" },
|
|
55
|
+
description: { type: "string" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 出参:无业务载荷(前端保存后经 user-info 重拉刷新 store)。
|
|
61
|
+
handler.post.response = { type: "null" };
|
|
62
|
+
|
|
63
|
+
export default handler;
|
|
@@ -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.
|
|
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,22 +10,21 @@ export default {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
try {
|
|
13
|
-
const rows = await db.
|
|
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", "phone_number", "description", "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.
|
|
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 ?? ""),
|
|
26
23
|
username: String(row.username),
|
|
27
24
|
displayName: row.display_name == null ? "" : String(row.display_name),
|
|
28
25
|
email: String(row.email ?? ""),
|
|
26
|
+
phoneNumber: row.phone_number == null ? "" : String(row.phone_number),
|
|
27
|
+
description: row.description == null ? "" : String(row.description),
|
|
29
28
|
roles: roleRows.map(r => r.role_name),
|
|
30
29
|
mustChange: Number(row.must_change) === 1,
|
|
31
30
|
permissions: [],
|
|
@@ -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>;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// 后端 BDD 夹具:node:sqlite 内存库 + **真实迁移文件**建表,装到 globalThis.db——
|
|
2
2
|
// 与 oj 运行时注入的全局同名同面(query/exec/tx/dialect),service 代码不改一行即可被测。
|
|
3
3
|
// 夹具直接读 api/src/<模块>/migrations/*.sql,与迁移不可能漂移(沿用 oj-wiki 先例)。
|
|
4
|
+
//
|
|
5
|
+
// **构造器边界(架构定论,勿在此夹具里补 table())**:db.table()/listTables() 等构造器是
|
|
6
|
+
// oj 二进制内的纯 Rust 实现,无 JS 模块可导出,Node 侧垫不出来——本夹具刻意只给
|
|
7
|
+
// {query, exec, tx, dialect}。两层测试的分工因此定型:
|
|
8
|
+
// · L2(本目录 *.spec.ts):构造器面用**记录型假 builder**(记 where/orderBy/limit 调用
|
|
9
|
+
// 序列做编排断言);真 SQL 执行断言走 db.query 直查。
|
|
10
|
+
// · L1(同目录 *.test.ts,`oj test` 收集):真 v8 + 真构造器闭环,守 SQL 渲染/过滤/转义
|
|
11
|
+
// 的真执行面(样板见 tests/l1-builder.test.ts;方法论见 devkit oj-test-guide.md)。
|
|
4
12
|
import { createRequire } from "node:module";
|
|
5
13
|
|
|
6
14
|
const req = createRequire(import.meta.url);
|
|
@@ -13,22 +21,6 @@ const SRC = join(dirname(fileURLToPath(import.meta.url)), "..", "src");
|
|
|
13
21
|
|
|
14
22
|
export const ADMIN = 1;
|
|
15
23
|
|
|
16
|
-
/**
|
|
17
|
-
* market_history 同构 DDL:表真源 = 市场方案包自带迁移(oj-market 0002__market_history),
|
|
18
|
-
* 不属平台迁移(installDb 不回放)——需要断言留痕的用例显式执行;守卫语义(表不存在静默跳过)
|
|
19
|
-
* 另有专项用例钉住。
|
|
20
|
-
*/
|
|
21
|
-
export const MARKET_HISTORY_DDL = `CREATE TABLE market_history (
|
|
22
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
-
solution TEXT NOT NULL,
|
|
24
|
-
version TEXT NOT NULL,
|
|
25
|
-
action TEXT NOT NULL,
|
|
26
|
-
status TEXT NOT NULL,
|
|
27
|
-
operator TEXT,
|
|
28
|
-
message TEXT,
|
|
29
|
-
created_at INTEGER NOT NULL
|
|
30
|
-
)`;
|
|
31
|
-
|
|
32
24
|
/** 建内存库 + 注入全局;返回 DatabaseSync 供用例直插数据/断言。 */
|
|
33
25
|
export function installDb(): any {
|
|
34
26
|
const dbo = new DatabaseSync(":memory:");
|
|
@@ -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 });
|