Compare commits
12 Commits
e38fd660e2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bec4e7758 | |||
| f9e4e61d93 | |||
| 8b6727dac1 | |||
| b97d33a24f | |||
| 6f12afcd10 | |||
| 5ab85d1d53 | |||
| 941f0f4a8c | |||
| 520e950dc5 | |||
| 2868899253 | |||
| 8305dbfcfd | |||
| 1cd5c3142d | |||
| 2686c54781 |
15
.env-example
15
.env-example
@@ -15,7 +15,7 @@ DATABASE_USERNAME = webman-buildadmin-mall
|
||||
DATABASE_PASSWORD = 123456
|
||||
DATABASE_HOSTPORT = 3306
|
||||
DATABASE_CHARSET = utf8mb4
|
||||
DATABASE_PREFIX =
|
||||
DATABASE_PREFIX =
|
||||
|
||||
# PlayX 配置
|
||||
# 提现折算:积分 -> 现金(如 10 分 = 1 元,则 points_to_cash_ratio=0.1)
|
||||
@@ -35,3 +35,16 @@ PLAYX_SESSION_EXPIRE_SECONDS=3600
|
||||
# PlayX API(商城调用 PlayX 时使用)
|
||||
PLAYX_API_BASE_URL=
|
||||
PLAYX_API_SECRET_KEY=
|
||||
|
||||
# 推送订单url
|
||||
PLAYX_ANGPOW_IMPORT_BASE_URL=https://ss2-staging2.ttwd8.com
|
||||
# 推送订单接口
|
||||
PLAYX_ANGPOW_IMPORT_PATH=/cashmarket/v3/merchant-api/angpow-imports
|
||||
# 商户编码merchant_code
|
||||
PLAYX_ANGPOW_MERCHANT_CODE=plx
|
||||
# HMAC 密钥(与对端一致)
|
||||
PLAYX_ANGPOW_IMPORT_AUTH_KEY=
|
||||
# HTTPS:CA 证书包绝对路径(推荐下载 https://curl.se/ca/cacert.pem 后填写,解决 cURL error 60)
|
||||
PLAYX_ANGPOW_IMPORT_CACERT=
|
||||
# 是否校验 SSL(1=校验;0=不校验,仅本地调试,生产勿用)
|
||||
PLAYX_ANGPOW_IMPORT_VERIFY_SSL=1
|
||||
@@ -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::where('type', MallOrder::TYPE_BONUS)
|
||||
->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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4,20 +4,21 @@ 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\library\MallBonusGrantPush;
|
||||
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;
|
||||
|
||||
@@ -46,6 +47,7 @@ class PlayxOrder extends Backend
|
||||
'receiver_name',
|
||||
'receiver_phone',
|
||||
'receiver_address',
|
||||
'mall_address_id',
|
||||
'create_time',
|
||||
'update_time',
|
||||
];
|
||||
@@ -53,7 +55,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 +73,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 +106,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 +129,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) {
|
||||
@@ -139,7 +141,52 @@ class PlayxOrder extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* PHYSICAL 驳回:更新状态为 REJECTED,并退回积分到 available_points
|
||||
* 审核通过(非 PHYSICAL):更新状态为 COMPLETED
|
||||
*/
|
||||
public function approve(Request $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$id = $request->post('id', 0);
|
||||
if (!$id) {
|
||||
return $this->error(__('Missing required fields'));
|
||||
}
|
||||
|
||||
$order = MallOrder::where('id', $id)->find();
|
||||
if (!$order) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
if ($order->status !== MallOrder::STATUS_PENDING) {
|
||||
return $this->error(__('Order status must be PENDING'));
|
||||
}
|
||||
if ($order->type === MallOrder::TYPE_PHYSICAL) {
|
||||
return $this->error(__('Order type not supported'));
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$order->status = MallOrder::STATUS_COMPLETED;
|
||||
$order->update_time = time();
|
||||
$order->save();
|
||||
|
||||
Db::commit();
|
||||
} catch (Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Approved successfully'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核驳回:更新状态为 REJECTED,并退回积分到 available_points(所有类型通用)
|
||||
*/
|
||||
public function reject(Request $request): Response
|
||||
{
|
||||
@@ -153,40 +200,46 @@ 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) {
|
||||
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) {
|
||||
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'));
|
||||
}
|
||||
|
||||
if ($order->type === MallOrder::TYPE_PHYSICAL && $rejectReason === '') {
|
||||
return $this->error(__('Missing required fields'));
|
||||
}
|
||||
|
||||
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;
|
||||
if ($order->type === MallOrder::TYPE_BONUS) {
|
||||
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||
} else {
|
||||
$order->grant_status = MallOrder::GRANT_NOT_APPLICABLE;
|
||||
}
|
||||
$order->update_time = time();
|
||||
$order->save();
|
||||
|
||||
Db::commit();
|
||||
@@ -199,7 +252,7 @@ class PlayxOrder extends Backend
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动重试(仅红利/提现,且必须 FAILED_RETRYABLE)
|
||||
* 手动推送红利(同步调用 PlayX,不限制自动重试次数;成功则 ACCEPTED,失败写入 fail_reason)
|
||||
*/
|
||||
public function retry(Request $request): Response
|
||||
{
|
||||
@@ -212,29 +265,54 @@ 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)) {
|
||||
return $this->error(__('Only BONUS/WITHDRAW can retry'));
|
||||
if ($order->type !== MallOrder::TYPE_BONUS) {
|
||||
return $this->error(__('Only BONUS can retry'));
|
||||
}
|
||||
if ($order->grant_status !== MallPlayxOrder::GRANT_FAILED_RETRYABLE) {
|
||||
return $this->error(__('Only FAILED_RETRYABLE can retry'));
|
||||
}
|
||||
if (intval($order->retry_count) >= 3) {
|
||||
return $this->error(__('Retry count exceeded'));
|
||||
if ($order->status !== MallOrder::STATUS_PENDING) {
|
||||
return $this->error(__('Order status must be PENDING'));
|
||||
}
|
||||
|
||||
$order->grant_status = MallPlayxOrder::GRANT_NOT_SENT;
|
||||
$allowedStatuses = [
|
||||
MallOrder::GRANT_NOT_SENT,
|
||||
MallOrder::GRANT_SENT_PENDING,
|
||||
MallOrder::GRANT_FAILED_RETRYABLE,
|
||||
MallOrder::GRANT_FAILED_FINAL,
|
||||
];
|
||||
if (!in_array($order->grant_status, $allowedStatuses, true)) {
|
||||
return $this->error(__('Current grant status cannot be manually pushed'));
|
||||
}
|
||||
|
||||
if (strval(config('playx.api.base_url', '')) === '') {
|
||||
return $this->error(__('PlayX API not configured'));
|
||||
}
|
||||
|
||||
$result = MallBonusGrantPush::push($order);
|
||||
if ($result['ok']) {
|
||||
$order->grant_status = MallOrder::GRANT_ACCEPTED;
|
||||
$order->playx_transaction_id = $result['playx_transaction_id'];
|
||||
$order->fail_reason = null;
|
||||
$order->update_time = time();
|
||||
$order->save();
|
||||
|
||||
return $this->success(__('Push succeeded'));
|
||||
}
|
||||
|
||||
$failReason = __('Manual push failed') . ': ' . $result['message'];
|
||||
$order->fail_reason = $failReason;
|
||||
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||
$order->update_time = time();
|
||||
$order->save();
|
||||
|
||||
return $this->success(__('Retry queued'));
|
||||
return $this->error($failReason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,4 +95,9 @@ return [
|
||||
'%d records and files have been deleted' => '%d records and files have been deleted',
|
||||
'Please input correct username' => 'Please enter the correct username',
|
||||
'Group Name Arr' => 'Group Name Arr',
|
||||
'Push succeeded' => 'Push succeeded',
|
||||
'Manual push failed' => 'Manual push failed',
|
||||
'PlayX API not configured' => 'PlayX API not configured',
|
||||
'Current grant status cannot be manually pushed' => 'Current grant status cannot be manually pushed',
|
||||
'Order status must be PENDING' => 'Order status must be PENDING',
|
||||
];
|
||||
@@ -114,4 +114,9 @@ return [
|
||||
'%d records and files have been deleted' => '已删除%d条记录和文件',
|
||||
'Please input correct username' => '请输入正确的用户名',
|
||||
'Group Name Arr' => '分组名称数组',
|
||||
'Push succeeded' => '推送成功',
|
||||
'Manual push failed' => '手动推送失败',
|
||||
'PlayX API not configured' => 'PlayX 接口未配置',
|
||||
'Current grant status cannot be manually pushed' => '当前发放状态不可手动推送',
|
||||
'Order status must be PENDING' => '订单状态须为处理中',
|
||||
];
|
||||
@@ -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 类型 token(user_id 为资产表主键)
|
||||
* 写入或复用 mall_user_asset;签发 muser 类型 token(user_id 为资产表主键)
|
||||
*/
|
||||
public function temLogin(Request $request): Response
|
||||
{
|
||||
@@ -115,12 +115,30 @@ class Auth extends Api
|
||||
}
|
||||
|
||||
$username = trim(strval($request->get('username', $request->post('username', ''))));
|
||||
// 兼容:querystring 中未编码的 '+' 会被解析为空格(application/x-www-form-urlencoded 规则)
|
||||
// 例如:/api/v1/temLogin?username=+607... 期望保留 '+',则从原始 querystring 提取并还原
|
||||
if ($username !== '' && str_contains($username, ' ')) {
|
||||
$qs = $request->queryString();
|
||||
if (is_string($qs) && $qs !== '') {
|
||||
foreach (explode('&', $qs) as $pair) {
|
||||
if ($pair === '' || !str_contains($pair, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$k, $v] = explode('=', $pair, 2);
|
||||
if (rawurldecode($k) === 'username') {
|
||||
// 先把 %xx 解码;注意这里不把 '+' 当空格处理,从而保留 '+'
|
||||
$username = trim(rawurldecode($v));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($username === '') {
|
||||
return $this->error(__('Parameter username can not be empty'));
|
||||
}
|
||||
|
||||
try {
|
||||
$asset = MallPlayxUserAsset::ensureForUsername($username);
|
||||
$asset = MallUserAsset::ensureForUsername($username);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
@@ -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.id(muser token、session、user_id 均指向资产表主键或 playx_user_id)
|
||||
* 从请求解析 mall_user_asset.id(muser 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,14 +141,14 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily Push API - PlayX 调用商城接收 T+1 数据
|
||||
* POST /api/v1/playx/daily-push
|
||||
* POST /api/v1/mall/dailyPush
|
||||
*/
|
||||
public function dailyPush(Request $request): Response
|
||||
{
|
||||
@@ -172,7 +173,7 @@ class Playx extends Api
|
||||
if ($sig === '' || $ts === '' || $rid === '') {
|
||||
return $this->error(__('Daily push signature missing or incomplete'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', json_encode($body));
|
||||
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/mall/dailyPush\n" . hash('sha256', json_encode($body));
|
||||
$expected = hash_hmac('sha256', $canonical, $secret);
|
||||
if (!hash_equals($expected, $sig)) {
|
||||
return $this->error(__('Daily push signature verification failed'), null, 0, ['statusCode' => 401]);
|
||||
@@ -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'] ?? '',
|
||||
@@ -381,7 +382,7 @@ class Playx extends Api
|
||||
|
||||
$token = strval($request->post('token', $request->post('session', $request->get('token', ''))));
|
||||
if ($token === '') {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
if (config('playx.verify_token_local_only', false)) {
|
||||
@@ -409,7 +410,7 @@ class Playx extends Api
|
||||
$data = json_decode(strval($res->getBody()), true);
|
||||
if ($code !== 200 || empty($data['user_id'])) {
|
||||
$remoteMsg = $data['message'] ?? '';
|
||||
$msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Invalid token');
|
||||
$msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Token expiration');
|
||||
|
||||
return $this->error($msg, null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
@@ -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,26 +448,26 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地校验 temLogin 等写入的商城 token(类型 muser),写入 mall_playx_session
|
||||
* 本地校验 temLogin 等写入的商城 token(类型 muser),写入 mall_session
|
||||
*/
|
||||
private function verifyTokenLocal(string $token): Response
|
||||
{
|
||||
$tokenData = Token::get($token);
|
||||
if (empty($tokenData) || (isset($tokenData['expire_time']) && intval($tokenData['expire_time']) <= time())) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
$tokenType = strval($tokenData['type'] ?? '');
|
||||
if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
$assetId = intval($tokenData['user_id'] ?? 0);
|
||||
if ($assetId <= 0) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
$asset = MallPlayxUserAsset::where('id', $assetId)->find();
|
||||
$asset = MallUserAsset::where('id', $assetId)->find();
|
||||
if (!$asset) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
$playxUserId = strval($asset->playx_user_id ?? '');
|
||||
@@ -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 ?? ''),
|
||||
@@ -506,7 +507,7 @@ class Playx extends Api
|
||||
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
$asset = $this->getAssetById($assetId);
|
||||
@@ -545,7 +546,10 @@ class Playx extends Api
|
||||
|
||||
$claimRequestId = strval($request->post('claim_request_id', ''));
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($claimRequestId === '' || $assetId === null) {
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
if ($claimRequestId === '') {
|
||||
return $this->error(__('claim_request_id and user_id/session_id required'));
|
||||
}
|
||||
|
||||
@@ -555,7 +559,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 +579,7 @@ class Playx extends Api
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
MallPlayxClaimLog::create([
|
||||
MallClaimLog::create([
|
||||
'claim_request_id' => $claimRequestId,
|
||||
'user_id' => $playxUserId,
|
||||
'claimed_amount' => $canClaim,
|
||||
@@ -659,14 +663,14 @@ class Playx extends Api
|
||||
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
$asset = $this->getAssetById($assetId);
|
||||
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
|
||||
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 +679,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(__('Token expiration'), 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(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
$phone = trim(strval($request->post('phone', '')));
|
||||
$receiverName = trim(strval($request->post('receiver_name', '')));
|
||||
$region = $request->post('region', '');
|
||||
$detailAddress = trim(strval($request->post('detail_address', '')));
|
||||
$defaultSetting = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
|
||||
|
||||
if ($phone === '' || $receiverName === '' || $detailAddress === '' || $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,
|
||||
'receiver_name' => $receiverName,
|
||||
'phone' => $phone,
|
||||
'region' => $region,
|
||||
'detail_address' => $detailAddress,
|
||||
'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(__('Token expiration'), 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('receiver_name', null) !== null) {
|
||||
$updates['receiver_name'] = trim(strval($request->post('receiver_name', '')));
|
||||
}
|
||||
if ($request->post('region', null) !== null) {
|
||||
$updates['region'] = $request->post('region', '');
|
||||
}
|
||||
if ($request->post('detail_address', null) !== null) {
|
||||
$updates['detail_address'] = trim(strval($request->post('detail_address', '')));
|
||||
}
|
||||
if ($request->post('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(__('Token expiration'), 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 [
|
||||
@@ -705,7 +900,10 @@ class Playx extends Api
|
||||
|
||||
$itemId = intval($request->post('item_id', 0));
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($itemId <= 0 || $assetId === null) {
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
if ($itemId <= 0) {
|
||||
return $this->error(__('item_id and user_id/session_id required'));
|
||||
}
|
||||
|
||||
@@ -732,16 +930,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(),
|
||||
]);
|
||||
@@ -771,11 +969,21 @@ class Playx extends Api
|
||||
}
|
||||
|
||||
$itemId = intval($request->post('item_id', 0));
|
||||
$addressId = intval($request->post('address_id', 0));
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
$receiverName = $request->post('receiver_name', '');
|
||||
$receiverPhone = $request->post('receiver_phone', '');
|
||||
$receiverAddress = $request->post('receiver_address', '');
|
||||
if ($itemId <= 0 || $assetId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
|
||||
if ($itemId <= 0 || $addressId <= 0) {
|
||||
return $this->error(__('Missing required fields'));
|
||||
}
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
|
||||
$addrRow = MallAddress::where('id', $addressId)->where('playx_user_asset_id', $assetId)->find();
|
||||
if (!$addrRow) {
|
||||
return $this->error(__('Shipping address not found'));
|
||||
}
|
||||
$snapshot = MallAddress::snapshotForPhysicalOrder($addrRow);
|
||||
if ($snapshot['receiver_phone'] === '' || $snapshot['receiver_address'] === '' || $snapshot['receiver_name'] === '') {
|
||||
return $this->error(__('Missing required fields'));
|
||||
}
|
||||
|
||||
@@ -798,15 +1006,17 @@ 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,
|
||||
'receiver_phone' => $receiverPhone,
|
||||
'receiver_address' => $receiverAddress,
|
||||
'mall_address_id' => $addressId,
|
||||
'receiver_name' => $snapshot['receiver_name'],
|
||||
'receiver_phone' => $snapshot['receiver_phone'],
|
||||
'receiver_address' => $snapshot['receiver_address'],
|
||||
'grant_status' => MallOrder::GRANT_NOT_APPLICABLE,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
@@ -834,7 +1044,10 @@ class Playx extends Api
|
||||
|
||||
$itemId = intval($request->post('item_id', 0));
|
||||
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||
if ($itemId <= 0 || $assetId === null) {
|
||||
if ($assetId === null) {
|
||||
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||
}
|
||||
if ($itemId <= 0) {
|
||||
return $this->error(__('item_id and user_id/session_id required'));
|
||||
}
|
||||
|
||||
@@ -861,16 +1074,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_APPLICABLE,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
@@ -881,18 +1094,13 @@ class Playx extends Api
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
$baseUrl = config('playx.api.base_url', '');
|
||||
if ($baseUrl !== '') {
|
||||
$this->callPlayxBalanceCredit($order, $playxUserId);
|
||||
}
|
||||
|
||||
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [
|
||||
'order_id' => $order->id,
|
||||
'status' => 'PENDING',
|
||||
]);
|
||||
}
|
||||
|
||||
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,53 +1125,18 @@ 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
|
||||
{
|
||||
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
|
||||
$url = config('playx.api.balance_credit_url', '/api/v1/balance/credit');
|
||||
if ($baseUrl === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = new \GuzzleHttp\Client(['timeout' => 15]);
|
||||
$res = $client->post($baseUrl . $url, [
|
||||
'json' => [
|
||||
'request_id' => 'mall_withdraw_' . uniqid(),
|
||||
'externalTransactionId' => $order->external_transaction_id,
|
||||
'user_id' => $userId,
|
||||
'amount' => $order->amount,
|
||||
'multiplier' => $order->multiplier,
|
||||
],
|
||||
]);
|
||||
$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->save();
|
||||
} else {
|
||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
||||
$order->fail_reason = $data['message'] ?? 'unknown';
|
||||
$order->save();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
||||
$order->fail_reason = $e->getMessage();
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ return [
|
||||
'Temp login is disabled' => 'Temp login is disabled',
|
||||
'Failed to create temp account' => 'Failed to allocate a unique phone number, please retry later',
|
||||
'Parameter username can not be empty' => 'Parameter username can not be empty',
|
||||
'Token expiration' => 'Session expired, please login again.',
|
||||
// Member center account
|
||||
'Data updated successfully~' => 'Data updated successfully~',
|
||||
'Password has been changed~' => 'Password has been changed~',
|
||||
@@ -49,6 +50,7 @@ return [
|
||||
'Insufficient points' => 'Insufficient points',
|
||||
'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes',
|
||||
'Missing required fields' => 'Missing required fields',
|
||||
'Shipping address not found' => 'Shipping address not found',
|
||||
'Out of stock' => 'Out of stock',
|
||||
'Redeem success' => 'Redeem successful',
|
||||
'Withdraw submitted, please wait about 10 minutes' => 'Withdrawal submitted, please wait about 10 minutes',
|
||||
|
||||
@@ -82,6 +82,8 @@ return [
|
||||
'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟',
|
||||
'Missing required fields' => '缺少必填字段',
|
||||
'Out of stock' => '库存不足',
|
||||
'Record not found' => '记录不存在',
|
||||
'Shipping address not found' => '收货地址不存在',
|
||||
'Redeem success' => '兑换成功',
|
||||
'Withdraw submitted, please wait about 10 minutes' => '提现申请已提交,请等待约 10 分钟',
|
||||
];
|
||||
85
app/common/library/MallBonusGrantPush.php
Normal file
85
app/common/library/MallBonusGrantPush.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
use app\common\model\MallItem;
|
||||
use app\common\model\MallOrder;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
/**
|
||||
* 红利订单调用 PlayX bonus/grant(与定时任务、后台手动推送共用)
|
||||
*/
|
||||
final class MallBonusGrantPush
|
||||
{
|
||||
/**
|
||||
* @return array{ok: bool, message: string, playx_transaction_id: string}
|
||||
*/
|
||||
public static function push(MallOrder $order): array
|
||||
{
|
||||
$baseUrl = rtrim(strval(config('playx.api.base_url', '')), '/');
|
||||
if ($baseUrl === '') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'PlayX base_url not configured',
|
||||
'playx_transaction_id' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$path = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
|
||||
$url = $baseUrl . $path;
|
||||
|
||||
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||||
$rewardName = $item ? strval($item->title) : '';
|
||||
$category = $item ? strval($item->category) : 'daily';
|
||||
$categoryTitle = $item ? strval($item->category_title) : '';
|
||||
$multiplier = intval($order->multiplier ?? 0);
|
||||
if ($multiplier <= 0) {
|
||||
$multiplier = 1;
|
||||
}
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => 20,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
|
||||
$requestId = 'mall_bonus_' . uniqid();
|
||||
|
||||
try {
|
||||
$res = $client->post($url, [
|
||||
'json' => [
|
||||
'request_id' => $requestId,
|
||||
'externalTransactionId' => $order->external_transaction_id,
|
||||
'user_id' => $order->user_id,
|
||||
'amount' => $order->amount,
|
||||
'rewardName' => $rewardName,
|
||||
'category' => $category,
|
||||
'categoryTitle' => $categoryTitle,
|
||||
'multiplier' => $multiplier,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode(strval($res->getBody()), true) ?? [];
|
||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => '',
|
||||
'playx_transaction_id' => strval($data['playx_transaction_id'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => strval($data['message'] ?? 'PlayX bonus grant not accepted'),
|
||||
'playx_transaction_id' => '',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'playx_transaction_id' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,12 +41,46 @@ class MallAddress extends Model
|
||||
public function getregionTextAttr($value, $row): string
|
||||
{
|
||||
if ($row['region'] === '' || $row['region'] === null) return '';
|
||||
$cityNames = \support\think\Db::name('area')->whereIn('id', $row['region'])->column('name');
|
||||
$region = $row['region'];
|
||||
$ids = $region;
|
||||
if (!is_array($ids)) {
|
||||
$ids = explode(',', (string) $ids);
|
||||
}
|
||||
$ids = array_values(array_filter(array_map('trim', $ids), static function ($s) {
|
||||
return $s !== '';
|
||||
}));
|
||||
if (empty($ids)) {
|
||||
return '';
|
||||
}
|
||||
$cityNames = \support\think\Db::name('area')->whereIn('id', $ids)->column('name');
|
||||
return $cityNames ? implode(',', $cityNames) : '';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 实物订单收货快照(写入 mall_order.receiver_*,与 mall_address 当前内容一致)
|
||||
*
|
||||
* @return array{receiver_name: string, receiver_phone: string, receiver_address: string}
|
||||
*/
|
||||
public static function snapshotForPhysicalOrder(self $addr): array
|
||||
{
|
||||
$regionText = $addr->region_text ?? '';
|
||||
$parts = array_filter([
|
||||
trim($regionText),
|
||||
trim($addr->detail_address ?? ''),
|
||||
], static function ($s) {
|
||||
return $s !== '';
|
||||
});
|
||||
$receiverAddress = implode(' ', $parts);
|
||||
|
||||
return [
|
||||
'receiver_name' => trim($addr->receiver_name ?? ''),
|
||||
'receiver_phone' => trim($addr->phone ?? ''),
|
||||
'receiver_address' => $receiverAddress,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace app\common\model;
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* PlayX 统一订单
|
||||
* 统一订单
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $user_id
|
||||
@@ -28,10 +28,11 @@ use support\think\Model;
|
||||
* @property string $receiver_name
|
||||
* @property string $receiver_phone
|
||||
* @property string|null $receiver_address
|
||||
* @property int|null $mall_address_id
|
||||
*/
|
||||
class MallPlayxOrder extends Model
|
||||
class MallOrder extends Model
|
||||
{
|
||||
protected string $name = 'mall_playx_order';
|
||||
protected string $name = 'mall_order';
|
||||
|
||||
protected bool $autoWriteTimestamp = true;
|
||||
|
||||
@@ -50,17 +51,27 @@ class MallPlayxOrder extends Model
|
||||
public const GRANT_FAILED_RETRYABLE = 'FAILED_RETRYABLE';
|
||||
public const GRANT_FAILED_FINAL = 'FAILED_FINAL';
|
||||
|
||||
/** 非红利订单不参与 PlayX/Angpow 推送,固定为该占位值 */
|
||||
public const GRANT_NOT_APPLICABLE = '---';
|
||||
|
||||
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',
|
||||
'mall_address_id' => 'integer',
|
||||
];
|
||||
|
||||
public function mallItem(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MallItem::class, 'mall_item_id', 'id');
|
||||
}
|
||||
|
||||
public function mallAddress(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MallAddress::class, 'mall_address_id', 'id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use ba\Random;
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* PlayX 用户资产(积分商城用户主表,含登录账号字段)
|
||||
*/
|
||||
class MallPlayxUserAsset extends Model
|
||||
{
|
||||
protected string $name = 'mall_playx_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',
|
||||
];
|
||||
|
||||
/**
|
||||
* H5 临时登录:按用户名查找或创建资产行,playx_user_id 使用 mall_{id}
|
||||
*/
|
||||
public static function ensureForUsername(string $username): self
|
||||
{
|
||||
$username = trim($username);
|
||||
$existing = self::where('username', $username)->find();
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$phone = self::allocateUniquePhone();
|
||||
if ($phone === null) {
|
||||
throw new \RuntimeException('Failed to allocate unique phone');
|
||||
}
|
||||
|
||||
$pwd = hash_password(Random::build('alnum', 16));
|
||||
$now = time();
|
||||
$temporaryPlayxId = 'tmp_' . bin2hex(random_bytes(16));
|
||||
$created = self::create([
|
||||
'playx_user_id' => $temporaryPlayxId,
|
||||
'username' => $username,
|
||||
'phone' => $phone,
|
||||
'password' => $pwd,
|
||||
'admin_id' => 0,
|
||||
'locked_points' => 0,
|
||||
'available_points' => 0,
|
||||
'today_limit' => 0,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => null,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
if (!$created) {
|
||||
throw new \RuntimeException('Failed to create mall_playx_user_asset');
|
||||
}
|
||||
|
||||
$id = intval($created->getKey());
|
||||
$finalPlayxId = 'mall_' . $id;
|
||||
if (self::where('playx_user_id', $finalPlayxId)->where('id', '<>', $id)->find()) {
|
||||
$finalPlayxId = 'mall_' . $id . '_' . bin2hex(random_bytes(4));
|
||||
}
|
||||
$created->playx_user_id = $finalPlayxId;
|
||||
$created->save();
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
private static function allocateUniquePhone(): ?string
|
||||
{
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
|
||||
if (!self::where('phone', $candidate)->find()) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
24
app/common/model/MallSession.php
Normal file
24
app/common/model/MallSession.php
Normal 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',
|
||||
];
|
||||
}
|
||||
|
||||
85
app/common/model/MallUserAsset.php
Normal file
85
app/common/model/MallUserAsset.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use ba\Random;
|
||||
use support\think\Model;
|
||||
|
||||
/**
|
||||
* 用户资产(积分商城用户主表,含登录账号字段)
|
||||
*/
|
||||
class MallUserAsset extends Model
|
||||
{
|
||||
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',
|
||||
];
|
||||
|
||||
/**
|
||||
* H5 临时登录:按用户名查找或创建资产行,playx_user_id 使用 mall_{id}
|
||||
*/
|
||||
public static function ensureForUsername(string $username): self
|
||||
{
|
||||
$username = trim($username);
|
||||
$existing = self::where('username', $username)->find();
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
// 创建用户时:phone 与 username 同值(H5 临时账号)
|
||||
$phone = $username;
|
||||
$phoneExisting = self::where('phone', $phone)->find();
|
||||
if ($phoneExisting) {
|
||||
// 若历史数据存在“手机号=用户名”的行,直接复用;若不一致则拒绝创建,避免污染
|
||||
if (trim((string) ($phoneExisting->username ?? '')) === $username) {
|
||||
return $phoneExisting;
|
||||
}
|
||||
throw new \RuntimeException('Username is already used by another account');
|
||||
}
|
||||
|
||||
$pwd = hash_password(Random::build('alnum', 16));
|
||||
$now = time();
|
||||
$temporaryPlayxId = 'tmp_' . bin2hex(random_bytes(16));
|
||||
$created = self::create([
|
||||
'playx_user_id' => $temporaryPlayxId,
|
||||
'username' => $username,
|
||||
'phone' => $phone,
|
||||
'password' => $pwd,
|
||||
'admin_id' => 0,
|
||||
'locked_points' => 0,
|
||||
'available_points' => 0,
|
||||
'today_limit' => 0,
|
||||
'today_claimed' => 0,
|
||||
'today_limit_date' => null,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
if (!$created) {
|
||||
throw new \RuntimeException('Failed to create mall_user_asset');
|
||||
}
|
||||
|
||||
$id = $created->getKey();
|
||||
$finalPlayxId = 'mall_' . $id;
|
||||
if (self::where('playx_user_id', $finalPlayxId)->where('id', '<>', $id)->find()) {
|
||||
$finalPlayxId = 'mall_' . $id . '_' . bin2hex(random_bytes(4));
|
||||
}
|
||||
$created->playx_user_id = $finalPlayxId;
|
||||
$created->save();
|
||||
|
||||
return $created;
|
||||
}
|
||||
|
||||
// allocateUniquePhone 已废弃:临时登录场景下 phone=用户名
|
||||
}
|
||||
|
||||
328
app/process/AngpowImportJobs.php
Normal file
328
app/process/AngpowImportJobs.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\common\model\MallItem;
|
||||
use app\common\model\MallOrder;
|
||||
use app\common\model\MallUserAsset;
|
||||
use GuzzleHttp\Client;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Angpow 导入推送任务
|
||||
* - 数据源:mall_order(type=BONUS)
|
||||
* - 推送频率:每 30 秒
|
||||
* - 批量:每次最多 100 条(对方文档限制)
|
||||
* - 幂等:merchant_code + report_date 级别签名;每条订单通过 external_transaction_id 在本地控制只推送一次
|
||||
*/
|
||||
class AngpowImportJobs
|
||||
{
|
||||
private const TIMER_SECONDS = 30;
|
||||
private const BATCH_LIMIT = 100;
|
||||
private const MAX_RETRY = 3;
|
||||
|
||||
protected Client $http;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 确保定时任务只在一个 worker 上运行
|
||||
if (!Worker::getAllWorkers()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->http = new Client($this->buildGuzzleOptions());
|
||||
|
||||
Timer::add(self::TIMER_SECONDS, [$this, 'pushPendingOrders']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guzzle 默认校验 HTTPS;Windows 未配置 CA 时会出现 cURL error 60。
|
||||
* 优先使用 PLAYX_ANGPOW_IMPORT_CACERT 指向 cacert.pem;否则可按环境关闭校验(仅开发)。
|
||||
*/
|
||||
private function buildGuzzleOptions(): array
|
||||
{
|
||||
$options = [
|
||||
'timeout' => 20,
|
||||
'http_errors' => false,
|
||||
];
|
||||
|
||||
$conf = config('playx.angpow_import');
|
||||
if (!is_array($conf)) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
$caFile = $conf['ca_file'] ?? '';
|
||||
if (is_string($caFile) && $caFile !== '' && is_file($caFile)) {
|
||||
$options['verify'] = $caFile;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
$verifySsl = $conf['verify_ssl'] ?? true;
|
||||
if ($verifySsl === false) {
|
||||
$options['verify'] = false;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function pushPendingOrders(): void
|
||||
{
|
||||
$conf = config('playx.angpow_import');
|
||||
if (!is_array($conf)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$baseUrl = $conf['base_url'] ?? '';
|
||||
$path = $conf['path'] ?? '';
|
||||
$merchantCode = $conf['merchant_code'] ?? '';
|
||||
$authKey = $conf['auth_key'] ?? '';
|
||||
|
||||
if (!is_string($baseUrl) || $baseUrl === '' || !is_string($path) || $path === '' || !is_string($merchantCode) || $merchantCode === '') {
|
||||
return;
|
||||
}
|
||||
if (!is_string($authKey) || $authKey === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = rtrim($baseUrl, '/') . $path;
|
||||
|
||||
$orders = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||
->whereIn('grant_status', [MallOrder::GRANT_NOT_SENT, MallOrder::GRANT_FAILED_RETRYABLE])
|
||||
->where('status', MallOrder::STATUS_PENDING)
|
||||
->where('retry_count', '<', self::MAX_RETRY)
|
||||
->order('id', 'asc')
|
||||
->limit(self::BATCH_LIMIT)
|
||||
->select();
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reportDate = strval(time());
|
||||
$signatureInput = 'merchant_code=' . $merchantCode . '&report_date=' . $reportDate;
|
||||
$signature = $this->buildSignature($signatureInput, $authKey);
|
||||
if ($signature === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'merchant_code' => $merchantCode,
|
||||
'report_date' => $reportDate,
|
||||
'angpow' => [],
|
||||
'currency_visual' => [
|
||||
[
|
||||
'currency' => strval($conf['currency'] ?? 'MYR'),
|
||||
'visual_name' => strval($conf['visual_name'] ?? 'Angpow'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$orderIds = [];
|
||||
foreach ($orders as $order) {
|
||||
if (!$order instanceof MallOrder) {
|
||||
continue;
|
||||
}
|
||||
$row = $this->buildAngpowRow($order);
|
||||
if ($row === null) {
|
||||
// 构造失败:直接标为可重试失败
|
||||
$this->markFailedAttempt($order, 'Build payload failed');
|
||||
continue;
|
||||
}
|
||||
$payload['angpow'][] = $row;
|
||||
$orderIds[] = $order->id;
|
||||
}
|
||||
|
||||
if (empty($payload['angpow'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 先标记“已尝试发送”,避免并发重复推送;同时只在这里累加 retry_count(一次推送=一次尝试)
|
||||
$now = time();
|
||||
foreach ($orders as $order) {
|
||||
if (!$order instanceof MallOrder) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($order->id, $orderIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$retry = $order->retry_count ?? 0;
|
||||
if (!is_int($retry)) {
|
||||
$retry = is_numeric($retry) ? intval($retry) : 0;
|
||||
}
|
||||
$order->retry_count = $retry + 1;
|
||||
$order->grant_status = MallOrder::GRANT_SENT_PENDING;
|
||||
$order->update_time = $now;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
$res = null;
|
||||
$body = '';
|
||||
try {
|
||||
$res = $this->http->post($url, [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Request-Signature' => $signature,
|
||||
],
|
||||
'json' => $payload,
|
||||
]);
|
||||
$body = strval($res->getBody());
|
||||
} catch (\Throwable $e) {
|
||||
// 网络/异常:对这一批订单记一次失败尝试
|
||||
foreach ($orders as $order) {
|
||||
if (!$order instanceof MallOrder) {
|
||||
continue;
|
||||
}
|
||||
$this->markFailedAttempt($order, $e->getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$data = json_decode($body, true);
|
||||
if (!is_array($data)) {
|
||||
foreach ($orders as $order) {
|
||||
if (!$order instanceof MallOrder) {
|
||||
continue;
|
||||
}
|
||||
$this->markFailedAttempt($order, 'Invalid response');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$code = $data['code'] ?? null;
|
||||
$message = $data['message'] ?? '';
|
||||
$msg = is_string($message) ? $message : 'Request failed';
|
||||
|
||||
// 成功:code=0
|
||||
if ($code === '0' || $code === 0) {
|
||||
MallOrder::whereIn('id', $orderIds)->update([
|
||||
'grant_status' => MallOrder::GRANT_ACCEPTED,
|
||||
'fail_reason' => null,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 失败:整批视为失败(对方未提供逐条返回)
|
||||
foreach ($orders as $order) {
|
||||
if (!$order instanceof MallOrder) {
|
||||
continue;
|
||||
}
|
||||
$this->markFailedAttempt($order, $msg);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildAngpowRow(MallOrder $order): ?array
|
||||
{
|
||||
$asset = MallUserAsset::where('playx_user_id', $order->user_id)->find();
|
||||
if (!$asset) {
|
||||
if (is_string($order->user_id) && ctype_digit($order->user_id)) {
|
||||
$byId = MallUserAsset::where('id', $order->user_id)->find();
|
||||
if ($byId) {
|
||||
$asset = $byId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$asset || !is_string($asset->playx_user_id ?? null) || strval($asset->playx_user_id) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = null;
|
||||
if ($order->mallItem) {
|
||||
$item = $order->mallItem;
|
||||
} else {
|
||||
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||||
}
|
||||
if (!$item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$createTime = $order->create_time ?? null;
|
||||
if (!is_int($createTime)) {
|
||||
if (is_numeric($createTime)) {
|
||||
$createTime = intval($createTime);
|
||||
} else {
|
||||
$createTime = time();
|
||||
}
|
||||
}
|
||||
|
||||
$start = gmdate('Y-m-d\TH:i:s\Z', $createTime);
|
||||
$end = gmdate('Y-m-d\TH:i:s\Z', $createTime + 86400);
|
||||
|
||||
return [
|
||||
'member_login' => strval($asset->playx_user_id),
|
||||
'start_time' => $start,
|
||||
'end_time' => $end,
|
||||
'amount' => $order->amount,
|
||||
'reward_name' => strval($item->title ?? ''),
|
||||
'description' => strval($item->description ?? ''),
|
||||
'member_inbox_message' => 'Congratulations! You received an angpow.',
|
||||
'category' => strval($item->category ?? ''),
|
||||
'category_title' => strval($item->category_title ?? ''),
|
||||
'one_time_turnover' => 'yes',
|
||||
'multiplier' => $order->multiplier,
|
||||
];
|
||||
}
|
||||
|
||||
private function markFailedAttempt(MallOrder $order, string $reason): void
|
||||
{
|
||||
// retry_count 在“准备发送”阶段已 +1;此处用当前 retry_count 作为 attempt 编号
|
||||
$retryCount = $order->retry_count ?? 0;
|
||||
$attempt = is_int($retryCount) ? $retryCount : (is_numeric($retryCount) ? intval($retryCount) : 0);
|
||||
if ($attempt <= 0) {
|
||||
$attempt = 1;
|
||||
$order->retry_count = 1;
|
||||
}
|
||||
|
||||
$prev = $order->fail_reason;
|
||||
$prefix = 'attempt ' . $attempt . ': ';
|
||||
$line = $prefix . $reason;
|
||||
$newReason = $line;
|
||||
if (is_string($prev) && $prev !== '') {
|
||||
$newReason = $prev . "\n" . $line;
|
||||
}
|
||||
|
||||
$final = $attempt >= self::MAX_RETRY;
|
||||
$order->grant_status = $final ? MallOrder::GRANT_FAILED_FINAL : MallOrder::GRANT_FAILED_RETRYABLE;
|
||||
$order->fail_reason = $newReason;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成对方要求的 Base64(HMAC-SHA1)
|
||||
* - 文档中示例 python 会 base64_decode(key) 后参与 hmac
|
||||
* - 生产 key 由 BA 提供,可能是 base64 或 hex;这里做兼容处理
|
||||
*/
|
||||
private function buildSignature(string $input, string $authKey): ?string
|
||||
{
|
||||
$keyBytes = null;
|
||||
|
||||
$maybeBase64 = base64_decode($authKey, true);
|
||||
if ($maybeBase64 !== false && $maybeBase64 !== '') {
|
||||
$keyBytes = $maybeBase64;
|
||||
}
|
||||
|
||||
if ($keyBytes === null) {
|
||||
$isHex = ctype_xdigit($authKey) && (strlen($authKey) % 2 === 0);
|
||||
if ($isHex) {
|
||||
$hex = hex2bin($authKey);
|
||||
if ($hex !== false && $hex !== '') {
|
||||
$keyBytes = $hex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($keyBytes === null) {
|
||||
$keyBytes = $authKey;
|
||||
}
|
||||
|
||||
$raw = hash_hmac('sha1', $input, $keyBytes, true);
|
||||
if (!is_string($raw) || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64_encode($raw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\common\model\MallItem;
|
||||
use app\common\model\MallPlayxOrder;
|
||||
use app\common\model\MallPlayxUserAsset;
|
||||
use app\common\library\MallBonusGrantPush;
|
||||
use app\common\model\MallOrder;
|
||||
use app\common\model\MallUserAsset;
|
||||
use GuzzleHttp\Client;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
@@ -47,14 +47,15 @@ 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('type', MallOrder::TYPE_BONUS)
|
||||
->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 +66,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);
|
||||
@@ -98,24 +99,20 @@ class PlayxJobs
|
||||
return;
|
||||
}
|
||||
|
||||
$bonusPath = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
|
||||
$withdrawPath = strval(config('playx.api.balance_credit_url', '/api/v1/balance/credit'));
|
||||
$bonusUrl = rtrim($baseUrl, '/') . $bonusPath;
|
||||
$withdrawUrl = rtrim($baseUrl, '/') . $withdrawPath;
|
||||
|
||||
$maxRetry = 3;
|
||||
$list = MallPlayxOrder::whereIn('grant_status', [
|
||||
MallPlayxOrder::GRANT_NOT_SENT,
|
||||
MallPlayxOrder::GRANT_FAILED_RETRYABLE,
|
||||
$list = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||
->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;
|
||||
@@ -124,25 +121,23 @@ class PlayxJobs
|
||||
$order->retry_count = intval($order->retry_count ?? 0) + 1;
|
||||
|
||||
try {
|
||||
$this->sendGrantByOrder($order, $bonusUrl, $withdrawUrl, $maxRetry);
|
||||
$this->sendGrantByOrder($order, $maxRetry);
|
||||
} 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->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,107 +162,39 @@ class PlayxJobs
|
||||
return false;
|
||||
}
|
||||
|
||||
private function sendGrantByOrder(MallPlayxOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void
|
||||
private function sendGrantByOrder(MallOrder $order, int $maxRetry): void
|
||||
{
|
||||
$item = null;
|
||||
if ($order->mallItem) {
|
||||
$item = $order->mallItem;
|
||||
} else {
|
||||
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||||
}
|
||||
|
||||
if ($order->type === MallPlayxOrder::TYPE_BONUS) {
|
||||
$rewardName = $item ? strval($item->title) : '';
|
||||
$category = $item ? strval($item->category) : 'daily';
|
||||
$categoryTitle = $item ? strval($item->category_title) : '';
|
||||
$multiplier = intval($order->multiplier ?? 0);
|
||||
if ($multiplier <= 0) {
|
||||
$multiplier = 1;
|
||||
}
|
||||
|
||||
$requestId = 'mall_retry_bonus_' . uniqid();
|
||||
$res = $this->http->post($bonusUrl, [
|
||||
'json' => [
|
||||
'request_id' => $requestId,
|
||||
'externalTransactionId' => $order->external_transaction_id,
|
||||
'user_id' => $order->user_id,
|
||||
'amount' => $order->amount,
|
||||
'rewardName' => $rewardName,
|
||||
'category' => $category,
|
||||
'categoryTitle' => $categoryTitle,
|
||||
'multiplier' => $multiplier,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode(strval($res->getBody()), true) ?? [];
|
||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
||||
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
|
||||
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
|
||||
$order->save();
|
||||
return;
|
||||
}
|
||||
|
||||
$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->save();
|
||||
$this->refundPoints($order);
|
||||
return;
|
||||
}
|
||||
|
||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
||||
$order->save();
|
||||
if ($order->type !== MallOrder::TYPE_BONUS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($order->type === MallPlayxOrder::TYPE_WITHDRAW) {
|
||||
$multiplier = intval($order->multiplier ?? 0);
|
||||
if ($multiplier <= 0) {
|
||||
$multiplier = 1;
|
||||
}
|
||||
$requestId = 'mall_retry_withdraw_' . uniqid();
|
||||
$res = $this->http->post($withdrawUrl, [
|
||||
'json' => [
|
||||
'request_id' => $requestId,
|
||||
'externalTransactionId' => $order->external_transaction_id,
|
||||
'user_id' => $order->user_id,
|
||||
'amount' => $order->amount,
|
||||
'multiplier' => $multiplier,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode(strval($res->getBody()), true) ?? [];
|
||||
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
|
||||
$order->grant_status = MallPlayxOrder::GRANT_ACCEPTED;
|
||||
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
|
||||
$order->save();
|
||||
return;
|
||||
}
|
||||
|
||||
$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->save();
|
||||
$this->refundPoints($order);
|
||||
return;
|
||||
}
|
||||
|
||||
$order->grant_status = MallPlayxOrder::GRANT_FAILED_RETRYABLE;
|
||||
$result = MallBonusGrantPush::push($order);
|
||||
if ($result['ok']) {
|
||||
$order->grant_status = MallOrder::GRANT_ACCEPTED;
|
||||
$order->playx_transaction_id = $result['playx_transaction_id'];
|
||||
$order->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// PHYSICAL 目前由后台手工发货/驳回,不参与 PlayX 发放重试
|
||||
$order->fail_reason = $result['message'];
|
||||
if (intval($order->retry_count) >= $maxRetry) {
|
||||
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
|
||||
$order->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"nelexa/zip": "^4.0.0",
|
||||
"voku/anti-xss": "^4.1",
|
||||
"topthink/think-validate": "^3.0",
|
||||
"firebase/php-jwt": "^7.0"
|
||||
"firebase/php-jwt": "^7.0",
|
||||
"guzzlehttp/guzzle": "^7.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
// Daily Push 签名校验(PlayX 调用商城时使用)
|
||||
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')),
|
||||
/**
|
||||
* 合作方 JWT 验签密钥(HS256)。非空时:daily-push 等回调需带 Authorization: Bearer
|
||||
* 合作方 JWT 验签密钥(HS256)。非空时:dailyPush 等回调需带 Authorization: Bearer
|
||||
* 仅写入部署环境变量,勿提交仓库。
|
||||
*/
|
||||
'partner_jwt_secret' => strval(env('PLAYX_PARTNER_JWT_SECRET', '')),
|
||||
@@ -33,4 +33,22 @@ return [
|
||||
'balance_credit_url' => '/api/v1/balance/credit',
|
||||
'transaction_status_url' => '/api/v1/transaction/status',
|
||||
],
|
||||
// Angpow Import API(商城调用对方 cashmarket/merchant-api 时使用)
|
||||
'angpow_import' => [
|
||||
// 对方 base_url,例如 https://ss2-staging2.ttwd8.com
|
||||
'base_url' => strval(env('PLAYX_ANGPOW_IMPORT_BASE_URL', '')),
|
||||
// 路径:文档示例为 /api/v3/merchant/angpow-imports;对方 curl 示例为 /cashmarket/v3/merchant-api/angpow-imports
|
||||
'path' => strval(env('PLAYX_ANGPOW_IMPORT_PATH', '/api/v3/merchant/angpow-imports')),
|
||||
// merchant_code(固定 plx 或按环境配置)
|
||||
'merchant_code' => strval(env('PLAYX_ANGPOW_MERCHANT_CODE', 'plx')),
|
||||
// HMAC-SHA1 的 auth key(生产环境由 BA 提供)
|
||||
'auth_key' => strval(env('PLAYX_ANGPOW_IMPORT_AUTH_KEY', '')),
|
||||
// HTTPS:指定 CA 证书包路径(推荐,下载 https://curl.se/ca/cacert.pem 后填绝对路径,可避免 cURL error 60)
|
||||
'ca_file' => strval(env('PLAYX_ANGPOW_IMPORT_CACERT', '')),
|
||||
// 是否校验 SSL(生产必须为 true;本地无 CA 时可临时 false,勿用于生产)
|
||||
'verify_ssl' => filter_var(env('PLAYX_ANGPOW_IMPORT_VERIFY_SSL', '1'), FILTER_VALIDATE_BOOLEAN),
|
||||
// 固定货币展示映射
|
||||
'currency' => 'MYR',
|
||||
'visual_name' => 'Angpow',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
use app\process\Http;
|
||||
use app\process\AngpowImportJobs;
|
||||
|
||||
global $argv;
|
||||
|
||||
@@ -65,4 +66,9 @@ return [
|
||||
'handler' => app\process\PlayxJobs::class,
|
||||
'reloadable' => false,
|
||||
],
|
||||
// Angpow 导入推送任务:订单兑换后推送到对方平台
|
||||
'angpow_import_jobs' => [
|
||||
'handler' => AngpowImportJobs::class,
|
||||
'reloadable' => false,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -113,15 +113,19 @@ Route::get('/api/v1/authToken', [\app\api\controller\v1\Auth::class, 'authToken'
|
||||
Route::get('/api/v1/temLogin', [\app\api\controller\v1\Auth::class, 'temLogin']);
|
||||
|
||||
// api/v1 PlayX 积分商城
|
||||
Route::post('/api/v1/playx/daily-push', [\app\api\controller\v1\Playx::class, 'dailyPush']);
|
||||
Route::post('/api/v1/playx/verify-token', [\app\api\controller\v1\Playx::class, 'verifyToken']);
|
||||
Route::get('/api/v1/playx/assets', [\app\api\controller\v1\Playx::class, 'assets']);
|
||||
Route::post('/api/v1/playx/claim', [\app\api\controller\v1\Playx::class, 'claim']);
|
||||
Route::get('/api/v1/playx/items', [\app\api\controller\v1\Playx::class, 'items']);
|
||||
Route::post('/api/v1/playx/bonus/redeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']);
|
||||
Route::post('/api/v1/playx/physical/redeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
||||
Route::post('/api/v1/playx/withdraw/apply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
|
||||
Route::get('/api/v1/playx/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
||||
Route::post('/api/v1/mall/dailyPush', [\app\api\controller\v1\Playx::class, 'dailyPush']);
|
||||
Route::post('/api/v1/mall/verifyToken', [\app\api\controller\v1\Playx::class, 'verifyToken']);
|
||||
Route::get('/api/v1/mall/assets', [\app\api\controller\v1\Playx::class, 'assets']);
|
||||
Route::post('/api/v1/mall/claim', [\app\api\controller\v1\Playx::class, 'claim']);
|
||||
Route::get('/api/v1/mall/items', [\app\api\controller\v1\Playx::class, 'items']);
|
||||
Route::post('/api/v1/mall/bonusRedeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']);
|
||||
Route::post('/api/v1/mall/physicalRedeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
||||
Route::post('/api/v1/mall/withdrawApply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
|
||||
Route::get('/api/v1/mall/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
||||
Route::get('/api/v1/mall/addressList', [\app\api\controller\v1\Playx::class, 'addressList']);
|
||||
Route::post('/api/v1/mall/addressAdd', [\app\api\controller\v1\Playx::class, 'addressAdd']);
|
||||
Route::post('/api/v1/mall/addressEdit', [\app\api\controller\v1\Playx::class, 'addressEdit']);
|
||||
Route::post('/api/v1/mall/addressDelete', [\app\api\controller\v1\Playx::class, 'addressDelete']);
|
||||
|
||||
// ==================== Admin 路由 ====================
|
||||
// Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容
|
||||
|
||||
205
docs/H5-积分商城接口文档.md
Normal file
205
docs/H5-积分商城接口文档.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# H5 积分商城接口文档(含流程说明)
|
||||
|
||||
> 面向:H5(活动页/积分商城前台)调用
|
||||
> 基础路径:`/api/v1`
|
||||
> 返回结构:BuildAdmin 通用 `code/msg/time/data`(成功 `code=1`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体流程说明
|
||||
|
||||
### 1.1 流程 A:H5 临时登录(推荐)
|
||||
|
||||
适用场景:H5 只需要“用户名级别”的轻量登录,不依赖 PlayX 的 token。
|
||||
|
||||
1. H5 调用 `GET/POST /api/v1/temLogin?username=xxx` 获取 **商城 token**(类型 `muser`)
|
||||
2. H5 后续请求统一携带该 token(推荐放在 Header:`token: <muser_token>`,也可用参数 `token`)
|
||||
3. H5 调用:
|
||||
- `GET /api/v1/mall/assets` 获取资产
|
||||
- `POST /api/v1/mall/claim` 领取积分(幂等)
|
||||
- `GET /api/v1/mall/items` 获取商品
|
||||
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply` 提交兑换/提现
|
||||
- `GET /api/v1/mall/orders` 查询订单
|
||||
- `GET/POST /api/v1/mall/address*` 管理地址(addressList/addressAdd/addressEdit/addressDelete)
|
||||
|
||||
### 1.2 流程 B:PlayX token 换取 session(兼容)
|
||||
|
||||
适用场景:H5 已经拿到了 PlayX 下发的 token,希望换取商城侧 `session_id`。
|
||||
|
||||
1. H5 调用 `POST /api/v1/mall/verifyToken`(传 `token` 或 `session`)
|
||||
2. 服务端返回 `data.session_id`
|
||||
3. H5 后续请求携带 `session_id`(优先级高于 token)
|
||||
|
||||
---
|
||||
|
||||
## 2. 身份与鉴权(重要)
|
||||
|
||||
以下接口都会通过服务端逻辑解析“当前资产主体”,优先级如下:
|
||||
|
||||
1. **`session_id`**(GET/POST):对应表 `mall_session`,未过期则可映射到资产主体
|
||||
2. **`token`**(GET/POST 或 Header):支持会员 token 或 `muser` token(H5 临时登录签发)
|
||||
3. **`user_id`**(GET/POST):
|
||||
- 纯数字:视为 `mall_user_asset.id`
|
||||
- 非纯数字:按 `mall_user_asset.playx_user_id` 查找
|
||||
|
||||
推荐做法:
|
||||
- H5 统一只用 **Header `token`**(值为 `muser` token),避免 URL 泄露与参数歧义。
|
||||
|
||||
---
|
||||
|
||||
## 3. 接口列表(H5 常用)
|
||||
|
||||
### 3.1 H5 临时登录
|
||||
|
||||
**GET/POST** ` /api/v1/temLogin `
|
||||
|
||||
参数:
|
||||
- `username`:必填,用户名(字符串)
|
||||
|
||||
成功返回 `data.userInfo`:
|
||||
- `id`:资产主键(`mall_user_asset.id`)
|
||||
- `username`:用户名
|
||||
- `playx_user_id`:映射的 PlayX 用户标识(字符串)
|
||||
- `token`:**muser token**(后续请求使用)
|
||||
- `refresh_token`:刷新 token(当前前端未强依赖可不接)
|
||||
- `expires_in`:秒
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
curl "https://{域名}/api/v1/temLogin?username=test001"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 资产查询
|
||||
|
||||
**GET** ` /api/v1/mall/assets `
|
||||
|
||||
鉴权:携带 `token` 或 `session_id` 或 `user_id`
|
||||
|
||||
成功返回 `data`:
|
||||
- `locked_points`:待领取积分
|
||||
- `available_points`:可用积分
|
||||
- `today_limit`:今日可领取上限
|
||||
- `today_claimed`:今日已领取
|
||||
- `withdrawable_cash`:可提现现金(积分按配置比例换算)
|
||||
|
||||
---
|
||||
|
||||
### 3.3 领取积分(幂等)
|
||||
|
||||
**POST** ` /api/v1/mall/claim `
|
||||
|
||||
参数:
|
||||
- `claim_request_id`:必填,幂等键(建议:`{业务前缀}_{assetId}_{毫秒时间戳}`)
|
||||
- 身份参数:`token` / `session_id` / `user_id` 三选一(推荐 `token`)
|
||||
|
||||
说明:
|
||||
- 同一个 `claim_request_id` 重复提交会直接返回成功(不会重复入账)
|
||||
- 会受 `today_limit/today_claimed/locked_points` 限制
|
||||
|
||||
---
|
||||
|
||||
### 3.4 商品列表
|
||||
|
||||
**GET** ` /api/v1/mall/items `
|
||||
|
||||
参数:
|
||||
- `type`:可选,`BONUS | PHYSICAL | WITHDRAW`
|
||||
|
||||
---
|
||||
|
||||
### 3.5 红利兑换(提交订单)
|
||||
|
||||
**POST** ` /api/v1/mall/bonusRedeem `
|
||||
|
||||
参数:
|
||||
- `item_id`:必填
|
||||
- 身份参数:`token` / `session_id` / `user_id`
|
||||
|
||||
返回:
|
||||
- `data.order_id`
|
||||
- `data.status`(通常 `PENDING`)
|
||||
|
||||
---
|
||||
|
||||
### 3.6 实物兑换(提交订单)
|
||||
|
||||
**POST** ` /api/v1/mall/physicalRedeem `
|
||||
|
||||
参数:
|
||||
- `item_id`:必填
|
||||
- `address_id`:必填,收货地址主键(`mall_address.id`,须为当前用户资产下地址)
|
||||
- 身份参数:`token` / `session_id` / `user_id`
|
||||
|
||||
说明:服务端会将该地址在下单时刻的 **收货人 / 电话 / 完整地址** 写入订单字段(快照),并写入 `mall_order.mall_address_id` 关联所选地址。
|
||||
|
||||
---
|
||||
|
||||
### 3.7 提现申请(提交订单)
|
||||
|
||||
**POST** ` /api/v1/mall/withdrawApply `
|
||||
|
||||
参数:
|
||||
- `item_id`:必填
|
||||
- 身份参数:`token` / `session_id` / `user_id`
|
||||
|
||||
---
|
||||
|
||||
### 3.8 订单列表
|
||||
|
||||
**GET** ` /api/v1/mall/orders `
|
||||
|
||||
鉴权:`token` / `session_id` / `user_id`
|
||||
|
||||
说明:
|
||||
- 返回最多 100 条
|
||||
- 订单里包含 `mallItem`(商品信息)
|
||||
|
||||
---
|
||||
|
||||
## 4. 地址管理(H5)
|
||||
|
||||
> 地址与资产主体通过 `playx_user_asset_id` 关联(即 `mall_user_asset.id`)。
|
||||
|
||||
### 4.1 地址列表
|
||||
**GET** ` /api/v1/mall/addressList `
|
||||
|
||||
### 4.2 新增地址
|
||||
**POST** ` /api/v1/mall/addressAdd `
|
||||
|
||||
Body 含 `receiver_name`(收货人,建议填写;实物兑换下单快照需要非空的收货人、电话与拼接后的完整地址)。
|
||||
|
||||
### 4.3 编辑地址
|
||||
**POST** ` /api/v1/mall/addressEdit `
|
||||
|
||||
### 4.4 删除地址
|
||||
**POST** ` /api/v1/mall/addressDelete `
|
||||
|
||||
---
|
||||
|
||||
## 5. session 换取(可选)
|
||||
|
||||
### 5.1 token 换 session
|
||||
|
||||
**POST** ` /api/v1/mall/verifyToken `
|
||||
|
||||
参数(二选一):
|
||||
- `token`
|
||||
- `session`
|
||||
|
||||
成功返回:
|
||||
- `data.session_id`
|
||||
- `data.user_id`
|
||||
- `data.username`
|
||||
- `data.token_expire_at`
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见错误与排查
|
||||
|
||||
- **401 登录态过期**:token/session 过期或不匹配;请重新 `temLogin` 或重新 `verifyToken`
|
||||
- **提示缺少必填字段**:按各接口参数补齐(如 `claim_request_id`、`item_id`、`address_id`(实物)、地址中收货人/电话/完整地址等)
|
||||
- **积分不足/无可领取积分**:`locked_points<=0` 或已达 `today_limit`
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
### 1.1 Daily Push API
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/daily-push`
|
||||
* 路径:`/api/v1/mall/dailyPush`
|
||||
|
||||
#### Header(多语言,可选)
|
||||
- `lang`: `zh`/`zh-cn` 返回中文(默认);`en` 返回英文
|
||||
@@ -25,7 +25,7 @@
|
||||
- `X-Signature`:签名(HMAC_SHA256)
|
||||
|
||||
服务端签名计算:
|
||||
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body)`
|
||||
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)`
|
||||
- `expected = hash_hmac('sha256', canonical, daily_push_secret)`
|
||||
- 校验:`hash_equals(expected, X-Signature)`
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
#### 示例(未开启签名校验)
|
||||
请求:
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"request_id":"req_1001",
|
||||
@@ -136,7 +136,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
#### 示例(新版批量上报)
|
||||
请求:
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"report_date": "1700000000",
|
||||
@@ -181,7 +181,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
## 2. PlayX -> 积分商城(商城调用 PlayX)
|
||||
|
||||
> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。
|
||||
> **说明**:H5 调商城的 **`/api/v1/playx/verify-token`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
|
||||
> **说明**:H5 调商城的 **`/api/v1/mall/verifyToken`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
|
||||
|
||||
### 2.1 Token Verification API(PlayX 侧实现,远程验证时使用)
|
||||
* 方法:`POST`
|
||||
@@ -406,7 +406,7 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
|
||||
### 3.3 Token 验证(换 session)
|
||||
|
||||
* 方法:`POST`(推荐 `GET` 传 `token` 亦可)
|
||||
* 路径:`/api/v1/playx/verify-token`
|
||||
* 路径:`/api/v1/mall/verifyToken`
|
||||
|
||||
#### 配置:本地验证 vs 远程 PlayX
|
||||
|
||||
@@ -442,11 +442,48 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
|
||||
#### 示例(本地验证)
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'token=上一步TemLogin返回的token'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.x 收货地址(`mall_address`)
|
||||
|
||||
> 下面接口用于 H5 维护收货地址。鉴权同本章其他接口:携带 `session_id` 或 `token` 或 `user_id`。
|
||||
|
||||
#### 3.x.1 地址列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/mall/addressList`
|
||||
|
||||
返回:`data.list` 为地址数组。
|
||||
|
||||
#### 3.x.2 添加地址
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/mall/addressAdd`
|
||||
|
||||
Body:
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `receiver_name` | 是 | 收货人 |
|
||||
| `phone` | 是 | 电话 |
|
||||
| `region` | 是 | 地区(数组或逗号分隔字符串) |
|
||||
| `detail_address` | 是 | 详细地址 |
|
||||
| `default_setting` | 否 | `1` 设为默认地址 |
|
||||
|
||||
#### 3.x.3 修改地址(含设为默认)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/mall/addressEdit`
|
||||
|
||||
Body:`id` 必填,其余字段按需传入更新。
|
||||
|
||||
#### 3.x.4 删除地址
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/mall/addressDelete`
|
||||
|
||||
Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。
|
||||
|
||||
#### 远程模式(`verify_token_local_only=false` + 已配置 `base_url`)
|
||||
|
||||
商城侧请求 URL:`${playx.api.base_url}${playx.api.token_verify_url}`(默认路径 `/api/v1/auth/verify-token`)。
|
||||
@@ -482,7 +519,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
|
||||
### 3.4 用户资产(Assets)
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/assets`
|
||||
* 路径:`/api/v1/mall/assets`
|
||||
|
||||
#### 请求参数(鉴权)
|
||||
|
||||
@@ -504,7 +541,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
```
|
||||
|
||||
响应(示例):
|
||||
@@ -527,7 +564,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上
|
||||
|
||||
### 3.5 领取(Claim)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/claim`
|
||||
* 路径:`/api/v1/mall/claim`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
@@ -544,7 +581,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'claim_request_id=claim_001' \
|
||||
--data-urlencode 'token=上一步temLogin返回的token'
|
||||
@@ -586,7 +623,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
|
||||
### 3.6 商品列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/items`
|
||||
* 路径:`/api/v1/mall/items`
|
||||
|
||||
#### 请求参数
|
||||
* `type`(可选):`BONUS` | `PHYSICAL` | `WITHDRAW`
|
||||
@@ -599,7 +636,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
#### 示例
|
||||
请求:
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDRAW'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=WITHDRAW'
|
||||
```
|
||||
|
||||
响应(示例):
|
||||
@@ -629,7 +666,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR
|
||||
|
||||
### 3.7 红利兑换(Bonus Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/bonus/redeem`
|
||||
* 路径:`/api/v1/mall/bonusRedeem`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
@@ -643,7 +680,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=123' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -666,14 +703,12 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
### 3.8 实物兑换(Physical Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/physical/redeem`
|
||||
* 路径:`/api/v1/mall/physicalRedeem`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
* `item_id`:商品 ID(要求 `mall_item.type=PHYSICAL` 且 `status=1`)
|
||||
* `receiver_name`:收货人
|
||||
* `receiver_phone`:收货电话
|
||||
* `receiver_address`:收货地址
|
||||
* `address_id`:收货地址 ID(`mall_address.id`,须属于当前用户资产;下单时写入 `mall_order.mall_address_id`,并将该地址快照写入 `receiver_name` / `receiver_phone` / `receiver_address`)
|
||||
鉴权:同 **3.1**(`session_id` / `token` / `user_id`)
|
||||
|
||||
#### 返回(成功)
|
||||
@@ -682,12 +717,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=200' \
|
||||
--data-urlencode 'receiver_name=张三' \
|
||||
--data-urlencode 'receiver_phone=18800001111' \
|
||||
--data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \
|
||||
--data-urlencode 'address_id=10' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
@@ -705,7 +738,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
### 3.9 提现申请(Withdraw Apply)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/withdraw/apply`
|
||||
* 路径:`/api/v1/mall/withdrawApply`
|
||||
|
||||
#### 请求 Body
|
||||
必填:
|
||||
@@ -719,7 +752,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
#### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=321' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -742,7 +775,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
|
||||
### 3.10 订单列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/orders`
|
||||
* 路径:`/api/v1/mall/orders`
|
||||
|
||||
#### 请求参数(鉴权)
|
||||
|
||||
@@ -756,7 +789,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
#### 示例
|
||||
请求:
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'token=上一步temLogin返回的token'
|
||||
```
|
||||
|
||||
响应(示例,简化):
|
||||
@@ -791,5 +824,5 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'token=上
|
||||
---
|
||||
|
||||
### 3.11 同步额度(可选)
|
||||
当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。
|
||||
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||
|
||||
|
||||
447
docs/PlayX-调用积分商城接口说明.md
Normal file
447
docs/PlayX-调用积分商城接口说明.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# PlayX 调用积分商城接口说明
|
||||
|
||||
本文档描述 **PlayX 平台(或 PlayX 侧脚本/服务)如何调用积分商城已开放的 HTTP 接口**:基础约定、推荐流程、鉴权方式、请求参数与返回结构。
|
||||
|
||||
实现依据:`config/route.php`、`app/api/controller/v1/Playx.php`、`config/playx.php`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 基础约定
|
||||
|
||||
### 1.1 Base URL
|
||||
|
||||
将下列路径拼在积分商城对外域名之后,例如:
|
||||
|
||||
`https://{商城域名}/api/v1/mall/dailyPush`
|
||||
|
||||
(联调时请向商城方索取正式环境与测试环境地址。)
|
||||
|
||||
### 1.2 通用响应结构(JSON)
|
||||
|
||||
所有接口成功或失败,响应体均为:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `code` | int | `1` 表示业务成功;`0` 表示业务失败 |
|
||||
| `msg` | string | 提示信息(失败时为错误原因) |
|
||||
| `time` | int | Unix 时间戳(秒) |
|
||||
| `data` | object/array/null | 业务数据;失败时可能为 `null` |
|
||||
|
||||
部分错误场景会通过 HTTP 状态码区分(如 **401**),此时 `code` 仍为 `0`,请同时判断 HTTP 状态与 `code`。
|
||||
|
||||
**成功示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "",
|
||||
"time": 1730000000,
|
||||
"data": { }
|
||||
}
|
||||
```
|
||||
|
||||
**失败示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "错误原因",
|
||||
"time": 1730000000,
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Content-Type
|
||||
|
||||
- 本文档中 **POST** 且带 JSON Body 的接口,请使用:`Content-Type: application/json`。
|
||||
|
||||
### 1.4 多语言(响应文案)
|
||||
|
||||
可通过请求头 `lang` 控制返回文案语言:
|
||||
|
||||
| Header | 值 | 说明 |
|
||||
|--------|----|------|
|
||||
| `lang` | `zh` / `zh-cn` | 返回中文(默认) |
|
||||
| `lang` | `en` | 返回英文 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 使用流程(推荐)
|
||||
|
||||
### 2.1 PlayX 服务端 → 商城:每日数据推送(主流程)
|
||||
|
||||
适用于 T+1 等业务数据由 **PlayX 服务端**主动推送到积分商城。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant PX as PlayX 服务端
|
||||
participant M as 积分商城
|
||||
|
||||
Note over PX,M: 按约定配置 HMAC 密钥
|
||||
PX->>M: POST /api/v1/mall/dailyPush(JSON Body)
|
||||
M-->>PX: code=1, data.accepted / deduped
|
||||
```
|
||||
|
||||
1. 与商城方约定 **商城 Base URL** 与 **HMAC**(`X-Signature` 等)密钥。
|
||||
2. 按 **§3** 构造请求并推送。
|
||||
3. 根据返回 `data.deduped` 判断是否为幂等重复推送。
|
||||
|
||||
### 2.2 用户侧(H5 / 内嵌页)→ 商城:会话与业务接口
|
||||
|
||||
以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用,通过 **`session_id`**(先调 `verifyToken` 获取)或 **`token`**(商城 `muser` 类 token)标识用户,**不一定由 PlayX 后端直接调用**:
|
||||
|
||||
- `POST /api/v1/mall/verifyToken`:用 PlayX token 换商城 `session_id`
|
||||
- `GET /api/v1/mall/assets`:查询资产
|
||||
- `POST /api/v1/mall/claim`:领取积分
|
||||
- `GET /api/v1/mall/items`:商品列表
|
||||
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply`:兑换与提现申请
|
||||
- `GET /api/v1/mall/orders`:订单列表
|
||||
|
||||
若 PlayX 后端需要代替用户调用上述接口,须同样携带有效的 `session_id` 或 `token`,并遵守同一用户身份规则(见 **§4 身份说明**)。
|
||||
|
||||
### 2.3 代理鉴权(非 PlayX 通用)
|
||||
|
||||
`GET /api/v1/authToken` 为 **渠道/代理**签名换 JWT(`authtoken`),与 PlayX 用户体系不同,一般不在本文「PlayX 平台对接」主流程中展开;需要时由运营向商城索取单独说明。
|
||||
|
||||
---
|
||||
|
||||
## 3. PlayX 服务端推送:Daily Push
|
||||
|
||||
### 3.1 概要
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 方法 | `POST` |
|
||||
| 路径 | `/api/v1/mall/dailyPush` |
|
||||
|
||||
### 3.2 鉴权(按商城部署配置,可组合)
|
||||
|
||||
#### 推荐方案:仅启用 HMAC(当前对接采用)
|
||||
|
||||
商城侧配置:设置环境变量 **`PLAYX_DAILY_PUSH_SECRET`** 为非空(启用 HMAC 校验)。
|
||||
|
||||
#### HMAC 签名(必填)
|
||||
|
||||
当商城配置 **`PLAYX_DAILY_PUSH_SECRET`** 非空时,需同时携带:
|
||||
|
||||
| Header | 说明 |
|
||||
|--------|------|
|
||||
| `X-Request-Id` | 请求 ID(建议与 Body 内可追溯字段一致) |
|
||||
| `X-Timestamp` | Unix 时间戳(秒,字符串) |
|
||||
| `X-Signature` | 签名(十六进制小写或大写需与实现一致,以下为十六进制字符串) |
|
||||
|
||||
签名原文与计算:
|
||||
|
||||
```
|
||||
canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)
|
||||
expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET )
|
||||
```
|
||||
|
||||
其中 `json_body` 为 **实际发送的 JSON 原始字符串** 计算出的 SHA256(十六进制);与 PHP `hash('sha256', $rawBody)` 一致。
|
||||
|
||||
校验:`hash_equals(expected, X-Signature)`。
|
||||
|
||||
#### Header 填写清单(HMAC 模式)
|
||||
- 必填:`X-Request-Id`、`X-Timestamp`、`X-Signature`
|
||||
|
||||
#### 重要注意:`json_body` 必须与实际发送一致
|
||||
为了保证签名可验通过:用于计算 sha256 的 `json_body` 必须是**实际发送到 HTTP body 的原始 JSON 字符串**(字节级一致)。\
|
||||
建议:在发送端先序列化 JSON 得到字符串 `rawBody`,用该 `rawBody` 做 sha256 与 HMAC,再把同一个 `rawBody` 作为请求 body 发送。
|
||||
|
||||
### 3.3 Body 参数(JSON)
|
||||
|
||||
`/api/v1/mall/dailyPush` 支持 **两种入参格式**(按字段自动识别):
|
||||
|
||||
#### 格式 A:旧版单条上报(兼容)
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `request_id` | string | 是 | 本次推送请求号;响应中原样返回 |
|
||||
| `date` | string | 是 | 业务日期,格式 `YYYY-MM-DD` |
|
||||
| `user_id` | string | 是 | PlayX 用户 ID(幂等键之一) |
|
||||
| `username` | string | 否 | 展示名;用于同步/创建商城侧用户资产展示信息 |
|
||||
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢;**小于 0** 时按配置比例计入待领取保障金(`locked_points`) |
|
||||
| `yesterday_total_deposit` | number | 否 | 昨日总充值;用于计算当日可领取上限等 |
|
||||
| `lifetime_total_deposit` | number | 否 | 历史总充值(冗余入库) |
|
||||
| `lifetime_total_withdraw` | number | 否 | 历史总提现(冗余入库) |
|
||||
|
||||
#### 格式 B:新版批量上报(你图中格式)
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `report_date` | string/number | 是 | 报表日期;可以为 Unix 秒时间戳(如 `1700000000`)或 `YYYY-MM-DD` |
|
||||
| `member` | array | 是 | 成员列表,每个成员包含一名 PlayX 用户数据 |
|
||||
|
||||
成员元素字段:
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `member_id` | string | 是 | PlayX 用户 ID(幂等键之一) |
|
||||
| `login` | string | 否 | 用户展示名 |
|
||||
| `yesterday_total_w` | number | 否 | 昨日净输赢;小于 0 才会累加到 `locked_points` |
|
||||
| `yesterday_total_deposit` | number | 否 | 昨日总充值;用于计算 `today_limit` |
|
||||
| `lty_deposit` | number | 否 | 历史总充值(冗余入库) |
|
||||
| `lty_withdrawal` | number | 否 | 历史总提现(冗余入库) |
|
||||
|
||||
#### Body 填写要求(批量模式)
|
||||
- **必须有**:`report_date`、`member`(数组且至少 1 个元素)、`member[].member_id`。
|
||||
- **允许缺省**:成员的 `login/yesterday_total_w/yesterday_total_deposit/lty_deposit/lty_withdrawal`;缺省时按 `0` 或空字符串处理。
|
||||
- **日期**:`report_date` 传 Unix 秒会自动转换成 `YYYY-MM-DD`;如果直接传 `YYYY-MM-DD` 也支持。
|
||||
|
||||
### 3.4 幂等
|
||||
|
||||
- 幂等键:**`user_id` + `date`**
|
||||
- 重复推送:不重复入账,返回 `data.deduped = true`
|
||||
|
||||
### 3.5 返回 `data` 字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `request_id` | string | 与请求一致 |
|
||||
| `accepted` | bool | 是否受理成功 |
|
||||
| `deduped` | bool | 是否为重复推送(幂等命中) |
|
||||
| `message` | string | 说明文案 |
|
||||
|
||||
#### 格式 B:批量上报的返回补充
|
||||
批量模式会在 `data` 中增加:`results`。
|
||||
|
||||
`data.results` 为数组,元素字段如下:
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `user_id` | string | 对应成员的 `member_id` |
|
||||
| `accepted` | bool | 是否受理成功 |
|
||||
| `deduped` | bool | 该成员是否为重复推送 |
|
||||
| `message` | string | `ok` 或 `duplicate input` |
|
||||
|
||||
**HTTP 401**:HMAC 不通过(签名缺失/不完整/校验失败)。
|
||||
|
||||
### 3.6 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-Request-Id: req_1700000000_123456' \
|
||||
-H 'X-Timestamp: 1700000000' \
|
||||
-H 'X-Signature: <按本文档 canonical 计算出的 HMAC_SHA256>' \
|
||||
-d '{
|
||||
"report_date": "1700000000",
|
||||
"member": [
|
||||
{
|
||||
"member_id": "123456",
|
||||
"login": "john",
|
||||
"lty_deposit": 15230.75,
|
||||
"lty_withdrawal": 12400.50,
|
||||
"yesterday_total_w": -320.25,
|
||||
"yesterday_total_deposit": 500.00
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
响应示例(首次写入,至少有一个成员非重复):
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "",
|
||||
"time": 0,
|
||||
"data": {
|
||||
"request_id": "report_2023-11-14",
|
||||
"accepted": true,
|
||||
"deduped": false,
|
||||
"message": "Ok",
|
||||
"results": [
|
||||
{
|
||||
"user_id": "123456",
|
||||
"accepted": true,
|
||||
"deduped": false,
|
||||
"message": "Ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 身份说明(`session_id` / `token` / `user_id`)
|
||||
|
||||
以下接口通过 **`resolvePlayxAssetIdFromRequest`** 解析当前用户,优先级如下:
|
||||
|
||||
1. **`session_id`**(POST/GET):对应商城表 `mall_playx_session`,未过期则映射到 `mall_playx_user_asset`。
|
||||
2. 若 `session_id` 实际是 **`muser` 类型 token**(历史兼容),也会按 token 解析。
|
||||
3. **`token`**(POST/GET 或标准鉴权头):商城 token 表内类型为会员或 **`muser`** 且未过期时,`user_id` 为 **`mall_playx_user_asset.id`**(资产表主键)。
|
||||
4. **`user_id`**(POST/GET):
|
||||
- 若**纯数字**:视为 **`mall_playx_user_asset.id`**;
|
||||
- 否则:按 **`playx_user_id`** 查找资产行。
|
||||
|
||||
无法解析身份时,通常返回 **401** 或参数错误提示。
|
||||
|
||||
---
|
||||
|
||||
## 5. 其他接口一览(摘要)
|
||||
|
||||
> 下列均为 **BuildAdmin 通用 `code/msg/time/data` 结构**;成功时 `code=1`。
|
||||
|
||||
### 5.1 `POST /api/v1/mall/verifyToken`
|
||||
|
||||
用于将 **PlayX token**(或本地联调 token)换 **商城 `session_id`**。
|
||||
|
||||
| 参数位置 | 名称 | 说明 |
|
||||
|----------|------|------|
|
||||
| POST/GET | `token` 或 `session` | PlayX 或商城 token |
|
||||
|
||||
**说明:** 若 `playx.verify_token_local_only=true`(默认),商城**仅本地校验** token,不请求 PlayX 远程接口;远程模式需配置 `PLAYX_API_BASE_URL` 等。
|
||||
|
||||
**成功 `data` 示例:**
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `session_id` | 后续接口可带此字段 |
|
||||
| `user_id` | PlayX 用户 ID 或映射后的标识 |
|
||||
| `username` | 用户名 |
|
||||
| `token_expire_at` | ISO8601 过期时间 |
|
||||
|
||||
---
|
||||
|
||||
### 5.2 `GET /api/v1/mall/assets`
|
||||
|
||||
查询积分资产;需 **§4** 身份。
|
||||
|
||||
**成功 `data`:**
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `locked_points` | 待领取积分 |
|
||||
| `available_points` | 可用积分 |
|
||||
| `today_limit` | 今日可领取上限 |
|
||||
| `today_claimed` | 今日已领取 |
|
||||
| `withdrawable_cash` | 可提现现金(由积分×配置比例换算,保留小数) |
|
||||
|
||||
---
|
||||
|
||||
### 5.3 `POST /api/v1/mall/claim`
|
||||
|
||||
领取积分;需 **§4** 身份。
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `claim_request_id` | 是 | 幂等请求号 |
|
||||
|
||||
**成功 `data`:** 与资产结构一致(含 `locked_points`、`available_points`、`today_limit`、`today_claimed`、`withdrawable_cash` 等)。
|
||||
|
||||
---
|
||||
|
||||
### 5.4 `GET /api/v1/mall/items`
|
||||
|
||||
商品列表。
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | 否 | `BONUS` / `PHYSICAL` / `WITHDRAW`,筛选类型 |
|
||||
|
||||
**成功 `data`:** `{ "list": [ ... ] }`
|
||||
|
||||
---
|
||||
|
||||
### 5.5 `POST /api/v1/mall/bonusRedeem`
|
||||
|
||||
红利兑换;需 **§4** 身份。
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `item_id` | 是 | 商品 ID |
|
||||
|
||||
**成功 `data`:** 含 `order_id`、`status`(如 `PENDING`)等。
|
||||
|
||||
---
|
||||
|
||||
### 5.6 `POST /api/v1/mall/physicalRedeem`
|
||||
|
||||
实物兑换;需 **§4** 身份。
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `item_id` | 是 | 实物商品 ID |
|
||||
| `address_id` | 是 | `mall_address.id`(当前用户下地址);订单保存 `mall_address_id` 与地址快照 |
|
||||
|
||||
---
|
||||
|
||||
### 5.7 `POST /api/v1/mall/withdrawApply`
|
||||
|
||||
提现类兑换申请;需 **§4** 身份。
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `item_id` | 是 | 提现档位商品 ID |
|
||||
|
||||
---
|
||||
|
||||
### 5.8 `GET /api/v1/mall/orders`
|
||||
|
||||
订单列表;需 **§4** 身份。
|
||||
|
||||
**成功 `data`:** `{ "list": [ ... ] }`(含关联商品等,以实际返回为准)。
|
||||
|
||||
---
|
||||
|
||||
### 5.9 收货地址(`mall_address`)
|
||||
|
||||
> 下列接口均需携带 **§4 身份参数**(`session_id` / `token` / `user_id` 之一)。
|
||||
|
||||
#### 5.9.1 获取收货地址列表
|
||||
- **方法**:`GET`
|
||||
- **路径**:`/api/v1/mall/addressList`
|
||||
|
||||
返回 `data.list`:地址数组(按 `default_setting` 优先,其次 id 倒序)。
|
||||
|
||||
#### 5.9.2 添加收货地址
|
||||
- **方法**:`POST`
|
||||
- **路径**:`/api/v1/mall/addressAdd`
|
||||
|
||||
Body(表单或 JSON 均可,建议 JSON):
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `receiver_name` | 是 | 收货人 |
|
||||
| `phone` | 是 | 联系电话 |
|
||||
| `region` | 是 | 地区(可传数组或逗号分隔字符串) |
|
||||
| `detail_address` | 是 | 详细地址(短文本) |
|
||||
| `default_setting` | 否 | `1` 设为默认地址;`0` 或不传为非默认 |
|
||||
|
||||
成功返回:`data.id` 为新地址 id。
|
||||
|
||||
#### 5.9.3 修改收货地址(含设置默认)
|
||||
- **方法**:`POST`
|
||||
- **路径**:`/api/v1/mall/addressEdit`
|
||||
|
||||
Body:
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | 是 | 地址 id |
|
||||
| `receiver_name/phone/region/detail_address/default_setting` | 否 | 需要修改的字段(只更新传入项) |
|
||||
|
||||
当 `default_setting=1`:会自动把该用户其他地址的 `default_setting` 置为 0。
|
||||
|
||||
#### 5.9.4 删除收货地址
|
||||
- **方法**:`POST`
|
||||
- **路径**:`/api/v1/mall/addressDelete`
|
||||
|
||||
Body:
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | 是 | 地址 id |
|
||||
|
||||
若删除的是默认地址:服务端会将剩余地址里 id 最大的一条自动设为默认(若存在)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置项(供运维/对方技术对照)
|
||||
|
||||
| 环境变量 / 配置 | 作用 |
|
||||
|-----------------|------|
|
||||
| `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 |
|
||||
| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 PlayX 远程 |
|
||||
| `PLAYX_API_BASE_URL` | 商城调用 PlayX 接口时使用(与「PlayX 调商城」方向相反) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 版本与变更
|
||||
|
||||
- 文档与仓库代码同步维护;接口路径以 `config/route.php` 为准。
|
||||
- 若后续升级鉴权策略(例如叠加 JWT),以部署环境变量与最新文档为准。
|
||||
@@ -11,7 +11,7 @@
|
||||
接收 PlayX 每日 T+1 数据推送。
|
||||
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/daily-push`
|
||||
* 路径:`/api/v1/mall/dailyPush`
|
||||
|
||||
##### 请求(Header)
|
||||
当配置了 `playx.daily_push_secret`(Daily Push 签名校验)时,需要携带:
|
||||
@@ -20,7 +20,7 @@
|
||||
* `X-Signature`:签名(HMAC_SHA256)
|
||||
|
||||
签名计算逻辑(服务端):
|
||||
* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/playx/daily-push\n{sha256(json_body)}`
|
||||
* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/mall/dailyPush\n{sha256(json_body)}`
|
||||
* expected:`hash_hmac('sha256', canonical, daily_push_secret)`
|
||||
|
||||
##### 请求(Body)
|
||||
@@ -51,7 +51,7 @@
|
||||
##### 示例
|
||||
无签名校验(`PLAYX_DAILY_PUSH_SECRET` 为空):
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"request_id":"req_1001",
|
||||
@@ -246,7 +246,7 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \
|
||||
### 1.3 商城内部 API(供 H5 前端调用)
|
||||
#### Token 验证
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/verify-token`
|
||||
* 路径:`/api/v1/mall/verifyToken`
|
||||
|
||||
请求(Body):
|
||||
* `token`(必填,优先读取)
|
||||
@@ -260,14 +260,14 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \
|
||||
|
||||
示例:
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'token=PLAYX_TOKEN_XXX'
|
||||
```
|
||||
|
||||
#### 用户资产
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/assets`
|
||||
* 路径:`/api/v1/mall/assets`
|
||||
|
||||
请求参数(二选一):
|
||||
* `session_id`(优先):从 `mall_playx_session` 查 user_id(并校验过期)
|
||||
@@ -282,7 +282,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id=7b1c....'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -301,7 +301,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
||||
|
||||
#### 领取(Claim)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/claim`
|
||||
* 路径:`/api/v1/mall/claim`
|
||||
|
||||
请求:
|
||||
* `claim_request_id`:幂等键(string,必填且唯一)
|
||||
@@ -312,7 +312,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id
|
||||
##### 示例
|
||||
(首次领取成功,可能返回 `msg=Claim success`;若幂等重复,`msg` 可能为空)
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'claim_request_id=claim_001' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -334,7 +334,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
|
||||
#### 商品列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/items`
|
||||
* 路径:`/api/v1/mall/items`
|
||||
|
||||
请求(可选):
|
||||
* `type=BONUS|PHYSICAL|WITHDRAW`
|
||||
@@ -344,7 +344,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=BONUS'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -370,7 +370,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
||||
|
||||
#### 红利兑换(Bonus Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/bonus/redeem`
|
||||
* 路径:`/api/v1/mall/bonusRedeem`
|
||||
|
||||
请求:
|
||||
* `item_id`:商品 ID(BONUS)
|
||||
@@ -382,7 +382,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS'
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=123' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -401,13 +401,11 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
#### 实物兑换(Physical Redeem)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/physical/redeem`
|
||||
* 路径:`/api/v1/mall/physicalRedeem`
|
||||
|
||||
请求:
|
||||
* `item_id`:商品 ID(PHYSICAL)
|
||||
* `receiver_name`
|
||||
* `receiver_phone`
|
||||
* `receiver_address`
|
||||
* `address_id`:`mall_address.id`(当前用户资产下地址;订单写入 `mall_address_id` 与收货快照)
|
||||
* 鉴权:`session_id` 或 `user_id`
|
||||
|
||||
成功返回:
|
||||
@@ -416,12 +414,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=200' \
|
||||
--data-urlencode 'receiver_name=张三' \
|
||||
--data-urlencode 'receiver_phone=18800001111' \
|
||||
--data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \
|
||||
--data-urlencode 'address_id=10' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
@@ -435,7 +431,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
#### 提现申请(Withdraw Apply)
|
||||
* 方法:`POST`
|
||||
* 路径:`/api/v1/playx/withdraw/apply`
|
||||
* 路径:`/api/v1/mall/withdrawApply`
|
||||
|
||||
请求:
|
||||
* `item_id`:商品 ID(WITHDRAW)
|
||||
@@ -447,7 +443,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'item_id=321' \
|
||||
--data-urlencode 'session_id=7b1c....'
|
||||
@@ -466,7 +462,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
|
||||
#### 订单列表
|
||||
* 方法:`GET`
|
||||
* 路径:`/api/v1/playx/orders`
|
||||
* 路径:`/api/v1/mall/orders`
|
||||
|
||||
请求:
|
||||
* `session_id` 或 `user_id`
|
||||
@@ -476,7 +472,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \
|
||||
|
||||
##### 示例
|
||||
```bash
|
||||
curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id=7b1c....'
|
||||
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=7b1c....'
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -502,7 +498,7 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
||||
```
|
||||
|
||||
#### 同步额度
|
||||
当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。
|
||||
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||
如需补齐,请在接口设计阶段新增对应实现与 PlayX API 对接。
|
||||
|
||||
---
|
||||
@@ -530,7 +526,7 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
||||
- 发货:录入物流公司、单号 → `SHIPPED`
|
||||
- 驳回:录入驳回原因 → `REJECTED`,自动退回积分
|
||||
- **红利/提现订单**:
|
||||
- 展示 `external_transaction_id`、`playx_transaction_id`、发放子状态
|
||||
- 展示 `external_transaction_id`、`playx_transaction_id`、推送playx
|
||||
- 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因
|
||||
|
||||
### 2.3 用户资产与人工调账
|
||||
@@ -607,29 +603,30 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id
|
||||
|
||||
**表名**:`mall_playx_order`(或统一改造 mall_pints_order / mall_redemption_order)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 主键 |
|
||||
| user_id | varchar(64) | 用户 ID |
|
||||
| type | enum | BONUS / PHYSICAL / WITHDRAW |
|
||||
| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED |
|
||||
| mall_item_id | int | 商品 ID |
|
||||
| points_cost | int | 消耗积分 |
|
||||
| amount | decimal(15,2) | 现金面值(红利/提现) |
|
||||
| multiplier | int | 流水倍数 |
|
||||
| external_transaction_id | varchar(64) | 外部交易幂等键 |
|
||||
| playx_transaction_id | varchar(64) | PlayX 流水号 |
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|----------------------------------------------------------------------|
|
||||
| id | int | 主键 |
|
||||
| user_id | varchar(64) | 用户 ID |
|
||||
| type | enum | BONUS / PHYSICAL / WITHDRAW |
|
||||
| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED |
|
||||
| mall_item_id | int | 商品 ID |
|
||||
| points_cost | int | 消耗积分 |
|
||||
| amount | decimal(15,2) | 现金面值(红利/提现) |
|
||||
| multiplier | int | 流水倍数 |
|
||||
| external_transaction_id | varchar(64) | 订单号 |
|
||||
| playx_transaction_id | varchar(64) | PlayX 流水号 |
|
||||
| grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL |
|
||||
| fail_reason | text | 失败原因 |
|
||||
| retry_count | int | 重试次数 |
|
||||
| reject_reason | varchar(255) | 驳回原因(实物) |
|
||||
| shipping_company | varchar(50) | 物流公司 |
|
||||
| shipping_no | varchar(64) | 物流单号 |
|
||||
| receiver_name | varchar(50) | 收货人 |
|
||||
| receiver_phone | varchar(20) | 收货电话 |
|
||||
| receiver_address | text | 收货地址 |
|
||||
| create_time | bigint | 创建时间 |
|
||||
| update_time | bigint | 更新时间 |
|
||||
| fail_reason | text | 失败原因 |
|
||||
| retry_count | int | 重试次数 |
|
||||
| reject_reason | varchar(255) | 驳回原因(实物) |
|
||||
| shipping_company | varchar(50) | 物流公司 |
|
||||
| shipping_no | varchar(64) | 物流单号 |
|
||||
| mall_address_id | int unsigned, NULL | 实物兑换所选 `mall_address.id`(快照仍见 `receiver_*`) |
|
||||
| receiver_name | varchar(50) | 收货人 |
|
||||
| receiver_phone | varchar(20) | 收货电话 |
|
||||
| receiver_address | text | 收货地址 |
|
||||
| create_time | bigint | 创建时间 |
|
||||
| update_time | bigint | 更新时间 |
|
||||
|
||||
**索引**:`user_id`、`external_transaction_id`、`type`、`status`
|
||||
|
||||
|
||||
27
tmp_sig.php
Normal file
27
tmp_sig.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
//脚本执行指令 php tmp_sig.php
|
||||
$secret = '5590a339502b133f4d0c545c3cdad159a4827dfccb3f51bb110c56f9b96568ca';
|
||||
$ts = '1775525663';
|
||||
$rid = 'req_1775525663_123';
|
||||
$body = [
|
||||
'report_date' => '1775525663',
|
||||
'member' => [
|
||||
[
|
||||
'member_id' => '123',
|
||||
'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/mall/dailyPush\n" . hash('sha256', $json);
|
||||
|
||||
echo "json={$json}\n";
|
||||
echo "sha256=" . hash('sha256', $json) . "\n";
|
||||
echo "X-Signature=" . hash_hmac('sha256', $canonical, $secret) . "\n";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* backend common language package
|
||||
*/
|
||||
import menu from './en/menu'
|
||||
|
||||
export default {
|
||||
menu,
|
||||
Balance: 'Balance',
|
||||
Integral: 'Integral',
|
||||
Connection: 'connection',
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ export default {
|
||||
id: 'id',
|
||||
playx_user_asset_id: 'PlayX user asset',
|
||||
playxuserasset__username: 'username',
|
||||
receiver_name: 'receiver name',
|
||||
phone: 'phone',
|
||||
region: 'region',
|
||||
detail_address: 'detail_address',
|
||||
address: 'address',
|
||||
default_setting: 'Default address',
|
||||
'default_setting 0': 'NO',
|
||||
'default_setting 0': '--',
|
||||
'default_setting 1': 'YES',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
|
||||
9
web/src/lang/backend/en/mall/claimLog.ts
Normal file
9
web/src/lang/backend/en/mall/claimLog.ts
Normal 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',
|
||||
}
|
||||
|
||||
13
web/src/lang/backend/en/mall/dailyPush.ts
Normal file
13
web/src/lang/backend/en/mall/dailyPush.ts
Normal 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',
|
||||
}
|
||||
|
||||
41
web/src/lang/backend/en/mall/order.ts
Normal file
41
web/src/lang/backend/en/mall/order.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export default {
|
||||
approve: 'Review',
|
||||
manual_retry: 'Retry grant',
|
||||
retry_confirm: 'Queue this order for grant retry?',
|
||||
id: 'Order ID',
|
||||
user_id: 'User ID',
|
||||
type: 'Type',
|
||||
'type BONUS': 'Bonus',
|
||||
'type PHYSICAL': 'Physical',
|
||||
'type WITHDRAW': 'Withdraw',
|
||||
status: 'Status',
|
||||
'status PENDING': 'Pending',
|
||||
'status COMPLETED': 'Completed',
|
||||
'status SHIPPED': 'Shipped',
|
||||
'status REJECTED': 'Rejected',
|
||||
mall_item_id: 'Product ID',
|
||||
mallitem__title: 'Product title',
|
||||
points_cost: 'Points spent',
|
||||
amount: 'Cash amount',
|
||||
multiplier: 'Turnover multiplier',
|
||||
external_transaction_id: 'Order number',
|
||||
playx_transaction_id: 'PlayX transaction ID',
|
||||
grant_status: 'Grant status',
|
||||
'grant_status NOT_SENT': 'Not sent',
|
||||
'grant_status SENT_PENDING': 'Sent (queued)',
|
||||
'grant_status ACCEPTED': 'Accepted',
|
||||
'grant_status FAILED_RETRYABLE': 'Failed (retryable)',
|
||||
'grant_status FAILED_FINAL': 'Failed (final)',
|
||||
'grant_status ---': '—',
|
||||
fail_reason: 'Failure reason',
|
||||
reject_reason: 'Rejection reason',
|
||||
shipping_company: 'Carrier',
|
||||
shipping_no: 'Tracking number',
|
||||
receiver_name: 'Recipient name',
|
||||
receiver_phone: 'Recipient phone',
|
||||
receiver_address: 'Shipping address',
|
||||
mall_address_id: 'Address ID',
|
||||
create_time: 'Created at',
|
||||
update_time: 'Updated at',
|
||||
'quick Search Fields': 'Order ID',
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export default {
|
||||
'grant_status ACCEPTED': 'ACCEPTED',
|
||||
'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE',
|
||||
'grant_status FAILED_FINAL': 'FAILED_FINAL',
|
||||
'grant_status ---': '---',
|
||||
fail_reason: 'fail_reason',
|
||||
reject_reason: 'reject_reason',
|
||||
shipping_company: 'shipping_company',
|
||||
|
||||
15
web/src/lang/backend/en/mall/userAsset.ts
Normal file
15
web/src/lang/backend/en/mall/userAsset.ts
Normal 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',
|
||||
}
|
||||
|
||||
119
web/src/lang/backend/en/menu.ts
Normal file
119
web/src/lang/backend/en/menu.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Admin menu titles (admin_rule.name → menu.names.{name with / as _})
|
||||
*/
|
||||
export default {
|
||||
names: {
|
||||
dashboard: 'Dashboard',
|
||||
dashboard_index: 'Browse',
|
||||
dashboard_dashboard: 'Dashboard',
|
||||
auth: 'Access control',
|
||||
auth_group: 'Admin groups',
|
||||
auth_group_index: 'Browse',
|
||||
auth_group_add: 'Add',
|
||||
auth_group_edit: 'Edit',
|
||||
auth_group_del: 'Delete',
|
||||
auth_admin: 'Administrators',
|
||||
auth_admin_index: 'Browse',
|
||||
auth_admin_add: 'Add',
|
||||
auth_admin_edit: 'Edit',
|
||||
auth_admin_del: 'Delete',
|
||||
auth_rule: 'Menu rules',
|
||||
auth_rule_index: 'Browse',
|
||||
auth_rule_add: 'Add',
|
||||
auth_rule_edit: 'Edit',
|
||||
auth_rule_del: 'Delete',
|
||||
auth_rule_sortable: 'Sort',
|
||||
auth_adminLog: 'Admin logs',
|
||||
auth_adminLog_index: 'Browse',
|
||||
user: 'Members',
|
||||
user_user: 'Members',
|
||||
user_user_index: 'Browse',
|
||||
user_user_add: 'Add',
|
||||
user_user_edit: 'Edit',
|
||||
user_user_del: 'Delete',
|
||||
user_group: 'Member groups',
|
||||
user_group_index: 'Browse',
|
||||
user_group_add: 'Add',
|
||||
user_group_edit: 'Edit',
|
||||
user_group_del: 'Delete',
|
||||
user_rule: 'Member rules',
|
||||
user_rule_index: 'Browse',
|
||||
user_rule_add: 'Add',
|
||||
user_rule_edit: 'Edit',
|
||||
user_rule_del: 'Delete',
|
||||
user_rule_sortable: 'Sort',
|
||||
user_moneyLog: 'Balance logs',
|
||||
user_moneyLog_index: 'Browse',
|
||||
user_moneyLog_add: 'Add',
|
||||
user_scoreLog: 'Points logs',
|
||||
user_scoreLog_index: 'Browse',
|
||||
user_scoreLog_add: 'Add',
|
||||
routine: 'General',
|
||||
routine_config: 'System config',
|
||||
routine_config_index: 'Browse',
|
||||
routine_config_edit: 'Edit',
|
||||
routine_config_add: 'Add',
|
||||
routine_config_del: 'Delete',
|
||||
routine_attachment: 'Attachments',
|
||||
routine_attachment_index: 'Browse',
|
||||
routine_attachment_edit: 'Edit',
|
||||
routine_attachment_del: 'Delete',
|
||||
routine_adminInfo: 'Profile',
|
||||
routine_adminInfo_index: 'Browse',
|
||||
routine_adminInfo_edit: 'Edit',
|
||||
security: 'Data security',
|
||||
security_dataRecycleLog: 'Recycle bin',
|
||||
security_dataRecycleLog_index: 'Browse',
|
||||
security_dataRecycleLog_del: 'Delete',
|
||||
security_dataRecycleLog_restore: 'Restore',
|
||||
security_dataRecycleLog_info: 'Details',
|
||||
security_sensitiveDataLog: 'Sensitive data logs',
|
||||
security_sensitiveDataLog_index: 'Browse',
|
||||
security_sensitiveDataLog_del: 'Delete',
|
||||
security_sensitiveDataLog_rollback: 'Rollback',
|
||||
security_sensitiveDataLog_info: 'Details',
|
||||
security_dataRecycle: 'Recycle rules',
|
||||
security_dataRecycle_index: 'Browse',
|
||||
security_dataRecycle_add: 'Add',
|
||||
security_dataRecycle_edit: 'Edit',
|
||||
security_dataRecycle_del: 'Delete',
|
||||
security_sensitiveData: 'Sensitive field rules',
|
||||
security_sensitiveData_index: 'Browse',
|
||||
security_sensitiveData_add: 'Add',
|
||||
security_sensitiveData_edit: 'Edit',
|
||||
security_sensitiveData_del: 'Delete',
|
||||
buildadmin: 'BuildAdmin',
|
||||
buildadmin_buildadmin: 'BuildAdmin',
|
||||
moduleStore_moduleStore: 'Module store',
|
||||
moduleStore_moduleStore_index: 'Browse',
|
||||
moduleStore_moduleStore_install: 'Install',
|
||||
moduleStore_moduleStore_changeState: 'Change state',
|
||||
moduleStore_moduleStore_uninstall: 'Uninstall',
|
||||
moduleStore_moduleStore_update: 'Update',
|
||||
crud_crud: 'CRUD generator',
|
||||
crud_crud_index: 'Browse',
|
||||
crud_crud_generate: 'Generate',
|
||||
crud_crud_delete: 'Delete',
|
||||
mall: 'Points mall',
|
||||
mall_userAsset: 'User assets',
|
||||
mall_userAsset_index: 'Browse',
|
||||
mall_userAsset_edit: 'Edit',
|
||||
mall_userAsset_del: 'Delete',
|
||||
mall_address: 'Shipping addresses',
|
||||
mall_order: 'Orders',
|
||||
mall_order_add: 'Add',
|
||||
mall_order_edit: 'Edit',
|
||||
mall_order_del: 'Delete',
|
||||
mall_order_approve: 'Approve',
|
||||
mall_dailyPush: 'Daily push',
|
||||
mall_claimLog: 'Claim log',
|
||||
mall_item: 'Products',
|
||||
mall_playxOrder: 'PlayX orders',
|
||||
mall_playxCenter: 'PlayX center',
|
||||
mall_playxClaimLog: 'PlayX claim log',
|
||||
mall_playxDailyPush: 'PlayX daily push',
|
||||
mall_playxUserAsset: 'PlayX user assets',
|
||||
mall_pintsOrder: 'Points orders',
|
||||
mall_redemptionOrder: 'Redemption orders',
|
||||
},
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
* 后台公共语言包
|
||||
* 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
|
||||
*/
|
||||
import menu from './zh-cn/menu'
|
||||
|
||||
export default {
|
||||
menu,
|
||||
Balance: '余额',
|
||||
Integral: '积分',
|
||||
Connection: '连接标识',
|
||||
|
||||
@@ -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': '现金面值合计',
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ export default {
|
||||
id: 'ID',
|
||||
playx_user_asset_id: '用户资产',
|
||||
playxuserasset__username: '用户名',
|
||||
receiver_name: '收货人',
|
||||
phone: '电话',
|
||||
region: '地区',
|
||||
detail_address: '详细地址',
|
||||
address: '地址',
|
||||
default_setting: '默认地址',
|
||||
'default_setting 0': '',
|
||||
'default_setting 0': '--',
|
||||
'default_setting 1': '是',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
|
||||
9
web/src/lang/backend/zh-cn/mall/claimLog.ts
Normal file
9
web/src/lang/backend/zh-cn/mall/claimLog.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
claim_request_id: '领取订单号',
|
||||
user_id: '用户ID',
|
||||
claimed_amount: '领取积分',
|
||||
create_time: '创建时间',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
|
||||
13
web/src/lang/backend/zh-cn/mall/dailyPush.ts
Normal file
13
web/src/lang/backend/zh-cn/mall/dailyPush.ts
Normal 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',
|
||||
}
|
||||
|
||||
42
web/src/lang/backend/zh-cn/mall/order.ts
Normal file
42
web/src/lang/backend/zh-cn/mall/order.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export default {
|
||||
approve: '审核',
|
||||
manual_retry: '手动重试',
|
||||
retry_confirm: '确认将该订单加入重试队列?',
|
||||
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: '推送playx状态',
|
||||
'grant_status NOT_SENT': '未发送',
|
||||
'grant_status SENT_PENDING': '已发送排队',
|
||||
'grant_status ACCEPTED': '已接收(accepted)',
|
||||
'grant_status FAILED_RETRYABLE': '失败可重试',
|
||||
'grant_status FAILED_FINAL': '失败最终',
|
||||
'grant_status ---': '---',
|
||||
fail_reason: '失败原因',
|
||||
reject_reason: '驳回原因',
|
||||
shipping_company: '物流公司',
|
||||
shipping_no: '物流单号',
|
||||
receiver_name: '收货人',
|
||||
receiver_phone: '收货电话',
|
||||
receiver_address: '收货地址',
|
||||
mall_address_id: '地址ID',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID',
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
title: 'PlayX 对接中心',
|
||||
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“发放子状态=失败可重试”的订单。',
|
||||
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“推送playx状态=失败可重试”的订单。',
|
||||
orders: '统一订单',
|
||||
dailyPush: '每日推送',
|
||||
claimLog: '领取记录',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
id: 'ID',
|
||||
claim_request_id: '领取幂等键',
|
||||
claim_request_id: '领取订单号',
|
||||
user_id: '用户ID',
|
||||
claimed_amount: '领取积分',
|
||||
create_time: '创建时间',
|
||||
|
||||
@@ -15,14 +15,15 @@ export default {
|
||||
points_cost: '消耗积分',
|
||||
amount: '现金面值',
|
||||
multiplier: '流水倍数',
|
||||
external_transaction_id: '外部交易幂等键',
|
||||
external_transaction_id: '订单号',
|
||||
playx_transaction_id: 'PlayX流水号',
|
||||
grant_status: '发放子状态',
|
||||
grant_status: '推送playx状态',
|
||||
'grant_status NOT_SENT': '未发送',
|
||||
'grant_status SENT_PENDING': '已发送排队',
|
||||
'grant_status ACCEPTED': '已接收(accepted)',
|
||||
'grant_status FAILED_RETRYABLE': '失败可重试',
|
||||
'grant_status FAILED_FINAL': '失败最终',
|
||||
'grant_status ---': '---',
|
||||
fail_reason: '失败原因',
|
||||
reject_reason: '驳回原因',
|
||||
shipping_company: '物流公司',
|
||||
|
||||
15
web/src/lang/backend/zh-cn/mall/userAsset.ts
Normal file
15
web/src/lang/backend/zh-cn/mall/userAsset.ts
Normal 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、用户名、手机号',
|
||||
}
|
||||
|
||||
120
web/src/lang/backend/zh-cn/menu.ts
Normal file
120
web/src/lang/backend/zh-cn/menu.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 后台菜单标题(与 admin_rule.name 对应:menu.names.{name 中 / 改为 _})
|
||||
*/
|
||||
export default {
|
||||
names: {
|
||||
/** version202 后菜单 name 为 dashboard,不再使用 dashboard/dashboard */
|
||||
dashboard: '控制台',
|
||||
dashboard_index: '查看',
|
||||
dashboard_dashboard: '控制台',
|
||||
auth: '权限管理',
|
||||
auth_group: '角色组管理',
|
||||
auth_group_index: '查看',
|
||||
auth_group_add: '添加',
|
||||
auth_group_edit: '编辑',
|
||||
auth_group_del: '删除',
|
||||
auth_admin: '管理员管理',
|
||||
auth_admin_index: '查看',
|
||||
auth_admin_add: '添加',
|
||||
auth_admin_edit: '编辑',
|
||||
auth_admin_del: '删除',
|
||||
auth_rule: '菜单规则管理',
|
||||
auth_rule_index: '查看',
|
||||
auth_rule_add: '添加',
|
||||
auth_rule_edit: '编辑',
|
||||
auth_rule_del: '删除',
|
||||
auth_rule_sortable: '快速排序',
|
||||
auth_adminLog: '管理员日志管理',
|
||||
auth_adminLog_index: '查看',
|
||||
user: '会员管理',
|
||||
user_user: '会员管理',
|
||||
user_user_index: '查看',
|
||||
user_user_add: '添加',
|
||||
user_user_edit: '编辑',
|
||||
user_user_del: '删除',
|
||||
user_group: '会员分组管理',
|
||||
user_group_index: '查看',
|
||||
user_group_add: '添加',
|
||||
user_group_edit: '编辑',
|
||||
user_group_del: '删除',
|
||||
user_rule: '会员规则管理',
|
||||
user_rule_index: '查看',
|
||||
user_rule_add: '添加',
|
||||
user_rule_edit: '编辑',
|
||||
user_rule_del: '删除',
|
||||
user_rule_sortable: '快速排序',
|
||||
user_moneyLog: '会员余额管理',
|
||||
user_moneyLog_index: '查看',
|
||||
user_moneyLog_add: '添加',
|
||||
user_scoreLog: '会员积分管理',
|
||||
user_scoreLog_index: '查看',
|
||||
user_scoreLog_add: '添加',
|
||||
routine: '常规管理',
|
||||
routine_config: '系统配置',
|
||||
routine_config_index: '查看',
|
||||
routine_config_edit: '编辑',
|
||||
routine_config_add: '添加',
|
||||
routine_config_del: '删除',
|
||||
routine_attachment: '附件管理',
|
||||
routine_attachment_index: '查看',
|
||||
routine_attachment_edit: '编辑',
|
||||
routine_attachment_del: '删除',
|
||||
routine_adminInfo: '个人资料',
|
||||
routine_adminInfo_index: '查看',
|
||||
routine_adminInfo_edit: '编辑',
|
||||
security: '数据安全管理',
|
||||
security_dataRecycleLog: '数据回收站',
|
||||
security_dataRecycleLog_index: '查看',
|
||||
security_dataRecycleLog_del: '删除',
|
||||
security_dataRecycleLog_restore: '还原',
|
||||
security_dataRecycleLog_info: '查看详情',
|
||||
security_sensitiveDataLog: '敏感数据修改记录',
|
||||
security_sensitiveDataLog_index: '查看',
|
||||
security_sensitiveDataLog_del: '删除',
|
||||
security_sensitiveDataLog_rollback: '回滚',
|
||||
security_sensitiveDataLog_info: '查看详情',
|
||||
security_dataRecycle: '数据回收规则管理',
|
||||
security_dataRecycle_index: '查看',
|
||||
security_dataRecycle_add: '添加',
|
||||
security_dataRecycle_edit: '编辑',
|
||||
security_dataRecycle_del: '删除',
|
||||
security_sensitiveData: '敏感字段规则管理',
|
||||
security_sensitiveData_index: '查看',
|
||||
security_sensitiveData_add: '添加',
|
||||
security_sensitiveData_edit: '编辑',
|
||||
security_sensitiveData_del: '删除',
|
||||
buildadmin: 'BuildAdmin',
|
||||
buildadmin_buildadmin: 'BuildAdmin',
|
||||
moduleStore_moduleStore: '模块市场',
|
||||
moduleStore_moduleStore_index: '查看',
|
||||
moduleStore_moduleStore_install: '安装',
|
||||
moduleStore_moduleStore_changeState: '调整状态',
|
||||
moduleStore_moduleStore_uninstall: '卸载',
|
||||
moduleStore_moduleStore_update: '更新',
|
||||
crud_crud: 'CRUD代码生成',
|
||||
crud_crud_index: '查看',
|
||||
crud_crud_generate: '生成',
|
||||
crud_crud_delete: '删除',
|
||||
mall: '积分商城',
|
||||
mall_userAsset: '用户资产',
|
||||
mall_userAsset_index: '查看',
|
||||
mall_userAsset_edit: '编辑',
|
||||
mall_userAsset_del: '删除',
|
||||
mall_address: '收货地址管理',
|
||||
mall_order: '统一订单',
|
||||
mall_order_add: '新增',
|
||||
mall_order_edit: '编辑',
|
||||
mall_order_del: '删除',
|
||||
mall_order_approve: '审核通过',
|
||||
mall_dailyPush: '每日推送',
|
||||
mall_claimLog: '领取记录',
|
||||
mall_item: '商品管理',
|
||||
mall_playxOrder: 'PlayX订单',
|
||||
mall_playxCenter: 'PlayX中心',
|
||||
mall_playxClaimLog: 'PlayX领取记录',
|
||||
mall_playxDailyPush: 'PlayX每日推送',
|
||||
mall_playxUserAsset: 'PlayX用户资产',
|
||||
mall_pintsOrder: '积分订单',
|
||||
mall_redemptionOrder: '兑换订单',
|
||||
},
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<el-sub-menu @click="onClickSubMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||
<template #title>
|
||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span>
|
||||
<span>{{ menuTitleFromRoute(menu) }}</span>
|
||||
</template>
|
||||
<MenuTree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children" />
|
||||
</el-sub-menu>
|
||||
@@ -12,7 +12,7 @@
|
||||
<template v-else>
|
||||
<el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span>
|
||||
<span>{{ menuTitleFromRoute(menu) }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
@@ -23,6 +23,7 @@ import { ElNotification } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import { menuTitleFromRoute } from '/@/utils/menuI18n'
|
||||
import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:ref="tabsRefs.set"
|
||||
:key="idx"
|
||||
>
|
||||
{{ item.meta.title }}
|
||||
{{ menuTitleFromRoute(item) }}
|
||||
<transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in">
|
||||
<Icon v-show="navTabs.state.tabsView.length > 1" class="close-icon" @click.stop="closeTab(item)" size="15" name="el-icon-Close" />
|
||||
</transition>
|
||||
@@ -29,6 +29,7 @@ import type { ContextMenuItem, ContextMenuItemClickEmitArg } from '/@/components
|
||||
import useCurrentInstance from '/@/utils/useCurrentInstance'
|
||||
import Contextmenu from '/@/components/contextmenu/index.vue'
|
||||
import horizontalScroll from '/@/utils/horizontalScroll'
|
||||
import { menuTitleFromRoute } from '/@/utils/menuI18n'
|
||||
import { getFirstRoute, routePush } from '/@/utils/router'
|
||||
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||||
|
||||
|
||||
33
web/src/utils/menuI18n.ts
Normal file
33
web/src/utils/menuI18n.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
|
||||
import { i18n } from '/@/lang/index'
|
||||
|
||||
/**
|
||||
* 后台菜单/标签标题:优先按路由 name(对应 admin_rule.name)匹配 menu.names.*
|
||||
*/
|
||||
export function menuI18nKeyFromName(name: string | symbol | null | undefined): string {
|
||||
if (name == null || name === '') {
|
||||
return ''
|
||||
}
|
||||
const n = String(name).trim()
|
||||
if (!n) {
|
||||
return ''
|
||||
}
|
||||
return `menu.names.${n.replace(/\//g, '_')}`
|
||||
}
|
||||
|
||||
export function menuTitleFromName(name: string | symbol | null | undefined, fallback?: string): string {
|
||||
const key = menuI18nKeyFromName(name)
|
||||
if (key && i18n.global.te(key)) {
|
||||
return String(i18n.global.t(key))
|
||||
}
|
||||
if (fallback && i18n.global.te(fallback)) {
|
||||
return String(i18n.global.t(fallback))
|
||||
}
|
||||
return fallback || ''
|
||||
}
|
||||
|
||||
export function menuTitleFromRoute(route: RouteRecordRaw | RouteLocationNormalized): string {
|
||||
const name = route.name
|
||||
const metaTitle = route.meta && typeof route.meta.title === 'string' ? route.meta.title : ''
|
||||
return menuTitleFromName(name, metaTitle) || metaTitle || String(i18n.global.t('noTitle'))
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -37,6 +37,17 @@ const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
const hasChinese = (s: string) => /[\u4e00-\u9fa5]/.test(s)
|
||||
|
||||
const formatRegion = (raw: string) => {
|
||||
const s = raw.toString().trim()
|
||||
if (!s) return ''
|
||||
if (hasChinese(s)) {
|
||||
return s.replace(/[,,\s]+/g, ',')
|
||||
}
|
||||
return s.replace(/[,,\s]+/g, ',')
|
||||
}
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
@@ -53,10 +64,19 @@ const baTable = new baTableClass(
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
showOverflowTooltip: true,
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
},
|
||||
{
|
||||
label: t('mall.address.receiver_name'),
|
||||
prop: 'receiver_name',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.address.phone'),
|
||||
prop: 'phone',
|
||||
@@ -65,7 +85,17 @@ const baTable = new baTableClass(
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('mall.address.region'), prop: 'region_text', align: 'center', operator: false },
|
||||
{
|
||||
label: t('mall.address.region'),
|
||||
prop: 'region_text',
|
||||
align: 'center',
|
||||
operator: false,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: TableRow, _column: TableColumn, cellValue: string) => {
|
||||
const raw = (cellValue || row.region || '').toString()
|
||||
return formatRegion(raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('mall.address.detail_address'),
|
||||
prop: 'detail_address',
|
||||
@@ -107,7 +137,7 @@ const baTable = new baTableClass(
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false },
|
||||
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'default_setting'],
|
||||
},
|
||||
|
||||
@@ -34,9 +34,16 @@
|
||||
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
|
||||
:label="t('mall.address.receiver_name')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_name"
|
||||
prop="receiver_name"
|
||||
:placeholder="t('Please input field', { field: t('mall.address.receiver_name') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.phone')"
|
||||
type="string"
|
||||
@@ -58,16 +65,6 @@
|
||||
prop="detail_address"
|
||||
:placeholder="t('Please input field', { field: t('mall.address.detail_address') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.address')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.address"
|
||||
prop="address"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||
:placeholder="t('Please input field', { field: t('mall.address.address') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.address.default_setting')"
|
||||
type="switch"
|
||||
|
||||
61
web/src/views/backend/mall/claimLog/index.vue
Normal file
61
web/src/views/backend/mall/claimLog/index.vue
Normal 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>
|
||||
|
||||
123
web/src/views/backend/mall/dailyPush/index.vue
Normal file
123
web/src/views/backend/mall/dailyPush/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<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',
|
||||
minWidth: 95,
|
||||
operator: 'RANGE',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
label: t('mall.dailyPush.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
{
|
||||
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>
|
||||
@@ -52,6 +52,8 @@ const baTable = new baTableClass(
|
||||
label: t('mall.item.description'),
|
||||
prop: 'description',
|
||||
align: 'center',
|
||||
minWidth: 80,
|
||||
// showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
@@ -60,6 +62,7 @@ const baTable = new baTableClass(
|
||||
label: t('mall.item.score'),
|
||||
prop: 'score',
|
||||
align: 'center',
|
||||
minWidth: 90,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
},
|
||||
@@ -173,7 +176,7 @@ const baTable = new baTableClass(
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
width: 80,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
|
||||
299
web/src/views/backend/mall/order/index.vue
Normal file
299
web/src/views/backend/mall/order/index.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<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', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.order.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 PopupForm from './popupForm.vue'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
|
||||
defineOptions({
|
||||
name: 'mall/order',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn) =>
|
||||
btn.name === 'edit'
|
||||
? {
|
||||
...btn,
|
||||
title: t('mall.order.approve'),
|
||||
type: 'primary',
|
||||
class: 'table-row-edit',
|
||||
icon: 'fa fa-check',
|
||||
}
|
||||
: btn
|
||||
)
|
||||
|
||||
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',
|
||||
effect: 'light',
|
||||
custom: { BONUS: 'success', PHYSICAL: 'primary', WITHDRAW: 'info' },
|
||||
minWidth: 140,
|
||||
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',
|
||||
effect: 'dark',
|
||||
custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' },
|
||||
minWidth: 160,
|
||||
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',
|
||||
minWidth: 90,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('mall.order.points_cost'), prop: 'points_cost', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.amount'), prop: 'amount', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false },
|
||||
{ label: t('mall.order.multiplier'), prop: 'multiplier', align: 'center', minWidth: 90, operator: 'eq', sortable: false },
|
||||
{
|
||||
label: t('mall.order.external_transaction_id'),
|
||||
prop: 'external_transaction_id',
|
||||
align: 'center',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.playx_transaction_id'),
|
||||
prop: 'playx_transaction_id',
|
||||
align: 'center',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.grant_status'),
|
||||
prop: 'grant_status',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
custom: {
|
||||
NOT_SENT: 'info',
|
||||
SENT_PENDING: 'primary',
|
||||
ACCEPTED: 'primary',
|
||||
FAILED_RETRYABLE: 'error',
|
||||
FAILED_FINAL: 'error',
|
||||
'---': 'info',
|
||||
},
|
||||
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'),
|
||||
'---': t('mall.order.grant_status ---'),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('mall.order.fail_reason'),
|
||||
prop: 'fail_reason',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.reject_reason'),
|
||||
prop: 'reject_reason',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.shipping_company'),
|
||||
prop: 'shipping_company',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.shipping_no'),
|
||||
prop: 'shipping_no',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.receiver_name'),
|
||||
prop: 'receiver_name',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.receiver_phone'),
|
||||
prop: 'receiver_phone',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('mall.order.receiver_address'),
|
||||
prop: 'receiver_address',
|
||||
align: 'center',
|
||||
showOverflowTooltip: true,
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{ label: t('mall.order.mall_address_id'), prop: 'mall_address_id', align: 'center', width: 100, operator: 'eq', sortable: false },
|
||||
{
|
||||
label: t('mall.order.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('mall.order.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 160,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
render: 'buttons',
|
||||
buttons: [
|
||||
...optButtons,
|
||||
{
|
||||
render: 'confirmButton',
|
||||
name: 'retry',
|
||||
title: t('mall.order.manual_retry'),
|
||||
text: '',
|
||||
type: 'primary',
|
||||
class: 'table-row-edit',
|
||||
icon: 'fa fa-refresh',
|
||||
display: (row: TableRow) =>
|
||||
row.type === 'BONUS' &&
|
||||
row.status === 'PENDING' &&
|
||||
['NOT_SENT', 'SENT_PENDING', 'FAILED_RETRYABLE', 'FAILED_FINAL'].includes(String(row.grant_status)),
|
||||
popconfirm: {
|
||||
title: t('mall.order.retry_confirm'),
|
||||
confirmButtonText: t('Confirm'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
confirmButtonType: 'primary',
|
||||
},
|
||||
click: async (row: TableRow) => {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/mall.Order/retry',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
showSuccessMessage: true,
|
||||
}
|
||||
)
|
||||
await baTable.getData()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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>
|
||||
304
web/src/views/backend/mall/order/popupForm.vue
Normal file
304
web/src/views/backend/mall/order/popupForm.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<!-- PENDING:两页审核流程(PHYSICAL 显示收货信息,其它类型只显示基本信息) -->
|
||||
<template v-if="usePagedActions && page === 1">
|
||||
<FormItem :label="t('mall.order.type')" type="string" v-model="baTable.form.items!.type" prop="type" :input-attr="{ disabled: true }" />
|
||||
<FormItem :label="t('mall.order.status')" type="string" v-model="baTable.form.items!.status" prop="status" :input-attr="{ disabled: true }" />
|
||||
<template v-if="isPhysical">
|
||||
<FormItem
|
||||
:label="t('mall.order.receiver_name')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_name"
|
||||
prop="receiver_name"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.receiver_phone')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_phone"
|
||||
prop="receiver_phone"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.receiver_address')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.receiver_address"
|
||||
prop="receiver_address"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="usePagedActions">
|
||||
<template v-if="action === 'approveShip' && isPhysical">
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_company')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_company"
|
||||
prop="shipping_company"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_no')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_no"
|
||||
prop="shipping_no"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="action === 'reject'">
|
||||
<FormItem
|
||||
v-if="isPhysical"
|
||||
:label="t('mall.order.reject_reason')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.reject_reason"
|
||||
prop="reject_reason"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
|
||||
/>
|
||||
<el-alert v-else type="info" :closable="false" show-icon>
|
||||
确认后将驳回该订单并退回积分(红利/提现订单无需填写驳回原因)。
|
||||
</el-alert>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 其它订单:保留常规可编辑表单 + 保存 -->
|
||||
<template v-else>
|
||||
<FormItem :label="t('mall.order.type')" type="string" v-model="baTable.form.items!.type" prop="type" :input-attr="{ disabled: true }" />
|
||||
<FormItem
|
||||
:label="t('mall.order.status')"
|
||||
type="select"
|
||||
v-model="baTable.form.items!.status"
|
||||
prop="status"
|
||||
:input-attr="{ content: { PENDING: t('mall.order.status PENDING'), COMPLETED: t('mall.order.status COMPLETED'), SHIPPED: t('mall.order.status SHIPPED'), REJECTED: t('mall.order.status REJECTED') } }"
|
||||
:placeholder="t('Please select field', { field: t('mall.order.status') })"
|
||||
/>
|
||||
<template v-if="isPhysical">
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_company')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_company"
|
||||
prop="shipping_company"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.shipping_no')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.shipping_no"
|
||||
prop="shipping_no"
|
||||
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.order.reject_reason')"
|
||||
type="textarea"
|
||||
v-model="baTable.form.items!.reject_reason"
|
||||
prop="reject_reason"
|
||||
:input-attr="{ rows: 3 }"
|
||||
@keyup.enter.stop=""
|
||||
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="onCancel">{{ t('Cancel') }}</el-button>
|
||||
|
||||
<template v-if="usePagedActions && page === 1">
|
||||
<el-button v-if="canApprove" @click="onApprove" type="success">审核通过</el-button>
|
||||
<el-button @click="goReject" type="danger">驳回</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="usePagedActions">
|
||||
<el-button @click="backToFirst">返回</el-button>
|
||||
<el-button
|
||||
v-if="action === 'approveShip'"
|
||||
v-blur
|
||||
:loading="submitting"
|
||||
@click="submitShip"
|
||||
type="success"
|
||||
>
|
||||
提交发货
|
||||
</el-button>
|
||||
<el-button v-if="action === 'reject'" v-blur :loading="submitting" @click="submitReject" type="danger">提交驳回</el-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const { t } = useI18n()
|
||||
|
||||
const isEdit = computed(() => baTable.form.operate === 'Edit')
|
||||
const isPending = computed(() => isEdit.value && baTable.form.items?.status === 'PENDING')
|
||||
const isPhysical = computed(() => baTable.form.items?.type === 'PHYSICAL')
|
||||
const canApprove = computed(() => isPending.value)
|
||||
const usePagedActions = computed(() => isPending.value)
|
||||
|
||||
const page = ref<1 | 2>(1)
|
||||
const action = ref<'approveShip' | 'reject' | null>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
const resetPager = () => {
|
||||
page.value = 1
|
||||
action.value = null
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => baTable.form.operate,
|
||||
() => {
|
||||
resetPager()
|
||||
}
|
||||
)
|
||||
|
||||
const onCancel = () => {
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
}
|
||||
|
||||
const backToFirst = () => {
|
||||
page.value = 1
|
||||
action.value = null
|
||||
}
|
||||
|
||||
const onApprove = async () => {
|
||||
if (!isPending.value) {
|
||||
return
|
||||
}
|
||||
if (isPhysical.value) {
|
||||
page.value = 2
|
||||
action.value = 'approveShip'
|
||||
return
|
||||
}
|
||||
const id = baTable.form.items?.id
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios({ url: '/admin/mall.Order/approve', method: 'post', data: { id } }, { showSuccessMessage: true })
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goReject = () => {
|
||||
page.value = 2
|
||||
action.value = 'reject'
|
||||
}
|
||||
|
||||
const submitShip = async () => {
|
||||
const id = baTable.form.items?.id
|
||||
const shippingCompany = (baTable.form.items?.shipping_company || '').toString().trim()
|
||||
const shippingNo = (baTable.form.items?.shipping_no || '').toString().trim()
|
||||
if (!id || shippingCompany === '' || shippingNo === '') {
|
||||
ElMessage.error('请填写物流公司与物流单号')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios(
|
||||
{ url: '/admin/mall.Order/ship', method: 'post', data: { id, shipping_company: shippingCompany, shipping_no: shippingNo } },
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitReject = async () => {
|
||||
const id = baTable.form.items?.id
|
||||
const rejectReason = (baTable.form.items?.reject_reason || '').toString().trim()
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
if (isPhysical.value && rejectReason === '') {
|
||||
ElMessage.error('请填写驳回原因')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios(
|
||||
{ url: '/admin/mall.Order/reject', method: 'post', data: { id, reject_reason: rejectReason } },
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
resetPager()
|
||||
baTable.toggleForm()
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const rules = computed<Partial<Record<string, FormItemRule[]>>>(() => {
|
||||
if (!isPhysical.value) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
shipping_company: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_company') })],
|
||||
shipping_no: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_no') })],
|
||||
reject_reason: [buildValidatorData({ name: 'required', title: t('mall.order.reject_reason') })],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,6 +83,7 @@ const baTable = new baTableClass(
|
||||
ACCEPTED: t('mall.playxOrder.grant_status ACCEPTED'),
|
||||
FAILED_RETRYABLE: t('mall.playxOrder.grant_status FAILED_RETRYABLE'),
|
||||
FAILED_FINAL: t('mall.playxOrder.grant_status FAILED_FINAL'),
|
||||
'---': t('mall.playxOrder.grant_status ---'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -116,7 +117,9 @@ const baTable = new baTableClass(
|
||||
type: 'warning',
|
||||
icon: '',
|
||||
display: (row: TableRow) =>
|
||||
(row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING',
|
||||
row.type === 'BONUS' &&
|
||||
row.status === 'PENDING' &&
|
||||
['NOT_SENT', 'SENT_PENDING', 'FAILED_RETRYABLE', 'FAILED_FINAL'].includes(String(row.grant_status)),
|
||||
popconfirm: {
|
||||
title: '确认将该订单加入重试队列?',
|
||||
confirmButtonText: '确认',
|
||||
@@ -126,7 +129,7 @@ const baTable = new baTableClass(
|
||||
click: async (row: TableRow) => {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/mall.PlayxOrder/retry',
|
||||
url: '/admin/mall.Order/retry',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: row.id,
|
||||
|
||||
@@ -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
|
||||
|
||||
123
web/src/views/backend/mall/userAsset/index.vue
Normal file
123
web/src/views/backend/mall/userAsset/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<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', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.userAsset.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'mall/userAsset',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
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',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', fixed: 'right', width: 80, render: 'buttons', buttons: optButtons, operator: false },
|
||||
],
|
||||
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>
|
||||
124
web/src/views/backend/mall/userAsset/popupForm.vue
Normal file
124
web/src/views/backend/mall/userAsset/popupForm.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<FormItem :label="t('mall.userAsset.id')" type="number" v-model="baTable.form.items!.id" prop="id" :input-attr="{ disabled: true }" />
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.playx_user_id')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.playx_user_id"
|
||||
prop="playx_user_id"
|
||||
:input-attr="{ disabled: true }"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.username')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.username"
|
||||
prop="username"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.username') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.phone')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.phone"
|
||||
prop="phone"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.phone') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.locked_points')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.locked_points"
|
||||
prop="locked_points"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.locked_points') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.available_points')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.available_points"
|
||||
prop="available_points"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.available_points') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.today_limit')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.today_limit"
|
||||
prop="today_limit"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.today_limit') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('mall.userAsset.today_claimed')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.today_claimed"
|
||||
prop="today_claimed"
|
||||
:input-attr="{ step: 1, min: 0 }"
|
||||
:placeholder="t('Please input field', { field: t('mall.userAsset.today_claimed') })"
|
||||
/>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const { t } = useI18n()
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('mall.userAsset.username') })],
|
||||
phone: [buildValidatorData({ name: 'required', title: t('mall.userAsset.phone') })],
|
||||
locked_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.locked_points') })],
|
||||
available_points: [buildValidatorData({ name: 'required', title: t('mall.userAsset.available_points') })],
|
||||
today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],
|
||||
today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
Reference in New Issue
Block a user