market_mobile.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095
  1. """市场专员/地区经理移动端专用 API。"""
  2. from __future__ import annotations
  3. from typing import Literal, Optional
  4. from fastapi import APIRouter, Depends, File, Query, UploadFile
  5. from sqlalchemy.orm import Session
  6. from app.core.database import get_db
  7. from app.core.mobile_menu_guard import (
  8. require_market_menu_code,
  9. user_has_tenant_menu_perm,
  10. )
  11. from app.core.response import error_response, success_response
  12. from app.schemas.market_mobile import (
  13. MarketAttachmentDownloadBody,
  14. MarketFreightCreateBody,
  15. MarketFreightItemsBody,
  16. MarketFreightUpdateBody,
  17. MarketPaymentTransferBody,
  18. MarketPaymentTransferCreateBody,
  19. MarketPresignOrderCreateBody,
  20. MarketPresignOrderBody,
  21. MarketPresignTransferBody,
  22. MarketProtocolBody,
  23. )
  24. from app.services import (
  25. market_freight,
  26. market_payment,
  27. market_presign,
  28. market_protocol,
  29. market_support,
  30. )
  31. from app.services.market_field_scope import MarketFieldContext, ScopeKind
  32. from app.services.web_auth_roles import resolve_web_role_for_user
  33. router = APIRouter(tags=["移动市场"])
  34. def _context(user: dict, scope_kind: ScopeKind) -> MarketFieldContext:
  35. return MarketFieldContext(
  36. tenant_no=str(user.get("tenant_no") or ""),
  37. user_id=str(user.get("user_id") or ""),
  38. scope_kind=scope_kind,
  39. )
  40. def _has_menu(db: Session, user: dict, permission: str) -> bool:
  41. tenant_no = str(user.get('tenant_no') or '').strip()
  42. user_id = str(user.get('user_id') or '').strip()
  43. return resolve_web_role_for_user(
  44. db, user_id, tenant_no=tenant_no
  45. ) == 'super_admin' or user_has_tenant_menu_perm(
  46. db,
  47. user_id=user_id,
  48. tenant_no=tenant_no,
  49. perm_code=permission,
  50. )
  51. def _register_presign_routes(scope_kind: ScopeKind) -> None:
  52. prefix = f"/mini/market/{scope_kind}/presign-orders"
  53. permission = f"mini:market:{scope_kind}:presign"
  54. permission_guard = require_market_menu_code(permission)
  55. @router.get(prefix)
  56. async def list_orders(
  57. page: int = Query(1, ge=1),
  58. page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
  59. keyword: Optional[str] = None,
  60. order_status: Optional[str] = Query(None, alias="orderStatus"),
  61. user: dict = Depends(permission_guard),
  62. db: Session = Depends(get_db),
  63. ):
  64. ctx = _context(user, scope_kind)
  65. items, total = market_presign.list_presign_orders(
  66. db,
  67. ctx=ctx,
  68. user_info=user,
  69. page=page,
  70. page_size=page_size,
  71. keyword=keyword,
  72. order_status=order_status,
  73. )
  74. items = [
  75. market_support.decorate_allowed_actions(
  76. 'presign', item, scope_kind=scope_kind
  77. )
  78. for item in items
  79. ]
  80. return success_response(
  81. {
  82. "list": items,
  83. "total": total,
  84. "page": page,
  85. "pageSize": page_size,
  86. "allowedActions": market_support.allowed_actions(
  87. 'presign-list', status='', scope_kind=scope_kind
  88. ),
  89. }
  90. )
  91. @router.get(f"{prefix}/form-options")
  92. async def presign_form_options(
  93. region_id: Optional[str] = Query(None, alias="regionId"),
  94. business_id: Optional[str] = Query(None, alias="businessId"),
  95. user_keyword: Optional[str] = Query(None, alias='userKeyword', max_length=64),
  96. user: dict = Depends(permission_guard),
  97. db: Session = Depends(get_db),
  98. ):
  99. try:
  100. return success_response(
  101. market_support.get_form_options(
  102. db,
  103. module="presign",
  104. ctx=_context(user, scope_kind),
  105. user_info=user,
  106. region_id=region_id,
  107. business_id=business_id,
  108. user_keyword=user_keyword,
  109. )
  110. )
  111. except (PermissionError, ValueError) as exc:
  112. return error_response(400, str(exc))
  113. @router.get(f"{prefix}/{{order_id}}")
  114. async def get_order(
  115. order_id: str,
  116. user: dict = Depends(permission_guard),
  117. db: Session = Depends(get_db),
  118. ):
  119. return success_response(
  120. market_support.decorate_allowed_actions(
  121. 'presign',
  122. market_presign.get_presign_order(
  123. db,
  124. order_id=order_id,
  125. ctx=_context(user, scope_kind),
  126. user_info=user,
  127. ),
  128. scope_kind=scope_kind,
  129. detail=True,
  130. )
  131. )
  132. @router.post(prefix)
  133. async def create_order(
  134. body: MarketPresignOrderCreateBody,
  135. user: dict = Depends(permission_guard),
  136. db: Session = Depends(get_db),
  137. ):
  138. try:
  139. data = market_presign.create_presign_order(
  140. db,
  141. body=body,
  142. ctx=_context(user, scope_kind),
  143. user_info=user,
  144. )
  145. return success_response(data, message="创建成功")
  146. except (PermissionError, ValueError) as exc:
  147. return error_response(400, str(exc))
  148. @router.put(f"{prefix}/{{order_id}}")
  149. async def update_order(
  150. order_id: str,
  151. body: MarketPresignOrderBody,
  152. user: dict = Depends(permission_guard),
  153. db: Session = Depends(get_db),
  154. ):
  155. try:
  156. data = market_presign.update_presign_order(
  157. db,
  158. order_id=order_id,
  159. body=body,
  160. ctx=_context(user, scope_kind),
  161. user_info=user,
  162. )
  163. return success_response(data, message="保存成功")
  164. except (PermissionError, ValueError) as exc:
  165. return error_response(400, str(exc))
  166. @router.delete(f"{prefix}/{{order_id}}")
  167. async def delete_order(
  168. order_id: str,
  169. user: dict = Depends(permission_guard),
  170. db: Session = Depends(get_db),
  171. ):
  172. try:
  173. market_presign.delete_presign_order(
  174. db,
  175. order_id=order_id,
  176. ctx=_context(user, scope_kind),
  177. user_info=user,
  178. )
  179. return success_response(message="删除成功")
  180. except PermissionError as exc:
  181. return error_response(403, str(exc))
  182. except ValueError as exc:
  183. return error_response(400, str(exc))
  184. @router.post(f"{prefix}/{{order_id}}/transfers")
  185. async def create_transfer(
  186. order_id: str,
  187. body: MarketPresignTransferBody,
  188. user: dict = Depends(permission_guard),
  189. db: Session = Depends(get_db),
  190. ):
  191. try:
  192. data = market_presign.create_presign_transfer(
  193. db,
  194. order_id=order_id,
  195. body=body,
  196. ctx=_context(user, scope_kind),
  197. user_info=user,
  198. )
  199. return success_response(data, message="创建成功")
  200. except PermissionError as exc:
  201. return error_response(403, str(exc))
  202. except ValueError as exc:
  203. return error_response(400, str(exc))
  204. transfer_prefix = f"/mini/market/{scope_kind}/presign-transfers"
  205. @router.put(f"{transfer_prefix}/{{transfer_id}}")
  206. async def update_transfer(
  207. transfer_id: str,
  208. body: MarketPresignTransferBody,
  209. user: dict = Depends(permission_guard),
  210. db: Session = Depends(get_db),
  211. ):
  212. try:
  213. data = market_presign.update_presign_transfer(
  214. db,
  215. transfer_id=transfer_id,
  216. body=body,
  217. ctx=_context(user, scope_kind),
  218. user_info=user,
  219. )
  220. return success_response(data, message="保存成功")
  221. except PermissionError as exc:
  222. return error_response(403, str(exc))
  223. except ValueError as exc:
  224. return error_response(400, str(exc))
  225. @router.delete(f"{transfer_prefix}/{{transfer_id}}")
  226. async def delete_transfer(
  227. transfer_id: str,
  228. user: dict = Depends(permission_guard),
  229. db: Session = Depends(get_db),
  230. ):
  231. try:
  232. market_presign.delete_presign_transfer(
  233. db,
  234. transfer_id=transfer_id,
  235. ctx=_context(user, scope_kind),
  236. user_info=user,
  237. )
  238. return success_response(message="删除成功")
  239. except PermissionError as exc:
  240. return error_response(403, str(exc))
  241. except ValueError as exc:
  242. return error_response(400, str(exc))
  243. @router.post(f"{transfer_prefix}/{{transfer_id}}/submit")
  244. async def submit_transfer(
  245. transfer_id: str,
  246. user: dict = Depends(permission_guard),
  247. db: Session = Depends(get_db),
  248. ):
  249. try:
  250. data = market_presign.submit_presign_transfer(
  251. db,
  252. transfer_id=transfer_id,
  253. ctx=_context(user, scope_kind),
  254. user_info=user,
  255. )
  256. return success_response(data, message="已提交财务确认")
  257. except PermissionError as exc:
  258. return error_response(403, str(exc))
  259. except ValueError as exc:
  260. return error_response(400, str(exc))
  261. @router.post(f"{transfer_prefix}/{{transfer_id}}/withdraw")
  262. async def withdraw_transfer(
  263. transfer_id: str,
  264. user: dict = Depends(permission_guard),
  265. db: Session = Depends(get_db),
  266. ):
  267. try:
  268. data = market_presign.withdraw_presign_transfer(
  269. db,
  270. transfer_id=transfer_id,
  271. ctx=_context(user, scope_kind),
  272. user_info=user,
  273. )
  274. return success_response(data, message="已撤回")
  275. except PermissionError as exc:
  276. return error_response(403, str(exc))
  277. except ValueError as exc:
  278. return error_response(400, str(exc))
  279. @router.post(f"{prefix}/{{order_id}}/convert")
  280. async def convert_order(
  281. order_id: str,
  282. user: dict = Depends(permission_guard),
  283. db: Session = Depends(get_db),
  284. ):
  285. try:
  286. data = market_presign.convert_presign_order(
  287. db,
  288. order_id=order_id,
  289. ctx=_context(user, scope_kind),
  290. user_info=user,
  291. )
  292. return success_response(data, message="已转正式协议")
  293. except (PermissionError, ValueError) as exc:
  294. return error_response(400, str(exc))
  295. _register_presign_routes("specialist")
  296. _register_presign_routes("manager")
  297. def _register_protocol_routes(scope_kind: ScopeKind) -> None:
  298. prefix = f'/mini/market/{scope_kind}/protocols'
  299. permission = f'mini:market:{scope_kind}:protocol'
  300. permission_guard = require_market_menu_code(permission)
  301. @router.get(prefix)
  302. async def list_protocols(
  303. page: int = Query(1, ge=1),
  304. page_size: int = Query(10, ge=1, le=100, alias='pageSize'),
  305. keyword: Optional[str] = None,
  306. source: Optional[Literal['cell_storage', 'research_donation']] = None,
  307. protocol_status: Optional[str] = Query(None, alias='protocolStatus'),
  308. create_time_start: Optional[str] = Query(None, alias='createTimeStart'),
  309. create_time_end: Optional[str] = Query(None, alias='createTimeEnd'),
  310. sample_collect_time_start: Optional[str] = Query(
  311. None, alias='sampleCollectTimeStart'
  312. ),
  313. sample_collect_time_end: Optional[str] = Query(
  314. None, alias='sampleCollectTimeEnd'
  315. ),
  316. user: dict = Depends(permission_guard),
  317. db: Session = Depends(get_db),
  318. ):
  319. try:
  320. items, total = market_protocol.list_protocols(
  321. db,
  322. ctx=_context(user, scope_kind),
  323. user_info=user,
  324. page=page,
  325. page_size=page_size,
  326. keyword=keyword,
  327. source=source,
  328. protocol_status=protocol_status,
  329. create_time_start=create_time_start,
  330. create_time_end=create_time_end,
  331. sample_collect_time_start=sample_collect_time_start,
  332. sample_collect_time_end=sample_collect_time_end,
  333. )
  334. except ValueError as exc:
  335. return error_response(400, str(exc))
  336. items = [
  337. market_support.decorate_allowed_actions(
  338. 'protocol', item, scope_kind=scope_kind
  339. )
  340. for item in items
  341. ]
  342. return success_response(
  343. {
  344. 'list': items,
  345. 'total': total,
  346. 'page': page,
  347. 'pageSize': page_size,
  348. 'allowedActions': market_support.allowed_actions(
  349. 'protocol-list', status='', scope_kind=scope_kind
  350. ),
  351. }
  352. )
  353. @router.get(f'{prefix}/form-options')
  354. async def protocol_form_options(
  355. region_id: Optional[str] = Query(None, alias='regionId'),
  356. business_id: Optional[str] = Query(None, alias='businessId'),
  357. code_filter: Optional[str] = Query(None, alias='codeFilter'),
  358. keyword: Optional[str] = Query(None, max_length=64),
  359. user: dict = Depends(permission_guard),
  360. db: Session = Depends(get_db),
  361. ):
  362. try:
  363. return success_response(
  364. market_support.get_form_options(
  365. db,
  366. module='protocol',
  367. ctx=_context(user, scope_kind),
  368. user_info=user,
  369. region_id=region_id,
  370. business_id=business_id,
  371. code_filter=code_filter,
  372. sample_code_keyword=keyword,
  373. )
  374. )
  375. except (PermissionError, ValueError) as exc:
  376. return error_response(400, str(exc))
  377. @router.get(f'{prefix}/customers')
  378. async def protocol_customers(
  379. keyword: Optional[str] = None,
  380. page: int = Query(1, ge=1),
  381. page_size: int = Query(20, ge=1, le=100, alias='pageSize'),
  382. user: dict = Depends(permission_guard),
  383. db: Session = Depends(get_db),
  384. ):
  385. try:
  386. return success_response(
  387. market_support.search_customers_for_protocol(
  388. db,
  389. ctx=_context(user, scope_kind),
  390. user_info=user,
  391. keyword=keyword,
  392. page=page,
  393. page_size=page_size,
  394. )
  395. )
  396. except (PermissionError, ValueError) as exc:
  397. return error_response(400, str(exc))
  398. @router.get(f'{prefix}/{{protocol_id}}')
  399. async def get_protocol(
  400. protocol_id: str,
  401. source: Optional[
  402. Literal['cell_storage', 'research_donation']
  403. ] = Query(None),
  404. user: dict = Depends(permission_guard),
  405. db: Session = Depends(get_db),
  406. ):
  407. return success_response(
  408. market_support.decorate_allowed_actions(
  409. 'protocol',
  410. market_protocol.get_protocol(
  411. db,
  412. source=source,
  413. protocol_id=protocol_id,
  414. ctx=_context(user, scope_kind),
  415. user_info=user,
  416. ),
  417. scope_kind=scope_kind,
  418. has_payment_permission=_has_menu(
  419. db, user, f'mini:market:{scope_kind}:payment'
  420. ),
  421. )
  422. )
  423. @router.post(prefix)
  424. async def create_protocol(
  425. body: MarketProtocolBody,
  426. user: dict = Depends(permission_guard),
  427. db: Session = Depends(get_db),
  428. ):
  429. try:
  430. data = market_protocol.create_protocol(
  431. db,
  432. body=body,
  433. ctx=_context(user, scope_kind),
  434. user_info=user,
  435. )
  436. return success_response(data, message='创建成功')
  437. except (PermissionError, ValueError) as exc:
  438. return error_response(400, str(exc))
  439. @router.put(f'{prefix}/{{protocol_id}}')
  440. async def update_protocol(
  441. protocol_id: str,
  442. body: MarketProtocolBody,
  443. user: dict = Depends(permission_guard),
  444. db: Session = Depends(get_db),
  445. ):
  446. try:
  447. data = market_protocol.update_protocol(
  448. db,
  449. source=body.source,
  450. protocol_id=protocol_id,
  451. body=body,
  452. ctx=_context(user, scope_kind),
  453. user_info=user,
  454. )
  455. return success_response(data, message='保存成功')
  456. except (PermissionError, ValueError) as exc:
  457. return error_response(400, str(exc))
  458. @router.delete(f'{prefix}/{{protocol_id}}')
  459. async def delete_protocol(
  460. protocol_id: str,
  461. source: Literal['cell_storage', 'research_donation'] = Query(...),
  462. user: dict = Depends(permission_guard),
  463. db: Session = Depends(get_db),
  464. ):
  465. try:
  466. market_protocol.delete_protocol(
  467. db,
  468. source=source,
  469. protocol_id=protocol_id,
  470. ctx=_context(user, scope_kind),
  471. user_info=user,
  472. )
  473. return success_response(message='删除成功')
  474. except PermissionError as exc:
  475. return error_response(403, str(exc))
  476. except ValueError as exc:
  477. return error_response(400, str(exc))
  478. _register_protocol_routes('specialist')
  479. _register_protocol_routes('manager')
  480. def _register_payment_routes(scope_kind: ScopeKind) -> None:
  481. prefix = f'/mini/market/{scope_kind}'
  482. chase_guard = require_market_menu_code(
  483. f'mini:market:{scope_kind}:payment-chase'
  484. )
  485. payment_guard = require_market_menu_code(
  486. f'mini:market:{scope_kind}:payment'
  487. )
  488. @router.get(f'{prefix}/payment-chase')
  489. async def list_payment_chase(
  490. page: int = Query(1, ge=1),
  491. page_size: int = Query(10, ge=1, le=100, alias='pageSize'),
  492. keyword: Optional[str] = None,
  493. chase_type: Optional[Literal['overdue', 'upcoming']] = Query(
  494. None, alias='chaseType'
  495. ),
  496. pay_mode: Optional[str] = Query(None, alias='payMode'),
  497. sales_user_id: Optional[str] = Query(None, alias='salesUserId'),
  498. user: dict = Depends(chase_guard),
  499. db: Session = Depends(get_db),
  500. ):
  501. items, total = market_payment.list_payment_chase(
  502. db,
  503. ctx=_context(user, scope_kind),
  504. user_info=user,
  505. page=page,
  506. page_size=page_size,
  507. keyword=keyword,
  508. chase_type=chase_type,
  509. pay_mode=pay_mode,
  510. sales_user_id=sales_user_id,
  511. )
  512. items = [
  513. market_support.decorate_allowed_actions(
  514. 'payment-chase',
  515. item,
  516. scope_kind=scope_kind,
  517. )
  518. for item in items
  519. ]
  520. return success_response(
  521. {'list': items, 'total': total, 'page': page, 'pageSize': page_size}
  522. )
  523. @router.get(f'{prefix}/payment-chase/{{schedule_id}}')
  524. async def get_payment_chase_detail(
  525. schedule_id: str,
  526. view: Literal['payment', 'protocol'] = Query(...),
  527. user: dict = Depends(chase_guard),
  528. db: Session = Depends(get_db),
  529. ):
  530. try:
  531. return success_response(
  532. market_payment.get_payment_chase_detail(
  533. db,
  534. schedule_id=schedule_id,
  535. view=view,
  536. ctx=_context(user, scope_kind),
  537. user_info=user,
  538. )
  539. )
  540. except (PermissionError, ValueError) as exc:
  541. return error_response(400, str(exc))
  542. @router.get(f'{prefix}/payments')
  543. async def list_payments(
  544. page: int = Query(1, ge=1),
  545. page_size: int = Query(10, ge=1, le=100, alias='pageSize'),
  546. keyword: Optional[str] = None,
  547. pay_mode: Optional[str] = Query(None, alias='payMode'),
  548. progress_status: Optional[str] = Query(None, alias='progressStatus'),
  549. installment_due_date_start: Optional[str] = Query(
  550. None, alias='installmentDueDateStart'
  551. ),
  552. installment_due_date_end: Optional[str] = Query(
  553. None, alias='installmentDueDateEnd'
  554. ),
  555. user: dict = Depends(payment_guard),
  556. db: Session = Depends(get_db),
  557. ):
  558. try:
  559. items, total = market_payment.list_payments(
  560. db,
  561. ctx=_context(user, scope_kind),
  562. user_info=user,
  563. page=page,
  564. page_size=page_size,
  565. keyword=keyword,
  566. pay_mode=pay_mode,
  567. progress_status=progress_status,
  568. installment_due_date_start=installment_due_date_start,
  569. installment_due_date_end=installment_due_date_end,
  570. )
  571. except ValueError as exc:
  572. return error_response(400, str(exc))
  573. items = [
  574. market_support.decorate_allowed_actions(
  575. 'payment', item, scope_kind=scope_kind
  576. )
  577. for item in items
  578. ]
  579. return success_response(
  580. {'list': items, 'total': total, 'page': page, 'pageSize': page_size}
  581. )
  582. @router.get(f'{prefix}/payments/form-options')
  583. async def payment_form_options(
  584. user: dict = Depends(payment_guard),
  585. db: Session = Depends(get_db),
  586. ):
  587. return success_response(
  588. market_support.get_form_options(
  589. db,
  590. module='payment',
  591. ctx=_context(user, scope_kind),
  592. user_info=user,
  593. )
  594. )
  595. @router.get(f'{prefix}/payments/{{payment_id}}')
  596. async def get_payment(
  597. payment_id: str,
  598. user: dict = Depends(payment_guard),
  599. db: Session = Depends(get_db),
  600. ):
  601. return success_response(
  602. market_support.decorate_allowed_actions(
  603. 'payment',
  604. market_payment.get_payment_detail(
  605. db,
  606. payment_id=payment_id,
  607. ctx=_context(user, scope_kind),
  608. user_info=user,
  609. ),
  610. scope_kind=scope_kind,
  611. )
  612. )
  613. @router.post(f'{prefix}/payment-transfers')
  614. async def create_payment_transfer(
  615. body: MarketPaymentTransferCreateBody,
  616. user: dict = Depends(payment_guard),
  617. db: Session = Depends(get_db),
  618. ):
  619. try:
  620. fields = body.model_dump(by_alias=True, exclude_unset=True)
  621. schedule_id = fields.pop('scheduleId')
  622. return success_response(
  623. market_support.decorate_allowed_actions(
  624. 'payment-transfer',
  625. market_payment.create_transfer(
  626. db,
  627. schedule_id=schedule_id,
  628. fields=fields,
  629. ctx=_context(user, scope_kind),
  630. user_info=user,
  631. ),
  632. scope_kind=scope_kind,
  633. ),
  634. message='创建成功',
  635. )
  636. except (PermissionError, ValueError) as exc:
  637. return error_response(400, str(exc))
  638. @router.put(f'{prefix}/payment-transfers/{{transfer_id}}')
  639. async def update_payment_transfer(
  640. transfer_id: str,
  641. body: MarketPaymentTransferBody,
  642. user: dict = Depends(payment_guard),
  643. db: Session = Depends(get_db),
  644. ):
  645. try:
  646. return success_response(
  647. market_support.decorate_allowed_actions(
  648. 'payment-transfer',
  649. market_payment.update_transfer(
  650. db,
  651. transfer_id=transfer_id,
  652. fields=body.model_dump(by_alias=True, exclude_unset=True),
  653. ctx=_context(user, scope_kind),
  654. user_info=user,
  655. ),
  656. scope_kind=scope_kind,
  657. ),
  658. message='保存成功',
  659. )
  660. except (PermissionError, ValueError) as exc:
  661. return error_response(400, str(exc))
  662. @router.post(f'{prefix}/payment-transfers/{{transfer_id}}/submit')
  663. async def submit_payment_transfer(
  664. transfer_id: str,
  665. user: dict = Depends(payment_guard),
  666. db: Session = Depends(get_db),
  667. ):
  668. try:
  669. return success_response(
  670. market_support.decorate_allowed_actions(
  671. 'payment-transfer',
  672. market_payment.submit_transfer(
  673. db,
  674. transfer_id=transfer_id,
  675. ctx=_context(user, scope_kind),
  676. user_info=user,
  677. ),
  678. scope_kind=scope_kind,
  679. ),
  680. message='已提交财务确认',
  681. )
  682. except (PermissionError, ValueError) as exc:
  683. return error_response(400, str(exc))
  684. @router.delete(f'{prefix}/payment-transfers/{{transfer_id}}')
  685. async def delete_payment_transfer(
  686. transfer_id: str,
  687. user: dict = Depends(payment_guard),
  688. db: Session = Depends(get_db),
  689. ):
  690. try:
  691. return success_response(
  692. market_payment.delete_transfer(
  693. db,
  694. transfer_id=transfer_id,
  695. ctx=_context(user, scope_kind),
  696. user_info=user,
  697. ),
  698. message='删除成功',
  699. )
  700. except (PermissionError, ValueError) as exc:
  701. return error_response(400, str(exc))
  702. @router.post(f'{prefix}/payment-transfers/{{transfer_id}}/withdraw')
  703. async def withdraw_payment_transfer(
  704. transfer_id: str,
  705. user: dict = Depends(payment_guard),
  706. db: Session = Depends(get_db),
  707. ):
  708. try:
  709. return success_response(
  710. market_support.decorate_allowed_actions(
  711. 'payment-transfer',
  712. market_payment.withdraw_transfer(
  713. db,
  714. transfer_id=transfer_id,
  715. ctx=_context(user, scope_kind),
  716. user_info=user,
  717. ),
  718. scope_kind=scope_kind,
  719. ),
  720. message='已撤回',
  721. )
  722. except (PermissionError, ValueError) as exc:
  723. return error_response(400, str(exc))
  724. _register_payment_routes('specialist')
  725. _register_payment_routes('manager')
  726. def _register_freight_routes(scope_kind: ScopeKind) -> None:
  727. prefix = f'/mini/market/{scope_kind}/freights'
  728. permission_guard = require_market_menu_code(
  729. f'mini:market:{scope_kind}:freight'
  730. )
  731. @router.get(prefix)
  732. async def list_freights(
  733. page: int = Query(1, ge=1),
  734. page_size: int = Query(10, ge=1, le=100, alias='pageSize'),
  735. keyword: Optional[str] = None,
  736. transport_mode: Optional[str] = Query(None, alias='transportMode'),
  737. sample_source: Optional[
  738. Literal['cell_storage', 'research_donation']
  739. ] = Query(None, alias='sampleSource'),
  740. include_items: bool = Query(False, alias='includeItems'),
  741. create_time_start: Optional[str] = Query(None, alias='createTimeStart'),
  742. create_time_end: Optional[str] = Query(None, alias='createTimeEnd'),
  743. user: dict = Depends(permission_guard),
  744. db: Session = Depends(get_db),
  745. ):
  746. try:
  747. items, total = market_freight.list_freights(
  748. db,
  749. ctx=_context(user, scope_kind),
  750. user_info=user,
  751. page=page,
  752. page_size=page_size,
  753. keyword=keyword,
  754. transport_mode=transport_mode,
  755. sample_source=sample_source,
  756. include_items=include_items,
  757. create_time_start=create_time_start,
  758. create_time_end=create_time_end,
  759. )
  760. except ValueError as exc:
  761. return error_response(400, str(exc))
  762. items = [
  763. market_support.decorate_allowed_actions(
  764. 'freight', item, scope_kind=scope_kind
  765. )
  766. for item in items
  767. ]
  768. return success_response(
  769. {
  770. 'list': items,
  771. 'total': total,
  772. 'page': page,
  773. 'pageSize': page_size,
  774. 'allowedActions': market_support.allowed_actions(
  775. 'freight-list', status='', scope_kind=scope_kind
  776. ),
  777. }
  778. )
  779. @router.get(f'{prefix}/sample-options')
  780. async def list_freight_sample_options(
  781. page: int = Query(1, ge=1),
  782. page_size: int = Query(20, ge=1, le=100, alias='pageSize'),
  783. keyword: Optional[str] = None,
  784. sample_source: Optional[
  785. Literal['cell_storage', 'research_donation']
  786. ] = Query(None, alias='sampleSource'),
  787. user: dict = Depends(permission_guard),
  788. db: Session = Depends(get_db),
  789. ):
  790. items, total = market_freight.list_sample_options(
  791. db,
  792. ctx=_context(user, scope_kind),
  793. user_info=user,
  794. page=page,
  795. page_size=page_size,
  796. keyword=keyword,
  797. sample_source=sample_source,
  798. )
  799. return success_response(
  800. {'list': items, 'total': total, 'page': page, 'pageSize': page_size}
  801. )
  802. @router.get(f'{prefix}/form-options')
  803. async def freight_form_options(
  804. user: dict = Depends(permission_guard),
  805. db: Session = Depends(get_db),
  806. ):
  807. return success_response(
  808. market_support.get_form_options(
  809. db,
  810. module='freight',
  811. ctx=_context(user, scope_kind),
  812. user_info=user,
  813. )
  814. )
  815. @router.get(f'{prefix}/{{freight_id}}')
  816. async def get_freight(
  817. freight_id: str,
  818. user: dict = Depends(permission_guard),
  819. db: Session = Depends(get_db),
  820. ):
  821. return success_response(
  822. market_support.decorate_allowed_actions(
  823. 'freight',
  824. market_freight.get_freight_detail(
  825. db,
  826. freight_id,
  827. ctx=_context(user, scope_kind),
  828. user_info=user,
  829. ),
  830. scope_kind=scope_kind,
  831. detail=True,
  832. )
  833. )
  834. @router.post(prefix)
  835. async def create_freight(
  836. body: MarketFreightCreateBody,
  837. user: dict = Depends(permission_guard),
  838. db: Session = Depends(get_db),
  839. ):
  840. try:
  841. return success_response(
  842. market_freight.create_freight(
  843. db,
  844. ctx=_context(user, scope_kind),
  845. user_info=user,
  846. transport_mode=body.transport_mode,
  847. freight_amount=body.freight_amount,
  848. waybill_no=body.waybill_no,
  849. items=[
  850. item.model_dump(by_alias=True) for item in body.items
  851. ],
  852. ),
  853. message='创建成功',
  854. )
  855. except (PermissionError, ValueError) as exc:
  856. return error_response(400, str(exc))
  857. @router.put(f'{prefix}/{{freight_id}}')
  858. async def update_freight(
  859. freight_id: str,
  860. body: MarketFreightUpdateBody,
  861. user: dict = Depends(permission_guard),
  862. db: Session = Depends(get_db),
  863. ):
  864. try:
  865. return success_response(
  866. market_freight.update_freight(
  867. db,
  868. freight_id,
  869. ctx=_context(user, scope_kind),
  870. user_info=user,
  871. transport_mode=body.transport_mode,
  872. freight_amount=body.freight_amount,
  873. waybill_no=body.waybill_no,
  874. waybill_supplied='waybill_no' in body.model_fields_set,
  875. amount_supplied='freight_amount' in body.model_fields_set,
  876. ),
  877. message='保存成功',
  878. )
  879. except PermissionError as exc:
  880. return error_response(403, str(exc))
  881. except ValueError as exc:
  882. return error_response(400, str(exc))
  883. @router.post(f'{prefix}/{{freight_id}}/link')
  884. async def link_freight_samples(
  885. freight_id: str,
  886. body: MarketFreightItemsBody,
  887. user: dict = Depends(permission_guard),
  888. db: Session = Depends(get_db),
  889. ):
  890. try:
  891. return success_response(
  892. market_freight.link_samples(
  893. db,
  894. freight_id,
  895. ctx=_context(user, scope_kind),
  896. user_info=user,
  897. items=[
  898. item.model_dump(by_alias=True) for item in body.items
  899. ],
  900. ),
  901. message='关联成功',
  902. )
  903. except (PermissionError, ValueError) as exc:
  904. return error_response(400, str(exc))
  905. @router.post(f'{prefix}/{{freight_id}}/unlink')
  906. async def unlink_freight_samples(
  907. freight_id: str,
  908. body: MarketFreightItemsBody,
  909. user: dict = Depends(permission_guard),
  910. db: Session = Depends(get_db),
  911. ):
  912. try:
  913. return success_response(
  914. market_freight.unlink_samples(
  915. db,
  916. freight_id,
  917. ctx=_context(user, scope_kind),
  918. user_info=user,
  919. items=[
  920. item.model_dump(by_alias=True) for item in body.items
  921. ],
  922. ),
  923. message='已取消关联',
  924. )
  925. except (PermissionError, ValueError) as exc:
  926. return error_response(400, str(exc))
  927. @router.delete(f'{prefix}/{{freight_id}}')
  928. async def delete_freight(
  929. freight_id: str,
  930. user: dict = Depends(permission_guard),
  931. db: Session = Depends(get_db),
  932. ):
  933. try:
  934. return success_response(
  935. market_freight.delete_freight(
  936. db,
  937. freight_id,
  938. ctx=_context(user, scope_kind),
  939. user_info=user,
  940. ),
  941. message='删除成功',
  942. )
  943. except PermissionError as exc:
  944. return error_response(403, str(exc))
  945. except ValueError as exc:
  946. return error_response(400, str(exc))
  947. _register_freight_routes('specialist')
  948. _register_freight_routes('manager')
  949. def _register_attachment_routes(scope_kind: ScopeKind) -> None:
  950. for module in ('presign', 'protocol', 'payment'):
  951. permission_guard = require_market_menu_code(
  952. f'mini:market:{scope_kind}:{module}'
  953. )
  954. path = f'/mini/market/{scope_kind}/{module}/attachments'
  955. download_path = f'{path}/download-url'
  956. def build_upload_endpoint(bound_module, bound_guard):
  957. async def upload_market_attachment(
  958. file: UploadFile = File(...),
  959. user: dict = Depends(bound_guard),
  960. db: Session = Depends(get_db),
  961. ):
  962. try:
  963. max_bytes = (
  964. market_support.PRESIGN_MAX_UPLOAD_BYTES
  965. if bound_module == 'presign'
  966. else market_support.PROTOCOL_MAX_UPLOAD_BYTES
  967. if bound_module == 'protocol'
  968. else market_support.MAX_UPLOAD_BYTES
  969. )
  970. content = await file.read(max_bytes + 1)
  971. return success_response(
  972. market_support.save_market_attachment(
  973. db,
  974. content=content,
  975. filename=file.filename or '',
  976. content_type=file.content_type or '',
  977. module=bound_module,
  978. ctx=_context(user, scope_kind),
  979. user_info=user,
  980. ),
  981. message='上传成功',
  982. )
  983. except (PermissionError, ValueError) as exc:
  984. return error_response(400, str(exc))
  985. finally:
  986. await file.close()
  987. return upload_market_attachment
  988. router.add_api_route(
  989. path,
  990. build_upload_endpoint(module, permission_guard),
  991. methods=['POST'],
  992. name=f'upload_market_{scope_kind}_{module}_attachment',
  993. )
  994. def build_download_endpoint(bound_module, bound_guard):
  995. async def download_market_attachment(
  996. body: MarketAttachmentDownloadBody,
  997. user: dict = Depends(bound_guard),
  998. db: Session = Depends(get_db),
  999. ):
  1000. try:
  1001. return success_response(
  1002. market_support.get_market_attachment_download_url(
  1003. db,
  1004. file_url=body.file_url,
  1005. file_name=body.file_name,
  1006. module=bound_module,
  1007. ctx=_context(user, scope_kind),
  1008. user_info=user,
  1009. )
  1010. )
  1011. except (PermissionError, ValueError):
  1012. return error_response(403, '附件不存在或无权下载')
  1013. except Exception:
  1014. return error_response(500, '获取下载链接失败')
  1015. return download_market_attachment
  1016. router.add_api_route(
  1017. download_path,
  1018. build_download_endpoint(module, permission_guard),
  1019. methods=['POST'],
  1020. name=f'download_market_{scope_kind}_{module}_attachment',
  1021. )
  1022. _register_attachment_routes('specialist')
  1023. _register_attachment_routes('manager')