Files
zyt/server/app/adminapi/logic/order/OrderLogic.php
T
2026-05-11 11:25:03 +08:00

1416 lines
52 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\adminapi\logic\order;
use app\common\model\Order;
use app\common\model\OrderDetail;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\service\wechat\WechatWorkExternalPayService;
use think\facade\Db;
use think\facade\Log;
/**
* 订单业务逻辑
* Class OrderLogic
* @package app\adminapi\logic\order
*/
class OrderLogic
{
/**
* @notes 生成订单号
* @return string
*/
public static function generateOrderNo(): string
{
return 'ORD' . date('YmdHis') . mt_rand(100000, 999999);
}
/**
* @notes 创建订单
* @param array $params
* @return Order|false
*/
public static function create(array $params)
{
try {
$channel = (string)($params['payment_channel'] ?? 'normal');
if (!in_array($channel, ['normal', 'fubei'], true)) {
$channel = 'normal';
}
$requirePaymentSlipAudit = (int)($params['require_payment_slip_audit'] ?? 0) === 1;
$order = new Order();
$order->order_no = self::generateOrderNo();
$order->patient_id = $params['patient_id'];
$order->creator_id = $params['creator_id'];
$order->order_type = $params['order_type'];
$order->amount = $params['amount'];
// 付呗且申请审核支付单:待审核(5);否则待支付(1)
if ($channel === 'fubei') {
$order->payment_method = 'fubei';
$order->status = $requirePaymentSlipAudit ? 5 : 1;
} else {
$order->status = 1; // 待支付
}
$order->remark = $params['remark'] ?? '';
$order->save();
return $order;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 创建企业微信对外收款订单(参考 https://developer.work.weixin.qq.com/document/path/93665
* 创建后返回 order_no,供员工在企业微信发起收款时作为 out_trade_no 使用
* 收款成功后,微信支付回调会自动更新订单为已支付
* @param array $params patient_id, order_type, amount, creator_id, remark
* @return array|false ['order_no' => '...', 'order' => Order]
*/
public static function createForWechatWork(array $params)
{
$order = self::create($params);
if (!$order) {
return false;
}
return [
'order_no' => $order->order_no,
'order' => $order->toArray(),
];
}
/**
* @notes 从企业微信对外收款账单同步订单(员工直接在企业微信发起收款,未经过后台创建)
* 拉取 get_bill_listtrade_state 1=已完成(已支付) 3=已完成有退款;会创建订单,并在状态变化时更新(如已支付→已退款)
* @param int $beginTime 开始时间戳(秒)
* @param int $endTime 结束时间戳(秒)
* @return array ['created' => int, 'updated' => int, 'skipped' => int, 'errors' => string[]]
*/
public static function syncFromWechatWorkBills(int $beginTime, int $endTime): array
{
$result = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []];
$service = new WechatWorkExternalPayService();
$wechatWorkConfig = config('pay.wechat_work', []);
$defaultCreatorId = $wechatWorkConfig['default_creator_id'] ?? '';
$defaultOrderType = $wechatWorkConfig['default_order_type'] ?? 6;
$payeeUserids = [];
$firstResp = $service->getBillList($beginTime, $endTime, '', '', 100);
if (count($firstResp['bill_list'] ?? []) === 0 && !empty($firstResp['next_cursor'])) {
$admins = Admin::where('work_wechat_userid', '<>', '')->whereNull('delete_time')->column('work_wechat_userid');
$payeeUserids = array_values(array_unique(array_filter($admins)));
}
if (empty($payeeUserids)) {
$payeeUserids = [''];
}
foreach ($payeeUserids as $payeeUserid) {
$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;
}
$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;
}
}
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);
}
return $result;
}
/**
* @notes 从回调数据创建订单(员工直接在企业微信发起收款时,订单不存在则自动创建)
* @param string $orderNo 商户订单号(out_trade_no
* @param float $amount 金额(元)
* @param string $tradeNo 微信交易号
* @return Order|null
*/
public static function createFromCallback(string $orderNo, float $amount, string $tradeNo = ''): ?Order
{
try {
$wechatWorkConfig = config('pay.wechat_work', []);
$creatorId = $wechatWorkConfig['default_creator_id'] ?? 1;
$orderType = $wechatWorkConfig['default_order_type'] ?? 6;
$order = new Order();
$order->order_no = $orderNo;
$order->patient_id = null; // 待关联,管理员可后续编辑
$order->creator_id = $creatorId;
$order->order_type = $orderType;
$order->amount = $amount;
$order->status = 2; // 已支付
$order->payment_method = 'wechat';
$order->payment_time = date('Y-m-d H:i:s');
$order->trade_no = $tradeNo;
$order->remark = '企业微信直接收款,待关联患者';
$order->save();
return $order;
} catch (\Exception $e) {
return null;
}
}
/**
* @notes 编辑订单
* @param int $id
* @param array $params
* @return bool
*/
public static function edit(int $id, array $params): bool
{
try {
$order = Order::find($id);
if (!$order) {
self::setError('订单不存在');
return false;
}
if (isset($params['remark'])) {
$order->remark = $params['remark'];
}
// 支持关联患者(待关联订单可后续关联)
if (array_key_exists('patient_id', $params)) {
$order->patient_id = $params['patient_id'] ? (int)$params['patient_id'] : null;
}
// 支持修改订单类型
if (isset($params['order_type'])) {
$order->order_type = (int)$params['order_type'];
}
$order->save();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 支付订单
* @param int $id
* @param string $paymentMethod
* @return bool
*/
public static function pay(int $id, string $paymentMethod): bool
{
try {
$order = Order::find($id);
if (!$order) {
self::setError('订单不存在');
return false;
}
$st = (int)$order->status;
// 1=待支付;5=待审核(付呗+申请支付单审核) 时允许录入手动到账
if ($st === 1) {
// 正常待支付
} elseif ($st === 5 && (string)($order->payment_method ?? '') === 'fubei') {
// 待审核的付呗单,通过人工确认后标记已支付
} else {
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());
return false;
}
}
/**
* @notes 取消订单
* @param int $id
* @return bool
*/
public static function cancel(int $id): bool
{
try {
$order = Order::find($id);
if (!$order) {
self::setError('订单不存在');
return false;
}
$st = (int)$order->status;
if ($st !== 1 && !($st === 5 && (string)($order->payment_method ?? '') === 'fubei')) {
self::setError('只有待支付或待审核(付呗)的订单才能取消');
return false;
}
$order->status = 3; // 已取消
$order->save();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 退款订单
* @param int $id
* @return bool
*/
public static function refund(int $id): bool
{
try {
$order = Order::find($id);
if (!$order) {
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());
return false;
}
}
/**
* @notes 删除订单
* @param int $id
* @return bool
*/
public static function delete(int $id): bool
{
try {
$order = Order::find($id);
if (!$order) {
self::setError('订单不存在');
return false;
}
// 删除订单
$order->delete();
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 拆分订单:待支付(1)、付呗待审核(5)、已支付(2);已关联业务订单的自动迁关联至各子单
*
* @param array<int, float|int|string> $amounts 子单金额(元),2~20 笔,分合计须与原单一致
* @param array<int, int|string> $orderTypes 与子单一一对应的订单类型 1~7
* @return array{new_order_ids: int[], orders: array<int, array<string, mixed>>}|false
*/
public static function split(int $orderId, array $amounts, array $orderTypes): array|false
{
$order = Order::whereNull('delete_time')->find($orderId);
if (! $order) {
self::setError('订单不存在');
return false;
}
$st = (int) $order->status;
if ($st === 5 && (string) ($order->payment_method ?? '') !== 'fubei') {
self::setError('待审核订单仅支持付呗渠道拆分');
return false;
}
if (! in_array($st, [1, 5, 2], true)) {
self::setError('仅「待支付」「待审核(付呗)」或「已支付」的订单可拆分');
return false;
}
$rawList = array_values($amounts);
$count = count($rawList);
if ($count < 2 || $count > 20) {
self::setError('拆分管数须在 220 之间');
return false;
}
$centsList = [];
foreach ($rawList as $a) {
$v = round((float) $a, 2);
if ($v <= 0) {
self::setError('每笔子单金额须大于 0');
return false;
}
$centsList[] = (int) round($v * 100);
}
$origCents = (int) round(round((float) $order->amount, 2) * 100);
if (array_sum($centsList) !== $origCents) {
self::setError(
'子单金额之和必须等于原订单金额(¥' . number_format($origCents / 100, 2, '.', '') . ''
);
return false;
}
$normAmounts = array_map(static fn (int $c) => round($c / 100, 2), $centsList);
$typeList = array_values($orderTypes);
if (count($typeList) !== $count) {
self::setError('订单类型笔数须与拆分数一致');
return false;
}
$allowedOt = [1, 2, 3, 4, 5, 6, 7, 8];
foreach ($typeList as $ot) {
if (! in_array((int) $ot, $allowedOt, true)) {
self::setError('订单类型无效,须为 18');
return false;
}
}
Db::startTrans();
try {
$origRemark = trim((string) ($order->remark ?? ''));
$splitTag = '[拆自原单#' . $orderId . '-' . (string) ($order->order_no ?? '') . ']';
$newOrders = [];
foreach ($normAmounts as $i => $amt) {
$n = new Order();
$n->order_no = self::generateOrderNo();
$n->patient_id = $order->patient_id;
$n->creator_id = $order->creator_id;
$n->order_type = (int) $typeList[$i];
$n->amount = $amt;
if ($st === 5) {
$n->payment_method = 'fubei';
$n->status = 5;
} elseif ($st === 2) {
$n->status = 2;
$n->payment_method = (string) ($order->payment_method ?? 'manual');
if ((string) ($order->payment_time ?? '') !== '') {
$n->payment_time = (string) $order->payment_time;
}
if ((string) ($order->trade_no ?? '') !== '') {
$n->trade_no = (string) $order->trade_no;
}
} else {
$n->status = 1;
}
$combined = $origRemark !== '' ? $origRemark . ' ' . $splitTag : $splitTag;
$n->remark = mb_substr($combined, 0, 500);
if ($order->payee_userid !== null && (string) $order->payee_userid !== '') {
$n->payee_userid = (string) $order->payee_userid;
}
if ($order->payer_external_userid !== null && (string) $order->payer_external_userid !== '') {
$n->payer_external_userid = (string) $order->payer_external_userid;
}
$n->save();
$newOrders[] = $n;
}
$newIds = [];
foreach ($newOrders as $o) {
$newIds[] = (int) $o->id;
}
// 处方业务订单 ↔ 支付单:原单关联迁移到全部子单(各业务订单对每笔子单各一条关联,合计金额不变)
$linkRows = PrescriptionOrderPayOrder::where('pay_order_id', $orderId)->select();
foreach ($linkRows as $link) {
$poId = (int) $link->prescription_order_id;
if ($poId <= 0) {
continue;
}
PrescriptionOrderPayOrder::where('prescription_order_id', $poId)
->where('pay_order_id', $orderId)
->delete();
foreach ($newIds as $nid) {
$dup = PrescriptionOrderPayOrder::where('prescription_order_id', $poId)
->where('pay_order_id', $nid)
->count();
if ($dup > 0) {
continue;
}
$nl = new PrescriptionOrderPayOrder();
$nl->prescription_order_id = $poId;
$nl->pay_order_id = $nid;
$nl->create_time = time();
$nl->save();
}
}
$order->remark = mb_substr(
($origRemark !== '' ? $origRemark . ' | ' : '') . '已拆分,子单ID:' . implode(',', $newIds),
0,
500
);
$order->save();
$order->delete();
Db::commit();
$brief = [];
foreach ($newOrders as $o) {
$brief[] = [
'id' => (int) $o->id,
'order_no' => (string) $o->order_no,
'amount' => (float) $o->amount,
'order_type' => (int) $o->order_type,
];
}
return [
'new_order_ids' => $newIds,
'orders' => $brief,
];
} catch (\Throwable $e) {
Db::rollback();
Log::error('Order split failed: ' . $e->getMessage(), ['order_id' => $orderId]);
self::setError('拆分失败:' . $e->getMessage());
return false;
}
}
/**
* @notes 今日收益(按角色权限:超管/主管看全部,普通员工看自己)
* @param int $adminId 当前管理员ID
* @param array $adminInfo 管理员信息(含 root、role 等)
* @return array ['amount' => string, 'count' => int]
*/
public static function todayRevenue(int $adminId, array $adminInfo = []): array
{
$query = Order::where('status', 2)
->whereBetweenTime('payment_time', date('Y-m-d') . ' 00:00:00', date('Y-m-d') . ' 23:59:59')
->whereNull('delete_time');
if (empty($adminInfo['root']) || $adminInfo['root'] != 1) {
$supervisorRoles = config('project.order_edit_all_roles', [0, 3]);
$roleIds = \app\common\model\auth\AdminRole::where('admin_id', $adminId)->column('role_id');
if (count(array_intersect($roleIds, $supervisorRoles)) === 0) {
$query->where('creator_id', $adminId);
}
}
$row = $query->fieldRaw('COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total')->find();
$total = is_object($row) ? ($row->total ?? 0) : ($row['total'] ?? 0);
$cnt = is_object($row) ? ($row->cnt ?? 0) : ($row['cnt'] ?? 0);
return [
'amount' => number_format((float)$total, 2, '.', ''),
'count' => (int)$cnt,
];
}
/**
* @notes 获取订单详情
* @param int $id
* @return array|null
*/
public static function detail(int $id): ?array
{
$order = Order::with(['patient', 'creator'])
->find($id);
if (!$order) {
return null;
}
return $order->toArray();
}
private static $error = '';
public static function setError(string $error): void
{
self::$error = $error;
}
public static function getError(): string
{
return self::$error;
}
/**
* @notes 生成支付宝签名
* @param array $params
* @param string $privateKey
* @return string
*/
private static function generateAlipaySign(array $params, string $privateKey): string
{
// 移除sign字段
unset($params['sign']);
// 按键排序
ksort($params);
// 构建签名字符串
$signStr = '';
foreach ($params as $key => $value) {
if ($value !== '' && $value !== null) {
$signStr .= $key . '=' . $value . '&';
}
}
$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);
}
/**
* @notes 生成微信签名
* @param array $params
* @param string $apiKey
* @return string
*/
private static function generateWechatSign(array $params, string $apiKey): string
{
// 移除sign字段
unset($params['sign']);
// 按键排序
ksort($params);
// 构建签名字符串
$signStr = '';
foreach ($params as $key => $value) {
if ($value !== '' && $value !== null) {
$signStr .= $key . '=' . $value . '&';
}
}
$signStr .= 'key=' . $apiKey;
// MD5签名
return strtoupper(md5($signStr));
}
/**
* @notes 生成随机字符串
* @param int $length
* @return string
*/
private static function generateNonceStr(int $length = 32): string
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $str;
}
/**
* @notes 支付宝支付
* @param $order
* @return string|false
*/
public static function alipayPay($order)
{
try {
// 这里调用支付宝SDK生成支付链接
// 示例:生成支付宝支付URL
$payUrl = self::generateAlipayUrl($order);
return $payUrl;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 微信支付
* @param $order
* @return array|false
*/
public static function wechatPay($order)
{
try {
// 这里调用微信SDK生成支付二维码或链接
// 示例:生成微信支付数据
$payData = self::generateWechatPayData($order);
return $payData;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* @notes 生成支付宝支付URL
* @param $order
* @return string
*/
private static function generateAlipayUrl($order): string
{
try {
// 获取订单号
$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'],
'method' => 'alipay.trade.page.pay',
'charset' => 'UTF-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'notify_url' => $alipayConfig['notify_url'] ?: config('app.url') . '/api/order/alipay-notify',
'return_url' => $alipayConfig['return_url'] ?: config('app.url') . '/order',
'biz_content' => json_encode([
'out_trade_no' => $orderNo,
'product_code' => 'FAST_INSTANT_TRADE_PAY',
'total_amount' => $amount,
'subject' => '订单支付',
'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());
return '';
}
}
/**
* @notes 生成微信支付数据
* @param $order
* @return array
*/
private static function generateWechatPayData($order): array
{
try {
// 获取订单号
$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'],
'mch_id' => $wechatConfig['mch_id'],
'nonce_str' => self::generateNonceStr(),
'body' => '订单支付',
'out_trade_no' => $orderNo,
'total_fee' => (int)($amount * 100), // 转换为分
'spbill_create_ip' => request()->ip() ?: '127.0.0.1',
'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',
'pay_data' => $payData,
'order_no' => $orderNo,
];
} catch (\Exception $e) {
self::setError($e->getMessage());
return [];
}
}
private static $orderTypeNames = [
0 => '退款统计',
1 => '挂号费',
2 => '问诊费',
3 => '药品费用',
4 => '首付费用',
5 => '尾款费用',
6 => '其他费用',
7 => '全部费用',
8 => '驼奶费用',
];
/**
* @notes 订单统计(按订单类型或退款)
* @param array $params order_type(-1全部已支付类型合计,0退款,1~8 各费用类型含驼奶费用), days(0今天,7,30)
* @return array
*/
public static function orderStats(array $params = [], int $adminId = 0, ?array $adminInfo = null)
{
$allPaidOrderTypes = [1, 2, 3, 4, 5, 6, 7, 8];
$orderType = array_key_exists('order_type', $params) ? (int) $params['order_type'] : 1;
if ($orderType !== -1 && $orderType !== 0 && !array_key_exists($orderType, self::$orderTypeNames)) {
$orderType = 1;
}
$orderTypeName = $orderType === -1 ? '全部(已支付)' : self::$orderTypeNames[$orderType];
$days = isset($params['days']) ? (int)$params['days'] : 7;
// 数据范围:当前用户可见 admin id 集合;null 表示全部
$visibleAdminIds = ($adminInfo !== null && $adminId > 0)
? \app\common\service\DataScope\DataScopeService::getVisibleAdminIds($adminId, $adminInfo)
: null;
$endTime = !empty($params['end_time']) ? strtotime($params['end_time']) : time();
if ($days === 0) {
$startTime = strtotime(date('Y-m-d'));
} else {
$days = $days > 0 ? min($days, 90) : 7;
$startTime = strtotime("-{$days} days", $endTime);
}
$startStr = date('Y-m-d H:i:s', $startTime);
$endStr = date('Y-m-d H:i:s', $endTime);
$depts = \app\common\model\dept\Dept::where('status', 1)
->whereNull('delete_time')
->order('sort desc, id asc')
->column('name', 'id');
$query = Order::where('create_time', 'between', [$startStr, $endStr])
->whereNull('delete_time');
if ($orderType === 0) {
$query->where('status', 4);
} elseif ($orderType === -1) {
$query->whereIn('order_type', $allPaidOrderTypes)->where('status', 2);
} else {
$query->where('order_type', $orderType)->where('status', 2);
}
if ($visibleAdminIds !== null) {
if ($visibleAdminIds === []) {
$query->whereRaw('0 = 1');
} else {
$query->whereIn('creator_id', $visibleAdminIds);
}
}
$orderRows = $query
->field('creator_id, count(*) as cnt, sum(amount) as total_amount')
->group('creator_id')
->select()
->toArray();
$creatorIds = array_unique(array_column($orderRows, 'creator_id'));
$creatorIds = array_filter($creatorIds);
if (empty($creatorIds)) {
return [
'order_type' => $orderType,
'order_type_name' => $orderTypeName,
'date_range' => [date('Y-m-d', $startTime), date('Y-m-d', $endTime)],
'today_total' => 0,
'today_amount' => '0.00',
'today_ranking' => [],
'departments' => [],
'ranking' => [],
'chart' => ['xAxis' => [], 'series' => []],
];
}
$counts = [];
$amounts = [];
foreach ($orderRows as $row) {
$counts[$row['creator_id']] = (int)$row['cnt'];
$amounts[$row['creator_id']] = round((float)$row['total_amount'], 2);
}
$adminDepts = \app\common\model\auth\AdminDept::whereIn('admin_id', $creatorIds)
->select()
->toArray();
$adminToDepts = [];
foreach ($adminDepts as $ad) {
$adminId = $ad['admin_id'];
if (!isset($adminToDepts[$adminId])) {
$adminToDepts[$adminId] = [];
}
$adminToDepts[$adminId][] = (int)$ad['dept_id'];
}
$admins = \app\common\model\auth\Admin::whereIn('id', $creatorIds)
->whereNull('delete_time')
->column('name', 'id');
$todayStart = strtotime(date('Y-m-d'));
$todayEnd = time();
$todayStr = date('Y-m-d H:i:s', $todayStart);
$todayEndStr = date('Y-m-d H:i:s', $todayEnd);
$todayQuery = Order::where('create_time', 'between', [$todayStr, $todayEndStr])
->whereNull('delete_time');
if ($orderType === 0) {
$todayQuery->where('status', 4);
} elseif ($orderType === -1) {
$todayQuery->whereIn('order_type', $allPaidOrderTypes)->where('status', 2);
} else {
$todayQuery->where('order_type', $orderType)->where('status', 2);
}
if ($visibleAdminIds !== null) {
if ($visibleAdminIds === []) {
$todayQuery->whereRaw('0 = 1');
} else {
$todayQuery->whereIn('creator_id', $visibleAdminIds);
}
}
$todayRows = $todayQuery
->field('creator_id, count(*) as cnt, sum(amount) as total_amount')
->group('creator_id')
->select()
->toArray();
$todayCounts = [];
$todayAmounts = [];
foreach ($todayRows as $row) {
$todayCounts[$row['creator_id']] = (int)$row['cnt'];
$todayAmounts[$row['creator_id']] = round((float)$row['total_amount'], 2);
}
$todayTotal = array_sum($todayCounts);
$todayAmount = array_sum($todayAmounts);
$todayRanking = [];
foreach ($creatorIds as $cid) {
$cnt = (int)($todayCounts[$cid] ?? 0);
$amt = (float)($todayAmounts[$cid] ?? 0);
if ($cnt > 0 || $amt > 0) {
$todayRanking[] = [
'id' => (int)$cid,
'name' => $admins[$cid] ?? '未知',
'count' => $cnt,
'amount' => number_format($amt, 2, '.', ''),
];
}
}
usort($todayRanking, fn($a, $b) => $b['count'] <=> $a['count']);
// 数据范围启用时(非 root/全部范围),无可见成员的部门不渲染
$hideEmptyDept = $visibleAdminIds !== null;
$deptData = [];
$assignedIds = [];
foreach ($depts as $deptId => $deptName) {
$members = [];
foreach ($adminToDepts as $adminId => $deptIdsArr) {
if (!in_array((int)$deptId, $deptIdsArr)) {
continue;
}
$assignedIds[] = $adminId;
$members[] = [
'id' => (int)$adminId,
'name' => $admins[$adminId] ?? '未知',
'count' => (int)($counts[$adminId] ?? 0),
'amount' => number_format((float)($amounts[$adminId] ?? 0), 2, '.', ''),
];
}
if ($hideEmptyDept && $members === []) {
continue;
}
$deptTotal = array_sum(array_column($members, 'count'));
$deptAmount = array_sum(array_map(fn($m) => (float)$m['amount'], $members));
$deptData[] = [
'dept_id' => (int)$deptId,
'dept_name' => $deptName,
'members' => $members,
'total' => $deptTotal,
'amount' => number_format($deptAmount, 2, '.', ''),
];
}
$unassignedIds = array_diff($creatorIds, array_unique($assignedIds));
if (!empty($unassignedIds)) {
$members = [];
foreach ($unassignedIds as $adminId) {
$members[] = [
'id' => (int)$adminId,
'name' => $admins[$adminId] ?? '未知',
'count' => (int)($counts[$adminId] ?? 0),
'amount' => number_format((float)($amounts[$adminId] ?? 0), 2, '.', ''),
];
}
$deptData[] = [
'dept_id' => 0,
'dept_name' => '未分配部门',
'members' => $members,
'total' => array_sum(array_column($members, 'count')),
'amount' => number_format(array_sum(array_map(fn($m) => (float)$m['amount'], $members)), 2, '.', ''),
];
}
$ranking = [];
foreach ($creatorIds as $adminId) {
$ranking[] = [
'id' => (int)$adminId,
'name' => $admins[$adminId] ?? '未知',
'count' => (int)($counts[$adminId] ?? 0),
'amount' => number_format((float)($amounts[$adminId] ?? 0), 2, '.', ''),
];
}
usort($ranking, fn($a, $b) => $b['count'] <=> $a['count']);
$chartNames = [];
$chartData = [];
foreach ($ranking as $i => $r) {
$chartNames[] = ($i + 1) . '.' . $r['name'];
$chartData[] = $r['count'];
}
return [
'order_type' => $orderType,
'order_type_name' => $orderTypeName,
'date_range' => [date('Y-m-d', $startTime), date('Y-m-d', $endTime)],
'today_total' => $todayTotal,
'today_amount' => number_format($todayAmount, 2, '.', ''),
'today_ranking' => $todayRanking,
'departments' => $deptData,
'ranking' => $ranking,
'chart' => [
'xAxis' => $chartNames,
'series' => [['name' => '单数', 'data' => $chartData]],
],
];
}
/**
* 与订单列表一致:超管或主管角色可见他人创建的订单
*/
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;
}
/**
* 业务订单可关联支付单列表:仅展示创建时间在此日 0 点及之后的支付单(编辑时当前单已关联的旧单仍保留在列表中)
*/
private static function payOrderLinkableMinCreateTs(): int
{
return strtotime('2026-04-20 00:00:00');
}
/**
* zyt_order.create_time 可能为 int 时间戳或 datetime 字符串,不可直接 (int) 强转(字符串会得到 0)
*/
private static function orderCreateTimeToUnix(mixed $value): int
{
if ($value === null || $value === '') {
return 0;
}
if ($value instanceof \DateTimeInterface) {
return $value->getTimestamp();
}
if (is_int($value)) {
return $value;
}
if (is_float($value)) {
return (int) $value;
}
if (is_string($value)) {
if (is_numeric($value) && strlen($value) <= 12) {
return (int) $value;
}
$ts = strtotime($value);
return $ts !== false ? $ts : 0;
}
return 0;
}
/**
* 某诊单下可关联的支付单(待支付/已支付),供关联业务订单多选(主管/诊单医助看该诊单全部;否则仅本人创建)
* 已占用(关联到未撤回/未取消的业务订单)的支付单会排除;编辑某业务订单时传 $exceptPrescriptionOrderId 以保留当前单已选中的项
* 列表仅含 2026-04-20 及之后创建的支付单(已关联到当前编辑业务订单的除外)
*
* @return array<int, array<string, mixed>>
*/
public static function listPaidOrdersForDiagnosis(int $diagnosisId, int $adminId, array $adminInfo, ?int $exceptPrescriptionOrderId = null): array
{
if ($diagnosisId <= 0) {
return [];
}
$exists = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->count() > 0;
if (!$exists) {
return [];
}
$q = Order::where('patient_id', $diagnosisId)
->whereIn('status', [2]) // 1=待支付, 2=已支付, 5=待审核
->whereNull('delete_time');
if (!self::isOrderListSupervisor($adminId, $adminInfo)) {
$assistantId = (int) Diagnosis::where('id', $diagnosisId)->value('assistant_id');
if ($assistantId !== $adminId) {
$q->where('creator_id', $adminId);
}
}
$rows = $q
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt'])
->order('id', 'desc')
->select()
->toArray();
$activePoIds = PrescriptionOrder::whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->column('id');
$busyPayIds = [];
if ($activePoIds !== []) {
$busyPayIds = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $activePoIds)
->column('pay_order_id');
$busyPayIds = array_unique(array_map('intval', $busyPayIds));
}
$whiteList = [];
if ($exceptPrescriptionOrderId !== null && $exceptPrescriptionOrderId > 0) {
$whiteList = array_map(
'intval',
PrescriptionOrderPayOrder::where('prescription_order_id', $exceptPrescriptionOrderId)->column('pay_order_id')
);
}
$minCreateTs = self::payOrderLinkableMinCreateTs();
$out = [];
foreach ($rows as $row) {
$oid = (int) ($row['id'] ?? 0);
if ($oid <= 0) {
continue;
}
$busy = in_array($oid, $busyPayIds, true);
$allowed = in_array($oid, $whiteList, true);
if ($busy && ! $allowed) {
continue;
}
$ct = self::orderCreateTimeToUnix($row['create_time'] ?? null);
if ($ct < $minCreateTs && ! $allowed) {
continue;
}
$out[] = $row;
}
return $out;
}
/**
* 校验支付单均可关联到指定诊单(已支付、归属与权限)
* 默认仅允许关联 2026-04-20 及之后创建的支付单;编辑业务订单时传入 $prescriptionOrderIdForDateGrace,已关联到该业务订单的旧单除外
*
* @param int[] $ids zyt_order.id
*/
public static function assertPayOrdersLinkable(
array $ids,
int $diagnosisId,
int $adminId,
array $adminInfo,
?int $prescriptionOrderIdForDateGrace = null
): ?string {
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn(int $v): bool => $v > 0)));
if ($diagnosisId <= 0) {
return '诊单无效';
}
if ($ids === []) {
return null;
}
$diagExists = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->count() > 0;
if (!$diagExists) {
return '诊单不存在';
}
$supervisor = self::isOrderListSupervisor($adminId, $adminInfo);
$assistantId = (int) Diagnosis::where('id', $diagnosisId)->value('assistant_id');
$isDiagAssistant = $assistantId === $adminId;
$orders = Order::whereIn('id', $ids)->whereNull('delete_time')->select();
if (count($orders) !== count($ids)) {
return '部分支付单不存在或已删除';
}
$minTs = self::payOrderLinkableMinCreateTs();
foreach ($orders as $o) {
if ((int) $o->patient_id !== $diagnosisId) {
return '支付单与诊单不匹配';
}
if ((int) $o->status !== 2) {
return '仅能关联已支付订单';
}
if (! $supervisor && ! $isDiagAssistant && (int) $o->creator_id !== $adminId) {
return '无权限关联所选支付单';
}
$ct = self::orderCreateTimeToUnix($o->create_time ?? null);
if ($ct < $minTs) {
$grace = false;
if ($prescriptionOrderIdForDateGrace !== null && $prescriptionOrderIdForDateGrace > 0) {
$grace = PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderIdForDateGrace)
->where('pay_order_id', (int) $o->id)
->count() > 0;
}
if (! $grace) {
return '仅可关联 2026年4月20日及之后创建的支付单';
}
}
}
return null;
}
/**
* 批量指派给医助:将「创建人」变更为目标医助(须为医助 role_id=2),用于列表筛选项为创建人=该医助
*
* @param int[] $orderIds
* @return array{success: int, per_log: list<array{order_id: int, summary: string}>, fail: int, errors: list<string>}|false
*/
public static function batchAssignAssistant(array $orderIds, int $assistantAdminId)
{
$assistantAdminId = (int) $assistantAdminId;
if ($assistantAdminId <= 0) {
self::setError('请选择医助');
return false;
}
if (
AdminRole::where('admin_id', $assistantAdminId)
->where('role_id', 2)
->count() === 0
) {
self::setError('目标账号不是医助角色');
return false;
}
$ids = array_values(array_unique(array_filter(array_map('intval', $orderIds), static fn (int $id) => $id > 0)));
if ($ids === []) {
self::setError('请选择订单');
return false;
}
if (count($ids) > 200) {
self::setError('单次最多200单');
return false;
}
$targetName = (string) Admin::where('id', $assistantAdminId)->whereNull('delete_time')->value('name');
if ($targetName === '') {
$targetName = (string) $assistantAdminId;
}
$perLog = [];
$errors = [];
foreach ($ids as $oid) {
$order = Order::where('id', $oid)->whereNull('delete_time')->find();
if (! $order) {
$errors[] = "订单{$oid}不存在或已删除";
continue;
}
$oldCreatorId = (int) $order->creator_id;
if ($oldCreatorId === $assistantAdminId) {
$errors[] = "订单{$oid}创建人已是该医助,已跳过";
continue;
}
$oldName = $oldCreatorId > 0
? (string) Admin::where('id', $oldCreatorId)->whereNull('delete_time')->value('name')
: '';
if ($oldName === '' && $oldCreatorId > 0) {
$oldName = (string) $oldCreatorId;
} elseif ($oldName === '') {
$oldName = '—';
}
$order->creator_id = $assistantAdminId;
if (! $order->save()) {
$errors[] = "订单{$oid}保存失败";
continue;
}
$perLog[] = [
'order_id' => (int) $order->id,
'summary' => '创建人由「' . $oldName . '」变更为医助「' . $targetName . '」',
];
}
if ($perLog === []) {
self::setError($errors !== [] ? implode('', $errors) : '未能变更任何订单');
return false;
}
return [
'success' => count($perLog),
'per_log' => $perLog,
'fail' => count($errors),
'errors' => $errors,
];
}
public static function setExempt(int $id, int $isExempt): bool
{
$order = Order::find($id);
if (!$order) {
self::setError('订单不存在');
return false;
}
$order->is_exempt = $isExempt;
$order->save();
return true;
}
}