Files
lotteryLaravel/app/Support/AdminAgentSettlementScope.php
kang 5b6d4cb74d feat: enhance agent management and validation logic
- Updated AGENTS.md to clarify agent account restrictions and permissions.
- Implemented checks in AgentNodeAdminUserStoreController and AgentNodeRoleStoreController to restrict admin user and role creation to the agent's own node.
- Enhanced validation in AdminPlayerStoreController and AdminPlayerUpdateController to enforce credit limit and rebate rate rules based on player funding mode.
- Refactored various request classes to utilize shared admin account field rules for consistency.
- Improved error handling in services related to credit allocation and rebate limits to ensure proper validation and messaging.
2026-06-14 21:13:27 +08:00

364 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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, 'admin.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, 'admin.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, 'admin.agent_cannot_operate_bill');
abort_if(
! self::billOperableByBoundAgent($actorId, $bill),
403,
(string) $bill->owner_type === 'player'
? 'admin.agent_cannot_operate_player_bill'
: 'admin.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>0counterparty 为收款方net<0owner 为收款方。 */
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;
}
}