更新
This commit is contained in:
@@ -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