feat: enhance agent settlement features and improve data access controls

- Added new section in AGENTS.md detailing learned workspace facts for better understanding of settlement processes.
- Updated AgentNodeDestroyController to remove unnecessary checks for admin users.
- Enhanced AgentSettlement controllers to assert permissions for finance adjustments and bill operations.
- Improved query scopes in AgentSettlement services to ensure proper data access based on admin roles.
- Refactored methods in SettlementPartyEnrichment for better bill row enrichment and data handling.
- Introduced new methods in AdminAgentSettlementScope for managing agent node visibility and finance adjustments.
This commit is contained in:
2026-06-12 15:59:05 +08:00
parent e14b7b4569
commit 980f3c9593
47 changed files with 2403 additions and 187 deletions

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Services\AgentSettlement;
use App\Models\AgentNode;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/** 代理账单:汇总下级代理在本期保留的占成。 */
final class SettlementBillDownlineShareBuilder
{
public function __construct(
private readonly SettlementPartyEnrichment $partyEnrichment,
) {}
/**
* @return array{
* total: int,
* items: list<array{owner_id: int, owner_label: string, share_profit: int}>
* }
*/
public function forBill(object $bill): array
{
if ((string) $bill->bill_type !== 'agent' || (string) $bill->owner_type !== 'agent') {
return ['total' => 0, 'items' => []];
}
$ownerId = (int) $bill->owner_id;
$periodId = (int) $bill->settlement_period_id;
if ($ownerId <= 0 || $periodId <= 0) {
return ['total' => 0, 'items' => []];
}
$owner = AgentNode::query()->find($ownerId);
if ($owner === null) {
return ['total' => 0, 'items' => []];
}
$descendantIds = AgentNode::query()
->where('admin_site_id', (int) $owner->admin_site_id)
->where('id', '!=', $ownerId)
->where('path', 'like', $owner->path.'%')
->pluck('id')
->map(static fn ($id): int => (int) $id)
->all();
if ($descendantIds === []) {
return ['total' => 0, 'items' => []];
}
$rows = DB::table('settlement_bills')
->where('settlement_period_id', $periodId)
->where('bill_type', 'agent')
->where('owner_type', 'agent')
->whereIn('owner_id', $descendantIds)
->orderBy('owner_id')
->get(['owner_id', 'meta_json']);
if ($rows->isEmpty()) {
return ['total' => 0, 'items' => []];
}
$agentIds = $rows->pluck('owner_id')->map(static fn ($id): int => (int) $id)->all();
$agents = $this->partyEnrichment->loadAgents($agentIds);
$items = [];
$total = 0;
foreach ($rows as $row) {
$shareProfit = $this->shareProfitFromMeta($row->meta_json ?? null);
if ($shareProfit === 0) {
continue;
}
$agentId = (int) $row->owner_id;
$items[] = [
'owner_id' => $agentId,
'owner_label' => $this->partyEnrichment->formatAgent($agents->get($agentId), $agentId),
'share_profit' => $shareProfit,
];
$total += $shareProfit;
}
usort($items, static fn (array $a, array $b): int => $b['share_profit'] <=> $a['share_profit']
?: $a['owner_label'] <=> $b['owner_label']);
return [
'total' => $total,
'items' => $items,
];
}
private function shareProfitFromMeta(mixed $metaJson): int
{
if ($metaJson === null || $metaJson === '') {
return 0;
}
$decoded = is_string($metaJson) ? json_decode($metaJson, true) : $metaJson;
return is_array($decoded) ? (int) ($decoded['share_profit'] ?? 0) : 0;
}
}