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.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); 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 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 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; } } }