Browse Source

Merge remote-tracking branch 'origin/redesign‌-260706' into redesign‌-260706

ouyang 4 weeks ago
parent
commit
b993c8c5a7

+ 1 - 1
app-ghs/controllers/ApplyController.php

@@ -29,7 +29,7 @@ class ApplyController extends BaseController
     {
         $get = Yii::$app->request->get();
         $mobile = $get['mobile'] ?? '';
-        if (stringUtil::isMobile($mobile) == false) {
+        if (!stringUtil::isMobile($mobile)) {
             util::fail('请输入正确手机号');
         }
         $rand = rand(11111, 99999);

+ 120 - 0
app-ghs/controllers/GhsNoticeController.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace ghs\controllers;
+
+use bizGhs\shop\classes\GhsNoticeClass;
+use common\components\util;
+use Yii;
+
+/**
+ * 批发端通知公告
+ */
+class GhsNoticeController extends BaseController
+{
+    /**
+     * 管理端公告列表
+     */
+    public function actionList()
+    {
+        $get = Yii::$app->request->get();
+        $list = GhsNoticeClass::searchList([
+            'mainId' => $this->mainId,
+            'title' => $get['title'] ?? '',
+            'isDel' => 0,
+            'order' => 'id DESC',
+        ]);
+        util::success($list);
+    }
+
+    /**
+     * 客户端公告列表(仅显示中的公告)
+     */
+    public function actionShowList()
+    {
+        $get = Yii::$app->request->get();
+        $list = GhsNoticeClass::searchList([
+            'mainId' => $this->mainId,
+            'title' => $get['title'] ?? '',
+            'position' => $get['position'] ?? '',
+            'isDel' => 0,
+            'status' => 1,
+            'order' => 'sort DESC, id DESC',
+        ]);
+        util::success($list);
+    }
+
+    /**
+     * 新增公告
+     */
+    public function actionAdd()
+    {
+        $post = Yii::$app->request->post();
+        $post['mainId'] = $this->mainId;
+        $post['staffId'] = intval($this->shopAdminId);
+        $id = GhsNoticeClass::addNotice($post);
+        $info = GhsNoticeClass::getDetail($id);
+        util::success($info);
+    }
+
+    /**
+     * 更新公告
+     */
+    public function actionUpdate()
+    {
+        $post = Yii::$app->request->post();
+        $id = intval($post['id'] ?? 0);
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = GhsNoticeClass::getById($id);
+        GhsNoticeClass::valid($info, $this->mainId);
+        $post['staffId'] = intval($this->shopAdminId);
+        GhsNoticeClass::updateNotice($id, $post);
+        util::complete('修改成功');
+    }
+
+    /**
+     * 公告详情
+     */
+    public function actionDetail()
+    {
+        $id = intval(Yii::$app->request->get('id', 0));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = GhsNoticeClass::getById($id);
+        GhsNoticeClass::valid($info, $this->mainId);
+        util::success(GhsNoticeClass::getDetail($id));
+    }
+
+    /**
+     * 软删除公告
+     */
+    public function actionDelete()
+    {
+        $id = intval(Yii::$app->request->post('id', Yii::$app->request->get('id', 0)));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = GhsNoticeClass::getById($id);
+        GhsNoticeClass::valid($info, $this->mainId);
+        GhsNoticeClass::deleteNotice($id, intval($this->shopAdminId));
+        util::complete('删除成功');
+    }
+
+    /**
+     * 上下架公告
+     */
+    public function actionUpdateStatus()
+    {
+        $id = intval(Yii::$app->request->post('id', Yii::$app->request->get('id', 0)));
+        $status = intval(Yii::$app->request->post('status', Yii::$app->request->get('status', 0)));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = GhsNoticeClass::getById($id);
+        GhsNoticeClass::valid($info, $this->mainId);
+        GhsNoticeClass::updateStatus($id, $status, intval($this->shopAdminId));
+        util::complete('操作成功');
+    }
+}

+ 4 - 4
app-ghs/controllers/TestController.php

@@ -842,7 +842,7 @@ class TestController extends BaseController
 
     public function actionClearQuery()
     {
-        $shopId = 8249;
+        $shopId = 91064;
         $shop = ShopClass::getById($shopId, true);
         if (empty($shop)) {
             util::stop('没有找到门店信息呢');
@@ -858,12 +858,12 @@ class TestController extends BaseController
             'lklCertificatePath' => $lklCertificatePath,
         ];
         $laResource = new Lakala($params);
-        $orderSn = 'CL29416751';
+        $orderSn = 'CL29443345';
         $queryParams = [
             'orderSn' => $orderSn,
-            //'tradeNo' => '66222923861204',
+            'tradeNo' => '66224623283235',
         ];
-        $response = $laResource->query($queryParams, 0);
+        $response = $laResource->query($queryParams, 1);
         echo "<pre>";
         print_r($response);
         util::stop();

+ 1 - 1
app-hd/controllers/ApplyController.php

@@ -326,4 +326,4 @@ class ApplyController extends BaseController
         util::success(['code' => rand(1111, 9999)]);
     }
 
-}
+}

+ 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

+ 61 - 1
app-hd/controllers/GhsController.php

@@ -8,6 +8,7 @@ use bizGhs\custom\classes\AccountMoneyClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\custom\classes\CustomLevelClass;
 use bizGhs\merchant\classes\WlClass;
+use bizGhs\shop\classes\GhsNoticeClass;
 use bizHd\purchase\classes\PurchaseClass;
 use bizHd\shop\classes\ShopExtClass;
 use bizGhs\stat\classes\StatVisitClass;
@@ -23,7 +24,7 @@ use Yii;
 class GhsController extends BaseController
 {
 
-    public $guestAccess = ['info', 'get-ghs-data', 'detail'];
+    public $guestAccess = ['info', 'get-ghs-data', 'detail', 'common-info', 'ghs-notice-detail'];
 
     //提示花店有多个供货商
     public function actionRemindMoreGhs()
@@ -496,4 +497,63 @@ class GhsController extends BaseController
         util::complete();
     }
 
+    public function actionCommonInfo()
+    {
+        $mainId = $this->resolveGhsMainId();
+        // 通用接口返回全部有效公告,前端按 positions 字段筛选展示位置
+        $ghsNoticeList = GhsNoticeClass::getClientNoticeList($mainId);
+        util::success(['ghsNoticeList' => $ghsNoticeList]);
+    }
+
+    /**
+     * 批发商公告详情(零售端采购页查看)
+     */
+    public function actionGhsNoticeDetail()
+    {
+        $id = intval(Yii::$app->request->get('id', 0));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        util::success(GhsNoticeClass::getClientDetail($id));
+    }
+
+    /**
+     * 根据批发商 ghsId 或 shopId 解析 mainId(列表接口用,勿与公告 id 混用)
+     */
+    protected function resolveGhsMainId(): int
+    {
+        $get = Yii::$app->request->get();
+        $ghsId = intval($get['ghsId'] ?? 0);
+        $shopId = intval($get['shopId'] ?? 0);
+        $ghsShopId = 0;
+
+        if ($ghsId > 0) {
+            $ghs = GhsClass::getById($ghsId);
+            if (empty($ghs)) {
+                util::fail('没有找到批发商');
+            }
+            if (!empty($this->shopId)) {
+                GhsClass::valid($ghs, $this->shopId);
+            }
+            $ghsShopId = intval($ghs['shopId'] ?? 0);
+        } elseif ($shopId > 0) {
+            $ghsShop = ShopClass::getById($shopId, true);
+            if (empty($ghsShop) || $ghsShop->ptStyle != dict::getDict('ptStyle', 'ghs')) {
+                util::fail('没有找到批发店');
+            }
+            $ghsShopId = $shopId;
+        } else {
+            util::fail('参数不对');
+        }
+
+        $ghsShop = ShopClass::getById($ghsShopId, true);
+        if (empty($ghsShop)) {
+            util::fail('没有找到批发商');
+        }
+        $mainId = intval($ghsShop->mainId ?? 0);
+        if ($mainId <= 0) {
+            util::fail('批发商信息不完整');
+        }
+        return $mainId;
+    }
 }

+ 12 - 1
app-hd/controllers/OrderController.php

@@ -35,6 +35,7 @@ use bizHd\promote\services\CouponService;
 use common\components\httpUtil;
 use biz\wx\classes\WxMessageClass;
 use common\components\lakala\Lakala;
+use yii\helpers\ArrayHelper;
 
 class OrderController extends BaseController
 {
@@ -270,6 +271,16 @@ class OrderController extends BaseController
         util::complete('操作成功');
     }
 
+    //批量确认取货 待发货的自取订单 每次500个
+    public function actionBatchFetch()
+    {
+        $where = ["status" => 2, "sendType" => 1, "shopId" => $this->shopId];
+        $orderList = OrderService::getLimitList("id",$where,500,"id desc");
+        $ids = ArrayHelper::getColumn($orderList,"id");
+        OrderService::updateByIds($ids, ["status" => 4]);
+        util::complete($ids);
+    }
+
     //确认发货
     public function actionSend()
     {
@@ -1486,4 +1497,4 @@ class OrderController extends BaseController
         util::success(['sedCost' => 10]);
     }
 
-}
+}

+ 7 - 0
app-hd/controllers/PurchaseController.php

@@ -726,6 +726,13 @@ class PurchaseController extends BaseController
                             util::fail('请返回选择 必选商品');
                         }
                     }
+                    if ($ghsShopId == 100883) {
+                        $hasMust = array_intersect($ids, [5017957,5036067,5036089,5036099,5017958,5036063,5017948,5035980,5036073,5036075,5036076,5036117,5036118,5017949,5035979,5035976,5035928,5035930,5036061,5035879]);
+                        if (empty($hasMust)) {
+                            util::fail('请返回选择 必选商品');
+                        }
+                    }
+
 
                     //三明易批花
                     if ($ghsShopId == 82200) {

+ 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]);
     }
 
-}
+}

+ 120 - 0
app-hd/controllers/ShopNoticeController.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace hd\controllers;
+
+use bizHd\shop\classes\ShopNoticeClass;
+use common\components\util;
+use Yii;
+
+/**
+ * 零售花店通知公告
+ */
+class ShopNoticeController extends BaseController
+{
+    /**
+     * 管理端公告列表
+     */
+    public function actionList()
+    {
+        $get = Yii::$app->request->get();
+        $list = ShopNoticeClass::searchList([
+            'mainId' => $this->mainId,
+            'title' => $get['title'] ?? '',
+            'isDel' => 0,
+            'order' => 'id DESC',
+        ]);
+        util::success($list);
+    }
+
+    /**
+     * 客户端公告列表(仅显示中的公告)
+     */
+    public function actionShowList()
+    {
+        $get = Yii::$app->request->get();
+        $list = ShopNoticeClass::searchList([
+            'mainId' => $this->mainId,
+            'title' => $get['title'] ?? '',
+            'position' => $get['position'] ?? '',
+            'isDel' => 0,
+            'status' => 1,
+            'order' => 'sort DESC, id DESC',
+        ]);
+        util::success($list);
+    }
+
+    /**
+     * 新增公告
+     */
+    public function actionAdd()
+    {
+        $post = Yii::$app->request->post();
+        $post['mainId'] = $this->mainId;
+        $post['staffId'] = intval($this->shopAdminId);
+        $id = ShopNoticeClass::addNotice($post);
+        $info = ShopNoticeClass::getDetail($id);
+        util::success($info);
+    }
+
+    /**
+     * 更新公告
+     */
+    public function actionUpdate()
+    {
+        $post = Yii::$app->request->post();
+        $id = intval($post['id'] ?? 0);
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = ShopNoticeClass::getById($id);
+        ShopNoticeClass::valid($info, $this->mainId);
+        $post['staffId'] = intval($this->shopAdminId);
+        ShopNoticeClass::updateNotice($id, $post);
+        util::complete('修改成功');
+    }
+
+    /**
+     * 公告详情
+     */
+    public function actionDetail()
+    {
+        $id = intval(Yii::$app->request->get('id', 0));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = ShopNoticeClass::getById($id);
+        ShopNoticeClass::valid($info, $this->mainId);
+        util::success(ShopNoticeClass::getDetail($id));
+    }
+
+    /**
+     * 软删除公告
+     */
+    public function actionDelete()
+    {
+        $id = intval(Yii::$app->request->post('id', Yii::$app->request->get('id', 0)));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = ShopNoticeClass::getById($id);
+        ShopNoticeClass::valid($info, $this->mainId);
+        ShopNoticeClass::deleteNotice($id, intval($this->shopAdminId));
+        util::complete('删除成功');
+    }
+
+    /**
+     * 上下架公告
+     */
+    public function actionUpdateStatus()
+    {
+        $id = intval(Yii::$app->request->post('id', Yii::$app->request->get('id', 0)));
+        $status = intval(Yii::$app->request->post('status', Yii::$app->request->get('status', 0)));
+        if ($id <= 0) {
+            util::fail('公告id不对');
+        }
+        $info = ShopNoticeClass::getById($id);
+        ShopNoticeClass::valid($info, $this->mainId);
+        ShopNoticeClass::updateStatus($id, $status, intval($this->shopAdminId));
+        util::complete('操作成功');
+    }
+}

+ 15 - 5
app-mall/controllers/OrderController.php

@@ -38,6 +38,16 @@ class OrderController extends BaseController
     //二维码收款使用
     public $guestAccess = ['order-relate', 'fast-pay'];
 
+    public function actionInform()
+    {
+        $get = Yii::$app->request->get();
+        $hdId = $get['hdId'] ?? 0;
+        $account = $get['account'] ?? 0;
+        $page = $get['page'] ?? '';
+        $userId = $this->userId;
+        noticeUtil::push("零售客户下单报错 hdId:{$hdId} account:{$account} userId:{$userId} page:{$page}", '15280215347');
+    }
+
     //记账单列表 ssh 20250627
     public function actionDebtList()
     {
@@ -493,8 +503,8 @@ class OrderController extends BaseController
                         util::fail("不满足最低消费金额{$hb->minConsume}元");
                     }
                     if ($this->shop->rechargeWeal == 3) {
-                        if(bccomp($custom->balance, $modifyPrice) == -1) {
-							//不要限制提交
+                        if (bccomp($custom->balance, $modifyPrice) == -1) {
+                            //不要限制提交
                             //util::fail('此红包非余额支付不可用(您的余额不足)');
                         }
                     }
@@ -826,7 +836,7 @@ class OrderController extends BaseController
                         util::fail("不满足最低消费金额{$hb->minConsume}元");
                     }
                     if ($this->shop->rechargeWeal == 3) {
-                        if($custom->balance < $modifyPrice) {
+                        if ($custom->balance < $modifyPrice) {
                             util::fail('此红包非余额支付不可用(您的余额不足)');
                         }
                     }
@@ -867,7 +877,7 @@ class OrderController extends BaseController
                 $hb->orderId = $return->id;
                 $hb->save();
             }
-            
+
             $orderId = $return->id;
             $orderSn = $return->orderSn;
             $actPrice = $return->actPrice ?? 0;
@@ -921,7 +931,7 @@ class OrderController extends BaseController
             }
             //支付前验证订单有效性
             OrderService::checkBeforePay($order);
-            
+
             $payWay = dict::getDict('payWay', 'balancePay');
             \bizHd\order\classes\OrderClass::payAfter($order, $payWay);
             $transaction->commit();

+ 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);
             }
         }
 

+ 288 - 0
biz-ghs/shop/classes/GhsNoticeClass.php

@@ -0,0 +1,288 @@
+<?php
+
+namespace bizGhs\shop\classes;
+
+use bizGhs\base\classes\BaseClass;
+use common\components\util;
+use Yii;
+use yii\db\Expression;
+
+/**
+ * 批发端通知公告业务类
+ */
+class GhsNoticeClass extends BaseClass
+{
+    public static $baseFile = '\bizGhs\shop\models\GhsNotice';
+
+    /** 展示位置:分类菜单 */
+    const POSITION_CATEGORY = 1;
+    /** 展示位置:订单提交 */
+    const POSITION_ORDER = 2;
+
+    /**
+     * 展示位置文案映射
+     */
+    public static function getPositionMap(): array
+    {
+        return [
+            self::POSITION_CATEGORY => '分类菜单',
+            self::POSITION_ORDER => '订单提交',
+        ];
+    }
+
+    /**
+     * 客户端公告列表(不分页)
+     */
+    public static function getClientNoticeList(int $mainId, int $position = 0): array
+    {
+        if ($mainId <= 0) {
+            return [];
+        }
+        $query = self::getModel()::find()
+            ->where(['mainId' => $mainId, 'isDel' => 0, 'status' => 1]);
+        if ($position > 0) {
+            $query->andWhere(new Expression('FIND_IN_SET(:position, [[position]])', [':position' => (string)$position]));
+        }
+        $list = $query->orderBy('sort DESC, id DESC')->asArray()->all();
+        return self::groupBaseInfo($list);
+    }
+
+    /**
+     * 客户端公告详情(仅校验公告本身有效)
+     */
+    public static function getClientDetail(int $id): array
+    {
+        $info = self::getById($id);
+        if (empty($info) || intval($info['isDel']) === 1 || intval($info['status']) !== 1) {
+            util::fail('公告不存在');
+        }
+        return self::getDetail($id);
+    }
+
+    /**
+     * 通用列表查询,供管理端与客户端复用
+     */
+    public static function searchList(array $params): array
+    {
+        $get = Yii::$app->request->get();
+        $page = isset($get['page']) ? max(1, intval($get['page'])) : 1;
+        $pageSize = !empty($get['pageSize']) ? intval($get['pageSize']) : Yii::$app->params['pageSize'];
+
+        $query = self::getModel()::find()->where(['mainId' => intval($params['mainId'])]);
+
+        if (isset($params['isDel'])) {
+            $query->andWhere(['isDel' => intval($params['isDel'])]);
+        }
+        if (isset($params['status'])) {
+            $query->andWhere(['status' => intval($params['status'])]);
+        }
+        if (!empty($params['title'])) {
+            $query->andWhere(['like', 'title', trim($params['title'])]);
+        }
+        if (!empty($params['position'])) {
+            $position = intval($params['position']);
+            $query->andWhere(new Expression('FIND_IN_SET(:position, [[position]])', [':position' => (string)$position]));
+        }
+
+        $order = !empty($params['order']) ? $params['order'] : 'id DESC';
+        $totalNum = (int)$query->count();
+        $totalPage = $pageSize > 0 ? (int)ceil($totalNum / $pageSize) : 0;
+        $list = $query->orderBy($order)
+            ->offset(($page - 1) * $pageSize)
+            ->limit($pageSize)
+            ->asArray()
+            ->all();
+
+        return [
+            'totalNum' => $totalNum,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+            'list' => self::groupBaseInfo($list),
+        ];
+    }
+
+    /**
+     * 组装列表展示字段
+     */
+    public static function groupBaseInfo(array $list): array
+    {
+        $positionMap = self::getPositionMap();
+        foreach ($list as $key => $item) {
+            $positions = self::formatPositionToArr($item['position'] ?? '');
+            $labels = [];
+            foreach ($positions as $pos) {
+                if (isset($positionMap[$pos])) {
+                    $labels[] = $positionMap[$pos];
+                }
+            }
+            $list[$key]['positions'] = $positions;
+            $list[$key]['positionLabels'] = $labels;
+        }
+        return $list;
+    }
+
+    /**
+     * 多选位置转逗号分隔字符串
+     */
+    public static function formatPositionToStr($positions): string
+    {
+        if (is_string($positions)) {
+            $positions = array_filter(array_map('trim', explode(',', $positions)));
+        }
+        if (!is_array($positions)) {
+            return '';
+        }
+        $valid = array_keys(self::getPositionMap());
+        $positions = array_values(array_unique(array_map('intval', $positions)));
+        $positions = array_values(array_intersect($positions, $valid));
+        sort($positions);
+        return implode(',', $positions);
+    }
+
+    /**
+     * 逗号分隔字符串转位置数组
+     */
+    public static function formatPositionToArr(string $position): array
+    {
+        if ($position === '') {
+            return [];
+        }
+        $positions = array_filter(array_map('trim', explode(',', $position)));
+        return array_values(array_map('intval', $positions));
+    }
+
+    /**
+     * 校验公告表单数据
+     */
+    public static function validateNoticeData(array $data): array
+    {
+        $title = trim($data['title'] ?? '');
+        $summary = trim($data['summary'] ?? '');
+        if ($title === '') {
+            util::fail('请输入公告标题');
+        }
+        if (mb_strlen($title) > 20) {
+            util::fail('标题不能超过20个字');
+        }
+        if ($summary === '') {
+            util::fail('请输入公告简介');
+        }
+        if (mb_strlen($summary) > 70) {
+            util::fail('公告简介不能超过70个字');
+        }
+
+        $position = self::formatPositionToStr($data['position'] ?? '');
+        if ($position === '') {
+            util::fail('请选择展示位置');
+        }
+
+        $content = $data['content'] ?? '';
+        if ($content === '' || $content === '[]') {
+            util::fail('请添加公告内容');
+        }
+        if (is_string($content)) {
+            $contentArr = json_decode($content, true);
+            if (!is_array($contentArr) || empty($contentArr)) {
+                util::fail('公告内容格式不正确');
+            }
+        }
+
+        return [
+            'title' => $title,
+            'summary' => $summary,
+            'content' => is_string($content) ? $content : json_encode($content, JSON_UNESCAPED_UNICODE),
+            'position' => $position,
+            'sort' => intval($data['sort'] ?? 0),
+            'status' => intval($data['status'] ?? 0) === 1 ? 1 : 0,
+        ];
+    }
+
+    /**
+     * 新增公告
+     */
+    public static function addNotice(array $data): int
+    {
+        $noticeData = self::validateNoticeData($data);
+        if (!empty($data['mainId'])) {
+            $noticeData['mainId'] = intval($data['mainId']);
+        }
+        if (!empty($data['staffId'])) {
+            $noticeData['staffId'] = intval($data['staffId']);
+        }
+        if ($noticeData['status'] === 1) {
+            $noticeData['publishTime'] = date('Y-m-d H:i:s');
+        }
+        $result = self::add($noticeData);
+        return is_array($result) ? intval($result['id'] ?? 0) : intval($result);
+    }
+
+    /**
+     * 更新公告
+     */
+    public static function updateNotice(int $id, array $data): void
+    {
+        $noticeData = self::validateNoticeData($data);
+        $info = self::getById($id);
+        if (empty($info)) {
+            util::fail('公告不存在');
+        }
+        if (!empty($data['staffId'])) {
+            $noticeData['staffId'] = intval($data['staffId']);
+        }
+        self::updateById($id, $noticeData);
+    }
+
+    /**
+     * 公告详情
+     */
+    public static function getDetail(int $id): array
+    {
+        $info = self::getById($id);
+        if (empty($info)) {
+            util::fail('公告不存在');
+        }
+        $list = self::groupBaseInfo([$info]);
+        return current($list);
+    }
+
+    /**
+     * 软删除公告
+     */
+    public static function deleteNotice(int $id, int $staffId = 0): void
+    {
+        $updateData = ['isDel' => 1];
+        if ($staffId > 0) {
+            $updateData['staffId'] = $staffId;
+        }
+        self::updateById($id, $updateData);
+    }
+
+    /**
+     * 上下架公告
+     */
+    public static function updateStatus(int $id, int $status, int $staffId = 0): void
+    {
+        $status = $status === 1 ? 1 : 0;
+        $updateData = ['status' => $status];
+        if ($status === 1) {
+            $updateData['publishTime'] = date('Y-m-d H:i:s');
+        }
+        if ($staffId > 0) {
+            $updateData['staffId'] = $staffId;
+        }
+        self::updateById($id, $updateData);
+    }
+
+    /**
+     * 校验公告归属权限
+     */
+    public static function valid(array $info, int $mainId): void
+    {
+        if (empty($info) || intval($info['mainId']) !== intval($mainId)) {
+            util::fail('没有权限操作该公告');
+        }
+        if (intval($info['isDel']) === 1) {
+            util::fail('公告已删除');
+        }
+    }
+}

+ 16 - 0
biz-ghs/shop/models/GhsNotice.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace bizGhs\shop\models;
+
+use bizGhs\base\models\Base;
+
+/**
+ * 批发端通知公告
+ */
+class GhsNotice extends Base
+{
+    public static function tableName()
+    {
+        return 'xhGhsNotice';
+    }
+}

+ 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, '.', '');
+    }
+}

+ 265 - 0
biz-hd/shop/classes/ShopNoticeClass.php

@@ -0,0 +1,265 @@
+<?php
+
+namespace bizHd\shop\classes;
+
+use bizHd\base\classes\BaseClass;
+use common\components\util;
+use Yii;
+use yii\db\Expression;
+
+/**
+ * 零售花店通知公告业务类
+ */
+class ShopNoticeClass extends BaseClass
+{
+    public static $baseFile = '\bizHd\shop\models\ShopNotice';
+
+    /** 展示位置:商城首页 */
+    const POSITION_HOME = 1;
+    /** 展示位置:分类菜单 */
+    const POSITION_CATEGORY = 2;
+    /** 展示位置:购物车 */
+    const POSITION_CART = 4;
+    /** 展示位置:订单提交 */
+    const POSITION_ORDER = 8;
+
+    /**
+     * 展示位置文案映射
+     */
+    public static function getPositionMap(): array
+    {
+        return [
+            self::POSITION_HOME => '商城首页',
+            self::POSITION_CATEGORY => '分类菜单',
+            self::POSITION_CART => '购物车',
+            self::POSITION_ORDER => '订单提交',
+        ];
+    }
+
+    /**
+     * 通用列表查询,供管理端与客户端复用
+     */
+    public static function searchList(array $params): array
+    {
+        $get = Yii::$app->request->get();
+        $page = isset($get['page']) ? max(1, intval($get['page'])) : 1;
+        $pageSize = !empty($get['pageSize']) ? intval($get['pageSize']) : Yii::$app->params['pageSize'];
+
+        $query = self::getModel()::find()->where(['mainId' => intval($params['mainId'])]);
+
+        if (isset($params['isDel'])) {
+            $query->andWhere(['isDel' => intval($params['isDel'])]);
+        }
+        if (isset($params['status'])) {
+            $query->andWhere(['status' => intval($params['status'])]);
+        }
+        if (!empty($params['title'])) {
+            $query->andWhere(['like', 'title', trim($params['title'])]);
+        }
+        if (!empty($params['position'])) {
+            $position = intval($params['position']);
+            $query->andWhere(new Expression('FIND_IN_SET(:position, [[position]])', [':position' => (string)$position]));
+        }
+
+        $order = !empty($params['order']) ? $params['order'] : 'id DESC';
+        $totalNum = (int)$query->count();
+        $totalPage = $pageSize > 0 ? (int)ceil($totalNum / $pageSize) : 0;
+        $list = $query->orderBy($order)
+            ->offset(($page - 1) * $pageSize)
+            ->limit($pageSize)
+            ->asArray()
+            ->all();
+
+        return [
+            'totalNum' => $totalNum,
+            'totalPage' => $totalPage,
+            'moreData' => $page < $totalPage ? 1 : 0,
+            'list' => self::groupBaseInfo($list),
+        ];
+    }
+
+    /**
+     * 组装列表展示字段
+     */
+    public static function groupBaseInfo(array $list): array
+    {
+        $positionMap = self::getPositionMap();
+        foreach ($list as $key => $item) {
+            $positions = self::formatPositionToArr($item['position'] ?? '');
+            $labels = [];
+            foreach ($positions as $pos) {
+                if (isset($positionMap[$pos])) {
+                    $labels[] = $positionMap[$pos];
+                }
+            }
+            $list[$key]['positions'] = $positions;
+            $list[$key]['positionLabels'] = $labels;
+        }
+        return $list;
+    }
+
+    /**
+     * 多选位置转逗号分隔字符串
+     */
+    public static function formatPositionToStr($positions): string
+    {
+        if (is_string($positions)) {
+            $positions = array_filter(array_map('trim', explode(',', $positions)));
+        }
+        if (!is_array($positions)) {
+            return '';
+        }
+        $valid = array_keys(self::getPositionMap());
+        $positions = array_values(array_unique(array_map('intval', $positions)));
+        $positions = array_values(array_intersect($positions, $valid));
+        sort($positions);
+        return implode(',', $positions);
+    }
+
+    /**
+     * 逗号分隔字符串转位置数组
+     */
+    public static function formatPositionToArr(string $position): array
+    {
+        if ($position === '') {
+            return [];
+        }
+        $positions = array_filter(array_map('trim', explode(',', $position)));
+        return array_values(array_map('intval', $positions));
+    }
+
+    /**
+     * 校验公告表单数据
+     */
+    public static function validateNoticeData(array $data): array
+    {
+        $title = trim($data['title'] ?? '');
+        $summary = trim($data['summary'] ?? '');
+        if ($title === '') {
+            util::fail('请输入公告标题');
+        }
+        if (mb_strlen($title) > 20) {
+            util::fail('标题不能超过20个字');
+        }
+        if ($summary === '') {
+            util::fail('请输入公告简介');
+        }
+        if (mb_strlen($summary) > 200) {
+            util::fail('公告简介不能超过200个字');
+        }
+
+        $position = self::formatPositionToStr($data['position'] ?? '');
+        if ($position === '') {
+            util::fail('请选择展示位置');
+        }
+
+        $content = $data['content'] ?? '';
+        if ($content === '' || $content === '[]') {
+            util::fail('请添加公告内容');
+        }
+        if (is_string($content)) {
+            $contentArr = json_decode($content, true);
+            if (!is_array($contentArr) || empty($contentArr)) {
+                util::fail('公告内容格式不正确');
+            }
+        }
+
+        return [
+            'title' => $title,
+            'summary' => $summary,
+            'content' => is_string($content) ? $content : json_encode($content, JSON_UNESCAPED_UNICODE),
+            'position' => $position,
+            'sort' => intval($data['sort'] ?? 0),
+            'status' => intval($data['status'] ?? 0) === 1 ? 1 : 0,
+        ];
+    }
+
+    /**
+     * 新增公告
+     */
+    public static function addNotice(array $data): int
+    {
+        $noticeData = self::validateNoticeData($data);
+        if (!empty($data['mainId'])) {
+            $noticeData['mainId'] = intval($data['mainId']);
+        }
+        if (!empty($data['staffId'])) {
+            $noticeData['staffId'] = intval($data['staffId']);
+        }
+        if ($noticeData['status'] === 1) {
+            $noticeData['publishTime'] = date('Y-m-d H:i:s');
+        }
+        $result = self::add($noticeData);
+        return is_array($result) ? intval($result['id'] ?? 0) : intval($result);
+    }
+
+    /**
+     * 更新公告
+     */
+    public static function updateNotice(int $id, array $data): void
+    {
+        $noticeData = self::validateNoticeData($data);
+        $info = self::getById($id);
+        if (empty($info)) {
+            util::fail('公告不存在');
+        }
+        if (!empty($data['staffId'])) {
+            $noticeData['staffId'] = intval($data['staffId']);
+        }
+        self::updateById($id, $noticeData);
+    }
+
+    /**
+     * 公告详情
+     */
+    public static function getDetail(int $id): array
+    {
+        $info = self::getById($id);
+        if (empty($info)) {
+            util::fail('公告不存在');
+        }
+        $list = self::groupBaseInfo([$info]);
+        return current($list);
+    }
+
+    /**
+     * 软删除公告
+     */
+    public static function deleteNotice(int $id, int $staffId = 0): void
+    {
+        $updateData = ['isDel' => 1];
+        if ($staffId > 0) {
+            $updateData['staffId'] = $staffId;
+        }
+        self::updateById($id, $updateData);
+    }
+
+    /**
+     * 上下架公告
+     */
+    public static function updateStatus(int $id, int $status, int $staffId = 0): void
+    {
+        $status = $status === 1 ? 1 : 0;
+        $updateData = ['status' => $status];
+        if ($status === 1) {
+            $updateData['publishTime'] = date('Y-m-d H:i:s');
+        }
+        if ($staffId > 0) {
+            $updateData['staffId'] = $staffId;
+        }
+        self::updateById($id, $updateData);
+    }
+
+    /**
+     * 校验公告归属权限
+     */
+    public static function valid(array $info, int $mainId): void
+    {
+        if (empty($info) || intval($info['mainId']) !== intval($mainId)) {
+            util::fail('没有权限操作该公告');
+        }
+        if (intval($info['isDel']) === 1) {
+            util::fail('公告已删除');
+        }
+    }
+}

+ 16 - 0
biz-hd/shop/models/ShopNotice.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace bizHd\shop\models;
+
+use bizHd\base\models\Base;
+
+/**
+ * 零售花店通知公告
+ */
+class ShopNotice extends Base
+{
+    public static function tableName()
+    {
+        return 'xhShopNotice';
+    }
+}

+ 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'):
                 //零售采购

+ 2 - 2
common/components/sms.php

@@ -89,7 +89,7 @@ class sms
             util::fail('没有找到平台');
         }
         //$name = isset($open['name']) ? $open['name'] : '花卉宝';
-        $name = '花掌柜';
+        $name = '厦门中花汇';
         $sign = isset($merchant) == true ? "【{$merchant['name']}】" : "【{$name}】";
         $msg = $sign . $msg;
 
@@ -116,4 +116,4 @@ class sms
         $sms->sendVariableSMS($msg, $params);
     }
 
-}
+}

+ 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;