"""fin_product 产品仓储:列表搜索(关键字/类型/风险/状态筛选 + 排序 + 分页)。""" from __future__ import annotations from sqlalchemy import or_, select from model.fin_product import FinProduct from repositories.base import BaseRepository from utils.exceptions import ParamError # 排序字段白名单:防止 sort_by 注入,字符串映射到 ORM 列(不拼 SQL) SORTABLE_FIELDS = { "expected_return": FinProduct.expected_return, "nav": FinProduct.nav, "fee_rate": FinProduct.fee_rate, "risk_level": FinProduct.risk_level, "create_time": FinProduct.create_time, } class ProductRepo(BaseRepository): model = FinProduct async def get_by_code(self, product_code: str) -> FinProduct | None: return await self.db.scalar( select(FinProduct).where(FinProduct.product_code == product_code) ) async def search( self, *, keyword: str | None = None, product_type: str | None = None, risk_level: str | None = None, status: str | None = "在售", sort_by: str = "create_time", sort_order: str = "desc", limit: int = 10, offset: int = 0, ) -> tuple[list[FinProduct], int]: conds = [] if status and status != "全部": conds.append(FinProduct.status == status) if product_type: conds.append(FinProduct.product_type == product_type) if risk_level: conds.append(FinProduct.risk_level == risk_level) if keyword: like = f"%{keyword}%" conds.append( or_( FinProduct.product_code.like(like), FinProduct.product_name.like(like), FinProduct.fund_manager.like(like), ) ) col = SORTABLE_FIELDS.get(sort_by) if col is None: raise ParamError(f"不支持的排序字段: {sort_by}") order_col = col.desc() if sort_order == "desc" else col.asc() stmt = ( select(FinProduct) .where(*conds) .order_by(order_col) .limit(limit) .offset(offset) ) items = list((await self.db.scalars(stmt)).all()) total = await self.count(where=conds) return items, total