transport_address.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. """运输通用地址库 API"""
  2. from datetime import datetime
  3. from typing import Any, List, Optional
  4. from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
  5. from pydantic import BaseModel, ConfigDict, Field
  6. from sqlalchemy import case, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.utils.excel_export import (
  9. EXPORT_MAX_ROWS,
  10. ExportColumn,
  11. ExportValidationError,
  12. export_rows_to_xlsx_response,
  13. filter_export_columns,
  14. )
  15. from app.core.database import get_db
  16. from app.core.db_auth import DbUserContext, get_db_current_user
  17. from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
  18. from app.core.tenant_context import get_tenant_no
  19. from app.core.utils import generate_uuid
  20. from app.models.pl_models import PlTransportAddress
  21. from app.schemas.pl.transport_address import (
  22. TransportAddressCreate,
  23. TransportAddressListResponse,
  24. TransportAddressRead,
  25. TransportAddressUpdate,
  26. )
  27. from app.services.pl.transport_address_list_filters import (
  28. build_transport_address_column_filters,
  29. )
  30. router = APIRouter(tags=["运输地址库"])
  31. def _merge_query(request: Request, **known) -> dict:
  32. out = {k: v for k, v in known.items() if v not in (None, "")}
  33. for k, v in request.query_params.multi_items():
  34. if k in ("page", "pageSize"):
  35. continue
  36. if k not in out and v not in (None, ""):
  37. out[k] = v
  38. return out
  39. def _param_get(params: dict, *keys: str) -> Any:
  40. for key in keys:
  41. if key in params and params[key] not in (None, ""):
  42. return params[key]
  43. return None
  44. def _body_to_params(body: BaseModel) -> dict:
  45. data = body.model_dump(by_alias=True, exclude_none=True)
  46. for key in ("ids", "columns"):
  47. data.pop(key, None)
  48. extra = getattr(body, "model_extra", None) or {}
  49. for key, value in extra.items():
  50. if key not in data and value not in (None, ""):
  51. data[key] = value
  52. return data
  53. def _transport_address_column_filter_kwargs(params: dict) -> dict:
  54. return dict(
  55. address_name=_param_get(params, "addressName", "address_name"),
  56. address_name_in=_param_get(params, "addressNameIn", "address_name_in"),
  57. full_address=_param_get(params, "fullAddress", "full_address"),
  58. full_address_in=_param_get(params, "fullAddressIn", "full_address_in"),
  59. status=_param_get(params, "status"),
  60. status_in=_param_get(params, "statusIn", "status_in"),
  61. contact_name=_param_get(params, "contactName", "contact_name"),
  62. contact_phone=_param_get(params, "contactPhone", "contact_phone"),
  63. notes=_param_get(params, "notes"),
  64. remark=_param_get(params, "remark"),
  65. )
  66. def _address_to_read(row: PlTransportAddress) -> TransportAddressRead:
  67. return TransportAddressRead.model_validate(row)
  68. def _get_address(
  69. db: Session, tenant_no: str, address_id: str, user: DbUserContext
  70. ) -> PlTransportAddress:
  71. row = (
  72. db.query(PlTransportAddress)
  73. .filter(
  74. PlTransportAddress.id == address_id,
  75. PlTransportAddress.tenant_no == tenant_no,
  76. PlTransportAddress.del_flag == 0,
  77. )
  78. .first()
  79. )
  80. if not row:
  81. raise HTTPException(status_code=404, detail="地址不存在")
  82. return row
  83. def _build_address_query(db: Session, tenant_no: str, user: DbUserContext):
  84. # 运输地址为主数据选项,不做 create_by 数据范围过滤
  85. _ = user
  86. query = db.query(PlTransportAddress).filter(
  87. PlTransportAddress.tenant_no == tenant_no,
  88. PlTransportAddress.del_flag == 0,
  89. )
  90. return query
  91. class TransportAddressColumnFacetBody(BaseModel):
  92. """运输地址列头可选值(去重;可附带列表上下文)"""
  93. model_config = ConfigDict(extra="allow")
  94. field: str = Field(..., description="addressName | fullAddress")
  95. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  96. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  97. _TRANSPORT_ADDRESS_FACET_COLUMNS = {
  98. "addressName": PlTransportAddress.address_name,
  99. "fullAddress": PlTransportAddress.full_address,
  100. }
  101. @router.post("/address/column-facet-values")
  102. def transport_address_column_facet_values(
  103. body: TransportAddressColumnFacetBody,
  104. db: Session = Depends(get_db),
  105. user: DbUserContext = Depends(get_db_current_user),
  106. ):
  107. """运输地址列头可选值:在列表上下文(排除本列)下去重。"""
  108. from app.services.column_facet import list_column_facet_values
  109. from app.services.pl.facet_context import strip_simple_facet_self_params
  110. column = _TRANSPORT_ADDRESS_FACET_COLUMNS.get(body.field)
  111. if column is None:
  112. raise HTTPException(status_code=400, detail="不支持的列")
  113. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  114. params = strip_simple_facet_self_params(_body_to_params(body), body.field)
  115. extra_filters = build_transport_address_column_filters(
  116. **_transport_address_column_filter_kwargs(params)
  117. )
  118. qv = _param_get(params, "q")
  119. if qv:
  120. keyword = str(qv).strip().lower()
  121. extra_filters.append(
  122. or_(
  123. func.lower(PlTransportAddress.address_name).contains(keyword),
  124. func.lower(PlTransportAddress.full_address).contains(keyword),
  125. func.lower(PlTransportAddress.contact_name).contains(keyword),
  126. PlTransportAddress.contact_phone.contains(str(qv).strip()),
  127. )
  128. )
  129. rows = list_column_facet_values(
  130. db,
  131. PlTransportAddress,
  132. column,
  133. tenant_no=tenant_no,
  134. keyword=body.keyword,
  135. limit=body.limit if body.limit is not None else 500,
  136. extra_filters=extra_filters or None,
  137. )
  138. return {"list": rows}
  139. @router.get("/address", response_model=TransportAddressListResponse)
  140. def list_transport_addresses(
  141. request: Request,
  142. q: Optional[str] = Query(default=None, description="搜索名称或地址"),
  143. status: Optional[str] = Query(default=None, description="状态 1-启用 0-停用"),
  144. page: int = Query(default=1, ge=1),
  145. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  146. db: Session = Depends(get_db),
  147. user: DbUserContext = Depends(get_db_current_user),
  148. ) -> TransportAddressListResponse:
  149. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  150. params = _merge_query(request, q=q, status=status)
  151. query = _build_address_query(db, tenant_no, user)
  152. qv = _param_get(params, "q")
  153. if qv:
  154. keyword = str(qv).strip().lower()
  155. query = query.filter(
  156. or_(
  157. func.lower(PlTransportAddress.address_name).contains(keyword),
  158. func.lower(PlTransportAddress.full_address).contains(keyword),
  159. func.lower(PlTransportAddress.contact_name).contains(keyword),
  160. PlTransportAddress.contact_phone.contains(str(qv).strip()),
  161. )
  162. )
  163. column_filters = build_transport_address_column_filters(
  164. **_transport_address_column_filter_kwargs(params)
  165. )
  166. if column_filters:
  167. query = query.filter(*column_filters)
  168. query = query.order_by(
  169. case((PlTransportAddress.order_num.is_(None), 1), else_=0).asc(),
  170. PlTransportAddress.create_time.desc(),
  171. )
  172. total = query.count()
  173. offset = (page - 1) * page_size
  174. rows = query.offset(offset).limit(page_size).all()
  175. return TransportAddressListResponse(
  176. total=total,
  177. list=[_address_to_read(row) for row in rows],
  178. )
  179. @router.get("/address/all", response_model=TransportAddressListResponse)
  180. def list_all_transport_addresses(
  181. q: Optional[str] = Query(default=None, description="模糊检索:仅地址名称、详细地址"),
  182. db: Session = Depends(get_db),
  183. user: DbUserContext = Depends(get_db_current_user),
  184. ) -> TransportAddressListResponse:
  185. """选择器用:仅返回启用中的地址(不分页)。q 仅匹配 address_name、full_address。"""
  186. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  187. query = _build_address_query(db, tenant_no, user).filter(PlTransportAddress.status == "1")
  188. if q:
  189. keyword = q.strip().lower()
  190. query = query.filter(
  191. or_(
  192. func.lower(PlTransportAddress.address_name).contains(keyword),
  193. func.lower(PlTransportAddress.full_address).contains(keyword),
  194. )
  195. )
  196. query = query.order_by(
  197. case((PlTransportAddress.order_num.is_(None), 1), else_=0).asc(),
  198. PlTransportAddress.create_time.desc(),
  199. )
  200. rows = query.limit(200).all()
  201. return TransportAddressListResponse(total=len(rows), list=[_address_to_read(row) for row in rows])
  202. ADDRESS_EXPORT_COLUMNS = [
  203. ExportColumn(title="地址名称", field="addressName"),
  204. ExportColumn(title="详细地址", field="fullAddress"),
  205. ExportColumn(title="联系人", field="contactName"),
  206. ExportColumn(title="联系电话", field="contactPhone"),
  207. ExportColumn(title="注意事项", field="notes"),
  208. ExportColumn(title="状态", field="statusLabel"),
  209. ExportColumn(title="备注", field="remark"),
  210. ]
  211. class TransportAddressExportBody(BaseModel):
  212. q: Optional[str] = None
  213. status: Optional[str] = None
  214. ids: Optional[List[str]] = None
  215. columns: Optional[List[str]] = None
  216. model_config = {"extra": "allow"}
  217. @router.post("/address/export")
  218. def export_transport_addresses(
  219. body: TransportAddressExportBody,
  220. db: Session = Depends(get_db),
  221. user: DbUserContext = Depends(get_db_current_user),
  222. ):
  223. """导出运输地址列表(当前筛选全量,或 ids 已选)。"""
  224. try:
  225. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  226. params = _body_to_params(body)
  227. query = _build_address_query(db, tenant_no, user)
  228. qv = _param_get(params, "q")
  229. if qv:
  230. keyword = str(qv).strip().lower()
  231. query = query.filter(
  232. or_(
  233. func.lower(PlTransportAddress.address_name).contains(keyword),
  234. func.lower(PlTransportAddress.full_address).contains(keyword),
  235. func.lower(PlTransportAddress.contact_name).contains(keyword),
  236. PlTransportAddress.contact_phone.contains(str(qv).strip()),
  237. )
  238. )
  239. column_filters = build_transport_address_column_filters(
  240. **_transport_address_column_filter_kwargs(params)
  241. )
  242. if column_filters:
  243. query = query.filter(*column_filters)
  244. if body.ids:
  245. query = query.filter(PlTransportAddress.id.in_(body.ids))
  246. query = query.order_by(
  247. case((PlTransportAddress.order_num.is_(None), 1), else_=0).asc(),
  248. PlTransportAddress.create_time.desc(),
  249. )
  250. rows = query.limit(EXPORT_MAX_ROWS + 1).all()
  251. items = []
  252. for row in rows:
  253. data = _address_to_read(row).model_dump(by_alias=True)
  254. st = str(data.get("status") or "")
  255. data["statusLabel"] = "启用" if st == "1" else ("停用" if st == "0" else st)
  256. items.append(data)
  257. requested = None
  258. if body.columns:
  259. requested = []
  260. for key in body.columns:
  261. if key == "status":
  262. requested.append("statusLabel")
  263. else:
  264. requested.append(key)
  265. cols = filter_export_columns(ADDRESS_EXPORT_COLUMNS, requested)
  266. date_str = datetime.now().strftime("%Y%m%d")
  267. return export_rows_to_xlsx_response(
  268. db,
  269. tenant_no=tenant_no,
  270. sheet_title="运输地址",
  271. columns=cols,
  272. rows=items,
  273. filename_stem=f"运输地址_{date_str}",
  274. )
  275. except ExportValidationError as e:
  276. raise HTTPException(status_code=400, detail=str(e)) from e
  277. @router.post("/address", response_model=TransportAddressRead, status_code=status.HTTP_201_CREATED)
  278. def create_transport_address(
  279. payload: TransportAddressCreate,
  280. db: Session = Depends(get_db),
  281. user: DbUserContext = Depends(get_db_current_user),
  282. ) -> TransportAddressRead:
  283. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  284. row = PlTransportAddress(
  285. id=generate_uuid(),
  286. tenant_no=tenant_no,
  287. address_name=payload.address_name.strip(),
  288. full_address=payload.full_address.strip(),
  289. lng=payload.lng,
  290. lat=payload.lat,
  291. contact_name=payload.contact_name,
  292. contact_phone=payload.contact_phone,
  293. notes=payload.notes,
  294. status=payload.status or "1",
  295. remark=payload.remark,
  296. create_by=user.id,
  297. create_dept_id=user.dept_id,
  298. )
  299. db.add(row)
  300. db.commit()
  301. db.refresh(row)
  302. return _address_to_read(row)
  303. @router.get("/address/{id}", response_model=TransportAddressRead)
  304. def get_transport_address(
  305. id: str,
  306. db: Session = Depends(get_db),
  307. user: DbUserContext = Depends(get_db_current_user),
  308. ) -> TransportAddressRead:
  309. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  310. row = _get_address(db, tenant_no, id, user)
  311. return _address_to_read(row)
  312. @router.put("/address/{id}", response_model=TransportAddressRead)
  313. def update_transport_address(
  314. id: str,
  315. payload: TransportAddressUpdate,
  316. db: Session = Depends(get_db),
  317. user: DbUserContext = Depends(get_db_current_user),
  318. ) -> TransportAddressRead:
  319. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  320. row = _get_address(db, tenant_no, id, user)
  321. for key, value in payload.model_dump(exclude_unset=True).items():
  322. if key in ("address_name", "full_address") and isinstance(value, str):
  323. value = value.strip()
  324. setattr(row, key, value)
  325. row.update_by = user.id
  326. db.commit()
  327. db.refresh(row)
  328. return _address_to_read(row)
  329. @router.delete("/address/{id}", status_code=status.HTTP_204_NO_CONTENT)
  330. def delete_transport_address(
  331. id: str,
  332. db: Session = Depends(get_db),
  333. user: DbUserContext = Depends(get_db_current_user),
  334. ) -> None:
  335. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  336. row = _get_address(db, tenant_no, id, user)
  337. assert_row_deletable_by_creator(row, user_info_from_context(user))
  338. row.del_flag = 1
  339. row.update_by = user.id
  340. db.commit()