This commit is contained in:
Your Name
2026-05-12 17:26:11 +08:00
parent c644f2554f
commit 796ca12fb8
10 changed files with 172 additions and 68 deletions
+39 -2
View File
@@ -102,6 +102,18 @@
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="添加时间">
<el-date-picker
v-model="addTimeRange"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
clearable
class="!w-[280px]"
@change="onAddTimeRangeChange"
/>
</el-form-item>
<el-form-item label="标签">
<el-select
v-model="queryParams.tag_ids"
@@ -557,12 +569,34 @@ const syncSettings = reactive({
interval: 3600
})
const queryParams = reactive<{ name: string; follow_user: string; tag_ids: string[] }>({
const queryParams = reactive<{
name: string
follow_user: string
tag_ids: string[]
add_time_start: string
add_time_end: string
}>({
name: '',
follow_user: '',
tag_ids: []
tag_ids: [],
add_time_start: '',
add_time_end: ''
})
/** 与列表「添加时间」列口径一致(库内 external_first_add_time / create_time */
const addTimeRange = ref<[string, string] | null>(null)
function onAddTimeRangeChange(val: [string, string] | null) {
if (val && val.length === 2 && val[0] && val[1]) {
queryParams.add_time_start = val[0]
queryParams.add_time_end = val[1]
} else {
queryParams.add_time_start = ''
queryParams.add_time_end = ''
}
resetPage()
}
// ── 标签维度(筛选下拉 + 抽屉面板共用同一份数据) ──────────────────────────
interface TagItem {
tag_id: string
@@ -793,6 +827,9 @@ function handleReset() {
queryParams.name = ''
queryParams.follow_user = ''
queryParams.tag_ids = []
queryParams.add_time_start = ''
queryParams.add_time_end = ''
addTimeRange.value = null
resetParams()
}
+13 -13
View File
@@ -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复诊三<b>归属</b>按订单创建人的人事部门<b>合计业绩</b>列同口径非二中心子树部门为 0
<b>名称含二中心</b>的部门及其组织下级候选订单为有诊单医助 <b>fulfillment_status</b> 4/9/10<b>NULL 计入</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,7 +395,7 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
按订单<b>创建时间</b>统计剔除履约已取消金额与<b>接诊诊单</b>列一致仅按<b>订单创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<b>医助排行榜诊金</b>同口径<br />
按订单<b>创建时间</b>统计剔除履约已取消/拒收/退款 4·9·10金额与<b>接诊诊单</b>列一致仅按<b>订单创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<b>医助排行榜诊金</b>同口径<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表对齐<br />
点击金额可查看对应业务订单明细侧栏默认合计业绩全量列表
</div>
@@ -409,7 +409,7 @@
<template #content>
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
{{ tb.channel_name }}渠道业绩与卡片标题渠道一致处方订单列表业绩<b>同口径</b>按订单
<b>create_time</b> 归日剔除履约已取消(4)<br />
<b>create_time</b> 归日剔除履约状态 4已取消/9拒收/10退款<br />
渠道判定用 EXISTS该患者有任一挂号 <b>channels 命中字典 + status=3</b>不限挂号时点<br />
点击金额可查看该部门本渠道订单明细侧栏渠道筛选列表
</div>
@@ -446,9 +446,9 @@
<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,9,10}</b><b>NULL 计入</b><b>订单创建人</b>的人事部门落在该部门子树即计入表格多行命中时取最深的展示部门<b>合计业绩 / 医助排行榜接诊诊单</b>同口径选定渠道时本列仍为全量
=======
<b>业务订单条数</b>非取消订单 <b>create_time</b> 落入区间<b>fulfillment_status 4</b>与列表筛选
<b>业务订单条数</b>计业绩订单 <b>create_time</b> 落入区间<b>fulfillment_status {4,9,10}</b>与列表筛选
<b>assistant_dept_id</b> 时一致<b>创建人</b>人事部门优先无创建人则诊单 <b>医助</b>落在该部门子树即计入表格多行命中时取最深的展示部门与侧栏勾选与表格业绩对齐时的集合可能略有差异选定渠道时本列仍为全量
>>>>>>> master
</div>
@@ -461,7 +461,7 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
{{ tb.channel_name }}渠道成交单数与渠道业绩同源区间内 <b>非取消业务订单</b> 条数按订单
{{ tb.channel_name }}渠道成交单数与渠道业绩同源区间内 <b>计业绩的业务订单</b> 条数剔除 4/9/10按订单
<b>create_time</b> 归日渠道判定同业绩列
</div>
</template>
@@ -974,7 +974,7 @@
>
<span class="yeji-order-drawer__stat-label">
业务订单金额合计
<span class="yeji-order-drawer__stat-note">(不含履约已取消,与上方业绩口径一致)</span>
<span class="yeji-order-drawer__stat-note">(不含履约 4·9·10,与上方业绩口径一致)</span>
</span>
<span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
¥{{ formatOrderDrawerMoney(orderDrawerTotalAmount) }}
@@ -2129,7 +2129,7 @@ type OrderDrawerFilter =
}
const orderDrawerFilter = ref<OrderDrawerFilter | null>(null)
/** 列表接口 extend,含 stats_order_amount_performance(与当前筛选、不含已取消口径一致) */
/** 列表接口 extend,含 stats_order_amount_performance(与当前筛选一致:剔除履约 4/9/10 */
const orderDrawerExtend = ref<Record<string, any> | null>(null)
const orderDrawerTotalAmount = computed(() => {
@@ -2641,7 +2641,7 @@ function buildOrderDrawerListParams(): Record<string, any> | null {
const params: Record<string, any> = {
start_time: `${f.start} 00:00:00`,
end_time: `${f.end} 23:59:59`,
/** 与业绩列口径一致:不列出履约已取消(4) */
/** 与业绩列口径一致:不列出履约 4(已取消)/9(拒收)/10(退款) */
exclude_fulfillment_cancelled: 1,
/**
* 业绩看板入口标志:后端 PrescriptionOrderLists::applyDoctorAssistantFilters 看到此标志后,
@@ -2760,7 +2760,7 @@ 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}`
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约已取消`
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2784,7 +2784,7 @@ function openOrderDrawerByYejiTableScope(
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
}
orderDrawerTitle.value = '业务订单 · 当前展示合计'
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约已取消`
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2841,7 +2841,7 @@ function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['row
end,
}
orderDrawerTitle.value = `业务订单 · ${row.name}`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2865,7 +2865,7 @@ function openOrderDrawerByAssistantErCenterRevisit(
}
const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
orderDrawerTitle.value = `业务订单 · ${assistant.name} · ${slotLbl}`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
+27 -10
View File
@@ -797,6 +797,32 @@ function buildTcmDiagnosisListRequestPayload(req: Record<string, unknown>): Reco
}
}
/**
* 待分配「订单月份」:未选月份时,未进入 Tab 前默认当月(与 handlePendingAssignTabClick 一致);
* 在 Tab 内清空月份表示不限月份。
*/
function resolvePendingAssignOrderMonthForRequest(): string {
const trimmed = String(formData.pending_assign_order_month ?? '').trim()
if (trimmed !== '') return trimmed
if (formData.pending_assign === '1') return ''
return dayjs().format('YYYY-MM')
}
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
function buildPendingAssignCountPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload({
...formData,
page_no: 1,
page_size: 1,
pending_assign: 1,
appointment_date: '',
has_appointment: '',
pending_booking: '',
completed_appointment: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest()
} as Record<string, unknown>) as Record<string, unknown>
}
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
tcmDiagnosisLists(buildTcmDiagnosisListRequestPayload(req) as any)
@@ -925,16 +951,7 @@ const fetchDateCounts = async () => {
tcmDiagnosisLists({ page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
tcmDiagnosisLists(
buildTcmDiagnosisListRequestPayload({
pending_assign: 1,
page_no: 1,
page_size: 1,
pending_assign_order_month: formData.pending_assign_order_month,
pending_assign_keyword: formData.pending_assign_keyword,
keyword: formData.keyword
}) as any
)
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
])
dateCounts.value = {
[yesterdayStr.value]: yesterday?.count ?? 0,
@@ -77,6 +77,23 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
}
}
// 添加时间:与 lists 排序口径一致(external_first_add_time 优先,0 则 create_time
$addStart = trim((string) ($this->params['add_time_start'] ?? ''));
$addEnd = trim((string) ($this->params['add_time_end'] ?? ''));
$effExpr = 'COALESCE(NULLIF(external_first_add_time, 0), create_time)';
if ($addStart !== '') {
$t = strtotime($addStart . ' 00:00:00');
if ($t !== false) {
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' >= ?', [$t]);
}
}
if ($addEnd !== '') {
$t = strtotime($addEnd . ' 23:59:59');
if ($t !== false) {
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' <= ?', [$t]);
}
}
return $query;
}
@@ -107,7 +107,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyCreatorOrOwnPrescriptionVisibility($query);
$this->applyDataScopeForPrescriptionOrder($query);
}
// 业绩看板侧栏等:不展示履约已取消(4),与 stats 业绩统计口径一致
// 业绩看板侧栏等:不展示履约 4/9/10,与 stats 业绩口径一致
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
}
@@ -378,7 +378,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyDataScopeForPrescriptionOrder($query);
}
}
// 与看板「合计业绩」一致:NULL 安全剔除履约已取消(4),勿用 `<>` 以免误丢 NULL 状态行
// 与看板「合计业绩」一致:NULL 安全剔除履约 4/9/10
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
}
@@ -570,13 +570,13 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'stats_order_amount' => $s['order_amount'],
'stats_order_amount_not_cancelled' => $s['order_amount_not_cancelled'],
'stats_order_amount_cancelled' => $s['order_amount_cancelled'],
/** 业绩:业务订单金额,剔除履约已取消(fulfillment_status=4),其余状态全部计入 */
'stats_order_amount_performance' => $s['order_amount_not_cancelled'],
/** 业绩:业务订单金额,剔除履约已取消/拒收/退款(4/9/10)NULL 与其它状态计入 */
'stats_order_amount_performance' => $s['order_amount_performance'] ?? $s['order_amount_not_cancelled'],
'stats_linked_pay_amount' => $s['linked_pay_amount'],
'stats_linked_pay_amount_not_cancelled' => $s['linked_pay_amount_not_cancelled'],
'stats_linked_pay_amount_cancelled' => $s['linked_pay_amount_cancelled'],
/** 业绩-关联实付:剔除履约已取消订单的关联支付金额 */
'stats_linked_pay_amount_performance' => $s['linked_pay_amount_not_cancelled'],
/** 业绩-关联实付:剔除履约 4/9/10 订单的关联支付金额 */
'stats_linked_pay_amount_performance' => $s['linked_pay_amount_performance'] ?? $s['linked_pay_amount_not_cancelled'],
'stats_time_start' => $s['label_start'],
'stats_time_end' => $s['label_end'],
/** 统计口径:all=支付审核白名单全量;assistant=医助仅诊单协助业绩;restricted=与列表同域 */
@@ -693,7 +693,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$t1 = $tmp;
}
$q = $this->buildBaseQueryForStats(false)->whereBetween('create_time', [$t0, $t1]);
// 全量「合计业绩」与看板列同口径:始终剔除履约已取消(4),不依赖 exclude 参数
// 全量「合计业绩」与看板列同口径:始终剔除履约 4/9/10,不依赖 exclude 参数
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
$amount = round((float) (clone $q)->sum('amount'), 2);
$count = (int) (clone $q)->count();
@@ -715,7 +715,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($pendingRxQuery, '');
return [
// 处方审核待处理(不含履约已取消
// 处方审核待处理(不含履约 4/9/10
'pending_rx' => (int) $pendingRxQuery->where('prescription_audit_status', 0)->count(),
// 支付审核待处理(前提:处方已通过)
'pending_pay' => (int) (clone $query)->where('prescription_audit_status', 1)->where('payment_slip_audit_status', 0)->count(),
@@ -761,8 +761,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
}
$q = $this->buildBaseQueryForStats()->whereBetween('create_time', [$t0, $t1]);
$orderTotal = round((float) (clone $q)->sum('amount'), 2);
$orderCancelled = round((float) (clone $q)->where('fulfillment_status', 4)->sum('amount'), 2);
$orderTotal = round((float) (clone $q)->sum('amount'), 2);
$orderCancelled = round((float) (clone $q)->where('fulfillment_status', 4)->sum('amount'), 2);
$orderNotCancelled = round($orderTotal - $orderCancelled, 2);
$idsAll = (clone $q)->column('id');
@@ -770,7 +770,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return $id > 0;
}));
if ($idsAll === []) {
return $this->statsExtendPayload(
$empty = $this->statsExtendPayload(
$orderTotal,
$orderNotCancelled,
$orderCancelled,
@@ -780,6 +780,12 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$l0,
$l1
);
$qPerfEmpty = clone $q;
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($qPerfEmpty, '');
$empty['order_amount_performance'] = round((float) $qPerfEmpty->sum('amount'), 2);
$empty['linked_pay_amount_performance'] = 0.0;
return $empty;
}
$idsCancel = (clone $q)->where('fulfillment_status', 4)->column('id');
$idsCancel = array_values(array_filter(array_map('intval', $idsCancel), static function (int $id): bool {
@@ -791,7 +797,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$linkedNotCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsNotCancelled);
$linkedCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsCancel);
return $this->statsExtendPayload(
$base = $this->statsExtendPayload(
$orderTotal,
$orderNotCancelled,
$orderCancelled,
@@ -801,6 +807,21 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$l0,
$l1
);
$qPerf = clone $q;
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($qPerf, '');
$base['order_amount_performance'] = round((float) $qPerf->sum('amount'), 2);
$idsExcludedPerf = (clone $q)
->whereIn('fulfillment_status', YejiStatsLogic::PRESCRIPTION_ORDER_FULFILLMENT_EXCLUDED_FROM_PERFORMANCE)
->column('id');
$idsExcludedPerf = array_values(array_filter(array_map('intval', $idsExcludedPerf), static function (int $id): bool {
return $id > 0;
}));
$idsPerf = array_values(array_diff($idsAll, $idsExcludedPerf));
$base['linked_pay_amount_performance'] = $this->sumLinkedPayForPrescriptionOrderIds($idsPerf);
return $base;
}
/**
@@ -808,7 +829,11 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
*/
private function statsZeroPayload(): array
{
return $this->statsExtendPayload(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, '', '');
$z = $this->statsExtendPayload(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, '', '');
$z['order_amount_performance'] = 0.0;
$z['linked_pay_amount_performance'] = 0.0;
return $z;
}
/**
+3 -2
View File
@@ -14,6 +14,7 @@
namespace app\adminapi\logic\dept;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\common\logic\BaseLogic;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept;
@@ -212,8 +213,8 @@ class DeptLogic extends BaseLogic
->join('tcm_diagnosis dg', 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->where('o.diagnosis_id', '>', 0)
->where('dg.assistant_id', '>', 0)
->whereRaw('NOT (o.fulfillment_status <=> ?)', [4]);
->where('dg.assistant_id', '>', 0);
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
if ($orderStartTs !== null && $orderEndTs !== null) {
$q->where('o.create_time', 'between', [$orderStartTs, $orderEndTs]);
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\common\model\finance\AccountCost;
use app\common\service\DataScope\DataScopeService;
@@ -1041,8 +1042,9 @@ class ConversionLogic
->alias('po')
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id')
->whereNull('po.delete_time')
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp])
->where('po.fulfillment_status', '<>', 4)
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp]);
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($businessQuery, 'po');
$businessQuery
->fieldRaw("{$businessSourceExpr} AS source_admin_id, SUM(po.amount) AS total_amount")
->group($businessSourceExpr);
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\common\model\auth\Admin;
use app\common\service\DataScope\DataScopeService;
use think\facade\Db;
@@ -14,7 +15,7 @@ use think\facade\Db;
* 医生范围:<b>admin_role.role_id = 1</b> 且管理员 <b>未软删</b>delete_time 为空);再按账号「数据范围」收窄。
*
* - 系统/手动开方:tcm_prescription.prescription_date ∈ [start,end];渠道/标签与业绩渠道列同源 EXISTS / 诊单标签
* - 成交:订单 create_time、排除履约(4),按处方 creator_id;渠道/标签同 sumPerformance 口径
* - 成交:订单 create_time、排除履约 4/9/10,按处方 creator_id;渠道/标签同 sumPerformance 口径
* - 挂号:appointment_date ∈ [start,end];选渠道时挂号 channels 命中字典值;标签渠道时 patient_id ∈ 标签诊单集
*/
class DoctorDailyStatsLogic
@@ -300,9 +301,9 @@ class DoctorDailyStatsLogic
->alias('o')
->join('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->whereBetween('o.create_time', [$startTs, $endTs])
->whereRaw('NOT (o.fulfillment_status <=> 4)')
->whereIn('rx.creator_id', $doctorIds)
->whereBetween('o.create_time', [$startTs, $endTs]);
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
$q->whereIn('rx.creator_id', $doctorIds)
->where('o.diagnosis_id', '>', 0);
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
@@ -22,15 +22,15 @@ use think\facade\Db;
* · **二中心及其组织下级在选定渠道下计 0**(与进线一致)
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按**订单创建人**所在部门在组织树内 rollup(含下级),与排行榜复诊一致
* - 已完成挂号 = status=3 挂号条数(appointment_date),列「已完成挂号」;部门归属:有效医助→接诊医生→展示行映射。
* - 接诊诊单 = 列名对应**业务订单条数**(非取消、create_time 区间),按**订单创建人**人事部门落在该行子树即计入;多行命中时取**最深的展示部门行**。与处方列表按 assistant_dept_id 筛选不同(列表保留旧口径:诊单医助 ∪ 创建人)。
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **非取消业务订单条数**剔除 fulfillment_status=4;NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(按订单创建人归属),非挂号条数。
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消(4) 与软删),**部门列**与「接诊诊单」一致:**订单创建人**人事部门落在该展示行子树则计入,多行命中取最深展示行;与排行榜诊金同口径。
* - 接诊诊单 = 列名对应**业务订单条数**(剔除履约 4/9/10、create_time 区间),按**订单创建人**人事部门落在该行子树即计入;多行命中时取**最深的展示部门行**。与处方列表按 assistant_dept_id 筛选不同(列表保留旧口径:诊单医助 ∪ 创建人)。
* - {渠道}成交单 = 选定渠道时与 {渠道}业绩同源:区间内 **计入业绩的业务订单条数**(剔除 fulfillment_status ∈ {4,9,10};NULL 与其它状态计入,按 create_time 归日);dict(channels) 与 tag 口径均按订单计(按订单创建人归属),非挂号条数。
* - 合计业绩 = 区间内业务订单 amount 求和(排除已取消/拒收/退款(4,9,10) 与软删),**部门列**与「接诊诊单」一致:**订单创建人**人事部门落在该展示行子树则计入,多行命中取最深展示行;与排行榜诊金同口径。
* 无法归入任一展示部门的金额单列「未归属中心」行;**底栏「合计业绩」「接诊诊单」= 当前表内各行业绩列相加**(含未归属行),与弹窗摊分口径一致。
* · 时间窗与 stats_order_amount_performance 一致;列表若另有患者筛选或账号数据域收窄,金额可能仍不同
* · 选定渠道时「{渠道}业绩」列仍按比例展示;合计列全量与渠道列含未归属补差;**二中心及其组织下级展示部门不参与该渠道进线/业绩/成交单/ROI 分子**(与同模块 DeptLogic::getErCenterSubtreeDeptIdSet 一致)
* - {渠道}业绩 = 与「处方订单列表」业绩同口径(≠4):按 **o.create_time** 落入区间,按**订单创建人**归部门;
* - {渠道}业绩 = 与「处方订单列表」业绩同口径(剔除 4/9/10:按 **o.create_time** 落入区间,按**订单创建人**归部门;
* 渠道判定仅用 EXISTS:"患者有任一已完成挂号 (status=3) 且 channels 命中字典",**不限挂号时点**;无字典映射时回退 tag/creator 口径
* - 成交单数 = 区间内非取消业务订单 **条数**;按**订单创建人**映射到「中心」;无法归属计入「未归属中心」;合计=全表 COUNT。
* - 成交单数 = 区间内计入业绩的业务订单 **条数**(剔除 4/9/10;按**订单创建人**映射到「中心」;无法归属计入「未归属中心」;合计=全表 COUNT。
* - 投放成本 = zyt_account_cost 在日期区间内 **amount** 合计(选定渠道时 `media_channel_code = channel_code` 过滤)。
* · **各部门行投放成本** = 总投放成本 × (该部门进线 ÷ 各展示部门进线合计)。相当于先算 **单进线成本** = 总成本 ÷ 总进线,再乘部门进线。
* · **二中心及其组织下级**不参与均摊投放成本(进线分摊分母对其计 0,与选定渠道下进线清零规则独立);其余部门按进线占比分摊。合计行成本 = 账务汇总总额。
@@ -53,6 +53,9 @@ use think\facade\Db;
*/
class YejiStatsLogic
{
/** 不计入业绩的履约状态:已取消(4)、拒收(9)、退款(10);NULL 与其它值仍计业绩 */
public const PRESCRIPTION_ORDER_FULFILLMENT_EXCLUDED_FROM_PERFORMANCE = [4, 9, 10];
/**
* overview / assistantLeaderboards 共用的筛选与时间上下文。
*
@@ -557,7 +560,7 @@ class YejiStatsLogic
. ($dataScopeRestricted
? ' 当前账号受数据范围限制时,底栏合计业绩与列表同域。'
: '');
$baseNoteNoChannel = '业绩口径:除履约已取消(4)外全部计入。部门行「合计业绩」「接诊诊单」「复诊」均按**订单创建人人事部门 ∈ 该展示行子树**归属(多行命中取最深展示行),与医助排行榜诊金一致;与处方订单列表 assistant_dept_id 默认筛选(诊单医助 ∪ 创建人)不同——侧栏点击会自动收窄为创建人口径。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。';
$baseNoteNoChannel = '业绩口径:除履约已取消(4)、拒收(9)、退款(10),其余(含 NULL计入。部门行「合计业绩」「接诊诊单」「复诊」均按**订单创建人人事部门 ∈ 该展示行子树**归属(多行命中取最深展示行),与医助排行榜诊金一致;与处方订单列表 assistant_dept_id 默认筛选(诊单医助 ∪ 创建人)不同——侧栏点击会自动收窄为创建人口径。多表「本月」区间为当月1日至服务器当日。**投放成本**按进线占比分摊,**二中心子树展示部门进线在分摊中计 0**(与部门 ErCenter 子树一致)。';
return [
'start_date' => $startDate,
@@ -567,9 +570,9 @@ class YejiStatsLogic
'channel_name' => $channelInfo ? (string) $channelInfo['channel_name'] : '',
'channel_group' => $channelInfo ? (string) $channelInfo['source_group_name'] : '',
'channel_filter_note' => $channelCode !== ''
? ('{渠道}业绩:按订单 create_time 归日、剔除履约已取消(4)(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。部门归属按**订单创建人**人事部门 ∈ 子树。'
. ' {渠道}成交单数:与{渠道}业绩同源,剔除已取消(4)的订单条数(非挂号条数),按订单创建人归部门。'
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(非取消),同样按订单创建人归部门。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
? ('{渠道}业绩:按订单 create_time 归日、剔除履约 4/9/10(NULL 与其它状态均计入);渠道判定用"该患者是否有挂号 channels 命中 dict + status=3"(不限挂号时点)。部门归属按**订单创建人**人事部门 ∈ 子树。'
. ' {渠道}成交单数:与{渠道}业绩同源,剔除 4/9/10 的订单条数(非挂号条数),按订单创建人归部门。'
. ' 「已完成挂号」列同 admin 挂号已完成列表;「接诊诊单」列为业务订单条数(同业绩剔除口径),同样按订单创建人归部门。**二中心及其组织下级**(与部门模块 ErCenter 子树一致)**不计入**该渠道进线、业绩与成交单。**投放成本**按各部门进线条数占展示部门进线合计的比例分摊,**二中心子树进线在分摊中计 0**(未选渠道时仍显示进线数,仅不参与均摊)。ROI = 渠道业绩(不含二中心子树)÷ 总投放成本。进线按所选标签客户精确过滤。'
. ($tagFallback
? ' ⚠ 当前业务尚未回填 tcm_diagnosis.external_userid 与 user_auth.unionid,已自动退化为「该标签客户的跟进医助 → 区间内接诊/业绩」口径,仅用于趋势观察。'
: '')
@@ -1799,7 +1802,7 @@ class YejiStatsLogic
/**
* 接诊诊单聚合:返回两组按部门归属的接诊计数。
* - all: 区间内 status=3 挂号条数按部门归属(与列表计数一致),不含「仅 role_id=2」等额外限制。
* - scoped: 选定渠道时「{渠道}成交单」= **非取消业务订单条数**(与 {渠道}业绩同源):dict 下 EXISTS 与侧栏一致(channels channel_source);有展示行时按列表部门摊行;否则走归因折叠。
* - scoped: 选定渠道时「{渠道}成交单」= **计入业绩的业务订单条数**(与 {渠道}业绩同源,剔除 4/9/10):dict 下 EXISTS 与侧栏一致(channels channel_source);有展示行时按列表部门摊行;否则走归因折叠。
* 未选渠道时 scoped == all(不再额外查一次)。
*
* @param int[] $appointmentChannelValues dict_data value 整型列表(通常 1=自媒体1)
@@ -2996,7 +2999,7 @@ class YejiStatsLogic
}
/**
* 与 sumPerformanceByAdmin 对应:按订单创建人统计成交单数(非取消业务订单条数),全量/渠道分支一致。
* 与 sumPerformanceByAdmin 对应:按订单创建人统计成交单数(剔除履约 4/9/10),全量/渠道分支一致。
*
* @param int[] $appointmentChannelValues
*
@@ -3296,18 +3299,19 @@ class YejiStatsLogic
}
/**
* 业绩/成交单统计:排除履约已取消 fulfillment_status = 4,其余(含 NULL 与任意其它状态)全部计入。
* 与看板「合计业绩」、处方列表业绩 extend 口径一致;勿用 `<> 4`,以免误排除 NULL
* 业绩/成交单统计:排除履约已取消/拒收/退款」(fulfillment_status 4、9、10)NULL 与其它状态计入。
* 与看板「合计业绩」、处方列表业绩 extend 口径一致。
*
* @param \think\db\BaseQuery|\think\Model $query Db::name / Model::where 等查询实例
* @param string $tableAlias 主表别名(如 Db 侧 `alias('o')` 传 `o`);无别名时传空字符串
*/
public static function applyPrescriptionOrderNotCancelledForPerformanceQuery($query, string $tableAlias = ''): void
{
$inList = implode(',', array_map(static fn (int $v): string => (string) $v, self::PRESCRIPTION_ORDER_FULFILLMENT_EXCLUDED_FROM_PERFORMANCE));
if ($tableAlias !== '') {
$query->whereRaw("NOT ({$tableAlias}.fulfillment_status <=> ?)", [4]);
$query->whereRaw("({$tableAlias}.fulfillment_status IS NULL OR {$tableAlias}.fulfillment_status NOT IN ({$inList}))");
} else {
$query->whereRaw('NOT (fulfillment_status <=> ?)', [4]);
$query->whereRaw("(fulfillment_status IS NULL OR fulfillment_status NOT IN ({$inList}))");
}
}
@@ -3609,7 +3613,7 @@ class YejiStatsLogic
/**
* 业绩聚合:返回两组按部门归属的业绩金额。
* 口径与 /tcm.prescriptionOrder/lists 的 stats_*_performance 完全一致:
* 业绩 = SUM(amount) WHERE NOT (fulfillment_status <=> 4) AND delete_time IS NULL
* 业绩 = SUM(amount) WHERE fulfillment_status 为 NULL 或 NOT IN (4,9,10),且 delete_time IS NULL
*
* - all: 部门全量业绩(不限渠道)。用于「合计业绩」列展示。
* - scoped: 「{渠道}业绩」:患者渠道 EXISTS 与侧栏/抽屉一致(channels channel_source);有展示行时部门列与「合计业绩」同为列表部门摊行。
@@ -3725,7 +3729,7 @@ class YejiStatsLogic
}
/**
* 渠道业绩:与「处方订单列表」业绩完全同口径 —— 按 o.create_time 归日、剔除已取消(4)(含 NULL);
* 渠道业绩:与「处方订单列表」业绩完全同口径 —— 按 o.create_time 归日、剔除履约 4/9/10(NULL 与其它状态计入);
* EXISTS 仅用于"该患者属于此渠道"的判定(有任一挂号 channels 命中 + status=3**不限挂号时点**);
* 部门归属按业绩归属表达式(订单创建人 o.creator_id)。
*/
@@ -3812,7 +3816,7 @@ class YejiStatsLogic
/**
* 单次业绩聚合:与 aggregatePerformanceAmountsByAdmin / aggregateDealOrderCount 同源 ——
* INNER 诊单,按业绩归属医助 SUM(amount) 后映射到展示「中心」。
* WHERE: delete_time IS NULL、NOT(fulfillment <=> 4)、create_time 区间、diagnosis_id>0、归属医助>0
* WHERE: delete_time IS NULL、fulfillment ∈ (NULL 除 4/9/10 外)、create_time 区间、diagnosis_id>0、归属医助>0
* 可选 tagdiagnosis_id IN、归属医助 IN。
*
* @return array<int, float>
@@ -3859,7 +3863,7 @@ class YejiStatsLogic
}
/**
* 全量成交单数:与 sumGlobalPerformanceAmount 同一 WHERE/COUNT非取消订单条数)。
* 全量成交单数:与 sumGlobalPerformanceAmount 同一 WHERE/COUNT剔除 4/9/10)。
*/
private static function countGlobalDealOrders(int $startTs, int $endTs): int
{
@@ -4516,7 +4520,7 @@ class YejiStatsLogic
'end_date' => (string) $c['endDate'],
'channel_code' => (string) $c['channelCode'],
'rows' => $rows,
'note' => '与看板「未归属中心」补差一致:按订单 create_time、剔除履约已取消(4);**订单创建人**人事部门无法映射到任一展示「中心」计入各行;其余(无诊单/创建人为空)汇总在首行。账号数据权限下仅列出可见医助。',
'note' => '与看板「未归属中心」补差一致:按订单 create_time、剔除履约 4/9/10;**订单创建人**人事部门无法映射到任一展示「中心」计入各行;其余(无诊单/创建人为空)汇总在首行。账号数据权限下仅列出可见医助。',
];
}
+2 -2
View File
@@ -36,8 +36,8 @@ class ExpressAutoUpdate extends Command
$startTime = microtime(true);
try {
$result = ExpressTrackingService::autoUpdateBatch(500);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(500);
$result = ExpressTrackingService::autoUpdateBatch(1000);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(1000);
$duration = round(microtime(true) - $startTime, 2);