docs: 更新 PROJECT_ISSUES.md 记录审批相关问题

添加问题记录:
- 问题25:Tab 状态持久化问题
- 问题26:我已审批列表不显示(ApprovalService 数据源错误)
- 问题27:审批流程不自动跳过通知节点
- 问题28:架构问题总结 - 数据模型混淆
- 问题29:数据库完整性建议

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-26 18:47:15 +08:00
parent e904d5cd97
commit ee5f7791de
+243
View File
@@ -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: 添加"抄送我的"审批列表选项