Files
MemoryBear/api/app/services/release_share_service.py
2025-12-15 14:09:43 +08:00

445 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
"创建分享配置",
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(
"更新分享配置",
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(
"删除分享配置",
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(
"重新生成分享 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
)