rk
9 小时以前 cf1d82548a1bd8155ffe9b486df8167aa9e63a7d
server/services/src/main/java/com/doumee/service/business/impl/DouyinVerifyServiceImpl.java
@@ -9,6 +9,7 @@
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.DouyinBoundProduct;
import com.doumee.core.douyin.dto.DouyinCancelParam;
import com.doumee.core.douyin.dto.DouyinCancelReq;
import com.doumee.core.douyin.dto.DouyinCancelResp;
@@ -135,7 +136,7 @@
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public DouyinVerifyRecord verify(DouyinVerifyParam param, String operator) {
    public DouyinVerifyRecord verify(DouyinVerifyParam param, String operator, DouyinBoundProduct boundProduct) {
        // 入参校验
        if (param == null || StringUtils.isBlank(param.getVerifyToken())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "verifyToken 不能为空");
@@ -153,6 +154,10 @@
        if (StringUtils.isBlank(param.getSkuId())) {
            // 无 skuId 则无法反查套餐(核销返回本身不含商品标识),直接拦截
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "skuId 不能为空");
        }
        if (boundProduct == null || boundProduct.getProduct() == null || boundProduct.getDiscount() == null) {
            // 兜底:套餐绑定结果缺失(scanVerify 应已校验,这里防 NPE)
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到该券对应的本地套餐,请先在管理端绑定");
        }
        Date now = new Date();
@@ -204,8 +209,16 @@
        }
        // 核销成功:为当前登录人开通套餐(任一步失败 → 整单回滚)
        openDiscountForVerify(rec, param, operator);
        // 注:scanVerify 已在核销前校验过套餐绑定并透传结果,这里不再重复查询
        openDiscountForVerify(rec, param, boundProduct, operator);
        return rec;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public DouyinVerifyRecord verify(DouyinVerifyParam param, String operator) {
        // 无预校验结果的兼容入口:核销前先校验套餐绑定(核销动作在重载内完成)
        return verify(param, operator, resolveBoundProduct(param.getSkuId()));
    }
    /**
@@ -215,19 +228,23 @@
     *
     * @param rec      核销记录(含 originCode/certificateId 等抖音标识,开通后回填套餐卡ID)
     * @param param    核销入参(含 skuId 反查套餐、payAmount 快照)
     * @param boundProduct 核销前已校验的抖音商品 + 本地套餐(避免重复查询)
     * @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);
    private void openDiscountForVerify(DouyinVerifyRecord rec, DouyinVerifyParam param,
                                       DouyinBoundProduct boundProduct, String operator) {
        // 商品 + 套餐已由 scanVerify 核销前校验(resolveBoundProduct)查出并透传,直接使用
        DouyinProduct product = boundProduct.getProduct();
        Discount discount = boundProduct.getDiscount();
        Date now = new Date();
        // ② 防重:同一券码已为该用户开过套餐卡则跳过(避免重复核销重开)
        // ② 防重:同一券码已为该用户开过「正常」套餐卡则跳过(避免重复核销重开)。
        //    注意:已作废(status=1,如撤销核销后)的卡不参与防重——撤销后重新核销应正常开新卡。
        DiscountMember existCard = discountMemberMapper.selectOne(new QueryWrapper<DiscountMember>().lambda()
                .eq(DiscountMember::getCode, rec.getOriginCode())
                .eq(DiscountMember::getMemberId, operator)
                .eq(DiscountMember::getStatus, Constants.ZERO)
                .eq(DiscountMember::getIsdeleted, Constants.ZERO)
                .last("limit 1"));
        if (existCard != null) {
@@ -340,13 +357,23 @@
    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
    public DouyinBoundProduct resolveBoundProduct(String skuId) {
        if (StringUtils.isBlank(skuId)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "skuId 不能为空");
        }
        // 复用既有校验链路:skuId → 抖音商品(在库 + 已绑 out_id)→ 本地套餐(有效)
        DouyinProduct product = resolveProduct(skuId);
        Discount discount = resolveDiscount(product);
        return new DouyinBoundProduct(product, discount);
    }
    @Override
@@ -389,10 +416,13 @@
        rec.setRawResponse(respText);
        // 撤销失败:更新记录描述后抛出(记录保留原核销状态)
        if (!ok) {
            // 失败原因:优先 extra.description,有 sub_description 时追加(sub_description 更具体)
            String desc = resp == null || resp.getExtra() == null ? "无响应" : resp.getExtra().getDescription();
            rec.setCancelMsg("撤销失败:" + desc);
            String subDesc = resp == null || resp.getExtra() == null ? null : resp.getExtra().getSubDescription();
            String failMsg = StringUtils.isBlank(subDesc) ? desc : desc + ";" + subDesc;
            rec.setCancelMsg("撤销失败:" + failMsg);
            douyinVerifyRecordMapper.updateById(rec);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "撤销失败:" + desc);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "撤销失败:" + failMsg);
        }
        // 撤销成功:置撤销状态、撤销时间与撤销人
        rec.setCancelStatus(Constants.DOUYIN_VERIFY_CANCEL_STATUS.DONE.getKey());
@@ -403,6 +433,15 @@
        // 同步作废本地已开通的套餐卡(防止抖音撤销后套餐仍被使用);参照 backGoodsorder 退卡
        cancelDiscountMember(rec, operator, now);
        return rec;
    }
    @Override
    public void fillPackageInfo(DouyinVerifyRecord record) {
        // 无套餐卡ID或查不到时置 null,不影响主流程(scanVerify 用于前端展示套餐信息)
        if (record == null || StringUtils.isBlank(record.getDiscountMemberId())) {
            return;
        }
        record.setPackageInfo(discountMemberMapper.selectById(record.getDiscountMemberId()));
    }
    /**
@@ -495,6 +534,8 @@
                .selectAs(DouyinProduct::getCategory, DouyinVerifyRecordPageVO::getCategory)
                // 抖音券名:discount_member.name(本地开通套餐名)
                .selectAs(DiscountMember::getName, DouyinVerifyRecordPageVO::getCouponName)
                // 套餐使用次数:子查询统计该套餐卡在 member_rides 的骑行记录数(t 为主表 douyin_verify_record)
                .select(" ( select count(1) from member_rides where discount_member_id = t.discount_member_id and isdeleted = 0 ) ", DouyinVerifyRecordPageVO::getUseCount)
                // 三表 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)