-
+
完成
-
- {{ prescriptionActionLabel(row) }}
-
+
+
+
+ {{ prescriptionActionLabel(row) }}
+
handleAction(cmd, row)" placement="bottom-end">
@@ -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
diff --git a/server/.example.env b/server/.example.env
index 12eba5de..f8abad4d 100644
--- a/server/.example.env
+++ b/server/.example.env
@@ -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
diff --git a/server/app/adminapi/controller/tcm/PrescriptionController.php b/server/app/adminapi/controller/tcm/PrescriptionController.php
index 41020870..dfca9595 100644
--- a/server/app/adminapi/controller/tcm/PrescriptionController.php
+++ b/server/app/adminapi/controller/tcm/PrescriptionController.php
@@ -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);
}
/**
diff --git a/server/app/adminapi/controller/tcm/PrescriptionOrderController.php b/server/app/adminapi/controller/tcm/PrescriptionOrderController.php
index e2975e58..a4ae40ee 100644
--- a/server/app/adminapi/controller/tcm/PrescriptionOrderController.php
+++ b/server/app/adminapi/controller/tcm/PrescriptionOrderController.php
@@ -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);
+ }
}
diff --git a/server/app/adminapi/lists/order/OrderLists.php b/server/app/adminapi/lists/order/OrderLists.php
index d133d650..f9385395 100644
--- a/server/app/adminapi/lists/order/OrderLists.php
+++ b/server/app/adminapi/lists/order/OrderLists.php
@@ -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');
}];
}
diff --git a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php
index 192a0eb3..ce77a5b3 100644
--- a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php
+++ b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php
@@ -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');
diff --git a/server/app/adminapi/logic/order/OrderLogic.php b/server/app/adminapi/logic/order/OrderLogic.php
index 66ebb354..79e48c74 100644
--- a/server/app/adminapi/logic/order/OrderLogic.php
+++ b/server/app/adminapi/logic/order/OrderLogic.php
@@ -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 '诊单无效';
}
diff --git a/server/app/adminapi/logic/tcm/PrescriptionLogic.php b/server/app/adminapi/logic/tcm/PrescriptionLogic.php
index d6da93e1..9fd1a94d 100644
--- a/server/app/adminapi/logic/tcm/PrescriptionLogic.php
+++ b/server/app/adminapi/logic/tcm/PrescriptionLogic.php
@@ -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();
}
/**
diff --git a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php
index 5a42e172..56b3d81c 100644
--- a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php
+++ b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php
@@ -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|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|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|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>
+ */
+ 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 $params id, order_type, amount, remark
+ * @return array|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|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) {
+ // 忽略日志写入错误
+ }
+ }
}
diff --git a/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php b/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php
index 04f32160..f61aec79 100644
--- a/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php
+++ b/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php
@@ -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'],
];
}
diff --git a/server/app/api/controller/ChatController.php b/server/app/api/controller/ChatController.php
index ab9b578a..e973d127 100644
--- a/server/app/api/controller/ChatController.php
+++ b/server/app/api/controller/ChatController.php
@@ -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('已记录');
+ }
/**
* 患者打开会话时通知医生
diff --git a/server/app/common/model/tcm/PrescriptionOrderLog.php b/server/app/common/model/tcm/PrescriptionOrderLog.php
new file mode 100644
index 00000000..d5e97ca0
--- /dev/null
+++ b/server/app/common/model/tcm/PrescriptionOrderLog.php
@@ -0,0 +1,17 @@
+ (float) env('PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT', 0),
+];
diff --git a/server/config/project.php b/server/config/project.php
index c66ea7d4..0c1160ca 100644
--- a/server/config/project.php
+++ b/server/config/project.php
@@ -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' => [
diff --git a/server/database/av_permission_log.sql b/server/database/av_permission_log.sql
new file mode 100644
index 00000000..2d88f691
--- /dev/null
+++ b/server/database/av_permission_log.sql
@@ -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='音视频权限拒绝行为日志';
diff --git a/server/database/migrations/2026_04_11_prescription_order_view_rx_herbs.sql b/server/database/migrations/2026_04_11_prescription_order_view_rx_herbs.sql
new file mode 100644
index 00000000..38668fb1
--- /dev/null
+++ b/server/database/migrations/2026_04_11_prescription_order_view_rx_herbs.sql
@@ -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');
diff --git a/server/extend.zip b/server/extend.zip
deleted file mode 100644
index a1d2af55..00000000
Binary files a/server/extend.zip and /dev/null differ