This commit is contained in:
Your Name
2026-07-09 17:58:02 +08:00
parent a650747fd3
commit f5a89b4310
290 changed files with 478 additions and 332 deletions
@@ -1453,7 +1453,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
->column('revisit_slot_start_offset', 'id');
$offsetMap = [];
foreach ($offsetRows as $id => $offset) {
$offsetMap[(int) $id] = max(1, min(20, (int) $offset));
$offsetMap[(int) $id] = max(0, min(20, (int) $offset));
}
/** @var array<int, int> $seqByOrderId order_id => global seq within diagnosis */
@@ -1475,7 +1475,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$oid = (int) ($row['id'] ?? 0);
$did = (int) ($row['diagnosis_id'] ?? 0);
$seq = (int) ($seqByOrderId[$oid] ?? 0);
$offset = (int) ($offsetMap[$did] ?? 1);
$offset = (int) ($offsetMap[$did] ?? 0);
$effectiveSlot = $seq > 0 ? $seq + $offset : 0;
$row['global_visit_seq'] = $effectiveSlot > 0 ? $effectiveSlot : null;
$row['raw_visit_seq'] = $seq > 0 ? $seq : null;
+32 -1
View File
@@ -151,6 +151,24 @@ class DeptLogic extends BaseLogic
* @return list<int>
*/
private static function findErCenterRootDeptIds(): array
{
return self::findCenterRootDeptIdsByNameKeyword('二中心');
}
/**
* 名称含「一中心」的部门 id。
*
* @return list<int>
*/
private static function findYiCenterRootDeptIds(): array
{
return self::findCenterRootDeptIdsByNameKeyword('一中心');
}
/**
* @return list<int>
*/
private static function findCenterRootDeptIdsByNameKeyword(string $keyword): array
{
$rows = Dept::whereNull('delete_time')
->field(['id', 'name'])
@@ -159,7 +177,7 @@ class DeptLogic extends BaseLogic
$out = [];
foreach ($rows as $r) {
$name = (string) ($r['name'] ?? '');
if ($name !== '' && mb_strpos($name, '二中心') !== false) {
if ($name !== '' && mb_strpos($name, $keyword) !== false) {
$out[] = (int) $r['id'];
}
}
@@ -201,6 +219,19 @@ class DeptLogic extends BaseLogic
return array_fill_keys($subtreeIds, true);
}
/**
* 名称含「一中心」的部门及其全部下级 id(map)。
*
* @return array<int, true>
*/
public static function getYiCenterSubtreeDeptIdSet(): array
{
$yiRoots = self::findYiCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($yiRoots);
return array_fill_keys($subtreeIds, true);
}
/**
* 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。
*
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\adminapi\logic\dept\DeptLogic;
use think\facade\Db;
/**
@@ -12,10 +13,11 @@ use think\facade\Db;
* 口径说明:
* - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0
* **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合;
* 同一诊单当月被多次指派给同一医助只计 1 次
* 同一诊单当月被多次指派给同一医助只计 1 次
* **再剔除**名下存在履约「拒收(9) / 退款(10)」业务订单(未软删)的诊单。
* - 第 N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔;
* 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。
* 诊单可配置 `revisit_slot_start_offset`(默认 1:第 1 笔实单计为诊;设为 2 则第 1 笔实单计为三诊,即在实单序号上叠加偏移,5 笔实单等价于计至七诊)。
* 诊单可配置 `revisit_slot_start_offset`(默认 0:第 1 笔实单计为诊;设为 1 则第 1 笔实单计为二诊;设为 2 则计为三诊,即在实单序号上叠加偏移,5 笔实单+偏移 2 等价于计至七诊)。
* - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**——
* 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属;
* 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。
@@ -23,7 +25,8 @@ use think\facade\Db;
* 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。
* - 分档动态产出:N 从 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据),
* 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;部门筛选(dept_ids,含组织下级)按该归属部门过滤。
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;**仅统计「二中心」及其组织下级**(与 DeptLogic::getErCenterSubtreeDeptIdSet 一致);
* 部门筛选下拉与未选部门时的默认范围均限定在该子树内,选定部门时含其组织下级。
* - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。
*/
class RevisitRateLogic
@@ -295,14 +298,19 @@ class RevisitRateLogic
}
/**
* 部门下拉(全量未删除部门,前端组树)。
* 部门下拉:仅「二中心」及其组织下级(与业绩看板 ErCenter 子树一致)。
*
* @return array{rows: list<array{id:int,pid:int,name:string}>}
*/
public static function deptOptions(): array
{
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erSet === []) {
return ['rows' => []];
}
$rows = Db::name('dept')
->whereNull('delete_time')
->whereIn('id', array_keys($erSet))
->field(['id', 'pid', 'name'])
->order('sort', 'desc')
->order('id', 'asc')
@@ -323,9 +331,9 @@ class RevisitRateLogic
/**
* 核心统计上下文:
* 1. 全量指派日志(≤ 月末)构建持有时间线;
* 2. 分母:当月非继承指派的「医助 × 诊单」;
* 3. 分子:曾被指派诊单的当月订单,统计诊次 = 实单序号 + 诊单偏移(默认第 1 笔实单为诊);
* 4. 应用部门筛选(含组织下级)。
* 2. 分母:当月非继承指派的「医助 × 诊单」,再剔除名下存在拒收(9)/退款(10) 订单的诊单
* 3. 分子:曾被指派诊单的当月订单,统计诊次 = 实单序号 + 诊单偏移(默认第 1 笔实单为诊);
* 4. 应用部门筛选:默认限定「二中心」子树;选定部门时再收窄到该部门及其下级(且须落在二中心子树内)。
*
* @param array{month?:string,dept_ids?:int[]|string} $params
*
@@ -376,7 +384,32 @@ class RevisitRateLogic
}
}
// 分子:曾被指派诊单的当月订单,统计诊次 = 实单全局序号 + 诊单偏移(默认偏移 1 → 第 1 笔实单为二诊
// 分母:剔除名下存在拒收(9)/退款(10) 业务订单的诊单(与明细 assignLines 同口径
$assignedDiagIds = [];
foreach ($diagsByAssistant as $diagSet) {
foreach ($diagSet as $did => $_) {
$assignedDiagIds[(int) $did] = true;
}
}
$refundRejectDiagSet = self::fetchRefundOrRejectDiagnosisSet(array_keys($assignedDiagIds));
if ($refundRejectDiagSet !== []) {
foreach ($diagsByAssistant as $aid => $diagSet) {
foreach ($diagSet as $did => $_) {
if (isset($refundRejectDiagSet[$did])) {
unset($diagsByAssistant[$aid][$did]);
}
}
if ($diagsByAssistant[$aid] === []) {
unset($diagsByAssistant[$aid]);
}
}
$pairsRaw = array_values(array_filter(
$pairsRaw,
static fn (array $p): bool => !isset($refundRejectDiagSet[(int) $p['diagnosis_id']])
));
}
// 分子:曾被指派诊单的当月订单,统计诊次 = 实单全局序号 + 诊单偏移(默认偏移 0 → 第 1 笔实单为一诊)
/** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */
$slotOrdersByAssistant = [];
$offsetMap = self::fetchRevisitSlotStartOffsetMap(array_keys($candidateDiagSet));
@@ -389,7 +422,7 @@ class RevisitRateLogic
$seq = 0;
$ptr = 0;
$holder = 0;
$offset = 1;
$offset = 0;
foreach ($orderRows as $r) {
$did = (int) ($r['diagnosis_id'] ?? 0);
if ($did <= 0) {
@@ -424,12 +457,15 @@ class RevisitRateLogic
}
}
// 医助归属部门 + 部门筛选(含组织下级)
// 医助归属部门 + 部门筛选(默认仅二中心子树;选定部门时再收窄,含组织下级)
$universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant);
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds);
$deptFilterIds = self::parseDeptIds($params['dept_ids'] ?? null);
if ($deptFilterIds !== []) {
$subtreeSet = self::expandDeptSubtreeSet($deptFilterIds);
$subtreeSet = self::resolveDeptFilterSet($params['dept_ids'] ?? null);
if ($subtreeSet === []) {
// 无二中心部门时整表为空,避免误展示其它中心数据
$diagsByAssistant = [];
$slotOrdersByAssistant = [];
} else {
foreach ($universeIds as $aid) {
$deptId = (int) ($assistantDept[$aid] ?? 0);
if ($deptId <= 0 || !isset($subtreeSet[$deptId])) {
@@ -548,6 +584,45 @@ class RevisitRateLogic
return [$canonical, $names];
}
/**
* 部门筛选集合:始终落在「二中心」子树内。
* - 未传 dept_ids:整棵二中心子树
* - 已传:所选部门及其下级 ∩ 二中心子树(非法/非二中心 id 被忽略)
*
* @param mixed $raw
*
* @return array<int, true>
*/
private static function resolveDeptFilterSet(mixed $raw): array
{
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erSet === []) {
return [];
}
$deptFilterIds = self::parseDeptIds($raw);
if ($deptFilterIds === []) {
return $erSet;
}
$allowedRoots = [];
foreach ($deptFilterIds as $id) {
if (isset($erSet[$id])) {
$allowedRoots[] = $id;
}
}
if ($allowedRoots === []) {
return [];
}
$expanded = self::expandDeptSubtreeSet($allowedRoots);
$out = [];
foreach ($expanded as $id => $_) {
if (isset($erSet[$id])) {
$out[$id] = true;
}
}
return $out;
}
/**
* @param mixed $raw int[] | 逗号分隔字符串
*
@@ -601,6 +676,35 @@ class RevisitRateLogic
return $set;
}
/**
* 名下存在履约「拒收(9) / 退款(10)」业务订单(未软删)的诊单集合。
* 用于「当月被指派总数」分母过滤;不限订单创建月份。
*
* @param list<int> $diagIds
*
* @return array<int, true>
*/
private static function fetchRefundOrRejectDiagnosisSet(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$out = [];
foreach (array_chunk($diagIds, 2000) as $chunk) {
$ids = Db::name('tcm_prescription_order')
->whereIn('diagnosis_id', $chunk)
->whereNull('delete_time')
->whereIn('fulfillment_status', [9, 10])
->group('diagnosis_id')
->column('diagnosis_id');
foreach ($ids as $id) {
$out[(int) $id] = true;
}
}
return $out;
}
/**
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
*
@@ -649,15 +753,15 @@ class RevisitRateLogic
}
/**
* 诊单复诊统计起始偏移(默认 1:第 1 笔实单计为诊;统计诊次 = 实单序号 + 偏移)
* 诊单复诊统计起始偏移(默认 0:第 1 笔实单计为诊;统计诊次 = 实单序号 + 偏移)
*
* @param array<int, int> $offsetMap
*/
private static function resolveRevisitSlotStartOffset(int $diagId, array $offsetMap): int
{
$offset = (int) ($offsetMap[$diagId] ?? 1);
if ($offset < 1) {
$offset = 1;
$offset = (int) ($offsetMap[$diagId] ?? 0);
if ($offset < 0) {
$offset = 0;
}
if ($offset > 20) {
$offset = 20;
@@ -759,8 +759,8 @@ class DiagnosisLogic extends BaseLogic
return false;
}
if ($offset < 1 || $offset > 20) {
self::setError('起始偏移须在 120 之间');
if ($offset < 0 || $offset > 20) {
self::setError('起始偏移须在 020 之间');
return false;
}
@@ -770,9 +770,12 @@ class DiagnosisLogic extends BaseLogic
return false;
}
$old = (int) ($diagnosis->revisit_slot_start_offset ?? 1);
if ($old < 1) {
$old = 1;
$old = (int) ($diagnosis->revisit_slot_start_offset ?? 0);
if ($old < 0) {
$old = 0;
}
if ($old > 20) {
$old = 20;
}
if ($old === $offset) {
return true;
@@ -44,7 +44,7 @@ class DiagnosisValidate extends BaseValidate
'end_date' => 'date|checkDateRange',
'diagnosis_id' => 'require|integer|checkDiagnosisId',
'tracking_content' => 'require|length:1,1000',
'revisit_slot_start_offset' => 'integer|between:1,20',
'revisit_slot_start_offset' => 'integer|between:0,20',
];
protected $message = [
@@ -144,7 +144,7 @@ class DiagnosisValidate extends BaseValidate
public function sceneSetRevisitSlotStartOffset()
{
return $this->only(['id', 'revisit_slot_start_offset'])
->append('revisit_slot_start_offset', 'require|integer|between:1,20');
->append('revisit_slot_start_offset', 'require|integer|between:0,20');
}
protected function checkDiagnosis($value)