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,372 +1,372 @@
|
|
|
1
|
-
# Python 后端编码规范
|
|
2
|
-
|
|
3
|
-
> **版本**: v1.0 | **修订日期**: 2026-07-15
|
|
4
|
-
> **适用范围**: 基于 Python + FastAPI + SQLAlchemy + SQLite 的后端项目
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## 一、技术栈
|
|
9
|
-
|
|
10
|
-
| 组件 | 版本 | 用途 |
|
|
11
|
-
|------------|---------|----------------------|
|
|
12
|
-
| Python | 3.11+ | 运行时 |
|
|
13
|
-
| FastAPI | 0.115+ | Web 框架 |
|
|
14
|
-
| SQLAlchemy | 2.0+ | ORM / 数据库抽象 |
|
|
15
|
-
| Alembic | 1.13+ | 数据库迁移 |
|
|
16
|
-
| Pydantic | 2.0+ | 数据验证与序列化 |
|
|
17
|
-
| Celery | 5.4+ | 异步任务队列 |
|
|
18
|
-
| Redis | 7.0+ | 缓存 / 消息代理 / 锁 |
|
|
19
|
-
| Uvicorn | 0.30+ | ASGI 服务器 |
|
|
20
|
-
| pytest | 8.0+ | 单元测试 |
|
|
21
|
-
| httpx | 0.27+ | HTTP 客户端 / 测试 |
|
|
22
|
-
| Ruff | 0.5+ | Linter + Formatter |
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## 二、项目结构
|
|
27
|
-
|
|
28
|
-
### 2.1 标准目录布局
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
project-root/
|
|
32
|
-
├── app/
|
|
33
|
-
│ ├── main.py # FastAPI 入口
|
|
34
|
-
│ ├── core/ # config, database, security, exceptions
|
|
35
|
-
│ ├── api/v1/endpoints/ # 按业务域划分路由
|
|
36
|
-
│ ├── models/ # SQLAlchemy 模型
|
|
37
|
-
│ ├── schemas/ # Pydantic Schema
|
|
38
|
-
│ ├── services/ # 业务逻辑层
|
|
39
|
-
│ ├── repositories/ # 数据访问层(可选)
|
|
40
|
-
│ ├── tasks/ # Celery 异步任务
|
|
41
|
-
│ └── utils/ # 工具函数
|
|
42
|
-
├── alembic/ # 数据库迁移
|
|
43
|
-
├── tests/ # 测试
|
|
44
|
-
└── pyproject.toml
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 2.2 分层职责
|
|
48
|
-
|
|
49
|
-
| 层 | 职责 | 禁止事项 |
|
|
50
|
-
|---------------|----------------------------------|------------------------|
|
|
51
|
-
| API 路由层 | 请求解析、参数校验、调用 Service | 直接操作数据库 |
|
|
52
|
-
| Service 层 | 业务逻辑编排、事务管理 | 直接构造 HTTP Response |
|
|
53
|
-
| Repository 层 | 数据库 CRUD 封装 | 包含业务判断逻辑 |
|
|
54
|
-
| Model 层 | SQLAlchemy ORM 定义 | 包含业务方法 |
|
|
55
|
-
| Schema 层 | Pydantic 请求/响应 | 包含数据库操作 |
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## 三、命名规范
|
|
60
|
-
|
|
61
|
-
### 3.1 类命名(PascalCase)
|
|
62
|
-
|
|
63
|
-
| 类型 | 后缀 | 示例 |
|
|
64
|
-
|-----------------|--------------|--------------------------|
|
|
65
|
-
| 模型 | 无 | Warehouse, MaterialLot |
|
|
66
|
-
| 创建请求 | Create | WarehouseCreate |
|
|
67
|
-
| 更新请求 | Update | WarehouseUpdate |
|
|
68
|
-
| 响应 | Response | WarehouseResponse |
|
|
69
|
-
| Service | Service | WarehouseService |
|
|
70
|
-
| Repository | Repository | WarehouseRepository |
|
|
71
|
-
| 异常 | Error | WarehouseNotFoundError |
|
|
72
|
-
|
|
73
|
-
### 3.2 函数命名(snake_case)
|
|
74
|
-
|
|
75
|
-
| 操作 | 模式 | 示例 |
|
|
76
|
-
|----------|---------------------------|-------------------------------|
|
|
77
|
-
| 创建 | create_{entity} | create_warehouse |
|
|
78
|
-
| 更新 | update_{entity} | update_warehouse |
|
|
79
|
-
| 删除 | delete_{entity} | delete_warehouse |
|
|
80
|
-
| 获取 | get_{entity} | get_warehouse |
|
|
81
|
-
| 分页 | get_{entity}_list | get_warehouse_list |
|
|
82
|
-
| 校验 | validate_{condition} | validate_warehouse_exists |
|
|
83
|
-
| 异步任务 | {action}_{entity}_task | sync_inventory_task |
|
|
84
|
-
|
|
85
|
-
### 3.3 变量与常量
|
|
86
|
-
|
|
87
|
-
- 变量: snake_case,布尔用 is_/has_/can_ 前缀
|
|
88
|
-
- 常量: 全大写 MAX_PAGE_SIZE, DEFAULT_TIMEOUT
|
|
89
|
-
- 禁止单字母变量和魔法值
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## 四、API 接口规范
|
|
94
|
-
|
|
95
|
-
### 4.1 RESTful 路由
|
|
96
|
-
|
|
97
|
-
```python
|
|
98
|
-
from fastapi import APIRouter, Depends, status
|
|
99
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
100
|
-
|
|
101
|
-
router = APIRouter(prefix="/warehouses", tags=["仓库管理"])
|
|
102
|
-
|
|
103
|
-
@router.post("", status_code=201, response_model=WarehouseResponse)
|
|
104
|
-
async def create_warehouse(
|
|
105
|
-
data: WarehouseCreate,
|
|
106
|
-
db: AsyncSession = Depends(get_db),
|
|
107
|
-
current_user: User = Depends(get_current_user),
|
|
108
|
-
):
|
|
109
|
-
"""创建仓库"""
|
|
110
|
-
return await warehouse_service.create(db, data, current_user)
|
|
111
|
-
|
|
112
|
-
@router.get("/{warehouse_id}", response_model=WarehouseResponse)
|
|
113
|
-
async def get_warehouse(warehouse_id: int, db: AsyncSession = Depends(get_db)):
|
|
114
|
-
"""获取仓库详情"""
|
|
115
|
-
return await warehouse_service.get_by_id(db, warehouse_id)
|
|
116
|
-
|
|
117
|
-
@router.get("", response_model=PageResponse[WarehouseListItem])
|
|
118
|
-
async def list_warehouses(
|
|
119
|
-
params: WarehouseListParams = Depends(),
|
|
120
|
-
db: AsyncSession = Depends(get_db),
|
|
121
|
-
):
|
|
122
|
-
"""分页查询仓库列表"""
|
|
123
|
-
return await warehouse_service.get_list(db, params)
|
|
124
|
-
|
|
125
|
-
@router.put("/{warehouse_id}", response_model=WarehouseResponse)
|
|
126
|
-
async def update_warehouse(
|
|
127
|
-
warehouse_id: int, data: WarehouseUpdate,
|
|
128
|
-
db: AsyncSession = Depends(get_db),
|
|
129
|
-
current_user: User = Depends(get_current_user),
|
|
130
|
-
):
|
|
131
|
-
"""更新仓库"""
|
|
132
|
-
return await warehouse_service.update(db, warehouse_id, data, current_user)
|
|
133
|
-
|
|
134
|
-
@router.delete("/{warehouse_id}", status_code=204)
|
|
135
|
-
async def delete_warehouse(warehouse_id: int, db: AsyncSession = Depends(get_db)):
|
|
136
|
-
"""删除仓库"""
|
|
137
|
-
await warehouse_service.delete(db, warehouse_id)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### 4.2 统一响应格式
|
|
141
|
-
|
|
142
|
-
```python
|
|
143
|
-
from pydantic import BaseModel
|
|
144
|
-
from typing import Generic, TypeVar, Optional
|
|
145
|
-
|
|
146
|
-
T = TypeVar("T")
|
|
147
|
-
|
|
148
|
-
class ApiResponse(BaseModel, Generic[T]):
|
|
149
|
-
code: int = 0
|
|
150
|
-
message: str = "success"
|
|
151
|
-
data: Optional[T] = None
|
|
152
|
-
|
|
153
|
-
class PageResponse(BaseModel, Generic[T]):
|
|
154
|
-
code: int = 0
|
|
155
|
-
message: str = "success"
|
|
156
|
-
data: PageData[T]
|
|
157
|
-
|
|
158
|
-
class PageData(BaseModel, Generic[T]):
|
|
159
|
-
items: list[T]
|
|
160
|
-
total: int
|
|
161
|
-
page: int
|
|
162
|
-
page_size: int
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### 4.3 错误码体系
|
|
166
|
-
|
|
167
|
-
```python
|
|
168
|
-
from enum import IntEnum
|
|
169
|
-
|
|
170
|
-
class ErrorCode(IntEnum):
|
|
171
|
-
SUCCESS = 0
|
|
172
|
-
BAD_REQUEST = 1000
|
|
173
|
-
UNAUTHORIZED = 1001
|
|
174
|
-
FORBIDDEN = 1003
|
|
175
|
-
NOT_FOUND = 1004
|
|
176
|
-
VALIDATION_ERROR = 1010
|
|
177
|
-
INTERNAL_ERROR = 1999
|
|
178
|
-
# 业务错误 2xxx-9xxx 按模块划分
|
|
179
|
-
WAREHOUSE_NOT_FOUND = 2001
|
|
180
|
-
WAREHOUSE_CODE_DUPLICATE = 2002
|
|
181
|
-
MATERIAL_LOT_NOT_FOUND = 3001
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## 五、Service 层规范
|
|
187
|
-
|
|
188
|
-
```python
|
|
189
|
-
class WarehouseService:
|
|
190
|
-
"""仓库管理服务"""
|
|
191
|
-
|
|
192
|
-
async def create(self, db: AsyncSession, data: WarehouseCreate, current_user: User) -> Warehouse:
|
|
193
|
-
await self._validate_code_unique(db, data.code)
|
|
194
|
-
warehouse = Warehouse(**data.model_dump(), created_by=current_user.id)
|
|
195
|
-
db.add(warehouse)
|
|
196
|
-
await db.commit()
|
|
197
|
-
await db.refresh(warehouse)
|
|
198
|
-
return warehouse
|
|
199
|
-
|
|
200
|
-
async def get_by_id(self, db: AsyncSession, warehouse_id: int) -> Warehouse:
|
|
201
|
-
warehouse = await db.get(Warehouse, warehouse_id)
|
|
202
|
-
if not warehouse or warehouse.is_deleted:
|
|
203
|
-
raise BusinessError(ErrorCode.WAREHOUSE_NOT_FOUND)
|
|
204
|
-
return warehouse
|
|
205
|
-
|
|
206
|
-
async def update(self, db: AsyncSession, warehouse_id: int, data: WarehouseUpdate, current_user: User) -> Warehouse:
|
|
207
|
-
warehouse = await self.get_by_id(db, warehouse_id)
|
|
208
|
-
for field, value in data.model_dump(exclude_unset=True).items():
|
|
209
|
-
setattr(warehouse, field, value)
|
|
210
|
-
warehouse.updated_by = current_user.id
|
|
211
|
-
await db.commit()
|
|
212
|
-
await db.refresh(warehouse)
|
|
213
|
-
return warehouse
|
|
214
|
-
|
|
215
|
-
async def delete(self, db: AsyncSession, warehouse_id: int, current_user: User) -> None:
|
|
216
|
-
warehouse = await self.get_by_id(db, warehouse_id)
|
|
217
|
-
warehouse.is_deleted = True
|
|
218
|
-
warehouse.updated_by = current_user.id
|
|
219
|
-
await db.commit()
|
|
220
|
-
|
|
221
|
-
async def _validate_code_unique(self, db: AsyncSession, code: str, exclude_id: int | None = None):
|
|
222
|
-
stmt = select(Warehouse).where(Warehouse.code == code, Warehouse.is_deleted == False)
|
|
223
|
-
if exclude_id:
|
|
224
|
-
stmt = stmt.where(Warehouse.id != exclude_id)
|
|
225
|
-
result = await db.execute(stmt)
|
|
226
|
-
if result.scalar_one_or_none():
|
|
227
|
-
raise BusinessError(ErrorCode.WAREHOUSE_CODE_DUPLICATE)
|
|
228
|
-
|
|
229
|
-
warehouse_service = WarehouseService() # 模块级单例
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
**编写规则**:
|
|
233
|
-
- Service 方法负责 commit,Repository 层不提交
|
|
234
|
-
- 业务异常用 BusinessError,禁止裸 raise Exception
|
|
235
|
-
- 用 Pydantic Schema 接收参数,禁止裸 dict
|
|
236
|
-
- 返回 Model 实例,由 API 层转 Schema
|
|
237
|
-
- 校验用 _validate_ 前缀私有方法
|
|
238
|
-
- 模块级单例实例化
|
|
239
|
-
|
|
240
|
-
---
|
|
241
|
-
|
|
242
|
-
## 六、Pydantic Schema 规范
|
|
243
|
-
|
|
244
|
-
```python
|
|
245
|
-
from pydantic import BaseModel, Field, ConfigDict
|
|
246
|
-
from datetime import datetime
|
|
247
|
-
|
|
248
|
-
class WarehouseBase(BaseModel):
|
|
249
|
-
code: str = Field(..., min_length=1, max_length=50, description="仓库编码")
|
|
250
|
-
name: str = Field(..., min_length=1, max_length=200, description="仓库名称")
|
|
251
|
-
location: str | None = Field(None, max_length=500, description="仓库位置")
|
|
252
|
-
|
|
253
|
-
class WarehouseCreate(WarehouseBase):
|
|
254
|
-
pass
|
|
255
|
-
|
|
256
|
-
class WarehouseUpdate(BaseModel):
|
|
257
|
-
name: str | None = Field(None, min_length=1, max_length=200)
|
|
258
|
-
location: str | None = Field(None, max_length=500)
|
|
259
|
-
|
|
260
|
-
class WarehouseResponse(WarehouseBase):
|
|
261
|
-
model_config = ConfigDict(from_attributes=True)
|
|
262
|
-
id: int
|
|
263
|
-
is_active: bool
|
|
264
|
-
created_at: datetime
|
|
265
|
-
updated_at: datetime
|
|
266
|
-
|
|
267
|
-
class WarehouseListItem(BaseModel):
|
|
268
|
-
model_config = ConfigDict(from_attributes=True)
|
|
269
|
-
id: int
|
|
270
|
-
code: str
|
|
271
|
-
name: str
|
|
272
|
-
is_active: bool
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
**规则**: Base 定义公共字段 | Create/Update/Response 继承复用 | Response 设 from_attributes=True | Update 全 Optional + exclude_unset
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
## 七、异常处理
|
|
280
|
-
|
|
281
|
-
```python
|
|
282
|
-
class BusinessError(Exception):
|
|
283
|
-
def __init__(self, code: ErrorCode, message: str | None = None):
|
|
284
|
-
self.code = code
|
|
285
|
-
self.message = message or ERROR_MESSAGES.get(code, "未知错误")
|
|
286
|
-
|
|
287
|
-
class NotFoundError(BusinessError):
|
|
288
|
-
def __init__(self, entity: str = "资源"):
|
|
289
|
-
super().__init__(ErrorCode.NOT_FOUND, f"{entity}不存在")
|
|
290
|
-
|
|
291
|
-
# 全局异常处理
|
|
292
|
-
@app.exception_handler(BusinessError)
|
|
293
|
-
async def business_error_handler(request: Request, exc: BusinessError):
|
|
294
|
-
return JSONResponse(status_code=200, content={"code": int(exc.code), "message": exc.message, "data": None})
|
|
295
|
-
|
|
296
|
-
@app.exception_handler(Exception)
|
|
297
|
-
async def global_error_handler(request: Request, exc: Exception):
|
|
298
|
-
logger.exception("Unhandled exception", exc_info=exc)
|
|
299
|
-
return JSONResponse(status_code=500, content={"code": 1999, "message": "服务器内部错误", "data": None})
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## 八、配置管理
|
|
305
|
-
|
|
306
|
-
```python
|
|
307
|
-
from pydantic_settings import BaseSettings
|
|
308
|
-
|
|
309
|
-
class Settings(BaseSettings):
|
|
310
|
-
model_config = {"env_file": ".env"}
|
|
311
|
-
APP_NAME: str = "MyApp"
|
|
312
|
-
DEBUG: bool = False
|
|
313
|
-
DATABASE_URL: str = "sqlite+aiosqlite:///./data/app.db"
|
|
314
|
-
REDIS_URL: str = "redis://localhost:6379/0"
|
|
315
|
-
CELERY_BROKER_URL: str = "redis://localhost:6379/1"
|
|
316
|
-
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"
|
|
317
|
-
SECRET_KEY: str = "change-me-in-production"
|
|
318
|
-
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
319
|
-
|
|
320
|
-
settings = Settings()
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## 九、代码质量
|
|
326
|
-
|
|
327
|
-
- Ruff 作为 linter + formatter(py311, line-width=120)
|
|
328
|
-
- 所有公共函数必须有类型注解 + Docstring
|
|
329
|
-
- I/O 操作必须 async/await
|
|
330
|
-
- 导入排序: 标准库 → 第三方 → 本地(Ruff isort)
|
|
331
|
-
- 最大函数长度: 50 行
|
|
332
|
-
- 最大参数个数: 5 个(超出用 Pydantic Model)
|
|
333
|
-
- 禁止魔法值,常量全大写
|
|
334
|
-
|
|
335
|
-
---
|
|
336
|
-
|
|
337
|
-
## 十、测试规范
|
|
338
|
-
|
|
339
|
-
命名: `test_{方法名}_{场景}_{期望结果}`
|
|
340
|
-
|
|
341
|
-
```python
|
|
342
|
-
# tests/conftest.py
|
|
343
|
-
@pytest.fixture
|
|
344
|
-
async def db_session():
|
|
345
|
-
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
346
|
-
async with engine.begin() as conn:
|
|
347
|
-
await conn.run_sync(Base.metadata.create_all)
|
|
348
|
-
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
|
349
|
-
async with async_session() as session:
|
|
350
|
-
yield session
|
|
351
|
-
await engine.dispose()
|
|
352
|
-
|
|
353
|
-
@pytest.fixture
|
|
354
|
-
async def client(db_session: AsyncSession):
|
|
355
|
-
app.dependency_overrides[get_db] = lambda: db_session
|
|
356
|
-
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
357
|
-
yield ac
|
|
358
|
-
app.dependency_overrides.clear()
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
|
-
## 十一、安全规范
|
|
364
|
-
|
|
365
|
-
| 规则 | 说明 |
|
|
366
|
-
|------------|----------------------------------------------|
|
|
367
|
-
| 密码存储 | bcrypt / argon2,禁止明文或 MD5 |
|
|
368
|
-
| Token | JWT + 合理过期时间 |
|
|
369
|
-
| SQL 注入 | SQLAlchemy ORM 参数化,禁止字符串拼接 |
|
|
370
|
-
| 敏感信息 | 环境变量注入,禁止硬编码 |
|
|
371
|
-
| CORS | 生产环境明确配置允许的 Origin |
|
|
372
|
-
| 输入校验 | 全部通过 Pydantic Schema 校验 |
|
|
1
|
+
# Python 后端编码规范
|
|
2
|
+
|
|
3
|
+
> **版本**: v1.0 | **修订日期**: 2026-07-15
|
|
4
|
+
> **适用范围**: 基于 Python + FastAPI + SQLAlchemy + SQLite 的后端项目
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 一、技术栈
|
|
9
|
+
|
|
10
|
+
| 组件 | 版本 | 用途 |
|
|
11
|
+
|------------|---------|----------------------|
|
|
12
|
+
| Python | 3.11+ | 运行时 |
|
|
13
|
+
| FastAPI | 0.115+ | Web 框架 |
|
|
14
|
+
| SQLAlchemy | 2.0+ | ORM / 数据库抽象 |
|
|
15
|
+
| Alembic | 1.13+ | 数据库迁移 |
|
|
16
|
+
| Pydantic | 2.0+ | 数据验证与序列化 |
|
|
17
|
+
| Celery | 5.4+ | 异步任务队列 |
|
|
18
|
+
| Redis | 7.0+ | 缓存 / 消息代理 / 锁 |
|
|
19
|
+
| Uvicorn | 0.30+ | ASGI 服务器 |
|
|
20
|
+
| pytest | 8.0+ | 单元测试 |
|
|
21
|
+
| httpx | 0.27+ | HTTP 客户端 / 测试 |
|
|
22
|
+
| Ruff | 0.5+ | Linter + Formatter |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 二、项目结构
|
|
27
|
+
|
|
28
|
+
### 2.1 标准目录布局
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
project-root/
|
|
32
|
+
├── app/
|
|
33
|
+
│ ├── main.py # FastAPI 入口
|
|
34
|
+
│ ├── core/ # config, database, security, exceptions
|
|
35
|
+
│ ├── api/v1/endpoints/ # 按业务域划分路由
|
|
36
|
+
│ ├── models/ # SQLAlchemy 模型
|
|
37
|
+
│ ├── schemas/ # Pydantic Schema
|
|
38
|
+
│ ├── services/ # 业务逻辑层
|
|
39
|
+
│ ├── repositories/ # 数据访问层(可选)
|
|
40
|
+
│ ├── tasks/ # Celery 异步任务
|
|
41
|
+
│ └── utils/ # 工具函数
|
|
42
|
+
├── alembic/ # 数据库迁移
|
|
43
|
+
├── tests/ # 测试
|
|
44
|
+
└── pyproject.toml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2.2 分层职责
|
|
48
|
+
|
|
49
|
+
| 层 | 职责 | 禁止事项 |
|
|
50
|
+
|---------------|----------------------------------|------------------------|
|
|
51
|
+
| API 路由层 | 请求解析、参数校验、调用 Service | 直接操作数据库 |
|
|
52
|
+
| Service 层 | 业务逻辑编排、事务管理 | 直接构造 HTTP Response |
|
|
53
|
+
| Repository 层 | 数据库 CRUD 封装 | 包含业务判断逻辑 |
|
|
54
|
+
| Model 层 | SQLAlchemy ORM 定义 | 包含业务方法 |
|
|
55
|
+
| Schema 层 | Pydantic 请求/响应 | 包含数据库操作 |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 三、命名规范
|
|
60
|
+
|
|
61
|
+
### 3.1 类命名(PascalCase)
|
|
62
|
+
|
|
63
|
+
| 类型 | 后缀 | 示例 |
|
|
64
|
+
|-----------------|--------------|--------------------------|
|
|
65
|
+
| 模型 | 无 | Warehouse, MaterialLot |
|
|
66
|
+
| 创建请求 | Create | WarehouseCreate |
|
|
67
|
+
| 更新请求 | Update | WarehouseUpdate |
|
|
68
|
+
| 响应 | Response | WarehouseResponse |
|
|
69
|
+
| Service | Service | WarehouseService |
|
|
70
|
+
| Repository | Repository | WarehouseRepository |
|
|
71
|
+
| 异常 | Error | WarehouseNotFoundError |
|
|
72
|
+
|
|
73
|
+
### 3.2 函数命名(snake_case)
|
|
74
|
+
|
|
75
|
+
| 操作 | 模式 | 示例 |
|
|
76
|
+
|----------|---------------------------|-------------------------------|
|
|
77
|
+
| 创建 | create_{entity} | create_warehouse |
|
|
78
|
+
| 更新 | update_{entity} | update_warehouse |
|
|
79
|
+
| 删除 | delete_{entity} | delete_warehouse |
|
|
80
|
+
| 获取 | get_{entity} | get_warehouse |
|
|
81
|
+
| 分页 | get_{entity}_list | get_warehouse_list |
|
|
82
|
+
| 校验 | validate_{condition} | validate_warehouse_exists |
|
|
83
|
+
| 异步任务 | {action}_{entity}_task | sync_inventory_task |
|
|
84
|
+
|
|
85
|
+
### 3.3 变量与常量
|
|
86
|
+
|
|
87
|
+
- 变量: snake_case,布尔用 is_/has_/can_ 前缀
|
|
88
|
+
- 常量: 全大写 MAX_PAGE_SIZE, DEFAULT_TIMEOUT
|
|
89
|
+
- 禁止单字母变量和魔法值
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 四、API 接口规范
|
|
94
|
+
|
|
95
|
+
### 4.1 RESTful 路由
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from fastapi import APIRouter, Depends, status
|
|
99
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
100
|
+
|
|
101
|
+
router = APIRouter(prefix="/warehouses", tags=["仓库管理"])
|
|
102
|
+
|
|
103
|
+
@router.post("", status_code=201, response_model=WarehouseResponse)
|
|
104
|
+
async def create_warehouse(
|
|
105
|
+
data: WarehouseCreate,
|
|
106
|
+
db: AsyncSession = Depends(get_db),
|
|
107
|
+
current_user: User = Depends(get_current_user),
|
|
108
|
+
):
|
|
109
|
+
"""创建仓库"""
|
|
110
|
+
return await warehouse_service.create(db, data, current_user)
|
|
111
|
+
|
|
112
|
+
@router.get("/{warehouse_id}", response_model=WarehouseResponse)
|
|
113
|
+
async def get_warehouse(warehouse_id: int, db: AsyncSession = Depends(get_db)):
|
|
114
|
+
"""获取仓库详情"""
|
|
115
|
+
return await warehouse_service.get_by_id(db, warehouse_id)
|
|
116
|
+
|
|
117
|
+
@router.get("", response_model=PageResponse[WarehouseListItem])
|
|
118
|
+
async def list_warehouses(
|
|
119
|
+
params: WarehouseListParams = Depends(),
|
|
120
|
+
db: AsyncSession = Depends(get_db),
|
|
121
|
+
):
|
|
122
|
+
"""分页查询仓库列表"""
|
|
123
|
+
return await warehouse_service.get_list(db, params)
|
|
124
|
+
|
|
125
|
+
@router.put("/{warehouse_id}", response_model=WarehouseResponse)
|
|
126
|
+
async def update_warehouse(
|
|
127
|
+
warehouse_id: int, data: WarehouseUpdate,
|
|
128
|
+
db: AsyncSession = Depends(get_db),
|
|
129
|
+
current_user: User = Depends(get_current_user),
|
|
130
|
+
):
|
|
131
|
+
"""更新仓库"""
|
|
132
|
+
return await warehouse_service.update(db, warehouse_id, data, current_user)
|
|
133
|
+
|
|
134
|
+
@router.delete("/{warehouse_id}", status_code=204)
|
|
135
|
+
async def delete_warehouse(warehouse_id: int, db: AsyncSession = Depends(get_db)):
|
|
136
|
+
"""删除仓库"""
|
|
137
|
+
await warehouse_service.delete(db, warehouse_id)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 4.2 统一响应格式
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from pydantic import BaseModel
|
|
144
|
+
from typing import Generic, TypeVar, Optional
|
|
145
|
+
|
|
146
|
+
T = TypeVar("T")
|
|
147
|
+
|
|
148
|
+
class ApiResponse(BaseModel, Generic[T]):
|
|
149
|
+
code: int = 0
|
|
150
|
+
message: str = "success"
|
|
151
|
+
data: Optional[T] = None
|
|
152
|
+
|
|
153
|
+
class PageResponse(BaseModel, Generic[T]):
|
|
154
|
+
code: int = 0
|
|
155
|
+
message: str = "success"
|
|
156
|
+
data: PageData[T]
|
|
157
|
+
|
|
158
|
+
class PageData(BaseModel, Generic[T]):
|
|
159
|
+
items: list[T]
|
|
160
|
+
total: int
|
|
161
|
+
page: int
|
|
162
|
+
page_size: int
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 4.3 错误码体系
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from enum import IntEnum
|
|
169
|
+
|
|
170
|
+
class ErrorCode(IntEnum):
|
|
171
|
+
SUCCESS = 0
|
|
172
|
+
BAD_REQUEST = 1000
|
|
173
|
+
UNAUTHORIZED = 1001
|
|
174
|
+
FORBIDDEN = 1003
|
|
175
|
+
NOT_FOUND = 1004
|
|
176
|
+
VALIDATION_ERROR = 1010
|
|
177
|
+
INTERNAL_ERROR = 1999
|
|
178
|
+
# 业务错误 2xxx-9xxx 按模块划分
|
|
179
|
+
WAREHOUSE_NOT_FOUND = 2001
|
|
180
|
+
WAREHOUSE_CODE_DUPLICATE = 2002
|
|
181
|
+
MATERIAL_LOT_NOT_FOUND = 3001
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 五、Service 层规范
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
class WarehouseService:
|
|
190
|
+
"""仓库管理服务"""
|
|
191
|
+
|
|
192
|
+
async def create(self, db: AsyncSession, data: WarehouseCreate, current_user: User) -> Warehouse:
|
|
193
|
+
await self._validate_code_unique(db, data.code)
|
|
194
|
+
warehouse = Warehouse(**data.model_dump(), created_by=current_user.id)
|
|
195
|
+
db.add(warehouse)
|
|
196
|
+
await db.commit()
|
|
197
|
+
await db.refresh(warehouse)
|
|
198
|
+
return warehouse
|
|
199
|
+
|
|
200
|
+
async def get_by_id(self, db: AsyncSession, warehouse_id: int) -> Warehouse:
|
|
201
|
+
warehouse = await db.get(Warehouse, warehouse_id)
|
|
202
|
+
if not warehouse or warehouse.is_deleted:
|
|
203
|
+
raise BusinessError(ErrorCode.WAREHOUSE_NOT_FOUND)
|
|
204
|
+
return warehouse
|
|
205
|
+
|
|
206
|
+
async def update(self, db: AsyncSession, warehouse_id: int, data: WarehouseUpdate, current_user: User) -> Warehouse:
|
|
207
|
+
warehouse = await self.get_by_id(db, warehouse_id)
|
|
208
|
+
for field, value in data.model_dump(exclude_unset=True).items():
|
|
209
|
+
setattr(warehouse, field, value)
|
|
210
|
+
warehouse.updated_by = current_user.id
|
|
211
|
+
await db.commit()
|
|
212
|
+
await db.refresh(warehouse)
|
|
213
|
+
return warehouse
|
|
214
|
+
|
|
215
|
+
async def delete(self, db: AsyncSession, warehouse_id: int, current_user: User) -> None:
|
|
216
|
+
warehouse = await self.get_by_id(db, warehouse_id)
|
|
217
|
+
warehouse.is_deleted = True
|
|
218
|
+
warehouse.updated_by = current_user.id
|
|
219
|
+
await db.commit()
|
|
220
|
+
|
|
221
|
+
async def _validate_code_unique(self, db: AsyncSession, code: str, exclude_id: int | None = None):
|
|
222
|
+
stmt = select(Warehouse).where(Warehouse.code == code, Warehouse.is_deleted == False)
|
|
223
|
+
if exclude_id:
|
|
224
|
+
stmt = stmt.where(Warehouse.id != exclude_id)
|
|
225
|
+
result = await db.execute(stmt)
|
|
226
|
+
if result.scalar_one_or_none():
|
|
227
|
+
raise BusinessError(ErrorCode.WAREHOUSE_CODE_DUPLICATE)
|
|
228
|
+
|
|
229
|
+
warehouse_service = WarehouseService() # 模块级单例
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**编写规则**:
|
|
233
|
+
- Service 方法负责 commit,Repository 层不提交
|
|
234
|
+
- 业务异常用 BusinessError,禁止裸 raise Exception
|
|
235
|
+
- 用 Pydantic Schema 接收参数,禁止裸 dict
|
|
236
|
+
- 返回 Model 实例,由 API 层转 Schema
|
|
237
|
+
- 校验用 _validate_ 前缀私有方法
|
|
238
|
+
- 模块级单例实例化
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 六、Pydantic Schema 规范
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
246
|
+
from datetime import datetime
|
|
247
|
+
|
|
248
|
+
class WarehouseBase(BaseModel):
|
|
249
|
+
code: str = Field(..., min_length=1, max_length=50, description="仓库编码")
|
|
250
|
+
name: str = Field(..., min_length=1, max_length=200, description="仓库名称")
|
|
251
|
+
location: str | None = Field(None, max_length=500, description="仓库位置")
|
|
252
|
+
|
|
253
|
+
class WarehouseCreate(WarehouseBase):
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
class WarehouseUpdate(BaseModel):
|
|
257
|
+
name: str | None = Field(None, min_length=1, max_length=200)
|
|
258
|
+
location: str | None = Field(None, max_length=500)
|
|
259
|
+
|
|
260
|
+
class WarehouseResponse(WarehouseBase):
|
|
261
|
+
model_config = ConfigDict(from_attributes=True)
|
|
262
|
+
id: int
|
|
263
|
+
is_active: bool
|
|
264
|
+
created_at: datetime
|
|
265
|
+
updated_at: datetime
|
|
266
|
+
|
|
267
|
+
class WarehouseListItem(BaseModel):
|
|
268
|
+
model_config = ConfigDict(from_attributes=True)
|
|
269
|
+
id: int
|
|
270
|
+
code: str
|
|
271
|
+
name: str
|
|
272
|
+
is_active: bool
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**规则**: Base 定义公共字段 | Create/Update/Response 继承复用 | Response 设 from_attributes=True | Update 全 Optional + exclude_unset
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 七、异常处理
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
class BusinessError(Exception):
|
|
283
|
+
def __init__(self, code: ErrorCode, message: str | None = None):
|
|
284
|
+
self.code = code
|
|
285
|
+
self.message = message or ERROR_MESSAGES.get(code, "未知错误")
|
|
286
|
+
|
|
287
|
+
class NotFoundError(BusinessError):
|
|
288
|
+
def __init__(self, entity: str = "资源"):
|
|
289
|
+
super().__init__(ErrorCode.NOT_FOUND, f"{entity}不存在")
|
|
290
|
+
|
|
291
|
+
# 全局异常处理
|
|
292
|
+
@app.exception_handler(BusinessError)
|
|
293
|
+
async def business_error_handler(request: Request, exc: BusinessError):
|
|
294
|
+
return JSONResponse(status_code=200, content={"code": int(exc.code), "message": exc.message, "data": None})
|
|
295
|
+
|
|
296
|
+
@app.exception_handler(Exception)
|
|
297
|
+
async def global_error_handler(request: Request, exc: Exception):
|
|
298
|
+
logger.exception("Unhandled exception", exc_info=exc)
|
|
299
|
+
return JSONResponse(status_code=500, content={"code": 1999, "message": "服务器内部错误", "data": None})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 八、配置管理
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from pydantic_settings import BaseSettings
|
|
308
|
+
|
|
309
|
+
class Settings(BaseSettings):
|
|
310
|
+
model_config = {"env_file": ".env"}
|
|
311
|
+
APP_NAME: str = "MyApp"
|
|
312
|
+
DEBUG: bool = False
|
|
313
|
+
DATABASE_URL: str = "sqlite+aiosqlite:///./data/app.db"
|
|
314
|
+
REDIS_URL: str = "redis://localhost:6379/0"
|
|
315
|
+
CELERY_BROKER_URL: str = "redis://localhost:6379/1"
|
|
316
|
+
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"
|
|
317
|
+
SECRET_KEY: str = "change-me-in-production"
|
|
318
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
319
|
+
|
|
320
|
+
settings = Settings()
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 九、代码质量
|
|
326
|
+
|
|
327
|
+
- Ruff 作为 linter + formatter(py311, line-width=120)
|
|
328
|
+
- 所有公共函数必须有类型注解 + Docstring
|
|
329
|
+
- I/O 操作必须 async/await
|
|
330
|
+
- 导入排序: 标准库 → 第三方 → 本地(Ruff isort)
|
|
331
|
+
- 最大函数长度: 50 行
|
|
332
|
+
- 最大参数个数: 5 个(超出用 Pydantic Model)
|
|
333
|
+
- 禁止魔法值,常量全大写
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 十、测试规范
|
|
338
|
+
|
|
339
|
+
命名: `test_{方法名}_{场景}_{期望结果}`
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
# tests/conftest.py
|
|
343
|
+
@pytest.fixture
|
|
344
|
+
async def db_session():
|
|
345
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
346
|
+
async with engine.begin() as conn:
|
|
347
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
348
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
|
349
|
+
async with async_session() as session:
|
|
350
|
+
yield session
|
|
351
|
+
await engine.dispose()
|
|
352
|
+
|
|
353
|
+
@pytest.fixture
|
|
354
|
+
async def client(db_session: AsyncSession):
|
|
355
|
+
app.dependency_overrides[get_db] = lambda: db_session
|
|
356
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
357
|
+
yield ac
|
|
358
|
+
app.dependency_overrides.clear()
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## 十一、安全规范
|
|
364
|
+
|
|
365
|
+
| 规则 | 说明 |
|
|
366
|
+
|------------|----------------------------------------------|
|
|
367
|
+
| 密码存储 | bcrypt / argon2,禁止明文或 MD5 |
|
|
368
|
+
| Token | JWT + 合理过期时间 |
|
|
369
|
+
| SQL 注入 | SQLAlchemy ORM 参数化,禁止字符串拼接 |
|
|
370
|
+
| 敏感信息 | 环境变量注入,禁止硬编码 |
|
|
371
|
+
| CORS | 生产环境明确配置允许的 Origin |
|
|
372
|
+
| 输入校验 | 全部通过 Pydantic Schema 校验 |
|