package com.talentcard.server.business.merchant; import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Duration; import java.math.BigDecimal; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.business.PageResult; import com.talentcard.server.business.merchant.auth.MerchantPrincipal; import com.talentcard.server.business.order.OrderFundBoundaryService; import com.talentcard.server.business.payment.PaymentFundConfirmationService; import com.talentcard.server.business.portal.ProductPriceCalculator; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.ObjectMapper; @Service public class MerchantWorkService { private static final long REVOCATION_HOURS = 3; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final TypeReference>> RIGHTS_TYPE = new TypeReference<>() {}; private final JdbcTemplate jdbcTemplate; private final ManualConsumptionService manualConsumptionService; private final OrderFundBoundaryService orderFundBoundary; private final PaymentFundConfirmationService fundConfirmation; public MerchantWorkService(JdbcTemplate jdbcTemplate, ManualConsumptionService manualConsumptionService, OrderFundBoundaryService orderFundBoundary, PaymentFundConfirmationService fundConfirmation) { this.jdbcTemplate = jdbcTemplate; this.manualConsumptionService = manualConsumptionService; this.orderFundBoundary = orderFundBoundary; this.fundConfirmation = fundConfirmation; } @Transactional(readOnly = true) public PageResult> orders(Long merchantId, Long storeId, int page, int size, MerchantOrderQuery query) { if (query != null && "stay".equals(normalize(query.category()))) { return freeStayOrders(merchantId, storeId, page, size, query); } int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 100); OrderFilters filters = filters(query); String conditions = " where o.deleted = 0 and o.merchant_id = ? and (o.store_id is null or o.store_id = ?)" + filters.conditions(); List arguments = new ArrayList<>(); arguments.add(merchantId); arguments.add(storeId); arguments.addAll(filters.arguments()); String recordSql = """ select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level, b.benefit_name, o.product_name_snapshot, o.order_type, o.order_amount, o.subsidy_amount, o.pay_amount, o.order_status, o.verify_status, o.created_at, verified.verify_time from benefit_order o left join talent_user tu on tu.id = o.talent_id left join talent_card tc on tc.id = o.card_id left join benefit b on b.id = o.benefit_id left join (select order_id, max(verify_time) verify_time from verify_record where status = 1 group by order_id) verified on verified.order_id = o.id """ + conditions + " order by o.created_at desc, o.id desc limit ? offset ?"; List recordArguments = new ArrayList<>(arguments); recordArguments.add(safeSize); recordArguments.add((safePage - 1) * safeSize); List> records = query(recordSql, recordArguments.toArray()).stream().map(this::toListItem).toList(); Long total = jdbcTemplate.queryForObject("select count(*) from benefit_order o" + conditions, Long.class, arguments.toArray()); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } public PageResult> orders(Long merchantId, Long storeId, int page, int size) { return orders(merchantId, storeId, page, size, new MerchantOrderQuery(null, null, null, null)); } @Transactional(readOnly = true) public Map order(String orderNo, Long merchantId, Long storeId) { List> rows = query(""" select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, o.card_id, tc.card_no, coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level, o.benefit_id, b.benefit_name, o.merchant_id, m.merchant_name, m.merchant_rights_config, o.product_id, o.product_name_snapshot, p.rights_config, o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount, o.subsidy_amount, o.pay_amount, o.order_status, o.verify_status, o.withdrawal_status, o.created_at, vr.verify_time, vr.status as verify_record_status from benefit_order o left join talent_user tu on tu.id = o.talent_id left join talent_card tc on tc.id = o.card_id left join benefit b on b.id = o.benefit_id left join merchant m on m.id = o.merchant_id left join merchant_product p on p.id = o.product_id and p.deleted = 0 left join verify_record vr on vr.order_id = o.id and vr.status = 1 where o.deleted = 0 and o.order_no = ? and o.merchant_id = ? and (o.store_id is null or o.store_id = ?) """, orderNo, merchantId, storeId); if (rows.isEmpty()) { List> freeStayRows = query(""" select r.id, r.verify_no as order_no, r.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, r.card_id, tc.card_no, tc.card_level, r.merchant_id, m.merchant_name, m.merchant_rights_config, '免费入住' as benefit_name, '1天免费入住' as product_name_snapshot, 'FREE_STAY' as order_type, 0 as order_amount, 0 as base_pay_amount, 0 as coupon_amount, r.subsidy_amount, 0 as pay_amount, 1 as order_status, 1 as verify_status, r.verify_time as created_at, r.verify_time, r.status as verify_record_status from verify_record r left join talent_user tu on tu.id = r.talent_id left join talent_card tc on tc.id = r.card_id left join merchant m on m.id = r.merchant_id where r.verify_no = ? and r.verify_type = 'HOTEL_FREE' and r.status = 1 and r.merchant_id = ? and (r.store_id is null or r.store_id = ?) limit 1 """, orderNo, merchantId, storeId); if (freeStayRows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在"); return enrichOrder(freeStayRows.get(0)); } return enrichOrder(rows.get(0)); } private PageResult> freeStayOrders(Long merchantId, Long storeId, int page, int size, MerchantOrderQuery query) { int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 100); StringBuilder condition = new StringBuilder(" where r.verify_type = 'HOTEL_FREE' and r.status = 1 and r.merchant_id = ? and (r.store_id is null or r.store_id = ?)"); List arguments = new ArrayList<>(); arguments.add(merchantId); arguments.add(storeId); LocalDate startDate = date(query.startDate(), "开始日期"); LocalDate endDate = date(query.endDate(), "结束日期"); if (startDate != null && endDate != null && startDate.isAfter(endDate)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "开始日期不能晚于结束日期"); } if (startDate != null) { condition.append(" and r.verify_time >= ?"); arguments.add(Timestamp.valueOf(startDate.atStartOfDay())); } if (endDate != null) { condition.append(" and r.verify_time < ?"); arguments.add(Timestamp.valueOf(endDate.plusDays(1).atStartOfDay())); } String select = """ select r.id, r.verify_no as order_no, r.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, r.card_id, tc.card_level, r.merchant_id, m.merchant_name, '免费入住' as benefit_name, '1天免费入住' as product_name_snapshot, 'FREE_STAY' as order_type, 0 as order_amount, 0 as base_pay_amount, 0 as coupon_amount, r.subsidy_amount, 0 as pay_amount, 1 as order_status, 1 as verify_status, r.verify_time as created_at, r.verify_time from verify_record r left join talent_user tu on tu.id = r.talent_id left join talent_card tc on tc.id = r.card_id left join merchant m on m.id = r.merchant_id """ + condition + " order by r.verify_time desc, r.id desc limit ? offset ?"; List recordArguments = new ArrayList<>(arguments); recordArguments.add(safeSize); recordArguments.add((safePage - 1) * safeSize); List> records = query(select, recordArguments.toArray()).stream().map(this::toListItem).toList(); Long total = jdbcTemplate.queryForObject("select count(*) from verify_record r" + condition, Long.class, arguments.toArray()); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } @Transactional public Map verify(String orderNo, MerchantPrincipal principal, VerifyRequest request, String idempotencyKey) { if (idempotencyKey == null || idempotencyKey.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少幂等键"); } Map order = order(orderNo, principal.merchantId(), principal.storeId()); // 先锁订单行,再复查幂等键和资金状态:并发核销(同键或不同键)都排队在这把锁上, // 后来者要么拿到首次结果,要么看到订单已核销。 OrderFundBoundaryService.LockedOrder locked = orderFundBoundary.lock(number(order.get("id"))); List> repeated = query(""" select order_id from verify_record where idempotency_key = ? and merchant_id = ? limit 1 """, idempotencyKey.trim(), principal.merchantId()); if (!repeated.isEmpty()) { if (!String.valueOf(order.get("id")).equals(String.valueOf(repeated.get(0).get("orderId")))) { throw new BusinessException(ErrorCode.CONFLICT, "幂等键已用于其他订单"); } return order(orderNo, principal.merchantId(), principal.storeId()); } locked.requireVerifiable(); String verifyNo = BusinessIdGenerator.next("V"); jdbcTemplate.update(""" insert into verify_record(verify_no, order_id, talent_id, card_id, benefit_id, merchant_id, staff_id, verify_type, verify_amount, subsidy_amount, remark, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, verifyNo, order.get("id"), order.get("talentId"), order.get("cardId"), order.get("benefitId"), order.get("merchantId"), principal.operatorId(), request.verifyType(), order.get("payAmount"), order.get("subsidyAmount"), request.remark(), idempotencyKey.trim()); int updated = jdbcTemplate.update("update benefit_order set verify_status = 1 where id = ? and verify_status = 0", locked.orderId()); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "订单已核销"); } // 核销通过后把钱放给商户:延迟入账的支付此前停在通道「待确认」,确认成功商户才真正拿到钱。 // 确认失败不会回滚核销(用户的服务已经核销掉了),资金保持待确认并留下记录供重试。 fundConfirmation.confirmForOrder(locked.orderId()); return order(orderNo, principal.merchantId(), principal.storeId()); } @Transactional public Map revokeVerification(MerchantPrincipal principal, MerchantRevokeVerificationRequest request, String idempotencyKey) { if (idempotencyKey == null || idempotencyKey.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少幂等键"); } String normalizedIdempotencyKey = idempotencyKey.trim(); Long orderId; try { orderId = Long.valueOf(request.orderId().trim()); } catch (NumberFormatException exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "订单编号无效"); } List> keyOwner = query(""" select order_id from verify_record where revoke_idempotency_key = ? limit 1 """, normalizedIdempotencyKey); // 幂等键归属必须按值比较:两个 Long 用 != 只在 -128..127 的缓存区间内碰巧相等, // 订单 ID 一旦超过这个区间,重复撤销就会被误判成「幂等键已用于其他订单」。 if (!keyOwner.isEmpty() && !Objects.equals(number(keyOwner.get(0).get("orderId")), orderId)) { throw new BusinessException(ErrorCode.CONFLICT, "幂等键已用于其他订单"); } List> repeated = query(""" select vr.revoked_at from verify_record vr join benefit_order o on o.id = vr.order_id where vr.revoke_idempotency_key = ? and o.id = ? and o.deleted = 0 and o.merchant_id = ? and (o.store_id is null or o.store_id = ?) limit 1 """, normalizedIdempotencyKey, orderId, principal.merchantId(), principal.storeId()); if (!repeated.isEmpty()) { return revokeResult(orderId, time(repeated.get(0).get("revokedAt"))); } List> orders = query(""" select o.id, o.order_no, o.order_type, o.order_status, o.verify_status, o.merchant_id, o.store_id, vr.id verify_record_id, vr.verify_time from benefit_order o left join verify_record vr on vr.order_id = o.id and vr.status = 1 where o.id = ? and o.deleted = 0 and o.merchant_id = ? and (o.store_id is null or o.store_id = ?) """, orderId, principal.merchantId(), principal.storeId()); if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在"); Map order = orders.get(0); // 撤销核销与核销、退款、提现锁定共用同一把订单行锁;锁内读到的核销记录才是稳定的。 orderFundBoundary.lock(orderId).requireRevocable(); if (!"GROUP_BUY".equals(String.valueOf(order.get("orderType")))) { throw new BusinessException(ErrorCode.CONFLICT, "当前业务类型不支持撤销核销"); } if (number(order.get("verifyStatus")) == null || number(order.get("verifyStatus")) != 1 || number(order.get("verifyRecordId")) == null) { throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可撤销核销"); } LocalDateTime verifyTime = time(order.get("verifyTime")); if (verifyTime == null || Duration.between(verifyTime, LocalDateTime.now()).toHours() >= REVOCATION_HOURS) { throw new BusinessException(ErrorCode.CONFLICT, "已过可撤销时间"); } String reason = request.reason() == null || request.reason().isBlank() ? "商户撤销核销" : request.reason().trim(); int updated = jdbcTemplate.update(""" update verify_record set status = 2, revoked_at = now(), revoked_by = ?, revoke_reason = ?, revoke_idempotency_key = ? where id = ? and status = 1 """, principal.operatorId(), reason, normalizedIdempotencyKey, number(order.get("verifyRecordId"))); if (updated == 0) throw new BusinessException(ErrorCode.CONFLICT, "核销已被处理,请刷新后重试"); jdbcTemplate.update("update benefit_order set verify_status = 0 where id = ? and verify_status = 1", orderId); return revokeResult(orderId, LocalDateTime.now()); } private Map revokeResult(Long orderId, LocalDateTime revokedAt) { Map result = new LinkedHashMap<>(); result.put("orderId", String.valueOf(orderId)); result.put("orderStatus", "pending_verification"); result.put("orderStatusText", "待核销"); result.put("revokedAt", (revokedAt == null ? LocalDateTime.now() : revokedAt).toString()); return result; } private String revokeReason(String orderType, boolean withdrawalLocked) { if (withdrawalLocked) return "订单已进入提现记录"; return "GROUP_BUY".equals(orderType) ? "已过可撤销时间" : "当前业务类型不支持撤销核销"; } private Map enrichOrder(Map order) { Map result = new LinkedHashMap<>(order); String orderType = String.valueOf(order.get("orderType")); String category = "GROUP_BUY".equals(orderType) ? "group" : ("STORE_CONSUME".equals(orderType) ? "store" : "stay"); Long orderStatus = number(order.get("orderStatus")); Long verifyStatus = number(order.get("verifyStatus")); LocalDateTime verifyTime = time(order.get("verifyTime")); // 核销状态必须同时有有效核销记录和核销时间。历史数据可能只更新了 // benefit_order.verify_status,不能据此把订单展示为已完成。 boolean verified = verifyStatus != null && verifyStatus == 1 && verifyTime != null; String status = orderStatus != null && orderStatus == 0 ? "pending_payment" : (verified ? "completed" : "pending_verification"); result.put("category", category); result.put("status", status); result.put("statusText", "pending_payment".equals(status) ? "待支付" : ("completed".equals(status) ? "已完成" : "待核销")); result.put("customerName", displayName(order.get("talentName"), number(order.get("gender")))); result.put("customerMobile", maskMobile(String.valueOf(order.get("talentMobile")))); result.put("title", orderTitle(order, category)); result.put("benefitName", currentBenefitText(order, category)); long orderAmount = number(order.get("orderAmount")) == null ? 0 : number(order.get("orderAmount")); long payAmount = number(order.get("payAmount")) == null ? 0 : number(order.get("payAmount")); long basePayAmount = number(order.get("basePayAmount")) == null ? orderAmount : number(order.get("basePayAmount")); long couponAmount = number(order.get("couponAmount")) == null ? 0 : number(order.get("couponAmount")); long configuredSubsidyAmount = number(order.get("subsidyAmount")) == null ? 0 : number(order.get("subsidyAmount")); long totalDiscount = Math.max(0, orderAmount - payAmount); long talentDiscountAmount = Math.max(0, orderAmount - basePayAmount); if ("GROUP_BUY".equals(orderType)) { couponAmount = Math.max(0, Math.min(couponAmount, totalDiscount)); // Older group-buy orders were created before all price snapshots were // consistently persisted. Reconcile the displayed breakdown with the // actual paid amount while preserving any valid coupon snapshot. if (talentDiscountAmount + couponAmount != totalDiscount) { talentDiscountAmount = Math.max(0, totalDiscount - couponAmount); } result.put("subsidyAmount", totalDiscount); } result.put("talentDiscountAmount", talentDiscountAmount); result.put("couponAmount", couponAmount); // 免费入住没有 benefit_order 金额,补贴来自核销记录中的后台配置,不能按原价减实付覆盖为 0。 result.put("subsidyAmount", "FREE_STAY".equals(orderType) ? configuredSubsidyAmount : totalDiscount); // 已进入提现记录的订单不能再撤销核销,商户端不值得展示一个必然失败的按钮。 Long withdrawalStatus = number(order.get("withdrawalStatus")); boolean withdrawalLocked = withdrawalStatus != null && withdrawalStatus == 1; boolean canRevoke = "GROUP_BUY".equals(orderType) && verifyTime != null && Duration.between(verifyTime, LocalDateTime.now()).toHours() < REVOCATION_HOURS && verifyStatus != null && verifyStatus == 1 && !withdrawalLocked; result.put("canRevoke", canRevoke); result.put("revokeDeadline", verifyTime == null ? null : verifyTime.plusHours(REVOCATION_HOURS).toString()); result.put("revokeReason", canRevoke ? "" : revokeReason(orderType, withdrawalLocked)); return result; } private List> query(String sql, Object... args) { return DatabaseRows.normalize(jdbcTemplate.queryForList(sql, args)); } private OrderFilters filters(MerchantOrderQuery query) { MerchantOrderQuery value = query == null ? new MerchantOrderQuery(null, null, null, null) : query; StringBuilder conditions = new StringBuilder(); List arguments = new ArrayList<>(); String category = normalize(value.category()); if (!category.isEmpty()) { String orderType = switch (category) { case "group" -> "GROUP_BUY"; case "store" -> "STORE_CONSUME"; case "stay" -> "FREE_STAY"; default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "订单业务类型无效"); }; conditions.append(" and o.order_type = ?"); arguments.add(orderType); } String status = normalize(value.status()); if (!status.isEmpty() && !"all".equals(status)) { switch (status) { case "pending_verification" -> conditions.append(" and o.order_status = 1 and o.verify_status = 0"); case "pending_payment" -> conditions.append(" and o.order_status = 0"); case "completed" -> conditions.append(" and o.order_status = 1 and o.verify_status = 1"); default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "订单状态无效"); } } LocalDate startDate = date(value.startDate(), "开始日期"); LocalDate endDate = date(value.endDate(), "结束日期"); if (startDate != null && endDate != null && startDate.isAfter(endDate)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "开始日期不能晚于结束日期"); } if (startDate != null) { conditions.append(" and o.created_at >= ?"); arguments.add(Timestamp.valueOf(startDate.atStartOfDay())); } if (endDate != null) { conditions.append(" and o.created_at < ?"); arguments.add(Timestamp.valueOf(endDate.plusDays(1).atStartOfDay())); } return new OrderFilters(conditions.toString(), arguments); } private Map toListItem(Map order) { Map result = new LinkedHashMap<>(order); String category = category(String.valueOf(order.get("orderType"))); String status = status(number(order.get("orderStatus")), number(order.get("verifyStatus"))); result.put("category", category); result.put("status", status); result.put("statusText", statusText(status)); result.put("customerName", displayName(order.get("talentName"), number(order.get("gender")))); result.put("customerMobile", maskMobile(String.valueOf(order.get("talentMobile")))); result.put("title", orderTitle(order, category)); return result; } private String orderTitle(Map order, String category) { String snapshot = text(order.get("productNameSnapshot")); if (!snapshot.isBlank()) return snapshot; String benefit = text(order.get("benefitName")); return benefit.isBlank() ? categoryTitle(category) : benefit; } private String currentBenefitText(Map order, String category) { String cardLevel = text(order.get("cardLevel")); String merchantDetail = merchantBenefitDetail(order.get("merchantRightsConfig"), cardLevel); if (!merchantDetail.isBlank()) return merchantDetail; if ("store".equals(category)) { Long merchantId = number(order.get("merchantId")); return merchantId == null ? "以当前商户到店消费规则为准" : manualConsumptionService.benefitTextForMerchant(merchantId, cardLevel); } if ("group".equals(category)) { ProductPriceCalculator.ProductRight right = ProductPriceCalculator.productRight(order.get("rightsConfig"), cardLevel); if (right != null) return productBenefitText(right); } String configured = text(order.get("benefitName")); return configured.isBlank() ? "以当前订单权益规则为准" : configured; } private String merchantBenefitDetail(Object rawConfig, String cardLevel) { if (rawConfig == null || cardLevel.isBlank()) return ""; try { List> configs = rawConfig instanceof List ? ((List) rawConfig).stream().filter(Map.class::isInstance) .map(item -> (Map) item).toList() : OBJECT_MAPPER.readValue(String.valueOf(rawConfig), RIGHTS_TYPE); for (Map config : configs) { if (!booleanValue(config.get("enabled")) || !sameCardLevel(config.get("level"), cardLevel)) continue; String detail = text(config.get("detail")); if (!detail.isBlank()) return detail; } } catch (Exception ignored) { // Invalid legacy configuration should fall back to the structured rule text. } return ""; } private boolean sameCardLevel(Object configured, String actual) { return normalizeCardLevel(configured).equals(normalizeCardLevel(actual)); } private String normalizeCardLevel(Object value) { String raw = text(value).toUpperCase(); if (raw.contains("卓越") || "EXCELLENT".equals(raw)) return "EXCELLENT"; if (raw.contains("杰出") || "OUTSTANDING".equals(raw)) return "OUTSTANDING"; if (raw.contains("菁英") || "ELITE".equals(raw)) return "ELITE"; return raw; } private boolean booleanValue(Object value) { return value instanceof Boolean bool ? bool : "1".equals(text(value)) || "true".equalsIgnoreCase(text(value)); } private String productBenefitText(ProductPriceCalculator.ProductRight right) { String discountText = discountText(right.discount()); if (!right.free()) return discountText.isBlank() ? "以商品配置为准" : "按市价" + discountText; String freeText = right.limitCount() > 0 ? "免费" + right.limitCount() + "次/" + cycleText(right.resetCycle()) : "免费"; return discountText.isBlank() ? freeText : freeText + ",超出次数按市价" + discountText; } private String discountText(BigDecimal discount) { if (discount == null || discount.signum() <= 0) return ""; return discount.stripTrailingZeros().toPlainString() + "折"; } private String cycleText(String cycle) { String value = normalize(cycle).toUpperCase(); return switch (value) { case "DAY", "DAILY", "每天" -> "天"; case "WEEK", "WEEKLY", "每周" -> "周"; case "MONTH", "MONTHLY", "每月" -> "月"; default -> "年"; }; } private String category(String orderType) { if ("GROUP_BUY".equals(orderType)) return "group"; if ("STORE_CONSUME".equals(orderType)) return "store"; if ("FREE_STAY".equals(orderType)) return "stay"; return "group"; } private String status(Long orderStatus, Long verifyStatus) { if (orderStatus != null && orderStatus == 0) return "pending_payment"; if (verifyStatus != null && verifyStatus == 1) return "completed"; return "pending_verification"; } private String statusText(String status) { if ("pending_payment".equals(status)) return "待支付"; if ("completed".equals(status)) return "已完成"; return "待核销"; } private String categoryTitle(String category) { if ("store".equals(category)) return "到店消费订单"; if ("stay".equals(category)) return "免费入住"; return "团购订单"; } private String maskName(String value) { if (value == null || value.isBlank() || "null".equals(value)) return ""; return value.length() == 1 ? value + "*" : value.substring(0, 1) + "*".repeat(value.length() - 1); } private String displayName(Object rawName, Long gender) { String name = text(rawName); if (name.isBlank()) return ""; if (gender != null && gender == 1) return name.substring(0, 1) + "先生"; if (gender != null && gender == 2) return name.substring(0, 1) + "女士"; return maskName(name); } private String text(Object value) { return value == null || "null".equals(String.valueOf(value)) ? "" : String.valueOf(value).trim(); } private String maskMobile(String value) { if (value == null || value.length() < 7 || "null".equals(value)) return ""; return value.substring(0, 3) + "****" + value.substring(value.length() - 4); } private String normalize(String value) { return value == null ? "" : value.trim(); } private LocalDate date(String value, String field) { if (value == null || value.isBlank()) return null; try { return LocalDate.parse(value); } catch (Exception exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, field + "格式无效"); } } private Long number(Object value) { return value instanceof Number number ? number.longValue() : null; } private LocalDateTime time(Object value) { if (value instanceof LocalDateTime dateTime) return dateTime; if (value instanceof Timestamp timestamp) return timestamp.toLocalDateTime(); return null; } private record OrderFilters(String conditions, List arguments) { } }