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 实现。
*
覆盖验券三步链路: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 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);
}
/**
* 验券(核销),成功后为当前操作人开通套餐卡(整单事务)。
* 方法级事务 {@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 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 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 反查本地套餐并为当前登录人开通套餐卡(含订单与开通日志)。
* 任一步失败抛异常 → 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().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().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().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().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 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().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 findPage(PageWrap pageWrap) {
IPage page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
QueryWrapper 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 findManagePage(PageWrap pageWrap) {
IPage page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
MPJLambdaWrapper 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 result = douyinVerifyRecordMapper.selectJoinPage(page, DouyinVerifyRecordPageVO.class, wrapper);
List 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 codes) {
if (codes == null || codes.isEmpty()) {
return null;
}
return String.join(",", codes);
}
}