优化接口以及后台页面样式

This commit is contained in:
2026-03-31 15:37:32 +08:00
parent 2868899253
commit 520e950dc5
28 changed files with 1241 additions and 311 deletions

View File

@@ -46,6 +46,7 @@ class Order extends Backend
'receiver_name',
'receiver_phone',
'receiver_address',
'mall_address_id',
'create_time',
'update_time',
];
@@ -139,7 +140,52 @@ class Order extends Backend
}
/**
* PHYSICAL 驳回:更新状态为 REJECTED并退回积分到 available_points
* 审核通过(非 PHYSICAL:更新状态为 COMPLETED
*/
public function approve(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->method() !== 'POST') {
return $this->error(__('Parameter error'));
}
$id = $request->post('id', 0);
if (!$id) {
return $this->error(__('Missing required fields'));
}
$order = MallOrder::where('id', $id)->find();
if (!$order) {
return $this->error(__('Record not found'));
}
if ($order->status !== MallOrder::STATUS_PENDING) {
return $this->error(__('Order status must be PENDING'));
}
if ($order->type === MallOrder::TYPE_PHYSICAL) {
return $this->error(__('Order type not supported'));
}
Db::startTrans();
try {
$order->status = MallOrder::STATUS_COMPLETED;
$order->update_time = time();
$order->save();
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success(__('Approved successfully'));
}
/**
* 审核驳回:更新状态为 REJECTED并退回积分到 available_points所有类型通用
*/
public function reject(Request $request): Response
{
@@ -164,9 +210,6 @@ class Order extends Backend
if (!$order) {
return $this->error(__('Record not found'));
}
if ($order->type !== MallOrder::TYPE_PHYSICAL) {
return $this->error(__('Order type not PHYSICAL'));
}
if ($order->status !== MallOrder::STATUS_PENDING) {
return $this->error(__('Order status must be PENDING'));
}
@@ -187,6 +230,7 @@ class Order extends Backend
$order->status = MallOrder::STATUS_REJECTED;
$order->reject_reason = $rejectReason;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->update_time = time();
$order->save();
Db::commit();

View File

@@ -115,6 +115,24 @@ class Auth extends Api
}
$username = trim(strval($request->get('username', $request->post('username', ''))));
// 兼容querystring 中未编码的 '+' 会被解析为空格application/x-www-form-urlencoded 规则)
// 例如:/api/v1/temLogin?username=+607... 期望保留 '+',则从原始 querystring 提取并还原
if ($username !== '' && str_contains($username, ' ')) {
$qs = $request->queryString();
if (is_string($qs) && $qs !== '') {
foreach (explode('&', $qs) as $pair) {
if ($pair === '' || !str_contains($pair, '=')) {
continue;
}
[$k, $v] = explode('=', $pair, 2);
if (rawurldecode($k) === 'username') {
// 先把 %xx 解码;注意这里不把 '+' 当空格处理,从而保留 '+'
$username = trim(rawurldecode($v));
break;
}
}
}
}
if ($username === '') {
return $this->error(__('Parameter username can not be empty'));
}

View File

@@ -382,7 +382,7 @@ class Playx extends Api
$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(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
if (config('playx.verify_token_local_only', false)) {
@@ -410,7 +410,7 @@ class Playx extends Api
$data = json_decode(strval($res->getBody()), true);
if ($code !== 200 || empty($data['user_id'])) {
$remoteMsg = $data['message'] ?? '';
$msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Invalid token');
$msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Token expiration');
return $this->error($msg, null, 0, ['statusCode' => 401]);
}
@@ -454,20 +454,20 @@ class Playx extends Api
{
$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]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$tokenType = strval($tokenData['type'] ?? '');
if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$assetId = intval($tokenData['user_id'] ?? 0);
if ($assetId <= 0) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$asset = MallUserAsset::where('id', $assetId)->find();
if (!$asset) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$playxUserId = strval($asset->playx_user_id ?? '');
@@ -507,7 +507,7 @@ class Playx extends Api
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$asset = $this->getAssetById($assetId);
@@ -546,7 +546,10 @@ class Playx extends Api
$claimRequestId = strval($request->post('claim_request_id', ''));
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($claimRequestId === '' || $assetId === null) {
if ($assetId === null) {
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
if ($claimRequestId === '') {
return $this->error(__('claim_request_id and user_id/session_id required'));
}
@@ -660,7 +663,7 @@ class Playx extends Api
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
@@ -689,7 +692,7 @@ class Playx extends Api
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$list = MallAddress::where('playx_user_asset_id', $assetId)
@@ -713,16 +716,16 @@ class Playx extends Api
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$phone = trim(strval($request->post('phone', '')));
$receiverName = trim(strval($request->post('receiver_name', '')));
$region = $request->post('region', '');
$detailAddress = trim(strval($request->post('detail_address', '')));
$address = trim(strval($request->post('address', '')));
$defaultSetting = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
if ($phone === '' || $detailAddress === '' || $address === '' || $region === '' || $region === null) {
if ($phone === '' || $receiverName === '' || $detailAddress === '' || $region === '' || $region === null) {
return $this->error(__('Missing required fields'));
}
@@ -734,10 +737,10 @@ class Playx extends Api
$created = MallAddress::create([
'playx_user_asset_id' => $assetId,
'receiver_name' => $receiverName,
'phone' => $phone,
'region' => $region,
'detail_address' => $detailAddress,
'address' => $address,
'default_setting' => $defaultSetting,
'create_time' => time(),
'update_time' => time(),
@@ -767,7 +770,7 @@ class Playx extends Api
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$id = intval($request->post('id', 0));
@@ -784,15 +787,15 @@ class Playx extends Api
if ($request->post('phone', null) !== null) {
$updates['phone'] = trim(strval($request->post('phone', '')));
}
if ($request->post('receiver_name', null) !== null) {
$updates['receiver_name'] = trim(strval($request->post('receiver_name', '')));
}
if ($request->post('region', null) !== null) {
$updates['region'] = $request->post('region', '');
}
if ($request->post('detail_address', null) !== null) {
$updates['detail_address'] = trim(strval($request->post('detail_address', '')));
}
if ($request->post('address', null) !== null) {
$updates['address'] = trim(strval($request->post('address', '')));
}
if ($request->post('default_setting', null) !== null) {
$updates['default_setting'] = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
}
@@ -830,7 +833,7 @@ class Playx extends Api
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$id = intval($request->post('id', 0));
@@ -897,7 +900,10 @@ class Playx extends Api
$itemId = intval($request->post('item_id', 0));
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($itemId <= 0 || $assetId === null) {
if ($assetId === null) {
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
if ($itemId <= 0) {
return $this->error(__('item_id and user_id/session_id required'));
}
@@ -963,11 +969,21 @@ class Playx extends Api
}
$itemId = intval($request->post('item_id', 0));
$addressId = intval($request->post('address_id', 0));
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
$receiverName = $request->post('receiver_name', '');
$receiverPhone = $request->post('receiver_phone', '');
$receiverAddress = $request->post('receiver_address', '');
if ($itemId <= 0 || $assetId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
if ($itemId <= 0 || $addressId <= 0) {
return $this->error(__('Missing required fields'));
}
if ($assetId === null) {
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
$addrRow = MallAddress::where('id', $addressId)->where('playx_user_asset_id', $assetId)->find();
if (!$addrRow) {
return $this->error(__('Shipping address not found'));
}
$snapshot = MallAddress::snapshotForPhysicalOrder($addrRow);
if ($snapshot['receiver_phone'] === '' || $snapshot['receiver_address'] === '' || $snapshot['receiver_name'] === '') {
return $this->error(__('Missing required fields'));
}
@@ -996,9 +1012,10 @@ class Playx extends Api
'status' => MallOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
'points_cost' => $item->score,
'receiver_name' => $receiverName,
'receiver_phone' => $receiverPhone,
'receiver_address' => $receiverAddress,
'mall_address_id' => $addressId,
'receiver_name' => $snapshot['receiver_name'],
'receiver_phone' => $snapshot['receiver_phone'],
'receiver_address' => $snapshot['receiver_address'],
'create_time' => time(),
'update_time' => time(),
]);
@@ -1026,7 +1043,10 @@ class Playx extends Api
$itemId = intval($request->post('item_id', 0));
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($itemId <= 0 || $assetId === null) {
if ($assetId === null) {
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
}
if ($itemId <= 0) {
return $this->error(__('item_id and user_id/session_id required'));
}

View File

@@ -22,6 +22,7 @@ return [
'Temp login is disabled' => 'Temp login is disabled',
'Failed to create temp account' => 'Failed to allocate a unique phone number, please retry later',
'Parameter username can not be empty' => 'Parameter username can not be empty',
'Token expiration' => 'Session expired, please login again.',
// Member center account
'Data updated successfully~' => 'Data updated successfully~',
'Password has been changed~' => 'Password has been changed~',
@@ -49,6 +50,7 @@ return [
'Insufficient points' => 'Insufficient points',
'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes',
'Missing required fields' => 'Missing required fields',
'Shipping address not found' => 'Shipping address not found',
'Out of stock' => 'Out of stock',
'Redeem success' => 'Redeem successful',
'Withdraw submitted, please wait about 10 minutes' => 'Withdrawal submitted, please wait about 10 minutes',

View File

@@ -82,6 +82,8 @@ return [
'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟',
'Missing required fields' => '缺少必填字段',
'Out of stock' => '库存不足',
'Record not found' => '记录不存在',
'Shipping address not found' => '收货地址不存在',
'Redeem success' => '兑换成功',
'Withdraw submitted, please wait about 10 minutes' => '提现申请已提交,请等待约 10 分钟',
];

View File

@@ -41,7 +41,18 @@ class MallAddress extends Model
public function getregionTextAttr($value, $row): string
{
if ($row['region'] === '' || $row['region'] === null) return '';
$cityNames = \support\think\Db::name('area')->whereIn('id', $row['region'])->column('name');
$region = $row['region'];
$ids = $region;
if (!is_array($ids)) {
$ids = explode(',', (string) $ids);
}
$ids = array_values(array_filter(array_map('trim', $ids), static function ($s) {
return $s !== '';
}));
if (empty($ids)) {
return '';
}
$cityNames = \support\think\Db::name('area')->whereIn('id', $ids)->column('name');
return $cityNames ? implode(',', $cityNames) : '';
}
@@ -49,4 +60,27 @@ class MallAddress extends Model
{
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
}
/**
* 实物订单收货快照(写入 mall_order.receiver_*,与 mall_address 当前内容一致)
*
* @return array{receiver_name: string, receiver_phone: string, receiver_address: string}
*/
public static function snapshotForPhysicalOrder(self $addr): array
{
$regionText = $addr->region_text ?? '';
$parts = array_filter([
trim($regionText),
trim($addr->detail_address ?? ''),
], static function ($s) {
return $s !== '';
});
$receiverAddress = implode(' ', $parts);
return [
'receiver_name' => trim($addr->receiver_name ?? ''),
'receiver_phone' => trim($addr->phone ?? ''),
'receiver_address' => $receiverAddress,
];
}
}

View File

@@ -28,6 +28,7 @@ use support\think\Model;
* @property string $receiver_name
* @property string $receiver_phone
* @property string|null $receiver_address
* @property int|null $mall_address_id
*/
class MallOrder extends Model
{
@@ -56,12 +57,18 @@ class MallOrder extends Model
'points_cost' => 'integer',
'amount' => 'float',
'multiplier' => 'integer',
'retry_count' => 'integer',
'retry_count' => 'integer',
'mall_address_id' => 'integer',
];
public function mallItem(): \think\model\relation\BelongsTo
{
return $this->belongsTo(MallItem::class, 'mall_item_id', 'id');
}
public function mallAddress(): \think\model\relation\BelongsTo
{
return $this->belongsTo(MallAddress::class, 'mall_address_id', 'id');
}
}

View File

@@ -31,18 +31,21 @@ class MallUserAsset extends Model
*/
public static function ensureForUsername(string $username): self
{
// $username = trim($username);
$username = trim($username);
$existing = self::where('username', $username)->find();
if ($existing) {
return $existing;
}
//手机号直接=用户名,暂时不做验证
// $phone = self::allocateUniquePhone();
// 创建用户时phone 与 username 同值H5 临时账号)
$phone = $username;
if ($phone === null) {
throw new \RuntimeException('Failed to allocate unique phone');
$phoneExisting = self::where('phone', $phone)->find();
if ($phoneExisting) {
// 若历史数据存在“手机号=用户名”的行,直接复用;若不一致则拒绝创建,避免污染
if (trim((string) ($phoneExisting->username ?? '')) === $username) {
return $phoneExisting;
}
throw new \RuntimeException('Username is already used by another account');
}
$pwd = hash_password(Random::build('alnum', 16));
@@ -77,16 +80,6 @@ class MallUserAsset extends Model
return $created;
}
private static function allocateUniquePhone(): ?string
{
for ($i = 0; $i < 8; $i++) {
$candidate = '13' . sprintf('%09d', mt_rand(0, 999999999));
if (!self::where('phone', $candidate)->find()) {
return $candidate;
}
}
return null;
}
// allocateUniquePhone 已废弃:临时登录场景下 phone=用户名
}