业绩明细:新增1688红包补贴备注并优化筛选统计

This commit is contained in:
yueyang 2026-10-20 11:28:53 +08:00
parent 2a2df7451e
commit cad8369fdb
3 changed files with 304 additions and 81 deletions

View File

@ -289,14 +289,21 @@ class Pages extends Controller
];
}
private static function receiptReasonNotBasisMarkedSql(): string
{
return "COALESCE(remark, '') NOT IN ('订单实付按卖家实收核对', '订单实付按应付/已付核对')";
}
/**
* 「差额异常备注」原因(不落库):说明差额为什么产生,一行最多一个,按顺序取第一条命中。
* 未匹配行 → 订单实付未匹配(细分原因见「未匹配原因」列);已匹配但基准金额=0 → 订单实付取数为0;
* 基准金额没有减掉优惠/冲减时,差额正好等于对应项。差额按有效值判定(未匹配按基准金额=0)。
* 「差额异常备注」基础原因(不落库),按页面优先级排列。
*
* @return array<string,string> 备注文字 => SQL 条件(已按顺序排除前面命中的行)
* 这里保留可重叠的原始条件;需要“一行只取第一个原因”时走
* receiptDifferenceRemarkIndexSql(),由单次 CASE 按顺序命中,避免把
* 前序大段条件重复拼进后续每一条,拖慢筛选联动。
*
* @return array<string,string> 备注文字 => 原始 SQL 条件
*/
private static function receiptDifferenceRemarkConditions(): array
private static function receiptDifferenceRemarkBaseRules(): array
{
$basis = self::differenceBasis();
$column = $basis['column'];
@ -367,7 +374,7 @@ class Pages extends Controller
$rules['差额=订单实付包含商家承担优惠'] = $jdSofiaGuard . ' AND NOT (' . $jdSofiaGuobu . ')';
// C 方案:这三条是"通用等式档"——口径(卖家实收 / 应付已付)能对上账单的行让位给口径兜底
// (归零 + 口径标识),只有口径也对不上时才显示这三档,避免把口径差标成一堆"未减优惠/冲减"。
$notBasisMarked = "COALESCE(remark, '') NOT IN ('订单实付按卖家实收核对', '订单实付按应付/已付核对')";
$notBasisMarked = self::receiptReasonNotBasisMarkedSql();
$rules[$label . '未减冲减'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - (COALESCE(income_amount, 0) + COALESCE(discount_amount, 0))) < 0.005 AND " . $notBasisMarked;
$rules[$label . '未减优惠'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - (COALESCE(income_amount, 0) + COALESCE(offset_amount, 0))) < 0.005 AND " . $notBasisMarked;
$rules[$label . '未减优惠和冲减'] = "match_status = '已匹配' AND ABS(COALESCE($column, 0) - COALESCE(income_amount, 0)) < 0.005 AND " . $notBasisMarked;
@ -405,27 +412,53 @@ class Pages extends Controller
. ' OR ' . $refundFullHit($refundEvidence['refund_sum'])
. ') ELSE 0 END = 1) OR ' . $taoRefundHit;
// 1688 先采后付订单的订单实付包含平台红包补贴,但支付宝实际到账是扣除补贴后的现金;
// 仅对已匹配、负差额、无优惠/冲减/退款且当前没有口径标识的行标注,不改差额金额。
$rules['订单实付含平台红包补贴'] = "platform = '淘系支付宝'"
. " AND (store_name LIKE '新佰广%' OR store_name LIKE '阿里巴巴%' OR store_name LIKE '1688%')"
. ' AND match_status = \'已匹配\''
. ' AND ' . $effective . ' < -0.005'
. ' AND ABS(COALESCE(discount_amount, 0)) < 0.005'
. ' AND ABS(COALESCE(offset_amount, 0)) < 0.005'
. ' AND ABS(COALESCE(refund_amount, 0)) < 0.005'
. ' AND COALESCE(paid_amount, 0) > COALESCE(income_amount, 0) + 0.005'
. ' AND ' . $notBasisMarked;
// 京喜分位尾差:具体原因都没命中时,1–3 分钱按四舍五入尾差标注。
$rules['差额=分位四舍五入尾差'] = "platform = '京喜账单' AND ABS(" . $effective . ') <= 0.0301 AND ' . $notBasisMarked;
$conditions = [];
$seen = [];
foreach ($rules as $label => $condition) {
$condition = 'ABS(' . $effective . ') > 0.005 AND (' . $condition . ')';
$conditions[$label] = $seen === [] ? $condition : $condition . ' AND NOT (' . implode(' OR ', $seen) . ')';
$seen[] = '(' . $condition . ')';
return $rules;
}
/** @return list<string> 差额异常备注的下拉顺序(含兜底档) */
private static function receiptDifferenceRemarkLabels(): array
{
return array_merge(array_keys(self::receiptDifferenceRemarkBaseRules()), ['差额待核实']);
}
/**
* 差额异常备注的优先级序号(1 开始;0=无备注)。
*
* 单次 CASE 命中后即停止,筛选统计与备注筛选共用同一口径。相比把
* 18 条条件逐条带上前序 NOT 再分别 SUM,避免重复计算退款等关联子查询。
*/
private static function receiptDifferenceRemarkIndexSql(): string
{
$effective = 'COALESCE(difference, COALESCE(income_amount, 0) + COALESCE(discount_amount, 0) + COALESCE(offset_amount, 0))';
$sql = 'CASE WHEN ABS(' . $effective . ') <= 0.005 THEN 0';
$index = 1;
foreach (self::receiptDifferenceRemarkBaseRules() as $condition) {
$sql .= ' WHEN (' . $condition . ') THEN ' . $index;
$index++;
}
// 兜底档:有差额但没命中任何原因 →「差额待核实」(与页面渲染的兜底备注保持一致)。
$conditions['差额待核实'] = 'ABS(' . $effective . ') > 0.005 AND NOT (' . implode(' OR ', $seen) . ') AND ' . $notBasisMarked;
return $conditions;
return $sql . ' WHEN ' . self::receiptReasonNotBasisMarkedSql() . ' THEN ' . $index . ' ELSE 0 END';
}
/**
* 该单的聚水潭应付(主单优先 pay_amount)是否大于账单收入(= 含国补/补贴类差额的口径)。
*
* 与 receiptDifferenceRemarkConditions() 里的聚水潭应付款子查询同口径;按单号缓存应付金额,
* 与 receiptDifferenceRemarkBaseRules() 里的聚水潭应付款子查询同口径;按单号缓存应付金额,
* 页面渲染与导出逐行调用时不会对同一单重复查询。
*/
private static function receiptJdSubsidyPayExceedsIncome(string $orderNo, float $income): bool
@ -496,6 +529,13 @@ class Pages extends Controller
return abs((float) ($row->paid_amount ?? 0) - (float) ($row->product_amount ?? 0)) < 0.02;
}
private static function is1688StoreName(string $storeName): bool
{
return str_starts_with($storeName, '新佰广')
|| str_starts_with($storeName, '阿里巴巴')
|| str_starts_with($storeName, '1688');
}
/** 单行的「差额异常备注」(与筛选条件同一套口径,页面与导出共用)。 */
/**
* 页面口径的「有效差额」SQL:口径兜底行(备注为空、但能对上实收/应付)按 0 计。
@ -503,17 +543,10 @@ class Pages extends Controller
*/
private static function receiptEffectiveDifferenceSql(): string
{
// 注意:要把「差额待核实」这条兜底规则排除掉,否则它本身就是"都没命中",
// 会让这里永远不成立(口径兜底行就归不了零)。
$rules = self::receiptDifferenceRemarkConditions();
unset($rules['差额待核实']);
$ruleUnion = '(' . implode(' OR ', array_map(
static fn (string $condition): string => '(' . $condition . ')',
$rules
)) . ')';
// 备注序号 > 0 表示命中了明确原因;只有口径兜底且没有明确原因时才归零。
$indexSql = self::receiptDifferenceRemarkIndexSql();
return "CASE WHEN remark IN (" . self::receiptBasisFallbackSqlList() . ')'
. " AND NOT COALESCE({$ruleUnion}, 0) THEN 0"
. " AND COALESCE({$indexSql}, 0) = 0 THEN 0"
. ' ELSE ' . self::receiptDefaultDifferenceSql() . ' END';
}
@ -529,14 +562,7 @@ class Pages extends Controller
*/
private static function receiptFallbackDifferenceAdjustmentSql(): string
{
$rules = self::receiptDifferenceRemarkConditions();
unset($rules['差额待核实']);
$ruleUnion = '(' . implode(' OR ', array_map(
static fn (string $condition): string => '(' . $condition . ')',
$rules
)) . ')';
return 'CASE WHEN NOT COALESCE(' . $ruleUnion . ', 0) THEN -('
return 'CASE WHEN COALESCE(' . self::receiptDifferenceRemarkIndexSql() . ', 0) = 0 THEN -('
. self::receiptDefaultDifferenceSql() . ') ELSE 0 END';
}
@ -688,6 +714,18 @@ class Pages extends Controller
return null;
}
// 1688 先采后付红包补贴:订单实付包含平台补贴,支付宝到账只含现金部分。
// 先只作为已知原因标注,不改变差额金额或税务收入。
if (($row->platform ?? '') === '淘系支付宝'
&& $difference < -0.005
&& self::is1688StoreName((string) ($row->store_name ?? ''))
&& abs($discount) < 0.005
&& abs($offset) < 0.005
&& abs((float) ($row->refund_amount ?? 0)) < 0.005
&& (float) ($row->paid_amount ?? 0) > $income + 0.005) {
return '订单实付含平台红包补贴';
}
// 京喜分位尾差:具体原因都没命中时,1–3 分钱按四舍五入尾差标注。
if (($row->platform ?? '') === '京喜账单' && abs($difference) <= 0.0301) {
return '差额=分位四舍五入尾差';
@ -1346,7 +1384,7 @@ class Pages extends Controller
)));
$stats['status_options'] = self::RECEIPT_STATUS_OPTIONS;
// 差额异常备注下拉:只保留「差额为什么产生」的原因备注(按差额公式判定)
$stats['remark_options'] = array_values(array_keys(self::receiptDifferenceRemarkConditions()));
$stats['remark_options'] = self::receiptDifferenceRemarkLabels();
// 未匹配原因下拉:未匹配行在库里的原因标注
$stats['unmatched_reason_options'] = self::unmatchedReasonOptions();
@ -1419,20 +1457,21 @@ class Pages extends Controller
return;
}
$differenceConditions = self::receiptDifferenceRemarkConditions();
$differenceLabels = array_values(array_intersect($remarkFilter, array_keys($differenceConditions)));
if ($differenceLabels === []) {
$labels = self::receiptDifferenceRemarkLabels();
$indexes = [];
foreach ($remarkFilter as $label) {
$index = array_search($label, $labels, true);
if ($index !== false) {
$indexes[] = (int) $index + 1;
}
}
if ($indexes === []) {
return;
}
$query->where(function ($group) use ($differenceLabels, $differenceConditions): void {
foreach ($differenceLabels as $index => $label) {
$method = $index === 0 ? 'where' : 'orWhere';
$group->{$method}(function ($sub) use ($label, $differenceConditions): void {
$sub->whereRaw($differenceConditions[$label]);
});
}
});
$query->whereRaw(
self::receiptDifferenceRemarkIndexSql() . ' IN (' . implode(', ', array_unique($indexes)) . ')'
);
}
/**
@ -1626,18 +1665,192 @@ class Pages extends Controller
return response()->json(cache()->remember($cacheKey, now()->addSeconds(600), function () use ($month, $filters, $orderNo) {
return [
'month' => substr($month, 0, 7),
'facets' => [
'platform' => $this->receiptOrderMatchFacet($month, 'platform', $filters, $orderNo, self::RECEIPT_PLATFORM_OPTIONS),
'store_name' => $this->receiptOrderMatchFacet($month, 'store_name', $filters, $orderNo),
'match_status' => $this->receiptOrderMatchFacet($month, 'match_status', $filters, $orderNo, self::RECEIPT_STATUS_OPTIONS),
'remark' => $this->receiptOrderMatchFacet($month, 'remark', $filters, $orderNo, array_keys(self::receiptDifferenceRemarkConditions())),
'unmatched_reason' => $this->receiptOrderMatchFacet($month, 'unmatched_reason', $filters, $orderNo, self::unmatchedReasonOptions()),
'difference_state' => $this->receiptDifferenceStateFacet($month, $filters, $orderNo),
],
'facets' => $this->receiptOrderMatchFacets($month, $filters, $orderNo),
];
}));
}
/**
* 一次请求生成全部筛选联动计数。
*
* 多个筛选项如果共用同一组“其它条件”,只查一次再一起聚合。页面当前先按
* 差额备注筛选时,原先 6 次重复全表条件判断会合并成 2 次查询。
*
* @param array<string,list<string>> $filters
* @return array<string,array{values:list<string>,counts:array<string,int>}>
*/
private function receiptOrderMatchFacets(string $month, array $filters, string $orderNo): array
{
$facetOrder = [
'platform' => self::RECEIPT_PLATFORM_OPTIONS,
'store_name' => [],
'match_status' => self::RECEIPT_STATUS_OPTIONS,
'remark' => self::receiptDifferenceRemarkLabels(),
'unmatched_reason' => self::unmatchedReasonOptions(),
'difference_state' => ['有差额', '无差额'],
];
// “统计某个筛选本身”时排除该筛选;条件完全相同的筛选项合并成一个查询。
$groups = [];
foreach (array_keys($facetOrder) as $column) {
$signature = $filters;
unset($signature[$column]);
$signature = array_filter(
$signature,
static fn ($values): bool => $values !== [] && $values !== ''
);
ksort($signature);
$key = md5(serialize([$orderNo, $signature]));
$groups[$key]['filters'] = $signature;
$groups[$key]['columns'][] = $column;
}
$facets = [];
foreach ($groups as $group) {
if ($group['filters'] === []) {
foreach ($group['columns'] as $column) {
$facets[$column] = $column === 'difference_state'
? $this->receiptDifferenceStateFacet($month, $filters, $orderNo)
: $this->receiptOrderMatchFacet($month, $column, $filters, $orderNo, $facetOrder[$column]);
}
continue;
}
$counts = $this->receiptOrderMatchFilteredFacetCounts(
$month,
$group['filters'],
$orderNo,
$group['columns']
);
foreach ($group['columns'] as $column) {
$columnCounts = $counts[$column] ?? [];
$ordered = array_values(array_intersect($facetOrder[$column], array_keys($columnCounts)));
$rest = array_values(array_diff(array_keys($columnCounts), $ordered));
sort($rest, SORT_NATURAL | SORT_FLAG_CASE);
$facets[$column] = [
'values' => array_merge($ordered, $rest),
'counts' => $columnCounts,
];
}
}
return array_replace(array_fill_keys(array_keys($facetOrder), ['values' => [], 'counts' => []]), $facets);
}
/**
* 共用同一组筛选条件时,一次分组查询算出多个筛选项的计数。
*
* @param array<string,list<string>> $filters
* @param list<string> $columns
* @return array<string,array<string,int>>
*/
private function receiptOrderMatchFilteredFacetCounts(string $month, array $filters, string $orderNo, array $columns): array
{
$labels = self::receiptDifferenceRemarkLabels();
$basisMarks = ['订单实付按卖家实收核对', '订单实付按应付/已付核对'];
$remarkIndexes = [];
foreach ((array) ($filters['remark'] ?? []) as $label) {
$index = array_search($label, $labels, true);
if ($index !== false) {
$remarkIndexes[] = (int) $index + 1;
}
}
$differenceValues = array_map('strval', (array) ($filters['difference_state'] ?? []));
$wantDifference = in_array('有差额', $differenceValues, true);
$wantNoDifference = in_array('无差额', $differenceValues, true);
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
$inner = DB::connection($connection)
->table('store_receipt_order_match_rows')
->where('report_month', $month)
->select(['platform', 'store_name', 'match_status', 'remark'])
->selectRaw(self::receiptDefaultDifferenceSql() . ' AS base_difference')
->selectRaw(self::receiptDifferenceRemarkIndexSql() . ' AS reason_index');
foreach ($filters as $key => $values) {
if ($key === 'remark' || $key === 'difference_state' || $values === []) {
continue;
}
if ($key === 'unmatched_reason') {
$this->applyReceiptUnmatchedReasonFilter($inner, (array) $values);
continue;
}
$inner->whereIn($key, $values);
}
if (trim($orderNo) !== '') {
$inner->where('online_order_no', 'like', '%' . addcslashes(trim($orderNo), '%_\\') . '%');
}
$outer = DB::connection($connection)
->query()
->fromSub($inner, 'facet_rows');
if ($remarkIndexes !== []) {
$outer->whereIn('reason_index', array_values(array_unique($remarkIndexes)));
}
if ($wantDifference !== $wantNoDifference) {
$effective = 'CASE WHEN remark IN ('
. implode(', ', array_map(static fn (string $mark): string => "'" . $mark . "'", $basisMarks))
. ') AND reason_index = 0 THEN 0 ELSE base_difference END';
$outer->whereRaw('ABS(' . $effective . ')' . ($wantDifference ? ' > 0.005' : ' <= 0.005'));
}
$rows = $outer
->select(['platform', 'store_name', 'match_status', 'remark', 'reason_index', 'base_difference'])
->selectRaw('COUNT(*) AS total')
->groupBy(['platform', 'store_name', 'match_status', 'remark', 'reason_index', 'base_difference'])
->get();
$counts = [
'platform' => [],
'store_name' => [],
'match_status' => [],
'remark' => [],
'unmatched_reason' => [],
'difference_state' => [],
];
$wanted = array_fill_keys($columns, true);
foreach ($rows as $row) {
$total = (int) ($row->total ?? 0);
$platform = (string) ($row->platform ?? '');
$store = (string) ($row->store_name ?? '');
$status = (string) ($row->match_status ?? '');
$rawRemark = trim((string) ($row->remark ?? ''));
$reasonIndex = (int) ($row->reason_index ?? 0);
$reasonLabel = $reasonIndex > 0 ? ($labels[$reasonIndex - 1] ?? null) : null;
if (isset($wanted['platform']) && $platform !== '') {
$counts['platform'][$platform] = ($counts['platform'][$platform] ?? 0) + $total;
}
if (isset($wanted['store_name']) && $store !== '') {
$counts['store_name'][$store] = ($counts['store_name'][$store] ?? 0) + $total;
}
if (isset($wanted['match_status']) && $status !== '') {
$counts['match_status'][$status] = ($counts['match_status'][$status] ?? 0) + $total;
}
if (isset($wanted['remark']) && $reasonLabel !== null) {
$counts['remark'][$reasonLabel] = ($counts['remark'][$reasonLabel] ?? 0) + $total;
}
if (isset($wanted['unmatched_reason'])) {
$unmatchedReason = ($status === '未匹配' && $rawRemark !== '')
? $rawRemark
: (in_array($rawRemark, $basisMarks, true) ? $rawRemark : null);
if ($unmatchedReason !== null) {
$counts['unmatched_reason'][$unmatchedReason] = ($counts['unmatched_reason'][$unmatchedReason] ?? 0) + $total;
}
}
if (isset($wanted['difference_state'])) {
$difference = (float) ($row->base_difference ?? 0);
if ($reasonIndex === 0 && in_array($rawRemark, $basisMarks, true)) {
$difference = 0.0;
}
$differenceLabel = abs($difference) > 0.005 ? '有差额' : '无差额';
$counts['difference_state'][$differenceLabel] = ($counts['difference_state'][$differenceLabel] ?? 0) + $total;
}
}
return $counts;
}
/**
* 单个筛选项的可选值与行数:应用除该筛选项以外的所有条件。
*
@ -1676,23 +1889,24 @@ class Pages extends Controller
$query->where('online_order_no', 'like', '%' . addcslashes($orderNo, '%_\\') . '%');
}
// 差额异常备注联动:5 条原因一次查询算完(原来是 5 次全表 count,下拉要等 1 秒多)
$differenceCounts = [];
$differenceLabels = array_keys(self::receiptDifferenceRemarkConditions());
if ($column === 'remark') {
$expressions = [];
foreach (array_values(self::receiptDifferenceRemarkConditions()) as $index => $condition) {
$expressions[] = 'SUM(CASE WHEN (' . $condition . ') THEN 1 ELSE 0 END) as c' . $index;
}
$row = (clone $query)->selectRaw(implode(', ', $expressions))->first();
foreach ($differenceLabels as $index => $label) {
$differenceCounts[$label] = (int) ($row->{'c' . $index} ?? 0);
}
}
$counts = [];
// 未匹配原因是页面级判定(只统计未匹配行,按库里的 remark 分组)
if ($column === 'unmatched_reason') {
if ($column === 'remark') {
// 一行只按优先级命中一次,再按序号分组,避免 18 条规则各跑一遍全表。
$indexSql = self::receiptDifferenceRemarkIndexSql();
$rows = (clone $query)
->selectRaw($indexSql . ' AS reason_index')
->selectRaw('COUNT(*) AS total')
->groupByRaw('reason_index')
->pluck('total', 'reason_index');
$labels = self::receiptDifferenceRemarkLabels();
foreach ($rows as $index => $total) {
$label = $labels[(int) $index - 1] ?? null;
if ($label !== null && (int) $total > 0) {
$counts[$label] = (int) $total;
}
}
} elseif ($column === 'unmatched_reason') {
// 未匹配原因是页面级判定(只统计未匹配行,按库里的 remark 分组)
$base = clone $query;
$rows = $query->where('match_status', '未匹配')
->select('remark')
@ -1711,8 +1925,7 @@ class Pages extends Controller
$counts[$basisMark] = (int) $marker;
}
}
} elseif ($column !== 'remark') {
// 差额异常备注是页面级判定,不再按库里的标注列分组
} else {
$rows = $query->select($column)
->selectRaw('count(*) as total')
->whereNotNull($column)
@ -1723,11 +1936,6 @@ class Pages extends Controller
$counts[(string) $value] = (int) $total;
}
}
foreach ($differenceCounts as $label => $total) {
if ((int) $total > 0) {
$counts[$label] = (int) $total;
}
}
$ordered = array_values(array_intersect($order, array_keys($counts)));
$rest = array_values(array_diff(array_keys($counts), $ordered));

View File

@ -1896,13 +1896,17 @@
}
function refresh() {
widgets.forEach(function (widget) { widget.setLoading(true); });
window.fetch(endpoint + '?' + currentParams().toString(), {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
credentials: 'same-origin'
}).then(function (response) {
return response.ok ? response.json() : null;
}).then(function (data) {
if (!data || !data.facets) { return; }
if (!data || !data.facets) {
widgets.forEach(function (widget) { widget.setLoading(false); });
return;
}
widgets.forEach(function (widget) {
var facet = data.facets[widget.root.getAttribute('data-name')];
if (!facet || !facet.values) { return; }
@ -1912,8 +1916,10 @@
});
widget.setOptions(options, facet.counts || {});
});
widgets.forEach(function (widget) { widget.setLoading(false); });
}).catch(function () {
// 联动统计失败时保留原有选项,不影响筛选本身。
widgets.forEach(function (widget) { widget.setLoading(false); });
});
}
@ -1942,7 +1948,8 @@
}
});
}
schedule();
// 首次加载立即取联动数量;只有勾选变化时才做 250ms 防抖。
refresh();
})();
</script>
<script>

View File

@ -149,6 +149,7 @@
var valuesEl = root.querySelector('[data-role="values"]');
var candidates = (opts.options || []).slice();
var counts = opts.counts || {};
var countsLoading = false;
var selectedList = [];
var keyword = '';
var optsEl = null;
@ -163,7 +164,9 @@
function isSel(v) { return selectedList.indexOf(v) >= 0; }
function countText(v) {
var total = counts[v];
if (total === null || total === undefined) { return ''; }
if (total === null || total === undefined) {
return countsLoading ? '统计中…' : '';
}
return String(total).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
@ -243,8 +246,13 @@
setOptions: function (list, nextCounts) {
candidates = (list || []).slice();
if (nextCounts) { counts = nextCounts; }
countsLoading = false;
renderOptions();
sync();
},
setLoading: function (loading) {
countsLoading = Boolean(loading);
renderOptions();
}
};