优化预约、取消、改约

main
岩仔88 1 month ago
parent 820782d3f6
commit 7d8fd2bbfe

@ -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生成**: 需安装wkhtmltopdflaravel-snappy依赖
4. **条形码生成**: 使用 `picqer/php-barcode-generator`
## 相关文档
- `项目需求规范示例.md` - 需求与设计文档模板参考
- 数据库详细结构见 `data.sql`

@ -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()

@ -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)
{

@ -0,0 +1,294 @@
<?php
namespace Tests\Feature;
use App\Services\Admin\YeWu\PlanListService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class PlanListServiceTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->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";
}
}

@ -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,

@ -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": {

File diff suppressed because it is too large Load Diff

@ -0,0 +1,34 @@
# 项目需求与设计文档
## 1. 原始需求
> 客户/业务原始描述(大白话)
- xxx
- xxx
## 2. 设计需求(功能需求)
- 功能1xxx
- 功能2xxx
- 角色:管理员 / 用户 / 游客
- 业务流程xxx → xxx → xxx
## 3. 设计规格(技术规范)
- 技术栈前端xxx / 后端xxx
- 接口规范xxx
- 数据库规范xxx
- 代码规范xxx
- 安全与权限xxx
## 4. 概要设计(整体架构)
- 系统架构
- 模块划分
- 数据流程
- 核心表/类设计思路
## 5. 详细设计(实现细节)
### 5.1 模块A
- 接口1URL、入参、出参、逻辑
- 接口2xxx
### 5.2 数据库表设计
- user 表id, username, password...
### 5.3 页面结构
- 首页布局、表单结构、弹窗逻辑
Loading…
Cancel
Save