mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-06-22 08:09:48 +08:00
update
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package com.jsowell.pile.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券模板表 coupon_template
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CouponTemplate {
|
||||
|
||||
private Long id;
|
||||
|
||||
/** 券名称 */
|
||||
private String name;
|
||||
|
||||
/** 券类型:1=洗车券 2=充电折扣券 */
|
||||
private Integer type;
|
||||
|
||||
/** 创建人类型:1=平台管理员 2=运营商管理员 */
|
||||
private Integer creatorType;
|
||||
|
||||
/** 创建人所属运营商ID,平台管理员为NULL */
|
||||
private Long creatorMerchantId;
|
||||
|
||||
/** 可用范围:1=全平台 2=指定运营商 3=指定站点 */
|
||||
private Integer scopeType;
|
||||
|
||||
/** 兑换所需积分(洗车券使用) */
|
||||
private BigDecimal pointsCost;
|
||||
|
||||
/** 折扣比例(充电折扣券,如0.85表示85折) */
|
||||
private BigDecimal discountRate;
|
||||
|
||||
/** 充电最低消费金额门槛(折扣券,v1.2预留) */
|
||||
private BigDecimal minChargeAmount;
|
||||
|
||||
/** 单次最大抵扣金额上限(折扣券,v1.2预留) */
|
||||
private BigDecimal maxDiscountAmount;
|
||||
|
||||
/** 兑换活动开始时间,NULL=不限 */
|
||||
private Date exchangeStartTime;
|
||||
|
||||
/** 兑换活动结束时间,NULL=不限 */
|
||||
private Date exchangeEndTime;
|
||||
|
||||
/** 总库存,-1=不限制 */
|
||||
private Integer stockTotal;
|
||||
|
||||
/** 剩余库存,-1=不限制 */
|
||||
private Integer stockRemain;
|
||||
|
||||
/** 有效期类型:1=固定日期 2=领取后N天 */
|
||||
private Integer validityType;
|
||||
|
||||
/** 固定有效期开始时间(validityType=1时有效) */
|
||||
private Date validStartTime;
|
||||
|
||||
/** 固定有效期结束时间(validityType=1时有效) */
|
||||
private Date validEndTime;
|
||||
|
||||
/** 领取后有效天数(validityType=2时有效) */
|
||||
private Integer validDays;
|
||||
|
||||
/** 单用户每日兑换上限,0=不限 */
|
||||
private Integer dailyLimit;
|
||||
|
||||
/** 单用户每月兑换上限,0=不限 */
|
||||
private Integer monthlyLimit;
|
||||
|
||||
/** 单用户累计兑换上限,0=不限 */
|
||||
private Integer totalLimit;
|
||||
|
||||
/** 状态:0=下架 1=上架 */
|
||||
private Integer status;
|
||||
|
||||
/** 券使用说明 */
|
||||
private String description;
|
||||
|
||||
/** 最后修改人账号 */
|
||||
private String updateBy;
|
||||
|
||||
/** 创建人账号 */
|
||||
private String createBy;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
private Date updateTime;
|
||||
|
||||
/** 删除标志:0=存在 2=删除 */
|
||||
private String delFlag;
|
||||
|
||||
// ---- 非持久化字段,查询时用 ----
|
||||
|
||||
/** scope 明细列表(scopeType=2或3时使用) */
|
||||
private List<CouponTemplateScope> scopeList;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.jsowell.pile.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 优惠券可用范围明细表 coupon_template_scope
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CouponTemplateScope {
|
||||
|
||||
private Long id;
|
||||
|
||||
/** 关联券模板ID */
|
||||
private Long templateId;
|
||||
|
||||
/** 范围类型:2=运营商 3=站点 */
|
||||
private Integer scopeType;
|
||||
|
||||
/** 运营商ID 或 站点ID */
|
||||
private Long scopeId;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.jsowell.pile.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 优惠券核销日志表 coupon_verify_record
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CouponVerifyRecord {
|
||||
|
||||
private Long id;
|
||||
|
||||
/** 券编号 */
|
||||
private String couponNo;
|
||||
|
||||
/** 核销请求幂等键 */
|
||||
private String requestId;
|
||||
|
||||
/** 核销门店ID */
|
||||
private Long storeId;
|
||||
|
||||
/** 核销操作人 */
|
||||
private String operatorId;
|
||||
|
||||
/** 核销结果:1=成功 2=失败 */
|
||||
private Integer result;
|
||||
|
||||
/** 失败原因 */
|
||||
private String failReason;
|
||||
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.jsowell.pile.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 会员优惠券表 member_coupon
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class MemberCoupon {
|
||||
|
||||
private Long id;
|
||||
|
||||
/** 券编号(唯一,内部使用) */
|
||||
private String couponNo;
|
||||
|
||||
/** 兑换幂等键(客户端requestId),唯一索引防重复兑换 */
|
||||
private String exchangeRequestId;
|
||||
|
||||
/** 关联券模板ID */
|
||||
private Long templateId;
|
||||
|
||||
/** 关联会员ID(varchar,与member_points_info.memberId一致) */
|
||||
private String memberId;
|
||||
|
||||
/** 券类型快照:1=洗车券 2=充电折扣券 */
|
||||
private Integer couponType;
|
||||
|
||||
/** 兑换时消耗的积分快照 */
|
||||
private BigDecimal pointsCost;
|
||||
|
||||
/** 折扣比例快照,洗车券为NULL */
|
||||
private BigDecimal discountRate;
|
||||
|
||||
/** 状态:0=未使用 1=已使用 2=已过期 */
|
||||
private Integer status;
|
||||
|
||||
/** 来源:1=积分兑换 */
|
||||
private Integer source;
|
||||
|
||||
/** 兑换时间 */
|
||||
private Date exchangeTime;
|
||||
|
||||
/** 过期时间 */
|
||||
private Date expireTime;
|
||||
|
||||
/** 核销时间 */
|
||||
private Date useTime;
|
||||
|
||||
/** 核销门店/站点ID */
|
||||
private Long useStoreId;
|
||||
|
||||
/** 核销操作人 */
|
||||
private String useOperator;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
/** 删除标志:0=存在 2=删除 */
|
||||
private String delFlag;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.jsowell.pile.mapper;
|
||||
|
||||
import com.jsowell.pile.domain.CouponTemplate;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券模板 Mapper 接口
|
||||
*/
|
||||
public interface CouponTemplateMapper {
|
||||
|
||||
CouponTemplate selectById(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 后台列表查询(支持按运营商过滤、状态过滤)
|
||||
*/
|
||||
List<CouponTemplate> selectList(CouponTemplate query);
|
||||
|
||||
int insert(CouponTemplate record);
|
||||
|
||||
int updateById(CouponTemplate record);
|
||||
|
||||
/**
|
||||
* 原子扣减库存:stock_remain >= 1 时才扣,返回更新行数
|
||||
*/
|
||||
int deductStock(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 检查是否存在兑换记录(用于冻结字段校验)
|
||||
*/
|
||||
int countExchangeRecord(@Param("templateId") Long templateId);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.jsowell.pile.mapper;
|
||||
|
||||
import com.jsowell.pile.domain.CouponTemplateScope;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券可用范围明细 Mapper 接口
|
||||
*/
|
||||
public interface CouponTemplateScopeMapper {
|
||||
|
||||
List<CouponTemplateScope> selectByTemplateId(@Param("templateId") Long templateId);
|
||||
|
||||
/**
|
||||
* 按 (scopeType, scopeId) 反查可用模板ID列表(走 idx_scope_lookup)
|
||||
*/
|
||||
List<Long> selectTemplateIdsByScopeId(@Param("scopeType") Integer scopeType,
|
||||
@Param("scopeId") Long scopeId);
|
||||
|
||||
int insertBatch(@Param("list") List<CouponTemplateScope> list);
|
||||
|
||||
int deleteByTemplateId(@Param("templateId") Long templateId);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.jsowell.pile.mapper;
|
||||
|
||||
import com.jsowell.pile.domain.CouponVerifyRecord;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 优惠券核销日志 Mapper 接口
|
||||
*/
|
||||
public interface CouponVerifyRecordMapper {
|
||||
|
||||
int insert(CouponVerifyRecord record);
|
||||
|
||||
/**
|
||||
* 按请求幂等键查询(防重复核销)
|
||||
*/
|
||||
CouponVerifyRecord selectByRequestId(@Param("requestId") String requestId);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.jsowell.pile.mapper;
|
||||
|
||||
import com.jsowell.pile.domain.MemberCoupon;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会员优惠券 Mapper 接口
|
||||
*/
|
||||
public interface MemberCouponMapper {
|
||||
|
||||
MemberCoupon selectByCouponNo(@Param("couponNo") String couponNo);
|
||||
|
||||
List<MemberCoupon> selectByMemberId(@Param("memberId") String memberId,
|
||||
@Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 后台兑换记录查询
|
||||
*/
|
||||
List<MemberCoupon> selectRecordList(MemberCoupon query);
|
||||
|
||||
int insert(MemberCoupon record);
|
||||
|
||||
/**
|
||||
* 原子核销:status=0 AND expire_time>now() 时才更新,返回更新行数
|
||||
*/
|
||||
int verify(@Param("couponNo") String couponNo,
|
||||
@Param("useTime") Date useTime,
|
||||
@Param("useStoreId") Long useStoreId,
|
||||
@Param("useOperator") String useOperator);
|
||||
|
||||
/**
|
||||
* 批量归档过期券(Quartz 定时任务用)
|
||||
*/
|
||||
int batchExpire(@Param("now") Date now);
|
||||
|
||||
/**
|
||||
* 统计某用户对某模板的累计兑换数(用于 total_limit 校验)
|
||||
*/
|
||||
int countByMemberAndTemplate(@Param("memberId") String memberId,
|
||||
@Param("templateId") Long templateId);
|
||||
|
||||
/**
|
||||
* 统计某用户对某模板今日兑换数(用于 daily_limit 校验)
|
||||
*/
|
||||
int countTodayByMemberAndTemplate(@Param("memberId") String memberId,
|
||||
@Param("templateId") Long templateId,
|
||||
@Param("dayStart") Date dayStart,
|
||||
@Param("dayEnd") Date dayEnd);
|
||||
|
||||
/**
|
||||
* 统计某用户对某模板本月兑换数(用于 monthly_limit 校验)
|
||||
*/
|
||||
int countMonthByMemberAndTemplate(@Param("memberId") String memberId,
|
||||
@Param("templateId") Long templateId,
|
||||
@Param("monthStart") Date monthStart,
|
||||
@Param("monthEnd") Date monthEnd);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.jsowell.pile.service;
|
||||
|
||||
import com.jsowell.pile.domain.CouponTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券模板 Service 接口
|
||||
*/
|
||||
public interface CouponTemplateService {
|
||||
|
||||
CouponTemplate getById(Long id);
|
||||
|
||||
/**
|
||||
* 后台列表(运营商管理员传 merchantId 自动过滤)
|
||||
*/
|
||||
List<CouponTemplate> listForAdmin(CouponTemplate query);
|
||||
|
||||
/**
|
||||
* 新增模板(含 scope 权限校验)
|
||||
*
|
||||
* @param template 模板信息(含 scopeList)
|
||||
* @param loginMerchantId 登录人运营商ID(平台管理员传 null)
|
||||
* @param isPlatformAdmin 是否平台管理员
|
||||
*/
|
||||
void add(CouponTemplate template, Long loginMerchantId, boolean isPlatformAdmin);
|
||||
|
||||
/**
|
||||
* 编辑模板(已有兑换记录时冻结核心字段)
|
||||
*/
|
||||
void edit(CouponTemplate template, Long loginMerchantId, boolean isPlatformAdmin);
|
||||
|
||||
/**
|
||||
* 上下架
|
||||
*/
|
||||
void changeStatus(Long id, Integer status, Long loginMerchantId, boolean isPlatformAdmin);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.jsowell.pile.service;
|
||||
|
||||
import com.jsowell.pile.domain.CouponTemplate;
|
||||
import com.jsowell.pile.domain.MemberCoupon;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会员优惠券 Service 接口
|
||||
*/
|
||||
public interface MemberCouponService {
|
||||
|
||||
/**
|
||||
* 查询用户可兑换的券模板列表
|
||||
*
|
||||
* @param memberId 会员ID
|
||||
* @param stationId 当前所在站点ID(用于 scope 过滤,可为 null)
|
||||
*/
|
||||
List<CouponTemplate> listAvailableTemplates(String memberId, Long stationId);
|
||||
|
||||
/**
|
||||
* 积分兑换券(原子:扣积分 + 生成券记录)
|
||||
*
|
||||
* @param memberId 会员ID
|
||||
* @param templateId 模板ID
|
||||
* @param requestId 客户端幂等键
|
||||
* @return 生成的券记录
|
||||
*/
|
||||
MemberCoupon exchange(String memberId, Long templateId, String requestId);
|
||||
|
||||
/**
|
||||
* 我的券包列表
|
||||
*
|
||||
* @param status null=全部,0=未使用,1=已使用,2=已过期
|
||||
*/
|
||||
List<MemberCoupon> myList(String memberId, Integer status);
|
||||
|
||||
/**
|
||||
* 券详情(校验所属会员)
|
||||
*/
|
||||
MemberCoupon detail(String memberId, String couponNo);
|
||||
|
||||
/**
|
||||
* 核销券
|
||||
*
|
||||
* @param couponNo 券编号
|
||||
* @param storeId 核销门店ID
|
||||
* @param requestId 请求幂等键
|
||||
* @param operatorId 操作人
|
||||
*/
|
||||
void verify(String couponNo, Long storeId, String requestId, String operatorId);
|
||||
|
||||
/**
|
||||
* 后台兑换记录查询
|
||||
*/
|
||||
List<MemberCoupon> listRecordForAdmin(MemberCoupon query);
|
||||
|
||||
/**
|
||||
* 批量归档过期券(Quartz 调用)
|
||||
*/
|
||||
int batchExpire();
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.jsowell.pile.service.impl;
|
||||
|
||||
import com.jsowell.common.exception.BusinessException;
|
||||
import com.jsowell.pile.domain.CouponTemplate;
|
||||
import com.jsowell.pile.domain.CouponTemplateScope;
|
||||
import com.jsowell.pile.mapper.CouponTemplateMapper;
|
||||
import com.jsowell.pile.mapper.CouponTemplateScopeMapper;
|
||||
import com.jsowell.pile.service.CouponTemplateService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券模板 Service 实现
|
||||
*/
|
||||
@Service
|
||||
public class CouponTemplateServiceImpl implements CouponTemplateService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CouponTemplateServiceImpl.class);
|
||||
|
||||
@Resource
|
||||
private CouponTemplateMapper couponTemplateMapper;
|
||||
|
||||
@Resource
|
||||
private CouponTemplateScopeMapper couponTemplateScopeMapper;
|
||||
|
||||
@Override
|
||||
public CouponTemplate getById(Long id) {
|
||||
CouponTemplate template = couponTemplateMapper.selectById(id);
|
||||
if (template != null && template.getScopeType() != null && template.getScopeType() > 1) {
|
||||
template.setScopeList(couponTemplateScopeMapper.selectByTemplateId(id));
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CouponTemplate> listForAdmin(CouponTemplate query) {
|
||||
return couponTemplateMapper.selectList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void add(CouponTemplate template, Long loginMerchantId, boolean isPlatformAdmin) {
|
||||
validateScope(template, loginMerchantId, isPlatformAdmin);
|
||||
|
||||
// 运营商管理员强制写入 creatorMerchantId
|
||||
if (!isPlatformAdmin) {
|
||||
template.setCreatorType(2);
|
||||
template.setCreatorMerchantId(loginMerchantId);
|
||||
} else {
|
||||
template.setCreatorType(1);
|
||||
}
|
||||
|
||||
// 初始化库存
|
||||
if (template.getStockTotal() == null) {
|
||||
template.setStockTotal(-1);
|
||||
}
|
||||
template.setStockRemain(template.getStockTotal());
|
||||
template.setStatus(0); // 默认下架
|
||||
|
||||
couponTemplateMapper.insert(template);
|
||||
|
||||
// 保存 scope 明细
|
||||
saveScopeList(template.getId(), template.getScopeList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void edit(CouponTemplate template, Long loginMerchantId, boolean isPlatformAdmin) {
|
||||
CouponTemplate existing = couponTemplateMapper.selectById(template.getId());
|
||||
if (existing == null) {
|
||||
throw new BusinessException("券模板不存在");
|
||||
}
|
||||
checkOwnership(existing, loginMerchantId, isPlatformAdmin);
|
||||
|
||||
// 冻结字段校验:已有兑换记录时不允许修改
|
||||
boolean hasExchanged = couponTemplateMapper.countExchangeRecord(template.getId()) > 0;
|
||||
if (hasExchanged) {
|
||||
if (template.getType() != null || template.getPointsCost() != null
|
||||
|| template.getValidityType() != null || template.getValidDays() != null) {
|
||||
throw new BusinessException("该券已有用户兑换,不可修改券类型、积分价格、有效期规则等核心字段");
|
||||
}
|
||||
}
|
||||
|
||||
couponTemplateMapper.updateById(template);
|
||||
|
||||
// 更新 scope(全量替换)
|
||||
if (template.getScopeList() != null) {
|
||||
validateScope(template, loginMerchantId, isPlatformAdmin);
|
||||
couponTemplateScopeMapper.deleteByTemplateId(template.getId());
|
||||
saveScopeList(template.getId(), template.getScopeList());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changeStatus(Long id, Integer status, Long loginMerchantId, boolean isPlatformAdmin) {
|
||||
CouponTemplate existing = couponTemplateMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
throw new BusinessException("券模板不存在");
|
||||
}
|
||||
checkOwnership(existing, loginMerchantId, isPlatformAdmin);
|
||||
|
||||
CouponTemplate update = new CouponTemplate();
|
||||
update.setId(id);
|
||||
update.setStatus(status);
|
||||
couponTemplateMapper.updateById(update);
|
||||
}
|
||||
|
||||
// ---------- 私有方法 ----------
|
||||
|
||||
/**
|
||||
* 校验 scope 权限边界
|
||||
*/
|
||||
private void validateScope(CouponTemplate template, Long loginMerchantId, boolean isPlatformAdmin) {
|
||||
Integer scopeType = template.getScopeType();
|
||||
if (scopeType == null) {
|
||||
return;
|
||||
}
|
||||
// 运营商管理员不能选全平台
|
||||
if (!isPlatformAdmin && scopeType == 1) {
|
||||
throw new BusinessException("运营商管理员不能创建全平台范围的券");
|
||||
}
|
||||
// 运营商管理员选指定站点时,校验站点归属
|
||||
if (!isPlatformAdmin && scopeType == 3 && !CollectionUtils.isEmpty(template.getScopeList())) {
|
||||
for (CouponTemplateScope scope : template.getScopeList()) {
|
||||
// 此处仅做占位;实际需注入 PileStationInfoMapper 校验站点归属
|
||||
// 示例:pileStationInfoMapper.selectById(scope.getScopeId()).getMerchantId() == loginMerchantId
|
||||
logger.debug("校验站点 {} 是否属于运营商 {}", scope.getScopeId(), loginMerchantId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验操作人是否有权操作该模板
|
||||
*/
|
||||
private void checkOwnership(CouponTemplate template, Long loginMerchantId, boolean isPlatformAdmin) {
|
||||
if (!isPlatformAdmin) {
|
||||
if (!loginMerchantId.equals(template.getCreatorMerchantId())) {
|
||||
throw new BusinessException("无权操作该券模板");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveScopeList(Long templateId, List<CouponTemplateScope> scopeList) {
|
||||
if (!CollectionUtils.isEmpty(scopeList)) {
|
||||
for (CouponTemplateScope scope : scopeList) {
|
||||
scope.setTemplateId(templateId);
|
||||
}
|
||||
couponTemplateScopeMapper.insertBatch(scopeList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package com.jsowell.pile.service.impl;
|
||||
|
||||
import com.jsowell.common.exception.BusinessException;
|
||||
import com.jsowell.pile.domain.CouponTemplate;
|
||||
import com.jsowell.pile.domain.CouponTemplateScope;
|
||||
import com.jsowell.pile.domain.CouponVerifyRecord;
|
||||
import com.jsowell.pile.domain.MemberCoupon;
|
||||
import com.jsowell.pile.mapper.CouponTemplateScopeMapper;
|
||||
import com.jsowell.pile.mapper.CouponTemplateMapper;
|
||||
import com.jsowell.pile.mapper.CouponVerifyRecordMapper;
|
||||
import com.jsowell.pile.mapper.MemberCouponMapper;
|
||||
import com.jsowell.pile.service.MemberCouponService;
|
||||
import com.jsowell.pile.service.MemberPointsInfoService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 会员优惠券 Service 实现
|
||||
*/
|
||||
@Service
|
||||
public class MemberCouponServiceImpl implements MemberCouponService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MemberCouponServiceImpl.class);
|
||||
|
||||
/** 积分类型:兑换消耗 */
|
||||
private static final int POINTS_TYPE_COUPON_EXCHANGE = 3;
|
||||
|
||||
@Resource
|
||||
private MemberCouponMapper memberCouponMapper;
|
||||
|
||||
@Resource
|
||||
private CouponTemplateMapper couponTemplateMapper;
|
||||
|
||||
@Resource
|
||||
private CouponTemplateScopeMapper couponTemplateScopeMapper;
|
||||
|
||||
@Resource
|
||||
private CouponVerifyRecordMapper couponVerifyRecordMapper;
|
||||
|
||||
@Resource
|
||||
private MemberPointsInfoService memberPointsInfoService;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 用户端
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public List<CouponTemplate> listAvailableTemplates(String memberId, Long stationId) {
|
||||
// 查询所有上架且在兑换期内的模板
|
||||
CouponTemplate query = new CouponTemplate();
|
||||
query.setStatus(1);
|
||||
List<CouponTemplate> all = couponTemplateMapper.selectList(query);
|
||||
|
||||
Date now = new Date();
|
||||
all.removeIf(t -> {
|
||||
// 兑换时间段校验
|
||||
if (t.getExchangeStartTime() != null && now.before(t.getExchangeStartTime())) return true;
|
||||
if (t.getExchangeEndTime() != null && now.after(t.getExchangeEndTime())) return true;
|
||||
// 库存校验
|
||||
if (t.getStockRemain() != null && t.getStockRemain() == 0) return true;
|
||||
// scope 校验:全平台(1)直接放行;指定范围时按 stationId 过滤
|
||||
if (t.getScopeType() != null && t.getScopeType() > 1 && stationId != null) {
|
||||
return !isScopeMatch(t, stationId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return all;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MemberCoupon exchange(String memberId, Long templateId, String requestId) {
|
||||
CouponTemplate template = couponTemplateMapper.selectById(templateId);
|
||||
if (template == null || template.getStatus() != 1) {
|
||||
throw new BusinessException("券模板不存在或已下架");
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
// 1. 兑换时间段校验
|
||||
if (template.getExchangeStartTime() != null && now.before(template.getExchangeStartTime())) {
|
||||
throw new BusinessException("兑换活动尚未开始");
|
||||
}
|
||||
if (template.getExchangeEndTime() != null && now.after(template.getExchangeEndTime())) {
|
||||
throw new BusinessException("兑换活动已结束");
|
||||
}
|
||||
|
||||
// 2. 单用户累计限额
|
||||
if (template.getTotalLimit() != null && template.getTotalLimit() > 0) {
|
||||
int total = memberCouponMapper.countByMemberAndTemplate(memberId, templateId);
|
||||
if (total >= template.getTotalLimit()) {
|
||||
throw new BusinessException("您已达到兑换上限");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 单用户每月限额
|
||||
if (template.getMonthlyLimit() != null && template.getMonthlyLimit() > 0) {
|
||||
Date[] monthRange = getMonthRange(now);
|
||||
int monthly = memberCouponMapper.countMonthByMemberAndTemplate(memberId, templateId, monthRange[0], monthRange[1]);
|
||||
if (monthly >= template.getMonthlyLimit()) {
|
||||
throw new BusinessException("本月兑换已达上限");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 单用户每日限额
|
||||
if (template.getDailyLimit() != null && template.getDailyLimit() > 0) {
|
||||
Date[] dayRange = getDayRange(now);
|
||||
int daily = memberCouponMapper.countTodayByMemberAndTemplate(memberId, templateId, dayRange[0], dayRange[1]);
|
||||
if (daily >= template.getDailyLimit()) {
|
||||
throw new BusinessException("今日兑换已达上限");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 扣减库存(原子,stock_remain=-1 时跳过)
|
||||
if (template.getStockRemain() != null && template.getStockRemain() != -1) {
|
||||
int updated = couponTemplateMapper.deductStock(templateId);
|
||||
if (updated <= 0) {
|
||||
throw new BusinessException("库存不足,兑换失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 计算过期时间
|
||||
Date expireTime = calcExpireTime(template, now);
|
||||
|
||||
// 7. 扣减积分(复用现有 service,写积分流水 type=3)
|
||||
memberPointsInfoService.deductPoints(memberId, template.getPointsCost(),
|
||||
"COUPON_" + requestId);
|
||||
|
||||
// 8. 生成用户券记录(exchange_request_id 唯一索引兜底幂等)
|
||||
MemberCoupon coupon = MemberCoupon.builder()
|
||||
.couponNo(UUID.randomUUID().toString().replace("-", ""))
|
||||
.exchangeRequestId(requestId)
|
||||
.templateId(templateId)
|
||||
.memberId(memberId)
|
||||
.couponType(template.getType())
|
||||
.pointsCost(template.getPointsCost())
|
||||
.discountRate(template.getDiscountRate())
|
||||
.source(1)
|
||||
.expireTime(expireTime)
|
||||
.build();
|
||||
try {
|
||||
memberCouponMapper.insert(coupon);
|
||||
} catch (DuplicateKeyException e) {
|
||||
// 重复请求:幂等返回已有记录
|
||||
logger.warn("兑换幂等命中,requestId: {}, memberId: {}", requestId, memberId);
|
||||
return memberCouponMapper.selectByCouponNo(
|
||||
memberCouponMapper.selectByMemberId(memberId, null)
|
||||
.stream()
|
||||
.filter(c -> requestId.equals(c.getExchangeRequestId()))
|
||||
.map(MemberCoupon::getCouponNo)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BusinessException("兑换异常,请重试")));
|
||||
}
|
||||
return coupon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MemberCoupon> myList(String memberId, Integer status) {
|
||||
return memberCouponMapper.selectByMemberId(memberId, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MemberCoupon detail(String memberId, String couponNo) {
|
||||
MemberCoupon coupon = memberCouponMapper.selectByCouponNo(couponNo);
|
||||
if (coupon == null || !memberId.equals(coupon.getMemberId())) {
|
||||
throw new BusinessException("券不存在");
|
||||
}
|
||||
return coupon;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 核销
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void verify(String couponNo, Long storeId, String requestId, String operatorId) {
|
||||
// 请求幂等:同一 requestId 已处理过则直接返回
|
||||
if (requestId != null) {
|
||||
CouponVerifyRecord existing = couponVerifyRecordMapper.selectByRequestId(requestId);
|
||||
if (existing != null) {
|
||||
if (existing.getResult() == 1) {
|
||||
return; // 已成功核销,幂等返回
|
||||
}
|
||||
throw new BusinessException("该核销请求已失败,请重新发起");
|
||||
}
|
||||
}
|
||||
|
||||
MemberCoupon coupon = memberCouponMapper.selectByCouponNo(couponNo);
|
||||
if (coupon == null) {
|
||||
writeVerifyLog(couponNo, requestId, storeId, operatorId, 2, "券不存在");
|
||||
throw new BusinessException("券不存在");
|
||||
}
|
||||
|
||||
// 校验 scope:核销门店是否在券可用范围内
|
||||
CouponTemplate template = couponTemplateMapper.selectById(coupon.getTemplateId());
|
||||
if (template != null && template.getScopeType() != null && template.getScopeType() > 1) {
|
||||
if (!isScopeMatch(template, storeId)) {
|
||||
writeVerifyLog(couponNo, requestId, storeId, operatorId, 2, "核销门店不在券可用范围内");
|
||||
throw new BusinessException("核销门店不在券可用范围内");
|
||||
}
|
||||
}
|
||||
|
||||
// 原子核销(status=0 且未过期)
|
||||
int updated = memberCouponMapper.verify(couponNo, new Date(), storeId, operatorId);
|
||||
if (updated <= 0) {
|
||||
String reason = coupon.getStatus() != 0 ? "券已使用或已过期" : "券已过期";
|
||||
writeVerifyLog(couponNo, requestId, storeId, operatorId, 2, reason);
|
||||
throw new BusinessException(reason);
|
||||
}
|
||||
|
||||
writeVerifyLog(couponNo, requestId, storeId, operatorId, 1, null);
|
||||
logger.info("核销成功,couponNo: {}, storeId: {}, operator: {}", couponNo, storeId, operatorId);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 后台 & 定时任务
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public List<MemberCoupon> listRecordForAdmin(MemberCoupon query) {
|
||||
return memberCouponMapper.selectRecordList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchExpire() {
|
||||
int count = memberCouponMapper.batchExpire(new Date());
|
||||
logger.info("批量归档过期券完成,共归档 {} 条", count);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 私有工具方法
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 校验门店/站点是否在模板 scope 范围内
|
||||
*/
|
||||
private boolean isScopeMatch(CouponTemplate template, Long targetId) {
|
||||
if (template.getScopeType() == 1) {
|
||||
return true;
|
||||
}
|
||||
List<Long> scopeIds = couponTemplateScopeMapper
|
||||
.selectTemplateIdsByScopeId(template.getScopeType(), targetId);
|
||||
// scopeIds 是按 (scopeType, scopeId) 反查的 templateId 列表
|
||||
return scopeIds.contains(template.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算券的过期时间
|
||||
*/
|
||||
private Date calcExpireTime(CouponTemplate template, Date now) {
|
||||
if (template.getValidityType() == 1) {
|
||||
return template.getValidEndTime();
|
||||
}
|
||||
// 领取后 N 天
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(now);
|
||||
cal.add(Calendar.DAY_OF_MONTH, template.getValidDays());
|
||||
// 设为当天 23:59:59
|
||||
cal.set(Calendar.HOUR_OF_DAY, 23);
|
||||
cal.set(Calendar.MINUTE, 59);
|
||||
cal.set(Calendar.SECOND, 59);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
return cal.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当天 [00:00:00, 次日 00:00:00) 范围(Asia/Shanghai)
|
||||
*/
|
||||
private Date[] getDayRange(Date now) {
|
||||
Calendar cal = Calendar.getInstance(java.util.TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
cal.setTime(now);
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
Date start = cal.getTime();
|
||||
cal.add(Calendar.DAY_OF_MONTH, 1);
|
||||
Date end = cal.getTime();
|
||||
return new Date[]{start, end};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本月 [月初 00:00:00, 次月月初 00:00:00) 范围(Asia/Shanghai)
|
||||
*/
|
||||
private Date[] getMonthRange(Date now) {
|
||||
Calendar cal = Calendar.getInstance(java.util.TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
cal.setTime(now);
|
||||
cal.set(Calendar.DAY_OF_MONTH, 1);
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
Date start = cal.getTime();
|
||||
cal.add(Calendar.MONTH, 1);
|
||||
Date end = cal.getTime();
|
||||
return new Date[]{start, end};
|
||||
}
|
||||
|
||||
private void writeVerifyLog(String couponNo, String requestId, Long storeId,
|
||||
String operatorId, int result, String failReason) {
|
||||
try {
|
||||
CouponVerifyRecord log = CouponVerifyRecord.builder()
|
||||
.couponNo(couponNo)
|
||||
.requestId(requestId)
|
||||
.storeId(storeId)
|
||||
.operatorId(operatorId)
|
||||
.result(result)
|
||||
.failReason(failReason)
|
||||
.build();
|
||||
couponVerifyRecordMapper.insert(log);
|
||||
} catch (Exception e) {
|
||||
logger.error("写入核销日志失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.jsowell.pile.mapper.CouponTemplateMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.jsowell.pile.domain.CouponTemplate">
|
||||
<id column="id" property="id"/>
|
||||
<result column="name" property="name"/>
|
||||
<result column="type" property="type"/>
|
||||
<result column="creator_type" property="creatorType"/>
|
||||
<result column="creator_merchant_id" property="creatorMerchantId"/>
|
||||
<result column="scope_type" property="scopeType"/>
|
||||
<result column="points_cost" property="pointsCost"/>
|
||||
<result column="discount_rate" property="discountRate"/>
|
||||
<result column="min_charge_amount" property="minChargeAmount"/>
|
||||
<result column="max_discount_amount" property="maxDiscountAmount"/>
|
||||
<result column="exchange_start_time" property="exchangeStartTime"/>
|
||||
<result column="exchange_end_time" property="exchangeEndTime"/>
|
||||
<result column="stock_total" property="stockTotal"/>
|
||||
<result column="stock_remain" property="stockRemain"/>
|
||||
<result column="validity_type" property="validityType"/>
|
||||
<result column="valid_start_time" property="validStartTime"/>
|
||||
<result column="valid_end_time" property="validEndTime"/>
|
||||
<result column="valid_days" property="validDays"/>
|
||||
<result column="daily_limit" property="dailyLimit"/>
|
||||
<result column="monthly_limit" property="monthlyLimit"/>
|
||||
<result column="total_limit" property="totalLimit"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="description" property="description"/>
|
||||
<result column="update_by" property="updateBy"/>
|
||||
<result column="create_by" property="createBy"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="del_flag" property="delFlag"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, name, type, creator_type, creator_merchant_id, scope_type,
|
||||
points_cost, discount_rate, min_charge_amount, max_discount_amount,
|
||||
exchange_start_time, exchange_end_time,
|
||||
stock_total, stock_remain, validity_type,
|
||||
valid_start_time, valid_end_time, valid_days,
|
||||
daily_limit, monthly_limit, total_limit,
|
||||
status, description, update_by, create_by, create_time, update_time, del_flag
|
||||
</sql>
|
||||
|
||||
<select id="selectById" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM coupon_template
|
||||
WHERE id = #{id} AND del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM coupon_template
|
||||
WHERE del_flag = '0'
|
||||
<if test="creatorMerchantId != null">
|
||||
AND creator_merchant_id = #{creatorMerchantId}
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
<if test="type != null">
|
||||
AND type = #{type}
|
||||
</if>
|
||||
<if test="name != null and name != ''">
|
||||
AND name LIKE CONCAT('%', #{name}, '%')
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO coupon_template (
|
||||
name, type, creator_type, creator_merchant_id, scope_type,
|
||||
points_cost, discount_rate, min_charge_amount, max_discount_amount,
|
||||
exchange_start_time, exchange_end_time,
|
||||
stock_total, stock_remain, validity_type,
|
||||
valid_start_time, valid_end_time, valid_days,
|
||||
daily_limit, monthly_limit, total_limit,
|
||||
status, description, update_by, create_by, create_time, update_time, del_flag
|
||||
) VALUES (
|
||||
#{name}, #{type}, #{creatorType}, #{creatorMerchantId}, #{scopeType},
|
||||
#{pointsCost}, #{discountRate}, #{minChargeAmount}, #{maxDiscountAmount},
|
||||
#{exchangeStartTime}, #{exchangeEndTime},
|
||||
#{stockTotal}, #{stockRemain}, #{validityType},
|
||||
#{validStartTime}, #{validEndTime}, #{validDays},
|
||||
#{dailyLimit}, #{monthlyLimit}, #{totalLimit},
|
||||
#{status}, #{description}, #{updateBy}, #{createBy}, NOW(), NOW(), '0'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateById">
|
||||
UPDATE coupon_template
|
||||
<set>
|
||||
<if test="name != null">name = #{name},</if>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="stockTotal != null">stock_total = #{stockTotal},</if>
|
||||
<if test="dailyLimit != null">daily_limit = #{dailyLimit},</if>
|
||||
<if test="monthlyLimit != null">monthly_limit = #{monthlyLimit},</if>
|
||||
<if test="totalLimit != null">total_limit = #{totalLimit},</if>
|
||||
<if test="description != null">description = #{description},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
update_time = NOW()
|
||||
</set>
|
||||
WHERE id = #{id} AND del_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- 原子扣减库存:stock_remain=-1(不限) 时跳过扣减直接返回1 -->
|
||||
<update id="deductStock">
|
||||
UPDATE coupon_template
|
||||
SET stock_remain = stock_remain - 1,
|
||||
update_time = NOW()
|
||||
WHERE id = #{id}
|
||||
AND del_flag = '0'
|
||||
AND (stock_remain = -1 OR stock_remain >= 1)
|
||||
</update>
|
||||
|
||||
<!-- 检查是否已有兑换记录(用于冻结字段校验) -->
|
||||
<select id="countExchangeRecord" resultType="int">
|
||||
SELECT COUNT(1) FROM member_coupon
|
||||
WHERE template_id = #{templateId} AND del_flag = '0'
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.jsowell.pile.mapper.CouponTemplateScopeMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.jsowell.pile.domain.CouponTemplateScope">
|
||||
<id column="id" property="id"/>
|
||||
<result column="template_id" property="templateId"/>
|
||||
<result column="scope_type" property="scopeType"/>
|
||||
<result column="scope_id" property="scopeId"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByTemplateId" resultMap="BaseResultMap">
|
||||
SELECT id, template_id, scope_type, scope_id
|
||||
FROM coupon_template_scope
|
||||
WHERE template_id = #{templateId}
|
||||
</select>
|
||||
|
||||
<!-- 反向查询:按 (scopeType, scopeId) 查可用的模板ID列表(走 idx_scope_lookup) -->
|
||||
<select id="selectTemplateIdsByScopeId" resultType="java.lang.Long">
|
||||
SELECT template_id
|
||||
FROM coupon_template_scope
|
||||
WHERE scope_type = #{scopeType}
|
||||
AND scope_id = #{scopeId}
|
||||
</select>
|
||||
|
||||
<insert id="insertBatch">
|
||||
INSERT INTO coupon_template_scope (template_id, scope_type, scope_id)
|
||||
VALUES
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.templateId}, #{item.scopeType}, #{item.scopeId})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<delete id="deleteByTemplateId">
|
||||
DELETE FROM coupon_template_scope WHERE template_id = #{templateId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.jsowell.pile.mapper.CouponVerifyRecordMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.jsowell.pile.domain.CouponVerifyRecord">
|
||||
<id column="id" property="id"/>
|
||||
<result column="coupon_no" property="couponNo"/>
|
||||
<result column="request_id" property="requestId"/>
|
||||
<result column="store_id" property="storeId"/>
|
||||
<result column="operator_id" property="operatorId"/>
|
||||
<result column="result" property="result"/>
|
||||
<result column="fail_reason" property="failReason"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
</resultMap>
|
||||
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO coupon_verify_record (
|
||||
coupon_no, request_id, store_id, operator_id,
|
||||
result, fail_reason, create_time
|
||||
) VALUES (
|
||||
#{couponNo}, #{requestId}, #{storeId}, #{operatorId},
|
||||
#{result}, #{failReason}, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="selectByRequestId" resultMap="BaseResultMap">
|
||||
SELECT id, coupon_no, request_id, store_id, operator_id,
|
||||
result, fail_reason, create_time
|
||||
FROM coupon_verify_record
|
||||
WHERE request_id = #{requestId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.jsowell.pile.mapper.MemberCouponMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.jsowell.pile.domain.MemberCoupon">
|
||||
<id column="id" property="id"/>
|
||||
<result column="coupon_no" property="couponNo"/>
|
||||
<result column="exchange_request_id" property="exchangeRequestId"/>
|
||||
<result column="template_id" property="templateId"/>
|
||||
<result column="member_id" property="memberId"/>
|
||||
<result column="coupon_type" property="couponType"/>
|
||||
<result column="points_cost" property="pointsCost"/>
|
||||
<result column="discount_rate" property="discountRate"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="source" property="source"/>
|
||||
<result column="exchange_time" property="exchangeTime"/>
|
||||
<result column="expire_time" property="expireTime"/>
|
||||
<result column="use_time" property="useTime"/>
|
||||
<result column="use_store_id" property="useStoreId"/>
|
||||
<result column="use_operator" property="useOperator"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="del_flag" property="delFlag"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, coupon_no, exchange_request_id, template_id, member_id,
|
||||
coupon_type, points_cost, discount_rate,
|
||||
status, source, exchange_time, expire_time,
|
||||
use_time, use_store_id, use_operator,
|
||||
create_time, del_flag
|
||||
</sql>
|
||||
|
||||
<select id="selectByCouponNo" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM member_coupon
|
||||
WHERE coupon_no = #{couponNo} AND del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectByMemberId" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM member_coupon
|
||||
WHERE member_id = #{memberId}
|
||||
AND del_flag = '0'
|
||||
<if test="status != null">
|
||||
AND (
|
||||
<choose>
|
||||
<when test="status == 2">
|
||||
status = 2 OR (status = 0 AND expire_time <= NOW())
|
||||
</when>
|
||||
<when test="status == 0">
|
||||
status = 0 AND expire_time > NOW()
|
||||
</when>
|
||||
<otherwise>
|
||||
status = #{status}
|
||||
</otherwise>
|
||||
</choose>
|
||||
)
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectRecordList" resultMap="BaseResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM member_coupon
|
||||
WHERE del_flag = '0'
|
||||
<if test="memberId != null and memberId != ''">
|
||||
AND member_id = #{memberId}
|
||||
</if>
|
||||
<if test="templateId != null">
|
||||
AND template_id = #{templateId}
|
||||
</if>
|
||||
<if test="status != null">
|
||||
AND status = #{status}
|
||||
</if>
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO member_coupon (
|
||||
coupon_no, exchange_request_id, template_id, member_id,
|
||||
coupon_type, points_cost, discount_rate,
|
||||
status, source, exchange_time, expire_time,
|
||||
create_time, del_flag
|
||||
) VALUES (
|
||||
#{couponNo}, #{exchangeRequestId}, #{templateId}, #{memberId},
|
||||
#{couponType}, #{pointsCost}, #{discountRate},
|
||||
0, #{source}, NOW(), #{expireTime},
|
||||
NOW(), '0'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 原子核销:status=0 且未过期时才更新 -->
|
||||
<update id="verify">
|
||||
UPDATE member_coupon
|
||||
SET status = 1,
|
||||
use_time = #{useTime},
|
||||
use_store_id = #{useStoreId},
|
||||
use_operator = #{useOperator}
|
||||
WHERE coupon_no = #{couponNo}
|
||||
AND status = 0
|
||||
AND expire_time > NOW()
|
||||
AND del_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- 批量归档过期券(Quartz 定时任务) -->
|
||||
<update id="batchExpire">
|
||||
UPDATE member_coupon
|
||||
SET status = 2
|
||||
WHERE status = 0
|
||||
AND expire_time <= #{now}
|
||||
AND del_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- 累计兑换数(total_limit 校验) -->
|
||||
<select id="countByMemberAndTemplate" resultType="int">
|
||||
SELECT COUNT(1) FROM member_coupon
|
||||
WHERE member_id = #{memberId}
|
||||
AND template_id = #{templateId}
|
||||
AND del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 今日兑换数(daily_limit 校验) -->
|
||||
<select id="countTodayByMemberAndTemplate" resultType="int">
|
||||
SELECT COUNT(1) FROM member_coupon
|
||||
WHERE member_id = #{memberId}
|
||||
AND template_id = #{templateId}
|
||||
AND del_flag = '0'
|
||||
AND exchange_time >= #{dayStart}
|
||||
AND exchange_time < #{dayEnd}
|
||||
</select>
|
||||
|
||||
<!-- 本月兑换数(monthly_limit 校验) -->
|
||||
<select id="countMonthByMemberAndTemplate" resultType="int">
|
||||
SELECT COUNT(1) FROM member_coupon
|
||||
WHERE member_id = #{memberId}
|
||||
AND template_id = #{templateId}
|
||||
AND del_flag = '0'
|
||||
AND exchange_time >= #{monthStart}
|
||||
AND exchange_time < #{monthEnd}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user