WHMS 实现企微HR主数据镜像同步
这个提交包含在:
@@ -16,7 +16,7 @@ Read only what is relevant:
|
||||
- For UI/page work, PC/mobile/WeCom experience, approval UX, or domestic OA benchmarking, read `docs/产品设计规则.md` and `references/visual-qa.md`. DG8 makes Feishu workbench/approval the single main UI/layout reference, with Hyinda branding and project rules taking priority. DG9 adds the A/B/C page layering and shared workbench/mobile samples; choose the DG9 sample before designing or coding a page. Other products are only function/process/control references. Mobile page work must treat `docs/产品设计规则.md` as the source of truth for the unified mobile shell, icon grid, card lists, deep Push pages, sticky actions, safe-area, and cross-viewport matrix. 移动页必须先对齐该设计事实源,再进入实现或验证。
|
||||
- For approval workflow, roles, self-approval, notification, deeplink, reminder, or escalation work, read `docs/需求-统一审批中心规约.md`.
|
||||
- For HR lifecycle, employee onboarding/offboarding, Enterprise WeChat directory sync, or account disablement work, read `docs/需求-企业微信基础设施规约.md`, `docs/管理员手册.md`, and the latest HCLT/HRWD Issue CSV evidence first.
|
||||
- For Enterprise WeChat-first HR/attendance runtime migration, read `docs/企微优先开发路线图.md` before planning or coding. WDRG makes the roadmap authoritative for stage order; WCPC has rerun the capability POC with an existing active test account and reached `GO_TO_HR_MASTER_SYNC`, so the next allowed step is planning `wecom_hr_master_sync_v1`. Do not start `wecom_attendance_approval_mirror_v1`, `oa_hr_emergency_mode_v1`, `wecom_recovery_writeback_v1`, or `wecom_ruleops_v1` until their preceding stages exit.
|
||||
- For Enterprise WeChat-first HR/attendance runtime migration, read `docs/企微优先开发路线图.md` before planning or coding. WDRG makes the roadmap authoritative for stage order; WCPC has rerun the capability POC with an existing active test account and reached `GO_TO_HR_MASTER_SYNC`; WHMS has locally implemented `wecom_hr_master_sync_v1` but it is not deployed and production batch apply is not approved. Do not start `wecom_attendance_approval_mirror_v1`, `oa_hr_emergency_mode_v1`, `wecom_recovery_writeback_v1`, or `wecom_ruleops_v1` until WHMS is deployed/verified and the next stage has its own Issue CSV.
|
||||
- For reimbursement, invoice, OCR, duplicate invoice, or attachment work, read `docs/报销发票附件规则.md` and `references/reimbursement.md`.
|
||||
- For local/WeCom/public trial-run, OCR runtime, Caddy, or DB drift work, read `docs/试运行与自动化验证手册.md` and `references/trialrun.md`.
|
||||
- For how this skill should evolve, read `docs/Agent开发交付实践.md`.
|
||||
@@ -105,7 +105,7 @@ Final responses should include:
|
||||
- R6 scope defaults to reimbursement, leave, procurement, approval, Enterprise WeChat entry/notification, finance payment/export.
|
||||
- Do not expand to budget, ERP, AI, full travel, or complex BPM during R6; record those as follow-up Issues.
|
||||
- Enterprise WeChat personal app message is the formal approval notification channel after R5; mock and group bots are not acceptance evidence for R6 business notification.
|
||||
- After WCPC, ERP is not a future OA integration target. HR/attendance employee daily approvals (leave, missed punch, outing, overtime) should reuse mature Enterprise WeChat capabilities first, while OA provides domain-based mirror, manual takeover, recovery writeback, finance closeout, and audit. Reimbursement, payment batches, AP, accounting handoff, procurement, expenses, contracts, projects, sensitive attachments, invoices, bank receipts, and accounting handoff materials remain OA-primary unless a later Issue changes that domain explicitly. OA-primary approvals may send Enterprise WeChat personal-app notifications, but messages must contain only dehydrated title/type/no/status/deeplink; do not upload procurement forms, contract bodies, quotes, supplier files, invoices, bank receipts, or accounting materials to WeCom native approvals or WeCom attachments. Before runtime migration, read `docs/企微优先开发路线图.md`, run or check `WECOM-CAPABILITY-POC-V1`/WCPC, and classify each capability as `CAN_REUSE_NOW`, `NEEDS_CONFIG`, `NEEDS_API_POC`, `KEEP_OA_PRIMARY`, or `NOT_SUPPORTED_BY_API`; missing Secret, callback, template, trusted IP, visible scope, or API permission must be recorded as `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`, not PASS. Current WCPC status: existing active test account `SMOKE002/测试员工` (reported only as `oa_smoke***`) reran successfully with report `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`, `status=PASS`, `conclusion=GO_TO_HR_MASTER_SYNC`, and `blockingCapabilities=[]`. `contact.member_read`, `hr.staff_read`, check-in record/rule read, and vacation quota are reusable for HR master sync; approval template ID, approval `sp_no`, callback config, and HR write API remain later-stage gaps. Do not skip stage order: `WECOM-CAPABILITY-POC-V1`/WCPC -> `wecom_hr_master_sync_v1` -> `wecom_attendance_approval_mirror_v1` -> `oa_hr_emergency_mode_v1` -> `wecom_recovery_writeback_v1` -> `wecom_ruleops_v1`. UI and API work must distinguish `OA 主办`, `企微镜像`, and `OA 应急接管`. OA-driven Enterprise WeChat directory create/update/disable is live verified; for later HR/WeCom directory issues, first check configuration, permission, department, and data-specific causes before treating the capability as unfinished. After WCFB, Enterprise WeChat check-in sync uses an authorized custom app Secret: `WECOM_CHECKIN_SECRET` is an explicit override for a dedicated check-in app, and when it is empty OA can reuse the authorized workbench app `WECOM_AGENT_SECRET`.
|
||||
- After WHMS, ERP is not a future OA integration target. HR/attendance employee daily approvals (leave, missed punch, outing, overtime) should reuse mature Enterprise WeChat capabilities first, while OA provides domain-based mirror, manual takeover, recovery writeback, finance closeout, and audit. Reimbursement, payment batches, AP, accounting handoff, procurement, expenses, contracts, projects, sensitive attachments, invoices, bank receipts, and accounting handoff materials remain OA-primary unless a later Issue changes that domain explicitly. OA-primary approvals may send Enterprise WeChat personal-app notifications, but messages must contain only dehydrated title/type/no/status/deeplink; do not upload procurement forms, contract bodies, quotes, supplier files, invoices, bank receipts, or accounting materials to WeCom native approvals or WeCom attachments. Before runtime migration, read `docs/企微优先开发路线图.md`, run or check `WECOM-CAPABILITY-POC-V1`/WCPC, and classify each capability as `CAN_REUSE_NOW`, `NEEDS_CONFIG`, `NEEDS_API_POC`, `KEEP_OA_PRIMARY`, or `NOT_SUPPORTED_BY_API`; missing Secret, callback, template, trusted IP, visible scope, or API permission must be recorded as `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`, not PASS. Current WCPC status: existing active test account `SMOKE002/测试员工` (reported only as `oa_smoke***`) reran successfully with report `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`, `status=PASS`, `conclusion=GO_TO_HR_MASTER_SYNC`, and `blockingCapabilities=[]`. Current WHMS status: `wecom_hr_master_sync_v1` is locally implemented with feature flag default `force_off`, HR/contact read service, dry-run/apply, sync run/item records, People management card, and QA-only runner `scripts/qa/check-wecom-hr-master-sync.mjs`; it is not deployed and production real-employee batch apply is not approved. WHMS rules: missing OA-required fields are skipped as `missing_required_field`, inactive WeCom status becomes `needs_offboarding_confirmation`, and OA accounts are not auto-disabled. Approval template ID, approval `sp_no`, callback config, attendance/approval mirror, emergency mode, recovery writeback, RuleOps, and HR write APIs remain later-stage gaps. Do not skip stage order: `WECOM-CAPABILITY-POC-V1`/WCPC -> `wecom_hr_master_sync_v1` deployment/verification -> `wecom_attendance_approval_mirror_v1` -> `oa_hr_emergency_mode_v1` -> `wecom_recovery_writeback_v1` -> `wecom_ruleops_v1`. UI and API work must distinguish `OA 主办`, `企微镜像`, and `OA 应急接管`. OA-driven Enterprise WeChat directory create/update/disable is live verified; for later HR/WeCom directory issues, first check configuration, permission, department, and data-specific causes before treating the capability as unfinished. After WCFB, Enterprise WeChat check-in sync uses an authorized custom app Secret: `WECOM_CHECKIN_SECRET` is an explicit override for a dedicated check-in app, and when it is empty OA can reuse the authorized workbench app `WECOM_AGENT_SECRET`.
|
||||
- Approval-chain correctness can be proven by PC or automation, while Enterprise WeChat validates entry, binding,免登录, notification, deeplink, and mobile layout.
|
||||
- If real-device mobile validation is paused after automation passes, record the gate as `PAUSED`; do not close RS6/UI6/R6 mobile blockers until representative real-device confirmation is recorded.
|
||||
- Role-specific pages should use one shared OA/mobile framework and tailor summaries, entries, actions, and sensitive fields by role. Do not create unrelated UI systems for employee, approver, finance, admin, or CEO views.
|
||||
|
||||
@@ -33,6 +33,55 @@ export type WecomDirectoryState = {
|
||||
latestSync: WecomDirectorySyncRun | null;
|
||||
};
|
||||
|
||||
export type WecomHrMasterSyncMode = "dry_run" | "apply";
|
||||
export type WecomHrMasterSyncRunStatus = "running" | "success" | "partial" | "failed" | "skipped";
|
||||
export type WecomHrMasterSyncItemStatus = "planned" | "success" | "skipped" | "conflict" | "failed";
|
||||
|
||||
export type WecomHrMasterSyncItem = {
|
||||
id: string;
|
||||
runId: string;
|
||||
employeeId: string | null;
|
||||
externalUserId: string;
|
||||
action: "create" | "update" | "noop" | "skip" | "conflict" | "failed";
|
||||
status: WecomHrMasterSyncItemStatus;
|
||||
errorCode: string | null;
|
||||
errorMessage: string | null;
|
||||
retryCount: number;
|
||||
fieldChanges: Record<string, unknown> | null;
|
||||
sourceSummary: Record<string, unknown> | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type WecomHrMasterSyncRun = {
|
||||
id: string;
|
||||
mode: WecomHrMasterSyncMode;
|
||||
status: WecomHrMasterSyncRunStatus;
|
||||
tokenSource: Record<string, string> | null;
|
||||
requestedUserids: string[] | null;
|
||||
limit: number | null;
|
||||
summary: {
|
||||
total?: number;
|
||||
created?: number;
|
||||
updated?: number;
|
||||
noop?: number;
|
||||
skipped?: number;
|
||||
conflicts?: number;
|
||||
failed?: number;
|
||||
planned?: number;
|
||||
success?: number;
|
||||
} | null;
|
||||
errorCode: string | null;
|
||||
errorMessage: string | null;
|
||||
triggeredBy: string | null;
|
||||
startedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
itemCount?: number;
|
||||
items?: WecomHrMasterSyncItem[];
|
||||
};
|
||||
|
||||
export type EmployeeDepartmentSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -457,6 +506,27 @@ export async function retryEmployeeWecomDirectorySync(employeeId: string, operat
|
||||
});
|
||||
}
|
||||
|
||||
export async function runWecomHrMasterSync(payload: {
|
||||
mode: WecomHrMasterSyncMode;
|
||||
userids?: string[];
|
||||
limit?: number;
|
||||
}) {
|
||||
return apiRequestData<WecomHrMasterSyncRun>("/api/employees/wecom-hr-master-sync/runs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchWecomHrMasterSyncRuns(pageSize = 5) {
|
||||
return apiRequestData<{ items: WecomHrMasterSyncRun[] }>(
|
||||
`/api/employees/wecom-hr-master-sync/runs?pageSize=${pageSize}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchWecomHrMasterSyncRun(runId: string) {
|
||||
return apiRequestData<WecomHrMasterSyncRun>(`/api/employees/wecom-hr-master-sync/runs/${runId}`);
|
||||
}
|
||||
|
||||
export async function fetchEmployeeOrganization() {
|
||||
return apiRequestData<{ items: OrganizationDepartmentSection[] }>("/api/employees/organization");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const FEATURE_FLAG_DISABLED_MESSAGES: Record<string, string> = {
|
||||
attendance_unified_v1_disabled: "企微考勤统一汇总当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
weekly_report_v1_disabled: "周报模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
employee_lifecycle_v1_disabled: "人员生命周期模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
wecom_hr_master_sync_v1_disabled: "企微 HR 主数据镜像当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
todo_board_v1_disabled: "待办模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
project_domain_v1_disabled: "项目模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
project_linking_v1_disabled: "项目关联功能当前未启用,请联系管理员检查环境开关与功能开关配置。",
|
||||
|
||||
@@ -17,6 +17,8 @@ export type FeatureFlagKey =
|
||||
| "attendance_unified_v1"
|
||||
| "weekly_report_v1"
|
||||
| "employee_lifecycle_v1"
|
||||
| "hr_wecom_directory_sync_v1"
|
||||
| "wecom_hr_master_sync_v1"
|
||||
| "todo_board_v1"
|
||||
| "wecom_entry_v1"
|
||||
| "project_domain_v1"
|
||||
|
||||
+134
-1
@@ -8,12 +8,15 @@ import {
|
||||
batchTransferEmployeesDepartment,
|
||||
fetchEmployees,
|
||||
fetchEmployeeOrganization,
|
||||
fetchWecomHrMasterSyncRuns,
|
||||
regularizeEmployee,
|
||||
retryEmployeeWecomDirectorySync,
|
||||
runWecomHrMasterSync,
|
||||
startEmployeeOffboarding,
|
||||
startEmployeeOnboarding,
|
||||
type EmployeeEmploymentStatus,
|
||||
type EmployeeSummary,
|
||||
type WecomHrMasterSyncRun,
|
||||
type WecomDirectoryState,
|
||||
type OrganizationTreeNode,
|
||||
} from "../api/employees";
|
||||
@@ -90,6 +93,34 @@ function shouldShowWecomRetry(state?: WecomDirectoryState) {
|
||||
return !state || ["unbound", "sync_failed", "sync_skipped"].includes(state.status);
|
||||
}
|
||||
|
||||
function wecomHrMasterRunStatusLabel(status: WecomHrMasterSyncRun["status"]) {
|
||||
const map: Record<WecomHrMasterSyncRun["status"], string> = {
|
||||
running: "执行中",
|
||||
success: "已完成",
|
||||
partial: "部分完成",
|
||||
failed: "失败",
|
||||
skipped: "已跳过",
|
||||
};
|
||||
return map[status] ?? status;
|
||||
}
|
||||
|
||||
function wecomHrMasterModeLabel(mode: WecomHrMasterSyncRun["mode"]) {
|
||||
return mode === "apply" ? "写入" : "预检";
|
||||
}
|
||||
|
||||
function formatSyncSummary(run?: WecomHrMasterSyncRun | null) {
|
||||
const summary = run?.summary;
|
||||
if (!summary) return "暂无统计";
|
||||
return [
|
||||
`总数 ${summary.total ?? 0}`,
|
||||
`新建 ${summary.created ?? 0}`,
|
||||
`更新 ${summary.updated ?? 0}`,
|
||||
`跳过 ${summary.skipped ?? 0}`,
|
||||
`冲突 ${summary.conflicts ?? 0}`,
|
||||
`失败 ${summary.failed ?? 0}`,
|
||||
].join(" / ");
|
||||
}
|
||||
|
||||
function collectOrganizationEmployees(nodes: OrganizationTreeNode[], target: Map<string, { id: string; name: string; employeeNo: string; departmentName: string }>) {
|
||||
for (const node of nodes) {
|
||||
if (!target.has(node.id)) {
|
||||
@@ -124,6 +155,7 @@ export function PeoplePage() {
|
||||
const [managerDialogOpen, setManagerDialogOpen] = useState(false);
|
||||
const [enableConfirmOpen, setEnableConfirmOpen] = useState(false);
|
||||
const [disableConfirmOpen, setDisableConfirmOpen] = useState(false);
|
||||
const [wecomHrApplyConfirmOpen, setWecomHrApplyConfirmOpen] = useState(false);
|
||||
const [offboardingTarget, setOffboardingTarget] = useState<EmployeeSummary | null>(null);
|
||||
const [targetDepartmentId, setTargetDepartmentId] = useState("");
|
||||
const [targetManagerId, setTargetManagerId] = useState("");
|
||||
@@ -155,6 +187,13 @@ export function PeoplePage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const wecomHrMasterRunsQuery = useQuery({
|
||||
queryKey: ["wecom-hr-master-sync", "runs"],
|
||||
queryFn: () => fetchWecomHrMasterSyncRuns(5),
|
||||
enabled: canManageUsers,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEmployeeIds([]);
|
||||
}, [q, status, departmentId, title, managerId, hireDateFrom, hireDateTo, employmentStatus]);
|
||||
@@ -221,6 +260,35 @@ export function PeoplePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const wecomHrDryRunMutation = useMutation({
|
||||
mutationFn: () => runWecomHrMasterSync({ mode: "dry_run", limit: 50 }),
|
||||
onSuccess: async (run) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["wecom-hr-master-sync"] }),
|
||||
invalidatePeopleQueries(),
|
||||
]);
|
||||
toast.success(`企微 HR 主数据预检完成:${formatSyncSummary(run)}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(mapApiMessage(error, "企微 HR 主数据预检失败"));
|
||||
},
|
||||
});
|
||||
|
||||
const wecomHrApplyMutation = useMutation({
|
||||
mutationFn: () => runWecomHrMasterSync({ mode: "apply", limit: 50 }),
|
||||
onSuccess: async (run) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["wecom-hr-master-sync"] }),
|
||||
invalidatePeopleQueries(),
|
||||
]);
|
||||
setWecomHrApplyConfirmOpen(false);
|
||||
toast.success(`企微 HR 主数据写入完成:${formatSyncSummary(run)}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(mapApiMessage(error, "企微 HR 主数据写入失败"));
|
||||
},
|
||||
});
|
||||
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: (format: "csv" | "xlsx") =>
|
||||
exportEmployees({
|
||||
@@ -320,8 +388,14 @@ export function PeoplePage() {
|
||||
setManagerMutation.isPending ||
|
||||
batchEnableMutation.isPending ||
|
||||
batchDisableMutation.isPending ||
|
||||
wecomDirectoryRetryMutation.isPending;
|
||||
wecomDirectoryRetryMutation.isPending ||
|
||||
wecomHrDryRunMutation.isPending ||
|
||||
wecomHrApplyMutation.isPending;
|
||||
const tableColumnCount = canManageUsers ? 8 : 7;
|
||||
const latestWecomHrRun = wecomHrMasterRunsQuery.data?.items?.[0] ?? null;
|
||||
const wecomHrMasterError = wecomHrMasterRunsQuery.isError
|
||||
? mapApiMessage(wecomHrMasterRunsQuery.error, "企微 HR 主数据同步未就绪")
|
||||
: "";
|
||||
|
||||
function toggleSelection(employeeId: string, checked: boolean) {
|
||||
setSelectedEmployeeIds((current) => {
|
||||
@@ -387,6 +461,54 @@ export function PeoplePage() {
|
||||
}
|
||||
/>
|
||||
|
||||
{canManageUsers ? (
|
||||
<div className="card-std p-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-base font-semibold text-zinc-900">企微 HR 主数据镜像</h2>
|
||||
{latestWecomHrRun ? (
|
||||
<StatusBadge
|
||||
status={latestWecomHrRun.status}
|
||||
label={`${wecomHrMasterModeLabel(latestWecomHrRun.mode)} · ${wecomHrMasterRunStatusLabel(latestWecomHrRun.status)}`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-600">
|
||||
{latestWecomHrRun
|
||||
? `${formatSyncSummary(latestWecomHrRun)} · ${latestWecomHrRun.finishedAt ?? latestWecomHrRun.createdAt ?? "-"}`
|
||||
: "企微/人事助手作为人员主源;OA 保存镜像、账号、权限和审计。"}
|
||||
</div>
|
||||
{wecomHrMasterError ? (
|
||||
<div className="text-xs text-amber-700">{wecomHrMasterError}</div>
|
||||
) : null}
|
||||
{latestWecomHrRun?.items?.length ? (
|
||||
<div className="text-xs text-zinc-500">
|
||||
最近失败项:{latestWecomHrRun.items.filter((item) => item.status === "failed" || item.status === "conflict").length}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={batchPending}
|
||||
onClick={() => wecomHrDryRunMutation.mutate()}
|
||||
>
|
||||
{wecomHrDryRunMutation.isPending ? "预检中..." : "同步预检"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={batchPending}
|
||||
onClick={() => setWecomHrApplyConfirmOpen(true)}
|
||||
>
|
||||
{wecomHrApplyMutation.isPending ? "写入中..." : "确认写入"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-4 p-4">
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
||||
<Input
|
||||
@@ -739,6 +861,17 @@ export function PeoplePage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={wecomHrApplyConfirmOpen}
|
||||
onOpenChange={setWecomHrApplyConfirmOpen}
|
||||
title="写入企微 HR 主数据镜像"
|
||||
description="将按当前企微/人事助手主数据创建或更新 OA 员工、部门、角色和企业微信绑定;不自动禁用不在职账号。"
|
||||
confirmLabel={wecomHrApplyMutation.isPending ? "写入中..." : "确认写入"}
|
||||
onConfirm={async () => {
|
||||
await wecomHrApplyMutation.mutateAsync();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(offboardingTarget)}
|
||||
onOpenChange={(open) => {
|
||||
|
||||
+3
-1
@@ -46,6 +46,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
|
||||
- 2026-05-18 WDRG 已完成“企微优先后续开发路线图”门禁:新增 `docs/企微优先开发路线图.md`,固定“路线图门禁 + 单功能 Issue CSV”的工程节奏。后续顺序为 `WECOM-CAPABILITY-POC-V1` -> `wecom_hr_master_sync_v1` -> `wecom_attendance_approval_mirror_v1` -> `oa_hr_emergency_mode_v1` -> `wecom_recovery_writeback_v1` -> `wecom_ruleops_v1`;阶段未退出前禁止跳阶段。本轮只更新路线图、交付规约、事实源、手册、项目 skill 和 Issue CSV,不改业务代码、不部署、不调用公网写接口。
|
||||
- 2026-05-18 WCP1 已完成企业微信能力 POC:新增 QA-only 只读 runner `scripts/qa/check-wecom-capability-poc.mjs`,本地不开启 live read 时可解释为 `PAUSE_FOR_API_POC`,本地开启 live read 但无真实 env 时可解释为 `PAUSE_FOR_CONFIG`;部署服务器容器内 live 只读探测报告为远端 `/tmp/wecom-capability-poc-wcp1/summary.json`,历史结论为 `PAUSE_FOR_CONFIG`。该报告继续作为初始配置缺口证据保留,当前 HR 主数据镜像准入以 WCPC 复跑结论为准。
|
||||
- 2026-05-18 WCPC 已完成 WCP1 配置收口复跑:先完成未补配置前远端基线复跑,报告目录 `/tmp/wecom-capability-poc-wcpc-baseline`,结论为 `PAUSE_FOR_CONFIG`;随后按用户要求自动查找并使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)复跑,只读报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json` 返回 `PASS / GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`,状态计数为 `CAN_REUSE_NOW=15`、`NEEDS_CONFIG=3`、`NEEDS_API_POC=1`、`KEEP_OA_PRIMARY=1`。本轮不部署、不改业务代码、不持久写服务器 `.env`;下一步允许规划 `wecom_hr_master_sync_v1`,审批模板/实例、回调配置和 HR 写接口仍留后续合同。
|
||||
- 2026-05-18 WHMS 已完成企微 HR 主数据镜像本地实现:新增 `wecom_hr_master_sync_v1` 默认 `force_off`,实现企业微信通讯录/人事助手读取、dry-run/apply、同步批次/逐员工记录、管理员 API、People 管理卡片和 QA-only runner;缺字段跳过、不在职只标记 `needs_offboarding_confirmation`,不自动禁用 OA 账号。本轮未部署公网,未执行生产真实员工批量 apply,不代表考勤/审批镜像、应急接管、恢复回写或 RuleOps 已完成。
|
||||
|
||||
## 关键证据索引
|
||||
|
||||
@@ -133,7 +134,8 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
|
||||
| WFDG 企微优先设计门禁 | 通过,无运行时变更 | Issue `issues/2026-05-18_12-21-01-wecom-first-design-gate.csv`;产品口径、PRD、产品设计规则、企业微信基础设施规约、统一审批规约、请假考勤规约、管理员手册、员工快速上手、文档索引、事实源矩阵和项目 skill 已回写。结论 `DONE_DESIGN_READY`,下一步进入 `WECOM-CAPABILITY-POC-V1`;不代表企微 HR/考勤镜像、OA 应急接管、恢复回写或 RuleOps 已实现。 |
|
||||
| WDRG 企微优先开发路线图门禁 | 通过,无运行时变更 | Issue `issues/2026-05-18_12-44-10-wecom-development-roadmap-gate.csv`;新增 `docs/企微优先开发路线图.md`,并回写交付规约、产品口径、PRD、管理员手册、员工快速上手、文档索引、事实源矩阵和项目 skill。结论 `DONE_ROADMAP_READY`;其下一步 `WECOM-CAPABILITY-POC-V1` 已由 WCP1/WCPC 执行并收口,不代表运行时迁移已完成。 |
|
||||
| WCP1 企业微信能力 POC | 完成只读 POC,历史结论 `PAUSE_FOR_CONFIG` | Issue `issues/2026-05-18_13-30-07-wecom-capability-poc-v1.csv`;新增 `scripts/qa/check-wecom-capability-poc.mjs`。本地脚本门禁通过,远端容器内 live 只读报告为 `/tmp/wecom-capability-poc-wcp1/summary.json`:`CAN_REUSE_NOW=9`、`NEEDS_CONFIG=8`、`NEEDS_API_POC=1`、`KEEP_OA_PRIMARY=1`。阻断为 `contact.member_read=wecom_errcode_48009`、`hr.staff_read=wecom_errcode_60021`、打卡同步未开启、缺审批模板/实例、假期权限 `wecom_errcode_301062` 和回调配置;该历史阻断已由 WCPC 复跑收口。 |
|
||||
| WCPC 企业微信能力 POC 配置收口 | 通过,结论 `GO_TO_HR_MASTER_SYNC` | Issue `issues/2026-05-18_15-01-23-wecom-capability-poc-config-closeout-v1.csv`;未补配置前远端基线复跑报告 `/tmp/wecom-capability-poc-wcpc-baseline` 为 `PASS / PAUSE_FOR_CONFIG`;随后使用既有在职测试号 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,`nextStageAllowed=true`,`blockingCapabilities=[]`,`CAN_REUSE_NOW=15`、`NEEDS_CONFIG=3`、`NEEDS_API_POC=1`、`KEEP_OA_PRIMARY=1`。下一步允许规划 `wecom_hr_master_sync_v1`;审批模板/实例、回调配置和 HR 写接口仍留后续合同。 |
|
||||
| WCPC 企业微信能力 POC 配置收口 | 通过,结论 `GO_TO_HR_MASTER_SYNC` | Issue `issues/2026-05-18_15-01-23-wecom-capability-poc-config-closeout-v1.csv`;未补配置前远端基线复跑报告 `/tmp/wecom-capability-poc-wcpc-baseline` 为 `PASS / PAUSE_FOR_CONFIG`;随后使用既有在职测试号 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,`nextStageAllowed=true`,`blockingCapabilities=[]`,`CAN_REUSE_NOW=15`、`NEEDS_CONFIG=3`、`NEEDS_API_POC=1`、`KEEP_OA_PRIMARY=1`。该准入已由 WHMS 承接;审批模板/实例、回调配置和 HR 写接口仍留后续合同。 |
|
||||
| WHMS 企微 HR 主数据镜像 | 本地已实现,待部署验证 | Issue `issues/2026-05-18_18-43-19-wecom-hr-master-sync-v1.csv`;新增 `wecom_hr_master_sync_v1`、同步批次/逐员工记录、企微通讯录/人事助手读取服务、管理员 API、People 管理卡片和 `scripts/qa/check-wecom-hr-master-sync.mjs`。默认 `force_off`,未部署公网,未执行生产真实员工批量 apply;考勤/审批镜像、审批回调、应急接管、恢复回写和 RuleOps 仍未完成。 |
|
||||
|
||||
## R7EP / OPS8 / QAF8 / UX8 / P2T 预检推进
|
||||
|
||||
|
||||
+10
-2
@@ -20,7 +20,7 @@
|
||||
- 企业微信打卡用于原始采集,保留其定位、人脸、Wi-Fi、规则与设备能力。
|
||||
- OA 后续同步企业微信打卡规则、排班、记录、日报、月报和 HR/考勤审批镜像,形成正式考勤口径。
|
||||
- 请假、补卡、外出、加班等考勤修正正常在企业微信完成;OA 只在应急模式下接管,并在企微恢复后 dry-run 和自动回写。
|
||||
- 后续实现必须另建 `wecom_hr_master_sync_v1`、`wecom_attendance_approval_mirror_v1`(承接旧口径 `wecom_native_approval_mirror_v1`)、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1` 或 `wecom_ruleops_v1` 合同。
|
||||
- `wecom_hr_master_sync_v1` 已本地实现但尚未部署公网;后续考勤/审批镜像、应急接管、恢复回写或 RuleOps 仍必须另建 `wecom_attendance_approval_mirror_v1`(承接旧口径 `wecom_native_approval_mirror_v1`)、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1` 或 `wecom_ruleops_v1` 合同。
|
||||
|
||||
### P0-3 按域镜像定义
|
||||
- 企微主办域镜像到 OA:通讯录/人事助手、打卡、请假、补卡、外出、加班、假期余额和 HR/考勤规则。
|
||||
@@ -50,7 +50,15 @@
|
||||
- WCPC 只做企业微信后台配置收口和只读 POC 复跑,不改业务代码、不部署、不持久写服务器 `.env`。
|
||||
- 已按用户要求自动查找并使用既有在职低风险测试号 `SMOKE002/测试员工` 复跑;报告和文档只记录脱敏企业微信 userid 前缀 `oa_smoke***`,不得使用已离职/禁用的 `HRWDTEST-20260517214548` 作为主测试号。
|
||||
- 复跑报告为远端 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。
|
||||
- 当前允许下一步规划 `wecom_hr_master_sync_v1`;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理,不得直接跳到考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
- WCPC 已由 WHMS 承接;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理,不得直接跳到考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
|
||||
### P0-8 WHMS 企微 HR 主数据镜像口径
|
||||
- WHMS 已本地实现 `wecom_hr_master_sync_v1`,默认 `force_off`,尚未部署公网,未执行生产真实员工批量 apply。
|
||||
- 企微/人事助手作为 HR 主数据源;OA 同步员工档案、部门、主管、职位、手机号、邮箱、默认角色、企业微信绑定和审计。
|
||||
- dry-run 只生成差异和同步记录,不写员工主数据;apply 需要管理员确认,生产真实员工批量 apply 需单独批准。
|
||||
- 缺姓名、手机号、邮箱或部门时记录 `missing_required_field` 并跳过,不生成占位数据;企微不在职状态记录 `needs_offboarding_confirmation`,不自动禁用 OA 账号。
|
||||
- 已有 OA 员工只更新安全主数据字段,不覆盖银行账户、身份证、工资、合同附件或历史审批。
|
||||
- WHMS 不代表考勤/审批镜像、审批回调、OA 应急接管、恢复回写或 RuleOps 已完成;下一阶段仍需另建 `wecom_attendance_approval_mirror_v1`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+33
-7
@@ -2,10 +2,10 @@
|
||||
|
||||
**文档状态**:当前开发路线图事实源。
|
||||
**分类**:开发治理 / 路线图。
|
||||
**适用时间**:WDRG / WCP1 之后。
|
||||
**关联 Issue**:`WDRG,WCP1`。
|
||||
**适用时间**:WDRG / WCP1 / WHMS 之后。
|
||||
**关联 Issue**:`WDRG,WCP1,WHMS`。
|
||||
**最近审计**:2026-05-18。
|
||||
**不得误读**:本文只控制后续开发节奏、阶段顺序和门禁,不代表企微 HR/考勤镜像、OA 应急接管、恢复回写或 RuleOps 已完成运行时实现。
|
||||
**不得误读**:本文只控制后续开发节奏、阶段顺序和门禁。WHMS 后只代表企微 HR 主数据镜像已本地实现,尚未部署;不代表企微考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps 已完成运行时实现。
|
||||
|
||||
## 1. 路线图原则
|
||||
|
||||
@@ -23,15 +23,16 @@
|
||||
| `WFDG` | 已完成 | 固定设计门禁:先判定主办方、数据边界、入口交互、隐私消息、同步审计、应急接管、恢复回写和 RuleOps 边界。 |
|
||||
| `WDRG` | 已完成 | 固定开发路线图和节奏;其下一步 `WECOM-CAPABILITY-POC-V1` 已由 WCP1/WCPC 执行并收口。 |
|
||||
| `WCP1` | 已完成 | 已完成只读 POC runner 和远端只读探测,历史结论为 `PAUSE_FOR_CONFIG`;后续由 WCPC 复跑收口。 |
|
||||
| `WCPC` | 已完成 | 已使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)完成远端只读复跑,结论 `GO_TO_HR_MASTER_SYNC`;下一步允许规划 `wecom_hr_master_sync_v1`,不得直接跳到考勤/审批镜像、应急、回写或 RuleOps。 |
|
||||
| `WCPC` | 已完成 | 已使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)完成远端只读复跑,结论 `GO_TO_HR_MASTER_SYNC`;该准入已由 WHMS 承接,不得直接跳到考勤/审批镜像、应急、回写或 RuleOps。 |
|
||||
| `WHMS` | 本地已实现,待部署/公网验证 | 已新增 `wecom_hr_master_sync_v1`、企微/人事助手主数据读取服务、dry-run/apply、同步批次/逐员工记录、People 管理入口和 QA-only runner;默认 `force_off`,未部署,未执行生产真实员工批量 apply。 |
|
||||
|
||||
## 3. 阶段路线
|
||||
|
||||
| 顺序 | 阶段 | 目标 | 进入条件 | 退出条件 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `WECOM-CAPABILITY-POC-V1` | 只读或低风险探测企微 API 能力,输出能力矩阵。 | WDRG 完成;企微后台权限、Secret、可信 IP、模板和可见范围按现状记录。 | 人事助手、通讯录、打卡、审批、假期余额、回调和权限配置均有 `CAN_REUSE_NOW`、`NEEDS_CONFIG`、`NEEDS_API_POC`、`KEEP_OA_PRIMARY` 或 `NOT_SUPPORTED_BY_API` 结论。 |
|
||||
| 2 | `wecom_hr_master_sync_v1` | 企微/人事助手员工、部门、主管、职位、联系方式和在职状态镜像到 OA。 | POC 证明人事助手/通讯录读取和回调能力可用,或明确缺口处理方式。 | OA 可自动创建/更新员工档案、账号、角色、企微绑定和审计;缺配置时记录 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 |
|
||||
| 3 | `wecom_attendance_approval_mirror_v1` | 企微打卡、请假、补卡、外出、加班、假期余额和审批历史镜像到 OA。 | HR 主数据镜像稳定;审批模板、打卡规则和回调能力已完成 POC 或有可解释替代方案。 | OA 可查询企微 HR/考勤镜像、附件摘要、审批历史、考勤解释和审计;不把 OA 主办高敏审批迁到企微。 |
|
||||
| 2 | `wecom_hr_master_sync_v1` | 企微/人事助手员工、部门、主管、职位、联系方式和在职状态镜像到 OA。 | POC 证明人事助手/通讯录读取和回调能力可用,或明确缺口处理方式。 | 本地实现通过门禁后进入待部署/公网验证;OA 可自动创建/更新员工档案、账号、角色、企微绑定和审计;缺配置时记录 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG`。 |
|
||||
| 3 | `wecom_attendance_approval_mirror_v1` | 企微打卡、请假、补卡、外出、加班、假期余额和审批历史镜像到 OA。 | WHMS 部署并完成受控 dry-run/apply 验证;审批模板、打卡规则和回调能力已完成 POC 或有可解释替代方案。 | OA 可查询企微 HR/考勤镜像、附件摘要、审批历史、考勤解释和审计;不把 OA 主办高敏审批迁到企微。 |
|
||||
| 4 | `oa_hr_emergency_mode_v1` | 企业微信不可用时,由管理员人工开启 OA HR/考勤应急接管。 | 企微镜像数据可支撑对账;OA 应急入口、权限和审计方案明确。 | 管理员可填写原因、范围、预计结束时间和责任人后开启/关闭;应急单据带 `EMERGENCY` 标记。 |
|
||||
| 5 | `wecom_recovery_writeback_v1` | 企微恢复后,OA 应急数据先 dry-run,再回写企微。 | 应急接管模式完成;企微写回接口、冲突规则和回滚策略已单独批准。 | 系统生成成功、失败、跳过和冲突清单;无冲突项可自动回写,冲突进入人工确认。 |
|
||||
| 6 | `wecom_ruleops_v1` | AI 草拟企微规则、生成差异报告和 dry-run,人工确认后写入企微。 | 前述同步/回写链路稳定;企微规则 API 能力和权限已验证。 | 每次写入保存前后快照、确认人、API `errcode`、失败原因、回滚建议和审计记录。 |
|
||||
@@ -111,4 +112,29 @@ POC 默认只读或低风险探测。任何生产写操作、企微规则写入
|
||||
- `callback.config=NEEDS_CONFIG`:本轮只读报告未证明真实回调配置存在。
|
||||
- `hr.staff_update_classification=NEEDS_API_POC`:人事助手资料更新属于写接口,需单独批准后再做低风险 POC。
|
||||
|
||||
当前允许进入下一步 `wecom_hr_master_sync_v1` 的方案和 Issue CSV 规划;考勤/审批镜像仍需先补审批模板、审批单、回调配置并另建 `wecom_attendance_approval_mirror_v1` 合同,不得把 WCPC 误读为运行时迁移已完成。
|
||||
WCPC 已允许进入 `wecom_hr_master_sync_v1`。WHMS 已承接该阶段并完成本地实现,考勤/审批镜像仍需先补审批模板、审批单、回调配置并另建 `wecom_attendance_approval_mirror_v1` 合同,不得把 WCPC 或 WHMS 误读为考勤/审批运行时迁移已完成。
|
||||
|
||||
## 9. WHMS HR 主数据镜像状态
|
||||
|
||||
2026-05-18 已建立 `WECOM-HR-MASTER-SYNC-V1` 合同,范围限定为企业微信通讯录/人事助手只读主数据同步到 OA。该阶段不做考勤/审批镜像、审批回调、恢复回写、RuleOps、HR 写企微接口、采购/合同/财务迁移。
|
||||
|
||||
本地实现内容:
|
||||
|
||||
- 新增 feature flag `wecom_hr_master_sync_v1`,默认 `force_off`;新增配置 `WECOM_HR_MASTER_SYNC_ENABLED`、`WECOM_HR_ASSISTANT_SECRET`,允许已验证工作台应用 Secret 作为只读 fallback 并记录 token source。
|
||||
- 新增管理 API:`POST /api/employees/wecom-hr-master-sync/runs`、`GET /api/employees/wecom-hr-master-sync/runs`、`GET /api/employees/wecom-hr-master-sync/runs/:id`。
|
||||
- 新增同步批次和逐员工记录,支持 `dry_run` 不写主数据、`apply` 写入 OA 员工、部门映射、默认 `employee` 角色、企业微信绑定、主管关系和审计。
|
||||
- People/人员管理页新增“企微 HR 主数据镜像”管理卡片,管理员可执行预检、确认写入并查看最近同步摘要;普通员工不可见入口。
|
||||
- 新增 QA-only runner `scripts/qa/check-wecom-hr-master-sync.mjs`,默认不做 live 写入;apply 必须显式传入 userids 并设置批准环境变量。
|
||||
|
||||
数据策略:
|
||||
|
||||
- 匹配优先级:`wecom_identity_bindings.external_user_id` -> 企微 userid/工号 -> 手机号 -> 邮箱。
|
||||
- 新员工创建要求姓名、手机号、邮箱和部门;缺字段时 `SKIP missing_required_field`,不生成占位邮箱或占位部门。
|
||||
- 企微不在职或禁用状态记录 `needs_offboarding_confirmation`,不自动禁用 OA 账号;离职仍走 OA checklist。
|
||||
- 已有 OA 员工只更新安全主数据字段:姓名、部门、职位、直属主管、手机号、邮箱和企微绑定;不覆盖银行账户、身份证、工资、合同附件或历史审批。
|
||||
|
||||
当前状态:
|
||||
|
||||
- 本地代码、文档和 QA runner 已实现,仍需通过本轮门禁后提交。
|
||||
- 尚未部署到公网,未执行生产真实员工批量 `apply`。
|
||||
- 下一阶段仍是 `wecom_attendance_approval_mirror_v1`,但必须在 WHMS 部署和受控验证后另建合同,不能直接跳到应急接管、恢复回写或 RuleOps。
|
||||
|
||||
+12
-11
@@ -66,7 +66,8 @@
|
||||
| `WFDG` | `issues/2026-05-18_12-21-01-wecom-first-design-gate.csv` | `EVIDENCE` | 企微优先设计规约门禁已收口:固定主办方与数据边界矩阵、入口交互、隐私附件消息、同步审计失败处理、应急接管、恢复回写、RuleOps 和 `WECOM-CAPABILITY-POC-V1` 验收口径 |
|
||||
| `WDRG` | `issues/2026-05-18_12-44-10-wecom-development-roadmap-gate.csv` | `EVIDENCE` | 企微优先开发路线图门禁已收口:固定路线图门禁、单功能 Issue CSV 节奏、阶段顺序、并行规则和下一轮 POC 合同口径 |
|
||||
| `WCP1` | `issues/2026-05-18_13-30-07-wecom-capability-poc-v1.csv` | `EVIDENCE` | 企业微信能力 POC 已完成,历史结论 `PAUSE_FOR_CONFIG`;初始配置缺口已由 WCPC 复跑收口 |
|
||||
| `WCPC` | `issues/2026-05-18_15-01-23-wecom-capability-poc-config-closeout-v1.csv` | `CURRENT` | WCP1 配置收口已用既有在职测试号复跑通过,结论 `GO_TO_HR_MASTER_SYNC`,允许下一步规划 `wecom_hr_master_sync_v1` |
|
||||
| `WCPC` | `issues/2026-05-18_15-01-23-wecom-capability-poc-config-closeout-v1.csv` | `EVIDENCE` | WCP1 配置收口已用既有在职测试号复跑通过,结论 `GO_TO_HR_MASTER_SYNC`,已由 WHMS 承接 |
|
||||
| `WHMS` | `issues/2026-05-18_18-43-19-wecom-hr-master-sync-v1.csv` | `CURRENT` | 企微 HR 主数据镜像已本地实现:`wecom_hr_master_sync_v1`、同步批次/逐员工记录、dry-run/apply、管理员 API、People 管理入口和 QA-only runner;默认 `force_off`,未部署公网,未执行生产真实员工批量 apply |
|
||||
|
||||
旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。
|
||||
|
||||
@@ -74,23 +75,23 @@
|
||||
|
||||
| Doc_ID | Path | Category | Authority | Status | Owner | Related_Issues | Evidence | Supersedes | Last_Audited | Audit_Method | Risk | Notes |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC` | `rg` | none | 2026-05-18 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 |
|
||||
| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC` | `audit-docs` | none | 2026-05-18 | `audit-docs --check-registry` | medium | 判断文档权威性 |
|
||||
| `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC` | Issue CSV | `docs/requirements-specification.md` | 2026-05-18 | `rg` | high | WCPC 后定义当前允许规划 HR 主数据镜像;ERP 未来不接入 OA |
|
||||
| `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T,WFDG,WDRG,WCP1,WCPC` | `rg` | none | 2026-05-18 | `rg` | high | 记录已拍板产品边界、企微/OA 设计门禁、开发路线图和 WCPC 准入结论 |
|
||||
| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS` | `rg` | none | 2026-05-18 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 |
|
||||
| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1,AUTO-REMAINING-CLOSEOUT,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS` | `audit-docs` | none | 2026-05-18 | `audit-docs --check-registry` | medium | 判断文档权威性 |
|
||||
| `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WDRG,WCP1,WCPC,WHMS` | Issue CSV | `docs/requirements-specification.md` | 2026-05-18 | `rg` | high | WHMS 后定义 HR 主数据镜像本地实现、待部署和下一阶段边界;ERP 未来不接入 OA |
|
||||
| `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T,WFDG,WDRG,WCP1,WCPC,WHMS` | `rg` | none | 2026-05-18 | `rg` | high | 记录已拍板产品边界、企微/OA 设计门禁、开发路线图、WCPC 准入结论和 WHMS 本地实现状态 |
|
||||
| `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9,WFDG` | Playwright/视觉审查/DG8/DG9/WFDG | none | 2026-05-18 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型;WFDG 后区分 OA 主办、企微镜像和 OA 应急接管 |
|
||||
| `ROADMAP-WECOM` | `docs/企微优先开发路线图.md` | 开发治理 | 企微优先开发路线图事实源 | `CURRENT` | product/dev | `WDRG,WCP1,WCPC` | Issue CSV | none | 2026-05-18 | `rg` | high | WCPC 后记录企微 HR/考勤路线阶段顺序和当前允许规划 HR 主数据镜像 |
|
||||
| `ROADMAP-WECOM` | `docs/企微优先开发路线图.md` | 开发治理 | 企微优先开发路线图事实源 | `CURRENT` | product/dev | `WDRG,WCP1,WCPC,WHMS` | Issue CSV | none | 2026-05-18 | `rg` | high | WHMS 后记录企微 HR/考勤路线阶段顺序、HR 主数据镜像本地实现和下一阶段准入边界 |
|
||||
| `DELIVERY` | `docs/开发交付与变更管理规约.md` | 开发治理 | 开发交付事实源 | `CURRENT` | dev/qa | `GOV8,WDRG` | `rg` | none | 2026-05-18 | `rg` | high | Issue、review、commit、交付规则;WDRG 后采用路线图门禁 + 单功能 Issue CSV |
|
||||
| `QA-GATE` | `docs/测试规约与回归门禁.md` | 测试验收 | 测试和回归门禁事实源 | `CURRENT` | qa | `QAF8,GOV8` | 回归脚本/Playwright | none | 2026-05-13 | `rg` | high | 测试计划和 DONE 门禁 |
|
||||
| `AGENT-PRACTICE` | `docs/Agent开发交付实践.md` | Agent 治理 | Agent 协作事实源 | `CURRENT` | dev | `GOV8,DG8` | Skill 校验 | none | 2026-05-13 | `rg` | medium | Agent 协作开发实践和设计主参考层级 |
|
||||
| `AGENTS` | `AGENTS.md` | 执行硬约束 | 项目硬规则 | `CURRENT` | lead | `GOV8` | 人工核对 | none | 2026-05-13 | `rg` | high | 不可违反约束 |
|
||||
| `SKILL-HYB` | `.agents/skills/hyb-oa-delivery/SKILL.md` | Skill | 项目交付 Skill | `CURRENT` | dev | `GOV8,DG8,WFDG,WDRG,WCP1,WCPC` | quick_validate | none | 2026-05-18 | `quick_validate.py` | medium | 每轮交付操作入口;企微/OA 后续先读路线图和 WCPC 结论,当前可规划 HR 主数据镜像 |
|
||||
| `SKILL-HYB` | `.agents/skills/hyb-oa-delivery/SKILL.md` | Skill | 项目交付 Skill | `CURRENT` | dev | `GOV8,DG8,WFDG,WDRG,WCP1,WCPC,WHMS` | quick_validate | none | 2026-05-18 | `quick_validate.py` | medium | 每轮交付操作入口;企微/OA 后续先读路线图和 WHMS 状态,不得跳过部署/验证直接做考勤审批镜像 |
|
||||
| `REQ-WF` | `docs/需求-统一审批中心规约.md` | 需求事实源 | 审批模块规约 | `CURRENT` | product/backend | `AP6,WF8,WFOC,DBWFOC,WFDG` | workflow 回归 | none | 2026-05-18 | `rg` | high | OA 主办审批、企微镜像和 OA 应急接管边界 |
|
||||
| `REQ-OCR` | `docs/需求-报销发票OCR规约.md` | 需求事实源 | OCR 模块规约 | `CURRENT` | product/backend | `RI6,OCR8,BIZ8` | OCR 回归 | none | 2026-05-13 | `rg` | high | OCR 和发票边界 |
|
||||
| `RULE-REIMB` | `docs/报销发票附件规则.md` | 需求事实源 | 报销发票附件规则 | `CURRENT` | product/finance | `RI6,BIZ8` | 报销回归 | none | 2026-05-13 | `rg` | high | 报销、发票、附件细则 |
|
||||
| `REQ-WECOM` | `docs/需求-企业微信基础设施规约.md` | 需求事实源 | 企业微信边界规约 | `CURRENT` | product/backend | `R7E,ORG8,WF8,ATTV1,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WCP1,WCPC` | live smoke/真机反馈/Issue CSV | none | 2026-05-18 | `rg` | high | 企微优先承担 HR/考勤员工日常审批和人事/打卡能力;WCPC 记录 HR 主数据镜像准入通过;采购、合同、项目、报销和财务继续 OA 主办 |
|
||||
| `REQ-WECOM` | `docs/需求-企业微信基础设施规约.md` | 需求事实源 | 企业微信边界规约 | `CURRENT` | product/backend | `R7E,ORG8,WF8,ATTV1,HR-WECOM-DIRECTORY-V1,WFOC,DBWFOC,WFDG,WCP1,WCPC,WHMS` | live smoke/真机反馈/Issue CSV | none | 2026-05-18 | `rg` | high | 企微优先承担 HR/考勤员工日常审批和人事/打卡能力;WHMS 记录 HR 主数据镜像本地实现;采购、合同、项目、报销和财务继续 OA 主办 |
|
||||
| `RUNBOOK-TRIAL` | `docs/试运行与自动化验证手册.md` | 操作手册 | 试运行验证手册 | `CURRENT` | qa/ops | `R7E,QAOPS8` | live readiness | none | 2026-05-13 | `rg` | medium | 本地、公网、企业微信和真实 OCR 验证 |
|
||||
| `MANUAL-ADMIN` | `docs/管理员手册.md` | 操作手册 | 管理员手册 | `CURRENT` | ops/admin | `ORG8,R7E,WFOC,DBWFOC,WFDG` | 人工验收 | none | 2026-05-18 | `rg` | medium | 管理员上线、企微优先边界、OA 主办数据边界、OA 应急接管、恢复回写和 POC 前置 |
|
||||
| `MANUAL-ADMIN` | `docs/管理员手册.md` | 操作手册 | 管理员手册 | `CURRENT` | ops/admin | `ORG8,R7E,WFOC,DBWFOC,WFDG,WCPC,WHMS` | 人工验收 | none | 2026-05-18 | `rg` | medium | 管理员上线、企微优先边界、OA 主办数据边界、HR 主数据镜像、OA 应急接管、恢复回写和 POC 前置 |
|
||||
| `MANUAL-EMPLOYEE` | `docs/员工快速上手.md` | 操作手册 | 员工手册 | `CURRENT` | product | `R7E,WFOC,DBWFOC,WFDG` | 人工验收 | none | 2026-05-18 | `rg` | low | 员工侧使用说明,HR/考勤优先企业微信,采购/合同/财务仍在 OA;区分企微镜像和 OA 应急接管 |
|
||||
| `R7-DRAFT` | `docs/trialrun/R7扩大试运行草案.md` | 草案 | R7 准备材料 | `DRAFT` | product/qa | `R7E` | R7EP 报告 | none | 2026-05-13 | `rg` | medium | 不代表当前 R7E 执行状态 |
|
||||
| `OPS-RUNBOOK` | `docs/trialrun/OPS7运维交接Runbook.md` | 运维草案 | OPS 准备材料 | `DRAFT` | ops | `OPS8,QAOPS8` | 只读检查 | none | 2026-05-13 | `rg` | medium | 不代表恢复演练已执行 |
|
||||
@@ -107,7 +108,7 @@
|
||||
| `docs/文档事实源状态矩阵.md` | 当前事实源 | 文档分类和恢复规则 |
|
||||
| `docs/海音达OA-MVP需求规格说明书.md` | 当前事实源 | PRD 主事实源 |
|
||||
| `docs/产品设计规则.md` | 当前事实源 | PC、移动、企业微信设计规则;DG8 主 UI/布局参考层级 |
|
||||
| `docs/企微优先开发路线图.md` | 当前事实源 | WDRG/WCP1/WCPC 后企微优先 HR/考勤改造阶段顺序、准入/退出门禁、禁止跳阶段和当前 HR 主数据镜像准入 |
|
||||
| `docs/企微优先开发路线图.md` | 当前事实源 | WDRG/WCP1/WCPC/WHMS 后企微优先 HR/考勤改造阶段顺序、准入/退出门禁、禁止跳阶段和当前 HR 主数据镜像本地实现状态 |
|
||||
| `docs/产品口径.md` | 当前事实源 | 产品边界和业务规则 |
|
||||
| `docs/需求-统一审批中心规约.md` | 当前事实源 | 审批、催办、超时、效率摘要 |
|
||||
| `docs/报销发票附件规则.md` | 当前事实源 | 报销、发票、附件细则 |
|
||||
@@ -182,4 +183,4 @@
|
||||
|
||||
- `ME6-3` 代表性企业微信真机门禁已关闭。
|
||||
- MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。
|
||||
- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核、ATTV1 考勤统一入口、AUTO-REMAINING-CLOSEOUT 剩余自动化收口、HRWD-CONTACT-LIVE-TEST 公网企微通讯录真实写入闭环、WFOC 需求重构、DBWFOC 数据边界修正、WFDG 设计门禁、WDRG 路线图门禁和 WCPC 配置复跑均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现的公网 `payment_batch_v1=force_off` 漂移已由 AUTOREM 恢复为 `force_on`,正式自动巡检结论为 `GO_TO_OPERATE`;`erp_sync=force_off`,ERP 未来不接入 OA,正式支付接口、图片 OCR 和银行流水导入继续不进入当前路线。WCP1 已完成企业微信能力 QA-only 只读 POC,历史结论为 `PAUSE_FOR_CONFIG`;WCPC 使用既有在职测试号 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。HR/考勤员工日常审批优先复用企业微信,OA 目标是按域镜像、人工接管、恢复回写和 RuleOps;采购、费用、合同、项目、报销、付款、AP、代账交接、审计和高敏附件继续 OA 主办。该方向尚未实现运行时迁移,当前允许进入 `wecom_hr_master_sync_v1` 方案和 Issue CSV 规划;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理。后续仍须按 [企微优先开发路线图](./企微优先开发路线图.md) 另拆 `wecom_hr_master_sync_v1`、`wecom_attendance_approval_mirror_v1`、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1`;阶段未退出前不得跳阶段。HRWD 已完成 OA 驱动企业微信通讯录同步,并通过 `HRWDTEST-20260517214548` 真实公网 create/update/disable 验证;测试成员按审计要求保留离职/禁用状态。
|
||||
- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核、ATTV1 考勤统一入口、AUTO-REMAINING-CLOSEOUT 剩余自动化收口、HRWD-CONTACT-LIVE-TEST 公网企微通讯录真实写入闭环、WFOC 需求重构、DBWFOC 数据边界修正、WFDG 设计门禁、WDRG 路线图门禁和 WCPC 配置复跑均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现的公网 `payment_batch_v1=force_off` 漂移已由 AUTOREM 恢复为 `force_on`,正式自动巡检结论为 `GO_TO_OPERATE`;`erp_sync=force_off`,ERP 未来不接入 OA,正式支付接口、图片 OCR 和银行流水导入继续不进入当前路线。WCP1 已完成企业微信能力 QA-only 只读 POC,历史结论为 `PAUSE_FOR_CONFIG`;WCPC 使用既有在职测试号 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。WHMS 已本地实现 `wecom_hr_master_sync_v1`、同步批次/逐员工记录、dry-run/apply、管理员 API、People 管理入口和 QA-only runner;默认 `force_off`,尚未部署公网,未执行生产真实员工批量 apply。HR/考勤员工日常审批优先复用企业微信,OA 目标是按域镜像、人工接管、恢复回写和 RuleOps;采购、费用、合同、项目、报销、付款、AP、代账交接、审计和高敏附件继续 OA 主办。考勤/审批镜像、审批回调、OA 应急接管、恢复回写和 RuleOps 尚未实现;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理。后续仍须按 [企微优先开发路线图](./企微优先开发路线图.md) 在 WHMS 部署和受控验证后另拆 `wecom_attendance_approval_mirror_v1`、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1`;阶段未退出前不得跳阶段。HRWD 已完成 OA 驱动企业微信通讯录同步,并通过 `HRWDTEST-20260517214548` 真实公网 create/update/disable 验证;测试成员按审计要求保留离职/禁用状态。
|
||||
|
||||
+7
-5
@@ -20,7 +20,7 @@
|
||||
|
||||
## 最近事实源链
|
||||
|
||||
R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1 -> AUTO-REMAINING-CLOSEOUT -> HR-WECOM-DIRECTORY-V1 -> HRWD-CONTACT-LIVE-TEST -> WCFB -> WFOC -> DBWFOC -> WFDG -> WDRG -> WCP1 -> WCPC。
|
||||
R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1 -> AUTO-REMAINING-CLOSEOUT -> HR-WECOM-DIRECTORY-V1 -> HRWD-CONTACT-LIVE-TEST -> WCFB -> WFOC -> DBWFOC -> WFDG -> WDRG -> WCP1 -> WCPC -> WHMS。
|
||||
|
||||
- R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。
|
||||
- ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。
|
||||
@@ -58,9 +58,10 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
|
||||
- WFOC 是企微优先 + OA 无痛接管需求重构合同:用户已确认优先复用企业微信成熟能力,OA 做镜像、人工接管、恢复回写、财务闭环和审计中台;其“HR/考勤/行政审批”和“完整镜像”宽口径已由 DBWFOC 修正为 HR/考勤员工日常审批和按域镜像。本轮只冻结需求和事实源,不改业务代码、不部署、不切换公网配置;后续实现需拆 `wecom_hr_master_sync_v1`、`wecom_native_approval_mirror_v1`、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1`。
|
||||
- DBWFOC 是 WFOC 后的数据边界修正合同:将“企微优先”收窄为 HR/考勤员工日常审批优先,采购、费用、合同、项目、报销、付款、AP、代账交接、审计和高敏附件继续 OA 主办;企业微信成熟功能可作为流程/字段/交互/规则参考,OA 主办审批只向企业微信发送脱敏提醒和 OA deeplink,不上传高敏附件到企微原生审批。
|
||||
- WFDG 是 DBWFOC 后的设计规约门禁合同:固定后续开发前必须先判定主办方、主事实源、操作端、镜像方向、附件归属和审计责任;PC/移动页面需区分 `OA 主办`、`企微镜像` 和 `OA 应急接管`;下一轮进入 `WECOM-CAPABILITY-POC-V1`,输出 `CAN_REUSE_NOW`、`NEEDS_CONFIG`、`NEEDS_API_POC`、`KEEP_OA_PRIMARY`、`NOT_SUPPORTED_BY_API` 能力矩阵。本轮不改业务代码、不部署、不调用生产写接口。
|
||||
- WDRG 是 WFDG 后的开发路线图门禁合同:新增 [企微优先开发路线图](./企微优先开发路线图.md),固定“路线图门禁 + 单功能 Issue CSV”的开发节奏和阶段顺序;其下一步 `WECOM-CAPABILITY-POC-V1` 已由 WCP1/WCPC 执行并收口,当前允许规划 HR 主数据镜像,不得跳过阶段直接开发考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。本轮不改业务代码、不部署、不调用公网写接口。
|
||||
- WDRG 是 WFDG 后的开发路线图门禁合同:新增 [企微优先开发路线图](./企微优先开发路线图.md),固定“路线图门禁 + 单功能 Issue CSV”的开发节奏和阶段顺序;其下一步 `WECOM-CAPABILITY-POC-V1` 已由 WCP1/WCPC 执行并收口,HR 主数据镜像已由 WHMS 本地实现,不得跳过阶段直接开发考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。本轮不改业务代码、不部署、不调用公网写接口。
|
||||
- WCP1 是 WDRG 后的企业微信能力 POC 合同:新增 QA-only 只读 runner `scripts/qa/check-wecom-capability-poc.mjs`,对人事助手、通讯录、打卡、审批、假期余额、回调和权限配置输出脱敏能力矩阵。远端 live 只读历史结论为 `PAUSE_FOR_CONFIG`,作为初始配置缺口证据保留;当前 HR 主数据镜像准入以 WCPC 复跑结论为准。
|
||||
- WCPC 是 WCP1 后的配置收口合同:已使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`。下一步允许规划 `wecom_hr_master_sync_v1`;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理。
|
||||
- WCPC 是 WCP1 后的配置收口合同:已使用既有在职低风险测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`。WHMS 已承接该准入结论;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理。
|
||||
- WHMS 是 WCPC 后的企微 HR 主数据镜像合同:已本地实现 `wecom_hr_master_sync_v1`、企微/人事助手读取服务、dry-run/apply、同步批次/逐员工记录、People 管理入口和 QA-only runner;默认 `force_off`,未部署公网,未执行生产真实员工批量 apply。下一阶段仍需在 WHMS 部署和受控验证后另建 `wecom_attendance_approval_mirror_v1`。
|
||||
|
||||
## 当前活跃治理流
|
||||
|
||||
@@ -107,7 +108,8 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
|
||||
| `WFDG` | `issues/2026-05-18_12-21-01-wecom-first-design-gate.csv` | 已收口:企微优先设计规约门禁完成,结论 `DONE_DESIGN_READY` | 固定主办方/数据边界矩阵、入口与交互规约、隐私附件消息规约、同步审计失败处理、应急接管、恢复回写、RuleOps 和下一轮 POC 验收口径 | 不改业务代码、不新增 API/schema/UI、不部署、不改公网配置、不调用生产写接口;不代表运行时迁移已完成 |
|
||||
| `WDRG` | `issues/2026-05-18_12-44-10-wecom-development-roadmap-gate.csv` | 已收口:企微优先开发路线图门禁 | 固定路线图门禁、阶段顺序、单功能 Issue CSV 节奏、并行规则和下一轮 POC 合同口径 | 不改业务代码、不新增 API/schema/UI、不部署、不改公网配置、不调用公网写接口;不代表任何运行时迁移已完成 |
|
||||
| `WCP1` | `issues/2026-05-18_13-30-07-wecom-capability-poc-v1.csv` | 已收口:企业微信能力 POC 已完成,历史结论 `PAUSE_FOR_CONFIG` | QA-only 只读探测人事助手、通讯录、打卡、审批、假期余额、回调和权限配置;输出能力矩阵和配置阻断清单 | 不改业务代码、不新增 API/schema/UI、不部署、不调用生产写接口;初始阻断已由 WCPC 复跑收口 |
|
||||
| `WCPC` | `issues/2026-05-18_15-01-23-wecom-capability-poc-config-closeout-v1.csv` | 已收口:既有测试号 POC 复跑通过 | 使用 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成远端只读复跑,结论 `GO_TO_HR_MASTER_SYNC`,允许下一步规划 `wecom_hr_master_sync_v1` | 不开发业务功能、不部署、不持久改服务器 env;考勤/审批镜像仍需后续补审批模板、审批单和回调配置 |
|
||||
| `WCPC` | `issues/2026-05-18_15-01-23-wecom-capability-poc-config-closeout-v1.csv` | 已收口:既有测试号 POC 复跑通过 | 使用 `SMOKE002/测试员工`(脱敏 `oa_smoke***`)完成远端只读复跑,结论 `GO_TO_HR_MASTER_SYNC`,已由 WHMS 承接 | 不开发业务功能、不部署、不持久改服务器 env;考勤/审批镜像仍需后续补审批模板、审批单和回调配置 |
|
||||
| `WHMS` | `issues/2026-05-18_18-43-19-wecom-hr-master-sync-v1.csv` | 本地已实现:企微 HR 主数据镜像待部署验证 | 新增 `wecom_hr_master_sync_v1`、Prisma 同步批次/逐员工记录、企微通讯录/人事助手读取、dry-run/apply、管理员 API、People 管理卡片和 QA-only runner | 不部署、不启用公网 flag、不对真实员工批量 apply;不做考勤/审批镜像、审批回调、应急接管、恢复回写或 RuleOps |
|
||||
|
||||
旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。
|
||||
|
||||
@@ -130,7 +132,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
|
||||
| [海音达OA-MVP需求规格说明书](./海音达OA-MVP需求规格说明书.md) | PRD 主事实源 |
|
||||
| [文档事实源状态矩阵](./文档事实源状态矩阵.md) | 文档分类、删除恢复和冲突规则 |
|
||||
| [产品设计规则](./产品设计规则.md) | PC、移动、企业微信 WebView 设计事实源;DG8 后飞书工作台/飞书审批为唯一主 UI/布局参考;DG9 后按页面分层和样板选型 |
|
||||
| [企微优先开发路线图](./企微优先开发路线图.md) | WDRG/WCP1/WCPC 后企微优先 HR/考勤改造的阶段顺序、准入/退出门禁、并行规则、禁止跳阶段和当前 HR 主数据镜像准入事实源 |
|
||||
| [企微优先开发路线图](./企微优先开发路线图.md) | WDRG/WCP1/WCPC/WHMS 后企微优先 HR/考勤改造的阶段顺序、准入/退出门禁、并行规则、禁止跳阶段和当前 HR 主数据镜像本地实现事实源 |
|
||||
| [产品口径](./产品口径.md) | 已拍板产品边界和业务规则 |
|
||||
| [需求 - 统一审批中心规约](./需求-统一审批中心规约.md) | OA 主办审批、企微主办 HR/考勤员工日常审批镜像、催办、超时提醒、审批效率 |
|
||||
| [报销发票附件规则](./报销发票附件规则.md) | 报销、发票、OCR、附件细则 |
|
||||
|
||||
+16
-1
@@ -111,7 +111,22 @@ WCP1 历史结论为 `PAUSE_FOR_CONFIG`,不是 `GO_TO_HR_MASTER_SYNC`;该阻
|
||||
|
||||
### 1.9 WCPC 配置收口
|
||||
|
||||
WCPC 是 WCP1 后的配置复跑合同,只处理企业微信后台配置和只读 POC 复跑,不开发运行时功能。2026-05-18 已按用户要求自动查找并使用既有在职低风险测试号 `SMOKE002/测试员工` 复跑,报告只记录脱敏企业微信 userid 前缀 `oa_smoke***`;远端报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json` 的结论为 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。下一步允许规划 `wecom_hr_master_sync_v1`;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理,不得直接跳到考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
WCPC 是 WCP1 后的配置复跑合同,只处理企业微信后台配置和只读 POC 复跑,不开发运行时功能。2026-05-18 已按用户要求自动查找并使用既有在职低风险测试号 `SMOKE002/测试员工` 复跑,报告只记录脱敏企业微信 userid 前缀 `oa_smoke***`;远端报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json` 的结论为 `GO_TO_HR_MASTER_SYNC`,`nextStageAllowed=true`,`blockingCapabilities=[]`。WHMS 已承接该结论并完成 `wecom_hr_master_sync_v1` 本地实现;审批模板 ID、审批单号 `sp_no`、真实回调配置和 HR 写接口仍需后续独立合同处理,不得直接跳到考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
|
||||
### 1.10 WHMS 企微 HR 主数据镜像
|
||||
|
||||
WHMS 将企业微信通讯录/人事助手作为 HR 主数据源的读取能力落成 OA 镜像能力。本阶段不做考勤/审批镜像、审批回调、恢复回写、RuleOps、HR 写企微接口、采购/合同/财务迁移。
|
||||
|
||||
本地实现后的产品能力:
|
||||
|
||||
- `wecom_hr_master_sync_v1` 默认 `force_off`,配置项为 `WECOM_HR_MASTER_SYNC_ENABLED`、`WECOM_HR_ASSISTANT_SECRET`,允许已验证工作台应用 Secret 作为只读 fallback。
|
||||
- 管理员可在 People/人员管理页查看“企微 HR 主数据镜像”卡片,执行 dry-run 预检、确认 apply、查看最近批次和失败摘要;普通员工不可见。
|
||||
- dry-run 只生成差异报告和同步记录,不写 OA 员工主数据;apply 写入员工档案、部门映射、默认 `employee` 角色、企业微信绑定、主管关系和审计。
|
||||
- 新员工创建必须具备姓名、手机号、邮箱和部门;缺字段记录 `SKIP missing_required_field`,不生成占位数据。
|
||||
- 企微不在职状态记录 `needs_offboarding_confirmation`,不自动禁用 OA 账号;离职仍走 OA checklist。
|
||||
- 已有 OA 员工只更新姓名、部门、职位、直属主管、手机号、邮箱和企微绑定,不覆盖银行账户、身份证、工资、合同附件或历史审批。
|
||||
|
||||
当前状态为本地已实现、待部署和公网验证;尚未开启公网 feature flag,尚未对真实员工执行生产批量 apply。下一阶段仍为 `wecom_attendance_approval_mirror_v1`,但必须在 WHMS 部署和受控验证后另建合同。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+15
-5
@@ -1,6 +1,6 @@
|
||||
# hyb-oa 管理员手册(10 人以下团队版)
|
||||
|
||||
**当前状态(2026-05-18 / WCPC)**:R7E、PRELAUNCH、CASHIER-VOUCHER-V1、LAUNCH-DEPLOY-FINANCE、FIN-PROD-ENABLE、ACCOUNTING-HANDOFF-V1、ATTV1、HR-WECOM-DIRECTORY-V1、HRWD-CONTACT-LIVE-TEST、DBWFOC、WFDG、WDRG 和 WCPC 均已收口;SYSTEM-COMPLETE-GATE 结论为 `GO_TO_USE`。WCP1 历史结论为 `PAUSE_FOR_CONFIG`,WCPC 已使用既有在职测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)复跑通过,结论为 `GO_TO_HR_MASTER_SYNC`。后续“路线图门禁 + 单功能 Issue CSV”的开发节奏保持不变:下一步只允许规划 `wecom_hr_master_sync_v1`;HR/考勤员工日常审批优先复用企业微信,OA 做按域镜像、人工接管、恢复回写、财务闭环和审计中台;采购、费用、合同、项目、报销、付款、AP、代账交接和高敏附件继续 OA 主办,企业微信只做脱敏提醒和 OA deeplink。本轮不代表运行时代码已完成迁移。公网 `payment_batch_v1=force_on`,`erp_sync=force_off`。公司当前无 ERP/独立财务系统,且 ERP 未来不接入 OA;财务闭环以 OA 内审批、台账、付款批次、银行对公付款回单和月度代账交接为准。
|
||||
**当前状态(2026-05-18 / WHMS)**:R7E、PRELAUNCH、CASHIER-VOUCHER-V1、LAUNCH-DEPLOY-FINANCE、FIN-PROD-ENABLE、ACCOUNTING-HANDOFF-V1、ATTV1、HR-WECOM-DIRECTORY-V1、HRWD-CONTACT-LIVE-TEST、DBWFOC、WFDG、WDRG、WCPC 均已收口;SYSTEM-COMPLETE-GATE 结论为 `GO_TO_USE`。WCP1 历史结论为 `PAUSE_FOR_CONFIG`,WCPC 已使用既有在职测试号 `SMOKE002/测试员工`(报告只记录 `oa_smoke***`)复跑通过,结论为 `GO_TO_HR_MASTER_SYNC`。WHMS 已本地实现 `wecom_hr_master_sync_v1`:企微/人事助手 HR 主数据可 dry-run/apply 到 OA 员工镜像、账号、角色、绑定和审计;默认 `force_off`,尚未部署公网,未对真实员工执行生产批量 apply。后续“路线图门禁 + 单功能 Issue CSV”的开发节奏保持不变:下一阶段为 `wecom_attendance_approval_mirror_v1`,但必须在 WHMS 部署和受控验证后另建合同;HR/考勤员工日常审批优先复用企业微信,OA 做按域镜像、人工接管、恢复回写、财务闭环和审计中台;采购、费用、合同、项目、报销、付款、AP、代账交接和高敏附件继续 OA 主办,企业微信只做脱敏提醒和 OA deeplink。公网 `payment_batch_v1=force_on`,`erp_sync=force_off`。公司当前无 ERP/独立财务系统,且 ERP 未来不接入 OA;财务闭环以 OA 内审批、台账、付款批次、银行对公付款回单和月度代账交接为准。
|
||||
|
||||
## 1. 适用范围
|
||||
- 适用于 `small_team_internal` 工作区模式。
|
||||
@@ -24,7 +24,8 @@
|
||||
5. 检查功能开关:
|
||||
- 建议开启:`portal_home_v1`、企业微信入口相关开关、报销/OCR、审批待办、请假、项目基础能力和 `payment_batch_v1`。
|
||||
- 按需开启:`announcements_v1`、`presence_board_v1`、`weekly_report_v1`、`employee_lifecycle_v1`、`hr_wecom_directory_sync_v1`、`todo_board_v1`、`global_search_v1`。
|
||||
- 后续按独立合同评估:`wecom_hr_master_sync_v1`、`wecom_attendance_approval_mirror_v1`(承接旧口径 `wecom_native_approval_mirror_v1`)、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1`。
|
||||
- 本地已实现待部署:`wecom_hr_master_sync_v1`,默认 `force_off`;启用和生产 apply 需单独确认。
|
||||
- 后续按独立合同评估:`wecom_attendance_approval_mirror_v1`(承接旧口径 `wecom_native_approval_mirror_v1`)、`oa_hr_emergency_mode_v1`、`wecom_recovery_writeback_v1`、`wecom_ruleops_v1`。
|
||||
- 建议保留备用:`cutover_console_v1`、预算、AI 和完整差旅相关开关;`erp_sync` 保持关闭且不再作为后续接入目标。
|
||||
|
||||
### 3.1 ORG8 组织账号与角色边界
|
||||
@@ -190,12 +191,12 @@ RuleOps:
|
||||
- 正式写入前必须人工确认;每次写入保存前后快照、确认人、API `errcode`、失败原因、回滚建议和审计记录。
|
||||
- 不得在报告、日志或截图中暴露 Secret、access token、完整位置、人脸数据、身份证号、完整手机号或原始密钥材料。
|
||||
|
||||
WCP1/WCPC 后续推进:
|
||||
WCP1/WCPC/WHMS 后续推进:
|
||||
|
||||
1. 继续先读取 [企微优先开发路线图](./企微优先开发路线图.md) 和 WCP1 报告,再判断能否进入下一阶段。
|
||||
2. WCP1 历史结论为 `PAUSE_FOR_CONFIG`;WCPC 已用既有在职测试号完成远端只读复跑,报告 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`,结论 `GO_TO_HR_MASTER_SYNC`。
|
||||
3. WCPC 已证明 HR 主数据镜像准入项可用:通讯录成员读取、人事助手花名册读取、打卡短窗口记录/规则读取和假期余额读取均达到可复用状态;审批模板、审批实例和回调配置仍是后续考勤/审批镜像前置项。
|
||||
4. 允许下一步另建 `wecom_hr_master_sync_v1` 方案和 Issue CSV;不得直接开发考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
4. WHMS 已本地实现 `wecom_hr_master_sync_v1`,包括同步批次、逐员工结果、dry-run/apply、People 管理入口和 QA-only runner;当前尚未部署公网。
|
||||
5. 正式开发仍按 `wecom_hr_master_sync_v1` -> `wecom_attendance_approval_mirror_v1` -> `oa_hr_emergency_mode_v1` -> `wecom_recovery_writeback_v1` -> `wecom_ruleops_v1` 分合同实施;阶段未退出前禁止跳阶段。
|
||||
6. 任何生产写接口、企微规则写入、应急模式开启或恢复回写都必须有单独批准、dry-run 或可审计记录。
|
||||
|
||||
@@ -203,10 +204,19 @@ WCPC 配置复跑清单:
|
||||
|
||||
1. 已完成:自动查找并指定已有在职测试号 `SMOKE002/测试员工`;未使用已离职/禁用的 `HRWDTEST-20260517214548` 作为主测试成员。
|
||||
2. 已完成:服务器复跑采用临时环境变量和既有容器配置,不持久写入 `.env`,不重启服务;报告落到远端 `/tmp/wecom-capability-poc-wcpc-smoke002/summary.json`。
|
||||
3. 已完成:复跑结论 `GO_TO_HR_MASTER_SYNC`,允许进入 `wecom_hr_master_sync_v1` 规划。
|
||||
3. 已完成:复跑结论 `GO_TO_HR_MASTER_SYNC`,该准入已由 WHMS 承接并完成本地实现。
|
||||
4. 后续考勤/审批镜像前仍需补齐:审批模板 ID、审批单号 `sp_no`、真实回调 URL/Token/EncodingAESKey 配置和 HR 写接口单独 POC。
|
||||
5. 测试审批单不得上传采购、合同、报销、发票、银行回单或其他敏感附件;Secret、Token、EncodingAESKey 不在聊天中明文发送。
|
||||
|
||||
WHMS 管理操作口径:
|
||||
|
||||
1. 部署并开启 `wecom_hr_master_sync_v1` 前,先确认 `WECOM_HR_MASTER_SYNC_ENABLED`、`WECOM_HR_ASSISTANT_SECRET` 或已验证 fallback Secret 可用。
|
||||
2. 在 `人员管理` 的“企微 HR 主数据镜像”卡片先执行 `同步预检`;预检只生成差异和同步记录,不写员工主数据。
|
||||
3. 确认差异后再执行 `确认写入`;生产真实员工批量 apply 必须另有批准,不在日常巡检中自动触发。
|
||||
4. 缺姓名、手机号、邮箱或部门时系统跳过并记录 `missing_required_field`,管理员应先回企微/人事助手补齐,不在 OA 造占位数据。
|
||||
5. 企微不在职或禁用状态只记录 `needs_offboarding_confirmation`,管理员按 OA 离职 checklist 处理,不由同步任务静默禁用账号。
|
||||
6. 同步报告、日志和审计只看脱敏摘要;不得导出完整手机号、邮箱、userid、Secret、access token 或企微原始 payload。
|
||||
|
||||
### 3.3 正式财务闭环与代账交接
|
||||
|
||||
公司当前没有 ERP 或独立财务系统,OA 不生成 ERP 实时凭证,也不接真实支付接口。正式财务闭环如下:
|
||||
|
||||
+33
-4
@@ -33,7 +33,7 @@
|
||||
| 能力 | 企微能力 | OA 使用程度 | 当前状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 通讯录 | 成员创建、读取、更新、禁用或删除 | 近期已验证 OA 写企微;后续改为企微主源同步 OA,OA 保留修复和审计 | `CAN_REUSE_NOW`:HCLT create/update/disable、部门读取和 WCPC 既有在职测试号 `contact.member_read` 均已通过 |
|
||||
| 人事助手 | 员工字段配置、花名册读取、花名册更新 | 作为员工主数据源,驱动 OA 员工、账号、角色、绑定和状态镜像 | `CAN_REUSE_NOW_FOR_READ`:字段配置和既有在职测试号花名册读取已通过;资料更新写接口仍需单独 POC |
|
||||
| 人事助手 | 员工字段配置、花名册读取、花名册更新 | 作为员工主数据源,驱动 OA 员工、账号、角色、绑定和状态镜像 | `LOCAL_IMPLEMENTED_FOR_READ_SYNC`:字段配置和既有在职测试号花名册读取已通过;WHMS 已本地实现只读同步到 OA;资料更新写接口仍需单独 POC |
|
||||
| 打卡 | 打卡记录、员工规则、日报、月报、排班、补卡、规则管理 | 企微主办规则和采集,OA 同步按域镜像和应急对账 | `CAN_REUSE_NOW_FOR_READ`:WCPC 短窗口打卡记录和规则读取通过;日报/月报/排班等细项留到考勤镜像合同继续验证 |
|
||||
| 审批 | 模板详情、提交审批、审批状态回调、审批详情、模板创建/更新、假期余额 | HR/考勤员工日常审批企微主办,OA 同步镜像;OA 主办财务、采购、合同、项目和报销审批不迁移 | `PARTIAL_NEEDS_CONFIG`:假期余额读取通过;审批模板 ID、测试审批单号和回调配置仍需在考勤/审批镜像前补齐 |
|
||||
| 工作台和消息 | 自建应用入口、应用消息、deeplink | 继续作为 OA 移动入口和通知渠道 | `CAN_REUSE_NOW` |
|
||||
@@ -173,7 +173,7 @@ deeplink 必须校验目标地址白名单,避免跳转到非 OA 域名或未
|
||||
| --- | --- | --- |
|
||||
| OA 员工生命周期 | `DONE` | 支持入职、转正、调岗、离职 checklist、账号禁用、会话失效和审计。 |
|
||||
| OA 驱动企微通讯录创建/更新/禁用 | `DONE_LIVE_VERIFIED` | 代码、同步记录、员工管理状态和重试入口已部署;HCLT 公网真实写入闭环通过,`HRWDTEST-20260517214548` create/update/disable 均返回 `errcode=0`。其他环境缺 Secret、默认部门或通讯录 API 权限时仍按 `SKIP/BLOCKED_BY_EXTERNAL_CONFIG` 或失败可重试处理。 |
|
||||
| 企微 HR 主数据同步 OA | `PLANNED_BY_WFOC` | 后续以 `wecom_hr_master_sync_v1` 拆实现合同,员工主数据从企微/人事助手同步到 OA。 |
|
||||
| 企微 HR 主数据同步 OA | `LOCAL_IMPLEMENTED_PENDING_DEPLOY` | `wecom_hr_master_sync_v1` 已本地实现:读取企微通讯录/人事助手,支持 dry-run/apply、同步记录、审计、People 管理入口和 QA-only runner;默认 `force_off`,尚未部署公网,未执行生产真实员工批量 apply。 |
|
||||
| 企业微信原生审批 | `PARTIAL_REUSE_BY_WFOC` | HR/考勤员工日常审批优先复用企业微信,OA 同步按域镜像;报销、财务、采购、合同、项目等 OA 主办审批不迁移。 |
|
||||
|
||||
---
|
||||
@@ -276,7 +276,36 @@ WCP1 历史结论不是 `GO_TO_HR_MASTER_SYNC`,但该阻断已由 WCPC 复跑
|
||||
| `hr.staff_update_classification` | `NEEDS_API_POC`,资料更新属于写接口,需单独批准后再做低风险 POC。 |
|
||||
| `approval.oa_primary_sensitive_domains` | `KEEP_OA_PRIMARY`,采购、合同、报销、财务等继续 OA 主办。 |
|
||||
|
||||
WCPC 已达到 `GO_TO_HR_MASTER_SYNC`。下一步允许进入 `wecom_hr_master_sync_v1` 方案和 Issue CSV;不得直接开发考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
WCPC 已达到 `GO_TO_HR_MASTER_SYNC`。WHMS 已承接该结论并完成 `wecom_hr_master_sync_v1` 本地实现;不得直接开发考勤/审批镜像、OA 应急接管、恢复回写或 RuleOps。
|
||||
|
||||
## 7.11 WHMS 企微 HR 主数据同步 OA
|
||||
|
||||
`WECOM-HR-MASTER-SYNC-V1` 承接 WCPC 的 `GO_TO_HR_MASTER_SYNC` 结论,把企业微信/人事助手作为员工主数据源的读取能力落成 OA 镜像能力。本阶段仅同步 HR 主数据,不迁移考勤/审批镜像、审批回调、恢复回写、RuleOps、HR 写企微接口、采购/合同/财务流程。
|
||||
|
||||
本地实现范围:
|
||||
|
||||
- 新增 feature flag `wecom_hr_master_sync_v1`,默认 `force_off`。
|
||||
- 新增配置 `WECOM_HR_MASTER_SYNC_ENABLED`、`WECOM_HR_ASSISTANT_SECRET`;`WECOM_AGENT_SECRET` 仅作为已验证授权应用的只读 fallback,并在同步批次中记录 token source,不输出 Secret。
|
||||
- 新增后端管理 API:`POST /api/employees/wecom-hr-master-sync/runs`、`GET /api/employees/wecom-hr-master-sync/runs`、`GET /api/employees/wecom-hr-master-sync/runs/:id`。
|
||||
- 新增 `WecomHrMasterSyncRun`、`WecomHrMasterSyncItem`,记录批次、逐员工动作、脱敏 externalUserId、字段差异、失败原因、重试次数和操作人。
|
||||
- People/人员管理页新增“企微 HR 主数据镜像”卡片,管理员可执行 dry-run、确认 apply、查看最近同步和失败摘要;普通员工不可见。
|
||||
- QA-only runner `scripts/qa/check-wecom-hr-master-sync.mjs` 默认只跑列表和 dry-run;apply 必须显式传入 userids 并设置批准环境变量。
|
||||
|
||||
同步规则:
|
||||
|
||||
- `dry_run` 不创建或更新 OA 员工、账号、部门或绑定,只生成差异和同步记录。
|
||||
- `apply` 写入 OA 员工档案、部门映射、默认 `employee` 角色、企业微信绑定和可解析主管关系,并记录审计。
|
||||
- 匹配优先级:`wecom_identity_bindings.external_user_id` -> 企微 userid/工号 -> 手机号 -> 邮箱。
|
||||
- 缺姓名、手机号、邮箱或部门时记录 `SKIP missing_required_field`,不自动生成占位数据。
|
||||
- 企微不在职或禁用状态记录 `needs_offboarding_confirmation`,不自动禁用 OA 账号;离职仍必须走 OA checklist。
|
||||
- 已有 OA 员工只更新安全主数据字段:姓名、部门、职位、直属主管、手机号、邮箱和企微绑定;不覆盖银行账户、身份证、工资、合同附件或历史审批。
|
||||
- 日志、报告和审计不得输出完整手机号、邮箱、userid、Secret、access token 或企微原始 payload。
|
||||
|
||||
当前状态:
|
||||
|
||||
- WHMS 已本地实现并进入本轮门禁验证;尚未部署公网。
|
||||
- 生产批量 `apply`、公网开启 `wecom_hr_master_sync_v1`、正式真实员工同步和部署复检必须另行确认或纳入部署/公网验证合同。
|
||||
- WHMS 不解除考勤/审批镜像前置缺口;`wecom_attendance_approval_mirror_v1` 仍需另建合同补审批模板、审批单号和回调配置。
|
||||
|
||||
---
|
||||
|
||||
@@ -307,7 +336,7 @@ WCPC 已达到 `GO_TO_HR_MASTER_SYNC`。下一步允许进入 `wecom_hr_master_s
|
||||
- A4:离职停用、组织权限和绑定异常拦截。
|
||||
- A5:企业微信通知 provider 和失败重试,不阻断主业务。
|
||||
- A6:deeplink 白名单和移动目标页回跳。
|
||||
- A7:`wecom_hr_master_sync_v1`,企微/人事助手员工主数据同步 OA。
|
||||
- A7:`wecom_hr_master_sync_v1`,企微/人事助手员工主数据同步 OA;WHMS 已本地实现,待部署和受控公网验证。
|
||||
- A8:`wecom_attendance_approval_mirror_v1`,企微请假、补卡、外出、加班审批和打卡数据按域镜像;该命名承接旧口径 `wecom_native_approval_mirror_v1`。
|
||||
- A9:`oa_hr_emergency_mode_v1`,OA 人工接管 HR/考勤应急提交。
|
||||
- A10:`wecom_recovery_writeback_v1`,企微恢复后 dry-run、自动回写、失败重试和冲突清单。
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
|
||||
"WHMS-0","合同建档","建立 WECOM-HR-MASTER-SYNC-V1 执行合同,锁定本轮为企业微信/人事助手只读主数据同步到 OA;不做考勤/审批镜像、审批回调、恢复回写、RuleOps、HR 写企微接口、采购/合同/财务迁移。","1) CSV 校验通过;2) 范围限定为 HR 主数据读取、dry-run、apply、同步记录、管理入口、QA runner、文档和 skill 回写;3) 新增 feature flag 默认 force_off;4) 结论不得误写为考勤/审批镜像已完成。","受影响流程:WCPC GO_TO_HR_MASTER_SYNC -> WHMS 合同 -> 后端/前端/QA/文档实现;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-18_18-43-19-wecom-hr-master-sync-v1.csv。","python3|manual","DONE","DONE","DONE","issues/2026-05-18_18-43-19-wecom-hr-master-sync-v1.csv","[]","回滚路径:删除本 CSV 或回退 WHMS 相关提交;不涉及生产配置或部署。"
|
||||
"WHMS-1","企微主数据读取服务","新增企微 HR 主数据读取服务,使用通讯录成员列表和人事助手 hr/get_staff_info 读取花名册详情,并做字段归一化和脱敏。","1) 能按 userids 或 limit 拉取候选员工;2) 支持 WECOM_HR_ASSISTANT_SECRET,必要时记录 WECOM_AGENT_SECRET fallback token source;3) 不打印完整手机号、邮箱、userid、token、secret 或原始 payload;4) 缺配置返回 SKIP/BLOCKED_BY_EXTERNAL_CONFIG 类状态。","受影响流程:企微通讯录/人事助手 -> OA HR 主数据同步;验证步骤:cd server && node --check src/services/integrations/wecomHrMaster.js;后端 focused 测试覆盖字段归一化、脱敏和缺配置。","node|vitest","DONE","DONE","DONE","server/src/services/integrations/wecomHrMaster.js|server/src/config.js","[""WHMS-0""]","回滚路径:回退新增服务和配置读取;不删除既有企微目录同步能力。"
|
||||
"WHMS-2","同步 dry-run","新增 dry-run 批次能力,生成将创建、更新、绑定、跳过和冲突的员工/部门差异报告,不写 OA 员工主数据。","1) dry-run 不创建或更新 Employee/User/Department/Binding;2) 差异报告记录脱敏 externalUserId、动作、字段差异、跳过原因;3) 缺 OA 必填字段默认 SKIP missing_required_field;4) 企微不在职状态记录 needs_offboarding_confirmation,不自动禁用 OA 账号。","受影响流程:管理员发起 dry-run -> 系统读取企微主数据 -> 输出差异 -> 不写库;验证步骤:后端 focused 测试执行 dry-run 后比对员工、账号、部门、绑定数量不变;cd server && npm run prisma:validate。","node|vitest|prisma","DONE","DONE","DONE","server/prisma/schema.prisma|server/src/services/integrations/wecomHrMaster.js","[""WHMS-1""]","涉及 Prisma 迁移;回滚路径:回退迁移和新增模型,或在部署前不执行迁移。"
|
||||
"WHMS-3","同步 apply","新增 apply 批次能力,管理员确认后写入 OA 员工档案、部门、主管、职位、手机号、邮箱、默认角色和企业微信绑定。","1) apply 幂等;2) 匹配优先级为 WecomIdentityBinding.external_user_id -> 企微 userid -> 手机号 -> 邮箱;3) 新建账号默认 employee 角色、随机不可见密码、must_change_password=true;4) 只更新安全主数据字段,不覆盖银行账户、身份证、工资、合同附件或历史审批。","受影响流程:管理员确认 apply -> 创建/更新员工与账号 -> 绑定企微 userid -> 审计留痕;验证步骤:后端 focused 测试覆盖新建、更新、绑定、幂等、冲突检测和不在职确认项。","node|vitest|prisma","DONE","DONE","DONE","server/src/services/integrations/wecomHrMaster.js|server/src/routes/employees.js|server/prisma/schema.prisma","[""WHMS-2""]","生产真实员工批量 apply 需单独确认;本轮实现能力但不部署、不做生产批量写入。"
|
||||
"WHMS-4","同步记录与审计","新增同步批次和逐员工结果模型/API 输出,记录统计、状态、操作人、失败原因、重试次数、字段差异和审计事件。","1) WecomHrMasterSyncRun 和 WecomHrMasterSyncItem 可记录 dry-run/apply 结果;2) 管理员可查询最近批次和批次详情;3) 审计覆盖 dry_run、apply、create、update、bind、skip、conflict;4) 普通员工不可访问同步状态和触发同步。","受影响流程:同步执行 -> 批次结果查询 -> 审计追踪;验证步骤:权限负例返回 403;GET runs 和 GET runs/:id 返回脱敏数据;审计日志不含 secret/token/full userid/full phone/email。","node|vitest|prisma","DONE","DONE","DONE","server/prisma/schema.prisma|server/src/routes/employees.js|server/src/services/integrations/wecomHrMaster.js","[""WHMS-3""]","涉及权限模型和审计;回滚路径:回退新增路由、服务和迁移。"
|
||||
"WHMS-5","管理端入口","在 People/人员管理页增加企微 HR 主数据同步卡片,支持管理员/HR 执行 dry-run、apply、查看最近同步和失败项;普通员工不可见。","1) 管理员/HR 可看到同步卡片、执行 dry-run、查看最近批次与失败项;2) apply 按显式确认触发;3) 普通员工不可见入口;4) 页面明确展示企微镜像、OA 主办和待确认状态,不误导为考勤/审批镜像已完成。","受影响流程:管理员打开人员管理 -> 执行 dry-run -> 查看差异 -> 执行 apply -> 查看批次;验证步骤:cd client && npm run typecheck && npm run build;必要时 Playwright/手工说明覆盖 People 页面同步卡片。","npm|react|playwright","DONE","DONE","DONE","client/src/**","[""WHMS-4""]","前端只调用本轮新增管理 API,不新增独立页面体系。"
|
||||
"WHMS-6","QA-only runner 与测试","新增 QA-only runner,支持 live 只读、dry-run、测试 userid apply 验证、权限负例和脱敏 JSON 报告。","1) runner 默认不做公网写入;2) apply 只允许显式 userids 和显式批准参数;3) 报告只输出脱敏 userid 前缀、统计、状态和错误码;4) 不输出 secret、token、完整手机号、邮箱或原始 payload。","受影响流程:QA runner -> 企微只读读取 -> OA dry-run/apply 测试账号 -> 报告生成;验证步骤:node --check scripts/qa/check-wecom-hr-master-sync.mjs;本地无 live env 时输出可解释 SKIP,不伪造成 PASS。","node|qa-only","DONE","DONE","DONE","scripts/qa/check-wecom-hr-master-sync.mjs|server/test/**","[""WHMS-5""]","生产写入、真实员工批量 apply、部署均不在本 Issue 自动执行。"
|
||||
"WHMS-7","文档与 skill 回写","更新 PRD、企业微信规约、管理员手册、路线图、文档索引、事实源矩阵、人工试运行报告和项目 skill,记录 WHMS 能力边界、测试证据和下一阶段限制。","1) 事实源记录 wecom_hr_master_sync_v1 已实现或阶段结论;2) 明确考勤/审批镜像仍未完成,下一阶段才是 wecom_attendance_approval_mirror_v1;3) 文档索引和事实源矩阵指向本合同;4) audit-docs、quick_validate 和 git diff --check 通过。","受影响流程:后续 agent/人工读取事实源 -> 判断当前能力和下一步;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;python3 /home/shunli/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/hyb-oa-delivery;git diff --check;rg -n 'WHMS|wecom_hr_master_sync_v1|企微 HR 主数据|GO_TO_ATTENDANCE_APPROVAL_MIRROR|needs_offboarding_confirmation' docs issues .agents/skills/hyb-oa-delivery/SKILL.md。","node|python3|git|rg","DONE","DONE","DONE","docs/企微优先开发路线图.md|docs/需求-企业微信基础设施规约.md|docs/海音达OA-MVP需求规格说明书.md|docs/管理员手册.md|docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md|.agents/skills/hyb-oa-delivery/SKILL.md","[""WHMS-6""]","本轮不部署;若后续部署和公网 apply 测试,另建上线/复检合同。"
|
||||
|
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
|
||||
const DEFAULT_REPORT_ROOT = "artifacts/qa-flow";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
||||
const options = {
|
||||
baseUrl: (process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, ""),
|
||||
live: process.env.WECOM_HR_MASTER_SYNC_LIVE === "1",
|
||||
json: process.env.WECOM_HR_MASTER_SYNC_JSON === "1",
|
||||
applyApproved: process.env.WECOM_HR_MASTER_SYNC_APPLY_APPROVED === "1",
|
||||
limit: Number(process.env.WECOM_HR_MASTER_SYNC_LIMIT || 10),
|
||||
userids: csvValues(process.env.WECOM_HR_MASTER_SYNC_USERIDS || ""),
|
||||
reportDir: process.env.WECOM_HR_MASTER_SYNC_REPORT_DIR || join(DEFAULT_REPORT_ROOT, `wecom-hr-master-sync-${stamp}`),
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--live") options.live = true;
|
||||
if (arg === "--json") options.json = true;
|
||||
if (arg === "--apply-approved") options.applyApproved = true;
|
||||
if (arg === "--limit") {
|
||||
options.limit = Number(argv[index + 1] || options.limit);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--limit=")) {
|
||||
options.limit = Number(arg.slice("--limit=".length));
|
||||
} else if (arg === "--userids") {
|
||||
options.userids = csvValues(argv[index + 1] || "");
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--userids=")) {
|
||||
options.userids = csvValues(arg.slice("--userids=".length));
|
||||
} else if (arg.startsWith("--report-dir=")) {
|
||||
options.reportDir = arg.slice("--report-dir=".length);
|
||||
}
|
||||
}
|
||||
|
||||
options.limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(50, Math.floor(options.limit))) : 10;
|
||||
return options;
|
||||
}
|
||||
|
||||
function csvValues(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function maskId(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return null;
|
||||
if (raw.length <= 6) return `${raw.slice(0, 2)}***`;
|
||||
return `${raw.slice(0, Math.min(8, raw.length))}***`;
|
||||
}
|
||||
|
||||
function getCredential() {
|
||||
const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim();
|
||||
const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim();
|
||||
return identifier && password ? { identifier, password } : null;
|
||||
}
|
||||
|
||||
async function fetchJson(url, init = {}) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init.headers || {}),
|
||||
...(init.body ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, json };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
json: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeError(json) {
|
||||
return {
|
||||
code: json?.error?.code ?? null,
|
||||
message: String(json?.error?.message ?? "").replace(/[A-Za-z0-9_-]{24,}/g, "***").slice(0, 160),
|
||||
details: json?.error?.details ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function login(baseUrl, credential) {
|
||||
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
identifier: credential.identifier,
|
||||
password: credential.password,
|
||||
}),
|
||||
});
|
||||
if (!result.ok || !result.json?.data?.accessToken) {
|
||||
throw new Error(`admin login failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim());
|
||||
}
|
||||
return result.json.data.accessToken;
|
||||
}
|
||||
|
||||
async function callApi(baseUrl, token, method, path, body) {
|
||||
const result = await fetchJson(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
body: body == null ? undefined : JSON.stringify(body),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function step(status, name, details = {}) {
|
||||
return { status, name, details };
|
||||
}
|
||||
|
||||
function buildPayload(options, mode) {
|
||||
return {
|
||||
mode,
|
||||
limit: options.limit,
|
||||
...(options.userids.length ? { userids: options.userids } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeRun(run) {
|
||||
if (!run) return null;
|
||||
return {
|
||||
id: run.id,
|
||||
mode: run.mode,
|
||||
status: run.status,
|
||||
summary: run.summary ?? null,
|
||||
errorCode: run.errorCode ?? null,
|
||||
requestedUserids: Array.isArray(run.requestedUserids) ? run.requestedUserids.map(maskId) : null,
|
||||
itemCount: run.itemCount ?? run.items?.length ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function execute(options) {
|
||||
const startedAt = new Date().toISOString();
|
||||
const steps = [];
|
||||
if (!options.live) {
|
||||
return {
|
||||
status: "SKIP",
|
||||
conclusion: "LIVE_DISABLED",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [step("SKIP", "live-disabled", { runWith: "WECOM_HR_MASTER_SYNC_LIVE=1" })],
|
||||
inputs: {
|
||||
userids: options.userids.map(maskId),
|
||||
limit: options.limit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const credential = getCredential();
|
||||
if (!credential) {
|
||||
return {
|
||||
status: "SKIP",
|
||||
conclusion: "MISSING_LIVE_SMOKE_ADMIN",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [step("SKIP", "missing-admin-credential")],
|
||||
};
|
||||
}
|
||||
|
||||
const token = await login(options.baseUrl, credential);
|
||||
steps.push(step("PASS", "admin-login"));
|
||||
|
||||
const list = await callApi(options.baseUrl, token, "GET", "/api/employees/wecom-hr-master-sync/runs?pageSize=1");
|
||||
if (!list.ok) {
|
||||
const error = summarizeError(list.json);
|
||||
const featureFlag = error.details?.featureFlag;
|
||||
return {
|
||||
status: featureFlag ? "SKIP" : "FAIL",
|
||||
conclusion: featureFlag ? "FEATURE_DISABLED_OR_UNAVAILABLE" : "RUN_LIST_FAILED",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [...steps, step(featureFlag ? "SKIP" : "FAIL", "list-runs", { status: list.status, error })],
|
||||
};
|
||||
}
|
||||
steps.push(step("PASS", "list-runs", { latest: sanitizeRun(list.json?.data?.items?.[0]) }));
|
||||
|
||||
const dryRun = await callApi(
|
||||
options.baseUrl,
|
||||
token,
|
||||
"POST",
|
||||
"/api/employees/wecom-hr-master-sync/runs",
|
||||
buildPayload(options, "dry_run"),
|
||||
);
|
||||
if (!dryRun.ok) {
|
||||
return {
|
||||
status: "FAIL",
|
||||
conclusion: "DRY_RUN_FAILED",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [...steps, step("FAIL", "dry-run", { status: dryRun.status, error: summarizeError(dryRun.json) })],
|
||||
};
|
||||
}
|
||||
steps.push(step("PASS", "dry-run", sanitizeRun(dryRun.json?.data)));
|
||||
|
||||
if (!options.applyApproved) {
|
||||
return {
|
||||
status: "PASS",
|
||||
conclusion: "DRY_RUN_ONLY",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [...steps, step("SKIP", "apply", { runWith: "WECOM_HR_MASTER_SYNC_APPLY_APPROVED=1" })],
|
||||
};
|
||||
}
|
||||
|
||||
if (!options.userids.length) {
|
||||
return {
|
||||
status: "FAIL",
|
||||
conclusion: "APPLY_REQUIRES_EXPLICIT_USERIDS",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [...steps, step("FAIL", "apply-guard", { reason: "missing_explicit_userids" })],
|
||||
};
|
||||
}
|
||||
|
||||
const apply = await callApi(
|
||||
options.baseUrl,
|
||||
token,
|
||||
"POST",
|
||||
"/api/employees/wecom-hr-master-sync/runs",
|
||||
buildPayload(options, "apply"),
|
||||
);
|
||||
if (!apply.ok) {
|
||||
return {
|
||||
status: "FAIL",
|
||||
conclusion: "APPLY_FAILED",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [...steps, step("FAIL", "apply", { status: apply.status, error: summarizeError(apply.json) })],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "PASS",
|
||||
conclusion: "APPLY_DONE",
|
||||
startedAt,
|
||||
finishedAt: new Date().toISOString(),
|
||||
steps: [...steps, step("PASS", "apply", sanitizeRun(apply.json?.data))],
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
mkdirSync(options.reportDir, { recursive: true });
|
||||
const summary = await execute(options);
|
||||
const reportPath = join(options.reportDir, "summary.json");
|
||||
writeFileSync(reportPath, `${JSON.stringify(summary, null, 2)}\n`);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} else {
|
||||
console.log(`[wecom-hr-master-sync] status=${summary.status} conclusion=${summary.conclusion}`);
|
||||
console.log(`[wecom-hr-master-sync] report=${reportPath}`);
|
||||
}
|
||||
|
||||
if (summary.status === "FAIL") process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[wecom-hr-master-sync][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
CREATE TABLE "wecom_hr_master_sync_runs" (
|
||||
"id" UUID NOT NULL,
|
||||
"mode" VARCHAR(16) NOT NULL,
|
||||
"status" VARCHAR(24) NOT NULL DEFAULT 'running',
|
||||
"token_source" JSONB,
|
||||
"requested_userids" JSONB,
|
||||
"limit" INTEGER,
|
||||
"summary" JSONB,
|
||||
"error_code" VARCHAR(80),
|
||||
"error_message" TEXT,
|
||||
"triggered_by" UUID,
|
||||
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finished_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "wecom_hr_master_sync_runs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "wecom_hr_master_sync_items" (
|
||||
"id" UUID NOT NULL,
|
||||
"run_id" UUID NOT NULL,
|
||||
"employee_id" UUID,
|
||||
"external_user_id_masked" VARCHAR(80) NOT NULL,
|
||||
"action" VARCHAR(32) NOT NULL,
|
||||
"status" VARCHAR(24) NOT NULL,
|
||||
"error_code" VARCHAR(80),
|
||||
"error_message" TEXT,
|
||||
"retry_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"field_changes" JSONB,
|
||||
"source_summary" JSONB,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "wecom_hr_master_sync_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "idx_wecom_hr_master_sync_runs_status_created"
|
||||
ON "wecom_hr_master_sync_runs"("status", "created_at");
|
||||
|
||||
CREATE INDEX "idx_wecom_hr_master_sync_runs_mode_created"
|
||||
ON "wecom_hr_master_sync_runs"("mode", "created_at");
|
||||
|
||||
CREATE INDEX "idx_wecom_hr_master_sync_runs_triggered_by"
|
||||
ON "wecom_hr_master_sync_runs"("triggered_by");
|
||||
|
||||
CREATE INDEX "idx_wecom_hr_master_sync_items_run_status"
|
||||
ON "wecom_hr_master_sync_items"("run_id", "status");
|
||||
|
||||
CREATE INDEX "idx_wecom_hr_master_sync_items_employee_created"
|
||||
ON "wecom_hr_master_sync_items"("employee_id", "created_at");
|
||||
|
||||
CREATE INDEX "idx_wecom_hr_master_sync_items_action_status"
|
||||
ON "wecom_hr_master_sync_items"("action", "status");
|
||||
|
||||
ALTER TABLE "wecom_hr_master_sync_runs"
|
||||
ADD CONSTRAINT "wecom_hr_master_sync_runs_triggered_by_fkey"
|
||||
FOREIGN KEY ("triggered_by") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "wecom_hr_master_sync_items"
|
||||
ADD CONSTRAINT "wecom_hr_master_sync_items_run_id_fkey"
|
||||
FOREIGN KEY ("run_id") REFERENCES "wecom_hr_master_sync_runs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "wecom_hr_master_sync_items"
|
||||
ADD CONSTRAINT "wecom_hr_master_sync_items_employee_id_fkey"
|
||||
FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
INSERT INTO "feature_flags" (
|
||||
"id",
|
||||
"flag_key",
|
||||
"override_mode",
|
||||
"rollout_type",
|
||||
"department_ids",
|
||||
"note",
|
||||
"version",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
) VALUES (
|
||||
'0e0a66a4-9e1d-4e23-9724-99e4a322b371'::uuid,
|
||||
'wecom_hr_master_sync_v1',
|
||||
'force_off',
|
||||
'global',
|
||||
'[]'::jsonb,
|
||||
'bootstrap default: force_off',
|
||||
1,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
) ON CONFLICT ("flag_key") DO NOTHING;
|
||||
@@ -622,6 +622,8 @@ model Employee {
|
||||
wecom_checkin_sync_runs WecomCheckinSyncRun[] @relation("WecomCheckinSyncRunTrigger")
|
||||
wecom_directory_sync_runs WecomDirectorySyncRun[] @relation("WecomDirectorySyncRunEmployee")
|
||||
triggered_wecom_directory_sync_runs WecomDirectorySyncRun[] @relation("WecomDirectorySyncRunTrigger")
|
||||
wecom_hr_master_sync_items WecomHrMasterSyncItem[] @relation("WecomHrMasterSyncItemEmployee")
|
||||
triggered_wecom_hr_master_sync_runs WecomHrMasterSyncRun[] @relation("WecomHrMasterSyncRunTrigger")
|
||||
attendance_corrections AttendanceCorrection[]
|
||||
owned_cost_centers CostCenter[] @relation("CostCenterOwner")
|
||||
purchase_requests_applicant PurchaseRequest[] @relation("PurchaseRequestApplicant")
|
||||
@@ -2112,6 +2114,57 @@ model WecomIdentityBinding {
|
||||
@@map("wecom_identity_bindings")
|
||||
}
|
||||
|
||||
model WecomHrMasterSyncRun {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
mode String @db.VarChar(16)
|
||||
status String @default("running") @db.VarChar(24)
|
||||
token_source Json?
|
||||
requested_userids Json?
|
||||
limit Int?
|
||||
summary Json?
|
||||
error_code String? @db.VarChar(80)
|
||||
error_message String?
|
||||
triggered_by String? @db.Uuid
|
||||
started_at DateTime @default(now())
|
||||
finished_at DateTime?
|
||||
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
|
||||
trigger Employee? @relation("WecomHrMasterSyncRunTrigger", fields: [triggered_by], references: [id], onDelete: SetNull)
|
||||
items WecomHrMasterSyncItem[]
|
||||
|
||||
@@index([status, created_at], map: "idx_wecom_hr_master_sync_runs_status_created")
|
||||
@@index([mode, created_at], map: "idx_wecom_hr_master_sync_runs_mode_created")
|
||||
@@index([triggered_by], map: "idx_wecom_hr_master_sync_runs_triggered_by")
|
||||
@@map("wecom_hr_master_sync_runs")
|
||||
}
|
||||
|
||||
model WecomHrMasterSyncItem {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
run_id String @db.Uuid
|
||||
employee_id String? @db.Uuid
|
||||
external_user_id_masked String @db.VarChar(80)
|
||||
action String @db.VarChar(32)
|
||||
status String @db.VarChar(24)
|
||||
error_code String? @db.VarChar(80)
|
||||
error_message String?
|
||||
retry_count Int @default(0)
|
||||
field_changes Json?
|
||||
source_summary Json?
|
||||
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
|
||||
run WecomHrMasterSyncRun @relation(fields: [run_id], references: [id], onDelete: Cascade)
|
||||
employee Employee? @relation("WecomHrMasterSyncItemEmployee", fields: [employee_id], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([run_id, status], map: "idx_wecom_hr_master_sync_items_run_status")
|
||||
@@index([employee_id, created_at], map: "idx_wecom_hr_master_sync_items_employee_created")
|
||||
@@index([action, status], map: "idx_wecom_hr_master_sync_items_action_status")
|
||||
@@map("wecom_hr_master_sync_items")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
project_no String @unique
|
||||
|
||||
@@ -65,6 +65,8 @@ function getConfig() {
|
||||
wecomContactSyncEnabled: process.env.WECOM_CONTACT_SYNC_ENABLED === "true",
|
||||
wecomContactSecret: process.env.WECOM_CONTACT_SECRET ?? "",
|
||||
wecomContactDefaultDepartmentId: process.env.WECOM_CONTACT_DEFAULT_DEPARTMENT_ID ?? "",
|
||||
wecomHrMasterSyncEnabled: process.env.WECOM_HR_MASTER_SYNC_ENABLED === "true",
|
||||
wecomHrAssistantSecret: process.env.WECOM_HR_ASSISTANT_SECRET ?? "",
|
||||
wecomOauthEnabled: process.env.WECOM_OAUTH_ENABLED === "true",
|
||||
wecomRedirectUri: process.env.WECOM_REDIRECT_URI ?? "",
|
||||
wecomAllowedReturnHosts: process.env.WECOM_ALLOWED_RETURN_HOSTS ?? "",
|
||||
|
||||
@@ -19,6 +19,11 @@ const {
|
||||
normalizeOperation,
|
||||
syncWecomDirectoryForEmployee,
|
||||
} = require("../services/integrations/wecomDirectory");
|
||||
const {
|
||||
getWecomHrMasterSyncRun,
|
||||
listWecomHrMasterSyncRuns,
|
||||
runWecomHrMasterSync,
|
||||
} = require("../services/integrations/wecomHrMaster");
|
||||
const { listOrganizationDepartments } = require("../services/organization");
|
||||
const { StorageNotFoundError, openReadStream, parseStoragePath, writeBuffer } = require("../services/storage");
|
||||
|
||||
@@ -82,6 +87,16 @@ const WECOM_DIRECTORY_RETRY_SCHEMA = z.object({
|
||||
operation: z.enum(["create", "update", "disable"]).optional(),
|
||||
}).strict();
|
||||
|
||||
const WECOM_HR_MASTER_SYNC_RUN_SCHEMA = z.object({
|
||||
mode: z.enum(["dry_run", "apply"]).default("dry_run"),
|
||||
userids: z.array(z.string().trim().min(1).max(128)).max(200).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||
}).strict();
|
||||
|
||||
const WECOM_HR_MASTER_SYNC_LIST_QUERY_SCHEMA = z.object({
|
||||
pageSize: z.coerce.number().int().min(1).max(50).optional(),
|
||||
}).strict();
|
||||
|
||||
const CONTRACT_CREATE_SCHEMA = z.object({
|
||||
contractNo: z.string().trim().min(1).max(100),
|
||||
contractType: CONTRACT_TYPE_SCHEMA,
|
||||
@@ -174,6 +189,32 @@ function canUseWecomDirectorySync(fastify) {
|
||||
);
|
||||
}
|
||||
|
||||
function canUseWecomHrMasterSync(fastify) {
|
||||
return Boolean(
|
||||
fastify?.prisma?.wecomHrMasterSyncRun
|
||||
&& fastify?.prisma?.wecomHrMasterSyncItem
|
||||
&& fastify?.prisma?.wecomIdentityBinding
|
||||
&& fastify?.prisma?.integrationBinding
|
||||
&& fastify?.prisma?.featureFlag,
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureWecomHrMasterSyncEnabled(fastify, request) {
|
||||
const gate = await evaluateFeatureFlag({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
flagKey: FEATURE_FLAG_KEYS.wecomHrMasterSync,
|
||||
departmentId: request.employee.department_id ?? null,
|
||||
strictDepartment: false,
|
||||
});
|
||||
if (!gate.enabled) {
|
||||
throw buildValidationError({ featureFlag: "wecom_hr_master_sync_v1_disabled", reason: gate.reason }, 409);
|
||||
}
|
||||
if (!canUseWecomHrMasterSync(fastify)) {
|
||||
throw buildValidationError({ featureFlag: "wecom_hr_master_sync_v1_unavailable" }, 409);
|
||||
}
|
||||
}
|
||||
|
||||
async function trySyncWecomDirectory(fastify, request, options) {
|
||||
if (!canUseWecomDirectorySync(fastify)) {
|
||||
return null;
|
||||
@@ -808,6 +849,67 @@ async function employeesRoutes(fastify) {
|
||||
return sendData(reply, request, stats);
|
||||
});
|
||||
|
||||
fastify.post("/employees/wecom-hr-master-sync/runs", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
await ensureWecomHrMasterSyncEnabled(fastify, request);
|
||||
const body = parseBody(WECOM_HR_MASTER_SYNC_RUN_SCHEMA, request.body);
|
||||
|
||||
const run = await runWecomHrMasterSync({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
actorId: request.employee.id,
|
||||
mode: body.mode,
|
||||
userids: body.userids ?? [],
|
||||
limit: body.limit,
|
||||
});
|
||||
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId: request.employee.id,
|
||||
action: body.mode === "apply" ? "WECOM_HR_MASTER_SYNC_APPLY" : "WECOM_HR_MASTER_SYNC_DRY_RUN",
|
||||
targetType: "wecom_hr_master_sync_run",
|
||||
targetId: run.id,
|
||||
extra: {
|
||||
mode: run.mode,
|
||||
status: run.status,
|
||||
summary: run.summary,
|
||||
requestedUserids: run.requestedUserids,
|
||||
tokenSource: run.tokenSource,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return sendData(reply, request, run);
|
||||
});
|
||||
|
||||
fastify.get("/employees/wecom-hr-master-sync/runs", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
await ensureWecomHrMasterSyncEnabled(fastify, request);
|
||||
const query = parseBody(WECOM_HR_MASTER_SYNC_LIST_QUERY_SCHEMA, request.query);
|
||||
const items = await listWecomHrMasterSyncRuns(fastify.prisma, { pageSize: query.pageSize });
|
||||
return sendData(reply, request, { items });
|
||||
});
|
||||
|
||||
fastify.get("/employees/wecom-hr-master-sync/runs/:id", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
await ensureWecomHrMasterSyncEnabled(fastify, request);
|
||||
const runId = parseId(request.params?.id);
|
||||
const run = await getWecomHrMasterSyncRun(fastify.prisma, runId);
|
||||
if (!run) {
|
||||
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||
}
|
||||
return sendData(reply, request, run);
|
||||
});
|
||||
|
||||
fastify.get("/employees/:id/wecom-directory-sync", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
|
||||
@@ -277,6 +277,15 @@ const FEATURE_FLAG_BACKEND_IMPACT = Object.freeze({
|
||||
],
|
||||
},
|
||||
],
|
||||
wecom_hr_master_sync_v1: [
|
||||
{
|
||||
label: "企微 HR 主数据镜像",
|
||||
routes: [
|
||||
"/api/employees/wecom-hr-master-sync/runs",
|
||||
"/api/employees/wecom-hr-master-sync/runs/:id",
|
||||
],
|
||||
},
|
||||
],
|
||||
todo_board_v1: [
|
||||
{
|
||||
label: "待办工作台",
|
||||
|
||||
@@ -324,6 +324,13 @@ const DEFAULT_FEATURE_FLAGS = [
|
||||
department_ids: [],
|
||||
note: "bootstrap default: force_off",
|
||||
},
|
||||
{
|
||||
flag_key: "wecom_hr_master_sync_v1",
|
||||
override_mode: "force_off",
|
||||
rollout_type: "global",
|
||||
department_ids: [],
|
||||
note: "bootstrap default: force_off",
|
||||
},
|
||||
{
|
||||
flag_key: "todo_board_v1",
|
||||
override_mode: "force_off",
|
||||
|
||||
@@ -16,6 +16,7 @@ const FEATURE_FLAG_KEYS = {
|
||||
weeklyReport: "weekly_report_v1",
|
||||
employeeLifecycle: "employee_lifecycle_v1",
|
||||
hrWecomDirectorySync: "hr_wecom_directory_sync_v1",
|
||||
wecomHrMasterSync: "wecom_hr_master_sync_v1",
|
||||
todoBoard: "todo_board_v1",
|
||||
wecomEntry: "wecom_entry_v1",
|
||||
projectDomain: "project_domain_v1",
|
||||
@@ -145,6 +146,10 @@ function resolveEnvGate(config, flagKey) {
|
||||
return Boolean(config.wecomContactSyncEnabled);
|
||||
}
|
||||
|
||||
if (flagKey === FEATURE_FLAG_KEYS.wecomHrMasterSync) {
|
||||
return Boolean(config.wecomHrMasterSyncEnabled);
|
||||
}
|
||||
|
||||
if (flagKey === FEATURE_FLAG_KEYS.todoBoard) {
|
||||
return Boolean(config.todoBoardEnabled);
|
||||
}
|
||||
|
||||
文件差异内容过多而无法显示
加载差异
@@ -0,0 +1,104 @@
|
||||
const {
|
||||
maskEmail,
|
||||
maskExternalUserId,
|
||||
maskPhone,
|
||||
normalizeWecomHrProfile,
|
||||
__testables,
|
||||
} = require("../../src/services/integrations/wecomHrMaster");
|
||||
|
||||
describe("wecom HR master sync helpers", () => {
|
||||
it("normalizes contact and HR assistant data without leaking full sensitive values in summary", () => {
|
||||
const departmentMap = new Map([
|
||||
["1", { externalId: "1", name: "研发部" }],
|
||||
]);
|
||||
|
||||
const profile = normalizeWecomHrProfile({
|
||||
departmentMap,
|
||||
contact: {
|
||||
userid: "oa_smoke002",
|
||||
name: "测试员工",
|
||||
mobile: "19912345678",
|
||||
email: "[email protected]",
|
||||
position: "测试工程师",
|
||||
department: [1],
|
||||
direct_leader: ["manager001"],
|
||||
enable: 1,
|
||||
},
|
||||
staff: {
|
||||
staff_info: [
|
||||
{ fieldid: "入职日期", value: "2026-05-01" },
|
||||
{ fieldid: "在职状态", value: "在职" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(profile.externalUserId).toBe("oa_smoke002");
|
||||
expect(profile.departmentName).toBe("研发部");
|
||||
expect(profile.managerExternalUserId).toBe("manager001");
|
||||
expect(profile.hireDate).toBe("2026-05-01");
|
||||
expect(profile.sourceStatus).toBe("active");
|
||||
expect(profile.missingRequiredFields).toEqual([]);
|
||||
expect(JSON.stringify(profile.sourceSummary)).not.toContain("19912345678");
|
||||
expect(JSON.stringify(profile.sourceSummary)).not.toContain("[email protected]");
|
||||
expect(JSON.stringify(profile.sourceSummary)).not.toContain("oa_smoke002");
|
||||
});
|
||||
|
||||
it("skips profiles missing OA-required source fields", () => {
|
||||
const profile = normalizeWecomHrProfile({
|
||||
contact: {
|
||||
userid: "missing-email",
|
||||
name: "缺邮箱",
|
||||
mobile: "19900000001",
|
||||
department: [1],
|
||||
},
|
||||
staff: {},
|
||||
departmentMap: new Map([["1", { externalId: "1", name: "行政部" }]]),
|
||||
});
|
||||
|
||||
expect(profile.missingRequiredFields).toContain("email");
|
||||
});
|
||||
|
||||
it("marks inactive WeCom members as confirmation-required instead of active", () => {
|
||||
const profile = normalizeWecomHrProfile({
|
||||
contact: {
|
||||
userid: "inactive001",
|
||||
name: "离职测试",
|
||||
mobile: "19900000002",
|
||||
email: "[email protected]",
|
||||
department: [1],
|
||||
enable: 0,
|
||||
},
|
||||
staff: {},
|
||||
departmentMap: new Map([["1", { externalId: "1", name: "行政部" }]]),
|
||||
});
|
||||
|
||||
expect(profile.sourceStatus).toBe("inactive");
|
||||
});
|
||||
|
||||
it("builds masked field changes for contact fields", () => {
|
||||
const changes = __testables.buildEmployeeChanges(
|
||||
{
|
||||
id: "employee-1",
|
||||
name: "旧员工",
|
||||
phone: "19911112222",
|
||||
email: "[email protected]",
|
||||
department_id: "dept-old",
|
||||
title: "旧岗位",
|
||||
wecom_identity_binding: null,
|
||||
},
|
||||
{
|
||||
externalUserId: "oa_smoke002",
|
||||
name: "新员工",
|
||||
phone: "19933334444",
|
||||
email: "[email protected]",
|
||||
title: "新岗位",
|
||||
},
|
||||
{ id: "dept-new", name: "研发部" },
|
||||
);
|
||||
|
||||
expect(changes.phone.before).toBe(maskPhone("19911112222"));
|
||||
expect(changes.phone.after).toBe(maskPhone("19933334444"));
|
||||
expect(changes.email.before).toBe(maskEmail("[email protected]"));
|
||||
expect(changes.externalUserId.after).toBe(maskExternalUserId("oa_smoke002"));
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户