package com.talentcard.server.business.merchant; import java.math.BigDecimal; import java.time.LocalTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.alibaba.fastjson.JSON; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.business.PageResult; import com.talentcard.server.business.merchant.auth.MerchantPrincipal; 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 MerchantManagementService { private static final List NOTICE_FIELDS = List.of("validity", "availableTime", "usage", "otherRules"); private final JdbcTemplate jdbc; public MerchantManagementService(JdbcTemplate jdbc) { this.jdbc = jdbc; } @Transactional(readOnly = true) public Map profile(MerchantPrincipal principal) { Map merchant = merchant(principal.merchantId()); Map store = store(principal); Map result = new LinkedHashMap<>(); result.put("merchantId", principal.merchantId()); result.put("storeId", principal.storeId()); result.put("merchantName", merchant.get("merchantName")); result.put("storeName", store.get("storeName")); result.put("address", value(store.get("address"), value(merchant.get("address"), ""))); result.put("coverUrl", value(merchant.get("coverUrl"), "")); result.put("businessStatus", isBusinessOpen(merchant, principal.storeId()) ? "open" : "closed"); result.put("updatedAt", merchant.get("updatedAt")); Long productCount = jdbc.queryForObject("select count(*) from merchant_product where merchant_id = ? and deleted = 0 and status = 1 and audit_status = 2", Long.class, principal.merchantId()); result.put("onlineProductCount", productCount == null ? 0 : productCount); return result; } @Transactional public Map updateBusinessStatus(MerchantPrincipal principal, MerchantBusinessStatusRequest request) { String status = normalizeBusinessStatus(request.status()); merchant(principal.merchantId()); store(principal); jdbc.update("update merchant set business_status = ? where id = ? and deleted = 0", status.toUpperCase(), principal.merchantId()); return Map.of( "businessStatus", isBusinessOpen(merchant(principal.merchantId()), principal.storeId()) ? "open" : "closed", "updatedAt", java.time.LocalDateTime.now().toString()); } @Transactional(readOnly = true) public Map businessHours(MerchantPrincipal principal) { store(principal); List> periods = rows(""" select start_time, end_time, sort_no from merchant_business_hours where merchant_id = ? and store_id = ? order by sort_no asc, id asc """, principal.merchantId(), principal.storeId()).stream().map(row -> Map.of( "start", value(row.get("startTime"), ""), "end", value(row.get("endTime"), "") )).toList(); return Map.of("periods", periods); } @Transactional public Map updateBusinessHours(MerchantPrincipal principal, MerchantBusinessHoursRequest request) { store(principal); List periods = new ArrayList<>(request.periods()); validatePeriods(periods); periods.sort((left, right) -> left.start().compareTo(right.start())); jdbc.update("delete from merchant_business_hours where merchant_id = ? and store_id = ?", principal.merchantId(), principal.storeId()); for (int index = 0; index < periods.size(); index++) { MerchantBusinessHoursRequest.Period period = periods.get(index); jdbc.update(""" insert into merchant_business_hours(merchant_id, store_id, start_time, end_time, sort_no) values (?, ?, ?, ?, ?) """, principal.merchantId(), principal.storeId(), period.start(), period.end(), (index + 1) * 10); } return businessHours(principal); } @Transactional(readOnly = true) public PageResult> products(MerchantPrincipal principal, String scope, int page, int size) { store(principal); int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 50); ProductScope productScope = ProductScope.from(scope); String conditions = " where merchant_id = ? and deleted = 0" + productScope.conditions(); List> records = rows(""" select id, product_no, product_name, cover_url, sale_price, status, audit_status, audit_remark, submit_time, updated_at from merchant_product """ + conditions + " order by updated_at desc, id desc limit ? offset ?", principal.merchantId(), safeSize, (safePage - 1) * safeSize) .stream().map(this::toProductListItem).toList(); Long total = jdbc.queryForObject("select count(*) from merchant_product" + conditions, Long.class, principal.merchantId()); return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total); } @Transactional(readOnly = true) public Map product(MerchantPrincipal principal, Long productId) { store(principal); List> records = rows(""" select id, product_no, product_name, product_type, cover_url, original_price, sale_price, detail_text, purchase_notice, product_images, rights_config, benefit_desc, status, audit_status, audit_remark, submit_time, updated_at from merchant_product where id = ? and merchant_id = ? and deleted = 0 """, productId, principal.merchantId()); if (records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商品不存在"); return toProductDetail(records.get(0)); } @Transactional public Map updateProduct(MerchantPrincipal principal, Long productId, MerchantProductUpdateRequest request) { store(principal); product(principal, productId); String coverUrl = request.coverUrl() == null ? "" : request.coverUrl().trim(); if (coverUrl.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传商品封面图"); String images = jsonArray(request.productImages() == null ? List.of() : request.productImages()); Map notice = new LinkedHashMap<>(); for (String field : NOTICE_FIELDS) notice.put(field, request.purchaseNotice() == null ? "" : value(request.purchaseNotice().get(field), "")); jdbc.update(""" update merchant_product set product_name = ?, sale_price = ?, cover_url = ?, product_images = ?, detail_text = ?, purchase_notice = ?, audit_status = 1, status = 0, submit_time = now(), audit_remark = null where id = ? and merchant_id = ? and deleted = 0 """, request.productName().trim(), request.salePrice(), coverUrl, images, value(request.detailText(), ""), jsonObject(notice), productId, principal.merchantId()); return product(principal, productId); } @Transactional public Map updateProductStatus(MerchantPrincipal principal, Long productId, MerchantProductStatusRequest request) { store(principal); Map product = product(principal, productId); String status = request.status().trim().toLowerCase(); if (!List.of("online", "offline").contains(status)) throw new BusinessException(ErrorCode.BAD_REQUEST, "商品状态无效"); if ("online".equals(status) && !"approved".equals(product.get("auditStatus"))) { throw new BusinessException(ErrorCode.CONFLICT, "商品审核通过后才能上架"); } jdbc.update("update merchant_product set status = ? where id = ? and merchant_id = ? and deleted = 0", "online".equals(status) ? 1 : 0, productId, principal.merchantId()); return product(principal, productId); } @Transactional(readOnly = true) public List> benefits(MerchantPrincipal principal) { store(principal); List> configured = configuredBenefits(principal.merchantId()); if (!configured.isEmpty()) return configured; return rows(""" select card_level, card_name, benefit_text, theme from merchant_card_benefit where merchant_id = ? and status = 1 order by sort_no desc, id desc """, principal.merchantId()).stream().map(row -> Map.of( "cardLevel", value(row.get("cardLevel"), ""), "cardName", value(row.get("cardName"), ""), "benefitText", value(row.get("benefitText"), ""), "theme", value(row.get("theme"), "excellent") )).toList(); } private List> configuredBenefits(Long merchantId) { List> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId); if (merchants.isEmpty()) return List.of(); Object raw = merchants.get(0).get("merchantRightsConfig"); if (raw == null || value(raw, "").isBlank()) return List.of(); List> rights = new ArrayList<>(); try { if (raw instanceof List list) { for (Object item : list) if (item instanceof Map map) rights.add(copyMap(map)); } else { for (Object item : JSON.parseArray(value(raw, ""), Map.class)) if (item instanceof Map map) rights.add(copyMap(map)); } } catch (Exception ignored) { return List.of(); } Map> byLevel = new LinkedHashMap<>(); for (Map right : rights) { if (!booleanValue(right.get("enabled"))) continue; String level = normalizeLevel(value(right.get("level"), "")); if (!List.of("EXCELLENT", "OUTSTANDING", "ELITE").contains(level)) continue; byLevel.putIfAbsent(level, right); } return List.of("EXCELLENT", "OUTSTANDING", "ELITE").stream() .filter(byLevel::containsKey).map(level -> toBenefit(byLevel.get(level))) .filter(item -> !value(item.get("benefitText"), "").isBlank()).toList(); } private Map toBenefit(Map right) { String level = value(right.get("level"), ""); String normalized = normalizeLevel(level); Map result = new LinkedHashMap<>(); result.put("cardLevel", normalized); result.put("cardName", value(right.get("cardName"), cardName(normalized, level))); String text = value(right.get("detail"), ""); if (text.isBlank()) text = value(right.get("consumeDesc"), ""); if (text.isBlank()) text = configuredConsumeText(right); result.put("benefitText", text); result.put("theme", theme(normalized)); return result; } private String configuredConsumeText(Map right) { String type = value(right.get("consumeType"), ""); if ("优惠".equals(type) && !value(right.get("consumeCoupon"), "").isBlank()) return "到店消费立减" + right.get("consumeCoupon") + "元"; String discount = "折扣".equals(type) ? value(right.get("consumeDiscount"), "") : value(right.get("discount"), ""); return discount.isBlank() ? "" : "到店消费" + discount + "折"; } private String normalizeLevel(String value) { String upper = value.toUpperCase(); if (upper.contains("EXCELLENT") || value.contains("卓越")) return "EXCELLENT"; if (upper.contains("OUTSTANDING") || value.contains("杰出")) return "OUTSTANDING"; if (upper.contains("ELITE") || value.contains("菁英") || value.contains("精英")) return "ELITE"; return upper; } private String cardName(String normalized, String fallback) { return switch (normalized) { case "EXCELLENT" -> "卓越人才卡"; case "OUTSTANDING" -> "杰出人才卡"; case "ELITE" -> "菁英人才卡"; default -> fallback; }; } private String theme(String normalized) { return switch (normalized) { case "OUTSTANDING" -> "outstanding"; case "ELITE" -> "elite"; default -> "excellent"; }; } private boolean booleanValue(Object value) { if (value instanceof Boolean bool) return bool; if (value instanceof Number number) return number.intValue() != 0; String text = value(value, ""); return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); } 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 Map merchant(Long merchantId) { List> merchants = rows(""" select id, merchant_name, address, cover_url, business_status, status, updated_at from merchant where id = ? and deleted = 0 """, merchantId); if (merchants.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商户不存在"); return merchants.get(0); } private Map store(MerchantPrincipal principal) { List> stores = rows(""" select id, merchant_id, store_name, address from merchant_store where id = ? and merchant_id = ? and status = 1 and deleted = 0 """, principal.storeId(), principal.merchantId()); if (stores.isEmpty()) throw new BusinessException(ErrorCode.FORBIDDEN, "当前门店不可操作"); return stores.get(0); } private Map toProductListItem(Map row) { Map result = new LinkedHashMap<>(); result.put("id", String.valueOf(row.get("id"))); result.put("productNo", value(row.get("productNo"), "")); result.put("productName", value(row.get("productName"), "")); result.put("coverUrl", value(row.get("coverUrl"), "")); result.put("salePrice", decimal(row.get("salePrice"))); result.put("status", number(row.get("status")) == 1 ? "online" : "offline"); result.put("auditStatus", auditStatus(number(row.get("auditStatus")))); result.put("auditRemark", value(row.get("auditRemark"), "")); result.put("submitTime", row.get("submitTime")); result.put("updatedAt", row.get("updatedAt")); return result; } private Map toProductDetail(Map row) { Map result = toProductListItem(row); result.put("productType", value(row.get("productType"), "")); result.put("originalPrice", decimal(row.get("originalPrice"))); result.put("detailText", value(row.get("detailText"), "")); result.put("purchaseNotice", object(row.get("purchaseNotice"))); result.put("productImages", strings(row.get("productImages"))); result.put("rightsConfig", object(row.get("rightsConfig"))); result.put("benefitDesc", value(row.get("benefitDesc"), "")); return result; } private void validatePeriods(List periods) { periods.sort((left, right) -> left.start().compareTo(right.start())); LocalTime previousEnd = null; for (MerchantBusinessHoursRequest.Period period : periods) { LocalTime start = LocalTime.parse(period.start()); LocalTime end = LocalTime.parse(period.end()); if (!start.isBefore(end)) throw new BusinessException(ErrorCode.BAD_REQUEST, "结束时间需晚于开始时间"); if (previousEnd != null && start.isBefore(previousEnd)) throw new BusinessException(ErrorCode.BAD_REQUEST, "营业时段不能重叠"); previousEnd = end; } } private String normalizeBusinessStatus(String input) { String status = input.trim().toLowerCase(); if ("open".equals(status) || "营业中".equals(status)) return "open"; if ("closed".equals(status) || "休息中".equals(status)) return "closed"; throw new BusinessException(ErrorCode.BAD_REQUEST, "营业状态无效"); } private String businessStatus(Object value) { String status = value(value, "").trim().toLowerCase(); return "closed".equals(status) || "休息中".equals(status) ? "closed" : "open"; } private boolean isBusinessOpen(Map merchant, Long storeId) { if (!Long.valueOf(1).equals(number(merchant.get("status"))) || "closed".equals(businessStatus(merchant.get("businessStatus")))) { return false; } Long configuredPeriods = jdbc.queryForObject(""" select count(*) from merchant_business_hours where merchant_id = ? and store_id = ? """, Long.class, merchant.get("id"), storeId); if (configuredPeriods == null || configuredPeriods == 0) { return true; } Long activePeriods = jdbc.queryForObject(""" select count(*) from merchant_business_hours where merchant_id = ? and store_id = ? and ( (start_time <= end_time and current_time() >= start_time and current_time() < end_time) or (start_time > end_time and (current_time() >= start_time or current_time() < end_time)) ) """, Long.class, merchant.get("id"), storeId); return activePeriods != null && activePeriods > 0; } private String auditStatus(Long status) { if (status != null && status == 2) return "approved"; if (status != null && status == 3) return "rejected"; return "pending"; } private List> rows(String sql, Object... arguments) { return DatabaseRows.normalize(jdbc.queryForList(sql, arguments)); } private Map object(Object value) { Map result = new LinkedHashMap<>(); String json = value(value, ""); for (String field : NOTICE_FIELDS) { String parsed = jsonField(json, field); if (!parsed.isEmpty()) result.put(field, parsed); } return result; } private List strings(Object value) { String json = value(value, ""); if (!json.startsWith("[")) return List.of(); List result = new ArrayList<>(); Matcher matcher = Pattern.compile("\\\"((?:\\\\.|[^\\\"\\\\])*)\\\"").matcher(json); while (matcher.find()) result.add(unescape(matcher.group(1))); return result; } private String jsonObject(Map values) { List entries = new ArrayList<>(); for (Map.Entry entry : values.entrySet()) { entries.add("\"" + escape(entry.getKey()) + "\":\"" + escape(entry.getValue()) + "\""); } return "{" + String.join(",", entries) + "}"; } private String jsonArray(List values) { List entries = new ArrayList<>(); for (String value : values) entries.add("\"" + escape(value == null ? "" : value.trim()) + "\""); return "[" + String.join(",", entries) + "]"; } private String jsonField(String json, String field) { Pattern pattern = Pattern.compile("\\\"" + Pattern.quote(field) + "\\\"\\s*:\\s*\\\"((?:\\\\.|[^\\\"\\\\])*)\\\""); Matcher matcher = pattern.matcher(json); return matcher.find() ? unescape(matcher.group(1)) : ""; } private String escape(String value) { return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t"); } private String unescape(String value) { StringBuilder result = new StringBuilder(); boolean escaped = false; for (int index = 0; index < value.length(); index++) { char item = value.charAt(index); if (!escaped) { if (item == '\\') escaped = true; else result.append(item); continue; } if (item == 'n') result.append('\n'); else if (item == 'r') result.append('\r'); else if (item == 't') result.append('\t'); else result.append(item); escaped = false; } if (escaped) result.append('\\'); return result.toString(); } private String value(Object value, String fallback) { if (value == null || "null".equals(String.valueOf(value))) return fallback; String text = String.valueOf(value).trim(); return text.isEmpty() ? fallback : text; } private Long number(Object value) { return value instanceof Number number ? number.longValue() : null; } private BigDecimal decimal(Object value) { if (value instanceof BigDecimal decimal) return decimal; if (value instanceof Number number) return BigDecimal.valueOf(number.doubleValue()); try { return new BigDecimal(String.valueOf(value)); } catch (Exception ignored) { return BigDecimal.ZERO; } } private enum ProductScope { ONLINE(" and status = 1 and audit_status = 2"), OFFLINE(" and status = 0 and audit_status = 2"), REVIEW(""); private final String conditions; ProductScope(String conditions) { this.conditions = conditions; } String conditions() { return conditions; } static ProductScope from(String value) { if ("offline".equals(value)) return OFFLINE; if ("review".equals(value)) return REVIEW; return ONLINE; } } }