Skip to content

Commit 87f1fa5

Browse files
committed
feat: 试剂订单增强与前后端优化
## 新增功能 - 新增 AutoComplete 组件(前端模糊匹配搜索) - 新增 formConfigs.tsx 表单配置 - 新增 options.ts 选项配置 - 新增 AutoComplete.tsx 组件 - 新增 user_utils.py 通用用户查询函数 - 新增多个文档分析文件 ## 后端优化 - 试剂订单 API 添加缓存机制 (REAGENT_ORDER_CACHE) - 导出 CSV 时填充申请人姓名 - 统一使用 batch_get_user_names 消除 N+1 查询 - 修复规格验证正则 (避免 1.5.5 等非法格式) - ConsumableOrder 改为 initial_quantity + unit 存储 - ReagentOrder.price 改为必填 (gt=0) ## 前端优化 - 添加 ReagentOrderStatus/ConsumableOrderStatus 枚举 - 删除无效的 supplier 字段 - price 验证改为大于 0 - 输入验证迁移到 validationSchemas.ts (Valibot) ## 组件更新 - BaseForm 支持 autocomplete 类型 - MoleculeStructure 组件优化 - Input/QuantityIndicator/StatusBadge/Dialog 组件完善 ## 代码清理 - 删除 inputValidation.ts (迁移到 valibot) - 删除 Inventory-old.tsx - 删除 .claude/.kilocode/.sonar 配置
1 parent 284b765 commit 87f1fa5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+29207
-3231
lines changed

.claude/GIT_STRATEGY.md

Lines changed: 0 additions & 103 deletions
This file was deleted.

.claude/Rules.md

Lines changed: 0 additions & 27 deletions
This file was deleted.

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ env/
2424
!.vscode/extensions.json
2525
.idea/
2626
.cursor/
27+
.github/
2728
.history/
2829
.claude/
2930
.kilocode/
@@ -58,3 +59,13 @@ dist/
5859

5960
# Template/External projects (do not commit)
6061
shadcn-admin/
62+
63+
64+
# Local library files
65+
frontend/public/lib/
66+
67+
# Docs subdirectories (keep root docs)
68+
docs/done/
69+
docs/exp/
70+
docs/plans/
71+
docs/prompt/

.kilocode/mcp.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

.kilocode/rules/GIT_STRATEGY.md

Lines changed: 0 additions & 36 deletions
This file was deleted.

.kilocode/rules/Rules.md

Lines changed: 0 additions & 27 deletions
This file was deleted.

.sonar/.sonar_lock

Whitespace-only changes.

.sonar/report-task.txt

Lines changed: 0 additions & 6 deletions
This file was deleted.

app/api/consumable_orders.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,20 @@
2020
)
2121
from app.models.user import User
2222
from app.services.image_service import process_uploaded_image
23+
from app.services.spec_utils import parse_specification, SpecificationError, format_specification
24+
from app.services.user_utils import batch_get_user_names
2325

2426
router = APIRouter(prefix="/consumable-orders", tags=["ConsumableOrders"])
2527

2628

29+
def _add_specification(item_dict: dict) -> dict:
30+
"""Add computed specification field to order response dict"""
31+
initial = item_dict.get("initial_quantity", 0)
32+
unit = item_dict.get("unit", "")
33+
item_dict["specification"] = format_specification(initial, unit)
34+
return item_dict
35+
36+
2737
def get_consumable_order_by_id(db: Session, order_id: int) -> Optional[ConsumableOrder]:
2838
"""Get consumable order by ID"""
2939
return db.get(ConsumableOrder, order_id)
@@ -36,14 +46,24 @@ def create_consumable_order(
3646
db: Session = Depends(get_db)
3747
):
3848
"""Create a new consumable order"""
49+
# Parse specification to get initial_quantity and unit
50+
try:
51+
initial_quantity, unit = parse_specification(order.specification)
52+
except SpecificationError as e:
53+
raise HTTPException(
54+
status_code=status.HTTP_400_BAD_REQUEST,
55+
detail=f"Invalid specification format: {e}"
56+
)
57+
3958
# Create order
4059
db_order = ConsumableOrder(
4160
name=order.name,
4261
english_name=order.english_name,
4362
alias=order.alias,
4463
category=order.category,
4564
brand=order.brand,
46-
specification=order.specification,
65+
initial_quantity=initial_quantity,
66+
unit=unit,
4767
quantity=order.quantity,
4868
price=order.price,
4969
order_reason=order.order_reason,
@@ -111,15 +131,11 @@ def list_consumable_orders(
111131

112132
# Enrich with applicant names
113133
applicant_ids = {o.applicant_id for o in orders if o.applicant_id}
114-
users_map: dict[int, str] = {}
115-
if applicant_ids:
116-
from app.models.user import User as UserModel
117-
users = db.exec(select(UserModel).where(UserModel.id.in_(applicant_ids))).all()
118-
users_map = {u.id: u.full_name or u.username for u in users}
134+
users_map = batch_get_user_names(db, applicant_ids)
119135

120136
return {
121137
"data": [
122-
{**ConsumableOrderResponse.model_validate(o).model_dump(), "applicant_name": users_map.get(o.applicant_id, "")}
138+
_add_specification({**ConsumableOrderResponse.model_validate(o).model_dump(), "applicant_name": users_map.get(o.applicant_id, "")})
123139
for o in orders
124140
],
125141
"total": total,
@@ -287,7 +303,7 @@ def get_my_consumable_orders(
287303
"order_id": order.id,
288304
"name": order.name,
289305
"english_name": order.english_name,
290-
"specification": order.specification,
306+
"specification": format_specification(order.initial_quantity, order.unit),
291307
"quantity": order.quantity,
292308
"price": order.price,
293309
"is_hazardous": order.is_hazardous,

app/api/inventory.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@
3333
from app.core.time_utils import get_utc_now
3434
from app.services.cas_utils import normalize_cas
3535
from app.services.internal_code import generate_internal_code
36-
from app.services.spec_utils import parse_specification, SpecificationError
36+
from app.services.spec_utils import parse_specification, SpecificationError, format_specification
3737
from app.services.pinyin_utils import compute_pinyin_fields
38+
from app.services.user_utils import batch_get_user_names
3839

3940
logger = logging.getLogger(__name__)
4041

@@ -127,7 +128,7 @@ def validate_uploaded_file(file: UploadFile) -> None:
127128
# ==================== Search Cache ====================
128129
# 简单内存缓存,用于减少重复搜索查询
129130
SEARCH_CACHE: Dict[str, tuple[Any, datetime]] = {}
130-
CACHE_TTL_SECONDS = 60 # 缓存有效期60秒
131+
CACHE_TTL_SECONDS = 10 # 缓存有效期10秒,与前端refetchInterval匹配
131132

132133

133134
def _get_cached_result(cache_key: str) -> Optional[Dict[str, Any]]:
@@ -225,14 +226,7 @@ def _add_specification(item_dict: dict) -> dict:
225226
"""Add computed specification field to inventory response dict"""
226227
initial = item_dict.get("initial_quantity", 0)
227228
unit = item_dict.get("unit", "")
228-
# Format: "500 ml" or "250.5 ml" (no trailing zeros, space between number and unit)
229-
if initial == int(initial):
230-
# No decimal part, show as integer
231-
formatted = f"{int(initial)} {unit}"
232-
else:
233-
# Has decimal part, keep the decimal without trailing zeros
234-
formatted = f"{float(initial)} {unit}"
235-
item_dict["specification"] = formatted if initial else None
229+
item_dict["specification"] = format_specification(initial, unit)
236230
return item_dict
237231

238232

@@ -602,7 +596,7 @@ def import_inventory(
602596
@router.get("/")
603597
def list_inventory(
604598
skip: int = 0,
605-
limit: int = 0, # 0 表示不分页,返回全部数据
599+
limit: int = 50, # 默认分页50条,与前端保持一致
606600
status_filter: Optional[InventoryStatus] = None,
607601
cas_filter: Optional[str] = None,
608602
hazardous_only: bool = False,
@@ -617,8 +611,13 @@ def list_inventory(
617611
# 生成缓存key(包含所有搜索参数,包括分页和排序)
618612
cache_key = f"list:{skip}:{limit}:{search or ''}:{status_filter or ''}:{cas_filter or ''}:{hazardous_only}:{search_field or ''}:{fuzzy}:{sort_by or ''}:{sort_order or ''}"
619613

620-
# 尝试从缓存获取(仅当是不分页查询或第一页时)
621-
if limit == 0 or skip == 0:
614+
# 尝试从缓存获取(仅当是第一页且无搜索条件时)
615+
# 这样可以避免分页数据不一致的问题
616+
is_first_page = skip == 0
617+
has_search = bool(search or status_filter or cas_filter or hazardous_only or sort_by)
618+
should_use_cache = is_first_page and not has_search
619+
620+
if should_use_cache:
622621
cached = _get_cached_result(cache_key)
623622
if cached is not None:
624623
# 返回缓存结果,但更新分页参数
@@ -775,12 +774,8 @@ def norm_field(field):
775774
if item.created_by_id:
776775
user_ids.add(item.created_by_id)
777776

778-
# 用一条 SQL IN 语句,一次性查出这 50 条数据对应的所有用户
779-
users_map = {}
780-
if user_ids:
781-
users = db.exec(select(User).where(User.id.in_(user_ids))).all()
782-
# 构建内存字典 { user_id: "姓名" },查找速度是 O(1)
783-
users_map = {u.id: (u.full_name or u.username) for u in users}
777+
# 使用通用函数批量查询用户姓名
778+
users_map = batch_get_user_names(db, user_ids)
784779

785780
# 在内存中完成数据组装,绝不再向数据库发请求
786781
result_data = []
@@ -804,8 +799,8 @@ def norm_field(field):
804799
"limit": limit,
805800
}
806801

807-
# 缓存查询结果(不分页或第一页时缓存
808-
if limit == 0 or skip == 0:
802+
# 缓存查询结果(仅当是第一页且无搜索条件时
803+
if should_use_cache:
809804
# 缓存时不包含分页参数
810805
cache_data = {
811806
"data": result["data"],

0 commit comments

Comments
 (0)