预约优化

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

@ -63,8 +63,15 @@ public function GetUserList(){
public function AutoLogin(){
$usercode = request('usercode');
$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();
if(!$dept) return \Yz::JsonError("科室不存在");
@ -81,6 +88,20 @@ public function AutoLogin(){
$access_token = $jwt->BuildJWT('yz','access',$user->id,$user->group,$accessTimeout);
$refresh_token = $jwt->BuildJWT('yz','refresh',$user->id,'',$refreshTimeout);
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]);
}
}

@ -436,6 +436,8 @@ public function GetOptimalPlan($type, $regnum, $entrustids, $episodeid, $appoint
if ($type === 'full_day') {
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 {
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, '获取成功', []);
}
// 同一天同一时间段分配:所有 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)
{

@ -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
$post_data =$data;
foreach ($post_data as $key => $post_datum) {

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

@ -2,6 +2,9 @@
use Illuminate\Http\Request;
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跳到此路由再跳转到对应的系统页面
Route::get('/yiji', function (Request $request) {
$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/admin/#/doctorappointment?".$queryString);
return redirect(config('app.globals.FrontendUrl')."/admin/#/doctorappointment?".$queryString);
});
//cas 登录

@ -101,13 +101,13 @@
</el-tooltip>
</div>
<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 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 placement="top" content="点击此按钮,系统会推荐最近的能在同一时段完成检查的号源" popper-class="tip-pre-line">
<el-button class="do_button_recommend" type="primary" @click="sameTimeSlotClick"></el-button>
<el-tooltip placement="top" content="点击此按钮,系统会推荐最近的能在同一天同一段完成检查的号源" popper-class="tip-pre-line">
<el-button class="do_button_recommend" type="primary" @click="sameTimeSlotClick"></el-button>
</el-tooltip>
</div>
<div>
@ -275,6 +275,7 @@
let TanChuangMsgDialogVisible=ref(false)
let crossTimeDialogVisible = ref(false)
let crossTimeDialogHtml = ref('')
let cachedCrossDialogData = ref(null) // dialog { html, idx }
let resultDialogVisible = ref(false)
let resultDialogHtml = ref('')
const getWeekday = (date1) => {
@ -376,6 +377,17 @@
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()
}
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 += '</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 = () => {
if (selectedEntrustId.value.length === 0) {
ElMessage.error('请选择检查项目')
@ -1149,9 +1170,9 @@
.filter(v => !selectedMianListId.value.includes(v.id) && v.list_status === 0 && v.temp_plan_id)
.map(v => v.temp_plan_id)
//
//
GetOptimalPlan({
type: 'full_day',
type: 'same_day_slot',
regnum: props.reg_num,
entrustid: selectedEntrustId.value,
episodeid: props.episode_id,
@ -1848,7 +1869,7 @@
}
.do_button_recommend {
width: 150px;
width: 180px;
}
.do_button {

@ -1,59 +1,118 @@
<template>
<!-- 医生直接跳转过来查看开单记录 -->
<view>
His登录
</view>
<div class="his-login-page">
<div class="login-loading" v-if="loading && !errorMsg">
<div class="loading-text">正在登录...</div>
</div>
<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>
<script setup>
import {
HisAutoLogin
} from "@/api/api.js";
import { WarningFilled } from '@element-plus/icons-vue'
import {
ElMessage
} from 'element-plus'
import {
ref,nextTick,onMounted
ref,
onMounted
} from 'vue'
const autoLogin=()=>{
let loading = ref(true);
let errorMsg = ref('');
const autoLogin = () => {
loading.value = true;
HisAutoLogin({
usercode: getParameterByName('usercode'),
deptcode: getParameterByName('deptcode')
deptcode: getParameterByName('deptcode'),
cardno: getParameterByName('cardno')
}).then(res => {
if(res.status){
if (res.status) {
sessionStorage.setItem('token', res.data.access_token);
sessionStorage.setItem('refreshToken', res.data.refresh_token);
sessionStorage.setItem('default_reg_num', getParameterByName('cardno'));//id
// sessionStorage.setItem('tk', JSON.stringify(res.data.tk));
sessionStorage.setItem('default_reg_num', getParameterByName('cardno'));
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");
window.location.href = "./#/yewu/mainList"
//window.location.href = "./#/info/EntrustList"
}else{
ElMessage.error("登录失败")
window.location.href = "./#/yewu/mainList?type=doctor"
} else {
errorMsg.value = '登录失败';
loading.value = false;
}
}else{
ElMessage.error(res.msg)
} else {
errorMsg.value = res.msg;
loading.value = false;
}
})
}).catch(() => {
errorMsg.value = '网络请求失败,请检查网络连接';
loading.value = false;
});
}
onMounted(()=>{
autoLogin()
onMounted(() => {
autoLogin()
})
//url
function getParameterByName(name, url) {
if (!url) url = decodeURIComponent(window.location.href)
name = name.replace(/[\[\]]/g, '\\$&')
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url)
if (!results) return null
if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
function getParameterByName(name, url) {
if (!url) url = decodeURIComponent(window.location.href)
name = name.replace(/[\[\]]/g, '\\$&')
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url)
if (!results) return null
if (!results[2]) return ''
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
</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>

@ -33,7 +33,7 @@
<el-avatar :size="40" :src="BaseUserInfo.img" />
</div>
<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">
{{BaseUserInfo.department_name}}
<el-icon class="el-icon--right">
@ -50,7 +50,7 @@
</el-dropdown-menu>
</template>
</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;">
{{BaseUserInfo.department_info[0].department_name}}
</div>

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

Loading…
Cancel
Save