This commit is contained in:
2025-09-30 00:03:43 +08:00
parent 19117de6c8
commit 8885fb766a
39 changed files with 517 additions and 159 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -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")

View File

@@ -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;
});
// 附加每个商品的图片列表

View File

@@ -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; }
}

View File

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

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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; }

View File

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

View File

@@ -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));

View File

@@ -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();

View File

@@ -1,3 +1,6 @@
# Fuzzy search global configuration
search.fuzzy.enabled=true
search.fuzzy.default-tolerance=1.0
spring.application.name=demo
# 数据源配置(通过环境变量注入,避免硬编码)

View File

@@ -226,3 +226,35 @@
2025-09-29 21:30:25.754 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:30:25.756 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:30:25.775 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:51:40.924 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:51:40.926 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:51:40.927 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:51:40.952 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:10:05.539 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:10:05.541 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 22:10:05.542 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 22:10:05.564 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:16:24.106 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:16:24.107 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 22:16:24.108 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 22:16:24.128 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:40:06.467 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:40:06.469 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 22:40:06.470 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 22:40:06.491 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:47:48.473 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:47:48.474 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 22:47:48.477 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 22:47:48.497 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:51:18.065 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 22:51:18.066 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 22:51:18.068 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 22:51:18.090 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 23:25:18.836 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 23:25:18.837 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 23:25:18.839 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 23:25:18.861 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 23:34:39.749 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 23:34:39.750 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 23:34:39.751 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 23:34:39.773 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}