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 位数字核销码:顾客屏幕上的数字码,相机不好用或屏幕反光时手输兜底。
|
*
|
* <p>它就是订单核销码的另一副面孔(同一张 {@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.
|
*
|
* <p>纯 6 位数字的输入走手输核销码那条路:码令牌是 32 位十六进制,不可能是 6 位数字。
|
*/
|
@Transactional
|
public Map<String, Object> scan(MerchantPrincipal principal, VerificationScanRequest request) {
|
String token = request.scanResult().trim();
|
if (NUMERIC_CODE.matcher(token).matches()) {
|
return scanNumericCode(principal, token);
|
}
|
Map<String, Object> talentCode = findTalentCode(token);
|
if (talentCode != null) {
|
return createTalentAttempt(principal, talentCode);
|
}
|
Map<String, Object> orderCode = findOrderCode(token, principal);
|
if (orderCode != null) {
|
return createOrderAttempt(principal, orderCode);
|
}
|
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, "二维码已失效、不可核销或不属于当前商户");
|
}
|
|
/**
|
* 手输 6 位数字核销码。
|
*
|
* <p>{@code display_code} 只有 100 万空间,同一时刻完全可能有两张活着的码撞在一起,所以按
|
* {@code limit 2} 取行:拿不到**唯一**一张就拒绝。而「猜错(0 条)」与「撞码(2 条)」必须用同一句话
|
* 回答——否则攻击者只要比较这两种答复,就知道「这个号确实有主」,那正是枚举要的探针。
|
*
|
* <p>答复里**不带订单号**:客户就在眼前,商户不需要靠接口知道这个码属于哪笔订单;带回去却等于给
|
* 枚举提供了一张「号码 → 订单」的映射表。确认核销时由服务端从扫码尝试里把订单找回来。
|
*/
|
private Map<String, Object> scanNumericCode(MerchantPrincipal principal, String code) {
|
numericCodeGuard.requireUsable(principal);
|
List<Map<String, Object>> codes = rows("""
|
select c.id order_verification_code_id, c.expires_at, o.order_no, o.order_status, o.verify_status,
|
o.merchant_id, o.store_id, coalesce(o.product_name_snapshot, '订单权益') product_name,
|
t.name talent_name, t.mobile talent_mobile, tc.card_level
|
from order_dynamic_verification_code c
|
join benefit_order o on o.id = c.order_id and o.deleted = 0
|
left join talent_user t on t.id = o.talent_id and t.deleted = 0
|
left join talent_card tc on tc.id = o.card_id and tc.deleted = 0
|
where c.display_code = ? and c.status = 0 and c.expires_at > now()
|
and o.merchant_id = ? and (o.store_id is null or o.store_id = ?)
|
and o.order_status = 1 and o.verify_status = 0
|
limit 2
|
""", code, principal.merchantId(), principal.storeId());
|
if (codes.size() != 1) {
|
numericCodeGuard.recordFailure(principal);
|
throw new BusinessException(ErrorCode.CONFLICT, "核销码无效或已过期,请核对后重新输入");
|
}
|
numericCodeGuard.recordSuccess(principal);
|
Map<String, Object> found = codes.get(0);
|
String verificationId = createAttempt(principal, ORDER_CODE, null,
|
number(found.get("orderVerificationCodeId")), null, found.get("expiresAt"));
|
Map<String, Object> result = scanResult(verificationId, ORDER_CODE, "核销码有效,请确认后完成核销",
|
found.get("talentName"), found.get("talentMobile"), found.get("cardLevel"), null, found.get("expiresAt"), 0L);
|
// 连字段都不留:空字符串容易被客户端当成「没查到」,直接不给这个键更清楚。
|
result.remove("orderNo");
|
result.put("numericEntry", true);
|
return result;
|
}
|
|
/**
|
* 免费入住券核销:与支付无关的独立业务——用户不付钱,核销只扣券次数,补贴由平台线下结算。
|
*
|
* <p>订单核销不走这里:它必须写核销记录并确认延迟入账的资金,统一在
|
* {@link OrderVerificationService#verifyByMerchant} 里实现,避免两处各写一遍导致漏改。
|
*/
|
@Transactional
|
public Map<String, Object> confirmFreeStay(MerchantPrincipal principal, VerificationConfirmRequest request, String idempotencyKey) {
|
if (idempotencyKey == null || idempotencyKey.isBlank()) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少幂等键");
|
}
|
List<Map<String, Object>> attempts = rows("""
|
select id attempt_id, credential_type, dynamic_code_id, order_verification_code_id,
|
free_stay_verification_code_id, status attempt_status, verify_no, expires_at
|
from merchant_verification_attempt
|
where verification_id = ? and merchant_id = ? and store_id = ? and operator_id = ?
|
limit 1 for update
|
""", request.verificationId().trim(), principal.merchantId(), principal.storeId(), principal.operatorId());
|
if (attempts.isEmpty()) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "核销凭证不存在或无权操作");
|
}
|
Map<String, Object> attempt = attempts.get(0);
|
if (number(attempt.get("attemptStatus")) == 1) {
|
return confirmed(attempt.get("verifyNo"), null);
|
}
|
String credentialType = String.valueOf(attempt.get("credentialType"));
|
if (!FREE_STAY_CODE.equals(credentialType)) {
|
// 订单核销码要确认资金,必须走订单核销接口;人才码要先发单。
|
throw new BusinessException(ErrorCode.CONFLICT, "该凭证不能在这里核销,请使用对应的核销入口");
|
}
|
// 免费入住券不看扫码码的有效期:码过期只影响扫码时机,券本身的 valid_end 才决定能不能核销
|
// (confirmFreeStayCode 里会校验)。这是既有行为,不要收紧。
|
if (number(attempt.get("attemptStatus")) != 0) {
|
jdbc.update("update merchant_verification_attempt set status = 2 where id = ? and status = 0", number(attempt.get("attemptId")));
|
throw new BusinessException(ErrorCode.CONFLICT, "核销凭证已失效,请重新扫码");
|
}
|
return confirmFreeStayCode(principal, attempt);
|
}
|
|
private Map<String, Object> createTalentAttempt(MerchantPrincipal principal, Map<String, Object> code) {
|
ensureActiveCard(code);
|
Long expectedMerchantId = number(code.get("expectedMerchantId"));
|
if (expectedMerchantId != null && !expectedMerchantId.equals(principal.merchantId())) {
|
throw new BusinessException(ErrorCode.CONFLICT, "该人才码不可在当前商户核销");
|
}
|
if (!manualConsumptionService.hasApplicableRule(principal.merchantId(), value(code.get("cardLevel"), ""), value(code.get("sourceAreaType"), ""))) {
|
throw new BusinessException(ErrorCode.CONFLICT, "当前商户未配置适用于该人才卡的到店消费权益");
|
}
|
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"), 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"), 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"));
|
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));
|
// 免费天数用完后按门店配置的到店消费折扣计价(如「门市价9折」),免费入住确认页要显示这段。
|
result.put("discountText", manualConsumptionService.freeStayExceedDiscountText(
|
principal.merchantId(), value(code.get("cardLevel"), ""), value(code.get("sourceAreaType"), "")));
|
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("""
|
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) {
|
String verificationId = BusinessIdGenerator.next("MV");
|
jdbc.update("""
|
insert into merchant_verification_attempt(verification_id, dynamic_code_id, credential_type,
|
order_verification_code_id, free_stay_verification_code_id,
|
merchant_id, store_id, operator_id, expires_at)
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
""", verificationId, dynamicCodeId, type, orderCodeId, freeStayCodeId,
|
principal.merchantId(), principal.storeId(), principal.operatorId(), expiresAt);
|
return verificationId;
|
}
|
|
private Map<String, Object> confirmTalentCode(MerchantPrincipal principal, Map<String, Object> attempt) {
|
List<Map<String, Object>> codes = rows("""
|
select d.id dynamic_code_id, d.card_id, d.talent_id, c.card_level, d.expires_at,
|
c.status card_status, c.valid_end, t.status talent_status
|
from talent_dynamic_code d
|
join talent_card c on c.id = d.card_id and c.deleted = 0
|
join talent_user t on t.id = d.talent_id and t.deleted = 0
|
where d.id = ? and d.status = 0
|
limit 1 for update
|
""", number(attempt.get("dynamicCodeId")));
|
if (codes.isEmpty()) {
|
throw new BusinessException(ErrorCode.CONFLICT, "人才码已被使用或已失效");
|
}
|
Map<String, Object> code = codes.get(0);
|
ensureActiveCard(code);
|
int updated = jdbc.update("""
|
update talent_dynamic_code set status = 1, merchant_id = ?, used_at = now()
|
where id = ? and status = 0 and expires_at > now()
|
""", principal.merchantId(), number(code.get("dynamicCodeId")));
|
if (updated == 0) {
|
throw new BusinessException(ErrorCode.CONFLICT, "人才码已被使用或已失效");
|
}
|
String verifyNo = insertRecord(principal, null, number(code.get("talentId")), number(code.get("cardId")), null,
|
TALENT_CODE, 0L, 0L, null);
|
finishAttempt(attempt, verifyNo);
|
return confirmed(verifyNo, code.get("cardLevel"));
|
}
|
|
|
private Map<String, Object> confirmFreeStayCode(MerchantPrincipal principal, Map<String, Object> attempt) {
|
List<Map<String, Object>> codes = rows("""
|
select c.id free_stay_verification_code_id, c.expires_at, v.id voucher_id, v.talent_id, v.card_id,
|
v.merchant_id, v.total_count, v.used_count, v.valid_end, v.status voucher_status, tc.card_level
|
from free_stay_verification_code c
|
join talent_free_stay_voucher v on v.id = c.voucher_id
|
left join talent_card tc on tc.id = v.card_id and tc.deleted = 0
|
where c.id = ? and c.status in (0, 2)
|
limit 1 for update
|
""", number(attempt.get("freeStayVerificationCodeId")));
|
if (codes.isEmpty()) {
|
throw new BusinessException(ErrorCode.CONFLICT, "免费入住核销码已被使用或已失效");
|
}
|
Map<String, Object> code = codes.get(0);
|
if (!principal.merchantId().equals(number(code.get("merchantId"))) || number(code.get("voucherStatus")) != 1
|
|| expiredDate(code.get("validEnd")) || number(code.get("usedCount")) >= number(code.get("totalCount"))) {
|
throw new BusinessException(ErrorCode.CONFLICT, "免费入住券当前不可核销");
|
}
|
int codeUpdated = jdbc.update("""
|
update free_stay_verification_code set status = 1
|
where id = ? and status in (0, 2)
|
""", number(code.get("freeStayVerificationCodeId")));
|
int voucherUpdated = jdbc.update("""
|
update talent_free_stay_voucher set used_count = used_count + 1
|
where id = ? and status = 1 and valid_end >= current_date and used_count < total_count
|
""", number(code.get("voucherId")));
|
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, subsidyAmount, "免费入住券(平台线下补贴)");
|
finishAttempt(attempt, verifyNo);
|
return confirmed(verifyNo, code.get("cardLevel"));
|
}
|
|
private String insertRecord(MerchantPrincipal principal, Long orderId, Long talentId, Long cardId, Long benefitId,
|
String verifyType, Long verifyAmount, Long subsidyAmount, String remark) {
|
String verifyNo = BusinessIdGenerator.next("V");
|
jdbc.update("""
|
insert into verify_record(verify_no, order_id, talent_id, card_id, benefit_id, merchant_id, store_id,
|
staff_id, verify_type, verify_amount, subsidy_amount, remark)
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
""", verifyNo, orderId, talentId, cardId, benefitId, principal.merchantId(), principal.storeId(),
|
principal.operatorId(), verifyType, verifyAmount, subsidyAmount, remark);
|
return verifyNo;
|
}
|
|
private void finishAttempt(Map<String, Object> attempt, String verifyNo) {
|
jdbc.update("""
|
update merchant_verification_attempt
|
set status = 1, verify_no = ?, confirmed_at = now()
|
where id = ? and status = 0
|
""", verifyNo, number(attempt.get("attemptId")));
|
}
|
|
private Map<String, Object> findTalentCode(String token) {
|
List<Map<String, Object>> codes = rows("""
|
select d.id dynamic_code_id, d.expected_merchant_id, d.expires_at,
|
c.id card_id, c.card_level, c.status card_status, c.valid_end,
|
t.id talent_id, t.name talent_name, t.mobile talent_mobile, t.status talent_status, t.source_area_type
|
from talent_dynamic_code d
|
join talent_card c on c.id = d.card_id and c.deleted = 0
|
join talent_user t on t.id = d.talent_id and t.deleted = 0
|
where d.code_token = ? and d.status = 0 and d.expires_at > now()
|
limit 1
|
""", token);
|
return codes.isEmpty() ? null : codes.get(0);
|
}
|
|
private Map<String, Object> findOrderCode(String token, MerchantPrincipal principal) {
|
List<Map<String, Object>> codes = rows("""
|
select c.id order_verification_code_id, c.expires_at, o.order_no, o.order_status, o.verify_status,
|
o.merchant_id, o.store_id, coalesce(o.product_name_snapshot, b.benefit_name, '订单权益') product_name,
|
t.name talent_name, t.mobile talent_mobile, tc.card_level
|
from order_dynamic_verification_code c
|
join benefit_order o on o.id = c.order_id and o.deleted = 0
|
left join benefit b on b.id = o.benefit_id
|
left join talent_user t on t.id = o.talent_id and t.deleted = 0
|
left join talent_card tc on tc.id = o.card_id and tc.deleted = 0
|
where c.code_token = ? and c.status = 0 and c.expires_at > now()
|
and o.merchant_id = ? and (o.store_id is null or o.store_id = ?)
|
and o.order_status = 1 and o.verify_status = 0
|
limit 1
|
""", token, principal.merchantId(), principal.storeId());
|
return codes.isEmpty() ? null : codes.get(0);
|
}
|
|
private Map<String, Object> findFreeStayCode(String token, MerchantPrincipal principal) {
|
// source_area_type 必须一起选:酒店按「本地/外地」分等级配置时,确认页要显示的
|
// 「超过天数按门市价X折」就是靠它去匹配对应等级的折扣,漏了会拿不到折扣文案。
|
List<Map<String, Object>> codes = rows("""
|
select c.id free_stay_verification_code_id, c.expires_at, v.total_count, v.used_count, v.valid_end,
|
v.status voucher_status, v.merchant_id, t.name talent_name, t.mobile talent_mobile, tc.card_level,
|
t.source_area_type
|
from free_stay_verification_code c
|
join talent_free_stay_voucher v on v.id = c.voucher_id
|
join talent_user t on t.id = v.talent_id and t.deleted = 0
|
left join talent_card tc on tc.id = v.card_id and tc.deleted = 0
|
where c.code_token = ? and c.status = 0 and c.expires_at > now()
|
and v.merchant_id = ? and v.status = 1 and v.valid_end >= current_date and v.used_count < v.total_count
|
limit 1
|
""", token, principal.merchantId());
|
return codes.isEmpty() ? null : codes.get(0);
|
}
|
|
private Map<String, Object> scanResult(String verificationId, String credentialType, String message,
|
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);
|
result.put("status", "pending_confirmation");
|
result.put("title", "核验通过");
|
result.put("message", message);
|
result.put("canVerify", true);
|
result.put("customerName", maskName(value(talentName, "")));
|
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);
|
result.put("verifyNo", verifyNo == null ? "" : String.valueOf(verifyNo));
|
result.put("status", "verified");
|
result.put("message", "核销成功");
|
result.put("cardLevel", cardLevel == null ? "人才卡" : String.valueOf(cardLevel));
|
result.put("verifiedAt", LocalDateTime.now().toString());
|
return result;
|
}
|
|
private boolean belongsToPrincipal(Map<String, Object> order, MerchantPrincipal principal) {
|
Long storeId = number(order.get("storeId"));
|
return principal.merchantId().equals(number(order.get("merchantId")))
|
&& (storeId == null || principal.storeId().equals(storeId));
|
}
|
|
private void ensureActiveCard(Map<String, Object> code) {
|
if (!Long.valueOf(1L).equals(number(code.get("talentStatus")))
|
|| !Long.valueOf(1L).equals(number(code.get("cardStatus")))
|
|| expiredDate(code.get("validEnd"))) {
|
throw new BusinessException(ErrorCode.CONFLICT, "人才卡当前不可核销");
|
}
|
}
|
|
private boolean expired(Object value) {
|
return value instanceof LocalDateTime dateTime && !dateTime.isAfter(LocalDateTime.now());
|
}
|
|
private boolean expiredDate(Object value) {
|
return value instanceof LocalDate date && date.isBefore(LocalDate.now());
|
}
|
|
private Long number(Object value) {
|
return value instanceof Number number ? number.longValue() : null;
|
}
|
|
private String value(Object value, String fallback) {
|
return value == null ? fallback : String.valueOf(value);
|
}
|
|
private String maskName(String name) {
|
if (name == null || name.isBlank()) return "*";
|
return name.length() == 1 ? name + "*" : name.substring(0, 1) + "*".repeat(name.length() - 1);
|
}
|
|
private String maskMobile(String mobile) {
|
if (mobile == null || mobile.length() < 7) return "";
|
return mobile.substring(0, 3) + "****" + mobile.substring(mobile.length() - 4);
|
}
|
|
private List<Map<String, Object>> rows(String sql, Object... args) {
|
return DatabaseRows.normalize(jdbc.queryForList(sql, args));
|
}
|
}
|