feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

View File

@@ -0,0 +1,444 @@
import uuid
from typing import Optional, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.models import ReleaseShare, AppRelease, App, AgentConfig
from app.repositories.release_share_repository import ReleaseShareRepository
from app.core.share_utils import (
generate_share_token,
hash_password,
verify_password,
build_share_url,
generate_embed_code
)
from app.core.exceptions import ResourceNotFoundException, BusinessException
from app.core.error_codes import BizCode
from app.core.logging_config import get_business_logger
from app.schemas import release_share_schema
logger = get_business_logger()
class ReleaseShareService:
"""发布版本分享服务"""
def __init__(self, db: Session):
self.db = db
self.repo = ReleaseShareRepository(db)
def create_or_update_share(
self,
release_id: uuid.UUID,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
data: release_share_schema.ReleaseShareCreate,
base_url: Optional[str] = None
) -> ReleaseShare:
"""创建或更新分享配置
Args:
release_id: 发布版本 ID
user_id: 用户 ID
workspace_id: 工作空间 ID
data: 分享配置数据
base_url: 基础 URL用于生成完整的分享链接
Returns:
分享配置
"""
# 验证发布版本存在且属于该工作空间
release = self._get_release_or_404(release_id)
self._validate_release_access(release, workspace_id)
# 检查是否已存在分享配置
existing_share = self.repo.get_by_release_id(release_id)
if existing_share:
# 更新现有配置
return self._update_share_internal(existing_share, data)
else:
# 创建新配置
return self._create_share_internal(release, user_id, data)
def _create_share_internal(
self,
release: AppRelease,
user_id: uuid.UUID,
data: release_share_schema.ReleaseShareCreate
) -> ReleaseShare:
"""内部方法:创建分享配置"""
# 生成唯一的 share_token
share_token = self._generate_unique_token()
# 处理密码
password_hash = None
if data.require_password and data.password:
password_hash = hash_password(data.password)
# 创建分享配置
share = ReleaseShare(
release_id=release.id,
app_id=release.app_id,
is_enabled=data.is_enabled,
share_token=share_token,
require_password=data.require_password,
password_hash=password_hash,
allow_embed=data.allow_embed,
embed_domains=data.embed_domains or [],
created_by=user_id
)
share = self.repo.create(share)
logger.info(
f"创建分享配置",
extra={
"share_id": str(share.id),
"release_id": str(release.id),
"app_id": str(release.app_id),
"share_token": share_token
}
)
return share
def _update_share_internal(
self,
share: ReleaseShare,
data: release_share_schema.ReleaseShareUpdate
) -> ReleaseShare:
"""内部方法:更新分享配置"""
if data.is_enabled is not None:
share.is_enabled = data.is_enabled
if data.require_password is not None:
share.require_password = data.require_password
if data.password is not None:
if data.password:
share.password_hash = hash_password(data.password)
else:
share.password_hash = None
if data.allow_embed is not None:
share.allow_embed = data.allow_embed
if data.embed_domains is not None:
share.embed_domains = data.embed_domains or []
share = self.repo.update(share)
logger.info(
f"更新分享配置",
extra={
"share_id": str(share.id),
"release_id": str(share.release_id)
}
)
return share
def update_share(
self,
release_id: uuid.UUID,
workspace_id: uuid.UUID,
data: release_share_schema.ReleaseShareUpdate
) -> ReleaseShare:
"""更新分享配置
Args:
release_id: 发布版本 ID
workspace_id: 工作空间 ID
data: 更新数据
Returns:
更新后的分享配置
"""
# 验证发布版本
release = self._get_release_or_404(release_id)
self._validate_release_access(release, workspace_id)
# 获取分享配置
share = self.repo.get_by_release_id(release_id)
if not share:
raise ResourceNotFoundException("分享配置", str(release_id))
return self._update_share_internal(share, data)
def get_share(
self,
release_id: uuid.UUID,
workspace_id: uuid.UUID,
base_url: Optional[str] = None
) -> Optional[release_share_schema.ReleaseShare]:
"""获取分享配置
Args:
release_id: 发布版本 ID
workspace_id: 工作空间 ID
base_url: 基础 URL
Returns:
分享配置 Schema
"""
# 验证发布版本
release = self._get_release_or_404(release_id)
self._validate_release_access(release, workspace_id)
share = self.repo.get_by_release_id(release_id)
if not share:
return None
return self._convert_to_schema(share, base_url)
def delete_share(
self,
release_id: uuid.UUID,
workspace_id: uuid.UUID
) -> None:
"""删除(禁用)分享配置
Args:
release_id: 发布版本 ID
workspace_id: 工作空间 ID
"""
# 验证发布版本
release = self._get_release_or_404(release_id)
self._validate_release_access(release, workspace_id)
share = self.repo.get_by_release_id(release_id)
if not share:
raise ResourceNotFoundException("分享配置", str(release_id))
self.repo.delete(share)
logger.info(
f"删除分享配置",
extra={
"share_id": str(share.id),
"release_id": str(release_id)
}
)
def regenerate_token(
self,
release_id: uuid.UUID,
workspace_id: uuid.UUID
) -> ReleaseShare:
"""重新生成分享 token旧链接失效
Args:
release_id: 发布版本 ID
workspace_id: 工作空间 ID
Returns:
更新后的分享配置
"""
# 验证发布版本
release = self._get_release_or_404(release_id)
self._validate_release_access(release, workspace_id)
share = self.repo.get_by_release_id(release_id)
if not share:
raise ResourceNotFoundException("分享配置", str(release_id))
# 生成新 token
old_token = share.share_token
share.share_token = self._generate_unique_token()
share = self.repo.update(share)
logger.info(
f"重新生成分享 token",
extra={
"share_id": str(share.id),
"old_token": old_token,
"new_token": share.share_token
}
)
return share
def get_shared_release_info(
self,
share_token: str,
password: Optional[str] = None
) -> release_share_schema.SharedReleaseInfo:
"""获取公开分享的发布版本信息
Args:
share_token: 分享 token
password: 访问密码(如果需要)
Returns:
分享的发布版本信息
"""
# 获取分享配置
share = self.repo.get_by_share_token(share_token)
if not share:
raise ResourceNotFoundException("分享链接", share_token)
# 检查是否启用
if not share.is_enabled:
raise BusinessException("该分享链接已禁用", BizCode.SHARE_DISABLED)
# 验证密码
is_password_verified = False
if share.require_password:
if not password:
# 需要密码但未提供,返回基本信息
release = self.db.get(AppRelease, share.release_id)
return release_share_schema.SharedReleaseInfo(
app_name=release.name,
app_description=release.description,
app_icon=release.icon,
app_type=release.type,
version=release.version,
release_notes=release.release_notes,
published_at=int(release.published_at.timestamp() * 1000),
config={},
require_password=True,
is_password_verified=False,
allow_embed=share.allow_embed
)
# 验证密码
if not share.password_hash or not verify_password(password, share.password_hash):
raise BusinessException("密码错误", BizCode.INVALID_PASSWORD)
is_password_verified = True
# 获取发布版本详细信息
release = self.db.get(AppRelease, share.release_id)
if not release:
raise ResourceNotFoundException("发布版本", str(share.release_id))
# 异步更新访问统计(不阻塞响应)
try:
self.repo.increment_view_count(share.id)
except Exception as e:
logger.warning(f"更新访问统计失败: {str(e)}")
# 返回完整信息
return release_share_schema.SharedReleaseInfo(
app_name=release.name,
app_description=release.description,
app_icon=release.icon,
app_type=release.type,
version=release.version,
release_notes=release.release_notes,
published_at=int(release.published_at.timestamp() * 1000),
config=release.config or {},
require_password=share.require_password,
is_password_verified=is_password_verified,
allow_embed=share.allow_embed
)
def verify_password(
self,
share_token: str,
password: str
) -> bool:
"""验证分享密码
Args:
share_token: 分享 token
password: 密码
Returns:
是否验证成功
"""
share = self.repo.get_by_share_token(share_token)
if not share:
raise ResourceNotFoundException("分享链接", share_token)
if not share.is_enabled:
raise BusinessException("该分享链接已禁用", BizCode.SHARE_DISABLED)
if not share.require_password:
return True
if not share.password_hash:
return False
return verify_password(password, share.password_hash)
def get_embed_code(
self,
share_token: str,
width: str = "100%",
height: str = "600px",
base_url: Optional[str] = None
) -> release_share_schema.EmbedCode:
"""获取嵌入代码
Args:
share_token: 分享 token
width: 宽度
height: 高度
base_url: 基础 URL
Returns:
嵌入代码
"""
share = self.repo.get_by_share_token(share_token)
if not share:
raise ResourceNotFoundException("分享链接", share_token)
if not share.is_enabled:
raise BusinessException("该分享链接已禁用", BizCode.SHARE_DISABLED)
if not share.allow_embed:
raise BusinessException("该分享不允许嵌入", BizCode.EMBED_NOT_ALLOWED)
embed_data = generate_embed_code(share_token, width, height, base_url)
return release_share_schema.EmbedCode(**embed_data)
def _generate_unique_token(self, max_attempts: int = 10) -> str:
"""生成唯一的分享 token"""
for _ in range(max_attempts):
token = generate_share_token()
if not self.repo.token_exists(token):
return token
raise BusinessException("生成唯一 token 失败,请重试", BizCode.INTERNAL_ERROR)
def _get_release_or_404(self, release_id: uuid.UUID) -> AppRelease:
"""获取发布版本或抛出 404"""
release = self.db.get(AppRelease, release_id)
if not release:
raise ResourceNotFoundException("发布版本", str(release_id))
return release
def _validate_release_access(self, release: AppRelease, workspace_id: uuid.UUID) -> None:
"""验证发布版本访问权限"""
app = self.db.get(App, release.app_id)
if not app:
raise ResourceNotFoundException("应用", str(release.app_id))
if app.workspace_id != workspace_id:
raise BusinessException("无权访问该发布版本", BizCode.PERMISSION_DENIED)
def _convert_to_schema(
self,
share: ReleaseShare,
base_url: Optional[str] = None
) -> release_share_schema.ReleaseShare:
"""转换为 Schema"""
share_url = build_share_url(share.share_token, base_url)
return release_share_schema.ReleaseShare(
id=share.id,
release_id=share.release_id,
app_id=share.app_id,
is_enabled=share.is_enabled,
share_token=share.share_token,
share_url=share_url,
require_password=share.require_password,
allow_embed=share.allow_embed,
embed_domains=share.embed_domains or [],
view_count=share.view_count,
last_accessed_at=share.last_accessed_at,
created_at=share.created_at,
updated_at=share.updated_at
)