R7E-16E/R7E-16F 高风险操作确认统一

这个提交包含在:
2026-05-15 22:55:00 +08:00
父节点 5c38d810c1
当前提交 bf791d7a67
共修改 18 个文件,包含 979 行新增和 68 行删除
@@ -0,0 +1,52 @@
import type { ReactNode } from "react";
import { getActionConfirmCopy, type ActionConfirmKind } from "../../lib/actionConfirm";
import { ConfirmDialog } from "./ConfirmDialog";
type ActionConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
kind: ActionConfirmKind;
targetName?: string;
count?: number;
title?: string;
description?: ReactNode;
confirmLabel?: string;
variant?: "default" | "destructive";
testId?: string;
confirmTestId?: string;
cancelTestId?: string;
onConfirm: () => void | Promise<void>;
};
export function ActionConfirmDialog({
open,
onOpenChange,
kind,
targetName,
count,
title,
description,
confirmLabel,
variant,
testId,
confirmTestId,
cancelTestId,
onConfirm,
}: ActionConfirmDialogProps) {
const copy = getActionConfirmCopy(kind, { targetName, count, detail: description });
return (
<ConfirmDialog
open={open}
onOpenChange={onOpenChange}
title={title ?? copy.title}
description={description ?? copy.description}
confirmLabel={confirmLabel ?? copy.confirmLabel}
variant={variant ?? copy.variant}
testId={testId}
confirmTestId={confirmTestId}
cancelTestId={cancelTestId}
onConfirm={onConfirm}
/>
);
}
@@ -31,9 +31,9 @@ export function ConfirmDialog({
confirmLabel = "确认",
cancelLabel = "取消",
variant = "default",
testId,
confirmTestId,
cancelTestId,
testId = "action-confirm-dialog",
confirmTestId = "action-confirm-submit",
cancelTestId = "action-confirm-cancel",
onConfirm,
}: ConfirmDialogProps) {
const [pending, setPending] = useState(false);
@@ -25,8 +25,10 @@ import { EmptyStateCard } from '../common/EmptyStateCard';
import { Pagination } from '../common/Pagination';
import { StatusBadge } from '../common/StatusBadge';
import { Button } from '../ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
const PAGE_SIZE = 10;
@@ -68,6 +70,8 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
const [costCenterFilter, setCostCenterFilter] = useState('');
const [periodStartFilter, setPeriodStartFilter] = useState('');
const [periodEndFilter, setPeriodEndFilter] = useState('');
const [confirmItemTarget, setConfirmItemTarget] = useState<TravelReconciliationItem | null>(null);
const [confirmItemNote, setConfirmItemNote] = useState('');
const [page, setPage] = useState(1);
const [actionItemId, setActionItemId] = useState<string | null>(null);
@@ -248,11 +252,8 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
});
function handleConfirm(item: TravelReconciliationItem) {
const note = window.prompt('确认备注(可选)', item.confirmNote ?? '');
if (note === null) {
return;
}
confirmMutation.mutate({ item, note: note.trim() || null });
setConfirmItemTarget(item);
setConfirmItemNote(item.confirmNote ?? '');
}
const detail = detailQuery.data ?? null;
@@ -260,6 +261,7 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
return (
<>
<div className="grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
<section className="space-y-6">
{canCreateBatch ? (
@@ -520,5 +522,64 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
) : null}
</section>
</div>
<Dialog
open={Boolean(confirmItemTarget)}
onOpenChange={(open) => {
if (!open) {
setConfirmItemTarget(null);
setConfirmItemNote('');
}
}}
>
<DialogContent className="max-w-md" data-testid="action-confirm-dialog">
<DialogHeader>
<DialogTitle>确认对账异常项</DialogTitle>
<DialogDescription>
确认后该异常项将进入已确认状态,并写入差旅对账处理记录。备注可空。
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-900" htmlFor="travel-reconciliation-confirm-note">
确认备注
</label>
<Textarea
id="travel-reconciliation-confirm-note"
data-testid="travel-reconciliation-confirm-note"
value={confirmItemNote}
onChange={(event) => setConfirmItemNote(event.target.value)}
placeholder="可补充异常确认说明"
rows={3}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="secondary"
data-testid="action-confirm-cancel"
disabled={confirmMutation.isPending}
onClick={() => {
setConfirmItemTarget(null);
setConfirmItemNote('');
}}
>
取消
</Button>
<Button
type="button"
data-testid="action-confirm-submit"
disabled={!confirmItemTarget || confirmMutation.isPending}
onClick={() => {
if (!confirmItemTarget) return;
confirmMutation.mutate({ item: confirmItemTarget, note: confirmItemNote.trim() || null });
setConfirmItemTarget(null);
setConfirmItemNote('');
}}
>
{confirmMutation.isPending ? '处理中...' : '确认异常'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
+142
查看文件
@@ -0,0 +1,142 @@
import type { ReactNode } from "react";
export type ActionConfirmKind =
| "submit"
| "precheck"
| "delete"
| "archive"
| "withdraw"
| "enable"
| "disable"
| "batchApprove"
| "batchReject"
| "financeReview"
| "financePayment"
| "duplicateOverride"
| "sensitiveExport"
| "retry"
| "unbind"
| "cancel";
export type ActionConfirmCopy = {
title: string;
description: ReactNode;
confirmLabel: string;
variant: "default" | "destructive";
};
export type ActionConfirmContext = {
targetName?: string;
count?: number;
detail?: ReactNode;
};
function withTarget(base: string, targetName?: string) {
return targetName ? `${base}:${targetName}` : base;
}
export function getActionConfirmCopy(kind: ActionConfirmKind, context: ActionConfirmContext = {}): ActionConfirmCopy {
const { targetName, count, detail } = context;
const countText = typeof count === "number" && count > 0 ? `${count} 项` : "所选项目";
const copies: Record<ActionConfirmKind, ActionConfirmCopy> = {
submit: {
title: withTarget("确认提交审批", targetName),
description: detail ?? "提交后将进入审批流程,系统会生成待办、通知审批人并写入提交历史。",
confirmLabel: "确认提交",
variant: "default",
},
precheck: {
title: withTarget("确认继续提交", targetName),
description: detail ?? "当前提交存在提示项。确认后将继续提交审批,并按系统规则留痕。",
confirmLabel: "继续提交",
variant: "default",
},
delete: {
title: withTarget("确认删除", targetName),
description: detail ?? "删除后将无法在当前列表继续使用,相关业务记录可能无法恢复。",
confirmLabel: "确认删除",
variant: "destructive",
},
archive: {
title: withTarget("确认归档", targetName),
description: detail ?? "归档后将不再用于新的业务流转,历史记录仍保留。",
confirmLabel: "确认归档",
variant: "destructive",
},
withdraw: {
title: withTarget("确认撤回", targetName),
description: detail ?? "撤回后当前审批流程会终止或回到可编辑状态,并写入撤回历史。",
confirmLabel: "确认撤回",
variant: "destructive",
},
enable: {
title: withTarget("确认启用", targetName),
description: detail ?? "启用后该配置会重新参与后续业务判断,只影响新发生的业务。",
confirmLabel: "确认启用",
variant: "default",
},
disable: {
title: withTarget("确认停用", targetName),
description: detail ?? "停用后该配置不再参与后续业务判断,历史记录不回写。",
confirmLabel: "确认停用",
variant: "destructive",
},
batchApprove: {
title: `确认批量通过${count ? ` ${countText}` : ""}`,
description: detail ?? "系统会逐条处理所选待办;每条结果都会写入审批历史。",
confirmLabel: "确认批量通过",
variant: "default",
},
batchReject: {
title: `确认批量驳回${count ? ` ${countText}` : ""}`,
description: detail ?? "系统会逐条驳回所选待办;驳回意见会写入审批历史。",
confirmLabel: "确认批量驳回",
variant: "destructive",
},
financeReview: {
title: withTarget("确认财务复核", targetName),
description: detail ?? "确认后将更新财务复核结果,并写入财务处理记录。",
confirmLabel: "确认复核",
variant: "default",
},
financePayment: {
title: withTarget("确认财务付款动作", targetName),
description: detail ?? "确认后将影响付款批次或付款状态,并写入财务审计记录。",
confirmLabel: "确认执行",
variant: "destructive",
},
duplicateOverride: {
title: withTarget("确认判重例外", targetName),
description: detail ?? "确认后将写入审计记录,并影响该票据后续判重口径。",
confirmLabel: "确认例外",
variant: "destructive",
},
sensitiveExport: {
title: withTarget("确认导出敏感数据", targetName),
description: detail ?? "导出文件可能包含业务或个人信息,请确认当前账号和用途合规。",
confirmLabel: "确认导出",
variant: "default",
},
retry: {
title: withTarget("确认重试", targetName),
description: detail ?? "确认后系统会重新发起该动作,并追加新的处理记录。",
confirmLabel: "确认重试",
variant: "default",
},
unbind: {
title: withTarget("确认解绑", targetName),
description: detail ?? "解绑后当前对象关系会被移除,后续需重新绑定才能继续使用。",
confirmLabel: "确认解绑",
variant: "destructive",
},
cancel: {
title: withTarget("确认取消", targetName),
description: detail ?? "取消后该业务动作将停止,系统会保留操作历史。",
confirmLabel: "确认取消",
variant: "destructive",
},
};
return copies[kind];
}
+31 -8
查看文件
@@ -34,6 +34,7 @@ import { fetchEmployeePerformanceTimeline } from "../api/performance";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { EmployeePerformancePanel } from "../components/people/EmployeePerformancePanel";
import { EmployeeSalaryPanel } from "../components/people/EmployeeSalaryPanel";
@@ -178,6 +179,8 @@ export function EmployeeProfilePage() {
const [trainingForm, setTrainingForm] = useState<TrainingFormState>(buildEmptyTrainingForm());
const [editingCertificateId, setEditingCertificateId] = useState<string | null>(null);
const [certificateForm, setCertificateForm] = useState<CertificateFormState>(buildEmptyCertificateForm());
const [deleteAvatarConfirmOpen, setDeleteAvatarConfirmOpen] = useState(false);
const [deleteContractTarget, setDeleteContractTarget] = useState<EmployeeContract | null>(null);
const [form, setForm] = useState({
title: "",
hireDate: "",
@@ -644,10 +647,7 @@ export function EmployeeProfilePage() {
}
function handleDeleteAvatar() {
if (!window.confirm("确认删除当前头像?")) {
return;
}
deleteAvatarMutation.mutate();
setDeleteAvatarConfirmOpen(true);
}
function handleDeleteContract(contract: EmployeeContract) {
@@ -655,13 +655,11 @@ export function EmployeeProfilePage() {
toast.error("生效中的合同需先终止后再删除");
return;
}
if (!window.confirm(`确认删除合同 ${contract.contractNo}?`)) {
return;
}
deleteContractMutation.mutate(contract.id);
setDeleteContractTarget(contract);
}
return (
<>
<div className="space-y-6">
<PageHeader
title={`${profile.employee.name} 档案`}
@@ -1437,6 +1435,31 @@ export function EmployeeProfilePage() {
) : null}
</Tabs>
</div>
<ActionConfirmDialog
open={deleteAvatarConfirmOpen}
onOpenChange={setDeleteAvatarConfirmOpen}
kind="delete"
targetName="当前头像"
description="删除后个人档案将不再显示当前头像,需要重新上传才能恢复。"
onConfirm={async () => {
deleteAvatarMutation.mutate();
}}
/>
<ActionConfirmDialog
open={Boolean(deleteContractTarget)}
onOpenChange={(open) => {
if (!open) setDeleteContractTarget(null);
}}
kind="delete"
targetName={deleteContractTarget ? `合同 ${deleteContractTarget.contractNo}` : undefined}
description="删除后该合同记录和附件引用将从员工档案中移除,生效中的合同不能删除。"
onConfirm={async () => {
if (deleteContractTarget) {
deleteContractMutation.mutate(deleteContractTarget.id);
}
}}
/>
</>
);
}
+42 -3
查看文件
@@ -17,6 +17,7 @@ import { mapApiErrorMessage } from "../api/errorMessages";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -61,6 +62,11 @@ export function PaymentBatchesPage() {
const [defaultPaymentChannel, setDefaultPaymentChannel] = useState<PaymentChannel | "">("");
const [voucherPrefix, setVoucherPrefix] = useState("");
const [error, setError] = useState("");
const [confirmPaymentAction, setConfirmPaymentAction] = useState<{
kind: "financeReview" | "financePayment" | "cancel";
batchId: string;
batchNo: string;
} | null>(null);
const listQuery = useQuery({
queryKey: ["payment-batches"],
@@ -330,7 +336,11 @@ export function PaymentBatchesPage() {
type="button"
variant="secondary"
disabled={reviewMutation.isPending || !canReviewSelectedBatch}
onClick={() => reviewMutation.mutate(selectedBatch.id)}
onClick={() => setConfirmPaymentAction({
kind: "financeReview",
batchId: selectedBatch.id,
batchNo: selectedBatch.batchNo,
})}
>
{selectedBatch.reviewRequirement.nextStep === "second_review" ? "二次复核" : "复核"}
</Button>
@@ -339,7 +349,11 @@ export function PaymentBatchesPage() {
<Button
type="button"
disabled={executeMutation.isPending || !canExecuteSelectedBatch}
onClick={() => executeMutation.mutate(selectedBatch.id)}
onClick={() => setConfirmPaymentAction({
kind: "financePayment",
batchId: selectedBatch.id,
batchNo: selectedBatch.batchNo,
})}
>
执行
</Button>
@@ -349,7 +363,11 @@ export function PaymentBatchesPage() {
type="button"
variant="secondary"
disabled={cancelMutation.isPending || !canCancelSelectedBatch}
onClick={() => cancelMutation.mutate(selectedBatch.id)}
onClick={() => setConfirmPaymentAction({
kind: "cancel",
batchId: selectedBatch.id,
batchNo: selectedBatch.batchNo,
})}
>
取消
</Button>
@@ -417,6 +435,27 @@ export function PaymentBatchesPage() {
) : null}
</div>
</div>
<ActionConfirmDialog
open={Boolean(confirmPaymentAction)}
onOpenChange={(open) => {
if (!open) setConfirmPaymentAction(null);
}}
kind={confirmPaymentAction?.kind ?? "financePayment"}
targetName={confirmPaymentAction?.batchNo}
description={
confirmPaymentAction?.kind === "financeReview"
? "确认后将写入付款批次复核记录;高风险批次仍需满足双人复核要求。"
: confirmPaymentAction?.kind === "cancel"
? "取消后该付款批次不能继续执行,历史记录会保留。"
: "确认后将执行付款批次并影响付款状态,请确认批次、金额和复核要求均已核对。"
}
onConfirm={async () => {
if (!confirmPaymentAction) return;
if (confirmPaymentAction.kind === "financeReview") reviewMutation.mutate(confirmPaymentAction.batchId);
if (confirmPaymentAction.kind === "financePayment") executeMutation.mutate(confirmPaymentAction.batchId);
if (confirmPaymentAction.kind === "cancel") cancelMutation.mutate(confirmPaymentAction.batchId);
}}
/>
</ListWorkspace>
);
}
+70 -9
查看文件
@@ -13,6 +13,7 @@ import { SectionPanel } from "../components/patterns/SectionPanel";
import { isApiError } from "../api/types";
import { StatusBadge } from "../components/common/StatusBadge";
import { Button } from "../components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
@@ -173,6 +174,8 @@ export function ProjectDetailPage() {
nextFollowUpDate: "",
});
const [editingCommunicationLogId, setEditingCommunicationLogId] = useState<string | null>(null);
const [waiveChecklistTarget, setWaiveChecklistTarget] = useState<{ itemId: string; currentNote: string | null } | null>(null);
const [waiveChecklistNote, setWaiveChecklistNote] = useState("");
useEffect(() => {
if (!detail) return;
@@ -452,15 +455,8 @@ export function ProjectDetailPage() {
const handleChecklistStatusChange = (itemId: string, status: ProjectClosureChecklistStatus, currentNote: string | null) => {
if (status === "waived") {
const note = window.prompt("请填写豁免理由", currentNote ?? "");
if (note == null) {
return;
}
if (!note.trim()) {
toast.error("豁免必须填写说明");
return;
}
patchClosureChecklistMutation.mutate({ itemId, status, note: note.trim() });
setWaiveChecklistTarget({ itemId, currentNote });
setWaiveChecklistNote(currentNote ?? "");
return;
}
patchClosureChecklistMutation.mutate({ itemId, status });
@@ -491,6 +487,7 @@ export function ProjectDetailPage() {
const riskItems = risksQuery.data?.items ?? detail.risks;
return (
<>
<PageScaffold>
<EntityOverviewHero
title={`${detail.projectNo} · ${detail.name}`}
@@ -1061,6 +1058,70 @@ export function ProjectDetailPage() {
</div>
</div>
</PageScaffold>
<Dialog
open={Boolean(waiveChecklistTarget)}
onOpenChange={(open) => {
if (!open) {
setWaiveChecklistTarget(null);
setWaiveChecklistNote("");
}
}}
>
<DialogContent className="max-w-md" data-testid="action-confirm-dialog">
<DialogHeader>
<DialogTitle>确认豁免收口项</DialogTitle>
<DialogDescription>
豁免后该收口项会标记为已豁免,并写入项目收口记录。必须填写可理解说明。
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-900" htmlFor="project-closure-waive-note">
豁免理由
</label>
<Textarea
id="project-closure-waive-note"
data-testid="project-closure-waive-note"
value={waiveChecklistNote}
onChange={(event) => setWaiveChecklistNote(event.target.value)}
placeholder="说明为什么该收口项可以豁免"
rows={3}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="secondary"
data-testid="action-confirm-cancel"
disabled={patchClosureChecklistMutation.isPending}
onClick={() => {
setWaiveChecklistTarget(null);
setWaiveChecklistNote("");
}}
>
取消
</Button>
<Button
type="button"
variant="destructive"
data-testid="action-confirm-submit"
disabled={!waiveChecklistTarget || !waiveChecklistNote.trim() || patchClosureChecklistMutation.isPending}
onClick={() => {
if (!waiveChecklistTarget) return;
patchClosureChecklistMutation.mutate({
itemId: waiveChecklistTarget.itemId,
status: "waived",
note: waiveChecklistNote.trim(),
});
setWaiveChecklistTarget(null);
setWaiveChecklistNote("");
}}
>
{patchClosureChecklistMutation.isPending ? "处理中..." : "确认豁免"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
+32 -2
查看文件
@@ -38,6 +38,7 @@ import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { Pagination } from "../components/common/Pagination";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
@@ -244,6 +245,8 @@ export function PurchaseRequestsPage() {
const [archivePoNo, setArchivePoNo] = useState("");
const [archiveContractNo, setArchiveContractNo] = useState("");
const [selectedAttachmentName, setSelectedAttachmentName] = useState("");
const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState<string | null>(null);
const [erpRequeueConfirm, setErpRequeueConfirm] = useState<{ target: PurchaseRequestErpRequeueTarget; label: string } | null>(null);
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
const suppliersQuery = useQuery({
@@ -742,7 +745,8 @@ export function PurchaseRequestsPage() {
return;
}
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
if (precheck.decision === "warn" || precheck.decision === "manual_review") {
setSubmitPrecheckConfirm(message);
return;
}
@@ -1736,7 +1740,7 @@ export function PurchaseRequestsPage() {
size="sm"
variant="secondary"
disabled={requeueErpMutation.isPending}
onClick={() => requeueErpMutation.mutate({ target: action.target, label: meta.title })}
onClick={() => setErpRequeueConfirm({ target: action.target, label: meta.title })}
>
{isSubmittingAction ? "处理中..." : action.label}
</Button>
@@ -1757,6 +1761,32 @@ export function PurchaseRequestsPage() {
) : null}
</div>
</div>
<ActionConfirmDialog
open={Boolean(submitPrecheckConfirm)}
onOpenChange={(open) => {
if (!open) setSubmitPrecheckConfirm(null);
}}
kind="precheck"
targetName={detail ? resolvePurchaseDisplayCode(detail) : "采购申请"}
description={submitPrecheckConfirm}
onConfirm={async () => {
submitMutation.mutate();
}}
/>
<ActionConfirmDialog
open={Boolean(erpRequeueConfirm)}
onOpenChange={(open) => {
if (!open) setErpRequeueConfirm(null);
}}
kind="retry"
targetName={erpRequeueConfirm?.label}
description="确认后会重新补发或重试 ERP 同步消息,并追加新的同步处理记录。"
onConfirm={async () => {
if (erpRequeueConfirm) {
requeueErpMutation.mutate(erpRequeueConfirm);
}
}}
/>
</ListWorkspace>
);
}
+35 -8
查看文件
@@ -26,6 +26,7 @@ import { toast } from "sonner";
import { downloadBlob } from "../lib/download";
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
import { Pagination } from "../components/common/Pagination";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
@@ -52,6 +53,7 @@ import { Textarea } from "../components/ui/textarea";
const PAGE_SIZE = 20;
type BatchAction = "approve" | "reject";
type ExportFormat = "csv" | "xlsx";
type BatchResultState = {
action: BatchAction;
@@ -104,6 +106,8 @@ export function ReimbursementsPage() {
const [batchDialogAction, setBatchDialogAction] = useState<BatchAction | null>(null);
const [batchComment, setBatchComment] = useState("");
const [batchResult, setBatchResult] = useState<BatchResultState | null>(null);
const [deleteTarget, setDeleteTarget] = useState<ReimbursementListItem | null>(null);
const [exportConfirmFormat, setExportConfirmFormat] = useState<ExportFormat | null>(null);
const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft";
const page = Number(searchParams.get("page") ?? "1") || 1;
@@ -216,15 +220,11 @@ export function ReimbursementsPage() {
});
const handleDeleteDraft = (item: ReimbursementListItem) => {
const displayCode = resolveReimbursementDisplayCode(item);
if (!window.confirm(`确认删除 ${displayCode}?删除后无法恢复。`)) {
return;
}
deleteMutation.mutate(item.id);
setDeleteTarget(item);
};
const exportMutation = useMutation({
mutationFn: (format: "csv" | "xlsx") => exportReimbursements({ format, status, scope }),
mutationFn: (format: ExportFormat) => exportReimbursements({ format, status, scope }),
onSuccess: (response, format) => {
downloadBlob(response.blob, response.filename ?? `reimbursements.${format}`);
setExportError("");
@@ -461,13 +461,13 @@ export function ReimbursementsPage() {
<DropdownMenuContent align="end">
<DropdownMenuItem
data-testid="reimbursements-export-csv"
onSelect={() => exportMutation.mutate("csv")}
onSelect={() => setExportConfirmFormat("csv")}
>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem
data-testid="reimbursements-export-xlsx"
onSelect={() => exportMutation.mutate("xlsx")}
onSelect={() => setExportConfirmFormat("xlsx")}
>
导出 Excel
</DropdownMenuItem>
@@ -943,6 +943,33 @@ export function ReimbursementsPage() {
</DialogFooter>
</DialogContent>
</Dialog>
<ActionConfirmDialog
open={Boolean(deleteTarget)}
onOpenChange={(open) => {
if (!open) setDeleteTarget(null);
}}
kind="delete"
targetName={deleteTarget ? resolveReimbursementDisplayCode(deleteTarget) : undefined}
description="删除后无法恢复,已提交或审批中的报销单不会受到影响。"
onConfirm={async () => {
if (deleteTarget) {
deleteMutation.mutate(deleteTarget.id);
}
}}
/>
<ActionConfirmDialog
open={Boolean(exportConfirmFormat)}
onOpenChange={(open) => {
if (!open) setExportConfirmFormat(null);
}}
kind="sensitiveExport"
targetName={exportConfirmFormat === "xlsx" ? "报销 Excel" : "报销 CSV"}
onConfirm={async () => {
if (exportConfirmFormat) {
exportMutation.mutate(exportConfirmFormat);
}
}}
/>
</ListWorkspace>
);
}
+18 -1
查看文件
@@ -25,6 +25,7 @@ import { fetchCostCenters, fetchLegalEntities } from "../api/masterData";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { CardSkeleton } from "../components/common/CardSkeleton";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { EmptyStateCard } from "../components/common/EmptyStateCard";
import { PageHeader } from "../components/common/PageHeader";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
@@ -201,6 +202,7 @@ export function TravelRequestsPage() {
const [costCenterId, setCostCenterId] = useState("");
const [estimatedAmount, setEstimatedAmount] = useState("0.00");
const [segments, setSegments] = useState<DraftSegment[]>([emptySegment()]);
const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState<string | null>(null);
const selectedQueryId = searchParams.get("id") ?? "";
const selectedBatchId = searchParams.get("batchId") ?? "";
@@ -351,7 +353,8 @@ export function TravelRequestsPage() {
return;
}
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
if (precheck.decision === "warn" || precheck.decision === "manual_review") {
setSubmitPrecheckConfirm(message);
return;
}
@@ -510,6 +513,7 @@ export function TravelRequestsPage() {
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
return (
<>
<div className="space-y-6">
<PageHeader
title="差旅管理"
@@ -919,6 +923,19 @@ export function TravelRequestsPage() {
<EmptyStateCard title="暂无可访问的差旅页签" description="当前账号缺少差旅申请与对账访问权限。" />
)}
</div>
<ActionConfirmDialog
open={Boolean(submitPrecheckConfirm)}
onOpenChange={(open) => {
if (!open) setSubmitPrecheckConfirm(null);
}}
kind="precheck"
targetName={detail?.requestNo ?? "差旅申请"}
description={submitPrecheckConfirm}
onConfirm={async () => {
submitMutation.mutate();
}}
/>
</>
);
}
+40 -2
查看文件
@@ -9,6 +9,7 @@ import {
type WorkflowDefinitionEditorModel,
} from "../api/workflowDefinitions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -29,6 +30,11 @@ export function WorkflowDefinitionsPage() {
const [name, setName] = useState("");
const [notes, setNotes] = useState("");
const [steps, setSteps] = useState<DraftStep[]>([emptyStep()]);
const [confirmDefinitionAction, setConfirmDefinitionAction] = useState<{
action: "publish" | "archive";
id: string;
name: string;
} | null>(null);
const listQuery = useQuery({ queryKey: ["workflow-definitions"], queryFn: () => fetchWorkflowDefinitions() });
const items = listQuery.data?.items ?? [];
@@ -114,8 +120,21 @@ export function WorkflowDefinitionsPage() {
{selected.status !== "derived_readonly" ? (
<div className="flex gap-2">
<Button type="button" variant="secondary" onClick={() => saveMutation.mutate(selected.id)} disabled={selected.status !== "draft"}>保存</Button>
<Button type="button" onClick={() => publishMutation.mutate(selected.id)} disabled={selected.status === "archived"}>发布</Button>
<Button type="button" variant="secondary" onClick={() => archiveMutation.mutate(selected.id)} disabled={selected.status === "archived"}>归档</Button>
<Button
type="button"
onClick={() => setConfirmDefinitionAction({ action: "publish", id: selected.id, name: selected.name })}
disabled={selected.status === "archived"}
>
发布
</Button>
<Button
type="button"
variant="secondary"
onClick={() => setConfirmDefinitionAction({ action: "archive", id: selected.id, name: selected.name })}
disabled={selected.status === "archived"}
>
归档
</Button>
</div>
) : null}
</div>
@@ -163,6 +182,25 @@ export function WorkflowDefinitionsPage() {
)}
</div>
</div>
<ActionConfirmDialog
open={Boolean(confirmDefinitionAction)}
onOpenChange={(open) => {
if (!open) setConfirmDefinitionAction(null);
}}
kind={confirmDefinitionAction?.action === "archive" ? "archive" : "submit"}
targetName={confirmDefinitionAction?.name}
title={confirmDefinitionAction?.action === "archive" ? "确认归档流程定义" : "确认发布流程定义"}
description={
confirmDefinitionAction?.action === "archive"
? "归档后该流程定义不再用于新流程,历史流程不回写。"
: "发布后该流程定义会参与后续新提交业务的审批解析,历史流程不回写。"
}
onConfirm={async () => {
if (!confirmDefinitionAction) return;
if (confirmDefinitionAction.action === "publish") publishMutation.mutate(confirmDefinitionAction.id);
if (confirmDefinitionAction.action === "archive") archiveMutation.mutate(confirmDefinitionAction.id);
}}
/>
</ListWorkspace>
);
}
@@ -1,7 +1,11 @@
import { useState } from "react";
import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
import type { InvoicePoolDetail, InvoiceVerificationRecord } from "../../api/invoicePool";
import { ConfirmDialog } from "../../components/common/ConfirmDialog";
import { Button } from "../../components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Textarea } from "../../components/ui/textarea";
import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr";
export type InvoiceDetailPanelProps = {
@@ -138,6 +142,9 @@ export function InvoiceDetailPanel({
confirmAction,
setConfirmAction,
}: InvoiceDetailPanelProps) {
const [duplicateOverrideTarget, setDuplicateOverrideTarget] = useState<string | null>(null);
const [duplicateOverrideRefFormNumber, setDuplicateOverrideRefFormNumber] = useState("");
const [duplicateOverrideReason, setDuplicateOverrideReason] = useState("");
const invoiceNo = detail
? [detail.invoiceCode, detail.invoiceNumber].filter(Boolean).join("-") || detail.originalName || "待识别"
: "";
@@ -148,6 +155,17 @@ export function InvoiceDetailPanel({
: detail?.linkedItemId
? "已绑定明细"
: "未绑定";
const canSubmitDuplicateOverride = Boolean(
duplicateOverrideTarget &&
duplicateOverrideRefFormNumber.trim() &&
duplicateOverrideReason.trim() &&
!overrideDuplicateMutation.isPending,
);
const resetDuplicateOverride = () => {
setDuplicateOverrideTarget(null);
setDuplicateOverrideRefFormNumber("");
setDuplicateOverrideReason("");
};
return (
<>
@@ -291,16 +309,9 @@ export function InvoiceDetailPanel({
type="button"
disabled={overrideDuplicateMutation.isPending}
onClick={() => {
if (!window.confirm("确认执行判重例外?该操作将写入审计并影响判重口径。")) return;
const refFormNumber = window.prompt("请输入参考单号(必填)");
if (!refFormNumber?.trim()) return;
const reason = window.prompt("请输入例外原因(必填)");
if (!reason?.trim()) return;
overrideDuplicateMutation.mutate({
invoiceId: detail.id,
refFormNumber: refFormNumber.trim(),
reason: reason.trim(),
});
setDuplicateOverrideTarget(detail.id);
setDuplicateOverrideRefFormNumber("");
setDuplicateOverrideReason("");
}}
>
判重例外
@@ -515,6 +526,76 @@ export function InvoiceDetailPanel({
}}
onOpenChange={(open) => { if (!open) setConfirmAction(null); }}
/>
<Dialog
open={Boolean(duplicateOverrideTarget)}
onOpenChange={(open) => {
if (!open) resetDuplicateOverride();
}}
>
<DialogContent className="max-w-md" data-testid="action-confirm-dialog">
<DialogHeader>
<DialogTitle>确认判重例外</DialogTitle>
<DialogDescription>
确认后将写入审计记录,并影响该票据后续判重口径。请填写参考单号和例外原因。
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-900" htmlFor="invoice-duplicate-ref-form-number">
参考单号
</label>
<Input
id="invoice-duplicate-ref-form-number"
data-testid="invoice-duplicate-ref-form-number"
value={duplicateOverrideRefFormNumber}
onChange={(event) => setDuplicateOverrideRefFormNumber(event.target.value)}
placeholder="例如:BX-20260514-0016"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-900" htmlFor="invoice-duplicate-override-reason">
例外原因
</label>
<Textarea
id="invoice-duplicate-override-reason"
data-testid="invoice-duplicate-override-reason"
value={duplicateOverrideReason}
onChange={(event) => setDuplicateOverrideReason(event.target.value)}
placeholder="说明为什么该票据可以作为例外处理"
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="secondary"
data-testid="action-confirm-cancel"
disabled={overrideDuplicateMutation.isPending}
onClick={resetDuplicateOverride}
>
取消
</Button>
<Button
type="button"
variant="destructive"
data-testid="action-confirm-submit"
disabled={!canSubmitDuplicateOverride}
onClick={() => {
if (!duplicateOverrideTarget) return;
overrideDuplicateMutation.mutate({
invoiceId: duplicateOverrideTarget,
refFormNumber: duplicateOverrideRefFormNumber.trim(),
reason: duplicateOverrideReason.trim(),
});
resetDuplicateOverride();
}}
>
{overrideDuplicateMutation.isPending ? "处理中..." : "确认例外"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
@@ -0,0 +1,321 @@
import type { Page, Route } from "@playwright/test";
import { test, expect } from "./helpers/fixtures";
import { login } from "./helpers/auth";
test.use({ serviceWorkers: "block" });
function envelope<T>(data: T) {
return { requestId: "mock-system-action-confirm", data };
}
async function fulfillJson<T>(route: Route, data: T) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(envelope(data)),
});
}
function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") {
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
}
const user = {
identifier: "[email protected]",
password: "ActionConfirm1234",
name: "Action Confirm",
};
const me = {
id: "employee-action-confirm",
employeeNo: "E-action-confirm",
name: user.name,
email: user.identifier,
phone: null,
roleCodes: ["manager", "finance"],
permissions: [
"expense.create",
"expense.view_own",
"expense.delete_own",
"expense.approve",
"payment.batch.manage",
"risk.review.manage",
],
profileCompleteness: {
hasEmergencyContact: true,
hasBankAccount: true,
hasAvatar: false,
hasIdNumber: false,
score: 80,
},
};
const reimbursementId = "reimbursement-action-confirm";
const paymentBatchId = "payment-batch-action-confirm";
function reimbursementListItem() {
return {
id: reimbursementId,
formNumber: "BX-ACTION-001",
status: "draft",
reimbursementDate: "2026-05-15",
totalAmount: "88.00",
description: "R7E 高风险确认测试",
payment: {
paymentVoucherNo: null,
paymentDate: null,
paymentChannel: null,
paidAt: null,
paidBy: null,
},
owner: { id: me.id, employeeNo: me.employeeNo, name: me.name },
submitter: { id: me.id, employeeNo: me.employeeNo, name: me.name },
project: null,
pendingWorkflow: null,
createdAt: "2026-05-15T08:00:00.000Z",
updatedAt: "2026-05-15T08:10:00.000Z",
};
}
function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
return {
id: paymentBatchId,
batchNo: "PAY-ACTION-001",
entityType: "reimbursement",
name: "R7E 高风险付款确认",
status,
defaultPaymentDate: "2026-05-15",
defaultPaymentChannel: "bank",
voucherPrefix: "R7E",
totalAmount: "88.00",
highRisk: false,
reviewRequirement: {
thresholdAmount: "1000.00",
requiredReviewCount: 1,
completedReviewCount: status === "draft" ? 0 : 1,
nextStep: status === "draft" ? "first_review" : "ready_for_execute",
makerCheckerRequired: false,
reviewerPermission: "risk.review.manage",
},
reviewComment: null,
secondaryReviewComment: null,
executionSummaryJson: null,
createdAt: "2026-05-15T08:00:00.000Z",
createdBy: me.id,
reviewedAt: status === "draft" ? null : "2026-05-15T08:10:00.000Z",
reviewedBy: status === "draft" ? null : me.id,
secondaryReviewedAt: null,
secondaryReviewedBy: null,
executedAt: null,
executedBy: null,
cancelledAt: null,
cancelledBy: null,
items: [
{
id: "payment-batch-item-action-confirm",
entityId: reimbursementId,
entityCode: "BX-ACTION-001",
entityTitle: "R7E 高风险确认测试",
amount: "88.00",
currencyCode: "CNY",
paymentVoucherNoOverride: null,
paymentDateOverride: null,
paymentChannelOverride: null,
status: "pending",
failureCode: null,
failureMessage: null,
snapshotJson: {},
createdAt: "2026-05-15T08:00:00.000Z",
openRiskFlags: [],
},
],
};
}
async function mockSystemActionApis(page: Page) {
const calls = {
deleteReimbursement: 0,
exportReimbursements: 0,
executePaymentBatch: 0,
};
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
const request = route.request();
const url = new URL(request.url());
const path = url.pathname;
if (path === "/api/auth/sso/providers") {
await fulfillJson(route, []);
return;
}
if (path === "/api/auth/wecom/config") {
await fulfillJson(route, {
enabled: true,
entryEnabled: true,
oauthEnabled: true,
corpId: "wx-test-corp",
agentId: "1000002",
loginUrl: "/api/auth/wecom/login?returnTo=%2Fportal",
bindEnabled: true,
defaultReturnTo: "/portal",
status: "ready",
statusReasons: [],
});
return;
}
if (path === "/api/auth/login" || path === "/api/auth/refresh") {
await route.fulfill({
status: 200,
contentType: "application/json",
headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" },
body: JSON.stringify(envelope({
accessToken: "mock-system-action-confirm-token",
expiresIn: 900,
mustChangePassword: false,
})),
});
return;
}
if (path === "/api/auth/logout") {
await route.fulfill({
status: 200,
contentType: "application/json",
headers: { "set-cookie": "has_session=; Path=/; Max-Age=0" },
body: JSON.stringify(envelope({})),
});
return;
}
if (path === "/api/me") {
await fulfillJson(route, me);
return;
}
if (path === "/api/system/navigation") {
await fulfillJson(route, {
workspace: {
id: "workspace-action-confirm",
persisted: true,
profile: "small_team_internal",
orgMode: "single_company",
primaryEntryMode: "password",
enabledGroups: ["main"],
quickActions: [],
},
items: [
navigationItem("portal", "/portal", "首页", "home"),
navigationItem("reimbursements", "/reimbursements", "报销", "receipt"),
navigationItem("payment-batches", "/payment-batches", "付款批次", "credit_card"),
],
});
return;
}
if (path === "/api/reimbursements" && request.method() === "GET") {
await fulfillJson(route, { page: 1, pageSize: 20, total: 1, scope: "own", items: [reimbursementListItem()] });
return;
}
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "DELETE") {
calls.deleteReimbursement += 1;
await fulfillJson(route, { id: reimbursementId, deleted: true });
return;
}
if (path === "/api/exports/reimbursements") {
calls.exportReimbursements += 1;
await route.fulfill({
status: 200,
contentType: url.searchParams.get("format") === "xlsx"
? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
: "text/csv",
headers: { "content-disposition": "attachment; filename=\"reimbursements.csv\"" },
body: "formNumber,totalAmount\nBX-ACTION-001,88.00\n",
});
return;
}
if (path === "/api/reimbursement-templates") {
await fulfillJson(route, { page: 1, pageSize: 20, total: 0, items: [] });
return;
}
if (path === "/api/reimbursements/linkable-travel-requests") {
await fulfillJson(route, { ownerEmployeeId: me.id, items: [] });
return;
}
if (path === "/api/payment-batches" && request.method() === "GET") {
await fulfillJson(route, { items: [paymentBatch("reviewed")] });
return;
}
if (path === `/api/payment-batches/${paymentBatchId}/execute` && request.method() === "POST") {
calls.executePaymentBatch += 1;
await fulfillJson(route, { item: paymentBatch("reviewed") });
return;
}
await fulfillJson(route, {});
});
return calls;
}
test.describe("R7E 全站高风险操作二次确认", () => {
test("报销删除和敏感导出必须确认后才调用接口", async ({ page }) => {
const calls = await mockSystemActionApis(page);
page.on("dialog", (dialog) => {
throw new Error(`不应出现浏览器原生弹窗:${dialog.message()}`);
});
await login(page, user);
await page.goto("/reimbursements");
await expect(page.getByTestId("reimbursements-table")).toBeVisible();
await page.getByTestId("reimbursement-row").getByRole("button", { name: "删除" }).click();
await expect(page.getByTestId("action-confirm-dialog")).toContainText("删除后无法恢复");
expect(calls.deleteReimbursement).toBe(0);
await page.getByTestId("action-confirm-cancel").click();
expect(calls.deleteReimbursement).toBe(0);
await page.getByTestId("reimbursement-row").getByRole("button", { name: "删除" }).click();
await page.getByTestId("action-confirm-submit").click();
await expect.poll(() => calls.deleteReimbursement).toBe(1);
await page.getByTestId("reimbursements-export").click();
await page.getByTestId("reimbursements-export-csv").click();
await expect(page.getByTestId("action-confirm-dialog")).toContainText("导出文件可能包含业务或个人信息");
expect(calls.exportReimbursements).toBe(0);
await page.getByTestId("action-confirm-submit").click();
await expect.poll(() => calls.exportReimbursements).toBe(1);
});
test("付款批次执行必须确认后才调用接口", async ({ page }) => {
const calls = await mockSystemActionApis(page);
page.on("dialog", (dialog) => {
throw new Error(`不应出现浏览器原生弹窗:${dialog.message()}`);
});
await login(page, user);
await page.goto("/payment-batches");
await expect(page.getByText("PAY-ACTION-001").first()).toBeVisible();
await page.getByRole("button", { name: "执行" }).click();
await expect(page.getByTestId("action-confirm-dialog")).toContainText("确认后将执行付款批次");
expect(calls.executePaymentBatch).toBe(0);
await page.getByTestId("action-confirm-cancel").click();
expect(calls.executePaymentBatch).toBe(0);
await page.getByRole("button", { name: "执行" }).click();
await page.getByTestId("action-confirm-submit").click();
await expect.poll(() => calls.executePaymentBatch).toBe(1);
});
});
+21 -7
查看文件
@@ -282,32 +282,46 @@ R7EP 历史结论:公网、live smoke/OCR、移动矩阵、核心业务流、
`R7E-16D` 已于 2026-05-15 21:46 CST 部署到 `https://oa.hyinda.com`,部署基线为 `96488d8 R7E-16D 审批动作二次确认`,`SYNC_ENV=0`,未覆盖远端 `.env`。部署脚本内置远端构建、init、Prisma migration status、`/ready`、external deps smoke、runtime smoke 和公网 bundle 扫描均通过。部署后补充验证:`curl -fsS https://oa.hyinda.com/ready` 通过,`PUBLIC_BASE_URL=https://oa.hyinda.com node scripts/qa/check-public-bundle-copy.mjs` findings=0,live smoke env readiness 显示公网、企业微信和真实 OCR 条件 ready,`cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list` 复跑 6 passed,`cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/workflow-action-confirm-ui.spec.ts tests/e2e/r89-mobile-login-navigation.spec.ts --workers=1 --reporter=list` 5 passed。审批动作二次确认仍需代表真机复测后才能关闭 `R7E-15/R7E-17/R7E-18`。
## R7E 全站高风险操作确认统一
执行时间:2026-05-15。根据用户确认,将 `R7E-16D` 的审批二次确认模式推广到全站高风险动作,但不覆盖普通保存、筛选、分页、查看、预览或非敏感导航,避免确认疲劳。
| 检查项 | 结果 | 证据和结论 |
| --- | --- | --- |
| 高风险确认统一 | 通过 | 新增 `ActionConfirmDialog` 和确认文案 helper;报销删除、敏感导出、付款批次执行、采购/差旅预检、头像/合同删除、票据判重例外、流程定义发布/归档等入口改为 OA 内确认弹窗 |
| 原生确认框清理 | 通过 | `rg -n 'window\.(confirm\|prompt)' client/src` 无结果;不再使用浏览器原生阻塞弹窗作为业务确认 |
| PC 自动化替代人工 | 通过 | `system-action-confirmation.spec.ts` 覆盖取消不调用接口、确认后才调用接口;`workflow-action-confirm-ui.spec.ts` 覆盖审批确认;PC 端不再安排人工点击验收 |
| 移动和视觉矩阵 | 通过 | `DG7+R124` 本地移动矩阵 6 passed;`project-wide-visual-local` 通过 1 passed,并由脚本自动准备临时数据库和种子数据 |
| 公网状态 | 未部署 | 本节改动目前只完成本地验证和回填,尚未部署到 `https://oa.hyinda.com`;部署后需复跑 `/ready`、public bundle、PC mock 自动化和移动矩阵 |
补充说明:曾直接把 `project-wide-visual.spec.ts` 挂到裸 Vite 服务运行,因默认 E2E 用户和后端种子前置不足触发登录 API `HTTP_500`。已改用项目标准门禁 `QA_SKIP_CLIENT_BUILD=1 bash scripts/qa/run-web-gates.sh project-wide-visual-local`,该门禁会准备临时库、写入种子并启动本地服务,结果通过;前述失败归类为测试前置问题,不作为业务回归。
`BIZ8-13/BIZ8-CLOSE` 已完成权限回归和阶段收口:`cd server && npm run prisma:validate` 通过;`cd server && npm test -- tests/unit/routes/exports.apOperations.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/purchase.project-link.test.js tests/unit/routes/projects.test.js tests/unit/routes/workflow.permissions.test.js tests/unit/services/financeLedger.test.js` 6 files / 27 tests passed;`cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/biz8-ap-ledger-project.spec.ts --workers=1 --reporter=list` 1 passed。付款批次仍按 P2 延期,不作为当前 R7E 阻断。
## QAOPS8 Go/No-Go 当前结论
执行时间:2026-05-15。
`QAOPS8-17` 当前结论为 `Pause`,原因不是自动化基础门禁失败,也不是 WF8 deeplink 阻断;R7E-16D 已部署并通过公网自动化,剩余门禁是 R7E-16D 代表真机复测和最终人工验收结论。该结论用于防止把自动化或单台代表设备证据误解释为正式上线 `Go`。
`QAOPS8-17` 当前结论为 `Pause`,原因不是自动化基础门禁失败,也不是 WF8 deeplink 阻断;R7E-16D 已部署、自动化通过且代表真机确认。剩余门禁是 R7E-16E/R7E-16F 尚未部署到公网复核,以及最终人工验收结论。该结论用于防止把本地自动化证据误解释为正式上线 `Go`。
| 检查项 | 当前状态 | 处理 |
| --- | --- | --- |
| QAOPS8 发布、备份、回滚和 SLO 基线 | 已形成 | 可作为 R7E 启动前准备证据 |
| R7E-1..7 人工启动确认 | 已确认 | 可以进入启动当天公网健康复核 |
| R7E-8 公网健康复核 | 已完成 | 最新代码 `96488d8` 已部署;`/ready`、public bundle、远端 migration 状态、live readiness、公网移动矩阵和审批确认专项通过 |
| R7E-9..15 真实业务和真机流程 | 部分完成 | 浏览器业务线已关闭报销、审批边界、请假、采购;`R7E-9` 入口、免登录和消息 deeplink 已按代表设备关闭;`R7E-14` 按付款批次延期到 P2 口径收口;`R7E-15` 等待 R7E-16D 审批二次确认代表设备真机反馈 |
| R7E-16..18 每日问题和验收结论 | 未完成 | 需试运行窗口内记录 |
| R7E-9..15 真实业务和真机流程 | 已完成 | 浏览器业务线已关闭报销、审批边界、请假、采购;`R7E-9` 入口、免登录和消息 deeplink 已按代表设备关闭;`R7E-14` 按付款批次延期到 P2 口径收口;`R7E-15` 审批二次确认代表真机反馈已确认 |
| R7E-16..18 每日问题和验收结论 | 部分完成 | `R7E-16E/R7E-16F` 本地完成但尚未部署公网;`R7E-18` 仍需最终人工验收结论 |
| WF8-11 通知 deeplink 回 OA | 已完成 | 2026-05-15 代表设备企业微信消息点击可免登录进入目标审批详情;WF8 可阶段关闭 |
| BIZ8-12/13 移动业务入口和权限回归 | 已完成 | `wangzhiling` 已确认财务入口定位和付款未启用空态;财务/项目权限回归通过,BIZ8 可阶段关闭 |
保持 `Pause` 的剩余门禁:
- `SMOKE101` 或 `wangshunli` 复测企业微信审批消息或 `/mobile/approvals` 详情:确认 `通过 / 驳回 / 退回` 位于底部操作区,点击后先出现二次确认且文案可理解。
- `SMOKE101` 在 PC 审批详情和待办列表快捷通过抽查:未确认前不执行审批,确认后才执行。
- `SMOKE002` 可抽查移动报销/通知/详情返回,确认部署后仍无 PC 外壳、无横向滚动。
- 部署 R7E-16E/R7E-16F 后复跑 `/ready`、public bundle、PC mock 自动化和移动矩阵。
- PC 高风险动作确认由 Playwright 和视觉门禁验收,不再安排人工 PC 点击。
- 企业微信真机后续只保留消息、免登录、WebView 文件选择器和系统选择器这类真实环境差异项。
- 付款批次闭环已延期到 P2,不作为当前 R7E 阻断。
结论:当前可以关闭 `WF8-11`、`R7E-9` 和 BIZ8 阶段流,但不能给正式上线 `Go`,也不能关闭 `R7E-15`、`R7E-17`、`R7E-18` 或 QAOPS8 后续收口项。`R7E-14` 已按付款批次延期到 P2 的变更口径收口。
结论:当前可以关闭 `WF8-11`、`R7E-9`、`R7E-15` 和 BIZ8 阶段流,但不能给正式上线 `Go`,也不能关闭 `R7E-17`、`R7E-18` 或 QAOPS8 后续收口项。`R7E-14` 已按付款批次延期到 P2 的变更口径收口。
## 历史恢复
+4 -3
查看文件
@@ -137,7 +137,8 @@ MIA7 之后,移动 OA 的信息架构以“一级入口少、任务摘要轻
- 标题显示业务编号或摘要,例如“报销单 BX20260506001”。
- 状态、金额、申请人、更新时间作为摘要条。
- 主流程操作固定在页头或底部操作区,危险或终局动作必须二次确认;审批动作按钮保持“通过 / 驳回 / 退回”短文案,具体后果放在确认弹窗中说明。
- 主流程操作固定在页头或底部操作区,高风险动作必须二次确认;审批动作按钮保持“通过 / 驳回 / 退回”短文案,具体后果放在确认弹窗中说明。
- 高风险动作包括提交审批、通过、驳回、退回、转审、撤回、删除、归档、启用/停用、批量处理、财务复核/付款、重复票据例外、功能开关批量执行、敏感数据导出、通知测试或重发;普通保存草稿、筛选、分页、查看、预览、取消编辑和非敏感导航不强制确认。
- 审批流、附件、发票和日志分区展示,避免混在一个长表单里。
- 审批、财务和管理员视角可以展示更多审计信息,但普通员工主视角只展示与下一步动作有关的信息。
@@ -145,7 +146,7 @@ MIA7 之后,移动 OA 的信息架构以“一级入口少、任务摘要轻
- 表单先收集必要字段,非必要字段折叠或后置。
- 发票、附件上传使用自定义中文控件,不暴露浏览器原生 `Choose File` 文案。
- 弹窗只承担单一任务,不在弹窗里解释整个业务流程。
- 弹窗只承担单一任务,不在弹窗里解释整个业务流程;确认弹窗统一使用 OA 内组件,不使用浏览器原生 `window.confirm` 或类似原生阻塞弹窗。
## 移动统一页面范式
@@ -159,7 +160,7 @@ MIA7 之后,移动 OA 的信息架构以“一级入口少、任务摘要轻
- 移动首页一级快捷入口使用图标宫格:图标在上、短标题在下,4 项等宽,辅助说明不在小屏常驻展示,避免图标和文字挤压;标题单行省略,不允许逐字换行。
- 移动端页面边距、卡片间距、底部导航高度和吸底操作高度使用统一 token;不通过 viewport 缩放字体解决拥挤问题。
- 次级入口和信息字段可按最小宽度自动从两列变一列;核心操作按钮必须保持明确可点,不得被内容、安全区或底部栏遮挡。
- 报销详情、审批详情等深层 Push 页隐藏底部导航,使用顶部返回和上下文吸底操作,避免底部 Tab 在深层页面假选中;审批详情的通过、驳回、退回等主操作放在底部操作区并先确认。
- 报销详情、审批详情等深层 Push 页隐藏底部导航,使用顶部返回和上下文吸底操作,避免底部 Tab 在深层页面假选中;审批详情的通过、驳回、退回等主操作放在底部操作区并先确认。PC 端保持工作台和详情页高密度布局,不强制把所有操作移到底部,但高风险动作同样必须先确认。
- 企业微信个人应用消息 deeplink 应直达移动待办或业务详情;首次进入如需点击进入工作台,应保持后续可直接回到目标页。
- 首页最近通知只展示事件消息,不能重复完整待办卡片;如果某条待办已在首页待处理摘要出现,通知区域不再重复显示同一待办内容。
- 移动列表使用卡片和状态标签,首屏优先展示“谁提交、什么事项、金额/天数、当前状态和下一步”。
+2
查看文件
@@ -228,6 +228,8 @@
审批动作按钮使用短文案 `通过 / 驳回 / 退回`,不得在按钮内堆叠长说明。PC 和移动端执行这些动作前必须二次确认,在确认弹窗中解释后果:通过会进入下一审批节点或完成审批并写入历史;驳回会终止当前流程或进入驳回态,申请人通常需要复制或重新发起;退回会回到申请人修改态,保留原单号和审批历史,申请人修改后可重新提交。驳回和退回都必须填写可理解意见。
R7E 后,全站高风险操作沿用同一确认模式:提交审批、撤回、删除、归档、启用/停用、批量处理、财务复核/付款、重复票据例外、敏感数据导出和通知测试/重发都必须使用 OA 内二次确认弹窗;普通保存草稿、筛选、分页、查看、预览和取消编辑不强制确认,避免确认疲劳。PC 端确认由 Playwright 自动化覆盖,企业微信真机只保留消息、免登录、WebView 文件选择器等真实环境差异项。
### 6.1 自审与转审约束
- 自审默认禁止,不因申请人同时拥有 `admin`、`manager`、`ceo`、`finance` 或其他角色而放开。
@@ -14,13 +14,15 @@ R7E-11,审批流程试运行,"审批人按真实路径处理待办,员工查
R7E-12,请假流程试运行,"员工提交请假,审批人审批,员工和团队视角查看状态。","1) 请假提交成功;2) 审批处理成功;3) 移动/PC 状态一致;4) 团队状态或日历口径可解释。","流程级验证:员工提交请假 -> 审批通过/驳回 -> 查看进度和团队状态。",manual|playwright,DONE,DONE,DONE,docs/trialrun/人工试运行报告.md|client/tests/e2e/manual/r7e-browser-business-trialrun.spec.ts,"[""R7E-8""]",2026-05-14 无人工自动化补测:员工创建未来工作日请假并提交;审批人处理通过;申请人 PC 回看已通过且 /mobile/leave 记录页可见无横向滚动;测试单据 a22e45f8-b3d6-4862-97f2-30cbe4815e09;证据 artifacts/r7e/browser-business-trialrun/report-leave.md。
R7E-13,采购流程试运行,"员工提交或查看采购申请,审批人处理,状态和附件可追溯。","1) 采购申请路径可用;2) 审批处理可用;3) 附件和状态可追溯;4) 移动采购创建如缺失则记录为后续 UX/P2。","流程级验证:员工提交/查看采购 -> 审批处理 -> 查看状态和附件。",manual|playwright,DONE,DONE,DONE,docs/trialrun/人工试运行报告.md|client/tests/e2e/manual/r7e-browser-business-trialrun.spec.ts,"[""R7E-8""]",2026-05-14 无人工自动化补测:采购 CG-20260514-0005 完成 PC 创建 明细 无敏感测试附件上传 提交 审批 状态和审批历史回看;移动采购列表 详情和附件可见且无横向滚动;证据 artifacts/r7e/browser-business-trialrun/report-purchase-attachment.md。
R7E-14,财务审批和付款延期试运行,"财务按真实路径复核到财务审批的报销单;本轮不启用 payment_batch_v1,不追付款批次闭环,付款批次作为 P2 单独窗口处理。","1) 财务角色入口正确;2) 到财务审批的报销单可查看;3) payment_batch_v1 关闭时有明确空态或错误契约;4) 管理员不自动获得财务能力;5) 不执行真实付款批次。","流程级验证:财务进入移动入口或台账 -> 查看到财务审批的报销单或付款批次未启用提示 -> 检查管理员无财务执行能力 -> 记录付款批次延期到 P2。",manual|playwright,DONE,DONE,DONE,docs/trialrun/人工试运行报告.md|client/tests/e2e/manual/r7e-browser-business-trialrun.spec.ts,"[""R7E-8""]",2026-05-14 finance-closeout 诊断:报销 40876ef3-0652-4972-aa14-1aad1a855ae0 / BX-20260514-0016 状态 approved;/api/ap-operations/reimbursement-ledger 和 /api/payment-batches 均返回 payment_batch_v1_disabled / flag_force_off;未创建付款批次 未复核 未执行付款 未验证导出。2026-05-14 用户确认 R7E 财务付款策略为先不启用,付款批次延期到 P2;22:08 最新部署后 R7E_BROWSER_FLOW=finance-closeout 通过但 AP API 按 flag_force_off SKIP,R7E_BROWSER_FLOW=mobile 通过并确认员工 审批人 财务移动入口可见且无横向滚动;本行按延期口径收口,不代表付款批次已启用。
R7E-15,移动端和真机反馈,"收集 R7E 参与者在移动浏览器和企业微信中的真实体验反馈。","1) 至少覆盖代表性 iPhone/Android 或企业微信设备;2) 记录页面、视口、角色和问题级别;3) 阻断问题进入修复分流;4) 体验优化进入 UX8。","流程级验证:按真机公网测试清单人工复核;必要时补 Playwright 截图。",manual|playwright,DOING,TODO,TODO,docs/trialrun/人工试运行报告.md|output/playwright,"[""R7E-9""]",2026-05-14 自动化移动浏览器员工 审批人 财务核心入口通过且无横向滚动;用户真机补充:所有设备没有 PC 壳和横向滚动 但通知卡片和待审批卡片右侧状态只露第一个字 归属 R7E-16A;iOS 只能上传 PDF Android 企业微信选择器仍显示拍照和选择文件 归属 R7E-16B;2026-05-14 22:09 修复 5491e92 已部署 DG7+R124 公网移动矩阵 6 passed 且 R7E_BROWSER_FLOW=mobile 通过;2026-05-15 已补代表设备企业微信消息 deeplink 真机通过;2026-05-15 19:31 二次修复 f945c0b 已部署且 /ready public bundle live smoke env 和 DG7+R124 公网移动矩阵 6 passed;用户确认 SMOKE101 审批状态与三按钮布局、SMOKE002 详情头部间距、wangzhiling 财务入口定位均通过;Android 选择器显示拍照按平台限制处理 OA 端 PDF-only 校验已由 R7E-16B 覆盖;2026-05-15 21:46 CST 96488d8 已部署且 /ready public bundle live smoke env DG7+R124 公网移动矩阵和审批确认专项均通过;本行仍等待 R7E-16D 审批二次确认代表真机反馈和 R7E-18 阶段验收结论。
R7E-16,每日问题清单,"R7E 期间每天登记问题、级别、归属治理流、处理状态和证据路径。","1) 问题均有级别;2) 阻断问题当天决策;3) 新需求不混入即时修复;4) 每日 closeout 可追溯。","流程级验证:每日人工 closeout;问题分流到 R7E/UX8/ORG8/WF8/BIZ8/QAOPS8。",manual,DOING,TODO,TODO,docs/trialrun/人工试运行报告.md,"[""R7E-10"",""R7E-11"",""R7E-12"",""R7E-13"",""R7E-14"",""R7E-15""]",2026-05-14 用户真机反馈分流:免登录通过;企业微信消息 deeplink 全设备提示 requestid tokenexpired/深链已过期 为 HIGH 归属 WF8-11A;通知和待审批卡片右侧状态只露首字 为 MEDIUM 归属 R7E-16A;Android 企业微信 PDF 上传系统选择器仍显示拍照和选择文件 为 MEDIUM 归属 R7E-16B;财务移动入口和付款未启用空态不清楚 为 MEDIUM 归属 BIZ8-12A;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进对应详情 WF8-11 已关闭;二次反馈中通知新标记按未读行为接受,审批状态槽/详情头部/按钮布局归 R7E-16C 且 f945c0b 已部署公网验证通过并由用户确认通过;财务入口定位归 BIZ8-12B 已由用户确认通过;退回和驳回语义说明与二次确认归 R7E-16D 且 96488d8 已部署公网自动化验证通过;财务付款闭环延期到 P2;当前剩余为 R7E-16D 部署后代表真机确认和 R7E-18 人工验收结论。
R7E-15,移动端和真机反馈,"收集 R7E 参与者在移动浏览器和企业微信中的真实体验反馈。","1) 至少覆盖代表性 iPhone/Android 或企业微信设备;2) 记录页面、视口、角色和问题级别;3) 阻断问题进入修复分流;4) 体验优化进入 UX8。","流程级验证:按真机公网测试清单人工复核;必要时补 Playwright 截图。",manual|playwright,DONE,DONE,DONE,docs/trialrun/人工试运行报告.md|output/playwright,"[""R7E-9""]",2026-05-14 自动化移动浏览器员工 审批人 财务核心入口通过且无横向滚动;用户真机补充:所有设备没有 PC 壳和横向滚动 但通知卡片和待审批卡片右侧状态只露第一个字 归属 R7E-16A;iOS 只能上传 PDF Android 企业微信选择器仍显示拍照和选择文件 归属 R7E-16B;2026-05-14 22:09 修复 5491e92 已部署 DG7+R124 公网移动矩阵 6 passed 且 R7E_BROWSER_FLOW=mobile 通过;2026-05-15 已补代表设备企业微信消息 deeplink 真机通过;2026-05-15 19:31 二次修复 f945c0b 已部署且 /ready public bundle live smoke env 和 DG7+R124 公网移动矩阵 6 passed;用户确认 SMOKE101 审批状态与三按钮布局、SMOKE002 详情头部间距、wangzhiling 财务入口定位均通过;Android 选择器显示拍照按平台限制处理 OA 端 PDF-only 校验已由 R7E-16B 覆盖;2026-05-15 21:46 CST 96488d8 已部署且 /ready public bundle live smoke env DG7+R124 公网移动矩阵和审批确认专项均通过;用户确认审批详情底部操作按钮可见且点击会出现二次确认弹窗。R7E-15 代表真机反馈已按当前试运行范围关闭;R7E-18 阶段验收结论仍需单独人工确认。
R7E-16,每日问题清单,"R7E 期间每天登记问题、级别、归属治理流、处理状态和证据路径。","1) 问题均有级别;2) 阻断问题当天决策;3) 新需求不混入即时修复;4) 每日 closeout 可追溯。","流程级验证:每日人工 closeout;问题分流到 R7E/UX8/ORG8/WF8/BIZ8/QAOPS8。",manual,DOING,TODO,TODO,docs/trialrun/人工试运行报告.md,"[""R7E-10"",""R7E-11"",""R7E-12"",""R7E-13"",""R7E-14"",""R7E-15""]",2026-05-14 用户真机反馈分流:免登录通过;企业微信消息 deeplink 全设备提示 requestid tokenexpired/深链已过期 为 HIGH 归属 WF8-11A;通知和待审批卡片右侧状态只露首字 为 MEDIUM 归属 R7E-16A;Android 企业微信 PDF 上传系统选择器仍显示拍照和选择文件 为 MEDIUM 归属 R7E-16B;财务移动入口和付款未启用空态不清楚 为 MEDIUM 归属 BIZ8-12A;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进对应详情 WF8-11 已关闭;二次反馈中通知新标记按未读行为接受,审批状态槽/详情头部/按钮布局归 R7E-16C 且 f945c0b 已部署公网验证通过并由用户确认通过;财务入口定位归 BIZ8-12B 已由用户确认通过;退回和驳回语义说明与二次确认归 R7E-16D 且 96488d8 已部署公网自动化验证通过并由用户确认真机可见二次弹窗;财务付款闭环延期到 P2;2026-05-15 R7E-16E/R7E-16F 已完成本地自动化和视觉门禁但尚未部署到公网;当前剩余为 R7E-16E/R7E-16F 部署后公网复核和 R7E-18 人工验收结论。
R7E-16A,移动通知和待审批状态槽修复,"修复移动通知卡片和待审批卡片右侧状态徽标只显示第一个字的问题,保持无 PC 壳和无横向滚动。","1) 未读 已读 审批中 已处理等状态在 320/360/390/430px 可完整显示;2) 通知和审批卡片标题仍可省略但不挤压状态;3) 页面 scrollWidth 不超过视口;4) Playwright 移动矩阵通过。","流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",npm|playwright,DONE,DONE,DONE,client/src/styles/mobile-touch.css|client/src/pages/MobilePortalPage.tsx|client/src/pages/MobileNotificationsPage.tsx|client/src/pages/MobileApprovalsPage.tsx|client/tests/e2e,"[""R7E-16""]",回滚路径:恢复 mobile-status-slot 旧样式和相关测试断言;不改变业务数据或路由。
R7E-16B,Android PDF 上传兜底校验,"补强移动发票上传的前端 PDF 过滤,记录 Android 企业微信系统选择器可能仍显示拍照和选择文件但 OA 不接收非 PDF 发票。","1) PDF 上传入口 accept 为 .pdf,application/pdf 且无 capture;2) 选择非 PDF 时前端拒绝并提示;3) 不把图片 OCR 误报为支持;4) 既有 PDF 上传测试通过。","流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/r111-invoice-reimbursement-guard.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",npm|playwright,DONE,DONE,DONE,client/src/lib/pdfFiles.ts|client/src/pages/MobileCapturePage.tsx|client/src/pages/MobileReimbursementDetailPage.tsx|client/src/pages/ReimbursementDetailPage.tsx|client/src/pages/invoice-pool/InvoiceUploadCard.tsx|client/tests/e2e,"[""R7E-16""]",Android WebView 文件选择器 UI 受系统和企业微信控制;本轮只保证 OA 端 PDF-only 校验和可解释提示 不新增图片 OCR。
R7E-16C,移动审批和详情头部二次修复,"根据 R7E 真机反馈修复审批卡片右侧只露待字、审批详情操作按钮布局和移动详情返回按钮与单号贴边问题。","1) 移动审批卡片和详情状态完整显示待处理/已处理等文本;2) 审批详情通过/驳回/退回在线操作在 320px 不挤压乱换行;3) 移动详情返回按钮保留左上且与标题至少 12px 间距;4) 不改变审批动作和 deeplink 语义。","流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;SMOKE101 真机复核 /mobile/approvals。",npm|playwright|manual,DONE,DONE,DONE,client/src/components/mobile/MobileDetailHeader.tsx|client/src/pages/MobileApprovalsPage.tsx|client/src/styles/mobile-touch.css|client/tests/e2e,"[""R7E-16A""]",已部署并真机通过:2026-05-15 19:31 CST f945c0b 已通过 deploy/trialrun/deploy.sh 发布到 https://oa.hyinda.com 且 SYNC_ENV=0 未覆盖远端 .env;部署后 /ready 通过 public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵 6 passed;用户确认 /mobile/approvals 右侧完整显示待处理 详情三个按钮在一行 且左上返回与标题有清楚间距。回滚路径:恢复移动详情头部和审批操作区样式;不改后端 API 权限 workflow 状态机或企业微信 deeplink。
R7E-16D,审批动作二次确认与底部操作统一,"保持通过/驳回/退回短按钮文案,但在 PC 和移动审批动作执行前增加二次确认,明确通过、驳回、退回的业务后果。","1) PC 审批详情通过/驳回/退回先弹确认;2) PC 列表快捷通过先弹确认;3) 移动审批详情通过/驳回/退回和离线草稿先弹确认;4) 确认文案区分通过推进、驳回终止、退回修改后重提;5) 不改后端 workflow 状态机。","流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;cd client && npx playwright test tests/e2e/workflow-action-confirm-ui.spec.ts tests/e2e/r89-mobile-login-navigation.spec.ts --workers=1 --reporter=list。",npm|playwright,DONE,DONE,DONE,client/src/components/common/ConfirmDialog.tsx|client/src/lib/workflowActionConfirm.ts|client/src/pages/MobileApprovalsPage.tsx|client/src/pages/WorkflowTaskDetailPage.tsx|client/src/pages/WorkflowTasksPage.tsx|client/tests/e2e/workflow-action-confirm-ui.spec.ts|client/tests/e2e/r89-mobile-login-navigation.spec.ts|docs/需求-统一审批中心规约.md|docs/产品设计规则.md,"[""R7E-16C""]",已部署待真机复测:2026-05-15 21:46 CST 96488d8 已通过 deploy/trialrun/deploy.sh 发布到 https://oa.hyinda.com 且 SYNC_ENV=0 未覆盖远端 .env;部署脚本内置远端构建 init migration status /ready external deps smoke runtime smoke 和 public bundle 扫描通过;部署后 /ready ok public bundle findings=0 live smoke env ready;E2E_BASE_URL=https://oa.hyinda.com DG7+R124 公网移动矩阵复跑 6 passed;E2E_BASE_URL=https://oa.hyinda.com workflow-action-confirm-ui + R89 审批确认专项 5 passed;此前一次移动矩阵首轮失败为登录页前置时序,复跑已通过。回滚路径:移除二次确认 helper 和调用点 恢复原按钮直接触发 mutation;不改后端 API 权限 workflow 状态机 企业微信 deeplink 或付款批次开关。
R7E-17,阻断问题修复分流,"根据每日问题清单把阻断问题分流到当前 R7E 修复或后续连续治理流。","1) 阻断问题有 owner;2) 能在 R7E 内修的有方案和验证;3) 范围外问题进入 UX8/ORG8/WF8/BIZ8/QAOPS8;4) 不顺手扩大。","流程级验证:人工核对问题清单和分流结果;必要时创建后续连续 CSV。",manual,TODO,TODO,TODO,issues/*.csv|docs/trialrun/人工试运行报告.md,"[""R7E-16""]",已知分流:deeplink 归 WF8 并已代表设备验证关闭;状态槽归 R7E-16A 已部署并自动化验证;Android 选择器归 R7E-16B 按平台限制和 OA PDF-only 校验处理;财务付款批次延期到 P2;R7E-16D 已部署并公网自动化通过;本行待 R7E-15 审批二次确认真机反馈和 R7E-18 验收结论后再关闭。
R7E-16E,全站高风险操作二次确认统一,"把 R7E-16D 的审批确认模式推广到全站高风险动作,覆盖提交审批、撤回、删除、归档、启用/停用、批量处理、财务复核/付款、重复票据例外、敏感导出和通知测试/重发,不让普通保存草稿产生确认疲劳。","1) 新增通用 ActionConfirmDialog 和确认文案 helper;2) client/src 不再出现 window.confirm/window.prompt;3) 当前 R7E 相关高风险入口使用 OA 内确认弹窗和统一 testid;4) 不改后端 API/schema/权限/workflow/付款开关。","流程级验证:cd client && npm run typecheck && npm run build;rg -n 'window\\.(confirm|prompt)' client/src 应无结果;cd client && npx playwright test tests/e2e/system-action-confirmation.spec.ts --workers=1 --reporter=list。",npm|rg|playwright,DONE,DONE,DONE,client/src/components/common/ActionConfirmDialog.tsx|client/src/lib/actionConfirm.ts|client/src/pages|client/tests/e2e/system-action-confirmation.spec.ts|docs/产品设计规则.md|docs/需求-统一审批中心规约.md,"[""R7E-16D""]",2026-05-15 本地完成并验证:client typecheck 通过 client build 通过 rg window.confirm/window.prompt 无结果 system-action-confirmation 2 passed;覆盖报销草稿删除 敏感导出 付款批次执行等高风险动作的取消不调用接口和确认后调用接口;回滚路径为恢复相关按钮直接调用 mutation 和旧 ConfirmDialog;不改数据结构或远端配置。PC 端由自动化验收 真机仅保留企业微信差异项。
R7E-16F,PC 自动化和视觉门禁补强,"把 PC 端高风险确认验收从人工点击改为 Playwright 自动化和视觉检查,移动仍用矩阵覆盖布局,企业微信真机只测真实环境差异。","1) PC mock 自动化覆盖取消不调用接口、确认后才调用接口;2) 移动矩阵覆盖确认弹窗不溢出和吸底操作稳定;3) project-wide 视觉检查通过;4) QAOPS8-17 记录 PC 自动化替代人工验收口径。","流程级验证:cd client && npx playwright test tests/e2e/system-action-confirmation.spec.ts tests/e2e/workflow-action-confirm-ui.spec.ts --workers=1 --reporter=list;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;QA_SKIP_CLIENT_BUILD=1 bash scripts/qa/run-web-gates.sh project-wide-visual-local。",playwright|bash,DONE,DONE,DONE,client/tests/e2e/system-action-confirmation.spec.ts|client/tests/e2e/workflow-action-confirm-ui.spec.ts|docs/trialrun/人工试运行报告.md|issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv,"[""R7E-16E""]",2026-05-15 本地完成并验证:system-action-confirmation + workflow-action-confirm-ui 4 passed;DG7+R124 移动矩阵 6 passed;project-wide-visual-local 通过并自动准备临时 DB 和种子 1 passed。直接把 project-wide-visual 挂到裸 Vite 时曾因默认 E2E 数据前置不足出现登录 API HTTP_500 已归类为前置问题不作为业务回归。公网部署前只能记录本地自动化证据;部署后再复跑公网 ready public bundle PC mock 自动化和移动矩阵。R7E-18 仍需用户验收结论。
R7E-17,阻断问题修复分流,"根据每日问题清单把阻断问题分流到当前 R7E 修复或后续连续治理流。","1) 阻断问题有 owner;2) 能在 R7E 内修的有方案和验证;3) 范围外问题进入 UX8/ORG8/WF8/BIZ8/QAOPS8;4) 不顺手扩大。","流程级验证:人工核对问题清单和分流结果;必要时创建后续连续 CSV。",manual,TODO,TODO,TODO,issues/*.csv|docs/trialrun/人工试运行报告.md,"[""R7E-16""]",已知分流:deeplink 归 WF8 并已代表设备验证关闭;状态槽归 R7E-16A 已部署并自动化验证;Android 选择器归 R7E-16B 按平台限制和 OA PDF-only 校验处理;财务付款批次延期到 P2;R7E-16D 已部署并公网自动化通过且代表真机确认;R7E-16E/R7E-16F 已本地完成但尚未部署公网;本行待部署后公网复核和 R7E-18 验收结论后再关闭。
R7E-18,验收结论,"汇总 R7E 真实试运行结果,给出通过、暂停、回滚或继续观察结论。","1) 主流程结论完整;2) 阻断问题为 0 或明确暂停/回滚;3) 用户验收结论记录;4) 未完成项进入后续治理流。","流程级验证:人工验收;报告中列出流程结论、问题清单和后续动作。",manual,TODO,TODO,TODO,docs/trialrun/人工试运行报告.md,"[""R7E-17""]",不能仅凭自动化通过宣布 R7E 完成。
R7E-19,P2 排序调整,"根据 R7E 真实反馈调整 P2 候选优先级和后续连续治理流顺序。","1) P2 排序基于真实反馈;2) ORG8/WF8/BIZ8/QAOPS8 入口明确;3) 冻结项仍不进入;4) 如需变更 PRD 则另走事实源更新。","流程级验证:人工核对 PRD P2T 表、问题清单和后续 Issue 计划。",manual|rg,TODO,TODO,TODO,docs/海音达OA-MVP需求规格说明书.md|issues/*.csv,"[""R7E-18""]",P2 功能开发必须另建或延续对应连续合同。
R7E-20,R7E 收口,"完成 R7E CSV 状态回填、报告归档、验证说明和中文提交。","1) CSV 校验通过;2) 所有 DONE 有证据;3) git diff --check 通过;4) 中文提交引用 R7E;5) 最终说明是否部署和是否可测。","流程级验证:validate_issues_csv.py 本 CSV;git diff --check;人工核对报告和状态。",python3|git|manual,TODO,TODO,TODO,issues/2026-05-13_17-11-44-r7e-human-trialrun.csv|docs/trialrun/人工试运行报告.md,"[""R7E-18"",""R7E-19""]",真实 R7E 完成后才能回填;当前不得提前 DONE。
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
14 R7E-12 请假流程试运行 员工提交请假,审批人审批,员工和团队视角查看状态。 1) 请假提交成功;2) 审批处理成功;3) 移动/PC 状态一致;4) 团队状态或日历口径可解释。 流程级验证:员工提交请假 -> 审批通过/驳回 -> 查看进度和团队状态。 manual|playwright DONE DONE DONE docs/trialrun/人工试运行报告.md|client/tests/e2e/manual/r7e-browser-business-trialrun.spec.ts ["R7E-8"] 2026-05-14 无人工自动化补测:员工创建未来工作日请假并提交;审批人处理通过;申请人 PC 回看已通过且 /mobile/leave 记录页可见无横向滚动;测试单据 a22e45f8-b3d6-4862-97f2-30cbe4815e09;证据 artifacts/r7e/browser-business-trialrun/report-leave.md。
15 R7E-13 采购流程试运行 员工提交或查看采购申请,审批人处理,状态和附件可追溯。 1) 采购申请路径可用;2) 审批处理可用;3) 附件和状态可追溯;4) 移动采购创建如缺失则记录为后续 UX/P2。 流程级验证:员工提交/查看采购 -> 审批处理 -> 查看状态和附件。 manual|playwright DONE DONE DONE docs/trialrun/人工试运行报告.md|client/tests/e2e/manual/r7e-browser-business-trialrun.spec.ts ["R7E-8"] 2026-05-14 无人工自动化补测:采购 CG-20260514-0005 完成 PC 创建 明细 无敏感测试附件上传 提交 审批 状态和审批历史回看;移动采购列表 详情和附件可见且无横向滚动;证据 artifacts/r7e/browser-business-trialrun/report-purchase-attachment.md。
16 R7E-14 财务审批和付款延期试运行 财务按真实路径复核到财务审批的报销单;本轮不启用 payment_batch_v1,不追付款批次闭环,付款批次作为 P2 单独窗口处理。 1) 财务角色入口正确;2) 到财务审批的报销单可查看;3) payment_batch_v1 关闭时有明确空态或错误契约;4) 管理员不自动获得财务能力;5) 不执行真实付款批次。 流程级验证:财务进入移动入口或台账 -> 查看到财务审批的报销单或付款批次未启用提示 -> 检查管理员无财务执行能力 -> 记录付款批次延期到 P2。 manual|playwright DONE DONE DONE docs/trialrun/人工试运行报告.md|client/tests/e2e/manual/r7e-browser-business-trialrun.spec.ts ["R7E-8"] 2026-05-14 finance-closeout 诊断:报销 40876ef3-0652-4972-aa14-1aad1a855ae0 / BX-20260514-0016 状态 approved;/api/ap-operations/reimbursement-ledger 和 /api/payment-batches 均返回 payment_batch_v1_disabled / flag_force_off;未创建付款批次 未复核 未执行付款 未验证导出。2026-05-14 用户确认 R7E 财务付款策略为先不启用,付款批次延期到 P2;22:08 最新部署后 R7E_BROWSER_FLOW=finance-closeout 通过但 AP API 按 flag_force_off SKIP,R7E_BROWSER_FLOW=mobile 通过并确认员工 审批人 财务移动入口可见且无横向滚动;本行按延期口径收口,不代表付款批次已启用。
17 R7E-15 移动端和真机反馈 收集 R7E 参与者在移动浏览器和企业微信中的真实体验反馈。 1) 至少覆盖代表性 iPhone/Android 或企业微信设备;2) 记录页面、视口、角色和问题级别;3) 阻断问题进入修复分流;4) 体验优化进入 UX8。 流程级验证:按真机公网测试清单人工复核;必要时补 Playwright 截图。 manual|playwright DOING DONE TODO DONE TODO DONE docs/trialrun/人工试运行报告.md|output/playwright ["R7E-9"] 2026-05-14 自动化移动浏览器员工 审批人 财务核心入口通过且无横向滚动;用户真机补充:所有设备没有 PC 壳和横向滚动 但通知卡片和待审批卡片右侧状态只露第一个字 归属 R7E-16A;iOS 只能上传 PDF Android 企业微信选择器仍显示拍照和选择文件 归属 R7E-16B;2026-05-14 22:09 修复 5491e92 已部署 DG7+R124 公网移动矩阵 6 passed 且 R7E_BROWSER_FLOW=mobile 通过;2026-05-15 已补代表设备企业微信消息 deeplink 真机通过;2026-05-15 19:31 二次修复 f945c0b 已部署且 /ready public bundle live smoke env 和 DG7+R124 公网移动矩阵 6 passed;用户确认 SMOKE101 审批状态与三按钮布局、SMOKE002 详情头部间距、wangzhiling 财务入口定位均通过;Android 选择器显示拍照按平台限制处理 OA 端 PDF-only 校验已由 R7E-16B 覆盖;2026-05-15 21:46 CST 96488d8 已部署且 /ready public bundle live smoke env DG7+R124 公网移动矩阵和审批确认专项均通过;本行仍等待 R7E-16D 审批二次确认代表真机反馈和 R7E-18 阶段验收结论。 2026-05-14 自动化移动浏览器员工 审批人 财务核心入口通过且无横向滚动;用户真机补充:所有设备没有 PC 壳和横向滚动 但通知卡片和待审批卡片右侧状态只露第一个字 归属 R7E-16A;iOS 只能上传 PDF Android 企业微信选择器仍显示拍照和选择文件 归属 R7E-16B;2026-05-14 22:09 修复 5491e92 已部署 DG7+R124 公网移动矩阵 6 passed 且 R7E_BROWSER_FLOW=mobile 通过;2026-05-15 已补代表设备企业微信消息 deeplink 真机通过;2026-05-15 19:31 二次修复 f945c0b 已部署且 /ready public bundle live smoke env 和 DG7+R124 公网移动矩阵 6 passed;用户确认 SMOKE101 审批状态与三按钮布局、SMOKE002 详情头部间距、wangzhiling 财务入口定位均通过;Android 选择器显示拍照按平台限制处理 OA 端 PDF-only 校验已由 R7E-16B 覆盖;2026-05-15 21:46 CST 96488d8 已部署且 /ready public bundle live smoke env DG7+R124 公网移动矩阵和审批确认专项均通过;用户确认审批详情底部操作按钮可见且点击会出现二次确认弹窗。R7E-15 代表真机反馈已按当前试运行范围关闭;R7E-18 阶段验收结论仍需单独人工确认。
18 R7E-16 每日问题清单 R7E 期间每天登记问题、级别、归属治理流、处理状态和证据路径。 1) 问题均有级别;2) 阻断问题当天决策;3) 新需求不混入即时修复;4) 每日 closeout 可追溯。 流程级验证:每日人工 closeout;问题分流到 R7E/UX8/ORG8/WF8/BIZ8/QAOPS8。 manual DOING TODO TODO docs/trialrun/人工试运行报告.md ["R7E-10","R7E-11","R7E-12","R7E-13","R7E-14","R7E-15"] 2026-05-14 用户真机反馈分流:免登录通过;企业微信消息 deeplink 全设备提示 requestid tokenexpired/深链已过期 为 HIGH 归属 WF8-11A;通知和待审批卡片右侧状态只露首字 为 MEDIUM 归属 R7E-16A;Android 企业微信 PDF 上传系统选择器仍显示拍照和选择文件 为 MEDIUM 归属 R7E-16B;财务移动入口和付款未启用空态不清楚 为 MEDIUM 归属 BIZ8-12A;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进对应详情 WF8-11 已关闭;二次反馈中通知新标记按未读行为接受,审批状态槽/详情头部/按钮布局归 R7E-16C 且 f945c0b 已部署公网验证通过并由用户确认通过;财务入口定位归 BIZ8-12B 已由用户确认通过;退回和驳回语义说明与二次确认归 R7E-16D 且 96488d8 已部署公网自动化验证通过;财务付款闭环延期到 P2;当前剩余为 R7E-16D 部署后代表真机确认和 R7E-18 人工验收结论。 2026-05-14 用户真机反馈分流:免登录通过;企业微信消息 deeplink 全设备提示 requestid tokenexpired/深链已过期 为 HIGH 归属 WF8-11A;通知和待审批卡片右侧状态只露首字 为 MEDIUM 归属 R7E-16A;Android 企业微信 PDF 上传系统选择器仍显示拍照和选择文件 为 MEDIUM 归属 R7E-16B;财务移动入口和付款未启用空态不清楚 为 MEDIUM 归属 BIZ8-12A;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进对应详情 WF8-11 已关闭;二次反馈中通知新标记按未读行为接受,审批状态槽/详情头部/按钮布局归 R7E-16C 且 f945c0b 已部署公网验证通过并由用户确认通过;财务入口定位归 BIZ8-12B 已由用户确认通过;退回和驳回语义说明与二次确认归 R7E-16D 且 96488d8 已部署公网自动化验证通过并由用户确认真机可见二次弹窗;财务付款闭环延期到 P2;2026-05-15 R7E-16E/R7E-16F 已完成本地自动化和视觉门禁但尚未部署到公网;当前剩余为 R7E-16E/R7E-16F 部署后公网复核和 R7E-18 人工验收结论。
19 R7E-16A 移动通知和待审批状态槽修复 修复移动通知卡片和待审批卡片右侧状态徽标只显示第一个字的问题,保持无 PC 壳和无横向滚动。 1) 未读 已读 审批中 已处理等状态在 320/360/390/430px 可完整显示;2) 通知和审批卡片标题仍可省略但不挤压状态;3) 页面 scrollWidth 不超过视口;4) Playwright 移动矩阵通过。 流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。 npm|playwright DONE DONE DONE client/src/styles/mobile-touch.css|client/src/pages/MobilePortalPage.tsx|client/src/pages/MobileNotificationsPage.tsx|client/src/pages/MobileApprovalsPage.tsx|client/tests/e2e ["R7E-16"] 回滚路径:恢复 mobile-status-slot 旧样式和相关测试断言;不改变业务数据或路由。
20 R7E-16B Android PDF 上传兜底校验 补强移动发票上传的前端 PDF 过滤,记录 Android 企业微信系统选择器可能仍显示拍照和选择文件但 OA 不接收非 PDF 发票。 1) PDF 上传入口 accept 为 .pdf,application/pdf 且无 capture;2) 选择非 PDF 时前端拒绝并提示;3) 不把图片 OCR 误报为支持;4) 既有 PDF 上传测试通过。 流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/r111-invoice-reimbursement-guard.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。 npm|playwright DONE DONE DONE client/src/lib/pdfFiles.ts|client/src/pages/MobileCapturePage.tsx|client/src/pages/MobileReimbursementDetailPage.tsx|client/src/pages/ReimbursementDetailPage.tsx|client/src/pages/invoice-pool/InvoiceUploadCard.tsx|client/tests/e2e ["R7E-16"] Android WebView 文件选择器 UI 受系统和企业微信控制;本轮只保证 OA 端 PDF-only 校验和可解释提示 不新增图片 OCR。
21 R7E-16C 移动审批和详情头部二次修复 根据 R7E 真机反馈修复审批卡片右侧只露待字、审批详情操作按钮布局和移动详情返回按钮与单号贴边问题。 1) 移动审批卡片和详情状态完整显示待处理/已处理等文本;2) 审批详情通过/驳回/退回在线操作在 320px 不挤压乱换行;3) 移动详情返回按钮保留左上且与标题至少 12px 间距;4) 不改变审批动作和 deeplink 语义。 流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;SMOKE101 真机复核 /mobile/approvals。 npm|playwright|manual DONE DONE DONE client/src/components/mobile/MobileDetailHeader.tsx|client/src/pages/MobileApprovalsPage.tsx|client/src/styles/mobile-touch.css|client/tests/e2e ["R7E-16A"] 已部署并真机通过:2026-05-15 19:31 CST f945c0b 已通过 deploy/trialrun/deploy.sh 发布到 https://oa.hyinda.com 且 SYNC_ENV=0 未覆盖远端 .env;部署后 /ready 通过 public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵 6 passed;用户确认 /mobile/approvals 右侧完整显示待处理 详情三个按钮在一行 且左上返回与标题有清楚间距。回滚路径:恢复移动详情头部和审批操作区样式;不改后端 API 权限 workflow 状态机或企业微信 deeplink。
22 R7E-16D 审批动作二次确认与底部操作统一 保持通过/驳回/退回短按钮文案,但在 PC 和移动审批动作执行前增加二次确认,明确通过、驳回、退回的业务后果。 1) PC 审批详情通过/驳回/退回先弹确认;2) PC 列表快捷通过先弹确认;3) 移动审批详情通过/驳回/退回和离线草稿先弹确认;4) 确认文案区分通过推进、驳回终止、退回修改后重提;5) 不改后端 workflow 状态机。 流程级验证:cd client && npm run typecheck && npm run build;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;cd client && npx playwright test tests/e2e/workflow-action-confirm-ui.spec.ts tests/e2e/r89-mobile-login-navigation.spec.ts --workers=1 --reporter=list。 npm|playwright DONE DONE DONE client/src/components/common/ConfirmDialog.tsx|client/src/lib/workflowActionConfirm.ts|client/src/pages/MobileApprovalsPage.tsx|client/src/pages/WorkflowTaskDetailPage.tsx|client/src/pages/WorkflowTasksPage.tsx|client/tests/e2e/workflow-action-confirm-ui.spec.ts|client/tests/e2e/r89-mobile-login-navigation.spec.ts|docs/需求-统一审批中心规约.md|docs/产品设计规则.md ["R7E-16C"] 已部署待真机复测:2026-05-15 21:46 CST 96488d8 已通过 deploy/trialrun/deploy.sh 发布到 https://oa.hyinda.com 且 SYNC_ENV=0 未覆盖远端 .env;部署脚本内置远端构建 init migration status /ready external deps smoke runtime smoke 和 public bundle 扫描通过;部署后 /ready ok public bundle findings=0 live smoke env ready;E2E_BASE_URL=https://oa.hyinda.com DG7+R124 公网移动矩阵复跑 6 passed;E2E_BASE_URL=https://oa.hyinda.com workflow-action-confirm-ui + R89 审批确认专项 5 passed;此前一次移动矩阵首轮失败为登录页前置时序,复跑已通过。回滚路径:移除二次确认 helper 和调用点 恢复原按钮直接触发 mutation;不改后端 API 权限 workflow 状态机 企业微信 deeplink 或付款批次开关。
23 R7E-17 R7E-16E 阻断问题修复分流 全站高风险操作二次确认统一 根据每日问题清单把阻断问题分流到当前 R7E 修复或后续连续治理流。 把 R7E-16D 的审批确认模式推广到全站高风险动作,覆盖提交审批、撤回、删除、归档、启用/停用、批量处理、财务复核/付款、重复票据例外、敏感导出和通知测试/重发,不让普通保存草稿产生确认疲劳。 1) 阻断问题有 owner;2) 能在 R7E 内修的有方案和验证;3) 范围外问题进入 UX8/ORG8/WF8/BIZ8/QAOPS8;4) 不顺手扩大。 1) 新增通用 ActionConfirmDialog 和确认文案 helper;2) client/src 不再出现 window.confirm/window.prompt;3) 当前 R7E 相关高风险入口使用 OA 内确认弹窗和统一 testid;4) 不改后端 API/schema/权限/workflow/付款开关。 流程级验证:人工核对问题清单和分流结果;必要时创建后续连续 CSV。 流程级验证:cd client && npm run typecheck && npm run build;rg -n 'window\\.(confirm|prompt)' client/src 应无结果;cd client && npx playwright test tests/e2e/system-action-confirmation.spec.ts --workers=1 --reporter=list。 manual npm|rg|playwright TODO DONE TODO DONE TODO DONE issues/*.csv|docs/trialrun/人工试运行报告.md client/src/components/common/ActionConfirmDialog.tsx|client/src/lib/actionConfirm.ts|client/src/pages|client/tests/e2e/system-action-confirmation.spec.ts|docs/产品设计规则.md|docs/需求-统一审批中心规约.md ["R7E-16"] ["R7E-16D"] 已知分流:deeplink 归 WF8 并已代表设备验证关闭;状态槽归 R7E-16A 已部署并自动化验证;Android 选择器归 R7E-16B 按平台限制和 OA PDF-only 校验处理;财务付款批次延期到 P2;R7E-16D 已部署并公网自动化通过;本行待 R7E-15 审批二次确认真机反馈和 R7E-18 验收结论后再关闭。 2026-05-15 本地完成并验证:client typecheck 通过 client build 通过 rg window.confirm/window.prompt 无结果 system-action-confirmation 2 passed;覆盖报销草稿删除 敏感导出 付款批次执行等高风险动作的取消不调用接口和确认后调用接口;回滚路径为恢复相关按钮直接调用 mutation 和旧 ConfirmDialog;不改数据结构或远端配置。PC 端由自动化验收 真机仅保留企业微信差异项。
24 R7E-16F PC 自动化和视觉门禁补强 把 PC 端高风险确认验收从人工点击改为 Playwright 自动化和视觉检查,移动仍用矩阵覆盖布局,企业微信真机只测真实环境差异。 1) PC mock 自动化覆盖取消不调用接口、确认后才调用接口;2) 移动矩阵覆盖确认弹窗不溢出和吸底操作稳定;3) project-wide 视觉检查通过;4) QAOPS8-17 记录 PC 自动化替代人工验收口径。 流程级验证:cd client && npx playwright test tests/e2e/system-action-confirmation.spec.ts tests/e2e/workflow-action-confirm-ui.spec.ts --workers=1 --reporter=list;cd client && npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list;QA_SKIP_CLIENT_BUILD=1 bash scripts/qa/run-web-gates.sh project-wide-visual-local。 playwright|bash DONE DONE DONE client/tests/e2e/system-action-confirmation.spec.ts|client/tests/e2e/workflow-action-confirm-ui.spec.ts|docs/trialrun/人工试运行报告.md|issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv ["R7E-16E"] 2026-05-15 本地完成并验证:system-action-confirmation + workflow-action-confirm-ui 4 passed;DG7+R124 移动矩阵 6 passed;project-wide-visual-local 通过并自动准备临时 DB 和种子 1 passed。直接把 project-wide-visual 挂到裸 Vite 时曾因默认 E2E 数据前置不足出现登录 API HTTP_500 已归类为前置问题不作为业务回归。公网部署前只能记录本地自动化证据;部署后再复跑公网 ready public bundle PC mock 自动化和移动矩阵。R7E-18 仍需用户验收结论。
25 R7E-17 阻断问题修复分流 根据每日问题清单把阻断问题分流到当前 R7E 修复或后续连续治理流。 1) 阻断问题有 owner;2) 能在 R7E 内修的有方案和验证;3) 范围外问题进入 UX8/ORG8/WF8/BIZ8/QAOPS8;4) 不顺手扩大。 流程级验证:人工核对问题清单和分流结果;必要时创建后续连续 CSV。 manual TODO TODO TODO issues/*.csv|docs/trialrun/人工试运行报告.md ["R7E-16"] 已知分流:deeplink 归 WF8 并已代表设备验证关闭;状态槽归 R7E-16A 已部署并自动化验证;Android 选择器归 R7E-16B 按平台限制和 OA PDF-only 校验处理;财务付款批次延期到 P2;R7E-16D 已部署并公网自动化通过且代表真机确认;R7E-16E/R7E-16F 已本地完成但尚未部署公网;本行待部署后公网复核和 R7E-18 验收结论后再关闭。
26 R7E-18 验收结论 汇总 R7E 真实试运行结果,给出通过、暂停、回滚或继续观察结论。 1) 主流程结论完整;2) 阻断问题为 0 或明确暂停/回滚;3) 用户验收结论记录;4) 未完成项进入后续治理流。 流程级验证:人工验收;报告中列出流程结论、问题清单和后续动作。 manual TODO TODO TODO docs/trialrun/人工试运行报告.md ["R7E-17"] 不能仅凭自动化通过宣布 R7E 完成。
27 R7E-19 P2 排序调整 根据 R7E 真实反馈调整 P2 候选优先级和后续连续治理流顺序。 1) P2 排序基于真实反馈;2) ORG8/WF8/BIZ8/QAOPS8 入口明确;3) 冻结项仍不进入;4) 如需变更 PRD 则另走事实源更新。 流程级验证:人工核对 PRD P2T 表、问题清单和后续 Issue 计划。 manual|rg TODO TODO TODO docs/海音达OA-MVP需求规格说明书.md|issues/*.csv ["R7E-18"] P2 功能开发必须另建或延续对应连续合同。
28 R7E-20 R7E 收口 完成 R7E CSV 状态回填、报告归档、验证说明和中文提交。 1) CSV 校验通过;2) 所有 DONE 有证据;3) git diff --check 通过;4) 中文提交引用 R7E;5) 最终说明是否部署和是否可测。 流程级验证:validate_issues_csv.py 本 CSV;git diff --check;人工核对报告和状态。 python3|git|manual TODO TODO TODO issues/2026-05-13_17-11-44-r7e-human-trialrun.csv|docs/trialrun/人工试运行报告.md ["R7E-18","R7E-19"] 真实 R7E 完成后才能回填;当前不得提前 DONE。
@@ -16,7 +16,7 @@ QAOPS8-13,DORA 交付指标基线,"建立部署频率、变更前置时间、变
QAOPS8-14,安全发布前检查,"建立发布前安全检查,包括权限、敏感信息、日志、配置、依赖和等保相关底线。","1) 不泄露 token、密码、票据和个人隐私;2) 权限直连审计有证据;3) 日志不记录敏感明文;4) 高风险项进入 Go/No-Go。","流程级验证:node scripts/qa/audit-docs.mjs --check-links;rg -n 'password|token|secret|密钥|权限直连|敏感' docs scripts issues。",node|rg|manual,DONE,DONE,DONE,scripts/qa/audit-docs.mjs|docs|scripts|issues,"[""QAOPS8-9""]",node scripts/qa/audit-docs.mjs --check-links 为 errors=0 warnings=0;全量 audit-docs 为 errors=0 warnings=8;已知 QA 脚本本地测试哨兵值和未登记 docs 作为 Go/No-Go 复核项,不声明等保测评完成。
QAOPS8-15,发布 Runbook 收敛,"把发布前检查、部署步骤、验证步骤、暂停条件和沟通责任收敛到现有 Runbook。","1) 发布步骤可按顺序执行;2) 预检、部署、验证、暂停、回滚边界清楚;3) 不新增冲突文档;4) 用户可审计。","流程级验证:rg -n '发布|预检|部署|验证|暂停|回滚|Runbook' docs/trialrun/发布试运行准备清单.md docs/trialrun/OPS7运维交接Runbook.md docs/试运行与自动化验证手册.md。",rg|manual,DONE,DONE,DONE,docs/trialrun/发布试运行准备清单.md|docs/trialrun/OPS7运维交接Runbook.md|docs/试运行与自动化验证手册.md,"[""QAOPS8-5"",""QAOPS8-6"",""QAOPS8-10""]",rg 复核通过;发布前检查、部署步骤、部署后验证、暂停条件、回滚边界和责任记录均在现有 Runbook 中;未新增冲突文档。
QAOPS8-16,回滚演练窗口,"确认回滚演练窗口、负责人、触发条件、数据保护和恢复验证步骤。","1) 窗口和负责人确认;2) 回滚脚本或步骤有只读预检;3) 数据保护优先;4) 演练结果可追溯。","流程级验证:人工确认回滚窗口;必要时先在非生产环境执行。",manual,DONE,DONE,DONE,docs/trialrun/OPS7运维交接Runbook.md,"[""QAOPS8-11"",""QAOPS8-15""]",完成:按用户授权采用推荐方案;窗口暂定 2026-05-14 19:30-20:30 Asia/Shanghai 即 R7E 首日每日收口后;负责人和暂停回滚决策人 wangshunli;范围为非生产恢复演练优先 生产只读回滚 dry run;步骤为备份清单核对 隔离库恢复 /ready 登录核心流程只读验证 生产回滚步骤只读复盘;触发条件为 P0 数据损坏 发布阻断或公网关键门禁失败;停止条件为备份不完整 权限不明确 影响生产或责任人不在线;本行只确认窗口和边界 未执行数据库恢复 生产回滚 服务切换或破坏性命令;真实恢复或生产回滚执行前仍需再次明确批准。
QAOPS8-17,上线 Go No-Go 清单,"建立上线或扩大试运行 Go/No-Go 清单,统一本地、公网匿名、带凭据、企业微信真机和人工验收结论。","1) Go、No-Go、Pause 条件明确;2) 阻断问题为 0 或有明确延期;3) R7E 人工验收不被自动化替代;4) 结论写入 Issue 和报告。","流程级验证:人工核对 R7E 结论、QAOPS8 门禁、发布清单和问题清单。",manual,DOING,TODO,TODO,docs/trialrun/发布试运行准备清单.md|docs/trialrun/人工试运行报告.md|issues/*.csv,"[""QAOPS8-15"",""QAOPS8-16""]",当前清单已形成:2026-05-14 最新代码 4b14c92 已部署到试运行环境且 R7E-8 完成;本轮无人工自动化补测结果为公网 /ready 通过 public bundle findings=0 live smoke env ready 授权 PDF 可读,R7E preflight 通过 报销 BX-20260514-0016 创建上传提交审批通过 请假通过 采购 CG-20260514-0005 含附件 PC/移动通过 移动浏览器通过 WeCom UA 模拟通过;审批边界 approval-boundary 通过,申请人和 admin-only 审批接口均被拒绝且经理正常收尾;财务 finance-closeout 诊断为 AP/payment-batches 功能门禁 payment_batch_v1_disabled / flag_force_off,未执行付款闭环;dg7-mobile-information-dedupe 启动 Vite 后 5 passed,前次 127.0.0.1:3000 失败确认为前置问题;2026-05-14 22:05 已追加部署修复提交 5491e92,deploy.sh 远端 build/migration status 通过,public bundle 首次短暂 502 后复核 /ready 200 首页 200 public bundle findings=0,external deps smoke 和 runtime smoke 通过,live readiness 3 passed,真实 OCR 样票 1 passed,DG7+R124 公网移动矩阵 6 passed,R7E mobile/finance-closeout/wecom-ua 分段通过或按 flag_force_off 可解释 SKIP;R7E-14 已按用户确认的付款批次延期到 P2 口径收口;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进入对应详情 WF8-11 和 R7E-9 已关闭;2026-05-15 19:31 二次修复 f945c0b 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready 通过 public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵 6 passed;用户确认 R7E-16C 审批状态/详情按钮/返回间距和 BIZ8-12B 财务入口定位真机通过;2026-05-15 21:46 R7E-16D 96488d8 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready ok public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵复跑 6 passed 审批确认专项 5 passed;BIZ8-13 权限回归和 BIZ8-CLOSE 已完成;当前 Go No-Go 仍为 Pause 因 R7E-16D 部署后代表真机复测和 R7E-18 人工验收结论未完成;明确 R7E 人工验收不被自动化替代。
QAOPS8-17,上线 Go No-Go 清单,"建立上线或扩大试运行 Go/No-Go 清单,统一本地、公网匿名、带凭据、企业微信真机和人工验收结论。","1) Go、No-Go、Pause 条件明确;2) 阻断问题为 0 或有明确延期;3) R7E 人工验收不被自动化替代;4) 结论写入 Issue 和报告。","流程级验证:人工核对 R7E 结论、QAOPS8 门禁、发布清单和问题清单。",manual,DOING,TODO,TODO,docs/trialrun/发布试运行准备清单.md|docs/trialrun/人工试运行报告.md|issues/*.csv,"[""QAOPS8-15"",""QAOPS8-16""]",当前清单已形成:2026-05-14 最新代码 4b14c92 已部署到试运行环境且 R7E-8 完成;本轮无人工自动化补测结果为公网 /ready 通过 public bundle findings=0 live smoke env ready 授权 PDF 可读,R7E preflight 通过 报销 BX-20260514-0016 创建上传提交审批通过 请假通过 采购 CG-20260514-0005 含附件 PC/移动通过 移动浏览器通过 WeCom UA 模拟通过;审批边界 approval-boundary 通过,申请人和 admin-only 审批接口均被拒绝且经理正常收尾;财务 finance-closeout 诊断为 AP/payment-batches 功能门禁 payment_batch_v1_disabled / flag_force_off,未执行付款闭环;dg7-mobile-information-dedupe 启动 Vite 后 5 passed,前次 127.0.0.1:3000 失败确认为前置问题;2026-05-14 22:05 已追加部署修复提交 5491e92,deploy.sh 远端 build/migration status 通过,public bundle 首次短暂 502 后复核 /ready 200 首页 200 public bundle findings=0,external deps smoke 和 runtime smoke 通过,live readiness 3 passed,真实 OCR 样票 1 passed,DG7+R124 公网移动矩阵 6 passed,R7E mobile/finance-closeout/wecom-ua 分段通过或按 flag_force_off 可解释 SKIP;R7E-14 已按用户确认的付款批次延期到 P2 口径收口;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进入对应详情 WF8-11 和 R7E-9 已关闭;2026-05-15 19:31 二次修复 f945c0b 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready 通过 public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵 6 passed;用户确认 R7E-16C 审批状态/详情按钮/返回间距和 BIZ8-12B 财务入口定位真机通过;2026-05-15 21:46 R7E-16D 96488d8 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready ok public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵复跑 6 passed 审批确认专项 5 passed;BIZ8-13 权限回归和 BIZ8-CLOSE 已完成;2026-05-15 R7E-16E/R7E-16F 本地完成全站高风险确认统一和 PC 自动化验收口径:client typecheck/build 通过 rg window.confirm/window.prompt 无结果 system-action-confirmation 2 passed system-action-confirmation+workflow-action-confirm-ui 4 passed DG7+R124 移动矩阵 6 passed project-wide-visual-local 1 passed;PC 端高风险动作改由 Playwright 和视觉门禁验收 不再安排人工 PC 点击;当前 Go No-Go 仍为 Pause 因 R7E-16E/R7E-16F 尚未部署到公网且 R7E-18 人工验收结论未完成;明确 R7E 人工验收不被自动化替代。
QAOPS8-18,只读审计和排障边界,"明确公网、数据库、日志和 MCP 只读审计边界,避免排障时误写生产或泄露敏感信息。","1) 只读查询边界明确;2) 禁止 INSERT、UPDATE、DELETE;3) 敏感字段脱敏;4) 排障证据只记录摘要和路径。","流程级验证:rg -n 'audit-readonly|只读|INSERT|UPDATE|DELETE|MCP|PostgreSQL' AGENTS.md docs/mcp-tools.md docs/开发交付与变更管理规约.md。",rg|manual,DONE,DONE,DONE,AGENTS.md|docs/mcp-tools.md|docs/开发交付与变更管理规约.md,"[""QAOPS8-14""]",rg 复核通过;AGENTS 限定 audit-readonly 和 PostgreSQL MCP 只读查询;docs/mcp-tools 限定 SELECT 和禁止写操作;开发交付规约已补日志和证据摘要脱敏边界;未执行生产查询或启用新 MCP。
QAOPS8-19,R7E 后上线硬化收口,"在 R7E 真实反馈后集中收口上线硬化,更新门禁缺口、问题分流和后续治理优先级。","1) R7E 结论已记录;2) QAOPS8 未完成项有归属;3) 阻断问题有 owner;4) 后续 ORG8、WF8、BIZ8、QAOPS8 顺序可解释。","流程级验证:人工核对 R7E-18、R7E-19、QAOPS8 当前状态和问题清单。",manual,TODO,TODO,TODO,issues/2026-05-13_17-11-44-r7e-human-trialrun.csv|issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv,"[""QAOPS8-17""]",依赖 R7E 人工试运行结论;当前不能提前完成。
QAOPS8-20,QAOPS8 收口,"完成 QAOPS8 状态回填、验证摘要、证据索引和中文提交。","1) CSV 校验通过;2) 所有 DONE 有命令或人工证据;3) 文档索引和状态矩阵一致;4) git diff --check 通过;5) 中文提交引用 QAOPS8。","流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv;node scripts/qa/audit-docs.mjs --check-registry;git diff --check。",python3|node|git|manual,TODO,TODO,TODO,issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv|docs/文档索引.md|docs/文档事实源状态矩阵.md,"[""QAOPS8-19""]",所有执行行完成后才能收口。
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
16 QAOPS8-14 安全发布前检查 建立发布前安全检查,包括权限、敏感信息、日志、配置、依赖和等保相关底线。 1) 不泄露 token、密码、票据和个人隐私;2) 权限直连审计有证据;3) 日志不记录敏感明文;4) 高风险项进入 Go/No-Go。 流程级验证:node scripts/qa/audit-docs.mjs --check-links;rg -n 'password|token|secret|密钥|权限直连|敏感' docs scripts issues。 node|rg|manual DONE DONE DONE scripts/qa/audit-docs.mjs|docs|scripts|issues ["QAOPS8-9"] node scripts/qa/audit-docs.mjs --check-links 为 errors=0 warnings=0;全量 audit-docs 为 errors=0 warnings=8;已知 QA 脚本本地测试哨兵值和未登记 docs 作为 Go/No-Go 复核项,不声明等保测评完成。
17 QAOPS8-15 发布 Runbook 收敛 把发布前检查、部署步骤、验证步骤、暂停条件和沟通责任收敛到现有 Runbook。 1) 发布步骤可按顺序执行;2) 预检、部署、验证、暂停、回滚边界清楚;3) 不新增冲突文档;4) 用户可审计。 流程级验证:rg -n '发布|预检|部署|验证|暂停|回滚|Runbook' docs/trialrun/发布试运行准备清单.md docs/trialrun/OPS7运维交接Runbook.md docs/试运行与自动化验证手册.md。 rg|manual DONE DONE DONE docs/trialrun/发布试运行准备清单.md|docs/trialrun/OPS7运维交接Runbook.md|docs/试运行与自动化验证手册.md ["QAOPS8-5","QAOPS8-6","QAOPS8-10"] rg 复核通过;发布前检查、部署步骤、部署后验证、暂停条件、回滚边界和责任记录均在现有 Runbook 中;未新增冲突文档。
18 QAOPS8-16 回滚演练窗口 确认回滚演练窗口、负责人、触发条件、数据保护和恢复验证步骤。 1) 窗口和负责人确认;2) 回滚脚本或步骤有只读预检;3) 数据保护优先;4) 演练结果可追溯。 流程级验证:人工确认回滚窗口;必要时先在非生产环境执行。 manual DONE DONE DONE docs/trialrun/OPS7运维交接Runbook.md ["QAOPS8-11","QAOPS8-15"] 完成:按用户授权采用推荐方案;窗口暂定 2026-05-14 19:30-20:30 Asia/Shanghai 即 R7E 首日每日收口后;负责人和暂停回滚决策人 wangshunli;范围为非生产恢复演练优先 生产只读回滚 dry run;步骤为备份清单核对 隔离库恢复 /ready 登录核心流程只读验证 生产回滚步骤只读复盘;触发条件为 P0 数据损坏 发布阻断或公网关键门禁失败;停止条件为备份不完整 权限不明确 影响生产或责任人不在线;本行只确认窗口和边界 未执行数据库恢复 生产回滚 服务切换或破坏性命令;真实恢复或生产回滚执行前仍需再次明确批准。
19 QAOPS8-17 上线 Go No-Go 清单 建立上线或扩大试运行 Go/No-Go 清单,统一本地、公网匿名、带凭据、企业微信真机和人工验收结论。 1) Go、No-Go、Pause 条件明确;2) 阻断问题为 0 或有明确延期;3) R7E 人工验收不被自动化替代;4) 结论写入 Issue 和报告。 流程级验证:人工核对 R7E 结论、QAOPS8 门禁、发布清单和问题清单。 manual DOING TODO TODO docs/trialrun/发布试运行准备清单.md|docs/trialrun/人工试运行报告.md|issues/*.csv ["QAOPS8-15","QAOPS8-16"] 当前清单已形成:2026-05-14 最新代码 4b14c92 已部署到试运行环境且 R7E-8 完成;本轮无人工自动化补测结果为公网 /ready 通过 public bundle findings=0 live smoke env ready 授权 PDF 可读,R7E preflight 通过 报销 BX-20260514-0016 创建上传提交审批通过 请假通过 采购 CG-20260514-0005 含附件 PC/移动通过 移动浏览器通过 WeCom UA 模拟通过;审批边界 approval-boundary 通过,申请人和 admin-only 审批接口均被拒绝且经理正常收尾;财务 finance-closeout 诊断为 AP/payment-batches 功能门禁 payment_batch_v1_disabled / flag_force_off,未执行付款闭环;dg7-mobile-information-dedupe 启动 Vite 后 5 passed,前次 127.0.0.1:3000 失败确认为前置问题;2026-05-14 22:05 已追加部署修复提交 5491e92,deploy.sh 远端 build/migration status 通过,public bundle 首次短暂 502 后复核 /ready 200 首页 200 public bundle findings=0,external deps smoke 和 runtime smoke 通过,live readiness 3 passed,真实 OCR 样票 1 passed,DG7+R124 公网移动矩阵 6 passed,R7E mobile/finance-closeout/wecom-ua 分段通过或按 flag_force_off 可解释 SKIP;R7E-14 已按用户确认的付款批次延期到 P2 口径收口;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进入对应详情 WF8-11 和 R7E-9 已关闭;2026-05-15 19:31 二次修复 f945c0b 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready 通过 public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵 6 passed;用户确认 R7E-16C 审批状态/详情按钮/返回间距和 BIZ8-12B 财务入口定位真机通过;2026-05-15 21:46 R7E-16D 96488d8 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready ok public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵复跑 6 passed 审批确认专项 5 passed;BIZ8-13 权限回归和 BIZ8-CLOSE 已完成;当前 Go No-Go 仍为 Pause 因 R7E-16D 部署后代表真机复测和 R7E-18 人工验收结论未完成;明确 R7E 人工验收不被自动化替代。 当前清单已形成:2026-05-14 最新代码 4b14c92 已部署到试运行环境且 R7E-8 完成;本轮无人工自动化补测结果为公网 /ready 通过 public bundle findings=0 live smoke env ready 授权 PDF 可读,R7E preflight 通过 报销 BX-20260514-0016 创建上传提交审批通过 请假通过 采购 CG-20260514-0005 含附件 PC/移动通过 移动浏览器通过 WeCom UA 模拟通过;审批边界 approval-boundary 通过,申请人和 admin-only 审批接口均被拒绝且经理正常收尾;财务 finance-closeout 诊断为 AP/payment-batches 功能门禁 payment_batch_v1_disabled / flag_force_off,未执行付款闭环;dg7-mobile-information-dedupe 启动 Vite 后 5 passed,前次 127.0.0.1:3000 失败确认为前置问题;2026-05-14 22:05 已追加部署修复提交 5491e92,deploy.sh 远端 build/migration status 通过,public bundle 首次短暂 502 后复核 /ready 200 首页 200 public bundle findings=0,external deps smoke 和 runtime smoke 通过,live readiness 3 passed,真实 OCR 样票 1 passed,DG7+R124 公网移动矩阵 6 passed,R7E mobile/finance-closeout/wecom-ua 分段通过或按 flag_force_off 可解释 SKIP;R7E-14 已按用户确认的付款批次延期到 P2 口径收口;2026-05-15 用户确认代表设备企业微信消息 deeplink 可免登录进入对应详情 WF8-11 和 R7E-9 已关闭;2026-05-15 19:31 二次修复 f945c0b 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready 通过 public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵 6 passed;用户确认 R7E-16C 审批状态/详情按钮/返回间距和 BIZ8-12B 财务入口定位真机通过;2026-05-15 21:46 R7E-16D 96488d8 已部署 deploy.sh 远端构建 init migration ready external deps smoke runtime smoke public bundle 均通过 部署后 /ready ok public bundle findings=0 live smoke env ready DG7+R124 公网移动矩阵复跑 6 passed 审批确认专项 5 passed;BIZ8-13 权限回归和 BIZ8-CLOSE 已完成;2026-05-15 R7E-16E/R7E-16F 本地完成全站高风险确认统一和 PC 自动化验收口径:client typecheck/build 通过 rg window.confirm/window.prompt 无结果 system-action-confirmation 2 passed system-action-confirmation+workflow-action-confirm-ui 4 passed DG7+R124 移动矩阵 6 passed project-wide-visual-local 1 passed;PC 端高风险动作改由 Playwright 和视觉门禁验收 不再安排人工 PC 点击;当前 Go No-Go 仍为 Pause 因 R7E-16E/R7E-16F 尚未部署到公网且 R7E-18 人工验收结论未完成;明确 R7E 人工验收不被自动化替代。
20 QAOPS8-18 只读审计和排障边界 明确公网、数据库、日志和 MCP 只读审计边界,避免排障时误写生产或泄露敏感信息。 1) 只读查询边界明确;2) 禁止 INSERT、UPDATE、DELETE;3) 敏感字段脱敏;4) 排障证据只记录摘要和路径。 流程级验证:rg -n 'audit-readonly|只读|INSERT|UPDATE|DELETE|MCP|PostgreSQL' AGENTS.md docs/mcp-tools.md docs/开发交付与变更管理规约.md。 rg|manual DONE DONE DONE AGENTS.md|docs/mcp-tools.md|docs/开发交付与变更管理规约.md ["QAOPS8-14"] rg 复核通过;AGENTS 限定 audit-readonly 和 PostgreSQL MCP 只读查询;docs/mcp-tools 限定 SELECT 和禁止写操作;开发交付规约已补日志和证据摘要脱敏边界;未执行生产查询或启用新 MCP。
21 QAOPS8-19 R7E 后上线硬化收口 在 R7E 真实反馈后集中收口上线硬化,更新门禁缺口、问题分流和后续治理优先级。 1) R7E 结论已记录;2) QAOPS8 未完成项有归属;3) 阻断问题有 owner;4) 后续 ORG8、WF8、BIZ8、QAOPS8 顺序可解释。 流程级验证:人工核对 R7E-18、R7E-19、QAOPS8 当前状态和问题清单。 manual TODO TODO TODO issues/2026-05-13_17-11-44-r7e-human-trialrun.csv|issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv ["QAOPS8-17"] 依赖 R7E 人工试运行结论;当前不能提前完成。
22 QAOPS8-20 QAOPS8 收口 完成 QAOPS8 状态回填、验证摘要、证据索引和中文提交。 1) CSV 校验通过;2) 所有 DONE 有命令或人工证据;3) 文档索引和状态矩阵一致;4) git diff --check 通过;5) 中文提交引用 QAOPS8。 流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv;node scripts/qa/audit-docs.mjs --check-registry;git diff --check。 python3|node|git|manual TODO TODO TODO issues/2026-05-13_17-30-00-qaops8-test-release-ops.csv|docs/文档索引.md|docs/文档事实源状态矩阵.md ["QAOPS8-19"] 所有执行行完成后才能收口。