|
|
@@ -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)
|