🎤 面试官:你们平台有优惠券吧?说说你是怎么做的?
我: 有,我们优惠券种类还挺多的:
- 满减券(满100减10)
- 折扣券(打9折)
- 无门槛券(直接减5块)
- 限时秒杀券、新人券、店铺券、平台券……
我主要负责的是发券、领券、用券这一整套流程的设计和开发。
说实话,别看“发个券”好像很简单,真做起来坑特别多,一不小心就超发、被薅、算错价格。
🎤 面试官:用户怎么领取优惠券?怎么防止被脚本抢?
我: 领券看着简单,其实最容易被“黄牛”搞。
比如我们搞“618大促”,放1万张“满200减50”的神券,结果一秒就被机器人抢光了,真实用户一张没有,那就尴尬了。
所以我们做了几层防刷:
- 登录才能领:没登录的用户看不到领券按钮,先登录再说;
- 一人限领一张:Redis里记个
user_coupon:userId:couponId,领过就写进去,再领直接拒绝;- IP限流:同一个IP,每分钟最多领3次,防脚本刷;
- 加验证码:热门券点击“领取”时弹个滑动验证码,机器人过不去;
- 异步队列削峰:领券请求先扔进RocketMQ,后端慢慢处理,避免数据库被打爆。
就这么几招一上,基本就能把“黄牛”挡在外面,真实用户也能抢到。
🎤 面试官:那如果Redis挂了,会不会重复发券?
我: 会啊,所以我们不敢只靠Redis。
Redis我们当“第一道防线”,速度快,但不保险。
真正防重的,是数据库唯一索引。
比如我们有张表叫
user_coupon,字段是:user_id, coupon_id, status然后加个唯一索引:
UNIQUE KEY `uk_user_coupon` (user_id, coupon_id)这样就算Redis没拦住,或者MQ重复消费,最后插数据库的时候也会报错“Duplicate entry”,我们就知道这人已经领过了。
所以说:Redis用来挡流量,数据库才是最终裁判。
🎤 面试官:下单时怎么使用优惠券?怎么计算优惠金额?
我: 这个比领券还复杂,因为涉及“选哪张券最划算”、“能不能和其他活动叠加”、“分摊到每个商品多少钱”……
我们是这么做的:
前端选券:用户下单页可以看到自己能用的券,比如“满200减20”、“全场9折”,他手动选一张;
后端校验:收到请求后,先查这张券是不是他的、有没有过期、能不能用在这个订单上;
计算优惠金额:
- 如果是“满100减10”,订单原价300,那就减30;
- 如果是“打9折”,那就原价 × 0.9;
- 如果是“无门槛减5”,直接减5;
金额分摊:比如订单有3个商品,总价300,用了“满300减30”,那每个商品分摊10块优惠,方便后续退款时算账。
有个细节:不能让用户自己传“用哪张券”和“减多少钱”,必须后端自己算,否则前端改个数字就能白嫖了。
🎤 面试官:优惠券和满减、秒杀活动能不能叠加?
我: 这个完全是产品说了算,我们开发只是实现规则。
一般来说:
- 平台券和店铺券不能叠加(避免亏太多);
- 优惠券和满减活动通常也不能叠加;
- 但新人券或红包可能可以和别的叠。
我们的做法是: 在券的配置里加个字段:
can叠加: false, 下单时根据这个字段判断要不要和其他优惠一起算。最麻烦的是“哪个更划算”的提示,比如系统要告诉用户:“用A券省20,用B券省25,推荐用B”。 这块我们是把所有可用券都试算一遍,挑个最省钱的推荐给他。
🎤 面试官:用户下单后取消订单,优惠券怎么处理?
我: 分两种情况:
- 订单未支付,用户主动取消:
- 我们就把优惠券“还回去”,状态从“已使用”改回“未使用”;
- 一般通过MQ发个消息,异步处理,避免卡主下单流程。
- 订单已支付,用户申请退款:
- 如果是全额退款,券还回去,还能用;
- 如果是部分退款,就按“优惠分摊比例”算: 比如订单300,用了30的券,退100的货,那就退回
30 × (100/300) = 10块的优惠额度;- 但有些券是“一次性”的,退了就不能再用,得看券的规则。
总之一句话:券能不能退、退多少,都得看当初发券时定的规则,我们只是忠实执行。
🎤 面试官:有没有遇到过被“薅羊毛”的情况?
我: 遇到过!最惨一次是——
我们发了个“注册送5元无门槛券”,结果有黑产批量注册了上万个账号,每人都领券下单,然后退款…… 我们一天亏了十几万。
后来我们加了风控:
- 新用户领券后,必须完成实名认证或首单支付成功才能用;
- 同一设备、同一IP、同一银行卡,限制最多注册3个账号;
- 对异常订单做标记,自动冻结提现和发券资格。
现在发券前,产品都得先找我们评估风险:“这券会不会被薅?怎么防?” 我们也学会了:发券不是送钱,是送规则。
🎤 面试官:你刚说新用户要实名认证才能用券,那你们的实名认证是怎么做的?
我: 我们做的不是那种“对公对私转账验证”的高级认证,而是最常见的——姓名 + 身份证号 + 手机号三要素核验。
用户在 App 里填上自己的:
- 真实姓名
- 身份证号
- 当前手机号
然后我们调第三方接口,比如阿里云、腾讯云、百度智能云提供的“实人认证”服务,他们背后对接的是公安系统的数据库,能实时验证这三个信息是不是匹配、身份证是不是真实有效的。
🎤 面试官:那用户随便填一个身份证号,也能过吗?
我: 不能。
我们调的是运营商级别的三要素接口,它不只是查“身份证号合不合法”,还会验证:
- 姓名和身份证号是不是对得上;
- 手机号是不是这个身份证本人实名注册的(也就是“本机号码核验”)。
比如你填张三的身份证,但手机号是李四办的,那就过不了。
这种接口我们是按次收费的,一次几毛钱,但很准,基本防住了99%的假信息。
🎤 面试官:那黑产去买真实的身份证信息呢?会不会还是被绕过?
我: 这个确实防不住,如果人家拿的是真实身份证+真实手机号,那系统是判断不了的。
但我们加了几层“行为风控”来补:
设备指纹:同一个手机设备,最多允许认证3个账号;
IP限制:同一IP下短时间内认证太多账号,直接拦截;
时间间隔:刚注册就马上认证,还领券下单,这种“一条龙”行为标记为高风险;
人脸活体检测:
对于高价值场景(比如提现、大额优惠),我们会弹一个人脸验证:
- 让用户拍张照片 or 做个眨眼动作;
- 调用阿里云“活体检测”接口,确认是真人,不是照片 or 录像。
这样一来,就算他有真实身份证,但批量操作的成本就上去了,一般黑产就不干了。
🎤 面试官:认证信息你们自己存吗?会不会有隐私问题?
我: 存,但加密存,而且有严格管控。
我们数据库里有张表叫
user_auth,字段是:sqluser_id, id_card_encrypted, name_encrypted, auth_status, create_time
- 身份证号和姓名都用AES加密存的,数据库里看不到明文;
- 只有风控 or 客服查问题时,才通过解密服务临时查看;
- 所有访问记录都打日志,审计留痕。
另外,我们也遵守《个人信息保护法》,用户注销账号后,我们会把他的认证信息彻底删除。
🎤 面试官:认证失败了怎么办?比如用户填错了。
我: 允许重试,但有限制:
- 每人每天最多尝试 5 次;
- 超过 5 次就锁住 24 小时,防止暴力试错;
- 每次失败都提示“信息不匹配”,但不告诉具体错在哪(比如不能说“身份证号错了”),防止被用来撞库。
用户如果一直过不了,可以走“人工审核”通道:
- 上传身份证正反面照片 + 手持身份证照片;
- 客服人工审核,一般1小时内处理。
🎤 面试官:你们用的是哪家的认证服务?
我: 我们用的是阿里云的“三要素验证”和“人脸核身”服务,主要是因为:
- 接口稳定,延迟低(平均200ms内返回);
- 支持按量付费,不用买年包;
- 和我们技术栈都是阿里的(比如RocketMQ、Nacos),集成方便;
- 出过问题能直接找技术支持,响应快。
当然,我们也接入了腾讯云作为备用,万一阿里云接口挂了,可以切过去,避免业务中断。
🎤 面试官:整个流程是同步还是异步?
我: 同步为主,异步兜底。
- 用户提交认证时,我们是同步调第三方接口,等结果回来再告诉用户“成功” or “失败”;
- 因为这个操作不频繁,用户也愿意等几秒;
- 但如果第三方接口超时 or 异常,我们会把请求记到一张“待补验”表里,用定时任务(XXL-JOB)隔5分钟重试一次,确保不丢数据。
🎤 面试官:你说用到了人脸活体检测,那你们是怎么集成的?自己开发的吗?
我: 肯定不是自己开发啊!
人脸识别 + 活体检测这种事,别说我们公司了,就是大厂一般也不自己搞。 为啥?
- 算法太专业,训练模型成本极高;
- 准确率拼不过公安系统、支付宝那种级别;
- 还得持续维护,对抗各种“照片、视频、3D面具”攻击。
所以我们用的是第三方云服务,比如阿里云的人脸核身(实人认证),他们背后有蚂蚁金服的技术,准确率高,防作弊能力强。
🎤 面试官:那具体是怎么用的?用户操作流程是啥?
我: 我们不是每个用户都让人脸验证,只在高风险场景才弹,比如:
- 新用户要领大额优惠券(比如“满500减100”);
- 账号异地登录 or 异常操作;
- 提现、解绑银行卡、修改密码等敏感操作。
流程是这样的:
- 用户点击“领取神券” or “提现”;
- 系统判断需要人脸验证,前端弹个弹窗:“请完成人脸核验”;
- 用户点击“开始验证”,调用我们后端接口;
- 后端去阿里云申请一个“活体检测任务”,拿到一个唯一会话ID(SessionId);
- 把这个 SessionId 返回给前端;
- 前端用阿里云的H5 SDK或App SDK,引导用户:
- 拍一张人脸照片;
- 或做“眨眼、张嘴、摇头”等动作(动作活体);
- SDK 自动把视频 or 照片传给阿里云;
- 阿里云返回结果:“通过” or “失败(是照片/录像/非真人)”;
- 我们后端收到结果,标记用户“已通过活体检测”,然后放行领券 or 提现。
整个过程大概 5~10 秒,用户觉得“有点麻烦”,但能接受。
🎤 面试官:怎么防止用户用照片 or 视频骗过检测?
我: 这就是“活体检测”的核心能力了,阿里云这种大厂做得挺强的。
他们用的是动作活体 + 质感分析 + 3D Depth 等多种技术:
- 动作活体:让你眨眼、张嘴、左右摇头,静态照片过不了;
- 质感分析:照片 or 手机屏幕有反光、摩尔纹,算法能识别;
- 3D Depth(深度摄像头):如果手机支持,会判断是不是平面图像;
- AI对抗模型:他们每天都在训练模型对抗最新的“AI换脸、Deepfake”攻击。
我们也测试过:
- 用打印的照片 —— 失败;
- 用手机播放视频 —— 失败;
- 用双屏对倒(A手机放B手机摄像头前)—— 也失败。
只有真人现场操作才能过。
🎤 面试官:你们后端做了啥?就只是转发请求吗?
我: 不是,后端做了不少事,主要是安全 + 防重放 + 状态管理。
具体流程:
生成唯一会话: 用户触发验证时,我们生成一个
session_id,存到 Redis:json{ "session_id": "abc123", "user_id": 1001, "biz_type": "coupon", "status": "created", "expire": 300 // 5分钟过期 }调阿里云接口获取认证 URL: 用阿里云 SDK 发起“初始化认证”请求,拿到一个带
session_id的认证链接 or 参数;前端完成认证后,阿里云回调我们: 阿里云会通过 HTTPS 回调通知(Callback)把结果推给我们:
json{ "session_id": "abc123", "result": "pass", "score": 95.6, "timestamp": "2025-04-05 10:20:30" }后端验证并处理:
- 查 Redis 看
session_id是否有效;- 验签(阿里云会带签名,防止伪造回调);
- 更新用户状态:“已通过活体检测”;
- 触发后续操作(发券、放行提现等)。
防重放攻击: 同一个
session_id只能成功一次,处理完就删掉,防止别人拿着回调数据重复请求。
🎤 面试官:如果阿里云接口挂了怎么办?
我: 我们做了降级方案:
短时失败:加了重试机制,最多重试 3 次;
长时间不可用:
- 弹提示:“人脸服务升级中,请稍后再试”;
- 对于非核心场景(比如领小券),直接放行 or 改为“短信验证码”;
- 同时发告警,运维去查是不是网络 or 配置问题;
备用服务商: 我们也接入了腾讯云的人脸核身,虽然没阿里云准,但能应急。
🎤 面试官:用户隐私怎么保护?你们能看到人脸照片吗?
我: 我们看不到,也不敢存。
- 阿里云只返回“通过/不通过”和一个分数,不会把用户照片 or 视频给我们;
- 我们后端也不记录人脸数据,只记录“是否通过”和时间;
- 所有调用都走 HTTPS,接口有 AK/SK 鉴权;
- 审计日志里只记录
session_id和结果,不存敏感信息。真正的人脸数据,只在阿里云那边保留一段时间(一般7天),用于对抗攻击模型训练,但他们也是加密存储,受国家监管。
🎤 面试官:优惠券怎么处理过期?是定时任务扫库吗?
我: 我们一开始是用定时任务每天扫一遍,后来发现不行—— 大促销时几百万张券,一到1点就全查出来,数据库直接被打满,还容易漏掉。
后来我们改了方案,核心思路是:让过期变得“可预测 + 自动触发”。
现在用的是 “延迟消息 + 状态校验”双保险。
✅ 方案一:发券时就发个“过期消息”(推荐)
就像之前说的“超时订单”,我们也用了 MQ 的延迟队列。
🔹 流程是这样的:
用户领券 or 系统发券时:
记录有效期,比如“30天后过期”;
同时发一条延迟消息到 MQ:
javamqTemplate.sendInDelay("coupon_expire_topic", JSON.toJSONString(new CouponExpireEvent(couponId)), 30分钟级别 or 自定义时间);
到时间后,消息自动送达消费者:
java@RocketMQListener("coupon_expire_topic") public void onMessage(String msg) { CouponExpireEvent event = parse(msg); // 先查券状态,防止重复处理 Coupon coupon = couponMapper.selectById(event.getCouponId()); if (coupon.getStatus() == USED) { return; // 已使用,跳过 } if (coupon.getExpireTime().before(new Date())) { coupon.setStatus(EXPIRED); couponMapper.update(coupon); // 可以发个通知:“您的优惠券已过期” } }
这样一来,每张券到点自动过期,不用每天扫库。
✅ 方案二:定时任务兜底(防 MQ 挂了)
虽然 MQ 很稳,但我们还是加了个每日对账任务,防止极端情况。
// 每天凌晨1点跑一次
@XxlJob("coupon-expire-check")
public void checkExpiredCoupons() {
List<Coupon> expiredList = couponMapper.findExpiredNotExpired();
for (Coupon coupon : expiredList) {
coupon.setStatus(EXPIRED);
couponMapper.update(coupon);
}
}这个任务只查“该过期但没过期”的券,数据量小,压力低,是最后一道保险。
🎤 面试官:那用户在快过期时用了券,结果延迟消息又来把券过期了,怎么办?
我: 这就是经典问题:状态冲突!
我们的做法是:所有过期操作都加“前置校验”。
在处理延迟消息 or 定时任务时,都先查一遍:
sqlSELECT * FROM coupon WHERE id = ? AND status = 'UNUSED' AND expire_time < NOW()只有“未使用 + 已过期”的券才更新,否则跳过。
而且更新时用:
UPDATE coupon SET status = 'EXPIRED' WHERE id = ? AND status = 'UNUSED' AND expire_time < ?加了条件更新,避免覆盖已使用状态。
一句话:不盲目过期,先看状态再动手。
✅ 过期时间怎么定?都是固定天数吗?
不是,我们支持多种策略:
| 类型 | 做法 |
|---|---|
| 领取后N天过期 | 比如“领取后7天内有效”,expire_time = 领取时间 + 7天 |
| 固定截止日 | 比如“双11大促券,11月11日23:59过期” |
| 次日达 | 比如“新人券,领取次日零点生效,3天后过期” |
| 活动周期绑定 | 比如“618专属券,6月18日23:59统一过期” |
这些都在发券时由业务决定,我们只是按
expire_time来处理。
✅ 用户体验:快过期了会提醒吗?
会! 我们加了个“临期提醒”功能。
比如:
提前 3 天、1 天、1 小时,发 App 推送 or 短信:
“您有一张50元券即将过期,今晚24点失效!”
实现方式:
- 发券时,除了“过期消息”,再发一条“提前提醒消息”;
- 用 MQ 延迟队列,比如“过期前24小时”触发;
- 消费者判断券是否还有效,有效就发提醒。
这样既能提升券核销率,又减少用户投诉。
✅ 数据库怎么设计?怎么查“可用券”?
我: 我们在
coupon表加了几个关键字段:sqlid, user_id, amount, status, valid_from(生效时间), expire_time(过期时间), -- 有效期范围 create_time, update_time查“可用券”时:
sqlSELECT * FROM coupon WHERE user_id = ? AND status = 'UNUSED' AND NOW() BETWEEN valid_from AND expire_time ORDER BY expire_time ASC这样就能拿到用户当前能用的所有券。
面试官:能具体说说你们是怎么设计一个“发优惠券活动”的?比如“新人领券” or “大促发神券”?
我: 这种活动我们搞过几十个,从简单到复杂都有。
核心目标就四个:
- 不能超发(券就1万张,不能发出去1.5万)
- 不能被刷(用户不能无限领)
- 用户体验好(点一下就到账,别卡)
- 运营能控制(能开能关、能看数据)
下面我从“活动配置 → 用户领取 → 防刷控制 → 发券逻辑”一步步说。
✅ 一、先让运营配个活动(后台管理)
我们有个运营后台,可以创建发券活动,填这些信息:
| 字段 | 说明 |
|---|---|
| 活动名称 | 比如“618新人专享” |
| 券模板ID | 关联一张预设好的优惠券(金额、门槛、有效期) |
| 发放总量 | 比如10000张,发完为止 |
| 每人限领 | 比如“每人限领1张” or “每天可领1张” |
| 领取条件 | 比如“仅限新用户”、“需登录”、“绑定手机号” |
| 生效时间 | 活动什么时候开始 |
| 结束时间 | 活动什么时候结束 |
| 活动状态 | 开启 / 关闭(可随时关停) |
这些信息存到数据库,比如
marketing_activity表。
🎤 面试官:你们的优惠券模板表是怎么设计的?都包含哪些字段?
我: 这张表叫
coupon_template,是发券活动的“源头”。就像做菜要有“菜谱”,发券也得先有个“模板”:
- 什么类型的券?
- 减多少钱?
- 满多少可用?
- 有效期多久?
所有这些规则都存在这张表里,然后“活动”去引用它。
✅ 一、核心字段设计
CREATE TABLE `coupon_template` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(64) NOT NULL COMMENT '模板名称,比如“满200减30神券”',
-- 【1. 券的类型】
`type` TINYINT NOT NULL COMMENT '1:满减券 2:折扣券 3:代金券 4:免运费券',
-- 【2. 面值规则】
`amount` DECIMAL(10,2) COMMENT '面额,如30.00(满减/代金)',
`discount` DECIMAL(3,2) COMMENT '折扣率,如0.9(9折券)',
`limit_amount` DECIMAL(10,2) NOT NULL COMMENT '使用门槛,如满200可用',
-- 【3. 有效期策略】
`validity_type` TINYINT NOT NULL COMMENT '1:固定周期 2:领取后N天',
`valid_from` DATETIME COMMENT '固定周期时的生效时间',
`valid_to` DATETIME COMMENT '固定周期时的过期时间',
`duration_days` INT COMMENT '领取后多少天生效(如0=立即,1=次日)',
`expire_days` INT COMMENT '领取后多少天过期,如7、30',
-- 【4. 使用范围】
`scope` TINYINT NOT NULL COMMENT '1:全平台 2:品类 3:商品',
`scope_value` VARCHAR(512) COMMENT '范围值,如品类ID列表或商品ID列表,JSON格式',
-- 【5. 发放控制】
`total_count` INT NOT NULL DEFAULT 0 COMMENT '该模板总共可发放多少张',
`issued_count` INT NOT NULL DEFAULT 0 COMMENT '已发放数量(冗余,防查慢)',
-- 【6. 状态控制】
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1:启用 0:禁用',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '优惠券模板表';✅ 二、每个字段是干啥的?
| 字段 | 说明 |
|---|---|
type | 是“满200减30”?还是“打8折”?还是“免运费”? |
amount / discount | 满减券填 amount=30,折扣券填 discount=0.8 |
limit_amount | 必须“满200才能用”,防止1块钱用30元券 |
validity_type | 决定怎么算有效期: • 固定时间(双11券) • 领取后N天(新人券) |
valid_from/to | 比如“2024-11-11 00:00 ~ 23:59” |
expire_days | 比如“领取后7天内有效” |
scope + scope_value | 控制使用范围: • 全平台:不限 • 品类:填 [101,102]• 商品:填 [2001,2002] |
total_count | 这个模板最多发多少张?比如1万张,发完就停 |
issued_count | 已发了多少张?冗余字段,避免每次去 user_coupon 表 count |
✅ 三、举个真实例子
假设我们要发一张:
“新人专享:满100减20,领取后7天有效,限平台自营商品可用,总共1万张”
那模板数据就是:
| 字段 | 值 |
|---|---|
name | 新人满100减20券 |
type | 1(满减券) |
amount | 20.00 |
limit_amount | 100.00 |
validity_type | 2(领取后N天) |
expire_days | 7 |
scope | 2(品类) |
scope_value | [10, 20](假设10=家电,20=数码) |
total_count | 10000 |
✅ 四、为啥要这样设计?考虑了哪些坑?
🔹 1. issued_count 为啥要冗余?
不能每次去
user_coupon表count(*),大促时慢得要死。 我们在发券成功后,用UPDATE coupon_template SET issued_count = issued_count + 1更新,快!
🔹 2. scope_value 为啥用 JSON?
因为可能是多个品类 or 多个商品,用逗号分隔不灵活,JSON 可读性强,也方便程序解析。
🔹 3. 有效期为啥分两种?
- 固定周期:大促统一时间;
- 领取后N天:新人券、任务奖励券; 不分开就没法灵活配置。
🔹 4. status 字段很重要
运营发现券发错了,一点“禁用”,新用户就领不了了(老用户已领的还能用)。
✅ 五、和其他表的关系
coupon_template (模板)
↓
marketing_activity (活动引用模板)
↓
user_coupon (用户持有的券,记录具体生效/过期时间)
marketing_activity表有个template_id字段,指向模板;- 用户领券时,从模板读规则,生成一条
user_coupon记录,填充valid_from,expire_time等。
✅ 二、用户点“领取”时,怎么控制不超发?
这是最关键的! 大促时几千人同时点,如果没控制好,库存就超了。
我们用了 “Redis + 数据库” 双重校验:
🔹 1. 用 Redis 做“高并发计数器”
String key = "activity:1001:issued_count"; // 活动ID=1001
Long current = redisTemplate.incr(key);
if (current > 10000) { // 总量1万张
// 超了,回滚
redisTemplate.decr(key);
return "已领完";
}Redis 快,能扛住瞬间高并发。
🔹 2. 再用数据库“最终扣减”(防 Redis 假阳性)
真正发券时,走数据库插入:
INSERT INTO user_coupon (user_id, coupon_id, status)
SELECT #{userId}, #{couponId}, 'UNUSED'
FROM DUAL
WHERE (
SELECT COUNT(*) FROM user_coupon
WHERE coupon_id = #{couponId} AND user_id = #{userId}
) < 1 -- 防止重复领
AND (
SELECT COUNT(*) FROM issued_record
WHERE activity_id = 1001
) < 10000; -- 总量没超如果插入成功,才算真正领到;失败就提示“已领完”。
这叫 “Redis 快速拦截 + DB 最终一致性”。
✅ 三、怎么防止用户刷券?
黑产真会刷!我们遇到过:
- 脚本自动领券;
- 一人注册几十个号;
- 抓包重复请求;
我们加了多层防护:
🔹 1. 身份限制
- 新用户?查
is_new = 1; - 要绑定手机?先去用户中心验证;
- 要实名认证?调风控接口判断。
🔹 2. 频率限制
- 同一 IP 每天最多领 3 次;
- 同一设备 ID 每周最多领 2 次;
- 用 Redis 记:
rate_limit:ip:123.45.67.89,超了就拦截。
🔹 3. 行为校验
- 加个滑块验证码(极验 or 腾讯防水墙);
- 或者点“领取”前要先看3秒广告(防脚本)。
🔹 4. 黑名单机制
- 风控系统标记的“作弊账号”,直接不让领;
- 发现异常批量领取,人工介入封号。
✅ 四、发券是同步还是异步?
我们是“同步返回结果,异步发券”。
为什么?
因为发券可能要:
- 写数据库
- 发 MQ(比如通知用户中心、积分系统)
- 调外部服务(如 CRM)
这些都慢,不能卡在接口里。
做法:
用户点领取 → 先做各种校验(限领、库存、身份);
校验通过后,
发个 MQ 消息:
json{ "activityId": 1001, "userId": 2001 }消费者异步处理发券逻辑;
接口立刻返回:“领取成功,优惠券已到账”(即使还没真发)
用户感知不到延迟,体验好。 真出问题(比如 MQ 消费失败),我们有告警 + 补偿任务。
✅ 五、活动数据怎么监控?
运营最关心:“发了多少?剩多少?谁在领?”
我们做了个实时看板:
- 已发放数量(从 DB 统计)
- 剩余库存(总量 - 已发)
- 领取趋势图(每小时多少人领)
- 用户画像(新/老用户比例、地域分布)
- 异常告警(短时间大量领取)
数据来自:
- MySQL 聚合查询
- Kafka 日志消费
- Redis 实时计数
✅ 六、如果活动要提前结束 or 紧急关闭?
我们在后台加了个“紧急关停”按钮:
- 一点“关闭”,活动状态变“已结束”;
- 前端接口判断状态,不再允许领取;
- 已进入队列的 MQ 消息,消费者会检查活动状态,跳过处理。
保证“秒级关停”,防止继续发券。