Преглед изворни кода

Merge branch 'redesign‌-260706' into dev

shizhongqi пре 1 дан
родитељ
комит
d530ad62b6

+ 32 - 0
app-mall/controllers/BalanceChangeController.php

@@ -2,6 +2,8 @@
 
 namespace mall\controllers;
 
+use bizHd\balance\classes\BalancePayChangeClass;
+use bizHd\custom\classes\CustomClass;
 use bizMall\balance\classes\BalanceChangeClass;
 use common\components\dateUtil;
 use common\components\util;
@@ -33,4 +35,34 @@ class BalanceChangeController extends BaseController
         util::success($respond);
     }
 
+    public function actionBalancePayChange()
+    {
+        $get = Yii::$app->request->get();
+        $where = [];
+        $where['mainId'] = $this->mainId;
+        $customId = $this->customId;
+        if (!empty($customId)) {
+            $custom = CustomClass::getById($customId, true);
+            if (empty($custom)) {
+                util::fail('没有找到客户');
+            }
+            $shopId = $this->shopId;
+            if ($custom->shopId != $shopId) {
+                util::fail('没有找到客户信息');
+            }
+            $where['customId'] = $customId;
+        } else {
+            util::fail('缺少参数customId');
+        }
+        $searchTime = $get['searchTime'] ?? '';
+        if (!empty($searchTime)) {
+            $startTime = $get['startTime'] ?? '';
+            $endTime = $get['endTime'] ?? '';
+            $period = dateUtil::formatTime($searchTime, $startTime, $endTime);
+            $where['addTime'] = ['between', [$period['startTime'], $period['endTime']]];
+        }
+        $respond = BalancePayChangeClass::getChangeList($where);
+        util::success($respond);
+    }
+
 }

+ 13 - 22
app-mall/controllers/RefundController.php

@@ -1,8 +1,8 @@
 <?php
 /**
  * 用途:商城顾客售后申请接口
- * 谁用:mallApp 订单详情「申请售后
- * 解决:顾客提交/查看/撤销售后申请;真正退款由商家在 hdApp 审核通过后执行
+ * 谁用:mallApp 申请售后页、退款售后列表/详情
+ * 解决:顾客提交/查看/撤销售后申请;列表展示店铺与商品快照;真正退款由商家在 hdApp 审核通过后执行
  */
 
 namespace mall\controllers;
@@ -74,6 +74,7 @@ class RefundController extends BaseController
 
     /**
      * 顾客提交售后申请(仅落待审核记录)
+     * 若该订单最新申请为已取消,则把原记录状态改回待审核,不新开一条
      */
     public function actionCreateOrder()
     {
@@ -144,38 +145,28 @@ class RefundController extends BaseController
     }
 
     /**
-     * 顾客售后申请列表
+     * 顾客售后申请列表(mallApp 退款售后 Tab)
+     * status:all 全部;0/pending 处理中;1/refunded 已退款;closed 已关闭(驳回+取消)
      */
     public function actionList()
     {
-        $where = ['customId' => (int)$this->customId];
-        $status = Yii::$app->request->get('status', '');
-        if ($status !== '' && $status !== null) {
-            $where['status'] = (int)$status;
-        }
-        $list = MallRefundApplyClass::getCustomApplyList($where);
-        if (!empty($list['list'])) {
-            foreach ($list['list'] as &$row) {
-                $row['statusText'] = MallRefundApplyClass::statusText($row['status'] ?? 0);
-            }
-            unset($row);
+        $tab = Yii::$app->request->get('status', 'all');
+        if ($tab === '' || $tab === null) {
+            $tab = 'all';
         }
+        $list = MallRefundApplyClass::getCustomApplyList((int)$this->userId, (string)$tab);
         util::success($list);
     }
 
     /**
-     * 顾客售后申请详情
+     * 顾客售后申请详情(列表「查看详情」)
      */
     public function actionDetail()
     {
         $id = (int)Yii::$app->request->get('id', 0);
         $apply = MallRefundApplyClass::getById($id, true);
-        MallRefundApplyClass::validByCustom($apply, (int)$this->customId);
-        $data = $apply->toArray();
-        $data['statusText'] = MallRefundApplyClass::statusText($apply->status);
-        if (!empty($data['product'])) {
-            $data['productList'] = json_decode($data['product'], true) ?: [];
-        }
+        MallRefundApplyClass::validByCustom($apply, (int)$this->customId, (int)$this->userId);
+        $data = MallRefundApplyClass::formatCustomDetail($apply->toArray());
         util::success($data);
     }
 
@@ -187,7 +178,7 @@ class RefundController extends BaseController
         $post = Yii::$app->request->post();
         $id = (int)($post['id'] ?? 0);
         $apply = MallRefundApplyClass::getById($id, true);
-        MallRefundApplyClass::validByCustom($apply, (int)$this->customId);
+        MallRefundApplyClass::validByCustom($apply, (int)$this->customId, (int)$this->userId);
 
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();

+ 0 - 1
biz-hd/balance/classes/BalanceChangeClass.php

@@ -5,7 +5,6 @@ namespace bizHd\balance\classes;
 use bizHd\order\classes\SettleClass;
 use bizHd\recharge\classes\RechargeClass;
 use common\components\dict;
-use Yii;
 use bizHd\base\classes\BaseClass;
 
 class BalanceChangeClass extends BaseClass

+ 0 - 1
biz-hd/balance/classes/BalancePayChangeClass.php

@@ -2,7 +2,6 @@
 
 namespace bizHd\balance\classes;
 
-use Yii;
 use bizHd\base\classes\BaseClass;
 
 class BalancePayChangeClass extends BaseClass

+ 394 - 12
biz-hd/refund/classes/MallRefundApplyClass.php

@@ -2,12 +2,19 @@
 /**
  * 用途:商城顾客售后申请业务类
  * 谁用:app-mall RefundController、app-hd RefundController 审核入口
- * 解决:申请记录的增删改查、归属校验、列表查询
+ * 解决:申请记录的增删改查、归属校验;顾客端列表按设计稿补齐店铺/商品/状态文案
  */
 
 namespace bizHd\refund\classes;
 
 use bizHd\base\classes\BaseClass;
+use bizHd\custom\classes\HdClass;
+use bizHd\order\classes\OrderClass;
+use bizHd\order\classes\OrderGoodsClass;
+use bizHd\order\classes\OrderItemClass;
+use bizHd\shop\classes\ShopClass;
+use biz\shop\classes\ShopClass as SharedShopClass;
+use common\components\imgUtil;
 use common\components\util;
 
 class MallRefundApplyClass extends BaseClass
@@ -46,20 +53,30 @@ class MallRefundApplyClass extends BaseClass
     }
 
     /**
-     * 校验申请归属顾客(商城端)
+     * 校验申请归属当前登录商城用户
+     * 列表/详情不带当前店 hdId 时 customId 可能为 0,需同时认 userId
      * @param object|null $apply
-     * @param int $customId
+     * @param int $customId 当前请求店铺的客户 id,可能为 0
+     * @param int $userId 商城登录用户 id
      * @return bool
      */
-    public static function validByCustom($apply, $customId)
+    public static function validByCustom($apply, $customId, $userId = 0)
     {
         if (empty($apply)) {
             util::fail('没有找到售后申请');
         }
-        if ((int)($apply->customId ?? 0) !== (int)$customId) {
-            util::fail('不是你的售后申请');
+        $userId = (int)$userId;
+        $customId = (int)$customId;
+        $applyUserId = (int)($apply->userId ?? 0);
+        $applyCustomId = (int)($apply->customId ?? 0);
+        if ($userId > 0 && $applyUserId === $userId) {
+            return true;
         }
-        return true;
+        if ($customId > 0 && $applyCustomId === $customId) {
+            return true;
+        }
+        util::fail('不是你的售后申请');
+        return false;
     }
 
     /**
@@ -107,17 +124,54 @@ class MallRefundApplyClass extends BaseClass
     }
 
     /**
-     * 顾客端申请列表
-     * @param array $where
+     * 顾客端售后列表(mallApp 退款售后页,跨店)
+     * 按商城 userId 过滤,避免请求里只有缓存店铺 customId 时漏单或 customId=0
+     * @param int $userId 当前登录商城用户
+     * @param string $tab all|pending|0|refunded|1|closed|2|3
+     * @return array 分页 list + pendingCount(处理中角标)
+     */
+    public static function getCustomApplyList($userId, $tab = 'all')
+    {
+        $userId = (int)$userId;
+        $where = ['userId' => $userId];
+        $tab = (string)$tab;
+        if ($tab === 'pending' || $tab === '0') {
+            $where['status'] = self::STATUS_PENDING;
+        } elseif ($tab === 'refunded' || $tab === '1') {
+            $where['status'] = self::STATUS_PASS;
+        } elseif ($tab === 'closed') {
+            // 已关闭:驳回 + 顾客撤销,对应设计稿「已关闭」Tab
+            $where['status'] = ['in', [self::STATUS_REJECT, self::STATUS_CANCEL]];
+        } elseif ($tab !== '' && $tab !== 'all' && is_numeric($tab)) {
+            $where['status'] = (int)$tab;
+        }
+
+        $data = self::getList('*', $where, 'addTime DESC');
+        $data['pendingCount'] = (int)self::getCount([
+            'userId' => $userId,
+            'status' => self::STATUS_PENDING,
+        ]);
+        if (!empty($data['list'])) {
+            $data['list'] = self::formatCustomList($data['list']);
+        } else {
+            $data['list'] = [];
+        }
+        return $data;
+    }
+
+    /**
+     * 顾客端售后详情:单条申请补齐与列表相同的展示字段
+     * @param array $row xhMallRefundApply 行
      * @return array
      */
-    public static function getCustomApplyList($where)
+    public static function formatCustomDetail($row)
     {
-        return self::getList('*', $where, 'addTime DESC');
+        $list = self::formatCustomList([$row]);
+        return $list[0] ?? $row;
     }
 
     /**
-     * 状态文案
+     * 状态文案(审核语义)
      * @param int $status
      * @return string
      */
@@ -131,4 +185,332 @@ class MallRefundApplyClass extends BaseClass
         ];
         return $map[(int)$status] ?? '';
     }
+
+    /**
+     * 列表卡片主状态文案,对齐设计稿「退款中·待商家处理 / 退款成功」
+     * @param int $status
+     * @param int $refundType 1退货退款 2仅退款
+     * @return string
+     */
+    public static function displayStatusTitle($status, $refundType)
+    {
+        $status = (int)$status;
+        if ($status === self::STATUS_PENDING) {
+            return (int)$refundType === self::REFUND_TYPE_MONEY_GOOD
+                ? '退货中·待商家处理'
+                : '退款中·待商家处理';
+        }
+        if ($status === self::STATUS_PASS) {
+            return '退款成功';
+        }
+        if ($status === self::STATUS_REJECT) {
+            return '已驳回';
+        }
+        if ($status === self::STATUS_CANCEL) {
+            return '已取消';
+        }
+        return self::statusText($status);
+    }
+
+    /**
+     * 卡片底部提示:处理中给审核时效,通过后带审核时间
+     * @param array $row
+     * @return string
+     */
+    public static function displayStatusTip($row)
+    {
+        $status = (int)($row['status'] ?? 0);
+        if ($status === self::STATUS_PENDING) {
+            return '商家审核中·预计 24 小时内处理';
+        }
+        if ($status === self::STATUS_PASS) {
+            $time = trim((string)($row['auditTime'] ?? ''));
+            if ($time !== '' && $time !== '0000-00-00 00:00:00') {
+                return '退款已到账·原路退回 ' . $time;
+            }
+            return '退款已到账·原路退回';
+        }
+        if ($status === self::STATUS_REJECT) {
+            $reason = trim((string)($row['rejectReason'] ?? ''));
+            return $reason !== '' ? ('商家已驳回·' . $reason) : '商家已驳回';
+        }
+        if ($status === self::STATUS_CANCEL) {
+            return '申请已取消';
+        }
+        return '';
+    }
+
+    /**
+     * 退款类型标签:1退货退款 2仅退款
+     * @param int $refundType
+     * @return string
+     */
+    public static function refundTypeText($refundType)
+    {
+        return (int)$refundType === self::REFUND_TYPE_MONEY_GOOD ? '退货退款' : '仅退款';
+    }
+
+    /**
+     * 批量补齐店铺名、商品封面/规格,避免列表 N+1
+     * @param array $list
+     * @return array
+     */
+    public static function formatCustomList($list)
+    {
+        if (empty($list) || !is_array($list)) {
+            return [];
+        }
+
+        $shopIds = [];
+        $hdIds = [];
+        $orderSns = [];
+        foreach ($list as $row) {
+            $shopId = (int)($row['shopId'] ?? 0);
+            if ($shopId > 0) {
+                $shopIds[$shopId] = $shopId;
+            }
+            $hdId = (int)($row['hdId'] ?? 0);
+            if ($hdId > 0) {
+                $hdIds[$hdId] = $hdId;
+            }
+            $sn = trim((string)($row['orderSn'] ?? ''));
+            if ($sn !== '') {
+                $orderSns[$sn] = $sn;
+            }
+        }
+
+        // 花店名称按申请 shopId 查 xhShop,首店展示商家名、分店拼 merchantName-shopName
+        $shopMap = [];
+        if (!empty($shopIds)) {
+            $shopMap = ShopClass::getByIds(array_values($shopIds), null, 'id', 'id,shopName,merchantName,avatar') ?: [];
+        }
+
+        // 联系客服要的是该店 xhHd.customId,申请表 customId 可能为 0
+        $hdCustomMap = [];
+        if (!empty($hdIds)) {
+            $hdRows = HdClass::getByIds(array_values($hdIds), null, 'id', 'id,customId') ?: [];
+            foreach ($hdRows as $id => $hd) {
+                $hdCustomMap[(int)$id] = (int)($hd['customId'] ?? 0);
+            }
+        }
+
+        $orderCustomMap = [];
+
+        $goodsBySn = [];
+        $itemBySn = [];
+        $firstGoodsBySn = [];
+        if (!empty($orderSns)) {
+            $snList = array_values($orderSns);
+            $orderRows = OrderClass::getAllByCondition(['orderSn' => ['in', $snList]], null, 'orderSn,customId') ?: [];
+            foreach ($orderRows as $order) {
+                $sn = $order['orderSn'] ?? '';
+                $cid = (int)($order['customId'] ?? 0);
+                if ($sn !== '' && $cid > 0) {
+                    $orderCustomMap[$sn] = $cid;
+                }
+            }
+            $goodsRows = OrderGoodsClass::getAllByCondition(['orderSn' => ['in', $snList]]) ?: [];
+            foreach ($goodsRows as $goods) {
+                $sn = $goods['orderSn'] ?? '';
+                $gid = (int)($goods['goodsId'] ?? 0);
+                if ($sn === '') {
+                    continue;
+                }
+                $goodsBySn[$sn][$gid] = $goods;
+                if (!isset($firstGoodsBySn[$sn])) {
+                    $firstGoodsBySn[$sn] = $goods;
+                }
+            }
+            $itemRows = OrderItemClass::getAllByCondition(['orderSn' => ['in', $snList]]) ?: [];
+            foreach ($itemRows as $item) {
+                $sn = $item['orderSn'] ?? '';
+                $itemId = (int)($item['itemId'] ?? 0);
+                if ($sn === '' || $itemId <= 0) {
+                    continue;
+                }
+                $itemBySn[$sn][$itemId] = $item;
+            }
+        }
+
+        $formatted = [];
+        foreach ($list as $row) {
+            $formatted[] = self::formatCustomRow($row, $shopMap, $goodsBySn, $itemBySn, $firstGoodsBySn, $hdCustomMap, $orderCustomMap);
+        }
+        return $formatted;
+    }
+
+    /**
+     * 单条申请转列表/详情展示结构
+     * @param array $row
+     * @param array $shopMap shopId => xhShop 行
+     * @param array $goodsBySn orderSn => [goodsId => 行]
+     * @param array $itemBySn orderSn => [itemId => 行]
+     * @param array $firstGoodsBySn 仅退款无勾选商品时用原单首件兜底
+     * @param array $hdCustomMap hdId => xhHd.customId
+     * @param array $orderCustomMap orderSn => 订单 customId
+     * @return array
+     */
+    public static function formatCustomRow($row, $shopMap = [], $goodsBySn = [], $itemBySn = [], $firstGoodsBySn = [], $hdCustomMap = [], $orderCustomMap = [])
+    {
+        $status = (int)($row['status'] ?? 0);
+        $refundType = (int)($row['refundType'] ?? self::REFUND_TYPE_MONEY);
+        $shopId = (int)($row['shopId'] ?? 0);
+        $orderSn = trim((string)($row['orderSn'] ?? ''));
+        $shop = $shopMap[$shopId] ?? [];
+        $row['customId'] = self::resolveChatCustomId($row, $hdCustomMap, $orderCustomMap);
+
+        $productList = [];
+        if (!empty($row['product'])) {
+            $decoded = is_array($row['product']) ? $row['product'] : json_decode($row['product'], true);
+            if (is_array($decoded)) {
+                $productList = $decoded;
+            }
+        }
+
+        $goodsList = [];
+        foreach ($productList as $product) {
+            $property = (int)($product['property'] ?? 0);
+            $productId = (int)($product['productId'] ?? 0);
+            $line = [];
+            if ($property === 1) {
+                $line = $itemBySn[$orderSn][$productId] ?? [];
+            } else {
+                $line = $goodsBySn[$orderSn][$productId] ?? [];
+            }
+            $goodsList[] = self::buildGoodsView($product, $line);
+        }
+
+        // 仅退款可能没勾选商品,用原订单首件商品撑起卡片,避免空白
+        if (empty($goodsList) && isset($firstGoodsBySn[$orderSn])) {
+            $goodsList[] = self::buildGoodsView([], $firstGoodsBySn[$orderSn]);
+        }
+
+        $row['productList'] = $productList;
+        $row['goodsList'] = $goodsList;
+        $row['goods'] = $goodsList[0] ?? self::emptyGoodsView();
+        $displayName = SharedShopClass::formatDisplayShopName($shop);
+        $row['hdName'] = $displayName !== '' ? $displayName : '花店';
+        $row['hdAvatar'] = self::coverUrl($shop['avatar'] ?? '');
+        $row['statusText'] = self::statusText($status);
+        $row['statusTitle'] = self::displayStatusTitle($status, $refundType);
+        $row['statusTip'] = self::displayStatusTip($row);
+        $row['statusTone'] = self::statusTone($status);
+        $row['refundTypeText'] = self::refundTypeText($refundType);
+        $row['reasonText'] = self::reasonText($row);
+        $row['canModify'] = $status === self::STATUS_PENDING ? 1 : 0;
+        $row['refundPrice'] = round((float)($row['refundPrice'] ?? 0), 2);
+        return $row;
+    }
+
+    /**
+     * 联系客服用的 customId:申请表可能为 0,依次用申请本身、xhHd、原订单补齐
+     * @param array $row
+     * @param array $hdCustomMap
+     * @param array $orderCustomMap
+     * @return int
+     */
+    protected static function resolveChatCustomId($row, $hdCustomMap, $orderCustomMap)
+    {
+        $applyCustomId = (int)($row['customId'] ?? 0);
+        if ($applyCustomId > 0) {
+            return $applyCustomId;
+        }
+        $hdId = (int)($row['hdId'] ?? 0);
+        if ($hdId > 0 && !empty($hdCustomMap[$hdId])) {
+            return (int)$hdCustomMap[$hdId];
+        }
+        $orderSn = trim((string)($row['orderSn'] ?? ''));
+        if ($orderSn !== '' && !empty($orderCustomMap[$orderSn])) {
+            return (int)$orderCustomMap[$orderSn];
+        }
+        return 0;
+    }
+
+    /**
+     * 处理中橙色、成功绿色、关闭灰色,供前端绑 class
+     * @param int $status
+     * @return string pending|success|closed
+     */
+    protected static function statusTone($status)
+    {
+        $status = (int)$status;
+        if ($status === self::STATUS_PENDING) {
+            return 'pending';
+        }
+        if ($status === self::STATUS_PASS) {
+            return 'success';
+        }
+        return 'closed';
+    }
+
+    /**
+     * 售后原因:优先顾客备注,没有则给兜底文案(申请表无 refundOptionName)
+     * @param array $row
+     * @return string
+     */
+    protected static function reasonText($row)
+    {
+        $remark = trim((string)($row['remark'] ?? ''));
+        if ($remark !== '') {
+            return $remark;
+        }
+        return '顾客申请售后';
+    }
+
+    /**
+     * 把申请商品快照与订单行封面/规格拼成卡片商品
+     * @param array $product
+     * @param array $line
+     * @return array
+     */
+    protected static function buildGoodsView($product, $line)
+    {
+        $name = $product['name'] ?? ($line['name'] ?? '商品');
+        $cover = $product['cover'] ?? ($line['cover'] ?? '');
+        $spec = $product['styleName'] ?? ($product['unitName'] ?? '');
+        if ($spec === '') {
+            $spec = $line['styleName'] ?? ($line['unitName'] ?? '');
+        }
+        $price = $product['unitPrice'] ?? ($line['unitPrice'] ?? 0);
+        $num = $product['num'] ?? ($line['num'] ?? 1);
+        return [
+            'name' => $name,
+            'cover' => self::coverUrl($cover),
+            'spec' => $spec,
+            'price' => round((float)$price, 2),
+            'num' => $num,
+        ];
+    }
+
+    /**
+     * 无商品时的占位,避免前端取 goods 空指针
+     * @return array
+     */
+    protected static function emptyGoodsView()
+    {
+        return [
+            'name' => '售后商品',
+            'cover' => self::coverUrl(''),
+            'spec' => '',
+            'price' => 0,
+            'num' => 1,
+        ];
+    }
+
+    /**
+     * 相对路径转可访问封面;已是 http(s) 则原样返回
+     * @param string $short
+     * @return string
+     */
+    protected static function coverUrl($short)
+    {
+        $short = trim((string)$short);
+        if ($short === '') {
+            return imgUtil::groupImg('retail/default-img.png');
+        }
+        if (strpos($short, 'http://') === 0 || strpos($short, 'https://') === 0) {
+            return $short;
+        }
+        return imgUtil::groupImg($short) . '?x-oss-process=image/resize,m_fill,h_200,w_200';
+    }
 }

+ 69 - 3
biz-hd/refund/services/MallRefundApplyService.php

@@ -1,8 +1,8 @@
 <?php
 /**
  * 用途:商城顾客售后申请服务
- * 谁用:app-mall 提交/撤销申请、app-hd 审核通过/驳回
- * 解决:申请只落待审核记录;审核通过时复用 HdRefundService 执行真实退款,并处理红包与分销重算
+ * 谁用:app-mall 提交/撤销/重新申请、app-hd 审核通过/驳回
+ * 解决:申请只落待审核记录;撤销后可把已取消记录改回待审核;审核通过时复用 HdRefundService 执行真实退款
  */
 
 namespace bizHd\refund\services;
@@ -21,6 +21,7 @@ class MallRefundApplyService extends BaseService
 
     /**
      * 创建售后申请(仅落库待审核,不触达资金)
+     * 若该订单最新申请为已取消,则复用该记录把状态改回待审核
      * @param array $post 含 refundType/product/price/remark 等
      * @param object $order xhOrder 对象
      * @param array $extra 含 userId/hdId/customId/shopId/mainId/sjId 等上下文
@@ -62,7 +63,7 @@ class MallRefundApplyService extends BaseService
             'mainId' => (int)($extra['mainId'] ?? $order->mainId ?? 0),
             'shopId' => (int)($extra['shopId'] ?? $order->shopId ?? 0),
             'hdId' => (int)($extra['hdId'] ?? $order->hdId ?? 0),
-            'customId' => (int)($extra['customId'] ?? $order->customId ?? 0),
+            'customId' => self::resolveApplyCustomId($extra, $order),
             'userId' => (int)($extra['userId'] ?? 0),
             'orderId' => (int)($order->id ?? 0),
             'orderSn' => $order->orderSn ?? '',
@@ -74,9 +75,64 @@ class MallRefundApplyService extends BaseService
             'status' => MallRefundApplyClass::STATUS_PENDING,
         ];
 
+        // 撤销后重新申请:复用已取消记录,把状态改回待审核,避免同一订单堆多条取消单
+        $latest = MallRefundApplyClass::getLatestByOrderId((int)($order->id ?? 0));
+        if (!empty($latest) && (int)($latest['status'] ?? -1) === MallRefundApplyClass::STATUS_CANCEL) {
+            return self::reopenCancelledApply((int)$latest['id'], $data);
+        }
+
         return MallRefundApplyClass::add($data, true);
     }
 
+    /**
+     * 将已取消申请改回待审核
+     * 顾客撤销后再次提交时复用同一条记录,刷新申请内容并清掉审核痕迹
+     * @param int $id 已取消申请 id
+     * @param array $data 新的申请字段(含 refundType/product/refundPrice/remark 等)
+     * @return object 更新后的申请对象
+     */
+    protected static function reopenCancelledApply($id, $data)
+    {
+        $apply = MallRefundApplyClass::getById($id, true);
+        if(empty($apply) || (int)$apply->status !== MallRefundApplyClass::STATUS_CANCEL) {
+            util::fail('申请状态已变化,请刷新后重试');
+        }
+
+        foreach($data as $key => $val) {
+            if($key === 'id') {
+                continue;
+            }
+            $apply->$key = $val;
+        }
+        // 重新进入待审核:清驳回/审核字段,申请时间改为本次提交时间
+        $apply->status = MallRefundApplyClass::STATUS_PENDING;
+        $apply->rejectReason = '';
+        $apply->auditAdminId = 0;
+        $apply->auditAdminName = '';
+        $apply->auditTime = null;
+        $apply->hdRefundId = 0;
+        $apply->addTime = date('Y-m-d H:i:s');
+        if(!$apply->save()) {
+            throw new \Exception('重新申请失败');
+        }
+        return $apply;
+    }
+
+    /**
+     * 申请客户 id:请求上下文 customId 为 0 时回退到原订单,保证联系客服能进会话
+     * @param array $extra
+     * @param object $order
+     * @return int
+     */
+    protected static function resolveApplyCustomId($extra, $order)
+    {
+        $customId = (int)($extra['customId'] ?? 0);
+        if ($customId > 0) {
+            return $customId;
+        }
+        return (int)($order->customId ?? 0);
+    }
+
     /**
      * 校验退货数量不超过可退数量,并补齐商品名快照
      * @param array $productList [{productId,num,unitPrice,property,name?}] property 0商品 1花材
@@ -120,6 +176,13 @@ class MallRefundApplyService extends BaseService
                     util::fail("{$name}超过可退数量");
                 }
                 $row['name'] = $row['name'] ?? ($line->name ?? '');
+                // 封面写入快照,售后列表不必再回查订单行
+                if (empty($row['cover'])) {
+                    $row['cover'] = $line->cover ?? '';
+                }
+                if (empty($row['styleName'])) {
+                    $row['styleName'] = $line->styleName ?? '';
+                }
             } else {
                 $line = $itemMap[$productId] ?? null;
                 if (empty($line)) {
@@ -134,6 +197,9 @@ class MallRefundApplyService extends BaseService
                 if (empty($row['unitName'])) {
                     $row['unitName'] = $line->unitName ?? '';
                 }
+                if (empty($row['cover'])) {
+                    $row['cover'] = $line->cover ?? '';
+                }
             }
             $enriched[] = $row;
         }