docs(readme): 编写项目README文档,描述功能与架构

- 完整撰写玉宗珠宝设计大师项目README,介绍项目概况及核心功能
- 说明用户认证系统实现及优势,包含JWT鉴权和密码加密细节
- 详细描述品类管理系统,支持多流程类型和多种玉石品类
- 说明设计图生成方案及技术,包含Pillow生成示例及字体支持
- 介绍设计管理功能,支持分页浏览、预览、下载和删除设计
- 个人信息管理模块说明,涵盖昵称、手机号、密码的安全修改
- 绘制业务流程图和关键数据流图,清晰展现系统架构与数据流
- 提供详细API调用链路及参数说明,涵盖用户、品类、设计接口
- 列明技术栈及版本,包含前后端框架、ORM、认证、加密等工具
- 展示目录结构,标明后端与前端项目布局
- 规划本地开发环境与启动步骤,包括数据库初始化及运行命令
- 说明服务器部署流程和Nginx配置方案
- 详细数据库表结构说明及环境变量配置指导
- 汇总常用开发及测试命令,方便开发调试与部署管理
This commit is contained in:
changyoutongxue
2026-03-27 13:10:17 +08:00
commit e3ff55b4db
69 changed files with 8551 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# 业务服务模块
from . import design_service
from .mock_generator import generate_mock_design
__all__ = [
"design_service",
"generate_mock_design",
]

View File

@@ -0,0 +1,67 @@
"""
认证服务
提供用户注册和登录业务逻辑
"""
from typing import Optional
from sqlalchemy.orm import Session
from ..models.user import User
from ..schemas.user import UserCreate
from ..utils.security import get_password_hash, verify_password
def register_user(db: Session, user_data: UserCreate) -> User:
"""
注册新用户
Args:
db: 数据库会话
user_data: 用户注册数据
Returns:
创建的用户对象
Raises:
ValueError: 用户名已存在时抛出
"""
# 检查用户名是否已存在
existing_user = db.query(User).filter(User.username == user_data.username).first()
if existing_user:
raise ValueError("用户名已存在")
# 创建新用户,密码加密存储
db_user = User(
username=user_data.username,
hashed_password=get_password_hash(user_data.password),
nickname=user_data.nickname or user_data.username
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def authenticate_user(db: Session, username: str, password: str) -> Optional[User]:
"""
验证用户登录
Args:
db: 数据库会话
username: 用户名
password: 明文密码
Returns:
验证成功返回用户对象,失败返回 None
"""
# 查询用户
user = db.query(User).filter(User.username == username).first()
if not user:
return None
# 验证密码
if not verify_password(password, user.hashed_password):
return None
return user

View File

@@ -0,0 +1,150 @@
"""
设计服务
处理设计相关的业务逻辑
"""
import os
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import desc
from ..models import Design, Category, SubType, Color
from ..schemas import DesignCreate
from ..config import settings
from .mock_generator import generate_mock_design
def create_design(db: Session, user_id: int, design_data: DesignCreate) -> Design:
"""
创建设计记录
1. 创建设计记录status=generating
2. 调用 mock_generator 生成图片
3. 更新设计记录status=completed, image_url
4. 返回设计对象
"""
# 获取关联信息
category = db.query(Category).filter(Category.id == design_data.category_id).first()
if not category:
raise ValueError(f"品类不存在: {design_data.category_id}")
sub_type = None
if design_data.sub_type_id:
sub_type = db.query(SubType).filter(SubType.id == design_data.sub_type_id).first()
color = None
if design_data.color_id:
color = db.query(Color).filter(Color.id == design_data.color_id).first()
# 创建设计记录
design = Design(
user_id=user_id,
category_id=design_data.category_id,
sub_type_id=design_data.sub_type_id,
color_id=design_data.color_id,
prompt=design_data.prompt,
carving_technique=design_data.carving_technique,
design_style=design_data.design_style,
motif=design_data.motif,
size_spec=design_data.size_spec,
surface_finish=design_data.surface_finish,
usage_scene=design_data.usage_scene,
status="generating"
)
db.add(design)
db.flush() # 获取 ID
# 生成图片
save_path = os.path.join(settings.UPLOAD_DIR, "designs", f"{design.id}.png")
image_url = generate_mock_design(
category_name=category.name,
sub_type_name=sub_type.name if sub_type else None,
color_name=color.name if color else None,
prompt=design_data.prompt,
save_path=save_path,
carving_technique=design_data.carving_technique,
design_style=design_data.design_style,
motif=design_data.motif,
size_spec=design_data.size_spec,
surface_finish=design_data.surface_finish,
usage_scene=design_data.usage_scene,
)
# 更新设计记录
design.image_url = image_url
design.status = "completed"
db.commit()
db.refresh(design)
return design
def get_user_designs(
db: Session,
user_id: int,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Design], int]:
"""
分页查询用户设计历史
Returns:
(设计列表, 总数)
"""
query = db.query(Design).filter(Design.user_id == user_id)
# 获取总数
total = query.count()
# 分页查询,按创建时间倒序
offset = (page - 1) * page_size
designs = query.order_by(desc(Design.created_at)).offset(offset).limit(page_size).all()
return designs, total
def get_design_by_id(db: Session, design_id: int, user_id: int) -> Optional[Design]:
"""
获取单个设计
只返回属于该用户的设计
"""
return db.query(Design).filter(
Design.id == design_id,
Design.user_id == user_id
).first()
def delete_design(db: Session, design_id: int, user_id: int) -> bool:
"""
删除设计
1. 查找设计(必须属于该用户)
2. 删除图片文件
3. 删除数据库记录
Returns:
是否删除成功
"""
design = db.query(Design).filter(
Design.id == design_id,
Design.user_id == user_id
).first()
if not design:
return False
# 删除图片文件
if design.image_url:
# image_url 格式: /uploads/designs/1001.png
# 转换为实际文件路径
file_path = design.image_url.lstrip("/")
if os.path.exists(file_path):
try:
os.remove(file_path)
except Exception:
pass # 忽略删除失败
# 删除数据库记录
db.delete(design)
db.commit()
return True

View File

@@ -0,0 +1,222 @@
"""
Mock 图片生成服务
使用 Pillow 生成带文字的占位设计图
"""
import os
from typing import Optional, Tuple, Union
from PIL import Image, ImageDraw, ImageFont
# 颜色映射表(中文颜色名 -> 十六进制)
COLOR_MAP = {
# 和田玉国标色种
"白玉": "#FEFEF2",
"青白玉": "#E8EDE4",
"青玉": "#7A8B6E",
"碧玉": "#2D5F2D",
"翠青": "#6BAF8D",
"黄玉": "#D4A843",
"糖玉": "#C4856C",
"墨玉": "#2C2C2C",
"藕粉": "#E8B4B8",
"烟紫": "#8B7D9B",
# 原有颜色
"糖白": "#F5F0E8",
# 通用颜色
"白色": "#FFFFFF",
"黑色": "#333333",
"红色": "#C41E3A",
"绿色": "#228B22",
"蓝色": "#4169E1",
"黄色": "#FFD700",
"紫色": "#9370DB",
"粉色": "#FFB6C1",
"橙色": "#FF8C00",
}
# 默认背景色(浅灰)
DEFAULT_BG_COLOR = "#E8E4DF"
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
"""将十六进制颜色转换为 RGB 元组"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def get_contrast_text_color(bg_color: str) -> str:
"""根据背景色计算合适的文字颜色(黑或白)"""
r, g, b = hex_to_rgb(bg_color)
# 使用亮度公式
brightness = (r * 299 + g * 587 + b * 114) / 1000
return "#333333" if brightness > 128 else "#FFFFFF"
def get_font(size: int = 24) -> Union[ImageFont.FreeTypeFont, ImageFont.ImageFont]:
"""
获取字体,优先使用系统中文字体
"""
# 常见中文字体路径
font_paths = [
# macOS
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/Library/Fonts/Arial Unicode.ttf",
# Linux
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
# Windows
"C:\\Windows\\Fonts\\msyh.ttc",
"C:\\Windows\\Fonts\\simsun.ttc",
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
return ImageFont.truetype(font_path, size)
except Exception:
continue
# 回退到默认字体
return ImageFont.load_default()
def generate_mock_design(
category_name: str,
sub_type_name: Optional[str],
color_name: Optional[str],
prompt: str,
save_path: str,
carving_technique: Optional[str] = None,
design_style: Optional[str] = None,
motif: Optional[str] = None,
size_spec: Optional[str] = None,
surface_finish: Optional[str] = None,
usage_scene: Optional[str] = None,
) -> str:
"""
生成 Mock 设计图
Args:
category_name: 品类名称
sub_type_name: 子类型名称(可选)
color_name: 颜色名称(可选)
prompt: 用户设计需求
save_path: 保存路径
Returns:
相对 URL 路径,如 /uploads/designs/1001.png
"""
# 确定背景色
if color_name and color_name in COLOR_MAP:
bg_color = COLOR_MAP[color_name]
elif color_name:
# 尝试直接使用颜色名(可能是十六进制)
bg_color = color_name if color_name.startswith("#") else DEFAULT_BG_COLOR
else:
bg_color = DEFAULT_BG_COLOR
# 创建图片
width, height = 800, 800
bg_rgb = hex_to_rgb(bg_color)
image = Image.new("RGB", (width, height), bg_rgb)
draw = ImageDraw.Draw(image)
# 获取文字颜色(与背景对比)
text_color = get_contrast_text_color(bg_color)
text_rgb = hex_to_rgb(text_color)
# 获取字体
title_font = get_font(48)
info_font = get_font(32)
prompt_font = get_font(28)
# 绘制标题
title = "玉宗设计"
draw.text((width // 2, 100), title, font=title_font, fill=text_rgb, anchor="mm")
# 绘制分隔线
line_y = 160
draw.line([(100, line_y), (700, line_y)], fill=text_rgb, width=2)
# 绘制品类信息
y_position = 220
info_lines = [f"品类: {category_name}"]
if sub_type_name:
info_lines.append(f"类型: {sub_type_name}")
if color_name:
info_lines.append(f"颜色: {color_name}")
if carving_technique:
info_lines.append(f"工艺: {carving_technique}")
if design_style:
info_lines.append(f"风格: {design_style}")
if motif:
info_lines.append(f"题材: {motif}")
if size_spec:
info_lines.append(f"尺寸: {size_spec}")
if surface_finish:
info_lines.append(f"表面: {surface_finish}")
if usage_scene:
info_lines.append(f"用途: {usage_scene}")
for line in info_lines:
draw.text((width // 2, y_position), line, font=info_font, fill=text_rgb, anchor="mm")
y_position += 50
# 绘制分隔线
y_position += 20
draw.line([(100, y_position), (700, y_position)], fill=text_rgb, width=1)
y_position += 40
# 绘制用户需求标题
draw.text((width // 2, y_position), "设计需求:", font=info_font, fill=text_rgb, anchor="mm")
y_position += 50
# 绘制用户需求文本(自动换行)
max_chars_per_line = 20
prompt_lines = []
current_line = ""
for char in prompt:
current_line += char
if len(current_line) >= max_chars_per_line:
prompt_lines.append(current_line)
current_line = ""
if current_line:
prompt_lines.append(current_line)
# 限制最多显示 5 行
for line in prompt_lines[:5]:
draw.text((width // 2, y_position), line, font=prompt_font, fill=text_rgb, anchor="mm")
y_position += 40
if len(prompt_lines) > 5:
draw.text((width // 2, y_position), "...", font=prompt_font, fill=text_rgb, anchor="mm")
# 绘制底部装饰
draw.rectangle([(50, 720), (750, 750)], outline=text_rgb, width=2)
draw.text((width // 2, 735), "AI Generated Mock Design", font=get_font(20), fill=text_rgb, anchor="mm")
# 确保目录存在
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# 保存图片
image.save(save_path, "PNG")
# 返回相对 URL 路径
# save_path 格式类似 uploads/designs/1001.png
# 需要转换为 /uploads/designs/1001.png
relative_path = save_path.replace("\\", "/")
if not relative_path.startswith("/"):
relative_path = "/" + relative_path
return relative_path