增加积分调整日志
This commit is contained in:
71
app/admin/controller/mall/PointsLog.php
Normal file
71
app/admin/controller/mall/PointsLog.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\controller\mall;
|
||||||
|
|
||||||
|
use app\admin\model\MallPointsLog;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\model\MallUserAsset;
|
||||||
|
use support\Response;
|
||||||
|
use Webman\Http\Request;
|
||||||
|
|
||||||
|
class PointsLog extends Backend
|
||||||
|
{
|
||||||
|
protected ?object $model = null;
|
||||||
|
|
||||||
|
protected array $withJoinTable = ['userAsset', 'admin'];
|
||||||
|
protected array|string $preExcludeFields = ['id', 'before', 'after', 'memo', 'admin_id', 'create_time'];
|
||||||
|
protected array|string $quickSearchField = [
|
||||||
|
'userAsset.playx_user_id',
|
||||||
|
'userAsset.username',
|
||||||
|
'userAsset.phone',
|
||||||
|
];
|
||||||
|
protected bool $autoFillAdminId = true;
|
||||||
|
|
||||||
|
protected function initController(Request $request): ?Response
|
||||||
|
{
|
||||||
|
$this->model = new MallPointsLog();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->method() === 'POST') {
|
||||||
|
return $this->_add();
|
||||||
|
}
|
||||||
|
|
||||||
|
$userAssetId = $request->get('userAssetId', $request->post('userAssetId', 0));
|
||||||
|
$userAsset = MallUserAsset::where('id', $userAssetId)->find();
|
||||||
|
if (!$userAsset) {
|
||||||
|
return $this->error(__('User asset not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', ['userAsset' => $userAsset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->error(__('Points log cannot be modified'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function del(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->error(__('Points log cannot be deleted'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,14 @@ class UserAsset extends Backend
|
|||||||
*/
|
*/
|
||||||
protected ?object $model = null;
|
protected ?object $model = null;
|
||||||
|
|
||||||
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time'];
|
protected array|string $preExcludeFields = [
|
||||||
|
'id',
|
||||||
|
'locked_points',
|
||||||
|
'available_points',
|
||||||
|
'today_claimed',
|
||||||
|
'create_time',
|
||||||
|
'update_time',
|
||||||
|
];
|
||||||
|
|
||||||
protected string|array $quickSearchField = [
|
protected string|array $quickSearchField = [
|
||||||
'playx_user_id',
|
'playx_user_id',
|
||||||
|
|||||||
13
app/admin/lang/en/mall/pointslog.php
Normal file
13
app/admin/lang/en/mall/pointslog.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'User asset' => 'User asset',
|
||||||
|
'Points adjustment' => 'Points adjustment',
|
||||||
|
'User asset not found' => 'User asset not found',
|
||||||
|
'Points adjustment must be a non-zero integer' => 'Points adjustment must be a non-zero integer',
|
||||||
|
'Insufficient available points' => 'Insufficient available points',
|
||||||
|
'Administrator added %s points' => 'Administrator added %s points',
|
||||||
|
'Administrator deducted %s points' => 'Administrator deducted %s points',
|
||||||
|
'Points log cannot be modified' => 'Points log cannot be modified',
|
||||||
|
'Points log cannot be deleted' => 'Points log cannot be deleted',
|
||||||
|
];
|
||||||
13
app/admin/lang/zh-cn/mall/pointslog.php
Normal file
13
app/admin/lang/zh-cn/mall/pointslog.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'User asset' => '用户资产',
|
||||||
|
'Points adjustment' => '调整积分',
|
||||||
|
'User asset not found' => '用户资产不存在',
|
||||||
|
'Points adjustment must be a non-zero integer' => '调整积分必须为非零整数',
|
||||||
|
'Insufficient available points' => '可用积分不足',
|
||||||
|
'Administrator added %s points' => '管理员增加积分 %s',
|
||||||
|
'Administrator deducted %s points' => '管理员扣除积分 %s',
|
||||||
|
'Points log cannot be modified' => '积分流水不允许修改',
|
||||||
|
'Points log cannot be deleted' => '积分流水不允许删除',
|
||||||
|
];
|
||||||
61
app/admin/model/MallPointsLog.php
Normal file
61
app/admin/model/MallPointsLog.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\model;
|
||||||
|
|
||||||
|
use app\common\model\MallUserAsset;
|
||||||
|
use think\model\relation\BelongsTo;
|
||||||
|
|
||||||
|
class MallPointsLog extends \app\common\model\MallPointsLog
|
||||||
|
{
|
||||||
|
public static function onBeforeInsert($model): void
|
||||||
|
{
|
||||||
|
$userAssetId = filter_var($model->user_asset_id, FILTER_VALIDATE_INT);
|
||||||
|
if ($userAssetId === false || $userAssetId <= 0) {
|
||||||
|
throw new \InvalidArgumentException(__('User asset not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$scoreValue = filter_var($model->score_value, FILTER_VALIDATE_INT);
|
||||||
|
if ($scoreValue === false || $scoreValue === 0) {
|
||||||
|
throw new \InvalidArgumentException(__('Points adjustment must be a non-zero integer'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$userAsset = MallUserAsset::where('id', $userAssetId)->lock(true)->find();
|
||||||
|
if (!$userAsset) {
|
||||||
|
throw new \RuntimeException(__('User asset not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = $userAsset->available_points;
|
||||||
|
$after = $before + $scoreValue;
|
||||||
|
if ($after < 0) {
|
||||||
|
throw new \RuntimeException(__('Insufficient available points'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$model->user_asset_id = $userAssetId;
|
||||||
|
$model->score_value = $scoreValue;
|
||||||
|
$model->before = $before;
|
||||||
|
$model->after = $after;
|
||||||
|
$model->memo = $scoreValue > 0
|
||||||
|
? __('Administrator added %s points', [abs($scoreValue)])
|
||||||
|
: __('Administrator deducted %s points', [abs($scoreValue)]);
|
||||||
|
|
||||||
|
$userAsset->available_points = $after;
|
||||||
|
$userAsset->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function onBeforeDelete(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userAsset(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(MallUserAsset::class, 'user_asset_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function admin(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Admin::class, 'admin_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/admin/validate/MallPointsLog.php
Normal file
30
app/admin/validate/MallPointsLog.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\validate;
|
||||||
|
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
class MallPointsLog extends Validate
|
||||||
|
{
|
||||||
|
protected $failException = true;
|
||||||
|
|
||||||
|
protected $rule = [
|
||||||
|
'user_asset_id' => 'require|integer|gt:0',
|
||||||
|
'score_value' => 'require|integer|notIn:0',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $scene = [
|
||||||
|
'add' => ['user_asset_id', 'score_value'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->field = [
|
||||||
|
'user_asset_id' => __('User asset'),
|
||||||
|
'score_value' => __('Points adjustment'),
|
||||||
|
];
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -847,7 +847,7 @@ class Playx extends Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 积分流水(领取/兑换/退回)
|
* 积分流水(领取/兑换/退回/管理员调整)
|
||||||
* GET /api/v1/mall/pointsLogs
|
* GET /api/v1/mall/pointsLogs
|
||||||
*
|
*
|
||||||
* 鉴权:token / session_id / user_id(同 assets/orders)
|
* 鉴权:token / session_id / user_id(同 assets/orders)
|
||||||
@@ -981,6 +981,28 @@ FROM (
|
|||||||
FROM mall_order o
|
FROM mall_order o
|
||||||
LEFT JOIN mall_item i ON i.id = o.mall_item_id
|
LEFT JOIN mall_item i ON i.id = o.mall_item_id
|
||||||
WHERE o.user_id = :user_id_refund AND o.status = 'REJECTED' AND o.points_cost > 0 AND o.update_time IS NOT NULL
|
WHERE o.user_id = :user_id_refund AND o.status = 'REJECTED' AND o.points_cost > 0 AND o.update_time IS NOT NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'ADMIN_ADJUST' AS biz_type,
|
||||||
|
CASE WHEN pl.score_value > 0 THEN 'IN' ELSE 'OUT' END AS direction,
|
||||||
|
ABS(pl.score_value) AS points,
|
||||||
|
pl.create_time AS ts,
|
||||||
|
pl.id AS ref_id,
|
||||||
|
'' AS order_no,
|
||||||
|
'' AS order_status,
|
||||||
|
0 AS item_id,
|
||||||
|
'' AS item_title,
|
||||||
|
0 AS item_type,
|
||||||
|
0 AS item_score,
|
||||||
|
0 AS item_amount,
|
||||||
|
0 AS item_multiplier,
|
||||||
|
'' AS item_category,
|
||||||
|
'' AS item_category_title,
|
||||||
|
CONCAT(LPAD(pl.create_time, 10, '0'), '_4_', LPAD(pl.id, 10, '0')) AS sort_key
|
||||||
|
FROM mall_points_log pl
|
||||||
|
WHERE pl.user_asset_id = :user_asset_id_adjust
|
||||||
) t
|
) t
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
SQL;
|
SQL;
|
||||||
@@ -989,6 +1011,7 @@ SQL;
|
|||||||
'user_id_claim' => $playxUserId,
|
'user_id_claim' => $playxUserId,
|
||||||
'user_id_redeem' => $playxUserId,
|
'user_id_redeem' => $playxUserId,
|
||||||
'user_id_refund' => $playxUserId,
|
'user_id_refund' => $playxUserId,
|
||||||
|
'user_asset_id_adjust' => $assetId,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($cursor !== '') {
|
if ($cursor !== '') {
|
||||||
@@ -1016,10 +1039,18 @@ SQL;
|
|||||||
if (!is_array($row)) {
|
if (!is_array($row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$memo = '';
|
||||||
|
if (($row['biz_type'] ?? '') === 'ADMIN_ADJUST') {
|
||||||
|
$memoKey = ($row['direction'] ?? '') === 'OUT'
|
||||||
|
? 'Administrator deducted %s points'
|
||||||
|
: 'Administrator added %s points';
|
||||||
|
$memo = __($memoKey, [$row['points'] ?? 0]);
|
||||||
|
}
|
||||||
$list[] = [
|
$list[] = [
|
||||||
'biz_type' => $row['biz_type'] ?? '',
|
'biz_type' => $row['biz_type'] ?? '',
|
||||||
'direction' => $row['direction'] ?? '',
|
'direction' => $row['direction'] ?? '',
|
||||||
'points' => $row['points'] ?? 0,
|
'points' => $row['points'] ?? 0,
|
||||||
|
'memo' => $memo,
|
||||||
'ts' => $row['ts'] ?? null,
|
'ts' => $row['ts'] ?? null,
|
||||||
'ref_id' => $row['ref_id'] ?? '',
|
'ref_id' => $row['ref_id'] ?? '',
|
||||||
'order_no' => $row['order_no'] ?? '',
|
'order_no' => $row['order_no'] ?? '',
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ return [
|
|||||||
'item_id and user_id/session_id required' => 'item_id and user_id/session_id/token are required',
|
'item_id and user_id/session_id required' => 'item_id and user_id/session_id/token are required',
|
||||||
'Item not found or not available' => 'Item not found or not available',
|
'Item not found or not available' => 'Item not found or not available',
|
||||||
'Insufficient points' => 'Insufficient points',
|
'Insufficient points' => 'Insufficient points',
|
||||||
|
'Administrator added %s points' => 'Administrator added %s points',
|
||||||
|
'Administrator deducted %s points' => 'Administrator deducted %s points',
|
||||||
'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes',
|
'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes',
|
||||||
'Missing required fields' => 'Missing required fields',
|
'Missing required fields' => 'Missing required fields',
|
||||||
'Out of stock' => 'Out of stock',
|
'Out of stock' => 'Out of stock',
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ return [
|
|||||||
'item_id and user_id/session_id required' => 'item_id dan user_id/session_id/token diperlukan',
|
'item_id and user_id/session_id required' => 'item_id dan user_id/session_id/token diperlukan',
|
||||||
'Item not found or not available' => 'Item tidak dijumpai atau tidak tersedia',
|
'Item not found or not available' => 'Item tidak dijumpai atau tidak tersedia',
|
||||||
'Insufficient points' => 'Mata tidak mencukupi',
|
'Insufficient points' => 'Mata tidak mencukupi',
|
||||||
|
'Administrator added %s points' => 'Pentadbir menambah %s mata',
|
||||||
|
'Administrator deducted %s points' => 'Pentadbir menolak %s mata',
|
||||||
'Redeem submitted, please wait about 10 minutes' => 'Penebusan dihantar, sila tunggu kira-kira 10 minit',
|
'Redeem submitted, please wait about 10 minutes' => 'Penebusan dihantar, sila tunggu kira-kira 10 minit',
|
||||||
'Missing required fields' => 'Medan wajib tiada',
|
'Missing required fields' => 'Medan wajib tiada',
|
||||||
'Out of stock' => 'Stok tidak mencukupi',
|
'Out of stock' => 'Stok tidak mencukupi',
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ return [
|
|||||||
'item_id and user_id/session_id required' => '缺少 item_id,或未提供有效的 user_id/session_id/token',
|
'item_id and user_id/session_id required' => '缺少 item_id,或未提供有效的 user_id/session_id/token',
|
||||||
'Item not found or not available' => '商品不存在或已下架',
|
'Item not found or not available' => '商品不存在或已下架',
|
||||||
'Insufficient points' => '积分不足',
|
'Insufficient points' => '积分不足',
|
||||||
|
'Administrator added %s points' => '管理员增加积分 %s',
|
||||||
|
'Administrator deducted %s points' => '管理员扣除积分 %s',
|
||||||
'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟',
|
'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟',
|
||||||
'Missing required fields' => '缺少必填字段',
|
'Missing required fields' => '缺少必填字段',
|
||||||
'Out of stock' => '库存不足',
|
'Out of stock' => '库存不足',
|
||||||
|
|||||||
18
app/common/model/MallPointsLog.php
Normal file
18
app/common/model/MallPointsLog.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use app\common\model\traits\TimestampInteger;
|
||||||
|
use support\think\Model;
|
||||||
|
|
||||||
|
class MallPointsLog extends Model
|
||||||
|
{
|
||||||
|
use TimestampInteger;
|
||||||
|
|
||||||
|
protected string $table = 'mall_points_log';
|
||||||
|
protected string $pk = 'id';
|
||||||
|
protected bool $autoWriteTimestamp = true;
|
||||||
|
protected bool $updateTime = false;
|
||||||
|
}
|
||||||
13
web/src/api/backend/mall/pointsLog.ts
Normal file
13
web/src/api/backend/mall/pointsLog.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import createAxios from '/@/utils/axios'
|
||||||
|
|
||||||
|
export const url = '/admin/mall.PointsLog/'
|
||||||
|
|
||||||
|
export function getUserAsset(userAssetId: string | number) {
|
||||||
|
return createAxios({
|
||||||
|
url: url + 'add',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
userAssetId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ export default {
|
|||||||
title: 'About this page',
|
title: 'About this page',
|
||||||
desc: 'History of users moving points from locked/pending to available, filterable by time and user.',
|
desc: 'History of users moving points from locked/pending to available, filterable by time and user.',
|
||||||
},
|
},
|
||||||
|
pointsLog: {
|
||||||
|
title: 'About this page',
|
||||||
|
desc: 'Records administrator additions and deductions to available points. Entries can only be added and viewed.',
|
||||||
|
},
|
||||||
item: {
|
item: {
|
||||||
title: 'About this page',
|
title: 'About this page',
|
||||||
desc: 'Manage catalog items, stock, and multilingual display text for the points mall.',
|
desc: 'Manage catalog items, stock, and multilingual display text for the points mall.',
|
||||||
|
|||||||
20
web/src/lang/backend/en/mall/pointsLog.ts
Normal file
20
web/src/lang/backend/en/mall/pointsLog.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
id: 'ID',
|
||||||
|
user_asset: 'User',
|
||||||
|
user_asset_id: 'User asset ID',
|
||||||
|
playx_user_id: 'PlayX ID',
|
||||||
|
username: 'Username',
|
||||||
|
available_points: 'Available points',
|
||||||
|
current_points: 'Current points',
|
||||||
|
score_value: 'Points adjustment',
|
||||||
|
before: 'Before',
|
||||||
|
after: 'After',
|
||||||
|
memo: 'Remark',
|
||||||
|
admin: 'Administrator',
|
||||||
|
create_time: 'Created at',
|
||||||
|
quick_search_fields: 'PlayX ID, username, phone',
|
||||||
|
non_zero_integer_required: 'Please enter a non-zero integer',
|
||||||
|
insufficient_available_points: 'Insufficient available points',
|
||||||
|
admin_add_memo: 'Administrator added {value} points',
|
||||||
|
admin_deduct_memo: 'Administrator deducted {value} points',
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
playx_user_id: 'playX_user_id',
|
playx_user_id: 'playX_user_id',
|
||||||
locked_points: 'locked_points',
|
locked_points: 'locked_points',
|
||||||
available_points: 'available_points',
|
available_points: 'available_points',
|
||||||
|
adjust_available_points: 'Adjust points',
|
||||||
today_limit: 'today_limit',
|
today_limit: 'today_limit',
|
||||||
today_claimed: 'today_claimed',
|
today_claimed: 'today_claimed',
|
||||||
today_limit_date: 'today_limit_date',
|
today_limit_date: 'today_limit_date',
|
||||||
@@ -12,4 +13,3 @@ export default {
|
|||||||
update_time: 'update_time',
|
update_time: 'update_time',
|
||||||
'quick Search Fields': 'id, playX_user_id, username, phone',
|
'quick Search Fields': 'id, playX_user_id, username, phone',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ export default {
|
|||||||
mall_order_approve: 'Approve',
|
mall_order_approve: 'Approve',
|
||||||
mall_dailyPush: 'Daily push',
|
mall_dailyPush: 'Daily push',
|
||||||
mall_claimLog: 'Claim log',
|
mall_claimLog: 'Claim log',
|
||||||
|
mall_pointsLog: 'Points management',
|
||||||
|
mall_pointsLog_index: 'Browse',
|
||||||
|
mall_pointsLog_add: 'Add',
|
||||||
mall_item: 'Products',
|
mall_item: 'Products',
|
||||||
mall_playxOrder: 'playX orders',
|
mall_playxOrder: 'playX orders',
|
||||||
mall_playxCenter: 'playX center',
|
mall_playxCenter: 'playX center',
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export default {
|
|||||||
title: '页面说明',
|
title: '页面说明',
|
||||||
desc: '用户从待领取积分划转至可用积分的操作流水,可按时间与用户追溯。',
|
desc: '用户从待领取积分划转至可用积分的操作流水,可按时间与用户追溯。',
|
||||||
},
|
},
|
||||||
|
pointsLog: {
|
||||||
|
title: '页面说明',
|
||||||
|
desc: '记录管理员对用户可用积分的人工增加与扣除;流水仅允许新增和查看。',
|
||||||
|
},
|
||||||
item: {
|
item: {
|
||||||
title: '页面说明',
|
title: '页面说明',
|
||||||
desc: '维护积分商城上架商品、库存与多语言展示信息。',
|
desc: '维护积分商城上架商品、库存与多语言展示信息。',
|
||||||
|
|||||||
20
web/src/lang/backend/zh-cn/mall/pointsLog.ts
Normal file
20
web/src/lang/backend/zh-cn/mall/pointsLog.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
id: 'ID',
|
||||||
|
user_asset: '用户',
|
||||||
|
user_asset_id: '用户资产ID',
|
||||||
|
playx_user_id: 'playX-ID',
|
||||||
|
username: '用户名',
|
||||||
|
available_points: '可用积分',
|
||||||
|
current_points: '当前积分',
|
||||||
|
score_value: '变更积分',
|
||||||
|
before: '调整前积分',
|
||||||
|
after: '调整后积分',
|
||||||
|
memo: '备注',
|
||||||
|
admin: '操作管理员',
|
||||||
|
create_time: '创建时间',
|
||||||
|
quick_search_fields: 'playX用户ID、用户名、手机号',
|
||||||
|
non_zero_integer_required: '请输入非零整数',
|
||||||
|
insufficient_available_points: '可用积分不足',
|
||||||
|
admin_add_memo: '管理员增加积分 {value}',
|
||||||
|
admin_deduct_memo: '管理员扣除积分 {value}',
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export default {
|
|||||||
playx_user_id: 'playX-ID',
|
playx_user_id: 'playX-ID',
|
||||||
locked_points: '待领取积分',
|
locked_points: '待领取积分',
|
||||||
available_points: '可用积分',
|
available_points: '可用积分',
|
||||||
|
adjust_available_points: '调整积分',
|
||||||
today_limit: '今日可领取上限',
|
today_limit: '今日可领取上限',
|
||||||
today_claimed: '今日已领取',
|
today_claimed: '今日已领取',
|
||||||
today_limit_date: '今日上限日期',
|
today_limit_date: '今日上限日期',
|
||||||
@@ -12,4 +13,3 @@ export default {
|
|||||||
update_time: '修改时间',
|
update_time: '修改时间',
|
||||||
'quick Search Fields': 'ID、playX用户ID、用户名、手机号',
|
'quick Search Fields': 'ID、playX用户ID、用户名、手机号',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ export default {
|
|||||||
mall_order_approve: '审核通过',
|
mall_order_approve: '审核通过',
|
||||||
mall_dailyPush: '每日推送',
|
mall_dailyPush: '每日推送',
|
||||||
mall_claimLog: '领取记录',
|
mall_claimLog: '领取记录',
|
||||||
|
mall_pointsLog: '积分管理',
|
||||||
|
mall_pointsLog_index: '查看',
|
||||||
|
mall_pointsLog_add: '新增',
|
||||||
mall_item: '商品管理',
|
mall_item: '商品管理',
|
||||||
mall_playxOrder: 'playX订单',
|
mall_playxOrder: 'playX订单',
|
||||||
mall_playxCenter: 'playX中心',
|
mall_playxCenter: 'playX中心',
|
||||||
|
|||||||
150
web/src/views/backend/mall/pointsLog/index.vue
Normal file
150
web/src/views/backend/mall/pointsLog/index.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="default-main ba-table-box">
|
||||||
|
<MallPageIntro page-key="pointsLog" />
|
||||||
|
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||||
|
|
||||||
|
<TableHeader
|
||||||
|
:buttons="['refresh', 'add', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||||
|
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.pointsLog.quick_search_fields') })"
|
||||||
|
>
|
||||||
|
<el-button v-if="state.userAsset.id" v-blur class="table-header-operate">
|
||||||
|
<span class="table-header-operate-text">
|
||||||
|
{{
|
||||||
|
state.userAsset.username +
|
||||||
|
' (ID:' +
|
||||||
|
state.userAsset.id +
|
||||||
|
') ' +
|
||||||
|
t('mall.pointsLog.available_points') +
|
||||||
|
': ' +
|
||||||
|
state.userAsset.available_points
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<Table />
|
||||||
|
<PopupForm />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { debounce } from 'lodash-es'
|
||||||
|
import { provide, reactive, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { getUserAsset, url } from '/@/api/backend/mall/pointsLog'
|
||||||
|
import { baTableApi } from '/@/api/common'
|
||||||
|
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
|
||||||
|
import TableHeader from '/@/components/table/header/index.vue'
|
||||||
|
import Table from '/@/components/table/index.vue'
|
||||||
|
import baTableClass from '/@/utils/baTable'
|
||||||
|
import PopupForm from './popupForm.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'mall/pointsLog',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface UserAssetInfo {
|
||||||
|
id?: number
|
||||||
|
username?: string
|
||||||
|
phone?: string
|
||||||
|
playx_user_id?: string
|
||||||
|
available_points?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const routeUserAssetId = route.query.user_asset_id
|
||||||
|
const defaultUserAssetId = typeof routeUserAssetId === 'string' ? routeUserAssetId : ''
|
||||||
|
const state = reactive<{ userAsset: UserAssetInfo }>({
|
||||||
|
userAsset: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const baTable = new baTableClass(
|
||||||
|
new baTableApi(url),
|
||||||
|
{
|
||||||
|
column: [
|
||||||
|
{ label: t('mall.pointsLog.id'), prop: 'id', align: 'center', operator: '=', width: 70 },
|
||||||
|
{ label: t('mall.pointsLog.user_asset_id'), prop: 'user_asset_id', align: 'center', operator: '=', width: 100 },
|
||||||
|
{
|
||||||
|
label: t('mall.pointsLog.playx_user_id'),
|
||||||
|
prop: 'userAsset.playx_user_id',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('mall.pointsLog.username'),
|
||||||
|
prop: 'userAsset.username',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
},
|
||||||
|
{ label: t('mall.pointsLog.score_value'), prop: 'score_value', align: 'center', operator: 'RANGE', sortable: 'custom' },
|
||||||
|
{ label: t('mall.pointsLog.before'), prop: 'before', align: 'center', operator: 'RANGE', sortable: 'custom' },
|
||||||
|
{ label: t('mall.pointsLog.after'), prop: 'after', align: 'center', operator: 'RANGE', sortable: 'custom' },
|
||||||
|
{
|
||||||
|
label: t('mall.pointsLog.memo'),
|
||||||
|
prop: 'memo',
|
||||||
|
align: 'center',
|
||||||
|
operator: 'LIKE',
|
||||||
|
operatorPlaceholder: t('Fuzzy query'),
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
},
|
||||||
|
{ label: t('mall.pointsLog.admin'), prop: 'admin.username', align: 'center', operator: 'LIKE' },
|
||||||
|
{
|
||||||
|
label: t('mall.pointsLog.create_time'),
|
||||||
|
prop: 'create_time',
|
||||||
|
align: 'center',
|
||||||
|
render: 'datetime',
|
||||||
|
sortable: 'custom',
|
||||||
|
operator: 'RANGE',
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dblClickNotEditColumn: ['all'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultItems: {
|
||||||
|
user_asset_id: defaultUserAssetId,
|
||||||
|
score_value: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const getUserAssetInfo = debounce((userAssetId: unknown) => {
|
||||||
|
if ((typeof userAssetId !== 'string' && typeof userAssetId !== 'number') || userAssetId === '') {
|
||||||
|
state.userAsset = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserAsset(userAssetId).then((response) => {
|
||||||
|
state.userAsset = response.data.userAsset
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
baTable.after.onSubmit = () => {
|
||||||
|
getUserAssetInfo(baTable.comSearch.form.user_asset_id)
|
||||||
|
}
|
||||||
|
baTable.after.onTableHeaderAction = ({ event }) => {
|
||||||
|
if (event == 'refresh') {
|
||||||
|
getUserAssetInfo(baTable.comSearch.form.user_asset_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baTable.mount()
|
||||||
|
baTable.getData()
|
||||||
|
provide('baTable', baTable)
|
||||||
|
|
||||||
|
getUserAssetInfo(baTable.comSearch.form.user_asset_id)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => baTable.comSearch.form.user_asset_id,
|
||||||
|
(newValue) => {
|
||||||
|
baTable.form.defaultItems!.user_asset_id = newValue
|
||||||
|
getUserAssetInfo(newValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
173
web/src/views/backend/mall/pointsLog/popupForm.vue
Normal file
173
web/src/views/backend/mall/pointsLog/popupForm.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="baTable.form.operate == 'Add'" @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 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"
|
||||||
|
:model="baTable.form.items"
|
||||||
|
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||||
|
:label-width="baTable.form.labelWidth + 'px'"
|
||||||
|
:rules="rules"
|
||||||
|
@keyup.enter="baTable.onSubmit(formRef)"
|
||||||
|
>
|
||||||
|
<FormItem
|
||||||
|
type="remoteSelect"
|
||||||
|
prop="user_asset_id"
|
||||||
|
:label="t('mall.pointsLog.user_asset')"
|
||||||
|
v-model="baTable.form.items!.user_asset_id"
|
||||||
|
:placeholder="t('Click select')"
|
||||||
|
:input-attr="{
|
||||||
|
pk: 'id',
|
||||||
|
field: 'username',
|
||||||
|
remoteUrl: '/admin/mall.UserAsset/select',
|
||||||
|
onChange: getUserAssetInfo,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<el-form-item :label="t('mall.pointsLog.playx_user_id')">
|
||||||
|
<el-input v-model="state.userAsset.playx_user_id" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('mall.pointsLog.username')">
|
||||||
|
<el-input v-model="state.userAsset.username" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('mall.pointsLog.current_points')">
|
||||||
|
<el-input v-model="state.userAsset.available_points" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="score_value" :label="t('mall.pointsLog.score_value')">
|
||||||
|
<el-input-number
|
||||||
|
v-model="baTable.form.items!.score_value"
|
||||||
|
:precision="0"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="changeScore"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('mall.pointsLog.after')">
|
||||||
|
<el-input v-model="state.after" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('mall.pointsLog.memo')">
|
||||||
|
<el-input v-model="state.memo" type="textarea" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</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" type="primary" @click="baTable.onSubmit(formRef)">
|
||||||
|
{{ t('Save') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormItemRule } from 'element-plus'
|
||||||
|
import { inject, reactive, useTemplateRef, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { getUserAsset } from '/@/api/backend/mall/pointsLog'
|
||||||
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
|
import { useConfig } from '/@/stores/config'
|
||||||
|
import type baTableClass from '/@/utils/baTable'
|
||||||
|
import { buildValidatorData } from '/@/utils/validate'
|
||||||
|
|
||||||
|
interface UserAssetInfo {
|
||||||
|
id?: number
|
||||||
|
username?: string
|
||||||
|
playx_user_id?: string
|
||||||
|
available_points?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = useConfig()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const injectedBaTable = inject<baTableClass>('baTable')
|
||||||
|
if (!injectedBaTable) {
|
||||||
|
throw new Error('baTable is not provided')
|
||||||
|
}
|
||||||
|
const baTable = injectedBaTable
|
||||||
|
const formRef = useTemplateRef('formRef')
|
||||||
|
|
||||||
|
const state = reactive<{
|
||||||
|
userAsset: UserAssetInfo
|
||||||
|
after: number
|
||||||
|
memo: string
|
||||||
|
}>({
|
||||||
|
userAsset: {},
|
||||||
|
after: 0,
|
||||||
|
memo: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateScoreValue: FormItemRule['validator'] = (_rule, value, callback) => {
|
||||||
|
if (typeof value !== 'number' || !Number.isInteger(value) || value === 0) {
|
||||||
|
callback(new Error(t('mall.pointsLog.non_zero_integer_required')))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ((state.userAsset.available_points ?? 0) + value < 0) {
|
||||||
|
callback(new Error(t('mall.pointsLog.insufficient_available_points')))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||||
|
user_asset_id: [buildValidatorData({ name: 'required', title: t('mall.pointsLog.user_asset') })],
|
||||||
|
score_value: [
|
||||||
|
{
|
||||||
|
validator: validateScoreValue,
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeScore = (value: number | undefined) => {
|
||||||
|
const scoreValue = value ?? 0
|
||||||
|
const availablePoints = state.userAsset.available_points ?? 0
|
||||||
|
state.after = availablePoints + scoreValue
|
||||||
|
if (scoreValue > 0) {
|
||||||
|
state.memo = t('mall.pointsLog.admin_add_memo', { value: Math.abs(scoreValue) })
|
||||||
|
} else if (scoreValue < 0) {
|
||||||
|
state.memo = t('mall.pointsLog.admin_deduct_memo', { value: Math.abs(scoreValue) })
|
||||||
|
} else {
|
||||||
|
state.memo = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserAssetInfo = () => {
|
||||||
|
const userAssetId = baTable.form.items!.user_asset_id
|
||||||
|
if ((typeof userAssetId !== 'string' && typeof userAssetId !== 'number') || userAssetId === '') {
|
||||||
|
state.userAsset = {}
|
||||||
|
state.after = 0
|
||||||
|
state.memo = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserAsset(userAssetId).then((response) => {
|
||||||
|
state.userAsset = response.data.userAsset
|
||||||
|
changeScore(baTable.form.items!.score_value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => baTable.form.operate,
|
||||||
|
(operate) => {
|
||||||
|
if (operate == 'Add') {
|
||||||
|
getUserAssetInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -27,7 +27,13 @@
|
|||||||
:label-width="baTable.form.labelWidth + 'px'"
|
:label-width="baTable.form.labelWidth + 'px'"
|
||||||
:rules="rules"
|
: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.id')"
|
||||||
|
type="number"
|
||||||
|
v-model="baTable.form.items!.id"
|
||||||
|
prop="id"
|
||||||
|
:input-attr="{ disabled: true }"
|
||||||
|
/>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.userAsset.playx_user_id')"
|
:label="t('mall.userAsset.playx_user_id')"
|
||||||
type="string"
|
type="string"
|
||||||
@@ -54,17 +60,15 @@
|
|||||||
type="number"
|
type="number"
|
||||||
v-model="baTable.form.items!.locked_points"
|
v-model="baTable.form.items!.locked_points"
|
||||||
prop="locked_points"
|
prop="locked_points"
|
||||||
:input-attr="{ step: 1, min: 0 }"
|
:input-attr="{ disabled: true }"
|
||||||
: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') })"
|
|
||||||
/>
|
/>
|
||||||
|
<el-form-item :label="t('mall.userAsset.available_points')">
|
||||||
|
<el-input v-model="baTable.form.items!.available_points" readonly>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="changeAvailablePoints">{{ t('mall.userAsset.adjust_available_points') }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
<FormItem
|
<FormItem
|
||||||
:label="t('mall.userAsset.today_limit')"
|
:label="t('mall.userAsset.today_limit')"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -78,8 +82,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
v-model="baTable.form.items!.today_claimed"
|
v-model="baTable.form.items!.today_claimed"
|
||||||
prop="today_claimed"
|
prop="today_claimed"
|
||||||
:input-attr="{ step: 1, min: 0 }"
|
:input-attr="{ disabled: true }"
|
||||||
:placeholder="t('Please input field', { field: t('mall.userAsset.today_claimed') })"
|
|
||||||
/>
|
/>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,8 +107,10 @@ import baTableClass from '/@/utils/baTable'
|
|||||||
import FormItem from '/@/components/formItem/index.vue'
|
import FormItem from '/@/components/formItem/index.vue'
|
||||||
import type { FormItemRule } from 'element-plus'
|
import type { FormItemRule } from 'element-plus'
|
||||||
import { buildValidatorData } from '/@/utils/validate'
|
import { buildValidatorData } from '/@/utils/validate'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
const router = useRouter()
|
||||||
const formRef = useTemplateRef('formRef')
|
const formRef = useTemplateRef('formRef')
|
||||||
const baTable = inject('baTable') as baTableClass
|
const baTable = inject('baTable') as baTableClass
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -118,7 +123,17 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
|||||||
today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],
|
today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],
|
||||||
today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })],
|
today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const changeAvailablePoints = () => {
|
||||||
|
const userAssetId = baTable.form.items!.id
|
||||||
|
baTable.toggleForm()
|
||||||
|
router.push({
|
||||||
|
name: 'mall/pointsLog',
|
||||||
|
query: {
|
||||||
|
user_asset_id: userAssetId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user