- 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.
362 lines
12 KiB
PHP
362 lines
12 KiB
PHP
<?php
|
||
|
||
namespace App\Support;
|
||
|
||
use App\Models\AdminUser;
|
||
use App\Models\AgentNode;
|
||
use Illuminate\Database\Query\Builder;
|
||
|
||
/** 结算中心账单:站点范围 + 绑定代理仅见直属边(玩家↔直属代理、代理↔直接上下级)。 */
|
||
final class AdminAgentSettlementScope
|
||
{
|
||
/**
|
||
* @return list<int>|null null = 不限制子树(超管或未绑定代理)
|
||
*/
|
||
public static function subtreeAgentNodeIds(AdminUser $admin): ?array
|
||
{
|
||
if ($admin->isSuperAdmin()) {
|
||
return null;
|
||
}
|
||
|
||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||
if ($actor === null) {
|
||
return null;
|
||
}
|
||
|
||
$ids = AgentNode::query()
|
||
->where('path', 'like', $actor->path.'%')
|
||
->pluck('id')
|
||
->map(static fn ($id): int => (int) $id)
|
||
->all();
|
||
|
||
return $ids;
|
||
}
|
||
|
||
public static function boundAgentNodeId(AdminUser $admin): ?int
|
||
{
|
||
if ($admin->isSuperAdmin()) {
|
||
return null;
|
||
}
|
||
|
||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||
|
||
return $actor !== null ? (int) $actor->id : null;
|
||
}
|
||
|
||
public static function applyToPeriodsQuery(Builder $query, AdminUser $admin, string $periodsAlias = 'settlement_periods'): void
|
||
{
|
||
$siteIds = $admin->accessibleAdminSiteIds();
|
||
if ($siteIds === null) {
|
||
return;
|
||
}
|
||
|
||
if ($siteIds === []) {
|
||
$query->whereRaw('0 = 1');
|
||
|
||
return;
|
||
}
|
||
|
||
$query->whereIn($periodsAlias.'.admin_site_id', $siteIds);
|
||
}
|
||
|
||
public static function applyToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void
|
||
{
|
||
$siteIds = $admin->accessibleAdminSiteIds();
|
||
if ($siteIds === null) {
|
||
self::applyDirectEdgeScopeToBillsQuery($query, $admin, $billsAlias);
|
||
|
||
return;
|
||
}
|
||
|
||
if ($siteIds === []) {
|
||
$query->whereRaw('0 = 1');
|
||
|
||
return;
|
||
}
|
||
|
||
$query->whereExists(function (Builder $sub) use ($siteIds, $billsAlias): void {
|
||
$sub->selectRaw('1')
|
||
->from('settlement_periods')
|
||
->whereColumn('settlement_periods.id', $billsAlias.'.settlement_period_id')
|
||
->whereIn('settlement_periods.admin_site_id', $siteIds);
|
||
});
|
||
|
||
self::applyDirectEdgeScopeToBillsQuery($query, $admin, $billsAlias);
|
||
}
|
||
|
||
/** @deprecated 使用 {@see applyDirectEdgeScopeToBillsQuery} */
|
||
public static function applySubtreeToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void
|
||
{
|
||
self::applyDirectEdgeScopeToBillsQuery($query, $admin, $billsAlias);
|
||
}
|
||
|
||
/**
|
||
* 绑定代理:
|
||
* - 玩家账单:仅直属玩家(players.agent_node_id = 本节点)
|
||
* - 代理账单:owner=本节点(向上)或 counterparty=本节点(下级向我结)
|
||
*/
|
||
/**
|
||
* 结算中心玩家维度:绑定代理仅见直属玩家;站点财务/超管见全站(由调用方再限 site_code)。
|
||
*/
|
||
public static function applyDirectPlayersToAlias(Builder $query, AdminUser $admin, string $alias = 'p'): void
|
||
{
|
||
if ($admin->isSuperAdmin() || self::canManageSitePeriods($admin)) {
|
||
return;
|
||
}
|
||
|
||
$actorId = self::boundAgentNodeId($admin);
|
||
if ($actorId === null) {
|
||
$query->whereRaw('0 = 1');
|
||
|
||
return;
|
||
}
|
||
|
||
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
|
||
return;
|
||
}
|
||
|
||
$query->where($alias.'.agent_node_id', $actorId);
|
||
}
|
||
|
||
public static function applyDirectEdgeScopeToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void
|
||
{
|
||
$actorId = self::boundAgentNodeId($admin);
|
||
if ($actorId === null) {
|
||
return;
|
||
}
|
||
|
||
$query->where(function (Builder $outer) use ($billsAlias, $actorId): void {
|
||
$outer->where(function (Builder $player) use ($billsAlias, $actorId): void {
|
||
$player->where($billsAlias.'.owner_type', 'player')
|
||
->whereExists(function (Builder $exists) use ($billsAlias, $actorId): void {
|
||
$exists->selectRaw('1')
|
||
->from('players')
|
||
->whereColumn('players.id', $billsAlias.'.owner_id')
|
||
->where('players.agent_node_id', $actorId);
|
||
});
|
||
})->orWhere(function (Builder $agent) use ($billsAlias, $actorId): void {
|
||
$agent->where($billsAlias.'.owner_type', 'agent')
|
||
->where(function (Builder $edge) use ($billsAlias, $actorId): void {
|
||
$edge->where($billsAlias.'.owner_id', $actorId)
|
||
->orWhere(function (Builder $incoming) use ($billsAlias, $actorId): void {
|
||
$incoming->where($billsAlias.'.counterparty_type', 'agent')
|
||
->where($billsAlias.'.counterparty_id', $actorId);
|
||
})
|
||
->orWhere(function (Builder $platform) use ($billsAlias, $actorId): void {
|
||
$platform->where($billsAlias.'.counterparty_type', 'platform')
|
||
->where($billsAlias.'.owner_id', $actorId);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
public static function periodAccessible(AdminUser $admin, int $settlementPeriodId): bool
|
||
{
|
||
$siteIds = $admin->accessibleAdminSiteIds();
|
||
if ($siteIds === null) {
|
||
return true;
|
||
}
|
||
|
||
if ($siteIds === []) {
|
||
return false;
|
||
}
|
||
|
||
return \Illuminate\Support\Facades\DB::table('settlement_periods')
|
||
->where('id', $settlementPeriodId)
|
||
->whereIn('admin_site_id', $siteIds)
|
||
->exists();
|
||
}
|
||
|
||
public static function siteAccessible(AdminUser $admin, int $adminSiteId): bool
|
||
{
|
||
$siteIds = $admin->accessibleAdminSiteIds();
|
||
if ($siteIds === null) {
|
||
return true;
|
||
}
|
||
|
||
return in_array($adminSiteId, $siteIds, true);
|
||
}
|
||
|
||
/** 绑定代理账号不可开/关全站账期(仅站点财务或超管)。 */
|
||
public static function canManageSitePeriods(AdminUser $admin): bool
|
||
{
|
||
if ($admin->isSuperAdmin()) {
|
||
return true;
|
||
}
|
||
|
||
return AdminAgentScope::primaryAgentNode($admin) === null;
|
||
}
|
||
|
||
public static function assertCanManageSitePeriods(AdminUser $admin): void
|
||
{
|
||
if (! self::canManageSitePeriods($admin)) {
|
||
abort(403, 'agent_bound_cannot_manage_periods');
|
||
}
|
||
}
|
||
|
||
/** 坏账核销 / 补差冲正仅站点财务或超管(绑定代理不可操作)。 */
|
||
public static function canPerformFinanceAdjustments(AdminUser $admin): bool
|
||
{
|
||
return self::canManageSitePeriods($admin);
|
||
}
|
||
|
||
public static function assertCanPerformFinanceAdjustments(AdminUser $admin): void
|
||
{
|
||
if (! self::canPerformFinanceAdjustments($admin)) {
|
||
abort(403, 'agent_bound_cannot_finance_adjust');
|
||
}
|
||
}
|
||
|
||
public static function billAccessible(AdminUser $admin, int $settlementBillId): bool
|
||
{
|
||
$siteIds = $admin->accessibleAdminSiteIds();
|
||
if ($siteIds !== null && $siteIds === []) {
|
||
return false;
|
||
}
|
||
|
||
$bill = \Illuminate\Support\Facades\DB::table('settlement_bills as sb')
|
||
->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
||
->where('sb.id', $settlementBillId)
|
||
->select([
|
||
'sb.owner_type',
|
||
'sb.owner_id',
|
||
'sb.counterparty_type',
|
||
'sb.counterparty_id',
|
||
'sp.admin_site_id',
|
||
])
|
||
->first();
|
||
|
||
if ($bill === null) {
|
||
return false;
|
||
}
|
||
|
||
if ($siteIds !== null && ! in_array((int) $bill->admin_site_id, $siteIds, true)) {
|
||
return false;
|
||
}
|
||
|
||
$actorId = self::boundAgentNodeId($admin);
|
||
if ($actorId === null) {
|
||
return true;
|
||
}
|
||
|
||
return self::billMatchesDirectEdgeScope($actorId, $bill);
|
||
}
|
||
|
||
public static function canOperateBill(AdminUser $admin, int $settlementBillId): bool
|
||
{
|
||
if (! self::billAccessible($admin, $settlementBillId)) {
|
||
return false;
|
||
}
|
||
|
||
if (self::canManageSitePeriods($admin)) {
|
||
return true;
|
||
}
|
||
|
||
$bill = \Illuminate\Support\Facades\DB::table('settlement_bills')
|
||
->where('id', $settlementBillId)
|
||
->select(['owner_type', 'owner_id', 'counterparty_type', 'counterparty_id', 'net_amount'])
|
||
->first();
|
||
|
||
if ($bill === null) {
|
||
return false;
|
||
}
|
||
|
||
$actorId = self::boundAgentNodeId($admin);
|
||
if ($actorId === null) {
|
||
return true;
|
||
}
|
||
|
||
return self::billOperableByBoundAgent($actorId, $bill);
|
||
}
|
||
|
||
public static function assertCanOperateBill(AdminUser $admin, int $settlementBillId): void
|
||
{
|
||
abort_if(! self::billAccessible($admin, $settlementBillId), 404);
|
||
|
||
if (self::canManageSitePeriods($admin)) {
|
||
return;
|
||
}
|
||
|
||
$bill = \Illuminate\Support\Facades\DB::table('settlement_bills')
|
||
->where('id', $settlementBillId)
|
||
->select(['owner_type', 'owner_id', 'counterparty_type', 'counterparty_id', 'net_amount'])
|
||
->first();
|
||
|
||
abort_if($bill === null, 404);
|
||
|
||
$actorId = self::boundAgentNodeId($admin);
|
||
abort_if($actorId === null, 403, 'agent_cannot_operate_bill');
|
||
|
||
abort_if(
|
||
! self::billOperableByBoundAgent($actorId, $bill),
|
||
403,
|
||
(string) $bill->owner_type === 'player' ? 'agent_cannot_operate_player_bill' : 'agent_cannot_operate_bill',
|
||
);
|
||
}
|
||
|
||
private static function billMatchesDirectEdgeScope(int $actorId, object $bill): bool
|
||
{
|
||
if ((string) $bill->owner_type === 'player') {
|
||
$agentNodeId = (int) (\Illuminate\Support\Facades\DB::table('players')
|
||
->where('id', (int) $bill->owner_id)
|
||
->value('agent_node_id') ?? 0);
|
||
|
||
return $agentNodeId === $actorId;
|
||
}
|
||
|
||
if ((string) $bill->owner_type === 'agent') {
|
||
return self::agentBillOnDirectEdge($actorId, $bill);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static function billOperableByBoundAgent(int $actorId, object $bill): bool
|
||
{
|
||
if ((string) $bill->owner_type === 'player') {
|
||
return (string) $bill->counterparty_type === 'agent'
|
||
&& (int) $bill->counterparty_id === $actorId;
|
||
}
|
||
|
||
if ((string) $bill->owner_type === 'agent') {
|
||
if (! self::agentBillOnDirectEdge($actorId, $bill)) {
|
||
return false;
|
||
}
|
||
|
||
[$payeeType, $payeeId] = self::billPayeeParty($bill);
|
||
|
||
return $payeeType === 'agent' && $payeeId === $actorId;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/** net>0:counterparty 为收款方;net<0:owner 为收款方。 */
|
||
private static function billPayeeParty(object $bill): array
|
||
{
|
||
if ((int) $bill->net_amount < 0) {
|
||
return [(string) $bill->owner_type, (int) $bill->owner_id];
|
||
}
|
||
|
||
return [(string) $bill->counterparty_type, (int) $bill->counterparty_id];
|
||
}
|
||
|
||
private static function agentBillOnDirectEdge(int $actorId, object $bill): bool
|
||
{
|
||
$ownerId = (int) $bill->owner_id;
|
||
$counterType = (string) $bill->counterparty_type;
|
||
$counterId = (int) $bill->counterparty_id;
|
||
|
||
if ($ownerId === $actorId) {
|
||
return true;
|
||
}
|
||
|
||
if ($counterType === 'agent' && $counterId === $actorId) {
|
||
return true;
|
||
}
|
||
|
||
return $counterType === 'platform' && $ownerId === $actorId;
|
||
}
|
||
}
|