预约优化

main
鹿和sa0ChunLuyu 2 weeks ago
parent eb14f73b32
commit 5b4862ba49

@ -63,8 +63,15 @@ public function GetUserList(){
public function AutoLogin(){ public function AutoLogin(){
$usercode = request('usercode'); $usercode = request('usercode');
$deptcode = request('deptcode'); $deptcode = request('deptcode');
if(empty($usercode) || empty($deptcode)){
return \Yz::JsonError("参数不全"); $missingParams = [];
if (empty($usercode)) $missingParams[] = '医生编码';
if (empty($deptcode)) $missingParams[] = '科室编码';
if (!empty($missingParams)) {
return \Yz::JsonReturn(false, '缺少参数:' . implode('、', $missingParams), [
'missing_params' => $missingParams
]);
} }
$dept=DB::table('s_department')->where(['department_number' => $deptcode])->first(); $dept=DB::table('s_department')->where(['department_number' => $deptcode])->first();
if(!$dept) return \Yz::JsonError("科室不存在"); if(!$dept) return \Yz::JsonError("科室不存在");
@ -81,6 +88,20 @@ public function AutoLogin(){
$access_token = $jwt->BuildJWT('yz','access',$user->id,$user->group,$accessTimeout); $access_token = $jwt->BuildJWT('yz','access',$user->id,$user->group,$accessTimeout);
$refresh_token = $jwt->BuildJWT('yz','refresh',$user->id,'',$refreshTimeout); $refresh_token = $jwt->BuildJWT('yz','refresh',$user->id,'',$refreshTimeout);
DB::table('users')->where(['id'=>$user->id,'status'=>1])->update(['token'=>md5($refresh_token)]); DB::table('users')->where(['id'=>$user->id,'status'=>1])->update(['token'=>md5($refresh_token)]);
// 记录 HIS 登录日志
try {
$allParams = request()->all();
\App\Services\LogService::DoctorLoginLog(
request()->ip(),
http_build_query($allParams),
json_encode($allParams, JSON_UNESCAPED_UNICODE),
'HIS登录'
);
} catch (\Exception $e) {
// 日志写入失败不影响主流程
}
return \Yz::JsonReturn(true,'登录成功',['access_token'=>$access_token,'refresh_token'=>$refresh_token]); return \Yz::JsonReturn(true,'登录成功',['access_token'=>$access_token,'refresh_token'=>$refresh_token]);
} }
} }

@ -436,6 +436,8 @@ public function GetOptimalPlan($type, $regnum, $entrustids, $episodeid, $appoint
if ($type === 'full_day') { if ($type === 'full_day') {
return $this->doFullDayAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids); return $this->doFullDayAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids);
} elseif ($type === 'same_day_slot') {
return $this->doSameDaySlotAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids);
} else { } else {
return $this->doOptimalTimeAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids); return $this->doOptimalTimeAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids);
} }
@ -619,6 +621,125 @@ private function doFullDayAssign($startDate, $endDate, $regnum, $entrustids, $ep
return \Yz::Return(true, '获取成功', []); return \Yz::Return(true, '获取成功', []);
} }
// 同一天同一时间段分配:所有 item 必须在同一天且同一时间段完成
private function doSameDaySlotAssign($startDate, $endDate, $regnum, $entrustids, $episodeid, $appointment_type, $items, $scope, $userDeptId, $occupied_plan_ids = [])
{
// 按科室分组
$deptGroups = $this->groupItemsByDept($items);
if (empty($deptGroups)) {
return \Yz::Return(true, '获取成功', []);
}
$currentDate = clone $startDate;
while ($currentDate <= $endDate) {
$nowdate = $currentDate->format('Y-m-d');
// 收集所有科室在该日期的可用号源
$allDeptPlans = [];
$allAssigned = true;
foreach ($deptGroups as $deptCode => $group) {
$availablePlans = $this->getAvailablePlansForDate($regnum, $group['entrustids'], $episodeid, $appointment_type, $nowdate, $scope, $userDeptId);
// 过滤已占用的号源
if (!empty($occupied_plan_ids)) {
$availablePlans = array_filter($availablePlans, function($plan) use ($occupied_plan_ids) {
return !in_array($plan->id, $occupied_plan_ids);
});
$availablePlans = array_values($availablePlans);
}
if (empty($availablePlans)) {
$allAssigned = false;
break;
}
$allDeptPlans[$deptCode] = $availablePlans;
}
if (!$allAssigned) {
$currentDate->modify('+1 day');
continue;
}
// 按时间段分组:提取所有科室共有的时间段
$slotGroups = [];
foreach ($allDeptPlans as $deptCode => $plans) {
foreach ($plans as $plan) {
$slotKey = $plan->begin_time . '-' . $plan->end_time;
if (!isset($slotGroups[$slotKey])) {
$slotGroups[$slotKey] = [];
}
if (!isset($slotGroups[$slotKey][$deptCode])) {
$slotGroups[$slotKey][$deptCode] = [];
}
$slotGroups[$slotKey][$deptCode][] = $plan;
}
}
// 对每个时间段,尝试分配所有项目
foreach ($slotGroups as $slotKey => $deptPlans) {
$result = [];
$allInSlot = true;
foreach ($deptGroups as $deptCode => $group) {
if (!isset($deptPlans[$deptCode])) {
$allInSlot = false;
break;
}
$availablePlans = $deptPlans[$deptCode];
$usedCapacity = [];
$assignedPlans = [];
foreach ($group['items'] as $item) {
$assigned = false;
foreach ($availablePlans as $plan) {
$planId = $plan->id;
$remaining = ($plan->count ?? 0) - ($plan->used_count ?? 0);
$used = $usedCapacity[$planId] ?? 0;
if (($remaining - $used) <= 0) continue;
// 冲突检查:同一科室组内,不同排班时间不能重叠
if ($this->isTimeConflict($assignedPlans, $plan)) continue;
$result[] = [
'id' => $item['id'],
'plan_id' => $planId,
'department_resources_name' => $plan->department_resources_name,
'date' => $plan->date,
'begin_time' => $plan->begin_time,
'end_time' => $plan->end_time
];
$usedCapacity[$planId] = ($usedCapacity[$planId] ?? 0) + 1;
$assignedPlans[] = [
'resource_name' => $plan->department_resources_name,
'begin_time' => $plan->begin_time,
'end_time' => $plan->end_time
];
$assigned = true;
break;
}
if (!$assigned) {
$allInSlot = false;
break 2;
}
}
}
if ($allInSlot) {
return \Yz::Return(true, '获取成功', $result);
}
}
$currentDate->modify('+1 day');
}
return \Yz::Return(true, '获取成功', []);
}
// 获取指定日期的可用号源(已排序) // 获取指定日期的可用号源(已排序)
private function getAvailablePlansForDate($regnum, $entrustids, $episodeid, $appointment_type, $date, $scope, $userDeptId) private function getAvailablePlansForDate($regnum, $entrustids, $episodeid, $appointment_type, $date, $scope, $userDeptId)
{ {

@ -66,6 +66,33 @@ public static function CheckTableName(){ // 查看日志表是否存在,每
// 并发时另一个请求已先创建,忽略此异常 // 并发时另一个请求已先创建,忽略此异常
} }
} }
public static function DoctorLoginLog($ip, $queryString, $params, $mark)
{
$tableName = 'zz_doctor_login_' . date('ym');
if (!Schema::hasTable($tableName)) {
try {
Schema::create($tableName, function (Blueprint $table) {
$table->id();
$table->string('mark', 50)->nullable();
$table->string('request_ip', 45)->nullable();
$table->text('query_string')->nullable();
$table->text('params')->nullable();
$table->string('create_time', 30);
$table->timestamps();
});
} catch (\Exception $e) {
// 并发时另一个请求已先创建,忽略
}
}
DB::table($tableName)->insert([
'mark' => $mark,
'request_ip' => $ip,
'query_string' => $queryString,
'params' => $params,
'create_time' => date('Y-m-d H:i:s'),
]);
}
public static function JsonEncode($data){ //格式化数据转json public static function JsonEncode($data){ //格式化数据转json
$post_data =$data; $post_data =$data;
foreach ($post_data as $key => $post_datum) { foreach ($post_data as $key => $post_datum) {

@ -3,6 +3,8 @@
return [ return [
'globals' => [ 'globals' => [
'FrontendUrl'=>env('FRONTEND_URL', 'http://192.168.80.76'), //前端页面地址
'YingGuBaseUrl'=>'http://192.168.0.1', //盈谷url 'YingGuBaseUrl'=>'http://192.168.0.1', //盈谷url

@ -2,6 +2,9 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -32,8 +35,21 @@
//his跳到此路由再跳转到对应的系统页面 //his跳到此路由再跳转到对应的系统页面
Route::get('/yiji', function (Request $request) { Route::get('/yiji', function (Request $request) {
$queryString = $request->server('QUERY_STRING'); // 获取原始查询字符串 $queryString = $request->server('QUERY_STRING'); // 获取原始查询字符串
// 记录医技预约跳转日志
try {
\App\Services\LogService::DoctorLoginLog(
$request->ip(),
$queryString,
json_encode($request->all(), JSON_UNESCAPED_UNICODE),
'医技预约'
);
} catch (\Exception $e) {
// 日志写入失败不影响主流程
}
//return redirect("http://192.168.80.76/jq_page/appointment.html?".$queryString); //return redirect("http://192.168.80.76/jq_page/appointment.html?".$queryString);
return redirect("http://192.168.80.76/admin/#/doctorappointment?".$queryString); return redirect(config('app.globals.FrontendUrl')."/admin/#/doctorappointment?".$queryString);
}); });
//cas 登录 //cas 登录

@ -101,13 +101,13 @@
</el-tooltip> </el-tooltip>
</div> </div>
<el-tooltip placement="top" content="点击此按钮,系统会推荐最近能用的时间段号源,多个检查项目可能会跨天跨时间段" popper-class="tip-pre-line"> <el-tooltip placement="top" content="点击此按钮,系统会推荐最近能用的时间段号源,多个检查项目可能会跨天跨时间段" popper-class="tip-pre-line">
<el-button class="do_button_recommend" type="primary" style="margin-left: 20px;" @click="optimalTimeClick"></el-button> <el-button class="do_button_recommend" type="primary" style="margin-left: 20px; width: 120px;" @click="optimalTimeClick"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip placement="top" content="点击此按钮,系统会推荐最近的能在同一天完成检查的号源" popper-class="tip-pre-line"> <el-tooltip placement="top" content="点击此按钮,系统会推荐最近的能在同一天完成检查的号源" popper-class="tip-pre-line">
<el-button class="do_button_recommend" type="primary" @click="fullDayClick"></el-button> <el-button class="do_button_recommend" type="primary" style="width: 120px;" @click="fullDayClick"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip placement="top" content="点击此按钮,系统会推荐最近的能在同一时段完成检查的号源" popper-class="tip-pre-line"> <el-tooltip placement="top" content="点击此按钮,系统会推荐最近的能在同一天同一段完成检查的号源" popper-class="tip-pre-line">
<el-button class="do_button_recommend" type="primary" @click="sameTimeSlotClick"></el-button> <el-button class="do_button_recommend" type="primary" @click="sameTimeSlotClick"></el-button>
</el-tooltip> </el-tooltip>
</div> </div>
<div> <div>
@ -275,6 +275,7 @@
let TanChuangMsgDialogVisible=ref(false) let TanChuangMsgDialogVisible=ref(false)
let crossTimeDialogVisible = ref(false) let crossTimeDialogVisible = ref(false)
let crossTimeDialogHtml = ref('') let crossTimeDialogHtml = ref('')
let cachedCrossDialogData = ref(null) // dialog { html, idx }
let resultDialogVisible = ref(false) let resultDialogVisible = ref(false)
let resultDialogHtml = ref('') let resultDialogHtml = ref('')
const getWeekday = (date1) => { const getWeekday = (date1) => {
@ -376,6 +377,17 @@
GetEnablePlanFunc(); GetEnablePlanFunc();
} }
} }
// dialog
if (cachedCrossDialogData.value) {
const currentIdx = entrustTableDate.value.findIndex(
r => selectedMianListId.value.includes(r.id)
) + 1
if (currentIdx === cachedCrossDialogData.value.idx) {
crossTimeDialogHtml.value = cachedCrossDialogData.value.html
crossTimeDialogVisible.value = true
cachedCrossDialogData.value = null
}
}
checkSameSourceRoom() checkSameSourceRoom()
} }
const checkSameSourceRoom = () => { const checkSameSourceRoom = () => {
@ -732,8 +744,17 @@
crossMsg += '<tr style="background:' + bgColor + ';"><td style="padding:4px 8px;border:1px solid #ddd;">' + p.idx + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.entrust + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.room + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.date + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.timeSlot + '</td></tr>' crossMsg += '<tr style="background:' + bgColor + ';"><td style="padding:4px 8px;border:1px solid #ddd;">' + p.idx + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.entrust + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.room + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.date + '</td><td style="padding:4px 8px;border:1px solid #ddd;">' + p.timeSlot + '</td></tr>'
}) })
crossMsg += '</table>' crossMsg += '</table>'
crossTimeDialogHtml.value = crossMsg //
crossTimeDialogVisible.value = true const minIdx = Math.min(...crossParts.map(p => p.idx))
const selectedIdx = entrustTableDate.value.findIndex(
r => selectedMianListId.value.includes(r.id)
) + 1
if (selectedIdx === minIdx) {
crossTimeDialogHtml.value = crossMsg
crossTimeDialogVisible.value = true
} else {
cachedCrossDialogData.value = { html: crossMsg, idx: minIdx }
}
} }
// //
@ -1111,7 +1132,7 @@
} }
}) })
} }
// //
const sameTimeSlotClick = () => { const sameTimeSlotClick = () => {
if (selectedEntrustId.value.length === 0) { if (selectedEntrustId.value.length === 0) {
ElMessage.error('请选择检查项目') ElMessage.error('请选择检查项目')
@ -1149,9 +1170,9 @@
.filter(v => !selectedMianListId.value.includes(v.id) && v.list_status === 0 && v.temp_plan_id) .filter(v => !selectedMianListId.value.includes(v.id) && v.list_status === 0 && v.temp_plan_id)
.map(v => v.temp_plan_id) .map(v => v.temp_plan_id)
// //
GetOptimalPlan({ GetOptimalPlan({
type: 'full_day', type: 'same_day_slot',
regnum: props.reg_num, regnum: props.reg_num,
entrustid: selectedEntrustId.value, entrustid: selectedEntrustId.value,
episodeid: props.episode_id, episodeid: props.episode_id,
@ -1848,7 +1869,7 @@
} }
.do_button_recommend { .do_button_recommend {
width: 150px; width: 180px;
} }
.do_button { .do_button {

@ -1,59 +1,118 @@
<template> <template>
<!-- 医生直接跳转过来查看开单记录 --> <div class="his-login-page">
<view> <div class="login-loading" v-if="loading && !errorMsg">
His登录 <div class="loading-text">正在登录...</div>
</div>
</view> <div class="error-card" v-if="errorMsg">
<div class="error-card-icon">
<el-icon :size="48" color="#F56C6C">
<WarningFilled />
</el-icon>
</div>
<div class="error-card-title">登录失败</div>
<div class="error-card-msg">{{ errorMsg }}</div>
<div class="error-card-hint">请检查 HIS 系统传参配置</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
HisAutoLogin HisAutoLogin
} from "@/api/api.js"; } from "@/api/api.js";
import { WarningFilled } from '@element-plus/icons-vue'
import { import {
ElMessage ref,
} from 'element-plus' onMounted
import {
ref,nextTick,onMounted
} from 'vue' } from 'vue'
const autoLogin=()=>{
let loading = ref(true);
let errorMsg = ref('');
const autoLogin = () => {
loading.value = true;
HisAutoLogin({ HisAutoLogin({
usercode: getParameterByName('usercode'), usercode: getParameterByName('usercode'),
deptcode: getParameterByName('deptcode') deptcode: getParameterByName('deptcode'),
cardno: getParameterByName('cardno')
}).then(res => { }).then(res => {
if(res.status){ if (res.status) {
sessionStorage.setItem('token', res.data.access_token); sessionStorage.setItem('token', res.data.access_token);
sessionStorage.setItem('refreshToken', res.data.refresh_token); sessionStorage.setItem('refreshToken', res.data.refresh_token);
sessionStorage.setItem('default_reg_num', getParameterByName('cardno'));//id sessionStorage.setItem('default_reg_num', getParameterByName('cardno'));
// sessionStorage.setItem('tk', JSON.stringify(res.data.tk));
var token = sessionStorage.getItem('token'); var token = sessionStorage.getItem('token');
console.log(token) if (token != null && token == res.data.access_token) {
if (token!=null && token == res.data.access_token) {
sessionStorage.setItem('LoginType', "HisLogin"); sessionStorage.setItem('LoginType', "HisLogin");
window.location.href = "./#/yewu/mainList" window.location.href = "./#/yewu/mainList?type=doctor"
//window.location.href = "./#/info/EntrustList" } else {
}else{ errorMsg.value = '登录失败';
ElMessage.error("登录失败") loading.value = false;
} }
}else{ } else {
ElMessage.error(res.msg) errorMsg.value = res.msg;
loading.value = false;
} }
}) }).catch(() => {
errorMsg.value = '网络请求失败,请检查网络连接';
loading.value = false;
});
} }
onMounted(()=>{
autoLogin() onMounted(() => {
autoLogin()
}) })
//url
function getParameterByName(name, url) { function getParameterByName(name, url) {
if (!url) url = decodeURIComponent(window.location.href) if (!url) url = decodeURIComponent(window.location.href)
name = name.replace(/[\[\]]/g, '\\$&') name = name.replace(/[\[\]]/g, '\\$&')
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url) results = regex.exec(url)
if (!results) return null if (!results) return null
if (!results[2]) return '' if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' ')) return decodeURIComponent(results[2].replace(/\+/g, ' '))
} }
</script> </script>
<style> <style scoped>
.his-login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
}
.login-loading {
text-align: center;
}
.loading-text {
font-size: 16px;
color: #999;
}
.error-card {
width: 420px;
padding: 48px 32px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
text-align: center;
}
.error-card-icon {
margin-bottom: 16px;
}
.error-card-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.error-card-msg {
font-size: 14px;
color: #666;
line-height: 1.6;
}
.error-card-hint {
font-size: 12px;
color: #bbb;
margin-top: 24px;
}
</style> </style>

@ -33,7 +33,7 @@
<el-avatar :size="40" :src="BaseUserInfo.img" /> <el-avatar :size="40" :src="BaseUserInfo.img" />
</div> </div>
<div v-if="BaseUserInfo.cn_name"> <div v-if="BaseUserInfo.cn_name">
<el-dropdown v-if="BaseUserInfo.department_id && BaseUserInfo.department_info?.length > 1" @command="DepartmentHandleCommand"> <el-dropdown v-if="BaseUserInfo.username !== 'admin' && BaseUserInfo.department_id && BaseUserInfo.department_info?.length > 1" @command="DepartmentHandleCommand">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
{{BaseUserInfo.department_name}} {{BaseUserInfo.department_name}}
<el-icon class="el-icon--right"> <el-icon class="el-icon--right">
@ -50,7 +50,7 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<div v-else-if="BaseUserInfo.department_id && BaseUserInfo.department_info?.length === 1" <div v-else-if="BaseUserInfo.username !== 'admin' && BaseUserInfo.department_id && BaseUserInfo.department_info?.length === 1"
style="font-size: 14px; margin-bottom: 4px; text-align: center;"> style="font-size: 14px; margin-bottom: 4px; text-align: center;">
{{BaseUserInfo.department_info[0].department_name}} {{BaseUserInfo.department_info[0].department_name}}
</div> </div>

@ -299,6 +299,7 @@
</template> </template>
<script setup> <script setup>
import { useRoute } from 'vue-router'
import { import {
ref, ref,
onMounted, onMounted,
@ -327,6 +328,8 @@
let pinia = usePinia() let pinia = usePinia()
const route = useRoute()
let YuYueVueDialogVisible=ref(false); let YuYueVueDialogVisible=ref(false);
let YuYueKey=ref(0); let YuYueKey=ref(0);
let AutoGroup=ref(true);// let AutoGroup=ref(true);//
@ -859,8 +862,10 @@
searchInfo.value.reg_num=default_reg_num searchInfo.value.reg_num=default_reg_num
} }
} }
// type=doctor
if (route.query.type === 'doctor') {
searchInfo.value.dateType = 'both';
}
} }
onMounted(() => { onMounted(() => {
loginType.value=sessionStorage.getItem('LoginType') loginType.value=sessionStorage.getItem('LoginType')

Loading…
Cancel
Save