Compare commits
30 Commits
5d8a0564b4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fdc1e2e88 | |||
| 186af5a55f | |||
| 6bec4e7758 | |||
| f9e4e61d93 | |||
| 8b6727dac1 | |||
| b97d33a24f | |||
| 6f12afcd10 | |||
| 5ab85d1d53 | |||
| 941f0f4a8c | |||
| 520e950dc5 | |||
| 2868899253 | |||
| 8305dbfcfd | |||
| 1cd5c3142d | |||
| 2686c54781 | |||
| e38fd660e2 | |||
| 6c402d4221 | |||
| ae7d7940d9 | |||
| c2ce8085c2 | |||
| 2d561f81b5 | |||
| 7db09b119e | |||
| d9dc31e388 | |||
| 179d67cb0e | |||
| 43a6c9ee47 | |||
| e08239dacf | |||
| a0d114fbc4 | |||
| e4e5a5cae2 | |||
| 8669a20dc8 | |||
| 09ef8edd15 | |||
| 4a42899bfe | |||
| b30ef21780 |
21
.env-example
21
.env-example
@@ -15,7 +15,7 @@ DATABASE_USERNAME = webman-buildadmin-mall
|
|||||||
DATABASE_PASSWORD = 123456
|
DATABASE_PASSWORD = 123456
|
||||||
DATABASE_HOSTPORT = 3306
|
DATABASE_HOSTPORT = 3306
|
||||||
DATABASE_CHARSET = utf8mb4
|
DATABASE_CHARSET = utf8mb4
|
||||||
DATABASE_PREFIX =
|
DATABASE_PREFIX =
|
||||||
|
|
||||||
# PlayX 配置
|
# PlayX 配置
|
||||||
# 提现折算:积分 -> 现金(如 10 分 = 1 元,则 points_to_cash_ratio=0.1)
|
# 提现折算:积分 -> 现金(如 10 分 = 1 元,则 points_to_cash_ratio=0.1)
|
||||||
@@ -24,10 +24,27 @@ PLAYX_POINTS_TO_CASH_RATIO=0.1
|
|||||||
PLAYX_RETURN_RATIO=0.1
|
PLAYX_RETURN_RATIO=0.1
|
||||||
# 解锁比例:今日可领取上限 = yesterday_total_deposit * unlock_ratio
|
# 解锁比例:今日可领取上限 = yesterday_total_deposit * unlock_ratio
|
||||||
PLAYX_UNLOCK_RATIO=0.1
|
PLAYX_UNLOCK_RATIO=0.1
|
||||||
# Daily Push 签名校验密钥(建议从部署系统注入,避免写入代码/仓库)
|
# Daily Push 签名校验密钥(HMAC,建议从部署系统注入,避免写入代码/仓库)
|
||||||
PLAYX_DAILY_PUSH_SECRET=
|
PLAYX_DAILY_PUSH_SECRET=
|
||||||
|
# 合作方回调 JWT 验签密钥(HS256,与对端私发密钥一致;与上一项可同时配置,则两种均需通过)
|
||||||
|
PLAYX_PARTNER_JWT_SECRET=
|
||||||
|
# Agent authtoken(/api/v1/authToken)JWT 签名密钥;留空则使用下方 buildadmin.token.key
|
||||||
|
AGENT_AUTH_JWT_SECRET=
|
||||||
# token 会话缓存过期时间(秒)
|
# token 会话缓存过期时间(秒)
|
||||||
PLAYX_SESSION_EXPIRE_SECONDS=3600
|
PLAYX_SESSION_EXPIRE_SECONDS=3600
|
||||||
# PlayX API(商城调用 PlayX 时使用)
|
# PlayX API(商城调用 PlayX 时使用)
|
||||||
PLAYX_API_BASE_URL=
|
PLAYX_API_BASE_URL=
|
||||||
PLAYX_API_SECRET_KEY=
|
PLAYX_API_SECRET_KEY=
|
||||||
|
|
||||||
|
# 推送订单url
|
||||||
|
PLAYX_ANGPOW_IMPORT_BASE_URL=https://ss2-staging2.ttwd8.com
|
||||||
|
# 推送订单接口
|
||||||
|
PLAYX_ANGPOW_IMPORT_PATH=/cashmarket/v3/merchant-api/angpow-imports
|
||||||
|
# 商户编码merchant_code
|
||||||
|
PLAYX_ANGPOW_MERCHANT_CODE=plx
|
||||||
|
# HMAC 密钥(与对端一致)
|
||||||
|
PLAYX_ANGPOW_IMPORT_AUTH_KEY=
|
||||||
|
# HTTPS:CA 证书包绝对路径(推荐下载 https://curl.se/ca/cacert.pem 后填写,解决 cURL error 60)
|
||||||
|
PLAYX_ANGPOW_IMPORT_CACERT=
|
||||||
|
# 是否校验 SSL(1=校验;0=不校验,仅本地调试,生产勿用)
|
||||||
|
PLAYX_ANGPOW_IMPORT_VERIFY_SSL=1
|
||||||
@@ -191,7 +191,7 @@ php webman migrate
|
|||||||
## 六、路由说明
|
## 六、路由说明
|
||||||
|
|
||||||
- **后台 API**:`/admin/{module}.{Controller}/{action}`
|
- **后台 API**:`/admin/{module}.{Controller}/{action}`
|
||||||
- 示例:`/admin/mall.Player/index` → `app\admin\controller\mall\Player::index`
|
- 示例:`/admin/mall.User/index` → `app\admin\controller\mall\User::index`
|
||||||
- **前台 API**:`/api/...`
|
- **前台 API**:`/api/...`
|
||||||
- **安装**:`/api/Install/...`
|
- **安装**:`/api/Install/...`
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ php webman migrate
|
|||||||
| 语言 key 命名错误 | `quick Search Fields` 改为 `quickSearchFields` |
|
| 语言 key 命名错误 | `quick Search Fields` 改为 `quickSearchFields` |
|
||||||
| 编辑时密码必填 | 编辑时密码可选,仅新增时必填;后端支持密码加密与重置 |
|
| 编辑时密码必填 | 编辑时密码可选,仅新增时必填;后端支持密码加密与重置 |
|
||||||
| 多余表单校验 | 移除 `create_time`、`update_time` 的表单校验 |
|
| 多余表单校验 | 移除 `create_time`、`update_time` 的表单校验 |
|
||||||
| mall_player 表缺失 | 新增迁移文件 `20250318120000_mall_player.php` |
|
| mall_user 与 PlayX 资产口径混用 | 新增重构迁移并改为 `mall_user` 一对一扩展资产表 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ declare(strict_types=1);
|
|||||||
namespace app\admin\controller;
|
namespace app\admin\controller;
|
||||||
|
|
||||||
use app\common\controller\Backend;
|
use app\common\controller\Backend;
|
||||||
|
use app\common\model\MallClaimLog;
|
||||||
|
use app\common\model\MallOrder;
|
||||||
|
use app\common\model\MallUserAsset;
|
||||||
|
use support\think\Db;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
|
|
||||||
@@ -15,8 +19,78 @@ class Dashboard extends Backend
|
|||||||
$response = $this->initializeBackend($request);
|
$response = $this->initializeBackend($request);
|
||||||
if ($response !== null) return $response;
|
if ($response !== null) return $response;
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$todayStart = strtotime(date('Y-m-d', $now) . ' 00:00:00');
|
||||||
|
$yesterdayStart = $todayStart - 86400;
|
||||||
|
|
||||||
|
$newPlayersToday = MallUserAsset::where('create_time', '>=', $todayStart)
|
||||||
|
->where('create_time', '<=', $now)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$yesterdayPointsClaimed = MallClaimLog::where('create_time', '>=', $yesterdayStart)
|
||||||
|
->where('create_time', '<', $todayStart)
|
||||||
|
->sum('claimed_amount');
|
||||||
|
|
||||||
|
$yesterdayRedeemQuery = MallOrder::where('create_time', '>=', $yesterdayStart)
|
||||||
|
->where('create_time', '<', $todayStart);
|
||||||
|
|
||||||
|
$yesterdayRedeemCount = (clone $yesterdayRedeemQuery)->count();
|
||||||
|
$yesterdayRedeemPointsCostSum = (clone $yesterdayRedeemQuery)->sum('points_cost');
|
||||||
|
$yesterdayRedeemAmountSum = (clone $yesterdayRedeemQuery)->sum('amount');
|
||||||
|
$yesterdayRedeemCompletedCount = (clone $yesterdayRedeemQuery)
|
||||||
|
->where('status', MallOrder::STATUS_COMPLETED)
|
||||||
|
->count();
|
||||||
|
$yesterdayRedeemRejectedCount = (clone $yesterdayRedeemQuery)
|
||||||
|
->where('status', MallOrder::STATUS_REJECTED)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$yesterdayRedeemByItem = Db::name('mall_order')
|
||||||
|
->alias('o')
|
||||||
|
->leftJoin('mall_item i', 'i.id = o.mall_item_id')
|
||||||
|
->where('o.create_time', '>=', $yesterdayStart)
|
||||||
|
->where('o.create_time', '<', $todayStart)
|
||||||
|
->group('o.mall_item_id, i.title')
|
||||||
|
->field([
|
||||||
|
'o.mall_item_id',
|
||||||
|
'i.title',
|
||||||
|
Db::raw('COUNT(*) as order_count'),
|
||||||
|
Db::raw('SUM(o.points_cost) as points_cost_sum'),
|
||||||
|
Db::raw('SUM(o.amount) as amount_sum'),
|
||||||
|
Db::raw('SUM(CASE WHEN o.status = "COMPLETED" THEN 1 ELSE 0 END) as completed_count'),
|
||||||
|
Db::raw('SUM(CASE WHEN o.status = "REJECTED" THEN 1 ELSE 0 END) as rejected_count'),
|
||||||
|
])
|
||||||
|
->orderRaw('order_count DESC')
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$pendingPhysicalToShip = MallOrder::where('type', MallOrder::TYPE_PHYSICAL)
|
||||||
|
->where('status', MallOrder::STATUS_PENDING)
|
||||||
|
->count();
|
||||||
|
$grantFailedRetryableCount = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||||
|
->where('grant_status', MallOrder::GRANT_FAILED_RETRYABLE)
|
||||||
|
->count();
|
||||||
|
|
||||||
return $this->success('', [
|
return $this->success('', [
|
||||||
'remark' => get_route_remark()
|
'remark' => get_route_remark(),
|
||||||
|
'playx' => [
|
||||||
|
'time_range' => [
|
||||||
|
'today_start' => $todayStart,
|
||||||
|
'yesterday_start' => $yesterdayStart,
|
||||||
|
'now' => $now,
|
||||||
|
],
|
||||||
|
'new_players_today' => $newPlayersToday,
|
||||||
|
'yesterday_points_claimed' => $yesterdayPointsClaimed,
|
||||||
|
'yesterday_redeem' => [
|
||||||
|
'order_count' => $yesterdayRedeemCount,
|
||||||
|
'points_cost_sum' => $yesterdayRedeemPointsCostSum,
|
||||||
|
'amount_sum' => $yesterdayRedeemAmountSum,
|
||||||
|
'completed_count' => $yesterdayRedeemCompletedCount,
|
||||||
|
'rejected_count' => $yesterdayRedeemRejectedCount,
|
||||||
|
'by_item' => $yesterdayRedeemByItem,
|
||||||
|
],
|
||||||
|
'pending_physical_to_ship' => $pendingPhysicalToShip,
|
||||||
|
'grant_failed_retryable' => $grantFailedRetryableCount,
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Admin extends Backend
|
|||||||
{
|
{
|
||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip', 'agent_id'];
|
protected array|string $preExcludeFields = ['create_time', 'update_time', 'password', 'salt', 'login_failure', 'last_login_time', 'last_login_ip', 'agent_id', 'agent_api_secret', 'channel_id'];
|
||||||
|
|
||||||
protected array|string $quickSearchField = ['username', 'nickname'];
|
protected array|string $quickSearchField = ['username', 'nickname'];
|
||||||
|
|
||||||
@@ -25,8 +25,6 @@ class Admin extends Backend
|
|||||||
|
|
||||||
protected string $dataLimitField = 'id';
|
protected string $dataLimitField = 'id';
|
||||||
|
|
||||||
protected array $withJoinTable = ['channel'];
|
|
||||||
|
|
||||||
protected function initController(Request $request): ?Response
|
protected function initController(Request $request): ?Response
|
||||||
{
|
{
|
||||||
$this->model = new AdminModel();
|
$this->model = new AdminModel();
|
||||||
@@ -46,8 +44,7 @@ class Admin extends Backend
|
|||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
$res = $this->model
|
$res = $this->model
|
||||||
->withoutField('login_failure,password,salt')
|
->withoutField('login_failure,password,salt')
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType ?? 'LEFT')
|
->withJoin($this->withJoinTable ?? [], $this->withJoinType ?? 'LEFT')
|
||||||
->visible(['channel' => ['name']])
|
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where)
|
->where($where)
|
||||||
->order($order)
|
->order($order)
|
||||||
@@ -81,13 +78,9 @@ class Admin extends Backend
|
|||||||
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile',
|
'mobile' => 'regex:/^1[3-9]\d{9}$/|unique:admin,mobile',
|
||||||
'group_arr' => 'required|array',
|
'group_arr' => 'required|array',
|
||||||
];
|
];
|
||||||
if ($this->auth->isSuperAdmin()) {
|
|
||||||
$rules['channel_id'] = 'required|integer|min:1';
|
|
||||||
}
|
|
||||||
$messages = [
|
$messages = [
|
||||||
'username.regex' => __('Please input correct username'),
|
'username.regex' => __('Please input correct username'),
|
||||||
'password.regex' => __('Please input correct password'),
|
'password.regex' => __('Please input correct password'),
|
||||||
'channel_id.required' => __('Please select channel'),
|
|
||||||
];
|
];
|
||||||
Validator::make($data, $rules, $messages)->validate();
|
Validator::make($data, $rules, $messages)->validate();
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
@@ -95,14 +88,6 @@ class Admin extends Backend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->auth->isSuperAdmin()) {
|
|
||||||
$currentChannelId = (int) ($this->auth->model->channel_id ?? 0);
|
|
||||||
if ($currentChannelId <= 0) {
|
|
||||||
return $this->error(__('Current admin has no channel bound'));
|
|
||||||
}
|
|
||||||
$data['channel_id'] = $currentChannelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$passwd = $data['password'] ?? '';
|
$passwd = $data['password'] ?? '';
|
||||||
$data = $this->excludeFields($data);
|
$data = $this->excludeFields($data);
|
||||||
$result = false;
|
$result = false;
|
||||||
@@ -115,7 +100,12 @@ class Admin extends Backend
|
|||||||
$result = $this->model->save($data);
|
$result = $this->model->save($data);
|
||||||
if ($result !== false) {
|
if ($result !== false) {
|
||||||
$agentId = strtolower(md5($this->model->username . $this->model->id));
|
$agentId = strtolower(md5($this->model->username . $this->model->id));
|
||||||
$this->model->where('id', $this->model->id)->update(['agent_id' => $agentId]);
|
$agentSecret = strtoupper(md5($this->model->username . $this->model->id));
|
||||||
|
// 使用原生 SQL,避免 ThinkORM 按当前表结构校验字段时因未迁移缺少 agent_api_secret 列而报错
|
||||||
|
Db::execute(
|
||||||
|
'UPDATE `admin` SET `agent_id` = ?, `agent_api_secret` = ? WHERE `id` = ?',
|
||||||
|
[$agentId, $agentSecret, $this->model->id]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!empty($data['group_arr'])) {
|
if (!empty($data['group_arr'])) {
|
||||||
$groupAccess = [];
|
$groupAccess = [];
|
||||||
@@ -306,6 +296,39 @@ class Admin extends Backend
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去掉已废弃的渠道字段,避免组合搜索生成对不存在列的条件
|
||||||
|
*/
|
||||||
|
protected function filterSearchArray(array $search): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter($search, static function ($item) {
|
||||||
|
if (!is_array($item) || !isset($item['field'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$f = (string) $item['field'];
|
||||||
|
if ($f === 'channel_id' || str_ends_with($f, '.channel_id')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($f === 'channel.name' || str_starts_with($f, 'channel.')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryOrderBuilder(): array
|
||||||
|
{
|
||||||
|
$order = parent::queryOrderBuilder();
|
||||||
|
foreach (array_keys($order) as $key) {
|
||||||
|
if ($key === 'channel_id' || (is_string($key) && str_contains($key, 'channel.'))) {
|
||||||
|
unset($order[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
|
||||||
private function checkGroupAuth(array $groups): ?Response
|
private function checkGroupAuth(array $groups): ?Response
|
||||||
{
|
{
|
||||||
if ($this->auth->isSuperAdmin()) {
|
if ($this->auth->isSuperAdmin()) {
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\admin\controller\channel;
|
|
||||||
|
|
||||||
use Throwable;
|
|
||||||
use app\common\controller\Backend;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渠道管理
|
|
||||||
*/
|
|
||||||
class Manage extends Backend
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* ChannelManage模型对象
|
|
||||||
* @var object|null
|
|
||||||
* @phpstan-var \app\common\model\ChannelManage|null
|
|
||||||
*/
|
|
||||||
protected ?object $model = null;
|
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'secret'];
|
|
||||||
|
|
||||||
protected array $withJoinTable = ['admin'];
|
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['id'];
|
|
||||||
|
|
||||||
protected bool $autoFillAdminId = true;
|
|
||||||
|
|
||||||
public function initialize(): void
|
|
||||||
{
|
|
||||||
parent::initialize();
|
|
||||||
$this->model = new \app\common\model\ChannelManage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查看
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function index(\Webman\Http\Request $request): \support\Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->get('select') || $request->post('select')) {
|
|
||||||
return $this->select($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则)
|
|
||||||
* 2. 以下的别名设置了主表别名,同时便于拼接查询参数等
|
|
||||||
* 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理
|
|
||||||
*/
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
|
||||||
$res = $this->model
|
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
|
||||||
->visible(['admin' => ['username']])
|
|
||||||
->alias($alias)
|
|
||||||
->where($where)
|
|
||||||
->order($order)
|
|
||||||
->paginate($limit);
|
|
||||||
|
|
||||||
return $this->success('', [
|
|
||||||
'list' => $res->items(),
|
|
||||||
'total' => $res->total(),
|
|
||||||
'remark' => get_route_remark(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 白名单(页面按钮规则,用于菜单规则中配置按钮权限)
|
|
||||||
* 实际编辑通过 edit 接口提交 ip_white 字段
|
|
||||||
*/
|
|
||||||
public function whitelist(\Webman\Http\Request $request): \support\Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
return $this->success();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渠道下拉选择(供 remoteSelect 使用)
|
|
||||||
*/
|
|
||||||
public function select(\Webman\Http\Request $request): \support\Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
|
||||||
$res = $this->model
|
|
||||||
->field('id,name,title')
|
|
||||||
->alias($alias)
|
|
||||||
->where($where)
|
|
||||||
->order($order)
|
|
||||||
->paginate($limit);
|
|
||||||
return $this->success('', [
|
|
||||||
'list' => $res->items(),
|
|
||||||
'total' => $res->total(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add、edit、del、sortable 已由父类 Backend 实现,无需重写即可直接使用
|
|
||||||
* 若需重写,请确保调用 initializeBackend($request) 并传入 Request 参数
|
|
||||||
* 若模型有 admin_id 字段需自动填充,可设置 protected bool $autoFillAdminId = true
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ class Address extends Backend
|
|||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||||
|
|
||||||
protected array $withJoinTable = ['mallUser'];
|
protected array $withJoinTable = ['playxUserAsset'];
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['id'];
|
protected string|array $quickSearchField = ['id'];
|
||||||
|
|
||||||
@@ -52,10 +52,10 @@ class Address extends Backend
|
|||||||
*/
|
*/
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
$res = $this->model
|
$res = $this->model
|
||||||
->with(['mallUser' => function ($query) {
|
->with(['playxUserAsset' => function ($query) {
|
||||||
$query->field('id,username');
|
$query->field('id,username');
|
||||||
}])
|
}])
|
||||||
->visible(['mallUser' => ['username']])
|
->visible(['playxUserAsset' => ['username']])
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where)
|
->where($where)
|
||||||
->order($order)
|
->order($order)
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ use support\Response;
|
|||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayX 领取记录(后台列表)
|
* 领取记录(后台列表)
|
||||||
*/
|
*/
|
||||||
class PlayxClaimLog extends Backend
|
class ClaimLog extends Backend
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var object|null
|
* @var object|null
|
||||||
* @phpstan-var \app\common\model\MallPlayxClaimLog|null
|
* @phpstan-var \app\common\model\MallClaimLog|null
|
||||||
*/
|
*/
|
||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class PlayxClaimLog extends Backend
|
|||||||
public function initialize(): void
|
public function initialize(): void
|
||||||
{
|
{
|
||||||
parent::initialize();
|
parent::initialize();
|
||||||
$this->model = new \app\common\model\MallPlayxClaimLog();
|
$this->model = new \app\common\model\MallClaimLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,13 +8,13 @@ use support\Response;
|
|||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayX 每日推送数据(后台列表)
|
* 每日推送数据(后台列表)
|
||||||
*/
|
*/
|
||||||
class PlayxDailyPush extends Backend
|
class DailyPush extends Backend
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var object|null
|
* @var object|null
|
||||||
* @phpstan-var \app\common\model\MallPlayxDailyPush|null
|
* @phpstan-var \app\common\model\MallDailyPush|null
|
||||||
*/
|
*/
|
||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class PlayxDailyPush extends Backend
|
|||||||
public function initialize(): void
|
public function initialize(): void
|
||||||
{
|
{
|
||||||
parent::initialize();
|
parent::initialize();
|
||||||
$this->model = new \app\common\model\MallPlayxDailyPush();
|
$this->model = new \app\common\model\MallDailyPush();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
318
app/admin/controller/mall/Order.php
Normal file
318
app/admin/controller/mall/Order.php
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\mall;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\library\MallBonusGrantPush;
|
||||||
|
use app\common\model\MallOrder;
|
||||||
|
use app\common\model\MallUserAsset;
|
||||||
|
use support\think\Db;
|
||||||
|
use support\Response;
|
||||||
|
use Webman\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一订单(后台列表)
|
||||||
|
*/
|
||||||
|
class Order extends Backend
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var object|null
|
||||||
|
* @phpstan-var \app\common\model\MallOrder|null
|
||||||
|
*/
|
||||||
|
protected ?object $model = null;
|
||||||
|
|
||||||
|
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||||
|
|
||||||
|
protected array $withJoinTable = ['mallItem'];
|
||||||
|
|
||||||
|
protected string|array $quickSearchField = ['user_id', 'external_transaction_id', 'playx_transaction_id'];
|
||||||
|
|
||||||
|
protected string|array $indexField = [
|
||||||
|
'id',
|
||||||
|
'user_id',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'mall_item_id',
|
||||||
|
'points_cost',
|
||||||
|
'amount',
|
||||||
|
'multiplier',
|
||||||
|
'external_transaction_id',
|
||||||
|
'playx_transaction_id',
|
||||||
|
'grant_status',
|
||||||
|
'fail_reason',
|
||||||
|
'reject_reason',
|
||||||
|
'shipping_company',
|
||||||
|
'shipping_no',
|
||||||
|
'receiver_name',
|
||||||
|
'receiver_phone',
|
||||||
|
'receiver_address',
|
||||||
|
'mall_address_id',
|
||||||
|
'create_time',
|
||||||
|
'update_time',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
parent::initialize();
|
||||||
|
$this->model = new \app\common\model\MallOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->get('select') || $request->post('select')) {
|
||||||
|
return $this->select($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$where, $alias, $limit, $order] = $this->queryBuilder();
|
||||||
|
$res = $this->model
|
||||||
|
->with(['mallItem' => function ($query) {
|
||||||
|
$query->field('id,title');
|
||||||
|
}])
|
||||||
|
->visible(['mallItem' => ['title']])
|
||||||
|
->alias($alias)
|
||||||
|
->where($where)
|
||||||
|
->order($order)
|
||||||
|
->paginate($limit);
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'list' => $res->items(),
|
||||||
|
'total' => $res->total(),
|
||||||
|
'remark' => get_route_remark(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHYSICAL 发货:更新 shipping_company/shipping_no,并将状态置为 SHIPPED
|
||||||
|
*/
|
||||||
|
public function ship(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->method() !== 'POST') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->post();
|
||||||
|
$id = $data['id'] ?? 0;
|
||||||
|
$shippingCompany = $data['shipping_company'] ?? '';
|
||||||
|
$shippingNo = $data['shipping_no'] ?? '';
|
||||||
|
|
||||||
|
if (!$id || $shippingCompany === '' || $shippingNo === '') {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = MallOrder::where('id', $id)->find();
|
||||||
|
if (!$order) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
if ($order->type !== MallOrder::TYPE_PHYSICAL) {
|
||||||
|
return $this->error(__('Order type not PHYSICAL'));
|
||||||
|
}
|
||||||
|
if ($order->status !== MallOrder::STATUS_PENDING) {
|
||||||
|
return $this->error(__('Order status must be PENDING'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$order->shipping_company = $shippingCompany;
|
||||||
|
$order->shipping_no = $shippingNo;
|
||||||
|
$order->status = MallOrder::STATUS_SHIPPED;
|
||||||
|
$order->save();
|
||||||
|
Db::commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(__('Shipped successfully'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核通过(非 PHYSICAL):更新状态为 COMPLETED
|
||||||
|
*/
|
||||||
|
public function approve(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->method() !== 'POST') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->post('id', 0);
|
||||||
|
if (!$id) {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = MallOrder::where('id', $id)->find();
|
||||||
|
if (!$order) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
if ($order->status !== MallOrder::STATUS_PENDING) {
|
||||||
|
return $this->error(__('Order status must be PENDING'));
|
||||||
|
}
|
||||||
|
if ($order->type === MallOrder::TYPE_PHYSICAL) {
|
||||||
|
return $this->error(__('Order type not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$order->status = MallOrder::STATUS_COMPLETED;
|
||||||
|
$order->update_time = time();
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(__('Approved successfully'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核驳回:更新状态为 REJECTED,并退回积分到 available_points(所有类型通用)
|
||||||
|
*/
|
||||||
|
public function reject(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->method() !== 'POST') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->post();
|
||||||
|
$id = $data['id'] ?? 0;
|
||||||
|
$rejectReason = $data['reject_reason'] ?? '';
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = MallOrder::where('id', $id)->find();
|
||||||
|
if (!$order) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
if ($order->status !== MallOrder::STATUS_PENDING) {
|
||||||
|
return $this->error(__('Order status must be PENDING'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($order->type === MallOrder::TYPE_PHYSICAL && $rejectReason === '') {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$asset = MallUserAsset::where('playx_user_id', $order->user_id ?? '')->find();
|
||||||
|
if (!$asset) {
|
||||||
|
throw new \RuntimeException('User asset not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$refund = $order->points_cost ?? 0;
|
||||||
|
if ($refund > 0) {
|
||||||
|
$asset->available_points += $refund;
|
||||||
|
$asset->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$order->status = MallOrder::STATUS_REJECTED;
|
||||||
|
$order->reject_reason = $rejectReason;
|
||||||
|
if ($order->type === MallOrder::TYPE_BONUS) {
|
||||||
|
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||||
|
} else {
|
||||||
|
$order->grant_status = MallOrder::GRANT_NOT_APPLICABLE;
|
||||||
|
}
|
||||||
|
$order->update_time = time();
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(__('Rejected successfully'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动推送红利(同步调用 PlayX,不限制自动重试次数;成功则 ACCEPTED,失败写入 fail_reason)
|
||||||
|
*/
|
||||||
|
public function retry(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->method() !== 'POST') {
|
||||||
|
return $this->error(__('Parameter error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $request->post('id', 0);
|
||||||
|
if (!$id) {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = MallOrder::where('id', $id)->find();
|
||||||
|
if (!$order) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
if ($order->type !== MallOrder::TYPE_BONUS) {
|
||||||
|
return $this->error(__('Only BONUS can retry'));
|
||||||
|
}
|
||||||
|
if ($order->status !== MallOrder::STATUS_PENDING) {
|
||||||
|
return $this->error(__('Order status must be PENDING'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedStatuses = [
|
||||||
|
MallOrder::GRANT_NOT_SENT,
|
||||||
|
MallOrder::GRANT_SENT_PENDING,
|
||||||
|
MallOrder::GRANT_FAILED_RETRYABLE,
|
||||||
|
MallOrder::GRANT_FAILED_FINAL,
|
||||||
|
];
|
||||||
|
if (!in_array($order->grant_status, $allowedStatuses, true)) {
|
||||||
|
return $this->error(__('Current grant status cannot be manually pushed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strval(config('playx.api.base_url', '')) === '') {
|
||||||
|
return $this->error(__('PlayX API not configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = MallBonusGrantPush::push($order);
|
||||||
|
if ($result['ok']) {
|
||||||
|
$order->grant_status = MallOrder::GRANT_ACCEPTED;
|
||||||
|
$order->playx_transaction_id = $result['playx_transaction_id'];
|
||||||
|
$order->fail_reason = null;
|
||||||
|
$order->update_time = time();
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return $this->success(__('Push succeeded'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$failReason = __('Manual push failed') . ': ' . $result['message'];
|
||||||
|
$order->fail_reason = $failReason;
|
||||||
|
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||||
|
$order->update_time = time();
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return $this->error($failReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class PintsOrder extends Backend
|
|||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||||
|
|
||||||
protected array $withJoinTable = ['mallUser'];
|
protected array $withJoinTable = ['playxUserAsset'];
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['id'];
|
protected string|array $quickSearchField = ['id'];
|
||||||
|
|
||||||
@@ -47,10 +47,10 @@ class PintsOrder extends Backend
|
|||||||
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
$res = $this->model
|
$res = $this->model
|
||||||
->with(['mallUser' => function ($query) {
|
->with(['playxUserAsset' => function ($query) {
|
||||||
$query->field('id,username');
|
$query->field('id,username');
|
||||||
}])
|
}])
|
||||||
->visible(['mallUser' => ['username']])
|
->visible(['playxUserAsset' => ['username']])
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where)
|
->where($where)
|
||||||
->order($order)
|
->order($order)
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\admin\controller\mall;
|
|
||||||
|
|
||||||
use app\common\controller\Backend;
|
|
||||||
use support\Response;
|
|
||||||
use Throwable;
|
|
||||||
use Webman\Http\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分商城用户
|
|
||||||
*/
|
|
||||||
class Player extends Backend
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Player模型对象
|
|
||||||
* @var object|null
|
|
||||||
* @phpstan-var \app\admin\model\mall\Player|null
|
|
||||||
*/
|
|
||||||
protected ?object $model = null;
|
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'password'];
|
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['id'];
|
|
||||||
|
|
||||||
/** 列表不返回密码字段 */
|
|
||||||
protected string|array $indexField = ['id', 'username', 'create_time', 'update_time', 'score'];
|
|
||||||
|
|
||||||
public function initialize(): void
|
|
||||||
{
|
|
||||||
parent::initialize();
|
|
||||||
$this->model = new \app\admin\model\mall\Player();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加(重写以支持密码加密)
|
|
||||||
*/
|
|
||||||
public function add(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response instanceof Response) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() !== 'POST') {
|
|
||||||
$this->error(__('Parameter error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $request->post();
|
|
||||||
if (!$data) {
|
|
||||||
$this->error(__('Parameter %s can not be empty', ['']));
|
|
||||||
}
|
|
||||||
|
|
||||||
$passwd = $data['password'] ?? '';
|
|
||||||
if (empty($passwd)) {
|
|
||||||
$this->error(__('Parameter %s can not be empty', [__('Password')]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->applyInputFilter($data);
|
|
||||||
$data = $this->excludeFields($data);
|
|
||||||
|
|
||||||
$result = false;
|
|
||||||
$this->model->startTrans();
|
|
||||||
try {
|
|
||||||
if ($this->modelValidate) {
|
|
||||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
|
||||||
if (class_exists($validate)) {
|
|
||||||
$validate = new $validate();
|
|
||||||
if ($this->modelSceneValidate) {
|
|
||||||
$validate->scene('add');
|
|
||||||
}
|
|
||||||
$validate->check($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->model->save($data);
|
|
||||||
if ($result !== false && $passwd) {
|
|
||||||
$this->model->resetPassword((int) $this->model->id, $passwd);
|
|
||||||
}
|
|
||||||
$this->model->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->model->rollback();
|
|
||||||
$this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑(重写以支持编辑时密码可选)
|
|
||||||
*/
|
|
||||||
public function edit(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response instanceof Response) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pk = $this->model->getPk();
|
|
||||||
$id = $request->post($pk) ?? $request->get($pk);
|
|
||||||
$row = $this->model->find($id);
|
|
||||||
if (!$row) {
|
|
||||||
$this->error(__('Record not found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() === 'POST') {
|
|
||||||
$data = $request->post();
|
|
||||||
if (!$data) {
|
|
||||||
$this->error(__('Parameter %s can not be empty', ['']));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($data['password'])) {
|
|
||||||
$this->model->resetPassword((int) $row->id, $data['password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->applyInputFilter($data);
|
|
||||||
$data = $this->excludeFields($data);
|
|
||||||
|
|
||||||
$result = false;
|
|
||||||
$this->model->startTrans();
|
|
||||||
try {
|
|
||||||
if ($this->modelValidate) {
|
|
||||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
|
||||||
if (class_exists($validate)) {
|
|
||||||
$validate = new $validate();
|
|
||||||
if ($this->modelSceneValidate) {
|
|
||||||
$validate->scene('edit');
|
|
||||||
}
|
|
||||||
$validate->check(array_merge($data, [$pk => $row[$pk]]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $row->save($data);
|
|
||||||
$this->model->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->model->rollback();
|
|
||||||
$this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($row['password']);
|
|
||||||
$row['password'] = '';
|
|
||||||
$this->success('', ['row' => $row]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 若需重写查看、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\admin\controller\mall;
|
|
||||||
|
|
||||||
use Throwable;
|
|
||||||
use app\common\controller\Backend;
|
|
||||||
use app\common\model\MallPlayxOrder;
|
|
||||||
use app\common\model\MallPlayxUserAsset;
|
|
||||||
use support\think\Db;
|
|
||||||
use support\Response;
|
|
||||||
use Webman\Http\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlayX 统一订单(后台列表)
|
|
||||||
*/
|
|
||||||
class PlayxOrder extends Backend
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var object|null
|
|
||||||
* @phpstan-var \app\common\model\MallPlayxOrder|null
|
|
||||||
*/
|
|
||||||
protected ?object $model = null;
|
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
|
||||||
|
|
||||||
protected array $withJoinTable = ['mallItem'];
|
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['user_id', 'external_transaction_id', 'playx_transaction_id'];
|
|
||||||
|
|
||||||
protected string|array $indexField = [
|
|
||||||
'id',
|
|
||||||
'user_id',
|
|
||||||
'type',
|
|
||||||
'status',
|
|
||||||
'mall_item_id',
|
|
||||||
'points_cost',
|
|
||||||
'amount',
|
|
||||||
'multiplier',
|
|
||||||
'external_transaction_id',
|
|
||||||
'playx_transaction_id',
|
|
||||||
'grant_status',
|
|
||||||
'fail_reason',
|
|
||||||
'reject_reason',
|
|
||||||
'shipping_company',
|
|
||||||
'shipping_no',
|
|
||||||
'receiver_name',
|
|
||||||
'receiver_phone',
|
|
||||||
'receiver_address',
|
|
||||||
'create_time',
|
|
||||||
'update_time',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function initialize(): void
|
|
||||||
{
|
|
||||||
parent::initialize();
|
|
||||||
$this->model = new \app\common\model\MallPlayxOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查看
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->get('select') || $request->post('select')) {
|
|
||||||
return $this->select($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
|
||||||
$res = $this->model
|
|
||||||
->with(['mallItem' => function ($query) {
|
|
||||||
$query->field('id,title');
|
|
||||||
}])
|
|
||||||
->visible(['mallItem' => ['title']])
|
|
||||||
->alias($alias)
|
|
||||||
->where($where)
|
|
||||||
->order($order)
|
|
||||||
->paginate($limit);
|
|
||||||
|
|
||||||
return $this->success('', [
|
|
||||||
'list' => $res->items(),
|
|
||||||
'total' => $res->total(),
|
|
||||||
'remark' => get_route_remark(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PHYSICAL 发货:更新 shipping_company/shipping_no,并将状态置为 SHIPPED
|
|
||||||
*/
|
|
||||||
public function ship(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() !== 'POST') {
|
|
||||||
return $this->error(__('Parameter error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $request->post();
|
|
||||||
$id = intval($data['id'] ?? 0);
|
|
||||||
$shippingCompany = strval($data['shipping_company'] ?? '');
|
|
||||||
$shippingNo = strval($data['shipping_no'] ?? '');
|
|
||||||
|
|
||||||
if ($id <= 0 || $shippingCompany === '' || $shippingNo === '') {
|
|
||||||
return $this->error(__('Missing required fields'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$order = MallPlayxOrder::where('id', $id)->find();
|
|
||||||
if (!$order) {
|
|
||||||
return $this->error(__('Record not found'));
|
|
||||||
}
|
|
||||||
if ($order->type !== MallPlayxOrder::TYPE_PHYSICAL) {
|
|
||||||
return $this->error(__('Order type not PHYSICAL'));
|
|
||||||
}
|
|
||||||
if ($order->status !== MallPlayxOrder::STATUS_PENDING) {
|
|
||||||
return $this->error(__('Order status must be PENDING'));
|
|
||||||
}
|
|
||||||
|
|
||||||
Db::startTrans();
|
|
||||||
try {
|
|
||||||
$order->shipping_company = $shippingCompany;
|
|
||||||
$order->shipping_no = $shippingNo;
|
|
||||||
$order->status = MallPlayxOrder::STATUS_SHIPPED;
|
|
||||||
$order->save();
|
|
||||||
Db::commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
Db::rollback();
|
|
||||||
return $this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(__('Shipped successfully'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PHYSICAL 驳回:更新状态为 REJECTED,并退回积分到 available_points
|
|
||||||
*/
|
|
||||||
public function reject(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() !== 'POST') {
|
|
||||||
return $this->error(__('Parameter error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $request->post();
|
|
||||||
$id = intval($data['id'] ?? 0);
|
|
||||||
$rejectReason = strval($data['reject_reason'] ?? '');
|
|
||||||
|
|
||||||
if ($id <= 0 || $rejectReason === '') {
|
|
||||||
return $this->error(__('Missing required fields'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$order = MallPlayxOrder::where('id', $id)->find();
|
|
||||||
if (!$order) {
|
|
||||||
return $this->error(__('Record not found'));
|
|
||||||
}
|
|
||||||
if ($order->type !== MallPlayxOrder::TYPE_PHYSICAL) {
|
|
||||||
return $this->error(__('Order type not PHYSICAL'));
|
|
||||||
}
|
|
||||||
if ($order->status !== MallPlayxOrder::STATUS_PENDING) {
|
|
||||||
return $this->error(__('Order status must be PENDING'));
|
|
||||||
}
|
|
||||||
|
|
||||||
Db::startTrans();
|
|
||||||
try {
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', strval($order->user_id ?? ''))->find();
|
|
||||||
if (!$asset) {
|
|
||||||
$asset = MallPlayxUserAsset::create([
|
|
||||||
'user_id' => strval($order->user_id ?? ''),
|
|
||||||
'username' => strval($order->user_id ?? ''),
|
|
||||||
'locked_points' => 0,
|
|
||||||
'available_points' => 0,
|
|
||||||
'today_limit' => 0,
|
|
||||||
'today_claimed' => 0,
|
|
||||||
'today_limit_date' => null,
|
|
||||||
'create_time' => time(),
|
|
||||||
'update_time' => time(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$refund = intval($order->points_cost ?? 0);
|
|
||||||
if ($refund > 0) {
|
|
||||||
$asset->available_points += $refund;
|
|
||||||
$asset->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
$order->status = MallPlayxOrder::STATUS_REJECTED;
|
|
||||||
$order->reject_reason = $rejectReason;
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
|
|
||||||
$order->save();
|
|
||||||
|
|
||||||
Db::commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
Db::rollback();
|
|
||||||
return $this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(__('Rejected successfully'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动重试(仅红利/提现,且必须 FAILED_RETRYABLE)
|
|
||||||
*/
|
|
||||||
public function retry(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() !== 'POST') {
|
|
||||||
return $this->error(__('Parameter error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = intval($request->post('id', 0));
|
|
||||||
if ($id <= 0) {
|
|
||||||
return $this->error(__('Missing required fields'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$order = MallPlayxOrder::where('id', $id)->find();
|
|
||||||
if (!$order) {
|
|
||||||
return $this->error(__('Record not found'));
|
|
||||||
}
|
|
||||||
if (!in_array($order->type, [MallPlayxOrder::TYPE_BONUS, MallPlayxOrder::TYPE_WITHDRAW], true)) {
|
|
||||||
return $this->error(__('Only BONUS/WITHDRAW can retry'));
|
|
||||||
}
|
|
||||||
if ($order->grant_status !== MallPlayxOrder::GRANT_FAILED_RETRYABLE) {
|
|
||||||
return $this->error(__('Only FAILED_RETRYABLE can retry'));
|
|
||||||
}
|
|
||||||
if (intval($order->retry_count) >= 3) {
|
|
||||||
return $this->error(__('Retry count exceeded'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_NOT_SENT;
|
|
||||||
$order->save();
|
|
||||||
|
|
||||||
return $this->success(__('Retry queued'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\admin\controller\mall;
|
|
||||||
|
|
||||||
use Throwable;
|
|
||||||
use app\common\controller\Backend;
|
|
||||||
use support\Response;
|
|
||||||
use Webman\Http\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlayX 用户资产(后台列表)
|
|
||||||
*/
|
|
||||||
class PlayxUserAsset extends Backend
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var object|null
|
|
||||||
* @phpstan-var \app\common\model\MallPlayxUserAsset|null
|
|
||||||
*/
|
|
||||||
protected ?object $model = null;
|
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['user_id', 'username'];
|
|
||||||
|
|
||||||
protected string|array $indexField = [
|
|
||||||
'id',
|
|
||||||
'user_id',
|
|
||||||
'username',
|
|
||||||
'locked_points',
|
|
||||||
'available_points',
|
|
||||||
'today_limit',
|
|
||||||
'today_claimed',
|
|
||||||
'today_limit_date',
|
|
||||||
'create_time',
|
|
||||||
'update_time',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function initialize(): void
|
|
||||||
{
|
|
||||||
parent::initialize();
|
|
||||||
$this->model = new \app\common\model\MallPlayxUserAsset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查看
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->_index();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class RedemptionOrder extends Backend
|
|||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||||
|
|
||||||
protected array $withJoinTable = ['mallUser', 'mallItem'];
|
protected array $withJoinTable = ['playxUserAsset', 'mallItem'];
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['id'];
|
protected string|array $quickSearchField = ['id'];
|
||||||
|
|
||||||
@@ -52,10 +52,10 @@ class RedemptionOrder extends Backend
|
|||||||
*/
|
*/
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||||
$res = $this->model
|
$res = $this->model
|
||||||
->with(['mallUser' => function ($query) {
|
->with(['playxUserAsset' => function ($query) {
|
||||||
$query->field('id,username');
|
$query->field('id,username');
|
||||||
}])
|
}])
|
||||||
->visible(['mallUser' => ['username'], 'mallItem' => ['title']])
|
->visible(['playxUserAsset' => ['username'], 'mallItem' => ['title']])
|
||||||
->alias($alias)
|
->alias($alias)
|
||||||
->where($where)
|
->where($where)
|
||||||
->order($order)
|
->order($order)
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\admin\controller\mall;
|
|
||||||
|
|
||||||
use Throwable;
|
|
||||||
use app\common\controller\Backend;
|
|
||||||
use support\Response;
|
|
||||||
use Webman\Http\Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商城用户
|
|
||||||
*/
|
|
||||||
class User extends Backend
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \app\common\model\MallUser|null
|
|
||||||
*/
|
|
||||||
protected ?object $model = null;
|
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'password'];
|
|
||||||
|
|
||||||
protected array $withJoinTable = ['admin'];
|
|
||||||
|
|
||||||
protected string|array $quickSearchField = ['id', 'username', 'phone'];
|
|
||||||
|
|
||||||
/** 列表不返回密码 */
|
|
||||||
protected string|array $indexField = ['id', 'username', 'phone', 'score', 'daily_claim', 'daily_claim_use', 'available_for_withdrawal', 'admin_id', 'create_time', 'update_time'];
|
|
||||||
|
|
||||||
public function initialize(): void
|
|
||||||
{
|
|
||||||
parent::initialize();
|
|
||||||
$this->model = new \app\common\model\MallUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查看
|
|
||||||
*/
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->get('select') || $request->post('select')) {
|
|
||||||
return $this->select($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
|
||||||
$res = $this->model
|
|
||||||
->withoutField('password')
|
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
|
||||||
->visible(['admin' => ['username']])
|
|
||||||
->alias($alias)
|
|
||||||
->where($where)
|
|
||||||
->order($order)
|
|
||||||
->paginate($limit);
|
|
||||||
|
|
||||||
return $this->success('', [
|
|
||||||
'list' => $res->items(),
|
|
||||||
'total' => $res->total(),
|
|
||||||
'remark' => get_route_remark(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加(密码加密)
|
|
||||||
*/
|
|
||||||
public function add(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() !== 'POST') {
|
|
||||||
return $this->error(__('Parameter error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $request->post();
|
|
||||||
if (!$data) {
|
|
||||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
|
||||||
}
|
|
||||||
|
|
||||||
$passwd = $data['password'] ?? '';
|
|
||||||
if (empty($passwd)) {
|
|
||||||
return $this->error(__('Parameter %s can not be empty', [__('Password')]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->applyInputFilter($data);
|
|
||||||
$data = $this->excludeFields($data);
|
|
||||||
|
|
||||||
//保存管理员admin_id
|
|
||||||
$data['admin_id'] = $this->auth->id;
|
|
||||||
|
|
||||||
$result = false;
|
|
||||||
$this->model->startTrans();
|
|
||||||
try {
|
|
||||||
if ($this->modelValidate) {
|
|
||||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
|
||||||
if (class_exists($validate)) {
|
|
||||||
$validate = new $validate();
|
|
||||||
if ($this->modelSceneValidate) {
|
|
||||||
$validate->scene('add');
|
|
||||||
}
|
|
||||||
$validate->check($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->model->save($data);
|
|
||||||
if ($result !== false && $passwd) {
|
|
||||||
$this->model->resetPassword((int) $this->model->id, $passwd);
|
|
||||||
}
|
|
||||||
$this->model->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->model->rollback();
|
|
||||||
return $this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑(密码可选更新)
|
|
||||||
*/
|
|
||||||
public function edit(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pk = $this->model->getPk();
|
|
||||||
$id = $request->post($pk) ?? $request->get($pk);
|
|
||||||
$row = $this->model->find($id);
|
|
||||||
if (!$row) {
|
|
||||||
return $this->error(__('Record not found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
|
||||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
|
||||||
return $this->error(__('You have no permission'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->method() === 'POST') {
|
|
||||||
$data = $request->post();
|
|
||||||
if (!$data) {
|
|
||||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($data['password'])) {
|
|
||||||
$this->model->resetPassword((int) $row->id, $data['password']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $this->applyInputFilter($data);
|
|
||||||
$data = $this->excludeFields($data);
|
|
||||||
|
|
||||||
$result = false;
|
|
||||||
$this->model->startTrans();
|
|
||||||
try {
|
|
||||||
if ($this->modelValidate) {
|
|
||||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
|
||||||
if (class_exists($validate)) {
|
|
||||||
$validate = new $validate();
|
|
||||||
if ($this->modelSceneValidate) {
|
|
||||||
$validate->scene('edit');
|
|
||||||
}
|
|
||||||
$validate->check(array_merge($data, [$pk => $row[$pk]]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $row->save($data);
|
|
||||||
$this->model->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->model->rollback();
|
|
||||||
return $this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($row['password']);
|
|
||||||
$row['password'] = '';
|
|
||||||
return $this->success('', ['row' => $row]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 远程下拉数据(供 remoteSelect 使用)
|
|
||||||
*/
|
|
||||||
public function select(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
|
||||||
$res = $this->model
|
|
||||||
->withoutField('password')
|
|
||||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
|
||||||
->visible(['admin' => ['username']])
|
|
||||||
->alias($alias)
|
|
||||||
->where($where)
|
|
||||||
->order($order)
|
|
||||||
->paginate($limit);
|
|
||||||
|
|
||||||
return $this->success('', [
|
|
||||||
'list' => $res->items(),
|
|
||||||
'total' => $res->total(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除
|
|
||||||
*/
|
|
||||||
public function del(Request $request): Response
|
|
||||||
{
|
|
||||||
$response = $this->initializeBackend($request);
|
|
||||||
if ($response !== null) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
return $this->_del();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
82
app/admin/controller/mall/UserAsset.php
Normal file
82
app/admin/controller/mall/UserAsset.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\admin\controller\mall;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use support\Response;
|
||||||
|
use Webman\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户资产(后台列表)
|
||||||
|
*/
|
||||||
|
class UserAsset extends Backend
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var object|null
|
||||||
|
* @phpstan-var \app\common\model\MallUserAsset|null
|
||||||
|
*/
|
||||||
|
protected ?object $model = null;
|
||||||
|
|
||||||
|
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||||
|
|
||||||
|
protected string|array $quickSearchField = [
|
||||||
|
'playx_user_id',
|
||||||
|
'username',
|
||||||
|
'phone',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected string|array $indexField = [
|
||||||
|
'id',
|
||||||
|
'playx_user_id',
|
||||||
|
'username',
|
||||||
|
'phone',
|
||||||
|
'locked_points',
|
||||||
|
'available_points',
|
||||||
|
'today_limit',
|
||||||
|
'today_claimed',
|
||||||
|
'today_limit_date',
|
||||||
|
'create_time',
|
||||||
|
'update_time',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
parent::initialize();
|
||||||
|
$this->model = new \app\common\model\MallUserAsset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程下拉:资产主键 id + 用户名(用于地址/订单等关联)
|
||||||
|
*/
|
||||||
|
public function select(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$where, $alias, $limit, $order] = $this->queryBuilder();
|
||||||
|
$res = $this->model
|
||||||
|
->field('id,username')
|
||||||
|
->alias($alias)
|
||||||
|
->where($where)
|
||||||
|
->order($order)
|
||||||
|
->paginate($limit);
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
foreach ($res->items() as $row) {
|
||||||
|
$arr = $row->toArray();
|
||||||
|
$list[] = [
|
||||||
|
'id' => $arr['id'] ?? 0,
|
||||||
|
'username' => $arr['username'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $res->total(),
|
||||||
|
'remark' => get_route_remark(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class AdminInfo extends Backend
|
|||||||
{
|
{
|
||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['username', 'last_login_time', 'password', 'salt', 'status'];
|
protected array|string $preExcludeFields = ['username', 'last_login_time', 'password', 'salt', 'status', 'channel_id', 'agent_id', 'agent_api_secret'];
|
||||||
protected array $authAllowFields = ['id', 'username', 'nickname', 'avatar', 'email', 'mobile', 'motto', 'last_login_time'];
|
protected array $authAllowFields = ['id', 'username', 'nickname', 'avatar', 'email', 'mobile', 'motto', 'last_login_time'];
|
||||||
|
|
||||||
protected function initController(Request $request): ?Response
|
protected function initController(Request $request): ?Response
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Attachment extends Backend
|
|||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
protected array|string $quickSearchField = 'name';
|
protected array|string $quickSearchField = 'name';
|
||||||
protected array $withJoinTable = ['admin', 'user'];
|
protected array $withJoinTable = ['admin'];
|
||||||
protected array|string $defaultSortField = ['last_upload_time' => 'desc'];
|
protected array|string $defaultSortField = ['last_upload_time' => 'desc'];
|
||||||
|
|
||||||
protected function initController(Request $request): ?Response
|
protected function initController(Request $request): ?Response
|
||||||
|
|||||||
@@ -95,4 +95,9 @@ return [
|
|||||||
'%d records and files have been deleted' => '%d records and files have been deleted',
|
'%d records and files have been deleted' => '%d records and files have been deleted',
|
||||||
'Please input correct username' => 'Please enter the correct username',
|
'Please input correct username' => 'Please enter the correct username',
|
||||||
'Group Name Arr' => 'Group Name Arr',
|
'Group Name Arr' => 'Group Name Arr',
|
||||||
|
'Push succeeded' => 'Push succeeded',
|
||||||
|
'Manual push failed' => 'Manual push failed',
|
||||||
|
'PlayX API not configured' => 'PlayX API not configured',
|
||||||
|
'Current grant status cannot be manually pushed' => 'Current grant status cannot be manually pushed',
|
||||||
|
'Order status must be PENDING' => 'Order status must be PENDING',
|
||||||
];
|
];
|
||||||
@@ -2,6 +2,4 @@
|
|||||||
return [
|
return [
|
||||||
'Group Name Arr' => 'Administrator Grouping ',
|
'Group Name Arr' => 'Administrator Grouping ',
|
||||||
'Please use another administrator account to disable the current account!' => 'Disable the current account, please use another administrator account!',
|
'Please use another administrator account to disable the current account!' => 'Disable the current account, please use another administrator account!',
|
||||||
'Please select channel' => 'Please select channel',
|
|
||||||
'Current admin has no channel bound' => 'Current admin has no channel bound',
|
|
||||||
];
|
];
|
||||||
@@ -114,4 +114,9 @@ return [
|
|||||||
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
||||||
'Please input correct username' => '请输入正确的用户名',
|
'Please input correct username' => '请输入正确的用户名',
|
||||||
'Group Name Arr' => '分组名称数组',
|
'Group Name Arr' => '分组名称数组',
|
||||||
|
'Push succeeded' => '推送成功',
|
||||||
|
'Manual push failed' => '手动推送失败',
|
||||||
|
'PlayX API not configured' => 'PlayX 接口未配置',
|
||||||
|
'Current grant status cannot be manually pushed' => '当前发放状态不可手动推送',
|
||||||
|
'Order status must be PENDING' => '订单状态须为处理中',
|
||||||
];
|
];
|
||||||
@@ -3,6 +3,4 @@ return [
|
|||||||
'Group Name Arr' => '管理员分组',
|
'Group Name Arr' => '管理员分组',
|
||||||
'Please use another administrator account to disable the current account!' => '请使用另外的管理员账户禁用当前账户!',
|
'Please use another administrator account to disable the current account!' => '请使用另外的管理员账户禁用当前账户!',
|
||||||
'You have no permission to add an administrator to this group!' => '您没有权限向此分组添加管理员!',
|
'You have no permission to add an administrator to this group!' => '您没有权限向此分组添加管理员!',
|
||||||
'Please select channel' => '请选择渠道',
|
|
||||||
'Current admin has no channel bound' => '当前管理员未绑定渠道',
|
|
||||||
];
|
];
|
||||||
@@ -33,7 +33,7 @@ class Auth extends \ba\Auth
|
|||||||
protected string $refreshToken = '';
|
protected string $refreshToken = '';
|
||||||
protected int $keepTime = 86400;
|
protected int $keepTime = 86400;
|
||||||
protected int $refreshTokenKeepTime = 2592000;
|
protected int $refreshTokenKeepTime = 2592000;
|
||||||
protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time', 'channel_id'];
|
protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time'];
|
||||||
|
|
||||||
public function __construct(array $config = [])
|
public function __construct(array $config = [])
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,13 +21,23 @@ use support\think\Db;
|
|||||||
* @property string $password 密码密文
|
* @property string $password 密码密文
|
||||||
* @property string $salt 密码盐
|
* @property string $salt 密码盐
|
||||||
* @property string $status 状态:enable=启用,disable=禁用
|
* @property string $status 状态:enable=启用,disable=禁用
|
||||||
* @property string $agent_id 代理ID(关联渠道)
|
* @property string $agent_id 代理 ID(API 鉴权)
|
||||||
* @property int $channel_id 渠道ID
|
* @property string $agent_api_secret Agent API 密钥
|
||||||
*/
|
*/
|
||||||
class Admin extends Model
|
class Admin extends Model
|
||||||
{
|
{
|
||||||
use TimestampInteger;
|
use TimestampInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已移除的 channel_id 等若仍被旧请求/缓存传入,禁止参与读写
|
||||||
|
*/
|
||||||
|
protected function getOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'disuse' => ['channel_id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected string $table = 'admin';
|
protected string $table = 'admin';
|
||||||
protected string $pk = 'id';
|
protected string $pk = 'id';
|
||||||
protected bool $autoWriteTimestamp = true;
|
protected bool $autoWriteTimestamp = true;
|
||||||
@@ -66,12 +76,4 @@ class Admin extends Model
|
|||||||
{
|
{
|
||||||
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
|
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 关联渠道
|
|
||||||
*/
|
|
||||||
public function channel(): \think\model\relation\BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(\app\common\model\ChannelManage::class, 'channel_id', 'id');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\admin\model\mall;
|
|
||||||
|
|
||||||
use app\common\model\traits\TimestampInteger;
|
|
||||||
use support\think\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Player
|
|
||||||
*/
|
|
||||||
class Player extends Model
|
|
||||||
{
|
|
||||||
use TimestampInteger;
|
|
||||||
|
|
||||||
// 表名
|
|
||||||
protected $name = 'mall_player';
|
|
||||||
|
|
||||||
// 自动写入时间戳字段
|
|
||||||
protected $autoWriteTimestamp = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置密码
|
|
||||||
*/
|
|
||||||
public function resetPassword(int $id, string $newPassword): bool
|
|
||||||
{
|
|
||||||
return $this->where(['id' => $id])->update(['password' => hash_password($newPassword)]) !== false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -75,6 +75,9 @@ class Common extends Api
|
|||||||
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') {
|
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') {
|
||||||
Token::set($newToken, UserAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
|
Token::set($newToken, UserAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
|
||||||
}
|
}
|
||||||
|
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE_MALL_USER . '-refresh') {
|
||||||
|
Token::set($newToken, UserAuth::TOKEN_TYPE_MALL_USER, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
|
||||||
|
}
|
||||||
|
|
||||||
return $this->success('', [
|
return $this->success('', [
|
||||||
'type' => $refreshToken['type'],
|
'type' => $refreshToken['type'],
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\api\controller\v1;
|
namespace app\api\controller\v1;
|
||||||
|
|
||||||
|
use ba\Random;
|
||||||
|
use Throwable;
|
||||||
use app\common\controller\Api;
|
use app\common\controller\Api;
|
||||||
|
use app\common\facade\Token;
|
||||||
|
use app\common\library\Auth as UserAuth;
|
||||||
use app\common\library\AgentJwt;
|
use app\common\library\AgentJwt;
|
||||||
use app\common\model\ChannelManage;
|
use app\common\model\MallUserAsset;
|
||||||
use app\admin\model\Admin;
|
use app\admin\model\Admin;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
@@ -26,6 +30,11 @@ class Auth extends Api
|
|||||||
*/
|
*/
|
||||||
protected int $timeTolerance = 300;
|
protected int $timeTolerance = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时登录 token 有效期(秒)
|
||||||
|
*/
|
||||||
|
protected int $tempTokenExpire = 86400;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取鉴权 Token(GET 请求)
|
* 获取鉴权 Token(GET 请求)
|
||||||
* 参数仅从 Query 读取:signature、secret、agent_id、time
|
* 参数仅从 Query 读取:signature、secret、agent_id、time
|
||||||
@@ -47,7 +56,7 @@ class Auth extends Api
|
|||||||
return $this->error(__('Parameter signature/secret/agent_id/time can not be empty'));
|
return $this->error(__('Parameter signature/secret/agent_id/time can not be empty'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$timestamp = (int) $time;
|
$timestamp = intval($time);
|
||||||
if ($timestamp <= 0) {
|
if ($timestamp <= 0) {
|
||||||
return $this->error(__('Invalid timestamp'));
|
return $this->error(__('Invalid timestamp'));
|
||||||
}
|
}
|
||||||
@@ -62,17 +71,12 @@ class Auth extends Api
|
|||||||
return $this->error(__('Agent not found'));
|
return $this->error(__('Agent not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$channelId = (int) ($admin->channel_id ?? 0);
|
$apiSecret = strval($admin->agent_api_secret ?? '');
|
||||||
if ($channelId <= 0) {
|
if ($apiSecret === '') {
|
||||||
return $this->error(__('Agent not found'));
|
return $this->error(__('Agent not found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$channel = ChannelManage::where('id', $channelId)->find();
|
if ($apiSecret !== $secret) {
|
||||||
if (!$channel || $channel->secret === '') {
|
|
||||||
return $this->error(__('Agent not found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($channel->secret !== $secret) {
|
|
||||||
return $this->error(__('Invalid agent or secret'));
|
return $this->error(__('Invalid agent or secret'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,11 +85,10 @@ class Auth extends Api
|
|||||||
return $this->error(__('Invalid signature'));
|
return $this->error(__('Invalid signature'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$expire = (int) config('buildadmin.agent_auth.token_expire', 86400);
|
$expire = intval(config('buildadmin.agent_auth.token_expire', 86400));
|
||||||
$payload = [
|
$payload = [
|
||||||
'agent_id' => $agentId,
|
'agent_id' => $agentId,
|
||||||
'channel_id' => $channel->id,
|
'admin_id' => $admin->id,
|
||||||
'admin_id' => $admin->id,
|
|
||||||
];
|
];
|
||||||
$authtoken = AgentJwt::encode($payload, $expire);
|
$authtoken = AgentJwt::encode($payload, $expire);
|
||||||
|
|
||||||
@@ -93,4 +96,70 @@ class Auth extends Api
|
|||||||
'authtoken' => $authtoken,
|
'authtoken' => $authtoken,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5 临时登录(GET/POST)
|
||||||
|
* 参数:username
|
||||||
|
* 写入或复用 mall_user_asset;签发 muser 类型 token(user_id 为资产表主键)
|
||||||
|
*/
|
||||||
|
public function temLogin(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeApi($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$enabled = config('buildadmin.agent_auth.temp_login_enable', false);
|
||||||
|
if (!$enabled) {
|
||||||
|
return $this->error(__('Temp login is disabled'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim(strval($request->get('username', $request->post('username', ''))));
|
||||||
|
// 兼容:querystring 中未编码的 '+' 会被解析为空格(application/x-www-form-urlencoded 规则)
|
||||||
|
// 例如:/api/v1/temLogin?username=+607... 期望保留 '+',则从原始 querystring 提取并还原
|
||||||
|
if ($username !== '' && str_contains($username, ' ')) {
|
||||||
|
$qs = $request->queryString();
|
||||||
|
if (is_string($qs) && $qs !== '') {
|
||||||
|
foreach (explode('&', $qs) as $pair) {
|
||||||
|
if ($pair === '' || !str_contains($pair, '=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$k, $v] = explode('=', $pair, 2);
|
||||||
|
if (rawurldecode($k) === 'username') {
|
||||||
|
// 先把 %xx 解码;注意这里不把 '+' 当空格处理,从而保留 '+'
|
||||||
|
$username = trim(rawurldecode($v));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($username === '') {
|
||||||
|
return $this->error(__('Parameter username can not be empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$asset = MallUserAsset::ensureForUsername($username);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = Random::uuid();
|
||||||
|
$refreshToken = Random::uuid();
|
||||||
|
$expire = config('buildadmin.agent_auth.temp_login_expire', $this->tempTokenExpire);
|
||||||
|
$assetId = intval($asset->getKey());
|
||||||
|
Token::set($token, UserAuth::TOKEN_TYPE_MALL_USER, $assetId, $expire);
|
||||||
|
Token::set($refreshToken, UserAuth::TOKEN_TYPE_MALL_USER . '-refresh', $assetId, 2592000);
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'userInfo' => [
|
||||||
|
'id' => $assetId,
|
||||||
|
'username' => strval($asset->username ?? ''),
|
||||||
|
'nickname' => strval($asset->username ?? ''),
|
||||||
|
'playx_user_id' => strval($asset->playx_user_id ?? ''),
|
||||||
|
'token' => $token,
|
||||||
|
'refresh_token' => $refreshToken,
|
||||||
|
'expires_in' => $expire,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\api\controller\v1;
|
namespace app\api\controller\v1;
|
||||||
|
|
||||||
|
use ba\Random;
|
||||||
use app\common\controller\Api;
|
use app\common\controller\Api;
|
||||||
|
use app\common\facade\Token;
|
||||||
|
use app\common\library\Auth as UserAuth;
|
||||||
use app\common\model\MallItem;
|
use app\common\model\MallItem;
|
||||||
use app\common\model\MallPlayxClaimLog;
|
use app\common\model\MallClaimLog;
|
||||||
use app\common\model\MallPlayxDailyPush;
|
use app\common\model\MallDailyPush;
|
||||||
use app\common\model\MallPlayxSession;
|
use app\common\model\MallSession;
|
||||||
use app\common\model\MallPlayxOrder;
|
use app\common\model\MallOrder;
|
||||||
use app\common\model\MallPlayxUserAsset;
|
use app\common\model\MallUserAsset;
|
||||||
|
use app\common\model\MallAddress;
|
||||||
use support\think\Db;
|
use support\think\Db;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
@@ -21,33 +25,130 @@ use support\Response;
|
|||||||
class Playx extends Api
|
class Playx extends Api
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 从请求中解析 PlayX 会话用户ID(优先 session_id,其次 user_id)
|
* 从请求解析 mall_user_asset.id(muser token、session、user_id 均指向资产表主键或 playx_user_id)
|
||||||
*/
|
*/
|
||||||
private function resolveUserIdFromRequest(Request $request): ?string
|
private function resolvePlayxAssetIdFromRequest(Request $request): ?int
|
||||||
{
|
{
|
||||||
$sessionId = strval($request->post('session_id', $request->get('session_id', '')));
|
$sessionId = strval($request->post('session_id', $request->get('session_id', '')));
|
||||||
if ($sessionId !== '') {
|
if ($sessionId !== '') {
|
||||||
$session = MallPlayxSession::where('session_id', $sessionId)->find();
|
$session = MallSession::where('session_id', $sessionId)->find();
|
||||||
if (!$session) {
|
if ($session) {
|
||||||
return null;
|
$expireTime = intval($session->expire_time ?? 0);
|
||||||
|
if ($expireTime > time()) {
|
||||||
|
$asset = MallUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find();
|
||||||
|
if ($asset) {
|
||||||
|
return intval($asset->getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$expireTime = intval($session->expire_time ?? 0);
|
$assetId = $this->resolveAssetIdByToken($sessionId);
|
||||||
if ($expireTime <= time()) {
|
if ($assetId !== null) {
|
||||||
return null;
|
return $assetId;
|
||||||
}
|
}
|
||||||
return strval($session->user_id ?? '');
|
}
|
||||||
|
|
||||||
|
$token = strval($request->post('token', $request->get('token', '')));
|
||||||
|
if ($token === '') {
|
||||||
|
$token = get_auth_token(['ba', 'token'], $request);
|
||||||
|
}
|
||||||
|
if ($token !== '') {
|
||||||
|
return $this->resolveAssetIdByToken($token);
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = strval($request->post('user_id', $request->get('user_id', '')));
|
$userId = strval($request->post('user_id', $request->get('user_id', '')));
|
||||||
if ($userId === '') {
|
if ($userId === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return $userId;
|
if (ctype_digit($userId)) {
|
||||||
|
return intval($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset = MallUserAsset::where('playx_user_id', $userId)->find();
|
||||||
|
if ($asset) {
|
||||||
|
return intval($asset->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveAssetIdByToken(string $token): ?int
|
||||||
|
{
|
||||||
|
$tokenData = Token::get($token);
|
||||||
|
$tokenType = strval($tokenData['type'] ?? '');
|
||||||
|
$isMemberOrMall = $tokenType === UserAuth::TOKEN_TYPE || $tokenType === UserAuth::TOKEN_TYPE_MALL_USER;
|
||||||
|
if (!empty($tokenData)
|
||||||
|
&& $isMemberOrMall
|
||||||
|
&& intval($tokenData['expire_time'] ?? 0) > time()
|
||||||
|
&& intval($tokenData['user_id'] ?? 0) > 0
|
||||||
|
) {
|
||||||
|
return intval($tokenData['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTempPhone(): ?string
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < 8; $i++) {
|
||||||
|
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
|
||||||
|
if (!MallUserAsset::where('phone', $candidate)->find()) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallUserAsset
|
||||||
|
{
|
||||||
|
$asset = MallUserAsset::where('playx_user_id', $playxUserId)->find();
|
||||||
|
if ($asset) {
|
||||||
|
return $asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effectiveUsername = trim($username);
|
||||||
|
if ($effectiveUsername === '') {
|
||||||
|
$effectiveUsername = 'playx_' . $playxUserId;
|
||||||
|
}
|
||||||
|
$byName = MallUserAsset::where('username', $effectiveUsername)->find();
|
||||||
|
if ($byName) {
|
||||||
|
$byName->playx_user_id = $playxUserId;
|
||||||
|
$byName->save();
|
||||||
|
|
||||||
|
return $byName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = $this->buildTempPhone();
|
||||||
|
if ($phone === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$pwd = hash_password(Random::build('alnum', 16));
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
return MallUserAsset::create([
|
||||||
|
'playx_user_id' => $playxUserId,
|
||||||
|
'username' => $effectiveUsername,
|
||||||
|
'phone' => $phone,
|
||||||
|
'password' => $pwd,
|
||||||
|
'admin_id' => 0,
|
||||||
|
'locked_points' => 0,
|
||||||
|
'available_points' => 0,
|
||||||
|
'today_limit' => 0,
|
||||||
|
'today_claimed' => 0,
|
||||||
|
'today_limit_date' => null,
|
||||||
|
'create_time' => $now,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAssetById(int $assetId): ?MallUserAsset
|
||||||
|
{
|
||||||
|
return MallUserAsset::where('id', $assetId)->find();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daily Push API - PlayX 调用商城接收 T+1 数据
|
* Daily Push API - PlayX 调用商城接收 T+1 数据
|
||||||
* POST /api/v1/playx/daily-push
|
* POST /api/v1/mall/dailyPush
|
||||||
*/
|
*/
|
||||||
public function dailyPush(Request $request): Response
|
public function dailyPush(Request $request): Response
|
||||||
{
|
{
|
||||||
@@ -64,45 +165,159 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$requestId = $body['request_id'] ?? '';
|
|
||||||
$date = $body['date'] ?? '';
|
|
||||||
$userId = $body['user_id'] ?? '';
|
|
||||||
$yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0;
|
|
||||||
$yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0;
|
|
||||||
|
|
||||||
if ($requestId === '' || $date === '' || $userId === '') {
|
|
||||||
return $this->error(__('Missing required fields: request_id, date, user_id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$secret = config('playx.daily_push_secret', '');
|
$secret = config('playx.daily_push_secret', '');
|
||||||
if ($secret !== '') {
|
if ($secret !== '') {
|
||||||
$sig = $request->header('X-Signature', '');
|
$sig = $request->header('X-Signature', '');
|
||||||
$ts = $request->header('X-Timestamp', '');
|
$ts = $request->header('X-Timestamp', '');
|
||||||
$rid = $request->header('X-Request-Id', '');
|
$rid = $request->header('X-Request-Id', '');
|
||||||
if ($sig === '' || $ts === '' || $rid === '') {
|
if ($sig === '' || $ts === '' || $rid === '') {
|
||||||
return $this->error('INVALID_SIGNATURE', null, 0, ['statusCode' => 401]);
|
return $this->error(__('Daily push signature missing or incomplete'), null, 0, ['statusCode' => 401]);
|
||||||
}
|
}
|
||||||
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', json_encode($body));
|
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/mall/dailyPush\n" . hash('sha256', json_encode($body));
|
||||||
$expected = hash_hmac('sha256', $canonical, $secret);
|
$expected = hash_hmac('sha256', $canonical, $secret);
|
||||||
if (!hash_equals($expected, $sig)) {
|
if (!hash_equals($expected, $sig)) {
|
||||||
return $this->error('INVALID_SIGNATURE', null, 0, ['statusCode' => 401]);
|
return $this->error(__('Daily push signature verification failed'), null, 0, ['statusCode' => 401]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$exists = MallPlayxDailyPush::where('user_id', $userId)->where('date', $date)->find();
|
// ===== 新版批量上报格式 =====
|
||||||
|
// 兼容你们截图:{ report_date, member:[{member_id, login, lty_deposit, lty_withdrawal, yesterday_total_w, yesterday_total_deposit}, ...] }
|
||||||
|
if (isset($body['report_date']) && isset($body['member']) && is_array($body['member'])) {
|
||||||
|
$reportDate = $body['report_date'];
|
||||||
|
$date = '';
|
||||||
|
if (is_numeric($reportDate)) {
|
||||||
|
$date = date('Y-m-d', intval($reportDate));
|
||||||
|
} else {
|
||||||
|
$date = strval($reportDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$members = $body['member'];
|
||||||
|
if ($date === '' || empty($members)) {
|
||||||
|
return $this->error(__('Missing required fields: report_date, member'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestId = strval($body['request_id'] ?? '');
|
||||||
|
if ($requestId === '') {
|
||||||
|
$requestId = 'report_' . $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnRatio = config('playx.return_ratio', 0.1);
|
||||||
|
$unlockRatio = config('playx.unlock_ratio', 0.1);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$allDeduped = true;
|
||||||
|
|
||||||
|
foreach ($members as $m) {
|
||||||
|
$playxUserId = strval($m['member_id'] ?? '');
|
||||||
|
if ($playxUserId === '') {
|
||||||
|
return $this->error(__('Missing required fields: member_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = strval($m['login'] ?? '');
|
||||||
|
$yesterdayWinLossNet = $m['yesterday_total_w'] ?? 0;
|
||||||
|
$yesterdayTotalDeposit = $m['yesterday_total_deposit'] ?? 0;
|
||||||
|
$lifetimeTotalDeposit = $m['lty_deposit'] ?? 0;
|
||||||
|
$lifetimeTotalWithdraw = $m['lty_withdrawal'] ?? 0;
|
||||||
|
|
||||||
|
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
|
||||||
|
if ($exists) {
|
||||||
|
$results[] = [
|
||||||
|
'user_id' => $playxUserId,
|
||||||
|
'deduped' => true,
|
||||||
|
'accepted' => true,
|
||||||
|
'message' => __('Duplicate input'),
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
MallDailyPush::create([
|
||||||
|
'user_id' => $playxUserId,
|
||||||
|
'date' => $date,
|
||||||
|
'username' => $username,
|
||||||
|
'yesterday_win_loss_net' => $yesterdayWinLossNet,
|
||||||
|
'yesterday_total_deposit' => $yesterdayTotalDeposit,
|
||||||
|
'lifetime_total_deposit' => $lifetimeTotalDeposit,
|
||||||
|
'lifetime_total_withdraw' => $lifetimeTotalWithdraw,
|
||||||
|
'create_time' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newLocked = 0;
|
||||||
|
if ($yesterdayWinLossNet < 0) {
|
||||||
|
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||||
|
}
|
||||||
|
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||||
|
|
||||||
|
$asset = $this->ensureAssetForPlayx($playxUserId, $username);
|
||||||
|
if (!$asset) {
|
||||||
|
throw new \RuntimeException(__('Failed to ensure PlayX user asset'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($asset->today_limit_date !== $date) {
|
||||||
|
$asset->today_claimed = 0;
|
||||||
|
$asset->today_limit_date = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
|
||||||
|
$asset->today_limit = $todayLimit;
|
||||||
|
$asset->playx_user_id = $playxUserId;
|
||||||
|
|
||||||
|
$uname = trim($username);
|
||||||
|
if ($uname !== '') {
|
||||||
|
$asset->username = $uname;
|
||||||
|
}
|
||||||
|
$asset->save();
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'user_id' => $playxUserId,
|
||||||
|
'deduped' => false,
|
||||||
|
'accepted' => true,
|
||||||
|
'message' => __('Ok'),
|
||||||
|
];
|
||||||
|
$allDeduped = false;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'request_id' => $requestId,
|
||||||
|
'accepted' => true,
|
||||||
|
'deduped' => $allDeduped,
|
||||||
|
'message' => $allDeduped ? __('Duplicate input') : __('Ok'),
|
||||||
|
'results' => $results,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 旧版单条上报格式(兼容)=====
|
||||||
|
$requestId = $body['request_id'] ?? '';
|
||||||
|
$date = $body['date'] ?? '';
|
||||||
|
$playxUserId = strval($body['user_id'] ?? '');
|
||||||
|
$yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0;
|
||||||
|
$yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0;
|
||||||
|
|
||||||
|
if ($requestId === '' || $date === '' || $playxUserId === '') {
|
||||||
|
return $this->error(__('Missing required fields: request_id, date, user_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
return $this->success('', [
|
return $this->success('', [
|
||||||
'request_id' => $requestId,
|
'request_id' => $requestId,
|
||||||
'accepted' => true,
|
'accepted' => true,
|
||||||
'deduped' => true,
|
'deduped' => true,
|
||||||
'message' => 'duplicate input',
|
'message' => __('Duplicate input'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
MallPlayxDailyPush::create([
|
MallDailyPush::create([
|
||||||
'user_id' => $userId,
|
'user_id' => $playxUserId,
|
||||||
'date' => $date,
|
'date' => $date,
|
||||||
'username' => $body['username'] ?? '',
|
'username' => $body['username'] ?? '',
|
||||||
'yesterday_win_loss_net' => $yesterdayWinLossNet,
|
'yesterday_win_loss_net' => $yesterdayWinLossNet,
|
||||||
@@ -112,40 +327,34 @@ class Playx extends Api
|
|||||||
'create_time' => time(),
|
'create_time' => time(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$newLocked = 0;
|
||||||
$returnRatio = config('playx.return_ratio', 0.1);
|
$returnRatio = config('playx.return_ratio', 0.1);
|
||||||
$unlockRatio = config('playx.unlock_ratio', 0.1);
|
$unlockRatio = config('playx.unlock_ratio', 0.1);
|
||||||
|
|
||||||
$newLocked = 0;
|
|
||||||
if ($yesterdayWinLossNet < 0) {
|
if ($yesterdayWinLossNet < 0) {
|
||||||
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
$newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio));
|
||||||
}
|
}
|
||||||
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
|
||||||
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
$asset = $this->ensureAssetForPlayx($playxUserId, strval($body['username'] ?? ''));
|
||||||
$todayLimitDate = $date;
|
if (!$asset) {
|
||||||
if ($asset) {
|
throw new \RuntimeException(__('Failed to ensure PlayX user asset'));
|
||||||
if ($asset->today_limit_date !== $todayLimitDate) {
|
|
||||||
$asset->today_claimed = 0;
|
|
||||||
$asset->today_limit_date = $todayLimitDate;
|
|
||||||
}
|
|
||||||
$asset->locked_points += $newLocked;
|
|
||||||
$asset->today_limit = $todayLimit;
|
|
||||||
$asset->username = $body['username'] ?? $asset->username;
|
|
||||||
$asset->save();
|
|
||||||
} else {
|
|
||||||
MallPlayxUserAsset::create([
|
|
||||||
'user_id' => $userId,
|
|
||||||
'username' => $body['username'] ?? '',
|
|
||||||
'locked_points' => $newLocked,
|
|
||||||
'available_points' => 0,
|
|
||||||
'today_limit' => $todayLimit,
|
|
||||||
'today_claimed' => 0,
|
|
||||||
'today_limit_date' => $todayLimitDate,
|
|
||||||
'create_time' => time(),
|
|
||||||
'update_time' => time(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($asset->today_limit_date !== $date) {
|
||||||
|
$asset->today_claimed = 0;
|
||||||
|
$asset->today_limit_date = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
|
||||||
|
$asset->today_limit = $todayLimit;
|
||||||
|
$asset->playx_user_id = $playxUserId;
|
||||||
|
|
||||||
|
$uname = trim(strval($body['username'] ?? ''));
|
||||||
|
if ($uname !== '') {
|
||||||
|
$asset->username = $uname;
|
||||||
|
}
|
||||||
|
$asset->save();
|
||||||
|
|
||||||
Db::commit();
|
Db::commit();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
@@ -156,13 +365,13 @@ class Playx extends Api
|
|||||||
'request_id' => $requestId,
|
'request_id' => $requestId,
|
||||||
'accepted' => true,
|
'accepted' => true,
|
||||||
'deduped' => false,
|
'deduped' => false,
|
||||||
'message' => 'ok',
|
'message' => __('Ok'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token 验证 - 接收前端 token,调用 PlayX 验证(占位,待 PlayX 提供 API)
|
* Token 验证 - POST /api/v1/playx/verify-token
|
||||||
* POST /api/v1/playx/verify-token
|
* 配置 playx.verify_token_local_only=true 时仅本地校验 token(不请求 PlayX)。
|
||||||
*/
|
*/
|
||||||
public function verifyToken(Request $request): Response
|
public function verifyToken(Request $request): Response
|
||||||
{
|
{
|
||||||
@@ -171,15 +380,19 @@ class Playx extends Api
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $request->post('token', $request->post('session', ''));
|
$token = strval($request->post('token', $request->post('session', $request->get('token', ''))));
|
||||||
if ($token === '') {
|
if ($token === '') {
|
||||||
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config('playx.verify_token_local_only', false)) {
|
||||||
|
return $this->verifyTokenLocal($token);
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseUrl = config('playx.api.base_url', '');
|
$baseUrl = config('playx.api.base_url', '');
|
||||||
$verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token');
|
$verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token');
|
||||||
if ($baseUrl === '') {
|
if ($baseUrl === '') {
|
||||||
return $this->error('PlayX API not configured');
|
return $this->error(__('PlayX API not configured'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -196,7 +409,10 @@ class Playx extends Api
|
|||||||
$code = $res->getStatusCode();
|
$code = $res->getStatusCode();
|
||||||
$data = json_decode(strval($res->getBody()), true);
|
$data = json_decode(strval($res->getBody()), true);
|
||||||
if ($code !== 200 || empty($data['user_id'])) {
|
if ($code !== 200 || empty($data['user_id'])) {
|
||||||
return $this->error($data['message'] ?? 'INVALID_TOKEN', null, 0, ['statusCode' => 401]);
|
$remoteMsg = $data['message'] ?? '';
|
||||||
|
$msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Token expiration');
|
||||||
|
|
||||||
|
return $this->error($msg, null, 0, ['statusCode' => 401]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = strval($data['user_id']);
|
$userId = strval($data['user_id']);
|
||||||
@@ -211,7 +427,7 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sessionId = bin2hex(random_bytes(16));
|
$sessionId = bin2hex(random_bytes(16));
|
||||||
MallPlayxSession::create([
|
MallSession::create([
|
||||||
'session_id' => $sessionId,
|
'session_id' => $sessionId,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
@@ -231,6 +447,53 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地校验 temLogin 等写入的商城 token(类型 muser),写入 mall_session
|
||||||
|
*/
|
||||||
|
private function verifyTokenLocal(string $token): Response
|
||||||
|
{
|
||||||
|
$tokenData = Token::get($token);
|
||||||
|
if (empty($tokenData) || (isset($tokenData['expire_time']) && intval($tokenData['expire_time']) <= time())) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
$tokenType = strval($tokenData['type'] ?? '');
|
||||||
|
if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
$assetId = intval($tokenData['user_id'] ?? 0);
|
||||||
|
if ($assetId <= 0) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset = MallUserAsset::where('id', $assetId)->find();
|
||||||
|
if (!$asset) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$playxUserId = strval($asset->playx_user_id ?? '');
|
||||||
|
if ($playxUserId === '') {
|
||||||
|
$playxUserId = strval($assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expireAt = time() + intval(config('playx.session_expire_seconds', 3600));
|
||||||
|
$sessionId = bin2hex(random_bytes(16));
|
||||||
|
MallSession::create([
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'user_id' => $playxUserId,
|
||||||
|
'username' => strval($asset->username ?? ''),
|
||||||
|
'expire_time' => $expireAt,
|
||||||
|
'create_time' => time(),
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'session_id' => $sessionId,
|
||||||
|
'user_id' => $playxUserId,
|
||||||
|
'username' => strval($asset->username ?? ''),
|
||||||
|
'token_expire_at' => date('c', $expireAt),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户资产
|
* 用户资产
|
||||||
* GET /api/v1/playx/assets?user_id=xxx
|
* GET /api/v1/playx/assets?user_id=xxx
|
||||||
@@ -242,12 +505,12 @@ class Playx extends Api
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = $this->resolveUserIdFromRequest($request);
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
if ($userId === null) {
|
if ($assetId === null) {
|
||||||
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
$asset = $this->getAssetById($assetId);
|
||||||
if (!$asset) {
|
if (!$asset) {
|
||||||
return $this->success('', [
|
return $this->success('', [
|
||||||
'locked_points' => 0,
|
'locked_points' => 0,
|
||||||
@@ -282,21 +545,24 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
$claimRequestId = strval($request->post('claim_request_id', ''));
|
$claimRequestId = strval($request->post('claim_request_id', ''));
|
||||||
$userId = $this->resolveUserIdFromRequest($request);
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
if ($claimRequestId === '' || $userId === null) {
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
if ($claimRequestId === '') {
|
||||||
return $this->error(__('claim_request_id and user_id/session_id required'));
|
return $this->error(__('claim_request_id and user_id/session_id required'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$exists = MallPlayxClaimLog::where('claim_request_id', $claimRequestId)->find();
|
$asset = $this->getAssetById($assetId);
|
||||||
if ($exists) {
|
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
|
||||||
return $this->success('', $this->formatAsset($asset));
|
|
||||||
}
|
|
||||||
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
|
||||||
if (!$asset) {
|
|
||||||
return $this->error(__('User asset not found'));
|
return $this->error(__('User asset not found'));
|
||||||
}
|
}
|
||||||
|
$playxUserId = strval($asset->playx_user_id);
|
||||||
|
|
||||||
|
$exists = MallClaimLog::where('claim_request_id', $claimRequestId)->find();
|
||||||
|
if ($exists) {
|
||||||
|
return $this->success('', $this->formatAsset($asset));
|
||||||
|
}
|
||||||
|
|
||||||
$todayLimitDate = date('Y-m-d');
|
$todayLimitDate = date('Y-m-d');
|
||||||
if ($asset->today_limit_date !== $todayLimitDate) {
|
if ($asset->today_limit_date !== $todayLimitDate) {
|
||||||
@@ -313,9 +579,9 @@ class Playx extends Api
|
|||||||
|
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
MallPlayxClaimLog::create([
|
MallClaimLog::create([
|
||||||
'claim_request_id' => $claimRequestId,
|
'claim_request_id' => $claimRequestId,
|
||||||
'user_id' => $userId,
|
'user_id' => $playxUserId,
|
||||||
'claimed_amount' => $canClaim,
|
'claimed_amount' => $canClaim,
|
||||||
'create_time' => time(),
|
'create_time' => time(),
|
||||||
]);
|
]);
|
||||||
@@ -395,12 +661,16 @@ class Playx extends Api
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = $this->resolveUserIdFromRequest($request);
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
if ($userId === null) {
|
if ($assetId === null) {
|
||||||
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
$asset = $this->getAssetById($assetId);
|
||||||
|
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
|
||||||
|
return $this->success('', ['list' => []]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = MallPlayxOrder::where('user_id', $userId)
|
$list = MallOrder::where('user_id', strval($asset->playx_user_id))
|
||||||
->with(['mallItem'])
|
->with(['mallItem'])
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
->limit(100)
|
->limit(100)
|
||||||
@@ -409,7 +679,198 @@ class Playx extends Api
|
|||||||
return $this->success('', ['list' => $list->toArray()]);
|
return $this->success('', ['list' => $list->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatAsset(?MallPlayxUserAsset $asset): array
|
/**
|
||||||
|
* 收货地址列表
|
||||||
|
* GET /api/v1/playx/address/list?session_id=xxx
|
||||||
|
*/
|
||||||
|
public function addressList(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeApi($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = MallAddress::where('playx_user_asset_id', $assetId)
|
||||||
|
->order('default_setting', 'desc')
|
||||||
|
->order('id', 'desc')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return $this->success('', ['list' => $list->toArray()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加收货地址(可设置默认)
|
||||||
|
* POST /api/v1/playx/address/add
|
||||||
|
*/
|
||||||
|
public function addressAdd(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeApi($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = trim(strval($request->post('phone', '')));
|
||||||
|
$receiverName = trim(strval($request->post('receiver_name', '')));
|
||||||
|
$region = $request->post('region', '');
|
||||||
|
$detailAddress = trim(strval($request->post('detail_address', '')));
|
||||||
|
$defaultSetting = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
|
||||||
|
|
||||||
|
if ($phone === '' || $receiverName === '' || $detailAddress === '' || $region === '' || $region === null) {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($defaultSetting === 1) {
|
||||||
|
MallAddress::where('playx_user_asset_id', $assetId)->update(['default_setting' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = MallAddress::create([
|
||||||
|
'playx_user_asset_id' => $assetId,
|
||||||
|
'receiver_name' => $receiverName,
|
||||||
|
'phone' => $phone,
|
||||||
|
'region' => $region,
|
||||||
|
'detail_address' => $detailAddress,
|
||||||
|
'default_setting' => $defaultSetting,
|
||||||
|
'create_time' => time(),
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'id' => $created ? $created->id : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改收货地址(包含设置默认地址)
|
||||||
|
* POST /api/v1/playx/address/edit
|
||||||
|
*/
|
||||||
|
public function addressEdit(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeApi($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = intval($request->post('id', 0));
|
||||||
|
if ($id <= 0) {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->find();
|
||||||
|
if (!$row) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$updates = [];
|
||||||
|
if ($request->post('phone', null) !== null) {
|
||||||
|
$updates['phone'] = trim(strval($request->post('phone', '')));
|
||||||
|
}
|
||||||
|
if ($request->post('receiver_name', null) !== null) {
|
||||||
|
$updates['receiver_name'] = trim(strval($request->post('receiver_name', '')));
|
||||||
|
}
|
||||||
|
if ($request->post('region', null) !== null) {
|
||||||
|
$updates['region'] = $request->post('region', '');
|
||||||
|
}
|
||||||
|
if ($request->post('detail_address', null) !== null) {
|
||||||
|
$updates['detail_address'] = trim(strval($request->post('detail_address', '')));
|
||||||
|
}
|
||||||
|
if ($request->post('default_setting', null) !== null) {
|
||||||
|
$updates['default_setting'] = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($updates)) {
|
||||||
|
return $this->success('', ['updated' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if (isset($updates['default_setting']) && $updates['default_setting'] === 1) {
|
||||||
|
MallAddress::where('playx_user_asset_id', $assetId)->update(['default_setting' => 0]);
|
||||||
|
}
|
||||||
|
$updates['update_time'] = time();
|
||||||
|
MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->update($updates);
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', ['updated' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除收货地址
|
||||||
|
* POST /api/v1/playx/address/delete
|
||||||
|
*/
|
||||||
|
public function addressDelete(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeApi($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = intval($request->post('id', 0));
|
||||||
|
if ($id <= 0) {
|
||||||
|
return $this->error(__('Missing required fields'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->find();
|
||||||
|
if (!$row) {
|
||||||
|
return $this->error(__('Record not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$wasDefault = intval($row->default_setting ?? 0) === 1;
|
||||||
|
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->delete();
|
||||||
|
|
||||||
|
if ($wasDefault) {
|
||||||
|
$fallback = MallAddress::where('playx_user_asset_id', $assetId)->order('id', 'desc')->find();
|
||||||
|
if ($fallback) {
|
||||||
|
$fallback->default_setting = 1;
|
||||||
|
$fallback->update_time = time();
|
||||||
|
$fallback->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', ['deleted' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatAsset(?MallUserAsset $asset): array
|
||||||
{
|
{
|
||||||
if (!$asset) {
|
if (!$asset) {
|
||||||
return [
|
return [
|
||||||
@@ -438,8 +899,11 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
$itemId = intval($request->post('item_id', 0));
|
$itemId = intval($request->post('item_id', 0));
|
||||||
$userId = $this->resolveUserIdFromRequest($request);
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
if ($itemId <= 0 || $userId === null) {
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
if ($itemId <= 0) {
|
||||||
return $this->error(__('item_id and user_id/session_id required'));
|
return $this->error(__('item_id and user_id/session_id required'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,10 +912,11 @@ class Playx extends Api
|
|||||||
return $this->error(__('Item not found or not available'));
|
return $this->error(__('Item not found or not available'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
$asset = $this->getAssetById($assetId);
|
||||||
if (!$asset || $asset->available_points < $item->score) {
|
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
|
||||||
return $this->error(__('Insufficient points'));
|
return $this->error(__('Insufficient points'));
|
||||||
}
|
}
|
||||||
|
$playxUserId = strval($asset->playx_user_id);
|
||||||
|
|
||||||
$multiplier = intval($item->multiplier ?? 0);
|
$multiplier = intval($item->multiplier ?? 0);
|
||||||
if ($multiplier <= 0) {
|
if ($multiplier <= 0) {
|
||||||
@@ -465,18 +930,20 @@ class Playx extends Api
|
|||||||
$asset->save();
|
$asset->save();
|
||||||
|
|
||||||
$orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
|
$orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
|
||||||
$order = MallPlayxOrder::create([
|
$order = MallOrder::create([
|
||||||
'user_id' => $userId,
|
'user_id' => $playxUserId,
|
||||||
'type' => MallPlayxOrder::TYPE_BONUS,
|
'type' => MallOrder::TYPE_BONUS,
|
||||||
'status' => MallPlayxOrder::STATUS_PENDING,
|
'status' => MallOrder::STATUS_PENDING,
|
||||||
'mall_item_id' => $item->id,
|
'mall_item_id' => $item->id,
|
||||||
'points_cost' => $item->score,
|
'points_cost' => $item->score,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
'multiplier' => $multiplier,
|
'multiplier' => $multiplier,
|
||||||
'external_transaction_id' => $orderNo,
|
'external_transaction_id' => $orderNo,
|
||||||
'grant_status' => MallPlayxOrder::GRANT_NOT_SENT,
|
'grant_status' => MallOrder::GRANT_NOT_SENT,
|
||||||
'create_time' => time(),
|
'create_time' => time(),
|
||||||
'update_time' => time(),
|
'update_time' => time(),
|
||||||
|
'start_time' => date('Y-m-d H:i:s', time()),
|
||||||
|
'end_time' => date('Y-m-d H:i:s', time()+86400*3),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Db::commit();
|
Db::commit();
|
||||||
@@ -487,7 +954,7 @@ class Playx extends Api
|
|||||||
|
|
||||||
$baseUrl = config('playx.api.base_url', '');
|
$baseUrl = config('playx.api.base_url', '');
|
||||||
if ($baseUrl !== '') {
|
if ($baseUrl !== '') {
|
||||||
$this->callPlayxBonusGrant($order, $item, $userId);
|
$this->callPlayxBonusGrant($order, $item, $playxUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(__('Redeem submitted, please wait about 10 minutes'), [
|
return $this->success(__('Redeem submitted, please wait about 10 minutes'), [
|
||||||
@@ -504,11 +971,21 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
$itemId = intval($request->post('item_id', 0));
|
$itemId = intval($request->post('item_id', 0));
|
||||||
$userId = $this->resolveUserIdFromRequest($request);
|
$addressId = intval($request->post('address_id', 0));
|
||||||
$receiverName = $request->post('receiver_name', '');
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
$receiverPhone = $request->post('receiver_phone', '');
|
if ($itemId <= 0 || $addressId <= 0) {
|
||||||
$receiverAddress = $request->post('receiver_address', '');
|
return $this->error(__('Missing required fields'));
|
||||||
if ($itemId <= 0 || $userId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
|
}
|
||||||
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$addrRow = MallAddress::where('id', $addressId)->where('playx_user_asset_id', $assetId)->find();
|
||||||
|
if (!$addrRow) {
|
||||||
|
return $this->error(__('Shipping address not found'));
|
||||||
|
}
|
||||||
|
$snapshot = MallAddress::snapshotForPhysicalOrder($addrRow);
|
||||||
|
if ($snapshot['receiver_phone'] === '' || $snapshot['receiver_address'] === '' || $snapshot['receiver_name'] === '') {
|
||||||
return $this->error(__('Missing required fields'));
|
return $this->error(__('Missing required fields'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,25 +997,28 @@ class Playx extends Api
|
|||||||
return $this->error(__('Out of stock'));
|
return $this->error(__('Out of stock'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
$asset = $this->getAssetById($assetId);
|
||||||
if (!$asset || $asset->available_points < $item->score) {
|
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
|
||||||
return $this->error(__('Insufficient points'));
|
return $this->error(__('Insufficient points'));
|
||||||
}
|
}
|
||||||
|
$playxUserId = strval($asset->playx_user_id);
|
||||||
|
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
$asset->available_points -= $item->score;
|
$asset->available_points -= $item->score;
|
||||||
$asset->save();
|
$asset->save();
|
||||||
|
|
||||||
MallPlayxOrder::create([
|
MallOrder::create([
|
||||||
'user_id' => $userId,
|
'user_id' => $playxUserId,
|
||||||
'type' => MallPlayxOrder::TYPE_PHYSICAL,
|
'type' => MallOrder::TYPE_PHYSICAL,
|
||||||
'status' => MallPlayxOrder::STATUS_PENDING,
|
'status' => MallOrder::STATUS_PENDING,
|
||||||
'mall_item_id' => $item->id,
|
'mall_item_id' => $item->id,
|
||||||
'points_cost' => $item->score,
|
'points_cost' => $item->score,
|
||||||
'receiver_name' => $receiverName,
|
'mall_address_id' => $addressId,
|
||||||
'receiver_phone' => $receiverPhone,
|
'receiver_name' => $snapshot['receiver_name'],
|
||||||
'receiver_address' => $receiverAddress,
|
'receiver_phone' => $snapshot['receiver_phone'],
|
||||||
|
'receiver_address' => $snapshot['receiver_address'],
|
||||||
|
'grant_status' => MallOrder::GRANT_NOT_APPLICABLE,
|
||||||
'create_time' => time(),
|
'create_time' => time(),
|
||||||
'update_time' => time(),
|
'update_time' => time(),
|
||||||
]);
|
]);
|
||||||
@@ -565,8 +1045,11 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
$itemId = intval($request->post('item_id', 0));
|
$itemId = intval($request->post('item_id', 0));
|
||||||
$userId = $this->resolveUserIdFromRequest($request);
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
if ($itemId <= 0 || $userId === null) {
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
if ($itemId <= 0) {
|
||||||
return $this->error(__('item_id and user_id/session_id required'));
|
return $this->error(__('item_id and user_id/session_id required'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,10 +1058,11 @@ class Playx extends Api
|
|||||||
return $this->error(__('Item not found or not available'));
|
return $this->error(__('Item not found or not available'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
$asset = $this->getAssetById($assetId);
|
||||||
if (!$asset || $asset->available_points < $item->score) {
|
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
|
||||||
return $this->error(__('Insufficient points'));
|
return $this->error(__('Insufficient points'));
|
||||||
}
|
}
|
||||||
|
$playxUserId = strval($asset->playx_user_id);
|
||||||
|
|
||||||
$multiplier = intval($item->multiplier ?? 0);
|
$multiplier = intval($item->multiplier ?? 0);
|
||||||
if ($multiplier <= 0) {
|
if ($multiplier <= 0) {
|
||||||
@@ -592,16 +1076,16 @@ class Playx extends Api
|
|||||||
$asset->save();
|
$asset->save();
|
||||||
|
|
||||||
$orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
|
$orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
|
||||||
$order = MallPlayxOrder::create([
|
$order = MallOrder::create([
|
||||||
'user_id' => $userId,
|
'user_id' => $playxUserId,
|
||||||
'type' => MallPlayxOrder::TYPE_WITHDRAW,
|
'type' => MallOrder::TYPE_WITHDRAW,
|
||||||
'status' => MallPlayxOrder::STATUS_PENDING,
|
'status' => MallOrder::STATUS_PENDING,
|
||||||
'mall_item_id' => $item->id,
|
'mall_item_id' => $item->id,
|
||||||
'points_cost' => $item->score,
|
'points_cost' => $item->score,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
'multiplier' => $multiplier,
|
'multiplier' => $multiplier,
|
||||||
'external_transaction_id' => $orderNo,
|
'external_transaction_id' => $orderNo,
|
||||||
'grant_status' => MallPlayxOrder::GRANT_NOT_SENT,
|
'grant_status' => MallOrder::GRANT_NOT_APPLICABLE,
|
||||||
'create_time' => time(),
|
'create_time' => time(),
|
||||||
'update_time' => time(),
|
'update_time' => time(),
|
||||||
]);
|
]);
|
||||||
@@ -612,18 +1096,13 @@ class Playx extends Api
|
|||||||
return $this->error($e->getMessage());
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseUrl = config('playx.api.base_url', '');
|
|
||||||
if ($baseUrl !== '') {
|
|
||||||
$this->callPlayxBalanceCredit($order, $userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [
|
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [
|
||||||
'order_id' => $order->id,
|
'order_id' => $order->id,
|
||||||
'status' => 'PENDING',
|
'status' => 'PENDING',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function callPlayxBonusGrant(MallPlayxOrder $order, MallItem $item, string $userId): void
|
private function callPlayxBonusGrant(MallOrder $order, MallItem $item, string $userId): void
|
||||||
{
|
{
|
||||||
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
|
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
|
||||||
$url = config('playx.api.bonus_grant_url', '/api/v1/bonus/grant');
|
$url = config('playx.api.bonus_grant_url', '/api/v1/bonus/grant');
|
||||||
@@ -648,53 +1127,18 @@ class Playx extends Api
|
|||||||
$data = json_decode(strval($res->getBody()), true);
|
$data = json_decode(strval($res->getBody()), true);
|
||||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
||||||
$order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
|
$order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
|
$order->grant_status = MallOrder::GRANT_ACCEPTED;
|
||||||
$order->save();
|
$order->save();
|
||||||
} else {
|
} else {
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
|
||||||
$order->fail_reason = $data['message'] ?? 'unknown';
|
$order->fail_reason = $data['message'] ?? 'unknown';
|
||||||
$order->save();
|
$order->save();
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
|
||||||
$order->fail_reason = $e->getMessage();
|
$order->fail_reason = $e->getMessage();
|
||||||
$order->save();
|
$order->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function callPlayxBalanceCredit(MallPlayxOrder $order, string $userId): void
|
|
||||||
{
|
|
||||||
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
|
|
||||||
$url = config('playx.api.balance_credit_url', '/api/v1/balance/credit');
|
|
||||||
if ($baseUrl === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$client = new \GuzzleHttp\Client(['timeout' => 15]);
|
|
||||||
$res = $client->post($baseUrl . $url, [
|
|
||||||
'json' => [
|
|
||||||
'request_id' => 'mall_withdraw_' . uniqid(),
|
|
||||||
'externalTransactionId' => $order->external_transaction_id,
|
|
||||||
'user_id' => $userId,
|
|
||||||
'amount' => $order->amount,
|
|
||||||
'multiplier' => $order->multiplier,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
$data = json_decode(strval($res->getBody()), true);
|
|
||||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
|
||||||
$order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
|
|
||||||
$order->save();
|
|
||||||
} else {
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
|
||||||
$order->fail_reason = $data['message'] ?? 'unknown';
|
|
||||||
$order->save();
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
|
||||||
$order->fail_reason = $e->getMessage();
|
|
||||||
$order->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,39 @@ return [
|
|||||||
'Invalid agent or secret' => 'Invalid agent or secret',
|
'Invalid agent or secret' => 'Invalid agent or secret',
|
||||||
'Invalid signature' => 'Invalid signature',
|
'Invalid signature' => 'Invalid signature',
|
||||||
'Agent not found' => 'Agent not found',
|
'Agent not found' => 'Agent not found',
|
||||||
|
'Temp login is disabled' => 'Temp login is disabled',
|
||||||
|
'Failed to create temp account' => 'Failed to allocate a unique phone number, please retry later',
|
||||||
|
'Parameter username can not be empty' => 'Parameter username can not be empty',
|
||||||
|
'Token expiration' => 'Session expired, please login again.',
|
||||||
// Member center account
|
// Member center account
|
||||||
'Data updated successfully~' => 'Data updated successfully~',
|
'Data updated successfully~' => 'Data updated successfully~',
|
||||||
'Password has been changed~' => 'Password has been changed~',
|
'Password has been changed~' => 'Password has been changed~',
|
||||||
'Password has been changed, please login again~' => 'Password has been changed, please login again~',
|
'Password has been changed, please login again~' => 'Password has been changed, please login again~',
|
||||||
'already exists' => 'already exists',
|
'already exists' => 'already exists',
|
||||||
'nicknameChsDash' => 'Usernames can only be Chinese characters, letters, numbers, underscores_ and dashes-.',
|
'nicknameChsDash' => 'Usernames can only be Chinese characters, letters, numbers, underscores_ and dashes-.',
|
||||||
|
// PlayX API v1 /api/v1/*
|
||||||
|
'Invalid token' => 'Invalid or expired token',
|
||||||
|
'PlayX API not configured' => 'PlayX API is not configured',
|
||||||
|
'Duplicate input' => 'Duplicate submission',
|
||||||
|
'Ok' => 'OK',
|
||||||
|
'Failed to map playx user to mall user' => 'Failed to map PlayX user to mall user',
|
||||||
|
'Missing required fields: request_id, date, user_id' => 'Missing required fields: request_id, date, user_id',
|
||||||
|
'Missing required fields: report_date, member' => 'Missing required fields: report_date, member',
|
||||||
|
'Missing required fields: member_id' => 'Missing required fields: member_id',
|
||||||
|
'Daily push signature missing or incomplete' => 'Daily push signature missing or incomplete',
|
||||||
|
'Daily push signature verification failed' => 'Daily push signature verification failed',
|
||||||
|
'Failed to ensure PlayX user asset' => 'Failed to ensure PlayX user asset',
|
||||||
|
'claim_request_id and user_id/session_id required' => 'claim_request_id and user_id/session_id/token are required',
|
||||||
|
'User asset not found' => 'User asset not found',
|
||||||
|
'No points to claim or limit reached' => 'No points to claim or daily limit reached',
|
||||||
|
'Claim success' => 'Claim successful',
|
||||||
|
'item_id and user_id/session_id required' => 'item_id and user_id/session_id/token are required',
|
||||||
|
'Item not found or not available' => 'Item not found or not available',
|
||||||
|
'Insufficient points' => 'Insufficient points',
|
||||||
|
'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes',
|
||||||
|
'Missing required fields' => 'Missing required fields',
|
||||||
|
'Shipping address not found' => 'Shipping address not found',
|
||||||
|
'Out of stock' => 'Out of stock',
|
||||||
|
'Redeem success' => 'Redeem successful',
|
||||||
|
'Withdraw submitted, please wait about 10 minutes' => 'Withdrawal submitted, please wait about 10 minutes',
|
||||||
];
|
];
|
||||||
@@ -49,6 +49,9 @@ return [
|
|||||||
'Invalid agent or secret' => '代理或密钥无效',
|
'Invalid agent or secret' => '代理或密钥无效',
|
||||||
'Invalid signature' => '签名无效',
|
'Invalid signature' => '签名无效',
|
||||||
'Agent not found' => '代理不存在',
|
'Agent not found' => '代理不存在',
|
||||||
|
'Temp login is disabled' => '临时登录已关闭',
|
||||||
|
'Failed to create temp account' => '无法生成唯一手机号,请稍后重试',
|
||||||
|
'Parameter username can not be empty' => '参数 username 不能为空',
|
||||||
'Token expiration' => '登录态过期,请重新登录!',
|
'Token expiration' => '登录态过期,请重新登录!',
|
||||||
'Captcha error' => '验证码错误!',
|
'Captcha error' => '验证码错误!',
|
||||||
// 会员中心 account
|
// 会员中心 account
|
||||||
@@ -57,4 +60,30 @@ return [
|
|||||||
'Password has been changed, please login again~' => '密码已修改,请重新登录~',
|
'Password has been changed, please login again~' => '密码已修改,请重新登录~',
|
||||||
'already exists' => '已存在',
|
'already exists' => '已存在',
|
||||||
'nicknameChsDash' => '用户名只能是汉字、字母、数字和下划线_及破折号-',
|
'nicknameChsDash' => '用户名只能是汉字、字母、数字和下划线_及破折号-',
|
||||||
|
// PlayX API v1 /api/v1/*
|
||||||
|
'Invalid token' => '令牌无效或已过期',
|
||||||
|
'PlayX API not configured' => '未配置 PlayX 接口地址',
|
||||||
|
'Duplicate input' => '重复提交',
|
||||||
|
'Ok' => '成功',
|
||||||
|
'Failed to map playx user to mall user' => '无法将 PlayX 用户关联到商城用户',
|
||||||
|
'Missing required fields: request_id, date, user_id' => '缺少必填字段:request_id、date、user_id',
|
||||||
|
'Missing required fields: report_date, member' => '缺少必填字段:report_date、member',
|
||||||
|
'Missing required fields: member_id' => '缺少必填字段:member_id',
|
||||||
|
'Daily push signature missing or incomplete' => '签名缺失或不完整',
|
||||||
|
'Daily push signature verification failed' => '签名校验失败',
|
||||||
|
'Failed to ensure PlayX user asset' => '创建/映射用户资产失败',
|
||||||
|
'claim_request_id and user_id/session_id required' => '缺少 claim_request_id,或未提供有效的 user_id/session_id/token',
|
||||||
|
'User asset not found' => '未找到用户资产',
|
||||||
|
'No points to claim or limit reached' => '暂无可领取积分或已达今日上限',
|
||||||
|
'Claim success' => '领取成功',
|
||||||
|
'item_id and user_id/session_id required' => '缺少 item_id,或未提供有效的 user_id/session_id/token',
|
||||||
|
'Item not found or not available' => '商品不存在或已下架',
|
||||||
|
'Insufficient points' => '积分不足',
|
||||||
|
'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟',
|
||||||
|
'Missing required fields' => '缺少必填字段',
|
||||||
|
'Out of stock' => '库存不足',
|
||||||
|
'Record not found' => '记录不存在',
|
||||||
|
'Shipping address not found' => '收货地址不存在',
|
||||||
|
'Redeem success' => '兑换成功',
|
||||||
|
'Withdraw submitted, please wait about 10 minutes' => '提现申请已提交,请等待约 10 分钟',
|
||||||
];
|
];
|
||||||
@@ -238,6 +238,7 @@ class Backend extends Api
|
|||||||
$limit = is_numeric($limit) ? intval($limit) : 10;
|
$limit = is_numeric($limit) ? intval($limit) : 10;
|
||||||
$search = $this->request->get('search', []);
|
$search = $this->request->get('search', []);
|
||||||
$search = is_array($search) ? $search : [];
|
$search = is_array($search) ? $search : [];
|
||||||
|
$search = $this->filterSearchArray($search);
|
||||||
$initKey = $this->request->get('initKey', $pk);
|
$initKey = $this->request->get('initKey', $pk);
|
||||||
$initValue = $this->request->get('initValue', '');
|
$initValue = $this->request->get('initValue', '');
|
||||||
$initOperator = $this->request->get('initOperator', 'in');
|
$initOperator = $this->request->get('initOperator', 'in');
|
||||||
@@ -352,6 +353,14 @@ class Backend extends Api
|
|||||||
return [$where, $alias, $limit, $this->queryOrderBuilder()];
|
return [$where, $alias, $limit, $this->queryOrderBuilder()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组合搜索条件过滤(子类可覆盖,例如去掉已删除的数据库字段)
|
||||||
|
*/
|
||||||
|
protected function filterSearchArray(array $search): array
|
||||||
|
{
|
||||||
|
return $search;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询的排序参数构建器
|
* 查询的排序参数构建器
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class AgentJwt
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 JWT authtoken
|
* 生成 JWT authtoken
|
||||||
* @param array $payload agent_id, channel_id, admin_id 等
|
* @param array $payload agent_id、admin_id 等
|
||||||
* @param int $expire 有效期(秒)
|
* @param int $expire 有效期(秒)
|
||||||
*/
|
*/
|
||||||
public static function encode(array $payload, int $expire = 86400): string
|
public static function encode(array $payload, int $expire = 86400): string
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class Auth extends \ba\Auth
|
|||||||
public const LOGGED_IN = 'logged in';
|
public const LOGGED_IN = 'logged in';
|
||||||
public const TOKEN_TYPE = 'user';
|
public const TOKEN_TYPE = 'user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分商城用户(mall_playx_user_asset 主键)Token 类型,与会员 user 表区分
|
||||||
|
*/
|
||||||
|
public const TOKEN_TYPE_MALL_USER = 'muser';
|
||||||
|
|
||||||
protected bool $loginEd = false;
|
protected bool $loginEd = false;
|
||||||
protected string $error = '';
|
protected string $error = '';
|
||||||
protected ?User $model = null;
|
protected ?User $model = null;
|
||||||
|
|||||||
85
app/common/library/MallBonusGrantPush.php
Normal file
85
app/common/library/MallBonusGrantPush.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\library;
|
||||||
|
|
||||||
|
use app\common\model\MallItem;
|
||||||
|
use app\common\model\MallOrder;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红利订单调用 PlayX bonus/grant(与定时任务、后台手动推送共用)
|
||||||
|
*/
|
||||||
|
final class MallBonusGrantPush
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{ok: bool, message: string, playx_transaction_id: string}
|
||||||
|
*/
|
||||||
|
public static function push(MallOrder $order): array
|
||||||
|
{
|
||||||
|
$baseUrl = rtrim(strval(config('playx.api.base_url', '')), '/');
|
||||||
|
if ($baseUrl === '') {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'PlayX base_url not configured',
|
||||||
|
'playx_transaction_id' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
|
||||||
|
$url = $baseUrl . $path;
|
||||||
|
|
||||||
|
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||||||
|
$rewardName = $item ? strval($item->title) : '';
|
||||||
|
$category = $item ? strval($item->category) : 'daily';
|
||||||
|
$categoryTitle = $item ? strval($item->category_title) : '';
|
||||||
|
$multiplier = intval($order->multiplier ?? 0);
|
||||||
|
if ($multiplier <= 0) {
|
||||||
|
$multiplier = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = new Client([
|
||||||
|
'timeout' => 20,
|
||||||
|
'http_errors' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$requestId = 'mall_bonus_' . uniqid();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$res = $client->post($url, [
|
||||||
|
'json' => [
|
||||||
|
'request_id' => $requestId,
|
||||||
|
'externalTransactionId' => $order->external_transaction_id,
|
||||||
|
'user_id' => $order->user_id,
|
||||||
|
'amount' => $order->amount,
|
||||||
|
'rewardName' => $rewardName,
|
||||||
|
'category' => $category,
|
||||||
|
'categoryTitle' => $categoryTitle,
|
||||||
|
'multiplier' => $multiplier,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = json_decode(strval($res->getBody()), true) ?? [];
|
||||||
|
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'message' => '',
|
||||||
|
'playx_transaction_id' => strval($data['playx_transaction_id'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => strval($data['message'] ?? 'PlayX bonus grant not accepted'),
|
||||||
|
'playx_transaction_id' => '',
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'playx_transaction_id' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/common/library/PlayxInboundJwt.php
Normal file
48
app/common/library/PlayxInboundJwt.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\library;
|
||||||
|
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
use Firebase\JWT\ExpiredException;
|
||||||
|
use Firebase\JWT\SignatureInvalidException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlayX / 合作方回调:校验 Authorization: Bearer JWT(HS256)
|
||||||
|
*/
|
||||||
|
class PlayxInboundJwt
|
||||||
|
{
|
||||||
|
public const ALG = 'HS256';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Authorization 头解析 Bearer token 并校验签名与有效期
|
||||||
|
*/
|
||||||
|
public static function verifyBearer(string $authorizationHeader, string $secret): bool
|
||||||
|
{
|
||||||
|
if ($secret === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$token = self::extractBearer($authorizationHeader);
|
||||||
|
if ($token === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JWT::decode($token, new Key($secret, self::ALG));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (ExpiredException|SignatureInvalidException|\Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function extractBearer(string $authorizationHeader): string
|
||||||
|
{
|
||||||
|
if (preg_match('/Bearer\s+(\S+)/i', $authorizationHeader, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,12 +11,15 @@ use Exception;
|
|||||||
*/
|
*/
|
||||||
class TokenExpirationException extends Exception
|
class TokenExpirationException extends Exception
|
||||||
{
|
{
|
||||||
|
protected array $data = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $message = '',
|
string $message = '',
|
||||||
protected int $code = 409,
|
int $code = 409,
|
||||||
protected array $data = [],
|
array $data = [],
|
||||||
?\Throwable $previous = null
|
?\Throwable $previous = null
|
||||||
) {
|
) {
|
||||||
|
$this->data = $data;
|
||||||
parent::__construct($message, $code, $previous);
|
parent::__construct($message, $code, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class AllowCrossDomain implements MiddlewareInterface
|
|||||||
'Access-Control-Allow-Credentials' => 'true',
|
'Access-Control-Allow-Credentials' => 'true',
|
||||||
'Access-Control-Max-Age' => '1800',
|
'Access-Control-Max-Age' => '1800',
|
||||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang, lang, server',
|
||||||
];
|
];
|
||||||
$origin = $request->header('origin');
|
$origin = $request->header('origin');
|
||||||
if (is_array($origin)) {
|
if (is_array($origin)) {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ use Webman\Http\Response;
|
|||||||
/**
|
/**
|
||||||
* 加载控制器语言包中间件(Webman 迁移版,等价 ThinkPHP LoadLangPack)
|
* 加载控制器语言包中间件(Webman 迁移版,等价 ThinkPHP LoadLangPack)
|
||||||
* 根据当前路由加载对应控制器的语言包到 Translator
|
* 根据当前路由加载对应控制器的语言包到 Translator
|
||||||
|
*
|
||||||
|
* 对外 api/:优先请求头 lang(zh / zh-cn → 中文包 zh-cn,en → 英文包),未传则 think-lang,再默认 zh-cn(不根据浏览器 Accept-Language)
|
||||||
|
* admin/:think-lang → Accept-Language → 配置默认
|
||||||
*/
|
*/
|
||||||
class LoadLangPack implements MiddlewareInterface
|
class LoadLangPack implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
@@ -25,22 +28,61 @@ class LoadLangPack implements MiddlewareInterface
|
|||||||
|
|
||||||
protected function loadLang(Request $request): void
|
protected function loadLang(Request $request): void
|
||||||
{
|
{
|
||||||
// 优先从请求头 think-lang 获取前端选择的语言(与前端 axios 发送的 header 对应)
|
$path = trim($request->path(), '/');
|
||||||
// 安装页等未发送 think-lang 时,回退到 Accept-Language 或配置默认值
|
$isApi = str_starts_with($path, 'api/');
|
||||||
$headerLang = $request->header('think-lang');
|
$isAdmin = str_starts_with($path, 'admin/');
|
||||||
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
|
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
|
||||||
if ($headerLang && in_array(str_replace('_', '-', strtolower($headerLang)), $allowLangList)) {
|
|
||||||
$langSet = str_replace('_', '-', strtolower($headerLang));
|
$langSet = null;
|
||||||
} else {
|
|
||||||
$acceptLang = $request->header('accept-language', '');
|
// 对外 API(PlayX、H5 等):优先 lang 请求头,默认中文 zh-cn,不跟随浏览器 Accept-Language
|
||||||
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
|
if ($isApi) {
|
||||||
|
$langHeader = $request->header('lang');
|
||||||
|
if (is_array($langHeader)) {
|
||||||
|
$langHeader = $langHeader[0] ?? '';
|
||||||
|
}
|
||||||
|
$langHeader = is_string($langHeader) ? trim($langHeader) : '';
|
||||||
|
if ($langHeader !== '') {
|
||||||
|
$langSet = $this->normalizeLangHeader($langHeader, $allowLangList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与后台 Vue 一致的 think-lang(对外 API 在 lang 未设置时仍可生效)
|
||||||
|
if ($langSet === null) {
|
||||||
|
$headerLang = $request->header('think-lang');
|
||||||
|
if (is_array($headerLang)) {
|
||||||
|
$headerLang = $headerLang[0] ?? '';
|
||||||
|
}
|
||||||
|
$headerLang = is_string($headerLang) ? trim($headerLang) : '';
|
||||||
|
if ($headerLang !== '') {
|
||||||
|
$normalized = str_replace('_', '-', strtolower($headerLang));
|
||||||
|
if (in_array($normalized, $allowLangList, true)) {
|
||||||
|
$langSet = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($langSet === null) {
|
||||||
|
if ($isApi) {
|
||||||
$langSet = 'zh-cn';
|
$langSet = 'zh-cn';
|
||||||
} elseif (preg_match('/^en/i', $acceptLang)) {
|
} elseif ($isAdmin) {
|
||||||
$langSet = 'en';
|
$acceptLang = $request->header('accept-language', '');
|
||||||
|
if (is_array($acceptLang)) {
|
||||||
|
$acceptLang = $acceptLang[0] ?? '';
|
||||||
|
}
|
||||||
|
$acceptLang = is_string($acceptLang) ? $acceptLang : '';
|
||||||
|
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
|
||||||
|
$langSet = 'zh-cn';
|
||||||
|
} elseif (preg_match('/^en/i', $acceptLang)) {
|
||||||
|
$langSet = 'en';
|
||||||
|
} else {
|
||||||
|
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
|
||||||
|
}
|
||||||
|
$langSet = str_replace('_', '-', strtolower((string) $langSet));
|
||||||
} else {
|
} else {
|
||||||
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
|
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
|
||||||
|
$langSet = str_replace('_', '-', strtolower((string) $langSet));
|
||||||
}
|
}
|
||||||
$langSet = str_replace('_', '-', strtolower($langSet));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前请求的翻译语言,使 __() 和 trans() 使用正确的语言
|
// 设置当前请求的翻译语言,使 __() 和 trans() 使用正确的语言
|
||||||
@@ -48,7 +90,6 @@ class LoadLangPack implements MiddlewareInterface
|
|||||||
locale($langSet);
|
locale($langSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = trim($request->path(), '/');
|
|
||||||
$parts = explode('/', $path);
|
$parts = explode('/', $path);
|
||||||
$app = $parts[0] ?? 'api';
|
$app = $parts[0] ?? 'api';
|
||||||
|
|
||||||
@@ -81,4 +122,26 @@ class LoadLangPack implements MiddlewareInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 lang 请求头取值映射为语言包标识(zh / zh-cn → zh-cn,en → en)
|
||||||
|
*/
|
||||||
|
private function normalizeLangHeader(string $raw, array $allowLangList): ?string
|
||||||
|
{
|
||||||
|
$s = str_replace('_', '-', strtolower(trim($raw)));
|
||||||
|
if ($s === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (in_array($s, $allowLangList, true)) {
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
if (str_starts_with($s, 'en')) {
|
||||||
|
return in_array('en', $allowLangList, true) ? 'en' : null;
|
||||||
|
}
|
||||||
|
if ($s === 'zh' || str_starts_with($s, 'zh-')) {
|
||||||
|
return in_array('zh-cn', $allowLangList, true) ? 'zh-cn' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,4 @@ class Attachment extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id');
|
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'user_id');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\model;
|
|
||||||
|
|
||||||
use support\think\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ChannelManage
|
|
||||||
*/
|
|
||||||
class ChannelManage extends Model
|
|
||||||
{
|
|
||||||
// 表名
|
|
||||||
protected $name = 'channel_manage';
|
|
||||||
|
|
||||||
// 自动写入时间戳字段
|
|
||||||
protected $autoWriteTimestamp = true;
|
|
||||||
|
|
||||||
// 字段类型转换
|
|
||||||
protected $type = [
|
|
||||||
'create_time' => 'integer',
|
|
||||||
'update_time' => 'integer',
|
|
||||||
'ip_white' => 'json',
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 IP 白名单,统一返回字符串数组格式
|
|
||||||
* 兼容:["127.0.0.1"]、[{"value":"127.0.0.1"}]、[{"127.0.0.1":""}]
|
|
||||||
*/
|
|
||||||
public function getipWhiteAttr($value): array
|
|
||||||
{
|
|
||||||
$arr = is_array($value) ? $value : (!$value ? [] : json_decode($value, true));
|
|
||||||
if (!is_array($arr)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$result = [];
|
|
||||||
foreach ($arr as $item) {
|
|
||||||
if (is_string($item)) {
|
|
||||||
$result[] = $item;
|
|
||||||
} elseif (is_array($item)) {
|
|
||||||
if (isset($item['value'])) {
|
|
||||||
$result[] = $item['value'];
|
|
||||||
} else {
|
|
||||||
$key = array_key_first($item);
|
|
||||||
if ($key !== null && $key !== '') {
|
|
||||||
$result[] = $key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array_values(array_filter($result));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写入 IP 白名单,存储格式 ["127.0.0.1","192.168.1.1"]
|
|
||||||
*/
|
|
||||||
public function setipWhiteAttr($value): array
|
|
||||||
{
|
|
||||||
$arr = is_array($value) ? $value : [];
|
|
||||||
$result = [];
|
|
||||||
foreach ($arr as $ip) {
|
|
||||||
$ip = is_string($ip) ? trim($ip) : '';
|
|
||||||
if ($ip !== '') {
|
|
||||||
$result[] = $ip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array_values($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建时自动生成密钥:strtoupper(md5(name+id))
|
|
||||||
*/
|
|
||||||
protected static function onAfterInsert($model): void
|
|
||||||
{
|
|
||||||
$pk = $model->getPk();
|
|
||||||
$secret = strtoupper(md5($model->name . $model->$pk));
|
|
||||||
$model->where($pk, $model->$pk)->update(['secret' => $secret]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function admin(): \think\model\relation\BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,12 +41,46 @@ class MallAddress extends Model
|
|||||||
public function getregionTextAttr($value, $row): string
|
public function getregionTextAttr($value, $row): string
|
||||||
{
|
{
|
||||||
if ($row['region'] === '' || $row['region'] === null) return '';
|
if ($row['region'] === '' || $row['region'] === null) return '';
|
||||||
$cityNames = \support\think\Db::name('area')->whereIn('id', $row['region'])->column('name');
|
$region = $row['region'];
|
||||||
|
$ids = $region;
|
||||||
|
if (!is_array($ids)) {
|
||||||
|
$ids = explode(',', (string) $ids);
|
||||||
|
}
|
||||||
|
$ids = array_values(array_filter(array_map('trim', $ids), static function ($s) {
|
||||||
|
return $s !== '';
|
||||||
|
}));
|
||||||
|
if (empty($ids)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$cityNames = \support\think\Db::name('area')->whereIn('id', $ids)->column('name');
|
||||||
return $cityNames ? implode(',', $cityNames) : '';
|
return $cityNames ? implode(',', $cityNames) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mallUser(): \think\model\relation\BelongsTo
|
public function playxUserAsset(): \think\model\relation\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\app\common\model\MallUser::class, 'mall_user_id', 'id');
|
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实物订单收货快照(写入 mall_order.receiver_*,与 mall_address 当前内容一致)
|
||||||
|
*
|
||||||
|
* @return array{receiver_name: string, receiver_phone: string, receiver_address: string}
|
||||||
|
*/
|
||||||
|
public static function snapshotForPhysicalOrder(self $addr): array
|
||||||
|
{
|
||||||
|
$regionText = $addr->region_text ?? '';
|
||||||
|
$parts = array_filter([
|
||||||
|
trim($regionText),
|
||||||
|
trim($addr->detail_address ?? ''),
|
||||||
|
], static function ($s) {
|
||||||
|
return $s !== '';
|
||||||
|
});
|
||||||
|
$receiverAddress = implode(' ', $parts);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'receiver_name' => trim($addr->receiver_name ?? ''),
|
||||||
|
'receiver_phone' => trim($addr->phone ?? ''),
|
||||||
|
'receiver_address' => $receiverAddress,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,14 +7,15 @@ namespace app\common\model;
|
|||||||
use support\think\Model;
|
use support\think\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayX 领取记录(幂等)
|
* 领取记录(幂等)
|
||||||
*/
|
*/
|
||||||
class MallPlayxClaimLog extends Model
|
class MallClaimLog extends Model
|
||||||
{
|
{
|
||||||
protected string $name = 'mall_playx_claim_log';
|
protected string $name = 'mall_claim_log';
|
||||||
|
|
||||||
protected array $type = [
|
protected array $type = [
|
||||||
'claimed_amount' => 'integer',
|
'claimed_amount' => 'integer',
|
||||||
'create_time' => 'integer',
|
'create_time' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,11 +7,11 @@ namespace app\common\model;
|
|||||||
use support\think\Model;
|
use support\think\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayX 每日推送数据
|
* 每日推送数据
|
||||||
*/
|
*/
|
||||||
class MallPlayxDailyPush extends Model
|
class MallDailyPush extends Model
|
||||||
{
|
{
|
||||||
protected string $name = 'mall_playx_daily_push';
|
protected string $name = 'mall_daily_push';
|
||||||
|
|
||||||
protected array $type = [
|
protected array $type = [
|
||||||
'yesterday_win_loss_net' => 'float',
|
'yesterday_win_loss_net' => 'float',
|
||||||
@@ -21,3 +21,4 @@ class MallPlayxDailyPush extends Model
|
|||||||
'create_time' => 'integer',
|
'create_time' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace app\common\model;
|
|||||||
use support\think\Model;
|
use support\think\Model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayX 统一订单
|
* 统一订单
|
||||||
*
|
*
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $user_id
|
* @property string $user_id
|
||||||
@@ -28,10 +28,11 @@ use support\think\Model;
|
|||||||
* @property string $receiver_name
|
* @property string $receiver_name
|
||||||
* @property string $receiver_phone
|
* @property string $receiver_phone
|
||||||
* @property string|null $receiver_address
|
* @property string|null $receiver_address
|
||||||
|
* @property int|null $mall_address_id
|
||||||
*/
|
*/
|
||||||
class MallPlayxOrder extends Model
|
class MallOrder extends Model
|
||||||
{
|
{
|
||||||
protected string $name = 'mall_playx_order';
|
protected string $name = 'mall_order';
|
||||||
|
|
||||||
protected bool $autoWriteTimestamp = true;
|
protected bool $autoWriteTimestamp = true;
|
||||||
|
|
||||||
@@ -50,17 +51,27 @@ class MallPlayxOrder extends Model
|
|||||||
public const GRANT_FAILED_RETRYABLE = 'FAILED_RETRYABLE';
|
public const GRANT_FAILED_RETRYABLE = 'FAILED_RETRYABLE';
|
||||||
public const GRANT_FAILED_FINAL = 'FAILED_FINAL';
|
public const GRANT_FAILED_FINAL = 'FAILED_FINAL';
|
||||||
|
|
||||||
|
/** 非红利订单不参与 PlayX/Angpow 推送,固定为该占位值 */
|
||||||
|
public const GRANT_NOT_APPLICABLE = '---';
|
||||||
|
|
||||||
protected array $type = [
|
protected array $type = [
|
||||||
'create_time' => 'integer',
|
'create_time' => 'integer',
|
||||||
'update_time' => 'integer',
|
'update_time' => 'integer',
|
||||||
'points_cost' => 'integer',
|
'points_cost' => 'integer',
|
||||||
'amount' => 'float',
|
'amount' => 'float',
|
||||||
'multiplier' => 'integer',
|
'multiplier' => 'integer',
|
||||||
'retry_count' => 'integer',
|
'retry_count' => 'integer',
|
||||||
|
'mall_address_id' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mallItem(): \think\model\relation\BelongsTo
|
public function mallItem(): \think\model\relation\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(MallItem::class, 'mall_item_id', 'id');
|
return $this->belongsTo(MallItem::class, 'mall_item_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mallAddress(): \think\model\relation\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(MallAddress::class, 'mall_address_id', 'id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ class MallPintsOrder extends Model
|
|||||||
protected $autoWriteTimestamp = true;
|
protected $autoWriteTimestamp = true;
|
||||||
|
|
||||||
|
|
||||||
public function mallUser(): \think\model\relation\BelongsTo
|
public function playxUserAsset(): \think\model\relation\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\app\common\model\MallUser::class, 'mall_user_id', 'id');
|
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\common\model;
|
|
||||||
|
|
||||||
use support\think\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlayX 会话缓存
|
|
||||||
*/
|
|
||||||
class MallPlayxSession extends Model
|
|
||||||
{
|
|
||||||
protected string $name = 'mall_playx_session';
|
|
||||||
|
|
||||||
protected bool $autoWriteTimestamp = true;
|
|
||||||
|
|
||||||
protected array $type = [
|
|
||||||
// 这里需要显式声明 create_time / update_time 为 integer,
|
|
||||||
// 否则 ThinkORM 可能把 bigint 时间戳当成字符串,导致写入时出现 now 字符串问题。
|
|
||||||
'create_time' => 'integer',
|
|
||||||
'update_time' => 'integer',
|
|
||||||
'expire_time' => 'integer',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\common\model;
|
|
||||||
|
|
||||||
use support\think\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlayX 用户资产
|
|
||||||
*/
|
|
||||||
class MallPlayxUserAsset extends Model
|
|
||||||
{
|
|
||||||
protected string $name = 'mall_playx_user_asset';
|
|
||||||
|
|
||||||
protected bool $autoWriteTimestamp = true;
|
|
||||||
|
|
||||||
protected array $type = [
|
|
||||||
'create_time' => 'integer',
|
|
||||||
'update_time' => 'integer',
|
|
||||||
'locked_points' => 'integer',
|
|
||||||
'available_points' => 'integer',
|
|
||||||
'today_limit' => 'integer',
|
|
||||||
'today_claimed' => 'integer',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -19,9 +19,9 @@ class MallRedemptionOrder extends Model
|
|||||||
protected $autoWriteTimestamp = true;
|
protected $autoWriteTimestamp = true;
|
||||||
|
|
||||||
|
|
||||||
public function mallUser(): \think\model\relation\BelongsTo
|
public function playxUserAsset(): \think\model\relation\BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\app\common\model\MallUser::class, 'mall_user_id', 'id');
|
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mallItem(): \think\model\relation\BelongsTo
|
public function mallItem(): \think\model\relation\BelongsTo
|
||||||
|
|||||||
24
app/common/model/MallSession.php
Normal file
24
app/common/model/MallSession.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use support\think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话缓存
|
||||||
|
*/
|
||||||
|
class MallSession extends Model
|
||||||
|
{
|
||||||
|
protected string $name = 'mall_session';
|
||||||
|
|
||||||
|
protected bool $autoWriteTimestamp = true;
|
||||||
|
|
||||||
|
protected array $type = [
|
||||||
|
'create_time' => 'integer',
|
||||||
|
'update_time' => 'integer',
|
||||||
|
'expire_time' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\model;
|
|
||||||
|
|
||||||
use app\common\model\traits\TimestampInteger;
|
|
||||||
use support\think\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商城用户模型
|
|
||||||
*/
|
|
||||||
class MallUser extends Model
|
|
||||||
{
|
|
||||||
use TimestampInteger;
|
|
||||||
|
|
||||||
protected string $name = 'mall_user';
|
|
||||||
|
|
||||||
protected bool $autoWriteTimestamp = true;
|
|
||||||
|
|
||||||
public function admin(): \think\model\relation\BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(\app\admin\model\Admin::class, 'admin_id', 'id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置密码(加密存储)
|
|
||||||
*/
|
|
||||||
public function resetPassword(int $id, string $newPassword): bool
|
|
||||||
{
|
|
||||||
return $this->where(['id' => $id])->update(['password' => hash_password($newPassword)]) !== false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
85
app/common/model/MallUserAsset.php
Normal file
85
app/common/model/MallUserAsset.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use ba\Random;
|
||||||
|
use support\think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户资产(积分商城用户主表,含登录账号字段)
|
||||||
|
*/
|
||||||
|
class MallUserAsset extends Model
|
||||||
|
{
|
||||||
|
protected string $name = 'mall_user_asset';
|
||||||
|
|
||||||
|
protected bool $autoWriteTimestamp = true;
|
||||||
|
|
||||||
|
protected array $type = [
|
||||||
|
'create_time' => 'integer',
|
||||||
|
'update_time' => 'integer',
|
||||||
|
'locked_points' => 'integer',
|
||||||
|
'available_points' => 'integer',
|
||||||
|
'today_limit' => 'integer',
|
||||||
|
'today_claimed' => 'integer',
|
||||||
|
'admin_id' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5 临时登录:按用户名查找或创建资产行,playx_user_id 使用 mall_{id}
|
||||||
|
*/
|
||||||
|
public static function ensureForUsername(string $username): self
|
||||||
|
{
|
||||||
|
$username = trim($username);
|
||||||
|
$existing = self::where('username', $username)->find();
|
||||||
|
if ($existing) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户时:phone 与 username 同值(H5 临时账号)
|
||||||
|
$phone = $username;
|
||||||
|
$phoneExisting = self::where('phone', $phone)->find();
|
||||||
|
if ($phoneExisting) {
|
||||||
|
// 若历史数据存在“手机号=用户名”的行,直接复用;若不一致则拒绝创建,避免污染
|
||||||
|
if (trim((string) ($phoneExisting->username ?? '')) === $username) {
|
||||||
|
return $phoneExisting;
|
||||||
|
}
|
||||||
|
throw new \RuntimeException('Username is already used by another account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pwd = hash_password(Random::build('alnum', 16));
|
||||||
|
$now = time();
|
||||||
|
$temporaryPlayxId = 'tmp_' . bin2hex(random_bytes(16));
|
||||||
|
$created = self::create([
|
||||||
|
'playx_user_id' => $temporaryPlayxId,
|
||||||
|
'username' => $username,
|
||||||
|
'phone' => $phone,
|
||||||
|
'password' => $pwd,
|
||||||
|
'admin_id' => 0,
|
||||||
|
'locked_points' => 0,
|
||||||
|
'available_points' => 0,
|
||||||
|
'today_limit' => 0,
|
||||||
|
'today_claimed' => 0,
|
||||||
|
'today_limit_date' => null,
|
||||||
|
'create_time' => $now,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
if (!$created) {
|
||||||
|
throw new \RuntimeException('Failed to create mall_user_asset');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $created->getKey();
|
||||||
|
$finalPlayxId = 'mall_' . $id;
|
||||||
|
if (self::where('playx_user_id', $finalPlayxId)->where('id', '<>', $id)->find()) {
|
||||||
|
$finalPlayxId = 'mall_' . $id . '_' . bin2hex(random_bytes(4));
|
||||||
|
}
|
||||||
|
$created->playx_user_id = $finalPlayxId;
|
||||||
|
$created->save();
|
||||||
|
|
||||||
|
return $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocateUniquePhone 已废弃:临时登录场景下 phone=用户名
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\validate;
|
|
||||||
|
|
||||||
use think\Validate;
|
|
||||||
|
|
||||||
class ChannelManage extends Validate
|
|
||||||
{
|
|
||||||
protected $failException = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证规则
|
|
||||||
*/
|
|
||||||
protected $rule = [
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提示消息
|
|
||||||
*/
|
|
||||||
protected $message = [
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证场景
|
|
||||||
*/
|
|
||||||
protected $scene = [
|
|
||||||
'add' => [],
|
|
||||||
'edit' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace app\common\validate;
|
|
||||||
|
|
||||||
use think\Validate;
|
|
||||||
|
|
||||||
class MallUser extends Validate
|
|
||||||
{
|
|
||||||
protected $failException = true;
|
|
||||||
|
|
||||||
protected $rule = [
|
|
||||||
'username' => 'require',
|
|
||||||
'phone' => 'require',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $message = [
|
|
||||||
'username.require' => '用户名不能为空',
|
|
||||||
'phone.require' => '手机号不能为空',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $scene = [
|
|
||||||
'add' => ['username', 'phone'],
|
|
||||||
'edit' => ['username', 'phone'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -167,7 +167,7 @@ if (!function_exists('get_auth_token')) {
|
|||||||
|
|
||||||
if (!function_exists('get_agent_jwt_payload')) {
|
if (!function_exists('get_agent_jwt_payload')) {
|
||||||
/**
|
/**
|
||||||
* 解析 Agent JWT authtoken,返回 payload(agent_id、channel_id、admin_id 等)
|
* 解析 Agent JWT authtoken,返回 payload(agent_id、admin_id 等)
|
||||||
* @param string $token authtoken
|
* @param string $token authtoken
|
||||||
* @return array 成功返回 payload,失败返回空数组
|
* @return array 成功返回 payload,失败返回空数组
|
||||||
*/
|
*/
|
||||||
@@ -200,7 +200,8 @@ if (!function_exists('get_controller_path')) {
|
|||||||
$parts = explode('\\', $relative);
|
$parts = explode('\\', $relative);
|
||||||
$path = [];
|
$path = [];
|
||||||
foreach ($parts as $p) {
|
foreach ($parts as $p) {
|
||||||
$path[] = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $p));
|
// 与 admin_rule.name / 前端 path 一致:驼峰首字母小写(如 AdminInfo -> adminInfo),不用 snake_case
|
||||||
|
$path[] = lcfirst($p);
|
||||||
}
|
}
|
||||||
return implode('/', $path);
|
return implode('/', $path);
|
||||||
}
|
}
|
||||||
@@ -216,7 +217,19 @@ if (!function_exists('get_controller_path')) {
|
|||||||
if (count($parts) < 2) {
|
if (count($parts) < 2) {
|
||||||
return $parts[0] ?? null;
|
return $parts[0] ?? null;
|
||||||
}
|
}
|
||||||
return implode('/', array_slice($parts, 1, -1)) ?: $parts[1];
|
// admin/routine.Attachment/index -> routine/attachment(ThinkPHP 风格 URL 中带点的控制器段)
|
||||||
|
$middle = array_slice($parts, 1, -1);
|
||||||
|
$segments = [];
|
||||||
|
foreach ($middle as $m) {
|
||||||
|
if (str_contains($m, '.')) {
|
||||||
|
foreach (explode('.', $m) as $dotPart) {
|
||||||
|
$segments[] = lcfirst($dotPart);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$segments[] = lcfirst($m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $segments !== [] ? implode('/', $segments) : ($parts[1] ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
328
app/process/AngpowImportJobs.php
Normal file
328
app/process/AngpowImportJobs.php
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\process;
|
||||||
|
|
||||||
|
use app\common\model\MallItem;
|
||||||
|
use app\common\model\MallOrder;
|
||||||
|
use app\common\model\MallUserAsset;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Workerman\Timer;
|
||||||
|
use Workerman\Worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angpow 导入推送任务
|
||||||
|
* - 数据源:mall_order(type=BONUS)
|
||||||
|
* - 推送频率:每 30 秒
|
||||||
|
* - 批量:每次最多 100 条(对方文档限制)
|
||||||
|
* - 幂等:merchant_code + report_date 级别签名;每条订单通过 external_transaction_id 在本地控制只推送一次
|
||||||
|
*/
|
||||||
|
class AngpowImportJobs
|
||||||
|
{
|
||||||
|
private const TIMER_SECONDS = 30;
|
||||||
|
private const BATCH_LIMIT = 100;
|
||||||
|
private const MAX_RETRY = 3;
|
||||||
|
|
||||||
|
protected Client $http;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// 确保定时任务只在一个 worker 上运行
|
||||||
|
if (!Worker::getAllWorkers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->http = new Client($this->buildGuzzleOptions());
|
||||||
|
|
||||||
|
Timer::add(self::TIMER_SECONDS, [$this, 'pushPendingOrders']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guzzle 默认校验 HTTPS;Windows 未配置 CA 时会出现 cURL error 60。
|
||||||
|
* 优先使用 PLAYX_ANGPOW_IMPORT_CACERT 指向 cacert.pem;否则可按环境关闭校验(仅开发)。
|
||||||
|
*/
|
||||||
|
private function buildGuzzleOptions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'timeout' => 20,
|
||||||
|
'http_errors' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$conf = config('playx.angpow_import');
|
||||||
|
if (!is_array($conf)) {
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caFile = $conf['ca_file'] ?? '';
|
||||||
|
if (is_string($caFile) && $caFile !== '' && is_file($caFile)) {
|
||||||
|
$options['verify'] = $caFile;
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verifySsl = $conf['verify_ssl'] ?? true;
|
||||||
|
if ($verifySsl === false) {
|
||||||
|
$options['verify'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pushPendingOrders(): void
|
||||||
|
{
|
||||||
|
$conf = config('playx.angpow_import');
|
||||||
|
if (!is_array($conf)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = $conf['base_url'] ?? '';
|
||||||
|
$path = $conf['path'] ?? '';
|
||||||
|
$merchantCode = $conf['merchant_code'] ?? '';
|
||||||
|
$authKey = $conf['auth_key'] ?? '';
|
||||||
|
|
||||||
|
if (!is_string($baseUrl) || $baseUrl === '' || !is_string($path) || $path === '' || !is_string($merchantCode) || $merchantCode === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!is_string($authKey) || $authKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim($baseUrl, '/') . $path;
|
||||||
|
|
||||||
|
$orders = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||||
|
->whereIn('grant_status', [MallOrder::GRANT_NOT_SENT, MallOrder::GRANT_FAILED_RETRYABLE])
|
||||||
|
->where('status', MallOrder::STATUS_PENDING)
|
||||||
|
->where('retry_count', '<', self::MAX_RETRY)
|
||||||
|
->order('id', 'asc')
|
||||||
|
->limit(self::BATCH_LIMIT)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
if ($orders->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportDate = strval(time());
|
||||||
|
$signatureInput = 'merchant_code=' . $merchantCode . '&report_date=' . $reportDate;
|
||||||
|
$signature = $this->buildSignature($signatureInput, $authKey);
|
||||||
|
if ($signature === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'merchant_code' => $merchantCode,
|
||||||
|
'report_date' => $reportDate,
|
||||||
|
'angpow' => [],
|
||||||
|
'currency_visual' => [
|
||||||
|
[
|
||||||
|
'currency' => strval($conf['currency'] ?? 'MYR'),
|
||||||
|
'visual_name' => strval($conf['visual_name'] ?? 'Angpow'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$orderIds = [];
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$row = $this->buildAngpowRow($order);
|
||||||
|
if ($row === null) {
|
||||||
|
// 构造失败:直接标为可重试失败
|
||||||
|
$this->markFailedAttempt($order, 'Build payload failed');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$payload['angpow'][] = $row;
|
||||||
|
$orderIds[] = $order->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($payload['angpow'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先标记“已尝试发送”,避免并发重复推送;同时只在这里累加 retry_count(一次推送=一次尝试)
|
||||||
|
$now = time();
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!in_array($order->id, $orderIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$retry = $order->retry_count ?? 0;
|
||||||
|
if (!is_int($retry)) {
|
||||||
|
$retry = is_numeric($retry) ? intval($retry) : 0;
|
||||||
|
}
|
||||||
|
$order->retry_count = $retry + 1;
|
||||||
|
$order->grant_status = MallOrder::GRANT_SENT_PENDING;
|
||||||
|
$order->update_time = $now;
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = null;
|
||||||
|
$body = '';
|
||||||
|
try {
|
||||||
|
$res = $this->http->post($url, [
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-Request-Signature' => $signature,
|
||||||
|
],
|
||||||
|
'json' => $payload,
|
||||||
|
]);
|
||||||
|
$body = strval($res->getBody());
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 网络/异常:对这一批订单记一次失败尝试
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->markFailedAttempt($order, $e->getMessage());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->markFailedAttempt($order, 'Invalid response');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $data['code'] ?? null;
|
||||||
|
$message = $data['message'] ?? '';
|
||||||
|
$msg = is_string($message) ? $message : 'Request failed';
|
||||||
|
|
||||||
|
// 成功:code=0
|
||||||
|
if ($code === '0' || $code === 0) {
|
||||||
|
MallOrder::whereIn('id', $orderIds)->update([
|
||||||
|
'grant_status' => MallOrder::GRANT_ACCEPTED,
|
||||||
|
'fail_reason' => null,
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败:整批视为失败(对方未提供逐条返回)
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->markFailedAttempt($order, $msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildAngpowRow(MallOrder $order): ?array
|
||||||
|
{
|
||||||
|
$asset = MallUserAsset::where('playx_user_id', $order->user_id)->find();
|
||||||
|
if (!$asset) {
|
||||||
|
if (is_string($order->user_id) && ctype_digit($order->user_id)) {
|
||||||
|
$byId = MallUserAsset::where('id', $order->user_id)->find();
|
||||||
|
if ($byId) {
|
||||||
|
$asset = $byId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$asset || !is_string($asset->playx_user_id ?? null) || strval($asset->playx_user_id) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = null;
|
||||||
|
if ($order->mallItem) {
|
||||||
|
$item = $order->mallItem;
|
||||||
|
} else {
|
||||||
|
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||||||
|
}
|
||||||
|
if (!$item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $createTime = $order->create_time ?? null;
|
||||||
|
// if (!is_int($createTime)) {
|
||||||
|
// if (is_numeric($createTime)) {
|
||||||
|
// $createTime = intval($createTime);
|
||||||
|
// } else {
|
||||||
|
// $createTime = time();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
$start = gmdate('Y-m-d H:i:s', strtotime($order->start_time));
|
||||||
|
$end = gmdate('Y-m-d H:i:s', strtotime($order->end_time));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'member_login' => strval($asset->playx_user_id),
|
||||||
|
'start_time' => $start,
|
||||||
|
'end_time' => $end,
|
||||||
|
'amount' => $order->amount,
|
||||||
|
'reward_name' => strval($item->title ?? ''),
|
||||||
|
'description' => strval($item->description ?? ''),
|
||||||
|
'member_inbox_message' => 'Congratulations! You received an angpow.',
|
||||||
|
'category' => strval($item->category ?? ''),
|
||||||
|
'category_title' => strval($item->category_title ?? ''),
|
||||||
|
'one_time_turnover' => 'yes',
|
||||||
|
'multiplier' => $order->multiplier,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markFailedAttempt(MallOrder $order, string $reason): void
|
||||||
|
{
|
||||||
|
// retry_count 在“准备发送”阶段已 +1;此处用当前 retry_count 作为 attempt 编号
|
||||||
|
$retryCount = $order->retry_count ?? 0;
|
||||||
|
$attempt = is_int($retryCount) ? $retryCount : (is_numeric($retryCount) ? intval($retryCount) : 0);
|
||||||
|
if ($attempt <= 0) {
|
||||||
|
$attempt = 1;
|
||||||
|
$order->retry_count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prev = $order->fail_reason;
|
||||||
|
$prefix = 'attempt ' . $attempt . ': ';
|
||||||
|
$line = $prefix . $reason;
|
||||||
|
$newReason = $line;
|
||||||
|
if (is_string($prev) && $prev !== '') {
|
||||||
|
$newReason = $prev . "\n" . $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
$final = $attempt >= self::MAX_RETRY;
|
||||||
|
$order->grant_status = $final ? MallOrder::GRANT_FAILED_FINAL : MallOrder::GRANT_FAILED_RETRYABLE;
|
||||||
|
$order->fail_reason = $newReason;
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成对方要求的 Base64(HMAC-SHA1)
|
||||||
|
* - 文档中示例 python 会 base64_decode(key) 后参与 hmac
|
||||||
|
* - 生产 key 由 BA 提供,可能是 base64 或 hex;这里做兼容处理
|
||||||
|
*/
|
||||||
|
private function buildSignature(string $input, string $authKey): ?string
|
||||||
|
{
|
||||||
|
$keyBytes = null;
|
||||||
|
|
||||||
|
$maybeBase64 = base64_decode($authKey, true);
|
||||||
|
if ($maybeBase64 !== false && $maybeBase64 !== '') {
|
||||||
|
$keyBytes = $maybeBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyBytes === null) {
|
||||||
|
$isHex = ctype_xdigit($authKey) && (strlen($authKey) % 2 === 0);
|
||||||
|
if ($isHex) {
|
||||||
|
$hex = hex2bin($authKey);
|
||||||
|
if ($hex !== false && $hex !== '') {
|
||||||
|
$keyBytes = $hex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyBytes === null) {
|
||||||
|
$keyBytes = $authKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = hash_hmac('sha1', $input, $keyBytes, true);
|
||||||
|
if (!is_string($raw) || $raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
namespace app\process;
|
namespace app\process;
|
||||||
|
|
||||||
use Webman\App;
|
use Webman\App;
|
||||||
use Webman\Http\Response;
|
|
||||||
|
|
||||||
class Http extends App
|
class Http extends App
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 在父类处理前拦截 OPTIONS 预检,直接返回 CORS 头(避免预检未命中路由时无 CORS)
|
* 在父类处理前拦截 OPTIONS 预检,直接返回 CORS 头(避免预检未命中路由时无 CORS)
|
||||||
|
* 必须与 AllowCrossDomain::optionsResponse 一致,否则会覆盖中间件里对 Allow-Headers(如 server)的配置
|
||||||
*/
|
*/
|
||||||
public function onMessage($connection, $request): void
|
public function onMessage($connection, $request): void
|
||||||
{
|
{
|
||||||
@@ -18,19 +18,8 @@ class Http extends App
|
|||||||
$path = is_string($path) ? trim($path, '/') : '';
|
$path = is_string($path) ? trim($path, '/') : '';
|
||||||
$isApiOrAdmin = $path !== '' && (str_starts_with($path, 'api') || str_starts_with($path, 'admin'));
|
$isApiOrAdmin = $path !== '' && (str_starts_with($path, 'api') || str_starts_with($path, 'admin'));
|
||||||
if ($isApiOrAdmin) {
|
if ($isApiOrAdmin) {
|
||||||
$origin = $request->header('origin');
|
$response = \app\common\middleware\AllowCrossDomain::optionsResponse($request);
|
||||||
$origin = is_array($origin) ? ($origin[0] ?? '') : (is_string($origin) ? trim($origin) : '');
|
$connection->send($response);
|
||||||
if ($origin === '') {
|
|
||||||
$origin = '*';
|
|
||||||
}
|
|
||||||
$headers = [
|
|
||||||
'Access-Control-Allow-Origin' => $origin,
|
|
||||||
'Access-Control-Allow-Credentials' => 'true',
|
|
||||||
'Access-Control-Max-Age' => '1800',
|
|
||||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
|
||||||
];
|
|
||||||
$connection->send(new Response(204, $headers, ''));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace app\process;
|
namespace app\process;
|
||||||
|
|
||||||
use app\common\model\MallItem;
|
use app\common\library\MallBonusGrantPush;
|
||||||
use app\common\model\MallPlayxOrder;
|
use app\common\model\MallOrder;
|
||||||
use app\common\model\MallPlayxUserAsset;
|
use app\common\model\MallUserAsset;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use Workerman\Timer;
|
use Workerman\Timer;
|
||||||
use Workerman\Worker;
|
use Workerman\Worker;
|
||||||
@@ -47,14 +47,15 @@ class PlayxJobs
|
|||||||
$path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status'));
|
$path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status'));
|
||||||
$url = rtrim($baseUrl, '/') . $path;
|
$url = rtrim($baseUrl, '/') . $path;
|
||||||
|
|
||||||
$list = MallPlayxOrder::where('grant_status', MallPlayxOrder::GRANT_ACCEPTED)
|
$list = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||||
->where('status', MallPlayxOrder::STATUS_PENDING)
|
->where('grant_status', MallOrder::GRANT_ACCEPTED)
|
||||||
|
->where('status', MallOrder::STATUS_PENDING)
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
->limit(50)
|
->limit(50)
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
foreach ($list as $order) {
|
foreach ($list as $order) {
|
||||||
/** @var MallPlayxOrder $order */
|
/** @var MallOrder $order */
|
||||||
try {
|
try {
|
||||||
$res = $this->http->get($url, [
|
$res = $this->http->get($url, [
|
||||||
'query' => [
|
'query' => [
|
||||||
@@ -65,16 +66,16 @@ class PlayxJobs
|
|||||||
$data = json_decode(strval($res->getBody()), true) ?? [];
|
$data = json_decode(strval($res->getBody()), true) ?? [];
|
||||||
$pxStatus = $data['status'] ?? '';
|
$pxStatus = $data['status'] ?? '';
|
||||||
|
|
||||||
if ($pxStatus === MallPlayxOrder::STATUS_COMPLETED) {
|
if ($pxStatus === MallOrder::STATUS_COMPLETED) {
|
||||||
$order->status = MallPlayxOrder::STATUS_COMPLETED;
|
$order->status = MallOrder::STATUS_COMPLETED;
|
||||||
$order->save();
|
$order->save();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pxStatus === 'FAILED' || $pxStatus === MallPlayxOrder::STATUS_REJECTED) {
|
if ($pxStatus === 'FAILED' || $pxStatus === MallOrder::STATUS_REJECTED) {
|
||||||
// 仅在从 PENDING 转 REJECTED 时退分,避免重复入账
|
// 仅在从 PENDING 转 REJECTED 时退分,避免重复入账
|
||||||
$order->status = MallPlayxOrder::STATUS_REJECTED;
|
$order->status = MallOrder::STATUS_REJECTED;
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
|
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||||
$order->fail_reason = strval($data['message'] ?? 'PlayX transaction failed');
|
$order->fail_reason = strval($data['message'] ?? 'PlayX transaction failed');
|
||||||
$order->save();
|
$order->save();
|
||||||
$this->refundPoints($order);
|
$this->refundPoints($order);
|
||||||
@@ -98,24 +99,20 @@ class PlayxJobs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bonusPath = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
|
|
||||||
$withdrawPath = strval(config('playx.api.balance_credit_url', '/api/v1/balance/credit'));
|
|
||||||
$bonusUrl = rtrim($baseUrl, '/') . $bonusPath;
|
|
||||||
$withdrawUrl = rtrim($baseUrl, '/') . $withdrawPath;
|
|
||||||
|
|
||||||
$maxRetry = 3;
|
$maxRetry = 3;
|
||||||
$list = MallPlayxOrder::whereIn('grant_status', [
|
$list = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||||
MallPlayxOrder::GRANT_NOT_SENT,
|
->whereIn('grant_status', [
|
||||||
MallPlayxOrder::GRANT_FAILED_RETRYABLE,
|
MallOrder::GRANT_NOT_SENT,
|
||||||
|
MallOrder::GRANT_FAILED_RETRYABLE,
|
||||||
])
|
])
|
||||||
->where('status', MallPlayxOrder::STATUS_PENDING)
|
->where('status', MallOrder::STATUS_PENDING)
|
||||||
->where('retry_count', '<', $maxRetry)
|
->where('retry_count', '<', $maxRetry)
|
||||||
->order('id', 'desc')
|
->order('id', 'desc')
|
||||||
->limit(50)
|
->limit(50)
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
foreach ($list as $order) {
|
foreach ($list as $order) {
|
||||||
/** @var MallPlayxOrder $order */
|
/** @var MallOrder $order */
|
||||||
$allow = $this->allowRetryByInterval($order);
|
$allow = $this->allowRetryByInterval($order);
|
||||||
if (!$allow) {
|
if (!$allow) {
|
||||||
continue;
|
continue;
|
||||||
@@ -124,25 +121,23 @@ class PlayxJobs
|
|||||||
$order->retry_count = intval($order->retry_count ?? 0) + 1;
|
$order->retry_count = intval($order->retry_count ?? 0) + 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->sendGrantByOrder($order, $bonusUrl, $withdrawUrl, $maxRetry);
|
$this->sendGrantByOrder($order, $maxRetry);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$order->fail_reason = $e->getMessage();
|
$order->fail_reason = $e->getMessage();
|
||||||
if (intval($order->retry_count) >= $maxRetry) {
|
if (intval($order->retry_count) >= $maxRetry) {
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
|
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||||
$order->status = MallPlayxOrder::STATUS_REJECTED;
|
|
||||||
$order->save();
|
$order->save();
|
||||||
$this->refundPoints($order);
|
|
||||||
} else {
|
} else {
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
|
||||||
$order->save();
|
$order->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function allowRetryByInterval(MallPlayxOrder $order): bool
|
private function allowRetryByInterval(MallOrder $order): bool
|
||||||
{
|
{
|
||||||
if ($order->grant_status === MallPlayxOrder::GRANT_NOT_SENT) {
|
if ($order->grant_status === MallOrder::GRANT_NOT_SENT) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,107 +162,39 @@ class PlayxJobs
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendGrantByOrder(MallPlayxOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void
|
private function sendGrantByOrder(MallOrder $order, int $maxRetry): void
|
||||||
{
|
{
|
||||||
$item = null;
|
if ($order->type !== MallOrder::TYPE_BONUS) {
|
||||||
if ($order->mallItem) {
|
|
||||||
$item = $order->mallItem;
|
|
||||||
} else {
|
|
||||||
$item = MallItem::where('id', $order->mall_item_id)->find();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($order->type === MallPlayxOrder::TYPE_BONUS) {
|
|
||||||
$rewardName = $item ? strval($item->title) : '';
|
|
||||||
$category = $item ? strval($item->category) : 'daily';
|
|
||||||
$categoryTitle = $item ? strval($item->category_title) : '';
|
|
||||||
$multiplier = intval($order->multiplier ?? 0);
|
|
||||||
if ($multiplier <= 0) {
|
|
||||||
$multiplier = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$requestId = 'mall_retry_bonus_' . uniqid();
|
|
||||||
$res = $this->http->post($bonusUrl, [
|
|
||||||
'json' => [
|
|
||||||
'request_id' => $requestId,
|
|
||||||
'externalTransactionId' => $order->external_transaction_id,
|
|
||||||
'user_id' => $order->user_id,
|
|
||||||
'amount' => $order->amount,
|
|
||||||
'rewardName' => $rewardName,
|
|
||||||
'category' => $category,
|
|
||||||
'categoryTitle' => $categoryTitle,
|
|
||||||
'multiplier' => $multiplier,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$data = json_decode(strval($res->getBody()), true) ?? [];
|
|
||||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
|
|
||||||
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
|
|
||||||
$order->save();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order->fail_reason = strval($data['message'] ?? 'PlayX bonus grant not accepted');
|
|
||||||
if (intval($order->retry_count) >= $maxRetry) {
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
|
|
||||||
$order->status = MallPlayxOrder::STATUS_REJECTED;
|
|
||||||
$order->save();
|
|
||||||
$this->refundPoints($order);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
|
||||||
$order->save();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($order->type === MallPlayxOrder::TYPE_WITHDRAW) {
|
$result = MallBonusGrantPush::push($order);
|
||||||
$multiplier = intval($order->multiplier ?? 0);
|
if ($result['ok']) {
|
||||||
if ($multiplier <= 0) {
|
$order->grant_status = MallOrder::GRANT_ACCEPTED;
|
||||||
$multiplier = 1;
|
$order->playx_transaction_id = $result['playx_transaction_id'];
|
||||||
}
|
|
||||||
$requestId = 'mall_retry_withdraw_' . uniqid();
|
|
||||||
$res = $this->http->post($withdrawUrl, [
|
|
||||||
'json' => [
|
|
||||||
'request_id' => $requestId,
|
|
||||||
'externalTransactionId' => $order->external_transaction_id,
|
|
||||||
'user_id' => $order->user_id,
|
|
||||||
'amount' => $order->amount,
|
|
||||||
'multiplier' => $multiplier,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$data = json_decode(strval($res->getBody()), true) ?? [];
|
|
||||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
|
|
||||||
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
|
|
||||||
$order->save();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order->fail_reason = strval($data['message'] ?? 'PlayX balance credit not accepted');
|
|
||||||
if (intval($order->retry_count) >= $maxRetry) {
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
|
|
||||||
$order->status = MallPlayxOrder::STATUS_REJECTED;
|
|
||||||
$order->save();
|
|
||||||
$this->refundPoints($order);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
|
||||||
$order->save();
|
$order->save();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHYSICAL 目前由后台手工发货/驳回,不参与 PlayX 发放重试
|
$order->fail_reason = $result['message'];
|
||||||
|
if (intval($order->retry_count) >= $maxRetry) {
|
||||||
|
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
|
||||||
|
$order->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function refundPoints(MallPlayxOrder $order): void
|
private function refundPoints(MallOrder $order): void
|
||||||
{
|
{
|
||||||
if ($order->points_cost <= 0) {
|
if ($order->points_cost <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$asset = MallPlayxUserAsset::where('user_id', $order->user_id)->find();
|
$asset = MallUserAsset::where('playx_user_id', $order->user_id)->find();
|
||||||
if (!$asset) {
|
if (!$asset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"nelexa/zip": "^4.0.0",
|
"nelexa/zip": "^4.0.0",
|
||||||
"voku/anti-xss": "^4.1",
|
"voku/anti-xss": "^4.1",
|
||||||
"topthink/think-validate": "^3.0",
|
"topthink/think-validate": "^3.0",
|
||||||
"firebase/php-jwt": "^7.0"
|
"firebase/php-jwt": "^7.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.10"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-event": "For better performance. "
|
"ext-event": "For better performance. "
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
// 允许跨域访问的域名(* 表示任意;开发可用 *,生产建议填具体域名)
|
// 允许跨域访问的域名(* 表示任意;开发可用 *,生产建议填具体域名)
|
||||||
'cors_request_domain' => '*',
|
'cors_request_domain' => '*,playx-admin.cjdhr.top',
|
||||||
// 是否开启会员登录验证码
|
// 是否开启会员登录验证码
|
||||||
'user_login_captcha' => true,
|
'user_login_captcha' => false,
|
||||||
// 是否开启管理员登录验证码
|
// 是否开启管理员登录验证码
|
||||||
'admin_login_captcha' => true,
|
'admin_login_captcha' => false,
|
||||||
// 会员登录失败可重试次数,false则无限
|
// 会员登录失败可重试次数,false则无限
|
||||||
'user_login_retry' => 10,
|
'user_login_retry' => 10,
|
||||||
// 管理员登录失败可重试次数,false则无限
|
// 管理员登录失败可重试次数,false则无限
|
||||||
@@ -87,10 +87,14 @@ return [
|
|||||||
'agents' => [
|
'agents' => [
|
||||||
// 'agent_001' => 'your_secret_key',
|
// 'agent_001' => 'your_secret_key',
|
||||||
],
|
],
|
||||||
// JWT 签名密钥(留空则使用 token.key)
|
// JWT 签名密钥(留空则使用 token.key);建议 AGENT_AUTH_JWT_SECRET 注入
|
||||||
'jwt_secret' => '',
|
'jwt_secret' => strval(env('AGENT_AUTH_JWT_SECRET', '')),
|
||||||
|
// 是否启用 H5 临时登录接口 /api/v1/temLogin
|
||||||
|
'temp_login_enable' => true,
|
||||||
// Token 有效期(秒),默认 24 小时
|
// Token 有效期(秒),默认 24 小时
|
||||||
'token_expire' => 86400,
|
'token_expire' => 86400,
|
||||||
|
// 临时登录 token 有效期(秒),默认 1 天
|
||||||
|
'temp_login_expire' => 86400,
|
||||||
],
|
],
|
||||||
// 版本号
|
// 版本号
|
||||||
'version' => 'v2.3.6',
|
'version' => 'v2.3.6',
|
||||||
|
|||||||
@@ -12,8 +12,18 @@ return [
|
|||||||
'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')),
|
'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')),
|
||||||
// Daily Push 签名校验(PlayX 调用商城时使用)
|
// Daily Push 签名校验(PlayX 调用商城时使用)
|
||||||
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')),
|
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')),
|
||||||
|
/**
|
||||||
|
* 合作方 JWT 验签密钥(HS256)。非空时:dailyPush 等回调需带 Authorization: Bearer
|
||||||
|
* 仅写入部署环境变量,勿提交仓库。
|
||||||
|
*/
|
||||||
|
'partner_jwt_secret' => strval(env('PLAYX_PARTNER_JWT_SECRET', '')),
|
||||||
// token 会话缓存过期时间(秒)
|
// token 会话缓存过期时间(秒)
|
||||||
'session_expire_seconds' => intval(env('PLAYX_SESSION_EXPIRE_SECONDS', '3600')),
|
'session_expire_seconds' => intval(env('PLAYX_SESSION_EXPIRE_SECONDS', '3600')),
|
||||||
|
/**
|
||||||
|
* 为 true 时:/api/v1/playx/verify-token 仅本地校验(查 token 表 + mall_playx_user_asset),不请求 PlayX。
|
||||||
|
* 联调/无 PlayX 环境可开;上线对接 PlayX 后请设为 false 并配置 api.base_url。
|
||||||
|
*/
|
||||||
|
'verify_token_local_only' => filter_var(env('PLAYX_VERIFY_TOKEN_LOCAL_ONLY', '1'), FILTER_VALIDATE_BOOLEAN),
|
||||||
// PlayX API 配置(商城调用 PlayX 时使用)
|
// PlayX API 配置(商城调用 PlayX 时使用)
|
||||||
'api' => [
|
'api' => [
|
||||||
'base_url' => strval(env('PLAYX_API_BASE_URL', '')),
|
'base_url' => strval(env('PLAYX_API_BASE_URL', '')),
|
||||||
@@ -23,4 +33,22 @@ return [
|
|||||||
'balance_credit_url' => '/api/v1/balance/credit',
|
'balance_credit_url' => '/api/v1/balance/credit',
|
||||||
'transaction_status_url' => '/api/v1/transaction/status',
|
'transaction_status_url' => '/api/v1/transaction/status',
|
||||||
],
|
],
|
||||||
|
// Angpow Import API(商城调用对方 cashmarket/merchant-api 时使用)
|
||||||
|
'angpow_import' => [
|
||||||
|
// 对方 base_url,例如 https://ss2-staging2.ttwd8.com
|
||||||
|
'base_url' => strval(env('PLAYX_ANGPOW_IMPORT_BASE_URL', '')),
|
||||||
|
// 路径:文档示例为 /api/v3/merchant/angpow-imports;对方 curl 示例为 /cashmarket/v3/merchant-api/angpow-imports
|
||||||
|
'path' => strval(env('PLAYX_ANGPOW_IMPORT_PATH', '/api/v3/merchant/angpow-imports')),
|
||||||
|
// merchant_code(固定 plx 或按环境配置)
|
||||||
|
'merchant_code' => strval(env('PLAYX_ANGPOW_MERCHANT_CODE', 'plx')),
|
||||||
|
// HMAC-SHA1 的 auth key(生产环境由 BA 提供)
|
||||||
|
'auth_key' => strval(env('PLAYX_ANGPOW_IMPORT_AUTH_KEY', '')),
|
||||||
|
// HTTPS:指定 CA 证书包路径(推荐,下载 https://curl.se/ca/cacert.pem 后填绝对路径,可避免 cURL error 60)
|
||||||
|
'ca_file' => strval(env('PLAYX_ANGPOW_IMPORT_CACERT', '')),
|
||||||
|
// 是否校验 SSL(生产必须为 true;本地无 CA 时可临时 false,勿用于生产)
|
||||||
|
'verify_ssl' => filter_var(env('PLAYX_ANGPOW_IMPORT_VERIFY_SSL', '1'), FILTER_VALIDATE_BOOLEAN),
|
||||||
|
// 固定货币展示映射
|
||||||
|
'currency' => 'MYR',
|
||||||
|
'visual_name' => 'Angpow',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
use support\Log;
|
use support\Log;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use app\process\Http;
|
use app\process\Http;
|
||||||
|
use app\process\AngpowImportJobs;
|
||||||
|
|
||||||
global $argv;
|
global $argv;
|
||||||
|
|
||||||
@@ -65,4 +66,9 @@ return [
|
|||||||
'handler' => app\process\PlayxJobs::class,
|
'handler' => app\process\PlayxJobs::class,
|
||||||
'reloadable' => false,
|
'reloadable' => false,
|
||||||
],
|
],
|
||||||
|
// Angpow 导入推送任务:订单兑换后推送到对方平台
|
||||||
|
'angpow_import_jobs' => [
|
||||||
|
'handler' => AngpowImportJobs::class,
|
||||||
|
'reloadable' => false,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -110,17 +110,22 @@ Route::post('/api/ems/send', [\app\api\controller\Ems::class, 'send']);
|
|||||||
|
|
||||||
// api/v1 鉴权
|
// api/v1 鉴权
|
||||||
Route::get('/api/v1/authToken', [\app\api\controller\v1\Auth::class, 'authToken']);
|
Route::get('/api/v1/authToken', [\app\api\controller\v1\Auth::class, 'authToken']);
|
||||||
|
Route::get('/api/v1/temLogin', [\app\api\controller\v1\Auth::class, 'temLogin']);
|
||||||
|
|
||||||
// api/v1 PlayX 积分商城
|
// api/v1 PlayX 积分商城
|
||||||
Route::post('/api/v1/playx/daily-push', [\app\api\controller\v1\Playx::class, 'dailyPush']);
|
Route::post('/api/v1/mall/dailyPush', [\app\api\controller\v1\Playx::class, 'dailyPush']);
|
||||||
Route::post('/api/v1/playx/verify-token', [\app\api\controller\v1\Playx::class, 'verifyToken']);
|
Route::post('/api/v1/mall/verifyToken', [\app\api\controller\v1\Playx::class, 'verifyToken']);
|
||||||
Route::get('/api/v1/playx/assets', [\app\api\controller\v1\Playx::class, 'assets']);
|
Route::get('/api/v1/mall/assets', [\app\api\controller\v1\Playx::class, 'assets']);
|
||||||
Route::post('/api/v1/playx/claim', [\app\api\controller\v1\Playx::class, 'claim']);
|
Route::post('/api/v1/mall/claim', [\app\api\controller\v1\Playx::class, 'claim']);
|
||||||
Route::get('/api/v1/playx/items', [\app\api\controller\v1\Playx::class, 'items']);
|
Route::get('/api/v1/mall/items', [\app\api\controller\v1\Playx::class, 'items']);
|
||||||
Route::post('/api/v1/playx/bonus/redeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']);
|
Route::post('/api/v1/mall/bonusRedeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']);
|
||||||
Route::post('/api/v1/playx/physical/redeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
Route::post('/api/v1/mall/physicalRedeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
||||||
Route::post('/api/v1/playx/withdraw/apply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
|
Route::post('/api/v1/mall/withdrawApply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
|
||||||
Route::get('/api/v1/playx/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
Route::get('/api/v1/mall/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
||||||
|
Route::get('/api/v1/mall/addressList', [\app\api\controller\v1\Playx::class, 'addressList']);
|
||||||
|
Route::post('/api/v1/mall/addressAdd', [\app\api\controller\v1\Playx::class, 'addressAdd']);
|
||||||
|
Route::post('/api/v1/mall/addressEdit', [\app\api\controller\v1\Playx::class, 'addressEdit']);
|
||||||
|
Route::post('/api/v1/mall/addressDelete', [\app\api\controller\v1\Playx::class, 'addressDelete']);
|
||||||
|
|
||||||
// ==================== Admin 路由 ====================
|
// ==================== Admin 路由 ====================
|
||||||
// Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容
|
// Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Phinx\Migration\AbstractMigration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分商城用户表 mall_player
|
|
||||||
*/
|
|
||||||
class MallPlayer extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function change(): void
|
|
||||||
{
|
|
||||||
if (!$this->hasTable('mall_player')) {
|
|
||||||
$table = $this->table('mall_player', [
|
|
||||||
'id' => false,
|
|
||||||
'comment' => '积分商城用户表',
|
|
||||||
'row_format' => 'DYNAMIC',
|
|
||||||
'primary_key' => 'id',
|
|
||||||
'collation' => 'utf8mb4_unicode_ci',
|
|
||||||
]);
|
|
||||||
$table->addColumn('id', 'integer', ['comment' => 'ID', 'signed' => false, 'identity' => true, 'null' => false])
|
|
||||||
->addColumn('username', 'string', ['limit' => 50, 'default' => '', 'comment' => '用户名', 'null' => false])
|
|
||||||
->addColumn('password', 'string', ['limit' => 255, 'default' => '', 'comment' => '密码', 'null' => false])
|
|
||||||
->addColumn('score', 'integer', ['signed' => false, 'default' => 0, 'comment' => '积分', 'null' => false])
|
|
||||||
->addColumn('create_time', 'biginteger', ['signed' => false, 'null' => true, 'default' => null, 'comment' => '创建时间'])
|
|
||||||
->addColumn('update_time', 'biginteger', ['signed' => false, 'null' => true, 'default' => null, 'comment' => '修改时间'])
|
|
||||||
->addIndex(['username'])
|
|
||||||
->create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
205
docs/H5-积分商城接口文档.md
Normal file
205
docs/H5-积分商城接口文档.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# H5 积分商城接口文档(含流程说明)
|
||||||
|
|
||||||
|
> 面向:H5(活动页/积分商城前台)调用
|
||||||
|
> 基础路径:`/api/v1`
|
||||||
|
> 返回结构:BuildAdmin 通用 `code/msg/time/data`(成功 `code=1`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 总体流程说明
|
||||||
|
|
||||||
|
### 1.1 流程 A:H5 临时登录(推荐)
|
||||||
|
|
||||||
|
适用场景:H5 只需要“用户名级别”的轻量登录,不依赖 PlayX 的 token。
|
||||||
|
|
||||||
|
1. H5 调用 `GET/POST /api/v1/temLogin?username=xxx` 获取 **商城 token**(类型 `muser`)
|
||||||
|
2. H5 后续请求统一携带该 token(推荐放在 Header:`token: <muser_token>`,也可用参数 `token`)
|
||||||
|
3. H5 调用:
|
||||||
|
- `GET /api/v1/mall/assets` 获取资产
|
||||||
|
- `POST /api/v1/mall/claim` 领取积分(幂等)
|
||||||
|
- `GET /api/v1/mall/items` 获取商品
|
||||||
|
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply` 提交兑换/提现
|
||||||
|
- `GET /api/v1/mall/orders` 查询订单
|
||||||
|
- `GET/POST /api/v1/mall/address*` 管理地址(addressList/addressAdd/addressEdit/addressDelete)
|
||||||
|
|
||||||
|
### 1.2 流程 B:PlayX token 换取 session(兼容)
|
||||||
|
|
||||||
|
适用场景:H5 已经拿到了 PlayX 下发的 token,希望换取商城侧 `session_id`。
|
||||||
|
|
||||||
|
1. H5 调用 `POST /api/v1/mall/verifyToken`(传 `token` 或 `session`)
|
||||||
|
2. 服务端返回 `data.session_id`
|
||||||
|
3. H5 后续请求携带 `session_id`(优先级高于 token)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 身份与鉴权(重要)
|
||||||
|
|
||||||
|
以下接口都会通过服务端逻辑解析“当前资产主体”,优先级如下:
|
||||||
|
|
||||||
|
1. **`session_id`**(GET/POST):对应表 `mall_session`,未过期则可映射到资产主体
|
||||||
|
2. **`token`**(GET/POST 或 Header):支持会员 token 或 `muser` token(H5 临时登录签发)
|
||||||
|
3. **`user_id`**(GET/POST):
|
||||||
|
- 纯数字:视为 `mall_user_asset.id`
|
||||||
|
- 非纯数字:按 `mall_user_asset.playx_user_id` 查找
|
||||||
|
|
||||||
|
推荐做法:
|
||||||
|
- H5 统一只用 **Header `token`**(值为 `muser` token),避免 URL 泄露与参数歧义。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 接口列表(H5 常用)
|
||||||
|
|
||||||
|
### 3.1 H5 临时登录
|
||||||
|
|
||||||
|
**GET/POST** ` /api/v1/temLogin `
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- `username`:必填,用户名(字符串)
|
||||||
|
|
||||||
|
成功返回 `data.userInfo`:
|
||||||
|
- `id`:资产主键(`mall_user_asset.id`)
|
||||||
|
- `username`:用户名
|
||||||
|
- `playx_user_id`:映射的 PlayX 用户标识(字符串)
|
||||||
|
- `token`:**muser token**(后续请求使用)
|
||||||
|
- `refresh_token`:刷新 token(当前前端未强依赖可不接)
|
||||||
|
- `expires_in`:秒
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://{域名}/api/v1/temLogin?username=test001"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 资产查询
|
||||||
|
|
||||||
|
**GET** ` /api/v1/mall/assets `
|
||||||
|
|
||||||
|
鉴权:携带 `token` 或 `session_id` 或 `user_id`
|
||||||
|
|
||||||
|
成功返回 `data`:
|
||||||
|
- `locked_points`:待领取积分
|
||||||
|
- `available_points`:可用积分
|
||||||
|
- `today_limit`:今日可领取上限
|
||||||
|
- `today_claimed`:今日已领取
|
||||||
|
- `withdrawable_cash`:可提现现金(积分按配置比例换算)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 领取积分(幂等)
|
||||||
|
|
||||||
|
**POST** ` /api/v1/mall/claim `
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- `claim_request_id`:必填,幂等键(建议:`{业务前缀}_{assetId}_{毫秒时间戳}`)
|
||||||
|
- 身份参数:`token` / `session_id` / `user_id` 三选一(推荐 `token`)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 同一个 `claim_request_id` 重复提交会直接返回成功(不会重复入账)
|
||||||
|
- 会受 `today_limit/today_claimed/locked_points` 限制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 商品列表
|
||||||
|
|
||||||
|
**GET** ` /api/v1/mall/items `
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- `type`:可选,`BONUS | PHYSICAL | WITHDRAW`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 红利兑换(提交订单)
|
||||||
|
|
||||||
|
**POST** ` /api/v1/mall/bonusRedeem `
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- `item_id`:必填
|
||||||
|
- 身份参数:`token` / `session_id` / `user_id`
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- `data.order_id`
|
||||||
|
- `data.status`(通常 `PENDING`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 实物兑换(提交订单)
|
||||||
|
|
||||||
|
**POST** ` /api/v1/mall/physicalRedeem `
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- `item_id`:必填
|
||||||
|
- `address_id`:必填,收货地址主键(`mall_address.id`,须为当前用户资产下地址)
|
||||||
|
- 身份参数:`token` / `session_id` / `user_id`
|
||||||
|
|
||||||
|
说明:服务端会将该地址在下单时刻的 **收货人 / 电话 / 完整地址** 写入订单字段(快照),并写入 `mall_order.mall_address_id` 关联所选地址。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 提现申请(提交订单)
|
||||||
|
|
||||||
|
**POST** ` /api/v1/mall/withdrawApply `
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- `item_id`:必填
|
||||||
|
- 身份参数:`token` / `session_id` / `user_id`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 订单列表
|
||||||
|
|
||||||
|
**GET** ` /api/v1/mall/orders `
|
||||||
|
|
||||||
|
鉴权:`token` / `session_id` / `user_id`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 返回最多 100 条
|
||||||
|
- 订单里包含 `mallItem`(商品信息)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 地址管理(H5)
|
||||||
|
|
||||||
|
> 地址与资产主体通过 `playx_user_asset_id` 关联(即 `mall_user_asset.id`)。
|
||||||
|
|
||||||
|
### 4.1 地址列表
|
||||||
|
**GET** ` /api/v1/mall/addressList `
|
||||||
|
|
||||||
|
### 4.2 新增地址
|
||||||
|
**POST** ` /api/v1/mall/addressAdd `
|
||||||
|
|
||||||
|
Body 含 `receiver_name`(收货人,建议填写;实物兑换下单快照需要非空的收货人、电话与拼接后的完整地址)。
|
||||||
|
|
||||||
|
### 4.3 编辑地址
|
||||||
|
**POST** ` /api/v1/mall/addressEdit `
|
||||||
|
|
||||||
|
### 4.4 删除地址
|
||||||
|
**POST** ` /api/v1/mall/addressDelete `
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. session 换取(可选)
|
||||||
|
|
||||||
|
### 5.1 token 换 session
|
||||||
|
|
||||||
|
**POST** ` /api/v1/mall/verifyToken `
|
||||||
|
|
||||||
|
参数(二选一):
|
||||||
|
- `token`
|
||||||
|
- `session`
|
||||||
|
|
||||||
|
成功返回:
|
||||||
|
- `data.session_id`
|
||||||
|
- `data.user_id`
|
||||||
|
- `data.username`
|
||||||
|
- `data.token_expire_at`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 常见错误与排查
|
||||||
|
|
||||||
|
- **401 登录态过期**:token/session 过期或不匹配;请重新 `temLogin` 或重新 `verifyToken`
|
||||||
|
- **提示缺少必填字段**:按各接口参数补齐(如 `claim_request_id`、`item_id`、`address_id`(实物)、地址中收货人/电话/完整地址等)
|
||||||
|
- **积分不足/无可领取积分**:`locked_points<=0` 或已达 `today_limit`
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
## 0. 交付说明(给 PlayX)
|
## 0. 交付说明(给 PlayX)
|
||||||
|
|
||||||
- **交付物**:本文件(接口清单 + 业务流程 + 联调验收清单)。
|
- **交付物**:本文件(接口清单 + 业务流程 + 联调验收清单)。
|
||||||
- **建议联调顺序**:Token 验证 → 每日推送 → 领取 → 红利发放 → 提现入账 → 实物后台处理。
|
- **建议联调顺序**:Token 验证(远程 PlayX 或本地 `verify_token_local_only`)→ 每日推送 → 领取 → 红利发放 → 提现入账 → 实物后台处理。
|
||||||
- **约定**:接口 URL、字段最终表、签名细节以 PlayX 提供的最终口径为准;本文档负责把流程、幂等、重试与最小字段集合先对齐。
|
- **约定**:接口 URL、字段最终表、签名细节以 PlayX 提供的最终口径为准;本文档负责把流程、幂等、重试与最小字段集合先对齐。
|
||||||
|
|
||||||
## 1. 文档目的与范围
|
## 1. 文档目的与范围
|
||||||
@@ -29,6 +29,8 @@ flowchart LR
|
|||||||
MallBackend -->|"BonusGrantAPI/BalanceCreditAPI"| PlayXBackend
|
MallBackend -->|"BonusGrantAPI/BalanceCreditAPI"| PlayXBackend
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 当 **`playx.verify_token_local_only=true`** 时,「Token 验证」一步在商城内完成,**不经过** `PlayXBackend` 的 Token Verification API;详见 **§4.1**。
|
||||||
|
|
||||||
## 3. 关键业务对象与状态机
|
## 3. 关键业务对象与状态机
|
||||||
|
|
||||||
### 3.1 资产口径(最小集合)
|
### 3.1 资产口径(最小集合)
|
||||||
@@ -55,17 +57,43 @@ flowchart LR
|
|||||||
|
|
||||||
### 4.1 登录鉴权(Iframe + token)
|
### 4.1 登录鉴权(Iframe + token)
|
||||||
|
|
||||||
|
> **接口与字段细节**以代码为准,完整说明见同目录《PlayX-接口文档.md》(§3 H5、§3.2 `temLogin`、§3.3 `verify-token`)。
|
||||||
|
|
||||||
|
#### 4.1.1 身份与数据模型(商城侧)
|
||||||
|
|
||||||
|
- **商城用户**:表 `mall_user`(H5 临时登录、后台创建等均落此表)。
|
||||||
|
- **PlayX 资产扩展**:表 `mall_playx_user_asset`,与 `mall_user` **一对一**(`mall_user_id`、`playx_user_id` 均唯一)。
|
||||||
|
- **业务侧用户标识**:对外接口中的 `user_id`(字符串)在多数场景下即 **`playx_user_id`**(PlayX 玩家 ID)。
|
||||||
|
- 若用户仅通过商城 **临时登录** 进入、尚无 PlayX 正式 ID,商城会生成占位 ID,形如 **`mall_{mall_user.id}`**,与每日推送中的真实 `user_id` 区分(避免与纯数字 ID 混淆)。
|
||||||
|
- **H5 调业务接口时**:服务端内部统一解析为 **`mall_user.id`**,再查资产与订单(解析规则见《PlayX-接口文档》§3.1)。
|
||||||
|
|
||||||
|
#### 4.1.2 模式 A:联调 PlayX(生产/预发,远程校验 token)
|
||||||
|
|
||||||
1. 用户在 PlayX 内打开积分商城入口(iframe)。
|
1. 用户在 PlayX 内打开积分商城入口(iframe)。
|
||||||
2. PlayX 前端通过 postMessage 发送 `token/session` 给商城前端。
|
2. PlayX 前端通过 postMessage 将 **PlayX 下发的 token**(及必要上下文)传给商城 H5。
|
||||||
3. 商城后端调用 PlayX 的 **Token Verification API** 校验 token。
|
3. 商城 H5 调用商城后端 **`POST /api/v1/playx/verify-token`**,由商城向 PlayX 的 **Token Verification API**(`playx.api.base_url` + `playx.api.token_verify_url`)发起校验。
|
||||||
4. PlayX 返回 `user_id`、`username`(以及会话有效期等)。
|
4. **前提**:配置 **`playx.verify_token_local_only = false`**,且 **`playx.api.base_url`** 已配置为可访问的 PlayX 基地址。
|
||||||
5. 商城建立会话,返回会员资产与商品列表数据。
|
5. PlayX 返回 **`user_id`、`username`**(及可选会话过期时间等)。
|
||||||
|
6. 商城写入 **`mall_playx_session`**(`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token,见模式 B)** 调用资产/领取等接口。
|
||||||
|
|
||||||
幂等、安全与会话续期:
|
幂等与安全:
|
||||||
|
|
||||||
- 前端不信任 `user_id` 直传;只接收 token/session。
|
- H5 **不要**把 PlayX 的 `user_id` 当作唯一可信凭据直传下单;**以 token 换 session** 或由商城签发 token 的流程为准。
|
||||||
- Token 验证接口需要签名/鉴权(见第 7 节)。
|
- PlayX 侧 Token Verification API 的鉴权/签名(若有)按双方约定(可参考《PlayX-接口文档》§2.1)。
|
||||||
- **会话续期**:由于玩家访问积分商城可能停留时间较长,当商城调用任意 API 遇到 Token 校验过期(如 HTTP 401)时,商城前端会通过 postMessage 向 PlayX 父级页面请求派发新的 Token 以实现静默续期,请 PlayX 配合予以支持。
|
|
||||||
|
#### 4.1.3 模式 B:本地 / 无 PlayX 环境(商城自校验,不请求 PlayX)
|
||||||
|
|
||||||
|
用于开发、联调前自测、或 PlayX 接口未就绪时:
|
||||||
|
|
||||||
|
1. 配置 **`playx.verify_token_local_only = true`**(环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**,默认可为开启,以项目 `config/playx.php` 为准)。
|
||||||
|
2. 此时 **`/api/v1/playx/verify-token` 不会访问 PlayX**,仅在商城内校验 **商城临时 token**(token 表类型 **`muser`**,由下方 `temLogin` 签发)。
|
||||||
|
3. 调用 **`GET/POST /api/v1/temLogin?username=...`**(需 **`buildadmin.agent_auth.temp_login_enable = true`**):不存在则创建 **`mall_user`**,并保证存在 **`mall_playx_user_asset`**(含 `playx_user_id`,默认 **`mall_{id}`**),返回 **`userInfo.token`**、**`playx_user_id`**、**`expires_in`** 等。
|
||||||
|
4. 再用该 token 调用 **`verify-token`** 可得到 **`session_id`**,与模式 A 一样供后续接口使用;或直接带 **`token` / `ba-token`** 调资产等接口(见《PlayX-接口文档》§3.1)。
|
||||||
|
|
||||||
|
#### 4.1.4 会话续期与前端约定
|
||||||
|
|
||||||
|
- **会话续期**:玩家停留时间较长时,若商城 API 返回 token/session 失效(如 401),H5 可通过 postMessage 请 PlayX 父页面 **重新派发 PlayX token**(模式 A);模式 B 下可重新 **`temLogin`** 或走 **`/api/common/refreshToken`**(`muser-refresh`)换取新 access token。
|
||||||
|
- 具体错误码与 Header(如 `ba-token`)以前端与《PlayX-接口文档》为准。
|
||||||
|
|
||||||
### 4.2 每日 T+1 入池(PlayX → 商城)
|
### 4.2 每日 T+1 入池(PlayX → 商城)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# PlayX 接口文档(按调用方向拆分)
|
# PlayX 接口文档(按调用方向拆分)
|
||||||
|
|
||||||
说明:本文档严格依据当前代码 `app/api/controller/v1/Playx.php` 与定时任务 `app/process/PlayxJobs.php` 整理。
|
说明:本文档严格依据当前代码 `app/api/controller/v1/Playx.php`、`app/api/controller/v1/Auth.php`(临时登录)、`config/playx.php` 与定时任务 `app/process/PlayxJobs.php` 整理。
|
||||||
|
|
||||||
三类接口分别为:
|
三类接口分别为:
|
||||||
- `积分商城 -> PlayX`(PlayX 调用商城)
|
- `积分商城 -> PlayX`(PlayX 调用商城)
|
||||||
@@ -13,31 +13,67 @@
|
|||||||
|
|
||||||
### 1.1 Daily Push API
|
### 1.1 Daily Push API
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/daily-push`
|
* 路径:`/api/v1/mall/dailyPush`
|
||||||
|
|
||||||
#### Header(签名校验:可选)
|
#### Header(多语言,可选)
|
||||||
当 `playx.daily_push_secret` 配置非空时,需要携带:
|
- `lang`: `zh`/`zh-cn` 返回中文(默认);`en` 返回英文
|
||||||
|
|
||||||
|
#### Header(签名校验:HMAC 必填)
|
||||||
|
当 `playx.daily_push_secret` 配置非空时,需要携带(HMAC):
|
||||||
- `X-Request-Id`:请求 ID
|
- `X-Request-Id`:请求 ID
|
||||||
- `X-Timestamp`:时间戳
|
- `X-Timestamp`:时间戳
|
||||||
- `X-Signature`:签名(HMAC_SHA256)
|
- `X-Signature`:签名(HMAC_SHA256)
|
||||||
|
|
||||||
服务端签名计算:
|
服务端签名计算:
|
||||||
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body)`
|
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)`
|
||||||
- `expected = hash_hmac('sha256', canonical, daily_push_secret)`
|
- `expected = hash_hmac('sha256', canonical, daily_push_secret)`
|
||||||
- 校验:`hash_equals(expected, X-Signature)`
|
- 校验:`hash_equals(expected, X-Signature)`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 本项目对接方案为 **仅启用 HMAC**,不使用 `Authorization` 头做校验。
|
||||||
|
|
||||||
#### Body
|
#### Body
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| `request_id` | string | 是 | 外部推送请求号(原样返回) |
|
| `request_id` | string | 是 | 外部推送请求号(原样返回) |
|
||||||
| `date` | string(YYYY-MM-DD) | 是 | 业务日期(入库到 `mall_playx_daily_push.date`) |
|
| `date` | string(YYYY-MM-DD) | 是 | 业务日期(入库到 `mall_playx_daily_push.date`) |
|
||||||
| `user_id` | string | 是 | PlayX 用户 ID(用于幂等) |
|
| `user_id` | string | 是 | PlayX 用户 ID(用于幂等;入库 `mall_playx_daily_push.user_id` 等;服务端会映射/创建 `mall_user` 与 `mall_playx_user_asset`) |
|
||||||
| `username` | string | 否 | 展示冗余 |
|
| `username` | string | 否 | 展示冗余(同步到商城用户侧逻辑时使用) |
|
||||||
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢(仅当 `< 0` 时计算新增保障金) |
|
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢(仅当 `< 0` 时计算新增保障金) |
|
||||||
| `yesterday_total_deposit` | number | 否 | 昨日总充值(用于计算今日可领取上限) |
|
| `yesterday_total_deposit` | number | 否 | 昨日总充值(用于计算今日可领取上限) |
|
||||||
| `lifetime_total_deposit` | number | 否 | 历史总充值 |
|
| `lifetime_total_deposit` | number | 否 | 历史总充值 |
|
||||||
| `lifetime_total_withdraw` | number | 否 | 历史总提现 |
|
| `lifetime_total_withdraw` | number | 否 | 历史总提现 |
|
||||||
|
|
||||||
|
##### 格式 B:新版批量上报(兼容你截图)
|
||||||
|
新版 body 形如:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"report_date": "1700000000",
|
||||||
|
"member": [
|
||||||
|
{
|
||||||
|
"member_id": "123456",
|
||||||
|
"login": "john",
|
||||||
|
"lty_deposit": 15230.75,
|
||||||
|
"lty_withdrawal": 12400.50,
|
||||||
|
"yesterday_total_w": -320.25,
|
||||||
|
"yesterday_total_deposit": 500.00
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段映射(服务端内部会转换成旧字段再计算):
|
||||||
|
- `report_date` -> `date`(若为 Unix 秒则转为 `YYYY-MM-DD`)
|
||||||
|
- `member[].member_id` -> `user_id`
|
||||||
|
- `member[].login` -> `username`
|
||||||
|
- `member[].yesterday_total_w` -> `yesterday_win_loss_net`
|
||||||
|
- `member[].yesterday_total_deposit` -> `yesterday_total_deposit`
|
||||||
|
- `member[].lty_deposit` -> `lifetime_total_deposit`
|
||||||
|
- `member[].lty_withdrawal` -> `lifetime_total_withdraw`
|
||||||
|
|
||||||
|
返回补充:
|
||||||
|
- 批量模式会在 `data` 里增加 `results[]`,每个成员一条结果(是否 `deduped`)。
|
||||||
|
|
||||||
#### 幂等规则
|
#### 幂等规则
|
||||||
* 幂等键:`user_id + date`
|
* 幂等键:`user_id + date`
|
||||||
* 重复推送:不会重复入账,返回 `data.deduped=true`
|
* 重复推送:不会重复入账,返回 `data.deduped=true`
|
||||||
@@ -68,7 +104,7 @@
|
|||||||
#### 示例(未开启签名校验)
|
#### 示例(未开启签名校验)
|
||||||
请求:
|
请求:
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{
|
-d '{
|
||||||
"request_id":"req_1001",
|
"request_id":"req_1001",
|
||||||
@@ -97,13 +133,57 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 示例(新版批量上报)
|
||||||
|
请求:
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"report_date": "1700000000",
|
||||||
|
"member": [
|
||||||
|
{
|
||||||
|
"member_id": "123456",
|
||||||
|
"login": "john",
|
||||||
|
"lty_deposit": 15230.75,
|
||||||
|
"lty_withdrawal": 12400.50,
|
||||||
|
"yesterday_total_w": -320.25,
|
||||||
|
"yesterday_total_deposit": 500.00
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
返回(首次写入至少一个成员时的示例):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "",
|
||||||
|
"time": 0,
|
||||||
|
"data": {
|
||||||
|
"request_id": "report_2023-11-14",
|
||||||
|
"accepted": true,
|
||||||
|
"deduped": false,
|
||||||
|
"message": "Ok",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"user_id": "123456",
|
||||||
|
"accepted": true,
|
||||||
|
"deduped": false,
|
||||||
|
"message": "Ok"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. PlayX -> 积分商城(商城调用 PlayX)
|
## 2. PlayX -> 积分商城(商城调用 PlayX)
|
||||||
|
|
||||||
> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。
|
> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。
|
||||||
|
> **说明**:H5 调商城的 **`/api/v1/mall/verifyToken`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
|
||||||
|
|
||||||
### 2.1 Token Verification API
|
### 2.1 Token Verification API(PlayX 侧实现,远程验证时使用)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* URL:`${playx.api.base_url}${playx.api.token_verify_url}`
|
* URL:`${playx.api.base_url}${playx.api.token_verify_url}`
|
||||||
* 默认:`/api/v1/auth/verify-token`
|
* 默认:`/api/v1/auth/verify-token`
|
||||||
@@ -257,48 +337,171 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
|||||||
|
|
||||||
## 3. 积分商城 -> H5(服务端提供给 H5 的接口)
|
## 3. 积分商城 -> H5(服务端提供给 H5 的接口)
|
||||||
|
|
||||||
说明:鉴权与用户解析规则由 `resolveUserIdFromRequest()` 决定。
|
### 3.0 数据模型说明(与代码一致)
|
||||||
* 优先使用 `session_id`(在 `mall_playx_session` 查到且未过期)
|
|
||||||
* 其次使用 `user_id`
|
|
||||||
|
|
||||||
公共鉴权字段:
|
* **商城用户**:表 `mall_user`(主键 `id`)。
|
||||||
* `session_id`:字符串
|
* **PlayX 资产扩展**:表 `mall_playx_user_asset`,与 `mall_user` **一对一**(`mall_user_id` 唯一,`playx_user_id` 唯一)。
|
||||||
* `user_id`:字符串
|
* **对外业务 ID**:接口里返回或订单里使用的 `user_id` 字符串多为 **PlayX 侧用户 ID**(`playx_user_id`);H5 临时登录场景若尚无真实 PlayX ID,会生成形如 **`mall_{mall_user.id}`** 的占位 ID(见 `temLogin`)。
|
||||||
|
* **服务端内部**:`Playx` 控制器内部用 **`mall_user.id`**(整型)解析资产;`session_id` / `token` / `user_id` 会映射到该 `mall_user`。
|
||||||
|
|
||||||
|
### 3.1 鉴权解析规则(`resolveMallUserIdFromRequest`)
|
||||||
|
|
||||||
|
以下接口在服务端最终都会解析出 **商城用户 `mall_user.id`**,再按该用户查询 `mall_playx_user_asset` 等。
|
||||||
|
|
||||||
|
优先级(由高到低):
|
||||||
|
|
||||||
|
1. **`session_id`**(`post` 优先,`get` 兼容)
|
||||||
|
* 在 `mall_playx_session` 中存在且未过期:用会话里的 `user_id`(即 `playx_user_id`)在 `mall_playx_user_asset` 反查 `mall_user_id`。
|
||||||
|
* 若会话无效:兼容把 `session_id` 参数误当作 **商城 token** 再试一次(UUID 形态 token)。
|
||||||
|
2. **`token`**(`post` / `get` 或请求头 **`ba-token`** / **`token`**)
|
||||||
|
* 校验 `token` 表:类型为会员 `user` 或商城临时 **`muser`**(`mall_user` 登录),未过期则 `user_id` 字段即为 **`mall_user.id`**。
|
||||||
|
3. **`user_id`**(`post` / `get` 兼容)
|
||||||
|
* **纯数字**:视为 **`mall_user.id`**。
|
||||||
|
* **非纯数字**:视为 **`playx_user_id`**,在 `mall_playx_user_asset` 查找对应 `mall_user_id`。
|
||||||
|
|
||||||
> 注意:请求参数的取值方式是 `post()` 优先,`get()` 兼容(即同字段既可传 post 也可传 get)。
|
> 注意:请求参数的取值方式是 `post()` 优先,`get()` 兼容(即同字段既可传 post 也可传 get)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.1 Token 验证
|
### 3.2 临时登录(获取商城 token)
|
||||||
* 方法:`POST`
|
|
||||||
* 路径:`/api/v1/playx/verify-token`
|
* 方法:`GET`(推荐)或 `POST`
|
||||||
|
* 路径:`/api/v1/temLogin`
|
||||||
|
* 开关:`config/buildadmin.php` → `agent_auth.temp_login_enable` 为 `true`;有效期 `agent_auth.temp_login_expire`(秒)。
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `username` | string | 是 | 商城用户名(唯一);不存在则自动创建 `mall_user` |
|
||||||
|
|
||||||
|
#### 行为说明
|
||||||
|
|
||||||
|
* 若 `mall_user` 不存在:创建用户(随机占位手机号、随机密码等,与后台「商城用户」一致)。
|
||||||
|
* **无论是否新用户**:保证存在 **`mall_playx_user_asset`** 一条记录(`MallPlayxUserAsset::ensureForMallUser`),`playx_user_id` 默认 **`mall_{mall_user.id}`**(与 PlayX 真实 ID 冲突概率低)。
|
||||||
|
* 签发 **商城 token**(类型 **`muser`**,非会员表 `user`),并签发 `muser-refresh` 刷新令牌。
|
||||||
|
|
||||||
|
#### 返回(成功 data.userInfo)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | int | `mall_user.id` |
|
||||||
|
| `username` | string | 用户名 |
|
||||||
|
| `nickname` | string | 同 `username` |
|
||||||
|
| `playx_user_id` | string | 资产表中的 `playx_user_id`(如 `mall_12`) |
|
||||||
|
| `token` | string | 访问 H5 接口时携带 |
|
||||||
|
| `refresh_token` | string | 调用 `/api/common/refreshToken` 时使用(类型 `muser-refresh`) |
|
||||||
|
| `expires_in` | int | token 有效秒数 |
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_h5'
|
||||||
|
```
|
||||||
|
|
||||||
|
用户名含 `+` 等号时需 URL 编码(如 `%2B60123456789`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 Token 验证(换 session)
|
||||||
|
|
||||||
|
* 方法:`POST`(推荐 `GET` 传 `token` 亦可)
|
||||||
|
* 路径:`/api/v1/mall/verifyToken`
|
||||||
|
|
||||||
|
#### 配置:本地验证 vs 远程 PlayX
|
||||||
|
|
||||||
|
* 配置项:`config/playx.php` → **`verify_token_local_only`**(环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**,未设置时默认为 **`1` / 开启本地验证)。
|
||||||
|
* **`verify_token_local_only = true`(默认)**
|
||||||
|
* **不请求** PlayX HTTP。
|
||||||
|
* 仅接受商城临时登录 token(类型 **`muser`**),校验 `token` 表后,根据 `mall_user` 与 `mall_playx_user_asset` 写入 `mall_playx_session`。
|
||||||
|
* 返回的 `data.user_id` 为 **`playx_user_id`**(无资产记录时回退为 `mall_user.id` 字符串,一般 temLogin 后已有资产)。
|
||||||
|
* **`verify_token_local_only = false`**(生产对接 PlayX)
|
||||||
|
* 需配置 **`playx.api.base_url`**,由商城向 PlayX 发起 `POST` 校验(见下文「远程模式」)。
|
||||||
|
* 若未配置 `base_url`,返回 `PlayX API not configured`。
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
#### 请求 Body
|
|
||||||
必填其一:
|
必填其一:
|
||||||
* `token`(优先读取)
|
|
||||||
* `session`(兼容字段,当 `token` 为空时会被当作 token)
|
* `token`(Body 优先;`session` 兼容字段;Query 也可传 `token`)
|
||||||
|
|
||||||
#### 返回(成功 data)
|
#### 返回(成功 data)
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `session_id` | string | 写入 `mall_playx_session` |
|
| `session_id` | string | 写入 `mall_playx_session` |
|
||||||
| `user_id` | string | PlayX 用户 ID |
|
| `user_id` | string | PlayX 用户 ID(即 `playx_user_id`,会话内与订单/推送一致) |
|
||||||
| `username` | string | 用户名 |
|
| `username` | string | 用户名 |
|
||||||
| `token_expire_at` | string | ISO 字符串(服务端 `date('c', expireAt)`) |
|
| `token_expire_at` | string | ISO 字符串(服务端 `date('c', expireAt)`) |
|
||||||
|
|
||||||
失败:
|
失败:
|
||||||
* token 为空:HTTP 401,msg=`INVALID_TOKEN`
|
|
||||||
* PlayX 未配置:msg=`PlayX API not configured`
|
|
||||||
|
|
||||||
#### 示例
|
* token 为空:HTTP 401,msg=`INVALID_TOKEN`
|
||||||
请求:
|
* 远程模式且 PlayX 未配置:`msg=PlayX API not configured`
|
||||||
|
|
||||||
|
#### 示例(本地验证)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'token=PLAYX_TOKEN_XXX'
|
--data-urlencode 'token=上一步TemLogin返回的token'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.x 收货地址(`mall_address`)
|
||||||
|
|
||||||
|
> 下面接口用于 H5 维护收货地址。鉴权同本章其他接口:携带 `session_id` 或 `token` 或 `user_id`。
|
||||||
|
|
||||||
|
#### 3.x.1 地址列表
|
||||||
|
* 方法:`GET`
|
||||||
|
* 路径:`/api/v1/mall/addressList`
|
||||||
|
|
||||||
|
返回:`data.list` 为地址数组。
|
||||||
|
|
||||||
|
#### 3.x.2 添加地址
|
||||||
|
* 方法:`POST`
|
||||||
|
* 路径:`/api/v1/mall/addressAdd`
|
||||||
|
|
||||||
|
Body:
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `receiver_name` | 是 | 收货人 |
|
||||||
|
| `phone` | 是 | 电话 |
|
||||||
|
| `region` | 是 | 地区(数组或逗号分隔字符串) |
|
||||||
|
| `detail_address` | 是 | 详细地址 |
|
||||||
|
| `default_setting` | 否 | `1` 设为默认地址 |
|
||||||
|
|
||||||
|
#### 3.x.3 修改地址(含设为默认)
|
||||||
|
* 方法:`POST`
|
||||||
|
* 路径:`/api/v1/mall/addressEdit`
|
||||||
|
|
||||||
|
Body:`id` 必填,其余字段按需传入更新。
|
||||||
|
|
||||||
|
#### 3.x.4 删除地址
|
||||||
|
* 方法:`POST`
|
||||||
|
* 路径:`/api/v1/mall/addressDelete`
|
||||||
|
|
||||||
|
Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。
|
||||||
|
|
||||||
|
#### 远程模式(`verify_token_local_only=false` + 已配置 `base_url`)
|
||||||
|
|
||||||
|
商城侧请求 URL:`${playx.api.base_url}${playx.api.token_verify_url}`(默认路径 `/api/v1/auth/verify-token`)。
|
||||||
|
|
||||||
|
#### 请求 Body(商城侧发送)——仅远程模式
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `request_id` | string | 是 | 形如 `mall_{uniqid}` |
|
||||||
|
| `token` | string | 是 | 前端传入的 PlayX token |
|
||||||
|
|
||||||
|
#### 返回(期望)——仅远程模式
|
||||||
|
|
||||||
|
* HTTP 状态码必须为 `200`
|
||||||
|
* 且响应体中必须包含 `user_id`
|
||||||
|
|
||||||
响应(成功示例):
|
响应(成功示例):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 1,
|
"code": 1,
|
||||||
@@ -314,13 +517,17 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.2 用户资产
|
### 3.4 用户资产(Assets)
|
||||||
* 方法:`GET`
|
* 方法:`GET`
|
||||||
* 路径:`/api/v1/playx/assets`
|
* 路径:`/api/v1/mall/assets`
|
||||||
|
|
||||||
#### 请求参数(鉴权)
|
#### 请求参数(鉴权)
|
||||||
* `session_id`(优先)
|
|
||||||
* `user_id`(兼容)
|
以下任选其一即可(与 **3.1 鉴权解析规则** 一致):
|
||||||
|
|
||||||
|
* `session_id`
|
||||||
|
* `token`(或请求头 `ba-token` / `token`)
|
||||||
|
* `user_id`(纯数字为 `mall_user.id`,否则为 `playx_user_id`)
|
||||||
|
|
||||||
#### 返回(成功 data)
|
#### 返回(成功 data)
|
||||||
若未找到资产:返回 0。
|
若未找到资产:返回 0。
|
||||||
@@ -334,7 +541,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
|||||||
|
|
||||||
#### 示例
|
#### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id=7b1c....'
|
curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'token=上一步temLogin返回的token'
|
||||||
```
|
```
|
||||||
|
|
||||||
响应(示例):
|
响应(示例):
|
||||||
@@ -355,15 +562,16 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.3 领取(Claim)
|
### 3.5 领取(Claim)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/claim`
|
* 路径:`/api/v1/mall/claim`
|
||||||
|
|
||||||
#### 请求 Body
|
#### 请求 Body
|
||||||
必填:
|
必填:
|
||||||
|
|
||||||
* `claim_request_id`:幂等键(string,唯一)
|
* `claim_request_id`:幂等键(string,唯一)
|
||||||
鉴权:
|
|
||||||
* `session_id` 或 `user_id`
|
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||||
|
|
||||||
#### 返回(成功 data)
|
#### 返回(成功 data)
|
||||||
与 `用户资产` 返回字段一致(资产快照)。
|
与 `用户资产` 返回字段一致(资产快照)。
|
||||||
@@ -373,10 +581,10 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
|||||||
|
|
||||||
#### 示例
|
#### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'claim_request_id=claim_001' \
|
--data-urlencode 'claim_request_id=claim_001' \
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'token=上一步temLogin返回的token'
|
||||||
```
|
```
|
||||||
|
|
||||||
响应(首次领取,示例):
|
响应(首次领取,示例):
|
||||||
@@ -413,9 +621,9 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.4 商品列表
|
### 3.6 商品列表
|
||||||
* 方法:`GET`
|
* 方法:`GET`
|
||||||
* 路径:`/api/v1/playx/items`
|
* 路径:`/api/v1/mall/items`
|
||||||
|
|
||||||
#### 请求参数
|
#### 请求参数
|
||||||
* `type`(可选):`BONUS` | `PHYSICAL` | `WITHDRAW`
|
* `type`(可选):`BONUS` | `PHYSICAL` | `WITHDRAW`
|
||||||
@@ -428,7 +636,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
|||||||
#### 示例
|
#### 示例
|
||||||
请求:
|
请求:
|
||||||
```bash
|
```bash
|
||||||
curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDRAW'
|
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=WITHDRAW'
|
||||||
```
|
```
|
||||||
|
|
||||||
响应(示例):
|
响应(示例):
|
||||||
@@ -456,15 +664,14 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.5 红利兑换(Bonus Redeem)
|
### 3.7 红利兑换(Bonus Redeem)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/bonus/redeem`
|
* 路径:`/api/v1/mall/bonusRedeem`
|
||||||
|
|
||||||
#### 请求 Body
|
#### 请求 Body
|
||||||
必填:
|
必填:
|
||||||
* `item_id`:商品 ID(要求 `mall_item.type=BONUS` 且 `status=1`)
|
* `item_id`:商品 ID(要求 `mall_item.type=BONUS` 且 `status=1`)
|
||||||
鉴权:
|
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||||
* `session_id` 或 `user_id`
|
|
||||||
|
|
||||||
#### 返回(成功)
|
#### 返回(成功)
|
||||||
* `msg`:`Redeem submitted, please wait about 10 minutes`
|
* `msg`:`Redeem submitted, please wait about 10 minutes`
|
||||||
@@ -473,7 +680,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR
|
|||||||
|
|
||||||
#### 示例
|
#### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'item_id=123' \
|
--data-urlencode 'item_id=123' \
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
@@ -494,18 +701,15 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.6 实物兑换(Physical Redeem)
|
### 3.8 实物兑换(Physical Redeem)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/physical/redeem`
|
* 路径:`/api/v1/mall/physicalRedeem`
|
||||||
|
|
||||||
#### 请求 Body
|
#### 请求 Body
|
||||||
必填:
|
必填:
|
||||||
* `item_id`:商品 ID(要求 `mall_item.type=PHYSICAL` 且 `status=1`)
|
* `item_id`:商品 ID(要求 `mall_item.type=PHYSICAL` 且 `status=1`)
|
||||||
* `receiver_name`:收货人
|
* `address_id`:收货地址 ID(`mall_address.id`,须属于当前用户资产;下单时写入 `mall_order.mall_address_id`,并将该地址快照写入 `receiver_name` / `receiver_phone` / `receiver_address`)
|
||||||
* `receiver_phone`:收货电话
|
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||||
* `receiver_address`:收货地址
|
|
||||||
鉴权:
|
|
||||||
* `session_id` 或 `user_id`
|
|
||||||
|
|
||||||
#### 返回(成功)
|
#### 返回(成功)
|
||||||
* `msg`:`Redeem success`
|
* `msg`:`Redeem success`
|
||||||
@@ -513,12 +717,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
|||||||
|
|
||||||
#### 示例
|
#### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'item_id=200' \
|
--data-urlencode 'item_id=200' \
|
||||||
--data-urlencode 'receiver_name=张三' \
|
--data-urlencode 'address_id=10' \
|
||||||
--data-urlencode 'receiver_phone=18800001111' \
|
|
||||||
--data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \
|
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -534,15 +736,14 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.7 提现申请(Withdraw Apply)
|
### 3.9 提现申请(Withdraw Apply)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/withdraw/apply`
|
* 路径:`/api/v1/mall/withdrawApply`
|
||||||
|
|
||||||
#### 请求 Body
|
#### 请求 Body
|
||||||
必填:
|
必填:
|
||||||
* `item_id`:商品 ID(要求 `mall_item.type=WITHDRAW` 且 `status=1`)
|
* `item_id`:商品 ID(要求 `mall_item.type=WITHDRAW` 且 `status=1`)
|
||||||
鉴权:
|
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||||
* `session_id` 或 `user_id`
|
|
||||||
|
|
||||||
#### 返回(成功)
|
#### 返回(成功)
|
||||||
* `msg`:`Withdraw submitted, please wait about 10 minutes`
|
* `msg`:`Withdraw submitted, please wait about 10 minutes`
|
||||||
@@ -551,7 +752,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
|||||||
|
|
||||||
#### 示例
|
#### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'item_id=321' \
|
--data-urlencode 'item_id=321' \
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
@@ -572,20 +773,23 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.8 订单列表
|
### 3.10 订单列表
|
||||||
* 方法:`GET`
|
* 方法:`GET`
|
||||||
* 路径:`/api/v1/playx/orders`
|
* 路径:`/api/v1/mall/orders`
|
||||||
|
|
||||||
#### 请求参数
|
#### 请求参数(鉴权)
|
||||||
* `session_id` 或 `user_id`
|
|
||||||
|
同 **3.1**(`session_id` / `token` / `user_id`)。
|
||||||
|
|
||||||
#### 返回(成功 data)
|
#### 返回(成功 data)
|
||||||
|
|
||||||
* `list`:订单列表(最多 100 条),并包含关联的 `mallItem`(关系对象)
|
* `list`:订单列表(最多 100 条),并包含关联的 `mallItem`(关系对象)
|
||||||
|
* 列表项中的 `user_id` 为 **PlayX 侧 `playx_user_id`**(字符串),与 `mall_playx_order.user_id` 一致
|
||||||
|
|
||||||
#### 示例
|
#### 示例
|
||||||
请求:
|
请求:
|
||||||
```bash
|
```bash
|
||||||
curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id=7b1c....'
|
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'token=上一步temLogin返回的token'
|
||||||
```
|
```
|
||||||
|
|
||||||
响应(示例,简化):
|
响应(示例,简化):
|
||||||
@@ -619,6 +823,6 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.9 同步额度(可选)
|
### 3.11 同步额度(可选)
|
||||||
当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。
|
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||||
|
|
||||||
|
|||||||
447
docs/PlayX-调用积分商城接口说明.md
Normal file
447
docs/PlayX-调用积分商城接口说明.md
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
# PlayX 调用积分商城接口说明
|
||||||
|
|
||||||
|
本文档描述 **PlayX 平台(或 PlayX 侧脚本/服务)如何调用积分商城已开放的 HTTP 接口**:基础约定、推荐流程、鉴权方式、请求参数与返回结构。
|
||||||
|
|
||||||
|
实现依据:`config/route.php`、`app/api/controller/v1/Playx.php`、`config/playx.php`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 基础约定
|
||||||
|
|
||||||
|
### 1.1 Base URL
|
||||||
|
|
||||||
|
将下列路径拼在积分商城对外域名之后,例如:
|
||||||
|
|
||||||
|
`https://{商城域名}/api/v1/mall/dailyPush`
|
||||||
|
|
||||||
|
(联调时请向商城方索取正式环境与测试环境地址。)
|
||||||
|
|
||||||
|
### 1.2 通用响应结构(JSON)
|
||||||
|
|
||||||
|
所有接口成功或失败,响应体均为:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `code` | int | `1` 表示业务成功;`0` 表示业务失败 |
|
||||||
|
| `msg` | string | 提示信息(失败时为错误原因) |
|
||||||
|
| `time` | int | Unix 时间戳(秒) |
|
||||||
|
| `data` | object/array/null | 业务数据;失败时可能为 `null` |
|
||||||
|
|
||||||
|
部分错误场景会通过 HTTP 状态码区分(如 **401**),此时 `code` 仍为 `0`,请同时判断 HTTP 状态与 `code`。
|
||||||
|
|
||||||
|
**成功示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "",
|
||||||
|
"time": 1730000000,
|
||||||
|
"data": { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "错误原因",
|
||||||
|
"time": 1730000000,
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Content-Type
|
||||||
|
|
||||||
|
- 本文档中 **POST** 且带 JSON Body 的接口,请使用:`Content-Type: application/json`。
|
||||||
|
|
||||||
|
### 1.4 多语言(响应文案)
|
||||||
|
|
||||||
|
可通过请求头 `lang` 控制返回文案语言:
|
||||||
|
|
||||||
|
| Header | 值 | 说明 |
|
||||||
|
|--------|----|------|
|
||||||
|
| `lang` | `zh` / `zh-cn` | 返回中文(默认) |
|
||||||
|
| `lang` | `en` | 返回英文 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 使用流程(推荐)
|
||||||
|
|
||||||
|
### 2.1 PlayX 服务端 → 商城:每日数据推送(主流程)
|
||||||
|
|
||||||
|
适用于 T+1 等业务数据由 **PlayX 服务端**主动推送到积分商城。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant PX as PlayX 服务端
|
||||||
|
participant M as 积分商城
|
||||||
|
|
||||||
|
Note over PX,M: 按约定配置 HMAC 密钥
|
||||||
|
PX->>M: POST /api/v1/mall/dailyPush(JSON Body)
|
||||||
|
M-->>PX: code=1, data.accepted / deduped
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 与商城方约定 **商城 Base URL** 与 **HMAC**(`X-Signature` 等)密钥。
|
||||||
|
2. 按 **§3** 构造请求并推送。
|
||||||
|
3. 根据返回 `data.deduped` 判断是否为幂等重复推送。
|
||||||
|
|
||||||
|
### 2.2 用户侧(H5 / 内嵌页)→ 商城:会话与业务接口
|
||||||
|
|
||||||
|
以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用,通过 **`session_id`**(先调 `verifyToken` 获取)或 **`token`**(商城 `muser` 类 token)标识用户,**不一定由 PlayX 后端直接调用**:
|
||||||
|
|
||||||
|
- `POST /api/v1/mall/verifyToken`:用 PlayX token 换商城 `session_id`
|
||||||
|
- `GET /api/v1/mall/assets`:查询资产
|
||||||
|
- `POST /api/v1/mall/claim`:领取积分
|
||||||
|
- `GET /api/v1/mall/items`:商品列表
|
||||||
|
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply`:兑换与提现申请
|
||||||
|
- `GET /api/v1/mall/orders`:订单列表
|
||||||
|
|
||||||
|
若 PlayX 后端需要代替用户调用上述接口,须同样携带有效的 `session_id` 或 `token`,并遵守同一用户身份规则(见 **§4 身份说明**)。
|
||||||
|
|
||||||
|
### 2.3 代理鉴权(非 PlayX 通用)
|
||||||
|
|
||||||
|
`GET /api/v1/authToken` 为 **渠道/代理**签名换 JWT(`authtoken`),与 PlayX 用户体系不同,一般不在本文「PlayX 平台对接」主流程中展开;需要时由运营向商城索取单独说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. PlayX 服务端推送:Daily Push
|
||||||
|
|
||||||
|
### 3.1 概要
|
||||||
|
|
||||||
|
| 项目 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 方法 | `POST` |
|
||||||
|
| 路径 | `/api/v1/mall/dailyPush` |
|
||||||
|
|
||||||
|
### 3.2 鉴权(按商城部署配置,可组合)
|
||||||
|
|
||||||
|
#### 推荐方案:仅启用 HMAC(当前对接采用)
|
||||||
|
|
||||||
|
商城侧配置:设置环境变量 **`PLAYX_DAILY_PUSH_SECRET`** 为非空(启用 HMAC 校验)。
|
||||||
|
|
||||||
|
#### HMAC 签名(必填)
|
||||||
|
|
||||||
|
当商城配置 **`PLAYX_DAILY_PUSH_SECRET`** 非空时,需同时携带:
|
||||||
|
|
||||||
|
| Header | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| `X-Request-Id` | 请求 ID(建议与 Body 内可追溯字段一致) |
|
||||||
|
| `X-Timestamp` | Unix 时间戳(秒,字符串) |
|
||||||
|
| `X-Signature` | 签名(十六进制小写或大写需与实现一致,以下为十六进制字符串) |
|
||||||
|
|
||||||
|
签名原文与计算:
|
||||||
|
|
||||||
|
```
|
||||||
|
canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)
|
||||||
|
expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET )
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `json_body` 为 **实际发送的 JSON 原始字符串** 计算出的 SHA256(十六进制);与 PHP `hash('sha256', $rawBody)` 一致。
|
||||||
|
|
||||||
|
校验:`hash_equals(expected, X-Signature)`。
|
||||||
|
|
||||||
|
#### Header 填写清单(HMAC 模式)
|
||||||
|
- 必填:`X-Request-Id`、`X-Timestamp`、`X-Signature`
|
||||||
|
|
||||||
|
#### 重要注意:`json_body` 必须与实际发送一致
|
||||||
|
为了保证签名可验通过:用于计算 sha256 的 `json_body` 必须是**实际发送到 HTTP body 的原始 JSON 字符串**(字节级一致)。\
|
||||||
|
建议:在发送端先序列化 JSON 得到字符串 `rawBody`,用该 `rawBody` 做 sha256 与 HMAC,再把同一个 `rawBody` 作为请求 body 发送。
|
||||||
|
|
||||||
|
### 3.3 Body 参数(JSON)
|
||||||
|
|
||||||
|
`/api/v1/mall/dailyPush` 支持 **两种入参格式**(按字段自动识别):
|
||||||
|
|
||||||
|
#### 格式 A:旧版单条上报(兼容)
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `request_id` | string | 是 | 本次推送请求号;响应中原样返回 |
|
||||||
|
| `date` | string | 是 | 业务日期,格式 `YYYY-MM-DD` |
|
||||||
|
| `user_id` | string | 是 | PlayX 用户 ID(幂等键之一) |
|
||||||
|
| `username` | string | 否 | 展示名;用于同步/创建商城侧用户资产展示信息 |
|
||||||
|
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢;**小于 0** 时按配置比例计入待领取保障金(`locked_points`) |
|
||||||
|
| `yesterday_total_deposit` | number | 否 | 昨日总充值;用于计算当日可领取上限等 |
|
||||||
|
| `lifetime_total_deposit` | number | 否 | 历史总充值(冗余入库) |
|
||||||
|
| `lifetime_total_withdraw` | number | 否 | 历史总提现(冗余入库) |
|
||||||
|
|
||||||
|
#### 格式 B:新版批量上报(你图中格式)
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `report_date` | string/number | 是 | 报表日期;可以为 Unix 秒时间戳(如 `1700000000`)或 `YYYY-MM-DD` |
|
||||||
|
| `member` | array | 是 | 成员列表,每个成员包含一名 PlayX 用户数据 |
|
||||||
|
|
||||||
|
成员元素字段:
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `member_id` | string | 是 | PlayX 用户 ID(幂等键之一) |
|
||||||
|
| `login` | string | 否 | 用户展示名 |
|
||||||
|
| `yesterday_total_w` | number | 否 | 昨日净输赢;小于 0 才会累加到 `locked_points` |
|
||||||
|
| `yesterday_total_deposit` | number | 否 | 昨日总充值;用于计算 `today_limit` |
|
||||||
|
| `lty_deposit` | number | 否 | 历史总充值(冗余入库) |
|
||||||
|
| `lty_withdrawal` | number | 否 | 历史总提现(冗余入库) |
|
||||||
|
|
||||||
|
#### Body 填写要求(批量模式)
|
||||||
|
- **必须有**:`report_date`、`member`(数组且至少 1 个元素)、`member[].member_id`。
|
||||||
|
- **允许缺省**:成员的 `login/yesterday_total_w/yesterday_total_deposit/lty_deposit/lty_withdrawal`;缺省时按 `0` 或空字符串处理。
|
||||||
|
- **日期**:`report_date` 传 Unix 秒会自动转换成 `YYYY-MM-DD`;如果直接传 `YYYY-MM-DD` 也支持。
|
||||||
|
|
||||||
|
### 3.4 幂等
|
||||||
|
|
||||||
|
- 幂等键:**`user_id` + `date`**
|
||||||
|
- 重复推送:不重复入账,返回 `data.deduped = true`
|
||||||
|
|
||||||
|
### 3.5 返回 `data` 字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `request_id` | string | 与请求一致 |
|
||||||
|
| `accepted` | bool | 是否受理成功 |
|
||||||
|
| `deduped` | bool | 是否为重复推送(幂等命中) |
|
||||||
|
| `message` | string | 说明文案 |
|
||||||
|
|
||||||
|
#### 格式 B:批量上报的返回补充
|
||||||
|
批量模式会在 `data` 中增加:`results`。
|
||||||
|
|
||||||
|
`data.results` 为数组,元素字段如下:
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `user_id` | string | 对应成员的 `member_id` |
|
||||||
|
| `accepted` | bool | 是否受理成功 |
|
||||||
|
| `deduped` | bool | 该成员是否为重复推送 |
|
||||||
|
| `message` | string | `ok` 或 `duplicate input` |
|
||||||
|
|
||||||
|
**HTTP 401**:HMAC 不通过(签名缺失/不完整/校验失败)。
|
||||||
|
|
||||||
|
### 3.6 请求示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'X-Request-Id: req_1700000000_123456' \
|
||||||
|
-H 'X-Timestamp: 1700000000' \
|
||||||
|
-H 'X-Signature: <按本文档 canonical 计算出的 HMAC_SHA256>' \
|
||||||
|
-d '{
|
||||||
|
"report_date": "1700000000",
|
||||||
|
"member": [
|
||||||
|
{
|
||||||
|
"member_id": "123456",
|
||||||
|
"login": "john",
|
||||||
|
"lty_deposit": 15230.75,
|
||||||
|
"lty_withdrawal": 12400.50,
|
||||||
|
"yesterday_total_w": -320.25,
|
||||||
|
"yesterday_total_deposit": 500.00
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
响应示例(首次写入,至少有一个成员非重复):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "",
|
||||||
|
"time": 0,
|
||||||
|
"data": {
|
||||||
|
"request_id": "report_2023-11-14",
|
||||||
|
"accepted": true,
|
||||||
|
"deduped": false,
|
||||||
|
"message": "Ok",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"user_id": "123456",
|
||||||
|
"accepted": true,
|
||||||
|
"deduped": false,
|
||||||
|
"message": "Ok"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 身份说明(`session_id` / `token` / `user_id`)
|
||||||
|
|
||||||
|
以下接口通过 **`resolvePlayxAssetIdFromRequest`** 解析当前用户,优先级如下:
|
||||||
|
|
||||||
|
1. **`session_id`**(POST/GET):对应商城表 `mall_playx_session`,未过期则映射到 `mall_playx_user_asset`。
|
||||||
|
2. 若 `session_id` 实际是 **`muser` 类型 token**(历史兼容),也会按 token 解析。
|
||||||
|
3. **`token`**(POST/GET 或标准鉴权头):商城 token 表内类型为会员或 **`muser`** 且未过期时,`user_id` 为 **`mall_playx_user_asset.id`**(资产表主键)。
|
||||||
|
4. **`user_id`**(POST/GET):
|
||||||
|
- 若**纯数字**:视为 **`mall_playx_user_asset.id`**;
|
||||||
|
- 否则:按 **`playx_user_id`** 查找资产行。
|
||||||
|
|
||||||
|
无法解析身份时,通常返回 **401** 或参数错误提示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 其他接口一览(摘要)
|
||||||
|
|
||||||
|
> 下列均为 **BuildAdmin 通用 `code/msg/time/data` 结构**;成功时 `code=1`。
|
||||||
|
|
||||||
|
### 5.1 `POST /api/v1/mall/verifyToken`
|
||||||
|
|
||||||
|
用于将 **PlayX token**(或本地联调 token)换 **商城 `session_id`**。
|
||||||
|
|
||||||
|
| 参数位置 | 名称 | 说明 |
|
||||||
|
|----------|------|------|
|
||||||
|
| POST/GET | `token` 或 `session` | PlayX 或商城 token |
|
||||||
|
|
||||||
|
**说明:** 若 `playx.verify_token_local_only=true`(默认),商城**仅本地校验** token,不请求 PlayX 远程接口;远程模式需配置 `PLAYX_API_BASE_URL` 等。
|
||||||
|
|
||||||
|
**成功 `data` 示例:**
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `session_id` | 后续接口可带此字段 |
|
||||||
|
| `user_id` | PlayX 用户 ID 或映射后的标识 |
|
||||||
|
| `username` | 用户名 |
|
||||||
|
| `token_expire_at` | ISO8601 过期时间 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 `GET /api/v1/mall/assets`
|
||||||
|
|
||||||
|
查询积分资产;需 **§4** 身份。
|
||||||
|
|
||||||
|
**成功 `data`:**
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `locked_points` | 待领取积分 |
|
||||||
|
| `available_points` | 可用积分 |
|
||||||
|
| `today_limit` | 今日可领取上限 |
|
||||||
|
| `today_claimed` | 今日已领取 |
|
||||||
|
| `withdrawable_cash` | 可提现现金(由积分×配置比例换算,保留小数) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 `POST /api/v1/mall/claim`
|
||||||
|
|
||||||
|
领取积分;需 **§4** 身份。
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `claim_request_id` | 是 | 幂等请求号 |
|
||||||
|
|
||||||
|
**成功 `data`:** 与资产结构一致(含 `locked_points`、`available_points`、`today_limit`、`today_claimed`、`withdrawable_cash` 等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 `GET /api/v1/mall/items`
|
||||||
|
|
||||||
|
商品列表。
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `type` | 否 | `BONUS` / `PHYSICAL` / `WITHDRAW`,筛选类型 |
|
||||||
|
|
||||||
|
**成功 `data`:** `{ "list": [ ... ] }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 `POST /api/v1/mall/bonusRedeem`
|
||||||
|
|
||||||
|
红利兑换;需 **§4** 身份。
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `item_id` | 是 | 商品 ID |
|
||||||
|
|
||||||
|
**成功 `data`:** 含 `order_id`、`status`(如 `PENDING`)等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 `POST /api/v1/mall/physicalRedeem`
|
||||||
|
|
||||||
|
实物兑换;需 **§4** 身份。
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `item_id` | 是 | 实物商品 ID |
|
||||||
|
| `address_id` | 是 | `mall_address.id`(当前用户下地址);订单保存 `mall_address_id` 与地址快照 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.7 `POST /api/v1/mall/withdrawApply`
|
||||||
|
|
||||||
|
提现类兑换申请;需 **§4** 身份。
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `item_id` | 是 | 提现档位商品 ID |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.8 `GET /api/v1/mall/orders`
|
||||||
|
|
||||||
|
订单列表;需 **§4** 身份。
|
||||||
|
|
||||||
|
**成功 `data`:** `{ "list": [ ... ] }`(含关联商品等,以实际返回为准)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.9 收货地址(`mall_address`)
|
||||||
|
|
||||||
|
> 下列接口均需携带 **§4 身份参数**(`session_id` / `token` / `user_id` 之一)。
|
||||||
|
|
||||||
|
#### 5.9.1 获取收货地址列表
|
||||||
|
- **方法**:`GET`
|
||||||
|
- **路径**:`/api/v1/mall/addressList`
|
||||||
|
|
||||||
|
返回 `data.list`:地址数组(按 `default_setting` 优先,其次 id 倒序)。
|
||||||
|
|
||||||
|
#### 5.9.2 添加收货地址
|
||||||
|
- **方法**:`POST`
|
||||||
|
- **路径**:`/api/v1/mall/addressAdd`
|
||||||
|
|
||||||
|
Body(表单或 JSON 均可,建议 JSON):
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `receiver_name` | 是 | 收货人 |
|
||||||
|
| `phone` | 是 | 联系电话 |
|
||||||
|
| `region` | 是 | 地区(可传数组或逗号分隔字符串) |
|
||||||
|
| `detail_address` | 是 | 详细地址(短文本) |
|
||||||
|
| `default_setting` | 否 | `1` 设为默认地址;`0` 或不传为非默认 |
|
||||||
|
|
||||||
|
成功返回:`data.id` 为新地址 id。
|
||||||
|
|
||||||
|
#### 5.9.3 修改收货地址(含设置默认)
|
||||||
|
- **方法**:`POST`
|
||||||
|
- **路径**:`/api/v1/mall/addressEdit`
|
||||||
|
|
||||||
|
Body:
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | 是 | 地址 id |
|
||||||
|
| `receiver_name/phone/region/detail_address/default_setting` | 否 | 需要修改的字段(只更新传入项) |
|
||||||
|
|
||||||
|
当 `default_setting=1`:会自动把该用户其他地址的 `default_setting` 置为 0。
|
||||||
|
|
||||||
|
#### 5.9.4 删除收货地址
|
||||||
|
- **方法**:`POST`
|
||||||
|
- **路径**:`/api/v1/mall/addressDelete`
|
||||||
|
|
||||||
|
Body:
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | 是 | 地址 id |
|
||||||
|
|
||||||
|
若删除的是默认地址:服务端会将剩余地址里 id 最大的一条自动设为默认(若存在)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 配置项(供运维/对方技术对照)
|
||||||
|
|
||||||
|
| 环境变量 / 配置 | 作用 |
|
||||||
|
|-----------------|------|
|
||||||
|
| `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 |
|
||||||
|
| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 PlayX 远程 |
|
||||||
|
| `PLAYX_API_BASE_URL` | 商城调用 PlayX 接口时使用(与「PlayX 调商城」方向相反) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 版本与变更
|
||||||
|
|
||||||
|
- 文档与仓库代码同步维护;接口路径以 `config/route.php` 为准。
|
||||||
|
- 若后续升级鉴权策略(例如叠加 JWT),以部署环境变量与最新文档为准。
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
接收 PlayX 每日 T+1 数据推送。
|
接收 PlayX 每日 T+1 数据推送。
|
||||||
|
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/daily-push`
|
* 路径:`/api/v1/mall/dailyPush`
|
||||||
|
|
||||||
##### 请求(Header)
|
##### 请求(Header)
|
||||||
当配置了 `playx.daily_push_secret`(Daily Push 签名校验)时,需要携带:
|
当配置了 `playx.daily_push_secret`(Daily Push 签名校验)时,需要携带:
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
* `X-Signature`:签名(HMAC_SHA256)
|
* `X-Signature`:签名(HMAC_SHA256)
|
||||||
|
|
||||||
签名计算逻辑(服务端):
|
签名计算逻辑(服务端):
|
||||||
* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/playx/daily-push\n{sha256(json_body)}`
|
* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/mall/dailyPush\n{sha256(json_body)}`
|
||||||
* expected:`hash_hmac('sha256', canonical, daily_push_secret)`
|
* expected:`hash_hmac('sha256', canonical, daily_push_secret)`
|
||||||
|
|
||||||
##### 请求(Body)
|
##### 请求(Body)
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
##### 示例
|
##### 示例
|
||||||
无签名校验(`PLAYX_DAILY_PUSH_SECRET` 为空):
|
无签名校验(`PLAYX_DAILY_PUSH_SECRET` 为空):
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{
|
-d '{
|
||||||
"request_id":"req_1001",
|
"request_id":"req_1001",
|
||||||
@@ -246,7 +246,7 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \
|
|||||||
### 1.3 商城内部 API(供 H5 前端调用)
|
### 1.3 商城内部 API(供 H5 前端调用)
|
||||||
#### Token 验证
|
#### Token 验证
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/verify-token`
|
* 路径:`/api/v1/mall/verifyToken`
|
||||||
|
|
||||||
请求(Body):
|
请求(Body):
|
||||||
* `token`(必填,优先读取)
|
* `token`(必填,优先读取)
|
||||||
@@ -260,14 +260,14 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \
|
|||||||
|
|
||||||
示例:
|
示例:
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'token=PLAYX_TOKEN_XXX'
|
--data-urlencode 'token=PLAYX_TOKEN_XXX'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 用户资产
|
#### 用户资产
|
||||||
* 方法:`GET`
|
* 方法:`GET`
|
||||||
* 路径:`/api/v1/playx/assets`
|
* 路径:`/api/v1/mall/assets`
|
||||||
|
|
||||||
请求参数(二选一):
|
请求参数(二选一):
|
||||||
* `session_id`(优先):从 `mall_playx_session` 查 user_id(并校验过期)
|
* `session_id`(优先):从 `mall_playx_session` 查 user_id(并校验过期)
|
||||||
@@ -282,7 +282,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
|||||||
|
|
||||||
##### 示例
|
##### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id=7b1c....'
|
curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'session_id=7b1c....'
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -301,7 +301,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
|||||||
|
|
||||||
#### 领取(Claim)
|
#### 领取(Claim)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/claim`
|
* 路径:`/api/v1/mall/claim`
|
||||||
|
|
||||||
请求:
|
请求:
|
||||||
* `claim_request_id`:幂等键(string,必填且唯一)
|
* `claim_request_id`:幂等键(string,必填且唯一)
|
||||||
@@ -312,7 +312,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
|||||||
##### 示例
|
##### 示例
|
||||||
(首次领取成功,可能返回 `msg=Claim success`;若幂等重复,`msg` 可能为空)
|
(首次领取成功,可能返回 `msg=Claim success`;若幂等重复,`msg` 可能为空)
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'claim_request_id=claim_001' \
|
--data-urlencode 'claim_request_id=claim_001' \
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
@@ -334,7 +334,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
|||||||
|
|
||||||
#### 商品列表
|
#### 商品列表
|
||||||
* 方法:`GET`
|
* 方法:`GET`
|
||||||
* 路径:`/api/v1/playx/items`
|
* 路径:`/api/v1/mall/items`
|
||||||
|
|
||||||
请求(可选):
|
请求(可选):
|
||||||
* `type=BONUS|PHYSICAL|WITHDRAW`
|
* `type=BONUS|PHYSICAL|WITHDRAW`
|
||||||
@@ -344,7 +344,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
|||||||
|
|
||||||
##### 示例
|
##### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=BONUS'
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -370,7 +370,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
|||||||
|
|
||||||
#### 红利兑换(Bonus Redeem)
|
#### 红利兑换(Bonus Redeem)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/bonus/redeem`
|
* 路径:`/api/v1/mall/bonusRedeem`
|
||||||
|
|
||||||
请求:
|
请求:
|
||||||
* `item_id`:商品 ID(BONUS)
|
* `item_id`:商品 ID(BONUS)
|
||||||
@@ -382,7 +382,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
|||||||
|
|
||||||
##### 示例
|
##### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'item_id=123' \
|
--data-urlencode 'item_id=123' \
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
@@ -401,13 +401,11 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
|||||||
|
|
||||||
#### 实物兑换(Physical Redeem)
|
#### 实物兑换(Physical Redeem)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/physical/redeem`
|
* 路径:`/api/v1/mall/physicalRedeem`
|
||||||
|
|
||||||
请求:
|
请求:
|
||||||
* `item_id`:商品 ID(PHYSICAL)
|
* `item_id`:商品 ID(PHYSICAL)
|
||||||
* `receiver_name`
|
* `address_id`:`mall_address.id`(当前用户资产下地址;订单写入 `mall_address_id` 与收货快照)
|
||||||
* `receiver_phone`
|
|
||||||
* `receiver_address`
|
|
||||||
* 鉴权:`session_id` 或 `user_id`
|
* 鉴权:`session_id` 或 `user_id`
|
||||||
|
|
||||||
成功返回:
|
成功返回:
|
||||||
@@ -416,12 +414,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
|||||||
|
|
||||||
##### 示例
|
##### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'item_id=200' \
|
--data-urlencode 'item_id=200' \
|
||||||
--data-urlencode 'receiver_name=张三' \
|
--data-urlencode 'address_id=10' \
|
||||||
--data-urlencode 'receiver_phone=18800001111' \
|
|
||||||
--data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \
|
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -435,7 +431,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
|||||||
|
|
||||||
#### 提现申请(Withdraw Apply)
|
#### 提现申请(Withdraw Apply)
|
||||||
* 方法:`POST`
|
* 方法:`POST`
|
||||||
* 路径:`/api/v1/playx/withdraw/apply`
|
* 路径:`/api/v1/mall/withdrawApply`
|
||||||
|
|
||||||
请求:
|
请求:
|
||||||
* `item_id`:商品 ID(WITHDRAW)
|
* `item_id`:商品 ID(WITHDRAW)
|
||||||
@@ -447,7 +443,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
|||||||
|
|
||||||
##### 示例
|
##### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
|
||||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'item_id=321' \
|
--data-urlencode 'item_id=321' \
|
||||||
--data-urlencode 'session_id=7b1c....'
|
--data-urlencode 'session_id=7b1c....'
|
||||||
@@ -466,7 +462,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
|||||||
|
|
||||||
#### 订单列表
|
#### 订单列表
|
||||||
* 方法:`GET`
|
* 方法:`GET`
|
||||||
* 路径:`/api/v1/playx/orders`
|
* 路径:`/api/v1/mall/orders`
|
||||||
|
|
||||||
请求:
|
请求:
|
||||||
* `session_id` 或 `user_id`
|
* `session_id` 或 `user_id`
|
||||||
@@ -476,7 +472,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
|||||||
|
|
||||||
##### 示例
|
##### 示例
|
||||||
```bash
|
```bash
|
||||||
curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id=7b1c....'
|
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=7b1c....'
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -502,7 +498,7 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 同步额度
|
#### 同步额度
|
||||||
当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。
|
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||||
如需补齐,请在接口设计阶段新增对应实现与 PlayX API 对接。
|
如需补齐,请在接口设计阶段新增对应实现与 PlayX API 对接。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -530,7 +526,7 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
|||||||
- 发货:录入物流公司、单号 → `SHIPPED`
|
- 发货:录入物流公司、单号 → `SHIPPED`
|
||||||
- 驳回:录入驳回原因 → `REJECTED`,自动退回积分
|
- 驳回:录入驳回原因 → `REJECTED`,自动退回积分
|
||||||
- **红利/提现订单**:
|
- **红利/提现订单**:
|
||||||
- 展示 `external_transaction_id`、`playx_transaction_id`、发放子状态
|
- 展示 `external_transaction_id`、`playx_transaction_id`、推送playx
|
||||||
- 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因
|
- 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因
|
||||||
|
|
||||||
### 2.3 用户资产与人工调账
|
### 2.3 用户资产与人工调账
|
||||||
@@ -607,29 +603,30 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
|||||||
|
|
||||||
**表名**:`mall_playx_order`(或统一改造 mall_pints_order / mall_redemption_order)
|
**表名**:`mall_playx_order`(或统一改造 mall_pints_order / mall_redemption_order)
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|----------------------------------------------------------------------|
|
||||||
| id | int | 主键 |
|
| id | int | 主键 |
|
||||||
| user_id | varchar(64) | 用户 ID |
|
| user_id | varchar(64) | 用户 ID |
|
||||||
| type | enum | BONUS / PHYSICAL / WITHDRAW |
|
| type | enum | BONUS / PHYSICAL / WITHDRAW |
|
||||||
| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED |
|
| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED |
|
||||||
| mall_item_id | int | 商品 ID |
|
| mall_item_id | int | 商品 ID |
|
||||||
| points_cost | int | 消耗积分 |
|
| points_cost | int | 消耗积分 |
|
||||||
| amount | decimal(15,2) | 现金面值(红利/提现) |
|
| amount | decimal(15,2) | 现金面值(红利/提现) |
|
||||||
| multiplier | int | 流水倍数 |
|
| multiplier | int | 流水倍数 |
|
||||||
| external_transaction_id | varchar(64) | 外部交易幂等键 |
|
| external_transaction_id | varchar(64) | 订单号 |
|
||||||
| playx_transaction_id | varchar(64) | PlayX 流水号 |
|
| playx_transaction_id | varchar(64) | PlayX 流水号 |
|
||||||
| grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL |
|
| grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL |
|
||||||
| fail_reason | text | 失败原因 |
|
| fail_reason | text | 失败原因 |
|
||||||
| retry_count | int | 重试次数 |
|
| retry_count | int | 重试次数 |
|
||||||
| reject_reason | varchar(255) | 驳回原因(实物) |
|
| reject_reason | varchar(255) | 驳回原因(实物) |
|
||||||
| shipping_company | varchar(50) | 物流公司 |
|
| shipping_company | varchar(50) | 物流公司 |
|
||||||
| shipping_no | varchar(64) | 物流单号 |
|
| shipping_no | varchar(64) | 物流单号 |
|
||||||
| receiver_name | varchar(50) | 收货人 |
|
| mall_address_id | int unsigned, NULL | 实物兑换所选 `mall_address.id`(快照仍见 `receiver_*`) |
|
||||||
| receiver_phone | varchar(20) | 收货电话 |
|
| receiver_name | varchar(50) | 收货人 |
|
||||||
| receiver_address | text | 收货地址 |
|
| receiver_phone | varchar(20) | 收货电话 |
|
||||||
| create_time | bigint | 创建时间 |
|
| receiver_address | text | 收货地址 |
|
||||||
| update_time | bigint | 更新时间 |
|
| create_time | bigint | 创建时间 |
|
||||||
|
| update_time | bigint | 更新时间 |
|
||||||
|
|
||||||
**索引**:`user_id`、`external_transaction_id`、`type`、`status`
|
**索引**:`user_id`、`external_transaction_id`、`type`、`status`
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ class Auth
|
|||||||
}
|
}
|
||||||
} elseif (in_array($rule, $name)) {
|
} elseif (in_array($rule, $name)) {
|
||||||
$list[] = $rule;
|
$list[] = $rule;
|
||||||
|
} else {
|
||||||
|
// 仅勾选父级菜单(如 auth/admin)时,允许访问子路径(auth/admin/index、add 等)
|
||||||
|
$baseRule = preg_replace('/\?.*$/U', '', $rule);
|
||||||
|
foreach ((array) $name as $n) {
|
||||||
|
if ($baseRule !== '' && str_starts_with((string) $n, $baseRule . '/')) {
|
||||||
|
$list[] = $rule;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($relation === 'or' && !empty($list)) {
|
if ($relation === 'or' && !empty($list)) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1,102 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve"> <image id="image0" width="200" height="200" x="0" y="0"
|
|
||||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
|
|
||||||
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAC+lBMVEX///9No/1Dnv1Dnv1D
|
|
||||||
nv1Dnv1Dnv1Dnv1Dnv1Dnv1Dnv1Dnv1Dnv1Dnv1Dnv1Fn/1En/1GoP1Dnv1Dnv1Dnv1Dnv1Dnv1D
|
|
||||||
nv1Dnv1Dnv1Dnv1Dnv1Dnv1Jof1Dnv1Dnv1Zqf1Dnv1cq/1OpP1Qpf1Tp/1Mo/1PpP1En/2antB2
|
|
||||||
w/9+vfeAvvVxvv6FltWas+F41f+Dyv+FzP9zwP5PpP1Vp/1Vp/1erP1erP1ss/1Fn/1Vp/3TlW9i
|
|
||||||
tP6/mLFfsv5Lov1psv1Mo/2Gzf+ck8Nruv5Jof1Spv28nrpJov1jr/1ttP1Tpv3Clpywj7Vbqv1M
|
|
||||||
o/1nmud7xf99x/9Rpf1Iof22ka9br/68k69Kov1OpP1VqP3FpoZ2ldhUp/2VkM5rs/3EmZZouP5K
|
|
||||||
ov1Qpf11leJMnfe3psVOpP1nsf1Vp/3AkaSIzv9bq/xRpf1Up/1bmu+Cyv+qkbpNpP3Gn4+TksRW
|
|
||||||
qP1HoP1GoP1dsf5Tpv1/yP+7kqtNpP3EqYJUp/2lk7xMo/1Spv1WqP1Uqv1WqP1jvP7FjqXifIWn
|
|
||||||
msOQmM3neH/LiZ6BpuLWgZFwqe7ag5DThpatl72UotS8hKJSpv05mf0/nP114f+Fzv+I0P+B1v+K
|
|
||||||
z/89m/1Anf1Dnv2Z5XaF0PyLz/+Uxe1+2f9Cnv07mv3BwVCd52d43v8+m/2v4zqq12aHzfnBsnqp
|
|
||||||
7jSurtKMzf+G1P86mv2h6Vm12jymtty2w2212kCGzvSj412s2l+s2GK9uXSh61Zz5P9Cnf2m3l6r
|
|
||||||
3lqu11+otNiwzmeo41at2F+Zv+p83f+k6FWz2kSKz+OCzf+v2FGLz96Pyvaz2kmw2U6N0Naos9qQ
|
|
||||||
xvLFrnWy2U+Q0c2fu+SS0cGw2VWV0bmV0ras2GGX0q+w2ViY0qr+YVz/XVb5Z2b/V03Nk6iawOH8
|
|
||||||
ZGL0bm37Z2WG6bT1bGzXiZmd8lv/XWL4amm/u2r/XWn/WlHFtWL/V277Y27rc3jDnLar3F/LrGSZ
|
|
||||||
+VTya23////Mjm1rAAAAlnRSTlMABAsRHCczOkFGST43LyQXDwhTX2ZvdWlkW08sVQJiIBJYaty+
|
|
||||||
zuHWd7zDwsW0l+Xl5uy6i7axf28x/cD4meOV8kKE8amp9NjwfGEjmdy7lO+FzdOQ98SP2X/npvaL
|
|
||||||
yaE24qPq4ox4/odNotP5jd7Gf+K26uyeqnr7k9TZzoH3nbHtlLqGrqPe+smh/t+r753z6c68y0O0
|
|
||||||
Xjp1AAAAAWJLR0QAiAUdSAAAAAd0SU1FB+YEBBcDHr5guP0AAA+vSURBVHja7V15fFTVFQ7ZzL6R
|
|
||||||
vABJALUI2koVWUOriIo7SgVqQFErm4hgRQOlYBBLpdpFu8wgcTIVzVhSsNKgUkRJQYjWarW2tNTW
|
|
||||||
2oRYhqaaGitd+KPzZubet913z/dme6G/fD9//hTecr+593z3nHvOuzctrR/96Ec/+hHGgPSMzKzs
|
|
||||||
U3Jy8/LycvMLCouKS0rdbpNTlBQV5pcNLK9QjKgcNHhIQVXmyUEnPaugulyRomJgXnZmjdsNlaG0
|
|
||||||
KH9ghYKhsjo70+32ilFSWIaSYCjPLeprHZNeWO2QBOuY3CK3265D0RCnfWHol/xitwmEUVowNA4W
|
|
||||||
EVRnuc0iLSMnns7QMDTbVWvJyE0IizAqCwa4RaMkgTTCVLJdoTEgPzGDyjDAXLCVqsqE01BRnWIF
|
|
||||||
K45x2gCQk0qrPyVpNEIoH5ay7hicTB4h5KamUwqSTCOEQSnwJ0vLks8jhIJk88hMjlhZUZbc6bEw
|
|
||||||
RTRCKE+mEOekjkcISZsda1JjHhqSZCglA1PMI6TDyeAxPPGuFY2yxPModoOHopyacB6u0Ahh8P8J
|
|
||||||
jwQzyXCPR0JHV7o79sGQOIuPf5kkPuQniEfygigUhU6ae9rpnxJjxOkMZ4wcxZ995lmfFuAzZ6Nt
|
|
||||||
G/3Zc84VYcx57Iqx48ZzTJgoxmkCIpNqPV4Knsm8IZ/b9OhmET4PEjm/cfNjQlzArpgCtGe8qEsu
|
|
||||||
9DX5CXibpnIiP3z80S2CdlyE8bj4iSe3CHlcwq4Y52km4A9ME/v6kz0UEb/nUj40nvrRo0Im2OC6
|
|
||||||
bKuYxxh2weW1LVRjmgNXiK3kyiYvdW+T7yr2pqt/vG27kMnlAI/p2xuJgTWC/lUDI+3s/Rrg5mvZ
|
|
||||||
m2Y8/RMxky/QPGY8s3WHkMd17IqZXnKcN3kn2REZMMtHD67x7F2zt9n0yRdJItc3/lTcIXXsijnA
|
|
||||||
MJ9rL8FXtDZTt/tq+dC5YeezQibnUjxufM7GQrh9zQ2QDWnxD5dMJqcH6B9iHnvbTc/v3LXtZwIm
|
|
||||||
lxBEzt8qll5N8ZChMUE2K97cAgzNsex1t+x+YdceAZOLRkl5TLeT3i+xKwD9DNwqn9/HA2NzPnvf
|
|
||||||
ghdf2iPsk/NkPBYu2iu2dD4kF9Py2ea7TU5kuJ9+RmAJe+PtP98nZLL0DgmR2Y/bWDoXiWXAr7k8
|
|
||||||
jcAEoFensaFz59Mv7Rcy+bLE0l+2kV4u21chLsaVFJGaabSdBeayd9514KCYib3Ldf6TYktfyqV3
|
|
||||||
BaA4SygeqgSTj/FO4RJ898729j0Cix9jx+OeV14lnKx6eg5oqR1OE0kbCYzQEeyto5//xf72Pa+1
|
|
||||||
//J1U/PsXK7LtooNhDNfuQqQ3okAj7RJgATrXK7d+0J98tobvzIzEbtcd71pI71fYVesBoz0DIQH
|
|
||||||
JMGt3OVS7T3E5K1fm5lcJ+KxcNEmMY+vsivGemnPNXAhRgSQYIPLtS/C5G0TE5HLtcZOeteyK+YD
|
|
||||||
lj4Z4wFJsM7lulftkhCTt0x9InC5bvzNJrH0nuXA0gHp5bgV+Fm4vasSHGbyhsnirRJ8vU18u5Rd
|
|
||||||
MAqx9GtgHmkTA23kQNVcLlWCI0yMfTLG7HLd84rNXMidrOXAWFhVghMpn+/E5QpLcLvA4s0ul530
|
|
||||||
cq8XcLL8rfU5MI9MRDvaAg3s/SGXK9onJouvM/BY91sb6eVjEHCyWu9TKmAiZYoyD3G5uAQfCtt7
|
|
||||||
2OLfft1Ogu9ctJWQ3oYAMIOtVxS06K4UnV/vZy2Yvu1glMnvXvu9vk/0ErymkZLeKYDGrA5dVw0S
|
|
||||||
Cadux9Fd0uJfyZpwQ8TeLUx0EkzHt8BE7G0LL6yBlcOnwj8Pl+CbovYeZSLQI1uvl3OFBkFkqRNb
|
|
||||||
Cy6NPHeJIwlm9m5mspRJ8D2HbbxevpIFmKVvVeRxWMqEFQUgEjyHW/LTUXs3M2Fztp30OnGymltZ
|
|
||||||
IASNLZZNB0S9WQuxuL2rTN76g8ZkrVx6v6bgv1sr/92qAB4DKhz0dWAWu3ght3eVyS6NSfgnt11w
|
|
||||||
4E5WA72S1eSdya4eAhAZxs1zZa0TCdbsPcTknV1/5EzUSGP2drH0LuW6Nw2U3giQOVFXb7IB0MNm
|
|
||||||
PgvcsvsFrU/eefZPjMkYidfL49v7gVf5dY4CUNClL9S4D/iZeKJhwYvc3tvb39WYbP668kAjIb11
|
|
||||||
zUAIdL+uZXSZSqnuamU9vTAT9hkiWMMl2MBk8znrXraRXj7xI/HtFH3L6Mm9SH+5cinkxTEJPqTr
|
|
||||||
EpXJn6N98opYebWVrJn0MkGbr0HfMNpIjAWkUyG/ml297sBBPZP3oky2NG4OdYhgcPEgE1nJmm9o
|
|
||||||
mEKWpZly0cBysm8Vl567NQmOMPkLs/gdT1rnEe5kIfGtd7GxYeRMYq5ZRByg5ezi0ToJ1vfJYzs2
|
|
||||||
PbfIbCfO4tvlpnZR0VW66XplLu1yeZt5rvdqvb1rfbJj7xMdndtNfeIkieCbZQ6bqVqbIjMRZT69
|
|
||||||
hKpJ8Ay9BHMmIR5HOrve32sgwte5p7Y5cYUYKgki1jJSZMVMk+DZ7x60Mtn716OdwWMdTxgCEp55
|
|
||||||
AJTRs8LSLCVdTiTfegeQKNYkeOG9L7W3m5j8TeXRHez6+yadcnHpReYq73prs4gKekEBDeRybWBX
|
|
||||||
X3zA2CX73/3gqQ8/DHZ3d/d0HtYcFR6nKNc6crJQ2aoRVZICLpdBgt/T83jhg3989JHKozvYu/FV
|
|
||||||
7jqe7eDhLf46QavkTkq68OtUxOXiud4FL+okOMTj439+EuYRQtczzJl3lkQYJ2qUXH8zhLVyQC5M
|
|
||||||
GPWaeAQ7jzdGKzf4SpajZScD5N5WpvAeSFi4C6FFvXoe6r+DXdH4Sotvgfytlns1QD6RZImJTKW9
|
|
||||||
bN37WNRr6o/QPx3BzeEucRLfepaJ2zRUSiRbfBOUwecjIBr1GnlEyHT9S+0SHt8CKzUWJ4uhXErE
|
|
||||||
7uOpUUhNBbfJmw6E7H3/vy08uruPHnvz1S1LudeLxLfzbJokn9ptv6uoR1SSN/CW3ftCPP5j4RHq
|
|
||||||
kvf3ak4WsppZuzImIvbfeiLzFl94XHBop5BHd/eRjsPnsKuAIjkb6VUhD63ybIkg5QiaBE/f9l8h
|
|
||||||
j1CXbOSWjhTJTVESTQSKevkCmnLvx0Ie3d2933Dw07RpafDEEQGi3mbN5Xrwk6MiHsHOh7iThfgL
|
|
||||||
y5QYici+h3YW9X7zRI+ASE/nt9gFSBKhaXGsRPIlRKCyNi6WM44fEXRI77fZ30OWvlzSGrlqST/C
|
|
||||||
BQoNdS7XmhNWJkc6v8P+GrB0lkSIhYj8O8M5TlZtFj5kIRLsup797Vi6HFa3ziTCICmRItmtSDGu
|
|
||||||
Lte77vgxE4+OHq1Sw0nYKcRAKRHiEyRnK5sP95jsvetM9ldIOsy3XtoWufdbIv8Utw6xUL7WPNrY
|
|
||||||
JcHOjfxBs2KMb3WQp0hKB8nvHkevDekCU6ME9/Ry6QWU3CuMb3UgPvOhvrlH/FXucs3Q23uw9xHe
|
|
||||||
sUgSYTLREmLLjiHE7YAE61yu2bouOdbBV7Lill4VxCfJ5PYBSPW9tnC+8Ygmvd9lfwiIHyG9Koic
|
|
||||||
VRV1P5JF1qLei5m9BztOcK93DrAIu4JqRwXx4X4m9QDI+Z7Gr36ESXDX99gfAUkEXf7WDvL5UJ+c
|
|
||||||
tgPkJOkk+EhUetmYh6Lm1VQr6K8sB5GPAAJUnXbeHrb3nl7uZAHS20JJbwinUETyyEc4W629U5Xg
|
|
||||||
YO8D7P+RIjlD/tYG5D4K2fQz0PqwCO46cSx49Ci3dKBIThLfaiihiNDWjlXsabLzcGhgOYpvAw10
|
|
||||||
E8opHmlpwJfSi2kJ9nvq2dUPHu89wQNHZFjOp1ug5NFEkE+MoapWPjU/0uEsvh0LtAAoPUO2BoKS
|
|
||||||
AVqu9/vsv+qQnNE8oAFKBk0E2hWh3gNUfk+13AY5WSuB9wMmYs20C4G4XJeab0JK2VrnIq+Hapih
|
|
||||||
LfKAmLupxRziIR+BzUHejlQ56SvPZACiXnPQ3QDEty2kk6UCrMWGtqqo89OfaZt88Vjzt1YA4qsC
|
|
||||||
8FIUyOUyGi5QJIc4WSrAXeqwsQVVOOuWCoEulCQRjCML3aMO2walwUfnzXSLt46L5OwBb/IE7gfm
|
|
||||||
6CMTIL5tRpwsFfBeaCXY86DcMmubs1hfiqEoD1bmTwJwudhoqQdWX2RJBD2QMmxn5g6Vvo2Dr1yO
|
|
||||||
vRU2dRXl2DM3AN/1hhXVWZJIDkc7CaHbGIK5XqBITveJJgFn++eDG1EhEuxdnEjpRWd1hmzwsYgf
|
|
||||||
OALJq/hA6aVKAGPtEsQz99cCA2sZ9j6nHYJbCVBwBSwAwdLruEPA+AqLemmg0otFVEYUgY+upyWY
|
|
||||||
BJ1EiIJauhYC3Wf5vviZ0EmEKBzt4caA7mm4nv5mguJBJhGioJbgbYBuiguU20hB5W81xLoFMGjv
|
|
||||||
a4GUoAzW5RYbxLzZLLIOrAIIYiUg87cM+OffFmDRu6IAu9tIOgRIIoQRz9bl4OACcr220L7JTNbA
|
|
||||||
UoHuKrsiZgluDtRjr4BWSe0B7nYPfDthA1h6gWVrKcCNTAFHXQggfxuBg/hWjBrMTOqA3W1E0Oo9
|
|
||||||
5EjANt/gBB+bBKPSm5BtmEHvEahcEnQIJr2ViTmfIBt6GVBLZgEa38Zr6AzYoULAwqMJUP5Wgdes
|
|
||||||
AUBnEQDlNuaBhS0txuS72wBaegQyhEZLx+LbxJ6wgERZlwM5W0OHQPlbsugkCUzGO3JUsKXFRPPA
|
|
||||||
RheQ+9EAOVnJOLmDKndU1IVH3OWC8rfJOTQtn34xHvVCTlayDn3MJt+8to1OFEY7hM7fVg5PEo+0
|
|
||||||
tGHkQiqQPwgDyN+emsxjazPI422ACudwh5D526Qca6MDFcYvgaJe2smKO/4gQa1uA1lP2skamIrT
|
|
||||||
BDPkc+NMpLyOcLKSPawY5KcI0i4X4WSl7hzBtGLZNE9XOMuTCLkpPQQ1WyLEVLmN1MkanOoTqdMl
|
|
||||||
0RbhcrVusL2zwo2DXIttwy15hVnA/iOwHJeObh9mZyqyqLfJ9vvb3ESF5jGgSExFFvXa5W/dpKEi
|
|
||||||
U+jd20uwWHor80vcpaEiI9+6GLnSbxf1iqR3cKFrx04bUZNlsXs7CbZKb2VuqgVXivTCauPMYlNu
|
|
||||||
Y8rfVuYVuXqcuZhL1RDdGBN/X2FIIgzN6YMsIqjJLKhmZETlNlr+dlBeocsqRZMprsoZXC4ut1Hj
|
|
||||||
24qhZQVZfZ2EhvRhVdatVLz+H2RlujR5x4EBllyv/HygvgvzSSbU+UB9F8aTTNoCt8X/SHdwoeE7
|
|
||||||
P89qt9sTO/SJRa/3ZrebEzuKdRLsqXe7NfHgDt4l2PlAfRbaYVInq/QysPP8Tl7pZRjZGnGyTlrp
|
|
||||||
ZZgUrsKmj2br+1D3+HRyPlCfhXqYlJPzgfouJnh8q04+l1eEWz1XxP+QvoCJ4NFsfR5Xnhb/M/rR
|
|
||||||
j370I4L/AYWr1zfavhjTAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIyLTA0LTA0VDE1OjAzOjMwKzA4
|
|
||||||
OjAwP7kofQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMi0wNC0wNFQxNTowMzozMCswODowME7kkMEA
|
|
||||||
AAAgdEVYdHNvZnR3YXJlAGh0dHBzOi8vaW1hZ2VtYWdpY2sub3JnvM8dnQAAABh0RVh0VGh1bWI6
|
|
||||||
OkRvY3VtZW50OjpQYWdlcwAxp/+7LwAAABh0RVh0VGh1bWI6OkltYWdlOjpIZWlnaHQAMjAwfdcV
|
|
||||||
aQAAABd0RVh0VGh1bWI6OkltYWdlOjpXaWR0aAAyMDDuJkU0AAAAGXRFWHRUaHVtYjo6TWltZXR5
|
|
||||||
cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADE2NDkwNTU4MTD76yvxAAAAEnRF
|
|
||||||
WHRUaHVtYjo6U2l6ZQA0NTEzQkLz/Q6yAAAARnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vYXBwL3Rt
|
|
||||||
cC9pbWFnZWxjL2ltZ3ZpZXcyXzlfMTY0Nzg0ODMyNDI4NTI2NzhfODZfWzBdhSsH1AAAAABJRU5E
|
|
||||||
rkJggg==" ></image>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,54 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/install/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>BuildAdmin-安装</title>
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
var urls = { adminUrl: '', frontUrl: '' };
|
|
||||||
fetch('/api/install/accessUrls').then(function(r){return r.json();}).then(function(res){
|
|
||||||
if (res && res.data) { urls.adminUrl = res.data.adminUrl || ''; urls.frontUrl = res.data.frontUrl || ''; }
|
|
||||||
}).catch(function(){});
|
|
||||||
function applyUrls() {
|
|
||||||
if (!urls.adminUrl && !urls.frontUrl) return;
|
|
||||||
document.querySelectorAll('input[type="text"], input:not([type])').forEach(function(inp){
|
|
||||||
var v = (inp.value || '').trim();
|
|
||||||
if (v && (v.indexOf('#/admin') >= 0 || v.indexOf('index.html') >= 0) && v.indexOf('#/') >= 0) {
|
|
||||||
inp.value = urls.adminUrl;
|
|
||||||
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.querySelectorAll('a[href*="#/admin"]').forEach(function(a){ if (urls.adminUrl) a.href = urls.adminUrl; });
|
|
||||||
document.querySelectorAll('a[href*="#/"]').forEach(function(a){
|
|
||||||
if (urls.frontUrl && a.href.indexOf('#/admin') < 0) a.href = urls.frontUrl;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ setInterval(applyUrls, 800); });
|
|
||||||
else setInterval(applyUrls, 800);
|
|
||||||
})();
|
|
||||||
(function(){
|
|
||||||
function closeMigrateModal() {
|
|
||||||
if (!document.body) return;
|
|
||||||
var txt = document.body.innerText || document.body.textContent || '';
|
|
||||||
if (txt.indexOf('数据表迁移失败') < 0 && txt.indexOf('数据表自动迁移失败') < 0) return;
|
|
||||||
var btns = document.body.querySelectorAll('button, [role="button"], .el-button');
|
|
||||||
for (var i = 0; i < btns.length; i++) {
|
|
||||||
var b = btns[i];
|
|
||||||
if (b.textContent && b.textContent.indexOf('继续安装') >= 0) { b.click(); return; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var obs = new MutationObserver(closeMigrateModal);
|
|
||||||
function start() { if (document.body) { obs.observe(document.body, { childList: true, subtree: true }); closeMigrateModal(); } }
|
|
||||||
if (document.body) start(); else document.addEventListener('DOMContentLoaded', start);
|
|
||||||
setInterval(closeMigrateModal, 150);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<script type="module" crossorigin src="/install/assets/index.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/install/assets/index.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
27
tmp_sig.php
Normal file
27
tmp_sig.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
//脚本执行指令 php tmp_sig.php
|
||||||
|
$secret = '5590a339502b133f4d0c545c3cdad159a4827dfccb3f51bb110c56f9b96568ca';
|
||||||
|
$ts = '1775525663';
|
||||||
|
$rid = 'req_1775525663_123';
|
||||||
|
$body = [
|
||||||
|
'report_date' => '1775525663',
|
||||||
|
'member' => [
|
||||||
|
[
|
||||||
|
'member_id' => '123',
|
||||||
|
'login' => 'zhenhui',
|
||||||
|
'ltv_deposit' => 1500,
|
||||||
|
'ltv_withdrawal' => 1800,
|
||||||
|
'yesterday_total_wl' => -300,
|
||||||
|
'yesterday_total_deposit' => 600,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$json = json_encode($body);
|
||||||
|
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/mall/dailyPush\n" . hash('sha256', $json);
|
||||||
|
|
||||||
|
echo "json={$json}\n";
|
||||||
|
echo "sha256=" . hash('sha256', $json) . "\n";
|
||||||
|
echo "X-Signature=" . hash_hmac('sha256', $canonical, $secret) . "\n";
|
||||||
|
|
||||||
@@ -8,4 +8,4 @@ VITE_BASE_PATH = '/'
|
|||||||
VITE_OUT_DIR = 'dist'
|
VITE_OUT_DIR = 'dist'
|
||||||
|
|
||||||
# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
|
# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
|
||||||
VITE_AXIOS_BASE_URL = 'getCurrentDomain'
|
VITE_AXIOS_BASE_URL = 'https://playx-api.cjdhr.top'
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* backend common language package
|
* backend common language package
|
||||||
*/
|
*/
|
||||||
|
import menu from './en/menu'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
menu,
|
||||||
Balance: 'Balance',
|
Balance: 'Balance',
|
||||||
Integral: 'Integral',
|
Integral: 'Integral',
|
||||||
Connection: 'connection',
|
Connection: 'connection',
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
nickname: 'Nickname',
|
nickname: 'Nickname',
|
||||||
channel_id: 'Channel',
|
|
||||||
channel_name: 'Channel name',
|
|
||||||
'Please select channel': 'Please select channel',
|
|
||||||
group: 'Group',
|
group: 'Group',
|
||||||
avatar: 'Avatar',
|
avatar: 'Avatar',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'id',
|
|
||||||
name: 'name',
|
|
||||||
ip_white: 'ip_white',
|
|
||||||
ip_placeholder: 'Please enter IP address',
|
|
||||||
whitelist: 'Whitelist',
|
|
||||||
title: 'title',
|
|
||||||
remark: 'remark',
|
|
||||||
admin_id: 'admin_id',
|
|
||||||
admin__username: 'username',
|
|
||||||
secret: 'secret',
|
|
||||||
create_time: 'create_time',
|
|
||||||
update_time: 'update_time',
|
|
||||||
'quick Search Fields': 'id',
|
|
||||||
}
|
|
||||||
@@ -36,4 +36,23 @@ export default {
|
|||||||
second: 'Second',
|
second: 'Second',
|
||||||
day: 'Day',
|
day: 'Day',
|
||||||
'Number of attachments Uploaded': 'Number of attachments upload',
|
'Number of attachments Uploaded': 'Number of attachments upload',
|
||||||
|
Today: 'Today',
|
||||||
|
Yesterday: 'Yesterday',
|
||||||
|
Orders: 'Orders',
|
||||||
|
Pending: 'Pending',
|
||||||
|
'Daily new players': 'Daily new players',
|
||||||
|
'Yesterday points': 'Yesterday points (claimed)',
|
||||||
|
'Yesterday redeem': 'Yesterday redeem',
|
||||||
|
'Pending physical to ship': 'Pending physical to ship',
|
||||||
|
'Yesterday item redeem stat': 'Yesterday item redeem stat',
|
||||||
|
'Yesterday redeem points sum': 'Yesterday redeem points sum',
|
||||||
|
'Yesterday redeem amount sum': 'Yesterday redeem amount sum',
|
||||||
|
'Grant failed retryable': 'Grant failed retryable',
|
||||||
|
'Item ID': 'Item ID',
|
||||||
|
'Item title': 'Item title',
|
||||||
|
'Order count': 'Order count',
|
||||||
|
Completed: 'Completed',
|
||||||
|
Rejected: 'Rejected',
|
||||||
|
'Points sum': 'Points sum',
|
||||||
|
'Amount sum': 'Amount sum',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
mall_user_id: 'mall_user_id',
|
playx_user_asset_id: 'PlayX user asset',
|
||||||
malluser__username: 'username',
|
playxuserasset__username: 'username',
|
||||||
|
receiver_name: 'receiver name',
|
||||||
phone: 'phone',
|
phone: 'phone',
|
||||||
region: 'region',
|
region: 'region',
|
||||||
detail_address: 'detail_address',
|
detail_address: 'detail_address',
|
||||||
address: 'address',
|
default_setting: 'Default address',
|
||||||
default_setting: 'default_setting',
|
'default_setting 0': '--',
|
||||||
'default_setting 0': 'default_setting 0',
|
'default_setting 1': 'YES',
|
||||||
'default_setting 1': 'default_setting 1',
|
|
||||||
create_time: 'create_time',
|
create_time: 'create_time',
|
||||||
update_time: 'update_time',
|
update_time: 'update_time',
|
||||||
'quick Search Fields': 'id',
|
'quick Search Fields': 'id',
|
||||||
|
|||||||
9
web/src/lang/backend/en/mall/claimLog.ts
Normal file
9
web/src/lang/backend/en/mall/claimLog.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
id: 'id',
|
||||||
|
claim_request_id: 'claim_request_id',
|
||||||
|
user_id: 'user_id',
|
||||||
|
claimed_amount: 'claimed_amount',
|
||||||
|
create_time: 'create_time',
|
||||||
|
'quick Search Fields': 'id',
|
||||||
|
}
|
||||||
|
|
||||||
13
web/src/lang/backend/en/mall/dailyPush.ts
Normal file
13
web/src/lang/backend/en/mall/dailyPush.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
id: 'id',
|
||||||
|
user_id: 'user_id',
|
||||||
|
date: 'date',
|
||||||
|
username: 'username',
|
||||||
|
yesterday_win_loss_net: 'yesterday_win_loss_net',
|
||||||
|
yesterday_total_deposit: 'yesterday_total_deposit',
|
||||||
|
lifetime_total_deposit: 'lifetime_total_deposit',
|
||||||
|
lifetime_total_withdraw: 'lifetime_total_withdraw',
|
||||||
|
create_time: 'create_time',
|
||||||
|
'quick Search Fields': 'id',
|
||||||
|
}
|
||||||
|
|
||||||
43
web/src/lang/backend/en/mall/order.ts
Normal file
43
web/src/lang/backend/en/mall/order.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export default {
|
||||||
|
approve: 'Review',
|
||||||
|
manual_retry: 'Retry grant',
|
||||||
|
retry_confirm: 'Queue this order for grant retry?',
|
||||||
|
id: 'Order ID',
|
||||||
|
user_id: 'User ID',
|
||||||
|
type: 'Type',
|
||||||
|
'type BONUS': 'Bonus',
|
||||||
|
'type PHYSICAL': 'Physical',
|
||||||
|
'type WITHDRAW': 'Withdraw',
|
||||||
|
status: 'Status',
|
||||||
|
'status PENDING': 'Pending',
|
||||||
|
'status COMPLETED': 'Completed',
|
||||||
|
'status SHIPPED': 'Shipped',
|
||||||
|
'status REJECTED': 'Rejected',
|
||||||
|
mall_item_id: 'Product ID',
|
||||||
|
mallitem__title: 'Product title',
|
||||||
|
points_cost: 'Points spent',
|
||||||
|
amount: 'Cash amount',
|
||||||
|
multiplier: 'Turnover multiplier',
|
||||||
|
external_transaction_id: 'Order number',
|
||||||
|
playx_transaction_id: 'PlayX transaction ID',
|
||||||
|
grant_status: 'Grant status',
|
||||||
|
'grant_status NOT_SENT': 'Not sent',
|
||||||
|
'grant_status SENT_PENDING': 'Sent (queued)',
|
||||||
|
'grant_status ACCEPTED': 'Accepted',
|
||||||
|
'grant_status FAILED_RETRYABLE': 'Failed (retryable)',
|
||||||
|
'grant_status FAILED_FINAL': 'Failed (final)',
|
||||||
|
'grant_status ---': '—',
|
||||||
|
fail_reason: 'Failure reason',
|
||||||
|
reject_reason: 'Rejection reason',
|
||||||
|
shipping_company: 'Carrier',
|
||||||
|
shipping_no: 'Tracking number',
|
||||||
|
receiver_name: 'Recipient name',
|
||||||
|
receiver_phone: 'Recipient phone',
|
||||||
|
receiver_address: 'Shipping address',
|
||||||
|
mall_address_id: 'Address ID',
|
||||||
|
start_time: 'Redemption time',
|
||||||
|
end_time: 'Collection end time',
|
||||||
|
create_time: 'Created at',
|
||||||
|
update_time: 'Updated at',
|
||||||
|
'quick Search Fields': 'Order ID',
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
order: 'order',
|
order: 'order',
|
||||||
mall_user_id: 'mall_user_id',
|
playx_user_asset_id: 'PlayX user asset',
|
||||||
malluser__username: 'username',
|
playxuserasset__username: 'username',
|
||||||
type: 'type',
|
type: 'type',
|
||||||
'type 1': 'type 1',
|
'type 1': 'type 1',
|
||||||
'type 2': 'type 2',
|
'type 2': 'type 2',
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'id',
|
|
||||||
username: 'username',
|
|
||||||
password: 'password',
|
|
||||||
create_time: 'create_time',
|
|
||||||
update_time: 'update_time',
|
|
||||||
score: 'score',
|
|
||||||
quickSearchFields: 'id',
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,7 @@ export default {
|
|||||||
'grant_status ACCEPTED': 'ACCEPTED',
|
'grant_status ACCEPTED': 'ACCEPTED',
|
||||||
'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE',
|
'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE',
|
||||||
'grant_status FAILED_FINAL': 'FAILED_FINAL',
|
'grant_status FAILED_FINAL': 'FAILED_FINAL',
|
||||||
|
'grant_status ---': '---',
|
||||||
fail_reason: 'fail_reason',
|
fail_reason: 'fail_reason',
|
||||||
reject_reason: 'reject_reason',
|
reject_reason: 'reject_reason',
|
||||||
shipping_company: 'shipping_company',
|
shipping_company: 'shipping_company',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
user_id: 'user_id',
|
|
||||||
username: 'username',
|
username: 'username',
|
||||||
|
phone: 'phone',
|
||||||
|
playx_user_id: 'playx_user_id',
|
||||||
locked_points: 'locked_points',
|
locked_points: 'locked_points',
|
||||||
available_points: 'available_points',
|
available_points: 'available_points',
|
||||||
today_limit: 'today_limit',
|
today_limit: 'today_limit',
|
||||||
@@ -9,5 +10,5 @@ export default {
|
|||||||
today_limit_date: 'today_limit_date',
|
today_limit_date: 'today_limit_date',
|
||||||
create_time: 'create_time',
|
create_time: 'create_time',
|
||||||
update_time: 'update_time',
|
update_time: 'update_time',
|
||||||
'quick Search Fields': 'id',
|
'quick Search Fields': 'id, playx_user_id, username, phone',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
order: 'order',
|
order: 'order',
|
||||||
mall_user_id: 'mall_user_id',
|
playx_user_asset_id: 'PlayX user asset',
|
||||||
malluser__username: 'username',
|
playxuserasset__username: 'username',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
'status 0': 'status 0',
|
'status 0': 'status 0',
|
||||||
'status 1': 'status 1',
|
'status 1': 'status 1',
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'id',
|
|
||||||
username: 'username',
|
|
||||||
phone: 'phone',
|
|
||||||
password: 'password',
|
|
||||||
score: 'score',
|
|
||||||
daily_claim: 'daily_claim',
|
|
||||||
daily_claim_use: 'daily_claim_use',
|
|
||||||
available_for_withdrawal: 'available_for_withdrawal',
|
|
||||||
admin_id: 'admin_id',
|
|
||||||
admin__username: 'username',
|
|
||||||
create_time: 'create_time',
|
|
||||||
update_time: 'update_time',
|
|
||||||
'quick Search Fields': 'id',
|
|
||||||
}
|
|
||||||
15
web/src/lang/backend/en/mall/userAsset.ts
Normal file
15
web/src/lang/backend/en/mall/userAsset.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
id: 'id',
|
||||||
|
username: 'username',
|
||||||
|
phone: 'phone',
|
||||||
|
playx_user_id: 'playx_user_id',
|
||||||
|
locked_points: 'locked_points',
|
||||||
|
available_points: 'available_points',
|
||||||
|
today_limit: 'today_limit',
|
||||||
|
today_claimed: 'today_claimed',
|
||||||
|
today_limit_date: 'today_limit_date',
|
||||||
|
create_time: 'create_time',
|
||||||
|
update_time: 'update_time',
|
||||||
|
'quick Search Fields': 'id, playx_user_id, username, phone',
|
||||||
|
}
|
||||||
|
|
||||||
119
web/src/lang/backend/en/menu.ts
Normal file
119
web/src/lang/backend/en/menu.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Admin menu titles (admin_rule.name → menu.names.{name with / as _})
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
names: {
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
dashboard_index: 'Browse',
|
||||||
|
dashboard_dashboard: 'Dashboard',
|
||||||
|
auth: 'Access control',
|
||||||
|
auth_group: 'Admin groups',
|
||||||
|
auth_group_index: 'Browse',
|
||||||
|
auth_group_add: 'Add',
|
||||||
|
auth_group_edit: 'Edit',
|
||||||
|
auth_group_del: 'Delete',
|
||||||
|
auth_admin: 'Administrators',
|
||||||
|
auth_admin_index: 'Browse',
|
||||||
|
auth_admin_add: 'Add',
|
||||||
|
auth_admin_edit: 'Edit',
|
||||||
|
auth_admin_del: 'Delete',
|
||||||
|
auth_rule: 'Menu rules',
|
||||||
|
auth_rule_index: 'Browse',
|
||||||
|
auth_rule_add: 'Add',
|
||||||
|
auth_rule_edit: 'Edit',
|
||||||
|
auth_rule_del: 'Delete',
|
||||||
|
auth_rule_sortable: 'Sort',
|
||||||
|
auth_adminLog: 'Admin logs',
|
||||||
|
auth_adminLog_index: 'Browse',
|
||||||
|
user: 'Members',
|
||||||
|
user_user: 'Members',
|
||||||
|
user_user_index: 'Browse',
|
||||||
|
user_user_add: 'Add',
|
||||||
|
user_user_edit: 'Edit',
|
||||||
|
user_user_del: 'Delete',
|
||||||
|
user_group: 'Member groups',
|
||||||
|
user_group_index: 'Browse',
|
||||||
|
user_group_add: 'Add',
|
||||||
|
user_group_edit: 'Edit',
|
||||||
|
user_group_del: 'Delete',
|
||||||
|
user_rule: 'Member rules',
|
||||||
|
user_rule_index: 'Browse',
|
||||||
|
user_rule_add: 'Add',
|
||||||
|
user_rule_edit: 'Edit',
|
||||||
|
user_rule_del: 'Delete',
|
||||||
|
user_rule_sortable: 'Sort',
|
||||||
|
user_moneyLog: 'Balance logs',
|
||||||
|
user_moneyLog_index: 'Browse',
|
||||||
|
user_moneyLog_add: 'Add',
|
||||||
|
user_scoreLog: 'Points logs',
|
||||||
|
user_scoreLog_index: 'Browse',
|
||||||
|
user_scoreLog_add: 'Add',
|
||||||
|
routine: 'General',
|
||||||
|
routine_config: 'System config',
|
||||||
|
routine_config_index: 'Browse',
|
||||||
|
routine_config_edit: 'Edit',
|
||||||
|
routine_config_add: 'Add',
|
||||||
|
routine_config_del: 'Delete',
|
||||||
|
routine_attachment: 'Attachments',
|
||||||
|
routine_attachment_index: 'Browse',
|
||||||
|
routine_attachment_edit: 'Edit',
|
||||||
|
routine_attachment_del: 'Delete',
|
||||||
|
routine_adminInfo: 'Profile',
|
||||||
|
routine_adminInfo_index: 'Browse',
|
||||||
|
routine_adminInfo_edit: 'Edit',
|
||||||
|
security: 'Data security',
|
||||||
|
security_dataRecycleLog: 'Recycle bin',
|
||||||
|
security_dataRecycleLog_index: 'Browse',
|
||||||
|
security_dataRecycleLog_del: 'Delete',
|
||||||
|
security_dataRecycleLog_restore: 'Restore',
|
||||||
|
security_dataRecycleLog_info: 'Details',
|
||||||
|
security_sensitiveDataLog: 'Sensitive data logs',
|
||||||
|
security_sensitiveDataLog_index: 'Browse',
|
||||||
|
security_sensitiveDataLog_del: 'Delete',
|
||||||
|
security_sensitiveDataLog_rollback: 'Rollback',
|
||||||
|
security_sensitiveDataLog_info: 'Details',
|
||||||
|
security_dataRecycle: 'Recycle rules',
|
||||||
|
security_dataRecycle_index: 'Browse',
|
||||||
|
security_dataRecycle_add: 'Add',
|
||||||
|
security_dataRecycle_edit: 'Edit',
|
||||||
|
security_dataRecycle_del: 'Delete',
|
||||||
|
security_sensitiveData: 'Sensitive field rules',
|
||||||
|
security_sensitiveData_index: 'Browse',
|
||||||
|
security_sensitiveData_add: 'Add',
|
||||||
|
security_sensitiveData_edit: 'Edit',
|
||||||
|
security_sensitiveData_del: 'Delete',
|
||||||
|
buildadmin: 'BuildAdmin',
|
||||||
|
buildadmin_buildadmin: 'BuildAdmin',
|
||||||
|
moduleStore_moduleStore: 'Module store',
|
||||||
|
moduleStore_moduleStore_index: 'Browse',
|
||||||
|
moduleStore_moduleStore_install: 'Install',
|
||||||
|
moduleStore_moduleStore_changeState: 'Change state',
|
||||||
|
moduleStore_moduleStore_uninstall: 'Uninstall',
|
||||||
|
moduleStore_moduleStore_update: 'Update',
|
||||||
|
crud_crud: 'CRUD generator',
|
||||||
|
crud_crud_index: 'Browse',
|
||||||
|
crud_crud_generate: 'Generate',
|
||||||
|
crud_crud_delete: 'Delete',
|
||||||
|
mall: 'Points mall',
|
||||||
|
mall_userAsset: 'User assets',
|
||||||
|
mall_userAsset_index: 'Browse',
|
||||||
|
mall_userAsset_edit: 'Edit',
|
||||||
|
mall_userAsset_del: 'Delete',
|
||||||
|
mall_address: 'Shipping addresses',
|
||||||
|
mall_order: 'Orders',
|
||||||
|
mall_order_add: 'Add',
|
||||||
|
mall_order_edit: 'Edit',
|
||||||
|
mall_order_del: 'Delete',
|
||||||
|
mall_order_approve: 'Approve',
|
||||||
|
mall_dailyPush: 'Daily push',
|
||||||
|
mall_claimLog: 'Claim log',
|
||||||
|
mall_item: 'Products',
|
||||||
|
mall_playxOrder: 'PlayX orders',
|
||||||
|
mall_playxCenter: 'PlayX center',
|
||||||
|
mall_playxClaimLog: 'PlayX claim log',
|
||||||
|
mall_playxDailyPush: 'PlayX daily push',
|
||||||
|
mall_playxUserAsset: 'PlayX user assets',
|
||||||
|
mall_pintsOrder: 'Points orders',
|
||||||
|
mall_redemptionOrder: 'Redemption orders',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
'Upload administrator': 'Upload administrator',
|
'Upload administrator': 'Upload administrator',
|
||||||
'Upload user': 'Upload member',
|
|
||||||
'Storage mode': 'Storage mode',
|
'Storage mode': 'Storage mode',
|
||||||
'Physical path': 'Physical path',
|
'Physical path': 'Physical path',
|
||||||
'image width': 'Picture width',
|
'image width': 'Picture width',
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
* 后台公共语言包
|
* 后台公共语言包
|
||||||
* 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
|
* 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
|
||||||
*/
|
*/
|
||||||
|
import menu from './zh-cn/menu'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
menu,
|
||||||
Balance: '余额',
|
Balance: '余额',
|
||||||
Integral: '积分',
|
Integral: '积分',
|
||||||
Connection: '连接标识',
|
Connection: '连接标识',
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
nickname: '昵称',
|
nickname: '昵称',
|
||||||
channel_id: '渠道',
|
|
||||||
channel_name: '渠道名称',
|
|
||||||
'Please select channel': '请选择渠道',
|
|
||||||
group: '角色组',
|
group: '角色组',
|
||||||
avatar: '头像',
|
avatar: '头像',
|
||||||
email: '电子邮箱',
|
email: '电子邮箱',
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'ID',
|
|
||||||
name: '渠道名',
|
|
||||||
ip_white: 'IP白名单',
|
|
||||||
ip_placeholder: '请输入IP地址',
|
|
||||||
whitelist: '白名单',
|
|
||||||
title: '标题',
|
|
||||||
remark: '备注',
|
|
||||||
admin_id: '管理员',
|
|
||||||
admin__username: '用户名',
|
|
||||||
secret: '密钥',
|
|
||||||
create_time: '创建时间',
|
|
||||||
update_time: '修改时间',
|
|
||||||
'quick Search Fields': 'ID',
|
|
||||||
}
|
|
||||||
@@ -36,4 +36,23 @@ export default {
|
|||||||
second: '秒',
|
second: '秒',
|
||||||
day: '天',
|
day: '天',
|
||||||
'Number of attachments Uploaded': '附件上传量',
|
'Number of attachments Uploaded': '附件上传量',
|
||||||
|
Today: '今日',
|
||||||
|
Yesterday: '昨日',
|
||||||
|
Orders: '订单',
|
||||||
|
Pending: '待处理',
|
||||||
|
'Daily new players': '每日新增玩家',
|
||||||
|
'Yesterday points': '昨日积分(领取)',
|
||||||
|
'Yesterday redeem': '昨日兑换',
|
||||||
|
'Pending physical to ship': '待发货实物单',
|
||||||
|
'Yesterday item redeem stat': '昨日商品兑换统计',
|
||||||
|
'Yesterday redeem points sum': '昨日兑换消耗积分合计',
|
||||||
|
'Yesterday redeem amount sum': '昨日兑换现金面值合计',
|
||||||
|
'Grant failed retryable': '发放失败待重试',
|
||||||
|
'Item ID': '商品ID',
|
||||||
|
'Item title': '商品名称',
|
||||||
|
'Order count': '兑换次数',
|
||||||
|
Completed: '已完成',
|
||||||
|
Rejected: '已驳回',
|
||||||
|
'Points sum': '消耗积分合计',
|
||||||
|
'Amount sum': '现金面值合计',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'ID',
|
id: 'ID',
|
||||||
mall_user_id: '用户',
|
playx_user_asset_id: '用户资产',
|
||||||
malluser__username: '用户名',
|
playxuserasset__username: '用户名',
|
||||||
|
receiver_name: '收货人',
|
||||||
phone: '电话',
|
phone: '电话',
|
||||||
region: '地区',
|
region: '地区',
|
||||||
detail_address: '详细地址',
|
detail_address: '详细地址',
|
||||||
address: '地址',
|
|
||||||
default_setting: '默认地址',
|
default_setting: '默认地址',
|
||||||
'default_setting 0': '关',
|
'default_setting 0': '--',
|
||||||
'default_setting 1': '开',
|
'default_setting 1': '是',
|
||||||
create_time: '创建时间',
|
create_time: '创建时间',
|
||||||
update_time: '修改时间',
|
update_time: '修改时间',
|
||||||
'quick Search Fields': 'ID',
|
'quick Search Fields': 'ID',
|
||||||
|
|||||||
9
web/src/lang/backend/zh-cn/mall/claimLog.ts
Normal file
9
web/src/lang/backend/zh-cn/mall/claimLog.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
id: 'ID',
|
||||||
|
claim_request_id: '领取订单号',
|
||||||
|
user_id: '用户ID',
|
||||||
|
claimed_amount: '领取积分',
|
||||||
|
create_time: '创建时间',
|
||||||
|
'quick Search Fields': 'ID',
|
||||||
|
}
|
||||||
|
|
||||||
13
web/src/lang/backend/zh-cn/mall/dailyPush.ts
Normal file
13
web/src/lang/backend/zh-cn/mall/dailyPush.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
id: 'ID',
|
||||||
|
user_id: '用户ID',
|
||||||
|
date: '业务日期',
|
||||||
|
username: '用户名',
|
||||||
|
yesterday_win_loss_net: '昨日净输赢',
|
||||||
|
yesterday_total_deposit: '昨日总充值',
|
||||||
|
lifetime_total_deposit: '历史总充值',
|
||||||
|
lifetime_total_withdraw: '历史总提现',
|
||||||
|
create_time: '创建时间',
|
||||||
|
'quick Search Fields': 'ID',
|
||||||
|
}
|
||||||
|
|
||||||
43
web/src/lang/backend/zh-cn/mall/order.ts
Normal file
43
web/src/lang/backend/zh-cn/mall/order.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export default {
|
||||||
|
approve: '审核',
|
||||||
|
manual_retry: '手动重试',
|
||||||
|
retry_confirm: '确认将该订单加入重试队列?',
|
||||||
|
id: 'ID',
|
||||||
|
user_id: '用户ID',
|
||||||
|
type: '类型',
|
||||||
|
'type BONUS': '红利(BONUS)',
|
||||||
|
'type PHYSICAL': '实物(PHYSICAL)',
|
||||||
|
'type WITHDRAW': '提现(WITHDRAW)',
|
||||||
|
status: '状态',
|
||||||
|
'status PENDING': '处理中(PENDING)',
|
||||||
|
'status COMPLETED': '已完成(COMPLETED)',
|
||||||
|
'status SHIPPED': '已发货(SHIPPED)',
|
||||||
|
'status REJECTED': '已驳回(REJECTED)',
|
||||||
|
mall_item_id: '商品ID',
|
||||||
|
mallitem__title: '商品标题',
|
||||||
|
points_cost: '消耗积分',
|
||||||
|
amount: '现金面值',
|
||||||
|
multiplier: '流水倍数',
|
||||||
|
external_transaction_id: '订单号',
|
||||||
|
playx_transaction_id: 'PlayX流水号',
|
||||||
|
grant_status: '推送playx状态',
|
||||||
|
'grant_status NOT_SENT': '未发送',
|
||||||
|
'grant_status SENT_PENDING': '已发送排队',
|
||||||
|
'grant_status ACCEPTED': '已接收',
|
||||||
|
'grant_status FAILED_RETRYABLE': '失败可重试',
|
||||||
|
'grant_status FAILED_FINAL': '失败最终',
|
||||||
|
'grant_status ---': '---',
|
||||||
|
fail_reason: '失败原因',
|
||||||
|
reject_reason: '驳回原因',
|
||||||
|
shipping_company: '物流公司',
|
||||||
|
shipping_no: '物流单号',
|
||||||
|
receiver_name: '收货人',
|
||||||
|
receiver_phone: '收货电话',
|
||||||
|
receiver_address: '收货地址',
|
||||||
|
mall_address_id: '地址ID',
|
||||||
|
start_time: '兑换时间',
|
||||||
|
end_time: '领取结束时间',
|
||||||
|
create_time: '创建时间',
|
||||||
|
update_time: '修改时间',
|
||||||
|
'quick Search Fields': 'ID',
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'ID',
|
id: 'ID',
|
||||||
order: '订单编号',
|
order: '订单编号',
|
||||||
mall_user_id: '用户',
|
playx_user_asset_id: '用户资产',
|
||||||
malluser__username: '用户名',
|
playxuserasset__username: '用户名',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
'type 1': '奖励',
|
'type 1': '奖励',
|
||||||
'type 2': '充值',
|
'type 2': '充值',
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'ID',
|
|
||||||
username: '用户名',
|
|
||||||
password: '密码',
|
|
||||||
create_time: '创建时间',
|
|
||||||
update_time: '修改时间',
|
|
||||||
score: '积分',
|
|
||||||
quickSearchFields: 'ID',
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
title: 'PlayX 对接中心',
|
title: 'PlayX 对接中心',
|
||||||
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“发放子状态=失败可重试”的订单。',
|
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“推送playx状态=失败可重试”的订单。',
|
||||||
orders: '统一订单',
|
orders: '统一订单',
|
||||||
dailyPush: '每日推送',
|
dailyPush: '每日推送',
|
||||||
claimLog: '领取记录',
|
claimLog: '领取记录',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'ID',
|
id: 'ID',
|
||||||
claim_request_id: '领取幂等键',
|
claim_request_id: '领取订单号',
|
||||||
user_id: '用户ID',
|
user_id: '用户ID',
|
||||||
claimed_amount: '领取积分',
|
claimed_amount: '领取积分',
|
||||||
create_time: '创建时间',
|
create_time: '创建时间',
|
||||||
|
|||||||
@@ -15,14 +15,15 @@ export default {
|
|||||||
points_cost: '消耗积分',
|
points_cost: '消耗积分',
|
||||||
amount: '现金面值',
|
amount: '现金面值',
|
||||||
multiplier: '流水倍数',
|
multiplier: '流水倍数',
|
||||||
external_transaction_id: '外部交易幂等键',
|
external_transaction_id: '订单号',
|
||||||
playx_transaction_id: 'PlayX流水号',
|
playx_transaction_id: 'PlayX流水号',
|
||||||
grant_status: '发放子状态',
|
grant_status: '推送playx状态',
|
||||||
'grant_status NOT_SENT': '未发送',
|
'grant_status NOT_SENT': '未发送',
|
||||||
'grant_status SENT_PENDING': '已发送排队',
|
'grant_status SENT_PENDING': '已发送排队',
|
||||||
'grant_status ACCEPTED': '已接收(accepted)',
|
'grant_status ACCEPTED': '已接收(accepted)',
|
||||||
'grant_status FAILED_RETRYABLE': '失败可重试',
|
'grant_status FAILED_RETRYABLE': '失败可重试',
|
||||||
'grant_status FAILED_FINAL': '失败最终',
|
'grant_status FAILED_FINAL': '失败最终',
|
||||||
|
'grant_status ---': '---',
|
||||||
fail_reason: '失败原因',
|
fail_reason: '失败原因',
|
||||||
reject_reason: '驳回原因',
|
reject_reason: '驳回原因',
|
||||||
shipping_company: '物流公司',
|
shipping_company: '物流公司',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'ID',
|
id: 'ID',
|
||||||
user_id: '用户ID',
|
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
|
phone: '手机号',
|
||||||
|
playx_user_id: 'PlayX用户ID',
|
||||||
locked_points: '待领取积分',
|
locked_points: '待领取积分',
|
||||||
available_points: '可用积分',
|
available_points: '可用积分',
|
||||||
today_limit: '今日可领取上限',
|
today_limit: '今日可领取上限',
|
||||||
@@ -9,6 +10,5 @@ export default {
|
|||||||
today_limit_date: '今日上限日期',
|
today_limit_date: '今日上限日期',
|
||||||
create_time: '创建时间',
|
create_time: '创建时间',
|
||||||
update_time: '修改时间',
|
update_time: '修改时间',
|
||||||
'quick Search Fields': 'ID',
|
'quick Search Fields': 'ID、PlayX用户ID、用户名、手机号',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
id: 'ID',
|
id: 'ID',
|
||||||
order: '订单号',
|
order: '订单号',
|
||||||
mall_user_id: '用户',
|
playx_user_asset_id: '用户资产',
|
||||||
malluser__username: '用户名',
|
playxuserasset__username: '用户名',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
'status 0': '待发放',
|
'status 0': '待发放',
|
||||||
'status 1': '已发放',
|
'status 1': '已发放',
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: 'ID',
|
|
||||||
username: '用户名',
|
|
||||||
phone: '手机号',
|
|
||||||
password: '密码',
|
|
||||||
score: '积分',
|
|
||||||
daily_claim: '每日限额',
|
|
||||||
daily_claim_use: '每日限额(已使用)',
|
|
||||||
available_for_withdrawal: '可提现金额',
|
|
||||||
admin_id: '归属管理员id',
|
|
||||||
admin__username: '归属管理员',
|
|
||||||
create_time: '创建时间',
|
|
||||||
update_time: '修改时间',
|
|
||||||
'quick Search Fields': 'ID',
|
|
||||||
}
|
|
||||||
15
web/src/lang/backend/zh-cn/mall/userAsset.ts
Normal file
15
web/src/lang/backend/zh-cn/mall/userAsset.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
id: 'ID',
|
||||||
|
username: '用户名',
|
||||||
|
phone: '手机号',
|
||||||
|
playx_user_id: 'PlayX用户ID',
|
||||||
|
locked_points: '待领取积分',
|
||||||
|
available_points: '可用积分',
|
||||||
|
today_limit: '今日可领取上限',
|
||||||
|
today_claimed: '今日已领取',
|
||||||
|
today_limit_date: '今日上限日期',
|
||||||
|
create_time: '创建时间',
|
||||||
|
update_time: '修改时间',
|
||||||
|
'quick Search Fields': 'ID、PlayX用户ID、用户名、手机号',
|
||||||
|
}
|
||||||
|
|
||||||
120
web/src/lang/backend/zh-cn/menu.ts
Normal file
120
web/src/lang/backend/zh-cn/menu.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* 后台菜单标题(与 admin_rule.name 对应:menu.names.{name 中 / 改为 _})
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
names: {
|
||||||
|
/** version202 后菜单 name 为 dashboard,不再使用 dashboard/dashboard */
|
||||||
|
dashboard: '控制台',
|
||||||
|
dashboard_index: '查看',
|
||||||
|
dashboard_dashboard: '控制台',
|
||||||
|
auth: '权限管理',
|
||||||
|
auth_group: '角色组管理',
|
||||||
|
auth_group_index: '查看',
|
||||||
|
auth_group_add: '添加',
|
||||||
|
auth_group_edit: '编辑',
|
||||||
|
auth_group_del: '删除',
|
||||||
|
auth_admin: '管理员管理',
|
||||||
|
auth_admin_index: '查看',
|
||||||
|
auth_admin_add: '添加',
|
||||||
|
auth_admin_edit: '编辑',
|
||||||
|
auth_admin_del: '删除',
|
||||||
|
auth_rule: '菜单规则管理',
|
||||||
|
auth_rule_index: '查看',
|
||||||
|
auth_rule_add: '添加',
|
||||||
|
auth_rule_edit: '编辑',
|
||||||
|
auth_rule_del: '删除',
|
||||||
|
auth_rule_sortable: '快速排序',
|
||||||
|
auth_adminLog: '管理员日志管理',
|
||||||
|
auth_adminLog_index: '查看',
|
||||||
|
user: '会员管理',
|
||||||
|
user_user: '会员管理',
|
||||||
|
user_user_index: '查看',
|
||||||
|
user_user_add: '添加',
|
||||||
|
user_user_edit: '编辑',
|
||||||
|
user_user_del: '删除',
|
||||||
|
user_group: '会员分组管理',
|
||||||
|
user_group_index: '查看',
|
||||||
|
user_group_add: '添加',
|
||||||
|
user_group_edit: '编辑',
|
||||||
|
user_group_del: '删除',
|
||||||
|
user_rule: '会员规则管理',
|
||||||
|
user_rule_index: '查看',
|
||||||
|
user_rule_add: '添加',
|
||||||
|
user_rule_edit: '编辑',
|
||||||
|
user_rule_del: '删除',
|
||||||
|
user_rule_sortable: '快速排序',
|
||||||
|
user_moneyLog: '会员余额管理',
|
||||||
|
user_moneyLog_index: '查看',
|
||||||
|
user_moneyLog_add: '添加',
|
||||||
|
user_scoreLog: '会员积分管理',
|
||||||
|
user_scoreLog_index: '查看',
|
||||||
|
user_scoreLog_add: '添加',
|
||||||
|
routine: '常规管理',
|
||||||
|
routine_config: '系统配置',
|
||||||
|
routine_config_index: '查看',
|
||||||
|
routine_config_edit: '编辑',
|
||||||
|
routine_config_add: '添加',
|
||||||
|
routine_config_del: '删除',
|
||||||
|
routine_attachment: '附件管理',
|
||||||
|
routine_attachment_index: '查看',
|
||||||
|
routine_attachment_edit: '编辑',
|
||||||
|
routine_attachment_del: '删除',
|
||||||
|
routine_adminInfo: '个人资料',
|
||||||
|
routine_adminInfo_index: '查看',
|
||||||
|
routine_adminInfo_edit: '编辑',
|
||||||
|
security: '数据安全管理',
|
||||||
|
security_dataRecycleLog: '数据回收站',
|
||||||
|
security_dataRecycleLog_index: '查看',
|
||||||
|
security_dataRecycleLog_del: '删除',
|
||||||
|
security_dataRecycleLog_restore: '还原',
|
||||||
|
security_dataRecycleLog_info: '查看详情',
|
||||||
|
security_sensitiveDataLog: '敏感数据修改记录',
|
||||||
|
security_sensitiveDataLog_index: '查看',
|
||||||
|
security_sensitiveDataLog_del: '删除',
|
||||||
|
security_sensitiveDataLog_rollback: '回滚',
|
||||||
|
security_sensitiveDataLog_info: '查看详情',
|
||||||
|
security_dataRecycle: '数据回收规则管理',
|
||||||
|
security_dataRecycle_index: '查看',
|
||||||
|
security_dataRecycle_add: '添加',
|
||||||
|
security_dataRecycle_edit: '编辑',
|
||||||
|
security_dataRecycle_del: '删除',
|
||||||
|
security_sensitiveData: '敏感字段规则管理',
|
||||||
|
security_sensitiveData_index: '查看',
|
||||||
|
security_sensitiveData_add: '添加',
|
||||||
|
security_sensitiveData_edit: '编辑',
|
||||||
|
security_sensitiveData_del: '删除',
|
||||||
|
buildadmin: 'BuildAdmin',
|
||||||
|
buildadmin_buildadmin: 'BuildAdmin',
|
||||||
|
moduleStore_moduleStore: '模块市场',
|
||||||
|
moduleStore_moduleStore_index: '查看',
|
||||||
|
moduleStore_moduleStore_install: '安装',
|
||||||
|
moduleStore_moduleStore_changeState: '调整状态',
|
||||||
|
moduleStore_moduleStore_uninstall: '卸载',
|
||||||
|
moduleStore_moduleStore_update: '更新',
|
||||||
|
crud_crud: 'CRUD代码生成',
|
||||||
|
crud_crud_index: '查看',
|
||||||
|
crud_crud_generate: '生成',
|
||||||
|
crud_crud_delete: '删除',
|
||||||
|
mall: '积分商城',
|
||||||
|
mall_userAsset: '用户资产',
|
||||||
|
mall_userAsset_index: '查看',
|
||||||
|
mall_userAsset_edit: '编辑',
|
||||||
|
mall_userAsset_del: '删除',
|
||||||
|
mall_address: '收货地址管理',
|
||||||
|
mall_order: '统一订单',
|
||||||
|
mall_order_add: '新增',
|
||||||
|
mall_order_edit: '编辑',
|
||||||
|
mall_order_del: '删除',
|
||||||
|
mall_order_approve: '审核通过',
|
||||||
|
mall_dailyPush: '每日推送',
|
||||||
|
mall_claimLog: '领取记录',
|
||||||
|
mall_item: '商品管理',
|
||||||
|
mall_playxOrder: 'PlayX订单',
|
||||||
|
mall_playxCenter: 'PlayX中心',
|
||||||
|
mall_playxClaimLog: 'PlayX领取记录',
|
||||||
|
mall_playxDailyPush: 'PlayX每日推送',
|
||||||
|
mall_playxUserAsset: 'PlayX用户资产',
|
||||||
|
mall_pintsOrder: '积分订单',
|
||||||
|
mall_redemptionOrder: '兑换订单',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
'Upload administrator': '上传管理员',
|
'Upload administrator': '上传管理员',
|
||||||
'Upload user': '上传会员',
|
|
||||||
'Storage mode': '存储方式',
|
'Storage mode': '存储方式',
|
||||||
'Physical path': '物理路径',
|
'Physical path': '物理路径',
|
||||||
'image width': '图片宽度',
|
'image width': '图片宽度',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<el-sub-menu @click="onClickSubMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
<el-sub-menu @click="onClickSubMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||||
<template #title>
|
<template #title>
|
||||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||||
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span>
|
<span>{{ menuTitleFromRoute(menu) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<MenuTree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children" />
|
<MenuTree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children" />
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
<el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||||
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span>
|
<span>{{ menuTitleFromRoute(menu) }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,6 +23,7 @@ import { ElNotification } from 'element-plus'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
|
import { menuTitleFromRoute } from '/@/utils/menuI18n'
|
||||||
import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router'
|
import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
:ref="tabsRefs.set"
|
:ref="tabsRefs.set"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
>
|
>
|
||||||
{{ item.meta.title }}
|
{{ menuTitleFromRoute(item) }}
|
||||||
<transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in">
|
<transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in">
|
||||||
<Icon v-show="navTabs.state.tabsView.length > 1" class="close-icon" @click.stop="closeTab(item)" size="15" name="el-icon-Close" />
|
<Icon v-show="navTabs.state.tabsView.length > 1" class="close-icon" @click.stop="closeTab(item)" size="15" name="el-icon-Close" />
|
||||||
</transition>
|
</transition>
|
||||||
@@ -29,6 +29,7 @@ import type { ContextMenuItem, ContextMenuItemClickEmitArg } from '/@/components
|
|||||||
import useCurrentInstance from '/@/utils/useCurrentInstance'
|
import useCurrentInstance from '/@/utils/useCurrentInstance'
|
||||||
import Contextmenu from '/@/components/contextmenu/index.vue'
|
import Contextmenu from '/@/components/contextmenu/index.vue'
|
||||||
import horizontalScroll from '/@/utils/horizontalScroll'
|
import horizontalScroll from '/@/utils/horizontalScroll'
|
||||||
|
import { menuTitleFromRoute } from '/@/utils/menuI18n'
|
||||||
import { getFirstRoute, routePush } from '/@/utils/router'
|
import { getFirstRoute, routePush } from '/@/utils/router'
|
||||||
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const useAdminInfo = defineStore('adminInfo', {
|
|||||||
token: '',
|
token: '',
|
||||||
refresh_token: '',
|
refresh_token: '',
|
||||||
super: false,
|
super: false,
|
||||||
channel_id: 0,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
@@ -113,8 +113,6 @@ export interface AdminInfo {
|
|||||||
refresh_token: string
|
refresh_token: string
|
||||||
// 是否是 superAdmin,用于判定是否显示终端按钮等,不做任何权限判断
|
// 是否是 superAdmin,用于判定是否显示终端按钮等,不做任何权限判断
|
||||||
super: boolean
|
super: boolean
|
||||||
// 渠道ID(创建子管理员时默认绑定)
|
|
||||||
channel_id?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ function createAxios<Data = any, T = ApiPromise<Data>>(axiosConfig: AxiosRequest
|
|||||||
timeout: 1000 * 10,
|
timeout: 1000 * 10,
|
||||||
headers: {
|
headers: {
|
||||||
'think-lang': config.lang.defaultLang,
|
'think-lang': config.lang.defaultLang,
|
||||||
server: true,
|
|
||||||
},
|
},
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
})
|
})
|
||||||
|
|||||||
33
web/src/utils/menuI18n.ts
Normal file
33
web/src/utils/menuI18n.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
|
||||||
|
import { i18n } from '/@/lang/index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台菜单/标签标题:优先按路由 name(对应 admin_rule.name)匹配 menu.names.*
|
||||||
|
*/
|
||||||
|
export function menuI18nKeyFromName(name: string | symbol | null | undefined): string {
|
||||||
|
if (name == null || name === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const n = String(name).trim()
|
||||||
|
if (!n) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `menu.names.${n.replace(/\//g, '_')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function menuTitleFromName(name: string | symbol | null | undefined, fallback?: string): string {
|
||||||
|
const key = menuI18nKeyFromName(name)
|
||||||
|
if (key && i18n.global.te(key)) {
|
||||||
|
return String(i18n.global.t(key))
|
||||||
|
}
|
||||||
|
if (fallback && i18n.global.te(fallback)) {
|
||||||
|
return String(i18n.global.t(fallback))
|
||||||
|
}
|
||||||
|
return fallback || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function menuTitleFromRoute(route: RouteRecordRaw | RouteLocationNormalized): string {
|
||||||
|
const name = route.name
|
||||||
|
const metaTitle = route.meta && typeof route.meta.title === 'string' ? route.meta.title : ''
|
||||||
|
return menuTitleFromName(name, metaTitle) || metaTitle || String(i18n.global.t('noTitle'))
|
||||||
|
}
|
||||||
@@ -61,13 +61,6 @@ const baTable = new baTableClass(
|
|||||||
operator: 'RANGE',
|
operator: 'RANGE',
|
||||||
width: 160,
|
width: 160,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: t('auth.admin.channel_name'),
|
|
||||||
prop: 'channel.name',
|
|
||||||
align: 'center',
|
|
||||||
minWidth: 120,
|
|
||||||
operator: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('auth.admin.agent_id'),
|
label: t('auth.admin.agent_id'),
|
||||||
prop: 'agent_id',
|
prop: 'agent_id',
|
||||||
|
|||||||
@@ -41,19 +41,6 @@
|
|||||||
prop="nickname"
|
prop="nickname"
|
||||||
:placeholder="t('Please input field', { field: t('auth.admin.nickname') })"
|
:placeholder="t('Please input field', { field: t('auth.admin.nickname') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
|
||||||
v-if="baTable.form.operate === 'Add' && adminInfo.super"
|
|
||||||
:label="t('auth.admin.channel_id')"
|
|
||||||
v-model="baTable.form.items!.channel_id"
|
|
||||||
prop="channel_id"
|
|
||||||
type="remoteSelect"
|
|
||||||
:input-attr="{
|
|
||||||
pk: 'id',
|
|
||||||
field: 'name',
|
|
||||||
remoteUrl: '/admin/channel.Manage/index',
|
|
||||||
placeholder: t('auth.admin.Please select channel'),
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('auth.admin.group')"
|
:label="t('auth.admin.group')"
|
||||||
v-model="baTable.form.items!.group_arr"
|
v-model="baTable.form.items!.group_arr"
|
||||||
@@ -147,17 +134,6 @@ const { t } = useI18n()
|
|||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||||
username: [buildValidatorData({ name: 'required', title: t('auth.admin.username') }), buildValidatorData({ name: 'account' })],
|
username: [buildValidatorData({ name: 'required', title: t('auth.admin.username') }), buildValidatorData({ name: 'account' })],
|
||||||
nickname: [buildValidatorData({ name: 'required', title: t('auth.admin.nickname') })],
|
nickname: [buildValidatorData({ name: 'required', title: t('auth.admin.nickname') })],
|
||||||
channel_id: [
|
|
||||||
{
|
|
||||||
validator: (_rule: any, val: any, callback: Function) => {
|
|
||||||
if (baTable.form.operate === 'Add' && adminInfo.super && !val) {
|
|
||||||
return callback(new Error(t('auth.admin.Please select channel')))
|
|
||||||
}
|
|
||||||
return callback()
|
|
||||||
},
|
|
||||||
trigger: 'change',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
group_arr: [buildValidatorData({ name: 'required', message: t('Please select field', { field: t('auth.admin.group') }) })],
|
group_arr: [buildValidatorData({ name: 'required', message: t('Please select field', { field: t('auth.admin.group') }) })],
|
||||||
email: [buildValidatorData({ name: 'email', message: t('Please enter the correct field', { field: t('auth.admin.email') }) })],
|
email: [buildValidatorData({ name: 'email', message: t('Please enter the correct field', { field: t('auth.admin.email') }) })],
|
||||||
mobile: [buildValidatorData({ name: 'mobile', message: t('Please enter the correct field', { field: t('auth.admin.mobile') }) })],
|
mobile: [buildValidatorData({ name: 'mobile', message: t('Please enter the correct field', { field: t('auth.admin.mobile') }) })],
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
<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', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
|
||||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('channel.manage.quick Search Fields') })"
|
|
||||||
></TableHeader>
|
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
|
||||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
|
||||||
<Table ref="tableRef"></Table>
|
|
||||||
|
|
||||||
<!-- 表单 -->
|
|
||||||
<PopupForm />
|
|
||||||
<!-- 白名单弹窗 -->
|
|
||||||
<WhitelistPopup v-model:row="whitelistRow" @saved="baTable.getData()" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, provide, ref, useTemplateRef } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import PopupForm from './popupForm.vue'
|
|
||||||
import WhitelistPopup from './whitelistPopup.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: 'channel/manage',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const tableRef = useTemplateRef('tableRef')
|
|
||||||
const whitelistRow = ref<TableRow | null>(null)
|
|
||||||
|
|
||||||
function openWhitelistDialog(row: TableRow) {
|
|
||||||
whitelistRow.value = row
|
|
||||||
}
|
|
||||||
|
|
||||||
const optButtons: OptButton[] = [
|
|
||||||
...defaultOptButtons(['edit', 'delete']),
|
|
||||||
{
|
|
||||||
render: 'tipButton',
|
|
||||||
name: 'whitelist',
|
|
||||||
title: 'channel.manage.whitelist',
|
|
||||||
text: '',
|
|
||||||
type: 'success',
|
|
||||||
icon: 'fa fa-list',
|
|
||||||
class: 'table-row-whitelist',
|
|
||||||
disabledTip: false,
|
|
||||||
display: (_row, _field) => baTable.auth('whitelist'),
|
|
||||||
click: (row: TableRow) => openWhitelistDialog(row),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
|
||||||
*/
|
|
||||||
const baTable = new baTableClass(
|
|
||||||
new baTableApi('/admin/channel.Manage/'),
|
|
||||||
{
|
|
||||||
pk: 'id',
|
|
||||||
column: [
|
|
||||||
{ type: 'selection', align: 'center', operator: false },
|
|
||||||
{ label: t('channel.manage.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
|
||||||
{
|
|
||||||
label: t('channel.manage.name'),
|
|
||||||
prop: 'name',
|
|
||||||
align: 'center',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.title'),
|
|
||||||
prop: 'title',
|
|
||||||
align: 'center',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.admin__username'),
|
|
||||||
prop: 'admin.username',
|
|
||||||
align: 'center',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
render: 'tags',
|
|
||||||
operator: 'LIKE',
|
|
||||||
comSearchRender: 'string',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.secret'),
|
|
||||||
prop: 'secret',
|
|
||||||
align: 'center',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.remark'),
|
|
||||||
prop: 'remark',
|
|
||||||
align: 'center',
|
|
||||||
minWidth: '100',
|
|
||||||
showOverflowTooltip: true,
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.ip_white'),
|
|
||||||
prop: 'ip_white',
|
|
||||||
align: 'center',
|
|
||||||
minWidth: '120',
|
|
||||||
render: 'tags',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.create_time'),
|
|
||||||
prop: 'create_time',
|
|
||||||
align: 'center',
|
|
||||||
render: 'datetime',
|
|
||||||
operator: 'RANGE',
|
|
||||||
comSearchRender: 'datetime',
|
|
||||||
sortable: 'custom',
|
|
||||||
width: 160,
|
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('channel.manage.update_time'),
|
|
||||||
prop: 'update_time',
|
|
||||||
align: 'center',
|
|
||||||
render: 'datetime',
|
|
||||||
operator: 'RANGE',
|
|
||||||
comSearchRender: 'datetime',
|
|
||||||
sortable: 'custom',
|
|
||||||
width: 160,
|
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
|
||||||
},
|
|
||||||
{ label: t('Operate'), align: 'center', width: 130, render: 'buttons', buttons: optButtons, operator: false },
|
|
||||||
],
|
|
||||||
dblClickNotEditColumn: [undefined],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultItems: { ip_white: [] },
|
|
||||||
before: {
|
|
||||||
onSubmit: () => {
|
|
||||||
const items = baTable.form.items
|
|
||||||
if (!items) return
|
|
||||||
// 从 popupForm 的 ipWhiteList 同步到 form.items(避免 watch 循环)
|
|
||||||
const ipWhiteRef = baTable.form.extend?.ipWhiteListRef
|
|
||||||
if (ipWhiteRef?.value) {
|
|
||||||
items.ip_white = ipWhiteRef.value.filter((ip: string) => ip && String(ip).trim() !== '')
|
|
||||||
} else if (Array.isArray(items.ip_white)) {
|
|
||||||
items.ip_white = items.ip_white.filter((ip: string) => ip && String(ip).trim() !== '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
provide('baTable', baTable)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
baTable.table.ref = tableRef.value
|
|
||||||
baTable.mount()
|
|
||||||
baTable.getData()?.then(() => {
|
|
||||||
baTable.initSort()
|
|
||||||
baTable.dragSort()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- 对话框表单 -->
|
|
||||||
<!-- 建议使用 Prettier 格式化代码 -->
|
|
||||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
:label="t('channel.manage.name')"
|
|
||||||
type="string"
|
|
||||||
v-model="baTable.form.items!.name"
|
|
||||||
prop="name"
|
|
||||||
:placeholder="t('Please input field', { field: t('channel.manage.name') })"
|
|
||||||
/>
|
|
||||||
<el-form-item :label="t('channel.manage.ip_white')" prop="ip_white">
|
|
||||||
<div class="ba-ip-white-list">
|
|
||||||
<div class="ba-ip-white-item" v-for="(ip, idx) in ipWhiteList" :key="idx">
|
|
||||||
<el-input v-model="ipWhiteList[idx]" :placeholder="t('channel.manage.ip_placeholder')" clearable />
|
|
||||||
<el-button @click="onDelIpWhite(idx)" size="small" icon="el-icon-Delete" circle />
|
|
||||||
</div>
|
|
||||||
<el-button v-blur class="ba-add-ip-white" @click="onAddIpWhite" icon="el-icon-Plus">{{ t('Add') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<FormItem
|
|
||||||
:label="t('channel.manage.title')"
|
|
||||||
type="string"
|
|
||||||
v-model="baTable.form.items!.title"
|
|
||||||
prop="title"
|
|
||||||
:placeholder="t('Please input field', { field: t('channel.manage.title') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('channel.manage.remark')"
|
|
||||||
type="textarea"
|
|
||||||
v-model="baTable.form.items!.remark"
|
|
||||||
prop="remark"
|
|
||||||
:input-attr="{ rows: 3 }"
|
|
||||||
@keyup.enter.stop=""
|
|
||||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
|
||||||
:placeholder="t('Please input field', { field: t('channel.manage.remark') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('channel.manage.admin_id')"
|
|
||||||
type="remoteSelect"
|
|
||||||
v-model="baTable.form.items!.admin_id"
|
|
||||||
prop="admin_id"
|
|
||||||
:input-attr="{ pk: 'admin.id', field: 'username', remoteUrl: '/admin/auth.Admin/index' }"
|
|
||||||
:placeholder="t('Please select field', { field: t('channel.manage.admin_id') })"
|
|
||||||
/>
|
|
||||||
</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">
|
|
||||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { FormItemRule } from 'element-plus'
|
|
||||||
import { inject, reactive, ref, useTemplateRef, watch } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
|
||||||
import { useConfig } from '/@/stores/config'
|
|
||||||
import type baTableClass from '/@/utils/baTable'
|
|
||||||
import { buildValidatorData } from '/@/utils/validate'
|
|
||||||
|
|
||||||
const config = useConfig()
|
|
||||||
const formRef = useTemplateRef('formRef')
|
|
||||||
const baTable = inject('baTable') as baTableClass
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
/** 将后端数据转为 IP 字符串数组(兼容旧格式 [{key,value}] 和新格式 [ip]) */
|
|
||||||
function normalizeIpWhite(val: unknown): string[] {
|
|
||||||
if (!val || !Array.isArray(val)) return []
|
|
||||||
return val.map((item) => (typeof item === 'string' ? item : (item?.value ?? '')))
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipWhiteList = ref<string[]>([])
|
|
||||||
|
|
||||||
// 仅当表单加载时从 form.items 同步到 ipWhiteList,避免双向 watch 导致循环更新
|
|
||||||
watch(
|
|
||||||
() => baTable.form.items?.ip_white,
|
|
||||||
(val) => {
|
|
||||||
ipWhiteList.value = normalizeIpWhite(val)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const onAddIpWhite = () => {
|
|
||||||
ipWhiteList.value.push('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDelIpWhite = (idx: number) => {
|
|
||||||
ipWhiteList.value.splice(idx, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将 ipWhiteList 暴露给 baTable,供提交时同步
|
|
||||||
if (!baTable.form.extend) baTable.form.extend = {}
|
|
||||||
baTable.form.extend.ipWhiteListRef = ipWhiteList
|
|
||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|
||||||
name: [buildValidatorData({ name: 'required', title: t('channel.manage.name') })],
|
|
||||||
title: [buildValidatorData({ name: 'required', title: t('channel.manage.title') })],
|
|
||||||
admin_id: [buildValidatorData({ name: 'required', title: t('channel.manage.admin_id') })],
|
|
||||||
create_time: [buildValidatorData({ name: 'date', title: t('channel.manage.create_time') })],
|
|
||||||
update_time: [buildValidatorData({ name: 'date', title: t('channel.manage.update_time') })],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.ba-ip-white-list {
|
|
||||||
.ba-ip-white-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
.el-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ba-add-ip-white {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="!!row" :title="t('channel.manage.whitelist')" @close="close">
|
|
||||||
<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
|
|
||||||
<div class="ba-operate-form">
|
|
||||||
<div class="ba-ip-white-list">
|
|
||||||
<div class="ba-ip-white-item" v-for="(ip, idx) in ipWhiteList" :key="idx">
|
|
||||||
<el-input v-model="ipWhiteList[idx]" :placeholder="t('channel.manage.ip_placeholder')" clearable />
|
|
||||||
<el-button @click="onDelIpWhite(idx)" size="small" icon="el-icon-Delete" circle />
|
|
||||||
</div>
|
|
||||||
<el-button v-blur class="ba-add-ip-white" @click="onAddIpWhite" icon="el-icon-Plus">{{ t('Add') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="close">{{ t('Cancel') }}</el-button>
|
|
||||||
<el-button v-blur :loading="submitLoading" @click="onSave" type="primary">{{ t('Save') }}</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { baTableApi } from '/@/api/common'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
row: TableRow | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:row', val: TableRow | null): void
|
|
||||||
(e: 'saved'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const api = new baTableApi('/admin/channel.Manage/')
|
|
||||||
const loading = ref(false)
|
|
||||||
const submitLoading = ref(false)
|
|
||||||
|
|
||||||
/** 将后端数据转为 IP 字符串数组(兼容旧格式 [{key,value}] 和新格式 [ip]) */
|
|
||||||
function normalizeIpWhite(val: unknown): string[] {
|
|
||||||
if (!val || !Array.isArray(val)) return []
|
|
||||||
return val.map((item) => (typeof item === 'string' ? item : (item?.value ?? '')))
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipWhiteList = ref<string[]>([])
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.row,
|
|
||||||
(val) => {
|
|
||||||
ipWhiteList.value = val ? normalizeIpWhite(val.ip_white) : []
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const onAddIpWhite = () => {
|
|
||||||
ipWhiteList.value.push('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDelIpWhite = (idx: number) => {
|
|
||||||
ipWhiteList.value.splice(idx, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
emit('update:row', null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSave = async () => {
|
|
||||||
if (!props.row) return
|
|
||||||
const ips = ipWhiteList.value.filter((ip) => ip.trim() !== '')
|
|
||||||
submitLoading.value = true
|
|
||||||
try {
|
|
||||||
await api.postData('edit', {
|
|
||||||
id: props.row.id,
|
|
||||||
ip_white: ips,
|
|
||||||
})
|
|
||||||
emit('saved')
|
|
||||||
close()
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.ba-ip-white-list {
|
|
||||||
.ba-ip-white-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
.el-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ba-add-ip-white {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -28,119 +28,88 @@
|
|||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :sm="12" :lg="6">
|
<el-col :sm="12" :lg="6">
|
||||||
<div class="small-panel user-reg suspension">
|
<div class="small-panel user-reg suspension">
|
||||||
<div class="small-panel-title">{{ t('dashboard.Member registration') }}</div>
|
<div class="small-panel-title">{{ t('dashboard.Daily new players') }}</div>
|
||||||
<div class="small-panel-content">
|
<div class="small-panel-content">
|
||||||
<div class="content-left">
|
<div class="content-left">
|
||||||
<Icon color="#8595F4" size="20" name="fa fa-line-chart" />
|
<Icon color="#8595F4" size="20" name="fa fa-line-chart" />
|
||||||
<el-statistic :value="userRegNumberOutput" :value-style="statisticValueStyle" />
|
<el-statistic :value="userRegNumberOutput" :value-style="statisticValueStyle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content-right">+14%</div>
|
<div class="content-right color-info">{{ t('dashboard.Today') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :lg="6">
|
<el-col :sm="12" :lg="6">
|
||||||
<div class="small-panel file suspension">
|
<div class="small-panel file suspension">
|
||||||
<div class="small-panel-title">{{ t('dashboard.Number of attachments Uploaded') }}</div>
|
<div class="small-panel-title">{{ t('dashboard.Yesterday points') }}</div>
|
||||||
<div class="small-panel-content">
|
<div class="small-panel-content">
|
||||||
<div class="content-left">
|
<div class="content-left">
|
||||||
<Icon color="#AD85F4" size="20" name="fa fa-file-text" />
|
<Icon color="#AD85F4" size="20" name="fa fa-file-text" />
|
||||||
<el-statistic :value="fileNumberOutput" :value-style="statisticValueStyle" />
|
<el-statistic :value="fileNumberOutput" :value-style="statisticValueStyle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content-right">+50%</div>
|
<div class="content-right color-info">{{ t('dashboard.Yesterday') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :lg="6">
|
<el-col :sm="12" :lg="6">
|
||||||
<div class="small-panel users suspension">
|
<div class="small-panel users suspension">
|
||||||
<div class="small-panel-title">{{ t('dashboard.Total number of members') }}</div>
|
<div class="small-panel-title">{{ t('dashboard.Yesterday redeem') }}</div>
|
||||||
<div class="small-panel-content">
|
<div class="small-panel-content">
|
||||||
<div class="content-left">
|
<div class="content-left">
|
||||||
<Icon color="#74A8B5" size="20" name="fa fa-users" />
|
<Icon color="#74A8B5" size="20" name="fa fa-users" />
|
||||||
<el-statistic :value="usersNumberOutput" :value-style="statisticValueStyle" />
|
<el-statistic :value="usersNumberOutput" :value-style="statisticValueStyle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content-right">+28%</div>
|
<div class="content-right color-info">{{ t('dashboard.Orders') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="12" :lg="6">
|
<el-col :sm="12" :lg="6">
|
||||||
<div class="small-panel addons suspension">
|
<div class="small-panel addons suspension">
|
||||||
<div class="small-panel-title">{{ t('dashboard.Number of installed plug-ins') }}</div>
|
<div class="small-panel-title">{{ t('dashboard.Pending physical to ship') }}</div>
|
||||||
<div class="small-panel-content">
|
<div class="small-panel-content">
|
||||||
<div class="content-left">
|
<div class="content-left">
|
||||||
<Icon color="#F48595" size="20" name="fa fa-object-group" />
|
<Icon color="#F48595" size="20" name="fa fa-object-group" />
|
||||||
<el-statistic :value="addonsNumberOutput" :value-style="statisticValueStyle" />
|
<el-statistic :value="addonsNumberOutput" :value-style="statisticValueStyle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content-right">+88%</div>
|
<div class="content-right color-info">{{ t('dashboard.Pending') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
<div class="growth-chart">
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="12" :lg="9">
|
|
||||||
<el-card shadow="hover" :header="t('dashboard.Membership growth')">
|
|
||||||
<div class="user-growth-chart" :ref="chartRefs.set"></div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="12" :lg="9">
|
|
||||||
<el-card shadow="hover" :header="t('dashboard.Annex growth')">
|
|
||||||
<div class="file-growth-chart" :ref="chartRefs.set"></div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="24" :md="24" :lg="6">
|
|
||||||
<el-card class="new-user-card" shadow="hover" :header="t('dashboard.New member')">
|
|
||||||
<div class="new-user-growth">
|
|
||||||
<el-scrollbar>
|
|
||||||
<div class="new-user-item">
|
|
||||||
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
|
|
||||||
<div class="new-user-base">
|
|
||||||
<div class="new-user-name">妙码生花</div>
|
|
||||||
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
|
|
||||||
</div>
|
|
||||||
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
|
|
||||||
</div>
|
|
||||||
<div class="new-user-item">
|
|
||||||
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
|
|
||||||
<div class="new-user-base">
|
|
||||||
<div class="new-user-name">码上生花</div>
|
|
||||||
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
|
|
||||||
</div>
|
|
||||||
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
|
|
||||||
</div>
|
|
||||||
<div class="new-user-item">
|
|
||||||
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
|
|
||||||
<div class="new-user-base">
|
|
||||||
<div class="new-user-name">Admin</div>
|
|
||||||
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
|
|
||||||
</div>
|
|
||||||
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
|
|
||||||
</div>
|
|
||||||
<div class="new-user-item">
|
|
||||||
<img class="new-user-avatar" :src="fullUrl('/static/images/avatar.png')" alt="" />
|
|
||||||
<div class="new-user-base">
|
|
||||||
<div class="new-user-name">纯属虚构</div>
|
|
||||||
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
|
|
||||||
</div>
|
|
||||||
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="growth-chart">
|
<div class="growth-chart">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="12">
|
<el-col :xs="24" :sm="24" :md="24" :lg="24">
|
||||||
<el-card shadow="hover" :header="t('dashboard.Member source')">
|
<el-card shadow="hover" :header="t('dashboard.Yesterday item redeem stat')">
|
||||||
<div class="user-source-chart" :ref="chartRefs.set"></div>
|
<div class="playx-kpis">
|
||||||
</el-card>
|
<div class="playx-kpi">
|
||||||
</el-col>
|
<div class="playx-kpi-title">{{ t('dashboard.Yesterday redeem points sum') }}</div>
|
||||||
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="12">
|
<div class="playx-kpi-value">{{ state.playx?.yesterday_redeem?.points_cost_sum ?? 0 }}</div>
|
||||||
<el-card shadow="hover" :header="t('dashboard.Member last name')">
|
</div>
|
||||||
<div class="user-surname-chart" :ref="chartRefs.set"></div>
|
<div class="playx-kpi">
|
||||||
|
<div class="playx-kpi-title">{{ t('dashboard.Yesterday redeem amount sum') }}</div>
|
||||||
|
<div class="playx-kpi-value">{{ state.playx?.yesterday_redeem?.amount_sum ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="playx-kpi">
|
||||||
|
<div class="playx-kpi-title">{{ t('dashboard.Grant failed retryable') }}</div>
|
||||||
|
<div class="playx-kpi-value">{{ state.playx?.grant_failed_retryable ?? 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="state.playxLoading"
|
||||||
|
:data="state.playx?.yesterday_redeem?.by_item ?? []"
|
||||||
|
size="small"
|
||||||
|
style="width: 100%; margin-top: 12px"
|
||||||
|
>
|
||||||
|
<el-table-column prop="mall_item_id" :label="t('dashboard.Item ID')" width="100" />
|
||||||
|
<el-table-column prop="title" :label="t('dashboard.Item title')" min-width="220" />
|
||||||
|
<el-table-column prop="order_count" :label="t('dashboard.Order count')" width="120" />
|
||||||
|
<el-table-column prop="completed_count" :label="t('dashboard.Completed')" width="120" />
|
||||||
|
<el-table-column prop="rejected_count" :label="t('dashboard.Rejected')" width="120" />
|
||||||
|
<el-table-column prop="points_cost_sum" :label="t('dashboard.Points sum')" width="140" />
|
||||||
|
<el-table-column prop="amount_sum" :label="t('dashboard.Amount sum')" width="140" />
|
||||||
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -149,17 +118,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener, useTemplateRefsList, useTransition } from '@vueuse/core'
|
import { useTransition } from '@vueuse/core'
|
||||||
import * as echarts from 'echarts'
|
import { CSSProperties, onMounted, onUnmounted, reactive, toRefs } from 'vue'
|
||||||
import { CSSProperties, nextTick, onActivated, onBeforeMount, onMounted, onUnmounted, reactive, toRefs, watch } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { index } from '/@/api/backend/dashboard'
|
import { index } from '/@/api/backend/dashboard'
|
||||||
import coffeeSvg from '/@/assets/dashboard/coffee.svg'
|
import coffeeSvg from '/@/assets/dashboard/coffee.svg'
|
||||||
import headerSvg from '/@/assets/dashboard/header-1.svg'
|
import headerSvg from '/@/assets/dashboard/header-1.svg'
|
||||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||||
import { WORKING_TIME } from '/@/stores/constant/cacheKey'
|
import { WORKING_TIME } from '/@/stores/constant/cacheKey'
|
||||||
import { useNavTabs } from '/@/stores/navTabs'
|
import { getGreet } from '/@/utils/common'
|
||||||
import { fullUrl, getGreet } from '/@/utils/common'
|
|
||||||
import { Local } from '/@/utils/storage'
|
import { Local } from '/@/utils/storage'
|
||||||
let workTimer: number
|
let workTimer: number
|
||||||
|
|
||||||
@@ -169,20 +136,20 @@ defineOptions({
|
|||||||
|
|
||||||
const d = new Date()
|
const d = new Date()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const navTabs = useNavTabs()
|
|
||||||
const adminInfo = useAdminInfo()
|
const adminInfo = useAdminInfo()
|
||||||
const chartRefs = useTemplateRefsList<HTMLDivElement>()
|
|
||||||
|
|
||||||
const state: {
|
const state: {
|
||||||
charts: any[]
|
|
||||||
remark: string
|
remark: string
|
||||||
workingTimeFormat: string
|
workingTimeFormat: string
|
||||||
pauseWork: boolean
|
pauseWork: boolean
|
||||||
|
playx: any | null
|
||||||
|
playxLoading: boolean
|
||||||
} = reactive({
|
} = reactive({
|
||||||
charts: [],
|
|
||||||
remark: 'dashboard.Loading',
|
remark: 'dashboard.Loading',
|
||||||
workingTimeFormat: '',
|
workingTimeFormat: '',
|
||||||
pauseWork: false,
|
pauseWork: false,
|
||||||
|
playx: null,
|
||||||
|
playxLoading: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,301 +173,20 @@ const statisticValueStyle: CSSProperties = {
|
|||||||
|
|
||||||
index().then((res) => {
|
index().then((res) => {
|
||||||
state.remark = res.data.remark
|
state.remark = res.data.remark
|
||||||
|
state.playx = res.data.playx ?? null
|
||||||
|
state.playxLoading = false
|
||||||
|
initCountUp()
|
||||||
|
}).catch(() => {
|
||||||
|
state.playxLoading = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const initCountUp = () => {
|
const initCountUp = () => {
|
||||||
// 虚拟数据
|
const playx = state.playx ?? {}
|
||||||
countUpRefs.userRegNumber.value = 5456
|
const yesterdayRedeem = playx.yesterday_redeem ?? {}
|
||||||
countUpRefs.fileNumber.value = 1234
|
countUpRefs.userRegNumber.value = playx.new_players_today ?? 0
|
||||||
countUpRefs.usersNumber.value = 9486
|
countUpRefs.fileNumber.value = playx.yesterday_points_claimed ?? 0
|
||||||
countUpRefs.addonsNumber.value = 875
|
countUpRefs.usersNumber.value = yesterdayRedeem.order_count ?? 0
|
||||||
}
|
countUpRefs.addonsNumber.value = playx.pending_physical_to_ship ?? 0
|
||||||
|
|
||||||
const initUserGrowthChart = () => {
|
|
||||||
const userGrowthChart = echarts.init(chartRefs.value[0] as HTMLElement)
|
|
||||||
const option = {
|
|
||||||
grid: {
|
|
||||||
top: 40,
|
|
||||||
right: 0,
|
|
||||||
bottom: 20,
|
|
||||||
left: 40,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
data: [
|
|
||||||
t('dashboard.Monday'),
|
|
||||||
t('dashboard.Tuesday'),
|
|
||||||
t('dashboard.Wednesday'),
|
|
||||||
t('dashboard.Thursday'),
|
|
||||||
t('dashboard.Friday'),
|
|
||||||
t('dashboard.Saturday'),
|
|
||||||
t('dashboard.Sunday'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
yAxis: {},
|
|
||||||
legend: {
|
|
||||||
data: [t('dashboard.Visits'), t('dashboard.Registration volume')],
|
|
||||||
textStyle: {
|
|
||||||
color: '#73767a',
|
|
||||||
},
|
|
||||||
top: 0,
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: t('dashboard.Visits'),
|
|
||||||
data: [100, 160, 280, 230, 190, 200, 480],
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {
|
|
||||||
color: '#8595F4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('dashboard.Registration volume'),
|
|
||||||
data: [45, 180, 146, 99, 210, 127, 288],
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {
|
|
||||||
color: '#F48595',
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
userGrowthChart.setOption(option)
|
|
||||||
state.charts.push(userGrowthChart)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initFileGrowthChart = () => {
|
|
||||||
const fileGrowthChart = echarts.init(chartRefs.value[1] as HTMLElement)
|
|
||||||
const option = {
|
|
||||||
grid: {
|
|
||||||
top: 30,
|
|
||||||
right: 0,
|
|
||||||
bottom: 20,
|
|
||||||
left: 0,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
type: 'scroll',
|
|
||||||
bottom: 0,
|
|
||||||
data: (function () {
|
|
||||||
var list = []
|
|
||||||
for (var i = 1; i <= 28; i++) {
|
|
||||||
list.push(i + 2000 + '')
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
})(),
|
|
||||||
textStyle: {
|
|
||||||
color: '#73767a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visualMap: {
|
|
||||||
top: 'middle',
|
|
||||||
right: 10,
|
|
||||||
color: ['red', 'yellow'],
|
|
||||||
calculable: true,
|
|
||||||
},
|
|
||||||
radar: {
|
|
||||||
indicator: [
|
|
||||||
{ name: t('dashboard.picture') },
|
|
||||||
{ name: t('dashboard.file') },
|
|
||||||
{ name: t('dashboard.table') },
|
|
||||||
{ name: t('dashboard.Compressed package') },
|
|
||||||
{ name: t('dashboard.other') },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
series: (function () {
|
|
||||||
var series = []
|
|
||||||
for (var i = 1; i <= 28; i++) {
|
|
||||||
series.push({
|
|
||||||
type: 'radar',
|
|
||||||
symbol: 'none',
|
|
||||||
lineStyle: {
|
|
||||||
width: 1,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
areaStyle: {
|
|
||||||
color: 'rgba(0,250,0,0.3)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: [(40 - i) * 10, (38 - i) * 4 + 60, i * 5 + 10, i * 9, (i * i) / 2],
|
|
||||||
name: i + 2000 + '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return series
|
|
||||||
})(),
|
|
||||||
}
|
|
||||||
fileGrowthChart.setOption(option)
|
|
||||||
state.charts.push(fileGrowthChart)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initUserSourceChart = () => {
|
|
||||||
const UserSourceChart = echarts.init(chartRefs.value[2] as HTMLElement)
|
|
||||||
const pathSymbols = {
|
|
||||||
reindeer:
|
|
||||||
'path://M-22.788,24.521c2.08-0.986,3.611-3.905,4.984-5.892 c-2.686,2.782-5.047,5.884-9.102,7.312c-0.992,0.005-0.25-2.016,0.34-2.362l1.852-0.41c0.564-0.218,0.785-0.842,0.902-1.347 c2.133-0.727,4.91-4.129,6.031-6.194c1.748-0.7,4.443-0.679,5.734-2.293c1.176-1.468,0.393-3.992,1.215-6.557 c0.24-0.754,0.574-1.581,1.008-2.293c-0.611,0.011-1.348-0.061-1.959-0.608c-1.391-1.245-0.785-2.086-1.297-3.313 c1.684,0.744,2.5,2.584,4.426,2.586C-8.46,3.012-8.255,2.901-8.04,2.824c6.031-1.952,15.182-0.165,19.498-3.937 c1.15-3.933-1.24-9.846-1.229-9.938c0.008-0.062-1.314-0.004-1.803-0.258c-1.119-0.771-6.531-3.75-0.17-3.33 c0.314-0.045,0.943,0.259,1.439,0.435c-0.289-1.694-0.92-0.144-3.311-1.946c0,0-1.1-0.855-1.764-1.98 c-0.836-1.09-2.01-2.825-2.992-4.031c-1.523-2.476,1.367,0.709,1.816,1.108c1.768,1.704,1.844,3.281,3.232,3.983 c0.195,0.203,1.453,0.164,0.926-0.468c-0.525-0.632-1.367-1.278-1.775-2.341c-0.293-0.703-1.311-2.326-1.566-2.711 c-0.256-0.384-0.959-1.718-1.67-2.351c-1.047-1.187-0.268-0.902,0.521-0.07c0.789,0.834,1.537,1.821,1.672,2.023 c0.135,0.203,1.584,2.521,1.725,2.387c0.102-0.259-0.035-0.428-0.158-0.852c-0.125-0.423-0.912-2.032-0.961-2.083 c-0.357-0.852-0.566-1.908-0.598-3.333c0.4-2.375,0.648-2.486,0.549-0.705c0.014,1.143,0.031,2.215,0.602,3.247 c0.807,1.496,1.764,4.064,1.836,4.474c0.561,3.176,2.904,1.749,2.281-0.126c-0.068-0.446-0.109-2.014-0.287-2.862 c-0.18-0.849-0.219-1.688-0.113-3.056c0.066-1.389,0.232-2.055,0.277-2.299c0.285-1.023,0.4-1.088,0.408,0.135 c-0.059,0.399-0.131,1.687-0.125,2.655c0.064,0.642-0.043,1.768,0.172,2.486c0.654,1.928-0.027,3.496,1,3.514 c1.805-0.424,2.428-1.218,2.428-2.346c-0.086-0.704-0.121-0.843-0.031-1.193c0.221-0.568,0.359-0.67,0.312-0.076 c-0.055,0.287,0.031,0.533,0.082,0.794c0.264,1.197,0.912,0.114,1.283-0.782c0.15-0.238,0.539-2.154,0.545-2.522 c-0.023-0.617,0.285-0.645,0.309,0.01c0.064,0.422-0.248,2.646-0.205,2.334c-0.338,1.24-1.105,3.402-3.379,4.712 c-0.389,0.12-1.186,1.286-3.328,2.178c0,0,1.729,0.321,3.156,0.246c1.102-0.19,3.707-0.027,4.654,0.269 c1.752,0.494,1.531-0.053,4.084,0.164c2.26-0.4,2.154,2.391-1.496,3.68c-2.549,1.405-3.107,1.475-2.293,2.984 c3.484,7.906,2.865,13.183,2.193,16.466c2.41,0.271,5.732-0.62,7.301,0.725c0.506,0.333,0.648,1.866-0.457,2.86 c-4.105,2.745-9.283,7.022-13.904,7.662c-0.977-0.194,0.156-2.025,0.803-2.247l1.898-0.03c0.596-0.101,0.936-0.669,1.152-1.139 c3.16-0.404,5.045-3.775,8.246-4.818c-4.035-0.718-9.588,3.981-12.162,1.051c-5.043,1.423-11.449,1.84-15.895,1.111 c-3.105,2.687-7.934,4.021-12.115,5.866c-3.271,3.511-5.188,8.086-9.967,10.414c-0.986,0.119-0.48-1.974,0.066-2.385l1.795-0.618 C-22.995,25.682-22.849,25.035-22.788,24.521z',
|
|
||||||
plane: 'path://M1.112,32.559l2.998,1.205l-2.882,2.268l-2.215-0.012L1.112,32.559z M37.803,23.96 c0.158-0.838,0.5-1.509,0.961-1.904c-0.096-0.037-0.205-0.071-0.344-0.071c-0.777-0.005-2.068-0.009-3.047-0.009 c-0.633,0-1.217,0.066-1.754,0.18l2.199,1.804H37.803z M39.738,23.036c-0.111,0-0.377,0.325-0.537,0.924h1.076 C40.115,23.361,39.854,23.036,39.738,23.036z M39.934,39.867c-0.166,0-0.674,0.705-0.674,1.986s0.506,1.986,0.674,1.986 s0.672-0.705,0.672-1.986S40.102,39.867,39.934,39.867z M38.963,38.889c-0.098-0.038-0.209-0.07-0.348-0.073 c-0.082,0-0.174,0-0.268-0.001l-7.127,4.671c0.879,0.821,2.42,1.417,4.348,1.417c0.979,0,2.27-0.006,3.047-0.01 c0.139,0,0.25-0.034,0.348-0.072c-0.646-0.555-1.07-1.643-1.07-2.967C37.891,40.529,38.316,39.441,38.963,38.889z M32.713,23.96 l-12.37-10.116l-4.693-0.004c0,0,4,8.222,4.827,10.121H32.713z M59.311,32.374c-0.248,2.104-5.305,3.172-8.018,3.172H39.629 l-25.325,16.61L9.607,52.16c0,0,6.687-8.479,7.95-10.207c1.17-1.6,3.019-3.699,3.027-6.407h-2.138 c-5.839,0-13.816-3.789-18.472-5.583c-2.818-1.085-2.396-4.04-0.031-4.04h0.039l-3.299-11.371h3.617c0,0,4.352,5.696,5.846,7.5 c2,2.416,4.503,3.678,8.228,3.87h30.727c2.17,0,4.311,0.417,6.252,1.046c3.49,1.175,5.863,2.7,7.199,4.027 C59.145,31.584,59.352,32.025,59.311,32.374z M22.069,30.408c0-0.815-0.661-1.475-1.469-1.475c-0.812,0-1.471,0.66-1.471,1.475 s0.658,1.475,1.471,1.475C21.408,31.883,22.069,31.224,22.069,30.408z M27.06,30.408c0-0.815-0.656-1.478-1.466-1.478 c-0.812,0-1.471,0.662-1.471,1.478s0.658,1.477,1.471,1.477C26.404,31.885,27.06,31.224,27.06,30.408z M32.055,30.408 c0-0.815-0.66-1.475-1.469-1.475c-0.808,0-1.466,0.66-1.466,1.475s0.658,1.475,1.466,1.475 C31.398,31.883,32.055,31.224,32.055,30.408z M37.049,30.408c0-0.815-0.658-1.478-1.467-1.478c-0.812,0-1.469,0.662-1.469,1.478 s0.656,1.477,1.469,1.477C36.389,31.885,37.049,31.224,37.049,30.408z M42.039,30.408c0-0.815-0.656-1.478-1.465-1.478 c-0.811,0-1.469,0.662-1.469,1.478s0.658,1.477,1.469,1.477C41.383,31.885,42.039,31.224,42.039,30.408z M55.479,30.565 c-0.701-0.436-1.568-0.896-2.627-1.347c-0.613,0.289-1.551,0.476-2.73,0.476c-1.527,0-1.639,2.263,0.164,2.316 C52.389,32.074,54.627,31.373,55.479,30.565z',
|
|
||||||
rocket: 'path://M-244.396,44.399c0,0,0.47-2.931-2.427-6.512c2.819-8.221,3.21-15.709,3.21-15.709s5.795,1.383,5.795,7.325C-237.818,39.679-244.396,44.399-244.396,44.399z M-260.371,40.827c0,0-3.881-12.946-3.881-18.319c0-2.416,0.262-4.566,0.669-6.517h17.684c0.411,1.952,0.675,4.104,0.675,6.519c0,5.291-3.87,18.317-3.87,18.317H-260.371z M-254.745,18.951c-1.99,0-3.603,1.676-3.603,3.744c0,2.068,1.612,3.744,3.603,3.744c1.988,0,3.602-1.676,3.602-3.744S-252.757,18.951-254.745,18.951z M-255.521,2.228v-5.098h1.402v4.969c1.603,1.213,5.941,5.069,7.901,12.5h-17.05C-261.373,7.373-257.245,3.558-255.521,2.228zM-265.07,44.399c0,0-6.577-4.721-6.577-14.896c0-5.942,5.794-7.325,5.794-7.325s0.393,7.488,3.211,15.708C-265.539,41.469-265.07,44.399-265.07,44.399z M-252.36,45.15l-1.176-1.22L-254.789,48l-1.487-4.069l-1.019,2.116l-1.488-3.826h8.067L-252.36,45.15z',
|
|
||||||
train: 'path://M67.335,33.596L67.335,33.596c-0.002-1.39-1.153-3.183-3.328-4.218h-9.096v-2.07h5.371 c-4.939-2.07-11.199-4.141-14.89-4.141H19.72v12.421v5.176h38.373c4.033,0,8.457-1.035,9.142-5.176h-0.027 c0.076-0.367,0.129-0.751,0.129-1.165L67.335,33.596L67.335,33.596z M27.999,30.413h-3.105v-4.141h3.105V30.413z M35.245,30.413 h-3.104v-4.141h3.104V30.413z M42.491,30.413h-3.104v-4.141h3.104V30.413z M49.736,30.413h-3.104v-4.141h3.104V30.413z M14.544,40.764c1.143,0,2.07-0.927,2.07-2.07V35.59V25.237c0-1.145-0.928-2.07-2.07-2.07H-9.265c-1.143,0-2.068,0.926-2.068,2.07 v10.351v3.105c0,1.144,0.926,2.07,2.068,2.07H14.544L14.544,40.764z M8.333,26.272h3.105v4.141H8.333V26.272z M1.087,26.272h3.105 v4.141H1.087V26.272z M-6.159,26.272h3.105v4.141h-3.105V26.272z M-9.265,41.798h69.352v1.035H-9.265V41.798z',
|
|
||||||
}
|
|
||||||
const option = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'none',
|
|
||||||
},
|
|
||||||
formatter: function (params: any) {
|
|
||||||
return params[0].name + ': ' + params[0].value
|
|
||||||
},
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
data: [t('dashboard.Baidu'), t('dashboard.Direct access'), t('dashboard.take a plane'), t('dashboard.Take the high-speed railway')],
|
|
||||||
axisTick: { show: false },
|
|
||||||
axisLine: { show: false },
|
|
||||||
axisLabel: {
|
|
||||||
color: '#e54035',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
splitLine: { show: false },
|
|
||||||
axisTick: { show: false },
|
|
||||||
axisLine: { show: false },
|
|
||||||
axisLabel: { show: false },
|
|
||||||
},
|
|
||||||
color: ['#e54035'],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'hill',
|
|
||||||
type: 'pictorialBar',
|
|
||||||
barCategoryGap: '-130%',
|
|
||||||
symbol: 'path://M0,10 L10,10 C5.5,10 5.5,5 5,0 C4.5,5 4.5,10 0,10 z',
|
|
||||||
itemStyle: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: [123, 60, 25, 80],
|
|
||||||
z: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'glyph',
|
|
||||||
type: 'pictorialBar',
|
|
||||||
barGap: '-100%',
|
|
||||||
symbolPosition: 'end',
|
|
||||||
symbolSize: 50,
|
|
||||||
symbolOffset: [0, '-120%'],
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: 123,
|
|
||||||
symbol: pathSymbols.reindeer,
|
|
||||||
symbolSize: [60, 60],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 60,
|
|
||||||
symbol: pathSymbols.rocket,
|
|
||||||
symbolSize: [50, 60],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 25,
|
|
||||||
symbol: pathSymbols.plane,
|
|
||||||
symbolSize: [65, 35],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 80,
|
|
||||||
symbol: pathSymbols.train,
|
|
||||||
symbolSize: [50, 30],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
UserSourceChart.setOption(option)
|
|
||||||
state.charts.push(UserSourceChart)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initUserSurnameChart = () => {
|
|
||||||
const userSurnameChart = echarts.init(chartRefs.value[3] as HTMLElement)
|
|
||||||
const data = genData(20)
|
|
||||||
const option = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
type: 'scroll',
|
|
||||||
orient: 'vertical',
|
|
||||||
right: 10,
|
|
||||||
top: 20,
|
|
||||||
bottom: 20,
|
|
||||||
data: data.legendData,
|
|
||||||
textStyle: {
|
|
||||||
color: '#73767a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: t('dashboard.full name'),
|
|
||||||
type: 'pie',
|
|
||||||
radius: '55%',
|
|
||||||
center: ['40%', '50%'],
|
|
||||||
data: data.seriesData,
|
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
function genData(count: any) {
|
|
||||||
// prettier-ignore
|
|
||||||
const nameList = [
|
|
||||||
'赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', '柏', '水', '窦', '章', '云', '苏', '潘', '葛', '奚', '范', '彭', '郎', '鲁', '韦', '昌', '马', '苗', '凤', '花', '方', '俞', '任', '袁', '柳', '酆', '鲍', '史', '唐', '费', '廉', '岑', '薛', '雷', '贺', '倪', '汤', '滕', '殷', '罗', '毕', '郝', '邬', '安', '常', '乐', '于', '时', '傅', '皮', '卞', '齐', '康', '伍', '余', '元', '卜', '顾', '孟', '平', '黄', '和', '穆', '萧', '尹', '姚', '邵', '湛', '汪', '祁', '毛', '禹', '狄', '米', '贝', '明', '臧', '计', '伏', '成', '戴', '谈', '宋', '茅', '庞', '熊', '纪', '舒', '屈', '项', '祝', '董', '梁', '杜', '阮', '蓝', '闵', '席', '季', '麻', '强', '贾', '路', '娄', '危'
|
|
||||||
];
|
|
||||||
const legendData = []
|
|
||||||
const seriesData = []
|
|
||||||
for (var i = 0; i < count; i++) {
|
|
||||||
var name = Math.random() > 0.85 ? makeWord(2, 1) + '·' + makeWord(2, 0) : makeWord(2, 1)
|
|
||||||
legendData.push(name)
|
|
||||||
seriesData.push({
|
|
||||||
name: name,
|
|
||||||
value: Math.round(Math.random() * 100000),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
legendData: legendData,
|
|
||||||
seriesData: seriesData,
|
|
||||||
}
|
|
||||||
function makeWord(max: any, min: any) {
|
|
||||||
const nameLen = Math.ceil(Math.random() * max + min)
|
|
||||||
const name = []
|
|
||||||
for (var i = 0; i < nameLen; i++) {
|
|
||||||
name.push(nameList[Math.round(Math.random() * nameList.length - 1)])
|
|
||||||
}
|
|
||||||
return name.join('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userSurnameChart.setOption(option)
|
|
||||||
state.charts.push(userSurnameChart)
|
|
||||||
}
|
|
||||||
|
|
||||||
const echartsResize = () => {
|
|
||||||
nextTick(() => {
|
|
||||||
for (const key in state.charts) {
|
|
||||||
state.charts[key].resize()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeWorkState = () => {
|
const onChangeWorkState = () => {
|
||||||
@@ -594,36 +280,13 @@ const formatSeconds = (seconds: number) => {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
echartsResize()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startWork()
|
startWork()
|
||||||
initCountUp()
|
|
||||||
initUserGrowthChart()
|
|
||||||
initFileGrowthChart()
|
|
||||||
initUserSourceChart()
|
|
||||||
initUserSurnameChart()
|
|
||||||
useEventListener(window, 'resize', echartsResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
for (const key in state.charts) {
|
|
||||||
state.charts[key].dispose()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(workTimer)
|
clearInterval(workTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
() => navTabs.state.tabFullScreen,
|
|
||||||
() => {
|
|
||||||
echartsResize()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -751,48 +414,27 @@ watch(
|
|||||||
.growth-chart {
|
.growth-chart {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.user-growth-chart,
|
.playx-kpis {
|
||||||
.file-growth-chart {
|
display: grid;
|
||||||
height: 260px;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.new-user-growth {
|
.playx-kpi {
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-source-chart,
|
|
||||||
.user-surname-chart {
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
.new-user-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 10px 15px;
|
|
||||||
box-shadow: 0 0 30px 0 rgba(82, 63, 105, 0.05);
|
|
||||||
background-color: var(--ba-bg-color-overlay);
|
background-color: var(--ba-bg-color-overlay);
|
||||||
.new-user-avatar {
|
border: 1px solid var(--ba-border-color);
|
||||||
height: 48px;
|
border-radius: var(--el-border-radius-base);
|
||||||
width: 48px;
|
padding: 12px;
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.new-user-base {
|
|
||||||
margin-left: 10px;
|
|
||||||
color: #2c3f5d;
|
|
||||||
.new-user-name {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.new-user-time {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.new-user-arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.new-user-card :deep(.el-card__body) {
|
.playx-kpi-title {
|
||||||
padding: 0;
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.playx-kpi-value {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 425px) {
|
@media screen and (max-width: 425px) {
|
||||||
.welcome-img {
|
.welcome-img {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ const { t } = useI18n()
|
|||||||
const tableRef = useTemplateRef('tableRef')
|
const tableRef = useTemplateRef('tableRef')
|
||||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||||
|
|
||||||
|
const hasChinese = (s: string) => /[\u4e00-\u9fa5]/.test(s)
|
||||||
|
|
||||||
|
const formatRegion = (raw: string) => {
|
||||||
|
const s = raw.toString().trim()
|
||||||
|
if (!s) return ''
|
||||||
|
if (hasChinese(s)) {
|
||||||
|
return s.replace(/[,,\s]+/g, ',')
|
||||||
|
}
|
||||||
|
return s.replace(/[,,\s]+/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||||
*/
|
*/
|
||||||
@@ -48,15 +59,24 @@ const baTable = new baTableClass(
|
|||||||
{ type: 'selection', align: 'center', operator: false },
|
{ type: 'selection', align: 'center', operator: false },
|
||||||
{ label: t('mall.address.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
{ label: t('mall.address.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||||
{
|
{
|
||||||
label: t('mall.address.malluser__username'),
|
label: t('mall.address.playxuserasset__username'),
|
||||||
prop: 'mallUser.username',
|
prop: 'playxUserAsset.username',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
render: 'tags',
|
showOverflowTooltip: true,
|
||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
comSearchRender: 'string',
|
comSearchRender: 'string',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.address.receiver_name'),
|
||||||
|
prop: 'receiver_name',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 100,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('mall.address.phone'),
|
label: t('mall.address.phone'),
|
||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
@@ -65,7 +85,17 @@ const baTable = new baTableClass(
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
},
|
},
|
||||||
{ label: t('mall.address.region'), prop: 'region_text', align: 'center', operator: false },
|
{
|
||||||
|
label: t('mall.address.region'),
|
||||||
|
prop: 'region_text',
|
||||||
|
align: 'center',
|
||||||
|
operator: false,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: TableRow, _column: TableColumn, cellValue: string) => {
|
||||||
|
const raw = (cellValue || row.region || '').toString()
|
||||||
|
return formatRegion(raw)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('mall.address.detail_address'),
|
label: t('mall.address.detail_address'),
|
||||||
prop: 'detail_address',
|
prop: 'detail_address',
|
||||||
@@ -78,9 +108,11 @@ const baTable = new baTableClass(
|
|||||||
label: t('mall.address.default_setting'),
|
label: t('mall.address.default_setting'),
|
||||||
prop: 'default_setting',
|
prop: 'default_setting',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
effect: 'dark',
|
||||||
|
custom: { 0: 'error', 1: 'primary' },
|
||||||
operator: 'eq',
|
operator: 'eq',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
render: 'switch',
|
render: 'tag',
|
||||||
replaceValue: { '0': t('mall.address.default_setting 0'), '1': t('mall.address.default_setting 1') },
|
replaceValue: { '0': t('mall.address.default_setting 0'), '1': t('mall.address.default_setting 1') },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -105,7 +137,7 @@ const baTable = new baTableClass(
|
|||||||
width: 160,
|
width: 160,
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
},
|
},
|
||||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||||
],
|
],
|
||||||
dblClickNotEditColumn: [undefined, 'default_setting'],
|
dblClickNotEditColumn: [undefined, 'default_setting'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,12 +30,19 @@
|
|||||||
:rules="rules"
|
:rules="rules"
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.address.mall_user_id')"
|
:label="t('mall.address.playx_user_asset_id')"
|
||||||
type="remoteSelect"
|
type="remoteSelect"
|
||||||
v-model="baTable.form.items!.mall_user_id"
|
v-model="baTable.form.items!.playx_user_asset_id"
|
||||||
prop="mall_user_id"
|
prop="playx_user_asset_id"
|
||||||
:input-attr="{ pk: 'mall_user.id', field: 'username', remoteUrl: '/admin/mall.User/index' }"
|
:input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }"
|
||||||
:placeholder="t('Please select field', { field: t('mall.address.mall_user_id') })"
|
:placeholder="t('Please select field', { field: t('mall.address.playx_user_asset_id') })"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.address.receiver_name')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.receiver_name"
|
||||||
|
prop="receiver_name"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.address.receiver_name') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.address.phone')"
|
:label="t('mall.address.phone')"
|
||||||
@@ -58,16 +65,6 @@
|
|||||||
prop="detail_address"
|
prop="detail_address"
|
||||||
:placeholder="t('Please input field', { field: t('mall.address.detail_address') })"
|
:placeholder="t('Please input field', { field: t('mall.address.detail_address') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
|
||||||
:label="t('mall.address.address')"
|
|
||||||
type="textarea"
|
|
||||||
v-model="baTable.form.items!.address"
|
|
||||||
prop="address"
|
|
||||||
:input-attr="{ rows: 3 }"
|
|
||||||
@keyup.enter.stop=""
|
|
||||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.address.address') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.address.default_setting')"
|
:label="t('mall.address.default_setting')"
|
||||||
type="switch"
|
type="switch"
|
||||||
|
|||||||
61
web/src/views/backend/mall/claimLog/index.vue
Normal file
61
web/src/views/backend/mall/claimLog/index.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<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('mall.claimLog.quick Search Fields') })"
|
||||||
|
></TableHeader>
|
||||||
|
|
||||||
|
<Table ref="tableRef"></Table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { baTableApi } from '/@/api/common'
|
||||||
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
|
import Table from '/@/components/table/index.vue'
|
||||||
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'mall/claimLog',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const tableRef = useTemplateRef('tableRef')
|
||||||
|
|
||||||
|
const baTable = new baTableClass(
|
||||||
|
new baTableApi('/admin/mall.ClaimLog/'),
|
||||||
|
{
|
||||||
|
pk: 'id',
|
||||||
|
column: [
|
||||||
|
{ type: 'selection', align: 'center', operator: false },
|
||||||
|
{ label: t('mall.claimLog.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||||
|
{ label: t('mall.claimLog.claim_request_id'), prop: 'claim_request_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||||
|
{ label: t('mall.claimLog.user_id'), prop: 'user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||||
|
{ label: t('mall.claimLog.claimed_amount'), prop: 'claimed_amount', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
|
{ label: t('mall.claimLog.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
|
||||||
|
],
|
||||||
|
dblClickNotEditColumn: [undefined],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultItems: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
provide('baTable', baTable)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
baTable.table.ref = tableRef.value
|
||||||
|
baTable.mount()
|
||||||
|
baTable.getData()?.then(() => {
|
||||||
|
baTable.initSort()
|
||||||
|
baTable.dragSort()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
|
|
||||||
123
web/src/views/backend/mall/dailyPush/index.vue
Normal file
123
web/src/views/backend/mall/dailyPush/index.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<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('mall.dailyPush.quick Search Fields') })"
|
||||||
|
></TableHeader>
|
||||||
|
|
||||||
|
<Table ref="tableRef"></Table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { baTableApi } from '/@/api/common'
|
||||||
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
|
import Table from '/@/components/table/index.vue'
|
||||||
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'mall/dailyPush',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const tableRef = useTemplateRef('tableRef')
|
||||||
|
|
||||||
|
const baTable = new baTableClass(
|
||||||
|
new baTableApi('/admin/mall.DailyPush/'),
|
||||||
|
{
|
||||||
|
pk: 'id',
|
||||||
|
column: [
|
||||||
|
{ type: 'selection', align: 'center', operator: false },
|
||||||
|
{ label: t('mall.dailyPush.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.user_id'),
|
||||||
|
prop: 'user_id',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.date'),
|
||||||
|
prop: 'date',
|
||||||
|
align: 'center',
|
||||||
|
render: 'date',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'date',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 120,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.username'),
|
||||||
|
prop: 'username',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.yesterday_win_loss_net'),
|
||||||
|
prop: 'yesterday_win_loss_net',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'RANGE',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.yesterday_total_deposit'),
|
||||||
|
prop: 'yesterday_total_deposit',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'RANGE',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.lifetime_total_deposit'),
|
||||||
|
prop: 'lifetime_total_deposit',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'RANGE',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.lifetime_total_withdraw'),
|
||||||
|
prop: 'lifetime_total_withdraw',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 95,
|
||||||
|
operator: 'RANGE',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.dailyPush.create_time'),
|
||||||
|
prop: 'create_time',
|
||||||
|
align: 'center',
|
||||||
|
render: 'datetime',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dblClickNotEditColumn: [undefined],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultItems: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
provide('baTable', baTable)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
baTable.table.ref = tableRef.value
|
||||||
|
baTable.mount()
|
||||||
|
baTable.getData()?.then(() => {
|
||||||
|
baTable.initSort()
|
||||||
|
baTable.dragSort()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -52,6 +52,8 @@ const baTable = new baTableClass(
|
|||||||
label: t('mall.item.description'),
|
label: t('mall.item.description'),
|
||||||
prop: 'description',
|
prop: 'description',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
minWidth: 80,
|
||||||
|
// showOverflowTooltip: true,
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
@@ -60,6 +62,7 @@ const baTable = new baTableClass(
|
|||||||
label: t('mall.item.score'),
|
label: t('mall.item.score'),
|
||||||
prop: 'score',
|
prop: 'score',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
minWidth: 90,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
operator: 'RANGE',
|
operator: 'RANGE',
|
||||||
},
|
},
|
||||||
@@ -173,7 +176,7 @@ const baTable = new baTableClass(
|
|||||||
{
|
{
|
||||||
label: t('Operate'),
|
label: t('Operate'),
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 80,
|
||||||
render: 'buttons',
|
render: 'buttons',
|
||||||
buttons: optButtons,
|
buttons: optButtons,
|
||||||
operator: false,
|
operator: false,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
:label-width="baTable.form.labelWidth + 'px'"
|
:label-width="baTable.form.labelWidth + 'px'"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
>
|
>
|
||||||
|
<!-- 先选类型:未选类型时只显示标题与类型,避免看到无关字段 -->
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.item.title')"
|
:label="t('mall.item.title')"
|
||||||
type="string"
|
type="string"
|
||||||
@@ -36,34 +37,6 @@
|
|||||||
prop="title"
|
prop="title"
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.title') })"
|
:placeholder="t('Please input field', { field: t('mall.item.title') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
|
||||||
:label="t('mall.item.description')"
|
|
||||||
type="textarea"
|
|
||||||
v-model="baTable.form.items!.description"
|
|
||||||
prop="description"
|
|
||||||
:input-attr="{ rows: 3 }"
|
|
||||||
@keyup.enter.stop=""
|
|
||||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.description') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('mall.item.remark')"
|
|
||||||
type="textarea"
|
|
||||||
v-model="baTable.form.items!.remark"
|
|
||||||
prop="remark"
|
|
||||||
:input-attr="{ rows: 3 }"
|
|
||||||
@keyup.enter.stop=""
|
|
||||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.remark') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('mall.item.score')"
|
|
||||||
type="number"
|
|
||||||
v-model="baTable.form.items!.score"
|
|
||||||
prop="score"
|
|
||||||
:input-attr="{ step: 1 }"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.score') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.item.type')"
|
:label="t('mall.item.type')"
|
||||||
type="select"
|
type="select"
|
||||||
@@ -72,65 +45,101 @@
|
|||||||
:input-attr="{ content: { '1': t('mall.item.type 1'), '2': t('mall.item.type 2'), '3': t('mall.item.type 3') } }"
|
:input-attr="{ content: { '1': t('mall.item.type 1'), '2': t('mall.item.type 2'), '3': t('mall.item.type 3') } }"
|
||||||
:placeholder="t('Please select field', { field: t('mall.item.type') })"
|
:placeholder="t('Please select field', { field: t('mall.item.type') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
|
||||||
:label="t('mall.item.amount')"
|
<template v-if="hasItemType">
|
||||||
type="number"
|
<FormItem
|
||||||
v-model="baTable.form.items!.amount"
|
:label="t('mall.item.description')"
|
||||||
prop="amount"
|
type="textarea"
|
||||||
:input-attr="{ step: 0.01 }"
|
v-model="baTable.form.items!.description"
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.amount') })"
|
prop="description"
|
||||||
v-if="isBonusOrWithdraw"
|
:input-attr="{ rows: 3 }"
|
||||||
/>
|
@keyup.enter.stop=""
|
||||||
<FormItem
|
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||||
:label="t('mall.item.multiplier')"
|
:placeholder="t('Please input field', { field: t('mall.item.description') })"
|
||||||
type="number"
|
/>
|
||||||
v-model="baTable.form.items!.multiplier"
|
<FormItem
|
||||||
prop="multiplier"
|
:label="t('mall.item.score')"
|
||||||
:input-attr="{ step: 1 }"
|
type="number"
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.multiplier') })"
|
v-model="baTable.form.items!.score"
|
||||||
v-if="isBonusOrWithdraw"
|
prop="score"
|
||||||
/>
|
:input-attr="{ step: 1 }"
|
||||||
<FormItem
|
:placeholder="t('Please input field', { field: t('mall.item.score') })"
|
||||||
:label="t('mall.item.category')"
|
/>
|
||||||
type="string"
|
|
||||||
v-model="baTable.form.items!.category"
|
<!-- BONUS / WITHDRAW:现金面值、流水倍数 -->
|
||||||
prop="category"
|
<FormItem
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.category') })"
|
v-if="isBonusOrWithdraw"
|
||||||
v-if="isBonusOrWithdraw"
|
:label="t('mall.item.amount')"
|
||||||
/>
|
type="number"
|
||||||
<FormItem
|
v-model="baTable.form.items!.amount"
|
||||||
:label="t('mall.item.category_title')"
|
prop="amount"
|
||||||
type="string"
|
:input-attr="{ step: 0.01 }"
|
||||||
v-model="baTable.form.items!.category_title"
|
:placeholder="t('Please input field', { field: t('mall.item.amount') })"
|
||||||
prop="category_title"
|
/>
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.category_title') })"
|
<FormItem
|
||||||
v-if="isBonusOrWithdraw"
|
v-if="isBonusOrWithdraw"
|
||||||
/>
|
:label="t('mall.item.multiplier')"
|
||||||
<FormItem :label="t('mall.item.image')" type="image" v-model="baTable.form.items!.image" />
|
type="number"
|
||||||
<FormItem
|
v-model="baTable.form.items!.multiplier"
|
||||||
:label="t('mall.item.stock')"
|
prop="multiplier"
|
||||||
type="number"
|
:input-attr="{ step: 1 }"
|
||||||
v-model="baTable.form.items!.stock"
|
:placeholder="t('Please input field', { field: t('mall.item.multiplier') })"
|
||||||
prop="stock"
|
/>
|
||||||
:input-attr="{ step: 1 }"
|
<!-- 仅 BONUS:Bonus Grant 需要 -->
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.stock') })"
|
<FormItem
|
||||||
v-if="isPhysical"
|
v-if="isBonus"
|
||||||
/>
|
:label="t('mall.item.category')"
|
||||||
<FormItem
|
type="string"
|
||||||
:label="t('mall.item.sort')"
|
v-model="baTable.form.items!.category"
|
||||||
type="number"
|
prop="category"
|
||||||
v-model="baTable.form.items!.sort"
|
:placeholder="t('Please input field', { field: t('mall.item.category') })"
|
||||||
prop="sort"
|
/>
|
||||||
:input-attr="{ step: 1 }"
|
<FormItem
|
||||||
:placeholder="t('Please input field', { field: t('mall.item.sort') })"
|
v-if="isBonus"
|
||||||
/>
|
:label="t('mall.item.category_title')"
|
||||||
<FormItem
|
type="string"
|
||||||
:label="t('mall.item.status')"
|
v-model="baTable.form.items!.category_title"
|
||||||
type="switch"
|
prop="category_title"
|
||||||
v-model="baTable.form.items!.status"
|
:placeholder="t('Please input field', { field: t('mall.item.category_title') })"
|
||||||
prop="status"
|
/>
|
||||||
:input-attr="{ content: { '0': t('mall.item.status 0'), '1': t('mall.item.status 1') } }"
|
<!-- 仅 PHYSICAL:库存 -->
|
||||||
/>
|
<FormItem
|
||||||
|
v-if="isPhysical"
|
||||||
|
:label="t('mall.item.stock')"
|
||||||
|
type="number"
|
||||||
|
v-model="baTable.form.items!.stock"
|
||||||
|
prop="stock"
|
||||||
|
:input-attr="{ step: 1 }"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.item.stock') })"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.item.remark')"
|
||||||
|
type="textarea"
|
||||||
|
v-model="baTable.form.items!.remark"
|
||||||
|
prop="remark"
|
||||||
|
:input-attr="{ rows: 3 }"
|
||||||
|
@keyup.enter.stop=""
|
||||||
|
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.item.remark') })"
|
||||||
|
/>
|
||||||
|
<FormItem :label="t('mall.item.image')" type="image" v-model="baTable.form.items!.image" />
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.item.sort')"
|
||||||
|
type="number"
|
||||||
|
v-model="baTable.form.items!.sort"
|
||||||
|
prop="sort"
|
||||||
|
:input-attr="{ step: 1 }"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.item.sort') })"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.item.status')"
|
||||||
|
type="switch"
|
||||||
|
v-model="baTable.form.items!.status"
|
||||||
|
prop="status"
|
||||||
|
:input-attr="{ content: { '0': t('mall.item.status 0'), '1': t('mall.item.status 1') } }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
@@ -166,7 +175,10 @@ const isPhysical = computed(() => itemType.value === 2 || itemType.value === '2'
|
|||||||
const isWithdraw = computed(() => itemType.value === 3 || itemType.value === '3')
|
const isWithdraw = computed(() => itemType.value === 3 || itemType.value === '3')
|
||||||
const isBonusOrWithdraw = computed(() => isBonus.value || isWithdraw.value)
|
const isBonusOrWithdraw = computed(() => isBonus.value || isWithdraw.value)
|
||||||
|
|
||||||
// 切换类型后,清理不适用的字段,避免“隐藏字段仍保留上一次的值”导致提交脏数据
|
/** 已选择商品类型(1/2/3)后,才展示其余表单项 */
|
||||||
|
const hasItemType = computed(() => isBonus.value || isPhysical.value || isWithdraw.value)
|
||||||
|
|
||||||
|
// 切换类型后,清理不适用的字段(严格:WITHDRAW 不保留红利类别字段)
|
||||||
watch(
|
watch(
|
||||||
itemType,
|
itemType,
|
||||||
(n, o) => {
|
(n, o) => {
|
||||||
@@ -176,14 +188,20 @@ watch(
|
|||||||
const typeNum = Number(n)
|
const typeNum = Number(n)
|
||||||
if (!Number.isFinite(typeNum)) return
|
if (!Number.isFinite(typeNum)) return
|
||||||
|
|
||||||
if (typeNum === 2) {
|
if (typeNum === 1) {
|
||||||
|
// BONUS:实物库存不适用
|
||||||
|
baTable.form.items.stock = 0
|
||||||
|
} else if (typeNum === 2) {
|
||||||
|
// PHYSICAL:现金/倍数/类别不适用
|
||||||
baTable.form.items.amount = 0
|
baTable.form.items.amount = 0
|
||||||
baTable.form.items.multiplier = 0
|
baTable.form.items.multiplier = 0
|
||||||
baTable.form.items.category = ''
|
baTable.form.items.category = ''
|
||||||
baTable.form.items.category_title = ''
|
baTable.form.items.category_title = ''
|
||||||
} else {
|
} else if (typeNum === 3) {
|
||||||
// BONUS / WITHDRAW
|
// WITHDRAW:提现接口不使用 category,清空避免误存
|
||||||
baTable.form.items.stock = 0
|
baTable.form.items.stock = 0
|
||||||
|
baTable.form.items.category = ''
|
||||||
|
baTable.form.items.category_title = ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ flush: 'post' },
|
{ flush: 'post' },
|
||||||
@@ -191,10 +209,33 @@ watch(
|
|||||||
|
|
||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||||
title: [buildValidatorData({ name: 'required', title: t('mall.item.title') })],
|
title: [buildValidatorData({ name: 'required', title: t('mall.item.title') })],
|
||||||
description: [buildValidatorData({ name: 'required', title: t('mall.item.description') })],
|
description: [
|
||||||
|
{
|
||||||
|
validator: (rule: any, val: any, callback: Function) => {
|
||||||
|
if (!hasItemType.value) return callback()
|
||||||
|
if (val === '' || val === null || val === undefined) {
|
||||||
|
return callback(new Error(t('Please input field', { field: t('mall.item.description') })))
|
||||||
|
}
|
||||||
|
return callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
score: [
|
score: [
|
||||||
buildValidatorData({ name: 'number', title: t('mall.item.score') }),
|
{
|
||||||
buildValidatorData({ name: 'required', title: t('mall.item.score') }),
|
validator: (rule: any, val: any, callback: Function) => {
|
||||||
|
if (!hasItemType.value) return callback()
|
||||||
|
if (val === '' || val === null || val === undefined) {
|
||||||
|
return callback(new Error(t('Please input field', { field: t('mall.item.score') })))
|
||||||
|
}
|
||||||
|
const num = Number(val)
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
return callback(new Error(t('Please enter the correct field', { field: t('mall.item.score') })))
|
||||||
|
}
|
||||||
|
return callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
type: [buildValidatorData({ name: 'required', title: t('mall.item.type') })],
|
type: [buildValidatorData({ name: 'required', title: t('mall.item.type') })],
|
||||||
amount: [
|
amount: [
|
||||||
@@ -238,7 +279,7 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|||||||
category: [
|
category: [
|
||||||
{
|
{
|
||||||
validator: (rule: any, val: any, callback: Function) => {
|
validator: (rule: any, val: any, callback: Function) => {
|
||||||
if (!isBonusOrWithdraw.value) return callback()
|
if (!isBonus.value) return callback()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
return callback(new Error(t('Please input field', { field: t('mall.item.category') })))
|
return callback(new Error(t('Please input field', { field: t('mall.item.category') })))
|
||||||
}
|
}
|
||||||
@@ -250,7 +291,7 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|||||||
category_title: [
|
category_title: [
|
||||||
{
|
{
|
||||||
validator: (rule: any, val: any, callback: Function) => {
|
validator: (rule: any, val: any, callback: Function) => {
|
||||||
if (!isBonusOrWithdraw.value) return callback()
|
if (!isBonus.value) return callback()
|
||||||
if (!val) {
|
if (!val) {
|
||||||
return callback(new Error(t('Please input field', { field: t('mall.item.category_title') })))
|
return callback(new Error(t('Please input field', { field: t('mall.item.category_title') })))
|
||||||
}
|
}
|
||||||
|
|||||||
318
web/src/views/backend/mall/order/index.vue
Normal file
318
web/src/views/backend/mall/order/index.vue
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<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', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||||
|
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.order.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 { baTableApi } from '/@/api/common'
|
||||||
|
import createAxios from '/@/utils/axios'
|
||||||
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
|
import Table from '/@/components/table/index.vue'
|
||||||
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
import PopupForm from './popupForm.vue'
|
||||||
|
import { defaultOptButtons } from '/@/components/table'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'mall/order',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const tableRef = useTemplateRef('tableRef')
|
||||||
|
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn) =>
|
||||||
|
btn.name === 'edit'
|
||||||
|
? {
|
||||||
|
...btn,
|
||||||
|
title: t('mall.order.approve'),
|
||||||
|
type: 'primary',
|
||||||
|
class: 'table-row-edit',
|
||||||
|
icon: 'fa fa-check',
|
||||||
|
}
|
||||||
|
: btn
|
||||||
|
)
|
||||||
|
|
||||||
|
const baTable = new baTableClass(
|
||||||
|
new baTableApi('/admin/mall.Order/'),
|
||||||
|
{
|
||||||
|
pk: 'id',
|
||||||
|
column: [
|
||||||
|
{ type: 'selection', align: 'center', operator: false },
|
||||||
|
{ label: t('mall.order.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||||
|
{
|
||||||
|
label: t('mall.order.user_id'),
|
||||||
|
prop: 'user_id',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.type'),
|
||||||
|
prop: 'type',
|
||||||
|
align: 'center',
|
||||||
|
effect: 'light',
|
||||||
|
custom: { BONUS: 'success', PHYSICAL: 'primary', WITHDRAW: 'info' },
|
||||||
|
minWidth: 140,
|
||||||
|
operator: 'eq',
|
||||||
|
sortable: false,
|
||||||
|
render: 'tag',
|
||||||
|
replaceValue: {
|
||||||
|
BONUS: t('mall.order.type BONUS'),
|
||||||
|
PHYSICAL: t('mall.order.type PHYSICAL'),
|
||||||
|
WITHDRAW: t('mall.order.type WITHDRAW'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.status'),
|
||||||
|
prop: 'status',
|
||||||
|
align: 'center',
|
||||||
|
effect: 'dark',
|
||||||
|
custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' },
|
||||||
|
minWidth: 160,
|
||||||
|
operator: 'eq',
|
||||||
|
sortable: false,
|
||||||
|
render: 'tag',
|
||||||
|
replaceValue: {
|
||||||
|
PENDING: t('mall.order.status PENDING'),
|
||||||
|
COMPLETED: t('mall.order.status COMPLETED'),
|
||||||
|
SHIPPED: t('mall.order.status SHIPPED'),
|
||||||
|
REJECTED: t('mall.order.status REJECTED'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: t('mall.order.mall_item_id'), prop: 'mall_item_id', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
|
{
|
||||||
|
label: t('mall.order.mallitem__title'),
|
||||||
|
prop: 'mallItem.title',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 90,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{ label: t('mall.order.points_cost'), prop: 'points_cost', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false },
|
||||||
|
{ label: t('mall.order.amount'), prop: 'amount', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false },
|
||||||
|
{ label: t('mall.order.multiplier'), prop: 'multiplier', align: 'center', minWidth: 90, operator: 'eq', sortable: false },
|
||||||
|
{
|
||||||
|
label: t('mall.order.external_transaction_id'),
|
||||||
|
prop: 'external_transaction_id',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 80,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.playx_transaction_id'),
|
||||||
|
prop: 'playx_transaction_id',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.grant_status'),
|
||||||
|
prop: 'grant_status',
|
||||||
|
align: 'center',
|
||||||
|
minWidth: 100,
|
||||||
|
custom: {
|
||||||
|
NOT_SENT: 'info',
|
||||||
|
SENT_PENDING: 'primary',
|
||||||
|
ACCEPTED: 'primary',
|
||||||
|
FAILED_RETRYABLE: 'error',
|
||||||
|
FAILED_FINAL: 'error',
|
||||||
|
'---': 'info',
|
||||||
|
},
|
||||||
|
operator: 'eq',
|
||||||
|
sortable: false,
|
||||||
|
render: 'tag',
|
||||||
|
replaceValue: {
|
||||||
|
NOT_SENT: t('mall.order.grant_status NOT_SENT'),
|
||||||
|
SENT_PENDING: t('mall.order.grant_status SENT_PENDING'),
|
||||||
|
ACCEPTED: t('mall.order.grant_status ACCEPTED'),
|
||||||
|
FAILED_RETRYABLE: t('mall.order.grant_status FAILED_RETRYABLE'),
|
||||||
|
FAILED_FINAL: t('mall.order.grant_status FAILED_FINAL'),
|
||||||
|
'---': t('mall.order.grant_status ---'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.fail_reason'),
|
||||||
|
prop: 'fail_reason',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.reject_reason'),
|
||||||
|
prop: 'reject_reason',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.shipping_company'),
|
||||||
|
prop: 'shipping_company',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.shipping_no'),
|
||||||
|
prop: 'shipping_no',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.receiver_name'),
|
||||||
|
prop: 'receiver_name',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.receiver_phone'),
|
||||||
|
prop: 'receiver_phone',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.receiver_address'),
|
||||||
|
prop: 'receiver_address',
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{ label: t('mall.order.mall_address_id'), prop: 'mall_address_id', align: 'center', width: 100, operator: 'eq', sortable: false },
|
||||||
|
{
|
||||||
|
label: t('mall.order.start_time'),
|
||||||
|
prop: 'start_time',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.end_time'),
|
||||||
|
prop: 'end_time',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.create_time'),
|
||||||
|
prop: 'create_time',
|
||||||
|
align: 'center',
|
||||||
|
render: 'datetime',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.order.update_time'),
|
||||||
|
prop: 'update_time',
|
||||||
|
align: 'center',
|
||||||
|
show: false,
|
||||||
|
render: 'datetime',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Operate'),
|
||||||
|
align: 'center',
|
||||||
|
width: 120,
|
||||||
|
fixed: 'right',
|
||||||
|
render: 'buttons',
|
||||||
|
buttons: [
|
||||||
|
...optButtons,
|
||||||
|
{
|
||||||
|
render: 'confirmButton',
|
||||||
|
name: 'retry',
|
||||||
|
title: t('mall.order.manual_retry'),
|
||||||
|
text: '',
|
||||||
|
type: 'primary',
|
||||||
|
class: 'table-row-edit',
|
||||||
|
icon: 'fa fa-refresh',
|
||||||
|
display: (row: TableRow) =>
|
||||||
|
row.type === 'BONUS' &&
|
||||||
|
row.status === 'PENDING' &&
|
||||||
|
['NOT_SENT', 'SENT_PENDING', 'FAILED_RETRYABLE', 'FAILED_FINAL'].includes(String(row.grant_status)),
|
||||||
|
popconfirm: {
|
||||||
|
title: t('mall.order.retry_confirm'),
|
||||||
|
confirmButtonText: t('Confirm'),
|
||||||
|
cancelButtonText: t('Cancel'),
|
||||||
|
confirmButtonType: 'primary',
|
||||||
|
},
|
||||||
|
click: async (row: TableRow) => {
|
||||||
|
await createAxios(
|
||||||
|
{
|
||||||
|
url: '/admin/mall.Order/retry',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
id: row.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
showSuccessMessage: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await baTable.getData()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dblClickNotEditColumn: [undefined],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultItems: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
provide('baTable', baTable)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
baTable.table.ref = tableRef.value
|
||||||
|
baTable.mount()
|
||||||
|
baTable.getData()?.then(() => {
|
||||||
|
baTable.initSort()
|
||||||
|
baTable.dragSort()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
304
web/src/views/backend/mall/order/popupForm.vue
Normal file
304
web/src/views/backend/mall/order/popupForm.vue
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<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"
|
||||||
|
>
|
||||||
|
<!-- PENDING:两页审核流程(PHYSICAL 显示收货信息,其它类型只显示基本信息) -->
|
||||||
|
<template v-if="usePagedActions && page === 1">
|
||||||
|
<FormItem :label="t('mall.order.type')" type="string" v-model="baTable.form.items!.type" prop="type" :input-attr="{ disabled: true }" />
|
||||||
|
<FormItem :label="t('mall.order.status')" type="string" v-model="baTable.form.items!.status" prop="status" :input-attr="{ disabled: true }" />
|
||||||
|
<template v-if="isPhysical">
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.receiver_name')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.receiver_name"
|
||||||
|
prop="receiver_name"
|
||||||
|
:input-attr="{ disabled: true }"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.receiver_phone')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.receiver_phone"
|
||||||
|
prop="receiver_phone"
|
||||||
|
:input-attr="{ disabled: true }"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.receiver_address')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.receiver_address"
|
||||||
|
prop="receiver_address"
|
||||||
|
:input-attr="{ disabled: true }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="usePagedActions">
|
||||||
|
<template v-if="action === 'approveShip' && isPhysical">
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.shipping_company')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.shipping_company"
|
||||||
|
prop="shipping_company"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.shipping_no')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.shipping_no"
|
||||||
|
prop="shipping_no"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="action === 'reject'">
|
||||||
|
<FormItem
|
||||||
|
v-if="isPhysical"
|
||||||
|
:label="t('mall.order.reject_reason')"
|
||||||
|
type="textarea"
|
||||||
|
v-model="baTable.form.items!.reject_reason"
|
||||||
|
prop="reject_reason"
|
||||||
|
:input-attr="{ rows: 3 }"
|
||||||
|
@keyup.enter.stop=""
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
|
||||||
|
/>
|
||||||
|
<el-alert v-else type="info" :closable="false" show-icon>
|
||||||
|
确认后将驳回该订单并退回积分(红利/提现订单无需填写驳回原因)。
|
||||||
|
</el-alert>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 其它订单:保留常规可编辑表单 + 保存 -->
|
||||||
|
<template v-else>
|
||||||
|
<FormItem :label="t('mall.order.type')" type="string" v-model="baTable.form.items!.type" prop="type" :input-attr="{ disabled: true }" />
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.status')"
|
||||||
|
type="select"
|
||||||
|
v-model="baTable.form.items!.status"
|
||||||
|
prop="status"
|
||||||
|
:input-attr="{ content: { PENDING: t('mall.order.status PENDING'), COMPLETED: t('mall.order.status COMPLETED'), SHIPPED: t('mall.order.status SHIPPED'), REJECTED: t('mall.order.status REJECTED') } }"
|
||||||
|
:placeholder="t('Please select field', { field: t('mall.order.status') })"
|
||||||
|
/>
|
||||||
|
<template v-if="isPhysical">
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.shipping_company')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.shipping_company"
|
||||||
|
prop="shipping_company"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.shipping_no')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.shipping_no"
|
||||||
|
prop="shipping_no"
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.order.reject_reason')"
|
||||||
|
type="textarea"
|
||||||
|
v-model="baTable.form.items!.reject_reason"
|
||||||
|
prop="reject_reason"
|
||||||
|
:input-attr="{ rows: 3 }"
|
||||||
|
@keyup.enter.stop=""
|
||||||
|
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||||
|
<el-button @click="onCancel">{{ t('Cancel') }}</el-button>
|
||||||
|
|
||||||
|
<template v-if="usePagedActions && page === 1">
|
||||||
|
<el-button v-if="canApprove" @click="onApprove" type="success">审核通过</el-button>
|
||||||
|
<el-button @click="goReject" type="danger">驳回</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="usePagedActions">
|
||||||
|
<el-button @click="backToFirst">返回</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="action === 'approveShip'"
|
||||||
|
v-blur
|
||||||
|
:loading="submitting"
|
||||||
|
@click="submitShip"
|
||||||
|
type="success"
|
||||||
|
>
|
||||||
|
提交发货
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="action === 'reject'" v-blur :loading="submitting" @click="submitReject" type="danger">提交驳回</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||||
|
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useConfig } from '/@/stores/config'
|
||||||
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
|
import type { FormItemRule } from 'element-plus'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { buildValidatorData } from '/@/utils/validate'
|
||||||
|
import createAxios from '/@/utils/axios'
|
||||||
|
|
||||||
|
const config = useConfig()
|
||||||
|
const formRef = useTemplateRef('formRef')
|
||||||
|
const baTable = inject('baTable') as baTableClass
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isEdit = computed(() => baTable.form.operate === 'Edit')
|
||||||
|
const isPending = computed(() => isEdit.value && baTable.form.items?.status === 'PENDING')
|
||||||
|
const isPhysical = computed(() => baTable.form.items?.type === 'PHYSICAL')
|
||||||
|
const canApprove = computed(() => isPending.value)
|
||||||
|
const usePagedActions = computed(() => isPending.value)
|
||||||
|
|
||||||
|
const page = ref<1 | 2>(1)
|
||||||
|
const action = ref<'approveShip' | 'reject' | null>(null)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const resetPager = () => {
|
||||||
|
page.value = 1
|
||||||
|
action.value = null
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => baTable.form.operate,
|
||||||
|
() => {
|
||||||
|
resetPager()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
resetPager()
|
||||||
|
baTable.toggleForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const backToFirst = () => {
|
||||||
|
page.value = 1
|
||||||
|
action.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onApprove = async () => {
|
||||||
|
if (!isPending.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isPhysical.value) {
|
||||||
|
page.value = 2
|
||||||
|
action.value = 'approveShip'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = baTable.form.items?.id
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await createAxios({ url: '/admin/mall.Order/approve', method: 'post', data: { id } }, { showSuccessMessage: true })
|
||||||
|
resetPager()
|
||||||
|
baTable.toggleForm()
|
||||||
|
await baTable.getData()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goReject = () => {
|
||||||
|
page.value = 2
|
||||||
|
action.value = 'reject'
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitShip = async () => {
|
||||||
|
const id = baTable.form.items?.id
|
||||||
|
const shippingCompany = (baTable.form.items?.shipping_company || '').toString().trim()
|
||||||
|
const shippingNo = (baTable.form.items?.shipping_no || '').toString().trim()
|
||||||
|
if (!id || shippingCompany === '' || shippingNo === '') {
|
||||||
|
ElMessage.error('请填写物流公司与物流单号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await createAxios(
|
||||||
|
{ url: '/admin/mall.Order/ship', method: 'post', data: { id, shipping_company: shippingCompany, shipping_no: shippingNo } },
|
||||||
|
{ showSuccessMessage: true }
|
||||||
|
)
|
||||||
|
resetPager()
|
||||||
|
baTable.toggleForm()
|
||||||
|
await baTable.getData()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitReject = async () => {
|
||||||
|
const id = baTable.form.items?.id
|
||||||
|
const rejectReason = (baTable.form.items?.reject_reason || '').toString().trim()
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isPhysical.value && rejectReason === '') {
|
||||||
|
ElMessage.error('请填写驳回原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await createAxios(
|
||||||
|
{ url: '/admin/mall.Order/reject', method: 'post', data: { id, reject_reason: rejectReason } },
|
||||||
|
{ showSuccessMessage: true }
|
||||||
|
)
|
||||||
|
resetPager()
|
||||||
|
baTable.toggleForm()
|
||||||
|
await baTable.getData()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = computed<Partial<Record<string, FormItemRule[]>>>(() => {
|
||||||
|
if (!isPhysical.value) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
shipping_company: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_company') })],
|
||||||
|
shipping_no: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_no') })],
|
||||||
|
reject_reason: [buildValidatorData({ name: 'required', title: t('mall.order.reject_reason') })],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ const baTable = new baTableClass(
|
|||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('mall.pintsOrder.malluser__username'),
|
label: t('mall.pintsOrder.playxuserasset__username'),
|
||||||
prop: 'mallUser.username',
|
prop: 'playxUserAsset.username',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
|||||||
@@ -37,12 +37,12 @@
|
|||||||
:placeholder="t('Please input field', { field: t('mall.pintsOrder.order') })"
|
:placeholder="t('Please input field', { field: t('mall.pintsOrder.order') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.pintsOrder.mall_user_id')"
|
:label="t('mall.pintsOrder.playx_user_asset_id')"
|
||||||
type="remoteSelect"
|
type="remoteSelect"
|
||||||
v-model="baTable.form.items!.mall_user_id"
|
v-model="baTable.form.items!.playx_user_asset_id"
|
||||||
prop="mall_user_id"
|
prop="playx_user_asset_id"
|
||||||
:input-attr="{ pk: 'mall_user.id', field: 'username', remoteUrl: '/admin/mall.User/index' }"
|
:input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }"
|
||||||
:placeholder="t('Please select field', { field: t('mall.pintsOrder.mall_user_id') })"
|
:placeholder="t('Please select field', { field: t('mall.pintsOrder.playx_user_asset_id') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.pintsOrder.type')"
|
:label="t('mall.pintsOrder.type')"
|
||||||
@@ -93,7 +93,7 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||||
order: [buildValidatorData({ name: 'required', title: t('mall.pintsOrder.order') })],
|
order: [buildValidatorData({ name: 'required', title: t('mall.pintsOrder.order') })],
|
||||||
mall_user_id: [buildValidatorData({ name: 'required', title: t('mall.pintsOrder.mall_user_id') })],
|
playx_user_asset_id: [buildValidatorData({ name: 'required', title: t('mall.pintsOrder.playx_user_asset_id') })],
|
||||||
type: [buildValidatorData({ name: 'required', title: t('mall.pintsOrder.type') })],
|
type: [buildValidatorData({ name: 'required', title: t('mall.pintsOrder.type') })],
|
||||||
score: [buildValidatorData({ name: 'number', title: t('mall.pintsOrder.score') })],
|
score: [buildValidatorData({ name: 'number', title: t('mall.pintsOrder.score') })],
|
||||||
create_time: [buildValidatorData({ name: 'date', title: t('mall.pintsOrder.create_time') })],
|
create_time: [buildValidatorData({ name: 'date', title: t('mall.pintsOrder.create_time') })],
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
<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', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
|
||||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.player.quickSearchFields') })"
|
|
||||||
></TableHeader>
|
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
|
||||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
|
||||||
<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: 'mall/player',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const tableRef = useTemplateRef('tableRef')
|
|
||||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
|
||||||
*/
|
|
||||||
const baTable = new baTableClass(
|
|
||||||
new baTableApi('/admin/mall.Player/'),
|
|
||||||
{
|
|
||||||
pk: 'id',
|
|
||||||
column: [
|
|
||||||
{ type: 'selection', align: 'center', operator: false },
|
|
||||||
{ label: t('mall.player.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
|
||||||
{
|
|
||||||
label: t('mall.player.username'),
|
|
||||||
prop: 'username',
|
|
||||||
align: 'center',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
sortable: false,
|
|
||||||
operator: 'LIKE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('mall.player.create_time'),
|
|
||||||
prop: 'create_time',
|
|
||||||
align: 'center',
|
|
||||||
render: 'datetime',
|
|
||||||
operator: 'RANGE',
|
|
||||||
comSearchRender: 'datetime',
|
|
||||||
sortable: 'custom',
|
|
||||||
width: 160,
|
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('mall.player.update_time'),
|
|
||||||
prop: 'update_time',
|
|
||||||
align: 'center',
|
|
||||||
render: 'datetime',
|
|
||||||
operator: 'RANGE',
|
|
||||||
comSearchRender: 'datetime',
|
|
||||||
sortable: 'custom',
|
|
||||||
width: 160,
|
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
|
||||||
},
|
|
||||||
{ label: t('mall.player.score'), prop: 'score', align: 'center', sortable: false, operator: 'RANGE' },
|
|
||||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
|
||||||
],
|
|
||||||
dblClickNotEditColumn: [undefined],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultItems: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
provide('baTable', baTable)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
baTable.table.ref = tableRef.value
|
|
||||||
baTable.mount()
|
|
||||||
baTable.getData()?.then(() => {
|
|
||||||
baTable.initSort()
|
|
||||||
baTable.dragSort()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- 对话框表单 -->
|
|
||||||
<!-- 建议使用 Prettier 格式化代码 -->
|
|
||||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<FormItem
|
|
||||||
:label="t('mall.player.username')"
|
|
||||||
type="string"
|
|
||||||
v-model="baTable.form.items!.username"
|
|
||||||
prop="username"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.player.username') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('mall.player.password')"
|
|
||||||
type="password"
|
|
||||||
v-model="baTable.form.items!.password"
|
|
||||||
prop="password"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.player.password') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('mall.player.score')"
|
|
||||||
type="number"
|
|
||||||
v-model="baTable.form.items!.score"
|
|
||||||
prop="score"
|
|
||||||
:input-attr="{ step: 1 }"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.player.score') })"
|
|
||||||
/>
|
|
||||||
</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">
|
|
||||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { FormItemRule } from 'element-plus'
|
|
||||||
import { inject, reactive, useTemplateRef } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
|
||||||
import { useConfig } from '/@/stores/config'
|
|
||||||
import type baTableClass from '/@/utils/baTable'
|
|
||||||
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
|
||||||
|
|
||||||
const config = useConfig()
|
|
||||||
const formRef = useTemplateRef('formRef')
|
|
||||||
const baTable = inject('baTable') as baTableClass
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|
||||||
username: [buildValidatorData({ name: 'required', title: t('mall.player.username') })],
|
|
||||||
password: [
|
|
||||||
{
|
|
||||||
validator: (_rule: unknown, val: string, callback: (error?: Error) => void) => {
|
|
||||||
if (baTable.form.operate === 'Add') {
|
|
||||||
if (!val) {
|
|
||||||
return callback(new Error(t('Please input field', { field: t('mall.player.password') })))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!val) {
|
|
||||||
return callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!regularPassword(val)) {
|
|
||||||
return callback(new Error(t('validate.Please enter the correct password')))
|
|
||||||
}
|
|
||||||
return callback()
|
|
||||||
},
|
|
||||||
trigger: 'blur',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
score: [buildValidatorData({ name: 'number', title: t('mall.player.score') })],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -83,6 +83,7 @@ const baTable = new baTableClass(
|
|||||||
ACCEPTED: t('mall.playxOrder.grant_status ACCEPTED'),
|
ACCEPTED: t('mall.playxOrder.grant_status ACCEPTED'),
|
||||||
FAILED_RETRYABLE: t('mall.playxOrder.grant_status FAILED_RETRYABLE'),
|
FAILED_RETRYABLE: t('mall.playxOrder.grant_status FAILED_RETRYABLE'),
|
||||||
FAILED_FINAL: t('mall.playxOrder.grant_status FAILED_FINAL'),
|
FAILED_FINAL: t('mall.playxOrder.grant_status FAILED_FINAL'),
|
||||||
|
'---': t('mall.playxOrder.grant_status ---'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,7 +117,9 @@ const baTable = new baTableClass(
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
icon: '',
|
icon: '',
|
||||||
display: (row: TableRow) =>
|
display: (row: TableRow) =>
|
||||||
(row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING',
|
row.type === 'BONUS' &&
|
||||||
|
row.status === 'PENDING' &&
|
||||||
|
['NOT_SENT', 'SENT_PENDING', 'FAILED_RETRYABLE', 'FAILED_FINAL'].includes(String(row.grant_status)),
|
||||||
popconfirm: {
|
popconfirm: {
|
||||||
title: '确认将该订单加入重试队列?',
|
title: '确认将该订单加入重试队列?',
|
||||||
confirmButtonText: '确认',
|
confirmButtonText: '确认',
|
||||||
@@ -126,7 +129,7 @@ const baTable = new baTableClass(
|
|||||||
click: async (row: TableRow) => {
|
click: async (row: TableRow) => {
|
||||||
await createAxios(
|
await createAxios(
|
||||||
{
|
{
|
||||||
url: '/admin/mall.PlayxOrder/retry',
|
url: '/admin/mall.Order/retry',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
|||||||
@@ -33,21 +33,73 @@ const baTable = new baTableClass(
|
|||||||
column: [
|
column: [
|
||||||
{ type: 'selection', align: 'center', operator: false },
|
{ type: 'selection', align: 'center', operator: false },
|
||||||
{ label: t('mall.playxUserAsset.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
{ label: t('mall.playxUserAsset.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||||
{ label: t('mall.playxUserAsset.user_id'), prop: 'user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
{
|
||||||
{ label: t('mall.playxUserAsset.username'), prop: 'username', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
label: t('mall.playxUserAsset.username'),
|
||||||
|
prop: 'username',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.playxUserAsset.phone'),
|
||||||
|
prop: 'phone',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.playxUserAsset.playx_user_id'),
|
||||||
|
prop: 'playx_user_id',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
{ label: t('mall.playxUserAsset.locked_points'), prop: 'locked_points', align: 'center', operator: 'RANGE', sortable: false },
|
{ label: t('mall.playxUserAsset.locked_points'), prop: 'locked_points', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
{ label: t('mall.playxUserAsset.available_points'), prop: 'available_points', align: 'center', operator: 'RANGE', sortable: false },
|
{ label: t('mall.playxUserAsset.available_points'), prop: 'available_points', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
{ label: t('mall.playxUserAsset.today_limit'), prop: 'today_limit', align: 'center', operator: 'RANGE', sortable: false },
|
{ label: t('mall.playxUserAsset.today_limit'), prop: 'today_limit', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
{ label: t('mall.playxUserAsset.today_claimed'), prop: 'today_claimed', align: 'center', operator: 'RANGE', sortable: false },
|
{ label: t('mall.playxUserAsset.today_claimed'), prop: 'today_claimed', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
{ label: t('mall.playxUserAsset.today_limit_date'), prop: 'today_limit_date', align: 'center', render: 'date', operator: 'RANGE', comSearchRender: 'date', sortable: 'custom', width: 120, operatorPlaceholder: t('Fuzzy query') },
|
{
|
||||||
{ label: t('mall.playxUserAsset.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
|
label: t('mall.playxUserAsset.today_limit_date'),
|
||||||
{ label: t('mall.playxUserAsset.update_time'), prop: 'update_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' },
|
prop: 'today_limit_date',
|
||||||
|
align: 'center',
|
||||||
|
render: 'date',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'date',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 120,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.playxUserAsset.create_time'),
|
||||||
|
prop: 'create_time',
|
||||||
|
align: 'center',
|
||||||
|
render: 'datetime',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.playxUserAsset.update_time'),
|
||||||
|
prop: 'update_time',
|
||||||
|
align: 'center',
|
||||||
|
render: 'datetime',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 160,
|
||||||
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
dblClickNotEditColumn: [undefined],
|
dblClickNotEditColumn: [undefined],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
defaultItems: {},
|
defaultItems: {},
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
provide('baTable', baTable)
|
provide('baTable', baTable)
|
||||||
@@ -63,4 +115,3 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ const baTable = new baTableClass(
|
|||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('mall.redemptionOrder.malluser__username'),
|
label: t('mall.redemptionOrder.playxuserasset__username'),
|
||||||
prop: 'mallUser.username',
|
prop: 'playxUserAsset.username',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
|||||||
@@ -37,12 +37,12 @@
|
|||||||
:placeholder="t('Please input field', { field: t('mall.redemptionOrder.order') })"
|
:placeholder="t('Please input field', { field: t('mall.redemptionOrder.order') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.redemptionOrder.mall_user_id')"
|
:label="t('mall.redemptionOrder.playx_user_asset_id')"
|
||||||
type="remoteSelect"
|
type="remoteSelect"
|
||||||
v-model="baTable.form.items!.mall_user_id"
|
v-model="baTable.form.items!.playx_user_asset_id"
|
||||||
prop="mall_user_id"
|
prop="playx_user_asset_id"
|
||||||
:input-attr="{ pk: 'mall_user.id', field: 'username', remoteUrl: '/admin/mall.User/index' }"
|
:input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }"
|
||||||
:placeholder="t('Please select field', { field: t('mall.redemptionOrder.mall_user_id') })"
|
:placeholder="t('Please select field', { field: t('mall.redemptionOrder.playx_user_asset_id') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.redemptionOrder.status')"
|
:label="t('mall.redemptionOrder.status')"
|
||||||
|
|||||||
@@ -2,19 +2,13 @@
|
|||||||
<div class="default-main ba-table-box">
|
<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 />
|
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||||
|
|
||||||
<!-- 表格顶部菜单 -->
|
|
||||||
<!-- 自定义按钮请使用插槽,甚至公共搜索也可以使用具名插槽渲染,参见文档 -->
|
|
||||||
<TableHeader
|
<TableHeader
|
||||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
:buttons="['refresh', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.user.quick Search Fields') })"
|
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.userAsset.quick Search Fields') })"
|
||||||
></TableHeader>
|
></TableHeader>
|
||||||
|
|
||||||
<!-- 表格 -->
|
|
||||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
|
||||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
|
||||||
<Table ref="tableRef"></Table>
|
<Table ref="tableRef"></Table>
|
||||||
|
|
||||||
<!-- 表单 -->
|
|
||||||
<PopupForm />
|
<PopupForm />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,47 +24,61 @@ import Table from '/@/components/table/index.vue'
|
|||||||
import baTableClass from '/@/utils/baTable'
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'mall/user',
|
name: 'mall/userAsset',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const tableRef = useTemplateRef('tableRef')
|
const tableRef = useTemplateRef('tableRef')
|
||||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||||
|
|
||||||
/**
|
|
||||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
|
||||||
*/
|
|
||||||
const baTable = new baTableClass(
|
const baTable = new baTableClass(
|
||||||
new baTableApi('/admin/mall.User/'),
|
new baTableApi('/admin/mall.UserAsset/'),
|
||||||
{
|
{
|
||||||
pk: 'id',
|
pk: 'id',
|
||||||
column: [
|
column: [
|
||||||
{ type: 'selection', align: 'center', operator: false },
|
{ type: 'selection', align: 'center', operator: false },
|
||||||
{ label: t('mall.user.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
{ label: t('mall.userAsset.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||||
{
|
{
|
||||||
label: t('mall.user.username'),
|
label: t('mall.userAsset.username'),
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
},
|
},
|
||||||
{ label: t('mall.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
|
||||||
{ label: t('mall.user.score'), prop: 'score', align: 'center', sortable: false, operator: 'RANGE' },
|
|
||||||
{ label: t('mall.user.daily_claim'), prop: 'daily_claim', align: 'center', sortable: false, operator: 'RANGE' },
|
|
||||||
{ label: t('mall.user.daily_claim_use'), prop: 'daily_claim_use', align: 'center', sortable: false, operator: 'RANGE' },
|
|
||||||
{ label: t('mall.user.available_for_withdrawal'), prop: 'available_for_withdrawal', align: 'center', sortable: false, operator: 'RANGE' },
|
|
||||||
{
|
{
|
||||||
label: t('mall.user.admin__username'),
|
label: t('mall.userAsset.phone'),
|
||||||
prop: 'admin.username',
|
prop: 'phone',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
render: 'tags',
|
sortable: false,
|
||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
comSearchRender: 'string',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('mall.user.create_time'),
|
label: t('mall.userAsset.playx_user_id'),
|
||||||
|
prop: 'playx_user_id',
|
||||||
|
align: 'center',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
sortable: false,
|
||||||
|
operator: 'LIKE',
|
||||||
|
},
|
||||||
|
{ label: t('mall.userAsset.locked_points'), prop: 'locked_points', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
|
{ label: t('mall.userAsset.available_points'), prop: 'available_points', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
|
{ label: t('mall.userAsset.today_limit'), prop: 'today_limit', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
|
{ label: t('mall.userAsset.today_claimed'), prop: 'today_claimed', align: 'center', operator: 'RANGE', sortable: false },
|
||||||
|
{
|
||||||
|
label: t('mall.userAsset.today_limit_date'),
|
||||||
|
prop: 'today_limit_date',
|
||||||
|
align: 'center',
|
||||||
|
render: 'date',
|
||||||
|
operator: 'RANGE',
|
||||||
|
comSearchRender: 'date',
|
||||||
|
sortable: 'custom',
|
||||||
|
width: 120,
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.userAsset.create_time'),
|
||||||
prop: 'create_time',
|
prop: 'create_time',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: 'datetime',
|
render: 'datetime',
|
||||||
@@ -81,7 +89,7 @@ const baTable = new baTableClass(
|
|||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('mall.user.update_time'),
|
label: t('mall.userAsset.update_time'),
|
||||||
prop: 'update_time',
|
prop: 'update_time',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: 'datetime',
|
render: 'datetime',
|
||||||
@@ -91,17 +99,12 @@ const baTable = new baTableClass(
|
|||||||
width: 160,
|
width: 160,
|
||||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||||
},
|
},
|
||||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
{ label: t('Operate'), align: 'center', fixed: 'right', width: 80, render: 'buttons', buttons: optButtons, operator: false },
|
||||||
],
|
],
|
||||||
dblClickNotEditColumn: [undefined],
|
dblClickNotEditColumn: [undefined],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
defaultItems: {
|
defaultItems: {},
|
||||||
score: 0,
|
|
||||||
daily_claim: 100,
|
|
||||||
daily_claim_use: 0,
|
|
||||||
available_for_withdrawal: 0,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 对话框表单 -->
|
|
||||||
<!-- 建议使用 Prettier 格式化代码 -->
|
|
||||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
class="ba-operate-dialog"
|
class="ba-operate-dialog"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
:model-value="['Edit'].includes(baTable.form.operate!)"
|
||||||
@close="baTable.toggleForm"
|
@close="baTable.toggleForm"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -13,6 +10,7 @@
|
|||||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||||
<div
|
<div
|
||||||
class="ba-operate-form"
|
class="ba-operate-form"
|
||||||
@@ -29,67 +27,69 @@
|
|||||||
:label-width="baTable.form.labelWidth + 'px'"
|
:label-width="baTable.form.labelWidth + 'px'"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
>
|
>
|
||||||
|
<FormItem :label="t('mall.userAsset.id')" type="number" v-model="baTable.form.items!.id" prop="id" :input-attr="{ disabled: true }" />
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.user.username')"
|
:label="t('mall.userAsset.playx_user_id')"
|
||||||
|
type="string"
|
||||||
|
v-model="baTable.form.items!.playx_user_id"
|
||||||
|
prop="playx_user_id"
|
||||||
|
:input-attr="{ disabled: true }"
|
||||||
|
/>
|
||||||
|
<FormItem
|
||||||
|
:label="t('mall.userAsset.username')"
|
||||||
type="string"
|
type="string"
|
||||||
v-model="baTable.form.items!.username"
|
v-model="baTable.form.items!.username"
|
||||||
prop="username"
|
prop="username"
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.username') })"
|
:placeholder="t('Please input field', { field: t('mall.userAsset.username') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.user.phone')"
|
:label="t('mall.userAsset.phone')"
|
||||||
type="string"
|
type="string"
|
||||||
v-model="baTable.form.items!.phone"
|
v-model="baTable.form.items!.phone"
|
||||||
prop="phone"
|
prop="phone"
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.phone') })"
|
:placeholder="t('Please input field', { field: t('mall.userAsset.phone') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.user.password')"
|
:label="t('mall.userAsset.locked_points')"
|
||||||
type="password"
|
|
||||||
v-model="baTable.form.items!.password"
|
|
||||||
prop="password"
|
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.password') })"
|
|
||||||
/>
|
|
||||||
<FormItem
|
|
||||||
:label="t('mall.user.score')"
|
|
||||||
type="number"
|
type="number"
|
||||||
v-model="baTable.form.items!.score"
|
v-model="baTable.form.items!.locked_points"
|
||||||
prop="score"
|
prop="locked_points"
|
||||||
:input-attr="{ step: 1 }"
|
:input-attr="{ step: 1, min: 0 }"
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.score') })"
|
:placeholder="t('Please input field', { field: t('mall.userAsset.locked_points') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.user.daily_claim')"
|
:label="t('mall.userAsset.available_points')"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="baTable.form.items!.daily_claim"
|
v-model="baTable.form.items!.available_points"
|
||||||
prop="daily_claim"
|
prop="available_points"
|
||||||
:input-attr="{ step: 1 }"
|
:input-attr="{ step: 1, min: 0 }"
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.daily_claim') })"
|
:placeholder="t('Please input field', { field: t('mall.userAsset.available_points') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.user.daily_claim_use')"
|
:label="t('mall.userAsset.today_limit')"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="baTable.form.items!.daily_claim_use"
|
v-model="baTable.form.items!.today_limit"
|
||||||
prop="daily_claim_use"
|
prop="today_limit"
|
||||||
:input-attr="{ step: 1 }"
|
:input-attr="{ step: 1, min: 0 }"
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.daily_claim_use') })"
|
:placeholder="t('Please input field', { field: t('mall.userAsset.today_limit') })"
|
||||||
/>
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.user.available_for_withdrawal')"
|
:label="t('mall.userAsset.today_claimed')"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="baTable.form.items!.available_for_withdrawal"
|
v-model="baTable.form.items!.today_claimed"
|
||||||
prop="available_for_withdrawal"
|
prop="today_claimed"
|
||||||
:input-attr="{ step: 1 }"
|
:input-attr="{ step: 1, min: 0 }"
|
||||||
:placeholder="t('Please input field', { field: t('mall.user.available_for_withdrawal') })"
|
:placeholder="t('Please input field', { field: t('mall.userAsset.today_claimed') })"
|
||||||
/>
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
{{ t('Save') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -97,31 +97,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormItemRule } from 'element-plus'
|
|
||||||
import { inject, reactive, useTemplateRef } from 'vue'
|
import { inject, reactive, useTemplateRef } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import FormItem from '/@/components/formItem/index.vue'
|
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import type baTableClass from '/@/utils/baTable'
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
|
import type { FormItemRule } from 'element-plus'
|
||||||
import { buildValidatorData } from '/@/utils/validate'
|
import { buildValidatorData } from '/@/utils/validate'
|
||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
const baTable = inject('baTable') as baTableClass
|
const baTable = inject('baTable') as baTableClass
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||||
username: [buildValidatorData({ name: 'required', title: t('mall.user.username') })],
|
username: [buildValidatorData({ name: 'required', title: t('mall.userAsset.username') })],
|
||||||
phone: [buildValidatorData({ name: 'required', title: t('mall.user.phone') })],
|
phone: [buildValidatorData({ name: 'required', title: t('mall.userAsset.phone') })],
|
||||||
password: [buildValidatorData({ name: 'password', title: t('mall.user.password') })],
|
locked_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.locked_points') })],
|
||||||
score: [buildValidatorData({ name: 'number', title: t('mall.user.score') })],
|
available_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.available_points') })],
|
||||||
daily_claim: [buildValidatorData({ name: 'number', title: t('mall.user.daily_claim') })],
|
today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],
|
||||||
daily_claim_use: [buildValidatorData({ name: 'number', title: t('mall.user.daily_claim_use') })],
|
today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })],
|
||||||
available_for_withdrawal: [buildValidatorData({ name: 'number', title: t('mall.user.available_for_withdrawal') })],
|
|
||||||
create_time: [buildValidatorData({ name: 'date', title: t('mall.user.create_time') })],
|
|
||||||
update_time: [buildValidatorData({ name: 'date', title: t('mall.user.update_time') })],
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|
||||||
@@ -82,13 +82,6 @@ const baTable = new baTableClass(new baTableApi('/admin/routine.Attachment/'), {
|
|||||||
operator: 'LIKE',
|
operator: 'LIKE',
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: t('routine.attachment.Upload user'),
|
|
||||||
prop: 'user.nickname',
|
|
||||||
align: 'center',
|
|
||||||
operator: 'LIKE',
|
|
||||||
operatorPlaceholder: t('Fuzzy query'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('utils.size'),
|
label: t('utils.size'),
|
||||||
prop: 'size',
|
prop: 'size',
|
||||||
|
|||||||
1
web/types/tableRenderer.d.ts
vendored
1
web/types/tableRenderer.d.ts
vendored
@@ -4,6 +4,7 @@ type TableRenderer =
|
|||||||
| 'color'
|
| 'color'
|
||||||
| 'customRender'
|
| 'customRender'
|
||||||
| 'customTemplate'
|
| 'customTemplate'
|
||||||
|
| 'date'
|
||||||
| 'datetime'
|
| 'datetime'
|
||||||
| 'icon'
|
| 'icon'
|
||||||
| 'image'
|
| 'image'
|
||||||
|
|||||||
Reference in New Issue
Block a user