[积分商城]优化对接API
This commit is contained in:
@@ -191,7 +191,7 @@ php webman migrate
|
||||
## 六、路由说明
|
||||
|
||||
- **后台 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/Install/...`
|
||||
|
||||
@@ -205,7 +205,7 @@ php webman migrate
|
||||
| 语言 key 命名错误 | `quick Search Fields` 改为 `quickSearchFields` |
|
||||
| 编辑时密码必填 | 编辑时密码可选,仅新增时必填;后端支持密码加密与重置 |
|
||||
| 多余表单校验 | 移除 `create_time`、`update_time` 的表单校验 |
|
||||
| mall_player 表缺失 | 新增迁移文件 `20250318120000_mall_player.php` |
|
||||
| mall_user 与 PlayX 资产口径混用 | 新增重构迁移并改为 `mall_user` 一对一扩展资产表 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class Address extends Backend
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||
|
||||
protected array $withJoinTable = ['mallUser'];
|
||||
protected array $withJoinTable = ['playxUserAsset'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
@@ -52,10 +52,10 @@ class Address extends Backend
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->with(['mallUser' => function ($query) {
|
||||
->with(['playxUserAsset' => function ($query) {
|
||||
$query->field('id,username');
|
||||
}])
|
||||
->visible(['mallUser' => ['username']])
|
||||
->visible(['playxUserAsset' => ['username']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
|
||||
@@ -19,7 +19,7 @@ class PintsOrder extends Backend
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||
|
||||
protected array $withJoinTable = ['mallUser'];
|
||||
protected array $withJoinTable = ['playxUserAsset'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
@@ -47,10 +47,10 @@ class PintsOrder extends Backend
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->with(['mallUser' => function ($query) {
|
||||
->with(['playxUserAsset' => function ($query) {
|
||||
$query->field('id,username');
|
||||
}])
|
||||
->visible(['mallUser' => ['username']])
|
||||
->visible(['playxUserAsset' => ['username']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\mall;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 积分商城用户
|
||||
*/
|
||||
class Player extends Backend
|
||||
{
|
||||
/**
|
||||
* Player模型对象
|
||||
* @var object|null
|
||||
* @phpstan-var \app\admin\model\mall\Player|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'password'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
/** 列表不返回密码字段 */
|
||||
protected string|array $indexField = ['id', 'username', 'create_time', 'update_time', 'score'];
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
$this->model = new \app\admin\model\mall\Player();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(重写以支持密码加密)
|
||||
*/
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
$this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$passwd = $data['password'] ?? '';
|
||||
if (empty($passwd)) {
|
||||
$this->error(__('Parameter %s can not be empty', [__('Password')]));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
if ($result !== false && $passwd) {
|
||||
$this->model->resetPassword((int) $this->model->id, $passwd);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(重写以支持编辑时密码可选)
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
$this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
$this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword((int) $row->id, $data['password']);
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$validate->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
unset($row['password']);
|
||||
$row['password'] = '';
|
||||
$this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若需重写查看、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
|
||||
*/
|
||||
}
|
||||
@@ -173,19 +173,9 @@ class PlayxOrder extends Backend
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$asset = MallPlayxUserAsset::where('user_id', strval($order->user_id ?? ''))->find();
|
||||
$asset = MallPlayxUserAsset::where('playx_user_id', strval($order->user_id ?? ''))->find();
|
||||
if (!$asset) {
|
||||
$asset = MallPlayxUserAsset::create([
|
||||
'user_id' => strval($order->user_id ?? ''),
|
||||
'username' => strval($order->user_id ?? ''),
|
||||
'locked_points' => 0,
|
||||
'available_points' => 0,
|
||||
'today_limit' => 0,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => null,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
throw new \RuntimeException('User asset not found');
|
||||
}
|
||||
|
||||
$refund = intval($order->points_cost ?? 0);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace app\admin\controller\mall;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
@@ -20,12 +19,17 @@ class PlayxUserAsset extends Backend
|
||||
|
||||
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 = [
|
||||
'id',
|
||||
'user_id',
|
||||
'playx_user_id',
|
||||
'username',
|
||||
'phone',
|
||||
'locked_points',
|
||||
'available_points',
|
||||
'today_limit',
|
||||
@@ -42,17 +46,36 @@ class PlayxUserAsset extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
* @throws Throwable
|
||||
* 远程下拉:资产主键 id + 用户名(用于地址/订单等关联)
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class RedemptionOrder extends Backend
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||
|
||||
protected array $withJoinTable = ['mallUser', 'mallItem'];
|
||||
protected array $withJoinTable = ['playxUserAsset', 'mallItem'];
|
||||
|
||||
protected string|array $quickSearchField = ['id'];
|
||||
|
||||
@@ -52,10 +52,10 @@ class RedemptionOrder extends Backend
|
||||
*/
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->with(['mallUser' => function ($query) {
|
||||
->with(['playxUserAsset' => function ($query) {
|
||||
$query->field('id,username');
|
||||
}])
|
||||
->visible(['mallUser' => ['username'], 'mallItem' => ['title']])
|
||||
->visible(['playxUserAsset' => ['username'], 'mallItem' => ['title']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\mall;
|
||||
|
||||
use Throwable;
|
||||
use app\common\controller\Backend;
|
||||
use support\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* 商城用户
|
||||
*/
|
||||
class User extends Backend
|
||||
{
|
||||
/**
|
||||
* @var \app\common\model\MallUser|null
|
||||
*/
|
||||
protected ?object $model = null;
|
||||
|
||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time', 'password'];
|
||||
|
||||
protected array $withJoinTable = ['admin'];
|
||||
|
||||
protected string|array $quickSearchField = ['id', 'username', 'phone'];
|
||||
|
||||
/** 列表不返回密码 */
|
||||
protected string|array $indexField = ['id', 'username', 'phone', 'score', 'daily_claim', 'daily_claim_use', 'available_for_withdrawal', 'admin_id', 'create_time', 'update_time'];
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
parent::initialize();
|
||||
$this->model = new \app\common\model\MallUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->get('select') || $request->post('select')) {
|
||||
return $this->select($request);
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withoutField('password')
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->visible(['admin' => ['username']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
'remark' => get_route_remark(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加(密码加密)
|
||||
*/
|
||||
public function add(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
$passwd = $data['password'] ?? '';
|
||||
if (empty($passwd)) {
|
||||
return $this->error(__('Parameter %s can not be empty', [__('Password')]));
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
//保存管理员admin_id
|
||||
$data['admin_id'] = $this->auth->id;
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
if ($result !== false && $passwd) {
|
||||
$this->model->resetPassword((int) $this->model->id, $passwd);
|
||||
}
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $result !== false ? $this->success(__('Added successfully')) : $this->error(__('No rows were added'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑(密码可选更新)
|
||||
*/
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->post($pk) ?? $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
|
||||
$dataLimitAdminIds = $this->getDataLimitAdminIds();
|
||||
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
|
||||
return $this->error(__('You have no permission'));
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!$data) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
|
||||
if (!empty($data['password'])) {
|
||||
$this->model->resetPassword((int) $row->id, $data['password']);
|
||||
}
|
||||
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('edit');
|
||||
}
|
||||
$validate->check(array_merge($data, [$pk => $row[$pk]]));
|
||||
}
|
||||
}
|
||||
$result = $row->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $result !== false ? $this->success(__('Update successful')) : $this->error(__('No rows updated'));
|
||||
}
|
||||
|
||||
unset($row['password']);
|
||||
$row['password'] = '';
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程下拉数据(供 remoteSelect 使用)
|
||||
*/
|
||||
public function select(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
list($where, $alias, $limit, $order) = $this->queryBuilder();
|
||||
$res = $this->model
|
||||
->withoutField('password')
|
||||
->withJoin($this->withJoinTable, $this->withJoinType)
|
||||
->visible(['admin' => ['username']])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->paginate($limit);
|
||||
|
||||
return $this->success('', [
|
||||
'list' => $res->items(),
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
public function del(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
return $this->_del();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\model\mall;
|
||||
|
||||
use app\common\model\traits\TimestampInteger;
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* Player
|
||||
*/
|
||||
class Player extends Model
|
||||
{
|
||||
use TimestampInteger;
|
||||
|
||||
// 表名
|
||||
protected $name = 'mall_player';
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
public function resetPassword(int $id, string $newPassword): bool
|
||||
{
|
||||
return $this->where(['id' => $id])->update(['password' => hash_password($newPassword)]) !== false;
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,9 @@ class Common extends Api
|
||||
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') {
|
||||
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('', [
|
||||
'type' => $refreshToken['type'],
|
||||
|
||||
@@ -4,9 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller\v1;
|
||||
|
||||
use ba\Random;
|
||||
use Throwable;
|
||||
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\model\ChannelManage;
|
||||
use app\common\model\MallPlayxUserAsset;
|
||||
use app\admin\model\Admin;
|
||||
use Webman\Http\Request;
|
||||
use support\Response;
|
||||
@@ -26,6 +31,11 @@ class Auth extends Api
|
||||
*/
|
||||
protected int $timeTolerance = 300;
|
||||
|
||||
/**
|
||||
* 临时登录 token 有效期(秒)
|
||||
*/
|
||||
protected int $tempTokenExpire = 86400;
|
||||
|
||||
/**
|
||||
* 获取鉴权 Token(GET 请求)
|
||||
* 参数仅从 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'));
|
||||
}
|
||||
|
||||
$timestamp = (int) $time;
|
||||
$timestamp = intval($time);
|
||||
if ($timestamp <= 0) {
|
||||
return $this->error(__('Invalid timestamp'));
|
||||
}
|
||||
@@ -62,7 +72,7 @@ class Auth extends Api
|
||||
return $this->error(__('Agent not found'));
|
||||
}
|
||||
|
||||
$channelId = (int) ($admin->channel_id ?? 0);
|
||||
$channelId = intval($admin->channel_id ?? 0);
|
||||
if ($channelId <= 0) {
|
||||
return $this->error(__('Agent not found'));
|
||||
}
|
||||
@@ -81,7 +91,7 @@ class Auth extends Api
|
||||
return $this->error(__('Invalid signature'));
|
||||
}
|
||||
|
||||
$expire = (int) config('buildadmin.agent_auth.token_expire', 86400);
|
||||
$expire = intval(config('buildadmin.agent_auth.token_expire', 86400));
|
||||
$payload = [
|
||||
'agent_id' => $agentId,
|
||||
'channel_id' => $channel->id,
|
||||
@@ -93,4 +103,52 @@ class Auth extends Api
|
||||
'authtoken' => $authtoken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* H5 临时登录(GET/POST)
|
||||
* 参数:username
|
||||
* 写入或复用 mall_playx_user_asset;签发 muser 类型 token(user_id 为资产表主键)
|
||||
*/
|
||||
public function temLogin(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeApi($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$enabled = config('buildadmin.agent_auth.temp_login_enable', false);
|
||||
if (!$enabled) {
|
||||
return $this->error(__('Temp login is disabled'));
|
||||
}
|
||||
|
||||
$username = trim(strval($request->get('username', $request->post('username', ''))));
|
||||
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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller\v1;
|
||||
|
||||
use ba\Random;
|
||||
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\MallPlayxClaimLog;
|
||||
use app\common\model\MallPlayxDailyPush;
|
||||
@@ -21,28 +24,125 @@ use support\Response;
|
||||
class Playx extends Api
|
||||
{
|
||||
/**
|
||||
* 从请求中解析 PlayX 会话用户ID(优先 session_id,其次 user_id)
|
||||
* 从请求解析 mall_playx_user_asset.id(muser token、session、user_id 均指向资产表主键或 playx_user_id)
|
||||
*/
|
||||
private function resolveUserIdFromRequest(Request $request): ?string
|
||||
private function resolvePlayxAssetIdFromRequest(Request $request): ?int
|
||||
{
|
||||
$sessionId = strval($request->post('session_id', $request->get('session_id', '')));
|
||||
if ($sessionId !== '') {
|
||||
$session = MallPlayxSession::where('session_id', $sessionId)->find();
|
||||
if (!$session) {
|
||||
return null;
|
||||
}
|
||||
if ($session) {
|
||||
$expireTime = intval($session->expire_time ?? 0);
|
||||
if ($expireTime <= time()) {
|
||||
return null;
|
||||
if ($expireTime > time()) {
|
||||
$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', '')));
|
||||
if ($userId === '') {
|
||||
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'] ?? '';
|
||||
$date = $body['date'] ?? '';
|
||||
$userId = $body['user_id'] ?? '';
|
||||
$playxUserId = strval($body['user_id'] ?? '');
|
||||
$yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 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'));
|
||||
}
|
||||
|
||||
@@ -80,29 +180,29 @@ class Playx extends Api
|
||||
$ts = $request->header('X-Timestamp', '');
|
||||
$rid = $request->header('X-Request-Id', '');
|
||||
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));
|
||||
$expected = hash_hmac('sha256', $canonical, $secret);
|
||||
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) {
|
||||
return $this->success('', [
|
||||
'request_id' => $requestId,
|
||||
'accepted' => true,
|
||||
'deduped' => true,
|
||||
'message' => 'duplicate input',
|
||||
'message' => __('Duplicate input'),
|
||||
]);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
MallPlayxDailyPush::create([
|
||||
'user_id' => $userId,
|
||||
'user_id' => $playxUserId,
|
||||
'date' => $date,
|
||||
'username' => $body['username'] ?? '',
|
||||
'yesterday_win_loss_net' => $yesterdayWinLossNet,
|
||||
@@ -121,30 +221,23 @@ class Playx extends Api
|
||||
}
|
||||
$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;
|
||||
if ($asset) {
|
||||
if ($asset->today_limit_date !== $todayLimitDate) {
|
||||
$asset->today_claimed = 0;
|
||||
$asset->today_limit_date = $todayLimitDate;
|
||||
}
|
||||
$asset->locked_points += $newLocked;
|
||||
$asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
|
||||
$asset->today_limit = $todayLimit;
|
||||
$asset->username = $body['username'] ?? $asset->username;
|
||||
$asset->save();
|
||||
} else {
|
||||
MallPlayxUserAsset::create([
|
||||
'user_id' => $userId,
|
||||
'username' => $body['username'] ?? '',
|
||||
'locked_points' => $newLocked,
|
||||
'available_points' => 0,
|
||||
'today_limit' => $todayLimit,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => $todayLimitDate,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
$asset->playx_user_id = $playxUserId;
|
||||
$uname = trim(strval($body['username'] ?? ''));
|
||||
if ($uname !== '') {
|
||||
$asset->username = $uname;
|
||||
}
|
||||
$asset->save();
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -156,13 +249,13 @@ class Playx extends Api
|
||||
'request_id' => $requestId,
|
||||
'accepted' => true,
|
||||
'deduped' => false,
|
||||
'message' => 'ok',
|
||||
'message' => __('Ok'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 验证 - 接收前端 token,调用 PlayX 验证(占位,待 PlayX 提供 API)
|
||||
* POST /api/v1/playx/verify-token
|
||||
* Token 验证 - POST /api/v1/playx/verify-token
|
||||
* 配置 playx.verify_token_local_only=true 时仅本地校验 token(不请求 PlayX)。
|
||||
*/
|
||||
public function verifyToken(Request $request): Response
|
||||
{
|
||||
@@ -171,15 +264,19 @@ class Playx extends Api
|
||||
return $response;
|
||||
}
|
||||
|
||||
$token = $request->post('token', $request->post('session', ''));
|
||||
$token = strval($request->post('token', $request->post('session', $request->get('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', '');
|
||||
$verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token');
|
||||
if ($baseUrl === '') {
|
||||
return $this->error('PlayX API not configured');
|
||||
return $this->error(__('PlayX API not configured'));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -196,7 +293,10 @@ class Playx extends Api
|
||||
$code = $res->getStatusCode();
|
||||
$data = json_decode(strval($res->getBody()), true);
|
||||
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']);
|
||||
@@ -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
|
||||
@@ -242,12 +389,12 @@ class Playx extends Api
|
||||
return $response;
|
||||
}
|
||||
|
||||
$userId = $this->resolveUserIdFromRequest($request);
|
||||
if ($userId === null) {
|
||||
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
||||
$asset = $this->getAssetById($assetId);
|
||||
if (!$asset) {
|
||||
return $this->success('', [
|
||||
'locked_points' => 0,
|
||||
@@ -282,22 +429,22 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
$claimRequestId = strval($request->post('claim_request_id', ''));
|
||||
$userId = $this->resolveUserIdFromRequest($request);
|
||||
if ($claimRequestId === '' || $userId === null) {
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($claimRequestId === '' || $assetId === null) {
|
||||
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();
|
||||
if ($exists) {
|
||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
||||
return $this->success('', $this->formatAsset($asset));
|
||||
}
|
||||
|
||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
||||
if (!$asset) {
|
||||
return $this->error(__('User asset not found'));
|
||||
}
|
||||
|
||||
$todayLimitDate = date('Y-m-d');
|
||||
if ($asset->today_limit_date !== $todayLimitDate) {
|
||||
$asset->today_claimed = 0;
|
||||
@@ -315,7 +462,7 @@ class Playx extends Api
|
||||
try {
|
||||
MallPlayxClaimLog::create([
|
||||
'claim_request_id' => $claimRequestId,
|
||||
'user_id' => $userId,
|
||||
'user_id' => $playxUserId,
|
||||
'claimed_amount' => $canClaim,
|
||||
'create_time' => time(),
|
||||
]);
|
||||
@@ -395,12 +542,16 @@ class Playx extends Api
|
||||
return $response;
|
||||
}
|
||||
|
||||
$userId = $this->resolveUserIdFromRequest($request);
|
||||
if ($userId === null) {
|
||||
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($assetId === null) {
|
||||
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'])
|
||||
->order('id', 'desc')
|
||||
->limit(100)
|
||||
@@ -438,8 +589,8 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
$itemId = intval($request->post('item_id', 0));
|
||||
$userId = $this->resolveUserIdFromRequest($request);
|
||||
if ($itemId <= 0 || $userId === null) {
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($itemId <= 0 || $assetId === null) {
|
||||
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'));
|
||||
}
|
||||
|
||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
||||
if (!$asset || $asset->available_points < $item->score) {
|
||||
$asset = $this->getAssetById($assetId);
|
||||
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
|
||||
return $this->error(__('Insufficient points'));
|
||||
}
|
||||
$playxUserId = strval($asset->playx_user_id);
|
||||
|
||||
$multiplier = intval($item->multiplier ?? 0);
|
||||
if ($multiplier <= 0) {
|
||||
@@ -466,7 +618,7 @@ class Playx extends Api
|
||||
|
||||
$orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
|
||||
$order = MallPlayxOrder::create([
|
||||
'user_id' => $userId,
|
||||
'user_id' => $playxUserId,
|
||||
'type' => MallPlayxOrder::TYPE_BONUS,
|
||||
'status' => MallPlayxOrder::STATUS_PENDING,
|
||||
'mall_item_id' => $item->id,
|
||||
@@ -487,7 +639,7 @@ class Playx extends Api
|
||||
|
||||
$baseUrl = config('playx.api.base_url', '');
|
||||
if ($baseUrl !== '') {
|
||||
$this->callPlayxBonusGrant($order, $item, $userId);
|
||||
$this->callPlayxBonusGrant($order, $item, $playxUserId);
|
||||
}
|
||||
|
||||
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));
|
||||
$userId = $this->resolveUserIdFromRequest($request);
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
$receiverName = $request->post('receiver_name', '');
|
||||
$receiverPhone = $request->post('receiver_phone', '');
|
||||
$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'));
|
||||
}
|
||||
|
||||
@@ -520,10 +672,11 @@ class Playx extends Api
|
||||
return $this->error(__('Out of stock'));
|
||||
}
|
||||
|
||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
||||
if (!$asset || $asset->available_points < $item->score) {
|
||||
$asset = $this->getAssetById($assetId);
|
||||
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
|
||||
return $this->error(__('Insufficient points'));
|
||||
}
|
||||
$playxUserId = strval($asset->playx_user_id);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
@@ -531,7 +684,7 @@ class Playx extends Api
|
||||
$asset->save();
|
||||
|
||||
MallPlayxOrder::create([
|
||||
'user_id' => $userId,
|
||||
'user_id' => $playxUserId,
|
||||
'type' => MallPlayxOrder::TYPE_PHYSICAL,
|
||||
'status' => MallPlayxOrder::STATUS_PENDING,
|
||||
'mall_item_id' => $item->id,
|
||||
@@ -565,8 +718,8 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
$itemId = intval($request->post('item_id', 0));
|
||||
$userId = $this->resolveUserIdFromRequest($request);
|
||||
if ($itemId <= 0 || $userId === null) {
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($itemId <= 0 || $assetId === null) {
|
||||
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'));
|
||||
}
|
||||
|
||||
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
|
||||
if (!$asset || $asset->available_points < $item->score) {
|
||||
$asset = $this->getAssetById($assetId);
|
||||
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
|
||||
return $this->error(__('Insufficient points'));
|
||||
}
|
||||
$playxUserId = strval($asset->playx_user_id);
|
||||
|
||||
$multiplier = intval($item->multiplier ?? 0);
|
||||
if ($multiplier <= 0) {
|
||||
@@ -593,7 +747,7 @@ class Playx extends Api
|
||||
|
||||
$orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
|
||||
$order = MallPlayxOrder::create([
|
||||
'user_id' => $userId,
|
||||
'user_id' => $playxUserId,
|
||||
'type' => MallPlayxOrder::TYPE_WITHDRAW,
|
||||
'status' => MallPlayxOrder::STATUS_PENDING,
|
||||
'mall_item_id' => $item->id,
|
||||
@@ -614,7 +768,7 @@ class Playx extends Api
|
||||
|
||||
$baseUrl = config('playx.api.base_url', '');
|
||||
if ($baseUrl !== '') {
|
||||
$this->callPlayxBalanceCredit($order, $userId);
|
||||
$this->callPlayxBalanceCredit($order, $playxUserId);
|
||||
}
|
||||
|
||||
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [
|
||||
|
||||
@@ -19,10 +19,32 @@ return [
|
||||
'Invalid agent or secret' => 'Invalid agent or secret',
|
||||
'Invalid signature' => 'Invalid signature',
|
||||
'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
|
||||
'Data updated successfully~' => 'Data updated successfully~',
|
||||
'Password has been changed~' => 'Password has been changed~',
|
||||
'Password has been changed, please login again~' => 'Password has been changed, please login again~',
|
||||
'already exists' => 'already exists',
|
||||
'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',
|
||||
];
|
||||
@@ -49,6 +49,9 @@ return [
|
||||
'Invalid agent or secret' => '代理或密钥无效',
|
||||
'Invalid signature' => '签名无效',
|
||||
'Agent not found' => '代理不存在',
|
||||
'Temp login is disabled' => '临时登录已关闭',
|
||||
'Failed to create temp account' => '无法生成唯一手机号,请稍后重试',
|
||||
'Parameter username can not be empty' => '参数 username 不能为空',
|
||||
'Token expiration' => '登录态过期,请重新登录!',
|
||||
'Captcha error' => '验证码错误!',
|
||||
// 会员中心 account
|
||||
@@ -57,4 +60,23 @@ return [
|
||||
'Password has been changed, please login again~' => '密码已修改,请重新登录~',
|
||||
'already exists' => '已存在',
|
||||
'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 分钟',
|
||||
];
|
||||
@@ -19,6 +19,11 @@ class Auth extends \ba\Auth
|
||||
public const LOGGED_IN = 'logged in';
|
||||
public const TOKEN_TYPE = 'user';
|
||||
|
||||
/**
|
||||
* 积分商城用户(mall_playx_user_asset 主键)Token 类型,与会员 user 表区分
|
||||
*/
|
||||
public const TOKEN_TYPE_MALL_USER = 'muser';
|
||||
|
||||
protected bool $loginEd = false;
|
||||
protected string $error = '';
|
||||
protected ?User $model = null;
|
||||
|
||||
@@ -11,12 +11,15 @@ use Exception;
|
||||
*/
|
||||
class TokenExpirationException extends Exception
|
||||
{
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct(
|
||||
protected string $message = '',
|
||||
protected int $code = 409,
|
||||
protected array $data = [],
|
||||
string $message = '',
|
||||
int $code = 409,
|
||||
array $data = [],
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
$this->data = $data;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class AllowCrossDomain implements MiddlewareInterface
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => '1800',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, batoken, ba-user-token, think-lang, lang',
|
||||
];
|
||||
$origin = $request->header('origin');
|
||||
if (is_array($origin)) {
|
||||
|
||||
@@ -11,6 +11,9 @@ use Webman\Http\Response;
|
||||
/**
|
||||
* 加载控制器语言包中间件(Webman 迁移版,等价 ThinkPHP LoadLangPack)
|
||||
* 根据当前路由加载对应控制器的语言包到 Translator
|
||||
*
|
||||
* 对外 api/:优先请求头 lang(zh / zh-cn → 中文包 zh-cn,en → 英文包),未传则 think-lang,再默认 zh-cn(不根据浏览器 Accept-Language)
|
||||
* admin/:think-lang → Accept-Language → 配置默认
|
||||
*/
|
||||
class LoadLangPack implements MiddlewareInterface
|
||||
{
|
||||
@@ -25,14 +28,49 @@ class LoadLangPack implements MiddlewareInterface
|
||||
|
||||
protected function loadLang(Request $request): void
|
||||
{
|
||||
// 优先从请求头 think-lang 获取前端选择的语言(与前端 axios 发送的 header 对应)
|
||||
// 安装页等未发送 think-lang 时,回退到 Accept-Language 或配置默认值
|
||||
$headerLang = $request->header('think-lang');
|
||||
$path = trim($request->path(), '/');
|
||||
$isApi = str_starts_with($path, 'api/');
|
||||
$isAdmin = str_starts_with($path, 'admin/');
|
||||
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
|
||||
if ($headerLang && in_array(str_replace('_', '-', strtolower($headerLang)), $allowLangList)) {
|
||||
$langSet = str_replace('_', '-', strtolower($headerLang));
|
||||
} else {
|
||||
|
||||
$langSet = null;
|
||||
|
||||
// 对外 API(PlayX、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', '');
|
||||
if (is_array($acceptLang)) {
|
||||
$acceptLang = $acceptLang[0] ?? '';
|
||||
}
|
||||
$acceptLang = is_string($acceptLang) ? $acceptLang : '';
|
||||
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
|
||||
$langSet = 'zh-cn';
|
||||
} elseif (preg_match('/^en/i', $acceptLang)) {
|
||||
@@ -40,7 +78,11 @@ class LoadLangPack implements MiddlewareInterface
|
||||
} else {
|
||||
$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() 使用正确的语言
|
||||
@@ -48,7 +90,6 @@ class LoadLangPack implements MiddlewareInterface
|
||||
locale($langSet);
|
||||
}
|
||||
|
||||
$path = trim($request->path(), '/');
|
||||
$parts = explode('/', $path);
|
||||
$app = $parts[0] ?? 'api';
|
||||
|
||||
@@ -81,4 +122,26 @@ class LoadLangPack implements MiddlewareInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 lang 请求头取值映射为语言包标识(zh / zh-cn → zh-cn,en → en)
|
||||
*/
|
||||
private function normalizeLangHeader(string $raw, array $allowLangList): ?string
|
||||
{
|
||||
$s = str_replace('_', '-', strtolower(trim($raw)));
|
||||
if ($s === '') {
|
||||
return null;
|
||||
}
|
||||
if (in_array($s, $allowLangList, true)) {
|
||||
return $s;
|
||||
}
|
||||
if (str_starts_with($s, 'en')) {
|
||||
return in_array('en', $allowLangList, true) ? 'en' : null;
|
||||
}
|
||||
if ($s === 'zh' || str_starts_with($s, 'zh-')) {
|
||||
return in_array('zh-cn', $allowLangList, true) ? 'zh-cn' : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ class MallAddress extends Model
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ class MallPintsOrder extends Model
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use ba\Random;
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* PlayX 用户资产
|
||||
* PlayX 用户资产(积分商城用户主表,含登录账号字段)
|
||||
*/
|
||||
class MallPlayxUserAsset extends Model
|
||||
{
|
||||
@@ -22,5 +23,66 @@ class MallPlayxUserAsset extends Model
|
||||
'available_points' => 'integer',
|
||||
'today_limit' => 'integer',
|
||||
'today_claimed' => 'integer',
|
||||
'admin_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* H5 临时登录:按用户名查找或创建资产行,playx_user_id 使用 mall_{id}
|
||||
*/
|
||||
public static function ensureForUsername(string $username): self
|
||||
{
|
||||
$username = trim($username);
|
||||
$existing = self::where('username', $username)->find();
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$phone = 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ class MallRedemptionOrder extends Model
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
@@ -267,7 +267,7 @@ class PlayxJobs
|
||||
if ($order->points_cost <= 0) {
|
||||
return;
|
||||
}
|
||||
$asset = MallPlayxUserAsset::where('user_id', $order->user_id)->find();
|
||||
$asset = MallPlayxUserAsset::where('playx_user_id', $order->user_id)->find();
|
||||
if (!$asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
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则无限
|
||||
'user_login_retry' => 10,
|
||||
// 管理员登录失败可重试次数,false则无限
|
||||
@@ -89,8 +89,12 @@ return [
|
||||
],
|
||||
// JWT 签名密钥(留空则使用 token.key)
|
||||
'jwt_secret' => '',
|
||||
// 是否启用 H5 临时登录接口 /api/v1/temLogin
|
||||
'temp_login_enable' => true,
|
||||
// Token 有效期(秒),默认 24 小时
|
||||
'token_expire' => 86400,
|
||||
// 临时登录 token 有效期(秒),默认 1 天
|
||||
'temp_login_expire' => 86400,
|
||||
],
|
||||
// 版本号
|
||||
'version' => 'v2.3.6',
|
||||
|
||||
@@ -14,6 +14,11 @@ return [
|
||||
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')),
|
||||
// token 会话缓存过期时间(秒)
|
||||
'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 时使用)
|
||||
'api' => [
|
||||
'base_url' => strval(env('PLAYX_API_BASE_URL', '')),
|
||||
|
||||
@@ -110,6 +110,7 @@ Route::post('/api/ems/send', [\app\api\controller\Ems::class, 'send']);
|
||||
|
||||
// api/v1 鉴权
|
||||
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 积分商城
|
||||
Route::post('/api/v1/playx/daily-push', [\app\api\controller\v1\Playx::class, 'dailyPush']);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
## 0. 交付说明(给 PlayX)
|
||||
|
||||
- **交付物**:本文件(接口清单 + 业务流程 + 联调验收清单)。
|
||||
- **建议联调顺序**:Token 验证 → 每日推送 → 领取 → 红利发放 → 提现入账 → 实物后台处理。
|
||||
- **建议联调顺序**:Token 验证(远程 PlayX 或本地 `verify_token_local_only`)→ 每日推送 → 领取 → 红利发放 → 提现入账 → 实物后台处理。
|
||||
- **约定**:接口 URL、字段最终表、签名细节以 PlayX 提供的最终口径为准;本文档负责把流程、幂等、重试与最小字段集合先对齐。
|
||||
|
||||
## 1. 文档目的与范围
|
||||
@@ -29,6 +29,8 @@ flowchart LR
|
||||
MallBackend -->|"BonusGrantAPI/BalanceCreditAPI"| PlayXBackend
|
||||
```
|
||||
|
||||
> 当 **`playx.verify_token_local_only=true`** 时,「Token 验证」一步在商城内完成,**不经过** `PlayXBackend` 的 Token Verification API;详见 **§4.1**。
|
||||
|
||||
## 3. 关键业务对象与状态机
|
||||
|
||||
### 3.1 资产口径(最小集合)
|
||||
@@ -55,17 +57,43 @@ flowchart LR
|
||||
|
||||
### 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)。
|
||||
2. PlayX 前端通过 postMessage 发送 `token/session` 给商城前端。
|
||||
3. 商城后端调用 PlayX 的 **Token Verification API** 校验 token。
|
||||
4. PlayX 返回 `user_id`、`username`(以及会话有效期等)。
|
||||
5. 商城建立会话,返回会员资产与商品列表数据。
|
||||
2. PlayX 前端通过 postMessage 将 **PlayX 下发的 token**(及必要上下文)传给商城 H5。
|
||||
3. 商城 H5 调用商城后端 **`POST /api/v1/playx/verify-token`**,由商城向 PlayX 的 **Token Verification API**(`playx.api.base_url` + `playx.api.token_verify_url`)发起校验。
|
||||
4. **前提**:配置 **`playx.verify_token_local_only = false`**,且 **`playx.api.base_url`** 已配置为可访问的 PlayX 基地址。
|
||||
5. PlayX 返回 **`user_id`、`username`**(及可选会话过期时间等)。
|
||||
6. 商城写入 **`mall_playx_session`**(`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token,见模式 B)** 调用资产/领取等接口。
|
||||
|
||||
幂等、安全与会话续期:
|
||||
幂等与安全:
|
||||
|
||||
- 前端不信任 `user_id` 直传;只接收 token/session。
|
||||
- Token 验证接口需要签名/鉴权(见第 7 节)。
|
||||
- **会话续期**:由于玩家访问积分商城可能停留时间较长,当商城调用任意 API 遇到 Token 校验过期(如 HTTP 401)时,商城前端会通过 postMessage 向 PlayX 父级页面请求派发新的 Token 以实现静默续期,请 PlayX 配合予以支持。
|
||||
- H5 **不要**把 PlayX 的 `user_id` 当作唯一可信凭据直传下单;**以 token 换 session** 或由商城签发 token 的流程为准。
|
||||
- PlayX 侧 Token Verification API 的鉴权/签名(若有)按双方约定(可参考《PlayX-接口文档》§2.1)。
|
||||
|
||||
#### 4.1.3 模式 B:本地 / 无 PlayX 环境(商城自校验,不请求 PlayX)
|
||||
|
||||
用于开发、联调前自测、或 PlayX 接口未就绪时:
|
||||
|
||||
1. 配置 **`playx.verify_token_local_only = true`**(环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**,默认可为开启,以项目 `config/playx.php` 为准)。
|
||||
2. 此时 **`/api/v1/playx/verify-token` 不会访问 PlayX**,仅在商城内校验 **商城临时 token**(token 表类型 **`muser`**,由下方 `temLogin` 签发)。
|
||||
3. 调用 **`GET/POST /api/v1/temLogin?username=...`**(需 **`buildadmin.agent_auth.temp_login_enable = true`**):不存在则创建 **`mall_user`**,并保证存在 **`mall_playx_user_asset`**(含 `playx_user_id`,默认 **`mall_{id}`**),返回 **`userInfo.token`**、**`playx_user_id`**、**`expires_in`** 等。
|
||||
4. 再用该 token 调用 **`verify-token`** 可得到 **`session_id`**,与模式 A 一样供后续接口使用;或直接带 **`token` / `ba-token`** 调资产等接口(见《PlayX-接口文档》§3.1)。
|
||||
|
||||
#### 4.1.4 会话续期与前端约定
|
||||
|
||||
- **会话续期**:玩家停留时间较长时,若商城 API 返回 token/session 失效(如 401),H5 可通过 postMessage 请 PlayX 父页面 **重新派发 PlayX token**(模式 A);模式 B 下可重新 **`temLogin`** 或走 **`/api/common/refreshToken`**(`muser-refresh`)换取新 access token。
|
||||
- 具体错误码与 Header(如 `ba-token`)以前端与《PlayX-接口文档》为准。
|
||||
|
||||
### 4.2 每日 T+1 入池(PlayX → 商城)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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 调用商城)
|
||||
@@ -31,8 +31,8 @@
|
||||
|------|------|------|------|
|
||||
| `request_id` | string | 是 | 外部推送请求号(原样返回) |
|
||||
| `date` | string(YYYY-MM-DD) | 是 | 业务日期(入库到 `mall_playx_daily_push.date`) |
|
||||
| `user_id` | string | 是 | PlayX 用户 ID(用于幂等) |
|
||||
| `username` | string | 否 | 展示冗余 |
|
||||
| `user_id` | string | 是 | PlayX 用户 ID(用于幂等;入库 `mall_playx_daily_push.user_id` 等;服务端会映射/创建 `mall_user` 与 `mall_playx_user_asset`) |
|
||||
| `username` | string | 否 | 展示冗余(同步到商城用户侧逻辑时使用) |
|
||||
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢(仅当 `< 0` 时计算新增保障金) |
|
||||
| `yesterday_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)
|
||||
|
||||
> 下面这些接口由 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 API(PlayX 侧实现,远程验证时使用)
|
||||
* 方法:`POST`
|
||||
* URL:`${playx.api.base_url}${playx.api.token_verify_url}`
|
||||
* 默认:`/api/v1/auth/verify-token`
|
||||
@@ -257,48 +258,134 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
|
||||
## 3. 积分商城 -> H5(服务端提供给 H5 的接口)
|
||||
|
||||
说明:鉴权与用户解析规则由 `resolveUserIdFromRequest()` 决定。
|
||||
* 优先使用 `session_id`(在 `mall_playx_session` 查到且未过期)
|
||||
* 其次使用 `user_id`
|
||||
### 3.0 数据模型说明(与代码一致)
|
||||
|
||||
公共鉴权字段:
|
||||
* `session_id`:字符串
|
||||
* `user_id`:字符串
|
||||
* **商城用户**:表 `mall_user`(主键 `id`)。
|
||||
* **PlayX 资产扩展**:表 `mall_playx_user_asset`,与 `mall_user` **一对一**(`mall_user_id` 唯一,`playx_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)。
|
||||
|
||||
---
|
||||
|
||||
### 3.1 Token 验证
|
||||
* 方法:`POST`
|
||||
### 3.2 临时登录(获取商城 token)
|
||||
|
||||
* 方法:`GET`(推荐)或 `POST`
|
||||
* 路径:`/api/v1/temLogin`
|
||||
* 开关:`config/buildadmin.php` → `agent_auth.temp_login_enable` 为 `true`;有效期 `agent_auth.temp_login_expire`(秒)。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `username` | string | 是 | 商城用户名(唯一);不存在则自动创建 `mall_user` |
|
||||
|
||||
#### 行为说明
|
||||
|
||||
* 若 `mall_user` 不存在:创建用户(随机占位手机号、随机密码等,与后台「商城用户」一致)。
|
||||
* **无论是否新用户**:保证存在 **`mall_playx_user_asset`** 一条记录(`MallPlayxUserAsset::ensureForMallUser`),`playx_user_id` 默认 **`mall_{mall_user.id}`**(与 PlayX 真实 ID 冲突概率低)。
|
||||
* 签发 **商城 token**(类型 **`muser`**,非会员表 `user`),并签发 `muser-refresh` 刷新令牌。
|
||||
|
||||
#### 返回(成功 data.userInfo)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | int | `mall_user.id` |
|
||||
| `username` | string | 用户名 |
|
||||
| `nickname` | string | 同 `username` |
|
||||
| `playx_user_id` | string | 资产表中的 `playx_user_id`(如 `mall_12`) |
|
||||
| `token` | string | 访问 H5 接口时携带 |
|
||||
| `refresh_token` | string | 调用 `/api/common/refreshToken` 时使用(类型 `muser-refresh`) |
|
||||
| `expires_in` | int | token 有效秒数 |
|
||||
|
||||
#### 示例
|
||||
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_h5'
|
||||
```
|
||||
|
||||
用户名含 `+` 等号时需 URL 编码(如 `%2B60123456789`)。
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Token 验证(换 session)
|
||||
|
||||
* 方法:`POST`(推荐 `GET` 传 `token` 亦可)
|
||||
* 路径:`/api/v1/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)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `session_id` | string | 写入 `mall_playx_session` |
|
||||
| `user_id` | string | PlayX 用户 ID |
|
||||
| `user_id` | string | PlayX 用户 ID(即 `playx_user_id`,会话内与订单/推送一致) |
|
||||
| `username` | string | 用户名 |
|
||||
| `token_expire_at` | string | ISO 字符串(服务端 `date('c', expireAt)`) |
|
||||
|
||||
失败:
|
||||
* token 为空:HTTP 401,msg=`INVALID_TOKEN`
|
||||
* PlayX 未配置:msg=`PlayX API not configured`
|
||||
|
||||
#### 示例
|
||||
请求:
|
||||
* token 为空:HTTP 401,msg=`INVALID_TOKEN`
|
||||
* 远程模式且 PlayX 未配置:`msg=PlayX API not configured`
|
||||
|
||||
#### 示例(本地验证)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
-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
|
||||
{
|
||||
"code": 1,
|
||||
@@ -314,13 +401,17 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
|
||||
---
|
||||
|
||||
### 3.2 用户资产
|
||||
### 3.4 用户资产(Assets)
|
||||
* 方法:`GET`
|
||||
* 路径:`/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)
|
||||
若未找到资产:返回 0。
|
||||
@@ -334,7 +425,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
|
||||
#### 示例
|
||||
```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`
|
||||
* 路径:`/api/v1/playx/claim`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
|
||||
* `claim_request_id`:幂等键(string,唯一)
|
||||
鉴权:
|
||||
* `session_id` 或 `user_id`
|
||||
|
||||
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||
|
||||
#### 返回(成功 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' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--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`
|
||||
* 路径:`/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`
|
||||
* 路径:`/api/v1/playx/bonus/redeem`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
* `item_id`:商品 ID(要求 `mall_item.type=BONUS` 且 `status=1`)
|
||||
鉴权:
|
||||
* `session_id` 或 `user_id`
|
||||
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||
|
||||
#### 返回(成功)
|
||||
* `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`
|
||||
* 路径:`/api/v1/playx/physical/redeem`
|
||||
|
||||
@@ -504,8 +595,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
* `receiver_name`:收货人
|
||||
* `receiver_phone`:收货电话
|
||||
* `receiver_address`:收货地址
|
||||
鉴权:
|
||||
* `session_id` 或 `user_id`
|
||||
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||
|
||||
#### 返回(成功)
|
||||
* `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`
|
||||
* 路径:`/api/v1/playx/withdraw/apply`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
* `item_id`:商品 ID(要求 `mall_item.type=WITHDRAW` 且 `status=1`)
|
||||
鉴权:
|
||||
* `session_id` 或 `user_id`
|
||||
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||
|
||||
#### 返回(成功)
|
||||
* `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`
|
||||
* 路径:`/api/v1/playx/orders`
|
||||
|
||||
#### 请求参数
|
||||
* `session_id` 或 `user_id`
|
||||
#### 请求参数(鉴权)
|
||||
|
||||
同 **3.1**(`session_id` / `token` / `user_id`)。
|
||||
|
||||
#### 返回(成功 data)
|
||||
|
||||
* `list`:订单列表(最多 100 条),并包含关联的 `mallItem`(关系对象)
|
||||
* 列表项中的 `user_id` 为 **PlayX 侧 `playx_user_id`**(字符串),与 `mall_playx_order.user_id` 一致
|
||||
|
||||
#### 示例
|
||||
请求:
|
||||
```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`。
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@ VITE_BASE_PATH = '/'
|
||||
VITE_OUT_DIR = 'dist'
|
||||
|
||||
# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
|
||||
VITE_AXIOS_BASE_URL = 'getCurrentDomain'
|
||||
VITE_AXIOS_BASE_URL = 'https://playx-api.cjdhr.top'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
mall_user_id: 'mall_user_id',
|
||||
malluser__username: 'username',
|
||||
playx_user_asset_id: 'PlayX user asset',
|
||||
playxuserasset__username: 'username',
|
||||
phone: 'phone',
|
||||
region: 'region',
|
||||
detail_address: 'detail_address',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
order: 'order',
|
||||
mall_user_id: 'mall_user_id',
|
||||
malluser__username: 'username',
|
||||
playx_user_asset_id: 'PlayX user asset',
|
||||
playxuserasset__username: 'username',
|
||||
type: 'type',
|
||||
'type 1': 'type 1',
|
||||
'type 2': 'type 2',
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
score: 'score',
|
||||
quickSearchFields: 'id',
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
user_id: 'user_id',
|
||||
username: 'username',
|
||||
phone: 'phone',
|
||||
playx_user_id: 'playx_user_id',
|
||||
locked_points: 'locked_points',
|
||||
available_points: 'available_points',
|
||||
today_limit: 'today_limit',
|
||||
@@ -9,5 +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_user_id, username, phone',
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
order: 'order',
|
||||
mall_user_id: 'mall_user_id',
|
||||
malluser__username: 'username',
|
||||
playx_user_asset_id: 'PlayX user asset',
|
||||
playxuserasset__username: 'username',
|
||||
status: 'status',
|
||||
'status 0': 'status 0',
|
||||
'status 1': 'status 1',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
phone: 'phone',
|
||||
password: 'password',
|
||||
score: 'score',
|
||||
daily_claim: 'daily_claim',
|
||||
daily_claim_use: 'daily_claim_use',
|
||||
available_for_withdrawal: 'available_for_withdrawal',
|
||||
admin_id: 'admin_id',
|
||||
admin__username: 'username',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'id',
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
mall_user_id: '用户',
|
||||
malluser__username: '用户名',
|
||||
playx_user_asset_id: '用户资产',
|
||||
playxuserasset__username: '用户名',
|
||||
phone: '电话',
|
||||
region: '地区',
|
||||
detail_address: '详细地址',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
order: '订单编号',
|
||||
mall_user_id: '用户',
|
||||
malluser__username: '用户名',
|
||||
playx_user_asset_id: '用户资产',
|
||||
playxuserasset__username: '用户名',
|
||||
type: '类型',
|
||||
'type 1': '奖励',
|
||||
'type 2': '充值',
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
score: '积分',
|
||||
quickSearchFields: 'ID',
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
user_id: '用户ID',
|
||||
username: '用户名',
|
||||
phone: '手机号',
|
||||
playx_user_id: 'PlayX用户ID',
|
||||
locked_points: '待领取积分',
|
||||
available_points: '可用积分',
|
||||
today_limit: '今日可领取上限',
|
||||
@@ -9,6 +10,5 @@ export default {
|
||||
today_limit_date: '今日上限日期',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID',
|
||||
'quick Search Fields': 'ID、PlayX用户ID、用户名、手机号',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
order: '订单号',
|
||||
mall_user_id: '用户',
|
||||
malluser__username: '用户名',
|
||||
playx_user_asset_id: '用户资产',
|
||||
playxuserasset__username: '用户名',
|
||||
status: '状态',
|
||||
'status 0': '待发放',
|
||||
'status 1': '已发放',
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
username: '用户名',
|
||||
phone: '手机号',
|
||||
password: '密码',
|
||||
score: '积分',
|
||||
daily_claim: '每日限额',
|
||||
daily_claim_use: '每日限额(已使用)',
|
||||
available_for_withdrawal: '可提现金额',
|
||||
admin_id: '归属管理员id',
|
||||
admin__username: '归属管理员',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
@@ -48,8 +48,8 @@ const baTable = new baTableClass(
|
||||
{ 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.malluser__username'),
|
||||
prop: 'mallUser.username',
|
||||
label: t('mall.address.playxuserasset__username'),
|
||||
prop: 'playxUserAsset.username',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
:rules="rules"
|
||||
>
|
||||
<FormItem
|
||||
:label="t('mall.address.mall_user_id')"
|
||||
:label="t('mall.address.playx_user_asset_id')"
|
||||
type="remoteSelect"
|
||||
v-model="baTable.form.items!.mall_user_id"
|
||||
prop="mall_user_id"
|
||||
:input-attr="{ pk: 'mall_user.id', field: 'username', remoteUrl: '/admin/mall.User/index' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.address.mall_user_id') })"
|
||||
v-model="baTable.form.items!.playx_user_asset_id"
|
||||
prop="playx_user_asset_id"
|
||||
:input-attr="{ pk: 'mall_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/select' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.address.playx_user_asset_id') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.phone')"
|
||||
|
||||
@@ -56,8 +56,8 @@ const baTable = new baTableClass(
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.pintsOrder.malluser__username'),
|
||||
prop: 'mallUser.username',
|
||||
label: t('mall.pintsOrder.playxuserasset__username'),
|
||||
prop: 'playxUserAsset.username',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
:placeholder="t('Please input field', { field: t('mall.pintsOrder.order') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.pintsOrder.mall_user_id')"
|
||||
:label="t('mall.pintsOrder.playx_user_asset_id')"
|
||||
type="remoteSelect"
|
||||
v-model="baTable.form.items!.mall_user_id"
|
||||
prop="mall_user_id"
|
||||
:input-attr="{ pk: 'mall_user.id', field: 'username', remoteUrl: '/admin/mall.User/index' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.pintsOrder.mall_user_id') })"
|
||||
v-model="baTable.form.items!.playx_user_asset_id"
|
||||
prop="playx_user_asset_id"
|
||||
:input-attr="{ pk: 'mall_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/select' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.pintsOrder.playx_user_asset_id') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.pintsOrder.type')"
|
||||
@@ -93,7 +93,7 @@ const { t } = useI18n()
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
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') })],
|
||||
score: [buildValidatorData({ name: 'number', title: t('mall.pintsOrder.score') })],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('mall.pintsOrder.create_time') })],
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<!-- 表格顶部菜单 -->
|
||||
<!-- 自定义按钮请使用插槽,甚至公共搜索也可以使用具名插槽渲染,参见文档 -->
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.player.quickSearchFields') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<!-- 表单 -->
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'mall/player',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/mall.Player/'),
|
||||
{
|
||||
pk: 'id',
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('mall.player.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('mall.player.username'),
|
||||
prop: 'username',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.player.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('mall.player.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('mall.player.score'), prop: 'score', align: 'center', sortable: false, operator: 'RANGE' },
|
||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
{
|
||||
defaultItems: {},
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,109 +0,0 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<FormItem
|
||||
:label="t('mall.player.username')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.username"
|
||||
prop="username"
|
||||
:placeholder="t('Please input field', { field: t('mall.player.username') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.player.password')"
|
||||
type="password"
|
||||
v-model="baTable.form.items!.password"
|
||||
prop="password"
|
||||
:placeholder="t('Please input field', { field: t('mall.player.password') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.player.score')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.score"
|
||||
prop="score"
|
||||
:input-attr="{ step: 1 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.player.score') })"
|
||||
/>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('mall.player.username') })],
|
||||
password: [
|
||||
{
|
||||
validator: (_rule: unknown, val: string, callback: (error?: Error) => void) => {
|
||||
if (baTable.form.operate === 'Add') {
|
||||
if (!val) {
|
||||
return callback(new Error(t('Please input field', { field: t('mall.player.password') })))
|
||||
}
|
||||
} else {
|
||||
if (!val) {
|
||||
return callback()
|
||||
}
|
||||
}
|
||||
if (!regularPassword(val)) {
|
||||
return callback(new Error(t('validate.Please enter the correct password')))
|
||||
}
|
||||
return callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
score: [buildValidatorData({ name: 'number', title: t('mall.player.score') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -33,21 +33,73 @@ const baTable = new baTableClass(
|
||||
column: [
|
||||
{ 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.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.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_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.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.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.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],
|
||||
},
|
||||
{
|
||||
defaultItems: {},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
@@ -63,4 +115,3 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ const baTable = new baTableClass(
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.redemptionOrder.malluser__username'),
|
||||
prop: 'mallUser.username',
|
||||
label: t('mall.redemptionOrder.playxuserasset__username'),
|
||||
prop: 'playxUserAsset.username',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
:placeholder="t('Please input field', { field: t('mall.redemptionOrder.order') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.redemptionOrder.mall_user_id')"
|
||||
:label="t('mall.redemptionOrder.playx_user_asset_id')"
|
||||
type="remoteSelect"
|
||||
v-model="baTable.form.items!.mall_user_id"
|
||||
prop="mall_user_id"
|
||||
:input-attr="{ pk: 'mall_user.id', field: 'username', remoteUrl: '/admin/mall.User/index' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.redemptionOrder.mall_user_id') })"
|
||||
v-model="baTable.form.items!.playx_user_asset_id"
|
||||
prop="playx_user_asset_id"
|
||||
:input-attr="{ pk: 'mall_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/select' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.redemptionOrder.playx_user_asset_id') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.redemptionOrder.status')"
|
||||
|
||||
@@ -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>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<FormItem
|
||||
:label="t('mall.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>
|
||||
Reference in New Issue
Block a user