shinnosuke
2026-09-27 35c0810b268661c79f776fe922f7e3834cba4186
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
@@ -24,6 +24,8 @@
    private static final String TALENT_CODE = "TALENT_CODE";
    private static final String ORDER_CODE = "ORDER_CODE";
    private static final String FREE_STAY_CODE = "FREE_STAY_CODE";
    /** 观影权益(票包兑换码):后台批量生成、绑影城、用户领取后出示二维码,店员扫码核销。 */
    private static final String TICKET_PACKAGE_CODE = "TICKET_PACKAGE_CODE";
    /**
     * 6 位数字核销码:顾客屏幕上的数字码,相机不好用或屏幕反光时手输兜底。
@@ -70,6 +72,10 @@
        Map<String, Object> freeStayCode = findFreeStayCode(token, principal);
        if (freeStayCode != null) {
            return createFreeStayAttempt(principal, freeStayCode);
        }
        Map<String, Object> ticketPackageCode = findTicketPackageCode(token, principal);
        if (ticketPackageCode != null) {
            return createTicketPackageAttempt(token, ticketPackageCode);
        }
        throw new BusinessException(ErrorCode.CONFLICT, "二维码已失效、不可核销或不属于当前商户");
    }
@@ -191,6 +197,47 @@
        return result;
    }
    /**
     * 观影权益兑换码:票包后台批量生成、绑到影城、用户在小程序领取后出示二维码。
     *
     * <p>只认「属于当前商户 + 已发放未核销 + 未过期」的码;核销本身走
     * {@code POST /api/merchant/benefit-verifications/verify}(与后台同一套记录),
     * 这里只做扫码后的资格确认,让店员先看清是谁的券再确认。
     */
    private Map<String, Object> findTicketPackageCode(String token, MerchantPrincipal principal) {
        List<Map<String, Object>> claims = rows("""
                select cl.id claim_id, cl.exchange_code, cl.talent_name, cl.talent_mobile, cl.card_level,
                       cl.valid_end, cl.status claim_status, p.package_name, m.merchant_name
                from ticket_package_claim cl
                join ticket_package p on p.id = cl.package_id and p.deleted = 0
                left join merchant m on m.id = cl.merchant_id and m.deleted = 0
                where cl.exchange_code = ? and cl.deleted = 0 and cl.merchant_id = ?
                limit 1
                """, token, principal.merchantId());
        if (claims.isEmpty()) return null;
        Map<String, Object> claim = claims.get(0);
        if (!"ISSUED".equalsIgnoreCase(value(claim.get("claimStatus"), "").trim())) {
            throw new BusinessException(ErrorCode.CONFLICT, "该观影权益已核销或已失效");
        }
        if (expiredDate(claim.get("validEnd"))) {
            throw new BusinessException(ErrorCode.CONFLICT, "该观影权益已过期");
        }
        return claim;
    }
    private Map<String, Object> createTicketPackageAttempt(String exchangeCode, Map<String, Object> claim) {
        // 这类码没有 attempt 表可用(它只服务动态码/订单码/免费入住券),核销记录由
        // benefit_verify_record 落库,因此这里用「TP+领取记录ID」当核销流水号。
        Map<String, Object> result = scanResult("TP" + number(claim.get("claimId")), TICKET_PACKAGE_CODE,
                "观影权益有效,请确认后核销",
                claim.get("talentName"), claim.get("talentMobile"), claim.get("cardLevel"), null, null, 0L);
        result.put("verifyCode", exchangeCode);
        result.put("packageName", value(claim.get("packageName"), ""));
        result.put("merchantName", value(claim.get("merchantName"), ""));
        result.put("validEnd", claim.get("validEnd") == null ? "" : String.valueOf(claim.get("validEnd")));
        return result;
    }
    private String freeStayBenefitText(Map<String, Object> code, MerchantPrincipal principal) {
        try {
            List<Map<String, Object>> benefits = rows("""