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<String> NOTICE_FIELDS = List.of("validity", "availableTime", "usage", "otherRules");
|
|
private final JdbcTemplate jdbc;
|
|
public MerchantManagementService(JdbcTemplate jdbc) {
|
this.jdbc = jdbc;
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> profile(MerchantPrincipal principal) {
|
Map<String, Object> merchant = merchant(principal.merchantId());
|
Map<String, Object> store = store(principal);
|
Map<String, Object> 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<String, Object> 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<String, Object> businessHours(MerchantPrincipal principal) {
|
store(principal);
|
List<Map<String, Object>> 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.<String, Object>of(
|
"start", value(row.get("startTime"), ""),
|
"end", value(row.get("endTime"), "")
|
)).toList();
|
return Map.of("periods", periods);
|
}
|
|
@Transactional
|
public Map<String, Object> updateBusinessHours(MerchantPrincipal principal, MerchantBusinessHoursRequest request) {
|
store(principal);
|
List<MerchantBusinessHoursRequest.Period> 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<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> product(MerchantPrincipal principal, Long productId) {
|
store(principal);
|
List<Map<String, Object>> 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<String, Object> 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<String, String> 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<String, Object> updateProductStatus(MerchantPrincipal principal, Long productId, MerchantProductStatusRequest request) {
|
store(principal);
|
Map<String, Object> 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<Map<String, Object>> benefits(MerchantPrincipal principal) {
|
store(principal);
|
List<Map<String, Object>> 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.<String, Object>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<Map<String, Object>> configuredBenefits(Long merchantId) {
|
List<Map<String, Object>> 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<Map<String, Object>> 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<String, Map<String, Object>> byLevel = new LinkedHashMap<>();
|
for (Map<String, Object> 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<String, Object> toBenefit(Map<String, Object> right) {
|
String level = value(right.get("level"), "");
|
String normalized = normalizeLevel(level);
|
Map<String, Object> 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<String, Object> 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<String, Object> copyMap(Map<?, ?> source) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
for (Map.Entry<?, ?> entry : source.entrySet()) result.put(String.valueOf(entry.getKey()), entry.getValue());
|
return result;
|
}
|
|
private Map<String, Object> merchant(Long merchantId) {
|
List<Map<String, Object>> 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<String, Object> store(MerchantPrincipal principal) {
|
List<Map<String, Object>> 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<String, Object> toProductListItem(Map<String, Object> row) {
|
Map<String, Object> 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<String, Object> toProductDetail(Map<String, Object> row) {
|
Map<String, Object> 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<MerchantBusinessHoursRequest.Period> 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<String, Object> 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<Map<String, Object>> rows(String sql, Object... arguments) {
|
return DatabaseRows.normalize(jdbc.queryForList(sql, arguments));
|
}
|
|
private Map<String, String> object(Object value) {
|
Map<String, String> 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<String> strings(Object value) {
|
String json = value(value, "");
|
if (!json.startsWith("[")) return List.of();
|
List<String> result = new ArrayList<>();
|
Matcher matcher = Pattern.compile("\\\"((?:\\\\.|[^\\\"\\\\])*)\\\"").matcher(json);
|
while (matcher.find()) result.add(unescape(matcher.group(1)));
|
return result;
|
}
|
|
private String jsonObject(Map<String, String> values) {
|
List<String> entries = new ArrayList<>();
|
for (Map.Entry<String, String> entry : values.entrySet()) {
|
entries.add("\"" + escape(entry.getKey()) + "\":\"" + escape(entry.getValue()) + "\"");
|
}
|
return "{" + String.join(",", entries) + "}";
|
}
|
|
private String jsonArray(List<String> values) {
|
List<String> 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;
|
}
|
}
|
}
|