@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.
- package/devkit/SKILL.md +1 -1
- package/devkit/framework-dev.md +2 -2
- package/devkit/manual.md +24 -14
- package/devkit/module-dev-and-cdn-playground.md +2 -2
- package/devkit/wecom-daikaifa.md +237 -14
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/templates-tenant-scope.test.ts +7 -2
- package/src/init.ts +1 -1
- package/src/market/__tests__/bootstrap.test.ts +1 -1
- package/src/market/__tests__/setup.test.ts +7 -7
- package/src/market/bootstrap.ts +1 -1
- package/templates/README.md +1 -1
- package/templates/init.include.yaml +12 -0
- package/templates/openapi.json +2863 -549
- package/templates/src/_platform/api/_shared/service/domain/contacts.ts +7 -0
- package/templates/src/_platform/api/_shared/service/domain/org-read.ts +451 -0
- package/templates/src/_platform/api/_shared/service/domain/org.ts +566 -0
- package/templates/src/_platform/api/_shared/service/domain/users.ts +140 -2
- package/templates/src/_platform/api/_shared/service/infra/query.ts +2 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +1 -1
- package/templates/src/_platform/api/manifest.yaml +3 -0
- package/templates/src/_platform/api/migrations/0016__org.mysql.sql +41 -0
- package/templates/src/_platform/api/migrations/0016__org.pg.sql +40 -0
- package/templates/src/_platform/api/migrations/0016__org.sql +62 -0
- package/templates/src/_platform/api/org/departments/api.ts +51 -0
- package/templates/src/_platform/api/org/is-member/api.ts +33 -0
- package/templates/src/_platform/api/org/members/api.ts +55 -0
- package/templates/src/_platform/api/org/picker/api.ts +59 -0
- package/templates/src/_platform/api/org/resolve/api.ts +38 -0
- package/templates/src/_platform/api/org/tree/api.ts +37 -0
- package/templates/src/_platform/api/org/user/api.ts +46 -0
- package/templates/src/_platform/api/schema.yaml +48 -0
- package/templates/src/_platform/api/seed.mysql.sql +10 -5
- package/templates/src/_platform/api/seed.pg.sql +10 -5
- package/templates/src/_platform/api/seed.sql +13 -5
- package/templates/src/_platform/web/__client/api.schemas.ts +126 -0
- package/templates/src/_platform/web/__client/api.ts +170 -1
- package/templates/src/auth/api/_shared/ldap-login.ts +2 -0
- package/templates/src/auth/api/_shared/session.ts +5 -1
- package/templates/src/auth/api/_shared/wecom/admin.ts +3 -1
- package/templates/src/auth/api/_shared/wecom/client.ts +243 -31
- package/templates/src/auth/api/_shared/wecom/config.ts +12 -1
- package/templates/src/auth/api/_shared/wecom/contacts.ts +263 -0
- package/templates/src/auth/api/_shared/wecom/crypt.ts +17 -7
- package/templates/src/auth/api/_shared/wecom/events.ts +111 -26
- package/templates/src/auth/api/_shared/wecom/login.ts +152 -1
- package/templates/src/auth/api/_shared/wecom/payload.ts +116 -0
- package/templates/src/auth/api/_shared/wecom/store.ts +65 -0
- package/templates/src/auth/api/_shared/wecom/sync.ts +487 -0
- package/templates/src/auth/api/manifest.yaml +3 -0
- package/templates/src/auth/api/migrations/0002__org_sync.mysql.sql +28 -0
- package/templates/src/auth/api/migrations/0002__org_sync.pg.sql +27 -0
- package/templates/src/auth/api/migrations/0002__org_sync.sql +41 -0
- package/templates/src/auth/api/schema.yaml +31 -0
- package/templates/src/auth/api/wecom/admin/api.ts +3 -1
- package/templates/src/auth/api/wecom/api.ts +100 -18
- package/templates/src/auth/api/wecom/sync/api.ts +199 -0
- package/templates/src/auth/web/__client/api.schemas.ts +97 -0
- package/templates/src/auth/web/__client/api.ts +73 -1
- package/templates/src/notification/api/seed.sql +1 -1
- package/templates/src/system/api/departments/api.ts +142 -0
- package/templates/src/system/api/departments/detach/api.ts +34 -0
- package/templates/src/system/api/departments/members/api.ts +142 -0
- package/templates/src/system/api/departments/move/api.ts +39 -0
- package/templates/src/system/api/users/api.ts +45 -5
- package/templates/src/system/api/users/profile/api.ts +44 -0
- package/templates/src/system/web/__client/api.schemas.ts +143 -0
- package/templates/src/system/web/__client/api.ts +293 -1
- package/templates/src/system/web/entry.ts +6 -0
- package/templates/src/system/web/locales/en-US.json +112 -6
- package/templates/src/system/web/locales/zh-CN.json +112 -6
- package/templates/src/system/web/pages/departments/index.tsx +346 -0
- package/templates/src/system/web/pages/departments/sync-bar.tsx +257 -0
- package/templates/src/system/web/pages/departments/tree.ts +66 -0
- package/templates/src/system/web/pages/errors.ts +1 -1
- package/templates/src/system/web/pages/users/index.tsx +49 -7
- package/templates/src/system/web/pages/wecom/index.tsx +14 -0
- package/templates/tests/l1-builder.test.ts +1 -1
- package/templates/tests/org-departments.spec.ts +311 -0
- package/templates/tests/org-read.spec.ts +469 -0
- package/templates/tests/rbac-contacts.spec.ts +13 -3
- package/templates/tests/rbac-seed.spec.ts +11 -0
- package/templates/tests/seed-role-accounts.spec.ts +6 -2
- package/templates/tests/wecom-admin.spec.ts +1 -0
- package/templates/tests/wecom-connector.spec.ts +115 -8
- package/templates/tests/wecom-crypt.spec.ts +12 -3
- package/templates/tests/wecom-endpoints.spec.ts +182 -14
- package/templates/tests/wecom-org-sync.spec.ts +598 -0
- package/templates/tests/wecom-sync.spec.ts +286 -0
- package/vendor/host-versions.json +3 -3
- 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
|
|
38
|
-
|
|
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
|
-
|
|
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": {
|