3
This commit is contained in:
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 |
@@ -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='商品';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 中触发;此处保持模板单体删除逻辑
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) " +
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 不触发;软删仅标记主表
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]}
|
||||
|
||||
Reference in New Issue
Block a user