feat: Add base project structure with API and web components
This commit is contained in:
444
api/app/services/release_share_service.py
Normal file
444
api/app/services/release_share_service.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user