shizhongqi 4 týždňov pred
rodič
commit
7086ab3e9a

+ 18 - 3
app-hd/controllers/BirthdayGiftController.php

@@ -8,7 +8,7 @@ use Yii;
 
 class BirthdayGiftController extends BaseController
 {
-    public $guestAccess = [];
+    public $guestAccess = ['notify-state'];
 
     // 工作台栏数据
     public function actionWorkbenchSummary()
@@ -49,11 +49,11 @@ class BirthdayGiftController extends BaseController
         $id = intval($get['id'] ?? 0);
         if ($id > 0) {
             BirthdayGiftClass::notifyOneTomorrow($this->shop, $id);
-            util::complete('通知成功');
+            util::complete('短信已提交,等待送达确认');
         }
         $result = BirthdayGiftClass::batchNotifyTomorrow($this->shop);
         if ($result['success'] > 0) {
-            util::complete('已成功通知' . $result['success'] . '人' . ($result['fail'] > 0 ? ',失败' . $result['fail'] . '人' : ''));
+            util::complete('已提交' . $result['success'] . '人' . ($result['fail'] > 0 ? ',失败' . $result['fail'] . '人' : '') . ',等待送达确认');
         }
         if ($result['fail'] > 0 && $result['success'] == 0) {
             util::fail('通知失败:' . implode(';', array_slice($result['messages'], 0, 3)));
@@ -61,6 +61,21 @@ class BirthdayGiftController extends BaseController
         util::fail('没有需要通知的明日生日客户');
     }
 
+    // 创蓝云智短信通知状态回调
+    public function actionNotifyState()
+    {
+        try {
+            $success = BirthdayGiftClass::handleNotifyState(Yii::$app->request->get());
+        } catch (\Throwable $throwable) {
+            Yii::error('生日礼品短信状态回调处理失败:' . $throwable->getMessage(), __METHOD__);
+            $success = false;
+        }
+
+        return $this->asJson([
+            'clcode' => $success ? '000000' : '111111',
+        ]);
+    }
+
     // 设置生日日期
     public function actionModifyBirthday()
     {

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

@@ -2,7 +2,6 @@
 
 namespace mall\controllers;
 
-use biz\shop\classes\ShopExtClass;
 use bizHd\birthday\classes\BirthdayGiftClass;
 use bizHd\custom\classes\CustomClass;
 use bizHd\wx\classes\WxOpenClass;

+ 179 - 36
biz-hd/birthday/classes/BirthdayGiftClass.php

@@ -4,6 +4,7 @@ namespace bizHd\birthday\classes;
 
 use biz\sj\classes\MerchantAssetClass;
 use biz\sj\classes\SjClass;
+use bizHd\birthday\models\BirthdayGift;
 use bizHd\custom\classes\CustomClass;
 use bizHd\custom\classes\HdClass;
 use bizHd\member\classes\MemberLevelClass;
@@ -31,6 +32,7 @@ class BirthdayGiftClass extends BaseClass
     const STATUS_COLLECTED = 4;      // 4已领取
     const STATUS_EXPIRED = 5;        // 5超时放弃
     const EXPIRE_SECONDS = 86400;    // 通知后24小时内回复有效
+    const WORKBENCH_SUMMARY_CACHE_TTL = 14400;
 
     public static function getStatusMap()
     {
@@ -50,7 +52,7 @@ class BirthdayGiftClass extends BaseClass
     public static function getLevelBenefitMap($mainId, $shopId)
     {
         $shop = ShopClass::getById($shopId, true);
-        $memberLevelSet = $shop->memberLevelSet ?? 0;
+        $memberLevelSet = $shop->memberLevelSet ?? 0; //会员等级 0未设置 1设置了
         if ($memberLevelSet == 1) {
             $list = MemberLevelClass::getAllByCondition(['shopId' => $shopId], 'level ASC', '*', null);
         } else {
@@ -63,6 +65,7 @@ class BirthdayGiftClass extends BaseClass
                 }
             }
         }
+
         $map = [];
         if (!empty($list)) {
             foreach ($list as $row) {
@@ -88,35 +91,43 @@ class BirthdayGiftClass extends BaseClass
 
     public static function hasBirthdayBenefit($custom, $levelMap)
     {
-        $member = intval($custom->member ?? $custom['member'] ?? 0);
+        $member = intval($custom->member ?? 0);
         return $member > 0 && !empty(self::getGiftNameForMember($member, $levelMap));
     }
 
     /**
-     * 当年用于比对的公历月日
+     * 获取生日公历月日数组 [月, 日]
      */
     public static function getBirthdayMonthDay($custom)
     {
-        $birthdayTime = intval($custom->birthdayTime ?? $custom['birthdayTime'] ?? 0);
+        $birthdayTime = intval($custom->birthdayTime ?? 0);
         if ($birthdayTime <= 0) {
             return null;
         }
-        $lunar = intval($custom->lunar ?? $custom['lunar'] ?? 0);
-        $birthday = $custom->birthday ?? $custom['birthday'] ?? '';
+        $lunar = $custom->lunar;
+        $birthday = $custom->birthday;
         if ($lunar == 1 && !empty($birthday) && $birthday != '0000-00-00 00:00:00') {
             $ts = strtotime($birthday);
             if ($ts > 0) {
                 return [intval(date('n', $ts)), intval(date('j', $ts))];
             }
         }
-        $m = intval($custom->birthdayMonth ?? $custom['birthdayMonth'] ?? 0);
-        $d = intval($custom->birthdayDate ?? $custom['birthdayDate'] ?? 0);
+        $m = intval($custom->birthdayMonth);
+        $d = intval($custom->birthdayDate);
         if ($m > 0 && $d > 0) {
             return [$m, $d];
         }
         return null;
     }
 
+    /**
+     * 转换农历生日为公历月日
+     * @param int $lunar 是否农历 1是 0否
+     * @param int $birthdayMonth 农历月
+     * @param int $birthdayDate 农历日
+     * @param int $year 年份
+     * @return array [公历月, 公历日]
+     */
     public static function convertBirthdayMonthDay($lunar, $birthdayMonth, $birthdayDate, $year = null)
     {
         $birthdayMonth = intval($birthdayMonth);
@@ -257,10 +268,24 @@ class BirthdayGiftClass extends BaseClass
      */
     public static function getWorkbenchSummary($shopId, $mainId)
     {
-        list($customs,) = self::getEligibleCustoms($shopId, $mainId);
+        $cacheKey = self::getWorkbenchSummaryCacheKey($shopId, $mainId);
+        try {
+            $cached = Yii::$app->redis->get($cacheKey);
+            if ($cached !== false && $cached !== null) {
+                $data = json_decode($cached, true);
+                if (is_array($data)) {
+                    return $data;
+                }
+            }
+        } catch (\Throwable $throwable) {
+            Yii::warning('读取生日工作台缓存失败:' . $throwable->getMessage(), __METHOD__);
+        }
+
         $today = 0;
         $tomorrow = 0;
         $week = 0;
+
+        list($customs,) = self::getEligibleCustoms($shopId, $mainId);
         foreach ($customs as $custom) {
             if (self::getBirthdayMonthDay($custom) === null) {
                 continue;
@@ -275,11 +300,40 @@ class BirthdayGiftClass extends BaseClass
                 $week++;
             }
         }
-        return [
+        $data = [
             'today' => $today,
             'tomorrow' => $tomorrow,
             'week' => $week,
         ];
+
+        try {
+            Yii::$app->redis->executeCommand('SETEX', [
+                $cacheKey,
+                self::WORKBENCH_SUMMARY_CACHE_TTL,
+                json_encode($data),
+            ]);
+        } catch (\Throwable $throwable) {
+            Yii::warning('写入生日工作台缓存失败:' . $throwable->getMessage(), __METHOD__);
+        }
+
+        return $data;
+    }
+
+    private static function getWorkbenchSummaryCacheKey($shopId, $mainId)
+    {
+        return 'hd:birthdayGift:workbenchSummary:'
+            . intval($mainId) . ':' . intval($shopId) . ':' . date('Ymd');
+    }
+
+    private static function clearWorkbenchSummaryCache($shopId, $mainId)
+    {
+        try {
+            Yii::$app->redis->executeCommand('DEL', [
+                self::getWorkbenchSummaryCacheKey($shopId, $mainId),
+            ]);
+        } catch (\Throwable $throwable) {
+            Yii::warning('清理生日工作台缓存失败:' . $throwable->getMessage(), __METHOD__);
+        }
     }
 
     public static function generateClaimToken()
@@ -297,7 +351,15 @@ class BirthdayGiftClass extends BaseClass
         ], true);
     }
 
-    public static function getOrCreateYearRecord($shop, $custom, $levelMap)
+    /**
+     * 创建或更新客户今年生日记录(按“门店 + 客户 + 自然年”唯一:一年一条活动记录)
+     * @param $shop
+     * @param $custom
+     * @param $levelMap
+     * @return mixed
+     * @throws IntegrityException
+     */
+    public static function createOrUpdateYearRecord($shop, $custom, $levelMap)
     {
         $year = intval(date('Y'));
         $shopId = intval($shop->id);
@@ -307,7 +369,6 @@ class BirthdayGiftClass extends BaseClass
         $member = intval($custom->member);
         $giftName = self::getGiftNameForMember($member, $levelMap);
         $hd = HdClass::getByCondition(['customId' => $customId, 'shopId' => $shopId], true);
-        $hdId = $hd->id ?? 0;
         $birthdayTime = intval($custom->birthdayTime);
         $status = $birthdayTime > 0 ? self::STATUS_PENDING_NOTIFY : self::STATUS_NO_BIRTHDAY;
         list($birthdayMonth, $birthdayDate) = self::convertBirthdayMonthDay(
@@ -320,7 +381,7 @@ class BirthdayGiftClass extends BaseClass
             'mainId' => $mainId,
             'shopId' => $shopId,
             'customId' => $customId,
-            'hdId' => $hdId,
+            'hdId' => $hd->id ?? 0,
             'userId' => intval($custom->userId),
             'year' => $year,
             'member' => $member,
@@ -368,8 +429,9 @@ class BirthdayGiftClass extends BaseClass
         try {
             $custom = CustomClass::modifyBirthday($customId, $shop->id, $params);
             $levelMap = self::getLevelBenefitMap($shop->mainId, $shop->id);
-            $gift = self::getOrCreateYearRecord($shop, $custom, $levelMap);
+            $gift = self::createOrUpdateYearRecord($shop, $custom, $levelMap);
             $transaction->commit();
+            self::clearWorkbenchSummaryCache($shop->id, $shop->mainId);
             return $gift;
         } catch (\Exception $exception) {
             $transaction->rollBack();
@@ -386,7 +448,7 @@ class BirthdayGiftClass extends BaseClass
     public static function ensureYearRecords($shop)
     {
         $shopId = intval($shop->id);
-        $year = intval(date('Y'));
+        $year = intval(date('Y')); //今年
         list($customs, $levelMap) = self::getEligibleCustoms($shopId, $shop->mainId);
         if (empty($customs)) {
             return true;
@@ -401,12 +463,13 @@ class BirthdayGiftClass extends BaseClass
                 }
             }
         }
+        // 遍历所有符合条件的客户,如果客户已存在当年记录,则跳过,否则创建当年生日记录
         foreach ($customs as $custom) {
             $customId = intval($custom->id);
             if (isset($existingIds[$customId])) {
                 continue;
             }
-            self::getOrCreateYearRecord($shop, $custom, $levelMap);
+            self::createOrUpdateYearRecord($shop, $custom, $levelMap);
             $existingIds[$customId] = 1;
         }
         return true;
@@ -595,9 +658,9 @@ class BirthdayGiftClass extends BaseClass
                     $name = '未配置礼品';
                 }
                 $st = intval($gift->status);
-                if ($st == self::STATUS_PENDING_NOTIFY) {
+                if (in_array($st, [self::STATUS_PENDING_NOTIFY, self::STATUS_PENDING_CLAIM])) {
                     $pending[$name] = ($pending[$name] ?? 0) + 1;
-                } elseif (in_array($st, [self::STATUS_PENDING_CLAIM, self::STATUS_PICKING_UP, self::STATUS_COLLECTED])) {
+                } elseif (in_array($st, [self::STATUS_PICKING_UP, self::STATUS_COLLECTED])) {
                     $collected[$name] = ($collected[$name] ?? 0) + 1;
                 }
             }
@@ -671,8 +734,8 @@ class BirthdayGiftClass extends BaseClass
         if (empty($merchant)) {
             return [false, '商家信息不存在'];
         }
-        $mobile = $custom->mobile ?? '';
-        if (empty($mobile)) {
+        $mobile = preg_replace('/\D/', '', (string)($custom->mobile ?? ''));
+        if (!preg_match('/^1\d{10}$/', $mobile)) {
             return [false, '客户无手机号'];
         }
         // 短信余额查询及可发数量削减 TODO
@@ -692,22 +755,30 @@ class BirthdayGiftClass extends BaseClass
         $msg = "{$shopName}:亲爱的会员您好,明天就是您生日啦!为您准备了专属生日礼品:{$giftDesc}。请您回复领取时间,如明天10点左右,24小时内回复有效。不领取请忽略短信,祝您生活愉快!拒收请回复R";
         $sms = new ChuanglanSmsApi();
         $sms->useAccount('marketing');
-        $re = $sms->sendSMS($mobile, $msg);
+        if (empty($gift->claimToken)) {
+            return [false, '短信回执标识不存在'];
+        }
+        $giftId = intval($gift->id);
+        $callbackSign = hash_hmac('sha256', $giftId . '|' . $mobile, (string)$gift->claimToken);
+        $callbackUid = $giftId . '.' . $callbackSign;
+        $callbackUrl = Yii::$app->params['hdHost'] . '/birthday-gift/notify-state';
+        $re = $sms->sendSMS($mobile, $msg, true, $callbackUrl, $callbackUid);
         Yii::info('生日礼品短信返回:' . json_encode($re, JSON_UNESCAPED_UNICODE));
         $re = json_decode($re, true);
-        if ($re['code'] != '000000') {
-            Yii::error('生日礼品短信发送失败:' . $re['errorMsg']);
+        if (!is_array($re) || ($re['code'] ?? '') != '000000') {
+            Yii::error('生日礼品短信发送失败:' . ($re['errorMsg'] ?? '响应格式错误'));
             return [false, '短信发送失败'];
         }
 
         // TODO 记录到 xhSms
         $sms = new SmsClass();
         $sms->add([
-            'merchantId' => $sjId,
+            'mainId' => $shop->mainId,
+            'shopId' => $shop->id,
             'userId' => $custom->userId,
+            'mobile' => $mobile,
             'content' => $msg,
-            'type' => '2', // 1.通知短信,2.营销短信
-            'addTime' => time()
+            'type' => 1, // 0通知类型, 1营销类型
         ]);
         
         return [true, ''];
@@ -724,7 +795,7 @@ class BirthdayGiftClass extends BaseClass
             if (!self::matchesBirthdayOffset($custom, 1)) {
                 continue;
             }
-            $gift = self::getOrCreateYearRecord($shop, $custom, $levelMap);
+            $gift = self::createOrUpdateYearRecord($shop, $custom, $levelMap);
             if (intval($gift->status) != self::STATUS_PENDING_NOTIFY) {
                 continue;
             }
@@ -749,18 +820,90 @@ class BirthdayGiftClass extends BaseClass
         if (!$ok) {
             return [false, $err];
         }
+        return [true, ''];
+    }
+
+    /**
+     * 处理创蓝短信状态回执。只有 DELIVRD 才推进生日赠礼业务。
+     */
+    public static function handleNotifyState($params)
+    {
+        Yii::info('生日礼品短信状态回调:' . json_encode($params, JSON_UNESCAPED_UNICODE), __METHOD__);
+
+        $uid = trim((string)($params['uid'] ?? ''));
+        $uidParts = explode('.', $uid, 2);
+        $giftId = intval($uidParts[0] ?? 0);
+        $sign = strtolower(trim((string)($uidParts[1] ?? '')));
+        $msgId = trim((string)($params['msgid'] ?? ''));
+        $mobile = preg_replace('/\D/', '', (string)($params['mobile'] ?? ''));
+        $status = strtoupper(trim((string)($params['status'] ?? '')));
+        if ($giftId <= 0 || !preg_match('/^[a-f0-9]{64}$/', $sign) || !preg_match('/^\d{32}$/', $msgId) || !preg_match('/^1\d{10}$/', $mobile) || $status === '') {
+            Yii::warning('生日礼品短信状态回调参数不完整', __METHOD__);
+            return false;
+        }
+
+        $gift = self::getById($giftId, true);
+        if (empty($gift) || empty($gift->claimToken)) {
+            Yii::warning('生日礼品短信状态回调标识无效:giftId=' . $giftId . ' msgId=' . $msgId, __METHOD__);
+            return false;
+        }
+        $expectedSign = hash_hmac('sha256', $giftId . '|' . $mobile, (string)$gift->claimToken);
+        if (!hash_equals($expectedSign, $sign)) {
+            Yii::warning('生日礼品短信状态回调手机号不匹配:giftId=' . $giftId . ' msgId=' . $msgId, __METHOD__);
+            return false;
+        }
+
+        if ($status !== 'DELIVRD') {
+            $statusDesc = urldecode((string)($params['statusDesc'] ?? ''));
+            Yii::warning('生日礼品短信未送达:giftId=' . $giftId . ' msgId=' . $msgId . ' status=' . $status . ' statusDesc=' . $statusDesc, __METHOD__);
+            return true;
+        }
+
+        return self::confirmNotifyDelivered($giftId);
+    }
+
+    /**
+     * 确认短信送达后,原子更新赠礼状态并创建过期延迟消息。
+     */
+    public static function confirmNotifyDelivered($giftId)
+    {
+        $gift = self::getById($giftId, true);
+        if (empty($gift) || intval($gift->smsSent) === 1) {
+            return true;
+        }
+        if (intval($gift->status) !== self::STATUS_PENDING_NOTIFY) {
+            Yii::warning('短信已送达,但生日赠礼状态不可推进:giftId=' . intval($giftId), __METHOD__);
+            return true;
+        }
+
         $now = date('Y-m-d H:i:s');
         $expire = date('Y-m-d H:i:s', time() + self::EXPIRE_SECONDS);
-        $gift->status = self::STATUS_PENDING_CLAIM;
-        $gift->notifyTime = $now;
-        $gift->expireTime = $expire;
-        $gift->smsSent = 1;
-        if (empty($gift->orderSn)) {
-            $gift->orderSn = 'BG' . date('ymdHis') . mt_rand(1000, 9999);
+        $orderSn = empty($gift->orderSn) ? 'BG' . date('ymdHis') . mt_rand(1000, 9999) : $gift->orderSn;
+        $transaction = Yii::$app->db->beginTransaction();
+        try {
+            $affectedRows = BirthdayGift::updateAll([
+                'status' => self::STATUS_PENDING_CLAIM,
+                'notifyTime' => $now,
+                'expireTime' => $expire,
+                'smsSent' => 1, // 更新为接收方已经收到短信
+                'orderSn' => $orderSn,
+            ], [
+                'id' => intval($giftId),
+                'status' => self::STATUS_PENDING_NOTIFY,
+                'smsSent' => 0,
+            ]);
+            if ($affectedRows > 0) {
+                self::scheduleExpireDelay($giftId, strtotime($expire));
+            }
+            $transaction->commit();
+        } catch (\Throwable $throwable) {
+            if ($transaction->getIsActive()) {
+                $transaction->rollBack();
+            }
+            throw $throwable;
         }
-        $gift->save();
-        self::scheduleExpireDelay($gift->id, strtotime($expire));
-        return [true, ''];
+
+        return true;
     }
 
     public static function notifyOneTomorrow($shop, $giftId)

+ 43 - 30
biz-hd/custom/classes/CustomClass.php

@@ -58,44 +58,57 @@ class CustomClass extends BaseClass
             util::fail('农历没有31号');
         }
 
-        $calendar = new Calendar();
-        if ($lunar == 1) {
-            try {
-                $result = $calendar->lunar(date("Y"), $birthdayMonth, $birthdayDate);
-            } catch (\InvalidArgumentException $e) {
-                $msg = $e->getMessage();
-                util::fail($msg);
-            }
-            //农历转阳历,有多处使用,需要同步修改,关键词 change_my_birthday
-            $month = $result['gregorian_month'] ?? 1;
-            $date = $result['gregorian_day'] ?? 1;
-            $birthday = date("Y") . '-' . $month . '-' . $date;
+        // 如果 xhUser 中已经设置了生日,则直接取用
+        if ($user->birthdayTime != 0) {
+            $custom->lunar = $user->lunar;
+            $custom->birthdayDate = $user->birthdayDate;
+            $custom->birthdayMonth = $user->birthdayMonth;
+            $custom->birthday = $user->birthday;
+            $custom->birthdayTime = $user->birthdayTime;
+            $custom->save();
         } else {
-            $birthday = date("Y") . '-' . $birthdayMonth . '-' . $birthdayDate;
-        }
-        $birthdayTime = strtotime($birthday);
+            $calendar = new Calendar();
+            if ($lunar == 1) {
+                try {
+                    $result = $calendar->lunar(date("Y"), $birthdayMonth, $birthdayDate);
+                } catch (\InvalidArgumentException $e) {
+                    $msg = $e->getMessage();
+                    util::fail($msg);
+                }
+                //农历转阳历,有多处使用,需要同步修改,关键词 change_my_birthday
+                $month = $result['gregorian_month'] ?? 1;
+                $date = $result['gregorian_day'] ?? 1;
+                $birthday = date("Y") . '-' . $month . '-' . $date;
+            } else {
+                $birthday = date("Y") . '-' . $birthdayMonth . '-' . $birthdayDate;
+            }
+            $birthdayTime = strtotime($birthday);
 
-        if (getenv('YII_ENV') != 'dev' && $user->birthdaySet == 1) {
-            util::fail('客户已填写,不能修改');
-        }
+            if (getenv('YII_ENV') != 'dev' && $user->birthdaySet == 1) {
+                util::fail('客户已填写,不能修改');
+            }
 
-        $custom->lunar = $lunar;
-        $custom->birthdayDate = $birthdayDate;
-        $custom->birthdayMonth = $birthdayMonth;
-        $custom->birthday = $birthday;
-        $custom->birthdayTime = $birthdayTime;
-        $custom->save();
+            $custom->lunar = $lunar;
+            $custom->birthdayDate = $birthdayDate;
+            $custom->birthdayMonth = $birthdayMonth;
+            $custom->birthday = $birthday;
+            $custom->birthdayTime = $birthdayTime;
+            $custom->save();
 
-        $user->lunar = $lunar;
-        $user->birthdayDate = $birthdayDate;
-        $user->birthdayMonth = $birthdayMonth;
-        $user->birthday = $birthday;
-        $user->birthdayTime = $birthdayTime;
-        $user->save();
+            $user->lunar = $lunar;
+            $user->birthdayDate = $birthdayDate;
+            $user->birthdayMonth = $birthdayMonth;
+            $user->birthday = $birthday;
+            $user->birthdayTime = $birthdayTime;
+            $user->save();
+        }
 
         // 同步变更生日赠礼记录表(xhBirthdayGift)中的数据(只更新当年年份的,往年的不去修改)
         $birthdayGift = BirthdayGiftClass::getByCondition(['userId'=>$user->id, 'shopId'=>$shopId, 'year'=>date("Y")], true);
         if (!empty($birthdayGift)) {
+            $lunar = intval($custom->lunar);
+            $birthdayMonth = intval($custom->birthdayMonth);
+            $birthdayDate = intval($custom->birthdayDate);
             list($giftBirthdayMonth, $giftBirthdayDate) = BirthdayGiftClass::convertBirthdayMonthDay(
                 $lunar,
                 $birthdayMonth,

+ 5 - 2
common/components/ChuanglanSmsApi.php

@@ -54,9 +54,10 @@ class ChuanglanSmsApi {
 	 *
 	 * @param string $mobile 		手机号码
 	 * @param string $msg 			短信内容
-	 * @param string $needstatus 	是否需要状态报告
+	 * @param string|bool $needstatus 是否需要状态报告
+	 * @param string $callbackUrl 	状态回执地址
 	 */
-	public function sendSMS( $mobile, $msg, $needstatus = 'true') {
+	public function sendSMS( $mobile, $msg, $needstatus = 'true', $callbackUrl = '', $uid = '') {
 		
 		//创蓝接口参数
 		$postArr = array (
@@ -65,6 +66,8 @@ class ChuanglanSmsApi {
 			'msg' => urlencode($msg),
 			'phone' => $mobile,
 			'report' => $needstatus,
+			'callbackUrl' => $callbackUrl,
+			'uid' => $uid //任意自定义参数(类型是字符串)
        		);
 		$result = $this->curlPost( self::API_SEND_URL, $postArr);
 		return $result;

+ 80 - 0
console/controllers/CustomController.php

@@ -109,6 +109,86 @@ class CustomController extends Controller
 
     }
 
+    /**
+     * 每天凌晨 3:35 执行:阴历生日跨年刷新
+     * 从 xhUser 分页取出 lunar=1 且 birthdayTime 已过的用户,农历转当年/次年阳历后回写用户并同步 xhCustom
+     * 命令:./yii custom/update-birthday
+     */
+    public function actionUpdateBirthday()
+    {
+        try {
+            date_default_timezone_set('PRC');
+            ini_set('memory_limit', '512M');
+            set_time_limit(0);
+
+            $calendar = new Calendar();
+            $now = time();
+            $todayStart = strtotime(date('Y-m-d'));
+            $updatedUserCount = 0;
+
+            // 生日主数据在 xhUser;按 id 分页避免一次加载全表
+            $query = User::find()
+                ->select('id, birthday, birthdayTime, birthdayMonth, birthdayDate, lunar')
+                ->where(['lunar' => 1])
+                ->andWhere(['<', 'birthdayTime', $now])
+                ->orderBy('id ASC');
+
+            foreach ($query->batch(200) as $userList) {
+                foreach ($userList as $user) {
+                    $userId = $user->id;
+                    $birthdayMonth = $user->birthdayMonth;
+                    $birthdayDate = $user->birthdayDate;
+                    $year = (int)date('Y');
+
+                    try {
+                        // 农历转公历,有多处使用,需要同步修改,关键词 change_my_birthday
+                        $result = $calendar->lunar($year, $birthdayMonth, $birthdayDate);
+                        $month = $result['gregorian_month'] ?? 1;
+                        $day = $result['gregorian_day'] ?? 1;
+                        $birthday = $year . '-' . $month . '-' . $day;
+
+                        // 今年阳历生日已过,则换算明年农历对应阳历
+                        if (strtotime($birthday) < $todayStart) {
+                            $year++;
+                            $result = $calendar->lunar($year, $birthdayMonth, $birthdayDate);
+                            $month = $result['gregorian_month'] ?? 1;
+                            $day = $result['gregorian_day'] ?? 1;
+                            $birthday = $year . '-' . $month . '-' . $day;
+                        }
+                    } catch (\InvalidArgumentException $e) {
+                        noticeUtil::push(
+                            '阴历转阳历脚本单条失败,userId:' . $userId . ' 月份:' . $birthdayMonth . ' 日期:' . $birthdayDate . ' ' . $e->getMessage(),
+                            '15280215347'
+                        );
+                        continue;
+                    }
+
+                    $birthdayTime = strtotime($birthday);
+                    if ($birthdayTime <= 0) {
+                        continue;
+                    }
+
+                    // 先更新用户表(生日源数据),再按 userId 同步门店客户
+                    UserClass::updateById($userId, [
+                        'birthday' => $birthday,
+                        'birthdayTime' => $birthdayTime,
+                    ]);
+                    CustomClass::updateByCondition(['userId' => $userId], [
+                        'birthday' => $birthday,
+                        'birthdayTime' => $birthdayTime,
+                    ]);
+                    $updatedUserCount++;
+                }
+            }
+
+            Yii::info('阴历转阳历脚本成功执行,更新用户数:' . $updatedUserCount);
+            echo "阴历转阳历完成,更新用户数:{$updatedUserCount}\n";
+        } catch (\Exception $e) {
+            Yii::error('阴历转阳历脚本执行失败,原因:' . $e->getMessage());
+            echo '阴历转阳历脚本执行失败,原因:' . $e->getMessage() . "\n";
+        }
+    }
+
     // 把所有客户的累计消费和成长值、积分都刷新一下
     public function actionRefreshCustomData()
     {

+ 1 - 1
console/controllers/UserController.php

@@ -102,4 +102,4 @@ class UserController extends Controller
         }
     }
 
-}
+}