From 2e0ecbaebecff614c8736e1a6d0cb61fea1d3589 Mon Sep 17 00:00:00 2001
From: zhenhui <1276357500@qq.com>
Date: Fri, 20 Mar 2026 18:10:41 +0800
Subject: [PATCH] =?UTF-8?q?[=E7=A7=AF=E5=88=86=E5=95=86=E5=9F=8E]PlayX?=
=?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=AE=A2=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/admin/controller/mall/PlayxOrder.php | 250 ++++++++++++++++++
app/common/model/MallPlayxOrder.php | 66 +++++
web/src/lang/backend/en/mall/playxOrder.ts | 36 +++
web/src/lang/backend/zh-cn/mall/playxOrder.ts | 37 +++
.../views/backend/mall/playxOrder/index.vue | 240 +++++++++++++++++
5 files changed, 629 insertions(+)
create mode 100644 app/admin/controller/mall/PlayxOrder.php
create mode 100644 app/common/model/MallPlayxOrder.php
create mode 100644 web/src/lang/backend/en/mall/playxOrder.ts
create mode 100644 web/src/lang/backend/zh-cn/mall/playxOrder.ts
create mode 100644 web/src/views/backend/mall/playxOrder/index.vue
diff --git a/app/admin/controller/mall/PlayxOrder.php b/app/admin/controller/mall/PlayxOrder.php
new file mode 100644
index 0000000..290f716
--- /dev/null
+++ b/app/admin/controller/mall/PlayxOrder.php
@@ -0,0 +1,250 @@
+model = new \app\common\model\MallPlayxOrder();
+ }
+
+ /**
+ * 查看
+ * @throws Throwable
+ */
+ public function index(Request $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ if ($request->get('select') || $request->post('select')) {
+ return $this->select($request);
+ }
+
+ list($where, $alias, $limit, $order) = $this->queryBuilder();
+ $res = $this->model
+ ->with(['mallItem' => function ($query) {
+ $query->field('id,title');
+ }])
+ ->visible(['mallItem' => ['title']])
+ ->alias($alias)
+ ->where($where)
+ ->order($order)
+ ->paginate($limit);
+
+ return $this->success('', [
+ 'list' => $res->items(),
+ 'total' => $res->total(),
+ 'remark' => get_route_remark(),
+ ]);
+ }
+
+ /**
+ * PHYSICAL 发货:更新 shipping_company/shipping_no,并将状态置为 SHIPPED
+ */
+ public function ship(Request $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ if ($request->method() !== 'POST') {
+ return $this->error(__('Parameter error'));
+ }
+
+ $data = $request->post();
+ $id = intval($data['id'] ?? 0);
+ $shippingCompany = strval($data['shipping_company'] ?? '');
+ $shippingNo = strval($data['shipping_no'] ?? '');
+
+ if ($id <= 0 || $shippingCompany === '' || $shippingNo === '') {
+ return $this->error(__('Missing required fields'));
+ }
+
+ $order = MallPlayxOrder::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) {
+ return $this->error(__('Order status must be PENDING'));
+ }
+
+ Db::startTrans();
+ try {
+ $order->shipping_company = $shippingCompany;
+ $order->shipping_no = $shippingNo;
+ $order->status = MallPlayxOrder::STATUS_SHIPPED;
+ $order->save();
+ Db::commit();
+ } catch (Throwable $e) {
+ Db::rollback();
+ return $this->error($e->getMessage());
+ }
+
+ return $this->success(__('Shipped successfully'));
+ }
+
+ /**
+ * PHYSICAL 驳回:更新状态为 REJECTED,并退回积分到 available_points
+ */
+ public function reject(Request $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ if ($request->method() !== 'POST') {
+ return $this->error(__('Parameter error'));
+ }
+
+ $data = $request->post();
+ $id = intval($data['id'] ?? 0);
+ $rejectReason = strval($data['reject_reason'] ?? '');
+
+ if ($id <= 0 || $rejectReason === '') {
+ return $this->error(__('Missing required fields'));
+ }
+
+ $order = MallPlayxOrder::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) {
+ return $this->error(__('Order status must be PENDING'));
+ }
+
+ Db::startTrans();
+ try {
+ $asset = MallPlayxUserAsset::where('user_id', strval($order->user_id ?? ''))->find();
+ if (!$asset) {
+ $asset = MallPlayxUserAsset::create([
+ 'user_id' => strval($order->user_id ?? ''),
+ 'username' => strval($order->user_id ?? ''),
+ 'locked_points' => 0,
+ 'available_points' => 0,
+ 'today_limit' => 0,
+ 'today_claimed' => 0,
+ 'today_limit_date' => null,
+ 'create_time' => time(),
+ 'update_time' => time(),
+ ]);
+ }
+
+ $refund = intval($order->points_cost ?? 0);
+ if ($refund > 0) {
+ $asset->available_points += $refund;
+ $asset->save();
+ }
+
+ $order->status = MallPlayxOrder::STATUS_REJECTED;
+ $order->reject_reason = $rejectReason;
+ $order->grant_status = MallPlayxOrder::GRANT_FAILED_FINAL;
+ $order->save();
+
+ Db::commit();
+ } catch (Throwable $e) {
+ Db::rollback();
+ return $this->error($e->getMessage());
+ }
+
+ return $this->success(__('Rejected successfully'));
+ }
+
+ /**
+ * 手动重试(仅红利/提现,且必须 FAILED_RETRYABLE)
+ */
+ public function retry(Request $request): Response
+ {
+ $response = $this->initializeBackend($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ if ($request->method() !== 'POST') {
+ return $this->error(__('Parameter error'));
+ }
+
+ $id = intval($request->post('id', 0));
+ if ($id <= 0) {
+ return $this->error(__('Missing required fields'));
+ }
+
+ $order = MallPlayxOrder::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->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'));
+ }
+
+ $order->grant_status = MallPlayxOrder::GRANT_NOT_SENT;
+ $order->save();
+
+ return $this->success(__('Retry queued'));
+ }
+}
+
diff --git a/app/common/model/MallPlayxOrder.php b/app/common/model/MallPlayxOrder.php
new file mode 100644
index 0000000..c6f50b8
--- /dev/null
+++ b/app/common/model/MallPlayxOrder.php
@@ -0,0 +1,66 @@
+ 'integer',
+ 'update_time' => 'integer',
+ 'points_cost' => 'integer',
+ 'amount' => 'float',
+ 'multiplier' => 'integer',
+ 'retry_count' => 'integer',
+ ];
+
+ public function mallItem(): \think\model\relation\BelongsTo
+ {
+ return $this->belongsTo(MallItem::class, 'mall_item_id', 'id');
+ }
+}
diff --git a/web/src/lang/backend/en/mall/playxOrder.ts b/web/src/lang/backend/en/mall/playxOrder.ts
new file mode 100644
index 0000000..ff648ee
--- /dev/null
+++ b/web/src/lang/backend/en/mall/playxOrder.ts
@@ -0,0 +1,36 @@
+export default {
+ id: 'id',
+ user_id: 'user_id',
+ type: 'type',
+ 'type BONUS': 'Bonus(BONUS)',
+ 'type PHYSICAL': 'Physical(PHYSICAL)',
+ 'type WITHDRAW': 'Withdraw(WITHDRAW)',
+ status: 'status',
+ 'status PENDING': 'Pending(PENDING)',
+ 'status COMPLETED': 'Completed(COMPLETED)',
+ 'status SHIPPED': 'Shipped(SHIPPED)',
+ 'status REJECTED': 'Rejected(REJECTED)',
+ mall_item_id: 'mall_item_id',
+ mallitem__title: 'title',
+ points_cost: 'points_cost',
+ amount: 'amount',
+ multiplier: 'multiplier',
+ external_transaction_id: 'external_transaction_id',
+ playx_transaction_id: 'playx_transaction_id',
+ grant_status: 'grant_status',
+ 'grant_status NOT_SENT': 'NOT_SENT',
+ 'grant_status SENT_PENDING': 'SENT_PENDING',
+ 'grant_status ACCEPTED': 'ACCEPTED',
+ 'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE',
+ 'grant_status FAILED_FINAL': 'FAILED_FINAL',
+ fail_reason: 'fail_reason',
+ reject_reason: 'reject_reason',
+ shipping_company: 'shipping_company',
+ shipping_no: 'shipping_no',
+ receiver_name: 'receiver_name',
+ receiver_phone: 'receiver_phone',
+ receiver_address: 'receiver_address',
+ create_time: 'create_time',
+ update_time: 'update_time',
+ 'quick Search Fields': 'ID',
+}
diff --git a/web/src/lang/backend/zh-cn/mall/playxOrder.ts b/web/src/lang/backend/zh-cn/mall/playxOrder.ts
new file mode 100644
index 0000000..99ae053
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/mall/playxOrder.ts
@@ -0,0 +1,37 @@
+export default {
+ id: 'ID',
+ user_id: '用户ID',
+ type: '类型',
+ 'type BONUS': '红利(BONUS)',
+ 'type PHYSICAL': '实物(PHYSICAL)',
+ 'type WITHDRAW': '提现(WITHDRAW)',
+ status: '状态',
+ 'status PENDING': '处理中(PENDING)',
+ 'status COMPLETED': '已完成(COMPLETED)',
+ 'status SHIPPED': '已发货(SHIPPED)',
+ 'status REJECTED': '已驳回(REJECTED)',
+ mall_item_id: '商品ID',
+ mallitem__title: '商品标题',
+ points_cost: '消耗积分',
+ amount: '现金面值',
+ multiplier: '流水倍数',
+ external_transaction_id: '外部交易幂等键',
+ playx_transaction_id: 'PlayX流水号',
+ grant_status: '发放子状态',
+ 'grant_status NOT_SENT': '未发送',
+ 'grant_status SENT_PENDING': '已发送排队',
+ 'grant_status ACCEPTED': '已接收(accepted)',
+ 'grant_status FAILED_RETRYABLE': '失败可重试',
+ 'grant_status FAILED_FINAL': '失败最终',
+ fail_reason: '失败原因',
+ reject_reason: '驳回原因',
+ shipping_company: '物流公司',
+ shipping_no: '物流单号',
+ receiver_name: '收货人',
+ receiver_phone: '收货电话',
+ receiver_address: '收货地址',
+ create_time: '创建时间',
+ update_time: '修改时间',
+ 'quick Search Fields': 'ID',
+}
+
diff --git a/web/src/views/backend/mall/playxOrder/index.vue b/web/src/views/backend/mall/playxOrder/index.vue
new file mode 100644
index 0000000..f75db2c
--- /dev/null
+++ b/web/src/views/backend/mall/playxOrder/index.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+