@oj-module/cli 0.2.46 → 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 +282 -27
- 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/config.yaml +26 -7
- 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 +17 -10
- package/templates/src/auth/api/_shared/wecom/client.ts +311 -40
- package/templates/src/auth/api/_shared/wecom/config.ts +40 -17
- 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 +7 -5
- 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/login/web/auth-provider.ts +9 -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 +113 -7
- package/templates/src/system/web/locales/zh-CN.json +113 -7
- 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 +16 -1
- 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 +64 -42
- package/templates/tests/wecom-client-timeout.spec.ts +99 -0
- package/templates/tests/wecom-connector.spec.ts +115 -11
- 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
package/devkit/wecom-daikaifa.md
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
| 你是 | 读 | 大约要做什么 |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| 部署 / 服务商运营 | §2 §3 §4 §7 §8 |
|
|
15
|
+
| 部署 / 服务商运营 | §2 §3 §4 §7 §8 | 备凭据、填后台、按顺序开通、日常启停与轮换;真机验收照 **§7.6** 打勾 |
|
|
16
16
|
| 本工程开发者 | §1 §6 §8 | 本地联调、加企业、改前端、写没有真实套件的自动化测试 |
|
|
17
|
+
| 企业管理员(客户的 IT) | §2.5 §2.6 §6.8 §7.6-B §8 | 开四项通讯录权限、选对授权模式、读懂同步页面上的卡片 |
|
|
17
18
|
| 企业员工(用户) | §5 | 怎么进、要不要注册、进不去怎么办 |
|
|
18
19
|
|
|
19
20
|
---
|
|
@@ -45,15 +46,33 @@
|
|
|
45
46
|
- 「哪个企业装了哪个应用」**不写在配置文件里**:新增企业只是多一行数据,不改配置、不重启。
|
|
46
47
|
- JIT(Just-In-Time)建档:员工第一次登录时自动建本地账号,用户名 `wecom:<企微userid>`。
|
|
47
48
|
|
|
49
|
+
第四条链路是**组织架构同步**(前三条是登录,这条是通讯录);它同样是「授权一次、之后自动跑」:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
企业管理员授权(auth_mode = 管理员授权,且四项通讯录权限已生效,§2.5)
|
|
53
|
+
│
|
|
54
|
+
├─ 轨一 登录自更新:某员工登录时顺手 user/get 一次(24h 节流)→ 补姓名/职务/部门
|
|
55
|
+
├─ 轨二 全量同步:/system/departments 点「立即同步」→ department/simplelist + user/list
|
|
56
|
+
│ └─ 分片推进(一片 12 秒)+ 调用方轮询 → 进度落在 wecom_sync_job
|
|
57
|
+
└─ 轨三 事件增量:change_contact(成员/部门增删改即时落库;失败只记日志,等轨二兜底)
|
|
58
|
+
↓
|
|
59
|
+
sys_department / sys_dept_member(origin='wecom')+ users(activated=0)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **归属权**(`origin`)决定谁能改:`local` 的行同步一根毛都不碰;`wecom` 的行管理员只能「转为本地维护」
|
|
63
|
+
之后才改得动,改了之后同步也不再覆盖。
|
|
64
|
+
- **可见范围永远是子集**:代开发只能拉到「本应用可见范围内」的部门,授权也解不开(§2.4)——
|
|
65
|
+
页面上「N 个部门因权限被跳过」通常就是它。
|
|
66
|
+
|
|
48
67
|
---
|
|
49
68
|
|
|
50
69
|
## 2. 凭据清单
|
|
51
70
|
|
|
52
|
-
### 2.1
|
|
71
|
+
### 2.1 八项部署常量(全部填在工程根 `config.yaml` 的 `vars:` 段)
|
|
53
72
|
|
|
54
|
-
一个部署 =
|
|
73
|
+
一个部署 = 一套代开发模板,所以这八项是**部署维度**的常量,不进数据库。
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
八项分三层,**来源与必填时序不同**——这是破解「先有 suite 还是先有回调」死锁的关键
|
|
57
76
|
(详见 plan §18):
|
|
58
77
|
|
|
59
78
|
- **Tier A(部署方本地生成,创建模版前即可配齐)**:`wecom_callback_token` /
|
|
@@ -61,19 +80,32 @@
|
|
|
61
80
|
回调验证,**不依赖企业微信下发**;即使 `suite_id` / `suite_secret` 还空着,服务也能跑通 echostr。
|
|
62
81
|
- **Tier B(企业微信在「代开发模版创建成功」后下发)**:`wecom_suite_id` / `wecom_suite_secret`。
|
|
63
82
|
只影响登录 / 授权等 suite 流程(login / callback / create_auth);**回调 URL 验证(echostr)不依赖它们**。
|
|
83
|
+
- **Tier C(服务商级凭证,只供「生成开通链接」用)**:`wecom_provider_id`(= 服务商后台
|
|
84
|
+
**服务商信息**页的 CorpID)/ `wecom_provider_secret`(= 服务商后台 **应用管理 → 通用开发参数** 点「编辑」
|
|
85
|
+
后的 `ProviderSecret` 字段,**不在**服务商信息页,也**不是** suite 的 secret)。代开发的开通入口走
|
|
86
|
+
`get_customized_auth_url`,该接口只认 `provider_access_token`,而它必须由服务商 corpid + provider_secret 换得。
|
|
87
|
+
缺它**不影响登录**(login 走 suite 流程),但 `/system/wecom` 点「生成开通链接」会 400 点名缺这两项。
|
|
64
88
|
|
|
65
89
|
Tier A 任一为空 → 服务启动即 400 拒绝(宁可不登录,也不明文存 `permanent_code`)。
|
|
66
90
|
Tier B 为空时服务仍可启动并响应 echostr,但 suite 流程会 400——先配 Tier A 把回调验证跑通、
|
|
67
|
-
创建模版、拿到 suite 凭据,再补全 Tier B 即可,无需鸡生蛋。
|
|
91
|
+
创建模版、拿到 suite 凭据,再补全 Tier B 即可,无需鸡生蛋。Tier C 可与 Tier B 同一阶段补全。
|
|
92
|
+
|
|
93
|
+
> 📌 **官方术语对齐**:企微服务商后台「代开发应用 → 模板详情 → **基本信息**」页里,
|
|
94
|
+
> 这两项就直接标作 **模板ID** 与 **模板Secret**(英文即 SuiteID / SuiteSecret)。
|
|
95
|
+
> 也就是说:本仓的 `wecom_suite_id` ← 官方「模板ID」,`wecom_suite_secret` ← 官方「模板Secret」,
|
|
96
|
+
> **不是**企业的 CorpID / CorpSecret,也**不是**某个自建应用的 Secret。照着「基本信息」页抄这两格即可。
|
|
97
|
+
> 两者都是「创建模板成功后」才出现在该页,创建前只有 Tier A 的 Token / EncodingAESKey 可设。
|
|
68
98
|
|
|
69
99
|
| # | 变量 | 层级 | 从哪取 | 格式 / 校验 | 配错或缺失的后果 |
|
|
70
100
|
|---|---|---|---|---|---|
|
|
71
|
-
| 1 | `wecom_suite_id` | Tier B | 企微服务商后台 → 应用管理 → 代开发应用 →
|
|
72
|
-
| 2 | `wecom_suite_secret` | Tier B |
|
|
101
|
+
| 1 | `wecom_suite_id` | Tier B | 企微服务商后台 → 应用管理 → 代开发应用 → 模板详情 → **基本信息**页(**创建成功后**才有),标作「**模板ID**」,即 **SuiteID**(形如 `ww…` / `wwb…`) | 非空字符串 | Tier A 配齐时服务可启动并验证回调;此项空 → login/callback/create_auth 等 suite 流程 400 `config vars 缺少 wecom_suite_id`;`event.post` 还会判 `SuiteId 与本部署配置不一致` |
|
|
102
|
+
| 2 | `wecom_suite_secret` | Tier B | 同页「基本信息」标作「**模板Secret**」,即 **SuiteSecret**(可重置) | 非空字符串;重置后旧值立即失效 | 换不出 `suite_access_token` → 502 `企微 get_suite_token 失败:errcode=40001 …`;开通链接生成也失败 |
|
|
73
103
|
| 3 | `wecom_callback_token` | Tier A | 同页 **Token**(自设,字母数字 3–32 位) | 与企微后台**逐字一致** | 保存回调 URL 时 `echostr` 解不解得开;事件被判验签失败,企微持续重推 |
|
|
74
|
-
| 4 | `wecom_encoding_aes_key` | Tier A | 同页 **EncodingAESKey
|
|
104
|
+
| 4 | `wecom_encoding_aes_key` | Tier A | 同页 **EncodingAESKey**(自设,43 字符) | **43 字符**,且与企微后台**逐字一致**(差一个字符就是 `pkcs7: bad padding`) | 回调报文解不开、`suite_ticket` 永远收不到;企业管理员扫码后提示「应用安装失败」 |
|
|
75
105
|
| 5 | `wecom_enc_key` | Tier A | **自己生成**,见 §2.4 | hex 32 字符(16 字节)或 64 字符(32 字节),或 base64 解码后 16 / 32 字节。**AES-192(24 字节)不支持** | 400 `企业微信配置错误:wecom_enc_key 必须是 16 或 32 字节的 hex(32/64 字符)或 base64` |
|
|
76
|
-
| 6 | `wecom_base_url` | Tier A |
|
|
106
|
+
| 6 | `wecom_base_url` | Tier A | **基址**:你站点的对外地址 + `/api/auth/wecom`(注意这是基址,**不是**企微后台要填的回调 URL) | **绝对地址、无尾斜杠、必须以 `/api/auth/wecom` 结尾** | 400 `wecom_base_url 必须是绝对地址` 或 `必须以 /api/auth/wecom 结尾`;配错还会让登录回跳落到别的应用或 404 |
|
|
107
|
+
| 7 | `wecom_provider_id` | Tier C | 服务商后台 → **服务商信息**页显示的 CorpID(服务商自己的 corpid,不是企业 corpid,也不是 suite_id) | 非空字符串 | 只影响「生成开通链接」:`get_provider_token` 换不出 `provider_access_token` → 生成链接 502;此项为空 → 生成链接 400 `config vars 缺少 wecom_provider_id` |
|
|
108
|
+
| 8 | `wecom_provider_secret` | Tier C | 服务商后台 → **应用管理 → 通用开发参数**(**点「编辑」按钮**那一栏的 `ProviderSecret` 字段)。**不在「服务商信息」页**(那里只显示 CorpID),也**不是** suite_secret | 非空字符串;重置后旧值立即失效 | 换不出 `provider_access_token` → 生成链接 502 `企微 get_provider_token 失败:errcode=40001 …`;此项为空 → 生成链接 400 `config vars 缺少 wecom_provider_secret` |
|
|
77
109
|
|
|
78
110
|
`wecom_base_url` 是唯一「配一处、派生两处」的值——它同时决定:
|
|
79
111
|
|
|
@@ -82,6 +114,17 @@ Tier B 为空时服务仍可启动并响应 echostr,但 suite 流程会 400—
|
|
|
82
114
|
- 登录成功后的站点回跳 = 去掉 `/api/auth/wecom` 的那部分(子路径部署也正确,例如
|
|
83
115
|
`https://example.com/oj/api/auth/wecom` → 回跳 `https://example.com/oj/login`)
|
|
84
116
|
|
|
117
|
+
> ⚠️ **`wecom_base_url` 不是回调 URL 本身,别把它直接填进企微后台。** 企微后台要填的
|
|
118
|
+
> 「指令回调 URL / 应用回调 URL」是 `<wecom_base_url>/event`(**必须带 `/event`**)。
|
|
119
|
+
> 裸的 `/api/auth/wecom` 没有任何 handler,企微验签时会打到它 → **404 → 验证失败、模版保存不了**。
|
|
120
|
+
> 派生关系如上:回调 URL = base + `/event`,网页授权回跳 = base + `/callback`。
|
|
121
|
+
>
|
|
122
|
+
> **别给这个 URL 加 `?callback_format=json`。** 这个参数只在「对外收款」的指令回调文档(97478)里有说明;
|
|
123
|
+
> 代开发 / 第三方应用所属的**通用回调配置**(96238 §3.2)只写 XML 信封,「部门通知事件」(92655)
|
|
124
|
+
> 虽然给了 JSON 示例、但示例地址同样不带它 —— 也就是说**我们这类回调没有文档依据**。
|
|
125
|
+
> 服务端两种包体都收(归一化在 `payload.ts`,XML / JSON 谁先来都不影响),所以哪天上游默认改 JSON
|
|
126
|
+
> 也不用动这里;但在文档给出依据前,地址就照文档写,别自己加参数。
|
|
127
|
+
|
|
85
128
|
### 2.2 两项备案(都在企微后台,不写在代码里)
|
|
86
129
|
|
|
87
130
|
| 备案项 | 填什么 | 不做的症状 |
|
|
@@ -113,14 +156,48 @@ openssl rand -hex 32 # 输出 64 个 hex 字符,直接贴进 wecom_enc_
|
|
|
113
156
|
用新 key 重写。轮换前后要留旧 key 用于读取,直到所有企业都重授权完毕。
|
|
114
157
|
- 与数据库分开保管:`db.sqlite` 备份 + `wecom_enc_key` 备份,**缺任何一个都等于全部企业要重新授权**。
|
|
115
158
|
|
|
159
|
+
### 2.5 组织架构四项权限(同步能不能拿到「人话」全看这四项)
|
|
160
|
+
|
|
161
|
+
组织架构同步读的是通讯录接口,能不能读到**姓名 / 部门名 / 职务 / 完整部门树**,由四项权限决定。
|
|
162
|
+
官方把它们分成两类,处置方式完全不同:
|
|
163
|
+
|
|
164
|
+
| # | 权限 | 类型 | 没申请的症状 |
|
|
165
|
+
|---|---|---|---|
|
|
166
|
+
| ① | 成员基本信息 → **姓名** | 基础 | 满屏工号(`name` 回成 userid) |
|
|
167
|
+
| ② | 成员基本信息 → **部门名** | 基础 | **一棵全无名节点的树**——注意它**不在**「组织架构」权限下,是最容易漏的一项 |
|
|
168
|
+
| ③ | 成员基本信息 → **职务** | 自定义 | `users.position` 一直是空 |
|
|
169
|
+
| ④ | 组织架构 → **部门及父部门 ID、部门负责人** | 自定义 | `department[]` 不完整,只拿得到可见范围内的部门 id |
|
|
170
|
+
|
|
171
|
+
- **基础**权限扫码即得、不可编辑;**自定义**权限每次编辑保存会向企业推一条权限变更消息(`97201`),
|
|
172
|
+
中间存在「**待企业客户确认**」状态,**企业没确认就不生效**。
|
|
173
|
+
- 重发开通链接只是**幂等 upsert**,**补不了权限**——必须企业管理员在企微后台确认。
|
|
174
|
+
- 本系统**不画**「未申请 / 待企业确认 / 已生效」三态:企微没有开放查询权限授予状态的接口,
|
|
175
|
+
画出来的三态只能靠猜。替代做法是**按症状反推**——同步完成或失败时页面会给自助卡片
|
|
176
|
+
(见 §6.8 与 §8),卡片直说「缺少通讯录权限」,对照上表自查即可。这是取舍,不是遗漏。
|
|
177
|
+
|
|
178
|
+
### 2.6 成员授权模式(阻断级:这种企业装上了也同步不了)
|
|
179
|
+
|
|
180
|
+
企业授权时选的是「**管理员授权**」还是「**成员授权**」,`get_permanent_code` 的
|
|
181
|
+
`auth_info.agent[].auth_mode` 会把它带回来(0=管理员 / 1=成员)。**成员授权模式下应用没有任何部门权限**,
|
|
182
|
+
`department/*`、`user/*` 一律 `60011`——跑同步只会得到一片无权限错误。
|
|
183
|
+
|
|
184
|
+
本系统把它落进 `wecom_app_profile.auth_mode`,并在三处用上:
|
|
185
|
+
|
|
186
|
+
- `POST /auth/wecom/sync/start`:`auth_mode=1` → **400 明确文案,一个上游请求都不发**;
|
|
187
|
+
- 组织架构页:直接给「该企业为成员授权模式」卡片,**不给同步按钮**(点了必然是 400);
|
|
188
|
+
- 代开发后台清单:「授权模式」列让服务商一眼看出哪家企业「装了但同步不了」。
|
|
189
|
+
|
|
190
|
+
企业管理员在企微后台把模式改回「管理员授权」后,企微只推一个 `change_auth`(**不带 AuthCode**),
|
|
191
|
+
本系统会用已存的 `permanent_code` 反查 `get_auth_info` 刷新 `auth_mode`,不需要重新开通。
|
|
192
|
+
|
|
116
193
|
---
|
|
117
194
|
|
|
118
195
|
## 3. 要填进企微后台的地址(逐字)
|
|
119
196
|
|
|
120
197
|
| 企微后台位置 | 值 | 说明 |
|
|
121
198
|
|---|---|---|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
199
|
+
| **模板**回调 URL(指令回调 / 事件接收) | `https://<站点>/api/auth/wecom/event` | 点保存时企微会 GET 一次带 `echostr`:必须**当次就通**,否则保存不了。`suite_ticket` 与 `create_auth` / `change_auth` / `cancel_auth` 都走这里 |
|
|
200
|
+
| **应用**回调 URL(数据回调 / 通讯录变更) | **同上** `https://<站点>/api/auth/wecom/event` | 一个端点吃两种回调,不用再开路由。`change_contact`(成员/部门增删改)走的就是这里,收到即落库、**不必等下一次全量同步**;处理失败只记一条 `wecom change_contact ignored …` 并照旧回 `success`(否则企微重推三次) |
|
|
124
201
|
| 可信域名 | `https://<站点域名>` | 不含路径 |
|
|
125
202
|
| 网页授权 `redirect_uri` | **不用手填**:后端按 `wecom_base_url` 拼出 `https://<站点>/api/auth/wecom/callback` | 企业的授权页里的域名必须与可信域名一致 |
|
|
126
203
|
|
|
@@ -149,12 +226,21 @@ openssl rand -hex 32 # 输出 64 个 hex 字符,直接贴进 wecom_enc_
|
|
|
149
226
|
| 步 | 动作 | 为什么必须在这一步 | 错序的症状 |
|
|
150
227
|
|---|---|---|---|
|
|
151
228
|
| 3 | 模版创建成功后,从模版详情拿 **SuiteID / SuiteSecret**,填入 §2.1 的 **Tier B**,重启服务 | 否则 `suite_ticket` 推来时 `event.post` 因缺 suite 凭据 400,票据永远收不到 | 日志看不到 `wecom suite_ticket updated`;`/system/wecom` 生成链接报 503 |
|
|
152
|
-
| 4 | **等 `suite_ticket` 到手**(企微每 10 分钟推一次) | 换 `suite_access_token`
|
|
153
|
-
| 5 |
|
|
154
|
-
| 6 |
|
|
229
|
+
| 4 | **等 `suite_ticket` 到手**(企微每 10 分钟推一次) | 换 `suite_access_token` 必须有票据,而票据只能由回调推来(登录流程要用) | 503 `suite_ticket 尚未收到:回调 URL 未打通或企微尚未推送` |
|
|
230
|
+
| 5 | 配好 §2.1 的 **Tier C**(`wecom_provider_id` / `wecom_provider_secret`,服务商后台「通用开发参数」页取),重启服务;再用超级管理员登录,进 `/system/wecom` → 「生成开通链接」 | 代开发的开通入口走 `get_customized_auth_url`(返回 `qrcode_url`),它只认 `provider_access_token`,而 provider_access_token 必须由服务商 corpid + provider_secret 换得(= Tier C)。**不是**第三方应用的 `get_pre_auth_code`(代开发调它会 48002) | 400 `config vars 缺少 wecom_provider_id / wecom_provider_secret`;或 502 `get_provider_token 失败`(provider_secret 错/重置);或链接生成失败 |
|
|
231
|
+
| 6 | 把链接/二维码发给企业管理员,由其扫码/打开授权 | 只有企业管理员能授权;确认后企微推 `create_auth` | 企业侧没有应用;本站无该企业记录 |
|
|
155
232
|
| 7 | 进 `/system/settings` 打开「启用企业微信登录」 | `wecom.enabled` 默认 **关**,只是「发现登录方式」的开关 | 登录页根本看不到「企业微信」页签 |
|
|
156
233
|
| 8 | 企业员工从企微工作台/扫码/专属链接进入 | 首次登录自动建档 | 401 `登录凭据无效或不在应用可见范围内`(多半是可见范围没配) |
|
|
157
234
|
|
|
235
|
+
> 🔐 **进 `/system/wecom` 必须用 `superadmin` 账号,不是 `admin`。** 该页是跨租户的服务商
|
|
236
|
+
> 运营面(生成开通链接 = 调企微 `get_customized_auth_url` 换 `qrcode_url`、管理所有已授权企业),前端(`system/web/pages/wecom/index.tsx:61`
|
|
237
|
+
> 的 `roles.includes("superadmin")`)与后端 `auth/wecom/admin/*`(每个端点 `assertSuperAdmin()`)
|
|
238
|
+
> 都只放行 `superadmin`。模板 seed 已把 `admin` 从 `superadmin` 角色解绑(收敛为「账号名 = 角色名」,
|
|
239
|
+
> 见 `_platform/api/seed.sql` 的 `DELETE … WHERE role_name='superadmin' AND username='admin'`),
|
|
240
|
+
> 所以拿 `admin` 登录会直接显示「无权限」。请用 `superadmin` 登录(seed 口令 `aA123456`,
|
|
241
|
+
> **登录后立即改密**);若坚持让 `admin` 也能进,由 `superadmin` 在 `/system/roles` 把 `admin`
|
|
242
|
+
> 绑到 `superadmin`,但不推荐——这会取消「日常管理员 vs 跨租户运营面」的隔离。
|
|
243
|
+
|
|
158
244
|
> **为什么第 4 步容易漏**:`suite_ticket` 是被推送的,不是拉取的。第一次部署时如果回调
|
|
159
245
|
> URL 保存得晚,就要等下一个 10 分钟周期。看到 503 不必怀疑代码——先确认回调真的通。
|
|
160
246
|
> **阶段二衔接的新坑**:若第 3 步的 suite 凭据还没填、企微就推来了 `suite_ticket`,那几条会被
|
|
@@ -211,7 +297,7 @@ openssl rand -hex 32 # 输出 64 个 hex 字符,直接贴进 wecom_enc_
|
|
|
211
297
|
企微只回调**公网 https**,所以本地必须内网穿透。最小闭环:
|
|
212
298
|
|
|
213
299
|
```sh
|
|
214
|
-
# 1)
|
|
300
|
+
# 1) 八项 vars 填进工程根 config.yaml 的 vars:(见 §2.1,含 Tier C 的 provider 凭证)
|
|
215
301
|
# 其中 wecom_base_url 先写成穿透地址:https://<穿透域名>/api/auth/wecom
|
|
216
302
|
# 2) 起前后端
|
|
217
303
|
pnpm dev
|
|
@@ -242,33 +328,64 @@ pnpm dev
|
|
|
242
328
|
- 开通链接支持环境标记:`state` 传 `env=test`(或 `test` / `staging` / `dev`)会落到
|
|
243
329
|
profile 的 `env` 列,缺省 `prod`。同一套代码区分「测试企业」与「正式企业」就靠它。
|
|
244
330
|
|
|
245
|
-
### 6.3 数据面:三张共享表 +
|
|
331
|
+
### 6.3 数据面:三张共享表 + 五张租户表
|
|
246
332
|
|
|
247
333
|
| 表 | 是否租户表 | 存什么 | 为什么这样放 |
|
|
248
334
|
|---|---|---|---|
|
|
249
|
-
| `wecom_app_profile` | **共享**(`tenant: false`) | 企业 × 应用 → 密文 `permanent_code`、`corp_name`、`env`、`status` | 它是「corp → 租户」映射的**权威表**;登录前还没确定租户,必须是部署级 |
|
|
335
|
+
| `wecom_app_profile` | **共享**(`tenant: false`) | 企业 × 应用 → 密文 `permanent_code`、`corp_name`、`env`、`status`、`auth_mode` | 它是「corp → 租户」映射的**权威表**;登录前还没确定租户,必须是部署级 |
|
|
250
336
|
| `wecom_suite_ticket` | **共享** | 每个部署一条 `suite_ticket` + 过期时刻 | 套件凭据属于整个部署,不属于某个企业 |
|
|
251
337
|
| `wecom_login_ticket` | **共享** | 一次性登录票据的 **sha256** + 归属租户 + `used` | 落地换票发生在「已知租户但尚未建会话」的瞬间;只存哈希,泄库不可重放 |
|
|
252
|
-
| `
|
|
338
|
+
| `wecom_sync_job` | **租户** | 同步任务的状态 / 阶段 / 游标 / 计数 / 脱敏错码 | 同步只在**调用方自己的租户**里跑(D7),跨租户读元数据要走显式 `tenant_id` 的裸 SQL |
|
|
339
|
+
| `wecom_sync_lock` | **租户** | 抢锁(`tenant_id + agent_id` 复合主键) | 同上;「插入撞主键」是唯一可靠的原子判据 |
|
|
340
|
+
| `sys_department` / `sys_dept_member` | **租户** | 部门树 / 部门成员(`origin` 标归属权) | 组织架构属于某个企业;`origin='wecom'` 的行归同步管 |
|
|
341
|
+
| `users` | **租户** | JIT 建的 `wecom:<userid>` 账号(`activated=0`) | 账号天然属于某个企业,`tenant_id = corp_id` |
|
|
253
342
|
|
|
254
343
|
三张共享表已列进 `config.yaml` 的 `tenant.shared_allow`。**不要**把它们改成租户表:
|
|
255
344
|
登录路径在 `ensureTenant` 之前就要读 profile,改成租户表会在守卫那里直接 403。
|
|
345
|
+
反之,两张同步表**不要**加进 `shared_allow`——它们存的是租户业务状态(进度、跳过计数),
|
|
346
|
+
共享化等于让所有企业的同步进度混在一张无人守卫的表里。
|
|
256
347
|
|
|
257
348
|
票据换会话的顺序是刚性的:**先绑租户,再抢票据(`used` 0→1 原子)**,然后才校验过期——
|
|
258
349
|
重放与并发双花都只会成功一次。
|
|
259
350
|
|
|
351
|
+
### 6.3.1 同步怎么跑:分片 + 轮询(不是后台长任务)
|
|
352
|
+
|
|
353
|
+
服务端 isolate **没有定时器**,路由也有超时(全局 `server.timeout` 缺省 30 秒),所以同步被切成
|
|
354
|
+
「一片一片推」:
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
POST /auth/wecom/sync/start 一次 = 推一片(预算 12 秒)→ 返回当前进度
|
|
358
|
+
GET /auth/wecom/sync/status 只读本地行,不打上游(页面 1~2 秒轮询一次)
|
|
359
|
+
POST /auth/wecom/sync/cancel 置 cancelled 并清游标;**不回滚**已写入的数据(同步幂等,重跑即覆盖)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
- 三个阶段固定顺序:`departments`(拉全量部门,按父子序落库)→ `members`(逐部门拉人)→
|
|
363
|
+
`reconcile`(墓碑回收)。游标存在 `wecom_sync_job.cursor_text`,**续跑就是再调一次 start**。
|
|
364
|
+
- 状态四态:`running`(还有下一片)/ `done` / `failed`(连续 10 次致命错熔断)/ `cancelled`。
|
|
365
|
+
页面看到 `running` 就继续调 start,看到其余三个就停。
|
|
366
|
+
- **限速 10 次/分**(`orgsync:<uid>`)与「一片 12 秒」是配对的:串行驱动天然 5 次/分,
|
|
367
|
+
10 次是不挡正常推进的余量。改一个必须看另一个。
|
|
368
|
+
- 抢锁:同一企业同一应用同时只允许一个同步,重复 start 会 409。
|
|
369
|
+
|
|
260
370
|
### 6.4 代开发后台(`/system/wecom`,仅超级管理员可见)
|
|
261
371
|
|
|
262
372
|
三件事,各对应一个端点(都在 `src/auth/api/wecom/admin/`):
|
|
263
373
|
|
|
264
374
|
| 界面动作 | 端点 | 说明 |
|
|
265
375
|
|---|---|---|
|
|
266
|
-
| 已开通企业清单 | `GET /auth/wecom/admin/apps` | **不返回** `permanent_code`,最多到 `corp_name` |
|
|
376
|
+
| 已开通企业清单 | `GET /auth/wecom/admin/apps` | **不返回** `permanent_code`,最多到 `corp_name`;带 `auth_mode`(授权模式列) |
|
|
267
377
|
| 启用 / 停用某企业某应用 | `POST /auth/wecom/admin/status` | 停用是安全动作:**只在该企业没有其它 active 应用时**才吊销该租户会话 |
|
|
268
|
-
| 生成开通链接 | `GET /auth/wecom/admin/install` | 返回 `{installUrl, callbackUrl, expiresIn}`;`callbackUrl` 就是要逐字填进企微后台的那个值 |
|
|
378
|
+
| 生成开通链接 | `GET /auth/wecom/admin/install` | 返回 `{installUrl, callbackUrl, expiresIn}`;`installUrl` 是代开发 `get_customized_auth_url` 返回的 `qrcode_url`(企业管理员扫/开即授权),`callbackUrl` 就是要逐字填进企微后台的那个值 |
|
|
379
|
+
|
|
380
|
+
> 清单里的「授权模式」列是**只读事实**:`成员授权` 意味着这家企业的应用做不了组织架构同步(§2.6)。
|
|
381
|
+
> 改模式只能由**企业管理员**在企微后台改,服务商这边没有接口能替他改。
|
|
382
|
+
> 同理,**跨租户手动触发同步**(superadmin 替某家企业点同步)目前**没有做**:它需要绕过租户守卫
|
|
383
|
+
> 并双写审计,属于后续排期;现在的同步入口只同步「调用方自己所在的那家企业」。
|
|
269
384
|
|
|
270
|
-
|
|
271
|
-
`
|
|
385
|
+
开通链接由企微 `get_customized_auth_url` 生成,**有效期以企微返回为准**(文档口径可达 10 天,实测可能较短),
|
|
386
|
+
过期后界面上的二维码会置灰,重新点一次即可(每次点都重新换 `provider_access_token` + 调
|
|
387
|
+
`get_customized_auth_url`,不做缓存)。代开发**不支持**第三方应用的 `get_pre_auth_code`——本接口
|
|
388
|
+
刻意走 `get_customized_auth_url`,踩过 48002 的坑。
|
|
272
389
|
|
|
273
390
|
权限是**超级管理员**而不是站点 admin:这三件事跨租户(能看别家企业的 corp_id)。
|
|
274
391
|
|
|
@@ -283,9 +400,14 @@ pnpm dev
|
|
|
283
400
|
(带 `owner_tenant`、未使用、未过期)。
|
|
284
401
|
4. 造「员工」:往 `users` 插 `wecom:<userid>`(`source='wecom'`,口令位 `!wecom`)。
|
|
285
402
|
5. 前端直接落在 `/login?wecom_ticket=<raw>&tenant=<corp>` → 走换票 → 断言进入首页。
|
|
403
|
+
6. 造「组织架构 / 同步状态」(**不碰企微上游**):`wecom_app_profile` 的 `auth_mode` 决定页面给哪张卡,
|
|
404
|
+
`wecom_sync_job` 的一行决定状态条文案。两个都造就能覆盖「成员授权 / 已取消授权 / 从没同步过 /
|
|
405
|
+
同步过且 N 个部门跳过」四种界面,全部可离线跑。仓库里 `tests/e2e/templates/wecom-org-sync.spec.ts`
|
|
406
|
+
就是这么做的(5 例),要改文案先看它。
|
|
286
407
|
|
|
287
408
|
纯函数也值得单测:`src/login/web/wecom.ts` 的 `parseWecomLanding()` 与 `wecomAuthorizePath()`
|
|
288
|
-
(无 react / 无 request,node
|
|
409
|
+
(无 react / 无 request,node 侧可直接跑);同步器的错码三分、分片、墓碑回收在
|
|
410
|
+
`packages/cli/templates/tests/wecom-org-sync.spec.ts`(23 例)与 `wecom-sync.spec.ts`(15 例)。
|
|
289
411
|
|
|
290
412
|
### 6.6 前端红线:整页跳转要拼 **API base**
|
|
291
413
|
|
|
@@ -314,6 +436,36 @@ window.location.assign(`${resolveApiBase()}${wecomAuthorizePath(corp)}`);
|
|
|
314
436
|
- `suite_ticket` 的真实推送节奏(10 分钟)
|
|
315
437
|
- 企业管理员确认授权的交互,以及「点了取消」时企微只回 `state` 不回 `code`
|
|
316
438
|
- 通讯录权限导致的兜底:拿不到姓名时显示名落回工号
|
|
439
|
+
- **四项权限的「三态」**(未申请 / 待企业确认 / 已生效):企微没有开放查询接口,只能看症状——
|
|
440
|
+
名字是工号 ⇒ 缺 ①;树节点无名 ⇒ 缺 ②;职务空 ⇒ 缺 ③;部门少 ⇒ 可能是 ④ 或可见范围窄。
|
|
441
|
+
这几条**只能真机验**,单测里全部是桩。
|
|
442
|
+
- **`auth_mode` 的真实取值**:`get_permanent_code` 回包里 `auth_info.agent[].auth_mode` 的形态
|
|
443
|
+
(是数组逐应用、还是回落到 `auth_info.auth_mode`)来自官方文档,本地只有桩;
|
|
444
|
+
第一次真机开通后请确认后台清单里的「授权模式」与实际授权方式一致。
|
|
445
|
+
|
|
446
|
+
### 6.8 组织架构页:文案与降级卡片(企业管理员看的)
|
|
447
|
+
|
|
448
|
+
页面在 `/system/departments`,顶部**常驻**一条状态条(不是弹窗、不是只在出错时出现)。
|
|
449
|
+
文案刻意不暴露内部名词(分片 / 三轨 / 适配器):
|
|
450
|
+
|
|
451
|
+
> 企微组织架构更新于 **X 分钟前** · 自动同步 · [立即同步]
|
|
452
|
+
|
|
453
|
+
四态与对应卡片,改文案前先确认这四条都还在:
|
|
454
|
+
|
|
455
|
+
| 事实 | 页面给什么 | 为什么 |
|
|
456
|
+
|---|---|---|
|
|
457
|
+
| 本站没装企微(无授权记录) | **整条不出现** | 纯 CRUD 站点不该看到企微的任何痕迹;「没装」不是错误,不画红叉 |
|
|
458
|
+
| `auth_mode=1`(成员授权) | 说明卡 + **不给同步按钮** | 点了必然是 400;不如直接说清怎么改 |
|
|
459
|
+
| `status=disabled`(取消授权/停用) | 冻结黄条 + 不给按钮,数据保留 | 「以下为最后一次同步的快照」 |
|
|
460
|
+
| 正常 | 「更新于 X · 自动同步 · [立即同步]」+ `N 个部门因权限被跳过` | 跳过是可解释的常态(可见范围外的部门),不是错误 |
|
|
461
|
+
|
|
462
|
+
失败时按错码给**自助卡片**(不是把 `errcode` 原文甩给人):`60011`/`48002` → 去企微后台开通讯录权限;
|
|
463
|
+
`60020` → 找服务商运维加可信 IP;`45009`/`45033` → 已自动退避,稍后点「继续同步」;
|
|
464
|
+
`timeout`/`network` → 查服务器出网。**卡片里永远不出现异常原文**:`client.ts` 把 `access_token`
|
|
465
|
+
拼在 URL query 里,异常消息可能夹带凭证,落库前统一只存 `{errcode, phase, deptId, at}`。
|
|
466
|
+
|
|
467
|
+
同步一个企业大概要几次点击?正常规模(几十个部门)一次点击会在内部串行推完所有分片再停;
|
|
468
|
+
大企业中途会显示进度条。**离开页面即停**(前端不续推),回来点「继续同步」从游标接着走,不会从零重来。
|
|
317
469
|
|
|
318
470
|
---
|
|
319
471
|
|
|
@@ -324,13 +476,17 @@ window.location.assign(`${resolveApiBase()}${wecomAuthorizePath(corp)}`);
|
|
|
324
476
|
- [ ] 服务商账号已认证,代开发应用模板已创建
|
|
325
477
|
- [ ] Tier A 四项已生成并填好(`wecom_callback_token` / `wecom_encoding_aes_key` 在企微后台同页自设;`wecom_enc_key` 见 §2.4 本地生成;`wecom_base_url` 为站点对外地址)
|
|
326
478
|
- [ ] 模版创建成功后拿到 **SuiteID / SuiteSecret**(Tier B),已填入配置并重启(详见 §4 阶段二)
|
|
327
|
-
- [ ] 站点已用 **https** 对外,反向代理放行 `POST /api/auth/wecom/event
|
|
479
|
+
- [ ] 站点已用 **https** 对外,反向代理放行 `POST /api/auth/wecom/event`(**请求体原样转发**:默认是 JSON,老地址可能是 XML,都不要改写或按表单解析)
|
|
328
480
|
- [ ] 可信域名已备案(含校验文件)
|
|
329
481
|
- [ ] 可信 IP 已列全所有出口 IP
|
|
330
482
|
- [ ] 服务器时间已 NTP 同步:回调只接受 ±5 分钟内的 `timestamp`,偏得多了**所有**指令都会被拒
|
|
331
483
|
- [ ] `wecom_enc_key` 已生成并**离线备份**(与数据库分开)
|
|
332
484
|
- [ ] 测试企业已准备好,且你自己是它的管理员
|
|
333
485
|
- [ ] 一个能登录本站的**超级管理员**账号(生成开通链接用)
|
|
486
|
+
- [ ] 部署后先跑 `oj migrate`:组织架构与同步需要 `0016__org`、`auth/0002__org_sync` 两组迁移
|
|
487
|
+
- [ ] 后台清单里「授权模式」是**管理员授权**(成员授权的企业同步不了,§2.6)
|
|
488
|
+
- [ ] 组织架构四项权限已核对(§2.5);同步一次后页面**没有**「N 个部门因权限被跳过」以外的告警
|
|
489
|
+
- [ ] 组织架构同步已照 **§7.6** 逐条打勾(含最后一行的「事件增量」验收),四个数已回填验收记录
|
|
334
490
|
|
|
335
491
|
### 7.2 上线顺序
|
|
336
492
|
|
|
@@ -374,6 +530,87 @@ window.location.assign(`${resolveApiBase()}${wecomAuthorizePath(corp)}`);
|
|
|
374
530
|
| `wecom.auth.apply` | `create_auth` / `reset_permanent_code` | 普通 |
|
|
375
531
|
| `wecom.auth.cancel` | 企业取消授权 | 安全事件 |
|
|
376
532
|
| `wecom.admin.enable` / `wecom.admin.disable` | 后台启停 | 停用按安全事件记 |
|
|
533
|
+
| `wecom.org.sync` | 组织架构同步(每次 start 一片一条,带 status/phase/stats) | 普通 |
|
|
534
|
+
| `wecom.org.sync.cancel` | 取消同步 | 安全事件(级别 4:中断了一次全量覆盖) |
|
|
535
|
+
|
|
536
|
+
同步的审计落在**本租户**(同步只动自己那家企业的数据),所以企业管理员在自己的操作日志里
|
|
537
|
+
就能看到「谁在什么时候同步过、同步到哪一步、跳过了几个部门」。
|
|
538
|
+
|
|
539
|
+
### 7.6 组织架构同步:真机验收清单
|
|
540
|
+
|
|
541
|
+
单测只能证明「拿到这样的上游回包时我们这样写库」,证明不了「上游真的这样回」——后者在这里收口(§6.7 列的就是这份清单要验的东西)。每条都写清 **谁点、点什么、看到什么算过、不对去看哪一行**,打勾时请把观测到的数字抄进 §7.6-D。
|
|
542
|
+
|
|
543
|
+
#### A. 一次性前置(服务商运维,约 10 分钟)
|
|
544
|
+
|
|
545
|
+
| 项 | 怎么验 | 不过怎么办 |
|
|
546
|
+
|---|---|---|
|
|
547
|
+
| 两组迁移 | `sqlite3 db.sqlite ".tables"` 里有 `wecom_sync_job` 与 `wecom_sync_lock` | 部署后漏跑 `oj migrate`;补跑再起服 |
|
|
548
|
+
| 两个回调地址 | 企微后台「模板回调 URL」与「应用回调 URL」**都**保存成功(两处都填 `/api/auth/wecom/event`,见 §3) | 只填了模板那个:同步与登录都能用,但通讯录变更事件收不到(事件增量那条验收需要它)。补填应用回调需要企业管理员配合 |
|
|
549
|
+
| 四项权限 | 企微后台权限列表里 §2.5 的 ①~④ 都在,且没有一条停在「待企业确认」 | 重发开通链接**补不了**权限(幂等 upsert,不带权限);只能让客户管理员在企微里确认那条权限变更消息 |
|
|
550
|
+
| 授权模式 | `/system/wecom` 清单里「授权模式」= 管理员授权 | 成员授权(§2.6):请企业管理员改,改完企微推 `change_auth`,本系统自动刷新,不用重新开通 |
|
|
551
|
+
| 时间与出网 | §7.1 的 NTP 与可信 IP 两项 | 时钟偏 >5 分钟 → 所有回调被拒;出口 IP 不在白名单 → 同步第 2 步就 60020 |
|
|
552
|
+
|
|
553
|
+
#### B. 企业管理员侧(客户的 IT,一次性)
|
|
554
|
+
|
|
555
|
+
| 动作 | 为什么只能本人做 |
|
|
556
|
+
|---|---|
|
|
557
|
+
| 扫码 / 打开开通链接完成授权 | `permanent_code` 只在 `create_auth` 时一次性下发,服务商后台不显示 |
|
|
558
|
+
| 在企微里点「确认权限变更」 | 不确认 = 权限永远「待企业确认」,同步能跑但拿不到姓名与部门名(满屏工号) |
|
|
559
|
+
| (若为成员授权)把授权模式改成管理员授权 | 成员授权下应用没有任何通讯录权限,同步 100% 失败 |
|
|
560
|
+
|
|
561
|
+
#### C. 逐项验收(每条独立打勾)
|
|
562
|
+
|
|
563
|
+
| # | 操作 | 期望观测 | 不符时先看 |
|
|
564
|
+
|---|---|---|---|
|
|
565
|
+
| 1 | 组织架构页点「立即同步」,等状态条停下 | 文案变「企微组织架构更新于 **刚刚** · 自动同步」;`SELECT status, stats_text FROM wecom_sync_job` → `done`,`stats.departments` / `stats.members` 都 > 0 | `status=paused` → 再点「继续同步」(分片没推完,不是失败);`err_code` 非空 → §6.8 的卡片对应项 |
|
|
566
|
+
| 2 | 抽 3 个部门核对人数与层级 | 每个部门的人数与企微后台一致;层级正确;**主部门唯一**(不是所有人都在「总公司」) | 人数全为 0 → 可见范围为「仅部分成员」;层级全平 → 缺 ④ 的父部门 ID |
|
|
567
|
+
| 3 | 看人名 | 显示名是人名(`display_name` 有值) | 满屏 `wecom:<工号>` → 缺 §2.5 ① |
|
|
568
|
+
| 4 | 看职务列 | 有职务 | 全空 → 缺 §2.5 ③ |
|
|
569
|
+
| 5 | 看状态条的跳过计数 | 「N 个部门因权限被跳过」,且 N **远小于**部门总数 | N 与总数接近 → 先查 §2.5 ④ 与可见范围,再查权限是否卡在待确认 |
|
|
570
|
+
| 6 | 让一个**普通员工**登录一次 | 该员工 `users.display_name` / `position` 被回写,`sys_dept_member` 与企微一致 | 登录本身失败看 §8;登录成功但资料没变 → 该员工不在可见范围,或 24 小时内已自更新过(缓存键 `wecom:self:<corp>:<userid>`) |
|
|
571
|
+
| 7 | 先在本地建一个部门,再点一次同步 | 本地部门**不消失、不改名**(数据源只管辖它自己带来的行) | 本地部门被覆盖 → 严重缺陷,保留现场(`SELECT origin …`)再报 |
|
|
572
|
+
| 8 | 企微后台取消授权 | 组织架构页变冻结黄条(数据保留);该租户 `sys_refresh_token` 清零 | 页面仍显示可同步 → `wecom_app_profile.status` 没落 `disabled`,查 `cancel_auth` 日志 |
|
|
573
|
+
| 9 | 重新扫码授权 | 清单该行恢复 `active`,`permanent_code_enc` 被重写(密文变化即可) | 仍是 `disabled` → 事件没到,回 A 表查回调地址 |
|
|
574
|
+
| 10 | **事件增量(轨三)**:请客户在企微后台改一个部门名,或加/删一个人 | 服务端日志出现 `wecom change_contact applied … changeType=…`;**不点「立即同步」**,刷新组织架构页也能看到变化 | 见下 |
|
|
575
|
+
|
|
576
|
+
最后一行的几种结果,一次就能分清「事件到了没 / 处理成没成 / 是哪种投递形态」:
|
|
577
|
+
|
|
578
|
+
| 日志 | 说明 | 下一步 |
|
|
579
|
+
|---|---|---|
|
|
580
|
+
| `wecom change_contact applied … changeType=create_user`(`msgType=event`、`event=change_contact`) | 数据回调(**应用侧形态**:`ToUserName` / `MsgType=event` / `Event`)收到了,且落库成功 | 正常。`fields` 之类不再需要抄,日志里 `changeType` 与 `corpId` 就够定位 |
|
|
581
|
+
| `wecom change_contact applied … changeType=update_party`(`infoType=change_contact`、`event=` 为空) | 同一条变更走的是**模板指令回调**(代开发形态:`SuiteId` / `AuthCorpId` / `InfoType`)。两种形态都会到,`event=` 空不代表事件没到 | 同样正常。两种形态由同一个端点吃下,不需要为它单独配地址或凭据 |
|
|
582
|
+
| `wecom change_contact ignored … reason=该企业未授权本站(wecom_app_profile 无记录)` | 报文到了、也解密了,但 `wecom_app_profile` 里没有这个企业 | 回 §7.6-A 核对:这家企业的开通流程是否真的走完(清单里要有行、`status=active`) |
|
|
583
|
+
| `wecom change_contact ignored … reason=企微事件 change_contact 缺少 UserID` / `…缺少 Id` | 明文里没有该有的字段:报文形态与文档不符,或被中间件改写 | 把 `changeType` 与原始明文交给开发;**别**靠重推解决(重推是同一份报文) |
|
|
584
|
+
| `wecom change_contact ignored … reason=部门事件既没有名字、本地也没有这一行:不建无名部门` | 代开发形态只给部门 `Id`,而回源 `department/get` 又拿不到(无通讯录读权限) | 补 §2.5 的 ①/④;补齐后点一次「立即同步」即可补上,不用等事件 |
|
|
585
|
+
| `wecom change_contact not consumed … changeType=update_tag` | 标签变更等本系统不消费的类型 | 正常,无需处理 |
|
|
586
|
+
| `wecom callback decrypt failed … reason=bad padding` | 签名过了但**钥匙**不对 → 数据回调用的是**另一套**凭据 | 记下同一行的 `envelopeToUser` 与 `aesKeyFp`:前者告诉你报文来自哪个套件/模板,后者证明本进程加载的是哪把钥匙。结论写进 §2.1 的缺口,再决定是否要单独配一套应用侧凭据 |
|
|
587
|
+
| `wecom callback decrypt failed … reason=receiver mismatch` | 钥匙对、但报文自称的身份(`receiveid`)既不等于配置的 `suite_id`、也不等于信封里的 `ToUserName` | 多见于信封缺 `ToUserName`;先确认服务端不是 0.2.48 或更早(那些版本只认 `suite_id`,所有数据回调都会这样,见下方注) |
|
|
588
|
+
| 一条日志都没有 | 事件根本没进本站 | 应用回调 URL 没填(A 表第 2 项),或反代挡了 POST |
|
|
589
|
+
|
|
590
|
+
> **别拿 `envelopeToUser` 和 `configuredSuite` 比大小**。`receiveid` 的含义随场景变(企微 90968 附注):
|
|
591
|
+
> 第三方事件的回调是 suite_id(`suite_ticket` / `create_auth`),**企业应用(含代开发应用的数据回调)是
|
|
592
|
+
> corpid**。所以数据回调那一行的 `envelopeToUser` 本来就是企业代码、与 `configuredSuite` 不同——服务端
|
|
593
|
+
> 0.2.48 之后的版本就是按「两个候选都试」解密的,这个差异本身不是故障。
|
|
594
|
+
|
|
595
|
+
日志在工程根 `logs/server-*.log`:
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
grep -h "wecom change_contact\|wecom event ignored\|callback decrypt failed" logs/server-*.log | tail -20
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
#### D. 验收记录(请回填,别只写「看起来正常」)
|
|
602
|
+
|
|
603
|
+
| 字段 | 值 |
|
|
604
|
+
|---|---|
|
|
605
|
+
| 企业代码 / 应用 | |
|
|
606
|
+
| 四项权限状态(①~④) | |
|
|
607
|
+
| 部门数 / 人数 / 跳过数 | |
|
|
608
|
+
| 单次全量耗时(`finished_at - started_at`) | |
|
|
609
|
+
| 事件增量行日志原文(`event=` 与 `tags=`) | |
|
|
610
|
+
| 结论 | 通过 / 不通过(不通过项编号) |
|
|
611
|
+
|
|
612
|
+
> 判定口径是**数字**:部门数、人数、跳过数、耗时四个数写不出来,就等于没验。
|
|
613
|
+
> 跳过数不为 0 不是失败(可见范围外是硬口径);但「跳过数接近总数」或「主部门不唯一」必须判不通过。
|
|
377
614
|
|
|
378
615
|
---
|
|
379
616
|
|
|
@@ -391,14 +628,31 @@ window.location.assign(`${resolveApiBase()}${wecomAuthorizePath(corp)}`);
|
|
|
391
628
|
| 400 `回调 timestamp 超出允许窗口:请检查服务器时间,或该请求为历史回调的重放` | 服务器时钟与真实时间偏差超过 5 分钟,或这条回调是被人回放的旧报文 | 校服务器时间(NTP);校正后企微的重推会自动成功 |
|
|
392
629
|
| 400 `回调 nonce 重复:同一条指令回调已被处理过(重放请求已拒绝)` | 同一条指令被投递了两次(企微重试、或反代重复转发) | 首次生效、重放被拒属预期保护;若常态出现,查反代是否重复转发 POST |
|
|
393
630
|
| 企微后台保存回调 URL 失败 | `Token` / `EncodingAESKey` 与 `config.yaml` 不一致 | 逐字对齐(43 字符别多空格) |
|
|
631
|
+
| 日志 `wecom callback decrypt failed … reason=pkcs7: bad padding` | 签名已经过了(说明后台 Token 与本部署一致),所以**钥匙不对**只剩两种:**EncodingAESKey 与后台不一致**(后台点过「随机生成」、或抄错/抄多一个字符),或这条推送其实来自**另一个模板**(那把钥匙解不开这份密文) | 先看 `aesKeyFp` 有没有变:改完 config 对不上 = 新配置没生效(没重启 / 改的不是运行中那份 config),重抄 43 字符到 `vars.wecom_encoding_aes_key`、**重启生效**,再回后台点一次「保存」触发 echostr 验证(页面会当场报验证失败)。`aesKeyFp` 没变仍 `bad padding` → 这条推送不是本套件的,`envelopeToUser` 告诉你它自称是谁(**它是 corpid 也正常**,见 §7.6-C 表最后一行下方那条注),去「应用与模板上线」核对回调地址填在哪 |
|
|
632
|
+
| 日志 `wecom callback decrypt failed … reason=receiver mismatch` | 钥匙对、但报文自称的身份两边都对不上(配置的 `suite_id` 与信封 `ToUserName` 都不是它) | 信封缺 `ToUserName` 时会这样;先确认服务端不是 0.2.48 或更早(那些版本只认 `suite_id`)。仍复现就把整行日志(含 `envelopeToUser` / `configuredSuite` / `aesKeyFp`)留档 |
|
|
633
|
+
| 企业管理员扫码/打开开通链接后提示「**应用安装失败,联系服务商**」 | 企微推 `create_auth` 后**约 1 秒内**没拿到纯文本 `success`(它只看 body 是不是 `success`,不看 HTTP 200)。三类原因:① 事件没到(指令回调 URL 不是当前地址 / 改过没重新上线);② 处理里同步出网超预算(`get_suite_token` + `get_permanent_code` 是两次往返);③ 处理过程抛错(如审计表在租户守卫下被拒) | 先翻服务日志:有 `wecom event failed … reason=<文案>` → 按文案修(缺配置/验签/解密各自有明确文案);**一条日志都没有** → 事件根本没到,回 §4 第 2 步验回调,并到「应用与模板上线」核对指令回调 URL 是否就是当前地址。若后台清单里**已有该企业且 status=active** → 授权其实成功了(失败发生在 profile 落库之后),让企业管理员再扫一次即可 |
|
|
394
634
|
| 400 `企业代码无效或应用未开通` | corp_id 打错,或该企业没装本应用 | 核对 corp_id(`ww` 开头);确认已在后台清单里 |
|
|
395
635
|
| 400 `该企业开通了多个应用,登录链接必须带 agent 参数` | 同企业装了测试+正式两个应用 | 链接加 `&agent=<agentid>` |
|
|
396
636
|
| 401 `登录凭据无效或不在应用可见范围内` | code 无效 / 员工不在应用可见范围 / 非本企业成员 | 企微后台检查应用可见范围 |
|
|
637
|
+
| 400 `本站尚未开通企业微信:没有可同步的授权记录` | 该租户在 `wecom_app_profile` 里没有行(这会出现在组织架构页的同步入口,不是登录) | 先走开沟通道(§4);单租户部署下「租户」= 企业代码 |
|
|
638
|
+
| 400 `该企业为「成员授权」模式,应用没有通讯录权限 …` | `auth_mode=1`(§2.6),此时任何通讯录接口都是 `60011` | **企业管理员**在企微后台把授权模式改为「管理员授权」;企微会推 `change_auth`,本系统自动刷新 `auth_mode`,无需重新开通 |
|
|
639
|
+
| 400 `该企业已取消授权或应用已停用:组织架构同步已冻结 …` | `status=disabled`(企业取消授权,或后台点了停用) | 服务商重新发开通链接;已同步的组织数据保留,可当快照用 |
|
|
640
|
+
| 组织架构页卡片「应用缺少通讯录权限(错误码 60011 / 48002)」 | 缺 §2.5 的 ①/②/④(自定义权限还卡在「待企业确认」也会这样) | 企业管理员:企微后台 → 应用管理 → 本应用 → 权限,勾选后提交,并**在企微里确认**那条权限变更消息 |
|
|
641
|
+
| 卡片「企微接口限流(错误码 45009 / 45033)」 | 触发企微频率/并发限制(大企业全量同步常见) | 已自动退避三次;仍失败就点「继续同步」,游标停在原部门,不会丢进度 |
|
|
642
|
+
| 卡片「服务器出口 IP 不在企微白名单(60020)」 | 同上方 60020 行 | 同上方处置(加可信 IP) |
|
|
643
|
+
| 状态条「N 个部门因权限被跳过」 | 这些部门在应用**可见范围外**(或负责人/父部门权限缺失) | 常态可忽略;若 N 与总部门数接近,先查 §2.5 ④ 与可见范围(§2.4 的话术可以直接发给企业管理员) |
|
|
644
|
+
| 企微后台改完通讯录,页面**没变**(也没点过同步) | 事件没到,或到了但处理失败。事件是加速器不是权威:失败**不会**报错、也不会重推成功 | `grep "wecom change_contact" logs/server-*.log \| tail`:`applied` = 已落库(刷新页面即可见);`ignored … reason=…` = 按 reason 修(未授权 / 缺字段 / 无通讯录读权限);一条都没有 = 应用回调 URL 没填(§3 第 2 项)。急着要数据就先点一次「立即同步」,全量同步是权威 |
|
|
645
|
+
| 409 `同步正在进行,请稍后再试` | 同一企业同一应用已有同步在跑(另一个标签页 / 上一次请求崩了留下锁) | 等一次轮询周期再点;崩溃残留的锁 120 秒后可被抢占,不用人工清 |
|
|
646
|
+
| 429 `rate limited` | `start`/`cancel` 限速 10 次/分(同一账号在两个页面同时驱动,或手动连点) | 等一分钟再点;进度已落库,不需要从头重来 |
|
|
647
|
+
| 点「立即同步」后按钮立刻回到可点、进度没动 | 这一片被限流或上游抖动提前收手(状态仍是 `running`) | 正常现象:再点「继续同步」;页面每 2 秒会自己刷新状态 |
|
|
397
648
|
| 403 `账号已被停用,请联系管理员` | 本站把该账号 `is_forbidden=1` | 后台启用该账号 |
|
|
398
649
|
| 401 `登录票据无效或已使用` / `登录票据已过期,请重新登录` | 票据被重放,或 180 秒没换 | 重新从入口进 |
|
|
399
650
|
| 400 `授权未完成:企业微信未返回 code` | 用户在授权页点了取消 | 重新授权 |
|
|
400
651
|
| 400 `登录状态已失效,请从企业微信重新进入` | `state` 过期(10 分钟)/ 已用 / 换过部署 | 重新进;跨部署时正常现象 |
|
|
401
|
-
| 502 `企微 … 失败:errcode=60020
|
|
652
|
+
| 502 `企微 … 失败:errcode=60020 … client ip 113.80.7.113` | 调用企微 API 的**服务器出口公网 IP**未在可信 IP 白名单(代开发套件级/服务商级;`get_suite_token` 与「生成开通链接」用的 `get_provider_token` / `get_customized_auth_url` **都必校**) | 企微服务商后台 → 应用管理 → 代开发应用 → 对应套件 → **IP 白名单/可信IP**,把报错里的 `client ip`(例 `113.80.7.113`,即你服务器访问公网的出口 IP,**不是内网 IP**)加进去;多出口/运营商 NAT 就加整段 CIDR。改完等几分钟再点「生成开通链接」。注:此码**不在自动重试验证集**(只有 40001/42001 重换一次),属配置阻断、重试无用 |
|
|
653
|
+
| 502 `企微 … 失败:errcode=48002 api forbidden` | 代开发应用调了第三方应用的 `get_pre_auth_code`(代开发**明确不支持**该接口) | 本系统「生成开通链接」已改用代开发专属的 `get_customized_auth_url`(返回 `qrcode_url`)。若仍见此码:确认跑的是本版代码(`admin.ts` 的 `createInstall` 不再调 `get_pre_auth_code`),并检查是否别处/旧版本在调它 |
|
|
654
|
+
| 502 `企微 … 失败:errcode=40058 state missing` | `get_customized_auth_url` 的 `state` 必填,本系统没传/传空(旧版) | 已改为始终发送 `state`(调用方不传则补默认常量)。若仍见此码:确认跑的是本版代码(`client.ts` 的 `customizedAuthUrl` 始终带 `state`) |
|
|
655
|
+
| 502 `企微 … 失败:errcode=842002 代开发模板尚未上架/上线` | 传入的代开发模板(`templateid_list` 里的 suite_id)只是「创建成功」但**未上线**——`get_customized_auth_url` 只在模板正式上线后才生成 `qrcode_url` | **运营动作,非代码缺陷**:服务商后台 → 应用管理 → 代开发应用上线 → 提交模板上线审核 → 审核通过后手动点「提交上线」。模板状态变「已上线/上架」后再点「生成开通链接」。凭证/参数都对却卡这步,就是模板没上线(创建成功 ≠ 上线) |
|
|
402
656
|
| 502 `企微 … 失败:errcode=40001/40014/42001` | 凭据错或 token 过期(会自动重换一次) | 若持续:核对 SuiteSecret / 是否重置过 |
|
|
403
657
|
| 502 `企微接口不可达` / `HTTP 5xx` | 出网被墙、DNS、代理 | 查机器出网与 DNS |
|
|
404
658
|
| 点登录页「企业微信」按钮页面只闪一下、无请求 | 跳转拼了站点 base 而不是 API base | 见 §6.6 |
|
|
@@ -411,11 +665,12 @@ window.location.assign(`${resolveApiBase()}${wecomAuthorizePath(corp)}`);
|
|
|
411
665
|
## 9. 一分钟自检
|
|
412
666
|
|
|
413
667
|
```text
|
|
414
|
-
[ ]
|
|
668
|
+
[ ] 八个 vars 非空(Tier A/B/C),wecom_base_url 以 /api/auth/wecom 结尾
|
|
415
669
|
[ ] 企微后台的指令回调 URL == <wecom_base_url>/event(逐字)
|
|
416
670
|
[ ] 服务日志有 wecom suite_ticket updated(近期)
|
|
417
|
-
[ ] /system/wecom 列表里有目标企业,status=active
|
|
671
|
+
[ ] /system/wecom 列表里有目标企业,status=active,授权模式=管理员授权
|
|
418
672
|
[ ] /system/settings 的「启用企业微信登录」= 开
|
|
419
673
|
[ ] 用普通员工账号(非管理员)完整走过一次登录
|
|
674
|
+
[ ] /system/departments 顶部状态条有「更新于 X」;树有名字、人挂在正确部门
|
|
420
675
|
[ ] db.sqlite 与 wecom_enc_key 各自有备份
|
|
421
676
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oj-module/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.48",
|
|
5
5
|
"description": "React Antd 模块工程命令行工具(含预构建宿主 shell-dist)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@faker-js/faker": "^10.4.0",
|
|
52
|
-
"@oj-module/runtime": "0.2.
|
|
52
|
+
"@oj-module/runtime": "0.2.48",
|
|
53
53
|
"@redocly/cli": "^2.51.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
"cac": "^6.7.14",
|
|
@@ -304,10 +304,10 @@ function getAppInfo() {
|
|
|
304
304
|
return {
|
|
305
305
|
"pkg": {
|
|
306
306
|
"name": "@oj-module/runtime",
|
|
307
|
-
"version": "0.2.
|
|
307
|
+
"version": "0.2.48",
|
|
308
308
|
"license": "MIT"
|
|
309
309
|
},
|
|
310
|
-
"lastBuildTime": "2026-
|
|
310
|
+
"lastBuildTime": "2026-11-01 10:19:28"
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
var init_get_app_info = __esmMin((() => {}));
|
package/shell-dist/index.html
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" href="/favicon.ico" />
|
|
7
7
|
<title>React Antd Admin · Shell</title>
|
|
8
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-3nM+tS10yKYnXNwkhSrSkw=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
|
|
9
9
|
|
|
10
|
-
<script type="importmap" nonce="
|
|
10
|
+
<script type="importmap" nonce="3nM+tS10yKYnXNwkhSrSkw==">{
|
|
11
11
|
"imports": {
|
|
12
12
|
"react": "/assets/react.js",
|
|
13
13
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
package/shell-dist/versions.json
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"react-router": "7.18.2",
|
|
8
8
|
"react-router/dom": "7.18.2",
|
|
9
9
|
"@tanstack/react-query": "5.102.8",
|
|
10
|
-
"@oj-module/runtime": "0.2.
|
|
11
|
-
"@oj-module/runtime/contract": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
10
|
+
"@oj-module/runtime": "0.2.48",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.48",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.48",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -115,11 +115,16 @@ describe("templates 租户定性(A 租户表 / B 共享表)", () => {
|
|
|
115
115
|
expect(unknown).toEqual([]);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
it("t-4
|
|
118
|
+
it("t-4 28 张表的 A/B 分组与方案 v3 §5.2 一致(新增表须先定性)", () => {
|
|
119
119
|
const { tables } = loadSchemaTables();
|
|
120
120
|
const shared = [...tables].filter(([, d]) => d.tenant === false).map(([n]) => n).sort();
|
|
121
121
|
// Phase 0 落地 21 张(11 A + 10 B);Phase 1 加企微三表,全部 B 组(D9:查它们时必然还没有租户上下文)
|
|
122
|
-
|
|
122
|
+
// 0016 组织架构加 sys_department / sys_dept_member 两张,均 A 组租户表(组织架构天然按租户隔离,
|
|
123
|
+
// 企微部署下 tenant_id = corp_id;只在有租户上下文时被读写,与 users / sys_oplog 同口径)。
|
|
124
|
+
// auth/0002 加 wecom_sync_job / wecom_sync_lock 两张,同属 A 组:同步只在调用方自己的租户内跑
|
|
125
|
+
// (D7 不接受 corpId 参数),故不进 shared_allow——跨租户读元数据那条另走显式 tenant_id 的裸 SQL。
|
|
126
|
+
// 改这张表清单 = 改租户隔离边界,必须同步 config.yaml 的 tenant.shared_allow(t-2/t-3 会把关)。
|
|
127
|
+
expect(tables.size).toBe(28);
|
|
123
128
|
expect(shared).toEqual([
|
|
124
129
|
"_oj_migrations",
|
|
125
130
|
"market_history",
|
package/src/init.ts
CHANGED
|
@@ -245,7 +245,7 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
245
245
|
console.log(`[ojm] · 跳过 ${item}`);
|
|
246
246
|
for (const item of report.warnings)
|
|
247
247
|
console.warn(`[ojm] ! ${item}`);
|
|
248
|
-
console.log("[ojm] 下一步:pnpm install && pnpm dev(登录 superadmin /
|
|
248
|
+
console.log("[ojm] 下一步:pnpm install && pnpm dev(登录 superadmin / aA123456,角色账号见 devkit 手册 §1.1)");
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
/** 默认密封器:bin/oj secret keygen + seal(失败返回 null,不抛) */
|
|
@@ -120,7 +120,7 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
|
|
|
120
120
|
const site = await bootstrapIsolatedSite({ projectRoot: src, starter });
|
|
121
121
|
|
|
122
122
|
expect(site.admin.user).toBe("superadmin");
|
|
123
|
-
expect(site.admin.pass).toBe("
|
|
123
|
+
expect(site.admin.pass).toBe("aA123456");
|
|
124
124
|
await site.cleanup();
|
|
125
125
|
});
|
|
126
126
|
|