diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 572b18ce..d8995271 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -931,31 +931,75 @@ - - +
+
+ 主方: + + +
+
+ 辅方: + + +
+
-
- 每天次数: - {{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }} -
-
- 处方开立: - {{ - detailPrescription.usage_days != null && detailPrescription.usage_days !== '' - ? detailPrescription.usage_days + ' 天' - : '—' - }} -
+ +
订单设置: {{ @@ -2426,9 +2470,9 @@
-

主方服法:{{ rxUsageText }}

+

主服法:{{ rxUsageText }}

服法:{{ rxUsageText }}

-

辅方服法:{{ rxAuxUsageText }}

+

辅服法:{{ rxAuxUsageText }}

医嘱:{{ rxAdviceText }}

备注:{{ rxRemarkText }}

@@ -3856,6 +3900,20 @@ const detailLinkedAppointmentChannelText = computed(() => { const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs)) +/** 详情:是否含辅方药材(用于决定是否展示辅方用量 / 用法) */ +const detailHasAuxHerbs = computed(() => { + const herbs = detailPrescription.value?.herbs + if (!Array.isArray(herbs)) return false + return herbs.some((h: any) => normalizeSlipFormulaType(h?.formula_type) === '辅方') +}) + +/** 详情:辅方用法(aux_usage JSON 经规范化后的对象,与处方类型一致) */ +const detailAuxUsage = computed(() => { + const rx = detailPrescription.value as any + if (!rx) return null + return normalizeSlipAuxUsageForm(rx.aux_usage, rx.prescription_type || '浓缩水丸') +}) + /** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */ const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false) @@ -5571,6 +5629,22 @@ const slipPillGrams = computed(() => { return days * times * doseG * bags }) +/** 辅方出丸克数:仅含辅方药材且为浓缩水丸时计算;公式同主方,用 aux_usage 字段 */ +const slipAuxPillGrams = computed(() => { + const d = prescriptionViewData.value as any + if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null + if (!slipAuxHerbs.value.length) return null + const aux = normalizeSlipAuxUsageForm(d.aux_usage, d.prescription_type || '浓缩水丸') + /** 辅方疗程优先用业务订单 medication_days(与主方共享一次配药周期),缺省回退辅方 usage_days */ + const days = Number(d.medication_days ?? aux.usage_days) + const times = Number(aux.times_per_day) + const doseG = Number(aux.dosage_amount) + const bags = Number(aux.dosage_bag_count) > 0 ? Number(aux.dosage_bag_count) : 1 + if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null + if (days <= 0 || times <= 0 || doseG <= 0) return null + return days * times * doseG * bags +}) + /* ============================================================ * A4 处方笺(药房联)相关计算 —— 与 consumer/prescription/index.vue 保持一致 * ============================================================ */ @@ -5684,10 +5758,14 @@ const rxAuxUsageText = computed(() => { ) }) -/** 出丸:优先用 slipPillGrams(每袋用量×袋数×每天次数×服用天数),否则按药材总量×剂数 */ +/** 出丸:优先用 slipPillGrams / slipAuxPillGrams(含辅方时相加),否则按药材总量×剂数 */ const rxOutPelletGrams = computed(() => { - const pill = slipPillGrams.value - if (typeof pill === 'number' && isFinite(pill) && pill > 0) return formatNum(pill) + const main = slipPillGrams.value + const aux = slipAuxPillGrams.value + const mainNum = typeof main === 'number' && isFinite(main) && main > 0 ? main : 0 + const auxNum = typeof aux === 'number' && isFinite(aux) && aux > 0 ? aux : 0 + const sum = mainNum + auxNum + if (sum > 0) return formatNum(sum) const v = prescriptionViewData.value as any if (!v) return '' const explicit = v.out_pellet || v.total_weight @@ -5723,7 +5801,17 @@ const rxPharmacyRemarkText = computed(() => { const rxOutPelletText = computed(() => { const out = rxOutPelletGrams.value - return out ? `${out}克` : '' + if (!out) return '' + /** 含辅方时附带「主方 X克 + 辅方 Y克」明细,便于药房核对 */ + const main = slipPillGrams.value + const aux = slipAuxPillGrams.value + if ( + typeof main === 'number' && isFinite(main) && main > 0 && + typeof aux === 'number' && isFinite(aux) && aux > 0 + ) { + return `${out}克(主方 ${formatNum(main)}克 + 辅方 ${formatNum(aux)}克)` + } + return `${out}克` }) function formatNum(n: number): string { diff --git a/server/app/adminapi/logic/stats/YejiStatsLogic.php b/server/app/adminapi/logic/stats/YejiStatsLogic.php index 3973c6fd..df9f3672 100755 --- a/server/app/adminapi/logic/stats/YejiStatsLogic.php +++ b/server/app/adminapi/logic/stats/YejiStatsLogic.php @@ -1437,6 +1437,16 @@ class YejiStatsLogic * @param int[] $deptIdsForAdmin * @param array $tableRowSet * @param array> $deptById + * + * 多链路 admin 的归属选择:按**原始 admin_dept 链路的深度**择优(更深 = 更具体的岗位归属), + * 而非按解析出的表格行深度。否则同层中心兄弟节点在父视图(如 [郑州一中心, 郑州二中心])会 + * 以 id 大小决胜,而子视图(钻取到某中心后行集变为其子组)则只剩唯一候选——同一条挂号在两个 + * 视图被路由到不同行,导致 admin「已完成挂号」父行 < 子行合计。 + * + * 向下兜底(resolveFirstTableRowUnderDept)按「链路深度 ≥ 最浅表格行深度 - 1」收窄: + * 仅当链路本身就是用户筛选根(与表格行同层或刚刚高一层)时才允许向下归到首个子表格行; + * 否则像「只挂在甄养堂(root) / 一中心」等过宽链路,会在不同视图被任意攀附到第一个子节点, + * 同一挂号父视图归到第一个中心、子视图归到该中心第一个子组 → 父子合计错位。 */ private static function mapAdminDeptLinksToTableRow(array $deptIdsForAdmin, array $tableRowDeptIds, array $tableRowSet, array $deptById): int { @@ -1449,7 +1459,7 @@ class YejiStatsLogic } $hit = self::resolveDeptToNearestTableRowUpward($d, $tableRowSet, $deptById); if ($hit > 0) { - $depth = self::deptDepthFromRoot($hit, $deptById); + $depth = self::deptDepthFromRoot($d, $deptById); if ($depth > $bestDepth || ($depth === $bestDepth && ($best === 0 || $hit < $best))) { $best = $hit; $bestDepth = $depth; @@ -1459,11 +1469,22 @@ class YejiStatsLogic if ($best > 0) { return $best; } + $minTableRowDepth = PHP_INT_MAX; + foreach ($tableRowDeptIds as $tid) { + $td = self::deptDepthFromRoot((int) $tid, $deptById); + if ($td > 0 && $td < $minTableRowDepth) { + $minTableRowDepth = $td; + } + } + $linkDepthFloor = $minTableRowDepth === PHP_INT_MAX ? PHP_INT_MAX : ($minTableRowDepth - 1); foreach ($deptIdsForAdmin as $d) { $d = (int) $d; if ($d <= 0) { continue; } + if (self::deptDepthFromRoot($d, $deptById) < $linkDepthFloor) { + continue; + } $hit = self::resolveFirstTableRowUnderDept($d, $tableRowDeptIds, $deptById); if ($hit > 0) { return $hit; @@ -1618,10 +1639,31 @@ class YejiStatsLogic $aids[] = $d; } } + $aids = array_values(array_unique($aids)); $adminToTable = ($tableRowDeptIds !== [] && $deptById !== []) ? self::batchMapPerformanceAdminsToTableDept($aids, $tableRowDeptIds, $deptById) : []; + /** + * 视图无关的「该 admin 是否有 admin_dept 归属」。 + * 用途:仅当 admin 完全无部门归属时才允许由"医助"回退到"接诊医生"。 + * 否则父视图能解析到 admin 的中心行 → 计入 assistant;子视图(钻取另一中心) + * adminToTable / adminToPrimary 双双 0 时会回退到 doctor,造成同一挂号在父视图归 assistant、 + * 在子视图归 doctor,父行 < 子行合计。 + * + * @var array $adminHasAnyDept + */ + $adminHasAnyDept = []; + if ($aids !== []) { + $linkedAids = Db::name('admin_dept')->whereIn('admin_id', $aids)->column('admin_id'); + foreach ($linkedAids as $lid) { + $lid = (int) $lid; + if ($lid > 0) { + $adminHasAnyDept[$lid] = true; + } + } + } + $byDept = []; foreach ($rows as $r) { $effId = (int) ($r['eff_a'] ?? 0); @@ -1657,6 +1699,14 @@ class YejiStatsLogic $primary = $p; break; } + /** + * 视图无关地有部门归属,仅是当前视图行集 / primaryDeptIds 不含 → + * 视为该 admin 的归属(视图外不可见),不再回退到下一候选(医生), + * 以避免父子视图归属分裂。narrowTotalsToTable 视图下 unmapped 会被自动隐藏。 + */ + if (isset($adminHasAnyDept[$aid])) { + break; + } } if ($primary > 0) { @@ -4058,7 +4108,14 @@ class YejiStatsLogic if ($candidates === []) { return 0; } - /** @var int[] $matching */ + /** + * 每个命中的表格行追加 ['rid'=>表格行 id, 'link_depth'=>原始 admin_dept 链路深度]。 + * 用链路深度(而非表格行深度)择优,保证父视图(同层中心兄弟节点行集)与子视图(钻取单中心后行集为其子组) + * 对同一 admin 的归属选择一致——否则同 admin 在父行被路由到「错」的中心,在子行被路由到正确中心子组, + * 表现为父行计数 < 子行合计。 + * + * @var array $matching + */ $matching = []; foreach ($tableRowDeptIds as $rid) { $rid = (int) $rid; @@ -4069,6 +4126,7 @@ class YejiStatsLogic if ($flip === []) { continue; } + $bestLinkDepth = -1; foreach ($candidates as $aid => $_) { $aid = (int) $aid; if ($aid <= 0) { @@ -4077,11 +4135,16 @@ class YejiStatsLogic foreach ($adminDeptIdList[$aid] ?? [] as $d) { $d = (int) $d; if ($d > 0 && isset($flip[$d])) { - $matching[] = $rid; - break 2; + $linkDepth = self::deptDepthFromRoot($d, $deptById); + if ($linkDepth > $bestLinkDepth) { + $bestLinkDepth = $linkDepth; + } } } } + if ($bestLinkDepth >= 0) { + $matching[] = ['rid' => $rid, 'link_depth' => $bestLinkDepth]; + } } if ($matching === []) { foreach ($candidates as $aid => $_) { @@ -4099,8 +4162,9 @@ class YejiStatsLogic } $best = 0; $bestDepth = -1; - foreach ($matching as $rid) { - $depth = self::deptDepthFromRoot($rid, $deptById); + foreach ($matching as $m) { + $rid = $m['rid']; + $depth = $m['link_depth']; if ($depth > $bestDepth || ($depth === $bestDepth && ($best === 0 || $rid < $best))) { $best = $rid; $bestDepth = $depth;