Bläddra i källkod

feat(birthday): 按短信送达结果推进生日赠礼

- 为生日通知短信增加带 HMAC 校验的送达状态回调
- 仅在确认送达后原子推进赠礼状态并安排过期任务
- 同步客户生日时以最终保存值更新当年赠礼记录
- API: 新增 GET /birthday-gift/notify-state 回调,并扩展短信发送可选回调参数
- Database: 无结构或数据变更
- Breaking Changes: 无
shizhongqi 4 veckor sedan
förälder
incheckning
2667aeb97d

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

+ 98 - 17
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;
@@ -671,8 +672,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 +693,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, ''];
@@ -749,18 +758,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;