1.新增充值档位配置
2.新增充值/提现配置
This commit is contained in:
@@ -43,3 +43,7 @@ AUTH_TOKEN_SECRET = 564d14asdasd113e46542asd6das1a2a
|
||||
# Webman Push:浏览器实际连接的 WS 基址(不含 /app/)。HTTPS 后台须用 wss://,与 Nginx 反代 3131 一致
|
||||
# 示例:PUSH_WEBSOCKET_CLIENT_URL = wss://zihua-api.yuliao666.top
|
||||
PUSH_WEBSOCKET_CLIENT_URL =
|
||||
|
||||
# 充值支付渠道:在代码注册表之外追加渠道(JSON 数组,每项含 code / name / name_en / sort)
|
||||
# 示例:DEPOSIT_CHANNELS_REGISTRY_JSON = [{"code":"bank_a","name":"银行转账A","name_en":"Bank A","sort":20}]
|
||||
DEPOSIT_CHANNELS_REGISTRY_JSON =
|
||||
|
||||
421
app/admin/controller/config/DepositChannel.php
Normal file
421
app/admin/controller/config/DepositChannel.php
Normal file
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\config;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\game\DepositChannel as DepositChannelLib;
|
||||
use app\common\library\game\DepositTier as DepositTierLib;
|
||||
use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib;
|
||||
use app\common\service\GameHotDataCoordinator;
|
||||
use app\common\service\GameHotDataLock;
|
||||
use InvalidArgumentException;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 充值支付渠道(game_config.deposit_channel)
|
||||
*/
|
||||
class DepositChannel extends Backend
|
||||
{
|
||||
protected bool $modelValidate = false;
|
||||
|
||||
protected array $noNeedPermission = ['index', 'save'];
|
||||
|
||||
private function hasNodePermission(WebmanRequest $request, string $action): bool
|
||||
{
|
||||
if (!$this->auth) {
|
||||
return false;
|
||||
}
|
||||
$controllerPath = get_controller_path($request);
|
||||
if (!$controllerPath) {
|
||||
return false;
|
||||
}
|
||||
$paths = [];
|
||||
$paths[] = $controllerPath . '/' . $action;
|
||||
$parts = explode('/', $controllerPath);
|
||||
foreach ($parts as &$part) {
|
||||
if (str_contains($part, '_')) {
|
||||
$part = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $part))));
|
||||
}
|
||||
}
|
||||
$paths[] = implode('/', $parts) . '/' . $action;
|
||||
foreach (array_values(array_unique($paths)) as $path) {
|
||||
if ($this->auth->check($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表(baTable:list / total / remark)+ registry + tier_options(弹窗用)
|
||||
*/
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'index')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() !== 'GET') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$tierOptions = $this->buildTierOptions();
|
||||
$registryOut = $this->buildRegistryOut();
|
||||
|
||||
$parsed = DepositChannelLib::parseStoredOverridesFromDb();
|
||||
$items = DepositChannelLib::expandRowsForAdmin($parsed);
|
||||
|
||||
$quickSearch = $request->get('quickSearch', '');
|
||||
if (is_string($quickSearch) && trim($quickSearch) !== '') {
|
||||
$q = mb_strtolower(trim($quickSearch));
|
||||
$items = array_values(array_filter($items, static function (array $it) use ($q, $registryOut): bool {
|
||||
$code = isset($it['code']) && is_string($it['code']) ? mb_strtolower($it['code']) : '';
|
||||
$name = '';
|
||||
if (isset($it['code']) && is_string($it['code']) && isset($registryOut[$it['code']])) {
|
||||
$meta = $registryOut[$it['code']];
|
||||
$name = isset($meta['name']) && is_string($meta['name']) ? mb_strtolower($meta['name']) : '';
|
||||
}
|
||||
|
||||
return $q === '' || str_contains($code, $q) || str_contains($name, $q);
|
||||
}));
|
||||
}
|
||||
|
||||
$orderRaw = $request->get('order', '');
|
||||
if (is_string($orderRaw) && str_contains($orderRaw, ',')) {
|
||||
$parts = explode(',', $orderRaw, 2);
|
||||
$field = trim($parts[0]);
|
||||
$dir = strtolower(trim($parts[1] ?? 'asc'));
|
||||
if ($field === 'sort') {
|
||||
usort($items, static function (array $a, array $b) use ($dir): int {
|
||||
$sa = isset($a['sort']) && is_numeric($a['sort']) ? intval($a['sort']) : 0;
|
||||
$sb = isset($b['sort']) && is_numeric($b['sort']) ? intval($b['sort']) : 0;
|
||||
if ($sa !== $sb) {
|
||||
return $dir === 'desc' ? ($sb <=> $sa) : ($sa <=> $sb);
|
||||
}
|
||||
$ca = isset($a['code']) && is_string($a['code']) ? $a['code'] : '';
|
||||
$cb = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';
|
||||
|
||||
return strcmp($ca, $cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$pageRaw = $request->get('page', '1');
|
||||
$limitRaw = $request->get('limit', '20');
|
||||
$page = is_numeric($pageRaw) ? max(1, intval($pageRaw)) : 1;
|
||||
$limit = is_numeric($limitRaw) ? max(1, min(500, intval($limitRaw))) : 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
$pageRows = array_slice($items, $offset, $limit);
|
||||
|
||||
foreach ($pageRows as &$pr) {
|
||||
if (!is_array($pr)) {
|
||||
continue;
|
||||
}
|
||||
$code = isset($pr['code']) && is_string($pr['code']) ? $pr['code'] : '';
|
||||
$meta = $code !== '' && isset($registryOut[$code]) ? $registryOut[$code] : null;
|
||||
$pr['display_name'] = is_array($meta) && isset($meta['name']) && is_string($meta['name']) ? $meta['name'] : $code;
|
||||
$pr['name_en'] = is_array($meta) && isset($meta['name_en']) && is_string($meta['name_en']) ? $meta['name_en'] : '';
|
||||
}
|
||||
unset($pr);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $pageRows,
|
||||
'total' => $total,
|
||||
'remark' => '',
|
||||
'registry' => $registryOut,
|
||||
'tier_options' => $tierOptions,
|
||||
'items' => $pageRows,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条读取 / 单条更新(弹窗)
|
||||
*/
|
||||
public function edit(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'edit')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() === 'GET') {
|
||||
$code = $request->get('code', '');
|
||||
if (!is_string($code) || trim($code) === '') {
|
||||
$code = $request->get('id', '');
|
||||
}
|
||||
if (!is_string($code) || trim($code) === '') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$code = strtolower(trim($code));
|
||||
$parsed = DepositChannelLib::parseStoredOverridesFromDb();
|
||||
$items = DepositChannelLib::expandRowsForAdmin($parsed);
|
||||
$found = null;
|
||||
foreach ($items as $it) {
|
||||
if (!is_array($it)) {
|
||||
continue;
|
||||
}
|
||||
$c = isset($it['code']) && is_string($it['code']) ? strtolower(trim($it['code'])) : '';
|
||||
if ($c === $code) {
|
||||
$found = $it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found === null) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
return $this->success('', ['row' => $found]);
|
||||
}
|
||||
if ($request->method() === 'POST') {
|
||||
$payload = $request->post();
|
||||
if (!is_array($payload)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$code = isset($payload['code']) && is_string($payload['code']) ? strtolower(trim($payload['code'])) : '';
|
||||
if ($code === '') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$parsed = DepositChannelLib::parseStoredOverridesFromDb();
|
||||
$items = DepositChannelLib::expandRowsForAdmin($parsed);
|
||||
$foundIdx = -1;
|
||||
$foundRow = null;
|
||||
foreach ($items as $k => $it) {
|
||||
if (!is_array($it)) {
|
||||
continue;
|
||||
}
|
||||
$c = isset($it['code']) && is_string($it['code']) ? strtolower(trim($it['code'])) : '';
|
||||
if ($c === $code) {
|
||||
$foundIdx = $k;
|
||||
$foundRow = $it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($foundIdx < 0 || $foundRow === null) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$items[$foundIdx] = $this->mergeDepositChannelEditPayload($foundRow, $payload, $code);
|
||||
|
||||
return $this->persistChannelList($items);
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function save(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'save')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$payload = $request->post();
|
||||
if (!is_array($payload)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$items = $payload['items'] ?? null;
|
||||
if (!is_array($items)) {
|
||||
return $this->error('items 必须为数组');
|
||||
}
|
||||
|
||||
return $this->persistChannelList($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $items
|
||||
*/
|
||||
private function persistChannelList(array $items): Response
|
||||
{
|
||||
try {
|
||||
$registry = DepositChannelLib::codeRegistry();
|
||||
$clean = DepositChannelLib::prepareOverridesForSave(array_values($items));
|
||||
$expectedCodes = array_keys($registry);
|
||||
sort($expectedCodes);
|
||||
$got = array_column($clean, 'code');
|
||||
sort($got);
|
||||
if ($expectedCodes !== $got) {
|
||||
return $this->error('请保存全部已注册渠道行(不可缺行)');
|
||||
}
|
||||
$json = DepositChannelLib::encodeForDb($clean);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$resourceKey = GameHotDataLock::safeResourceKeyForConfig(DepositChannelLib::CONFIG_KEY);
|
||||
$lock = GameHotDataLock::tryAcquire(GameHotDataLock::TYPE_GAME_CONFIG, $resourceKey);
|
||||
if (!$lock['acquired']) {
|
||||
return $this->error('该配置正在被其他操作占用,请稍后再试');
|
||||
}
|
||||
try {
|
||||
try {
|
||||
$exists = Db::name('game_config')->where('config_key', DepositChannelLib::CONFIG_KEY)->find();
|
||||
if ($exists) {
|
||||
Db::name('game_config')->where('config_key', DepositChannelLib::CONFIG_KEY)->update([
|
||||
'config_value' => $json,
|
||||
'value_type' => 'json',
|
||||
'update_time' => $now,
|
||||
]);
|
||||
} else {
|
||||
Db::name('game_config')->insert([
|
||||
'config_key' => DepositChannelLib::CONFIG_KEY,
|
||||
'config_value' => $json,
|
||||
'value_type' => 'json',
|
||||
'remark' => '充值支付渠道 JSON(与 finance_cashier.channels 同步)',
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
$fcExists = Db::name('game_config')->where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->find();
|
||||
if ($fcExists) {
|
||||
$fcPayload = FinanceCashierConfigLib::parseFromConfigValue($fcExists['config_value'] ?? null);
|
||||
$fcPayload['channels'] = $clean;
|
||||
$fcJson = FinanceCashierConfigLib::encodeForDb($fcPayload);
|
||||
Db::name('game_config')->where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->update([
|
||||
'config_value' => $fcJson,
|
||||
'value_type' => 'json',
|
||||
'update_time' => $now,
|
||||
]);
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(FinanceCashierConfigLib::CONFIG_KEY);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(DepositChannelLib::CONFIG_KEY);
|
||||
|
||||
return $this->success(__('Saved successfully'));
|
||||
} finally {
|
||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_CONFIG, $resourceKey, $lock['token'], $lock['redis_lock']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: string, label: string}>
|
||||
*/
|
||||
private function buildTierOptions(): array
|
||||
{
|
||||
$tierRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$allTiers = DepositTierLib::parseFromConfigValue($tierRow['config_value'] ?? null);
|
||||
$tierOptions = [];
|
||||
foreach ($allTiers as $t) {
|
||||
if (!is_array($t)) {
|
||||
continue;
|
||||
}
|
||||
$tid = isset($t['id']) && is_string($t['id']) ? $t['id'] : '';
|
||||
if ($tid === '') {
|
||||
continue;
|
||||
}
|
||||
$title = isset($t['title']) && is_string($t['title']) ? trim($t['title']) : '';
|
||||
$tierOptions[] = [
|
||||
'id' => $tid,
|
||||
'label' => $title !== '' ? $title . ' (' . $tid . ')' : $tid,
|
||||
];
|
||||
}
|
||||
|
||||
return $tierOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{name: string, name_en: string, sort: int}>
|
||||
*/
|
||||
private function buildRegistryOut(): array
|
||||
{
|
||||
$registry = DepositChannelLib::codeRegistry();
|
||||
$registryOut = [];
|
||||
foreach ($registry as $code => $meta) {
|
||||
if (!is_array($meta)) {
|
||||
continue;
|
||||
}
|
||||
$registryOut[$code] = [
|
||||
'name' => isset($meta['name']) && is_string($meta['name']) ? $meta['name'] : '',
|
||||
'name_en' => isset($meta['name_en']) && is_string($meta['name_en']) ? $meta['name_en'] : '',
|
||||
'sort' => isset($meta['sort']) && is_numeric($meta['sort']) ? intval($meta['sort']) : 10,
|
||||
];
|
||||
}
|
||||
|
||||
return $registryOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗整表提交与列表内开关均走此合并:未出现在 payload 中的字段沿用 $current。
|
||||
*
|
||||
* @param array<string, mixed> $current
|
||||
* @param array<string, mixed> $payload
|
||||
*
|
||||
* @return array{code: string, sort: int, status: int, tier_ids: list<string>}
|
||||
*/
|
||||
private function mergeDepositChannelEditPayload(array $current, array $payload, string $code): array
|
||||
{
|
||||
$form = [];
|
||||
if (array_key_exists('sort', $payload)) {
|
||||
$form['sort'] = $payload['sort'];
|
||||
} else {
|
||||
$form['sort'] = $current['sort'] ?? 0;
|
||||
}
|
||||
if (array_key_exists('status', $payload)) {
|
||||
$form['status'] = $payload['status'];
|
||||
} else {
|
||||
$form['status'] = $current['status'] ?? 0;
|
||||
}
|
||||
if (array_key_exists('tier_ids', $payload) && is_array($payload['tier_ids'])) {
|
||||
$form['tier_ids'] = $payload['tier_ids'];
|
||||
} else {
|
||||
$form['tier_ids'] = isset($current['tier_ids']) && is_array($current['tier_ids']) ? $current['tier_ids'] : [];
|
||||
}
|
||||
|
||||
return $this->normalizeChannelFormRow($form, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*
|
||||
* @return array{code: string, sort: int, status: int, tier_ids: list<string>}
|
||||
*/
|
||||
private function normalizeChannelFormRow(array $payload, string $code): array
|
||||
{
|
||||
$sort = isset($payload['sort']) && is_numeric($payload['sort']) ? intval($payload['sort']) : 0;
|
||||
$status = 0;
|
||||
$st = $payload['status'] ?? 1;
|
||||
if ($st === true || $st === 1 || $st === '1') {
|
||||
$status = 1;
|
||||
}
|
||||
$tierIds = [];
|
||||
if (isset($payload['tier_ids']) && is_array($payload['tier_ids'])) {
|
||||
foreach ($payload['tier_ids'] as $tid) {
|
||||
if (is_string($tid)) {
|
||||
$t = trim($tid);
|
||||
if ($t !== '' && preg_match('/^[a-zA-Z0-9_\-]{1,32}$/', $t)) {
|
||||
$tierIds[] = $t;
|
||||
}
|
||||
}
|
||||
}
|
||||
$tierIds = array_values(array_unique($tierIds));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'sort' => $sort,
|
||||
'status' => $status,
|
||||
'tier_ids' => $tierIds,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class DepositTier extends Backend
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ class DepositTier extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 game_config.deposit_tier 的档位列表
|
||||
* 列表(baTable:list / total / remark),支持 quickSearch、分页
|
||||
*/
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
@@ -69,13 +70,219 @@ class DepositTier extends Backend
|
||||
}
|
||||
$row = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$items = DepositTierLib::parseFromConfigValue($row['config_value'] ?? null);
|
||||
|
||||
$quickSearch = $request->get('quickSearch', '');
|
||||
if (is_string($quickSearch) && trim($quickSearch) !== '') {
|
||||
$q = mb_strtolower(trim($quickSearch));
|
||||
$items = array_values(array_filter($items, static function (array $it) use ($q): bool {
|
||||
$id = isset($it['id']) && is_string($it['id']) ? mb_strtolower($it['id']) : '';
|
||||
$t = isset($it['title']) && is_string($it['title']) ? mb_strtolower($it['title']) : '';
|
||||
$te = isset($it['title_en']) && is_string($it['title_en']) ? mb_strtolower($it['title_en']) : '';
|
||||
|
||||
return $q === '' || str_contains($id, $q) || str_contains($t, $q) || str_contains($te, $q);
|
||||
}));
|
||||
}
|
||||
|
||||
$orderRaw = $request->get('order', '');
|
||||
if (is_string($orderRaw) && str_contains($orderRaw, ',')) {
|
||||
$parts = explode(',', $orderRaw, 2);
|
||||
$field = trim($parts[0]);
|
||||
$dir = strtolower(trim($parts[1] ?? 'asc'));
|
||||
if ($field === 'sort') {
|
||||
usort($items, static function (array $a, array $b) use ($dir): int {
|
||||
$sa = isset($a['sort']) && is_numeric($a['sort']) ? intval($a['sort']) : 0;
|
||||
$sb = isset($b['sort']) && is_numeric($b['sort']) ? intval($b['sort']) : 0;
|
||||
if ($sa !== $sb) {
|
||||
return $dir === 'desc' ? ($sb <=> $sa) : ($sa <=> $sb);
|
||||
}
|
||||
$ida = isset($a['id']) && is_string($a['id']) ? $a['id'] : '';
|
||||
$idb = isset($b['id']) && is_string($b['id']) ? $b['id'] : '';
|
||||
|
||||
return strcmp($ida, $idb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$pageRaw = $request->get('page', '1');
|
||||
$limitRaw = $request->get('limit', '20');
|
||||
$page = is_numeric($pageRaw) ? max(1, intval($pageRaw)) : 1;
|
||||
$limit = is_numeric($limitRaw) ? max(1, min(500, intval($limitRaw))) : 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
$pageRows = array_slice($items, $offset, $limit);
|
||||
|
||||
return $this->success('', [
|
||||
'items' => $items,
|
||||
'list' => $pageRows,
|
||||
'total' => $total,
|
||||
'remark' => '',
|
||||
'items' => $pageRows,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 JSON 数组(value_type=json)
|
||||
* 单条读取(弹窗编辑)
|
||||
*/
|
||||
public function edit(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'edit')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() === 'GET') {
|
||||
$id = $request->get('id', '');
|
||||
if (!is_string($id) || trim($id) === '') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$row = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$items = DepositTierLib::parseFromConfigValue($row['config_value'] ?? null);
|
||||
$found = null;
|
||||
foreach ($items as $it) {
|
||||
if (!is_array($it)) {
|
||||
continue;
|
||||
}
|
||||
$rid = $it['id'] ?? '';
|
||||
if (is_string($rid) && $rid === $id) {
|
||||
$found = $it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found === null) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
return $this->success('', ['row' => $found]);
|
||||
}
|
||||
if ($request->method() === 'POST') {
|
||||
if (!$this->hasNodePermission($request, 'edit')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
$payload = $request->post();
|
||||
if (!is_array($payload)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$id = isset($payload['id']) && is_string($payload['id']) ? trim($payload['id']) : '';
|
||||
if ($id === '') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$cfgRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$items = DepositTierLib::parseFromConfigValue($cfgRow['config_value'] ?? null);
|
||||
$foundIdx = -1;
|
||||
$foundRow = null;
|
||||
foreach ($items as $k => $it) {
|
||||
if (!is_array($it)) {
|
||||
continue;
|
||||
}
|
||||
$rid = $it['id'] ?? '';
|
||||
if (is_string($rid) && $rid === $id) {
|
||||
$foundIdx = $k;
|
||||
$foundRow = $it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($foundIdx < 0 || $foundRow === null) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
$items[$foundIdx] = $this->mergeDepositTierEditPayload($foundRow, $payload, $id);
|
||||
|
||||
return $this->persistTierList($items);
|
||||
}
|
||||
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增一条档位
|
||||
*/
|
||||
public function add(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'add')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$payload = $request->post();
|
||||
if (!is_array($payload)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$cfgRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$items = DepositTierLib::parseFromConfigValue($cfgRow['config_value'] ?? null);
|
||||
$newId = isset($payload['id']) && is_string($payload['id']) ? trim($payload['id']) : '';
|
||||
if ($newId === '') {
|
||||
$newId = DepositTierLib::generateId();
|
||||
}
|
||||
foreach ($items as $it) {
|
||||
if (!is_array($it)) {
|
||||
continue;
|
||||
}
|
||||
$rid = $it['id'] ?? '';
|
||||
if (is_string($rid) && $rid === $newId) {
|
||||
return $this->error('档位 ID 已存在');
|
||||
}
|
||||
}
|
||||
$items[] = $this->normalizeFormRow($payload, $newId);
|
||||
|
||||
return $this->persistTierList($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除(支持批量 ids)
|
||||
*/
|
||||
public function del(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'del')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() !== 'DELETE') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$ids = $request->get('ids', []);
|
||||
if (!is_array($ids)) {
|
||||
if (is_string($ids) && $ids !== '') {
|
||||
$ids = [$ids];
|
||||
} else {
|
||||
$ids = [];
|
||||
}
|
||||
}
|
||||
$idSet = [];
|
||||
foreach ($ids as $id) {
|
||||
if (is_string($id) && trim($id) !== '') {
|
||||
$idSet[trim($id)] = true;
|
||||
}
|
||||
}
|
||||
if ($idSet === []) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$cfgRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$items = DepositTierLib::parseFromConfigValue($cfgRow['config_value'] ?? null);
|
||||
$filtered = [];
|
||||
foreach ($items as $it) {
|
||||
if (!is_array($it)) {
|
||||
continue;
|
||||
}
|
||||
$rid = isset($it['id']) && is_string($it['id']) ? $it['id'] : '';
|
||||
if ($rid !== '' && isset($idSet[$rid])) {
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $it;
|
||||
}
|
||||
|
||||
return $this->persistTierList($filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 整表保存 JSON 数组(兼容旧版批量提交)
|
||||
*/
|
||||
public function save(WebmanRequest $request): Response
|
||||
{
|
||||
@@ -97,6 +304,15 @@ class DepositTier extends Backend
|
||||
if (!is_array($items)) {
|
||||
return $this->error('items 必须为数组');
|
||||
}
|
||||
|
||||
return $this->persistTierList($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $items
|
||||
*/
|
||||
private function persistTierList(array $items): Response
|
||||
{
|
||||
try {
|
||||
$clean = DepositTierLib::prepareItemsForSave(array_values($items));
|
||||
$json = DepositTierLib::encodeForDb($clean);
|
||||
@@ -132,7 +348,6 @@ class DepositTier extends Backend
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(DepositTierLib::CONFIG_KEY);
|
||||
|
||||
return $this->success(__('Saved successfully'));
|
||||
@@ -140,4 +355,67 @@ class DepositTier extends Backend
|
||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_CONFIG, $resourceKey, $lock['token'], $lock['redis_lock']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗整表提交与列表内开关:未出现在 payload 中的字段沿用 $current。
|
||||
*
|
||||
* @param array<string, mixed> $current
|
||||
* @param array<string, mixed> $payload
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function mergeDepositTierEditPayload(array $current, array $payload, string $id): array
|
||||
{
|
||||
$merged = [];
|
||||
$merged['id'] = $id;
|
||||
if (array_key_exists('sort', $payload)) {
|
||||
$merged['sort'] = $payload['sort'];
|
||||
} else {
|
||||
$merged['sort'] = $current['sort'] ?? 0;
|
||||
}
|
||||
if (array_key_exists('status', $payload)) {
|
||||
$merged['status'] = $payload['status'];
|
||||
} else {
|
||||
$merged['status'] = $current['status'] ?? 0;
|
||||
}
|
||||
$stringKeys = ['title', 'title_en', 'currency', 'pay_amount', 'amount', 'bonus_amount', 'desc', 'desc_en'];
|
||||
foreach ($stringKeys as $key) {
|
||||
if (array_key_exists($key, $payload)) {
|
||||
$merged[$key] = $payload[$key];
|
||||
} else {
|
||||
$merged[$key] = $current[$key] ?? ($key === 'bonus_amount' ? '0' : '');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->normalizeFormRow($merged, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function normalizeFormRow(array $payload, string $id): array
|
||||
{
|
||||
$sort = isset($payload['sort']) && is_numeric($payload['sort']) ? intval($payload['sort']) : 0;
|
||||
$status = 0;
|
||||
$st = $payload['status'] ?? 1;
|
||||
if ($st === true || $st === 1 || $st === '1') {
|
||||
$status = 1;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $id,
|
||||
'title' => isset($payload['title']) && is_string($payload['title']) ? $payload['title'] : '',
|
||||
'title_en' => isset($payload['title_en']) && is_string($payload['title_en']) ? $payload['title_en'] : '',
|
||||
'currency' => isset($payload['currency']) && is_string($payload['currency']) ? $payload['currency'] : '',
|
||||
'pay_amount' => $payload['pay_amount'] ?? '',
|
||||
'amount' => $payload['amount'] ?? '',
|
||||
'bonus_amount' => $payload['bonus_amount'] ?? '0',
|
||||
'desc' => isset($payload['desc']) && is_string($payload['desc']) ? $payload['desc'] : '',
|
||||
'desc_en' => isset($payload['desc_en']) && is_string($payload['desc_en']) ? $payload['desc_en'] : '',
|
||||
'sort' => $sort,
|
||||
'status' => $status,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
225
app/admin/controller/config/FinanceCashierConfig.php
Normal file
225
app/admin/controller/config/FinanceCashierConfig.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\config;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\game\DepositChannel as DepositChannelLib;
|
||||
use app\common\library\game\DepositTier as DepositTierLib;
|
||||
use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib;
|
||||
use app\common\service\GameHotDataCoordinator;
|
||||
use app\common\service\GameHotDataLock;
|
||||
use InvalidArgumentException;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 支付/收款配置(game_config.finance_cashier,含充值渠道)
|
||||
*/
|
||||
class FinanceCashierConfig extends Backend
|
||||
{
|
||||
protected bool $modelValidate = false;
|
||||
|
||||
protected array $noNeedPermission = ['index', 'save'];
|
||||
|
||||
private function hasNodePermission(WebmanRequest $request, string $action): bool
|
||||
{
|
||||
if (!$this->auth) {
|
||||
return false;
|
||||
}
|
||||
$controllerPath = get_controller_path($request);
|
||||
if (!$controllerPath) {
|
||||
return false;
|
||||
}
|
||||
$paths = [];
|
||||
$paths[] = $controllerPath . '/' . $action;
|
||||
$parts = explode('/', $controllerPath);
|
||||
foreach ($parts as &$part) {
|
||||
if (str_contains($part, '_')) {
|
||||
$part = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $part))));
|
||||
}
|
||||
}
|
||||
$paths[] = implode('/', $parts) . '/' . $action;
|
||||
foreach (array_values(array_unique($paths)) as $path) {
|
||||
if ($this->auth->check($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'index')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() !== 'GET') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$row = Db::name('game_config')->where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->find();
|
||||
$form = FinanceCashierConfigLib::parseFromConfigValue($row['config_value'] ?? null);
|
||||
|
||||
$decoded = null;
|
||||
if (is_array($row) && isset($row['config_value']) && is_string($row['config_value']) && trim($row['config_value']) !== '') {
|
||||
$tmp = json_decode($row['config_value'], true);
|
||||
if (is_array($tmp)) {
|
||||
$decoded = $tmp;
|
||||
}
|
||||
}
|
||||
$channelsKeyPresent = is_array($decoded) && array_key_exists('channels', $decoded);
|
||||
if (!$channelsKeyPresent) {
|
||||
$depRow = Db::name('game_config')->where('config_key', DepositChannelLib::CONFIG_KEY)->find();
|
||||
$legacy = DepositChannelLib::parseOverridesFromConfigValue(is_array($depRow) ? ($depRow['config_value'] ?? null) : null);
|
||||
$form['channels'] = DepositChannelLib::expandRowsForAdmin($legacy);
|
||||
} else {
|
||||
$form['channels'] = DepositChannelLib::expandRowsForAdmin($form['channels'] ?? []);
|
||||
}
|
||||
|
||||
return $this->success('', [
|
||||
'form' => $form,
|
||||
'registry' => $this->buildRegistryOut(),
|
||||
'tier_options' => $this->buildTierOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if (!$this->hasNodePermission($request, 'save')) {
|
||||
return $this->error(__('You have no permission'), [], 401);
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$payload = $request->post();
|
||||
if (!is_array($payload)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
try {
|
||||
$json = FinanceCashierConfigLib::encodeForDb($payload);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$resourceKey = GameHotDataLock::safeResourceKeyForConfig(FinanceCashierConfigLib::CONFIG_KEY);
|
||||
$lock = GameHotDataLock::tryAcquire(GameHotDataLock::TYPE_GAME_CONFIG, $resourceKey);
|
||||
if (!$lock['acquired']) {
|
||||
return $this->error('该配置正在被其他操作占用,请稍后再试');
|
||||
}
|
||||
try {
|
||||
try {
|
||||
$exists = Db::name('game_config')->where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->find();
|
||||
if ($exists) {
|
||||
Db::name('game_config')->where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->update([
|
||||
'config_value' => $json,
|
||||
'value_type' => 'json',
|
||||
'update_time' => $now,
|
||||
]);
|
||||
} else {
|
||||
Db::name('game_config')->insert([
|
||||
'config_key' => FinanceCashierConfigLib::CONFIG_KEY,
|
||||
'config_value' => $json,
|
||||
'value_type' => 'json',
|
||||
'remark' => '支付/收款配置(表单维护,含充值渠道)',
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
$decodedSave = json_decode($json, true);
|
||||
$chList = is_array($decodedSave) && isset($decodedSave['channels']) && is_array($decodedSave['channels'])
|
||||
? $decodedSave['channels']
|
||||
: [];
|
||||
$mirrorJson = DepositChannelLib::encodeForDb($chList);
|
||||
$depExists = Db::name('game_config')->where('config_key', DepositChannelLib::CONFIG_KEY)->find();
|
||||
if ($depExists) {
|
||||
Db::name('game_config')->where('config_key', DepositChannelLib::CONFIG_KEY)->update([
|
||||
'config_value' => $mirrorJson,
|
||||
'value_type' => 'json',
|
||||
'update_time' => $now,
|
||||
]);
|
||||
} else {
|
||||
Db::name('game_config')->insert([
|
||||
'config_key' => DepositChannelLib::CONFIG_KEY,
|
||||
'config_value' => $mirrorJson,
|
||||
'value_type' => 'json',
|
||||
'remark' => '充值渠道(与 finance_cashier.channels 同步)',
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(FinanceCashierConfigLib::CONFIG_KEY);
|
||||
GameHotDataCoordinator::afterGameConfigKeyCommitted(DepositChannelLib::CONFIG_KEY);
|
||||
|
||||
return $this->success(__('Saved successfully'));
|
||||
} finally {
|
||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_CONFIG, $resourceKey, $lock['token'], $lock['redis_lock']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: string, label: string}>
|
||||
*/
|
||||
private function buildTierOptions(): array
|
||||
{
|
||||
$tierRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find();
|
||||
$allTiers = DepositTierLib::parseFromConfigValue(is_array($tierRow) ? ($tierRow['config_value'] ?? null) : null);
|
||||
$tierOptions = [];
|
||||
foreach ($allTiers as $t) {
|
||||
if (!is_array($t)) {
|
||||
continue;
|
||||
}
|
||||
$tid = isset($t['id']) && is_string($t['id']) ? $t['id'] : '';
|
||||
if ($tid === '') {
|
||||
continue;
|
||||
}
|
||||
$title = isset($t['title']) && is_string($t['title']) ? trim($t['title']) : '';
|
||||
$tierOptions[] = [
|
||||
'id' => $tid,
|
||||
'label' => $title !== '' ? $title . ' (' . $tid . ')' : $tid,
|
||||
];
|
||||
}
|
||||
|
||||
return $tierOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{name: string, name_en: string, sort: int}>
|
||||
*/
|
||||
private function buildRegistryOut(): array
|
||||
{
|
||||
$registry = DepositChannelLib::codeRegistry();
|
||||
$registryOut = [];
|
||||
foreach ($registry as $code => $meta) {
|
||||
if (!is_array($meta)) {
|
||||
continue;
|
||||
}
|
||||
$registryOut[$code] = [
|
||||
'name' => isset($meta['name']) && is_string($meta['name']) ? $meta['name'] : '',
|
||||
'name_en' => isset($meta['name_en']) && is_string($meta['name_en']) ? $meta['name_en'] : '',
|
||||
'sort' => isset($meta['sort']) && is_numeric($meta['sort']) ? intval($meta['sort']) : 10,
|
||||
];
|
||||
}
|
||||
|
||||
return $registryOut;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace app\admin\controller\config;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\library\game\DepositChannel;
|
||||
use app\common\library\game\DepositTier;
|
||||
use app\common\library\game\FinanceCashierConfig;
|
||||
use app\common\library\game\StreakWinReward;
|
||||
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
|
||||
use support\Response;
|
||||
@@ -42,6 +44,8 @@ class GameConfig extends Backend
|
||||
return [
|
||||
ZiHuaDictionaryLib::CONFIG_KEY,
|
||||
DepositTier::CONFIG_KEY,
|
||||
DepositChannel::CONFIG_KEY,
|
||||
FinanceCashierConfig::CONFIG_KEY,
|
||||
StreakWinReward::CONFIG_KEY,
|
||||
];
|
||||
}
|
||||
|
||||
29
app/admin/controller/order/DepositChannelOrder.php
Normal file
29
app/admin/controller/order/DepositChannelOrder.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\order;
|
||||
|
||||
use app\common\library\game\DepositChannel;
|
||||
|
||||
/**
|
||||
* 渠道充值订单:仅列出已注册且启用的支付渠道(pay_channel)产生的充值单
|
||||
*/
|
||||
class DepositChannelOrder extends DepositOrder
|
||||
{
|
||||
/**
|
||||
* @param list<array<mixed>> $where
|
||||
*/
|
||||
protected function appendDepositOrderIndexWhere(array &$where, string $mainShort): void
|
||||
{
|
||||
if ($mainShort === '') {
|
||||
return;
|
||||
}
|
||||
$effective = DepositChannel::effectiveRowsFromDb();
|
||||
$codes = DepositChannel::enabledPayChannelCodes($effective);
|
||||
if ($codes === []) {
|
||||
$where[] = [$mainShort . '.pay_channel', '=', '__no_pay_channel__'];
|
||||
|
||||
return;
|
||||
}
|
||||
$where[] = [$mainShort . '.pay_channel', 'in', $codes];
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class DepositOrder extends Backend
|
||||
$channelIds = $this->getScopedChannelIdsForFilter();
|
||||
$where[] = [$mainShort . '.channel_id', 'in', $channelIds !== [] ? $channelIds : [0]];
|
||||
}
|
||||
$this->appendDepositOrderIndexWhere($where, $mainShort);
|
||||
|
||||
$res = $this->model
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
@@ -72,6 +73,15 @@ class DepositOrder extends Backend
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类可追加列表过滤条件(例如仅展示已注册充值渠道的订单)
|
||||
*
|
||||
* @param list<array<mixed>> $where
|
||||
*/
|
||||
protected function appendDepositOrderIndexWhere(array &$where, string $mainShort): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 时返回关联信息,便于前端详情弹窗直接渲染 user.username / channel.name;
|
||||
* POST 一律拒绝,保证充值订单的金额/状态只能由结算服务变更。
|
||||
|
||||
@@ -6,7 +6,9 @@ namespace app\api\controller;
|
||||
|
||||
use app\common\library\finance\DepositSettlement;
|
||||
use app\common\library\finance\WithdrawFlow;
|
||||
use app\common\library\game\DepositChannel as DepositChannelLib;
|
||||
use app\common\library\game\DepositTier as DepositTierLib;
|
||||
use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib;
|
||||
use app\common\model\DepositOrder;
|
||||
use app\common\model\GameConfig;
|
||||
use app\common\model\WithdrawOrder;
|
||||
@@ -29,19 +31,30 @@ class Finance extends MobileBase
|
||||
|
||||
$lang = $this->currentLang();
|
||||
$tiers = $this->loadEnabledTiers();
|
||||
$effectiveChannels = $this->loadDepositChannelEffective();
|
||||
$list = [];
|
||||
foreach ($tiers as $tier) {
|
||||
$amount = $this->amountString($tier['amount'] ?? '0');
|
||||
$bonus = $this->amountString($tier['bonus_amount'] ?? '0');
|
||||
$total = bcadd($amount, $bonus, 4);
|
||||
$payAmount = $this->amountString($tier['pay_amount'] ?? '0');
|
||||
$currency = isset($tier['currency']) && is_string($tier['currency']) ? strtoupper(trim($tier['currency'])) : 'CNY';
|
||||
if ($currency === '') {
|
||||
$currency = 'CNY';
|
||||
}
|
||||
$localized = DepositTierLib::localize($tier, $lang);
|
||||
$tierId = isset($tier['id']) && is_string($tier['id']) ? $tier['id'] : '';
|
||||
$list[] = [
|
||||
'id' => $tier['id'],
|
||||
'id' => $tierId,
|
||||
'tier_key' => $tierId,
|
||||
'title' => $localized['title'],
|
||||
'currency' => $currency,
|
||||
'pay_amount' => $payAmount,
|
||||
'amount' => $amount,
|
||||
'bonus_amount' => $bonus,
|
||||
'total_amount' => $total,
|
||||
'desc' => $localized['desc'],
|
||||
'channels' => DepositChannelLib::channelsForTier($tierId, $effectiveChannels, $lang),
|
||||
];
|
||||
}
|
||||
return $this->mobileSuccess([
|
||||
@@ -69,7 +82,8 @@ class Finance extends MobileBase
|
||||
* 并把入账动作放到网关回调里完成(回调中调用 DepositSettlement::settle)。
|
||||
*
|
||||
* 请求:application/json 或 x-www-form-urlencoded
|
||||
* - tier_id: 必填,档位 ID(需在 game_config.deposit_tier 启用档位内)
|
||||
* - tier_id / tier_key: 必填,档位唯一标识(与 depositTierList 中 id、tier_key 一致)
|
||||
* - channel_code: 必填,支付渠道代码(与 depositTierList 各档位 channels[].code 一致)
|
||||
* - idempotency_key: 必填,客户端幂等键,短时间内重复提交只生成一次订单
|
||||
*
|
||||
* 响应(统一结构,未来接入第三方也保持此形状):
|
||||
@@ -83,8 +97,12 @@ class Finance extends MobileBase
|
||||
}
|
||||
|
||||
$tierId = $this->stringParam($request->input('tier_id'));
|
||||
if ($tierId === '') {
|
||||
$tierId = $this->stringParam($request->input('tier_key'));
|
||||
}
|
||||
$channelCode = strtolower($this->stringParam($request->input('channel_code')));
|
||||
$idempotencyKey = $this->stringParam($request->input('idempotency_key'));
|
||||
if ($tierId === '' || $idempotencyKey === '') {
|
||||
if ($tierId === '' || $channelCode === '' || $idempotencyKey === '') {
|
||||
return $this->mobileError(1001, 'Missing parameters');
|
||||
}
|
||||
if (mb_strlen($idempotencyKey) > 64) {
|
||||
@@ -96,6 +114,10 @@ class Finance extends MobileBase
|
||||
if (!$tier) {
|
||||
return $this->mobileError(2003, 'Deposit tier not available');
|
||||
}
|
||||
$effectiveChannels = $this->loadDepositChannelEffective();
|
||||
if (!DepositChannelLib::assertChannelAllowsTier($channelCode, $tierId, $effectiveChannels)) {
|
||||
return $this->mobileError(2004, 'Pay channel not available');
|
||||
}
|
||||
|
||||
// 幂等命中:直接返回已有订单
|
||||
try {
|
||||
@@ -112,14 +134,21 @@ class Finance extends MobileBase
|
||||
|
||||
$user = $this->auth->getUser();
|
||||
$orderNo = 'DP' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6);
|
||||
$curSnap = isset($tier['currency']) && is_string($tier['currency']) ? strtoupper(trim($tier['currency'])) : 'CNY';
|
||||
if ($curSnap === '') {
|
||||
$curSnap = 'CNY';
|
||||
}
|
||||
$tierSnapshot = [
|
||||
'id' => $tier['id'],
|
||||
'title' => is_string($tier['title'] ?? null) ? $tier['title'] : '',
|
||||
'title_en' => is_string($tier['title_en'] ?? null) ? $tier['title_en'] : '',
|
||||
'currency' => $curSnap,
|
||||
'pay_amount' => $this->amountString($tier['pay_amount'] ?? '0'),
|
||||
'amount' => $this->amountString($tier['amount'] ?? '0'),
|
||||
'bonus_amount' => $this->amountString($tier['bonus_amount'] ?? '0'),
|
||||
'desc' => is_string($tier['desc'] ?? null) ? $tier['desc'] : '',
|
||||
'desc_en' => is_string($tier['desc_en'] ?? null) ? $tier['desc_en'] : '',
|
||||
'channel_code' => $channelCode,
|
||||
];
|
||||
|
||||
$now = time();
|
||||
@@ -138,7 +167,7 @@ class Finance extends MobileBase
|
||||
'amount' => $tierSnapshot['amount'],
|
||||
'bonus_amount' => $tierSnapshot['bonus_amount'],
|
||||
'status' => 0,
|
||||
'pay_channel' => 'mock_gateway',
|
||||
'pay_channel' => $channelCode,
|
||||
'deposit_tier_id' => $tier['id'],
|
||||
'proof_image' => '',
|
||||
'pay_account_snapshot' => json_encode($tierSnapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
@@ -163,7 +192,9 @@ class Finance extends MobileBase
|
||||
DepositSettlement::settle(
|
||||
$orderId,
|
||||
DepositSettlement::SOURCE_MOCK_GATEWAY,
|
||||
'mock gateway auto settled'
|
||||
'mock gateway auto settled',
|
||||
null,
|
||||
'channel_code=' . $channelCode
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
return $this->mobileError(2000, $e->getMessage());
|
||||
@@ -483,6 +514,214 @@ class Finance extends MobileBase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收银台配置:货币列表(含充值/提现汇率)、支付渠道(pay_channels)、提现银行与文案(供充值/提现页展示)
|
||||
*/
|
||||
public function cashierConfig(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeMobile($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
$lang = $this->currentLang();
|
||||
$isZh = str_starts_with($lang, 'zh');
|
||||
$row = GameConfig::where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->find();
|
||||
$cfg = FinanceCashierConfigLib::parseFromConfigValue($row?->config_value ?? null);
|
||||
|
||||
$pc = $cfg['platform_coin'] ?? [];
|
||||
$platformLabel = '';
|
||||
if (is_array($pc)) {
|
||||
$platformLabel = $isZh
|
||||
? (is_string($pc['label_zh'] ?? null) ? $pc['label_zh'] : '')
|
||||
: (is_string($pc['label_en'] ?? null) ? $pc['label_en'] : '');
|
||||
}
|
||||
|
||||
$currencies = [];
|
||||
if (isset($cfg['currencies']) && is_array($cfg['currencies'])) {
|
||||
$list = $cfg['currencies'];
|
||||
usort($list, function (array $a, array $b): int {
|
||||
return $this->sortBySortKeyThenCode($a, $b);
|
||||
});
|
||||
foreach ($list as $c) {
|
||||
if (!is_array($c)) {
|
||||
continue;
|
||||
}
|
||||
$code = isset($c['code']) && is_string($c['code']) ? $c['code'] : '';
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$dep = isset($c['deposit_coins_per_fiat']) && is_string($c['deposit_coins_per_fiat']) ? $c['deposit_coins_per_fiat'] : '';
|
||||
$wdr = isset($c['withdraw_coins_per_fiat']) && is_string($c['withdraw_coins_per_fiat']) ? $c['withdraw_coins_per_fiat'] : '';
|
||||
$currencies[] = [
|
||||
'code' => $code,
|
||||
'label' => $isZh
|
||||
? (is_string($c['label_zh'] ?? null) ? $c['label_zh'] : '')
|
||||
: (is_string($c['label_en'] ?? null) ? $c['label_en'] : ''),
|
||||
'deposit_coins_per_fiat' => $dep,
|
||||
'withdraw_coins_per_fiat' => $wdr,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$rates = [];
|
||||
foreach ($currencies as $c) {
|
||||
if (!is_array($c)) {
|
||||
continue;
|
||||
}
|
||||
$cur = isset($c['code']) && is_string($c['code']) ? $c['code'] : '';
|
||||
$ratio = isset($c['withdraw_coins_per_fiat']) && is_string($c['withdraw_coins_per_fiat']) ? $c['withdraw_coins_per_fiat'] : '';
|
||||
if ($cur === '' || $ratio === '') {
|
||||
continue;
|
||||
}
|
||||
$rates[] = [
|
||||
'currency' => $cur,
|
||||
'diamonds_per_fiat_unit' => $ratio,
|
||||
];
|
||||
}
|
||||
|
||||
$banks = [];
|
||||
if (isset($cfg['withdraw_banks']) && is_array($cfg['withdraw_banks'])) {
|
||||
$list = $cfg['withdraw_banks'];
|
||||
usort($list, function (array $a, array $b): int {
|
||||
$cmp = $this->sortBySortKeyOnly($a, $b);
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
$ca = isset($a['code']) && is_string($a['code']) ? $a['code'] : '';
|
||||
$cb = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';
|
||||
|
||||
return strcmp($ca, $cb);
|
||||
});
|
||||
foreach ($list as $b) {
|
||||
if (!is_array($b)) {
|
||||
continue;
|
||||
}
|
||||
$code = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$banks[] = [
|
||||
'code' => $code,
|
||||
'name' => $isZh
|
||||
? (is_string($b['name_zh'] ?? null) ? $b['name_zh'] : '')
|
||||
: (is_string($b['name_en'] ?? null) ? $b['name_en'] : ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$wl = $cfg['withdraw_limits'] ?? [];
|
||||
$minEw = is_array($wl) && isset($wl['min_ewallet']) && is_string($wl['min_ewallet']) ? $wl['min_ewallet'] : '0';
|
||||
$minBk = is_array($wl) && isset($wl['min_bank']) && is_string($wl['min_bank']) ? $wl['min_bank'] : '0';
|
||||
|
||||
$wc = $cfg['withdraw_copy'] ?? [];
|
||||
$rateMode = is_array($wc) && isset($wc['rate_mode']) && is_string($wc['rate_mode']) ? $wc['rate_mode'] : 'fixed';
|
||||
|
||||
$wf = $cfg['withdraw_fields'] ?? [];
|
||||
$reqCard = is_array($wf) && !empty($wf['require_cardholder']);
|
||||
$reqAcct = is_array($wf) && !empty($wf['require_bank_account']);
|
||||
$reqMail = is_array($wf) && !empty($wf['require_email']);
|
||||
$reqMob = is_array($wf) && !empty($wf['require_mobile']);
|
||||
|
||||
$payChannels = [];
|
||||
$effectiveCh = DepositChannelLib::effectiveRowsFromDb();
|
||||
$regCh = DepositChannelLib::codeRegistry();
|
||||
foreach ($effectiveCh as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$code = isset($row['code']) && is_string($row['code']) ? $row['code'] : '';
|
||||
if ($code === '' || !isset($regCh[$code]) || !is_array($regCh[$code])) {
|
||||
continue;
|
||||
}
|
||||
$meta = $regCh[$code];
|
||||
$nameZh = isset($meta['name']) && is_string($meta['name']) ? $meta['name'] : '';
|
||||
$nameEn = isset($meta['name_en']) && is_string($meta['name_en']) ? $meta['name_en'] : '';
|
||||
$sortNum = filter_var($row['sort'] ?? 0, FILTER_VALIDATE_INT);
|
||||
$statusNum = filter_var($row['status'] ?? 0, FILTER_VALIDATE_INT);
|
||||
$payChannels[] = [
|
||||
'code' => $code,
|
||||
'name' => $isZh ? $nameZh : ($nameEn !== '' ? $nameEn : $nameZh),
|
||||
'sort' => $sortNum !== false ? $sortNum : 0,
|
||||
'status' => $statusNum !== false ? $statusNum : 0,
|
||||
'tier_ids' => isset($row['tier_ids']) && is_array($row['tier_ids']) ? $row['tier_ids'] : [],
|
||||
];
|
||||
}
|
||||
usort($payChannels, function (array $a, array $b): int {
|
||||
$sa = isset($a['sort']) ? filter_var($a['sort'], FILTER_VALIDATE_INT) : false;
|
||||
$sb = isset($b['sort']) ? filter_var($b['sort'], FILTER_VALIDATE_INT) : false;
|
||||
$ia = $sa === false ? 0 : $sa;
|
||||
$ib = $sb === false ? 0 : $sb;
|
||||
if ($ia !== $ib) {
|
||||
return $ia <=> $ib;
|
||||
}
|
||||
$ca = isset($a['code']) && is_string($a['code']) ? $a['code'] : '';
|
||||
$cb = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';
|
||||
|
||||
return strcmp($ca, $cb);
|
||||
});
|
||||
|
||||
return $this->mobileSuccess([
|
||||
'platform_coin_label' => $platformLabel,
|
||||
'currencies' => $currencies,
|
||||
'rates' => $rates,
|
||||
'pay_channels' => $payChannels,
|
||||
'withdraw' => [
|
||||
'banks' => $banks,
|
||||
'min_ewallet' => $minEw,
|
||||
'min_bank' => $minBk,
|
||||
'rate_hint' => $isZh
|
||||
? (is_array($wc) && is_string($wc['rate_hint_zh'] ?? null) ? $wc['rate_hint_zh'] : '')
|
||||
: (is_array($wc) && is_string($wc['rate_hint_en'] ?? null) ? $wc['rate_hint_en'] : ''),
|
||||
'processing_note' => $isZh
|
||||
? (is_array($wc) && is_string($wc['processing_zh'] ?? null) ? $wc['processing_zh'] : '')
|
||||
: (is_array($wc) && is_string($wc['processing_en'] ?? null) ? $wc['processing_en'] : ''),
|
||||
'fee_note' => $isZh
|
||||
? (is_array($wc) && is_string($wc['fee_note_zh'] ?? null) ? $wc['fee_note_zh'] : '')
|
||||
: (is_array($wc) && is_string($wc['fee_note_en'] ?? null) ? $wc['fee_note_en'] : ''),
|
||||
'rate_mode' => $rateMode,
|
||||
'fields' => [
|
||||
'require_cardholder' => $reqCard,
|
||||
'require_bank_account' => $reqAcct,
|
||||
'require_email' => $reqMail,
|
||||
'require_mobile' => $reqMob,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $a
|
||||
* @param array<string, mixed> $b
|
||||
*/
|
||||
private function sortBySortKeyThenCode(array $a, array $b): int
|
||||
{
|
||||
$sa = isset($a['sort']) ? filter_var($a['sort'], FILTER_VALIDATE_INT) : false;
|
||||
$sb = isset($b['sort']) ? filter_var($b['sort'], FILTER_VALIDATE_INT) : false;
|
||||
$ia = $sa === false ? 0 : $sa;
|
||||
$ib = $sb === false ? 0 : $sb;
|
||||
if ($ia !== $ib) {
|
||||
return $ia <=> $ib;
|
||||
}
|
||||
$ca = isset($a['code']) && is_string($a['code']) ? $a['code'] : '';
|
||||
$cb = isset($b['code']) && is_string($b['code']) ? $b['code'] : '';
|
||||
|
||||
return strcmp($ca, $cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $a
|
||||
* @param array<string, mixed> $b
|
||||
*/
|
||||
private function sortBySortKeyOnly(array $a, array $b): int
|
||||
{
|
||||
$sa = isset($a['sort']) ? filter_var($a['sort'], FILTER_VALIDATE_INT) : false;
|
||||
$sb = isset($b['sort']) ? filter_var($b['sort'], FILTER_VALIDATE_INT) : false;
|
||||
$ia = $sa === false ? 0 : $sa;
|
||||
$ib = $sb === false ? 0 : $sb;
|
||||
|
||||
return $ia <=> $ib;
|
||||
}
|
||||
|
||||
private function stringParam($raw): string
|
||||
{
|
||||
if ($raw === null) {
|
||||
@@ -501,6 +740,14 @@ class Finance extends MobileBase
|
||||
return DepositTierLib::publicList($all);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
private function loadDepositChannelEffective(): array
|
||||
{
|
||||
return DepositChannelLib::effectiveRowsFromDb();
|
||||
}
|
||||
|
||||
private function mapDepositStatus($status): string
|
||||
{
|
||||
if ($this->intValue($status) === 1) {
|
||||
|
||||
@@ -74,6 +74,10 @@ return [
|
||||
'运营公告' => 'Operation notices',
|
||||
'用户阅读记录' => 'User read log',
|
||||
'充值档位' => 'Deposit tiers',
|
||||
'充值提现收银配置' => 'Pay / receipt settings',
|
||||
'支付/收款配置' => 'Pay / receipt settings',
|
||||
'充值渠道' => 'Deposit channels',
|
||||
'渠道充值订单' => 'Channel deposit orders',
|
||||
'连胜奖励' => 'Win streak rewards',
|
||||
'连胜降低档位' => 'Streak reduction tiers',
|
||||
'钱包加减点' => 'Wallet adjust',
|
||||
|
||||
438
app/common/library/game/DepositChannel.php
Normal file
438
app/common/library/game/DepositChannel.php
Normal file
@@ -0,0 +1,438 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\game;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 充值支付渠道:优先读取 game_config.finance_cashier.channels;无此键时回退 game_config.deposit_channel(迁移期镜像)
|
||||
*
|
||||
* 每项:code(须在代码/环境注册表内)、sort、status(0/1)、tier_ids(空=全部启用档位)
|
||||
*
|
||||
* 渠道展示名以代码注册表为准;运营只配置开关、排序、可用档位。
|
||||
*/
|
||||
final class DepositChannel
|
||||
{
|
||||
public const CONFIG_KEY = 'deposit_channel';
|
||||
|
||||
/**
|
||||
* @return array<string, array{name: string, name_en: string, sort: int}>
|
||||
*/
|
||||
public static function codeRegistry(): array
|
||||
{
|
||||
$base = [
|
||||
'directpay' => ['name' => 'DirectPay', 'name_en' => 'DirectPay', 'sort' => 10],
|
||||
];
|
||||
$extra = self::registryFromEnv();
|
||||
foreach ($extra as $code => $meta) {
|
||||
if (!is_string($code) || trim($code) === '' || !is_array($meta)) {
|
||||
continue;
|
||||
}
|
||||
$normCode = strtolower(trim($code));
|
||||
if (!preg_match('/^[a-z0-9_\-]{1,24}$/', $normCode)) {
|
||||
continue;
|
||||
}
|
||||
$name = isset($meta['name']) && is_string($meta['name']) ? trim($meta['name']) : '';
|
||||
$nameEn = isset($meta['name_en']) && is_string($meta['name_en']) ? trim($meta['name_en']) : '';
|
||||
$sort = isset($meta['sort']) && is_numeric($meta['sort']) ? intval($meta['sort']) : 50;
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$base[$normCode] = [
|
||||
'name' => $name,
|
||||
'name_en' => $nameEn !== '' ? $nameEn : $name,
|
||||
'sort' => $sort,
|
||||
];
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function registryFromEnv(): array
|
||||
{
|
||||
$raw = getenv('DEPOSIT_CHANNELS_REGISTRY_JSON');
|
||||
if (!is_string($raw) || trim($raw) === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($decoded as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$code = isset($item['code']) && is_string($item['code']) ? strtolower(trim($item['code'])) : '';
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$out[$code] = $item;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function parseOverridesFromConfigValue($raw): array
|
||||
{
|
||||
if (!is_string($raw) || trim($raw) === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
$list = isset($decoded['channels']) && is_array($decoded['channels']) ? $decoded['channels'] : $decoded;
|
||||
|
||||
return self::normalizeOverrides($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 库中「运营覆盖」原始列表(未与注册表合并):finance_cashier 含 channels 键时用其值;否则读 deposit_channel
|
||||
*
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
public static function parseStoredOverridesFromDb(): array
|
||||
{
|
||||
$fcRow = Db::name('game_config')->where('config_key', FinanceCashierConfig::CONFIG_KEY)->find();
|
||||
$rawFc = is_array($fcRow) ? ($fcRow['config_value'] ?? null) : null;
|
||||
$decoded = null;
|
||||
if (is_string($rawFc) && trim($rawFc) !== '') {
|
||||
$tmp = json_decode($rawFc, true);
|
||||
if (is_array($tmp)) {
|
||||
$decoded = $tmp;
|
||||
}
|
||||
}
|
||||
$channelsKeyPresent = is_array($decoded) && array_key_exists('channels', $decoded);
|
||||
if ($channelsKeyPresent) {
|
||||
$list = isset($decoded['channels']) && is_array($decoded['channels']) ? $decoded['channels'] : [];
|
||||
|
||||
return self::normalizeOverrides($list);
|
||||
}
|
||||
$depRow = Db::name('game_config')->where('config_key', self::CONFIG_KEY)->find();
|
||||
$depRaw = is_array($depRow) ? ($depRow['config_value'] ?? null) : null;
|
||||
|
||||
return self::parseOverridesFromConfigValue($depRaw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与注册表合并并排序后的有效渠道行(业务侧统一入口)
|
||||
*
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
public static function effectiveRowsFromDb(): array
|
||||
{
|
||||
$stored = self::parseStoredOverridesFromDb();
|
||||
|
||||
return self::effectiveOverrides(self::expandRowsForAdmin($stored));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<mixed> $items
|
||||
*
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
public static function normalizeOverrides(array $items): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($items as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$code = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$sort = isset($row['sort']) && is_numeric($row['sort']) ? intval($row['sort']) : 0;
|
||||
$status = isset($row['status']) && is_numeric($row['status']) ? intval($row['status']) : 1;
|
||||
$status = $status === 1 ? 1 : 0;
|
||||
$tierIds = [];
|
||||
if (isset($row['tier_ids']) && is_array($row['tier_ids'])) {
|
||||
foreach ($row['tier_ids'] as $tid) {
|
||||
if (is_string($tid)) {
|
||||
$t = trim($tid);
|
||||
if ($t !== '' && preg_match('/^[a-zA-Z0-9_\-]{1,32}$/', $t)) {
|
||||
$tierIds[] = $t;
|
||||
}
|
||||
}
|
||||
}
|
||||
$tierIds = array_values(array_unique($tierIds));
|
||||
}
|
||||
$out[] = [
|
||||
'code' => $code,
|
||||
'sort' => $sort,
|
||||
'status' => $status,
|
||||
'tier_ids' => $tierIds,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并注册表与运营覆盖;若库中无覆盖则对注册表内全部渠道启用默认行
|
||||
*
|
||||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $overrides
|
||||
*
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
public static function effectiveOverrides(array $overrides): array
|
||||
{
|
||||
$registry = self::codeRegistry();
|
||||
$byCode = [];
|
||||
foreach ($overrides as $row) {
|
||||
if (!isset($registry[$row['code']])) {
|
||||
continue;
|
||||
}
|
||||
$byCode[$row['code']] = $row;
|
||||
}
|
||||
if ($byCode === []) {
|
||||
foreach ($registry as $code => $meta) {
|
||||
$sortMeta = $meta['sort'] ?? 10;
|
||||
$sortVal = is_numeric($sortMeta) ? intval($sortMeta) : 10;
|
||||
$byCode[$code] = [
|
||||
'code' => $code,
|
||||
'sort' => $sortVal,
|
||||
'status' => 1,
|
||||
'tier_ids' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
$list = array_values($byCode);
|
||||
usort($list, static function (array $a, array $b): int {
|
||||
if ($a['sort'] !== $b['sort']) {
|
||||
return $a['sort'] <=> $b['sort'];
|
||||
}
|
||||
|
||||
return strcmp($a['code'], $b['code']);
|
||||
});
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{code: string, sort: int, status: int, tier_ids: list<string>} $overrideRow
|
||||
*/
|
||||
public static function isTierAllowed(array $overrideRow, string $tierId): bool
|
||||
{
|
||||
$ids = $overrideRow['tier_ids'] ?? [];
|
||||
if (!is_array($ids) || $ids === []) {
|
||||
return true;
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
if (is_string($id) && $id === $tierId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $effectiveRows
|
||||
*/
|
||||
public static function findMergedByCode(array $effectiveRows, string $code): ?array
|
||||
{
|
||||
$norm = strtolower(trim($code));
|
||||
foreach ($effectiveRows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$c = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||||
if ($c === $norm) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $overrideRows
|
||||
*
|
||||
* @return list<array{code: string, name: string, sort: int}>
|
||||
*/
|
||||
public static function channelsForTier(string $tierId, array $overrideRows, string $lang): array
|
||||
{
|
||||
$registry = self::codeRegistry();
|
||||
$out = [];
|
||||
foreach ($overrideRows as $row) {
|
||||
if (($row['status'] ?? 0) !== 1) {
|
||||
continue;
|
||||
}
|
||||
$code = $row['code'];
|
||||
if (!isset($registry[$code])) {
|
||||
continue;
|
||||
}
|
||||
if (!self::isTierAllowed($row, $tierId)) {
|
||||
continue;
|
||||
}
|
||||
$meta = $registry[$code];
|
||||
$name = self::pickLangName($meta, $lang);
|
||||
$sortRaw = $row['sort'] ?? 0;
|
||||
$sortVal = is_numeric($sortRaw) ? intval($sortRaw) : 0;
|
||||
$out[] = [
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'sort' => $sortVal,
|
||||
];
|
||||
}
|
||||
usort($out, static function (array $a, array $b): int {
|
||||
if ($a['sort'] !== $b['sort']) {
|
||||
return $a['sort'] <=> $b['sort'];
|
||||
}
|
||||
|
||||
return strcmp($a['code'], $b['code']);
|
||||
});
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $meta
|
||||
*/
|
||||
private static function pickLangName(array $meta, string $lang): string
|
||||
{
|
||||
$name = isset($meta['name']) && is_string($meta['name']) ? $meta['name'] : '';
|
||||
$nameEn = isset($meta['name_en']) && is_string($meta['name_en']) ? $meta['name_en'] : '';
|
||||
$normalized = strtolower(str_replace('_', '-', trim($lang)));
|
||||
$isEn = $normalized === 'en' || str_starts_with($normalized, 'en-');
|
||||
if ($isEn) {
|
||||
return $nameEn !== '' ? $nameEn : $name;
|
||||
}
|
||||
|
||||
return $name !== '' ? $name : $nameEn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $effectiveRows
|
||||
*/
|
||||
public static function assertChannelAllowsTier(string $channelCode, string $tierId, array $effectiveRows): bool
|
||||
{
|
||||
$row = self::findMergedByCode($effectiveRows, $channelCode);
|
||||
if ($row === null) {
|
||||
return false;
|
||||
}
|
||||
if (($row['status'] ?? 0) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::isTierAllowed($row, $tierId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $effectiveRows
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function enabledPayChannelCodes(array $effectiveRows): array
|
||||
{
|
||||
$registry = self::codeRegistry();
|
||||
$codes = [];
|
||||
foreach ($effectiveRows as $row) {
|
||||
if (($row['status'] ?? 0) !== 1) {
|
||||
continue;
|
||||
}
|
||||
$c = isset($row['code']) && is_string($row['code']) ? $row['code'] : '';
|
||||
if ($c !== '' && isset($registry[$c])) {
|
||||
$codes[] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($codes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $items
|
||||
*
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
public static function prepareOverridesForSave(array $items): array
|
||||
{
|
||||
$registry = self::codeRegistry();
|
||||
$seen = [];
|
||||
$out = [];
|
||||
foreach ($items as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行格式错误');
|
||||
}
|
||||
$code = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||||
if ($code === '' || !isset($registry[$code])) {
|
||||
throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行渠道 code 未注册');
|
||||
}
|
||||
if (isset($seen[$code])) {
|
||||
throw new InvalidArgumentException('渠道 code 重复:' . $code);
|
||||
}
|
||||
$seen[$code] = true;
|
||||
$norm = self::normalizeOverrides([array_merge($row, ['code' => $code])]);
|
||||
if ($norm === []) {
|
||||
throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行渠道数据无效');
|
||||
}
|
||||
$out[] = $norm[0];
|
||||
}
|
||||
usort($out, static function (array $a, array $b): int {
|
||||
if ($a['sort'] !== $b['sort']) {
|
||||
return $a['sort'] <=> $b['sort'];
|
||||
}
|
||||
|
||||
return strcmp($a['code'], $b['code']);
|
||||
});
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $items
|
||||
*/
|
||||
public static function encodeForDb(array $items): string
|
||||
{
|
||||
$wrapped = ['channels' => $items];
|
||||
$encoded = json_encode($wrapped, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($encoded === false) {
|
||||
throw new InvalidArgumentException('JSON 编码失败');
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台编辑用:为注册表内每个 code 补齐一行(合并库内覆盖)
|
||||
*
|
||||
* @param list<array{code: string, sort: int, status: int, tier_ids: list<string>}> $storedOverrides
|
||||
*
|
||||
* @return list<array{code: string, sort: int, status: int, tier_ids: list<string>}>
|
||||
*/
|
||||
public static function expandRowsForAdmin(array $storedOverrides): array
|
||||
{
|
||||
$registry = self::codeRegistry();
|
||||
$effective = self::effectiveOverrides($storedOverrides);
|
||||
$byCode = [];
|
||||
foreach ($effective as $r) {
|
||||
$byCode[$r['code']] = $r;
|
||||
}
|
||||
$items = [];
|
||||
foreach ($registry as $code => $meta) {
|
||||
$sortMeta = $meta['sort'] ?? 10;
|
||||
$sortDefault = is_numeric($sortMeta) ? intval($sortMeta) : 10;
|
||||
$items[] = $byCode[$code] ?? [
|
||||
'code' => $code,
|
||||
'sort' => $sortDefault,
|
||||
'status' => 1,
|
||||
'tier_ids' => [],
|
||||
];
|
||||
}
|
||||
usort($items, static function (array $a, array $b): int {
|
||||
if ($a['sort'] !== $b['sort']) {
|
||||
return $a['sort'] <=> $b['sort'];
|
||||
}
|
||||
|
||||
return strcmp($a['code'], $b['code']);
|
||||
});
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,13 @@ use InvalidArgumentException;
|
||||
* 充值档位(game_config.deposit_tier):仅存 JSON 数组
|
||||
*
|
||||
* 每一项字段(mock/第三方支付模式,已不再保存收款账户信息;支持中英文双语):
|
||||
* - id : string,档位稳定 ID(如 t_xxxxxxxx)
|
||||
* - id : string,档位稳定唯一标识(如 t_xxxxxxxx),移动端下单 tier_id / tier_key
|
||||
* - title : string,档位中文名称(必填,前端中文环境展示)
|
||||
* - title_en : string,档位英文名称(可选,前端英文环境展示;为空时回退到 title)
|
||||
* - amount : string,充值金额(4 位小数)
|
||||
* - bonus_amount : string,赠送金额(4 位小数,可为 0)
|
||||
* - currency : string,支付货币代码(3~8 位大写字母,如 MYR、CNY)
|
||||
* - pay_amount : string,玩家支付的法币/支付货币额度(4 位小数)
|
||||
* - amount : string,到账基础平台币(4 位小数)
|
||||
* - bonus_amount : string,赠送平台币(4 位小数,可为 0)
|
||||
* - desc : string,档位中文描述(可空,<=255)
|
||||
* - desc_en : string,档位英文描述(可空,<=255,为空时回退到 desc)
|
||||
* - sort : int,排序权重(小值在前)
|
||||
@@ -33,6 +35,8 @@ final class DepositTier
|
||||
* id: string,
|
||||
* title: string,
|
||||
* title_en: string,
|
||||
* currency: string,
|
||||
* pay_amount: string,
|
||||
* amount: string,
|
||||
* bonus_amount: string,
|
||||
* desc: string,
|
||||
@@ -83,8 +87,18 @@ final class DepositTier
|
||||
}
|
||||
$titleEn = self::stringField($row, 'title_en');
|
||||
|
||||
$currency = self::normalizeCurrency($row['currency'] ?? '');
|
||||
if ($currency === '') {
|
||||
$currency = 'CNY';
|
||||
}
|
||||
|
||||
$payAmount = self::normalizeAmount($row['pay_amount'] ?? '');
|
||||
$amount = self::normalizeAmount($row['amount'] ?? '');
|
||||
$bonus = self::normalizeAmount($row['bonus_amount'] ?? '0');
|
||||
if (bccomp($payAmount, '0', 4) <= 0 && bccomp($amount, '0', 4) > 0) {
|
||||
// 历史数据仅有 amount(平台币)时,用占位同步 pay_amount,运营应在后台改为真实支付额度
|
||||
$payAmount = $amount;
|
||||
}
|
||||
|
||||
$desc = self::stringField($row, 'desc');
|
||||
if ($desc === '') {
|
||||
@@ -100,6 +114,8 @@ final class DepositTier
|
||||
'id' => $id,
|
||||
'title' => $title,
|
||||
'title_en' => $titleEn,
|
||||
'currency' => $currency,
|
||||
'pay_amount' => $payAmount,
|
||||
'amount' => $amount,
|
||||
'bonus_amount' => $bonus,
|
||||
'desc' => $desc,
|
||||
@@ -165,9 +181,19 @@ final class DepositTier
|
||||
throw new InvalidArgumentException('第 ' . $no . ' 行英文充值名称过长');
|
||||
}
|
||||
|
||||
$currency = self::normalizeCurrency($row['currency'] ?? '');
|
||||
if ($currency === '') {
|
||||
throw new InvalidArgumentException('第 ' . $no . ' 行支付货币不能为空');
|
||||
}
|
||||
|
||||
$payAmount = self::normalizeAmount($row['pay_amount'] ?? '');
|
||||
if (bccomp($payAmount, '0', 4) <= 0) {
|
||||
throw new InvalidArgumentException('第 ' . $no . ' 行支付货币额度必须大于 0');
|
||||
}
|
||||
|
||||
$amount = self::normalizeAmount($row['amount'] ?? '');
|
||||
if (bccomp($amount, '0', 4) <= 0) {
|
||||
throw new InvalidArgumentException('第 ' . $no . ' 行充值金额必须大于 0');
|
||||
throw new InvalidArgumentException('第 ' . $no . ' 行基础平台币到账必须大于 0');
|
||||
}
|
||||
|
||||
$bonus = self::normalizeAmount($row['bonus_amount'] ?? '0');
|
||||
@@ -193,6 +219,8 @@ final class DepositTier
|
||||
'id' => $id,
|
||||
'title' => $title,
|
||||
'title_en' => $titleEn,
|
||||
'currency' => $currency,
|
||||
'pay_amount' => $payAmount,
|
||||
'amount' => $amount,
|
||||
'bonus_amount' => $bonus,
|
||||
'desc' => $desc,
|
||||
@@ -340,6 +368,26 @@ final class DepositTier
|
||||
return is_string($v) ? trim($v) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付货币代码:3~8 位字母,统一大写
|
||||
*
|
||||
* @param mixed $raw
|
||||
*/
|
||||
private static function normalizeCurrency($raw): string
|
||||
{
|
||||
if (!is_string($raw)) {
|
||||
return '';
|
||||
}
|
||||
$s = strtoupper(trim($raw));
|
||||
if ($s === '') {
|
||||
return '';
|
||||
}
|
||||
if (!preg_match('/^[A-Z]{3,8}$/', $s)) {
|
||||
return '';
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
private static function isEnglishLang(string $lang): bool
|
||||
{
|
||||
$normalized = strtolower(str_replace('_', '-', trim($lang)));
|
||||
|
||||
392
app/common/library/game/FinanceCashierConfig.php
Normal file
392
app/common/library/game/FinanceCashierConfig.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library\game;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* 移动端支付/收款展示(game_config.finance_cashier,含 channels 充值渠道)
|
||||
*
|
||||
* 货币列表每行含:
|
||||
* - deposit_coins_per_fiat:充值参考,每支付 1 单位该货币到账的平台币数量
|
||||
* - withdraw_coins_per_fiat:提现换算,每兑换 1 单位该货币所需平台币数量
|
||||
*/
|
||||
final class FinanceCashierConfig
|
||||
{
|
||||
public const CONFIG_KEY = 'finance_cashier';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function defaultPayload(): array
|
||||
{
|
||||
return [
|
||||
'platform_coin' => [
|
||||
'label_zh' => '钻石',
|
||||
'label_en' => 'Diamonds',
|
||||
],
|
||||
'currencies' => [
|
||||
[
|
||||
'code' => 'MYR',
|
||||
'label_zh' => '马来西亚林吉特',
|
||||
'label_en' => 'Malaysian Ringgit',
|
||||
'sort' => 10,
|
||||
'deposit_coins_per_fiat' => '100',
|
||||
'withdraw_coins_per_fiat' => '100',
|
||||
],
|
||||
[
|
||||
'code' => 'VND',
|
||||
'label_zh' => '越南盾',
|
||||
'label_en' => 'Vietnamese Dong',
|
||||
'sort' => 20,
|
||||
'deposit_coins_per_fiat' => '10',
|
||||
'withdraw_coins_per_fiat' => '10',
|
||||
],
|
||||
[
|
||||
'code' => 'USDT',
|
||||
'label_zh' => 'USDT',
|
||||
'label_en' => 'USDT',
|
||||
'sort' => 30,
|
||||
'deposit_coins_per_fiat' => '1',
|
||||
'withdraw_coins_per_fiat' => '1',
|
||||
],
|
||||
],
|
||||
'withdraw_banks' => [
|
||||
['code' => 'agrobank', 'name_zh' => 'Agrobank', 'name_en' => 'Agrobank', 'sort' => 10],
|
||||
],
|
||||
'withdraw_limits' => [
|
||||
'min_ewallet' => '10',
|
||||
'min_bank' => '10',
|
||||
],
|
||||
'withdraw_copy' => [
|
||||
'rate_hint_zh' => '汇率为参考价格,实际以提现时为准。',
|
||||
'rate_hint_en' => 'Exchange rates are for reference only; the actual rate at withdrawal time prevails.',
|
||||
'processing_zh' => '9秒即可到账',
|
||||
'processing_en' => 'Arrives in seconds',
|
||||
'fee_note_zh' => '注意:RM 10 — RM 99.99 之间的交易将收取最低 RM 1 的提现手续费。',
|
||||
'fee_note_en' => 'A minimum RM 1 handling fee may apply for withdrawals between RM 10 and RM 99.99.',
|
||||
'rate_mode' => 'fixed',
|
||||
],
|
||||
'withdraw_fields' => [
|
||||
'require_cardholder' => true,
|
||||
'require_bank_account' => true,
|
||||
'require_email' => true,
|
||||
'require_mobile' => true,
|
||||
],
|
||||
'channels' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function parseFromConfigValue(mixed $raw): array
|
||||
{
|
||||
$out = self::defaultPayload();
|
||||
if (!is_string($raw) || trim($raw) === '') {
|
||||
return self::normalizePayload($out, []);
|
||||
}
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
return self::normalizePayload($out, []);
|
||||
}
|
||||
if (isset($decoded['platform_coin']) && is_array($decoded['platform_coin'])) {
|
||||
$out['platform_coin'] = array_replace($out['platform_coin'], array_intersect_key($decoded['platform_coin'], $out['platform_coin']));
|
||||
}
|
||||
$legacyRates = [];
|
||||
if (isset($decoded['rates']) && is_array($decoded['rates'])) {
|
||||
$legacyRates = array_values($decoded['rates']);
|
||||
}
|
||||
foreach (['currencies', 'withdraw_banks', 'channels'] as $listKey) {
|
||||
if (isset($decoded[$listKey]) && is_array($decoded[$listKey])) {
|
||||
$out[$listKey] = array_values($decoded[$listKey]);
|
||||
}
|
||||
}
|
||||
if (isset($decoded['withdraw_limits']) && is_array($decoded['withdraw_limits'])) {
|
||||
$out['withdraw_limits'] = array_replace($out['withdraw_limits'], array_intersect_key($decoded['withdraw_limits'], $out['withdraw_limits']));
|
||||
}
|
||||
if (isset($decoded['withdraw_copy']) && is_array($decoded['withdraw_copy'])) {
|
||||
$out['withdraw_copy'] = array_replace($out['withdraw_copy'], array_intersect_key($decoded['withdraw_copy'], $out['withdraw_copy']));
|
||||
}
|
||||
if (isset($decoded['withdraw_fields']) && is_array($decoded['withdraw_fields'])) {
|
||||
$out['withdraw_fields'] = array_replace($out['withdraw_fields'], array_intersect_key($decoded['withdraw_fields'], $out['withdraw_fields']));
|
||||
}
|
||||
|
||||
return self::normalizePayload($out, $legacyRates);
|
||||
}
|
||||
|
||||
public static function encodeForDb(array $payload): string
|
||||
{
|
||||
$legacyRates = [];
|
||||
if (isset($payload['rates']) && is_array($payload['rates'])) {
|
||||
$legacyRates = array_values($payload['rates']);
|
||||
}
|
||||
$normalized = self::normalizePayload($payload, $legacyRates);
|
||||
$normalized['channels'] = DepositChannel::prepareOverridesForSave(
|
||||
DepositChannel::expandRowsForAdmin($normalized['channels'] ?? [])
|
||||
);
|
||||
self::validate($normalized);
|
||||
|
||||
return json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $p
|
||||
* @param list<array<string, mixed>> $legacyRates
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function normalizePayload(array $p, array $legacyRates): array
|
||||
{
|
||||
$defaults = self::defaultPayload();
|
||||
$out = array_replace($defaults, $p);
|
||||
unset($out['rates'], $out['fx_pairs']);
|
||||
|
||||
$withdrawFromLegacyRates = [];
|
||||
foreach ($legacyRates as $r) {
|
||||
if (!is_array($r)) {
|
||||
continue;
|
||||
}
|
||||
$cur = isset($r['currency']) && is_string($r['currency']) ? strtoupper(trim($r['currency'])) : '';
|
||||
$ratio = self::ratioStringFromRow($r, 'diamonds_per_fiat_unit');
|
||||
if ($cur !== '' && $ratio !== '') {
|
||||
$withdrawFromLegacyRates[$cur] = $ratio;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($out['platform_coin']) && is_array($out['platform_coin'])) {
|
||||
$out['platform_coin'] = array_replace($defaults['platform_coin'], $out['platform_coin']);
|
||||
}
|
||||
if (isset($out['currencies']) && is_array($out['currencies'])) {
|
||||
foreach ($out['currencies'] as $i => $row) {
|
||||
if (!is_array($row)) {
|
||||
unset($out['currencies'][$i]);
|
||||
continue;
|
||||
}
|
||||
$code = isset($row['code']) && is_string($row['code']) ? strtoupper(trim($row['code'])) : '';
|
||||
$dep = self::ratioStringFromRow($row, 'deposit_coins_per_fiat');
|
||||
$wdr = self::ratioStringFromRow($row, 'withdraw_coins_per_fiat');
|
||||
if ($wdr === '') {
|
||||
$wdr = self::ratioStringFromRow($row, 'diamonds_per_fiat_unit');
|
||||
}
|
||||
if ($wdr === '' && $code !== '' && isset($withdrawFromLegacyRates[$code])) {
|
||||
$wdr = $withdrawFromLegacyRates[$code];
|
||||
}
|
||||
if ($dep === '' && $wdr !== '') {
|
||||
$dep = $wdr;
|
||||
}
|
||||
if ($wdr === '' && $dep !== '') {
|
||||
$wdr = $dep;
|
||||
}
|
||||
if ($code !== '' && $dep === '' && $wdr === '') {
|
||||
$dep = '100';
|
||||
$wdr = '100';
|
||||
}
|
||||
$out['currencies'][$i] = [
|
||||
'code' => $code,
|
||||
'label_zh' => isset($row['label_zh']) && is_string($row['label_zh']) ? trim($row['label_zh']) : '',
|
||||
'label_en' => isset($row['label_en']) && is_string($row['label_en']) ? trim($row['label_en']) : '',
|
||||
'sort' => self::normalizeSort($row['sort'] ?? 0),
|
||||
'deposit_coins_per_fiat' => $dep,
|
||||
'withdraw_coins_per_fiat' => $wdr,
|
||||
];
|
||||
}
|
||||
$out['currencies'] = array_values(array_filter($out['currencies'], static fn ($r) => is_array($r) && $r['code'] !== ''));
|
||||
}
|
||||
|
||||
usort($out['currencies'], static function (array $a, array $b): int {
|
||||
$sa = $a['sort'] ?? 0;
|
||||
$sb = $b['sort'] ?? 0;
|
||||
if ($sa !== $sb) {
|
||||
return $sa <=> $sb;
|
||||
}
|
||||
$ca = $a['code'] ?? '';
|
||||
$cb = $b['code'] ?? '';
|
||||
|
||||
return strcmp($ca, $cb);
|
||||
});
|
||||
|
||||
if (isset($out['withdraw_banks']) && is_array($out['withdraw_banks'])) {
|
||||
foreach ($out['withdraw_banks'] as $i => $row) {
|
||||
if (!is_array($row)) {
|
||||
unset($out['withdraw_banks'][$i]);
|
||||
continue;
|
||||
}
|
||||
$code = isset($row['code']) && is_string($row['code']) ? strtolower(trim($row['code'])) : '';
|
||||
$out['withdraw_banks'][$i] = [
|
||||
'code' => $code,
|
||||
'name_zh' => isset($row['name_zh']) && is_string($row['name_zh']) ? trim($row['name_zh']) : '',
|
||||
'name_en' => isset($row['name_en']) && is_string($row['name_en']) ? trim($row['name_en']) : '',
|
||||
'sort' => self::normalizeSort($row['sort'] ?? 0),
|
||||
];
|
||||
}
|
||||
$out['withdraw_banks'] = array_values(array_filter($out['withdraw_banks'], static fn ($r) => is_array($r) && $r['code'] !== ''));
|
||||
usort($out['withdraw_banks'], static function (array $a, array $b): int {
|
||||
$sa = $a['sort'] ?? 0;
|
||||
$sb = $b['sort'] ?? 0;
|
||||
if ($sa !== $sb) {
|
||||
return $sa <=> $sb;
|
||||
}
|
||||
$ca = $a['code'] ?? '';
|
||||
$cb = $b['code'] ?? '';
|
||||
|
||||
return strcmp($ca, $cb);
|
||||
});
|
||||
}
|
||||
if (isset($out['withdraw_limits']) && is_array($out['withdraw_limits'])) {
|
||||
$wl = array_replace($defaults['withdraw_limits'], $out['withdraw_limits']);
|
||||
foreach (['min_ewallet', 'min_bank'] as $k) {
|
||||
if (isset($wl[$k]) && is_numeric($wl[$k]) && !is_string($wl[$k])) {
|
||||
$wl[$k] = strval($wl[$k]);
|
||||
}
|
||||
if (!isset($wl[$k]) || !is_string($wl[$k])) {
|
||||
$wl[$k] = $defaults['withdraw_limits'][$k];
|
||||
}
|
||||
}
|
||||
$out['withdraw_limits'] = $wl;
|
||||
}
|
||||
if (isset($out['withdraw_copy']) && is_array($out['withdraw_copy'])) {
|
||||
$out['withdraw_copy'] = array_replace($defaults['withdraw_copy'], array_intersect_key($out['withdraw_copy'], $defaults['withdraw_copy']));
|
||||
$mode = isset($out['withdraw_copy']['rate_mode']) && is_string($out['withdraw_copy']['rate_mode'])
|
||||
? strtolower(trim($out['withdraw_copy']['rate_mode']))
|
||||
: 'fixed';
|
||||
if (!in_array($mode, ['fixed', 'live'], true)) {
|
||||
$mode = 'fixed';
|
||||
}
|
||||
$out['withdraw_copy']['rate_mode'] = $mode;
|
||||
}
|
||||
if (isset($out['withdraw_fields']) && is_array($out['withdraw_fields'])) {
|
||||
$wf = array_replace($defaults['withdraw_fields'], array_intersect_key($out['withdraw_fields'], $defaults['withdraw_fields']));
|
||||
foreach (array_keys($defaults['withdraw_fields']) as $fk) {
|
||||
$wf[$fk] = !empty($wf[$fk]);
|
||||
}
|
||||
$out['withdraw_fields'] = $wf;
|
||||
}
|
||||
if (isset($out['channels']) && is_array($out['channels'])) {
|
||||
$out['channels'] = DepositChannel::normalizeOverrides(array_values($out['channels']));
|
||||
} else {
|
||||
$out['channels'] = [];
|
||||
}
|
||||
usort($out['channels'], static function (array $a, array $b): int {
|
||||
$sa = $a['sort'] ?? 0;
|
||||
$sb = $b['sort'] ?? 0;
|
||||
if ($sa !== $sb) {
|
||||
return $sa <=> $sb;
|
||||
}
|
||||
|
||||
return strcmp($a['code'] ?? '', $b['code'] ?? '');
|
||||
});
|
||||
|
||||
unset($out['fx_pairs']);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private static function ratioStringFromRow(array $row, string $key): string
|
||||
{
|
||||
if (!isset($row[$key])) {
|
||||
return '';
|
||||
}
|
||||
if (is_string($row[$key])) {
|
||||
return trim($row[$key]);
|
||||
}
|
||||
if (is_numeric($row[$key])) {
|
||||
return strval($row[$key]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function normalizeSort(mixed $v): int
|
||||
{
|
||||
$opt = filter_var($v, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 99999]]);
|
||||
if ($opt !== false) {
|
||||
return $opt;
|
||||
}
|
||||
if (is_numeric($v)) {
|
||||
$f = filter_var($v, FILTER_VALIDATE_FLOAT);
|
||||
if ($f !== false) {
|
||||
$rounded = round($f);
|
||||
$opt2 = filter_var($rounded, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 99999]]);
|
||||
if ($opt2 !== false) {
|
||||
return $opt2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $p
|
||||
*/
|
||||
private static function validate(array $p): void
|
||||
{
|
||||
if (!isset($p['platform_coin']) || !is_array($p['platform_coin'])) {
|
||||
throw new InvalidArgumentException('platform_coin 格式错误');
|
||||
}
|
||||
$lz = $p['platform_coin']['label_zh'] ?? '';
|
||||
$le = $p['platform_coin']['label_en'] ?? '';
|
||||
if (!is_string($lz) || trim($lz) === '' || !is_string($le) || trim($le) === '') {
|
||||
throw new InvalidArgumentException('请填写平台币中英文名称');
|
||||
}
|
||||
if (!isset($p['currencies']) || !is_array($p['currencies']) || $p['currencies'] === []) {
|
||||
throw new InvalidArgumentException('至少保留一条货币');
|
||||
}
|
||||
$seenCodes = [];
|
||||
foreach ($p['currencies'] as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
throw new InvalidArgumentException('货币列表第 ' . ($idx + 1) . ' 行格式错误');
|
||||
}
|
||||
$code = $row['code'] ?? '';
|
||||
if (!is_string($code) || !preg_match('/^[A-Z0-9]{2,12}$/', $code)) {
|
||||
throw new InvalidArgumentException('货币代码非法:' . (is_string($code) ? $code : ''));
|
||||
}
|
||||
if (isset($seenCodes[$code])) {
|
||||
throw new InvalidArgumentException('货币代码不能重复:' . $code);
|
||||
}
|
||||
$seenCodes[$code] = true;
|
||||
$dep = $row['deposit_coins_per_fiat'] ?? '';
|
||||
$wdr = $row['withdraw_coins_per_fiat'] ?? '';
|
||||
if (!is_string($dep) || $dep === '' || !is_numeric($dep) || bccomp($dep, '0', 8) <= 0) {
|
||||
throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行:充值汇率须为大于 0 的数字');
|
||||
}
|
||||
if (!is_string($wdr) || $wdr === '' || !is_numeric($wdr) || bccomp($wdr, '0', 8) <= 0) {
|
||||
throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行:提现汇率须为大于 0 的数字');
|
||||
}
|
||||
}
|
||||
if (isset($p['withdraw_banks']) && is_array($p['withdraw_banks'])) {
|
||||
$seen = [];
|
||||
foreach ($p['withdraw_banks'] as $idx => $row) {
|
||||
if (!is_array($row)) {
|
||||
throw new InvalidArgumentException('银行第 ' . ($idx + 1) . ' 行格式错误');
|
||||
}
|
||||
$code = $row['code'] ?? '';
|
||||
if (!is_string($code) || !preg_match('/^[a-z0-9][a-z0-9_\-]{0,31}$/', $code)) {
|
||||
throw new InvalidArgumentException('银行代码非法');
|
||||
}
|
||||
if (isset($seen[$code])) {
|
||||
throw new InvalidArgumentException('银行代码重复:' . $code);
|
||||
}
|
||||
$seen[$code] = true;
|
||||
}
|
||||
}
|
||||
if (isset($p['withdraw_limits']) && is_array($p['withdraw_limits'])) {
|
||||
foreach (['min_ewallet', 'min_bank'] as $k) {
|
||||
$v = $p['withdraw_limits'][$k] ?? '0';
|
||||
if (!is_string($v) || !is_numeric($v) || bccomp($v, '0', 4) < 0) {
|
||||
throw new InvalidArgumentException('提现最低限额须为不小于 0 的数字');
|
||||
}
|
||||
}
|
||||
}
|
||||
$reg = DepositChannel::codeRegistry();
|
||||
if ($reg !== [] && (!isset($p['channels']) || !is_array($p['channels']) || $p['channels'] === [])) {
|
||||
throw new InvalidArgumentException('请配置充值渠道');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,7 @@ Route::add(['GET', 'POST'], '/api/finance/depositTierList', [\app\api\controller
|
||||
Route::post('/api/finance/depositCreate', [\app\api\controller\Finance::class, 'depositCreate']);
|
||||
Route::add(['GET', 'POST'], '/api/finance/depositDetail', [\app\api\controller\Finance::class, 'depositDetail']);
|
||||
Route::add(['GET', 'POST'], '/api/finance/depositList', [\app\api\controller\Finance::class, 'depositList']);
|
||||
Route::add(['GET', 'POST'], '/api/finance/cashierConfig', [\app\api\controller\Finance::class, 'cashierConfig']);
|
||||
Route::post('/api/finance/withdrawCreate', [\app\api\controller\Finance::class, 'withdrawCreate']);
|
||||
Route::add(['GET', 'POST'], '/api/finance/withdrawDetail', [\app\api\controller\Finance::class, 'withdrawDetail']);
|
||||
Route::add(['GET', 'POST'], '/api/finance/withdrawList', [\app\api\controller\Finance::class, 'withdrawList']);
|
||||
|
||||
@@ -269,6 +269,16 @@
|
||||
| 提现 | 有效投注 ≥ 总充值×配置倍数;最低提现与 0.5% 手续费;大额 / Jackpot 进审核 |
|
||||
| 灾难恢复 | 模拟期卡在「计算中」,启动退本流程:期作废、本金退回账本 |
|
||||
|
||||
#### 8.2.1 后台「游戏实时对局」:运行开关与作废
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 配置键 | `game_config.config_key = game_live_runtime_enabled`:`1` = 运行(默认),`0` = 维护/关闭 |
|
||||
| 关闭时服务端行为 | **禁止下注**(移动端 **`betPlace`** 返回 `3001`);**无进行中局时**定时 **`GamePeriodAutoTicker`** 不创建新期;派彩宽限期结束后的 **`createNextRecordAfterDraw`** 不插入下一期。**当局仍继续**:**`tickAutoDraw` / `drawResult` / 结算**照常执行,直至本期结束。后台 **`POST /admin/game.Live/runtime` 且 `enabled=1`** 时,若 **`hasActiveRecord()` 为假**,则 **`bootstrapPeriodWhenRuntimeEnabled`** 立即插入新一期 |
|
||||
| 后台快照 `maintenance_ui` | `!game_live_runtime_enabled && !hasActiveRecord()` 时为 `true`:表示**当局已完全结束**(含派彩),此时后台页展示「维护中」并锁定操作区(仅顶部「游戏运行」开关可用);当局尚在 0–3 状态时为 `false`,倒计时与操作区保持可用直至收尾完成 |
|
||||
| 作废本局(后台按钮) | 仅当本期为 **下注开放 / 已封盘**(`game_record.status` 为 `0` 或 `1`):将所有 **待开奖** 注单(`bet_order.status = 1`)按 `total_amount` 退回余额并置 `bet_order.status = 3`,写钱包流水 `biz_type = bet_void`;本期 **`game_record.status = 5`**,写入 **`void_reason`**;并 **强制将 `game_live_runtime_enabled` 置为 `0`**。需管理员在后台再次打开「游戏运行」开关后,才恢复下注与后续期次创建 |
|
||||
| 后台接口 | `POST /admin/game.Live/runtime`(body:`enabled` 0/1);`POST /admin/game.Live/voidPeriod`(body:`record_id` 可选、`void_reason` 必填) |
|
||||
|
||||
### 8.3 代理与结算(逻辑就绪后)
|
||||
|
||||
1. 构造多代理树与多用户下注,跑一期结算脚本或单元测试纯算法。
|
||||
|
||||
@@ -222,9 +222,10 @@
|
||||
|
||||
返回参数:
|
||||
- `server_time`:int(含义:服务端当前时间,用于客户端校时)
|
||||
- `runtime_enabled`:bool(含义:**游戏运行开关**;`false` 时表示后台维护——**禁止下注**,且 idle 时不会自动创建新期、派彩结束后也不会自动创建下一期;**当前已开盘的局仍会开奖、派彩并结算**。移动端应禁用下注入口并提示「维护」类文案)
|
||||
- `period`:object
|
||||
- `period_no`:string(含义:当前全局期号)
|
||||
- `status`:string(`betting`/`locked`/`settling`/`finished`,含义:当前期状态)
|
||||
- `status`:string(`betting`/`locked`/`settling`/`finished`/`void`,含义:当前期状态;`void` 表示该期已作废)
|
||||
- `countdown`:int(含义:当前期倒计时秒数)
|
||||
- `lock_at`:int(含义:封盘时间戳)
|
||||
- `open_at`:int(含义:预计开奖时间戳)
|
||||
@@ -266,9 +267,10 @@
|
||||
- **POST** `/api/game/periodCurrent`
|
||||
|
||||
返回参数:
|
||||
- `runtime_enabled`:bool(含义:同 `lobbyInit.runtime_enabled`)
|
||||
- `period_id`:int(含义:当前期主键 ID)
|
||||
- `period_no`:string(含义:当前期号)
|
||||
- `status`:string(含义:当前期状态)
|
||||
- `status`:string(含义:当前期状态,含 `void` 已作废)
|
||||
- `countdown`:int(含义:当前期剩余秒数)
|
||||
- `bet_close_in`:int(含义:距离封盘剩余秒数)
|
||||
- `result_number`:int/null(未开奖为 null,含义:开奖号码)
|
||||
@@ -293,6 +295,7 @@
|
||||
|
||||
**可能错误码(补充)**(其余见文档头部错误码分段;扣款与缓存一致性强相关):
|
||||
|
||||
- `3001`:游戏已暂停(`runtime_enabled=false`,后台「游戏实时对局」关闭运行开关或作废本局后未重新开启;与非法流程类错误同段)
|
||||
- `5000`:系统繁忙;或 **用户 Redis 互斥锁**未获取(与后台钱包/并发写同一用户串行,文案与后台一致:「该用户正在被其他管理员操作(钱包/并发保存),请稍后再试」);或 **`coin` 条件更新**未命中(并发下注/派彩/后台已改余额:「扣款失败:该用户余额已被其他请求修改(如下注、派彩或其他管理员已保存),请刷新后重试」)。
|
||||
|
||||
> 说明:一键重复上一注、自动托管开启/停止均由前端控制,客户端在相应时机调用 `/api/game/betPlace` 即可完成,不再提供独立接口。
|
||||
@@ -359,6 +362,7 @@
|
||||
- `bet`:下注扣款(提交下注时从玩家余额扣除的投注金额)
|
||||
- `payout`:开奖派彩(中奖后系统将奖金入账到玩家余额)
|
||||
- `adjust`:人工调整(后台管理员加/扣点,对应 `biz_type=admin_credit/admin_deduct`)
|
||||
- `bet_void`:期次作废退款(后台「游戏实时对局」作废本局时,退回待开奖注单本金)
|
||||
|
||||
返回参数:
|
||||
- `list`:array<object>
|
||||
|
||||
16
web/src/lang/backend/en/config/depositChannel.ts
Normal file
16
web/src/lang/backend/en/config/depositChannel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
desc: 'Pay channels are registered in code and environment variables (display name, code). Here you only toggle availability, sort order, and which deposit tiers each channel accepts. Leave tiers empty to allow all enabled tiers. Changes apply immediately.',
|
||||
btn_save: 'Save',
|
||||
sort: 'Sort',
|
||||
status: 'Enabled',
|
||||
code: 'Channel code',
|
||||
display_name: 'Display name (read-only)',
|
||||
tier_ids: 'Allowed tier ids',
|
||||
tier_ids_ph: 'Empty = all tiers',
|
||||
'quick Search Fields': 'channel code / display name',
|
||||
form_tip: 'Display names come from the code/env registry and cannot be edited here; adjust sort, status, and allowed tiers.',
|
||||
status_on: 'Enabled',
|
||||
status_off: 'Disabled',
|
||||
tier_all: 'All tiers',
|
||||
rule_sort: 'Sort value is required',
|
||||
}
|
||||
@@ -1,29 +1,47 @@
|
||||
export default {
|
||||
title: 'Deposit Tiers',
|
||||
desc: 'Configure the deposit tiers players can pick when creating a deposit order. In the third-party payment mode, only tier specs (name, amount, bonus, description) are maintained; receiving accounts are no longer stored here. Maintain both Chinese and English text for the title and description: the mobile API returns the language matching the request `lang` header, falling back to Chinese if English is blank. Changes take effect immediately.',
|
||||
desc: 'Configure mobile deposit tiers: titles (ZH/EN), pay currency & pay amount, credited platform coins (base + bonus), and stable tier id (tier_key) for orders and channel binding. Disabled tiers are hidden from players. Changes apply immediately.',
|
||||
'quick Search Fields': 'tier id / title (ZH) / title (EN)',
|
||||
form_tip: 'The table is read-only; use toolbar Add / Edit in the dialog. Changes apply immediately.',
|
||||
tier_id_optional: 'Tier id (optional)',
|
||||
tier_id_optional_ph: 'Leave empty to auto-generate',
|
||||
status_on: 'Enabled',
|
||||
status_off: 'Disabled',
|
||||
total_platform: 'Total credit (base + bonus)',
|
||||
rule_title: 'Title (ZH) is required',
|
||||
rule_currency: 'Pay currency is required',
|
||||
rule_pay_amount: 'Pay amount must be a number greater than 0',
|
||||
rule_platform_base: 'Base platform coin must be a number greater than 0',
|
||||
rule_bonus: 'Bonus must be a number no less than 0',
|
||||
btn_add: 'Add Tier',
|
||||
btn_save: 'Save',
|
||||
btn_remove: 'Delete',
|
||||
confirm_remove: 'Delete this deposit tier?',
|
||||
tier_id: 'Tier ID',
|
||||
tier_id: 'Tier key',
|
||||
auto_id: '(generated on save)',
|
||||
sort: 'Sort',
|
||||
status: 'Enabled',
|
||||
title_col: 'Title (ZH)',
|
||||
title_ph: 'e.g. 新手首充、VIP 高额充值',
|
||||
title_ph: 'e.g. First recharge, VIP top-up bundle',
|
||||
title_en_col: 'Title (EN)',
|
||||
title_en_ph: 'e.g. Starter Pack, VIP Recharge',
|
||||
amount: 'Amount',
|
||||
amount_ph: 'e.g. 100.00',
|
||||
bonus_amount: 'Bonus',
|
||||
pay_currency: 'Pay currency',
|
||||
pay_currency_ph: 'Select or type',
|
||||
pay_amount: 'Pay amount (fiat)',
|
||||
pay_amount_ph: 'e.g. 3.00',
|
||||
platform_base: 'Base platform coin',
|
||||
platform_base_ph: 'e.g. 210.00',
|
||||
bonus_amount: 'Bonus platform coin',
|
||||
bonus_ph: 'e.g. 20.00, use 0 if none',
|
||||
platform_coin_suffix: 'coin',
|
||||
desc_col: 'Description (ZH)',
|
||||
desc_ph: 'Optional Chinese description, up to 255 chars',
|
||||
desc_ph: 'Optional description for Chinese locale, up to 255 characters',
|
||||
desc_en_col: 'Description (EN)',
|
||||
desc_en_ph: 'Optional English description, up to 255 chars',
|
||||
currency: '',
|
||||
operate: 'Action',
|
||||
err_title: 'Row {no}: Chinese title is required',
|
||||
err_amount: 'Row {no}: amount must be a number greater than 0',
|
||||
err_title: 'Row {no}: Title (ZH) is required',
|
||||
err_currency: 'Row {no}: pay currency is required',
|
||||
err_pay_amount: 'Row {no}: pay amount must be a number greater than 0',
|
||||
err_platform_base: 'Row {no}: base platform coin must be a number greater than 0',
|
||||
err_bonus: 'Row {no}: bonus must be a number no less than 0',
|
||||
}
|
||||
|
||||
50
web/src/lang/backend/en/config/financeCashierConfig.ts
Normal file
50
web/src/lang/backend/en/config/financeCashierConfig.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export default {
|
||||
desc: 'Mobile pay & receipt settings: platform coin labels, currencies and rates, deposit pay channels (on/off, sort, tier scope), withdraw banks, limits, copy, and required withdraw fields. Deposit tiers are configured separately.',
|
||||
btn_save: 'Save',
|
||||
btn_add_row: 'Add row',
|
||||
sec_platform: 'Platform coin labels',
|
||||
platform_label_zh: 'Label (Chinese)',
|
||||
platform_label_en: 'Label (English)',
|
||||
sec_currencies: 'Currencies (deposit/withdraw selectors)',
|
||||
sec_deposit_channels: 'Deposit pay channels',
|
||||
deposit_channels_hint: 'Display names come from the registry; here you only set enabled state, sort order, and applicable deposit tiers. Leave tiers empty to allow all tiers.',
|
||||
currency_rates_hint: 'Deposit rate: platform coins credited per 1 fiat paid. Withdraw rate: platform coins needed per 1 fiat redeemed (e.g. 100 ⇒ 100 coins = 1 fiat unit).',
|
||||
err_dup_code: 'Duplicate currency codes are not allowed.',
|
||||
sec_banks: 'Withdraw bank codes',
|
||||
sec_limits: 'Minimum withdraw (fiat amount; match your copy currency)',
|
||||
min_ewallet: 'E-wallet minimum',
|
||||
min_bank: 'Bank minimum',
|
||||
sec_copy: 'Withdraw page copy',
|
||||
rate_mode: 'Rate display',
|
||||
rate_mode_fixed: 'Fixed rate',
|
||||
rate_mode_live: 'Live rate (display)',
|
||||
rate_hint_zh: 'Rate hint (ZH)',
|
||||
rate_hint_en: 'Rate hint (EN)',
|
||||
processing_zh: 'Processing note (ZH)',
|
||||
processing_en: 'Processing note (EN)',
|
||||
fee_note_zh: 'Fee note (ZH)',
|
||||
fee_note_en: 'Fee note (EN)',
|
||||
sec_fields: 'Withdraw form (required)',
|
||||
field_cardholder: 'Cardholder name',
|
||||
field_bank_account: 'Bank account',
|
||||
field_email: 'Payee email',
|
||||
field_mobile: 'Payee mobile',
|
||||
col_code: 'Code',
|
||||
col_label_zh: 'Name (ZH)',
|
||||
col_label_en: 'Name (EN)',
|
||||
col_sort: 'Sort',
|
||||
col_deposit_rate: 'Deposit rate',
|
||||
col_withdraw_rate: 'Withdraw rate',
|
||||
col_bank_code: 'Bank code',
|
||||
col_name_zh: 'Bank (ZH)',
|
||||
col_name_en: 'Bank (EN)',
|
||||
ph_ratio: 'e.g. 100',
|
||||
ph_currency_code: 'e.g. MYR',
|
||||
ph_bank_code: 'e.g. agrobank',
|
||||
ch_code: 'Channel code',
|
||||
ch_display_name: 'Display name',
|
||||
ch_sort: 'Sort',
|
||||
ch_status: 'Enabled',
|
||||
ch_tier_ids: 'Allowed deposit tiers',
|
||||
ch_tier_ids_ph: 'Empty = all tiers',
|
||||
}
|
||||
3
web/src/lang/backend/en/order/depositChannelOrder.ts
Normal file
3
web/src/lang/backend/en/order/depositChannelOrder.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
'quick Search Fields': 'order no / pay channel / tier id / idempotency key',
|
||||
}
|
||||
16
web/src/lang/backend/zh-cn/config/depositChannel.ts
Normal file
16
web/src/lang/backend/zh-cn/config/depositChannel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
desc: '支付渠道在代码与环境变量中注册(展示名、code);此处仅配置开关、排序,以及各渠道允许的充值档位(唯一标识 tier id)。留空「适用档位」表示不限制。修改后立即生效。',
|
||||
btn_save: '保存',
|
||||
sort: '排序',
|
||||
status: '启用',
|
||||
code: '渠道代码',
|
||||
display_name: '展示名称(只读)',
|
||||
tier_ids: '适用充值档位',
|
||||
tier_ids_ph: '空=全部档位',
|
||||
'quick Search Fields': '渠道代码 / 展示名称',
|
||||
form_tip: '展示名由代码与环境注册表决定,此处不可改;可调整排序、开关与适用档位。',
|
||||
status_on: '启用',
|
||||
status_off: '停用',
|
||||
tier_all: '全部档位',
|
||||
rule_sort: '请填写排序值',
|
||||
}
|
||||
@@ -1,11 +1,23 @@
|
||||
export default {
|
||||
title: '充值档位',
|
||||
desc: '配置玩家创建充值订单时可选的充值档位。第三方支付模式下仅需维护档位规格:名称、充值金额、赠送金额、描述等;不再保存收款账户信息。充值名称/描述需分别维护中英文两套:移动端接口会根据请求头 `lang` 返回对应语言,英文缺省时回退到中文。修改后立即生效。',
|
||||
desc: '配置移动端充值档位:充值名称(中英文)、支付货币与支付额度、到账平台币(基础+赠送)、唯一档位标识(id/tier_key)用于下单与渠道绑定。关闭「启用」后玩家不可选该档。修改后立即生效。',
|
||||
'quick Search Fields': '唯一标识 / 中文名 / 英文名',
|
||||
form_tip: '列表为只读展示;请使用工具栏「添加 / 编辑」在弹窗中维护。保存后立即生效。',
|
||||
tier_id_optional: '唯一标识(可选)',
|
||||
tier_id_optional_ph: '留空则由系统自动生成',
|
||||
status_on: '启用',
|
||||
status_off: '停用',
|
||||
total_platform: '到账合计(基础+赠送)',
|
||||
rule_title: '请填写中文充值名称',
|
||||
rule_currency: '请选择或填写支付货币',
|
||||
rule_pay_amount: '支付货币额度须为大于 0 的数字',
|
||||
rule_platform_base: '基础平台币须为大于 0 的数字',
|
||||
rule_bonus: '赠送平台币须为不小于 0 的数字',
|
||||
btn_add: '新增档位',
|
||||
btn_save: '保存',
|
||||
btn_remove: '删除',
|
||||
confirm_remove: '确定删除该充值档位?',
|
||||
tier_id: '档位 ID',
|
||||
tier_id: '唯一标识',
|
||||
auto_id: '(保存时生成)',
|
||||
sort: '排序',
|
||||
status: '启用',
|
||||
@@ -13,17 +25,23 @@ export default {
|
||||
title_ph: '例如:新手首充、VIP 高额充值',
|
||||
title_en_col: '充值名称(英文)',
|
||||
title_en_ph: 'e.g. Starter Pack, VIP Recharge',
|
||||
amount: '充值金额',
|
||||
amount_ph: '例如:100.00',
|
||||
bonus_amount: '赠送金额',
|
||||
pay_currency: '支付货币',
|
||||
pay_currency_ph: '选择或输入',
|
||||
pay_amount: '支付货币额度',
|
||||
pay_amount_ph: '例如:3.00',
|
||||
platform_base: '到账平台币(基础)',
|
||||
platform_base_ph: '例如:210.00',
|
||||
bonus_amount: '赠送平台币',
|
||||
bonus_ph: '例如:20.00,无赠送填 0',
|
||||
platform_coin_suffix: '币',
|
||||
desc_col: '描述(中文)',
|
||||
desc_ph: '可选,展示给中文玩家的档位说明,最长 255 字',
|
||||
desc_en_col: '描述(英文)',
|
||||
desc_en_ph: 'Optional English description for EN players, up to 255 chars',
|
||||
currency: '币',
|
||||
operate: '操作',
|
||||
err_title: '第 {no} 行:中文充值名称不能为空',
|
||||
err_amount: '第 {no} 行:充值金额必须为大于 0 的数字',
|
||||
err_bonus: '第 {no} 行:赠送金额必须为不小于 0 的数字',
|
||||
err_currency: '第 {no} 行:请选择支付货币',
|
||||
err_pay_amount: '第 {no} 行:支付货币额度必须为大于 0 的数字',
|
||||
err_platform_base: '第 {no} 行:基础平台币到账必须为大于 0 的数字',
|
||||
err_bonus: '第 {no} 行:赠送平台币必须为不小于 0 的数字',
|
||||
}
|
||||
|
||||
51
web/src/lang/backend/zh-cn/config/financeCashierConfig.ts
Normal file
51
web/src/lang/backend/zh-cn/config/financeCashierConfig.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export default {
|
||||
desc: '配置移动端支付与收款展示:平台币名称、货币与汇率、充值支付渠道(开关/排序/适用档位)、提现银行、最低限额、文案与提现表单字段。充值档位在「充值档位」中维护。',
|
||||
btn_save: '保存',
|
||||
btn_add_row: '新增一行',
|
||||
sec_platform: '平台币展示名',
|
||||
platform_label_zh: '名称(中文)',
|
||||
platform_label_en: '名称(英文)',
|
||||
sec_currencies: '货币列表(充值/提现货币下拉)',
|
||||
sec_deposit_channels: '充值支付渠道',
|
||||
deposit_channels_hint: '展示名由环境注册表决定,此处仅维护启用状态、排序与适用充值档位;不选档位表示全部档位可用。',
|
||||
currency_rates_hint: '充值汇率:每支付 1 单位该货币到账的平台币;提现汇率:每兑换 1 单位该货币所需平台币(例 100 表示 100 平台币 = 1 单位)。',
|
||||
err_dup_code: '货币代码不能重复,请检查后再保存。',
|
||||
sec_banks: '提现支持银行代码',
|
||||
sec_limits: '提现最低限额(法币金额,与文案中币种一致)',
|
||||
min_ewallet: '电子钱包最低',
|
||||
min_bank: '银行最低',
|
||||
sec_copy: '提现页文案',
|
||||
rate_mode: '汇率展示',
|
||||
rate_mode_fixed: '固定汇率',
|
||||
rate_mode_live: '实时汇率(展示用)',
|
||||
rate_hint_zh: '汇率提示(中文)',
|
||||
rate_hint_en: '汇率提示(英文)',
|
||||
processing_zh: '到账说明(中文)',
|
||||
processing_en: '到账说明(英文)',
|
||||
fee_note_zh: '手续费说明(中文)',
|
||||
fee_note_en: '手续费说明(英文)',
|
||||
sec_fields: '提现表单字段(必填)',
|
||||
field_cardholder: '持卡人姓名',
|
||||
field_bank_account: '银行账号',
|
||||
field_email: '收款邮箱',
|
||||
field_mobile: '收款手机',
|
||||
col_code: '代码',
|
||||
col_label_zh: '中文名',
|
||||
col_label_en: '英文名',
|
||||
col_sort: '排序',
|
||||
col_deposit_rate: '充值汇率',
|
||||
col_withdraw_rate: '提现汇率',
|
||||
col_bank_code: '银行代码',
|
||||
col_name_zh: '银行名(中文)',
|
||||
col_name_en: '银行名(英文)',
|
||||
ph_ratio: '如 100',
|
||||
ph_currency_code: '如 MYR',
|
||||
ph_bank_code: '如 agrobank',
|
||||
/** 本页「充值支付渠道」表格(不依赖 depositChannel 路由语言包) */
|
||||
ch_code: '渠道代码',
|
||||
ch_display_name: '展示名称',
|
||||
ch_sort: '排序',
|
||||
ch_status: '启用',
|
||||
ch_tier_ids: '适用充值档位',
|
||||
ch_tier_ids_ph: '不选表示全部档位',
|
||||
}
|
||||
3
web/src/lang/backend/zh-cn/order/depositChannelOrder.ts
Normal file
3
web/src/lang/backend/zh-cn/order/depositChannelOrder.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
'quick Search Fields': '订单号/支付通道/档位ID/幂等键',
|
||||
}
|
||||
155
web/src/views/backend/config/depositChannel/index.vue
Normal file
155
web/src/views/backend/config/depositChannel/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositChannel.desc') }}
|
||||
</el-alert>
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'edit', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.depositChannel.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/depositChannel',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/config.DepositChannel/'),
|
||||
{
|
||||
pk: 'code',
|
||||
filter: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
},
|
||||
defaultOrder: { prop: 'sort', order: 'asc' },
|
||||
extend: {
|
||||
registry: {} as Record<string, { name: string; name_en: string; sort: number }>,
|
||||
tier_options: [] as { id: string; label: string }[],
|
||||
},
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{
|
||||
label: t('config.depositChannel.sort'),
|
||||
prop: 'sort',
|
||||
align: 'center',
|
||||
width: 88,
|
||||
operator: 'RANGE',
|
||||
sortable: 'custom',
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'switch',
|
||||
replaceValue: { 0: t('config.depositChannel.status_off'), 1: t('config.depositChannel.status_on') },
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.code'),
|
||||
prop: 'code',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.display_name'),
|
||||
prop: 'display_name',
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.tier_ids'),
|
||||
prop: 'tier_ids',
|
||||
align: 'center',
|
||||
minWidth: 240,
|
||||
operator: false,
|
||||
render: 'tags',
|
||||
formatter: (row: anyObj) => {
|
||||
const ids = row.tier_ids
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return [t('config.depositChannel.tier_all')]
|
||||
}
|
||||
const opts = (baTable.table.extend?.tier_options ?? []) as { id: string; label: string }[]
|
||||
return ids.map((id: string) => {
|
||||
const o = opts.find((x) => x.id === id)
|
||||
return o ? o.label : id
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 90,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: {
|
||||
code: '',
|
||||
sort: 10,
|
||||
status: 1,
|
||||
tier_ids: [] as string[],
|
||||
},
|
||||
},
|
||||
{},
|
||||
{
|
||||
getData({ res }) {
|
||||
const d = res.data as
|
||||
| {
|
||||
registry?: Record<string, { name: string; name_en: string; sort: number }>
|
||||
tier_options?: { id: string; label: string }[]
|
||||
}
|
||||
| undefined
|
||||
if (d?.registry) {
|
||||
baTable.table.extend.registry = d.registry
|
||||
}
|
||||
if (d?.tier_options) {
|
||||
baTable.table.extend.tier_options = d.tier_options
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
109
web/src/views/backend/config/depositChannel/popupForm.vue
Normal file
109
web/src/views/backend/config/depositChannel/popupForm.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="baTable.form.operate === 'Edit'"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ t('Edit') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form ba-Edit-form"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="formRules"
|
||||
>
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositChannel.form_tip') }}
|
||||
</el-alert>
|
||||
|
||||
<el-form-item :label="t('config.depositChannel.code')" prop="code">
|
||||
<el-input :model-value="String(baTable.form.items!.code ?? '')" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositChannel.display_name')">
|
||||
<el-input :model-value="registryDisplayName" readonly />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositChannel.sort')" prop="sort">
|
||||
<el-input-number v-model="baTable.form.items!.sort" :min="0" :max="9999" :controls="true" class="w100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositChannel.status')" prop="status">
|
||||
<el-switch v-model="baTable.form.items!.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositChannel.tier_ids')" prop="tier_ids">
|
||||
<el-select
|
||||
v-model="baTable.form.items!.tier_ids"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
filterable
|
||||
clearable
|
||||
class="w100"
|
||||
:placeholder="t('config.depositChannel.tier_ids_ph')"
|
||||
>
|
||||
<el-option v-for="opt in tierOptions" :key="opt.id" :label="opt.label" :value="opt.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" type="primary" @click="baTable.onSubmit(formRef)">
|
||||
{{ t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { computed, inject, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
|
||||
type TierOpt = { id: string; label: string }
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||
const baTable = inject('baTable') as baTable
|
||||
const { t } = useI18n()
|
||||
|
||||
const tierOptions = computed(() => {
|
||||
const raw = baTable.table.extend?.tier_options
|
||||
return Array.isArray(raw) ? (raw as TierOpt[]) : []
|
||||
})
|
||||
|
||||
const registryDisplayName = computed(() => {
|
||||
const code = String(baTable.form.items?.code ?? '')
|
||||
const reg = baTable.table.extend?.registry as Record<string, { name?: string }> | undefined
|
||||
if (!code || !reg || !reg[code]) {
|
||||
return code
|
||||
}
|
||||
const n = reg[code].name
|
||||
return typeof n === 'string' && n !== '' ? n : code
|
||||
})
|
||||
|
||||
const formRules = {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,278 +1,209 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box deposit-tier-page">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositTier.desc') }}
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :disabled="loading" @click="onAdd">
|
||||
<Icon name="el-icon-Plus" />
|
||||
<span class="ml-6">{{ t('config.depositTier.btn_add') }}</span>
|
||||
</el-button>
|
||||
<el-button type="success" :loading="saving" :disabled="loading" @click="onSave">
|
||||
{{ t('config.depositTier.btn_save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.depositTier.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="items"
|
||||
row-key="_rowKey"
|
||||
max-height="720"
|
||||
class="deposit-tier-table"
|
||||
header-align="center"
|
||||
>
|
||||
<el-table-column prop="sort" :label="t('config.depositTier.sort')" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-center">
|
||||
<el-input-number v-model="row.sort" :min="0" :max="9999" :controls="false" style="width: 100%; max-width: 160px" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.status')" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-center">
|
||||
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.title_col')" min-width="180" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.title" maxlength="64" :placeholder="t('config.depositTier.title_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.title_en_col')" min-width="180" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.title_en" maxlength="64" :placeholder="t('config.depositTier.title_en_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.amount')" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.amount" :placeholder="t('config.depositTier.amount_ph')">
|
||||
<template #suffix>
|
||||
<span class="currency">{{ t('config.depositTier.currency') }}</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.bonus_amount')" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.bonus_amount" :placeholder="t('config.depositTier.bonus_ph')">
|
||||
<template #suffix>
|
||||
<span class="currency">{{ t('config.depositTier.currency') }}</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.desc_col')" min-width="220" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.desc" maxlength="255" :autosize="{ minRows: 1, maxRows: 3 }" type="textarea" :placeholder="t('config.depositTier.desc_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.desc_en_col')" min-width="220" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.desc_en" maxlength="255" :autosize="{ minRows: 1, maxRows: 3 }" type="textarea" :placeholder="t('config.depositTier.desc_en_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.tier_id')" width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-text class="tier-id" truncated>{{ row.id || t('config.depositTier.auto_id') }}</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.operate')" width="90" align="center" header-align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="onRemove($index)">
|
||||
{{ t('config.depositTier.btn_remove') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { auth } from '/@/utils/common'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/depositTier',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
type Tier = {
|
||||
id: string
|
||||
title: string
|
||||
title_en: string
|
||||
amount: string
|
||||
bonus_amount: string
|
||||
desc: string
|
||||
desc_en: string
|
||||
sort: number
|
||||
status: number
|
||||
_rowKey?: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const items = ref<Tier[]>([])
|
||||
|
||||
function genRowKey(): string {
|
||||
return 'r_' + Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
|
||||
function emptyTier(): Tier {
|
||||
return {
|
||||
id: '',
|
||||
title: '',
|
||||
title_en: '',
|
||||
amount: '',
|
||||
bonus_amount: '0',
|
||||
desc: '',
|
||||
desc_en: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
_rowKey: genRowKey(),
|
||||
function formatAmount4(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
}
|
||||
const s = String(cellValue).trim().replace(',', '.')
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(cellValue)
|
||||
}
|
||||
return n.toFixed(4)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/config.DepositTier/index',
|
||||
method: 'get',
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
const list = (res.data.items || []) as Tier[]
|
||||
items.value = (Array.isArray(list) ? list : []).map((it) => ({
|
||||
...emptyTier(),
|
||||
...it,
|
||||
_rowKey: genRowKey(),
|
||||
}))
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
function formatPayCell(row: anyObj, _column: any, cellValue: unknown) {
|
||||
const amt = formatAmount4(row, _column, cellValue)
|
||||
const cur = row.currency && String(row.currency).trim() !== '' ? String(row.currency).toUpperCase() : ''
|
||||
if (amt === '-' || cur === '') {
|
||||
return amt
|
||||
}
|
||||
return `${amt} ${cur}`
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
items.value.push(emptyTier())
|
||||
function formatTotalPlatform(row: anyObj) {
|
||||
const a = parseFloat(String(row.amount ?? '0').replace(',', '.'))
|
||||
const b = parseFloat(String(row.bonus_amount ?? '0').replace(',', '.'))
|
||||
const base = Number.isFinite(a) ? a : 0
|
||||
const bonus = Number.isFinite(b) ? b : 0
|
||||
return (base + bonus).toFixed(4)
|
||||
}
|
||||
|
||||
async function onRemove(idx: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm(t('config.depositTier.confirm_remove'), t('Warning'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: t('Delete'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
items.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!auth('save')) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const row = items.value[i]
|
||||
if (!row.title || !row.title.trim()) {
|
||||
ElMessage.warning(t('config.depositTier.err_title', { no: i + 1 }))
|
||||
return
|
||||
}
|
||||
const amount = Number(row.amount)
|
||||
if (!row.amount || Number.isNaN(amount) || amount <= 0) {
|
||||
ElMessage.warning(t('config.depositTier.err_amount', { no: i + 1 }))
|
||||
return
|
||||
}
|
||||
const bonusRaw = row.bonus_amount === '' || row.bonus_amount === null || row.bonus_amount === undefined ? '0' : row.bonus_amount
|
||||
const bonus = Number(bonusRaw)
|
||||
if (Number.isNaN(bonus) || bonus < 0) {
|
||||
ElMessage.warning(t('config.depositTier.err_bonus', { no: i + 1 }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/config.DepositTier/save',
|
||||
method: 'post',
|
||||
data: {
|
||||
items: items.value.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
title_en: r.title_en || '',
|
||||
amount: r.amount,
|
||||
bonus_amount: r.bonus_amount === '' || r.bonus_amount === null || r.bonus_amount === undefined ? '0' : r.bonus_amount,
|
||||
desc: r.desc || '',
|
||||
desc_en: r.desc_en || '',
|
||||
sort: r.sort,
|
||||
status: r.status,
|
||||
})),
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/config.DepositTier/'),
|
||||
{
|
||||
pk: 'id',
|
||||
filter: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
},
|
||||
defaultOrder: { prop: 'sort', order: 'asc' },
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{
|
||||
label: t('config.depositTier.sort'),
|
||||
prop: 'sort',
|
||||
align: 'center',
|
||||
width: 88,
|
||||
operator: 'RANGE',
|
||||
sortable: 'custom',
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
{
|
||||
label: t('config.depositTier.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'switch',
|
||||
replaceValue: { 0: t('config.depositTier.status_off'), 1: t('config.depositTier.status_on') },
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.tier_id'),
|
||||
prop: 'id',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.title_col'),
|
||||
prop: 'title',
|
||||
align: 'center',
|
||||
minWidth: 140,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.title_en_col'),
|
||||
prop: 'title_en',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.pay_currency'),
|
||||
prop: 'currency',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.pay_amount'),
|
||||
prop: 'pay_amount',
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
operator: 'RANGE',
|
||||
formatter: formatPayCell,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.platform_base'),
|
||||
prop: 'amount',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount4,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.bonus_amount'),
|
||||
prop: 'bonus_amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount4,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.total_platform'),
|
||||
prop: '__total_platform',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: false,
|
||||
formatter: (row: anyObj) => formatTotalPlatform(row),
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.desc_col'),
|
||||
prop: 'desc',
|
||||
align: 'center',
|
||||
minWidth: 160,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: {
|
||||
id: '',
|
||||
title: '',
|
||||
title_en: '',
|
||||
currency: 'MYR',
|
||||
pay_amount: '',
|
||||
amount: '',
|
||||
bonus_amount: '0',
|
||||
desc: '',
|
||||
desc_en: '',
|
||||
sort: 10,
|
||||
status: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.deposit-tier-page {
|
||||
.toolbar {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tier-id {
|
||||
display: inline-block;
|
||||
max-width: 120px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.deposit-tier-table {
|
||||
:deep(.el-table__header th),
|
||||
:deep(.el-table__body td) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
152
web/src/views/backend/config/depositTier/popupForm.vue
Normal file
152
web/src/views/backend/config/depositTier/popupForm.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositTier.form_tip') }}
|
||||
</el-alert>
|
||||
|
||||
<el-form-item v-if="baTable.form.operate === 'Edit'" :label="t('config.depositTier.tier_id')" prop="id">
|
||||
<el-input :model-value="String(baTable.form.items!.id ?? '')" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="baTable.form.operate === 'Add'" :label="t('config.depositTier.tier_id_optional')" prop="id">
|
||||
<el-input v-model="baTable.form.items!.id" clearable :placeholder="t('config.depositTier.tier_id_optional_ph')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositTier.sort')" prop="sort">
|
||||
<el-input-number v-model="baTable.form.items!.sort" :min="0" :max="9999" :controls="true" class="w100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.status')" prop="status">
|
||||
<el-switch v-model="baTable.form.items!.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositTier.title_col')" prop="title">
|
||||
<el-input v-model="baTable.form.items!.title" maxlength="64" :placeholder="t('config.depositTier.title_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.title_en_col')" prop="title_en">
|
||||
<el-input v-model="baTable.form.items!.title_en" maxlength="64" :placeholder="t('config.depositTier.title_en_ph')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositTier.pay_currency')" prop="currency">
|
||||
<el-select v-model="baTable.form.items!.currency" filterable allow-create default-first-option class="w100" :placeholder="t('config.depositTier.pay_currency_ph')">
|
||||
<el-option v-for="c in payCurrencies" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.pay_amount')" prop="pay_amount">
|
||||
<el-input v-model="baTable.form.items!.pay_amount" :placeholder="t('config.depositTier.pay_amount_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.platform_base')" prop="amount">
|
||||
<el-input v-model="baTable.form.items!.amount" :placeholder="t('config.depositTier.platform_base_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.bonus_amount')" prop="bonus_amount">
|
||||
<el-input v-model="baTable.form.items!.bonus_amount" :placeholder="t('config.depositTier.bonus_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.desc_col')" prop="desc">
|
||||
<el-input v-model="baTable.form.items!.desc" type="textarea" :rows="2" maxlength="255" :placeholder="t('config.depositTier.desc_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.desc_en_col')" prop="desc_en">
|
||||
<el-input v-model="baTable.form.items!.desc_en" type="textarea" :rows="2" maxlength="255" :placeholder="t('config.depositTier.desc_en_ph')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||
const baTable = inject('baTable') as baTable
|
||||
const { t } = useI18n()
|
||||
|
||||
const payCurrencies = ['MYR', 'CNY', 'USD', 'USDT', 'VND', 'THB', 'SGD', 'IDR']
|
||||
|
||||
function positiveAmountRule(msg: string): FormItemRule {
|
||||
return {
|
||||
validator: (_rule, val, callback) => {
|
||||
const s = val === null || val === undefined ? '' : String(val).trim().replace(',', '.')
|
||||
if (s === '' || !/^-?\d+(\.\d+)?$/.test(s)) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}
|
||||
}
|
||||
|
||||
function nonNegAmountRule(msg: string): FormItemRule {
|
||||
return {
|
||||
validator: (_rule, val, callback) => {
|
||||
const s = val === null || val === undefined || val === '' ? '0' : String(val).trim().replace(',', '.')
|
||||
if (!/^-?\d+(\.\d+)?$/.test(s)) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
title: [{ required: true, message: t('config.depositTier.rule_title'), trigger: 'blur' }],
|
||||
currency: [{ required: true, message: t('config.depositTier.rule_currency'), trigger: 'change' }],
|
||||
pay_amount: [positiveAmountRule(t('config.depositTier.rule_pay_amount'))],
|
||||
amount: [positiveAmountRule(t('config.depositTier.rule_platform_base'))],
|
||||
bonus_amount: [nonNegAmountRule(t('config.depositTier.rule_bonus'))],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
518
web/src/views/backend/config/financeCashierConfig/index.vue
Normal file
518
web/src/views/backend/config/financeCashierConfig/index.vue
Normal file
@@ -0,0 +1,518 @@
|
||||
<template>
|
||||
<div class="default-main finance-cashier-page">
|
||||
<el-alert type="info" :closable="false" show-icon class="mb-3">
|
||||
{{ t('config.financeCashierConfig.desc') }}
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :loading="saving" :disabled="loading" @click="onSave">
|
||||
{{ t('config.financeCashierConfig.btn_save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-scrollbar max-height="calc(100vh - 220px)">
|
||||
<el-form v-loading="loading" label-width="160px" class="finance-cashier-form">
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>{{ t('config.financeCashierConfig.sec_platform') }}</template>
|
||||
<el-form-item :label="t('config.financeCashierConfig.platform_label_zh')">
|
||||
<el-input v-model="form.platform_coin.label_zh" maxlength="32" class="w400" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.platform_label_en')">
|
||||
<el-input v-model="form.platform_coin.label_en" maxlength="32" class="w400" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<span>{{ t('config.financeCashierConfig.sec_currencies') }}</span>
|
||||
<el-button type="primary" link class="ml-2" @click="addCurrency">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
|
||||
</template>
|
||||
<p class="hint">{{ t('config.financeCashierConfig.currency_rates_hint') }}</p>
|
||||
<el-table :data="form.currencies" border stripe size="small">
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_code')" prop="code" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.code"
|
||||
maxlength="12"
|
||||
:placeholder="t('config.financeCashierConfig.ph_currency_code')"
|
||||
@blur="normalizeCurrencyCode(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_label_zh')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.label_zh" maxlength="64" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_label_en')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.label_en" maxlength="64" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_deposit_rate')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.deposit_coins_per_fiat" :placeholder="t('config.financeCashierConfig.ph_ratio')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_withdraw_rate')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.withdraw_coins_per_fiat" :placeholder="t('config.financeCashierConfig.ph_ratio')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_sort')" width="108">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.sort" :min="0" :max="99999" :controls="true" class="w100p" @change="resortCurrenciesInPlace" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('Operate')" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removeRow(form.currencies, $index)">{{ t('Delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>{{ t('config.financeCashierConfig.sec_deposit_channels') }}</template>
|
||||
<p class="hint">{{ t('config.financeCashierConfig.deposit_channels_hint') }}</p>
|
||||
<el-table :data="form.channels" border stripe size="small">
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_code')" prop="code" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input :model-value="String(row.code ?? '')" readonly />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_display_name')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input :model-value="channelDisplayName(String(row.code ?? ''))" readonly />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_sort')" width="108">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.sort"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:controls="true"
|
||||
class="w100p"
|
||||
@change="resortChannelsInPlace"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_status')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_tier_ids')" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.tier_ids"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
filterable
|
||||
clearable
|
||||
class="w100p"
|
||||
:placeholder="t('config.financeCashierConfig.ch_tier_ids_ph')"
|
||||
>
|
||||
<el-option v-for="opt in tierOptions" :key="opt.id" :label="opt.label" :value="opt.id" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<span>{{ t('config.financeCashierConfig.sec_banks') }}</span>
|
||||
<el-button type="primary" link class="ml-2" @click="addBank">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
|
||||
</template>
|
||||
<el-table :data="form.withdraw_banks" border stripe size="small">
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_bank_code')" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.code" maxlength="32" :placeholder="t('config.financeCashierConfig.ph_bank_code')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_name_zh')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name_zh" maxlength="64" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_name_en')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name_en" maxlength="64" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_sort')" width="108">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.sort" :min="0" :max="99999" :controls="true" class="w100p" @change="resortBanksInPlace" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('Operate')" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removeRow(form.withdraw_banks, $index)">{{ t('Delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>{{ t('config.financeCashierConfig.sec_limits') }}</template>
|
||||
<el-form-item :label="t('config.financeCashierConfig.min_ewallet')">
|
||||
<el-input v-model="form.withdraw_limits.min_ewallet" class="w240" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.min_bank')">
|
||||
<el-input v-model="form.withdraw_limits.min_bank" class="w240" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>{{ t('config.financeCashierConfig.sec_copy') }}</template>
|
||||
<el-form-item :label="t('config.financeCashierConfig.rate_mode')">
|
||||
<el-select v-model="form.withdraw_copy.rate_mode" class="w240">
|
||||
<el-option :label="t('config.financeCashierConfig.rate_mode_fixed')" value="fixed" />
|
||||
<el-option :label="t('config.financeCashierConfig.rate_mode_live')" value="live" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.rate_hint_zh')">
|
||||
<el-input v-model="form.withdraw_copy.rate_hint_zh" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.rate_hint_en')">
|
||||
<el-input v-model="form.withdraw_copy.rate_hint_en" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.processing_zh')">
|
||||
<el-input v-model="form.withdraw_copy.processing_zh" maxlength="255" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.processing_en')">
|
||||
<el-input v-model="form.withdraw_copy.processing_en" maxlength="255" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.fee_note_zh')">
|
||||
<el-input v-model="form.withdraw_copy.fee_note_zh" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.fee_note_en')">
|
||||
<el-input v-model="form.withdraw_copy.fee_note_en" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>{{ t('config.financeCashierConfig.sec_fields') }}</template>
|
||||
<el-form-item :label="t('config.financeCashierConfig.field_cardholder')">
|
||||
<el-switch v-model="form.withdraw_fields.require_cardholder" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.field_bank_account')">
|
||||
<el-switch v-model="form.withdraw_fields.require_bank_account" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.field_email')">
|
||||
<el-switch v-model="form.withdraw_fields.require_email" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.field_mobile')">
|
||||
<el-switch v-model="form.withdraw_fields.require_mobile" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { auth } from '/@/utils/common'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/financeCashierConfig',
|
||||
})
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
type CurrencyRow = {
|
||||
code: string
|
||||
label_zh: string
|
||||
label_en: string
|
||||
sort: number
|
||||
deposit_coins_per_fiat: string
|
||||
withdraw_coins_per_fiat: string
|
||||
}
|
||||
type BankRow = { code: string; name_zh: string; name_en: string; sort: number }
|
||||
type ChannelRow = { code: string; sort: number; status: number; tier_ids: string[] }
|
||||
type TierOpt = { id: string; label: string }
|
||||
type RegistryMeta = { name?: string; name_en?: string; sort?: number }
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const registry = ref<Record<string, RegistryMeta>>({})
|
||||
const tierOptions = ref<TierOpt[]>([])
|
||||
|
||||
const form = reactive({
|
||||
platform_coin: { label_zh: '', label_en: '' },
|
||||
currencies: [] as CurrencyRow[],
|
||||
withdraw_banks: [] as BankRow[],
|
||||
withdraw_limits: { min_ewallet: '0', min_bank: '0' },
|
||||
withdraw_copy: {
|
||||
rate_hint_zh: '',
|
||||
rate_hint_en: '',
|
||||
processing_zh: '',
|
||||
processing_en: '',
|
||||
fee_note_zh: '',
|
||||
fee_note_en: '',
|
||||
rate_mode: 'fixed',
|
||||
},
|
||||
withdraw_fields: {
|
||||
require_cardholder: true,
|
||||
require_bank_account: true,
|
||||
require_email: true,
|
||||
require_mobile: true,
|
||||
},
|
||||
channels: [] as ChannelRow[],
|
||||
})
|
||||
|
||||
function rowSortValue(row: { sort?: unknown }): number {
|
||||
const s = row.sort
|
||||
if (typeof s === 'number' && Number.isFinite(s)) {
|
||||
return Math.round(s)
|
||||
}
|
||||
if (typeof s === 'string' && s.trim() !== '') {
|
||||
const n = Number(s)
|
||||
if (!Number.isNaN(n) && Number.isFinite(n)) {
|
||||
return Math.round(n)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function nextSort(rows: { sort?: unknown }[]): number {
|
||||
let m = 0
|
||||
for (const r of rows) {
|
||||
const v = rowSortValue(r)
|
||||
if (v > m) {
|
||||
m = v
|
||||
}
|
||||
}
|
||||
return m > 0 ? m + 10 : 10
|
||||
}
|
||||
|
||||
function addCurrency() {
|
||||
form.currencies.push({
|
||||
code: '',
|
||||
label_zh: '',
|
||||
label_en: '',
|
||||
sort: nextSort(form.currencies),
|
||||
deposit_coins_per_fiat: '100',
|
||||
withdraw_coins_per_fiat: '100',
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeCurrencyCode(row: CurrencyRow) {
|
||||
row.code = String(row.code || '').trim().toUpperCase()
|
||||
}
|
||||
|
||||
function resortCurrenciesInPlace() {
|
||||
form.currencies.sort((a, b) => {
|
||||
const ds = rowSortValue(a) - rowSortValue(b)
|
||||
if (ds !== 0) {
|
||||
return ds
|
||||
}
|
||||
return String(a.code || '').localeCompare(String(b.code || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function resortBanksInPlace() {
|
||||
form.withdraw_banks.sort((a, b) => {
|
||||
const ds = rowSortValue(a) - rowSortValue(b)
|
||||
if (ds !== 0) {
|
||||
return ds
|
||||
}
|
||||
return String(a.code || '').localeCompare(String(b.code || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function resortChannelsInPlace() {
|
||||
form.channels.sort((a, b) => {
|
||||
const ds = rowSortValue(a) - rowSortValue(b)
|
||||
if (ds !== 0) {
|
||||
return ds
|
||||
}
|
||||
return String(a.code || '').localeCompare(String(b.code || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function channelDisplayName(code: string): string {
|
||||
const r = registry.value[code]
|
||||
if (!r) {
|
||||
return code
|
||||
}
|
||||
const loc = String(locale.value ?? '').toLowerCase().replaceAll('_', '-')
|
||||
const preferEn = loc === 'en' || loc.startsWith('en-')
|
||||
if (preferEn) {
|
||||
const ne = r.name_en
|
||||
if (typeof ne === 'string' && ne !== '') {
|
||||
return ne
|
||||
}
|
||||
}
|
||||
const n = r.name
|
||||
if (typeof n === 'string' && n !== '') {
|
||||
return n
|
||||
}
|
||||
const ne = r.name_en
|
||||
if (typeof ne === 'string' && ne !== '') {
|
||||
return ne
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
function normalizeChannelRow(c: Record<string, unknown>): ChannelRow {
|
||||
const tierIds: string[] = []
|
||||
if (Array.isArray(c.tier_ids)) {
|
||||
for (const x of c.tier_ids) {
|
||||
if (typeof x === 'string') {
|
||||
const t = x.trim()
|
||||
if (t !== '') {
|
||||
tierIds.push(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const st = c.status
|
||||
const statusOn = st === 1 || st === true || st === '1'
|
||||
return {
|
||||
code: typeof c.code === 'string' ? c.code : '',
|
||||
sort: rowSortValue({ sort: c.sort }),
|
||||
status: statusOn ? 1 : 0,
|
||||
tier_ids: tierIds,
|
||||
}
|
||||
}
|
||||
|
||||
function addBank() {
|
||||
form.withdraw_banks.push({ code: '', name_zh: '', name_en: '', sort: nextSort(form.withdraw_banks) })
|
||||
}
|
||||
|
||||
function removeRow<T>(arr: T[], index: number) {
|
||||
arr.splice(index, 1)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/config.FinanceCashierConfig/index',
|
||||
method: 'get',
|
||||
})
|
||||
if (res.code === 1 && res.data && res.data.form) {
|
||||
const f = res.data.form as typeof form
|
||||
const regRaw = res.data.registry
|
||||
registry.value =
|
||||
regRaw !== null && typeof regRaw === 'object' && !Array.isArray(regRaw)
|
||||
? (regRaw as Record<string, RegistryMeta>)
|
||||
: {}
|
||||
const topts = res.data.tier_options
|
||||
tierOptions.value = Array.isArray(topts) ? (topts as TierOpt[]) : []
|
||||
Object.assign(form.platform_coin, f.platform_coin || {})
|
||||
const curList = Array.isArray(f.currencies) ? f.currencies : []
|
||||
const normalized: CurrencyRow[] = curList.map((c: Record<string, unknown>) => ({
|
||||
code: typeof c.code === 'string' ? c.code : '',
|
||||
label_zh: typeof c.label_zh === 'string' ? c.label_zh : '',
|
||||
label_en: typeof c.label_en === 'string' ? c.label_en : '',
|
||||
sort: rowSortValue({ sort: c.sort }),
|
||||
deposit_coins_per_fiat:
|
||||
typeof c.deposit_coins_per_fiat === 'string'
|
||||
? c.deposit_coins_per_fiat
|
||||
: typeof c.deposit_coins_per_fiat === 'number'
|
||||
? String(c.deposit_coins_per_fiat)
|
||||
: '100',
|
||||
withdraw_coins_per_fiat:
|
||||
typeof c.withdraw_coins_per_fiat === 'string'
|
||||
? c.withdraw_coins_per_fiat
|
||||
: typeof c.withdraw_coins_per_fiat === 'number'
|
||||
? String(c.withdraw_coins_per_fiat)
|
||||
: '100',
|
||||
}))
|
||||
form.currencies.splice(0, form.currencies.length, ...normalized)
|
||||
resortCurrenciesInPlace()
|
||||
const bankList = Array.isArray(f.withdraw_banks) ? f.withdraw_banks : []
|
||||
const banksNorm: BankRow[] = bankList.map((b: Record<string, unknown>) => ({
|
||||
code: typeof b.code === 'string' ? b.code : '',
|
||||
name_zh: typeof b.name_zh === 'string' ? b.name_zh : '',
|
||||
name_en: typeof b.name_en === 'string' ? b.name_en : '',
|
||||
sort: rowSortValue({ sort: b.sort }),
|
||||
}))
|
||||
form.withdraw_banks.splice(0, form.withdraw_banks.length, ...banksNorm)
|
||||
resortBanksInPlace()
|
||||
const chList = Array.isArray(f.channels) ? f.channels : []
|
||||
const channelsNorm: ChannelRow[] = chList.map((c) => normalizeChannelRow(c as Record<string, unknown>))
|
||||
form.channels.splice(0, form.channels.length, ...channelsNorm)
|
||||
resortChannelsInPlace()
|
||||
Object.assign(form.withdraw_limits, f.withdraw_limits || {})
|
||||
Object.assign(form.withdraw_copy, f.withdraw_copy || {})
|
||||
Object.assign(form.withdraw_fields, f.withdraw_fields || {})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!auth('save')) {
|
||||
return
|
||||
}
|
||||
for (const row of form.currencies) {
|
||||
normalizeCurrencyCode(row)
|
||||
}
|
||||
const codes = form.currencies.map((c) => String(c.code || '').trim().toUpperCase()).filter((c) => c !== '')
|
||||
if (new Set(codes).size !== codes.length) {
|
||||
ElMessage.warning(t('config.financeCashierConfig.err_dup_code'))
|
||||
return
|
||||
}
|
||||
resortCurrenciesInPlace()
|
||||
resortBanksInPlace()
|
||||
resortChannelsInPlace()
|
||||
saving.value = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/config.FinanceCashierConfig/save',
|
||||
method: 'post',
|
||||
data: JSON.parse(JSON.stringify(form)),
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.finance-cashier-page {
|
||||
.toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.section-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.w400 {
|
||||
max-width: 400px;
|
||||
}
|
||||
.w240 {
|
||||
max-width: 240px;
|
||||
}
|
||||
.w100p {
|
||||
width: 100%;
|
||||
}
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
214
web/src/views/backend/order/depositChannelOrder/index.vue
Normal file
214
web/src/views/backend/order/depositChannelOrder/index.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('order.depositChannelOrder.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from '../depositOrder/popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'order/depositChannelOrder',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||
|
||||
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
}
|
||||
const s = String(cellValue).trim().replace(',', '.')
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(cellValue)
|
||||
}
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/order.DepositChannelOrder/'),
|
||||
{
|
||||
pk: 'id',
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('order.depositOrder.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('order.depositOrder.order_no'),
|
||||
prop: 'order_no',
|
||||
align: 'center',
|
||||
minWidth: 170,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.user_username'),
|
||||
prop: 'user.username',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.channel_name'),
|
||||
prop: 'channel.name',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.amount'),
|
||||
prop: 'amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.bonus_amount'),
|
||||
prop: 'bonus_amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
render: 'tag',
|
||||
effect: 'dark',
|
||||
custom: {
|
||||
'0': 'info',
|
||||
'1': 'success',
|
||||
'2': 'danger',
|
||||
'3': 'warning',
|
||||
},
|
||||
replaceValue: {
|
||||
'0': t('order.depositOrder.status 0'),
|
||||
'1': t('order.depositOrder.status 1'),
|
||||
'2': t('order.depositOrder.status 2'),
|
||||
'3': t('order.depositOrder.status 3'),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.pay_channel'),
|
||||
prop: 'pay_channel',
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.deposit_tier_id'),
|
||||
prop: 'deposit_tier_id',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.pay_time'),
|
||||
prop: 'pay_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.idempotency_key'),
|
||||
prop: 'idempotency_key',
|
||||
align: 'center',
|
||||
minWidth: 170,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.remark'),
|
||||
prop: 'remark',
|
||||
align: 'center',
|
||||
minWidth: 150,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 90,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
defaultItems: { status: 0, amount: '0.0000', bonus_amount: '0.0000' },
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user