diff --git a/app/admin/controller/Dashboard.php b/app/admin/controller/Dashboard.php
index 0c128d7..d659f16 100644
--- a/app/admin/controller/Dashboard.php
+++ b/app/admin/controller/Dashboard.php
@@ -5,6 +5,10 @@ declare(strict_types=1);
namespace app\admin\controller;
use app\common\controller\Backend;
+use app\common\model\MallPlayxClaimLog;
+use app\common\model\MallPlayxOrder;
+use app\common\model\MallPlayxUserAsset;
+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 = MallPlayxUserAsset::where('create_time', '>=', $todayStart)
+ ->where('create_time', '<=', $now)
+ ->count();
+
+ $yesterdayPointsClaimed = MallPlayxClaimLog::where('create_time', '>=', $yesterdayStart)
+ ->where('create_time', '<', $todayStart)
+ ->sum('claimed_amount');
+
+ $yesterdayRedeemQuery = MallPlayxOrder::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', MallPlayxOrder::STATUS_COMPLETED)
+ ->count();
+ $yesterdayRedeemRejectedCount = (clone $yesterdayRedeemQuery)
+ ->where('status', MallPlayxOrder::STATUS_REJECTED)
+ ->count();
+
+ $yesterdayRedeemByItem = Db::name('mall_playx_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 = MallPlayxOrder::where('type', MallPlayxOrder::TYPE_PHYSICAL)
+ ->where('status', MallPlayxOrder::STATUS_PENDING)
+ ->count();
+ $grantFailedRetryableCount = MallPlayxOrder::whereIn('type', [MallPlayxOrder::TYPE_BONUS, MallPlayxOrder::TYPE_WITHDRAW])
+ ->where('grant_status', MallPlayxOrder::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,
+ ],
]);
}
}
diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php
index 86fc050..de13853 100644
--- a/app/api/controller/v1/Playx.php
+++ b/app/api/controller/v1/Playx.php
@@ -14,6 +14,7 @@ use app\common\model\MallPlayxDailyPush;
use app\common\model\MallPlayxSession;
use app\common\model\MallPlayxOrder;
use app\common\model\MallPlayxUserAsset;
+use app\common\model\MallAddress;
use support\think\Db;
use Webman\Http\Request;
use support\Response;
@@ -675,6 +676,197 @@ class Playx extends Api
return $this->success('', ['list' => $list->toArray()]);
}
+ /**
+ * 收货地址列表
+ * GET /api/v1/playx/address/list?session_id=xxx
+ */
+ public function addressList(Request $request): Response
+ {
+ $response = $this->initializeApi($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ $assetId = $this->resolvePlayxAssetIdFromRequest($request);
+ if ($assetId === null) {
+ return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
+ }
+
+ $list = MallAddress::where('playx_user_asset_id', $assetId)
+ ->order('default_setting', 'desc')
+ ->order('id', 'desc')
+ ->select();
+
+ return $this->success('', ['list' => $list->toArray()]);
+ }
+
+ /**
+ * 添加收货地址(可设置默认)
+ * POST /api/v1/playx/address/add
+ */
+ public function addressAdd(Request $request): Response
+ {
+ $response = $this->initializeApi($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ $assetId = $this->resolvePlayxAssetIdFromRequest($request);
+ if ($assetId === null) {
+ return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
+ }
+
+ $phone = trim(strval($request->post('phone', '')));
+ $region = $request->post('region', '');
+ $detailAddress = trim(strval($request->post('detail_address', '')));
+ $address = trim(strval($request->post('address', '')));
+ $defaultSetting = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
+
+ if ($phone === '' || $detailAddress === '' || $address === '' || $region === '' || $region === null) {
+ return $this->error(__('Missing required fields'));
+ }
+
+ Db::startTrans();
+ try {
+ if ($defaultSetting === 1) {
+ MallAddress::where('playx_user_asset_id', $assetId)->update(['default_setting' => 0]);
+ }
+
+ $created = MallAddress::create([
+ 'playx_user_asset_id' => $assetId,
+ 'phone' => $phone,
+ 'region' => $region,
+ 'detail_address' => $detailAddress,
+ 'address' => $address,
+ 'default_setting' => $defaultSetting,
+ 'create_time' => time(),
+ 'update_time' => time(),
+ ]);
+
+ Db::commit();
+ } catch (\Throwable $e) {
+ Db::rollback();
+ return $this->error($e->getMessage());
+ }
+
+ return $this->success('', [
+ 'id' => $created ? $created->id : null,
+ ]);
+ }
+
+ /**
+ * 修改收货地址(包含设置默认地址)
+ * POST /api/v1/playx/address/edit
+ */
+ public function addressEdit(Request $request): Response
+ {
+ $response = $this->initializeApi($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ $assetId = $this->resolvePlayxAssetIdFromRequest($request);
+ if ($assetId === null) {
+ return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
+ }
+
+ $id = intval($request->post('id', 0));
+ if ($id <= 0) {
+ return $this->error(__('Missing required fields'));
+ }
+
+ $row = MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->find();
+ if (!$row) {
+ return $this->error(__('Record not found'));
+ }
+
+ $updates = [];
+ if ($request->post('phone', null) !== null) {
+ $updates['phone'] = trim(strval($request->post('phone', '')));
+ }
+ if ($request->post('region', null) !== null) {
+ $updates['region'] = $request->post('region', '');
+ }
+ if ($request->post('detail_address', null) !== null) {
+ $updates['detail_address'] = trim(strval($request->post('detail_address', '')));
+ }
+ if ($request->post('address', null) !== null) {
+ $updates['address'] = trim(strval($request->post('address', '')));
+ }
+ if ($request->post('default_setting', null) !== null) {
+ $updates['default_setting'] = strval($request->post('default_setting', '0')) === '1' ? 1 : 0;
+ }
+
+ if (empty($updates)) {
+ return $this->success('', ['updated' => false]);
+ }
+
+ Db::startTrans();
+ try {
+ if (isset($updates['default_setting']) && $updates['default_setting'] === 1) {
+ MallAddress::where('playx_user_asset_id', $assetId)->update(['default_setting' => 0]);
+ }
+ $updates['update_time'] = time();
+ MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->update($updates);
+ Db::commit();
+ } catch (\Throwable $e) {
+ Db::rollback();
+ return $this->error($e->getMessage());
+ }
+
+ return $this->success('', ['updated' => true]);
+ }
+
+ /**
+ * 删除收货地址
+ * POST /api/v1/playx/address/delete
+ */
+ public function addressDelete(Request $request): Response
+ {
+ $response = $this->initializeApi($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ $assetId = $this->resolvePlayxAssetIdFromRequest($request);
+ if ($assetId === null) {
+ return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
+ }
+
+ $id = intval($request->post('id', 0));
+ if ($id <= 0) {
+ return $this->error(__('Missing required fields'));
+ }
+
+ $row = MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->find();
+ if (!$row) {
+ return $this->error(__('Record not found'));
+ }
+
+ $wasDefault = intval($row->default_setting ?? 0) === 1;
+
+ Db::startTrans();
+ try {
+ MallAddress::where('id', $id)->where('playx_user_asset_id', $assetId)->delete();
+
+ if ($wasDefault) {
+ $fallback = MallAddress::where('playx_user_asset_id', $assetId)->order('id', 'desc')->find();
+ if ($fallback) {
+ $fallback->default_setting = 1;
+ $fallback->update_time = time();
+ $fallback->save();
+ }
+ }
+
+ Db::commit();
+ } catch (\Throwable $e) {
+ Db::rollback();
+ return $this->error($e->getMessage());
+ }
+
+ return $this->success('', ['deleted' => true]);
+ }
+
private function formatAsset(?MallPlayxUserAsset $asset): array
{
if (!$asset) {
diff --git a/config/route.php b/config/route.php
index a2f7931..8c77845 100644
--- a/config/route.php
+++ b/config/route.php
@@ -122,6 +122,10 @@ Route::post('/api/v1/playx/bonus/redeem', [\app\api\controller\v1\Playx::class,
Route::post('/api/v1/playx/physical/redeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
Route::post('/api/v1/playx/withdraw/apply', [\app\api\controller\v1\Playx::class, 'withdrawApply']);
Route::get('/api/v1/playx/orders', [\app\api\controller\v1\Playx::class, 'orders']);
+Route::get('/api/v1/playx/address/list', [\app\api\controller\v1\Playx::class, 'addressList']);
+Route::post('/api/v1/playx/address/add', [\app\api\controller\v1\Playx::class, 'addressAdd']);
+Route::post('/api/v1/playx/address/edit', [\app\api\controller\v1\Playx::class, 'addressEdit']);
+Route::post('/api/v1/playx/address/delete', [\app\api\controller\v1\Playx::class, 'addressDelete']);
// ==================== Admin 路由 ====================
// Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容
diff --git a/docs/PlayX-接口文档.md b/docs/PlayX-接口文档.md
index 4fcc601..2b09c0e 100644
--- a/docs/PlayX-接口文档.md
+++ b/docs/PlayX-接口文档.md
@@ -447,6 +447,43 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \
--data-urlencode 'token=上一步TemLogin返回的token'
```
+---
+
+### 3.x 收货地址(`mall_address`)
+
+> 下面接口用于 H5 维护收货地址。鉴权同本章其他接口:携带 `session_id` 或 `token` 或 `user_id`。
+
+#### 3.x.1 地址列表
+* 方法:`GET`
+* 路径:`/api/v1/playx/address/list`
+
+返回:`data.list` 为地址数组。
+
+#### 3.x.2 添加地址
+* 方法:`POST`
+* 路径:`/api/v1/playx/address/add`
+
+Body:
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `phone` | 是 | 电话 |
+| `region` | 是 | 地区(数组或逗号分隔字符串) |
+| `detail_address` | 是 | 详细地址 |
+| `address` | 是 | 地址补充 |
+| `default_setting` | 否 | `1` 设为默认地址 |
+
+#### 3.x.3 修改地址(含设为默认)
+* 方法:`POST`
+* 路径:`/api/v1/playx/address/edit`
+
+Body:`id` 必填,其余字段按需传入更新。
+
+#### 3.x.4 删除地址
+* 方法:`POST`
+* 路径:`/api/v1/playx/address/delete`
+
+Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。
+
#### 远程模式(`verify_token_local_only=false` + 已配置 `base_url`)
商城侧请求 URL:`${playx.api.base_url}${playx.api.token_verify_url}`(默认路径 `/api/v1/auth/verify-token`)。
diff --git a/web/src/lang/backend/en/dashboard.ts b/web/src/lang/backend/en/dashboard.ts
index 3927977..1bf160b 100644
--- a/web/src/lang/backend/en/dashboard.ts
+++ b/web/src/lang/backend/en/dashboard.ts
@@ -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',
}
diff --git a/web/src/lang/backend/en/mall/address.ts b/web/src/lang/backend/en/mall/address.ts
index 4c3bc6a..d143ee4 100644
--- a/web/src/lang/backend/en/mall/address.ts
+++ b/web/src/lang/backend/en/mall/address.ts
@@ -7,7 +7,7 @@ export default {
detail_address: 'detail_address',
address: 'address',
default_setting: 'Default address',
- 'default_setting 0': 'NO',
+ 'default_setting 0': '--',
'default_setting 1': 'YES',
create_time: 'create_time',
update_time: 'update_time',
diff --git a/web/src/lang/backend/zh-cn/dashboard.ts b/web/src/lang/backend/zh-cn/dashboard.ts
index 009eaa8..19234e4 100644
--- a/web/src/lang/backend/zh-cn/dashboard.ts
+++ b/web/src/lang/backend/zh-cn/dashboard.ts
@@ -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': '现金面值合计',
}
diff --git a/web/src/lang/backend/zh-cn/mall/address.ts b/web/src/lang/backend/zh-cn/mall/address.ts
index eef609c..bf0e4a2 100644
--- a/web/src/lang/backend/zh-cn/mall/address.ts
+++ b/web/src/lang/backend/zh-cn/mall/address.ts
@@ -7,7 +7,7 @@ export default {
detail_address: '详细地址',
address: '地址',
default_setting: '默认地址',
- 'default_setting 0': '',
+ 'default_setting 0': '--',
'default_setting 1': '是',
create_time: '创建时间',
update_time: '修改时间',
diff --git a/web/src/views/backend/dashboard.vue b/web/src/views/backend/dashboard.vue
index 0921c94..dea07e9 100644
--- a/web/src/views/backend/dashboard.vue
+++ b/web/src/views/backend/dashboard.vue
@@ -28,119 +28,88 @@
-
-
-