feat: production branch with deploy config for baota panel
- Add deploy/ directory with production env, supervisor, nginx, migration configs - Include all latest features: admin management, feedback, footer with ICP/beian - Database: foreign_trade (PostgreSQL), user: foreign_trade - Frontend: trade.yuzhiran.com, backend proxy via Nginx
This commit is contained in:
+253
-16
@@ -1,11 +1,12 @@
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, Any, List, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_
|
||||
from app.models.user import User
|
||||
from sqlalchemy import select, func, text, and_
|
||||
from app.models.user import User, Product
|
||||
from app.models.team import Team, TeamMember
|
||||
from app.models.analytics import UsageLog
|
||||
from app.models.customer import Customer
|
||||
from app.models.quotation import Quotation
|
||||
from app.models.system_config import SystemConfig
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
@@ -64,26 +65,36 @@ class AdminService:
|
||||
],
|
||||
}
|
||||
|
||||
async def list_users(self, page: int = 1, size: int = 20) -> Dict[str, Any]:
|
||||
query = select(User).order_by(User.created_at.desc()).offset((page - 1) * size).limit(size)
|
||||
def _user_to_dict(self, u: User) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": str(u.id),
|
||||
"username": u.username,
|
||||
"phone": u.phone,
|
||||
"email": u.email,
|
||||
"tier": u.tier,
|
||||
"role": u.role,
|
||||
"is_active": u.is_active,
|
||||
"last_login_at": u.last_login_at.isoformat() if u.last_login_at else None,
|
||||
"login_count": u.login_count or 0,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
"settings": u.settings,
|
||||
}
|
||||
|
||||
async def list_users(self, page: int = 1, size: int = 20, role: Optional[str] = None) -> Dict[str, Any]:
|
||||
base_query = select(User)
|
||||
count_query = select(func.count(User.id))
|
||||
if role:
|
||||
base_query = base_query.where(User.role == role)
|
||||
count_query = count_query.where(User.role == role)
|
||||
|
||||
query = base_query.order_by(User.created_at.desc()).offset((page - 1) * size).limit(size)
|
||||
|
||||
total = await self.db.execute(count_query)
|
||||
result = await self.db.execute(query)
|
||||
users = result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": str(u.id),
|
||||
"username": u.username,
|
||||
"phone": u.phone,
|
||||
"tier": u.tier,
|
||||
"is_active": u.is_active,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
}
|
||||
for u in users
|
||||
],
|
||||
"items": [self._user_to_dict(u) for u in users],
|
||||
"total": total.scalar(),
|
||||
"page": page,
|
||||
"size": size,
|
||||
@@ -107,6 +118,58 @@ class AdminService:
|
||||
await self.db.flush()
|
||||
return True
|
||||
|
||||
async def update_user_role(self, user_id: str, role: str) -> Optional[Dict[str, Any]]:
|
||||
if role not in ("user", "admin"):
|
||||
raise ValueError("Invalid role")
|
||||
result = await self.db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
return None
|
||||
user.role = role
|
||||
await self.db.flush()
|
||||
return self._user_to_dict(user)
|
||||
|
||||
async def get_user_detail(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
result = await self.db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
now = datetime.utcnow()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_start_date = today_start.date()
|
||||
|
||||
product_count = await self.db.execute(select(func.count()).select_from(User.__table__).where(User.id == user_id))
|
||||
|
||||
prod_count = await self.db.execute(
|
||||
select(func.count(Product.id)).where(Product.user_id == user_id)
|
||||
)
|
||||
cust_count = await self.db.execute(
|
||||
select(func.count(Customer.id)).where(Customer.user_id == user_id)
|
||||
)
|
||||
quot_count = await self.db.execute(
|
||||
select(func.count(Quotation.id)).where(Quotation.user_id == user_id)
|
||||
)
|
||||
usage_today = await self.db.execute(
|
||||
select(func.count(UsageLog.id)).where(
|
||||
UsageLog.user_id == user_id, UsageLog.created_at >= today_start
|
||||
)
|
||||
)
|
||||
usage_total = await self.db.execute(
|
||||
select(func.count(UsageLog.id)).where(UsageLog.user_id == user_id)
|
||||
)
|
||||
|
||||
return {
|
||||
**self._user_to_dict(user),
|
||||
"stats": {
|
||||
"products": prod_count.scalar() or 0,
|
||||
"customers": cust_count.scalar() or 0,
|
||||
"quotations": quot_count.scalar() or 0,
|
||||
"usage_today": usage_today.scalar() or 0,
|
||||
"usage_total": usage_total.scalar() or 0,
|
||||
},
|
||||
}
|
||||
|
||||
async def get_system_health(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"status": "healthy",
|
||||
@@ -127,3 +190,177 @@ class AdminService:
|
||||
await self.db.flush()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to log usage: {e}")
|
||||
|
||||
async def get_usage_stats(self) -> Dict[str, Any]:
|
||||
now = datetime.utcnow()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_ago = now - timedelta(days=7)
|
||||
|
||||
by_action = await self.db.execute(
|
||||
select(UsageLog.action, func.count(UsageLog.id))
|
||||
.where(UsageLog.created_at >= today_start)
|
||||
.group_by(UsageLog.action)
|
||||
.order_by(func.count(UsageLog.id).desc())
|
||||
)
|
||||
|
||||
daily_result = await self.db.execute(
|
||||
text(
|
||||
"SELECT date_trunc('day', created_at) AS day, count(id) "
|
||||
"FROM usage_logs WHERE created_at >= :week_ago "
|
||||
"GROUP BY date_trunc('day', created_at) "
|
||||
"ORDER BY date_trunc('day', created_at)"
|
||||
),
|
||||
{"week_ago": week_ago},
|
||||
)
|
||||
|
||||
today_logs = await self.db.execute(
|
||||
select(func.count(UsageLog.id)).where(UsageLog.created_at >= today_start)
|
||||
)
|
||||
|
||||
dau = await self.db.execute(
|
||||
select(func.count(func.distinct(UsageLog.user_id)))
|
||||
.where(UsageLog.created_at >= today_start)
|
||||
)
|
||||
|
||||
total_users = await self.db.execute(select(func.count(User.id)))
|
||||
|
||||
return {
|
||||
"today_total": today_logs.scalar() or 0,
|
||||
"dau": dau.scalar() or 0,
|
||||
"total_users": total_users.scalar() or 0,
|
||||
"by_action": {row[0]: row[1] for row in by_action.all()},
|
||||
"daily_trend": [
|
||||
{"date": str(row[0].date()), "count": row[1]}
|
||||
for row in daily_result.all()
|
||||
],
|
||||
}
|
||||
|
||||
async def get_logs(
|
||||
self,
|
||||
page: int = 1,
|
||||
size: int = 50,
|
||||
action: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None,
|
||||
) -> Dict[str, Any]:
|
||||
filters = []
|
||||
if action:
|
||||
filters.append(UsageLog.action == action)
|
||||
if user_id:
|
||||
filters.append(UsageLog.user_id == user_id)
|
||||
if date_from:
|
||||
filters.append(UsageLog.created_at >= date_from)
|
||||
if date_to:
|
||||
filters.append(UsageLog.created_at <= date_to)
|
||||
|
||||
count_query = select(func.count(UsageLog.id))
|
||||
data_query = select(UsageLog).order_by(UsageLog.created_at.desc())
|
||||
|
||||
if filters:
|
||||
where_clause = and_(*filters)
|
||||
count_query = count_query.where(where_clause)
|
||||
data_query = data_query.where(where_clause)
|
||||
|
||||
total = await self.db.execute(count_query)
|
||||
result = await self.db.execute(
|
||||
data_query.offset((page - 1) * size).limit(size)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": str(log.id),
|
||||
"user_id": str(log.user_id),
|
||||
"action": log.action,
|
||||
"detail": log.detail,
|
||||
"ip_address": log.ip_address,
|
||||
"user_agent": log.user_agent,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
"total": total.scalar(),
|
||||
"page": page,
|
||||
"size": size,
|
||||
}
|
||||
|
||||
async def _seed_default_configs(self):
|
||||
defaults = [
|
||||
SystemConfig(key="ai_provider_translate", value={"primary": "sensenova", "fallback": ["openai", "local"]}, description="翻译任务 AI 模型选择"),
|
||||
SystemConfig(key="ai_provider_reply", value={"primary": "sensenova", "fallback": ["anthropic", "local"]}, description="回复建议 AI 模型选择"),
|
||||
SystemConfig(key="ai_provider_marketing", value={"primary": "sensenova", "fallback": ["openai", "local"]}, description="营销文案 AI 模型选择"),
|
||||
SystemConfig(key="ai_provider_extract", value={"primary": "sensenova", "fallback": ["openai"]}, description="信息提取 AI 模型选择"),
|
||||
SystemConfig(key="ai_provider_quotation", value={"primary": "sensenova", "fallback": ["openai"]}, description="报价单 AI 模型选择"),
|
||||
SystemConfig(key="feature_guest_mode", value={"enabled": True}, description="游客模式开关"),
|
||||
SystemConfig(key="feature_wechat_login", value={"enabled": False}, description="微信登录开关"),
|
||||
SystemConfig(key="feature_registration", value={"enabled": True}, description="新用户注册开关"),
|
||||
SystemConfig(key="free_daily_limits", value={"translate_chars": 5000, "replies": 20, "marketing": 5, "customers": 5, "products": 1, "quotations": 3}, description="免费版每日配额"),
|
||||
SystemConfig(key="pro_daily_limits", value={"translate_chars": 50000, "replies": 200, "marketing": 50, "customers": 100, "products": 20, "quotations": 30}, description="Pro 版每日配额"),
|
||||
]
|
||||
for cfg in defaults:
|
||||
self.db.add(cfg)
|
||||
await self.db.flush()
|
||||
|
||||
async def list_config(self) -> List[Dict[str, Any]]:
|
||||
result = await self.db.execute(
|
||||
select(func.count(SystemConfig.id))
|
||||
)
|
||||
if result.scalar() == 0:
|
||||
await self._seed_default_configs()
|
||||
|
||||
result = await self.db.execute(
|
||||
select(SystemConfig).order_by(SystemConfig.key)
|
||||
)
|
||||
configs = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"key": c.key,
|
||||
"value": c.value,
|
||||
"description": c.description,
|
||||
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
||||
}
|
||||
for c in configs
|
||||
]
|
||||
|
||||
async def update_config(self, key: str, value: Any) -> Optional[Dict[str, Any]]:
|
||||
result = await self.db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key == key)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
if not config:
|
||||
return None
|
||||
config.value = value
|
||||
config.updated_at = datetime.utcnow()
|
||||
await self.db.flush()
|
||||
return {
|
||||
"key": config.key,
|
||||
"value": config.value,
|
||||
"description": config.description,
|
||||
"updated_at": config.updated_at.isoformat() if config.updated_at else None,
|
||||
}
|
||||
|
||||
async def search_users(self, query: str) -> List[Dict[str, Any]]:
|
||||
result = await self.db.execute(
|
||||
select(User)
|
||||
.where(
|
||||
(User.username.ilike(f"%{query}%")) | (User.phone.ilike(f"%{query}%"))
|
||||
)
|
||||
.order_by(User.created_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
users = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"id": str(u.id),
|
||||
"username": u.username,
|
||||
"phone": u.phone,
|
||||
"tier": u.tier,
|
||||
"role": u.role,
|
||||
"is_active": u.is_active,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
"settings": u.settings,
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user