安装费明细增加平台筛选与平台汇总 #595

Merged
qiuxiang merged 2 commits from feature/qiuxiang-installation-fee-platform-summary into main 2026-10-26 14:44:54 +08:00
4 changed files with 171 additions and 11 deletions

View File

@ -128,7 +128,7 @@ class InstallationDetailMirrorRepository
/**
* 按客服审核时间汇总安装费;金额取安装明细子表的结算明细金额。
*
* @return array{ready:bool,total:float,rows:list<array{store_name:string,installation_company:string,payment_channel:string,amount:float}>}
* @return array{ready:bool,total:float,rows:list<array{platform:string,store_name:string,installation_company:string,payment_channel:string,amount:float}>}
*/
public function feeDetails(array $filters = []): array
{
@ -160,27 +160,43 @@ class InstallationDetailMirrorRepository
->where('i.kfshTime', '>=', $from)
->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString());
$platformSql = "CASE"
. " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '%阿里巴巴%'"
. " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%天猫%'"
. " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%新佰广%'"
. " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%淘工厂%'"
. " OR COALESCE(TRIM(install_main.shop_name), '') LIKE '%淘宝%' THEN '淘系'"
. " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '抖音-%' THEN '抖音'"
. " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '拼多多-%' THEN '拼多多'"
. " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '唯品会-%' THEN '唯品会'"
. " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '京东-%' THEN '京东'"
. " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '京喜-%' THEN '京喜'"
. " ELSE '其他' END";
$rows = $query
->selectRaw($platformSql . ' AS platform')
->selectRaw("COALESCE(NULLIF(TRIM(install_main.shop_name), ''), '未填写店铺') AS store_name")
->selectRaw("COALESCE(NULLIF(TRIM(install_main.installCompanyName), ''), '未填写安装公司') AS installation_company")
->selectRaw("CASE WHEN install_main.costWay IS NULL OR TRIM(install_main.costWay) = '' THEN '包安装' ELSE '不包安装' END AS payment_channel")
->selectRaw('ROUND(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2))), 2) AS amount')
->groupByRaw(
"COALESCE(NULLIF(TRIM(install_main.shop_name), ''), '未填写店铺'),"
$platformSql . ','
. " COALESCE(NULLIF(TRIM(install_main.shop_name), ''), '未填写店铺'),"
. " COALESCE(NULLIF(TRIM(install_main.installCompanyName), ''), '未填写安装公司'),"
. " CASE WHEN install_main.costWay IS NULL OR TRIM(install_main.costWay) = '' THEN '包安装' ELSE '不包安装' END"
)
->havingRaw('ABS(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2)))) > 0.004')
->get()
->map(static fn (object $row): array => [
'platform' => trim((string) ($row->platform ?? '其他')),
'store_name' => trim((string) ($row->store_name ?? '')),
'installation_company' => trim((string) ($row->installation_company ?? '')),
'payment_channel' => (string) ($row->payment_channel ?? '包安装'),
'amount' => round((float) ($row->amount ?? 0), 2),
])
->sort(static fn (array $left, array $right): int =>
[abs($right['amount']), $left['store_name'], $left['installation_company']]
<=> [abs($left['amount']), $right['store_name'], $right['installation_company']]
[abs($right['amount']), $left['platform'], $left['store_name'], $left['installation_company']]
<=> [abs($left['amount']), $right['platform'], $right['store_name'], $right['installation_company']]
)
->values()
->all();

View File

@ -164,7 +164,7 @@
.installation-fee-filters {
display: grid;
grid-template-columns: repeat(3, minmax(10rem, 1fr));
grid-template-columns: repeat(4, minmax(8.5rem, 1fr));
gap: 0.75rem;
padding: 0.8rem 1.1rem;
border-bottom: 1px solid #eee8dd;
@ -228,6 +228,42 @@
text-align: center;
}
.installation-fee-platform-summary {
margin: 0.75rem 0 1rem;
padding-top: 0.75rem;
border-top: 1px solid #d8d2c7;
}
.installation-fee-platform-summary h3 {
margin: 0 0 0.5rem;
color: #171717;
font-size: 0.75rem;
}
.installation-fee-platform-summary table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.installation-fee-platform-summary th,
.installation-fee-platform-summary td {
padding: 0.5rem;
border-bottom: 1px solid #eee8dd;
text-align: left;
}
.installation-fee-platform-summary th {
background: #f7f0de;
color: #595959;
font-weight: 600;
}
.installation-fee-platform-summary .is-number {
text-align: right;
}
.installation-fee-dialog__foot {
padding: 0.65rem 1.1rem;
border-top: 1px solid #eee8dd;
@ -235,6 +271,10 @@
font-size: 0.6875rem;
}
@media (max-width: 900px) {
.installation-fee-filters { grid-template-columns: repeat(2, minmax(10rem, 1fr)); }
}
@media (max-width: 720px) {
.installation-fee-filters { grid-template-columns: 1fr; }
.installation-fee-dialog__head { flex-direction: column; }
@ -278,6 +318,12 @@
</div>
</div>
<div class="installation-fee-filters" aria-label="安装费明细筛选">
<label class="installation-fee-filter">
<span>平台</span>
<select data-installation-fee-platform aria-label="筛选平台">
<option value="">全部平台</option>
</select>
</label>
<label class="installation-fee-filter">
<span>店铺名称</span>
<select data-installation-fee-store aria-label="筛选店铺名称">
@ -310,6 +356,18 @@
<tbody data-installation-fee-rows></tbody>
</table>
<div class="installation-fee-empty" data-installation-fee-empty>请选择客服审核日期后查看明细</div>
<section class="installation-fee-platform-summary" aria-label="安装费平台汇总">
<h3>平台汇总</h3>
<table>
<thead>
<tr>
<th scope="col">平台</th>
<th scope="col" class="is-number">结算明细金额</th>
</tr>
</thead>
<tbody data-installation-fee-platform-rows></tbody>
</table>
</section>
</div>
<div class="installation-fee-dialog__foot">
<span data-installation-fee-count>共 0 条明细汇总</span>
@ -329,10 +387,12 @@
var status = document.querySelector('[data-installation-fee-status]');
var subtitle = dialog.querySelector('[data-installation-fee-subtitle]');
var currentTotal = dialog.querySelector('[data-installation-fee-current-total]');
var platformFilter = dialog.querySelector('[data-installation-fee-platform]');
var storeFilter = dialog.querySelector('[data-installation-fee-store]');
var companyFilter = dialog.querySelector('[data-installation-fee-company]');
var channelFilter = dialog.querySelector('[data-installation-fee-channel]');
var rowsEl = dialog.querySelector('[data-installation-fee-rows]');
var platformRowsEl = dialog.querySelector('[data-installation-fee-platform-rows]');
var emptyEl = dialog.querySelector('[data-installation-fee-empty]');
var countEl = dialog.querySelector('[data-installation-fee-count]');
var allRows = [];
@ -389,11 +449,13 @@
}
function renderRows() {
var platform = platformFilter.value;
var store = storeFilter.value;
var company = companyFilter.value;
var channel = channelFilter.value;
var rows = allRows.filter(function (row) {
return (!store || row.store_name === store)
return (!platform || row.platform === platform)
&& (!store || row.store_name === store)
&& (!company || row.installation_company === company)
&& (!channel || row.payment_channel === channel);
});
@ -416,6 +478,28 @@
rowsEl.appendChild(tr);
});
var byPlatform = {};
rows.forEach(function (row) {
var key = row.platform || '其他';
byPlatform[key] ??= 0;
byPlatform[key] += Number(row.amount || 0);
});
platformRowsEl.replaceChildren();
Object.keys(byPlatform).sort(function (left, right) {
return Math.abs(byPlatform[right]) - Math.abs(byPlatform[left])
|| left.localeCompare(right, 'zh-CN');
}).forEach(function (name) {
var tr = document.createElement('tr');
var platformCell = document.createElement('td');
platformCell.textContent = name;
tr.appendChild(platformCell);
var amount = document.createElement('td');
amount.className = 'is-number';
amount.textContent = yuan(byPlatform[name]);
tr.appendChild(amount);
platformRowsEl.appendChild(tr);
});
currentTotal.textContent = yuan(total);
countEl.textContent = '共 ' + rows.length.toLocaleString('zh-CN') + ' 条明细汇总';
emptyEl.hidden = rows.length > 0;
@ -428,11 +512,13 @@
allRows = [];
loadedSignature = '';
rowsEl.replaceChildren();
platformRowsEl.replaceChildren();
currentTotal.textContent = '¥0.00';
countEl.textContent = '共 0 条明细汇总';
emptyEl.hidden = false;
emptyEl.textContent = message;
fillFilter(storeFilter, '全部店铺', []);
fillFilter(platformFilter, '全部平台', []);
fillFilter(companyFilter, '全部安装公司', []);
fillFilter(channelFilter, '全部渠道', []);
}
@ -498,6 +584,7 @@
cardTotal.textContent = wan(data.total);
status.textContent = value.from + ' 至 ' + value.to + ' · ' + allRows.length + ' 条汇总';
subtitle.textContent = value.from + ' 至 ' + value.to + ' · 按客服审核时间汇总';
fillFilter(platformFilter, '全部平台', distinct('platform'));
fillFilter(storeFilter, '全部店铺', distinct('store_name'));
fillFilter(companyFilter, '全部安装公司', distinct('installation_company'));
fillFilter(channelFilter, '全部渠道', distinct('payment_channel'));
@ -526,7 +613,7 @@
[fromInput, toInput].forEach(function (input) {
if (input) { input.addEventListener('change', scheduleRefresh); }
});
[storeFilter, companyFilter, channelFilter].forEach(function (select) {
[platformFilter, storeFilter, companyFilter, channelFilter].forEach(function (select) {
select.addEventListener('change', renderRows);
});
dialog.querySelector('[data-installation-fee-close]').addEventListener('click', closeDialog);

View File

@ -268,10 +268,13 @@ class ManagementCrudTest extends FeatureTestCase
->assertSee('id="installation-fee-dialog"', false)
->assertSeeText('安装费金额')
->assertSeeText('按客服审核时间汇总安装明细的结算明细金额')
->assertSeeText('平台')
->assertSeeText('店铺名称')
->assertSeeText('安装公司')
->assertSeeText('收款渠道')
->assertSeeText('结算明细金额')
->assertSee('data-installation-fee-platform', false)
->assertSee('data-installation-fee-platform-rows', false)
->assertSee('refresh(true)', false)
->assertSee("dispatchEvent(new Event('change'", false)
->assertSee('installation-details-table-wrap', false)
@ -336,8 +339,8 @@ class ManagementCrudTest extends FeatureTestCase
'ready' => true,
'total' => 2000.50,
'rows' => [
['store_name' => '店铺A', 'installation_company' => '万师傅', 'payment_channel' => '包安装', 'amount' => 1250.50],
['store_name' => '店铺B', 'installation_company' => '鲁班到家', 'payment_channel' => '不包安装', 'amount' => 750.00],
['platform' => '淘系', 'store_name' => '店铺A', 'installation_company' => '万师傅', 'payment_channel' => '包安装', 'amount' => 1250.50],
['platform' => '京东', 'store_name' => '店铺B', 'installation_company' => '鲁班到家', 'payment_channel' => '不包安装', 'amount' => 750.00],
],
]);
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
@ -349,6 +352,7 @@ class ManagementCrudTest extends FeatureTestCase
]))
->assertOk()
->assertJsonPath('total', 2000.5)
->assertJsonPath('rows.0.platform', '淘系')
->assertJsonPath('rows.0.store_name', '店铺A')
->assertJsonPath('rows.0.payment_channel', '包安装')
->assertJsonPath('rows.1.payment_channel', '不包安装');

View File

@ -154,12 +154,65 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
$this->assertTrue($result['ready']);
$this->assertSame([
['store_name'=>'测试店','installation_company'=>'万师傅','payment_channel'=>'包安装','amount'=>100.0],
['store_name'=>'其他店','installation_company'=>'鲁班到家','payment_channel'=>'不包安装','amount'=>80.0],
['platform'=>'其他','store_name'=>'测试店','installation_company'=>'万师傅','payment_channel'=>'包安装','amount'=>100.0],
['platform'=>'其他','store_name'=>'其他店','installation_company'=>'鲁班到家','payment_channel'=>'不包安装','amount'=>80.0],
],$result['rows']);
$this->assertSame(180.0,$result['total']);
}
public function test_fee_details_classifies_platform_by_store_name_rules(): void
{
$stores = [
'阿里巴巴-测试店' => '淘系',
'天猫-测试店' => '淘系',
'新佰广1688' => '淘系',
'淘工厂-测试店' => '淘系',
'淘宝-测试店' => '淘系',
'抖音-测试店' => '抖音',
'拼多多-测试店' => '拼多多',
'唯品会-测试店' => '唯品会',
'京东-测试店' => '京东',
'京喜-测试店' => '京喜',
'其他平台店铺' => '其他',
];
$mainRows = [];
$itemRows = [];
$id = 100;
foreach (array_keys($stores) as $store) {
$guid = 'platform-' . $id;
$mainRows[] = [
'Id' => $id,
'SourceGuid' => $guid,
'OrderNum' => 'ORDER-' . $id,
'shop_name' => $store,
'SubmitTime' => '2026-09-01',
'ShipTIme' => '2026-09-02',
'installCompanyName' => '万师傅',
'costWay' => null,
];
$itemRows[] = [
'Id' => $id,
'guid' => $guid,
'shopproid' => 'PRODUCT-' . $id,
'kfshTime' => '2026-09-15 10:00:00',
'settleItemAmount' => 1,
];
$id++;
}
DB::connection('business_mirror')->table('fixture_install')->insert($mainRows);
DB::connection('business_mirror')->table('fixture_items')->insert($itemRows);
$result = $this->repository->feeDetails([
'customer_review_from' => '2026-09-01',
'customer_review_to' => '2026-09-30',
]);
$actual = collect($result['rows'])->pluck('platform', 'store_name')->all();
foreach ($stores as $store => $platform) {
$this->assertSame($platform, $actual[$store] ?? null, $store);
}
}
public function test_end_date_alone_is_valid_and_invalid_status_is_rejected(): void
{
request()->merge(['created_to'=>'2026-08-20']);