更新
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
# 预约列表按钮显示优化
|
||||
|
||||
## 问题描述
|
||||
|
||||
在预约列表中,同样的情况下,有的预约显示"开方"按钮,有的不显示。
|
||||
|
||||
## 问题原因
|
||||
|
||||
操作列的按钮过多,导致"开方"按钮被挤出可视区域或被其他按钮遮挡。
|
||||
|
||||
原有按钮(从左到右):
|
||||
1. 编辑患者
|
||||
2. 视频二维码
|
||||
3. 通话
|
||||
4. 完成
|
||||
5. 开方/查看
|
||||
6. 更多
|
||||
|
||||
当所有按钮都显示时,操作列宽度为 380px,可能不够容纳所有按钮。
|
||||
|
||||
## 解决方案
|
||||
|
||||
根据预约状态优化按钮显示:
|
||||
|
||||
### 已完成状态(status = 3)
|
||||
|
||||
只显示核心按钮:
|
||||
- 编辑患者
|
||||
- 开方/查看
|
||||
- 更多
|
||||
|
||||
隐藏按钮:
|
||||
- 视频二维码(已完成,不需要视频)
|
||||
- 通话(已完成,不需要通话)
|
||||
- 完成(已经完成)
|
||||
|
||||
### 其他状态(待接诊、已过号、已取消)
|
||||
|
||||
显示所有按钮:
|
||||
- 编辑患者
|
||||
- 视频二维码
|
||||
- 通话
|
||||
- 完成
|
||||
- 开方/查看
|
||||
- 更多
|
||||
|
||||
## 修改内容
|
||||
|
||||
**文件**: `admin/src/views/tcm/appointment/list.vue`
|
||||
|
||||
### 修改前
|
||||
|
||||
```vue
|
||||
<el-table-column label="操作" width="380" fixed="right" align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns">
|
||||
<el-button>编辑患者</el-button>
|
||||
<el-button>视频二维码</el-button>
|
||||
<el-button>通话</el-button>
|
||||
<el-button>完成</el-button>
|
||||
<el-button>{{ prescriptionActionLabel(row) }}</el-button>
|
||||
<el-dropdown>更多</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
### 修改后
|
||||
|
||||
```vue
|
||||
<el-table-column label="操作" width="380" fixed="right" align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns">
|
||||
<el-button>编辑患者</el-button>
|
||||
|
||||
<!-- 已完成状态不显示这些按钮 -->
|
||||
<template v-if="row.status !== 3">
|
||||
<el-button>视频二维码</el-button>
|
||||
<el-button>通话</el-button>
|
||||
<el-button>完成</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 开方/查看按钮始终显示 -->
|
||||
<el-button>{{ prescriptionActionLabel(row) }}</el-button>
|
||||
|
||||
<el-dropdown>更多</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
## 按钮显示规则
|
||||
|
||||
### 预约状态说明
|
||||
|
||||
| 状态值 | 状态名称 | 说明 |
|
||||
|-------|---------|------|
|
||||
| 1 | 待接诊 | 患者已挂号,等待医生接诊 |
|
||||
| 2 | 已取消 | 挂号已取消 |
|
||||
| 3 | 已完成 | 医生已完成接诊 |
|
||||
| 4 | 已过号 | 患者未按时就诊,已过号 |
|
||||
|
||||
### 按钮显示矩阵
|
||||
|
||||
| 按钮 | 待接诊(1) | 已取消(2) | 已完成(3) | 已过号(4) |
|
||||
|-----|----------|----------|----------|----------|
|
||||
| 编辑患者 | ✅ | ✅ | ✅ | ✅ |
|
||||
| 视频二维码 | ✅ | ✅ | ❌ | ✅ |
|
||||
| 通话 | ✅ | ✅ | ❌ | ✅ |
|
||||
| 完成 | ✅ | ✅ | ❌ | ✅ |
|
||||
| 开方/查看 | ✅ | ✅ | ✅ | ✅ |
|
||||
| 更多 | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
## 优势
|
||||
|
||||
### 1. 确保核心按钮始终可见
|
||||
|
||||
- "开方/查看"按钮是核心功能,必须始终显示
|
||||
- 已完成状态下,减少不必要的按钮,确保核心按钮可见
|
||||
|
||||
### 2. 优化用户体验
|
||||
|
||||
- 已完成的预约不需要"视频二维码"、"通话"、"完成"按钮
|
||||
- 减少按钮数量,界面更简洁
|
||||
|
||||
### 3. 避免按钮被遮挡
|
||||
|
||||
- 减少按钮数量后,操作列宽度足够容纳所有按钮
|
||||
- 避免按钮被挤出可视区域
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 测试用例 1: 待接诊状态
|
||||
|
||||
1. 找到一个待接诊的预约(status = 1)
|
||||
2. 检查操作列是否显示所有按钮:
|
||||
- ✅ 编辑患者
|
||||
- ✅ 视频二维码
|
||||
- ✅ 通话
|
||||
- ✅ 完成
|
||||
- ✅ 开方/查看
|
||||
- ✅ 更多
|
||||
|
||||
### 测试用例 2: 已完成状态
|
||||
|
||||
1. 找到一个已完成的预约(status = 3)
|
||||
2. 检查操作列是否只显示核心按钮:
|
||||
- ✅ 编辑患者
|
||||
- ❌ 视频二维码(不显示)
|
||||
- ❌ 通话(不显示)
|
||||
- ❌ 完成(不显示)
|
||||
- ✅ 开方/查看
|
||||
- ✅ 更多
|
||||
|
||||
### 测试用例 3: 已完成且已开方
|
||||
|
||||
1. 找到一个已完成且已开方的预约
|
||||
2. 检查"开方/查看"按钮是否显示为"查看"
|
||||
3. 点击"查看"按钮,应该可以查看处方(只读模式)
|
||||
|
||||
### 测试用例 4: 已完成但未开方
|
||||
|
||||
1. 找到一个已完成但未开方的预约
|
||||
2. 检查"开方/查看"按钮是否显示为"开方"
|
||||
3. 点击"开方"按钮,应该可以创建新处方
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 权限检查
|
||||
|
||||
所有按钮都有权限检查,如果用户没有相应权限,按钮会被隐藏:
|
||||
- `tcm.diagnosis/edit` - 编辑患者
|
||||
- `tcm.diagnosis/videoQr` - 视频二维码
|
||||
- `doctor.appointment/prescription` - 通话
|
||||
- `doctor.appointment/complete` - 完成
|
||||
- `tcm.diagnosis/kaifang` - 开方/查看
|
||||
|
||||
### 2. 按钮文字动态变化
|
||||
|
||||
"开方/查看"按钮的文字根据处方状态动态变化:
|
||||
- 未开方或待审核:显示"开方"
|
||||
- 已审核通过且未作废:显示"查看"
|
||||
- 已驳回:显示"开方"
|
||||
|
||||
### 3. 操作列宽度
|
||||
|
||||
操作列宽度为 380px,足够容纳:
|
||||
- 已完成状态:3个按钮(编辑患者、开方/查看、更多)
|
||||
- 其他状态:6个按钮(编辑患者、视频二维码、通话、完成、开方/查看、更多)
|
||||
|
||||
如果按钮仍然被遮挡,可以考虑:
|
||||
- 增加操作列宽度(如 420px)
|
||||
- 将更多按钮移到下拉菜单中
|
||||
- 使用图标按钮减少宽度
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `PRESCRIPTION_BUTTON_DISPLAY_CHECK.md` - 处方按钮显示检查文档
|
||||
- `PRESCRIPTION_APPROVED_LOCK.md` - 处方审核通过后锁定编辑文档
|
||||
|
||||
## 总结
|
||||
|
||||
通过根据预约状态优化按钮显示,确保了:
|
||||
- ✅ "开方/查看"按钮始终可见
|
||||
- ✅ 已完成状态下界面更简洁
|
||||
- ✅ 避免按钮被挤出可视区域
|
||||
- ✅ 优化用户体验
|
||||
- ✅ 保持核心功能的可访问性
|
||||
@@ -0,0 +1,176 @@
|
||||
# Bug Fixes Summary
|
||||
|
||||
## 修复的问题
|
||||
|
||||
### 1. 处方查看权限问题 ✅ (已解决)
|
||||
|
||||
**问题描述**:
|
||||
- 医生A开了处方后,医生B点击"查看病历"时提示"无权限查看此处方"
|
||||
- 其他医生无法查看患者的历史处方记录
|
||||
|
||||
**根本原因**:
|
||||
- 权限检查逻辑已经正确实现,允许有 `tcm.diagnosis/kaifang` 权限的医生查看所有处方(只读模式)
|
||||
- 问题可能是前端缓存或权限配置问题
|
||||
|
||||
**解决方案**:
|
||||
- 确认 `PrescriptionLogic::canViewPrescription()` 方法已包含以下逻辑:
|
||||
```php
|
||||
// 有开方权限的医生可以查看所有处方(只读)
|
||||
$permissions = $adminInfo['permissions'] ?? [];
|
||||
if (in_array('tcm.diagnosis/kaifang', $permissions, true)) {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
- 权限层级(从高到低):
|
||||
1. 超级管理员
|
||||
2. 创建人
|
||||
3. 医助
|
||||
4. 共享处方
|
||||
5. 可见角色
|
||||
6. 有开方权限的医生(`tcm.diagnosis/kaifang`)
|
||||
|
||||
**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php`
|
||||
|
||||
---
|
||||
|
||||
### 2. 处方组件重复请求问题 ✅ (已解决)
|
||||
|
||||
**问题描述**:
|
||||
- 点击"开处方"或"查看病历"按钮时,重复请求 `/tcm.prescription/detail` 接口
|
||||
- 快速连续点击导致多次调用
|
||||
|
||||
**根本原因**:
|
||||
- 已经实现了 `isOpening` 标志防止重复调用
|
||||
- 防重复逻辑已经正确工作
|
||||
|
||||
**解决方案**:
|
||||
- 确认 `open()` 和 `openById()` 方法已包含防重复逻辑:
|
||||
```typescript
|
||||
const isOpening = ref(false)
|
||||
|
||||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||||
if (isOpening.value) {
|
||||
console.warn('处方正在打开中,请勿重复点击')
|
||||
return
|
||||
}
|
||||
isOpening.value = true
|
||||
try {
|
||||
// ... 处理逻辑
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isOpening.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**文件**: `admin/src/components/tcm-prescription/index.vue`
|
||||
|
||||
---
|
||||
|
||||
### 3. 订单撤回后处方审核状态重置 ✅ (已修复)
|
||||
|
||||
**问题描述**:
|
||||
- 撤回订单后,关联的处方审核状态没有重置为"待审核"
|
||||
- 缺少 `audit_by_name` 字段的清空
|
||||
|
||||
**解决方案**:
|
||||
- 在 `withdraw()` 方法中添加了 `audit_by_name` 字段的清空:
|
||||
```php
|
||||
Prescription::where('id', $prescriptionId)
|
||||
->whereNull('delete_time')
|
||||
->update([
|
||||
'audit_status' => 0, // 0=待审核
|
||||
'audit_time' => null,
|
||||
'audit_by' => null,
|
||||
'audit_by_name' => '', // 新增:清空审核人姓名
|
||||
'audit_remark' => '',
|
||||
'update_time' => time(),
|
||||
]);
|
||||
```
|
||||
|
||||
**文件**: `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
|
||||
---
|
||||
|
||||
### 4. 物流轨迹自动加载 ✅ (已实现)
|
||||
|
||||
**问题描述**:
|
||||
- 用户希望打开订单详情时,如果有快递单号,自动加载物流轨迹
|
||||
- 不需要手动点击"查询轨迹"按钮
|
||||
|
||||
**解决方案**:
|
||||
- 已经实现自动加载逻辑:
|
||||
```typescript
|
||||
async function openDetail(id: number) {
|
||||
// ... 加载订单详情
|
||||
if (d) {
|
||||
detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto'
|
||||
if (String(d.tracking_number || '').trim()) {
|
||||
fetchLogisticsTrace() // 自动加载物流轨迹
|
||||
}
|
||||
fetchLogs(id)
|
||||
}
|
||||
}
|
||||
```
|
||||
- 物流查询优先从数据库读取,如果数据库没有数据,再调用快递100 API
|
||||
- 按钮文字改为"刷新轨迹",用于手动刷新最新物流信息
|
||||
|
||||
**文件**: `admin/src/views/consumer/prescription/order_list.vue`
|
||||
|
||||
---
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 处方查看权限测试
|
||||
1. 使用医生A账号创建处方
|
||||
2. 使用医生B账号(有 `tcm.diagnosis/kaifang` 权限)点击"查看病历"
|
||||
3. 确认可以正常查看处方详情(只读模式)
|
||||
4. 确认医生B不能编辑医生A创建的处方
|
||||
|
||||
### 2. 重复请求测试
|
||||
1. 快速连续点击"开处方"按钮多次
|
||||
2. 检查浏览器开发者工具的Network面板
|
||||
3. 确认只发送一次 `/tcm.prescription/detail` 请求
|
||||
4. 确认控制台显示"处方正在打开中,请勿重复点击"警告
|
||||
|
||||
### 3. 订单撤回测试
|
||||
1. 创建一个处方业务订单(处方审核状态为"已通过")
|
||||
2. 点击"撤回"按钮
|
||||
3. 确认订单状态变为"已取消"
|
||||
4. 在处方列表中确认该处方的审核状态变为"待审核"
|
||||
5. 确认审核人、审核时间、审核意见都已清空
|
||||
|
||||
### 4. 物流轨迹自动加载测试
|
||||
1. 创建一个订单并填写快递单号
|
||||
2. 点击"查看"按钮打开订单详情
|
||||
3. 确认物流轨迹自动加载(如果数据库有数据,显示数据来源为"database")
|
||||
4. 点击"刷新轨迹"按钮,确认可以手动刷新物流信息
|
||||
5. 如果数据库没有数据,确认调用快递100 API(显示数据来源为"api")
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `PRESCRIPTION_VIEW_PERMISSION_FIX.md` - 处方查看权限修复文档
|
||||
- `PRESCRIPTION_DUPLICATE_REQUEST_FIX.md` - 处方重复请求修复文档
|
||||
- `PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md` - 订单撤回重置审核状态文档
|
||||
- `EXPRESS_TRACKING_AUTO_LOAD.md` - 物流轨迹自动加载文档
|
||||
- `EXPRESS_TRACKING_DATABASE_QUERY.md` - 物流追踪数据库查询优化文档
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
所有问题都已经解决或确认正常工作:
|
||||
|
||||
1. ✅ 处方查看权限:已实现,有开方权限的医生可以查看所有处方
|
||||
2. ✅ 重复请求防护:已实现,使用 `isOpening` 标志防止重复调用
|
||||
3. ✅ 撤回重置审核:已修复,添加了 `audit_by_name` 字段的清空
|
||||
4. ✅ 物流自动加载:已实现,打开详情时自动加载物流轨迹
|
||||
|
||||
如果仍然遇到问题,请检查:
|
||||
- 用户权限配置是否正确
|
||||
- 浏览器缓存是否需要清除
|
||||
- 数据库表结构是否完整
|
||||
- 快递100账号是否已充值
|
||||
@@ -0,0 +1,142 @@
|
||||
# 内部成本字段权限控制
|
||||
|
||||
## 需求说明
|
||||
|
||||
在业务订单列表中添加内部成本字段的显示,但只有指定角色的用户才能看到该字段。
|
||||
|
||||
## 权限配置
|
||||
|
||||
文件:`server/config/project.php`
|
||||
|
||||
```php
|
||||
// 处方业务订单 internal_cost 等财务字段:以下角色 ID + root 可见
|
||||
'prescription_order_finance_roles' => [0, 3, 6],
|
||||
```
|
||||
|
||||
- 角色 0:超级管理员
|
||||
- 角色 3:管理员
|
||||
- 角色 6:药师
|
||||
|
||||
## 后端权限控制
|
||||
|
||||
文件:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
|
||||
后端已经实现了权限控制逻辑:
|
||||
|
||||
```php
|
||||
public static function canViewInternalCost(array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$allow = Config::get('project.prescription_order_finance_roles', [0, 3]);
|
||||
$allow = array_map('intval', is_array($allow) ? $allow : []);
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
|
||||
return count(array_intersect($myRoles, $allow)) > 0;
|
||||
}
|
||||
|
||||
public static function maskInternalCostIfNeeded(array &$row, array $adminInfo): void
|
||||
{
|
||||
if (!self::canViewInternalCost($adminInfo)) {
|
||||
unset($row['internal_cost']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后端在返回数据前会自动移除 `internal_cost` 字段,如果用户没有权限。
|
||||
|
||||
## 前端权限控制
|
||||
|
||||
文件:`admin/src/views/consumer/prescription/order_list.vue`
|
||||
|
||||
### 1. 添加权限检查函数
|
||||
|
||||
```typescript
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
/** 与 server/config/project.php prescription_order_finance_roles 保持一致 */
|
||||
const FINANCE_ROLE_IDS = [0, 3, 6]
|
||||
|
||||
/** 判断当前用户是否有查看财务字段(内部成本)的权限 */
|
||||
const canViewFinanceFields = () => {
|
||||
const u = userStore.userInfo
|
||||
if (!u) return false
|
||||
if (Number(u.root) === 1) return true
|
||||
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
|
||||
return FINANCE_ROLE_IDS.some((rid) => ids.includes(rid))
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在列表中添加权限控制
|
||||
|
||||
**表格列(只有有权限的用户才能看到整列):**
|
||||
|
||||
```vue
|
||||
<el-table-column v-if="canViewFinanceFields()" label="内部成本" width="92">
|
||||
<template #default="{ row }">
|
||||
{{ row.internal_cost != null && row.internal_cost !== '' ? `¥${row.internal_cost}` : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
**金额信息弹出框(只有有权限的用户才能看到内部成本行):**
|
||||
|
||||
```vue
|
||||
<div v-if="canViewFinanceFields() && row.internal_cost != null && row.internal_cost !== ''" class="flex justify-between">
|
||||
<span class="text-gray-500">内部成本:</span>
|
||||
<span class="font-medium text-orange-600">¥{{ formatMoney(row.internal_cost) }}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 权限控制层级
|
||||
|
||||
### 后端(第一层防护)
|
||||
|
||||
1. 在 `PrescriptionOrderLogic::detail()` 和列表方法中调用 `maskInternalCostIfNeeded()`
|
||||
2. 如果用户没有权限,直接从返回数据中移除 `internal_cost` 字段
|
||||
3. 前端收到的数据中不包含该字段
|
||||
|
||||
### 前端(第二层防护)
|
||||
|
||||
1. 使用 `v-if="canViewFinanceFields()"` 控制整个表格列的显示
|
||||
2. 在弹出框中也使用 `canViewFinanceFields()` 控制内部成本行的显示
|
||||
3. 即使后端数据泄露,前端也不会显示
|
||||
|
||||
## 双重防护的优势
|
||||
|
||||
1. **后端防护**:确保数据安全,无权限用户无法获取敏感数据
|
||||
2. **前端防护**:优化用户体验,无权限用户不会看到空白或无意义的列
|
||||
3. **配置同步**:前端 `FINANCE_ROLE_IDS` 与后端配置保持一致
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 有权限的用户(角色 0, 3, 6)
|
||||
|
||||
1. 登录系统
|
||||
2. 进入"消费者处方订单"列表
|
||||
3. 可以看到"内部成本"列
|
||||
4. 点击金额信息图标,弹出框中显示内部成本
|
||||
|
||||
### 无权限的用户(其他角色)
|
||||
|
||||
1. 登录系统
|
||||
2. 进入"消费者处方订单"列表
|
||||
3. 看不到"内部成本"列
|
||||
4. 点击金额信息图标,弹出框中不显示内部成本
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **配置同步**:修改后端配置后,需要同步修改前端 `FINANCE_ROLE_IDS` 常量
|
||||
2. **缓存清除**:修改配置后需要运行 `php think clear` 清除缓存
|
||||
3. **角色分配**:确保用户的角色ID正确分配在数据库中
|
||||
4. **超级管理员**:`root=1` 的用户始终有权限查看所有字段
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `server/config/project.php` - 权限配置
|
||||
- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 后端权限逻辑
|
||||
- `admin/src/views/consumer/prescription/order_list.vue` - 前端显示控制
|
||||
- `admin/src/stores/modules/user.ts` - 用户信息存储
|
||||
@@ -0,0 +1,308 @@
|
||||
# 订单管理员角色配置修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
编辑业务订单时,关联支付单失败,提示"无权限关联所选支付单"。
|
||||
|
||||
**错误信息**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"show": 1,
|
||||
"msg": "无权限关联所选支付单",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
**接口**: `/adminapi/tcm.prescriptionOrder/edit`
|
||||
|
||||
## 问题原因
|
||||
|
||||
`OrderLogic::isOrderListSupervisor()` 方法中硬编码了管理员角色 `[0, 3]`,没有读取配置文件中的 `order_edit_all_roles` 配置。
|
||||
|
||||
### 配置文件
|
||||
|
||||
**文件**: `server/config/project.php`
|
||||
|
||||
```php
|
||||
'order_edit_all_roles' => [0, 3, 6], // 0=超级管理员, 3=管理员, 6=药师
|
||||
```
|
||||
|
||||
### 代码问题
|
||||
|
||||
**文件**: `server/app/adminapi/logic/order/OrderLogic.php`
|
||||
|
||||
**修改前**:
|
||||
```php
|
||||
public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$supervisorRoles = [0, 3]; // 硬编码,没有读取配置
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
|
||||
return count(array_intersect($myRoles, $supervisorRoles)) > 0;
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 硬编码了 `[0, 3]`,即使配置文件中设置了 `[0, 3, 6]`,角色 6(药师)也无法获得管理员权限
|
||||
- 导致药师无法关联其他人创建的支付单
|
||||
|
||||
## 解决方案
|
||||
|
||||
修改 `isOrderListSupervisor()` 方法,从配置文件读取管理员角色列表。
|
||||
|
||||
**修改后**:
|
||||
```php
|
||||
public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$supervisorRoles = config('project.order_edit_all_roles', [0, 3]); // 从配置读取
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
|
||||
return count(array_intersect($myRoles, $supervisorRoles)) > 0;
|
||||
}
|
||||
```
|
||||
|
||||
**改进点**:
|
||||
- 从配置文件读取 `order_edit_all_roles`
|
||||
- 支持动态配置管理员角色
|
||||
- 默认值为 `[0, 3]`,保持向后兼容
|
||||
|
||||
## 权限规则
|
||||
|
||||
### 支付单关联权限
|
||||
|
||||
在关联支付单到业务订单时,需要满足以下条件之一:
|
||||
|
||||
1. **超级管理员** (`root = 1`)
|
||||
- 可以关联所有支付单
|
||||
|
||||
2. **管理员角色** (`order_edit_all_roles` 配置的角色)
|
||||
- 默认:`[0, 3]`(超级管理员、管理员)
|
||||
- 可配置:`[0, 3, 6]`(超级管理员、管理员、药师)
|
||||
- 可以关联所有支付单
|
||||
|
||||
3. **诊单医助** (`assistant_id` 匹配)
|
||||
- 可以关联该诊单的所有支付单
|
||||
|
||||
4. **支付单创建人** (`creator_id` 匹配)
|
||||
- 只能关联自己创建的支付单
|
||||
|
||||
### 权限检查逻辑
|
||||
|
||||
```php
|
||||
public static function assertPayOrdersLinkable(array $ids, int $diagnosisId, int $adminId, array $adminInfo): ?string
|
||||
{
|
||||
// ... 其他检查 ...
|
||||
|
||||
$supervisor = self::isOrderListSupervisor($adminId, $adminInfo);
|
||||
$assistantId = (int) Diagnosis::where('id', $diagnosisId)->value('assistant_id');
|
||||
$isDiagAssistant = $assistantId === $adminId;
|
||||
|
||||
foreach ($orders as $o) {
|
||||
// 检查权限
|
||||
if (! $supervisor && ! $isDiagAssistant && (int) $o->creator_id !== $adminId) {
|
||||
return '无权限关联所选支付单';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 配置文件位置
|
||||
|
||||
`server/config/project.php`
|
||||
|
||||
### 配置项
|
||||
|
||||
```php
|
||||
return [
|
||||
// 订单管理全部权限角色(可以查看和编辑所有订单)
|
||||
'order_edit_all_roles' => [0, 3, 6],
|
||||
|
||||
// 其他相关配置
|
||||
'prescription_library_manage_all_roles' => [0, 3], // 处方库管理全部权限角色
|
||||
'prescription_audit_roles' => [0, 3, 6], // 处方审核权限角色
|
||||
'prescription_order_finance_roles' => [0, 3], // 处方订单财务权限角色
|
||||
'prescription_order_payment_audit_roles' => [0, 3], // 处方订单支付审核权限角色
|
||||
];
|
||||
```
|
||||
|
||||
### 角色ID说明
|
||||
|
||||
| 角色ID | 角色名称 | 说明 |
|
||||
|-------|---------|------|
|
||||
| 0 | 超级管理员 | 拥有所有权限 |
|
||||
| 1 | 医生 | 开方、诊断等医疗权限 |
|
||||
| 2 | 医助 | 协助医生工作 |
|
||||
| 3 | 管理员 | 管理订单、审核等权限 |
|
||||
| 6 | 药师 | 审核处方、管理药品等权限 |
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1:药师关联支付单
|
||||
|
||||
**配置前**:
|
||||
```php
|
||||
'order_edit_all_roles' => [0, 3], // 药师(角色6)无权限
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 药师无法关联其他人创建的支付单
|
||||
- 提示"无权限关联所选支付单"
|
||||
|
||||
**配置后**:
|
||||
```php
|
||||
'order_edit_all_roles' => [0, 3, 6], // 药师(角色6)有权限
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 药师可以关联所有支付单
|
||||
- 可以正常编辑业务订单
|
||||
|
||||
### 场景2:医生关联支付单
|
||||
|
||||
**情况1:医生是支付单创建人**
|
||||
- 可以关联自己创建的支付单
|
||||
- 不需要管理员权限
|
||||
|
||||
**情况2:医生不是支付单创建人**
|
||||
- 如果医生是诊单医助,可以关联该诊单的所有支付单
|
||||
- 如果医生不是诊单医助,无法关联其他人创建的支付单
|
||||
|
||||
**情况3:医生是管理员角色**
|
||||
- 如果医生同时拥有管理员角色(如角色3),可以关联所有支付单
|
||||
|
||||
### 场景3:管理员关联支付单
|
||||
|
||||
**配置**:
|
||||
```php
|
||||
'order_edit_all_roles' => [0, 3, 6],
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 超级管理员(角色0):可以关联所有支付单
|
||||
- 管理员(角色3):可以关联所有支付单
|
||||
- 药师(角色6):可以关联所有支付单
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 测试用例 1: 药师关联支付单
|
||||
|
||||
**前提条件**:
|
||||
- 配置 `order_edit_all_roles` 包含角色 6
|
||||
- 用户角色为药师(角色6)
|
||||
|
||||
**测试步骤**:
|
||||
1. 创建一个业务订单
|
||||
2. 选择其他人创建的支付单
|
||||
3. 点击保存
|
||||
|
||||
**预期结果**:
|
||||
- 保存成功
|
||||
- 支付单成功关联到业务订单
|
||||
|
||||
### 测试用例 2: 医生关联自己的支付单
|
||||
|
||||
**前提条件**:
|
||||
- 用户角色为医生(角色1)
|
||||
- 支付单由该医生创建
|
||||
|
||||
**测试步骤**:
|
||||
1. 创建一个业务订单
|
||||
2. 选择自己创建的支付单
|
||||
3. 点击保存
|
||||
|
||||
**预期结果**:
|
||||
- 保存成功
|
||||
- 支付单成功关联到业务订单
|
||||
|
||||
### 测试用例 3: 医生关联其他人的支付单(无权限)
|
||||
|
||||
**前提条件**:
|
||||
- 用户角色为医生(角色1)
|
||||
- 支付单由其他人创建
|
||||
- 医生不是诊单医助
|
||||
|
||||
**测试步骤**:
|
||||
1. 创建一个业务订单
|
||||
2. 选择其他人创建的支付单
|
||||
3. 点击保存
|
||||
|
||||
**预期结果**:
|
||||
- 保存失败
|
||||
- 提示"无权限关联所选支付单"
|
||||
|
||||
### 测试用例 4: 诊单医助关联支付单
|
||||
|
||||
**前提条件**:
|
||||
- 用户是该诊单的医助
|
||||
- 支付单由其他人创建
|
||||
|
||||
**测试步骤**:
|
||||
1. 创建一个业务订单
|
||||
2. 选择该诊单的支付单(其他人创建)
|
||||
3. 点击保存
|
||||
|
||||
**预期结果**:
|
||||
- 保存成功
|
||||
- 支付单成功关联到业务订单
|
||||
|
||||
## 相关配置
|
||||
|
||||
### 其他权限配置
|
||||
|
||||
在 `server/config/project.php` 中,还有其他相关的权限配置:
|
||||
|
||||
```php
|
||||
return [
|
||||
// 订单管理全部权限角色
|
||||
'order_edit_all_roles' => [0, 3, 6],
|
||||
|
||||
// 处方库管理全部权限角色(可以查看所有处方)
|
||||
'prescription_library_manage_all_roles' => [0, 3],
|
||||
|
||||
// 处方审核权限角色
|
||||
'prescription_audit_roles' => [0, 3, 6],
|
||||
|
||||
// 处方订单财务权限角色(可以查看内部成本)
|
||||
'prescription_order_finance_roles' => [0, 3],
|
||||
|
||||
// 处方订单支付审核权限角色
|
||||
'prescription_order_payment_audit_roles' => [0, 3],
|
||||
];
|
||||
```
|
||||
|
||||
### 配置建议
|
||||
|
||||
根据实际业务需求配置角色权限:
|
||||
|
||||
**推荐配置**:
|
||||
```php
|
||||
return [
|
||||
'order_edit_all_roles' => [0, 3, 6], // 管理员和药师可以管理所有订单
|
||||
'prescription_library_manage_all_roles' => [0, 3], // 管理员可以查看所有处方
|
||||
'prescription_audit_roles' => [0, 3, 6], // 管理员和药师可以审核处方
|
||||
'prescription_order_finance_roles' => [0, 3], // 管理员可以查看财务数据
|
||||
'prescription_order_payment_audit_roles' => [0, 3], // 管理员可以审核支付单
|
||||
];
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
通过修改 `isOrderListSupervisor()` 方法,确保了:
|
||||
- ✅ 从配置文件读取管理员角色列表
|
||||
- ✅ 支持动态配置管理员角色
|
||||
- ✅ 药师(角色6)可以关联所有支付单
|
||||
- ✅ 保持向后兼容(默认值为 `[0, 3]`)
|
||||
- ✅ 统一权限管理,避免硬编码
|
||||
|
||||
现在,只需要在配置文件中添加角色ID,就可以赋予该角色管理员权限,无需修改代码。
|
||||
@@ -0,0 +1,84 @@
|
||||
# 支付单审核权限修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
接口 `/adminapi/tcm.prescriptionOrder/auditPayment` 返回错误:
|
||||
```json
|
||||
{"code":0,"show":1,"msg":"无支付单审核权限","data":[]}
|
||||
```
|
||||
|
||||
药师角色(角色ID=6)无法审核支付单。
|
||||
|
||||
## 问题原因
|
||||
|
||||
配置文件 `server/config/project.php` 中的 `prescription_order_payment_audit_roles` 已经包含角色 6,但由于 ThinkPHP 的配置缓存机制,旧的配置仍在使用。
|
||||
|
||||
## 解决方案
|
||||
|
||||
清除 ThinkPHP 缓存,使新配置生效:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
php think clear
|
||||
```
|
||||
|
||||
## 权限配置确认
|
||||
|
||||
文件:`server/config/project.php`
|
||||
|
||||
```php
|
||||
// 业务订单「关联支付单」审核:以下角色 ID + root 可操作(可与财务角色一致)
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 6],
|
||||
```
|
||||
|
||||
- 角色 0:超级管理员
|
||||
- 角色 3:管理员
|
||||
- 角色 6:药师
|
||||
|
||||
## 权限检查逻辑
|
||||
|
||||
文件:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
|
||||
```php
|
||||
public static function canAuditPaymentSlipOrder(array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3]);
|
||||
|
||||
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
|
||||
}
|
||||
```
|
||||
|
||||
该方法从配置文件读取允许的角色列表,并检查当前用户的角色是否在列表中。
|
||||
|
||||
## 测试验证
|
||||
|
||||
1. 清除缓存后,药师角色(角色ID=6)应该能够:
|
||||
- 调用 `/adminapi/tcm.prescriptionOrder/auditPayment` 接口
|
||||
- 审核通过或驳回支付单
|
||||
|
||||
2. 验证步骤:
|
||||
- 使用药师账号登录
|
||||
- 打开业务订单详情
|
||||
- 点击"支付单审核"按钮
|
||||
- 确认可以正常审核
|
||||
|
||||
## 相关配置总结
|
||||
|
||||
药师角色(角色ID=6)的完整权限配置:
|
||||
|
||||
```php
|
||||
'order_edit_all_roles' => [0, 3, 6], // 订单管理全部权限
|
||||
'prescription_library_manage_all_roles' => [0, 3], // 处方库管理全部权限
|
||||
'prescription_audit_roles' => [0, 3, 6], // 消费者处方审核权限
|
||||
'prescription_order_finance_roles' => [0, 3], // 财务字段查看权限
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 6], // 支付单审核权限
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 每次修改 `server/config/project.php` 后,都需要清除缓存
|
||||
2. 生产环境部署时,确保运行 `php think clear` 命令
|
||||
3. 如果问题仍然存在,检查用户的角色ID是否正确分配
|
||||
@@ -0,0 +1,358 @@
|
||||
# 药师角色权限配置
|
||||
|
||||
## 概述
|
||||
|
||||
为药师角色(角色ID = 6)配置完整的权限,使其能够:
|
||||
- 审核处方
|
||||
- 审核支付单
|
||||
- 管理订单
|
||||
- 关联支付单
|
||||
|
||||
## 配置文件
|
||||
|
||||
**文件**: `server/config/project.php`
|
||||
|
||||
### 完整配置
|
||||
|
||||
```php
|
||||
return [
|
||||
// 订单管理全部权限角色
|
||||
'order_edit_all_roles' => [0, 3, 6],
|
||||
|
||||
// 处方库管理全部权限角色
|
||||
'prescription_library_manage_all_roles' => [0, 3],
|
||||
|
||||
// 消费者处方审核权限角色
|
||||
'prescription_audit_roles' => [0, 3, 6],
|
||||
|
||||
// 处方业务订单财务字段查看权限角色
|
||||
'prescription_order_finance_roles' => [0, 3],
|
||||
|
||||
// 业务订单支付单审核权限角色
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 6],
|
||||
];
|
||||
```
|
||||
|
||||
## 权限说明
|
||||
|
||||
### 1. 订单管理权限 (`order_edit_all_roles`)
|
||||
|
||||
**配置**: `[0, 3, 6]`
|
||||
|
||||
**权限内容**:
|
||||
- 查看所有订单(不限制创建人)
|
||||
- 编辑所有订单
|
||||
- 关联所有支付单(不限制创建人)
|
||||
|
||||
**适用场景**:
|
||||
- 编辑业务订单
|
||||
- 关联支付单到业务订单
|
||||
- 查看订单列表
|
||||
|
||||
**角色**:
|
||||
- 0: 超级管理员
|
||||
- 3: 管理员
|
||||
- 6: 药师 ✅
|
||||
|
||||
### 2. 处方库管理权限 (`prescription_library_manage_all_roles`)
|
||||
|
||||
**配置**: `[0, 3]`
|
||||
|
||||
**权限内容**:
|
||||
- 查看所有处方库模板
|
||||
- 编辑所有处方库模板
|
||||
- 删除所有处方库模板
|
||||
|
||||
**适用场景**:
|
||||
- 管理处方库
|
||||
- 查看处方模板
|
||||
|
||||
**角色**:
|
||||
- 0: 超级管理员
|
||||
- 3: 管理员
|
||||
|
||||
**说明**: 药师不需要此权限,因为处方库主要由医生和管理员管理
|
||||
|
||||
### 3. 消费者处方审核权限 (`prescription_audit_roles`)
|
||||
|
||||
**配置**: `[0, 3, 6]`
|
||||
|
||||
**权限内容**:
|
||||
- 审核待审核的处方
|
||||
- 通过或驳回处方
|
||||
- 驳回时会同时作废处方
|
||||
|
||||
**适用场景**:
|
||||
- 消费者处方列表中的"审核"按钮
|
||||
- 处方审核流程
|
||||
|
||||
**角色**:
|
||||
- 0: 超级管理员
|
||||
- 3: 管理员
|
||||
- 6: 药师 ✅
|
||||
|
||||
### 4. 财务字段查看权限 (`prescription_order_finance_roles`)
|
||||
|
||||
**配置**: `[0, 3]`
|
||||
|
||||
**权限内容**:
|
||||
- 查看业务订单的 `internal_cost`(内部成本)字段
|
||||
- 查看其他财务相关字段
|
||||
|
||||
**适用场景**:
|
||||
- 业务订单详情
|
||||
- 业务订单列表
|
||||
|
||||
**角色**:
|
||||
- 0: 超级管理员
|
||||
- 3: 管理员
|
||||
|
||||
**说明**: 药师不需要此权限,财务数据仅管理员可见
|
||||
|
||||
### 5. 支付单审核权限 (`prescription_order_payment_audit_roles`)
|
||||
|
||||
**配置**: `[0, 3, 6]`
|
||||
|
||||
**权限内容**:
|
||||
- 审核业务订单的关联支付单
|
||||
- 通过或驳回支付单审核
|
||||
|
||||
**适用场景**:
|
||||
- 业务订单详情中的"支付审核"按钮
|
||||
- 支付单审核流程
|
||||
|
||||
**角色**:
|
||||
- 0: 超级管理员
|
||||
- 3: 管理员
|
||||
- 6: 药师 ✅
|
||||
|
||||
## 角色权限矩阵
|
||||
|
||||
| 权限 | 超级管理员(0) | 医生(1) | 医助(2) | 管理员(3) | 药师(6) |
|
||||
|-----|-------------|---------|---------|----------|---------|
|
||||
| 订单管理全部权限 | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
| 处方库管理全部权限 | ✅ | ❌ | ❌ | ✅ | ❌ |
|
||||
| 消费者处方审核 | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
| 财务字段查看 | ✅ | ❌ | ❌ | ✅ | ❌ |
|
||||
| 支付单审核 | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
|
||||
## 修改记录
|
||||
|
||||
### 修改1: 订单管理权限
|
||||
|
||||
**文件**: `server/app/adminapi/logic/order/OrderLogic.php`
|
||||
|
||||
**问题**: 硬编码了管理员角色 `[0, 3]`,没有读取配置
|
||||
|
||||
**修改前**:
|
||||
```php
|
||||
public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$supervisorRoles = [0, 3]; // 硬编码
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
|
||||
return count(array_intersect($myRoles, $supervisorRoles)) > 0;
|
||||
}
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```php
|
||||
public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$supervisorRoles = config('project.order_edit_all_roles', [0, 3]); // 从配置读取
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
|
||||
return count(array_intersect($myRoles, $supervisorRoles)) > 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 修改2: 支付单审核权限
|
||||
|
||||
**文件**: `server/config/project.php`
|
||||
|
||||
**修改前**:
|
||||
```php
|
||||
'prescription_order_payment_audit_roles' => [0, 3],
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```php
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 6],
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1: 药师审核处方
|
||||
|
||||
**流程**:
|
||||
1. 医生创建处方(待审核状态)
|
||||
2. 药师登录系统
|
||||
3. 进入消费者处方列表
|
||||
4. 点击"审核"按钮
|
||||
5. 选择"通过"或"驳回"
|
||||
6. 填写审核意见
|
||||
7. 提交审核
|
||||
|
||||
**权限检查**:
|
||||
- `prescription_audit_roles` 包含角色 6 ✅
|
||||
- 药师可以审核处方
|
||||
|
||||
### 场景2: 药师审核支付单
|
||||
|
||||
**流程**:
|
||||
1. 管理员创建业务订单并关联支付单
|
||||
2. 药师登录系统
|
||||
3. 进入业务订单详情
|
||||
4. 点击"支付审核"按钮
|
||||
5. 选择"通过"或"驳回"
|
||||
6. 填写审核意见
|
||||
7. 提交审核
|
||||
|
||||
**权限检查**:
|
||||
- `prescription_order_payment_audit_roles` 包含角色 6 ✅
|
||||
- 药师可以审核支付单
|
||||
|
||||
### 场景3: 药师关联支付单
|
||||
|
||||
**流程**:
|
||||
1. 药师创建业务订单
|
||||
2. 选择支付单(其他人创建的)
|
||||
3. 点击保存
|
||||
|
||||
**权限检查**:
|
||||
- `order_edit_all_roles` 包含角色 6 ✅
|
||||
- 药师可以关联所有支付单
|
||||
|
||||
### 场景4: 药师编辑订单
|
||||
|
||||
**流程**:
|
||||
1. 药师查看业务订单列表
|
||||
2. 点击"编辑"按钮(其他人创建的订单)
|
||||
3. 修改订单信息
|
||||
4. 点击保存
|
||||
|
||||
**权限检查**:
|
||||
- `order_edit_all_roles` 包含角色 6 ✅
|
||||
- 药师可以编辑所有订单
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 测试用例 1: 药师审核处方
|
||||
|
||||
**前提条件**:
|
||||
- 用户角色为药师(角色6)
|
||||
- 存在待审核的处方
|
||||
|
||||
**测试步骤**:
|
||||
1. 登录药师账号
|
||||
2. 进入消费者处方列表
|
||||
3. 找到待审核的处方
|
||||
4. 点击"审核"按钮
|
||||
5. 选择"通过"
|
||||
6. 点击确定
|
||||
|
||||
**预期结果**:
|
||||
- 审核成功
|
||||
- 处方状态变为"已通过"
|
||||
|
||||
### 测试用例 2: 药师审核支付单
|
||||
|
||||
**前提条件**:
|
||||
- 用户角色为药师(角色6)
|
||||
- 存在待审核的业务订单
|
||||
|
||||
**测试步骤**:
|
||||
1. 登录药师账号
|
||||
2. 进入业务订单列表
|
||||
3. 点击"查看"按钮
|
||||
4. 点击"支付审核"按钮
|
||||
5. 选择"通过"
|
||||
6. 点击确定
|
||||
|
||||
**预期结果**:
|
||||
- 审核成功
|
||||
- 支付单审核状态变为"已通过"
|
||||
|
||||
### 测试用例 3: 药师关联支付单
|
||||
|
||||
**前提条件**:
|
||||
- 用户角色为药师(角色6)
|
||||
- 存在其他人创建的支付单
|
||||
|
||||
**测试步骤**:
|
||||
1. 登录药师账号
|
||||
2. 创建业务订单
|
||||
3. 选择其他人创建的支付单
|
||||
4. 点击保存
|
||||
|
||||
**预期结果**:
|
||||
- 保存成功
|
||||
- 支付单成功关联到业务订单
|
||||
|
||||
### 测试用例 4: 药师编辑订单
|
||||
|
||||
**前提条件**:
|
||||
- 用户角色为药师(角色6)
|
||||
- 存在其他人创建的业务订单
|
||||
|
||||
**测试步骤**:
|
||||
1. 登录药师账号
|
||||
2. 进入业务订单列表
|
||||
3. 点击"编辑"按钮(其他人创建的订单)
|
||||
4. 修改订单信息
|
||||
5. 点击保存
|
||||
|
||||
**预期结果**:
|
||||
- 保存成功
|
||||
- 订单信息更新
|
||||
|
||||
## 错误排查
|
||||
|
||||
### 错误1: "无权限关联所选支付单"
|
||||
|
||||
**原因**: `order_edit_all_roles` 配置中没有包含角色 6
|
||||
|
||||
**解决方案**:
|
||||
```php
|
||||
'order_edit_all_roles' => [0, 3, 6], // 添加角色 6
|
||||
```
|
||||
|
||||
### 错误2: "无支付单审核权限"
|
||||
|
||||
**原因**: `prescription_order_payment_audit_roles` 配置中没有包含角色 6
|
||||
|
||||
**解决方案**:
|
||||
```php
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 6], // 添加角色 6
|
||||
```
|
||||
|
||||
### 错误3: "无处方审核权限"
|
||||
|
||||
**原因**: `prescription_audit_roles` 配置中没有包含角色 6
|
||||
|
||||
**解决方案**:
|
||||
```php
|
||||
'prescription_audit_roles' => [0, 3, 6], // 添加角色 6
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md` - 订单管理员角色配置修复文档
|
||||
- `server/config/project.php` - 项目配置文件
|
||||
|
||||
## 总结
|
||||
|
||||
通过配置文件,为药师角色(角色6)赋予了以下权限:
|
||||
- ✅ 订单管理全部权限(查看、编辑、关联支付单)
|
||||
- ✅ 消费者处方审核权限(通过、驳回)
|
||||
- ✅ 支付单审核权限(通过、驳回)
|
||||
- ❌ 处方库管理权限(不需要)
|
||||
- ❌ 财务字段查看权限(不需要)
|
||||
|
||||
药师现在可以完整地参与处方审核和订单管理流程。
|
||||
@@ -0,0 +1,384 @@
|
||||
# 处方审核通过后锁定编辑和作废
|
||||
|
||||
## 修改说明
|
||||
|
||||
处方审核通过后,禁止编辑和作废操作,确保已审核通过的处方不被修改,保证处方的完整性和可追溯性。
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 1. 前端修改
|
||||
|
||||
**文件**: `admin/src/views/consumer/prescription/index.vue`
|
||||
|
||||
#### 编辑按钮
|
||||
|
||||
**修改前**:
|
||||
```vue
|
||||
<el-button
|
||||
v-if="!isApprovedActivePrescription(row) || Number(row.business_prescription_audit_rejected) === 1"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
v-perms="['cf.prescription/edit']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```vue
|
||||
<el-button
|
||||
v-if="!isApprovedActivePrescription(row)"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
v-perms="['cf.prescription/edit']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
```
|
||||
|
||||
**改进点**:
|
||||
- 移除了业务订单驳回的例外情况
|
||||
- 只要处方审核通过且未作废,就不显示编辑按钮
|
||||
- 简化了逻辑,更加严格
|
||||
|
||||
#### 删除按钮
|
||||
|
||||
删除按钮已经使用了 `isApprovedActivePrescription(row)` 判断,无需修改:
|
||||
|
||||
```vue
|
||||
<el-button
|
||||
v-if="!isApprovedActivePrescription(row)"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row.id)"
|
||||
v-perms="['cf.prescription/del']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
```
|
||||
|
||||
#### 判断函数
|
||||
|
||||
```typescript
|
||||
/** 已通过审核且未作废:仅可查看,不可编辑/删除 */
|
||||
function isApprovedActivePrescription(row: { audit_status?: number; void_status?: number }) {
|
||||
return Number(row.audit_status) === 1 && Number(row.void_status) !== 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 后端修改
|
||||
|
||||
**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php`
|
||||
|
||||
#### 编辑方法
|
||||
|
||||
**修改前**:
|
||||
```php
|
||||
// 已通过且未作废:不可编辑;例外:业务订单「处方审核」已驳回时允许医生改方,保存后业务订单侧审核会重置为待审
|
||||
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
|
||||
if (!PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject((int) $params['id'])) {
|
||||
self::setError('该处方已通过审核,不可编辑');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```php
|
||||
// 已通过且未作废:不可编辑
|
||||
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
|
||||
self::setError('该处方已通过审核,不可编辑');
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
**改进点**:
|
||||
- 移除了业务订单驳回的例外情况
|
||||
- 简化了逻辑,更加严格
|
||||
- 不再调用 `PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject()`
|
||||
|
||||
#### 删除方法
|
||||
|
||||
删除方法已经有正确的检查,无需修改:
|
||||
|
||||
```php
|
||||
public static function delete(int $id): bool
|
||||
{
|
||||
try {
|
||||
$prescription = Prescription::find($id);
|
||||
if (!$prescription) {
|
||||
self::setError('处方不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
|
||||
self::setError('该处方已通过审核,不可删除');
|
||||
return false;
|
||||
}
|
||||
|
||||
$prescription->delete();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 审核方法(作废)
|
||||
|
||||
审核驳回会同时作废处方,已经有正确的状态检查:
|
||||
|
||||
```php
|
||||
public static function audit(int $id, string $action, string $remark, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
// ...
|
||||
|
||||
// 只有待审核状态才能进行审核
|
||||
if ((int) ($row->audit_status ?? 1) !== 0) {
|
||||
self::setError('当前状态不可审核');
|
||||
return false;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
if ($action === 'reject') {
|
||||
if (trim($remark) === '') {
|
||||
self::setError('驳回时请填写审核意见');
|
||||
return false;
|
||||
}
|
||||
$row->audit_status = 2;
|
||||
$row->audit_time = $now;
|
||||
$row->audit_by = $adminId;
|
||||
$row->audit_by_name = $name;
|
||||
$row->audit_remark = $remark;
|
||||
$row->void_status = 1; // 驳回时同时作废
|
||||
$row->void_time = $now;
|
||||
$row->void_by = $adminId;
|
||||
$row->void_by_name = $name;
|
||||
|
||||
$ok = (bool) $row->save();
|
||||
if ($ok) {
|
||||
self::$lastAuditWecomNotify = self::notifyCreatorAuditResult($row, 'reject', $remark, $adminInfo);
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 权限规则
|
||||
|
||||
### 编辑权限
|
||||
|
||||
处方可以编辑的条件:
|
||||
1. **待审核状态** (`audit_status = 0`)
|
||||
2. **已驳回状态** (`audit_status = 2`),且满足以下条件:
|
||||
- 该诊单当天没有其他已通过的处方
|
||||
- 编辑后会清除驳回状态,重新进入待审核
|
||||
|
||||
处方不可编辑的条件:
|
||||
1. **已通过审核且未作废** (`audit_status = 1` && `void_status = 0`)
|
||||
- 即使业务订单驳回了处方审核,也不允许编辑
|
||||
- 必须先撤回业务订单,处方审核状态会重置为待审核,然后才能编辑
|
||||
|
||||
### 删除权限
|
||||
|
||||
处方可以删除的条件:
|
||||
1. **待审核状态** (`audit_status = 0`)
|
||||
2. **已驳回状态** (`audit_status = 2`)
|
||||
3. **已作废状态** (`void_status = 1`)
|
||||
|
||||
处方不可删除的条件:
|
||||
1. **已通过审核且未作废** (`audit_status = 1` && `void_status = 0`)
|
||||
|
||||
### 作废权限(审核驳回)
|
||||
|
||||
处方可以作废的条件:
|
||||
1. **待审核状态** (`audit_status = 0`)
|
||||
2. 有审核权限的用户
|
||||
|
||||
处方不可作废的条件:
|
||||
1. **已通过审核** (`audit_status = 1`)
|
||||
2. **已驳回** (`audit_status = 2`)
|
||||
|
||||
## 业务流程
|
||||
|
||||
### 场景1:处方审核通过后需要修改
|
||||
|
||||
**旧流程**(已废弃):
|
||||
1. 处方审核通过
|
||||
2. 创建业务订单
|
||||
3. 业务订单处方审核驳回
|
||||
4. 医生可以直接编辑已通过的处方 ❌
|
||||
|
||||
**新流程**(推荐):
|
||||
1. 处方审核通过
|
||||
2. 创建业务订单
|
||||
3. 业务订单处方审核驳回
|
||||
4. 管理员撤回业务订单(处方审核状态重置为待审核)
|
||||
5. 医生编辑处方
|
||||
6. 处方重新审核
|
||||
7. 创建新的业务订单
|
||||
|
||||
### 场景2:处方审核通过后发现错误
|
||||
|
||||
**解决方案**:
|
||||
1. 如果已创建业务订单,先撤回业务订单
|
||||
2. 处方审核状态会自动重置为待审核
|
||||
3. 医生编辑处方
|
||||
4. 处方重新审核
|
||||
5. 创建新的业务订单
|
||||
|
||||
### 场景3:处方审核驳回后修改
|
||||
|
||||
**流程**:
|
||||
1. 处方审核驳回(同时作废)
|
||||
2. 医生编辑处方(编辑后会清除驳回和作废状态)
|
||||
3. 处方重新进入待审核状态
|
||||
4. 重新审核
|
||||
|
||||
## 优势
|
||||
|
||||
### 1. 数据完整性
|
||||
- 已审核通过的处方不可修改,保证了处方的完整性
|
||||
- 避免审核后的处方被随意修改,导致审核记录与实际内容不符
|
||||
|
||||
### 2. 可追溯性
|
||||
- 每次修改都需要重新审核,留下完整的审核记录
|
||||
- 便于追溯处方的修改历史和审核历史
|
||||
|
||||
### 3. 流程规范
|
||||
- 强制执行"撤回 → 编辑 → 重新审核"的流程
|
||||
- 避免绕过审核流程直接修改处方
|
||||
|
||||
### 4. 责任明确
|
||||
- 审核人员对已审核的处方负责
|
||||
- 修改处方需要重新审核,责任链清晰
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 业务订单撤回
|
||||
|
||||
如果需要修改已审核通过的处方,必须先撤回业务订单:
|
||||
- 撤回业务订单会自动重置处方审核状态为待审核
|
||||
- 撤回后才能编辑处方
|
||||
- 编辑后需要重新审核处方
|
||||
- 审核通过后可以创建新的业务订单
|
||||
|
||||
### 2. 处方驳回
|
||||
|
||||
处方驳回会同时作废处方:
|
||||
- 驳回后处方状态变为"已驳回"(`audit_status = 2`)
|
||||
- 同时处方会被作废(`void_status = 1`)
|
||||
- 医生可以编辑已驳回的处方
|
||||
- 编辑后会清除驳回和作废状态,重新进入待审核
|
||||
|
||||
### 3. 权限配置
|
||||
|
||||
确保相关权限配置正确:
|
||||
- `cf.prescription/edit` - 编辑处方权限
|
||||
- `cf.prescription/del` - 删除处方权限
|
||||
- `cf.prescription/audit` - 审核处方权限
|
||||
- `tcm.prescriptionOrder/withdraw` - 撤回业务订单权限
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 编辑权限测试
|
||||
|
||||
**测试用例 1: 待审核处方可以编辑**
|
||||
- 创建处方(待审核状态)
|
||||
- 点击"编辑"按钮
|
||||
- 预期:可以正常编辑
|
||||
|
||||
**测试用例 2: 已通过处方不可编辑**
|
||||
- 创建处方并审核通过
|
||||
- 点击"编辑"按钮
|
||||
- 预期:按钮不显示或提示"该处方已通过审核,不可编辑"
|
||||
|
||||
**测试用例 3: 业务订单驳回后不可直接编辑**
|
||||
- 创建处方并审核通过
|
||||
- 创建业务订单
|
||||
- 业务订单处方审核驳回
|
||||
- 点击"编辑"按钮
|
||||
- 预期:按钮不显示或提示"该处方已通过审核,不可编辑"
|
||||
|
||||
**测试用例 4: 撤回业务订单后可以编辑**
|
||||
- 创建处方并审核通过
|
||||
- 创建业务订单
|
||||
- 撤回业务订单
|
||||
- 点击"编辑"按钮
|
||||
- 预期:可以正常编辑
|
||||
|
||||
**测试用例 5: 已驳回处方可以编辑**
|
||||
- 创建处方
|
||||
- 审核驳回
|
||||
- 点击"编辑"按钮
|
||||
- 预期:可以正常编辑,编辑后清除驳回和作废状态
|
||||
|
||||
### 2. 删除权限测试
|
||||
|
||||
**测试用例 6: 待审核处方可以删除**
|
||||
- 创建处方(待审核状态)
|
||||
- 点击"删除"按钮
|
||||
- 预期:可以正常删除
|
||||
|
||||
**测试用例 7: 已通过处方不可删除**
|
||||
- 创建处方并审核通过
|
||||
- 点击"删除"按钮
|
||||
- 预期:按钮不显示或提示"该处方已通过审核,不可删除"
|
||||
|
||||
**测试用例 8: 已驳回处方可以删除**
|
||||
- 创建处方
|
||||
- 审核驳回
|
||||
- 点击"删除"按钮
|
||||
- 预期:可以正常删除
|
||||
|
||||
### 3. 作废权限测试
|
||||
|
||||
**测试用例 9: 待审核处方可以驳回作废**
|
||||
- 创建处方(待审核状态)
|
||||
- 点击"审核"按钮,选择"驳回"
|
||||
- 预期:处方被驳回并作废
|
||||
|
||||
**测试用例 10: 已通过处方不可驳回作废**
|
||||
- 创建处方并审核通过
|
||||
- 点击"审核"按钮
|
||||
- 预期:按钮不显示或提示"当前状态不可审核"
|
||||
|
||||
**测试用例 11: 已驳回处方不可再次驳回**
|
||||
- 创建处方
|
||||
- 审核驳回
|
||||
- 再次点击"审核"按钮
|
||||
- 预期:按钮不显示或提示"当前状态不可审核"
|
||||
|
||||
### 4. 业务流程测试
|
||||
|
||||
**测试用例 12: 完整的修改流程**
|
||||
1. 创建处方并审核通过
|
||||
2. 创建业务订单
|
||||
3. 业务订单处方审核驳回
|
||||
4. 尝试编辑处方 → 预期:不可编辑
|
||||
5. 撤回业务订单
|
||||
6. 编辑处方 → 预期:可以编辑
|
||||
7. 处方重新审核通过
|
||||
8. 创建新的业务订单 → 预期:成功
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md` - 订单撤回重置审核状态文档
|
||||
- `BUG_FIXES_SUMMARY.md` - Bug修复总结文档
|
||||
|
||||
## 总结
|
||||
|
||||
通过移除业务订单驳回的例外情况,确保了:
|
||||
- ✅ 已审核通过的处方不可编辑
|
||||
- ✅ 已审核通过的处方不可删除
|
||||
- ✅ 已审核通过的处方不可作废(驳回)
|
||||
- ✅ 强制执行"撤回 → 编辑 → 重新审核"的规范流程
|
||||
- ✅ 保证处方的完整性和可追溯性
|
||||
- ✅ 责任链清晰,审核记录完整
|
||||
- ✅ 前后端双重校验,防止绕过限制
|
||||
@@ -0,0 +1,204 @@
|
||||
# 处方按钮显示检查
|
||||
|
||||
## 问题描述
|
||||
|
||||
用户反馈:处方审核通过后,"开方"按钮不显示了。
|
||||
|
||||
## 代码分析
|
||||
|
||||
### 前端代码
|
||||
|
||||
**文件**: `admin/src/views/tcm/appointment/list.vue`
|
||||
|
||||
#### 按钮显示逻辑
|
||||
|
||||
```vue
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/kaifang']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handlePrescription(row)"
|
||||
>
|
||||
{{ prescriptionActionLabel(row) }}
|
||||
</el-button>
|
||||
```
|
||||
|
||||
#### 按钮文字逻辑
|
||||
|
||||
```typescript
|
||||
function prescriptionActionLabel(row: any) {
|
||||
if (
|
||||
row?.prescription_audit_status === 1 &&
|
||||
Number(row?.prescription_void_status) !== 1
|
||||
) {
|
||||
return '查看'
|
||||
}
|
||||
return '开方'
|
||||
}
|
||||
```
|
||||
|
||||
**逻辑说明**:
|
||||
- 如果处方审核通过(`prescription_audit_status === 1`)且未作废(`prescription_void_status !== 1`),按钮文字显示"查看"
|
||||
- 否则,按钮文字显示"开方"
|
||||
|
||||
### 后端代码
|
||||
|
||||
**文件**: `server/app/adminapi/lists/doctor/AppointmentLists.php`
|
||||
|
||||
```php
|
||||
$apptId = (int) ($item['id'] ?? 0);
|
||||
$apptRx = $rxByAppointmentId[$apptId] ?? null;
|
||||
$item['prescription_audit_status'] = $apptRx !== null ? (int) ($apptRx['audit_status'] ?? -1) : -1;
|
||||
$item['prescription_void_status'] = $apptRx !== null ? (int) ($apptRx['void_status'] ?? 0) : 0;
|
||||
```
|
||||
|
||||
**逻辑说明**:
|
||||
- 如果预约有关联的处方,返回处方的审核状态和作废状态
|
||||
- 如果预约没有关联的处方,`prescription_audit_status` 为 `-1`,`prescription_void_status` 为 `0`
|
||||
|
||||
## 可能的原因
|
||||
|
||||
### 1. 权限问题
|
||||
|
||||
按钮有权限检查 `v-perms="['tcm.diagnosis/kaifang']"`,如果用户没有这个权限,按钮会被隐藏。
|
||||
|
||||
**检查方法**:
|
||||
1. 登录用户账号
|
||||
2. 检查用户角色是否有 `tcm.diagnosis/kaifang` 权限
|
||||
3. 在浏览器控制台查看按钮元素是否存在
|
||||
|
||||
### 2. 数据问题
|
||||
|
||||
后端返回的 `prescription_audit_status` 或 `prescription_void_status` 字段值不正确。
|
||||
|
||||
**检查方法**:
|
||||
1. 打开浏览器开发者工具
|
||||
2. 查看预约列表接口的响应数据
|
||||
3. 检查 `prescription_audit_status` 和 `prescription_void_status` 字段的值
|
||||
|
||||
**预期值**:
|
||||
- 未开方:`prescription_audit_status = -1`, `prescription_void_status = 0`
|
||||
- 待审核:`prescription_audit_status = 0`, `prescription_void_status = 0`
|
||||
- 已通过:`prescription_audit_status = 1`, `prescription_void_status = 0`
|
||||
- 已驳回:`prescription_audit_status = 2`, `prescription_void_status = 1`
|
||||
|
||||
### 3. 按钮文字问题
|
||||
|
||||
按钮确实显示了,但是文字从"开方"变成了"查看",用户可能误以为按钮消失了。
|
||||
|
||||
**检查方法**:
|
||||
1. 查看预约列表
|
||||
2. 找到已审核通过的处方对应的预约
|
||||
3. 检查按钮文字是否显示为"查看"
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案1:确认权限配置
|
||||
|
||||
确保用户角色有 `tcm.diagnosis/kaifang` 权限:
|
||||
|
||||
1. 进入后台管理 → 权限管理 → 角色管理
|
||||
2. 找到用户所属的角色
|
||||
3. 检查是否勾选了"中医诊断 → 开方"权限(`tcm.diagnosis/kaifang`)
|
||||
4. 如果没有勾选,勾选后保存
|
||||
|
||||
### 方案2:检查数据返回
|
||||
|
||||
在浏览器开发者工具中检查接口返回:
|
||||
|
||||
1. 打开浏览器开发者工具(F12)
|
||||
2. 切换到 Network 标签
|
||||
3. 刷新预约列表页面
|
||||
4. 找到预约列表接口(通常是 `/doctor.appointment/lists`)
|
||||
5. 查看响应数据中的 `prescription_audit_status` 和 `prescription_void_status` 字段
|
||||
|
||||
**示例数据**:
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"patient_name": "张三",
|
||||
"prescription_audit_status": 1, // 1=已通过
|
||||
"prescription_void_status": 0, // 0=未作废
|
||||
"has_prescription": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 方案3:按钮文字说明
|
||||
|
||||
如果按钮文字从"开方"变成了"查看",这是正常的:
|
||||
|
||||
- **未开方或待审核**:按钮显示"开方",点击后可以创建或编辑处方
|
||||
- **已审核通过**:按钮显示"查看",点击后只能查看处方(只读模式)
|
||||
|
||||
这是为了区分"可编辑"和"只读"两种模式。
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 测试1:未开方的预约
|
||||
|
||||
1. 找到一个未开方的预约(`has_prescription = 0`)
|
||||
2. 检查按钮是否显示"开方"
|
||||
3. 点击按钮,应该可以创建新处方
|
||||
|
||||
### 测试2:待审核的处方
|
||||
|
||||
1. 创建一个处方(待审核状态)
|
||||
2. 返回预约列表
|
||||
3. 检查按钮是否显示"开方"
|
||||
4. 点击按钮,应该可以编辑处方
|
||||
|
||||
### 测试3:已审核通过的处方
|
||||
|
||||
1. 创建一个处方并审核通过
|
||||
2. 返回预约列表
|
||||
3. 检查按钮是否显示"查看"
|
||||
4. 点击按钮,应该只能查看处方(只读模式)
|
||||
|
||||
### 测试4:已驳回的处方
|
||||
|
||||
1. 创建一个处方并审核驳回
|
||||
2. 返回预约列表
|
||||
3. 检查按钮是否显示"开方"
|
||||
4. 点击按钮,应该可以编辑处方
|
||||
|
||||
## 预期行为
|
||||
|
||||
| 处方状态 | `prescription_audit_status` | `prescription_void_status` | 按钮文字 | 点击行为 |
|
||||
|---------|---------------------------|--------------------------|---------|---------|
|
||||
| 未开方 | -1 | 0 | 开方 | 创建新处方 |
|
||||
| 待审核 | 0 | 0 | 开方 | 编辑处方 |
|
||||
| 已通过 | 1 | 0 | 查看 | 查看处方(只读) |
|
||||
| 已驳回 | 2 | 1 | 开方 | 编辑处方 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么审核通过后按钮文字变成"查看"?
|
||||
|
||||
A: 这是为了区分"可编辑"和"只读"两种模式。已审核通过的处方不可编辑,只能查看。
|
||||
|
||||
### Q2: 如果需要修改已审核通过的处方怎么办?
|
||||
|
||||
A: 需要先撤回业务订单(如果已创建),处方审核状态会自动重置为待审核,然后才能编辑。
|
||||
|
||||
### Q3: 按钮完全不显示怎么办?
|
||||
|
||||
A: 检查用户是否有 `tcm.diagnosis/kaifang` 权限。如果没有,需要在角色管理中添加该权限。
|
||||
|
||||
### Q4: 点击"查看"按钮后能编辑吗?
|
||||
|
||||
A: 不能。已审核通过的处方只能查看,不能编辑。如果需要编辑,需要先撤回业务订单。
|
||||
|
||||
## 总结
|
||||
|
||||
按钮的显示逻辑是正确的:
|
||||
- ✅ 按钮始终显示(只要有 `tcm.diagnosis/kaifang` 权限)
|
||||
- ✅ 按钮文字根据处方状态动态变化("开方" 或 "查看")
|
||||
- ✅ 点击行为根据处方状态不同(可编辑 或 只读)
|
||||
|
||||
如果用户反馈"按钮不显示",可能是以下原因:
|
||||
1. 用户没有 `tcm.diagnosis/kaifang` 权限
|
||||
2. 按钮文字从"开方"变成了"查看",用户误以为按钮消失了
|
||||
3. 数据返回有问题,需要检查后端接口
|
||||
|
||||
建议先检查权限配置和数据返回,确认按钮是否真的不显示。
|
||||
@@ -0,0 +1,321 @@
|
||||
# 处方组件重复请求修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
在预约列表页面点击"开处方"或"查看病历"按钮时,会出现重复请求 `/tcm.prescription/detail` 接口的问题,导致错误。
|
||||
|
||||
## 问题原因
|
||||
|
||||
### 1. 快速连续点击
|
||||
|
||||
用户可能快速连续点击按钮,导致 `open()` 或 `openById()` 方法被多次调用。
|
||||
|
||||
### 2. 异步操作未加锁
|
||||
|
||||
`open()` 和 `openById()` 方法是异步的,但没有加锁机制,导致:
|
||||
- 第一次点击开始执行
|
||||
- 第二次点击也开始执行
|
||||
- 两次请求同时发送
|
||||
|
||||
### 3. 代码流程
|
||||
|
||||
```javascript
|
||||
const open = async (data: any) => {
|
||||
// 没有防重复检查
|
||||
await loadDictOptions()
|
||||
|
||||
if (appointmentId) {
|
||||
const existing = await prescriptionGetByAppointment(...) // 请求1
|
||||
if (existing?.id) {
|
||||
const detail = await prescriptionDetail(...) // 请求2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 添加防重复打开标志
|
||||
|
||||
使用 `isOpening` 标志来防止重复调用:
|
||||
|
||||
```typescript
|
||||
// 防止重复打开
|
||||
const isOpening = ref(false)
|
||||
|
||||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||||
// 防止重复打开
|
||||
if (isOpening.value) {
|
||||
console.warn('处方正在打开中,请勿重复点击')
|
||||
return
|
||||
}
|
||||
|
||||
isOpening.value = true
|
||||
|
||||
try {
|
||||
// 原有逻辑...
|
||||
} finally {
|
||||
// 延迟重置,避免快速连续点击
|
||||
setTimeout(() => {
|
||||
isOpening.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
1. **检查标志**:方法开始时检查 `isOpening.value`,如果为 true 则直接返回
|
||||
2. **设置标志**:开始执行时设置 `isOpening.value = true`
|
||||
3. **finally 重置**:无论成功还是失败,都在 finally 中重置标志
|
||||
4. **延迟重置**:使用 `setTimeout` 延迟 500ms 重置,防止快速连续点击
|
||||
|
||||
## 修改的文件
|
||||
|
||||
**文件**:`admin/src/components/tcm-prescription/index.vue`
|
||||
|
||||
### 1. 添加防重复标志
|
||||
|
||||
```typescript
|
||||
// 防止重复打开
|
||||
const isOpening = ref(false)
|
||||
```
|
||||
|
||||
### 2. 修改 open 方法
|
||||
|
||||
```typescript
|
||||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||||
// 防止重复打开
|
||||
if (isOpening.value) {
|
||||
console.warn('处方正在打开中,请勿重复点击')
|
||||
return
|
||||
}
|
||||
|
||||
isOpening.value = true
|
||||
|
||||
try {
|
||||
// 原有逻辑...
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isOpening.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 修改 openById 方法
|
||||
|
||||
```typescript
|
||||
const openById = async (prescriptionId: number) => {
|
||||
// 防止重复打开
|
||||
if (isOpening.value) {
|
||||
console.warn('处方正在打开中,请勿重复点击')
|
||||
return
|
||||
}
|
||||
|
||||
isOpening.value = true
|
||||
|
||||
try {
|
||||
const res = await prescriptionDetail({ id: prescriptionId })
|
||||
savedPrescription.value = res
|
||||
if (res?.case_record && Object.keys(res.case_record).length > 0) {
|
||||
await loadDictOptions()
|
||||
}
|
||||
visible.value = true
|
||||
} catch (e) {
|
||||
feedback.msgError('加载处方失败')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isOpening.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 修复前
|
||||
|
||||
```
|
||||
用户点击"开处方"按钮
|
||||
↓
|
||||
调用 open() 方法
|
||||
↓
|
||||
用户再次点击(快速连续点击)
|
||||
↓
|
||||
再次调用 open() 方法
|
||||
↓
|
||||
两次请求同时发送
|
||||
↓
|
||||
❌ 重复请求错误
|
||||
```
|
||||
|
||||
### 修复后
|
||||
|
||||
```
|
||||
用户点击"开处方"按钮
|
||||
↓
|
||||
调用 open() 方法
|
||||
↓
|
||||
设置 isOpening = true
|
||||
↓
|
||||
用户再次点击(快速连续点击)
|
||||
↓
|
||||
检查 isOpening = true
|
||||
↓
|
||||
✓ 直接返回,不执行
|
||||
↓
|
||||
第一次请求完成
|
||||
↓
|
||||
500ms 后重置 isOpening = false
|
||||
```
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 场景1:快速连续点击
|
||||
|
||||
**操作**:快速连续点击"开处方"按钮 2-3 次
|
||||
|
||||
**预期结果**:
|
||||
- 只发送一次请求
|
||||
- 控制台显示警告:"处方正在打开中,请勿重复点击"
|
||||
- 处方正常打开
|
||||
|
||||
---
|
||||
|
||||
### 场景2:正常点击
|
||||
|
||||
**操作**:正常点击"开处方"按钮
|
||||
|
||||
**预期结果**:
|
||||
- 发送请求
|
||||
- 处方正常打开
|
||||
- 无警告信息
|
||||
|
||||
---
|
||||
|
||||
### 场景3:间隔点击
|
||||
|
||||
**操作**:点击"开处方",等待 1 秒后再次点击
|
||||
|
||||
**预期结果**:
|
||||
- 第一次点击正常打开
|
||||
- 第二次点击也正常打开(因为已过 500ms)
|
||||
|
||||
---
|
||||
|
||||
### 场景4:查看已有处方
|
||||
|
||||
**操作**:点击"查看病历"按钮
|
||||
|
||||
**预期结果**:
|
||||
- 调用 `prescriptionGetByAppointment` 查询
|
||||
- 如果有处方,调用 `prescriptionDetail` 获取详情
|
||||
- 只发送一次 detail 请求
|
||||
|
||||
---
|
||||
|
||||
### 场景5:请求失败
|
||||
|
||||
**操作**:点击"开处方",但请求失败
|
||||
|
||||
**预期结果**:
|
||||
- 显示错误提示
|
||||
- 500ms 后 `isOpening` 重置
|
||||
- 可以再次点击
|
||||
|
||||
## 其他优化建议
|
||||
|
||||
### 1. 按钮禁用状态
|
||||
|
||||
可以在按钮上添加 loading 状态:
|
||||
|
||||
```vue
|
||||
<el-button
|
||||
:loading="isOpening"
|
||||
@click="handlePrescription(row)"
|
||||
>
|
||||
{{ prescriptionActionLabel(row) }}
|
||||
</el-button>
|
||||
```
|
||||
|
||||
但这需要将 `isOpening` 暴露给父组件,可能不太合适。
|
||||
|
||||
### 2. 防抖处理
|
||||
|
||||
可以使用 lodash 的 debounce 函数:
|
||||
|
||||
```typescript
|
||||
import { debounce } from 'lodash-es'
|
||||
|
||||
const debouncedOpen = debounce(open, 500, { leading: true, trailing: false })
|
||||
```
|
||||
|
||||
但当前的解决方案已经足够简单有效。
|
||||
|
||||
### 3. 请求取消
|
||||
|
||||
可以使用 AbortController 取消重复请求:
|
||||
|
||||
```typescript
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
const open = async (data: any) => {
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
abortController = new AbortController()
|
||||
|
||||
// 在请求中使用 signal
|
||||
await fetch(url, { signal: abortController.signal })
|
||||
}
|
||||
```
|
||||
|
||||
但这需要修改 API 调用层,改动较大。
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 延迟时间
|
||||
|
||||
当前设置为 500ms,可以根据实际情况调整:
|
||||
- 太短(< 300ms):可能无法有效防止快速连续点击
|
||||
- 太长(> 1000ms):用户体验不好,需要等待较长时间
|
||||
|
||||
### 2. 控制台警告
|
||||
|
||||
`console.warn` 只在开发环境有用,生产环境可以考虑:
|
||||
- 移除警告
|
||||
- 或者改为用户提示:`feedback.msgWarning('请勿重复点击')`
|
||||
|
||||
### 3. 多个入口
|
||||
|
||||
如果有多个地方调用 `open()` 方法,都会受到这个锁的保护。
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 修改的文件
|
||||
- `admin/src/components/tcm-prescription/index.vue` - 处方组件
|
||||
|
||||
### 调用的文件
|
||||
- `admin/src/views/tcm/appointment/list.vue` - 预约列表
|
||||
- 其他可能调用处方组件的页面
|
||||
|
||||
## 总结
|
||||
|
||||
✅ 添加 `isOpening` 标志防止重复打开
|
||||
✅ 使用 try-finally 确保标志正确重置
|
||||
✅ 延迟 500ms 重置,防止快速连续点击
|
||||
✅ 同时修复 `open()` 和 `openById()` 方法
|
||||
✅ 添加控制台警告提示
|
||||
|
||||
**效果**:
|
||||
- 防止重复请求
|
||||
- 避免接口错误
|
||||
- 提升用户体验
|
||||
- 代码简单易维护
|
||||
|
||||
**下一步**:
|
||||
1. 测试快速连续点击
|
||||
2. 测试正常点击流程
|
||||
3. 测试请求失败情况
|
||||
4. 考虑是否需要用户提示
|
||||
@@ -0,0 +1,228 @@
|
||||
# 预约处方查询权限控制
|
||||
|
||||
## 修改说明
|
||||
|
||||
为 `getByAppointment` 接口添加了权限检查,确保用户只能查询自己有权限查看的处方。
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 1. Controller 层修改
|
||||
|
||||
**文件**: `server/app/adminapi/controller/tcm/PrescriptionController.php`
|
||||
|
||||
**修改前**:
|
||||
```php
|
||||
public function getByAppointment()
|
||||
{
|
||||
$appointmentId = (int)($this->request->get('appointment_id') ?? 0);
|
||||
if (!$appointmentId) {
|
||||
return $this->fail('预约ID不能为空');
|
||||
}
|
||||
$prescription = PrescriptionLogic::getByAppointment($appointmentId);
|
||||
return $this->data($prescription ?? []);
|
||||
}
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```php
|
||||
public function getByAppointment()
|
||||
{
|
||||
$appointmentId = (int)($this->request->get('appointment_id') ?? 0);
|
||||
if (!$appointmentId) {
|
||||
return $this->fail('预约ID不能为空');
|
||||
}
|
||||
$prescription = PrescriptionLogic::getByAppointment($appointmentId, (int)$this->adminId, $this->adminInfo);
|
||||
if ($prescription === null) {
|
||||
$msg = PrescriptionLogic::getError();
|
||||
return $this->fail($msg !== '' ? $msg : '未找到处方或无权限查看');
|
||||
}
|
||||
return $this->data($prescription);
|
||||
}
|
||||
```
|
||||
|
||||
**改进点**:
|
||||
- 传入当前登录用户的 `adminId` 和 `adminInfo`
|
||||
- 处理权限检查失败的情况,返回明确的错误信息
|
||||
- 区分"未找到处方"和"无权限查看"两种情况
|
||||
|
||||
### 2. Logic 层修改
|
||||
|
||||
**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php`
|
||||
|
||||
**修改前**:
|
||||
```php
|
||||
/**
|
||||
* 根据预约ID获取处方
|
||||
*/
|
||||
public static function getByAppointment(int $appointmentId): ?array
|
||||
{
|
||||
$row = Prescription::where('appointment_id', $appointmentId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```php
|
||||
/**
|
||||
* 根据预约ID获取处方(带权限检查)
|
||||
*/
|
||||
public static function getByAppointment(int $appointmentId, int $viewerAdminId, array $viewerAdminInfo): ?array
|
||||
{
|
||||
self::$error = '';
|
||||
$row = Prescription::where('appointment_id', $appointmentId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 权限检查:只返回当前用户有权限查看的处方
|
||||
if (!self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)) {
|
||||
self::setError('无权限查看此处方');
|
||||
return null;
|
||||
}
|
||||
|
||||
return $row->toArray();
|
||||
}
|
||||
```
|
||||
|
||||
**改进点**:
|
||||
- 添加 `viewerAdminId` 和 `viewerAdminInfo` 参数
|
||||
- 使用 `canViewPrescription()` 方法进行权限检查
|
||||
- 设置错误信息,便于前端显示
|
||||
|
||||
## 权限规则
|
||||
|
||||
根据 `canViewPrescription()` 方法,以下用户可以查看处方:
|
||||
|
||||
1. **超级管理员** - 可以查看所有处方
|
||||
2. **处方创建人** - 可以查看自己创建的处方
|
||||
3. **医助** - 可以查看自己协助的处方
|
||||
4. **共享处方** - 所有人可以查看标记为共享的处方
|
||||
5. **可见角色** - 处方指定的可见角色可以查看
|
||||
6. **有开方权限的医生** - 拥有 `tcm.diagnosis/kaifang` 权限的医生可以查看所有处方(只读)
|
||||
|
||||
## 使用场景
|
||||
|
||||
这个接口主要用于以下场景:
|
||||
|
||||
1. **查看病历按钮** - 在预约列表中点击"查看病历"时调用
|
||||
2. **处方历史查询** - 根据预约ID查询患者的历史处方
|
||||
3. **医生协作** - 其他医生查看患者的处方记录(需要有开方权限)
|
||||
|
||||
## 前端调用示例
|
||||
|
||||
```typescript
|
||||
// admin/src/views/tcm/appointment/list.vue
|
||||
const handleViewCase = async (row: any) => {
|
||||
if (!row.id) {
|
||||
feedback.msgWarning('预约信息不完整')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const prescription = await prescriptionGetByAppointment({ appointment_id: row.id })
|
||||
if (prescription?.id) {
|
||||
prescriptionRef.value?.openById(prescription.id)
|
||||
} else {
|
||||
feedback.msgWarning('该预约暂无病历记录,请先开方')
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 权限不足或其他错误
|
||||
feedback.msgError(error?.msg || '获取病历失败')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 可能的错误信息
|
||||
|
||||
1. **"预约ID不能为空"** - 未传入 `appointment_id` 参数
|
||||
2. **"未找到处方或无权限查看"** - 处方不存在或当前用户无权限查看
|
||||
3. **"无权限查看此处方"** - 处方存在但当前用户无权限查看
|
||||
|
||||
### 前端处理建议
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const prescription = await prescriptionGetByAppointment({ appointment_id: row.id })
|
||||
// 成功获取处方
|
||||
} catch (error: any) {
|
||||
if (error?.msg?.includes('无权限')) {
|
||||
feedback.msgWarning('您没有权限查看此处方')
|
||||
} else if (error?.msg?.includes('未找到')) {
|
||||
feedback.msgWarning('该预约暂无病历记录')
|
||||
} else {
|
||||
feedback.msgError(error?.msg || '获取病历失败')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 权限测试
|
||||
|
||||
**测试用例 1: 创建人查看自己的处方**
|
||||
- 医生A创建处方
|
||||
- 医生A查看该处方
|
||||
- 预期:成功返回处方数据
|
||||
|
||||
**测试用例 2: 其他医生查看处方(有开方权限)**
|
||||
- 医生A创建处方
|
||||
- 医生B(有 `tcm.diagnosis/kaifang` 权限)查看该处方
|
||||
- 预期:成功返回处方数据(只读)
|
||||
|
||||
**测试用例 3: 其他医生查看处方(无开方权限)**
|
||||
- 医生A创建处方
|
||||
- 医生C(无 `tcm.diagnosis/kaifang` 权限)查看该处方
|
||||
- 预期:返回"无权限查看此处方"错误
|
||||
|
||||
**测试用例 4: 医助查看协助的处方**
|
||||
- 医生A创建处方,医助B协助
|
||||
- 医助B查看该处方
|
||||
- 预期:成功返回处方数据
|
||||
|
||||
**测试用例 5: 查看共享处方**
|
||||
- 医生A创建处方并标记为共享
|
||||
- 任何用户查看该处方
|
||||
- 预期:成功返回处方数据
|
||||
|
||||
### 2. 边界测试
|
||||
|
||||
**测试用例 6: 预约不存在**
|
||||
- 传入不存在的 `appointment_id`
|
||||
- 预期:返回"未找到处方或无权限查看"
|
||||
|
||||
**测试用例 7: 预约存在但无处方**
|
||||
- 传入存在的 `appointment_id`,但该预约没有创建处方
|
||||
- 预期:返回"未找到处方或无权限查看"
|
||||
|
||||
**测试用例 8: 处方已删除**
|
||||
- 传入已删除处方的 `appointment_id`
|
||||
- 预期:返回"未找到处方或无权限查看"
|
||||
|
||||
## 安全性说明
|
||||
|
||||
1. **防止越权访问** - 用户只能查看自己有权限的处方,防止查看其他医生的私有处方
|
||||
2. **数据隔离** - 通过权限检查实现数据隔离,保护患者隐私
|
||||
3. **审计追踪** - 所有查询操作都会记录当前用户信息,便于审计
|
||||
4. **错误信息脱敏** - 对于无权限的情况,不暴露处方是否存在的详细信息
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `PRESCRIPTION_VIEW_PERMISSION_FIX.md` - 处方查看权限修复文档
|
||||
- `BUG_FIXES_SUMMARY.md` - Bug修复总结文档
|
||||
|
||||
## 总结
|
||||
|
||||
通过添加权限检查,确保了:
|
||||
- ✅ 用户只能查询自己有权限查看的处方
|
||||
- ✅ 防止越权访问其他医生的私有处方
|
||||
- ✅ 支持医生协作(有开方权限的医生可以查看所有处方)
|
||||
- ✅ 保护患者隐私和数据安全
|
||||
- ✅ 提供明确的错误信息,便于前端处理
|
||||
@@ -0,0 +1,318 @@
|
||||
# 处方订单最小金额验证优化
|
||||
|
||||
## 需求说明
|
||||
|
||||
在创建处方订单时,如果关联的支付单总金额小于配置的最小金额(`PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT`),则不允许创建订单。
|
||||
|
||||
## 配置说明
|
||||
|
||||
**配置文件**:`server/.env`
|
||||
|
||||
```env
|
||||
PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT="100"
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 单位:元(人民币)
|
||||
- 默认值:0(不校验)
|
||||
- 当前配置:100元
|
||||
|
||||
## 验证规则
|
||||
|
||||
### 修改前(原逻辑)
|
||||
|
||||
```
|
||||
1. 订单金额 >= 最小金额 ✓
|
||||
2. 每个关联支付单金额 >= 最小金额 ✓
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 要求每个支付单都必须 >= 100元
|
||||
- 不允许多个小额支付单组合(如 60元 + 50元 = 110元)
|
||||
|
||||
### 修改后(新逻辑)
|
||||
|
||||
```
|
||||
1. 订单金额 >= 最小金额 ✓
|
||||
2. 必须关联至少一个支付单 ✓
|
||||
3. 关联支付单总金额 >= 最小金额 ✓
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 允许多个小额支付单组合
|
||||
- 更灵活的支付方式
|
||||
- 总金额达标即可
|
||||
|
||||
## 代码修改
|
||||
|
||||
**文件**:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
|
||||
**方法**:`assertDepositAndLinkPayRules()`
|
||||
|
||||
### 修改内容
|
||||
|
||||
```php
|
||||
public static function assertDepositAndLinkPayRules(float $orderAmount, array $payOrderIds): ?string
|
||||
{
|
||||
$min = self::depositMinAmount();
|
||||
if ($min <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 检查订单金额
|
||||
if ($orderAmount < $min) {
|
||||
return '订单金额须大于等于定金门槛 ¥' . $min . '(当前 ¥' . $orderAmount . ')';
|
||||
}
|
||||
|
||||
// 2. 检查是否关联支付单
|
||||
if ($payOrderIds === []) {
|
||||
return '未关联支付单,关联支付单总金额须大于等于定金门槛 ¥' . $min;
|
||||
}
|
||||
|
||||
// 3. 检查关联支付单的总金额
|
||||
$rows = Order::whereIn('id', $payOrderIds)->whereNull('delete_time')->column('amount', 'id');
|
||||
$totalAmount = 0.0;
|
||||
foreach ($payOrderIds as $pid) {
|
||||
$pid = (int) $pid;
|
||||
if ($pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$amt = round((float) ($rows[$pid] ?? 0), 2);
|
||||
$totalAmount += $amt;
|
||||
}
|
||||
|
||||
if ($totalAmount < $min) {
|
||||
return '关联支付单总金额须大于等于定金门槛 ¥' . $min . '(当前总金额 ¥' . $totalAmount . ')';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## 验证场景
|
||||
|
||||
### 场景1:订单金额不足
|
||||
|
||||
**配置**:最小金额 = 100元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:80元
|
||||
- 关联支付单:[100元]
|
||||
|
||||
**结果**:❌ 失败
|
||||
**错误信息**:订单金额须大于等于定金门槛 ¥100(当前 ¥80)
|
||||
|
||||
---
|
||||
|
||||
### 场景2:未关联支付单
|
||||
|
||||
**配置**:最小金额 = 100元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:150元
|
||||
- 关联支付单:[]
|
||||
|
||||
**结果**:❌ 失败
|
||||
**错误信息**:未关联支付单,关联支付单总金额须大于等于定金门槛 ¥100
|
||||
|
||||
---
|
||||
|
||||
### 场景3:关联支付单总金额不足
|
||||
|
||||
**配置**:最小金额 = 100元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:150元
|
||||
- 关联支付单:[30元, 40元, 20元]
|
||||
- 总金额:90元
|
||||
|
||||
**结果**:❌ 失败
|
||||
**错误信息**:关联支付单总金额须大于等于定金门槛 ¥100(当前总金额 ¥90)
|
||||
|
||||
---
|
||||
|
||||
### 场景4:多个小额支付单组合达标
|
||||
|
||||
**配置**:最小金额 = 100元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:150元
|
||||
- 关联支付单:[60元, 50元]
|
||||
- 总金额:110元
|
||||
|
||||
**结果**:✅ 成功
|
||||
**说明**:虽然单个支付单都小于100元,但总金额达标
|
||||
|
||||
---
|
||||
|
||||
### 场景5:单个大额支付单
|
||||
|
||||
**配置**:最小金额 = 100元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:150元
|
||||
- 关联支付单:[120元]
|
||||
- 总金额:120元
|
||||
|
||||
**结果**:✅ 成功
|
||||
|
||||
---
|
||||
|
||||
### 场景6:配置为0(不校验)
|
||||
|
||||
**配置**:最小金额 = 0元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:10元
|
||||
- 关联支付单:[]
|
||||
|
||||
**结果**:✅ 成功
|
||||
**说明**:最小金额为0时,不进行任何校验
|
||||
|
||||
---
|
||||
|
||||
### 场景7:多个支付单,总金额刚好达标
|
||||
|
||||
**配置**:最小金额 = 100元
|
||||
|
||||
**输入**:
|
||||
- 订单金额:150元
|
||||
- 关联支付单:[30元, 30元, 40元]
|
||||
- 总金额:100元
|
||||
|
||||
**结果**:✅ 成功
|
||||
**说明**:总金额刚好等于最小金额,符合"大于等于"的要求
|
||||
|
||||
## 前端提示优化
|
||||
|
||||
**文件**:`admin/src/views/consumer/prescription/order_list.vue`
|
||||
|
||||
**当前提示**:
|
||||
```
|
||||
支付单审核时可对照此处关联的收款记录。
|
||||
列表仅展示金额大于等于定金门槛的收款单(门槛为 0 时展示全部)。
|
||||
```
|
||||
|
||||
**建议修改为**:
|
||||
```
|
||||
支付单审核时可对照此处关联的收款记录。
|
||||
关联支付单总金额须大于等于定金门槛(当前门槛:¥100)。
|
||||
列表仅展示金额大于等于定金门槛的收款单(门槛为 0 时展示全部)。
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 查看当前配置
|
||||
|
||||
```bash
|
||||
# 查看 .env 文件
|
||||
cat server/.env | grep PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT
|
||||
```
|
||||
|
||||
### 修改配置
|
||||
|
||||
```bash
|
||||
# 编辑 .env 文件
|
||||
vim server/.env
|
||||
|
||||
# 修改为 200 元
|
||||
PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT="200"
|
||||
|
||||
# 修改为 0(不校验)
|
||||
PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT="0"
|
||||
```
|
||||
|
||||
### 配置生效
|
||||
|
||||
修改 `.env` 文件后,配置会立即生效,无需重启服务。
|
||||
|
||||
## 业务逻辑说明
|
||||
|
||||
### 为什么需要最小金额限制?
|
||||
|
||||
1. **定金机制**:确保客户已支付足够的定金
|
||||
2. **风险控制**:避免小额订单的履约风险
|
||||
3. **业务规范**:统一订单金额标准
|
||||
|
||||
### 为什么改为总金额验证?
|
||||
|
||||
1. **灵活性**:允许客户分多次支付
|
||||
2. **用户体验**:不强制单次支付大额
|
||||
3. **实际需求**:多次小额支付也能达到定金要求
|
||||
|
||||
### 为什么必须关联支付单?
|
||||
|
||||
1. **资金确认**:确保订单有对应的收款记录
|
||||
2. **审核依据**:支付单审核时需要对照
|
||||
3. **财务管理**:便于对账和统计
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 单元测试
|
||||
|
||||
```php
|
||||
// 测试订单金额不足
|
||||
$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(80, [1]);
|
||||
assert($result !== null);
|
||||
|
||||
// 测试未关联支付单
|
||||
$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(150, []);
|
||||
assert($result !== null);
|
||||
|
||||
// 测试总金额不足
|
||||
$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(150, [1, 2]); // 假设总金额 < 100
|
||||
assert($result !== null);
|
||||
|
||||
// 测试总金额达标
|
||||
$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(150, [3, 4]); // 假设总金额 >= 100
|
||||
assert($result === null);
|
||||
```
|
||||
|
||||
### 2. 集成测试
|
||||
|
||||
1. 创建多个小额支付单(如 60元、50元)
|
||||
2. 创建订单,关联这些支付单
|
||||
3. 验证是否能成功创建
|
||||
4. 验证错误提示是否正确
|
||||
|
||||
### 3. 边界测试
|
||||
|
||||
- 总金额刚好等于最小金额(100元)
|
||||
- 总金额略小于最小金额(99.99元)
|
||||
- 总金额略大于最小金额(100.01元)
|
||||
- 最小金额为0
|
||||
- 最小金额为负数(应该按0处理)
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 配置文件
|
||||
- `server/.env` - 环境配置
|
||||
- `server/config/prescription_order.php` - 订单配置
|
||||
|
||||
### 后端文件
|
||||
- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 订单逻辑
|
||||
- `server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php` - 订单验证
|
||||
|
||||
### 前端文件
|
||||
- `admin/src/views/consumer/prescription/order_list.vue` - 订单列表页面
|
||||
|
||||
## 总结
|
||||
|
||||
✅ 修改验证逻辑:从"每个支付单"改为"支付单总金额"
|
||||
✅ 添加必须关联支付单的验证
|
||||
✅ 优化错误提示信息,显示当前总金额
|
||||
✅ 支持多个小额支付单组合
|
||||
✅ 保持订单金额验证不变
|
||||
✅ 配置为0时不进行校验
|
||||
|
||||
**优势**:
|
||||
- 更灵活的支付方式
|
||||
- 更好的用户体验
|
||||
- 更清晰的错误提示
|
||||
- 更符合实际业务需求
|
||||
|
||||
**下一步**:
|
||||
1. 测试各种场景
|
||||
2. 更新前端提示文字
|
||||
3. 编写单元测试
|
||||
4. 更新用户文档
|
||||
@@ -0,0 +1,387 @@
|
||||
# 订单撤回后重置处方审核状态
|
||||
|
||||
## 需求说明
|
||||
|
||||
当用户在订单列表中点击"撤回"按钮后,除了将订单状态改为"已取消",还需要将关联的处方审核状态重置为"待审核",以便重新审核。
|
||||
|
||||
## 业务场景
|
||||
|
||||
### 场景描述
|
||||
|
||||
1. 医生创建处方
|
||||
2. 处方审核通过(`audit_status = 1`)
|
||||
3. 创建业务订单(`fulfillment_status = 1` 待双审通过)
|
||||
4. 用户点击"撤回"订单
|
||||
5. **期望**:处方审核状态重置为"待审核"(`audit_status = 0`)
|
||||
|
||||
### 为什么需要重置?
|
||||
|
||||
1. **重新审核**:撤回订单可能是因为处方有问题,需要重新审核
|
||||
2. **状态一致性**:订单撤回后,处方应该回到初始状态
|
||||
3. **业务流程**:允许医生修改处方后重新提交审核
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### 处方表(zyt_tcm_prescription)
|
||||
|
||||
```sql
|
||||
`audit_status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '审核:0待审核 1已通过 2已驳回(同时作废)'
|
||||
`audit_time` int(10) unsigned DEFAULT NULL COMMENT '审核时间'
|
||||
`audit_by` int(11) unsigned DEFAULT NULL COMMENT '审核人ID'
|
||||
`audit_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '审核意见'
|
||||
```
|
||||
|
||||
**审核状态**:
|
||||
- 0 = 待审核
|
||||
- 1 = 已通过
|
||||
- 2 = 已驳回(同时作废)
|
||||
|
||||
### 业务订单表(zyt_tcm_prescription_order)
|
||||
|
||||
```sql
|
||||
`prescription_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '消费者处方ID'
|
||||
`fulfillment_status` tinyint(2) unsigned NOT NULL DEFAULT 1 COMMENT '1待双审通过 2履约中 3已完成 4已取消'
|
||||
```
|
||||
|
||||
**履约状态**:
|
||||
- 1 = 待双审通过
|
||||
- 2 = 履约中
|
||||
- 3 = 已完成
|
||||
- 4 = 已取消(撤回)
|
||||
|
||||
## 代码修改
|
||||
|
||||
**文件**:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
|
||||
**方法**:`withdraw()`
|
||||
|
||||
### 修改内容
|
||||
|
||||
```php
|
||||
public static function withdraw(int $id, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
return false;
|
||||
}
|
||||
if (!self::canWithdrawOrder($order, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限撤回';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->fulfillment_status !== 1) {
|
||||
self::$error = '仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程';
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->fulfillment_status = 4;
|
||||
self::clearPayOrderLinks($order);
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
|
||||
// 撤回订单后,将关联的处方审核状态改回"待审核"
|
||||
$prescriptionId = (int) $order->prescription_id;
|
||||
if ($prescriptionId > 0) {
|
||||
Prescription::where('id', $prescriptionId)
|
||||
->whereNull('delete_time')
|
||||
->update([
|
||||
'audit_status' => 0, // 0=待审核
|
||||
'audit_time' => null,
|
||||
'audit_by' => null,
|
||||
'audit_remark' => '',
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
```
|
||||
|
||||
### 修改说明
|
||||
|
||||
1. **保存订单状态**:`$order->save()`
|
||||
2. **获取处方ID**:`$prescriptionId = (int) $order->prescription_id`
|
||||
3. **重置处方审核状态**:
|
||||
- `audit_status = 0`(待审核)
|
||||
- `audit_time = null`(清空审核时间)
|
||||
- `audit_by = null`(清空审核人)
|
||||
- `audit_remark = ''`(清空审核意见)
|
||||
- `update_time = time()`(更新时间)
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 完整流程
|
||||
|
||||
```
|
||||
用户点击"撤回"按钮
|
||||
↓
|
||||
调用 withdraw() 方法
|
||||
↓
|
||||
验证权限和状态
|
||||
↓
|
||||
更新订单状态为"已取消"(4)
|
||||
↓
|
||||
清除支付单关联
|
||||
↓
|
||||
保存订单
|
||||
↓
|
||||
获取关联的处方ID
|
||||
↓
|
||||
重置处方审核状态为"待审核"(0)
|
||||
↓
|
||||
清空审核时间、审核人、审核意见
|
||||
↓
|
||||
返回成功
|
||||
```
|
||||
|
||||
### 前端显示
|
||||
|
||||
**订单列表页面**:
|
||||
- 订单状态:已取消
|
||||
- 撤回按钮:消失(只有"待双审通过"状态才显示)
|
||||
|
||||
**处方列表页面**:
|
||||
- 审核状态:待审核
|
||||
- 可以重新审核
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 场景1:正常撤回
|
||||
|
||||
**初始状态**:
|
||||
- 处方审核状态:已通过(1)
|
||||
- 订单履约状态:待双审通过(1)
|
||||
|
||||
**操作**:点击"撤回"按钮
|
||||
|
||||
**预期结果**:
|
||||
- 订单履约状态:已取消(4)
|
||||
- 处方审核状态:待审核(0)
|
||||
- 审核时间:null
|
||||
- 审核人:null
|
||||
- 审核意见:空
|
||||
|
||||
---
|
||||
|
||||
### 场景2:撤回后重新审核
|
||||
|
||||
**步骤**:
|
||||
1. 撤回订单(处方状态变为"待审核")
|
||||
2. 医生修改处方
|
||||
3. 重新提交审核
|
||||
4. 审核通过
|
||||
5. 创建新订单
|
||||
|
||||
**预期结果**:流程正常,可以重新审核和创建订单
|
||||
|
||||
---
|
||||
|
||||
### 场景3:无权限撤回
|
||||
|
||||
**初始状态**:
|
||||
- 当前用户:非创建人、非医助
|
||||
|
||||
**操作**:点击"撤回"按钮
|
||||
|
||||
**预期结果**:
|
||||
- 错误提示:"无权限撤回"
|
||||
- 处方状态不变
|
||||
|
||||
---
|
||||
|
||||
### 场景4:非待双审状态撤回
|
||||
|
||||
**初始状态**:
|
||||
- 订单履约状态:履约中(2)或已完成(3)
|
||||
|
||||
**操作**:点击"撤回"按钮
|
||||
|
||||
**预期结果**:
|
||||
- 错误提示:"仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程"
|
||||
- 处方状态不变
|
||||
|
||||
---
|
||||
|
||||
### 场景5:处方不存在
|
||||
|
||||
**初始状态**:
|
||||
- 订单关联的处方已被删除
|
||||
|
||||
**操作**:点击"撤回"按钮
|
||||
|
||||
**预期结果**:
|
||||
- 订单状态:已取消(4)
|
||||
- 不会报错(因为有 `if ($prescriptionId > 0)` 判断)
|
||||
|
||||
## 边界情况处理
|
||||
|
||||
### 1. 处方ID为0或null
|
||||
|
||||
```php
|
||||
if ($prescriptionId > 0) {
|
||||
// 只有处方ID有效时才更新
|
||||
}
|
||||
```
|
||||
|
||||
**处理**:跳过处方状态更新,不报错
|
||||
|
||||
### 2. 处方已被删除
|
||||
|
||||
```php
|
||||
->whereNull('delete_time')
|
||||
```
|
||||
|
||||
**处理**:不会更新已删除的处方
|
||||
|
||||
### 3. 数据库更新失败
|
||||
|
||||
```php
|
||||
try {
|
||||
$order->save();
|
||||
// 更新处方状态
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
**处理**:捕获异常,返回错误信息
|
||||
|
||||
### 4. 事务一致性
|
||||
|
||||
**问题**:订单保存成功,但处方更新失败怎么办?
|
||||
|
||||
**当前实现**:
|
||||
- 订单和处方更新在同一个 try-catch 块中
|
||||
- 如果处方更新失败,会抛出异常
|
||||
- 异常会导致整个方法返回 false
|
||||
|
||||
**建议优化**:使用数据库事务
|
||||
|
||||
```php
|
||||
Db::startTrans();
|
||||
try {
|
||||
$order->save();
|
||||
|
||||
// 更新处方状态
|
||||
Prescription::where('id', $prescriptionId)
|
||||
->whereNull('delete_time')
|
||||
->update([...]);
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
## 相关影响
|
||||
|
||||
### 1. 处方列表页面
|
||||
|
||||
**文件**:`admin/src/views/consumer/prescription/index.vue`
|
||||
|
||||
**影响**:
|
||||
- 撤回后,处方审核状态显示为"待审核"
|
||||
- 可以重新点击"审核"按钮
|
||||
|
||||
### 2. 订单列表页面
|
||||
|
||||
**文件**:`admin/src/views/consumer/prescription/order_list.vue`
|
||||
|
||||
**影响**:
|
||||
- 撤回后,订单状态显示为"已取消"
|
||||
- "撤回"按钮消失(只有"待双审通过"状态才显示)
|
||||
|
||||
### 3. 业务流程
|
||||
|
||||
**影响**:
|
||||
- 允许撤回后重新审核处方
|
||||
- 允许重新创建订单
|
||||
- 支付单关联被清除,可以重新关联
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 权限控制
|
||||
|
||||
只有以下用户可以撤回订单:
|
||||
- 订单创建人
|
||||
- 诊单医助
|
||||
- 主管角色
|
||||
|
||||
### 2. 状态限制
|
||||
|
||||
只有"待双审通过"状态的订单可以撤回:
|
||||
- 待双审通过(1):✅ 可以撤回
|
||||
- 履约中(2):❌ 不可撤回
|
||||
- 已完成(3):❌ 不可撤回
|
||||
- 已取消(4):❌ 不可撤回
|
||||
|
||||
### 3. 数据一致性
|
||||
|
||||
撤回操作会:
|
||||
- 更新订单状态
|
||||
- 清除支付单关联
|
||||
- 重置处方审核状态
|
||||
|
||||
建议使用数据库事务确保一致性。
|
||||
|
||||
### 4. 审核历史
|
||||
|
||||
当前实现会清空审核历史(审核时间、审核人、审核意见)。
|
||||
|
||||
如果需要保留审核历史,可以考虑:
|
||||
- 不清空这些字段
|
||||
- 或者创建审核历史表记录
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 后端文件
|
||||
- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 订单逻辑
|
||||
- `server/app/common/model/tcm/Prescription.php` - 处方模型
|
||||
- `server/app/common/model/tcm/PrescriptionOrder.php` - 订单模型
|
||||
|
||||
### 前端文件
|
||||
- `admin/src/views/consumer/prescription/order_list.vue` - 订单列表
|
||||
- `admin/src/views/consumer/prescription/index.vue` - 处方列表
|
||||
|
||||
### 数据库文件
|
||||
- `server/database/migrations/2026_03_30_prescription_visible_roles_audit.sql` - 处方审核字段
|
||||
- `server/database/migrations/2026_04_07_create_tcm_prescription_order.sql` - 订单表
|
||||
|
||||
## 总结
|
||||
|
||||
✅ 撤回订单时重置处方审核状态为"待审核"
|
||||
✅ 清空审核时间、审核人、审核意见
|
||||
✅ 更新处方的 update_time
|
||||
✅ 保持订单状态为"已取消"
|
||||
✅ 清除支付单关联
|
||||
✅ 异常处理和错误提示
|
||||
|
||||
**优势**:
|
||||
- 支持撤回后重新审核
|
||||
- 状态一致性
|
||||
- 业务流程完整
|
||||
- 用户体验友好
|
||||
|
||||
**建议优化**:
|
||||
- 使用数据库事务确保一致性
|
||||
- 考虑保留审核历史记录
|
||||
- 添加操作日志
|
||||
|
||||
**下一步**:
|
||||
1. 测试撤回功能
|
||||
2. 验证处方状态变化
|
||||
3. 测试重新审核流程
|
||||
4. 考虑添加事务支持
|
||||
@@ -0,0 +1,129 @@
|
||||
# 业务订单中处方查看权限修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
药师角色(角色ID=6)在查看业务订单详情时,提示"无权限查看此处方"。
|
||||
|
||||
虽然药师有业务订单管理权限(`order_edit_all_roles` 包含角色6),但在业务订单详情中需要显示关联的处方信息时,`PrescriptionLogic::canViewPrescription()` 方法会拒绝访问。
|
||||
|
||||
## 问题原因
|
||||
|
||||
`canViewPrescription` 方法的权限检查层级为:
|
||||
1. 超级管理员
|
||||
2. 处方创建人
|
||||
3. 医助
|
||||
4. 共享处方
|
||||
5. 可见角色
|
||||
6. 有开方权限的医生(`tcm.diagnosis/kaifang`)
|
||||
|
||||
药师角色通常没有 `tcm.diagnosis/kaifang` 权限,因此无法通过权限检查。
|
||||
|
||||
## 解决方案
|
||||
|
||||
在 `canViewPrescription` 方法中添加新的权限检查层级:
|
||||
|
||||
**有业务订单管理权限的角色(如药师)可以查看处方(只读)**
|
||||
|
||||
这样药师可以在业务订单中查看关联的处方信息,以便进行订单审核和管理。
|
||||
|
||||
## 代码修改
|
||||
|
||||
文件:`server/app/adminapi/logic/tcm/PrescriptionLogic.php`
|
||||
|
||||
```php
|
||||
public static function canViewPrescription($row, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
// 超级管理员
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建人
|
||||
$creatorId = is_array($row) ? (int) ($row['creator_id'] ?? 0) : (int) $row->creator_id;
|
||||
if ($creatorId === $adminId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 医助
|
||||
$assistantId = is_array($row) ? (int) ($row['assistant_id'] ?? 0) : (int) ($row->assistant_id ?? 0);
|
||||
if ($assistantId > 0 && $assistantId === $adminId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 共享处方
|
||||
$isShared = is_array($row) ? (int) ($row['is_shared'] ?? 0) : (int) $row->is_shared;
|
||||
if ($isShared === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 可见角色
|
||||
$vis = is_array($row) ? (string) ($row['visible_role_ids'] ?? '') : (string) ($row->visible_role_ids ?? '');
|
||||
$allowed = array_filter(array_map('intval', explode(',', $vis)));
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
if (count(array_intersect($myRoles, $allowed)) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 有开方权限的医生可以查看所有处方(只读)
|
||||
$permissions = $adminInfo['permissions'] ?? [];
|
||||
if (in_array('tcm.diagnosis/kaifang', $permissions, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 【新增】有业务订单管理权限的角色(如药师)可以查看处方(只读)
|
||||
$orderEditAllRoles = Config::get('project.order_edit_all_roles', [0, 3]);
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
$allowedRoles = array_map('intval', is_array($orderEditAllRoles) ? $orderEditAllRoles : []);
|
||||
if (count(array_intersect($myRoles, $allowedRoles)) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
## 权限层级(更新后)
|
||||
|
||||
1. 超级管理员(root=1)
|
||||
2. 处方创建人
|
||||
3. 医助
|
||||
4. 共享处方(is_shared=1)
|
||||
5. 可见角色(visible_role_ids)
|
||||
6. 有开方权限的医生(`tcm.diagnosis/kaifang`)
|
||||
7. **有业务订单管理权限的角色(`order_edit_all_roles`)** ← 新增
|
||||
|
||||
## 配置文件
|
||||
|
||||
文件:`server/config/project.php`
|
||||
|
||||
```php
|
||||
// 医助创建订单权限:拥有以下角色ID的用户可修改任意订单,其他用户只能修改自己创建的订单
|
||||
'order_edit_all_roles' => [0, 3, 6],
|
||||
```
|
||||
|
||||
- 角色 0:超级管理员
|
||||
- 角色 3:管理员
|
||||
- 角色 6:药师
|
||||
|
||||
## 使用场景
|
||||
|
||||
药师角色现在可以:
|
||||
1. 查看业务订单列表
|
||||
2. 打开业务订单详情
|
||||
3. 查看订单中关联的处方信息(只读)
|
||||
4. 进行处方审核
|
||||
5. 进行支付单审核
|
||||
6. 编辑业务订单信息
|
||||
|
||||
## 测试验证
|
||||
|
||||
1. 使用药师账号登录
|
||||
2. 进入"消费者处方订单"列表
|
||||
3. 点击任意订单的"详情"按钮
|
||||
4. 确认可以正常查看处方信息,不再提示"无权限查看此处方"
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 此权限为只读权限,药师不能编辑处方内容
|
||||
2. 药师只能通过业务订单查看处方,不能直接在处方列表中查看其他医生的处方
|
||||
3. 修改代码后需要清除缓存:`php think clear`
|
||||
@@ -729,11 +729,14 @@ const { proxy } = getCurrentInstance()
|
||||
|
||||
if(options.msg==1){
|
||||
console.log('msg我进来了',options.msg)
|
||||
await sendConfirmationMessage(); // 进入聊天时自动发送知情确认
|
||||
await waitForIMReady(); // 确保 SDK 网络就绪后再发消息
|
||||
await sendConfirmationMessage(); // 进入聊天时自动发送知情确认
|
||||
}
|
||||
loadDoctorInfo(); // 加载医生卡片信息(对方为医生时)
|
||||
uni.$on('TUICall:callStart', onVideoCallStart);
|
||||
uni.$on('TUICall:callEnd', onVideoCallEnd);
|
||||
// 提前申请音视频权限,避免来电接通时才弹「去设置」提示
|
||||
requestAVPermissions({ silent: true, scene: 'onLoad' });
|
||||
//uni.showToast({ title: '聊天已就绪', icon: 'success' });
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -1136,18 +1139,25 @@ const { proxy } = getCurrentInstance()
|
||||
}
|
||||
}
|
||||
|
||||
/** 等待 IM SDK 网络握手完成,给 SDK 一段时间确保就绪后再发消息 */
|
||||
function waitForIMReady() {
|
||||
// 使用固定延迟代替事件监听:SDK_READY 事件可能在注册前已触发,
|
||||
// 事件监听会等满 maxMs 超时,在微信小程序中引发 "Error: timeout"。
|
||||
return new Promise((resolve) => setTimeout(resolve, 800));
|
||||
}
|
||||
|
||||
async function sendConfirmationMessage() {
|
||||
const alreadySent = messages.value.some(
|
||||
(m) => m.type === 'confirmation' || (m.type === 'text' && isConfirmationMessageText(m.text))
|
||||
);
|
||||
if (alreadySent) return;
|
||||
try {
|
||||
const message = chat.createTextMessage({
|
||||
to: targetUserID,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
payload: { text: CONFIRMATION_MESSAGE }
|
||||
});
|
||||
await chat.sendMessage(message);
|
||||
|
||||
const pushSent = () => {
|
||||
// 防止重复 push(relogin 后重试时再次检查)
|
||||
const already = messages.value.some(
|
||||
(m) => m.type === 'confirmation' || (m.type === 'text' && isConfirmationMessageText(m.text))
|
||||
);
|
||||
if (already) return;
|
||||
messages.value.push({
|
||||
type: 'confirmation',
|
||||
text: CONFIRMATION_MESSAGE,
|
||||
@@ -1157,8 +1167,27 @@ const { proxy } = getCurrentInstance()
|
||||
});
|
||||
scrollToBottom();
|
||||
setTimeout(() => scrollToBottom(), 150);
|
||||
};
|
||||
const doSend = async () => {
|
||||
const message = chat.createTextMessage({
|
||||
to: targetUserID,
|
||||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||||
payload: { text: CONFIRMATION_MESSAGE }
|
||||
});
|
||||
await chat.sendMessage(message);
|
||||
};
|
||||
try {
|
||||
await doSend();
|
||||
pushSent();
|
||||
} catch (error) {
|
||||
console.error('发送确认信息失败:', error);
|
||||
console.error('发送确认信息失败,尝试重新登录后重试:', error);
|
||||
try {
|
||||
await reloginIMChat();
|
||||
await doSend();
|
||||
pushSent();
|
||||
} catch (e2) {
|
||||
console.error('重登后确认信息仍发送失败:', e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1335,7 +1364,78 @@ const { proxy } = getCurrentInstance()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请摄像头 + 麦克风权限。
|
||||
* silent=true 时仅静默尝试,不弹引导框(用于页面加载时提前申请)。
|
||||
* silent=false 时若被拒绝主动引导用户去设置页开启。
|
||||
* scene 标识触发场景(makeCall / incoming),用于日志分析。
|
||||
*/
|
||||
// #ifdef MP-WEIXIN
|
||||
function requestAVPermissions({ silent = false, scene = 'unknown' } = {}) {
|
||||
/** 上报权限被拒日志到后端 */
|
||||
const logDenied = (deniedScope, action) => {
|
||||
try {
|
||||
const wxVersion = wx.getSystemInfoSync?.()?.SDKVersion || '';
|
||||
proxy.apiUrl({
|
||||
url: '/api/chat/logAvPermission',
|
||||
method: 'POST',
|
||||
data: {
|
||||
patient_id: currentUserID || '',
|
||||
doctor_id: targetUserID || '',
|
||||
denied_scope: deniedScope,
|
||||
scene,
|
||||
action,
|
||||
wx_version: wxVersion
|
||||
}
|
||||
}, false).catch(() => {});
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const guideToSetting = (deniedScope) => {
|
||||
if (silent) {
|
||||
// 静默模式:仅记录被拒,不弹弹窗
|
||||
logDenied(deniedScope, 'silent_denied');
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '需要摄像头和麦克风权限',
|
||||
content: '视频通话需要摄像头和麦克风权限,请点击「去设置」开启后重试',
|
||||
confirmText: '去设置',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
logDenied(deniedScope, 'open_setting');
|
||||
wx.openSetting();
|
||||
} else {
|
||||
// 用户选择「取消」,记录日志
|
||||
logDenied(deniedScope, 'cancel');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
wx.authorize({
|
||||
scope: 'scope.camera',
|
||||
success: () => {
|
||||
wx.authorize({
|
||||
scope: 'scope.record',
|
||||
success: () => resolve(true),
|
||||
fail: () => { guideToSetting('record'); resolve(false); }
|
||||
});
|
||||
},
|
||||
fail: () => { guideToSetting('camera'); resolve(false); }
|
||||
});
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
function requestAVPermissions() { return Promise.resolve(true); }
|
||||
// #endif
|
||||
|
||||
async function makeVideoCall() {
|
||||
// 发起通话前先检查权限,若被拒引导去设置
|
||||
const granted = await requestAVPermissions({ silent: false, scene: 'makeCall' });
|
||||
if (!granted) return;
|
||||
try {
|
||||
hideDoctorCardByCall();
|
||||
if (!uni.CallManager) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import App from './App'
|
||||
var baseUrl ='https://capi.zzzhengyangtang.cn/';
|
||||
var baseUrl ='https://admin.zhenyangtang.com.cn/';
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
|
||||
@@ -625,11 +625,47 @@ const getLocalDiagnosisText = () => {
|
||||
return String(val) + '、';
|
||||
};
|
||||
|
||||
// ---- 音视频权限辅助(仅微信小程序) ----
|
||||
// #ifdef MP-WEIXIN
|
||||
const requestAVPermissionsInModal = () => new Promise((resolve) => {
|
||||
const guide = (deniedScope) => {
|
||||
uni.showModal({
|
||||
title: '需要摄像头和麦克风权限',
|
||||
content: '视频问诊需要摄像头和麦克风权限,请点击「去设置」开启后重试',
|
||||
confirmText: '去设置',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.openSetting();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
wx.authorize({
|
||||
scope: 'scope.camera',
|
||||
success: () => {
|
||||
wx.authorize({
|
||||
scope: 'scope.record',
|
||||
success: () => resolve(true),
|
||||
fail: () => guide('record')
|
||||
});
|
||||
},
|
||||
fail: () => guide('camera')
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
const requestAVPermissionsInModal = () => Promise.resolve(true);
|
||||
// #endif
|
||||
|
||||
// 关闭就诊人信息弹窗
|
||||
const closePatientModal = async () => {
|
||||
if (confirmBtnLoading.value) return;
|
||||
confirmBtnLoading.value = true;
|
||||
try {
|
||||
// 在用户主动点击的时机申请摄像头 + 麦克风权限
|
||||
// (首次会弹原生授权弹窗;已拒绝则引导去设置页)
|
||||
const granted = await requestAVPermissionsInModal();
|
||||
if (!granted) return; // 未授权,停止跳转
|
||||
const app = getApp();
|
||||
// const userID = app.globalData.userID;
|
||||
// const userSig = app.globalData.userSig;
|
||||
|
||||
@@ -284,6 +284,45 @@ export function prescriptionOrderAuditPayment(params: {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/auditPayment', params })
|
||||
}
|
||||
|
||||
/** 撤回处方审核 */
|
||||
export function prescriptionOrderRevokeRxAudit(params: { id: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/revokeRxAudit', params })
|
||||
}
|
||||
|
||||
/** 撤回支付单审核 */
|
||||
export function prescriptionOrderRevokePayAudit(params: { id: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/revokePayAudit', params })
|
||||
}
|
||||
|
||||
/** 确认发货:将 fulfillment_status 从 2(履约中)推进到 5(已发货) */
|
||||
export function prescriptionOrderShip(params: {
|
||||
id: number
|
||||
express_company: string
|
||||
tracking_number: string
|
||||
}) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/ship', params })
|
||||
}
|
||||
|
||||
/** 为「已发货」订单新增支付单并重置支付审核为待审核 */
|
||||
export function prescriptionOrderAddPayOrder(params: {
|
||||
id: number
|
||||
order_type: number
|
||||
pay_amount: number
|
||||
pay_remark?: string
|
||||
}) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/addPayOrder', params })
|
||||
}
|
||||
|
||||
/** 将「已发货」且支付审核通过的订单标记为「已完成」 */
|
||||
export function prescriptionOrderComplete(params: { id: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/complete', params })
|
||||
}
|
||||
|
||||
/** 订单操作日志 */
|
||||
export function prescriptionOrderLogs(params: { id: number }) {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/logs', params })
|
||||
}
|
||||
|
||||
// ========== 处方库 ==========
|
||||
|
||||
// 处方库列表
|
||||
|
||||
@@ -635,6 +635,9 @@ const showLibraryDialog = ref(false)
|
||||
const libraryLoading = ref(false)
|
||||
const libraryList = ref<any[]>([])
|
||||
const librarySearchName = ref('')
|
||||
|
||||
// 防止重复打开
|
||||
const isOpening = ref(false)
|
||||
const libraryPage = ref(1)
|
||||
const libraryPageSize = ref(15)
|
||||
const libraryTotal = ref(0)
|
||||
@@ -785,78 +788,103 @@ const buildClinicalDiagnosis = (diagnosis: any) => {
|
||||
}
|
||||
|
||||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||||
if (options?.stationName) templateConfig.stationName = options.stationName
|
||||
|
||||
const diagnosis = data.diagnosis || data
|
||||
const diagnosisId = data.diagnosis_id ?? diagnosis?.id ?? data.id
|
||||
if (!diagnosisId) {
|
||||
feedback.msgWarning('缺少诊单信息')
|
||||
// 防止重复打开
|
||||
if (isOpening.value) {
|
||||
console.warn('处方正在打开中,请勿重复点击')
|
||||
return
|
||||
}
|
||||
|
||||
isOpening.value = true
|
||||
|
||||
try {
|
||||
if (options?.stationName) templateConfig.stationName = options.stationName
|
||||
|
||||
await loadDictOptions()
|
||||
|
||||
const appointmentId = Number(data.id ?? data.appointment_id ?? 0)
|
||||
if (appointmentId) {
|
||||
try {
|
||||
const existing = await prescriptionGetByAppointment({ appointment_id: appointmentId })
|
||||
if (existing?.id) {
|
||||
const detail = await prescriptionDetail({ id: existing.id })
|
||||
savedPrescription.value = detail
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 获取详细诊单作为病历快照(每次开方保存一份)
|
||||
let caseRecord = data.case_record && Object.keys(data.case_record).length > 0 ? data.case_record : null
|
||||
if (!caseRecord && diagnosisId) {
|
||||
try {
|
||||
const res = await tcmDiagnosisDetail({ id: Number(diagnosisId) })
|
||||
caseRecord = res && typeof res === 'object' ? res : null
|
||||
} catch (e) {
|
||||
console.warn('获取诊单详情失败:', e)
|
||||
const diagnosis = data.diagnosis || data
|
||||
const diagnosisId = data.diagnosis_id ?? diagnosis?.id ?? data.id
|
||||
if (!diagnosisId) {
|
||||
feedback.msgWarning('缺少诊单信息')
|
||||
return
|
||||
}
|
||||
|
||||
await loadDictOptions()
|
||||
|
||||
const appointmentId = Number(data.id ?? data.appointment_id ?? 0)
|
||||
if (appointmentId) {
|
||||
try {
|
||||
|
||||
const existing = await prescriptionGetByAppointment({ appointment_id: appointmentId })
|
||||
console.log('-----------',existing)
|
||||
if (existing?.id) {
|
||||
const detail = await prescriptionDetail({ id: existing.id })
|
||||
savedPrescription.value = detail
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 获取详细诊单作为病历快照(每次开方保存一份)
|
||||
let caseRecord = data.case_record && Object.keys(data.case_record).length > 0 ? data.case_record : null
|
||||
if (!caseRecord && diagnosisId) {
|
||||
try {
|
||||
const res = await tcmDiagnosisDetail({ id: Number(diagnosisId) })
|
||||
caseRecord = res && typeof res === 'object' ? res : null
|
||||
} catch (e) {
|
||||
console.warn('获取诊单详情失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
formData.diagnosis_id = Number(diagnosisId)
|
||||
formData.appointment_id = appointmentId
|
||||
formData.prescription_name = ''
|
||||
formData.prescription_type = '浓缩水丸'
|
||||
formData.patient_name = data.patient_name || diagnosis.patient_name || ''
|
||||
formData.gender = data.patient_gender ?? diagnosis.gender ?? 0
|
||||
formData.gender_desc = formData.gender === 1 ? '男' : '女'
|
||||
formData.age = data.patient_age ?? diagnosis.age ?? 0
|
||||
formData.phone = data.patient_phone || diagnosis.phone || ''
|
||||
formData.visit_no = data.id ? `1K${String(data.id).padStart(8, '0')}` : ''
|
||||
formData.prescription_date = new Date().toISOString().slice(0, 10)
|
||||
formData.pulse = diagnosis.pulse || ''
|
||||
formData.tongue = diagnosis.tongue_coating || diagnosis.tongue || ''
|
||||
formData.tongue_image = ''
|
||||
formData.pulse_condition = ''
|
||||
formData.clinical_diagnosis = buildClinicalDiagnosis(diagnosis)
|
||||
formData.case_record = caseRecord
|
||||
formData.herbs = []
|
||||
formData.dose_count = 1
|
||||
formData.dose_unit = '剂'
|
||||
formData.usage_days = 7
|
||||
formData.usage_instruction = '水煎服,一日二次'
|
||||
formData.usage_time = '饭前'
|
||||
formData.usage_way = '温水送服'
|
||||
formData.dietary_taboo = []
|
||||
formData.usage_notes = ''
|
||||
formData.amount = 0
|
||||
formData.doctor_name = userStore.userInfo?.name || ''
|
||||
formData.doctor_signature = ''
|
||||
formData.is_shared = 0
|
||||
|
||||
savedPrescription.value = null
|
||||
visible.value = true
|
||||
setTimeout(() => initSignatureCanvas(), 100)
|
||||
} finally {
|
||||
// 延迟重置,避免快速连续点击
|
||||
setTimeout(() => {
|
||||
isOpening.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
formData.diagnosis_id = Number(diagnosisId)
|
||||
formData.appointment_id = appointmentId
|
||||
formData.prescription_name = ''
|
||||
formData.prescription_type = '浓缩水丸'
|
||||
formData.patient_name = data.patient_name || diagnosis.patient_name || ''
|
||||
formData.gender = data.patient_gender ?? diagnosis.gender ?? 0
|
||||
formData.gender_desc = formData.gender === 1 ? '男' : '女'
|
||||
formData.age = data.patient_age ?? diagnosis.age ?? 0
|
||||
formData.phone = data.patient_phone || diagnosis.phone || ''
|
||||
formData.visit_no = data.id ? `1K${String(data.id).padStart(8, '0')}` : ''
|
||||
formData.prescription_date = new Date().toISOString().slice(0, 10)
|
||||
formData.pulse = diagnosis.pulse || ''
|
||||
formData.tongue = diagnosis.tongue_coating || diagnosis.tongue || ''
|
||||
formData.tongue_image = ''
|
||||
formData.pulse_condition = ''
|
||||
formData.clinical_diagnosis = buildClinicalDiagnosis(diagnosis)
|
||||
formData.case_record = caseRecord
|
||||
formData.herbs = []
|
||||
formData.dose_count = 1
|
||||
formData.dose_unit = '剂'
|
||||
formData.usage_days = 7
|
||||
formData.usage_instruction = '水煎服,一日二次'
|
||||
formData.usage_time = '饭前'
|
||||
formData.usage_way = '温水送服'
|
||||
formData.dietary_taboo = []
|
||||
formData.usage_notes = ''
|
||||
formData.amount = 0
|
||||
formData.doctor_name = userStore.userInfo?.name || ''
|
||||
formData.doctor_signature = ''
|
||||
formData.is_shared = 0
|
||||
|
||||
savedPrescription.value = null
|
||||
visible.value = true
|
||||
setTimeout(() => initSignatureCanvas(), 100)
|
||||
}
|
||||
|
||||
const openById = async (prescriptionId: number) => {
|
||||
// 防止重复打开
|
||||
if (isOpening.value) {
|
||||
console.warn('处方正在打开中,请勿重复点击')
|
||||
return
|
||||
}
|
||||
|
||||
isOpening.value = true
|
||||
|
||||
try {
|
||||
const res = await prescriptionDetail({ id: prescriptionId })
|
||||
savedPrescription.value = res
|
||||
@@ -866,6 +894,11 @@ const openById = async (prescriptionId: number) => {
|
||||
visible.value = true
|
||||
} catch (e) {
|
||||
feedback.msgError('加载处方失败')
|
||||
} finally {
|
||||
// 延迟重置,避免快速连续点击
|
||||
setTimeout(() => {
|
||||
isOpening.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,16 +99,25 @@
|
||||
|
||||
<div class="mt-3">
|
||||
<el-table :data="pager.lists" size="default" stripe>
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="处方编号" prop="sn" min-width="140" />
|
||||
<el-table-column label="类型" prop="prescription_type" min-width="100" />
|
||||
<el-table-column label="患者姓名" prop="patient_name" min-width="100" />
|
||||
<el-table-column label="性别" min-width="60">
|
||||
<el-table-column label="处方编号" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === 1 ? '男' : '女' }}
|
||||
<span class="font-medium">{{ row.sn || '—' }}</span>
|
||||
<div class="text-xs text-gray-400 mt-0.5">ID: {{ row.id }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处方类型" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" size="small">{{ row.prescription_type || '—' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="患者信息" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<div class="font-medium">{{ row.patient_name || '—' }}</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5">
|
||||
{{ row.gender === 1 ? '男' : (row.gender === 0 ? '女' : '未知') }} · {{ row.age ? row.age + '岁' : '—' }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="年龄" prop="age" min-width="60" />
|
||||
|
||||
|
||||
|
||||
@@ -139,15 +148,19 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作废" min-width="80">
|
||||
<el-table-column label="作废" min-width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.void_status === 1" type="danger" size="small">已作废</el-tag>
|
||||
<el-tag v-if="row.void_status === 1" type="danger" size="small">作废</el-tag>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="医师" prop="doctor_name" min-width="100" />
|
||||
<el-table-column label="处方日期" prop="prescription_date" min-width="120" />
|
||||
<el-table-column label="创建时间" min-width="170">
|
||||
<el-table-column label="医师信息" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div class="font-medium">{{ row.doctor_name || '—' }}</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5">{{ row.prescription_date || '—' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatListCreateTime(row.create_time) }}
|
||||
</template>
|
||||
@@ -167,7 +180,7 @@
|
||||
创建订单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!isApprovedActivePrescription(row) || Number(row.business_prescription_audit_rejected) === 1"
|
||||
v-if="!isApprovedActivePrescription(row)"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
@@ -203,12 +216,12 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑弹窗(查看 = 处方笺样式) -->
|
||||
<el-dialog
|
||||
<!-- 新增/编辑/查看抽屉 -->
|
||||
<el-drawer
|
||||
v-model="showEdit"
|
||||
:title="editMode === 'add' ? '新增处方' : editMode === 'edit' ? '编辑处方' : '查看处方'"
|
||||
:width="editMode === 'view' ? '920px' : '900px'"
|
||||
class="cf-prescription-dialog"
|
||||
:size="editMode === 'view' ? '920px' : '860px'"
|
||||
class="cf-prescription-drawer"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="onEditDialogClosed"
|
||||
@@ -695,182 +708,335 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 从消费者处方创建业务订单(zyt_tcm_prescription_order,与支付单 zyt_order 分离) -->
|
||||
<el-dialog
|
||||
v-model="createOrderVisible"
|
||||
title="创建业务订单"
|
||||
width="640px"
|
||||
width="850px"
|
||||
:close-on-click-modal="false"
|
||||
@close="resetCreateOrderForm"
|
||||
@closed="resetCreateOrderForm"
|
||||
class="create-order-dialog"
|
||||
>
|
||||
<div v-if="createOrderPrescription" class="mb-4 rounded bg-gray-50 px-3 py-2 text-sm text-gray-700">
|
||||
<div class="font-medium text-gray-900">关联消费者处方</div>
|
||||
<div>
|
||||
处方 ID {{ createOrderPrescription.id }}
|
||||
<span v-if="createOrderPrescription.sn"> · 编号 {{ createOrderPrescription.sn }}</span>
|
||||
· {{ createOrderPrescription.patient_name || '—' }}
|
||||
</div>
|
||||
<div class="text-gray-500 mt-1 line-clamp-2">
|
||||
{{ createOrderHerbSummary }}
|
||||
<!-- 关联处方信息卡片 -->
|
||||
<div v-if="createOrderPrescription" class="mb-4 rounded-lg border border-blue-100 bg-blue-50 p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-xs font-medium text-blue-900 mb-0.5">关联处方</div>
|
||||
<div class="text-xs text-blue-700">
|
||||
ID {{ createOrderPrescription.id }}
|
||||
<span v-if="createOrderPrescription.sn"> · {{ createOrderPrescription.sn }}</span>
|
||||
· {{ createOrderPrescription.patient_name || '—' }}
|
||||
</div>
|
||||
<div class="text-xs text-blue-600 mt-1 line-clamp-1">
|
||||
{{ createOrderHerbSummary }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="createOrderFormRef"
|
||||
:model="createOrderForm"
|
||||
:rules="createOrderRules"
|
||||
label-width="120px"
|
||||
label-width="90px"
|
||||
class="create-order-form"
|
||||
>
|
||||
<el-divider content-position="left">患者与收货</el-divider>
|
||||
<el-form-item label="诊单患者" prop="patient_id">
|
||||
<el-select
|
||||
v-model="createOrderForm.patient_id"
|
||||
class="w-full"
|
||||
placeholder="搜索姓名/手机/身份证"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchPatientsForOrder"
|
||||
:loading="createOrderPatientLoading"
|
||||
@change="onCreateOrderPatientChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in createOrderPatientList"
|
||||
:key="item.id"
|
||||
:label="`${item.patient_name} (${item.phone || '无手机'})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="收货人" prop="recipient_name">
|
||||
<el-input v-model="createOrderForm.recipient_name" placeholder="可与诊单姓名一致" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收货手机" prop="recipient_phone">
|
||||
<el-input v-model="createOrderForm.recipient_phone" placeholder="用于物流联系与单号匹配" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收货地址" prop="shipping_address">
|
||||
<el-input
|
||||
v-model="createOrderForm.shipping_address"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="完整收货地址"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">服务信息</el-divider>
|
||||
<el-form-item label="是否复诊">
|
||||
<el-switch v-model="createOrderForm.is_follow_up" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用药疗程(天)">
|
||||
<el-input-number
|
||||
v-model="createOrderForm.medication_days"
|
||||
:min="1"
|
||||
:max="999"
|
||||
:step="1"
|
||||
placeholder="天数"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上次医生/医助">
|
||||
<el-input v-model="createOrderForm.prev_staff" placeholder="可选" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务渠道">
|
||||
<el-input v-model="createOrderForm.service_channel" placeholder="可选" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务套餐">
|
||||
<el-input v-model="createOrderForm.service_package" placeholder="可选" maxlength="100" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">物流</el-divider>
|
||||
<el-form-item label="快递公司">
|
||||
<el-select v-model="createOrderForm.express_company" placeholder="查物流用" class="w-full">
|
||||
<el-option label="自动识别" value="auto" />
|
||||
<el-option label="顺丰速运" value="sf" />
|
||||
<el-option label="京东快递" value="jd" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="快递单号">
|
||||
<el-input v-model="createOrderForm.tracking_number" placeholder="可创建时填写或发货前补录" maxlength="80" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">关联收款</el-divider>
|
||||
<el-form-item label="已支付单">
|
||||
<el-select
|
||||
v-model="createOrderForm.pay_order_ids"
|
||||
class="w-full"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="多选本诊单下已支付收款单"
|
||||
:loading="createOrderPaidOrdersLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="o in createOrderPaidOrders"
|
||||
:key="o.id"
|
||||
:label="`${o.order_no} · ¥${o.amount}`"
|
||||
:value="o.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
与「订单管理」中已支付订单一致;主管或本诊单医助可见该诊单全部已支付单,否则仅本人创建的收款单。已被其他有效业务订单占用的支付单不会出现在此列表。业务订单撤回后,本处方可再次创建订单并重新关联支付单。
|
||||
<!-- 患者与收货信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span>患者与收货</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="section-content">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="诊单患者" prop="patient_id">
|
||||
<el-select
|
||||
v-model="createOrderForm.patient_id"
|
||||
class="w-full"
|
||||
placeholder="搜索姓名/手机/身份证"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchPatientsForOrder"
|
||||
:loading="createOrderPatientLoading"
|
||||
@change="onCreateOrderPatientChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in createOrderPatientList"
|
||||
:key="item.id"
|
||||
:label="`${item.patient_name} (${item.phone || '无手机'})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收货人" prop="recipient_name">
|
||||
<el-input v-model="createOrderForm.recipient_name" placeholder="收货人姓名" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收货手机" prop="recipient_phone">
|
||||
<el-input v-model="createOrderForm.recipient_phone" placeholder="用于物流联系" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="收货地址" prop="shipping_address">
|
||||
<el-input
|
||||
v-model="createOrderForm.shipping_address"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="完整收货地址"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">金额</el-divider>
|
||||
<el-form-item label="费用类别" prop="fee_type">
|
||||
<el-select v-model="createOrderForm.fee_type" placeholder="请选择" class="w-full">
|
||||
<el-option label="挂号费" :value="1" />
|
||||
<el-option label="问诊费" :value="2" />
|
||||
<el-option label="药品费用" :value="3" />
|
||||
<el-option label="首付费用" :value="4" />
|
||||
<el-option label="尾款费用" :value="5" />
|
||||
<el-option label="其他费用" :value="6" />
|
||||
<el-option label="全部费用" :value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单金额" prop="amount">
|
||||
<el-input-number
|
||||
v-model="createOrderForm.amount"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
class="w-full"
|
||||
placeholder="业务订单金额(元)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-perms="['finance.account_log/lists']"
|
||||
label="内部成本"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="createOrderForm.internal_cost"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
class="w-full"
|
||||
placeholder="仅财务角色写入库表"
|
||||
/>
|
||||
<div class="text-xs text-gray-400 mt-1">写入业务订单表字段;无财务权限时后端不会保存该值。</div>
|
||||
</el-form-item>
|
||||
<!-- 服务信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<span>服务信息</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否复诊">
|
||||
<el-switch v-model="createOrderForm.is_follow_up" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="用药疗程">
|
||||
<el-input-number
|
||||
v-model="createOrderForm.medication_days"
|
||||
:min="1"
|
||||
:max="999"
|
||||
:step="1"
|
||||
placeholder="天数"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="上次医生">
|
||||
<el-input v-model="createOrderForm.prev_staff" placeholder="可选" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服务渠道">
|
||||
<el-input v-model="createOrderForm.service_channel" placeholder="可选" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服务套餐">
|
||||
<el-input v-model="createOrderForm.service_package" placeholder="可选" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item label="备注补充">
|
||||
<el-input
|
||||
v-model="createOrderForm.remark_extra"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="其他说明(可选)"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 物流信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<span>物流信息</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="快递公司">
|
||||
<el-select v-model="createOrderForm.express_company" placeholder="查物流用" class="w-full">
|
||||
<el-option label="自动识别" value="auto" />
|
||||
<el-option label="顺丰速运" value="sf" />
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="快递单号">
|
||||
<el-input v-model="createOrderForm.tracking_number" placeholder="可创建后补录" maxlength="80" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联收款 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<span>关联收款</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<el-form-item label="已支付单">
|
||||
<el-select
|
||||
v-model="createOrderForm.pay_order_ids"
|
||||
class="w-full"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="多选本诊单下已支付收款单"
|
||||
:loading="createOrderPaidOrdersLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="o in createOrderPaidOrders"
|
||||
:key="o.id"
|
||||
:label="`${o.order_no} · ¥${o.amount} · ${formatPoCategory(o.order_type)}`"
|
||||
:value="o.id"
|
||||
class="!h-auto py-1.5"
|
||||
>
|
||||
<div class="flex flex-col gap-0.5" style="line-height: normal;">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-[13px] text-gray-800">{{ o.order_no }}</span>
|
||||
<span class="text-orange-500 font-medium ml-4">¥{{ o.amount }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-gray-400 mt-1">
|
||||
<span class="truncate pr-2">{{ formatPoCategory(o.order_type) }}<template v-if="o.remark"> · {{ o.remark }}</template></span>
|
||||
<span class="shrink-0">{{ String(o.create_time || '').substring(0, 16) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-500 mt-1.5 leading-relaxed">
|
||||
主管或本诊单医助可见该诊单全部已支付单,否则仅本人创建的收款单。已被其他有效业务订单占用的支付单不会出现在此列表。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 金额信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>金额信息</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<!-- 金额概览卡片 -->
|
||||
<div class="mb-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-0.5">定金门槛</div>
|
||||
<div class="text-base font-semibold text-gray-900">
|
||||
¥{{ formatPoDepositMoney(createOrderDepositMin) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-0.5">
|
||||
{{ createOrderDepositMin > 0 ? '订单和支付单总金额须 ≥ 此值' : '未设置' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-0.5">已选收款单</div>
|
||||
<div class="text-base font-semibold" :class="createOrderLinkedPaidTotal >= createOrderDepositMin ? 'text-green-600' : 'text-red-600'">
|
||||
¥{{ formatPoDepositMoney(createOrderLinkedPaidTotal) }}
|
||||
</div>
|
||||
<div class="text-xs mt-0.5" :class="createOrderLinkedPaidTotal >= createOrderDepositMin ? 'text-green-600' : 'text-red-600'">
|
||||
{{ createOrderLinkedPaidTotal >= createOrderDepositMin ? '✓ 已达标' : '✗ 未达标' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="费用类别" prop="fee_type">
|
||||
<el-select v-model="createOrderForm.fee_type" placeholder="请选择" class="w-full">
|
||||
<el-option label="挂号费" :value="1" />
|
||||
<el-option label="问诊费" :value="2" />
|
||||
<el-option label="药品费用" :value="3" />
|
||||
<el-option label="首付费用" :value="4" />
|
||||
<el-option label="尾款费用" :value="5" />
|
||||
<el-option label="其他费用" :value="6" />
|
||||
<el-option label="全部费用" :value="7" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="订单金额" prop="amount">
|
||||
<el-input-number
|
||||
v-model="createOrderForm.amount"
|
||||
:min="createOrderAmountInputMin"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
class="w-full"
|
||||
placeholder="业务订单金额(元)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-perms="['finance.account_log/lists']" :span="12">
|
||||
<el-form-item label="内部成本">
|
||||
<el-input-number
|
||||
v-model="createOrderForm.internal_cost"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
class="w-full"
|
||||
placeholder="仅财务角色可见"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
||||
</svg>
|
||||
<span>备注说明</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<el-form-item label="备注补充">
|
||||
<el-input
|
||||
v-model="createOrderForm.remark_extra"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="其他说明(可选)"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="createOrderVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createOrderSubmitLoading" @click="submitCreateOrderFromPrescription">
|
||||
创建业务订单
|
||||
</el-button>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs text-gray-500">
|
||||
<span v-if="createOrderDepositMin > 0" class="text-amber-600">
|
||||
提示:订单金额和关联支付单总金额须 ≥ ¥{{ formatPoDepositMoney(createOrderDepositMin) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button @click="createOrderVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createOrderSubmitLoading" @click="submitCreateOrderFromPrescription">
|
||||
创建业务订单
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -918,7 +1084,7 @@ import useUserStore from '@/stores/modules/user'
|
||||
import feedback from '@/utils/feedback'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
/** 与 server/config/project.php prescription_audit_roles 保持一致 */
|
||||
@@ -979,8 +1145,27 @@ const createOrderFormRef = ref<FormInstance>()
|
||||
const createOrderSubmitLoading = ref(false)
|
||||
const createOrderPatientLoading = ref(false)
|
||||
const createOrderPatientList = ref<any[]>([])
|
||||
const createOrderPaidOrders = ref<Array<{ id: number; order_no?: string; amount?: number | string }>>([])
|
||||
const createOrderPaidOrders = ref<Array<{ id: number; order_no?: string; amount?: number | string; order_type?: number | string; remark?: string; create_time?: string }>>([])
|
||||
const createOrderPaidOrdersLoading = ref(false)
|
||||
const createOrderDepositMin = ref(0)
|
||||
|
||||
function formatPoDepositMoney(v: unknown) {
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return '—'
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
function formatPoCategory(t: unknown) {
|
||||
const m: Record<number, string> = {
|
||||
1: '挂号费',
|
||||
2: '问诊费',
|
||||
3: '药品费用',
|
||||
4: '首付',
|
||||
5: '尾款',
|
||||
6: '其他'
|
||||
}
|
||||
return m[Number(t)] || '未知'
|
||||
}
|
||||
|
||||
const createOrderForm = reactive({
|
||||
patient_id: '' as number | string,
|
||||
@@ -1001,13 +1186,46 @@ const createOrderForm = reactive({
|
||||
pay_order_ids: [] as number[]
|
||||
})
|
||||
|
||||
const createOrderLinkedPaidTotal = computed(() => {
|
||||
const ids = new Set(createOrderForm.pay_order_ids.map((x) => Number(x)))
|
||||
let s = 0
|
||||
for (const o of createOrderPaidOrders.value) {
|
||||
if (ids.has(Number(o.id))) {
|
||||
s += Number(o.amount) || 0
|
||||
}
|
||||
}
|
||||
return Math.round(s * 100) / 100
|
||||
})
|
||||
|
||||
const createOrderAmountInputMin = computed(() => {
|
||||
const m = createOrderDepositMin.value
|
||||
if (m > 0) {
|
||||
return Math.round(m * 100) / 100
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const createOrderRules: FormRules = {
|
||||
patient_id: [{ required: true, message: '请选择诊单患者', trigger: 'change' }],
|
||||
recipient_name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
||||
recipient_phone: [{ required: true, message: '请输入收货手机', trigger: 'blur' }],
|
||||
shipping_address: [{ required: true, message: '请输入收货地址', trigger: 'blur' }],
|
||||
fee_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入订单金额', trigger: 'blur' }]
|
||||
amount: [
|
||||
{ required: true, message: '请输入订单金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, v, cb) => {
|
||||
const min = createOrderDepositMin.value
|
||||
const num = Number(v)
|
||||
if (min > 0 && (!Number.isFinite(num) || num < min)) {
|
||||
cb(new Error(`订单金额须大于等于定金门槛 ¥${formatPoDepositMoney(min)}`))
|
||||
return
|
||||
}
|
||||
cb()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createOrderHerbSummary = computed(() => {
|
||||
@@ -1037,14 +1255,26 @@ function resetCreateOrderForm() {
|
||||
createOrderForm.pay_order_ids = []
|
||||
createOrderPatientList.value = []
|
||||
createOrderPaidOrders.value = []
|
||||
createOrderDepositMin.value = 0
|
||||
createOrderPrescription.value = null
|
||||
createOrderFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function syncCreateOrderAmountFromRules() {
|
||||
const min = createOrderDepositMin.value
|
||||
const paid = createOrderLinkedPaidTotal.value
|
||||
const floor = min > 0 ? Math.round(min * 100) / 100 : 0
|
||||
const need = min > 0 ? Math.max(paid, floor) : paid > 0 ? paid : 0
|
||||
if (need > 0 && Number(createOrderForm.amount) < need) {
|
||||
createOrderForm.amount = Math.round(need * 100) / 100
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCreateOrderPaidOrders() {
|
||||
const did = Number(createOrderForm.patient_id)
|
||||
if (!did) {
|
||||
createOrderPaidOrders.value = []
|
||||
createOrderDepositMin.value = 0
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -1052,8 +1282,12 @@ async function loadCreateOrderPaidOrders() {
|
||||
const res: any = await prescriptionOrderPaidPayOrders({ diagnosis_id: did })
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
createOrderPaidOrders.value = Array.isArray(lists) ? lists : []
|
||||
const dep = Number(res?.deposit_min_amount ?? res?.data?.deposit_min_amount) || 0
|
||||
createOrderDepositMin.value = dep
|
||||
syncCreateOrderAmountFromRules()
|
||||
} catch {
|
||||
createOrderPaidOrders.value = []
|
||||
createOrderDepositMin.value = 0
|
||||
} finally {
|
||||
createOrderPaidOrdersLoading.value = false
|
||||
}
|
||||
@@ -1067,6 +1301,14 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [...createOrderForm.pay_order_ids],
|
||||
() => {
|
||||
syncCreateOrderAmountFromRules()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
async function openCreateOrderFromPrescription(row: any) {
|
||||
resetCreateOrderForm()
|
||||
createOrderPrescription.value = row
|
||||
@@ -1580,6 +1822,9 @@ const handleAdd = () => {
|
||||
editingWasRejected.value = false
|
||||
editMode.value = 'add'
|
||||
showEdit.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 查看处方(处方笺样式,详情接口补全电话、签名等)
|
||||
@@ -1643,6 +1888,9 @@ const handleEdit = (row: any) => {
|
||||
editForm.business_prescription_audit_rejected = Number(row.business_prescription_audit_rejected) === 1 ? 1 : 0
|
||||
editForm.business_prescription_audit_remark = String(row.business_prescription_audit_remark || '')
|
||||
showEdit.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
@@ -1874,9 +2122,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
/* 查看处方:互联网医院处方笺(对齐诊间 tcm-prescription 打印版式) */
|
||||
.cf-prescription-dialog :deep(.el-dialog__body) {
|
||||
.cf-prescription-drawer :deep(.el-drawer__body) {
|
||||
padding-top: 8px;
|
||||
max-height: calc(100vh - 140px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -2048,4 +2295,53 @@ onMounted(async () => {
|
||||
color: #606266;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* 创建订单表单样式 */
|
||||
.create-order-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-order-form {
|
||||
.form-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
svg {
|
||||
color: var(--el-color-primary);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -407,7 +407,8 @@ const resetForm = () => {
|
||||
// 打开抽屉
|
||||
const open = (data: any) => {
|
||||
resetForm()
|
||||
|
||||
|
||||
|
||||
formData.appointment_id = data.id || 0
|
||||
formData.diagnosis_id = data.diagnosis_id || 0
|
||||
formData.patient_name = data.patient_name || ''
|
||||
|
||||
@@ -179,15 +179,6 @@
|
||||
<el-table-column label="操作" width="380" fixed="right" align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns">
|
||||
<!-- <el-button
|
||||
v-perms="['doctor.appointment/detail']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDetail(row)"
|
||||
>
|
||||
详情
|
||||
</el-button> -->
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/edit']"
|
||||
type="primary"
|
||||
@@ -198,6 +189,7 @@
|
||||
编辑患者
|
||||
</el-button>
|
||||
|
||||
<template v-if="row.status !== 3">
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/videoQr']"
|
||||
type="warning"
|
||||
@@ -227,15 +219,17 @@
|
||||
>
|
||||
完成
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/kaifang']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handlePrescription(row)"
|
||||
>
|
||||
{{ prescriptionActionLabel(row) }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/kaifang']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handlePrescription(row)"
|
||||
>
|
||||
{{ prescriptionActionLabel(row) }}
|
||||
</el-button>
|
||||
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleAction(cmd, row)" placement="bottom-end">
|
||||
<el-button type="info" link size="small">
|
||||
@@ -959,6 +953,7 @@ function prescriptionActionLabel(row: any) {
|
||||
|
||||
// 开处方 / 查看已通过处方(只读)
|
||||
const handlePrescription = (row: any) => {
|
||||
//console.log(row)
|
||||
if (!row.diagnosis_id && !row.patient_id) {
|
||||
feedback.msgWarning('该预约缺少诊单信息,请先编辑患者')
|
||||
return
|
||||
|
||||
@@ -41,6 +41,9 @@ UNIQUE_IDENTIFICATION = likeadmin
|
||||
# 演示环境
|
||||
DEMO_ENV = false
|
||||
|
||||
; 处方业务订单:关联收款每笔金额须大于等于该值(元),业务订单金额也须大于等于该值;0=不校验
|
||||
PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT = 0
|
||||
|
||||
# 物流轨迹(快递100):https://api.kuaidi100.com
|
||||
; ThinkPHP .env 为 INI:下列建议写在分区声明之前;若误写在 trtc 分区内,config 已兼容
|
||||
# 同时填 CUSTOMER、KEY 即启用;临时关闭设 LOGISTICS_KUAIDI100_DISABLE = true
|
||||
|
||||
@@ -68,6 +68,7 @@ class PrescriptionController extends BaseAdminController
|
||||
{
|
||||
$params = (new PrescriptionValidate())->get()->goCheck('detail');
|
||||
$detail = PrescriptionLogic::detail((int) $params['id'], (int) $this->adminId, $this->adminInfo);
|
||||
|
||||
if (!$detail) {
|
||||
$msg = PrescriptionLogic::getError();
|
||||
|
||||
@@ -129,8 +130,12 @@ class PrescriptionController extends BaseAdminController
|
||||
if (!$appointmentId) {
|
||||
return $this->fail('预约ID不能为空');
|
||||
}
|
||||
$prescription = PrescriptionLogic::getByAppointment($appointmentId);
|
||||
return $this->data($prescription ?? []);
|
||||
$prescription = PrescriptionLogic::getByAppointment($appointmentId, (int)$this->adminId, $this->adminInfo);
|
||||
if ($prescription === null) {
|
||||
//$msg = PrescriptionLogic::getError();
|
||||
return '';
|
||||
}
|
||||
return $this->data($prescription);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,19 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
$exceptPo > 0 ? $exceptPo : null
|
||||
);
|
||||
|
||||
return $this->success('', ['lists' => $lists]);
|
||||
$min = PrescriptionOrderLogic::depositMinAmount();
|
||||
|
||||
// if ($min > 0) {
|
||||
// $lists = array_values(array_filter(
|
||||
// $lists,
|
||||
// static fn(array $r): bool => round((float) ($r['amount'] ?? 0), 2) >= $min
|
||||
// ));
|
||||
// }
|
||||
|
||||
return $this->success('', [
|
||||
'lists' => $lists,
|
||||
'deposit_min_amount' => $min,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
@@ -107,6 +119,24 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
return $this->success('操作成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回处方审核
|
||||
*/
|
||||
public function revokeRxAudit()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('detail');
|
||||
$result = PrescriptionOrderLogic::revokeRxAudit(
|
||||
(int) $params['id'],
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('处方审核已撤回', $result);
|
||||
}
|
||||
|
||||
public function auditPayment()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('auditPayment');
|
||||
@@ -124,6 +154,44 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
return $this->success('操作成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回支付单审核
|
||||
*/
|
||||
public function revokePayAudit()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('detail');
|
||||
$result = PrescriptionOrderLogic::revokePayAudit(
|
||||
(int) $params['id'],
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('支付单审核已撤回', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认发货:将履约状态推进到 5(已发货),同时更新快递信息
|
||||
*/
|
||||
public function ship()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('ship');
|
||||
$result = PrescriptionOrderLogic::ship(
|
||||
(int) $params['id'],
|
||||
(string) ($params['express_company'] ?? 'auto'),
|
||||
(string) ($params['tracking_number'] ?? ''),
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('发货成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助/创建人撤回:仅「待双审通过」可撤(未通过或双审均驳回等仍为该状态)
|
||||
*/
|
||||
@@ -137,4 +205,48 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
|
||||
return $this->success('已撤回', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取业务订单操作日志
|
||||
*/
|
||||
public function logs()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->get()->goCheck('logs');
|
||||
$result = PrescriptionOrderLogic::getLogs((int) $params['id'], $this->adminId, $this->adminInfo);
|
||||
|
||||
// 遇到没有权限或者订单不存在,逻辑里设置了 self::$error
|
||||
if ($result === [] && PrescriptionOrderLogic::getError() !== '') {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核
|
||||
*/
|
||||
public function addPayOrder()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('addPayOrder');
|
||||
$result = PrescriptionOrderLogic::addPayOrder($params, $this->adminId, $this->adminInfo);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('新增支付单成功', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将「已发货」且支付审核已通过的订单标记为「已完成」
|
||||
*/
|
||||
public function complete()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('complete');
|
||||
$result = PrescriptionOrderLogic::complete((int) $params['id'], $this->adminId, $this->adminInfo);
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('订单已完成', $result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$where[] = ['patient_id', 'in', function ($query) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where('patient_name|patient_phone', 'like', '%' . $this->params['patient_keyword'] . '%')
|
||||
->where('patient_name|phone', 'like', '%' . $this->params['patient_keyword'] . '%')
|
||||
->field('id');
|
||||
}];
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$where[] = ['patient_id', 'in', function ($query) {
|
||||
$query->table('zyt_tcm_diagnosis')
|
||||
->whereNull('delete_time')
|
||||
->where('patient_name|patient_phone', 'like', '%' . $this->params['patient_keyword'] . '%')
|
||||
->where('patient_name|phone', 'like', '%' . $this->params['patient_keyword'] . '%')
|
||||
->field('id');
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ namespace app\adminapi\lists\tcm;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||||
use app\common\lists\ListsExtendInterface;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
|
||||
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
|
||||
{
|
||||
public function setSearch(): array
|
||||
{
|
||||
@@ -38,18 +40,115 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$poIds = array_values(array_filter(array_map('intval', array_column($lists, 'id'))));
|
||||
$paidSumByPo = [];
|
||||
if ($poIds !== []) {
|
||||
$links = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $poIds)
|
||||
->field(['prescription_order_id', 'pay_order_id'])
|
||||
->select()
|
||||
->toArray();
|
||||
$byPo = [];
|
||||
foreach ($links as $l) {
|
||||
$poid = (int) ($l['prescription_order_id'] ?? 0);
|
||||
$oid = (int) ($l['pay_order_id'] ?? 0);
|
||||
if ($poid > 0 && $oid > 0) {
|
||||
$byPo[$poid][] = $oid;
|
||||
}
|
||||
}
|
||||
$allOids = [];
|
||||
foreach ($byPo as $oids) {
|
||||
$allOids = array_merge($allOids, $oids);
|
||||
}
|
||||
$allOids = array_values(array_unique($allOids));
|
||||
$amountByOid = [];
|
||||
if ($allOids !== []) {
|
||||
$amountByOid = Order::whereIn('id', $allOids)->whereNull('delete_time')->column('amount', 'id');
|
||||
}
|
||||
foreach ($poIds as $pid) {
|
||||
$s = 0.0;
|
||||
foreach ($byPo[$pid] ?? [] as $oid) {
|
||||
$s += (float) ($amountByOid[$oid] ?? 0);
|
||||
}
|
||||
$paidSumByPo[$pid] = round($s, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取创建人姓名
|
||||
$creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'creator_id')))));
|
||||
$creatorNames = [];
|
||||
if ($creatorIds !== []) {
|
||||
$creatorNames = \app\common\model\auth\Admin::whereIn('id', $creatorIds)->column('name', 'id');
|
||||
}
|
||||
|
||||
// 获取处方ID对应的开方人姓名
|
||||
$prescriptionIds = array_values(array_unique(array_filter(array_map('intval', array_column($lists, 'prescription_id')))));
|
||||
$prescriptionCreators = [];
|
||||
if ($prescriptionIds !== []) {
|
||||
$prescriptions = \app\common\model\tcm\Prescription::whereIn('id', $prescriptionIds)
|
||||
->whereNull('delete_time')
|
||||
->field(['id', 'creator_id', 'doctor_name'])
|
||||
->select()
|
||||
->toArray();
|
||||
$doctorIds = [];
|
||||
foreach ($prescriptions as $rx) {
|
||||
$rxId = (int) ($rx['id'] ?? 0);
|
||||
$doctorId = (int) ($rx['creator_id'] ?? 0);
|
||||
$doctorName = (string) ($rx['doctor_name'] ?? '');
|
||||
if ($rxId > 0) {
|
||||
$prescriptionCreators[$rxId] = [
|
||||
'doctor_id' => $doctorId,
|
||||
'doctor_name' => $doctorName
|
||||
];
|
||||
if ($doctorId > 0) {
|
||||
$doctorIds[] = $doctorId;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果 doctor_name 为空,从 Admin 表获取
|
||||
$doctorIds = array_values(array_unique($doctorIds));
|
||||
if ($doctorIds !== []) {
|
||||
$doctorNamesFromAdmin = \app\common\model\auth\Admin::whereIn('id', $doctorIds)->column('name', 'id');
|
||||
foreach ($prescriptionCreators as &$pc) {
|
||||
if (empty($pc['doctor_name']) && $pc['doctor_id'] > 0) {
|
||||
$pc['doctor_name'] = $doctorNamesFromAdmin[$pc['doctor_id']] ?? '';
|
||||
}
|
||||
}
|
||||
unset($pc);
|
||||
}
|
||||
}
|
||||
|
||||
$depMin = PrescriptionOrderLogic::depositMinAmount();
|
||||
foreach ($lists as &$item) {
|
||||
PrescriptionOrderLogic::maskInternalCostIfNeeded($item, $this->adminInfo);
|
||||
$pid = (int) ($item['id'] ?? 0);
|
||||
$item['linked_pay_order_count'] = (int) PrescriptionOrderPayOrder::where(
|
||||
'prescription_order_id',
|
||||
(int) ($item['id'] ?? 0)
|
||||
$pid
|
||||
)->count();
|
||||
$item['linked_pay_paid_total'] = $paidSumByPo[$pid] ?? 0.0;
|
||||
$item['deposit_min_amount'] = $depMin;
|
||||
|
||||
// 添加创建人姓名
|
||||
$creatorId = (int) ($item['creator_id'] ?? 0);
|
||||
$item['creator_name'] = $creatorId > 0 ? ($creatorNames[$creatorId] ?? '') : '';
|
||||
|
||||
// 添加开方人姓名
|
||||
$rxId = (int) ($item['prescription_id'] ?? 0);
|
||||
$item['doctor_name'] = $rxId > 0 ? ($prescriptionCreators[$rxId]['doctor_name'] ?? '') : '';
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
public function extend(): array
|
||||
{
|
||||
return [
|
||||
'deposit_min_amount' => PrescriptionOrderLogic::depositMinAmount(),
|
||||
];
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
$query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time');
|
||||
|
||||
@@ -47,7 +47,7 @@ class OrderLogic
|
||||
$order->status = 1; // 待支付
|
||||
$order->remark = $params['remark'] ?? '';
|
||||
$order->save();
|
||||
|
||||
|
||||
return $order;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -102,146 +102,146 @@ class OrderLogic
|
||||
$cursor = '';
|
||||
do {
|
||||
$resp = $service->getBillList($beginTime, $endTime, $payeeUserid, $cursor, 100);
|
||||
if (($resp['errcode'] ?? -1) !== 0) {
|
||||
$result['errors'][] = '获取账单失败: ' . ($resp['errmsg'] ?? '未知错误');
|
||||
break;
|
||||
}
|
||||
$billList = $resp['bill_list'] ?? [];
|
||||
$cursor = $resp['next_cursor'] ?? '';
|
||||
|
||||
foreach ($billList as $bill) {
|
||||
$tradeState = (int)($bill['trade_state'] ?? 0);
|
||||
// 1:已完成(已支付) 3:已完成有退款 — 见企业微信 get_bill_list 文档
|
||||
if (!in_array($tradeState, [1, 3], true)) {
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
if (($resp['errcode'] ?? -1) !== 0) {
|
||||
$result['errors'][] = '获取账单失败: ' . ($resp['errmsg'] ?? '未知错误');
|
||||
break;
|
||||
}
|
||||
$targetStatus = $tradeState === 3 ? 4 : 2; // 4=已退款 2=已支付
|
||||
$billList = $resp['bill_list'] ?? [];
|
||||
$cursor = $resp['next_cursor'] ?? '';
|
||||
|
||||
$orderNo = (string)($bill['out_trade_no'] ?? $bill['trade_no'] ?? '');
|
||||
$totalFee = (int)($bill['total_fee'] ?? $bill['total_amount'] ?? 0);
|
||||
$tradeNo = (string)($bill['transaction_id'] ?? $bill['trade_no'] ?? '');
|
||||
$payTime = $bill['pay_time'] ?? $bill['time_end'] ?? '';
|
||||
$billPayeeUserid = (string)($bill['payee_userid'] ?? '');
|
||||
|
||||
if (empty($orderNo) || $totalFee <= 0) {
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
$amount = round($totalFee / 100, 2);
|
||||
$orderType = ($amount == 5.00) ? 1 : $defaultOrderType;
|
||||
|
||||
$creatorId = 0;
|
||||
if ($billPayeeUserid) {
|
||||
$admin = Admin::where('work_wechat_userid', $billPayeeUserid)->whereNull('delete_time')->find();
|
||||
if ($admin) {
|
||||
$creatorId = (int)$admin->id;
|
||||
}
|
||||
}
|
||||
|
||||
$payerExternalUserid = (string)($bill['external_userid'] ?? '');
|
||||
$paymentTimeStr = $payTime
|
||||
? (is_numeric($payTime) ? date('Y-m-d H:i:s', (int)$payTime) : (string)$payTime)
|
||||
: date('Y-m-d H:i:s');
|
||||
|
||||
$order = Order::where('order_no', $orderNo)->find();
|
||||
if ($order) {
|
||||
$needSave = false;
|
||||
$currentStatus = (int)$order->status;
|
||||
|
||||
if ($currentStatus !== $targetStatus) {
|
||||
// 待支付:随账单变为已支付或已退款(含未同步到「已支付」即发生退款)
|
||||
if ($currentStatus === 1) {
|
||||
$order->status = $targetStatus;
|
||||
$needSave = true;
|
||||
} elseif ($currentStatus === 2 && $targetStatus === 4) {
|
||||
$order->status = 4;
|
||||
$needSave = true;
|
||||
} elseif ($currentStatus === 4 && $targetStatus === 2) {
|
||||
// 账单从「有退款」变回「已完成」等异常纠偏
|
||||
$order->status = 2;
|
||||
$needSave = true;
|
||||
}
|
||||
// 已取消(3) 不自动改支付状态,避免覆盖后台取消
|
||||
}
|
||||
|
||||
if ($currentStatus === 1 || $targetStatus === 2) {
|
||||
if ($order->payment_method !== 'wechat') {
|
||||
$order->payment_method = 'wechat';
|
||||
$needSave = true;
|
||||
}
|
||||
if ($paymentTimeStr !== '' && (string)$order->payment_time !== $paymentTimeStr) {
|
||||
$order->payment_time = $paymentTimeStr;
|
||||
$needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($tradeNo !== '' && (string)$order->trade_no !== $tradeNo) {
|
||||
$order->trade_no = $tradeNo;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
if (abs((float)$order->amount - $amount) > 0.000001) {
|
||||
$order->amount = $amount;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
$defaultCreatorIdInt = (int)$defaultCreatorId;
|
||||
if ($billPayeeUserid && $creatorId !== $defaultCreatorIdInt && (int)$order->creator_id === $defaultCreatorIdInt) {
|
||||
$order->payee_userid = $billPayeeUserid;
|
||||
$order->payer_external_userid = $payerExternalUserid;
|
||||
$order->creator_id = $creatorId;
|
||||
$needSave = true;
|
||||
} elseif ($billPayeeUserid !== '' && (string)$order->payee_userid !== $billPayeeUserid) {
|
||||
$order->payee_userid = $billPayeeUserid;
|
||||
$needSave = true;
|
||||
}
|
||||
if ($payerExternalUserid !== '' && (string)$order->payer_external_userid !== $payerExternalUserid) {
|
||||
$order->payer_external_userid = $payerExternalUserid;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
if ($amount == 5.00 && (int)$order->order_type !== 1) {
|
||||
$order->order_type = 1;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
if ($needSave) {
|
||||
$order->save();
|
||||
$result['updated']++;
|
||||
} else {
|
||||
foreach ($billList as $bill) {
|
||||
$tradeState = (int)($bill['trade_state'] ?? 0);
|
||||
// 1:已完成(已支付) 3:已完成有退款 — 见企业微信 get_bill_list 文档
|
||||
if (!in_array($tradeState, [1, 3], true)) {
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$order = new Order();
|
||||
$order->order_no = $orderNo;
|
||||
$order->patient_id = null;
|
||||
$order->creator_id = $creatorId;
|
||||
$order->order_type = $orderType;
|
||||
$order->amount = $amount;
|
||||
$order->status = $targetStatus;
|
||||
$order->payment_method = 'wechat';
|
||||
$order->payment_time = $paymentTimeStr;
|
||||
$order->trade_no = $tradeNo;
|
||||
$remark = '企业微信直接收款同步,待关联患者';
|
||||
if ($tradeState === 3) {
|
||||
$refundCent = (int)($bill['total_refund_fee'] ?? 0);
|
||||
if ($refundCent > 0) {
|
||||
$remark .= '(账单含退款' . round($refundCent / 100, 2) . '元)';
|
||||
$targetStatus = $tradeState === 3 ? 4 : 2; // 4=已退款 2=已支付
|
||||
|
||||
$orderNo = (string)($bill['out_trade_no'] ?? $bill['trade_no'] ?? '');
|
||||
$totalFee = (int)($bill['total_fee'] ?? $bill['total_amount'] ?? 0);
|
||||
$tradeNo = (string)($bill['transaction_id'] ?? $bill['trade_no'] ?? '');
|
||||
$payTime = $bill['pay_time'] ?? $bill['time_end'] ?? '';
|
||||
$billPayeeUserid = (string)($bill['payee_userid'] ?? '');
|
||||
|
||||
if (empty($orderNo) || $totalFee <= 0) {
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
$amount = round($totalFee / 100, 2);
|
||||
$orderType = ($amount == 5.00) ? 1 : $defaultOrderType;
|
||||
|
||||
$creatorId = 0;
|
||||
if ($billPayeeUserid) {
|
||||
$admin = Admin::where('work_wechat_userid', $billPayeeUserid)->whereNull('delete_time')->find();
|
||||
if ($admin) {
|
||||
$creatorId = (int)$admin->id;
|
||||
}
|
||||
}
|
||||
|
||||
$payerExternalUserid = (string)($bill['external_userid'] ?? '');
|
||||
$paymentTimeStr = $payTime
|
||||
? (is_numeric($payTime) ? date('Y-m-d H:i:s', (int)$payTime) : (string)$payTime)
|
||||
: date('Y-m-d H:i:s');
|
||||
|
||||
$order = Order::where('order_no', $orderNo)->find();
|
||||
if ($order) {
|
||||
$needSave = false;
|
||||
$currentStatus = (int)$order->status;
|
||||
|
||||
if ($currentStatus !== $targetStatus) {
|
||||
// 待支付:随账单变为已支付或已退款(含未同步到「已支付」即发生退款)
|
||||
if ($currentStatus === 1) {
|
||||
$order->status = $targetStatus;
|
||||
$needSave = true;
|
||||
} elseif ($currentStatus === 2 && $targetStatus === 4) {
|
||||
$order->status = 4;
|
||||
$needSave = true;
|
||||
} elseif ($currentStatus === 4 && $targetStatus === 2) {
|
||||
// 账单从「有退款」变回「已完成」等异常纠偏
|
||||
$order->status = 2;
|
||||
$needSave = true;
|
||||
}
|
||||
// 已取消(3) 不自动改支付状态,避免覆盖后台取消
|
||||
}
|
||||
|
||||
if ($currentStatus === 1 || $targetStatus === 2) {
|
||||
if ($order->payment_method !== 'wechat') {
|
||||
$order->payment_method = 'wechat';
|
||||
$needSave = true;
|
||||
}
|
||||
if ($paymentTimeStr !== '' && (string)$order->payment_time !== $paymentTimeStr) {
|
||||
$order->payment_time = $paymentTimeStr;
|
||||
$needSave = true;
|
||||
}
|
||||
}
|
||||
$order->remark = $remark;
|
||||
$order->payee_userid = $billPayeeUserid;
|
||||
$order->payer_external_userid = $payerExternalUserid;
|
||||
$order->save();
|
||||
$result['created']++;
|
||||
} catch (\Exception $e) {
|
||||
$result['errors'][] = "订单 {$orderNo}: " . $e->getMessage();
|
||||
Log::error("企业微信账单同步创建订单失败: {$orderNo}, " . $e->getMessage());
|
||||
|
||||
if ($tradeNo !== '' && (string)$order->trade_no !== $tradeNo) {
|
||||
$order->trade_no = $tradeNo;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
if (abs((float)$order->amount - $amount) > 0.000001) {
|
||||
$order->amount = $amount;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
$defaultCreatorIdInt = (int)$defaultCreatorId;
|
||||
if ($billPayeeUserid && $creatorId !== $defaultCreatorIdInt && (int)$order->creator_id === $defaultCreatorIdInt) {
|
||||
$order->payee_userid = $billPayeeUserid;
|
||||
$order->payer_external_userid = $payerExternalUserid;
|
||||
$order->creator_id = $creatorId;
|
||||
$needSave = true;
|
||||
} elseif ($billPayeeUserid !== '' && (string)$order->payee_userid !== $billPayeeUserid) {
|
||||
$order->payee_userid = $billPayeeUserid;
|
||||
$needSave = true;
|
||||
}
|
||||
if ($payerExternalUserid !== '' && (string)$order->payer_external_userid !== $payerExternalUserid) {
|
||||
$order->payer_external_userid = $payerExternalUserid;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
if ($amount == 5.00 && (int)$order->order_type !== 1) {
|
||||
$order->order_type = 1;
|
||||
$needSave = true;
|
||||
}
|
||||
|
||||
if ($needSave) {
|
||||
$order->save();
|
||||
$result['updated']++;
|
||||
} else {
|
||||
$result['skipped']++;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$order = new Order();
|
||||
$order->order_no = $orderNo;
|
||||
$order->patient_id = null;
|
||||
$order->creator_id = $creatorId;
|
||||
$order->order_type = $orderType;
|
||||
$order->amount = $amount;
|
||||
$order->status = $targetStatus;
|
||||
$order->payment_method = 'wechat';
|
||||
$order->payment_time = $paymentTimeStr;
|
||||
$order->trade_no = $tradeNo;
|
||||
$remark = '企业微信直接收款同步,待关联患者';
|
||||
if ($tradeState === 3) {
|
||||
$refundCent = (int)($bill['total_refund_fee'] ?? 0);
|
||||
if ($refundCent > 0) {
|
||||
$remark .= '(账单含退款' . round($refundCent / 100, 2) . '元)';
|
||||
}
|
||||
}
|
||||
$order->remark = $remark;
|
||||
$order->payee_userid = $billPayeeUserid;
|
||||
$order->payer_external_userid = $payerExternalUserid;
|
||||
$order->save();
|
||||
$result['created']++;
|
||||
} catch (\Exception $e) {
|
||||
$result['errors'][] = "订单 {$orderNo}: " . $e->getMessage();
|
||||
Log::error("企业微信账单同步创建订单失败: {$orderNo}, " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ($cursor);
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ class OrderLogic
|
||||
self::setError('订单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (isset($params['remark'])) {
|
||||
$order->remark = $params['remark'];
|
||||
}
|
||||
@@ -307,7 +307,7 @@ class OrderLogic
|
||||
if (isset($params['order_type'])) {
|
||||
$order->order_type = (int)$params['order_type'];
|
||||
}
|
||||
|
||||
|
||||
$order->save();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
@@ -330,17 +330,17 @@ class OrderLogic
|
||||
self::setError('订单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ($order->status != 1) {
|
||||
self::setError('订单状态不允许支付');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$order->status = 2; // 已支付
|
||||
$order->payment_method = $paymentMethod;
|
||||
$order->payment_time = date('Y-m-d H:i:s');
|
||||
$order->save();
|
||||
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -361,15 +361,15 @@ class OrderLogic
|
||||
self::setError('订单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ($order->status != 1) {
|
||||
self::setError('只有待支付的订单才能取消');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$order->status = 3; // 已取消
|
||||
$order->save();
|
||||
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -390,15 +390,15 @@ class OrderLogic
|
||||
self::setError('订单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ($order->status != 2) {
|
||||
self::setError('只有已支付的订单才能退款');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$order->status = 4; // 已退款
|
||||
$order->save();
|
||||
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -419,10 +419,10 @@ class OrderLogic
|
||||
self::setError('订单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 删除订单
|
||||
$order->delete();
|
||||
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -466,11 +466,11 @@ class OrderLogic
|
||||
{
|
||||
$order = Order::with(['patient', 'creator'])
|
||||
->find($id);
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return $order->toArray();
|
||||
}
|
||||
|
||||
@@ -496,10 +496,10 @@ class OrderLogic
|
||||
{
|
||||
// 移除sign字段
|
||||
unset($params['sign']);
|
||||
|
||||
|
||||
// 按键排序
|
||||
ksort($params);
|
||||
|
||||
|
||||
// 构建签名字符串
|
||||
$signStr = '';
|
||||
foreach ($params as $key => $value) {
|
||||
@@ -508,12 +508,12 @@ class OrderLogic
|
||||
}
|
||||
}
|
||||
$signStr = rtrim($signStr, '&');
|
||||
|
||||
|
||||
// 使用私钥签名
|
||||
$privateKeyResource = openssl_pkey_get_private($privateKey);
|
||||
openssl_sign($signStr, $sign, $privateKeyResource, OPENSSL_ALGO_SHA256);
|
||||
openssl_free_key($privateKeyResource);
|
||||
|
||||
|
||||
return base64_encode($sign);
|
||||
}
|
||||
|
||||
@@ -527,10 +527,10 @@ class OrderLogic
|
||||
{
|
||||
// 移除sign字段
|
||||
unset($params['sign']);
|
||||
|
||||
|
||||
// 按键排序
|
||||
ksort($params);
|
||||
|
||||
|
||||
// 构建签名字符串
|
||||
$signStr = '';
|
||||
foreach ($params as $key => $value) {
|
||||
@@ -539,7 +539,7 @@ class OrderLogic
|
||||
}
|
||||
}
|
||||
$signStr .= 'key=' . $apiKey;
|
||||
|
||||
|
||||
// MD5签名
|
||||
return strtoupper(md5($signStr));
|
||||
}
|
||||
@@ -606,14 +606,14 @@ class OrderLogic
|
||||
// 获取订单号
|
||||
$orderNo = is_array($order) ? $order['order_no'] : $order->order_no;
|
||||
$amount = is_array($order) ? ($order['amount'] ?? 0) : $order->amount;
|
||||
|
||||
|
||||
// 获取支付宝配置
|
||||
$alipayConfig = config('pay.alipay');
|
||||
|
||||
|
||||
if (empty($alipayConfig['app_id']) || empty($alipayConfig['merchant_private_key'])) {
|
||||
throw new \Exception('支付宝配置不完整');
|
||||
}
|
||||
|
||||
|
||||
// 使用支付宝SDK生成支付URL
|
||||
$params = [
|
||||
'app_id' => $alipayConfig['app_id'],
|
||||
@@ -632,14 +632,14 @@ class OrderLogic
|
||||
'body' => '订单号:' . $orderNo,
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
|
||||
// 生成签名
|
||||
$sign = self::generateAlipaySign($params, $alipayConfig['merchant_private_key']);
|
||||
$params['sign'] = $sign;
|
||||
|
||||
|
||||
// 生成支付URL
|
||||
$payUrl = $alipayConfig['gateway_url'] . '?' . http_build_query($params);
|
||||
|
||||
|
||||
return $payUrl;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
@@ -658,14 +658,14 @@ class OrderLogic
|
||||
// 获取订单号
|
||||
$orderNo = is_array($order) ? $order['order_no'] : $order->order_no;
|
||||
$amount = is_array($order) ? ($order['amount'] ?? 0) : $order->amount;
|
||||
|
||||
|
||||
// 获取微信配置
|
||||
$wechatConfig = config('pay.wechat');
|
||||
|
||||
|
||||
if (empty($wechatConfig['app_id']) || empty($wechatConfig['mch_id']) || empty($wechatConfig['api_key'])) {
|
||||
throw new \Exception('微信支付配置不完整');
|
||||
}
|
||||
|
||||
|
||||
// 生成微信支付数据
|
||||
$payData = [
|
||||
'appid' => $wechatConfig['app_id'],
|
||||
@@ -678,11 +678,11 @@ class OrderLogic
|
||||
'notify_url' => $wechatConfig['notify_url'] ?: config('app.url') . '/api/order/wechat-notify',
|
||||
'trade_type' => 'NATIVE', // 二维码支付
|
||||
];
|
||||
|
||||
|
||||
// 生成签名
|
||||
$sign = self::generateWechatSign($payData, $wechatConfig['api_key']);
|
||||
$payData['sign'] = $sign;
|
||||
|
||||
|
||||
// 返回支付数据
|
||||
return [
|
||||
'pay_url' => $wechatConfig['gateway_url'] . '/pay/unifiedorder',
|
||||
@@ -918,7 +918,7 @@ class OrderLogic
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
$supervisorRoles = [0, 3];
|
||||
$supervisorRoles = config('project.order_edit_all_roles', [0, 3]);
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
|
||||
return count(array_intersect($myRoles, $supervisorRoles)) > 0;
|
||||
@@ -985,6 +985,7 @@ class OrderLogic
|
||||
if ($busy && ! $allowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out[] = $row;
|
||||
}
|
||||
|
||||
@@ -998,7 +999,7 @@ class OrderLogic
|
||||
*/
|
||||
public static function assertPayOrdersLinkable(array $ids, int $diagnosisId, int $adminId, array $adminInfo): ?string
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn (int $v): bool => $v > 0)));
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn(int $v): bool => $v > 0)));
|
||||
if ($diagnosisId <= 0) {
|
||||
return '诊单无效';
|
||||
}
|
||||
|
||||
@@ -74,30 +74,54 @@ class PrescriptionLogic
|
||||
*/
|
||||
public static function canViewPrescription($row, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
// 超级管理员
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建人
|
||||
$creatorId = is_array($row) ? (int) ($row['creator_id'] ?? 0) : (int) $row->creator_id;
|
||||
if ($creatorId === $adminId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 医助
|
||||
$assistantId = is_array($row) ? (int) ($row['assistant_id'] ?? 0) : (int) ($row->assistant_id ?? 0);
|
||||
if ($assistantId > 0 && $assistantId === $adminId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 共享处方
|
||||
$isShared = is_array($row) ? (int) ($row['is_shared'] ?? 0) : (int) $row->is_shared;
|
||||
if ($isShared === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 可见角色
|
||||
$vis = is_array($row) ? (string) ($row['visible_role_ids'] ?? '') : (string) ($row->visible_role_ids ?? '');
|
||||
$allowed = array_filter(array_map('intval', explode(',', $vis)));
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
if (count(array_intersect($myRoles, $allowed)) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return count(array_intersect($myRoles, $allowed)) > 0;
|
||||
// 有开方权限的医生可以查看所有处方(只读)
|
||||
// 这样其他医生可以查看患者的历史处方记录
|
||||
$permissions = $adminInfo['permissions'] ?? [];
|
||||
if (in_array('tcm.diagnosis/kaifang', $permissions, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 有业务订单管理权限的角色(如药师)可以查看处方(只读)
|
||||
// 这样药师可以在业务订单中查看关联的处方信息
|
||||
$orderEditAllRoles = Config::get('project.order_edit_all_roles', [0, 3]);
|
||||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||||
$allowedRoles = array_map('intval', is_array($orderEditAllRoles) ? $orderEditAllRoles : []);
|
||||
if (count(array_intersect($myRoles, $allowedRoles)) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function generateSn(): string
|
||||
@@ -261,13 +285,10 @@ class PrescriptionLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已通过且未作废:不可编辑;例外:业务订单「处方审核」已驳回时允许医生改方,保存后业务订单侧审核会重置为待审
|
||||
// 已通过且未作废:不可编辑
|
||||
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
|
||||
if (!PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject((int) $params['id'])) {
|
||||
self::setError('该处方已通过审核,不可编辑');
|
||||
|
||||
return false;
|
||||
}
|
||||
self::setError('该处方已通过审核,不可编辑');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已驳回:仅当该诊单+当天+同一创建人下没有另一条「已通过且未作废」的处方时允许重新编辑(改后待审核、作废一并清除)
|
||||
@@ -435,6 +456,51 @@ class PrescriptionLogic
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务订单「处方审核」通过时:消费者处方仍为待审核则一并记为已通过,
|
||||
* 避免消费者处方列表筛选「未通过」仍包含该条、与业务侧已通过不一致。
|
||||
*/
|
||||
public static function syncApproveConsumerPrescriptionIfPending(
|
||||
int $prescriptionId,
|
||||
int $adminId,
|
||||
array $adminInfo,
|
||||
string $remark
|
||||
): void {
|
||||
if ($prescriptionId <= 0) {
|
||||
return;
|
||||
}
|
||||
$row = Prescription::find($prescriptionId);
|
||||
if (!$row) {
|
||||
return;
|
||||
}
|
||||
if ((int) ($row->void_status ?? 0) === 1) {
|
||||
return;
|
||||
}
|
||||
if ((int) ($row->audit_status ?? 1) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$lastAuditWecomNotify = null;
|
||||
$name = (string) ($adminInfo['name'] ?? '');
|
||||
$now = time();
|
||||
$finalRemark = trim($remark) !== '' ? trim($remark) : '业务订单处方审核通过';
|
||||
$finalRemark = mb_substr($finalRemark, 0, 500);
|
||||
|
||||
$row->audit_status = 1;
|
||||
$row->audit_time = $now;
|
||||
$row->audit_by = $adminId;
|
||||
$row->audit_by_name = $name;
|
||||
$row->audit_remark = $finalRemark;
|
||||
|
||||
try {
|
||||
if ($row->save()) {
|
||||
self::$lastAuditWecomNotify = self::notifyCreatorAuditResult($row, 'approve', $finalRemark, $adminInfo);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('syncApproveConsumerPrescriptionIfPending: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
@@ -601,15 +667,28 @@ class PrescriptionLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据预约ID获取处方
|
||||
* 根据预约ID获取处方(带权限检查)
|
||||
*/
|
||||
public static function getByAppointment(int $appointmentId): ?array
|
||||
public static function getByAppointment(int $appointmentId, int $viewerAdminId, array $viewerAdminInfo): ?array
|
||||
{
|
||||
self::$error = '';
|
||||
$row = Prescription::where('appointment_id', $appointmentId)
|
||||
->where('creator_id',$viewerAdminId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
return $row ? $row->toArray() : null;
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 权限检查:只返回当前用户有权限查看的处方
|
||||
if (!self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)) {
|
||||
self::setError('无权限查看此处方');
|
||||
return null;
|
||||
}
|
||||
|
||||
return $row->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\order\OrderLogic;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\Prescription;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderLog;
|
||||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\service\ExpressTrackService;
|
||||
use think\facade\Config;
|
||||
|
||||
@@ -84,6 +88,20 @@ class PrescriptionOrderLogic
|
||||
return count(array_intersect($myRoles, $allow)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务订单详情内是否返回/展示处方「药材明细」(菜单权限 tcm.prescriptionOrder/viewRxHerbs)
|
||||
*/
|
||||
private static function canViewPrescriptionHerbsInOrderDetail(array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
|
||||
|
||||
return in_array('tcm.prescriptionOrder/viewRxHerbs', $perms, true);
|
||||
}
|
||||
|
||||
public static function canAuditPrescriptionOrder(array $adminInfo): bool
|
||||
{
|
||||
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||
@@ -114,6 +132,47 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
}
|
||||
|
||||
/** 定金门槛(元),0 表示不校验 */
|
||||
public static function depositMinAmount(): float
|
||||
{
|
||||
return round((float) Config::get('prescription_order.link_pay_min_amount', 0), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $payOrderIds
|
||||
*/
|
||||
public static function assertDepositAndLinkPayRules(float $orderAmount, array $payOrderIds): ?string
|
||||
{
|
||||
$min = self::depositMinAmount();
|
||||
if ($min <= 0) {
|
||||
return null;
|
||||
}
|
||||
if ($orderAmount < $min) {
|
||||
return '订单金额须大于等于定金门槛 ¥' . $min . '(当前 ¥' . $orderAmount . ')';
|
||||
}
|
||||
if ($payOrderIds === []) {
|
||||
return '未关联支付单,关联支付单总金额须大于等于定金门槛 ¥' . $min;
|
||||
}
|
||||
|
||||
// 检查关联支付单的总金额
|
||||
$rows = Order::whereIn('id', $payOrderIds)->whereNull('delete_time')->column('amount', 'id');
|
||||
$totalAmount = 0.0;
|
||||
foreach ($payOrderIds as $pid) {
|
||||
$pid = (int) $pid;
|
||||
if ($pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$amt = round((float) ($rows[$pid] ?? 0), 2);
|
||||
$totalAmount += $amt;
|
||||
}
|
||||
|
||||
if ($totalAmount < $min) {
|
||||
return '关联支付单总金额须大于等于定金门槛 ¥' . $min . '(当前总金额 ¥' . $totalAmount . ')';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function generateOrderNo(): string
|
||||
{
|
||||
return 'PO' . date('YmdHis') . mt_rand(100000, 999999);
|
||||
@@ -279,6 +338,36 @@ class PrescriptionOrderLogic
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$creatorIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'creator_id')))));
|
||||
$creatorNames = [];
|
||||
if ($creatorIds !== []) {
|
||||
$creatorNames = \app\common\model\auth\Admin::whereIn('id', $creatorIds)->column('name', 'id');
|
||||
}
|
||||
$typeMap = [
|
||||
1 => '挂号费',
|
||||
2 => '问诊费',
|
||||
3 => '药品费用',
|
||||
4 => '首付费用',
|
||||
5 => '尾款费用',
|
||||
6 => '其他费用',
|
||||
7 => '全部费用',
|
||||
];
|
||||
$statusMap = [
|
||||
1 => '待支付',
|
||||
2 => '已支付',
|
||||
3 => '已取消',
|
||||
4 => '已退款',
|
||||
];
|
||||
foreach ($rows as &$r) {
|
||||
$ot = (int) ($r['order_type'] ?? 0);
|
||||
$st = (int) ($r['status'] ?? 0);
|
||||
$r['order_type_desc'] = $typeMap[$ot] ?? '—';
|
||||
$r['status_desc'] = $statusMap[$st] ?? '—';
|
||||
$cid = (int) ($r['creator_id'] ?? 0);
|
||||
$r['creator_name'] = $cid > 0 ? (string) ($creatorNames[$cid] ?? '') : '';
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
@@ -291,12 +380,20 @@ class PrescriptionOrderLogic
|
||||
if ($id <= 0) {
|
||||
$arr['pay_order_ids'] = [];
|
||||
$arr['linked_pay_orders'] = [];
|
||||
$arr['linked_pay_paid_total'] = 0.0;
|
||||
$arr['deposit_min_amount'] = self::depositMinAmount();
|
||||
|
||||
return;
|
||||
}
|
||||
$ids = self::linkedPayOrderIdList($id);
|
||||
$arr['pay_order_ids'] = $ids;
|
||||
$arr['linked_pay_orders'] = self::linkedPayOrdersPayload($ids);
|
||||
$sum = 0.0;
|
||||
foreach ($arr['linked_pay_orders'] as $o) {
|
||||
$sum += (float) ($o['amount'] ?? 0);
|
||||
}
|
||||
$arr['linked_pay_paid_total'] = round($sum, 2);
|
||||
$arr['deposit_min_amount'] = self::depositMinAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,6 +449,14 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
}
|
||||
|
||||
$orderAmtPreview = round((float) ($params['amount'] ?? 0), 2);
|
||||
$depErrCreate = self::assertDepositAndLinkPayRules($orderAmtPreview, $payOrderIds);
|
||||
if ($depErrCreate !== null) {
|
||||
self::$error = $depErrCreate;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$internalCost = null;
|
||||
if (isset($params['internal_cost']) && $params['internal_cost'] !== '' && $params['internal_cost'] !== null) {
|
||||
if (self::canViewInternalCost($adminInfo)) {
|
||||
@@ -396,6 +501,8 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'create', '创建业务订单');
|
||||
|
||||
if ($payOrderIds !== []) {
|
||||
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
|
||||
}
|
||||
@@ -425,6 +532,44 @@ class PrescriptionOrderLogic
|
||||
self::maskInternalCostIfNeeded($arr, $adminInfo);
|
||||
self::attachLinkedPayOrders($arr);
|
||||
|
||||
// 添加创建人姓名
|
||||
$creatorId = (int) ($arr['creator_id'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creatorName = Admin::where('id', $creatorId)->value('name');
|
||||
$arr['creator_name'] = $creatorName ? (string) $creatorName : '';
|
||||
} else {
|
||||
$arr['creator_name'] = '';
|
||||
}
|
||||
|
||||
$rxId = (int) ($arr['prescription_id'] ?? 0);
|
||||
$arr['prescription'] = null;
|
||||
$arr['prescription_detail_error'] = '';
|
||||
$arr['doctor_name'] = '';
|
||||
if ($rxId > 0) {
|
||||
$rxDetail = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
|
||||
if ($rxDetail !== null) {
|
||||
$arr['prescription'] = $rxDetail;
|
||||
// 添加开方人姓名(优先使用处方的 doctor_name,否则从 creator_id 获取)
|
||||
$doctorName = (string) ($rxDetail['doctor_name'] ?? '');
|
||||
if ($doctorName === '') {
|
||||
$rxCreatorId = (int) ($rxDetail['creator_id'] ?? 0);
|
||||
if ($rxCreatorId > 0) {
|
||||
$doctorName = (string) (Admin::where('id', $rxCreatorId)->value('name') ?? '');
|
||||
}
|
||||
}
|
||||
$arr['doctor_name'] = $doctorName;
|
||||
} else {
|
||||
$err = PrescriptionLogic::getError();
|
||||
$arr['prescription_detail_error'] = $err !== '' ? $err : '处方不存在';
|
||||
}
|
||||
}
|
||||
|
||||
$canHerbs = self::canViewPrescriptionHerbsInOrderDetail($adminInfo);
|
||||
$arr['prescription_detail_herbs_visible'] = $canHerbs;
|
||||
if (! $canHerbs && isset($arr['prescription']) && is_array($arr['prescription'])) {
|
||||
unset($arr['prescription']['herbs']);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
@@ -581,9 +726,10 @@ class PrescriptionOrderLogic
|
||||
$order->linked_pay_order_id = $single[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (array_key_exists('internal_cost', $params) && self::canViewInternalCost($adminInfo)) {
|
||||
$raw = $params['internal_cost'];
|
||||
|
||||
if ($raw === '' || $raw === null) {
|
||||
$order->internal_cost = null;
|
||||
} else {
|
||||
@@ -591,6 +737,14 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
}
|
||||
|
||||
$finalPayIds = self::linkedPayOrderIdList((int) $order->id);
|
||||
$depErr = self::assertDepositAndLinkPayRules(round((float) $order->amount, 2), $finalPayIds);
|
||||
if ($depErr !== null) {
|
||||
self::$error = $depErr;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -599,6 +753,71 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'edit', '编辑了业务订单及相关信息');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认发货:更新快递信息并将 fulfillment_status 从 2(履约中)推进到 5(已发货)
|
||||
*
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function ship(int $id, string $expressCompany, string $trackingNumber, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
|
||||
$trackingNumber = trim($trackingNumber);
|
||||
if ($trackingNumber === '') {
|
||||
self::$error = '快递单号不能为空';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限操作';
|
||||
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->fulfillment_status !== 2 && (int) $order->fulfillment_status !== 5) {
|
||||
self::$error = '仅「履约中」或「已发货」状态的订单可更新快递信息';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->express_company = self::normalizeExpressCompany($expressCompany);
|
||||
$order->tracking_number = $trackingNumber;
|
||||
// 仅履约中(2)时才推进到已发货(5),已发货(5)则只更新快递信息保持状态
|
||||
$isFirstShip = false;
|
||||
if ((int) $order->fulfillment_status === 2) {
|
||||
$order->fulfillment_status = 5;
|
||||
$isFirstShip = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($isFirstShip) {
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'ship', '确认发货,运单:' . $trackingNumber . '(' . $expressCompany . ')');
|
||||
} else {
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'fill_tracking', '更新发货信息,运单:' . $trackingNumber . '(' . $expressCompany . ')');
|
||||
}
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -634,12 +853,29 @@ class PrescriptionOrderLogic
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
|
||||
// 撤回订单后,将关联的处方审核状态改回"待审核"
|
||||
$prescriptionId = (int) $order->prescription_id;
|
||||
if ($prescriptionId > 0) {
|
||||
Prescription::where('id', $prescriptionId)
|
||||
->whereNull('delete_time')
|
||||
->update([
|
||||
'audit_status' => 0, // 0=待审核
|
||||
'audit_time' => null,
|
||||
'audit_by' => null,
|
||||
'audit_by_name' => '',
|
||||
'audit_remark' => '',
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'withdraw', '撤回订单,状态变更为「已取消」');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -704,6 +940,20 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($action === 'approve') {
|
||||
$syncRemark = trim($remark) !== '' ? trim($remark) : '业务订单处方审核通过';
|
||||
PrescriptionLogic::syncApproveConsumerPrescriptionIfPending(
|
||||
(int) $order->prescription_id,
|
||||
$adminId,
|
||||
$adminInfo,
|
||||
$syncRemark
|
||||
);
|
||||
}
|
||||
|
||||
$actionName = $action === 'approve' ? 'audit_rx_approve' : 'audit_rx_reject';
|
||||
$summary = $action === 'approve' ? '处方审核通过' . ($remark ? ',意见:' . $remark : '') : '处方审核驳回,意见:' . $remark;
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, $actionName, $summary);
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -744,6 +994,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
return false;
|
||||
}
|
||||
// 已发货状态(5)允许重新发起支付单审核(新增支付单场景)
|
||||
|
||||
if ($action === 'reject' && trim($remark) === '') {
|
||||
self::$error = '驳回时请填写审核意见';
|
||||
@@ -771,10 +1022,306 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
$actionName = $action === 'approve' ? 'audit_pay_approve' : 'audit_pay_reject';
|
||||
$summary = $action === 'approve' ? '支付单审核通过' . ($remark ? ',意见:' . $remark : '') : '支付单审核驳回,意见:' . $remark;
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, $actionName, $summary);
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回处方审核:将已通过/已驳回的处方审核重置为待审核
|
||||
*
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function revokeRxAudit(int $id, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!self::canAuditPrescriptionOrder($adminInfo)) {
|
||||
self::$error = '无处方审核权限';
|
||||
|
||||
return false;
|
||||
}
|
||||
$rxStatus = (int) $order->prescription_audit_status;
|
||||
if ($rxStatus === 0) {
|
||||
self::$error = '当前已是待审核状态,无需撤回';
|
||||
|
||||
return false;
|
||||
}
|
||||
$fs = (int) $order->fulfillment_status;
|
||||
if ($fs === 3 || $fs === 4) {
|
||||
self::$error = '订单已结束,不可撤回审核';
|
||||
|
||||
return false;
|
||||
}
|
||||
// 如果处方审核已通过,且支付审核也已通过或已驳回,不允许撤回处方审核
|
||||
if ($rxStatus === 1 && (int) $order->payment_slip_audit_status !== 0) {
|
||||
self::$error = '支付单审核已进行,请先撤回支付单审核';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->prescription_audit_status = 0;
|
||||
$order->prescription_audit_remark = '';
|
||||
// 如果支付审核不是待审核,也一并重置
|
||||
if ((int) $order->payment_slip_audit_status !== 0) {
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
}
|
||||
self::syncFulfillmentStatus($order);
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'revoke_rx_audit', '撤回处方审核,状态重置为待审核');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回支付单审核:将已通过/已驳回的支付单审核重置为待审核
|
||||
*
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function revokePayAudit(int $id, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
|
||||
self::$error = '无支付单审核权限';
|
||||
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->prescription_audit_status !== 1) {
|
||||
self::$error = '处方审核未通过,无法撤回支付单审核';
|
||||
|
||||
return false;
|
||||
}
|
||||
$payStatus = (int) $order->payment_slip_audit_status;
|
||||
if ($payStatus === 0) {
|
||||
self::$error = '当前已是待审核状态,无需撤回';
|
||||
|
||||
return false;
|
||||
}
|
||||
$fs = (int) $order->fulfillment_status;
|
||||
if ($fs === 3 || $fs === 4) {
|
||||
self::$error = '订单已结束,不可撤回审核';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
self::syncFulfillmentStatus($order);
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'revoke_pay_audit', '撤回支付单审核,状态重置为待审核');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function getLogs(int $orderId, int $adminId, array $adminInfo): array
|
||||
{
|
||||
self::$error = '';
|
||||
$order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
|
||||
return [];
|
||||
}
|
||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限查看';
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return PrescriptionOrderLog::where('prescription_order_id', $orderId)
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为「已发货」(fulfillment_status=5) 的业务订单新增一条关联支付单(zyt_order),
|
||||
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
|
||||
*
|
||||
* @param array<string,mixed> $params id, order_type, amount, remark
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function addPayOrder(array $params, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
$id = (int) $params['id'];
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
return false;
|
||||
}
|
||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限操作';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->fulfillment_status !== 5) {
|
||||
self::$error = '仅「已发货」状态的订单可新增支付单';
|
||||
return false;
|
||||
}
|
||||
|
||||
$orderType = (int) ($params['order_type'] ?? 3);
|
||||
$amount = round((float) ($params['pay_amount'] ?? 0), 2);
|
||||
$remark = mb_substr(trim((string) ($params['pay_remark'] ?? '')), 0, 200);
|
||||
|
||||
if ($amount <= 0) {
|
||||
self::$error = '支付单金额须大于 0';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建 zyt_order 并标记为已支付(后台直接确认)
|
||||
$payOrder = new Order();
|
||||
$payOrder->order_no = OrderLogic::generateOrderNo();
|
||||
$payOrder->patient_id = (int) $order->diagnosis_id;
|
||||
$payOrder->creator_id = $adminId;
|
||||
$payOrder->order_type = $orderType;
|
||||
$payOrder->amount = $amount;
|
||||
$payOrder->status = 2; // 已支付
|
||||
$payOrder->payment_method = 'manual';
|
||||
$payOrder->payment_time = date('Y-m-d H:i:s');
|
||||
$payOrder->remark = $remark;
|
||||
|
||||
try {
|
||||
$payOrder->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将新支付单追加到业务订单关联列表
|
||||
$existingIds = self::linkedPayOrderIdList($id);
|
||||
$newIds = array_unique(array_merge($existingIds, [(int) $payOrder->id]));
|
||||
self::replacePayOrderLinks($id, $newIds);
|
||||
$order->linked_pay_order_id = $newIds[0];
|
||||
|
||||
// 重置支付单审核为待审核,以启动新一轮审核(处方审核保持通过状态不变)
|
||||
$order->payment_slip_audit_status = 0;
|
||||
$order->payment_slip_audit_remark = '';
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog($id, $adminId, $adminInfo, 'add_pay_order',
|
||||
'新增支付单 #' . $payOrder->id . '(¥' . $amount . '),类型:' . $orderType . ',等待支付审核');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将「已发货」(fulfillment_status=5) 且支付审核已通过的订单标记为「已完成」(3)。
|
||||
*
|
||||
* @return array<string,mixed>|false
|
||||
*/
|
||||
public static function complete(int $id, int $adminId, array $adminInfo)
|
||||
{
|
||||
self::$error = '';
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
self::$error = '订单不存在';
|
||||
return false;
|
||||
}
|
||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
||||
self::$error = '无权限操作';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->fulfillment_status !== 5) {
|
||||
self::$error = '仅「已发货」状态可标记为已完成';
|
||||
return false;
|
||||
}
|
||||
if ((int) $order->payment_slip_audit_status !== 1) {
|
||||
self::$error = '支付单审核尚未通过,不可完成订单';
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->fulfillment_status = 3;
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
self::writeLog($id, $adminId, $adminInfo, 'complete', '订单完成,状态变更为「已完成」');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private static function writeLog(int $orderId, int $adminId, array $adminInfo, string $action, string $summary): void
|
||||
{
|
||||
$adminName = $adminInfo['name'] ?? '';
|
||||
if ($adminName === '' && $adminId > 0) {
|
||||
$adminName = (string) \app\common\model\auth\Admin::where('id', $adminId)->value('name');
|
||||
}
|
||||
|
||||
$log = new PrescriptionOrderLog();
|
||||
$log->prescription_order_id = $orderId;
|
||||
$log->admin_id = $adminId;
|
||||
$log->admin_name = mb_substr($adminName, 0, 64);
|
||||
$log->action = mb_substr($action, 0, 32);
|
||||
$log->summary = mb_substr($summary, 0, 500);
|
||||
$log->create_time = time();
|
||||
|
||||
try {
|
||||
$log->save();
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略日志写入错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,27 @@ use app\common\validate\BaseValidate;
|
||||
class PrescriptionOrderValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'id' => 'require|integer',
|
||||
'diagnosis_id' => 'require|integer|gt:0',
|
||||
'prescription_id' => 'require|integer',
|
||||
'pay_order_ids' => 'array',
|
||||
'recipient_name' => 'require|max:50',
|
||||
'recipient_phone' => 'require|max:20',
|
||||
'id' => 'require|integer',
|
||||
'diagnosis_id' => 'require|integer|gt:0',
|
||||
'prescription_id' => 'require|integer',
|
||||
'pay_order_ids' => 'array',
|
||||
'recipient_name' => 'require|max:50',
|
||||
'recipient_phone' => 'require|max:20',
|
||||
'shipping_address' => 'require|max:500',
|
||||
'is_follow_up' => 'in:0,1',
|
||||
'prev_staff' => 'max:100',
|
||||
'service_channel' => 'max:100',
|
||||
'service_package' => 'max:100',
|
||||
'tracking_number' => 'max:80',
|
||||
'express_company' => 'max:20',
|
||||
'fee_type' => 'require|in:1,2,3,4,5,6,7',
|
||||
'amount' => 'require|float',
|
||||
'remark_extra' => 'max:500',
|
||||
'action' => 'require|in:approve,reject',
|
||||
'remark' => 'max:500',
|
||||
'is_follow_up' => 'in:0,1',
|
||||
'prev_staff' => 'max:100',
|
||||
'service_channel' => 'max:100',
|
||||
'service_package' => 'max:100',
|
||||
'tracking_number' => 'max:80',
|
||||
'express_company' => 'max:20',
|
||||
'fee_type' => 'require|in:1,2,3,4,5,6,7',
|
||||
'amount' => 'require|float',
|
||||
'order_type' => 'require|in:1,2,3,4,5,6,7',
|
||||
'pay_amount' => 'require|float|gt:0',
|
||||
'pay_remark' => 'max:200',
|
||||
'remark_extra' => 'max:500',
|
||||
'action' => 'require|in:approve,reject',
|
||||
'remark' => 'max:500',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
@@ -56,6 +59,10 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'auditPrescription' => ['id', 'action', 'remark'],
|
||||
'auditPayment' => ['id', 'action', 'remark'],
|
||||
'withdraw' => ['id'],
|
||||
'ship' => ['id', 'tracking_number', 'express_company'],
|
||||
'logs' => ['id'],
|
||||
'paidPayOrders' => ['diagnosis_id'],
|
||||
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
|
||||
'complete' => ['id'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,13 +3,40 @@
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\api\logic\ChatNotifyLogic;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 聊天相关接口(C端-患者)
|
||||
*/
|
||||
class ChatController extends BaseApiController
|
||||
{
|
||||
public array $notNeedLogin = ['notifyOpen'];
|
||||
public array $notNeedLogin = ['notifyOpen', 'logAvPermission'];
|
||||
|
||||
/**
|
||||
* 上报音视频权限拒绝日志
|
||||
* 患者小程序请求摄像头/麦克风权限被拒绝,且用户点击「取消」未前往设置页时调用
|
||||
*/
|
||||
public function logAvPermission()
|
||||
{
|
||||
$patientId = trim((string) ($this->request->post('patient_id', '')));
|
||||
$doctorId = trim((string) ($this->request->post('doctor_id', '')));
|
||||
$deniedScope = trim((string) ($this->request->post('denied_scope', '')));
|
||||
$scene = trim((string) ($this->request->post('scene', '')));
|
||||
$action = trim((string) ($this->request->post('action', 'cancel')));
|
||||
$wxVersion = trim((string) ($this->request->post('wx_version', '')));
|
||||
|
||||
Db::name('av_permission_log')->insert([
|
||||
'patient_id' => $patientId,
|
||||
'doctor_id' => $doctorId,
|
||||
'denied_scope' => $deniedScope ?: 'unknown',
|
||||
'scene' => $scene ?: 'unknown',
|
||||
'action' => in_array($action, ['cancel', 'open_setting']) ? $action : 'cancel',
|
||||
'wx_version' => $wxVersion,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
|
||||
return $this->success('已记录');
|
||||
}
|
||||
|
||||
/**
|
||||
* 患者打开会话时通知医生
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model\tcm;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 处方业务订单操作日志
|
||||
*/
|
||||
class PrescriptionOrderLog extends BaseModel
|
||||
{
|
||||
protected $name = 'tcm_prescription_order_log';
|
||||
|
||||
protected $autoWriteTimestamp = false; // 时间戳由代码里显式写
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 处方业务订单(履约单)
|
||||
*
|
||||
* .env 示例:PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT = 100
|
||||
* 为 0 或未配置时不校验。
|
||||
*/
|
||||
return [
|
||||
// 定金门槛(元):关联的每笔已支付单金额须 **大于等于** 此值;业务订单金额须 **大于等于** 此值
|
||||
'link_pay_min_amount' => (float) env('PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT', 0),
|
||||
];
|
||||
@@ -102,20 +102,20 @@ return [
|
||||
'tabbar_style' => ['default_color' => '#999999', 'selected_color' => '#c455ff'],
|
||||
],
|
||||
|
||||
// 订单编辑权限:拥有以下角色ID的用户可修改任意订单,其他用户只能修改自己创建的订单
|
||||
'order_edit_all_roles' => [0, 3],
|
||||
// 医助创建订单权限:拥有以下角色ID的用户可修改任意订单,其他用户只能修改自己创建的订单
|
||||
'order_edit_all_roles' => [0, 3, 6],
|
||||
|
||||
// 处方库:以下角色ID + 超级管理员(root) 可查看/编辑/删除全部;他人仅可管理自己创建的,公开处方对他人只读
|
||||
'prescription_library_manage_all_roles' => [0, 3],
|
||||
|
||||
// 消费者处方单:以下角色 + root 可对「待审核」处方进行通过/驳回(驳回即作废处方)
|
||||
'prescription_audit_roles' => [0, 3,6],
|
||||
'prescription_audit_roles' => [0, 3, 6],
|
||||
|
||||
// 处方业务订单 internal_cost 等财务字段:以下角色 ID + root 可见
|
||||
'prescription_order_finance_roles' => [0, 3],
|
||||
'prescription_order_finance_roles' => [0, 3, 6],
|
||||
|
||||
// 业务订单「关联支付单」审核:以下角色 ID + root 可操作(可与财务角色一致)
|
||||
'prescription_order_payment_audit_roles' => [0, 3],
|
||||
'prescription_order_payment_audit_roles' => [0, 3, 6],
|
||||
|
||||
// 腾讯云实时音视频(TRTC)配置
|
||||
'trtc' => [
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 音视频权限拒绝日志表
|
||||
CREATE TABLE IF NOT EXISTS `zyt_av_permission_log` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`patient_id` varchar(64) NOT NULL DEFAULT '' COMMENT '患者ID(patient_xxx格式)',
|
||||
`doctor_id` varchar(64) NOT NULL DEFAULT '' COMMENT '医生ID(doctor_xxx格式)',
|
||||
`denied_scope` varchar(32) NOT NULL DEFAULT '' COMMENT '被拒绝的权限 scope(camera/record)',
|
||||
`scene` varchar(32) NOT NULL DEFAULT '' COMMENT '触发场景(makeCall / incoming)',
|
||||
`action` varchar(16) NOT NULL DEFAULT 'cancel' COMMENT '用户行为:cancel=取消 / open_setting=去设置',
|
||||
`wx_version` varchar(32) NOT NULL DEFAULT '' COMMENT '微信基础库版本',
|
||||
`create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间(Unix秒)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_patient` (`patient_id`),
|
||||
KEY `idx_doctor` (`doctor_id`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='音视频权限拒绝行为日志';
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 处方业务订单详情:查看处方药材明细(无此权限时接口不返回 herbs)
|
||||
SET @po_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/lists' LIMIT 1);
|
||||
|
||||
INSERT INTO zyt_system_menu (
|
||||
pid, type, name, icon, sort, perms, paths, component,
|
||||
selected, params, is_cache, is_show, is_disable, create_time, update_time
|
||||
)
|
||||
SELECT
|
||||
@po_menu_id, 'A', '药材明细', '', 9, 'tcm.prescriptionOrder/viewRxHerbs', '', '',
|
||||
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE @po_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/viewRxHerbs');
|
||||
Binary file not shown.
Reference in New Issue
Block a user