增加图片取件码

This commit is contained in:
aaron 2025-02-21 16:49:51 +08:00
parent 30b68a662d
commit 6da0d075ad
5 changed files with 294 additions and 81 deletions

32
app/api/endpoints/ocr.py Normal file
View File

@ -0,0 +1,32 @@
from fastapi import APIRouter, Depends, UploadFile, File
from app.core.response import success_response, error_response, ResponseModel
from app.core.ocr_service import ocr_service
from app.api.deps import get_current_user
from app.models.user import UserDB
router = APIRouter()
@router.post("/pickup_code", response_model=ResponseModel)
async def recognize_pickup_code(
file: UploadFile = File(...),
current_user: UserDB = Depends(get_current_user)
):
"""识别收件码图片"""
try:
# 检查文件类型
if not file.content_type.startswith('image/'):
return error_response(code=400, message="只能上传图片文件")
# 读取文件内容
content = await file.read()
# 调用OCR服务识别图片
result = await ocr_service.recognize_pickup_code(content)
if not result.get("stations") or not any(station["pickup_codes"] for station in result["stations"]):
return error_response(code=400, message="未能识别到取件码")
return success_response(data=result)
except Exception as e:
return error_response(code=500, message=f"识别失败: {str(e)}")

View File

@ -51,6 +51,9 @@ def calculate_price(price_request: OrderPriceCalculateRequest,user: UserDB,db: S
OrderPriceResult: 包含价格信息和使用的优惠券/积分信息 OrderPriceResult: 包含价格信息和使用的优惠券/积分信息
""" """
# 计算所有包裹中的取件码总数 # 计算所有包裹中的取件码总数
if price_request.package_count > 0:
package_count = price_request.package_count
else:
package_count = sum( package_count = sum(
# 如果package.pickup_codes是空字符串则取0 # 如果package.pickup_codes是空字符串则取0
0 if len(package.pickup_codes.split(',')) == 0 else len(package.pickup_codes.split(',')) 0 if len(package.pickup_codes.split(',')) == 0 else len(package.pickup_codes.split(','))
@ -61,10 +64,10 @@ def calculate_price(price_request: OrderPriceCalculateRequest,user: UserDB,db: S
result = OrderPriceResult( result = OrderPriceResult(
price_info=OrderPriceInfo( price_info=OrderPriceInfo(
package_count=package_count, package_count=package_count,
original_amount=settings.ORDER_BASE_PRICE,
coupon_discount_amount=0, coupon_discount_amount=0,
points_discount_amount=0, points_discount_amount=0,
final_amount=settings.ORDER_BASE_PRICE original_amount=0,
final_amount=0
), ),
price_detail_text=settings.ORDER_PREORDER_PRICE_TEXT price_detail_text=settings.ORDER_PREORDER_PRICE_TEXT
) )
@ -185,6 +188,7 @@ async def create_order(
address_community_name=address.community_name, address_community_name=address.community_name,
address_community_building_name=address.community_building_name, address_community_building_name=address.community_building_name,
address_detail=address.address_detail, address_detail=address.address_detail,
pickup_images=order.price_request.pickup_images,
package_count=price_info.package_count, package_count=price_info.package_count,
original_amount=original_amount, original_amount=original_amount,
coupon_discount_amount=coupon_discount, coupon_discount_amount=coupon_discount,
@ -197,6 +201,7 @@ async def create_order(
db.add(db_order) db.add(db_order)
# 创建订单包裹 # 创建订单包裹
if order.price_request.packages:
for package in order.price_request.packages: for package in order.price_request.packages:
# 如果包裹有取件码,则创建包裹 # 如果包裹有取件码,则创建包裹
if len(package.pickup_codes) > 0: if len(package.pickup_codes) > 0:
@ -294,17 +299,6 @@ async def get_order_detail(
if not order: if not order:
return error_response(code=404, message="订单不存在") return error_response(code=404, message="订单不存在")
# 查询包裹信息,包含驿站名称
packages = db.query(
ShippingOrderPackageDB,
StationDB.name.label('station_name')
).join(
StationDB,
ShippingOrderPackageDB.station_id == StationDB.id
).filter(
ShippingOrderPackageDB.orderid == orderid
).all()
# 如果有配送员 id则获取配送员信息 # 如果有配送员 id则获取配送员信息
if order.deliveryman_user_id: if order.deliveryman_user_id:
deliveryman_user = db.query(UserDB).filter( deliveryman_user = db.query(UserDB).filter(
@ -326,16 +320,32 @@ async def get_order_detail(
# 计算配送员分账金额 # 计算配送员分账金额
deliveryman_share = round(order.original_amount * settings.ORDER_DELIVERYMAN_SHARE_RATIO, 1) deliveryman_share = round(order.original_amount * settings.ORDER_DELIVERYMAN_SHARE_RATIO, 1)
# 构建完成图片
complete_images = [] # 查询包裹信息
if order.complete_images: packages = db.query(
complete_images = order.complete_images.split(",") ShippingOrderPackageDB
complete_images = [process_image(image).thumbnail(500, 500).format(ImageFormat.WEBP).build() for image in complete_images] ).filter(
ShippingOrderPackageDB.orderid == orderid
).all()
if packages:
# 构建包裹信息,包含驿站名称
package_list = [{
"id": p.id,
"orderid": p.orderid,
"station_id": p.station_id,
"station_name": p.station_name,
"pickup_codes": p.pickup_codes,
"create_time": p.create_time
} for p in packages]
else:
package_list = []
# 构建响应数据 # 构建响应数据
order_data = { order_data = {
"orderid": order.orderid, "orderid": order.orderid,
"userid": order.userid, "userid": order.userid,
"pickup_images": order.optimized_pickup_images,
"package_count": order.package_count, "package_count": order.package_count,
"original_amount": order.original_amount, "original_amount": order.original_amount,
"coupon_discount_amount": order.coupon_discount_amount, "coupon_discount_amount": order.coupon_discount_amount,
@ -343,7 +353,8 @@ async def get_order_detail(
"final_amount": order.final_amount, "final_amount": order.final_amount,
"deliveryman_share": deliveryman_share, "deliveryman_share": deliveryman_share,
"status": order.status, "status": order.status,
"complete_images": complete_images, "complete_images": order.optimized_complete_images,
"packages": package_list,
"create_time": order.create_time, "create_time": order.create_time,
"complete_time": order.completed_time, "complete_time": order.completed_time,
@ -367,19 +378,10 @@ async def get_order_detail(
"community_name": order.address_community_name "community_name": order.address_community_name
} }
# 构建包裹信息,包含驿站名称
package_list = [{
"id": p.ShippingOrderPackageDB.id,
"orderid": p.ShippingOrderPackageDB.orderid,
"station_id": p.ShippingOrderPackageDB.station_id,
"station_name": p.station_name,
"pickup_codes": p.ShippingOrderPackageDB.pickup_codes,
"create_time": p.ShippingOrderPackageDB.create_time
} for p in packages]
return success_response(data={ return success_response(data={
"order": order_data, "order": order_data
"packages": package_list
}) })
# 提供一个接口传入community_id返回订单状态数量 # 提供一个接口传入community_id返回订单状态数量
@ -526,13 +528,15 @@ async def get_user_orders(
).all() ).all()
# 格式化包裹信息 # 格式化包裹信息
if packages:
package_list = [{ package_list = [{
"id": package.id, "id": package.id,
"station_id": package.station_id, "station_id": package.station_id,
"station_name": package.station_name, "station_name": package.station_name,
"pickup_codes": package.pickup_codes "pickup_codes": package.pickup_codes
} for package in packages] } for package in packages]
else:
package_list = []
#查询子订单 #查询子订单
sub_orders = db.query(PointProductOrderDB).filter( sub_orders = db.query(PointProductOrderDB).filter(
@ -543,6 +547,7 @@ async def get_user_orders(
"orderid": order.orderid, "orderid": order.orderid,
"userid": order.userid, "userid": order.userid,
"status": order.status, "status": order.status,
"pickup_images": order.optimized_pickup_images,
"package_count": order.package_count, "package_count": order.package_count,
"create_time": order.create_time, "create_time": order.create_time,
"delivery_method": order.delivery_method, "delivery_method": order.delivery_method,
@ -706,22 +711,21 @@ async def get_deliveryman_orders(
for order in results: for order in results:
# 查询订单包裹信息 # 查询订单包裹信息
packages = db.query( packages = db.query(
ShippingOrderPackageDB, ShippingOrderPackageDB
StationDB.name.label('station_name')
).join(
StationDB,
ShippingOrderPackageDB.station_id == StationDB.id
).filter( ).filter(
ShippingOrderPackageDB.orderid == order.orderid ShippingOrderPackageDB.orderid == order.orderid
).all() ).all()
# 格式化包裹信息 # 格式化包裹信息
if packages:
package_list = [{ package_list = [{
"id": package.ShippingOrderPackageDB.id, "id": package.id,
"station_id": package.ShippingOrderPackageDB.station_id, "station_id": package.station_id,
"station_name": package.station_name, "station_name": package.station_name,
"pickup_codes": package.ShippingOrderPackageDB.pickup_codes "pickup_codes": package.pickup_codes
} for package in packages] } for package in packages]
else:
package_list = []
# 查询子订单 # 查询子订单
sub_orders = db.query(PointProductOrderDB).filter( sub_orders = db.query(PointProductOrderDB).filter(
@ -732,9 +736,14 @@ async def get_deliveryman_orders(
"orderid": order.orderid, "orderid": order.orderid,
"userid": order.userid, "userid": order.userid,
"status": order.status, "status": order.status,
"pickup_images": order.optimized_pickup_images,
"package_count": order.package_count, "package_count": order.package_count,
"create_time": order.create_time, "create_time": order.create_time,
"delivery_method": order.delivery_method, "delivery_method": order.delivery_method,
"original_amount": order.original_amount,
"coupon_discount_amount": order.coupon_discount_amount,
"point_discount_amount": order.point_discount_amount,
"final_amount": order.final_amount,
"packages": package_list, "packages": package_list,
"sub_orders": [PointProductOrderInfo.model_validate(sub_order) for sub_order in sub_orders], "sub_orders": [PointProductOrderInfo.model_validate(sub_order) for sub_order in sub_orders],
"address": { "address": {
@ -1087,27 +1096,29 @@ async def get_admin_orders(
for order in results: for order in results:
# 查询订单包裹信息 # 查询订单包裹信息
packages = db.query( packages = db.query(
ShippingOrderPackageDB, ShippingOrderPackageDB
StationDB.name.label('station_name')
).join(
StationDB,
ShippingOrderPackageDB.station_id == StationDB.id
).filter( ).filter(
ShippingOrderPackageDB.orderid == order.orderid ShippingOrderPackageDB.orderid == order.orderid
).all() ).all()
# 格式化包裹信息 # 格式化包裹信息
package_list = [{ package_list = [{
"id": package.ShippingOrderPackageDB.id, "id": package.id,
"station_id": package.ShippingOrderPackageDB.station_id, "station_id": package.station_id,
"station_name": package.station_name, "station_name": package.station_name,
"pickup_codes": package.ShippingOrderPackageDB.pickup_codes "pickup_codes": package.pickup_codes
} for package in packages] } for package in packages]
# 查询子订单
sub_orders = db.query(PointProductOrderDB).filter(
PointProductOrderDB.delivery_order_id == order.orderid
).all()
orders.append({ orders.append({
"orderid": order.orderid, "orderid": order.orderid,
"userid": order.userid, "userid": order.userid,
"status": order.status, "status": order.status,
"pickup_images": order.optimized_pickup_images,
"package_count": order.package_count, "package_count": order.package_count,
"create_time": order.create_time, "create_time": order.create_time,
"delivery_method": order.delivery_method, "delivery_method": order.delivery_method,
@ -1116,13 +1127,17 @@ async def get_admin_orders(
"point_discount_amount": order.point_discount_amount, "point_discount_amount": order.point_discount_amount,
"final_amount": order.final_amount, "final_amount": order.final_amount,
"packages": package_list, "packages": package_list,
"sub_orders": [PointProductOrderInfo.model_validate(sub_order) for sub_order in sub_orders],
"address": { "address": {
"name": order.address_customer_name, "name": order.address_customer_name,
"phone": order.address_customer_phone, "phone": order.address_customer_phone,
"gender": order.address_customer_gender,
"community_id": order.address_community_id,
"community_name": order.address_community_name, "community_name": order.address_community_name,
"building_id": order.address_community_building_id,
"building_name": order.address_community_building_name, "building_name": order.address_community_building_name,
"address_detail": order.address_detail "address_detail": order.address_detail
} },
}) })
return success_response(data={ return success_response(data={

154
app/core/ocr_service.py Normal file
View File

@ -0,0 +1,154 @@
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.ocr.v20181119 import ocr_client, models
from app.core.config import settings
import json
import base64
class OCRService:
def __init__(self):
cred = credential.Credential(settings.TENCENT_SECRET_ID, settings.TENCENT_SECRET_KEY)
httpProfile = HttpProfile()
httpProfile.endpoint = "ocr.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
self.client = ocr_client.OcrClient(cred, settings.TENCENT_REGION, clientProfile)
async def recognize_pickup_code(self, image_content: bytes) -> dict:
"""识别收件码图片"""
try:
# 将图片内容转为base64
img_base64 = base64.b64encode(image_content).decode()
req = models.GeneralAccurateOCRRequest()
req.ImageBase64 = img_base64
resp = self.client.GeneralAccurateOCR(req)
result = json.loads(resp.to_json_string())
print(result)
# 解析文本内容
text_list = []
for item in result.get("TextDetections", []):
text_list.append(item["DetectedText"])
# 提取关键信息
pickup_info = self._extract_pickup_info(text_list)
return pickup_info
except Exception as e:
raise Exception(f"识别失败: {str(e)}")
def _is_valid_pickup_code(self, text: str) -> bool:
"""验证是否是有效的取件码格式"""
import re
# 匹配格式xx-x-xxx 或 xx-xx-xxx 等类似格式
patterns = [
r'\b\d{1,2}-\d{1,2}-\d{2,3}\b', # 15-4-223
r'\b\d{4,8}\b', # 普通4-8位数字
]
for pattern in patterns:
if re.search(pattern, text):
return True
return False
def _extract_pickup_info(self, text_list: list) -> dict:
"""提取收件码信息"""
# 存储所有驿站信息
stations = []
current_station = None
current_codes = []
pickup_info = {
"stations": [], # 驿站列表
"app_type": None # APP类型(菜鸟/京东等)
}
# 识别APP类型
app_keywords = {
"菜鸟": "CAINIAO",
"京东": "JD",
"顺丰": "SF"
}
for text in text_list:
# 查找APP类型
for keyword, app_type in app_keywords.items():
if keyword in text:
pickup_info["app_type"] = app_type
break
# 查找驿站名称
is_station = False
if "驿站" in text:
is_station = True
elif "站点" in text:
is_station = True
elif "" in text:
is_station = True
elif "" in text:
is_station = True
elif "分拨" in text:
is_station = True
elif "分拣" in text:
is_station = True
elif "分拨" in text:
is_station = True
if is_station:
# 如果之前有未保存的驿站信息,先保存
if current_station and current_codes:
stations.append({
"station_name": current_station,
"pickup_codes": current_codes
})
# 开始新的驿站信息收集
current_station = text
current_codes = []
# 查找取件码
if self._is_valid_pickup_code(text):
# 清理文本中的多余字符
cleaned_text = ''.join(c for c in text if c.isdigit() or c == '-')
# 提取所有匹配的取件码
import re
for pattern in [r'\d{1,2}-\d{1,2}-\d{2,3}', r'\d{4,8}']:
matches = re.finditer(pattern, cleaned_text)
for match in matches:
code = match.group()
# 如果已找到驿站,将取件码添加到当前驿站
if current_station and code not in current_codes:
current_codes.append(code)
# 如果还没找到驿站,暂存取件码
elif code not in current_codes:
current_codes.append(code)
# 保存最后一个驿站的信息
if current_station and current_codes:
stations.append({
"station_name": current_station,
"pickup_codes": current_codes
})
# 如果有未分配到驿站的取件码,创建一个默认驿站
elif current_codes:
stations.append({
"station_name": None,
"pickup_codes": current_codes
})
# 如果找到了取件码但没找到APP类型根据取件码格式推测
if stations and not pickup_info["app_type"]:
# 如果任一取件码包含连字符,判定为菜鸟
for station in stations:
if any('-' in code for code in station["pickup_codes"]):
pickup_info["app_type"] = "CAINIAO"
break
pickup_info["stations"] = stations
return pickup_info
ocr_service = OCRService()

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order, coupon_activity from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order, coupon_activity, ocr
from app.models.database import Base, engine from app.models.database import Base, engine
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -58,6 +58,7 @@ app.include_router(message.router, prefix="/api/message", tags=["消息中心"])
app.include_router(upload.router, prefix="/api/upload", tags=["文件上传"]) app.include_router(upload.router, prefix="/api/upload", tags=["文件上传"])
app.include_router(config.router, prefix="/api/config", tags=["系统配置"]) app.include_router(config.router, prefix="/api/config", tags=["系统配置"])
app.include_router(log.router, prefix="/api/logs", tags=["系统日志"]) app.include_router(log.router, prefix="/api/logs", tags=["系统日志"])
app.include_router(ocr.router, prefix="/api/ai/ocr", tags=["图像识别"])
@app.get("/") @app.get("/")

View File

@ -51,6 +51,8 @@ class ShippingOrderDB(Base):
address_community_building_name = Column(String(50), nullable=False, default='') # 楼栋名称快照 address_community_building_name = Column(String(50), nullable=False, default='') # 楼栋名称快照
address_detail = Column(String(100), nullable=False, default='') # 详细地址快照 address_detail = Column(String(100), nullable=False, default='') # 详细地址快照
# 取件图片
pickup_images = Column(String(1000), nullable=True) # 取件图片URL多个URL用逗号分隔
delivery_method = Column(Enum(DeliveryMethod), nullable=False) delivery_method = Column(Enum(DeliveryMethod), nullable=False)
package_count = Column(Integer, nullable=False) package_count = Column(Integer, nullable=False)
@ -82,6 +84,12 @@ class ShippingOrderDB(Base):
return [process_image(image).quality(80).thumbnail(width=450, height=450).format(ImageFormat.WEBP).build() for image in self.complete_images.split(",")] return [process_image(image).quality(80).thumbnail(width=450, height=450).format(ImageFormat.WEBP).build() for image in self.complete_images.split(",")]
return [] return []
@property
def optimized_pickup_images(self):
if self.pickup_images:
return [process_image(image).quality(80).thumbnail(width=450, height=450).format(ImageFormat.WEBP).build() for image in self.pickup_images.split(",")]
return []
class ShippingOrderPackageDB(Base): class ShippingOrderPackageDB(Base):
__tablename__ = "shipping_order_packages" __tablename__ = "shipping_order_packages"
@ -99,7 +107,9 @@ class OrderPackage(BaseModel):
# 先定义 OrderPriceCalculateRequest # 先定义 OrderPriceCalculateRequest
class OrderPriceCalculateRequest(BaseModel): class OrderPriceCalculateRequest(BaseModel):
packages: List[OrderPackage] package_count: Optional[int] = None
pickup_images: Optional[str] = None
packages: Optional[List[OrderPackage]] = None
# 然后再定义 OrderCreate # 然后再定义 OrderCreate
class OrderCreate(BaseModel): class OrderCreate(BaseModel):
@ -123,6 +133,7 @@ class OrderInfo(BaseModel):
address_detail: str address_detail: str
address_customer_gender: Gender address_customer_gender: Gender
pickup_images: Optional[str] = None
package_count: int package_count: int
original_amount: float original_amount: float
coupon_discount_amount: float coupon_discount_amount: float
@ -165,12 +176,12 @@ class OrderPackageInfo(BaseModel):
from_attributes = True from_attributes = True
class OrderPriceInfo(BaseModel): class OrderPriceInfo(BaseModel):
package_count: int package_count: int = 0
original_amount: float original_amount: float = 0
coupon_discount_amount: float = 0 coupon_discount_amount: float = 0
points_discount_amount: float = 0 points_discount_amount: float = 0
coupon_id: Optional[int] = None coupon_id: Optional[int] = None
final_amount: float final_amount: float = 0
# 添加取消订单请求模型 # 添加取消订单请求模型
class OrderCancel(BaseModel): class OrderCancel(BaseModel):