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;
import java.util.List;
import java.util.Map;
import com.talentcard.server.business.BusinessIdGenerator;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
import com.talentcard.server.business.order.OrderVerificationService;
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;
@Service
public class MerchantVerificationService {
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 位数字核销码:顾客屏幕上的数字码,相机不好用或屏幕反光时手输兜底。
*
*
它就是订单核销码的另一副面孔(同一张 {@code order_dynamic_verification_code} 的
* {@code display_code}),因此有效期与动态码完全相同;但它只有 100 万空间,必须限次,
* 见 {@link MerchantNumericCodeAttemptGuard}。
*/
private static final java.util.regex.Pattern NUMERIC_CODE = java.util.regex.Pattern.compile("\\d{6}");
private final JdbcTemplate jdbc;
private final ManualConsumptionService manualConsumptionService;
private final OrderVerificationService orderVerificationService;
private final MerchantNumericCodeAttemptGuard numericCodeGuard;
public MerchantVerificationService(JdbcTemplate jdbc, ManualConsumptionService manualConsumptionService,
OrderVerificationService orderVerificationService,
MerchantNumericCodeAttemptGuard numericCodeGuard) {
this.jdbc = jdbc;
this.manualConsumptionService = manualConsumptionService;
this.orderVerificationService = orderVerificationService;
this.numericCodeGuard = numericCodeGuard;
}
/**
* A QR token is opaque to the client. Its source table is the authority for its type and scope.
*
*
纯 6 位数字的输入走手输核销码那条路:码令牌是 32 位十六进制,不可能是 6 位数字。
*/
@Transactional
public Map scan(MerchantPrincipal principal, VerificationScanRequest request) {
String token = request.scanResult().trim();
if (NUMERIC_CODE.matcher(token).matches()) {
return scanNumericCode(principal, token);
}
Map talentCode = findTalentCode(token);
if (talentCode != null) {
return createTalentAttempt(principal, talentCode);
}
Map orderCode = findOrderCode(token, principal);
if (orderCode != null) {
return createOrderAttempt(principal, orderCode);
}
Map freeStayCode = findFreeStayCode(token, principal);
if (freeStayCode != null) {
return createFreeStayAttempt(principal, freeStayCode);
}
Map ticketPackageCode = findTicketPackageCode(token, principal);
if (ticketPackageCode != null) {
return createTicketPackageAttempt(token, ticketPackageCode);
}
throw new BusinessException(ErrorCode.CONFLICT, "二维码已失效、不可核销或不属于当前商户");
}
/**
* 手输 6 位数字核销码。
*
* {@code display_code} 只有 100 万空间,同一时刻完全可能有两张活着的码撞在一起,所以按
* {@code limit 2} 取行:拿不到**唯一**一张就拒绝。而「猜错(0 条)」与「撞码(2 条)」必须用同一句话
* 回答——否则攻击者只要比较这两种答复,就知道「这个号确实有主」,那正是枚举要的探针。
*
*
答复里**不带订单号**:客户就在眼前,商户不需要靠接口知道这个码属于哪笔订单;带回去却等于给
* 枚举提供了一张「号码 → 订单」的映射表。确认核销时由服务端从扫码尝试里把订单找回来。
*/
private Map scanNumericCode(MerchantPrincipal principal, String code) {
numericCodeGuard.requireUsable(principal);
List