更新
This commit is contained in:
@@ -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'])) {
|
||||
|
||||
@@ -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_event(add_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;
|
||||
|
||||
Reference in New Issue
Block a user