This commit is contained in:
Your Name
2026-05-11 11:36:26 +08:00
parent bacdd6386f
commit 62d15efbaf
11 changed files with 629 additions and 230 deletions
@@ -552,7 +552,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
unset($item);
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists);
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists, $this->params);
}
return $lists;
@@ -28,6 +28,20 @@ use think\facade\Log;
*/
class AppointmentLogic extends BaseLogic
{
/** 仅这些字典名称需填写 channel_source_detail(与 admin 预约弹窗白名单一致,按 name 全等) */
private const CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL = [
'自媒体4H',
'自媒体3Q',
'自媒体3H',
'自媒体2H',
'自媒体2Q',
];
private static function channelDictNameRequiresSourceDetail(string $name): bool
{
return in_array($name, self::CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL, true);
}
/**
* @notes 获取可用时间段
* @param array $params
@@ -233,6 +247,21 @@ class AppointmentLogic extends BaseLogic
Db::rollback();
return false;
}
$chSrc = trim((string) ($params['channel_source'] ?? ''));
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
->where('value', $chSrc)
->value('name') ?? ''));
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
if ($chDetail === '') {
self::setError('请填写自媒体渠道补充信息');
Db::rollback();
return false;
}
}
$data = [
'patient_id' => $params['patient_id'],
'assistant_id'=>$params['assistant_id'],
@@ -244,7 +273,8 @@ class AppointmentLogic extends BaseLogic
'appointment_type' => $params['appointment_type'] ?? 'video',
'remark' => $params['remark'] ?? '',
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
'channel_source' => trim((string) ($params['channel_source'] ?? '')),
'channel_source' => $chSrc,
'channel_source_detail' => $chDetail,
'status' => 1,
'create_time' => time(),
'update_time' => time(),
@@ -4701,7 +4701,8 @@ class YejiStatsLogic
}
/**
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source
* **二中心及其组织下级**展示部门与看板一致:选定渠道收窄时本条无渠道订单(表中渠道业绩已为 0)。
*
* @param Query $query PrescriptionOrder 主表查询
* @param array<string, mixed> $params
@@ -4715,6 +4716,16 @@ class YejiStatsLogic
if ($channelCode === '') {
return;
}
/** @see excludeErCenterSubtreeFromChannelMetrics 看板选定渠道时对二中心子树渠道列置 0,侧栏须同步 */
$assistantDeptForDrawer = (int) ($params['assistant_dept_id'] ?? 0);
if ($assistantDeptForDrawer > 0) {
$erCenterSubtree = DeptLogic::getErCenterSubtreeDeptIdSet();
if (isset($erCenterSubtree[$assistantDeptForDrawer])) {
$query->whereRaw('0 = 1');
return;
}
}
$startDate = '';
$endDate = '';
$st = trim((string) ($params['start_time'] ?? ''));
@@ -4803,4 +4814,110 @@ class YejiStatsLogic
$query->whereRaw('0 = 1');
}
}
/**
* 业绩看板业务订单侧栏:在「渠道筛选」模式下,为每个诊单挑一条用于「关联挂号」展示的预约行——
* 须为已完成(status=3)、挂号助理为医助(role_id=2),且 channels/channel_source 与当前 channel_code 同源命中(与 {@see applyYejiDrawerChannelFilterToPrescriptionOrderQuery} 的 dict 收窄一致)。
* 若为 tag 等未走 channels 表达式的情况则返回空图,交由列表沿用处方/最近挂号。
*
* @param int[] $diagnosisIds doctor_appointment.patient_id(与本系统诊单 id 一致)
* @param array<string, mixed> $params 与列表同源:yeji_order_drawer、channel_code、start_time、end_time
*
* @return array<int, int> diagnosis_id => appointment_id
*/
public static function batchResolveYejiChannelDrawerHighlightAppointmentIds(array $diagnosisIds, array $params): array
{
$diagnosisIds = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0)));
if ($diagnosisIds === []) {
return [];
}
if ((int) ($params['yeji_order_drawer'] ?? 0) !== 1) {
return [];
}
$channelCode = trim((string) ($params['channel_code'] ?? ''));
if ($channelCode === '') {
return [];
}
$startDate = '';
$endDate = '';
$st = trim((string) ($params['start_time'] ?? ''));
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $st, $m)) {
$startDate = $m[1];
}
$et = trim((string) ($params['end_time'] ?? ''));
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $et, $m)) {
$endDate = $m[1];
}
if ($startDate === '' || $endDate === '') {
return [];
}
$ctx = self::resolveYejiContext([
'start_date' => $startDate,
'end_date' => $endDate,
'channel_code' => $channelCode,
'dept_ids' => null,
]);
if (!$ctx['channelFilterActive'] || $ctx['channelInfo'] === null) {
return [];
}
$norm = self::normalizeAppointmentChannelValues($ctx['appointmentChannelValues']);
$srcStrings = self::appointmentChannelDictStringValuesForDrawer($ctx['channelInfo']);
if ($norm === [] && $srcStrings === []) {
return [];
}
$apTable = self::tableWithPrefix('doctor_appointment');
$arTable = self::tableWithPrefix('admin_role');
$hasCh = self::doctorAppointmentHasChannelsColumn();
$orParts = [];
$channelBind = [];
if ($norm !== [] && $hasCh) {
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $norm)));
$ph = implode(',', array_fill(0, count($strVals), '?'));
$orParts[] = "ap.`channels` IN ({$ph})";
foreach ($strVals as $sv) {
$channelBind[] = $sv;
}
}
if ($srcStrings !== [] && self::doctorAppointmentHasChannelSourceColumn()) {
$ph = implode(',', array_fill(0, count($srcStrings), '?'));
$orParts[] = "ap.`channel_source` IN ({$ph})";
foreach ($srcStrings as $s) {
$channelBind[] = $s;
}
}
if ($orParts === []) {
return [];
}
$inPh = implode(',', array_fill(0, count($diagnosisIds), '?'));
$bind = array_merge($diagnosisIds, $channelBind);
$orSql = '(' . implode(' OR ', $orParts) . ')';
$sql = "SELECT ap.`patient_id` AS dg, MAX(ap.`id`) AS aid FROM `{$apTable}` ap "
. "INNER JOIN `{$arTable}` ar ON ar.`admin_id` = ap.`assistant_id` AND ar.`role_id` = 2 "
. "WHERE ap.`patient_id` IN ({$inPh}) AND ap.`status` = 3 AND {$orSql} "
. 'GROUP BY ap.`patient_id`';
try {
$rows = Db::query($sql, $bind);
} catch (\Throwable) {
return [];
}
$out = [];
foreach ($rows as $r) {
$dg = (int) ($r['dg'] ?? $r['DG'] ?? 0);
$aid = (int) ($r['aid'] ?? $r['AID'] ?? 0);
if ($dg > 0 && $aid > 0) {
$out[$dg] = $aid;
}
}
return $out;
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\order\OrderLogic;
use app\common\model\Order;
@@ -1049,11 +1050,13 @@ class PrescriptionOrderLogic
}
/**
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约
* 业绩侧栏且带 channel_code 时优先展示与渠道收窄同源的已完成挂号,避免列表行属自媒体2Q 但展示关联为自媒体1)
*
* @param array<int, array<string, mixed>> $lists
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(可选)
*/
public static function appendLinkedAppointmentsToListRows(array &$lists): void
public static function appendLinkedAppointmentsToListRows(array &$lists, array $listsParams = []): void
{
if ($lists === []) {
return;
@@ -1063,6 +1066,23 @@ class PrescriptionOrderLogic
}
unset($item);
$yejiChannelHighlightByDiag = [];
if (
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
) {
$allDiagIdsForYeji = array_values(array_unique(array_filter(
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
static fn (int $id): bool => $id > 0
)));
if ($allDiagIdsForYeji !== []) {
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
$allDiagIdsForYeji,
$listsParams
);
}
}
$rxIds = array_values(array_unique(array_filter(array_map(
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
$lists
@@ -1113,6 +1133,10 @@ class PrescriptionOrderLogic
if ($resolved <= 0 && $dgId > 0) {
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
}
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
if ($preferredYeji > 0) {
$resolved = $preferredYeji;
}
if ($resolved > 0) {
$resolvedPairs[$idx] = [$resolved, $rxApptId];
}
@@ -25,6 +25,7 @@ class AppointmentValidate extends BaseValidate
'appointment_time' => 'require',
'appointment_type' => 'in:video,text,phone',
'channel_source' => 'require',
'channel_source_detail' => 'max:128',
'note_id' => 'require|integer',
'image_type' => 'require|in:tongue_images,report_files',
'image_path' => 'require',
@@ -44,6 +45,7 @@ class AppointmentValidate extends BaseValidate
'appointment_time' => '预约时间',
'appointment_type' => '预约类型',
'channel_source' => '渠道来源',
'channel_source_detail' => '渠道补充说明',
];
/**
@@ -52,7 +54,7 @@ class AppointmentValidate extends BaseValidate
*/
public function sceneCreate()
{
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source']);
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source', 'channel_source_detail']);
}
/**