1038 lines
38 KiB
PHP
1038 lines
38 KiB
PHP
<?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\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 {
|
||
$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'];
|
||
$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_list:trade_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;
|
||
}
|
||
|
||
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());
|
||
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;
|
||
}
|
||
|
||
if ($order->status != 1) {
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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 => '其他费用',
|
||
];
|
||
|
||
/**
|
||
* @notes 订单统计(按订单类型或退款)
|
||
* @param array $params order_type(-1全部已支付类型合计,0退款,1挂号费,2问诊费,3药品费用,4首付,5尾款,6其他), days(0今天,7,30)
|
||
* @return array
|
||
*/
|
||
public static function orderStats(array $params = [])
|
||
{
|
||
$allPaidOrderTypes = [1, 2, 3, 4, 5, 6];
|
||
$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;
|
||
|
||
$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);
|
||
}
|
||
$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);
|
||
}
|
||
$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']);
|
||
|
||
$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, '.', ''),
|
||
];
|
||
}
|
||
$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 = [0, 3];
|
||
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
|
||
|
||
return count(array_intersect($myRoles, $supervisorRoles)) > 0;
|
||
}
|
||
|
||
/**
|
||
* 某诊单下已支付支付单,供关联业务订单多选(主管/诊单医助看该诊单全部;否则仅本人创建)
|
||
* 已占用(关联到未撤回/未取消的业务订单)的支付单会排除;编辑某业务订单时传 $exceptPrescriptionOrderId 以保留当前单已选中的项
|
||
*
|
||
* @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)
|
||
->where('status', 2)
|
||
->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'])
|
||
->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')
|
||
);
|
||
}
|
||
|
||
$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;
|
||
}
|
||
$out[] = $row;
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* 校验支付单均可关联到指定诊单(已支付、归属与权限)
|
||
*
|
||
* @param int[] $ids zyt_order.id
|
||
*/
|
||
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)));
|
||
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 '部分支付单不存在或已删除';
|
||
}
|
||
|
||
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 '无权限关联所选支付单';
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|