支持企微通讯录缺失手机号(07-26-internal-ops-recovery-baseline)

这个提交包含在:
2026-07-27 13:05:27 +08:00
父节点 9875bc37a4
当前提交 2526052e73
共修改 24 个文件,包含 845 行新增和 63 行删除
+96 -5
查看文件
@@ -168,9 +168,10 @@ WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
- `login|bind` always keeps `snsapi_base`; only explicit `onboard` uses
`snsapi_privateinfo`.
- The member must be visible to the app, active, return the same userid from
both identity calls, and authorize a valid phone plus personal email or
business email. A blank/invalid personal email may fall back to a valid
business email.
both identity calls, and authorize a valid personal email or business email.
Phone may be absent; when returned it must be normalized and checked for
uniqueness. A blank/invalid personal email may fall back to a valid business
email.
- `user_ticket`, OAuth code, access token and raw provider responses exist only
in request memory. Do not persist them or include them in Cookie, audit,
response details or logs; provider/network failures use fixed safe codes.
@@ -190,8 +191,9 @@ WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
- Provider/network failure -> `wecom_api_failed` or safe auth error; no raw
payload, token or ticket in output/logs.
- Userid mismatch -> `identity_mismatch`; inactive member -> `member_inactive`.
- Missing/invalid phone, both emails invalid, or unresolved department ->
`missing_required_field`.
- Both emails invalid or unresolved department -> `missing_required_field`.
- Phone absent -> continue with `NULL`; phone present but conflicting ->
`employee_conflict`.
- Multiple employees, unique-field race or cross-binding -> `employee_conflict`.
- Department binding points to missing/archived data -> `department_invalid`;
multiple same-name OA departments -> `department_ambiguous`.
@@ -232,3 +234,92 @@ ordinary login -> snsapi_base
one-time onboard -> explicit private consent -> safe unique employee-only write
business role assignment -> separate administrator-audited operation
```
## Scenario: WeCom Directory Export With Optional Phone
### 1. Scope / Trigger
- Trigger: importing an administrator-exported WeCom directory XLSX, or
changing `Employee.phone` across database, API, HR sync, OAuth and UI layers.
- Purpose: allow a real member to remain temporarily without a phone while
preserving stable identity, uniqueness and privacy controls.
### 2. Signatures
- DB: `Employee.phone String? @unique`; migration drops only the `NOT NULL`
constraint and keeps the unique index.
- CLI: `npm run import:wecom-directory-export -- --input=- --dry-run`.
- Apply: `WECOM_DIRECTORY_EXPORT_APPLY=1 ... --input=- --apply
--actor-employee-no=<employee-no>`.
- User API and frontend employee/user types expose `phone: string | null`;
create/update accepts an omitted or explicit `null` phone.
### 3. Contracts
- Missing phone is stored as SQL `NULL`, never `""`, a repeated placeholder or
a synthesized fake number. Multiple `NULL` rows are valid; non-null phones
remain unique.
- WeCom account and valid enterprise email remain required for directory-export
onboarding. Account, non-null phone and enterprise email duplicates block
the input.
- An empty source phone never clears an existing OA phone. Only an explicitly
supplied valid phone participates in update and unique-conflict checks.
- The XLSX is accepted only from stdin and defaults to dry-run. Apply requires
both `--apply` and `WECOM_DIRECTORY_EXPORT_APPLY=1`, plus an active actor.
- CLI output contains counts and categories only. The workbook, full local path,
names, accounts, phones, emails, departments and raw rows must not enter Git,
Trellis artifacts or reports.
- New accounts receive only `employee`; finance, CEO and admin roles remain a
separate administrator-audited operation.
### 4. Validation & Error Matrix
- Missing phone cell -> valid profile with `phone=null`.
- Supplied malformed phone -> incomplete/blocked; do not downgrade it to an
absent phone and do not write it.
- Missing/invalid enterprise email, account, name or department -> skipped or
blocked as `missing_required_field`.
- Duplicate account/non-null phone/enterprise email in the workbook ->
`duplicate_values`; error output contains counts only.
- Existing OA unique-field or binding conflict -> conflict; no automatic merge.
- `--apply` without confirmation env or active actor -> refuse before sync.
- Migration rollback with any `phone IS NULL` row -> stop; do not fill fake
values merely to restore `NOT NULL`.
### 5. Good/Base/Bad Cases
- Good: five members have unique phones, one member has no phone, all six have
unique accounts and enterprise emails; dry-run plans six records and reports
only aggregate counts.
- Base: a later export still omits a member phone; the member's existing OA
phone is preserved.
- Bad: write an empty string or shared placeholder, use personal email in place
of the required enterprise email for bulk identity, or auto-grant a business
role during import.
### 6. Tests Required
- Prisma validation plus a migration review proving only `DROP NOT NULL`.
- Unit tests for instruction rows/Chinese headers, five-present/one-missing,
malformed phone, missing enterprise email and duplicate account/phone/email.
- Route/service tests for create/update/import `null`, non-empty unique checks,
OAuth missing phone and empty-source preservation of an existing phone.
- Frontend typecheck/build and displays that render a clear empty-phone label.
- Full backend regression, Trellis governance, sensitive-content scan and
`git diff --check`; a real workbook may be parsed only into aggregate counts
and remains Git-ignored.
### 7. Wrong vs Correct
#### Wrong
```js
phone: source.phone || "00000000000"
```
#### Correct
```js
phone: source.phone || null
// Existing employee updates omit `phone` entirely when the source is empty.
```