Authored by tanling

优惠券

Showing 22 changed files with 354 additions and 37 deletions
package com.yohoufo.common.cache;
import com.yoho.core.redis.cluster.operations.nosync.YHValueOperations;
import com.yoho.core.redis.cluster.operations.serializer.RedisKeyBuilder;
import java.util.Collection;
... ... @@ -11,6 +12,8 @@ import java.util.Map;
*/
public interface CacheClient {
public boolean increaseMax(RedisKeyBuilder key, RedisGwCacheClient.CounterCallBack counterCallBack, long expireInSeconds, long maxValue);
public void mset(Map<RedisKeyBuilder, ? extends Object> map, long timeout);
public <T> Map<String, T> mget(List<RedisKeyBuilder> keys, Class<T> clazz);
... ...
... ... @@ -6,6 +6,7 @@ import java.util.stream.Collectors;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
... ... @@ -31,7 +32,6 @@ public class RedisGwCacheClient implements CacheClient {
private YHHashOperations hashOperations;
public <T> Map<String, T> mget(List<RedisKeyBuilder> keys, Class<T> clazz) {
long beginTime = System.currentTimeMillis();
try {
... ... @@ -312,4 +312,45 @@ public class RedisGwCacheClient implements CacheClient {
}
return t;
}
/**
* 增加到最大值之后返回错误,其他的返回正确且增加1
*
* @param key
* @param counterCallBack
* @param expireInSeconds
* @param maxValue
* @return
*/
public boolean increaseMax(RedisKeyBuilder key, CounterCallBack counterCallBack, long expireInSeconds, long maxValue) {
String s = valueOperations.get(key);
if (StringUtils.isBlank(s)) {
// 回调 计数器使用者, 初始化计数器
int initialize = counterCallBack.initialize();
String initialValue = String.valueOf(counterCallBack.initialize());
// 如果已经被初始化过, 则不用再初始化, 因为其他线程可能已经进行了 increase, 所以必须使用 setIfAbsent
valueOperations.setIfAbsent(key, initialValue);
// 设置有效期, redis所有的key, 必须设置有效期
redis.expire(key, expireInSeconds, TimeUnit.SECONDS);
//初始化值已经到最大值
if (initialize >= maxValue) {
return false;
}
} else {
long num = NumberUtils.toLong(s);
if (num >= maxValue) {
return false;
}
}
Long increment = valueOperations.increment(key, 1L);
return increment > maxValue ? false : true;
}
public interface CounterCallBack {
int initialize();
}
}
... ...
... ... @@ -68,7 +68,7 @@ public class DateUtil {
if (time == null) {
return "";
}
SimpleDateFormat sdf=new SimpleDateFormat(yyyy_MM_dd);
SimpleDateFormat sdf=new SimpleDateFormat(YYYY_MM_DD_DOT);
Calendar c=Calendar.getInstance();
c.setTimeInMillis((time.longValue()*1000));
return sdf.format(c.getTime());
... ...
... ... @@ -15,6 +15,8 @@ public interface CouponMapper {
Coupon selectByPrimaryKey(Integer id);
int updateCouponSendNum(@Param("couponToken") String couponToken, @Param("sendNum") int sendNum);
Coupon selectByCouponToken(@Param("couponToken") String couponToken);
List<Coupon> selectByCouponIds(@Param("ids") List<Integer> id);
... ...
package com.yohoufo.dal.promotion;
import com.yohoufo.dal.promotion.model.Coupon;
import com.yohoufo.dal.promotion.model.CouponType;
import org.apache.ibatis.annotations.Param;
... ...
... ... @@ -11,7 +11,7 @@ public class UserCoupon {
private String couponCode;
private Byte status;
private Integer status;
private Long orderCode;
... ... @@ -73,11 +73,11 @@ public class UserCoupon {
this.couponCode = couponCode == null ? null : couponCode.trim();
}
public Byte getStatus() {
public Integer getStatus() {
return status;
}
public void setStatus(Byte status) {
public void setStatus(Integer status) {
this.status = status;
}
... ...
... ... @@ -41,6 +41,10 @@
where coupon_token = #{couponToken,jdbcType=VARCHAR}
</select>
<update id="updateCouponSendNum" >
update coupon set send_num=#{sendNum} where coupon_token=#{couponToken}
</update>
<select id="selectByCouponIds" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
... ... @@ -61,7 +65,7 @@
AND coupon_token IN
<foreach item="token" index="index" collection="list"
open="(" separator="," close=")">
#{id}
#{token}
</foreach>
</select>
... ...
... ... @@ -8,7 +8,7 @@
<result column="coupon_token" property="couponToken" jdbcType="VARCHAR" />
<result column="coupon_type" property="couponType" jdbcType="INTEGER" />
<result column="coupon_code" property="couponCode" jdbcType="VARCHAR" />
<result column="status" property="status" jdbcType="TINYINT" />
<result column="status" property="status" jdbcType="INTEGER" />
<result column="order_code" property="orderCode" jdbcType="BIGINT" />
<result column="use_time" property="useTime" jdbcType="INTEGER" />
<result column="start_time" property="startTime" jdbcType="INTEGER" />
... ... @@ -49,8 +49,8 @@
<sql id="CouponsLogsQueryUsable" >
<!-- 在有效时间范围内 -->
and <![CDATA[ start_time > #{now, jdbcType=INTEGER} ]]>
and <![CDATA[ end_time < #{now, jdbcType=INTEGER} ]]>
and <![CDATA[ start_time < #{now, jdbcType=INTEGER} ]]>
and <![CDATA[ end_time > #{now, jdbcType=INTEGER} ]]>
<!-- 未使用-->
and status = 0
</sql>
... ... @@ -71,11 +71,11 @@
insert into user_coupon (id, uid, coupon_id,
coupon_type, coupon_code, status,
order_code, use_time, start_time,
end_time, create_time)
end_time, create_time,coupon_token)
values (#{id,jdbcType=INTEGER}, #{uid,jdbcType=INTEGER}, #{couponId,jdbcType=INTEGER},
#{couponType,jdbcType=INTEGER}, #{couponCode,jdbcType=VARCHAR}, #{status,jdbcType=TINYINT},
#{couponType,jdbcType=INTEGER}, #{couponCode,jdbcType=VARCHAR}, #{status,jdbcType=INTEGER},
#{orderCode,jdbcType=BIGINT}, #{useTime,jdbcType=INTEGER}, #{startTime,jdbcType=INTEGER},
#{endTime,jdbcType=INTEGER}, #{createTime,jdbcType=INTEGER})
#{endTime,jdbcType=INTEGER}, #{createTime,jdbcType=INTEGER}, #{couponToken,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="com.yohoufo.dal.promotion.model.UserCoupon" >
insert into user_coupon
... ...
... ... @@ -13,4 +13,10 @@ public interface ExpiredTime {
* coupons_type ---缓存时间
*/
int COUPON_TYPE_CACHE_TIME = 3600;
/**
* 券发放数量缓存
*/
int COUPON_SEND_NUM_CACHE_TIME = 7 * 24 * 3600;
}
... ...
... ... @@ -20,4 +20,11 @@ public class KeyBuilder {
return RedisKeyBuilder.newInstance().appendFixed("ufo:promotion:coupons:serverNode");
}
/**
* 券领用数量统计
*/
public static RedisKeyBuilder buildCouponSendNum(String couponToken) {
return RedisKeyBuilder.newInstance().appendFixed("ufo:promotion:coupons:couponToken:").appendVar(couponToken).appendFixed(":acquireNum");
}
}
... ...
package com.yohoufo.promotion.common;
public interface Constant {
/**
*同步券数量从Redis到DB,执行频率 300*1000ms=300S
*/
int SYNC_COUPON_SEND_NUM_REDIS_2_DB = 300000;
}
... ...
package com.yohoufo.promotion.common;
/**
* 描述:券状态
* Created by pangjie@yoho.cn on 2016/5/24.
*/
public enum CouponsStatusEnum {
//ERP中老的状态为: 0:未启用 1:启用 3:作废
... ...
package com.yohoufo.promotion.common;
public enum UserCouponsStatusEnum {
NO_USE(0, "未使用"),
USED(1, "已使用");
/**
* 编码
*/
private int code;
/**
* 描述
*/
private String desc;
/**
* 操作列表
*
* @param code
* @param desc
* @return
*/
UserCouponsStatusEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
... ...
... ... @@ -44,7 +44,7 @@ public class CouponController {
* @param uid
* @return
*/
@RequestMapping(params = "method=app.coupons.count")
@RequestMapping(params = "method=app.coupons.cnt")
@ResponseBody
public ApiResponse queryCouponCnt(@RequestParam(value = "uid") Integer uid) {
... ... @@ -65,9 +65,9 @@ public class CouponController {
public ApiResponse sendCoupon(@RequestParam(value = "uid") Integer uid,
@RequestParam(value = "coupon_token") String couponToken) {
logger.info("send coupons enter, uid: {}", uid);
logger.info("send coupons enter, uid: {}, couponToken:{}", uid, couponToken);
String couponCode = couponService.senCoupon(uid, couponToken);
logger.info("send coupons success uid: {}, couponCode: {}", uid, couponCode);
logger.info("send coupons success uid: {}, couponToken:{},couponCode: {}", uid,couponToken,couponCode);
return new ApiResponse.ApiResponseBuilder().code(200).data(couponCode).build();
}
}
... ...
... ... @@ -7,6 +7,7 @@ import com.yohoufo.dal.promotion.model.CouponType;
import com.yohoufo.dal.promotion.model.UserCoupon;
import com.yohoufo.promotion.common.ProductLimitType;
import com.yohoufo.promotion.common.UseLimitType;
import com.yohoufo.promotion.common.UserCouponsStatusEnum;
import com.yohoufo.promotion.model.response.CouponInfo;
import java.text.MessageFormat;
... ... @@ -80,6 +81,7 @@ public class CouponConvert {
// TODO
userCoupon.setEndTime(couponAndType.getCoupon().getEndTime());
userCoupon.setCreateTime(now);
userCoupon.setStatus(UserCouponsStatusEnum.NO_USE.getCode());
userCoupon.setCouponToken(couponAndType.getCoupon().getCouponToken());
return userCoupon;
}
... ...
... ... @@ -20,4 +20,12 @@ public interface ICouponCacheService {
* @return
*/
public CouponAndType getCouponAndType(String couponToken);
/**
* 获取coupon
* @param couponToken
* @return
*/
public Coupon getCouponWithCache(String couponToken);
}
... ...
package com.yohoufo.promotion.service;
import com.yohoufo.dal.promotion.model.Coupon;
public interface SingleCentCouponService {
public boolean syncCoupSendNum(String couponToken);
public void checkAndAddSendCouponNum(int uid, Coupon coupon);
}
... ...
package com.yohoufo.promotion.service;
import com.google.common.collect.Sets;
import com.yohoufo.promotion.common.Constant;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class SyncCouponSendNumService {
private static final Logger logger = LoggerFactory.getLogger(SyncCouponSendNumService.class);
/**
* 是否正在运行,防止任务重复执行
*/
private static volatile boolean IS_RUNNING = false;
/**
* 优惠券ID set集合
*/
private static Set<String> COUPON_TOKEN_SET = Sets.newConcurrentHashSet();
@Autowired
private SingleCentCouponService singleCentSyncCoupNumService;
public static void addCouponToken(String couponToken) {
COUPON_TOKEN_SET.add(couponToken);
}
/**
* 前一次任务执行完5分钟后再执行,且任务中又随机睡10分钟,将redis中的领用数量同步到DB中持久化
*/
@Scheduled(fixedDelay = Constant.SYNC_COUPON_SEND_NUM_REDIS_2_DB)
public synchronized void syncCouponSendNumRedis2DB() {
if (IS_RUNNING) {
logger.info("sync send num to db task is running");
return;
}
try {
IS_RUNNING = true;
int sleepSeconds = new Random().nextInt(600);
logger.info("sync coupon send num just sleep:{}s", sleepSeconds);
TimeUnit.SECONDS.sleep(sleepSeconds);
if (CollectionUtils.isEmpty(COUPON_TOKEN_SET)) {
logger.info("no coupons send num need sync");
}
Iterator<String> iterator = COUPON_TOKEN_SET.iterator();
for (; iterator.hasNext(); ) {
String couponToken = iterator.next();
try {
if (singleCentSyncCoupNumService.syncCoupSendNum(couponToken)) {
iterator.remove();
}
} catch (Exception e) {
logger.error("sync coupon send num error:{},{}", couponToken, e.getMessage());
}
}
} catch (Exception e) {
logger.error("sync send coupon num error:{}", e.getMessage());
} finally {
IS_RUNNING = false;
}
}
}
... ...
package com.yohoufo.promotion.service.impl;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.yoho.core.redis.cluster.operations.serializer.RedisKeyBuilder;
import com.yoho.error.ServiceError;
import com.yoho.error.exception.ServiceException;
import com.yohoufo.common.cache.CacheClient;
import com.yohoufo.common.utils.DateUtil;
import com.yohoufo.dal.promotion.CouponMapper;
import com.yohoufo.dal.promotion.CouponTypeMapper;
import com.yohoufo.dal.promotion.UserCouponMapper;
import com.yohoufo.dal.promotion.model.Coupon;
import com.yohoufo.dal.promotion.model.CouponAndType;
import com.yohoufo.dal.promotion.model.CouponType;
import com.yohoufo.dal.promotion.model.UserCoupon;
import com.yohoufo.promotion.cache.ExpiredTime;
import com.yohoufo.promotion.cache.KeyBuilder;
import com.yohoufo.promotion.common.CouponsStatusEnum;
import com.yohoufo.promotion.common.ProductLimitType;
import com.yohoufo.promotion.common.UseLimitType;
import com.yohoufo.promotion.convert.CouponConvert;
import com.yohoufo.promotion.model.response.CouponInfo;
import com.yohoufo.promotion.model.response.CouponInfoListBO;
import com.yohoufo.promotion.service.CouponCodeGenerate;
import com.yohoufo.promotion.service.ICouponCacheService;
import com.yohoufo.promotion.service.ICouponService;
import com.yohoufo.promotion.service.*;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
... ... @@ -53,6 +38,12 @@ public class CouponServiceImpl implements ICouponService {
@Autowired
CouponCodeGenerate couponCodeGenerate;
@Autowired
CouponMapper couponMapper;
@Autowired
SingleCentCouponService singleCentSyncCoupNumService;
private final Logger logger = LoggerFactory.getLogger(getClass());
... ... @@ -76,6 +67,7 @@ public class CouponServiceImpl implements ICouponService {
checkCanAcquire(uid, couponToken, couponAndType);
// 校验 优惠券发放总数
checkAndAddCouponSendNum(uid, couponToken);
String couponCode = couponAndType.getCouponType().getAlphabet() + couponCodeGenerate.getCode();
... ... @@ -97,6 +89,20 @@ public class CouponServiceImpl implements ICouponService {
}
/**
* 优惠券数量校验
* @param uid
* @param couponToken
*/
public void checkAndAddCouponSendNum(int uid, String couponToken){
Coupon coupon = couponCacheService.getCouponWithCache(couponToken);
singleCentSyncCoupNumService.checkAndAddSendCouponNum(uid, coupon);
SyncCouponSendNumService.addCouponToken(couponToken);
}
private void checkCanAcquire(Integer uid, String couponToken, CouponAndType couponAndType) {
// 验证该用户是否重复领取
... ... @@ -113,7 +119,7 @@ public class CouponServiceImpl implements ICouponService {
throw new ServiceException(ServiceError.PROMOTION_COUPON_NOT_ARRIVE_GET_START_TIME);
}
if (couponAndType.getCoupon().getEndTime() > now){
if (couponAndType.getCoupon().getEndTime() < now){
logger.info("couponrulebo is expired:{},{}", uid, couponToken);
throw new ServiceException(ServiceError.PROMOTION_COUPON_TIME_OUT_CAN_NOT_GET);
}
... ...
package com.yohoufo.promotion.service.impl;
import com.google.common.collect.Sets;
import com.yoho.core.redis.cluster.operations.serializer.RedisKeyBuilder;
import com.yoho.error.ServiceError;
import com.yoho.error.exception.ServiceException;
import com.yohoufo.common.cache.CacheClient;
import com.yohoufo.dal.promotion.CouponMapper;
import com.yohoufo.dal.promotion.model.Coupon;
import com.yohoufo.promotion.cache.ExpiredTime;
import com.yohoufo.promotion.cache.KeyBuilder;
import com.yohoufo.promotion.service.SingleCentCouponService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class SingleCentCouponServiceImpl implements SingleCentCouponService {
private static final Logger logger = LoggerFactory.getLogger(SingleCentCouponServiceImpl.class);
@Autowired
CouponMapper couponMapper;
@Autowired
CacheClient cacheClient;
/**
* 正在同步发券数量的优惠券ID
*/
private Set<String> syncCouponTokens = Sets.newConcurrentHashSet();
public boolean syncCoupSendNum(String couponToken) {
try {
RedisKeyBuilder redisKey = KeyBuilder.buildCouponSendNum(couponToken);
Integer sendNum = cacheClient.get(redisKey, Integer.class);
if (sendNum == null) {
logger.info("cache not exist coupon send num:{}", couponToken);
return true;
}
//包含表示该节点正在同步发券数量
if (syncCouponTokens.contains(couponToken)) {
return true;
}
syncCouponTokens.add(couponToken);
couponMapper.updateCouponSendNum(couponToken, sendNum);
return true;
} finally {
syncCouponTokens.remove(couponToken);
}
}
public void checkAndAddSendCouponNum(int uid, Coupon coupon){
boolean succ = false;
try {
succ = cacheClient.increaseMax(KeyBuilder.buildCouponSendNum(coupon.getCouponToken()),
() -> couponMapper.selectByCouponToken(coupon.getCouponToken()).getSendNum(),
ExpiredTime.COUPON_SEND_NUM_CACHE_TIME,
coupon.getCouponNum());
} catch (Exception e) {
logger.error("incr send coupon num error:{},{},{}",uid, coupon.getCouponToken(), e.getMessage());
throw new ServiceException(ServiceError.PROMOTION_COUPON_SEND_FAIL);
}
if (!succ) {
logger.warn("coupon has arrive max num,no coupon can acquire:{},{}", uid, coupon.getCouponToken());
throw new ServiceException(ServiceError.PROMOTION_COUPON_HAS_NO_VAILD);
}
}
}
... ...
... ... @@ -50,6 +50,18 @@ datasources:
- com.yohoufo.dal.order.OrdersPayTransferMapper
- com.yohoufo.dal.order.ManualTransferMapper
ufo_promotion:
servers:
- 192.168.102.219:3306
- 192.168.102.219:3306
username: yh_test
password: 9nm0icOwt6bMHjMusIfMLw==
daos:
- com.yohoufo.dal.promotion.CouponMapper
- com.yohoufo.dal.promotion.CouponProductLimitMapper
- com.yohoufo.dal.promotion.CouponTypeMapper
- com.yohoufo.dal.promotion.UserCouponMapper
ufo_resource:
servers:
- 192.168.102.219:3306
... ...
... ... @@ -50,6 +50,19 @@ datasources:
- com.yohoufo.dal.order.OrdersPayTransferMapper
- com.yohoufo.dal.order.ManualTransferMapper
ufo_promotion:
servers:
- ${jdbc.mysql.ufo.master}
- ${jdbc.mysql.ufo.master}
username: ${jdbc.mysql.ufo.username}
password: ${jdbc.mysql.ufo.password}
daos:
- com.yohoufo.dal.promotion.CouponMapper
- com.yohoufo.dal.promotion.CouponProductLimitMapper
- com.yohoufo.dal.promotion.CouponTypeMapper
- com.yohoufo.dal.promotion.UserCouponMapper
ufo_resource:
servers:
- ${jdbc.mysql.ufo.master}
... ...