[积分商城]优化对接API

This commit is contained in:
2026-03-30 11:47:32 +08:00
parent b30ef21780
commit 4a42899bfe
55 changed files with 835 additions and 1241 deletions

View File

@@ -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` 一对一扩展资产表 |
--- ---

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 中对应的方法至此进行重写
*/
}

View File

@@ -173,19 +173,9 @@ class PlayxOrder extends Backend
Db::startTrans(); Db::startTrans();
try { try {
$asset = MallPlayxUserAsset::where('user_id', strval($order->user_id ?? ''))->find(); $asset = MallPlayxUserAsset::where('playx_user_id', strval($order->user_id ?? ''))->find();
if (!$asset) { if (!$asset) {
$asset = MallPlayxUserAsset::create([ throw new \RuntimeException('User asset not found');
'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); $refund = intval($order->points_cost ?? 0);

View File

@@ -2,7 +2,6 @@
namespace app\admin\controller\mall; namespace app\admin\controller\mall;
use Throwable;
use app\common\controller\Backend; use app\common\controller\Backend;
use support\Response; use support\Response;
use Webman\Http\Request; use Webman\Http\Request;
@@ -20,12 +19,17 @@ class PlayxUserAsset extends Backend
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time']; protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
protected string|array $quickSearchField = ['user_id', 'username']; protected string|array $quickSearchField = [
'playx_user_id',
'username',
'phone',
];
protected string|array $indexField = [ protected string|array $indexField = [
'id', 'id',
'user_id', 'playx_user_id',
'username', 'username',
'phone',
'locked_points', 'locked_points',
'available_points', 'available_points',
'today_limit', 'today_limit',
@@ -42,17 +46,36 @@ class PlayxUserAsset extends Backend
} }
/** /**
* 查看 * 远程下拉:资产主键 id + 用户名(用于地址/订单等关联)
* @throws Throwable
*/ */
public function index(Request $request): Response public function select(Request $request): Response
{ {
$response = $this->initializeBackend($request); $response = $this->initializeBackend($request);
if ($response !== null) { if ($response !== null) {
return $response; return $response;
} }
return $this->_index(); list($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' => intval($arr['id'] ?? 0),
'username' => strval($arr['username'] ?? ''),
];
}
return $this->success('', [
'list' => $list,
'total' => $res->total(),
'remark' => get_route_remark(),
]);
} }
} }

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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'],

View File

@@ -4,9 +4,14 @@ 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\ChannelManage;
use app\common\model\MallPlayxUserAsset;
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 +31,11 @@ class Auth extends Api
*/ */
protected int $timeTolerance = 300; protected int $timeTolerance = 300;
/**
* 临时登录 token 有效期(秒)
*/
protected int $tempTokenExpire = 86400;
/** /**
* 获取鉴权 TokenGET 请求) * 获取鉴权 TokenGET 请求)
* 参数仅从 Query 读取signature、secret、agent_id、time * 参数仅从 Query 读取signature、secret、agent_id、time
@@ -47,7 +57,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,7 +72,7 @@ class Auth extends Api
return $this->error(__('Agent not found')); return $this->error(__('Agent not found'));
} }
$channelId = (int) ($admin->channel_id ?? 0); $channelId = intval($admin->channel_id ?? 0);
if ($channelId <= 0) { if ($channelId <= 0) {
return $this->error(__('Agent not found')); return $this->error(__('Agent not found'));
} }
@@ -81,7 +91,7 @@ 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, 'channel_id' => $channel->id,
@@ -93,4 +103,52 @@ class Auth extends Api
'authtoken' => $authtoken, 'authtoken' => $authtoken,
]); ]);
} }
/**
* H5 临时登录GET/POST
* 参数username
* 写入或复用 mall_playx_user_asset签发 muser 类型 tokenuser_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', ''))));
if ($username === '') {
return $this->error(__('Parameter username can not be empty'));
}
try {
$asset = MallPlayxUserAsset::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,
],
]);
}
} }

View File

@@ -4,7 +4,10 @@ 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\MallPlayxClaimLog;
use app\common\model\MallPlayxDailyPush; use app\common\model\MallPlayxDailyPush;
@@ -21,28 +24,125 @@ use support\Response;
class Playx extends Api class Playx extends Api
{ {
/** /**
* 从请求解析 PlayX 会话用户ID优先 session_id其次 user_id * 从请求解析 mall_playx_user_asset.idmuser 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 = MallPlayxSession::where('session_id', $sessionId)->find();
if (!$session) { if ($session) {
return null;
}
$expireTime = intval($session->expire_time ?? 0); $expireTime = intval($session->expire_time ?? 0);
if ($expireTime <= time()) { if ($expireTime > time()) {
return null; $asset = MallPlayxUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find();
if ($asset) {
return intval($asset->getKey());
} }
return strval($session->user_id ?? ''); }
}
$assetId = $this->resolveAssetIdByToken($sessionId);
if ($assetId !== null) {
return $assetId;
}
}
$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 = MallPlayxUserAsset::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 (!MallPlayxUserAsset::where('phone', $candidate)->find()) {
return $candidate;
}
}
return null;
}
private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallPlayxUserAsset
{
$asset = MallPlayxUserAsset::where('playx_user_id', $playxUserId)->find();
if ($asset) {
return $asset;
}
$effectiveUsername = trim($username);
if ($effectiveUsername === '') {
$effectiveUsername = 'playx_' . $playxUserId;
}
$byName = MallPlayxUserAsset::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 MallPlayxUserAsset::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): ?MallPlayxUserAsset
{
return MallPlayxUserAsset::where('id', $assetId)->find();
} }
/** /**
@@ -66,11 +166,11 @@ class Playx extends Api
$requestId = $body['request_id'] ?? ''; $requestId = $body['request_id'] ?? '';
$date = $body['date'] ?? ''; $date = $body['date'] ?? '';
$userId = $body['user_id'] ?? ''; $playxUserId = strval($body['user_id'] ?? '');
$yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0; $yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0;
$yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0; $yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0;
if ($requestId === '' || $date === '' || $userId === '') { if ($requestId === '' || $date === '' || $playxUserId === '') {
return $this->error(__('Missing required fields: request_id, date, user_id')); return $this->error(__('Missing required fields: request_id, date, user_id'));
} }
@@ -80,29 +180,29 @@ class Playx extends Api
$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(__('Invalid signature'), 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/playx/daily-push\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(__('Invalid signature'), null, 0, ['statusCode' => 401]);
} }
} }
$exists = MallPlayxDailyPush::where('user_id', $userId)->where('date', $date)->find(); $exists = MallPlayxDailyPush::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([ MallPlayxDailyPush::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,
@@ -121,30 +221,23 @@ class Playx extends Api
} }
$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'] ?? ''));
if (!$asset) {
throw new \RuntimeException(__('Failed to map playx user to mall user'));
}
$todayLimitDate = $date; $todayLimitDate = $date;
if ($asset) {
if ($asset->today_limit_date !== $todayLimitDate) { if ($asset->today_limit_date !== $todayLimitDate) {
$asset->today_claimed = 0; $asset->today_claimed = 0;
$asset->today_limit_date = $todayLimitDate; $asset->today_limit_date = $todayLimitDate;
} }
$asset->locked_points += $newLocked; $asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
$asset->today_limit = $todayLimit; $asset->today_limit = $todayLimit;
$asset->username = $body['username'] ?? $asset->username; $asset->playx_user_id = $playxUserId;
$asset->save(); $uname = trim(strval($body['username'] ?? ''));
} else { if ($uname !== '') {
MallPlayxUserAsset::create([ $asset->username = $uname;
'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(),
]);
} }
$asset->save();
Db::commit(); Db::commit();
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -156,13 +249,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 +264,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(__('Invalid token'), 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 +293,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 : __('Invalid token');
return $this->error($msg, null, 0, ['statusCode' => 401]);
} }
$userId = strval($data['user_id']); $userId = strval($data['user_id']);
@@ -231,6 +331,53 @@ class Playx extends Api
} }
} }
/**
* 本地校验 temLogin 等写入的商城 token类型 muser写入 mall_playx_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(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$tokenType = strval($tokenData['type'] ?? '');
if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$assetId = intval($tokenData['user_id'] ?? 0);
if ($assetId <= 0) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$asset = MallPlayxUserAsset::where('id', $assetId)->find();
if (!$asset) {
return $this->error(__('Invalid token'), 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));
MallPlayxSession::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 +389,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(__('Invalid token'), 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,22 +429,22 @@ 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 ($claimRequestId === '' || $assetId === null) {
return $this->error(__('claim_request_id and user_id/session_id required')); return $this->error(__('claim_request_id and user_id/session_id required'));
} }
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
return $this->error(__('User asset not found'));
}
$playxUserId = strval($asset->playx_user_id);
$exists = MallPlayxClaimLog::where('claim_request_id', $claimRequestId)->find(); $exists = MallPlayxClaimLog::where('claim_request_id', $claimRequestId)->find();
if ($exists) { if ($exists) {
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
return $this->success('', $this->formatAsset($asset)); return $this->success('', $this->formatAsset($asset));
} }
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
if (!$asset) {
return $this->error(__('User asset not found'));
}
$todayLimitDate = date('Y-m-d'); $todayLimitDate = date('Y-m-d');
if ($asset->today_limit_date !== $todayLimitDate) { if ($asset->today_limit_date !== $todayLimitDate) {
$asset->today_claimed = 0; $asset->today_claimed = 0;
@@ -315,7 +462,7 @@ class Playx extends Api
try { try {
MallPlayxClaimLog::create([ MallPlayxClaimLog::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 +542,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(__('Invalid token'), 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 = MallPlayxOrder::where('user_id', strval($asset->playx_user_id))
->with(['mallItem']) ->with(['mallItem'])
->order('id', 'desc') ->order('id', 'desc')
->limit(100) ->limit(100)
@@ -438,8 +589,8 @@ 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 ($itemId <= 0 || $assetId === null) {
return $this->error(__('item_id and user_id/session_id required')); return $this->error(__('item_id and user_id/session_id required'));
} }
@@ -448,10 +599,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) {
@@ -466,7 +618,7 @@ class Playx extends Api
$orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999); $orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
$order = MallPlayxOrder::create([ $order = MallPlayxOrder::create([
'user_id' => $userId, 'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_BONUS, 'type' => MallPlayxOrder::TYPE_BONUS,
'status' => MallPlayxOrder::STATUS_PENDING, 'status' => MallPlayxOrder::STATUS_PENDING,
'mall_item_id' => $item->id, 'mall_item_id' => $item->id,
@@ -487,7 +639,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 +656,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);
$receiverName = $request->post('receiver_name', ''); $receiverName = $request->post('receiver_name', '');
$receiverPhone = $request->post('receiver_phone', ''); $receiverPhone = $request->post('receiver_phone', '');
$receiverAddress = $request->post('receiver_address', ''); $receiverAddress = $request->post('receiver_address', '');
if ($itemId <= 0 || $userId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') { if ($itemId <= 0 || $assetId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
return $this->error(__('Missing required fields')); return $this->error(__('Missing required fields'));
} }
@@ -520,10 +672,11 @@ 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 {
@@ -531,7 +684,7 @@ class Playx extends Api
$asset->save(); $asset->save();
MallPlayxOrder::create([ MallPlayxOrder::create([
'user_id' => $userId, 'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_PHYSICAL, 'type' => MallPlayxOrder::TYPE_PHYSICAL,
'status' => MallPlayxOrder::STATUS_PENDING, 'status' => MallPlayxOrder::STATUS_PENDING,
'mall_item_id' => $item->id, 'mall_item_id' => $item->id,
@@ -565,8 +718,8 @@ 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 ($itemId <= 0 || $assetId === null) {
return $this->error(__('item_id and user_id/session_id required')); return $this->error(__('item_id and user_id/session_id required'));
} }
@@ -575,10 +728,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) {
@@ -593,7 +747,7 @@ class Playx extends Api
$orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999); $orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
$order = MallPlayxOrder::create([ $order = MallPlayxOrder::create([
'user_id' => $userId, 'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_WITHDRAW, 'type' => MallPlayxOrder::TYPE_WITHDRAW,
'status' => MallPlayxOrder::STATUS_PENDING, 'status' => MallPlayxOrder::STATUS_PENDING,
'mall_item_id' => $item->id, 'mall_item_id' => $item->id,
@@ -614,7 +768,7 @@ class Playx extends Api
$baseUrl = config('playx.api.base_url', ''); $baseUrl = config('playx.api.base_url', '');
if ($baseUrl !== '') { if ($baseUrl !== '') {
$this->callPlayxBalanceCredit($order, $userId); $this->callPlayxBalanceCredit($order, $playxUserId);
} }
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [ return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [

View File

@@ -19,10 +19,32 @@ 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',
// 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',
'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',
'Out of stock' => 'Out of stock',
'Redeem success' => 'Redeem successful',
'Withdraw submitted, please wait about 10 minutes' => 'Withdrawal submitted, please wait about 10 minutes',
]; ];

View File

@@ -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,23 @@ 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',
'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' => '库存不足',
'Redeem success' => '兑换成功',
'Withdraw submitted, please wait about 10 minutes' => '提现申请已提交,请等待约 10 分钟',
]; ];

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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',
]; ];
$origin = $request->header('origin'); $origin = $request->header('origin');
if (is_array($origin)) { if (is_array($origin)) {

View File

@@ -11,6 +11,9 @@ use Webman\Http\Response;
/** /**
* 加载控制器语言包中间件Webman 迁移版,等价 ThinkPHP LoadLangPack * 加载控制器语言包中间件Webman 迁移版,等价 ThinkPHP LoadLangPack
* 根据当前路由加载对应控制器的语言包到 Translator * 根据当前路由加载对应控制器的语言包到 Translator
*
* 对外 api/:优先请求头 langzh / zh-cn → 中文包 zh-cnen → 英文包),未传则 think-lang再默认 zh-cn不根据浏览器 Accept-Language
* admin/think-lang → Accept-Language → 配置默认
*/ */
class LoadLangPack implements MiddlewareInterface class LoadLangPack implements MiddlewareInterface
{ {
@@ -25,14 +28,49 @@ 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 {
// 对外 APIPlayX、H5 等):优先 lang 请求头,默认中文 zh-cn不跟随浏览器 Accept-Language
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';
} elseif ($isAdmin) {
$acceptLang = $request->header('accept-language', ''); $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)) { if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
$langSet = 'zh-cn'; $langSet = 'zh-cn';
} elseif (preg_match('/^en/i', $acceptLang)) { } elseif (preg_match('/^en/i', $acceptLang)) {
@@ -40,7 +78,11 @@ class LoadLangPack implements MiddlewareInterface
} 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($langSet)); $langSet = str_replace('_', '-', strtolower((string) $langSet));
} else {
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
$langSet = str_replace('_', '-', strtolower((string) $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-cnen → 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;
}
} }

View File

@@ -45,8 +45,8 @@ class MallAddress extends Model
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\MallPlayxUserAsset::class, 'playx_user_asset_id', 'id');
} }
} }

View File

@@ -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\MallPlayxUserAsset::class, 'playx_user_asset_id', 'id');
} }
} }

View File

@@ -4,10 +4,11 @@ declare(strict_types=1);
namespace app\common\model; namespace app\common\model;
use ba\Random;
use support\think\Model; use support\think\Model;
/** /**
* PlayX 用户资产 * PlayX 用户资产(积分商城用户主表,含登录账号字段)
*/ */
class MallPlayxUserAsset extends Model class MallPlayxUserAsset extends Model
{ {
@@ -22,5 +23,66 @@ class MallPlayxUserAsset extends Model
'available_points' => 'integer', 'available_points' => 'integer',
'today_limit' => 'integer', 'today_limit' => 'integer',
'today_claimed' => '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 = self::allocateUniquePhone();
if ($phone === null) {
throw new \RuntimeException('Failed to allocate unique phone');
}
$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_playx_user_asset');
}
$id = intval($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;
}
private static function allocateUniquePhone(): ?string
{
for ($i = 0; $i < 8; $i++) {
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
if (!self::where('phone', $candidate)->find()) {
return $candidate;
}
}
return null;
}
} }

View File

@@ -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\MallPlayxUserAsset::class, 'playx_user_asset_id', 'id');
} }
public function mallItem(): \think\model\relation\BelongsTo public function mallItem(): \think\model\relation\BelongsTo

View File

@@ -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;
}
}

View File

@@ -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'],
];
}

View File

@@ -267,7 +267,7 @@ class PlayxJobs
if ($order->points_cost <= 0) { if ($order->points_cost <= 0) {
return; return;
} }
$asset = MallPlayxUserAsset::where('user_id', $order->user_id)->find(); $asset = MallPlayxUserAsset::where('playx_user_id', $order->user_id)->find();
if (!$asset) { if (!$asset) {
return; return;
} }

View File

@@ -5,11 +5,11 @@
return [ return [
// 允许跨域访问的域名(* 表示任意;开发可用 *,生产建议填具体域名) // 允许跨域访问的域名(* 表示任意;开发可用 *,生产建议填具体域名)
'cors_request_domain' => '*', 'cors_request_domain' => '*,playx-api.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则无限
@@ -89,8 +89,12 @@ return [
], ],
// JWT 签名密钥(留空则使用 token.key // JWT 签名密钥(留空则使用 token.key
'jwt_secret' => '', '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',

View File

@@ -14,6 +14,11 @@ return [
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')), 'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_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', '')),

View File

@@ -110,6 +110,7 @@ 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/playx/daily-push', [\app\api\controller\v1\Playx::class, 'dailyPush']);

View File

@@ -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();
}
}
}

View File

@@ -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` 当作唯一可信凭据直传下单;**以 tokensession** 或由商城签发 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 失效(如 401H5 可通过 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 → 商城)

View File

@@ -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 调用商城)
@@ -31,8 +31,8 @@
|------|------|------|------| |------|------|------|------|
| `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 | 否 | 历史总充值 |
@@ -102,8 +102,9 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
## 2. PlayX -> 积分商城(商城调用 PlayX ## 2. PlayX -> 积分商城(商城调用 PlayX
> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。 > 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。
> **说明**H5 调商城的 **`/api/v1/playx/verify-token`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
### 2.1 Token Verification API ### 2.1 Token Verification APIPlayX 侧实现,远程验证时使用)
* 方法:`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 +258,134 @@ 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`
* 方法:`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/playx/verify-token` * 路径:`/api/v1/playx/verify-token`
#### 请求 Body #### 配置:本地验证 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`
#### 请求参数
必填其一: 必填其一:
* `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 401msg=`INVALID_TOKEN`
* PlayX 未配置msg=`PlayX API not configured`
#### 示例 * token 为空HTTP 401msg=`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/playx/verify-token' \
-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'
``` ```
#### 远程模式(`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 +401,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/playx/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 +425,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/playx/assets' --data-urlencode 'token=上一步temLogin返回的token'
``` ```
响应(示例): 响应(示例):
@@ -355,15 +446,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/playx/claim`
#### 请求 Body #### 请求 Body
必填: 必填:
* `claim_request_id`幂等键string唯一 * `claim_request_id`幂等键string唯一
鉴权:
* `session_id` `user_id` 鉴权:同 **3.1**`session_id` / `token` / `user_id`
#### 返回(成功 data #### 返回(成功 data
`用户资产` 返回字段一致(资产快照)。 `用户资产` 返回字段一致(资产快照)。
@@ -376,7 +468,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ curl -X POST 'http://localhost:1818/api/v1/playx/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,7 +505,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
--- ---
### 3.4 商品列表 ### 3.6 商品列表
* 方法:`GET` * 方法:`GET`
* 路径:`/api/v1/playx/items` * 路径:`/api/v1/playx/items`
@@ -456,15 +548,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/playx/bonus/redeem`
#### 请求 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`
@@ -494,7 +585,7 @@ 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/playx/physical/redeem`
@@ -504,8 +595,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
* `receiver_name`:收货人 * `receiver_name`:收货人
* `receiver_phone`:收货电话 * `receiver_phone`:收货电话
* `receiver_address`:收货地址 * `receiver_address`:收货地址
鉴权: 鉴权:**3.1**`session_id` / `token` / `user_id`
* `session_id``user_id`
#### 返回(成功) #### 返回(成功)
* `msg``Redeem success` * `msg``Redeem success`
@@ -534,15 +624,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/playx/withdraw/apply`
#### 请求 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`
@@ -572,20 +661,23 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
--- ---
### 3.8 订单列表 ### 3.10 订单列表
* 方法:`GET` * 方法:`GET`
* 路径:`/api/v1/playx/orders` * 路径:`/api/v1/playx/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/playx/orders' --data-urlencode 'token=上一步temLogin返回的token'
``` ```
响应(示例,简化): 响应(示例,简化):
@@ -619,6 +711,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/playx/sync-limit`

View File

@@ -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'

View File

@@ -1,7 +1,7 @@
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',
phone: 'phone', phone: 'phone',
region: 'region', region: 'region',
detail_address: 'detail_address', detail_address: 'detail_address',

View File

@@ -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',

View File

@@ -1,9 +0,0 @@
export default {
id: 'id',
username: 'username',
password: 'password',
create_time: 'create_time',
update_time: 'update_time',
score: 'score',
quickSearchFields: 'id',
}

View File

@@ -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',
} }

View File

@@ -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',

View File

@@ -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',
}

View File

@@ -1,7 +1,7 @@
export default { export default {
id: 'ID', id: 'ID',
mall_user_id: '用户', playx_user_asset_id: '用户资产',
malluser__username: '用户名', playxuserasset__username: '用户名',
phone: '电话', phone: '电话',
region: '地区', region: '地区',
detail_address: '详细地址', detail_address: '详细地址',

View File

@@ -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': '充值',

View File

@@ -1,9 +0,0 @@
export default {
id: 'ID',
username: '用户名',
password: '密码',
create_time: '创建时间',
update_time: '修改时间',
score: '积分',
quickSearchFields: 'ID',
}

View File

@@ -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、用户名、手机号',
} }

View File

@@ -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': '已发放',

View File

@@ -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',
}

View File

@@ -48,8 +48,8 @@ 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'),

View File

@@ -30,12 +30,12 @@
: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_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/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 <FormItem
:label="t('mall.address.phone')" :label="t('mall.address.phone')"

View File

@@ -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'),

View File

@@ -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_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/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') })],

View File

@@ -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>

View File

@@ -1,109 +0,0 @@
<template>
<!-- 对话框表单 -->
<!-- 建议使用 Prettier 格式化代码 -->
<!-- el-form 内可以混用 el-form-itemFormItemba-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>

View File

@@ -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>

View File

@@ -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'),

View File

@@ -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_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/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')"

View File

@@ -1,120 +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.user.quick Search Fields') })"
></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/user',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
/**
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
*/
const baTable = new baTableClass(
new baTableApi('/admin/mall.User/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('mall.user.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{
label: t('mall.user.username'),
prop: 'username',
align: 'center',
operatorPlaceholder: t('Fuzzy query'),
sortable: false,
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'),
prop: 'admin.username',
align: 'center',
operatorPlaceholder: t('Fuzzy query'),
render: 'tags',
operator: 'LIKE',
comSearchRender: 'string',
},
{
label: t('mall.user.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.user.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: 100, render: 'buttons', buttons: optButtons, operator: false },
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: {
score: 0,
daily_claim: 100,
daily_claim_use: 0,
available_for_withdrawal: 0,
},
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -1,127 +0,0 @@
<template>
<!-- 对话框表单 -->
<!-- 建议使用 Prettier 格式化代码 -->
<!-- el-form 内可以混用 el-form-itemFormItemba-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.user.username')"
type="string"
v-model="baTable.form.items!.username"
prop="username"
:placeholder="t('Please input field', { field: t('mall.user.username') })"
/>
<FormItem
:label="t('mall.user.phone')"
type="string"
v-model="baTable.form.items!.phone"
prop="phone"
:placeholder="t('Please input field', { field: t('mall.user.phone') })"
/>
<FormItem
:label="t('mall.user.password')"
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"
v-model="baTable.form.items!.score"
prop="score"
:input-attr="{ step: 1 }"
:placeholder="t('Please input field', { field: t('mall.user.score') })"
/>
<FormItem
:label="t('mall.user.daily_claim')"
type="number"
v-model="baTable.form.items!.daily_claim"
prop="daily_claim"
:input-attr="{ step: 1 }"
:placeholder="t('Please input field', { field: t('mall.user.daily_claim') })"
/>
<FormItem
:label="t('mall.user.daily_claim_use')"
type="number"
v-model="baTable.form.items!.daily_claim_use"
prop="daily_claim_use"
:input-attr="{ step: 1 }"
:placeholder="t('Please input field', { field: t('mall.user.daily_claim_use') })"
/>
<FormItem
:label="t('mall.user.available_for_withdrawal')"
type="number"
v-model="baTable.form.items!.available_for_withdrawal"
prop="available_for_withdrawal"
:input-attr="{ step: 1 }"
:placeholder="t('Please input field', { field: t('mall.user.available_for_withdrawal') })"
/>
</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 } 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.user.username') })],
phone: [buildValidatorData({ name: 'required', title: t('mall.user.phone') })],
password: [buildValidatorData({ name: 'password', title: t('mall.user.password') })],
score: [buildValidatorData({ name: 'number', title: t('mall.user.score') })],
daily_claim: [buildValidatorData({ name: 'number', title: t('mall.user.daily_claim') })],
daily_claim_use: [buildValidatorData({ name: 'number', title: t('mall.user.daily_claim_use') })],
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>
<style scoped lang="scss"></style>