# 积分冻结与过期机制 — 未实现功能设计 > 本文档仅覆盖**尚未实现的功能**中需要用到积分冻结/过期机制的场景。 > 已实现功能(订单冻结、活动冻结、批次过期等)不在本文档范围内。 > 编写日期:2026-03-21 --- ## 一、积分转赠(待实现功能 #26) ### 1.1 冻结机制 | 环节 | 操作 | 说明 | |------|------|------| | 发起转赠 | `freezePoints(senderId, amount, transferId)` | A 发起转赠→冻结 A 的积分 | | 对方接收 | `consumeFrozenPoints(senderId, amount, transferId)` + `earnPoints(receiverId, ...)` | B 确认→扣除 A 冻结积分,发放给 B | | 对方拒绝/超时 | `unfreezePoints(senderId, amount, transferId)` | 超过24小时未接收→自动解冻退回 A | ### 1.2 过期机制 - B 收到的转赠积分创建新批次,有效期建议 **90天**(防止积分通过转赠无限续期) - 转赠积分的有效期不应继承原批次剩余有效期(否则可通过互转刷新有效期) ### 1.3 风控要点 - 单日转赠上限:每人每日最多转出 500 积分 - 手续费:转赠扣除 10% 手续费(A 转 100,B 收到 90) - 转赠积分不可再次转赠(标记 `source = 'transfer_in'`,转赠时校验批次来源) ### 1.4 数据库设计建议 ```sql CREATE TABLE points_transfers ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sender_id BIGINT NOT NULL COMMENT '转出方用户ID', receiver_id BIGINT NOT NULL COMMENT '接收方用户ID', amount INT NOT NULL COMMENT '转赠积分(扣除手续费前)', fee INT NOT NULL DEFAULT 0 COMMENT '手续费积分', actual_amount INT NOT NULL COMMENT '实际到账积分', status ENUM('pending', 'accepted', 'rejected', 'expired', 'cancelled') DEFAULT 'pending', expired_at DATETIME NOT NULL COMMENT '接收截止时间(24小时)', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_sender (sender_id), INDEX idx_receiver (receiver_id), INDEX idx_status (status) ) COMMENT='积分转赠记录表'; ``` ### 1.5 接口设计 ``` POST /api/v1/points/transfer 发起转赠(冻结积分) POST /api/v1/points/transfer/{id}/accept 接收转赠(消费冻结+发放) POST /api/v1/points/transfer/{id}/reject 拒绝转赠(解冻退回) GET /api/v1/points/transfers 我的转赠记录 ``` --- ## 二、优惠券系统(待实现功能 #16) ### 2.1 冻结机制 — 积分兑换优惠券 | 环节 | 操作 | 说明 | |------|------|------| | 兑换优惠券 | `freezePoints(userId, amount, couponOrderId)` | 用户用积分兑换优惠券→先冻结 | | 兑换确认 | `consumeFrozenPoints(userId, amount, couponOrderId)` | 系统发放优惠券成功→消费冻结积分 | | 兑换失败 | `unfreezePoints(userId, amount, couponOrderId)` | 优惠券库存不足或系统异常→解冻退回 | ### 2.2 过期机制 — 优惠券与积分联动 - 优惠券本身有独立有效期,与积分过期无直接关系 - 但如果优惠券过期未使用且支持退还积分,退还的积分建议有效期 **30天**(短周期促消费) ### 2.3 风控要点 - 同一优惠券每人限兑 1 张(防囤券) - 兑换冻结窗口 ≤ 5秒(快速确认,减少积分被长时间锁定) --- ## 三、限时折扣/秒杀活动(待实现功能 #17) ### 3.1 冻结机制 — 积分参与秒杀 | 环节 | 操作 | 说明 | |------|------|------| | 参与秒杀 | `freezeForActivity(userId, amount, activityId, title)` | 用户参与积分秒杀→冻结积分 | | 秒杀成功 | `consumeFrozenForActivity(userId, amount, activityId, title)` | 中标→消费冻结积分,发放 Skill | | 秒杀失败 | `unfreezeForActivity(userId, amount, activityId, title)` | 未中→解冻退回 | ### 3.2 过期机制 — 活动赠送积分 - 限时活动赠送的积分建议有效期 **7-15天**(制造紧迫感,推动快速消费) - 活动积分批次 `source = 'activity'`,定时过期任务统一处理 ### 3.3 关键设计 - 秒杀冻结必须原子操作(Redis + 乐观锁防超卖) - 活动结束后批量解冻所有未中标用户的冻结积分(定时任务 / 活动结束回调) --- ## 四、安全风控体系(待实现功能 #25) ### 4.1 冻结机制 — 异常行为冻结 | 场景 | 冻结操作 | 说明 | |------|---------|------| | 刷积分检测 | 冻结全部可用积分 | 检测到短时间大量签到/邀请奖励异常→冻结账户积分 | | 恶意退款检测 | 冻结退还积分 | 频繁购买-退款套利→冻结退还的积分待人工审核 | | 多账号关联 | 冻结关联账号积分 | 检测到同设备/IP多账号互相邀请→冻结所有关联账号 | ### 4.2 实现方式 ```java // 风控冻结:与业务冻结区分,使用独立的 source 标记 void freezeByRisk(Long userId, int amount, String riskType, String reason); void unfreezeByRisk(Long userId, int amount, Long auditId); // 管理员审核后解冻 ``` ### 4.3 数据库设计建议 ```sql CREATE TABLE risk_freeze_records ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, freeze_amount INT NOT NULL COMMENT '冻结积分数', risk_type VARCHAR(50) NOT NULL COMMENT '风险类型: point_abuse/refund_abuse/multi_account', reason VARCHAR(500) COMMENT '冻结原因', status ENUM('frozen', 'unfrozen', 'deducted') DEFAULT 'frozen', auditor_id BIGINT COMMENT '审核人ID', audit_note VARCHAR(500) COMMENT '审核备注', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, resolved_at DATETIME COMMENT '处理时间', INDEX idx_user (user_id), INDEX idx_status (status) ) COMMENT='风控冻结记录表'; ``` ### 4.4 风控规则示例 | 规则 | 触发条件 | 冻结策略 | |------|---------|---------| | 签到异常 | 同一设备 ≥3 个账号当天签到 | 冻结所有关联账号当天签到积分 | | 邀请刷量 | 24小时内邀请 ≥10 人且被邀请人无后续活跃 | 冻结邀请奖励积分 | | 退款套利 | 30天内退款 ≥3 次 | 冻结账户全部积分,人工审核 | --- ## 五、积分规则后台配置(待实现功能 #30) ### 5.1 过期机制 — 差异化有效期配置 当前 `DEFAULT_EXPIRE_DAYS = 365` 是硬编码。需要改为后台可配置: ```sql -- 在 points_rules 表新增字段 ALTER TABLE points_rules ADD COLUMN expire_days INT DEFAULT 365 COMMENT '积分有效期(天),0=永不过期'; ``` ### 5.2 配置示例 | 积分来源 | 建议有效期 | 配置字段 | |---------|-----------|---------| | register | 30天 | `expire_days = 30` | | sign_in | 90天 | `expire_days = 90` | | invite | 180天 | `expire_days = 180` | | recharge | 365天 | `expire_days = 365` | | activity | 7-15天 | `expire_days = 7` (按活动单独配置) | | review | 90天 | `expire_days = 90` | | admin_add | 0(永不过期) | `expire_days = 0` | | refund | 180天 | `expire_days = 180` | | transfer_in | 90天 | `expire_days = 90` | ### 5.3 代码改造要点 ```java // PointsServiceImpl.createBatch() 改为从 rules 表读取 expire_days private void createBatch(Long userId, String source, int amount, Long relatedId, String relatedType) { int expireDays = getExpireDaysBySource(source); // 从 points_rules 表查询 LocalDateTime expireAt = expireDays > 0 ? LocalDateTime.now().plusDays(expireDays) : LocalDateTime.of(2099, 12, 31, 23, 59, 59); // 0=永不过期 // ... 创建批次 } ``` --- ## 六、在线客服/工单系统(待实现功能 #22) ### 6.1 冻结机制 — 争议工单积分冻结 | 场景 | 操作 | 说明 | |------|------|------| | 用户提交争议工单 | 可选冻结争议订单关联积分 | 涉及积分纠纷的工单,客服可手动冻结相关积分 | | 工单解决 | 解冻或扣除 | 根据裁决结果解冻退回或扣除 | ### 6.2 实现方式 - 客服后台增加"冻结用户积分"操作按钮 - 记录操作日志(关联工单ID),审计可追溯 - 解冻需要高级客服或管理员权限 --- ## 七、实现优先级建议 | 优先级 | 功能 | 冻结/过期关键点 | 依赖 | |--------|------|----------------|------| | **P1** | #30 积分规则后台配置 | 差异化有效期是所有过期策略的基础 | 无 | | **P1** | #25 风控体系 | 风控冻结是防刷核心手段 | 无 | | **P2** | #26 积分转赠 | 冻结防并发+过期防续期 | 无 | | **P2** | #16 优惠券系统 | 兑换冻结+退还过期 | 优惠券模块 | | **P2** | #17 秒杀活动 | 参与冻结+活动积分短过期 | 活动模块增强 | | **P3** | #22 客服工单 | 争议冻结 | 客服系统 | --- ## 八、通用技术要点 ### 8.1 冻结与解冻的幂等性 所有冻结/解冻操作必须带业务ID(orderId/transferId/activityId),防止重复冻结: ```java // 冻结前检查是否已冻结 boolean alreadyFrozen = recordRepo.exists( new LambdaQueryWrapper() .eq(PointsRecord::getRelatedId, bizId) .eq(PointsRecord::getRelatedType, bizType) .eq(PointsRecord::getPointsType, "freeze") ); if (alreadyFrozen) return; // 幂等,跳过 ``` ### 8.2 过期批次的FIFO消费 消费积分时优先扣最早到期的批次(已实现 `consumeBatchesFIFO`),确保: - 用户总是先用"快过期"的积分 - 减少过期浪费,提升用户体验 ### 8.3 冻结积分不参与过期 被冻结的积分在冻结期间不应触发过期(过期定时任务需排除冻结状态的批次)。如果业务解冻后发现原批次已过期,应创建新批次并设合理有效期。 --- *本文档随功能开发进度持续更新*