adspecs 0.1.18 → 0.1.20

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.
Files changed (88) hide show
  1. package/.adspecs/extensions/git/commands/adspecs.git.initialize.md +4 -4
  2. package/.adspecs/extensions/git/extension.yml +1 -1
  3. package/.adspecs/extensions/git/git-config.yml +17 -2
  4. package/.adspecs/extensions.yml +32 -0
  5. package/.adspecs/feature.json +16 -16
  6. package/.adspecs/feature.yml +28 -28
  7. package/.adspecs/paths.json +17 -0
  8. package/.adspecs/templates/02-/344/272/247/345/223/201/351/234/200/346/261/202/350/257/264/346/230/216/344/271/246/346/250/241/346/235/277.md +4 -0
  9. package/.adspecs/workflows/adspecs/workflow.yml +5 -4
  10. package/.claude-plugin/marketplace.json +23 -23
  11. package/.claude-plugin/plugin.json +18 -18
  12. package/.qoder-plugin/plugin.json +31 -31
  13. package/CLAUDE.md +1 -5
  14. package/README.md +395 -395
  15. package/bin/adspecs.js +129 -129
  16. package/hooks/commit-queue.js +245 -245
  17. package/hooks/hooks.json +63 -63
  18. package/hooks/session-start.js +44 -44
  19. package/hooks/wiki-queue.js +127 -127
  20. package/package.json +61 -61
  21. package/references/ant6-front-standard/index.md +99 -99
  22. package/references/antd-front-demo/.env.develop +12 -0
  23. package/references/antd-front-demo/.env.production +5 -0
  24. package/references/antd-front-demo/package-lock.json +2 -2
  25. package/references/antd-front-demo/package.json +1 -1
  26. package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
  27. package/references/ecp-end-standard/index.md +63 -63
  28. 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
  29. 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
  30. 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
  31. package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
  32. package/scripts/postinstall.js +107 -107
  33. package/scripts/sync-version.js +105 -105
  34. package/skills/.claude/.wiki-update-queue +26 -26
  35. package/skills/adspecs-constitution/SKILL.md +157 -0
  36. package/skills/adspecs-export-word/SKILL.md +498 -498
  37. package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
  38. package/skills/adspecs-export-word/references/package-lock.json +220 -220
  39. package/skills/adspecs-export-word/references/package.json +10 -10
  40. package/skills/adspecs-front-prototype/SKILL.md +405 -0
  41. package/skills/adspecs-git-commit/SKILL.md +23 -8
  42. package/skills/adspecs-git-initialize/SKILL.md +54 -0
  43. package/skills/adspecs-plan/SKILL.md +59 -69
  44. package/skills/adspecs-prd/SKILL.md +13 -5
  45. package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
  46. package/skills/adspecs-tasks/SKILL.md +175 -204
  47. package/skills/adspecs-update-status/SKILL.md +382 -382
  48. package/skills/adspecs-utest/SKILL.md +107 -116
  49. package/skills/grill-me/SKILL.md +7 -0
  50. package/skills/grill-me/agents/openai.yaml +5 -0
  51. package/skills/playwright-cli/SKILL.md +420 -0
  52. package/skills/playwright-cli/references/element-attributes.md +23 -0
  53. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  54. package/skills/playwright-cli/references/request-mocking.md +87 -0
  55. package/skills/playwright-cli/references/running-code.md +241 -0
  56. package/skills/playwright-cli/references/session-management.md +225 -0
  57. package/skills/playwright-cli/references/storage-state.md +275 -0
  58. package/skills/playwright-cli/references/test-generation.md +433 -0
  59. package/skills/playwright-cli/references/tracing.md +139 -0
  60. package/skills/playwright-cli/references/video-recording.md +143 -0
  61. package/skills/playwright-trace/SKILL.md +171 -0
  62. package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
  63. package/skills/wiki-update/SKILL.md +232 -232
  64. package/src/commands/doctor.js +197 -197
  65. package/src/commands/init.js +83 -83
  66. package/src/commands/plugin.js +165 -165
  67. package/src/commands/update.js +87 -87
  68. package/src/lib/area-scanner.js +129 -129
  69. package/src/lib/copier.js +104 -104
  70. package/src/lib/dir-utils.js +133 -133
  71. package/src/lib/json-merge.js +114 -114
  72. package/src/lib/paths-defaults.js +37 -37
  73. package/src/lib/prompts.js +347 -345
  74. package/src/lib/readme-gen.js +143 -143
  75. package/src/lib/report.js +327 -327
  76. package/src/lib/scaffolder.js +518 -518
  77. package/src/lib/short-name.js +36 -36
  78. package/src/utils.js +80 -80
  79. package/references/antd-front-demo/.env +0 -15
  80. package/references/antd-front-demo/src/pages/aps/coil/components/CoilDrawer.tsx +0 -141
  81. package/references/antd-front-demo/src/pages/aps/coil/index.tsx +0 -209
  82. package/references/antd-front-demo/src/pages/aps/optimization/index.tsx +0 -192
  83. package/references/antd-front-demo/src/pages/aps/order/components/OrderDrawer.tsx +0 -251
  84. package/references/antd-front-demo/src/pages/aps/order/index.tsx +0 -273
  85. package/references/antd-front-demo/src/pages/aps/param/index.tsx +0 -153
  86. package/references/antd-front-demo/src/pages/aps/plan/components/CuttingPlanVisualization.tsx +0 -821
  87. package/references/antd-front-demo/src/pages/aps/plan/index.tsx +0 -254
  88. package/references/antd-front-demo/src/pages/aps/task/index.tsx +0 -190
@@ -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 |