@oj-module/cli 0.2.50 → 0.2.54

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 (130) hide show
  1. package/devkit/SKILL.md +2 -2
  2. package/devkit/manual.md +32 -25
  3. package/devkit/wecom-internal-app.md +111 -4
  4. package/package.json +2 -2
  5. package/shell-dist/assets/{index-FYt9CjFd.js → index-e_Mp2mb5.js} +5 -1
  6. package/shell-dist/assets/runtime.js +29 -5
  7. package/shell-dist/index.html +9 -12
  8. package/shell-dist/versions.json +3 -3
  9. package/src/__tests__/templates-tenant-scope.test.ts +3 -2
  10. package/src/build.ts +19 -2
  11. package/src/config.ts +9 -0
  12. package/src/contract/__tests__/client-path.test.ts +30 -0
  13. package/src/contract/__tests__/emit-client.test.ts +18 -0
  14. package/src/contract/__tests__/handler-scan.test.ts +23 -0
  15. package/src/contract/check.ts +5 -5
  16. package/src/contract/client-path.ts +30 -0
  17. package/src/contract/emit-client.ts +18 -4
  18. package/src/contract/handler-scan.ts +40 -3
  19. package/src/contract/run.ts +5 -3
  20. package/src/init.ts +19 -1
  21. package/src/shared-deps.ts +27 -9
  22. package/src/site-base.ts +1 -1
  23. package/templates/README.md +40 -44
  24. package/templates/init.include.yaml +15 -14
  25. package/templates/openapi.json +3422 -1955
  26. package/templates/src/_platform/api/_shared/service/domain/users.ts +40 -4
  27. package/templates/src/_platform/api/contacts/api.ts +29 -1
  28. package/templates/src/_platform/api/contacts/detail/api.ts +25 -1
  29. package/templates/src/_platform/api/solutions/api.ts +60 -1
  30. package/templates/src/_platform/web/__client/api.schemas.ts +99 -35
  31. package/templates/src/_platform/web/__client/api.ts +103 -59
  32. package/templates/src/app/web/entry.ts +77 -0
  33. package/templates/src/{home/web → app/web/home}/__client/api.ts +5 -2
  34. package/templates/src/app/web/home/routes.ts +41 -0
  35. package/templates/src/{login/web → app/web/login}/pages/login.tsx +54 -24
  36. package/templates/src/app/web/login/routes.ts +71 -0
  37. package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
  38. package/templates/src/{notification/web → app/web/notification}/__client/api.ts +67 -20
  39. package/templates/src/app/web/notification/routes.ts +70 -0
  40. package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.ts +5 -2
  41. package/templates/src/app/web/personal-center/routes.ts +49 -0
  42. package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +28 -22
  43. package/templates/src/{system/web → app/web/system}/__client/api.ts +103 -58
  44. package/templates/src/{system/web → app/web/system}/locales/en-US.json +14 -0
  45. package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +14 -0
  46. package/templates/src/{system/web → app/web/system}/pages/users/index.tsx +5 -2
  47. package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +14 -3
  48. package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +139 -1
  49. package/templates/src/app/web/system/routes.ts +97 -0
  50. package/templates/src/auth/api/_shared/wecom/admin.ts +4 -0
  51. package/templates/src/auth/api/_shared/wecom/client.ts +9 -2
  52. package/templates/src/auth/api/_shared/wecom/events.ts +3 -0
  53. package/templates/src/auth/api/_shared/wecom/tenant-admin.ts +194 -0
  54. package/templates/src/auth/api/api.ts +59 -0
  55. package/templates/src/auth/api/ldap-login/api.ts +37 -1
  56. package/templates/src/auth/api/wecom/admin/tenant-admin/api.ts +41 -0
  57. package/templates/src/auth/api/wecom/admin/tenant-admin/password/api.ts +66 -0
  58. package/templates/src/auth/api/wecom/api.ts +23 -0
  59. package/templates/src/auth/web/__client/api.schemas.ts +133 -13
  60. package/templates/src/auth/web/__client/api.ts +129 -31
  61. package/templates/src/notification/api/api.ts +17 -0
  62. package/templates/src/notification/api/del/api.ts +2 -0
  63. package/templates/src/system/api/roles/api.ts +3 -0
  64. package/templates/src/system/api/roles/bindings/api.ts +3 -0
  65. package/templates/src/system/api/settings/api.ts +6 -3
  66. package/templates/src/system/api/users/forbidden/api.ts +3 -0
  67. package/templates/src/system/api/users/password/api.ts +3 -0
  68. package/templates/src/system/api/users/roles/api.ts +3 -0
  69. package/templates/src/web/api/get-async-routes/api.ts +16 -1
  70. package/templates/src/web/api/hello/api.ts +13 -1
  71. package/templates/src/web/api/user-info/api.ts +21 -1
  72. package/templates/src/web/web/__client/api.schemas.ts +28 -0
  73. package/templates/src/web/web/__client/api.ts +33 -9
  74. package/templates/tsconfig.paths.json +0 -1
  75. package/templates/web.config.ts +22 -17
  76. package/vendor/host-versions.json +3 -3
  77. package/shell-dist/assets/echarts-charts.js +0 -58571
  78. package/shell-dist/assets/echarts-features.js +0 -24057
  79. package/shell-dist/assets/echarts-for-react.js +0 -704
  80. package/shell-dist/assets/echarts.js +0 -82916
  81. package/shell-dist/assets/editor.js +0 -161457
  82. package/shell-dist/assets/vditor-index-css.js +0 -7
  83. package/shell-dist/assets/vditor.js +0 -15223
  84. package/templates/src/demo/api/manifest.yaml +0 -5
  85. package/templates/src/demo/api/migrations/0001__create_todos.sql +0 -9
  86. package/templates/src/demo/api/migrations/0002__tenant.mysql.sql +0 -2
  87. package/templates/src/demo/api/migrations/0002__tenant.sql +0 -2
  88. package/templates/src/demo/api/schema.yaml +0 -12
  89. package/templates/src/demo/api/seed.sql +0 -5
  90. package/templates/src/demo/api/todos/api.ts +0 -59
  91. package/templates/src/demo/web/__client/api.schemas.ts +0 -20
  92. package/templates/src/demo/web/__client/api.ts +0 -89
  93. package/templates/src/demo/web/entry.ts +0 -42
  94. package/templates/src/demo/web/locales/en-US.json +0 -10
  95. package/templates/src/demo/web/locales/zh-CN.json +0 -10
  96. package/templates/src/demo/web/pages/index.tsx +0 -45
  97. package/templates/src/home/web/entry.ts +0 -49
  98. package/templates/src/login/web/entry.ts +0 -79
  99. package/templates/src/notification/web/entry.ts +0 -84
  100. package/templates/src/personal-center/web/entry.ts +0 -60
  101. package/templates/src/system/web/entry.ts +0 -108
  102. /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
  103. /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
  104. /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
  105. /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
  106. /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
  107. /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
  108. /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
  109. /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
  110. /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
  111. /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
  112. /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
  113. /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
  114. /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
  115. /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
  116. /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
  117. /package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +0 -0
  118. /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
  119. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
  120. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
  121. /package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +0 -0
  122. /package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +0 -0
  123. /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
  124. /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
  125. /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
  126. /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
  127. /package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +0 -0
  128. /package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +0 -0
  129. /package/templates/src/{system/web → app/web/system}/pages/roles/index.tsx +0 -0
  130. /package/templates/src/{system/web → app/web/system}/pages/settings/index.tsx +0 -0
@@ -0,0 +1,71 @@
1
+ import type { AppRouteRecordRaw, ModuleContext } from "@oj-module/runtime";
2
+
3
+ import { useAuthStore, useTenantStore } from "@oj-module/runtime";
4
+ import { lazy } from "react";
5
+
6
+ import { authProvider } from "./auth-provider";
7
+
8
+ /**
9
+ * 登录根 `/login`(+ 强制改密 `/change-password`)。
10
+ *
11
+ * shell 宿主只消费模块路由(`getRoutes()`),**不挂 runtime 的内置 baseRoutes**,
12
+ * 因此缺少该根时 `/login` 无路由可跳(登出/回跳登录会落空)。本根补上路由:
13
+ * 登录链路仍回落到 runtime 默认链(`fetchLogin` → 后端内置 `auth/login` +
14
+ * 守卫 `getUserInfo` → `web/user-info`),本根只负责「内容区」。
15
+ *
16
+ * `handle.login: true` 让框架识别为登录页(`resolveLoginRoute` 校验路径必须是
17
+ * `/login`);`handle.layout: "fullscreen"` 由框架注入全屏外壳。
18
+ */
19
+ export const routes: AppRouteRecordRaw[] = [
20
+ {
21
+ path: "/login",
22
+ handle: {
23
+ layout: "fullscreen",
24
+ login: true,
25
+ hideInMenu: true,
26
+ title: "login:page.title",
27
+ },
28
+ children: [
29
+ {
30
+ index: true,
31
+ Component: lazy(() => import("./pages/login")),
32
+ handle: { title: "login:page.title" },
33
+ },
34
+ ],
35
+ },
36
+ {
37
+ // 首登/重置后强制改密(must_change 悬挂期唯一可用写端);
38
+ // 管理面 403 msg=password change required 时也引导至此
39
+ path: "/change-password",
40
+ handle: {
41
+ layout: "fullscreen",
42
+ hideInMenu: true,
43
+ title: "login:page.changePwd.title",
44
+ },
45
+ children: [
46
+ {
47
+ index: true,
48
+ Component: lazy(() => import("./pages/change-password")),
49
+ handle: { title: "login:page.changePwd.title", hideInMenu: true },
50
+ },
51
+ ],
52
+ },
53
+ ];
54
+
55
+ /**
56
+ * 认证 provider:仅接管「企微票据换会话」,其余方式原样委托内置链(见 auth-provider.ts)。
57
+ * 注册时机必须在任何登录动作之前——onInit 先于路由渲染。
58
+ */
59
+ export async function onInit(ctx: ModuleContext) {
60
+ ctx.register.authProvider(authProvider);
61
+ // 登出必须清租户:否则下一个用户会带着上一个用户的租户头(跨租户请求 fail-closed 403,
62
+ // 但单租户部署会静默落到别人的 default 视图)。token 由非空转空即视为登出。
63
+ //
64
+ // 租户上下文的请求头由**运行时**注入(`runtime/store/tenant.ts`),本根不注册
65
+ // headerProvider——宿主 shell 的 hydrateUser / solution-routes 都在任何模块 onInit
66
+ // 之前就发请求,模块注册必然晚一步(2026-10-30 冷启动 e2e 实测两个 400)。
67
+ useAuthStore.subscribe((state, prev) => {
68
+ if (prev.token && !state.token)
69
+ useTenantStore.getState().clear();
70
+ });
71
+ }
@@ -21,6 +21,13 @@ export const schemas = {
21
21
  body: z.object({
22
22
  "id": z.string()
23
23
  }),
24
+ data: z.null(),
25
+ },
26
+ "notificationNotificationsReadAllPost": {
27
+ data: z.null(),
28
+ },
29
+ "notificationNotificationsClearPost": {
30
+ data: z.null(),
24
31
  },
25
32
  "notificationPushPost": {
26
33
  body: z.object({
@@ -30,6 +37,9 @@ export const schemas = {
30
37
  "link": z.string().optional(),
31
38
  "avatar": z.string().optional(),
32
39
  "category": z.enum(["notice", "pm", "mention"]).optional()
40
+ }),
41
+ data: z.object({
42
+ "id": z.string()
33
43
  }),
34
44
  },
35
45
  "notificationPushBroadcastPost": {
@@ -39,6 +49,9 @@ export const schemas = {
39
49
  "link": z.string().optional(),
40
50
  "avatar": z.string().optional(),
41
51
  "category": z.enum(["notice", "pm", "mention"]).optional()
52
+ }),
53
+ data: z.object({
54
+ "id": z.string()
42
55
  }),
43
56
  },
44
57
  "notificationDelPost": {
@@ -46,5 +59,6 @@ export const schemas = {
46
59
  "id": z.string(),
47
60
  "userId": z.union([z.string(), z.number()]).optional()
48
61
  }),
62
+ data: z.null(),
49
63
  },
50
64
  };
@@ -17,6 +17,9 @@ export const API_PREFIX = "/notification";
17
17
  /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
18
  export const MODULE_PREFIX = "/notification";
19
19
 
20
+ /** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
21
+ export const MODULE_OWNER = "/app";
22
+
20
23
  /**
21
24
  * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
25
  * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
@@ -28,8 +31,8 @@ export function createNotificationClient(ctx: ModuleContext) {
28
31
  // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
32
  // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
33
  // 属于别的命名空间,认领身份仍按目录名。
31
- if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
- console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
34
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
35
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
36
  }
34
37
  ctx.register.apiPrefix(API_PREFIX);
35
38
  bindRequest(ctx.utils.request);
@@ -88,15 +91,22 @@ export async function notificationNotificationsGet(): Promise<NotificationNotifi
88
91
  }
89
92
 
90
93
  export type NotificationNotificationsReadPostBody = z.input<(typeof schemas)["notificationNotificationsReadPost"]["body"]>;
94
+ export type NotificationNotificationsReadPostData = z.infer<(typeof schemas)["notificationNotificationsReadPost"]["data"]>;
91
95
 
92
- export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<unknown> {
96
+ export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<NotificationNotificationsReadPostData> {
93
97
  const client = ensureReq();
94
98
  try {
95
- const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<unknown>>();
99
+ const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<NotificationNotificationsReadPostData>>();
96
100
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
97
101
  if (typeof env.code === "number" && env.code !== 0)
98
102
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
99
- const data = env.data as unknown;
103
+ const data = env.data as NotificationNotificationsReadPostData;
104
+ if (import.meta.env.DEV) {
105
+ const { schemas } = await import("./api.schemas");
106
+ const r = schemas["notificationNotificationsReadPost"].data.safeParse(data);
107
+ if (!r.success)
108
+ throw new ContractApiError(-1, `[契约违例] notificationNotificationsReadPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
109
+ }
100
110
  return data;
101
111
  }
102
112
  catch (e) {
@@ -104,14 +114,22 @@ export async function notificationNotificationsReadPost(body: NotificationNotifi
104
114
  }
105
115
  }
106
116
 
107
- export async function notificationNotificationsReadAllPost(): Promise<unknown> {
117
+ export type NotificationNotificationsReadAllPostData = z.infer<(typeof schemas)["notificationNotificationsReadAllPost"]["data"]>;
118
+
119
+ export async function notificationNotificationsReadAllPost(): Promise<NotificationNotificationsReadAllPostData> {
108
120
  const client = ensureReq();
109
121
  try {
110
- const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<unknown>>();
122
+ const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<NotificationNotificationsReadAllPostData>>();
111
123
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
112
124
  if (typeof env.code === "number" && env.code !== 0)
113
125
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
114
- const data = env.data as unknown;
126
+ const data = env.data as NotificationNotificationsReadAllPostData;
127
+ if (import.meta.env.DEV) {
128
+ const { schemas } = await import("./api.schemas");
129
+ const r = schemas["notificationNotificationsReadAllPost"].data.safeParse(data);
130
+ if (!r.success)
131
+ throw new ContractApiError(-1, `[契约违例] notificationNotificationsReadAllPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
132
+ }
115
133
  return data;
116
134
  }
117
135
  catch (e) {
@@ -119,14 +137,22 @@ export async function notificationNotificationsReadAllPost(): Promise<unknown> {
119
137
  }
120
138
  }
121
139
 
122
- export async function notificationNotificationsClearPost(): Promise<unknown> {
140
+ export type NotificationNotificationsClearPostData = z.infer<(typeof schemas)["notificationNotificationsClearPost"]["data"]>;
141
+
142
+ export async function notificationNotificationsClearPost(): Promise<NotificationNotificationsClearPostData> {
123
143
  const client = ensureReq();
124
144
  try {
125
- const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<unknown>>();
145
+ const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<NotificationNotificationsClearPostData>>();
126
146
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
127
147
  if (typeof env.code === "number" && env.code !== 0)
128
148
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
129
- const data = env.data as unknown;
149
+ const data = env.data as NotificationNotificationsClearPostData;
150
+ if (import.meta.env.DEV) {
151
+ const { schemas } = await import("./api.schemas");
152
+ const r = schemas["notificationNotificationsClearPost"].data.safeParse(data);
153
+ if (!r.success)
154
+ throw new ContractApiError(-1, `[契约违例] notificationNotificationsClearPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
155
+ }
130
156
  return data;
131
157
  }
132
158
  catch (e) {
@@ -135,15 +161,22 @@ export async function notificationNotificationsClearPost(): Promise<unknown> {
135
161
  }
136
162
 
137
163
  export type NotificationPushPostBody = z.input<(typeof schemas)["notificationPushPost"]["body"]>;
164
+ export type NotificationPushPostData = z.infer<(typeof schemas)["notificationPushPost"]["data"]>;
138
165
 
139
- export async function notificationPushPost(body: NotificationPushPostBody): Promise<unknown> {
166
+ export async function notificationPushPost(body: NotificationPushPostBody): Promise<NotificationPushPostData> {
140
167
  const client = ensureReq();
141
168
  try {
142
- const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<unknown>>();
169
+ const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<NotificationPushPostData>>();
143
170
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
144
171
  if (typeof env.code === "number" && env.code !== 0)
145
172
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
146
- const data = env.data as unknown;
173
+ const data = env.data as NotificationPushPostData;
174
+ if (import.meta.env.DEV) {
175
+ const { schemas } = await import("./api.schemas");
176
+ const r = schemas["notificationPushPost"].data.safeParse(data);
177
+ if (!r.success)
178
+ throw new ContractApiError(-1, `[契约违例] notificationPushPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
179
+ }
147
180
  return data;
148
181
  }
149
182
  catch (e) {
@@ -152,15 +185,22 @@ export async function notificationPushPost(body: NotificationPushPostBody): Prom
152
185
  }
153
186
 
154
187
  export type NotificationPushBroadcastPostBody = z.input<(typeof schemas)["notificationPushBroadcastPost"]["body"]>;
188
+ export type NotificationPushBroadcastPostData = z.infer<(typeof schemas)["notificationPushBroadcastPost"]["data"]>;
155
189
 
156
- export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<unknown> {
190
+ export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<NotificationPushBroadcastPostData> {
157
191
  const client = ensureReq();
158
192
  try {
159
- const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<unknown>>();
193
+ const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<NotificationPushBroadcastPostData>>();
160
194
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
161
195
  if (typeof env.code === "number" && env.code !== 0)
162
196
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
163
- const data = env.data as unknown;
197
+ const data = env.data as NotificationPushBroadcastPostData;
198
+ if (import.meta.env.DEV) {
199
+ const { schemas } = await import("./api.schemas");
200
+ const r = schemas["notificationPushBroadcastPost"].data.safeParse(data);
201
+ if (!r.success)
202
+ throw new ContractApiError(-1, `[契约违例] notificationPushBroadcastPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
203
+ }
164
204
  return data;
165
205
  }
166
206
  catch (e) {
@@ -169,15 +209,22 @@ export async function notificationPushBroadcastPost(body: NotificationPushBroadc
169
209
  }
170
210
 
171
211
  export type NotificationDelPostBody = z.input<(typeof schemas)["notificationDelPost"]["body"]>;
212
+ export type NotificationDelPostData = z.infer<(typeof schemas)["notificationDelPost"]["data"]>;
172
213
 
173
- export async function notificationDelPost(body: NotificationDelPostBody): Promise<unknown> {
214
+ export async function notificationDelPost(body: NotificationDelPostBody): Promise<NotificationDelPostData> {
174
215
  const client = ensureReq();
175
216
  try {
176
- const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<unknown>>();
217
+ const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<NotificationDelPostData>>();
177
218
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
178
219
  if (typeof env.code === "number" && env.code !== 0)
179
220
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
180
- const data = env.data as unknown;
221
+ const data = env.data as NotificationDelPostData;
222
+ if (import.meta.env.DEV) {
223
+ const { schemas } = await import("./api.schemas");
224
+ const r = schemas["notificationDelPost"].data.safeParse(data);
225
+ if (!r.success)
226
+ throw new ContractApiError(-1, `[契约违例] notificationDelPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
227
+ }
181
228
  return data;
182
229
  }
183
230
  catch (e) {
@@ -0,0 +1,70 @@
1
+ import type { AppRouteRecordRaw, ModuleContext, NotificationsApiProvider } from "@oj-module/runtime";
2
+
3
+ import { BellOutlined } from "@ant-design/icons";
4
+
5
+ import { createElement, lazy } from "react";
6
+
7
+ import { createNotificationClient } from "./__client/api";
8
+
9
+ const NotificationCenter = lazy(() => import("./pages/center"));
10
+
11
+ /**
12
+ * 通知中心页(N3):全量消息 + 类型 Tab,铃铛弹层「查看全部」跳转至此。
13
+ * hideInMenu——入口在铃铛,不占侧边导航。
14
+ * 布局挂父路由 handle(resolveRouteLayouts 只给「无 Component 有 children」的
15
+ * 父路由注入 layout,页面组件写父级上布局会被忽略、裸渲染无 chrome)。
16
+ */
17
+ export const routes: AppRouteRecordRaw[] = [
18
+ {
19
+ path: "/notification-center",
20
+ handle: {
21
+ layout: "container",
22
+ title: "notification:menu.notificationCenter",
23
+ icon: createElement(BellOutlined),
24
+ hideInMenu: true,
25
+ },
26
+ children: [
27
+ {
28
+ index: true,
29
+ Component: NotificationCenter,
30
+ handle: {
31
+ title: "notification:menu.notificationCenter",
32
+ icon: createElement(BellOutlined),
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ ];
38
+
39
+ /**
40
+ * 通知聚合的唯一前端消费面(G5 模板形态)。
41
+ *
42
+ * onInit 全量接管通知 API(D9 + G4):把通知读/写收敛到本模块 `/notification`
43
+ * 前缀下的 oj 后端,通知铃的互动(单条已读/全部已读/清空)由此生效;发射端
44
+ * 三方法(push/pushBroadcast/del)使本模块同时作为跨模块通知原语的接收端
45
+ * (前端 facade 委托至此)。
46
+ */
47
+ export async function onInit(ctx: ModuleContext) {
48
+ // 构造即接线:createNotificationClient 登记契约前缀 /notification
49
+ // (唯一真源在 __client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
50
+ // request(越界/穿越发请求前即拒绝,前缀收敛 D11),返回的 client
51
+ // 方法都自动带 /api/notification/ 前缀。
52
+ const notificationClient = createNotificationClient(ctx);
53
+ const provider: NotificationsApiProvider = {
54
+ // 端点函数名由 oj operationId 派生(camelCase 子路径 + 方法),
55
+ // 见 __client/api.ts;provider 方法名(fetchNotifications/markRead…)不变。
56
+ fetchNotifications: () => notificationClient.notificationNotificationsGet(),
57
+ // id 为命名空间串("u:<id>" / "b:<id>"),原样透传
58
+ markRead: id => notificationClient.notificationNotificationsReadPost({ id: String(id) }).then(() => {}),
59
+ markAllRead: () => notificationClient.notificationNotificationsReadAllPost().then(() => {}),
60
+ clearAll: () => notificationClient.notificationNotificationsClearPost().then(() => {}),
61
+ push: input => notificationClient
62
+ .notificationPushPost({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
63
+ .then(r => String((r as { id: string }).id)),
64
+ pushBroadcast: input => notificationClient
65
+ .notificationPushBroadcastPost({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
66
+ .then(r => String((r as { id: string }).id)),
67
+ del: (id, userId) => notificationClient.notificationDelPost({ id: String(id), userId }).then(() => {}),
68
+ };
69
+ ctx.register.notificationsApi(provider);
70
+ }
@@ -17,6 +17,9 @@ export const API_PREFIX = "/personal-center";
17
17
  /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
18
  export const MODULE_PREFIX = "/personal-center";
19
19
 
20
+ /** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
21
+ export const MODULE_OWNER = "/app";
22
+
20
23
  /**
21
24
  * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
25
  * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
@@ -28,8 +31,8 @@ export function createPersonalCenterClient(ctx: ModuleContext) {
28
31
  // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
32
  // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
33
  // 属于别的命名空间,认领身份仍按目录名。
31
- if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
- console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
34
+ if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
35
+ console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
36
  }
34
37
  ctx.register.apiPrefix(API_PREFIX);
35
38
  bindRequest(ctx.utils.request);
@@ -0,0 +1,49 @@
1
+ import type { AppRouteRecordRaw, ModuleContext, UploadApiProvider } from "@oj-module/runtime";
2
+ import { ProfileCardIcon, RiAccountCircleLine, useAuthStore } from "@oj-module/runtime";
3
+
4
+ import { createElement, lazy } from "react";
5
+
6
+ import { createPersonalCenterClient } from "./__client/api";
7
+
8
+ const MyProfile = lazy(() => import("./pages/my-profile"));
9
+
10
+ /** 个人中心根 `/personal-center`:runtime 用户菜单固定导航 `/personal-center/my-profile`,缺则点击落空。 */
11
+ export const routes: AppRouteRecordRaw[] = [
12
+ {
13
+ path: "/personal-center",
14
+ handle: {
15
+ layout: "container",
16
+ order: 110,
17
+ title: "personal-center:menu.personalCenter",
18
+ icon: createElement(RiAccountCircleLine),
19
+ },
20
+ children: [
21
+ {
22
+ path: "/personal-center/my-profile",
23
+ Component: MyProfile,
24
+ handle: {
25
+ title: "personal-center:menu.profile",
26
+ icon: createElement(ProfileCardIcon),
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ ];
32
+
33
+ /**
34
+ * P4-3:接管头像上传端点(D9)。antd Upload 直连 action,headers 在上传瞬间
35
+ * 取当前 token 注入 Bearer(token 存 runtime auth store,跨渲染期读 getState)。
36
+ */
37
+ export async function onInit(ctx: ModuleContext) {
38
+ // 构造即接线:登记契约前缀 /personal-center + 绑定 scoped request,
39
+ // 页面直接 import 的 personalCenterProfilePost 才有可用的请求通道
40
+ createPersonalCenterClient(ctx);
41
+ const provider: UploadApiProvider = {
42
+ action: "/api/personal-center/upload",
43
+ headers: (): Record<string, string> => {
44
+ const token = useAuthStore.getState().token;
45
+ return token ? { Authorization: `Bearer ${token}` } : {};
46
+ },
47
+ };
48
+ ctx.register.uploadApi(provider);
49
+ }
@@ -48,17 +48,6 @@ export const schemas = {
48
48
  }),
49
49
  data: z.object({
50
50
  "ok": z.boolean()
51
- }),
52
- },
53
- "systemDepartmentsMovePut": {
54
- query: z.object({
55
- "id": z.number()
56
- }),
57
- body: z.object({
58
- "parentId": z.number().nullable()
59
- }),
60
- data: z.object({
61
- "ok": z.boolean()
62
51
  }),
63
52
  },
64
53
  "systemDepartmentsDetachPost": {
@@ -118,6 +107,17 @@ export const schemas = {
118
107
  }),
119
108
  data: z.object({
120
109
  "ok": z.boolean()
110
+ }),
111
+ },
112
+ "systemDepartmentsMovePut": {
113
+ query: z.object({
114
+ "id": z.number()
115
+ }),
116
+ body: z.object({
117
+ "parentId": z.number().nullable()
118
+ }),
119
+ data: z.object({
120
+ "ok": z.boolean()
121
121
  }),
122
122
  },
123
123
  "systemLoginMethodsGet": {
@@ -197,6 +197,7 @@ export const schemas = {
197
197
  query: z.object({
198
198
  "name": z.string().min(1)
199
199
  }),
200
+ data: z.null(),
200
201
  },
201
202
  "systemRolesBindingsPut": {
202
203
  query: z.object({
@@ -205,6 +206,7 @@ export const schemas = {
205
206
  body: z.object({
206
207
  "permissions": z.array(z.string())
207
208
  }),
209
+ data: z.null(),
208
210
  },
209
211
  "systemSettingsGet": {
210
212
  data: z.array(z.object({
@@ -228,6 +230,7 @@ export const schemas = {
228
230
  "key": z.string().min(1),
229
231
  "value": z.string()
230
232
  }),
233
+ data: z.null(),
231
234
  },
232
235
  "systemUsersGet": {
233
236
  data: z.array(z.object({
@@ -265,33 +268,35 @@ export const schemas = {
265
268
  "ok": z.boolean()
266
269
  }),
267
270
  },
268
- "systemUsersProfilePut": {
271
+ "systemUsersForbiddenPut": {
269
272
  query: z.object({
270
273
  "uid": z.number()
271
274
  }),
272
275
  body: z.object({
273
- "displayName": z.string().nullable().optional(),
274
- "email": z.string().nullable().optional(),
275
- "position": z.string().nullable().optional()
276
- }),
277
- data: z.object({
278
- "ok": z.boolean()
276
+ "forbidden": z.boolean()
279
277
  }),
278
+ data: z.null(),
280
279
  },
281
- "systemUsersForbiddenPut": {
280
+ "systemUsersPasswordPut": {
282
281
  query: z.object({
283
282
  "uid": z.number()
284
283
  }),
285
284
  body: z.object({
286
- "forbidden": z.boolean()
285
+ "password": z.string().min(6)
287
286
  }),
287
+ data: z.null(),
288
288
  },
289
- "systemUsersPasswordPut": {
289
+ "systemUsersProfilePut": {
290
290
  query: z.object({
291
291
  "uid": z.number()
292
292
  }),
293
293
  body: z.object({
294
- "password": z.string().min(6)
294
+ "displayName": z.string().nullable().optional(),
295
+ "email": z.string().nullable().optional(),
296
+ "position": z.string().nullable().optional()
297
+ }),
298
+ data: z.object({
299
+ "ok": z.boolean()
295
300
  }),
296
301
  },
297
302
  "systemUsersRolesPut": {
@@ -301,5 +306,6 @@ export const schemas = {
301
306
  body: z.object({
302
307
  "roles": z.array(z.string())
303
308
  }),
309
+ data: z.null(),
304
310
  },
305
311
  };