From ac962a91fed7143223b01f08bbf2c1f1db3751e4 Mon Sep 17 00:00:00 2001 From: dsp Date: Thu, 10 Sep 2026 20:42:49 +0800 Subject: [PATCH] =?UTF-8?q?DSP=20=E5=8F=8C=E7=AB=AF=E7=9F=AD=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=B9=B3=E5=8F=B0:Android=20+=20Web(Django/DRF=20+=20?= =?UTF-8?q?Vue3=20+=20Channels)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 12 模块:用户/视频/评论/消息/搜索/历史/访客/日志/音乐/通知/审核/创作者 - JWT 双 token + /accounts/refresh 无感续期(双端共用) - Web 端 Vue3+TS+Vite:沉浸 feed/评论/搜索/私信 WS/上传/通知中心/个人主页 - Android Kotlin+Compose:平台标识头 + 401 单飞刷新 - 部署:docker compose(nginx 网关 SPA+API+WS+媒体),已上线 192.168.5.7:19000 - 测试:pytest 19 用例(含 WS 回归)+ Playwright 冒烟脚本 --- .github/workflows/ci.yml | 40 + .gitignore | 40 + dsp/android/app/build.gradle.kts | 91 + dsp/android/app/src/main/AndroidManifest.xml | 32 + .../app/src/main/java/com/dsp/app/DspApp.kt | 12 + .../src/main/java/com/dsp/app/MainActivity.kt | 329 +++ .../java/com/dsp/app/data/api/ApiClient.kt | 135 + .../com/dsp/app/data/api/DspApiService.kt | 216 ++ .../com/dsp/app/data/local/TokenManager.kt | 44 + .../com/dsp/app/data/log/ClientLogReporter.kt | 77 + .../com/dsp/app/data/media/DspVideoCache.kt | 87 + .../java/com/dsp/app/data/model/Models.kt | 403 +++ .../java/com/dsp/app/ui/auth/LoginScreen.kt | 227 ++ .../com/dsp/app/ui/auth/RegisterScreen.kt | 208 ++ .../dsp/app/ui/comment/CommentBottomSheet.kt | 301 +++ .../com/dsp/app/ui/components/BottomNavBar.kt | 128 + .../dsp/app/ui/components/VideoPlayerView.kt | 91 + .../dsp/app/ui/creator/CreatorStudioScreen.kt | 262 ++ .../java/com/dsp/app/ui/feed/FeedScreen.kt | 410 +++ .../dsp/app/ui/history/WatchHistoryScreen.kt | 159 ++ .../dsp/app/ui/message/ChatDetailScreen.kt | 219 ++ .../com/dsp/app/ui/message/MessageScreen.kt | 323 +++ .../dsp/app/ui/message/SystemMessageScreen.kt | 159 ++ .../com/dsp/app/ui/moderation/ReportDialog.kt | 120 + .../com/dsp/app/ui/music/MusicDetailScreen.kt | 240 ++ .../com/dsp/app/ui/navigation/NavRoutes.kt | 40 + .../notifications/NotificationListScreen.kt | 217 ++ .../dsp/app/ui/profile/EditProfileScreen.kt | 152 ++ .../com/dsp/app/ui/profile/ProfileScreen.kt | 328 +++ .../com/dsp/app/ui/search/SearchScreen.kt | 378 +++ .../main/java/com/dsp/app/ui/theme/Color.kt | 12 + .../main/java/com/dsp/app/ui/theme/Theme.kt | 25 + .../main/java/com/dsp/app/ui/theme/Type.kt | 40 + .../dsp/app/ui/upload/VideoUploadScreen.kt | 257 ++ .../com/dsp/app/ui/visitor/VisitorScreen.kt | 158 ++ .../app/src/main/res/values/colors.xml | 8 + .../app/src/main/res/values/strings.xml | 8 + .../app/src/main/res/values/themes.xml | 6 + dsp/android/build.gradle.kts | 6 + dsp/android/gradle.properties | 4 + dsp/android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + dsp/android/gradlew | 249 ++ dsp/android/gradlew.bat | 92 + dsp/android/settings.gradle.kts | 17 + dsp/backend/.env.example | 25 + dsp/backend/Dockerfile | 43 + dsp/backend/apps/__init__.py | 0 dsp/backend/apps/accounts/__init__.py | 2 + dsp/backend/apps/accounts/apps.py | 8 + dsp/backend/apps/accounts/auth.py | 33 + dsp/backend/apps/accounts/jwt_utils.py | 67 + .../apps/accounts/migrations/0001_initial.py | 87 + .../apps/accounts/migrations/__init__.py | 1 + dsp/backend/apps/accounts/models.py | 86 + dsp/backend/apps/accounts/serializers.py | 55 + dsp/backend/apps/accounts/urls.py | 13 + dsp/backend/apps/accounts/views.py | 181 ++ dsp/backend/apps/comments/__init__.py | 2 + dsp/backend/apps/comments/apps.py | 7 + .../apps/comments/migrations/0001_initial.py | 60 + .../apps/comments/migrations/__init__.py | 1 + dsp/backend/apps/comments/models.py | 42 + dsp/backend/apps/comments/serializers.py | 49 + dsp/backend/apps/comments/urls.py | 10 + dsp/backend/apps/comments/views.py | 205 ++ dsp/backend/apps/creator/__init__.py | 2 + dsp/backend/apps/creator/apps.py | 7 + .../apps/creator/migrations/0001_initial.py | 35 + .../apps/creator/migrations/__init__.py | 0 dsp/backend/apps/creator/models.py | 28 + dsp/backend/apps/creator/urls.py | 9 + dsp/backend/apps/creator/views.py | 133 + dsp/backend/apps/history/__init__.py | 2 + dsp/backend/apps/history/apps.py | 7 + .../apps/history/migrations/0001_initial.py | 63 + .../apps/history/migrations/__init__.py | 1 + dsp/backend/apps/history/models.py | 47 + dsp/backend/apps/history/serializers.py | 35 + dsp/backend/apps/history/services.py | 51 + dsp/backend/apps/history/urls.py | 10 + dsp/backend/apps/history/views.py | 87 + dsp/backend/apps/logs/__init__.py | 2 + dsp/backend/apps/logs/apps.py | 7 + .../apps/logs/migrations/0001_initial.py | 54 + .../migrations/0002_requestlog_platform.py | 18 + dsp/backend/apps/logs/migrations/__init__.py | 1 + dsp/backend/apps/logs/models.py | 56 + dsp/backend/apps/logs/serializers.py | 54 + dsp/backend/apps/logs/urls.py | 10 + dsp/backend/apps/logs/views.py | 94 + dsp/backend/apps/messages_app/__init__.py | 2 + dsp/backend/apps/messages_app/apps.py | 7 + dsp/backend/apps/messages_app/consumers.py | 88 + .../messages_app/migrations/0001_initial.py | 87 + .../apps/messages_app/migrations/__init__.py | 1 + dsp/backend/apps/messages_app/models.py | 72 + dsp/backend/apps/messages_app/routing.py | 8 + dsp/backend/apps/messages_app/serializers.py | 56 + dsp/backend/apps/messages_app/urls.py | 16 + dsp/backend/apps/messages_app/views.py | 206 ++ dsp/backend/apps/moderation/__init__.py | 2 + dsp/backend/apps/moderation/admin.py | 18 + dsp/backend/apps/moderation/apps.py | 7 + .../moderation/migrations/0001_initial.py | 37 + .../migrations/0002_sensitiveword.py | 34 + .../apps/moderation/migrations/__init__.py | 0 dsp/backend/apps/moderation/models.py | 86 + dsp/backend/apps/moderation/serializers.py | 36 + dsp/backend/apps/moderation/urls.py | 20 + dsp/backend/apps/moderation/views.py | 150 ++ dsp/backend/apps/moderation/wordlist.py | 51 + dsp/backend/apps/music/__init__.py | 2 + dsp/backend/apps/music/apps.py | 7 + .../apps/music/migrations/0001_initial.py | 37 + dsp/backend/apps/music/migrations/__init__.py | 0 dsp/backend/apps/music/models.py | 34 + dsp/backend/apps/music/serializers.py | 39 + dsp/backend/apps/music/urls.py | 10 + dsp/backend/apps/music/views.py | 76 + dsp/backend/apps/notifications/__init__.py | 2 + dsp/backend/apps/notifications/apps.py | 7 + .../notifications/migrations/0001_initial.py | 37 + .../apps/notifications/migrations/__init__.py | 0 dsp/backend/apps/notifications/models.py | 44 + dsp/backend/apps/notifications/serializers.py | 26 + dsp/backend/apps/notifications/services.py | 40 + dsp/backend/apps/notifications/urls.py | 10 + dsp/backend/apps/notifications/views.py | 99 + dsp/backend/apps/search/__init__.py | 2 + dsp/backend/apps/search/apps.py | 7 + .../apps/search/migrations/0001_initial.py | 27 + .../apps/search/migrations/__init__.py | 1 + dsp/backend/apps/search/models.py | 14 + dsp/backend/apps/search/serializers.py | 12 + dsp/backend/apps/search/urls.py | 10 + dsp/backend/apps/search/views.py | 92 + dsp/backend/apps/videos/__init__.py | 2 + dsp/backend/apps/videos/apps.py | 7 + .../apps/videos/migrations/0001_initial.py | 114 + .../videos/migrations/0002_video_music.py | 20 + .../apps/videos/migrations/__init__.py | 1 + dsp/backend/apps/videos/models.py | 102 + dsp/backend/apps/videos/serializers.py | 95 + dsp/backend/apps/videos/urls.py | 24 + dsp/backend/apps/videos/views.py | 371 +++ dsp/backend/apps/visitors/__init__.py | 2 + dsp/backend/apps/visitors/apps.py | 7 + .../apps/visitors/migrations/0001_initial.py | 33 + .../apps/visitors/migrations/__init__.py | 1 + dsp/backend/apps/visitors/models.py | 37 + dsp/backend/apps/visitors/serializers.py | 17 + dsp/backend/apps/visitors/urls.py | 10 + dsp/backend/apps/visitors/views.py | 65 + dsp/backend/config/asgi.py | 34 + dsp/backend/config/settings.py | 230 ++ dsp/backend/config/urls.py | 30 + dsp/backend/config/wsgi.py | 7 + dsp/backend/conftest.py | 46 + dsp/backend/core/__init__.py | 3 + dsp/backend/core/api.py | 12 + dsp/backend/core/apps.py | 7 + dsp/backend/core/dfa.py | 97 + dsp/backend/core/health_urls.py | 10 + dsp/backend/core/logging.py | 54 + dsp/backend/core/middleware.py | 49 + dsp/backend/core/pagination.py | 24 + dsp/backend/core/utils.py | 1 + dsp/backend/manage.py | 21 + dsp/backend/media/videos/smoke.mp4 | Bin 0 -> 24 bytes dsp/backend/pyproject.toml | 40 + dsp/backend/pytest.ini | 8 + dsp/backend/tests/__init__.py | 0 dsp/backend/tests/test_new_modules.py | 297 +++ dsp/backend/tests/test_smoke.py | 212 ++ dsp/backend/tests/test_ws_chat.py | 65 + dsp/deploy/docker-compose.yml | 106 + dsp/docs/architecture-dual-client.md | 214 ++ dsp/docs/architecture.md | 192 ++ dsp/docs/competitor-analysis.md | 75 + dsp/docs/prod-web-verify.png | Bin 0 -> 35519 bytes dsp/docs/web-feed-smoke.png | Bin 0 -> 19791 bytes dsp/docs/web-messages-ws.png | Bin 0 -> 27603 bytes dsp/nginx/nginx.conf | 97 + dsp/web/.dockerignore | 4 + dsp/web/Dockerfile | 12 + dsp/web/e2e/smoke.mjs | 103 + dsp/web/index.html | 12 + dsp/web/package-lock.json | 2202 +++++++++++++++++ dsp/web/package.json | 26 + dsp/web/src/App.vue | 20 + dsp/web/src/api/client.ts | 92 + dsp/web/src/api/index.ts | 275 ++ dsp/web/src/constants.ts | 5 + dsp/web/src/main.ts | 13 + dsp/web/src/router/index.ts | 28 + dsp/web/src/stores/auth.ts | 96 + dsp/web/src/styles.css | 37 + dsp/web/src/utils/ws.ts | 75 + dsp/web/src/views/auth/LoginView.vue | 135 + dsp/web/src/views/feed/CommentPanel.vue | 238 ++ dsp/web/src/views/feed/FeedView.vue | 396 +++ dsp/web/src/views/history/HistoryView.vue | 149 ++ dsp/web/src/views/message/MessagesView.vue | 342 +++ .../views/notifications/NotificationsView.vue | 157 ++ dsp/web/src/views/profile/ProfileView.vue | 200 ++ dsp/web/src/views/search/SearchView.vue | 259 ++ dsp/web/src/views/upload/UploadView.vue | 184 ++ dsp/web/src/views/user/UserView.vue | 238 ++ dsp/web/src/views/visitor/VisitorsView.vue | 81 + dsp/web/src/vite-env.d.ts | 3 + dsp/web/tsconfig.json | 24 + dsp/web/tsconfig.tsbuildinfo | 1 + dsp/web/vite.config.ts | 41 + 214 files changed, 18053 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 dsp/android/app/build.gradle.kts create mode 100644 dsp/android/app/src/main/AndroidManifest.xml create mode 100644 dsp/android/app/src/main/java/com/dsp/app/DspApp.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/MainActivity.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/data/api/ApiClient.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/data/api/DspApiService.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/data/local/TokenManager.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/data/log/ClientLogReporter.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/data/media/DspVideoCache.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/data/model/Models.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/auth/LoginScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/auth/RegisterScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/comment/CommentBottomSheet.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/components/BottomNavBar.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/components/VideoPlayerView.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/creator/CreatorStudioScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/feed/FeedScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/history/WatchHistoryScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/message/ChatDetailScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/message/MessageScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/message/SystemMessageScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/moderation/ReportDialog.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/music/MusicDetailScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/navigation/NavRoutes.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/notifications/NotificationListScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/profile/EditProfileScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/profile/ProfileScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/search/SearchScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/theme/Color.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/theme/Theme.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/theme/Type.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/upload/VideoUploadScreen.kt create mode 100644 dsp/android/app/src/main/java/com/dsp/app/ui/visitor/VisitorScreen.kt create mode 100644 dsp/android/app/src/main/res/values/colors.xml create mode 100644 dsp/android/app/src/main/res/values/strings.xml create mode 100644 dsp/android/app/src/main/res/values/themes.xml create mode 100644 dsp/android/build.gradle.kts create mode 100644 dsp/android/gradle.properties create mode 100644 dsp/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 dsp/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 dsp/android/gradlew create mode 100644 dsp/android/gradlew.bat create mode 100644 dsp/android/settings.gradle.kts create mode 100644 dsp/backend/.env.example create mode 100644 dsp/backend/Dockerfile create mode 100644 dsp/backend/apps/__init__.py create mode 100644 dsp/backend/apps/accounts/__init__.py create mode 100644 dsp/backend/apps/accounts/apps.py create mode 100644 dsp/backend/apps/accounts/auth.py create mode 100644 dsp/backend/apps/accounts/jwt_utils.py create mode 100644 dsp/backend/apps/accounts/migrations/0001_initial.py create mode 100644 dsp/backend/apps/accounts/migrations/__init__.py create mode 100644 dsp/backend/apps/accounts/models.py create mode 100644 dsp/backend/apps/accounts/serializers.py create mode 100644 dsp/backend/apps/accounts/urls.py create mode 100644 dsp/backend/apps/accounts/views.py create mode 100644 dsp/backend/apps/comments/__init__.py create mode 100644 dsp/backend/apps/comments/apps.py create mode 100644 dsp/backend/apps/comments/migrations/0001_initial.py create mode 100644 dsp/backend/apps/comments/migrations/__init__.py create mode 100644 dsp/backend/apps/comments/models.py create mode 100644 dsp/backend/apps/comments/serializers.py create mode 100644 dsp/backend/apps/comments/urls.py create mode 100644 dsp/backend/apps/comments/views.py create mode 100644 dsp/backend/apps/creator/__init__.py create mode 100644 dsp/backend/apps/creator/apps.py create mode 100644 dsp/backend/apps/creator/migrations/0001_initial.py create mode 100644 dsp/backend/apps/creator/migrations/__init__.py create mode 100644 dsp/backend/apps/creator/models.py create mode 100644 dsp/backend/apps/creator/urls.py create mode 100644 dsp/backend/apps/creator/views.py create mode 100644 dsp/backend/apps/history/__init__.py create mode 100644 dsp/backend/apps/history/apps.py create mode 100644 dsp/backend/apps/history/migrations/0001_initial.py create mode 100644 dsp/backend/apps/history/migrations/__init__.py create mode 100644 dsp/backend/apps/history/models.py create mode 100644 dsp/backend/apps/history/serializers.py create mode 100644 dsp/backend/apps/history/services.py create mode 100644 dsp/backend/apps/history/urls.py create mode 100644 dsp/backend/apps/history/views.py create mode 100644 dsp/backend/apps/logs/__init__.py create mode 100644 dsp/backend/apps/logs/apps.py create mode 100644 dsp/backend/apps/logs/migrations/0001_initial.py create mode 100644 dsp/backend/apps/logs/migrations/0002_requestlog_platform.py create mode 100644 dsp/backend/apps/logs/migrations/__init__.py create mode 100644 dsp/backend/apps/logs/models.py create mode 100644 dsp/backend/apps/logs/serializers.py create mode 100644 dsp/backend/apps/logs/urls.py create mode 100644 dsp/backend/apps/logs/views.py create mode 100644 dsp/backend/apps/messages_app/__init__.py create mode 100644 dsp/backend/apps/messages_app/apps.py create mode 100644 dsp/backend/apps/messages_app/consumers.py create mode 100644 dsp/backend/apps/messages_app/migrations/0001_initial.py create mode 100644 dsp/backend/apps/messages_app/migrations/__init__.py create mode 100644 dsp/backend/apps/messages_app/models.py create mode 100644 dsp/backend/apps/messages_app/routing.py create mode 100644 dsp/backend/apps/messages_app/serializers.py create mode 100644 dsp/backend/apps/messages_app/urls.py create mode 100644 dsp/backend/apps/messages_app/views.py create mode 100644 dsp/backend/apps/moderation/__init__.py create mode 100644 dsp/backend/apps/moderation/admin.py create mode 100644 dsp/backend/apps/moderation/apps.py create mode 100644 dsp/backend/apps/moderation/migrations/0001_initial.py create mode 100644 dsp/backend/apps/moderation/migrations/0002_sensitiveword.py create mode 100644 dsp/backend/apps/moderation/migrations/__init__.py create mode 100644 dsp/backend/apps/moderation/models.py create mode 100644 dsp/backend/apps/moderation/serializers.py create mode 100644 dsp/backend/apps/moderation/urls.py create mode 100644 dsp/backend/apps/moderation/views.py create mode 100644 dsp/backend/apps/moderation/wordlist.py create mode 100644 dsp/backend/apps/music/__init__.py create mode 100644 dsp/backend/apps/music/apps.py create mode 100644 dsp/backend/apps/music/migrations/0001_initial.py create mode 100644 dsp/backend/apps/music/migrations/__init__.py create mode 100644 dsp/backend/apps/music/models.py create mode 100644 dsp/backend/apps/music/serializers.py create mode 100644 dsp/backend/apps/music/urls.py create mode 100644 dsp/backend/apps/music/views.py create mode 100644 dsp/backend/apps/notifications/__init__.py create mode 100644 dsp/backend/apps/notifications/apps.py create mode 100644 dsp/backend/apps/notifications/migrations/0001_initial.py create mode 100644 dsp/backend/apps/notifications/migrations/__init__.py create mode 100644 dsp/backend/apps/notifications/models.py create mode 100644 dsp/backend/apps/notifications/serializers.py create mode 100644 dsp/backend/apps/notifications/services.py create mode 100644 dsp/backend/apps/notifications/urls.py create mode 100644 dsp/backend/apps/notifications/views.py create mode 100644 dsp/backend/apps/search/__init__.py create mode 100644 dsp/backend/apps/search/apps.py create mode 100644 dsp/backend/apps/search/migrations/0001_initial.py create mode 100644 dsp/backend/apps/search/migrations/__init__.py create mode 100644 dsp/backend/apps/search/models.py create mode 100644 dsp/backend/apps/search/serializers.py create mode 100644 dsp/backend/apps/search/urls.py create mode 100644 dsp/backend/apps/search/views.py create mode 100644 dsp/backend/apps/videos/__init__.py create mode 100644 dsp/backend/apps/videos/apps.py create mode 100644 dsp/backend/apps/videos/migrations/0001_initial.py create mode 100644 dsp/backend/apps/videos/migrations/0002_video_music.py create mode 100644 dsp/backend/apps/videos/migrations/__init__.py create mode 100644 dsp/backend/apps/videos/models.py create mode 100644 dsp/backend/apps/videos/serializers.py create mode 100644 dsp/backend/apps/videos/urls.py create mode 100644 dsp/backend/apps/videos/views.py create mode 100644 dsp/backend/apps/visitors/__init__.py create mode 100644 dsp/backend/apps/visitors/apps.py create mode 100644 dsp/backend/apps/visitors/migrations/0001_initial.py create mode 100644 dsp/backend/apps/visitors/migrations/__init__.py create mode 100644 dsp/backend/apps/visitors/models.py create mode 100644 dsp/backend/apps/visitors/serializers.py create mode 100644 dsp/backend/apps/visitors/urls.py create mode 100644 dsp/backend/apps/visitors/views.py create mode 100644 dsp/backend/config/asgi.py create mode 100644 dsp/backend/config/settings.py create mode 100644 dsp/backend/config/urls.py create mode 100644 dsp/backend/config/wsgi.py create mode 100644 dsp/backend/conftest.py create mode 100644 dsp/backend/core/__init__.py create mode 100644 dsp/backend/core/api.py create mode 100644 dsp/backend/core/apps.py create mode 100644 dsp/backend/core/dfa.py create mode 100644 dsp/backend/core/health_urls.py create mode 100644 dsp/backend/core/logging.py create mode 100644 dsp/backend/core/middleware.py create mode 100644 dsp/backend/core/pagination.py create mode 100644 dsp/backend/core/utils.py create mode 100644 dsp/backend/manage.py create mode 100644 dsp/backend/media/videos/smoke.mp4 create mode 100644 dsp/backend/pyproject.toml create mode 100644 dsp/backend/pytest.ini create mode 100644 dsp/backend/tests/__init__.py create mode 100644 dsp/backend/tests/test_new_modules.py create mode 100644 dsp/backend/tests/test_smoke.py create mode 100644 dsp/backend/tests/test_ws_chat.py create mode 100644 dsp/deploy/docker-compose.yml create mode 100644 dsp/docs/architecture-dual-client.md create mode 100644 dsp/docs/architecture.md create mode 100644 dsp/docs/competitor-analysis.md create mode 100644 dsp/docs/prod-web-verify.png create mode 100644 dsp/docs/web-feed-smoke.png create mode 100644 dsp/docs/web-messages-ws.png create mode 100644 dsp/nginx/nginx.conf create mode 100644 dsp/web/.dockerignore create mode 100644 dsp/web/Dockerfile create mode 100644 dsp/web/e2e/smoke.mjs create mode 100644 dsp/web/index.html create mode 100644 dsp/web/package-lock.json create mode 100644 dsp/web/package.json create mode 100644 dsp/web/src/App.vue create mode 100644 dsp/web/src/api/client.ts create mode 100644 dsp/web/src/api/index.ts create mode 100644 dsp/web/src/constants.ts create mode 100644 dsp/web/src/main.ts create mode 100644 dsp/web/src/router/index.ts create mode 100644 dsp/web/src/stores/auth.ts create mode 100644 dsp/web/src/styles.css create mode 100644 dsp/web/src/utils/ws.ts create mode 100644 dsp/web/src/views/auth/LoginView.vue create mode 100644 dsp/web/src/views/feed/CommentPanel.vue create mode 100644 dsp/web/src/views/feed/FeedView.vue create mode 100644 dsp/web/src/views/history/HistoryView.vue create mode 100644 dsp/web/src/views/message/MessagesView.vue create mode 100644 dsp/web/src/views/notifications/NotificationsView.vue create mode 100644 dsp/web/src/views/profile/ProfileView.vue create mode 100644 dsp/web/src/views/search/SearchView.vue create mode 100644 dsp/web/src/views/upload/UploadView.vue create mode 100644 dsp/web/src/views/user/UserView.vue create mode 100644 dsp/web/src/views/visitor/VisitorsView.vue create mode 100644 dsp/web/src/vite-env.d.ts create mode 100644 dsp/web/tsconfig.json create mode 100644 dsp/web/tsconfig.tsbuildinfo create mode 100644 dsp/web/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a0b1331 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + +jobs: + backend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: dsp/backend + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install deps + run: | + pip install "Django>=5.2,<5.3" "djangorestframework>=3.15,<3.16" "adrf>=0.1.9" \ + "psycopg[binary]>=3.2,<4.0" "redis>=5.0,<6.0" "channels>=4.1,<5.0" "channels-redis>=4.2" \ + "daphne>=4.1" "django-cors-headers>=4.4" "django-redis>=5.4" "Pillow>=10.4" \ + "python-decouple>=3.8" "argon2-cffi>=23.1" "PyJWT>=2.9" \ + "pytest" "pytest-asyncio" "pytest-django" "httpx" + - name: Tests (DSP_TEST=1, zero external deps) + run: DSP_TEST=1 python -m pytest tests/ -q + + web: + runs-on: ubuntu-latest + defaults: + run: + working-directory: dsp/web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npx vue-tsc -b + - run: npm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28f4470 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# ---- 依赖与构建产物 ---- +node_modules/ +dist/ +.vite/ +build/ +.gradle/ +.gradle-dist/ +app/build/ + +# ---- Python ---- +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +test_db.sqlite3 + +# ---- 运行时数据 ---- +backend/media/ +*.log + +# ---- 本地环境 ---- +.env +local.properties + +# ---- IDE / 工具 ---- +.idea/ +.vscode/ +.kotlin/ +.playwright-mcp/ +*.tar.gz + +# ---- Android ---- +android/.gradle/ +android/build/ +android/app/build/ +android/.kotlin/ +android/app/release/ +*.apk +*.aab +dsp/.test/ diff --git a/dsp/android/app/build.gradle.kts b/dsp/android/app/build.gradle.kts new file mode 100644 index 0000000..39a3aa1 --- /dev/null +++ b/dsp/android/app/build.gradle.kts @@ -0,0 +1,91 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.kotlin.plugin.serialization") +} + +android { + namespace = "com.dsp.app" + compileSdk = 34 + + defaultConfig { + applicationId = "com.dsp.app" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += listOf( + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", + "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi" + ) + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Core & Lifecycle + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4") + implementation("androidx.activity:activity-compose:1.9.1") + + // Jetpack Compose + implementation(platform("androidx.compose:compose-bom:2024.08.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.navigation:navigation-compose:2.7.7") + + // Media3 (ExoPlayer) + implementation("androidx.media3:media3-exoplayer:1.4.0") + implementation("androidx.media3:media3-ui:1.4.0") + implementation("androidx.media3:media3-common:1.4.0") + + // Image Loading + implementation("io.coil-kt:coil-compose:2.7.0") + + // Network & Serialization + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") + + // Local Storage + implementation("androidx.datastore:datastore-preferences:1.1.1") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") +} diff --git a/dsp/android/app/src/main/AndroidManifest.xml b/dsp/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cdba9f8 --- /dev/null +++ b/dsp/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dsp/android/app/src/main/java/com/dsp/app/DspApp.kt b/dsp/android/app/src/main/java/com/dsp/app/DspApp.kt new file mode 100644 index 0000000..0870c89 --- /dev/null +++ b/dsp/android/app/src/main/java/com/dsp/app/DspApp.kt @@ -0,0 +1,12 @@ +package com.dsp.app + +import android.app.Application +import com.dsp.app.data.log.ClientLogReporter + +class DspApp : Application() { + override fun onCreate() { + super.onCreate() + // 全局未捕获异常日志捕获与上报(日志系统) + ClientLogReporter.initCrashHandler(this) + } +} diff --git a/dsp/android/app/src/main/java/com/dsp/app/MainActivity.kt b/dsp/android/app/src/main/java/com/dsp/app/MainActivity.kt new file mode 100644 index 0000000..3488eed --- /dev/null +++ b/dsp/android/app/src/main/java/com/dsp/app/MainActivity.kt @@ -0,0 +1,329 @@ +package com.dsp.app + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.NavType +import androidx.navigation.compose.* +import androidx.navigation.navArgument +import com.dsp.app.data.api.ApiClient +import com.dsp.app.data.local.TokenManager +import com.dsp.app.ui.auth.LoginScreen +import com.dsp.app.ui.auth.RegisterScreen +import com.dsp.app.ui.components.DspBottomNavBar +import com.dsp.app.ui.creator.CreatorStudioScreen +import com.dsp.app.ui.feed.FeedScreen +import com.dsp.app.ui.history.WatchHistoryScreen +import com.dsp.app.ui.message.ChatDetailScreen +import com.dsp.app.ui.message.MessageScreen +import com.dsp.app.ui.message.SystemMessageScreen +import com.dsp.app.ui.music.MusicDetailScreen +import com.dsp.app.ui.navigation.NavRoutes +import com.dsp.app.ui.notifications.NotificationListScreen +import com.dsp.app.ui.profile.EditProfileScreen +import com.dsp.app.ui.profile.ProfileScreen +import com.dsp.app.ui.search.SearchScreen +import com.dsp.app.ui.theme.DspTheme +import com.dsp.app.ui.upload.VideoUploadScreen +import com.dsp.app.ui.visitor.VisitorScreen +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + DspTheme { + MainAppContent() + } + } + } +} + +@Composable +fun MainAppContent() { + val context = LocalContext.current + val tokenManager = remember { TokenManager(context) } + val api = remember { ApiClient.getService(context) } + val scope = rememberCoroutineScope() + val navController = rememberNavController() + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + val isLoggedIn = remember { tokenManager.isLoggedIn() } + val startDestination = if (isLoggedIn) NavRoutes.Feed.route else NavRoutes.Login.route + + var unreadCount by remember { mutableIntStateOf(0) } + + // Poll unread message count + LaunchedEffect(isLoggedIn) { + if (isLoggedIn) { + while (isActive) { + try { + val resp = api.getUnreadCount() + if (resp.isSuccessful) { + val body = resp.body() + if (body != null) { + unreadCount = body.systemUnread + body.dmUnread + } + } + } catch (e: Exception) { + // ignore + } + delay(10000) + } + } + } + + val showBottomBar = currentRoute in listOf( + NavRoutes.Feed.route, + NavRoutes.Search.route, + NavRoutes.Messages.route, + NavRoutes.Profile.route + ) + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + if (showBottomBar) { + DspBottomNavBar( + currentRoute = currentRoute, + unreadCount = unreadCount, + onNavigate = { route -> + navController.navigate(route) { + popUpTo(NavRoutes.Feed.route) { saveState = true } + launchSingleTop = true + restoreState = true + } + }, + onUploadClick = { + navController.navigate(NavRoutes.VideoUpload.createRoute()) + } + ) + } + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = startDestination, + modifier = Modifier.padding(innerPadding) + ) { + composable(NavRoutes.Login.route) { + LoginScreen( + onLoginSuccess = { + navController.navigate(NavRoutes.Feed.route) { + popUpTo(NavRoutes.Login.route) { inclusive = true } + } + }, + onNavigateToRegister = { + navController.navigate(NavRoutes.Register.route) + } + ) + } + + composable(NavRoutes.Register.route) { + RegisterScreen( + onRegisterSuccess = { + navController.navigate(NavRoutes.Feed.route) { + popUpTo(NavRoutes.Login.route) { inclusive = true } + } + }, + onNavigateToLogin = { + navController.popBackStack() + } + ) + } + + composable(NavRoutes.Feed.route) { + FeedScreen( + onNavigateToProfile = { userId -> + navController.navigate(NavRoutes.UserProfile.createRoute(userId)) + }, + onNavigateToSearch = { + navController.navigate(NavRoutes.Search.route) + }, + onNavigateToMusic = { musicId -> + navController.navigate(NavRoutes.MusicDetail.createRoute(musicId)) + } + ) + } + + composable(NavRoutes.Search.route) { + SearchScreen( + onBack = { navController.popBackStack() }, + onNavigateToUserProfile = { userId -> + navController.navigate(NavRoutes.UserProfile.createRoute(userId)) + } + ) + } + + composable(NavRoutes.Messages.route) { + MessageScreen( + onNavigateToChat = { convId, peerId, peerName -> + navController.navigate(NavRoutes.ChatDetail.createRoute(convId, peerId, peerName)) + }, + onNavigateToSystemMessages = { + navController.navigate(NavRoutes.SystemMessages.route) + }, + onNavigateToNotifications = { type -> + navController.navigate(NavRoutes.Notifications.createRoute(type)) + } + ) + } + + composable( + route = NavRoutes.Notifications.route, + arguments = listOf(navArgument("type") { type = NavType.StringType }) + ) { backStackEntry -> + val type = backStackEntry.arguments?.getString("type") ?: "all" + NotificationListScreen( + initialType = if (type == "all") null else type, + onBack = { navController.popBackStack() }, + onUserClick = { userId -> + navController.navigate(NavRoutes.UserProfile.createRoute(userId)) + }, + onVideoClick = { } + ) + } + + composable( + route = NavRoutes.MusicDetail.route, + arguments = listOf(navArgument("musicId") { type = NavType.LongType }) + ) { backStackEntry -> + val musicId = backStackEntry.arguments?.getLong("musicId") ?: 0L + MusicDetailScreen( + musicId = musicId, + onBack = { navController.popBackStack() }, + onShootSameSoundtrack = { mId, mTitle -> + navController.navigate(NavRoutes.VideoUpload.createRoute(musicId = mId, musicTitle = mTitle)) + }, + onVideoClick = { } + ) + } + + composable( + route = NavRoutes.ChatDetail.route, + arguments = listOf( + navArgument("convId") { type = NavType.LongType }, + navArgument("peerId") { type = NavType.LongType }, + navArgument("peerName") { type = NavType.StringType } + ) + ) { backStackEntry -> + val convId = backStackEntry.arguments?.getLong("convId") ?: 0L + val peerId = backStackEntry.arguments?.getLong("peerId") ?: 0L + val peerName = backStackEntry.arguments?.getString("peerName") ?: "Chat" + ChatDetailScreen( + convId = convId, + peerId = peerId, + peerName = peerName, + onBack = { navController.popBackStack() } + ) + } + + composable(NavRoutes.SystemMessages.route) { + SystemMessageScreen(onBack = { navController.popBackStack() }) + } + + composable(NavRoutes.Profile.route) { + ProfileScreen( + targetUserId = null, + onNavigateToEditProfile = { navController.navigate(NavRoutes.EditProfile.route) }, + onNavigateToWatchHistory = { navController.navigate(NavRoutes.WatchHistory.route) }, + onNavigateToVisitors = { navController.navigate(NavRoutes.Visitors.route) }, + onNavigateToCreatorStudio = { navController.navigate(NavRoutes.CreatorStudio.route) }, + onNavigateToChat = { convId, peerId, peerName -> + navController.navigate(NavRoutes.ChatDetail.createRoute(convId, peerId, peerName)) + }, + onLogout = { + tokenManager.clear() + navController.navigate(NavRoutes.Login.route) { + popUpTo(0) { inclusive = true } + } + } + ) + } + + composable( + route = NavRoutes.UserProfile.route, + arguments = listOf(navArgument("userId") { type = NavType.LongType }) + ) { backStackEntry -> + val userId = backStackEntry.arguments?.getLong("userId") ?: 0L + ProfileScreen( + targetUserId = userId, + onNavigateToEditProfile = {}, + onNavigateToWatchHistory = {}, + onNavigateToVisitors = {}, + onNavigateToCreatorStudio = {}, + onNavigateToChat = { convId, peerId, peerName -> + navController.navigate(NavRoutes.ChatDetail.createRoute(convId, peerId, peerName)) + }, + onLogout = {} + ) + } + + composable(NavRoutes.EditProfile.route) { + EditProfileScreen(onBack = { navController.popBackStack() }) + } + + composable(NavRoutes.WatchHistory.route) { + WatchHistoryScreen( + onBack = { navController.popBackStack() }, + onVideoClick = { } + ) + } + + composable(NavRoutes.Visitors.route) { + VisitorScreen( + onBack = { navController.popBackStack() }, + onUserClick = { userId -> + navController.navigate(NavRoutes.UserProfile.createRoute(userId)) + } + ) + } + + composable( + route = NavRoutes.VideoUpload.route, + arguments = listOf( + navArgument("musicId") { + type = NavType.StringType + nullable = true + defaultValue = null + }, + navArgument("musicTitle") { + type = NavType.StringType + nullable = true + defaultValue = null + } + ) + ) { backStackEntry -> + val musicIdStr = backStackEntry.arguments?.getString("musicId") + val musicTitle = backStackEntry.arguments?.getString("musicTitle") + VideoUploadScreen( + musicId = musicIdStr?.toLongOrNull(), + musicTitle = musicTitle, + onBack = { navController.popBackStack() }, + onUploadSuccess = { + navController.navigate(NavRoutes.Feed.route) { + popUpTo(NavRoutes.Feed.route) { inclusive = true } + } + } + ) + } + + composable(NavRoutes.CreatorStudio.route) { + CreatorStudioScreen( + onBack = { navController.popBackStack() }, + onVideoClick = { } + ) + } + } + } +} diff --git a/dsp/android/app/src/main/java/com/dsp/app/data/api/ApiClient.kt b/dsp/android/app/src/main/java/com/dsp/app/data/api/ApiClient.kt new file mode 100644 index 0000000..fcc7e03 --- /dev/null +++ b/dsp/android/app/src/main/java/com/dsp/app/data/api/ApiClient.kt @@ -0,0 +1,135 @@ +package com.dsp.app.data.api + +import android.content.Context +import com.dsp.app.data.local.TokenManager +import com.dsp.app.data.model.AuthResponse +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.Authenticator +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.Route +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit + +const val CLIENT_PLATFORM = "android" +const val CLIENT_VERSION = "1.0.0" + +class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val requestBuilder = chain.request().newBuilder() + val token = tokenManager.getAccessToken() + if (!token.isNullOrEmpty()) { + requestBuilder.addHeader("Authorization", "Bearer $token") + } + requestBuilder.addHeader("Accept", "application/json") + // 双端平台标识,后端按此写入 logs_request_log.platform + requestBuilder.addHeader("X-Client-Platform", CLIENT_PLATFORM) + requestBuilder.addHeader("X-Client-Version", CLIENT_VERSION) + return chain.proceed(requestBuilder.build()) + } +} + +/** + * 401 时用 refresh token 无感续期(与 Web 端单飞刷新对齐): + * refresh 失败(过期/无效)返回 null 停止重试,由上层走重新登录。 + */ +class RefreshAuthenticator(private val tokenManager: TokenManager) : Authenticator { + private val refreshClient = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .build() + private val json = Json { ignoreUnknownKeys = true; coerceInputValues = true } + + override fun authenticate(route: Route?, response: Response): Request? { + if (responseCount(response) >= 2) return null + val refresh = tokenManager.getRefreshToken() ?: return null + + val body = """{"refresh":"$refresh"}""" + .toRequestBody("application/json".toMediaType()) + val request = Request.Builder() + .url(ApiClient.BASE_URL + "api/v1/accounts/refresh") + .post(body) + .header("X-Client-Platform", CLIENT_PLATFORM) + .header("X-Client-Version", CLIENT_VERSION) + .build() + + val parsed = try { + refreshClient.newCall(request).execute().use { resp -> + if (!resp.isSuccessful) return null + json.decodeFromString(resp.body!!.string()) + } + } catch (e: Exception) { + return null + } + tokenManager.saveTokens(parsed.access, parsed.refresh) + return response.request.newBuilder() + .header("Authorization", "Bearer ${parsed.access}") + .build() + } + + private fun responseCount(response: Response): Int { + var count = 1 + var prior = response.priorResponse + while (prior != null) { + count++ + prior = prior.priorResponse + } + return count + } +} + +object ApiClient { + // 默认指向 LAN 部署的 nginx 网关(Retrofit 路径自带 api/v1 前缀,基址为站点根) + @Volatile + var BASE_URL = "http://192.168.5.7:19000/" + + private val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + encodeDefaults = true + } + + private var retrofit: Retrofit? = null + private var service: DspApiService? = null + + fun getService(context: Context): DspApiService { + return service ?: synchronized(this) { + val tokenManager = TokenManager(context.applicationContext) + val logging = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + val okHttpClient = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(20, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor(AuthInterceptor(tokenManager)) + .authenticator(RefreshAuthenticator(tokenManager)) + .addInterceptor(logging) + .build() + + val newRetrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + retrofit = newRetrofit + val newService = newRetrofit.create(DspApiService::class.java) + service = newService + newService + } + } + + fun updateBaseUrl(newUrl: String) { + BASE_URL = if (newUrl.endsWith("/")) newUrl else "$newUrl/" + service = null + retrofit = null + } +} diff --git a/dsp/android/app/src/main/java/com/dsp/app/data/api/DspApiService.kt b/dsp/android/app/src/main/java/com/dsp/app/data/api/DspApiService.kt new file mode 100644 index 0000000..01b9bee --- /dev/null +++ b/dsp/android/app/src/main/java/com/dsp/app/data/api/DspApiService.kt @@ -0,0 +1,216 @@ +package com.dsp.app.data.api + +import com.dsp.app.data.model.* +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.Response +import retrofit2.http.* + +interface DspApiService { + + // ==================== 1. 用户系统 ==================== + @POST("api/v1/accounts/register") + suspend fun register(@Body request: RegisterRequest): Response + + @POST("api/v1/accounts/login") + suspend fun login(@Body request: LoginRequest): Response + + @GET("api/v1/accounts/me") + suspend fun getMe(): Response + + @PATCH("api/v1/accounts/me") + suspend fun updateProfile(@Body request: ProfileUpdateRequest): Response + + @GET("api/v1/accounts/{user_id}") + suspend fun getUserDetail(@Path("user_id") userId: Long): Response + + @POST("api/v1/accounts/{user_id}/follow") + suspend fun followUser(@Path("user_id") userId: Long): Response + + @DELETE("api/v1/accounts/{user_id}/follow") + suspend fun unfollowUser(@Path("user_id") userId: Long): Response + + // ==================== 模块 1: 音乐/原声系统 ==================== + @GET("api/v1/music/") + suspend fun getMusics( + @Query("q") query: String? = null, + @Query("page_size") pageSize: Int = 20 + ): Response + + @GET("api/v1/music/{music_id}") + suspend fun getMusicDetail(@Path("music_id") musicId: Long): Response + + @GET("api/v1/music/{music_id}/videos") + suspend fun getMusicVideos( + @Path("music_id") musicId: Long, + @Query("cursor") cursor: String? = null, + @Query("page_size") pageSize: Int = 20 + ): Response + + // ==================== 2. 短视频系统 ==================== + @GET("api/v1/videos/feed") + suspend fun getFeed( + @Query("cursor") cursor: String? = null, + @Query("page_size") pageSize: Int = 20 + ): Response + + @Multipart + @POST("api/v1/videos/") + suspend fun uploadVideo( + @Part("title") title: RequestBody, + @Part("description") description: RequestBody?, + @Part videoFile: MultipartBody.Part, + @Part coverImage: MultipartBody.Part? = null, + @Part("music_id") musicId: RequestBody? = null + ): Response