shinnosuke
2026-08-31 cb5a42c97fe9ae23b891bdc3231dc8eb100de218
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
@@ -1,5 +1,7 @@
package com.talentcard.server.business.merchant;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
@@ -11,6 +13,7 @@
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -94,22 +97,42 @@
        }
        String verificationId = createAttempt(principal, TALENT_CODE, number(code.get("dynamicCodeId")), null, null, code.get("expiresAt"));
        return scanResult(verificationId, TALENT_CODE, "人才码有效,请录入消费金额并发送待支付订单", code.get("talentName"),
                code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"));
                code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"), 0L);
    }
    private Map<String, Object> createOrderAttempt(MerchantPrincipal principal, Map<String, Object> code) {
        String verificationId = createAttempt(principal, ORDER_CODE, null, number(code.get("orderVerificationCodeId")), null, code.get("expiresAt"));
        String title = value(code.get("productName"), "订单权益");
        return scanResult(verificationId, ORDER_CODE, "订单核销码有效,请确认后完成核销:" + title,
                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), code.get("orderNo"), code.get("expiresAt"));
                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), code.get("orderNo"), code.get("expiresAt"), 0L);
    }
    private Map<String, Object> createFreeStayAttempt(MerchantPrincipal principal, Map<String, Object> code) {
        Long remaining = number(code.get("totalCount")) - number(code.get("usedCount"));
        String verificationId = createAttempt(principal, FREE_STAY_CODE, null, null,
                number(code.get("freeStayVerificationCodeId")), code.get("expiresAt"));
        return scanResult(verificationId, FREE_STAY_CODE, "免费入住券有效,剩余 " + remaining + " 次,请确认后核销",
                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"));
        Map<String, Object> result = scanResult(verificationId, FREE_STAY_CODE, "免费入住券有效,剩余 " + remaining + " 次,请确认后核销",
                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"),
                hotelSubsidyAmountCents());
        result.put("freeStayTotalCount", number(code.get("totalCount")));
        result.put("freeStayRemainingCount", Math.max(remaining, 0L));
        result.put("benefitText", freeStayBenefitText(code, principal));
        return result;
    }
    private String freeStayBenefitText(Map<String, Object> code, MerchantPrincipal principal) {
        try {
            List<Map<String, Object>> benefits = rows("""
                    select benefit_text
                    from merchant_card_benefit
                    where merchant_id = ? and card_level = ? and status = 1
                    limit 1
                    """, principal.merchantId(), value(code.get("cardLevel"), ""));
            return benefits.isEmpty() ? "" : value(benefits.get(0).get("benefitText"), "");
        } catch (DataAccessException ignored) {
            // Older installations may not have the optional display table yet.
            return "";
        }
    }
    private String createAttempt(MerchantPrincipal principal, String type, Long dynamicCodeId, Long orderCodeId, Long freeStayCodeId, Object expiresAt) {
@@ -217,8 +240,9 @@
        if (codeUpdated == 0 || voucherUpdated == 0) {
            throw new BusinessException(ErrorCode.CONFLICT, "免费入住券已被使用或已失效");
        }
        Long subsidyAmount = hotelSubsidyAmountCents();
        String verifyNo = insertRecord(principal, null, number(code.get("talentId")), number(code.get("cardId")), null,
                "HOTEL_FREE", 0L, 0L, "免费入住券");
                "HOTEL_FREE", 0L, subsidyAmount, "免费入住券(平台线下补贴)");
        finishAttempt(attempt, verifyNo);
        return confirmed(verifyNo, code.get("cardLevel"));
    }
@@ -291,7 +315,8 @@
    }
    private Map<String, Object> scanResult(String verificationId, String credentialType, String message,
                                            Object talentName, Object talentMobile, Object cardLevel, Object orderNo, Object expiresAt) {
                                            Object talentName, Object talentMobile, Object cardLevel, Object orderNo,
                                            Object expiresAt, Long subsidyAmount) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("verificationId", verificationId);
        result.put("credentialType", credentialType);
@@ -303,10 +328,36 @@
        result.put("customerMobile", maskMobile(value(talentMobile, "")));
        result.put("cardLevel", cardLevel == null ? "人才卡" : String.valueOf(cardLevel));
        result.put("orderNo", orderNo == null ? "" : String.valueOf(orderNo));
        result.put("subsidyAmount", subsidyAmount == null ? 0L : subsidyAmount);
        result.put("expiresAt", expiresAt);
        return result;
    }
    private Long hotelSubsidyAmountCents() {
        List<Map<String, Object>> settings = rows("""
                select setting_value
                from admin_setting
                where setting_key = 'hotelSubsidy'
                limit 1
                """);
        if (settings.isEmpty()) {
            return 0L;
        }
        String configuredAmount = value(settings.get(0).get("settingValue"), "").trim();
        if (configuredAmount.isEmpty()) {
            return 0L;
        }
        try {
            BigDecimal yuan = new BigDecimal(configuredAmount).setScale(2, RoundingMode.UNNECESSARY);
            if (yuan.signum() < 0) {
                throw new ArithmeticException("negative subsidy");
            }
            return yuan.movePointRight(2).longValueExact();
        } catch (ArithmeticException | NumberFormatException exception) {
            throw new BusinessException(ErrorCode.CONFLICT, "酒店免费入住补贴金额配置不正确,请联系平台管理员");
        }
    }
    private Map<String, Object> confirmed(Object verifyNo, Object cardLevel) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("verified", true);