Compare commits

..
5 Commits
Author SHA1 Message Date
Your Name 0c1709d589 Merge branch 'master' into yeji-5-11 2026-05-12 10:43:04 +08:00
Your Name a3892001d3 更新 2026-05-12 10:42:14 +08:00
Your Name 7add364e20 更新 2026-05-11 17:48:47 +08:00
Your Name 0ebea74fde 更新 2026-05-11 17:46:29 +08:00
Your Name 15cf6324f6 更新 2026-05-11 16:10:06 +08:00
14 changed files with 3559 additions and 197 deletions
+57
View File
@@ -103,3 +103,60 @@ export function doctorDailyStatsOverview(params: {
}) {
return request.get({ url: '/stats.doctorDailyStats/overview', params })
}
/** 提成结算接口计算较重,单独放宽超时(默认多为 60s) */
const COMMISSION_SETTLEMENT_TIMEOUT_MS = 120000
/** 提成结算业绩(独立于业绩看板 yejiStats) */
export function commissionSettlementOverview(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
}) {
return request.get(
{ url: '/stats.commissionSettlement/overview', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
export function commissionSettlementDeptOptions() {
return request.get({ url: '/stats.commissionSettlement/deptOptions' })
}
export function commissionSettlementChannelOptions() {
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
}
/** 提成核对:订单明细分页 bucket: 空|current|deferred */
export function commissionSettlementOrderLines(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
page?: number
page_size?: number
bucket?: string
}) {
return request.get(
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
export function commissionSettlementConfirmStatus(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
}) {
return request.get({ url: '/stats.commissionSettlement/confirmStatus', params })
}
export function commissionSettlementSaveReconcile(params: Record<string, any>) {
return request.post({ url: '/stats.commissionSettlement/saveReconcile', params })
}
export function commissionSettlementConfirmFinalize(params: Record<string, any>) {
return request.post(
{ url: '/stats.commissionSettlement/confirmFinalize', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
File diff suppressed because it is too large Load Diff
+26 -6
View File
@@ -290,12 +290,12 @@
</table>
</div>
<div class="yeji-table__foot yeji-table__foot--left">
<span>诊金单位 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线/进线</span>
<span>诊金单位 · 诊金/接诊诊单/复诊均按订单创建人 = 该医助归属与部门合计业绩 / 接诊诊单 / 复诊同口径 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线/进线</span>
<span v-if="lb.er_center_subtree">
· 复诊列为二中心口径同一诊单业务单序列第 2 笔起点击数字查看对应业务订单
</span>
<span>
· 预约诊单预约日期在统计区间内状态含已预约/已完成/已过号不含已取消医助归属与接诊单数一致各科组展示全部医助
· 预约诊单预约日期在统计区间内状态含已预约/已完成/已过号不含已取消医助归属与接诊单数一致按挂号 assistant_id 优先否则诊单 assistant_id各科组展示全部医助
</span>
</div>
</div>
@@ -370,7 +370,7 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
<b>名称含二中心</b>的部门及其组织下级与本表区间一致按患者业务单 <b>create_time</b> 排序<b> 1 笔不计</b> 2 笔起计入合计分列复诊2复诊三含本部门及下级汇总订单条件同合计业绩非取消非二中心子树部门为 0
<b>名称含二中心</b>的部门及其组织下级候选订单为有诊单医助的非取消业务单按患者 <b>create_time</b> 排序<b> 1 笔不计</b> 2 笔起计入合计分列复诊2复诊三<b>归属</b>按订单创建人的人事部门<b>合计业绩</b>列同口径非二中心子树部门为 0
</div>
</template>
<el-icon class="col-info"><InfoFilled /></el-icon>
@@ -395,8 +395,13 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
<<<<<<< HEAD
按订单<b>创建时间</b>统计剔除履约已取消金额与<b>接诊诊单</b>列一致仅按<b>订单创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<b>医助排行榜诊金</b>同口径<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表对齐<br />
=======
按订单<b>创建时间</b>统计剔除履约已取消金额与<b>接诊诊单</b>列一致<b>创建人</b>人事部门优先无创建人时回退诊单<b>医助</b>落在本展示行含下级即计入同表多行命中时取最深的展示部门<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表可对齐<br />
>>>>>>> master
点击金额可查看对应业务订单明细侧栏默认合计业绩全量列表
</div>
</template>
@@ -445,8 +450,12 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
<<<<<<< HEAD
<b>业务订单条数</b>非取消订单 <b>create_time</b> 落入区间<b>fulfillment_status 4</b><b>订单创建人</b>的人事部门落在该部门子树即计入表格多行命中时取最深的展示部门<b>合计业绩 / 医助排行榜接诊诊单</b>同口径选定渠道时本列仍为全量
=======
<b>业务订单条数</b>非取消订单 <b>create_time</b> 落入区间<b>fulfillment_status 4</b>与列表筛选
<b>assistant_dept_id</b> 时一致<b>创建人</b>人事部门优先无创建人则诊单 <b>医助</b>落在该部门子树即计入表格多行命中时取最深的展示部门与侧栏勾选与表格业绩对齐时的集合可能略有差异选定渠道时本列仍为全量
>>>>>>> master
</div>
</template>
<el-icon class="col-info"><InfoFilled /></el-icon>
@@ -2638,16 +2647,23 @@ function buildOrderDrawerListParams(): Record<string, any> | null {
end_time: `${f.end} 23:59:59`,
/** 与业绩列口径一致:不列出履约已取消(4) */
exclude_fulfillment_cancelled: 1,
/** 侧栏展示约诊数等 extend 字段 */
/**
* 业绩看板入口标志:后端 PrescriptionOrderLists::applyDoctorAssistantFilters 看到此标志后,
* assistant_id / assistant_dept_id 一律按订单创建人收窄(与表格诊金/合计业绩/复诊同口径),
* 而非默认的「创建人 ∪ 诊单医助」并集。同时启用 extend 字段(约诊数、合计业绩对照)。
*/
yeji_order_drawer: 1,
}
if (f.mode === 'dept') {
params.assistant_dept_id = f.deptId
<<<<<<< HEAD
=======
/** 与 YejiStatsLogic 最深展示行 + 创建人优先归属一致 */
if (f.yejiTableRowDeptIds && f.yejiTableRowDeptIds.length > 0) {
params.yeji_table_row_dept_ids = f.yejiTableRowDeptIds.join(',')
}
/** 勿传 yeji_drawer_match_table_performance:该参数按「业绩归因」医助收窄,与看板列表摊行不一致 */
>>>>>>> master
if (selectedDeptIds.value.length > 0) {
params.dept_ids = selectedDeptIds.value.join(',')
}
@@ -2756,7 +2772,11 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: '
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
}
orderDrawerTitle.value = `业务订单 · ${row.dept_name}`
<<<<<<< HEAD
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约已取消`
=======
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「接诊诊单」同口径:**创建人**人事部门优先,无创建人则诊单医助;同表多行间取最深归属 · 不含履约已取消`
>>>>>>> master
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2837,7 +2857,7 @@ function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['row
end,
}
orderDrawerTitle.value = `业务订单 · ${row.name}`
orderDrawerHint.value = `创建时间:${start} ${end} · 诊单医助 · 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约已取消`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2861,7 +2881,7 @@ function openOrderDrawerByAssistantErCenterRevisit(
}
const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
orderDrawerTitle.value = `业务订单 · ${assistant.name} · ${slotLbl}`
orderDrawerHint.value = `创建时间:${start} ${end} · 诊单医助 · 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -143,6 +143,8 @@ const load = async () => {
await loadChannels()
const res = await appointmentLists({
patient_id: props.diagnosisId,
/** 诊单维度拉挂号:后端豁免医生/医助与数据范围收窄 */
diag_scope_relax: 1,
page_no: 1,
page_size: 500
})
File diff suppressed because one or more lines are too long
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\YejiStatsLogic;
/**
* 提成结算业绩(独立于业绩看板 yejiStats 接口)
*
* - GET stats.commissionSettlement/overview
* - GET stats.commissionSettlement/orderLines
* - GET stats.commissionSettlement/confirmStatus
* - POST stats.commissionSettlement/saveReconcile
* - POST stats.commissionSettlement/confirmFinalize
* - GET stats.commissionSettlement/deptOptions
* - GET stats.commissionSettlement/channelOptions
*/
class CommissionSettlementController extends BaseAdminController
{
public function overview()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::commissionSettlementOverview($params, $this->adminId, $this->adminInfo));
}
public function orderLines()
{
@set_time_limit(120);
return $this->data(YejiStatsLogic::commissionSettlementOrderLines($this->request->get(), $this->adminId, $this->adminInfo));
}
public function confirmStatus()
{
return $this->data(YejiStatsLogic::commissionSettlementConfirmStatus($this->request->get(), $this->adminId, $this->adminInfo));
}
public function saveReconcile()
{
return $this->data(YejiStatsLogic::commissionSettlementSaveReconcile($this->request->post(), $this->adminId, $this->adminInfo));
}
public function confirmFinalize()
{
@set_time_limit(120);
return $this->data(YejiStatsLogic::commissionSettlementConfirmFinalize($this->request->post(), $this->adminId, $this->adminInfo));
}
public function deptOptions()
{
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
}
public function channelOptions()
{
return $this->data(YejiStatsLogic::channelOptions(false));
}
}
@@ -20,6 +20,16 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
{
use HasDataScopeFilter;
/**
* 诊单编辑/详情「挂号记录」Tab:按诊单 ID 拉全量挂号,不做医生/医助角色收窄与数据范围过滤。
* 须同时传 patient_id(挂号表存的是诊单 id)与本开关,避免列表页被滥用拓宽可见范围。
*/
private function appointmentListsScopeRelaxedForDiagnosis(): bool
{
return (int) ($this->params['diag_scope_relax'] ?? 0) === 1
&& (int) ($this->params['patient_id'] ?? 0) > 0;
}
/**
* 数据隔离:医生或医助(u.assistant_id = diag.assistant_id)命中可见集合;progress_board 场景放开(看板跨医生查看)
*/
@@ -28,6 +38,9 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
if ($progressBoard) {
return;
}
if ($this->appointmentListsScopeRelaxedForDiagnosis()) {
return;
}
if (!$this->dataScopeShouldApply()) {
return;
}
@@ -128,7 +141,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
// 面诊进度看板:可切换查看各医生挂号,不按当前账号角色收窄
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
if (!$progressBoard) {
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
if (!$progressBoard && !$diagScopeRelax) {
// 如果是医生角色(role_id=1),只显示挂自己号的预约
if (in_array(1, $roleIds)) {
$query->where('a.doctor_id', $this->adminId);
@@ -288,7 +302,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
}
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
if (!$progressBoard) {
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
if (!$progressBoard && !$diagScopeRelax) {
if (in_array(1, $roleIds)) {
$query->where('a.doctor_id', $this->adminId);
}
@@ -117,6 +117,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
/**
* 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_timerevisit_slot=0 为全部复诊分项合计)。
* admin 维度按订单创建人 o.creator_id(与部门表复诊列、排行榜复诊列同口径)。
*/
private function applyYejiErCenterRevisitOrderFilter($query): void
{
@@ -138,7 +139,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
if ($t0 === false || $t1 === false) {
return;
}
$ids = DeptLogic::listErCenterRevisitOrderIdsForAssistant(
$ids = DeptLogic::listErCenterRevisitOrderIdsForCreator(
$assistantId,
$slot,
(int) $t0,
@@ -870,16 +871,23 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$query->whereExists("SELECT 1 FROM {$rxTbl} rx WHERE rx.id = {$poTbl}.prescription_id AND rx.delete_time IS NULL AND rx.creator_id = {$doctorId}");
}
/** 业绩看板入口(yeji_order_drawer=1)所有筛选统一按订单创建人;其它入口(处方订单列表 / 患者跟进等)保留旧口径(创建人 ∪ 诊单医助) */
$yejiPanel = (int) ($this->params['yeji_order_drawer'] ?? 0) === 1;
if (isset($this->params['assistant_id']) && (int) $this->params['assistant_id'] > 0) {
$assistantId = (int) $this->params['assistant_id'];
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) {
$q->where('creator_id', $assistantId);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
);
});
if ($yejiPanel) {
$query->where('creator_id', $assistantId);
} else {
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) {
$q->where('creator_id', $assistantId);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
);
});
}
}
if (isset($this->params['assistant_dept_id']) && $this->params['assistant_dept_id'] !== '' && (int) $this->params['assistant_dept_id'] > 0) {
@@ -927,16 +935,38 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return;
}
$inList = implode(',', $deptIds);
$diagTbl = (new Diagnosis())->getTable();
$adTbl = (new AdminDept())->getTable();
<<<<<<< HEAD
if ($yejiPanel) {
/** 业绩看板部门侧栏:仅订单创建人人事部门 ∈ 子树(与「合计业绩」/「接诊诊单」/「复诊」聚合同口径) */
$query->whereExists(
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})"
);
} else {
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $adTbl, $inList) {
$q->whereExists(
"SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})"
);
$q->whereExists(
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})",
'OR'
);
});
}
=======
$poRef = '`' . str_replace('`', '', $poTbl) . '`';
$query->whereRaw(
YejiStatsLogic::sqlPrescriptionOrderListDeptSubtreeMatchCreatorFirst($poTbl, $diagTbl, $adTbl, $inList)
YejiStatsLogic::sqlPrescriptionOrderListDeptSubtreeMatchCreatorFirst($poRef, $diagTbl, $adTbl, $inList)
);
>>>>>>> master
}
}
/**
* 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单(与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致)。
* 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单
* (与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致——按订单创建人)。
*
* @param int[] $adminIds
*/
@@ -950,18 +980,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$rxTbl = (new Prescription())->getTable();
$inList = implode(',', $adminIds);
$query->whereRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg "
. "LEFT JOIN `{$rxTbl}` rx ON rx.`id` = `{$poTbl}`.`prescription_id` AND rx.`delete_time` IS NULL "
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL AND `{$poTbl}`.`diagnosis_id` > 0 AND "
. '(CASE WHEN dg.`assistant_id` > 0 THEN dg.`assistant_id` '
. "WHEN `{$poTbl}`.`creator_id` > 0 AND (rx.`id` IS NULL OR NOT (`{$poTbl}`.`creator_id` <=> rx.`creator_id`)) THEN `{$poTbl}`.`creator_id` "
. 'ELSE 0 END) IN (' . $inList . '))'
);
$query->whereIn('creator_id', $adminIds);
}
/**
+53 -70
View File
@@ -219,21 +219,24 @@ class DeptLogic extends BaseLogic
}
return $q
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id'])
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id', 'o.creator_id'])
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
->select()
->toArray();
}
/**
* 二中心子树:各部门复诊 rollup;同时返回按诊单医助聚合的分项(供医助排行榜)。
* 二中心子树:各部门复诊 rollup(按订单创建人 creator_id 归属)。
* 候选订单仍要求 dg.assistant_id > 0(保持「er-center 案例 = 有医助的诊单」的定义),
* 但**复诊归属**改为订单创建人——leaf 部门归属取 creator_id 在二中心子树内的 canonical 部门,
* 并产出 by_creator_slots 供医助排行榜复诊列复用,与诊金 / 接诊诊单口径完全一致。
*
* @param list<int> $subtreeDeptIds
*
* @return array{
* totals: array<int, int>,
* slots: array<int, array<int, int>>,
* by_assistant_slots: array<int, array<int, int>>
* by_creator_slots: array<int, array<int, int>>
* }
*/
private static function computeErCenterRevisitFullPack(
@@ -249,12 +252,12 @@ class DeptLogic extends BaseLogic
$emptyTotals[$did] = 0;
}
if ($subtreeDeptIds === []) {
return ['totals' => [], 'slots' => [], 'by_assistant_slots' => []];
return ['totals' => [], 'slots' => [], 'by_creator_slots' => []];
}
$subtreeSet = array_fill_keys($subtreeDeptIds, true);
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_assistant_slots' => []];
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_creator_slots' => []];
}
$byPatient = [];
@@ -266,29 +269,30 @@ class DeptLogic extends BaseLogic
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
$creatorNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid > 0) {
$creatorNeed[$cid] = true;
}
}
}
$assistantIds = array_keys($assistantNeed);
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($assistantIds, $subtreeSet);
$creatorIds = array_keys($creatorNeed);
/** 复用 buildAssistantCanonicalDeptInSubtree —— 该方法实际是 admin → 子树内 canonical 部门,与 admin 角色无关 */
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($creatorIds, $subtreeSet);
/** @var array<int, array<int, int>> $leafByDeptSlot dept_id => [ slot => cnt ]slot≥2 */
$leafByDeptSlot = [];
/** @var array<int, array<int, int>> $byAssistantSlot */
$byAssistantSlot = [];
/** @var array<int, array<int, int>> $byCreatorSlot 复诊按订单创建人聚合(与诊金/接诊诊单口径一致) */
$byCreatorSlot = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
$deptId = $canonicalDept[$aid] ?? null;
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
$deptId = $canonicalDept[$cid] ?? null;
if ($deptId === null || !isset($subtreeSet[$deptId])) {
continue;
}
@@ -296,11 +300,11 @@ class DeptLogic extends BaseLogic
$leafByDeptSlot[$deptId] = [];
}
$leafByDeptSlot[$deptId][$slot] = ($leafByDeptSlot[$deptId][$slot] ?? 0) + 1;
if ($aid > 0) {
if (!isset($byAssistantSlot[$aid])) {
$byAssistantSlot[$aid] = [];
if ($cid > 0) {
if (!isset($byCreatorSlot[$cid])) {
$byCreatorSlot[$cid] = [];
}
$byAssistantSlot[$aid][$slot] = ($byAssistantSlot[$aid][$slot] ?? 0) + 1;
$byCreatorSlot[$cid][$slot] = ($byCreatorSlot[$cid][$slot] ?? 0) + 1;
}
}
}
@@ -313,16 +317,16 @@ class DeptLogic extends BaseLogic
$rollupTotals[$id] = array_sum($rollupSlots[$id] ?? []);
}
foreach ($byAssistantSlot as $aid => $sm) {
foreach ($byCreatorSlot as $cid => $sm) {
if ($sm !== []) {
ksort($byAssistantSlot[$aid], SORT_NUMERIC);
ksort($byCreatorSlot[$cid], SORT_NUMERIC);
}
}
return [
'totals' => $rollupTotals,
'slots' => $rollupSlots,
'by_assistant_slots' => $byAssistantSlot,
'by_creator_slots' => $byCreatorSlot,
];
}
@@ -345,11 +349,11 @@ class DeptLogic extends BaseLogic
}
/**
* 二中心子树内:各医助成交的复诊业务单笔数(合计 + 复诊2/3/…分项),口径与部门 rollup 同源
* 二中心子树复诊:按订单创建人 creator_id 聚合(与部门 rollup 同源,与诊金/接诊诊单口径一致)
*
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
*/
public static function getErCenterRevisitCountsByAssistant(?int $orderStartTs = null, ?int $orderEndTs = null): array
public static function getErCenterRevisitCountsByCreator(?int $orderStartTs = null, ?int $orderEndTs = null): array
{
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
@@ -357,42 +361,36 @@ class DeptLogic extends BaseLogic
return ['totals' => [], 'slots' => []];
}
$pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs);
$by = $pack['by_assistant_slots'];
$by = $pack['by_creator_slots'];
$totals = [];
$slots = [];
foreach ($by as $aid => $slotMap) {
$aid = (int) $aid;
$totals[$aid] = (int) array_sum($slotMap);
$slots[$aid] = $slotMap;
foreach ($by as $cid => $slotMap) {
$cid = (int) $cid;
$totals[$cid] = (int) array_sum($slotMap);
$slots[$cid] = $slotMap;
}
return ['totals' => $totals, 'slots' => $slots];
}
/**
* 某医助在区间内、计入二中心复诊的业务订单 id(用于看板侧栏按复诊笔数下钻)。
* 二中心子树复诊:列出某「订单创建人」在区间内、复诊序列(slot≥2)命中的业务订单 id(用于看板侧栏按复诊笔数下钻)。
* $revisitSlot = 0 表示累计全部复诊(第 2 笔起);≥2 时仅该分项。
*
* @return list<int>
*/
public static function listErCenterRevisitOrderIdsForAssistant(
int $assistantId,
public static function listErCenterRevisitOrderIdsForCreator(
int $creatorId,
int $revisitSlot,
int $orderStartTs,
int $orderEndTs
): array {
if ($assistantId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
if ($creatorId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
return [];
}
if ($revisitSlot < 0 || $revisitSlot === 1) {
return [];
}
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
if ($subtreeIds === []) {
return [];
}
$subtreeSet = array_fill_keys($subtreeIds, true);
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return [];
@@ -405,31 +403,16 @@ class DeptLogic extends BaseLogic
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
}
}
}
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $subtreeSet);
$ids = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid !== $assistantId) {
continue;
}
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
continue;
}
$dcan = $canonicalDept[$aid] ?? null;
if ($dcan === null || !isset($subtreeSet[$dcan])) {
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid !== $creatorId) {
continue;
}
$oid = (int) ($orders[$i]['id'] ?? 0);
@@ -443,8 +426,8 @@ class DeptLogic extends BaseLogic
}
/**
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按诊单医助拆解笔数。
* 与看板行内「复诊」列口径一致:诊单下订单序列第 2 笔起;医助 canonical 部门须落在二中心子树且在该部门行的子树内。
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按**订单创建人**拆解笔数。
* 与看板行内「复诊」列口径一致:订单序列第 2 笔起;创建人 canonical 部门须落在二中心子树且在该部门行的子树内。
*
* @return array{rows: list<array{admin_id: int, name: string, order_count: int}>}
*/
@@ -484,17 +467,17 @@ class DeptLogic extends BaseLogic
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
$creatorNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid > 0) {
$creatorNeed[$cid] = true;
}
}
}
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $erSubtreeSet);
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($creatorNeed), $erSubtreeSet);
/** @var array<int, int> $counts */
$counts = [];
foreach ($byPatient as $orders) {
@@ -504,15 +487,15 @@ class DeptLogic extends BaseLogic
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
continue;
}
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid <= 0) {
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid <= 0) {
continue;
}
$canon = $canonicalDept[$aid] ?? null;
$canon = $canonicalDept[$cid] ?? null;
if ($canon === null || !isset($erSubtreeSet[$canon]) || !isset($descFlip[$canon])) {
continue;
}
$counts[$aid] = ($counts[$aid] ?? 0) + 1;
$counts[$cid] = ($counts[$cid] ?? 0) + 1;
}
}
if ($counts === []) {
@@ -527,10 +510,10 @@ class DeptLogic extends BaseLogic
->column('name', 'id');
}
$rows = [];
foreach ($counts as $aid => $cnt) {
foreach ($counts as $cid => $cnt) {
$rows[] = [
'admin_id' => (int) $aid,
'name' => (string) ($nameRows[$aid] ?? ('#' . $aid)),
'admin_id' => (int) $cid,
'name' => (string) ($nameRows[$cid] ?? ('#' . $cid)),
'order_count' => (int) $cnt,
];
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
-- 提成结算:核对记录 + 顺延结转(上期「确定业绩」时写入,下期统计自动并入订单池)
CREATE TABLE IF NOT EXISTS `zyt_commission_settlement_confirm` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`settlement_month` char(7) NOT NULL COMMENT '结算月 YYYY-MM',
`scope_channel_code` varchar(128) NOT NULL DEFAULT '' COMMENT '与查询渠道一致,空为全渠道',
`scope_dept_ids_key` varchar(255) NOT NULL DEFAULT '*' COMMENT '部门筛选键:* 或排序后的 id 逗号串',
`status` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0已保存核对 1已确定业绩',
`reconcile_note` varchar(500) NOT NULL DEFAULT '' COMMENT '核对备注',
`totals_json` text COMMENT '确定时汇总快照 JSON',
`confirmed_admin_id` int unsigned NOT NULL DEFAULT 0,
`confirmed_admin_name` varchar(64) NOT NULL DEFAULT '',
`confirmed_at` int unsigned NOT NULL DEFAULT 0 COMMENT '确定业绩时间戳',
`create_time` int unsigned NOT NULL DEFAULT 0,
`update_time` int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_scope` (`settlement_month`,`scope_channel_code`(64),`scope_dept_ids_key`(128))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提成结算核对/确定';
CREATE TABLE IF NOT EXISTS `zyt_commission_settlement_carry` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`prescription_order_id` int unsigned NOT NULL COMMENT '业务订单 tcm_prescription_order.id',
`target_settlement_month` char(7) NOT NULL COMMENT '计入哪个月结算池',
`source_settlement_month` char(7) NOT NULL COMMENT '从哪个月确定顺延出来',
`scope_channel_code` varchar(128) NOT NULL DEFAULT '',
`scope_dept_ids_key` varchar(255) NOT NULL DEFAULT '*',
`create_time` int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_carry` (`prescription_order_id`,`target_settlement_month`,`scope_channel_code`(64),`scope_dept_ids_key`(64)),
KEY `idx_target_scope` (`target_settlement_month`,`scope_channel_code`(32),`scope_dept_ids_key`(64)),
KEY `idx_source_scope` (`source_settlement_month`,`scope_channel_code`(32),`scope_dept_ids_key`(64))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提成顺延结转';
@@ -0,0 +1,101 @@
-- 提成结算业绩:独立页面 fans/commission-settlement + stats.commissionSettlement/* 接口权限
-- 与业绩看板 yeji 同父菜单(pid = 业绩看板菜单 id 293);幂等
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
y.pid,
'C',
'提成结算业绩',
'',
96,
'fans/commission-settlement',
'/fans/commission-settlement',
'fans/commission-settlement',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT pid FROM `zyt_system_menu` WHERE id = 293 LIMIT 1) AS y
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` WHERE id = 293)
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m WHERE m.component = 'fans/commission-settlement'
);
SET @commission_cs_pid := (
SELECT id FROM `zyt_system_menu` WHERE component = 'fans/commission-settlement' ORDER BY id DESC LIMIT 1
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-汇总',
'',
1,
'stats.commissionSettlement/overview',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/overview'
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-部门下拉',
'',
2,
'stats.commissionSettlement/deptOptions',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/deptOptions'
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-渠道下拉',
'',
3,
'stats.commissionSettlement/channelOptions',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/channelOptions'
);
@@ -0,0 +1,101 @@
-- 提成结算:核对/确定业绩接口权限(挂在 fans/commission-settlement 页面菜单下)
SET @commission_cs_pid := (
SELECT id FROM `zyt_system_menu` WHERE component = 'fans/commission-settlement' ORDER BY id DESC LIMIT 1
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-核对明细',
'',
11,
'stats.commissionSettlement/orderLines',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/orderLines'
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-确认状态',
'',
12,
'stats.commissionSettlement/confirmStatus',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/confirmStatus'
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-保存核对',
'',
13,
'stats.commissionSettlement/saveReconcile',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/saveReconcile'
);
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@commission_cs_pid,
'A',
'提成结算-确定业绩',
'',
14,
'stats.commissionSettlement/confirmFinalize',
'',
'',
'',
'',
1,
0,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP()
FROM (SELECT 1 AS `_`) AS `_exec`
WHERE @commission_cs_pid IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/confirmFinalize'
);
@@ -0,0 +1,8 @@
-- 历史库若早于 2026_04_07 建表可能缺少 creator_id,会导致业绩看板等 SQL 报 Unknown column … creator_id
-- 执行前可先:SHOW COLUMNS FROM `zyt_tcm_prescription_order` LIKE 'creator_id';
-- 若列已存在,本脚本会报错,跳过即可。
ALTER TABLE `zyt_tcm_prescription_order`
ADD COLUMN `creator_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建人管理员ID' AFTER `diagnosis_id`;
-- 若需索引且尚未存在,可再执行(已存在会报错可忽略):
-- ALTER TABLE `zyt_tcm_prescription_order` ADD KEY `idx_creator_id` (`creator_id`);