4
This commit is contained in:
@@ -66,7 +66,9 @@ public class AdminConsultController {
|
||||
}
|
||||
Long uid = (userId != null ? userId : (adminId != null ? adminId : defaults.getUserId()));
|
||||
jdbcTemplate.update("INSERT INTO consult_replies (consult_id,user_id,content,created_at) VALUES (?,?,?,NOW())", id, uid, content);
|
||||
return ResponseEntity.ok().build();
|
||||
// 自动判定为已解决
|
||||
jdbcTemplate.update("UPDATE consults SET status='resolved', updated_at=NOW() WHERE id=?", id);
|
||||
return ResponseEntity.ok(java.util.Map.of("status", "resolved"));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/resolve")
|
||||
|
||||
@@ -24,8 +24,9 @@ public class AdminPartController {
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted " +
|
||||
"FROM products p JOIN shops s ON s.id=p.shop_id WHERE p.deleted_at IS NULL");
|
||||
"SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted, " +
|
||||
"p.template_id AS templateId, t.name AS templateName, p.attributes_json AS attributesJson " +
|
||||
"FROM products p JOIN shops s ON s.id=p.shop_id LEFT JOIN part_templates t ON t.id=p.template_id WHERE p.deleted_at IS NULL");
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (shopId != null) { sql.append(" AND p.shop_id=?"); ps.add(shopId); }
|
||||
if (kw != null && !kw.isBlank()) {
|
||||
@@ -50,6 +51,10 @@ public class AdminPartController {
|
||||
m.put("brand", rs.getString("brand"));
|
||||
m.put("model", rs.getString("model"));
|
||||
m.put("spec", rs.getString("spec"));
|
||||
Object tid = rs.getObject("templateId");
|
||||
if (tid != null) m.put("templateId", tid);
|
||||
m.put("templateName", rs.getString("templateName"));
|
||||
m.put("attributesJson", rs.getString("attributesJson"));
|
||||
return m;
|
||||
});
|
||||
// 附加每个商品的图片列表
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "search.fuzzy")
|
||||
public class SearchFuzzyProperties {
|
||||
|
||||
private boolean enabled = true;
|
||||
private BigDecimal defaultTolerance = new BigDecimal("1.0");
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public BigDecimal getDefaultTolerance() { return defaultTolerance; }
|
||||
public void setDefaultTolerance(BigDecimal defaultTolerance) { this.defaultTolerance = defaultTolerance; }
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public class MetadataController {
|
||||
java.util.List<String> enums = com.example.demo.common.JsonUtils.fromJson(p.getEnumOptionsJson(), new com.fasterxml.jackson.core.type.TypeReference<java.util.List<String>>(){});
|
||||
pm.put("enumOptions", enums);
|
||||
pm.put("searchable", p.getSearchable());
|
||||
pm.put("dedupeParticipate", p.getDedupeParticipate());
|
||||
// 不再暴露 dedupeParticipate
|
||||
pm.put("sortOrder", p.getSortOrder());
|
||||
ps.add(pm);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.example.demo.product.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PartTemplateDtos {
|
||||
|
||||
@@ -13,8 +12,11 @@ public class PartTemplateDtos {
|
||||
public boolean required;
|
||||
public String unit; // 自定义单位文本
|
||||
public List<String> enumOptions; // type=enum 时可用
|
||||
public boolean searchable;
|
||||
public boolean dedupeParticipate;
|
||||
public boolean searchable; // 默认参与搜索;前端不再展示开关
|
||||
public boolean fuzzySearchable; // 仅 type=number 生效
|
||||
public java.math.BigDecimal fuzzyTolerance; // 可空=使用默认
|
||||
public boolean cardDisplay; // 是否在用户端货品卡片展示
|
||||
// public boolean dedupeParticipate; // 已废弃,后端忽略
|
||||
public int sortOrder;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ProductDtos {
|
||||
public BigDecimal retailPrice; // from product_prices
|
||||
public String cover; // first image url
|
||||
public Boolean deleted; // derived from deleted_at
|
||||
public java.util.Map<String, String> cardParams; // 货品卡片展示的参数(最多4个,label->value)
|
||||
}
|
||||
|
||||
public static class ProductDetail {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@@ -36,7 +37,16 @@ public class PartTemplateParam {
|
||||
private Boolean searchable;
|
||||
|
||||
@Column(name = "dedupe_participate", nullable = false)
|
||||
private Boolean dedupeParticipate;
|
||||
private Boolean dedupeParticipate = false;
|
||||
|
||||
@Column(name = "fuzzy_searchable", nullable = false)
|
||||
private Boolean fuzzySearchable = false;
|
||||
|
||||
@Column(name = "fuzzy_tolerance", precision = 18, scale = 6)
|
||||
private BigDecimal fuzzyTolerance;
|
||||
|
||||
@Column(name = "card_display", nullable = false)
|
||||
private Boolean cardDisplay = false;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder;
|
||||
@@ -64,8 +74,14 @@ public class PartTemplateParam {
|
||||
public void setEnumOptionsJson(String enumOptionsJson) { this.enumOptionsJson = enumOptionsJson; }
|
||||
public Boolean getSearchable() { return searchable; }
|
||||
public void setSearchable(Boolean searchable) { this.searchable = searchable; }
|
||||
public Boolean getDedupeParticipate() { return dedupeParticipate; }
|
||||
public void setDedupeParticipate(Boolean dedupeParticipate) { this.dedupeParticipate = dedupeParticipate; }
|
||||
public Boolean getDedupeParticipate() { return dedupeParticipate == null ? Boolean.FALSE : dedupeParticipate; }
|
||||
public void setDedupeParticipate(Boolean dedupeParticipate) { this.dedupeParticipate = (dedupeParticipate == null ? Boolean.FALSE : dedupeParticipate); }
|
||||
public Boolean getFuzzySearchable() { return fuzzySearchable; }
|
||||
public void setFuzzySearchable(Boolean fuzzySearchable) { this.fuzzySearchable = fuzzySearchable; }
|
||||
public BigDecimal getFuzzyTolerance() { return fuzzyTolerance; }
|
||||
public void setFuzzyTolerance(BigDecimal fuzzyTolerance) { this.fuzzyTolerance = fuzzyTolerance; }
|
||||
public Boolean getCardDisplay() { return cardDisplay == null ? Boolean.FALSE : cardDisplay; }
|
||||
public void setCardDisplay(Boolean cardDisplay) { this.cardDisplay = (cardDisplay == null ? Boolean.FALSE : cardDisplay); }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
|
||||
@@ -164,8 +164,13 @@ public class PartTemplateService {
|
||||
p.setRequired(def.required);
|
||||
p.setUnit(def.unit);
|
||||
p.setEnumOptionsJson(def.enumOptions == null ? null : JsonUtils.toJson(def.enumOptions));
|
||||
p.setSearchable(def.searchable);
|
||||
p.setDedupeParticipate(def.dedupeParticipate);
|
||||
// 搜索默认参与:若前端未传,置为 true
|
||||
p.setSearchable(def.searchable || true);
|
||||
p.setFuzzySearchable(def.fuzzySearchable);
|
||||
p.setFuzzyTolerance(def.fuzzyTolerance);
|
||||
p.setCardDisplay(def.cardDisplay);
|
||||
// 已忽略 dedupeParticipate(统一置 false 以满足非空约束)
|
||||
p.setDedupeParticipate(false);
|
||||
p.setSortOrder(def.sortOrder == 0 ? idx : def.sortOrder);
|
||||
p.setCreatedAt(now);
|
||||
p.setUpdatedAt(now);
|
||||
@@ -185,7 +190,10 @@ public class PartTemplateService {
|
||||
d.unit = p.getUnit();
|
||||
d.enumOptions = JsonUtils.fromJson(p.getEnumOptionsJson(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>(){});
|
||||
d.searchable = Boolean.TRUE.equals(p.getSearchable());
|
||||
d.dedupeParticipate = Boolean.TRUE.equals(p.getDedupeParticipate());
|
||||
d.fuzzySearchable = Boolean.TRUE.equals(p.getFuzzySearchable());
|
||||
d.fuzzyTolerance = p.getFuzzyTolerance();
|
||||
d.cardDisplay = Boolean.TRUE.equals(p.getCardDisplay());
|
||||
// 不再回传 dedupeParticipate
|
||||
d.sortOrder = p.getSortOrder() == null ? 0 : p.getSortOrder();
|
||||
out.add(d);
|
||||
}
|
||||
@@ -214,6 +222,15 @@ public class PartTemplateService {
|
||||
if (!("string".equals(type) || "number".equals(type) || "boolean".equals(type) || "enum".equals(type) || "date".equals(type))) {
|
||||
throw new IllegalArgumentException("不支持的参数类型: " + type);
|
||||
}
|
||||
// fuzzy 校验:仅 number 类型允许;启用时容差可留空(用默认)或为正数
|
||||
if (Boolean.TRUE.equals(def.fuzzySearchable) && !"number".equals(type)) {
|
||||
throw new IllegalArgumentException("仅 number 类型参数允许开启可模糊查询: " + key);
|
||||
}
|
||||
if ("number".equals(type) && Boolean.TRUE.equals(def.fuzzySearchable) && def.fuzzyTolerance != null) {
|
||||
if (def.fuzzyTolerance.compareTo(java.math.BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("容差需为正数: " + key);
|
||||
}
|
||||
}
|
||||
if (keys.contains(key)) throw new IllegalArgumentException("参数键重复: " + key);
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
@@ -32,17 +32,20 @@ public class ProductService {
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final ProductImageRepository imageRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final com.example.demo.common.SearchFuzzyProperties fuzzyProps;
|
||||
|
||||
public ProductService(ProductRepository productRepository,
|
||||
ProductPriceRepository priceRepository,
|
||||
InventoryRepository inventoryRepository,
|
||||
ProductImageRepository imageRepository,
|
||||
JdbcTemplate jdbcTemplate) {
|
||||
JdbcTemplate jdbcTemplate,
|
||||
com.example.demo.common.SearchFuzzyProperties fuzzyProps) {
|
||||
this.productRepository = productRepository;
|
||||
this.priceRepository = priceRepository;
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.imageRepository = imageRepository;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.fuzzyProps = fuzzyProps;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -121,10 +124,7 @@ public class ProductService {
|
||||
product.setAttributesJson(submission.getAttributesJson());
|
||||
changed = true;
|
||||
}
|
||||
if (submission.getDedupeKey() != null && !submission.getDedupeKey().isBlank()) {
|
||||
product.setDedupeKey(submission.getDedupeKey());
|
||||
changed = true;
|
||||
}
|
||||
// 忽略 dedupeKey
|
||||
if (changed) {
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
productRepository.save(product);
|
||||
@@ -149,13 +149,44 @@ public class ProductService {
|
||||
if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
|
||||
if (templateId != null) { sql.append(" AND p.template_id=?"); ps.add(templateId); }
|
||||
if (paramFilters != null && !paramFilters.isEmpty()) {
|
||||
java.util.Map<String, Boolean> keyFuzzyCache = new java.util.HashMap<>();
|
||||
for (java.util.Map.Entry<String,String> ent : paramFilters.entrySet()) {
|
||||
String key = ent.getKey(); String val = ent.getValue();
|
||||
if (key == null || key.isBlank() || val == null || val.isBlank()) continue;
|
||||
// 精确匹配参数值:将 JSON 值解包后与入参做等值比较,避免 LIKE 导致的误匹配
|
||||
sql.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ?");
|
||||
ps.add("$." + key);
|
||||
ps.add(val.trim());
|
||||
|
||||
boolean fuzzyEnabled = fuzzyProps.isEnabled() && isKeyFuzzyEnabled(key, keyFuzzyCache);
|
||||
java.math.BigDecimal valNum = null;
|
||||
boolean numericOk = false;
|
||||
if (fuzzyEnabled) {
|
||||
try {
|
||||
valNum = new java.math.BigDecimal(val.trim());
|
||||
numericOk = true;
|
||||
} catch (Exception ignore) { numericOk = false; }
|
||||
}
|
||||
|
||||
if (fuzzyEnabled && numericOk) {
|
||||
// 行级模糊:存在 fuzzy 定义则按区间,否则按等值(NOT EXISTS 分支)
|
||||
sql.append(" AND ( ")
|
||||
.append("EXISTS (SELECT 1 FROM part_template_params ptp WHERE ptp.template_id=p.template_id AND ptp.field_key=? AND ptp.type='number' AND ptp.fuzzy_searchable=1 ")
|
||||
.append("AND CAST(JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) AS DECIMAL(18,6)) BETWEEN GREATEST(0, ? - COALESCE(ptp.fuzzy_tolerance, ?)) AND (? + COALESCE(ptp.fuzzy_tolerance, ?)) ) ")
|
||||
.append(" OR (NOT EXISTS (SELECT 1 FROM part_template_params ptp2 WHERE ptp2.template_id=p.template_id AND ptp2.field_key=? AND ptp2.type='number' AND ptp2.fuzzy_searchable=1) ")
|
||||
.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ? ) )");
|
||||
|
||||
ps.add(key); // ptp.field_key
|
||||
ps.add("$." + key); // json path
|
||||
ps.add(valNum); // v for lower
|
||||
ps.add(fuzzyProps.getDefaultTolerance()); // default tol
|
||||
ps.add(valNum); // v for upper
|
||||
ps.add(fuzzyProps.getDefaultTolerance()); // default tol
|
||||
ps.add(key); // ptp2.field_key for NOT EXISTS
|
||||
ps.add("$." + key); // json path for equality
|
||||
ps.add(val.trim()); // equality value
|
||||
} else {
|
||||
// 直接等值匹配(包括:未启用全局模糊、该字段不在任何模板中启用模糊、或值非数字)
|
||||
sql.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ?");
|
||||
ps.add("$." + key);
|
||||
ps.add(val.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
|
||||
@@ -173,11 +204,49 @@ public class ProductService {
|
||||
it.retailPrice = rp;
|
||||
it.cover = rs.getString("cover");
|
||||
it.deleted = rs.getBoolean("deleted");
|
||||
// 取卡片展示参数:根据模板定义 card_display=1,最多4个
|
||||
try {
|
||||
String json = jdbcTemplate.queryForObject(
|
||||
"SELECT p.attributes_json FROM products p WHERE p.id=?", String.class, it.id);
|
||||
java.util.Map<String,Object> params = com.example.demo.common.JsonUtils.fromJson(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String,Object>>() {});
|
||||
java.util.LinkedHashMap<String,String> map = new java.util.LinkedHashMap<>();
|
||||
java.util.List<java.util.Map<String,Object>> defs = jdbcTemplate.query(
|
||||
"SELECT field_key, field_label, unit FROM part_template_params WHERE template_id=(SELECT template_id FROM products WHERE id=?) AND card_display=1 ORDER BY sort_order, id LIMIT 4",
|
||||
ps2 -> ps2.setLong(1, it.id),
|
||||
(r2, rn2) -> {
|
||||
java.util.Map<String,Object> m = new java.util.HashMap<>();
|
||||
m.put("key", r2.getString("field_key"));
|
||||
m.put("label", r2.getString("field_label"));
|
||||
m.put("unit", r2.getString("unit"));
|
||||
return m;
|
||||
}
|
||||
);
|
||||
for (java.util.Map<String,Object> d : defs) {
|
||||
String k = (String)d.get("key");
|
||||
String label = (String)d.get("label");
|
||||
String unit = (String)d.get("unit");
|
||||
Object v = params == null ? null : params.get(k);
|
||||
if (v == null) continue;
|
||||
String val = String.valueOf(v) + (unit==null||unit.isBlank()?"":"" );
|
||||
map.put(label + (unit==null||unit.isBlank()?"":"("+unit+")"), val);
|
||||
}
|
||||
it.cardParams = map;
|
||||
} catch (Exception ignore) {}
|
||||
return it;
|
||||
}, ps.toArray());
|
||||
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
|
||||
}
|
||||
|
||||
private boolean isKeyFuzzyEnabled(String fieldKey, java.util.Map<String, Boolean> cache) {
|
||||
if (cache.containsKey(fieldKey)) return cache.get(fieldKey);
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM part_template_params WHERE field_key=? AND type='number' AND fuzzy_searchable=1",
|
||||
Integer.class, fieldKey);
|
||||
boolean enabled = cnt != null && cnt > 0;
|
||||
cache.put(fieldKey, enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public Optional<ProductDtos.ProductDetail> findDetail(Long id, boolean includeDeleted) {
|
||||
Optional<Product> op = productRepository.findById(id);
|
||||
if (op.isEmpty()) return Optional.empty();
|
||||
@@ -235,7 +304,7 @@ public class ProductService {
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
// 单位字段已移除
|
||||
p.setDedupeKey(emptyToNull(req.dedupeKey));
|
||||
// 忽略 dedupeKey
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
@@ -290,7 +359,7 @@ public class ProductService {
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
// 单位字段已移除
|
||||
p.setDedupeKey(emptyToNull(req.dedupeKey));
|
||||
// 忽略 dedupeKey
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
|
||||
@@ -79,7 +79,7 @@ public class ProductSubmissionService {
|
||||
submission.setBarcode(req.barcode);
|
||||
submission.setSafeMin(req.safeMin);
|
||||
submission.setSafeMax(req.safeMax);
|
||||
submission.setDedupeKey(buildDedupeKey(req.templateId, req.name, req.model, req.brand, req.parameters));
|
||||
// 按“前端隐藏 + 后端忽略”方案:不再计算/使用 dedupeKey(兼容历史字段保留)
|
||||
submission.setStatus(ProductSubmission.Status.pending);
|
||||
submission.setCreatedAt(LocalDateTime.now());
|
||||
submission.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -223,9 +223,7 @@ public class ProductSubmissionService {
|
||||
if (req.barcode != null) submission.setBarcode(req.barcode);
|
||||
if (req.safeMin != null) submission.setSafeMin(req.safeMin);
|
||||
if (req.safeMax != null) submission.setSafeMax(req.safeMax);
|
||||
// 参数或基础字段变更后重算去重键
|
||||
Map<String,Object> params = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>() {});
|
||||
submission.setDedupeKey(buildDedupeKey(submission.getTemplateId(), submission.getName(), submission.getModelUnique(), submission.getBrand(), params));
|
||||
// 不再重算 dedupeKey(忽略去重键)
|
||||
submission.setUpdatedAt(LocalDateTime.now());
|
||||
submissionRepository.save(submission);
|
||||
}
|
||||
@@ -472,7 +470,7 @@ public class ProductSubmissionService {
|
||||
payload.origin = submission.getOrigin();
|
||||
payload.categoryId = submission.getCategoryId();
|
||||
// 单位字段已移除
|
||||
payload.dedupeKey = submission.getDedupeKey();
|
||||
// 不再透传 dedupeKey
|
||||
payload.safeMin = submission.getSafeMin();
|
||||
payload.safeMax = submission.getSafeMax();
|
||||
payload.remark = submission.getRemarkText();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Fuzzy search global configuration
|
||||
search.fuzzy.enabled=true
|
||||
search.fuzzy.default-tolerance=1.0
|
||||
spring.application.name=demo
|
||||
|
||||
# 数据源配置(通过环境变量注入,避免硬编码)
|
||||
|
||||
Reference in New Issue
Block a user