diff --git a/AGENTS.md b/AGENTS.md index 7554809..54f6807 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,27 @@ -## 目录结构 +## Repository Structure -- `infra/`: 基础设施与运维(Docker、脚本、部署相关) -- `backend/`: FastAPI 后端 -- `apps/`: Flutter 手机端 -- `docs/`: 文档与方案 +- `infra/`: Infrastructure and operations (Docker, scripts, deployment). +- `backend/`: FastAPI backend. +- `apps/`: Flutter mobile app. +- `docs/`: Documentation and design/planning artifacts. -## Agent 规则分层 +## Rules Hierarchy -- 根目录 `AGENTS.md` 为通用规则,所有修改均需遵守 -- 编辑 `backend/` 目录时,必须同时遵守 `backend/AGENTS.md` -- 编辑 `apps/` 目录时,必须同时遵守 `apps/AGENTS.md` +- This root `AGENTS.md` defines global rules and applies to all changes. +- When editing `backend/`, you must also follow `backend/AGENTS.md`. +- When editing `apps/`, you must also follow `apps/AGENTS.md`. -## Docker 启动 +## Docker Startup Always start services with the env file: + ```bash docker compose --env-file .env -f infra/docker/docker-compose.yml up -d ``` + +## Git Branch and Worktree Policy + +- Use `dev` as the default base branch for day-to-day development. +- New development worktrees must be created from `dev` (never from `main`). +- Do not develop or commit directly on `main` outside explicit release/merge workflows. +- Do not rewrite `main` history unless explicitly requested (including reset and force push). diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 51d2f5d..db9b6bc 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -1,4 +1,47 @@ ## Mobile Rules -- Flutter 手机端规则在此维护 -- 若未声明更细规则,默认遵守根目录 `AGENTS.md` +- Flutter mobile rules are maintained here. +- If no more specific rule is defined here, follow the root `AGENTS.md`. + +## Flutter Design-to-Code Workflow + +Before writing any Flutter UI code, follow this sequence: + +1. **Get editor state**: Use `pencil_get_editor_state` to confirm the active design. +2. **Get structure**: Use `pencil_batch_get` to inspect node hierarchy and layout. +3. **Get variables**: Use `pencil_get_variables` to fetch colors, typography, and tokens. +4. **Implement**: Match design values and container hierarchy exactly. + +## Layout Mapping Rules + +Map design layout properties to Flutter explicitly: + +1. **Always set `crossAxisAlignment` on `Row`/`Column`**: + - `alignItems: center` -> `CrossAxisAlignment.center` + - `alignItems: start` -> `CrossAxisAlignment.start` + - `alignItems: stretch` -> `CrossAxisAlignment.stretch` +2. **Map full container chain**: From root to leaf, ensure each `alignItems` and `justifyContent` has a Flutter equivalent. +3. **Analyze before coding**: Use `pencil_snapshot_layout` or `pencil_batch_get` to verify each container's alignment settings. + +## Centering and Visual Balance + +Apply these rules on any screen that relies on centered composition: + +1. Centering must be evaluated inside **`SafeArea` bounds**, not full-screen bounds. +2. Avoid relying on proportional `Spacer` values as the only centering mechanism for critical content. +3. For layouts with persistent top/bottom regions (for example headers or footers), center the primary content in the remaining available region. +4. Distinguish geometric centering from visual centering; validate final visual balance with screenshot review. + +## Quality Gate + +For important screens, add widget tests that reduce layout-regression risk: + +1. Verify primary content remains centered relative to the usable viewport. +2. Add at least one constrained viewport scenario (small height or large text scale). + +## Prohibitions + +- Do not use colors or themes not defined in the design. +- Do not skip design container layers. +- Do not start implementation before retrieving design variables. +- Do not hardcode colors; use design variables. diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..545cb6d --- /dev/null +++ b/apps/README.md @@ -0,0 +1,16 @@ +# social_app + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/apps/analysis_options.yaml b/apps/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/apps/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/apps/android/.gitignore b/apps/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/apps/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts new file mode 100644 index 0000000..f87f188 --- /dev/null +++ b/apps/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.social.social_app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.social.social_app" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/apps/android/app/src/debug/AndroidManifest.xml b/apps/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/apps/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..515ce09 --- /dev/null +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/android/app/src/main/kotlin/com/social/social_app/MainActivity.kt b/apps/android/app/src/main/kotlin/com/social/social_app/MainActivity.kt new file mode 100644 index 0000000..792930d --- /dev/null +++ b/apps/android/app/src/main/kotlin/com/social/social_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.social.social_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/apps/android/app/src/main/res/drawable-v21/launch_background.xml b/apps/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/apps/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/android/app/src/main/res/drawable/launch_background.xml b/apps/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/apps/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/apps/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/apps/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/apps/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/apps/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/apps/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/android/app/src/main/res/values-night/styles.xml b/apps/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/apps/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/android/app/src/main/res/values/styles.xml b/apps/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/apps/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/android/app/src/profile/AndroidManifest.xml b/apps/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/apps/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/android/build.gradle.kts b/apps/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/apps/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/apps/android/gradle.properties b/apps/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/apps/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/apps/android/gradle/wrapper/gradle-wrapper.properties b/apps/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/apps/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/apps/android/settings.gradle.kts b/apps/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/apps/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/apps/assets/images/logo.png b/apps/assets/images/logo.png new file mode 100644 index 0000000..d84a542 Binary files /dev/null and b/apps/assets/images/logo.png differ diff --git a/apps/ios/.gitignore b/apps/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/apps/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/apps/ios/Flutter/AppFrameworkInfo.plist b/apps/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/apps/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/apps/ios/Flutter/Debug.xcconfig b/apps/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/apps/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/apps/ios/Flutter/Release.xcconfig b/apps/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/apps/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/apps/ios/Runner.xcodeproj/project.pbxproj b/apps/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..cb86fcc --- /dev/null +++ b/apps/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.social.socialApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.social.socialApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.social.socialApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.social.socialApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.social.socialApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.social.socialApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/apps/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/apps/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/apps/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/apps/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/apps/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/apps/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/apps/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/apps/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/ios/Runner/AppDelegate.swift b/apps/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/apps/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/apps/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/apps/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/apps/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/Runner/Base.lproj/Main.storyboard b/apps/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/apps/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/Runner/Info.plist b/apps/ios/Runner/Info.plist new file mode 100644 index 0000000..3b51aff --- /dev/null +++ b/apps/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Social App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + social_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/apps/ios/Runner/Runner-Bridging-Header.h b/apps/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/apps/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/apps/ios/RunnerTests/RunnerTests.swift b/apps/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/apps/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/apps/pubspec.yaml b/apps/pubspec.yaml new file mode 100644 index 0000000..0228cae --- /dev/null +++ b/apps/pubspec.yaml @@ -0,0 +1,23 @@ +name: social_app +description: "Social App - A Flutter mobile application" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.10.7 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + equatable: ^2.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true + assets: + - assets/images/ diff --git a/apps/test/widget_test.dart b/apps/test/widget_test.dart new file mode 100644 index 0000000..ee923f4 --- /dev/null +++ b/apps/test/widget_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/main.dart'; + +void main() { + testWidgets('Login screen loads correctly', (WidgetTester tester) async { + await tester.pumpWidget(const LinksyApp()); + expect(find.text('linksy'), findsOneWidget); + expect(find.text('继续'), findsOneWidget); + expect(find.text('还没有账号?去注册'), findsOneWidget); + }); + + testWidgets('Main content is vertically centered above footer', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const LinksyApp()); + + final safeAreaRect = tester.getRect(find.byType(SafeArea)); + final mainRect = tester.getRect( + find.byKey(const Key('login_main_content')), + ); + final footerRect = tester.getRect(find.byKey(const Key('login_footer'))); + + final topSpace = mainRect.top - safeAreaRect.top; + final bottomSpace = footerRect.top - mainRect.bottom; + + expect((topSpace - bottomSpace).abs(), lessThanOrEqualTo(2)); + }); +} diff --git a/backend/Dockerfile b/backend/Dockerfile index cbb0093..f845494 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,6 +8,7 @@ COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev COPY backend/src ./backend/src +COPY backend/alembic ./backend/alembic ENV PYTHONPATH=/app/backend/src diff --git a/backend/alembic/versions/20260224_bind_profiles_to_auth_users.py b/backend/alembic/versions/20260224_bind_profiles_to_auth_users.py new file mode 100644 index 0000000..123d22f --- /dev/null +++ b/backend/alembic/versions/20260224_bind_profiles_to_auth_users.py @@ -0,0 +1,112 @@ +"""bind_profiles_to_auth_users + +Revision ID: 20260224_bind_profiles_auth +Revises: 85d25a191d06 +Create Date: 2026-02-24 17:55:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "20260224_bind_profiles_auth" +down_revision: Union[str, Sequence[str], None] = "85d25a191d06" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Remove orphan profiles that are not backed by auth.users. + # This guarantees FK creation will not fail on historical inconsistent data. + op.execute( + """ + DELETE FROM public.profiles p + WHERE NOT EXISTS ( + SELECT 1 + FROM auth.users u + WHERE u.id = p.id + ) + """ + ) + + # Backfill profile rows for existing auth users. + op.execute( + """ + INSERT INTO public.profiles (id, username, display_name) + SELECT + u.id, + 'user_' || substr(replace(u.id::text, '-', ''), 1, 25), + COALESCE( + NULLIF(u.raw_user_meta_data->>'display_name', ''), + NULLIF(u.raw_user_meta_data->>'full_name', '') + ) + FROM auth.users u + LEFT JOIN public.profiles p ON p.id = u.id + WHERE p.id IS NULL + """ + ) + + # Enforce one-to-one binding between profiles.id and auth.users.id. + op.execute( + """ + ALTER TABLE public.profiles + ADD CONSTRAINT fk_profiles_id_auth_users + FOREIGN KEY (id) REFERENCES auth.users(id) + ON DELETE CASCADE + """ + ) + + # Auto-create profile rows when new auth users are registered. + op.execute( + """ + CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + BEGIN + INSERT INTO public.profiles (id, username, display_name) + VALUES ( + NEW.id, + 'user_' || substr(replace(NEW.id::text, '-', ''), 1, 25), + COALESCE( + NULLIF(NEW.raw_user_meta_data->>'display_name', ''), + NULLIF(NEW.raw_user_meta_data->>'full_name', '') + ) + ) + ON CONFLICT (id) DO NOTHING; + + RETURN NEW; + END; + $$ + """ + ) + + op.execute( + """ + DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users + """ + ) + op.execute( + """ + CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW + EXECUTE FUNCTION public.create_profile_for_new_user() + """ + ) + + +def downgrade() -> None: + op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users") + op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()") + op.execute( + """ + ALTER TABLE public.profiles + DROP CONSTRAINT IF EXISTS fk_profiles_id_auth_users + """ + ) diff --git a/backend/src/models/profile.py b/backend/src/models/profile.py index 8ec69e1..f4d1cb6 100644 --- a/backend/src/models/profile.py +++ b/backend/src/models/profile.py @@ -2,7 +2,7 @@ from __future__ import annotations import uuid -from sqlalchemy import String, Text +from sqlalchemy import ForeignKey, String, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column @@ -21,6 +21,7 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base): id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), + ForeignKey("auth.users.id", ondelete="CASCADE"), primary_key=True, ) username: Mapped[str] = mapped_column( diff --git a/docs/plans/2026-02-24-auth-profile-design.md b/docs/plans/2026-02-24-auth-profile-design.md new file mode 100644 index 0000000..3666394 --- /dev/null +++ b/docs/plans/2026-02-24-auth-profile-design.md @@ -0,0 +1,70 @@ +# Auth + Profile(Supabase 优先)设计 + +**Date:** 2026-02-24 +**Status:** Approved + +## 目标 + +在最大化复用 Supabase Auth 能力前提下,完成以下能力: + +1. 用户注册:`username + email + password` +2. 用户登录:`email + password` +3. 用户按 email 查询(Auth 维度) +4. 用户资料更新(Profile 维度) + +## 范围与原则 + +- 保留并复用现有 `/auth/signup`、`/auth/login`、`/profile/me` 主体能力 +- 仅补齐差距,不重复造轮子 +- 登录标识仅使用 email,`username` 仅用于展示 +- `profiles.username` 允许重复,不加唯一约束 +- 当前为开发阶段,按“无历史数据”处理迁移 + +## 数据模型设计 + +`public.profiles` 字段调整: + +- 保留:`id`, `username`, `avatar_url`, `bio`, `created_at`, `updated_at`, `deleted_at` +- 删除:`display_name` +- `id` 继续绑定 `auth.users.id`(FK + ON DELETE CASCADE) + +## 认证与资料流转 + +### 注册 + +- 请求:`username + email + password` +- 后端调用 Supabase `auth.sign_up`,并将 `username` 写入 `raw_user_meta_data.username` +- 由数据库触发器在 `auth.users` 插入后自动创建 `public.profiles` + - `profiles.id = auth.users.id` + - `profiles.username = raw_user_meta_data.username` + +### 登录 + +- 保持现有 `email + password` 登录,不改协议 + +### 按 email 查找用户 + +- 新增后端接口:`GET /auth/users/by-email?email=...` +- 通过 Supabase Admin 能力查询(后端 service_role) +- 返回最小必要字段(例如 `id/email/created_at/email_confirmed_at`) + +### 资料更新 + +- `PATCH /profile/me` 继续使用 +- 去除 `display_name` 更新项 +- 允许更新:`username/avatar_url/bio` + +## 安全与权限 + +- 按 email 查找接口必须后端受控,避免普通用户枚举 +- 注册与更新时做输入校验(格式、长度) +- 认证继续依赖 Supabase 签发 token,后端负责业务鉴权 + +## 验收标准 + +1. 注册时提交 `username+email+password` 可成功创建 Auth 用户 +2. 注册后自动创建 profile,且 `profiles.username` 等于注册时 `username` +3. 登录保持 `email+password` 可用 +4. `PATCH /profile/me` 可更新 `username/avatar_url/bio` +5. `GET /auth/users/by-email` 返回正确用户或 404 +6. `profiles` 表无 `display_name` 字段 diff --git a/docs/plans/PLAN-base-service-redis-qdrant-2026-02-05.md b/docs/plans/PLAN-base-service-redis-qdrant-2026-02-05.md deleted file mode 100644 index 73c3e9e..0000000 --- a/docs/plans/PLAN-base-service-redis-qdrant-2026-02-05.md +++ /dev/null @@ -1,108 +0,0 @@ -# Plan: Base Service for Redis and Qdrant - -**Date:** 2026-02-05 -**Author:** AI Assistant -**Status:** Draft - -## Overview - -Create a reusable base service module under `backend/src/services/base` that standardizes Redis and Qdrant client creation, lifecycle management, and error handling. Align the design with the DIVA-backend equivalent (once provided) and integrate configuration through existing `SOCIAL_REDIS__*` and `SOCIAL_QDRANT__*` settings. - -## Requirements - -### Functional -- [ ] Provide a base service abstraction that exposes Redis and Qdrant clients to other services. -- [ ] Use async client implementations compatible with FastAPI async execution. -- [ ] Support connection lifecycle hooks (initialize, health check, close). -- [ ] Centralize error handling and translate connection failures to consistent HTTP errors. -- [ ] Mirror DIVA-backend base service features and naming conventions where applicable. - -### Non-Functional -- [ ] Performance: reuse client instances; avoid per-request connection creation. -- [ ] Security: never log secrets (API keys/passwords); enforce TLS settings when enabled. -- [ ] Reliability: implement timeouts and retry policy where supported by client libraries. - -## Technical Approach - -Introduce a `services/base` package that provides a small, composable base class plus Redis/Qdrant client factories. Configuration will be sourced from `core/config/settings.py` using the existing `.env` keys. The base service will accept injected clients to keep testability high and avoid global state, while a module-level factory will handle creation and cleanup. - -### Key Decisions -| Decision | Rationale | -|----------|-----------| -| Use async Redis and Qdrant clients | Matches FastAPI async usage and avoids blocking the event loop. | -| Constructor injection with factories | Keeps services testable and avoids hidden global state. | -| Centralized error mapping in base service | Ensures consistent HTTP 503 responses and logging. | - -## Implementation Steps - -### Phase 1: DIVA-backend Parity Review (1-2 hours) -1. Locate DIVA-backend base service module (path or repo) and document its responsibilities, public API, and lifecycle behavior. -2. Produce a parity checklist to map DIVA behaviors to this repo (naming, error types, retry policy, health checks). - -### Phase 2: Configuration and Client Factories (3 hours) -1. Add `RedisSettings` and `QdrantSettings` sections to `backend/src/core/config/settings.py` using existing `SOCIAL_REDIS__*` and `SOCIAL_QDRANT__*` env keys. -2. Create `backend/src/services/base/redis_client.py` and `backend/src/services/base/qdrant_client.py` with async client factory functions and close helpers. -3. Add structured logging for client initialization, connection failures, and shutdown paths. - -### Phase 3: Base Service Class (3 hours) -1. Create `backend/src/services/base/service.py` with a `BaseService` that accepts optional Redis/Qdrant clients (dependency injection). -2. Add helper methods (e.g., `require_redis()`, `require_qdrant()`) that raise HTTP 503 on unavailable clients. -3. Define error translation utilities for Redis/Qdrant exceptions with consistent messages and logging. - -### Phase 4: Tests (TDD) and Minimal Integration (4 hours) -1. Unit tests for settings parsing and default values (RED/GREEN). -2. Unit tests for base service behavior: missing client errors, exception mapping, and logging context. -3. Integration tests using running Redis/Qdrant containers to verify client factories can connect and execute a simple command. -4. E2E test that exercises a minimal endpoint using the base service (e.g., `/health/infra`), or record an explicit exception if no API integration is allowed. - -## Files to Modify - -| File | Changes | -|------|---------| -| backend/src/core/config/settings.py | Add Redis/Qdrant settings models and defaults. | -| backend/src/app.py | (If needed) register startup/shutdown hooks for client lifecycle. | -| backend/src/v1/router.py | (If needed) add an infra health endpoint to support E2E. | - -## Files to Create - -| File | Purpose | -|------|---------| -| backend/src/services/base/__init__.py | Package export surface for base services. | -| backend/src/services/base/service.py | Base service class for Redis/Qdrant access. | -| backend/src/services/base/redis_client.py | Redis client factory and teardown helpers. | -| backend/src/services/base/qdrant_client.py | Qdrant client factory and teardown helpers. | -| backend/tests/unit/services/base/test_service.py | Unit tests for base service error handling. | -| backend/tests/unit/services/base/test_clients.py | Unit tests for client factory behavior. | -| backend/tests/integration/services/base/test_clients.py | Integration tests with Redis/Qdrant containers. | -| backend/tests/e2e/test_infra_health.py | E2E test for an endpoint using base service. | - -## Dependencies - -- [ ] `redis` (async client) for Redis connectivity. -- [ ] `qdrant-client` for Qdrant connectivity (async/GRPC as configured). -- [ ] No additional infra services required (Redis/Qdrant already in Docker compose). - -## Testing Strategy - -- **Unit Tests:** Base service behavior, missing client errors, exception translation, settings parsing. -- **Integration Tests:** Connect to Redis and Qdrant, run minimal ping/health operations. -- **E2E Tests:** Call a minimal endpoint that uses the base service to validate wiring and error handling. - -## Risks & Mitigations - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|------------|------------| -| DIVA-backend module not available | Medium | High | Add a parity checklist and update plan once module location is provided. | -| Client library mismatch (sync vs async) | Medium | Medium | Select async-supported libraries and verify compatibility in unit tests. | -| Lack of API integration for E2E | High | Medium | Add a minimal infra health endpoint or record a documented exception. | -| Connection config mismatches | Medium | Medium | Validate settings with integration tests and mirror `.env.example`. | - -## Estimated Effort - -| Phase | Effort | -|-------|--------| -| Phase 1 | 2 hours | -| Phase 2 | 3 hours | -| Phase 3 | 3 hours | -| Phase 4 | 4 hours | -| **Total** | **12 hours** | diff --git a/docs/plans/PLAN-env-config-refactor-2026-02-05.md b/docs/plans/PLAN-env-config-refactor-2026-02-05.md deleted file mode 100644 index fef9cde..0000000 --- a/docs/plans/PLAN-env-config-refactor-2026-02-05.md +++ /dev/null @@ -1,143 +0,0 @@ -# Plan: Env Config Refactor - -**Date:** 2026-02-05 -**Author:** AI Assistant -**Status:** Draft - -## Overview - -对 `.env` / `.env.example` 与 `backend/src/core/config/settings.py` 做一次一致性重构,消除同一含义的重复配置来源(例如 `DATABASE_URL` 与分段口令、host/port 与完整 URL)。目标是明确一组规范化环境变量,确保后端仅通过 Settings 读取,并兼顾 `infra/docker/docker-compose.yml` 的现有依赖。 - -## Requirements - -### Functional -- [ ] 提出“规范化环境变量”清单(canonical set),覆盖后端与 Supabase 本地栈的关键配置。 -- [ ] 定义 Settings 的读取与推导策略(优先级、默认值、派生字段)。 -- [ ] 给出 `.env` / `.env.example` 的迁移步骤与兼容策略。 -- [ ] 兼容 `infra/docker/docker-compose.yml` 使用的变量(保证 compose 不被破坏)。 - -### Non-Functional -- [ ] Performance: Settings 解析不增加明显启动耗时 -- [ ] Security: 不在仓库中暴露真实密钥;对后端使用的数据库 URL 与密钥来源保持单一可信源 - -## Technical Approach - -以“后端设置单一来源 + docker-compose 继续使用 Supabase 变量”为原则: -- 后端只接受 `SOCIAL_DATABASE_URL` 与必要的 Supabase 访问变量(`public_url/anon_key/service_role_key/jwt_secret`)。 -- Supabase stack 继续使用 `SOCIAL_SUPABASE__*` 变量,保持 compose 模板稳定。 -- 通过 Settings 做派生字段(例如 `supabase.url`)与兼容性读入(可选旧字段,设置弃用期)。 - -### Key Decisions -| Decision | Rationale | -|----------|-----------| -| 保留 `SOCIAL_DATABASE_URL` 作为后端唯一数据库连接来源 | 避免与分段 `POSTGRES_*` 产生冲突,清晰配置入口 | -| Supabase stack 变量继续使用 `SOCIAL_SUPABASE__*` | docker-compose 已广泛引用,改动成本高 | -| Settings 允许短期兼容旧字段 | 保障迁移期间部署安全,减少切换风险 | - -## Implementation Steps - -### Phase 1: Inventory & Mapping (2 hours) -1. 盘点 `.env` / `.env.example` 与 `settings.py` 的变量差异,标注重复与冲突字段。 -2. 输出 canonical env vars 列表与映射关系表(旧 -> 新)。 - -### Phase 2: Settings Refactor (3 hours) -1. 在 `settings.py` 中实现新的读取优先级与派生字段。 -2. 为旧字段加兼容读取与弃用注记(仅内存兼容,不继续写入)。 - -### Phase 3: Env Templates Update (2 hours) -1. 更新 `.env.example` 为 canonical 变量,并标注“后端使用/compose 使用”。 -2. 更新 `.env`(本地开发用)以匹配新模板。 - -### Phase 4: Validation & Docs (2 hours) -1. 本地启动 docker-compose,验证 Supabase stack 与后端连接正常。 -2. 写简要迁移说明(README 或 docs/ 中短节)。 - -## Files to Modify - -| File | Changes | -|------|---------| -| `.env.example` | 替换为 canonical 变量,移除重复字段 | -| `.env` | 与模板对齐,移除重复字段 | -| `backend/src/core/config/settings.py` | 调整 Settings 读取与派生策略 | -| `infra/docker/docker-compose.yml` | 仅在必要时新增兼容映射变量 | -| `README.md` 或 `docs/*` | 增加迁移说明 | - -## Files to Create - -| File | Purpose | -|------|---------| -| `docs/plans/PLAN-env-config-refactor-2026-02-05.md` | 规划文档 | - -## Dependencies - -- [ ] 无新增第三方依赖 - -## Proposed Canonical Env Vars - -### Backend (Settings) -- `SOCIAL_DATABASE_URL` (required) — 后端数据库连接(唯一来源) -- `SOCIAL_SUPABASE__PUBLIC_URL` — Supabase 公网/本地外部访问 URL -- `SOCIAL_SUPABASE__API_EXTERNAL_URL` — Supabase Auth 回调外部 URL -- `SOCIAL_SUPABASE__ANON_KEY` — 前端/匿名访问 key -- `SOCIAL_SUPABASE__SERVICE_ROLE_KEY` — 后端服务角色 key -- `SOCIAL_SUPABASE__JWT_SECRET` — JWT 验证密钥(后端验证用) - -### Supabase Stack (docker-compose) -- `SOCIAL_SUPABASE__POSTGRES_HOST` -- `SOCIAL_SUPABASE__POSTGRES_PORT` -- `SOCIAL_SUPABASE__POSTGRES_DB` -- `SOCIAL_SUPABASE__POSTGRES_PASSWORD` -- `SOCIAL_SUPABASE__KONG_HTTP_PORT` -- `SOCIAL_SUPABASE__KONG_HTTPS_PORT` -- `SOCIAL_SUPABASE__SITE_URL` -- `SOCIAL_SUPABASE__JWT_SECRET` -- `SOCIAL_SUPABASE__ANON_KEY` -- `SOCIAL_SUPABASE__SERVICE_ROLE_KEY` -- 其余 `SOCIAL_SUPABASE__*` 保持现状(Logflare、SMTP、Pooler 等) - -## Mapping Strategy - -1. **Backend DB** - - Only: `SOCIAL_DATABASE_URL` - - Deprecated: `SOCIAL_SUPABASE__POSTGRES_*`(后端不再拼接) - -2. **Supabase URL** - - Primary: `SOCIAL_SUPABASE__PUBLIC_URL` - - Fallback: `SOCIAL_SUPABASE__API_EXTERNAL_URL` - - Settings 中 `supabase.url` 由上述字段派生 - -3. **Docker Compose** - - 继续读 `SOCIAL_SUPABASE__POSTGRES_*` - - 不引入 `SOCIAL_DATABASE_URL` 到 compose,以免混淆职责 - -## Migration Steps - -1. 在 `settings.py` 中增加兼容逻辑:若 `SOCIAL_DATABASE_URL` 不存在,可临时从 `SOCIAL_SUPABASE__POSTGRES_*` 组装(同时记录弃用)。 -2. 更新 `.env.example`:只保留 canonical 变量并标注用途。 -3. 更新 `.env`:移除重复字段,确保本地后端使用 `SOCIAL_DATABASE_URL`。 -4. 校验 compose:`infra/docker/docker-compose.yml` 不依赖被移除字段。 -5. 发布说明:提示下游用户迁移并在下个版本移除兼容逻辑。 - -## Testing Strategy - -- **Unit Tests:** Settings 派生字段与优先级规则 -- **Integration Tests:** 后端连接 Supabase DB(使用 `SOCIAL_DATABASE_URL`) -- **E2E Tests:** 关键登录/读写流程(确认 JWT 与 Service Role 配置无误) - -## Risks & Mitigations - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|------------|------------| -| compose 变量被误删导致 Supabase 启动失败 | High | Medium | 迁移前后对照 `docker-compose.yml`,保留全部 `SOCIAL_SUPABASE__*` 依赖 | -| 后端数据库连接断开 | High | Medium | 兼容旧字段,先引入新变量再切换 | -| 开发环境 `.env` 未更新 | Medium | High | 更新模板并在 README 明确迁移步骤 | - -## Estimated Effort - -| Phase | Effort | -|-------|--------| -| Phase 1 | 2 hours | -| Phase 2 | 3 hours | -| Phase 3 | 2 hours | -| Phase 4 | 2 hours | -| **Total** | **9 hours** | diff --git a/docs/plans/PLAN-logging-manager-2026-01-29.md b/docs/plans/PLAN-logging-manager-2026-01-29.md deleted file mode 100644 index 197a4de..0000000 --- a/docs/plans/PLAN-logging-manager-2026-01-29.md +++ /dev/null @@ -1,176 +0,0 @@ -# Plan: FastAPI + Celery 日志管理器系统 - -**Date:** 2026-01-29 -**Author:** AI Assistant -**Status:** Draft - -## Overview - -构建一个统一、可扩展的日志管理器系统,覆盖 FastAPI 与 Celery worker 的运行时日志,提供结构化 JSON 输出、错误分离、日志轮转与上下文追踪。目标是满足生产环境可观测性需求,便于检索、关联与故障排查,并与当前项目配置体系保持一致。 - -## Requirements - -### Functional -- [ ] 统一管理 FastAPI 与 Celery worker 日志 -- [ ] 日志持久化到 `logs/`,错误日志单独输出到 `logs/errors/` -- [ ] 支持按大小或按时间进行日志轮转 -- [ ] 结构化日志(JSON),包含时间戳、级别、模块/函数、消息与上下文 -- [ ] ERROR/CRITICAL 记录完整堆栈与错误上下文 -- [ ] 支持环境差异化配置(dev/test/prod) - -### Non-Functional -- [ ] 性能:日志写入对请求延迟影响可控,支持异步队列化扩展 -- [ ] 安全:避免记录敏感信息,支持字段脱敏 -- [ ] 可维护性:模块化、可测试、与现有配置体系一致 - -## Technical Approach - -### 调研摘要 -- Python 官方建议使用 `logging` + `dictConfig` 管理多 handler、多 formatter 与过滤器,适用于生产环境配置化管理。 -- FastAPI 通常通过中间件注入 request_id 和上下文,并使用结构化日志输出以便集中检索。 -- Celery 官方文档建议在自定义场景下关闭 `worker_hijack_root_logger`,通过信号配置自定义 handler。 -- 结构化日志库中,structlog 更贴近标准 logging,可与 `logging` 生态协同;loguru 简化配置但替换性强、与 Celery 深度集成时可控性较弱。 -- 生产环境推荐 JSON 结构化日志 + 轮转 + 错误分离,并通过外部系统聚合与告警(如 Sentry)。 - -### 方案对比(至少两种) -| 方案 | 描述 | 优点 | 缺点 | 结论 | -|------|------|------|------|------| -| 方案 A:stdlib logging + 自定义 JSON Formatter | 纯标准库实现 JSON formatter + handler/filters | 依赖最少,符合标准库,易与 Celery/FastAPI 集成 | 结构化上下文绑定与 request_id 传递需手写 | 可作为备选最小方案 | -| 方案 B:stdlib logging + structlog | 用 structlog 生成结构化事件,输出到 logging handler | 结构化上下文与 contextvars 支持好,兼容 logging handler | 引入第三方依赖与配置复杂度 | 推荐主方案 | -| 方案 C:loguru | 直接使用 loguru logger | 配置简单、体验好 | 与 Celery/标准 logging 生态整合成本高 | 不推荐作为主方案 | - -### 选型结论 -- 采用方案 B:`logging` 作为底座,structlog 负责结构化事件与上下文绑定;保留可切换到方案 A 的最小实现路径。 -- 通过 `dictConfig` 做环境配置,使用 Rotating/TimedRotating handler 支持按大小或时间轮转。 - -## Implementation Steps - -### Phase 1: 基础日志骨架与配置 (3 hours) -1. 新增日志配置模型(Settings 扩展),支持环境、轮转方式与路径配置。 -2. 创建日志模块骨架:formatter、handler、filter、context。 -3. 集成 `dictConfig` 初始化入口,支持 dev/test/prod 配置切换。 - -### Phase 2: FastAPI 集成与上下文 (4 hours) -1. 实现请求中间件:生成 `request_id`,绑定用户与请求上下文(IP、路径、方法)。 -2. 定义异常处理器:捕获未处理异常并记录堆栈与上下文。 -3. 添加应用启动时日志初始化流程。 - -### Phase 3: Celery 集成 (3 hours) -1. 在 Celery 应用配置中设置 `worker_hijack_root_logger = False`。 -2. 使用 Celery 信号(`setup_logging`、`after_setup_task_logger`)初始化日志并注入 task 上下文。 -3. 统一日志格式、error 处理与 request_id 关联(如 task_id)。 - -### Phase 4: 错误分离与轮转策略 (3 hours) -1. 添加 error handler:仅接受 ERROR/CRITICAL,输出到 `logs/errors/`。 -2. 实现轮转策略配置(按大小、按时间),并提供统一切换配置项。 -3. 增加字段脱敏与敏感字段黑名单过滤器。 - -### Phase 5: 可选增强功能 (4 hours) -1. 日志查询与过滤接口(基础 API + 分页)。 -2. 日志聚合统计(按级别/模块/时间窗口)。 -3. Sentry 集成与异常告警。 - -## Files to Modify - -| File | Changes | -|------|---------| -| backend/src/core/config/settings.py | 扩展日志相关配置模型 | - -## Files to Create - -| File | Purpose | -|------|---------| -| backend/src/core/logging/__init__.py | 模块导出与初始化入口 | -| backend/src/core/logging/config.py | dictConfig 构建与环境配置 | -| backend/src/core/logging/formatters.py | JSON formatter 与字段规范 | -| backend/src/core/logging/handlers.py | 文件、控制台、错误 handler | -| backend/src/core/logging/filters.py | 等级过滤、敏感字段脱敏 | -| backend/src/core/logging/context.py | contextvars 绑定与获取 | -| backend/src/core/logging/middleware.py | FastAPI 请求中间件 | -| backend/src/core/logging/celery.py | Celery 日志信号集成 | -| backend/src/core/logging/examples.py | 使用示例(可选) | - -## Dependencies - -- [ ] structlog: 结构化日志与 contextvars 支持 -- [ ] python-json-logger(备选): 若需要纯 logging JSON formatter -- [ ] sentry-sdk(可选): 异常告警与追踪 - -## 配置示例 - -```toml -# .env 示例(通过 pydantic settings 读取) -SOCIAL_RUNTIME__LOG_LEVEL=INFO -SOCIAL_RUNTIME__LOG_JSON=true -SOCIAL_RUNTIME__LOG_ROTATION=TIME -SOCIAL_RUNTIME__LOG_ROTATION_WHEN=midnight -SOCIAL_RUNTIME__LOG_ROTATION_BACKUP_COUNT=14 -SOCIAL_RUNTIME__LOG_DIR=logs -SOCIAL_RUNTIME__LOG_ERROR_DIR=logs/errors -``` - -## 使用示例代码 - -```python -from core.logging import configure_logging, get_logger - -configure_logging() -logger = get_logger(__name__) - -logger.info("user login", extra={"user_id": "u_123"}) -``` - -## Testing Strategy - -- **Unit Tests:** formatter 输出结构、filter 脱敏规则、context 绑定行为 -- **Integration Tests:** FastAPI 中间件注入的 request_id 与错误分离写入 -- **E2E Tests:** 关键流程触发错误,验证 error 日志输出与轮转 - -## Test Database 约定 - -### Supabase 组件能力范围 - -- Supabase 的 Auth/Storage/Realtime 等组件是独立服务,默认指向同一个主数据库。 -- 单独创建一个“测试数据库”(Postgres database)并不会自动获得这些组件的能力,除非显式为这些服务配置新的数据库连接。 -- 因此,“测试数据库”默认只具备纯 Postgres 能力;Supabase 组件能力仍然作用在主数据库上。 - -### 对测试的影响 - -- **只走直连数据库的测试**(如通过 SQLAlchemy/psycopg 直连)不会受影响。 -- **依赖 Supabase 组件的测试**(例如通过 Auth/Storage/Realtime API)会仍然落到主数据库,可能导致: - - 测试数据污染主库 - - 并发测试互相干扰 -- 若需要 Supabase 组件也“指向测试数据库”,需要启动一套独立 Supabase 栈或重新配置各服务连接(通常不建议在同一栈内动态切换)。 - -### 环境变量与自动创建 - -- 建议为“测试数据库”提供独立环境变量(仅测试环境读取),例如: - - `SOCIAL_TEST_DATABASE__HOST` - - `SOCIAL_TEST_DATABASE__PORT` - - `SOCIAL_TEST_DATABASE__NAME` - - `SOCIAL_TEST_DATABASE__USER` - - `SOCIAL_TEST_DATABASE__PASSWORD` -- 若使用 Docker 启动 Postgres,建议在容器初始化阶段自动创建测试数据库(避免手动创建): - - 通过 `docker-entrypoint-initdb.d` 的 init SQL 脚本创建测试数据库与权限 - - 保证容器重建后自动恢复测试数据库 -- 若使用独立 Supabase 栈做测试,测试环境变量应指向该栈的数据库与服务端口。 - -## Risks & Mitigations - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|------------|------------| -| Celery 日志被自动劫持导致重复或丢失 | High | Medium | 设置 `worker_hijack_root_logger=False` 并通过信号统一配置 | -| 结构化字段不一致导致下游解析失败 | Medium | Medium | 统一 schema,增加单元测试与校验 | -| 误记录敏感信息 | High | Medium | 增加脱敏过滤器与字段黑名单 | -| 日志量过大影响性能 | Medium | Medium | 轮转 + 级别控制 + 可选异步队列化 | - -## Estimated Effort - -| Phase | Effort | -|-------|--------| -| Phase 1 | 3 hours | -| Phase 2 | 4 hours | -| Phase 3 | 3 hours | -| Phase 4 | 3 hours | -| Phase 5 | 4 hours | -| **Total** | **17 hours** | diff --git a/docs/plans/PLAN-supabase-compose-base-services-2026-02-05.md b/docs/plans/PLAN-supabase-compose-base-services-2026-02-05.md deleted file mode 100644 index 6957d5b..0000000 --- a/docs/plans/PLAN-supabase-compose-base-services-2026-02-05.md +++ /dev/null @@ -1,113 +0,0 @@ -# Plan: Merge Supabase Compose and Base Services - -**Date:** 2026-02-05 -**Author:** AI Assistant -**Status:** Draft - -## Overview - -Integrate Supabase Docker services into the project's `infra/docker/docker-compose.yml` and align all environment variables with the project's `.env` conventions. Add reusable BaseRepository and BaseService abstractions (soft-delete filtering and auth/user validation) and refactor profile/auth services to use them, with full TDD coverage. - -## Requirements - -### Functional -- [ ] Merge Supabase Docker Compose services into `infra/docker/docker-compose.yml` using project `.env` variable names. -- [ ] Update `.env.example` to include all required Supabase compose variables. -- [ ] Implement BaseRepository with standard soft-delete filtering (excludes `deleted_at` rows by default). -- [ ] Implement BaseService with shared auth/user validation helpers. -- [ ] Refactor profile repository/service and auth service to use BaseRepository/BaseService. -- [ ] Add unit, integration, and E2E tests following TDD. - -### Non-Functional -- [ ] Performance: keep repository queries indexed and avoid extra round-trips. -- [ ] Security: validate user identity consistently; no secrets in repo; no bypass of auth checks. -- [ ] Compatibility: keep Supabase config compatible with existing `Settings` and `.env` prefixes. - -## Technical Approach - -Introduce small, reusable base classes in `backend/src/core` for repository and service concerns, then refactor profile and auth modules to leverage them. Merge the Supabase compose services from the official template into `infra/docker/docker-compose.yml`, mapping variables to `SOCIAL_SUPABASE__*` and related infra keys already used in `backend/src/core/config/settings.py`. - -### Key Decisions -| Decision | Rationale | -|----------|-----------| -| BaseRepository provides a `base_select()` or `apply_soft_delete_filter()` | Avoid duplicated `deleted_at` filters and enforce consistent behavior. | -| BaseService handles user validation helpers | Keeps auth checks consistent across services and reduces duplicated error handling. | -| Compose variables aligned to `SOCIAL_*` prefixes | Matches existing settings resolution and simplifies local/dev parity. | - -## Implementation Steps - -### Phase 1: Compose Merge and Env Alignment (3 hours) -1. Identify the Supabase Docker Compose template to merge (official Supabase Docker template) and list required services and env vars. -2. Merge Supabase services into `infra/docker/docker-compose.yml`, keeping existing Redis/Qdrant services intact and aligning ports/volumes. -3. Map Supabase compose env variables to project `.env` names (e.g., `SOCIAL_SUPABASE__*`, `SOCIAL_INFRA__*` where needed). -4. Update `.env.example` with all required Supabase-related variables, keeping comments updated for local vs. cloud usage. -5. Add/adjust docker compose healthchecks or depends_on as needed for startup ordering. - -### Phase 2: BaseRepository and BaseService (4 hours) -1. Add `backend/src/core/db/repository.py` (or `backend/src/core/repository/base.py`) with a BaseRepository that applies `SoftDeleteMixin` filters by default. -2. Add `backend/src/core/services/base.py` with BaseService helpers for current user validation (e.g., `require_user`, `require_user_id`). -3. Add unit tests for BaseRepository soft delete filtering and BaseService auth validation (TDD red/green). - -### Phase 3: Refactor Profile/Auth (4 hours) -1. Refactor `backend/src/v1/profile/repository.py` to inherit from BaseRepository and remove duplicated `deleted_at` logic. -2. Refactor `backend/src/v1/profile/service.py` to inherit from BaseService and use shared validation helpers where applicable. -3. Refactor `backend/src/v1/auth/service.py` to adopt BaseService helpers for user validation (where applicable) and keep gateway contract unchanged. -4. Update unit tests for profile and auth services to reflect base class usage and ensure behavior unchanged. - -### Phase 4: Integration/E2E Tests and Hardening (4 hours) -1. Add integration tests for repository soft delete behavior using SQLAlchemy session fixtures. -2. Add or update E2E tests for profile flow to ensure auth/user validation still enforced. -3. Run coverage check (80%+), fix gaps, and verify CI pre-commit tooling passes. - -## Files to Modify - -| File | Changes | -|------|---------| -| infra/docker/docker-compose.yml | Merge Supabase services; map env vars to `SOCIAL_*`. | -| .env.example | Add Supabase compose variables and update comments. | -| backend/src/v1/profile/repository.py | Inherit BaseRepository; simplify soft delete filtering. | -| backend/src/v1/profile/service.py | Inherit BaseService; use shared validation helpers. | -| backend/src/v1/auth/service.py | Use BaseService helpers where applicable. | -| backend/tests/unit/v1/profile/* | Update tests for BaseRepository/BaseService. | -| backend/tests/unit/v1/auth/* | Update tests for base service helpers (if needed). | -| backend/tests/integration/* | Add/adjust tests for soft delete filtering. | -| backend/tests/e2e/* | Update/extend critical auth/profile flow tests. | - -## Files to Create - -| File | Purpose | -|------|---------| -| backend/src/core/db/repository.py | BaseRepository with soft-delete filtering. | -| backend/src/core/services/base.py | BaseService with auth/user validation helpers. | -| backend/tests/unit/core/db/test_base_repository.py | Unit tests for soft delete filters. | -| backend/tests/unit/core/services/test_base_service.py | Unit tests for auth/user validation. | - -## Dependencies - -- [ ] Supabase official Docker Compose template (source of services/env vars). -- [ ] No new Python dependencies expected. - -## Testing Strategy - -- **Unit Tests:** BaseRepository soft-delete filter logic; BaseService user validation helpers; updated profile/auth service behavior. -- **Integration Tests:** SQLAlchemy queries exclude soft-deleted rows; profile endpoints still return expected responses. -- **E2E Tests:** Critical profile flow with authenticated user; verify unauthorized access remains blocked. - -## Risks & Mitigations - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|------------|------------| -| Missing or outdated Supabase compose template | Medium | Medium | Pin to official template version and document source in plan. | -| Env var mismatches break local auth or DB connections | High | Medium | Add validation checklist and update `.env.example` with exact mappings. | -| BaseRepository changes alter query behavior | Medium | Medium | Add unit/integration tests and verify no regressions. | -| Auth validation refactor introduces regressions | High | Low | TDD with unit + E2E tests; keep behavior parity. | - -## Estimated Effort - -| Phase | Effort | -|-------|--------| -| Phase 1 | 3 hours | -| Phase 2 | 4 hours | -| Phase 3 | 4 hours | -| Phase 4 | 4 hours | -| **Total** | **15 hours** | diff --git a/docs/plans/PLAN-test-db-isolation-2026-02-05.md b/docs/plans/PLAN-test-db-isolation-2026-02-05.md deleted file mode 100644 index 5ab73e2..0000000 --- a/docs/plans/PLAN-test-db-isolation-2026-02-05.md +++ /dev/null @@ -1,148 +0,0 @@ -# 测试数据隔离方案(Supabase + Python 后端) - -## 背景现状 - -- 后端在 `backend/src/core/config/settings.py` 使用 `SOCIAL_DATABASE__*` 生成 `database_url`。 -- 本地 Supabase 通过 `supabase-db` 容器提供 Postgres,宿主端口由 `SOCIAL_DATABASE__PORT` 控制(默认映射到容器 5432)。 -- 注意:`supabase-pooler` 的 5432 仅用于连接池;测试与迁移应直连 `supabase-db` 的宿主端口。 -- 单元数据库测试目前使用 SQLite 内存库(见 `tests/unit/database/*`),不影响开发库。 -- 真实 Postgres 的集成/E2E 当前未统一隔离策略;当开始接入真实 DB 时,需要按本文方案隔离与清理。 - -## 目标 - -- 测试过程不污染开发数据。 -- 测试可重复、可并行、可在本地与 CI 稳定运行。 -- 变更成本可控,优先在现有架构上落地。 - -## 结论(适配本项目) - -采用“事务回滚 + 独立测试数据库”的混合策略: - -- 默认测试使用事务回滚,快速、零污染(适用于单连接/单事务场景)。 -- 需要真实提交、并发或触发器行为的测试使用独立测试数据库。 - -## 方案设计 - -### A. 事务回滚(默认) - -适用:单元测试、绝大多数集成测试(当这些测试连接真实 Postgres 时)。 - -核心思路: - -- 每个测试在事务中运行。 -- 测试结束自动回滚。 - -优点: - -- 速度快,无需新增数据库。 -- 测试间完全隔离。 - -限制: - -- 无法验证真实 COMMIT 结果。 -- 并发、多连接事务隔离测试不准确。 - -### B. 独立测试数据库(E2E/并发) - -适用:E2E、并发、触发器、LISTEN/NOTIFY 等需要真实提交的场景。 - -核心思路: - -- 在现有 Supabase Postgres 实例中创建独立数据库。 -- 测试使用专用 `SOCIAL_DATABASE__NAME` 连接,端口/账号/密码来自 `SOCIAL_DATABASE__*`。 -- 测试前应用迁移,测试后清理。 - -优点: - -- 行为最接近真实环境。 -- 与开发数据完全隔离。 - -成本: - -- 需要迁移与清理策略。 - -## 与现有测试模块的衔接 - -- `tests/unit/database/*` 已使用 SQLite 内存库,无需改造。 -- 未来若 `tests/integration/*` 或 `tests/e2e/*` 连接真实 Postgres,应切换到本文的测试库策略。 -- 使用 `SOCIAL_DATABASE__NAME=postgres_test` 启动测试,以避免污染开发库。 - -## 实施步骤(与项目当前结构对齐) - -### 1) 创建独立测试数据库 - -在本地 Supabase 容器中创建测试库: - -```bash -docker exec -e PGPASSWORD="$SOCIAL_DATABASE__PASSWORD" supabase-db \ - psql -U "$SOCIAL_DATABASE__USER" -c "CREATE DATABASE postgres_test;" -``` - -说明: - -- 容器名为 `supabase-db`(已在 `infra/docker` 运行)。 -- 数据库名建议 `postgres_test`,与 `.env` 的 `SOCIAL_DATABASE__NAME=postgres` 区分。 - -### 2) 运行迁移到测试库 - -使用测试环境变量指向测试库后,应用 Alembic 迁移: - -```bash -SOCIAL_RUNTIME__ENVIRONMENT=test \ -SOCIAL_DATABASE__NAME=postgres_test \ -uv run alembic upgrade head -``` - -说明: - -- 执行位置:`/home/qzl/Code/social-app/backend`。 -- 仍使用当前 `.env` 中的 `SOCIAL_DATABASE__HOST` 与 `SOCIAL_DATABASE__PORT`。 - -### 3) 事务回滚测试(默认) - -测试执行时注入事务回滚机制: - -- 在测试会话层创建单连接事务。 -- 对每个测试用例使用 SAVEPOINT(或嵌套事务)。 -- 测试结束回滚到 SAVEPOINT。 - -这套策略可保持速度与隔离性,同时不需要额外数据库。 - -### 4) 独立测试数据库执行(E2E/并发) - -对于需要真实提交的测试,使用测试库运行: - -```bash -SOCIAL_RUNTIME__ENVIRONMENT=test \ -SOCIAL_DATABASE__NAME=postgres_test \ -uv run pytest tests/e2e -``` - -清理策略(二选一): - -- 小规模测试:TRUNCATE public schema 的业务表(不影响 `auth` 等系统 schema)。 -- 大规模测试:`DROP DATABASE postgres_test;` 后重建并迁移。 - -### 5) 本地/CI 统一策略 - -- 本地默认:事务回滚。 -- CI:独立测试库(保证完全隔离、无隐式依赖)。 - -## 风险与规避 - -- 不要在清理时操作 `auth`、`storage` 等 Supabase 系统 schema。 -- E2E 使用独立数据库,避免与开发数据交叉。 -- 迁移必须由 Alembic 统一维护,禁止手动改库。 - -## 落地检查清单 - -- [ ] 已创建 `postgres_test` 数据库。 -- [ ] 测试库迁移已应用。 -- [ ] 事务回滚测试已接入(默认路径)。 -- [ ] E2E 使用测试库运行。 -- [ ] 清理策略执行脚本可复用。 - -## 备注 - -本方案基于当前项目的 Supabase 本地 Docker 结构与后端配置方式(`SOCIAL_DATABASE__*`)。 -无需变更 Supabase 组件,优先在测试层完成隔离与清理。 diff --git a/docs/runtime/runtime-runbook.md b/docs/runtime/runtime-runbook.md index 23fb489..6ccf92f 100644 --- a/docs/runtime/runtime-runbook.md +++ b/docs/runtime/runtime-runbook.md @@ -8,21 +8,30 @@ ### 一键启动 (推荐) ```bash -# 使用一键启动脚本 -./infra/scripts/start.sh +# 前提:基础设施已手动启动(redis + supabase) +# docker compose --env-file .env -f infra/docker/docker-compose.yml up -d + +# 一键执行 bootstrap + 拉起 web/worker(tmux) +bash infra/scripts/dev-app-up.sh + +# 查看窗口 +tmux list-windows -t social-dev + +# 进入会话观察日志 +tmux attach -t social-dev ``` 或者手动执行: ```bash -# 1. 启动基础设施 -docker compose --env-file .env -f infra/docker/docker-compose.yml up -d redis db +# 1. 启动基础设施(当前编排不包含 web/worker) +docker compose --env-file .env -f infra/docker/docker-compose.yml up -d # 2. 运行迁移和初始化 -docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm init-job +docker compose --env-file .env -f infra/docker/docker-compose.yml --profile job run --rm init-job -# 3. 启动 Web 和 Worker -docker compose --env-file .env -f infra/docker/docker-compose.yml up -d web worker-critical worker-default worker-bulk +# 3. 一键执行应用层启动(bootstrap + web + workers) +bash infra/scripts/dev-app-up.sh ``` ### 本地 CLI (开发调试) @@ -42,15 +51,25 @@ PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info - PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=bulk --concurrency=1 ``` +### tmux 会话管理 + +```bash +# 进入会话 +tmux attach -t social-dev + +# 杀掉会话(停止 web/workers) +tmux kill-session -t social-dev +``` + ## 服务说明 -| 服务 | 说明 | 队列 | +| 服务 | 说明 | 备注 | |------|------|------| -| web | Web 服务 (gunicorn) | - | -| worker-critical | 关键任务 worker | critical | -| worker-default | 默认任务 worker | default | -| worker-bulk | 批量任务 worker | bulk | -| init-job | 数据库迁移和初始化 | - | +| redis | 缓存与 Celery broker | docker-compose 编排 | +| supabase-* | 认证与数据库相关服务 | docker-compose 编排 | +| init-job | 数据库迁移和初始化 | docker-compose 按需 run | +| web | Web 服务 (gunicorn) | 本地 CLI 启动 | +| worker-* | Celery worker | 本地 CLI 启动 | ## 配置说明 @@ -79,15 +98,21 @@ PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info - ## 健康检查 ```bash -curl -fsS http://127.0.0.1:8000/health +# Supabase 网关 +curl -fsS http://127.0.0.1:${SOCIAL_SUPABASE__KONG_HTTP_PORT:-8000}/health + +# 数据库迁移与初始化 +docker compose --env-file .env -f infra/docker/docker-compose.yml --profile job run --rm init-job ``` ## 查看服务状态 ```bash docker compose --env-file .env -f infra/docker/docker-compose.yml ps -docker compose --env-file .env -f infra/docker/docker-compose.yml logs -f web -docker compose --env-file .env -f infra/docker/docker-compose.yml logs -f worker-critical +docker compose --env-file .env -f infra/docker/docker-compose.yml logs -f db + +# init-job 为一次性任务(run --rm),如需查看日志请重跑: +docker compose --env-file .env -f infra/docker/docker-compose.yml --profile job run --rm init-job ``` --- @@ -98,3 +123,5 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml logs -f worker |------|------| | 2026-02-24 | 创建运行时手册,删除 legacy 脚本,统一使用 gunicorn | | 2026-02-24 | 清理配置:合并 AppSettings 到 WebSettings,删除 Worker 旧配置 (enabled_queues/queues),统一使用 SOCIAL_WEB__GUNICORN__* 命名 | +| 2026-02-24 | 开发阶段 compose 暂不编排 web/worker,仅保留 redis/supabase 与 init-job | +| 2026-02-24 | 新增 dev-app-up 脚本:手动基础设施后,一键 bootstrap + tmux 拉起 web/worker | diff --git a/infra/docker/docker-compose.yml b/infra/docker/docker-compose.yml index 5d91108..7e32552 100644 --- a/infra/docker/docker-compose.yml +++ b/infra/docker/docker-compose.yml @@ -389,130 +389,9 @@ services: command: ["/bin/sh", "-c", "/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"] - web: - build: - context: ../.. - dockerfile: backend/Dockerfile - image: social-local-backend - container_name: social-local-web - restart: unless-stopped - ports: - - "${SOCIAL_WEB__PORT:-8000}:8000" - environment: - - PYTHONPATH=/app/backend/src - - SOCIAL_DATABASE__HOST=db - - SOCIAL_DATABASE__PORT=5432 - - SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD} - - SOCIAL_REDIS__HOST=redis - - SOCIAL_REDIS__PORT=6379 - - SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-} - - SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY} - - SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY} - - SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET} - - SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev} - - SOCIAL_WEB__HOST=${SOCIAL_WEB__HOST:-0.0.0.0} - - SOCIAL_WEB__PORT=${SOCIAL_WEB__PORT:-8000} - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - working_dir: /app/backend - command: > - sh -c "uv run gunicorn app:app --bind ${SOCIAL_WEB__HOST:-0.0.0.0}:${SOCIAL_WEB__PORT:-8000} --workers $${SOCIAL_WEB__GUNICORN__WORKERS:-2} --worker-class $${SOCIAL_WEB__GUNICORN__WORKER_CLASS:-uvicorn.workers.UvicornWorker} --timeout $${SOCIAL_WEB__GUNICORN__TIMEOUT:-60}" - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 15s - - worker-critical: - build: - context: ../.. - dockerfile: backend/Dockerfile - image: social-local-backend - container_name: social-local-worker-critical - restart: unless-stopped - environment: - - PYTHONPATH=/app/backend/src - - SOCIAL_DATABASE__HOST=db - - SOCIAL_DATABASE__PORT=5432 - - SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD} - - SOCIAL_REDIS__HOST=redis - - SOCIAL_REDIS__PORT=6379 - - SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-} - - SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY} - - SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY} - - SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET} - - SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev} - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - working_dir: /app/backend - command: uv run celery -A core.celery.app worker --loglevel=info --queues=critical --concurrency=2 - profiles: - - worker - - worker-default: - build: - context: ../.. - dockerfile: backend/Dockerfile - image: social-local-backend - container_name: social-local-worker-default - restart: unless-stopped - environment: - - PYTHONPATH=/app/backend/src - - SOCIAL_DATABASE__HOST=db - - SOCIAL_DATABASE__PORT=5432 - - SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD} - - SOCIAL_REDIS__HOST=redis - - SOCIAL_REDIS__PORT=6379 - - SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-} - - SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY} - - SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY} - - SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET} - - SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev} - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - working_dir: /app/backend - command: uv run celery -A core.celery.app worker --loglevel=info --queues=default --concurrency=2 - profiles: - - worker - - worker-bulk: - build: - context: ../.. - dockerfile: backend/Dockerfile - image: social-local-backend - container_name: social-local-worker-bulk - restart: unless-stopped - environment: - - PYTHONPATH=/app/backend/src - - SOCIAL_DATABASE__HOST=db - - SOCIAL_DATABASE__PORT=5432 - - SOCIAL_DATABASE__PASSWORD=${SOCIAL_DATABASE__PASSWORD} - - SOCIAL_REDIS__HOST=redis - - SOCIAL_REDIS__PORT=6379 - - SOCIAL_REDIS__PASSWORD=${SOCIAL_REDIS__PASSWORD:-} - - SOCIAL_SUPABASE__ANON_KEY=${SOCIAL_SUPABASE__ANON_KEY} - - SOCIAL_SUPABASE__SERVICE_ROLE_KEY=${SOCIAL_SUPABASE__SERVICE_ROLE_KEY} - - SOCIAL_SUPABASE__JWT_SECRET=${SOCIAL_SUPABASE__JWT_SECRET} - - SOCIAL_RUNTIME__ENVIRONMENT=${SOCIAL_RUNTIME__ENVIRONMENT:-dev} - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - working_dir: /app/backend - command: uv run celery -A core.celery.app worker --loglevel=info --queues=bulk --concurrency=1 - profiles: - - worker + # 开发阶段暂时禁用业务镜像(web/worker)。 + # 如需恢复,请从 git 历史恢复以下服务定义:web, worker-critical, + # worker-default, worker-bulk。 init-job: build: @@ -538,6 +417,8 @@ services: condition: service_healthy working_dir: /app/backend command: uv run python -m core.runtime.cli bootstrap + profiles: + - job volumes: redis_data: diff --git a/infra/scripts/dev-app-up.sh b/infra/scripts/dev-app-up.sh new file mode 100755 index 0000000..39ec2b6 --- /dev/null +++ b/infra/scripts/dev-app-up.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +SESSION_NAME="${SESSION_NAME:-social-dev}" +COMPOSE_FILE="$ROOT_DIR/infra/docker/docker-compose.yml" +ENV_FILE="$ROOT_DIR/.env" + +echo "=== Dev App Up ===" +echo "This script assumes redis/supabase are already running via docker compose." +echo "" + +if ! command -v tmux >/dev/null 2>&1; then + echo "Error: tmux is required." >&2 + exit 1 +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: docker is required." >&2 + exit 1 +fi + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: env file not found at $ENV_FILE" >&2 + exit 1 +fi + +if [ ! -f "$COMPOSE_FILE" ]; then + echo "Error: compose file not found at $COMPOSE_FILE" >&2 + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +. "$ENV_FILE" +set +a + +if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then + echo "Error: tmux session '$SESSION_NAME' already exists." >&2 + echo "Hint: tmux kill-session -t $SESSION_NAME" >&2 + exit 1 +fi + +echo "[1/2] Running bootstrap once (migrate + init-data)..." +running_services="$(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" ps --status running --services || true)" +if ! printf '%s\n' "$running_services" | grep -qx "db"; then + echo "Error: db service is not running. Start infra first." >&2 + echo "Hint: docker compose --env-file .env -f infra/docker/docker-compose.yml up -d" >&2 + exit 1 +fi +if ! printf '%s\n' "$running_services" | grep -qx "redis"; then + echo "Error: redis service is not running. Start infra first." >&2 + echo "Hint: docker compose --env-file .env -f infra/docker/docker-compose.yml up -d" >&2 + exit 1 +fi + +docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" --profile job run --rm init-job + +echo "[2/2] Starting web + worker processes in tmux session '$SESSION_NAME'..." + +WEB_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src uv run gunicorn app:app --bind \ +${SOCIAL_WEB__HOST:-0.0.0.0}:${SOCIAL_WEB__PORT:-8000} --workers \ +${SOCIAL_WEB__GUNICORN__WORKERS:-2} --worker-class \ +${SOCIAL_WEB__GUNICORN__WORKER_CLASS:-uvicorn.workers.UvicornWorker} --timeout \ +${SOCIAL_WEB__GUNICORN__TIMEOUT:-60}" + +WORKER_CRITICAL_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=critical --concurrency=${SOCIAL_WORKER__GROUPS__CRITICAL__CONCURRENCY:-2}" +WORKER_DEFAULT_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=default --concurrency=${SOCIAL_WORKER__GROUPS__DEFAULT__CONCURRENCY:-2}" +WORKER_BULK_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src uv run celery -A core.celery.app worker --loglevel=info --queues=bulk --concurrency=${SOCIAL_WORKER__GROUPS__BULK__CONCURRENCY:-1}" + +tmux new-session -d -s "$SESSION_NAME" -n web "bash -lc \"$WEB_CMD; echo '[web] exited'; exec bash\"" +tmux new-window -t "$SESSION_NAME" -n worker-critical "bash -lc \"$WORKER_CRITICAL_CMD; echo '[worker-critical] exited'; exec bash\"" +tmux new-window -t "$SESSION_NAME" -n worker-default "bash -lc \"$WORKER_DEFAULT_CMD; echo '[worker-default] exited'; exec bash\"" +tmux new-window -t "$SESSION_NAME" -n worker-bulk "bash -lc \"$WORKER_BULK_CMD; echo '[worker-bulk] exited'; exec bash\"" + +echo "" +echo "=== Dev App Started ===" +echo "tmux attach -t $SESSION_NAME" +echo "tmux list-windows -t $SESSION_NAME" diff --git a/infra/scripts/runtime-bootstrap-gate.sh b/infra/scripts/runtime-bootstrap-gate.sh index fd4a4d6..3701b07 100755 --- a/infra/scripts/runtime-bootstrap-gate.sh +++ b/infra/scripts/runtime-bootstrap-gate.sh @@ -1,15 +1,43 @@ #!/bin/bash set -euo pipefail -COMPOSE_FILE="infra/docker/docker-compose.yml" -ENV_FILE=".env" +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +COMPOSE_FILE="$ROOT_DIR/infra/docker/docker-compose.yml" +ENV_FILE="$ROOT_DIR/.env" + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: env file not found at $ENV_FILE" >&2 + exit 1 +fi + +if [ ! -f "$COMPOSE_FILE" ]; then + echo "Error: compose file not found at $COMPOSE_FILE" >&2 + exit 1 +fi + +required_services=(init-job web worker-critical worker-default worker-bulk redis db) +available_services="$(docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" --profile job config --services)" + +missing_services=() +for service in "${required_services[@]}"; do + if ! printf '%s\n' "$available_services" | grep -qx "$service"; then + missing_services+=("$service") + fi +done + +if [ "${#missing_services[@]}" -gt 0 ]; then + echo "Error: runtime bootstrap gate requires services not found in compose:" >&2 + printf ' - %s\n' "${missing_services[@]}" >&2 + echo "Hint: this gate is for deployment compose that includes web/worker/init-job." >&2 + exit 1 +fi echo "=== Runtime Bootstrap Gate ===" echo "This is the ONLY allowed entry point for deployment." echo "" echo "[1/2] Running bootstrap (migrate + init-data)..." -docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" run --rm init-job bootstrap +docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" --profile job run --rm init-job echo "[2/2] Starting web and worker services..." docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d web worker-critical worker-default worker-bulk redis db