@oj-module/cli 0.2.1 → 0.2.5
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/devkit/README.md +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +147 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +46 -7
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +56 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +162 -0
- package/templates/web/src/system/__client/api.ts +382 -0
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// 第一方 bootstrap key(P2-12,G3 双轨第一轨):模板内置 oj-market 首包发布方公钥——
|
|
2
|
+
// market 首装免 TOFU(信任库为空时自动播种,直接过验签)。发布流程(devkit「签名与信任」章节,
|
|
3
|
+
// P2-14 引用):发布人 `ojm keygen` 生成 Ed25519 对(私钥永不出 ~/.ojm/keys/<id>)→ `ojm sign`
|
|
4
|
+
// 签 oj-market 首发包 → 公钥(64 hex)+ key_id 回填本文件随模板发布。**模板内只有公钥**——
|
|
5
|
+
// 私钥入仓即信任根基烧毁(tests/lifecycle/bootstrap.spec 钉全仓 grep 私钥 pattern 零命中)。
|
|
6
|
+
// 播种纪律:只在信任库文件尚不存在时播种一次——不与运维吊销对抗(remove 后不复活,G5 语义)。
|
|
7
|
+
|
|
8
|
+
import { fsFace } from "../_lifecycle/atomic";
|
|
9
|
+
import { createTrustStore } from "../_lifecycle/trust";
|
|
10
|
+
|
|
11
|
+
export interface BootstrapKey {
|
|
12
|
+
key_id: string
|
|
13
|
+
/** 32B raw Ed25519 公钥 hex(C2-35 口径) */
|
|
14
|
+
public_key: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 第一方发布方 key(oj-market 首包)。发布时回填——回填前为 null(播种即 no-op,
|
|
19
|
+
* 首装走 TOFU 双轨第二轨:needs_trust → ojm trust confirm)。
|
|
20
|
+
*/
|
|
21
|
+
export const BOOTSTRAP_KEY: BootstrapKey | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 引导播种(installBody 接线,runInstall 之前):信任库文件尚不存在且 bootstrap key 在场 →
|
|
25
|
+
* 播种并审计(operator="bootstrap" 标记机器播种)。key 为 null(未回填)或库已初始化 → no-op。
|
|
26
|
+
*/
|
|
27
|
+
export async function seedBootstrapKey(trustPath: string, key: BootstrapKey | null): Promise<void> {
|
|
28
|
+
if (key === null) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await fsFace().readFile(trustPath);
|
|
33
|
+
return; // 库文件已存在(含空库)= 已初始化过 → 不播种(防吊销复活)
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// 不存在 → 首次引导
|
|
37
|
+
}
|
|
38
|
+
await createTrustStore(trustPath).add(key.key_id, key.public_key, { operator: "bootstrap" });
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- 方案注册路由表重构 mysql 方言覆盖(sqlite 版见 0010__sys_solution_route.sql)。
|
|
2
|
+
-- 差异:保留字反引号;整型列取 BIGINT;主键 TEXT → VARCHAR(255);
|
|
3
|
+
-- ALTER ADD COLUMN / CREATE INDEX 无 IF NOT EXISTS(迁移账本一次应用语义兜底,同 0008)。
|
|
4
|
+
ALTER TABLE sys_setting ADD COLUMN deprecated BIGINT NOT NULL DEFAULT 0;
|
|
5
|
+
|
|
6
|
+
DROP TABLE IF EXISTS sys_solution_route;
|
|
7
|
+
CREATE TABLE sys_solution_route (
|
|
8
|
+
path VARCHAR(255) PRIMARY KEY,
|
|
9
|
+
module TEXT NOT NULL,
|
|
10
|
+
category TEXT NOT NULL,
|
|
11
|
+
`order` BIGINT NOT NULL DEFAULT 0,
|
|
12
|
+
title_key TEXT,
|
|
13
|
+
required_permission TEXT,
|
|
14
|
+
deprecated BIGINT NOT NULL DEFAULT 0,
|
|
15
|
+
enabled BIGINT NOT NULL DEFAULT 1
|
|
16
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
17
|
+
CREATE INDEX idx_sys_solution_route_module ON sys_solution_route (module);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- 方案注册路由表重构 pg 方言覆盖(sqlite 版见 0010__sys_solution_route.sql)。
|
|
2
|
+
-- 差异:无(ADD COLUMN / DROP TABLE / 双引号保留字 / CREATE INDEX IF NOT EXISTS pg 原生支持)。
|
|
3
|
+
ALTER TABLE sys_setting ADD COLUMN deprecated INTEGER NOT NULL DEFAULT 0;
|
|
4
|
+
|
|
5
|
+
DROP TABLE IF EXISTS sys_solution_route;
|
|
6
|
+
CREATE TABLE sys_solution_route (
|
|
7
|
+
path TEXT PRIMARY KEY,
|
|
8
|
+
module TEXT NOT NULL,
|
|
9
|
+
category TEXT NOT NULL,
|
|
10
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
title_key TEXT,
|
|
12
|
+
required_permission TEXT,
|
|
13
|
+
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
enabled INTEGER NOT NULL DEFAULT 1
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_sys_solution_route_module ON sys_solution_route (module);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- 方案注册路由表重构(P1-8,D10 注册唯一出口的落点):0004 版以 (name, version, path) 为主键、
|
|
2
|
+
-- 无 module/required_permission/deprecated 列,与「站点级路由注册(path 唯一 + 声明消失标
|
|
3
|
+
-- deprecated,镜像 AC2-3/4)」的语义不兼容;旧表从未被运行时读写(仅存在性断言),DROP 重建零数据损失。
|
|
4
|
+
-- 同时为 sys_setting 补 deprecated 列:设置定义行声明消失同样保留标 deprecated(P1-8 register 语义)。
|
|
5
|
+
ALTER TABLE sys_setting ADD COLUMN deprecated INTEGER NOT NULL DEFAULT 0;
|
|
6
|
+
|
|
7
|
+
DROP TABLE IF EXISTS sys_solution_route;
|
|
8
|
+
CREATE TABLE sys_solution_route (
|
|
9
|
+
path TEXT PRIMARY KEY,
|
|
10
|
+
module TEXT NOT NULL,
|
|
11
|
+
category TEXT NOT NULL,
|
|
12
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
title_key TEXT,
|
|
14
|
+
required_permission TEXT,
|
|
15
|
+
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
enabled INTEGER NOT NULL DEFAULT 1
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_sys_solution_route_module ON sys_solution_route (module);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- market_history mysql 方言覆盖(方言覆盖必胜,通用兜底;oj-bin docs/migration.md §3.1)。
|
|
2
|
+
-- 差异:AUTOINCREMENT → BIGINT AUTO_INCREMENT;TEXT 列按业务长度收窄为 VARCHAR;
|
|
3
|
+
-- created_at 用 BIGINT(秒级 epoch)。
|
|
4
|
+
CREATE TABLE IF NOT EXISTS market_history (
|
|
5
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
6
|
+
solution VARCHAR(255) NOT NULL,
|
|
7
|
+
version VARCHAR(255) NOT NULL,
|
|
8
|
+
action VARCHAR(64) NOT NULL,
|
|
9
|
+
status VARCHAR(64) NOT NULL,
|
|
10
|
+
operator VARCHAR(255),
|
|
11
|
+
message TEXT,
|
|
12
|
+
created_at BIGINT NOT NULL
|
|
13
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- market_history pg 方言覆盖(方言覆盖必胜,通用兜底;oj-bin docs/migration.md §3.1)。
|
|
2
|
+
-- 差异:AUTOINCREMENT → BIGSERIAL;TEXT 在 pg 合法,保留 TEXT。
|
|
3
|
+
CREATE TABLE IF NOT EXISTS market_history (
|
|
4
|
+
id BIGSERIAL PRIMARY KEY,
|
|
5
|
+
solution TEXT NOT NULL,
|
|
6
|
+
version TEXT NOT NULL,
|
|
7
|
+
action TEXT NOT NULL,
|
|
8
|
+
status TEXT NOT NULL,
|
|
9
|
+
operator TEXT,
|
|
10
|
+
message TEXT,
|
|
11
|
+
created_at BIGINT NOT NULL
|
|
12
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- market_history 表上收 _platform(P4 裁决:市场操作历史为平台级审计面,归属平台核心,
|
|
2
|
+
-- 不再由 oj-market 方案包自带迁移创建)。install/enable/disable/uninstall/upgrade/rollback
|
|
3
|
+
-- 六类动作留痕;前端历史抽屉与版本历史对比只读消费。
|
|
4
|
+
-- 结构只进 migrations(S006);与 schema.yaml 列集一致。
|
|
5
|
+
CREATE TABLE IF NOT EXISTS market_history (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
solution TEXT NOT NULL,
|
|
8
|
+
version TEXT NOT NULL,
|
|
9
|
+
action TEXT NOT NULL,
|
|
10
|
+
status TEXT NOT NULL,
|
|
11
|
+
operator TEXT,
|
|
12
|
+
message TEXT,
|
|
13
|
+
created_at INTEGER NOT NULL
|
|
14
|
+
);
|
|
@@ -24,8 +24,10 @@ tables:
|
|
|
24
24
|
value: {type: text}
|
|
25
25
|
default_value: {type: text}
|
|
26
26
|
label_key: {type: text}
|
|
27
|
+
group_label_key: {type: text}
|
|
27
28
|
options: {type: text}
|
|
28
29
|
secret: {type: integer, null: false}
|
|
30
|
+
deprecated: {type: integer, null: false}
|
|
29
31
|
update_time: {type: integer, null: false}
|
|
30
32
|
sys_oplog:
|
|
31
33
|
pk: id
|
|
@@ -41,14 +43,16 @@ tables:
|
|
|
41
43
|
detail: {type: text}
|
|
42
44
|
level: {type: integer, null: false}
|
|
43
45
|
sys_solution_route:
|
|
44
|
-
|
|
46
|
+
# 0010 重构后形状(站点级路由,path 主键;与 migrations/0010__sys_solution_route.sql 逐列一致)。
|
|
47
|
+
pk: path
|
|
45
48
|
columns:
|
|
46
|
-
name: {type: text, null: false}
|
|
47
|
-
version: {type: text, null: false}
|
|
48
|
-
category: {type: text, null: false}
|
|
49
49
|
path: {type: text, null: false}
|
|
50
|
+
module: {type: text, null: false}
|
|
51
|
+
category: {type: text, null: false}
|
|
50
52
|
order: {type: integer, null: false}
|
|
51
53
|
title_key: {type: text}
|
|
54
|
+
required_permission: {type: text}
|
|
55
|
+
deprecated: {type: integer, null: false}
|
|
52
56
|
enabled: {type: integer, null: false}
|
|
53
57
|
sys_role:
|
|
54
58
|
pk: name
|
|
@@ -84,3 +88,16 @@ tables:
|
|
|
84
88
|
expires_at: {type: integer, null: false}
|
|
85
89
|
revoked: {type: integer, null: false}
|
|
86
90
|
created_at: {type: integer, null: false}
|
|
91
|
+
# 市场操作历史(P4 上收 _platform):install/enable/disable/uninstall/upgrade/rollback 六类动作留痕。
|
|
92
|
+
# 写入由平台编排面(_lifecycle/history.ts recordHistory)落到本表;市场页只读消费。
|
|
93
|
+
market_history:
|
|
94
|
+
pk: id
|
|
95
|
+
columns:
|
|
96
|
+
id: {type: integer, autoincrement: true}
|
|
97
|
+
solution: {type: text, null: false}
|
|
98
|
+
version: {type: text, null: false}
|
|
99
|
+
action: {type: text, null: false}
|
|
100
|
+
status: {type: text, null: false}
|
|
101
|
+
operator: {type: text}
|
|
102
|
+
message: {type: text}
|
|
103
|
+
created_at: {type: integer, null: false}
|
|
@@ -11,9 +11,9 @@ INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('editor',
|
|
|
11
11
|
INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1);
|
|
12
12
|
-- seed 超管绑定(sys_user_role 唯一权威源,D10)
|
|
13
13
|
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin';
|
|
14
|
-
-- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;
|
|
14
|
+
-- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;framework.market_cdn_url 安全组仅 superadmin 可改)
|
|
15
15
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
|
|
16
|
-
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('
|
|
16
|
+
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
|
|
17
17
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
|
|
18
18
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
|
|
19
19
|
INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_base_dn', 'ldap', 'ldap', 'string', '', '', 'system:setting.ldap_base_dn', 0, 0);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { ModuleEntryIntegrity, SolutionRouteRow, SolutionsEnv, VerifyItem } from "../_lifecycle/verify-checks";
|
|
2
|
+
import type { VerifyReport } from "../_lifecycle/verify-report";
|
|
3
|
+
import type { FetchInitFace } from "../_shared/site-facade";
|
|
4
|
+
// 平台方案路由读端点 /api/_platform/solutions/routes(P1-14,R21 前端归并数据源)+ 深检端点
|
|
5
|
+
// /api/_platform/solutions/verify?name=(P2-7,C2-42;admin 域、全程不触 exec)。
|
|
6
|
+
// 登录即可、只读、无权限点(contacts/api.ts 同纪律);读操作不记审计。
|
|
7
|
+
// routes:只下发未 deprecated 行全列(required_permission 仅供深检对比,前端不做权限级菜单过滤,R22)。
|
|
8
|
+
// verify:四项检查(路由可达/RBAC/设置/迁移账本)编排 + 可信性项(P2-8 trust_state)+ i18n 依赖项;
|
|
9
|
+
// 返回 VerifyReport{ok,checks,advise} 并落盘 verify-reports(时间序 N=10 留存)。
|
|
10
|
+
// 行序稳定:module → order → path。
|
|
11
|
+
import { fsFace } from "../_lifecycle/atomic";
|
|
12
|
+
import { sha384B64 } from "../_lifecycle/sha384";
|
|
13
|
+
import { createStateStore } from "../_lifecycle/state";
|
|
14
|
+
import {
|
|
15
|
+
declaredMigrationsOf,
|
|
16
|
+
declaredSettingsOf,
|
|
17
|
+
diffLedger,
|
|
18
|
+
diffPermRows,
|
|
19
|
+
diffSettingRows,
|
|
20
|
+
diffWebIntegrity,
|
|
21
|
+
readSolutionManifest,
|
|
22
|
+
|
|
23
|
+
verifyRouteReachability,
|
|
24
|
+
} from "../_lifecycle/verify-checks";
|
|
25
|
+
import { getLastReport, persistReport, verifyReportsDirOf } from "../_lifecycle/verify-report";
|
|
26
|
+
import { respond } from "../_shared/respond";
|
|
27
|
+
import { assertSystemAdmin } from "../_shared/service/access";
|
|
28
|
+
import { journalHeadOf, siteOrigin } from "../_shared/site-facade";
|
|
29
|
+
|
|
30
|
+
/** 线格式行(与 packages/runtime/src/api/rbac/types.ts 的 SolutionRouteRow 对齐) */
|
|
31
|
+
export type { SolutionRouteRow } from "../_lifecycle/verify-checks";
|
|
32
|
+
|
|
33
|
+
/** oj serve release 布局生产缺省 */
|
|
34
|
+
export function solutionsEnv(): SolutionsEnv {
|
|
35
|
+
return {
|
|
36
|
+
sharedRoot: "dist",
|
|
37
|
+
statePath: "modules/.solutions/state.yaml",
|
|
38
|
+
modulesJsonPath: "dist/modules.json",
|
|
39
|
+
apiPrefix: "/api",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fetchOf(env: SolutionsEnv): FetchInitFace {
|
|
44
|
+
return env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 路由行映射(sys_solution_route → SolutionRouteRow) */
|
|
48
|
+
function rowOf(r: Record<string, unknown>): SolutionRouteRow {
|
|
49
|
+
return {
|
|
50
|
+
path: String(r.path),
|
|
51
|
+
module: String(r.module),
|
|
52
|
+
category: r.category === "admin" || r.category === "hidden" ? r.category : "main",
|
|
53
|
+
order: Number(r.order),
|
|
54
|
+
title_key: r.title_key == null ? null : String(r.title_key),
|
|
55
|
+
required_permission: r.required_permission == null ? null : String(r.required_permission),
|
|
56
|
+
enabled: Number(r.enabled) === 1,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 取并校验深检 name 参数(空 → 400;端点门之一) */
|
|
61
|
+
function verifyNameOf(): string {
|
|
62
|
+
const name = String((globalThis as { http?: { query?: Record<string, unknown> } }).http?.query?.name ?? "").trim();
|
|
63
|
+
if (name === "")
|
|
64
|
+
throw Object.assign(new Error("name is required"), { status: 400 });
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 可信性项(P2-8):发布方 key 状态(G5 吊销后已装方案继续运行却无人知晓的补位)。
|
|
70
|
+
* trusted → 干净不打项;untrusted → error(key 已吊销仍在跑);absent/未写(P1 遗留)→ info(无法校验来源,非阻断)。
|
|
71
|
+
*/
|
|
72
|
+
async function trustItemOf(env: SolutionsEnv, name: string): Promise<VerifyItem | null> {
|
|
73
|
+
const state = await createStateStore(env.statePath);
|
|
74
|
+
const ts = state.read().solutions[name]?.trust_state;
|
|
75
|
+
if (ts === "trusted")
|
|
76
|
+
return null;
|
|
77
|
+
if (ts === "untrusted")
|
|
78
|
+
return { id: "trust", severity: "error", subject: name, advise_key: "lifecycle.verify.trust.untrusted" };
|
|
79
|
+
return { id: "trust", severity: "info", subject: name, advise_key: "lifecycle.verify.trust.absent" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 深检主体:四项检查编排 + 可信性项 + i18n 依赖项,组装 VerifyReport{ok,checks,advise}(persist 归端点) */
|
|
83
|
+
async function verifyBody(env: SolutionsEnv, name: string): Promise<VerifyReport> {
|
|
84
|
+
// 已装校验:sys_solution_route 有该 module 行(未装 → 404,端点门之一)
|
|
85
|
+
const routeRows = await db.query(
|
|
86
|
+
`select path, module, category, "order", title_key, required_permission, enabled
|
|
87
|
+
from sys_solution_route where deprecated = 0 and module = ?`,
|
|
88
|
+
[name],
|
|
89
|
+
) as Array<Record<string, unknown>>;
|
|
90
|
+
if (routeRows.length === 0)
|
|
91
|
+
throw Object.assign(new Error(`方案「${name}」未安装或无路由注册`), { status: 404 });
|
|
92
|
+
|
|
93
|
+
const routes = routeRows.map(rowOf);
|
|
94
|
+
|
|
95
|
+
// ① 路由可达性(I/O 探针;siteOrigin 复用 = SSRF 钉② header 清洗,probeUrlOf 内建钉①)
|
|
96
|
+
const reach = await verifyRouteReachability(fetchOf(env), routes, siteOrigin());
|
|
97
|
+
|
|
98
|
+
// ② RBAC 完整性:声明权限点 ↔ sys_permission 实有
|
|
99
|
+
const declaredPerms = [...new Set(routes.map(r => r.required_permission).filter((p): p is string => p != null))];
|
|
100
|
+
const permRows = await db.query("select code from sys_permission") as Array<{ code: string }>;
|
|
101
|
+
const rbac = diffPermRows(declaredPerms, new Set(permRows.map(r => r.code)));
|
|
102
|
+
|
|
103
|
+
// ③ 设置完整性:manifest 声明设置 key ↔ sys_setting 实有(永不含值)
|
|
104
|
+
const manifest = await readSolutionManifest(env, name);
|
|
105
|
+
const declaredSettings = manifest ? declaredSettingsOf(manifest.manifest) : [];
|
|
106
|
+
const settingRows = await db.query("select key from sys_setting where module = ? and deprecated = 0", [name]) as Array<{ key: string }>;
|
|
107
|
+
const settings = diffSettingRows(declaredSettings, new Set(settingRows.map(r => r.key)));
|
|
108
|
+
|
|
109
|
+
// ④ 迁移账本:manifest 迁移目录最高版本 ↔ _oj_migrations 实有(journalHeadOf 复用,DRY)
|
|
110
|
+
const declaredMig = manifest
|
|
111
|
+
? declaredMigrationsOf(await listMigrationFiles(env, name, manifest.version), name)
|
|
112
|
+
: [];
|
|
113
|
+
const applied = new Map<string, number | null>();
|
|
114
|
+
applied.set(name, Number(await journalHeadOf(name)));
|
|
115
|
+
const ledger = diffLedger(declaredMig, applied);
|
|
116
|
+
|
|
117
|
+
// ⑤ web 入口 integrity(P4-2,P3-10#2 收口):modules.json 条目 integrity ↔ 本地 dist 资产重算
|
|
118
|
+
// sha384 比对;SSRF 钉:只比对本地资产,绝不经 fetch 出站(与路由可达性探针严格隔离)。
|
|
119
|
+
let webChecks: VerifyItem[] = [];
|
|
120
|
+
try {
|
|
121
|
+
const mjPath = env.modulesJsonPath;
|
|
122
|
+
if (typeof mjPath === "string") {
|
|
123
|
+
const slash = mjPath.lastIndexOf("/");
|
|
124
|
+
const distRoot = slash === -1 ? "." : mjPath.slice(0, slash);
|
|
125
|
+
const list = JSON.parse(new TextDecoder().decode(await fsFace().readFile(mjPath))) as unknown[];
|
|
126
|
+
if (Array.isArray(list))
|
|
127
|
+
webChecks = await diffWebIntegrity(list as ModuleEntryIntegrity[], distRoot, sha384B64);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// modules.json 缺失/不可读 → 不打 web 项(非阻断,避免深检自身因装配清单缺失而误报)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const checks: VerifyItem[] = [...reach, ...rbac, ...settings, ...ledger, ...webChecks];
|
|
135
|
+
const trust = await trustItemOf(env, name);
|
|
136
|
+
if (trust !== null)
|
|
137
|
+
checks.push(trust);
|
|
138
|
+
|
|
139
|
+
// i18n 依赖项(info,非阻断):本仓只下发待查 key 清单(advise),前端经 oj-market 解析文案;
|
|
140
|
+
// 缺执行降级「未校验」不阻断。先据既有检查项判定是否需要 i18n 解析,再纳入 advise 清单。
|
|
141
|
+
const baseAdvise = [...new Set(checks.map(c => c.advise_key))].sort();
|
|
142
|
+
if (baseAdvise.length > 0)
|
|
143
|
+
checks.push({ id: "i18n", severity: "info", subject: String(baseAdvise.length), advise_key: "lifecycle.verify.i18n.dependency" });
|
|
144
|
+
|
|
145
|
+
const advise = [...new Set(checks.map(c => c.advise_key))].sort();
|
|
146
|
+
const ok = !checks.some(c => c.severity === "error");
|
|
147
|
+
return { ok, checks, advise };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 版本目录内迁移文件名清单(读不到目录 → 空,意为无迁移声明) */
|
|
151
|
+
async function listMigrationFiles(env: SolutionsEnv, name: string, version: string): Promise<string[]> {
|
|
152
|
+
try {
|
|
153
|
+
return await fsFace().readdir(`${env.sharedRoot}/${name}-${version}/migrations`);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createSolutionsApi(env: SolutionsEnv) {
|
|
161
|
+
return {
|
|
162
|
+
// GET /api/_platform/solutions/routes —— 前端归并数据源(登录即可)。
|
|
163
|
+
routes: {
|
|
164
|
+
async get() {
|
|
165
|
+
if (!http.user?.id) {
|
|
166
|
+
json.fail(401, "unauthorized");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await respond(async () => {
|
|
170
|
+
const rows = await db.query(
|
|
171
|
+
`select path, module, category, "order", title_key, required_permission, enabled
|
|
172
|
+
from sys_solution_route where deprecated = 0 order by module, "order", path`,
|
|
173
|
+
) as Array<Record<string, unknown>>;
|
|
174
|
+
return rows.map(rowOf);
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
// POST /api/_platform/solutions/verify?name= —— 深检(admin 域;不触 exec)。
|
|
179
|
+
verify: {
|
|
180
|
+
async post() {
|
|
181
|
+
await respond(async () => {
|
|
182
|
+
if (!http.user?.id)
|
|
183
|
+
throw Object.assign(new Error("unauthorized"), { status: 401 });
|
|
184
|
+
await assertSystemAdmin();
|
|
185
|
+
const name = verifyNameOf();
|
|
186
|
+
const report = await verifyBody(env, name);
|
|
187
|
+
// 落盘 verify-reports(时间序 N=10 留存;persist 失败不应阻断响应——但异常须上抛由 respond 收口)
|
|
188
|
+
await persistReport(verifyReportsDirOf(env), name, report);
|
|
189
|
+
return report;
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
// GET /api/_platform/solutions/verify/last?name= —— 取该方案最新深检报告(登录即可,只读)。
|
|
193
|
+
last: {
|
|
194
|
+
async get() {
|
|
195
|
+
if (!http.user?.id) {
|
|
196
|
+
json.fail(401, "unauthorized");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await respond(async () => {
|
|
200
|
+
const name = verifyNameOf();
|
|
201
|
+
const report = await getLastReport(verifyReportsDirOf(env), name);
|
|
202
|
+
if (report === null)
|
|
203
|
+
throw Object.assign(new Error(`方案「${name}」暂无深检报告`), { status: 404 });
|
|
204
|
+
return report;
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default createSolutionsApi(solutionsEnv());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- demo 模块建表(P3-2)。todos 待办表,S007 要求序号连续且唯一,单文件 0001。
|
|
2
|
+
-- done 用 INTEGER(0/1) 承载布尔,handler 映射为 boolean。
|
|
3
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
title TEXT NOT NULL DEFAULT '',
|
|
6
|
+
done INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
create_time INTEGER NOT NULL DEFAULT 0,
|
|
8
|
+
update_time INTEGER NOT NULL DEFAULT 0
|
|
9
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 声明式表结构(§4.2 / D1-C):demo 模块自有表 todos。
|
|
2
|
+
# 加表/加可空列由 reconcile 自动收敛;类型变更/改名走 migrations/。
|
|
3
|
+
tables:
|
|
4
|
+
todos:
|
|
5
|
+
pk: id
|
|
6
|
+
columns:
|
|
7
|
+
id: {type: integer, autoincrement: true}
|
|
8
|
+
title: {type: text, null: false}
|
|
9
|
+
done: {type: integer, null: false}
|
|
10
|
+
create_time: {type: integer, null: false}
|
|
11
|
+
update_time: {type: integer, null: false}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- demo 模块种子(P3-2):示例待办,幂等 INSERT OR IGNORE,按分号朴素切分(S006)。
|
|
2
|
+
INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (1, '学习 uni-dev 契约机制', 1, 0, 0);
|
|
3
|
+
INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (2, '完成 playground-oj system 模块', 0, 0, 0);
|
|
4
|
+
INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (3, '编写 P3 教学文档', 0, 0, 0);
|
|
5
|
+
INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (4, '联调 demo 真实数据层', 0, 0, 0);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// GET /api/demo/todos —— 演示待办列表(真实化 P3-2,AC-D9 apiPrefix=/demo)。
|
|
2
|
+
// Bearer 守卫保护;keyword 模糊过滤 title;snake_case → camelCase,done 映射布尔。
|
|
3
|
+
|
|
4
|
+
// 契约(v0.1.71「最新方式」):动词挂 `.schema` 四通道(params/query/body/response),
|
|
5
|
+
// oj 运行期据此校验入参、并供 `oj openapi` 生成 openapi.json → 前端 `ojm api` 生成 __client。
|
|
6
|
+
const todos = {
|
|
7
|
+
async get() {
|
|
8
|
+
const kw = http.query.keyword;
|
|
9
|
+
const where = kw ? "WHERE title LIKE ?" : "";
|
|
10
|
+
const params = kw ? [`%${kw}%`] : [];
|
|
11
|
+
try {
|
|
12
|
+
const totalRows = await db.query(`SELECT COUNT(*) AS c FROM todos ${where}`, params);
|
|
13
|
+
const total = Number(totalRows[0]?.c ?? 0);
|
|
14
|
+
const rows = await db.query(
|
|
15
|
+
`SELECT id, title, done, create_time, update_time FROM todos ${where} ORDER BY id ASC`,
|
|
16
|
+
params,
|
|
17
|
+
);
|
|
18
|
+
const list = rows.map(r => ({
|
|
19
|
+
id: Number(r.id),
|
|
20
|
+
title: String(r.title),
|
|
21
|
+
done: Number(r.done) === 1,
|
|
22
|
+
}));
|
|
23
|
+
json.ok({ list, total });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
json.fail(500, String(e));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
todos.get.schema = {
|
|
32
|
+
query: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: { keyword: { type: "string" } },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// oj v0.1.72 起出参走平级 .response(.schema 只放入参)。
|
|
39
|
+
todos.get.response = {
|
|
40
|
+
type: "object",
|
|
41
|
+
required: ["list", "total"],
|
|
42
|
+
properties: {
|
|
43
|
+
list: {
|
|
44
|
+
type: "array",
|
|
45
|
+
items: {
|
|
46
|
+
type: "object",
|
|
47
|
+
required: ["id", "title", "done"],
|
|
48
|
+
properties: {
|
|
49
|
+
id: { type: "number" },
|
|
50
|
+
title: { type: "string" },
|
|
51
|
+
done: { type: "boolean" },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
total: { type: "number" },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default todos;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// POST /api/home/line —— 首页折线(真实化 P4-1,AC-D9 apiPrefix=/home)。
|
|
2
|
+
// 窗口聚合 home_line:range(week/month/year) → 天数;在 [today-N+1, today] 内
|
|
3
|
+
// 逐日取 SUM(value),缺失日补 0(F6 幂等补数)。返回 number[](与日序对齐)。
|
|
4
|
+
const RANGE_DAYS: Record<string, number> = { week: 7, month: 30, year: 365 };
|
|
5
|
+
|
|
6
|
+
// 契约(v0.1.71「最新方式」):动词挂 `.schema` 四通道。
|
|
7
|
+
const line = {
|
|
8
|
+
async post() {
|
|
9
|
+
try {
|
|
10
|
+
const range = String((http.body && (http.body as Record<string, unknown>).range) ?? "week");
|
|
11
|
+
const days = RANGE_DAYS[range] ?? 7;
|
|
12
|
+
const today = Math.floor(Date.now() / 86400000);
|
|
13
|
+
const start = today - days + 1;
|
|
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
|
+
);
|
|
19
|
+
const map = new Map<number, number>();
|
|
20
|
+
for (const r of rows)
|
|
21
|
+
map.set(Number(r.day), Number(r.v));
|
|
22
|
+
|
|
23
|
+
let out: number[] = Array.from({ length: today - start + 1 }, (_, i) => {
|
|
24
|
+
const d = start + i;
|
|
25
|
+
return map.get(d) ?? 0;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 懒补数(F6):窗口内完全无 seed(seed 随时间变旧)时,回落确定式趋势,
|
|
29
|
+
// 保证图表非空;有 seed 时仅对缺失日补 0(上面 ?? 0 已处理)。
|
|
30
|
+
if (out.every(v => v === 0)) {
|
|
31
|
+
out = Array.from({ length: today - start + 1 }, (_, i) => {
|
|
32
|
+
const d = start + i;
|
|
33
|
+
return 50 + Math.round(40 * Math.sin(d / 3)) + (d % 7) * 3;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
json.ok(out);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
json.fail(500, String(e));
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
line.post.schema = {
|
|
45
|
+
body: {
|
|
46
|
+
type: "object",
|
|
47
|
+
required: ["range"],
|
|
48
|
+
properties: { range: { type: "string" } },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
line.post.response = {
|
|
53
|
+
type: "array",
|
|
54
|
+
items: { type: "number" },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default line;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- home 模块建表(P4-1)。home_pie 五分类聚合源;home_line 日窗口时间序列。
|
|
2
|
+
-- S007 要求序号连续且唯一,两表并入单文件 0001。day 用 epoch-day 整数便于窗口算术。
|
|
3
|
+
CREATE TABLE IF NOT EXISTS home_pie (
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
category TEXT NOT NULL DEFAULT '',
|
|
6
|
+
value INTEGER NOT NULL DEFAULT 0
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS home_line (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
day INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
value INTEGER NOT NULL DEFAULT 0
|
|
13
|
+
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// GET /api/home/pie —— 首页饼图(真实化 P4-1,AC-D9 apiPrefix=/home)。
|
|
2
|
+
// 按 category 聚合 home_pie,返回 {code,value} 数组(对齐契约 pieData)。
|
|
3
|
+
|
|
4
|
+
// 契约(oj v0.1.72「最新方式」):入参挂 `.schema`,出参挂平级 `.response`;
|
|
5
|
+
// union 用类型数组表达(见 by)。
|
|
6
|
+
const pie = {
|
|
7
|
+
async get() {
|
|
8
|
+
try {
|
|
9
|
+
const rows = await db.query(
|
|
10
|
+
"SELECT category, SUM(value) AS v FROM home_pie GROUP BY category",
|
|
11
|
+
[],
|
|
12
|
+
);
|
|
13
|
+
json.ok(rows.map(r => ({ code: String(r.category), value: Number(r.v) })));
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
json.fail(500, String(e));
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
pie.get.schema = {
|
|
22
|
+
query: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: { by: { type: ["string", "number"] } },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
pie.get.response = {
|
|
29
|
+
type: "array",
|
|
30
|
+
items: {
|
|
31
|
+
type: "object",
|
|
32
|
+
required: ["value", "code"],
|
|
33
|
+
properties: {
|
|
34
|
+
value: { type: "number" },
|
|
35
|
+
code: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default pie;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 声明式表结构(§4.2 / D1-C):home 模块自有表 home_pie / home_line。
|
|
2
|
+
tables:
|
|
3
|
+
home_pie:
|
|
4
|
+
pk: id
|
|
5
|
+
columns:
|
|
6
|
+
id: {type: integer, autoincrement: true}
|
|
7
|
+
category: {type: text, null: false}
|
|
8
|
+
value: {type: integer, null: false}
|
|
9
|
+
home_line:
|
|
10
|
+
pk: id
|
|
11
|
+
columns:
|
|
12
|
+
id: {type: integer, autoincrement: true}
|
|
13
|
+
day: {type: integer, null: false}
|
|
14
|
+
value: {type: integer, null: false}
|