diff --git a/PROJECT_ISSUES.md b/PROJECT_ISSUES.md index 63f6301..640b258 100644 --- a/PROJECT_ISSUES.md +++ b/PROJECT_ISSUES.md @@ -696,3 +696,246 @@ CREATE TABLE IF NOT EXISTS `feishu_login_session` ( ### 配置要求 - 飞书应用 `tenant_key`: `14e7bc0fe5985740` - 飞书授权回调地址: `https://xsm.dev.hnxinyouhe.com/login/qrcode/{id}` + +--- + +## 25. 审批列表 Tab 状态持久化问题 + +**问题**:查看审批详情后返回上一页,tab 重置为"我发起的",而不是保持用户之前选择的 tab + +**原因**: +1. React Router 的 `navigate(-1)` 不传递 `location.state` +2. 页面组件重新挂载时,`useState` 初始值恢复为默认值 +3. 即使使用 `navigate(path, { state: { fromTab: activeTab } })`,`navigate(-1)` 时 state 仍然为 null + +**解决方案**: +使用 `sessionStorage` 持久化 tab 状态,而不是依赖 `location.state`: + +```typescript +// 优先从 sessionStorage 恢复 tab,其次从 location.state,最后默认为 'my' +const savedTab = sessionStorage.getItem('approval-tab') as string +const stateTab = ((location.state as any)?.fromTab as string) || undefined +const initialTab = (savedTab && ['my', 'pending', 'approved', 'cc'].includes(savedTab)) ? savedTab : (stateTab || 'my') +const [activeTab, setActiveTab] = useState<'my' | 'pending' | 'approved' | 'cc'>(initialTab) + +// 切换标签时保存到 sessionStorage +const handleTabChange = (key: string) => { + setActiveTab(key as 'my' | 'pending' | 'approved' | 'cc') + sessionStorage.setItem('approval-tab', key) // 保存到 sessionStorage + setList([]) + setHasMore(false) + setPage(1) + loadData(1, true, key) +} + +// 查看详情时同时保存 state(兼容其他场景) +const handleViewDetail = (id: number) => { + navigate(`/approval/${id}`, { state: { fromTab: activeTab } }) +} +``` + +**相关文件**: +- `frontend/src/pages/approval/list.tsx` +- `frontend/src/pages/business-trip/list.tsx` + +--- + +## 26. "我已审批"列表不显示已审批订单 + +**问题**:用户审批完成后,在"我已审批"列表中看不到该订单 + +**根本原因**:架构问题 - 数据模型混淆 + +### 问题分析 + +审批系统中有两个数据来源,它们的职责混淆了: + +1. **`approval_node.approvers`** - 静态模板数据 + - 用途:定义审批流程模板中每个节点的默认审批人配置 + - 问题:运行时不会根据用户实际选择的审批人更新 + +2. **`approval_order.form_data.approvers`** - 运行时数据 + - 用途:存储用户提交申请时实际选择的审批人 + - 正确:这才是判断审批状态应该使用的数据 + +### 历史演变 + +| 时间 | BusinessTripService | ApprovalService | 结果 | +|------|---------------------|-----------------|------| +| 早期 | 使用 `approval_node.approvers` | 使用 `approval_node.approvers` | 一致但错误 | +| 修复后 | 使用 `form_data.approvers` | 使用 `approval_node.approvers` | 不一致!| +| 现在 | 使用 `form_data.approvers` | 使用 `form_data.approvers` | 一致且正确 | + +### 代码修复 + +**ApprovalService::isNodeCompleted()** - 判断节点是否完成 +```php +// ❌ 旧代码:从 approval_node 获取审批人(错误) +$node = ApprovalNode::findOrFail($nodeId); +$approvers = $node->approvers ?? []; // 静态模板数据,可能为空 + +// ✅ 新代码:从 form_data 获取审批人(正确) +$order = ApprovalOrder::find($orderId); +$formData = $order->form_data ?? []; +$approvers = $formData['approvers'] ?? []; // 运行时数据,用户实际选择 +``` + +**ApprovalService::getPendingOrderIds()** - 获取待审批订单 +```php +// ❌ 旧代码 +$node = ApprovalNode::whereIn('id', $currentNodeIds)->first(); +$approvers = $node->approvers ?? []; + +// ✅ 新代码 +$formData = $order->form_data ?? []; +$approvers = $formData['approvers'] ?? []; +``` + +**兼容性处理**: +```php +// 兼容两种格式:[{id:1, name:'...'}] 或 [1] +if (!empty($approvers) && is_array($approvers[0] ?? null) && isset($approvers[0]['id'])) { + $approverIds = array_column($approvers, 'id'); +} else { + $approverIds = $approvers; +} +``` + +**相关文件**:`backend/app/service/ApprovalService.php` + +--- + +## 27. 审批流程不自动跳过通知节点 + +**问题**:审批节点完成后,下一节点是"通知"节点时,流程状态仍为"审批中",`current_nodes` 包含通知节点 ID + +**原因**: +- `ApprovalService::approve()` 没有检查下一节点类型 +- 通知节点(notify)不应该阻止流程完成,应该自动跳过 + +**解决方案**: +```php +// 移动到下一节点 +$nextNodes = $this->getNextNodes($order->flow, $currentNode->order); + +if (empty($nextNodes)) { + // 没有下一节点,审批完成 + $order->status = ApprovalOrder::STATUS_APPROVED; + $order->current_nodes = []; +} else { + // 检查第一个下一节点 + $firstNextNode = $nextNodes[0] ?? null; + if ($firstNextNode && $firstNextNode['type'] === 'notify') { + // 下一个是通知节点,跳过并完成流程 + $order->status = ApprovalOrder::STATUS_APPROVED; + $order->current_nodes = []; + } else { + // 下一个是审批节点,移动到该节点 + $order->current_nodes = collect($nextNodes)->pluck('id')->toArray(); + } +} +``` + +**相关文件**:`backend/app/service/ApprovalService.php` + +--- + +## 28. 架构问题总结 - 为什么问题一直出现 + +### 根本原因:数据模型职责不清晰 + +审批系统长期存在问题的根源是**静态模板数据**和**运行时数据**混淆: + +| 数据源 | 表/字段 | 用途 | 问题 | +|--------|---------|------|------| +| **静态模板** | `approval_node.approvers` | 流程模板中每个节点的默认审批人 | 不应随用户选择变化 | +| **运行时数据** | `approval_order.form_data.approvers` | 用户提交时实际选择的审批人 | 判断审批状态的正确来源 | + +### 为什么会反复出现问题 + +1. **两个服务使用不同数据源** + - `BusinessTripService` 修复后使用 `form_data` + - `ApprovalService` 仍使用 `approval_node` + - 导致相同操作在不同页面结果不一致 + +2. **没有明确的数据访问规范** + - 开发时容易混淆应该读取哪个数据源 + - 缺少代码注释说明数据来源的用途 + +3. **缺少单元测试** + - 数据逻辑变更后没有自动化测试验证 + - 依赖手工测试,容易遗漏边界情况 + +### 长期解决方案 + +1. **代码层面**: + - 在 `ApprovalNode` 模型上添加注释,明确 `approvers` 是模板数据 + - 在 `ApprovalOrder` 模型上添加辅助方法 `getRuntimeApprovers()`,统一访问入口 + - 添加单元测试覆盖所有审批流程场景 + +2. **数据库层面**: + - 添加外键约束确保数据一致性 + - 考虑将 `approval_node.approvers` 重命名为 `default_approvers`,更清晰表达语义 + +3. **文档层面**: + - 在开发文档中明确说明数据模型的设计原则 + - 提供常见问题的排查指南 + +--- + +## 29. 数据库完整性建议 + +### 当前问题 +- `approval_order` 表记录可能被孤立(业务表引用了不存在的 approval_order_id) +- 没有外键约束确保引用完整性 +- 清理数据时可能遗留孤立记录 + +### 建议的修复方案 + +```sql +-- 1. 添加外键约束(确保数据一致性) +ALTER TABLE business_trip +ADD CONSTRAINT fk_business_trip_approval_order +FOREIGN KEY (approval_order_id) REFERENCES approval_order(id) +ON DELETE SET NULL ON UPDATE CASCADE; + +-- 2. 清理孤立数据 +DELETE FROM approval_record +WHERE order_id NOT IN (SELECT id FROM approval_order); + +-- 3. 添加触发器自动维护一致性 +DELIMITER $$ +CREATE TRIGGER before_approval_order_delete +BEFORE DELETE ON approval_order +FOR EACH ROW +BEGIN + UPDATE business_trip + SET approval_order_id = NULL + WHERE approval_order_id = OLD.id; +END$$ +DELIMITER ; +``` + +**相关文件**:`/tmp/fix_proposal.sql` + +--- + +## 最新修改记录(2025-02) + +### 前端文件 +- `frontend/src/pages/approval/list.tsx` - 添加"抄送我的"tab,使用 sessionStorage 持久化 tab 状态 +- `frontend/src/pages/business-trip/list.tsx` - 使用 sessionStorage 持久化 tab 状态 +- `frontend/src/modules/approval/types.ts` - 添加 'cc' 类型到 ApprovalListParams + +### 后端文件 +- `backend/app/service/ApprovalService.php` + - 添加 'cc' case 支持"抄送我的"列表 + - 修复 `isNodeCompleted()` 使用 `form_data.approvers` + - 修复 `getPendingOrderIds()` 使用 `form_data.approvers` + - 添加跳过 notify 节点逻辑 + +### Git 提交记录 +- `e904d5c` - fix: 修复 ApprovalService 使用 form_data.approvers +- `8ce070b` - fix: 使用 sessionStorage 保持 tab 状态 +- `f3a9e44` - fix: 返回列表时保持之前的 tab 状态 +- `ea7f0f8` - feat: 添加"抄送我的"审批列表选项