更新
This commit is contained in:
+20
-1
@@ -110,6 +110,13 @@ const COMMISSION_SETTLEMENT_TIMEOUT_MS = 120000
|
|||||||
/** 提成结算业绩(独立于业绩看板 yejiStats) */
|
/** 提成结算业绩(独立于业绩看板 yejiStats) */
|
||||||
export function commissionSettlementOverview(params: {
|
export function commissionSettlementOverview(params: {
|
||||||
settlement_month: string
|
settlement_month: string
|
||||||
|
/** 与 tcm.prescriptionOrder/lists 同源:create_time between */
|
||||||
|
start_time?: string
|
||||||
|
end_time?: string
|
||||||
|
/** 默认 3=履约完成,与列表 fulfillment_status 一致 */
|
||||||
|
fulfillment_status?: number
|
||||||
|
/** 传 1 时仅统计 is_system_auto=1;显式时段下默认不传(含手动) */
|
||||||
|
require_system_auto_prescription?: 0 | 1
|
||||||
dept_ids?: number[] | string
|
dept_ids?: number[] | string
|
||||||
channel_code?: string
|
channel_code?: string
|
||||||
}) {
|
}) {
|
||||||
@@ -127,14 +134,21 @@ export function commissionSettlementChannelOptions() {
|
|||||||
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
|
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提成核对:订单明细分页 bucket: 空|current|deferred */
|
/** 提成核对:订单明细分页 bucket: 空|current|deferred;appt_channel_value 可与 assistant_id/doctor_id 组合(0=未匹配挂号渠道) */
|
||||||
export function commissionSettlementOrderLines(params: {
|
export function commissionSettlementOrderLines(params: {
|
||||||
settlement_month: string
|
settlement_month: string
|
||||||
|
start_time?: string
|
||||||
|
end_time?: string
|
||||||
|
fulfillment_status?: number
|
||||||
|
require_system_auto_prescription?: 0 | 1
|
||||||
dept_ids?: number[] | string
|
dept_ids?: number[] | string
|
||||||
channel_code?: string
|
channel_code?: string
|
||||||
page?: number
|
page?: number
|
||||||
page_size?: number
|
page_size?: number
|
||||||
bucket?: string
|
bucket?: string
|
||||||
|
assistant_id?: number
|
||||||
|
doctor_id?: number
|
||||||
|
appt_channel_value?: number
|
||||||
}) {
|
}) {
|
||||||
return request.get(
|
return request.get(
|
||||||
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||||
@@ -160,3 +174,8 @@ export function commissionSettlementConfirmFinalize(params: Record<string, any>)
|
|||||||
{ ignoreCancelToken: true }
|
{ ignoreCancelToken: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 撤回「确定本期业绩」:清除顺延结转,状态变为可再次核对/确定 */
|
||||||
|
export function commissionSettlementConfirmRevoke(params: Record<string, any>) {
|
||||||
|
return request.post({ url: '/stats.commissionSettlement/confirmRevoke', params })
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
</span>
|
</span>
|
||||||
<h1 class="cs-masthead__title">提成结算业绩</h1>
|
<h1 class="cs-masthead__title">提成结算业绩</h1>
|
||||||
<p class="cs-masthead__desc">
|
<p class="cs-masthead__desc">
|
||||||
系统代开处方对应的已完成业务订单;按结算月 7 日截止拆分本期提成与顺延下期;可按部门汇总或下发至业绩归属医助与处方开方医生
|
订单池默认与<strong>处方订单列表</strong>同源:<strong>创建时间 start_time~end_time</strong> +
|
||||||
|
<strong>fulfillment_status</strong>(默认完成态 3),账号<strong>列表可见性/数据域</strong>一并生效。
|
||||||
|
选结算月<strong>仍可</strong>套用「物流签收<strong>与</strong>尾款支付<strong>均须在</strong><strong>结算月 7 日 24 点</strong>前」拆分「本期提成 / 顺延下期」
|
||||||
|
(仅有签收在范围内、尾款晚于截止仍会顺延);
|
||||||
|
展示部门/渠道下拉与列表不同,勾选后会与列表<strong>计数不同</strong>。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<dl v-if="meta.settlement_month || meta.cutoff_end" class="cs-masthead__stats">
|
<dl v-if="meta.settlement_month || meta.cutoff_end" class="cs-masthead__stats">
|
||||||
@@ -17,9 +21,9 @@
|
|||||||
<dt class="cs-stat__label">结算月</dt>
|
<dt class="cs-stat__label">结算月</dt>
|
||||||
<dd class="cs-stat__value">{{ meta.settlement_month || settlementMonth }}</dd>
|
<dd class="cs-stat__value">{{ meta.settlement_month || settlementMonth }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="meta.order_month" class="cs-stat">
|
<div v-if="meta.order_month || meta.order_start_time" class="cs-stat">
|
||||||
<dt class="cs-stat__label">订单创建月</dt>
|
<dt class="cs-stat__label">订单时间窗</dt>
|
||||||
<dd class="cs-stat__value">{{ meta.order_month }}</dd>
|
<dd class="cs-stat__value cs-stat__value--xs">{{ orderWindowLabel }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="meta.cutoff_end" class="cs-stat">
|
<div v-if="meta.cutoff_end" class="cs-stat">
|
||||||
<dt class="cs-stat__label">截止时刻</dt>
|
<dt class="cs-stat__label">截止时刻</dt>
|
||||||
@@ -42,6 +46,21 @@
|
|||||||
class="filter-month"
|
class="filter-month"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-field filter-field--rangewrap">
|
||||||
|
<span class="lbl">订单创建</span>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="orderTimeRange"
|
||||||
|
type="datetimerange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="列表 start_time"
|
||||||
|
end-placeholder="列表 end_time"
|
||||||
|
class="filter-datetimerange"
|
||||||
|
/>
|
||||||
|
<el-checkbox v-model="requireSystemAutoOnly" class="filter-sa-check">
|
||||||
|
仅系统代开处方
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
<div class="filter-field">
|
<div class="filter-field">
|
||||||
<span class="lbl">渠道来源</span>
|
<span class="lbl">渠道来源</span>
|
||||||
<el-select
|
<el-select
|
||||||
@@ -117,17 +136,26 @@
|
|||||||
<div class="cs-alert__body">
|
<div class="cs-alert__body">
|
||||||
<p v-if="meta.rule_note">{{ meta.rule_note }}</p>
|
<p v-if="meta.rule_note">{{ meta.rule_note }}</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
订单创建月为<strong>结算月的上一自然月</strong>;仅<strong>系统代开处方</strong>(完成挂号等自动生成)对应的<strong>履约已完成</strong>业务订单。
|
请选择结算月并设置<strong>订单创建时间范围</strong>(与处方订单列表 URL 参数
|
||||||
<strong>签收</strong>时间取物流签收时间;<strong>尾款支付</strong>取关联支付单已支付时间(优先尾款/全部费用类型)。
|
<code>start_time</code>/<code>end_time</code>/<code>fulfillment_status</code>
|
||||||
签收与尾款均在<strong>结算月 7 日 24:00 前</strong>完成的计入「本期提成」,其余计入「顺延下期」。
|
一致);切换结算月会自动带出<strong>上一自然月整段</strong>作为默认时段,可按业务修改(例如 2026-04-02~04-30)。
|
||||||
|
「仅系统代开处方」勾选后与列表在未限制开方类型时的条数会不一致。
|
||||||
</p>
|
</p>
|
||||||
<p v-if="meta.cutoff_end" class="cs-alert__meta">
|
<p v-if="meta.cutoff_end" class="cs-alert__meta">
|
||||||
当前结算月:<strong>{{ meta.settlement_month }}</strong>
|
当前结算月:<strong>{{ meta.settlement_month }}</strong>
|
||||||
· 订单创建月:<strong>{{ meta.order_month }}</strong>
|
· 订单时间:<strong>{{ orderWindowLabel }}</strong>
|
||||||
|
· 履约状态:<strong>{{ meta.fulfillment_status ?? 3 }}</strong>
|
||||||
· 截止时刻:<strong>{{ meta.cutoff_end }}</strong>
|
· 截止时刻:<strong>{{ meta.cutoff_end }}</strong>
|
||||||
<template v-if="meta.channel_name">
|
<template v-if="meta.channel_name">
|
||||||
· 渠道:<strong>{{ meta.channel_name }}</strong>
|
· 渠道:<strong>{{ meta.channel_name }}</strong>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="meta.settlement_month">
|
||||||
|
· 处方池:<strong>{{
|
||||||
|
meta.require_system_auto_prescription ? '仅系统代开' : '含手动与系统'
|
||||||
|
}}</strong>
|
||||||
|
<template v-if="meta.explicit_order_time_window">
|
||||||
|
· 列表对齐:<strong>是</strong></template>
|
||||||
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
@@ -136,27 +164,43 @@
|
|||||||
<div class="cs-reconcile">
|
<div class="cs-reconcile">
|
||||||
<div class="cs-reconcile__head">
|
<div class="cs-reconcile__head">
|
||||||
<div class="cs-reconcile__tags">
|
<div class="cs-reconcile__tags">
|
||||||
<el-tag v-if="confirmInfo?.status === 1" type="success" effect="dark">已确定业绩</el-tag>
|
<el-tag v-if="settlementConfirmedLocked" type="success" effect="dark">已确定业绩</el-tag>
|
||||||
<el-tag v-else type="warning" effect="plain">待确定业绩</el-tag>
|
<el-tag v-else-if="confirmInfo?.status === 2" type="warning" effect="plain">已撤回确定</el-tag>
|
||||||
|
<el-tag v-else type="info" effect="plain">待确定业绩</el-tag>
|
||||||
<span
|
<span
|
||||||
v-if="confirmInfo?.status === 1 && confirmInfo.confirmed_at_text"
|
v-if="settlementConfirmedLocked && confirmInfo?.confirmed_at_text"
|
||||||
class="cs-reconcile__meta"
|
class="cs-reconcile__meta"
|
||||||
>
|
>
|
||||||
{{ confirmInfo.confirmed_admin_name || '—' }} · {{ confirmInfo.confirmed_at_text }}
|
{{ confirmInfo.confirmed_admin_name || '—' }} · {{ confirmInfo.confirmed_at_text }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="confirmInfo?.status === 2 && confirmInfo.revoked_at_text"
|
||||||
|
class="cs-reconcile__meta"
|
||||||
|
>
|
||||||
|
撤回:{{ confirmInfo.revoked_admin_name || '—' }} · {{ confirmInfo.revoked_at_text }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-reconcile__toolbar">
|
<div class="cs-reconcile__toolbar">
|
||||||
<el-button type="primary" plain @click="openReconcileDialog">业绩明细核对</el-button>
|
<el-button type="primary" plain @click="openReconcileDialog">业绩明细核对</el-button>
|
||||||
<el-button :disabled="confirmInfo?.status === 1 || reconcileSaving" @click="handleSaveReconcile">
|
<el-button :disabled="settlementConfirmedLocked || reconcileSaving" @click="handleSaveReconcile">
|
||||||
保存核对备注
|
保存核对备注
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
:disabled="confirmInfo?.status === 1 || confirmSubmitting"
|
:disabled="settlementConfirmedLocked || confirmSubmitting"
|
||||||
@click="handleConfirmFinalize"
|
@click="handleConfirmFinalize"
|
||||||
>
|
>
|
||||||
确定本期业绩
|
确定本期业绩
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="settlementConfirmedLocked"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:loading="confirmRevoking"
|
||||||
|
@click="handleConfirmRevoke"
|
||||||
|
>
|
||||||
|
撤回确定
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-input
|
<el-input
|
||||||
@@ -166,7 +210,7 @@
|
|||||||
maxlength="500"
|
maxlength="500"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="核对备注(可与财务线下对齐后填写;确定业绩前可反复保存)"
|
placeholder="核对备注(可与财务线下对齐后填写;确定业绩前可反复保存)"
|
||||||
:disabled="confirmInfo?.status === 1"
|
:disabled="settlementConfirmedLocked"
|
||||||
class="cs-reconcile__note"
|
class="cs-reconcile__note"
|
||||||
/>
|
/>
|
||||||
<p v-if="Number(totalAgg.carry_in_order_count) > 0" class="cs-reconcile__carry">
|
<p v-if="Number(totalAgg.carry_in_order_count) > 0" class="cs-reconcile__carry">
|
||||||
@@ -245,6 +289,8 @@
|
|||||||
按诊单业绩归属医助 <strong>×</strong> 挂号渠道(doctor_appointment.channels)拆分到行;
|
按诊单业绩归属医助 <strong>×</strong> 挂号渠道(doctor_appointment.channels)拆分到行;
|
||||||
<strong>「复诊2」「复诊3」「自媒体1」等保持各自独立</strong>,未关联挂号统一归到「未匹配挂号渠道」。
|
<strong>「复诊2」「复诊3」「自媒体1」等保持各自独立</strong>,未关联挂号统一归到「未匹配挂号渠道」。
|
||||||
同一医助多行<strong>自动合并「医助 / 归属中心」单元格</strong>,「顺延汇入」为上期结转进入本期订单池的单数。
|
同一医助多行<strong>自动合并「医助 / 归属中心」单元格</strong>,「顺延汇入」为上期结转进入本期订单池的单数。
|
||||||
|
<strong>展示部门</strong>有勾选时,本表仅列出<strong>可归入该部门上下文</strong>的业绩医助(与人事部门落在所选部门范围内的账号一致)。
|
||||||
|
<strong>点击挂号渠道</strong>:查看该行对应的业务订单明细(与上方部门筛选一致)。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
@@ -264,9 +310,10 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag
|
<el-tag
|
||||||
size="small"
|
size="small"
|
||||||
class="cs-channel-tag"
|
class="cs-channel-tag cs-channel-tag--clickable"
|
||||||
:class="channelTagClass(row.appt_channel_name)"
|
:class="channelTagClass(row.appt_channel_name)"
|
||||||
effect="plain"
|
effect="plain"
|
||||||
|
@click.stop="openOrdersForAssistantChannel(row)"
|
||||||
>
|
>
|
||||||
{{ row.appt_channel_name }}
|
{{ row.appt_channel_name }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
@@ -293,6 +340,8 @@
|
|||||||
<span class="cs-table-head__hint">
|
<span class="cs-table-head__hint">
|
||||||
按处方创建账号(开方人) <strong>×</strong> 挂号渠道拆分;
|
按处方创建账号(开方人) <strong>×</strong> 挂号渠道拆分;
|
||||||
可与医助维度交叉核对;同一医生多行自动合并「医生 / 归属中心」单元格。
|
可与医助维度交叉核对;同一医生多行自动合并「医生 / 归属中心」单元格。
|
||||||
|
<strong>展示部门</strong>有勾选时,与医助表相同,仅统计<strong>该部门上下文内</strong>订单对应的开方医生。
|
||||||
|
<strong>点击挂号渠道</strong>:查看该行对应的业务订单明细。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
@@ -312,9 +361,10 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag
|
<el-tag
|
||||||
size="small"
|
size="small"
|
||||||
class="cs-channel-tag"
|
class="cs-channel-tag cs-channel-tag--clickable"
|
||||||
:class="channelTagClass(row.appt_channel_name)"
|
:class="channelTagClass(row.appt_channel_name)"
|
||||||
effect="plain"
|
effect="plain"
|
||||||
|
@click.stop="openOrdersForDoctorChannel(row)"
|
||||||
>
|
>
|
||||||
{{ row.appt_channel_name }}
|
{{ row.appt_channel_name }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
@@ -341,7 +391,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="reconcileDialogVisible"
|
v-model="reconcileDialogVisible"
|
||||||
title="业绩明细核对"
|
title="业绩明细核对"
|
||||||
width="1040px"
|
width="1280px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
class="cs-dialog"
|
class="cs-dialog"
|
||||||
@open="onReconcileDialogOpen"
|
@open="onReconcileDialogOpen"
|
||||||
@@ -354,7 +404,26 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-loading="linesLoading" :data="linesRows" border stripe size="small" max-height="440">
|
<el-table v-loading="linesLoading" :data="linesRows" border stripe size="small" max-height="440">
|
||||||
<el-table-column prop="order_no" label="业务订单号" min-width="116" show-overflow-tooltip />
|
<el-table-column prop="order_no" label="业务订单号" min-width="128" show-overflow-tooltip />
|
||||||
|
<el-table-column label="快递单号" min-width="132" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.tracking_number || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂号日期" min-width="124" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.appointment_date_text || '—' }}</span>
|
||||||
|
<el-tag
|
||||||
|
v-if="row.appointment_status && Number(row.appointment_status) !== 3"
|
||||||
|
class="cs-tag-appt-muted"
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
>未完结挂号</el-tag
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂号渠道" min-width="124" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.appt_channel_name || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
|
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
|
||||||
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
|
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
|
||||||
<el-table-column prop="primary_dept_name" label="归属部门" min-width="100" show-overflow-tooltip />
|
<el-table-column prop="primary_dept_name" label="归属部门" min-width="100" show-overflow-tooltip />
|
||||||
@@ -373,8 +442,20 @@
|
|||||||
<span v-else class="cs-muted">—</span>
|
<span v-else class="cs-muted">—</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="sign_time_text" label="签收时间" min-width="152" show-overflow-tooltip />
|
<el-table-column label="顺延说明" min-width="200" show-overflow-tooltip>
|
||||||
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip />
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.bucket === 'deferred' && row.bucket_defer_text" class="cs-muted">{{
|
||||||
|
row.bucket_defer_text
|
||||||
|
}}</span>
|
||||||
|
<span v-else class="cs-muted">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="物流签收" min-width="152" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.sign_time_text || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.pay_time_text || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="create_time_text" label="订单创建" width="152" />
|
<el-table-column prop="create_time_text" label="订单创建" width="152" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="cs-dialog__pager">
|
<div class="cs-dialog__pager">
|
||||||
@@ -390,8 +471,8 @@
|
|||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="leafOrdersVisible"
|
v-model="leafOrdersVisible"
|
||||||
:title="leafOrdersDept ? `已完成业绩订单 · ${leafOrdersDept.name}` : '已完成业绩订单'"
|
:title="leafOrdersDialogTitle"
|
||||||
width="1040px"
|
width="1280px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
class="cs-dialog"
|
class="cs-dialog"
|
||||||
>
|
>
|
||||||
@@ -403,12 +484,33 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<span class="cs-dialog__hint">
|
<span class="cs-dialog__hint">
|
||||||
结算月:<strong>{{ meta.settlement_month || settlementMonth }}</strong>
|
结算月:<strong>{{ meta.settlement_month || settlementMonth }}</strong>
|
||||||
<template v-if="meta.order_month"> · 订单创建月:<strong>{{ meta.order_month }}</strong></template>
|
<template v-if="orderWindowLabel && orderWindowLabel !== '—'"
|
||||||
|
> · 订单创建:<strong>{{ orderWindowLabel }}</strong></template
|
||||||
|
>
|
||||||
<template v-if="meta.channel_name"> · 渠道:<strong>{{ meta.channel_name }}</strong></template>
|
<template v-if="meta.channel_name"> · 渠道:<strong>{{ meta.channel_name }}</strong></template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-loading="leafOrdersLoading" :data="leafOrdersRows" border stripe size="small" max-height="440">
|
<el-table v-loading="leafOrdersLoading" :data="leafOrdersRows" border stripe size="small" max-height="440">
|
||||||
<el-table-column prop="order_no" label="业务订单号" min-width="116" show-overflow-tooltip />
|
<el-table-column prop="order_no" label="业务订单号" min-width="128" show-overflow-tooltip />
|
||||||
|
<el-table-column label="快递单号" min-width="132" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.tracking_number || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂号日期" min-width="124" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.appointment_date_text || '—' }}</span>
|
||||||
|
<el-tag
|
||||||
|
v-if="row.appointment_status && Number(row.appointment_status) !== 3"
|
||||||
|
class="cs-tag-appt-muted"
|
||||||
|
type="info"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
>未完结挂号</el-tag
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂号渠道" min-width="124" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.appt_channel_name || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
|
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
|
||||||
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
|
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
|
||||||
<el-table-column prop="primary_dept_name" label="归属部门" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="primary_dept_name" label="归属部门" min-width="120" show-overflow-tooltip />
|
||||||
@@ -427,8 +529,20 @@
|
|||||||
<span v-else class="cs-muted">—</span>
|
<span v-else class="cs-muted">—</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="sign_time_text" label="签收时间" min-width="152" show-overflow-tooltip />
|
<el-table-column label="顺延说明" min-width="200" show-overflow-tooltip>
|
||||||
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip />
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.bucket === 'deferred' && row.bucket_defer_text" class="cs-muted">{{
|
||||||
|
row.bucket_defer_text
|
||||||
|
}}</span>
|
||||||
|
<span v-else class="cs-muted">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="物流签收" min-width="152" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.sign_time_text || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.pay_time_text || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="create_time_text" label="订单创建" width="152" />
|
<el-table-column prop="create_time_text" label="订单创建" width="152" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="cs-dialog__pager">
|
<div class="cs-dialog__pager">
|
||||||
@@ -445,12 +559,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ArrowRight, ArrowUp, RefreshRight, Search, Tickets } from '@element-plus/icons-vue'
|
import { ArrowRight, ArrowUp, RefreshRight, Search, Tickets } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
commissionSettlementChannelOptions,
|
commissionSettlementChannelOptions,
|
||||||
commissionSettlementConfirmFinalize,
|
commissionSettlementConfirmFinalize,
|
||||||
|
commissionSettlementConfirmRevoke,
|
||||||
commissionSettlementDeptOptions,
|
commissionSettlementDeptOptions,
|
||||||
commissionSettlementOrderLines,
|
commissionSettlementOrderLines,
|
||||||
commissionSettlementOverview,
|
commissionSettlementOverview,
|
||||||
@@ -565,6 +680,16 @@ interface CsDoctorRow {
|
|||||||
deferred_period_amount: number
|
deferred_period_amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LeafOrderDrillDept = { mode: 'dept'; dept_id: number; dept_name: string }
|
||||||
|
type LeafOrderDrillChannel = {
|
||||||
|
mode: 'channel'
|
||||||
|
title: string
|
||||||
|
assistant_id?: number
|
||||||
|
doctor_id?: number
|
||||||
|
appt_channel_value: number
|
||||||
|
}
|
||||||
|
type LeafOrderDrill = LeafOrderDrillDept | LeafOrderDrillChannel
|
||||||
|
|
||||||
interface CsAssistChannelRow extends CsAssistRow {
|
interface CsAssistChannelRow extends CsAssistRow {
|
||||||
appt_channel_value: number
|
appt_channel_value: number
|
||||||
appt_channel_name: string
|
appt_channel_name: string
|
||||||
@@ -584,7 +709,27 @@ function defaultSettlementMonth(): string {
|
|||||||
return `${y}-${m}`
|
return `${y}-${m}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pad2(n: number): string {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(d: Date): string {
|
||||||
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单池默认:结算月「上一自然月」整段(与未传 start/end 的后端行为一致) */
|
||||||
|
function defaultOrderTimeRangeForSettlementMonth(sm: string): [string, string] {
|
||||||
|
const m = /^(\d{4})-(\d{2})$/.exec(sm)
|
||||||
|
const y = m ? parseInt(m[1], 10) : new Date().getFullYear()
|
||||||
|
const monthNum = m ? parseInt(m[2], 10) : new Date().getMonth() + 1
|
||||||
|
const start = new Date(y, monthNum - 2, 1, 0, 0, 0)
|
||||||
|
const end = new Date(y, monthNum - 1, 0, 23, 59, 59)
|
||||||
|
return [formatDateTime(start), formatDateTime(end)]
|
||||||
|
}
|
||||||
|
|
||||||
const settlementMonth = ref<string>(defaultSettlementMonth())
|
const settlementMonth = ref<string>(defaultSettlementMonth())
|
||||||
|
const orderTimeRange = ref<[string, string] | null>(defaultOrderTimeRangeForSettlementMonth(settlementMonth.value))
|
||||||
|
const requireSystemAutoOnly = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const deptOptions = ref<DeptOption[]>([])
|
const deptOptions = ref<DeptOption[]>([])
|
||||||
const deptOptionsLoading = ref(false)
|
const deptOptionsLoading = ref(false)
|
||||||
@@ -612,9 +757,14 @@ const totalAgg = ref<{
|
|||||||
const meta = reactive({
|
const meta = reactive({
|
||||||
settlement_month: '',
|
settlement_month: '',
|
||||||
order_month: '',
|
order_month: '',
|
||||||
|
order_start_time: '',
|
||||||
|
order_end_time: '',
|
||||||
|
fulfillment_status: 3,
|
||||||
|
explicit_order_time_window: false,
|
||||||
cutoff_end: '',
|
cutoff_end: '',
|
||||||
rule_note: '',
|
rule_note: '',
|
||||||
channel_name: '',
|
channel_name: '',
|
||||||
|
require_system_auto_prescription: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const confirmInfo = ref<{
|
const confirmInfo = ref<{
|
||||||
@@ -622,10 +772,17 @@ const confirmInfo = ref<{
|
|||||||
reconcile_note?: string
|
reconcile_note?: string
|
||||||
confirmed_admin_name?: string
|
confirmed_admin_name?: string
|
||||||
confirmed_at_text?: string
|
confirmed_at_text?: string
|
||||||
|
revoked_admin_name?: string
|
||||||
|
revoked_at_text?: string
|
||||||
|
is_confirmed_locked?: boolean
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const reconcileNote = ref('')
|
const reconcileNote = ref('')
|
||||||
const reconcileSaving = ref(false)
|
const reconcileSaving = ref(false)
|
||||||
const confirmSubmitting = ref(false)
|
const confirmSubmitting = ref(false)
|
||||||
|
const confirmRevoking = ref(false)
|
||||||
|
|
||||||
|
/** 仅 status=1 时锁定,禁止保存备注 / 重复确定;status=2 已撤回可再操作 */
|
||||||
|
const settlementConfirmedLocked = computed(() => Number(confirmInfo.value?.status) === 1)
|
||||||
|
|
||||||
const reconcileDialogVisible = ref(false)
|
const reconcileDialogVisible = ref(false)
|
||||||
const linesLoading = ref(false)
|
const linesLoading = ref(false)
|
||||||
@@ -636,7 +793,7 @@ const linesPage = ref(1)
|
|||||||
const linesPageSize = ref(20)
|
const linesPageSize = ref(20)
|
||||||
|
|
||||||
const leafOrdersVisible = ref(false)
|
const leafOrdersVisible = ref(false)
|
||||||
const leafOrdersDept = ref<{ id: number; name: string } | null>(null)
|
const leafOrderDrill = ref<LeafOrderDrill | null>(null)
|
||||||
const leafOrdersBucket = ref<string>('all')
|
const leafOrdersBucket = ref<string>('all')
|
||||||
const leafOrdersRows = ref<Record<string, any>[]>([])
|
const leafOrdersRows = ref<Record<string, any>[]>([])
|
||||||
const leafOrdersTotal = ref(0)
|
const leafOrdersTotal = ref(0)
|
||||||
@@ -644,6 +801,20 @@ const leafOrdersPage = ref(1)
|
|||||||
const leafOrdersPageSize = ref(20)
|
const leafOrdersPageSize = ref(20)
|
||||||
const leafOrdersLoading = ref(false)
|
const leafOrdersLoading = ref(false)
|
||||||
|
|
||||||
|
const leafOrdersDialogTitle = computed(() => {
|
||||||
|
const d = leafOrderDrill.value
|
||||||
|
if (!d) return '已完成业绩订单'
|
||||||
|
if (d.mode === 'dept') return `已完成业绩订单 · ${d.dept_name}`
|
||||||
|
return d.title
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderWindowLabel = computed(() => {
|
||||||
|
if (meta.order_start_time && meta.order_end_time) {
|
||||||
|
return `${meta.order_start_time}~${meta.order_end_time}`
|
||||||
|
}
|
||||||
|
return meta.order_month || '—'
|
||||||
|
})
|
||||||
|
|
||||||
const deptChildrenMap = computed(() => {
|
const deptChildrenMap = computed(() => {
|
||||||
const map = new Map<number, number[]>()
|
const map = new Map<number, number[]>()
|
||||||
for (const d of deptOptions.value) {
|
for (const d of deptOptions.value) {
|
||||||
@@ -718,7 +889,45 @@ function openOrderLinesForDept(deptId: number, deptName: string) {
|
|||||||
ElMessage.warning('请选择结算月')
|
ElMessage.warning('请选择结算月')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
leafOrdersDept.value = { id: deptId, name: deptName }
|
leafOrderDrill.value = { mode: 'dept', dept_id: deptId, dept_name: deptName }
|
||||||
|
leafOrdersBucket.value = 'all'
|
||||||
|
leafOrdersPage.value = 1
|
||||||
|
leafOrdersRows.value = []
|
||||||
|
leafOrdersTotal.value = 0
|
||||||
|
leafOrdersVisible.value = true
|
||||||
|
loadLeafOrders(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrdersForAssistantChannel(row: CsAssistChannelRow) {
|
||||||
|
if (!settlementMonth.value) {
|
||||||
|
ElMessage.warning('请选择结算月')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
leafOrderDrill.value = {
|
||||||
|
mode: 'channel',
|
||||||
|
title: `业务订单 · ${row.appt_channel_name} · 医助 ${row.assistant_name}`,
|
||||||
|
assistant_id: row.assistant_id,
|
||||||
|
appt_channel_value: Number(row.appt_channel_value ?? 0),
|
||||||
|
}
|
||||||
|
leafOrdersBucket.value = 'all'
|
||||||
|
leafOrdersPage.value = 1
|
||||||
|
leafOrdersRows.value = []
|
||||||
|
leafOrdersTotal.value = 0
|
||||||
|
leafOrdersVisible.value = true
|
||||||
|
loadLeafOrders(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrdersForDoctorChannel(row: CsDoctorChannelRow) {
|
||||||
|
if (!settlementMonth.value) {
|
||||||
|
ElMessage.warning('请选择结算月')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
leafOrderDrill.value = {
|
||||||
|
mode: 'channel',
|
||||||
|
title: `业务订单 · ${row.appt_channel_name} · 医生 ${row.doctor_name}`,
|
||||||
|
doctor_id: row.doctor_id,
|
||||||
|
appt_channel_value: Number(row.appt_channel_value ?? 0),
|
||||||
|
}
|
||||||
leafOrdersBucket.value = 'all'
|
leafOrdersBucket.value = 'all'
|
||||||
leafOrdersPage.value = 1
|
leafOrdersPage.value = 1
|
||||||
leafOrdersRows.value = []
|
leafOrdersRows.value = []
|
||||||
@@ -728,18 +937,30 @@ function openOrderLinesForDept(deptId: number, deptName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadLeafOrders(page: number) {
|
async function loadLeafOrders(page: number) {
|
||||||
if (!settlementMonth.value || !leafOrdersDept.value) return
|
if (!settlementMonth.value || !leafOrderDrill.value) return
|
||||||
leafOrdersLoading.value = true
|
leafOrdersLoading.value = true
|
||||||
leafOrdersPage.value = page
|
leafOrdersPage.value = page
|
||||||
try {
|
try {
|
||||||
|
const d = leafOrderDrill.value
|
||||||
const p: Record<string, any> = {
|
const p: Record<string, any> = {
|
||||||
settlement_month: settlementMonth.value,
|
settlement_month: settlementMonth.value,
|
||||||
dept_ids: String(leafOrdersDept.value.id),
|
|
||||||
page,
|
page,
|
||||||
page_size: leafOrdersPageSize.value,
|
page_size: leafOrdersPageSize.value,
|
||||||
}
|
}
|
||||||
if (selectedChannel.value) {
|
if (d.mode === 'dept') {
|
||||||
p.channel_code = selectedChannel.value
|
p.dept_ids = String(d.dept_id)
|
||||||
|
if (selectedChannel.value) {
|
||||||
|
p.channel_code = selectedChannel.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.assign(p, buildQueryParams())
|
||||||
|
p.appt_channel_value = d.appt_channel_value
|
||||||
|
if (d.assistant_id != null) {
|
||||||
|
p.assistant_id = d.assistant_id
|
||||||
|
}
|
||||||
|
if (d.doctor_id != null) {
|
||||||
|
p.doctor_id = d.doctor_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (leafOrdersBucket.value && leafOrdersBucket.value !== 'all') {
|
if (leafOrdersBucket.value && leafOrdersBucket.value !== 'all') {
|
||||||
p.bucket = leafOrdersBucket.value
|
p.bucket = leafOrdersBucket.value
|
||||||
@@ -796,6 +1017,11 @@ async function loadOverview() {
|
|||||||
ElMessage.warning('请选择结算月')
|
ElMessage.warning('请选择结算月')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const ot = orderTimeRange.value
|
||||||
|
if (!ot || !ot[0] || !ot[1]) {
|
||||||
|
ElMessage.warning('请设置订单创建时间范围')
|
||||||
|
return
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await commissionSettlementOverview(buildQueryParams() as any)
|
const res: any = await commissionSettlementOverview(buildQueryParams() as any)
|
||||||
@@ -807,9 +1033,21 @@ async function loadOverview() {
|
|||||||
totalAgg.value = res?.total || {}
|
totalAgg.value = res?.total || {}
|
||||||
meta.settlement_month = res?.settlement_month || sm
|
meta.settlement_month = res?.settlement_month || sm
|
||||||
meta.order_month = res?.order_month || ''
|
meta.order_month = res?.order_month || ''
|
||||||
|
meta.order_start_time = res?.order_start_time ? String(res.order_start_time) : ''
|
||||||
|
meta.order_end_time = res?.order_end_time ? String(res.order_end_time) : ''
|
||||||
|
meta.fulfillment_status =
|
||||||
|
res?.fulfillment_status != null ? Number(res.fulfillment_status) : 3
|
||||||
|
meta.explicit_order_time_window =
|
||||||
|
typeof res?.explicit_order_time_window === 'boolean'
|
||||||
|
? res.explicit_order_time_window
|
||||||
|
: !!(meta.order_start_time && meta.order_end_time)
|
||||||
meta.cutoff_end = res?.cutoff_end || ''
|
meta.cutoff_end = res?.cutoff_end || ''
|
||||||
meta.rule_note = res?.rule_note || ''
|
meta.rule_note = res?.rule_note || ''
|
||||||
meta.channel_name = res?.channel_name || ''
|
meta.channel_name = res?.channel_name || ''
|
||||||
|
meta.require_system_auto_prescription =
|
||||||
|
typeof res?.require_system_auto_prescription === 'boolean'
|
||||||
|
? res.require_system_auto_prescription
|
||||||
|
: false
|
||||||
confirmInfo.value = res?.confirm ?? null
|
confirmInfo.value = res?.confirm ?? null
|
||||||
reconcileNote.value =
|
reconcileNote.value =
|
||||||
res?.confirm?.reconcile_note != null ? String(res.confirm.reconcile_note) : ''
|
res?.confirm?.reconcile_note != null ? String(res.confirm.reconcile_note) : ''
|
||||||
@@ -829,7 +1067,18 @@ async function loadOverview() {
|
|||||||
|
|
||||||
function buildQueryParams(): Record<string, any> {
|
function buildQueryParams(): Record<string, any> {
|
||||||
const sm = settlementMonth.value
|
const sm = settlementMonth.value
|
||||||
const p: Record<string, any> = { settlement_month: sm }
|
const p: Record<string, any> = {
|
||||||
|
settlement_month: sm,
|
||||||
|
fulfillment_status: 3,
|
||||||
|
}
|
||||||
|
const ot = orderTimeRange.value
|
||||||
|
if (ot && ot[0] && ot[1]) {
|
||||||
|
p.start_time = ot[0]
|
||||||
|
p.end_time = ot[1]
|
||||||
|
}
|
||||||
|
if (requireSystemAutoOnly.value) {
|
||||||
|
p.require_system_auto_prescription = 1
|
||||||
|
}
|
||||||
if (selectedDeptIds.value.length > 0) {
|
if (selectedDeptIds.value.length > 0) {
|
||||||
p.dept_ids = selectedDeptIds.value.join(',')
|
p.dept_ids = selectedDeptIds.value.join(',')
|
||||||
}
|
}
|
||||||
@@ -882,8 +1131,8 @@ async function handleSaveReconcile() {
|
|||||||
ElMessage.warning('请选择结算月')
|
ElMessage.warning('请选择结算月')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (confirmInfo.value?.status === 1) {
|
if (settlementConfirmedLocked.value) {
|
||||||
ElMessage.warning('已确定业绩不可修改备注')
|
ElMessage.warning('已确定业绩不可修改备注;请先撤回确定')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reconcileSaving.value = true
|
reconcileSaving.value = true
|
||||||
@@ -908,7 +1157,7 @@ async function handleConfirmFinalize() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
'确定后将为「顺延下期」订单写入结转记录;下一结算月在相同渠道与部门筛选下自动并入统计。已确定后不可在线撤销,请与财务核对后再操作。',
|
'确定后将为「顺延下期」订单写入结转记录,下一结算月在相同渠道与部门筛选下自动并入统计。确定后仍可「撤回确定」以清除结转并重新核对(与财务对齐后再操作)。',
|
||||||
'确定本期业绩',
|
'确定本期业绩',
|
||||||
{
|
{
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@@ -936,8 +1185,40 @@ async function handleConfirmFinalize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleConfirmRevoke() {
|
||||||
|
if (!settlementMonth.value) {
|
||||||
|
ElMessage.warning('请选择结算月')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'将撤回本次「确定业绩」状态,并删除已写入的顺延结转记录(下期将不再汇入这些单)。确定后继续?',
|
||||||
|
'撤回确定业绩',
|
||||||
|
{
|
||||||
|
confirmButtonText: '撤回',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmRevoking.value = true
|
||||||
|
try {
|
||||||
|
await commissionSettlementConfirmRevoke(buildQueryParams() as any)
|
||||||
|
ElMessage.success('已撤回确定;可重新保存备注并再次确定本期业绩')
|
||||||
|
await loadOverview()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '撤回失败')
|
||||||
|
} finally {
|
||||||
|
confirmRevoking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
settlementMonth.value = defaultSettlementMonth()
|
settlementMonth.value = defaultSettlementMonth()
|
||||||
|
orderTimeRange.value = defaultOrderTimeRangeForSettlementMonth(settlementMonth.value)
|
||||||
|
requireSystemAutoOnly.value = false
|
||||||
selectedDeptIds.value = []
|
selectedDeptIds.value = []
|
||||||
selectedChannel.value = ''
|
selectedChannel.value = ''
|
||||||
loadOverview()
|
loadOverview()
|
||||||
@@ -1053,6 +1334,12 @@ function getSummaries(param: { columns: Array<{ property?: string }>; data: CsRo
|
|||||||
return sums
|
return sums
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(settlementMonth, (v) => {
|
||||||
|
if (v) {
|
||||||
|
orderTimeRange.value = defaultOrderTimeRangeForSettlementMonth(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadDeptOptions(), loadChannelOptions()])
|
await Promise.all([loadDeptOptions(), loadChannelOptions()])
|
||||||
await loadOverview()
|
await loadOverview()
|
||||||
@@ -1249,7 +1536,24 @@ onMounted(async () => {
|
|||||||
flex: 1 1 280px;
|
flex: 1 1 280px;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
|
.filter-field--rangewrap {
|
||||||
|
flex: 1 1 440px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
.filter-month { width: 168px; }
|
.filter-month { width: 168px; }
|
||||||
|
.filter-datetimerange {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
.filter-sa-check {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--cs-text);
|
||||||
|
}
|
||||||
|
.cs-tag-appt-muted {
|
||||||
|
margin-left: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
.filter-select--channel { width: 220px; }
|
.filter-select--channel { width: 220px; }
|
||||||
.filter-dept-tree-select { width: 100%; min-width: 240px; }
|
.filter-dept-tree-select { width: 100%; min-width: 240px; }
|
||||||
.filter-actions {
|
.filter-actions {
|
||||||
@@ -1581,6 +1885,12 @@ onMounted(async () => {
|
|||||||
border-color: #cbd5e1 !important;
|
border-color: #cbd5e1 !important;
|
||||||
color: #64748b !important;
|
color: #64748b !important;
|
||||||
}
|
}
|
||||||
|
&--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
&--neutral {
|
&--neutral {
|
||||||
background: #f0fdfa !important;
|
background: #f0fdfa !important;
|
||||||
border-color: #99f6e4 !important;
|
border-color: #99f6e4 !important;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -32,6 +32,8 @@ use app\adminapi\logic\stats\YejiStatsLogic;
|
|||||||
|
|
||||||
* - POST stats.commissionSettlement/confirmFinalize
|
* - POST stats.commissionSettlement/confirmFinalize
|
||||||
|
|
||||||
|
* - POST stats.commissionSettlement/confirmRevoke
|
||||||
|
|
||||||
* - GET stats.commissionSettlement/deptOptions
|
* - GET stats.commissionSettlement/deptOptions
|
||||||
|
|
||||||
* - GET stats.commissionSettlement/channelOptions
|
* - GET stats.commissionSettlement/channelOptions
|
||||||
@@ -106,6 +108,16 @@ class CommissionSettlementController extends BaseAdminController
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function confirmRevoke()
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
return $this->data(YejiStatsLogic::commissionSettlementConfirmRevoke($this->request->post(), $this->adminId, $this->adminInfo));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function deptOptions()
|
public function deptOptions()
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ declare(strict_types=1);
|
|||||||
namespace app\adminapi\logic\stats;
|
namespace app\adminapi\logic\stats;
|
||||||
|
|
||||||
use app\adminapi\logic\dept\DeptLogic;
|
use app\adminapi\logic\dept\DeptLogic;
|
||||||
|
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||||||
use app\common\model\auth\AdminDept;
|
use app\common\model\auth\AdminDept;
|
||||||
use app\common\model\tcm\Diagnosis;
|
use app\common\model\tcm\Diagnosis;
|
||||||
use app\common\model\tcm\PrescriptionOrder;
|
use app\common\model\tcm\PrescriptionOrder;
|
||||||
use app\common\service\DataScope\DataScopeService;
|
use app\common\service\DataScope\DataScopeService;
|
||||||
|
use app\common\service\ExpressTrackingService;
|
||||||
use app\common\service\qywx\MediaChannelService;
|
use app\common\service\qywx\MediaChannelService;
|
||||||
use think\db\Query;
|
use think\db\Query;
|
||||||
|
use think\facade\Config;
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,21 +186,95 @@ class YejiStatsLogic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$c['adminToPrimary'] = $filtered;
|
$c['adminToPrimary'] = $filtered;
|
||||||
$allowedPrimaries = array_values(array_unique(array_values($filtered)));
|
$primariesFiltered = array_values(array_unique(array_filter(array_values($filtered), static fn (int $v): bool => $v > 0)));
|
||||||
if ($allowedPrimaries === []) {
|
if ($primariesFiltered === []) {
|
||||||
|
$c['tableRowDeptIds'] = [];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$tableRowDeptIdsSrc = array_values(array_map('intval', $c['tableRowDeptIds'] ?? []));
|
||||||
|
$deptById = isset($c['deptById']) && \is_array($c['deptById']) ? $c['deptById'] : [];
|
||||||
|
$visibleAdminIds = array_map('intval', array_keys($filtered));
|
||||||
|
|
||||||
|
/** 与折叠业绩到表格行同源:SELF/本部门 等场景下台账锚点为父「中心」、表格仅有子行为时不能用 id 精确交集 */
|
||||||
|
$keepFlip = [];
|
||||||
|
if ($tableRowDeptIdsSrc !== [] && $deptById !== [] && $visibleAdminIds !== []) {
|
||||||
|
$mapped = self::batchMapPerformanceAdminsToTableDept($visibleAdminIds, $tableRowDeptIdsSrc, $deptById);
|
||||||
|
foreach ($mapped as $__tid) {
|
||||||
|
$t = (int) $__tid;
|
||||||
|
if ($t > 0) {
|
||||||
|
$keepFlip[$t] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($keepFlip === [] && $tableRowDeptIdsSrc !== [] && $deptById !== []) {
|
||||||
|
foreach ($primariesFiltered as $p) {
|
||||||
|
foreach ($tableRowDeptIdsSrc as $tid) {
|
||||||
|
if ($tid <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (self::yejiDeptOnPathBetween($tid, $p, $deptById)) {
|
||||||
|
$keepFlip[$tid] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($keepFlip === []) {
|
||||||
$c['tableRowDeptIds'] = [];
|
$c['tableRowDeptIds'] = [];
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$allowFlip = array_flip($allowedPrimaries);
|
|
||||||
$c['tableRowDeptIds'] = array_values(array_filter(
|
$c['tableRowDeptIds'] = array_values(array_filter(
|
||||||
$c['tableRowDeptIds'],
|
$tableRowDeptIdsSrc,
|
||||||
static function (int $did) use ($allowFlip): bool {
|
static function (int $did) use ($keepFlip): bool {
|
||||||
return $did > 0 && isset($allowFlip[$did]);
|
return $did > 0 && isset($keepFlip[$did]);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任一部门是否为另一部门的上级(ancestor 祖先、descendant 沿 pid 上移可遇到 ancestor)。
|
||||||
|
*/
|
||||||
|
private static function yejiDeptIsAncestorOfDept(int $ancestorId, int $descendantId, array $deptById): bool
|
||||||
|
{
|
||||||
|
if ($ancestorId <= 0 || $descendantId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$cur = $descendantId;
|
||||||
|
for ($i = 0; $i < 64; $i++) {
|
||||||
|
if ($cur === $ancestorId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isset($deptById[$cur])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$cur = (int) ($deptById[$cur]['pid'] ?? 0);
|
||||||
|
if ($cur <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格行 dept 与台账 primary 是否应对齐到同一 subtree(任一为另一祖先)。
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $deptById
|
||||||
|
*/
|
||||||
|
private static function yejiDeptOnPathBetween(int $tableRowDeptId, int $ledgerPrimaryDeptId, array $deptById): bool
|
||||||
|
{
|
||||||
|
if ($tableRowDeptId <= 0 || $ledgerPrimaryDeptId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($tableRowDeptId === $ledgerPrimaryDeptId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::yejiDeptIsAncestorOfDept($tableRowDeptId, $ledgerPrimaryDeptId, $deptById)
|
||||||
|
|| self::yejiDeptIsAncestorOfDept($ledgerPrimaryDeptId, $tableRowDeptId, $deptById);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与业绩看板相同的日期、部门树、渠道、标签与数据范围上下文,供医生日统计等复用。
|
* 与业绩看板相同的日期、部门树、渠道、标签与数据范围上下文,供医生日统计等复用。
|
||||||
*
|
*
|
||||||
@@ -5043,15 +5120,22 @@ class YejiStatsLogic
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$r = is_array($row) ? $row : $row->toArray();
|
$r = is_array($row) ? $row : $row->toArray();
|
||||||
|
$st = (int) ($r['status'] ?? 0);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => (int) ($r['status'] ?? 0),
|
'status' => $st,
|
||||||
'reconcile_note' => (string) ($r['reconcile_note'] ?? ''),
|
'reconcile_note' => (string) ($r['reconcile_note'] ?? ''),
|
||||||
'confirmed_admin_id' => (int) ($r['confirmed_admin_id'] ?? 0),
|
'confirmed_admin_id' => (int) ($r['confirmed_admin_id'] ?? 0),
|
||||||
'confirmed_admin_name' => (string) ($r['confirmed_admin_name'] ?? ''),
|
'confirmed_admin_name' => (string) ($r['confirmed_admin_name'] ?? ''),
|
||||||
'confirmed_at' => (int) ($r['confirmed_at'] ?? 0),
|
'confirmed_at' => (int) ($r['confirmed_at'] ?? 0),
|
||||||
'confirmed_at_text' => !empty($r['confirmed_at']) ? date('Y-m-d H:i:s', (int) $r['confirmed_at']) : '',
|
'confirmed_at_text' => !empty($r['confirmed_at']) ? date('Y-m-d H:i:s', (int) $r['confirmed_at']) : '',
|
||||||
'totals_json' => (string) ($r['totals_json'] ?? ''),
|
'totals_json' => (string) ($r['totals_json'] ?? ''),
|
||||||
|
'revoked_at' => (int) ($r['revoked_at'] ?? 0),
|
||||||
|
'revoked_admin_id' => (int) ($r['revoked_admin_id'] ?? 0),
|
||||||
|
'revoked_admin_name' => (string) ($r['revoked_admin_name'] ?? ''),
|
||||||
|
'revoked_at_text' => !empty($r['revoked_at']) ? date('Y-m-d H:i:s', (int) $r['revoked_at']) : '',
|
||||||
|
// 仅 status=1 锁定,禁止重复确定;status=2 已撤回可再核对/确定
|
||||||
|
'is_confirmed_locked' => $st === 1,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5071,11 +5155,54 @@ class YejiStatsLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
$prevMonthAnchor = strtotime('-1 month', $anchor);
|
$prevMonthAnchor = strtotime('-1 month', $anchor);
|
||||||
$orderCreateStartTs = (int) strtotime(date('Y-m-01 00:00:00', $prevMonthAnchor));
|
if ($prevMonthAnchor === false) {
|
||||||
$orderCreateEndTs = (int) strtotime(date('Y-m-t 23:59:59', $prevMonthAnchor));
|
throw new \think\Exception('结算月日期无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stRaw = trim((string) ($params['start_time'] ?? ''));
|
||||||
|
$etRaw = trim((string) ($params['end_time'] ?? ''));
|
||||||
|
$explicitOrderTimeWindow = ($stRaw !== '' && $etRaw !== '');
|
||||||
|
|
||||||
|
if ($explicitOrderTimeWindow) {
|
||||||
|
$t0 = strtotime($stRaw);
|
||||||
|
$t1 = strtotime($etRaw);
|
||||||
|
if ($t0 === false || $t1 === false) {
|
||||||
|
throw new \think\Exception('订单创建时间 start_time / end_time 无效,请与处方订单列表一致传入(如 2026-04-02 00:00:00)');
|
||||||
|
}
|
||||||
|
$orderCreateStartTs = (int) $t0;
|
||||||
|
$orderCreateEndTs = (int) $t1;
|
||||||
|
if ($orderCreateStartTs > $orderCreateEndTs) {
|
||||||
|
throw new \think\Exception('订单开始时间不能晚于结束时间');
|
||||||
|
}
|
||||||
|
$orderMonthLabel = date('Y-m-d H:i:s', $orderCreateStartTs) . ' ~ ' . date('Y-m-d H:i:s', $orderCreateEndTs);
|
||||||
|
} else {
|
||||||
|
$orderCreateStartTs = (int) strtotime(date('Y-m-01 00:00:00', $prevMonthAnchor));
|
||||||
|
$orderCreateEndTs = (int) strtotime(date('Y-m-t 23:59:59', $prevMonthAnchor));
|
||||||
|
$orderMonthLabel = date('Y-m', $prevMonthAnchor);
|
||||||
|
}
|
||||||
|
|
||||||
$cutoffTs = (int) strtotime(date('Y-m-07 23:59:59', $anchor));
|
$cutoffTs = (int) strtotime(date('Y-m-07 23:59:59', $anchor));
|
||||||
|
|
||||||
$orderMonthLabel = date('Y-m', $prevMonthAnchor);
|
$fulfillmentStatus = (int) ($params['fulfillment_status'] ?? 3);
|
||||||
|
if ($fulfillmentStatus < 0) {
|
||||||
|
$fulfillmentStatus = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($explicitOrderTimeWindow) {
|
||||||
|
if (array_key_exists('require_system_auto_prescription', $params)) {
|
||||||
|
$v = $params['require_system_auto_prescription'];
|
||||||
|
$requireSystemAutoEffective = $v === true || $v === 1 || $v === '1';
|
||||||
|
} else {
|
||||||
|
$requireSystemAutoEffective = false;
|
||||||
|
}
|
||||||
|
$applyListSqlVisibility = true;
|
||||||
|
$skipPhpDataScopeAttributionFilter = true;
|
||||||
|
} else {
|
||||||
|
$requireSystemAutoEffective = (bool) Config::get('project.commission_settlement.require_system_auto_prescription', true);
|
||||||
|
$applyListSqlVisibility = false;
|
||||||
|
$skipPhpDataScopeAttributionFilter = false;
|
||||||
|
}
|
||||||
|
|
||||||
$ctxParams = array_merge($params, [
|
$ctxParams = array_merge($params, [
|
||||||
'start_date' => date('Y-m-d', $orderCreateStartTs),
|
'start_date' => date('Y-m-d', $orderCreateStartTs),
|
||||||
'end_date' => date('Y-m-d', $orderCreateEndTs),
|
'end_date' => date('Y-m-d', $orderCreateEndTs),
|
||||||
@@ -5094,12 +5221,19 @@ class YejiStatsLogic
|
|||||||
$rxTable = self::tableWithPrefix('tcm_prescription');
|
$rxTable = self::tableWithPrefix('tcm_prescription');
|
||||||
$att = self::sqlPerformanceAttributionAdminExpr();
|
$att = self::sqlPerformanceAttributionAdminExpr();
|
||||||
|
|
||||||
|
$assistAttributionRestrictSelfId = 0;
|
||||||
|
if ($viewerAdminId > 0 && PrescriptionOrderLogic::statsRestrictedToOwnAssistantPerformanceDomain($viewerAdminInfo)) {
|
||||||
|
$assistAttributionRestrictSelfId = $viewerAdminId;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'monthRaw' => $monthRaw,
|
'monthRaw' => $monthRaw,
|
||||||
'cutoffTs' => $cutoffTs,
|
'cutoffTs' => $cutoffTs,
|
||||||
'orderCreateStartTs' => $orderCreateStartTs,
|
'orderCreateStartTs' => $orderCreateStartTs,
|
||||||
'orderCreateEndTs' => $orderCreateEndTs,
|
'orderCreateEndTs' => $orderCreateEndTs,
|
||||||
'orderMonthLabel' => $orderMonthLabel,
|
'orderMonthLabel' => $orderMonthLabel,
|
||||||
|
'orderStartTimeText' => date('Y-m-d H:i:s', $orderCreateStartTs),
|
||||||
|
'orderEndTimeText' => date('Y-m-d H:i:s', $orderCreateEndTs),
|
||||||
'nextSettlementMonth' => self::commissionSettlementNextMonth($monthRaw),
|
'nextSettlementMonth' => self::commissionSettlementNextMonth($monthRaw),
|
||||||
'deptKey' => $deptKey,
|
'deptKey' => $deptKey,
|
||||||
'scopeChannelCode' => $scopeChannelCode,
|
'scopeChannelCode' => $scopeChannelCode,
|
||||||
@@ -5111,6 +5245,15 @@ class YejiStatsLogic
|
|||||||
'diagTable' => $diagTable,
|
'diagTable' => $diagTable,
|
||||||
'rxTable' => $rxTable,
|
'rxTable' => $rxTable,
|
||||||
'att' => $att,
|
'att' => $att,
|
||||||
|
'assistAttributionRestrictSelfId' => $assistAttributionRestrictSelfId,
|
||||||
|
'fulfillmentStatus' => $fulfillmentStatus,
|
||||||
|
'explicitOrderTimeWindow' => $explicitOrderTimeWindow,
|
||||||
|
'requireSystemAutoEffective' => $requireSystemAutoEffective,
|
||||||
|
'applyListSqlVisibility' => $applyListSqlVisibility,
|
||||||
|
'skipPhpDataScopeAttributionFilter' => $skipPhpDataScopeAttributionFilter,
|
||||||
|
'viewerAdminId' => $viewerAdminId,
|
||||||
|
'viewerAdminInfo' => $viewerAdminInfo,
|
||||||
|
'commissionRequestParams' => $params,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5178,6 +5321,45 @@ class YejiStatsLogic
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单关联挂号摘要展示:优先 status=3(已完成),否则取该诊单维度下最近一条挂号(patient_id=诊单 id)。
|
||||||
|
* doctor_appointment.patient_id 与诊单主键对齐,见 DiagnosisLists。
|
||||||
|
*
|
||||||
|
* @param int[] $diagnosisIds
|
||||||
|
*
|
||||||
|
* @return array<int, array{appointment_id: int, channels: int, appointment_date: string, appointment_status: int}> diagnosis_id => 摘要
|
||||||
|
*/
|
||||||
|
private static function commissionSettlementDiagnosisLatestCompletedAppointment(array $diagnosisIds): array
|
||||||
|
{
|
||||||
|
$ids = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0)));
|
||||||
|
if ($ids === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = Db::name('doctor_appointment')
|
||||||
|
->whereIn('patient_id', $ids)
|
||||||
|
->orderRaw('`patient_id` asc, (case when `status` = 3 then 1 else 0 end) desc, `id` desc')
|
||||||
|
->field(['id', 'patient_id', 'channels', 'appointment_date', 'status'])
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$dg = (int) ($r['patient_id'] ?? 0);
|
||||||
|
if ($dg <= 0 || isset($out[$dg])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[$dg] = [
|
||||||
|
'appointment_id' => (int) ($r['id'] ?? 0),
|
||||||
|
'channels' => (int) ($r['channels'] ?? 0),
|
||||||
|
'appointment_date' => trim((string) ($r['appointment_date'] ?? '')),
|
||||||
|
'appointment_status' => (int) ($r['status'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 挂号字典 channels:value(int) => name(string)。仅取本次涉及到的 value 集合,减少 dict 全量读取。
|
* 挂号字典 channels:value(int) => name(string)。仅取本次涉及到的 value 集合,减少 dict 全量读取。
|
||||||
*
|
*
|
||||||
@@ -5230,16 +5412,91 @@ class YejiStatsLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN,避免 SELECT 中逐行相关子查询导致 overview 超时。
|
* 「本期提成 / 顺延下期」单笔判定(与 overview / orderLines / 确定结转一致)。
|
||||||
|
*
|
||||||
|
* @return array{eligible: bool, defer_code: string, defer_text: string}
|
||||||
|
*/
|
||||||
|
private static function commissionSettlementBucketEligible(int $signTs, int $payTs, int $cutoffTs): array
|
||||||
|
{
|
||||||
|
$deadlineText = date('Y-m-d H:i:s', $cutoffTs);
|
||||||
|
if ($signTs <= 0) {
|
||||||
|
return [
|
||||||
|
'eligible' => false,
|
||||||
|
'defer_code' => 'missing_sign',
|
||||||
|
'defer_text' => '库内无有效物流签收时间(轨迹未入库或运单号与物流主表不匹配)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($payTs <= 0) {
|
||||||
|
return [
|
||||||
|
'eligible' => false,
|
||||||
|
'defer_code' => 'missing_pay',
|
||||||
|
'defer_text' => '无已支付关联单或未能解析尾款支付时间',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($signTs > $cutoffTs) {
|
||||||
|
return [
|
||||||
|
'eligible' => false,
|
||||||
|
'defer_code' => 'sign_after_cutoff',
|
||||||
|
'defer_text' => '物流签收晚于结算截止(' . $deadlineText . ')',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($payTs > $cutoffTs) {
|
||||||
|
return [
|
||||||
|
'eligible' => false,
|
||||||
|
'defer_code' => 'pay_after_cutoff',
|
||||||
|
'defer_text' => '尾款支付(' . date('Y-m-d H:i:s', $payTs) . ')晚于结算截止(' . $deadlineText . '),仅此一项也会导致顺延',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['eligible' => true, 'defer_code' => '', 'defer_text' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN。
|
||||||
|
* 签收时间不仅用 express_tracking.sign_time:历史数据或部分同步路径下 sign_time 仍为 0,
|
||||||
|
* 但 express_trace 已有「签收」语义轨迹(与 logisticsTrace 展示同源),此处用轨迹时间戳回填,避免误算为「无签收」而全部顺延。
|
||||||
*/
|
*/
|
||||||
private static function commissionSettlementAttachExpressPayAggregates(Query $query): void
|
private static function commissionSettlementAttachExpressPayAggregates(Query $query): void
|
||||||
{
|
{
|
||||||
$etTable = self::tableWithPrefix('express_tracking');
|
$etTable = self::tableWithPrefix('express_tracking');
|
||||||
|
$traceTable = self::tableWithPrefix('express_trace');
|
||||||
$linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order');
|
$linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order');
|
||||||
$payTbl = self::tableWithPrefix('order');
|
$payTbl = self::tableWithPrefix('order');
|
||||||
|
|
||||||
$etAggSql = "(SELECT et.order_id, MAX(et.sign_time) AS cs_sign_ts FROM {$etTable} et "
|
// 单条物流记录上的「有效签收时间戳」:sign_time > 0 优先;否则签收类轨迹;再否则主表已签收/已标记签收时用最新轨迹时间兜底
|
||||||
. "WHERE et.order_type = 'prescription' AND et.delete_time IS NULL GROUP BY et.order_id) cs_et_sig";
|
$sigTraceCond = "`tr_sig`.`status_code` IN ('3','301','302','304') OR `tr_sig`.`status` LIKE '%签收%' "
|
||||||
|
. "OR `tr_sig`.`trace_context` LIKE '%签收%' OR `tr_sig`.`trace_context` LIKE '%妥投%' "
|
||||||
|
. "OR `tr_sig`.`trace_context` LIKE '%已送达%' OR `tr_sig`.`trace_context` LIKE '%本人签收%'";
|
||||||
|
$rowSignExpr = 'GREATEST('
|
||||||
|
. 'IF(IFNULL(`et`.`sign_time`, 0) > 0, `et`.`sign_time`, 0), '
|
||||||
|
. 'IFNULL((SELECT MAX(`tr_sig`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_sig` '
|
||||||
|
. 'WHERE `tr_sig`.`tracking_id` = `et`.`id` AND IFNULL(`tr_sig`.`trace_time_stamp`, 0) > 0 AND ('
|
||||||
|
. $sigTraceCond
|
||||||
|
. ')), 0), '
|
||||||
|
. 'IF((`et`.`current_state` = \'3\' OR IFNULL(`et`.`is_signed`, 0) = 1), '
|
||||||
|
. 'IFNULL((SELECT MAX(`tr_any`.`trace_time_stamp`) FROM `' . $traceTable . '` `tr_any` '
|
||||||
|
. 'WHERE `tr_any`.`tracking_id` = `et`.`id` AND IFNULL(`tr_any`.`trace_time_stamp`, 0) > 0), 0), '
|
||||||
|
. '0)'
|
||||||
|
. ')';
|
||||||
|
|
||||||
|
$poTable = self::tableWithPrefix('tcm_prescription_order');
|
||||||
|
// tracking_number:TRIM 避免空格导致匹配失败;collation 统一避免 HY000 1267
|
||||||
|
$trackingJoinOn =
|
||||||
|
'TRIM(`po`.`tracking_number`) COLLATE utf8mb4_unicode_ci = TRIM(`et`.`tracking_number`) COLLATE utf8mb4_unicode_ci';
|
||||||
|
// 与 ExpressTrackingService::getDetailByTrackingNumber 一致:按单号可查即应能统计签收;勿再依赖 et.order_type(历史数据可能空串/未回写)。
|
||||||
|
// 口径一:express_tracking.order_id 指向处方业务单(用业务单表限定,而非 et.order_type 字符串)。
|
||||||
|
// 口径二:order_id 未绑定或漂移时,用处方单运单号与物流主表 TRIM 后等值关联。
|
||||||
|
$etAggSql = '(SELECT `u`.`order_id`, MAX(`u`.`cs_row_sign`) AS `cs_sign_ts` FROM ('
|
||||||
|
. 'SELECT `et`.`order_id` AS `order_id`, ' . $rowSignExpr . ' AS `cs_row_sign` FROM `' . $etTable . '` `et` '
|
||||||
|
. 'INNER JOIN `' . $poTable . '` `po_by_id` ON `po_by_id`.`id` = `et`.`order_id` AND `po_by_id`.`delete_time` IS NULL '
|
||||||
|
. 'WHERE `et`.`delete_time` IS NULL AND IFNULL(`et`.`order_id`, 0) > 0 '
|
||||||
|
. 'UNION ALL '
|
||||||
|
. 'SELECT `po`.`id` AS `order_id`, ' . $rowSignExpr . ' AS `cs_row_sign` FROM `' . $etTable . '` `et` '
|
||||||
|
. 'INNER JOIN `' . $poTable . '` `po` ON ' . $trackingJoinOn
|
||||||
|
. " AND LENGTH(TRIM(IFNULL(`po`.`tracking_number`, ''))) > 0 "
|
||||||
|
. ' AND `po`.`delete_time` IS NULL '
|
||||||
|
. 'WHERE `et`.`delete_time` IS NULL'
|
||||||
|
. ') AS `u` GROUP BY `u`.`order_id`) `cs_et_sig`';
|
||||||
|
|
||||||
$payAggSql = "(SELECT l.prescription_order_id AS cs_pay_oid, "
|
$payAggSql = "(SELECT l.prescription_order_id AS cs_pay_oid, "
|
||||||
. 'MAX(CASE WHEN pay.order_type IN (5, 7) THEN UNIX_TIMESTAMP(pay.payment_time) END) AS cs_pay_tail_ts, '
|
. 'MAX(CASE WHEN pay.order_type IN (5, 7) THEN UNIX_TIMESTAMP(pay.payment_time) END) AS cs_pay_tail_ts, '
|
||||||
@@ -5273,16 +5530,27 @@ class YejiStatsLogic
|
|||||||
$rxTable = $pack['rxTable'];
|
$rxTable = $pack['rxTable'];
|
||||||
$att = $pack['att'];
|
$att = $pack['att'];
|
||||||
|
|
||||||
|
$requireSa = !empty($pack['requireSystemAutoEffective']);
|
||||||
|
$fulfillmentStatus = (int) ($pack['fulfillmentStatus'] ?? 3);
|
||||||
|
|
||||||
|
$rxJoinCond = 'rx.id = o.prescription_id AND rx.delete_time IS NULL';
|
||||||
|
if ($requireSa) {
|
||||||
|
$rxJoinCond .= ' AND IFNULL(rx.is_system_auto, 0) = 1';
|
||||||
|
}
|
||||||
|
|
||||||
$query = Db::name('tcm_prescription_order')
|
$query = Db::name('tcm_prescription_order')
|
||||||
->alias('o')
|
->alias('o')
|
||||||
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
->join("{$diagTable} dg", 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER');
|
||||||
->join("{$rxTable} rx", 'rx.id = o.prescription_id AND rx.delete_time IS NULL AND IFNULL(rx.is_system_auto, 0) = 1', 'INNER');
|
if ($requireSa) {
|
||||||
|
$query->join("{$rxTable} rx", $rxJoinCond, 'INNER');
|
||||||
|
} else {
|
||||||
|
$query->join("{$rxTable} rx", $rxJoinCond, 'LEFT');
|
||||||
|
}
|
||||||
self::commissionSettlementAttachExpressPayAggregates($query);
|
self::commissionSettlementAttachExpressPayAggregates($query);
|
||||||
|
|
||||||
$query->whereNull('o.delete_time')
|
$query->whereNull('o.delete_time')
|
||||||
->where('o.fulfillment_status', 3)
|
->where('o.fulfillment_status', $fulfillmentStatus)
|
||||||
->where('o.diagnosis_id', '>', 0)
|
->where('o.diagnosis_id', '>', 0);
|
||||||
->whereRaw("({$att}) > 0", []);
|
|
||||||
|
|
||||||
$query->where(function ($qq) use ($pack) {
|
$query->where(function ($qq) use ($pack) {
|
||||||
$qq->whereBetween('o.create_time', [$pack['orderCreateStartTs'], $pack['orderCreateEndTs']]);
|
$qq->whereBetween('o.create_time', [$pack['orderCreateStartTs'], $pack['orderCreateEndTs']]);
|
||||||
@@ -5295,6 +5563,24 @@ class YejiStatsLogic
|
|||||||
|
|
||||||
self::applyCommissionSettlementChannelFilter($query, $c);
|
self::applyCommissionSettlementChannelFilter($query, $c);
|
||||||
|
|
||||||
|
$viewerAdminId = (int) ($pack['viewerAdminId'] ?? 0);
|
||||||
|
$viewerAdminInfo = $pack['viewerAdminInfo'] ?? [];
|
||||||
|
if (!empty($pack['applyListSqlVisibility']) && $viewerAdminId > 0 && \is_array($viewerAdminInfo)) {
|
||||||
|
$reqParams = $pack['commissionRequestParams'] ?? [];
|
||||||
|
PrescriptionOrderLogic::applyPrescriptionOrderListAccessForCommissionQuery(
|
||||||
|
$query,
|
||||||
|
'o',
|
||||||
|
$viewerAdminId,
|
||||||
|
$viewerAdminInfo,
|
||||||
|
\is_array($reqParams) ? $reqParams : []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selfAid = (int) ($pack['assistAttributionRestrictSelfId'] ?? 0);
|
||||||
|
if ($selfAid > 0) {
|
||||||
|
$query->whereRaw("({$att}) = ?", [$selfAid]);
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5303,9 +5589,9 @@ class YejiStatsLogic
|
|||||||
*
|
*
|
||||||
* 规则摘要:
|
* 规则摘要:
|
||||||
* - 订单池:结算月上一自然月内创建;关联处方 is_system_auto=1(系统代开);履约已完成 fulfillment_status=3;未软删。
|
* - 订单池:结算月上一自然月内创建;关联处方 is_system_auto=1(系统代开);履约已完成 fulfillment_status=3;未软删。
|
||||||
* - 签收时间:express_tracking.order_type=prescription 下 MAX(sign_time)。
|
* - 签收时间:express_tracking 与处方单关联(order_id 命中业务单 **或** 运单号 TRIM 后一致);sign_time / express_trace 轨迹回填,与 logisticsTrace(getDetailByTrackingNumber) 同源,不依赖 et.order_type 字符串(历史数据可能未写入)。
|
||||||
* - 尾款支付时间:关联支付单 status=2,优先 order_type∈(5,7),否则回退为任意已支付关联单的 MAX(payment_time)。
|
* - 尾款支付时间:关联支付单 status=2,优先 order_type∈(5,7),否则回退为任意已支付关联单的 MAX(payment_time)。
|
||||||
* - 计入本期:签收与尾款时间均已成立且不晚于结算月 7 日 23:59:59。
|
* - 计入本期:签收与尾款时间均已成立,且**两者均**不晚于结算月 7 日 23:59:59(仅签收在截止前但尾款更晚→仍顺延)。
|
||||||
* - 其余已完成单计入「顺延下期」(含签收或支付缺失、或晚于截止)。
|
* - 其余已完成单计入「顺延下期」(含签收或支付缺失、或晚于截止)。
|
||||||
*
|
*
|
||||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, tag_id?: string} $params
|
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, tag_id?: string} $params
|
||||||
@@ -5326,6 +5612,9 @@ class YejiStatsLogic
|
|||||||
|
|
||||||
$confirm = self::commissionSettlementGetConfirmRow($monthRaw, $pack['scopeChannelCode'], $pack['deptKey']);
|
$confirm = self::commissionSettlementGetConfirmRow($monthRaw, $pack['scopeChannelCode'], $pack['deptKey']);
|
||||||
|
|
||||||
|
$requireSysAutoPo = !empty($pack['requireSystemAutoEffective']);
|
||||||
|
$explicitOrderTimeWindow = !empty($pack['explicitOrderTimeWindow']);
|
||||||
|
|
||||||
$emptyTotal = [
|
$emptyTotal = [
|
||||||
'completed_order_count' => 0,
|
'completed_order_count' => 0,
|
||||||
'completed_order_amount' => 0.0,
|
'completed_order_amount' => 0.0,
|
||||||
@@ -5337,22 +5626,30 @@ class YejiStatsLogic
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||||
|
$ruleLegacy = '订单创建月为结算月上一个月的自然月整段;或未传 start/end 时为整月时间段。签收与尾款均在结算月 7 日前完成的计入本期,否则顺延。上期「确定业绩」产生的顺延订单会并入本期。';
|
||||||
|
$ruleAligned = '传 start_time+end_time 时订单池与处方订单列表一致(创建时间与 fulfillment_status);默认含手动与系统处方,可见性与列表同源。结转与签收/尾款截止日期规则同上。';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'settlement_month' => $monthRaw,
|
'settlement_month' => $monthRaw,
|
||||||
'order_month' => $orderMonthLabel,
|
'order_month' => $orderMonthLabel,
|
||||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
'order_start_time' => $pack['orderStartTimeText'],
|
||||||
'rule_note' => '订单创建月为结算月的上一自然月;统计系统代开处方对应的已完成业务订单;签收与尾款均在结算月 7 日 24 点前完成的金额计入本期提成,否则计入顺延下期。上期「确定业绩」写入的顺延订单会在本期并入统计。',
|
'order_end_time' => $pack['orderEndTimeText'],
|
||||||
'channel_code' => $c['channelCode'],
|
'fulfillment_status' => $pack['fulfillmentStatus'],
|
||||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
'explicit_order_time_window' => $explicitOrderTimeWindow,
|
||||||
'scope_dept_ids_key' => $pack['deptKey'],
|
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||||
'carry_in_order_count' => 0,
|
'rule_note' => $explicitOrderTimeWindow ? $ruleAligned : $ruleLegacy,
|
||||||
'confirm' => $confirm,
|
'channel_code' => $c['channelCode'],
|
||||||
'rows' => [],
|
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||||
'assistant_rows' => [],
|
'scope_dept_ids_key' => $pack['deptKey'],
|
||||||
'doctor_rows' => [],
|
'carry_in_order_count' => 0,
|
||||||
'assistant_channel_rows' => [],
|
'confirm' => $confirm,
|
||||||
'doctor_channel_rows' => [],
|
'rows' => [],
|
||||||
'total' => $emptyTotal,
|
'assistant_rows' => [],
|
||||||
|
'doctor_rows' => [],
|
||||||
|
'assistant_channel_rows' => [],
|
||||||
|
'doctor_channel_rows' => [],
|
||||||
|
'total' => $emptyTotal,
|
||||||
|
'require_system_auto_prescription' => $requireSysAutoPo,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5366,6 +5663,9 @@ class YejiStatsLogic
|
|||||||
'o.diagnosis_id',
|
'o.diagnosis_id',
|
||||||
'o.create_time',
|
'o.create_time',
|
||||||
'o.amount',
|
'o.amount',
|
||||||
|
'o.tracking_number',
|
||||||
|
'o.express_company',
|
||||||
|
'o.recipient_phone',
|
||||||
Db::raw("({$att}) AS assistant_id"),
|
Db::raw("({$att}) AS assistant_id"),
|
||||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||||
], $signPayFields))->select()->toArray();
|
], $signPayFields))->select()->toArray();
|
||||||
@@ -5412,17 +5712,49 @@ class YejiStatsLogic
|
|||||||
$doctorChDefCnt = [];
|
$doctorChDefCnt = [];
|
||||||
$doctorChCarry = [];
|
$doctorChCarry = [];
|
||||||
|
|
||||||
|
$assistScopeFlip = null;
|
||||||
|
if (!empty($c['dataScopeRestricted'])
|
||||||
|
&& isset($c['dataScopeVisibleAdminIds'])
|
||||||
|
&& $c['dataScopeVisibleAdminIds'] !== []) {
|
||||||
|
$assistScopeFlip = array_flip(array_map('intval', $c['dataScopeVisibleAdminIds']));
|
||||||
|
}
|
||||||
|
$selfOnlyAttributionAid = (int) ($pack['assistAttributionRestrictSelfId'] ?? 0);
|
||||||
|
if ($selfOnlyAttributionAid > 0) {
|
||||||
|
$assistScopeFlip = [$selfOnlyAttributionAid => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
$skipPhpDs = !empty($pack['skipPhpDataScopeAttributionFilter']);
|
||||||
|
|
||||||
foreach ($rowsRaw as $r) {
|
foreach ($rowsRaw as $r) {
|
||||||
|
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||||
|
if (!$skipPhpDs && $assistScopeFlip !== null && ($aid <= 0 || !isset($assistScopeFlip[$aid]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 展示部门有勾选时:只统计可归入当前部门上下文(adminToPrimary)的医助,与部门汇总口径一致;否则「按医助下发」会混入部门外的业绩医助。
|
||||||
|
if (!empty($c['explicitDeptFilter']) && ($aid <= 0 || !isset($adminToPrimary[$aid]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$oid = (int) ($r['id'] ?? 0);
|
$oid = (int) ($r['id'] ?? 0);
|
||||||
$isCarry = $oid > 0 && isset($carryFlip[$oid]);
|
$isCarry = $oid > 0 && isset($carryFlip[$oid]);
|
||||||
if ($isCarry) {
|
if ($isCarry) {
|
||||||
$carryInCnt++;
|
$carryInCnt++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
|
||||||
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
||||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||||
|
if ($signTs <= 0) {
|
||||||
|
$tn0 = trim((string) ($r['tracking_number'] ?? ''));
|
||||||
|
if ($tn0 !== '') {
|
||||||
|
$signTs = ExpressTrackingService::resolveSignUnixTimeForCommission(
|
||||||
|
$tn0,
|
||||||
|
(string) ($r['express_company'] ?? 'auto'),
|
||||||
|
(string) ($r['recipient_phone'] ?? ''),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
$amt = round((float) ($r['amount'] ?? 0), 2);
|
$amt = round((float) ($r['amount'] ?? 0), 2);
|
||||||
$dgId = (int) ($r['diagnosis_id'] ?? 0);
|
$dgId = (int) ($r['diagnosis_id'] ?? 0);
|
||||||
$apCh = (int) ($diagToApptChannel[$dgId] ?? 0);
|
$apCh = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||||
@@ -5434,7 +5766,7 @@ class YejiStatsLogic
|
|||||||
$totAgg[$aid] = ($totAgg[$aid] ?? 0.0) + $amt;
|
$totAgg[$aid] = ($totAgg[$aid] ?? 0.0) + $amt;
|
||||||
$cntByAsst[$aid] = ($cntByAsst[$aid] ?? 0) + 1;
|
$cntByAsst[$aid] = ($cntByAsst[$aid] ?? 0) + 1;
|
||||||
|
|
||||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
$eligible = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs)['eligible'];
|
||||||
if ($eligible) {
|
if ($eligible) {
|
||||||
$curAgg[$aid] = ($curAgg[$aid] ?? 0.0) + $amt;
|
$curAgg[$aid] = ($curAgg[$aid] ?? 0.0) + $amt;
|
||||||
$curCntByAsst[$aid] = ($curCntByAsst[$aid] ?? 0) + 1;
|
$curCntByAsst[$aid] = ($curCntByAsst[$aid] ?? 0) + 1;
|
||||||
@@ -5710,22 +6042,42 @@ class YejiStatsLogic
|
|||||||
return ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
return ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($explicitOrderTimeWindow) {
|
||||||
|
$ruleNoteFull = '订单池与「处方订单列表」对齐:create_time 介于 start_time~end_time(与列表 between_time 同源),'
|
||||||
|
. ('fulfillment_status=' . (string) (int) ($pack['fulfillmentStatus'] ?? 3) . '。')
|
||||||
|
. ($requireSysAutoPo
|
||||||
|
? '当前已开启仅统计关联处方 is_system_auto=1(系统代开)的订单;与未限制开方类型的列表条数可能不一致。'
|
||||||
|
: '未限制仅系统代开处方(含手动),与列表在同一时段、同一状态下条数应一致(另受本页展示部门/渠道筛选影响)。')
|
||||||
|
. ' 签收与尾款时间不晚于结算月 7 日 24 点的计入「本期提成」,其余计入「顺延下期」。'
|
||||||
|
. '上期同渠道同部门范围内「确定业绩」产生的顺延订单并入本期。'
|
||||||
|
. '「按医助」「按医生」拆分按 (人 × 挂号渠道):挂号渠道取最新已完成挂号 channels;未关联展示为「未匹配挂号渠道」。';
|
||||||
|
} else {
|
||||||
|
$ruleNoteFull = ($requireSysAutoPo
|
||||||
|
? '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成(默认 fulfillment_status=3)业务订单。 '
|
||||||
|
: '订单创建月为结算月的上一自然月;当前配置同时包含手动与系统代开关联处方(订单池更接近列表;财务请确认口径)。 ')
|
||||||
|
. '签收时间取物流签收最大值;尾款取关联支付单已支付时间(优先尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点计入「本期提成」,其余计入「顺延下期」。结转规则同上。';
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'settlement_month' => $monthRaw,
|
'settlement_month' => $monthRaw,
|
||||||
'order_month' => $orderMonthLabel,
|
'order_month' => $orderMonthLabel,
|
||||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
'order_start_time' => $pack['orderStartTimeText'],
|
||||||
'rule_note' => '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成业务订单。签收时间取物流表签收时间最大值;尾款时间取关联支付单已支付记录(优先类型为尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点的计入「本期提成」,其余已完成订单计入「顺延下期」。上期同渠道同部门范围内「确定业绩」产生的顺延订单,会计入本期订单池。「按医助下发」「按医生下发」按 (人 × 挂号渠道) 拆分;挂号渠道取该订单对应诊单的最新已完成挂号 doctor_appointment.channels(dict_data type_value=channels),如「复诊2」「自媒体1」等保持各自独立一行,未关联挂号统一展示为「未匹配挂号渠道」。',
|
'order_end_time' => $pack['orderEndTimeText'],
|
||||||
'channel_code' => $c['channelCode'],
|
'fulfillment_status' => $pack['fulfillmentStatus'],
|
||||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
'explicit_order_time_window' => $explicitOrderTimeWindow,
|
||||||
'scope_dept_ids_key' => $pack['deptKey'],
|
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||||
'carry_in_order_count' => $carryInCnt,
|
'rule_note' => $ruleNoteFull,
|
||||||
'confirm' => $confirm,
|
'channel_code' => $c['channelCode'],
|
||||||
'rows' => $rows,
|
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||||
'assistant_rows' => $assistantRows,
|
'scope_dept_ids_key' => $pack['deptKey'],
|
||||||
'doctor_rows' => $doctorRows,
|
'carry_in_order_count' => $carryInCnt,
|
||||||
'assistant_channel_rows' => $assistantChannelRows,
|
'confirm' => $confirm,
|
||||||
'doctor_channel_rows' => $doctorChannelRows,
|
'rows' => $rows,
|
||||||
'total' => [
|
'assistant_rows' => $assistantRows,
|
||||||
|
'doctor_rows' => $doctorRows,
|
||||||
|
'assistant_channel_rows' => $assistantChannelRows,
|
||||||
|
'doctor_channel_rows' => $doctorChannelRows,
|
||||||
|
'total' => [
|
||||||
'completed_order_count' => $sumCompletedCnt,
|
'completed_order_count' => $sumCompletedCnt,
|
||||||
'completed_order_amount' => round($sumCompletedAmt, 2),
|
'completed_order_amount' => round($sumCompletedAmt, 2),
|
||||||
'current_period_count' => $sumCurCnt,
|
'current_period_count' => $sumCurCnt,
|
||||||
@@ -5734,13 +6086,14 @@ class YejiStatsLogic
|
|||||||
'deferred_period_amount' => round($sumDefAmt, 2),
|
'deferred_period_amount' => round($sumDefAmt, 2),
|
||||||
'carry_in_order_count' => $carryInCnt,
|
'carry_in_order_count' => $carryInCnt,
|
||||||
],
|
],
|
||||||
|
'require_system_auto_prescription' => $requireSysAutoPo,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提成核对:订单明细(分页)。
|
* 提成核对:订单明细(分页)。
|
||||||
*
|
*
|
||||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, page?: int|string, page_size?: int|string, bucket?: string} $params bucket: 空|current|deferred
|
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, page?: int|string, page_size?: int|string, bucket?: string, assistant_id?: int|string, doctor_id?: int|string, appt_channel_value?: int|string|null} $params bucket: 空|current|deferred;appt_channel_value 与其它筛选同时传递时一并生效(含 0=未匹配挂号渠道)
|
||||||
*
|
*
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -5756,11 +6109,31 @@ class YejiStatsLogic
|
|||||||
$bucket = trim((string) ($params['bucket'] ?? ''));
|
$bucket = trim((string) ($params['bucket'] ?? ''));
|
||||||
$page = max(1, (int) ($params['page'] ?? 1));
|
$page = max(1, (int) ($params['page'] ?? 1));
|
||||||
$pageSize = min(100, max(10, (int) ($params['page_size'] ?? 20)));
|
$pageSize = min(100, max(10, (int) ($params['page_size'] ?? 20)));
|
||||||
|
$filterAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
|
||||||
|
$filterDoctorId = max(0, (int) ($params['doctor_id'] ?? 0));
|
||||||
|
$filterApptChannelActive = array_key_exists('appt_channel_value', $params)
|
||||||
|
&& $params['appt_channel_value'] !== null
|
||||||
|
&& $params['appt_channel_value'] !== '';
|
||||||
|
$filterApptChannelVal = $filterApptChannelActive ? (int) $params['appt_channel_value'] : null;
|
||||||
|
|
||||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||||
return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize];
|
return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cPack = $pack['c'];
|
||||||
|
$linesAssistScopeFlip = null;
|
||||||
|
if (!empty($cPack['dataScopeRestricted'])
|
||||||
|
&& isset($cPack['dataScopeVisibleAdminIds'])
|
||||||
|
&& $cPack['dataScopeVisibleAdminIds'] !== []) {
|
||||||
|
$linesAssistScopeFlip = array_flip(array_map('intval', $cPack['dataScopeVisibleAdminIds']));
|
||||||
|
}
|
||||||
|
$linesSelfAid = (int) ($pack['assistAttributionRestrictSelfId'] ?? 0);
|
||||||
|
if ($linesSelfAid > 0) {
|
||||||
|
$linesAssistScopeFlip = [$linesSelfAid => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
$linesSkipPhpDs = !empty($pack['skipPhpDataScopeAttributionFilter']);
|
||||||
|
|
||||||
$query = self::commissionSettlementBuildFilteredOrderQuery($pack);
|
$query = self::commissionSettlementBuildFilteredOrderQuery($pack);
|
||||||
$att = $pack['att'];
|
$att = $pack['att'];
|
||||||
$signPayFields = self::commissionSettlementSignPayFieldRawList();
|
$signPayFields = self::commissionSettlementSignPayFieldRawList();
|
||||||
@@ -5768,6 +6141,9 @@ class YejiStatsLogic
|
|||||||
$baseRows = $query->field(array_merge([
|
$baseRows = $query->field(array_merge([
|
||||||
'o.id',
|
'o.id',
|
||||||
'o.order_no',
|
'o.order_no',
|
||||||
|
'o.tracking_number',
|
||||||
|
'o.express_company',
|
||||||
|
'o.recipient_phone',
|
||||||
'o.diagnosis_id',
|
'o.diagnosis_id',
|
||||||
'o.create_time',
|
'o.create_time',
|
||||||
'o.amount',
|
'o.amount',
|
||||||
@@ -5775,11 +6151,59 @@ class YejiStatsLogic
|
|||||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||||
], $signPayFields))->order('o.id', 'desc')->select()->toArray();
|
], $signPayFields))->order('o.id', 'desc')->select()->toArray();
|
||||||
|
|
||||||
|
$diagIdSet = [];
|
||||||
|
foreach ($baseRows as $r) {
|
||||||
|
$dg = (int) ($r['diagnosis_id'] ?? 0);
|
||||||
|
if ($dg > 0) {
|
||||||
|
$diagIdSet[$dg] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$diagKeys = array_keys($diagIdSet);
|
||||||
|
$diagToApptChannel = self::commissionSettlementDiagnosisLatestApptChannel($diagKeys);
|
||||||
|
$diagToApptDetail = self::commissionSettlementDiagnosisLatestCompletedAppointment($diagKeys);
|
||||||
|
$chValsForDict = [];
|
||||||
|
foreach ($diagToApptChannel as $v) {
|
||||||
|
$chValsForDict[] = (int) $v;
|
||||||
|
}
|
||||||
|
foreach ($diagToApptDetail as $drow) {
|
||||||
|
$chValsForDict[] = (int) ($drow['channels'] ?? 0);
|
||||||
|
}
|
||||||
|
$apptChannelsDictMap = self::commissionSettlementApptChannelsDictMap($chValsForDict);
|
||||||
|
|
||||||
$mapped = [];
|
$mapped = [];
|
||||||
foreach ($baseRows as $r) {
|
foreach ($baseRows as $r) {
|
||||||
|
$dgId = (int) ($r['diagnosis_id'] ?? 0);
|
||||||
|
$apCh = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||||
|
if ($filterApptChannelActive && $apCh !== (int) $filterApptChannelVal) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$preAid = (int) ($r['assistant_id'] ?? 0);
|
||||||
|
if (!$linesSkipPhpDs && $linesAssistScopeFlip !== null && ($preAid <= 0 || !isset($linesAssistScopeFlip[$preAid]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!empty($cPack['explicitDeptFilter']) && ($preAid <= 0 || !isset($adminToPrimary[$preAid]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$preDoctorId = (int) ($r['doctor_id'] ?? 0);
|
||||||
|
if ($filterAssistantId > 0 && $preAid !== $filterAssistantId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($filterDoctorId > 0 && $preDoctorId !== $filterDoctorId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
$tnForSig = trim((string) ($r['tracking_number'] ?? ''));
|
||||||
|
if ($signTs <= 0 && $tnForSig !== '') {
|
||||||
|
$signTs = ExpressTrackingService::resolveSignUnixTimeForCommission(
|
||||||
|
$tnForSig,
|
||||||
|
(string) ($r['express_company'] ?? 'auto'),
|
||||||
|
(string) ($r['recipient_phone'] ?? ''),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$bucketMeta = self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs);
|
||||||
|
$eligible = $bucketMeta['eligible'];
|
||||||
$b = $eligible ? 'current' : 'deferred';
|
$b = $eligible ? 'current' : 'deferred';
|
||||||
if ($bucket === 'current' && $b !== 'current') {
|
if ($bucket === 'current' && $b !== 'current') {
|
||||||
continue;
|
continue;
|
||||||
@@ -5788,8 +6212,16 @@ class YejiStatsLogic
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$oid = (int) ($r['id'] ?? 0);
|
$oid = (int) ($r['id'] ?? 0);
|
||||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
$aid = $preAid;
|
||||||
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
$doctorId = $preDoctorId;
|
||||||
|
$apDet = $diagToApptDetail[$dgId] ?? null;
|
||||||
|
$chForLabel = \is_array($apDet) ? (int) ($apDet['channels'] ?? 0) : 0;
|
||||||
|
if ($chForLabel <= 0) {
|
||||||
|
$chForLabel = (int) ($diagToApptChannel[$dgId] ?? 0);
|
||||||
|
}
|
||||||
|
$apptDateRaw = \is_array($apDet) ? trim((string) ($apDet['appointment_date'] ?? '')) : '';
|
||||||
|
$apptStatus = \is_array($apDet) ? (int) ($apDet['appointment_status'] ?? 0) : 0;
|
||||||
|
$trackingNo = trim((string) ($r['tracking_number'] ?? ''));
|
||||||
$foldRows = [['assistant_id' => $aid, 'cnt' => 1]];
|
$foldRows = [['assistant_id' => $aid, 'cnt' => 1]];
|
||||||
$byDept = self::foldPerformanceRowsToDeptByTable($foldRows, $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
$byDept = self::foldPerformanceRowsToDeptByTable($foldRows, $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
||||||
$primaryDeptId = 0;
|
$primaryDeptId = 0;
|
||||||
@@ -5801,6 +6233,8 @@ class YejiStatsLogic
|
|||||||
$mapped[] = [
|
$mapped[] = [
|
||||||
'id' => $oid,
|
'id' => $oid,
|
||||||
'order_no' => (string) ($r['order_no'] ?? ''),
|
'order_no' => (string) ($r['order_no'] ?? ''),
|
||||||
|
'tracking_number' => $trackingNo,
|
||||||
|
'express_company' => trim((string) ($r['express_company'] ?? '')),
|
||||||
'diagnosis_id' => (int) ($r['diagnosis_id'] ?? 0),
|
'diagnosis_id' => (int) ($r['diagnosis_id'] ?? 0),
|
||||||
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
||||||
'create_time' => (int) ($r['create_time'] ?? 0),
|
'create_time' => (int) ($r['create_time'] ?? 0),
|
||||||
@@ -5814,9 +6248,17 @@ class YejiStatsLogic
|
|||||||
'assistant_name' => '',
|
'assistant_name' => '',
|
||||||
'doctor_name' => '',
|
'doctor_name' => '',
|
||||||
'bucket' => $b,
|
'bucket' => $b,
|
||||||
|
'bucket_defer_code' => $eligible ? '' : (string) ($bucketMeta['defer_code'] ?? ''),
|
||||||
|
'bucket_defer_text' => $eligible ? '' : (string) ($bucketMeta['defer_text'] ?? ''),
|
||||||
'is_carry_in' => $oid > 0 && isset($carryFlip[$oid]) ? 1 : 0,
|
'is_carry_in' => $oid > 0 && isset($carryFlip[$oid]) ? 1 : 0,
|
||||||
'primary_dept_id' => $primaryDeptId,
|
'primary_dept_id' => $primaryDeptId,
|
||||||
'primary_dept_name' => $primaryDeptId > 0 ? self::formatYejiDeptRowDisplayName($primaryDeptId, $deptById) : '',
|
'primary_dept_name' => $primaryDeptId > 0 ? self::formatYejiDeptRowDisplayName($primaryDeptId, $deptById) : '',
|
||||||
|
'appointment_id' => \is_array($apDet) ? (int) ($apDet['appointment_id'] ?? 0) : 0,
|
||||||
|
'appointment_date' => $apptDateRaw,
|
||||||
|
'appointment_date_text' => $apptDateRaw !== '' ? $apptDateRaw : '',
|
||||||
|
'appointment_status' => $apptStatus,
|
||||||
|
'appt_channel_value' => $chForLabel,
|
||||||
|
'appt_channel_name' => self::commissionSettlementApptChannelLabel($chForLabel, $apptChannelsDictMap),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5894,7 +6336,7 @@ class YejiStatsLogic
|
|||||||
if ($exists) {
|
if ($exists) {
|
||||||
$ex = is_array($exists) ? $exists : $exists->toArray();
|
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||||
if ((int) ($ex['status'] ?? 0) === 1) {
|
if ((int) ($ex['status'] ?? 0) === 1) {
|
||||||
throw new \think\Exception('已确定业绩的记录不可修改核对备注,如需更正请联系管理员处理数据');
|
throw new \think\Exception('当前为「已确定业绩」锁定状态,不可修改备注;请先「撤回确定」后再编辑');
|
||||||
}
|
}
|
||||||
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
||||||
'reconcile_note' => $note,
|
'reconcile_note' => $note,
|
||||||
@@ -5960,6 +6402,9 @@ class YejiStatsLogic
|
|||||||
$rowsRaw = $query->field(array_merge([
|
$rowsRaw = $query->field(array_merge([
|
||||||
'o.id',
|
'o.id',
|
||||||
'o.create_time',
|
'o.create_time',
|
||||||
|
'o.tracking_number',
|
||||||
|
'o.express_company',
|
||||||
|
'o.recipient_phone',
|
||||||
Db::raw("({$att}) AS assistant_id"),
|
Db::raw("({$att}) AS assistant_id"),
|
||||||
], $signPayFields))->select()->toArray();
|
], $signPayFields))->select()->toArray();
|
||||||
|
|
||||||
@@ -5967,8 +6412,18 @@ class YejiStatsLogic
|
|||||||
foreach ($rowsRaw as $r) {
|
foreach ($rowsRaw as $r) {
|
||||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
if ($signTs <= 0) {
|
||||||
if (!$eligible) {
|
$tn0 = trim((string) ($r['tracking_number'] ?? ''));
|
||||||
|
if ($tn0 !== '') {
|
||||||
|
$signTs = ExpressTrackingService::resolveSignUnixTimeForCommission(
|
||||||
|
$tn0,
|
||||||
|
(string) ($r['express_company'] ?? 'auto'),
|
||||||
|
(string) ($r['recipient_phone'] ?? ''),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!self::commissionSettlementBucketEligible($signTs, $payTs, $cutoffTs)['eligible']) {
|
||||||
$deferredIds[] = (int) ($r['id'] ?? 0);
|
$deferredIds[] = (int) ($r['id'] ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6022,6 +6477,9 @@ class YejiStatsLogic
|
|||||||
'confirmed_admin_id' => $viewerAdminId,
|
'confirmed_admin_id' => $viewerAdminId,
|
||||||
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
||||||
'confirmed_at' => $t,
|
'confirmed_at' => $t,
|
||||||
|
'revoked_at' => 0,
|
||||||
|
'revoked_admin_id' => 0,
|
||||||
|
'revoked_admin_name' => '',
|
||||||
'update_time' => $t,
|
'update_time' => $t,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
@@ -6035,6 +6493,9 @@ class YejiStatsLogic
|
|||||||
'confirmed_admin_id' => $viewerAdminId,
|
'confirmed_admin_id' => $viewerAdminId,
|
||||||
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
||||||
'confirmed_at' => $t,
|
'confirmed_at' => $t,
|
||||||
|
'revoked_at' => 0,
|
||||||
|
'revoked_admin_id' => 0,
|
||||||
|
'revoked_admin_name' => '',
|
||||||
'create_time' => $t,
|
'create_time' => $t,
|
||||||
'update_time' => $t,
|
'update_time' => $t,
|
||||||
]);
|
]);
|
||||||
@@ -6052,4 +6513,65 @@ class YejiStatsLogic
|
|||||||
'deferred_carry_count' => count($deferredIds),
|
'deferred_carry_count' => count($deferredIds),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回「确定本期业绩」:status 1→2,并删除本 scope 下因该次确定写入的顺延结转,避免下期重复汇入。
|
||||||
|
* 仅 status=1 时可撤回;撤回后可再次保存备注并重新确定(仍受 uk_scope 唯一行约束)。
|
||||||
|
*
|
||||||
|
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string} $params
|
||||||
|
*
|
||||||
|
* @return array{ok: bool}
|
||||||
|
*/
|
||||||
|
public static function commissionSettlementConfirmRevoke(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||||
|
{
|
||||||
|
if ($viewerAdminId <= 0) {
|
||||||
|
throw new \think\Exception('未登录');
|
||||||
|
}
|
||||||
|
$pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo);
|
||||||
|
$tableRowDeptIds = $pack['tableRowDeptIds'];
|
||||||
|
$adminToPrimary = $pack['adminToPrimary'];
|
||||||
|
|
||||||
|
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||||
|
throw new \think\Exception('当前账号无可操作的展示部门');
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = Db::name('commission_settlement_confirm')
|
||||||
|
->where('settlement_month', $pack['monthRaw'])
|
||||||
|
->where('scope_channel_code', $pack['scopeChannelCode'])
|
||||||
|
->where('scope_dept_ids_key', $pack['deptKey'])
|
||||||
|
->find();
|
||||||
|
if (!$exists) {
|
||||||
|
throw new \think\Exception('当前筛选下无核对记录,无需撤回');
|
||||||
|
}
|
||||||
|
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||||
|
if ((int) ($ex['status'] ?? 0) !== 1) {
|
||||||
|
throw new \think\Exception('仅「已确定业绩」状态可撤回;当前为草稿、或已撤回状态');
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminName = (string) (Db::name('admin')->where('id', $viewerAdminId)->whereNull('delete_time')->value('name') ?: '');
|
||||||
|
$t = time();
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
Db::name('commission_settlement_carry')
|
||||||
|
->where('source_settlement_month', $pack['monthRaw'])
|
||||||
|
->where('scope_channel_code', $pack['scopeChannelCode'])
|
||||||
|
->where('scope_dept_ids_key', $pack['deptKey'])
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
||||||
|
'status' => 2,
|
||||||
|
'revoked_at' => $t,
|
||||||
|
'revoked_admin_id' => $viewerAdminId,
|
||||||
|
'revoked_admin_name' => mb_substr($adminName, 0, 64),
|
||||||
|
'update_time' => $t,
|
||||||
|
]);
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use app\common\model\auth\Admin;
|
|||||||
use app\common\service\DataScope\DataScopeService;
|
use app\common\service\DataScope\DataScopeService;
|
||||||
use app\common\service\ExpressTrackService;
|
use app\common\service\ExpressTrackService;
|
||||||
use app\common\service\gancao\GancaoScmRecipelService;
|
use app\common\service\gancao\GancaoScmRecipelService;
|
||||||
|
use think\db\Query;
|
||||||
use think\facade\Config;
|
use think\facade\Config;
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
use think\facade\Log;
|
use think\facade\Log;
|
||||||
@@ -268,6 +269,166 @@ class PrescriptionOrderLogic
|
|||||||
return self::roleIntersect($adminInfo, $allow);
|
return self::roleIntersect($adminInfo, $allow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与业务订单列表金额统计一致:命中「统计医助」角色且非农单支付审核档时,业务侧按「仅限本人关联诊单的业绩域」收口。
|
||||||
|
* 提成结算等对业绩归因 SQL 收窄时需与之对齐。
|
||||||
|
*/
|
||||||
|
public static function statsRestrictedToOwnAssistantPerformanceDomain(array $adminInfo): bool
|
||||||
|
{
|
||||||
|
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (self::canViewOrderListStatsAllScope($adminInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
|
||||||
|
|
||||||
|
return $assistantRid > 0 && self::roleIntersect($adminInfo, [$assistantRid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提成结算等与列表对齐:按开方医生 / 诊单医助 ∪ 订单创建人 筛选(与 PrescriptionOrderLists::applyDoctorAssistantFilters 在非业绩抽屉场景一致)。
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $params
|
||||||
|
*/
|
||||||
|
public static function applyCommissionOrderDoctorAssistantFilters(Query $query, string $alias, array $params): void
|
||||||
|
{
|
||||||
|
if (isset($params['doctor_id']) && (int) $params['doctor_id'] > 0) {
|
||||||
|
$doctorId = (int) $params['doctor_id'];
|
||||||
|
$rxTbl = (new Prescription())->getTable();
|
||||||
|
$query->whereExists(
|
||||||
|
"SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id` "
|
||||||
|
. "AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$doctorId}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($params['assistant_id']) && (int) $params['assistant_id'] > 0) {
|
||||||
|
$assistantId = (int) $params['assistant_id'];
|
||||||
|
$diagTbl = (new Diagnosis())->getTable();
|
||||||
|
$query->where(function ($q) use ($alias, $diagTbl, $assistantId): void {
|
||||||
|
$q->where("{$alias}.creator_id", $assistantId);
|
||||||
|
$q->whereOrRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||||
|
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与处方订单列表 HasDataScopeFilter + applyCreatorOrOwnPrescriptionVisibility 等价;主业务订单别名 $alias。
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $params 可含 assistant_id(显式收窄时校验数据域)
|
||||||
|
*/
|
||||||
|
public static function applyPrescriptionOrderListAccessForCommissionQuery(
|
||||||
|
Query $query,
|
||||||
|
string $alias,
|
||||||
|
int $viewerAdminId,
|
||||||
|
array $viewerAdminInfo,
|
||||||
|
array $params = []
|
||||||
|
): void {
|
||||||
|
if ($viewerAdminId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::applyCommissionOrderDoctorAssistantFilters($query, $alias, $params);
|
||||||
|
|
||||||
|
if (self::canSeeAllPrescriptionOrders($viewerAdminInfo)) {
|
||||||
|
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rxTbl = (new Prescription())->getTable();
|
||||||
|
$diagTbl = (new Diagnosis())->getTable();
|
||||||
|
|
||||||
|
$explicitAssistant = (int) ($params['assistant_id'] ?? 0);
|
||||||
|
$allowExplicitAssistantVisibility = false;
|
||||||
|
if ($explicitAssistant > 0) {
|
||||||
|
if (!DataScopeService::isEnabled()) {
|
||||||
|
$allowExplicitAssistantVisibility = true;
|
||||||
|
} else {
|
||||||
|
$visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
|
||||||
|
if ($visibleIds === null || in_array($explicitAssistant, $visibleIds, true)) {
|
||||||
|
$allowExplicitAssistantVisibility = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->where(function ($q) use (
|
||||||
|
$alias,
|
||||||
|
$rxTbl,
|
||||||
|
$diagTbl,
|
||||||
|
$viewerAdminId,
|
||||||
|
$explicitAssistant,
|
||||||
|
$allowExplicitAssistantVisibility,
|
||||||
|
$viewerAdminInfo
|
||||||
|
): void {
|
||||||
|
if (self::canViewOrdersForOwnPrescription($viewerAdminInfo)) {
|
||||||
|
$q->where(function ($qq) use ($alias, $rxTbl, $diagTbl, $viewerAdminId): void {
|
||||||
|
$qq->where("{$alias}.creator_id", $viewerAdminId);
|
||||||
|
$qq->whereOrRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id`"
|
||||||
|
. " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$viewerAdminId})"
|
||||||
|
);
|
||||||
|
$qq->whereOrRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||||
|
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$viewerAdminId})"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$q->where(function ($qq) use ($alias, $diagTbl, $viewerAdminId): void {
|
||||||
|
$qq->where("{$alias}.creator_id", $viewerAdminId);
|
||||||
|
$qq->whereOrRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||||
|
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$viewerAdminId})"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($allowExplicitAssistantVisibility) {
|
||||||
|
$q->whereOr("{$alias}.creator_id", $explicitAssistant);
|
||||||
|
$q->whereOrRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
|
||||||
|
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$explicitAssistant})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表数据域:创建人 ∈ 可见 或 诊单医助 ∈ 可见。
|
||||||
|
*/
|
||||||
|
private static function applyCommissionOrderDataScope(
|
||||||
|
Query $query,
|
||||||
|
string $alias,
|
||||||
|
int $viewerAdminId,
|
||||||
|
array $viewerAdminInfo
|
||||||
|
): void {
|
||||||
|
if (!DataScopeService::isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$ids = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
|
||||||
|
if ($ids === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($ids === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$diagTbl = (new Diagnosis())->getTable();
|
||||||
|
$inList = implode(',', array_map('intval', $ids));
|
||||||
|
$query->where(function ($q) use ($alias, $diagTbl, $inList): void {
|
||||||
|
$q->whereIn("{$alias}.creator_id", explode(',', $inList));
|
||||||
|
$q->whereOrRaw(
|
||||||
|
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id` "
|
||||||
|
. "AND dg.`delete_time` IS NULL AND dg.`assistant_id` IN ({$inList}))"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $row
|
* @param array<string,mixed> $row
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -862,4 +862,237 @@ class ExpressTrackingService
|
|||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 PrescriptionOrderLogic::logisticsTrace 一致:优先库表 express_tracking + express_trace;
|
||||||
|
* 库内算不出签收时(或库无记录),可选走快递100(与详情「刷新轨迹」同源)。
|
||||||
|
*
|
||||||
|
* 提成结算 orderLines 等对 SQL JOIN 无法覆盖「仅 API 有轨迹」的运单做补算。
|
||||||
|
*
|
||||||
|
* @param bool $queryKuaidiWhenDbUnusable true 时与 logisticsTrace 完全同路径;false 时仅数据库推导(overview 大批量避免 HTTP)
|
||||||
|
*/
|
||||||
|
public static function resolveSignUnixTimeForCommission(
|
||||||
|
string $trackingNumber,
|
||||||
|
string $expressCompany,
|
||||||
|
string $recipientPhone,
|
||||||
|
bool $queryKuaidiWhenDbUnusable = true
|
||||||
|
): int {
|
||||||
|
$num = trim($trackingNumber);
|
||||||
|
if ($num === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = $num . '|' . ($queryKuaidiWhenDbUnusable ? '1' : '0');
|
||||||
|
static $memo = [];
|
||||||
|
if (isset($memo[$cacheKey])) {
|
||||||
|
return $memo[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
$detail = self::loadTrackingDetailNormalized($num);
|
||||||
|
if (is_array($detail)) {
|
||||||
|
$fromDb = self::effectiveSignUnixFromTrackingDetail($detail);
|
||||||
|
if ($fromDb > 0) {
|
||||||
|
$memo[$cacheKey] = $fromDb;
|
||||||
|
|
||||||
|
return $fromDb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$queryKuaidiWhenDbUnusable) {
|
||||||
|
$memo[$cacheKey] = 0;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ec = strtolower(trim($expressCompany));
|
||||||
|
if ($ec === '') {
|
||||||
|
$ec = 'auto';
|
||||||
|
}
|
||||||
|
$api = ExpressTrackService::query($ec, $num, $recipientPhone, '');
|
||||||
|
$fromApi = self::effectiveSignUnixFromKuaidiPayload($api);
|
||||||
|
$memo[$cacheKey] = $fromApi;
|
||||||
|
|
||||||
|
return $fromApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单号精确匹配失败时尝试 TRIM(TrackingNumber)(历史数据首尾空格)。
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private static function loadTrackingDetailNormalized(string $trackingNumber): ?array
|
||||||
|
{
|
||||||
|
$n = trim($trackingNumber);
|
||||||
|
if ($n === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = self::getDetailByTrackingNumber($n);
|
||||||
|
if ($first !== null) {
|
||||||
|
return $first;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tracking = ExpressTracking::with(['traces' => function ($query) {
|
||||||
|
$query->order('trace_time_stamp', 'desc');
|
||||||
|
}])
|
||||||
|
->whereRaw('TRIM(`tracking_number`) = ?', [$n])
|
||||||
|
->whereNull('delete_time')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$tracking) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $tracking->toArray();
|
||||||
|
$data['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 YejiStatsLogic 物流子查询中每条 express_tracking 的 GREATEST 语义对齐。
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $detail getDetail* / loadTrackingDetailNormalized 结果
|
||||||
|
*/
|
||||||
|
private static function effectiveSignUnixFromTrackingDetail(array $detail): int
|
||||||
|
{
|
||||||
|
$a = (int) ($detail['sign_time'] ?? 0);
|
||||||
|
|
||||||
|
$traces = $detail['traces'] ?? [];
|
||||||
|
if (!is_array($traces)) {
|
||||||
|
$traces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$b = 0;
|
||||||
|
foreach ($traces as $tr) {
|
||||||
|
if (!is_array($tr)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!self::dbTraceRowLooksSigned($tr)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ts = self::traceRowToUnix($tr);
|
||||||
|
if ($ts > 0) {
|
||||||
|
$b = max($b, $ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$c = 0;
|
||||||
|
$stateOk = ((string) ($detail['current_state'] ?? '') === ExpressTracking::STATE_SIGNED
|
||||||
|
|| (int) ($detail['is_signed'] ?? 0) === 1);
|
||||||
|
if ($stateOk) {
|
||||||
|
foreach ($traces as $tr) {
|
||||||
|
if (!is_array($tr)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ts = self::traceRowToUnix($tr);
|
||||||
|
if ($ts > 0) {
|
||||||
|
$c = max($c, $ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max($a, $b, $c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $tr express_trace 行
|
||||||
|
*/
|
||||||
|
private static function dbTraceRowLooksSigned(array $tr): bool
|
||||||
|
{
|
||||||
|
$code = (string) ($tr['status_code'] ?? '');
|
||||||
|
if (in_array($code, ['3', '301', '302', '304'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$st = (string) ($tr['status'] ?? '');
|
||||||
|
$ctx = (string) ($tr['trace_context'] ?? '');
|
||||||
|
$hay = $st . ' ' . $ctx;
|
||||||
|
foreach (['签收', '妥投', '已送达', '本人签收'] as $k) {
|
||||||
|
if (mb_stripos($hay, $k) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $tr
|
||||||
|
*/
|
||||||
|
private static function traceRowToUnix(array $tr): int
|
||||||
|
{
|
||||||
|
$ts = (int) ($tr['trace_time_stamp'] ?? 0);
|
||||||
|
if ($ts > 0) {
|
||||||
|
return $ts;
|
||||||
|
}
|
||||||
|
$raw = trim((string) ($tr['trace_time'] ?? ''));
|
||||||
|
if ($raw === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$p = strtotime($raw);
|
||||||
|
|
||||||
|
return $p !== false ? (int) $p : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $api ExpressTrackService::query 返回值
|
||||||
|
*/
|
||||||
|
private static function effectiveSignUnixFromKuaidiPayload(array $api): int
|
||||||
|
{
|
||||||
|
$traces = $api['traces'] ?? [];
|
||||||
|
if (!is_array($traces) || $traces === []) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = (string) ($api['state'] ?? '');
|
||||||
|
$best = 0;
|
||||||
|
|
||||||
|
foreach ($traces as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tstr = (string) ($row['time'] ?? '');
|
||||||
|
$ctx = (string) ($row['context'] ?? '');
|
||||||
|
$p = strtotime($tstr);
|
||||||
|
$ts = $p !== false ? (int) $p : 0;
|
||||||
|
if ($ts <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$signedLike = self::plainContextLooksSigned($ctx);
|
||||||
|
if ($signedLike) {
|
||||||
|
$best = max($best, $ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($best > 0) {
|
||||||
|
return $best;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state === '3') {
|
||||||
|
foreach ($traces as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tstr = (string) ($row['time'] ?? '');
|
||||||
|
$p = strtotime($tstr);
|
||||||
|
$ts = $p !== false ? (int) $p : 0;
|
||||||
|
if ($ts > 0) {
|
||||||
|
$best = max($best, $ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function plainContextLooksSigned(string $context): bool
|
||||||
|
{
|
||||||
|
foreach (['签收', '妥投', '已送达', '本人签收', '快件已送达'] as $k) {
|
||||||
|
if (mb_stripos($context, $k) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,16 @@ return [
|
|||||||
'exempt_roles' => [],
|
'exempt_roles' => [],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 提成结算 stats.commissionSettlement(YejiStatsLogic)
|
||||||
|
* require_system_auto_prescription:true(默认)仅系统代开口径(is_system_auto=1),常与订单列表「全部已完成」条数不一致。
|
||||||
|
* false 时与列表一致纳入手动开方单(仍需履约完成、业绩归因等其它条件)。
|
||||||
|
* .env 可选:commission_settlement.require_system_auto = 1|0 ,未配置时读本项。
|
||||||
|
*/
|
||||||
|
'commission_settlement' => [
|
||||||
|
'require_system_auto_prescription' => (int) env('commission_settlement.require_system_auto', 1) === 1,
|
||||||
|
],
|
||||||
|
|
||||||
// 腾讯云实时音视频(TRTC)配置
|
// 腾讯云实时音视频(TRTC)配置
|
||||||
'trtc' => [
|
'trtc' => [
|
||||||
'sdkAppId' => (int)env('trtc.sdk_app_id', 1600127710),
|
'sdkAppId' => (int)env('trtc.sdk_app_id', 1600127710),
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- 提成结算:支持「撤回已确定业绩」标记与审计字段
|
||||||
|
-- status:0=已保存核对 1=已确定业绩(锁定,不可重复确定)2=已撤回确定(可再次核对/确定,顺延结转已清除)
|
||||||
|
|
||||||
|
ALTER TABLE `zyt_commission_settlement_confirm`
|
||||||
|
MODIFY COLUMN `status` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0已保存核对 1已确定业绩(锁定) 2已撤回确定',
|
||||||
|
ADD COLUMN `revoked_at` int unsigned NOT NULL DEFAULT 0 COMMENT '撤回确定时间戳' AFTER `confirmed_at`,
|
||||||
|
ADD COLUMN `revoked_admin_id` int unsigned NOT NULL DEFAULT 0 COMMENT '撤回操作人' AFTER `revoked_at`,
|
||||||
|
ADD COLUMN `revoked_admin_name` varchar(64) NOT NULL DEFAULT '' COMMENT '撤回操作人姓名' AFTER `revoked_admin_id`;
|
||||||
Reference in New Issue
Block a user