From 4a42899bfe3c2eea51a2cca7e32426bb85055dde Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Mon, 30 Mar 2026 11:47:32 +0800 Subject: [PATCH] =?UTF-8?q?[=E7=A7=AF=E5=88=86=E5=95=86=E5=9F=8E]=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=AF=B9=E6=8E=A5API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- app/admin/controller/mall/Address.php | 6 +- app/admin/controller/mall/PintsOrder.php | 6 +- app/admin/controller/mall/Player.php | 149 --------- app/admin/controller/mall/PlayxOrder.php | 14 +- app/admin/controller/mall/PlayxUserAsset.php | 39 ++- app/admin/controller/mall/RedemptionOrder.php | 6 +- app/admin/controller/mall/User.php | 224 ------------- app/admin/model/mall/Player.php | 28 -- app/api/controller/Common.php | 3 + app/api/controller/v1/Auth.php | 64 +++- app/api/controller/v1/Playx.php | 314 +++++++++++++----- app/api/lang/en.php | 22 ++ app/api/lang/zh-cn.php | 22 ++ app/common/library/Auth.php | 5 + .../token/TokenExpirationException.php | 9 +- app/common/middleware/AllowCrossDomain.php | 2 +- app/common/middleware/LoadLangPack.php | 87 ++++- app/common/model/MallAddress.php | 4 +- app/common/model/MallPintsOrder.php | 4 +- app/common/model/MallPlayxUserAsset.php | 76 ++++- app/common/model/MallRedemptionOrder.php | 4 +- app/common/model/MallUser.php | 31 -- app/common/validate/MallUser.php | 25 -- app/process/PlayxJobs.php | 2 +- config/buildadmin.php | 10 +- config/playx.php | 5 + config/route.php | 1 + .../migrations/20250318120000_mall_player.php | 30 -- docs/PlayX-对接文档(积分商城).md | 46 ++- docs/PlayX-接口文档.md | 182 +++++++--- web/.env.production | 2 +- web/src/lang/backend/en/mall/address.ts | 4 +- web/src/lang/backend/en/mall/pintsOrder.ts | 4 +- web/src/lang/backend/en/mall/player.ts | 9 - .../lang/backend/en/mall/playxUserAsset.ts | 5 +- .../lang/backend/en/mall/redemptionOrder.ts | 4 +- web/src/lang/backend/en/mall/user.ts | 15 - web/src/lang/backend/zh-cn/mall/address.ts | 4 +- web/src/lang/backend/zh-cn/mall/pintsOrder.ts | 4 +- web/src/lang/backend/zh-cn/mall/player.ts | 9 - .../lang/backend/zh-cn/mall/playxUserAsset.ts | 6 +- .../backend/zh-cn/mall/redemptionOrder.ts | 4 +- web/src/lang/backend/zh-cn/mall/user.ts | 15 - web/src/views/backend/mall/address/index.vue | 4 +- .../views/backend/mall/address/popupForm.vue | 10 +- .../views/backend/mall/pintsOrder/index.vue | 4 +- .../backend/mall/pintsOrder/popupForm.vue | 12 +- web/src/views/backend/mall/player/index.vue | 102 ------ .../views/backend/mall/player/popupForm.vue | 109 ------ .../backend/mall/playxUserAsset/index.vue | 65 +++- .../backend/mall/redemptionOrder/index.vue | 4 +- .../mall/redemptionOrder/popupForm.vue | 10 +- web/src/views/backend/mall/user/index.vue | 120 ------- web/src/views/backend/mall/user/popupForm.vue | 127 ------- 55 files changed, 835 insertions(+), 1241 deletions(-) delete mode 100644 app/admin/controller/mall/Player.php delete mode 100644 app/admin/controller/mall/User.php delete mode 100644 app/admin/model/mall/Player.php delete mode 100644 app/common/model/MallUser.php delete mode 100644 app/common/validate/MallUser.php delete mode 100644 database/migrations/20250318120000_mall_player.php delete mode 100644 web/src/lang/backend/en/mall/player.ts delete mode 100644 web/src/lang/backend/en/mall/user.ts delete mode 100644 web/src/lang/backend/zh-cn/mall/player.ts delete mode 100644 web/src/lang/backend/zh-cn/mall/user.ts delete mode 100644 web/src/views/backend/mall/player/index.vue delete mode 100644 web/src/views/backend/mall/player/popupForm.vue delete mode 100644 web/src/views/backend/mall/user/index.vue delete mode 100644 web/src/views/backend/mall/user/popupForm.vue diff --git a/README.md b/README.md index f06a1ca..d59fd1f 100644 --- a/README.md +++ b/README.md @@ -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` 一对一扩展资产表 | --- diff --git a/app/admin/controller/mall/Address.php b/app/admin/controller/mall/Address.php index f791fb6..b22f316 100644 --- a/app/admin/controller/mall/Address.php +++ b/app/admin/controller/mall/Address.php @@ -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) diff --git a/app/admin/controller/mall/PintsOrder.php b/app/admin/controller/mall/PintsOrder.php index a228356..266c5b7 100644 --- a/app/admin/controller/mall/PintsOrder.php +++ b/app/admin/controller/mall/PintsOrder.php @@ -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) diff --git a/app/admin/controller/mall/Player.php b/app/admin/controller/mall/Player.php deleted file mode 100644 index 315567a..0000000 --- a/app/admin/controller/mall/Player.php +++ /dev/null @@ -1,149 +0,0 @@ -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 中对应的方法至此进行重写 - */ -} \ No newline at end of file diff --git a/app/admin/controller/mall/PlayxOrder.php b/app/admin/controller/mall/PlayxOrder.php index 290f716..32ac678 100644 --- a/app/admin/controller/mall/PlayxOrder.php +++ b/app/admin/controller/mall/PlayxOrder.php @@ -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); diff --git a/app/admin/controller/mall/PlayxUserAsset.php b/app/admin/controller/mall/PlayxUserAsset.php index a6b6ac7..0dc8228 100644 --- a/app/admin/controller/mall/PlayxUserAsset.php +++ b/app/admin/controller/mall/PlayxUserAsset.php @@ -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(), + ]); } } - diff --git a/app/admin/controller/mall/RedemptionOrder.php b/app/admin/controller/mall/RedemptionOrder.php index df111c4..e556323 100644 --- a/app/admin/controller/mall/RedemptionOrder.php +++ b/app/admin/controller/mall/RedemptionOrder.php @@ -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) diff --git a/app/admin/controller/mall/User.php b/app/admin/controller/mall/User.php deleted file mode 100644 index b3c43a0..0000000 --- a/app/admin/controller/mall/User.php +++ /dev/null @@ -1,224 +0,0 @@ -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(); - } -} diff --git a/app/admin/model/mall/Player.php b/app/admin/model/mall/Player.php deleted file mode 100644 index 5c47204..0000000 --- a/app/admin/model/mall/Player.php +++ /dev/null @@ -1,28 +0,0 @@ -where(['id' => $id])->update(['password' => hash_password($newPassword)]) !== false; - } -} \ No newline at end of file diff --git a/app/api/controller/Common.php b/app/api/controller/Common.php index 6077282..d9a70e9 100644 --- a/app/api/controller/Common.php +++ b/app/api/controller/Common.php @@ -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'], diff --git a/app/api/controller/v1/Auth.php b/app/api/controller/v1/Auth.php index 6eb5f8e..bf7195e 100644 --- a/app/api/controller/v1/Auth.php +++ b/app/api/controller/v1/Auth.php @@ -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, + ], + ]); + } } diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index af796d2..fcebd0d 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -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()) { + $asset = MallPlayxUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find(); + if ($asset) { + return intval($asset->getKey()); + } + } } - $expireTime = intval($session->expire_time ?? 0); - if ($expireTime <= time()) { - return null; + $assetId = $this->resolveAssetIdByToken($sessionId); + if ($assetId !== null) { + return $assetId; } - return strval($session->user_id ?? ''); + } + + $token = strval($request->post('token', $request->get('token', ''))); + if ($token === '') { + $token = get_auth_token(['ba', 'token'], $request); + } + if ($token !== '') { + return $this->resolveAssetIdByToken($token); } $userId = strval($request->post('user_id', $request->get('user_id', ''))); 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(); - $todayLimitDate = $date; - if ($asset) { - if ($asset->today_limit_date !== $todayLimitDate) { - $asset->today_claimed = 0; - $asset->today_limit_date = $todayLimitDate; - } - $asset->locked_points += $newLocked; - $asset->today_limit = $todayLimit; - $asset->username = $body['username'] ?? $asset->username; - $asset->save(); - } else { - MallPlayxUserAsset::create([ - 'user_id' => $userId, - 'username' => $body['username'] ?? '', - 'locked_points' => $newLocked, - 'available_points' => 0, - 'today_limit' => $todayLimit, - 'today_claimed' => 0, - 'today_limit_date' => $todayLimitDate, - 'create_time' => time(), - 'update_time' => time(), - ]); + $asset = $this->ensureAssetForPlayx($playxUserId, strval($body['username'] ?? '')); + if (!$asset) { + throw new \RuntimeException(__('Failed to map playx user to mall user')); } + $todayLimitDate = $date; + if ($asset->today_limit_date !== $todayLimitDate) { + $asset->today_claimed = 0; + $asset->today_limit_date = $todayLimitDate; + } + $asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked; + $asset->today_limit = $todayLimit; + $asset->playx_user_id = $playxUserId; + $uname = trim(strval($body['username'] ?? '')); + if ($uname !== '') { + $asset->username = $uname; + } + $asset->save(); Db::commit(); } 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'), [ diff --git a/app/api/lang/en.php b/app/api/lang/en.php index 6de9c7d..5b61696 100644 --- a/app/api/lang/en.php +++ b/app/api/lang/en.php @@ -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', ]; \ No newline at end of file diff --git a/app/api/lang/zh-cn.php b/app/api/lang/zh-cn.php index c36c48b..2d6410f 100644 --- a/app/api/lang/zh-cn.php +++ b/app/api/lang/zh-cn.php @@ -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 分钟', ]; \ No newline at end of file diff --git a/app/common/library/Auth.php b/app/common/library/Auth.php index 3174717..0da4f2a 100644 --- a/app/common/library/Auth.php +++ b/app/common/library/Auth.php @@ -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; diff --git a/app/common/library/token/TokenExpirationException.php b/app/common/library/token/TokenExpirationException.php index 9878967..81b976f 100644 --- a/app/common/library/token/TokenExpirationException.php +++ b/app/common/library/token/TokenExpirationException.php @@ -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); } diff --git a/app/common/middleware/AllowCrossDomain.php b/app/common/middleware/AllowCrossDomain.php index 347eb79..81996f7 100644 --- a/app/common/middleware/AllowCrossDomain.php +++ b/app/common/middleware/AllowCrossDomain.php @@ -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)) { diff --git a/app/common/middleware/LoadLangPack.php b/app/common/middleware/LoadLangPack.php index 8a3d29f..70a479a 100644 --- a/app/common/middleware/LoadLangPack.php +++ b/app/common/middleware/LoadLangPack.php @@ -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,22 +28,61 @@ 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 { - $acceptLang = $request->header('accept-language', ''); - if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) { + + $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 (preg_match('/^en/i', $acceptLang)) { - $langSet = 'en'; + } 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)) { + $langSet = 'en'; + } else { + $langSet = config('lang.default_lang', config('translation.locale', 'zh-cn')); + } + $langSet = str_replace('_', '-', strtolower((string) $langSet)); } else { $langSet = config('lang.default_lang', config('translation.locale', 'zh-cn')); + $langSet = str_replace('_', '-', strtolower((string) $langSet)); } - $langSet = str_replace('_', '-', strtolower($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; + } } diff --git a/app/common/model/MallAddress.php b/app/common/model/MallAddress.php index e096a6d..bba9067 100644 --- a/app/common/model/MallAddress.php +++ b/app/common/model/MallAddress.php @@ -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'); } } \ No newline at end of file diff --git a/app/common/model/MallPintsOrder.php b/app/common/model/MallPintsOrder.php index cd722a9..5ea2932 100644 --- a/app/common/model/MallPintsOrder.php +++ b/app/common/model/MallPintsOrder.php @@ -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'); } } \ No newline at end of file diff --git a/app/common/model/MallPlayxUserAsset.php b/app/common/model/MallPlayxUserAsset.php index d138a45..3958560 100644 --- a/app/common/model/MallPlayxUserAsset.php +++ b/app/common/model/MallPlayxUserAsset.php @@ -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 { @@ -16,11 +17,72 @@ class MallPlayxUserAsset extends Model protected bool $autoWriteTimestamp = true; protected array $type = [ - 'create_time' => 'integer', - 'update_time' => 'integer', - 'locked_points' => 'integer', - 'available_points' => 'integer', - 'today_limit' => 'integer', - 'today_claimed' => 'integer', + 'create_time' => 'integer', + 'update_time' => 'integer', + 'locked_points' => 'integer', + 'available_points' => 'integer', + 'today_limit' => 'integer', + 'today_claimed' => 'integer', + 'admin_id' => 'integer', ]; + + /** + * H5 临时登录:按用户名查找或创建资产行,playx_user_id 使用 mall_{id} + */ + public static function ensureForUsername(string $username): self + { + $username = trim($username); + $existing = self::where('username', $username)->find(); + if ($existing) { + return $existing; + } + + $phone = 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; + } } diff --git a/app/common/model/MallRedemptionOrder.php b/app/common/model/MallRedemptionOrder.php index cc381de..684ed3b 100644 --- a/app/common/model/MallRedemptionOrder.php +++ b/app/common/model/MallRedemptionOrder.php @@ -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 diff --git a/app/common/model/MallUser.php b/app/common/model/MallUser.php deleted file mode 100644 index 15299bb..0000000 --- a/app/common/model/MallUser.php +++ /dev/null @@ -1,31 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/common/validate/MallUser.php b/app/common/validate/MallUser.php deleted file mode 100644 index 845f967..0000000 --- a/app/common/validate/MallUser.php +++ /dev/null @@ -1,25 +0,0 @@ - 'require', - 'phone' => 'require', - ]; - - protected $message = [ - 'username.require' => '用户名不能为空', - 'phone.require' => '手机号不能为空', - ]; - - protected $scene = [ - 'add' => ['username', 'phone'], - 'edit' => ['username', 'phone'], - ]; -} diff --git a/app/process/PlayxJobs.php b/app/process/PlayxJobs.php index 6b905ed..d1ec663 100644 --- a/app/process/PlayxJobs.php +++ b/app/process/PlayxJobs.php @@ -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; } diff --git a/config/buildadmin.php b/config/buildadmin.php index 7182521..40694a1 100644 --- a/config/buildadmin.php +++ b/config/buildadmin.php @@ -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', diff --git a/config/playx.php b/config/playx.php index a837b80..4bd4af4 100644 --- a/config/playx.php +++ b/config/playx.php @@ -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', '')), diff --git a/config/route.php b/config/route.php index e67780c..a2f7931 100644 --- a/config/route.php +++ b/config/route.php @@ -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']); diff --git a/database/migrations/20250318120000_mall_player.php b/database/migrations/20250318120000_mall_player.php deleted file mode 100644 index 522f7aa..0000000 --- a/database/migrations/20250318120000_mall_player.php +++ /dev/null @@ -1,30 +0,0 @@ -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(); - } - } -} diff --git a/docs/PlayX-对接文档(积分商城).md b/docs/PlayX-对接文档(积分商城).md index 022858c..ad7b858 100644 --- a/docs/PlayX-对接文档(积分商城).md +++ b/docs/PlayX-对接文档(积分商城).md @@ -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 → 商城) diff --git a/docs/PlayX-接口文档.md b/docs/PlayX-接口文档.md index 612eabf..b6009c0 100644 --- a/docs/PlayX-接口文档.md +++ b/docs/PlayX-接口文档.md @@ -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 | 否 | 历史总充值 | @@ -101,9 +101,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \ ## 2. PlayX -> 积分商城(商城调用 PlayX) -> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。 +> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。 +> **说明**:H5 调商城的 **`/api/v1/playx/verify-token`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。 -### 2.1 Token Verification API +### 2.1 Token Verification 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`。 diff --git a/web/.env.production b/web/.env.production index 7ef3784..d765cd2 100644 --- a/web/.env.production +++ b/web/.env.production @@ -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' diff --git a/web/src/lang/backend/en/mall/address.ts b/web/src/lang/backend/en/mall/address.ts index 41ecce3..050cef0 100644 --- a/web/src/lang/backend/en/mall/address.ts +++ b/web/src/lang/backend/en/mall/address.ts @@ -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', diff --git a/web/src/lang/backend/en/mall/pintsOrder.ts b/web/src/lang/backend/en/mall/pintsOrder.ts index 688c4b1..2691df0 100644 --- a/web/src/lang/backend/en/mall/pintsOrder.ts +++ b/web/src/lang/backend/en/mall/pintsOrder.ts @@ -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', diff --git a/web/src/lang/backend/en/mall/player.ts b/web/src/lang/backend/en/mall/player.ts deleted file mode 100644 index c84f935..0000000 --- a/web/src/lang/backend/en/mall/player.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default { - id: 'id', - username: 'username', - password: 'password', - create_time: 'create_time', - update_time: 'update_time', - score: 'score', - quickSearchFields: 'id', -} diff --git a/web/src/lang/backend/en/mall/playxUserAsset.ts b/web/src/lang/backend/en/mall/playxUserAsset.ts index 005e731..d390294 100644 --- a/web/src/lang/backend/en/mall/playxUserAsset.ts +++ b/web/src/lang/backend/en/mall/playxUserAsset.ts @@ -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', } diff --git a/web/src/lang/backend/en/mall/redemptionOrder.ts b/web/src/lang/backend/en/mall/redemptionOrder.ts index 8538ebd..44ff1ff 100644 --- a/web/src/lang/backend/en/mall/redemptionOrder.ts +++ b/web/src/lang/backend/en/mall/redemptionOrder.ts @@ -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', diff --git a/web/src/lang/backend/en/mall/user.ts b/web/src/lang/backend/en/mall/user.ts deleted file mode 100644 index 3d67d60..0000000 --- a/web/src/lang/backend/en/mall/user.ts +++ /dev/null @@ -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', -} diff --git a/web/src/lang/backend/zh-cn/mall/address.ts b/web/src/lang/backend/zh-cn/mall/address.ts index 2ffbbfb..8d8e745 100644 --- a/web/src/lang/backend/zh-cn/mall/address.ts +++ b/web/src/lang/backend/zh-cn/mall/address.ts @@ -1,7 +1,7 @@ export default { id: 'ID', - mall_user_id: '用户', - malluser__username: '用户名', + playx_user_asset_id: '用户资产', + playxuserasset__username: '用户名', phone: '电话', region: '地区', detail_address: '详细地址', diff --git a/web/src/lang/backend/zh-cn/mall/pintsOrder.ts b/web/src/lang/backend/zh-cn/mall/pintsOrder.ts index eb03429..6e4a9ff 100644 --- a/web/src/lang/backend/zh-cn/mall/pintsOrder.ts +++ b/web/src/lang/backend/zh-cn/mall/pintsOrder.ts @@ -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': '充值', diff --git a/web/src/lang/backend/zh-cn/mall/player.ts b/web/src/lang/backend/zh-cn/mall/player.ts deleted file mode 100644 index ed43906..0000000 --- a/web/src/lang/backend/zh-cn/mall/player.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default { - id: 'ID', - username: '用户名', - password: '密码', - create_time: '创建时间', - update_time: '修改时间', - score: '积分', - quickSearchFields: 'ID', -} diff --git a/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts b/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts index 4754a4f..3e8aaaa 100644 --- a/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts +++ b/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts @@ -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、用户名、手机号', } - diff --git a/web/src/lang/backend/zh-cn/mall/redemptionOrder.ts b/web/src/lang/backend/zh-cn/mall/redemptionOrder.ts index 666454d..892f2de 100644 --- a/web/src/lang/backend/zh-cn/mall/redemptionOrder.ts +++ b/web/src/lang/backend/zh-cn/mall/redemptionOrder.ts @@ -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': '已发放', diff --git a/web/src/lang/backend/zh-cn/mall/user.ts b/web/src/lang/backend/zh-cn/mall/user.ts deleted file mode 100644 index 3d3c76f..0000000 --- a/web/src/lang/backend/zh-cn/mall/user.ts +++ /dev/null @@ -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', -} diff --git a/web/src/views/backend/mall/address/index.vue b/web/src/views/backend/mall/address/index.vue index 2489bde..a2c605b 100644 --- a/web/src/views/backend/mall/address/index.vue +++ b/web/src/views/backend/mall/address/index.vue @@ -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'), diff --git a/web/src/views/backend/mall/address/popupForm.vue b/web/src/views/backend/mall/address/popupForm.vue index e83ca7c..3d23717 100644 --- a/web/src/views/backend/mall/address/popupForm.vue +++ b/web/src/views/backend/mall/address/popupForm.vue @@ -30,12 +30,12 @@ :rules="rules" > > = 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') })], diff --git a/web/src/views/backend/mall/player/index.vue b/web/src/views/backend/mall/player/index.vue deleted file mode 100644 index 5502f2b..0000000 --- a/web/src/views/backend/mall/player/index.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - - diff --git a/web/src/views/backend/mall/player/popupForm.vue b/web/src/views/backend/mall/player/popupForm.vue deleted file mode 100644 index dd829c7..0000000 --- a/web/src/views/backend/mall/player/popupForm.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - diff --git a/web/src/views/backend/mall/playxUserAsset/index.vue b/web/src/views/backend/mall/playxUserAsset/index.vue index e056a76..1679910 100644 --- a/web/src/views/backend/mall/playxUserAsset/index.vue +++ b/web/src/views/backend/mall/playxUserAsset/index.vue @@ -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(() => { - diff --git a/web/src/views/backend/mall/redemptionOrder/index.vue b/web/src/views/backend/mall/redemptionOrder/index.vue index 612a717..eec5f84 100644 --- a/web/src/views/backend/mall/redemptionOrder/index.vue +++ b/web/src/views/backend/mall/redemptionOrder/index.vue @@ -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'), diff --git a/web/src/views/backend/mall/redemptionOrder/popupForm.vue b/web/src/views/backend/mall/redemptionOrder/popupForm.vue index 82f038c..174a9db 100644 --- a/web/src/views/backend/mall/redemptionOrder/popupForm.vue +++ b/web/src/views/backend/mall/redemptionOrder/popupForm.vue @@ -37,12 +37,12 @@ :placeholder="t('Please input field', { field: t('mall.redemptionOrder.order') })" /> -
- - - - - - - - - -
- - - -
- - - - - diff --git a/web/src/views/backend/mall/user/popupForm.vue b/web/src/views/backend/mall/user/popupForm.vue deleted file mode 100644 index 2dcf999..0000000 --- a/web/src/views/backend/mall/user/popupForm.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - -