@oj-module/cli 0.2.47 → 0.2.48

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 (94) hide show
  1. package/devkit/SKILL.md +1 -1
  2. package/devkit/framework-dev.md +2 -2
  3. package/devkit/manual.md +24 -14
  4. package/devkit/module-dev-and-cdn-playground.md +2 -2
  5. package/devkit/wecom-daikaifa.md +237 -14
  6. package/package.json +2 -2
  7. package/shell-dist/assets/runtime.js +2 -2
  8. package/shell-dist/index.html +2 -2
  9. package/shell-dist/versions.json +3 -3
  10. package/src/__tests__/templates-tenant-scope.test.ts +7 -2
  11. package/src/init.ts +1 -1
  12. package/src/market/__tests__/bootstrap.test.ts +1 -1
  13. package/src/market/__tests__/setup.test.ts +7 -7
  14. package/src/market/bootstrap.ts +1 -1
  15. package/templates/README.md +1 -1
  16. package/templates/init.include.yaml +12 -0
  17. package/templates/openapi.json +2863 -549
  18. package/templates/src/_platform/api/_shared/service/domain/contacts.ts +7 -0
  19. package/templates/src/_platform/api/_shared/service/domain/org-read.ts +451 -0
  20. package/templates/src/_platform/api/_shared/service/domain/org.ts +566 -0
  21. package/templates/src/_platform/api/_shared/service/domain/users.ts +140 -2
  22. package/templates/src/_platform/api/_shared/service/infra/query.ts +2 -1
  23. package/templates/src/_platform/api/fixtures/l1-test-user.sql +1 -1
  24. package/templates/src/_platform/api/manifest.yaml +3 -0
  25. package/templates/src/_platform/api/migrations/0016__org.mysql.sql +41 -0
  26. package/templates/src/_platform/api/migrations/0016__org.pg.sql +40 -0
  27. package/templates/src/_platform/api/migrations/0016__org.sql +62 -0
  28. package/templates/src/_platform/api/org/departments/api.ts +51 -0
  29. package/templates/src/_platform/api/org/is-member/api.ts +33 -0
  30. package/templates/src/_platform/api/org/members/api.ts +55 -0
  31. package/templates/src/_platform/api/org/picker/api.ts +59 -0
  32. package/templates/src/_platform/api/org/resolve/api.ts +38 -0
  33. package/templates/src/_platform/api/org/tree/api.ts +37 -0
  34. package/templates/src/_platform/api/org/user/api.ts +46 -0
  35. package/templates/src/_platform/api/schema.yaml +48 -0
  36. package/templates/src/_platform/api/seed.mysql.sql +10 -5
  37. package/templates/src/_platform/api/seed.pg.sql +10 -5
  38. package/templates/src/_platform/api/seed.sql +13 -5
  39. package/templates/src/_platform/web/__client/api.schemas.ts +126 -0
  40. package/templates/src/_platform/web/__client/api.ts +170 -1
  41. package/templates/src/auth/api/_shared/ldap-login.ts +2 -0
  42. package/templates/src/auth/api/_shared/session.ts +5 -1
  43. package/templates/src/auth/api/_shared/wecom/admin.ts +3 -1
  44. package/templates/src/auth/api/_shared/wecom/client.ts +243 -31
  45. package/templates/src/auth/api/_shared/wecom/config.ts +12 -1
  46. package/templates/src/auth/api/_shared/wecom/contacts.ts +263 -0
  47. package/templates/src/auth/api/_shared/wecom/crypt.ts +17 -7
  48. package/templates/src/auth/api/_shared/wecom/events.ts +111 -26
  49. package/templates/src/auth/api/_shared/wecom/login.ts +152 -1
  50. package/templates/src/auth/api/_shared/wecom/payload.ts +116 -0
  51. package/templates/src/auth/api/_shared/wecom/store.ts +65 -0
  52. package/templates/src/auth/api/_shared/wecom/sync.ts +487 -0
  53. package/templates/src/auth/api/manifest.yaml +3 -0
  54. package/templates/src/auth/api/migrations/0002__org_sync.mysql.sql +28 -0
  55. package/templates/src/auth/api/migrations/0002__org_sync.pg.sql +27 -0
  56. package/templates/src/auth/api/migrations/0002__org_sync.sql +41 -0
  57. package/templates/src/auth/api/schema.yaml +31 -0
  58. package/templates/src/auth/api/wecom/admin/api.ts +3 -1
  59. package/templates/src/auth/api/wecom/api.ts +100 -18
  60. package/templates/src/auth/api/wecom/sync/api.ts +199 -0
  61. package/templates/src/auth/web/__client/api.schemas.ts +97 -0
  62. package/templates/src/auth/web/__client/api.ts +73 -1
  63. package/templates/src/notification/api/seed.sql +1 -1
  64. package/templates/src/system/api/departments/api.ts +142 -0
  65. package/templates/src/system/api/departments/detach/api.ts +34 -0
  66. package/templates/src/system/api/departments/members/api.ts +142 -0
  67. package/templates/src/system/api/departments/move/api.ts +39 -0
  68. package/templates/src/system/api/users/api.ts +45 -5
  69. package/templates/src/system/api/users/profile/api.ts +44 -0
  70. package/templates/src/system/web/__client/api.schemas.ts +143 -0
  71. package/templates/src/system/web/__client/api.ts +293 -1
  72. package/templates/src/system/web/entry.ts +6 -0
  73. package/templates/src/system/web/locales/en-US.json +112 -6
  74. package/templates/src/system/web/locales/zh-CN.json +112 -6
  75. package/templates/src/system/web/pages/departments/index.tsx +346 -0
  76. package/templates/src/system/web/pages/departments/sync-bar.tsx +257 -0
  77. package/templates/src/system/web/pages/departments/tree.ts +66 -0
  78. package/templates/src/system/web/pages/errors.ts +1 -1
  79. package/templates/src/system/web/pages/users/index.tsx +49 -7
  80. package/templates/src/system/web/pages/wecom/index.tsx +14 -0
  81. package/templates/tests/l1-builder.test.ts +1 -1
  82. package/templates/tests/org-departments.spec.ts +311 -0
  83. package/templates/tests/org-read.spec.ts +469 -0
  84. package/templates/tests/rbac-contacts.spec.ts +13 -3
  85. package/templates/tests/rbac-seed.spec.ts +11 -0
  86. package/templates/tests/seed-role-accounts.spec.ts +6 -2
  87. package/templates/tests/wecom-admin.spec.ts +1 -0
  88. package/templates/tests/wecom-connector.spec.ts +115 -8
  89. package/templates/tests/wecom-crypt.spec.ts +12 -3
  90. package/templates/tests/wecom-endpoints.spec.ts +182 -14
  91. package/templates/tests/wecom-org-sync.spec.ts +598 -0
  92. package/templates/tests/wecom-sync.spec.ts +286 -0
  93. package/vendor/host-versions.json +3 -3
  94. package/templates/src/auth/api/_shared/wecom/xml.ts +0 -36
@@ -0,0 +1,142 @@
1
+ import type { OrgDeps } from "#/_platform/_shared/service/domain/org";
2
+ import { respond } from "#/_platform/_shared/respond";
3
+ // 管理域端点组 /api/system/departments —— 组织架构 CRUD 基线(方案 A2)。
4
+ // handler 只编排:assertSystemAdmin 守卫 → org 领域服务 → json 信封;归属权红线在 org.ts(D13)。
5
+ //
6
+ // 列表**返回平表而不是树**:oj 的响应契约不支持递归 schema(contract IR 无 $ref),
7
+ // 树由前端按 parentId 组装。管理员要看全量(含 sync_visible=0 与软删壳),故两个开关全开。
8
+ import {
9
+ createDepartment,
10
+ deleteDepartment,
11
+ listDepartments,
12
+ memberCounts,
13
+
14
+ updateDepartment,
15
+ } from "#/_platform/_shared/service/domain/org";
16
+ import { assertSystemAdmin } from "#/_platform/_shared/service/infra/access";
17
+
18
+ /** 秒级 epoch(与 sys_oplog / wecom store 同一口径) */
19
+ const orgDeps: OrgDeps = { now: () => Math.floor(Date.now() / 1000) };
20
+
21
+ // GET /api/system/departments —— 部门平表(含成员计数),管理页树的数据源。
22
+ async function get() {
23
+ await respond(async () => {
24
+ await assertSystemAdmin();
25
+ const [depts, counts] = await Promise.all([
26
+ listDepartments({ includeDeleted: true, includeHidden: true }),
27
+ memberCounts(),
28
+ ]);
29
+ return depts.map(d => ({ ...d, memberCount: counts.get(d.id) ?? 0 }));
30
+ });
31
+ }
32
+ get.response = {
33
+ type: "array",
34
+ items: {
35
+ type: "object",
36
+ required: ["id", "origin", "originId", "parentId", "name", "orderNo", "syncVisible", "deleted", "memberCount"],
37
+ properties: {
38
+ id: { type: "number" },
39
+ // 归属权标识:local = 本站可 CRUD;其余 = 对应适配器管辖(D13)
40
+ origin: { type: "string" },
41
+ originId: { type: "string" },
42
+ parentId: { type: ["number", "null"] },
43
+ name: { type: ["string", "null"] },
44
+ nameEn: { type: ["string", "null"] },
45
+ orderNo: { type: "number" },
46
+ syncVisible: { type: "boolean" },
47
+ deleted: { type: "boolean" },
48
+ memberCount: { type: "number" },
49
+ },
50
+ },
51
+ };
52
+
53
+ // POST /api/system/departments —— 建部门(origin 恒 local)。
54
+ async function post() {
55
+ await respond(async () => {
56
+ await assertSystemAdmin();
57
+ const b = (http.body || {}) as { name?: string, parentId?: number | null, nameEn?: string | null, orderNo?: number };
58
+ return { id: await createDepartment({
59
+ name: String(b.name ?? ""),
60
+ parentId: b.parentId ?? null,
61
+ nameEn: b.nameEn ?? null,
62
+ orderNo: b.orderNo,
63
+ }, orgDeps) };
64
+ });
65
+ }
66
+ post.schema = {
67
+ body: {
68
+ type: "object",
69
+ required: ["name"],
70
+ properties: {
71
+ name: { type: "string", minLength: 1 },
72
+ // 不给 parentId 即建在根下
73
+ parentId: { type: ["number", "null"] },
74
+ nameEn: { type: ["string", "null"] },
75
+ orderNo: { type: "number" },
76
+ },
77
+ },
78
+ };
79
+ post.response = {
80
+ type: "object",
81
+ required: ["id"],
82
+ properties: { id: { type: "number" } },
83
+ };
84
+
85
+ // PUT /api/system/departments?id= —— 改名/排序。部门归属 origin='local' 才放行(D13)。
86
+ async function put() {
87
+ await respond(async () => {
88
+ await assertSystemAdmin();
89
+ const q = http.query as Record<string, unknown>;
90
+ const b = (http.body || {}) as { name?: string, nameEn?: string | null, orderNo?: number };
91
+ await updateDepartment(Number(q.id), {
92
+ name: b.name,
93
+ nameEn: b.nameEn,
94
+ orderNo: b.orderNo,
95
+ }, orgDeps);
96
+ return { ok: true };
97
+ });
98
+ }
99
+ put.schema = {
100
+ query: {
101
+ type: "object",
102
+ required: ["id"],
103
+ properties: { id: { type: "number" } },
104
+ },
105
+ body: {
106
+ type: "object",
107
+ properties: {
108
+ name: { type: "string", minLength: 1 },
109
+ nameEn: { type: ["string", "null"] },
110
+ orderNo: { type: "number" },
111
+ },
112
+ },
113
+ };
114
+ put.response = {
115
+ type: "object",
116
+ required: ["ok"],
117
+ properties: { ok: { type: "boolean" } },
118
+ };
119
+
120
+ // DELETE /api/system/departments?id= —— 删部门(有子部门即拒;关联成员行一并清)。
121
+ async function del() {
122
+ await respond(async () => {
123
+ await assertSystemAdmin();
124
+ const q = http.query as Record<string, unknown>;
125
+ await deleteDepartment(Number(q.id));
126
+ return { ok: true };
127
+ });
128
+ }
129
+ del.schema = {
130
+ query: {
131
+ type: "object",
132
+ required: ["id"],
133
+ properties: { id: { type: "number" } },
134
+ },
135
+ };
136
+ del.response = {
137
+ type: "object",
138
+ required: ["ok"],
139
+ properties: { ok: { type: "boolean" } },
140
+ };
141
+
142
+ export default { get, post, put, del };
@@ -0,0 +1,34 @@
1
+ import type { OrgDeps } from "#/_platform/_shared/service/domain/org";
2
+ import { respond } from "#/_platform/_shared/respond";
3
+ import { detachToLocal } from "#/_platform/_shared/service/domain/org";
4
+ // POST /api/system/departments/detach?id= —— 「转为本地维护」(D14 保底出口)。
5
+ //
6
+ // 把一行脱离适配器管辖(origin → local):此后改名/移动/删除全开,且后续同步轮次
7
+ // 不再改写它。这是「外部源只给了残缺数据(如企微未授权部门名)」时唯一的出路。
8
+ // 反向操作不提供:转回去后下一次同步立刻按源口径改写,语义不可控。
9
+ import { assertSystemAdmin } from "#/_platform/_shared/service/infra/access";
10
+
11
+ const orgDeps: OrgDeps = { now: () => Math.floor(Date.now() / 1000) };
12
+
13
+ async function post() {
14
+ await respond(async () => {
15
+ await assertSystemAdmin();
16
+ const q = http.query as Record<string, unknown>;
17
+ await detachToLocal(Number(q.id), orgDeps);
18
+ return { ok: true };
19
+ });
20
+ }
21
+ post.schema = {
22
+ query: {
23
+ type: "object",
24
+ required: ["id"],
25
+ properties: { id: { type: "number" } },
26
+ },
27
+ };
28
+ post.response = {
29
+ type: "object",
30
+ required: ["ok"],
31
+ properties: { ok: { type: "boolean" } },
32
+ };
33
+
34
+ export default { post };
@@ -0,0 +1,142 @@
1
+ import type { OrgDeps } from "#/_platform/_shared/service/domain/org";
2
+ import { respond } from "#/_platform/_shared/respond";
3
+ import {
4
+ addMember,
5
+ listMembers,
6
+
7
+ removeMember,
8
+ updateMember,
9
+ } from "#/_platform/_shared/service/domain/org";
10
+ // /api/system/departments/members?id= —— 部门成员的增删查改(方案 A2)。
11
+ //
12
+ // 加人**不看部门的 origin**:「谁在这个部门」本站始终可调,即便部门本身由企微同步而来;
13
+ // 受 D13 限制的只有部门的**结构字段**(名称/层级/删除),两者不是一回事。
14
+ import { assertSystemAdmin } from "#/_platform/_shared/service/infra/access";
15
+
16
+ const orgDeps: OrgDeps = { now: () => Math.floor(Date.now() / 1000) };
17
+
18
+ const MEMBER_ITEM = {
19
+ type: "object",
20
+ required: ["userId", "deptId", "orderNo", "isLeader", "isMain", "username"],
21
+ properties: {
22
+ userId: { type: "number" },
23
+ deptId: { type: "number" },
24
+ orderNo: { type: "number" },
25
+ isLeader: { type: "boolean" },
26
+ isMain: { type: "boolean" },
27
+ username: { type: "string" },
28
+ displayName: { type: ["string", "null"] },
29
+ },
30
+ } as const;
31
+
32
+ // GET /api/system/departments/members?id= —— 成员列表(含 users 展示字段)
33
+ async function get() {
34
+ await respond(async () => {
35
+ await assertSystemAdmin();
36
+ return listMembers(Number((http.query as Record<string, unknown>).id));
37
+ });
38
+ }
39
+ get.schema = {
40
+ query: {
41
+ type: "object",
42
+ required: ["id"],
43
+ properties: { id: { type: "number" } },
44
+ },
45
+ };
46
+ get.response = { type: "array", items: MEMBER_ITEM };
47
+
48
+ // POST /api/system/departments/members?id= —— 加人
49
+ async function post() {
50
+ await respond(async () => {
51
+ await assertSystemAdmin();
52
+ const q = http.query as Record<string, unknown>;
53
+ const b = (http.body || {}) as { userId?: number, isLeader?: boolean, isMain?: boolean, orderNo?: number };
54
+ await addMember(Number(q.id), Number(b.userId), {
55
+ isLeader: b.isLeader,
56
+ isMain: b.isMain,
57
+ orderNo: b.orderNo,
58
+ }, orgDeps);
59
+ return { ok: true };
60
+ });
61
+ }
62
+ post.schema = {
63
+ query: {
64
+ type: "object",
65
+ required: ["id"],
66
+ properties: { id: { type: "number" } },
67
+ },
68
+ body: {
69
+ type: "object",
70
+ required: ["userId"],
71
+ properties: {
72
+ userId: { type: "number" },
73
+ isLeader: { type: "boolean" },
74
+ isMain: { type: "boolean" },
75
+ orderNo: { type: "number" },
76
+ },
77
+ },
78
+ };
79
+ post.response = {
80
+ type: "object",
81
+ required: ["ok"],
82
+ properties: { ok: { type: "boolean" } },
83
+ };
84
+
85
+ // PUT /api/system/departments/members?id=&userId= —— 改成员属性(负责人/主部门/排序)
86
+ async function put() {
87
+ await respond(async () => {
88
+ await assertSystemAdmin();
89
+ const q = http.query as Record<string, unknown>;
90
+ const b = (http.body || {}) as { isLeader?: boolean, isMain?: boolean, orderNo?: number };
91
+ await updateMember(Number(q.id), Number(q.userId), {
92
+ isLeader: b.isLeader,
93
+ isMain: b.isMain,
94
+ orderNo: b.orderNo,
95
+ }, orgDeps);
96
+ return { ok: true };
97
+ });
98
+ }
99
+ put.schema = {
100
+ query: {
101
+ type: "object",
102
+ required: ["id", "userId"],
103
+ properties: { id: { type: "number" }, userId: { type: "number" } },
104
+ },
105
+ body: {
106
+ type: "object",
107
+ properties: {
108
+ isLeader: { type: "boolean" },
109
+ isMain: { type: "boolean" },
110
+ orderNo: { type: "number" },
111
+ },
112
+ },
113
+ };
114
+ put.response = {
115
+ type: "object",
116
+ required: ["ok"],
117
+ properties: { ok: { type: "boolean" } },
118
+ };
119
+
120
+ // DELETE /api/system/departments/members?id=&userId= —— 移出部门
121
+ async function del() {
122
+ await respond(async () => {
123
+ await assertSystemAdmin();
124
+ const q = http.query as Record<string, unknown>;
125
+ await removeMember(Number(q.id), Number(q.userId), orgDeps);
126
+ return { ok: true };
127
+ });
128
+ }
129
+ del.schema = {
130
+ query: {
131
+ type: "object",
132
+ required: ["id", "userId"],
133
+ properties: { id: { type: "number" }, userId: { type: "number" } },
134
+ },
135
+ };
136
+ del.response = {
137
+ type: "object",
138
+ required: ["ok"],
139
+ properties: { ok: { type: "boolean" } },
140
+ };
141
+
142
+ export default { get, post, put, del };
@@ -0,0 +1,39 @@
1
+ import type { OrgDeps } from "#/_platform/_shared/service/domain/org";
2
+ import { respond } from "#/_platform/_shared/respond";
3
+ import { moveDepartment } from "#/_platform/_shared/service/domain/org";
4
+ // PUT /api/system/departments/move?id= —— 改父级(拖拽排序的数据出口)。
5
+ // parentId=null 即移到根下;自挂与「挂到自己的子孙」由领域层判环后 400。
6
+ import { assertSystemAdmin } from "#/_platform/_shared/service/infra/access";
7
+
8
+ const orgDeps: OrgDeps = { now: () => Math.floor(Date.now() / 1000) };
9
+
10
+ async function put() {
11
+ await respond(async () => {
12
+ await assertSystemAdmin();
13
+ const q = http.query as Record<string, unknown>;
14
+ const b = http.body || {};
15
+ const raw = (b as { parentId?: number | null }).parentId;
16
+ await moveDepartment(Number(q.id), raw == null ? null : Number(raw), orgDeps);
17
+ return { ok: true };
18
+ });
19
+ }
20
+ put.schema = {
21
+ query: {
22
+ type: "object",
23
+ required: ["id"],
24
+ properties: { id: { type: "number" } },
25
+ },
26
+ body: {
27
+ type: "object",
28
+ required: ["parentId"],
29
+ // 显式 null = 提到根;不给这个键按 schema 视为未提供
30
+ properties: { parentId: { type: ["number", "null"] } },
31
+ },
32
+ };
33
+ put.response = {
34
+ type: "object",
35
+ required: ["ok"],
36
+ properties: { ok: { type: "boolean" } },
37
+ };
38
+
39
+ export default { put };
@@ -1,5 +1,5 @@
1
1
  import { respond } from "#/_platform/_shared/respond";
2
- import { createUser, listUsers } from "#/_platform/_shared/service/domain/users";
2
+ import { createUser, deleteUser, listUsers } from "#/_platform/_shared/service/domain/users";
3
3
  // 管理域端点组 /api/system/users(AC1-7~10):handler 只编排——assertSystemAdmin 守卫 → service → json。
4
4
  // 保护红线在 service 层硬 enforcement:禁止操作自己、admin 不碰成员资格、最后 superadmin 兜底。
5
5
  // 子端点:roles/password/forbidden 各为独立目录(wiki per-endpoint 先例)。
@@ -18,12 +18,26 @@ get.response = {
18
18
  type: "array",
19
19
  items: {
20
20
  type: "object",
21
- required: ["id", "username", "display_name", "email", "is_forbidden", "must_change", "roles"],
21
+ required: ["id", "username", "display_name", "email", "position", "departments", "is_forbidden", "must_change", "roles"],
22
22
  properties: {
23
23
  id: { type: "number" },
24
24
  username: { type: "string" },
25
25
  display_name: { type: ["string", "null"] },
26
26
  email: { type: ["string", "null"] },
27
+ position: { type: ["string", "null"] },
28
+ // 所属部门(主部门在前);未加入任何部门即空数组。带 id 供前端按部门筛选
29
+ departments: {
30
+ type: "array",
31
+ items: {
32
+ type: "object",
33
+ required: ["id", "name", "isMain"],
34
+ properties: {
35
+ id: { type: "number" },
36
+ name: { type: ["string", "null"] },
37
+ isMain: { type: "boolean" },
38
+ },
39
+ },
40
+ },
27
41
  // service 已做 Number(x)===1 布尔归一,此处是真 boolean
28
42
  is_forbidden: { type: "boolean" },
29
43
  must_change: { type: "boolean" },
@@ -33,9 +47,13 @@ get.response = {
33
47
  };
34
48
 
35
49
  // POST /api/system/users —— 建号(B6 加固;默认 must_change=1 首登改密)。
50
+ // 必须走 respond 收口:createUser 的业务拒绝是 `fail(400, …)`,不收口会被 oj 一律抹成
51
+ // 500(实测:重名建号返回 500「username exists」,前端拿不到可展示的 400 文案)。
36
52
  async function post() {
37
- await assertSystemAdmin();
38
- json.ok({ id: await createUser(http.body || {}, bcryptDeps) });
53
+ await respond(async () => {
54
+ await assertSystemAdmin();
55
+ return { id: await createUser(http.body || {}, bcryptDeps) };
56
+ });
39
57
  }
40
58
  post.schema = {
41
59
  body: {
@@ -55,4 +73,26 @@ post.response = {
55
73
  properties: { id: { type: "number" } },
56
74
  };
57
75
 
58
- export default { get, post };
76
+ // DELETE /api/system/users?uid= —— 删号。级联清部门成员/角色/会话,最后 superadmin 拒删。
77
+ async function del() {
78
+ await respond(async () => {
79
+ await assertSystemAdmin();
80
+ const q = http.query as Record<string, unknown>;
81
+ await deleteUser(Number(http.user?.id), Number(q.uid));
82
+ return { ok: true };
83
+ });
84
+ }
85
+ del.schema = {
86
+ query: {
87
+ type: "object",
88
+ required: ["uid"],
89
+ properties: { uid: { type: "number" } },
90
+ },
91
+ };
92
+ del.response = {
93
+ type: "object",
94
+ required: ["ok"],
95
+ properties: { ok: { type: "boolean" } },
96
+ };
97
+
98
+ export default { get, post, del };
@@ -0,0 +1,44 @@
1
+ import { respond } from "#/_platform/_shared/respond";
2
+ import { updateUserProfile } from "#/_platform/_shared/service/domain/users";
3
+ // PUT /api/system/users/profile?uid= —— 改资料(显示名 / 邮箱 / 职务)。
4
+ //
5
+ // 用户名**不在可改之列**:它是外部身份的主键(`wecom:<userid>` 这类),改名会让下一次
6
+ // 登录认错人(B6 命名空间抢占,见 RESERVED_USERNAME_PREFIX)。启停走 users/forbidden、
7
+ // 改密走 users/password——那两者带「级联删 refresh」的副作用,不混进通用改资料。
8
+ import { assertSystemAdmin } from "#/_platform/_shared/service/infra/access";
9
+
10
+ async function put() {
11
+ await respond(async () => {
12
+ await assertSystemAdmin();
13
+ const q = http.query as Record<string, unknown>;
14
+ const b = (http.body || {}) as { displayName?: string | null, email?: string | null, position?: string | null };
15
+ await updateUserProfile(Number(http.user?.id), Number(q.uid), {
16
+ displayName: b.displayName,
17
+ email: b.email,
18
+ position: b.position,
19
+ });
20
+ return { ok: true };
21
+ });
22
+ }
23
+ put.schema = {
24
+ query: {
25
+ type: "object",
26
+ required: ["uid"],
27
+ properties: { uid: { type: "number" } },
28
+ },
29
+ body: {
30
+ type: "object",
31
+ properties: {
32
+ displayName: { type: ["string", "null"] },
33
+ email: { type: ["string", "null"] },
34
+ position: { type: ["string", "null"] },
35
+ },
36
+ },
37
+ };
38
+ put.response = {
39
+ type: "object",
40
+ required: ["ok"],
41
+ properties: { ok: { type: "boolean" } },
42
+ };
43
+
44
+ export default { put };
@@ -4,6 +4,122 @@
4
4
  import { z } from "@oj-module/runtime";
5
5
 
6
6
  export const schemas = {
7
+ "systemDepartmentsGet": {
8
+ data: z.array(z.object({
9
+ "id": z.number(),
10
+ "origin": z.string(),
11
+ "originId": z.string(),
12
+ "parentId": z.number().nullable(),
13
+ "name": z.string().nullable(),
14
+ "nameEn": z.string().nullable().optional(),
15
+ "orderNo": z.number(),
16
+ "syncVisible": z.boolean(),
17
+ "deleted": z.boolean(),
18
+ "memberCount": z.number()
19
+ })),
20
+ },
21
+ "systemDepartmentsPost": {
22
+ body: z.object({
23
+ "name": z.string().min(1),
24
+ "parentId": z.number().nullable().optional(),
25
+ "nameEn": z.string().nullable().optional(),
26
+ "orderNo": z.number().optional()
27
+ }),
28
+ data: z.object({
29
+ "id": z.number()
30
+ }),
31
+ },
32
+ "systemDepartmentsPut": {
33
+ query: z.object({
34
+ "id": z.number()
35
+ }),
36
+ body: z.object({
37
+ "name": z.string().min(1).optional(),
38
+ "nameEn": z.string().nullable().optional(),
39
+ "orderNo": z.number().optional()
40
+ }),
41
+ data: z.object({
42
+ "ok": z.boolean()
43
+ }),
44
+ },
45
+ "systemDepartmentsDelete": {
46
+ query: z.object({
47
+ "id": z.number()
48
+ }),
49
+ data: z.object({
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
+ }),
63
+ },
64
+ "systemDepartmentsDetachPost": {
65
+ query: z.object({
66
+ "id": z.number()
67
+ }),
68
+ data: z.object({
69
+ "ok": z.boolean()
70
+ }),
71
+ },
72
+ "systemDepartmentsMembersGet": {
73
+ query: z.object({
74
+ "id": z.number()
75
+ }),
76
+ data: z.array(z.object({
77
+ "userId": z.number(),
78
+ "deptId": z.number(),
79
+ "orderNo": z.number(),
80
+ "isLeader": z.boolean(),
81
+ "isMain": z.boolean(),
82
+ "username": z.string(),
83
+ "displayName": z.string().nullable().optional()
84
+ })),
85
+ },
86
+ "systemDepartmentsMembersPost": {
87
+ query: z.object({
88
+ "id": z.number()
89
+ }),
90
+ body: z.object({
91
+ "userId": z.number(),
92
+ "isLeader": z.boolean().optional(),
93
+ "isMain": z.boolean().optional(),
94
+ "orderNo": z.number().optional()
95
+ }),
96
+ data: z.object({
97
+ "ok": z.boolean()
98
+ }),
99
+ },
100
+ "systemDepartmentsMembersPut": {
101
+ query: z.object({
102
+ "id": z.number(),
103
+ "userId": z.number()
104
+ }),
105
+ body: z.object({
106
+ "isLeader": z.boolean().optional(),
107
+ "isMain": z.boolean().optional(),
108
+ "orderNo": z.number().optional()
109
+ }),
110
+ data: z.object({
111
+ "ok": z.boolean()
112
+ }),
113
+ },
114
+ "systemDepartmentsMembersDelete": {
115
+ query: z.object({
116
+ "id": z.number(),
117
+ "userId": z.number()
118
+ }),
119
+ data: z.object({
120
+ "ok": z.boolean()
121
+ }),
122
+ },
7
123
  "systemLoginMethodsGet": {
8
124
  data: z.object({
9
125
  "methods": z.array(z.string())
@@ -119,6 +235,12 @@ export const schemas = {
119
235
  "username": z.string(),
120
236
  "display_name": z.string().nullable(),
121
237
  "email": z.string().nullable(),
238
+ "position": z.string().nullable(),
239
+ "departments": z.array(z.object({
240
+ "id": z.number(),
241
+ "name": z.string().nullable(),
242
+ "isMain": z.boolean()
243
+ })),
122
244
  "is_forbidden": z.boolean(),
123
245
  "must_change": z.boolean(),
124
246
  "roles": z.array(z.string())
@@ -133,6 +255,27 @@ export const schemas = {
133
255
  }),
134
256
  data: z.object({
135
257
  "id": z.number()
258
+ }),
259
+ },
260
+ "systemUsersDelete": {
261
+ query: z.object({
262
+ "uid": z.number()
263
+ }),
264
+ data: z.object({
265
+ "ok": z.boolean()
266
+ }),
267
+ },
268
+ "systemUsersProfilePut": {
269
+ query: z.object({
270
+ "uid": z.number()
271
+ }),
272
+ 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()
136
279
  }),
137
280
  },
138
281
  "systemUsersForbiddenPut": {