feat : auto dispense coupon

This commit is contained in:
2025-03-19 16:25:54 +08:00
parent f4d681a23a
commit 44170132e7
10 changed files with 331 additions and 59 deletions

View File

@@ -39,7 +39,7 @@ class CouponAutoDispenseConsumer extends ConsumerMessage
*/
public function consumeMessage($data, AMQPMessage $message): Result
{
if (!$data['coupon_dispense_id']) {
if (!$data['coupon_dispense_id'] || !$data['user_id']) {
$this->log->error('CouponAutoDispenseConsumer:error:NoData:'.json_encode($data));
return Result::ACK;
}
@@ -48,6 +48,7 @@ class CouponAutoDispenseConsumer extends ConsumerMessage
$service = new AutoDispenseService();
$service->couponDispenseId = $data['coupon_dispense_id'];
$service->userId = $data['user_id'];
$service->handle();

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Amqp\Consumer;
use App\Constants\Common\ThirdCode;
use App\Constants\Common\WxMiniCode;
use App\Exception\ErrException;
use App\Lib\Log;
use App\Model\UserThird;
use App\Service\ServiceTrait\Api\WxMiniTrait;
use Exception;
use Hyperf\Amqp\Message\Type;
use Hyperf\Amqp\Result;
use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Amqp\Message\ConsumerMessage;
use Hyperf\Di\Annotation\Inject;
use PhpAmqpLib\Message\AMQPMessage;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
#[Consumer(exchange: 'wxSubMessage', routingKey: 'wxSubMessage', queue: 'wxSubMessage.send', name: "WxSubMessageConsumer", nums: 1)]
class WxSubMessageConsumer extends ConsumerMessage
{
use WxMiniTrait;
/**
* @var Type|string 消息类型
*/
protected Type|string $type = Type::DIRECT;
/**
* @var Log
*/
#[Inject]
protected Log $log;
/**
* @var UserThird
*/
#[Inject]
protected UserThird $userThirdModel;
/**
* @param $data
* @param AMQPMessage $message
* @return Result
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function consumeMessage($data, AMQPMessage $message): Result
{
if (!$data['type'] || !$data['user_id'] || !$data['data']) {
$this->log->error('WxSubMessageConsumer:error:NoData:'.json_encode($data));
return Result::ACK;
}
$openId = $this->userThirdModel->where('user_id',$data['user_id'])->where('type',ThirdCode::WX_LOGIN)->value('open_id') ?? null;
if (empty($openId)) {
$this->log->error('WxSubMessageConsumer:error:NoOpenId:'.json_encode($data));
return Result::ACK;
}
try {
$this->sendSubMessage(WxMiniCode::TEMPLATE_ID_LIST[$data['type']],$openId,$data['data'],$data['page'] ?? null);
} catch (Exception|ErrException $e) {
$this->log->error('WxSubMessageConsumer:error:'.$e->getMessage().':data:'.json_encode($data));
}
return Result::ACK;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Amqp\Producer;
use Hyperf\Amqp\Annotation\Producer;
use Hyperf\Amqp\Message\ProducerMessage;
use Hyperf\Amqp\Message\Type;
#[Producer(exchange: 'wxSubMessage', routingKey: 'wxSubMessage')]
class WxSubMessageProducer extends ProducerMessage
{
/**
* @var Type|string 消息类型
*/
protected Type|string $type = Type::DIRECT;
public function __construct($data)
{
/**
* $data string array => {"type":"1","user_id":"1","data":[],"page":null}
*/
$this->payload = $data;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Constants\Common;
class WxMiniCode
{
/**
* 发送优惠券订阅消息模板id
*/
const string SEND_SUB_MESSAGE_GET_COUPON_TEMPLATE_ID = 'akV-wJt0AHWBpzPKvIsa0IRuzrf5yWPaMYvRRioI8XM';
const string SEND_SUB_MESSAGE_DELIVER_TEMPLATE_ID = 'xxxxxxxxxx';
/**
* @var int 发送消息类型 1=获取优惠券 2=送达
*/
CONST INT SEND_MSG_TYPE_GET_COUPON = 1;
CONST INT SEND_MSG_TYPE_DELIVER = 2;
const array TEMPLATE_ID_LIST = [
self::SEND_MSG_TYPE_GET_COUPON => self::SEND_SUB_MESSAGE_GET_COUPON_TEMPLATE_ID,
self::SEND_MSG_TYPE_DELIVER => self::SEND_SUB_MESSAGE_DELIVER_TEMPLATE_ID
];
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Model;
use App\Constants\Common\CouponCode;
use Hyperf\Database\Model\Builder;
use Hyperf\DbConnection\Model\Model;
/**
@@ -58,6 +59,16 @@ class CouponDispenseUser extends Model
->toArray();
}
/**
* @param int $dispenseId
* @param int $userId
* @return Builder|\Hyperf\Database\Model\Model|null
*/
public function getInfoByDispenseIdAndUserId(int $dispenseId, int $userId): \Hyperf\Database\Model\Model|Builder|null
{
return $this->where('dispense_id', $dispenseId)->where('user_id', $userId)->first();
}
/**
* @param array $userIds
* @param int $count

View File

@@ -71,6 +71,12 @@ class DispenseAddService extends BaseService
#[Inject]
protected CouponTemplate $couponTemplateModel;
/**
* @var Producer
*/
#[Inject]
protected Producer $producer;
/**
* @var int
*/
@@ -132,14 +138,7 @@ class DispenseAddService extends BaseService
$this->addSubTableInformation($this->dispenseId);
});
if ($this->claimRule == CouponCode::DISPENSE_CLAIM_RULE_POST_ACCOUNT) {
//mq发放优惠券
$message = new CouponAutoDispenseProducer([
'coupon_dispense_id' => $this->dispenseId,
]);
$producer = ApplicationContext::getContainer()->get(Producer::class);
$producer->produce($message);
}
$this->sendAutoMq();
return $this->return->success();
}catch (Exception $e) {
@@ -147,6 +146,28 @@ class DispenseAddService extends BaseService
}
}
/**
* @return void
*/
private function sendAutoMq(): void
{
if ($this->claimRule != CouponCode::DISPENSE_CLAIM_RULE_POST_ACCOUNT) return;
if (empty($this->userIds)) return;
foreach ($this->userIds as $userId) {
//mq发放优惠券
$message = new CouponAutoDispenseProducer([
'coupon_dispense_id' => $this->dispenseId,
'user_id' => $userId,
]);
// $producer = ApplicationContext::getContainer()->get(Producer::class);
$this->producer->produce($message);
}
//todo 加一个缓存进度控制器 有没有必要
}
/**
* @param int $dispenseId
* @return void

View File

@@ -10,19 +10,22 @@ declare(strict_types=1);
namespace App\Service\Amqp\Coupon;
use App\Amqp\Producer\WxSubMessageProducer;
use App\Constants\Common\CouponCode;
use App\Constants\Common\WxMiniCode;
use App\Model\CouponDispenseLog;
use App\Model\CouponDispenseUser;
use App\Model\CouponTemplate;
use App\Model\UserCoupon;
use App\Service\ServiceTrait\Api\CouponTrait;
use Exception;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Amqp\Producer;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
class AutoDispenseService
{
const int SINGLE_MAX = 1;
use CouponTrait;
/**
* @var CouponDispenseLog
@@ -36,59 +39,108 @@ class AutoDispenseService
#[Inject]
protected CouponDispenseUser $couponDispenseUserModel;
/**
* @var UserCoupon
*/
#[Inject]
protected UserCoupon $userCouponModel;
/**
* @var CouponTemplate
*/
#[Inject]
protected CouponTemplate $couponTemplateModel;
/**
* @var Producer
*/
#[Inject]
protected Producer $producer;
/**
* @var int
*/
public int $couponDispenseId;
/**
* @var int
*/
public int $userId;
/**
* @return void
* @throws Exception
*/
public function handle(): void
{
$dispenseInfo = $this->couponDispenseLogModel->getInfoById($this->couponDispenseId);
if (empty($dispenseInfo)) throw new Exception('分发记录不存在');
$userIds = $this->couponDispenseUserModel->getNoSendUserListByDispenseId($this->couponDispenseId);
if (empty($userIds)) throw new Exception('该分发已发送完毕');
$templateInfo = $this->couponTemplateModel->getInfoById($dispenseInfo->coupon_template_id);
if (empty($templateInfo)) throw new Exception('优惠券模板不存在');
$partArr = array_chunk($userIds, self::SINGLE_MAX);
$validityTime = $this->getValidityTime((int)$dispenseInfo->validity_time_type, $dispenseInfo->validity_time_value);
//有效期不存在或者到期销毁当前分发逻辑
if (empty($validityTime) || empty($validityTime['start_time']) || empty($validityTime['end_time']) || $validityTime['end_time'] < date('Y-m-d H:i:s')) {
$dispenseInfo->status = CouponCode::DISPENSE_NULL;
if (!$dispenseInfo->save()) throw new Exception('修改分发记录状态失败');
throw new Exception('该分发逻辑已失效');
}
foreach ($partArr as $onePart) {
$insertData = [];
foreach ($onePart as $oneUser) {
// 模拟数据
$oneUserData = [
'user_id' => $oneUser,
'coupon_template_id' => $dispenseInfo->coupon_template_id,
'coupon_name' => $dispenseInfo->coupon_name,
'coupon_dispense_id' => $dispenseInfo->id,
'status' => CouponCode::COUPON_STATUS_UNUSED,
'validity_start_time' => $dispenseInfo->validity_start_time,
'validity_end_time' => $dispenseInfo->validity_end_time,
];
$userDispenseInfo = $this->couponDispenseUserModel->getInfoByDispenseIdAndUserId($this->couponDispenseId,$this->userId);
if (empty($userDispenseInfo)) throw new Exception('该用户不属于该分发逻辑');
$copies = array_map(function () use ($oneUserData) {
return $oneUserData;
}, array_fill(0, $dispenseInfo->item_count, null));
$userCount = $this->userCouponModel->where('coupon_dispense_id',$this->couponDispenseId)->where('user_id',$this->userId)->count() ?? 0;
if ($userCount >= $dispenseInfo->item_count) throw new Exception('该用户该分发逻辑已分发完毕');
$insertData = array_merge($insertData, $copies);
}
// 数据
$oneData = [
'user_id' => $this->userId,
'coupon_template_id' => $dispenseInfo->coupon_template_id,
'coupon_name' => $dispenseInfo->coupon_name,
'coupon_dispense_id' => $dispenseInfo->id,
'status' => CouponCode::COUPON_STATUS_UNUSED,
'validity_start_time' => $validityTime['start_time'],
'validity_end_time' => $validityTime['end_time'],
];
Db::transaction(function () use ($insertData,$dispenseInfo,$onePart) {
//添加优惠券信息
if (!(new UserCoupon())->insert($insertData)) throw new Exception('写入数据失败');
//修改分发用户信息
if (!(new CouponDispenseUser())->updateReceiveCountByUserIds($onePart,$dispenseInfo->item_count)) throw new Exception('修改分发用户信息失败');
$insertData = array_map(function () use ($oneData) {
return $oneData;
}, array_fill(0, $dispenseInfo->item_count - $userCount, null));
//修改分发 log 信息 (修改receive_count = origin_value + count($insertData) ps : 查询还没发的用户 * item_count / total_count = 完成百分比)
if (!(new CouponDispenseLog())->updateReceiveCountById($dispenseInfo->id,count($insertData))) throw new Exception('修改分发log信息失败');
});
Db::transaction(function () use ($insertData,$dispenseInfo,$userDispenseInfo) {
// 添加优惠券信息
if (!(new UserCoupon())->insert($insertData)) throw new Exception('写入数据失败');
// 休息1秒
Coroutine::sleep(1);
// 修改分发用户信息
$userDispenseInfo->receive_count = $userDispenseInfo->receive_count + count($insertData);
$userDispenseInfo->is_receive = CouponCode::DISPENSE_STATUS_IS_RECEIVED;
if (!$userDispenseInfo->save()) throw new Exception('修改分发用户信息失败');
//修改分发 log 信息 (修改receive_count = origin_value + count($insertData) ps : 查询还没发的用户 * item_count / total_count = 完成百分比)
$dispenseInfo->receive_count = $dispenseInfo->receive_count + count($insertData);
if (!$dispenseInfo->save()) throw new Exception('修改分发log信息失败');
});
$nominalValue = match ($templateInfo->coupon_type) {
CouponCode::COUPON_TYPE_INSTANT_REDUCTION => $templateInfo->amount.'元',
CouponCode::COUPON_TYPE_DISCOUNT => ($templateInfo->ratio * 10).'%',
};
foreach ($insertData as $item) {
$msgData = [
'thing2' => ['value' => $dispenseInfo->coupon_name],
'thing9' => ['value' => $nominalValue],
'thing8' => ['value' => $validityTime['start_time'].'/'.$validityTime['end_time']],
'thing4' => ['value' => 'xxx'],
];
$message = new WxSubMessageProducer([
'type' => WxMiniCode::SEND_MSG_TYPE_GET_COUPON,
'user_id' => $this->userId,
'data' => $msgData,
'page' => null
]);
$this->producer->produce($message);
}
}
}

View File

@@ -16,11 +16,14 @@ use App\Extend\DateUtil;
use App\Model\CouponDispenseLog;
use App\Model\UserCoupon;
use App\Service\Api\BaseService;
use App\Service\ServiceTrait\Api\CouponTrait;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
class ReceiveService extends BaseService
{
use CouponTrait;
/**
* @var CouponDispenseLog
*/
@@ -71,22 +74,10 @@ class ReceiveService extends BaseService
if (empty($couponArr)) return;
foreach ($couponArr as $item) {
$validityTime = [];
switch ($item['validity_time_type']) {
case CouponCode::VALIDITY_TIME_TYPE_CYCLE:
$validityValue = json_decode($item['validity_time_value'],true);
$validityTime = [
'start_time' => date('Y-m-d H:i:s'),
'end_time' => date('Y-m-d H:i:s', time() + ($validityValue['day_num'] * DateUtil::DAY))
];
break;
case CouponCode::VALIDITY_TIME_TYPE_FIX:
$validityTime = json_decode($item['validity_time_value'],true);
break;
}
$validityTime = $this->getValidityTime((int)$item['validity_time_type'], $item['validity_time_value']);
//有效期不存在或者到期销毁当前分发逻辑
if (($validityTime['end_time'] ?? date('Y-m-d H:i:s')) < date('Y-m-d H:i:s')) {
if (empty($validityTime) || ($validityTime['end_time'] ?? date('Y-m-d H:i:s')) < date('Y-m-d H:i:s')) {
$this->destroyDispenseData[] = $item['id'];
continue;
}

View File

@@ -4,6 +4,7 @@ namespace App\Service\ServiceTrait\Api;
use App\Constants\Common\CouponCode;
use App\Constants\Common\OrderCode;
use App\Extend\DateUtil;
use App\Model\UserCoupon;
use Exception;
use Hyperf\Di\Annotation\Inject;
@@ -38,4 +39,25 @@ trait CouponTrait
if (!$couponInfo->save()) throw new Exception('CancelOrderConsumer:error:couponStatusUpdateError:'.json_encode($orderInfo->toArray()));
}
/**
* @param int $type
* @param string $value
* @return array
*/
protected function getValidityTime(int $type,string $value): array
{
switch ($type) {
case CouponCode::VALIDITY_TIME_TYPE_CYCLE:
$validityValue = json_decode($value,true);
return [
'start_time' => date('Y-m-d H:i:s'),
'end_time' => date('Y-m-d H:i:s', time() + ($validityValue['day_num'] * DateUtil::DAY))
];
case CouponCode::VALIDITY_TIME_TYPE_FIX:
return json_decode($value,true);
default:
return [];
}
}
}

View File

@@ -15,6 +15,7 @@ use App\Cache\Redis\Common\CommonRedisKey;
use App\Cache\Redis\RedisCache;
use App\Constants\RedisCode;
use App\Exception\ErrException;
use App\Extend\SystemUtil;
use App\Lib\Log;
use GuzzleHttp\Exception\GuzzleException;
use Hyperf\Di\Annotation\Inject;
@@ -194,4 +195,54 @@ trait WxMiniTrait
throw new ErrException($e->getMessage());
}
}
/**
* @param string $templateId
* @param string $toUserOpenId
* @param array $data
* @param string|null $page
* @return mixed
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
protected function sendSubMessage(string $templateId, string $toUserOpenId, array $data ,string $page = null): mixed
{
$url = sprintf(
'/cgi-bin/message/subscribe/send?access_token=%s',
$this->getAccessTokenCache()
);
$params = [
'template_id' => $templateId,
'touser' => $toUserOpenId,
'data' => $data,
'miniprogram_state' => !SystemUtil::checkProEnv() ? 'developer' : 'formal', //不是正式环境 使用开发版
'lang' => 'zh_CN'
];
if (!empty($page)) $params['page'] = $page;
try {
$wxResponse = $this->clientFactory->create([
'base_uri' => $this->BaseUri,
'timeout' => 5
])->post($url,[
'headers' => [
'Content-Type' => 'application/json'
],
'body' => json_encode($params)
]);
$contents = $wxResponse->getBody()->getContents();
$this->log->callbackLog(__class__.':微信服务器返回send_sub_message信息:'.json_encode($contents));
$res = json_decode($contents,true);
if (!empty($res['errcode']) && $res['errcode'] != 0) throw new ErrException($res['errmsg'] ?? '系统繁忙');
return $res;
}catch (GuzzleException $e){
throw new ErrException($e->getMessage());
}
}
}