优化接口以及后台页面样式
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 分钟',
|
||||
];
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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=用户名
|
||||
}
|
||||
|
||||
|
||||
@@ -113,19 +113,19 @@ 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']);
|
||||
Route::post('/api/v1/playx/verify-token', [\app\api\controller\v1\Playx::class, 'verifyToken']);
|
||||
Route::get('/api/v1/playx/assets', [\app\api\controller\v1\Playx::class, 'assets']);
|
||||
Route::post('/api/v1/playx/claim', [\app\api\controller\v1\Playx::class, 'claim']);
|
||||
Route::get('/api/v1/playx/items', [\app\api\controller\v1\Playx::class, 'items']);
|
||||
Route::post('/api/v1/playx/bonus/redeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']);
|
||||
Route::post('/api/v1/playx/physical/redeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
||||
Route::post('/api/v1/playx/withdraw/apply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
|
||||
Route::get('/api/v1/playx/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
||||
Route::get('/api/v1/playx/address/list', [\app\api\controller\v1\Playx::class, 'addressList']);
|
||||
Route::post('/api/v1/playx/address/add', [\app\api\controller\v1\Playx::class, 'addressAdd']);
|
||||
Route::post('/api/v1/playx/address/edit', [\app\api\controller\v1\Playx::class, 'addressEdit']);
|
||||
Route::post('/api/v1/playx/address/delete', [\app\api\controller\v1\Playx::class, 'addressDelete']);
|
||||
Route::post('/api/v1/mall/dailyPush', [\app\api\controller\v1\Playx::class, 'dailyPush']);
|
||||
Route::post('/api/v1/mall/verifyToken', [\app\api\controller\v1\Playx::class, 'verifyToken']);
|
||||
Route::get('/api/v1/mall/assets', [\app\api\controller\v1\Playx::class, 'assets']);
|
||||
Route::post('/api/v1/mall/claim', [\app\api\controller\v1\Playx::class, 'claim']);
|
||||
Route::get('/api/v1/mall/items', [\app\api\controller\v1\Playx::class, 'items']);
|
||||
Route::post('/api/v1/mall/bonusRedeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']);
|
||||
Route::post('/api/v1/mall/physicalRedeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
||||
Route::post('/api/v1/mall/withdrawApply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
|
||||
Route::get('/api/v1/mall/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
||||
Route::get('/api/v1/mall/addressList', [\app\api\controller\v1\Playx::class, 'addressList']);
|
||||
Route::post('/api/v1/mall/addressAdd', [\app\api\controller\v1\Playx::class, 'addressAdd']);
|
||||
Route::post('/api/v1/mall/addressEdit', [\app\api\controller\v1\Playx::class, 'addressEdit']);
|
||||
Route::post('/api/v1/mall/addressDelete', [\app\api\controller\v1\Playx::class, 'addressDelete']);
|
||||
|
||||
// ==================== Admin 路由 ====================
|
||||
// Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容
|
||||
|
||||
205
docs/H5-积分商城接口文档.md
Normal file
205
docs/H5-积分商城接口文档.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# H5 积分商城接口文档(含流程说明)
|
||||
|
||||
> 面向:H5(活动页/积分商城前台)调用
|
||||
> 基础路径:`/api/v1`
|
||||
> 返回结构:BuildAdmin 通用 `code/msg/time/data`(成功 `code=1`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体流程说明
|
||||
|
||||
### 1.1 流程 A:H5 临时登录(推荐)
|
||||
|
||||
适用场景:H5 只需要“用户名级别”的轻量登录,不依赖 PlayX 的 token。
|
||||
|
||||
1. H5 调用 `GET/POST /api/v1/temLogin?username=xxx` 获取 **商城 token**(类型 `muser`)
|
||||
2. H5 后续请求统一携带该 token(推荐放在 Header:`token: <muser_token>`,也可用参数 `token`)
|
||||
3. H5 调用:
|
||||
- `GET /api/v1/mall/assets` 获取资产
|
||||
- `POST /api/v1/mall/claim` 领取积分(幂等)
|
||||
- `GET /api/v1/mall/items` 获取商品
|
||||
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply` 提交兑换/提现
|
||||
- `GET /api/v1/mall/orders` 查询订单
|
||||
- `GET/POST /api/v1/mall/address*` 管理地址(addressList/addressAdd/addressEdit/addressDelete)
|
||||
|
||||
### 1.2 流程 B:PlayX token 换取 session(兼容)
|
||||
|
||||
适用场景:H5 已经拿到了 PlayX 下发的 token,希望换取商城侧 `session_id`。
|
||||
|
||||
1. H5 调用 `POST /api/v1/mall/verifyToken`(传 `token` 或 `session`)
|
||||
2. 服务端返回 `data.session_id`
|
||||
3. H5 后续请求携带 `session_id`(优先级高于 token)
|
||||
|
||||
---
|
||||
|
||||
## 2. 身份与鉴权(重要)
|
||||
|
||||
以下接口都会通过服务端逻辑解析“当前资产主体”,优先级如下:
|
||||
|
||||
1. **`session_id`**(GET/POST):对应表 `mall_session`,未过期则可映射到资产主体
|
||||
2. **`token`**(GET/POST 或 Header):支持会员 token 或 `muser` token(H5 临时登录签发)
|
||||
3. **`user_id`**(GET/POST):
|
||||
- 纯数字:视为 `mall_user_asset.id`
|
||||
- 非纯数字:按 `mall_user_asset.playx_user_id` 查找
|
||||
|
||||
推荐做法:
|
||||
- H5 统一只用 **Header `token`**(值为 `muser` token),避免 URL 泄露与参数歧义。
|
||||
|
||||
---
|
||||
|
||||
## 3. 接口列表(H5 常用)
|
||||
|
||||
### 3.1 H5 临时登录
|
||||
|
||||
**GET/POST** ` /api/v1/temLogin `
|
||||
|
||||
参数:
|
||||
- `username`:必填,用户名(字符串)
|
||||
|
||||
成功返回 `data.userInfo`:
|
||||
- `id`:资产主键(`mall_user_asset.id`)
|
||||
- `username`:用户名
|
||||
- `playx_user_id`:映射的 PlayX 用户标识(字符串)
|
||||
- `token`:**muser token**(后续请求使用)
|
||||
- `refresh_token`:刷新 token(当前前端未强依赖可不接)
|
||||
- `expires_in`:秒
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
curl "https://{域名}/api/v1/temLogin?username=test001"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 资产查询
|
||||
|
||||
**GET** ` /api/v1/mall/assets `
|
||||
|
||||
鉴权:携带 `token` 或 `session_id` 或 `user_id`
|
||||
|
||||
成功返回 `data`:
|
||||
- `locked_points`:待领取积分
|
||||
- `available_points`:可用积分
|
||||
- `today_limit`:今日可领取上限
|
||||
- `today_claimed`:今日已领取
|
||||
- `withdrawable_cash`:可提现现金(积分按配置比例换算)
|
||||
|
||||
---
|
||||
|
||||
### 3.3 领取积分(幂等)
|
||||
|
||||
**POST** ` /api/v1/mall/claim `
|
||||
|
||||
参数:
|
||||
- `claim_request_id`:必填,幂等键(建议:`{业务前缀}_{assetId}_{毫秒时间戳}`)
|
||||
- 身份参数:`token` / `session_id` / `user_id` 三选一(推荐 `token`)
|
||||
|
||||
说明:
|
||||
- 同一个 `claim_request_id` 重复提交会直接返回成功(不会重复入账)
|
||||
- 会受 `today_limit/today_claimed/locked_points` 限制
|
||||
|
||||
---
|
||||
|
||||
### 3.4 商品列表
|
||||
|
||||
**GET** ` /api/v1/mall/items `
|
||||
|
||||
参数:
|
||||
- `type`:可选,`BONUS | PHYSICAL | WITHDRAW`
|
||||
|
||||
---
|
||||
|
||||
### 3.5 红利兑换(提交订单)
|
||||
|
||||
**POST** ` /api/v1/mall/bonusRedeem `
|
||||
|
||||
参数:
|
||||
- `item_id`:必填
|
||||
- 身份参数:`token` / `session_id` / `user_id`
|
||||
|
||||
返回:
|
||||
- `data.order_id`
|
||||
- `data.status`(通常 `PENDING`)
|
||||
|
||||
---
|
||||
|
||||
### 3.6 实物兑换(提交订单)
|
||||
|
||||
**POST** ` /api/v1/mall/physicalRedeem `
|
||||
|
||||
参数:
|
||||
- `item_id`:必填
|
||||
- `address_id`:必填,收货地址主键(`mall_address.id`,须为当前用户资产下地址)
|
||||
- 身份参数:`token` / `session_id` / `user_id`
|
||||
|
||||
说明:服务端会将该地址在下单时刻的 **收货人 / 电话 / 完整地址** 写入订单字段(快照),并写入 `mall_order.mall_address_id` 关联所选地址。
|
||||
|
||||
---
|
||||
|
||||
### 3.7 提现申请(提交订单)
|
||||
|
||||
**POST** ` /api/v1/mall/withdrawApply `
|
||||
|
||||
参数:
|
||||
- `item_id`:必填
|
||||
- 身份参数:`token` / `session_id` / `user_id`
|
||||
|
||||
---
|
||||
|
||||
### 3.8 订单列表
|
||||
|
||||
**GET** ` /api/v1/mall/orders `
|
||||
|
||||
鉴权:`token` / `session_id` / `user_id`
|
||||
|
||||
说明:
|
||||
- 返回最多 100 条
|
||||
- 订单里包含 `mallItem`(商品信息)
|
||||
|
||||
---
|
||||
|
||||
## 4. 地址管理(H5)
|
||||
|
||||
> 地址与资产主体通过 `playx_user_asset_id` 关联(即 `mall_user_asset.id`)。
|
||||
|
||||
### 4.1 地址列表
|
||||
**GET** ` /api/v1/mall/addressList `
|
||||
|
||||
### 4.2 新增地址
|
||||
**POST** ` /api/v1/mall/addressAdd `
|
||||
|
||||
Body 含 `receiver_name`(收货人,建议填写;实物兑换下单快照需要非空的收货人、电话与拼接后的完整地址)。
|
||||
|
||||
### 4.3 编辑地址
|
||||
**POST** ` /api/v1/mall/addressEdit `
|
||||
|
||||
### 4.4 删除地址
|
||||
**POST** ` /api/v1/mall/addressDelete `
|
||||
|
||||
---
|
||||
|
||||
## 5. session 换取(可选)
|
||||
|
||||
### 5.1 token 换 session
|
||||
|
||||
**POST** ` /api/v1/mall/verifyToken `
|
||||
|
||||
参数(二选一):
|
||||
- `token`
|
||||
- `session`
|
||||
|
||||
成功返回:
|
||||
- `data.session_id`
|
||||
- `data.user_id`
|
||||
- `data.username`
|
||||
- `data.token_expire_at`
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见错误与排查
|
||||
|
||||
- **401 登录态过期**:token/session 过期或不匹配;请重新 `temLogin` 或重新 `verifyToken`
|
||||
- **提示缺少必填字段**:按各接口参数补齐(如 `claim_request_id`、`item_id`、`address_id`(实物)、地址中收货人/电话/完整地址等)
|
||||
- **积分不足/无可领取积分**:`locked_points<=0` 或已达 `today_limit`
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
### 1.1 Daily Push API
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/daily-push`
|
||||
* 路径:`/api/v1/mall/dailyPush`
|
||||
|
||||
#### Header(多语言,可选)
|
||||
- `lang`: `zh`/`zh-cn` 返回中文(默认);`en` 返回英文
|
||||
@@ -25,7 +25,7 @@
|
||||
- `X-Signature`:签名(HMAC_SHA256)
|
||||
|
||||
服务端签名计算:
|
||||
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body)`
|
||||
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)`
|
||||
- `expected = hash_hmac('sha256', canonical, daily_push_secret)`
|
||||
- 校验:`hash_equals(expected, X-Signature)`
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
#### 示例(未开启签名校验)
|
||||
请求:
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"request_id":"req_1001",
|
||||
@@ -136,7 +136,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
#### 示例(新版批量上报)
|
||||
请求:
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"report_date": "1700000000",
|
||||
@@ -181,7 +181,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
## 2. PlayX -> 积分商城(商城调用 PlayX)
|
||||
|
||||
> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。
|
||||
> **说明**:H5 调商城的 **`/api/v1/playx/verify-token`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
|
||||
> **说明**:H5 调商城的 **`/api/v1/mall/verifyToken`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
|
||||
|
||||
### 2.1 Token Verification API(PlayX 侧实现,远程验证时使用)
|
||||
* 方法:`POST`
|
||||
@@ -406,7 +406,7 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
|
||||
### 3.3 Token 验证(换 session)
|
||||
|
||||
* 方法:`POST`(推荐 `GET` 传 `token` 亦可)
|
||||
* 路径:`/api/v1/playx/verify-token`
|
||||
* 路径:`/api/v1/mall/verifyToken`
|
||||
|
||||
#### 配置:本地验证 vs 远程 PlayX
|
||||
|
||||
@@ -442,7 +442,7 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
|
||||
#### 示例(本地验证)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'token=上一步TemLogin返回的token'
|
||||
```
|
||||
@@ -455,32 +455,32 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
|
||||
#### 3.x.1 地址列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/address/list`
|
||||
* 路径:`/api/v1/mall/addressList`
|
||||
|
||||
返回:`data.list` 为地址数组。
|
||||
|
||||
#### 3.x.2 添加地址
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/address/add`
|
||||
* 路径:`/api/v1/mall/addressAdd`
|
||||
|
||||
Body:
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `receiver_name` | 是 | 收货人 |
|
||||
| `phone` | 是 | 电话 |
|
||||
| `region` | 是 | 地区(数组或逗号分隔字符串) |
|
||||
| `detail_address` | 是 | 详细地址 |
|
||||
| `address` | 是 | 地址补充 |
|
||||
| `default_setting` | 否 | `1` 设为默认地址 |
|
||||
|
||||
#### 3.x.3 修改地址(含设为默认)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/address/edit`
|
||||
* 路径:`/api/v1/mall/addressEdit`
|
||||
|
||||
Body:`id` 必填,其余字段按需传入更新。
|
||||
|
||||
#### 3.x.4 删除地址
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/address/delete`
|
||||
* 路径:`/api/v1/mall/addressDelete`
|
||||
|
||||
Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。
|
||||
|
||||
@@ -519,7 +519,7 @@ Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩
|
||||
|
||||
### 3.4 用户资产(Assets)
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/assets`
|
||||
* 路径:`/api/v1/mall/assets`
|
||||
|
||||
#### 请求参数(鉴权)
|
||||
|
||||
@@ -541,7 +541,7 @@ Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
```
|
||||
|
||||
响应(示例):
|
||||
@@ -564,7 +564,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上
|
||||
|
||||
### 3.5 领取(Claim)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/claim`
|
||||
* 路径:`/api/v1/mall/claim`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
@@ -581,7 +581,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'claim_request_id=claim_001' \
|
||||
--data-urlencode 'token=上一步temLogin返回的token'
|
||||
@@ -623,7 +623,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
|
||||
### 3.6 商品列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/items`
|
||||
* 路径:`/api/v1/mall/items`
|
||||
|
||||
#### 请求参数
|
||||
* `type`(可选):`BONUS` | `PHYSICAL` | `WITHDRAW`
|
||||
@@ -636,7 +636,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
#### 示例
|
||||
请求:
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDRAW'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=WITHDRAW'
|
||||
```
|
||||
|
||||
响应(示例):
|
||||
@@ -666,7 +666,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR
|
||||
|
||||
### 3.7 红利兑换(Bonus Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/bonus/redeem`
|
||||
* 路径:`/api/v1/mall/bonusRedeem`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
@@ -680,7 +680,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=123' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -703,14 +703,12 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
### 3.8 实物兑换(Physical Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/physical/redeem`
|
||||
* 路径:`/api/v1/mall/physicalRedeem`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
* `item_id`:商品 ID(要求 `mall_item.type=PHYSICAL` 且 `status=1`)
|
||||
* `receiver_name`:收货人
|
||||
* `receiver_phone`:收货电话
|
||||
* `receiver_address`:收货地址
|
||||
* `address_id`:收货地址 ID(`mall_address.id`,须属于当前用户资产;下单时写入 `mall_order.mall_address_id`,并将该地址快照写入 `receiver_name` / `receiver_phone` / `receiver_address`)
|
||||
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||
|
||||
#### 返回(成功)
|
||||
@@ -719,12 +717,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=200' \
|
||||
--data-urlencode 'receiver_name=张三' \
|
||||
--data-urlencode 'receiver_phone=18800001111' \
|
||||
--data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \
|
||||
--data-urlencode 'address_id=10' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
@@ -742,7 +738,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
### 3.9 提现申请(Withdraw Apply)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/withdraw/apply`
|
||||
* 路径:`/api/v1/mall/withdrawApply`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
@@ -756,7 +752,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=321' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -779,7 +775,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
|
||||
### 3.10 订单列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/orders`
|
||||
* 路径:`/api/v1/mall/orders`
|
||||
|
||||
#### 请求参数(鉴权)
|
||||
|
||||
@@ -793,7 +789,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
#### 示例
|
||||
请求:
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
```
|
||||
|
||||
响应(示例,简化):
|
||||
@@ -828,5 +824,5 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'token=上
|
||||
---
|
||||
|
||||
### 3.11 同步额度(可选)
|
||||
当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。
|
||||
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
将下列路径拼在积分商城对外域名之后,例如:
|
||||
|
||||
`https://{商城域名}/api/v1/playx/daily-push`
|
||||
`https://{商城域名}/api/v1/mall/dailyPush`
|
||||
|
||||
(联调时请向商城方索取正式环境与测试环境地址。)
|
||||
|
||||
@@ -78,7 +78,7 @@ sequenceDiagram
|
||||
participant M as 积分商城
|
||||
|
||||
Note over PX,M: 按约定配置 HMAC 密钥
|
||||
PX->>M: POST /api/v1/playx/daily-push(JSON Body)
|
||||
PX->>M: POST /api/v1/mall/dailyPush(JSON Body)
|
||||
M-->>PX: code=1, data.accepted / deduped
|
||||
```
|
||||
|
||||
@@ -88,14 +88,14 @@ sequenceDiagram
|
||||
|
||||
### 2.2 用户侧(H5 / 内嵌页)→ 商城:会话与业务接口
|
||||
|
||||
以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用,通过 **`session_id`**(先调 `verify-token` 获取)或 **`token`**(商城 `muser` 类 token)标识用户,**不一定由 PlayX 后端直接调用**:
|
||||
以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用,通过 **`session_id`**(先调 `verifyToken` 获取)或 **`token`**(商城 `muser` 类 token)标识用户,**不一定由 PlayX 后端直接调用**:
|
||||
|
||||
- `POST /api/v1/playx/verify-token`:用 PlayX token 换商城 `session_id`
|
||||
- `GET /api/v1/playx/assets`:查询资产
|
||||
- `POST /api/v1/playx/claim`:领取积分
|
||||
- `GET /api/v1/playx/items`:商品列表
|
||||
- `POST /api/v1/playx/bonus/redeem` / `physical/redeem` / `withdraw/apply`:兑换与提现申请
|
||||
- `GET /api/v1/playx/orders`:订单列表
|
||||
- `POST /api/v1/mall/verifyToken`:用 PlayX token 换商城 `session_id`
|
||||
- `GET /api/v1/mall/assets`:查询资产
|
||||
- `POST /api/v1/mall/claim`:领取积分
|
||||
- `GET /api/v1/mall/items`:商品列表
|
||||
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply`:兑换与提现申请
|
||||
- `GET /api/v1/mall/orders`:订单列表
|
||||
|
||||
若 PlayX 后端需要代替用户调用上述接口,须同样携带有效的 `session_id` 或 `token`,并遵守同一用户身份规则(见 **§4 身份说明**)。
|
||||
|
||||
@@ -112,7 +112,7 @@ sequenceDiagram
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 方法 | `POST` |
|
||||
| 路径 | `/api/v1/playx/daily-push` |
|
||||
| 路径 | `/api/v1/mall/dailyPush` |
|
||||
|
||||
### 3.2 鉴权(按商城部署配置,可组合)
|
||||
|
||||
@@ -133,7 +133,7 @@ sequenceDiagram
|
||||
签名原文与计算:
|
||||
|
||||
```
|
||||
canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body)
|
||||
canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)
|
||||
expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET )
|
||||
```
|
||||
|
||||
@@ -150,7 +150,7 @@ expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET )
|
||||
|
||||
### 3.3 Body 参数(JSON)
|
||||
|
||||
`/api/v1/playx/daily-push` 支持 **两种入参格式**(按字段自动识别):
|
||||
`/api/v1/mall/dailyPush` 支持 **两种入参格式**(按字段自动识别):
|
||||
|
||||
#### 格式 A:旧版单条上报(兼容)
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
@@ -215,7 +215,7 @@ expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET )
|
||||
### 3.6 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-Request-Id: req_1700000000_123456' \
|
||||
-H 'X-Timestamp: 1700000000' \
|
||||
@@ -279,7 +279,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
> 下列均为 **BuildAdmin 通用 `code/msg/time/data` 结构**;成功时 `code=1`。
|
||||
|
||||
### 5.1 `POST /api/v1/playx/verify-token`
|
||||
### 5.1 `POST /api/v1/mall/verifyToken`
|
||||
|
||||
用于将 **PlayX token**(或本地联调 token)换 **商城 `session_id`**。
|
||||
|
||||
@@ -300,7 +300,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
---
|
||||
|
||||
### 5.2 `GET /api/v1/playx/assets`
|
||||
### 5.2 `GET /api/v1/mall/assets`
|
||||
|
||||
查询积分资产;需 **§4** 身份。
|
||||
|
||||
@@ -316,7 +316,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
---
|
||||
|
||||
### 5.3 `POST /api/v1/playx/claim`
|
||||
### 5.3 `POST /api/v1/mall/claim`
|
||||
|
||||
领取积分;需 **§4** 身份。
|
||||
|
||||
@@ -328,7 +328,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
---
|
||||
|
||||
### 5.4 `GET /api/v1/playx/items`
|
||||
### 5.4 `GET /api/v1/mall/items`
|
||||
|
||||
商品列表。
|
||||
|
||||
@@ -340,7 +340,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
---
|
||||
|
||||
### 5.5 `POST /api/v1/playx/bonus/redeem`
|
||||
### 5.5 `POST /api/v1/mall/bonusRedeem`
|
||||
|
||||
红利兑换;需 **§4** 身份。
|
||||
|
||||
@@ -352,20 +352,18 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
---
|
||||
|
||||
### 5.6 `POST /api/v1/playx/physical/redeem`
|
||||
### 5.6 `POST /api/v1/mall/physicalRedeem`
|
||||
|
||||
实物兑换;需 **§4** 身份。
|
||||
|
||||
| 参数 | 必填 |
|
||||
|------|------|
|
||||
| `item_id` | 是 |
|
||||
| `receiver_name` | 是 |
|
||||
| `receiver_phone` | 是 |
|
||||
| `receiver_address` | 是 |
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `item_id` | 是 | 实物商品 ID |
|
||||
| `address_id` | 是 | `mall_address.id`(当前用户下地址);订单保存 `mall_address_id` 与地址快照 |
|
||||
|
||||
---
|
||||
|
||||
### 5.7 `POST /api/v1/playx/withdraw/apply`
|
||||
### 5.7 `POST /api/v1/mall/withdrawApply`
|
||||
|
||||
提现类兑换申请;需 **§4** 身份。
|
||||
|
||||
@@ -375,7 +373,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
---
|
||||
|
||||
### 5.8 `GET /api/v1/playx/orders`
|
||||
### 5.8 `GET /api/v1/mall/orders`
|
||||
|
||||
订单列表;需 **§4** 身份。
|
||||
|
||||
@@ -389,40 +387,40 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \
|
||||
|
||||
#### 5.9.1 获取收货地址列表
|
||||
- **方法**:`GET`
|
||||
- **路径**:`/api/v1/playx/address/list`
|
||||
- **路径**:`/api/v1/mall/addressList`
|
||||
|
||||
返回 `data.list`:地址数组(按 `default_setting` 优先,其次 id 倒序)。
|
||||
|
||||
#### 5.9.2 添加收货地址
|
||||
- **方法**:`POST`
|
||||
- **路径**:`/api/v1/playx/address/add`
|
||||
- **路径**:`/api/v1/mall/addressAdd`
|
||||
|
||||
Body(表单或 JSON 均可,建议 JSON):
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `receiver_name` | 是 | 收货人 |
|
||||
| `phone` | 是 | 联系电话 |
|
||||
| `region` | 是 | 地区(可传数组或逗号分隔字符串) |
|
||||
| `detail_address` | 是 | 详细地址(短文本) |
|
||||
| `address` | 是 | 地址补充(可长文本) |
|
||||
| `default_setting` | 否 | `1` 设为默认地址;`0` 或不传为非默认 |
|
||||
|
||||
成功返回:`data.id` 为新地址 id。
|
||||
|
||||
#### 5.9.3 修改收货地址(含设置默认)
|
||||
- **方法**:`POST`
|
||||
- **路径**:`/api/v1/playx/address/edit`
|
||||
- **路径**:`/api/v1/mall/addressEdit`
|
||||
|
||||
Body:
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | 是 | 地址 id |
|
||||
| `phone/region/detail_address/address/default_setting` | 否 | 需要修改的字段(只更新传入项) |
|
||||
| `receiver_name/phone/region/detail_address/default_setting` | 否 | 需要修改的字段(只更新传入项) |
|
||||
|
||||
当 `default_setting=1`:会自动把该用户其他地址的 `default_setting` 置为 0。
|
||||
|
||||
#### 5.9.4 删除收货地址
|
||||
- **方法**:`POST`
|
||||
- **路径**:`/api/v1/playx/address/delete`
|
||||
- **路径**:`/api/v1/mall/addressDelete`
|
||||
|
||||
Body:
|
||||
| 字段 | 必填 | 说明 |
|
||||
@@ -438,7 +436,7 @@ Body:
|
||||
| 环境变量 / 配置 | 作用 |
|
||||
|-----------------|------|
|
||||
| `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 |
|
||||
| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verify-token 不请求 PlayX 远程 |
|
||||
| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 PlayX 远程 |
|
||||
| `PLAYX_API_BASE_URL` | 商城调用 PlayX 接口时使用(与「PlayX 调商城」方向相反) |
|
||||
|
||||
---
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
接收 PlayX 每日 T+1 数据推送。
|
||||
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/daily-push`
|
||||
* 路径:`/api/v1/mall/dailyPush`
|
||||
|
||||
##### 请求(Header)
|
||||
当配置了 `playx.daily_push_secret`(Daily Push 签名校验)时,需要携带:
|
||||
@@ -20,7 +20,7 @@
|
||||
* `X-Signature`:签名(HMAC_SHA256)
|
||||
|
||||
签名计算逻辑(服务端):
|
||||
* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/playx/daily-push\n{sha256(json_body)}`
|
||||
* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/mall/dailyPush\n{sha256(json_body)}`
|
||||
* expected:`hash_hmac('sha256', canonical, daily_push_secret)`
|
||||
|
||||
##### 请求(Body)
|
||||
@@ -51,7 +51,7 @@
|
||||
##### 示例
|
||||
无签名校验(`PLAYX_DAILY_PUSH_SECRET` 为空):
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"request_id":"req_1001",
|
||||
@@ -246,7 +246,7 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \
|
||||
### 1.3 商城内部 API(供 H5 前端调用)
|
||||
#### Token 验证
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/verify-token`
|
||||
* 路径:`/api/v1/mall/verifyToken`
|
||||
|
||||
请求(Body):
|
||||
* `token`(必填,优先读取)
|
||||
@@ -260,14 +260,14 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \
|
||||
|
||||
示例:
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'token=PLAYX_TOKEN_XXX'
|
||||
```
|
||||
|
||||
#### 用户资产
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/assets`
|
||||
* 路径:`/api/v1/mall/assets`
|
||||
|
||||
请求参数(二选一):
|
||||
* `session_id`(优先):从 `mall_playx_session` 查 user_id(并校验过期)
|
||||
@@ -282,7 +282,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/mall/assets' --data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -301,7 +301,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
||||
|
||||
#### 领取(Claim)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/claim`
|
||||
* 路径:`/api/v1/mall/claim`
|
||||
|
||||
请求:
|
||||
* `claim_request_id`:幂等键(string,必填且唯一)
|
||||
@@ -312,7 +312,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
||||
##### 示例
|
||||
(首次领取成功,可能返回 `msg=Claim success`;若幂等重复,`msg` 可能为空)
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'claim_request_id=claim_001' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -334,7 +334,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
|
||||
#### 商品列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/items`
|
||||
* 路径:`/api/v1/mall/items`
|
||||
|
||||
请求(可选):
|
||||
* `type=BONUS|PHYSICAL|WITHDRAW`
|
||||
@@ -344,7 +344,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=BONUS'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -370,7 +370,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
||||
|
||||
#### 红利兑换(Bonus Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/bonus/redeem`
|
||||
* 路径:`/api/v1/mall/bonusRedeem`
|
||||
|
||||
请求:
|
||||
* `item_id`:商品 ID(BONUS)
|
||||
@@ -382,7 +382,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=123' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -401,13 +401,11 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
#### 实物兑换(Physical Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/physical/redeem`
|
||||
* 路径:`/api/v1/mall/physicalRedeem`
|
||||
|
||||
请求:
|
||||
* `item_id`:商品 ID(PHYSICAL)
|
||||
* `receiver_name`
|
||||
* `receiver_phone`
|
||||
* `receiver_address`
|
||||
* `address_id`:`mall_address.id`(当前用户资产下地址;订单写入 `mall_address_id` 与收货快照)
|
||||
* 鉴权:`session_id` 或 `user_id`
|
||||
|
||||
成功返回:
|
||||
@@ -416,12 +414,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=200' \
|
||||
--data-urlencode 'receiver_name=张三' \
|
||||
--data-urlencode 'receiver_phone=18800001111' \
|
||||
--data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \
|
||||
--data-urlencode 'address_id=10' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
@@ -435,7 +431,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
#### 提现申请(Withdraw Apply)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/withdraw/apply`
|
||||
* 路径:`/api/v1/mall/withdrawApply`
|
||||
|
||||
请求:
|
||||
* `item_id`:商品 ID(WITHDRAW)
|
||||
@@ -447,7 +443,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=321' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -466,7 +462,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
|
||||
#### 订单列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/orders`
|
||||
* 路径:`/api/v1/mall/orders`
|
||||
|
||||
请求:
|
||||
* `session_id` 或 `user_id`
|
||||
@@ -476,7 +472,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id=7b1c....'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -502,7 +498,7 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
||||
```
|
||||
|
||||
#### 同步额度
|
||||
当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。
|
||||
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||
如需补齐,请在接口设计阶段新增对应实现与 PlayX API 对接。
|
||||
|
||||
---
|
||||
@@ -607,29 +603,30 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
||||
|
||||
**表名**:`mall_playx_order`(或统一改造 mall_pints_order / mall_redemption_order)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 主键 |
|
||||
| user_id | varchar(64) | 用户 ID |
|
||||
| type | enum | BONUS / PHYSICAL / WITHDRAW |
|
||||
| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED |
|
||||
| mall_item_id | int | 商品 ID |
|
||||
| points_cost | int | 消耗积分 |
|
||||
| amount | decimal(15,2) | 现金面值(红利/提现) |
|
||||
| multiplier | int | 流水倍数 |
|
||||
| external_transaction_id | varchar(64) | 外部交易幂等键 |
|
||||
| playx_transaction_id | varchar(64) | PlayX 流水号 |
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|----------------------------------------------------------------------|
|
||||
| id | int | 主键 |
|
||||
| user_id | varchar(64) | 用户 ID |
|
||||
| type | enum | BONUS / PHYSICAL / WITHDRAW |
|
||||
| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED |
|
||||
| mall_item_id | int | 商品 ID |
|
||||
| points_cost | int | 消耗积分 |
|
||||
| amount | decimal(15,2) | 现金面值(红利/提现) |
|
||||
| multiplier | int | 流水倍数 |
|
||||
| external_transaction_id | varchar(64) | 订单号 |
|
||||
| playx_transaction_id | varchar(64) | PlayX 流水号 |
|
||||
| grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL |
|
||||
| fail_reason | text | 失败原因 |
|
||||
| retry_count | int | 重试次数 |
|
||||
| reject_reason | varchar(255) | 驳回原因(实物) |
|
||||
| shipping_company | varchar(50) | 物流公司 |
|
||||
| shipping_no | varchar(64) | 物流单号 |
|
||||
| receiver_name | varchar(50) | 收货人 |
|
||||
| receiver_phone | varchar(20) | 收货电话 |
|
||||
| receiver_address | text | 收货地址 |
|
||||
| create_time | bigint | 创建时间 |
|
||||
| update_time | bigint | 更新时间 |
|
||||
| fail_reason | text | 失败原因 |
|
||||
| retry_count | int | 重试次数 |
|
||||
| reject_reason | varchar(255) | 驳回原因(实物) |
|
||||
| shipping_company | varchar(50) | 物流公司 |
|
||||
| shipping_no | varchar(64) | 物流单号 |
|
||||
| mall_address_id | int unsigned, NULL | 实物兑换所选 `mall_address.id`(快照仍见 `receiver_*`) |
|
||||
| receiver_name | varchar(50) | 收货人 |
|
||||
| receiver_phone | varchar(20) | 收货电话 |
|
||||
| receiver_address | text | 收货地址 |
|
||||
| create_time | bigint | 创建时间 |
|
||||
| update_time | bigint | 更新时间 |
|
||||
|
||||
**索引**:`user_id`、`external_transaction_id`、`type`、`status`
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ export default {
|
||||
id: 'id',
|
||||
playx_user_asset_id: 'PlayX user asset',
|
||||
playxuserasset__username: 'username',
|
||||
receiver_name: 'receiver name',
|
||||
phone: 'phone',
|
||||
region: 'region',
|
||||
detail_address: 'detail_address',
|
||||
address: 'address',
|
||||
default_setting: 'Default address',
|
||||
'default_setting 0': '--',
|
||||
'default_setting 1': 'YES',
|
||||
|
||||
@@ -30,6 +30,7 @@ export default {
|
||||
receiver_name: 'receiver_name',
|
||||
receiver_phone: 'receiver_phone',
|
||||
receiver_address: 'receiver_address',
|
||||
mall_address_id: 'mall_address_id',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'ID',
|
||||
|
||||
@@ -2,10 +2,10 @@ export default {
|
||||
id: 'ID',
|
||||
playx_user_asset_id: '用户资产',
|
||||
playxuserasset__username: '用户名',
|
||||
receiver_name: '收货人',
|
||||
phone: '电话',
|
||||
region: '地区',
|
||||
detail_address: '详细地址',
|
||||
address: '地址',
|
||||
default_setting: '默认地址',
|
||||
'default_setting 0': '--',
|
||||
'default_setting 1': '是',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
claim_request_id: '领取幂等键',
|
||||
claim_request_id: '领取订单号',
|
||||
user_id: '用户ID',
|
||||
claimed_amount: '领取积分',
|
||||
create_time: '创建时间',
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
points_cost: '消耗积分',
|
||||
amount: '现金面值',
|
||||
multiplier: '流水倍数',
|
||||
external_transaction_id: '外部交易幂等键',
|
||||
external_transaction_id: '订单号',
|
||||
playx_transaction_id: 'PlayX流水号',
|
||||
grant_status: '发放子状态',
|
||||
'grant_status NOT_SENT': '未发送',
|
||||
@@ -30,6 +30,7 @@ export default {
|
||||
receiver_name: '收货人',
|
||||
receiver_phone: '收货电话',
|
||||
receiver_address: '收货地址',
|
||||
mall_address_id: '地址ID',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
claim_request_id: '领取幂等键',
|
||||
claim_request_id: '领取订单号',
|
||||
user_id: '用户ID',
|
||||
claimed_amount: '领取积分',
|
||||
create_time: '创建时间',
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
points_cost: '消耗积分',
|
||||
amount: '现金面值',
|
||||
multiplier: '流水倍数',
|
||||
external_transaction_id: '外部交易幂等键',
|
||||
external_transaction_id: '订单号',
|
||||
playx_transaction_id: 'PlayX流水号',
|
||||
grant_status: '发放子状态',
|
||||
'grant_status NOT_SENT': '未发送',
|
||||
|
||||
@@ -37,6 +37,17 @@ const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
const hasChinese = (s: string) => /[\u4e00-\u9fa5]/.test(s)
|
||||
|
||||
const formatRegion = (raw: string) => {
|
||||
const s = raw.toString().trim()
|
||||
if (!s) return ''
|
||||
if (hasChinese(s)) {
|
||||
return s.replace(/[,,\s]+/g, ',')
|
||||
}
|
||||
return s.replace(/[,,\s]+/g, ',')
|
||||
}
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
@@ -53,10 +64,19 @@ const baTable = new baTableClass(
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
showOverflowTooltip: true,
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('mall.address.receiver_name'),
|
||||
prop: 'receiver_name',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.address.phone'),
|
||||
prop: 'phone',
|
||||
@@ -65,7 +85,17 @@ const baTable = new baTableClass(
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('mall.address.region'), prop: 'region_text', align: 'center', operator: false },
|
||||
{
|
||||
label: t('mall.address.region'),
|
||||
prop: 'region_text',
|
||||
align: 'center',
|
||||
operator: false,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: TableRow, _column: TableColumn, cellValue: string) => {
|
||||
const raw = (cellValue || row.region || '').toString()
|
||||
return formatRegion(raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('mall.address.detail_address'),
|
||||
prop: 'detail_address',
|
||||
@@ -107,7 +137,7 @@ const baTable = new baTableClass(
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
||||
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'default_setting'],
|
||||
},
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
:input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }"
|
||||
:placeholder="t('Please select field', { field: t('mall.address.playx_user_asset_id') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.receiver_name')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_name"
|
||||
prop="receiver_name"
|
||||
:placeholder="t('Please input field', { field: t('mall.address.receiver_name') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.phone')"
|
||||
type="string"
|
||||
@@ -58,16 +65,6 @@
|
||||
prop="detail_address"
|
||||
:placeholder="t('Please input field', { field: t('mall.address.detail_address') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.address')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.address"
|
||||
prop="address"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||
:placeholder="t('Please input field', { field: t('mall.address.address') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.default_setting')"
|
||||
type="switch"
|
||||
|
||||
@@ -33,14 +33,73 @@ const baTable = new baTableClass(
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('mall.dailyPush.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{ label: t('mall.dailyPush.user_id'), prop: 'user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.dailyPush.date'), prop: 'date', align: 'center', render: 'date', operator: 'RANGE', comSearchRender: 'date', sortable: 'custom', width: 120, operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.dailyPush.username'), prop: 'username', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.dailyPush.yesterday_win_loss_net'), prop: 'yesterday_win_loss_net', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.dailyPush.yesterday_total_deposit'), prop: 'yesterday_total_deposit', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.dailyPush.lifetime_total_deposit'), prop: 'lifetime_total_deposit', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.dailyPush.lifetime_total_withdraw'), prop: 'lifetime_total_withdraw', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.dailyPush.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.dailyPush.user_id'),
|
||||
prop: 'user_id',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.date'),
|
||||
prop: 'date',
|
||||
align: 'center',
|
||||
render: 'date',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'date',
|
||||
sortable: 'custom',
|
||||
width: 120,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.username'),
|
||||
prop: 'username',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.yesterday_win_loss_net'),
|
||||
prop: 'yesterday_win_loss_net',
|
||||
align: 'center',
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.yesterday_total_deposit'),
|
||||
prop: 'yesterday_total_deposit',
|
||||
align: 'center',
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.lifetime_total_deposit'),
|
||||
prop: 'lifetime_total_deposit',
|
||||
align: 'center',
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.lifetime_total_withdraw'),
|
||||
prop: 'lifetime_total_withdraw',
|
||||
align: 'center',
|
||||
minWidth: 95,
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
@@ -62,4 +121,3 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ const baTable = new baTableClass(
|
||||
label: t('mall.item.description'),
|
||||
prop: 'description',
|
||||
align: 'center',
|
||||
minWidth: 80,
|
||||
// showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
@@ -60,6 +62,7 @@ const baTable = new baTableClass(
|
||||
label: t('mall.item.score'),
|
||||
prop: 'score',
|
||||
align: 'center',
|
||||
minWidth: 90,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
},
|
||||
@@ -173,7 +176,7 @@ const baTable = new baTableClass(
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
width: 80,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.order.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,7 +21,8 @@ import createAxios from '/@/utils/axios'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
|
||||
defineOptions({
|
||||
name: 'mall/order',
|
||||
@@ -27,6 +30,17 @@ defineOptions({
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn) =>
|
||||
btn.name === 'edit'
|
||||
? {
|
||||
...btn,
|
||||
title: '审核',
|
||||
type: 'primary',
|
||||
class: 'table-row-edit',
|
||||
icon: 'fa fa-check',
|
||||
}
|
||||
: btn
|
||||
)
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/mall.Order/'),
|
||||
@@ -35,11 +49,21 @@ const baTable = new baTableClass(
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('mall.order.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{ label: t('mall.order.user_id'), prop: 'user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{
|
||||
label: t('mall.order.user_id'),
|
||||
prop: 'user_id',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.type'),
|
||||
prop: 'type',
|
||||
align: 'center',
|
||||
effect: 'light',
|
||||
custom: { BONUS: 'success', PHYSICAL: 'primary', WITHDRAW: 'info' },
|
||||
minWidth: 140,
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'tag',
|
||||
@@ -53,6 +77,9 @@ const baTable = new baTableClass(
|
||||
label: t('mall.order.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
effect: 'dark',
|
||||
custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' },
|
||||
minWidth: 160,
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'tag',
|
||||
@@ -64,12 +91,36 @@ const baTable = new baTableClass(
|
||||
},
|
||||
},
|
||||
{ label: t('mall.order.mall_item_id'), prop: 'mall_item_id', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.mallitem__title'), prop: 'mallItem.title', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.order.points_cost'), prop: 'points_cost', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.amount'), prop: 'amount', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.multiplier'), prop: 'multiplier', align: 'center', operator: 'eq', sortable: false },
|
||||
{ label: t('mall.order.external_transaction_id'), prop: 'external_transaction_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.order.playx_transaction_id'), prop: 'playx_transaction_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{
|
||||
label: t('mall.order.mallitem__title'),
|
||||
prop: 'mallItem.title',
|
||||
align: 'center',
|
||||
minWidth: 90,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('mall.order.points_cost'), prop: 'points_cost', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.amount'), prop: 'amount', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.multiplier'), prop: 'multiplier', align: 'center', minWidth: 90, operator: 'eq', sortable: false },
|
||||
{
|
||||
label: t('mall.order.external_transaction_id'),
|
||||
prop: 'external_transaction_id',
|
||||
align: 'center',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.playx_transaction_id'),
|
||||
prop: 'playx_transaction_id',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.grant_status'),
|
||||
prop: 'grant_status',
|
||||
@@ -85,21 +136,100 @@ const baTable = new baTableClass(
|
||||
FAILED_FINAL: t('mall.order.grant_status FAILED_FINAL'),
|
||||
},
|
||||
},
|
||||
{ label: t('mall.order.fail_reason'), prop: 'fail_reason', align: 'center', showOverflowTooltip: true, operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.order.reject_reason'), prop: 'reject_reason', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.order.shipping_company'), prop: 'shipping_company', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.order.shipping_no'), prop: 'shipping_no', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.order.receiver_name'), prop: 'receiver_name', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.order.receiver_phone'), prop: 'receiver_phone', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.order.receiver_address'), prop: 'receiver_address', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
|
||||
{ label: t('mall.order.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.order.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.order.fail_reason'),
|
||||
prop: 'fail_reason',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.reject_reason'),
|
||||
prop: 'reject_reason',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.shipping_company'),
|
||||
prop: 'shipping_company',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.shipping_no'),
|
||||
prop: 'shipping_no',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.receiver_name'),
|
||||
prop: 'receiver_name',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.receiver_phone'),
|
||||
prop: 'receiver_phone',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.receiver_address'),
|
||||
prop: 'receiver_address',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{ label: t('mall.order.mall_address_id'), prop: 'mall_address_id', align: 'center', width: 100, operator: 'eq', sortable: false },
|
||||
{
|
||||
label: t('mall.order.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.order.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 220,
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
render: 'buttons',
|
||||
buttons: [
|
||||
...optButtons,
|
||||
{
|
||||
render: 'confirmButton',
|
||||
name: 'retry',
|
||||
@@ -131,81 +261,6 @@ const baTable = new baTableClass(
|
||||
await baTable.getData()
|
||||
},
|
||||
},
|
||||
{
|
||||
render: 'basicButton',
|
||||
name: 'ship',
|
||||
title: 'Ship',
|
||||
text: '发货',
|
||||
type: 'success',
|
||||
icon: '',
|
||||
display: (row: TableRow) => row.type === 'PHYSICAL' && row.status === 'PENDING',
|
||||
click: async (row: TableRow) => {
|
||||
try {
|
||||
const shippingNoRes = await ElMessageBox.prompt('请输入物流单号', '发货', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
const shippingCompanyRes = await ElMessageBox.prompt('请输入物流公司', '发货', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
|
||||
const shippingNo = shippingNoRes.value
|
||||
const shippingCompany = shippingCompanyRes.value
|
||||
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/mall.Order/ship',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: row.id,
|
||||
shipping_company: shippingCompany,
|
||||
shipping_no: shippingNo,
|
||||
},
|
||||
},
|
||||
{
|
||||
showSuccessMessage: true,
|
||||
}
|
||||
)
|
||||
await baTable.getData()
|
||||
} catch {
|
||||
// 用户取消弹窗:不做任何提示,避免控制台报错
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
render: 'basicButton',
|
||||
name: 'reject',
|
||||
title: 'Reject',
|
||||
text: '驳回',
|
||||
type: 'danger',
|
||||
icon: '',
|
||||
display: (row: TableRow) => row.type === 'PHYSICAL' && row.status === 'PENDING',
|
||||
click: async (row: TableRow) => {
|
||||
try {
|
||||
const res = await ElMessageBox.prompt('请输入驳回原因', '驳回', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/mall.Order/reject',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: row.id,
|
||||
reject_reason: res.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
showSuccessMessage: true,
|
||||
}
|
||||
)
|
||||
await baTable.getData()
|
||||
} catch {
|
||||
// 用户取消:不提示
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -229,4 +284,3 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
|
||||
290
web/src/views/backend/mall/order/popupForm.vue
Normal file
290
web/src/views/backend/mall/order/popupForm.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<!-- PENDING:两页审核流程(PHYSICAL 显示收货信息,其它类型只显示基本信息) -->
|
||||
<template v-if="usePagedActions && page === 1">
|
||||
<FormItem :label="t('mall.order.type')" type="string" v-model="baTable.form.items!.type" prop="type" :input-attr="{ disabled: true }" />
|
||||
<FormItem :label="t('mall.order.status')" type="string" v-model="baTable.form.items!.status" prop="status" :input-attr="{ disabled: true }" />
|
||||
<template v-if="isPhysical">
|
||||
<FormItem
|
||||
:label="t('mall.order.receiver_name')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_name"
|
||||
prop="receiver_name"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.receiver_phone')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_phone"
|
||||
prop="receiver_phone"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.receiver_address')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_address"
|
||||
prop="receiver_address"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="usePagedActions">
|
||||
<template v-if="action === 'approveShip'">
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_company')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_company"
|
||||
prop="shipping_company"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_no')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_no"
|
||||
prop="shipping_no"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="action === 'reject'">
|
||||
<FormItem
|
||||
:label="t('mall.order.reject_reason')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.reject_reason"
|
||||
prop="reject_reason"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 其它订单:保留常规可编辑表单 + 保存 -->
|
||||
<template v-else>
|
||||
<FormItem :label="t('mall.order.type')" type="string" v-model="baTable.form.items!.type" prop="type" :input-attr="{ disabled: true }" />
|
||||
<FormItem
|
||||
:label="t('mall.order.status')"
|
||||
type="select"
|
||||
v-model="baTable.form.items!.status"
|
||||
prop="status"
|
||||
:input-attr="{ content: { PENDING: t('mall.order.status PENDING'), COMPLETED: t('mall.order.status COMPLETED'), SHIPPED: t('mall.order.status SHIPPED'), REJECTED: t('mall.order.status REJECTED') } }"
|
||||
:placeholder="t('Please select field', { field: t('mall.order.status') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_company')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_company"
|
||||
prop="shipping_company"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_no')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_no"
|
||||
prop="shipping_no"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.reject_reason')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.reject_reason"
|
||||
prop="reject_reason"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
|
||||
/>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="onCancel">{{ t('Cancel') }}</el-button>
|
||||
|
||||
<template v-if="usePagedActions && page === 1">
|
||||
<el-button v-if="canApprove" @click="onApprove" type="success">审核通过</el-button>
|
||||
<el-button @click="goReject" type="danger">驳回</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="usePagedActions">
|
||||
<el-button @click="backToFirst">返回</el-button>
|
||||
<el-button
|
||||
v-if="action === 'approveShip'"
|
||||
v-blur
|
||||
:loading="submitting"
|
||||
@click="submitShip"
|
||||
type="success"
|
||||
>
|
||||
提交发货
|
||||
</el-button>
|
||||
<el-button v-if="action === 'reject'" v-blur :loading="submitting" @click="submitReject" type="danger">提交驳回</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const { t } = useI18n()
|
||||
|
||||
const isEdit = computed(() => baTable.form.operate === 'Edit')
|
||||
const isPending = computed(() => isEdit.value && baTable.form.items?.status === 'PENDING')
|
||||
const isPhysical = computed(() => baTable.form.items?.type === 'PHYSICAL')
|
||||
const canApprove = computed(() => isPending.value)
|
||||
const usePagedActions = computed(() => isPending.value)
|
||||
|
||||
const page = ref<1 | 2>(1)
|
||||
const action = ref<'approveShip' | 'reject' | null>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const resetPager = () => {
|
||||
page.value = 1
|
||||
action.value = null
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => baTable.form.operate,
|
||||
() => {
|
||||
resetPager()
|
||||
}
|
||||
)
|
||||
|
||||
const onCancel = () => {
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
}
|
||||
|
||||
const backToFirst = () => {
|
||||
page.value = 1
|
||||
action.value = null
|
||||
}
|
||||
|
||||
const onApprove = async () => {
|
||||
if (!isPending.value) {
|
||||
return
|
||||
}
|
||||
if (isPhysical.value) {
|
||||
page.value = 2
|
||||
action.value = 'approveShip'
|
||||
return
|
||||
}
|
||||
const id = baTable.form.items?.id
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios({ url: '/admin/mall.Order/approve', method: 'post', data: { id } }, { showSuccessMessage: true })
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goReject = () => {
|
||||
page.value = 2
|
||||
action.value = 'reject'
|
||||
}
|
||||
|
||||
const submitShip = async () => {
|
||||
const id = baTable.form.items?.id
|
||||
const shippingCompany = (baTable.form.items?.shipping_company || '').toString().trim()
|
||||
const shippingNo = (baTable.form.items?.shipping_no || '').toString().trim()
|
||||
if (!id || shippingCompany === '' || shippingNo === '') {
|
||||
ElMessage.error('请填写物流公司与物流单号')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios(
|
||||
{ url: '/admin/mall.Order/ship', method: 'post', data: { id, shipping_company: shippingCompany, shipping_no: shippingNo } },
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitReject = async () => {
|
||||
const id = baTable.form.items?.id
|
||||
const rejectReason = (baTable.form.items?.reject_reason || '').toString().trim()
|
||||
if (!id || rejectReason === '') {
|
||||
ElMessage.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios(
|
||||
{ url: '/admin/mall.Order/reject', method: 'post', data: { id, reject_reason: rejectReason } },
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
shipping_company: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_company') })],
|
||||
shipping_no: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_no') })],
|
||||
reject_reason: [buildValidatorData({ name: 'required', title: t('mall.order.reject_reason') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:buttons="['refresh', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.userAsset.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
@@ -25,6 +29,7 @@ defineOptions({
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/mall.UserAsset/'),
|
||||
@@ -33,16 +38,68 @@ const baTable = new baTableClass(
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('mall.userAsset.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
|
||||
{ label: t('mall.userAsset.username'), prop: 'username', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.userAsset.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{ label: t('mall.userAsset.playx_user_id'), prop: 'playx_user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{
|
||||
label: t('mall.userAsset.username'),
|
||||
prop: 'username',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.userAsset.phone'),
|
||||
prop: 'phone',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.userAsset.playx_user_id'),
|
||||
prop: 'playx_user_id',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('mall.userAsset.locked_points'), prop: 'locked_points', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.userAsset.available_points'), prop: 'available_points', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.userAsset.today_limit'), prop: 'today_limit', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.userAsset.today_claimed'), prop: 'today_claimed', align: 'center', operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.userAsset.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.userAsset.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.userAsset.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.userAsset.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.userAsset.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.userAsset.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', fixed: 'right', width: 80, render: 'buttons', buttons: optButtons, operator: false },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
@@ -64,4 +121,3 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
|
||||
124
web/src/views/backend/mall/userAsset/popupForm.vue
Normal file
124
web/src/views/backend/mall/userAsset/popupForm.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<FormItem :label="t('mall.userAsset.id')" type="number" v-model="baTable.form.items!.id" prop="id" :input-attr="{ disabled: true }" />
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.playx_user_id')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.playx_user_id"
|
||||
prop="playx_user_id"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.username')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.username"
|
||||
prop="username"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.username') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.phone')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.phone"
|
||||
prop="phone"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.phone') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.locked_points')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.locked_points"
|
||||
prop="locked_points"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.locked_points') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.available_points')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.available_points"
|
||||
prop="available_points"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.available_points') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.today_limit')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.today_limit"
|
||||
prop="today_limit"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.today_limit') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.today_claimed')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.today_claimed"
|
||||
prop="today_claimed"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.today_claimed') })"
|
||||
/>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('mall.userAsset.username') })],
|
||||
phone: [buildValidatorData({ name: 'required', title: t('mall.userAsset.phone') })],
|
||||
locked_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.locked_points') })],
|
||||
available_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.available_points') })],
|
||||
today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],
|
||||
today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
Reference in New Issue
Block a user