diff --git a/.env.example b/.env.example index c6a746a..16affb9 100644 --- a/.env.example +++ b/.env.example @@ -81,3 +81,15 @@ SOCIAL_LLM__PROVIDER_KEYS__MOONSHOT= SOCIAL_LLM__PROVIDER_KEYS__DEEPSEEK= SOCIAL_LLM__PROVIDER_KEYS__ARK= SOCIAL_LLM__PROVIDER_KEYS__ZAI= + +############ +# App 版本更新配置 +############ +# 安装包目录,相对于项目根目录下的 deploy/static/ +SOCIAL_APP_VERSION__RELEASES_DIR=releases +# 当前版本号(语义化版本) +SOCIAL_APP_VERSION__CURRENT_VERSION=0.1.0 +# 当前构建号(整数,每次打包递增) +SOCIAL_APP_VERSION__CURRENT_BUILD=1 +# 下载链接基础域名(生产环境需配置) +SOCIAL_APP_VERSION__DOWNLOAD_BASE_URL= diff --git a/apps/lib/core/constants/app_constants.dart b/apps/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..4997721 --- /dev/null +++ b/apps/lib/core/constants/app_constants.dart @@ -0,0 +1,13 @@ +import 'package:package_info_plus/package_info_plus.dart'; + +class AppConstants { + static String version = '0.1.0'; + static int build = 1; + + static Future init() async { + final info = await PackageInfo.fromPlatform(); + version = info.version; + final buildStr = info.buildNumber.isEmpty ? '1' : info.buildNumber; + build = int.tryParse(buildStr) ?? 1; + } +} diff --git a/apps/lib/core/di/injection.dart b/apps/lib/core/di/injection.dart index ec8babf..21113ce 100644 --- a/apps/lib/core/di/injection.dart +++ b/apps/lib/core/di/injection.dart @@ -15,6 +15,7 @@ import '../../features/calendar/data/services/calendar_service.dart'; import '../../features/calendar/ui/calendar_state_manager.dart'; import '../../features/friends/data/friends_api.dart'; import '../../features/messages/data/inbox_api.dart'; +import '../../features/settings/data/settings_api.dart'; import '../../features/users/data/users_api.dart'; import '../../features/todo/data/todo_api.dart'; @@ -55,6 +56,9 @@ Future configureDependencies() async { final friendsApi = FriendsApi(apiClient); sl.registerSingleton(friendsApi); + final settingsApi = SettingsApi(apiClient); + sl.registerSingleton(settingsApi); + final inboxApi = InboxApi(apiClient); sl.registerSingleton(inboxApi); diff --git a/apps/lib/features/settings/data/settings_api.dart b/apps/lib/features/settings/data/settings_api.dart new file mode 100644 index 0000000..4de5dbe --- /dev/null +++ b/apps/lib/features/settings/data/settings_api.dart @@ -0,0 +1,59 @@ +import 'package:social_app/core/api/i_api_client.dart'; + +class AppVersionResponse { + final bool hasUpdate; + final String latestVersion; + final int latestBuild; + final String minRequiredVersion; + final String updateType; + final String? downloadUrl; + final String? releaseNotes; + + AppVersionResponse({ + required this.hasUpdate, + required this.latestVersion, + required this.latestBuild, + required this.minRequiredVersion, + required this.updateType, + this.downloadUrl, + this.releaseNotes, + }); + + factory AppVersionResponse.fromJson(Map json) { + return AppVersionResponse( + hasUpdate: json['has_update'] as bool, + latestVersion: json['latest_version'] as String, + latestBuild: json['latest_build'] as int, + minRequiredVersion: json['min_required_version'] as String, + updateType: json['update_type'] as String, + downloadUrl: json['download_url'] as String?, + releaseNotes: json['release_notes'] as String?, + ); + } +} + +class SettingsApi { + final IApiClient _client; + static const _prefix = '/api/v1/app'; + + SettingsApi(this._client); + + Future checkUpdates({ + required int currentBuild, + String? currentVersion, + String platform = 'android', + }) async { + final params = { + 'platform': platform, + 'current_build': currentBuild.toString(), + }; + if (currentVersion != null) { + params['current_version'] = currentVersion; + } + final queryString = params.entries + .map((e) => '${e.key}=${e.value}') + .join('&'); + final response = await _client.get('$_prefix/check-updates?$queryString'); + return AppVersionResponse.fromJson(response.data); + } +} diff --git a/apps/lib/features/settings/ui/screens/settings_screen.dart b/apps/lib/features/settings/ui/screens/settings_screen.dart index 99fdf81..a8dc4f3 100644 --- a/apps/lib/features/settings/ui/screens/settings_screen.dart +++ b/apps/lib/features/settings/ui/screens/settings_screen.dart @@ -1,11 +1,16 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import '../../../../core/di/injection.dart'; -import '../../../../core/theme/design_tokens.dart'; -import '../../../../shared/widgets/page_header.dart' as widgets; -import '../../../friends/data/friends_api.dart'; -import '../../../users/data/models/user_response.dart'; -import '../../../users/data/users_api.dart'; +import 'package:social_app/core/constants/app_constants.dart'; +import 'package:social_app/core/di/injection.dart'; +import 'package:social_app/core/theme/design_tokens.dart'; +import 'package:social_app/shared/widgets/app_loading_indicator.dart'; +import 'package:social_app/shared/widgets/page_header.dart' as widgets; +import 'package:social_app/shared/widgets/toast/toast.dart'; +import 'package:social_app/shared/widgets/toast/toast_type.dart'; +import 'package:social_app/features/friends/data/friends_api.dart'; +import 'package:social_app/features/settings/data/settings_api.dart'; +import 'package:social_app/features/users/data/models/user_response.dart'; +import 'package:social_app/features/users/data/users_api.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -68,7 +73,12 @@ class _SettingsScreenState extends State { const widgets.PageHeader(leading: widgets.BackButton()), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + padding: const EdgeInsets.fromLTRB( + AppSpacing.xl, + AppSpacing.sm, + AppSpacing.xl, + AppSpacing.xl, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -93,13 +103,13 @@ class _SettingsScreenState extends State { if (_isLoading) { return Container( width: double.infinity, - height: 100, - padding: const EdgeInsets.all(20), + height: 120, + padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( color: AppColors.white, - borderRadius: BorderRadius.circular(22), + borderRadius: BorderRadius.circular(24), ), - child: const Center(child: CircularProgressIndicator()), + child: const Center(child: AppLoadingIndicator(size: 22)), ); } @@ -108,82 +118,101 @@ class _SettingsScreenState extends State { return Container( width: double.infinity, - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - stops: [0, 1], - colors: [Color(0xFFFFFFFF), AppColors.surfaceInfoLight], - transform: GradientRotation(35 * 3.14159 / 180), + colors: [AppColors.white, Color(0xF8F9FCFF)], ), - borderRadius: BorderRadius.circular(22), - border: Border.all(color: const Color(0xFFE5ECF8)), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.borderSecondary), boxShadow: const [ BoxShadow( - color: Color(0x1A0F172A), - blurRadius: 14, - offset: Offset(0, 4), + color: Color(0x05000000), + blurRadius: 12, + offset: Offset(0, 3), ), ], ), child: Row( children: [ Container( - width: 72, - height: 72, + width: 64, + height: 64, decoration: BoxDecoration( - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Color(0xFFEAF1FF), Color(0xFFF8FBFF)], + colors: [AppColors.blue100, AppColors.blue50], ), - borderRadius: BorderRadius.circular(36), - border: Border.all(color: const Color(0xFFD9E5FA)), + borderRadius: BorderRadius.circular(32), + boxShadow: [ + BoxShadow( + color: Color.fromRGBO( + AppColors.blue400.r.toInt(), + AppColors.blue400.g.toInt(), + AppColors.blue400.b.toInt(), + 0.2, + ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), - child: const Icon(Icons.person, size: 30, color: AppColors.blue500), + child: const Icon(Icons.person, size: 28, color: AppColors.blue600), ), - const SizedBox(width: 12), + const SizedBox(width: AppSpacing.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - username, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w600, - color: AppColors.slate900, + Expanded( + child: Text( + username, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: AppColors.slate900, + ), + overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, - vertical: 4, + vertical: 5, ), decoration: BoxDecoration( - color: AppColors.surfaceTertiary, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFDEE7F6)), + gradient: LinearGradient( + colors: [ + AppColors.blue50, + AppColors.surfaceInfoLight, + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderQuaternary), ), child: const Text( 'Free', style: TextStyle( fontSize: 11, - fontWeight: FontWeight.w500, - color: AppColors.slate500, + fontWeight: FontWeight.w600, + color: AppColors.blue600, ), ), ), ], ), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( email, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.slate500, @@ -208,49 +237,34 @@ class _SettingsScreenState extends State { } Widget _buildQuickActions(BuildContext context) { - return Container( - height: 120, - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(18), - border: Border.all(color: const Color(0xFFE7EDF6)), - ), - child: Row( - children: [ - Expanded( - child: _buildQuickActionCard( - icon: Icons.people, - iconColor: AppColors.blue600, - iconBg: AppColors.surfaceTertiary, - iconBorder: const Color(0xFFE6ECF7), - title: '联系人', - subtitle: _buildFriendsSubtitle(), - onTap: () => context.push('/contacts'), - ), + return Row( + children: [ + Expanded( + child: _buildActionCard( + icon: Icons.people, + iconColor: AppColors.blue500, + title: '联系人', + subtitle: _buildFriendsSubtitle(), + onTap: () => context.push('/contacts'), ), - const SizedBox(width: 10), - Expanded( - child: _buildQuickActionCard( - icon: Icons.auto_awesome, - iconColor: const Color(0xFF0EA5A4), - iconBg: const Color(0xFFF7FAFF), - iconBorder: const Color(0xFFE6ECF7), - title: '常用功能', - subtitle: '已启用:会议提醒', - onTap: () => context.push('/settings/features'), - ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: _buildActionCard( + icon: Icons.auto_awesome, + iconColor: const Color(0xFF8B5CF6), + title: '常用功能', + subtitle: '已启用:会议提醒', + onTap: () => context.push('/settings/features'), ), - ], - ), + ), + ], ); } - Widget _buildQuickActionCard({ + Widget _buildActionCard({ required IconData icon, required Color iconColor, - required Color iconBg, - required Color iconBorder, required String title, required String subtitle, required VoidCallback onTap, @@ -258,47 +272,56 @@ class _SettingsScreenState extends State { return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: iconBg, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: iconBorder), + color: AppColors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.borderSecondary), + boxShadow: const [ + BoxShadow( + color: Color(0x04000000), + blurRadius: 6, + offset: Offset(0, 1), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Icon(icon, size: 18, color: iconColor), - const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Color(0xFF1E293B), - ), - ), - ], - ), - const Icon( - Icons.chevron_right, - size: 16, - color: AppColors.slate400, + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.surfaceTertiary, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 18, color: iconColor), ), + const Spacer(), + Icon(Icons.chevron_right, size: 16, color: AppColors.slate300), ], ), + const SizedBox(height: AppSpacing.md), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.slate900, + ), + ), + const SizedBox(height: 2), Text( subtitle, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.slate500, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), @@ -308,65 +331,95 @@ class _SettingsScreenState extends State { Widget _buildSubscriptionCard() { return Container( - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE3EAF6)), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.white, const Color(0xFFFAFBFF)], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.borderSecondary), + boxShadow: const [ + BoxShadow( + color: Color(0x03000000), + blurRadius: 6, + offset: Offset(0, 1), + ), + ], ), child: Row( children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.blue100, AppColors.blue50], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.blue200.withValues(alpha: 0.45), + blurRadius: 6, + offset: const Offset(0, 1), + ), + ], + ), + child: const Icon( + Icons.workspace_premium, + size: 22, + color: AppColors.blue600, + ), + ), + const SizedBox(width: AppSpacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - '套餐等级 Free', + '升级到 Pro', style: TextStyle( - fontSize: 15, + fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.slate900, ), ), - const SizedBox(height: 8), - const Text( - '∞ / ∞', + const SizedBox(height: 2), + Text( + '解锁更多高级功能', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.slate500, ), ), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular(999), - child: LinearProgressIndicator( - value: 0, - backgroundColor: const Color(0xFFE8EEF8), - valueColor: const AlwaysStoppedAnimation(AppColors.blue400), - minHeight: 8, - ), - ), ], ), ), - const SizedBox(width: 12), Container( - width: 72, - height: 32, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - color: const Color(0xFFE2E8F0), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFCBD5E1)), - ), - child: const Center( - child: Text( - '升级', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF94A3B8), + gradient: const LinearGradient( + colors: [AppColors.blue500, AppColors.blue600], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0x4D60A5FA), + blurRadius: 8, + offset: const Offset(0, 2), ), + ], + ), + child: const Text( + '升级', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.white, ), ), ), @@ -401,6 +454,13 @@ class _SettingsScreenState extends State { title: '我的账户', onTap: () => context.push('/settings/account'), ), + _buildDivider(), + _buildMenuItem( + icon: Icons.system_update, + title: '检查更新', + trailing: 'v${AppConstants.version}', + onTap: () => _checkForUpdates(context), + ), ], ), ); @@ -464,7 +524,59 @@ class _SettingsScreenState extends State { return Container( height: 1, margin: const EdgeInsets.symmetric(horizontal: 14), - color: const Color(0xFFEEF2F7), + color: AppColors.slate100, ); } + + Future _checkForUpdates(BuildContext context) async { + try { + final settingsApi = sl(); + final result = await settingsApi.checkUpdates( + currentBuild: AppConstants.build, + currentVersion: AppConstants.version, + platform: 'android', + ); + + if (!mounted) return; + + if (!result.hasUpdate) { + Toast.show(context, '当前已是最新版本', type: ToastType.success); + return; + } + + final message = result.updateType == 'required' + ? '有新版本可用 (${result.latestVersion}),请立即更新' + : '发现新版本 (${result.latestVersion}),是否更新?'; + + final shouldUpdate = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('检查更新'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + if (result.downloadUrl != null) + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('更新'), + ), + ], + ), + ); + + if (shouldUpdate == true && result.downloadUrl != null && mounted) { + Toast.show( + context, + '下载链接: ${result.downloadUrl}', + type: ToastType.info, + ); + } + } catch (e) { + if (!mounted) return; + Toast.show(context, '检查更新失败', type: ToastType.error); + } + } } diff --git a/apps/lib/main.dart b/apps/lib/main.dart index 8b83711..f98e431 100644 --- a/apps/lib/main.dart +++ b/apps/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'core/constants/app_constants.dart'; import 'core/di/injection.dart'; import 'core/notifications/local_notification_service.dart'; import 'core/router/app_router.dart'; @@ -16,6 +17,7 @@ import 'features/calendar/data/services/calendar_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await configureDependencies(); + await AppConstants.init(); final authBloc = sl(); authBloc.add(AuthStarted()); diff --git a/apps/pubspec.yaml b/apps/pubspec.yaml index 3b049b0..c6baa3b 100644 --- a/apps/pubspec.yaml +++ b/apps/pubspec.yaml @@ -1,7 +1,7 @@ name: social_app description: "Social App - A Flutter mobile application" publish_to: 'none' -version: 1.0.0+1 +version: 0.1.0+1 environment: sdk: ^3.10.7 @@ -25,6 +25,7 @@ dependencies: flutter_local_notifications: ^17.2.4 timezone: ^0.9.4 image_picker: ^1.0.7 + package_info_plus: ^8.0.3 dev_dependencies: flutter_test: diff --git a/backend/src/core/config/settings.py b/backend/src/core/config/settings.py index 77180a3..21c68a4 100644 --- a/backend/src/core/config/settings.py +++ b/backend/src/core/config/settings.py @@ -198,6 +198,25 @@ class DatabaseSettings(BaseModel): ) +class AppVersionSettings(BaseModel): + releases_dir: str = Field( + default="releases", + description="安装包目录,相对于项目根目录", + ) + current_version: str = Field( + default="0.1.0", + description="当前版本号", + ) + current_build: int = Field( + default=1, + description="当前构建号", + ) + download_base_url: str = Field( + default="", + description="下载链接基础域名,如 https://your-domain.com", + ) + + def _resolve_env_file() -> str: current = Path(__file__).resolve() for parent in [current, *current.parents]: @@ -221,6 +240,7 @@ class Settings(BaseSettings): agent_runtime: AgentRuntimeSettings = AgentRuntimeSettings() taskiq: TaskiqSettings = TaskiqSettings() database: DatabaseSettings = DatabaseSettings() + app_version: AppVersionSettings = AppVersionSettings() @computed_field @property diff --git a/backend/src/v1/app/router.py b/backend/src/v1/app/router.py new file mode 100644 index 0000000..e0ebf79 --- /dev/null +++ b/backend/src/v1/app/router.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import Literal + +from fastapi import APIRouter, Query +from pydantic import BaseModel, Field + +from core.config.settings import config + + +class AppVersionInfo(BaseModel): + has_update: bool = Field(description="是否有新版本可用") + latest_version: str = Field(description="最新版本号,如 0.1.0") + latest_build: int = Field(description="最新构建号") + min_required_version: str = Field(description="强制更新版本号") + update_type: Literal["none", "optional", "required"] = Field( + description="更新类型: none=无更新, optional=可选更新, required=必须更新" + ) + download_url: str | None = Field(default=None, description="安装包下载链接") + release_notes: str | None = Field(default=None, description="版本更新说明") + + +router = APIRouter(prefix="/app", tags=["app"]) + + +def _parse_version(filename: str) -> tuple[str, int] | None: + pattern = r"app[-_]v?(\d+\.\d+\.\d+)\+(\d+)\.(?:apk|ipa)" + match = re.search(pattern, filename, re.IGNORECASE) + if match: + version = match.group(1) + build = int(match.group(2)) + return (version, build) + return None + + +def _get_latest_release( + platform: Literal["ios", "android"], +) -> tuple[str, int, str] | None: + releases_dir = config.app_version.releases_dir + base_path = Path.cwd().parent / "deploy" / "static" / releases_dir + + if not base_path.exists(): + return None + + ext = "ipa" if platform == "ios" else "apk" + candidates = [] + + for f in base_path.iterdir(): + if f.is_file() and f.suffix.lstrip(".").lower() == ext.lower(): + parsed = _parse_version(f.name) + if parsed: + version, build = parsed + candidates.append((version, build, f.name)) + + if not candidates: + return None + + candidates.sort(key=lambda x: (x[0], x[1]), reverse=True) + return candidates[0][0], candidates[0][1], candidates[0][2] + + +def _compare_versions( + current_version: str, current_build: int, latest_version: str, latest_build: int +) -> tuple[bool, Literal["none", "optional", "required"]]: + if current_build >= latest_build: + return False, "none" + + if current_build < latest_build - 2: + return True, "required" + return True, "optional" + + +@router.get("/check-updates", response_model=AppVersionInfo) +async def check_updates( + current_version: str | None = Query(None, description="前端当前版本,如 0.1.0"), + current_build: int | None = Query(None, description="前端当前构建号,如 1"), + platform: Literal["ios", "android"] = Query("ios", description="平台类型"), +) -> AppVersionInfo: + current_build = current_build or 0 + + latest = _get_latest_release(platform) + if not latest: + return AppVersionInfo( + has_update=False, + latest_version=config.app_version.current_version, + latest_build=config.app_version.current_build, + min_required_version=config.app_version.current_version, + update_type="none", + download_url=None, + release_notes=None, + ) + + latest_version, latest_build, filename = latest + + has_update, update_type = _compare_versions( + current_version or "0.0.0", + current_build, + latest_version, + latest_build, + ) + + download_url: str | None = None + if has_update and config.app_version.download_base_url: + download_url = f"{config.app_version.download_base_url.rstrip('/')}/{config.app_version.releases_dir}/{filename}" + + return AppVersionInfo( + has_update=has_update, + latest_version=latest_version, + latest_build=latest_build, + min_required_version=latest_version, + update_type=update_type, + download_url=download_url, + release_notes="问题修复和体验优化", + ) diff --git a/backend/src/v1/router.py b/backend/src/v1/router.py index 3d12164..db242a5 100644 --- a/backend/src/v1/router.py +++ b/backend/src/v1/router.py @@ -3,6 +3,7 @@ from __future__ import annotations from fastapi import APIRouter from v1.agent.router import router as agent_router +from v1.app.router import router as app_router from v1.auth.router import router as auth_router from v1.friendships.router import router as friendships_router from v1.inbox_messages.router import router as inbox_messages_router @@ -12,8 +13,10 @@ from v1.users.router import router as users_router router = APIRouter(prefix="/api/v1") +router.include_router(app_router) router.include_router(auth_router) router.include_router(agent_router) +router.include_router(agent_router) router.include_router(friendships_router) router.include_router(users_router) router.include_router(schedule_items_router) diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 9ad5a6c..690ca39 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -68,6 +68,7 @@ services: condition: service_healthy volumes: - ../logs:/app/logs + - ./static/releases:/app/static/releases:ro healthcheck: test: [ @@ -104,6 +105,7 @@ services: condition: service_healthy volumes: - ../logs:/app/logs + - ./static/releases:/app/static/releases:ro worker-default: image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod} @@ -129,6 +131,7 @@ services: condition: service_healthy volumes: - ../logs:/app/logs + - ./static/releases:/app/static/releases:ro worker-bulk: image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod} @@ -154,6 +157,7 @@ services: condition: service_healthy volumes: - ../logs:/app/logs + - ./static/releases:/app/static/releases:ro init-job: image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod} @@ -178,6 +182,7 @@ services: condition: service_healthy volumes: - ../logs:/app/logs + - ./static/releases:/app/static/releases:ro profiles: - job