更新
This commit is contained in:
@@ -54,6 +54,20 @@ export function yejiStatsUnassignedBreakdown(params: {
|
|||||||
return request.get({ url: '/stats.yejiStats/unassignedBreakdown', params })
|
return request.get({ url: '/stats.yejiStats/unassignedBreakdown', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 部门行「进线数据」逐条明细(与看板同口径) */
|
||||||
|
export function yejiStatsLeadLines(params: {
|
||||||
|
start_date: string
|
||||||
|
end_date: string
|
||||||
|
dept_id: number
|
||||||
|
dept_ids?: string
|
||||||
|
channel_code?: string
|
||||||
|
tag_id?: string
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
}) {
|
||||||
|
return request.get({ url: '/stats.yejiStats/leadLines', params })
|
||||||
|
}
|
||||||
|
|
||||||
export function doctorDailyStatsOverview(params: {
|
export function doctorDailyStatsOverview(params: {
|
||||||
start_date?: string
|
start_date?: string
|
||||||
end_date?: string
|
end_date?: string
|
||||||
|
|||||||
@@ -1912,6 +1912,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-loading="prescriptionViewLoading" class="rx-wrap">
|
<div v-loading="prescriptionViewLoading" class="rx-wrap">
|
||||||
|
<!-- 处方签签 -->
|
||||||
|
<el-tabs v-model="prescriptionTabType" class="mx-4 mt-2" style="margin-bottom: 0px; z-index: 1; position: relative;" v-if="prescriptionViewData">
|
||||||
|
<el-tab-pane label="药房联" name="internal" />
|
||||||
|
<el-tab-pane label="处方联" name="user" />
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
<!-- 状态条(不进入打印/导出范围) -->
|
<!-- 状态条(不进入打印/导出范围) -->
|
||||||
<div class="rx-statusbar" v-if="prescriptionViewData">
|
<div class="rx-statusbar" v-if="prescriptionViewData">
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -1962,7 +1968,7 @@
|
|||||||
|
|
||||||
<!-- A4 处方纸(药房联) -->
|
<!-- A4 处方纸(药房联) -->
|
||||||
<div v-if="prescriptionViewData" ref="prescriptionSlipPrintRef" class="rx-paper">
|
<div v-if="prescriptionViewData" ref="prescriptionSlipPrintRef" class="rx-paper">
|
||||||
<div class="rx-title">处方单</div>
|
<div class="rx-title">{{ prescriptionTabType === 'internal' ? '药房联' : '处方单' }}</div>
|
||||||
<div class="rx-notice">
|
<div class="rx-notice">
|
||||||
<span class="rx-notice-text">
|
<span class="rx-notice-text">
|
||||||
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点
|
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点
|
||||||
@@ -2020,7 +2026,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rx-rp">
|
<div class="rx-rp">
|
||||||
<div class="rx-watermark">药房联</div>
|
<div class="rx-watermark">{{ prescriptionTabType === 'internal' ? '药房联' : '处方联' }}</div>
|
||||||
<div class="rx-rp-head">
|
<div class="rx-rp-head">
|
||||||
<div class="rx-rp-label">Rp.</div>
|
<div class="rx-rp-label">Rp.</div>
|
||||||
<div class="rx-rp-cols-head">
|
<div class="rx-rp-cols-head">
|
||||||
@@ -2036,8 +2042,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="rx-herbs">
|
<div class="rx-herbs">
|
||||||
<div v-for="(h, i) in slipHerbsList" :key="i" class="rx-herb-cell">
|
<div v-for="(h, i) in slipHerbsList" :key="i" class="rx-herb-cell">
|
||||||
<span class="rx-herb-name">{{ h.name }} ({{ h.dosage }}克)</span>
|
<span class="rx-herb-name" v-if="prescriptionTabType === 'internal'">{{ h.name }} ({{ h.dosage }}克)</span>
|
||||||
<span class="rx-herb-total">{{ rxHerbTotal(h.dosage) }}克</span>
|
<span class="rx-herb-name" v-else>{{ h.name }}</span>
|
||||||
|
<span class="rx-herb-total" v-if="prescriptionTabType === 'internal'">{{ rxHerbTotal(h.dosage) }}克</span>
|
||||||
|
<span class="rx-herb-total" v-else>{{ h.dosage }}克</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4408,6 +4416,7 @@ const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄
|
|||||||
const prescriptionViewVisible = ref(false)
|
const prescriptionViewVisible = ref(false)
|
||||||
const prescriptionViewLoading = ref(false)
|
const prescriptionViewLoading = ref(false)
|
||||||
const prescriptionViewData = ref<any>(null)
|
const prescriptionViewData = ref<any>(null)
|
||||||
|
const prescriptionTabType = ref('internal')
|
||||||
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
|
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
|
||||||
const prescriptionSlipExporting = ref(false)
|
const prescriptionSlipExporting = ref(false)
|
||||||
|
|
||||||
@@ -4674,6 +4683,7 @@ async function openPrescriptionView(row: any) {
|
|||||||
prescriptionViewVisible.value = true
|
prescriptionViewVisible.value = true
|
||||||
prescriptionViewLoading.value = true
|
prescriptionViewLoading.value = true
|
||||||
prescriptionViewData.value = null
|
prescriptionViewData.value = null
|
||||||
|
prescriptionTabType.value = 'internal'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await prescriptionDetail({ id: row.prescription_id })
|
const res: any = await prescriptionDetail({ id: row.prescription_id })
|
||||||
|
|||||||
@@ -603,7 +603,13 @@
|
|||||||
@click="onYejiDeptRowClick(tb, r)"
|
@click="onYejiDeptRowClick(tb, r)"
|
||||||
>
|
>
|
||||||
<td class="col-dept">{{ r.dept_name }}</td>
|
<td class="col-dept">{{ r.dept_name }}</td>
|
||||||
<td>{{ formatInt(r.lead_count) }}</td>
|
<td @click="onLeadCountCellClick($event, tb, r)">
|
||||||
|
<span
|
||||||
|
v-if="r.dept_id > 0 && Number(r.lead_count) > 0"
|
||||||
|
class="yeji-lead-cell--link"
|
||||||
|
>{{ formatInt(r.lead_count) }}</span>
|
||||||
|
<template v-else>{{ formatInt(r.lead_count) }}</template>
|
||||||
|
</td>
|
||||||
<td>{{ formatMoney(r.performance_amount) }}</td>
|
<td>{{ formatMoney(r.performance_amount) }}</td>
|
||||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
|
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
|
||||||
<td>{{ formatInt(r.consult_count) }}</td>
|
<td>{{ formatInt(r.consult_count) }}</td>
|
||||||
@@ -858,6 +864,58 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="leadLinesDialogVisible"
|
||||||
|
width="min(920px, 96vw)"
|
||||||
|
destroy-on-close
|
||||||
|
class="yeji-leadlines-dialog"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="yeji-unassigned-dialog__head">
|
||||||
|
<span class="yeji-unassigned-dialog__title">进线数据明细</span>
|
||||||
|
<p class="yeji-unassigned-dialog__sub">{{ leadLinesSubtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-if="leadLinesApiNote" class="yeji-unassigned-dialog__note">{{ leadLinesApiNote }}</p>
|
||||||
|
<div v-loading="leadLinesLoading" class="yeji-unassigned-dialog__body">
|
||||||
|
<el-table
|
||||||
|
v-if="leadLinesRows.length > 0 || leadLinesLoading"
|
||||||
|
:data="leadLinesRows"
|
||||||
|
size="small"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
max-height="440"
|
||||||
|
class="yeji-unassigned-dialog__table"
|
||||||
|
:empty-text="leadLinesLoading ? '加载中…' : '暂无数据'"
|
||||||
|
>
|
||||||
|
<el-table-column prop="event_time_text" label="进线时间" min-width="156" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="reception_admin_name" label="接待" width="88" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="external_contact_name" label="客户" min-width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="external_userid" label="外部联系人ID" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="user_id" label="企微成员ID" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="state" label="渠道参数" width="96" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
<el-empty
|
||||||
|
v-if="!leadLinesLoading && leadLinesRows.length === 0"
|
||||||
|
description="暂无进线明细"
|
||||||
|
/>
|
||||||
|
<div v-if="leadLinesCount > 0" class="yeji-leadlines-dialog__pager">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="leadLinesPage"
|
||||||
|
:page-size="leadLinesPageSize"
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="leadLinesCount"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:disabled="leadLinesLoading"
|
||||||
|
:hide-on-single-page="false"
|
||||||
|
@current-change="onLeadLinesPageChange"
|
||||||
|
@size-change="onLeadLinesPageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -874,6 +932,7 @@ import {
|
|||||||
yejiStatsMulti,
|
yejiStatsMulti,
|
||||||
yejiStatsOverview,
|
yejiStatsOverview,
|
||||||
yejiStatsUnassignedBreakdown,
|
yejiStatsUnassignedBreakdown,
|
||||||
|
yejiStatsLeadLines,
|
||||||
} from '@/api/stats'
|
} from '@/api/stats'
|
||||||
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
|
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
|
||||||
import { prescriptionOrderLists } from '@/api/tcm'
|
import { prescriptionOrderLists } from '@/api/tcm'
|
||||||
@@ -962,6 +1021,17 @@ interface YejiUnassignedBreakdownRow {
|
|||||||
amount: number
|
amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface YejiLeadLineRow {
|
||||||
|
id: number
|
||||||
|
event_time: number
|
||||||
|
event_time_text: string
|
||||||
|
external_userid: string
|
||||||
|
user_id: string
|
||||||
|
state: string
|
||||||
|
reception_admin_name: string
|
||||||
|
external_contact_name: string
|
||||||
|
}
|
||||||
|
|
||||||
interface YejiRow {
|
interface YejiRow {
|
||||||
dept_id: number
|
dept_id: number
|
||||||
dept_name: string
|
dept_name: string
|
||||||
@@ -1045,6 +1115,16 @@ const unassignedDialogSubtitle = ref('')
|
|||||||
const unassignedDialogNote = ref('')
|
const unassignedDialogNote = ref('')
|
||||||
const unassignedDialogRows = ref<YejiUnassignedBreakdownRow[]>([])
|
const unassignedDialogRows = ref<YejiUnassignedBreakdownRow[]>([])
|
||||||
|
|
||||||
|
const leadLinesDialogVisible = ref(false)
|
||||||
|
const leadLinesLoading = ref(false)
|
||||||
|
const leadLinesSubtitle = ref('')
|
||||||
|
const leadLinesApiNote = ref('')
|
||||||
|
const leadLinesRows = ref<YejiLeadLineRow[]>([])
|
||||||
|
const leadLinesCount = ref(0)
|
||||||
|
const leadLinesPage = ref(1)
|
||||||
|
const leadLinesPageSize = ref(20)
|
||||||
|
const leadLinesContext = ref<{ tb: YejiTable; row: YejiRow } | null>(null)
|
||||||
|
|
||||||
/** 图表 / 数据表切换 */
|
/** 图表 / 数据表切换 */
|
||||||
const yejiDisplayTab = ref<'charts' | 'table'>('table')
|
const yejiDisplayTab = ref<'charts' | 'table'>('table')
|
||||||
|
|
||||||
@@ -2039,6 +2119,72 @@ async function openUnassignedBreakdown(tb: YejiTable) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLeadCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) {
|
||||||
|
if (row.dept_id <= 0 || Number(row.lead_count) <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ev.stopPropagation()
|
||||||
|
void openLeadLinesDialog(tb, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLeadLinesPage() {
|
||||||
|
const ctx = leadLinesContext.value
|
||||||
|
if (!ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { tb, row } = ctx
|
||||||
|
leadLinesLoading.value = true
|
||||||
|
try {
|
||||||
|
const p: Record<string, string | number> = {
|
||||||
|
start_date: tb.start_date,
|
||||||
|
end_date: tb.end_date,
|
||||||
|
dept_id: row.dept_id,
|
||||||
|
page: leadLinesPage.value,
|
||||||
|
page_size: leadLinesPageSize.value,
|
||||||
|
}
|
||||||
|
if (selectedDeptIds.value.length > 0) {
|
||||||
|
p.dept_ids = selectedDeptIds.value.join(',')
|
||||||
|
}
|
||||||
|
if (selectedChannel.value) {
|
||||||
|
p.channel_code = selectedChannel.value
|
||||||
|
}
|
||||||
|
const res: any = await yejiStatsLeadLines(p as any)
|
||||||
|
leadLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
|
||||||
|
leadLinesCount.value = Number(res?.count ?? 0)
|
||||||
|
leadLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '加载进线明细失败')
|
||||||
|
leadLinesRows.value = []
|
||||||
|
leadLinesCount.value = 0
|
||||||
|
leadLinesApiNote.value = ''
|
||||||
|
} finally {
|
||||||
|
leadLinesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openLeadLinesDialog(tb: YejiTable, row: YejiRow) {
|
||||||
|
leadLinesContext.value = { tb, row }
|
||||||
|
leadLinesSubtitle.value = `${row.dept_name} · ${buildUnassignedBreakdownSubtitle(tb)}`
|
||||||
|
leadLinesApiNote.value = ''
|
||||||
|
leadLinesRows.value = []
|
||||||
|
leadLinesCount.value = 0
|
||||||
|
leadLinesPage.value = 1
|
||||||
|
leadLinesPageSize.value = 20
|
||||||
|
leadLinesDialogVisible.value = true
|
||||||
|
await fetchLeadLinesPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeadLinesPageChange(p: number) {
|
||||||
|
leadLinesPage.value = p
|
||||||
|
void fetchLeadLinesPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeadLinesPageSizeChange(size: number) {
|
||||||
|
leadLinesPageSize.value = size
|
||||||
|
leadLinesPage.value = 1
|
||||||
|
void fetchLeadLinesPage()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单;「未归属中心」→ 按创建人拆解弹窗
|
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单;「未归属中心」→ 按创建人拆解弹窗
|
||||||
*/
|
*/
|
||||||
@@ -3379,6 +3525,24 @@ onMounted(async () => {
|
|||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.yeji-lead-cell--link {
|
||||||
|
color: var(--yj-brand, #2563eb);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr.yeji-row--action:hover .yeji-lead-cell--link {
|
||||||
|
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yeji-leadlines-dialog__pager {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.date-pill,
|
.date-pill,
|
||||||
.filter-btn,
|
.filter-btn,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,11 +10,11 @@ use app\adminapi\logic\stats\YejiStatsLogic;
|
|||||||
/**
|
/**
|
||||||
* 业绩看板(部门 × 时间区间)控制器
|
* 业绩看板(部门 × 时间区间)控制器
|
||||||
*
|
*
|
||||||
* - GET stats.yeji-stats/overview 单区间
|
* - GET stats.yejiStats/overview 单区间
|
||||||
* - GET stats.yeji-stats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
|
* - GET stats.yejiStats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
|
||||||
* - GET stats.yeji-stats/dept-options 部门下拉
|
* - GET stats.yejiStats/deptOptions 部门下拉
|
||||||
* - GET stats.yeji-stats/leaderboard 医助排行榜(按展示部门分表)
|
* - GET stats.yejiStats/leaderboard 医助排行榜(按展示部门分表)
|
||||||
* - GET stats.yeji-stats/unassignedBreakdown 「未归属中心」合计业绩按订单创建人拆解
|
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
|
||||||
*/
|
*/
|
||||||
class YejiStatsController extends BaseAdminController
|
class YejiStatsController extends BaseAdminController
|
||||||
{
|
{
|
||||||
@@ -112,4 +112,13 @@ class YejiStatsController extends BaseAdminController
|
|||||||
|
|
||||||
return $this->data(YejiStatsLogic::unassignedCenterBreakdown($params, $this->adminId, $this->adminInfo));
|
return $this->data(YejiStatsLogic::unassignedCenterBreakdown($params, $this->adminId, $this->adminInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 进线数据明细:与看板「进线数据」同口径 */
|
||||||
|
public function leadLines()
|
||||||
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
$params = $this->request->get();
|
||||||
|
|
||||||
|
return $this->data(YejiStatsLogic::leadLineList($params, $this->adminId, $this->adminInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1445,6 +1445,203 @@ class YejiStatsLogic
|
|||||||
return $byAdmin;
|
return $byAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进线事件查询公共条件(与 countLeads / countLeadsByAdmin 一致)。
|
||||||
|
*
|
||||||
|
* @param \think\db\BaseQuery $query
|
||||||
|
*/
|
||||||
|
private static function applyLeadEventBaseFilter($query, int $startTs, int $endTs, string $tagFilterId): void
|
||||||
|
{
|
||||||
|
$query->where('e.change_type', 'add_external_contact')
|
||||||
|
->where('e.event_time', 'between', [$startTs, $endTs])
|
||||||
|
->where('e.user_id', '<>', '');
|
||||||
|
if ($tagFilterId !== '') {
|
||||||
|
$tagTable = self::tableWithPrefix('qywx_external_contact_tag');
|
||||||
|
$query->whereRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM {$tagTable} t WHERE t.external_userid = e.external_userid AND t.tag_id = ?)",
|
||||||
|
[$tagFilterId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归属到指定展示「中心」行部门的接待成员企微 userid 列表。
|
||||||
|
*
|
||||||
|
* @param array<int, int> $adminToPrimary
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function collectWorkWxUserIdsForYejiDeptRow(int $deptRowId, array $adminToPrimary): array
|
||||||
|
{
|
||||||
|
$adminIds = [];
|
||||||
|
foreach ($adminToPrimary as $aid => $pid) {
|
||||||
|
if ((int) $pid === $deptRowId) {
|
||||||
|
$adminIds[] = (int) $aid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($adminIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$wids = Db::name('admin')
|
||||||
|
->whereIn('id', $adminIds)
|
||||||
|
->whereNull('delete_time')
|
||||||
|
->column('work_wechat_userid');
|
||||||
|
$out = [];
|
||||||
|
foreach ($wids as $w) {
|
||||||
|
$w = trim((string) $w);
|
||||||
|
if ($w !== '') {
|
||||||
|
$out[] = $w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($out));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进线明细:与看板「进线数据」同口径的 add_external_contact 逐条列表。
|
||||||
|
*
|
||||||
|
* @param array{
|
||||||
|
* start_date?: string,
|
||||||
|
* end_date?: string,
|
||||||
|
* dept_ids?: int[]|string,
|
||||||
|
* channel_code?: string,
|
||||||
|
* tag_id?: string,
|
||||||
|
* dept_id?: int|string,
|
||||||
|
* page?: int|string,
|
||||||
|
* page_size?: int|string
|
||||||
|
* } $params
|
||||||
|
*
|
||||||
|
* @return array{
|
||||||
|
* start_date: string,
|
||||||
|
* end_date: string,
|
||||||
|
* dept_id: int,
|
||||||
|
* dept_name: string,
|
||||||
|
* channel_code: string,
|
||||||
|
* count: int,
|
||||||
|
* lists: list<array<string, mixed>>,
|
||||||
|
* note: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function leadLineList(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||||
|
{
|
||||||
|
$c = self::resolveYejiContext($params);
|
||||||
|
if ($viewerAdminId > 0) {
|
||||||
|
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deptId = (int) ($params['dept_id'] ?? 0);
|
||||||
|
$empty = static function (array $c, int $deptId, string $deptName, string $note) {
|
||||||
|
return [
|
||||||
|
'start_date' => (string) $c['startDate'],
|
||||||
|
'end_date' => (string) $c['endDate'],
|
||||||
|
'dept_id' => $deptId,
|
||||||
|
'dept_name' => $deptName,
|
||||||
|
'channel_code' => (string) $c['channelCode'],
|
||||||
|
'count' => 0,
|
||||||
|
'lists' => [],
|
||||||
|
'note' => $note,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($deptId <= 0) {
|
||||||
|
return $empty($c, 0, '', '未归属中心无进线明细(看板该行进线为 0)。');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($deptId, $c['tableRowDeptIds'], true)) {
|
||||||
|
return $empty(
|
||||||
|
$c,
|
||||||
|
$deptId,
|
||||||
|
self::formatYejiDeptRowDisplayName($deptId, $c['deptById']),
|
||||||
|
'该部门不在当前展示部门筛选或数据权限范围内。'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deptNameMap = $c['deptNameMap'];
|
||||||
|
$channelCode = (string) $c['channelCode'];
|
||||||
|
if ($channelCode !== '' && self::isErCenterDisplayPrimaryName((string) ($deptNameMap[$deptId] ?? ''))) {
|
||||||
|
return $empty(
|
||||||
|
$c,
|
||||||
|
$deptId,
|
||||||
|
self::formatYejiDeptRowDisplayName($deptId, $c['deptById']),
|
||||||
|
'与看板一致:名称含「二中心」的展示部门在选定渠道下进线计 0,无明细。'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTs = (int) $c['startTs'];
|
||||||
|
$endTs = (int) $c['endTs'];
|
||||||
|
$tagFilterId = (string) $c['tagFilterId'];
|
||||||
|
$page = max(1, (int) ($params['page'] ?? 1));
|
||||||
|
$limit = min(100, max(1, (int) ($params['page_size'] ?? 20)));
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$wxUserIds = self::collectWorkWxUserIdsForYejiDeptRow($deptId, $c['adminToPrimary']);
|
||||||
|
$deptName = self::formatYejiDeptRowDisplayName($deptId, $c['deptById']);
|
||||||
|
$ecTable = self::tableWithPrefix('qywx_external_contact');
|
||||||
|
|
||||||
|
if ($wxUserIds === []) {
|
||||||
|
return [
|
||||||
|
'start_date' => (string) $c['startDate'],
|
||||||
|
'end_date' => (string) $c['endDate'],
|
||||||
|
'dept_id' => $deptId,
|
||||||
|
'dept_name' => $deptName,
|
||||||
|
'channel_code' => $channelCode,
|
||||||
|
'count' => 0,
|
||||||
|
'lists' => [],
|
||||||
|
'note' => '当前部门下没有可映射到接待企微成员的管理员,无进线流水。',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQuery = Db::name('qywx_external_contact_event')->alias('e');
|
||||||
|
self::applyLeadEventBaseFilter($countQuery, $startTs, $endTs, $tagFilterId);
|
||||||
|
$countQuery->whereIn('e.user_id', $wxUserIds);
|
||||||
|
$count = (int) $countQuery->count();
|
||||||
|
|
||||||
|
$listQuery = Db::name('qywx_external_contact_event')->alias('e')
|
||||||
|
->leftJoin('admin a', 'a.work_wechat_userid = e.user_id AND a.delete_time IS NULL')
|
||||||
|
->leftJoin("{$ecTable} ec", 'ec.external_userid = e.external_userid AND ec.delete_time IS NULL');
|
||||||
|
self::applyLeadEventBaseFilter($listQuery, $startTs, $endTs, $tagFilterId);
|
||||||
|
$listQuery->whereIn('e.user_id', $wxUserIds);
|
||||||
|
$rawList = $listQuery
|
||||||
|
->field(
|
||||||
|
'e.id,e.event_time,e.external_userid,e.user_id,e.state,'
|
||||||
|
. 'a.name AS reception_admin_name,ec.name AS external_contact_name'
|
||||||
|
)
|
||||||
|
->order('e.event_time', 'desc')
|
||||||
|
->order('e.id', 'desc')
|
||||||
|
->limit($offset, $limit)
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$lists = [];
|
||||||
|
foreach ($rawList as $rw) {
|
||||||
|
$ts = (int) ($rw['event_time'] ?? 0);
|
||||||
|
$lists[] = [
|
||||||
|
'id' => (int) ($rw['id'] ?? 0),
|
||||||
|
'event_time' => $ts,
|
||||||
|
'event_time_text' => $ts > 0 ? date('Y-m-d H:i:s', $ts) : '',
|
||||||
|
'external_userid' => (string) ($rw['external_userid'] ?? ''),
|
||||||
|
'user_id' => (string) ($rw['user_id'] ?? ''),
|
||||||
|
'state' => (string) ($rw['state'] ?? ''),
|
||||||
|
'reception_admin_name' => (string) ($rw['reception_admin_name'] ?? ''),
|
||||||
|
'external_contact_name' => (string) ($rw['external_contact_name'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$note = '与看板「进线数据」同口径:企微 change_type=add_external_contact,按接待成员归属当前部门行;'
|
||||||
|
. ($tagFilterId !== '' ? '已按所选渠道标签过滤客户。' : '未选渠道时不限制客户标签。');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'start_date' => (string) $c['startDate'],
|
||||||
|
'end_date' => (string) $c['endDate'],
|
||||||
|
'dept_id' => $deptId,
|
||||||
|
'dept_name' => $deptName,
|
||||||
|
'channel_code' => $channelCode,
|
||||||
|
'count' => $count,
|
||||||
|
'lists' => $lists,
|
||||||
|
'note' => $note,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与 sumPerformance 对应,按 admin 维度返回全量/渠道业绩金额(不按部门折叠)。
|
* 与 sumPerformance 对应,按 admin 维度返回全量/渠道业绩金额(不按部门折叠)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use think\console\Output;
|
|||||||
* 使用方法:
|
* 使用方法:
|
||||||
* php think express:auto-update
|
* php think express:auto-update
|
||||||
*
|
*
|
||||||
* 配置crontab(每10分钟执行一次):
|
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货/已签收」核对释放诊单医助(与是否甘草单无关)
|
||||||
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
|
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
|
||||||
*/
|
*/
|
||||||
class ExpressAutoUpdate extends Command
|
class ExpressAutoUpdate extends Command
|
||||||
@@ -34,6 +34,7 @@ class ExpressAutoUpdate extends Command
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$result = ExpressTrackingService::autoUpdateBatch(200);
|
$result = ExpressTrackingService::autoUpdateBatch(200);
|
||||||
|
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(200);
|
||||||
|
|
||||||
$duration = round(microtime(true) - $startTime, 2);
|
$duration = round(microtime(true) - $startTime, 2);
|
||||||
|
|
||||||
@@ -41,9 +42,9 @@ class ExpressAutoUpdate extends Command
|
|||||||
$output->writeln("总数: {$result['total']}");
|
$output->writeln("总数: {$result['total']}");
|
||||||
$output->writeln("成功: {$result['success']}");
|
$output->writeln("成功: {$result['success']}");
|
||||||
$output->writeln("失败: {$result['failed']}");
|
$output->writeln("失败: {$result['failed']}");
|
||||||
$output->writeln("医助已移除: " . (int) ($result['assistant_cleared'] ?? 0));
|
$output->writeln("医助已移除(物流任务+指派日志): " . (int) ($result['assistant_cleared'] ?? 0));
|
||||||
$output->writeln("医助保留(有指派记录): " . (int) ($result['assistant_skipped_assign_log'] ?? 0));
|
$output->writeln("医助已移除(履约已发货/签收核对): " . (int) ($recon['cleared'] ?? 0) . " (扫描 " . (int) ($recon['scanned'] ?? 0) . " 单)");
|
||||||
$lines = $result['assistant_lines'] ?? [];
|
$lines = array_merge($result['assistant_lines'] ?? [], $recon['lines'] ?? []);
|
||||||
if ($lines === []) {
|
if ($lines === []) {
|
||||||
$output->writeln('医助明细: (无)');
|
$output->writeln('医助明细: (无)');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\command;
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\service\ExpressTrackingService;
|
||||||
use app\common\service\gancao\GancaoLogisticsRouteService;
|
use app\common\service\gancao\GancaoLogisticsRouteService;
|
||||||
use app\common\service\gancao\GancaoScmRecipelService;
|
use app\common\service\gancao\GancaoScmRecipelService;
|
||||||
use think\console\Command;
|
use think\console\Command;
|
||||||
@@ -60,6 +61,10 @@ class GancaoSyncLogisticsRoute extends Command
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$stats = GancaoLogisticsRouteService::syncBatch($limit, $onlyOrderId);
|
$stats = GancaoLogisticsRouteService::syncBatch($limit, $onlyOrderId);
|
||||||
|
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(200);
|
||||||
|
$stats['reconcile_cleared'] = (int) ($recon['cleared'] ?? 0);
|
||||||
|
$stats['reconcile_scanned'] = (int) ($recon['scanned'] ?? 0);
|
||||||
|
$stats['reconcile_lines'] = $recon['lines'] ?? [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$output->error('同步异常:' . $e->getMessage());
|
$output->error('同步异常:' . $e->getMessage());
|
||||||
return 1;
|
return 1;
|
||||||
@@ -94,9 +99,9 @@ class GancaoSyncLogisticsRoute extends Command
|
|||||||
$output->writeln('总数:' . $stats['total']);
|
$output->writeln('总数:' . $stats['total']);
|
||||||
$output->writeln('成功:' . $stats['success']);
|
$output->writeln('成功:' . $stats['success']);
|
||||||
$output->writeln('失败:' . $stats['failed']);
|
$output->writeln('失败:' . $stats['failed']);
|
||||||
$output->writeln('医助已移除:' . (int) ($stats['assistant_cleared'] ?? 0));
|
$output->writeln('医助已移除(甘草同步+指派日志):' . (int) ($stats['assistant_cleared'] ?? 0));
|
||||||
$output->writeln('医助保留(有指派记录):' . (int) ($stats['assistant_skipped_assign_log'] ?? 0));
|
$output->writeln('医助已移除(履约已发货/签收核对):' . (int) ($stats['reconcile_cleared'] ?? 0) . '(扫描 ' . (int) ($stats['reconcile_scanned'] ?? 0) . ' 单)');
|
||||||
$lines = $stats['assistant_lines'] ?? [];
|
$lines = array_merge($stats['assistant_lines'] ?? [], $stats['reconcile_lines'] ?? []);
|
||||||
if ($lines === []) {
|
if ($lines === []) {
|
||||||
$output->writeln('医助明细:(无)');
|
$output->writeln('医助明细:(无)');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use app\common\model\tcm\Diagnosis;
|
|||||||
use app\common\model\tcm\DiagnosisAssignLog;
|
use app\common\model\tcm\DiagnosisAssignLog;
|
||||||
use app\common\model\tcm\PrescriptionOrder;
|
use app\common\model\tcm\PrescriptionOrder;
|
||||||
use app\common\model\tcm\PrescriptionOrderLog;
|
use app\common\model\tcm\PrescriptionOrderLog;
|
||||||
|
use think\facade\Db;
|
||||||
use think\facade\Log;
|
use think\facade\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,7 +177,7 @@ class ExpressTrackingService
|
|||||||
self::logStateChange($tracking, $oldState);
|
self::logStateChange($tracking, $oldState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定时拉轨迹:业务订单已发货时释放诊单医助(有正式指派记录则跳过,与结案逻辑一致)
|
// 定时拉轨迹:履约已发货/已签收时与甘草侧共用 applyAssistantReleaseForShippedPrescriptionOrder,释放诊单医助并写指派日志
|
||||||
$assistantSync = null;
|
$assistantSync = null;
|
||||||
if ($isAuto) {
|
if ($isAuto) {
|
||||||
$assistantSync = self::maybeClearDiagnosisAssistantWhenShippedPrescription($tracking);
|
$assistantSync = self::maybeClearDiagnosisAssistantWhenShippedPrescription($tracking);
|
||||||
@@ -242,20 +243,19 @@ class ExpressTrackingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 业务订单已发货(5)/已签收(6)时清空关联诊单医助;存在 tcm_diagnosis_assign_log 则跳过。
|
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log。
|
||||||
* 供 express:auto-update、gancao:sync-logistics、甘草回调等共用。
|
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
|
||||||
|
* 例外:指派日志中若存在「从某医助改派到另一医助」(原、新医助 ID 均大于 0)的记录,视为人工指派链,自动释放跳过。
|
||||||
*
|
*
|
||||||
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
|
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
|
||||||
* @return array{
|
* @return array{
|
||||||
* action: 'cleared'|'skipped_assign_log',
|
* action: 'cleared',
|
||||||
* source?: string,
|
* source?: string,
|
||||||
* diagnosis_id: int,
|
* diagnosis_id: int,
|
||||||
* prescription_order_id: int,
|
* prescription_order_id: int,
|
||||||
* tracking_id: int,
|
* tracking_id: int,
|
||||||
* tracking_number: string,
|
* tracking_number: string,
|
||||||
* former_assistant_id?: string,
|
* former_assistant_id?: string,
|
||||||
* assistant_id?: string,
|
|
||||||
* assign_log_count?: int,
|
|
||||||
* fulfillment_status?: int
|
* fulfillment_status?: int
|
||||||
* }|null
|
* }|null
|
||||||
*/
|
*/
|
||||||
@@ -284,52 +284,73 @@ class ExpressTrackingService
|
|||||||
if (!$diag) {
|
if (!$diag) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$oldAssistant = (string) ($diag->getAttr('assistant_id') ?? '');
|
$fromAssistantId = (int) ($diag->getAttr('assistant_id') ?? 0);
|
||||||
if ($oldAssistant === '' || $oldAssistant === '0') {
|
if ($fromAssistantId <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$assignLogCount = (int) DiagnosisAssignLog::where('diagnosis_id', $diagnosisId)
|
if (self::diagnosisHasFormalAssistantReassignInAssignLog($diagnosisId)) {
|
||||||
->whereNull('delete_time')
|
Log::info('Skipped auto assistant release (shipped/signed): formal assistant reassign in assign log', [
|
||||||
->count();
|
'diagnosis_id' => $diagnosisId,
|
||||||
if ($assignLogCount > 0) {
|
|
||||||
return [
|
|
||||||
'action' => 'skipped_assign_log',
|
|
||||||
'source' => $source,
|
|
||||||
'diagnosis_id' => $diagnosisId,
|
|
||||||
'prescription_order_id' => $orderId,
|
'prescription_order_id' => $orderId,
|
||||||
'tracking_id' => $trackingId,
|
'source' => $source,
|
||||||
'tracking_number' => $tn,
|
'current_assistant_id' => $fromAssistantId,
|
||||||
'assistant_id' => $oldAssistant,
|
]);
|
||||||
'assign_log_count' => $assignLogCount,
|
|
||||||
'fulfillment_status' => $fs,
|
return null;
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->update(['assistant_id' => '']);
|
$operatorName = match ($source) {
|
||||||
|
'express_auto_update' => '系统·物流自动同步',
|
||||||
|
'gancao_route', 'gancao_route_sync' => '系统·甘草路由同步',
|
||||||
|
'gancao_callback' => '系统·甘草回调',
|
||||||
|
'shipped_fulfillment_reconcile' => '系统·已发货履约核对',
|
||||||
|
default => '系统·订单已发货/签收',
|
||||||
|
};
|
||||||
|
|
||||||
Log::info('Cleared diagnosis assistant (shipped/signed prescription order)', [
|
Db::startTrans();
|
||||||
'source' => $source,
|
try {
|
||||||
'diagnosis_id' => $diagnosisId,
|
Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->update(['assistant_id' => 0]);
|
||||||
|
|
||||||
|
DiagnosisAssignLog::create([
|
||||||
|
'diagnosis_id' => $diagnosisId,
|
||||||
|
'from_assistant_id' => $fromAssistantId,
|
||||||
|
'to_assistant_id' => 0,
|
||||||
|
'operator_admin_id' => 0,
|
||||||
|
'operator_name' => $operatorName,
|
||||||
|
'operator_account' => '',
|
||||||
|
'ip' => '',
|
||||||
|
'create_time' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Cleared diagnosis assistant (prescription shipped/signed), assign log written', [
|
||||||
|
'source' => $source,
|
||||||
|
'diagnosis_id' => $diagnosisId,
|
||||||
'prescription_order_id' => $orderId,
|
'prescription_order_id' => $orderId,
|
||||||
'tracking_id' => $trackingId,
|
'tracking_id' => $trackingId,
|
||||||
'fulfillment_status' => $fs,
|
'fulfillment_status' => $fs,
|
||||||
'former_assistant_id' => $oldAssistant,
|
'former_assistant_id' => $fromAssistantId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'action' => 'cleared',
|
'action' => 'cleared',
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
'diagnosis_id' => $diagnosisId,
|
'diagnosis_id' => $diagnosisId,
|
||||||
'prescription_order_id' => $orderId,
|
'prescription_order_id' => $orderId,
|
||||||
'tracking_id' => $trackingId,
|
'tracking_id' => $trackingId,
|
||||||
'tracking_number' => $tn,
|
'tracking_number' => $tn,
|
||||||
'former_assistant_id' => $oldAssistant,
|
'former_assistant_id' => (string) $fromAssistantId,
|
||||||
'fulfillment_status' => $fs,
|
'fulfillment_status' => $fs,
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::warning('applyAssistantReleaseForShippedPrescriptionOrder failed: ' . $e->getMessage(), [
|
Log::warning('applyAssistantReleaseForShippedPrescriptionOrder failed: ' . $e->getMessage(), [
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
'order_id' => $orderId,
|
'order_id' => $orderId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -338,19 +359,20 @@ class ExpressTrackingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* 是否存在「由原医助改派到新医助」的指派记录(两端 admin 均大于 0)。有此记录则已发货自动释放医助不执行。
|
||||||
* action: 'cleared'|'skipped_assign_log',
|
|
||||||
* source?: string,
|
|
||||||
* diagnosis_id: int,
|
|
||||||
* prescription_order_id: int,
|
|
||||||
* tracking_id: int,
|
|
||||||
* tracking_number: string,
|
|
||||||
* former_assistant_id?: string,
|
|
||||||
* assistant_id?: string,
|
|
||||||
* assign_log_count?: int,
|
|
||||||
* fulfillment_status?: int
|
|
||||||
* }|null
|
|
||||||
*/
|
*/
|
||||||
|
private static function diagnosisHasFormalAssistantReassignInAssignLog(int $diagnosisId): bool
|
||||||
|
{
|
||||||
|
if ($diagnosisId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiagnosisAssignLog::where('diagnosis_id', $diagnosisId)
|
||||||
|
->where('from_assistant_id', '>', 0)
|
||||||
|
->where('to_assistant_id', '>', 0)
|
||||||
|
->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static function maybeClearDiagnosisAssistantWhenShippedPrescription(ExpressTracking $tracking): ?array
|
private static function maybeClearDiagnosisAssistantWhenShippedPrescription(ExpressTracking $tracking): ?array
|
||||||
{
|
{
|
||||||
if ((string) $tracking->order_type !== 'prescription') {
|
if ((string) $tracking->order_type !== 'prescription') {
|
||||||
@@ -368,14 +390,14 @@ class ExpressTrackingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return self::applyAssistantReleaseForShippedPrescriptionOrder($order, [
|
return self::applyAssistantReleaseForShippedPrescriptionOrder($order, [
|
||||||
'tracking_id' => (int) $tracking->id,
|
'tracking_id' => (int) $tracking->id,
|
||||||
'tracking_number' => (string) $tracking->tracking_number,
|
'tracking_number' => (string) $tracking->tracking_number,
|
||||||
'source' => 'express_auto_update',
|
'source' => 'express_auto_update',
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::warning('maybeClearDiagnosisAssistantWhenShippedPrescription failed: ' . $e->getMessage(), [
|
Log::warning('maybeClearDiagnosisAssistantWhenShippedPrescription failed: ' . $e->getMessage(), [
|
||||||
'tracking_id' => (int) $tracking->id,
|
'tracking_id' => (int) $tracking->id,
|
||||||
'order_id' => $orderId,
|
'order_id' => $orderId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -528,6 +550,54 @@ class ExpressTrackingService
|
|||||||
$log->save();
|
$log->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按处方订单履约核对:已发货(5)/已签收(6) 时诊单仍挂在医助的,一律释放并写指派日志。
|
||||||
|
* 与快递 100 / 甘草来自哪个物流渠道无关,保障「已发货」后患者侧医助被清掉。
|
||||||
|
*
|
||||||
|
* @return array{scanned: int, cleared: int, lines: list<string>}
|
||||||
|
*/
|
||||||
|
public static function reconcileAssistantReleaseForShippedPrescriptionOrders(int $limit = 200): array
|
||||||
|
{
|
||||||
|
$dTable = (new Diagnosis())->getTable();
|
||||||
|
$orders = PrescriptionOrder::alias('po')
|
||||||
|
->join($dTable . ' d', 'd.id = po.diagnosis_id')
|
||||||
|
->whereNull('po.delete_time')
|
||||||
|
->whereNull('d.delete_time')
|
||||||
|
->whereIn('po.fulfillment_status', [5, 6])
|
||||||
|
->where('po.diagnosis_id', '>', 0)
|
||||||
|
->where('d.assistant_id', '>', 0)
|
||||||
|
->order('po.id', 'asc')
|
||||||
|
->limit($limit)
|
||||||
|
->field('po.*')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$cleared = 0;
|
||||||
|
$lines = [];
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$sync = self::applyAssistantReleaseForShippedPrescriptionOrder($order, [
|
||||||
|
'source' => 'shipped_fulfillment_reconcile',
|
||||||
|
'tracking_number' => (string) ($order->tracking_number ?? ''),
|
||||||
|
]);
|
||||||
|
if (is_array($sync) && ($sync['action'] ?? '') === 'cleared') {
|
||||||
|
$cleared++;
|
||||||
|
$lines[] = sprintf(
|
||||||
|
'[履约核对·已发货/签收] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
|
||||||
|
(int) ($sync['diagnosis_id'] ?? 0),
|
||||||
|
(int) ($sync['prescription_order_id'] ?? 0),
|
||||||
|
(string) ($sync['tracking_number'] ?? ''),
|
||||||
|
(string) ($sync['former_assistant_id'] ?? ''),
|
||||||
|
(int) ($sync['fulfillment_status'] ?? 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scanned' => count($orders),
|
||||||
|
'cleared' => $cleared,
|
||||||
|
'lines' => $lines,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量自动更新(定时任务调用)
|
* 批量自动更新(定时任务调用)
|
||||||
*
|
*
|
||||||
@@ -539,7 +609,7 @@ class ExpressTrackingService
|
|||||||
* assistant_cleared: int,
|
* assistant_cleared: int,
|
||||||
* assistant_skipped_assign_log: int,
|
* assistant_skipped_assign_log: int,
|
||||||
* assistant_lines: list<string>
|
* assistant_lines: list<string>
|
||||||
* }
|
* } assistant_skipped_assign_log 保留兼容恒为 0(已发货/签收一律写指派日志并清空医助)
|
||||||
*/
|
*/
|
||||||
public static function autoUpdateBatch(int $limit = 50): array
|
public static function autoUpdateBatch(int $limit = 50): array
|
||||||
{
|
{
|
||||||
@@ -580,7 +650,6 @@ class ExpressTrackingService
|
|||||||
$success = 0;
|
$success = 0;
|
||||||
$failed = 0;
|
$failed = 0;
|
||||||
$assistantCleared = 0;
|
$assistantCleared = 0;
|
||||||
$assistantSkippedAssign = 0;
|
|
||||||
$assistantLines = [];
|
$assistantLines = [];
|
||||||
|
|
||||||
foreach ($list as $tracking) {
|
foreach ($list as $tracking) {
|
||||||
@@ -593,24 +662,13 @@ class ExpressTrackingService
|
|||||||
if ($act === 'cleared') {
|
if ($act === 'cleared') {
|
||||||
$assistantCleared++;
|
$assistantCleared++;
|
||||||
$assistantLines[] = sprintf(
|
$assistantLines[] = sprintf(
|
||||||
'[移除医助] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
|
'[移除医助+指派日志] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
|
||||||
(int) ($sync['diagnosis_id'] ?? 0),
|
(int) ($sync['diagnosis_id'] ?? 0),
|
||||||
(int) ($sync['prescription_order_id'] ?? 0),
|
(int) ($sync['prescription_order_id'] ?? 0),
|
||||||
(string) ($sync['tracking_number'] ?? ''),
|
(string) ($sync['tracking_number'] ?? ''),
|
||||||
(string) ($sync['former_assistant_id'] ?? ''),
|
(string) ($sync['former_assistant_id'] ?? ''),
|
||||||
(int) ($sync['fulfillment_status'] ?? 0)
|
(int) ($sync['fulfillment_status'] ?? 0)
|
||||||
);
|
);
|
||||||
} elseif ($act === 'skipped_assign_log') {
|
|
||||||
$assistantSkippedAssign++;
|
|
||||||
$assistantLines[] = sprintf(
|
|
||||||
'[保留医助] 诊单=%d 业务订单=%d 运单=%s 医助ID=%s 指派记录=%d条 履约状态=%d',
|
|
||||||
(int) ($sync['diagnosis_id'] ?? 0),
|
|
||||||
(int) ($sync['prescription_order_id'] ?? 0),
|
|
||||||
(string) ($sync['tracking_number'] ?? ''),
|
|
||||||
(string) ($sync['assistant_id'] ?? ''),
|
|
||||||
(int) ($sync['assign_log_count'] ?? 0),
|
|
||||||
(int) ($sync['fulfillment_status'] ?? 0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -628,7 +686,7 @@ class ExpressTrackingService
|
|||||||
'success' => $success,
|
'success' => $success,
|
||||||
'failed' => $failed,
|
'failed' => $failed,
|
||||||
'assistant_cleared' => $assistantCleared,
|
'assistant_cleared' => $assistantCleared,
|
||||||
'assistant_skipped_assign_log' => $assistantSkippedAssign,
|
'assistant_skipped_assign_log' => 0,
|
||||||
'assistant_lines' => $assistantLines,
|
'assistant_lines' => $assistantLines,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,24 +484,13 @@ final class GancaoLogisticsRouteService
|
|||||||
if ($act === 'cleared') {
|
if ($act === 'cleared') {
|
||||||
$stats['assistant_cleared']++;
|
$stats['assistant_cleared']++;
|
||||||
$stats['assistant_lines'][] = sprintf(
|
$stats['assistant_lines'][] = sprintf(
|
||||||
'[移除医助][甘草路由] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
|
'[移除医助+指派日志][甘草路由] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
|
||||||
(int) ($sync['diagnosis_id'] ?? 0),
|
(int) ($sync['diagnosis_id'] ?? 0),
|
||||||
(int) ($sync['prescription_order_id'] ?? 0),
|
(int) ($sync['prescription_order_id'] ?? 0),
|
||||||
(string) ($sync['tracking_number'] ?? ''),
|
(string) ($sync['tracking_number'] ?? ''),
|
||||||
(string) ($sync['former_assistant_id'] ?? ''),
|
(string) ($sync['former_assistant_id'] ?? ''),
|
||||||
(int) ($sync['fulfillment_status'] ?? 0)
|
(int) ($sync['fulfillment_status'] ?? 0)
|
||||||
);
|
);
|
||||||
} elseif ($act === 'skipped_assign_log') {
|
|
||||||
$stats['assistant_skipped_assign_log']++;
|
|
||||||
$stats['assistant_lines'][] = sprintf(
|
|
||||||
'[保留医助][甘草路由] 诊单=%d 业务订单=%d 运单=%s 医助ID=%s 指派记录=%d条 履约状态=%d',
|
|
||||||
(int) ($sync['diagnosis_id'] ?? 0),
|
|
||||||
(int) ($sync['prescription_order_id'] ?? 0),
|
|
||||||
(string) ($sync['tracking_number'] ?? ''),
|
|
||||||
(string) ($sync['assistant_id'] ?? ''),
|
|
||||||
(int) ($sync['assign_log_count'] ?? 0),
|
|
||||||
(int) ($sync['fulfillment_status'] ?? 0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 业绩看板「进线数据」明细接口(fans/yeji · pid=293)
|
||||||
|
-- 幂等:按 perms 判重
|
||||||
|
|
||||||
|
INSERT INTO `zyt_system_menu`
|
||||||
|
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||||
|
SELECT
|
||||||
|
293, 'A', '业绩-进线明细', '', 10,
|
||||||
|
'stats.yejiStats/leadLines', '', '', '', '',
|
||||||
|
1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||||
|
FROM DUAL
|
||||||
|
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` p WHERE p.id = 293)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `zyt_system_menu` m
|
||||||
|
WHERE m.perms = 'stats.yejiStats/leadLines'
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user