549 lines
21 KiB
PHP
549 lines
21 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\dice\service;
|
||
|
||
use app\dice\helper\AdminScopeHelper;
|
||
use app\dice\logic\reward\DiceRewardLogic;
|
||
use app\dice\model\reward\DiceReward;
|
||
use app\dice\model\reward_config\DiceRewardConfig;
|
||
use plugin\saiadmin\app\model\system\SystemDept;
|
||
use plugin\saiadmin\app\model\system\SystemUser;
|
||
use plugin\saiadmin\exception\ApiException;
|
||
use support\think\Db;
|
||
|
||
/**
|
||
* 渠道默认配置复制、补齐与关联删除
|
||
* 默认配置:dept_id = 0(与超管「默认配置模板」一致)
|
||
*/
|
||
class DiceChannelConfigService
|
||
{
|
||
/** 需 (dept_id, id) 复合唯一的配置表 */
|
||
private const COMPOSITE_KEY_TABLES = [
|
||
'dice_config',
|
||
'dice_reward_config',
|
||
];
|
||
/** 从默认模板复制的配置表 */
|
||
private const CONFIG_TABLES = [
|
||
'dice_config',
|
||
'dice_ante_config',
|
||
'dice_lottery_pool_config',
|
||
'dice_reward_config',
|
||
'dice_game',
|
||
];
|
||
|
||
/** 复制时必须保留主键 id(非自增或固定 0-25) */
|
||
private const TABLES_KEEP_ID = [
|
||
'dice_config',
|
||
'dice_reward_config',
|
||
];
|
||
|
||
/** 可关联删除的业务表 */
|
||
private const RELATION_TABLES = [
|
||
'dice_config' => ['label' => '游戏键值配置', 'group' => 'configs'],
|
||
'dice_ante_config' => ['label' => '底注配置', 'group' => 'configs'],
|
||
'dice_lottery_pool_config' => ['label' => '彩金池配置', 'group' => 'configs'],
|
||
'dice_reward_config' => ['label' => '奖励索引配置', 'group' => 'configs'],
|
||
'dice_reward' => ['label' => '中奖概率(奖励对照)', 'group' => 'configs'],
|
||
'dice_game' => ['label' => '游戏管理', 'group' => 'configs'],
|
||
'dice_player' => ['label' => '玩家', 'group' => 'players'],
|
||
'dice_play_record' => ['label' => '抽奖记录', 'group' => 'records'],
|
||
'dice_play_record_test' => ['label' => '测试抽奖记录', 'group' => 'records'],
|
||
'dice_player_wallet_record' => ['label' => '钱包流水', 'group' => 'records'],
|
||
'dice_player_ticket_record' => ['label' => '票券记录', 'group' => 'records'],
|
||
'dice_reward_config_record' => ['label' => '权重测试记录', 'group' => 'records'],
|
||
];
|
||
|
||
/** 关联数据删除顺序:先删流水/记录,再删玩家,最后删配置 */
|
||
private const DELETE_TABLE_ORDER = [
|
||
'dice_play_record',
|
||
'dice_play_record_test',
|
||
'dice_player_wallet_record',
|
||
'dice_player_ticket_record',
|
||
'dice_reward_config_record',
|
||
'dice_player',
|
||
'dice_reward',
|
||
'dice_reward_config',
|
||
'dice_config',
|
||
'dice_ante_config',
|
||
'dice_lottery_pool_config',
|
||
'dice_game',
|
||
];
|
||
|
||
/**
|
||
* 默认模板 dept_id 统一为 0,并为固定 id 的配置表建立 (dept_id, id) 唯一约束
|
||
*/
|
||
public function ensureConfigCompositeKeys(): void
|
||
{
|
||
foreach (array_merge(self::CONFIG_TABLES, ['dice_reward']) as $table) {
|
||
if ($this->tableHasColumn($table, 'dept_id')) {
|
||
Db::table($table)->whereNull('dept_id')->update(['dept_id' => AdminScopeHelper::DEFAULT_TEMPLATE_DEPT]);
|
||
}
|
||
}
|
||
foreach (self::COMPOSITE_KEY_TABLES as $table) {
|
||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||
continue;
|
||
}
|
||
if (!$this->tableHasColumn($table, 'row_id')) {
|
||
if ($table === 'dice_reward_config') {
|
||
Db::execute(
|
||
'ALTER TABLE `dice_reward_config`'
|
||
. ' MODIFY `id` int(11) NOT NULL COMMENT \'ID\','
|
||
. ' ADD COLUMN `row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,'
|
||
. ' DROP PRIMARY KEY,'
|
||
. ' ADD PRIMARY KEY (`row_id`)'
|
||
);
|
||
} else {
|
||
Db::execute(
|
||
'ALTER TABLE `dice_config`'
|
||
. ' ADD COLUMN `row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,'
|
||
. ' DROP PRIMARY KEY,'
|
||
. ' ADD PRIMARY KEY (`row_id`)'
|
||
);
|
||
}
|
||
}
|
||
$indexes = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = 'uk_dept_config'");
|
||
if (empty($indexes)) {
|
||
Db::execute("ALTER TABLE `{$table}` ADD UNIQUE KEY `uk_dept_config` (`dept_id`, `id`)");
|
||
}
|
||
}
|
||
$this->ensureDeptScopedUniqueIndexes();
|
||
}
|
||
|
||
/**
|
||
* 将全局唯一键改为按渠道 (dept_id, 业务键) 唯一,便于复制默认模板
|
||
*/
|
||
private function ensureDeptScopedUniqueIndexes(): void
|
||
{
|
||
if ($this->tableHasColumn('dice_lottery_pool_config', 'dept_id')) {
|
||
$old = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'dice_lottery_poll_config_unique'");
|
||
if (!empty($old)) {
|
||
Db::execute('ALTER TABLE `dice_lottery_pool_config` DROP INDEX `dice_lottery_poll_config_unique`');
|
||
}
|
||
$uk = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'uk_dept_name'");
|
||
if (empty($uk)) {
|
||
Db::execute('ALTER TABLE `dice_lottery_pool_config` ADD UNIQUE KEY `uk_dept_name` (`dept_id`, `name`)');
|
||
}
|
||
}
|
||
if ($this->tableHasColumn('dice_game', 'dept_id')) {
|
||
foreach (['uk_dice_game_code', 'uk_dice_game_key'] as $idx) {
|
||
$exists = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = '{$idx}'");
|
||
if (!empty($exists)) {
|
||
Db::execute("ALTER TABLE `dice_game` DROP INDEX `{$idx}`");
|
||
}
|
||
}
|
||
$ukCode = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = 'uk_dept_game_code'");
|
||
if (empty($ukCode)) {
|
||
Db::execute('ALTER TABLE `dice_game` ADD UNIQUE KEY `uk_dept_game_code` (`dept_id`, `game_code`)');
|
||
}
|
||
$ukKey = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = 'uk_dept_game_key'");
|
||
if (empty($ukKey)) {
|
||
Db::execute('ALTER TABLE `dice_game` ADD UNIQUE KEY `uk_dept_game_key` (`dept_id`, `game_key`)');
|
||
}
|
||
}
|
||
if ($this->tableHasColumn('dice_reward', 'dept_id')) {
|
||
$old = Db::query("SHOW INDEX FROM `dice_reward` WHERE Key_name = 'uk_direction_grid_number'");
|
||
if (!empty($old)) {
|
||
Db::execute('ALTER TABLE `dice_reward` DROP INDEX `uk_direction_grid_number`');
|
||
}
|
||
$uk = Db::query("SHOW INDEX FROM `dice_reward` WHERE Key_name = 'uk_dept_direction_grid'");
|
||
if (empty($uk)) {
|
||
Db::execute('ALTER TABLE `dice_reward` ADD UNIQUE KEY `uk_dept_direction_grid` (`dept_id`, `direction`, `grid_number`)');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将当前无 dept_id 的配置标记为默认模板(仅执行一次迁移)
|
||
*/
|
||
public function markLegacyConfigAsDefault(): int
|
||
{
|
||
$this->ensureConfigCompositeKeys();
|
||
$total = 0;
|
||
foreach (self::CONFIG_TABLES as $table) {
|
||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||
continue;
|
||
}
|
||
$total += $this->countByDept($table, AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||
}
|
||
return $total;
|
||
}
|
||
|
||
/**
|
||
* 为单个渠道从默认模板复制配置(已存在则跳过)
|
||
*/
|
||
public function copyDefaultConfigToDept(int $deptId): array
|
||
{
|
||
if ($deptId <= 0) {
|
||
throw new ApiException('Invalid channel id');
|
||
}
|
||
$result = ['dept_id' => $deptId, 'copied' => [], 'skipped' => [], 'merged' => []];
|
||
foreach (self::CONFIG_TABLES as $table) {
|
||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||
continue;
|
||
}
|
||
if (in_array($table, self::TABLES_KEEP_ID, true)) {
|
||
$merged = $this->syncCompositeIdTableFromDefault($table, $deptId);
|
||
if ($merged > 0) {
|
||
$result['merged'][$table] = $merged;
|
||
} elseif ($this->countByDept($table, $deptId) > 0) {
|
||
$result['skipped'][] = $table;
|
||
}
|
||
continue;
|
||
}
|
||
if ($this->countByDept($table, $deptId) > 0) {
|
||
$result['skipped'][] = $table;
|
||
continue;
|
||
}
|
||
$rows = $this->defaultTemplateRows($table);
|
||
if (empty($rows)) {
|
||
continue;
|
||
}
|
||
foreach ($rows as $row) {
|
||
$row = (array) $row;
|
||
unset($row['id'], $row['row_id'], $row['create_time'], $row['update_time'], $row['delete_time']);
|
||
$row['dept_id'] = $deptId;
|
||
Db::table($table)->insert($row);
|
||
}
|
||
$result['copied'][] = $table;
|
||
}
|
||
$this->ensureRewardReferenceForDept($deptId);
|
||
DiceRewardConfig::refreshCache($deptId);
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 按业务 id 从默认模板补齐配置(dice_config / dice_reward_config)
|
||
*/
|
||
private function syncCompositeIdTableFromDefault(string $table, int $deptId): int
|
||
{
|
||
$templateRows = $this->defaultTemplateRows($table);
|
||
if (empty($templateRows)) {
|
||
return 0;
|
||
}
|
||
$inserted = 0;
|
||
foreach ($templateRows as $row) {
|
||
$row = (array) $row;
|
||
if (!isset($row['id'])) {
|
||
continue;
|
||
}
|
||
$businessId = $row['id'];
|
||
$exists = Db::table($table)->where('dept_id', $deptId)->where('id', $businessId)->count();
|
||
if ($exists > 0) {
|
||
continue;
|
||
}
|
||
unset($row['row_id'], $row['create_time'], $row['update_time'], $row['delete_time']);
|
||
$row['dept_id'] = $deptId;
|
||
Db::table($table)->insert($row);
|
||
$inserted++;
|
||
}
|
||
return $inserted;
|
||
}
|
||
|
||
/**
|
||
* 渠道已有奖励索引时,自动生成 dice_reward 对照表
|
||
*/
|
||
public function ensureRewardReferenceForDept(int $deptId): void
|
||
{
|
||
if ($deptId <= 0 || !$this->tableHasColumn('dice_reward', 'dept_id')) {
|
||
return;
|
||
}
|
||
if ($this->countByDept('dice_reward_config', $deptId) <= 0) {
|
||
return;
|
||
}
|
||
if ($this->countByDept('dice_reward', $deptId) > 0) {
|
||
return;
|
||
}
|
||
$logic = new DiceRewardLogic();
|
||
$logic->createRewardReferenceFromConfig($deptId);
|
||
}
|
||
|
||
/**
|
||
* 复制默认 dice_reward 到渠道
|
||
*/
|
||
public function copyDefaultRewardsToDept(int $deptId): void
|
||
{
|
||
$this->ensureRewardReferenceForDept($deptId);
|
||
if (!$this->tableHasColumn('dice_reward', 'dept_id')) {
|
||
return;
|
||
}
|
||
if ($this->countByDept('dice_reward', $deptId) > 0) {
|
||
return;
|
||
}
|
||
if ($this->countByDept('dice_reward_config', $deptId) > 0) {
|
||
return;
|
||
}
|
||
$rows = $this->defaultTemplateRows('dice_reward');
|
||
foreach ($rows as $row) {
|
||
$row = (array) $row;
|
||
unset($row['id'], $row['row_id']);
|
||
unset($row['create_time'], $row['update_time'], $row['delete_time']);
|
||
$row['dept_id'] = $deptId;
|
||
Db::table('dice_reward')->insert($row);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 为所有已有渠道补齐缺失配置
|
||
*/
|
||
public function syncAllChannelsFromDefault(): array
|
||
{
|
||
$deptIds = SystemDept::column('id');
|
||
$summary = [];
|
||
foreach ($deptIds as $deptId) {
|
||
$deptId = (int) $deptId;
|
||
if ($deptId <= 0) {
|
||
continue;
|
||
}
|
||
$summary[$deptId] = $this->copyDefaultConfigToDept($deptId);
|
||
}
|
||
return $summary;
|
||
}
|
||
|
||
/**
|
||
* 修复已删除渠道 ID、无管理员关联的遗留数据,归并到首个顶级渠道
|
||
*/
|
||
public function repairOrphanDeptReferences(): array
|
||
{
|
||
$validDeptIds = array_map('intval', SystemDept::column('id') ?: []);
|
||
if (empty($validDeptIds)) {
|
||
return [];
|
||
}
|
||
$rootDeptId = min($validDeptIds);
|
||
$stats = [];
|
||
$inList = implode(',', $validDeptIds);
|
||
|
||
$stats['sa_system_user'] = Db::execute(
|
||
"UPDATE sa_system_user SET dept_id = {$rootDeptId}
|
||
WHERE dept_id IS NOT NULL AND dept_id > 0 AND dept_id NOT IN ({$inList})"
|
||
);
|
||
|
||
$bizTables = [
|
||
'dice_player',
|
||
'dice_play_record',
|
||
'dice_play_record_test',
|
||
'dice_player_wallet_record',
|
||
'dice_player_ticket_record',
|
||
'dice_reward_config_record',
|
||
];
|
||
foreach ($bizTables as $table) {
|
||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||
continue;
|
||
}
|
||
$stats[$table . '_invalid_dept'] = Db::execute(
|
||
"UPDATE `{$table}` SET dept_id = {$rootDeptId}
|
||
WHERE dept_id IS NOT NULL AND dept_id > 0 AND dept_id NOT IN ({$inList})"
|
||
);
|
||
}
|
||
|
||
return $stats;
|
||
}
|
||
|
||
/**
|
||
* 根据管理员/玩家回填 dept_id
|
||
*/
|
||
public function backfillDataDeptId(): array
|
||
{
|
||
$stats = $this->repairOrphanDeptReferences();
|
||
if ($this->tableHasColumn('dice_player', 'dept_id') && $this->tableHasColumn('dice_player', 'admin_id')) {
|
||
$stats['dice_player'] = Db::execute(
|
||
'UPDATE dice_player p INNER JOIN sa_system_user u ON p.admin_id = u.id
|
||
SET p.dept_id = u.dept_id WHERE (p.dept_id IS NULL OR p.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0'
|
||
);
|
||
}
|
||
$validDeptIds = SystemDept::column('id') ?: [];
|
||
if (!empty($validDeptIds) && $this->tableHasColumn('dice_player', 'dept_id')) {
|
||
$rootDeptId = (int) min($validDeptIds);
|
||
$stats['dice_player_legacy'] = Db::table('dice_player')
|
||
->where(function ($q) {
|
||
$q->whereNull('dept_id')->whereOr('dept_id', 0);
|
||
})
|
||
->update(['dept_id' => $rootDeptId]);
|
||
}
|
||
$stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_play_record'));
|
||
$stats = array_merge($stats, $this->backfillRecordDeptIdByAdmin('dice_play_record'));
|
||
$stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_player_wallet_record'));
|
||
$stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_player_ticket_record'));
|
||
$stats = array_merge($stats, $this->backfillRecordDeptIdByAdmin('dice_play_record_test'));
|
||
if (!empty($validDeptIds) && $this->tableHasColumn('dice_play_record_test', 'dept_id')) {
|
||
$rootDeptId = (int) min($validDeptIds);
|
||
$stats['dice_play_record_test_legacy'] = Db::table('dice_play_record_test')
|
||
->where(function ($q) {
|
||
$q->whereNull('dept_id')->whereOr('dept_id', 0);
|
||
})
|
||
->update(['dept_id' => $rootDeptId]);
|
||
}
|
||
if ($this->tableHasColumn('dice_reward_config_record', 'dept_id')) {
|
||
$stats['dice_reward_config_record'] = Db::execute(
|
||
'UPDATE dice_reward_config_record r INNER JOIN sa_system_user u ON r.admin_id = u.id
|
||
SET r.dept_id = u.dept_id WHERE (r.dept_id IS NULL OR r.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0'
|
||
);
|
||
}
|
||
return $stats;
|
||
}
|
||
|
||
/**
|
||
* 删除渠道前关联数据统计
|
||
*/
|
||
public function getDestroyPreview(array $deptIds): array
|
||
{
|
||
$items = [];
|
||
foreach ($deptIds as $deptId) {
|
||
$deptId = (int) $deptId;
|
||
if ($deptId <= 0) {
|
||
continue;
|
||
}
|
||
$dept = SystemDept::find($deptId);
|
||
$row = [
|
||
'dept_id' => $deptId,
|
||
'dept_name' => $dept ? $dept->name : '',
|
||
'user_count' => SystemUser::where('dept_id', $deptId)->count(),
|
||
'relations' => [],
|
||
];
|
||
foreach (self::RELATION_TABLES as $table => $meta) {
|
||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||
continue;
|
||
}
|
||
$count = $this->countByDept($table, $deptId);
|
||
if ($count > 0) {
|
||
$row['relations'][] = [
|
||
'table' => $table,
|
||
'label' => $meta['label'],
|
||
'group' => $meta['group'],
|
||
'count' => $count,
|
||
];
|
||
}
|
||
}
|
||
$items[] = $row;
|
||
}
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* 删除渠道及勾选的关联数据
|
||
*
|
||
* @param array $deleteTables 要删除的表名列表
|
||
*/
|
||
public function destroyDeptWithRelations(int $deptId, array $deleteTables): void
|
||
{
|
||
if ($deptId <= 0) {
|
||
throw new ApiException('Invalid channel id');
|
||
}
|
||
$userCount = SystemUser::where('dept_id', $deptId)->count();
|
||
if ($userCount > 0) {
|
||
throw new ApiException('This channel has users, please delete or transfer them first');
|
||
}
|
||
$tablesToDelete = $this->sortTablesForDelete($deleteTables);
|
||
Db::startTrans();
|
||
try {
|
||
foreach ($tablesToDelete as $table) {
|
||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||
continue;
|
||
}
|
||
Db::table($table)->where('dept_id', $deptId)->delete();
|
||
}
|
||
Db::name('sa_system_role_dept')->where('dept_id', $deptId)->delete();
|
||
SystemDept::destroy($deptId, true);
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
throw new ApiException('Channel delete failed: ' . $e->getMessage());
|
||
}
|
||
DiceRewardConfig::refreshCache($deptId);
|
||
DiceReward::refreshCache($deptId);
|
||
}
|
||
|
||
/**
|
||
* 按依赖顺序排列待删表(勾选顺序无关)
|
||
*
|
||
* @param array<int, string> $deleteTables
|
||
* @return array<int, string>
|
||
*/
|
||
private function sortTablesForDelete(array $deleteTables): array
|
||
{
|
||
$allowed = array_keys(self::RELATION_TABLES);
|
||
$picked = [];
|
||
foreach ($deleteTables as $table) {
|
||
if (is_string($table) && in_array($table, $allowed, true)) {
|
||
$picked[$table] = true;
|
||
}
|
||
}
|
||
$ordered = [];
|
||
foreach (self::DELETE_TABLE_ORDER as $table) {
|
||
if (isset($picked[$table])) {
|
||
$ordered[] = $table;
|
||
unset($picked[$table]);
|
||
}
|
||
}
|
||
foreach (array_keys($picked) as $table) {
|
||
$ordered[] = $table;
|
||
}
|
||
return $ordered;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
private function defaultTemplateRows(string $table): array
|
||
{
|
||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||
$rows = Db::table($table)->where('dept_id', $templateId)->select()->toArray();
|
||
if (!empty($rows)) {
|
||
return $rows;
|
||
}
|
||
return Db::table($table)->whereNull('dept_id')->select()->toArray();
|
||
}
|
||
|
||
private function backfillRecordDeptIdByPlayer(string $table): array
|
||
{
|
||
if (!$this->tableHasColumn($table, 'dept_id') || !$this->tableHasColumn($table, 'player_id')) {
|
||
return [];
|
||
}
|
||
return [
|
||
$table => Db::execute(
|
||
"UPDATE `{$table}` r INNER JOIN dice_player p ON r.player_id = p.id
|
||
SET r.dept_id = p.dept_id WHERE (r.dept_id IS NULL OR r.dept_id = 0) AND p.dept_id IS NOT NULL AND p.dept_id > 0"
|
||
),
|
||
];
|
||
}
|
||
|
||
private function backfillRecordDeptIdByAdmin(string $table): array
|
||
{
|
||
if (!$this->tableHasColumn($table, 'dept_id') || !$this->tableHasColumn($table, 'admin_id')) {
|
||
return [];
|
||
}
|
||
return [
|
||
$table => Db::execute(
|
||
"UPDATE `{$table}` r INNER JOIN sa_system_user u ON r.admin_id = u.id
|
||
SET r.dept_id = u.dept_id WHERE (r.dept_id IS NULL OR r.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0"
|
||
),
|
||
];
|
||
}
|
||
|
||
private function countByDept(string $table, ?int $deptId): int
|
||
{
|
||
$query = Db::table($table);
|
||
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||
$query->where(function ($q) use ($templateId) {
|
||
$q->where('dept_id', $templateId)->whereOr(function ($sub) {
|
||
$sub->whereNull('dept_id');
|
||
});
|
||
});
|
||
} else {
|
||
$query->where('dept_id', $deptId);
|
||
}
|
||
return $query->count();
|
||
}
|
||
|
||
private function tableHasColumn(string $table, string $column): bool
|
||
{
|
||
try {
|
||
$fields = Db::getFields($table);
|
||
return isset($fields[$column]);
|
||
} catch (\Throwable $e) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|