adspecs 0.1.19 → 0.1.21
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/.adspecs/feature.json +16 -16
- package/.adspecs/feature.yml +28 -28
- package/.adspecs/paths.json +17 -17
- package/.adspecs/templates/04-/345/211/215/347/253/257/345/212/237/350/203/275/350/256/276/350/256/241/346/250/241/346/235/277.md +1 -1
- package/.adspecs/templates/05-/345/220/216/347/253/257/344/273/273/345/212/241/346/270/205/345/215/225/346/250/241/346/235/277.md +613 -724
- package/.adspecs/templates/05b-/345/211/215/347/253/257/344/273/273/345/212/241/346/270/205/345/215/225/346/250/241/346/235/277.md +51 -51
- package/.claude-plugin/marketplace.json +23 -23
- package/.claude-plugin/plugin.json +18 -18
- package/.qoder-plugin/plugin.json +31 -31
- package/CLAUDE.md +1 -5
- package/INSTALL.md +3 -3
- package/README.md +395 -395
- package/bin/adspecs.js +129 -129
- package/hooks/commit-queue.js +245 -245
- package/hooks/hooks.json +63 -63
- package/hooks/session-start.js +44 -44
- package/hooks/wiki-queue.js +127 -127
- package/package.json +61 -61
- package/references/ant6-front-standard/index.md +99 -99
- package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
- package/references/ecp-end-standard/index.md +63 -63
- package/references/python-end-standard/01-Python/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +372 -372
- package/references/python-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +226 -226
- package/references/python-end-standard/03-Celery/345/274/202/346/255/245/344/273/273/345/212/241/350/247/204/350/214/203.md +237 -237
- package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
- package/scripts/postinstall.js +107 -107
- package/scripts/sync-version.js +105 -105
- package/skills/.claude/.wiki-update-queue +26 -26
- package/skills/adspecs-constitution/SKILL.md +157 -0
- package/skills/adspecs-export-word/SKILL.md +498 -498
- package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
- package/skills/adspecs-export-word/references/package-lock.json +220 -220
- package/skills/adspecs-export-word/references/package.json +10 -10
- package/skills/adspecs-front-prototype/SKILL.md +405 -405
- package/skills/adspecs-front-spec/SKILL.md +4 -4
- package/skills/adspecs-front-tasks/SKILL.md +213 -173
- package/skills/adspecs-plan/SKILL.md +59 -69
- package/skills/adspecs-prd/SKILL.md +13 -5
- package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
- package/skills/adspecs-tasks/SKILL.md +175 -204
- package/skills/adspecs-update-status/SKILL.md +382 -382
- package/skills/adspecs-utest/SKILL.md +107 -116
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/grill-me/agents/openai.yaml +5 -0
- package/skills/playwright-cli/SKILL.md +420 -0
- package/skills/playwright-cli/references/element-attributes.md +23 -0
- package/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/skills/playwright-cli/references/request-mocking.md +87 -0
- package/skills/playwright-cli/references/running-code.md +241 -0
- package/skills/playwright-cli/references/session-management.md +225 -0
- package/skills/playwright-cli/references/storage-state.md +275 -0
- package/skills/playwright-cli/references/test-generation.md +433 -0
- package/skills/playwright-cli/references/tracing.md +139 -0
- package/skills/playwright-cli/references/video-recording.md +143 -0
- package/skills/playwright-trace/SKILL.md +171 -0
- package/skills/project-init/SKILL.md +93 -22
- package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
- package/skills/wiki-update/SKILL.md +232 -232
- package/src/commands/doctor.js +197 -197
- package/src/commands/init.js +83 -83
- package/src/commands/plugin.js +165 -165
- package/src/commands/update.js +87 -87
- package/src/lib/area-scanner.js +129 -129
- package/src/lib/copier.js +104 -104
- package/src/lib/dir-utils.js +161 -133
- package/src/lib/json-merge.js +114 -114
- package/src/lib/paths-defaults.js +37 -37
- package/src/lib/prompts.js +428 -347
- package/src/lib/readme-gen.js +143 -143
- package/src/lib/report.js +338 -327
- package/src/lib/scaffolder.js +551 -518
- package/src/lib/short-name.js +36 -36
- package/src/utils.js +80 -80
- package/references/antd-front-demo/.env +0 -15
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
# 数据库设计与使用规范
|
|
2
|
-
|
|
3
|
-
> **版本**: v1.0 | **修订日期**: 2026-07-15
|
|
4
|
-
> **技术栈**: SQLAlchemy 2.0+ / Alembic / SQLite (aiosqlite)
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 一、SQLAlchemy 基类
|
|
9
|
-
|
|
10
|
-
### 1.1 声明式基类
|
|
11
|
-
|
|
12
|
-
```python
|
|
13
|
-
# app/models/base.py
|
|
14
|
-
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
15
|
-
from sqlalchemy import Integer, String, Boolean, DateTime, func
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
|
|
18
|
-
class Base(DeclarativeBase):
|
|
19
|
-
"""SQLAlchemy 声明式基类"""
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
class TimestampMixin:
|
|
23
|
-
"""时间戳 Mixin — 自动记录创建/更新时间"""
|
|
24
|
-
created_at: Mapped[datetime] = mapped_column(
|
|
25
|
-
DateTime, server_default=func.now(), nullable=False
|
|
26
|
-
)
|
|
27
|
-
updated_at: Mapped[datetime] = mapped_column(
|
|
28
|
-
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
class SoftDeleteMixin:
|
|
32
|
-
"""软删除 Mixin"""
|
|
33
|
-
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
34
|
-
|
|
35
|
-
class AuditMixin:
|
|
36
|
-
"""审计 Mixin — 记录操作人"""
|
|
37
|
-
created_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
38
|
-
updated_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### 1.2 模型定义示例
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
# app/models/warehouse.py
|
|
45
|
-
from sqlalchemy import String, Integer, Text, Index
|
|
46
|
-
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
47
|
-
from app.models.base import Base, TimestampMixin, SoftDeleteMixin, AuditMixin
|
|
48
|
-
|
|
49
|
-
class Warehouse(Base, TimestampMixin, SoftDeleteMixin, AuditMixin):
|
|
50
|
-
"""仓库模型"""
|
|
51
|
-
__tablename__ = "warehouses"
|
|
52
|
-
__table_args__ = (
|
|
53
|
-
Index("ix_warehouse_code", "code", unique=True),
|
|
54
|
-
Index("ix_warehouse_active", "is_active"),
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
58
|
-
code: Mapped[str] = mapped_column(String(50), nullable=False, comment="仓库编码")
|
|
59
|
-
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="仓库名称")
|
|
60
|
-
location: Mapped[str | None] = mapped_column(String(500), comment="位置")
|
|
61
|
-
is_active: Mapped[bool] = mapped_column(default=True, comment="是否启用")
|
|
62
|
-
|
|
63
|
-
# relationships
|
|
64
|
-
material_lots: Mapped[list["MaterialLot"]] = relationship(back_populates="warehouse")
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## 二、SQLite 特有注意事项
|
|
70
|
-
|
|
71
|
-
| 事项 | 规范 |
|
|
72
|
-
|----------------|------------------------------------------------------------|
|
|
73
|
-
| WAL 模式 | 生产环境必须启用 `PRAGMA journal_mode=WAL` |
|
|
74
|
-
| 外键约束 | 默认关闭,必须执行 `PRAGMA foreign_keys=ON` |
|
|
75
|
-
| 类型亲和性 | 严格按模型定义列类型,避免隐式转换 |
|
|
76
|
-
| 并发写入 | SQLite 不支持并发写,Celery worker 必须 `--concurrency=1` |
|
|
77
|
-
| 文件权限 | 数据库文件目录需要读写权限 |
|
|
78
|
-
| 备份 | 定期 VACUUM + 文件拷贝 |
|
|
79
|
-
|
|
80
|
-
### 引擎配置
|
|
81
|
-
|
|
82
|
-
```python
|
|
83
|
-
# app/core/database.py
|
|
84
|
-
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
85
|
-
from sqlalchemy import text
|
|
86
|
-
from app.core.config import settings
|
|
87
|
-
|
|
88
|
-
engine = create_async_engine(
|
|
89
|
-
settings.DATABASE_URL,
|
|
90
|
-
echo=settings.DEBUG,
|
|
91
|
-
connect_args={"check_same_thread": False},
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
|
95
|
-
|
|
96
|
-
async def init_db():
|
|
97
|
-
"""初始化数据库(启用 WAL 模式)"""
|
|
98
|
-
async with engine.connect() as conn:
|
|
99
|
-
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
|
100
|
-
await conn.execute(text("PRAGMA foreign_keys=ON"))
|
|
101
|
-
await conn.commit()
|
|
102
|
-
|
|
103
|
-
async def get_db():
|
|
104
|
-
"""数据库会话依赖注入"""
|
|
105
|
-
async with async_session() as session:
|
|
106
|
-
try:
|
|
107
|
-
yield session
|
|
108
|
-
except Exception:
|
|
109
|
-
await session.rollback()
|
|
110
|
-
raise
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## 三、Alembic 迁移规范
|
|
116
|
-
|
|
117
|
-
### 3.1 初始化
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
alembic init alembic
|
|
121
|
-
# 修改 alembic.ini 的 sqlalchemy.url
|
|
122
|
-
# 修改 alembic/env.py 导入 Base.metadata
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### 3.2 迁移命名
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
alembic revision --autogenerate -m "add_warehouse_table"
|
|
129
|
-
alembic revision --autogenerate -m "add_index_warehouse_code"
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### 3.3 迁移规则
|
|
133
|
-
|
|
134
|
-
| 规则 | 说明 |
|
|
135
|
-
|------------|------------------------------------------------|
|
|
136
|
-
| 自动生成 | 始终使用 `--autogenerate`,手动 review 后提交 |
|
|
137
|
-
| 不可逆操作 | DROP TABLE/DROP COLUMN 前必须确认数据安全 |
|
|
138
|
-
| 数据迁移 | 数据迁移和结构迁移分开为两个 revision |
|
|
139
|
-
| 测试 | 迁移后必须运行测试验证 |
|
|
140
|
-
| 提交 | 迁移文件必须纳入版本控制 |
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## 四、查询规范
|
|
145
|
-
|
|
146
|
-
### 4.1 使用 select() 风格
|
|
147
|
-
|
|
148
|
-
```python
|
|
149
|
-
from sqlalchemy import select, func
|
|
150
|
-
|
|
151
|
-
# 单条查询
|
|
152
|
-
stmt = select(Warehouse).where(Warehouse.id == warehouse_id)
|
|
153
|
-
result = await db.execute(stmt)
|
|
154
|
-
warehouse = result.scalar_one_or_none()
|
|
155
|
-
|
|
156
|
-
# 分页查询
|
|
157
|
-
stmt = (
|
|
158
|
-
select(Warehouse)
|
|
159
|
-
.where(Warehouse.is_deleted == False, Warehouse.is_active == True)
|
|
160
|
-
.order_by(Warehouse.created_at.desc())
|
|
161
|
-
.offset((page - 1) * page_size)
|
|
162
|
-
.limit(page_size)
|
|
163
|
-
)
|
|
164
|
-
result = await db.execute(stmt)
|
|
165
|
-
items = result.scalars().all()
|
|
166
|
-
|
|
167
|
-
# 计数
|
|
168
|
-
count_stmt = select(func.count()).select_from(Warehouse).where(Warehouse.is_deleted == False)
|
|
169
|
-
total = (await db.execute(count_stmt)).scalar()
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### 4.2 禁止事项
|
|
173
|
-
|
|
174
|
-
- 禁止使用 `session.query()` 风格(SQLAlchemy 1.x 遗留)
|
|
175
|
-
- 禁止字符串拼接 SQL
|
|
176
|
-
- 禁止在循环中执行查询(N+1 问题)
|
|
177
|
-
- 禁止不带 WHERE 的 UPDATE/DELETE
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## 五、Repository 模式
|
|
182
|
-
|
|
183
|
-
```python
|
|
184
|
-
# app/repositories/warehouse.py
|
|
185
|
-
class WarehouseRepository:
|
|
186
|
-
"""仓库数据访问层"""
|
|
187
|
-
|
|
188
|
-
async def get_by_id(self, db: AsyncSession, warehouse_id: int) -> Warehouse | None:
|
|
189
|
-
return await db.get(Warehouse, warehouse_id)
|
|
190
|
-
|
|
191
|
-
async def get_by_code(self, db: AsyncSession, code: str) -> Warehouse | None:
|
|
192
|
-
stmt = select(Warehouse).where(Warehouse.code == code, Warehouse.is_deleted == False)
|
|
193
|
-
result = await db.execute(stmt)
|
|
194
|
-
return result.scalar_one_or_none()
|
|
195
|
-
|
|
196
|
-
async def get_page(
|
|
197
|
-
self, db: AsyncSession, page: int, page_size: int, filters: dict | None = None
|
|
198
|
-
) -> tuple[list[Warehouse], int]:
|
|
199
|
-
base = select(Warehouse).where(Warehouse.is_deleted == False)
|
|
200
|
-
if filters:
|
|
201
|
-
if "name" in filters:
|
|
202
|
-
base = base.where(Warehouse.name.contains(filters["name"]))
|
|
203
|
-
if "is_active" in filters:
|
|
204
|
-
base = base.where(Warehouse.is_active == filters["is_active"])
|
|
205
|
-
|
|
206
|
-
count_stmt = select(func.count()).select_from(base.subquery())
|
|
207
|
-
total = (await db.execute(count_stmt)).scalar()
|
|
208
|
-
|
|
209
|
-
stmt = base.order_by(Warehouse.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
|
210
|
-
result = await db.execute(stmt)
|
|
211
|
-
return result.scalars().all(), total
|
|
212
|
-
|
|
213
|
-
warehouse_repo = WarehouseRepository()
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
## 六、索引策略
|
|
219
|
-
|
|
220
|
-
| 场景 | 索引类型 | 示例 |
|
|
221
|
-
|----------------|------------------|-------------------------------------------|
|
|
222
|
-
| 唯一约束 | Unique Index | code, email |
|
|
223
|
-
| 高频查询条件 | 普通索引 | is_active, status |
|
|
224
|
-
| 组合查询 | 复合索引 | (warehouse_id, material_type) |
|
|
225
|
-
| 外键 | 自动创建 | relationship 自动生成 |
|
|
226
|
-
| 软删除过滤 | 部分索引 | WHERE is_deleted = False |
|
|
1
|
+
# 数据库设计与使用规范
|
|
2
|
+
|
|
3
|
+
> **版本**: v1.0 | **修订日期**: 2026-07-15
|
|
4
|
+
> **技术栈**: SQLAlchemy 2.0+ / Alembic / SQLite (aiosqlite)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、SQLAlchemy 基类
|
|
9
|
+
|
|
10
|
+
### 1.1 声明式基类
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
# app/models/base.py
|
|
14
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
15
|
+
from sqlalchemy import Integer, String, Boolean, DateTime, func
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
class Base(DeclarativeBase):
|
|
19
|
+
"""SQLAlchemy 声明式基类"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class TimestampMixin:
|
|
23
|
+
"""时间戳 Mixin — 自动记录创建/更新时间"""
|
|
24
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
25
|
+
DateTime, server_default=func.now(), nullable=False
|
|
26
|
+
)
|
|
27
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
28
|
+
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
class SoftDeleteMixin:
|
|
32
|
+
"""软删除 Mixin"""
|
|
33
|
+
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
34
|
+
|
|
35
|
+
class AuditMixin:
|
|
36
|
+
"""审计 Mixin — 记录操作人"""
|
|
37
|
+
created_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
38
|
+
updated_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 1.2 模型定义示例
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# app/models/warehouse.py
|
|
45
|
+
from sqlalchemy import String, Integer, Text, Index
|
|
46
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
47
|
+
from app.models.base import Base, TimestampMixin, SoftDeleteMixin, AuditMixin
|
|
48
|
+
|
|
49
|
+
class Warehouse(Base, TimestampMixin, SoftDeleteMixin, AuditMixin):
|
|
50
|
+
"""仓库模型"""
|
|
51
|
+
__tablename__ = "warehouses"
|
|
52
|
+
__table_args__ = (
|
|
53
|
+
Index("ix_warehouse_code", "code", unique=True),
|
|
54
|
+
Index("ix_warehouse_active", "is_active"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
58
|
+
code: Mapped[str] = mapped_column(String(50), nullable=False, comment="仓库编码")
|
|
59
|
+
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="仓库名称")
|
|
60
|
+
location: Mapped[str | None] = mapped_column(String(500), comment="位置")
|
|
61
|
+
is_active: Mapped[bool] = mapped_column(default=True, comment="是否启用")
|
|
62
|
+
|
|
63
|
+
# relationships
|
|
64
|
+
material_lots: Mapped[list["MaterialLot"]] = relationship(back_populates="warehouse")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 二、SQLite 特有注意事项
|
|
70
|
+
|
|
71
|
+
| 事项 | 规范 |
|
|
72
|
+
|----------------|------------------------------------------------------------|
|
|
73
|
+
| WAL 模式 | 生产环境必须启用 `PRAGMA journal_mode=WAL` |
|
|
74
|
+
| 外键约束 | 默认关闭,必须执行 `PRAGMA foreign_keys=ON` |
|
|
75
|
+
| 类型亲和性 | 严格按模型定义列类型,避免隐式转换 |
|
|
76
|
+
| 并发写入 | SQLite 不支持并发写,Celery worker 必须 `--concurrency=1` |
|
|
77
|
+
| 文件权限 | 数据库文件目录需要读写权限 |
|
|
78
|
+
| 备份 | 定期 VACUUM + 文件拷贝 |
|
|
79
|
+
|
|
80
|
+
### 引擎配置
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# app/core/database.py
|
|
84
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
85
|
+
from sqlalchemy import text
|
|
86
|
+
from app.core.config import settings
|
|
87
|
+
|
|
88
|
+
engine = create_async_engine(
|
|
89
|
+
settings.DATABASE_URL,
|
|
90
|
+
echo=settings.DEBUG,
|
|
91
|
+
connect_args={"check_same_thread": False},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
|
95
|
+
|
|
96
|
+
async def init_db():
|
|
97
|
+
"""初始化数据库(启用 WAL 模式)"""
|
|
98
|
+
async with engine.connect() as conn:
|
|
99
|
+
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
|
100
|
+
await conn.execute(text("PRAGMA foreign_keys=ON"))
|
|
101
|
+
await conn.commit()
|
|
102
|
+
|
|
103
|
+
async def get_db():
|
|
104
|
+
"""数据库会话依赖注入"""
|
|
105
|
+
async with async_session() as session:
|
|
106
|
+
try:
|
|
107
|
+
yield session
|
|
108
|
+
except Exception:
|
|
109
|
+
await session.rollback()
|
|
110
|
+
raise
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 三、Alembic 迁移规范
|
|
116
|
+
|
|
117
|
+
### 3.1 初始化
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
alembic init alembic
|
|
121
|
+
# 修改 alembic.ini 的 sqlalchemy.url
|
|
122
|
+
# 修改 alembic/env.py 导入 Base.metadata
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 3.2 迁移命名
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
alembic revision --autogenerate -m "add_warehouse_table"
|
|
129
|
+
alembic revision --autogenerate -m "add_index_warehouse_code"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3.3 迁移规则
|
|
133
|
+
|
|
134
|
+
| 规则 | 说明 |
|
|
135
|
+
|------------|------------------------------------------------|
|
|
136
|
+
| 自动生成 | 始终使用 `--autogenerate`,手动 review 后提交 |
|
|
137
|
+
| 不可逆操作 | DROP TABLE/DROP COLUMN 前必须确认数据安全 |
|
|
138
|
+
| 数据迁移 | 数据迁移和结构迁移分开为两个 revision |
|
|
139
|
+
| 测试 | 迁移后必须运行测试验证 |
|
|
140
|
+
| 提交 | 迁移文件必须纳入版本控制 |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 四、查询规范
|
|
145
|
+
|
|
146
|
+
### 4.1 使用 select() 风格
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from sqlalchemy import select, func
|
|
150
|
+
|
|
151
|
+
# 单条查询
|
|
152
|
+
stmt = select(Warehouse).where(Warehouse.id == warehouse_id)
|
|
153
|
+
result = await db.execute(stmt)
|
|
154
|
+
warehouse = result.scalar_one_or_none()
|
|
155
|
+
|
|
156
|
+
# 分页查询
|
|
157
|
+
stmt = (
|
|
158
|
+
select(Warehouse)
|
|
159
|
+
.where(Warehouse.is_deleted == False, Warehouse.is_active == True)
|
|
160
|
+
.order_by(Warehouse.created_at.desc())
|
|
161
|
+
.offset((page - 1) * page_size)
|
|
162
|
+
.limit(page_size)
|
|
163
|
+
)
|
|
164
|
+
result = await db.execute(stmt)
|
|
165
|
+
items = result.scalars().all()
|
|
166
|
+
|
|
167
|
+
# 计数
|
|
168
|
+
count_stmt = select(func.count()).select_from(Warehouse).where(Warehouse.is_deleted == False)
|
|
169
|
+
total = (await db.execute(count_stmt)).scalar()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 4.2 禁止事项
|
|
173
|
+
|
|
174
|
+
- 禁止使用 `session.query()` 风格(SQLAlchemy 1.x 遗留)
|
|
175
|
+
- 禁止字符串拼接 SQL
|
|
176
|
+
- 禁止在循环中执行查询(N+1 问题)
|
|
177
|
+
- 禁止不带 WHERE 的 UPDATE/DELETE
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 五、Repository 模式
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# app/repositories/warehouse.py
|
|
185
|
+
class WarehouseRepository:
|
|
186
|
+
"""仓库数据访问层"""
|
|
187
|
+
|
|
188
|
+
async def get_by_id(self, db: AsyncSession, warehouse_id: int) -> Warehouse | None:
|
|
189
|
+
return await db.get(Warehouse, warehouse_id)
|
|
190
|
+
|
|
191
|
+
async def get_by_code(self, db: AsyncSession, code: str) -> Warehouse | None:
|
|
192
|
+
stmt = select(Warehouse).where(Warehouse.code == code, Warehouse.is_deleted == False)
|
|
193
|
+
result = await db.execute(stmt)
|
|
194
|
+
return result.scalar_one_or_none()
|
|
195
|
+
|
|
196
|
+
async def get_page(
|
|
197
|
+
self, db: AsyncSession, page: int, page_size: int, filters: dict | None = None
|
|
198
|
+
) -> tuple[list[Warehouse], int]:
|
|
199
|
+
base = select(Warehouse).where(Warehouse.is_deleted == False)
|
|
200
|
+
if filters:
|
|
201
|
+
if "name" in filters:
|
|
202
|
+
base = base.where(Warehouse.name.contains(filters["name"]))
|
|
203
|
+
if "is_active" in filters:
|
|
204
|
+
base = base.where(Warehouse.is_active == filters["is_active"])
|
|
205
|
+
|
|
206
|
+
count_stmt = select(func.count()).select_from(base.subquery())
|
|
207
|
+
total = (await db.execute(count_stmt)).scalar()
|
|
208
|
+
|
|
209
|
+
stmt = base.order_by(Warehouse.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
|
210
|
+
result = await db.execute(stmt)
|
|
211
|
+
return result.scalars().all(), total
|
|
212
|
+
|
|
213
|
+
warehouse_repo = WarehouseRepository()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 六、索引策略
|
|
219
|
+
|
|
220
|
+
| 场景 | 索引类型 | 示例 |
|
|
221
|
+
|----------------|------------------|-------------------------------------------|
|
|
222
|
+
| 唯一约束 | Unique Index | code, email |
|
|
223
|
+
| 高频查询条件 | 普通索引 | is_active, status |
|
|
224
|
+
| 组合查询 | 复合索引 | (warehouse_id, material_type) |
|
|
225
|
+
| 外键 | 自动创建 | relationship 自动生成 |
|
|
226
|
+
| 软删除过滤 | 部分索引 | WHERE is_deleted = False |
|