Compare commits

...

2 Commits

Author SHA1 Message Date
1cd5c3142d 优化页面和模型 2026-03-30 18:33:24 +08:00
2686c54781 优化首页和收货地址管理 2026-03-30 17:37:28 +08:00
40 changed files with 1226 additions and 637 deletions

View File

@@ -5,6 +5,10 @@ declare(strict_types=1);
namespace app\admin\controller;
use app\common\controller\Backend;
use app\common\model\MallClaimLog;
use app\common\model\MallOrder;
use app\common\model\MallUserAsset;
use support\think\Db;
use Webman\Http\Request;
use support\Response;
@@ -15,8 +19,78 @@ class Dashboard extends Backend
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
$now = time();
$todayStart = strtotime(date('Y-m-d', $now) . ' 00:00:00');
$yesterdayStart = $todayStart - 86400;
$newPlayersToday = MallUserAsset::where('create_time', '>=', $todayStart)
->where('create_time', '<=', $now)
->count();
$yesterdayPointsClaimed = MallClaimLog::where('create_time', '>=', $yesterdayStart)
->where('create_time', '<', $todayStart)
->sum('claimed_amount');
$yesterdayRedeemQuery = MallOrder::where('create_time', '>=', $yesterdayStart)
->where('create_time', '<', $todayStart);
$yesterdayRedeemCount = (clone $yesterdayRedeemQuery)->count();
$yesterdayRedeemPointsCostSum = (clone $yesterdayRedeemQuery)->sum('points_cost');
$yesterdayRedeemAmountSum = (clone $yesterdayRedeemQuery)->sum('amount');
$yesterdayRedeemCompletedCount = (clone $yesterdayRedeemQuery)
->where('status', MallOrder::STATUS_COMPLETED)
->count();
$yesterdayRedeemRejectedCount = (clone $yesterdayRedeemQuery)
->where('status', MallOrder::STATUS_REJECTED)
->count();
$yesterdayRedeemByItem = Db::name('mall_order')
->alias('o')
->leftJoin('mall_item i', 'i.id = o.mall_item_id')
->where('o.create_time', '>=', $yesterdayStart)
->where('o.create_time', '<', $todayStart)
->group('o.mall_item_id, i.title')
->field([
'o.mall_item_id',
'i.title',
Db::raw('COUNT(*) as order_count'),
Db::raw('SUM(o.points_cost) as points_cost_sum'),
Db::raw('SUM(o.amount) as amount_sum'),
Db::raw('SUM(CASE WHEN o.status = "COMPLETED" THEN 1 ELSE 0 END) as completed_count'),
Db::raw('SUM(CASE WHEN o.status = "REJECTED" THEN 1 ELSE 0 END) as rejected_count'),
])
->orderRaw('order_count DESC')
->select()
->toArray();
$pendingPhysicalToShip = MallOrder::where('type', MallOrder::TYPE_PHYSICAL)
->where('status', MallOrder::STATUS_PENDING)
->count();
$grantFailedRetryableCount = MallOrder::whereIn('type', [MallOrder::TYPE_BONUS, MallOrder::TYPE_WITHDRAW])
->where('grant_status', MallOrder::GRANT_FAILED_RETRYABLE)
->count();
return $this->success('', [
'remark' => get_route_remark()
'remark' => get_route_remark(),
'playx' => [
'time_range' => [
'today_start' => $todayStart,
'yesterday_start' => $yesterdayStart,
'now' => $now,
],
'new_players_today' => $newPlayersToday,
'yesterday_points_claimed' => $yesterdayPointsClaimed,
'yesterday_redeem' => [
'order_count' => $yesterdayRedeemCount,
'points_cost_sum' => $yesterdayRedeemPointsCostSum,
'amount_sum' => $yesterdayRedeemAmountSum,
'completed_count' => $yesterdayRedeemCompletedCount,
'rejected_count' => $yesterdayRedeemRejectedCount,
'by_item' => $yesterdayRedeemByItem,
],
'pending_physical_to_ship' => $pendingPhysicalToShip,
'grant_failed_retryable' => $grantFailedRetryableCount,
],
]);
}
}

View File

@@ -8,13 +8,13 @@ use support\Response;
use Webman\Http\Request;
/**
* PlayX 领取记录(后台列表)
* 领取记录(后台列表)
*/
class PlayxClaimLog extends Backend
class ClaimLog extends Backend
{
/**
* @var object|null
* @phpstan-var \app\common\model\MallPlayxClaimLog|null
* @phpstan-var \app\common\model\MallClaimLog|null
*/
protected ?object $model = null;
@@ -33,7 +33,7 @@ class PlayxClaimLog extends Backend
public function initialize(): void
{
parent::initialize();
$this->model = new \app\common\model\MallPlayxClaimLog();
$this->model = new \app\common\model\MallClaimLog();
}
/**

View File

@@ -8,13 +8,13 @@ use support\Response;
use Webman\Http\Request;
/**
* PlayX 每日推送数据(后台列表)
* 每日推送数据(后台列表)
*/
class PlayxDailyPush extends Backend
class DailyPush extends Backend
{
/**
* @var object|null
* @phpstan-var \app\common\model\MallPlayxDailyPush|null
* @phpstan-var \app\common\model\MallDailyPush|null
*/
protected ?object $model = null;
@@ -37,7 +37,7 @@ class PlayxDailyPush extends Backend
public function initialize(): void
{
parent::initialize();
$this->model = new \app\common\model\MallPlayxDailyPush();
$this->model = new \app\common\model\MallDailyPush();
}
/**

View File

@@ -4,20 +4,20 @@ namespace app\admin\controller\mall;
use Throwable;
use app\common\controller\Backend;
use app\common\model\MallPlayxOrder;
use app\common\model\MallPlayxUserAsset;
use app\common\model\MallOrder;
use app\common\model\MallUserAsset;
use support\think\Db;
use support\Response;
use Webman\Http\Request;
/**
* PlayX 统一订单(后台列表)
* 统一订单(后台列表)
*/
class PlayxOrder extends Backend
class Order extends Backend
{
/**
* @var object|null
* @phpstan-var \app\common\model\MallPlayxOrder|null
* @phpstan-var \app\common\model\MallOrder|null
*/
protected ?object $model = null;
@@ -53,7 +53,7 @@ class PlayxOrder extends Backend
public function initialize(): void
{
parent::initialize();
$this->model = new \app\common\model\MallPlayxOrder();
$this->model = new \app\common\model\MallOrder();
}
/**
@@ -71,7 +71,7 @@ class PlayxOrder extends Backend
return $this->select($request);
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
[$where, $alias, $limit, $order] = $this->queryBuilder();
$res = $this->model
->with(['mallItem' => function ($query) {
$query->field('id,title');
@@ -104,22 +104,22 @@ class PlayxOrder extends Backend
}
$data = $request->post();
$id = intval($data['id'] ?? 0);
$shippingCompany = strval($data['shipping_company'] ?? '');
$shippingNo = strval($data['shipping_no'] ?? '');
$id = $data['id'] ?? 0;
$shippingCompany = $data['shipping_company'] ?? '';
$shippingNo = $data['shipping_no'] ?? '';
if ($id <= 0 || $shippingCompany === '' || $shippingNo === '') {
if (!$id || $shippingCompany === '' || $shippingNo === '') {
return $this->error(__('Missing required fields'));
}
$order = MallPlayxOrder::where('id', $id)->find();
$order = MallOrder::where('id', $id)->find();
if (!$order) {
return $this->error(__('Record not found'));
}
if ($order->type !== MallPlayxOrder::TYPE_PHYSICAL) {
if ($order->type !== MallOrder::TYPE_PHYSICAL) {
return $this->error(__('Order type not PHYSICAL'));
}
if ($order->status !== MallPlayxOrder::STATUS_PENDING) {
if ($order->status !== MallOrder::STATUS_PENDING) {
return $this->error(__('Order status must be PENDING'));
}
@@ -127,7 +127,7 @@ class PlayxOrder extends Backend
try {
$order->shipping_company = $shippingCompany;
$order->shipping_no = $shippingNo;
$order->status = MallPlayxOrder::STATUS_SHIPPED;
$order->status = MallOrder::STATUS_SHIPPED;
$order->save();
Db::commit();
} catch (Throwable $e) {
@@ -153,40 +153,40 @@ class PlayxOrder extends Backend
}
$data = $request->post();
$id = intval($data['id'] ?? 0);
$rejectReason = strval($data['reject_reason'] ?? '');
$id = $data['id'] ?? 0;
$rejectReason = $data['reject_reason'] ?? '';
if ($id <= 0 || $rejectReason === '') {
if (!$id || $rejectReason === '') {
return $this->error(__('Missing required fields'));
}
$order = MallPlayxOrder::where('id', $id)->find();
$order = MallOrder::where('id', $id)->find();
if (!$order) {
return $this->error(__('Record not found'));
}
if ($order->type !== MallPlayxOrder::TYPE_PHYSICAL) {
if ($order->type !== MallOrder::TYPE_PHYSICAL) {
return $this->error(__('Order type not PHYSICAL'));
}
if ($order->status !== MallPlayxOrder::STATUS_PENDING) {
if ($order->status !== MallOrder::STATUS_PENDING) {
return $this->error(__('Order status must be PENDING'));
}
Db::startTrans();
try {
$asset = MallPlayxUserAsset::where('playx_user_id', strval($order->user_id ?? ''))->find();
$asset = MallUserAsset::where('playx_user_id', $order->user_id ?? '')->find();
if (!$asset) {
throw new \RuntimeException('User asset not found');
}
$refund = intval($order->points_cost ?? 0);
$refund = $order->points_cost ?? 0;
if ($refund > 0) {
$asset->available_points += $refund;
$asset->save();
}
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->status = MallOrder::STATUS_REJECTED;
$order->reject_reason = $rejectReason;
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->save();
Db::commit();
@@ -212,26 +212,26 @@ class PlayxOrder extends Backend
return $this->error(__('Parameter error'));
}
$id = intval($request->post('id', 0));
if ($id <= 0) {
$id = $request->post('id', 0);
if (!$id) {
return $this->error(__('Missing required fields'));
}
$order = MallPlayxOrder::where('id', $id)->find();
$order = MallOrder::where('id', $id)->find();
if (!$order) {
return $this->error(__('Record not found'));
}
if (!in_array($order->type, [MallPlayxOrder::TYPE_BONUS, MallPlayxOrder::TYPE_WITHDRAW], true)) {
if (!in_array($order->type, [MallOrder::TYPE_BONUS, MallOrder::TYPE_WITHDRAW], true)) {
return $this->error(__('Only BONUS/WITHDRAW can retry'));
}
if ($order->grant_status !== MallPlayxOrder::GRANT_FAILED_RETRYABLE) {
if ($order->grant_status !== MallOrder::GRANT_FAILED_RETRYABLE) {
return $this->error(__('Only FAILED_RETRYABLE can retry'));
}
if (intval($order->retry_count) >= 3) {
if (($order->retry_count ?? 0) >= 3) {
return $this->error(__('Retry count exceeded'));
}
$order->grant_status = MallPlayxOrder::GRANT_NOT_SENT;
$order->grant_status = MallOrder::GRANT_NOT_SENT;
$order->save();
return $this->success(__('Retry queued'));

View File

@@ -7,13 +7,13 @@ use support\Response;
use Webman\Http\Request;
/**
* PlayX 用户资产(后台列表)
* 用户资产(后台列表)
*/
class PlayxUserAsset extends Backend
class UserAsset extends Backend
{
/**
* @var object|null
* @phpstan-var \app\common\model\MallPlayxUserAsset|null
* @phpstan-var \app\common\model\MallUserAsset|null
*/
protected ?object $model = null;
@@ -42,7 +42,7 @@ class PlayxUserAsset extends Backend
public function initialize(): void
{
parent::initialize();
$this->model = new \app\common\model\MallPlayxUserAsset();
$this->model = new \app\common\model\MallUserAsset();
}
/**
@@ -55,7 +55,7 @@ class PlayxUserAsset extends Backend
return $response;
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
[$where, $alias, $limit, $order] = $this->queryBuilder();
$res = $this->model
->field('id,username')
->alias($alias)
@@ -67,8 +67,8 @@ class PlayxUserAsset extends Backend
foreach ($res->items() as $row) {
$arr = $row->toArray();
$list[] = [
'id' => intval($arr['id'] ?? 0),
'username' => strval($arr['username'] ?? ''),
'id' => $arr['id'] ?? 0,
'username' => $arr['username'] ?? '',
];
}
@@ -79,3 +79,4 @@ class PlayxUserAsset extends Backend
]);
}
}

View File

@@ -10,7 +10,7 @@ use app\common\controller\Api;
use app\common\facade\Token;
use app\common\library\Auth as UserAuth;
use app\common\library\AgentJwt;
use app\common\model\MallPlayxUserAsset;
use app\common\model\MallUserAsset;
use app\admin\model\Admin;
use Webman\Http\Request;
use support\Response;
@@ -100,7 +100,7 @@ class Auth extends Api
/**
* H5 临时登录GET/POST
* 参数username
* 写入或复用 mall_playx_user_asset签发 muser 类型 tokenuser_id 为资产表主键)
* 写入或复用 mall_user_asset签发 muser 类型 tokenuser_id 为资产表主键)
*/
public function temLogin(Request $request): Response
{
@@ -120,7 +120,7 @@ class Auth extends Api
}
try {
$asset = MallPlayxUserAsset::ensureForUsername($username);
$asset = MallUserAsset::ensureForUsername($username);
} catch (Throwable $e) {
return $this->error($e->getMessage());
}

View File

@@ -9,11 +9,12 @@ use app\common\controller\Api;
use app\common\facade\Token;
use app\common\library\Auth as UserAuth;
use app\common\model\MallItem;
use app\common\model\MallPlayxClaimLog;
use app\common\model\MallPlayxDailyPush;
use app\common\model\MallPlayxSession;
use app\common\model\MallPlayxOrder;
use app\common\model\MallPlayxUserAsset;
use app\common\model\MallClaimLog;
use app\common\model\MallDailyPush;
use app\common\model\MallSession;
use app\common\model\MallOrder;
use app\common\model\MallUserAsset;
use app\common\model\MallAddress;
use support\think\Db;
use Webman\Http\Request;
use support\Response;
@@ -24,17 +25,17 @@ use support\Response;
class Playx extends Api
{
/**
* 从请求解析 mall_playx_user_asset.idmuser token、session、user_id 均指向资产表主键或 playx_user_id
* 从请求解析 mall_user_asset.idmuser token、session、user_id 均指向资产表主键或 playx_user_id
*/
private function resolvePlayxAssetIdFromRequest(Request $request): ?int
{
$sessionId = strval($request->post('session_id', $request->get('session_id', '')));
if ($sessionId !== '') {
$session = MallPlayxSession::where('session_id', $sessionId)->find();
$session = MallSession::where('session_id', $sessionId)->find();
if ($session) {
$expireTime = intval($session->expire_time ?? 0);
if ($expireTime > time()) {
$asset = MallPlayxUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find();
$asset = MallUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find();
if ($asset) {
return intval($asset->getKey());
}
@@ -62,7 +63,7 @@ class Playx extends Api
return intval($userId);
}
$asset = MallPlayxUserAsset::where('playx_user_id', $userId)->find();
$asset = MallUserAsset::where('playx_user_id', $userId)->find();
if ($asset) {
return intval($asset->getKey());
}
@@ -90,7 +91,7 @@ class Playx extends Api
{
for ($i = 0; $i < 8; $i++) {
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
if (!MallPlayxUserAsset::where('phone', $candidate)->find()) {
if (!MallUserAsset::where('phone', $candidate)->find()) {
return $candidate;
}
}
@@ -98,9 +99,9 @@ class Playx extends Api
return null;
}
private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallPlayxUserAsset
private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallUserAsset
{
$asset = MallPlayxUserAsset::where('playx_user_id', $playxUserId)->find();
$asset = MallUserAsset::where('playx_user_id', $playxUserId)->find();
if ($asset) {
return $asset;
}
@@ -109,7 +110,7 @@ class Playx extends Api
if ($effectiveUsername === '') {
$effectiveUsername = 'playx_' . $playxUserId;
}
$byName = MallPlayxUserAsset::where('username', $effectiveUsername)->find();
$byName = MallUserAsset::where('username', $effectiveUsername)->find();
if ($byName) {
$byName->playx_user_id = $playxUserId;
$byName->save();
@@ -124,7 +125,7 @@ class Playx extends Api
$pwd = hash_password(Random::build('alnum', 16));
$now = time();
return MallPlayxUserAsset::create([
return MallUserAsset::create([
'playx_user_id' => $playxUserId,
'username' => $effectiveUsername,
'phone' => $phone,
@@ -140,9 +141,9 @@ class Playx extends Api
]);
}
private function getAssetById(int $assetId): ?MallPlayxUserAsset
private function getAssetById(int $assetId): ?MallUserAsset
{
return MallPlayxUserAsset::where('id', $assetId)->find();
return MallUserAsset::where('id', $assetId)->find();
}
/**
@@ -218,7 +219,7 @@ class Playx extends Api
$lifetimeTotalDeposit = $m['lty_deposit'] ?? 0;
$lifetimeTotalWithdraw = $m['lty_withdrawal'] ?? 0;
$exists = MallPlayxDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
if ($exists) {
$results[] = [
'user_id' => $playxUserId,
@@ -231,7 +232,7 @@ class Playx extends Api
Db::startTrans();
try {
MallPlayxDailyPush::create([
MallDailyPush::create([
'user_id' => $playxUserId,
'date' => $date,
'username' => $username,
@@ -303,7 +304,7 @@ class Playx extends Api
return $this->error(__('Missing required fields: request_id, date, user_id'));
}
$exists = MallPlayxDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
$exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
if ($exists) {
return $this->success('', [
'request_id' => $requestId,
@@ -315,7 +316,7 @@ class Playx extends Api
Db::startTrans();
try {
MallPlayxDailyPush::create([
MallDailyPush::create([
'user_id' => $playxUserId,
'date' => $date,
'username' => $body['username'] ?? '',
@@ -426,7 +427,7 @@ class Playx extends Api
}
$sessionId = bin2hex(random_bytes(16));
MallPlayxSession::create([
MallSession::create([
'session_id' => $sessionId,
'user_id' => $userId,
'username' => $username,
@@ -447,7 +448,7 @@ class Playx extends Api
}
/**
* 本地校验 temLogin 等写入的商城 token类型 muser写入 mall_playx_session
* 本地校验 temLogin 等写入的商城 token类型 muser写入 mall_session
*/
private function verifyTokenLocal(string $token): Response
{
@@ -464,7 +465,7 @@ class Playx extends Api
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$asset = MallPlayxUserAsset::where('id', $assetId)->find();
$asset = MallUserAsset::where('id', $assetId)->find();
if (!$asset) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
@@ -476,7 +477,7 @@ class Playx extends Api
$expireAt = time() + intval(config('playx.session_expire_seconds', 3600));
$sessionId = bin2hex(random_bytes(16));
MallPlayxSession::create([
MallSession::create([
'session_id' => $sessionId,
'user_id' => $playxUserId,
'username' => strval($asset->username ?? ''),
@@ -555,7 +556,7 @@ class Playx extends Api
}
$playxUserId = strval($asset->playx_user_id);
$exists = MallPlayxClaimLog::where('claim_request_id', $claimRequestId)->find();
$exists = MallClaimLog::where('claim_request_id', $claimRequestId)->find();
if ($exists) {
return $this->success('', $this->formatAsset($asset));
}
@@ -575,7 +576,7 @@ class Playx extends Api
Db::startTrans();
try {
MallPlayxClaimLog::create([
MallClaimLog::create([
'claim_request_id' => $claimRequestId,
'user_id' => $playxUserId,
'claimed_amount' => $canClaim,
@@ -666,7 +667,7 @@ class Playx extends Api
return $this->success('', ['list' => []]);
}
$list = MallPlayxOrder::where('user_id', strval($asset->playx_user_id))
$list = MallOrder::where('user_id', strval($asset->playx_user_id))
->with(['mallItem'])
->order('id', 'desc')
->limit(100)
@@ -675,7 +676,198 @@ class Playx extends Api
return $this->success('', ['list' => $list->toArray()]);
}
private function formatAsset(?MallPlayxUserAsset $asset): array
/**
* 收货地址列表
* GET /api/v1/playx/address/list?session_id=xxx
*/
public function addressList(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) {
return $response;
}
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$list = MallAddress::where('playx_user_asset_id', $assetId)
->order('default_setting', 'desc')
->order('id', 'desc')
->select();
return $this->success('', ['list' => $list->toArray()]);
}
/**
* 添加收货地址(可设置默认)
* POST /api/v1/playx/address/add
*/
public function addressAdd(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) {
return $response;
}
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$phone = trim(strval($request->post('phone', '')));
$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) {
return $this->error(__('Missing required fields'));
}
Db::startTrans();
try {
if ($defaultSetting === 1) {
MallAddress::where('playx_user_asset_id', $assetId)->update(['default_setting' => 0]);
}
$created = MallAddress::create([
'playx_user_asset_id' => $assetId,
'phone' => $phone,
'region' => $region,
'detail_address' => $detailAddress,
'address' => $address,
'default_setting' => $defaultSetting,
'create_time' => time(),
'update_time' => time(),
]);
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success('', [
'id' => $created ? $created->id : null,
]);
}
/**
* 修改收货地址(包含设置默认地址)
* POST /api/v1/playx/address/edit
*/
public function addressEdit(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) {
return $response;
}
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$id = intval($request->post('id', 0));
if ($id <= 0) {
return $this->error(__('Missing required fields'));
}
$row = MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->find();
if (!$row) {
return $this->error(__('Record not found'));
}
$updates = [];
if ($request->post('phone', null) !== null) {
$updates['phone'] = trim(strval($request->post('phone', '')));
}
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;
}
if (empty($updates)) {
return $this->success('', ['updated' => false]);
}
Db::startTrans();
try {
if (isset($updates['default_setting']) && $updates['default_setting'] === 1) {
MallAddress::where('playx_user_asset_id', $assetId)->update(['default_setting' => 0]);
}
$updates['update_time'] = time();
MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->update($updates);
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success('', ['updated' => true]);
}
/**
* 删除收货地址
* POST /api/v1/playx/address/delete
*/
public function addressDelete(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) {
return $response;
}
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$id = intval($request->post('id', 0));
if ($id <= 0) {
return $this->error(__('Missing required fields'));
}
$row = MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->find();
if (!$row) {
return $this->error(__('Record not found'));
}
$wasDefault = intval($row->default_setting ?? 0) === 1;
Db::startTrans();
try {
MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->delete();
if ($wasDefault) {
$fallback = MallAddress::where('playx_user_asset_id', $assetId)->order('id', 'desc')->find();
if ($fallback) {
$fallback->default_setting = 1;
$fallback->update_time = time();
$fallback->save();
}
}
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success('', ['deleted' => true]);
}
private function formatAsset(?MallUserAsset $asset): array
{
if (!$asset) {
return [
@@ -732,16 +924,16 @@ class Playx extends Api
$asset->save();
$orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
$order = MallPlayxOrder::create([
$order = MallOrder::create([
'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_BONUS,
'status' => MallPlayxOrder::STATUS_PENDING,
'type' => MallOrder::TYPE_BONUS,
'status' => MallOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
'points_cost' => $item->score,
'amount' => $amount,
'multiplier' => $multiplier,
'external_transaction_id' => $orderNo,
'grant_status' => MallPlayxOrder::GRANT_NOT_SENT,
'grant_status' => MallOrder::GRANT_NOT_SENT,
'create_time' => time(),
'update_time' => time(),
]);
@@ -798,10 +990,10 @@ class Playx extends Api
$asset->available_points -= $item->score;
$asset->save();
MallPlayxOrder::create([
MallOrder::create([
'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_PHYSICAL,
'status' => MallPlayxOrder::STATUS_PENDING,
'type' => MallOrder::TYPE_PHYSICAL,
'status' => MallOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
'points_cost' => $item->score,
'receiver_name' => $receiverName,
@@ -861,16 +1053,16 @@ class Playx extends Api
$asset->save();
$orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
$order = MallPlayxOrder::create([
$order = MallOrder::create([
'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_WITHDRAW,
'status' => MallPlayxOrder::STATUS_PENDING,
'type' => MallOrder::TYPE_WITHDRAW,
'status' => MallOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
'points_cost' => $item->score,
'amount' => $amount,
'multiplier' => $multiplier,
'external_transaction_id' => $orderNo,
'grant_status' => MallPlayxOrder::GRANT_NOT_SENT,
'grant_status' => MallOrder::GRANT_NOT_SENT,
'create_time' => time(),
'update_time' => time(),
]);
@@ -892,7 +1084,7 @@ class Playx extends Api
]);
}
private function callPlayxBonusGrant(MallPlayxOrder $order, MallItem $item, string $userId): void
private function callPlayxBonusGrant(MallOrder $order, MallItem $item, string $userId): void
{
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
$url = config('playx.api.bonus_grant_url', '/api/v1/bonus/grant');
@@ -917,21 +1109,21 @@ class Playx extends Api
$data = json_decode(strval($res->getBody()), true);
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->save();
} else {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->fail_reason = $data['message'] ?? 'unknown';
$order->save();
}
} catch (\Throwable $e) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->fail_reason = $e->getMessage();
$order->save();
}
}
private function callPlayxBalanceCredit(MallPlayxOrder $order, string $userId): void
private function callPlayxBalanceCredit(MallOrder $order, string $userId): void
{
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
$url = config('playx.api.balance_credit_url', '/api/v1/balance/credit');
@@ -953,15 +1145,15 @@ class Playx extends Api
$data = json_decode(strval($res->getBody()), true);
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->save();
} else {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->fail_reason = $data['message'] ?? 'unknown';
$order->save();
}
} catch (\Throwable $e) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->fail_reason = $e->getMessage();
$order->save();
}

View File

@@ -47,6 +47,6 @@ class MallAddress extends Model
public function playxUserAsset(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\common\model\MallPlayxUserAsset::class, 'playx_user_asset_id', 'id');
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
}
}

View File

@@ -7,14 +7,15 @@ namespace app\common\model;
use support\think\Model;
/**
* PlayX 领取记录(幂等)
* 领取记录(幂等)
*/
class MallPlayxClaimLog extends Model
class MallClaimLog extends Model
{
protected string $name = 'mall_playx_claim_log';
protected string $name = 'mall_claim_log';
protected array $type = [
'claimed_amount' => 'integer',
'create_time' => 'integer',
];
}

View File

@@ -7,11 +7,11 @@ namespace app\common\model;
use support\think\Model;
/**
* PlayX 每日推送数据
* 每日推送数据
*/
class MallPlayxDailyPush extends Model
class MallDailyPush extends Model
{
protected string $name = 'mall_playx_daily_push';
protected string $name = 'mall_daily_push';
protected array $type = [
'yesterday_win_loss_net' => 'float',
@@ -21,3 +21,4 @@ class MallPlayxDailyPush extends Model
'create_time' => 'integer',
];
}

View File

@@ -7,7 +7,7 @@ namespace app\common\model;
use support\think\Model;
/**
* PlayX 统一订单
* 统一订单
*
* @property int $id
* @property string $user_id
@@ -29,9 +29,9 @@ use support\think\Model;
* @property string $receiver_phone
* @property string|null $receiver_address
*/
class MallPlayxOrder extends Model
class MallOrder extends Model
{
protected string $name = 'mall_playx_order';
protected string $name = 'mall_order';
protected bool $autoWriteTimestamp = true;
@@ -51,12 +51,12 @@ class MallPlayxOrder extends Model
public const GRANT_FAILED_FINAL = 'FAILED_FINAL';
protected array $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'points_cost' => 'integer',
'amount' => 'float',
'multiplier' => 'integer',
'retry_count' => 'integer',
'create_time' => 'integer',
'update_time' => 'integer',
'points_cost' => 'integer',
'amount' => 'float',
'multiplier' => 'integer',
'retry_count' => 'integer',
];
public function mallItem(): \think\model\relation\BelongsTo
@@ -64,3 +64,4 @@ class MallPlayxOrder extends Model
return $this->belongsTo(MallItem::class, 'mall_item_id', 'id');
}
}

View File

@@ -21,6 +21,6 @@ class MallPintsOrder extends Model
public function playxUserAsset(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\common\model\MallPlayxUserAsset::class, 'playx_user_asset_id', 'id');
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
}
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace app\common\model;
use support\think\Model;
/**
* PlayX 会话缓存
*/
class MallPlayxSession extends Model
{
protected string $name = 'mall_playx_session';
protected bool $autoWriteTimestamp = true;
protected array $type = [
// 这里需要显式声明 create_time / update_time 为 integer
// 否则 ThinkORM 可能把 bigint 时间戳当成字符串,导致写入时出现 now 字符串问题。
'create_time' => 'integer',
'update_time' => 'integer',
'expire_time' => 'integer',
];
}

View File

@@ -21,7 +21,7 @@ class MallRedemptionOrder extends Model
public function playxUserAsset(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\common\model\MallPlayxUserAsset::class, 'playx_user_asset_id', 'id');
return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id');
}
public function mallItem(): \think\model\relation\BelongsTo

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace app\common\model;
use support\think\Model;
/**
* 会话缓存
*/
class MallSession extends Model
{
protected string $name = 'mall_session';
protected bool $autoWriteTimestamp = true;
protected array $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'expire_time' => 'integer',
];
}

View File

@@ -8,22 +8,22 @@ use ba\Random;
use support\think\Model;
/**
* PlayX 用户资产(积分商城用户主表,含登录账号字段)
* 用户资产(积分商城用户主表,含登录账号字段)
*/
class MallPlayxUserAsset extends Model
class MallUserAsset extends Model
{
protected string $name = 'mall_playx_user_asset';
protected string $name = 'mall_user_asset';
protected bool $autoWriteTimestamp = true;
protected array $type = [
'create_time' => 'integer',
'update_time' => 'integer',
'locked_points' => 'integer',
'available_points' => 'integer',
'today_limit' => 'integer',
'today_claimed' => 'integer',
'admin_id' => 'integer',
'create_time' => 'integer',
'update_time' => 'integer',
'locked_points' => 'integer',
'available_points' => 'integer',
'today_limit' => 'integer',
'today_claimed' => 'integer',
'admin_id' => 'integer',
];
/**
@@ -46,24 +46,24 @@ class MallPlayxUserAsset extends Model
$now = time();
$temporaryPlayxId = 'tmp_' . bin2hex(random_bytes(16));
$created = self::create([
'playx_user_id' => $temporaryPlayxId,
'username' => $username,
'phone' => $phone,
'password' => $pwd,
'admin_id' => 0,
'locked_points' => 0,
'available_points' => 0,
'today_limit' => 0,
'today_claimed' => 0,
'today_limit_date' => null,
'create_time' => $now,
'update_time' => $now,
'playx_user_id' => $temporaryPlayxId,
'username' => $username,
'phone' => $phone,
'password' => $pwd,
'admin_id' => 0,
'locked_points' => 0,
'available_points' => 0,
'today_limit' => 0,
'today_claimed' => 0,
'today_limit_date' => null,
'create_time' => $now,
'update_time' => $now,
]);
if (!$created) {
throw new \RuntimeException('Failed to create mall_playx_user_asset');
throw new \RuntimeException('Failed to create mall_user_asset');
}
$id = intval($created->getKey());
$id = $created->getKey();
$finalPlayxId = 'mall_' . $id;
if (self::where('playx_user_id', $finalPlayxId)->where('id', '<>', $id)->find()) {
$finalPlayxId = 'mall_' . $id . '_' . bin2hex(random_bytes(4));
@@ -77,7 +77,7 @@ class MallPlayxUserAsset extends Model
private static function allocateUniquePhone(): ?string
{
for ($i = 0; $i < 8; $i++) {
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
$candidate = '13' . str_pad(mt_rand(0, 999999999), 9, '0', STR_PAD_LEFT);
if (!self::where('phone', $candidate)->find()) {
return $candidate;
}
@@ -86,3 +86,4 @@ class MallPlayxUserAsset extends Model
return null;
}
}

View File

@@ -3,8 +3,8 @@
namespace app\process;
use app\common\model\MallItem;
use app\common\model\MallPlayxOrder;
use app\common\model\MallPlayxUserAsset;
use app\common\model\MallOrder;
use app\common\model\MallUserAsset;
use GuzzleHttp\Client;
use Workerman\Timer;
use Workerman\Worker;
@@ -47,14 +47,14 @@ class PlayxJobs
$path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status'));
$url = rtrim($baseUrl, '/') . $path;
$list = MallPlayxOrder::where('grant_status', MallPlayxOrder::GRANT_ACCEPTED)
->where('status', MallPlayxOrder::STATUS_PENDING)
$list = MallOrder::where('grant_status', MallOrder::GRANT_ACCEPTED)
->where('status', MallOrder::STATUS_PENDING)
->order('id', 'desc')
->limit(50)
->select();
foreach ($list as $order) {
/** @var MallPlayxOrder $order */
/** @var MallOrder $order */
try {
$res = $this->http->get($url, [
'query' => [
@@ -65,16 +65,16 @@ class PlayxJobs
$data = json_decode(strval($res->getBody()), true) ?? [];
$pxStatus = $data['status'] ?? '';
if ($pxStatus === MallPlayxOrder::STATUS_COMPLETED) {
$order->status = MallPlayxOrder::STATUS_COMPLETED;
if ($pxStatus === MallOrder::STATUS_COMPLETED) {
$order->status = MallOrder::STATUS_COMPLETED;
$order->save();
continue;
}
if ($pxStatus === 'FAILED' || $pxStatus === MallPlayxOrder::STATUS_REJECTED) {
if ($pxStatus === 'FAILED' || $pxStatus === MallOrder::STATUS_REJECTED) {
// 仅在从 PENDING 转 REJECTED 时退分,避免重复入账
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->fail_reason = strval($data['message'] ?? 'PlayX transaction failed');
$order->save();
$this->refundPoints($order);
@@ -104,18 +104,18 @@ class PlayxJobs
$withdrawUrl = rtrim($baseUrl, '/') . $withdrawPath;
$maxRetry = 3;
$list = MallPlayxOrder::whereIn('grant_status', [
MallPlayxOrder::GRANT_NOT_SENT,
MallPlayxOrder::GRANT_FAILED_RETRYABLE,
$list = MallOrder::whereIn('grant_status', [
MallOrder::GRANT_NOT_SENT,
MallOrder::GRANT_FAILED_RETRYABLE,
])
->where('status', MallPlayxOrder::STATUS_PENDING)
->where('status', MallOrder::STATUS_PENDING)
->where('retry_count', '<', $maxRetry)
->order('id', 'desc')
->limit(50)
->select();
foreach ($list as $order) {
/** @var MallPlayxOrder $order */
/** @var MallOrder $order */
$allow = $this->allowRetryByInterval($order);
if (!$allow) {
continue;
@@ -128,21 +128,21 @@ class PlayxJobs
} catch (\Throwable $e) {
$order->fail_reason = $e->getMessage();
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
} else {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save();
}
}
}
}
private function allowRetryByInterval(MallPlayxOrder $order): bool
private function allowRetryByInterval(MallOrder $order): bool
{
if ($order->grant_status === MallPlayxOrder::GRANT_NOT_SENT) {
if ($order->grant_status === MallOrder::GRANT_NOT_SENT) {
return true;
}
@@ -167,7 +167,7 @@ class PlayxJobs
return false;
}
private function sendGrantByOrder(MallPlayxOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void
private function sendGrantByOrder(MallOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void
{
$item = null;
if ($order->mallItem) {
@@ -176,7 +176,7 @@ class PlayxJobs
$item = MallItem::where('id', $order->mall_item_id)->find();
}
if ($order->type === MallPlayxOrder::TYPE_BONUS) {
if ($order->type === MallOrder::TYPE_BONUS) {
$rewardName = $item ? strval($item->title) : '';
$category = $item ? strval($item->category) : 'daily';
$categoryTitle = $item ? strval($item->category_title) : '';
@@ -201,7 +201,7 @@ class PlayxJobs
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
$order->save();
return;
@@ -209,19 +209,19 @@ class PlayxJobs
$order->fail_reason = strval($data['message'] ?? 'PlayX bonus grant not accepted');
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
return;
}
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save();
return;
}
if ($order->type === MallPlayxOrder::TYPE_WITHDRAW) {
if ($order->type === MallOrder::TYPE_WITHDRAW) {
$multiplier = intval($order->multiplier ?? 0);
if ($multiplier <= 0) {
$multiplier = 1;
@@ -239,7 +239,7 @@ class PlayxJobs
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
$order->save();
return;
@@ -247,14 +247,14 @@ class PlayxJobs
$order->fail_reason = strval($data['message'] ?? 'PlayX balance credit not accepted');
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
$order->status = MallPlayxOrder::STATUS_REJECTED;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
return;
}
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save();
return;
}
@@ -262,12 +262,12 @@ class PlayxJobs
// PHYSICAL 目前由后台手工发货/驳回,不参与 PlayX 发放重试
}
private function refundPoints(MallPlayxOrder $order): void
private function refundPoints(MallOrder $order): void
{
if ($order->points_cost <= 0) {
return;
}
$asset = MallPlayxUserAsset::where('playx_user_id', $order->user_id)->find();
$asset = MallUserAsset::where('playx_user_id', $order->user_id)->find();
if (!$asset) {
return;
}

View File

@@ -122,6 +122,10 @@ Route::post('/api/v1/playx/bonus/redeem', [\app\api\controller\v1\Playx::class,
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']);
// ==================== Admin 路由 ====================
// Admin 多为 JSON API前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容

View File

@@ -447,6 +447,43 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
--data-urlencode 'token=上一步TemLogin返回的token'
```
---
### 3.x 收货地址(`mall_address`
> 下面接口用于 H5 维护收货地址。鉴权同本章其他接口:携带 `session_id` 或 `token` 或 `user_id`。
#### 3.x.1 地址列表
* 方法:`GET`
* 路径:`/api/v1/playx/address/list`
返回:`data.list` 为地址数组。
#### 3.x.2 添加地址
* 方法:`POST`
* 路径:`/api/v1/playx/address/add`
Body
| 字段 | 必填 | 说明 |
|------|------|------|
| `phone` | 是 | 电话 |
| `region` | 是 | 地区(数组或逗号分隔字符串) |
| `detail_address` | 是 | 详细地址 |
| `address` | 是 | 地址补充 |
| `default_setting` | 否 | `1` 设为默认地址 |
#### 3.x.3 修改地址(含设为默认)
* 方法:`POST`
* 路径:`/api/v1/playx/address/edit`
Body`id` 必填,其余字段按需传入更新。
#### 3.x.4 删除地址
* 方法:`POST`
* 路径:`/api/v1/playx/address/delete`
Body`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。
#### 远程模式(`verify_token_local_only=false` + 已配置 `base_url`
商城侧请求 URL`${playx.api.base_url}${playx.api.token_verify_url}`(默认路径 `/api/v1/auth/verify-token`)。

26
tmp_sig.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
$secret = '5590a339502b133f4d0c545c3cdad159a4827dfccb3f51bb110c56f9b96568ca';
$ts = '1700000123';
$rid = 'req_1700000000_234567';
$body = [
'report_date' => '1700000123',
'member' => [
[
'member_id' => '234567',
'login' => 'zhenhui',
'ltv_deposit' => 1500,
'ltv_withdrawal' => 1800,
'yesterday_total_wl' => -300,
'yesterday_total_deposit' => 600,
],
],
];
$json = json_encode($body);
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', $json);
echo "json={$json}\n";
echo "sha256=" . hash('sha256', $json) . "\n";
echo "X-Signature=" . hash_hmac('sha256', $canonical, $secret) . "\n";

View File

@@ -36,4 +36,23 @@ export default {
second: 'Second',
day: 'Day',
'Number of attachments Uploaded': 'Number of attachments upload',
Today: 'Today',
Yesterday: 'Yesterday',
Orders: 'Orders',
Pending: 'Pending',
'Daily new players': 'Daily new players',
'Yesterday points': 'Yesterday points (claimed)',
'Yesterday redeem': 'Yesterday redeem',
'Pending physical to ship': 'Pending physical to ship',
'Yesterday item redeem stat': 'Yesterday item redeem stat',
'Yesterday redeem points sum': 'Yesterday redeem points sum',
'Yesterday redeem amount sum': 'Yesterday redeem amount sum',
'Grant failed retryable': 'Grant failed retryable',
'Item ID': 'Item ID',
'Item title': 'Item title',
'Order count': 'Order count',
Completed: 'Completed',
Rejected: 'Rejected',
'Points sum': 'Points sum',
'Amount sum': 'Amount sum',
}

View File

@@ -7,7 +7,7 @@ export default {
detail_address: 'detail_address',
address: 'address',
default_setting: 'Default address',
'default_setting 0': 'NO',
'default_setting 0': '--',
'default_setting 1': 'YES',
create_time: 'create_time',
update_time: 'update_time',

View File

@@ -0,0 +1,9 @@
export default {
id: 'id',
claim_request_id: 'claim_request_id',
user_id: 'user_id',
claimed_amount: 'claimed_amount',
create_time: 'create_time',
'quick Search Fields': 'id',
}

View File

@@ -0,0 +1,13 @@
export default {
id: 'id',
user_id: 'user_id',
date: 'date',
username: 'username',
yesterday_win_loss_net: 'yesterday_win_loss_net',
yesterday_total_deposit: 'yesterday_total_deposit',
lifetime_total_deposit: 'lifetime_total_deposit',
lifetime_total_withdraw: 'lifetime_total_withdraw',
create_time: 'create_time',
'quick Search Fields': 'id',
}

View File

@@ -0,0 +1,37 @@
export default {
id: 'id',
user_id: 'user_id',
type: 'type',
'type BONUS': 'Bonus(BONUS)',
'type PHYSICAL': 'Physical(PHYSICAL)',
'type WITHDRAW': 'Withdraw(WITHDRAW)',
status: 'status',
'status PENDING': 'Pending(PENDING)',
'status COMPLETED': 'Completed(COMPLETED)',
'status SHIPPED': 'Shipped(SHIPPED)',
'status REJECTED': 'Rejected(REJECTED)',
mall_item_id: 'mall_item_id',
mallitem__title: 'title',
points_cost: 'points_cost',
amount: 'amount',
multiplier: 'multiplier',
external_transaction_id: 'external_transaction_id',
playx_transaction_id: 'playx_transaction_id',
grant_status: 'grant_status',
'grant_status NOT_SENT': 'NOT_SENT',
'grant_status SENT_PENDING': 'SENT_PENDING',
'grant_status ACCEPTED': 'ACCEPTED',
'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE',
'grant_status FAILED_FINAL': 'FAILED_FINAL',
fail_reason: 'fail_reason',
reject_reason: 'reject_reason',
shipping_company: 'shipping_company',
shipping_no: 'shipping_no',
receiver_name: 'receiver_name',
receiver_phone: 'receiver_phone',
receiver_address: 'receiver_address',
create_time: 'create_time',
update_time: 'update_time',
'quick Search Fields': 'ID',
}

View File

@@ -0,0 +1,15 @@
export default {
id: 'id',
username: 'username',
phone: 'phone',
playx_user_id: 'playx_user_id',
locked_points: 'locked_points',
available_points: 'available_points',
today_limit: 'today_limit',
today_claimed: 'today_claimed',
today_limit_date: 'today_limit_date',
create_time: 'create_time',
update_time: 'update_time',
'quick Search Fields': 'id, playx_user_id, username, phone',
}

View File

@@ -36,4 +36,23 @@ export default {
second: '秒',
day: '天',
'Number of attachments Uploaded': '附件上传量',
Today: '今日',
Yesterday: '昨日',
Orders: '订单',
Pending: '待处理',
'Daily new players': '每日新增玩家',
'Yesterday points': '昨日积分(领取)',
'Yesterday redeem': '昨日兑换',
'Pending physical to ship': '待发货实物单',
'Yesterday item redeem stat': '昨日商品兑换统计',
'Yesterday redeem points sum': '昨日兑换消耗积分合计',
'Yesterday redeem amount sum': '昨日兑换现金面值合计',
'Grant failed retryable': '发放失败待重试',
'Item ID': '商品ID',
'Item title': '商品名称',
'Order count': '兑换次数',
Completed: '已完成',
Rejected: '已驳回',
'Points sum': '消耗积分合计',
'Amount sum': '现金面值合计',
}

View File

@@ -7,7 +7,7 @@ export default {
detail_address: '详细地址',
address: '地址',
default_setting: '默认地址',
'default_setting 0': '',
'default_setting 0': '--',
'default_setting 1': '是',
create_time: '创建时间',
update_time: '修改时间',

View File

@@ -0,0 +1,9 @@
export default {
id: 'ID',
claim_request_id: '领取幂等键',
user_id: '用户ID',
claimed_amount: '领取积分',
create_time: '创建时间',
'quick Search Fields': 'ID',
}

View File

@@ -0,0 +1,13 @@
export default {
id: 'ID',
user_id: '用户ID',
date: '业务日期',
username: '用户名',
yesterday_win_loss_net: '昨日净输赢',
yesterday_total_deposit: '昨日总充值',
lifetime_total_deposit: '历史总充值',
lifetime_total_withdraw: '历史总提现',
create_time: '创建时间',
'quick Search Fields': 'ID',
}

View File

@@ -0,0 +1,37 @@
export default {
id: 'ID',
user_id: '用户ID',
type: '类型',
'type BONUS': '红利(BONUS)',
'type PHYSICAL': '实物(PHYSICAL)',
'type WITHDRAW': '提现(WITHDRAW)',
status: '状态',
'status PENDING': '处理中(PENDING)',
'status COMPLETED': '已完成(COMPLETED)',
'status SHIPPED': '已发货(SHIPPED)',
'status REJECTED': '已驳回(REJECTED)',
mall_item_id: '商品ID',
mallitem__title: '商品标题',
points_cost: '消耗积分',
amount: '现金面值',
multiplier: '流水倍数',
external_transaction_id: '外部交易幂等键',
playx_transaction_id: 'PlayX流水号',
grant_status: '发放子状态',
'grant_status NOT_SENT': '未发送',
'grant_status SENT_PENDING': '已发送排队',
'grant_status ACCEPTED': '已接收(accepted)',
'grant_status FAILED_RETRYABLE': '失败可重试',
'grant_status FAILED_FINAL': '失败最终',
fail_reason: '失败原因',
reject_reason: '驳回原因',
shipping_company: '物流公司',
shipping_no: '物流单号',
receiver_name: '收货人',
receiver_phone: '收货电话',
receiver_address: '收货地址',
create_time: '创建时间',
update_time: '修改时间',
'quick Search Fields': 'ID',
}

View File

@@ -0,0 +1,15 @@
export default {
id: 'ID',
username: '用户名',
phone: '手机号',
playx_user_id: 'PlayX用户ID',
locked_points: '待领取积分',
available_points: '可用积分',
today_limit: '今日可领取上限',
today_claimed: '今日已领取',
today_limit_date: '今日上限日期',
create_time: '创建时间',
update_time: '修改时间',
'quick Search Fields': 'ID、PlayX用户ID、用户名、手机号',
}

View File

@@ -28,119 +28,88 @@
<el-row :gutter="20">
<el-col :sm="12" :lg="6">
<div class="small-panel user-reg suspension">
<div class="small-panel-title">{{ t('dashboard.Member registration') }}</div>
<div class="small-panel-title">{{ t('dashboard.Daily new players') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#8595F4" size="20" name="fa fa-line-chart" />
<el-statistic :value="userRegNumberOutput" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+14%</div>
<div class="content-right color-info">{{ t('dashboard.Today') }}</div>
</div>
</div>
</el-col>
<el-col :sm="12" :lg="6">
<div class="small-panel file suspension">
<div class="small-panel-title">{{ t('dashboard.Number of attachments Uploaded') }}</div>
<div class="small-panel-title">{{ t('dashboard.Yesterday points') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#AD85F4" size="20" name="fa fa-file-text" />
<el-statistic :value="fileNumberOutput" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+50%</div>
<div class="content-right color-info">{{ t('dashboard.Yesterday') }}</div>
</div>
</div>
</el-col>
<el-col :sm="12" :lg="6">
<div class="small-panel users suspension">
<div class="small-panel-title">{{ t('dashboard.Total number of members') }}</div>
<div class="small-panel-title">{{ t('dashboard.Yesterday redeem') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#74A8B5" size="20" name="fa fa-users" />
<el-statistic :value="usersNumberOutput" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+28%</div>
<div class="content-right color-info">{{ t('dashboard.Orders') }}</div>
</div>
</div>
</el-col>
<el-col :sm="12" :lg="6">
<div class="small-panel addons suspension">
<div class="small-panel-title">{{ t('dashboard.Number of installed plug-ins') }}</div>
<div class="small-panel-title">{{ t('dashboard.Pending physical to ship') }}</div>
<div class="small-panel-content">
<div class="content-left">
<Icon color="#F48595" size="20" name="fa fa-object-group" />
<el-statistic :value="addonsNumberOutput" :value-style="statisticValueStyle" />
</div>
<div class="content-right">+88%</div>
<div class="content-right color-info">{{ t('dashboard.Pending') }}</div>
</div>
</div>
</el-col>
</el-row>
</div>
<div class="growth-chart">
<el-row :gutter="20">
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="12" :lg="9">
<el-card shadow="hover" :header="t('dashboard.Membership growth')">
<div class="user-growth-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="12" :lg="9">
<el-card shadow="hover" :header="t('dashboard.Annex growth')">
<div class="file-growth-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="6">
<el-card class="new-user-card" shadow="hover" :header="t('dashboard.New member')">
<div class="new-user-growth">
<el-scrollbar>
<div class="new-user-item">
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
<div class="new-user-base">
<div class="new-user-name">妙码生花</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
<div class="new-user-item">
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
<div class="new-user-base">
<div class="new-user-name">码上生花</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
<div class="new-user-item">
<img class="new-user-avatar" src="~assets/login-header.png" alt="" />
<div class="new-user-base">
<div class="new-user-name">Admin</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
<div class="new-user-item">
<img class="new-user-avatar" :src="fullUrl('/static/images/avatar.png')" alt="" />
<div class="new-user-base">
<div class="new-user-name">纯属虚构</div>
<div class="new-user-time">12分钟前{{ t('dashboard.Joined us') }}</div>
</div>
<Icon class="new-user-arrow" color="#8595F4" name="fa fa-angle-right" />
</div>
</el-scrollbar>
</div>
</el-card>
</el-col>
</el-row>
</div>
<div class="growth-chart">
<el-row :gutter="20">
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="12">
<el-card shadow="hover" :header="t('dashboard.Member source')">
<div class="user-source-chart" :ref="chartRefs.set"></div>
</el-card>
</el-col>
<el-col class="lg-mb-20" :xs="24" :sm="24" :md="24" :lg="12">
<el-card shadow="hover" :header="t('dashboard.Member last name')">
<div class="user-surname-chart" :ref="chartRefs.set"></div>
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<el-card shadow="hover" :header="t('dashboard.Yesterday item redeem stat')">
<div class="playx-kpis">
<div class="playx-kpi">
<div class="playx-kpi-title">{{ t('dashboard.Yesterday redeem points sum') }}</div>
<div class="playx-kpi-value">{{ state.playx?.yesterday_redeem?.points_cost_sum ?? 0 }}</div>
</div>
<div class="playx-kpi">
<div class="playx-kpi-title">{{ t('dashboard.Yesterday redeem amount sum') }}</div>
<div class="playx-kpi-value">{{ state.playx?.yesterday_redeem?.amount_sum ?? 0 }}</div>
</div>
<div class="playx-kpi">
<div class="playx-kpi-title">{{ t('dashboard.Grant failed retryable') }}</div>
<div class="playx-kpi-value">{{ state.playx?.grant_failed_retryable ?? 0 }}</div>
</div>
</div>
<el-table
v-loading="state.playxLoading"
:data="state.playx?.yesterday_redeem?.by_item ?? []"
size="small"
style="width: 100%; margin-top: 12px"
>
<el-table-column prop="mall_item_id" :label="t('dashboard.Item ID')" width="100" />
<el-table-column prop="title" :label="t('dashboard.Item title')" min-width="220" />
<el-table-column prop="order_count" :label="t('dashboard.Order count')" width="120" />
<el-table-column prop="completed_count" :label="t('dashboard.Completed')" width="120" />
<el-table-column prop="rejected_count" :label="t('dashboard.Rejected')" width="120" />
<el-table-column prop="points_cost_sum" :label="t('dashboard.Points sum')" width="140" />
<el-table-column prop="amount_sum" :label="t('dashboard.Amount sum')" width="140" />
</el-table>
</el-card>
</el-col>
</el-row>
@@ -149,17 +118,15 @@
</template>
<script setup lang="ts">
import { useEventListener, useTemplateRefsList, useTransition } from '@vueuse/core'
import * as echarts from 'echarts'
import { CSSProperties, nextTick, onActivated, onBeforeMount, onMounted, onUnmounted, reactive, toRefs, watch } from 'vue'
import { useTransition } from '@vueuse/core'
import { CSSProperties, onMounted, onUnmounted, reactive, toRefs } from 'vue'
import { useI18n } from 'vue-i18n'
import { index } from '/@/api/backend/dashboard'
import coffeeSvg from '/@/assets/dashboard/coffee.svg'
import headerSvg from '/@/assets/dashboard/header-1.svg'
import { useAdminInfo } from '/@/stores/adminInfo'
import { WORKING_TIME } from '/@/stores/constant/cacheKey'
import { useNavTabs } from '/@/stores/navTabs'
import { fullUrl, getGreet } from '/@/utils/common'
import { getGreet } from '/@/utils/common'
import { Local } from '/@/utils/storage'
let workTimer: number
@@ -169,20 +136,20 @@ defineOptions({
const d = new Date()
const { t } = useI18n()
const navTabs = useNavTabs()
const adminInfo = useAdminInfo()
const chartRefs = useTemplateRefsList<HTMLDivElement>()
const state: {
charts: any[]
remark: string
workingTimeFormat: string
pauseWork: boolean
playx: any | null
playxLoading: boolean
} = reactive({
charts: [],
remark: 'dashboard.Loading',
workingTimeFormat: '',
pauseWork: false,
playx: null,
playxLoading: true,
})
/**
@@ -206,301 +173,20 @@ const statisticValueStyle: CSSProperties = {
index().then((res) => {
state.remark = res.data.remark
state.playx = res.data.playx ?? null
state.playxLoading = false
initCountUp()
}).catch(() => {
state.playxLoading = false
})
const initCountUp = () => {
// 虚拟数据
countUpRefs.userRegNumber.value = 5456
countUpRefs.fileNumber.value = 1234
countUpRefs.usersNumber.value = 9486
countUpRefs.addonsNumber.value = 875
}
const initUserGrowthChart = () => {
const userGrowthChart = echarts.init(chartRefs.value[0] as HTMLElement)
const option = {
grid: {
top: 40,
right: 0,
bottom: 20,
left: 40,
},
xAxis: {
data: [
t('dashboard.Monday'),
t('dashboard.Tuesday'),
t('dashboard.Wednesday'),
t('dashboard.Thursday'),
t('dashboard.Friday'),
t('dashboard.Saturday'),
t('dashboard.Sunday'),
],
},
yAxis: {},
legend: {
data: [t('dashboard.Visits'), t('dashboard.Registration volume')],
textStyle: {
color: '#73767a',
},
top: 0,
},
series: [
{
name: t('dashboard.Visits'),
data: [100, 160, 280, 230, 190, 200, 480],
type: 'line',
smooth: true,
areaStyle: {
color: '#8595F4',
},
},
{
name: t('dashboard.Registration volume'),
data: [45, 180, 146, 99, 210, 127, 288],
type: 'line',
smooth: true,
areaStyle: {
color: '#F48595',
opacity: 0.5,
},
},
],
}
userGrowthChart.setOption(option)
state.charts.push(userGrowthChart)
}
const initFileGrowthChart = () => {
const fileGrowthChart = echarts.init(chartRefs.value[1] as HTMLElement)
const option = {
grid: {
top: 30,
right: 0,
bottom: 20,
left: 0,
},
tooltip: {
trigger: 'item',
},
legend: {
type: 'scroll',
bottom: 0,
data: (function () {
var list = []
for (var i = 1; i <= 28; i++) {
list.push(i + 2000 + '')
}
return list
})(),
textStyle: {
color: '#73767a',
},
},
visualMap: {
top: 'middle',
right: 10,
color: ['red', 'yellow'],
calculable: true,
},
radar: {
indicator: [
{ name: t('dashboard.picture') },
{ name: t('dashboard.file') },
{ name: t('dashboard.table') },
{ name: t('dashboard.Compressed package') },
{ name: t('dashboard.other') },
],
},
series: (function () {
var series = []
for (var i = 1; i <= 28; i++) {
series.push({
type: 'radar',
symbol: 'none',
lineStyle: {
width: 1,
},
emphasis: {
areaStyle: {
color: 'rgba(0,250,0,0.3)',
},
},
data: [
{
value: [(40 - i) * 10, (38 - i) * 4 + 60, i * 5 + 10, i * 9, (i * i) / 2],
name: i + 2000 + '',
},
],
})
}
return series
})(),
}
fileGrowthChart.setOption(option)
state.charts.push(fileGrowthChart)
}
const initUserSourceChart = () => {
const UserSourceChart = echarts.init(chartRefs.value[2] as HTMLElement)
const pathSymbols = {
reindeer:
'path://M-22.788,24.521c2.08-0.986,3.611-3.905,4.984-5.892 c-2.686,2.782-5.047,5.884-9.102,7.312c-0.992,0.005-0.25-2.016,0.34-2.362l1.852-0.41c0.564-0.218,0.785-0.842,0.902-1.347 c2.133-0.727,4.91-4.129,6.031-6.194c1.748-0.7,4.443-0.679,5.734-2.293c1.176-1.468,0.393-3.992,1.215-6.557 c0.24-0.754,0.574-1.581,1.008-2.293c-0.611,0.011-1.348-0.061-1.959-0.608c-1.391-1.245-0.785-2.086-1.297-3.313 c1.684,0.744,2.5,2.584,4.426,2.586C-8.46,3.012-8.255,2.901-8.04,2.824c6.031-1.952,15.182-0.165,19.498-3.937 c1.15-3.933-1.24-9.846-1.229-9.938c0.008-0.062-1.314-0.004-1.803-0.258c-1.119-0.771-6.531-3.75-0.17-3.33 c0.314-0.045,0.943,0.259,1.439,0.435c-0.289-1.694-0.92-0.144-3.311-1.946c0,0-1.1-0.855-1.764-1.98 c-0.836-1.09-2.01-2.825-2.992-4.031c-1.523-2.476,1.367,0.709,1.816,1.108c1.768,1.704,1.844,3.281,3.232,3.983 c0.195,0.203,1.453,0.164,0.926-0.468c-0.525-0.632-1.367-1.278-1.775-2.341c-0.293-0.703-1.311-2.326-1.566-2.711 c-0.256-0.384-0.959-1.718-1.67-2.351c-1.047-1.187-0.268-0.902,0.521-0.07c0.789,0.834,1.537,1.821,1.672,2.023 c0.135,0.203,1.584,2.521,1.725,2.387c0.102-0.259-0.035-0.428-0.158-0.852c-0.125-0.423-0.912-2.032-0.961-2.083 c-0.357-0.852-0.566-1.908-0.598-3.333c0.4-2.375,0.648-2.486,0.549-0.705c0.014,1.143,0.031,2.215,0.602,3.247 c0.807,1.496,1.764,4.064,1.836,4.474c0.561,3.176,2.904,1.749,2.281-0.126c-0.068-0.446-0.109-2.014-0.287-2.862 c-0.18-0.849-0.219-1.688-0.113-3.056c0.066-1.389,0.232-2.055,0.277-2.299c0.285-1.023,0.4-1.088,0.408,0.135 c-0.059,0.399-0.131,1.687-0.125,2.655c0.064,0.642-0.043,1.768,0.172,2.486c0.654,1.928-0.027,3.496,1,3.514 c1.805-0.424,2.428-1.218,2.428-2.346c-0.086-0.704-0.121-0.843-0.031-1.193c0.221-0.568,0.359-0.67,0.312-0.076 c-0.055,0.287,0.031,0.533,0.082,0.794c0.264,1.197,0.912,0.114,1.283-0.782c0.15-0.238,0.539-2.154,0.545-2.522 c-0.023-0.617,0.285-0.645,0.309,0.01c0.064,0.422-0.248,2.646-0.205,2.334c-0.338,1.24-1.105,3.402-3.379,4.712 c-0.389,0.12-1.186,1.286-3.328,2.178c0,0,1.729,0.321,3.156,0.246c1.102-0.19,3.707-0.027,4.654,0.269 c1.752,0.494,1.531-0.053,4.084,0.164c2.26-0.4,2.154,2.391-1.496,3.68c-2.549,1.405-3.107,1.475-2.293,2.984 c3.484,7.906,2.865,13.183,2.193,16.466c2.41,0.271,5.732-0.62,7.301,0.725c0.506,0.333,0.648,1.866-0.457,2.86 c-4.105,2.745-9.283,7.022-13.904,7.662c-0.977-0.194,0.156-2.025,0.803-2.247l1.898-0.03c0.596-0.101,0.936-0.669,1.152-1.139 c3.16-0.404,5.045-3.775,8.246-4.818c-4.035-0.718-9.588,3.981-12.162,1.051c-5.043,1.423-11.449,1.84-15.895,1.111 c-3.105,2.687-7.934,4.021-12.115,5.866c-3.271,3.511-5.188,8.086-9.967,10.414c-0.986,0.119-0.48-1.974,0.066-2.385l1.795-0.618 C-22.995,25.682-22.849,25.035-22.788,24.521z',
plane: 'path://M1.112,32.559l2.998,1.205l-2.882,2.268l-2.215-0.012L1.112,32.559z M37.803,23.96 c0.158-0.838,0.5-1.509,0.961-1.904c-0.096-0.037-0.205-0.071-0.344-0.071c-0.777-0.005-2.068-0.009-3.047-0.009 c-0.633,0-1.217,0.066-1.754,0.18l2.199,1.804H37.803z M39.738,23.036c-0.111,0-0.377,0.325-0.537,0.924h1.076 C40.115,23.361,39.854,23.036,39.738,23.036z M39.934,39.867c-0.166,0-0.674,0.705-0.674,1.986s0.506,1.986,0.674,1.986 s0.672-0.705,0.672-1.986S40.102,39.867,39.934,39.867z M38.963,38.889c-0.098-0.038-0.209-0.07-0.348-0.073 c-0.082,0-0.174,0-0.268-0.001l-7.127,4.671c0.879,0.821,2.42,1.417,4.348,1.417c0.979,0,2.27-0.006,3.047-0.01 c0.139,0,0.25-0.034,0.348-0.072c-0.646-0.555-1.07-1.643-1.07-2.967C37.891,40.529,38.316,39.441,38.963,38.889z M32.713,23.96 l-12.37-10.116l-4.693-0.004c0,0,4,8.222,4.827,10.121H32.713z M59.311,32.374c-0.248,2.104-5.305,3.172-8.018,3.172H39.629 l-25.325,16.61L9.607,52.16c0,0,6.687-8.479,7.95-10.207c1.17-1.6,3.019-3.699,3.027-6.407h-2.138 c-5.839,0-13.816-3.789-18.472-5.583c-2.818-1.085-2.396-4.04-0.031-4.04h0.039l-3.299-11.371h3.617c0,0,4.352,5.696,5.846,7.5 c2,2.416,4.503,3.678,8.228,3.87h30.727c2.17,0,4.311,0.417,6.252,1.046c3.49,1.175,5.863,2.7,7.199,4.027 C59.145,31.584,59.352,32.025,59.311,32.374z M22.069,30.408c0-0.815-0.661-1.475-1.469-1.475c-0.812,0-1.471,0.66-1.471,1.475 s0.658,1.475,1.471,1.475C21.408,31.883,22.069,31.224,22.069,30.408z M27.06,30.408c0-0.815-0.656-1.478-1.466-1.478 c-0.812,0-1.471,0.662-1.471,1.478s0.658,1.477,1.471,1.477C26.404,31.885,27.06,31.224,27.06,30.408z M32.055,30.408 c0-0.815-0.66-1.475-1.469-1.475c-0.808,0-1.466,0.66-1.466,1.475s0.658,1.475,1.466,1.475 C31.398,31.883,32.055,31.224,32.055,30.408z M37.049,30.408c0-0.815-0.658-1.478-1.467-1.478c-0.812,0-1.469,0.662-1.469,1.478 s0.656,1.477,1.469,1.477C36.389,31.885,37.049,31.224,37.049,30.408z M42.039,30.408c0-0.815-0.656-1.478-1.465-1.478 c-0.811,0-1.469,0.662-1.469,1.478s0.658,1.477,1.469,1.477C41.383,31.885,42.039,31.224,42.039,30.408z M55.479,30.565 c-0.701-0.436-1.568-0.896-2.627-1.347c-0.613,0.289-1.551,0.476-2.73,0.476c-1.527,0-1.639,2.263,0.164,2.316 C52.389,32.074,54.627,31.373,55.479,30.565z',
rocket: 'path://M-244.396,44.399c0,0,0.47-2.931-2.427-6.512c2.819-8.221,3.21-15.709,3.21-15.709s5.795,1.383,5.795,7.325C-237.818,39.679-244.396,44.399-244.396,44.399z M-260.371,40.827c0,0-3.881-12.946-3.881-18.319c0-2.416,0.262-4.566,0.669-6.517h17.684c0.411,1.952,0.675,4.104,0.675,6.519c0,5.291-3.87,18.317-3.87,18.317H-260.371z M-254.745,18.951c-1.99,0-3.603,1.676-3.603,3.744c0,2.068,1.612,3.744,3.603,3.744c1.988,0,3.602-1.676,3.602-3.744S-252.757,18.951-254.745,18.951z M-255.521,2.228v-5.098h1.402v4.969c1.603,1.213,5.941,5.069,7.901,12.5h-17.05C-261.373,7.373-257.245,3.558-255.521,2.228zM-265.07,44.399c0,0-6.577-4.721-6.577-14.896c0-5.942,5.794-7.325,5.794-7.325s0.393,7.488,3.211,15.708C-265.539,41.469-265.07,44.399-265.07,44.399z M-252.36,45.15l-1.176-1.22L-254.789,48l-1.487-4.069l-1.019,2.116l-1.488-3.826h8.067L-252.36,45.15z',
train: 'path://M67.335,33.596L67.335,33.596c-0.002-1.39-1.153-3.183-3.328-4.218h-9.096v-2.07h5.371 c-4.939-2.07-11.199-4.141-14.89-4.141H19.72v12.421v5.176h38.373c4.033,0,8.457-1.035,9.142-5.176h-0.027 c0.076-0.367,0.129-0.751,0.129-1.165L67.335,33.596L67.335,33.596z M27.999,30.413h-3.105v-4.141h3.105V30.413z M35.245,30.413 h-3.104v-4.141h3.104V30.413z M42.491,30.413h-3.104v-4.141h3.104V30.413z M49.736,30.413h-3.104v-4.141h3.104V30.413z M14.544,40.764c1.143,0,2.07-0.927,2.07-2.07V35.59V25.237c0-1.145-0.928-2.07-2.07-2.07H-9.265c-1.143,0-2.068,0.926-2.068,2.07 v10.351v3.105c0,1.144,0.926,2.07,2.068,2.07H14.544L14.544,40.764z M8.333,26.272h3.105v4.141H8.333V26.272z M1.087,26.272h3.105 v4.141H1.087V26.272z M-6.159,26.272h3.105v4.141h-3.105V26.272z M-9.265,41.798h69.352v1.035H-9.265V41.798z',
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'none',
},
formatter: function (params: any) {
return params[0].name + ': ' + params[0].value
},
},
xAxis: {
data: [t('dashboard.Baidu'), t('dashboard.Direct access'), t('dashboard.take a plane'), t('dashboard.Take the high-speed railway')],
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: '#e54035',
},
},
yAxis: {
splitLine: { show: false },
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { show: false },
},
color: ['#e54035'],
series: [
{
name: 'hill',
type: 'pictorialBar',
barCategoryGap: '-130%',
symbol: 'path://M0,10 L10,10 C5.5,10 5.5,5 5,0 C4.5,5 4.5,10 0,10 z',
itemStyle: {
opacity: 0.5,
},
emphasis: {
itemStyle: {
opacity: 1,
},
},
data: [123, 60, 25, 80],
z: 10,
},
{
name: 'glyph',
type: 'pictorialBar',
barGap: '-100%',
symbolPosition: 'end',
symbolSize: 50,
symbolOffset: [0, '-120%'],
data: [
{
value: 123,
symbol: pathSymbols.reindeer,
symbolSize: [60, 60],
},
{
value: 60,
symbol: pathSymbols.rocket,
symbolSize: [50, 60],
},
{
value: 25,
symbol: pathSymbols.plane,
symbolSize: [65, 35],
},
{
value: 80,
symbol: pathSymbols.train,
symbolSize: [50, 30],
},
],
},
],
}
UserSourceChart.setOption(option)
state.charts.push(UserSourceChart)
}
const initUserSurnameChart = () => {
const userSurnameChart = echarts.init(chartRefs.value[3] as HTMLElement)
const data = genData(20)
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: data.legendData,
textStyle: {
color: '#73767a',
},
},
series: [
{
name: t('dashboard.full name'),
type: 'pie',
radius: '55%',
center: ['40%', '50%'],
data: data.seriesData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
function genData(count: any) {
// prettier-ignore
const nameList = [
'赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', '柏', '水', '窦', '章', '云', '苏', '潘', '葛', '奚', '范', '彭', '郎', '鲁', '韦', '昌', '马', '苗', '凤', '花', '方', '俞', '任', '袁', '柳', '酆', '鲍', '史', '唐', '费', '廉', '岑', '薛', '雷', '贺', '倪', '汤', '滕', '殷', '罗', '毕', '郝', '邬', '安', '常', '乐', '于', '时', '傅', '皮', '卞', '齐', '康', '伍', '余', '元', '卜', '顾', '孟', '平', '黄', '和', '穆', '萧', '尹', '姚', '邵', '湛', '汪', '祁', '毛', '禹', '狄', '米', '贝', '明', '臧', '计', '伏', '成', '戴', '谈', '宋', '茅', '庞', '熊', '纪', '舒', '屈', '项', '祝', '董', '梁', '杜', '阮', '蓝', '闵', '席', '季', '麻', '强', '贾', '路', '娄', '危'
];
const legendData = []
const seriesData = []
for (var i = 0; i < count; i++) {
var name = Math.random() > 0.85 ? makeWord(2, 1) + '·' + makeWord(2, 0) : makeWord(2, 1)
legendData.push(name)
seriesData.push({
name: name,
value: Math.round(Math.random() * 100000),
})
}
return {
legendData: legendData,
seriesData: seriesData,
}
function makeWord(max: any, min: any) {
const nameLen = Math.ceil(Math.random() * max + min)
const name = []
for (var i = 0; i < nameLen; i++) {
name.push(nameList[Math.round(Math.random() * nameList.length - 1)])
}
return name.join('')
}
}
userSurnameChart.setOption(option)
state.charts.push(userSurnameChart)
}
const echartsResize = () => {
nextTick(() => {
for (const key in state.charts) {
state.charts[key].resize()
}
})
const playx = state.playx ?? {}
const yesterdayRedeem = playx.yesterday_redeem ?? {}
countUpRefs.userRegNumber.value = playx.new_players_today ?? 0
countUpRefs.fileNumber.value = playx.yesterday_points_claimed ?? 0
countUpRefs.usersNumber.value = yesterdayRedeem.order_count ?? 0
countUpRefs.addonsNumber.value = playx.pending_physical_to_ship ?? 0
}
const onChangeWorkState = () => {
@@ -594,36 +280,13 @@ const formatSeconds = (seconds: number) => {
return result
}
onActivated(() => {
echartsResize()
})
onMounted(() => {
startWork()
initCountUp()
initUserGrowthChart()
initFileGrowthChart()
initUserSourceChart()
initUserSurnameChart()
useEventListener(window, 'resize', echartsResize)
})
onBeforeMount(() => {
for (const key in state.charts) {
state.charts[key].dispose()
}
})
onUnmounted(() => {
clearInterval(workTimer)
})
watch(
() => navTabs.state.tabFullScreen,
() => {
echartsResize()
}
)
</script>
<style scoped lang="scss">
@@ -751,48 +414,27 @@ watch(
.growth-chart {
margin-bottom: 20px;
}
.user-growth-chart,
.file-growth-chart {
height: 260px;
.playx-kpis {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.new-user-growth {
height: 300px;
}
.user-source-chart,
.user-surname-chart {
height: 400px;
}
.new-user-item {
display: flex;
align-items: center;
padding: 20px;
margin: 10px 15px;
box-shadow: 0 0 30px 0 rgba(82, 63, 105, 0.05);
.playx-kpi {
background-color: var(--ba-bg-color-overlay);
.new-user-avatar {
height: 48px;
width: 48px;
border-radius: 50%;
}
.new-user-base {
margin-left: 10px;
color: #2c3f5d;
.new-user-name {
font-size: 15px;
}
.new-user-time {
font-size: 13px;
}
}
.new-user-arrow {
margin-left: auto;
}
border: 1px solid var(--ba-border-color);
border-radius: var(--el-border-radius-base);
padding: 12px;
}
.new-user-card :deep(.el-card__body) {
padding: 0;
.playx-kpi-title {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.playx-kpi-value {
margin-top: 6px;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
@media screen and (max-width: 425px) {
.welcome-img {
display: none;

View File

@@ -34,7 +34,7 @@
type="remoteSelect"
v-model="baTable.form.items!.playx_user_asset_id"
prop="playx_user_asset_id"
:input-attr="{ pk: 'mall_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/select' }"
: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

View File

@@ -0,0 +1,61 @@
<template>
<div class="default-main ba-table-box">
<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']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.claimLog.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'mall/claimLog',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const baTable = new baTableClass(
new baTableApi('/admin/mall.ClaimLog/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('mall.claimLog.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{ label: t('mall.claimLog.claim_request_id'), prop: 'claim_request_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{ label: t('mall.claimLog.user_id'), prop: 'user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{ label: t('mall.claimLog.claimed_amount'), prop: 'claimed_amount', align: 'center', operator: 'RANGE', sortable: false },
{ label: t('mall.claimLog.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],
},
{
defaultItems: {},
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="default-main ba-table-box">
<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']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.dailyPush.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'mall/dailyPush',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const baTable = new baTableClass(
new baTableApi('/admin/mall.DailyPush/'),
{
pk: 'id',
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' },
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: {},
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,232 @@
<template>
<div class="default-main ba-table-box">
<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']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.order.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
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'
defineOptions({
name: 'mall/order',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const baTable = new baTableClass(
new baTableApi('/admin/mall.Order/'),
{
pk: 'id',
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.type'),
prop: 'type',
align: 'center',
operator: 'eq',
sortable: false,
render: 'tag',
replaceValue: {
BONUS: t('mall.order.type BONUS'),
PHYSICAL: t('mall.order.type PHYSICAL'),
WITHDRAW: t('mall.order.type WITHDRAW'),
},
},
{
label: t('mall.order.status'),
prop: 'status',
align: 'center',
operator: 'eq',
sortable: false,
render: 'tag',
replaceValue: {
PENDING: t('mall.order.status PENDING'),
COMPLETED: t('mall.order.status COMPLETED'),
SHIPPED: t('mall.order.status SHIPPED'),
REJECTED: t('mall.order.status REJECTED'),
},
},
{ 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.grant_status'),
prop: 'grant_status',
align: 'center',
operator: 'eq',
sortable: false,
render: 'tag',
replaceValue: {
NOT_SENT: t('mall.order.grant_status NOT_SENT'),
SENT_PENDING: t('mall.order.grant_status SENT_PENDING'),
ACCEPTED: t('mall.order.grant_status ACCEPTED'),
FAILED_RETRYABLE: t('mall.order.grant_status FAILED_RETRYABLE'),
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('Operate'),
align: 'center',
width: 220,
render: 'buttons',
buttons: [
{
render: 'confirmButton',
name: 'retry',
title: 'Retry',
text: '手动重试',
type: 'warning',
icon: '',
display: (row: TableRow) =>
(row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING',
popconfirm: {
title: '确认将该订单加入重试队列?',
confirmButtonText: '确认',
cancelButtonText: '取消',
confirmButtonType: 'warning',
},
click: async (row: TableRow) => {
await createAxios(
{
url: '/admin/mall.Order/retry',
method: 'post',
data: {
id: row.id,
},
},
{
showSuccessMessage: true,
}
)
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 {
// 用户取消:不提示
}
},
},
],
},
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: {},
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -41,7 +41,7 @@
type="remoteSelect"
v-model="baTable.form.items!.playx_user_asset_id"
prop="playx_user_asset_id"
:input-attr="{ pk: 'mall_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/select' }"
:input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }"
:placeholder="t('Please select field', { field: t('mall.pintsOrder.playx_user_asset_id') })"
/>
<FormItem

View File

@@ -41,7 +41,7 @@
type="remoteSelect"
v-model="baTable.form.items!.playx_user_asset_id"
prop="playx_user_asset_id"
:input-attr="{ pk: 'mall_playx_user_asset.id', field: 'username', remoteUrl: '/admin/mall.PlayxUserAsset/select' }"
:input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }"
:placeholder="t('Please select field', { field: t('mall.redemptionOrder.playx_user_asset_id') })"
/>
<FormItem

View File

@@ -0,0 +1,67 @@
<template>
<div class="default-main ba-table-box">
<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']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.userAsset.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'mall/userAsset',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const baTable = new baTableClass(
new baTableApi('/admin/mall.UserAsset/'),
{
pk: 'id',
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.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' },
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: {},
}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>