[积分商城]优化对接API
This commit is contained in:
@@ -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()) {
|
||||
$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'), [
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user