欢迎光临
我们一直在努力

CRMEB Pro 优惠券过期处理:定时任务、活动关联和历史订单怎么兼容

摘要

优惠券过期不是简单把状态改成“已过期”。在 CRMEB Pro 里,优惠券可能来自手动领取、系统发放、新人礼、会员卡激活、下单赠送、直播间领取,也可能已经被订单使用,或者在退款时需要回退。二开时如果只写一个定时任务批量更新状态,很容易误伤历史订单、赠券记录和直播间统计。

这篇文章围绕 CRMEB Pro 优惠券过期、使用、回退、订单赠券几个关键点做源码拆解,并给出适合二开的任务设计建议。

1. 用户券状态有三类

用户领取后的优惠券模型在:

app/model/activity/coupon/StoreCouponUser.php

状态映射很清楚:

protected $statusType = [
0 => '未使用',
1 => '已使用',
2 => '已过期'
];

获取方式也不止一种:

protected array $gainType = [
'send' => '系统发放',
'get' => '手动领取',
'newcomer' => '新人礼赠送',
'activate_level' => '会员卡激活赠送',
'user_first' => '用户注册赠送',
'order' => '下单赠送',
'luck_lottery' => '抽奖赠送',
'live' => '直播间领取'
];

这就是为什么“过期处理”不能只看一张表的 end_time。不同来源的券,后续关联动作不一样。

2. CRMEB Pro 已经有按用户检查过期的方法

StoreCouponUserServices 里有一个很关键的方法:

/**
* 过期优惠卷失效
*/

public function checkInvalidCoupon($uid = 0)
{
$this->dao->update([
['uid', '=', $uid],
['end_time', '<', time()],
['status', '=', '0']
], [
'status' => 2
]);
}

获取用户有效券数量时,会先检查过期:

public function getUserValidCouponCount(int $uid)
{
$this->checkInvalidCoupon($uid);
return $this->dao->getCount([
'uid' => $uid,
'status' => 0
]);
}

这个逻辑适合“用户访问时顺手刷新自己的券状态”。但如果后台要做全量过期扫描,不能直接传 uid = 0,否则只会处理 uid = 0 的数据。

3. 二开全量过期任务,建议单独封装 Dao 查询

不要在 Job 里直接写 Db::name('store_coupon_user')。按照项目分层,建议在 StoreCouponUserDao 增加方法:

/**
* 分页获取已过期但未处理的用户券ID
* @param int $limit 每批处理数量
* @return array
*/

public function getExpiredUnusedIds(int $limit = 500): array
{
return $this->getModel()
->where('status', 0)
->where('end_time', '<', time())
->limit($limit)
->column('id');
}

再加一个批量更新方法:

/**
* 批量标记用户券过期
* @param array $ids 用户券ID
* @return bool
*/

public function markExpiredByIds(array $ids): bool
{
$ids = array_values(array_filter(array_map('intval', $ids)));
if (!$ids) {
return true;
}

return $this->getModel()
->whereIn('id', $ids)
->where('status', 0)
->update(['status' => 2]) !== false;
}

Service 层负责编排:

/**
* 批量处理过期用户券
* @param int $limit 每批数量
* @return int 本批处理数量
*/

public function batchInvalidExpiredCoupon(int $limit = 500): int
{
$ids = $this->dao->getExpiredUnusedIds($limit);
if (!$ids) {
return 0;
}

$this->dao->markExpiredByIds($ids);
return count($ids);
}

Job 只调用 Service:

class CouponExpireJob extends BaseJobs
{
use QueueTrait;

public function doJob(int $limit = 500)
{
try {
$services = app()->make(StoreCouponUserServices::class);
do {
$count = $services->batchInvalidExpiredCoupon($limit);
} while ($count === $limit);
} catch (\\Throwable $e) {
response_log_write([
'message' => '优惠券过期处理失败:' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}

return true;
}
}

这样做有三个好处:

Job 不直接查库
Dao 负责查询和批量更新
Service 负责循环和业务编排

4. 订单使用后,不能被过期任务改掉

用户下单使用优惠券时,会调用用户券服务:

public function useCoupon(int $couponId, int $uid, array $cartInfo, array $promotions = [], int $liveRoomId = 0)
{
if (!$couponId || !$uid || !$cartInfo) {
return true;
}

$promotionsServices = app()->make(StorePromotionsServices::class);
[$couponInfo, $couponPrice] = $promotionsServices->useCoupon(
$couponId,
$uid,
$cartInfo,
$promotions,
$liveRoomId
);

if ($couponInfo) {
$this->dao->useCoupon($couponId);
}

return true;
}

Dao 里会把状态改为已使用:

public function useCoupon(int $id)
{
return $this->getModel()
->where('id', $id)
->update([
'status' => 1,
'use_time' => time()
]);
}

因此全量过期任务必须只处理:

status = 0
end_time < 当前时间

不要写成“所有 end_time 小于当前时间都改过期”。已使用的券是历史凭证,不能被覆盖成已过期,否则订单详情、售后、统计报表都会产生歧义。

5. 退款时优惠券可能要回退

订单退款时,CRMEB Pro 有回退优惠券逻辑:

public function integralAndCouponBack($order)
{
$res = true;

// 回退优惠卷,拆分子订单不退优惠券
if (!$order['pid'] && $order['coupon_id'] && $order['coupon_price']) {
$coumonUserServices = app()->make(StoreCouponUserServices::class);
$res = $res && $coumonUserServices->recoverCoupon((int)$order['coupon_id']);
}

[$order, $changeIntegral] = $this->regressionIntegral($order);
return $res && $order->save();
}

recoverCoupon() 会把已使用券恢复为未使用:

public function recoverCoupon(int $id)
{
$coupon = $this->dao->getOne([
'id' => $id
], 'id,status,live_room_coupon_id');

if (!$coupon || !(int)$coupon['status']) {
return true;
}

$res = $this->dao->update($id, [
'status' => 0,
'use_time' => 0
]);

if ((int)$coupon['status'] === 1 && !empty($coupon['live_room_coupon_id'])) {
$liveRoomCouponDao = app()->make(LiveRoomCouponDao::class);
$liveRoomCouponDao->decUseNum((int)$coupon['live_room_coupon_id']);
}

return $res;
}

这里有一个细节:恢复成未使用后,如果优惠券本身已经超过 end_time,下次用户进入优惠券列表或定时任务扫描时,应该再变成过期。不要在退款回退时直接判断过期并丢掉,因为回退动作的职责是恢复“这张券没有被订单消耗”。

6. 下单赠券也要考虑订单取消和退款

CRMEB Pro 支持商品关联优惠券,下单后赠送。相关 Job 在:

app/jobs/product/ProductCouponJob.php

下单后赠券:

public function doJob($orderInfo)
{
if (!$orderInfo) return true;

try {
$storeProductCouponServices = app()->make(StoreProductCouponServices::class);
$storeProductCouponServices->giveOrderProductCoupon(
(int)$orderInfo['uid'],
(int)$orderInfo['id']
);
} catch (\\Throwable $e) {
response_log_write([
'message' => '赠送订单商品关联优惠券发生错误,错误原因:' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}

return true;
}

退款或订单失败时,会让赠券失效:

public function failureProductCoupon($orderInfo)
{
if (!$orderInfo) return true;

try {
$storeProductCouponServices = app()->make(StoreProductCouponServices::class);
$storeProductCouponServices->failureOrderProductGiveCoupon(
$orderInfo['uid'],
$orderInfo['id']
);
} catch (\\Throwable $e) {
response_log_write([
'message' => '订单退款退还优惠券发生错误,错误原因:' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}

return true;
}

Service 中会按订单、用户、商品和状态筛选赠券:

$where = [
['coupon_product_id', 'in', $productIds],
['uid', '=', $uid],
['oid', 'in', $oidAll],
['status', '=', 0],
['type', '=', 'order']
];

$res = $storeCouponUserServices->update($where, ['status' => 2]);

这说明二开“赠券过期/失效”时,要区分两类状态:

自然过期:end_time 到期,未使用券 status 改为 2
业务失效:订单退款、活动失败、赠券来源失效,未使用券 status 改为 2

状态一样,但备注、日志、触发原因最好分开记录,方便售后排查。

7. 发布券本身过期和用户券过期不是一回事

发布券表 store_coupon_issue 用于控制“这张券还能不能被领取”,用户券表 store_coupon_user 用于控制“某个用户手里的券还能不能使用”。

发布券有效查询在 Dao 中:

public function validSearch(array $where = [])
{
return parent::search($where)
->where('status', 1)
->where('is_del', 0)
->where('remain_count > 0 OR is_permanent = 1')
->where(function ($query) {
$query->where(function ($query) {
$query->where('start_time', '<=', time())
->where('end_time', '>=', time());
})->whereOr(function ($query) {
$query->where('start_time', 0)
->where('end_time', 0);
});
})
->where(function ($query4) {
$query4->where(function ($query5) {
$query5->where('coupon_time', 0)
->where('end_use_time', '>=', time());
})->whereOr('coupon_time', '>', 0);
});
}

这里判断的是发布券是否还能展示、领取、发放。用户已经领取到手的券,要看 store_coupon_user.start_time/end_time/status。

二开时别把两个概念混到一个定时任务里:

发布券过期:影响后续能不能领
用户券过期:影响用户手里的券能不能用

8. 建议补一个过期处理日志

如果项目后续要做更细的售后排查,建议不要只改 status,可以补一个轻量日志表或复用现有操作日志体系,记录:

coupon_user_id
uid
cid
old_status
new_status
reason
operator_type
add_time

Service 示例:

/**
* 标记用户券过期并记录原因
* @param array $ids 用户券ID
* @param string $reason 过期原因
* @return bool
*/

public function markCouponExpired(array $ids, string $reason = '自然过期'): bool
{
return $this->transaction(function () use ($ids, $reason) {
$res = $this->dao->markExpiredByIds($ids);

// 如果项目已有日志服务,建议在这里批量写入日志。
// 注意日志写入也要走对应 Services/Dao,不要在这里直接拼 SQL。

return $res;
});
}

日志不是为了“好看”,而是为了回答售后经常问的三个问题:

这张券为什么不能用了?
是自己过期,还是退款导致失效?
什么时候被系统处理的?

9. 关键目录说明

app/model/activity/coupon/StoreCouponUser.php
用户券模型,包含状态和获取方式映射。

app/services/activity/coupon/StoreCouponUserServices.php
用户券过期检查、使用、回退、可用券筛选。

app/dao/activity/coupon/StoreCouponUserDao.php
用户券查询和状态更新,适合放批量过期查询。

app/services/order/StoreOrderRefundServices.php
退款时回退积分和优惠券。

app/jobs/product/ProductCouponJob.php
商品关联优惠券赠送、退款失效任务。

app/services/product/product/StoreProductCouponServices.php
下单赠券、订单失败后赠券失效。

10. 二开注意事项

  • 全量过期任务只处理 status = 0 的用户券,不要覆盖已使用券。
  • 用户券过期和发布券过期是两件事,分别对应不同表和不同业务含义。
  • 退款回退优惠券时,先恢复“未使用”,后续再由过期检查处理是否已经到期。
  • 下单赠券失效要按订单、用户、来源类型筛选,避免误伤用户自己领取的券。
  • Job 里不要直接查库,批量查询和更新放 Dao,业务编排放 Services。
  • 如果后续要做售后排查,建议记录过期原因或处理日志。
  • 标签建议

    CRMEB Pro
    优惠券过期
    定时任务
    订单退款
    二次开发
    ThinkPHP
    源码解析

    赞(0)
    未经允许不得转载:171主机测评 » CRMEB Pro 优惠券过期处理:定时任务、活动关联和历史订单怎么兼容
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址