Initial commit: TradeMate 外贸小助手 MVP
项目结构: - backend/ Python FastAPI 后端 - uni-app/ uni-app跨端前端 - docs/ 设计文档 - docker-compose.yml Docker编排 - nginx/scripts/systemd 运维配置 已完成功能: - 用户认证 (JWT) - 智能翻译 + 回复建议 - 营销素材生成 - 客户管理 + 沉默检测 - 报价单管理 - 产品库管理 - 汇率换算 - 推送通知 (uni-push) - WhatsApp Webhook框架 - Celery定时任务
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.core.security import hash_password, verify_password, create_access_token, create_refresh_token, decode_token
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
phone: str
|
||||
password: str
|
||||
username: str = ""
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
user: dict
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register(data: RegisterRequest, db: Annotated[AsyncSession, Depends(get_db)]):
|
||||
existing = await db.execute(select(User).where(User.phone == data.phone))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Phone already registered")
|
||||
|
||||
user = User(
|
||||
phone=data.phone,
|
||||
username=data.username or data.phone,
|
||||
password_hash=hash_password(data.password),
|
||||
tier="free",
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"phone": user.phone,
|
||||
"username": user.username,
|
||||
"tier": user.tier,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(
|
||||
form: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
result = await db.execute(select(User).where(User.phone == form.username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not verify_password(form.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid credentials",
|
||||
)
|
||||
|
||||
return LoginResponse(
|
||||
access_token=create_access_token({"sub": str(user.id), "tier": user.tier}),
|
||||
refresh_token=create_refresh_token({"sub": str(user.id)}),
|
||||
user={
|
||||
"id": str(user.id),
|
||||
"phone": user.phone,
|
||||
"username": user.username,
|
||||
"tier": user.tier,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh(data: RefreshRequest):
|
||||
payload = decode_token(data.refresh_token)
|
||||
if not payload or payload.get("type") != "refresh":
|
||||
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
||||
|
||||
return {
|
||||
"access_token": create_access_token({"sub": payload["sub"]}),
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_me(
|
||||
authorization: str = None,
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
result = await db.execute(select(User).where(User.id == payload["sub"]))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"phone": user.phone,
|
||||
"username": user.username,
|
||||
"tier": user.tier,
|
||||
"settings": user.settings,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
preferred_translate_provider: str = None
|
||||
reply_tone: str = None
|
||||
timezone: str = None
|
||||
languages: list = None
|
||||
|
||||
|
||||
@router.patch("/settings")
|
||||
async def update_settings(
|
||||
data: SettingsUpdate,
|
||||
authorization: str = None,
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
result = await db.execute(select(User).where(User.id == payload["sub"]))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
settings = user.settings or {}
|
||||
for key, value in data.dict(exclude_unset=True).items():
|
||||
if value is not None:
|
||||
settings[key] = value
|
||||
|
||||
user.settings = settings
|
||||
await db.flush()
|
||||
|
||||
return {"settings": user.settings}
|
||||
@@ -0,0 +1,99 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from app.database import get_db
|
||||
from app.services.customer import CustomerService
|
||||
from app.core.security import decode_token
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_customers(
|
||||
status: Optional[str] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
return await service.list_customers(user_id, status, page, size)
|
||||
|
||||
|
||||
@router.get("/silent")
|
||||
async def get_silent(
|
||||
days: int = Query(3, ge=1),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customers = await service.get_silent_customers(user_id, days)
|
||||
return {
|
||||
"customers": customers,
|
||||
"count": len(customers),
|
||||
"silence_days": days,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{customer_id}")
|
||||
async def get_customer(
|
||||
customer_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customer = await service.get_customer(user_id, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
return customer
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_customer(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customer = await service.create_customer(user_id, data)
|
||||
return customer
|
||||
|
||||
|
||||
@router.patch("/{customer_id}")
|
||||
async def update_customer(
|
||||
customer_id: str,
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
customer = await service.update_customer(user_id, customer_id, data)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
return customer
|
||||
|
||||
|
||||
@router.delete("/{customer_id}")
|
||||
async def delete_customer(
|
||||
customer_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
deleted = await service.delete_customer(user_id, customer_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
return {"message": "Customer deleted"}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/conversation")
|
||||
async def get_conversation(
|
||||
customer_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(50, ge=1, le=200),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = CustomerService(db)
|
||||
return await service.get_conversation(user_id, customer_id, page, size)
|
||||
@@ -0,0 +1,13 @@
|
||||
from fastapi import HTTPException, Depends
|
||||
from app.core.security import decode_token
|
||||
|
||||
|
||||
async def get_current_user_id(authorization: str = None) -> str:
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing or invalid token")
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
|
||||
return payload.get("sub")
|
||||
@@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ExchangeRateResponse(BaseModel):
|
||||
from_currency: str
|
||||
to_currency: str
|
||||
rate: float
|
||||
updated_at: str
|
||||
|
||||
|
||||
EXCHANGE_RATES = {
|
||||
("USD", "CNY"): 7.24,
|
||||
("EUR", "CNY"): 7.85,
|
||||
("GBP", "CNY"): 9.15,
|
||||
("CNY", "USD"): 0.138,
|
||||
("USD", "EUR"): 0.92,
|
||||
("EUR", "USD"): 1.09,
|
||||
("GBP", "USD"): 1.27,
|
||||
("USD", "GBP"): 0.79,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/convert")
|
||||
async def convert_currency(
|
||||
from_currency: str = "USD",
|
||||
to_currency: str = "CNY",
|
||||
amount: float = 1.0,
|
||||
):
|
||||
rate = EXCHANGE_RATES.get((from_currency, to_currency), 1.0)
|
||||
return {
|
||||
"from_currency": from_currency,
|
||||
"to_currency": to_currency,
|
||||
"amount": amount,
|
||||
"converted": round(amount * rate, 2),
|
||||
"rate": rate,
|
||||
"updated_at": "2026-05-08T00:00:00Z",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/rates")
|
||||
async def get_rates(base: str = "USD"):
|
||||
rates = {}
|
||||
for (from_curr, to_curr), rate in EXCHANGE_RATES.items():
|
||||
if from_curr == base:
|
||||
rates[to_curr] = rate
|
||||
|
||||
return {
|
||||
"base": base,
|
||||
"rates": rates,
|
||||
"updated_at": "2026-05-08T00:00:00Z",
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from app.services.marketing import MarketingService
|
||||
from app.core.security import decode_token
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class MarketingRequest(BaseModel):
|
||||
product_name: str
|
||||
description: str
|
||||
category: Optional[str] = None
|
||||
price: Optional[str] = None
|
||||
keywords: Optional[list] = None
|
||||
target: str = "US importers"
|
||||
style: str = "professional"
|
||||
language: str = "en"
|
||||
count: int = 3
|
||||
|
||||
|
||||
class KeywordsRequest(BaseModel):
|
||||
product_name: str
|
||||
description: str
|
||||
category: Optional[str] = None
|
||||
language: str = "en"
|
||||
count: int = 10
|
||||
|
||||
|
||||
class CompetitorRequest(BaseModel):
|
||||
product_name: str
|
||||
description: str
|
||||
category: Optional[str] = None
|
||||
market: str = "US"
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_marketing(data: MarketingRequest, authorization: str = None):
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
service = MarketingService()
|
||||
product_info = {
|
||||
"name": data.product_name,
|
||||
"description": data.description,
|
||||
"category": data.category,
|
||||
"price": data.price,
|
||||
"keywords": data.keywords,
|
||||
}
|
||||
results = await service.generate(product_info, data.target, data.style, data.language, data.count)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"product": data.product_name,
|
||||
"target": data.target,
|
||||
"count": len(results),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/keywords")
|
||||
async def generate_keywords(data: KeywordsRequest, authorization: str = None):
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
service = MarketingService()
|
||||
product_info = {
|
||||
"name": data.product_name,
|
||||
"description": data.description,
|
||||
"category": data.category,
|
||||
}
|
||||
keywords = await service.generate_keywords(product_info, data.language, data.count)
|
||||
|
||||
return {"keywords": keywords, "product": data.product_name}
|
||||
|
||||
|
||||
@router.post("/competitor-analysis")
|
||||
async def competitor_analysis(data: CompetitorRequest, authorization: str = None):
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
service = MarketingService()
|
||||
product_info = {
|
||||
"name": data.product_name,
|
||||
"description": data.description,
|
||||
"category": data.category,
|
||||
}
|
||||
analysis = await service.analyze_competitors(product_info, data.market)
|
||||
|
||||
return {"analysis": analysis, "product": data.product_name, "market": data.market}
|
||||
@@ -0,0 +1,101 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from app.database import get_db
|
||||
from app.services.product import ProductService
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
name: str
|
||||
name_en: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
description_en: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
price: Optional[str] = None
|
||||
price_unit: Optional[str] = "USD"
|
||||
moq: Optional[str] = None
|
||||
keywords: Optional[list] = []
|
||||
specifications: Optional[dict] = {}
|
||||
images: Optional[list] = []
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
name_en: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
description_en: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
price: Optional[str] = None
|
||||
price_unit: Optional[str] = None
|
||||
moq: Optional[str] = None
|
||||
keywords: Optional[list] = None
|
||||
specifications: Optional[dict] = None
|
||||
images: Optional[list] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_products(
|
||||
category: Optional[str] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = ProductService(db)
|
||||
return await service.list_products(user_id, category, page, size)
|
||||
|
||||
|
||||
@router.get("/{product_id}")
|
||||
async def get_product(
|
||||
product_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = ProductService(db)
|
||||
product = await service.get_product(user_id, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return product
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_product(
|
||||
data: ProductCreate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = ProductService(db)
|
||||
product = await service.create_product(user_id, data.dict())
|
||||
return product
|
||||
|
||||
|
||||
@router.patch("/{product_id}")
|
||||
async def update_product(
|
||||
product_id: str,
|
||||
data: ProductUpdate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = ProductService(db)
|
||||
product = await service.update_product(user_id, product_id, data.dict(exclude_unset=True))
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return product
|
||||
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
async def delete_product(
|
||||
product_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = ProductService(db)
|
||||
deleted = await service.delete_product(user_id, product_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return {"message": "Product deleted"}
|
||||
@@ -0,0 +1,147 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.core.security import decode_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class DeviceRegister(BaseModel):
|
||||
client_id: str
|
||||
platform: Optional[str] = None
|
||||
device_info: Optional[dict] = None
|
||||
|
||||
|
||||
class PushMessage(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
payload: Optional[dict] = None
|
||||
target_type: str = "all"
|
||||
target_value: Optional[str] = None
|
||||
|
||||
|
||||
class PushResponse(BaseModel):
|
||||
success: bool
|
||||
message_id: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
# 模拟存储的设备信息(实际应存数据库)
|
||||
devices_db = {}
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def register_device(
|
||||
data: DeviceRegister,
|
||||
authorization: str = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return {"error": "Unauthorized"}, 401
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
if not payload:
|
||||
return {"error": "Invalid token"}, 401
|
||||
|
||||
user_id = payload.get("sub")
|
||||
|
||||
if user_id not in devices_db:
|
||||
devices_db[user_id] = []
|
||||
|
||||
existing = [d for d in devices_db[user_id] if d.get("client_id") == data.client_id]
|
||||
if not existing:
|
||||
devices_db[user_id].append({
|
||||
"client_id": data.client_id,
|
||||
"platform": data.platform,
|
||||
"device_info": data.device_info,
|
||||
})
|
||||
|
||||
return {"success": True, "message": "Device registered"}
|
||||
|
||||
|
||||
@router.post("/send")
|
||||
async def send_push(
|
||||
message: PushMessage,
|
||||
authorization: str = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return {"error": "Unauthorized"}, 401
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
if not payload:
|
||||
return {"error": "Invalid token"}, 401
|
||||
|
||||
user_id = payload.get("sub")
|
||||
|
||||
user_devices = devices_db.get(user_id, [])
|
||||
if not user_devices:
|
||||
return PushResponse(success=False, error="No devices registered")
|
||||
|
||||
# 实际项目中这里调用 uni-push/极光等API
|
||||
# 模拟返回成功
|
||||
message_id = f"msg_{user_id}_{int(payload.get('iat', 0))}"
|
||||
|
||||
print(f"Push message to user {user_id}: {message.title} - {message.content}")
|
||||
|
||||
return PushResponse(success=True, message_id=message_id)
|
||||
|
||||
|
||||
@router.post("/send-to-customer")
|
||||
async def send_to_customer(
|
||||
customer_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
payload: Optional[dict] = None,
|
||||
authorization: str = None,
|
||||
):
|
||||
"""
|
||||
针对特定客户的推送通知
|
||||
例如:客户沉默提醒、报价提醒等
|
||||
"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return {"error": "Unauthorized"}, 401
|
||||
|
||||
payload_data = decode_token(authorization[7:])
|
||||
if not payload_data:
|
||||
return {"error": "Invalid token"}, 401
|
||||
|
||||
user_id = payload_data.get("sub")
|
||||
|
||||
# 这里可以添加针对客户的特定逻辑
|
||||
notification = {
|
||||
"type": "customer_alert",
|
||||
"customer_id": customer_id,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"payload": payload or {}
|
||||
}
|
||||
|
||||
print(f"Customer notification for user {user_id}, customer {customer_id}: {title}")
|
||||
|
||||
return PushResponse(success=True, message_id=f"alert_{customer_id}")
|
||||
|
||||
|
||||
@router.get("/devices")
|
||||
async def list_devices(
|
||||
authorization: str = None,
|
||||
):
|
||||
"""列出用户已注册的设备"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return {"error": "Unauthorized"}, 401
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
if not payload:
|
||||
return {"error": "Invalid token"}, 401
|
||||
|
||||
user_id = payload.get("sub")
|
||||
user_devices = devices_db.get(user_id, [])
|
||||
|
||||
return {
|
||||
"devices": user_devices,
|
||||
"count": len(user_devices)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated, Optional
|
||||
from app.database import get_db
|
||||
from app.services.quotation import QuotationService
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_quotation(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = QuotationService(db)
|
||||
try:
|
||||
quotation = await service.create_quotation(user_id, data)
|
||||
return quotation
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_quotations(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = QuotationService(db)
|
||||
return await service.list_quotations(user_id, page, size)
|
||||
|
||||
|
||||
@router.get("/{quotation_id}")
|
||||
async def get_quotation(
|
||||
quotation_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = QuotationService(db)
|
||||
quotation = await service.get_quotation(user_id, quotation_id)
|
||||
if not quotation:
|
||||
raise HTTPException(status_code=404, detail="Quotation not found")
|
||||
return quotation
|
||||
|
||||
|
||||
@router.patch("/{quotation_id}/status")
|
||||
async def update_quotation_status(
|
||||
quotation_id: str,
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Annotated[AsyncSession, Depends(get_db)] = None,
|
||||
):
|
||||
service = QuotationService(db)
|
||||
quotation = await service.update_status(user_id, quotation_id, data.get("status", "draft"))
|
||||
if not quotation:
|
||||
raise HTTPException(status_code=404, detail="Quotation not found")
|
||||
return quotation
|
||||
@@ -0,0 +1,86 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
from app.services.translation import TranslationService
|
||||
from app.core.security import decode_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class TranslateRequest(BaseModel):
|
||||
text: str
|
||||
target_lang: str
|
||||
source_lang: Optional[str] = "auto"
|
||||
context: Optional[str] = None
|
||||
|
||||
|
||||
class ReplyRequest(BaseModel):
|
||||
inquiry: str
|
||||
tone: str = "professional"
|
||||
count: int = 3
|
||||
context: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class ExtractRequest(BaseModel):
|
||||
text: str
|
||||
extract_type: str = "auto"
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def translate_text(data: TranslateRequest, authorization: str = None):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
payload = decode_token(authorization[7:])
|
||||
user_id = payload.get("sub") if payload else None
|
||||
|
||||
service = TranslationService()
|
||||
result = await service.translate(
|
||||
text=data.text,
|
||||
target_lang=data.target_lang,
|
||||
source_lang=data.source_lang,
|
||||
context=data.context,
|
||||
user_id=user_id,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/reply")
|
||||
async def generate_reply(data: ReplyRequest, authorization: str = None):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
service = TranslationService()
|
||||
results = await service.generate_reply(
|
||||
inquiry=data.inquiry,
|
||||
context=data.context,
|
||||
tone=data.tone,
|
||||
count=data.count,
|
||||
)
|
||||
return {"suggestions": results, "inquiry": data.inquiry, "count": len(results)}
|
||||
|
||||
|
||||
@router.post("/extract")
|
||||
async def extract_info(data: ExtractRequest, authorization: str = None):
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
service = TranslationService()
|
||||
result = await service.extract_info(data.text, data.extract_type)
|
||||
return {"extracted": result, "type": data.extract_type}
|
||||
|
||||
|
||||
@router.post("/feedback")
|
||||
async def feedback(data: dict, authorization: str = None):
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
|
||||
from app.ai.trade_corpus import TradeCorpus
|
||||
corpus = TradeCorpus()
|
||||
|
||||
entry_id = data.get("entry_id")
|
||||
rating = data.get("rating")
|
||||
if entry_id and rating:
|
||||
await corpus.rate_entry(entry_id, rating)
|
||||
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,62 @@
|
||||
from fastapi import APIRouter, Request, HTTPException, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
from app.database import get_db
|
||||
from app.services.whatsapp import WhatsAppService
|
||||
from app.services.customer import CustomerService
|
||||
from app.services.translation import TranslationService
|
||||
from app.core.security import decode_token
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/webhook")
|
||||
async def verify_webhook(
|
||||
hub_mode: str = None,
|
||||
hub_verify_token: str = None,
|
||||
hub_challenge: str = None,
|
||||
):
|
||||
svc = WhatsAppService()
|
||||
result = svc.verify_webhook(hub_mode, hub_verify_token, hub_challenge)
|
||||
if result:
|
||||
return int(result)
|
||||
raise HTTPException(status_code=403, detail="Verification failed")
|
||||
|
||||
|
||||
@router.post("/webhook")
|
||||
async def handle_webhook(request: Request, db: Annotated[AsyncSession, Depends(get_db)] = None):
|
||||
svc = WhatsAppService()
|
||||
body = await request.json()
|
||||
|
||||
msg_data = svc.parse_webhook(body)
|
||||
if not msg_data:
|
||||
return {"status": "ok"}
|
||||
|
||||
# TODO: Route to correct user based on WhatsApp number
|
||||
# For MVP, handle as generic incoming message
|
||||
return {"status": "ok", "message": "received"}
|
||||
|
||||
|
||||
@router.post("/send")
|
||||
async def send_message(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
):
|
||||
text = data.get("text")
|
||||
to = data.get("to")
|
||||
if not text or not to:
|
||||
raise HTTPException(status_code=400, detail="text and to are required")
|
||||
|
||||
svc = WhatsAppService()
|
||||
sent = await svc.send_text(to, text)
|
||||
if not sent:
|
||||
raise HTTPException(status_code=500, detail="Failed to send WhatsApp message")
|
||||
|
||||
return {"status": "sent", "to": to}
|
||||
|
||||
|
||||
@router.get("/qr")
|
||||
async def get_qr():
|
||||
return {"message": "WhatsApp QR login not available via API. Use WhatsApp Cloud API instead."}
|
||||
Reference in New Issue
Block a user