This commit is contained in:
Your Name
2026-04-07 18:13:03 +08:00
parent a780356908
commit fdf714f833
397 changed files with 15086 additions and 1043 deletions
+102 -4
View File
@@ -29,6 +29,83 @@ use think\facade\Log;
*/
class LoginLogic extends BaseLogic
{
/** 非 root 未绑定企微时接口返回,与 axios 约定一致 */
public const CODE_NEED_BIND_WORK_WECHAT = 10;
/**
* 企业微信网页授权 / 扫码绑定所需配置是否齐全
*/
public static function isWorkWechatOAuthConfigured(): bool
{
$corpId = (string) env('work_wechat.corp_id', '');
$secret = (string) env('work_wechat.secret', '');
$agentId = (string) env('work_wechat.agent_id', '');
return $corpId !== '' && $secret !== '' && $agentId !== '';
}
/**
* .env 是否开启「非 root 须绑定企微」。
* 推荐在 [work_wechat] 下写 FORCE_BIND_LOGIN=true;勿在节内写 WORK_WECHAT_FORCE_BIND_LOGIN(会变成双前缀键读不到)。
*/
public static function isForceBindWorkWechatFromEnv(): bool
{
$candidates = [
env('WORK_WECHAT_FORCE_BIND_LOGIN', false),
env('work_wechat.force_bind_login', false),
env('work_wechat.work_wechat_force_bind_login', false),
];
$v = false;
foreach ($candidates as $c) {
if ($c !== false && $c !== null && $c !== '') {
$v = $c;
break;
}
}
if (is_bool($v)) {
return $v;
}
$v = strtolower(trim((string) $v));
return in_array($v, ['1', 'true', 'yes', 'on'], true);
}
/**
* .env 开启强制绑定 + 企微 OAuth 已配置 + 非 root + 未绑定 work_wechat_userid
*
* @param array{root?:int|string,work_wechat_userid?:string} $adminInfo
*/
public static function adminMustBindWorkWechat(array $adminInfo): bool
{
if (!self::isForceBindWorkWechatFromEnv()) {
return false;
}
if (!self::isWorkWechatOAuthConfigured()) {
return false;
}
if ((int) ($adminInfo['root'] ?? 0) === 1) {
return false;
}
return trim((string) ($adminInfo['work_wechat_userid'] ?? '')) === '';
}
/**
* 开启「须绑定企微」时,这些 action 仍须放行(与 $request->action() 比较,不区分大小写)。
* 路由/网关若把 action 变成全小写,严格 in_array('bindWorkWechat') 会失败,导致绑定接口被误判为 code=10,扫码页反复刷新。
*/
public static function isWorkWechatBindExemptActionName(string $action): bool
{
$a = strtolower(trim($action));
return in_array($a, [
'bindworkwechat',
'unbindworkwechat',
'myself',
'logout',
], true);
}
/**
* @notes 管理员账号登录
* @param $params
@@ -55,15 +132,29 @@ class LoginLogic extends BaseLogic
//返回登录信息
$avatar = $admin->avatar ? $admin->avatar : Config::get('project.default_image.admin_avatar');
$avatar = FileService::getFileUrl($avatar);
return [
$row = [
'name' => $adminInfo['name'],
'avatar' => $avatar,
'role_name' => $adminInfo['role_name'],
'token' => $adminInfo['token'],
'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常
];
$row['need_bind_work_wechat'] = self::adminMustBindWorkWechat([
'root' => (int) ($admin->root ?? 0),
'work_wechat_userid' => (string) ($admin->work_wechat_userid ?? ''),
]);
return $row;
}
/**
* 从 auth/getuserinfo 响应解析企业成员 userid。
* 非通讯录成员时接口可能只返回 openid / external_userid,无 userid。
*/
public static function workWechatUserIdFromAuthResponse(array $response): string
{
return trim((string) ($response['userid'] ?? $response['UserId'] ?? ''));
}
/**
* @notes 企业微信授权登录
@@ -98,9 +189,14 @@ class LoginLogic extends BaseLogic
return false;
}
$wxUserId = $response['userid'] ?? '';
if (empty($wxUserId)) {
self::setError('未获取到企业微信用户身份,可能是外部联系人');
$wxUserId = self::workWechatUserIdFromAuthResponse($response);
if ($wxUserId === '') {
$hasOpenId = trim((string) ($response['openid'] ?? $response['OpenId'] ?? '')) !== '';
self::setError(
$hasOpenId
? '当前身份非企业通讯录成员(或未同步到通讯录),无法用此账号登录后台,请使用企业内成员账号或联系管理员将你加入通讯录'
: '未获取到企业微信成员 userid,请检查自建应用 Secret、可信域名是否与当前扫码应用一致'
);
return false;
}
@@ -135,6 +231,8 @@ class LoginLogic extends BaseLogic
'role_name' => $adminInfo['role_name'],
'token' => $adminInfo['token'],
'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常
// 企微扫码登录即已绑定 userid
'need_bind_work_wechat' => false,
];
}
@@ -14,6 +14,7 @@
namespace app\adminapi\logic\auth;
use app\adminapi\logic\LoginLogic;
use app\common\cache\AdminAuthCache;
use app\common\enum\YesNoEnum;
use app\common\logic\BaseLogic;
@@ -314,6 +315,11 @@ class AdminLogic extends BaseLogic
$authRoleIds = AdminRole::where('admin_id', $params['id'])->column('role_id');
$admin['role_ids'] = array_values(array_map('intval', $authRoleIds));
$admin['need_bind_work_wechat'] = LoginLogic::adminMustBindWorkWechat([
'root' => (int) ($admin['root'] ?? 0),
'work_wechat_userid' => (string) ($admin['work_wechat_userid'] ?? ''),
]);
$result['user'] = $admin;
// 当前管理员角色拥有的菜单
$result['menu'] = MenuLogic::getMenuByAdminId($params['id']);
+214 -28
View File
@@ -7,6 +7,9 @@ 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;
@@ -73,7 +76,7 @@ class OrderLogic
/**
* @notes 从企业微信对外收款账单同步订单(员工直接在企业微信发起收款,未经过后台创建)
* 拉取 get_bill_list 接口的已支付记录,创建或更新本地订单
* 拉取 get_bill_listtrade_state 1=已完成(已支付) 3=已完成有退款;会创建订单,并在状态变化时更新(如已支付→已退款)
* @param int $beginTime 开始时间戳(秒)
* @param int $endTime 结束时间戳(秒)
* @return array ['created' => int, 'updated' => int, 'skipped' => int, 'errors' => string[]]
@@ -108,10 +111,13 @@ class OrderLogic
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'] ?? '');
@@ -134,34 +140,76 @@ class OrderLogic
}
$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) {
if ($order->status == 1) {
$order->status = 2;
$order->payment_method = 'wechat';
$order->payment_time = $payTime ? (is_numeric($payTime) ? date('Y-m-d H:i:s', (int)$payTime) : $payTime) : date('Y-m-d H:i:s');
$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;
$order->order_type = $orderType;
$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 {
$needSave = false;
if ($billPayeeUserid && $creatorId !== $defaultCreatorId && (int)$order->creator_id === $defaultCreatorId) {
$order->payee_userid = $billPayeeUserid;
$order->payer_external_userid = $payerExternalUserid;
$order->creator_id = $creatorId;
$needSave = true;
}
if ($amount == 5.00 && (int)$order->order_type !== 1) {
$order->order_type = 1;
$needSave = true;
}
if ($needSave) {
$order->save();
}
$result['skipped']++;
}
} else {
@@ -172,11 +220,18 @@ class OrderLogic
$order->creator_id = $creatorId;
$order->order_type = $orderType;
$order->amount = $amount;
$order->status = 2;
$order->status = $targetStatus;
$order->payment_method = 'wechat';
$order->payment_time = $payTime ? (is_numeric($payTime) ? date('Y-m-d H:i:s', (int)$payTime) : $payTime) : date('Y-m-d H:i:s');
$order->payment_time = $paymentTimeStr;
$order->trade_no = $tradeNo;
$order->remark = '企业微信直接收款同步,待关联患者';
$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();
@@ -652,15 +707,17 @@ class OrderLogic
/**
* @notes 订单统计(按订单类型或退款)
* @param array $params order_type(0退款,1挂号费,2问诊费,3药品费用,4首付,5尾款,6其他), days(0今天,7,30)
* @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 = [])
{
$orderType = isset($params['order_type']) ? (int)$params['order_type'] : 1;
if (!array_key_exists($orderType, self::$orderTypeNames)) {
$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();
@@ -682,6 +739,8 @@ class OrderLogic
->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);
}
@@ -696,7 +755,7 @@ class OrderLogic
if (empty($creatorIds)) {
return [
'order_type' => $orderType,
'order_type_name' => self::$orderTypeNames[$orderType],
'order_type_name' => $orderTypeName,
'date_range' => [date('Y-m-d', $startTime), date('Y-m-d', $endTime)],
'today_total' => 0,
'today_amount' => '0.00',
@@ -738,6 +797,8 @@ class OrderLogic
->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);
}
@@ -835,7 +896,7 @@ class OrderLogic
return [
'order_type' => $orderType,
'order_type_name' => self::$orderTypeNames[$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, '.', ''),
@@ -848,4 +909,129 @@ class OrderLogic
],
];
}
/**
* 与订单列表一致:超管或主管角色可见他人创建的订单
*/
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;
}
}
@@ -2066,7 +2066,57 @@ class DiagnosisLogic extends BaseLogic
->order('a.id', 'asc')
->select()
->toArray();
if ($assistants === []) {
return [];
}
$adminIds = array_column($assistants, 'id');
$adminDepts = \app\common\model\auth\AdminDept::whereIn('admin_id', $adminIds)
->select()
->toArray();
$adminToDeptIds = [];
foreach ($adminDepts as $ad) {
$aid = (int) ($ad['admin_id'] ?? 0);
if ($aid <= 0) {
continue;
}
$adminToDeptIds[$aid][] = (int) ($ad['dept_id'] ?? 0);
}
$allDeptIds = [];
foreach ($adminToDeptIds as $deptIdList) {
foreach ($deptIdList as $did) {
if ($did > 0) {
$allDeptIds[$did] = true;
}
}
}
$allDeptIds = array_keys($allDeptIds);
$deptNameMap = [];
if ($allDeptIds !== []) {
$deptNameMap = \app\common\model\dept\Dept::whereIn('id', $allDeptIds)
->whereNull('delete_time')
->column('name', 'id');
}
foreach ($assistants as &$row) {
$aid = (int) ($row['id'] ?? 0);
$idsForAdmin = [];
foreach ($adminToDeptIds[$aid] ?? [] as $did) {
if ($did > 0) {
$idsForAdmin[] = $did;
}
}
$row['dept_ids'] = array_values(array_unique($idsForAdmin));
$names = [];
foreach ($row['dept_ids'] as $did) {
if (!empty($deptNameMap[$did])) {
$names[] = (string) $deptNameMap[$did];
}
}
$row['dept_names'] = $names !== [] ? implode('、', array_unique($names)) : '';
}
unset($row);
return $assistants;
} catch (\Exception $e) {
\think\facade\Log::error('获取医助列表失败: ' . $e->getMessage());
@@ -2737,7 +2787,9 @@ class DiagnosisLogic extends BaseLogic
'systolic_pressure' => !empty($params['systolic_pressure']) ? intval($params['systolic_pressure']) : null,
'diastolic_pressure' => !empty($params['diastolic_pressure']) ? intval($params['diastolic_pressure']) : null,
'fasting_blood_sugar' => !empty($params['fasting_blood_sugar']) ? floatval($params['fasting_blood_sugar']) : null,
'diabetes_discovery_year' => !empty($params['diabetes_discovery_year']) ? intval($params['diabetes_discovery_year']) : null,
'diabetes_discovery_year' => isset($params['diabetes_discovery_year'])
? (trim((string) $params['diabetes_discovery_year']) !== '' ? trim((string) $params['diabetes_discovery_year']) : null)
: null,
'local_hospital_diagnosis' => $params['local_hospital_diagnosis'] ?? '',
'local_hospital_name' => $params['local_hospital_name'] ?? '',
'local_hospital_visit_date' => $params['local_hospital_visit_date'] ?? '',
@@ -6,6 +6,7 @@ namespace app\adminapi\logic\tcm;
use app\common\model\auth\Admin;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\Diagnosis;
use app\common\service\wechat\WechatWorkAppMessageService;
use think\facade\Config;
@@ -82,6 +83,11 @@ class PrescriptionLogic
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;
@@ -187,6 +193,12 @@ class PrescriptionLogic
$sn = self::generateSn();
}
$assistantIdForRx = 0;
if ($diagnosisIdRule > 0) {
$diagAssistant = Diagnosis::where('id', $diagnosisIdRule)->value('assistant_id');
$assistantIdForRx = (int) ($diagAssistant ?? 0);
}
$data = [
'sn' => $sn,
'prescription_name' => $params['prescription_name'] ?? '',
@@ -228,11 +240,13 @@ class PrescriptionLogic
'audit_by_name' => '',
'audit_remark' => '',
'creator_id' => $adminId,
'assistant_id' => $assistantIdForRx,
];
$prescription = new Prescription();
$prescription->save($data);
return (int)$prescription->id;
return (int) $prescription->id;
}
/**
@@ -247,10 +261,13 @@ class PrescriptionLogic
return false;
}
// 已通过且未作废:不可编辑(与消费者端仅「查看」一致)
// 已通过且未作废:不可编辑;例外:业务订单「处方审核」已驳回时允许医生改方,保存后业务订单侧审核会重置为待审
if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) {
self::setError('该处方已通过审核,不可编辑');
return false;
if (!PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject((int) $params['id'])) {
self::setError('该处方已通过审核,不可编辑');
return false;
}
}
// 已驳回:仅当该诊单+当天+同一创建人下没有另一条「已通过且未作废」的处方时允许重新编辑(改后待审核、作废一并清除)
@@ -300,8 +317,17 @@ class PrescriptionLogic
$wasVoid = (int) ($prescription->void_status ?? 0) === 1;
$assistantIdForRx = (int) ($prescription->assistant_id ?? 0);
if ($newDiagnosisId > 0) {
$diagAssistant = Diagnosis::where('id', $newDiagnosisId)->value('assistant_id');
$assistantIdForRx = (int) ($diagAssistant ?? 0);
} else {
$assistantIdForRx = 0;
}
$data = [
'diagnosis_id' => $newDiagnosisId,
'assistant_id' => $assistantIdForRx,
'prescription_name' => $params['prescription_name'] ?? $prescription->prescription_name,
'prescription_type' => $params['prescription_type'] ?? $prescription->prescription_type,
'patient_name' => $params['patient_name'] ?? $prescription->patient_name,
@@ -344,6 +370,8 @@ class PrescriptionLogic
}
$prescription->save($data);
PrescriptionOrderLogic::onConsumerPrescriptionSaved((int) $params['id']);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
@@ -395,6 +423,15 @@ class PrescriptionLogic
$arr['gender_desc'] = $arr['gender'] == 1 ? '男' : '女';
$arr['visible_role_ids'] = self::visibleRoleIdsToArray($arr['visible_role_ids'] ?? '');
$bizPo = PrescriptionOrder::where('prescription_id', $id)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->order('id', 'desc')
->find();
$arr['business_prescription_audit_rejected'] = $bizPo ? 1 : 0;
$arr['business_prescription_audit_remark'] = $bizPo ? (string) ($bizPo->prescription_audit_remark ?? '') : '';
return $arr;
}
@@ -0,0 +1,780 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\adminapi\logic\order\OrderLogic;
use app\common\model\Order;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\service\ExpressTrackService;
use think\facade\Config;
class PrescriptionOrderLogic
{
private static string $error = '';
public static function setError(string $msg): void
{
self::$error = $msg;
}
public static function getError(): string
{
return self::$error;
}
/**
* @param int[] $allowRoleIds
*/
private static function roleIntersect(array $adminInfo, array $allowRoleIds): bool
{
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
$allow = array_map('intval', $allowRoleIds);
return count(array_intersect($myRoles, $allow)) > 0;
}
/** 与 order 列表一致:超管或 order_edit_all_roles 可见全部业务订单 */
public static function canSeeAllPrescriptionOrders(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$roles = Config::get('project.order_edit_all_roles', [0, 3]);
return self::roleIntersect($adminInfo, is_array($roles) ? $roles : []);
}
/**
* @param PrescriptionOrder $row
*/
public static function canAccessOrder($row, int $adminId, array $adminInfo): bool
{
if (self::canSeeAllPrescriptionOrders($adminInfo)) {
return true;
}
if ((int) $row->creator_id === $adminId) {
return true;
}
$aid = (int) Diagnosis::where('id', (int) $row->diagnosis_id)->whereNull('delete_time')->value('assistant_id');
return $aid === $adminId;
}
/**
* @param PrescriptionOrder $row
*/
public static function canWithdrawOrder($row, int $adminId, array $adminInfo): bool
{
return self::canAccessOrder($row, $adminId, $adminInfo);
}
public static function canViewInternalCost(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_order_finance_roles', [0, 3]);
$allow = array_map('intval', is_array($allow) ? $allow : []);
$myRoles = array_map('intval', $adminInfo['role_id'] ?? []);
return count(array_intersect($myRoles, $allow)) > 0;
}
public static function canAuditPrescriptionOrder(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_audit_roles', [0, 3, 6]);
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
}
public static function canAuditPaymentSlipOrder(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3]);
return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []);
}
/**
* @param array<string,mixed> $row
*/
public static function maskInternalCostIfNeeded(array &$row, array $adminInfo): void
{
if (!self::canViewInternalCost($adminInfo)) {
unset($row['internal_cost']);
}
}
public static function generateOrderNo(): string
{
return 'PO' . date('YmdHis') . mt_rand(100000, 999999);
}
private static function normalizeExpressCompany($raw): string
{
$ec = strtolower(trim((string) ($raw ?? '')));
if ($ec === '') {
return 'auto';
}
if (!in_array($ec, ['sf', 'jd', 'jt', 'jtexpress', 'auto'], true)) {
return 'auto';
}
// 统一 jtexpress 为 jt
if ($ec === 'jtexpress') {
return 'jt';
}
return $ec;
}
/** 业务订单「处方审核」驳回后,允许医生继续编辑已通过的消费者处方 */
public static function allowDoctorEditApprovedPrescriptionDueToBusinessReject(int $prescriptionId): bool
{
return PrescriptionOrder::where('prescription_id', $prescriptionId)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->count() > 0;
}
/**
* 医生保存消费者处方后:重置关联业务订单的处方/支付单审核为待审(因处方内容已变)
*/
public static function onConsumerPrescriptionSaved(int $prescriptionId): void
{
$rows = PrescriptionOrder::where('prescription_id', $prescriptionId)
->where('prescription_audit_status', 2)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->select();
foreach ($rows as $order) {
$order->prescription_audit_status = 0;
$order->prescription_audit_remark = '';
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
self::syncFulfillmentStatus($order);
$order->save();
}
}
private static function syncFulfillmentStatus(PrescriptionOrder $order): void
{
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
return;
}
$pa = (int) $order->prescription_audit_status;
$pay = (int) $order->payment_slip_audit_status;
if ($pa === 1 && $pay === 1) {
$order->fulfillment_status = 2;
} elseif ($pa === 2 || $pay === 2) {
$order->fulfillment_status = 1;
}
}
/**
* @param mixed $raw
* @return int[]
*/
private static function normalizePayOrderIds($raw): array
{
if (!is_array($raw)) {
return [];
}
$ids = array_map('intval', $raw);
return array_values(array_unique(array_filter($ids, static fn (int $v): bool => $v > 0)));
}
/**
* 解除业务订单与支付单的关联(撤回/驳回后支付单可再次被选用)
*/
private static function clearPayOrderLinks(PrescriptionOrder $order): void
{
PrescriptionOrderPayOrder::where('prescription_order_id', (int) $order->id)->delete();
$order->linked_pay_order_id = null;
}
/**
* 支付单不可同时关联多条「有效」业务订单(未删除且未撤回/取消);$exceptPrescriptionOrderId 为当前编辑单 ID 时跳过自检
*
* @param int[] $payOrderIds
*/
private static function assertPayOrdersExclusive(array $payOrderIds, ?int $exceptPrescriptionOrderId): ?string
{
$ids = array_values(array_unique(array_filter(array_map('intval', $payOrderIds), static fn (int $v): bool => $v > 0)));
if ($ids === []) {
return null;
}
$activePoIds = PrescriptionOrder::whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->column('id');
if ($activePoIds === []) {
return null;
}
$activeSet = array_fill_keys(array_map('intval', $activePoIds), true);
$links = PrescriptionOrderPayOrder::whereIn('pay_order_id', $ids)->select();
foreach ($links as $link) {
$poId = (int) $link->prescription_order_id;
if ($exceptPrescriptionOrderId !== null && $poId === $exceptPrescriptionOrderId) {
continue;
}
if (!isset($activeSet[$poId])) {
continue;
}
return '支付单 #' . (int) $link->pay_order_id . ' 已关联其他有效业务订单,不可重复关联';
}
return null;
}
/**
* @param int[] $payOrderIds
*/
private static function replacePayOrderLinks(int $prescriptionOrderId, array $payOrderIds): void
{
PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderId)->delete();
$t = time();
foreach ($payOrderIds as $pid) {
$link = new PrescriptionOrderPayOrder();
$link->prescription_order_id = $prescriptionOrderId;
$link->pay_order_id = $pid;
$link->create_time = $t;
$link->save();
}
}
/**
* @return int[]
*/
private static function linkedPayOrderIdList(int $prescriptionOrderId): array
{
return array_map(
'intval',
PrescriptionOrderPayOrder::where('prescription_order_id', $prescriptionOrderId)->order('id', 'asc')->column('pay_order_id')
);
}
/**
* @return array<int, array<string, mixed>>
*/
private static function linkedPayOrdersPayload(array $ids): array
{
if ($ids === []) {
return [];
}
$rows = Order::whereIn('id', $ids)->whereNull('delete_time')
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark'])
->order('id', 'asc')
->select()
->toArray();
return $rows;
}
/**
* @param array<string,mixed> $arr
*/
private static function attachLinkedPayOrders(array &$arr): void
{
$id = (int) ($arr['id'] ?? 0);
if ($id <= 0) {
$arr['pay_order_ids'] = [];
$arr['linked_pay_orders'] = [];
return;
}
$ids = self::linkedPayOrderIdList($id);
$arr['pay_order_ids'] = $ids;
$arr['linked_pay_orders'] = self::linkedPayOrdersPayload($ids);
}
/**
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
public static function create(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$rxId = (int) $params['prescription_id'];
$diagId = (int) $params['diagnosis_id'];
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids'] ?? []);
$rx = PrescriptionLogic::detail($rxId, $adminId, $adminInfo);
if ($rx === null) {
$err = PrescriptionLogic::getError();
self::$error = $err !== '' ? $err : '处方不存在';
return false;
}
if ((int) ($rx['diagnosis_id'] ?? 0) !== $diagId) {
self::$error = '诊单与处方不匹配';
return false;
}
if (PrescriptionOrder::where('prescription_id', $rxId)->whereNull('delete_time')->where('fulfillment_status', '<>', 4)->count() > 0) {
self::$error = '该处方已存在有效业务订单(未撤回前不可重复创建)';
return false;
}
$diag = Diagnosis::where('id', $diagId)->whereNull('delete_time')->find();
if (!$diag) {
self::$error = '诊单不存在';
return false;
}
if ($payOrderIds !== []) {
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
$exErr = self::assertPayOrdersExclusive($payOrderIds, null);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
}
$internalCost = null;
if (isset($params['internal_cost']) && $params['internal_cost'] !== '' && $params['internal_cost'] !== null) {
if (self::canViewInternalCost($adminInfo)) {
$internalCost = round((float) $params['internal_cost'], 2);
}
}
$medDays = $params['medication_days'] ?? null;
$medDays = $medDays === '' || $medDays === null ? null : (int) $medDays;
$order = new PrescriptionOrder();
$order->order_no = self::generateOrderNo();
$order->prescription_id = $rxId;
$order->diagnosis_id = $diagId;
$order->creator_id = $adminId;
$order->recipient_name = (string) $params['recipient_name'];
$order->recipient_phone = (string) $params['recipient_phone'];
$order->shipping_address = (string) $params['shipping_address'];
$order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0;
$order->medication_days = $medDays > 0 ? $medDays : null;
$order->prev_staff = (string) ($params['prev_staff'] ?? '');
$order->service_channel = (string) ($params['service_channel'] ?? '');
$order->service_package = (string) ($params['service_package'] ?? '');
$order->tracking_number = (string) ($params['tracking_number'] ?? '');
$order->express_company = self::normalizeExpressCompany($params['express_company'] ?? 'auto');
$order->fee_type = (int) $params['fee_type'];
$order->amount = round((float) $params['amount'], 2);
$order->internal_cost = $internalCost;
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
$order->prescription_audit_status = 0;
$order->prescription_audit_remark = '';
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
$order->fulfillment_status = 1;
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
if ($payOrderIds !== []) {
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
public static function detail(int $id, int $adminId, array $adminInfo): ?array
{
self::$error = '';
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$row) {
self::$error = '订单不存在';
return null;
}
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
self::$error = '无权限查看';
return null;
}
$arr = $row->toArray();
self::maskInternalCostIfNeeded($arr, $adminInfo);
self::attachLinkedPayOrders($arr);
return $arr;
}
/**
* 物流轨迹(顺丰/京东等,优先快递100;未配置密钥时返回官网链接)
*
* @return array<string,mixed>|null
*/
public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo): ?array
{
self::$error = '';
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$row) {
self::$error = '订单不存在';
return null;
}
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
self::$error = '无权限查看';
return null;
}
$num = trim((string) ($row->tracking_number ?? ''));
if ($num === '') {
self::$error = '未填写快递单号';
return null;
}
$ec = trim($expressCompanyOverride) !== '' ? self::normalizeExpressCompany($expressCompanyOverride) : self::normalizeExpressCompany($row->express_company ?? 'auto');
// 优先从数据库查询物流追踪信息
$trackingData = \app\common\service\ExpressTrackingService::getDetailByTrackingNumber($num);
if ($trackingData && !empty($trackingData['traces'])) {
// 从数据库获取到数据,直接返回
$payload = [
'carrier' => $trackingData['express_company'],
'carrier_label' => $trackingData['express_company_name'],
'kuaidi_com' => $trackingData['express_company'],
'traces' => array_map(function($trace) {
return [
'time' => $trace['trace_time'],
'ftime' => $trace['trace_time'],
'context' => $trace['trace_context'],
'location' => $trace['location'],
'status' => $trace['status'],
'statusCode' => $trace['status_code'],
];
}, $trackingData['traces']),
'state' => $trackingData['current_state'],
'state_text' => $trackingData['current_state_text'],
'source' => 'database',
'hint' => '',
'official_url' => '',
'last_query_time' => date('Y-m-d H:i:s', $trackingData['last_query_time']),
'query_count' => $trackingData['query_count'],
];
} else {
// 数据库没有数据,调用快递100 API
$payload = ExpressTrackService::query($ec, $num, (string) ($row->recipient_phone ?? ''));
$payload['source'] = 'api';
}
$payload['official_urls'] = ExpressTrackService::officialUrls($num);
$payload['tracking_number'] = $num;
$payload['express_company_used'] = $ec;
return $payload;
}
/**
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
public static function edit(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;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '已完成或已取消的订单不可编辑';
return false;
}
$medDays = $params['medication_days'] ?? null;
$medDays = $medDays === '' || $medDays === null ? null : (int) $medDays;
$order->recipient_name = (string) $params['recipient_name'];
$order->recipient_phone = (string) $params['recipient_phone'];
$order->shipping_address = (string) $params['shipping_address'];
$order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0;
$order->medication_days = $medDays > 0 ? $medDays : null;
$order->prev_staff = (string) ($params['prev_staff'] ?? '');
$order->service_channel = (string) ($params['service_channel'] ?? '');
$order->service_package = (string) ($params['service_package'] ?? '');
$order->tracking_number = (string) ($params['tracking_number'] ?? '');
if (array_key_exists('express_company', $params)) {
$order->express_company = self::normalizeExpressCompany($params['express_company']);
}
$order->fee_type = (int) $params['fee_type'];
$order->amount = round((float) $params['amount'], 2);
$order->remark_extra = (string) ($params['remark_extra'] ?? '');
$diagId = (int) $order->diagnosis_id;
if (array_key_exists('pay_order_ids', $params)) {
$payOrderIds = self::normalizePayOrderIds($params['pay_order_ids']);
$linkErr = OrderLogic::assertPayOrdersLinkable($payOrderIds, $diagId, $adminId, $adminInfo);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
$exErr = self::assertPayOrdersExclusive($payOrderIds, (int) $order->id);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
$order->linked_pay_order_id = $payOrderIds !== [] ? $payOrderIds[0] : null;
} elseif (array_key_exists('linked_pay_order_id', $params)) {
$lp = $params['linked_pay_order_id'];
if ($lp === '' || $lp === null || (int) $lp <= 0) {
self::replacePayOrderLinks((int) $order->id, []);
$order->linked_pay_order_id = null;
} else {
$single = [(int) $lp];
$linkErr = OrderLogic::assertPayOrdersLinkable($single, $diagId, $adminId, $adminInfo);
if ($linkErr !== null) {
self::$error = $linkErr;
return false;
}
$exErr = self::assertPayOrdersExclusive($single, (int) $order->id);
if ($exErr !== null) {
self::$error = $exErr;
return false;
}
self::replacePayOrderLinks((int) $order->id, $single);
$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 {
$order->internal_cost = round((float) $raw, 2);
}
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<string,mixed>|false
*/
public static function withdraw(int $id, int $adminId, array $adminInfo)
{
self::$error = '';
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canWithdrawOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限撤回';
return false;
}
if ((int) $order->fulfillment_status !== 1) {
self::$error = '仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程';
return false;
}
$order->fulfillment_status = 4;
self::clearPayOrderLinks($order);
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<string,mixed>|false
*/
public static function auditPrescription(int $id, string $action, string $remark, 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;
}
if ((int) $order->prescription_audit_status !== 0) {
self::$error = '当前无需处方审核';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '订单已结束';
return false;
}
if ($action === 'reject' && trim($remark) === '') {
self::$error = '驳回时请填写审核意见';
return false;
}
if ($action === 'approve') {
$order->prescription_audit_status = 1;
$order->prescription_audit_remark = mb_substr(trim($remark), 0, 500);
} else {
$order->prescription_audit_status = 2;
$order->prescription_audit_remark = mb_substr(trim($remark), 0, 500);
$order->payment_slip_audit_status = 0;
$order->payment_slip_audit_remark = '';
}
self::syncFulfillmentStatus($order);
if ($action === 'reject') {
self::clearPayOrderLinks($order);
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
/**
* @return array<string,mixed>|false
*/
public static function auditPaymentSlip(int $id, string $action, string $remark, 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;
}
if ((int) $order->payment_slip_audit_status !== 0) {
self::$error = '当前无需支付单审核';
return false;
}
$fs = (int) $order->fulfillment_status;
if ($fs === 3 || $fs === 4) {
self::$error = '订单已结束';
return false;
}
if ($action === 'reject' && trim($remark) === '') {
self::$error = '驳回时请填写审核意见';
return false;
}
if ($action === 'approve') {
$order->payment_slip_audit_status = 1;
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
} else {
$order->payment_slip_audit_status = 2;
$order->payment_slip_audit_remark = mb_substr(trim($remark), 0, 500);
}
self::syncFulfillmentStatus($order);
if ($action === 'reject') {
self::clearPayOrderLinks($order);
}
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
}