package com.talentcard.server.business.portal; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.TypeReference; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.business.PageResult; 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; @Service public class ParkingMerchantPortalService { private final JdbcTemplate jdbc; public ParkingMerchantPortalService(JdbcTemplate jdbc) { this.jdbc = jdbc; } @Transactional(readOnly = true) public Map detail(Long merchantId, Long talentCardId) { Map merchant = parkingMerchant(merchantId); Map benefit = parkingBenefit(merchantId); String cardLevel = talentCardId == null ? "" : talentCardLevel(talentCardId); List> rights = parseList(merchant.get("merchantRightsConfig")); Map right = matchingRight(rights, cardLevel); boolean hasCardRightsConfig = !rights.isEmpty() && !cardLevel.isBlank(); boolean available = number(merchant.get("status")) == 1 && (!hasCardRightsConfig || right != null) && (right == null || booleanValue(right.get("enabled"))); Map result = new LinkedHashMap<>(); result.put("merchantId", merchant.get("id")); result.put("merchantName", merchant.get("merchantName")); result.put("merchantType", "parking"); result.put("coverUrl", merchant.get("coverUrl")); result.put("address", merchant.get("address")); result.put("contactMobile", merchant.get("contactMobile")); result.put("latitude", merchant.get("latitude")); result.put("longitude", merchant.get("longitude")); result.put("available", available); result.put("unavailableReason", available ? "" : unavailableReason(merchant, right)); result.put("serviceLabel", text(merchant.get("serviceLabel"), "停车服务")); result.put("benefitText", benefitText(merchant, right, benefit)); result.put("applicableCards", applicableCards(right, benefit)); result.put("usageInstructions", text(merchant.get("usageInstructions"), "请咨询商户")); result.put("rightsText", rightsText(merchant, right)); String parkingType = text(right == null ? null : right.get("parkingType"), "paid"); result.put("actionMode", "free".equalsIgnoreCase(parkingType) ? "talentCode" : "plate"); result.put("plateFormMode", text(benefit.get("plateFormMode"), "simple")); result.put("benefit", benefitView(benefit)); result.put("applicableStores", parkingStores(merchant.get("usageInstructions"))); result.put("merchantRightsConfig", rights); result.put("merchantRight", right == null ? Map.of() : right); result.put("parkingType", parkingType); result.put("parkingScope", text(right == null ? null : right.get("parkingScope"), "all")); result.put("consumeDesc", text(right == null ? null : right.get("consumeDesc"), "")); return result; } @Transactional(readOnly = true) public PageResult> parkingLots(Long merchantId, String keyword, int page, int size) { Map merchant = parkingMerchant(merchantId); List> stores = parkingStores(merchant.get("usageInstructions")); String cleaned = keyword == null ? "" : keyword.trim(); List> filtered = stores.stream() .filter(item -> cleaned.isEmpty() || text(item.get("storeName"), "").contains(cleaned) || text(item.get("parkingLotName"), "").contains(cleaned) || text(item.get("name"), "").contains(cleaned) || text(item.get("address"), "").contains(cleaned)) .toList(); int safePage = Math.max(1, page); int safeSize = Math.min(50, Math.max(1, size)); int from = Math.min(filtered.size(), (safePage - 1) * safeSize); int to = Math.min(filtered.size(), from + safeSize); return new PageResult<>(filtered.subList(from, to), safePage, safeSize, filtered.size()); } @Transactional public Map registerPlate(Long merchantId, ParkingPlateRegistrationRequest request, String idempotencyKey) { if (idempotencyKey == null || idempotencyKey.isBlank() || idempotencyKey.length() > 128) { throw new BusinessException(ErrorCode.BAD_REQUEST, "提交凭证不能为空"); } List> existing = query(""" select id, registration_no, status from transport_parking_plate_registration where idempotency_key = ? and deleted = 0 """, idempotencyKey); if (!existing.isEmpty()) return registrationView(existing.get(0)); Map merchant = parkingMerchant(merchantId); if (number(merchant.get("status")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "停车服务当前不可用"); Map benefit = one(""" select id, action_mode, plate_form_mode, status from transport_parking_benefit where id = ? and merchant_id = ? and deleted = 0 """, request.benefitId(), merchantId); if (number(benefit.get("status")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "停车权益当前不可用"); if (!"plate".equals(benefit.get("actionMode"))) throw new BusinessException(ErrorCode.CONFLICT, "当前停车权益无需登记车牌"); if ("withParking".equals(benefit.get("plateFormMode"))) { if (request.parkingLotId() == null) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择停车场"); boolean matchedLot = parkingStores(merchant.get("usageInstructions")).stream() .anyMatch(item -> String.valueOf(request.parkingLotId()).equals(String.valueOf(item.get("id")))); if (!matchedLot) throw new BusinessException(ErrorCode.BAD_REQUEST, "停车场不可用"); } Integer talentCount = jdbc.queryForObject("select count(*) from talent_user where id = ? and deleted = 0", Integer.class, request.talentId()); if (talentCount == null || talentCount == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "人才用户不存在"); Integer duplicateCount = jdbc.queryForObject(""" select count(*) from transport_parking_plate_registration where merchant_id = ? and talent_id = ? and plate_number = ? and deleted = 0 and status in ('SUBMITTED', 'ACTIVE') """, Integer.class, merchantId, request.talentId(), request.plateNumber()); if (duplicateCount != null && duplicateCount > 0) throw new BusinessException(ErrorCode.CONFLICT, "该车牌已登记,如需变更请联系停车服务商"); String registrationNo = BusinessIdGenerator.next("PR"); jdbc.update(""" insert into transport_parking_plate_registration(registration_no, merchant_id, benefit_id, parking_lot_id, talent_id, plate_number, status, idempotency_key) values (?, ?, ?, ?, ?, ?, 'SUBMITTED', ?) """, registrationNo, merchantId, request.benefitId(), request.parkingLotId(), request.talentId(), request.plateNumber(), idempotencyKey); return registrationView(one(""" select id, registration_no, status from transport_parking_plate_registration where idempotency_key = ? and deleted = 0 """, idempotencyKey)); } private Map parkingMerchant(Long merchantId) { return one(""" select id, merchant_name, contact_mobile, address, cover_url, service_label, usage_instructions, rights_text, merchant_rights_config, status, latitude, longitude from merchant where id = ? and merchant_type in ('parking', '停车服务') and deleted = 0 """, merchantId); } private Map parkingBenefit(Long merchantId) { List> rows = query(""" select id, talent_levels, benefit_text, action_mode, plate_form_mode, valid_end, unavailable_reason, status from transport_parking_benefit where merchant_id = ? and deleted = 0 order by id asc limit 1 """, merchantId); return rows.isEmpty() ? new LinkedHashMap<>() : rows.get(0); } private List> parkingStores(Object usageInstructions) { List> stores = parseList(usageInstructions); int index = 0; for (Map store : stores) { Object id = store.get("id"); if (id == null) store.put("id", index + 1); Object name = store.get("name"); if (name != null && store.get("storeName") == null) store.put("storeName", name); if (name != null && store.get("parkingLotName") == null) store.put("parkingLotName", name); store.put("canNavigate", hasCoordinate(store)); index++; } return stores; } private Map benefitView(Map benefit) { Map result = new LinkedHashMap<>(); result.put("benefitId", benefit.get("id")); result.put("actionMode", text(benefit.get("actionMode"), "plate")); result.put("plateFormMode", text(benefit.get("plateFormMode"), "simple")); result.put("validEnd", benefit.get("validEnd")); return result; } private Map registrationView(Map row) { Map result = new LinkedHashMap<>(); result.put("registrationId", row.get("id")); result.put("registrationNo", row.get("registrationNo")); result.put("status", row.get("status")); return result; } private Map matchingRight(List> rights, String cardLevel) { if (rights.isEmpty()) return null; String normalizedCardLevel = normalizedCardLevel(cardLevel); if (normalizedCardLevel.isBlank()) { return rights.stream().filter(item -> booleanValue(item.get("enabled"))).findFirst().orElse(null); } for (Map right : rights) { if (!booleanValue(right.get("enabled"))) continue; if (normalizedCardLevel.equals(normalizedCardLevel(text(right.get("level"), "")))) return right; } return null; } private String applicableCards(Map right, Map benefit) { return text(right == null ? null : right.get("level"), text(benefit.get("talentLevels"), "请咨询商户")); } private String rightsText(Map merchant, Map right) { if (right != null) { String detail = text(right.get("detail"), ""); if (!detail.isBlank()) return detail; String consumeDesc = text(right.get("consumeDesc"), ""); if (!consumeDesc.isBlank()) return consumeDesc; String tag = text(right.get("tag"), ""); if (!tag.isBlank()) return tag; } return text(merchant.get("rightsText"), "请咨询商户"); } private String benefitText(Map merchant, Map right, Map benefit) { if (right != null) { String detail = text(right.get("detail"), ""); if (!detail.isBlank()) return detail; String tag = text(right.get("tag"), ""); if (!tag.isBlank()) return tag; } return text(benefit.get("benefitText"), text(merchant.get("rightsText"), "免费停车")); } private String unavailableReason(Map merchant, Map right) { if (number(merchant.get("status")) != 1) return "停车服务当前不可用"; if (right == null) return "当前人才卡暂无可用停车权益"; return "停车权益当前不可用"; } private List> parseList(Object value) { if (value instanceof List list) { return list.stream().filter(Map.class::isInstance).map(item -> copyMap((Map) item)).toList(); } String text = value == null ? "" : String.valueOf(value).trim(); if (text.isBlank()) return List.of(); try { List> rows = JSON.parseObject(text, new TypeReference>>() {}); return rows == null ? List.of() : rows.stream().map(this::copyMap).toList(); } catch (Exception ignored) { return List.of(); } } private Map copyMap(Map source) { Map result = new LinkedHashMap<>(); for (Map.Entry entry : source.entrySet()) result.put(String.valueOf(entry.getKey()), entry.getValue()); return result; } private String talentCardLevel(Long talentCardId) { List> rows = query("select card_level from talent_card where id = ? and deleted = 0 limit 1", talentCardId); return rows.isEmpty() ? "" : text(rows.get(0).get("cardLevel"), ""); } private String normalizedCardLevel(String value) { String raw = value == null ? "" : value.trim(); String upper = raw.toUpperCase(Locale.ROOT); if (upper.contains("EXCELLENT") || raw.contains("卓越")) return "卓越人才卡"; if (upper.contains("OUTSTANDING") || raw.contains("杰出")) return "杰出人才卡"; if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) return "菁英人才卡"; return raw; } private boolean hasCoordinate(Map store) { return decimal(store.get("latitude")) != 0D && decimal(store.get("longitude")) != 0D; } private double decimal(Object value) { if (value instanceof Number number) return number.doubleValue(); try { return value == null ? 0D : Double.parseDouble(String.valueOf(value)); } catch (Exception ignored) { return 0D; } } private boolean booleanValue(Object value) { if (value instanceof Boolean bool) return bool; if (value instanceof Number number) return number.intValue() != 0; String text = value == null ? "" : String.valueOf(value).trim(); return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); } private List> query(String sql, Object... args) { return jdbc.queryForList(sql, args).stream().map(DatabaseRows::normalize).toList(); } private Map one(String sql, Object... args) { List> rows = query(sql, args); if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "停车权益配置不存在"); return rows.get(0); } private int number(Object value) { return value instanceof Number number ? number.intValue() : 0; } private String text(Object value, String fallback) { if (value == null) return fallback; String text = String.valueOf(value).trim(); return text.isEmpty() ? fallback : text; } }