| | |
| | | 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; |
| | |
| | | |
| | | @Service |
| | | public class MerchantManagementService { |
| | | private static final List<String> NOTICE_FIELDS = List.of("validity", "availableTime", "usage", "otherRules"); |
| | | /** |
| | | * 购买须知里要落库的字段。{@code validPeriod} 是体检医院在后台团单里叫「可预约时间」的那一项, |
| | | * 后台保存时写在同一个 purchase_notice 里,商家端要能读写同一个键,两端才不会各存一份。 |
| | | */ |
| | | private static final List<String> NOTICE_FIELDS = List.of("validity", "availableTime", "validPeriod", "usage", "otherRules"); |
| | | |
| | | private final JdbcTemplate jdbc; |
| | | |
| | |
| | | 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", businessStatus(merchant.get("businessStatus"))); |
| | | result.put("businessStatus", isBusinessOpen(merchant, principal.storeId()) ? "open" : "closed"); |
| | | // 商家端要按商家类型决定商品表单显示哪些字段(体检医院只配可预约时间 + 检查项目), |
| | | // 类型跟着资料一起返回,页面不用再打一次接口。 |
| | | result.put("merchantTypeCode", value(merchant.get("merchantTypeCode"), "")); |
| | | 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); |
| | |
| | | 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", status, "updatedAt", java.time.LocalDateTime.now().toString()); |
| | | return Map.of( |
| | | "businessStatus", isBusinessOpen(merchant(principal.merchantId()), principal.storeId()) ? "open" : "closed", |
| | | "updatedAt", java.time.LocalDateTime.now().toString()); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | 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)); |
| | | Map<String, Object> detail = toProductDetail(records.get(0)); |
| | | detail.put("merchantTypeCode", value(merchant(principal.merchantId()).get("merchantTypeCode"), "")); |
| | | return detail; |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | product(principal, productId); |
| | | String coverUrl = request.coverUrl() == null ? "" : request.coverUrl().trim(); |
| | | if (coverUrl.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传商品封面图"); |
| | | // 体检医院在后台团单里没有「商品金额」这一项(后台保存时落 0,人才端据此显示「免费」), |
| | | // 所以这里按商家类型区分:非体检医院仍然必须有大于 0 的金额。 |
| | | BigDecimal salePrice = request.salePrice(); |
| | | if (salePrice == null || salePrice.signum() <= 0) { |
| | | if (!"medical".equals(value(merchant(principal.merchantId()).get("merchantTypeCode"), ""))) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商品金额必须大于0"); |
| | | } |
| | | salePrice = BigDecimal.ZERO; |
| | | } |
| | | 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), "")); |
| | |
| | | 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, |
| | | """, request.productName().trim(), salePrice, coverUrl, images, |
| | | value(request.detailText(), ""), jsonObject(notice), productId, principal.merchantId()); |
| | | appendProductAuditRecord(productId); |
| | | return product(principal, productId); |
| | | } |
| | | |
| | | private void appendProductAuditRecord(Long productId) { |
| | | jdbc.update(""" |
| | | update merchant_product_audit_record |
| | | set audit_status = 4, audit_remark = coalesce(audit_remark, 'superseded by a new submission') |
| | | where product_id = ? and deleted = 0 and audit_status = 1 |
| | | """, productId); |
| | | jdbc.update(""" |
| | | insert into merchant_product_audit_record( |
| | | product_id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, source_created_at |
| | | ) |
| | | select id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, created_at |
| | | from merchant_product |
| | | where id = ? and deleted = 0 |
| | | """, productId); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | @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 |
| | |
| | | )).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, updated_at |
| | | select id, merchant_name, merchant_type_code, 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 "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 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"; |