This commit is contained in:
2025-09-29 21:38:32 +08:00
parent ed26244cdb
commit 19117de6c8
182 changed files with 11590 additions and 2156 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -156,7 +156,7 @@ CREATE TABLE IF NOT EXISTS products (
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(120) NOT NULL,
category_id BIGINT UNSIGNED NULL,
unit_id BIGINT UNSIGNED NOT NULL,
-- unit_id 已移除
brand VARCHAR(64) NULL,
model VARCHAR(64) NULL,
spec VARCHAR(128) NULL,
@@ -175,12 +175,12 @@ CREATE TABLE IF NOT EXISTS products (
UNIQUE KEY ux_products_shop_barcode (shop_id, barcode),
KEY idx_products_shop (shop_id),
KEY idx_products_category (category_id),
KEY idx_products_unit (unit_id),
-- KEY idx_products_unit (unit_id),
FULLTEXT KEY ft_products_search (name, brand, model, spec, search_text),
CONSTRAINT fk_products_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_products_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES product_categories(id),
CONSTRAINT fk_products_unit FOREIGN KEY (unit_id) REFERENCES product_units(id),
-- CONSTRAINT fk_products_unit FOREIGN KEY (unit_id) REFERENCES product_units(id),
CONSTRAINT fk_products_globalsku FOREIGN KEY (global_sku_id) REFERENCES global_skus(id),
CONSTRAINT ck_products_safe_range CHECK (safe_min IS NULL OR safe_max IS NULL OR safe_min <= safe_max)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品';

View File

@@ -71,11 +71,7 @@ public class AdminDictController {
public ResponseEntity<?> deleteUnit(@PathVariable("id") Long id) {
ProductUnit u = unitRepository.findById(id).orElse(null);
if (u == null || !u.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
// 引用保护:若有商品使用该单位,阻止删除
Long cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE unit_id=?", Long.class, id);
if (cnt != null && cnt > 0) {
return ResponseEntity.status(409).body(Map.of("message","存在引用,无法删除"));
}
// 按新方案:移除对 products.unit_id 的引用校验(该字段已移除)
unitRepository.deleteById(id);
return ResponseEntity.ok().build();
}
@@ -121,12 +117,15 @@ public class AdminDictController {
public ResponseEntity<?> deleteCategory(@PathVariable("id") Long id) {
ProductCategory c = categoryRepository.findById(id).orElse(null);
if (c == null || !c.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
// 子类与引用保护
Long child = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM product_categories WHERE parent_id=?", Long.class, id);
if (child != null && child > 0) return ResponseEntity.status(409).body(Map.of("message","存在子类,无法删除"));
Long cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE category_id=?", Long.class, id);
if (cnt != null && cnt > 0) return ResponseEntity.status(409).body(Map.of("message","存在引用,无法删除"));
categoryRepository.deleteById(id);
// 平台管理员二次确认可在拦截器或前端完成;此处执行软删级联
// 1) 软删分类
jdbcTemplate.update("UPDATE product_categories SET deleted_at=NOW(), updated_at=NOW() WHERE id=? AND deleted_at IS NULL", id);
// 2) 软删分类下模板(使用 deleted_at 统一标记)
jdbcTemplate.update("UPDATE part_templates SET deleted_at=NOW(), updated_at=NOW() WHERE category_id=? AND (deleted_at IS NULL)", id);
// 3) 软删该分类下的所有商品:包括通过模板创建的与直接挂分类的
jdbcTemplate.update("UPDATE products SET deleted_at=NOW(), updated_at=NOW() WHERE (category_id=? OR template_id IN (SELECT id FROM part_templates WHERE category_id=?)) AND deleted_at IS NULL", id, id);
// 4) 软删该分类下的所有配件提交:包含直接指向分类的与指向该分类下模板的
jdbcTemplate.update("UPDATE part_submissions SET deleted_at=NOW(), updated_at=NOW() WHERE (category_id=? OR template_id IN (SELECT id FROM part_templates WHERE category_id=?)) AND deleted_at IS NULL", id, id);
return ResponseEntity.ok().build();
}
}

View File

@@ -21,16 +21,20 @@ public class EmailAuthService {
private final com.example.demo.common.ShopDefaultsProperties shopDefaults;
private final EmailSenderService emailSender;
private final com.example.demo.common.DefaultSeedService defaultSeedService;
public EmailAuthService(JdbcTemplate jdbcTemplate,
JwtService jwtService,
JwtProperties jwtProps,
com.example.demo.common.ShopDefaultsProperties shopDefaults,
EmailSenderService emailSender) {
EmailSenderService emailSender,
com.example.demo.common.DefaultSeedService defaultSeedService) {
this.jdbcTemplate = jdbcTemplate;
this.jwtService = jwtService;
this.jwtProps = jwtProps;
this.shopDefaults = shopDefaults;
this.emailSender = emailSender;
this.defaultSeedService = defaultSeedService;
}
public static class SendCodeRequest { public String email; public String scene; }
@@ -205,6 +209,9 @@ public class EmailAuthService {
Number userGenId = userKey.getKey();
if (userGenId == null) throw new IllegalStateException("创建用户失败");
userId = userGenId.longValue();
// 初始化默认客户/供应商(幂等)
defaultSeedService.initializeForShop(shopId, userId);
}
String token = jwtService.signToken(userId, shopId, null, "email_otp", email);

View File

@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Objects;
import java.util.LinkedHashMap;
@RestController
@RequestMapping("/api/normal-admin")
@@ -27,6 +28,11 @@ public class NormalAdminApplyController {
} else { sidFinal = shopId; }
// 校验 VIP根据配置可选
boolean requireVip = true; // 默认要求VIP有效
try {
String v = jdbc.query("SELECT value FROM system_parameters WHERE `key`='normalAdmin.requiredVipActive' ORDER BY id DESC LIMIT 1",
rs -> rs.next() ? rs.getString(1) : null);
if (v != null) { v = v.trim(); if (v.startsWith("\"") && v.endsWith("\"")) v = v.substring(1, v.length()-1); requireVip = ("true".equalsIgnoreCase(v) || "1".equals(v)); }
} catch (Exception ignored) {}
Integer vipOk = jdbc.query(
"SELECT CASE WHEN (is_vip=1 AND status=1 AND (expire_at IS NULL OR expire_at>NOW())) THEN 1 ELSE 0 END FROM vip_users WHERE user_id=? AND shop_id=? ORDER BY id DESC LIMIT 1",
ps -> { ps.setLong(1, userId); ps.setLong(2, sidFinal); },
@@ -41,7 +47,12 @@ public class NormalAdminApplyController {
ps -> { ps.setLong(1, sidFinal); ps.setLong(2, userId); ps.setString(3, "apply"); ps.setString(4, remark); });
// 是否自动通过
boolean autoApprove = false; // 默认false后续接入 system_parameters
boolean autoApprove = false;
try {
String v = jdbc.query("SELECT value FROM system_parameters WHERE `key`='normalAdmin.autoApprove' ORDER BY id DESC LIMIT 1",
rs -> rs.next() ? rs.getString(1) : null);
if (v != null) { v = v.trim(); if (v.startsWith("\"") && v.endsWith("\"")) v = v.substring(1, v.length()-1); autoApprove = ("true".equalsIgnoreCase(v) || "1".equals(v)); }
} catch (Exception ignored) {}
if (autoApprove) {
// 将角色变更为 normal_admin 并写入 approve 审计
String prev = jdbc.query("SELECT role FROM users WHERE id=?", ps -> ps.setLong(1, userId), rs -> rs.next()? rs.getString(1): null);
@@ -52,6 +63,62 @@ public class NormalAdminApplyController {
return ResponseEntity.ok(Map.of("ok", true));
}
@GetMapping("/application/status")
public ResponseEntity<?> myApplicationStatus(@RequestHeader(name = "X-User-Id") long userId) {
try {
Map<String, Object> out = new LinkedHashMap<>();
// 当前角色
String role = null;
try {
role = jdbc.query("SELECT role FROM users WHERE id=? LIMIT 1",
ps -> ps.setLong(1, userId), rs -> rs.next() ? rs.getString(1) : null);
} catch (Exception ignored) {}
boolean isNormalAdmin = role != null && "normal_admin".equalsIgnoreCase(role.trim());
// 最近一次审计动作
Map<String, Object> last = null;
try {
last = jdbc.query(
"SELECT action, created_at AS createdAt, remark FROM normal_admin_audits WHERE user_id=? ORDER BY created_at DESC LIMIT 1",
ps -> ps.setLong(1, userId),
rs -> {
if (!rs.next()) return null;
Map<String,Object> m = new LinkedHashMap<>();
m.put("action", rs.getString("action"));
m.put("createdAt", rs.getTimestamp("createdAt"));
m.put("remark", rs.getString("remark"));
return m;
}
);
} catch (Exception ignored) {}
String applicationStatus = "none";
if (isNormalAdmin) {
applicationStatus = "approved";
} else if (last != null) {
String action = (String) last.get("action");
if ("apply".equalsIgnoreCase(action)) applicationStatus = "pending";
else if ("approve".equalsIgnoreCase(action)) applicationStatus = "approved";
else if ("reject".equalsIgnoreCase(action)) applicationStatus = "rejected";
else if ("revoke".equalsIgnoreCase(action)) applicationStatus = "revoked";
}
out.put("isNormalAdmin", isNormalAdmin);
out.put("applicationStatus", applicationStatus);
if (last != null) {
out.put("lastAction", last.get("action"));
out.put("lastActionAt", last.get("createdAt"));
out.put("lastRemark", last.get("remark"));
}
return ResponseEntity.ok(out);
} catch (Exception e) {
Map<String,Object> fallback = new LinkedHashMap<>();
fallback.put("isNormalAdmin", false);
fallback.put("applicationStatus", "none");
return ResponseEntity.ok(fallback);
}
}
}

View File

@@ -20,6 +20,7 @@ public class RegisterService {
private final JwtProperties jwtProps;
private final ShopDefaultsProperties shopDefaults;
private AppDefaultsProperties appDefaults;
private com.example.demo.common.DefaultSeedService defaultSeedService;
public RegisterService(JdbcTemplate jdbcTemplate,
JwtService jwtService,
@@ -36,6 +37,11 @@ public class RegisterService {
this.appDefaults = appDefaults;
}
@Autowired
public void setDefaultSeedService(com.example.demo.common.DefaultSeedService defaultSeedService) {
this.defaultSeedService = defaultSeedService;
}
private String hashPassword(String raw) {
try {
return org.springframework.security.crypto.bcrypt.BCrypt.hashpw(raw, org.springframework.security.crypto.bcrypt.BCrypt.gensalt(10));
@@ -108,6 +114,11 @@ public class RegisterService {
// 3) 创建默认账户(现金/银行存款/微信)
createDefaultAccounts(shopId, userId);
// 4) 初始化默认客户/供应商(幂等)
if (defaultSeedService != null) {
defaultSeedService.initializeForShop(shopId, userId);
}
}
String token = jwtService.signToken(userId, shopId, phone, "register");

View File

@@ -18,6 +18,10 @@ public class AppDefaultsProperties {
private String accountWechatName = "微信";
private String accountAlipayName = "支付宝";
// 默认往来单位名称(配置化,避免硬编码)
private String customerName = "散客";
private String supplierName = "默认供应商";
public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; }
@@ -34,6 +38,12 @@ public class AppDefaultsProperties {
public void setAccountWechatName(String accountWechatName) { this.accountWechatName = accountWechatName; }
public String getAccountAlipayName() { return accountAlipayName; }
public void setAccountAlipayName(String accountAlipayName) { this.accountAlipayName = accountAlipayName; }
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public String getSupplierName() { return supplierName; }
public void setSupplierName(String supplierName) { this.supplierName = supplierName; }
}

View File

@@ -0,0 +1,41 @@
package com.example.demo.common;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DefaultSeedService {
private final JdbcTemplate jdbcTemplate;
private final AppDefaultsProperties appDefaults;
public DefaultSeedService(JdbcTemplate jdbcTemplate, AppDefaultsProperties appDefaults) {
this.jdbcTemplate = jdbcTemplate;
this.appDefaults = appDefaults;
}
/**
* 幂等初始化:为新店铺创建默认客户/供应商(若不存在)。
*/
@Transactional
public void initializeForShop(Long shopId, Long userId) {
if (shopId == null || userId == null) return;
// 默认客户
jdbcTemplate.update(
"INSERT INTO customers (shop_id,user_id,name,price_level,status,created_at,updated_at) " +
"SELECT ?, ?, ?, 'retail', 1, NOW(), NOW() FROM DUAL " +
"WHERE NOT EXISTS (SELECT 1 FROM customers WHERE shop_id=? AND name=?)",
shopId, userId, appDefaults.getCustomerName(), shopId, appDefaults.getCustomerName()
);
// 默认供应商
jdbcTemplate.update(
"INSERT INTO suppliers (shop_id,user_id,name,status,created_at,updated_at) " +
"SELECT ?, ?, ?, 1, NOW(), NOW() FROM DUAL " +
"WHERE NOT EXISTS (SELECT 1 FROM suppliers WHERE shop_id=? AND name=?)",
shopId, userId, appDefaults.getSupplierName(), shopId, appDefaults.getSupplierName()
);
}
}

View File

@@ -59,7 +59,16 @@ public class NormalAdminAuthInterceptor implements HandlerInterceptor {
}
// 可选校验VIP 有效
boolean requireVip = Boolean.parseBoolean(String.valueOf(System.getenv().getOrDefault("NORMAL_ADMIN_REQUIRE_VIP_ACTIVE", "true")));
boolean requireVip;
try {
String v = jdbcTemplate.query("SELECT value FROM system_parameters WHERE `key`='normalAdmin.requiredVipActive' ORDER BY id DESC LIMIT 1",
rs -> rs.next() ? rs.getString(1) : null);
if (v == null) requireVip = true; else {
v = v.trim();
if (v.startsWith("\"") && v.endsWith("\"")) v = v.substring(1, v.length()-1);
requireVip = "true".equalsIgnoreCase(v) || "1".equals(v);
}
} catch (Exception e) { requireVip = true; }
if (requireVip) {
Integer vipOk = jdbcTemplate.query(
"SELECT CASE WHEN (is_vip=1 AND status=1 AND (expire_at IS NULL OR expire_at>NOW())) THEN 1 ELSE 0 END FROM vip_users WHERE user_id=? AND shop_id=? ORDER BY id DESC LIMIT 1",

View File

@@ -4,6 +4,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@@ -29,6 +30,19 @@ public class WebConfig implements WebMvcConfigurer {
InterceptorRegistration nr = registry.addInterceptor(normalAdminAuthInterceptor);
nr.addPathPatterns("/api/normal-admin/parts/**");
}
@Override
public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) {
// 将 /static/** 映射到前端静态资源目录(开发时)与 classpath 静态目录(部署时)
String userDir = System.getProperty("user.dir");
String frontendStatic = userDir + java.io.File.separator + "frontend" + java.io.File.separator + "static" + java.io.File.separator;
registry.addResourceHandler("/static/**")
.addResourceLocations(
"file:" + frontendStatic,
"classpath:/static/",
"classpath:/public/"
);
}
}

View File

@@ -27,14 +27,18 @@ public class OrderService {
private final JdbcTemplate jdbcTemplate;
private final ProductPriceRepository productPriceRepository;
private final com.example.demo.common.AppDefaultsProperties appDefaults;
public OrderService(InventoryRepository inventoryRepository,
JdbcTemplate jdbcTemplate,
AccountDefaultsProperties accountDefaults,
ProductPriceRepository productPriceRepository) {
ProductPriceRepository productPriceRepository,
com.example.demo.common.AppDefaultsProperties appDefaults) {
this.inventoryRepository = inventoryRepository;
this.jdbcTemplate = jdbcTemplate;
this.accountDefaults = accountDefaults;
this.productPriceRepository = productPriceRepository;
this.appDefaults = appDefaults;
}
@Transactional
@@ -134,8 +138,12 @@ public class OrderService {
"VALUES (?,?,?,?,'approved', ?, 0, ?, NOW(), NOW())";
}
Long customerId = req.customerId;
Long supplierId = req.supplierId;
final Long customerId = (isSalesHead && req.customerId == null)
? resolveOrCreateDefaultCustomer(shopId, userId)
: req.customerId;
final Long supplierId = (isPurchaseHead && req.supplierId == null)
? resolveOrCreateDefaultSupplier(shopId, userId)
: req.supplierId;
jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(headSql, new String[]{"id"});
int idx = 1;
@@ -188,6 +196,28 @@ public class OrderService {
return new OrderDtos.CreateOrderResponse(orderId, orderNo);
}
private Long resolveOrCreateDefaultCustomer(Long shopId, Long userId) {
String name = appDefaults.getCustomerName();
java.util.List<Long> ids = jdbcTemplate.query("SELECT id FROM customers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
jdbcTemplate.update("INSERT INTO customers (shop_id,user_id,name,price_level,status,created_at,updated_at) VALUES (?,?,?,'retail',1,NOW(),NOW())",
shopId, userId, name);
ids = jdbcTemplate.query("SELECT id FROM customers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
throw new IllegalStateException("默认客户创建失败");
}
private Long resolveOrCreateDefaultSupplier(Long shopId, Long userId) {
String name = appDefaults.getSupplierName();
java.util.List<Long> ids = jdbcTemplate.query("SELECT id FROM suppliers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
jdbcTemplate.update("INSERT INTO suppliers (shop_id,user_id,name,status,created_at,updated_at) VALUES (?,?,?,1,NOW(),NOW())",
shopId, userId, name);
ids = jdbcTemplate.query("SELECT id FROM suppliers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
throw new IllegalStateException("默认供应商创建失败");
}
private BigDecimal resolveProductCostPrice(Long productId, Long shopId) {
return productPriceRepository.findById(productId)
.filter(price -> price.getShopId().equals(shopId))

View File

@@ -49,11 +49,14 @@ public class MetadataController {
@GetMapping("/api/product-templates")
public ResponseEntity<?> listTemplates(@RequestParam(name = "categoryId", required = false) Long categoryId) {
Map<String, Object> body = new HashMap<>();
// 排除已软删模板;仍要求 status=1 才可见
java.util.List<com.example.demo.product.entity.PartTemplate> list =
categoryId == null ? templateRepository.findByStatusOrderByIdDesc(1)
: templateRepository.findByStatusAndCategoryIdOrderByIdDesc(1, categoryId);
(categoryId == null)
? templateRepository.findByStatusOrderByIdDesc(1)
: templateRepository.findByStatusAndCategoryIdOrderByIdDesc(1, categoryId);
java.util.List<java.util.Map<String,Object>> out = new java.util.ArrayList<>();
for (com.example.demo.product.entity.PartTemplate t : list) {
try { if (t.getDeletedAt() != null) continue; } catch (Exception ignore) {}
java.util.Map<String,Object> m = new java.util.HashMap<>();
m.put("id", t.getId());
m.put("categoryId", t.getCategoryId());

View File

@@ -1,7 +1,9 @@
package com.example.demo.product.controller;
import com.example.demo.product.service.ProductSubmissionService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -9,19 +11,28 @@ import org.springframework.web.bind.annotation.*;
public class NormalAdminSubmissionController {
private final ProductSubmissionService submissionService;
private final JdbcTemplate jdbc;
public NormalAdminSubmissionController(ProductSubmissionService submissionService) {
public NormalAdminSubmissionController(ProductSubmissionService submissionService,
JdbcTemplate jdbc) {
this.submissionService = submissionService;
this.jdbc = jdbc;
}
// 代理现有管理端接口,但不暴露跨店查询参数,实际范围由拦截器限定
private Long findShopIdByUser(Long userId) {
if (userId == null) return null;
return jdbc.query("SELECT shop_id FROM users WHERE id=?", ps -> ps.setLong(1, userId), rs -> rs.next()? rs.getLong(1): null);
}
// 代理现有管理端接口,但范围限定为当前用户所属店铺
@GetMapping("/submissions")
public ResponseEntity<?> list(@RequestParam(name = "status", required = false) String status,
public ResponseEntity<?> list(@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size) {
// 普通管理端不允许跨店过滤reviewer/shopId 均不提供
return ResponseEntity.ok(submissionService.listAdmin(status, kw, null, null, null, null, page, size));
Long shopId = findShopIdByUser(userId);
return ResponseEntity.ok(submissionService.listAdmin(status, kw, shopId, null, null, null, page, size));
}
@GetMapping("/submissions/{id}")
@@ -42,7 +53,6 @@ public class NormalAdminSubmissionController {
public ResponseEntity<?> approve(@PathVariable("id") Long id,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody(required = false) com.example.demo.product.dto.ProductSubmissionDtos.ApproveRequest req) {
// 这里将 X-User-Id 作为审批人记录(普通管理员为用户表)
var resp = submissionService.approve(id, userId, req);
return ResponseEntity.ok(resp);
}
@@ -54,6 +64,15 @@ public class NormalAdminSubmissionController {
submissionService.reject(id, userId, req);
return ResponseEntity.ok(java.util.Map.of("ok", true));
}
@GetMapping("/submissions/export")
public void export(@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "kw", required = false) String kw,
HttpServletResponse response) {
Long shopId = findShopIdByUser(userId);
submissionService.export(status, kw, shopId, null, null, null, response);
}
}

View File

@@ -38,6 +38,15 @@ public class PartTemplateController {
public ResponseEntity<?> list() {
return ResponseEntity.ok(templateService.list());
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable("id") Long id,
@RequestParam(value = "force", required = false) Boolean force) {
templateService.delete(id, Boolean.TRUE.equals(force));
return ResponseEntity.ok(java.util.Map.of("ok", true));
}
// 分类级联软删将在 AdminDictController 中触发;此处保持模板单体删除逻辑
}

View File

@@ -23,18 +23,29 @@ public class ProductController {
public ResponseEntity<?> search(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "categoryId", required = false) Long categoryId,
@RequestParam(name = "templateId", required = false) Long templateId,
@RequestParam java.util.Map<String, String> requestParams,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "50") int size) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, Math.max(page - 1, 0), size);
java.util.Map<String, String> paramFilters = new java.util.HashMap<>();
for (java.util.Map.Entry<String, String> e : requestParams.entrySet()) {
String k = e.getKey();
if (k != null && k.startsWith("param_") && e.getValue() != null && !e.getValue().isBlank()) {
String key = k.substring(6);
if (!key.isBlank()) paramFilters.put(key, e.getValue());
}
}
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, templateId, paramFilters, Math.max(page - 1, 0), size);
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("list", result.getContent());
return ResponseEntity.ok(body);
}
@GetMapping("/{id}")
public ResponseEntity<?> detail(@PathVariable("id") Long id) {
return productService.findDetail(id)
public ResponseEntity<?> detail(@PathVariable("id") Long id,
@RequestParam(name = "includeDeleted", required = false, defaultValue = "false") boolean includeDeleted) {
return productService.findDetail(id, includeDeleted)
.<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@@ -61,6 +72,15 @@ public class ProductController {
productService.update(id, sid, uid, req);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable("id") Long id,
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
productService.delete(id, sid);
return ResponseEntity.ok().build();
}
}

View File

@@ -15,6 +15,7 @@ public class ProductDtos {
public BigDecimal stock; // from inventories.quantity
public BigDecimal retailPrice; // from product_prices
public String cover; // first image url
public Boolean deleted; // derived from deleted_at
}
public static class ProductDetail {
@@ -26,7 +27,8 @@ public class ProductDtos {
public String spec;
public String origin;
public Long categoryId;
public Long unitId;
// 单位字段已移除
public Long templateId;
public BigDecimal safeMin;
public BigDecimal safeMax;
public BigDecimal stock;
@@ -35,6 +37,10 @@ public class ProductDtos {
public BigDecimal wholesalePrice;
public BigDecimal bigClientPrice;
public List<Image> images;
public Map<String, Object> parameters;
public Long sourceSubmissionId;
public String externalCode;
public Boolean deleted;
}
public static class Image {

View File

@@ -8,12 +8,13 @@ public class ProductSubmissionDtos {
public static class CreateRequest {
public Long templateId;
public String externalCode; // 外部编号
public String name;
public String model;
public String brand;
public String spec;
public String origin;
public Long unitId;
// 单位字段已移除
public Long categoryId;
public Map<String, Object> parameters;
public List<String> images;
@@ -25,11 +26,12 @@ public class ProductSubmissionDtos {
public static class UpdateRequest {
public Long templateId;
public String externalCode; // 外部编号
public String name;
public String brand;
public String spec;
public String origin;
public Long unitId;
// 单位字段已移除
public Long categoryId;
public Map<String, Object> parameters;
public List<String> images;
@@ -72,12 +74,13 @@ public class ProductSubmissionDtos {
public Long shopId;
public Long userId;
public Long templateId;
public String externalCode;
public String name;
public String model;
public String brand;
public String spec;
public String origin;
public Long unitId;
// 单位字段已移除
public Long categoryId;
public Map<String, Object> parameters;
public List<String> images;

View File

@@ -32,6 +32,9 @@ public class PartTemplate {
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
public Long getId() { return id; }
public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
@@ -47,6 +50,8 @@ public class PartTemplate {
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}

View File

@@ -24,9 +24,6 @@ public class Product {
@Column(name = "category_id")
private Long categoryId;
@Column(name = "unit_id", nullable = false)
private Long unitId;
@Column(name = "template_id")
private Long templateId;
@@ -84,8 +81,6 @@ public class Product {
public void setName(String name) { this.name = name; }
public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
public Long getUnitId() { return unitId; }
public void setUnitId(Long unitId) { this.unitId = unitId; }
public Long getTemplateId() { return templateId; }
public void setTemplateId(Long templateId) { this.templateId = templateId; }
public String getBrand() { return brand; }

View File

@@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
public interface PartTemplateRepository extends JpaRepository<PartTemplate, Long> {
java.util.List<PartTemplate> findByStatusOrderByIdDesc(Integer status);
java.util.List<PartTemplate> findByStatusAndCategoryIdOrderByIdDesc(Integer status, Long categoryId);
java.util.List<PartTemplate> findByDeletedAtIsNullOrderByIdDesc();
}

View File

@@ -11,10 +11,11 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE p.shopId = :shopId AND (p.deletedAt IS NULL) AND " +
"(:kw IS NULL OR :kw = '' OR p.name LIKE CONCAT('%', :kw, '%') OR p.brand LIKE CONCAT('%', :kw, '%') OR p.model LIKE CONCAT('%', :kw, '%') OR p.spec LIKE CONCAT('%', :kw, '%') OR p.barcode LIKE CONCAT('%', :kw, '%')) AND " +
"(:categoryId IS NULL OR p.categoryId = :categoryId) ORDER BY p.id DESC")
"(:categoryId IS NULL OR p.categoryId = :categoryId) AND (:templateId IS NULL OR p.templateId = :templateId) ORDER BY p.id DESC")
Page<Product> search(@Param("shopId") Long shopId,
@Param("kw") String kw,
@Param("categoryId") Long categoryId,
@Param("templateId") Long templateId,
Pageable pageable);
boolean existsByShopIdAndBarcode(Long shopId, String barcode);

View File

@@ -14,7 +14,9 @@ public interface ProductSubmissionRepository extends JpaRepository<ProductSubmis
Page<ProductSubmission> findByShopIdAndUserIdAndStatusIn(Long shopId, Long userId, List<ProductSubmission.Status> statuses, Pageable pageable);
@Query("SELECT ps FROM ProductSubmission ps WHERE (:statusList IS NULL OR ps.status IN :statusList) " +
Page<ProductSubmission> findByShopIdAndUserIdAndStatusInAndDeletedAtIsNull(Long shopId, Long userId, List<ProductSubmission.Status> statuses, Pageable pageable);
@Query("SELECT ps FROM ProductSubmission ps WHERE ps.deletedAt IS NULL AND (:statusList IS NULL OR ps.status IN :statusList) " +
"AND (:kw IS NULL OR ps.modelUnique LIKE :kw OR ps.name LIKE :kw OR ps.brand LIKE :kw) " +
"AND (:shopId IS NULL OR ps.shopId = :shopId) " +
"AND (:reviewerId IS NULL OR ps.reviewerId = :reviewerId) " +

View File

@@ -64,6 +64,15 @@ public class PartTemplateService {
if (req.modelRule != null) t.setModelRule(req.modelRule);
if (req.status != null) t.setStatus(req.status);
t.setUpdatedAt(LocalDateTime.now());
try {
// 若模板已被软删,不允许通过 update 将其“启用”,需运维恢复
java.lang.reflect.Field f = t.getClass().getDeclaredField("deletedAt");
f.setAccessible(true);
Object v = f.get(t);
if (v != null && (req.status != null && req.status == 1)) {
throw new IllegalStateException("模板已删除,无法启用。请联系平台管理员");
}
} catch (NoSuchFieldException ignore) { } catch (IllegalAccessException ignore) { }
templateRepository.save(t);
if (req.params != null) {
@@ -117,6 +126,32 @@ public class PartTemplateService {
return out;
}
@Transactional
public void delete(Long id, boolean force) {
if (!force) {
// 软删除:隐藏模板并级联软删该模板下商品
PartTemplate t = templateRepository.findById(id).orElseThrow();
t.setStatus(0);
t.setUpdatedAt(LocalDateTime.now());
// 统一软删标记:写入 deleted_at
try { jdbcTemplate.update("UPDATE part_templates SET deleted_at=NOW() WHERE id=? AND deleted_at IS NULL", id); } catch (Exception ignore) {}
templateRepository.save(t);
// 级联软删商品与配件提交
jdbcTemplate.update("UPDATE products SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
jdbcTemplate.update("UPDATE part_submissions SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
return;
}
// 永久删除:删除参数与模板,并清理关联数据
paramRepository.findByTemplateIdOrderBySortOrderAscIdAsc(id).forEach(p -> paramRepository.deleteById(p.getId()));
jdbcTemplate.update("UPDATE products SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
jdbcTemplate.update("DELETE FROM product_images WHERE product_id IN (SELECT id FROM products WHERE template_id=? )", id);
jdbcTemplate.update("DELETE FROM product_prices WHERE product_id IN (SELECT id FROM products WHERE template_id=? )", id);
jdbcTemplate.update("DELETE FROM inventories WHERE product_id IN (SELECT id FROM products WHERE template_id=? )", id);
jdbcTemplate.update("UPDATE part_submissions SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
templateRepository.deleteById(id);
}
private void upsertParams(Long templateId, List<PartTemplateDtos.ParamDef> params, LocalDateTime now) {
if (params == null) return;
int idx = 0;

View File

@@ -96,10 +96,7 @@ public class ProductService {
product.setCategoryId(submission.getCategoryId());
changed = true;
}
if (submission.getUnitId() != null && !submission.getUnitId().equals(product.getUnitId())) {
product.setUnitId(submission.getUnitId());
changed = true;
}
// 单位字段已移除
if (submission.getRemarkText() != null && !submission.getRemarkText().isBlank()) {
product.setDescription(submission.getRemarkText());
changed = true;
@@ -137,58 +134,55 @@ public class ProductService {
syncImages(submission.getUserId(), productId, product.getShopId(), images);
}
public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, int page, int size) {
try {
Page<Product> p = productRepository.search(shopId, kw, categoryId, PageRequest.of(page, size));
return p.map(prod -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = prod.getId();
it.name = prod.getName();
it.brand = prod.getBrand();
it.model = prod.getModel();
it.spec = prod.getSpec();
inventoryRepository.findById(prod.getId()).ifPresent(inv -> it.stock = inv.getQuantity());
priceRepository.findById(prod.getId()).ifPresent(pr -> it.retailPrice = pr.getRetailPrice());
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(prod.getId());
it.cover = imgs.isEmpty() ? null : imgs.get(0).getUrl();
return it;
});
} catch (Exception e) {
// 安全回退为 JDBC 查询,保障功能可用
StringBuilder sql = new StringBuilder("SELECT p.id,p.name,p.brand,p.model,p.spec,\n" +
"(SELECT i.quantity FROM inventories i WHERE i.product_id=p.id) AS stock,\n" +
"(SELECT pr.retail_price FROM product_prices pr WHERE pr.product_id=p.id) AS retail_price,\n" +
"(SELECT img.url FROM product_images img WHERE img.product_id=p.id ORDER BY img.sort_order, img.id LIMIT 1) AS cover\n" +
"FROM products p WHERE p.shop_id=? AND p.deleted_at IS NULL");
List<Object> ps = new ArrayList<>();
ps.add(shopId);
if (kw != null && !kw.isBlank()) { sql.append(" AND (p.name LIKE ? OR p.brand LIKE ? OR p.model LIKE ? OR p.spec LIKE ? OR p.barcode LIKE ?)");
String like = "%" + kw + "%"; ps.add(like); ps.add(like); ps.add(like); ps.add(like); ps.add(like); }
if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = rs.getLong("id");
it.name = rs.getString("name");
it.brand = rs.getString("brand");
it.model = rs.getString("model");
it.spec = rs.getString("spec");
java.math.BigDecimal st = (java.math.BigDecimal) rs.getObject("stock");
it.stock = st;
java.math.BigDecimal rp = (java.math.BigDecimal) rs.getObject("retail_price");
it.retailPrice = rp;
it.cover = rs.getString("cover");
return it;
}, ps.toArray());
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, Long templateId, java.util.Map<String,String> paramFilters, int page, int size) {
// 直接使用 JDBC 支持 JSON_EXTRACT 过滤MySQL
StringBuilder sql = new StringBuilder("SELECT p.id,p.name,p.brand,p.model,p.spec,\n" +
"(SELECT i.quantity FROM inventories i WHERE i.product_id=p.id) AS stock,\n" +
"(SELECT pr.retail_price FROM product_prices pr WHERE pr.product_id=p.id) AS retail_price,\n" +
"(SELECT img.url FROM product_images img WHERE img.product_id=p.id ORDER BY img.sort_order, img.id LIMIT 1) AS cover,\n" +
"(p.deleted_at IS NOT NULL) AS deleted\n" +
"FROM products p WHERE p.shop_id=? AND p.deleted_at IS NULL");
List<Object> ps = new ArrayList<>();
ps.add(shopId);
if (kw != null && !kw.isBlank()) { sql.append(" AND (p.name LIKE ? OR p.brand LIKE ? OR p.model LIKE ? OR p.spec LIKE ? OR p.barcode LIKE ?)");
String like = "%" + kw + "%"; ps.add(like); ps.add(like); ps.add(like); ps.add(like); ps.add(like); }
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()) {
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());
}
}
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = rs.getLong("id");
it.name = rs.getString("name");
it.brand = rs.getString("brand");
it.model = rs.getString("model");
it.spec = rs.getString("spec");
java.math.BigDecimal st = (java.math.BigDecimal) rs.getObject("stock");
it.stock = st;
java.math.BigDecimal rp = (java.math.BigDecimal) rs.getObject("retail_price");
it.retailPrice = rp;
it.cover = rs.getString("cover");
it.deleted = rs.getBoolean("deleted");
return it;
}, ps.toArray());
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
}
public Optional<ProductDtos.ProductDetail> findDetail(Long id) {
public Optional<ProductDtos.ProductDetail> findDetail(Long id, boolean includeDeleted) {
Optional<Product> op = productRepository.findById(id);
if (op.isEmpty()) return Optional.empty();
Product p = op.get();
if (p.getDeletedAt() != null && !includeDeleted) return Optional.empty();
ProductDtos.ProductDetail d = new ProductDtos.ProductDetail();
d.id = p.getId();
d.name = p.getName();
@@ -198,7 +192,7 @@ public class ProductService {
d.spec = p.getSpec();
d.origin = p.getOrigin();
d.categoryId = p.getCategoryId();
d.unitId = p.getUnitId();
d.templateId = p.getTemplateId();
d.safeMin = p.getSafeMin();
d.safeMax = p.getSafeMax();
inventoryRepository.findById(p.getId()).ifPresent(inv -> d.stock = inv.getQuantity());
@@ -216,6 +210,11 @@ public class ProductService {
list.add(i);
}
d.images = list;
d.parameters = JsonUtils.fromJson(p.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String,Object>>() {});
d.sourceSubmissionId = p.getSourceSubmissionId();
// deleted 标志供前端展示
d.deleted = (p.getDeletedAt() != null);
// externalCode 来自 submission若来源存在可透传此处留空由前端兼容
return Optional.of(d);
}
@@ -235,7 +234,7 @@ public class ProductService {
p.setSpec(emptyToNull(req.spec));
p.setOrigin(emptyToNull(req.origin));
p.setCategoryId(req.categoryId);
p.setUnitId(req.unitId);
// 单位字段已移除
p.setDedupeKey(emptyToNull(req.dedupeKey));
p.setSafeMin(req.safeMin);
p.setSafeMax(req.safeMax);
@@ -290,7 +289,7 @@ public class ProductService {
p.setSpec(emptyToNull(req.spec));
p.setOrigin(emptyToNull(req.origin));
p.setCategoryId(req.categoryId);
p.setUnitId(req.unitId);
// 单位字段已移除
p.setDedupeKey(emptyToNull(req.dedupeKey));
p.setSafeMin(req.safeMin);
p.setSafeMax(req.safeMax);
@@ -306,7 +305,7 @@ public class ProductService {
private void validate(Long shopId, ProductDtos.CreateOrUpdateProductRequest req) {
if (req.name == null || req.name.isBlank()) throw new IllegalArgumentException("name必填");
if (req.unitId == null) throw new IllegalArgumentException("unitId必填");
// 不再要求 unitId
if (req.safeMin != null && req.safeMax != null) {
if (req.safeMin.compareTo(req.safeMax) > 0) throw new IllegalArgumentException("安全库存区间不合法");
}
@@ -360,6 +359,16 @@ public class ProductService {
private static <T> T nvl(T v, T def) { return v != null ? v : def; }
private static String emptyToNull(String s) { return (s == null || s.isBlank()) ? null : s; }
@Transactional
public void delete(Long id, Long shopId) {
Product p = productRepository.findById(id).orElse(null);
if (p == null) return;
if (!p.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺数据");
p.setDeletedAt(LocalDateTime.now());
productRepository.save(p);
// 关联数据:价格/库存采用 ON DELETE CASCADE 不触发;软删仅标记主表
}
}

View File

@@ -14,6 +14,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,15 +44,18 @@ public class ProductSubmissionService {
private final ProductService productService;
private final PartTemplateParamRepository templateParamRepository;
private final AppDefaultsProperties defaults;
private final JdbcTemplate jdbcTemplate;
public ProductSubmissionService(ProductSubmissionRepository submissionRepository,
ProductService productService,
AppDefaultsProperties defaults,
PartTemplateParamRepository templateParamRepository) {
PartTemplateParamRepository templateParamRepository,
JdbcTemplate jdbcTemplate) {
this.submissionRepository = submissionRepository;
this.productService = productService;
this.defaults = defaults;
this.templateParamRepository = templateParamRepository;
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
@@ -61,12 +65,13 @@ public class ProductSubmissionService {
submission.setShopId(shopId);
submission.setUserId(userId);
submission.setTemplateId(req.templateId);
if (req.externalCode != null && !req.externalCode.isBlank()) submission.setExternalCode(req.externalCode.trim());
submission.setName(req.name);
submission.setModelUnique(normalizeModel(req.model));
submission.setBrand(req.brand);
submission.setSpec(req.spec);
submission.setOrigin(req.origin);
submission.setUnitId(req.unitId);
// 单位字段已移除
submission.setCategoryId(req.categoryId);
submission.setAttributesJson(JsonUtils.toJson(req.parameters));
submission.setImagesJson(JsonUtils.toJson(req.images));
@@ -84,7 +89,7 @@ public class ProductSubmissionService {
public ProductSubmissionDtos.PageResult<ProductSubmissionDtos.SubmissionItem> listMine(Long shopId, Long userId, String status, int page, int size) {
String normalizedStatus = (status == null || status.isBlank() || "undefined".equalsIgnoreCase(status)) ? null : status;
Page<ProductSubmission> result = submissionRepository.findByShopIdAndUserIdAndStatusIn(
Page<ProductSubmission> result = submissionRepository.findByShopIdAndUserIdAndStatusInAndDeletedAtIsNull(
shopId, userId, resolveStatuses(normalizedStatus), PageRequest.of(Math.max(page - 1, 0), size, Sort.by(Sort.Direction.DESC, "createdAt")));
return toPageResult(result);
}
@@ -109,6 +114,26 @@ public class ProductSubmissionService {
LocalDateTime endTime = parseDate(endAt);
List<ProductSubmission> records = submissionRepository.searchAdmin(statuses.isEmpty() ? null : statuses,
kwLike, shopId, reviewerId, start, endTime, PageRequest.of(0, 2000, Sort.by(Sort.Direction.DESC, "createdAt"))).getContent();
// 收集所有模板的必填参数标题
java.util.LinkedHashSet<String> requiredParamLabels = new java.util.LinkedHashSet<>();
java.util.Map<Long, java.util.Map<String,String>> labelToKeyByTemplate = new java.util.HashMap<>();
for (ProductSubmission s : records) {
Long tid = s.getTemplateId();
if (tid == null || tid <= 0) continue;
if (!labelToKeyByTemplate.containsKey(tid)) {
java.util.Map<String,String> map = new java.util.LinkedHashMap<>();
var defs = templateParamRepository.findByTemplateIdOrderBySortOrderAscIdAsc(tid);
for (var d : defs) {
if (Boolean.TRUE.equals(d.getRequired())) {
map.put(d.getFieldLabel(), d.getFieldKey());
requiredParamLabels.add(d.getFieldLabel());
}
}
labelToKeyByTemplate.put(tid, map);
} else {
for (var e : labelToKeyByTemplate.get(tid).keySet()) requiredParamLabels.add(e);
}
}
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Submissions");
CreationHelper creationHelper = workbook.getCreationHelper();
@@ -117,27 +142,36 @@ public class ProductSubmissionService {
int rowIdx = 0;
Row header = sheet.createRow(rowIdx++);
String[] headers = {"ID", "型号", "名称", "品牌", "状态", "提交人", "店铺", "提交时间", "审核时间", "审核备注"};
for (int i = 0; i < headers.length; i++) {
header.createCell(i).setCellValue(headers[i]);
}
java.util.List<String> headers = new java.util.ArrayList<>();
headers.add("编号");
headers.add("分类");
headers.add("品牌");
headers.add("型号");
headers.addAll(requiredParamLabels);
headers.add("备注");
for (int i = 0; i < headers.size(); i++) header.createCell(i).setCellValue(headers.get(i));
for (ProductSubmission submission : records) {
Row row = sheet.createRow(rowIdx++);
int col = 0;
row.createCell(col++).setCellValue(submission.getId());
row.createCell(col++).setCellValue(nvl(submission.getModelUnique()));
row.createCell(col++).setCellValue(nvl(submission.getName()));
// 编号、分类、品牌、型号
row.createCell(col++).setCellValue(nvl(submission.getExternalCode()));
row.createCell(col++).setCellValue(nvl(resolveCategoryName(submission.getCategoryId())));
row.createCell(col++).setCellValue(nvl(submission.getBrand()));
row.createCell(col++).setCellValue(submission.getStatus().name());
row.createCell(col++).setCellValue(submission.getUserId() != null ? submission.getUserId() : 0);
row.createCell(col++).setCellValue(submission.getShopId() != null ? submission.getShopId() : 0);
setDateCell(row.createCell(col++), submission.getCreatedAt(), dateStyle);
setDateCell(row.createCell(col++), submission.getReviewedAt(), dateStyle);
row.createCell(col).setCellValue(nvl(submission.getReviewRemark()));
row.createCell(col++).setCellValue(nvl(submission.getModelUnique()));
// 模板必填参数值
java.util.Map<String,Object> params = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String,Object>>() {});
java.util.Map<String,String> l2k = labelToKeyByTemplate.getOrDefault(submission.getTemplateId(), java.util.Collections.emptyMap());
for (String label : requiredParamLabels) {
String key = l2k.get(label);
Object v = (key == null || params == null) ? null : params.get(key);
row.createCell(col++).setCellValue(v == null ? "" : String.valueOf(v));
}
// 备注
row.createCell(col).setCellValue(nvl(submission.getRemarkText()));
}
for (int i = 0; i < headers.length; i++) {
for (int i = 0; i < headers.size(); i++) {
sheet.autoSizeColumn(i);
int width = sheet.getColumnWidth(i);
sheet.setColumnWidth(i, Math.min(width + 512, 10000));
@@ -153,6 +187,15 @@ public class ProductSubmissionService {
}
}
private final java.util.Map<Long, String> categoryNameCache = new java.util.HashMap<>();
private String resolveCategoryName(Long categoryId) {
if (categoryId == null) return "";
if (categoryNameCache.containsKey(categoryId)) return categoryNameCache.get(categoryId);
String name = jdbcTemplate.query("SELECT name FROM product_categories WHERE id=?", ps -> ps.setLong(1, categoryId), rs -> rs.next()? rs.getString(1): "");
categoryNameCache.put(categoryId, name == null ? "" : name);
return name == null ? "" : name;
}
public Optional<ProductSubmissionDtos.SubmissionDetail> findDetail(Long id) {
return submissionRepository.findById(id).map(this::toDetail);
}
@@ -168,10 +211,11 @@ public class ProductSubmissionService {
throw new IllegalArgumentException("仅待审核记录可编辑");
}
submission.setName(req.name != null ? req.name : submission.getName());
if (req.externalCode != null) submission.setExternalCode(req.externalCode);
submission.setBrand(req.brand != null ? req.brand : submission.getBrand());
submission.setSpec(req.spec != null ? req.spec : submission.getSpec());
submission.setOrigin(req.origin != null ? req.origin : submission.getOrigin());
submission.setUnitId(req.unitId != null ? req.unitId : submission.getUnitId());
// 单位字段已移除
submission.setCategoryId(req.categoryId != null ? req.categoryId : submission.getCategoryId());
if (req.parameters != null) submission.setAttributesJson(JsonUtils.toJson(req.parameters));
if (req.images != null) submission.setImagesJson(JsonUtils.toJson(req.images));
@@ -372,11 +416,12 @@ public class ProductSubmissionService {
detail.shopId = submission.getShopId();
detail.userId = submission.getUserId();
detail.name = submission.getName();
detail.externalCode = submission.getExternalCode();
detail.model = submission.getModelUnique();
detail.brand = submission.getBrand();
detail.spec = submission.getSpec();
detail.origin = submission.getOrigin();
detail.unitId = submission.getUnitId();
// 单位字段已移除
detail.categoryId = submission.getCategoryId();
detail.templateId = submission.getTemplateId();
detail.parameters = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>() {});
@@ -426,7 +471,7 @@ public class ProductSubmissionService {
payload.spec = submission.getSpec();
payload.origin = submission.getOrigin();
payload.categoryId = submission.getCategoryId();
payload.unitId = submission.getUnitId();
// 单位字段已移除
payload.dedupeKey = submission.getDedupeKey();
payload.safeMin = submission.getSafeMin();
payload.safeMax = submission.getSafeMax();

View File

@@ -94,3 +94,135 @@
2025-09-27 22:56:27.814 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-27 22:56:27.815 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-27 22:56:27.839 | 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-27 23:05:41.132 | 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-27 23:05:41.133 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-27 23:05:41.135 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-27 23:05:41.171 | 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-27 23:47:53.925 | 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-27 23:47:53.926 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-27 23:47:53.928 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-27 23:47:53.957 | 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-28 01:24:20.344 | 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-28 01:24:20.346 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 01:24:20.363 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 01:24:20.585 | 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-28 22:00:52.538 | 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-28 22:00:52.542 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:00:52.560 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:00:52.876 | 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-28 22:02:38.951 | 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-28 22:02:38.954 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:02:38.956 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:02:38.980 | 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-28 22:29:34.466 | 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-28 22:29:34.468 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:29:34.476 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:29:34.670 | 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-28 22:35:16.776 | 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-28 22:35:16.779 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:35:16.780 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:35:16.811 | 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-28 22:38:31.014 | 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-28 22:38:31.017 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:38:31.020 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:38:31.048 | 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-28 23:06:13.053 | 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-28 23:06:13.055 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:06:13.057 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:06:13.108 | 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-28 23:15:20.745 | 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-28 23:15:20.749 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:15:20.753 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:15:20.798 | 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-28 23:22:54.219 | 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-28 23:22:54.223 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:22:54.226 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:22:54.264 | 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-28 23:23:45.474 | 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-28 23:23:45.482 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:23:45.490 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:23:45.530 | 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-28 23:41:40.864 | 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-28 23:41:40.869 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:41:40.873 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:41:40.910 | 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-28 23:50:01.655 | 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-28 23:50:01.658 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:50:01.661 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:50:01.696 | 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 00:03:06.082 | 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 00:03:06.095 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 00:03:06.106 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 00:03:06.388 | 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 11:43:33.011 | 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 11:43:33.016 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 11:43:33.032 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 11:43:33.358 | 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 11:59:27.297 | 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 11:59:27.298 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 11:59:27.306 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 11:59:27.418 | 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 12:21:49.423 | 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 12:21:49.425 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 12:21:49.427 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 12:21:49.456 | 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 12:24:55.373 | 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 12:24:55.375 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 12:24:55.389 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 12:24:55.608 | 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 12:54:19.142 | 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 12:54:19.144 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 12:54:19.146 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 12:54:19.168 | 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 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_part_begin with no data
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_field with data[38:57]
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_value with data[59:125]
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_end with no data
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_field with data[127:139]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_value with data[141:151]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_end with no data
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_field with data[153:167]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_value with data[169:174]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_end with no data
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_headers_finished with no data
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_part_data with data[178:69513]
2025-09-29 13:00:11.539 | DEBUG | python_multipart.multipart | Calling on_part_data with data[0:14631]
2025-09-29 13:00:11.539 | DEBUG | python_multipart.multipart | Calling on_part_end with no data
2025-09-29 13:00:11.539 | DEBUG | python_multipart.multipart | Calling on_end with no data
2025-09-29 13:00:11.550 | DEBUG | app.server.main | /api/barcode/scan 收到图像: shape=(1440, 1080, 3), dtype=uint8
2025-09-29 13:00:11.559 | DEBUG | pyzbar_engine | 调用 pyzbar: symbols=8, rotations=[0, 90, 180, 270], try_invert=True
2025-09-29 13:00:11.802 | DEBUG | pyzbar_engine | pyzbar 返回结果数: 1
2025-09-29 13:00:11.803 | DEBUG | EAN13Recognizer | pyzbar 返回 1 条结果
2025-09-29 13:00:11.803 | DEBUG | EAN13Recognizer | 输入尺寸=(1707, 1280, 3), 预处理后尺寸=(1707, 1280, 3)
2025-09-29 13:00:11.804 | DEBUG | pyzbar_engine | 调用 pyzbar: symbols=8, rotations=[0, 90, 180, 270], try_invert=True
2025-09-29 13:00:12.040 | DEBUG | pyzbar_engine | pyzbar 返回结果数: 1
2025-09-29 13:00:12.040 | DEBUG | EAN13Recognizer | pyzbar 识别到 1 条结果
2025-09-29 13:00:12.056 | DEBUG | EAN13Recognizer | ROI bbox=(372, 627, 654, 190)
2025-09-29 13:00:12.060 | DEBUG | EAN13Recognizer | 透视矫正后尺寸=(120, 413)
2025-09-29 13:00:12.098 | DEBUG | EAN13Recognizer | 自研 EAN13 解码失败
2025-09-29 13:00:12.099 | DEBUG | EAN13Recognizer | recognize_any 未命中 EAN13, others=1
2025-09-29 13:00:12.100 | INFO | app.server.main | /api/barcode/scan 命中非 EAN: type=CODE128, code=84455470401081732071, cost_ms=561.2
2025-09-29 13:11:28.027 | 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 13:11:28.029 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 13:11:28.030 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 13:11:28.059 | 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 19:35:33.086 | 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 19:35:33.087 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 19:35:33.105 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 19:35:33.458 | 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:02:28.561 | 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:02:28.563 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:02:28.579 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:02:28.828 | 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:13:29.629 | 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:13:29.631 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:13:29.632 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:13:29.653 | 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:26:27.378 | 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:26:27.379 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:26:27.382 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:26:27.404 | 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:30:25.753 | 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: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]}