Merge branch 'master' into tongji-99

This commit is contained in:
Your Name
2026-05-07 15:43:49 +08:00
16 changed files with 11147 additions and 117 deletions
@@ -82,7 +82,7 @@ class AuthMiddleware
$AdminUris = $adminAuthCache->getAdminUri() ?? [];
$AdminUris = $this->formatUrl($AdminUris);
if (in_array($accessUri, $AdminUris)) {
if (in_array($accessUri, $AdminUris) || $this->matchPermissionAlias($accessUri, $AdminUris)) {
return $next($request);
}
return JsonService::fail('权限不足,无法访问或操作');
@@ -103,6 +103,25 @@ class AuthMiddleware
}, $data);
}
/**
* 日常记录权限域:前端统一收口到 tcm.diagnosis/dailyRecord
* 但待办/跟踪备注接口仍保留历史路由名,故在鉴权层做精确别名映射。
*/
private function matchPermissionAlias(string $accessUri, array $adminUris): bool
{
if (!in_array('tcm.diagnosis/dailyrecord', $adminUris, true)) {
return false;
}
return in_array($accessUri, [
'tcm.diagnosistodo/lists',
'tcm.diagnosistodo/add',
'tcm.diagnosistodo/cancel',
'tcm.diagnosis/trackingnotes',
'tcm.diagnosis/addtrackingnote',
], true);
}
/**
* 面诊进度专用:auth.admin/lists、doctor.appointment/lists + progress_board=1,不校验菜单权限
*/
@@ -118,4 +137,4 @@ class AuthMiddleware
return in_array($c, ['auth.admin', 'doctor.appointment'], true);
}
}
}
@@ -9,6 +9,7 @@ use app\adminapi\logic\tcm\DiagnosisLogic;
use app\common\model\finance\AccountCost;
use app\common\service\DataScope\DataScopeService;
use app\common\service\qywx\MediaChannelService;
use think\db\Query;
use think\facade\Db;
class ConversionLogic
@@ -68,8 +69,6 @@ class ConversionLogic
$entities = self::loadEntities($dimension, $params, $visibleAdminIds);
$entityIds = array_keys($entities);
$allocationEntities = self::loadEntities($dimension, $params, $visibleAdminIds);
$allocationEntityIds = array_keys($allocationEntities);
if ($entityIds === []) {
$result = [
@@ -96,8 +95,9 @@ class ConversionLogic
}
$adminToDeptIds = self::loadAdminDeptMap();
self::hydrateFanStats($allocationEntities, $dimension, $allocationEntityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
self::hydrateFanStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
$allocationEntities = $entities;
$allocationEntityIds = $entityIds;
self::hydrateAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
// 数据隔离:可见部门 = 可见 admin 所属部门并集;用于 account_cost 与下游 cost 分摊。
@@ -257,11 +257,13 @@ class ConversionLogic
?array $adminInfo = null
): array
{
$allowedDeptIds = self::resolveFilterAllowedDeptIds($visibleAdminIds, $eligibleDeptIds);
return [
'departments' => self::buildDepartmentOptions($visibleAdminIds, $eligibleDeptIds),
'departments' => self::buildDepartmentOptions($visibleAdminIds, $eligibleDeptIds, $allowedDeptIds),
'assistants' => DiagnosisLogic::getAssistants($adminId, $adminInfo),
'doctors' => self::buildDoctorOptions($visibleAdminIds),
'media_channels' => self::buildMediaChannelOptions($visibleAdminIds, $eligibleDeptIds),
'media_channels' => self::buildMediaChannelOptions($visibleAdminIds, $eligibleDeptIds, $allowedDeptIds),
];
}
@@ -272,36 +274,19 @@ class ConversionLogic
*
* @return array<int, array<string, mixed>>
*/
private static function buildDepartmentOptions(?array $visibleAdminIds, array $eligibleDeptIds): array
private static function buildDepartmentOptions(?array $visibleAdminIds, array $eligibleDeptIds, ?array $allowedDeptIds = null): array
{
$allTree = DeptLogic::getAllData();
if ($visibleAdminIds === null) {
return $allTree;
}
$allowedDeptIds = [];
if ($visibleAdminIds !== []) {
$deptIds = Db::name('admin_dept')
->whereIn('admin_id', $visibleAdminIds)
->column('dept_id');
foreach ($deptIds as $deptId) {
$deptId = (int)$deptId;
if ($deptId > 0) {
$allowedDeptIds[$deptId] = true;
}
}
}
foreach ($eligibleDeptIds as $deptId) {
$deptId = (int)$deptId;
if ($deptId > 0) {
$allowedDeptIds[$deptId] = true;
}
}
$allowedDeptIds ??= self::resolveFilterAllowedDeptIds($visibleAdminIds, $eligibleDeptIds);
if ($allowedDeptIds === []) {
return [];
}
return self::filterDeptTreeByIds($allTree, array_keys($allowedDeptIds));
return self::filterDeptTreeByIds($allTree, $allowedDeptIds);
}
/**
@@ -340,7 +325,7 @@ class ConversionLogic
*
* @return array<int, array<string, mixed>>
*/
private static function buildMediaChannelOptions(?array $visibleAdminIds, array $eligibleDeptIds): array
private static function buildMediaChannelOptions(?array $visibleAdminIds, array $eligibleDeptIds, ?array $allowedDeptIds = null): array
{
$allOptions = MediaChannelService::getOptions();
if ($visibleAdminIds === null) {
@@ -350,30 +335,13 @@ class ConversionLogic
return $allOptions;
}
$allowedDeptIds = [];
if ($visibleAdminIds !== []) {
$deptIds = Db::name('admin_dept')
->whereIn('admin_id', $visibleAdminIds)
->column('dept_id');
foreach ($deptIds as $deptId) {
$deptId = (int)$deptId;
if ($deptId > 0) {
$allowedDeptIds[$deptId] = true;
}
}
}
foreach ($eligibleDeptIds as $deptId) {
$deptId = (int)$deptId;
if ($deptId > 0) {
$allowedDeptIds[$deptId] = true;
}
}
$allowedDeptIds ??= self::resolveFilterAllowedDeptIds($visibleAdminIds, $eligibleDeptIds);
if ($allowedDeptIds === []) {
return [];
}
$allowedCodes = Db::name('account_cost')
->whereIn('dept_id', array_keys($allowedDeptIds))
->whereIn('dept_id', $allowedDeptIds)
->where('media_channel_code', '<>', '')
->distinct(true)
->column('media_channel_code');
@@ -831,8 +799,14 @@ class ConversionLogic
->leftJoin('admin a', 'a.work_wechat_userid = e.user_id AND a.delete_time IS NULL')
->where('e.change_type', 'add_external_contact')
->where('e.event_time', 'between', [$startTimestamp, $endTimestamp])
->fieldRaw('e.user_id, a.id AS admin_id, COUNT(*) AS add_fans_count')
->group('e.user_id, a.id');
->fieldRaw('a.id AS admin_id, COUNT(*) AS add_fans_count')
->group('a.id');
if ($visibleAdminIds !== null) {
$query->whereIn('a.id', $visibleAdminIds);
} elseif ($dimension !== 'dept' && $entityIds !== []) {
$query->whereIn('a.id', $entityIds);
}
if ($mediaChannel !== null) {
$query->leftJoin('qywx_external_contact q', 'q.external_userid = e.external_userid');
@@ -904,10 +878,16 @@ class ConversionLogic
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->where('a.appointment_date', '>=', $startDate)
->where('a.appointment_date', '<=', $endDate)
->whereRaw('(u.id IS NULL OR u.delete_time IS NULL)')
->fieldRaw("{$sourceExpr} AS source_admin_id, a.patient_id AS diagnosis_id, COUNT(*) AS appointment_count, SUM(CASE WHEN a.status = 3 THEN 1 ELSE 0 END) AS interview_count")
->group("{$sourceExpr}, a.patient_id");
$query->where(static function (Query $subQuery): void {
$subQuery->whereNull('u.id')
->whereOr(static function (Query $orQuery): void {
$orQuery->whereNull('u.delete_time');
});
});
if ($mediaChannel !== null) {
$legacyChannelValues = MediaChannelService::getLegacyAppointmentChannelValues($mediaChannel);
if ($legacyChannelValues !== []) {
@@ -976,7 +956,9 @@ class ConversionLogic
->where('o.status', 2)
->where('o.order_type', 1)
->where('o.amount', 5)
->whereRaw('(o.payment_time IS NOT NULL AND o.payment_time <> "" AND o.payment_time >= ? AND o.payment_time <= ?)', [$startDateTime, $endDateTime])
->whereNotNull('o.payment_time')
->where('o.payment_time', '<>', '')
->whereBetweenTime('o.payment_time', $startDateTime, $endDateTime)
->fieldRaw('o.creator_id AS source_admin_id, COUNT(*) AS paid_appointment_count')
->group('o.creator_id');
@@ -1896,7 +1878,8 @@ class ConversionLogic
// 单条 LIKE 没命中再退化全表(量大时会慢,因此仅在极少数员工场景下兜底)。
$followRows = Db::name('qywx_external_contact')
->whereNull('delete_time')
->whereRaw('follow_users IS NOT NULL AND follow_users <> ""')
->whereNotNull('follow_users')
->where('follow_users', '<>', '')
->limit(2000)
->field('follow_users')
->select()
@@ -2385,4 +2368,38 @@ class ConversionLogic
return round($numerator / $denominator, 2);
}
/**
* @param int[]|null $visibleAdminIds
* @param int[] $eligibleDeptIds
* @return int[]
*/
private static function resolveFilterAllowedDeptIds(?array $visibleAdminIds, array $eligibleDeptIds): array
{
if ($visibleAdminIds === null) {
return array_values(array_unique(array_filter(array_map('intval', $eligibleDeptIds), static fn (int $deptId): bool => $deptId > 0)));
}
$allowedDeptIds = [];
if ($visibleAdminIds !== []) {
$deptIds = Db::name('admin_dept')
->whereIn('admin_id', $visibleAdminIds)
->column('dept_id');
foreach ($deptIds as $deptId) {
$deptId = (int)$deptId;
if ($deptId > 0) {
$allowedDeptIds[$deptId] = $deptId;
}
}
}
foreach ($eligibleDeptIds as $deptId) {
$deptId = (int)$deptId;
if ($deptId > 0) {
$allowedDeptIds[$deptId] = $deptId;
}
}
return array_values($allowedDeptIds);
}
}
@@ -31,7 +31,8 @@ use app\common\model\tcm\DiagnosisTodo;
* cancel() → status=2 (已取消,仅 status=0 可取消)
*
* 鉴权:
* add:诊单需存在;数据权限交由路由层「tcm.diagnosisTodo/add」节点 + 前端 tab disabled 控制
* add:诊单需存在;数据权限交由路由层统一「tcm.diagnosis/dailyRecord」权限域控制
* 并兼容历史 diagnosisTodo/* 接口路由。
* cancel:仅创建人本人或超级管理员 (role_id=1) 可取消,且仅 status=0 时。
*
* @package app\adminapi\logic\tcm
@@ -152,6 +152,19 @@ class PrescriptionLogic
return $ts ? date('Y-m-d', $ts) : date('Y-m-d');
}
private static function normalizeDosageBagCount($raw): int
{
$count = (int) $raw;
if ($count < 1) {
return 1;
}
if ($count > 5) {
return 5;
}
return $count;
}
/**
* 同一诊单、同一开方人、同一处方日期:仅允许一张「未作废」的未删除记录;已作废的可再新开一张。
*
@@ -230,6 +243,7 @@ class PrescriptionLogic
'prescription_type' => $params['prescription_type'] ?? '浓缩水丸',
'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : null,
'dosage_unit' => $params['dosage_unit'] ?? '',
'dosage_bag_count' => self::normalizeDosageBagCount($params['dosage_bag_count'] ?? 1),
'need_decoction' => (int)($params['need_decoction'] ?? 0),
'bags_per_dose' => isset($params['bags_per_dose']) ? (int)$params['bags_per_dose'] : 1,
'diagnosis_id' => (int)($params['diagnosis_id'] ?? 0),
@@ -359,6 +373,7 @@ class PrescriptionLogic
'prescription_type' => $params['prescription_type'] ?? $prescription->prescription_type,
'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : $prescription->dosage_amount,
'dosage_unit' => $params['dosage_unit'] ?? $prescription->dosage_unit,
'dosage_bag_count' => self::normalizeDosageBagCount($params['dosage_bag_count'] ?? ($prescription->dosage_bag_count ?? 1)),
'need_decoction' => isset($params['need_decoction']) ? (int)$params['need_decoction'] : (int)($prescription->need_decoction ?? 0),
'bags_per_dose' => isset($params['bags_per_dose']) ? (int)$params['bags_per_dose'] : (int)($prescription->bags_per_dose ?? 1),
'patient_name' => $params['patient_name'] ?? $prescription->patient_name,
@@ -811,6 +826,7 @@ class PrescriptionLogic
'prescription_type' => '浓缩水丸',
'dosage_amount' => 1.0,
'dosage_unit' => 'g',
'dosage_bag_count' => 1,
'need_decoction' => 0,
'bags_per_dose' => 1,
'diagnosis_id' => $diagnosisId,
@@ -12,6 +12,7 @@ class PrescriptionValidate extends BaseValidate
'id' => 'require',
'diagnosis_id' => 'number',
'appointment_id' => 'number',
'dosage_bag_count' => 'integer|between:1,5',
'patient_name' => 'require',
'clinical_diagnosis' => 'require',
'herbs' => 'require|array',
@@ -20,6 +21,8 @@ class PrescriptionValidate extends BaseValidate
];
protected $message = [
'dosage_bag_count.integer' => '用量袋数必须为整数',
'dosage_bag_count.between' => '用量袋数必须在1到5袋之间',
'patient_name.require' => '患者姓名不能为空',
'clinical_diagnosis.require' => '临床诊断不能为空',
'herbs.require' => '请添加中药',
@@ -28,7 +31,7 @@ class PrescriptionValidate extends BaseValidate
public function sceneAdd()
{
return $this->only([
'prescription_type', 'dosage_amount', 'dosage_unit', 'need_decoction', 'bags_per_dose',
'prescription_type', 'dosage_amount', 'dosage_unit', 'dosage_bag_count', 'need_decoction', 'bags_per_dose',
'patient_name', 'gender', 'age',
'visit_no', 'prescription_date', 'tongue', 'tongue_image', 'pulse',
'pulse_condition', 'clinical_diagnosis', 'herbs', 'dose_count', 'dose_unit',
@@ -41,7 +44,7 @@ class PrescriptionValidate extends BaseValidate
public function sceneEdit()
{
return $this->only([
'id', 'prescription_type', 'dosage_amount', 'dosage_unit', 'need_decoction', 'bags_per_dose',
'id', 'prescription_type', 'dosage_amount', 'dosage_unit', 'dosage_bag_count', 'need_decoction', 'bags_per_dose',
'patient_name', 'gender', 'age',
'visit_no', 'prescription_date', 'tongue', 'tongue_image', 'pulse',
'pulse_condition', 'clinical_diagnosis', 'herbs', 'dose_count', 'dose_unit',
@@ -24,6 +24,7 @@ class Prescription extends BaseModel
// 字段类型转换
protected $type = [
'dosage_amount' => 'float',
'dosage_bag_count' => 'integer',
'need_decoction' => 'integer',
];
@@ -0,0 +1,17 @@
# 2026-05-07
# (perms=tcm.diagnosis/lists)
# tcm.diagnosis/dailyRecord ////
SET @diag_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.diagnosis/lists' LIMIT 1);
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@diag_menu_id, 'A', '日常记录', '', 59,
'tcm.diagnosis/dailyRecord', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @diag_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.diagnosis/dailyRecord');
@@ -0,0 +1,3 @@
-- 添加处方用量袋数字段(浓缩水丸等开方场景,1-5袋)
ALTER TABLE `zyt_tcm_prescription`
ADD COLUMN `dosage_bag_count` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '用量袋数(1-5袋)' AFTER `dosage_unit`;