create-dwy 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/create.js +50 -3
- package/src/utils.js +4 -0
- package/templates/claude-global/CLAUDE.md +81 -6
- package/templates/claude-global/rules/dwy-postgres.md +89 -0
- package/templates/claude-global/rules/dwy-python-backend.md +492 -0
- package/templates/claude-global/rules/dwy-python-core.md +94 -634
- package/templates/claude-global/rules/dwy-python-testing.md +115 -13
- package/templates/claude-global/rules/dwy-redis.md +88 -0
- package/templates/claude-global/rules/dwy-vue-core.md +341 -645
- package/templates/claude-global/rules/dwy-vue-pinia.md +47 -34
- package/templates/claude-global/rules/dwy-vue-testing.md +120 -55
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +1 -1
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +1 -1
- package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +22 -15
- package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +23 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +100 -1
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +142 -276
- package/templates/claude-global/skills/dwy-eui/SKILL.md +264 -182
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +117 -129
- package/templates/claude-global/skills/dwy-git-commit/SKILL.md +125 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -1
- package/templates/claude-global/{rules/dwy-tech-stack.md → skills/dwy-tech-stack/SKILL.md} +4 -3
- package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +81 -0
- package/templates/project/fullstack-monorepo/backend/.python-version +1 -0
- package/templates/project/fullstack-monorepo/backend/CLAUDE.md.hbs +68 -0
- package/templates/project/fullstack-monorepo/backend/Dockerfile.dev +23 -0
- package/templates/project/fullstack-monorepo/backend/alembic/env.py.hbs +68 -0
- package/templates/project/fullstack-monorepo/backend/alembic/script.py.mako +26 -0
- package/templates/project/fullstack-monorepo/backend/alembic.ini.hbs +43 -0
- package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/pyproject.toml +16 -0
- package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/src/dwy_shared/__init__.py +12 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/pyproject.toml.hbs +20 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/__init__.py.hbs +40 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/config.py +9 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/models.py.hbs +17 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/protocols.py +14 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/router.py.hbs +22 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/schemas.py +21 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/service.py +11 -0
- package/templates/project/fullstack-monorepo/backend/pyproject.toml.hbs +84 -0
- package/templates/project/fullstack-monorepo/backend/src/app/__init__.py +3 -0
- package/templates/project/fullstack-monorepo/backend/src/app/config.py.hbs +21 -0
- package/templates/project/fullstack-monorepo/backend/src/app/exceptions.py +26 -0
- package/templates/project/fullstack-monorepo/backend/src/app/factories.py +16 -0
- package/templates/project/fullstack-monorepo/backend/src/app/infra/__init__.py +1 -0
- package/templates/project/fullstack-monorepo/backend/src/app/main.py.hbs +65 -0
- package/templates/project/fullstack-monorepo/backend/src/app/middleware/__init__.py +1 -0
- package/templates/project/fullstack-monorepo/backend/tests/__init__.py +0 -0
- package/templates/project/fullstack-monorepo/backend/tests/conftest.py.hbs +42 -0
- package/templates/project/fullstack-monorepo/dev.sh.hbs +83 -0
- package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +36 -0
- package/templates/project/fullstack-monorepo/frontend/CLAUDE.md.hbs +35 -0
- package/templates/project/fullstack-monorepo/frontend/Dockerfile +18 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/index.html.hbs +13 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/package.json.hbs +31 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/App.vue.hbs +10 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/api/client.ts.hbs +11 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/features/{{initialDomain}}/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/index.css +12 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/main.ts +17 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/router/index.ts.hbs +24 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/stores/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/types/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/views/HomeView.vue.hbs +36 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/tsconfig.json +7 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/vite.config.ts.hbs +27 -0
- package/templates/project/fullstack-monorepo/frontend/nginx.conf +11 -0
- package/templates/project/fullstack-monorepo/frontend/package.json.hbs +13 -0
- package/templates/project/fullstack-monorepo/frontend/packages/core/package.json.hbs +15 -0
- package/templates/project/fullstack-monorepo/frontend/packages/core/src/index.ts +3 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/package.json.hbs +14 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/index.ts.hbs +16 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/pages/IndexPage.vue.hbs +9 -0
- package/templates/project/fullstack-monorepo/frontend/packages/ui/package.json.hbs +15 -0
- package/templates/project/fullstack-monorepo/frontend/packages/ui/src/index.ts +3 -0
- package/templates/project/fullstack-monorepo/frontend/pnpm-workspace.yaml.hbs +23 -0
- package/templates/project/fullstack-monorepo/frontend/tsconfig.base.json.hbs +23 -0
- package/templates/claude-global/rules/dwy-api-design.md +0 -110
- package/templates/claude-global/rules/dwy-git-commit-convention.md +0 -82
- package/templates/claude-global/rules/dwy-git-security.md +0 -53
- package/templates/claude-global/rules/dwy-payload-limits.md +0 -186
- package/templates/claude-global/rules/dwy-prefer-open-source.md +0 -32
- package/templates/claude-global/rules/dwy-project-structure.md +0 -181
- package/templates/claude-global/rules/dwy-python-fastapi.md +0 -272
- package/templates/claude-global/rules/dwy-python-orm.md +0 -61
- package/templates/claude-global/rules/dwy-python-security.md +0 -636
- package/templates/claude-global/rules/dwy-server-security.md +0 -518
- package/templates/claude-global/rules/dwy-vue-api.md +0 -146
- package/templates/claude-global/skills/dwy-ekit/references/data.md +0 -135
- package/templates/claude-global/skills/dwy-ekit/references/form.md +0 -68
- package/templates/claude-global/skills/dwy-ekit/references/request.md +0 -183
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +0 -109
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +0 -890
- /package/templates/{claude-global/commands → project/fullstack-monorepo/backend/alembic/versions}/.gitkeep +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# {{projectName}} backend
|
|
2
|
+
|
|
3
|
+
FastAPI 后端,uv workspace monorepo。
|
|
4
|
+
|
|
5
|
+
## 架构分层
|
|
6
|
+
|
|
7
|
+
- **装配层** `src/app/` -- 只做装配:加载配置、初始化基础设施、注册业务包。**不写业务逻辑**
|
|
8
|
+
- **业务包** `packages/<scope>-<domain>/` -- 业务实现,域分包,每个包独立 `pyproject.toml`
|
|
9
|
+
|
|
10
|
+
## 域分包五件套
|
|
11
|
+
|
|
12
|
+
每个业务包目录结构:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
packages/{{scopePrefix}}-<domain>/
|
|
16
|
+
src/{{scopePrefix}}_<domain>/
|
|
17
|
+
__init__.py # register(app, *, config, **providers) 入口
|
|
18
|
+
config.py # 包级 DomainConfig(嵌入装配层 Settings)
|
|
19
|
+
router.py # APIRouter,只做参数校验 + service 调用
|
|
20
|
+
service.py # 业务逻辑,依赖通过 get_config() / get_provider() 注入
|
|
21
|
+
schemas.py # Pydantic Request/Response
|
|
22
|
+
models.py # SQLAlchemy ORM(可选)
|
|
23
|
+
protocols.py # 外部服务 Protocol(SmsProvider / OssProvider 等)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Provider 模式
|
|
27
|
+
|
|
28
|
+
外部服务(数据库、Redis、SMS、OSS 等)抽象成 Protocol,具体实现由装配层 `src/app/factories.py` 按 `settings` 选择(dev=in-memory,prod=real),`main.py` 通过 `register(...)` 传给业务包。业务代码只依赖 Protocol 不依赖具体实现。
|
|
29
|
+
|
|
30
|
+
## dwyeapi 优先
|
|
31
|
+
|
|
32
|
+
数据库/Redis/JWT/响应封装/异常处理一律先用 `dwyeapi`,不重复造轮子。
|
|
33
|
+
|
|
34
|
+
## 加新业务包
|
|
35
|
+
|
|
36
|
+
1. 复制 `packages/{{scopePrefix}}-{{initialDomain}}/` 改名为 `{{scopePrefix}}-<newdomain>/`
|
|
37
|
+
2. 改 `pyproject.toml` 的 `name`,改包内 `__init__.py` 的导出
|
|
38
|
+
3. 根 `pyproject.toml` 的 `[tool.uv.sources]` 增加 workspace 引用
|
|
39
|
+
4. `src/app/main.py` 增加 `register_<newdomain>(app, config=settings.<newdomain>, ...)`
|
|
40
|
+
5. `src/app/config.py` 嵌入 `<newdomain>: <Newdomain>Config = ...`
|
|
41
|
+
6. `alembic/env.py` 增加 `import {{scopePrefix}}_<newdomain>.models`
|
|
42
|
+
|
|
43
|
+
## 常用命令
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 安装
|
|
47
|
+
uv sync --extra dev
|
|
48
|
+
|
|
49
|
+
# 启动 dev
|
|
50
|
+
uv run uvicorn app.main:app --reload --port {{portPrefix}}8001
|
|
51
|
+
|
|
52
|
+
# 测试
|
|
53
|
+
uv run pytest tests/ -v
|
|
54
|
+
|
|
55
|
+
# Lint
|
|
56
|
+
uv run ruff check src/ packages/
|
|
57
|
+
uv run ruff format src/ packages/
|
|
58
|
+
|
|
59
|
+
# 迁移
|
|
60
|
+
uv run alembic revision --autogenerate -m "msg"
|
|
61
|
+
uv run alembic upgrade head
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 服务端口约定
|
|
65
|
+
|
|
66
|
+
- Backend: `{{portPrefix}}8001`
|
|
67
|
+
- PostgreSQL: `{{portPrefix}}5432`
|
|
68
|
+
- Redis: `{{portPrefix}}6379`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
FROM python:3.11.15-slim
|
|
2
|
+
|
|
3
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
4
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
5
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
6
|
+
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
# 安装 uv
|
|
10
|
+
RUN pip install --no-cache-dir uv
|
|
11
|
+
|
|
12
|
+
# 复制 workspace 配置(利用 Docker 缓存)
|
|
13
|
+
COPY pyproject.toml ./
|
|
14
|
+
COPY packages ./packages
|
|
15
|
+
COPY src ./src
|
|
16
|
+
|
|
17
|
+
# 同步依赖(含 dev 组)
|
|
18
|
+
RUN uv sync --extra dev
|
|
19
|
+
|
|
20
|
+
# 容器内端口固定 8001,外部映射由 docker-compose 处理
|
|
21
|
+
EXPOSE 8001
|
|
22
|
+
|
|
23
|
+
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Alembic 环境配置 -- 从 app.config 读取数据库 URL。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from logging.config import fileConfig
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import pool
|
|
7
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
8
|
+
|
|
9
|
+
# 导入所有 packages 的 models,让 Alembic 能发现表定义。
|
|
10
|
+
# dwy_shared 暂未提供 models,占位 import 用于将来扩展时只在此处追加。
|
|
11
|
+
import dwy_shared # noqa: F401
|
|
12
|
+
import {{scopePrefix}}_{{initialDomain}}.models # noqa: F401
|
|
13
|
+
from alembic import context
|
|
14
|
+
from app.config import settings
|
|
15
|
+
from dwyeapi.database import Base
|
|
16
|
+
|
|
17
|
+
# Alembic Config
|
|
18
|
+
config = context.config
|
|
19
|
+
if config.config_file_name is not None:
|
|
20
|
+
fileConfig(config.config_file_name)
|
|
21
|
+
|
|
22
|
+
# 使用 app.config 中的数据库 URL
|
|
23
|
+
config.set_main_option("sqlalchemy.url", settings.database_url)
|
|
24
|
+
|
|
25
|
+
target_metadata = Base.metadata
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_migrations_offline() -> None:
|
|
29
|
+
"""离线模式运行迁移。"""
|
|
30
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
31
|
+
context.configure(
|
|
32
|
+
url=url,
|
|
33
|
+
target_metadata=target_metadata,
|
|
34
|
+
literal_binds=True,
|
|
35
|
+
dialect_opts={"paramstyle": "named"},
|
|
36
|
+
)
|
|
37
|
+
with context.begin_transaction():
|
|
38
|
+
context.run_migrations()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def do_run_migrations(connection) -> None:
|
|
42
|
+
"""在给定连接上运行迁移。"""
|
|
43
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
44
|
+
with context.begin_transaction():
|
|
45
|
+
context.run_migrations()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def run_async_migrations() -> None:
|
|
49
|
+
"""异步模式运行迁移。"""
|
|
50
|
+
connectable = async_engine_from_config(
|
|
51
|
+
config.get_section(config.config_ini_section, {}),
|
|
52
|
+
prefix="sqlalchemy.",
|
|
53
|
+
poolclass=pool.NullPool,
|
|
54
|
+
)
|
|
55
|
+
async with connectable.connect() as connection:
|
|
56
|
+
await connection.run_sync(do_run_migrations)
|
|
57
|
+
await connectable.dispose()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_migrations_online() -> None:
|
|
61
|
+
"""在线模式运行迁移。"""
|
|
62
|
+
asyncio.run(run_async_migrations())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if context.is_offline_mode():
|
|
66
|
+
run_migrations_offline()
|
|
67
|
+
else:
|
|
68
|
+
run_migrations_online()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
${upgrades if upgrades else "pass"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Alembic 配置 -- sqlalchemy.url 由 env.py 从 app.config.settings 注入,这里留空。
|
|
2
|
+
[alembic]
|
|
3
|
+
script_location = alembic
|
|
4
|
+
prepend_sys_path = . src
|
|
5
|
+
path_separator = os
|
|
6
|
+
version_path_separator = os
|
|
7
|
+
sqlalchemy.url =
|
|
8
|
+
|
|
9
|
+
[post_write_hooks]
|
|
10
|
+
|
|
11
|
+
[loggers]
|
|
12
|
+
keys = root,sqlalchemy,alembic
|
|
13
|
+
|
|
14
|
+
[handlers]
|
|
15
|
+
keys = console
|
|
16
|
+
|
|
17
|
+
[formatters]
|
|
18
|
+
keys = generic
|
|
19
|
+
|
|
20
|
+
[logger_root]
|
|
21
|
+
level = WARN
|
|
22
|
+
handlers = console
|
|
23
|
+
qualname =
|
|
24
|
+
|
|
25
|
+
[logger_sqlalchemy]
|
|
26
|
+
level = WARN
|
|
27
|
+
handlers =
|
|
28
|
+
qualname = sqlalchemy.engine
|
|
29
|
+
|
|
30
|
+
[logger_alembic]
|
|
31
|
+
level = INFO
|
|
32
|
+
handlers =
|
|
33
|
+
qualname = alembic
|
|
34
|
+
|
|
35
|
+
[handler_console]
|
|
36
|
+
class = StreamHandler
|
|
37
|
+
args = (sys.stderr,)
|
|
38
|
+
level = NOTSET
|
|
39
|
+
formatter = generic
|
|
40
|
+
|
|
41
|
+
[formatter_generic]
|
|
42
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
43
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dwy-shared"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "跨项目通用 kernel(业务无关基础设施)"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
# 依赖仅声明包名,版本号由根 pyproject.toml 统一管理
|
|
7
|
+
dependencies = [
|
|
8
|
+
"dwyeapi",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/dwy_shared"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{scopePrefix}}-{{initialDomain}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{initialDomain}} 业务包"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
# 依赖仅声明包名,版本号由根 pyproject.toml 统一管理
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastapi",
|
|
9
|
+
"sqlalchemy",
|
|
10
|
+
"pydantic",
|
|
11
|
+
"dwyeapi",
|
|
12
|
+
"dwy-shared",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/{{scopePrefix}}_{{initialDomain}}"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""{{initialDomain}} 业务包入口。
|
|
2
|
+
|
|
3
|
+
外部调用约定:
|
|
4
|
+
from {{scopePrefix}}_{{initialDomain}} import register
|
|
5
|
+
register(app, config=settings.{{initialDomain}}, **providers)
|
|
6
|
+
|
|
7
|
+
包内通过 get_config() / get_provider() 拿到装配层注入的依赖,
|
|
8
|
+
不直接 import app.config(避免循环依赖)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
|
|
13
|
+
from .config import DomainConfig
|
|
14
|
+
from .router import router
|
|
15
|
+
from .schemas import HealthResponse
|
|
16
|
+
|
|
17
|
+
__all__ = ["DomainConfig", "HealthResponse", "get_config", "register", "router"]
|
|
18
|
+
|
|
19
|
+
# 模块级状态:装配层 register() 注入,业务代码通过 get_config() 读取
|
|
20
|
+
_config: DomainConfig | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(app: FastAPI, *, config: DomainConfig, **providers: object) -> None:
|
|
24
|
+
"""装载本业务包到 FastAPI 应用。
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
app: FastAPI 实例。
|
|
28
|
+
config: 业务包配置(由装配层从 Settings 传入)。
|
|
29
|
+
**providers: 外部 Provider(如 sms_provider/oss_provider 等)。
|
|
30
|
+
"""
|
|
31
|
+
global _config
|
|
32
|
+
_config = config
|
|
33
|
+
app.include_router(router)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_config() -> DomainConfig:
|
|
37
|
+
"""业务代码调用:获取本包配置。"""
|
|
38
|
+
if _config is None:
|
|
39
|
+
raise RuntimeError("{{scopePrefix}}_{{initialDomain}} 未调用 register(),配置不可用")
|
|
40
|
+
return _config
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""{{initialDomain}} ORM 模型。
|
|
2
|
+
|
|
3
|
+
在此定义 SQLAlchemy 模型,Alembic 通过 alembic/env.py 的 import 发现表。
|
|
4
|
+
|
|
5
|
+
示例:
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
from dwyeapi.database import Base, TimestampMixin
|
|
8
|
+
|
|
9
|
+
class Item(Base, TimestampMixin):
|
|
10
|
+
__tablename__ = "{{initialDomain}}_item"
|
|
11
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
12
|
+
name: Mapped[str] = mapped_column(index=True)
|
|
13
|
+
|
|
14
|
+
模板初始不定义任何模型,避免 alembic autogenerate 生成业务无关表。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""外部服务 Protocol 定义。
|
|
2
|
+
|
|
3
|
+
业务代码只依赖此处定义的 Protocol,具体实现由装配层 src/app/factories.py
|
|
4
|
+
按 settings.environment 选择(dev=in-memory,prod=real)。
|
|
5
|
+
|
|
6
|
+
示例:
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
class SmsProvider(Protocol):
|
|
10
|
+
async def send(self, phone: str, message: str) -> None: ...
|
|
11
|
+
|
|
12
|
+
class OssProvider(Protocol):
|
|
13
|
+
async def put(self, key: str, data: bytes) -> str: ...
|
|
14
|
+
"""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""{{initialDomain}} 路由 -- 只做参数校验 + service 调用,业务逻辑放 service.py。"""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from .schemas import HealthResponse
|
|
6
|
+
|
|
7
|
+
router = APIRouter(prefix="/api/{{initialDomain}}", tags=["{{initialDomain}}"])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@router.get("/health", response_model=HealthResponse)
|
|
11
|
+
async def health() -> HealthResponse:
|
|
12
|
+
"""业务包健康检查。"""
|
|
13
|
+
return HealthResponse(status="ok", domain="{{initialDomain}}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 在此扩展业务 endpoint,调用 service 层完成实际逻辑:
|
|
17
|
+
#
|
|
18
|
+
# from .service import DomainService
|
|
19
|
+
#
|
|
20
|
+
# @router.post("/items")
|
|
21
|
+
# async def create_item(payload: CreateItemRequest) -> ItemResponse:
|
|
22
|
+
# return await DomainService().create_item(payload)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Pydantic Request/Response models。"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HealthResponse(BaseModel):
|
|
7
|
+
"""健康检查响应。"""
|
|
8
|
+
|
|
9
|
+
status: str
|
|
10
|
+
domain: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 在此追加业务 schema:
|
|
14
|
+
# class CreateItemRequest(BaseModel):
|
|
15
|
+
# name: str
|
|
16
|
+
# ...
|
|
17
|
+
#
|
|
18
|
+
# class ItemResponse(BaseModel):
|
|
19
|
+
# id: int
|
|
20
|
+
# name: str
|
|
21
|
+
# ...
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{projectName}}-backend"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{projectDescription}}"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
# workspace members(本地 editable,详见 [tool.uv.sources])
|
|
8
|
+
"dwy-shared",
|
|
9
|
+
"{{scopePrefix}}-{{initialDomain}}",
|
|
10
|
+
# 单一版本中心 -- 所有 workspace member 用到的外部依赖在这里集中管理版本号,
|
|
11
|
+
# member 自己的 pyproject.toml 只写包名,uv workspace 解析时从顶层取约束。
|
|
12
|
+
"dwyeapi>=0.9.0",
|
|
13
|
+
"fastapi>=0.115.0",
|
|
14
|
+
"uvicorn[standard]>=0.30.0",
|
|
15
|
+
"sqlalchemy[asyncio]>=2.0.0",
|
|
16
|
+
"asyncpg>=0.30.0",
|
|
17
|
+
"redis[hiredis]>=5.0.0",
|
|
18
|
+
"pydantic[email]>=2.12.5",
|
|
19
|
+
"python-jose[cryptography]>=3.3.0",
|
|
20
|
+
"bcrypt>=4.0.0",
|
|
21
|
+
"alembic>=1.13.0",
|
|
22
|
+
"python-multipart>=0.0.9",
|
|
23
|
+
"httpx>=0.27.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=8.0.0",
|
|
29
|
+
"pytest-asyncio>=0.24.0",
|
|
30
|
+
"aiosqlite>=0.20.0",
|
|
31
|
+
"fakeredis[json,lua]>=2.21.0",
|
|
32
|
+
"ruff>=0.6.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/app"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py311"
|
|
44
|
+
line-length = 120
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E", "W", "F", "I", "N", "UP", "B", "SIM", "RUF"]
|
|
48
|
+
ignore = [
|
|
49
|
+
"B008", # Depends() in function defaults is standard FastAPI pattern
|
|
50
|
+
"RUF001", # Fullwidth punctuation in Chinese strings is intentional
|
|
51
|
+
"RUF002", # Fullwidth punctuation in Chinese docstrings is intentional
|
|
52
|
+
"RUF003", # Fullwidth punctuation in Chinese comments is intentional
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.isort]
|
|
56
|
+
known-first-party = ["app", "dwy_shared", "{{scopePrefix}}_{{initialDomain}}"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
asyncio_mode = "auto"
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
|
|
62
|
+
[[tool.uv.index]]
|
|
63
|
+
name = "aliyun"
|
|
64
|
+
url = "https://mirrors.aliyun.com/pypi/simple/"
|
|
65
|
+
default = true
|
|
66
|
+
|
|
67
|
+
[[tool.uv.index]]
|
|
68
|
+
name = "pypi-org"
|
|
69
|
+
url = "https://pypi.org/simple/"
|
|
70
|
+
explicit = true
|
|
71
|
+
|
|
72
|
+
[tool.uv.workspace]
|
|
73
|
+
# 后端 monorepo:每个 member 都有独立 pyproject.toml,具备独立发包能力
|
|
74
|
+
# members:
|
|
75
|
+
# dwy-shared -- 业务无关 kernel(数据库 / Redis / 中间件等),跨项目可复用
|
|
76
|
+
# {{scopePrefix}}-{{initialDomain}} -- 初始业务包雏形,复制此目录改名即可新增业务包
|
|
77
|
+
members = ["packages/*"]
|
|
78
|
+
|
|
79
|
+
[tool.uv.sources]
|
|
80
|
+
# dwyeapi 走 pypi-org(避免 aliyun 镜像延迟)
|
|
81
|
+
dwyeapi = { index = "pypi-org" }
|
|
82
|
+
# workspace member:本地引用,uv sync 自动 editable 安装
|
|
83
|
+
dwy-shared = { workspace = true }
|
|
84
|
+
{{scopePrefix}}-{{initialDomain}} = { workspace = true }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""装配层配置 -- 嵌入各业务包 Config。
|
|
2
|
+
|
|
3
|
+
继承 `dwyeapi.config.BaseSettings`,基类已统一提供 database_url / redis_url /
|
|
4
|
+
secret_key / environment / allowed_origins / log_* 等字段,本文件只追加项目专属
|
|
5
|
+
与插件嵌套配置。`.env` 使用双下划线分隔嵌套层级(例 `<DOMAIN_UPPER>__ENABLED=true`,
|
|
6
|
+
其中 DOMAIN_UPPER 是 initialDomain 的全大写形式)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dwyeapi.config import BaseSettings as DwyBaseSettings
|
|
10
|
+
|
|
11
|
+
from {{scopePrefix}}_{{initialDomain}}.config import DomainConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Settings(DwyBaseSettings):
|
|
15
|
+
"""装配层配置。"""
|
|
16
|
+
|
|
17
|
+
# ── 业务包嵌套 Config(各包字段在对应 Config 类定义)──
|
|
18
|
+
{{initialDomain}}: DomainConfig = DomainConfig()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
settings = Settings()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""异常处理器注册 -- 统一响应信封 {code, message, data, timestamp}。"""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
7
|
+
"""注册全局异常处理器。
|
|
8
|
+
|
|
9
|
+
优先复用 dwyeapi 的 handler;若需追加项目特有异常(如 426 UpgradeRequired),
|
|
10
|
+
在本函数内 `@app.exception_handler(MyError)` 追加。
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
from dwyeapi.exceptions import register_handlers
|
|
14
|
+
|
|
15
|
+
register_handlers(app)
|
|
16
|
+
except ImportError:
|
|
17
|
+
# dwyeapi 未提供 register_handlers 时的最简兜底
|
|
18
|
+
from fastapi import Request
|
|
19
|
+
from fastapi.responses import JSONResponse
|
|
20
|
+
|
|
21
|
+
@app.exception_handler(Exception)
|
|
22
|
+
async def _fallback_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
23
|
+
return JSONResponse(
|
|
24
|
+
status_code=500,
|
|
25
|
+
content={"code": "INTERNAL_ERROR", "message": str(exc), "data": None},
|
|
26
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Provider 工厂 -- 按 settings 选择外部服务的具体实现。
|
|
2
|
+
|
|
3
|
+
业务包只依赖 Protocol(在各包 protocols.py 定义),具体实现由本文件根据
|
|
4
|
+
settings.environment / settings.<domain>.provider 等字段决定:
|
|
5
|
+
- dev: 内存实现(InMemorySmsProvider 等)
|
|
6
|
+
- prod: 真实实现(AliyunSmsProvider / TencentOssProvider 等)
|
|
7
|
+
|
|
8
|
+
main.py 调用本文件的 make_* 函数,把 Provider 注入业务包的 register()。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# 示例占位:实际新增业务时,在此添加 make_xxx_provider(settings) -> XxxProvider
|
|
12
|
+
#
|
|
13
|
+
# def make_sms_provider(settings: Settings) -> SmsProvider:
|
|
14
|
+
# if settings.environment == "dev":
|
|
15
|
+
# return InMemorySmsProvider()
|
|
16
|
+
# return AliyunSmsProvider(access_key=settings.sms.access_key, ...)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 基础设施适配占位 -- HTTP client / 第三方 SDK 封装等放此目录。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""FastAPI 应用入口 -- 装配层。
|
|
2
|
+
|
|
3
|
+
职责:加载配置 -> 初始化基础设施(日志/DB/Redis) -> 注册业务包路由。
|
|
4
|
+
业务逻辑一律在 packages/ 下,本文件只做"装配"。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
12
|
+
|
|
13
|
+
from app import __version__
|
|
14
|
+
from app.config import settings
|
|
15
|
+
from app.exceptions import register_exception_handlers
|
|
16
|
+
from {{scopePrefix}}_{{initialDomain}} import register as register_{{initialDomain}}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@asynccontextmanager
|
|
20
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
21
|
+
"""启动时初始化日志/DB/Redis,关闭时反向释放。"""
|
|
22
|
+
# TODO: 在此初始化日志、数据库连接池、Redis 连接
|
|
23
|
+
# 示例:
|
|
24
|
+
# from dwyeapi import logger as dwy_logger
|
|
25
|
+
# from dwyeapi import cache as dwy_cache
|
|
26
|
+
# dwy_logger.configure(level=settings.log_level)
|
|
27
|
+
# await dwy_cache.configure(settings.redis_url)
|
|
28
|
+
yield
|
|
29
|
+
# TODO: 反向释放
|
|
30
|
+
# await dwy_cache.close_redis()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# prod 环境关闭 Swagger,避免接口结构对外暴露
|
|
34
|
+
_docs_disabled = settings.environment == "prod"
|
|
35
|
+
|
|
36
|
+
app = FastAPI(
|
|
37
|
+
title="{{projectName}} API",
|
|
38
|
+
version=__version__,
|
|
39
|
+
description="{{projectDescription}}",
|
|
40
|
+
lifespan=lifespan,
|
|
41
|
+
docs_url=None if _docs_disabled else "/docs",
|
|
42
|
+
redoc_url=None if _docs_disabled else "/redoc",
|
|
43
|
+
openapi_url=None if _docs_disabled else "/openapi.json",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# CORS -- dev 全放开,prod 通过 settings.allowed_origins 收紧
|
|
47
|
+
app.add_middleware(
|
|
48
|
+
CORSMiddleware,
|
|
49
|
+
allow_origins=["*"] if settings.environment == "dev" else settings.allowed_origins,
|
|
50
|
+
allow_credentials=True,
|
|
51
|
+
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
52
|
+
allow_headers=["*"],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 异常处理
|
|
56
|
+
register_exception_handlers(app)
|
|
57
|
+
|
|
58
|
+
# 业务包装配(Provider/Hook 在此注入)
|
|
59
|
+
register_{{initialDomain}}(app, config=settings.{{initialDomain}})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.get("/api/health")
|
|
63
|
+
async def health() -> dict:
|
|
64
|
+
"""健康检查 -- 只探活,不探依赖。"""
|
|
65
|
+
return {"status": "ok", "service": "{{projectName}}", "version": __version__}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 自定义中间件占位 -- 鉴权/限流/请求日志等放此目录。
|
|
File without changes
|