Initial commit - Zhangmenu Django project
A menu management application built with Django, featuring: - Dish management with cover images - Order system with party dates and participants - REST API with Django REST Framework - Bootstrap-styled frontend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from .dish_service import DishService
|
||||
from .order_service import OrderService
|
||||
from .parser_service import ParserService
|
||||
|
||||
__all__ = ['DishService', 'OrderService', 'ParserService']
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.db.models import F
|
||||
from menu.models import Dish
|
||||
|
||||
|
||||
class DishService:
|
||||
"""菜品业务逻辑"""
|
||||
|
||||
@staticmethod
|
||||
def get_queryset(category_id=None, search=None):
|
||||
"""获取菜品查询集"""
|
||||
queryset = Dish.objects.select_related('category')
|
||||
|
||||
if category_id:
|
||||
queryset = queryset.filter(category_id=category_id)
|
||||
if search:
|
||||
queryset = queryset.filter(name__icontains=search)
|
||||
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def increment_view_count(dish):
|
||||
"""增加浏览次数"""
|
||||
Dish.objects.filter(pk=dish.pk).update(view_count=F('view_count') + 1)
|
||||
dish.refresh_from_db(fields=['view_count'])
|
||||
return dish
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.utils import timezone
|
||||
|
||||
from menu.models import Order
|
||||
|
||||
|
||||
class OrderService:
|
||||
"""订单业务逻辑"""
|
||||
|
||||
@staticmethod
|
||||
def get_queryset(status_filter=None):
|
||||
"""获取订单查询集"""
|
||||
queryset = Order.objects.prefetch_related('dishes', 'dishes__category')
|
||||
if status_filter:
|
||||
queryset = queryset.filter(status=status_filter)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def create_order(serializer, dish_ids):
|
||||
"""创建订单并绑定菜品"""
|
||||
if dish_ids is None:
|
||||
dish_ids = []
|
||||
|
||||
order = serializer.save(dish_ids=dish_ids)
|
||||
return Order.objects.prefetch_related('dishes', 'dishes__category').get(pk=order.pk)
|
||||
|
||||
@staticmethod
|
||||
def complete_order(order):
|
||||
"""完成订单"""
|
||||
order.status = 'completed'
|
||||
order.completed_at = timezone.now()
|
||||
order.save()
|
||||
return order
|
||||
@@ -0,0 +1,217 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from django.conf import settings
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
class ParserService:
|
||||
"""URL解析业务逻辑"""
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""解析外部URL获取菜品信息"""
|
||||
if not url:
|
||||
raise ValueError('请提供URL')
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
hostname = parsed_url.hostname
|
||||
allowed_hosts = getattr(settings, 'ALLOWED_RECIPE_HOSTS', set())
|
||||
|
||||
if parsed_url.scheme not in {'http', 'https'}:
|
||||
raise ValueError('URL协议不受支持')
|
||||
if not hostname:
|
||||
raise ValueError('URL缺少主机名')
|
||||
if hostname not in allowed_hosts:
|
||||
raise ValueError('该站点不在允许的解析范围内')
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
result = {
|
||||
'name': '',
|
||||
'cover_image': '',
|
||||
'description': '',
|
||||
'ingredients': []
|
||||
}
|
||||
|
||||
# 根据不同网站选择解析方法
|
||||
if hostname in {'xiachufang.com', 'www.xiachufang.com'}:
|
||||
result = ParserService._parse_xiachufang(soup, url)
|
||||
elif hostname in {'meishij.net', 'www.meishij.net'}:
|
||||
result = ParserService._parse_meishij(soup)
|
||||
elif hostname in {'caipuw.com', 'www.caipuw.com'}:
|
||||
result = ParserService._parse_caipuw(soup)
|
||||
else:
|
||||
result = ParserService._parse_generic(soup)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _parse_xiachufang(soup, url):
|
||||
"""解析下厨房网站"""
|
||||
result = {
|
||||
'name': '',
|
||||
'cover_image': '',
|
||||
'description': '',
|
||||
'ingredients': []
|
||||
}
|
||||
|
||||
# 尝试从JSON-LD获取数据
|
||||
json_ld = soup.find_all('script', type='application/ld+json')
|
||||
for script in json_ld:
|
||||
try:
|
||||
data = json.loads(script.string)
|
||||
if data.get('@type') == 'Recipe':
|
||||
result['name'] = data.get('name', '')
|
||||
result['cover_image'] = data.get('image', '')
|
||||
if isinstance(result['cover_image'], list):
|
||||
result['cover_image'] = result['cover_image'][0]
|
||||
|
||||
# 解析材料
|
||||
for ing in data.get('recipeIngredient', []):
|
||||
match = re.match(r'^([\d.]+\s*[克g个勺片块大中小]+)?\s*(.+)$', ing)
|
||||
if match and match.group(2):
|
||||
amount, name = match.groups()
|
||||
result['ingredients'].append({
|
||||
'name': name.strip(),
|
||||
'amount': (amount.strip() + '克') if amount and not ('克' in amount or '个' in amount or '勺' in amount or '片' in amount or '块' in amount) else (amount.strip() if amount else '')
|
||||
})
|
||||
else:
|
||||
result['ingredients'].append({'name': ing, 'amount': ''})
|
||||
|
||||
# 解析做法
|
||||
steps = []
|
||||
recipe_steps = data.get('recipeInstructions', [])
|
||||
if isinstance(recipe_steps, list):
|
||||
for step in recipe_steps:
|
||||
text = step.get('text', '') if isinstance(step, dict) else str(step)
|
||||
if text:
|
||||
steps.append(text)
|
||||
elif isinstance(recipe_steps, str):
|
||||
parts = re.split(r'(?=\d+\.)', recipe_steps)
|
||||
for part in parts:
|
||||
part = re.sub(r'^\d+\.\s*', '', part.strip())
|
||||
if part:
|
||||
steps.append(part)
|
||||
|
||||
if steps:
|
||||
result['description'] = '\n\n'.join([f'{i+1}. {s}' for i, s in enumerate(steps)])
|
||||
break
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
continue
|
||||
|
||||
# 回退到HTML解析
|
||||
if not result['name']:
|
||||
title = soup.select_one('h1.title')
|
||||
if title:
|
||||
result['name'] = title.get_text(strip=True)
|
||||
|
||||
if not result['cover_image']:
|
||||
cover = soup.select_one('div.cover img') or soup.select_one('div.image img')
|
||||
if cover:
|
||||
result['cover_image'] = cover.get('src', '')
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _parse_meishij(soup):
|
||||
"""解析美食杰网站"""
|
||||
result = {
|
||||
'name': '',
|
||||
'cover_image': '',
|
||||
'description': '',
|
||||
'ingredients': []
|
||||
}
|
||||
|
||||
title = soup.select_one('h1.recipe_title')
|
||||
if title:
|
||||
result['name'] = title.get_text(strip=True)
|
||||
|
||||
cover = soup.select_one('div.recipe_img img')
|
||||
if cover:
|
||||
result['cover_image'] = cover.get('src', '')
|
||||
|
||||
# 材料
|
||||
ing_list = soup.select('div.yl li')
|
||||
for ing in ing_list:
|
||||
text = ing.get_text(strip=True)
|
||||
if text:
|
||||
parts = text.split()
|
||||
if len(parts) >= 2:
|
||||
result['ingredients'].append({'name': parts[0], 'amount': ' '.join(parts[1:])})
|
||||
elif len(parts) == 1:
|
||||
result['ingredients'].append({'name': parts[0], 'amount': ''})
|
||||
|
||||
# 做法
|
||||
steps = []
|
||||
step_list = soup.select('div.step_content')
|
||||
for step in step_list:
|
||||
text = step.get_text(strip=True)
|
||||
if text:
|
||||
steps.append(text)
|
||||
result['description'] = '\n\n'.join([f'{i+1}. {s}' for i, s in enumerate(steps)])
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _parse_caipuw(soup):
|
||||
"""解析菜谱大全网站"""
|
||||
result = {
|
||||
'name': '',
|
||||
'cover_image': '',
|
||||
'description': '',
|
||||
'ingredients': []
|
||||
}
|
||||
|
||||
title = soup.select_one('h1.title')
|
||||
if title:
|
||||
result['name'] = title.get_text(strip=True)
|
||||
|
||||
cover = soup.select_one('div.image img')
|
||||
if cover:
|
||||
result['cover_image'] = cover.get('src', '')
|
||||
|
||||
# 材料
|
||||
ing_list = soup.select('div.ingredients li') or soup.select('ul.ingredients li')
|
||||
for ing in ing_list:
|
||||
text = ing.get_text(strip=True)
|
||||
if text:
|
||||
result['ingredients'].append({'name': text, 'amount': ''})
|
||||
|
||||
# 做法
|
||||
steps = []
|
||||
step_list = soup.select('div.steps li') or soup.select('div.step')
|
||||
for step in step_list:
|
||||
text = step.get_text(strip=True)
|
||||
if text:
|
||||
steps.append(text)
|
||||
result['description'] = '\n\n'.join([f'{i+1}. {s}' for i, s in enumerate(steps)])
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _parse_generic(soup):
|
||||
"""通用解析"""
|
||||
result = {
|
||||
'name': '',
|
||||
'cover_image': '',
|
||||
'description': '',
|
||||
'ingredients': []
|
||||
}
|
||||
|
||||
title = soup.select_one('h1') or soup.select_one('title')
|
||||
if title:
|
||||
result['name'] = title.get_text(strip=True)[:100]
|
||||
|
||||
img = soup.select_one('article img') or soup.select_one('div.content img') or soup.select_one('img')
|
||||
if img:
|
||||
result['cover_image'] = img.get('src', '')
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user