init
This commit is contained in:
210
server/README.md
Normal file
210
server/README.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 正念 APP(后端)
|
||||
|
||||
本目录为正念 APP 的后端服务(FastAPI),负责:
|
||||
|
||||
- 提供业务 API(情绪文案/卡片、用户配置、推送配置等)
|
||||
- 支撑定时任务(推送、内容刷新等)
|
||||
- 对接数据库与缓存(MySQL / Redis)
|
||||
|
||||
> 说明:当前 `server/` 目录仅包含说明文档,尚未初始化具体代码与依赖文件。本文档按“推荐工程形态”编写,等代码落地后只需把入口模块名与配置字段名对齐即可。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python + FastAPI
|
||||
- SQLAlchemy(建议 2.x,异步)+ MySQL 8.0
|
||||
- Alembic(数据库迁移)
|
||||
- Celery + Redis(任务调度/定时推送)
|
||||
- Pydantic v2(数据校验)+ pydantic-settings(环境配置)
|
||||
- pytest(测试)
|
||||
|
||||
## 本地开发
|
||||
|
||||
### 1. 环境要求
|
||||
|
||||
- Python:3.11+
|
||||
- MySQL:8.0+
|
||||
|
||||
### 2. 创建虚拟环境
|
||||
|
||||
在 `server/` 目录下:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install -U pip
|
||||
```
|
||||
|
||||
### 3. 安装依赖
|
||||
|
||||
|
||||
- 方式 :`requirements.txt`(示例)
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 配置(dev / prod)
|
||||
|
||||
推荐使用 `.env.dev` 与 `.env.prod`,由 `pydantic-settings` 读取。
|
||||
|
||||
### 1. 环境变量示例
|
||||
|
||||
在 `server/` 下创建 `.env.dev`(示例字段,可按代码中的 Settings 字段名调整):
|
||||
|
||||
```dotenv
|
||||
# 运行环境
|
||||
APP_ENV=dev
|
||||
APP_NAME=mindfulness-server
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
|
||||
# 数据库(推荐用统一的 DATABASE_URL,或拆分为 MYSQL_*)
|
||||
DATABASE_URL=mysql+aiomysql://<用户名>:<密码>@<MySQL_HOST>:3306/mindfulness_dev?charset=utf8mb4
|
||||
# MYSQL_HOST=127.0.0.1
|
||||
# MYSQL_PORT=3306
|
||||
# MYSQL_USER=root
|
||||
# MYSQL_PASSWORD=password
|
||||
# MYSQL_DB=mindfulness_dev
|
||||
|
||||
# Redis(缓存/任务队列)
|
||||
REDIS_URL=redis://dev_user:devpassword@127.0.0.1:6379/0
|
||||
最大的存储限制是256 请尽量少使用
|
||||
# Celery(建议先只用 broker;如需结果存储请务必设置 TTL)
|
||||
CELERY_BROKER_URL=redis://dev_user:devpassword@127.0.0.1:6379/0
|
||||
# CELERY_RESULT_BACKEND=redis://dev_user:devpassword@127.0.0.1:6379/0
|
||||
|
||||
# 推送/第三方(按实际接入补充)
|
||||
# PUSH_PROVIDER=expo
|
||||
# EXPO_ACCESS_TOKEN=
|
||||
```
|
||||
|
||||
`.env.prod` 同理,替换为生产环境地址与密钥即可。
|
||||
|
||||
### 1.1 MySQL 命名与 dev/pro 区分(约定)
|
||||
|
||||
- **生产库(prod)**:`mindfulness`
|
||||
- **开发库(dev)**:`mindfulness_dev`
|
||||
|
||||
建议:
|
||||
|
||||
- **库名/表名/字段名**:统一小写 + 下划线(snake_case)
|
||||
- **字符集**:统一 `utf8mb4`
|
||||
|
||||
`.env.dev` 指向 `mindfulness_dev`,`.env.prod` 指向 `mindfulness`:
|
||||
|
||||
```dotenv
|
||||
# prod 示例
|
||||
DATABASE_URL=mysql+aiomysql://<用户名>:<密码>@<MySQL_HOST>:3306/mindfulness?charset=utf8mb4
|
||||
```
|
||||
|
||||
### 1.2 Redis:通过 ACL 限制 dev/pro 前缀访问
|
||||
|
||||
只启动 **一个 Redis 服务**,用 **ACL + key 前缀**隔离环境数据:
|
||||
|
||||
```text
|
||||
# Dev 用户,只能访问 dev:* 前缀
|
||||
user dev_user on >devpassword ~dev:* +@all
|
||||
|
||||
# Pro 用户,只能访问 pro:* 前缀
|
||||
user pro_user on >propassword ~pro:* +@all
|
||||
```
|
||||
|
||||
连接串示例(dev/pro 使用不同账号):
|
||||
|
||||
```dotenv
|
||||
# dev
|
||||
REDIS_URL=redis://dev_user:devpassword@127.0.0.1:6379/0
|
||||
CELERY_BROKER_URL=redis://dev_user:devpassword@127.0.0.1:6379/0
|
||||
|
||||
# prod
|
||||
# REDIS_URL=redis://pro_user:propassword@127.0.0.1:6379/0
|
||||
# CELERY_BROKER_URL=redis://pro_user:propassword@127.0.0.1:6379/0
|
||||
```
|
||||
|
||||
重要约定:
|
||||
|
||||
- **应用写入 Redis 的 key 必须带前缀**:dev 环境使用 `dev:`,prod 环境使用 `pro:`
|
||||
- **Celery 队列/键也建议带环境前缀**(例如队列名 `dev:push` / `pro:push`),避免同机多环境混用时相互影响
|
||||
|
||||
### 2. 配置加载约定(建议)
|
||||
|
||||
- 通过 `APP_ENV` 决定加载 `.env.dev` 或 `.env.prod`
|
||||
- 代码中使用 `pydantic-settings` 定义 `Settings`,并集中在 `app/core/config.py`(仅建议命名)
|
||||
|
||||
## 启动服务(FastAPI)
|
||||
|
||||
> 下方入口模块名为示例:假设你的 FastAPI 实例位于 `app/main.py` 中的 `app` 变量。若实际路径不同,请自行替换。
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
启动后可访问:
|
||||
|
||||
- OpenAPI 文档:`/docs`
|
||||
- ReDoc:`/redoc`
|
||||
|
||||
## 数据库迁移(Alembic)
|
||||
|
||||
> 若你采用 Alembic:建议把迁移脚本放在 `server/alembic/`,并在 `alembic.ini` 中配置数据库连接(或从环境变量读取)。
|
||||
|
||||
常用命令(示例):
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate -m "初始化表结构"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
## 任务调度(Celery + Redis)
|
||||
|
||||
> 下方入口模块名为示例:假设 Celery 实例位于 `app/worker.py` 中的 `celery_app` 变量。若实际路径不同,请自行替换。
|
||||
|
||||
启动 Worker(处理异步任务):
|
||||
|
||||
```bash
|
||||
celery -A app.worker:celery_app worker -l info
|
||||
```
|
||||
|
||||
启动 Beat(定时任务调度,若你使用 celery beat):
|
||||
|
||||
```bash
|
||||
celery -A app.worker:celery_app beat -l info
|
||||
```
|
||||
|
||||
## 运行测试(pytest)
|
||||
|
||||
```bash
|
||||
pytest -q
|
||||
```
|
||||
|
||||
## 推荐目录结构(建议)
|
||||
|
||||
> 仅为建议,便于后续协作与扩展。
|
||||
|
||||
```text
|
||||
server/
|
||||
app/
|
||||
main.py # FastAPI 入口(app = FastAPI())
|
||||
core/
|
||||
config.py # Settings(pydantic-settings)
|
||||
logging.py
|
||||
db/
|
||||
session.py # 数据库会话/引擎
|
||||
models/ # ORM 模型
|
||||
migrations/ # (如不使用默认 alembic 目录,可自定义)
|
||||
api/
|
||||
v1/
|
||||
routes/
|
||||
tasks/
|
||||
push.py # 推送相关任务
|
||||
worker.py # Celery 入口(celery_app = Celery(...))
|
||||
tests/
|
||||
README.md
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **MySQL 连接报错/字符集问题**:建议统一使用 `utf8mb4`,并在连接串中带上 `charset=utf8mb4`。
|
||||
- **Celery 无法连接 Redis**:检查 `CELERY_BROKER_URL` 与 Redis 是否可达、端口是否开放。
|
||||
6
server/app/__init__.py
Normal file
6
server/app/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
后端应用包入口。
|
||||
|
||||
说明:目前仅提供项目初始化骨架,后续会逐步补齐业务模块。
|
||||
"""
|
||||
|
||||
71
server/app/core/config.py
Normal file
71
server/app/core/config.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
def _guess_env_file(app_env: str) -> Optional[str]:
|
||||
"""
|
||||
根据 APP_ENV 选择 env 文件。
|
||||
|
||||
说明:
|
||||
- `.env.dev` / `.env.prod` 不进入仓库,通常由运维在部署时放到 `server/` 目录
|
||||
- 如果文件不存在,则返回 None,让系统从“真实环境变量”读取
|
||||
"""
|
||||
|
||||
base_dir = Path(__file__).resolve().parents[2] # .../server/app -> .../server
|
||||
candidates = {
|
||||
"dev": base_dir / ".env.dev",
|
||||
"prod": base_dir / ".env.prod",
|
||||
}
|
||||
p = candidates.get(app_env)
|
||||
if p and p.exists():
|
||||
return str(p)
|
||||
return None
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
应用配置(统一从环境变量读取)。
|
||||
|
||||
注意:请不要把真实账号密码写入仓库;使用 `.env.dev/.env.prod` 或部署系统注入。
|
||||
"""
|
||||
|
||||
app_env: Literal["dev", "prod"] = "dev"
|
||||
app_name: str = "mindfulness-server"
|
||||
app_host: str = "0.0.0.0"
|
||||
app_port: int = 8000
|
||||
|
||||
database_url: str
|
||||
|
||||
redis_url: str
|
||||
celery_broker_url: str
|
||||
celery_result_backend: Optional[str] = None
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="",
|
||||
case_sensitive=False,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
"""
|
||||
获取配置(带缓存)。
|
||||
|
||||
加载顺序:
|
||||
- 优先使用 `.env.dev/.env.prod`(若存在)
|
||||
- 否则使用系统环境变量
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
env = os.getenv("APP_ENV", "dev").strip() or "dev"
|
||||
env_file = _guess_env_file(env)
|
||||
|
||||
return Settings(_env_file=env_file)
|
||||
|
||||
6
server/app/db/base.py
Normal file
6
server/app/db/base.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""SQLAlchemy ORM Base。"""
|
||||
|
||||
36
server/app/db/session.py
Normal file
36
server/app/db/session.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def create_engine():
|
||||
"""
|
||||
创建异步 Engine。
|
||||
|
||||
注意:连接串从环境变量 `DATABASE_URL` 读取。
|
||||
"""
|
||||
|
||||
settings = get_settings()
|
||||
return create_async_engine(settings.database_url, pool_pre_ping=True)
|
||||
|
||||
|
||||
engine = create_engine()
|
||||
|
||||
AsyncSessionLocal: async_sessionmaker[AsyncSession] = async_sessionmaker(
|
||||
bind=engine,
|
||||
expire_on_commit=False,
|
||||
autoflush=False,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""
|
||||
FastAPI 依赖:获取数据库会话。
|
||||
"""
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
|
||||
25
server/app/main.py
Normal file
25
server/app/main.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""
|
||||
创建 FastAPI 应用实例。
|
||||
|
||||
说明:目前仅提供最小可运行骨架(health check),后续逐步加入路由与中间件。
|
||||
"""
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
app = FastAPI(title=settings.app_name)
|
||||
|
||||
@app.get("/healthz")
|
||||
async def healthz() -> dict:
|
||||
return {"status": "ok", "env": settings.app_env}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
4
server/app/tasks/__init__.py
Normal file
4
server/app/tasks/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Celery 任务集合。
|
||||
"""
|
||||
|
||||
13
server/app/tasks/ping.py
Normal file
13
server/app/tasks/ping.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task(name="tasks.ping")
|
||||
def ping(msg: str = "pong") -> str:
|
||||
"""
|
||||
示例任务:用于验证 Celery worker 可消费任务。
|
||||
|
||||
注意:任务参数请尽量保持小(只传 id/短文本),避免 Redis 队列膨胀。
|
||||
"""
|
||||
|
||||
return msg
|
||||
|
||||
43
server/app/worker.py
Normal file
43
server/app/worker.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from celery import Celery
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def _env_prefix(app_env: str) -> str:
|
||||
"""
|
||||
根据环境生成前缀:
|
||||
- dev -> dev
|
||||
- prod -> pro
|
||||
|
||||
说明:Redis ACL 限制使用 `dev:*` / `pro:*`。
|
||||
"""
|
||||
|
||||
return "dev" if app_env == "dev" else "pro"
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
prefix = _env_prefix(settings.app_env)
|
||||
|
||||
# 关键:使用 Redis transport 的全局 key 前缀,确保所有 broker key 都在 ACL 允许范围内
|
||||
broker_transport_options = {"global_keyprefix": f"{prefix}:"}
|
||||
|
||||
celery_app = Celery(
|
||||
"mindfulness",
|
||||
broker=settings.celery_broker_url,
|
||||
backend=settings.celery_result_backend,
|
||||
broker_transport_options=broker_transport_options,
|
||||
)
|
||||
|
||||
# 默认不存结果(降低 Redis 占用)。如需结果存储,可在业务中显式开启并设置 TTL。
|
||||
celery_app.conf.update(
|
||||
task_ignore_result=True if not settings.celery_result_backend else False,
|
||||
task_default_queue=f"{prefix}:celery",
|
||||
task_default_exchange=f"{prefix}:celery",
|
||||
task_default_routing_key=f"{prefix}:celery",
|
||||
)
|
||||
|
||||
# 自动发现任务(app/tasks 下的 shared_task)
|
||||
celery_app.autodiscover_tasks(["app.tasks"])
|
||||
|
||||
20
server/env.example
Normal file
20
server/env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# 运行环境:dev 或 prod
|
||||
APP_ENV=dev
|
||||
|
||||
# Web 服务
|
||||
APP_NAME=mindfulness-server
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
|
||||
# 数据库(dev 指向 mindfulness_dev;prod 指向 mindfulness)
|
||||
DATABASE_URL=mysql+aiomysql://<用户名>:<密码>@<MYSQL_HOST>:3306/mindfulness_dev?charset=utf8mb4
|
||||
|
||||
# Redis(使用 ACL 用户;并确保应用侧 key 带 dev:/pro: 前缀)
|
||||
REDIS_URL=redis://<REDIS_USER>:<REDIS_PASSWORD>@<REDIS_HOST>:6379/0
|
||||
|
||||
# Celery(默认不启用结果存储,避免 Redis 内存压力)
|
||||
CELERY_BROKER_URL=redis://<REDIS_USER>:<REDIS_PASSWORD>@<REDIS_HOST>:6379/0
|
||||
# CELERY_RESULT_BACKEND=redis://<REDIS_USER>:<REDIS_PASSWORD>@<REDIS_HOST>:6379/0
|
||||
|
||||
# 推送(Expo)
|
||||
# EXPO_ACCESS_TOKEN=
|
||||
20
server/requirements.txt
Normal file
20
server/requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
fastapi>=0.110
|
||||
uvicorn[standard]>=0.27
|
||||
|
||||
# 数据库(SQLAlchemy 2.x 异步 + MySQL)
|
||||
SQLAlchemy>=2.0
|
||||
aiomysql>=0.2
|
||||
|
||||
# 配置
|
||||
pydantic>=2.6
|
||||
pydantic-settings>=2.2
|
||||
|
||||
# 迁移
|
||||
alembic>=1.13
|
||||
|
||||
# 任务调度
|
||||
celery>=5.3
|
||||
redis>=5.0
|
||||
|
||||
# 测试
|
||||
pytest>=8.0
|
||||
Reference in New Issue
Block a user