CRMEB Pro 优惠券过期处理:定时任务、活动关联和历史订单怎么兼容
摘要优惠券过期不是简单把状态改成“已过期”。在 CRMEB Pro 里优惠券可能来自手动领取、系统发放、新人礼、会员卡激活、下单赠送、直播间领取也可能已经被订单使用或者在退款时需要回退。二开时如果只写一个定时任务批量更新状态很容易误伤历史订单、赠券记录和直播间统计。这篇文章围绕 CRMEB Pro 优惠券过期、使用、回退、订单赠券几个关键点做源码拆解并给出适合二开的任务设计建议。1. 用户券状态有三类用户领取后的优惠券模型在app/model/activity/coupon/StoreCouponUser.php状态映射很清楚protected$statusType[0未使用,1已使用,2已过期];获取方式也不止一种protectedarray$gainType[send系统发放,get手动领取,newcomer新人礼赠送,activate_level会员卡激活赠送,user_first用户注册赠送,order下单赠送,luck_lottery抽奖赠送,live直播间领取];这就是为什么“过期处理”不能只看一张表的end_time。不同来源的券后续关联动作不一样。2. CRMEB Pro 已经有按用户检查过期的方法StoreCouponUserServices里有一个很关键的方法/** * 过期优惠卷失效 */publicfunctioncheckInvalidCoupon($uid0){$this-dao-update([[uid,,$uid],[end_time,,time()],[status,,0]],[status2]);}获取用户有效券数量时会先检查过期publicfunctiongetUserValidCouponCount(int$uid){$this-checkInvalidCoupon($uid);return$this-dao-getCount([uid$uid,status0]);}这个逻辑适合“用户访问时顺手刷新自己的券状态”。但如果后台要做全量过期扫描不能直接传uid 0否则只会处理uid 0的数据。3. 二开全量过期任务建议单独封装 Dao 查询不要在 Job 里直接写Db::name(store_coupon_user)。按照项目分层建议在StoreCouponUserDao增加方法/** * 分页获取已过期但未处理的用户券ID * param int $limit 每批处理数量 * return array */publicfunctiongetExpiredUnusedIds(int$limit500):array{return$this-getModel()-where(status,0)-where(end_time,,time())-limit($limit)-column(id);}再加一个批量更新方法/** * 批量标记用户券过期 * param array $ids 用户券ID * return bool */publicfunctionmarkExpiredByIds(array$ids):bool{$idsarray_values(array_filter(array_map(intval,$ids)));if(!$ids){returntrue;}return$this-getModel()-whereIn(id,$ids)-where(status,0)-update([status2])!false;}Service 层负责编排/** * 批量处理过期用户券 * param int $limit 每批数量 * return int 本批处理数量 */publicfunctionbatchInvalidExpiredCoupon(int$limit500):int{$ids$this-dao-getExpiredUnusedIds($limit);if(!$ids){return0;}$this-dao-markExpiredByIds($ids);returncount($ids);}Job 只调用 ServiceclassCouponExpireJobextendsBaseJobs{useQueueTrait;publicfunctiondoJob(int$limit500){try{$servicesapp()-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(),]);}returntrue;}}这样做有三个好处Job 不直接查库 Dao 负责查询和批量更新 Service 负责循环和业务编排4. 订单使用后不能被过期任务改掉用户下单使用优惠券时会调用用户券服务publicfunctionuseCoupon(int$couponId,int$uid,array$cartInfo,array$promotions[],int$liveRoomId0){if(!$couponId||!$uid||!$cartInfo){returntrue;}$promotionsServicesapp()-make(StorePromotionsServices::class);[$couponInfo,$couponPrice]$promotionsServices-useCoupon($couponId,$uid,$cartInfo,$promotions,$liveRoomId);if($couponInfo){$this-dao-useCoupon($couponId);}returntrue;}Dao 里会把状态改为已使用publicfunctionuseCoupon(int$id){return$this-getModel()-where(id,$id)-update([status1,use_timetime()]);}因此全量过期任务必须只处理status 0 end_time 当前时间不要写成“所有 end_time 小于当前时间都改过期”。已使用的券是历史凭证不能被覆盖成已过期否则订单详情、售后、统计报表都会产生歧义。5. 退款时优惠券可能要回退订单退款时CRMEB Pro 有回退优惠券逻辑publicfunctionintegralAndCouponBack($order){$restrue;// 回退优惠卷拆分子订单不退优惠券if(!$order[pid]$order[coupon_id]$order[coupon_price]){$coumonUserServicesapp()-make(StoreCouponUserServices::class);$res$res$coumonUserServices-recoverCoupon((int)$order[coupon_id]);}[$order,$changeIntegral]$this-regressionIntegral($order);return$res$order-save();}recoverCoupon()会把已使用券恢复为未使用publicfunctionrecoverCoupon(int$id){$coupon$this-dao-getOne([id$id],id,status,live_room_coupon_id);if(!$coupon||!(int)$coupon[status]){returntrue;}$res$this-dao-update($id,[status0,use_time0]);if((int)$coupon[status]1!empty($coupon[live_room_coupon_id])){$liveRoomCouponDaoapp()-make(LiveRoomCouponDao::class);$liveRoomCouponDao-decUseNum((int)$coupon[live_room_coupon_id]);}return$res;}这里有一个细节恢复成未使用后如果优惠券本身已经超过end_time下次用户进入优惠券列表或定时任务扫描时应该再变成过期。不要在退款回退时直接判断过期并丢掉因为回退动作的职责是恢复“这张券没有被订单消耗”。6. 下单赠券也要考虑订单取消和退款CRMEB Pro 支持商品关联优惠券下单后赠送。相关 Job 在app/jobs/product/ProductCouponJob.php下单后赠券publicfunctiondoJob($orderInfo){if(!$orderInfo)returntrue;try{$storeProductCouponServicesapp()-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()]);}returntrue;}退款或订单失败时会让赠券失效publicfunctionfailureProductCoupon($orderInfo){if(!$orderInfo)returntrue;try{$storeProductCouponServicesapp()-make(StoreProductCouponServices::class);$storeProductCouponServices-failureOrderProductGiveCoupon($orderInfo[uid],$orderInfo[id]);}catch(\Throwable$e){response_log_write([message订单退款退还优惠券发生错误,错误原因:.$e-getMessage(),file$e-getFile(),line$e-getLine()]);}returntrue;}Service 中会按订单、用户、商品和状态筛选赠券$where[[coupon_product_id,in,$productIds],[uid,,$uid],[oid,in,$oidAll],[status,,0],[type,,order]];$res$storeCouponUserServices-update($where,[status2]);这说明二开“赠券过期/失效”时要区分两类状态自然过期end_time 到期未使用券 status 改为 2 业务失效订单退款、活动失败、赠券来源失效未使用券 status 改为 2状态一样但备注、日志、触发原因最好分开记录方便售后排查。7. 发布券本身过期和用户券过期不是一回事发布券表store_coupon_issue用于控制“这张券还能不能被领取”用户券表store_coupon_user用于控制“某个用户手里的券还能不能使用”。发布券有效查询在 Dao 中publicfunctionvalidSearch(array$where[]){returnparent::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_timeService 示例/** * 标记用户券过期并记录原因 * param array $ids 用户券ID * param string $reason 过期原因 * return bool */publicfunctionmarkCouponExpired(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 源码解析