diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7b43e39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,322 @@ +# AGENTS.md - 秦皇岛中医院医技预约系统 + +## 项目简介 +秦皇岛中医院医技预约系统,用于医院医技检查项目的预约管理,包括科室、设备、号源管理、预约登记等核心功能。系统与HIS系统集成,支持多渠道预约(自助机、H5、管理后台)。 + +## 项目结构 + +``` +QHD_ZhongYiYuan_YiJiYuYue/ +├── Laravel/ # PHP后端API服务 (Laravel 8) +│ ├── app/ # 应用核心代码 +│ │ ├── Http/Controllers/API/ # API控制器 +│ │ ├── Models/ # 数据模型 +│ │ ├── Lib/ # 自定义类库 +│ │ └── Middleware/ # 中间件 +│ ├── routes/ # 路由定义 +│ ├── config/ # 配置文件 +│ └── database/ # 数据库相关 +│ +├── YiJi-admin/ # 管理后台前端 (Vue 3 + Vite + Element Plus) +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ ├── components/ # 公共组件 +│ │ ├── router/ # 路由配置 +│ │ ├── store/ # Pinia状态管理 +│ │ └── api/ # API接口 +│ └── vite.config.js # Vite配置 +│ +├── ZiZhuJi/ # 自助机前端 (Vue 3 + Vite + Element Plus) +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ ├── tools/ # 工具类 +│ │ └── router/ # 路由配置 +│ └── vite.config.js +│ +├── h5/ # 移动端H5 (uni-app) +│ ├── pages/ # 页面 +│ ├── api/ # API接口 +│ ├── static/ # 静态资源 +│ └── uni_modules/ # uni-app组件模块 +│ +├── bot/ # WebSocket服务 (Workerman) +│ └── workerman/ # Workerman框架 +│ +├── 秦皇岛中医院监听mq/ # HIS消息队列监听服务 (C#) +│ └ MyHosListener/ +│ ├── common/ +│ │ ├── listener/ # MQ消息监听器 +│ │ ├── DBTools.cs # 数据库工具 +│ │ └ Tools.cs # 通用工具 +│ └ App.config # 应用配置 +│ +└── data.sql # 数据库结构文件 +``` + +## 技术栈 + +### 后端 +- **框架**: Laravel 8 +- **PHP版本**: ^7.4|^8.0 +- **数据库**: MySQL 5.7+ (数据库名: yiji_qhdzhongyiyuan) +- **认证**: Laravel Sanctum +- **关键依赖**: + - barryvdh/laravel-snappy (PDF生成) + - picqer/php-barcode-generator (条形码生成) + +### 前端 +- **管理后台 & 自助机**: Vue 3 + Vite + Element Plus + Pinia/Vue Router +- **移动端H5**: uni-app (支持多端) +- **HTTP客户端**: Axios + +### 其他服务 +- **WebSocket**: Workerman (PHP) +- **HIS集成**: C# Windows服务监听MQ消息 + +## 构建与运行命令 + +### Laravel后端 +```bash +cd Laravel + +# 安装依赖 +composer install + +# 配置环境 +cp .env.example .env +php artisan key:generate + +# 运行开发服务器 +php artisan serve + +# 数据库迁移(如有) +php artisan migrate + +# 运行测试 +php artisan test +# 或 +phpunit +``` + +### YiJi-admin 管理后台 +```bash +cd YiJi-admin + +# 安装依赖 +npm install + +# 开发模式 +npm run dev + +# 生产构建 +npm run build + +# 预览构建结果 +npm run preview + +# 代码检查 +npm run lint + +# 代码格式化 +npm run format +``` + +### ZiZhuJi 自助机 +```bash +cd ZiZhuJi + +# 安装依赖 +npm install + +# 开发模式 +npm run dev + +# 生产构建 +npm run build + +# 预览构建结果 +npm run preview +``` + +### h5 移动端 +```bash +cd h5 + +# 使用HBuilderX或uni-app CLI +# 开发:通过HBuilderX运行到浏览器或模拟器 +# 构建:通过HBuilderX发布为H5/App +``` + +### Workerman WebSocket +```bash +cd bot + +# 安装依赖 +composer install + +# 启动WebSocket服务(Windows) +php workerman/start.php + +# Linux下通常使用 +php workerman/start.php start +php workerman/start.php stop +php workerman/start.php restart +``` + +### HIS监听服务 (C#) +```bash +# 使用Visual Studio编译运行 +# 或通过已编译的exe文件启动 +``` + +## 数据库核心表结构 + +### 权限与用户管理 +- `users` - 系统用户表 +- `group` - 用户分组 +- `group_menu` - 分组菜单关联 +- `menu` - 系统菜单 + +### 业务核心表 +- `s_department` - 科室表 +- `s_department_resources` - 科室资源(检查室/机房) +- `s_devices` - 设备表 +- `s_check_item` - 检查项目表 +- `s_check_item_class` - 检查项目分类 +- `s_check_item_device` - 检查项目-设备绑定关系 + +### 预约管理 +- `s_appointment_type` - 预约类型(住院/门诊/急诊/体检等渠道) +- `s_appointment_type_ratio` - 预约类型比例配置 +- `s_period` - 预约时间段配置 +- `s_source_roster` - 预约计划模板 +- `s_source_roster_count` - 模板各渠道号源数量 +- `s_source_roster_detail` - 预约计划明细(每日号源) +- `s_source_roster_detail_count` - 明细各渠道号源数量(含乐观锁version) +- `s_holiday` - 节假日配置 + +### 预约记录 +- `s_list` - **预约主表**(核心业务表,包含患者信息、医嘱、预约状态等) + - 主要状态字段 `list_status`: 0-正在申请, 1-预约, 2-登记, 3-结束 + - 患者类型 `patient_type`: 0-住院, 1-门诊, 2-急诊, 3-体检 +- `s_list_log` - 预约状态变更日志 + +### 其他 +- `configs` - 系统配置 +- `s_huchi` - 检查项目互斥配置 +- `s_inpatient_ward` - 病区表 +- `outside_user` - 第三方对接用户表 +- `institutional_calendar` - 体检机构日历(如用于体检预约) + +## API路由规范 + +API基础路径: `/api/v1/` (需携带token认证) + +### 认证相关 +- `POST /api/admin/login` - 登录 +- `POST /api/tokenRefresh` - 刷新token + +### 系统管理 +- `POST /api/v1/admin/getBaseMenuList` - 获取用户菜单 +- `POST /api/v1/admin/getUserList` - 用户列表 +- `POST /api/v1/admin/getGroupList` - 分组列表 +- `POST /api/v1/admin/GetConfigInfo` - 获取配置 + +### 业务管理 +- `POST /api/v1/admin/GetDepartmentList` - 科室列表 +- `POST /api/v1/admin/GetCheckItemList` - 检查项目列表 +- `POST /api/v1/admin/GetDeviceList` - 设备列表 +- `POST /api/v1/admin/PlanModelGetList` - 预约计划模板 +- `POST /api/v1/admin/PlanListGetList` - 预约计划明细 +- `POST /api/v1/admin/GetMainList` - 预约记录列表 +- `POST /api/v1/admin/CancelYuYue` - 取消预约 + +**命名规范**: 控制器位于 `App\Http\Controllers\API\Admin\` 目录,按业务模块分子目录(如 `YeWu\`) + +## HIS集成消息类型 + +C#监听服务处理以下MQ消息类型(位于 `秦皇岛中医院监听mq\MyHosListener\common\listener\`): +- `MI0150Listener` - 医嘱信息 +- `MI0164Listener` - 医嘱状态变更 +- `MU0165Listener` - 医嘱更新 +- `CI0047Listener` - 检查信息 +- `CI0054Listener` / `CI0055Listener` - 检查相关 +- `CD0050Listener` - 检查确认 +- `OI0083Listener` - 医嘱订单 +- `OD0084Listener` - 订单更新 +- `MU0151Listener` - 其他更新 + +消息处理后写入 `s_list` 表及相关业务表。 + +## 代码规范 + +### Laravel +- 遵循PSR-12编码规范 +- 控制器命名: `XxxController` +- 模型命名: 单数形式,对应表名前缀通常为 `s_` +- 使用Eloquent ORM操作数据库 +- 复杂业务逻辑封装在 `app/Lib/` 或Service层 + +### Vue前端 +- 组件命名: PascalCase (如 `CheckItemMainList.vue`) +- 使用Composition API或Options API +- Element Plus组件库统一UI风格 +- API调用统一通过 `src/api/` 模块 + +### 通用规范 +- 时间字段命名: `created_at`, `updated_at` +- 状态字段: `status` (0-关闭/禁用, 1-开启/正常) +- 删除标记: `is_del` (0-未删除, 1-已删除) +- 注意软删除和状态区分 + +## 开发注意事项 + +1. **号源管理** + - 使用乐观锁(`s_source_roster_detail_count.version`)防止并发预约冲突 + - 预约时需检查项目互斥(`s_huchi`表) + - 预约需在 `end_reservation_time` 前完成 + +2. **HIS数据同步** + - HIS通过MQ推送医嘱数据,监听服务接收并写入数据库 + - `entrust_id` 为HIS医嘱唯一标识,需确保唯一性 + - 注意门诊缴费状态同步(`is_pay`字段) + +3. **预约状态流转** + - 正常流程: 申请(0) → 预约(1) → 登记(2) → 结束(3) + - 支持取消预约(记录取消时间 `canel_time`) + - 所有状态变更记录到 `s_list_log` + +4. **安全与认证** + - 所有业务API需通过 `checktoken` 中间件验证 + - Token使用Laravel Sanctum管理 + - 密码需加密存储 + +5. **环境配置** + - Laravel `.env` 配置数据库、应用URL等 + - C#监听服务 `App.config` 配置MQ连接、数据库连接 + - 前端需配置API基础路径 + +## 测试 + +```bash +# Laravel测试 +cd Laravel +php artisan test + +# 或使用PHPUnit直接运行 +phpunit +``` + +前端项目暂无测试脚本配置,建议补充单元测试和E2E测试。 + +## 常见问题 + +1. **数据库导入**: 使用 `data.sql` 初始化数据库结构 +2. **跨域配置**: Laravel已配置 `fruitcake/laravel-cors` +3. **PDF生成**: 需安装wkhtmltopdf(laravel-snappy依赖) +4. **条形码生成**: 使用 `picqer/php-barcode-generator` + +## 相关文档 + +- `项目需求规范示例.md` - 需求与设计文档模板参考 +- 数据库详细结构见 `data.sql` \ No newline at end of file diff --git a/Laravel/app/Http/Controllers/API/Admin/YeWu/WorkMainController.php b/Laravel/app/Http/Controllers/API/Admin/YeWu/WorkMainController.php index f5cc384..947f6f3 100644 --- a/Laravel/app/Http/Controllers/API/Admin/YeWu/WorkMainController.php +++ b/Laravel/app/Http/Controllers/API/Admin/YeWu/WorkMainController.php @@ -9,6 +9,7 @@ use DateTime; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Tools; class WorkMainController extends Controller @@ -285,50 +286,56 @@ class WorkMainController extends Controller } return \Yz::Return(true,'可以预约',[]); } - //检查是否有超时未支付的门诊预约记录,如果有则给其取消,并恢复名额 public function NoPayCancel() { - $success_count=0; - //检测是否有作废医嘱 - $list_zuofei=DB::table('s_list')->where(['list_status'=>1,'is_del'=>0,'is_nullify'=>1])->get(); - foreach ($list_zuofei as $key=>$item){ - $u_data=[ - 'list_status' => 0, - 'reservation_date' => null, - 'reservation_time' => null, - 'reservation_sources' => null, - 'services_group' => null, - 'roster_id' => null, - 'xuhao' => null, - 'department_id'=>null, - 'appointment_type_id' => null, - 'canel_time' => date('Y-m-d H:i:s'), - ]; - $u_mainList = DB::table('s_list')->where(['id' => $item->id,'list_status'=>1])->update($u_data); - $i_log=DB::table('s_list_log')->insert([ - 'list_id'=>$item->id, - 'reg_num'=>$item->reg_num, - 'old_status'=>$item->list_status, - 'new_status'=>0, - 'create_user'=>'sys_auto', - 'note'=>'作废取消', - 'data'=>json_encode($u_data) - ]); - $u_count=false; - if($u_mainList){ - $u_count = DB::table('s_source_roster_detail_count')->where(['roster_detail_id' => $item->roster_id, 'appointment_type_id' => $item->appointment_type_id])->decrement('used_count'); - } - if($u_count){ - $success_count++; + $success_count = 0; + $service = new PlanListService(); + + $list_zuofei = DB::table('s_list')->where(['list_status' => 1, 'is_del' => 0, 'is_nullify' => 1])->get(); + foreach ($list_zuofei as $key => $item) { + DB::beginTransaction(); + try { + $service->releaseSourceFromDetail($item->id, $item); + + $u_data = [ + 'list_status' => 0, + 'reservation_date' => null, + 'reservation_time' => null, + 'reservation_sources' => null, + 'services_group' => null, + 'roster_id' => null, + 'xuhao' => null, + 'department_id' => null, + 'appointment_type_id' => null, + 'appointment_use_plan_detail' => null, + 'canel_time' => date('Y-m-d H:i:s'), + ]; + $u_mainList = DB::table('s_list')->where(['id' => $item->id, 'list_status' => 1])->update($u_data); + $i_log = DB::table('s_list_log')->insert([ + 'list_id' => $item->id, + 'reg_num' => $item->reg_num, + 'old_status' => $item->list_status, + 'new_status' => 0, + 'create_user' => 'sys_auto', + 'note' => '作废取消', + 'data' => json_encode($u_data) + ]); + + DB::commit(); + if ($u_mainList) { + $success_count++; + } + } catch (\Exception $e) { + DB::rollBack(); + Log::error("作废取消失败", ['id' => $item->id, 'error' => $e->getMessage()]); } } - //检测是否有超时未支付的 date_default_timezone_set('PRC'); - $config=DB::table('configs')->where(['label'=>'门诊缴费超时时间'])->first(); - if($config->value>0){ + $config = DB::table('configs')->where(['label' => '门诊缴费超时时间'])->first(); + if ($config && $config->value > 0) { $currentDateTime = Carbon::now()->subHours($config->value); - $list=DB::table('s_list')->where([ + $list = DB::table('s_list')->where([ 'list_status' => 1, 'patient_type' => 1, 'is_pay' => 0, @@ -343,43 +350,48 @@ class WorkMainController extends Controller }); })->get(); - - foreach ($list as $key=>$item){ - $u_data=[ - 'list_status' => 0, - 'reservation_date' => null, - 'reservation_time' => null, - 'reservation_sources' => null, - 'services_group' => null, - 'roster_id' => null, - 'xuhao' => null, - 'department_id'=>null, - 'appointment_type_id' => null, - 'canel_time' => date('Y-m-d H:i:s'), - ]; - $u_mainList = DB::table('s_list')->where(['id' => $item->id,'list_status'=>1])->update($u_data); - $i_log=DB::table('s_list_log')->insert([ - 'list_id'=>$item->id, - 'reg_num'=>$item->reg_num, - 'old_status'=>$item->list_status, - 'new_status'=>0, - 'create_user'=>null, - 'note'=>'超时取消', - 'data'=>json_encode($u_data) - ]); - $u_count=false; - if($u_mainList){ - $u_count = DB::table('s_source_roster_detail_count')->where(['roster_detail_id' => $item->roster_id, 'appointment_type_id' => $item->appointment_type_id])->decrement('used_count'); - } - if($u_count){ - $success_count++; + foreach ($list as $key => $item) { + DB::beginTransaction(); + try { + $service->releaseSourceFromDetail($item->id, $item); + + $u_data = [ + 'list_status' => 0, + 'reservation_date' => null, + 'reservation_time' => null, + 'reservation_sources' => null, + 'services_group' => null, + 'roster_id' => null, + 'xuhao' => null, + 'department_id' => null, + 'appointment_type_id' => null, + 'appointment_use_plan_detail' => null, + 'canel_time' => date('Y-m-d H:i:s'), + ]; + $u_mainList = DB::table('s_list')->where(['id' => $item->id, 'list_status' => 1])->update($u_data); + $i_log = DB::table('s_list_log')->insert([ + 'list_id' => $item->id, + 'reg_num' => $item->reg_num, + 'old_status' => $item->list_status, + 'new_status' => 0, + 'create_user' => 'sys_auto', + 'note' => '超时取消', + 'data' => json_encode($u_data) + ]); + + DB::commit(); + if ($u_mainList) { + $success_count++; + } + } catch (\Exception $e) { + DB::rollBack(); + Log::error("超时取消失败", ['id' => $item->id, 'error' => $e->getMessage()]); } } - return \Yz::Return(true,"执行完成",['success_count'=>$success_count]); - }else{ + return \Yz::Return(true, "执行完成", ['success_count' => $success_count]); + } else { return \Yz::echoError1("超时参数未设置"); } - } //医生取消预约 public function DoctorCancelYuYue() diff --git a/Laravel/app/Services/Admin/YeWu/PlanListService.php b/Laravel/app/Services/Admin/YeWu/PlanListService.php index ce02d9e..730cb0c 100644 --- a/Laravel/app/Services/Admin/YeWu/PlanListService.php +++ b/Laravel/app/Services/Admin/YeWu/PlanListService.php @@ -323,6 +323,11 @@ WHERE DB::beginTransaction(); try { + if ($do_type == 2) { + foreach ($oldMainInfos as $oldMainInfo) { + $this->releaseSourceFromDetail($oldMainInfo->id, $oldMainInfo); + } + } //查询号源渠道是否有合并 $appointment_types=[$appointment_type]; @@ -495,45 +500,57 @@ WHERE } - //更新计划明细表使用数量 - $up_plan_count_all_success =true; + //更新计划明细表使用数量(使用乐观锁) + $up_plan_count_all_success = true; + $versionMismatchIds = []; foreach ($roster_detail_counts as $key => $planCount) { $countToDeduct = $plan_qudao_tempCount[$key]; if ($countToDeduct == 0) continue; - $query = DB::table('s_source_roster_detail_count')->where(['id' => $planCount->id]); - - // 非紧急模式:严格检查库存充足 if ($is_emergency !== 1) { - // 只有当 剩余物理库存 >= 本次扣减 时才执行 - // 公式:count - used - locked >= countToDeduct => count >= used + locked + countToDeduct - $query->whereRaw('count >= (used_count + IFNULL(locked_count, 0) + ?)', [$countToDeduct]); - } - // 紧急模式:不加 whereRaw 限制,允许超卖 (used_count 可以大于 count) - - $affected = $query->increment('used_count', $countToDeduct); - - if ($affected == 0) { - if ($is_emergency !== 1) { - // 非紧急模式扣减失败,说明并发导致库存不足 - $up_plan_count_all_success = false; - break; - } else { - // 紧急模式扣减失败,通常是因为记录不存在,需检查 + $affected = DB::table('s_source_roster_detail_count') + ->where(['id' => $planCount->id, 'version' => $planCount->version]) + ->whereRaw('count >= (used_count + IFNULL(locked_count, 0) + ?)', [$countToDeduct]) + ->update([ + 'used_count' => DB::raw('used_count + ' . $countToDeduct), + 'version' => DB::raw('version + 1'), + 'updated_at' => date('Y-m-d H:i:s') + ]); + } else { + $affected = DB::table('s_source_roster_detail_count') + ->where(['id' => $planCount->id, 'version' => $planCount->version]) + ->update([ + 'used_count' => DB::raw('used_count + ' . $countToDeduct), + 'version' => DB::raw('version + 1'), + 'updated_at' => date('Y-m-d H:i:s') + ]); + if ($affected == 0) { $check = DB::table('s_source_roster_detail_count')->find($planCount->id); if (!$check) { $up_plan_count_all_success = false; break; } - // 如果记录存在但 affected=0 (极少见,可能是行锁超时等),在紧急模式下我们尝试强制更新 - // 这里为了简单,假设 increment 只要记录存在就会成功返回 1 (即使超卖) - // 如果确实返回 0 且记录存在,可能需要重试或报错,视具体 DB 行为而定 - // 大多数情况下,不加 whereRaw 的 increment 都会成功 + $affected = DB::table('s_source_roster_detail_count') + ->where(['id' => $planCount->id]) + ->update([ + 'used_count' => DB::raw('used_count + ' . $countToDeduct), + 'version' => DB::raw('version + 1'), + 'updated_at' => date('Y-m-d H:i:s') + ]); } } + + if ($affected == 0 && $is_emergency !== 1) { + $versionMismatchIds[] = $planCount->id; + $up_plan_count_all_success = false; + } } if (!$up_plan_count_all_success && $is_emergency !== 1) { + if (count($versionMismatchIds) > 0) { + Log::warning("预约乐观锁冲突", ['planid' => $planid, 'conflict_ids' => $versionMismatchIds]); + throw new \Exception('号源数据已被其他用户修改,请刷新后重试'); + } throw new \Exception('名额不足,扣减失败'); } @@ -577,83 +594,24 @@ WHERE throw new \Exception('更新医嘱状态失败'); } + $note = $do_type == 2 ? "改约" : "预约"; + foreach ($oldMainInfos as $key => $oldMainInfo) { + $i_log = DB::table('s_list_log')->insert([ + 'list_id' => $oldMainInfo->id, + 'reg_num' => $oldMainInfo->reg_num, + 'old_status' => $oldMainInfo->list_status, + 'new_status' => 1, + 'create_user' => $do_user, + 'note' => $note, + 'data' => json_encode($u_data) + ]); + } - $note = "预约"; - foreach ($oldMainInfos as $key => $oldMainInfo) { - - if ($do_type == 2) { - // if(count($mainlistids)>1) return \Yz::echoError1('请选择1条医嘱改约,暂不支持批量'); - $note = "改约"; - //如果是改约,则恢复原来的数量 - $useDetailJson = $oldMainInfo->appointment_use_plan_detail; - if (empty($useDetailJson)) { - return \Yz::echoError1('取消失败:缺少占位明细数据,无法精确回退,请联系管理员'); - } - $useDetails = json_decode($useDetailJson, true); - if (!is_array($useDetails)) { - return \Yz::echoError1('取消失败:占位明细数据格式错误'); - } - foreach ($useDetails as $detail) { - $rosterDetailCountId = $detail['roster_detail_count_id'] ?? null; - $countToRelease = (int)($detail['count'] ?? 0); - - if (!$rosterDetailCountId || $countToRelease <= 0) { - continue; - } - $affected = DB::table('s_source_roster_detail_count') - ->where(['id' => $rosterDetailCountId]) - ->lockForUpdate() - ->where('used_count', '>=', $countToRelease) // 防御性编程:确保不会减成负数 - ->decrement('used_count', $countToRelease); - if ($affected == 0) { - // 如果 affected 为 0,可能是记录不存在,或者 used_count 不足 - $checkRecord = DB::table('s_source_roster_detail_count')->where(['id' => $rosterDetailCountId])->first(); - if (!$checkRecord) { - Log::error("改约-号源记录缺失", [ - 'MainListId' => $oldMainInfo->id, - 'count_id' => $rosterDetailCountId - ]); - } - if ($checkRecord->used_count < $countToRelease) { - // 【关键修改】:余额不足,但不阻断取消 - $actualRelease = $checkRecord->used_count; // 最多只能退这么多 - - if ($actualRelease > 0) { - // 把剩余的余额全部退完,强制设为 0 (或者 decrement actualRelease) - DB::table('s_source_roster_detail_count') - ->where(['id' => $rosterDetailCountId]) - ->update(['used_count' => 0]); // 直接置零,防止浮点数或并发问题 - } - - // 记录严重报警日志 - $msg = "数据不一致警告:订单 {$oldMainInfo->id} 试图退还 {$countToRelease} 个名额,但渠道 {$rosterDetailCountId} 仅剩 {$checkRecord->used_count} 个。"; - Log::error($msg); - } - } - } - - } - - $i_log = DB::table('s_list_log')->insert([ - 'list_id' => $oldMainInfo->id, - 'reg_num' => $oldMainInfo->reg_num, - 'old_status' => $oldMainInfo->list_status, - 'new_status' => 1, - 'create_user' => $do_user, - 'note' => $note, - 'data' => json_encode($u_data) - ]); - } - if ($u_mainList) { - DB::commit(); - if(config('app.globals.预约完成短信通知')==1){ - $this->SendMsg($oldMainInfos,$do_type); - } - return \Yz::Return(true, '预约成功', ['planid' => $planid, 'mainlistids' => $mainlistids]); - } else { - DB::rollBack(); - return \Yz::echoError1('预约失败'); - } + DB::commit(); + if(config('app.globals.预约完成短信通知')==1){ + $this->SendMsg($oldMainInfos,$do_type); + } + return \Yz::Return(true, '预约成功', ['planid' => $planid, 'mainlistids' => $mainlistids]); @@ -720,15 +678,13 @@ WHERE } } - public function CancelYuYue($MainListId, $reg_num,$do_user=null) +public function CancelYuYue($MainListId, $reg_num,$do_user=null) { date_default_timezone_set('PRC'); $nowdatetime = date("Y-m-d H:i:s"); - // 开启数据库事务 DB::beginTransaction(); try { - // 1. 查询并锁定主预约记录(防并发) $mainInfo = DB::table('s_list') ->where(['id' => $MainListId, 'reg_num' => $reg_num]) ->lockForUpdate() @@ -738,58 +694,12 @@ WHERE return \Yz::echoError1('医嘱不存在'); } - // 2. 检查当前状态是否允许取消(仅状态为1可取消) if ($mainInfo->list_status != 1) { return \Yz::echoError1('该记录无法取消,当前状态:' . $mainInfo->list_status); } - $useDetailJson = $mainInfo->appointment_use_plan_detail; - if (empty($useDetailJson)) { - return \Yz::echoError1('取消失败:缺少占位明细数据,无法精确回退,请联系管理员'); - } - $useDetails = json_decode($useDetailJson, true); - if (!is_array($useDetails)) { - return \Yz::echoError1('取消失败:占位明细数据格式错误'); - } - foreach ($useDetails as $detail) { - $rosterDetailCountId = $detail['roster_detail_count_id'] ?? null; - $countToRelease = (int)($detail['count'] ?? 0); - - if (!$rosterDetailCountId || $countToRelease <= 0) { - continue; - } - $affected = DB::table('s_source_roster_detail_count') - ->where(['id' => $rosterDetailCountId]) - ->lockForUpdate() - ->where('used_count', '>=', $countToRelease) // 防御性编程:确保不会减成负数 - ->decrement('used_count', $countToRelease); - if ($affected == 0) { - // 如果 affected 为 0,可能是记录不存在,或者 used_count 不足 - $checkRecord = DB::table('s_source_roster_detail_count')->where(['id' => $rosterDetailCountId])->first(); - if (!$checkRecord) { - Log::error("取消预约-记录缺失", [ - 'MainListId' => $MainListId, - 'count_id' => $rosterDetailCountId - ]); - } - if ($checkRecord->used_count < $countToRelease) { - // 【关键修改】:余额不足,但不阻断取消 - $actualRelease = $checkRecord->used_count; // 最多只能退这么多 - - if ($actualRelease > 0) { - // 把剩余的余额全部退完,强制设为 0 (或者 decrement actualRelease) - DB::table('s_source_roster_detail_count') - ->where(['id' => $rosterDetailCountId]) - ->update(['used_count' => 0]); // 直接置零,防止浮点数或并发问题 - } - // 记录严重报警日志 - $msg = "数据不一致警告:订单 {$MainListId} 试图退还 {$countToRelease} 个名额,但渠道 {$rosterDetailCountId} 仅剩 {$checkRecord->used_count} 个。"; - Log::error($msg); - } - } - } + $this->releaseSourceFromDetail($MainListId, $mainInfo); - // 8. 更新主表状态为“已取消”(0) $u_data = [ 'list_status' => 0, 'reservation_date' => null, @@ -800,13 +710,13 @@ WHERE 'xuhao' => null, 'department_id' => null, 'appointment_type_id' => null, + 'appointment_use_plan_detail' => null, 'canel_time' => $nowdatetime, - 'is_emergency'=>0 + 'is_emergency' => 0 ]; $u_mainList = DB::table('s_list')->where(['id' => $MainListId])->update($u_data); - // 9. 写入操作日志 $i_log = DB::table('s_list_log')->insert([ 'list_id' => $mainInfo->id, 'reg_num' => $mainInfo->reg_num, @@ -818,18 +728,131 @@ WHERE 'created_at' => $nowdatetime, ]); - // 10. 提交事务 DB::commit(); return \Yz::Return(true, '取消成功', []); - } catch (\Exception $e) { +} catch (\Exception $e) { DB::rollBack(); - // 记录错误日志(建议使用 Laravel 日志系统) - Log::error("取消预约失败 - ID: {$MainListId}, reg_num: {$reg_num}, 错误: " . $e->getMessage()); + Log::error("取消预约失败 - ID: {$MainListId}, reg_num: {$reg_num}, 错误: " . $e->getMessage()); return \Yz::echoError1('取消失败:系统异常,请稍后重试'); } } + + public function releaseSourceFromDetail($mainListId, $mainInfo = null) + { + if (!$mainInfo) { + $mainInfo = DB::table('s_list')->where(['id' => $mainListId])->first(); + } + if (!$mainInfo) { + Log::error("释放号源失败-记录不存在", ['MainListId' => $mainListId]); + return false; + } + + $useDetailJson = $mainInfo->appointment_use_plan_detail; + if (empty($useDetailJson)) { + $fallbackResult = $this->releaseSourceFallback($mainInfo); + return $fallbackResult; + } + + $useDetails = json_decode($useDetailJson, true); + if (!is_array($useDetails)) { + Log::error("释放号源失败-明细格式错误", ['MainListId' => $mainListId, 'detail' => $useDetailJson]); + return $this->releaseSourceFallback($mainInfo); + } + + $allSuccess = true; + foreach ($useDetails as $detail) { + $rosterDetailCountId = $detail['roster_detail_count_id'] ?? null; + $countToRelease = (int)($detail['count'] ?? 0); + + if (!$rosterDetailCountId || $countToRelease <= 0) { + continue; + } + + $countRecord = DB::table('s_source_roster_detail_count') + ->where(['id' => $rosterDetailCountId]) + ->first(); + + if (!$countRecord) { + Log::error("释放号源-记录缺失", ['MainListId' => $mainListId, 'count_id' => $rosterDetailCountId]); + $allSuccess = false; + continue; + } + + $currentVersion = $countRecord->version; + $affected = DB::table('s_source_roster_detail_count') + ->where(['id' => $rosterDetailCountId, 'version' => $currentVersion]) + ->where('used_count', '>=', $countToRelease) + ->update([ + 'used_count' => DB::raw('used_count - ' . $countToRelease), + 'version' => DB::raw('version + 1'), + 'updated_at' => date('Y-m-d H:i:s') + ]); + + if ($affected == 0) { + $retryRecord = DB::table('s_source_roster_detail_count')->where(['id' => $rosterDetailCountId])->first(); + if (!$retryRecord) { + Log::error("释放号源重试-记录缺失", ['MainListId' => $mainListId, 'count_id' => $rosterDetailCountId]); + $allSuccess = false; + continue; + } + + if ($retryRecord->used_count < $countToRelease) { + $actualRelease = $retryRecord->used_count; + if ($actualRelease > 0) { + DB::table('s_source_roster_detail_count') + ->where(['id' => $rosterDetailCountId]) + ->update([ + 'used_count' => 0, + 'version' => DB::raw('version + 1'), + 'updated_at' => date('Y-m-d H:i:s') + ]); + } + Log::warning("释放号源-数量不足", [ + 'MainListId' => $mainListId, + 'count_id' => $rosterDetailCountId, + 'expected' => $countToRelease, + 'actual' => $actualRelease + ]); + } else { + DB::table('s_source_roster_detail_count') + ->where(['id' => $rosterDetailCountId]) + ->update([ + 'used_count' => DB::raw('used_count - ' . $countToRelease), + 'version' => DB::raw('version + 1'), + 'updated_at' => date('Y-m-d H:i:s') + ]); + Log::warning("释放号源-乐观锁冲突已重试", ['MainListId' => $mainListId, 'count_id' => $rosterDetailCountId]); + } + } + } + + return $allSuccess; + } + + private function releaseSourceFallback($mainInfo) + { + if (empty($mainInfo->roster_id) || empty($mainInfo->appointment_type_id)) { + Log::error("释放号源兜底失败-缺少必要字段", ['MainListId' => $mainInfo->id]); + return false; + } + + $affected = DB::table('s_source_roster_detail_count') + ->where([ + 'roster_detail_id' => $mainInfo->roster_id, + 'appointment_type_id' => $mainInfo->appointment_type_id + ]) + ->where('used_count', '>', 0) + ->decrement('used_count', 1); + + if ($affected == 0) { + Log::warning("释放号源兜底-无匹配记录或已为0", ['MainListId' => $mainInfo->id]); + } + + return $affected > 0; + } + //短信提醒 public function SendMsg($infos,$dotype=1) { diff --git a/Laravel/tests/Feature/PlanListServiceTest.php b/Laravel/tests/Feature/PlanListServiceTest.php new file mode 100644 index 0000000..2b9fa85 --- /dev/null +++ b/Laravel/tests/Feature/PlanListServiceTest.php @@ -0,0 +1,294 @@ +initTestData(); + } + + protected function tearDown(): void + { + $this->cleanupTestData(); + parent::tearDown(); + } + + private function initTestData() + { + $this->cleanupTestData(); + + DB::table('s_department')->insert([ + 'id' => 9999, + 'department_name' => '测试科室', + 'department_number' => 'TEST001', + 'department_status' => 1, + 'is_del' => 0, + ]); + + DB::table('s_appointment_type')->insert([ + 'id' => 9999, + 'name' => '测试渠道', + 'jiancheng' => '测试', + 'status' => 1, + 'is_del' => 0, + ]); + + DB::table('s_period')->insert([ + 'id' => 9999, + 'period_name' => '测试时段', + 'period_begin_time' => '08:00:00', + 'period_end_time' => '10:00:00', + 'department_id' => 9999, + 'date_type' => 1, + 'period_status' => 1, + ]); + + DB::table('s_department_resources')->insert([ + 'id' => 9999, + 'department_id' => 9999, + 'department_resources_name' => '测试资源', + 'is_del' => 0, + ]); + + DB::table('s_source_roster_detail')->insert([ + 'id' => 9999, + 'department_id' => 9999, + 'resources_id' => 9999, + 'period_id' => 9999, + 'device_id' => '1', + 'date' => date('Y-m-d', strtotime('+1 day')), + 'begin_time' => '08:00:00', + 'end_time' => '10:00:00', + 'end_reservation_time' => '23:59:59', + 'patient_type' => '0,1,2,3', + 'weekname' => '1', + 'status' => 1, + 'is_del' => 0, + ]); + + DB::table('s_source_roster_detail_count')->insert([ + 'id' => 9999, + 'roster_detail_id' => 9999, + 'appointment_type_id' => 9999, + 'count' => 10, + 'used_count' => 3, + 'locked_count' => 0, + 'version' => 1, + ]); + } + + private function cleanupTestData() + { + DB::table('s_list')->where('reg_num', 'like', 'TEST_REG_%')->delete(); + DB::table('s_list_log')->where('reg_num', 'like', 'TEST_REG_%')->delete(); + + $tables = [ + 's_source_roster_detail_count', + 's_source_roster_detail', 's_department_resources', + 's_period', 's_appointment_type', 's_department' + ]; + foreach ($tables as $table) { + DB::table($table)->where('id', '>=', 9999)->delete(); + } + } + + private function createTestList($status = 1, $detail = 'default') + { + if ($detail === 'default') { + $detail = json_encode([['roster_detail_count_id' => 9999, 'count' => 2]]); + } + + return DB::table('s_list')->insertGetId([ + 'list_status' => $status, + 'reg_num' => 'TEST_REG_' . time() . '_' . rand(1000, 9999), + 'user_name' => '测试患者', + 'entrust' => '测试检查项目', + 'entrust_code' => 'TEST001', + 'patient_type' => 1, + 'roster_id' => 9999, + 'appointment_type_id' => 9999, + 'appointment_use_plan_detail' => $detail, + 'reservation_date' => date('Y-m-d', strtotime('+1 day')), + 'reservation_time' => 9999, + 'is_del' => 0, + 'is_nullify' => 0, + ]); + } + + public function test_release_source_from_detail() + { + $countBefore = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(3, $countBefore->used_count); + + $detail = json_encode([['roster_detail_count_id' => 9999, 'count' => 2]]); + $listId = $this->createTestList(1, $detail); + + $service = new PlanListService(); + $result = $service->releaseSourceFromDetail($listId); + + $this->assertTrue($result); + + $countAfter = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(1, $countAfter->used_count); + $this->assertEquals(2, $countAfter->version); + + echo "\n✅ 测试1通过: releaseSourceFromDetail 精确释放号源成功\n"; + echo " 释放前 used_count=3, 释放后 used_count=1, version从1变为2\n"; + } + + public function test_release_source_with_version_check() + { + DB::table('s_source_roster_detail_count')->where('id', 9999)->update(['version' => 5]); + + $detail = json_encode([['roster_detail_count_id' => 9999, 'count' => 1]]); + $listId = $this->createTestList(1, $detail); + + $service = new PlanListService(); + $result = $service->releaseSourceFromDetail($listId); + + $this->assertTrue($result); + + $countAfter = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(6, $countAfter->version); + + echo "\n✅ 测试2通过: 乐观锁version正确递增\n"; + echo " 释放前 version=5, 释放后 version=6\n"; + } + + public function test_release_source_fallback_when_no_detail() + { + DB::table('s_source_roster_detail_count')->where('id', 9999)->update(['used_count' => 3, 'version' => 1]); + DB::table('s_list')->where('reg_num', 'like', 'TEST_REG_%')->delete(); + + $listId = $this->createTestList(1, null); + $mainInfo = DB::table('s_list')->where('id', $listId)->first(); + + $countBefore = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(3, $countBefore->used_count, '初始used_count应为3'); + + $service = new PlanListService(); + $result = $service->releaseSourceFromDetail($listId, $mainInfo); + + $this->assertTrue($result, 'releaseSourceFromDetail应返回true'); + + $countAfter = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(2, $countAfter->used_count, '释放后used_count应为2'); + + echo "\n✅ 测试3通过: 无明细时fallback降级释放成功\n"; + echo " 无appointment_use_plan_detail时,按roster_id+appointment_type_id释放1个名额\n"; + } + + public function test_release_source_multiple_channels() + { + DB::table('s_source_roster_detail_count')->insert([ + 'id' => 10000, + 'roster_detail_id' => 9999, + 'appointment_type_id' => 10000, + 'count' => 5, + 'used_count' => 3, + 'locked_count' => 0, + 'version' => 1, + ]); + + $detail = json_encode([ + ['roster_detail_count_id' => 9999, 'count' => 2], + ['roster_detail_count_id' => 10000, 'count' => 1], + ]); + $listId = $this->createTestList(1, $detail); + + $service = new PlanListService(); + $result = $service->releaseSourceFromDetail($listId); + + $this->assertTrue($result); + + $count1 = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $count2 = DB::table('s_source_roster_detail_count')->where('id', 10000)->first(); + + $this->assertEquals(1, $count1->used_count); + $this->assertEquals(2, $count2->used_count); + + DB::table('s_source_roster_detail_count')->where('id', 10000)->delete(); + + echo "\n✅ 测试4通过: 多渠道合并号源精确释放成功\n"; + echo " 渠道9999: used_count从3变为1\n"; + echo " 渠道10000: used_count从3变为2\n"; + } + + public function test_release_source_with_insufficient_used_count() + { + DB::table('s_source_roster_detail_count')->where('id', 9999)->update(['used_count' => 1]); + + $detail = json_encode([['roster_detail_count_id' => 9999, 'count' => 5]]); + $listId = $this->createTestList(1, $detail); + + $service = new PlanListService(); + $result = $service->releaseSourceFromDetail($listId); + + $this->assertTrue($result); + + $countAfter = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(0, $countAfter->used_count); + + echo "\n✅ 测试5通过: 释放数量大于used_count时,置零并记录警告日志\n"; + echo " 尝试释放5个,实际只有1个,已置零并记录日志\n"; + } + + public function test_cancel_yu_yue_clears_detail() + { + $detail = json_encode([['roster_detail_count_id' => 9999, 'count' => 2]]); + $listId = $this->createTestList(1, $detail); + $listInfo = DB::table('s_list')->where('id', $listId)->first(); + + $service = new PlanListService(); + $result = $service->CancelYuYue($listId, $listInfo->reg_num); + + $this->assertTrue($result['status']); + + $listAfter = DB::table('s_list')->where('id', $listId)->first(); + $this->assertEquals(0, $listAfter->list_status); + $this->assertNull($listAfter->appointment_use_plan_detail); + + $countAfter = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $this->assertEquals(1, $countAfter->used_count); + + echo "\n✅ 测试6通过: CancelYuYue正确释放号源并清空明细\n"; + echo " list_status变为0, appointment_use_plan_detail被清空\n"; + echo " used_count从3变为1\n"; + } + + public function test_optimistic_lock_conflict_detection() + { + $countRecord = DB::table('s_source_roster_detail_count')->where('id', 9999)->first(); + $oldVersion = $countRecord->version; + + $affected = DB::table('s_source_roster_detail_count') + ->where(['id' => 9999, 'version' => $oldVersion]) + ->update([ + 'used_count' => DB::raw('used_count + 1'), + 'version' => DB::raw('version + 1'), + ]); + + $this->assertEquals(1, $affected); + + $conflictAffected = DB::table('s_source_roster_detail_count') + ->where(['id' => 9999, 'version' => $oldVersion]) + ->update([ + 'used_count' => DB::raw('used_count + 1'), + 'version' => DB::raw('version + 1'), + ]); + + $this->assertEquals(0, $conflictAffected); + + echo "\n✅ 测试7通过: 乐观锁冲突检测生效\n"; + echo " 第一次更新成功(affected=1)\n"; + echo " 第二次使用旧version更新失败(affected=0)\n"; + } +} \ No newline at end of file diff --git a/data.sql b/data.sql index 5594e93..7485417 100644 --- a/data.sql +++ b/data.sql @@ -11,7 +11,7 @@ Target Server Version : 50726 File Encoding : 65001 - Date: 04/02/2026 22:00:37 + Date: 03/04/2026 21:19:56 */ SET NAMES utf8mb4; @@ -212,8 +212,10 @@ CREATE TABLE `s_department_resources` ( `department_resources_status` int(11) NULL DEFAULT NULL COMMENT '当前状态0不可用1可用', `department_id` int(11) NULL DEFAULT NULL COMMENT '所属科室', `adduser` int(11) NULL DEFAULT NULL COMMENT '添加人', - `is_del` int(11) NULL DEFAULT NULL COMMENT '是否删除0否1是', `department_resources_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '资源位置', + `time_mode` tinyint(4) NULL DEFAULT 0 COMMENT '时间模式 0默认 1开启冬夏时令', + `time_range` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开启冬夏时令后各时令的时间范围json', + `is_del` int(11) NULL DEFAULT NULL COMMENT '是否删除0否1是', `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE @@ -312,12 +314,14 @@ CREATE TABLE `s_list` ( `reservation_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '预约时间段id', `reg_date` date NULL DEFAULT NULL COMMENT '登记日期', `reg_time` time(0) NULL DEFAULT NULL COMMENT '登记时间', - `reservation_department` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '入院科室-代表开医嘱的科室', + `reservation_department` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '申请科室-代表开医嘱的科室', + `reservation_department_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请科室代码', `entrust_date` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '医嘱日期', `entrust_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '医嘱时间', `money` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `check_num` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '检查号', `user_brithday` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '生日', + `doctor_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开单医生编码', `docotr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请医生,开遗嘱的医生', `patient_type` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT ' 住院 -> 0 门诊 -> 1 急诊 -> 2 体检 -> 3', `reg_equipment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '登记设备', @@ -368,13 +372,16 @@ CREATE TABLE `s_list` ( `baodaoren` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `xuhaopanduan` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, `appointment_type_id` int(11) NULL DEFAULT NULL COMMENT '真正占用号源的 预约类型id 对应 appointment_type表,有可能医生渠道,但是使用护士的号源池', - `qudao_appointment_type_id` int(11) NULL DEFAULT NULL COMMENT '来源渠道', + `qudao_appointment_type_id` int(11) NULL DEFAULT NULL COMMENT '来源渠道(通过哪个渠道预约的)', + `appointment_use_plan_detail` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '预约后占用号源详情', `medicalHistory` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '病史摘要', `diagnosisName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '临床诊断(诊断名称)', `idCardNumber` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '身份证号', `check_aply_pdf` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '检查申请单pdf地址', `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `his_is_emergency` tinyint(4) NULL DEFAULT 0 COMMENT 'his里是否标注了加急0否1是', + `is_emergency` tinyint(4) NULL DEFAULT 0 COMMENT '预约的时候是否使用了加急预约0否1是', PRIMARY KEY (`id`) USING BTREE, INDEX `list_status`(`list_status`) USING BTREE, INDEX `reg_num`(`reg_num`) USING BTREE, @@ -406,7 +413,7 @@ CREATE TABLE `s_list_log` ( `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 575 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '医嘱日志表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 699 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '医嘱日志表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s_list1111 @@ -526,6 +533,7 @@ CREATE TABLE `s_period` ( DROP TABLE IF EXISTS `s_source_roster`; CREATE TABLE `s_source_roster` ( `id` int(11) NOT NULL AUTO_INCREMENT, + `type` tinyint(4) NULL DEFAULT 0 COMMENT '模板类型0默认1夏令时2冬令时', `date_type` tinyint(4) NULL DEFAULT 1 COMMENT '1工作日2节假日', `weekname` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '星期几', `department_id` int(11) NULL DEFAULT NULL COMMENT '科室id', @@ -546,7 +554,7 @@ CREATE TABLE `s_source_roster` ( INDEX `department`(`department_id`) USING BTREE COMMENT '科室id索引', INDEX `resourece`(`resources_id`) USING BTREE COMMENT '资源id索引', INDEX `services`(`device_id`) USING BTREE COMMENT '服务组id索引' -) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '资源计划预约模板' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 63 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '资源计划预约模板' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s_source_roster_count @@ -558,10 +566,11 @@ CREATE TABLE `s_source_roster_count` ( `appointment_type_id` int(10) NULL DEFAULT NULL COMMENT '预约类型id', `count` int(10) NULL DEFAULT NULL COMMENT '渠道可预约数量', `max_total` int(10) NULL DEFAULT NULL COMMENT '单日全渠道最大量', + `locked_count` int(10) NULL DEFAULT 0 COMMENT '占位数量', `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 223 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '预约计划模板对应的各个渠道可预约数量' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 283 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '预约计划模板对应的各个渠道可预约数量' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s_source_roster_detail @@ -590,7 +599,7 @@ CREATE TABLE `s_source_roster_detail` ( INDEX `department`(`department_id`) USING BTREE COMMENT '科室id索引', INDEX `resourece`(`resources_id`) USING BTREE COMMENT '资源id索引', INDEX `services`(`device_id`) USING BTREE COMMENT '服务组id索引' -) ENGINE = InnoDB AUTO_INCREMENT = 393 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '资源计划预约详情' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 589 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '资源计划预约详情' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s_source_roster_detail_count @@ -600,14 +609,15 @@ CREATE TABLE `s_source_roster_detail_count` ( `id` int(11) NOT NULL AUTO_INCREMENT, `roster_detail_id` int(10) NULL DEFAULT NULL COMMENT '计划模板id', `appointment_type_id` int(10) NULL DEFAULT NULL COMMENT '预约类型id', - `count` int(10) NULL DEFAULT NULL COMMENT '渠道可预约数量', + `count` int(10) NULL DEFAULT 0 COMMENT '渠道可预约数量', `used_count` int(10) NULL DEFAULT 0 COMMENT '已经预约的数量', - `locked_count` int(10) NULL DEFAULT NULL COMMENT '占位数量', - `max_total` int(10) NULL DEFAULT NULL COMMENT '单日全渠道最大量', + `locked_count` int(10) NULL DEFAULT 0 COMMENT '占位数量', + `max_total` int(10) NULL DEFAULT 0 COMMENT '单日全渠道最大量', + `version` int(11) NULL DEFAULT 0 COMMENT '乐观锁', `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1631 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '预约计划详情对应的各个渠道数量和已经预约数量' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 2491 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '预约计划详情对应的各个渠道数量和已经预约数量' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s_source_roster_detail_device @@ -620,7 +630,7 @@ CREATE TABLE `s_source_roster_detail_device` ( `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 393 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '计划明细关联设备表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 589 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '计划明细关联设备表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for s_source_roster_detail_log @@ -635,7 +645,7 @@ CREATE TABLE `s_source_roster_detail_log` ( `created_at` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` datetime(0) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '号源日志' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '号源日志' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for users @@ -653,6 +663,8 @@ CREATE TABLE `users` ( `img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', `status` int(11) NULL DEFAULT NULL COMMENT '0锁定1正常2已删除', `group` int(11) NULL DEFAULT NULL COMMENT '分租', + `group_locked` int(11) NULL DEFAULT 0 COMMENT '是否锁定分组,0否1是,如果锁定了,从his同步信息时不更新分组', + `special_privileges` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '特殊权限,json字符串', `times` int(11) NULL DEFAULT 5 COMMENT '校验密钥剩余次数', `lock_to` datetime(0) NULL DEFAULT '1999-01-01 00:00:00' COMMENT '锁定至时间', `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, diff --git a/opencode.json b/opencode.json index 2bdb7d7..8829f7f 100644 --- a/opencode.json +++ b/opencode.json @@ -1,14 +1,46 @@ { "$schema": "https://opencode.ai/config.json", "provider":{ - "packyapi": { - "name": "统一网关", + "openai-xianyu": { + "name": "xianyu codex", "options": { - "baseURL": "https://ai.pumpkinai.online", - "apiKey": "sk-S8RjGGQmCypHix8jY7t8kyxJVSaWnDsi0OVNMQOeNo0zrvdx" + "api": "openai-responses", + "baseURL": "https://api.asxs.top/v1", + "apiKey": "sk-7c013cc9d4d9ab9bba95a5df3fe3c97a" }, "models": { - "gpt-5.3-codex": { "name": "gpt-5.3-codex" } + "gpt-5.3-codex": { + "name": "GPT-5.3 Codex", + "limit": { + "context": 400000, + "output": 128000 + }, + "options": { + "store": false + }, + "variants": { + "low": {}, + "medium": {}, + "high": {}, + "xhigh": {} + } + }, + "gpt-5.4": { + "name": "GPT-5.4", + "limit": { + "context": 1050000, + "output": 128000 + }, + "options": { + "store": false + }, + "variants": { + "low": {}, + "medium": {}, + "high": {}, + "xhigh": {} + } + } } }, "bailian-coding-plan": { diff --git a/需求文档.md b/需求文档.md new file mode 100644 index 0000000..bd194a0 --- /dev/null +++ b/需求文档.md @@ -0,0 +1,1780 @@ +# 秦皇岛中医院医技预约系统需求文档 + +## 1. 原始需求 + +### 1.1 业务背景 +秦皇岛中医院作为综合性医疗机构,拥有多个医技检查科室(如CT、MRI、超声、内镜等)。随着医疗业务量增长,传统的手工预约管理模式已无法满足需求,存在以下问题: +- 患者排队时间长,检查安排效率低 +- 各科室号源管理混乱,无法合理分配 +- 住院、门诊、急诊、体检等不同渠道的患者预约需求难以统筹 +- HIS系统医嘱信息无法及时同步到检查科室 +- 缺乏统一的报到和登记流程 + +### 1.2 核心需求(大白话描述) +- 医生开医嘱后,患者能通过各种方式(自助机、手机、窗口)预约检查时间 +- 各科室能设置每天的号源,按住院、门诊、急诊等不同渠道分配号源比例 +- 患者预约后能按时段报到,科室能管理检查进度 +- HIS系统开医嘱后自动推送到预约系统,不用人工录入 +- 管理后台能统计预约情况,监控科室运营状况 + +### 1.3 业务场景 +1. **住院患者预约场景**:住院医生在HIS系统开医嘱 → HIS推送医嘱信息 → 护士站工作人员查看待预约列表 → 根据病情和科室号源情况预约合适时段 → 通知患者检查时间 → 患者按时报到检查 +2. **门诊患者预约场景**:门诊医生开医嘱 → 患者缴费后 → 通过自助机或手机H5预约 → 选择合适时段 → 按预约时间报到检查 +3. **急诊患者预约场景**:急诊医生开医嘱 → 急诊优先通道快速预约 → 优先检查 +4. **体检患者预约场景**:体检中心开检查单 → 患者预约体检时段 → 按时段完成检查 + +--- + +## 2. 设计需求(功能需求) + +### 2.1 系统角色定义 + +#### 2.1.1 系统管理员 +- 权限:系统全局配置、用户管理、权限分配、科室基础信息管理 +- 职责:维护系统正常运行,配置全局参数 + +#### 2.1.2 科室管理员(医技科室) +- 权限:本科室号源管理、预约计划制定、科室设备管理、预约记录查询 +- 职责:管理本科室号源,制定预约计划,处理预约业务 + +#### 2.1.3 护士站工作人员(住院科室) +- 权限:查看本病区医嘱、为住院患者预约、取消预约、查看预约记录 +- 职责:为本病区住院患者预约医技检查 + +#### 2.1.4 临床医生 +- 权限:查看本人开具医嘱的预约情况、为患者预约 +- 职责:开医嘱后可查看或协助患者预约 + +#### 2.1.5 患者/家属 +- 权限:查看本人医嘱、自助预约、取消预约、改约 +- 职责:通过自助机或H5完成自助预约 + +#### 2.1.6 第三方系统 +- 权限:通过接口推送医嘱、查询预约状态 +- 著作:HIS系统、PACS系统、体检系统等外部系统对接 + +### 2.2 功能模块划分 + +#### 2.2.1 系统管理模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| SYS-001 | 用户管理 | 添加、修改、删除系统用户,分配用户分组,重置密码 | 系统管理员 | +| SYS-002 | 分组管理 | 创建用户分组,分配菜单权限 | 系统管理员 | +| SYS-003 | 菜单管理 | 配置系统菜单结构、菜单权限 | 系统管理员 | +| SYS-004 | 系统配置 | 配置系统全局参数(如预约超时时间、缴费超时等) | 系统管理员 | +| SYS-005 | 密码修改 | 用户修改自身密码 | 所有用户 | + +#### 2.2.2 基础数据管理模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| BASE-001 | 科室管理 | 维护医技科室信息(科室名称、编号、位置、状态) | 系统管理员/科室管理员 | +| BASE-002 | 科室资源管理 | 维护科室检查室/机房资源(资源名称、位置、状态、冬夏时令配置) | 科室管理员 | +| BASE-003 | 设备管理 | 维护检查设备信息(设备名称、状态) | 科室管理员 | +| BASE-004 | 检查项目管理 | 维护检查项目(项目名称、代码、分类、检查须知、空腹要求、检查时长、关联服务组) | 科室管理员 | +| BASE-005 | 检查项目分类 | 维护检查项目分类信息 | 科室管理员 | +| BASE-006 | 项目设备绑定 | 配置检查项目可用的设备/服务组 | 科室管理员 | +| BASE-007 | 项目互斥配置 | 配置检查项目之间的互斥关系(如某些检查不能在同一天进行) | 科室管理员 | +| BASE-008 | 时间段管理 | 配置预约时间段(时间段名称、开始时间、结束时间、截止预约时间) | 科室管理员 | +| BASE-009 | 病区管理 | 维护住院病区信息 | 系统管理员 | +| BASE-010 | HIS数据同步 | 从HIS系统同步科室、用户、检查项目等基础数据 | 系统管理员 | + +#### 2.2.3 预约计划管理模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| PLAN-001 | 预约计划模板 | 配置每周的号源模板(按星期、时间段、资源、设备配置号源) | 科室管理员 | +| PLAN-002 | 渠道比例配置 | 配置各预约渠道(住院/门诊/急诊/体检)的号源占比 | 科室管理员 | +| PLAN-003 | 模板占位管理 | 设置模板的占位数量(预留号源) | 科室管理员 | +| PLAN-004 | 生成计划明细 | 根据模板批量生成未来日期的号源明细 | 科室管理员 | +| PLAN-005 | 计划明细管理 | 查看、修改、删除已生成的号源明细 | 科室管理员 | +| PLAN-006 | 明细占位管理 | 调整特定日期号源的占位数量 | 科室管理员 | +| PLAN-007 | 节假日配置 | 配置特殊日期的工作日/节假日标识 | 科室管理员 | +| PLAN-008 | 体检日历管理 | 为体检机构配置预约日历 | 科室管理员 | + +#### 2.2.4 预约业务处理模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| YUYUE-001 | 医嘱查询列表 | 查看待预约、已预约、已报到、已完成的医嘱列表 | 科室管理员/护士/医生 | +| YUYUE-002 | 开始预约 | 为患者选择合适的时段完成预约 | 科室管理员/护士/医生/患者 | +| YUYUE-003 | 取消预约 | 取消已预约记录,释放号源 | 科室管理员/护士/医生/患者 | +| YUYUE-004 | 更改预约 | 修改预约时间,调整到其他时段 | 科室管理员/护士/医生/患者 | +| YUYUE-005 | 报到登记 | 患者到院后报到登记 | 科室管理员 | +| YUYUE-006 | 取消报到 | 取消报到状态 | 科室管理员 | +| YUYUE-007 | 查看日志 | 查看医嘱状态变更日志 | 所有用户 | +| YUYUE-008 | 打印申请单 | 打印检查申请单(含条形码) | 科室管理员/护士 | +| YUYUE-009 | 自动预约 | 系统根据规则自动为患者分配时段 | 系统 | +| YUYUE-010 | 门诊缴费检查 | 检查门诊患者是否缴费,未缴费提示 | 系统 | +| YUYUE-011 | 超时取消 | 门诊患者缴费超时自动取消预约 | 系统 | + +#### 2.2.5 统计查询模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| STAT-001 | 预约统计 | 统计各科室、各时段的预约情况 | 科室管理员 | +| STAT-002 | 渠道统计 | 统计各预约渠道的使用情况 | 科室管理员 | +| STAT-003 | 开单统计 | 统计医生开单情况 | 科室管理员 | +| STAT-004 | 号源占用查询 | 查看特定时段号源占用明细 | 科室管理员 | + +#### 2.2.6 HIS集成模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| HIS-001 | 医嘱消息接收 | 接收HIS推送的医嘱信息(MI0150) | 系统 | +| HIS-002 | 医嘱状态变更 | 接收医嘱状态变更消息(MI0164) | 系统 | +| HIS-003 | 医嘱更新 | 接收医嘱更新消息(MU0165) | 系统 | +| HIS-004 | 检查信息同步 | 接收检查信息消息(CI0047/CI0054/CI0055) | 系统 | +| HIS-005 | 检查确认同步 | 接收检查确认消息(CD0050) | 系统 | +| HIS-006 | 医嘱订单同步 | 接收医嘱订单消息(OI0083/OD0084) | 系统 | +| HIS-007 | 向PACS推送申请单 | 预约后向PACS系统推送检查申请单 | 系统 | +| HIS-008 | 从PACS接收报到 | 接收PACS系统的报到/取消报到通知 | 系统 | + +#### 2.2.7 自助机模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| ZIZHU-001 | 患者登录 | 通过登记号/就诊卡登录 | 患者 | +| ZIZHU-002 | 查看医嘱列表 | 查看本人的医嘱检查项目 | 患者 | +| ZIZHU-003 | 自助预约 | 选择时段完成预约 | 患者 | +| ZIZHU-004 | 取消/改约 | 取消预约或更改预约时间 | 患者 | + +#### 2.2.8 H5移动端模块 +| 功能编号 | 功能名称 | 功能描述 | 角色 | +|---------|---------|---------|------| +| H5-001 | 患者登录 | 通过患者ID登录 | 患者 | +| H5-002 | 查看医嘱 | 查看本人医嘱检查项目 | 患者 | +| H5-003 | 预约操作 | 完成预约、取消预约 | 患者 | +| H5-004 | 查看详情 | 查看医嘱详情、预约信息 | 患者 | + +### 2.3 核心业务流程 + +#### 2.3.1 住院患者预约流程 +``` +HIS开医嘱 → MQ推送医嘱信息 → 监听服务接收写入s_list(状态0) → +护士站查询待预约列表 → 选择医嘱 → 选择时段 → 预约操作(状态1) → +通知患者 → 患者按时报到(状态2) → 检查完成(状态3) +``` + +#### 2.3.2 门诊患者预约流程 +``` +HIS开医嘱 → MQ推送医嘱信息 → 监听服务接收写入s_list(状态0) → +患者缴费 → 缴费状态更新 → 患者自助机/H5登录 → +查看医嘱 → 选择时段预约 → 检查缴费状态 → 预约成功(状态1) → +患者按时报到(状态2) → 检查完成(状态3) +``` + +#### 2.3.3 号源生成流程 +``` +科室管理员配置预约计划模板 → 配置各渠道号源比例 → +选择模板 → 设置日期范围 → 批量生成计划明细 → +生成s_source_roster_detail表记录 → +生成s_source_roster_detail_count表记录(含乐观锁version) +``` + +#### 2.3.4 HIS消息处理流程 +``` +HIS系统推送MQ消息 → C#监听服务接收 → +解析XML消息 → 提取医嘱信息 → +写入/更新s_list表 → 记录操作日志 → +返回确认消息 +``` + +--- + +## 3. 设计规格(技术规范) + +### 3.1 技术栈选择 + +#### 3.1.1 后端技术栈 +- **框架**: Laravel 8 (PHP 7.4+/8.0+) +- **数据库**: MySQL 5.7+ +- **认证**: Laravel Sanctum (Token认证) +- **PDF生成**: barryvdh/laravel-snappy + wkhtmltopdf +- **条形码生成**: picqer/php-barcode-generator +- **跨域处理**: fruitcake/laravel-cors + +#### 3.1.2 前端技术栈 +- **管理后台**: Vue 3 + Vite 4 + Element Plus 2.3 + Pinia + Vue Router +- **自助机**: Vue 3 + Vite 5 + Element Plus 2.7 + Vue Router +- **H5移动端**: uni-app (支持多端发布) +- **HTTP客户端**: Axios + +#### 3.1.3 其他服务 +- **WebSocket服务**: Workerman (PHP) +- **HIS监听服务**: C# Windows Service + MQ Client + +### 3.2 接口规范 + +#### 3.2.1 API基础规范 +- **基础路径**: `/api/v1/` +- **认证方式**: Bearer Token (Sanctum) +- **请求方式**: POST (业务接口)、GET (查询接口) +- **响应格式**: JSON + ```json + { + "status": true/false, + "msg": "提示信息", + "data": {} + } + ``` + +#### 3.2.2 接口分类 +1. **系统管理接口** (`/api/v1/admin/*`) + - 用户、分组、菜单、配置管理 +2. **业务管理接口** (`/api/v1/admin/*`) + - 科室、设备、检查项目、预约计划管理 +3. **H5端接口** (`/api/v1/H5/*`) + - 患者登录、医嘱查询、预约操作 +4. **第三方接口** (`/api/v1/T/*`) + - HIS推送医嘱、PACS报到通知 + +#### 3.2.3 中间件 +- `checktoken`: 验证用户Token +- `check.sign`: 验证第三方接口签名 +- `log`: 记录接口访问日志 +- `xmllog`: 记录XML格式接口日志 + +### 3.3 数据库规范 + +#### 3.3.1 表命名规范 +- 系统表:无前缀 (如 `users`, `group`, `menu`) +- 业务表:前缀 `s_` (如 `s_department`, `s_list`) +- 字段使用下划线命名法 + +#### 3.3.2 通用字段规范 +- **主键**: `id` (自增INT或BIGINT) +- **创建时间**: `created_at` (timestamp, 自动填充) +- **更新时间**: `updated_at` (datetime, 自动更新) +- **状态字段**: `status` (0-关闭/禁用, 1-开启/正常) +- **删除标记**: `is_del` (0-未删除, 1-已删除,软删除) +- **操作人**: `adduser` / `create_user` (INT) + +#### 3.3.3 状态编码规范 +- **预约状态** (`list_status`): + - 0: 正在申请(医嘱已接收,待预约) + - 1: 已预约 + - 2: 已登记(已报到) + - 3: 已结束(检查完成) +- **患者类型** (`patient_type`): + - 0: 住院 + - 1: 门诊 + - 2: 急诊 + - 3: 体检 + +#### 3.3.4 乐观锁机制 +- 号源数量表 `s_source_roster_detail_count` 使用 `version` 字段实现乐观锁 +- 预约操作时需检查version并递增,防止并发冲突 + +### 3.4 代码规范 + +#### 3.4.1 Laravel代码规范 +- 遵循PSR-12编码规范 +- 控制器命名: `XxxController.php` +- 模型命名: 单数形式,对应表名 +- 控制器位于 `App\Http\Controllers\API\Admin\YeWu\` 目录 +- 复杂业务逻辑封装在 `app\Lib\` 或Service层 +- 使用Eloquent ORM操作数据库 + +#### 3.4.2 Vue前端规范 +- 组件命名: PascalCase (如 `PlanModel.vue`) +- 使用Composition API (推荐) 或 Options API +- Element Plus组件库统一UI风格 +- API调用通过 `src/api/` 模块统一管理 +- 使用Pinia进行状态管理 + +#### 3.4.3 通用规范 +- 避免硬编码,使用配置表或环境变量 +- 异常统一捕获和记录 +- 关键操作记录日志 +- 防止SQL注入、XSS攻击 + +### 3.5 安全与权限规范 + +#### 3.5.1 认证机制 +- 用户登录: 用户名+密码验证,返回Token +- Token有效期: 可配置,支持Token刷新 +- Token存储: 客户端存储access_token和refresh_token + +#### 3.5.2 权限控制 +- 基于分组+菜单的权限体系 +- 用户属于分组,分组关联菜单 +- API层面检查用户分组和菜单权限 +- 科室管理员只能操作本科室数据 + +#### 3.5.3 数据安全 +- 密码加密存储 (bcrypt/hash) +- 第三方接口签名验证 (app_id + app_secret + timestamp + sign) +- HIS医嘱ID唯一性校验 (`entrust_id`字段) +- 软删除机制保护数据不物理删除 + +#### 3.5.4 业务安全 +- 门诊患者未缴费不允许预约(可配置) +- 预约超时自动取消并释放号源 +- 检查项目互斥校验(部分检查不能同一天进行) +- 号源数量一致性校验(乐观锁) + +--- + +## 4. 概要设计(整体架构) + +### 4.1 系统架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ HIS系统 │ +│ (医嘱开立、缴费、医嘱状态变更、检查信息) │ +└─────────────────┬───────────────────────────────────────────┘ + │ MQ消息队列 + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ HIS消息监听服务 (C# Windows Service) │ +│ - MI0150Listener (医嘱信息) │ +│ - MI0164Listener (医嘱状态变更) │ +│ - MU0165Listener (医嘱更新) │ +│ - CI0047/CI0054/CI0055Listener (检查信息) │ +│ - CD0050Listener (检查确认) │ +│ - OI0083/OD0084Listener (医嘱订单) │ +└─────────────────┬───────────────────────────────────────────┘ + │ 写入数据库 + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ MySQL数据库 │ +│ - 基础数据表 (科室、设备、检查项目) │ +│ - 号源表 (计划模板、计划明细、号源数量) │ +│ - 业务表 (s_list预约主表、日志表) │ +│ - 系统表 (用户、分组、菜单) │ +└─────────────────┬───────────────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Laravel后端API服务 │ +│ ├─ 系统管理API (用户、权限、配置) │ +│ ├─ 基础数据API (科室、设备、项目) │ +│ ├─ 预约管理API (计划模板、号源管理) │ +│ ├─ 业务处理API (预约、取消、报到) │ +│ ├─ H5端API (患者自助预约) │ +│ ├─ 第三方API (HIS推送、PACS报到) │ +│ └─ WebSocket推送 (实时通知) │ +└─────────────────┬───────────────────────────────────────────┘ + │ HTTP/WebSocket + ↓ +┌──────────────┬──────────────┬──────────────┬────────────────┐ +│ 管理后台 │ 自助机 │ H5移动端 │ PACS系统 │ +│ (Vue 3) │ (Vue 3) │ (uni-app) │ │ +│ │ │ │ │ +│ - 科室管理 │ - 患者登录 │ - 患者登录 │ - 接收申请单 │ +│ - 号源配置 │ - 查看医嘱 │ - 查看医嘱 │ - 报到通知 │ +│ - 预约管理 │ - 自助预约 │ - 自助预约 │ │ +│ - 统计查询 │ - 取消改约 │ - 取消改约 │ │ +└──────────────┴──────────────┴──────────────┴────────────────┘ +``` + +### 4.2 模块划分 + +#### 4.2.1 后端模块结构 +``` +Laravel/ +├── app/ +│ ├── Http/Controllers/API/ +│ │ ├── Admin/ +│ │ │ ├── YeWu/ # 业务控制器 +│ │ │ │ ├── DepartmentController.php +│ │ │ │ ├── DevicesController.php +│ │ │ │ ├── CheckItemController.php +│ │ │ │ ├── PlanModelController.php +│ │ │ │ ├── PlanListController.php +│ │ │ │ ├── WorkMainController.php +│ │ │ │ ├── SignInController.php +│ │ │ │ └── ... +│ │ │ ├── LoginController.php +│ │ │ ├── UserController.php +│ │ │ ├── GroupController.php +│ │ │ ├── MenuController.php +│ │ │ └── ... +│ │ ├── H5/ # H5端控制器 +│ │ ├── His/ # HIS对接控制器 +│ │ ├── Third/ # 第三方接口控制器 +│ │ └── PdfController.php +│ ├── Lib/ # 自定义类库 +│ │ ├── Tools.php # 工具类 +│ │ ├── Yz.php # 响应格式化类 +│ │ └── JWT.php # JWT处理类 +│ ├── Models/ # 数据模型 +│ ├── Middleware/ # 中间件 +│ │ ├── CheckToken.php +│ │ ├── Log.php +│ │ └── CheckSign.php +│ └── Services/ # 服务层(业务逻辑) +│ └ Admin/YeWu/ +│ │ ├── RosterService.php +│ │ ├── PlanListService.php + │ └── ... +├── routes/ +│ ├── api.php # API路由 +│ └── web.php +├── config/ +│ ├── app.php +│ ├── database.php +│ └── ... +└── database/ + └── migrations/ # 数据库迁移文件 +``` + +#### 4.2.2 前端模块结构 +``` +YiJi-admin/ # 管理后台 +├── src/ +│ ├── views/ +│ │ ├── YeWu/ # 业务管理页面 +│ │ │ ├── DepartmentConfig.vue +│ │ │ ├── DevicesConfig.vue +│ │ │ ├── CheckItemConfig.vue +│ │ │ ├── MainList.vue +│ │ │ ├── DoctorYuYue.vue +│ │ │ └── ... +│ │ ├── AppointmentMngr/ # 预约管理页面 +│ │ │ ├── PlanModel.vue +│ │ │ ├── PlanList.vue +│ │ │ ├── TimePeriodMngr.vue +│ │ │ ├── RatioMngr.vue +│ │ │ └── ... +│ │ ├── SystemMngr/ # 系统管理页面 +│ │ │ ├── User/List.vue +│ │ │ ├── Group/List.vue +│ │ │ ├── Menu/List.vue +│ │ │ └── ... +│ │ ├── Info/ # 统计查询页面 +│ │ └── Login.vue +│ ├── components/ # 公共组件 +│ ├── router/ # 路由配置 +│ ├── store/ # Pinia状态管理 +│ ├── api/ # API接口模块 +│ └── utils/ # 工具函数 + +ZiZhuJi/ # 自助机 +├── src/ +│ ├── views/ +│ │ ├── Login.vue +│ │ ├── CheckItemMainList.vue +│ │ ├── PlanList.vue +│ │ └── ... +│ ├── tools/ +│ ├── router/ +│ └── api/ + +h5/ # H5移动端 +├── pages/ +│ ├── Login.vue +│ ├── CheckItemMainList.vue +│ ├── PlanList.vue +│ └── ... +├── api/ +├── static/ +└── uni_modules/ # uni-app组件模块 +``` + +### 4.3 数据流程 + +#### 4.3.1 医嘱接收流程 +``` +HIS开医嘱 + ↓ +MQ推送XML消息 (MI0150) + ↓ +C#监听服务接收消息 + ↓ +解析XML提取医嘱信息: + - 患者信息 (姓名、性别、年龄、身份证) + - 医嘱信息 (医嘱名称、医嘱代码、医嘱时间) + - 科室信息 (申请科室、执行科室) + - 患者类型 (住院/门诊/急诊) + - 住院信息 (病区、床号) + ↓ +写入s_list表 (list_status=0) + ↓ +记录s_list_log日志 + ↓ +返回确认消息给HIS +``` + +#### 4.3.2 预约操作流程 +``` +用户选择医嘱 + ↓ +查询可预约时段 + ↓ +GetEnablePlan接口查询: + - 根据医嘱查找关联科室/资源/设备 + - 查询s_source_roster_detail可用号源 + - 检查号源数量 (s_source_roster_detail_count) + - 检查项目互斥关系 (s_huchi) + - 检查门诊缴费状态 + ↓ +返回可用时段列表 + ↓ +用户选择时段 + ↓ +YuYue接口执行预约: + - 乐观锁检查version + - 更新号源数量 (递增used_count, 递增version) + - 更新s_list记录 (list_status=1, 预约时间、资源) + - 记录s_list_log日志 + - 向PACS推送申请单 + ↓ +预约成功 +``` + +#### 4.3.3 号源生成流程 +``` +科室管理员配置模板 + ↓ +PlanModelSave接口保存: + - 写入s_source_roster表 + - 配置时间段、资源、设备 + - 配置患者类型 + ↓ +配置渠道比例 + ↓ +SaveAppointmentRatio接口保存: + - 写入s_appointment_type_ratio表 + - 设置各渠道占比 + - 设置合并号源渠道 + ↓ +生成计划明细 + ↓ +Create接口批量生成: + - 根据模板和日期范围 + - 写入s_source_roster_detail表 (每日号源) + - 写入s_source_roster_detail_count表 (各渠道数量) + - 初始化version=0 + ↓ +号源生成完成 +``` + +### 4.4 核心表设计思路 + +#### 4.4.1 号源管理表设计 +``` +预约计划模板 (s_source_roster) + ↑ 模板配置 + ↓ +模板渠道数量 (s_source_roster_count) + ↑ 各渠道号源配置 + ↓ +预约计划明细 (s_source_roster_detail) + ↑ 每日具体号源 + ↓ +明细渠道数量 (s_source_roster_detail_count) + ↑ 各渠道实际数量 + 乐观锁version + ↓ +预约记录 (s_list) + ↑ 占用号源 +``` + +#### 4.4.2 预约主表设计 +``` +s_list表 (预约主表) +├─ 患者信息字段 +│ - user_name, user_sex, user_age, user_brithday +│ - user_phone, idCardNumber +│ - hospital_number (住院号), prescription_id (患者ID) +│ +├─ 医嘱信息字段 +│ - entrust (医嘱名称), entrust_code (医嘱代码) +│ - entrust_id (HIS医嘱ID, 唯一标识) +│ - entrust_date, entrust_time (医嘱时间) +│ - app_num (申请单号) +│ +├─ 科室信息字段 +│ - reservation_department (申请科室) +│ - RISRAcceptDeptCode (执行科室代码) +│ - department_id (预约科室ID) +│ +├─ 预约信息字段 +│ - reservation_date (预约日期) +│ - reservation_time (时间段ID) +│ - reservation_sources (预约资源ID) +│ - roster_id (计划明细ID) +│ - services_group (服务组) +│ - appointment_type_id (占用号源的渠道) +│ - qudao_appointment_type_id (预约来源渠道) +│ +├─ 住院信息字段 +│ - warddesc (病区), wardcode (病区编号) +│ - bedname, bedno (床号) +│ +├─ 状态字段 +│ - list_status (0申请/1预约/2登记/3结束) +│ - is_pay (是否缴费) +│ - is_del (删除标记) +│ - is_nullify (作废标记) +│ - his_is_emergency (HIS加急标记) +│ - is_emergency (加急预约) +│ +├─ 时间字段 +│ - canel_time (取消预约时间) +│ - reg_date, reg_time (登记时间) +│ - addtime (预约时间) +│ +└─ 其他字段 + - reg_num (登记号) + - check_num (检查号) + - medicalHistory (病史) + - diagnosisName (诊断) + - check_aply_pdf (申请单PDF地址) +``` + +--- + +## 5. 详细设计(实现细节) + +### 5.1 全部接口清单 + +系统共提供 **91个接口**,分为以下几大类: +- 系统管理接口(15个) +- 基础数据管理接口(20个) +- 预约计划管理接口(15个) +- 预约业务处理接口(20个) +- HIS集成接口(6个) +- H5移动端接口(5个) +- 第三方对接接口(10个) + +--- + +#### 5.1.1 系统管理接口(15个) + +##### 1. 用户登录 +- **URL**: `POST /api/admin/login` +- **权限**: 无需认证 +- **入参**: + ```json + { + "username": "用户名", + "pwd": "密码" + } + ``` +- **出参**: + ```json + { + "status": true, + "msg": "登录成功", + "data": { + "token": "access_token", + "refresh_token": "refresh_token", + "userinfo": { + "id": 1, + "cn_name": "姓名", + "username": "用户名", + "group": 1, + "department_id": 1 + } + } + } + ``` + +##### 2. 刷新Token +- **URL**: `POST /api/tokenRefresh` +- **权限**: 无需认证 +- **入参**: + ```json + { + "refresh_token": "refresh_token" + } + ``` +- **出参**: 返回新的access_token和refresh_token + +##### 3. 获取服务器时间 +- **URL**: `POST /api/GetServiceDateTime` +- **权限**: 无需认证 +- **入参**: 无 +- **出参**: 返回当前服务器时间 + +##### 4. 获取用户菜单列表 +- **URL**: `POST /api/v1/admin/getBaseMenuList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回当前用户权限范围内的菜单树 + +##### 5. 获取用户基本信息 +- **URL**: `POST /api/v1/admin/GetBaseUserInfo` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回用户详细信息 + +##### 6. 获取菜单列表 +- **URL**: `POST /api/v1/admin/getMenuList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回所有菜单列表(树形结构) + +##### 7. 获取一级菜单 +- **URL**: `POST /api/v1/admin/GetFatherMenuList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回一级菜单列表 + +##### 8. 添加菜单 +- **URL**: `POST /api/v1/admin/AddMenu` +- **权限**: 需要Token +- **入参**: + ```json + { + "pid": 0, + "name": "菜单名称", + "url": "/path", + "order": 1, + "icon": "icon-name" + } + ``` +- **出参**: 返回添加结果 + +##### 9. 修改菜单 +- **URL**: `POST /api/v1/admin/EditMenu` +- **权限**: 需要Token +- **入参**: 菜单完整信息(含id) +- **出参**: 返回修改结果 + +##### 10. 获取用户列表 +- **URL**: `POST /api/v1/admin/getUserList` +- **权限**: 需要Token +- **入参**: + ```json + { + "page": 1, + "pageSize": 20, + "searchInfo": {} + } + ``` +- **出参**: 返回用户列表和总数 + +##### 11. 获取用户详情 +- **URL**: `POST /api/v1/admin/GetSystemUserDetail` +- **权限**: 需要Token +- **入参**: `{ "id": 1 }` +- **出参**: 返回用户详细信息 + +##### 12. 保存用户信息 +- **URL**: `POST /api/v1/admin/SaveSystemUserInfo` +- **权限**: 需要Token +- **入参**: 用户完整信息(新增/编辑) +- **出参**: 返回保存结果 + +##### 13. 重置用户密码 +- **URL**: `POST /api/v1/admin/resetPwd` +- **权限**: 需要Token +- **入参**: `{ "id": 1 }` +- **出参**: 返回重置后的默认密码 + +##### 14. 修改密码 +- **URL**: `POST /api/v1/admin/adminChangePwd` +- **权限**: 需要Token +- **入参**: + ```json + { + "old_pwd": "旧密码", + "new_pwd": "新密码" + } + ``` +- **出参**: 返回修改结果 + +##### 15. 修改个人信息 +- **URL**: `POST /api/v1/admin/ChangInfo` +- **权限**: 需要Token +- **入参**: 可修改的个人信息字段 +- **出参**: 返回修改结果 + +##### 16. 获取分组列表 +- **URL**: `POST /api/v1/admin/getGroupList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回用户分组列表 + +##### 17. 保存分组 +- **URL**: `POST /api/v1/admin/SaveGroup` +- **权限**: 需要Token +- **入参**: 分组信息(新增/编辑) +- **出参**: 返回保存结果 + +##### 18. 获取分组菜单列表 +- **URL**: `POST /api/v1/admin/GetGroupMenuList` +- **权限**: 需要Token +- **入参**: `{ "group_id": 1 }` +- **出参**: 返回该分组关联的菜单ID列表 + +##### 19. 修改分组菜单 +- **URL**: `POST /api/v1/admin/GroupChangeMenu` +- **权限**: 需要Token +- **入参**: + ```json + { + "group_id": 1, + "menu_ids": [1, 2, 3] + } + ``` +- **出参**: 返回修改结果 + +##### 20. 检查菜单权限 +- **URL**: `POST /api/v1/admin/CheckMenuAuth` +- **权限**: 需要Token +- **入参**: `{ "menu_id": 1 }` +- **出参**: 返回是否有权限 + +##### 21. 获取配置信息 +- **URL**: `POST /api/v1/admin/GetConfigInfo` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回系统配置参数 + +##### 22. 保存配置信息 +- **URL**: `POST /api/v1/admin/SaveConfigInfo` +- **权限**: 需要Token +- **入参**: 配置参数键值对 +- **出参**: 返回保存结果 + +##### 23. 上传文件 +- **URL**: `POST /api/UpFile` +- **权限**: 需要Token +- **入参**: multipart/form-data文件 +- **出参**: 返回文件URL + +##### 24. 切换默认科室 +- **URL**: `POST /api/v1/admin/ChangeDefaultDept` +- **权限**: 需要Token +- **入参**: `{ "department_id": 1 }` +- **出参**: 返回切换结果 + +--- + +#### 5.1.2 基础数据管理接口(20个) + +##### 1. 获取科室列表 +- **URL**: `POST /api/v1/admin/GetDepartmentList` +- **权限**: 需要Token +- **入参**: + ```json + { + "page": 1, + "pageSize": 20, + "searchInfo": {} + } + ``` +- **出参**: 返回科室列表 + +##### 2. 获取启用的科室列表 +- **URL**: `POST /api/v1/admin/GetEnableDepartmentList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回启用状态的科室列表 + +##### 3. 保存科室信息 +- **URL**: `POST /api/v1/admin/SaveDepartment` +- **权限**: 需要Token +- **入参**: 科室完整信息 +- **出参**: 返回保存结果 + +##### 4. 删除科室 +- **URL**: `POST /api/v1/admin/DelDepartment` +- **权限**: 需要Token +- **入参**: `{ "ids": [1, 2, 3] }` +- **出参**: 返回删除结果 + +##### 5. 获取科室资源列表 +- **URL**: `POST /api/v1/admin/DepartmentResourceGetList` +- **权限**: 需要Token +- **入参**: + ```json + { + "page": 1, + "pageSize": 20, + "searchInfo": { "department_id": 1 } + } + ``` +- **出参**: 返回科室资源列表 + +##### 6. 获取可用科室资源 +- **URL**: `POST /api/v1/admin/DepartmentResourceGetEnableList` +- **权限**: 需要Token +- **入参**: `{ "department_id": 1 }` +- **出参**: 返回可用资源列表 + +##### 7. 保存科室资源 +- **URL**: `POST /api/v1/admin/SaveDepartmentResource` +- **权限**: 需要Token +- **入参**: 资源完整信息 +- **出参**: 返回保存结果 + +##### 8. 删除科室资源 +- **URL**: `POST /api/v1/admin/DepartmentResourceDel` +- **权限**: 需要Token +- **入参**: `{ "ids": [1, 2, 3] }` +- **出参**: 返回删除结果 + +##### 9. 绑定科室资源与设备 +- **URL**: `POST /api/v1/admin/DepartmentResourceBindDevice` +- **权限**: 需要Token +- **入参**: + ```json + { + "resource_id": 1, + "device_ids": [1, 2, 3] + } + ``` +- **出参**: 返回绑定结果 + +##### 10. 获取资源已绑定设备 +- **URL**: `POST /api/v1/admin/ResourceGetBindDeviceList` +- **权限**: 需要Token +- **入参**: `{ "resource_id": 1 }` +- **出参**: 返回已绑定的设备列表 + +##### 11. 获取设备列表 +- **URL**: `POST /api/v1/admin/GetDeviceList` +- **权限**: 需要Token +- **入参**: + ```json + { + "page": 1, + "pageSize": 20, + "searchInfo": {} + } + ``` +- **出参**: 返回设备列表 + +##### 12. 获取可用设备列表 +- **URL**: `POST /api/v1/admin/GetEnableDeviceList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回启用状态的设备列表 + +##### 13. 保存设备 +- **URL**: `POST /api/v1/admin/SaveDeviceList` +- **权限**: 需要Token +- **入参**: 设备完整信息 +- **出参**: 返回保存结果 + +##### 14. 删除设备 +- **URL**: `POST /api/v1/admin/DelDevice` +- **权限**: 需要Token +- **入参**: `{ "ids": [1, 2, 3] }` +- **出参**: 返回删除结果 + +##### 15. 获取检查项目分类列表 +- **URL**: `POST /api/v1/admin/GetCheckItemClassList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回检查项目分类列表 + +##### 16. 获取检查项目列表 +- **URL**: `POST /api/v1/admin/GetCheckItemList` +- **权限**: 需要Token +- **入参**: + ```json + { + "page": 1, + "pageSize": 20, + "searchInfo": {} + } + ``` +- **出参**: 返回检查项目列表 + +##### 17. 保存检查项目 +- **URL**: `POST /api/v1/admin/SaveItemInfo` +- **权限**: 需要Token +- **入参**: 检查项目完整信息 +- **出参**: 返回保存结果 + +##### 18. 检查项目绑定设备 +- **URL**: `POST /api/v1/admin/ItemBindDevice` +- **权限**: 需要Token +- **入参**: + ```json + { + "item_id": 1, + "device_ids": [1, 2, 3] + } + ``` +- **出参**: 返回绑定结果 + +##### 19. 设置项目互斥 +- **URL**: `POST /api/v1/admin/SetHuChi` +- **权限**: 需要Token +- **入参**: + ```json + { + "code1": "检查项目代码1", + "code2": "检查项目代码2", + "time": 24 + } + ``` +- **出参**: 返回设置结果 + +##### 20. 删除项目互斥 +- **URL**: `POST /api/v1/admin/DelHuChi` +- **权限**: 需要Token +- **入参**: `{ "id": 1 }` +- **出参**: 返回删除结果 + +##### 21. 获取项目互斥列表 +- **URL**: `POST /api/v1/admin/GetHuChiList` +- **权限**: 需要Token +- **入参**: `{ "item_code": "项目代码" }` +- **出参**: 返回该项目已设置的互斥列表 + +##### 22. 获取时间段列表 +- **URL**: `POST /api/v1/admin/TimePeriodGetList` +- **权限**: 需要Token +- **入参**: + ```json + { + "page": 1, + "pageSize": 20, + "searchInfo": { "department_id": 1 } + } + ``` +- **出参**: 返回时间段列表 + +##### 23. 获取启用的时间段列表 +- **URL**: `POST /api/v1/admin/TimePeriodGetEnableList` +- **权限**: 需要Token +- **入参**: `{ "department_id": 1 }` +- **出参**: 返回启用的时间段列表 + +##### 24. 获取时间段详情 +- **URL**: `POST /api/v1/admin/TimePeriodGetDetail` +- **权限**: 需要Token +- **入参**: `{ "id": 1 }` +- **出参**: 返回时间段详细信息 + +##### 25. 保存时间段 +- **URL**: `POST /api/v1/admin/TimePeriodSave` +- **权限**: 需要Token +- **入参**: 时间段完整信息 +- **出参**: 返回保存结果 + +##### 26. 删除时间段 +- **URL**: `POST /api/v1/admin/TimePeriodDel` +- **权限**: 需要Token +- **入参**: `{ "ids": [1, 2, 3] }` +- **出参**: 返回删除结果 + +##### 27. 获取病区列表 +- **URL**: `POST /api/v1/admin/InpatientWardGetList` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回病区列表 + +##### 28. 保存病区 +- **URL**: `POST /api/v1/admin/InpatientWardSave` +- **权限**: 需要Token +- **入参**: 病区信息 +- **出参**: 返回保存结果 + +##### 29. 删除病区 +- **URL**: `POST /api/v1/admin/InpatientWardDel` +- **权限**: 需要Token +- **入参**: `{ "ids": [1, 2, 3] }` +- **出参**: 返回删除结果 + +##### 30. 获取预约类型列表 +- **URL**: `POST /api/v1/admin/GetYuYueTypes` +- **权限**: 需要Token +- **入参**: 无 +- **出参**: 返回预约类型列表 + +--- + +#### 5.1.3 预约计划管理接口(15个) + +##### 1. 获取预约计划模板列表 +- **URL**: `POST /api/v1/admin/PlanModelGetList` +- **入参**: + ```json + { + "searchInfo": { + "date_type": 1, // 1工作日/2节假日 + "type": 0, // 0默认/1夏令时/2冬令时 + "resources_id": 1 // 资源ID + } + } + ``` +- **出参**: 返回模板列表,含各渠道号源配置 +- **逻辑**: + 1. 查询s_source_roster表 + 2. 关联s_source_roster_count表获取渠道数量 + 3. 按科室、资源、时间段分组展示 + +#### 5.1.3 生成预约计划明细 +- **URL**: `POST /api/v1/admin/CreatePlanList` +- **入参**: + ```json + { + "dateRange": ["2024-01-01", "2024-12-31"], + "ids": [1, 2, 3], // 模板ID数组 + "date_type": 1, // 1工作日/2节假日 + "HolidayEnable": true // 是否生成节假日 + } + ``` +- **出参**: + ```json + { + "status": true, + "msg": "执行完成,共计生成计划 X 条", + "data": { + "count": 365 + } + } + ``` +- **逻辑**: + 1. 调用RosterService.generatePlans服务 + 2. 根据模板和日期范围生成明细 + 3. 写入s_source_roster_detail表 + 4. 写入s_source_roster_detail_count表 + 5. 初始化version=0 + +#### 5.1.4 获取医嘱列表 +- **URL**: `POST /api/v1/admin/GetMainList` +- **入参**: + ```json + { + "searchInfo": { + "dateRange": ["2024-01-01", "2024-12-31"], + "list_status": 0, // 状态筛选 + "patient_type": 0, // 患者类型 + "reg_num": "", // 登记号 + "user_name": "", // 患者姓名 + "resources": [], // 资源ID数组 + "services_group": 1 // 服务组 + }, + "page": 1, + "pageSize": 20 + } + ``` +- **出参**: 返回医嘱列表,含患者信息、预约信息、状态等 +- **逻辑**: + 1. 根据用户分组过滤数据(科室管理员/护士/医生) + 2. 查询s_list表 + 3. 关联s_period、s_department_resources表 + 4. 计算患者年龄、匹配设备信息 + 5. 分页返回 + +#### 5.1.5 获取可预约时段 +- **URL**: `POST /api/v1/admin/GetEnablePlan` +- **入参**: + ```json + { + "regnum": "登记号", + "entrustid": "医嘱ID", + "episodeid": "就诊ID", + "appointment_type": 4 // 预约渠道类型 + } + ``` +- **出参**: 返回可用时段列表,含号源剩余数量 +- **逻辑**: + 1. 查询医嘱对应的检查项目 + 2. 查找项目关联的科室、资源、设备 + 3. 查询可用号源 (s_source_roster_detail) + 4. 检查号源数量 (used_count < count) + 5. 检查项目互斥关系 + 6. 检查门诊缴费状态 + 7. 检查医嘱等待时间 (check_begin_time) + 8. 返回可用时段 + +#### 5.1.6 执行预约 +- **URL**: `POST /api/v1/admin/PlanYuYue` +- **入参**: + ```json + { + "regnum": "登记号", + "entrustid": ["医嘱ID1", "医嘱ID2"], + "roster_detail_id": 123, // 号源明细ID + "appointment_type": 4, // 预约渠道 + "do_user": 1, // 操作人ID + "do_type": 1 // 1预约/2改约 + } + ``` +- **出参**: 预约成功/失败信息 +- **逻辑**: + 1. 检查医嘱状态(必须是状态0或可改约状态1) + 2. 使用乐观锁更新号源数量: + - 查询当前version + - UPDATE时WHERE version=当前值 + - 递增used_count和version + - 如果UPDATE失败(version已变),重试或返回失败 + 3. 更新s_list表: + - list_status = 1 + - 预约时间、资源、服务组等 + 4. 记录s_list_log日志 + 5. 向PACS推送申请单 + 6. 返回预约结果 + +#### 5.1.7 取消预约 +- **URL**: `POST /api/v1/admin/CancelYuYue` +- **入参**: + ```json + { + "ids": [1, 2, 3], // 医嘱记录ID数组 + "cancel_type": "系统取消" // 取消类型 + } + ``` +- **出参**: 取消成功/失败信息 +- **逻辑**: + 1. 检查医嘱状态(必须是已预约状态1) + 2. 释放号源: + - 递减used_count + - 不递增version(释放不需要锁) + 3. 更新s_list表: + - list_status恢复原状态或保持0 + - 记录canel_time + - 记录cancel_type + 4. 记录s_list_log日志 + 5. 向PACS推送取消申请单 + 6. 返回取消结果 + +#### 5.1.8 报到接口 +- **URL**: `POST /api/v1/admin/SignIn` +- **入参**: + ```json + { + "id": 1 // 医嘱记录ID + } + ``` +- **出参**: 报到成功/失败信息 +- **逻辑**: + 1. 检查医嘱状态(必须是已预约状态1) + 2. 检查预约日期是否为当天或已过期 + 3. 更新s_list表: + - list_status = 2 + - 记录reg_date, reg_time + 4. 记录s_list_log日志 + 5. 返回报到结果 + +#### 5.1.9 HIS医嘱推送接口 +- **URL**: `POST /api/v1/T/CreateRecordXml` +- **入参**: XML格式的医嘱信息(HL7标准) +- **出参**: 接收确认消息 +- **逻辑**: + 1. 解析XML消息 + 2. 提取医嘱信息 + 3. 检查entrust_id是否已存在(防止重复) + 4. 写入s_list表(list_status=0) + 5. 返回确认消息 + +#### 5.1.10 PACS报到通知接口 +- **URL**: `POST /api/PacsSignIn` +- **入参**: + ```json + { + "reg_num": "登记号", + "entrust_id": "医嘱ID" + } + ``` +- **出参**: 报到确认 +- **逻辑**: + 1. 查询医嘱记录 + 2. 更新list_status=2 + 3. 记录报到时间 + 4. 记录日志 + +### 5.2 数据库表详细设计 + +#### 5.2.1 预约主表 (s_list) +```sql +CREATE TABLE `s_list` ( + `id` bigint(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `list_status` int(11) DEFAULT NULL COMMENT '0申请中/1预约/2登记/3结束', + `reg_num` varchar(255) COMMENT '登记号', + `user_name` varchar(255) COMMENT '患者姓名', + `user_sex` varchar(255) COMMENT '患者性别', + `user_age` varchar(255) COMMENT '患者年龄', + `user_brithday` varchar(255) COMMENT '生日', + `user_phone` varchar(255) COMMENT '电话', + `idCardNumber` varchar(100) COMMENT '身份证号', + + `entrust` varchar(255) COMMENT '医嘱名称', + `entrust_code` varchar(100) COMMENT '医嘱代码', + `entrust_id` varchar(255) COMMENT 'HIS医嘱ID(唯一)', + `entrust_date` varchar(255) COMMENT '医嘱日期', + `entrust_time` varchar(255) COMMENT '医嘱时间', + `app_num` varchar(255) COMMENT '申请单号', + + `reservation_department` varchar(255) COMMENT '申请科室', + `reservation_department_code` varchar(255) COMMENT '申请科室代码', + `RISRAcceptDeptCode` varchar(255) COMMENT '执行科室代码', + `department_id` int(11) COMMENT '预约科室ID', + + `reservation_date` date COMMENT '预约日期', + `reservation_time` varchar(255) COMMENT '预约时间段ID', + `reservation_sources` varchar(255) COMMENT '预约资源ID', + `roster_id` int(11) COMMENT '预约计划明细ID', + `services_group` varchar(100) COMMENT '服务组', + `appointment_type_id` int(11) COMMENT '占用号源的渠道ID', + `qudao_appointment_type_id` int(11) COMMENT '预约来源渠道ID', + + `patient_type` varchar(11) COMMENT '0住院/1门诊/2急诊/3体检', + `is_pay` varchar(255) COMMENT '是否缴费 1是/0否', + `his_is_emergency` tinyint(4) DEFAULT 0 COMMENT 'HIS加急标记', + `is_emergency` tinyint(4) DEFAULT 0 COMMENT '加急预约', + + `warddesc` varchar(255) COMMENT '病区', + `wardcode` varchar(50) COMMENT '病区编号', + `bedname` varchar(255) COMMENT '床号名称', + `bedno` varchar(50) COMMENT '床号', + `hospital_number` varchar(255) COMMENT '住院号', + + `reg_date` date COMMENT '登记日期', + `reg_time` time COMMENT '登记时间', + `canel_time` datetime COMMENT '取消预约时间', + `canel_type` varchar(255) COMMENT '取消类型', + `addtime` datetime COMMENT '预约时间', + + `check_num` varchar(255) COMMENT '检查号', + `RISRExamID` varchar(255) COMMENT '检查号', + `medicalHistory` varchar(1000) COMMENT '病史摘要', + `diagnosisName` varchar(255) COMMENT '临床诊断', + `check_aply_pdf` varchar(1000) COMMENT '申请单PDF地址', + + `is_del` int(4) DEFAULT 0 COMMENT '是否删除', + `is_nullify` int(4) DEFAULT 0 COMMENT '是否作废', + `entrust_status` varchar(2) COMMENT '医嘱状态', + `flag_tag` varchar(255) COMMENT 'HIS同步标记', + `flag_str` varchar(2550) COMMENT '取消原因', + + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + + PRIMARY KEY (`id`), + INDEX `list_status` (`list_status`), + INDEX `reg_num` (`reg_num`), + INDEX `entrust_id` (`entrust_id`), + INDEX `reservation_date` (`reservation_date`) +) ENGINE=InnoDB; +``` + +#### 5.2.2 预约计划明细渠道数量表 (s_source_roster_detail_count) +```sql +CREATE TABLE `s_source_roster_detail_count` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `roster_detail_id` int(10) COMMENT '计划明细ID', + `appointment_type_id` int(10) COMMENT '预约渠道ID', + `count` int(10) DEFAULT 0 COMMENT '可预约数量', + `used_count` int(10) DEFAULT 0 COMMENT '已预约数量', + `locked_count` int(10) DEFAULT 0 COMMENT '占位数量', + `max_total` int(10) DEFAULT 0 COMMENT '单日最大总量', + `version` int(11) DEFAULT 0 COMMENT '乐观锁版本号', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB COMMENT='号源数量表(含乐观锁)'; +``` + +#### 5.2.3 检查项目表 (s_check_item) +```sql +CREATE TABLE `s_check_item` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `item_code` varchar(255) COMMENT '检查项目代码', + `item_class_id` int(11) COMMENT '项目分类ID', + `sheetType` varchar(20) COMMENT 'HIS sheetType', + `item_name` varchar(255) COMMENT '检查项目名称', + `item_desc` varchar(255) COMMENT '描述', + `reservation_method` varchar(100) COMMENT '支持的预约渠道', + `limosis` int(11) COMMENT '是否空腹 1是/0否', + `check_notice` varchar(8000) COMMENT '检查须知', + `check_time` int(11) COMMENT '检查时长(分钟)', + `check_begin_time` int(11) COMMENT '医嘱后等待时间(分钟)', + `use_seats` int(10) DEFAULT 1 COMMENT '占位数量', + `services_group` int(255) COMMENT '关联服务组', + `status` int(4) DEFAULT 1 COMMENT '1启用/0停用', + `is_del` int(11) DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) +) ENGINE=InnoDB COMMENT='检查项目表'; +``` + +#### 5.2.4 检查项目互斥表 (s_huchi) +```sql +CREATE TABLE `s_huchi` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `code1` varchar(100) COMMENT '检查项目代码1', + `code2` varchar(100) COMMENT '检查项目代码2', + `time` int(11) COMMENT '互斥时间(小时),0永久互斥', + `add_user` int(11) COMMENT '添加人', + `is_del` int(4) COMMENT '是否删除', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB COMMENT='项目互斥表'; +``` + +### 5.3 页面结构设计 + +#### 5.3.1 管理后台页面结构 + +**预约计划模板页面 (PlanModel.vue)** +``` +页面布局: +┌─────────────────────────────────────────┐ +│ 操作按钮区 │ +│ [添加] [删除] [生成选中的计划] │ +├─────────────────────────────────────────┤ +│ 筛选区 │ +│ [工作日/节假日] [夏令时/冬令时] │ +├─────────────────────────────────────────┤ +│ 号源模板表格 │ +│ ┌─左侧资源列表──┬─右侧时段表格───────┐ │ +│ │ 资源1 │ 时段1 时段2 时段3 │ │ +│ │ 资源2 │ 周一 周二 周三... │ │ +│ │ ... │ [号源配置格子] │ │ +│ └─────────────┴────────────────────┘ │ +│ │ +│ 号源格子结构: │ +│ ┌─────────────┐ │ +│ │ [勾选框] │ │ +│ │ 总:X 住院:X │ │ +│ │ 门诊:X 急诊:X│ │ +│ │ 体检:X │ │ +│ │ [编辑] [占位]│ │ +│ └─────────────┘ │ +└─────────────────────────────────────────┘ + +弹窗: +- 添加/编辑模板弹窗 +- 占位数量设置弹窗 +``` + +**医嘱工作列表页面 (MainList.vue)** +``` +页面布局: +┌─────────────────────────────────────────┐ +│ 搜索区 │ +│ [日期范围] [状态] [患者类型] [资源] │ +│ [服务组] [登记号] [姓名] [科室] [医生] │ +│ [查询] │ +├─────────────────────────────────────────┤ +│ 操作按钮区 │ +│ [预约] [取消预约] [更改预约] │ +│ [报到] [取消报到] [打印申请单] │ +├─────────────────────────────────────────┤ +│ 医嘱列表表格 │ +│ ┌────┬────┬────┬────┬────┬────┬────┐ │ +│ │勾选│状态│登记号│姓名│性别│年龄│医嘱│ │ +│ ├────┼────┼────┼────┼────┼────┼────┤ │ +│ │ □ │申请│002.. │张三│男 │45 │CT │ │ +│ │ □ │预约│003.. │李四│女 │30 │MRI │ │ +│ └───────────────────────────────────┘ │ +│ │ +│ 表格列: │ +│ - 勾选框、状态、登记号、姓名、性别 │ +│ - 年龄、医嘱、是否缴费、预约日期 │ +│ - 预约时间、预约资源、服务组、申请科室 │ +│ - 医嘱时间、申请医生、病人类型、电话 │ +│ - 操作(查看日志) │ +└─────────────────────────────────────────┘ + +弹窗: +- 预约弹窗(选择时段) +- 日志查看弹窗 +- 申请单打印预览 +``` + +#### 5.3.2 自助机页面结构 + +**医嘱列表页面 (CheckItemMainList.vue)** +``` +页面布局(大屏触摸屏风格): +┌─────────────────────────────────────────┐ +│ 患者信息区 │ +│ 姓名:张XX 性别:女 电话:199... │ +│ 登记号:002000022 │ +├─────────────────────────────────────────┤ +│ 日期选择 │ +│ [日期范围选择器] │ +├─────────────────────────────────────────┤ +│ 医嘱列表(大卡片) │ +│ ┌───────────────────────────────────┐ │ +│ │ 检查项目:CT检查 [待预约] │ │ +│ │ 医嘱时间:2024-01-01 申请科室:内科 │ │ +│ │ [开始预约] │ │ +│ └───────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────┐ │ +│ │ 检查项目:MRI检查 [已预约] │ │ +│ │ 预约时段:2024-01-05 08:00~09:00 │ │ +│ │ [取消] [改约] │ │ +│ └───────────────────────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +**预约时段选择页面 (PlanList.vue)** +``` +页面布局: +┌─────────────────────────────────────────┐ +│ 顶部 │ +│ [返回] 选择预约时间 │ +├─────────────────────────────────────────┤ +│ 日期选择 │ +│ [2024-01-01] [2024-01-02] [2024-01-03] │ +├─────────────────────────────────────────┤ +│ 时段列表(大按钮) │ +│ ┌───────┬───────┬───────┬───────┐ │ +│ │08:00 │09:00 │10:00 │11:00 │ │ +│ │~09:00 │~10:00 │~11:00 │~12:00 │ │ +│ │[剩余5]│[剩余3]│[剩余0]│[剩余2]│ │ +│ └───────┴───────┴───────┴───────┘ │ +│ │ +│ 点击时段显示详情: │ +│ - 检查项目: XXX │ +│ - 预约时段: XXX │ +│ - 检查须知: XXX │ +│ - [确认预约] [取消] │ +└─────────────────────────────────────────┘ +``` + +#### 5.3.3 H5移动端页面结构 + +**登录页面 (Login.vue)** +``` +页面布局(移动端适配): +┌─────────────────────────────────────────┐ +│ │ +│ 患者登录 │ +│ │ +│ [请输入患者ID] │ +│ │ +│ [登录按钮] │ +│ │ +└─────────────────────────────────────────┘ +``` + +**医嘱列表页面** +``` +页面布局(uni-app移动端): +┌─────────────────────────────────────────┐ +│ 标题: 我的检查项目 │ +├─────────────────────────────────────────┤ +│ 医嘱卡片列表 │ +│ ┌───────────────────────────────────┐ │ +│ │ 检查项目:CT检查 │ │ +│ │ 状态:待预约 │ │ +│ │ 申请时间:2024-01-01 │ │ +│ │ [预约] │ │ +│ └───────────────────────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +--- + +## 6. 部署与运维 + +### 6.1 系统部署架构 + +``` +生产环境部署建议: +┌─────────────────────────────────────────┐ +│ Web服务器 (Nginx/Apache) │ +│ ├─ 管理后台域名: admin.xxx.com │ +│ ├─ 自助机域名:自助机.xxx.com │ +│ └─ H5域名: h5.xxx.com │ +└─────────────────────────────────────────┘ + ↓ 反向代理 +┌─────────────────────────────────────────┐ +│ Laravel后端服务 │ +│ - PHP-FPM运行 │ +│ - 端口: 9000 │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ MySQL数据库 │ +│ - 主从复制建议 │ +│ - 定期备份 │ +└─────────────────────────────────────────┘ + +┌─────────────────────────────────────────┐ +│ Workerman WebSocket服务 │ +│ - 端口: 2346 │ +│ - 实时消息推送 │ +└─────────────────────────────────────────┘ + +┌─────────────────────────────────────────┐ +│ C# HIS监听服务 (Windows服务器) │ +│ - 安装为Windows Service │ +│ - 监听MQ消息队列 │ +│ - 日志记录: log4net │ +└─────────────────────────────────────────┘ +``` + +### 6.2 配置管理 + +#### 6.2.1 Laravel配置 (.env) +```env +APP_NAME=秦皇岛中医院医技预约系统 +APP_ENV=production +APP_URL=https://admin.xxx.com + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=yiji_qhdzhongyiyuan +DB_USERNAME=root +DB_PASSWORD=xxx + +# HIS对接配置 +HIS_MQ_HOST=192.168.1.100 +HIS_MQ_PORT=1414 +HIS_MQ_CHANNEL=CHAN1 +HIS_MQ_QUEUE=QUEUE1 + +# PACS对接配置 +PACS_API_URL=http://192.168.1.101/api +PACS_API_KEY=xxx + +# 系统配置 +PAY_TIMEOUT=30 # 门诊缴费超时(分钟) +APPOINTMENT_TIMEOUT=60 # 预约超时(分钟) +``` + +#### 6.2.2 前端配置 +```javascript +// YiJi-admin/src/config.js +export default { + API_BASE_URL: 'https://admin.xxx.com/api/v1', + WS_URL: 'ws://192.168.1.10:2346', + TOKEN_KEY: 'access_token', + REFRESH_TOKEN_KEY: 'refresh_token' +} +``` + +#### 6.2.3 C#监听服务配置 (App.config) +```xml + + + + + + + +``` + +### 6.3 运维监控 + +#### 6.3.1 日志管理 +- Laravel日志: `storage/logs/laravel.log` +- C#监听日志: log4net配置输出到文件 +- API访问日志: 中间件`log`记录到数据库 + +#### 6.3.2 性能监控 +- 数据库慢查询监控 +- API响应时间监控 +- 号源并发冲突监控(version失败率) + +#### 6.3.3 异常告警 +- HIS消息接收异常告警 +- 号源生成失败告警 +- 预约并发冲突告警 + +--- + +## 7. 附录 + +### 7.1 HIS消息类型对照表 + +| 消息类型 | 消息说明 | 主要字段 | 处理逻辑 | +|---------|---------|---------|---------| +| MI0150 | 医嘱信息 | 患者信息、医嘱信息、科室信息 | 新增s_list记录 | +| MI0164 | 医嘱状态变更 | 医嘱ID、状态 | 更新医嘱状态 | +| MU0165 | 医嘱更新 | 医嘱信息 | 更新s_list记录 | +| CI0047 | 检查信息 | 检查详情 | 更新检查信息 | +| CI0054 | 检查相关 | 检查状态 | 更新检查状态 | +| CI0055 | 检查相关 | 检查结果 | 更新检查结果 | +| CD0050 | 检查确认 | 确认信息 | 更新确认状态 | +| OI0083 | 医嘱订单 | 订单信息 | 处理订单 | +| OD0084 | 订单更新 | 订单状态 | 更新订单状态 | +| MU0151 | 其他更新 | 各类更新 | 按类型处理 | + +### 7.2 预约渠道编码表 + +| 渠道ID | 渠道名称 | 简称 | 说明 | +|-------|---------|-----|------| +| 1 | 住院 | 住院 | 住院患者预约通道 | +| 2 | 门诊 | 门诊 | 门诊患者预约通道 | +| 3 | 急诊 | 急诊 | 急诊患者预约通道 | +| 4 | 体检 | 体检 | 体检患者预约通道 | +| 5 | 护士站 | 护士 | 护士站预约通道 | +| 6 | 医生 | 医生 | 医生端预约通道 | + +### 7.3 系统菜单结构示例 + +``` +系统管理 +├─ 用户管理 +├─ 分组管理 +├─ 菜单管理 +└─ 系统配置 + +基础数据 +├─ 科室管理 +├─ 科室资源管理 +├─ 设备管理 +├─ 检查项目管理 +├─ 项目分类管理 +├─ 项目互斥配置 +├─ 时间段管理 +├─ 病区管理 +└─ HIS数据同步 + +预约管理 +├─ 预约计划模板 +├─ 渠道比例配置 +├─ 预约计划明细 +├─ 节假日配置 +└─ 体检日历管理 + +业务处理 +├─ 医嘱工作列表 +├─ 医生预约入口 +├─ 预约统计 +├─ 渠道统计 +└─ 开单统计 +``` + +### 7.4 关键业务规则 + +#### 7.4.1 预约规则 +1. 门诊患者必须缴费后才能预约(可配置开关) +2. 急诊患者优先预约,可使用加急通道 +3. 检查项目互斥校验(如增强CT和普通CT不能同一天) +4. 医嘱开立后需等待check_begin_time才能预约 +5. 预约必须在end_reservation_time之前完成 + +#### 7.4.2 号源分配规则 +1. 各渠道号源按比例分配(s_appointment_type_ratio) +2. 可合并渠道号源(link字段配置) +3. 占位号源优先预留(locked_count) +4. 号源不足时可跨渠道借号源(需配置) + +#### 7.4.3 取消预约规则 +1. 门诊患者缴费超时自动取消(可配置超时时间) +2. 取消后释放号源(递减used_count) +3. 记录取消原因和时间 +4. 向PACS推送取消通知 + +#### 7.4.4 报到规则 +1. 必须在预约日期当天报到 +2. 过期预约不可报到,需重新预约 +3. 报到后状态变为"已登记" +4. PACS报到同步(接收PacsSignIn接口) + +--- + +## 8. 总结 + +本需求文档详细描述了秦皇岛中医院医技预约系统的业务需求、技术架构和实现细节。系统核心功能包括: + +1. **HIS集成**: 通过MQ消息队列接收医嘱信息,自动同步基础数据 +2. **多渠道预约**: 支持管理后台、自助机、H5移动端等多种预约方式 +3. **号源管理**: 灵活的号源模板和渠道比例配置,支持乐观锁防止并发冲突 +4. **业务流程**: 完整的医嘱接收→预约→报到→检查完成流程 +5. **统计分析**: 多维度预约统计和运营监控 + +系统采用前后端分离架构,后端基于Laravel 8提供RESTful API,前端使用Vue 3和uni-app构建多端应用,通过C#监听服务实现HIS集成,整体架构清晰、扩展性强。 + +--- + +**文档版本**: V1.0 +**编写日期**: 2026-04-03 +**编写人**: Claude AI +**审核人**: 待定 +**批准人**: 待定 \ No newline at end of file diff --git a/项目需求规范示例.md b/项目需求规范示例.md new file mode 100644 index 0000000..986f129 --- /dev/null +++ b/项目需求规范示例.md @@ -0,0 +1,34 @@ +# 项目需求与设计文档 + +## 1. 原始需求 +> 客户/业务原始描述(大白话) +- xxx +- xxx + +## 2. 设计需求(功能需求) +- 功能1:xxx +- 功能2:xxx +- 角色:管理员 / 用户 / 游客 +- 业务流程:xxx → xxx → xxx + +## 3. 设计规格(技术规范) +- 技术栈:前端xxx / 后端xxx +- 接口规范:xxx +- 数据库规范:xxx +- 代码规范:xxx +- 安全与权限:xxx + +## 4. 概要设计(整体架构) +- 系统架构 +- 模块划分 +- 数据流程 +- 核心表/类设计思路 + +## 5. 详细设计(实现细节) +### 5.1 模块A +- 接口1:URL、入参、出参、逻辑 +- 接口2:xxx +### 5.2 数据库表设计 +- user 表:id, username, password... +### 5.3 页面结构 +- 首页布局、表单结构、弹窗逻辑 \ No newline at end of file