更新
This commit is contained in:
@@ -103,3 +103,60 @@ export function doctorDailyStatsOverview(params: {
|
||||
}) {
|
||||
return request.get({ url: '/stats.doctorDailyStats/overview', params })
|
||||
}
|
||||
|
||||
/** 提成结算接口计算较重,单独放宽超时(默认多为 60s) */
|
||||
const COMMISSION_SETTLEMENT_TIMEOUT_MS = 120000
|
||||
|
||||
/** 提成结算业绩(独立于业绩看板 yejiStats) */
|
||||
export function commissionSettlementOverview(params: {
|
||||
settlement_month: string
|
||||
dept_ids?: number[] | string
|
||||
channel_code?: string
|
||||
}) {
|
||||
return request.get(
|
||||
{ url: '/stats.commissionSettlement/overview', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
export function commissionSettlementDeptOptions() {
|
||||
return request.get({ url: '/stats.commissionSettlement/deptOptions' })
|
||||
}
|
||||
|
||||
export function commissionSettlementChannelOptions() {
|
||||
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
|
||||
}
|
||||
|
||||
/** 提成核对:订单明细分页 bucket: 空|current|deferred */
|
||||
export function commissionSettlementOrderLines(params: {
|
||||
settlement_month: string
|
||||
dept_ids?: number[] | string
|
||||
channel_code?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
bucket?: string
|
||||
}) {
|
||||
return request.get(
|
||||
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
export function commissionSettlementConfirmStatus(params: {
|
||||
settlement_month: string
|
||||
dept_ids?: number[] | string
|
||||
channel_code?: string
|
||||
}) {
|
||||
return request.get({ url: '/stats.commissionSettlement/confirmStatus', params })
|
||||
}
|
||||
|
||||
export function commissionSettlementSaveReconcile(params: Record<string, any>) {
|
||||
return request.post({ url: '/stats.commissionSettlement/saveReconcile', params })
|
||||
}
|
||||
|
||||
export function commissionSettlementConfirmFinalize(params: Record<string, any>) {
|
||||
return request.post(
|
||||
{ url: '/stats.commissionSettlement/confirmFinalize', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,988 @@
|
||||
<template>
|
||||
<div class="cs-page">
|
||||
<header class="cs-masthead">
|
||||
<div class="cs-masthead__inner">
|
||||
<div>
|
||||
<h1 class="cs-masthead__title">提成结算业绩</h1>
|
||||
<p class="cs-masthead__desc">
|
||||
系统代开处方对应的已完成业务订单;按结算月 7 日截止拆分本期提成与顺延下期;可按部门汇总或下发至业绩归属医助与处方开方医生
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-card class="cs-panel filter-card" shadow="never">
|
||||
<div class="filter-surface">
|
||||
<div class="filter-row filter-row--top">
|
||||
<div class="filter-field">
|
||||
<span class="lbl">结算月</span>
|
||||
<el-date-picker
|
||||
v-model="settlementMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="选择结算月"
|
||||
class="filter-month"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<span class="lbl">渠道来源</span>
|
||||
<el-select
|
||||
v-model="selectedChannel"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="全部"
|
||||
class="filter-select filter-select--channel"
|
||||
:loading="channelOptionsLoading"
|
||||
@change="loadOverview"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option-group
|
||||
v-for="g in channelGroups"
|
||||
:key="g.group_name"
|
||||
:label="g.group_name"
|
||||
>
|
||||
<el-option
|
||||
v-for="ch in g.channels"
|
||||
:key="ch.channel_code"
|
||||
:label="ch.channel_name"
|
||||
:value="ch.channel_code"
|
||||
/>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-field filter-field--wide">
|
||||
<span class="lbl">展示部门</span>
|
||||
<div
|
||||
class="filter-select filter-select--dept filter-dept-tree-wrap"
|
||||
v-loading="deptOptionsLoading"
|
||||
>
|
||||
<el-tree-select
|
||||
v-model="selectedDeptIds"
|
||||
:data="deptTreeOptions"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
filterable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
:default-expand-all="true"
|
||||
placeholder="默认:所有「中心」部门"
|
||||
class="filter-dept-tree-select"
|
||||
node-key="id"
|
||||
:props="deptTreeSelectProps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="filter-btn filter-btn--query"
|
||||
:loading="loading"
|
||||
@click="loadOverview"
|
||||
>
|
||||
<el-icon class="filter-btn__icon"><Search /></el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button class="filter-btn filter-btn--reset" :disabled="loading" @click="handleReset">
|
||||
<el-icon class="filter-btn__icon"><RefreshRight /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-alert type="info" :closable="false" show-icon class="cs-alert">
|
||||
<template #title>
|
||||
<span class="cs-alert__title">统计口径</span>
|
||||
</template>
|
||||
<div class="cs-alert__body">
|
||||
<p v-if="meta.rule_note">{{ meta.rule_note }}</p>
|
||||
<p v-else>
|
||||
订单创建月为<strong>结算月的上一自然月</strong>;仅<strong>系统代开处方</strong>(完成挂号等自动生成)对应的<strong>履约已完成</strong>业务订单。
|
||||
<strong>签收</strong>时间取物流签收时间;<strong>尾款支付</strong>取关联支付单已支付时间(优先尾款/全部费用类型)。
|
||||
签收与尾款均在<strong>结算月 7 日 24:00 前</strong>完成的计入「本期提成」,其余计入「顺延下期」。
|
||||
</p>
|
||||
<p v-if="meta.cutoff_end" class="cs-alert__meta">
|
||||
当前结算月:<strong>{{ meta.settlement_month }}</strong>
|
||||
· 订单创建月:<strong>{{ meta.order_month }}</strong>
|
||||
· 截止时刻:<strong>{{ meta.cutoff_end }}</strong>
|
||||
<template v-if="meta.channel_name">
|
||||
· 渠道:<strong>{{ meta.channel_name }}</strong>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<el-card class="cs-panel cs-reconcile-card" shadow="never">
|
||||
<div class="cs-reconcile">
|
||||
<div class="cs-reconcile__head">
|
||||
<div class="cs-reconcile__tags">
|
||||
<el-tag v-if="confirmInfo?.status === 1" type="success" effect="dark">已确定业绩</el-tag>
|
||||
<el-tag v-else type="warning" effect="plain">待确定业绩</el-tag>
|
||||
<span
|
||||
v-if="confirmInfo?.status === 1 && confirmInfo.confirmed_at_text"
|
||||
class="cs-reconcile__meta"
|
||||
>
|
||||
{{ confirmInfo.confirmed_admin_name || '—' }} · {{ confirmInfo.confirmed_at_text }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cs-reconcile__toolbar">
|
||||
<el-button type="primary" plain @click="openReconcileDialog">业绩明细核对</el-button>
|
||||
<el-button :disabled="confirmInfo?.status === 1 || reconcileSaving" @click="handleSaveReconcile">
|
||||
保存核对备注
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
:disabled="confirmInfo?.status === 1 || confirmSubmitting"
|
||||
@click="handleConfirmFinalize"
|
||||
>
|
||||
确定本期业绩
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="reconcileNote"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="核对备注(可与财务线下对齐后填写;确定业绩前可反复保存)"
|
||||
:disabled="confirmInfo?.status === 1"
|
||||
class="cs-reconcile__note"
|
||||
/>
|
||||
<p v-if="Number(totalAgg.carry_in_order_count) > 0" class="cs-reconcile__carry">
|
||||
上期顺延汇入本期:<strong>{{ totalAgg.carry_in_order_count }}</strong> 单(已并入当前汇总)
|
||||
</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="cs-panel cs-summary-card" shadow="never">
|
||||
<el-tabs v-model="summaryTab" class="cs-summary-tabs">
|
||||
<el-tab-pane label="部门汇总" name="dept">
|
||||
<div class="cs-table-head">
|
||||
<span class="cs-table-head__title">部门汇总</span>
|
||||
<span class="cs-table-head__hint">
|
||||
金额单位:元;与业绩看板相同的部门摊列规则(业绩归属医助 → 展示「中心」)
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
class="cs-table"
|
||||
show-summary
|
||||
:summary-method="getSummaries"
|
||||
>
|
||||
<el-table-column prop="dept_name" label="部门" min-width="120" fixed />
|
||||
<el-table-column prop="completed_order_count" label="已完成单数" min-width="104" align="right" />
|
||||
<el-table-column prop="completed_order_amount" label="已完成金额" min-width="112" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="current_period_count" label="本期提成单数" min-width="118" align="right" />
|
||||
<el-table-column prop="current_period_amount" label="本期提成金额" min-width="118" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.current_period_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deferred_period_count" label="顺延下期单数" min-width="118" align="right" />
|
||||
<el-table-column prop="deferred_period_amount" label="顺延下期金额" min-width="118" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.deferred_period_amount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="按医助下发" name="assistant">
|
||||
<div class="cs-table-head">
|
||||
<span class="cs-table-head__title">业绩归属医助</span>
|
||||
<span class="cs-table-head__hint">
|
||||
按诊单业绩归属规则聚合到具体医助;「顺延汇入」为上期结转进入本期订单池的单数
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="assistantRows"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
class="cs-table"
|
||||
show-summary
|
||||
:summary-method="getPersonSummaries"
|
||||
>
|
||||
<el-table-column prop="assistant_name" label="医助" min-width="120" fixed show-overflow-tooltip />
|
||||
<el-table-column prop="primary_dept_name" label="归属中心" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="carry_in_order_count" label="顺延汇入单数" min-width="112" align="right" />
|
||||
<el-table-column prop="completed_order_count" label="已完成单数" min-width="104" align="right" />
|
||||
<el-table-column prop="completed_order_amount" label="已完成金额" min-width="112" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="current_period_count" label="本期提成单数" min-width="118" align="right" />
|
||||
<el-table-column prop="current_period_amount" label="本期提成金额" min-width="118" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.current_period_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deferred_period_count" label="顺延下期单数" min-width="118" align="right" />
|
||||
<el-table-column prop="deferred_period_amount" label="顺延下期金额" min-width="118" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.deferred_period_amount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="按医生下发" name="doctor">
|
||||
<div class="cs-table-head">
|
||||
<span class="cs-table-head__title">处方开方医生</span>
|
||||
<span class="cs-table-head__hint">
|
||||
按处方创建账号(开方人)聚合;可与医助维度交叉核对
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="doctorRows"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
class="cs-table"
|
||||
show-summary
|
||||
:summary-method="getPersonSummaries"
|
||||
>
|
||||
<el-table-column prop="doctor_name" label="医生" min-width="120" fixed show-overflow-tooltip />
|
||||
<el-table-column prop="primary_dept_name" label="归属中心" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="carry_in_order_count" label="顺延汇入单数" min-width="112" align="right" />
|
||||
<el-table-column prop="completed_order_count" label="已完成单数" min-width="104" align="right" />
|
||||
<el-table-column prop="completed_order_amount" label="已完成金额" min-width="112" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="current_period_count" label="本期提成单数" min-width="118" align="right" />
|
||||
<el-table-column prop="current_period_amount" label="本期提成金额" min-width="118" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.current_period_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deferred_period_count" label="顺延下期单数" min-width="118" align="right" />
|
||||
<el-table-column prop="deferred_period_amount" label="顺延下期金额" min-width="118" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.deferred_period_amount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="reconcileDialogVisible"
|
||||
title="业绩明细核对"
|
||||
width="1040px"
|
||||
destroy-on-close
|
||||
class="cs-dialog"
|
||||
@open="onReconcileDialogOpen"
|
||||
>
|
||||
<div class="cs-dialog__bar">
|
||||
<el-radio-group v-model="linesBucket" size="small" @change="loadOrderLines(1)">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button label="current">本期提成</el-radio-button>
|
||||
<el-radio-button label="deferred">顺延下期</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<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="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="primary_dept_name" label="归属部门" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="amount" label="金额" width="108" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分段" width="88" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.bucket === 'current'" type="success" size="small">本期</el-tag>
|
||||
<el-tag v-else type="warning" size="small">顺延</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结转" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.is_carry_in">是</span>
|
||||
<span v-else class="cs-muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sign_time_text" label="签收时间" min-width="152" show-overflow-tooltip />
|
||||
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip />
|
||||
<el-table-column prop="create_time_text" label="订单创建" width="152" />
|
||||
</el-table>
|
||||
<div class="cs-dialog__pager">
|
||||
<el-pagination
|
||||
layout="total, prev, pager, next"
|
||||
:total="linesTotal"
|
||||
:page-size="linesPageSize"
|
||||
:current-page="linesPage"
|
||||
@current-change="loadOrderLines"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Search, RefreshRight } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
commissionSettlementChannelOptions,
|
||||
commissionSettlementConfirmFinalize,
|
||||
commissionSettlementDeptOptions,
|
||||
commissionSettlementOrderLines,
|
||||
commissionSettlementOverview,
|
||||
commissionSettlementSaveReconcile,
|
||||
} from '@/api/stats'
|
||||
|
||||
interface DeptOption {
|
||||
id: number
|
||||
name: string
|
||||
pid: number
|
||||
path: string
|
||||
}
|
||||
|
||||
interface DeptTreeNode extends DeptOption {
|
||||
children?: DeptTreeNode[]
|
||||
}
|
||||
|
||||
const deptTreeSelectProps = {
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
}
|
||||
|
||||
function buildDeptTreeFromFlat(flat: DeptOption[]): DeptTreeNode[] {
|
||||
if (!flat.length) return []
|
||||
const map = new Map<number, DeptTreeNode>()
|
||||
for (const d of flat) {
|
||||
map.set(d.id, { ...d, children: [] })
|
||||
}
|
||||
const roots: DeptTreeNode[] = []
|
||||
for (const d of flat) {
|
||||
const node = map.get(d.id)!
|
||||
const pid = Number(d.pid)
|
||||
if (pid > 0 && map.has(pid)) {
|
||||
map.get(pid)!.children!.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
}
|
||||
const stripEmptyChildren = (n: DeptTreeNode) => {
|
||||
if (n.children?.length) {
|
||||
n.children.forEach(stripEmptyChildren)
|
||||
} else {
|
||||
delete n.children
|
||||
}
|
||||
}
|
||||
roots.forEach(stripEmptyChildren)
|
||||
const sortLevel = (nodes: DeptTreeNode[]) => {
|
||||
nodes.sort((a, b) => {
|
||||
const ac = a.name.includes('中心') ? -1 : 1
|
||||
const bc = b.name.includes('中心') ? -1 : 1
|
||||
if (ac !== bc) return ac - bc
|
||||
return a.id - b.id
|
||||
})
|
||||
for (const n of nodes) {
|
||||
if (n.children?.length) sortLevel(n.children)
|
||||
}
|
||||
}
|
||||
sortLevel(roots)
|
||||
return roots
|
||||
}
|
||||
|
||||
interface ChannelOption {
|
||||
channel_code: string
|
||||
channel_name: string
|
||||
source_tag_id: string
|
||||
source_group_name: string
|
||||
customer_count: number
|
||||
}
|
||||
|
||||
interface ChannelGroup {
|
||||
group_name: string
|
||||
channels: ChannelOption[]
|
||||
}
|
||||
|
||||
interface CsRow {
|
||||
dept_id: number
|
||||
dept_name: string
|
||||
completed_order_count: number
|
||||
completed_order_amount: number
|
||||
current_period_count: number
|
||||
current_period_amount: number
|
||||
deferred_period_count: number
|
||||
deferred_period_amount: number
|
||||
}
|
||||
|
||||
interface CsAssistRow {
|
||||
assistant_id: number
|
||||
assistant_name: string
|
||||
primary_dept_id: number
|
||||
primary_dept_name: string
|
||||
carry_in_order_count: number
|
||||
completed_order_count: number
|
||||
completed_order_amount: number
|
||||
current_period_count: number
|
||||
current_period_amount: number
|
||||
deferred_period_count: number
|
||||
deferred_period_amount: number
|
||||
}
|
||||
|
||||
interface CsDoctorRow {
|
||||
doctor_id: number
|
||||
doctor_name: string
|
||||
primary_dept_id: number
|
||||
primary_dept_name: string
|
||||
carry_in_order_count: number
|
||||
completed_order_count: number
|
||||
completed_order_amount: number
|
||||
current_period_count: number
|
||||
current_period_amount: number
|
||||
deferred_period_count: number
|
||||
deferred_period_amount: number
|
||||
}
|
||||
|
||||
type CsPersonMetricRow = CsAssistRow | CsDoctorRow
|
||||
|
||||
function defaultSettlementMonth(): string {
|
||||
const d = new Date()
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
return `${y}-${m}`
|
||||
}
|
||||
|
||||
const settlementMonth = ref<string>(defaultSettlementMonth())
|
||||
const loading = ref(false)
|
||||
const deptOptions = ref<DeptOption[]>([])
|
||||
const deptOptionsLoading = ref(false)
|
||||
const deptTreeOptions = computed(() => buildDeptTreeFromFlat(deptOptions.value))
|
||||
const selectedDeptIds = ref<number[]>([])
|
||||
const channelGroups = ref<ChannelGroup[]>([])
|
||||
const channelOptionsLoading = ref(false)
|
||||
const selectedChannel = ref<string>('')
|
||||
|
||||
const rows = ref<CsRow[]>([])
|
||||
const assistantRows = ref<CsAssistRow[]>([])
|
||||
const doctorRows = ref<CsDoctorRow[]>([])
|
||||
const summaryTab = ref<'dept' | 'assistant' | 'doctor'>('dept')
|
||||
const totalAgg = ref<{
|
||||
completed_order_count?: number
|
||||
completed_order_amount?: number
|
||||
current_period_count?: number
|
||||
current_period_amount?: number
|
||||
deferred_period_count?: number
|
||||
deferred_period_amount?: number
|
||||
carry_in_order_count?: number
|
||||
}>({})
|
||||
const meta = reactive({
|
||||
settlement_month: '',
|
||||
order_month: '',
|
||||
cutoff_end: '',
|
||||
rule_note: '',
|
||||
channel_name: '',
|
||||
})
|
||||
|
||||
const confirmInfo = ref<{
|
||||
status?: number
|
||||
reconcile_note?: string
|
||||
confirmed_admin_name?: string
|
||||
confirmed_at_text?: string
|
||||
} | null>(null)
|
||||
const reconcileNote = ref('')
|
||||
const reconcileSaving = ref(false)
|
||||
const confirmSubmitting = ref(false)
|
||||
|
||||
const reconcileDialogVisible = ref(false)
|
||||
const linesLoading = ref(false)
|
||||
const linesBucket = ref<string>('all')
|
||||
const linesRows = ref<Record<string, any>[]>([])
|
||||
const linesTotal = ref(0)
|
||||
const linesPage = ref(1)
|
||||
const linesPageSize = ref(20)
|
||||
|
||||
function formatMoney(v: number | null | undefined) {
|
||||
if (v === null || v === undefined || !Number.isFinite(Number(v))) return '—'
|
||||
return `¥ ${Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
}
|
||||
|
||||
async function loadDeptOptions() {
|
||||
deptOptionsLoading.value = true
|
||||
try {
|
||||
const res: DeptOption[] = await commissionSettlementDeptOptions()
|
||||
deptOptions.value = (res || []).sort((a, b) => {
|
||||
const aIsCenter = a.name.includes('中心') ? -1 : 1
|
||||
const bIsCenter = b.name.includes('中心') ? -1 : 1
|
||||
if (aIsCenter !== bIsCenter) return aIsCenter - bIsCenter
|
||||
return a.id - b.id
|
||||
})
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
} finally {
|
||||
deptOptionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChannelOptions() {
|
||||
channelOptionsLoading.value = true
|
||||
try {
|
||||
const res: any = await commissionSettlementChannelOptions()
|
||||
channelGroups.value = res?.groups || []
|
||||
} catch (_e) {
|
||||
// ignore
|
||||
} finally {
|
||||
channelOptionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
const sm = settlementMonth.value
|
||||
if (!sm) {
|
||||
ElMessage.warning('请选择结算月')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await commissionSettlementOverview(buildQueryParams() as any)
|
||||
rows.value = res?.rows || []
|
||||
assistantRows.value = res?.assistant_rows || []
|
||||
doctorRows.value = res?.doctor_rows || []
|
||||
totalAgg.value = res?.total || {}
|
||||
meta.settlement_month = res?.settlement_month || sm
|
||||
meta.order_month = res?.order_month || ''
|
||||
meta.cutoff_end = res?.cutoff_end || ''
|
||||
meta.rule_note = res?.rule_note || ''
|
||||
meta.channel_name = res?.channel_name || ''
|
||||
confirmInfo.value = res?.confirm ?? null
|
||||
reconcileNote.value =
|
||||
res?.confirm?.reconcile_note != null ? String(res.confirm.reconcile_note) : ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载失败')
|
||||
rows.value = []
|
||||
assistantRows.value = []
|
||||
doctorRows.value = []
|
||||
totalAgg.value = {}
|
||||
confirmInfo.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryParams(): Record<string, any> {
|
||||
const sm = settlementMonth.value
|
||||
const p: Record<string, any> = { settlement_month: sm }
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
p.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
if (selectedChannel.value) {
|
||||
p.channel_code = selectedChannel.value
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
function openReconcileDialog() {
|
||||
if (!settlementMonth.value) {
|
||||
ElMessage.warning('请选择结算月')
|
||||
return
|
||||
}
|
||||
reconcileDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onReconcileDialogOpen() {
|
||||
linesBucket.value = 'all'
|
||||
loadOrderLines(1)
|
||||
}
|
||||
|
||||
async function loadOrderLines(page: number) {
|
||||
if (!settlementMonth.value) return
|
||||
linesLoading.value = true
|
||||
linesPage.value = page
|
||||
try {
|
||||
const p: Record<string, any> = {
|
||||
...buildQueryParams(),
|
||||
page,
|
||||
page_size: linesPageSize.value,
|
||||
}
|
||||
if (linesBucket.value && linesBucket.value !== 'all') {
|
||||
p.bucket = linesBucket.value
|
||||
}
|
||||
const res: any = await commissionSettlementOrderLines(p as any)
|
||||
linesRows.value = res?.list || []
|
||||
linesTotal.value = Number(res?.count ?? 0)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载明细失败')
|
||||
linesRows.value = []
|
||||
linesTotal.value = 0
|
||||
} finally {
|
||||
linesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveReconcile() {
|
||||
if (!settlementMonth.value) {
|
||||
ElMessage.warning('请选择结算月')
|
||||
return
|
||||
}
|
||||
if (confirmInfo.value?.status === 1) {
|
||||
ElMessage.warning('已确定业绩不可修改备注')
|
||||
return
|
||||
}
|
||||
reconcileSaving.value = true
|
||||
try {
|
||||
await commissionSettlementSaveReconcile({
|
||||
...buildQueryParams(),
|
||||
reconcile_note: reconcileNote.value,
|
||||
})
|
||||
ElMessage.success('已保存核对备注')
|
||||
await loadOverview()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '保存失败')
|
||||
} finally {
|
||||
reconcileSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmFinalize() {
|
||||
if (!settlementMonth.value) {
|
||||
ElMessage.warning('请选择结算月')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定后将为「顺延下期」订单写入结转记录;下一结算月在相同渠道与部门筛选下自动并入统计。已确定后不可在线撤销,请与财务核对后再操作。',
|
||||
'确定本期业绩',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
confirmSubmitting.value = true
|
||||
try {
|
||||
const out: any = await commissionSettlementConfirmFinalize({
|
||||
...buildQueryParams(),
|
||||
reconcile_note: reconcileNote.value,
|
||||
})
|
||||
ElMessage.success(
|
||||
`已确定本期业绩;结转 ${Number(out?.deferred_carry_count ?? 0)} 单至 ${String(out?.next_settlement_month ?? '下期')}`
|
||||
)
|
||||
await loadOverview()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '提交失败')
|
||||
} finally {
|
||||
confirmSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
settlementMonth.value = defaultSettlementMonth()
|
||||
selectedDeptIds.value = []
|
||||
selectedChannel.value = ''
|
||||
loadOverview()
|
||||
}
|
||||
|
||||
function getPersonSummaries(param: { columns: Array<{ property?: string }>; data: CsPersonMetricRow[] }) {
|
||||
const { columns } = param
|
||||
const t = totalAgg.value
|
||||
const sums: string[] = []
|
||||
columns.forEach((column, index: number) => {
|
||||
if (index === 0) {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
const prop = column.property as string | undefined
|
||||
if (!t || !prop) {
|
||||
sums[index] = ''
|
||||
return
|
||||
}
|
||||
if (prop === 'primary_dept_name') {
|
||||
sums[index] = ''
|
||||
return
|
||||
}
|
||||
if (prop === 'carry_in_order_count') {
|
||||
sums[index] = String(t.carry_in_order_count ?? 0)
|
||||
return
|
||||
}
|
||||
const map: Record<string, string> = {
|
||||
completed_order_count: String(t.completed_order_count ?? 0),
|
||||
completed_order_amount: formatMoney(t.completed_order_amount),
|
||||
current_period_count: String(t.current_period_count ?? 0),
|
||||
current_period_amount: formatMoney(t.current_period_amount),
|
||||
deferred_period_count: String(t.deferred_period_count ?? 0),
|
||||
deferred_period_amount: formatMoney(t.deferred_period_amount),
|
||||
}
|
||||
sums[index] = map[String(prop)] ?? ''
|
||||
})
|
||||
return sums
|
||||
}
|
||||
|
||||
function getSummaries(param: { columns: Array<{ property?: string }>; data: CsRow[] }) {
|
||||
const { columns } = param
|
||||
const t = totalAgg.value
|
||||
const sums: string[] = []
|
||||
columns.forEach((column, index: number) => {
|
||||
if (index === 0) {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
const prop = column.property as keyof CsRow | undefined
|
||||
if (!t || !prop) {
|
||||
sums[index] = ''
|
||||
return
|
||||
}
|
||||
const map: Record<string, string> = {
|
||||
completed_order_count: String(t.completed_order_count ?? 0),
|
||||
completed_order_amount: formatMoney(t.completed_order_amount),
|
||||
current_period_count: String(t.current_period_count ?? 0),
|
||||
current_period_amount: formatMoney(t.current_period_amount),
|
||||
deferred_period_count: String(t.deferred_period_count ?? 0),
|
||||
deferred_period_amount: formatMoney(t.deferred_period_amount),
|
||||
}
|
||||
sums[index] = map[String(prop)] ?? ''
|
||||
})
|
||||
return sums
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadDeptOptions(), loadChannelOptions()])
|
||||
await loadOverview()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cs-page {
|
||||
min-width: 0;
|
||||
--cs-brand: #6366f1;
|
||||
--cs-brand-soft: #eef2ff;
|
||||
--cs-ink: #1e293b;
|
||||
--cs-muted: #64748b;
|
||||
--cs-line: #e2e8f0;
|
||||
--cs-surface: #ffffff;
|
||||
--cs-page: #f1f5f9;
|
||||
--cs-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
--cs-radius: 16px;
|
||||
--cs-radius-sm: 10px;
|
||||
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
color: var(--cs-ink);
|
||||
background: var(--cs-page);
|
||||
}
|
||||
|
||||
.cs-masthead {
|
||||
margin-bottom: 24px;
|
||||
&__inner {
|
||||
padding: 28px 32px;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
border-radius: var(--cs-radius);
|
||||
box-shadow: var(--cs-shadow-sm);
|
||||
}
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
&__desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-panel {
|
||||
margin-bottom: 20px;
|
||||
border-radius: var(--cs-radius);
|
||||
border: 1px solid var(--cs-line);
|
||||
background: var(--cs-surface);
|
||||
box-shadow: var(--cs-shadow-sm);
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.filter-surface {
|
||||
padding: 20px;
|
||||
background: var(--cs-surface);
|
||||
}
|
||||
.filter-row--top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 16px 20px;
|
||||
}
|
||||
.filter-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
.lbl {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--cs-muted);
|
||||
}
|
||||
}
|
||||
.filter-field--wide {
|
||||
flex: 1 1 260px;
|
||||
min-width: 220px;
|
||||
}
|
||||
.filter-month {
|
||||
width: 168px;
|
||||
}
|
||||
.filter-select--channel {
|
||||
width: 220px;
|
||||
}
|
||||
.filter-dept-tree-select {
|
||||
width: 100%;
|
||||
min-width: 260px;
|
||||
}
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: 8px 20px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--cs-radius-sm);
|
||||
&__icon {
|
||||
margin-right: 4px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
&--query {
|
||||
background: var(--cs-brand);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
&--reset {
|
||||
border: 1px solid var(--cs-line);
|
||||
background: var(--cs-surface);
|
||||
}
|
||||
}
|
||||
|
||||
.cs-alert {
|
||||
margin-bottom: 20px;
|
||||
border-radius: var(--cs-radius-sm);
|
||||
&__title {
|
||||
font-weight: 700;
|
||||
}
|
||||
&__body {
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: var(--cs-muted);
|
||||
p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
&__meta {
|
||||
margin-top: 10px !important;
|
||||
font-size: 12px;
|
||||
color: var(--cs-ink);
|
||||
}
|
||||
}
|
||||
|
||||
.cs-summary-card {
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 12px 20px 0;
|
||||
border-bottom: 1px solid var(--cs-line);
|
||||
}
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 0;
|
||||
}
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-table-head {
|
||||
padding: 14px 20px 10px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--cs-line);
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
&__hint {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--cs-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.cs-table {
|
||||
width: 100%;
|
||||
:deep(.el-table__header th) {
|
||||
background: #f8fafc !important;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
:deep(.el-table__footer-wrapper td) {
|
||||
font-weight: 700;
|
||||
background: #fff7ed !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-reconcile-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-reconcile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
&__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
&__meta {
|
||||
font-size: 12px;
|
||||
color: var(--cs-muted);
|
||||
}
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
&__note {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
}
|
||||
&__carry {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--cs-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-dialog__bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cs-dialog__pager {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cs-muted {
|
||||
color: var(--cs-muted);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
|
||||
namespace app\adminapi\controller\stats;
|
||||
|
||||
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
* 提成结算业绩(独立于业绩看板 yejiStats 接口)
|
||||
|
||||
*
|
||||
|
||||
* - GET stats.commissionSettlement/overview
|
||||
|
||||
* - GET stats.commissionSettlement/orderLines
|
||||
|
||||
* - GET stats.commissionSettlement/confirmStatus
|
||||
|
||||
* - POST stats.commissionSettlement/saveReconcile
|
||||
|
||||
* - POST stats.commissionSettlement/confirmFinalize
|
||||
|
||||
* - GET stats.commissionSettlement/deptOptions
|
||||
|
||||
* - GET stats.commissionSettlement/channelOptions
|
||||
|
||||
*/
|
||||
|
||||
class CommissionSettlementController extends BaseAdminController
|
||||
|
||||
{
|
||||
|
||||
public function overview()
|
||||
|
||||
{
|
||||
|
||||
@set_time_limit(120);
|
||||
|
||||
$params = $this->request->get();
|
||||
|
||||
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementOverview($params, $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function orderLines()
|
||||
|
||||
{
|
||||
|
||||
@set_time_limit(120);
|
||||
|
||||
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementOrderLines($this->request->get(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmStatus()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementConfirmStatus($this->request->get(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function saveReconcile()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementSaveReconcile($this->request->post(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmFinalize()
|
||||
|
||||
{
|
||||
|
||||
@set_time_limit(120);
|
||||
|
||||
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementConfirmFinalize($this->request->post(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function deptOptions()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function channelOptions()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::channelOptions(false));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -916,6 +916,8 @@ class YejiStatsLogic
|
||||
/**
|
||||
* 渠道下拉(按 source_group_name 分组),同时附带客户/成本统计便于排序展示
|
||||
*
|
||||
* @param bool $includeTagCustomerCounts 为 false 时跳过标签客户数统计(qywx_external_contact_tag 大表聚合较慢);提成结算等仅需筛选项时可关闭以提速。
|
||||
*
|
||||
* @return array{
|
||||
* groups: array<int, array{
|
||||
* group_name: string,
|
||||
@@ -928,7 +930,7 @@ class YejiStatsLogic
|
||||
* total: int
|
||||
* }
|
||||
*/
|
||||
public static function channelOptions(): array
|
||||
public static function channelOptions(bool $includeTagCustomerCounts = true): array
|
||||
{
|
||||
$rows = Db::name('qywx_media_channel')
|
||||
->where('status', 1)
|
||||
@@ -939,24 +941,26 @@ class YejiStatsLogic
|
||||
return ['groups' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
// 顺路给 tag_* 渠道带上「打了该标签的客户数」(直接用 contact_tag 表)
|
||||
$tagIds = [];
|
||||
foreach ($rows as $r) {
|
||||
$code = (string) $r['channel_code'];
|
||||
if (str_starts_with($code, 'tag_')) {
|
||||
$tagIds[] = substr($code, 4);
|
||||
}
|
||||
}
|
||||
// 顺路给 tag_* 渠道带上「打了该标签的客户数」(qywx_external_contact_tag 数据量大时聚合较慢,可按需关闭)
|
||||
$tagToCount = [];
|
||||
if ($tagIds !== []) {
|
||||
$cntRows = Db::name('qywx_external_contact_tag')
|
||||
->whereIn('tag_id', $tagIds)
|
||||
->fieldRaw('tag_id, COUNT(DISTINCT external_userid) AS c')
|
||||
->group('tag_id')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($cntRows as $cr) {
|
||||
$tagToCount[(string) $cr['tag_id']] = (int) $cr['c'];
|
||||
if ($includeTagCustomerCounts) {
|
||||
$tagIds = [];
|
||||
foreach ($rows as $r) {
|
||||
$code = (string) $r['channel_code'];
|
||||
if (str_starts_with($code, 'tag_')) {
|
||||
$tagIds[] = substr($code, 4);
|
||||
}
|
||||
}
|
||||
if ($tagIds !== []) {
|
||||
$cntRows = Db::name('qywx_external_contact_tag')
|
||||
->whereIn('tag_id', $tagIds)
|
||||
->fieldRaw('tag_id, COUNT(DISTINCT external_userid) AS c')
|
||||
->group('tag_id')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($cntRows as $cr) {
|
||||
$tagToCount[(string) $cr['tag_id']] = (int) $cr['c'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4920,4 +4924,923 @@ class YejiStatsLogic
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提成结算统计:渠道收窄条件应用到业务订单查询(与 sumPerformance「scoped」同源)。
|
||||
*/
|
||||
private static function applyCommissionSettlementChannelFilter(Query $query, array $c): void
|
||||
{
|
||||
if (empty($c['channelFilterActive'])) {
|
||||
return;
|
||||
}
|
||||
/** @var array<int, mixed> $appointmentChannelValues */
|
||||
$appointmentChannelValues = $c['appointmentChannelValues'] ?? [];
|
||||
$tagDiagIds = $c['tagDiagIds'] ?? null;
|
||||
$tagAssistantIds = $c['tagAssistantIds'] ?? null;
|
||||
$tagFallback = !empty($c['tagFallback']);
|
||||
$channelInfo = $c['channelInfo'] ?? null;
|
||||
|
||||
if ($appointmentChannelValues !== []) {
|
||||
$norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
|
||||
$pack = self::buildChannelScopedAppointmentExistsSqlAndBindings('o.diagnosis_id', $norm, $channelInfo);
|
||||
if ($pack === null) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$query->whereRaw($pack[0], $pack[1]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($tagAssistantIds !== null && $tagAssistantIds === [] && $tagFallback) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$att = self::sqlPerformanceAttributionAdminExpr();
|
||||
$narrowed = false;
|
||||
if ($tagDiagIds !== null) {
|
||||
$idsSan = array_values(array_unique(array_filter(array_map('intval', $tagDiagIds), static fn (int $x): bool => $x > 0)));
|
||||
if ($idsSan === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$narrowed = true;
|
||||
$chunks = array_chunk($idsSan, 800);
|
||||
if (\count($chunks) === 1) {
|
||||
$query->whereIn('o.diagnosis_id', $chunks[0]);
|
||||
} else {
|
||||
$query->where(function ($qq) use ($chunks): void {
|
||||
foreach ($chunks as $i => $chunk) {
|
||||
if ($i === 0) {
|
||||
$qq->whereIn('o.diagnosis_id', $chunk);
|
||||
} else {
|
||||
$qq->whereOr(function ($q2) use ($chunk): void {
|
||||
$q2->whereIn('o.diagnosis_id', $chunk);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($tagAssistantIds !== null) {
|
||||
$ids = array_map('intval', array_keys($tagAssistantIds));
|
||||
if ($ids === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$in = implode(',', $ids);
|
||||
$query->whereRaw("({$att}) IN ({$in})");
|
||||
$narrowed = true;
|
||||
}
|
||||
if (!$narrowed) {
|
||||
$query->whereRaw('0 = 1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提成结算:部门筛选规范化键(与确认/结转 scope 一致)。
|
||||
*
|
||||
* @param int[]|string|null $deptIds
|
||||
*/
|
||||
private static function commissionSettlementDeptIdsKey($deptIds): string
|
||||
{
|
||||
if ($deptIds === null || $deptIds === '' || $deptIds === []) {
|
||||
return '*';
|
||||
}
|
||||
if (is_array($deptIds)) {
|
||||
$ids = array_map('intval', $deptIds);
|
||||
} else {
|
||||
$ids = array_map('intval', explode(',', (string) $deptIds));
|
||||
}
|
||||
$ids = array_values(array_unique(array_filter($ids, static function (int $x): bool {
|
||||
return $x > 0;
|
||||
})));
|
||||
sort($ids);
|
||||
|
||||
return $ids === [] ? '*' : implode(',', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private static function commissionSettlementCarryOrderIdsForTarget(string $targetSettlementMonth, string $scopeChannelCode, string $scopeDeptKey): array
|
||||
{
|
||||
$ids = Db::name('commission_settlement_carry')
|
||||
->where('target_settlement_month', $targetSettlementMonth)
|
||||
->where('scope_channel_code', $scopeChannelCode)
|
||||
->where('scope_dept_ids_key', $scopeDeptKey)
|
||||
->column('prescription_order_id');
|
||||
|
||||
return array_values(array_unique(array_filter(array_map('intval', $ids), static function (int $x): bool {
|
||||
return $x > 0;
|
||||
})));
|
||||
}
|
||||
|
||||
private static function commissionSettlementNextMonth(string $monthRaw): string
|
||||
{
|
||||
$t = strtotime($monthRaw . '-01 12:00:00');
|
||||
|
||||
return date('Y-m', strtotime('+1 month', $t !== false ? $t : time()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private static function commissionSettlementGetConfirmRow(string $settlementMonth, string $scopeChannelCode, string $scopeDeptKey): ?array
|
||||
{
|
||||
$row = Db::name('commission_settlement_confirm')
|
||||
->where('settlement_month', $settlementMonth)
|
||||
->where('scope_channel_code', $scopeChannelCode)
|
||||
->where('scope_dept_ids_key', $scopeDeptKey)
|
||||
->find();
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
$r = is_array($row) ? $row : $row->toArray();
|
||||
|
||||
return [
|
||||
'status' => (int) ($r['status'] ?? 0),
|
||||
'reconcile_note' => (string) ($r['reconcile_note'] ?? ''),
|
||||
'confirmed_admin_id' => (int) ($r['confirmed_admin_id'] ?? 0),
|
||||
'confirmed_admin_name' => (string) ($r['confirmed_admin_name'] ?? ''),
|
||||
'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']) : '',
|
||||
'totals_json' => (string) ($r['totals_json'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function commissionSettlementResolveBasePack(array $params, int $viewerAdminId, array $viewerAdminInfo): array
|
||||
{
|
||||
$monthRaw = trim((string) ($params['settlement_month'] ?? ''));
|
||||
if (!preg_match('/^\d{4}-\d{2}$/', $monthRaw)) {
|
||||
throw new \think\Exception('请传入有效的结算月 settlement_month(格式 YYYY-MM)');
|
||||
}
|
||||
|
||||
$anchor = strtotime($monthRaw . '-01 12:00:00');
|
||||
if ($anchor === false) {
|
||||
throw new \think\Exception('结算月日期无效');
|
||||
}
|
||||
|
||||
$prevMonthAnchor = strtotime('-1 month', $anchor);
|
||||
$orderCreateStartTs = (int) strtotime(date('Y-m-01 00:00:00', $prevMonthAnchor));
|
||||
$orderCreateEndTs = (int) strtotime(date('Y-m-t 23:59:59', $prevMonthAnchor));
|
||||
$cutoffTs = (int) strtotime(date('Y-m-07 23:59:59', $anchor));
|
||||
|
||||
$orderMonthLabel = date('Y-m', $prevMonthAnchor);
|
||||
$ctxParams = array_merge($params, [
|
||||
'start_date' => date('Y-m-d', $orderCreateStartTs),
|
||||
'end_date' => date('Y-m-d', $orderCreateEndTs),
|
||||
]);
|
||||
|
||||
$c = self::resolveYejiContext($ctxParams);
|
||||
if ($viewerAdminId > 0) {
|
||||
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
|
||||
}
|
||||
|
||||
$deptKey = self::commissionSettlementDeptIdsKey($params['dept_ids'] ?? null);
|
||||
$scopeChannelCode = (string) ($c['channelCode'] ?? '');
|
||||
$carryOrderIds = self::commissionSettlementCarryOrderIdsForTarget($monthRaw, $scopeChannelCode, $deptKey);
|
||||
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$rxTable = self::tableWithPrefix('tcm_prescription');
|
||||
$att = self::sqlPerformanceAttributionAdminExpr();
|
||||
|
||||
return [
|
||||
'monthRaw' => $monthRaw,
|
||||
'cutoffTs' => $cutoffTs,
|
||||
'orderCreateStartTs' => $orderCreateStartTs,
|
||||
'orderCreateEndTs' => $orderCreateEndTs,
|
||||
'orderMonthLabel' => $orderMonthLabel,
|
||||
'nextSettlementMonth' => self::commissionSettlementNextMonth($monthRaw),
|
||||
'deptKey' => $deptKey,
|
||||
'scopeChannelCode' => $scopeChannelCode,
|
||||
'carryOrderIds' => $carryOrderIds,
|
||||
'c' => $c,
|
||||
'tableRowDeptIds' => $c['tableRowDeptIds'],
|
||||
'deptById' => $c['deptById'],
|
||||
'adminToPrimary' => $c['adminToPrimary'],
|
||||
'diagTable' => $diagTable,
|
||||
'rxTable' => $rxTable,
|
||||
'att' => $att,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private static function commissionSettlementBatchAdminNames(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Db::name('admin')
|
||||
->whereIn('id', $ids)
|
||||
->whereNull('delete_time')
|
||||
->column('name', 'id');
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $id => $name) {
|
||||
$out[(int) $id] = (string) $name;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 签收 / 尾款时间:对物流与支付关联做 GROUP BY 后 LEFT JOIN,避免 SELECT 中逐行相关子查询导致 overview 超时。
|
||||
*/
|
||||
private static function commissionSettlementAttachExpressPayAggregates(Query $query): void
|
||||
{
|
||||
$etTable = self::tableWithPrefix('express_tracking');
|
||||
$linkTbl = self::tableWithPrefix('tcm_prescription_order_pay_order');
|
||||
$payTbl = self::tableWithPrefix('order');
|
||||
|
||||
$etAggSql = "(SELECT et.order_id, MAX(et.sign_time) AS cs_sign_ts FROM {$etTable} et "
|
||||
. "WHERE et.order_type = 'prescription' AND et.delete_time IS NULL GROUP BY et.order_id) cs_et_sig";
|
||||
|
||||
$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(UNIX_TIMESTAMP(pay.payment_time)) AS cs_pay_any_ts '
|
||||
. "FROM {$linkTbl} l INNER JOIN {$payTbl} pay ON pay.id = l.pay_order_id AND pay.delete_time IS NULL "
|
||||
. 'WHERE pay.status = 2 GROUP BY l.prescription_order_id) cs_pay_agg';
|
||||
|
||||
// 须传入带子查询别名的字符串;ThinkPHP parseJoin 对 Db::raw 整段 JOIN 表会直接传给 parseTable,触发类型错误
|
||||
$query->leftJoin($etAggSql, 'cs_et_sig.order_id = o.id');
|
||||
$query->leftJoin($payAggSql, 'cs_pay_agg.cs_pay_oid = o.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private static function commissionSettlementSignPayFieldRawList(): array
|
||||
{
|
||||
return [
|
||||
Db::raw('IFNULL(cs_et_sig.cs_sign_ts, 0) AS sign_ts'),
|
||||
Db::raw('COALESCE(cs_pay_agg.cs_pay_tail_ts, cs_pay_agg.cs_pay_any_ts, 0) AS pay_ts'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $pack commissionSettlementResolveBasePack 返回值
|
||||
*/
|
||||
private static function commissionSettlementBuildFilteredOrderQuery(array $pack): Query
|
||||
{
|
||||
$c = $pack['c'];
|
||||
$diagTable = $pack['diagTable'];
|
||||
$rxTable = $pack['rxTable'];
|
||||
$att = $pack['att'];
|
||||
|
||||
$query = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->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');
|
||||
self::commissionSettlementAttachExpressPayAggregates($query);
|
||||
|
||||
$query->whereNull('o.delete_time')
|
||||
->where('o.fulfillment_status', 3)
|
||||
->where('o.diagnosis_id', '>', 0)
|
||||
->whereRaw("({$att}) > 0", []);
|
||||
|
||||
$query->where(function ($qq) use ($pack) {
|
||||
$qq->whereBetween('o.create_time', [$pack['orderCreateStartTs'], $pack['orderCreateEndTs']]);
|
||||
if ($pack['carryOrderIds'] !== []) {
|
||||
$qq->whereOr(function ($q2) use ($pack) {
|
||||
$q2->whereIn('o.id', $pack['carryOrderIds']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
self::applyCommissionSettlementChannelFilter($query, $c);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提成结算业绩看板:上月(相对结算月)系统处方对应的已完成业务单,按签收与尾款支付是否在结算月 7 日 24 点前拆分「计入本期提成 / 顺延下期」。
|
||||
*
|
||||
* 规则摘要:
|
||||
* - 订单池:结算月上一自然月内创建;关联处方 is_system_auto=1(系统代开);履约已完成 fulfillment_status=3;未软删。
|
||||
* - 签收时间:express_tracking.order_type=prescription 下 MAX(sign_time)。
|
||||
* - 尾款支付时间:关联支付单 status=2,优先 order_type∈(5,7),否则回退为任意已支付关联单的 MAX(payment_time)。
|
||||
* - 计入本期:签收与尾款时间均已成立且不晚于结算月 7 日 23:59:59。
|
||||
* - 其余已完成单计入「顺延下期」(含签收或支付缺失、或晚于截止)。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, tag_id?: string} $params
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function commissionSettlementOverview(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||
{
|
||||
$pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$monthRaw = $pack['monthRaw'];
|
||||
$cutoffTs = $pack['cutoffTs'];
|
||||
$orderMonthLabel = $pack['orderMonthLabel'];
|
||||
$c = $pack['c'];
|
||||
$tableRowDeptIds = $pack['tableRowDeptIds'];
|
||||
$deptById = $pack['deptById'];
|
||||
$adminToPrimary = $pack['adminToPrimary'];
|
||||
$carryFlip = array_flip($pack['carryOrderIds']);
|
||||
|
||||
$confirm = self::commissionSettlementGetConfirmRow($monthRaw, $pack['scopeChannelCode'], $pack['deptKey']);
|
||||
|
||||
$emptyTotal = [
|
||||
'completed_order_count' => 0,
|
||||
'completed_order_amount' => 0.0,
|
||||
'current_period_count' => 0,
|
||||
'current_period_amount' => 0.0,
|
||||
'deferred_period_count' => 0,
|
||||
'deferred_period_amount' => 0.0,
|
||||
'carry_in_order_count' => 0,
|
||||
];
|
||||
|
||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||
return [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;统计系统代开处方对应的已完成业务订单;签收与尾款均在结算月 7 日 24 点前完成的金额计入本期提成,否则计入顺延下期。上期「确定业绩」写入的顺延订单会在本期并入统计。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => 0,
|
||||
'confirm' => $confirm,
|
||||
'rows' => [],
|
||||
'assistant_rows' => [],
|
||||
'doctor_rows' => [],
|
||||
'total' => $emptyTotal,
|
||||
];
|
||||
}
|
||||
|
||||
$query = self::commissionSettlementBuildFilteredOrderQuery($pack);
|
||||
$att = $pack['att'];
|
||||
$signPayFields = self::commissionSettlementSignPayFieldRawList();
|
||||
|
||||
$rowsRaw = $query->field(array_merge([
|
||||
'o.id',
|
||||
'o.order_no',
|
||||
'o.diagnosis_id',
|
||||
'o.create_time',
|
||||
'o.amount',
|
||||
Db::raw("({$att}) AS assistant_id"),
|
||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||
], $signPayFields))->select()->toArray();
|
||||
|
||||
$curAgg = [];
|
||||
$defAgg = [];
|
||||
$totAgg = [];
|
||||
$cntByAsst = [];
|
||||
$curCntByAsst = [];
|
||||
$defCntByAsst = [];
|
||||
$carryInCnt = 0;
|
||||
$carryInByAssist = [];
|
||||
$carryInByDoctor = [];
|
||||
$totByDoctor = [];
|
||||
$curByDoctor = [];
|
||||
$defByDoctor = [];
|
||||
$cntDoctorTot = [];
|
||||
$cntDoctorCur = [];
|
||||
$cntDoctorDef = [];
|
||||
|
||||
foreach ($rowsRaw as $r) {
|
||||
$oid = (int) ($r['id'] ?? 0);
|
||||
$isCarry = $oid > 0 && isset($carryFlip[$oid]);
|
||||
if ($isCarry) {
|
||||
$carryInCnt++;
|
||||
}
|
||||
|
||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
$amt = round((float) ($r['amount'] ?? 0), 2);
|
||||
|
||||
if ($aid <= 0 || $amt < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totAgg[$aid] = ($totAgg[$aid] ?? 0.0) + $amt;
|
||||
$cntByAsst[$aid] = ($cntByAsst[$aid] ?? 0) + 1;
|
||||
|
||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
||||
if ($eligible) {
|
||||
$curAgg[$aid] = ($curAgg[$aid] ?? 0.0) + $amt;
|
||||
$curCntByAsst[$aid] = ($curCntByAsst[$aid] ?? 0) + 1;
|
||||
} else {
|
||||
$defAgg[$aid] = ($defAgg[$aid] ?? 0.0) + $amt;
|
||||
$defCntByAsst[$aid] = ($defCntByAsst[$aid] ?? 0) + 1;
|
||||
}
|
||||
|
||||
if ($isCarry) {
|
||||
$carryInByAssist[$aid] = ($carryInByAssist[$aid] ?? 0) + 1;
|
||||
$carryInByDoctor[$doctorId] = ($carryInByDoctor[$doctorId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
$totByDoctor[$doctorId] = ($totByDoctor[$doctorId] ?? 0.0) + $amt;
|
||||
$cntDoctorTot[$doctorId] = ($cntDoctorTot[$doctorId] ?? 0) + 1;
|
||||
if ($eligible) {
|
||||
$curByDoctor[$doctorId] = ($curByDoctor[$doctorId] ?? 0.0) + $amt;
|
||||
$cntDoctorCur[$doctorId] = ($cntDoctorCur[$doctorId] ?? 0) + 1;
|
||||
} else {
|
||||
$defByDoctor[$doctorId] = ($defByDoctor[$doctorId] ?? 0.0) + $amt;
|
||||
$cntDoctorDef[$doctorId] = ($cntDoctorDef[$doctorId] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
$foldAmt = static function (array $byAssistant): array {
|
||||
$outRows = [];
|
||||
foreach ($byAssistant as $aid => $amt) {
|
||||
$aid = (int) $aid;
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$outRows[] = ['assistant_id' => $aid, 'amt' => round((float) $amt, 2)];
|
||||
}
|
||||
|
||||
return $outRows;
|
||||
};
|
||||
|
||||
$foldCnt = static function (array $byAssistant): array {
|
||||
$outRows = [];
|
||||
foreach ($byAssistant as $aid => $v) {
|
||||
$aid = (int) $aid;
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$outRows[] = ['assistant_id' => $aid, 'cnt' => (int) $v];
|
||||
}
|
||||
|
||||
return $outRows;
|
||||
};
|
||||
|
||||
$byDeptAmtTot = self::foldPerformanceRowsToDeptByTable($foldAmt($totAgg), $adminToPrimary, $tableRowDeptIds, $deptById, 'amt');
|
||||
$byDeptAmtCur = self::foldPerformanceRowsToDeptByTable($foldAmt($curAgg), $adminToPrimary, $tableRowDeptIds, $deptById, 'amt');
|
||||
$byDeptAmtDef = self::foldPerformanceRowsToDeptByTable($foldAmt($defAgg), $adminToPrimary, $tableRowDeptIds, $deptById, 'amt');
|
||||
|
||||
$byDeptCntTot = self::foldPerformanceRowsToDeptByTable($foldCnt($cntByAsst), $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
||||
$byDeptCntCur = self::foldPerformanceRowsToDeptByTable($foldCnt($curCntByAsst), $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
||||
$byDeptCntDef = self::foldPerformanceRowsToDeptByTable($foldCnt($defCntByAsst), $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
||||
|
||||
$rows = [];
|
||||
$sumCompletedCnt = 0;
|
||||
$sumCompletedAmt = 0.0;
|
||||
$sumCurCnt = 0;
|
||||
$sumCurAmt = 0.0;
|
||||
$sumDefCnt = 0;
|
||||
$sumDefAmt = 0.0;
|
||||
|
||||
foreach ($tableRowDeptIds as $deptId) {
|
||||
$deptId = (int) $deptId;
|
||||
$cCnt = (int) ($byDeptCntTot[$deptId] ?? 0);
|
||||
$cAmt = round((float) ($byDeptAmtTot[$deptId] ?? 0), 2);
|
||||
$kCnt = (int) ($byDeptCntCur[$deptId] ?? 0);
|
||||
$kAmt = round((float) ($byDeptAmtCur[$deptId] ?? 0), 2);
|
||||
$dCnt = (int) ($byDeptCntDef[$deptId] ?? 0);
|
||||
$dAmt = round((float) ($byDeptAmtDef[$deptId] ?? 0), 2);
|
||||
|
||||
$rows[] = [
|
||||
'dept_id' => $deptId,
|
||||
'dept_name' => self::formatYejiDeptRowDisplayName($deptId, $deptById),
|
||||
'completed_order_count' => $cCnt,
|
||||
'completed_order_amount' => $cAmt,
|
||||
'current_period_count' => $kCnt,
|
||||
'current_period_amount' => $kAmt,
|
||||
'deferred_period_count' => $dCnt,
|
||||
'deferred_period_amount' => $dAmt,
|
||||
];
|
||||
|
||||
$sumCompletedCnt += $cCnt;
|
||||
$sumCompletedAmt += $cAmt;
|
||||
$sumCurCnt += $kCnt;
|
||||
$sumCurAmt += $kAmt;
|
||||
$sumDefCnt += $dCnt;
|
||||
$sumDefAmt += $dAmt;
|
||||
}
|
||||
|
||||
$assistantRows = [];
|
||||
foreach (array_keys($totAgg) as $aid) {
|
||||
$aid = (int) $aid;
|
||||
if ($aid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$pid = (int) ($adminToPrimary[$aid] ?? 0);
|
||||
$assistantRows[] = [
|
||||
'assistant_id' => $aid,
|
||||
'assistant_name' => '',
|
||||
'primary_dept_id' => $pid,
|
||||
'primary_dept_name' => $pid > 0 ? self::formatYejiDeptRowDisplayName($pid, $deptById) : '',
|
||||
'carry_in_order_count' => (int) ($carryInByAssist[$aid] ?? 0),
|
||||
'completed_order_count' => (int) ($cntByAsst[$aid] ?? 0),
|
||||
'completed_order_amount' => round((float) ($totAgg[$aid] ?? 0), 2),
|
||||
'current_period_count' => (int) ($curCntByAsst[$aid] ?? 0),
|
||||
'current_period_amount' => round((float) ($curAgg[$aid] ?? 0), 2),
|
||||
'deferred_period_count' => (int) ($defCntByAsst[$aid] ?? 0),
|
||||
'deferred_period_amount' => round((float) ($defAgg[$aid] ?? 0), 2),
|
||||
];
|
||||
}
|
||||
$assistNameMap = self::commissionSettlementBatchAdminNames(array_column($assistantRows, 'assistant_id'));
|
||||
foreach ($assistantRows as &$ar) {
|
||||
$aid = (int) ($ar['assistant_id'] ?? 0);
|
||||
$ar['assistant_name'] = (string) ($assistNameMap[$aid] ?? ('#' . $aid));
|
||||
}
|
||||
unset($ar);
|
||||
usort($assistantRows, static function (array $a, array $b): int {
|
||||
$cmp = ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
||||
|
||||
return $cmp !== 0 ? $cmp : (($b['assistant_id'] ?? 0) <=> ($a['assistant_id'] ?? 0));
|
||||
});
|
||||
|
||||
$doctorRows = [];
|
||||
foreach (array_keys($totByDoctor) as $did) {
|
||||
$did = (int) $did;
|
||||
$pid = $did > 0 ? (int) ($adminToPrimary[$did] ?? 0) : 0;
|
||||
$doctorRows[] = [
|
||||
'doctor_id' => $did,
|
||||
'doctor_name' => '',
|
||||
'primary_dept_id' => $pid,
|
||||
'primary_dept_name' => $pid > 0 ? self::formatYejiDeptRowDisplayName($pid, $deptById) : '',
|
||||
'carry_in_order_count' => (int) ($carryInByDoctor[$did] ?? 0),
|
||||
'completed_order_count' => (int) ($cntDoctorTot[$did] ?? 0),
|
||||
'completed_order_amount' => round((float) ($totByDoctor[$did] ?? 0), 2),
|
||||
'current_period_count' => (int) ($cntDoctorCur[$did] ?? 0),
|
||||
'current_period_amount' => round((float) ($curByDoctor[$did] ?? 0), 2),
|
||||
'deferred_period_count' => (int) ($cntDoctorDef[$did] ?? 0),
|
||||
'deferred_period_amount' => round((float) ($defByDoctor[$did] ?? 0), 2),
|
||||
];
|
||||
}
|
||||
$doctorNameIds = array_values(array_filter(array_column($doctorRows, 'doctor_id'), static fn ($id) => (int) $id > 0));
|
||||
$doctorNameMap = self::commissionSettlementBatchAdminNames($doctorNameIds);
|
||||
foreach ($doctorRows as &$dr) {
|
||||
$did = (int) ($dr['doctor_id'] ?? 0);
|
||||
$dr['doctor_name'] = $did > 0 ? (string) ($doctorNameMap[$did] ?? ('#' . $did)) : '未关联开方人';
|
||||
}
|
||||
unset($dr);
|
||||
usort($doctorRows, static function (array $a, array $b): int {
|
||||
$za = (int) ($a['doctor_id'] ?? 0) === 0 ? 1 : 0;
|
||||
$zb = (int) ($b['doctor_id'] ?? 0) === 0 ? 1 : 0;
|
||||
if ($za !== $zb) {
|
||||
return $za <=> $zb;
|
||||
}
|
||||
$cmp = ($b['completed_order_amount'] ?? 0) <=> ($a['completed_order_amount'] ?? 0);
|
||||
|
||||
return $cmp !== 0 ? $cmp : (($b['doctor_id'] ?? 0) <=> ($a['doctor_id'] ?? 0));
|
||||
});
|
||||
|
||||
return [
|
||||
'settlement_month' => $monthRaw,
|
||||
'order_month' => $orderMonthLabel,
|
||||
'cutoff_end' => date('Y-m-d H:i:s', $cutoffTs),
|
||||
'rule_note' => '订单创建月为结算月的上一自然月;仅统计系统代开处方(is_system_auto=1)对应的履约已完成业务订单。签收时间取物流表签收时间最大值;尾款时间取关联支付单已支付记录(优先类型为尾款/全部)。签收与尾款均不晚于结算月 7 日 24 点的计入「本期提成」,其余已完成订单计入「顺延下期」。上期同渠道同部门范围内「确定业绩」产生的顺延订单,会计入本期订单池。「按医助下发」按业绩归属医助聚合;「按医生下发」按处方开方人(creator_id)聚合。',
|
||||
'channel_code' => $c['channelCode'],
|
||||
'channel_name' => $c['channelInfo'] ? (string) $c['channelInfo']['channel_name'] : '',
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
'confirm' => $confirm,
|
||||
'rows' => $rows,
|
||||
'assistant_rows' => $assistantRows,
|
||||
'doctor_rows' => $doctorRows,
|
||||
'total' => [
|
||||
'completed_order_count' => $sumCompletedCnt,
|
||||
'completed_order_amount' => round($sumCompletedAmt, 2),
|
||||
'current_period_count' => $sumCurCnt,
|
||||
'current_period_amount' => round($sumCurAmt, 2),
|
||||
'deferred_period_count' => $sumDefCnt,
|
||||
'deferred_period_amount' => round($sumDefAmt, 2),
|
||||
'carry_in_order_count' => $carryInCnt,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 提成核对:订单明细(分页)。
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function commissionSettlementOrderLines(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||
{
|
||||
$pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$cutoffTs = $pack['cutoffTs'];
|
||||
$tableRowDeptIds = $pack['tableRowDeptIds'];
|
||||
$deptById = $pack['deptById'];
|
||||
$adminToPrimary = $pack['adminToPrimary'];
|
||||
$carryFlip = array_flip($pack['carryOrderIds']);
|
||||
|
||||
$bucket = trim((string) ($params['bucket'] ?? ''));
|
||||
$page = max(1, (int) ($params['page'] ?? 1));
|
||||
$pageSize = min(100, max(10, (int) ($params['page_size'] ?? 20)));
|
||||
|
||||
if ($tableRowDeptIds === [] || $adminToPrimary === []) {
|
||||
return ['list' => [], 'count' => 0, 'page' => $page, 'page_size' => $pageSize];
|
||||
}
|
||||
|
||||
$query = self::commissionSettlementBuildFilteredOrderQuery($pack);
|
||||
$att = $pack['att'];
|
||||
$signPayFields = self::commissionSettlementSignPayFieldRawList();
|
||||
|
||||
$baseRows = $query->field(array_merge([
|
||||
'o.id',
|
||||
'o.order_no',
|
||||
'o.diagnosis_id',
|
||||
'o.create_time',
|
||||
'o.amount',
|
||||
Db::raw("({$att}) AS assistant_id"),
|
||||
Db::raw('IFNULL(rx.creator_id, 0) AS doctor_id'),
|
||||
], $signPayFields))->order('o.id', 'desc')->select()->toArray();
|
||||
|
||||
$mapped = [];
|
||||
foreach ($baseRows as $r) {
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
||||
$b = $eligible ? 'current' : 'deferred';
|
||||
if ($bucket === 'current' && $b !== 'current') {
|
||||
continue;
|
||||
}
|
||||
if ($bucket === 'deferred' && $b !== 'deferred') {
|
||||
continue;
|
||||
}
|
||||
$oid = (int) ($r['id'] ?? 0);
|
||||
$aid = (int) ($r['assistant_id'] ?? 0);
|
||||
$doctorId = (int) ($r['doctor_id'] ?? 0);
|
||||
$foldRows = [['assistant_id' => $aid, 'cnt' => 1]];
|
||||
$byDept = self::foldPerformanceRowsToDeptByTable($foldRows, $adminToPrimary, $tableRowDeptIds, $deptById, 'cnt');
|
||||
$primaryDeptId = 0;
|
||||
foreach ($byDept as $did => $_c) {
|
||||
$primaryDeptId = (int) $did;
|
||||
break;
|
||||
}
|
||||
|
||||
$mapped[] = [
|
||||
'id' => $oid,
|
||||
'order_no' => (string) ($r['order_no'] ?? ''),
|
||||
'diagnosis_id' => (int) ($r['diagnosis_id'] ?? 0),
|
||||
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
||||
'create_time' => (int) ($r['create_time'] ?? 0),
|
||||
'create_time_text' => !empty($r['create_time']) ? date('Y-m-d H:i:s', (int) $r['create_time']) : '',
|
||||
'sign_ts' => $signTs,
|
||||
'sign_time_text' => $signTs > 0 ? date('Y-m-d H:i:s', $signTs) : '',
|
||||
'pay_ts' => $payTs,
|
||||
'pay_time_text' => $payTs > 0 ? date('Y-m-d H:i:s', $payTs) : '',
|
||||
'assistant_id' => $aid,
|
||||
'doctor_id' => $doctorId,
|
||||
'assistant_name' => '',
|
||||
'doctor_name' => '',
|
||||
'bucket' => $b,
|
||||
'is_carry_in' => $oid > 0 && isset($carryFlip[$oid]) ? 1 : 0,
|
||||
'primary_dept_id' => $primaryDeptId,
|
||||
'primary_dept_name' => $primaryDeptId > 0 ? self::formatYejiDeptRowDisplayName($primaryDeptId, $deptById) : '',
|
||||
];
|
||||
}
|
||||
|
||||
$nameIds = [];
|
||||
foreach ($mapped as $row) {
|
||||
$na = (int) ($row['assistant_id'] ?? 0);
|
||||
if ($na > 0) {
|
||||
$nameIds[$na] = true;
|
||||
}
|
||||
$nd = (int) ($row['doctor_id'] ?? 0);
|
||||
if ($nd > 0) {
|
||||
$nameIds[$nd] = true;
|
||||
}
|
||||
}
|
||||
$adminNames = self::commissionSettlementBatchAdminNames(array_map('intval', array_keys($nameIds)));
|
||||
foreach ($mapped as &$row) {
|
||||
$na = (int) ($row['assistant_id'] ?? 0);
|
||||
$nd = (int) ($row['doctor_id'] ?? 0);
|
||||
$row['assistant_name'] = $na > 0 ? (string) ($adminNames[$na] ?? ('#' . $na)) : '—';
|
||||
$row['doctor_name'] = $nd > 0 ? (string) ($adminNames[$nd] ?? ('#' . $nd)) : '未关联开方人';
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$total = count($mapped);
|
||||
$slice = array_slice($mapped, ($page - 1) * $pageSize, $pageSize);
|
||||
|
||||
return [
|
||||
'list' => $slice,
|
||||
'count' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前筛选条件下的核对 / 确定状态。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string} $params
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function commissionSettlementConfirmStatus(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
|
||||
{
|
||||
$pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$row = self::commissionSettlementGetConfirmRow($pack['monthRaw'], $pack['scopeChannelCode'], $pack['deptKey']);
|
||||
|
||||
return [
|
||||
'settlement_month' => $pack['monthRaw'],
|
||||
'scope_channel_code' => $pack['scopeChannelCode'],
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'confirm' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存核对备注(未确定前可反复保存)。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, reconcile_note?: string} $params
|
||||
*/
|
||||
public static function commissionSettlementSaveReconcile(array $params, int $viewerAdminId, array $viewerAdminInfo): array
|
||||
{
|
||||
if ($viewerAdminId <= 0) {
|
||||
throw new \think\Exception('未登录');
|
||||
}
|
||||
$pack = self::commissionSettlementResolveBasePack($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$note = mb_substr(trim((string) ($params['reconcile_note'] ?? '')), 0, 500);
|
||||
$t = time();
|
||||
|
||||
$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) {
|
||||
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||
if ((int) ($ex['status'] ?? 0) === 1) {
|
||||
throw new \think\Exception('已确定业绩的记录不可修改核对备注,如需更正请联系管理员处理数据');
|
||||
}
|
||||
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
||||
'reconcile_note' => $note,
|
||||
'update_time' => $t,
|
||||
]);
|
||||
} else {
|
||||
Db::name('commission_settlement_confirm')->insert([
|
||||
'settlement_month' => $pack['monthRaw'],
|
||||
'scope_channel_code' => $pack['scopeChannelCode'],
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'status' => 0,
|
||||
'reconcile_note' => $note,
|
||||
'totals_json' => '',
|
||||
'confirmed_admin_id' => 0,
|
||||
'confirmed_admin_name' => '',
|
||||
'confirmed_at' => 0,
|
||||
'create_time' => $t,
|
||||
'update_time' => $t,
|
||||
]);
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定本期业绩:写入快照,并将「顺延下期」订单结转至下一结算月同 scope。
|
||||
*
|
||||
* @param array{settlement_month?: string, dept_ids?: int[]|string, channel_code?: string, reconcile_note?: string} $params
|
||||
*/
|
||||
public static function commissionSettlementConfirmFinalize(array $params, int $viewerAdminId, 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) {
|
||||
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||
if ((int) ($ex['status'] ?? 0) === 1) {
|
||||
throw new \think\Exception('该结算月在同渠道同部门筛选下已确定业绩,请勿重复提交');
|
||||
}
|
||||
}
|
||||
|
||||
$overview = self::commissionSettlementOverview($params, $viewerAdminId, $viewerAdminInfo);
|
||||
$total = $overview['total'] ?? [];
|
||||
|
||||
$query = self::commissionSettlementBuildFilteredOrderQuery($pack);
|
||||
$att = $pack['att'];
|
||||
$signPayFields = self::commissionSettlementSignPayFieldRawList();
|
||||
$cutoffTs = $pack['cutoffTs'];
|
||||
|
||||
$rowsRaw = $query->field(array_merge([
|
||||
'o.id',
|
||||
'o.create_time',
|
||||
Db::raw("({$att}) AS assistant_id"),
|
||||
], $signPayFields))->select()->toArray();
|
||||
|
||||
$deferredIds = [];
|
||||
foreach ($rowsRaw as $r) {
|
||||
$signTs = (int) ($r['sign_ts'] ?? 0);
|
||||
$payTs = (int) ($r['pay_ts'] ?? 0);
|
||||
$eligible = $signTs > 0 && $payTs > 0 && $signTs <= $cutoffTs && $payTs <= $cutoffTs;
|
||||
if (!$eligible) {
|
||||
$deferredIds[] = (int) ($r['id'] ?? 0);
|
||||
}
|
||||
}
|
||||
$deferredIds = array_values(array_unique(array_filter($deferredIds, static fn (int $x): bool => $x > 0)));
|
||||
|
||||
$adminName = (string) (Db::name('admin')->where('id', $viewerAdminId)->whereNull('delete_time')->value('name') ?: '');
|
||||
$note = mb_substr(trim((string) ($params['reconcile_note'] ?? '')), 0, 500);
|
||||
$t = time();
|
||||
$next = $pack['nextSettlementMonth'];
|
||||
|
||||
$totalsJson = json_encode([
|
||||
'total' => $total,
|
||||
'deferred_ids' => $deferredIds,
|
||||
'next_carry_month' => $next,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
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();
|
||||
|
||||
foreach ($deferredIds as $poId) {
|
||||
$dup = Db::name('commission_settlement_carry')
|
||||
->where('prescription_order_id', $poId)
|
||||
->where('target_settlement_month', $next)
|
||||
->where('scope_channel_code', $pack['scopeChannelCode'])
|
||||
->where('scope_dept_ids_key', $pack['deptKey'])
|
||||
->find();
|
||||
if ($dup) {
|
||||
continue;
|
||||
}
|
||||
Db::name('commission_settlement_carry')->insert([
|
||||
'prescription_order_id' => $poId,
|
||||
'target_settlement_month' => $next,
|
||||
'source_settlement_month' => $pack['monthRaw'],
|
||||
'scope_channel_code' => $pack['scopeChannelCode'],
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'create_time' => $t,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($exists) {
|
||||
$ex = is_array($exists) ? $exists : $exists->toArray();
|
||||
Db::name('commission_settlement_confirm')->where('id', (int) $ex['id'])->update([
|
||||
'status' => 1,
|
||||
'reconcile_note' => $note !== '' ? $note : (string) ($ex['reconcile_note'] ?? ''),
|
||||
'totals_json' => (string) $totalsJson,
|
||||
'confirmed_admin_id' => $viewerAdminId,
|
||||
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
||||
'confirmed_at' => $t,
|
||||
'update_time' => $t,
|
||||
]);
|
||||
} else {
|
||||
Db::name('commission_settlement_confirm')->insert([
|
||||
'settlement_month' => $pack['monthRaw'],
|
||||
'scope_channel_code' => $pack['scopeChannelCode'],
|
||||
'scope_dept_ids_key' => $pack['deptKey'],
|
||||
'status' => 1,
|
||||
'reconcile_note' => $note,
|
||||
'totals_json' => (string) $totalsJson,
|
||||
'confirmed_admin_id' => $viewerAdminId,
|
||||
'confirmed_admin_name' => mb_substr($adminName, 0, 64),
|
||||
'confirmed_at' => $t,
|
||||
'create_time' => $t,
|
||||
'update_time' => $t,
|
||||
]);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'next_settlement_month' => $next,
|
||||
'deferred_carry_count' => count($deferredIds),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- 提成结算:核对记录 + 顺延结转(上期「确定业绩」时写入,下期统计自动并入订单池)
|
||||
CREATE TABLE IF NOT EXISTS `zyt_commission_settlement_confirm` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`settlement_month` char(7) NOT NULL COMMENT '结算月 YYYY-MM',
|
||||
`scope_channel_code` varchar(128) NOT NULL DEFAULT '' COMMENT '与查询渠道一致,空为全渠道',
|
||||
`scope_dept_ids_key` varchar(255) NOT NULL DEFAULT '*' COMMENT '部门筛选键:* 或排序后的 id 逗号串',
|
||||
`status` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0已保存核对 1已确定业绩',
|
||||
`reconcile_note` varchar(500) NOT NULL DEFAULT '' COMMENT '核对备注',
|
||||
`totals_json` text COMMENT '确定时汇总快照 JSON',
|
||||
`confirmed_admin_id` int unsigned NOT NULL DEFAULT 0,
|
||||
`confirmed_admin_name` varchar(64) NOT NULL DEFAULT '',
|
||||
`confirmed_at` int unsigned NOT NULL DEFAULT 0 COMMENT '确定业绩时间戳',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_scope` (`settlement_month`,`scope_channel_code`(64),`scope_dept_ids_key`(128))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提成结算核对/确定';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_commission_settlement_carry` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`prescription_order_id` int unsigned NOT NULL COMMENT '业务订单 tcm_prescription_order.id',
|
||||
`target_settlement_month` char(7) NOT NULL COMMENT '计入哪个月结算池',
|
||||
`source_settlement_month` char(7) NOT NULL COMMENT '从哪个月确定顺延出来',
|
||||
`scope_channel_code` varchar(128) NOT NULL DEFAULT '',
|
||||
`scope_dept_ids_key` varchar(255) NOT NULL DEFAULT '*',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_carry` (`prescription_order_id`,`target_settlement_month`,`scope_channel_code`(64),`scope_dept_ids_key`(64)),
|
||||
KEY `idx_target_scope` (`target_settlement_month`,`scope_channel_code`(32),`scope_dept_ids_key`(64)),
|
||||
KEY `idx_source_scope` (`source_settlement_month`,`scope_channel_code`(32),`scope_dept_ids_key`(64))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提成顺延结转';
|
||||
@@ -0,0 +1,101 @@
|
||||
-- 提成结算业绩:独立页面 fans/commission-settlement + stats.commissionSettlement/* 接口权限
|
||||
-- 与业绩看板 yeji 同父菜单(pid = 业绩看板菜单 id 293);幂等
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
y.pid,
|
||||
'C',
|
||||
'提成结算业绩',
|
||||
'',
|
||||
96,
|
||||
'fans/commission-settlement',
|
||||
'/fans/commission-settlement',
|
||||
'fans/commission-settlement',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT pid FROM `zyt_system_menu` WHERE id = 293 LIMIT 1) AS y
|
||||
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` WHERE id = 293)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m WHERE m.component = 'fans/commission-settlement'
|
||||
);
|
||||
|
||||
SET @commission_cs_pid := (
|
||||
SELECT id FROM `zyt_system_menu` WHERE component = 'fans/commission-settlement' ORDER BY id DESC LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-汇总',
|
||||
'',
|
||||
1,
|
||||
'stats.commissionSettlement/overview',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/overview'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-部门下拉',
|
||||
'',
|
||||
2,
|
||||
'stats.commissionSettlement/deptOptions',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/deptOptions'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-渠道下拉',
|
||||
'',
|
||||
3,
|
||||
'stats.commissionSettlement/channelOptions',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/channelOptions'
|
||||
);
|
||||
@@ -0,0 +1,101 @@
|
||||
-- 提成结算:核对/确定业绩接口权限(挂在 fans/commission-settlement 页面菜单下)
|
||||
|
||||
SET @commission_cs_pid := (
|
||||
SELECT id FROM `zyt_system_menu` WHERE component = 'fans/commission-settlement' ORDER BY id DESC LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-核对明细',
|
||||
'',
|
||||
11,
|
||||
'stats.commissionSettlement/orderLines',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/orderLines'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-确认状态',
|
||||
'',
|
||||
12,
|
||||
'stats.commissionSettlement/confirmStatus',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/confirmStatus'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-保存核对',
|
||||
'',
|
||||
13,
|
||||
'stats.commissionSettlement/saveReconcile',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/saveReconcile'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu`
|
||||
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
|
||||
SELECT
|
||||
@commission_cs_pid,
|
||||
'A',
|
||||
'提成结算-确定业绩',
|
||||
'',
|
||||
14,
|
||||
'stats.commissionSettlement/confirmFinalize',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
0,
|
||||
UNIX_TIMESTAMP(),
|
||||
UNIX_TIMESTAMP()
|
||||
FROM (SELECT 1 AS `_`) AS `_exec`
|
||||
WHERE @commission_cs_pid IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu` m
|
||||
WHERE m.pid = @commission_cs_pid AND m.perms = 'stats.commissionSettlement/confirmFinalize'
|
||||
);
|
||||
Reference in New Issue
Block a user