package com.doumee.service.business.impl;
|
|
import com.alibaba.fastjson.JSON;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.doumee.core.constants.Constants;
|
import com.doumee.core.constants.ResponseStatus;
|
import com.doumee.core.douyin.DouyinClient;
|
import com.doumee.core.douyin.dto.DouyinBaseResp;
|
import com.doumee.core.douyin.dto.DouyinCancelParam;
|
import com.doumee.core.douyin.dto.DouyinCancelReq;
|
import com.doumee.core.douyin.dto.DouyinCancelResp;
|
import com.doumee.core.douyin.dto.DouyinPrepareParam;
|
import com.doumee.core.douyin.dto.DouyinPrepareReq;
|
import com.doumee.core.douyin.dto.DouyinPrepareResp;
|
import com.doumee.core.douyin.dto.DouyinVerifyParam;
|
import com.doumee.core.douyin.dto.DouyinVerifyReq;
|
import com.doumee.core.douyin.dto.DouyinVerifyResp;
|
import com.doumee.core.exception.BusinessException;
|
import com.doumee.core.model.PageData;
|
import com.doumee.core.model.PageWrap;
|
import com.doumee.core.utils.DateUtil;
|
import com.doumee.core.utils.ID;
|
import com.doumee.dao.business.DiscountLogMapper;
|
import com.doumee.dao.business.DiscountMapper;
|
import com.doumee.dao.business.DiscountMemberMapper;
|
import com.doumee.dao.business.DouyinProductMapper;
|
import com.doumee.dao.business.DouyinProductSkuMapper;
|
import com.doumee.dao.business.DouyinVerifyRecordMapper;
|
import com.doumee.dao.business.GoodsorderMapper;
|
import com.doumee.dao.business.model.Discount;
|
import com.doumee.dao.business.model.DiscountLog;
|
import com.doumee.dao.business.model.DiscountMember;
|
import com.doumee.dao.business.model.DouyinProduct;
|
import com.doumee.dao.business.model.DouyinProductSku;
|
import com.doumee.dao.business.model.DouyinVerifyRecord;
|
import com.doumee.dao.business.model.Goodsorder;
|
import com.doumee.dao.business.model.Member;
|
import com.doumee.dao.business.vo.DouyinVerifyRecordPageVO;
|
import com.doumee.service.business.DouyinVerifyService;
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.lang3.StringUtils;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.util.Date;
|
import java.util.List;
|
|
/**
|
* 抖音券核销 Service 实现。
|
* <p>覆盖验券三步链路:prepare(验券准备,扫码/输码拿 verifyToken 与券列表)
|
* → verify(核销,落核销记录 + 开通套餐)→ cancel(核销后 1 小时内撤销)。
|
* 操作人 operator 由调用端传入(web 端取登录会员 id),service 不依赖任何鉴权框架。
|
*
|
* @author rk
|
* @date 2026/06/22
|
*/
|
@Slf4j
|
@Service
|
public class DouyinVerifyServiceImpl implements DouyinVerifyService {
|
|
@Autowired
|
private DouyinClient douyinClient;
|
@Autowired
|
private DouyinVerifyRecordMapper douyinVerifyRecordMapper;
|
@Autowired
|
private DouyinProductSkuMapper douyinProductSkuMapper;
|
@Autowired
|
private DouyinProductMapper douyinProductMapper;
|
@Autowired
|
private DiscountMapper discountMapper;
|
@Autowired
|
private DiscountMemberMapper discountMemberMapper;
|
@Autowired
|
private GoodsorderMapper goodsorderMapper;
|
@Autowired
|
private DiscountLogMapper discountLogMapper;
|
|
/** 抖音验券接口返回的核销结果码:0成功(非本地表字段,不并入 Constants 枚举) */
|
private static final int VERIFY_OK = 0;
|
|
/** goodsorder 交易类型:套餐卡购买 */
|
private static final int GOODSORDER_TYPE_DISCOUNT = 1;
|
/** goodsorder 关联对象类型:套餐卡 */
|
private static final int GOODSORDER_OBJ_TYPE_DISCOUNT = 0;
|
/** goodsorder 已支付状态(订单状态 / 支付状态均为 1) */
|
private static final int GOODSORDER_PAID = 1;
|
/** 支付方式:抖音券核销(需前端支付方式字典配合展示) */
|
private static final int PAY_WAY_DOUYIN = 2;
|
/** discount_log 操作类型:平台调整 */
|
private static final int DISCOUNT_LOG_TYPE_ADJUST = 2;
|
|
@Override
|
public DouyinBaseResp<DouyinPrepareResp> prepare(DouyinPrepareParam param) {
|
// poiId 为空时兜底取字典配置(单门店,后台可改)
|
if (param != null && StringUtils.isBlank(param.getPoiId())) {
|
param.setPoiId(douyinClient.getPoiId());
|
}
|
// 入参校验:门店 + (二维码 | 券码) 必填
|
if (param == null || StringUtils.isBlank(param.getPoiId())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "poiId 未配置(前端未传且字典 POI_ID 为空)");
|
}
|
if (StringUtils.isBlank(param.getQrContent()) && StringUtils.isBlank(param.getCode())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "qrContent 与 code 至少传一个");
|
}
|
DouyinPrepareReq req = new DouyinPrepareReq();
|
req.setPoiId(param.getPoiId());
|
// 券码明文(手动输入)场景
|
if (StringUtils.isNotBlank(param.getCode())) {
|
req.setCode(param.getCode());
|
}
|
// 扫码场景:先把短链 / 含 object_id 的长链解析成 encrypted_data
|
if (StringUtils.isNotBlank(param.getQrContent())) {
|
String encryptedData = douyinClient.resolveShortLink(param.getQrContent());
|
// 解析不到就把原文当 encrypted_data 兜底交给抖音
|
req.setEncryptedData(StringUtils.isBlank(encryptedData) ? param.getQrContent() : encryptedData);
|
}
|
return douyinClient.prepare(req);
|
}
|
|
/**
|
* 验券(核销),成功后为当前操作人开通套餐卡(整单事务)。
|
* <p>方法级事务 {@code @Transactional}:抖音核销接口失败、券不可核销、开套餐任一环节异常,
|
* 均整单回滚(核销记录 / 套餐卡 / 订单 / 日志同生共灭)。controller 层 {@code douyin_verify_log}
|
* 在 finally 独立保存,仍留痕便于事后凭券码补开。
|
*
|
* @param param 核销入参(verifyToken/poiId/encryptedCodes + skuId 反查套餐)
|
* @param operator 操作人 = 套餐归属人(web 端登录会员 id)
|
*/
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public DouyinVerifyRecord verify(DouyinVerifyParam param, String operator) {
|
// 入参校验
|
if (param == null || StringUtils.isBlank(param.getVerifyToken())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "verifyToken 不能为空");
|
}
|
// poiId 为空时兜底取字典配置(单门店,后台可改)
|
if (StringUtils.isBlank(param.getPoiId())) {
|
param.setPoiId(douyinClient.getPoiId());
|
}
|
if (StringUtils.isBlank(param.getPoiId())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "poiId 未配置(前端未传且字典 POI_ID 为空)");
|
}
|
if (param.getEncryptedCodes() == null || param.getEncryptedCodes().isEmpty()) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "encryptedCodes 不能为空");
|
}
|
if (StringUtils.isBlank(param.getSkuId())) {
|
// 无 skuId 则无法反查套餐(核销返回本身不含商品标识),直接拦截
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "skuId 不能为空");
|
}
|
Date now = new Date();
|
|
// 组装抖音验券请求
|
DouyinVerifyReq req = new DouyinVerifyReq();
|
req.setVerifyToken(param.getVerifyToken());
|
req.setPoiId(param.getPoiId());
|
req.setEncryptedCodes(param.getEncryptedCodes());
|
req.setAccountId(param.getAccountId());
|
|
// 调用抖音验券
|
DouyinBaseResp<DouyinVerifyResp> resp = douyinClient.verify(req);
|
String respText = JSON.toJSONString(resp);
|
|
// 接口级失败(extra.errorCode 非 0):整单回滚,由 controller 日志留痕
|
Integer extraCode = resp == null || resp.getExtra() == null ? null : resp.getExtra().getErrorCode();
|
if (extraCode == null || extraCode != 0) {
|
String desc = resp == null || resp.getExtra() == null ? "无响应" : resp.getExtra().getDescription();
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "验券失败:" + desc);
|
}
|
|
// 接口成功,取首张券的核销结果(当前按首张处理)
|
DouyinVerifyResp data = resp.getData();
|
List<DouyinVerifyResp.VerifyResult> results = data == null ? null : data.getVerifyResults();
|
DouyinVerifyResp.VerifyResult first = (results == null || results.isEmpty()) ? null : results.get(0);
|
boolean ok = first != null && first.getResult() != null && first.getResult() == VERIFY_OK;
|
|
// 落核销记录(成功 / 失败都先落;券不可核销时随事务回滚)
|
DouyinVerifyRecord rec = baseRecord(req, respText, operator, now);
|
rec.setVerifyStatus(ok ? Constants.DOUYIN_VERIFY_STATUS.SUCCESS.getKey() : Constants.DOUYIN_VERIFY_STATUS.FAIL.getKey());
|
rec.setVerifyMsg(first == null ? (data == null ? null : data.getDescription()) : first.getMsg());
|
rec.setPoiId(param.getPoiId());
|
rec.setAccountId(first != null ? first.getAccountId() : param.getAccountId());
|
rec.setEncryptedCode(joinCodes(param.getEncryptedCodes()));
|
if (first != null) {
|
// 快照抖音返回的核销关键标识,撤销核销时要用
|
rec.setVerifyId(first.getVerifyId());
|
rec.setCertificateId(first.getCertificateId());
|
rec.setOriginCode(first.getOriginCode());
|
rec.setOrderId(first.getOrderId());
|
}
|
rec.setCancelStatus(Constants.ZERO);
|
douyinVerifyRecordMapper.insert(rec);
|
|
// 接口成功但券本身不可核销(如已核销 / 已退款):抛出,整单回滚
|
if (!ok) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
|
"验券失败:" + (rec.getVerifyMsg() == null ? "未知原因" : rec.getVerifyMsg()));
|
}
|
|
// 核销成功:为当前登录人开通套餐(任一步失败 → 整单回滚)
|
openDiscountForVerify(rec, param, operator);
|
return rec;
|
}
|
|
/**
|
* 核销成功后,按 skuId 反查本地套餐并为当前登录人开通套餐卡(含订单与开通日志)。
|
* <p>任一步失败抛异常 → verify 的 {@code @Transactional} 整单回滚(核销记录一并回滚);
|
* controller 层 {@code douyin_verify_log} 在 finally 独立保存,仍留痕便于事后补开。
|
*
|
* @param rec 核销记录(含 originCode/certificateId 等抖音标识,开通后回填套餐卡ID)
|
* @param param 核销入参(含 skuId 反查套餐、payAmount 快照)
|
* @param operator 操作人 = 套餐归属人(web 端登录会员 id)
|
*/
|
private void openDiscountForVerify(DouyinVerifyRecord rec, DouyinVerifyParam param, String operator) {
|
// ① 反查套餐:skuId → douyin_product_sku → product_id → douyin_product.out_id → discount
|
DouyinProduct product = resolveProduct(param.getSkuId());
|
Discount discount = resolveDiscount(product);
|
|
Date now = new Date();
|
|
// ② 防重:同一券码已为该用户开过套餐卡则跳过(避免重复核销重开)
|
DiscountMember existCard = discountMemberMapper.selectOne(new QueryWrapper<DiscountMember>().lambda()
|
.eq(DiscountMember::getCode, rec.getOriginCode())
|
.eq(DiscountMember::getMemberId, operator)
|
.eq(DiscountMember::getIsdeleted, Constants.ZERO)
|
.last("limit 1"));
|
if (existCard != null) {
|
log.warn("该券码已开通套餐,跳过重复开卡 originCode={}", rec.getOriginCode());
|
rec.setDiscountMemberId(existCard.getId());
|
douyinVerifyRecordMapper.updateById(rec);
|
return;
|
}
|
|
// ③ 主键
|
String goodsorderId = Constants.getUUID();
|
String discountMemberId = Constants.getUUID();
|
|
// ④ 开 discount_member(复用 createDiscountOrderPay 的开卡段,直接置已支付)
|
DiscountMember dm = new DiscountMember();
|
BeanUtils.copyProperties(discount, dm);
|
dm.setId(discountMemberId);
|
dm.setCreateDate(now);
|
dm.setEditDate(now);
|
dm.setCreator(null);
|
dm.setEditor(null);
|
dm.setMemberId(operator);
|
dm.setCode(rec.getOriginCode()); // 原始券码当票号
|
dm.setGoodsorderId(goodsorderId);
|
dm.setStatus(Constants.ZERO); // 正常,核销即视为已支付
|
// 有效期:useType != 0(非固定时间段)时按购买逻辑计算
|
if (!Constants.equalsInteger(dm.getUseType(), Constants.ZERO)) {
|
if (Constants.equalsInteger(dm.getUseType(), Constants.ONE)) {
|
// 购买后生效:使用开始 = 今天
|
dm.setUseStartDate(DateUtil.StringToDateFormat(DateUtil.getCurrDate(), "yyyy-MM-dd"));
|
}
|
// 使用结束 = 使用开始 + (useDays - 1)
|
dm.setUseEndDate(DateUtil.StringToDateFormat(
|
DateUtil.getXDaysAfter(dm.getUseStartDate(), dm.getUseDays() - 1), "yyyy-MM-dd"));
|
}
|
discountMemberMapper.insert(dm);
|
|
// ⑤ 建 goodsorder(对齐支付回调,直接置已支付)
|
Goodsorder goodsorder = new Goodsorder();
|
goodsorder.setId(goodsorderId);
|
goodsorder.setCode(goodsorderId);
|
goodsorder.setCreateDate(now);
|
goodsorder.setIsdeleted(Constants.ZERO);
|
goodsorder.setMemberId(operator);
|
goodsorder.setType(GOODSORDER_TYPE_DISCOUNT); // 1 套餐卡购买
|
goodsorder.setObjType(GOODSORDER_OBJ_TYPE_DISCOUNT); // 0 套餐卡
|
goodsorder.setObjId(discount.getId());
|
goodsorder.setMoney(BigDecimal.ZERO); // 核销免费兑换,平台无实收
|
goodsorder.setStatus(GOODSORDER_PAID); // 1 已支付
|
goodsorder.setPayStatus(GOODSORDER_PAID); // 1 已支付
|
goodsorder.setPayWay(PAY_WAY_DOUYIN); // 2 抖音券核销
|
goodsorder.setPayDate(now);
|
goodsorder.setInfo("抖音券核销兑换");
|
goodsorderMapper.insert(goodsorder);
|
|
// ⑥ discount_log 开通日志(平台调整)
|
DiscountLog discountLog = new DiscountLog();
|
discountLog.setId(Constants.getUUID());
|
discountLog.setCreateDate(now);
|
discountLog.setCreator(operator);
|
discountLog.setIsdeleted(Constants.ZERO);
|
discountLog.setDiscountMemberId(discountMemberId);
|
discountLog.setGoodsorderId(goodsorderId);
|
discountLog.setType(DISCOUNT_LOG_TYPE_ADJUST); // 2 平台调整
|
discountLog.setInfo("抖音券核销开通,券码 " + rec.getOriginCode());
|
discountLogMapper.insert(discountLog);
|
|
// ⑦ 回填核销记录(商品快照 + 套餐卡ID)
|
rec.setProductId(product.getProductId());
|
rec.setProductName(product.getProductName());
|
if (param.getPayAmount() != null) {
|
rec.setPayAmount(param.getPayAmount());
|
}
|
rec.setDiscountMemberId(discountMemberId);
|
douyinVerifyRecordMapper.updateById(rec);
|
}
|
|
/**
|
* 按 skuId 反查抖音商品(product_id / out_id 的来源)。
|
* 链路:skuId → douyin_product_sku(sku_id,isdeleted=0) → product_id
|
*
|
* @param skuId 核销券对应的抖音 SKU ID
|
* @return 抖音商品;查不到抛「未找到套餐」业务异常(触发整单回滚)
|
*/
|
private DouyinProduct resolveProduct(String skuId) {
|
DouyinProductSku sku = douyinProductSkuMapper.selectOne(new QueryWrapper<DouyinProductSku>().lambda()
|
.eq(DouyinProductSku::getSkuId, skuId)
|
.eq(DouyinProductSku::getIsdeleted, Constants.ZERO)
|
.last("limit 1"));
|
if (sku == null) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到该券对应的本地套餐,请先在管理端绑定");
|
}
|
DouyinProduct product = douyinProductMapper.selectOne(new QueryWrapper<DouyinProduct>().lambda()
|
.eq(DouyinProduct::getProductId, sku.getProductId())
|
.eq(DouyinProduct::getIsdeleted, Constants.ZERO)
|
.last("limit 1"));
|
if (product == null || StringUtils.isBlank(product.getOutId())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到该券对应的本地套餐,请先在管理端绑定");
|
}
|
return product;
|
}
|
|
/**
|
* 按已查到的抖音商品反查本地套餐。
|
* 链路:douyin_product.out_id → discount(id=out_id, status=0 正常, isdeleted=0)
|
*
|
* @param product 已反查到的抖音商品(需有 out_id)
|
* @return 本地套餐;查不到抛「未找到套餐」业务异常
|
*/
|
private Discount resolveDiscount(DouyinProduct product) {
|
Discount discount = discountMapper.selectOne(new QueryWrapper<Discount>().lambda()
|
.eq(Discount::getId, product.getOutId())
|
.eq(Discount::getStatus, Constants.ZERO)
|
.eq(Discount::getIsdeleted, Constants.ZERO)
|
.last("limit 1"));
|
if (discount == null) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到该券对应的本地套餐,请先在管理端绑定");
|
}
|
return discount;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public DouyinVerifyRecord cancel(DouyinCancelParam param, String operator) {
|
if (param == null || StringUtils.isBlank(param.getId())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "id 不能为空");
|
}
|
// 取本地核销记录
|
DouyinVerifyRecord rec = douyinVerifyRecordMapper.selectById(param.getId());
|
if (rec == null || Constants.equalsInteger(rec.getIsdeleted(), Constants.ONE)) {
|
throw new BusinessException(ResponseStatus.DATA_EMPTY);
|
}
|
// 已撤销,防重复操作
|
if (Constants.equalsInteger(rec.getCancelStatus(), Constants.DOUYIN_VERIFY_CANCEL_STATUS.DONE.getKey())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该记录已撤销,请勿重复操作");
|
}
|
// 只有核销成功的记录才能撤销(管理端运营操作,不再受"核销后1小时内"时间窗限制)
|
if (!Constants.equalsInteger(rec.getVerifyStatus(), Constants.DOUYIN_VERIFY_STATUS.SUCCESS.getKey())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅成功核销的记录可撤销");
|
}
|
|
Date now = new Date();
|
|
// 用核销时拿到的标识去抖音撤销
|
DouyinCancelReq req = new DouyinCancelReq();
|
req.setCertificateId(rec.getCertificateId());
|
req.setVerifyId(rec.getVerifyId());
|
req.setAccountId(rec.getAccountId());
|
|
DouyinBaseResp<DouyinCancelResp> resp = douyinClient.cancel(req);
|
// 成功判据:外层 extra 与 data 的 error_code 都为 0
|
Integer extraCode = resp == null || resp.getExtra() == null ? null : resp.getExtra().getErrorCode();
|
Integer dataCode = resp == null || resp.getData() == null ? null : resp.getData().getErrorCode();
|
boolean ok = extraCode != null && extraCode == 0 && dataCode != null && dataCode == 0;
|
|
String respText = JSON.toJSONString(resp);
|
rec.setEditDate(now);
|
rec.setEditor(operator);
|
rec.setRawResponse(respText);
|
// 撤销失败:更新记录描述后抛出(记录保留原核销状态)
|
if (!ok) {
|
String desc = resp == null || resp.getExtra() == null ? "无响应" : resp.getExtra().getDescription();
|
rec.setCancelMsg("撤销失败:" + desc);
|
douyinVerifyRecordMapper.updateById(rec);
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "撤销失败:" + desc);
|
}
|
// 撤销成功:置撤销状态、撤销时间与撤销人
|
rec.setCancelStatus(Constants.DOUYIN_VERIFY_CANCEL_STATUS.DONE.getKey());
|
rec.setCancelTime(now);
|
rec.setCancelUserId(operator);
|
rec.setCancelMsg("撤销成功");
|
douyinVerifyRecordMapper.updateById(rec);
|
// 同步作废本地已开通的套餐卡(防止抖音撤销后套餐仍被使用);参照 backGoodsorder 退卡
|
cancelDiscountMember(rec, operator, now);
|
return rec;
|
}
|
|
/**
|
* 撤销核销后作废关联的套餐卡:仅正常(status=0)套餐卡作废,已作废跳过(幂等);记 discount_log(平台作废)。
|
*
|
* @param rec 核销记录(含 discountMemberId)
|
* @param operator 撤销操作人(管理端 Shiro 登录用户 id)
|
* @param now 撤销时间
|
*/
|
private void cancelDiscountMember(DouyinVerifyRecord rec, String operator, Date now) {
|
if (StringUtils.isBlank(rec.getDiscountMemberId())) {
|
return;
|
}
|
DiscountMember dm = discountMemberMapper.selectById(rec.getDiscountMemberId());
|
// 无套餐卡或已作废,跳过(幂等)
|
if (dm == null || !Constants.equalsInteger(dm.getStatus(), Constants.ZERO)) {
|
return;
|
}
|
discountMemberMapper.update(null, new UpdateWrapper<DiscountMember>().lambda()
|
.set(DiscountMember::getStatus, Constants.ONE) // 1 作废
|
.set(DiscountMember::getEditDate, now)
|
.set(DiscountMember::getEditor, operator)
|
.eq(DiscountMember::getId, dm.getId()));
|
DiscountLog discountLog = new DiscountLog();
|
discountLog.setId(Constants.getUUID());
|
discountLog.setCreateDate(now);
|
discountLog.setCreator(operator);
|
discountLog.setIsdeleted(Constants.ZERO);
|
discountLog.setDiscountMemberId(dm.getId());
|
discountLog.setType(Constants.ONE); // 1 平台作废
|
discountLog.setEditInfo("撤销核销作废");
|
discountLog.setGoodsorderId(dm.getGoodsorderId());
|
discountLogMapper.insert(discountLog);
|
}
|
|
@Override
|
public PageData<DouyinVerifyRecord> findPage(PageWrap<DouyinVerifyRecord> pageWrap) {
|
IPage<DouyinVerifyRecord> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
|
QueryWrapper<DouyinVerifyRecord> wrapper = new QueryWrapper<>();
|
// 仅查未删除
|
wrapper.lambda().eq(DouyinVerifyRecord::getIsdeleted, Constants.ZERO);
|
DouyinVerifyRecord m = pageWrap.getModel();
|
// 按查询条件逐项精确匹配(非空才拼接)
|
if (m != null) {
|
if (StringUtils.isNotBlank(m.getVerifyId())) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getVerifyId, m.getVerifyId());
|
}
|
if (StringUtils.isNotBlank(m.getCertificateId())) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getCertificateId, m.getCertificateId());
|
}
|
if (StringUtils.isNotBlank(m.getOriginCode())) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getOriginCode, m.getOriginCode());
|
}
|
if (StringUtils.isNotBlank(m.getOrderId())) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getOrderId, m.getOrderId());
|
}
|
if (StringUtils.isNotBlank(m.getPoiId())) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getPoiId, m.getPoiId());
|
}
|
if (m.getVerifyStatus() != null) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getVerifyStatus, m.getVerifyStatus());
|
}
|
if (m.getCancelStatus() != null) {
|
wrapper.lambda().eq(DouyinVerifyRecord::getCancelStatus, m.getCancelStatus());
|
}
|
}
|
// 默认按核销时间倒序
|
wrapper.lambda().orderByDesc(DouyinVerifyRecord::getVerifyTime);
|
return PageData.from(douyinVerifyRecordMapper.selectPage(page, wrapper));
|
}
|
|
@Override
|
public PageData<DouyinVerifyRecordPageVO> findManagePage(PageWrap<DouyinVerifyRecordPageVO> pageWrap) {
|
IPage<DouyinVerifyRecordPageVO> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
|
MPJLambdaWrapper<DouyinVerifyRecord> wrapper = new MPJLambdaWrapper<>();
|
// 显式选主表列(避开 product_name 快照,团购商品名改用 join 的 douyin_product.product_name)
|
wrapper.select(DouyinVerifyRecord::getId)
|
.select(DouyinVerifyRecord::getOriginCode)
|
.select(DouyinVerifyRecord::getVerifyTime)
|
.select(DouyinVerifyRecord::getVerifyStatus)
|
.select(DouyinVerifyRecord::getCancelStatus)
|
// 订单编号:discount_member.goodsorder_id(核销时自动建的 goodsorder 订单)
|
.selectAs(DiscountMember::getGoodsorderId, DouyinVerifyRecordPageVO::getOrderCode)
|
// 会员 openid/手机号/兑换人姓名:member
|
.selectAs(Member::getOpenid, DouyinVerifyRecordPageVO::getMemberOpenid)
|
.selectAs(Member::getPhone, DouyinVerifyRecordPageVO::getMemberPhone)
|
.selectAs(Member::getName, DouyinVerifyRecordPageVO::getExchangerName)
|
// 团购商品名/类目:douyin_product(经 product_id 关联,非主键字段)
|
.selectAs(DouyinProduct::getProductName, DouyinVerifyRecordPageVO::getProductName)
|
.selectAs(DouyinProduct::getCategory, DouyinVerifyRecordPageVO::getCategory)
|
// 抖音券名:discount_member.name(本地开通套餐名)
|
.selectAs(DiscountMember::getName, DouyinVerifyRecordPageVO::getCouponName)
|
// 三表 leftJoin:discount_member(经 discount_member_id)→ member(经 member_id);douyin_product(经 product_id)
|
.leftJoin(DiscountMember.class, DiscountMember::getId, DouyinVerifyRecord::getDiscountMemberId)
|
.leftJoin(Member.class, Member::getId, DiscountMember::getMemberId)
|
.leftJoin(DouyinProduct.class, DouyinProduct::getProductId, DouyinVerifyRecord::getProductId)
|
.eq(DouyinVerifyRecord::getIsdeleted, Constants.ZERO);
|
DouyinVerifyRecordPageVO m = pageWrap.getModel();
|
if (m != null) {
|
// 查询条件:抖音券码(精确)、验券状态、撤销状态
|
wrapper.eq(StringUtils.isNotBlank(m.getOriginCode()), DouyinVerifyRecord::getOriginCode, m.getOriginCode())
|
.eq(m.getVerifyStatus() != null, DouyinVerifyRecord::getVerifyStatus, m.getVerifyStatus())
|
.eq(m.getCancelStatus() != null, DouyinVerifyRecord::getCancelStatus, m.getCancelStatus());
|
}
|
wrapper.orderByDesc(DouyinVerifyRecord::getVerifyTime);
|
IPage<DouyinVerifyRecordPageVO> result = douyinVerifyRecordMapper.selectJoinPage(page, DouyinVerifyRecordPageVO.class, wrapper);
|
List<DouyinVerifyRecordPageVO> records = result.getRecords();
|
if (records != null) {
|
for (DouyinVerifyRecordPageVO vo : records) {
|
// 手机号脱敏 + 状态文案(内存回填,非逐行查询)
|
vo.setMemberPhone(maskPhone(vo.getMemberPhone()));
|
vo.setStatusName(statusName(vo.getVerifyStatus(), vo.getCancelStatus()));
|
}
|
}
|
return PageData.from(result);
|
}
|
|
/** 手机号脱敏:138****1234(前3后4,中间4位*);长度 < 7 原样返回 */
|
private String maskPhone(String phone) {
|
if (StringUtils.isBlank(phone) || phone.length() < 7) {
|
return phone;
|
}
|
return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
|
}
|
|
/** 核销状态文案:已撤销 > 核销失败 > 已兑换 */
|
private String statusName(Integer verifyStatus, Integer cancelStatus) {
|
if (Constants.equalsInteger(cancelStatus, Constants.ONE)) {
|
return "已撤销";
|
}
|
if (Constants.equalsInteger(verifyStatus, Constants.ONE)) {
|
return "核销失败";
|
}
|
return "已兑换";
|
}
|
|
@Override
|
public DouyinVerifyRecord findById(String id) {
|
return douyinVerifyRecordMapper.selectById(id);
|
}
|
|
/**
|
* 构造一条核销记录的公共字段(主键 / 时间 / 操作人 / 请求响应快照 / 删除与撤销初值)
|
*/
|
private DouyinVerifyRecord baseRecord(DouyinVerifyReq req, String respText, String operator, Date now) {
|
DouyinVerifyRecord rec = new DouyinVerifyRecord();
|
rec.setId(ID.nextGUID());
|
rec.setVerifyTime(now);
|
rec.setVerifyUserId(operator);
|
rec.setCreateDate(now);
|
rec.setCreator(operator);
|
rec.setIsdeleted(Constants.ZERO);
|
rec.setCancelStatus(Constants.ZERO);
|
rec.setRawRequest(JSON.toJSONString(req));
|
rec.setRawResponse(respText);
|
return rec;
|
}
|
|
/**
|
* 把加密券码列表拼成逗号分隔字符串,便于单字段存储
|
*/
|
private String joinCodes(List<String> codes) {
|
if (codes == null || codes.isEmpty()) {
|
return null;
|
}
|
return String.join(",", codes);
|
}
|
}
|