From 03bc65e5511afabc118be0998ba810365b7f24d8 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 21 Mar 2025 11:33:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E5=A2=9E=E5=8A=A0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=AD=97=E6=AE=B5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/merchant.py | 158 ++++++++++++++++++++++++++++++++- app/api/endpoints/upload.py | 24 ++++- app/models/merchant.py | 28 +++++- app/models/merchant_auth.py | 46 ++++++++++ app/models/merchant_product.py | 46 +++++++++- app/sql/v1.1.sql | 37 ++++++++ jobs.sqlite | Bin 24576 -> 24576 bytes 7 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 app/models/merchant_auth.py create mode 100644 app/sql/v1.1.sql diff --git a/app/api/endpoints/merchant.py b/app/api/endpoints/merchant.py index a31da83..8b82adc 100644 --- a/app/api/endpoints/merchant.py +++ b/app/api/endpoints/merchant.py @@ -9,15 +9,165 @@ from app.models.merchant import ( MerchantInfo) from app.models.merchant_category import MerchantCategoryDB from app.models.database import get_db -from app.api.deps import get_admin_user +from app.api.deps import get_admin_user, get_current_user from app.models.user import UserDB from app.core.response import success_response, error_response, ResponseModel from app.models.merchant_pay_order import MerchantPayOrderDB from sqlalchemy.sql import func, desc from app.models.merchant_product import MerchantProductDB, ProductStatus - +from app.models.merchant import MerchantStatus, MerchantApply +from app.models.merchant_auth import MerchantAuthDB, MerchantAuthInfo router = APIRouter() +@router.get("/apply", response_model=ResponseModel) +async def get_merchant_apply( + db: Session = Depends(get_db), + user: UserDB = Depends(get_current_user) +): + """获取商家申请信息""" + merchant = db.query(MerchantDB).filter(MerchantDB.user_id == user.userid).first() + + if not merchant: + return error_response(code=404, message="商家申请信息不存在") + + auth = db.query(MerchantAuthDB).filter(MerchantAuthDB.merchant_id == merchant.id).first() + + return success_response(data={ + "merchant": MerchantInfo.model_validate(merchant), + "auth": MerchantAuthInfo.model_validate(auth) + }) + +@router.put("/apply", response_model=ResponseModel) +async def update_merchant_apply( + apply: MerchantApply, + db: Session = Depends(get_db), + user: UserDB = Depends(get_current_user) +): + """更新商家申请信息""" + merchant = db.query(MerchantDB).filter(MerchantDB.user_id == user.userid).first() + if not merchant: + return error_response(code=404, message="商家不存在") + + merchant.name = apply.name + merchant.business_hours = apply.business_hours + merchant.address = apply.address + merchant.longitude = apply.longitude + merchant.latitude = apply.latitude + merchant.phone = apply.phone + merchant.brand_image_url = apply.brand_image_url + merchant.category_id = apply.category_id + merchant.pay_share_rate = apply.pay_share_rate + + auth = db.query(MerchantAuthDB).filter(MerchantAuthDB.merchant_id == merchant.id).first() + if auth: + auth.license_image_url = apply.license_image_url + auth.id_front_url = apply.id_front_url + auth.id_back_url = apply.id_back_url + + db.commit() + db.refresh(merchant) + return success_response(data=MerchantInfo.model_validate(merchant)) + +@router.post("/apply", response_model=ResponseModel) +async def apply_merchant( + apply: MerchantApply, + db: Session = Depends(get_db), + user: UserDB = Depends(get_current_user) +): + """申请成为商家""" + + try: + # 创建商家 + merchant_apply = MerchantDB( + user_id=user.userid, + name=apply.name, + business_hours=apply.business_hours, + address=apply.address, + longitude=apply.longitude, + latitude=apply.latitude, + phone=apply.phone, + brand_image_url=apply.brand_image_url, + category_id=apply.category_id, + pay_share_rate=apply.pay_share_rate, + status=MerchantStatus.PENDING + ) + db.add(merchant_apply) + db.refresh(merchant_apply) + + # 创建商家认证信息 + auth = db.query(MerchantAuthDB).filter(MerchantAuthDB.merchant_id == merchant_apply.id).first() + if not auth: + merchant_auth = MerchantAuthDB( + merchant_id=merchant_apply.id, + license_image_url=apply.license_image_url, + id_front_url=apply.id_front_url, + id_back_url=apply.id_back_url + ) + else: + auth.license_image_url = apply.license_image_url + auth.id_front_url = apply.id_front_url + auth.id_back_url = apply.id_back_url + db.add(merchant_auth) + + db.commit() + return success_response() + except Exception as e: + db.rollback() + return error_response(code=500, message=f"申请失败: {str(e)}") + +@router.put("/{merchant_id}/approve", response_model=ResponseModel) +async def approve_merchant( + merchant_id: int, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """审核通过商家申请(管理员)""" + merchant = db.query(MerchantDB).filter(MerchantDB.id == merchant_id).first() + if not merchant: + return error_response(code=404, message="商家不存在") + + merchant.status = MerchantStatus.OFFLINE + + db.commit() + db.refresh(merchant) + return success_response(data=merchant) + +@router.put("/{merchant_id}/offline", response_model=ResponseModel) +async def offline_merchant( + merchant_id: int, + db: Session = Depends(get_db), + admin: UserDB = Depends(get_admin_user) +): + """下线商家(管理员)""" + merchant = db.query(MerchantDB).filter(MerchantDB.id == merchant_id).first() + if not merchant: + return error_response(code=404, message="商家不存在") + + merchant.status = MerchantStatus.OFFLINE + db.commit() + db.refresh(merchant) + return success_response(data=merchant) + +@router.get("/{merchant_id}/audit", response_model=ResponseModel) +async def get_merchant_audit( + merchant_id: int, + db: Session = Depends(get_db) +): + """获取商家审核信息""" + merchant = db.query(MerchantDB).filter(MerchantDB.id == merchant_id).first() + if not merchant: + return error_response(code=404, message="商家不存在") + + auth = db.query(MerchantAuthDB).filter(MerchantAuthDB.merchant_id == merchant_id).first() + if not auth: + return error_response(code=404, message="商家认证信息不存在") + + result = { + "merchant": MerchantInfo.model_validate(merchant), + "auth": MerchantAuthInfo.model_validate(auth) + } + return success_response(data=result) + @router.post("", response_model=ResponseModel) async def create_merchant( merchant: MerchantCreate, @@ -147,6 +297,7 @@ async def list_merchants( longitude: Optional[float] = None, latitude: Optional[float] = None, category_id: Optional[int] = None, + status: Optional[MerchantStatus] = None, skip: int = 0, limit: int = 20, db: Session = Depends(get_db) @@ -165,6 +316,9 @@ async def list_merchants( MerchantDB.user_id == UserDB.userid ) + if status is not None: + query = query.filter(MerchantDB.status == status) + # 添加分类过滤 if category_id is not None: query = query.filter(MerchantDB.category_id == category_id) diff --git a/app/api/endpoints/upload.py b/app/api/endpoints/upload.py index c5804cf..c5b1c93 100644 --- a/app/api/endpoints/upload.py +++ b/app/api/endpoints/upload.py @@ -9,16 +9,34 @@ from app.models.user import UserDB router = APIRouter() +@router.post("/upload_merchant_auth_image", response_model=ResponseModel) +async def upload_merchant_auth_image( + image: UploadFile = File(...), + current_user: UserDB = Depends(get_current_user) +): + """上传商家认证图片""" + if not image.content_type.startswith('image/'): + return error_response(code=400, message="只能上传图片文件") + + try: + folder = f"merchant_auth/{current_user.userid}" + url = await qcloud_manager.upload_file(image, folder) + return success_response(data=url) + except Exception as e: + return error_response(code=500, message=f"上传失败: {str(e)}") + @router.post("/image", response_model=ResponseModel) async def upload_image( file: UploadFile = File(...), + current_user: UserDB = Depends(get_current_user) ): """上传单张图片""" if not file.content_type.startswith('image/'): return error_response(code=400, message="只能上传图片文件") try: - url = await qcloud_manager.upload_file(file) + folder = f"uploads/{current_user.userid}" + url = await qcloud_manager.upload_file(file, folder) return success_response(data=UploadResponse(url=url)) except Exception as e: return error_response(code=500, message=f"上传失败: {str(e)}") @@ -26,6 +44,7 @@ async def upload_image( @router.post("/images", response_model=ResponseModel) async def upload_images( files: List[UploadFile] = File(...), + current_user: UserDB = Depends(get_current_user) ): """上传多张图片""" if len(files) > 5: @@ -36,7 +55,8 @@ async def upload_images( for file in files: if not file.content_type.startswith('image/'): return error_response(code=400, message="只能上传图片文件") - url = await qcloud_manager.upload_file(file) + folder = f"uploads/{current_user.userid}" + url = await qcloud_manager.upload_file(file, folder) urls.append(url) return success_response(data=MultiUploadResponse(urls=urls)) except Exception as e: diff --git a/app/models/merchant.py b/app/models/merchant.py index a6ee3a7..653df15 100644 --- a/app/models/merchant.py +++ b/app/models/merchant.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, String, Integer, Float, DateTime, JSON, ForeignKey +from sqlalchemy import Column, String, Integer, Float, DateTime, JSON, ForeignKey, Enum from sqlalchemy.dialects.mysql import DECIMAL from sqlalchemy.sql import func, select from pydantic import BaseModel, Field @@ -7,6 +7,13 @@ from datetime import datetime from .database import Base from app.core.utils import CommonUtils from app.core.imageprocessor import process_image, ImageFormat +import enum + +class MerchantStatus(str, enum.Enum): + """商家状态枚举""" + PENDING = "PENDING" # 申请中 + ONLINE = "ONLINE" # 已上线 + OFFLINE = "OFFLINE" # 已下线 # 数据库模型 class MerchantDB(Base): @@ -23,6 +30,7 @@ class MerchantDB(Base): brand_image_url = Column(String(200)) # 品牌图片地址 pay_gift_points_rate = Column(DECIMAL(4,2), nullable=False, default=0.00) # 支付赠送积分比例,默认0% pay_share_rate = Column(DECIMAL(4,2), nullable=False, default=0.00) # 在线买单分润比例,默认0% + status = Column(Enum(MerchantStatus), default=MerchantStatus.PENDING, nullable=False) # 商家状态 create_time = Column(DateTime(timezone=True), server_default=func.now()) update_time = Column(DateTime(timezone=True), onupdate=func.now()) category_id = Column(Integer, ForeignKey("merchant_categories.id"), nullable=True) @@ -43,6 +51,22 @@ class MerchantCreate(BaseModel): pay_share_rate: Optional[float] = Field(0.00, ge=0, le=100) # 在线买单分润比例 brand_image_url: Optional[str] = Field(None, max_length=200) category_id: Optional[int] = None + status: Optional[MerchantStatus] = Field(MerchantStatus.PENDING, description="商家状态") + +class MerchantApply(BaseModel): + name: str = Field(..., max_length=100) + business_hours: str = Field(..., max_length=100) + address: str = Field(..., max_length=200) + longitude: float = Field(..., ge=-180, le=180, description="经度") + latitude: float = Field(..., ge=-90, le=90, description="纬度") + phone: str = Field(..., max_length=20, pattern=r'^\d+$') + brand_image_url: str = Field(..., max_length=200) + category_id: int = Field(..., ge=0) + pay_share_rate: float = Field(0.00, ge=0, le=100) # 在线买单分润比例 + + license_image_url: str = Field(..., max_length=200) + id_front_url: str = Field(..., max_length=200) + id_back_url: str = Field(..., max_length=200) class MerchantUpdate(BaseModel): user_id: Optional[int] = None @@ -56,6 +80,7 @@ class MerchantUpdate(BaseModel): pay_share_rate: Optional[float] = Field(None, ge=0, le=100) # 在线买单分润比例 brand_image_url: Optional[str] = Field(None, max_length=200) category_id: Optional[int] = None + status: Optional[MerchantStatus] = None class MerchantInfo(BaseModel): id: int @@ -74,6 +99,7 @@ class MerchantInfo(BaseModel): pay_share_rate: float brand_image_url: Optional[str] = None optimized_brand_image_url: Optional[str] = None + status: MerchantStatus # 商家状态 create_time: datetime update_time: Optional[datetime] distance: Optional[int] = None # 距离(米) diff --git a/app/models/merchant_auth.py b/app/models/merchant_auth.py new file mode 100644 index 0000000..9f4d689 --- /dev/null +++ b/app/models/merchant_auth.py @@ -0,0 +1,46 @@ +from sqlalchemy import Column, String, Integer, DateTime, ForeignKey +from sqlalchemy.sql import func +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from .database import Base +from sqlalchemy.orm import relationship + +class MerchantAuthDB(Base): + """商家认证信息表""" + __tablename__ = "merchant_auths" + + id = Column(Integer, primary_key=True, autoincrement=True) + merchant_id = Column(Integer, ForeignKey("merchants.id"), nullable=False, unique=True) # 商家ID,一个商家只能有一条认证信息 + license_image_url = Column(String(200), nullable=False) # 营业执照图片URL + id_front_url = Column(String(200), nullable=False) # 身份证正面图片URL + id_back_url = Column(String(200), nullable=False) # 身份证背面图片URL + create_time = Column(DateTime(timezone=True), server_default=func.now()) + update_time = Column(DateTime(timezone=True), onupdate=func.now()) + + # 关联商家 + merchant = relationship("MerchantDB", backref="auth_info") + +# Pydantic 模型,用于API请求和响应 +class MerchantAuthCreate(BaseModel): + merchant_id: int = Field(..., description="商家ID") + license_image_url: str = Field(..., description="营业执照图片URL") + id_front_url: str = Field(..., description="身份证正面图片URL") + id_back_url: str = Field(..., description="身份证背面图片URL") + +class MerchantAuthUpdate(BaseModel): + license_image_url: Optional[str] = Field(None, description="营业执照图片URL") + id_front_url: Optional[str] = Field(None, description="身份证正面图片URL") + id_back_url: Optional[str] = Field(None, description="身份证背面图片URL") + +class MerchantAuthInfo(BaseModel): + id: int + merchant_id: int + license_image_url: str + id_front_url: str + id_back_url: str + create_time: datetime + update_time: Optional[datetime] = None + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/models/merchant_product.py b/app/models/merchant_product.py index 96d6403..29fa74b 100644 --- a/app/models/merchant_product.py +++ b/app/models/merchant_product.py @@ -1,9 +1,9 @@ -from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey, Enum, Boolean +from sqlalchemy import Column, String, Integer, Float, DateTime, ForeignKey, Enum, Boolean, Date, Text from sqlalchemy.dialects.mysql import DECIMAL from sqlalchemy.sql import func from pydantic import BaseModel, Field from typing import Optional, List -from datetime import datetime +from datetime import datetime, date from .database import Base import enum from app.core.utils import CommonUtils @@ -13,6 +13,16 @@ class ProductStatus(str, enum.Enum): LISTING = "LISTING" # 上架 UNLISTING = "UNLISTING" # 下架 +class DeliveryType(str, enum.Enum): + """配送类型枚举""" + DELIVERY = "DELIVERY" # 配送到家 + PICKUP = "PICKUP" # 自提 + +class DeliveryTimeType(str, enum.Enum): + """配送时间类型枚举""" + IMMEDIATE = "IMMEDIATE" # 立即送 + SCHEDULED = "SCHEDULED" # 定时送 + class MerchantProductDB(Base): __tablename__ = "merchant_products" @@ -27,6 +37,14 @@ class MerchantProductDB(Base): purchase_limit = Column(Integer, nullable=False, default=0) # 限购次数,0表示不限购 gift_points_rate = Column(DECIMAL(4,2), nullable=False, default=0.00) # 购买赠送积分比例,默认0% promotion_text = Column(String(100)) # 促销文本 + product_detail = Column(Text, nullable=True) # 产品详细描述,Markdown格式 + purchase_note = Column(Text, nullable=True) # 购买须知,用于提供商品购买相关注意事项 + qty = Column(Integer, nullable=False, default=0) # 库存 + is_sellout = Column(Boolean, nullable=False, default=False) # 是否售罄 + delivery_type = Column(Enum(DeliveryType), nullable=False, default=DeliveryType.DELIVERY) # 配送类型 + pickup_place = Column(String(200), nullable=True) # 自提点 + delivery_time_type = Column(Enum(DeliveryTimeType), nullable=False, default=DeliveryTimeType.IMMEDIATE) # 配送时间类型 + delivery_date = Column(Date, nullable=True) # 配送日期,仅对定时送有效 create_time = Column(DateTime(timezone=True), server_default=func.now()) update_time = Column(DateTime(timezone=True), onupdate=func.now()) status = Column(Enum(ProductStatus), nullable=False, default=ProductStatus.UNLISTING) @@ -45,6 +63,14 @@ class MerchantProductCreate(BaseModel): settlement_amount: float = Field(..., gt=0) tags: str = Field("", max_length=200) purchase_limit: int = Field(0, ge=0) # 限购次数,默认0表示不限购 + product_detail: Optional[str] = None # 产品详细描述,Markdown格式 + purchase_note: Optional[str] = None # 购买须知,用于提供商品购买相关注意事项 + qty: int = Field(0, ge=0) # 库存 + is_sellout: bool = Field(False) # 是否售罄 + delivery_type: DeliveryType = Field(DeliveryType.DELIVERY) # 配送类型 + pickup_place: Optional[str] = Field(None, max_length=200) # 自提点 + delivery_time_type: DeliveryTimeType = Field(DeliveryTimeType.IMMEDIATE) # 配送时间类型 + delivery_date: Optional[date] = Field(None) # 配送日期 status: ProductStatus = ProductStatus.UNLISTING promotion_text: Optional[str] = Field(None, max_length=100) # 促销文本 gift_points_rate: Optional[float] = Field(10.00, ge=0, le=100) # 购买赠送积分比例 @@ -57,6 +83,14 @@ class MerchantProductUpdate(BaseModel): settlement_amount: Optional[float] = Field(None, gt=0) tags: Optional[str] = Field(None, max_length=200) purchase_limit: Optional[int] = Field(None, ge=0) # 限购次数,可选字段 + product_detail: Optional[str] = None # 产品详细描述,Markdown格式 + purchase_note: Optional[str] = None # 购买须知,用于提供商品购买相关注意事项 + qty: Optional[int] = Field(None, ge=0) # 库存 + is_sellout: Optional[bool] = None # 是否售罄 + delivery_type: Optional[DeliveryType] = None # 配送类型 + pickup_place: Optional[str] = Field(None, max_length=200) # 自提点 + delivery_time_type: Optional[DeliveryTimeType] = None # 配送时间类型 + delivery_date: Optional[date] = None # 配送日期 status: Optional[ProductStatus] = None promotion_text: Optional[str] = Field(None, max_length=100) # 促销文本 gift_points_rate: Optional[float] = Field(None, ge=0, le=100) # 购买赠送积分比例 @@ -73,6 +107,14 @@ class MerchantProductInfo(BaseModel): settlement_amount: float tags: str purchase_limit: int # 限购次数 + product_detail: Optional[str] = None # 产品详细描述,Markdown格式 + purchase_note: Optional[str] = None # 购买须知,用于提供商品购买相关注意事项 + qty: int # 库存 + is_sellout: bool # 是否售罄 + delivery_type: DeliveryType # 配送类型 + pickup_place: Optional[str] = None # 自提点 + delivery_time_type: DeliveryTimeType # 配送时间类型 + delivery_date: Optional[date] = None # 配送日期 gift_points_rate: float promotion_text: Optional[str] = None # 促销文本 create_time: datetime diff --git a/app/sql/v1.1.sql b/app/sql/v1.1.sql new file mode 100644 index 0000000..bfeeaa4 --- /dev/null +++ b/app/sql/v1.1.sql @@ -0,0 +1,37 @@ +-- 1. 添加库存字段 +ALTER TABLE merchant_products +ADD COLUMN qty INT NOT NULL DEFAULT 0 COMMENT '库存数量'; + +-- 2. 添加是否售罄字段 +ALTER TABLE merchant_products +ADD COLUMN is_sellout TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否售罄,0-未售罄,1-已售罄'; + +-- 3. 添加配送类型字段(使用MySQL的ENUM类型) +ALTER TABLE merchant_products +ADD COLUMN delivery_type ENUM('DELIVERY', 'PICKUP') NOT NULL DEFAULT 'DELIVERY' COMMENT '配送类型,DELIVERY-配送到家,PICKUP-自提'; + +-- 4. 添加自提点字段 +ALTER TABLE merchant_products +ADD COLUMN pickup_place VARCHAR(200) NULL COMMENT '自提点'; + +-- 5. 添加配送时间类型字段(使用MySQL的ENUM类型) +ALTER TABLE merchant_products +ADD COLUMN delivery_time_type ENUM('IMMEDIATE', 'SCHEDULED') NOT NULL DEFAULT 'IMMEDIATE' COMMENT '配送时间类型,IMMEDIATE-立即送,SCHEDULED-定时送'; + +-- 6. 添加配送日期字段 +ALTER TABLE merchant_products +ADD COLUMN delivery_date DATE NULL COMMENT '配送日期,仅对定时送有效'; + +-- 7. 创建索引以提高查询效率 +CREATE INDEX idx_merchant_products_delivery_type ON merchant_products(delivery_type); +CREATE INDEX idx_merchant_products_delivery_time_type ON merchant_products(delivery_time_type); +CREATE INDEX idx_merchant_products_is_sellout ON merchant_products(is_sellout); + + +-- 为merchant_products表添加product_detail字段 +ALTER TABLE merchant_products +ADD COLUMN product_detail TEXT COMMENT '产品详细描述,Markdown格式'; + +-- 为merchant_products表添加purchase_note字段 +ALTER TABLE merchant_products +ADD COLUMN purchase_note TEXT COMMENT '购买须知,用于提供商品购买相关注意事项'; \ No newline at end of file diff --git a/jobs.sqlite b/jobs.sqlite index 37ab9d5caaa567c1c769a1604381561876d64cf1..553c7c91e7673f571eea36b5e69c04eed19c38ea 100644 GIT binary patch delta 96 zcmZoTz}Rqrae@>REB{0pCm^{o;jROZP+>Cm^{o;j