更新之后,我也不知道有没有问题

This commit is contained in:
2025-05-08 21:16:58 +08:00
parent 390eeb67af
commit 7b7d491663
26 changed files with 1840 additions and 145 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+60 -143
View File
@@ -9,6 +9,12 @@ import logging
from typing import Dict, Tuple, Optional, Any, List, Union
from ..utils.log_utils import get_logger
from .handlers.barcode_mapper import BarcodeMapper
from .handlers.unit_converter_handlers import (
JianUnitHandler, BoxUnitHandler, TiHeUnitHandler,
GiftUnitHandler, UnitHandler
)
from .validators import ProductValidator
logger = get_logger(__name__)
@@ -93,7 +99,28 @@ class UnitConverter:
# "xxmL*1"或"xx毫升*1"格式
(r'([\d\.]+)(?:mL|毫升)[*xX×]?(\d+)?', r'\1mL*\2' if r'\2' else r'\1mL*1'),
]
# 初始化处理程序
self._init_handlers()
# 初始化验证器
self.validator = ProductValidator()
def _init_handlers(self):
"""
初始化各种处理程序
"""
# 创建条码处理程序
self.barcode_mapper = BarcodeMapper(self.special_barcodes)
# 创建单位处理程序列表,优先级从高到低
self.unit_handlers: List[UnitHandler] = [
GiftUnitHandler(), # 首先处理赠品,优先级最高
JianUnitHandler(), # 处理"件"单位
BoxUnitHandler(), # 处理"箱"单位
TiHeUnitHandler() # 处理"提"和"盒"单位
]
def extract_unit_from_quantity(self, quantity_str: str) -> Tuple[Optional[float], Optional[str]]:
"""
从数量字符串中提取单位
@@ -368,103 +395,15 @@ class UnitConverter:
logger.error(f"解析规格时出错: {e}")
return 1, 1, None
def _process_standard_unit_conversion(self, product: Dict) -> Dict:
"""
处理标准单位转换(件、箱、提、盒等单位)
Args:
product: 商品信息字典
Returns:
处理后的商品信息字典
"""
# 复制原始数据,避免修改原始字典
result = product.copy()
unit = result.get('unit', '')
quantity = result.get('quantity', 0)
price = result.get('price', 0)
specification = result.get('specification', '')
# 跳过无效数据
if not specification:
return result
# 解析规格信息
level1, level2, level3 = self.parse_specification(specification)
# "件"单位处理
if unit in ['']:
# 计算包装数量(二级*三级,如果无三级则仅二级)
packaging_count = level2 * (level3 or 1)
# 数量×包装数量
new_quantity = quantity * packaging_count
# 单价÷包装数量
new_price = price / packaging_count if price else 0
logger.info(f"件单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 件 -> 瓶")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = ''
return result
# "箱"单位处理 - 与"件"单位处理相同
if unit in ['']:
# 计算包装数量
packaging_count = level2 * (level3 or 1)
# 数量×包装数量
new_quantity = quantity * packaging_count
# 单价÷包装数量
new_price = price / packaging_count if price else 0
logger.info(f"箱单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 箱 -> 瓶")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = ''
return result
# "提"和"盒"单位处理
if unit in ['', '']:
# 如果是三级规格,按件处理
if level3 is not None:
# 计算包装数量 - 只乘以最后一级数量
packaging_count = level3
# 数量×包装数量
new_quantity = quantity * packaging_count
# 单价÷包装数量
new_price = price / packaging_count if price else 0
logger.info(f"提/盒单位(三级规格)处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {unit} -> 瓶")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = ''
else:
# 如果是二级规格,保持不变
logger.info(f"提/盒单位(二级规格)处理: 保持原样 数量: {quantity}, 单价: {price}, 单位: {unit}")
return result
# 其他单位保持不变
logger.info(f"其他单位处理: 保持原样 数量: {quantity}, 单价: {price}, 单位: {unit}")
return result
def process_unit_conversion(self, product: Dict) -> Dict:
"""
处理单位转换,按照以下规则:
1. 特殊条码: 优先处理特殊条码
2. ""单位: 数量×包装数量, 单价÷包装数量, 单位转为""
3. ""单位: 数量×包装数量, 单价÷包装数量, 单位转为""
4. """"单位: 如果是三级规格, 按件处理; 如果是二级规格, 保持不变
5. 其他单位: 保持不变
2. 赠品处理: 对于赠品,维持数量转换但单价为0
3. ""单位: 数量×包装数量, 单价÷包装数量, 单位转为""
4. ""单位: 数量×包装数量, 单价÷包装数量, 单位转为""
5. """"单位: 如果是三级规格, 按件处理; 如果是二级规格, 保持不变
6. 其他单位: 保持不变
Args:
product: 商品信息字典
@@ -472,62 +411,40 @@ class UnitConverter:
Returns:
处理后的商品信息字典
"""
# 首先验证商品数据
product = self.validator.validate_product(product)
# 复制原始数据,避免修改原始字典
result = product.copy()
barcode = result.get('barcode', '')
unit = result.get('unit', '')
quantity = result.get('quantity', 0)
price = result.get('price', 0)
specification = result.get('specification', '')
# 跳过无效数据
if not barcode or not quantity:
if not barcode:
return result
# 特殊条码处理
if barcode in self.special_barcodes:
special_config = self.special_barcodes[barcode]
# 处理条码映射情况
if 'map_to' in special_config:
new_barcode = special_config['map_to']
logger.info(f"条码映射: {barcode} -> {new_barcode}")
result['barcode'] = new_barcode
# 如果只是条码映射且没有其他特殊处理,继续执行标准单位处理
if len(special_config) == 2: # 只有map_to和description两个字段
# 继续标准处理流程,不提前返回
return self._process_standard_unit_conversion(result)
multiplier = special_config.get('multiplier', 1)
target_unit = special_config.get('target_unit', '')
# 数量乘以倍数
new_quantity = quantity * multiplier
# 如果有单价,单价除以倍数
new_price = price / multiplier if price else 0
# 如果有固定单价,优先使用
if 'fixed_price' in special_config:
new_price = special_config['fixed_price']
logger.info(f"特殊条码({barcode})使用固定单价: {new_price}")
# 如果有固定规格,设置规格
if 'specification' in special_config:
result['specification'] = special_config['specification']
# 解析规格以获取包装数量
package_quantity = self.parse_specification(special_config['specification'])
if package_quantity:
result['package_quantity'] = package_quantity
logger.info(f"特殊条码({barcode})使用固定规格: {special_config['specification']}, 包装数量={package_quantity}")
logger.info(f"特殊条码处理: {barcode}, 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {unit} -> {target_unit}")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = target_unit
return result
# 先处理条码映射
result = self.barcode_mapper.map_barcode(result)
# 没有特殊条码,使用标准单位处理
return self._process_standard_unit_conversion(result)
# 如果没有规格信息,无法进行单位转换
if not specification:
# 尝试从商品名称推断规格
inferred_spec = self.infer_specification_from_name(result.get('name', ''))
if inferred_spec:
result['specification'] = inferred_spec
logger.info(f"从商品名称推断规格: {result.get('name', '')} -> {inferred_spec}")
else:
return result
# 解析规格信息
level1, level2, level3 = self.parse_specification(result.get('specification', ''))
# 使用单位处理程序处理单位转换
for handler in self.unit_handlers:
if handler.can_handle(result):
return handler.handle(result, level1, level2, level3)
# 没有找到适用的处理程序,保持不变
logger.info(f"其他单位处理: 保持原样 数量: {result.get('quantity', 0)}, 单价: {result.get('price', 0)}, 单位: {result.get('unit', '')}")
return result
+11
View File
@@ -0,0 +1,11 @@
"""
单位转换处理程序包
-----------------
提供单位转换和条码处理的各种处理程序
"""
from typing import Dict, Any
# 导出所有处理程序类
from .barcode_mapper import BarcodeMapper
from .unit_converter_handlers import JianUnitHandler, BoxUnitHandler, TiHeUnitHandler, GiftUnitHandler, UnitHandler
+83
View File
@@ -0,0 +1,83 @@
"""
条码映射处理程序
-------------
处理特殊条码的映射和转换
"""
import logging
from typing import Dict, Optional, Any
from ...utils.log_utils import get_logger
logger = get_logger(__name__)
class BarcodeMapper:
"""
条码映射器:负责特殊条码的映射和处理
"""
def __init__(self, special_barcodes: Dict[str, Dict[str, Any]]):
"""
初始化条码映射器
Args:
special_barcodes: 特殊条码配置字典
"""
self.special_barcodes = special_barcodes or {}
def map_barcode(self, product: Dict[str, Any]) -> Dict[str, Any]:
"""
映射商品条码,处理特殊情况
Args:
product: 包含条码的商品信息字典
Returns:
处理后的商品信息字典
"""
result = product.copy()
barcode = result.get('barcode', '')
# 如果条码不在特殊条码列表中,直接返回
if not barcode or barcode not in self.special_barcodes:
return result
special_config = self.special_barcodes[barcode]
# 处理条码映射
if 'map_to' in special_config:
new_barcode = special_config['map_to']
logger.info(f"条码映射: {barcode} -> {new_barcode}")
result['barcode'] = new_barcode
# 处理特殊倍数
if 'multiplier' in special_config:
multiplier = special_config.get('multiplier', 1)
target_unit = special_config.get('target_unit', '')
# 数量乘以倍数
quantity = result.get('quantity', 0)
new_quantity = quantity * multiplier
# 单价除以倍数
price = result.get('price', 0)
new_price = price / multiplier if price else 0
# 如果有固定单价,优先使用
if 'fixed_price' in special_config:
new_price = special_config['fixed_price']
logger.info(f"特殊条码({barcode})使用固定单价: {new_price}")
# 如果有固定规格,设置规格
if 'specification' in special_config:
result['specification'] = special_config['specification']
logger.info(f"特殊条码({barcode})使用固定规格: {special_config['specification']}")
logger.info(f"特殊条码处理: {barcode}, 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {result.get('unit', '')} -> {target_unit}")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = target_unit
return result
@@ -0,0 +1,284 @@
"""
单位转换处理程序
-------------
处理不同单位的转换逻辑
"""
import logging
from typing import Dict, Optional, Any, Tuple, Protocol
from abc import ABC, abstractmethod
from ...utils.log_utils import get_logger
logger = get_logger(__name__)
class UnitHandler(ABC):
"""
单位处理器基类:定义单位处理接口
"""
@abstractmethod
def can_handle(self, product: Dict[str, Any]) -> bool:
"""
检查是否可以处理该商品
Args:
product: 商品信息字典
Returns:
是否可以处理
"""
pass
@abstractmethod
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
"""
处理单位转换
Args:
product: 商品信息字典
level1: 一级包装数量
level2: 二级包装数量
level3: 三级包装数量,可能为None
Returns:
处理后的商品信息字典
"""
pass
class JianUnitHandler(UnitHandler):
"""
处理""单位的转换
"""
def can_handle(self, product: Dict[str, Any]) -> bool:
"""
检查是否可以处理该商品(单位为""
Args:
product: 商品信息字典
Returns:
是否可以处理
"""
unit = product.get('unit', '')
return unit == ''
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
"""
处理""单位转换:数量×包装数量,单价÷包装数量,单位转为""
Args:
product: 商品信息字典
level1: 一级包装数量
level2: 二级包装数量
level3: 三级包装数量,可能为None
Returns:
处理后的商品信息字典
"""
result = product.copy()
quantity = result.get('quantity', 0)
price = result.get('price', 0)
# 计算包装数量(二级*三级,如果无三级则仅二级)
packaging_count = level2 * (level3 or 1)
# 数量×包装数量
new_quantity = quantity * packaging_count
# 单价÷包装数量
new_price = price / packaging_count if price else 0
logger.info(f"件单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 件 -> 瓶")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = ''
return result
class BoxUnitHandler(UnitHandler):
"""
处理""单位的转换
"""
def can_handle(self, product: Dict[str, Any]) -> bool:
"""
检查是否可以处理该商品(单位为""
Args:
product: 商品信息字典
Returns:
是否可以处理
"""
unit = product.get('unit', '')
return unit == ''
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
"""
处理""单位转换:数量×包装数量,单价÷包装数量,单位转为""
Args:
product: 商品信息字典
level1: 一级包装数量
level2: 二级包装数量
level3: 三级包装数量,可能为None
Returns:
处理后的商品信息字典
"""
result = product.copy()
quantity = result.get('quantity', 0)
price = result.get('price', 0)
# 计算包装数量(二级*三级,如果无三级则仅二级)
packaging_count = level2 * (level3 or 1)
# 数量×包装数量
new_quantity = quantity * packaging_count
# 单价÷包装数量
new_price = price / packaging_count if price else 0
logger.info(f"箱单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 箱 -> 瓶")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = ''
return result
class TiHeUnitHandler(UnitHandler):
"""
处理""""单位的转换
"""
def can_handle(self, product: Dict[str, Any]) -> bool:
"""
检查是否可以处理该商品(单位为""""
Args:
product: 商品信息字典
Returns:
是否可以处理
"""
unit = product.get('unit', '')
return unit in ['', '']
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
"""
处理""""单位转换:
- 如果是三级规格,按件处理(数量×包装数量,单价÷包装数量,单位转为""
- 如果是二级规格,保持不变
Args:
product: 商品信息字典
level1: 一级包装数量
level2: 二级包装数量
level3: 三级包装数量,可能为None
Returns:
处理后的商品信息字典
"""
result = product.copy()
quantity = result.get('quantity', 0)
price = result.get('price', 0)
unit = result.get('unit', '')
# 如果是三级规格,按件处理
if level3 is not None:
# 计算包装数量 - 只乘以最后一级数量
packaging_count = level3
# 数量×包装数量
new_quantity = quantity * packaging_count
# 单价÷包装数量
new_price = price / packaging_count if price else 0
logger.info(f"提/盒单位(三级规格)处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {unit} -> 瓶")
result['quantity'] = new_quantity
result['price'] = new_price
result['unit'] = ''
else:
# 如果是二级规格,保持不变
logger.info(f"提/盒单位(二级规格)处理: 保持原样 数量: {quantity}, 单价: {price}, 单位: {unit}")
return result
class GiftUnitHandler(UnitHandler):
"""
处理赠品的特殊情况
"""
def can_handle(self, product: Dict[str, Any]) -> bool:
"""
检查是否可以处理该商品(是否为赠品)
Args:
product: 商品信息字典
Returns:
是否可以处理
"""
return product.get('is_gift', False) is True
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
"""
处理赠品的单位转换:
- 对于件/箱单位,数量仍然需要转换,但赠品的单价保持为0
Args:
product: 商品信息字典
level1: 一级包装数量
level2: 二级包装数量
level3: 三级包装数量,可能为None
Returns:
处理后的商品信息字典
"""
result = product.copy()
unit = result.get('unit', '')
quantity = result.get('quantity', 0)
# 根据单位类型选择适当的包装数计算
if unit in ['', '']:
# 计算包装数量(二级*三级,如果无三级则仅二级)
packaging_count = level2 * (level3 or 1)
# 数量×包装数量
new_quantity = quantity * packaging_count
logger.info(f"赠品{unit}单位处理: 数量: {quantity} -> {new_quantity}, 单价: 0, 单位: {unit} -> 瓶")
result['quantity'] = new_quantity
result['unit'] = ''
elif unit in ['', ''] and level3 is not None:
# 对于三级规格的提/盒,类似件处理
new_quantity = quantity * level3
logger.info(f"赠品{unit}单位(三级规格)处理: 数量: {quantity} -> {new_quantity}, 单价: 0, 单位: {unit} -> 瓶")
result['quantity'] = new_quantity
result['unit'] = ''
else:
# 其他情况保持不变
logger.info(f"赠品{unit}单位处理: 保持原样 数量: {quantity}, 单价: 0, 单位: {unit}")
# 确保单价为0
result['price'] = 0
return result
+8 -1
View File
@@ -295,8 +295,15 @@ class ExcelProcessor:
if package_quantity:
product['package_quantity'] = package_quantity
logger.info(f"解析规格: {product['specification']} -> 包装数量={package_quantity}")
elif column_mapping.get('specification') and not pd.isna(row[column_mapping['specification']]):
# 添加这段逻辑以处理通过列映射找到的规格列
product['specification'] = str(row[column_mapping['specification']])
package_quantity = self.parse_specification(product['specification'])
if package_quantity:
product['package_quantity'] = package_quantity
logger.info(f"从映射列解析规格: {product['specification']} -> 包装数量={package_quantity}")
else:
# 逻辑1: 如果规格为空,尝试从商品名称推断规格
# 只有在无法从Excel获取规格时,才尝试从商品名称推断规格
if product['name']:
# 特殊处理:优先检查名称中是否包含"容量*数量"格式
container_pattern = r'.*?(\d+(?:\.\d+)?)\s*(?:ml|[mM][lL]|[lL]|升|毫升)[*×xX](\d+).*'
+355
View File
@@ -0,0 +1,355 @@
"""
单位转换器测试模块
---------------
测试单位转换和条码映射逻辑
"""
import os
import sys
import unittest
from typing import Dict, Any
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
from app.core.excel.converter import UnitConverter
from app.core.excel.validators import ProductValidator
class TestUnitConverter(unittest.TestCase):
"""
测试单位转换器功能
"""
def setUp(self):
"""
测试前的准备工作
"""
self.converter = UnitConverter()
def test_jian_unit_conversion(self):
"""
测试""单位的转换
"""
# 准备测试数据
product = {
'barcode': '6954767400129',
'name': '美汁源果粒橙1.8L*8瓶',
'specification': '1.8L*8',
'quantity': 1.0,
'unit': '',
'price': 65.0
}
# 执行转换
result = self.converter.process_unit_conversion(product)
# 验证结果
self.assertEqual(result['quantity'], 8.0)
self.assertEqual(result['price'], 8.125)
self.assertEqual(result['unit'], '')
def test_box_unit_conversion(self):
"""
测试""单位的转换
"""
# 准备测试数据
product = {
'barcode': '6925303721244',
'name': '统一鲜橙多2L*6瓶',
'specification': '2L*6',
'quantity': 1.0,
'unit': '',
'price': 43.0
}
# 执行转换
result = self.converter.process_unit_conversion(product)
# 验证结果
self.assertEqual(result['quantity'], 6.0)
self.assertEqual(result['price'], 7.1666666666666667)
self.assertEqual(result['unit'], '')
def test_tihe_unit_conversion_level3(self):
"""
测试""单位的转换(三级规格)
"""
# 准备测试数据(三级规格:1*6*4,表示1排6提,每提4瓶)
product = {
'barcode': '6921168509347',
'name': '农夫山泉550ml*24瓶',
'specification': '1*6*4',
'quantity': 2.0,
'unit': '',
'price': 16.0
}
# 执行转换
result = self.converter.process_unit_conversion(product)
# 验证结果:三级规格,提单位特殊处理,数量*最后一级
self.assertEqual(result['quantity'], 8.0) # 2提 * 4瓶/提
self.assertEqual(result['price'], 4.0) # 16元/提 ÷ 4瓶/提
self.assertEqual(result['unit'], '')
def test_tihe_unit_conversion_level2(self):
"""
测试""单位的转换(二级规格)
"""
# 准备测试数据(二级规格:1*4,表示每件4提)
product = {
'barcode': '6921168509347',
'name': '农夫山泉550ml*4瓶',
'specification': '1*4',
'quantity': 5.0,
'unit': '',
'price': 10.0
}
# 执行转换
result = self.converter.process_unit_conversion(product)
# 验证结果:二级规格,提单位保持不变
self.assertEqual(result['quantity'], 5.0)
self.assertEqual(result['price'], 10.0)
self.assertEqual(result['unit'], '')
def test_barcode_mapping(self):
"""
测试条码映射
"""
# 准备测试数据(使用需要被映射的条码)
product = {
'barcode': '6920584471055', # 这个条码应映射到6920584471017
'name': '测试映射条码商品',
'specification': '1*12',
'quantity': 1.0,
'unit': '',
'price': 60.0
}
# 执行转换
result = self.converter.process_unit_conversion(product)
# 验证结果:条码应该被映射
self.assertEqual(result['barcode'], '6920584471017')
self.assertEqual(result['quantity'], 12.0) # 同时处理件单位转换
self.assertEqual(result['price'], 5.0) # 60元/件 ÷ 12瓶/件
self.assertEqual(result['unit'], '')
def test_special_barcode_multiplier(self):
"""
测试特殊条码的倍数处理
"""
# 准备测试数据(使用特殊条码)
product = {
'barcode': '6925019900087', # 特殊条码:数量*10,单位转瓶
'name': '特殊条码商品',
'specification': '1*10',
'quantity': 2.0,
'unit': '',
'price': 100.0
}
# 执行转换
result = self.converter.process_unit_conversion(product)
# 验证结果:特殊条码乘数应该生效
self.assertEqual(result['quantity'], 20.0) # 2箱 * 10倍数
self.assertEqual(result['price'], 5.0) # 100元/箱 ÷ 10倍数/箱
self.assertEqual(result['unit'], '')
class TestProductValidator(unittest.TestCase):
"""
测试商品数据验证器功能
"""
def setUp(self):
"""
测试前的准备工作
"""
self.validator = ProductValidator()
def test_validate_barcode(self):
"""
测试条码验证
"""
# 测试有效条码
is_valid, barcode, error = self.validator.validate_barcode('6925303721244')
self.assertTrue(is_valid)
self.assertEqual(barcode, '6925303721244')
self.assertIsNone(error)
# 测试包含非数字字符的条码
is_valid, barcode, error = self.validator.validate_barcode('6925303-721244')
self.assertTrue(is_valid)
self.assertEqual(barcode, '6925303721244')
self.assertIsNone(error)
# 测试5开头的条码修正
is_valid, barcode, error = self.validator.validate_barcode('5925303721244')
self.assertTrue(is_valid)
self.assertEqual(barcode, '6925303721244')
self.assertIsNone(error)
# 测试过短的条码
is_valid, barcode, error = self.validator.validate_barcode('12345')
self.assertFalse(is_valid)
self.assertEqual(barcode, '12345')
self.assertIn("条码长度异常", error)
# 测试仓库标识
is_valid, barcode, error = self.validator.validate_barcode('仓库')
self.assertFalse(is_valid)
self.assertEqual(barcode, '仓库')
self.assertEqual(error, "条码为仓库标识")
# 测试空值
is_valid, barcode, error = self.validator.validate_barcode(None)
self.assertFalse(is_valid)
self.assertEqual(barcode, "")
self.assertEqual(error, "条码为空")
def test_validate_quantity(self):
"""
测试数量验证
"""
# 测试有效数量
is_valid, quantity, error = self.validator.validate_quantity(10)
self.assertTrue(is_valid)
self.assertEqual(quantity, 10.0)
self.assertIsNone(error)
# 测试字符串数量
is_valid, quantity, error = self.validator.validate_quantity("25.5")
self.assertTrue(is_valid)
self.assertEqual(quantity, 25.5)
self.assertIsNone(error)
# 测试带单位的数量
is_valid, quantity, error = self.validator.validate_quantity("30瓶")
self.assertTrue(is_valid)
self.assertEqual(quantity, 30.0)
self.assertIsNone(error)
# 测试零数量
is_valid, quantity, error = self.validator.validate_quantity(0)
self.assertFalse(is_valid)
self.assertEqual(quantity, 0.0)
self.assertIn("数量必须大于0", error)
# 测试负数量
is_valid, quantity, error = self.validator.validate_quantity(-5)
self.assertFalse(is_valid)
self.assertEqual(quantity, 0.0)
self.assertIn("数量必须大于0", error)
# 测试非数字
is_valid, quantity, error = self.validator.validate_quantity("abc")
self.assertFalse(is_valid)
self.assertEqual(quantity, 0.0)
self.assertIn("数量不包含数字", error)
# 测试空值
is_valid, quantity, error = self.validator.validate_quantity(None)
self.assertFalse(is_valid)
self.assertEqual(quantity, 0.0)
self.assertEqual(error, "数量为空")
def test_validate_price(self):
"""
测试单价验证
"""
# 测试有效单价
is_valid, price, is_gift, error = self.validator.validate_price(12.5)
self.assertTrue(is_valid)
self.assertEqual(price, 12.5)
self.assertFalse(is_gift)
self.assertIsNone(error)
# 测试字符串单价
is_valid, price, is_gift, error = self.validator.validate_price("8.0")
self.assertTrue(is_valid)
self.assertEqual(price, 8.0)
self.assertFalse(is_gift)
self.assertIsNone(error)
# 测试零单价(赠品)
is_valid, price, is_gift, error = self.validator.validate_price(0)
self.assertTrue(is_valid)
self.assertEqual(price, 0.0)
self.assertTrue(is_gift)
self.assertIsNone(error)
# 测试"赠品"标记
is_valid, price, is_gift, error = self.validator.validate_price("赠品")
self.assertTrue(is_valid)
self.assertEqual(price, 0.0)
self.assertTrue(is_gift)
self.assertIsNone(error)
# 测试负单价
is_valid, price, is_gift, error = self.validator.validate_price(-5)
self.assertFalse(is_valid)
self.assertEqual(price, 0.0)
self.assertTrue(is_gift)
self.assertIn("单价不能为负数", error)
# 测试空值
is_valid, price, is_gift, error = self.validator.validate_price(None)
self.assertFalse(is_valid)
self.assertEqual(price, 0.0)
self.assertTrue(is_gift)
self.assertEqual(error, "单价为空,视为赠品")
def test_validate_product(self):
"""
测试商品数据验证
"""
# 准备测试数据(有效商品)
product = {
'barcode': '6954767400129',
'name': '测试商品',
'specification': '1*12',
'quantity': 3.0,
'price': 36.0,
'unit': '',
'is_gift': False
}
# 验证有效商品
result = self.validator.validate_product(product)
self.assertEqual(result['barcode'], '6954767400129')
self.assertEqual(result['quantity'], 3.0)
self.assertEqual(result['price'], 36.0)
self.assertFalse(result['is_gift'])
# 验证赠品商品
gift_product = product.copy()
gift_product['price'] = 0
result = self.validator.validate_product(gift_product)
self.assertEqual(result['price'], 0.0)
self.assertTrue(result['is_gift'])
# 验证需要修复的商品
invalid_product = {
'barcode': '5954767-400129', # 需要修复前缀和移除非数字
'name': '测试商品',
'specification': '1*12',
'quantity': '2件', # 需要提取数字
'price': '赠品', # 赠品标记
'unit': '',
'is_gift': False
}
result = self.validator.validate_product(invalid_product)
self.assertEqual(result['barcode'], '6954767400129') # 5->6,移除 '-'
self.assertEqual(result['quantity'], 2.0) # 提取数字
self.assertEqual(result['price'], 0.0) # 赠品价格为0
self.assertTrue(result['is_gift']) # 标记为赠品
if __name__ == '__main__':
unittest.main()
+215
View File
@@ -0,0 +1,215 @@
"""
数据验证器模块
----------
提供对商品数据的验证和修复功能
"""
import re
import logging
from typing import Dict, Any, Optional, List, Tuple, Union
from ..utils.log_utils import get_logger
logger = get_logger(__name__)
class ProductValidator:
"""
商品数据验证器:验证和修复商品数据
"""
def __init__(self):
"""
初始化商品数据验证器
"""
# 仓库标识列表
self.warehouse_identifiers = ["仓库", "仓库全名", "warehouse"]
def validate_barcode(self, barcode: Any) -> Tuple[bool, str, Optional[str]]:
"""
验证并修复条码
Args:
barcode: 原始条码值
Returns:
(是否有效, 修复后的条码, 错误信息)元组
"""
error_message = None
# 处理空值
if barcode is None:
return False, "", "条码为空"
# 转为字符串
barcode_str = str(barcode).strip()
# 处理"仓库"特殊情况
if barcode_str in self.warehouse_identifiers:
return False, barcode_str, "条码为仓库标识"
# 清理条码格式(移除非数字字符)
barcode_clean = re.sub(r'\D', '', barcode_str)
# 如果清理后为空,无效
if not barcode_clean:
return False, barcode_str, "条码不包含数字"
# 对特定的错误条码进行修正(5开头改为6开头)
if len(barcode_clean) > 8 and barcode_clean.startswith('5') and not barcode_clean.startswith('53'):
original_barcode = barcode_clean
barcode_clean = '6' + barcode_clean[1:]
logger.info(f"修正条码前缀 5->6: {original_barcode} -> {barcode_clean}")
# 验证条码长度
if len(barcode_clean) < 8 or len(barcode_clean) > 13:
error_message = f"条码长度异常: {barcode_clean}, 长度={len(barcode_clean)}"
logger.warning(error_message)
return False, barcode_clean, error_message
# 验证条码是否全为数字
if not barcode_clean.isdigit():
error_message = f"条码包含非数字字符: {barcode_clean}"
logger.warning(error_message)
return False, barcode_clean, error_message
# 对于序号9的特殊情况,允许其条码格式
if barcode_clean == "5321545613":
logger.info(f"特殊条码验证通过: {barcode_clean}")
return True, barcode_clean, None
logger.debug(f"条码验证通过: {barcode_clean}")
return True, barcode_clean, None
def validate_quantity(self, quantity: Any) -> Tuple[bool, float, Optional[str]]:
"""
验证并修复数量
Args:
quantity: 原始数量值
Returns:
(是否有效, 修复后的数量, 错误信息)元组
"""
# 处理空值
if quantity is None:
return False, 0.0, "数量为空"
# 如果是字符串,尝试解析
if isinstance(quantity, str):
# 去除空白和非数字字符(保留小数点)
quantity_clean = re.sub(r'[^\d\.]', '', quantity.strip())
if not quantity_clean:
return False, 0.0, "数量不包含数字"
try:
quantity_value = float(quantity_clean)
except ValueError:
return False, 0.0, f"无法将数量 '{quantity}' 转换为数字"
else:
# 尝试直接转换
try:
quantity_value = float(quantity)
except (ValueError, TypeError):
return False, 0.0, f"无法将数量 '{quantity}' 转换为数字"
# 数量必须大于0
if quantity_value <= 0:
return False, 0.0, f"数量必须大于0,当前值: {quantity_value}"
return True, quantity_value, None
def validate_price(self, price: Any) -> Tuple[bool, float, bool, Optional[str]]:
"""
验证并修复单价
Args:
price: 原始单价值
Returns:
(是否有效, 修复后的单价, 是否为赠品, 错误信息)元组
"""
# 初始化不是赠品
is_gift = False
# 处理空值
if price is None:
return False, 0.0, True, "单价为空,视为赠品"
# 如果是字符串,检查赠品标识
if isinstance(price, str):
price_str = price.strip().lower()
if price_str in ["赠品", "gift", "赠送", "0", ""]:
return True, 0.0, True, None
# 去除空白和非数字字符(保留小数点)
price_clean = re.sub(r'[^\d\.]', '', price_str)
if not price_clean:
return False, 0.0, True, "单价不包含数字,视为赠品"
try:
price_value = float(price_clean)
except ValueError:
return False, 0.0, True, f"无法将单价 '{price}' 转换为数字,视为赠品"
else:
# 尝试直接转换
try:
price_value = float(price)
except (ValueError, TypeError):
return False, 0.0, True, f"无法将单价 '{price}' 转换为数字,视为赠品"
# 单价为0视为赠品
if price_value == 0:
return True, 0.0, True, None
# 单价必须大于0
if price_value < 0:
return False, 0.0, True, f"单价不能为负数: {price_value},视为赠品"
return True, price_value, False, None
def validate_product(self, product: Dict[str, Any]) -> Dict[str, Any]:
"""
验证并修复商品数据
Args:
product: 商品数据字典
Returns:
修复后的商品数据字典
"""
# 创建新字典,避免修改原始数据
validated_product = product.copy()
# 验证条码
barcode = product.get('barcode', '')
is_valid, fixed_barcode, error_msg = self.validate_barcode(barcode)
if is_valid:
validated_product['barcode'] = fixed_barcode
else:
logger.warning(f"条码验证失败: {error_msg}")
if fixed_barcode:
# 即使验证失败,但如果有修复后的条码仍然使用它
validated_product['barcode'] = fixed_barcode
# 验证数量
quantity = product.get('quantity', 0)
is_valid, fixed_quantity, error_msg = self.validate_quantity(quantity)
if is_valid:
validated_product['quantity'] = fixed_quantity
else:
logger.warning(f"数量验证失败: {error_msg}")
validated_product['quantity'] = 0.0
# 验证单价
price = product.get('price', 0)
is_valid, fixed_price, is_gift, error_msg = self.validate_price(price)
validated_product['price'] = fixed_price
# 如果单价验证结果表示为赠品,更新赠品标识
if is_gift:
validated_product['is_gift'] = True
if error_msg:
logger.info(error_msg)
return validated_product