ソースを参照

Merge branch 'zhongqi-rechargeStat' into notice-260707

shish 1 ヶ月 前
コミット
3f816ef22f

+ 32 - 12
app-hd/controllers/CustomController.php

@@ -36,8 +36,12 @@ class CustomController extends BaseController
     {
         $get = Yii::$app->request->get();
         $customId = $get['customId'];
-        CustomClass::modifyBirthday($customId, $this->shopId, $get);
-        util::complete('修改成功');
+        list(, $updateResult) = CustomClass::modifyBirthday($customId, $this->shopId, $get);
+        if ($updateResult) {
+            util::complete('生日修改成功');
+        } else {
+            util::complete('客户本人设置了,您不需要修改它');
+        }
     }
 
     public function actionChangeLevel()
@@ -197,7 +201,6 @@ class CustomController extends BaseController
     {
         $get = Yii::$app->request->get();
         $type = $get['type'] ?? 0;
-        $searchStyle = $get['searchStyle'] ?? 0;
         $where = ['shopId' => $this->shopId];
         $all = CustomClass::getCount($where);
 
@@ -223,6 +226,31 @@ class CustomController extends BaseController
             $sort = 'visitTime DESC';
         }
 
+        $where = array_merge($where, $this->getListSearchWhere($get));
+        $list = CustomService::getCustomList($where, $sort);
+        $list['all'] = $all; //全部的总人数
+        $list['balance'] = $all; //余额排行的总人数
+        $list['buyAmount'] = $all; //消费排行的总人数
+        $list['birth'] = CustomClass::getCount(array_merge(['shopId' => $this->shopId], ['birthdayTime>' => 0])); //有设置生日的总人数
+        $list['deleted'] = CustomClass::getCount(array_merge(['shopId' => $this->shopId], ['delStatus' => 1])); //被删除的总人数
+        $list['vip'] = CustomClass::getCount(array_merge(['shopId' => $this->shopId], ['vip' => 1])); //是vip的总人数
+
+        util::success($list);
+    }
+
+    //客户余额统计 ssh 20260708
+    public function actionBalanceStat()
+    {
+        $get = Yii::$app->request->get();
+        $where = array_merge(['shopId' => $this->shopId], $this->getListSearchWhere($get));
+        $respond = CustomClass::getBalanceStat($where);
+        util::success($respond);
+    }
+
+    private function getListSearchWhere($get)
+    {
+        $where = [];
+        $searchStyle = $get['searchStyle'] ?? 0;
         $name = $get['name'] ?? '';
         if (empty($name)) {
             $name = $get['keyword'] ?? '';
@@ -241,15 +269,7 @@ class CustomController extends BaseController
                 }
             }
         }
-        $list = CustomService::getCustomList($where, $sort);
-        $list['all'] = $all; //全部的总人数
-        $list['balance'] = $all; //余额排行的总人数
-        $list['buyAmount'] = $all; //消费排行的总人数
-        $list['birth'] = CustomClass::getCount(array_merge(['shopId' => $this->shopId], ['birthdayTime>' => 0])); //有设置生日的总人数
-        $list['deleted'] = CustomClass::getCount(array_merge(['shopId' => $this->shopId], ['delStatus' => 1])); //被删除的总人数
-        $list['vip'] = CustomClass::getCount(array_merge(['shopId' => $this->shopId], ['vip' => 1])); //是vip的总人数
-
-        util::success($list);
+        return $where;
     }
 
     //获取客户信息 ssh 20250510

+ 86 - 8
app-hd/controllers/RechargeController.php

@@ -12,6 +12,7 @@ use bizHd\order\classes\OrderClass;
 use bizHd\recharge\classes\RechargeSqClass;
 use bizHd\wx\classes\WxOpenClass;
 use bizMall\wx\classes\WxMiniClass;
+use common\components\dateUtil;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\miniUtil;
@@ -230,18 +231,95 @@ class RechargeController extends BaseController
     {
         $get = Yii::$app->request->get();
         $customId = $get['customId'] ?? 0;
-        $custom = \bizHd\custom\classes\CustomClass::getById($customId, true);
-        if (empty($custom)) {
-            util::fail('没有找到客户');
+        $where = ['shopId' => $this->shopId, 'status' => 1];
+        if (!empty($customId)) {
+            $custom = \bizHd\custom\classes\CustomClass::getById($customId, true);
+            if (empty($custom)) {
+                util::fail('没有找到客户');
+            }
+            if ($custom->shopId != $this->shopId) {
+                util::fail('不是你的客户');
+            }
+            $where['customId'] = $customId;
         }
-        if ($custom->shopId != $this->shopId) {
-            util::fail('不是你的客户');
+
+        $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']]];
         }
-        $where = ['shopId' => $this->shopId, 'customId' => $customId];
-        $list = RechargeClass::getRechargeList($where);
+
+        $channelKey = $get['channelKey'] ?? 'all';
+        $io = null;
+        if (isset($get['io']) && $get['io'] !== '') {
+            if (!in_array((string)$get['io'], ['0', '1'], true)) {
+                util::fail('io参数错误');
+            }
+            $io = (int)$get['io'];
+        }
+        $list = \bizHd\recharge\classes\RechargeClass::getRechargeList($where, $channelKey, $io);
         util::success($list);
     }
 
+    //充值统计
+    public function actionStat()
+    {
+        $lookMoney = \bizGhs\shop\classes\ShopAdminClass::lookMoneyPower($this->shopAdmin, $this->shop);
+        if ($lookMoney == 0) {
+            util::fail('无法查看');
+        }
+
+        $shopAdmin = $this->shopAdmin;
+        if (isset($shopAdmin->super) == false || $shopAdmin->super != 1) {
+            util::fail('超管才能查看');
+        }
+
+//        $adminId = $shopAdmin->adminId ?? 0;
+//        if (in_array($adminId, [2366, 2812, 4004, 3405, 3407])) {
+//            util::fail('暂无权限');
+//        }
+
+        $get = Yii::$app->request->get();
+        $data = \bizHd\recharge\classes\RechargeClass::getStatProfile($this->shopId, $get);
+        util::success($data);
+    }
+
+    //充值记录退款:财务权限;线上原路退,线下扣减客户余额。
+    public function actionRefund()
+    {
+        $staff = $this->shopAdmin;
+        if (!isset($staff->finance) || intval($staff->finance) === 0) {
+            util::fail('请有财务权限的人操作');
+        }
+
+        $post = Yii::$app->request->post();
+        $id = intval($post['id'] ?? 0);
+        if ($id <= 0) {
+            util::fail('参数错误');
+        }
+
+        $cacheKey = 'hd_recharge_refund_' . $id;
+        $has = Yii::$app->redis->executeCommand('GET', [$cacheKey]);
+        if (!empty($has)) {
+            util::fail('请5秒之后再提交');
+        }
+        Yii::$app->redis->executeCommand('SETEX', [$cacheKey, 5, 'has']);
+
+        $connection = Yii::$app->db;
+        $transaction = $connection->beginTransaction();
+        try {
+            \bizHd\recharge\classes\RechargeClass::refundPaidRecharge($id, $this->shop, $staff);
+            $transaction->commit();
+            util::complete('退款成功');
+        } catch (\Exception $e) {
+            $transaction->rollBack();
+            Yii::info('充值退款失败:' . $e->getMessage());
+            util::fail($e->getMessage() ?: '退款失败');
+        }
+    }
+
     //免费续期活动 ssh 2021.2.21
     public function actionRenew()
     {
@@ -454,4 +532,4 @@ class RechargeController extends BaseController
         util::success(['deadline' => $deadline]);
     }
 
-}
+}

+ 3 - 1
app-mall/controllers/UserController.php

@@ -15,7 +15,6 @@ use biz\shop\classes\ShopClass;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\jwt;
-use common\components\noticeUtil;
 use common\components\stringUtil;
 use Yii;
 use common\components\util;
@@ -110,6 +109,9 @@ class UserController extends BaseController
                 $custom->birthday = $birthday;
                 $custom->birthdayTime = $birthdayTime;
                 $custom->save();
+
+                //变更客户生日的同时,同步清空含对应客户的门店的缓存
+                BirthdayGiftClass::clearWorkbenchSummaryCache($custom->shopId);
             }
         }
 

+ 73 - 20
biz-hd/birthday/classes/BirthdayGiftClass.php

@@ -241,23 +241,32 @@ class BirthdayGiftClass extends BaseClass
      */
     public static function getEligibleCustoms($shopId, $mainId)
     {
+        $errMsgs = '';
         $levelMap = self::getLevelBenefitMap($mainId, $shopId);
         $list = CustomClass::getAllByCondition([
             'shopId' => $shopId,
             'member>' => 0,
             'delStatus' => 0,
         ], 'id ASC', '*', null, true);
-        $result = [];
+        $customs = [];
         if (!empty($list)) {
             foreach ($list as $custom) {
                 if (self::hasBirthdayBenefit($custom, $levelMap)) {
-                    $result[] = $custom;
+                    $customs[] = $custom;
                 } else {
-                    noticeUtil::push('客户:' . $custom->name . ' id:' . $custom->id . ' 等级:' . $custom->member . ',没有设置生日礼物');
+                    $errMsgs .= '客户' . $custom->name . '(等级' . $custom->member . ')没有生日权益' . ', ';
                 }
             }
         }
-        return [$result, $levelMap];
+
+        if ($errMsgs != '') {
+            $msg = '门店id='.$shopId.':'.$errMsgs;
+            //noticeUtil::push($msg);
+            Yii::info($msg);
+            return [false, false];
+        }
+
+        return [$customs, $levelMap];
     }
 
     /**
@@ -268,7 +277,7 @@ class BirthdayGiftClass extends BaseClass
      */
     public static function getWorkbenchSummary($shopId, $mainId)
     {
-        $cacheKey = self::getWorkbenchSummaryCacheKey($shopId, $mainId);
+        $cacheKey = self::getWorkbenchSummaryCacheKey($shopId);
         try {
             $cached = Yii::$app->redis->get($cacheKey);
             if ($cached !== false && $cached !== null) {
@@ -286,6 +295,11 @@ class BirthdayGiftClass extends BaseClass
         $week = 0;
 
         list($customs,) = self::getEligibleCustoms($shopId, $mainId);
+        if ($customs === false) { //门店的会员等级设置异常,直接返回
+            Yii::error('门店'.$shopId.'--会员等级设置异常');
+            return [];
+        }
+
         foreach ($customs as $custom) {
             if (self::getBirthdayMonthDay($custom) === null) {
                 continue;
@@ -319,17 +333,16 @@ class BirthdayGiftClass extends BaseClass
         return $data;
     }
 
-    private static function getWorkbenchSummaryCacheKey($shopId, $mainId)
+    private static function getWorkbenchSummaryCacheKey($shopId)
     {
-        return 'hd:birthdayGift:workbenchSummary:'
-            . intval($mainId) . ':' . intval($shopId) . ':' . date('Ymd');
+        return 'hd_birthdayGift_workbenchSummary:' . intval($shopId) . ':' . date('Ymd');
     }
 
-    private static function clearWorkbenchSummaryCache($shopId, $mainId)
+    public static function clearWorkbenchSummaryCache($shopId)
     {
         try {
             Yii::$app->redis->executeCommand('DEL', [
-                self::getWorkbenchSummaryCacheKey($shopId, $mainId),
+                self::getWorkbenchSummaryCacheKey($shopId),
             ]);
         } catch (\Throwable $throwable) {
             Yii::warning('清理生日工作台缓存失败:' . $throwable->getMessage(), __METHOD__);
@@ -427,11 +440,11 @@ class BirthdayGiftClass extends BaseClass
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
         try {
-            $custom = CustomClass::modifyBirthday($customId, $shop->id, $params);
+            list($custom, $isUpdateBirthday) = CustomClass::modifyBirthday($customId, $shop->id, $params);
             $levelMap = self::getLevelBenefitMap($shop->mainId, $shop->id);
             $gift = self::createOrUpdateYearRecord($shop, $custom, $levelMap);
             $transaction->commit();
-            self::clearWorkbenchSummaryCache($shop->id, $shop->mainId);
+            self::clearWorkbenchSummaryCache($shop->id);
             return $gift;
         } catch (\Exception $exception) {
             $transaction->rollBack();
@@ -450,8 +463,9 @@ class BirthdayGiftClass extends BaseClass
         $shopId = intval($shop->id);
         $year = intval(date('Y')); //今年
         list($customs, $levelMap) = self::getEligibleCustoms($shopId, $shop->mainId);
-        if (empty($customs)) {
-            return true;
+        if ($customs === false) {
+            Yii::error('会员权益配置异常');
+            return false;
         }
         $existingRows = self::getAllByCondition(['shopId' => $shopId, 'year' => $year], null, 'customId');
         $existingIds = [];
@@ -511,7 +525,10 @@ class BirthdayGiftClass extends BaseClass
      */
     public static function getBoardList($shop, $params)
     {
-        self::ensureYearRecords($shop);
+        $re = self::ensureYearRecords($shop);
+        if ($re === false) {
+            return [];
+        }
         $period = $params['period'] ?? 'week';
         $statusFilter = isset($params['status']) ? intval($params['status']) : -1;
         $searchText = trim($params['searchText'] ?? $params['keyword'] ?? '');
@@ -519,6 +536,10 @@ class BirthdayGiftClass extends BaseClass
         $pageSize = max(1, min(50, intval($params['pageSize'] ?? 20)));
 
         list($customs,) = self::getEligibleCustoms($shop->id, $shop->mainId);
+        if ($customs === false) {
+            Yii::error('会员权益配置异常');
+            return [];
+        }
         $customMap = [];
         foreach ($customs as $c) {
             $customMap[$c->id] = $c;
@@ -583,9 +604,17 @@ class BirthdayGiftClass extends BaseClass
 
     public static function getStatusCounts($shop, $params)
     {
-        self::ensureYearRecords($shop);
+        $re = self::ensureYearRecords($shop);
+        if ($re === false) {
+            return [];
+        }
         $period = $params['period'] ?? 'week';
         list($customs,) = self::getEligibleCustoms($shop->id, $shop->mainId);
+        if ($customs === false) {
+            Yii::error('会员权益配置异常');
+            return [];
+        }
+
         $customMap = [];
         foreach ($customs as $c) {
             $customMap[$c->id] = $c;
@@ -634,8 +663,15 @@ class BirthdayGiftClass extends BaseClass
 
     public static function getGiftStats($shop, $period)
     {
-        self::ensureYearRecords($shop);
+        $re = self::ensureYearRecords($shop);
+        if ($re === false) {
+            return [];
+        }
         list($customs,) = self::getEligibleCustoms($shop->id, $shop->mainId);
+        if ($customs === false) {
+            Yii::error('会员权益配置异常');
+            return [];
+        }
         $customMap = [];
         foreach ($customs as $c) {
             $customMap[$c->id] = $c;
@@ -770,7 +806,7 @@ class BirthdayGiftClass extends BaseClass
             return [false, '短信发送失败'];
         }
 
-        // TODO 记录到 xhSms
+        // 记录到 xhSms
         $sms = new SmsClass();
         $sms->add([
             'mainId' => $shop->mainId,
@@ -786,8 +822,22 @@ class BirthdayGiftClass extends BaseClass
 
     public static function batchNotifyTomorrow($shop)
     {
-        self::ensureYearRecords($shop);
+        $re = self::ensureYearRecords($shop);
+        if ($re === false) {
+            return [
+                'success' => 0,
+                'fail' => 1,
+                'messages' => ['会员等级设置异常'],
+            ];
+        }
         list($customs, $levelMap) = self::getEligibleCustoms($shop->id, $shop->mainId);
+        if ($customs === false) {
+            return [
+                'success' => 0,
+                'fail' => 1,
+                'messages' => ['会员等级设置异常'],
+            ];
+        }
         $success = 0;
         $fail = 0;
         $messages = [];
@@ -908,7 +958,10 @@ class BirthdayGiftClass extends BaseClass
 
     public static function notifyOneTomorrow($shop, $giftId)
     {
-        self::ensureYearRecords($shop);
+        $re = self::ensureYearRecords($shop);
+        if ($re === false) {
+            util::fail('会员等级设置异常');
+        }
         $gift = self::getById($giftId, true);
         if (empty($gift) || intval($gift->shopId) != intval($shop->id)) {
             util::fail('记录不存在');

+ 38 - 2
biz-hd/custom/classes/CustomClass.php

@@ -27,7 +27,41 @@ class CustomClass extends BaseClass
 
     public static $baseFile = '\bizHd\custom\models\Custom';
 
-    //修改客户生日 ssh 20250814
+    public static function getBalanceStat($where)
+    {
+        $cacheKey = self::getBalanceStatCacheKey($where);
+        $cached = \Yii::$app->redis->executeCommand('GET', [$cacheKey]);
+        if ($cached !== false && $cached !== null && $cached !== '') {
+            $data = json_decode($cached, true);
+            if (is_array($data)) {
+                return $data;
+            }
+        }
+
+        $countWhere = array_merge($where, ['balance>' => 0]);
+        $count = self::getCount($countWhere);
+        $totalBalance = self::sum($where, 'balance');
+        $data = [
+            'count' => intval($count),
+            'totalBalance' => bcadd($totalBalance ?: '0', '0', 2),
+        ];
+        \Yii::$app->redis->executeCommand('SETEX', [$cacheKey, 60, json_encode($data)]);
+        return $data;
+    }
+
+    public static function getBalanceStatCacheKey($where)
+    {
+        return 'hd_custom_balance_stat:' . md5(json_encode($where, JSON_UNESCAPED_UNICODE));
+    }
+
+    /**
+     * 修改客户生日
+     * @param $customId
+     * @param $shopId
+     * @param $params
+     * @return array [custom, bool] custom表示客户数据,bool表示是否变动了生日
+     * @throws \Exception
+     */
     public static function modifyBirthday($customId, $shopId, $params)
     {
         date_default_timezone_set('PRC');
@@ -58,6 +92,8 @@ class CustomClass extends BaseClass
             util::fail('农历没有31号');
         }
 
+        $isUpdateBirthday = $user->birthdayTime == 0;
+
         // 如果 xhUser 中已经设置了生日,则直接取用
         if ($user->birthdayTime != 0) {
             $custom->lunar = $user->lunar;
@@ -122,7 +158,7 @@ class CustomClass extends BaseClass
             $birthdayGift->save();
         }
 
-        return $custom;
+        return [$custom, $isUpdateBirthday];
     }
 
     //用余额支付,客户余额减少 ssh 20250410

+ 494 - 2
biz-hd/recharge/classes/RechargeClass.php

@@ -17,17 +17,97 @@ use bizHd\shop\classes\ShopExtClass;
 use bizHd\shop\classes\MainClass;
 use bizHd\user\classes\UserGrowthClass;
 use bizHd\user\classes\UserIntegralClass;
+use common\components\dateUtil;
 use common\components\dict;
 use common\components\lakala\Lakala;
 use common\components\noticeUtil;
+use common\components\orderSn;
 use common\components\util;
 use Yii;
 use bizHd\base\classes\BaseClass;
+use yii\db\Expression;
 
 class RechargeClass extends BaseClass
 {
 
     public static $baseFile = '\bizHd\recharge\models\Recharge';
+    const STAT_CACHE_TTL = 300; //统计数据缓存时长
+
+    //充值记录
+    public static function getRechargeList($where, $channelKey = 'all', $io = null)
+    {
+        switch ($channelKey) {
+            case 'system':
+                $where['onlinePay'] = 2;
+                break;
+            case 'online_0':
+                $where['onlinePay'] = 2;
+                $where['payWay'] = 0;
+                break;
+            case 'online_1':
+                $where['onlinePay'] = 2;
+                $where['payWay'] = 1;
+                break;
+            case 'pay_0':
+                $where['onlinePay!='] = 2;
+                $where['payWay'] = 0;
+                break;
+            case 'pay_1':
+                $where['onlinePay!='] = 2;
+                $where['payWay'] = 1;
+                break;
+            case 'pay_4':
+                $where['onlinePay!='] = 2;
+                $where['payWay'] = 4;
+                break;
+            case 'pay_5':
+                $where['onlinePay!='] = 2;
+                $where['payWay'] = 5;
+                break;
+            case 'other':
+                $where['onlinePay!='] = 2;
+                $where['payWay'] = ['not in', [0, 1, 4, 5]];
+                break;
+            default:
+                break;
+        }
+
+        if ($io === null || $io === '') {
+            return self::getList('*', $where, 'addTime DESC,id DESC');
+        }
+
+        $model = self::getModel();
+        $query = $model->conditionQuery($where)->select('*');
+        if ((int)$io === 1) {
+            $query->andWhere('IFNULL(amount, 0) + IFNULL(giveAmount, 0) > 0');
+        } else {
+            $query->andWhere(['isRefund' => 1]);
+        }
+
+        return self::getRechargePageList($query, 'addTime DESC,id DESC');
+    }
+
+    private static function getRechargePageList($query, $order = '')
+    {
+        $get = Yii::$app->request->get();
+        $page = isset($get['page']) ? $get['page'] : 1;
+        Yii::$app->params['page'] = $page;
+        $pageSize = isset($get['pageSize']) && !empty($get['pageSize']) ? $get['pageSize'] : Yii::$app->params['pageSize'];
+        $offset = ($page - 1) * $pageSize;
+
+        $clone = clone $query;
+        $count = $clone->count();
+        $totalPage = ceil($count / $pageSize);
+        $data['totalNum'] = $count;
+        $data['totalPage'] = $totalPage;
+        $data['moreData'] = $totalPage > $page ? 1 : 0;
+
+        if (!empty($order)) {
+            $query->orderBy($order);
+        }
+        $data['list'] = $query->offset($offset)->limit($pageSize)->asArray()->all();
+        return $data;
+    }
 
     public static function addRecharge($data)
     {
@@ -91,7 +171,7 @@ class RechargeClass extends BaseClass
     }
 
     //第三支付后流程 ssh 2021.4.27
-    public static function thirdPay($payWay, $orderSn, $totalFee, $attach)
+    public static function thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId = '')
     {
         $recharge = RechargeClass::getByCondition(['orderSn' => $orderSn], true);
         if (empty($recharge)) {
@@ -112,6 +192,8 @@ class RechargeClass extends BaseClass
             'onlinePay' => 2,
             'side' => 1,
         ];
+        $recharge->returnCode = $transactionId;
+        $recharge->save(false);
         self::complete($recharge, $payWay, $params);
         return $recharge;
     }
@@ -382,6 +464,11 @@ class RechargeClass extends BaseClass
             }
         }
 
+        $recharge->settleId = $settleId;
+        $recharge->settleAmount = $settleAmount;
+        $recharge->settleNo = $settleNo;
+        $recharge->save();
+
         $remark = $recharge->remark;
         $change = [
             'customId' => $customId,
@@ -521,6 +608,236 @@ class RechargeClass extends BaseClass
         return $returnChange;
     }
 
+    /**
+     * 花店后台:对已付款客户充值单退款。线上原路退,线下扣减客户余额;xhRecharge.isRefund 标记退款状态。
+     */
+    public static function refundPaidRecharge($rechargeId, $shop, $staff)
+    {
+        $rechargeId = intval($rechargeId);
+        if ($rechargeId <= 0) {
+            util::fail('参数错误');
+        }
+        $recharge = self::getLockById($rechargeId);
+        if (empty($recharge)) {
+            util::fail('没有找到充值记录');
+        }
+        if (intval($recharge->mainId ?? 0) !== intval($shop->mainId ?? 0) || intval($recharge->shopId ?? 0) !== intval($shop->id ?? 0)) {
+            util::fail('没有权限操作该充值记录');
+        }
+        if (intval($recharge->payStatus ?? 0) !== 1 || intval($recharge->status ?? 0) !== 1) {
+            util::fail('仅已付款的充值可退款');
+        }
+        if (self::isRechargeRefunded($recharge)) {
+            util::fail('该充值已退款');
+        }
+
+        $amount = bcadd((string)($recharge->amount ?? '0'), '0', 2);
+        if (bccomp($amount, '0', 2) <= 0) {
+            util::fail('充值金额有误');
+        }
+
+        $customId = intval($recharge->customId ?? 0);
+        $hdId = intval($recharge->hdId ?? 0);
+        $custom = CustomClass::getLockById($customId);
+        $hd = HdClass::getLockById($hdId);
+        if (empty($custom) || empty($hd)) {
+            util::fail('没有找到客户余额账户');
+        }
+
+        $isOnline = intval($recharge->onlinePay ?? 0) === 2;
+        $payWay = intval($recharge->payWay ?? -1);
+        $wxPay = intval(dict::getDict('payWay', 'wxPay'));
+        $aliPay = intval(dict::getDict('payWay', 'alipay'));
+        $cashPay = intval(dict::getDict('payWay', 'cash'));
+        $onlineMain = null;
+
+        if ($isOnline) {
+            if ($payWay !== $wxPay && $payWay !== $aliPay) {
+                util::fail('暂不支持该支付方式原路退款');
+            }
+            $returnCode = trim((string)($recharge->returnCode ?? ''));
+            if ($returnCode === '') {
+                util::fail('缺少支付流水号,无法原路退款');
+            }
+            $onlineMain = MainClass::getLockById($shop->mainId ?? 0);
+            if (empty($onlineMain)) {
+                util::fail('没有找到资产信息');
+            }
+            if (bccomp((string)($onlineMain->balance ?? '0'), $amount, 2) < 0) {
+                util::fail('门店余额不足,无法退款');
+            }
+            self::refundOnlineByLakala($recharge, $shop, $amount, $returnCode);
+        }
+
+        $giveAmount = bcadd((string)($recharge->giveAmount ?? '0'), '0', 2);
+        if (bccomp($giveAmount, '0', 2) < 0) {
+            $giveAmount = '0.00';
+        }
+        $currentGive = bcadd((string)($hd->balanceGive ?? '0'), '0', 2);
+        $deductGive = '0.00';
+        if (bccomp($giveAmount, '0', 2) === 1 && bccomp($currentGive, '0', 2) === 1) {
+            $deductGive = bccomp($currentGive, $giveAmount, 2) >= 0 ? $giveAmount : $currentGive;
+        }
+        $deductPay = bcadd($amount, bcsub($giveAmount, $deductGive, 2), 2);
+        $totalDeduct = bcadd($deductPay, $deductGive, 2);
+
+        $custom->balancePay = bcsub((string)($custom->balancePay ?? '0'), $deductPay, 2);
+        $hd->balancePay = bcsub((string)($hd->balancePay ?? '0'), $deductPay, 2);
+        if (bccomp($deductGive, '0', 2) === 1) {
+            $custom->balanceGive = bcsub((string)($custom->balanceGive ?? '0'), $deductGive, 2);
+            $hd->balanceGive = bcsub((string)($hd->balanceGive ?? '0'), $deductGive, 2);
+        }
+        $custom->balance = bcsub((string)($custom->balance ?? '0'), $totalDeduct, 2);
+        $hd->balance = bcsub((string)($hd->balance ?? '0'), $totalDeduct, 2);
+        $custom->save(false);
+        $hd->save(false);
+
+        if (floatval($custom->balance) != floatval($hd->balance) || floatval($custom->balancePay) != floatval($hd->balancePay) || floatval($custom->balanceGive) != floatval($hd->balanceGive)) {
+            util::fail('账户余额有问题,请联系管理员');
+        }
+
+        $staffId = intval($staff->id ?? 0);
+        $staffName = (string)($staff->name ?? '');
+        $capitalType = dict::getDict('capitalType', 'customRechargeRefund', 'id');
+        $event = '充值退款(操作人:' . $staffName . ')单号:' . ($recharge->orderSn ?? '');
+        $changeBase = [
+            'hdId' => $hdId,
+            'hdName' => $hd->name ?? ($recharge->hdName ?? ''),
+            'customId' => $customId,
+            'customName' => $custom->name ?? ($recharge->customName ?? ''),
+            'relateId' => $rechargeId,
+            'onlinePay' => $recharge->onlinePay ?? 1,
+            'capitalType' => $capitalType,
+            'side' => 0,
+            'payWay' => $payWay,
+            'event' => $event,
+            'staffId' => $staffId,
+            'staffName' => $staffName,
+            'shopId' => $recharge->shopId ?? 0,
+            'mainId' => $recharge->mainId ?? 0,
+            'remark' => '',
+        ];
+
+        BalanceChangeClass::add(array_merge($changeBase, [
+            'amount' => $totalDeduct,
+            'balance' => $hd->balance,
+            'io' => 0,
+        ]), true);
+
+        BalancePayChangeClass::add(array_merge($changeBase, [
+            'amount' => $deductPay,
+            'balance' => $hd->balancePay,
+            'io' => 0,
+        ]), true);
+
+        if (bccomp($deductGive, '0', 2) === 1) {
+            BalanceGiveChangeClass::add(array_merge($changeBase, [
+                'amount' => $deductGive,
+                'balance' => $hd->balanceGive,
+                'io' => 0,
+            ]), true);
+        }
+
+        if ($isOnline) {
+            $main = $onlineMain ?: MainClass::getLockById($shop->mainId ?? 0);
+            if (empty($main)) {
+                util::fail('没有找到资产信息');
+            }
+            ShopClass::customRechargeRefundReduceBalance($main, $shop, $recharge, dict::getDict('capitalType', 'xhRecharge', 'id'), $staffName);
+        } elseif ($payWay === $cashPay) {
+            self::cashRechargeRefundReduceMoney($recharge, $shop, $staffName, $amount);
+        }
+
+        self::markRechargeRefunded($recharge);
+        $recharge->balance = $hd->balance;
+        $recharge->save(false);
+
+        return $recharge;
+    }
+
+    protected static function cashRechargeRefundReduceMoney($recharge, $shop, $staffName, $amount)
+    {
+        $main = MainClass::getLockById($shop->mainId ?? 0);
+        if (empty($main)) {
+            util::fail('没有找到现金账户');
+        }
+        if (bccomp((string)($main->money ?? '0'), $amount, 2) < 0) {
+            util::fail('现金不足' . floatval($amount) . '元');
+        }
+        $main->money = bcsub((string)$main->money, $amount, 2);
+        $main->save(false);
+        ShopMoneyClass::addData([
+            'mainId' => $shop->mainId ?? 0,
+            'sjId' => 0,
+            'amount' => $amount,
+            'balance' => $main->money,
+            'io' => 0,
+            'staffId' => $recharge->staffId ?? 0,
+            'staffName' => $staffName,
+            'customName' => $recharge->customName ?? '',
+            'ptStyle' => dict::getDict('ptStyle', 'hd'),
+            'capitalType' => dict::getDict('capitalType', 'xhRecharge', 'id'),
+            'event' => ($recharge->customName ?? '客户') . '现金充值退款' . floatval($amount) . '元',
+            'remark' => '操作人:' . $staffName,
+        ]);
+    }
+
+    protected static function refundOnlineByLakala($recharge, $shop, $amount, $returnCode)
+    {
+        $termNo = $shop->lklScanTermNo ?? '';
+        if (intval($recharge->payWay ?? 0) === intval(dict::getDict('payWay', 'alipay'))) {
+            $termNo = $shop->lklB2BTermNo ?? '';
+        }
+        if (empty($shop->lklSjNo) || empty($termNo)) {
+            util::fail('门店未配置拉卡拉商户信息');
+        }
+        $merchantPrivateKeyPath = Yii::getAlias('@vendor/lakala') . '/production/api_private_key.pem';
+        $lklCertificatePath = Yii::getAlias('@vendor/lakala') . '/production/lkl-apigw-v1.cer';
+        $laResource = new Lakala([
+            'appid' => 'OP00002119',
+            'serial_no' => '018b08cfddbd',
+            'merchant_no' => $shop->lklSjNo,
+            'term_no' => $termNo,
+            'merchantPrivateKeyPath' => $merchantPrivateKeyPath,
+            'lklCertificatePath' => $lklCertificatePath,
+        ]);
+        $response = $laResource->refund([
+            'refundSn' => orderSn::getRechargeSn(),
+            'orderSn' => $recharge->orderSn ?? '',
+            'refundAmount' => bcmul($amount, 100),
+            'refundReason' => '充值退款',
+            'thirdNo' => $returnCode,
+        ]);
+        if (!isset($response['code']) || $response['code'] != 'BBS00000') {
+            util::fail('原路退款失败:' . ($response['msg'] ?? '退款失败'));
+        }
+    }
+
+    protected static function isRechargeRefunded($recharge)
+    {
+        if (empty($recharge)) {
+            return true;
+        }
+        if (self::modelHasAttr($recharge, 'isRefund')) {
+            return intval($recharge->isRefund ?? 0) === 1;
+        }
+        return false;
+    }
+
+    protected static function markRechargeRefunded($recharge)
+    {
+        if (empty($recharge) || !self::modelHasAttr($recharge, 'isRefund')) {
+            return;
+        }
+        $recharge->isRefund = 1;
+        $recharge->save(false, ['isRefund']);
+    }
+
+    protected static function modelHasAttr($model, $attr)
+    {
+        return is_object($model) && method_exists($model, 'hasAttribute') && $model->hasAttribute($attr);
+    }
+
     /**
      * 售后付款单是否已经充值过了 ssh 20250801
      */
@@ -533,4 +850,179 @@ class RechargeClass extends BaseClass
         }
     }
 
-}
+    public static function getStatProfile($shopId, $params = [])
+    {
+        $searchTime = $params['searchTime'] ?? 'today';
+        $startTime = $params['startTime'] ?? '';
+        $endTime = $params['endTime'] ?? '';
+        $range = dateUtil::formatTime($searchTime, $startTime, $endTime);
+        $start = $range['startTime'];
+        $end = $range['endTime'];
+        $cacheKey = 'hd_recharge_stat:' . $shopId . '_' . md5($start . '_' . $end);
+        $cache = Yii::$app->redis->executeCommand('GET', [$cacheKey]);
+        if (!empty($cache)) {
+            $data = json_decode($cache, true);
+            if (is_array($data)) {
+                return $data;
+            }
+        }
+
+        $summary = self::getRechargeStatSummary($shopId, $start, $end);
+        $channelList = self::getRechargeStatChannelList($shopId, $start, $end);
+        $customerList = self::getRechargeStatCustomerList($shopId, $start, $end);
+
+        $data = [
+            'summary' => $summary,
+            'channelList' => $channelList,
+            'customerList' => $customerList,
+            'startTime' => $start,
+            'endTime' => $end,
+        ];
+        Yii::$app->redis->executeCommand('SETEX', [$cacheKey, self::STAT_CACHE_TTL, json_encode($data, JSON_UNESCAPED_UNICODE)]);
+        return $data;
+    }
+
+    private static function getPaidQuery($shopId, $start, $end)
+    {
+        $model = self::getActiveRecord();
+        return $model::find()
+            ->where([
+                'shopId' => $shopId,
+                'payStatus' => 1,
+                'status' => 1,
+            ])
+            ->andWhere(['between', 'addTime', $start, $end]);
+    }
+
+    private static function getRechargeStatSummary($shopId, $start, $end)
+    {
+        $row = self::getPaidQuery($shopId, $start, $end)
+            ->select([
+                'rechargeCount' => new Expression('SUM(CASE WHEN IFNULL(amount, 0) + IFNULL(giveAmount, 0) > 0 THEN 1 ELSE 0 END)'),
+                'reduceCount' => new Expression('SUM(CASE WHEN IFNULL(isRefund, 0) = 1 THEN 1 ELSE 0 END)'),
+                'realAmount' => new Expression('IFNULL(SUM(amount), 0)'),
+                'giveAmount' => new Expression('IFNULL(SUM(giveAmount), 0)'),
+                'reduceAmount' => new Expression('IFNULL(SUM(CASE WHEN IFNULL(isRefund, 0) = 1 THEN amount ELSE 0 END), 0)'),
+            ])
+            ->asArray()
+            ->one();
+
+        $realAmount = $row['realAmount'] ?? 0;
+        $giveAmount = $row['giveAmount'] ?? 0;
+        $reduceAmount = $row['reduceAmount'] ?? 0;
+        return [
+            'rechargeAmount' => self::moneyAdd($realAmount, $giveAmount),
+            'rechargeCount' => intval($row['rechargeCount'] ?? 0),
+            'reduceAmount' => self::moneyValue($reduceAmount),
+            'reduceCount' => intval($row['reduceCount'] ?? 0),
+            'realAmount' => self::moneyValue($realAmount),
+        ];
+    }
+
+    private static function getRechargeStatChannelList($shopId, $start, $end)
+    {
+        $rows = self::getPaidQuery($shopId, $start, $end)
+            ->select([
+                'channelKey' => new Expression("IF(onlinePay = 2, CONCAT('online_', payWay), CONCAT('pay_', payWay))"),
+                'rechargeCount' => new Expression('SUM(CASE WHEN IFNULL(amount, 0) + IFNULL(giveAmount, 0) > 0 THEN 1 ELSE 0 END)'),
+                'reduceCount' => new Expression('SUM(CASE WHEN IFNULL(isRefund, 0) = 1 THEN 1 ELSE 0 END)'),
+                'realAmount' => new Expression('IFNULL(SUM(amount), 0)'),
+                'giveAmount' => new Expression('IFNULL(SUM(giveAmount), 0)'),
+                'reduceAmount' => new Expression('IFNULL(SUM(CASE WHEN IFNULL(isRefund, 0) = 1 THEN amount ELSE 0 END), 0)'),
+            ])
+            ->groupBy(new Expression("IF(onlinePay = 2, CONCAT('online_', payWay), CONCAT('pay_', payWay))"))
+            ->asArray()
+            ->all();
+
+        $payWayName = dict::getDict('payWayName');
+        $defaultList = [
+            'online_0' => '线上微信',
+            'online_1' => '线上支付宝',
+            'pay_0' => '线下微信',
+            'pay_1' => '线下支付宝',
+            'pay_4' => '现金',
+            'pay_5' => '银行卡',
+        ];
+        $map = [];
+        foreach ($rows as $row) {
+            $key = $row['channelKey'] ?? '';
+            $payWay = intval(str_replace('pay_', '', $key));
+            $map[$key] = self::buildStatRow(
+                $defaultList[$key] ?? ($payWayName[$payWay] ?? '其它'),
+                $row
+            );
+        }
+
+        $list = [];
+        foreach ($defaultList as $key => $name) {
+            $list[] = $map[$key] ?? self::emptyStatRow($name);
+            unset($map[$key]);
+        }
+        foreach ($map as $item) {
+            $list[] = $item;
+        }
+        return $list;
+    }
+
+    private static function getRechargeStatCustomerList($shopId, $start, $end)
+    {
+        $rows = self::getPaidQuery($shopId, $start, $end)
+            ->select([
+                'customId',
+                'customName',
+                'rechargeCount' => new Expression('SUM(CASE WHEN IFNULL(amount, 0) + IFNULL(giveAmount, 0) > 0 THEN 1 ELSE 0 END)'),
+                'reduceCount' => new Expression('SUM(CASE WHEN IFNULL(isRefund, 0) = 1 THEN 1 ELSE 0 END)'),
+                'realAmount' => new Expression('IFNULL(SUM(amount), 0)'),
+                'giveAmount' => new Expression('IFNULL(SUM(giveAmount), 0)'),
+                'reduceAmount' => new Expression('IFNULL(SUM(CASE WHEN IFNULL(isRefund, 0) = 1 THEN amount ELSE 0 END), 0)'),
+            ])
+            ->groupBy(['customId', 'customName'])
+            ->orderBy(new Expression('IFNULL(SUM(amount), 0) + IFNULL(SUM(giveAmount), 0) DESC'))
+            ->limit(100)
+            ->asArray()
+            ->all();
+
+        $list = [];
+        foreach ($rows as $row) {
+            $list[] = self::buildStatRow($row['customName'] ?: '未命名', $row);
+        }
+        return $list;
+    }
+
+    private static function buildStatRow($name, $row)
+    {
+        $realAmount = $row['realAmount'] ?? 0;
+        $giveAmount = $row['giveAmount'] ?? 0;
+        return [
+            'customId' => $row['customId'] ?? 0,
+            'name' => $name,
+            'rechargeAmount' => self::moneyAdd($realAmount, $giveAmount),
+            'rechargeCount' => intval($row['rechargeCount'] ?? 0),
+            'reduceAmount' => self::moneyValue($row['reduceAmount'] ?? 0),
+            'reduceCount' => intval($row['reduceCount'] ?? 0),
+            'realAmount' => self::moneyValue($realAmount),
+        ];
+    }
+
+    private static function emptyStatRow($name)
+    {
+        return [
+            'name' => $name,
+            'rechargeAmount' => '0.00',
+            'rechargeCount' => 0,
+            'reduceAmount' => '0.00',
+            'reduceCount' => 0,
+            'realAmount' => '0.00',
+        ];
+    }
+
+    private static function moneyAdd($left, $right)
+    {
+        return self::moneyValue(bcadd((string)$left, (string)$right, 2));
+    }
+
+    private static function moneyValue($amount)
+    {
+        return number_format((float)$amount, 2, '.', '');
+    }
+}

+ 8 - 0
common/components/dateUtil.php

@@ -99,6 +99,14 @@ class dateUtil
                     $endTime = date("Ymd", strtotime($endTime));
                 }
                 break;
+            case 'all':
+                $startTime = "2015-01-01 00:00:00"; //用项目起始日期做开始时间
+                $endTime = date("Y-m-d 23:59:59");
+                if ($Stat) {
+                    $startTime = "20150101";
+                    $endTime = date("Ymd");
+                }
+                break;
             default:
                 //今天
                 $startTime = date("Y-m-d 00:00:00");

+ 1 - 1
common/components/payUtil.php

@@ -32,7 +32,7 @@ class payUtil
         switch ($capitalType) {
             case dict::getDict('capitalType', 'xhRecharge', 'id'):
                 //散客向花店充值
-                RechargeClass::thirdPay($payWay, $orderSn, $totalFee, $attach);
+                RechargeClass::thirdPay($payWay, $orderSn, $totalFee, $attach, $transactionId);
                 break;
             case dict::getDict('capitalType', 'xhPurchase', 'id'):
                 //零售采购

+ 38 - 0
scripts/sql/optimize_xh_recharge_hd_stat.sql

@@ -0,0 +1,38 @@
+-- =============================================================================
+-- hdApp 充值记录/统计查询索引建议 - xhRecharge
+-- =============================================================================
+-- 当前查询形态:
+-- 1. /recharge/list:
+--    shopId + status,按 addTime DESC,id DESC 分页;
+--    可选 customId / onlinePay + payWay / isRefund。
+-- 2. /recharge/stat:
+--    shopId + payStatus + status + addTime 时间范围;
+--    按渠道或客户聚合,并用 isRefund 统计减少金额。
+--
+-- 当前表已有索引:
+-- PRIMARY(id), orderSn(orderSn), custom_status(customId,status),
+-- main_custom_status(mainId,customId,status)。
+-- 这些索引缺少 shopId、payStatus、addTime、isRefund,对当前 hdApp 统计和分页查询帮助有限。
+--
+-- 执行前建议先在目标环境 EXPLAIN /recharge/list 与 /recharge/stat 的 SQL;
+-- 大表请在低峰期执行,并按 MySQL 版本确认 ALGORITHM/LOCK 支持情况。
+-- =============================================================================
+
+ALTER TABLE `xhRecharge`
+    ADD COLUMN `isRefund` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否退款:0不是 1是' AFTER status;
+
+ALTER TABLE `xhRecharge`
+    ADD INDEX `idx_hd_recharge_list` (`shopId`, `status`, `addTime`),
+    ALGORITHM = INPLACE,
+    LOCK = NONE;
+
+ALTER TABLE `xhRecharge`
+    ADD INDEX `idx_hd_recharge_stat` (`shopId`, `payStatus`, `status`, `addTime`),
+    ALGORITHM = INPLACE,
+    LOCK = NONE;
+
+-- 如果客户维度明细页仍然慢,再评估增加这个索引;不要在未验证前盲目增加写入开销。
+-- ALTER TABLE `xhRecharge`
+--     ADD INDEX `idx_hd_recharge_custom_list` (`shopId`, `customId`, `status`, `addTime`),
+--     ALGORITHM = INPLACE,
+--     LOCK = NONE;