This commit is contained in:
Your Name
2026-04-21 18:02:27 +08:00
parent a1d2ab4649
commit d899e0fe0a
301 changed files with 1937 additions and 344 deletions
@@ -45,6 +45,25 @@ class CustomerController extends BaseAdminController
return $this->data($stats);
}
/**
* @notes 今日进入分布(用于页面"今日新增"卡片的迷你柱/最近进入时间/渠道 Top5)
*/
public function todayArrival()
{
return $this->data(CustomerLogic::getTodayArrivalStats());
}
/**
* @notes 今日进入明细流水(每一次 add_external_contact 推送 = 一行,按时间倒序分页)
*/
public function todayArrivalList()
{
$pageNo = (int) $this->request->get('page_no', 1);
$pageSize = (int) $this->request->get('page_size', 20);
return $this->data(CustomerLogic::getTodayArrivalList($pageNo, $pageSize));
}
/**
* @notes 获取同步设置
*/
@@ -160,7 +160,7 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
->whereNull('delete_time')
->where('void_status', 0)
->order('id', 'desc')
->field(['id', 'appointment_id', 'audit_status', 'void_status'])
->field(['id', 'appointment_id', 'audit_status', 'void_status', 'is_system_auto'])
->select()
->toArray();
foreach ($rxRows as $rx) {
@@ -193,6 +193,7 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
$apptRx = $rxByAppointmentId[$apptId] ?? null;
$item['prescription_audit_status'] = $apptRx !== null ? (int) ($apptRx['audit_status'] ?? -1) : -1;
$item['prescription_void_status'] = $apptRx !== null ? (int) ($apptRx['void_status'] ?? 0) : 0;
$item['prescription_is_system_auto'] = $apptRx !== null ? (int) ($apptRx['is_system_auto'] ?? 0) : 0;
// 格式化时间戳为日期时间
if (isset($item['create_time']) && is_numeric($item['create_time'])) {
+23 -2
View File
@@ -86,7 +86,18 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
->field('id');
}];
}
// 按诊单医助筛选(订单 patient_id 存诊单 id
$assistantId = (int) ($this->params['assistant_id'] ?? 0);
if ($assistantId > 0) {
$where[] = ['patient_id', 'in', function ($query) use ($assistantId) {
$query->table('zyt_tcm_diagnosis')
->whereNull('delete_time')
->where('assistant_id', '=', $assistantId)
->field('id');
}];
}
// 处理创建时间范围
if (!empty($this->params['create_time_start'])) {
$where[] = ['create_time', '>=', $this->params['create_time_start'] . ' 00:00:00'];
@@ -132,7 +143,17 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
->field('id');
}];
}
$assistantIdCount = (int) ($this->params['assistant_id'] ?? 0);
if ($assistantIdCount > 0) {
$where[] = ['patient_id', 'in', function ($query) use ($assistantIdCount) {
$query->table('zyt_tcm_diagnosis')
->whereNull('delete_time')
->where('assistant_id', '=', $assistantIdCount)
->field('id');
}];
}
// 处理创建时间范围
if (!empty($this->params['create_time_start'])) {
$where[] = ['create_time', '>=', $this->params['create_time_start'] . ' 00:00:00'];
@@ -42,8 +42,12 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
*/
public function lists(): array
{
// 按首次添加时间倒序:库字段 external_first_add_time;未回填(0)时回退 create_time(与列表「添加时间」展示一致)
$lists = $this->baseQuery()
->order('id', 'desc')
->orderRaw(
'(COALESCE(NULLIF(external_first_add_time, 0), create_time) = 0) ASC, '
. 'COALESCE(NULLIF(external_first_add_time, 0), create_time) DESC, id DESC'
)
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
@@ -115,6 +115,13 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
}
}
// 至少有一条挂号为「已完成」(status=3),用于列表顶部「已完成」Tab
if (isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1') {
$aptTbl = (new Appointment())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$query->whereExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status = 3");
}
// 仅已开方(有处方)的诊单:随访列表传 only_has_prescription=1;问诊等页面不传则不过滤
if (isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
$rxTbl = (new Prescription())->getTable();
@@ -474,6 +481,13 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereNotExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status IN (1,3,4)");
}
}
// 至少有一条挂号为「已完成」(status=3)
if (isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1') {
$aptTbl = (new Appointment())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$query->whereExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status = 3");
}
return $query->count();
}
@@ -177,6 +177,7 @@ class PrescriptionLists extends BaseAdminDataLists implements ListsSearchInterfa
$item['usage_notes'] = $item['usage_notes'] ?? '';
$item['usage_days'] = $item['usage_days'] ?? 7;
$item['is_shared'] = $item['is_shared'] ?? 0;
$item['is_system_auto'] = (int) ($item['is_system_auto'] ?? 0);
$item['audit_status'] = (int) ($item['audit_status'] ?? 1);
$item['void_status'] = (int) ($item['void_status'] ?? 0);
$item['visible_role_ids'] = PrescriptionLogic::visibleRoleIdsToArray((string) ($item['visible_role_ids'] ?? ''));
@@ -10,6 +10,7 @@ use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\model\Order;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderPayOrder;
use app\common\service\gancao\GancaoScmRecipelService;
@@ -27,6 +28,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
public function lists(): array
{
$query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time');
$this->applyDoctorAssistantFilters($query);
$this->applyPatientKeywordFilter($query);
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
$diagIds = Diagnosis::where('assistant_id', $this->adminId)->whereNull('delete_time')->column('id');
$query->where(function ($q) use ($diagIds) {
@@ -165,6 +168,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
public function count(): int
{
$query = PrescriptionOrder::where($this->searchWhere)->whereNull('delete_time');
$this->applyDoctorAssistantFilters($query);
$this->applyPatientKeywordFilter($query);
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
$diagIds = Diagnosis::where('assistant_id', $this->adminId)->whereNull('delete_time')->column('id');
$query->where(function ($q) use ($diagIds) {
@@ -177,4 +182,46 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return (int) $query->count();
}
/**
* 按开方医生(关联处方 creator_id)/ 诊单医助(关联诊单 assistant_id)筛选
*/
private function applyDoctorAssistantFilters($query): void
{
$poTbl = (new PrescriptionOrder())->getTable();
if (isset($this->params['doctor_id']) && (int) $this->params['doctor_id'] > 0) {
$doctorId = (int) $this->params['doctor_id'];
$rxTbl = (new Prescription())->getTable();
$query->whereExists("SELECT 1 FROM {$rxTbl} rx WHERE rx.id = {$poTbl}.prescription_id AND rx.delete_time IS NULL AND rx.creator_id = {$doctorId}");
}
if (isset($this->params['assistant_id']) && (int) $this->params['assistant_id'] > 0) {
$assistantId = (int) $this->params['assistant_id'];
$diagTbl = (new Diagnosis())->getTable();
$query->whereExists("SELECT 1 FROM {$diagTbl} dg WHERE dg.id = {$poTbl}.diagnosis_id AND dg.delete_time IS NULL AND dg.assistant_id = {$assistantId}");
}
}
/**
* 按诊单患者姓名 / 手机号模糊检索(关联 diagnosis_id
*/
private function applyPatientKeywordFilter($query): void
{
$kw = trim((string) ($this->params['patient_keyword'] ?? ''));
if ($kw === '') {
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$likeInner = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $kw);
$like = '%' . $likeInner . '%';
$likeSql = str_replace("'", "''", $like);
$query->whereExists(
"SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL "
. "AND (dg.`patient_name` LIKE '{$likeSql}' OR dg.`phone` LIKE '{$likeSql}')"
);
}
}
@@ -9,7 +9,9 @@ use app\common\service\doctor\RosterSegmentService;
use app\common\model\tcm\Diagnosis;
use app\common\model\auth\Admin;
use app\api\logic\ChatNotifyLogic;
use app\adminapi\logic\tcm\PrescriptionLogic;
use think\facade\Db;
use think\facade\Log;
/**
* 医生预约逻辑
@@ -429,6 +431,13 @@ class AppointmentLogic extends BaseLogic
\think\facade\Log::warning('面诊结束通知医助失败: ' . $e->getMessage());
}
// 完成就诊后系统代开处方(无药材、标记 is_system_auto;失败不影响「完成」成功)
try {
PrescriptionLogic::createSystemAutoFromCompletedAppointment((int) $appointment->id);
} catch (\Throwable $e) {
Log::warning('完成挂号后系统代开处方失败: ' . $e->getMessage());
}
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
@@ -652,6 +652,73 @@ class CustomerLogic extends BaseLogic
);
}
/**
* 事件流水(零误差进入计数)
*
* 每次 change_external_contact 回调都调用一次,`INSERT IGNORE` 幂等:
* - 企微对非 2xx 响应最多重试 3 次,同一 (change_type,user_id,external_userid,event_time) 只会计一行;
* - 与业务 UPSERT 解耦,即便后续 DB 逻辑抛错也不影响"今日进入数"统计。
*
* 入参 $data
* - change_type string
* - user_id string
* - external_userid string
* - state string
* - fail_reason string
* - welcome_code int 0/1(只存是否带欢迎码,不落原 code,避免泄漏)
* - event_time int 企微 CreateTime(秒)
* - raw mixed 原始消息(用于留痕;非标量会 json_encode
*
* @param array<string, mixed> $data
*/
public static function recordExternalContactEvent(array $data): void
{
$changeType = (string) ($data['change_type'] ?? '');
if ($changeType === '') {
// 没有 ChangeType 的事件流水没有价值,直接丢弃
return;
}
$eventTime = (int) ($data['event_time'] ?? 0);
if ($eventTime <= 0) {
$eventTime = time();
}
$raw = $data['raw'] ?? null;
if ($raw !== null && !is_string($raw)) {
try {
$raw = json_encode($raw, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR);
} catch (\Throwable) {
$raw = null;
}
}
$row = [
'change_type' => $changeType,
'user_id' => (string) ($data['user_id'] ?? ''),
'external_userid' => (string) ($data['external_userid'] ?? ''),
'state' => mb_substr((string) ($data['state'] ?? ''), 0, 128),
'fail_reason' => mb_substr((string) ($data['fail_reason'] ?? ''), 0, 64),
'welcome_code' => !empty($data['welcome_code']) ? 1 : 0,
'event_time' => $eventTime,
'create_time' => time(),
'raw' => $raw,
];
try {
// INSERT IGNORE:命中唯一键 (change_type,user_id,external_userid,event_time) 时静默跳过。
// ThinkPHP 未提供统一的 ignore helper,这里直接用原生 SQL(与 SyncImChatArchive 的做法一致)。
$cols = array_keys($row);
$table = config('database.connections.mysql.prefix') . 'qywx_external_contact_event';
$sql = 'INSERT IGNORE INTO `' . $table . '` (`' . implode('`,`', $cols) . '`) VALUES ('
. implode(',', array_fill(0, count($cols), '?')) . ')';
Db::execute($sql, array_values($row));
} catch (\Throwable $e) {
// 事件流水只用于统计,失败只记日志不阻塞主回调
Log::warning('qywx external contact event insert failed: ' . $e->getMessage());
}
}
/**
* 客户联系「删除企业客户」等事件:本地软删除一行。
*/
@@ -669,6 +736,74 @@ class CustomerLogic extends BaseLogic
]);
}
/**
* `del_follow_user` 事件:某员工不再跟进该客户。
* 只更新本地 `follow_users` JSON:移除匹配的 userid;若已无跟进人则软删该行。
* 不再回调 /externalcontact/get,避免 84061「not external contact」刷 warning。
*/
public static function removeFollowUserFromLocal(string $externalUserId, string $userId): void
{
$externalUserId = trim($externalUserId);
$userId = trim($userId);
if ($externalUserId === '' || $userId === '') {
return;
}
$row = Db::name('qywx_external_contact')
->where('external_userid', $externalUserId)
->find();
if (!$row) {
return;
}
$followUsers = json_decode((string) ($row['follow_users'] ?? '[]'), true);
$followUsers = is_array($followUsers) ? $followUsers : [];
$kept = [];
$removed = false;
foreach ($followUsers as $fu) {
if (!is_array($fu)) {
continue;
}
$uid = trim((string) ($fu['userid'] ?? ''));
if ($uid === $userId) {
$removed = true;
continue;
}
$kept[] = $fu;
}
if (!$removed) {
return;
}
$now = time();
if ($kept === []) {
Db::name('qywx_external_contact')
->where('id', (int) $row['id'])
->update([
'follow_users' => json_encode([], JSON_UNESCAPED_UNICODE),
'delete_time' => $now,
'update_time' => $now,
]);
return;
}
$minCreate = self::minFollowCreatetime($kept);
$update = [
'follow_users' => json_encode($kept, JSON_UNESCAPED_UNICODE),
'update_time' => $now,
];
if ($minCreate > 0) {
// 移除最早那条跟进人后,首次添加时间可能后移;重新刷新字段以与 JSON 保持一致
$update['external_first_add_time'] = $minCreate;
}
Db::name('qywx_external_contact')
->where('id', (int) $row['id'])
->update($update);
}
private static function upsertOneExternalContactBundle(
array $externalContact,
array $followUsers,
@@ -821,13 +956,39 @@ class CustomerLogic extends BaseLogic
$total = QywxExternalContact::whereNull('delete_time')->count();
$todayStart = strtotime(date('Y-m-d 00:00:00'));
// 今日新增:以企微首次添加时间为准;历史行未回填字段(0)时退回 create_time
$today = QywxExternalContact::whereNull('delete_time')
// 今日进入数 = 今日收到的 add_external_contact 事件条数。
// - 事件表 INSERT IGNORE 幂等,企微自动重试不会重复计数;
// - 与业务表 UPSERT/软删解耦,客户即使很快被删、或 /externalcontact/get 同步失败,都不影响计数;
// - 取 event_time(企微 CreateTime)而不是 create_time,跨日分钟内推送过来也归到事件真实日期。
$today = (int) Db::name('qywx_external_contact_event')
->where('change_type', 'add_external_contact')
->where('event_time', '>=', $todayStart)
->count();
/** 今日新增客户中,跟进人条数合计(同一 userid 多客户或多条均累计,不去重) */
$todayFollowStaff = 0;
$followJsonList = QywxExternalContact::whereNull('delete_time')
->whereRaw(
'(external_first_add_time >= ? OR (external_first_add_time = 0 AND create_time >= ?))',
[$todayStart, $todayStart]
)
->count();
->column('follow_users');
foreach ($followJsonList as $json) {
$raw = json_decode((string) $json, true);
if (!is_array($raw)) {
continue;
}
foreach ($raw as $fu) {
if (!is_array($fu)) {
continue;
}
$wx = trim((string) ($fu['userid'] ?? ''));
if ($wx !== '') {
$todayFollowStaff++;
}
}
}
$settings = self::getSyncSettings();
$lastSyncTime = $settings['last_sync_time'] ?? 0;
@@ -836,12 +997,167 @@ class CustomerLogic extends BaseLogic
return [
'total' => $total,
'today' => $today,
'today_follow_staff' => $todayFollowStaff,
'lastSync' => $lastSync,
'syncStatus' => $settings['sync_status'] ?? 'idle',
'syncStatusText' => self::getSyncStatusText($settings['sync_status'] ?? 'idle'),
];
}
/**
* 今日"进入"分布概览(直观卡片用)
*
* 数据源:qywx_external_contact_eventadd_external_contact)——回调一到就幂等写入,
* 与 qywx_external_contact 的软删/UPSERT 解耦,保证"进入数"零误差。
*
* 返回:
* - total int 今日进入总数(= stats.today
* - recent_time int 今日最近一条 add 事件时间戳(0 表示今天还没进人)
* - hourly int[24] 按 0-23 点分布(本地时区)
* - by_state array 按 state 分组 Top 5[{state, count}]
*
* @return array<string, mixed>
*/
public static function getTodayArrivalStats(): array
{
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = $todayStart + 86400;
$total = (int) Db::name('qywx_external_contact_event')
->where('change_type', 'add_external_contact')
->where('event_time', '>=', $todayStart)
->where('event_time', '<', $todayEnd)
->count();
$recent = (int) Db::name('qywx_external_contact_event')
->where('change_type', 'add_external_contact')
->where('event_time', '>=', $todayStart)
->where('event_time', '<', $todayEnd)
->max('event_time');
$hourly = array_fill(0, 24, 0);
if ($total > 0) {
// 用 FROM_UNIXTIME 按本地时区分桶;php.ini / mysql time_zone 有差异时,可改 DATE_FORMAT
$rows = Db::name('qywx_external_contact_event')
->fieldRaw('HOUR(FROM_UNIXTIME(event_time)) AS h, COUNT(*) AS c')
->where('change_type', 'add_external_contact')
->where('event_time', '>=', $todayStart)
->where('event_time', '<', $todayEnd)
->group('h')
->select()
->toArray();
foreach ($rows as $r) {
$h = (int) ($r['h'] ?? 0);
if ($h >= 0 && $h <= 23) {
$hourly[$h] = (int) ($r['c'] ?? 0);
}
}
}
$byState = [];
if ($total > 0) {
$stateRows = Db::name('qywx_external_contact_event')
->field(['state', 'COUNT(*) AS c'])
->where('change_type', 'add_external_contact')
->where('event_time', '>=', $todayStart)
->where('event_time', '<', $todayEnd)
->group('state')
->orderRaw('c DESC')
->limit(5)
->select()
->toArray();
foreach ($stateRows as $r) {
$byState[] = [
'state' => (string) ($r['state'] ?? ''),
'count' => (int) ($r['c'] ?? 0),
];
}
}
return [
'total' => $total,
'recent_time' => $recent,
'hourly' => array_values($hourly),
'by_state' => $byState,
];
}
/**
* 今日"进入明细"流水(分页,按时间倒序)
*
* 左联客户表补客户名/头像,左联 admin 补员工名;客户已删 / 同步未回不影响流水本身。
*
* @return array{total:int, page_no:int, page_size:int, lists:array<int,array<string,mixed>>}
*/
public static function getTodayArrivalList(int $pageNo, int $pageSize): array
{
$pageNo = max(1, $pageNo);
$pageSize = min(100, max(1, $pageSize));
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$todayEnd = $todayStart + 86400;
$base = Db::name('qywx_external_contact_event')
->where('change_type', 'add_external_contact')
->where('event_time', '>=', $todayStart)
->where('event_time', '<', $todayEnd);
$total = (int) (clone $base)->count();
$rows = (clone $base)
->orderRaw('event_time DESC, id DESC')
->page($pageNo, $pageSize)
->select()
->toArray();
if ($rows === []) {
return ['total' => $total, 'page_no' => $pageNo, 'page_size' => $pageSize, 'lists' => []];
}
$extIds = array_values(array_unique(array_filter(array_map(static fn($r) => (string) ($r['external_userid'] ?? ''), $rows))));
$userIds = array_values(array_unique(array_filter(array_map(static fn($r) => (string) ($r['user_id'] ?? ''), $rows))));
$customerMap = [];
if ($extIds !== []) {
$customerRows = Db::name('qywx_external_contact')
->field(['external_userid', 'name', 'avatar'])
->whereIn('external_userid', $extIds)
->select()
->toArray();
foreach ($customerRows as $c) {
$customerMap[(string) $c['external_userid']] = $c;
}
}
$adminMap = [];
if ($userIds !== []) {
$adminMap = Admin::whereIn('work_wechat_userid', $userIds)->column('name', 'work_wechat_userid');
}
$lists = [];
foreach ($rows as $r) {
$ext = (string) ($r['external_userid'] ?? '');
$uid = (string) ($r['user_id'] ?? '');
$lists[] = [
'id' => (int) $r['id'],
'event_time' => (int) $r['event_time'],
'user_id' => $uid,
'admin_name' => (string) ($adminMap[$uid] ?? ''),
'external_userid' => $ext,
'customer_name' => (string) ($customerMap[$ext]['name'] ?? ''),
'customer_avatar' => (string) ($customerMap[$ext]['avatar'] ?? ''),
'state' => (string) ($r['state'] ?? ''),
'welcome_code' => (int) ($r['welcome_code'] ?? 0),
];
}
return [
'total' => $total,
'page_no' => $pageNo,
'page_size' => $pageSize,
'lists' => $lists,
];
}
/**
* @notes 获取同步设置
*/
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\common\model\auth\Admin;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\Diagnosis;
@@ -463,6 +464,13 @@ class PrescriptionLogic
$arr['business_prescription_audit_rejected'] = $bizPo ? 1 : 0;
$arr['business_prescription_audit_remark'] = $bizPo ? (string) ($bizPo->prescription_audit_remark ?? '') : '';
// 与 PrescriptionLists「业务订单」角标一致:未删除且非已取消(4) 即视为存在有效业务订单
$hasBizOrder = PrescriptionOrder::where('prescription_id', $id)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->count() > 0;
$arr['has_prescription_order'] = $hasBizOrder ? 1 : 0;
return $arr;
}
@@ -701,6 +709,133 @@ class PrescriptionLogic
return $row->toArray();
}
/**
* 挂号标记「完成」后自动生成处方:无药材配方,is_system_auto=1,其余字段与常规开方一致(患者/诊单来自预约关联诊单)。
* 若该预约已有未作废处方、或同日同诊单同医师已存在未作废处方,则跳过(不抛错)。
*
* @return int|null 新建处方 id;跳过返回 null
*/
public static function createSystemAutoFromCompletedAppointment(int $appointmentId): ?int
{
if ($appointmentId <= 0) {
return null;
}
$appointment = Appointment::find($appointmentId);
if (!$appointment) {
return null;
}
$diagnosisId = (int) ($appointment->patient_id ?? 0);
if ($diagnosisId <= 0) {
Log::info('createSystemAutoFromCompletedAppointment: no diagnosis (patient_id), skip appt=' . $appointmentId);
return null;
}
$doctorId = (int) ($appointment->doctor_id ?? 0);
if ($doctorId <= 0) {
return null;
}
$dupAppt = Prescription::where('appointment_id', $appointmentId)
->where('void_status', 0)
->whereNull('delete_time')
->find();
if ($dupAppt) {
return null;
}
$diagnosis = Diagnosis::find($diagnosisId);
if (!$diagnosis) {
self::setError('诊单不存在');
return null;
}
$apptDateRaw = $appointment->appointment_date ?? '';
$dateYmd = is_string($apptDateRaw) && preg_match('/^\d{4}-\d{2}-\d{2}/', trim($apptDateRaw))
? substr(trim($apptDateRaw), 0, 10)
: self::normalizePrescriptionDate(date('Y-m-d'));
if (!self::assertUniquePrescriptionPerDiagnosisDay($diagnosisId, $doctorId, $dateYmd, null)) {
Log::info("createSystemAutoFromCompletedAppointment: unique rule skip diagnosis={$diagnosisId} doctor={$doctorId} date={$dateYmd}");
return null;
}
$sn = self::generateSn();
while (Prescription::where('sn', $sn)->find()) {
$sn = self::generateSn();
}
$clinical = trim((string) ($diagnosis->symptoms ?? ''));
if ($clinical === '') {
$clinical = '就诊完成系统生成(暂无药材配方,请医师补充诊断与药材)';
}
$doctorName = (string) (Admin::where('id', $doctorId)->value('name') ?? '');
$assistantIdForRx = (int) ($diagnosis->assistant_id ?? 0);
$visitNo = '1K' . str_pad((string) $diagnosisId, 8, '0', STR_PAD_LEFT);
$herbs = [];
$data = [
'sn' => $sn,
'prescription_name' => '系统代开',
'prescription_type' => '浓缩水丸',
'dosage_amount' => 1.0,
'dosage_unit' => 'g',
'need_decoction' => 0,
'bags_per_dose' => 1,
'diagnosis_id' => $diagnosisId,
'appointment_id' => $appointmentId,
'patient_id' => 0,
'patient_name' => (string) ($diagnosis->patient_name ?? ''),
'gender' => (int) ($diagnosis->gender ?? 0),
'age' => (int) ($diagnosis->age ?? 0),
'phone' => (string) ($diagnosis->phone ?? ''),
'visit_no' => $visitNo,
'prescription_date' => $dateYmd,
'pulse' => (string) ($diagnosis->pulse ?? ''),
'pulse_condition' => '',
'tongue' => (string) ($diagnosis->tongue ?? ''),
'tongue_image' => '',
'clinical_diagnosis' => $clinical,
'case_record' => [],
'herbs' => $herbs,
'dose_count' => 1,
'dose_unit' => '剂',
'usage_days' => 7,
'times_per_day' => 2,
'usage_instruction' => '水煎服,一日二次',
'usage_time' => '饭前',
'usage_way' => '温水送服',
'dietary_taboo' => '',
'usage_notes' => '',
'amount' => 0.0,
'doctor_name' => $doctorName,
'doctor_signature' => '',
'template_id' => 0,
'is_shared' => 0,
'is_system_auto' => 1,
'visible_role_ids' => self::normalizeVisibleRoleIds([]),
'audit_status' => 0,
'audit_time' => null,
'audit_by' => null,
'audit_by_name' => '',
'audit_remark' => '',
'creator_id' => $doctorId,
'assistant_id' => $assistantIdForRx,
];
$prescription = new Prescription();
$prescription->save($data);
return (int) $prescription->id;
}
/**
* 作废处方
*/
@@ -715,6 +850,16 @@ class PrescriptionLogic
self::setError('该处方已作废');
return false;
}
$rxId = (int) ($row->id ?? 0);
$bizCount = (int) PrescriptionOrder::where('prescription_id', $rxId)
->whereNull('delete_time')
->where('fulfillment_status', '<>', 4)
->count();
if ($bizCount > 0) {
self::setError('该处方已存在业务订单,无法作废');
return false;
}
$row->void_status = 1;
$row->void_time = time();
$row->void_by = $adminId;