geng
This commit is contained in:
@@ -627,10 +627,35 @@ class AppointmentLogic extends BaseLogic
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public static function reception(array $params): array
|
||||
{
|
||||
// 1) 挂号详情(已包含 patient_name / patient_phone / doctor_name / status_desc 等)
|
||||
$appointment = self::detail($params);
|
||||
public static function reception(array $params, int $adminId, array $adminInfo): array
|
||||
{
|
||||
self::$error = '';
|
||||
$appointmentId = (int) ($params['id'] ?? 0);
|
||||
$appointmentRow = $appointmentId > 0
|
||||
? Appointment::where('id', $appointmentId)->field(['id', 'patient_id', 'doctor_id'])->find()
|
||||
: null;
|
||||
if (!$appointmentRow) {
|
||||
self::setError('预约记录不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
$diagnosisRow = Diagnosis::where('id', (int) $appointmentRow->patient_id)
|
||||
->whereNull('delete_time')
|
||||
->field(['id', 'assistant_id'])
|
||||
->find();
|
||||
if (!self::appointmentRowManageableByAdmin(
|
||||
$appointmentRow,
|
||||
$diagnosisRow ?: null,
|
||||
$adminId,
|
||||
$adminInfo
|
||||
)) {
|
||||
self::setError('预约记录不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1) 挂号详情(已包含 patient_name / patient_phone / doctor_name / status_desc 等)
|
||||
$appointment = self::detail($params);
|
||||
if (empty($appointment)) {
|
||||
return [];
|
||||
}
|
||||
@@ -883,40 +908,62 @@ class AppointmentLogic extends BaseLogic
|
||||
/**
|
||||
* 与 AppointmentLists 一致的可见性(不含 progress_board / diag_scope_relax)
|
||||
*/
|
||||
private static function appointmentRowManageableByAdmin(
|
||||
Appointment $appointment,
|
||||
?Diagnosis $diag,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): bool {
|
||||
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
|
||||
if (in_array(1, $roleIds, true) && (int) $appointment->doctor_id !== $adminId) {
|
||||
return false;
|
||||
}
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
$asst = $diag ? (int) $diag->assistant_id : 0;
|
||||
if ($asst !== $adminId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DataScopeService::isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
$ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($ids === []) {
|
||||
return false;
|
||||
}
|
||||
if ($ids === null) {
|
||||
return true;
|
||||
}
|
||||
$docId = (int) $appointment->doctor_id;
|
||||
$asstId = $diag ? (int) $diag->assistant_id : 0;
|
||||
|
||||
return in_array($docId, $ids, true)
|
||||
|| ($asstId > 0 && in_array($asstId, $ids, true));
|
||||
}
|
||||
private static function appointmentRowManageableByAdmin(
|
||||
Appointment $appointment,
|
||||
?Diagnosis $diag,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): bool {
|
||||
$docId = (int) $appointment->doctor_id;
|
||||
$asstId = $diag ? (int) $diag->assistant_id : 0;
|
||||
$isRoot = !empty($adminInfo['root']) && (int) $adminInfo['root'] === 1;
|
||||
$roleIds = $isRoot
|
||||
? []
|
||||
: array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
$visibleIds = null;
|
||||
if (!$isRoot && DataScopeService::isEnabled()) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
}
|
||||
|
||||
return self::appointmentRowManageableForScope(
|
||||
$docId,
|
||||
$asstId,
|
||||
$adminId,
|
||||
$roleIds,
|
||||
$visibleIds,
|
||||
$isRoot
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $roleIds
|
||||
* @param array<int, int>|null $visibleIds null 表示未启用数据范围或全量可见
|
||||
*/
|
||||
private static function appointmentRowManageableForScope(
|
||||
int $doctorId,
|
||||
int $assistantId,
|
||||
int $adminId,
|
||||
array $roleIds,
|
||||
?array $visibleIds,
|
||||
bool $isRoot
|
||||
): bool {
|
||||
if ($isRoot) {
|
||||
return true;
|
||||
}
|
||||
if (in_array(1, $roleIds, true) && $doctorId !== $adminId) {
|
||||
return false;
|
||||
}
|
||||
if (in_array(2, $roleIds, true) && $assistantId !== $adminId) {
|
||||
return false;
|
||||
}
|
||||
if ($visibleIds === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $visibleIds === null
|
||||
|| in_array($doctorId, $visibleIds, true)
|
||||
|| ($assistantId > 0 && in_array($assistantId, $visibleIds, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台编辑挂号(预约日期/时段/类型/状态/备注/医助)
|
||||
|
||||
@@ -25,8 +25,8 @@ class DoctorNoteLogic extends BaseLogic
|
||||
->find();
|
||||
|
||||
$newContent = trim($params['content'] ?? '');
|
||||
$newImages = array_map([self::class, 'toRelativePath'], self::parseJsonArray($params['tongue_images'] ?? []));
|
||||
$newReports = array_map([self::class, 'toRelativePath'], self::parseJsonArray($params['report_files'] ?? []));
|
||||
$newImages = self::normalizeNewAttachmentPaths($params['tongue_images'] ?? []);
|
||||
$newReports = self::normalizeNewAttachmentPaths($params['report_files'] ?? []);
|
||||
|
||||
if ($existing) {
|
||||
$data = [];
|
||||
@@ -169,15 +169,51 @@ class DoctorNoteLogic extends BaseLogic
|
||||
*/
|
||||
private static function toRelativePath(string $url): string
|
||||
{
|
||||
if (empty($url)) return $url;
|
||||
if (stripos($url, 'http://') !== 0 && stripos($url, 'https://') !== 0) {
|
||||
$url = trim($url);
|
||||
if ($url === '') return $url;
|
||||
|
||||
$urlParts = parse_url($url);
|
||||
if (!is_array($urlParts) || empty($urlParts['scheme'])) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$scheme = strtolower((string) $urlParts['scheme']);
|
||||
if (!in_array($scheme, ['http', 'https'], true)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
// 获取当前存储域名
|
||||
$domain = self::getStorageDomain();
|
||||
if ($domain && stripos($url, rtrim($domain, '/')) === 0) {
|
||||
$relative = substr($url, strlen(rtrim($domain, '/')));
|
||||
return ltrim($relative, '/');
|
||||
$domain = rtrim(self::getStorageDomain(), '/');
|
||||
$domainParts = $domain !== '' ? parse_url($domain) : false;
|
||||
if (is_array($domainParts)) {
|
||||
$domainScheme = strtolower((string) ($domainParts['scheme'] ?? ''));
|
||||
$urlHost = strtolower(rtrim((string) ($urlParts['host'] ?? ''), '.'));
|
||||
$domainHost = strtolower(rtrim((string) ($domainParts['host'] ?? ''), '.'));
|
||||
$urlPort = (int) ($urlParts['port'] ?? ($scheme === 'https' ? 443 : 80));
|
||||
$domainPort = (int) (
|
||||
$domainParts['port'] ?? ($domainScheme === 'https' ? 443 : 80)
|
||||
);
|
||||
$urlPath = (string) ($urlParts['path'] ?? '');
|
||||
$domainPath = rtrim((string) ($domainParts['path'] ?? ''), '/');
|
||||
$pathInsideDomain = $domainPath === ''
|
||||
|| $urlPath === $domainPath
|
||||
|| str_starts_with($urlPath, $domainPath . '/');
|
||||
|
||||
if (
|
||||
$domainScheme === $scheme
|
||||
&& $domainHost !== ''
|
||||
&& $domainHost === $urlHost
|
||||
&& $domainPort === $urlPort
|
||||
&& $pathInsideDomain
|
||||
) {
|
||||
$relative = $domainPath === ''
|
||||
? $urlPath
|
||||
: substr($urlPath, strlen($domainPath));
|
||||
if (isset($urlParts['query']) && $urlParts['query'] !== '') {
|
||||
$relative .= '?' . $urlParts['query'];
|
||||
}
|
||||
return ltrim($relative, '/');
|
||||
}
|
||||
}
|
||||
// 非当前存储域名,保留完整 URL
|
||||
return $url;
|
||||
@@ -193,6 +229,36 @@ class DoctorNoteLogic extends BaseLogic
|
||||
return $storage ? ($storage['domain'] ?? '') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 备注附件只接受站内相对路径或当前存储域已上传的 URL。
|
||||
* 存储域 URL 先转为相对路径,避免将任意外部 URL 持久化到病例页。
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private static function normalizeNewAttachmentPaths($value): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (self::parseJsonArray($value) as $rawPath) {
|
||||
$path = trim((string) $rawPath);
|
||||
if ($path === '') {
|
||||
continue;
|
||||
}
|
||||
$path = self::toRelativePath($path);
|
||||
$scheme = parse_url($path, PHP_URL_SCHEME);
|
||||
if (
|
||||
(is_string($scheme) && $scheme !== '')
|
||||
|| str_starts_with($path, '//')
|
||||
|| str_contains($path, "\0")
|
||||
) {
|
||||
throw new \InvalidArgumentException('备注附件必须来自当前文件存储域');
|
||||
}
|
||||
$paths[] = $path;
|
||||
}
|
||||
|
||||
return array_values(array_unique($paths));
|
||||
}
|
||||
|
||||
private static function parseJsonArray($value): array
|
||||
{
|
||||
if (is_array($value)) return $value;
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\firstvisit;
|
||||
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\stats\ConversionLogic;
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
@@ -23,6 +24,9 @@ use think\facade\Db;
|
||||
class FirstVisitConversionLogic
|
||||
{
|
||||
private const ASSISTANT_ROLE_ID = 2;
|
||||
private const FINANCE_PERMISSION = 'firstvisit.conversion/viewFinance';
|
||||
private const FINANCE_ALWAYS_ROLE_NAMES = ['经理', '管理员', '系统管理员'];
|
||||
private const FINANCE_FIELD_KEYS = ['account_cost', 'cash_cost', 'roi'];
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function overview(array $params, int $adminId, array $adminInfo): array
|
||||
@@ -64,11 +68,11 @@ class FirstVisitConversionLogic
|
||||
$effectiveAdminIds = self::intersectVisibleIds($effectiveAdminIds, $deptAdminIds);
|
||||
}
|
||||
|
||||
$selectedAssistantValid = $selectedAssistantId <= 0;
|
||||
if ($selectedAssistantId > 0) {
|
||||
$selectedAssistantValid = self::isActiveAssistant($selectedAssistantId)
|
||||
&& ($effectiveAdminIds === null || in_array($selectedAssistantId, $effectiveAdminIds, true));
|
||||
$effectiveAdminIds = $selectedAssistantValid ? [$selectedAssistantId] : [];
|
||||
$selectedAssistantValid = $selectedAssistantId <= 0;
|
||||
if ($selectedAssistantId > 0) {
|
||||
$selectedAssistantValid = self::isActiveAssistant($selectedAssistantId)
|
||||
&& ($effectiveAdminIds === null || in_array($selectedAssistantId, $effectiveAdminIds, true));
|
||||
$effectiveAdminIds = $selectedAssistantValid ? [$selectedAssistantId] : [];
|
||||
}
|
||||
$costAllocationAdminIds = self::costAllocationAdminIds(
|
||||
$effectiveAdminIds,
|
||||
@@ -137,23 +141,36 @@ class FirstVisitConversionLogic
|
||||
(int) $summary['total_open_count']
|
||||
);
|
||||
|
||||
$rankingKind = self::rankingKind($scopeValue, $selectedAssistantId);
|
||||
$rankingRows = self::rankingRows($rows, $rankingKind);
|
||||
$rankingKind = self::rankingKind($scopeValue, $selectedAssistantId);
|
||||
$rankingRows = self::rankingRows($rows, $rankingKind);
|
||||
// 目前只维护了部门月度目标;本人范围或筛选单个员工时不能拿整个部门目标冒充个人目标。
|
||||
$targetDeptIds = ($scopeValue === DataScopeService::SCOPE_SELF || $selectedAssistantId > 0)
|
||||
? []
|
||||
: self::resolveTargetDeptIds($allowedDeptSet, $selectedDeptIds, $selectedDeptId);
|
||||
$target = self::buildTargetProgress((int) date('Y'), $effectiveAdminIds, $targetDeptIds);
|
||||
|
||||
$selectedDeptName = $selectedDeptId > 0 && $deptSelectionValid
|
||||
? (string) (Db::name('dept')->where('id', $selectedDeptId)->whereNull('delete_time')->value('name') ?? '')
|
||||
: '';
|
||||
$selectedAssistantName = $selectedAssistantId > 0 && $selectedAssistantValid
|
||||
? (string) (Admin::where('id', $selectedAssistantId)->whereNull('delete_time')->value('name') ?? '')
|
||||
: '';
|
||||
$selectedDeptName = $selectedDeptId > 0 && $deptSelectionValid
|
||||
? (string) (Db::name('dept')->where('id', $selectedDeptId)->whereNull('delete_time')->value('name') ?? '')
|
||||
: '';
|
||||
$selectedAssistantName = $selectedAssistantId > 0 && $selectedAssistantValid
|
||||
? (string) (Admin::where('id', $selectedAssistantId)->whereNull('delete_time')->value('name') ?? '')
|
||||
: '';
|
||||
$selectedMediaChannelName = $selectedMediaChannelCode !== ''
|
||||
? (string) ($selectedMediaChannel['channel_name'] ?? $selectedMediaChannelCode)
|
||||
: '';
|
||||
if ($selectedMediaChannelName !== '' && !empty($selectedMediaChannel['is_group'])) {
|
||||
$selectedMediaChannelName .= '(全部)';
|
||||
}
|
||||
$canViewFinance = self::canViewFinance($adminId, $adminInfo);
|
||||
if (!$canViewFinance) {
|
||||
$summary = self::maskFinanceFields($summary);
|
||||
foreach ($rows as &$row) {
|
||||
if (is_array($row)) {
|
||||
$row = self::maskFinanceFields($row);
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
return [
|
||||
'meta' => [
|
||||
'time_type' => $timeType,
|
||||
@@ -161,9 +178,9 @@ class FirstVisitConversionLogic
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'scope_value' => $scopeValue,
|
||||
'scope_label' => DataScopeService::scopeLabel($scopeValue),
|
||||
'ranking_kind' => $rankingKind,
|
||||
'scope_value' => $scopeValue,
|
||||
'scope_label' => DataScopeService::scopeLabel($scopeValue),
|
||||
'ranking_kind' => $rankingKind,
|
||||
'selected_dept_name' => $selectedDeptName,
|
||||
'selected_assistant_name' => $selectedAssistantName,
|
||||
'selected_media_channel_code' => $selectedMediaChannelCode,
|
||||
@@ -171,6 +188,7 @@ class FirstVisitConversionLogic
|
||||
'open_count_source' => $selectedMediaChannelCode === ''
|
||||
? '个人业绩录入'
|
||||
: '个人业绩录入(按渠道名称匹配)',
|
||||
'can_view_finance' => $canViewFinance,
|
||||
'appointment_rule' => '按预约日期统计,归属优先挂号医助、再回退诊单医助;仅含已预约、已完成和已过号',
|
||||
'registration_rule' => '按支付时间统计已支付且实收金额大于 0、低于 10 元的订单,每笔计 1 个挂号并按订单创建人归属',
|
||||
'performance_rule' => '按业务订单创建时间和创建人统计,排除取消、拒收、退款及已发生退款的订单',
|
||||
@@ -520,45 +538,101 @@ class FirstVisitConversionLogic
|
||||
return [];
|
||||
}
|
||||
|
||||
$values = [
|
||||
$channelCode,
|
||||
$channel['channel_name'] ?? '',
|
||||
$channel['source_tag_name'] ?? '',
|
||||
$channel['legacy_channel_name'] ?? '',
|
||||
$channel['legacy_source_tag_name'] ?? '',
|
||||
];
|
||||
foreach (['channel_codes', 'channel_names', 'source_tag_names'] as $listKey) {
|
||||
if (!isset($channel[$listKey]) || !is_array($channel[$listKey])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($channel[$listKey] as $item) {
|
||||
$values[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
static fn ($value): string => trim((string) $value),
|
||||
[
|
||||
$channelCode,
|
||||
$channel['channel_name'] ?? '',
|
||||
$channel['source_tag_name'] ?? '',
|
||||
$channel['legacy_channel_name'] ?? '',
|
||||
$channel['legacy_source_tag_name'] ?? '',
|
||||
]
|
||||
), static fn (string $value): bool => $value !== '')));
|
||||
$values
|
||||
), static fn (string $value): bool => $value !== '' && !str_starts_with($value, MediaChannelService::GROUP_CODE_PREFIX))));
|
||||
}
|
||||
|
||||
/** 根据生效数据范围返回排行榜展示维度,不能把 scope_value 当作角色枚举。 */
|
||||
private static function rankingKind(int $scopeValue, int $selectedAssistantId = 0): string
|
||||
{
|
||||
if ($scopeValue === DataScopeService::SCOPE_SELF || $selectedAssistantId > 0) {
|
||||
return 'hidden';
|
||||
}
|
||||
|
||||
return $scopeValue === DataScopeService::SCOPE_DEPT ? 'member' : 'group';
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
|
||||
private static function rankingRows(array $rows, string $rankingKind): array
|
||||
{
|
||||
if ($rankingKind === 'hidden') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// “仅本部门”范围使用可见成员维度;更大范围使用当前可见组织根节点的
|
||||
// 直属下级,避免父子汇总同时参与占比。
|
||||
if ($rankingKind === 'member') {
|
||||
$members = [];
|
||||
self::collectRankingMembers($rows, $members);
|
||||
|
||||
return array_values($members);
|
||||
}
|
||||
|
||||
// lists 里可能同时存在“未绑定/未分配部门”等虚拟根节点。它们会让顶层节点数量
|
||||
private static function canViewFinance(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if ((int) ($adminInfo['root'] ?? 0) === 1) {
|
||||
return true;
|
||||
}
|
||||
foreach (self::roleNamesFromAdminInfo($adminInfo) as $roleName) {
|
||||
if (in_array($roleName, self::FINANCE_ALWAYS_ROLE_NAMES, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ($adminId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(self::FINANCE_PERMISSION, AuthLogic::getAuthByAdminId($adminId), true);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
private static function roleNamesFromAdminInfo(array $adminInfo): array
|
||||
{
|
||||
$names = preg_split('/[\/,,、]/u', (string) ($adminInfo['role_name'] ?? '')) ?: [];
|
||||
|
||||
return array_values(array_filter(array_map('trim', $names), static fn (string $name): bool => $name !== ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entity
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function maskFinanceFields(array $entity): array
|
||||
{
|
||||
foreach (self::FINANCE_FIELD_KEYS as $key) {
|
||||
unset($entity[$key]);
|
||||
}
|
||||
if (isset($entity['children']) && is_array($entity['children'])) {
|
||||
foreach ($entity['children'] as &$child) {
|
||||
if (is_array($child)) {
|
||||
$child = self::maskFinanceFields($child);
|
||||
}
|
||||
}
|
||||
unset($child);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/** 根据生效数据范围返回排行榜展示维度,不能把 scope_value 当作角色枚举。 */
|
||||
private static function rankingKind(int $scopeValue, int $selectedAssistantId = 0): string
|
||||
{
|
||||
if ($scopeValue === DataScopeService::SCOPE_SELF || $selectedAssistantId > 0) {
|
||||
return 'hidden';
|
||||
}
|
||||
|
||||
return $scopeValue === DataScopeService::SCOPE_DEPT ? 'member' : 'group';
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
|
||||
private static function rankingRows(array $rows, string $rankingKind): array
|
||||
{
|
||||
if ($rankingKind === 'hidden') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// “仅本部门”范围使用可见成员维度;更大范围使用当前可见组织根节点的
|
||||
// 直属下级,避免父子汇总同时参与占比。
|
||||
if ($rankingKind === 'member') {
|
||||
$members = [];
|
||||
self::collectRankingMembers($rows, $members);
|
||||
|
||||
return array_values($members);
|
||||
}
|
||||
|
||||
// lists 里可能同时存在“未绑定/未分配部门”等虚拟根节点。它们会让顶层节点数量
|
||||
// 大于 1,导致原逻辑无法展开唯一的真实组织根节点,图表最终只显示医院汇总行。
|
||||
$visibleRows = array_values(array_filter($rows, static function (array $row): bool {
|
||||
return (int) ($row['id'] ?? 0) > 0 && !((bool) ($row['_virtual_bucket'] ?? false));
|
||||
@@ -583,59 +657,59 @@ class FirstVisitConversionLogic
|
||||
$chartRows[] = $row;
|
||||
}
|
||||
|
||||
return $chartRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $rows
|
||||
* @param array<int,array<string,mixed>> $members
|
||||
*/
|
||||
private static function collectRankingMembers(array $rows, array &$members): void
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
if ((string) ($row['type'] ?? '') === 'member') {
|
||||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||||
if ($adminId > 0) {
|
||||
$members[$adminId] = $row;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
self::collectRankingMembers(
|
||||
is_array($row['children'] ?? null) ? $row['children'] : [],
|
||||
$members
|
||||
);
|
||||
}
|
||||
}
|
||||
return $chartRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $rows
|
||||
* @param array<int,array<string,mixed>> $members
|
||||
*/
|
||||
private static function collectRankingMembers(array $rows, array &$members): void
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
if ((string) ($row['type'] ?? '') === 'member') {
|
||||
$adminId = (int) ($row['admin_id'] ?? 0);
|
||||
if ($adminId > 0) {
|
||||
$members[$adminId] = $row;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
self::collectRankingMembers(
|
||||
is_array($row['children'] ?? null) ? $row['children'] : [],
|
||||
$members
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
|
||||
private static function topRows(array $rows, string $metric): array
|
||||
{
|
||||
$rows = array_values(array_filter($rows, static function (array $row): bool {
|
||||
if ((string) ($row['type'] ?? '') === 'member') {
|
||||
return (int) ($row['admin_id'] ?? 0) > 0;
|
||||
}
|
||||
|
||||
return (int) ($row['id'] ?? 0) > 0;
|
||||
}));
|
||||
usort($rows, static function (array $left, array $right) use ($metric): int {
|
||||
$valueCompare = (float) ($right[$metric] ?? 0) <=> (float) ($left[$metric] ?? 0);
|
||||
if ($valueCompare !== 0) {
|
||||
return $valueCompare;
|
||||
}
|
||||
$nameCompare = strnatcasecmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
|
||||
if ($nameCompare !== 0) {
|
||||
return $nameCompare;
|
||||
}
|
||||
|
||||
return strcmp((string) ($left['id'] ?? ''), (string) ($right['id'] ?? ''));
|
||||
});
|
||||
|
||||
return array_map(static fn (array $row): array => [
|
||||
'id' => $row['id'] ?? 0,
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'value' => round((float) ($row[$metric] ?? 0), 2),
|
||||
], $rows);
|
||||
}
|
||||
private static function topRows(array $rows, string $metric): array
|
||||
{
|
||||
$rows = array_values(array_filter($rows, static function (array $row): bool {
|
||||
if ((string) ($row['type'] ?? '') === 'member') {
|
||||
return (int) ($row['admin_id'] ?? 0) > 0;
|
||||
}
|
||||
|
||||
return (int) ($row['id'] ?? 0) > 0;
|
||||
}));
|
||||
usort($rows, static function (array $left, array $right) use ($metric): int {
|
||||
$valueCompare = (float) ($right[$metric] ?? 0) <=> (float) ($left[$metric] ?? 0);
|
||||
if ($valueCompare !== 0) {
|
||||
return $valueCompare;
|
||||
}
|
||||
$nameCompare = strnatcasecmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
|
||||
if ($nameCompare !== 0) {
|
||||
return $nameCompare;
|
||||
}
|
||||
|
||||
return strcmp((string) ($left['id'] ?? ''), (string) ($right['id'] ?? ''));
|
||||
});
|
||||
|
||||
return array_map(static fn (array $row): array => [
|
||||
'id' => $row['id'] ?? 0,
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'value' => round((float) ($row[$metric] ?? 0), 2),
|
||||
], $rows);
|
||||
}
|
||||
|
||||
/** @param int[]|null $baseVisibleAdminIds @param int[] $selectedDeptIds @return array<int,array{id:int,name:string}> */
|
||||
private static function assistantOptions(?array $baseVisibleAdminIds, array $selectedDeptIds, int $selectedDeptId): array
|
||||
|
||||
@@ -62,7 +62,9 @@ class ConversionLogic
|
||||
if ($mediaChannel === null && $requestedMediaChannelCode !== '') {
|
||||
$mediaChannel = MediaChannelService::getChannelByCode($requestedMediaChannelCode);
|
||||
}
|
||||
$mediaChannelCode = $mediaChannel !== null ? $requestedMediaChannelCode : '';
|
||||
$mediaChannelCodes = $mediaChannel !== null
|
||||
? MediaChannelService::getChannelCodesForStats($mediaChannel)
|
||||
: null;
|
||||
$filterEmptyEntities = $mediaChannel !== null;
|
||||
[$startTimestamp, $endTimestamp, $startDate, $endDate] = self::resolveTimeRange($params);
|
||||
$pageNo = max(1, (int)($params['page_no'] ?? 1));
|
||||
@@ -188,11 +190,11 @@ class ConversionLogic
|
||||
);
|
||||
// 数据隔离:可见部门 = 可见 admin 所属部门并集;用于 account_cost 与下游 cost 分摊。
|
||||
$visibleDeptIds = self::resolveVisibleDeptIds($visibleAdminIds);
|
||||
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode, $visibleDeptIds);
|
||||
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCodes, $visibleDeptIds);
|
||||
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
||||
$restrictAccountCostByDept = $supportsDeptBinding;
|
||||
$channelBoundDeptIds = $supportsDeptBinding && $mediaChannelCode !== ''
|
||||
? self::loadChannelBoundDeptIds($mediaChannelCode)
|
||||
$channelBoundDeptIds = $supportsDeptBinding && $mediaChannelCodes !== null && $mediaChannelCodes !== []
|
||||
? self::loadChannelBoundDeptIds($mediaChannelCodes)
|
||||
: [];
|
||||
// 渠道尚未维护投放成本时,不能把真实的加粉、挂号和订单一并过滤为空。
|
||||
// 已维护绑定关系的渠道继续按绑定部门收窄;成本本身仍只在实际成本部门内分摊。
|
||||
@@ -267,7 +269,7 @@ class ConversionLogic
|
||||
$startDate,
|
||||
$endDate,
|
||||
$mediaChannel,
|
||||
$mediaChannelCode,
|
||||
$mediaChannelCodes,
|
||||
$restrictAccountCostByDept,
|
||||
$eligibleDeptIds,
|
||||
$adminToDeptIds,
|
||||
@@ -824,17 +826,18 @@ class ConversionLogic
|
||||
/**
|
||||
* 渠道绑定部门不依赖当前统计区间,避免某天没有录入成本时把统计实体过滤为空。
|
||||
*
|
||||
* @param string[] $mediaChannelCodes
|
||||
* @return int[]
|
||||
*/
|
||||
private static function loadChannelBoundDeptIds(string $mediaChannelCode): array
|
||||
private static function loadChannelBoundDeptIds(array $mediaChannelCodes): array
|
||||
{
|
||||
$mediaChannelCode = trim($mediaChannelCode);
|
||||
if ($mediaChannelCode === '' || !AccountCost::supportsDeptBinding()) {
|
||||
$mediaChannelCodes = self::normalizeMediaChannelCodes($mediaChannelCodes);
|
||||
if ($mediaChannelCodes === [] || !AccountCost::supportsDeptBinding()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$deptIds = Db::name('account_cost')
|
||||
->where('media_channel_code', $mediaChannelCode)
|
||||
->whereIn('media_channel_code', $mediaChannelCodes)
|
||||
->where('dept_id', '>', 0)
|
||||
->distinct(true)
|
||||
->column('dept_id');
|
||||
@@ -1596,6 +1599,7 @@ class ConversionLogic
|
||||
* 账户消耗:来源于独立维护表 zyt_account_cost。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $entities
|
||||
* @param string[]|null $mediaChannelCodes null=不按渠道过滤;[]=已选渠道但无匹配 code,成本记 0
|
||||
* @param int[]|null $visibleDeptIds 可见部门集合(null = SCOPE_ALL,不收窄)
|
||||
* @return array{0: float, 1: int[]}
|
||||
*/
|
||||
@@ -1603,10 +1607,21 @@ class ConversionLogic
|
||||
array &$entities,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
string $mediaChannelCode,
|
||||
?array $mediaChannelCodes,
|
||||
?array $visibleDeptIds = null
|
||||
): array
|
||||
{
|
||||
$mediaChannelCodes = $mediaChannelCodes === null ? null : self::normalizeMediaChannelCodes($mediaChannelCodes);
|
||||
if ($mediaChannelCodes === []) {
|
||||
foreach ($entities as &$entity) {
|
||||
$entity['account_cost'] = 0.0;
|
||||
$entity['_global_account_cost'] = 0.0;
|
||||
}
|
||||
unset($entity);
|
||||
|
||||
return [0.0, []];
|
||||
}
|
||||
|
||||
$supportsDeptBinding = AccountCost::supportsDeptBinding();
|
||||
$query = Db::name('account_cost')
|
||||
->where('cost_date', '>=', $startDate)
|
||||
@@ -1618,8 +1633,8 @@ class ConversionLogic
|
||||
$query->field('amount');
|
||||
}
|
||||
|
||||
if ($mediaChannelCode !== '') {
|
||||
$query->where('media_channel_code', $mediaChannelCode);
|
||||
if ($mediaChannelCodes !== null) {
|
||||
$query->whereIn('media_channel_code', $mediaChannelCodes);
|
||||
}
|
||||
|
||||
if ($supportsDeptBinding) {
|
||||
@@ -2049,7 +2064,7 @@ class ConversionLogic
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
?array $mediaChannel,
|
||||
string $mediaChannelCode,
|
||||
?array $mediaChannelCodes,
|
||||
bool $restrictAccountCostByDept,
|
||||
array $eligibleDeptIds,
|
||||
array $adminToDeptIds,
|
||||
@@ -2081,9 +2096,9 @@ class ConversionLogic
|
||||
if ($globalAccountCost < 0) {
|
||||
// 任选一个非空 entity 集合查一次即可——查询本身只与日期 / 渠道相关。
|
||||
if ($assistantIds !== []) {
|
||||
[$globalAccountCost] = self::hydrateAccountCostStats($assistantEntities, $startDate, $endDate, $mediaChannelCode);
|
||||
[$globalAccountCost] = self::hydrateAccountCostStats($assistantEntities, $startDate, $endDate, $mediaChannelCodes);
|
||||
} elseif ($doctorIds !== []) {
|
||||
[$globalAccountCost] = self::hydrateAccountCostStats($doctorEntities, $startDate, $endDate, $mediaChannelCode);
|
||||
[$globalAccountCost] = self::hydrateAccountCostStats($doctorEntities, $startDate, $endDate, $mediaChannelCodes);
|
||||
} else {
|
||||
$globalAccountCost = 0.0;
|
||||
}
|
||||
@@ -2905,10 +2920,29 @@ class ConversionLogic
|
||||
return [
|
||||
'code' => (string)($mediaChannel['channel_code'] ?? ''),
|
||||
'tag_id' => (string)($mediaChannel['source_tag_id'] ?? ''),
|
||||
'tag_ids' => $mediaChannel['source_tag_ids'] ?? [],
|
||||
'tag_name' => (string)($mediaChannel['source_tag_name'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $mediaChannelCode
|
||||
* @return string[]
|
||||
*/
|
||||
private static function normalizeMediaChannelCodes(string|array $mediaChannelCode): array
|
||||
{
|
||||
$values = is_array($mediaChannelCode) ? $mediaChannelCode : [$mediaChannelCode];
|
||||
$codes = [];
|
||||
foreach ($values as $value) {
|
||||
$code = trim((string)$value);
|
||||
if ($code !== '' && !str_starts_with($code, MediaChannelService::GROUP_CODE_PREFIX)) {
|
||||
$codes[$code] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|null $visibleAdminIds
|
||||
* @param int[] $eligibleDeptIds
|
||||
|
||||
@@ -4,13 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\cache\AdminAuthCache;
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisAiReport;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\DifyChatService;
|
||||
use app\common\cache\AdminAuthCache;
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\tcm\DiagnosisAiReport;
|
||||
use app\common\service\DifyChatService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
@@ -201,6 +200,44 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
string $prompt,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): ?array {
|
||||
$prepared = self::prepareAssistant($diagnosisId, $task, $prompt, $adminId, $adminInfo);
|
||||
if ($prepared === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = DifyChatService::chat(
|
||||
$prepared['profile'],
|
||||
$prepared['inputs'],
|
||||
$prepared['query'],
|
||||
$prepared['user']
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
self::logAssistantFailure($diagnosisId, $prepared['profile'], $adminId, $e);
|
||||
self::setError('AI 助手暂时不可用,请稍后重试');
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::formatAssistantResult($prepared, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 SSE headers 发出前完成参数、权限、DataScope、病例和模型选择预检。
|
||||
* 返回值只供同一请求内的流执行使用,绝不能直接序列化给客户端。
|
||||
*
|
||||
* @param array<string,mixed> $adminInfo
|
||||
* @return array{
|
||||
* diagnosis_id:int,profile:string,model_name:string,model_label:string,task:string,
|
||||
* inputs:array<string,mixed>,query:string,user:string,admin_id:int
|
||||
* }|null
|
||||
*/
|
||||
public static function prepareAssistant(
|
||||
int $diagnosisId,
|
||||
string $task,
|
||||
string $prompt,
|
||||
int $adminId,
|
||||
array $adminInfo
|
||||
): ?array {
|
||||
$diagnosis = self::loadAuthorizedDiagnosis(
|
||||
$diagnosisId,
|
||||
@@ -239,28 +276,63 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'profile' => $profile,
|
||||
'model_name' => $model,
|
||||
'model_label' => $modelLabel,
|
||||
'task' => $task,
|
||||
'inputs' => self::buildUpstreamInputs(
|
||||
$context,
|
||||
'病例问诊助手',
|
||||
self::ASSISTANT_PROMPT_VERSION
|
||||
),
|
||||
'query' => self::buildAssistantPrompt($context, $task, $prompt),
|
||||
'user' => 'admin-diagnosis-assistant-' . $adminId,
|
||||
'admin_id' => $adminId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $prepared prepareAssistant() 的内部返回值
|
||||
* @param callable(string):mixed $onDelta
|
||||
* @param callable():bool|null $shouldAbort
|
||||
* @return array{answer:string,model_key:string,model_label:string,model_name:string,task:string}|null
|
||||
*/
|
||||
public static function streamPreparedAssistant(
|
||||
array $prepared,
|
||||
callable $onDelta,
|
||||
?callable $shouldAbort = null
|
||||
): ?array {
|
||||
$diagnosisId = (int) ($prepared['diagnosis_id'] ?? 0);
|
||||
$profile = (string) ($prepared['profile'] ?? '');
|
||||
$adminId = (int) ($prepared['admin_id'] ?? 0);
|
||||
|
||||
try {
|
||||
$result = DifyChatService::chat(
|
||||
$result = DifyChatService::streamChat(
|
||||
$profile,
|
||||
self::buildUpstreamInputs(
|
||||
$context,
|
||||
'病例问诊助手',
|
||||
self::ASSISTANT_PROMPT_VERSION
|
||||
),
|
||||
self::buildAssistantPrompt($context, $task, $prompt),
|
||||
'admin-diagnosis-assistant-' . $adminId
|
||||
is_array($prepared['inputs'] ?? null) ? $prepared['inputs'] : [],
|
||||
(string) ($prepared['query'] ?? ''),
|
||||
(string) ($prepared['user'] ?? ''),
|
||||
$onDelta,
|
||||
$shouldAbort
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('diagnosis ai assistant upstream call failed', [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'profile' => $profile,
|
||||
'admin_id' => $adminId,
|
||||
'exception_class' => get_class($e),
|
||||
]);
|
||||
self::logAssistantFailure($diagnosisId, $profile, $adminId, $e);
|
||||
self::setError('AI 助手暂时不可用,请稍后重试');
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::formatAssistantResult($prepared, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $prepared
|
||||
* @param array<string,mixed> $result
|
||||
* @return array{answer:string,model_key:string,model_label:string,model_name:string,task:string}|null
|
||||
*/
|
||||
private static function formatAssistantResult(array $prepared, array $result): ?array
|
||||
{
|
||||
if (empty($result['ok'])) {
|
||||
self::setError((string) ($result['error'] ?? 'AI 助手暂时不可用,请稍后重试'));
|
||||
return null;
|
||||
@@ -273,13 +345,27 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
|
||||
return [
|
||||
'answer' => $content,
|
||||
'model_key' => $profile,
|
||||
'model_label' => $modelLabel,
|
||||
'model_name' => $model,
|
||||
'task' => $task,
|
||||
'model_key' => (string) ($prepared['profile'] ?? ''),
|
||||
'model_label' => (string) ($prepared['model_label'] ?? ''),
|
||||
'model_name' => (string) ($prepared['model_name'] ?? ''),
|
||||
'task' => (string) ($prepared['task'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private static function logAssistantFailure(
|
||||
int $diagnosisId,
|
||||
string $profile,
|
||||
int $adminId,
|
||||
\Throwable $exception
|
||||
): void {
|
||||
Log::warning('diagnosis ai assistant upstream call failed', [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'profile' => $profile,
|
||||
'admin_id' => $adminId,
|
||||
'exception_class' => get_class($exception),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接诊台结构化 AI 智能分析。每次只调用客户端白名单键对应的服务端模型,
|
||||
* 上游失败或响应不符合契约时直接失败,不构造本地伪分析。
|
||||
@@ -604,28 +690,10 @@ class DiagnosisAiLogic extends BaseLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
$accessQuery = Diagnosis::where('id', $id)->whereNull('delete_time');
|
||||
$isRoot = !empty($adminInfo['root']) && (int) $adminInfo['root'] === 1;
|
||||
if (!$isRoot) {
|
||||
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
$accessQuery->where('assistant_id', $adminId);
|
||||
}
|
||||
if (DataScopeService::isEnabled()) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return null;
|
||||
}
|
||||
if (is_array($visibleIds)) {
|
||||
$accessQuery->whereIn('assistant_id', $visibleIds);
|
||||
}
|
||||
}
|
||||
if (!MyPatientLogic::canAccessDiagnosis($id, $adminId, $adminInfo)) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return null;
|
||||
}
|
||||
if (!$accessQuery->find()) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return null;
|
||||
}
|
||||
|
||||
$diagnosis = DiagnosisLogic::detail(['id' => $id], $adminInfo);
|
||||
if ($diagnosis === [] || empty($diagnosis['id'])) {
|
||||
|
||||
@@ -28,10 +28,11 @@ use app\common\model\DiagnosisViewRecord;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\doctor\DoctorNoteLogic;
|
||||
use app\adminapi\logic\doctor\AppointmentLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\doctor\DoctorNoteLogic;
|
||||
use app\adminapi\logic\doctor\AppointmentLogic;
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\common\service\ConfigService;
|
||||
use app\common\service\FileService;
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
@@ -4182,32 +4183,10 @@ class DiagnosisLogic extends BaseLogic
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1) 数据权限闸 — 不通过则返回「不存在或无权访问」
|
||||
$accessQuery = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time');
|
||||
|
||||
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
// 医助仅看自己被指派的
|
||||
$accessQuery->where('assistant_id', $adminId);
|
||||
}
|
||||
|
||||
if (DataScopeService::isEnabled()) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
if (is_array($visibleIds)) {
|
||||
$accessQuery->whereIn('assistant_id', $visibleIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$accessQuery->find()) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
// 1) 数据权限闸 — 不通过则返回「不存在或无权访问」
|
||||
if (!self::canViewReadonlyDiagnosis($diagnosisId, $adminId, $adminInfo)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2) 诊单详情(含图片聚合等)+ 字典翻译
|
||||
$diagnosis = self::detail(['id' => $diagnosisId]);
|
||||
@@ -4238,24 +4217,43 @@ class DiagnosisLogic extends BaseLogic
|
||||
$unservedDays = $maxRecordTs > 0 ? max(0, (int) floor((time() - $maxRecordTs) / 86400)) : null;
|
||||
$lastBloodRecordAt = $maxRecordTs > 0 ? date('Y-m-d', $maxRecordTs) : null;
|
||||
|
||||
return [
|
||||
return [
|
||||
'appointment' => $appointment,
|
||||
'diagnosis' => $diagnosis,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
'tracking_notes' => $trackingNotes,
|
||||
'unserved_days' => $unservedDays,
|
||||
'last_blood_record_at' => $lastBloodRecordAt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 readonlyDetail 共用的诊单行级可见性入口。
|
||||
*
|
||||
* 复用“我的患者”统一行权策略:医生按有效接诊关系,医助按归属关系,
|
||||
* 团队管理角色才使用 DataScope。不存在与越权使用同一错误避免枚举。
|
||||
*/
|
||||
public static function canViewReadonlyDiagnosis(int $diagnosisId, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
self::$error = '';
|
||||
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $adminId, $adminInfo)) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取指定日期区间内的三类跟踪记录(血糖血压 / 饮食 / 运动),供 readonlyDetail 与
|
||||
* 医生接诊台 reception 通过独立接口 lazy load。
|
||||
*
|
||||
* 区间语义:闭区间 [startDate, endDate](Y-m-d),均不传则不限。
|
||||
*
|
||||
* @return array{
|
||||
* blood_records: array<int,array<string,mixed>>,
|
||||
* diagnosis_id: int,
|
||||
* blood_records: array<int,array<string,mixed>>,
|
||||
* diet_records: array<int,array<string,mixed>>,
|
||||
* exercise_records: array<int,array<string,mixed>>,
|
||||
* start_date: string,
|
||||
@@ -4267,10 +4265,11 @@ class DiagnosisLogic extends BaseLogic
|
||||
$sinceTs = $startDate !== '' ? (int) strtotime($startDate . ' 00:00:00') : 0;
|
||||
$untilTs = $endDate !== '' ? (int) strtotime($endDate . ' 23:59:59') : 0;
|
||||
$sinceTs = $sinceTs > 0 ? $sinceTs : 0;
|
||||
$untilTs = $untilTs > 0 ? $untilTs : 0;
|
||||
|
||||
return [
|
||||
'blood_records' => self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
$untilTs = $untilTs > 0 ? $untilTs : 0;
|
||||
|
||||
return [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'blood_records' => self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'diet_records' => self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'exercise_records' => self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
|
||||
'start_date' => $startDate,
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\common\model\auth\Admin;
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\doctor\Appointment;
|
||||
use app\common\model\doctor\Medicine as DoctorMedicine;
|
||||
use app\common\model\tcm\Prescription;
|
||||
@@ -934,14 +935,35 @@ class PrescriptionLogic
|
||||
/**
|
||||
* 根据诊单ID获取处方列表
|
||||
*/
|
||||
public static function listByDiagnosis(int $diagnosisId): array
|
||||
{
|
||||
return Prescription::where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
public static function listByDiagnosis(int $diagnosisId, int $viewerAdminId, array $viewerAdminInfo): array
|
||||
{
|
||||
self::$error = '';
|
||||
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $viewerAdminId, $viewerAdminInfo)) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Prescription::where('diagnosis_id', $diagnosisId)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return self::filterViewablePrescriptions($rows, $viewerAdminId, $viewerAdminInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string,mixed>> $rows
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private static function filterViewablePrescriptions(array $rows, int $viewerAdminId, array $viewerAdminInfo): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$rows,
|
||||
static fn (array $row): bool => self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据预约ID获取处方(带权限检查)
|
||||
|
||||
Reference in New Issue
Block a user