chore: sync current workspace to dev

This commit is contained in:
qzl
2026-02-24 18:18:42 +08:00
parent 105cf82d21
commit 08571cfc95
79 changed files with 1899 additions and 844 deletions
+18 -10
View File
@@ -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).
+45
View File
@@ -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
+45 -2
View File
@@ -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.
+16
View File
@@ -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.
+28
View File
@@ -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
+14
View File
@@ -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
+44
View File
@@ -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 = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="social_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.social.social_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -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
+26
View File
@@ -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")
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+34
View File
@@ -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
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+616
View File
@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+13
View File
@@ -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)
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -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.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Social App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>social_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+12
View File
@@ -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.
}
}
+23
View File
@@ -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/
+29
View File
@@ -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));
});
}
+1
View File
@@ -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
@@ -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
"""
)
+2 -1
View File
@@ -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(
@@ -0,0 +1,70 @@
# Auth + ProfileSupabase 优先)设计
**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` 字段
@@ -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** |
@@ -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** |
@@ -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)。
### 方案对比(至少两种)
| 方案 | 描述 | 优点 | 缺点 | 结论 |
|------|------|------|------|------|
| 方案 Astdlib logging + 自定义 JSON Formatter | 纯标准库实现 JSON formatter + handler/filters | 依赖最少,符合标准库,易与 Celery/FastAPI 集成 | 结构化上下文绑定与 request_id 传递需手写 | 可作为备选最小方案 |
| 方案 Bstdlib logging + structlog | 用 structlog 生成结构化事件,输出到 logging handler | 结构化上下文与 contextvars 支持好,兼容 logging handler | 引入第三方依赖与配置复杂度 | 推荐主方案 |
| 方案 Cloguru | 直接使用 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** |
@@ -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** |
@@ -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 组件,优先在测试层完成隔离与清理。
+43 -16
View File
@@ -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/workertmux
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 |
+5 -124
View File
@@ -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:
+79
View File
@@ -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"
+31 -3
View File
@@ -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