service.py 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372
  1. from datetime import datetime
  2. from decimal import Decimal
  3. from typing import Any, Optional
  4. from redis.asyncio import Redis
  5. from app.api.v1.module_system.auth.schema import AuthSchema
  6. from app.core.alipay import AlipayClient
  7. from app.core.exceptions import CustomException
  8. from app.core.logger import log
  9. from app.utils.snowflake import get_snowflake_id_str
  10. from app.plugin.module_payment.enterprise.crud import EnterpriseCRUD
  11. from .crud import AccountCRUD, TransferCRUD, DepositCRUD, WithdrawCRUD
  12. from .enums import (
  13. DepositStatusEnum,
  14. WithdrawStatusEnum,
  15. )
  16. from .schema import (
  17. AccountAuthorizeApplySchema,
  18. AccountAuthorizeApplyOutSchema,
  19. AccountCreateSchema,
  20. AccountDepositSchema,
  21. AccountDepositOutSchema,
  22. AccountOperationOutSchema,
  23. AccountQuerySchema,
  24. AccountTransferSchema,
  25. AccountTransferOutSchema,
  26. AccountWithdrawSchema,
  27. ReceiptApplySchema,
  28. TransferListOutSchema,
  29. TransferOutSchema,
  30. TenantTransferCreate,
  31. TenantTransferResponse,
  32. )
  33. from ..openapi.crud import OpenTransferCRUD
  34. # 支付宝资金专户转账错误码 → 友好提示
  35. _TRANSFER_ERROR_HINTS = {
  36. "SYSTEM_ERROR": "系统繁忙,请稍后重试",
  37. "INVALID_PARAMETER": "请求参数有误,请检查后重试",
  38. "AMOUNT_LESS_THAN_ONE_CENT": "转账金额不能低于 0.01 元",
  39. "BALANCE_IS_NOT_ENOUGH": "企业余额不足,建议充值",
  40. "BANK_RESPONSE_ERROR": "银行处理失败:账户异常",
  41. "CARD_BIN_ERROR": "收款银行账号不正确,请确认",
  42. "DUPLICATE_DIFFERENT_REQUEST": "重复请求但参数不一致,请检查",
  43. "EXCEED_LIMIT_SM_MIN_AMOUNT": "转账金额不能低于 0.1 元",
  44. "EXCEED_LIMIT_DM_MAX_AMOUNT": "超出单日转账限额,请明天再试或联系管理员提升限额",
  45. "INVALID_ACCOUNT_BOOK": "资金专户不存在,请检查专户号",
  46. "INVALID_CARDNO": "无效的收款银行卡号",
  47. "INVALID_IDENTITY_TYPE": "收款方身份类型不匹配",
  48. "NO_AGREEMENT": "无转账权限,请联系管理员",
  49. "PAYEE_CARD_INFO_ERROR": "收款方账号或银行卡信息有误,请核实",
  50. "PAYEE_NOT_EXIST": "收款账号不存在或姓名有误",
  51. "PAYER_BALANCE_NOT_ENOUGH": "付款方余额不足,建议充值",
  52. "REQUEST_PROCESSING": "系统处理中,请稍后重试",
  53. "TRANS_AUTH_NO_EXIST": "转账授权协议不存在,请先签约",
  54. }
  55. def _parse_dt(val: str | None) -> datetime | None:
  56. """解析支付宝日期字符串"""
  57. if not val:
  58. return None
  59. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
  60. try:
  61. return datetime.strptime(val, fmt)
  62. except ValueError:
  63. continue
  64. return None
  65. class AccountService:
  66. """资金专户服务层"""
  67. @classmethod
  68. async def stat_transfer_amount_service(
  69. cls,
  70. auth: AuthSchema,
  71. tenant_id: Optional[int] = None,
  72. enterprise_id: Optional[str] = None,
  73. start_date: Optional[datetime] = None,
  74. end_date: Optional[datetime] = None,
  75. ) -> Decimal:
  76. """
  77. 统计转账金额(✅)
  78. 统计企业在指定时间范围内的转账总金额以及每天的转账金额。
  79. """
  80. crud = TransferCRUD(auth)
  81. return await crud.get_transfer_amount(
  82. tenant_id=tenant_id,
  83. enterprise_id=enterprise_id,
  84. start_date=start_date,
  85. end_date=end_date,
  86. )
  87. @classmethod
  88. async def stat_consume_amount_service(
  89. cls,
  90. auth: AuthSchema,
  91. tenant_id: Optional[int] = None,
  92. enterprise_id: Optional[str] = None,
  93. start_date: Optional[datetime] = None,
  94. end_date: Optional[datetime] = None,
  95. ) -> Decimal:
  96. """
  97. 统计消费金额(✅)
  98. 统计企业在指定时间范围内的消费总金额。
  99. 数据来源: pay_bill 表,consume_type=CONSUME, status=PROCESSED
  100. """
  101. from app.plugin.module_payment.notification.crud import BillCRUD
  102. crud = BillCRUD(auth)
  103. return await crud.get_consume_amount(
  104. tenant_id=tenant_id,
  105. enterprise_id=enterprise_id,
  106. start_date=start_date,
  107. end_date=end_date,
  108. )
  109. @classmethod
  110. async def stat_summary_amount_service(
  111. cls,
  112. auth: AuthSchema,
  113. tenant_id: Optional[int] = None,
  114. enterprise_id: Optional[str] = None,
  115. start_date: Optional[datetime] = None,
  116. end_date: Optional[datetime] = None,
  117. ) -> Decimal:
  118. """
  119. 汇总统计金额(✅)
  120. 汇总 = 消费统计 + 转账统计,对应时间段结果相加
  121. """
  122. transfer_amount = await cls.stat_transfer_amount_service(
  123. auth=auth,
  124. tenant_id=tenant_id,
  125. enterprise_id=enterprise_id,
  126. start_date=start_date,
  127. end_date=end_date,
  128. )
  129. consume_amount = await cls.stat_consume_amount_service(
  130. auth=auth,
  131. tenant_id=tenant_id,
  132. enterprise_id=enterprise_id,
  133. start_date=start_date,
  134. end_date=end_date,
  135. )
  136. return transfer_amount + consume_amount
  137. @classmethod
  138. async def authorize_apply_service(
  139. cls,
  140. auth: AuthSchema,
  141. data: AccountAuthorizeApplySchema
  142. ) -> AccountAuthorizeApplyOutSchema:
  143. """
  144. 申请转账授权签约(✅)
  145. 调用: alipay.commerce.ec.trans.authorize.apply
  146. """
  147. from alipay.aop.api.request.AlipayCommerceEcTransAuthorizeApplyRequest import (
  148. AlipayCommerceEcTransAuthorizeApplyRequest,
  149. )
  150. from alipay.aop.api.domain.AlipayCommerceEcTransAuthorizeApplyModel import (
  151. AlipayCommerceEcTransAuthorizeApplyModel,
  152. )
  153. from alipay.aop.api.response.AlipayCommerceEcTransAuthorizeApplyResponse import (
  154. AlipayCommerceEcTransAuthorizeApplyResponse,
  155. )
  156. model = AlipayCommerceEcTransAuthorizeApplyModel()
  157. model.enterprise_id = data.enterprise_id
  158. request = AlipayCommerceEcTransAuthorizeApplyRequest()
  159. request.biz_model = model
  160. client = AlipayClient.get_client()
  161. response = client.execute(request)
  162. if not response:
  163. raise CustomException(msg="申请转账授权失败: 无响应")
  164. result = AlipayCommerceEcTransAuthorizeApplyResponse()
  165. result.parse_response_content(response)
  166. if not result.is_success():
  167. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  168. raise CustomException(msg=f"申请转账授权失败: {result.msg}")
  169. return AccountAuthorizeApplyOutSchema(
  170. sign_url=result.sign_url,
  171. )
  172. @classmethod
  173. async def create_account_service(
  174. cls,
  175. auth: AuthSchema,
  176. data: AccountCreateSchema
  177. ) -> AccountOperationOutSchema:
  178. """
  179. 开通资金专户(✅)
  180. 调用: alipay.commerce.ec.trans.account.create
  181. """
  182. from alipay.aop.api.request.AlipayCommerceEcTransAccountCreateRequest import (
  183. AlipayCommerceEcTransAccountCreateRequest,
  184. )
  185. from alipay.aop.api.domain.AlipayCommerceEcTransAccountCreateModel import (
  186. AlipayCommerceEcTransAccountCreateModel,
  187. )
  188. from alipay.aop.api.response.AlipayCommerceEcTransAccountCreateResponse import (
  189. AlipayCommerceEcTransAccountCreateResponse,
  190. )
  191. model = AlipayCommerceEcTransAccountCreateModel()
  192. model.enterprise_id = data.enterprise_id
  193. # model.account_type = data.account_type or "ALL" # 收支全能户
  194. # model.scene = data.scene or "B2B_TRANS" # ToB转账场景
  195. model.account_type = "ALL"
  196. model.scene = "B2B_TRANS"
  197. request = AlipayCommerceEcTransAccountCreateRequest()
  198. request.biz_model = model
  199. client = AlipayClient.get_client()
  200. response = client.execute(request)
  201. if not response:
  202. raise CustomException(msg="开通资金专户失败: 无响应")
  203. result = AlipayCommerceEcTransAccountCreateResponse()
  204. result.parse_response_content(response)
  205. if not result.is_success():
  206. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  207. raise CustomException(msg=f"开通资金专户失败: {result.msg}")
  208. account_data = AccountCreateSchema(
  209. enterprise_id=model.enterprise_id,
  210. account_book_id=result.account_book_id,
  211. account_type=model.account_type,
  212. scene=model.scene,
  213. )
  214. if result.account_book_id:
  215. account_data.account_book_id = result.account_book_id
  216. await AccountCRUD(auth).create(account_data)
  217. return AccountOperationOutSchema(
  218. enterprise_id=account_data.enterprise_id,
  219. account_book_id=account_data.account_book_id,
  220. )
  221. @classmethod
  222. async def deposit_service(
  223. cls,
  224. auth: AuthSchema,
  225. data: AccountDepositSchema
  226. ) -> AccountDepositOutSchema:
  227. """
  228. 资金专户充值(✅)
  229. 调用: alipay.commerce.ec.trans.account.deposit
  230. """
  231. from alipay.aop.api.request.AlipayCommerceEcTransAccountDepositRequest import (
  232. AlipayCommerceEcTransAccountDepositRequest,
  233. )
  234. from alipay.aop.api.domain.AlipayCommerceEcTransAccountDepositModel import (
  235. AlipayCommerceEcTransAccountDepositModel,
  236. )
  237. from alipay.aop.api.response.AlipayCommerceEcTransAccountDepositResponse import (
  238. AlipayCommerceEcTransAccountDepositResponse,
  239. )
  240. model = AlipayCommerceEcTransAccountDepositModel()
  241. model.enterprise_id = data.enterprise_id
  242. model.account_book_id = data.account_book_id
  243. model.amount = str(data.amount)
  244. model.out_biz_no = get_snowflake_id_str(auth.tenant_id)
  245. request = AlipayCommerceEcTransAccountDepositRequest()
  246. request.biz_model = model
  247. client = AlipayClient.get_client()
  248. response = client.execute(request)
  249. if not response:
  250. raise CustomException(msg="充值失败: 无响应")
  251. result = AlipayCommerceEcTransAccountDepositResponse()
  252. result.parse_response_content(response)
  253. if not result.is_success():
  254. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  255. raise CustomException(msg=f"充值失败: {result.msg}")
  256. deposit_crud = DepositCRUD(auth)
  257. deposit_data = {
  258. "enterprise_id": data.enterprise_id,
  259. "out_biz_no": model.out_biz_no,
  260. "account_book_id": data.account_book_id,
  261. "amount": data.amount,
  262. "url": result.url,
  263. "status": DepositStatusEnum.DEALING.value,
  264. "remark": data.remark,
  265. }
  266. await deposit_crud.create(deposit_data)
  267. return AccountDepositOutSchema(
  268. url=result.url,
  269. )
  270. @classmethod
  271. async def transfer_service(
  272. cls,
  273. auth: AuthSchema,
  274. data: AccountTransferSchema
  275. ) -> AccountTransferOutSchema:
  276. """
  277. 资金专户转账(✅)
  278. 调用: alipay.commerce.ec.trans.account.transfer
  279. """
  280. from alipay.aop.api.request.AlipayCommerceEcTransAccountTransferRequest import (
  281. AlipayCommerceEcTransAccountTransferRequest,
  282. )
  283. from alipay.aop.api.domain.AlipayCommerceEcTransAccountTransferModel import (
  284. AlipayCommerceEcTransAccountTransferModel,
  285. )
  286. from alipay.aop.api.response.AlipayCommerceEcTransAccountTransferResponse import (
  287. AlipayCommerceEcTransAccountTransferResponse,
  288. )
  289. from alipay.aop.api.domain.TransParticipant import (
  290. TransParticipant,
  291. )
  292. from alipay.aop.api.domain.BankCardExtInfoDTO import (
  293. BankCardExtInfoDTO,
  294. )
  295. # 检查资金专户是否存在
  296. account = await AccountCRUD(auth).get_by_account_book_id(data.account_book_id)
  297. if not account:
  298. raise CustomException(msg="资金账户不存在")
  299. if account.tenant_id != auth.tenant_id:
  300. raise CustomException(msg="无权限操作")
  301. if data.enterprise_id and account.enterprise_id != data.enterprise_id:
  302. raise CustomException(msg="参数错误")
  303. if not data.order_title and account.enterprise_id:
  304. enterprise = await EnterpriseCRUD(auth).get_by_enterprise_id(account.enterprise_id)
  305. if not enterprise:
  306. raise CustomException(msg="资金账户所属企业不存在")
  307. data.order_title = f"来自{enterprise.name}转账"
  308. model = AlipayCommerceEcTransAccountTransferModel()
  309. model.enterprise_id = account.enterprise_id
  310. model.account_book_id = account.account_book_id
  311. model.out_biz_no = get_snowflake_id_str(auth.tenant_id)
  312. # 转账总金额,单位为元,精确到小数点后两位
  313. model.amount = str(data.amount)
  314. model.order_title = data.order_title
  315. payee_info = TransParticipant()
  316. payee_info.identity_type = data.payee_info.identity_type
  317. payee_info.name = data.payee_info.name
  318. payee_info.identity = data.payee_info.identity
  319. if data.payee_info.bankcard_ext_info:
  320. payee_info.bankcard_ext_info = BankCardExtInfoDTO.from_alipay_dict(
  321. data.payee_info.bankcard_ext_info.model_dump(exclude_none=True)
  322. )
  323. model.payee_info = payee_info
  324. request = AlipayCommerceEcTransAccountTransferRequest()
  325. request.biz_model = model
  326. client = AlipayClient.get_client()
  327. response = client.execute(request)
  328. if not response:
  329. raise CustomException(msg="转账失败: 无响应")
  330. result = AlipayCommerceEcTransAccountTransferResponse()
  331. result.parse_response_content(response)
  332. sub_code = getattr(result, 'sub_code', '') or ''
  333. sub_msg = getattr(result, 'sub_msg', '') or ''
  334. # 构建转账记录数据,但延迟写入:
  335. # - 成功时在当前会话写入
  336. # - 失败时使用独立事务写入并提交,避免被外层回滚吞掉
  337. transfer_crud = TransferCRUD(auth)
  338. transfer_data = {
  339. "enterprise_id": model.enterprise_id,
  340. "out_biz_no": model.out_biz_no,
  341. "account_book_id": model.account_book_id,
  342. "amount": model.amount,
  343. "order_title": model.order_title,
  344. "payee_info": data.payee_info.model_dump() if data.payee_info else None,
  345. "payee_type": data.payee_info.identity_type if data.payee_info else None,
  346. "status": result.status,
  347. "order_no": result.order_no,
  348. "fund_order_id": result.fund_order_id,
  349. "remark": "",
  350. }
  351. log.info(f"记录转账: {transfer_data}")
  352. if not result.is_success():
  353. # 优先用 sub_code 匹配
  354. hint = _TRANSFER_ERROR_HINTS.get(sub_code)
  355. # sub_code 无匹配时,尝试从 sub_msg 中提取错误码(支付宝部分接口sub_code返回unknown-sub-code)
  356. if not hint:
  357. for code_key, code_hint in _TRANSFER_ERROR_HINTS.items():
  358. if code_key in sub_msg:
  359. hint = code_hint
  360. break
  361. hint = hint or sub_msg or result.msg or "转账失败"
  362. log.error(f"支付宝接口调用失败: {result.code} - {result.msg} (sub_code={sub_code}, sub_msg={sub_msg})")
  363. # 使用独立的 session/事务保证失败记录能被持久化
  364. from app.core.database import async_db_session
  365. async with async_db_session() as _session:
  366. async with _session.begin():
  367. new_auth = AuthSchema(db=_session, check_data_scope=False)
  368. # 保持 tenant_id
  369. new_auth.tenant_id = getattr(auth, "tenant_id", None)
  370. transfer_data["status"]="FAIL"
  371. transfer_data["remark"]=f"{result.msg} ({sub_code} {sub_msg})"
  372. await TransferCRUD(new_auth).create(transfer_data)
  373. raise CustomException(msg=f"转账失败: {hint}")
  374. # 成功时写入当前会话
  375. await transfer_crud.create(transfer_data)
  376. return AccountTransferOutSchema(
  377. status=result.status,
  378. order_no=result.order_no,
  379. fund_order_id=result.fund_order_id,
  380. out_biz_no=model.out_biz_no,
  381. )
  382. @classmethod
  383. async def tenant_transfer_service(
  384. cls,
  385. auth: AuthSchema,
  386. tenant_id: int,
  387. data: TenantTransferCreate,
  388. request_ip: str,
  389. api_key_id: int | None = None,
  390. ) -> TenantTransferResponse:
  391. """
  392. 租户API转账(通过API Key认证)
  393. 调用: alipay.commerce.ec.trans.account.transfer
  394. """
  395. from alipay.aop.api.request.AlipayCommerceEcTransAccountTransferRequest import (
  396. AlipayCommerceEcTransAccountTransferRequest,
  397. )
  398. from alipay.aop.api.domain.AlipayCommerceEcTransAccountTransferModel import (
  399. AlipayCommerceEcTransAccountTransferModel,
  400. )
  401. from alipay.aop.api.response.AlipayCommerceEcTransAccountTransferResponse import (
  402. AlipayCommerceEcTransAccountTransferResponse,
  403. )
  404. from alipay.aop.api.domain.TransParticipant import (
  405. TransParticipant,
  406. )
  407. from alipay.aop.api.domain.BankCardExtInfoDTO import (
  408. BankCardExtInfoDTO,
  409. )
  410. # 检查资金专户是否存在
  411. account = await AccountCRUD(auth).get_by_account_book_id(data.account_book_id)
  412. if not account:
  413. raise CustomException(msg="资金账户不存在")
  414. if account.tenant_id != tenant_id:
  415. raise CustomException(msg="无权限操作")
  416. if data.enterprise_id and account.enterprise_id != data.enterprise_id:
  417. raise CustomException(msg="参数错误")
  418. if not data.order_title and account.enterprise_id:
  419. enterprise = await EnterpriseCRUD(auth).get_by_enterprise_id(account.enterprise_id)
  420. if not enterprise:
  421. raise CustomException(msg="资金账户所属企业不存在")
  422. data.order_title = f"来自{enterprise.name}转账"
  423. model = AlipayCommerceEcTransAccountTransferModel()
  424. model.enterprise_id = account.enterprise_id
  425. model.account_book_id = account.account_book_id
  426. model.out_biz_no = get_snowflake_id_str(tenant_id)
  427. # 转账总金额,单位为元,精确到小数点后两位
  428. model.amount = str(data.amount)
  429. model.order_title = data.order_title
  430. payee_info = TransParticipant()
  431. payee_info.identity_type = data.payee_info.identity_type
  432. payee_info.name = data.payee_info.name
  433. payee_info.identity = data.payee_info.identity
  434. if data.payee_info.bankcard_ext_info:
  435. payee_info.bankcard_ext_info = BankCardExtInfoDTO.from_alipay_dict(
  436. data.payee_info.bankcard_ext_info.model_dump(exclude_none=True)
  437. )
  438. model.payee_info = payee_info
  439. request = AlipayCommerceEcTransAccountTransferRequest()
  440. request.biz_model = model
  441. client = AlipayClient.get_client()
  442. response = client.execute(request)
  443. if not response:
  444. raise CustomException(msg="转账失败: 无响应")
  445. result = AlipayCommerceEcTransAccountTransferResponse()
  446. result.parse_response_content(response)
  447. if not result.is_success():
  448. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  449. raise CustomException(msg=f"转账失败: {result.sub_msg or result.msg or result.code}")
  450. transfer_crud = TransferCRUD(auth)
  451. transfer_data = {
  452. "enterprise_id": model.enterprise_id,
  453. "out_biz_no": model.out_biz_no,
  454. "account_book_id": model.account_book_id,
  455. "amount": model.amount,
  456. "order_title": model.order_title,
  457. "payee_info": data.payee_info.model_dump() if data.payee_info else None,
  458. "payee_type": data.payee_info.identity_type if data.payee_info else None,
  459. "status": result.status,
  460. "order_no": result.order_no,
  461. "fund_order_id": result.fund_order_id,
  462. }
  463. await transfer_crud.create(transfer_data)
  464. return TenantTransferResponse(
  465. status=result.status,
  466. order_no=result.order_no,
  467. fund_order_id=result.fund_order_id,
  468. )
  469. @classmethod
  470. async def withdraw_service(
  471. cls,
  472. auth: AuthSchema,
  473. data: AccountWithdrawSchema
  474. ) -> AccountOperationOutSchema:
  475. """
  476. 资金专户提现
  477. 调用: alipay.commerce.ec.trans.account.withdraw
  478. 接口文档: https://opendocs.alipay.com/pre-open/d651859b_alipay.commerce.ec.trans.account.withdraw
  479. 参数说明:
  480. - enterprise_id: 企业ID
  481. - account_book_id: 资金专户号
  482. - amount: 提现金额
  483. - out_biz_no: 商家侧订单号(唯一)
  484. """
  485. from alipay.aop.api.request.AlipayCommerceEcTransAccountWithdrawRequest import (
  486. AlipayCommerceEcTransAccountWithdrawRequest,
  487. )
  488. from alipay.aop.api.domain.AlipayCommerceEcTransAccountWithdrawModel import (
  489. AlipayCommerceEcTransAccountWithdrawModel,
  490. )
  491. from alipay.aop.api.response.AlipayCommerceEcTransAccountWithdrawResponse import (
  492. AlipayCommerceEcTransAccountWithdrawResponse,
  493. )
  494. crud = AccountCRUD(auth)
  495. enterprise = await crud.get_by_enterprise_id(data.enterprise_id)
  496. if not enterprise:
  497. raise CustomException(msg="企业不存在")
  498. model = AlipayCommerceEcTransAccountWithdrawModel()
  499. model.enterprise_id = enterprise.enterprise_id
  500. model.account_book_id = data.account_book_id
  501. model.amount = str(data.amount)
  502. model.out_biz_no = get_snowflake_id_str(auth.tenant_id)
  503. if data.remark:
  504. model.remark = data.remark
  505. request = AlipayCommerceEcTransAccountWithdrawRequest()
  506. request.biz_model = model
  507. client = AlipayClient.get_client()
  508. response = client.execute(request)
  509. if not response:
  510. raise CustomException(msg="提现失败: 无响应")
  511. result = AlipayCommerceEcTransAccountWithdrawResponse()
  512. result.parse_response_content(response)
  513. if not result.is_success():
  514. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  515. raise CustomException(msg=f"提现失败: {result.msg}")
  516. withdraw_crud = WithdrawCRUD(auth)
  517. withdraw_data = {
  518. "enterprise_id": data.enterprise_id,
  519. "out_biz_no": model.out_biz_no,
  520. "account_book_id": data.account_book_id,
  521. "amount": data.amount,
  522. # 专户提现到余额户是同步操作,要么执行成功,要么执行异常,
  523. # 出参status设计多余,遵循规范使用业务码区分成功与失败
  524. "status": WithdrawStatusEnum.SUCCESS.value,
  525. "order_no": result.order_no,
  526. }
  527. await withdraw_crud.create(withdraw_data)
  528. log.info(f"资金专户提现发起成功: 企业: {data.enterprise_id}, 金额: {data.amount}")
  529. return AccountOperationOutSchema(
  530. enterprise_id=data.enterprise_id,
  531. account_book_id=data.account_book_id,
  532. )
  533. @classmethod
  534. async def query_account_service(
  535. cls,
  536. auth: AuthSchema,
  537. data: AccountQuerySchema
  538. ) -> list[Any]:
  539. """
  540. 查询资金专户(调用支付宝接口)
  541. 调用: alipay.commerce.ec.trans.account.query
  542. """
  543. from alipay.aop.api.request.AlipayCommerceEcTransAccountQueryRequest import (
  544. AlipayCommerceEcTransAccountQueryRequest,
  545. )
  546. from alipay.aop.api.domain.AlipayCommerceEcTransAccountQueryModel import (
  547. AlipayCommerceEcTransAccountQueryModel,
  548. )
  549. from alipay.aop.api.response.AlipayCommerceEcTransAccountQueryResponse import (
  550. AlipayCommerceEcTransAccountQueryResponse,
  551. )
  552. from alipay.aop.api.domain.FundAccountApiDTO import (
  553. FundAccountApiDTO,
  554. )
  555. model = AlipayCommerceEcTransAccountQueryModel()
  556. model.enterprise_id = data.enterprise_id
  557. request = AlipayCommerceEcTransAccountQueryRequest()
  558. request.biz_model = model
  559. client = AlipayClient.get_client()
  560. response = client.execute(request)
  561. if not response:
  562. raise CustomException(msg="查询资金专户失败: 无响应")
  563. result = AlipayCommerceEcTransAccountQueryResponse()
  564. result.parse_response_content(response)
  565. if not result.is_success():
  566. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  567. raise CustomException(msg=f"查询资金专户失败: {result.msg}")
  568. collect = []
  569. for v in list(result.account_list or []):
  570. if not hasattr(v, "account_book_id"):
  571. continue
  572. if not hasattr(v, "scene") or v.scene != "B2B_TRANS":
  573. continue
  574. account = FundAccountApiDTO.to_alipay_dict(v)
  575. collect.append(account)
  576. return collect
  577. @classmethod
  578. async def transfer_detail_service(
  579. cls,
  580. auth: AuthSchema,
  581. out_biz_no: str
  582. ) -> TransferOutSchema:
  583. """
  584. 查询转账记录详情
  585. """
  586. crud = TransferCRUD(auth)
  587. transfer = await crud.get_by_out_biz_no(out_biz_no)
  588. if not transfer:
  589. raise CustomException(msg="转账记录不存在")
  590. transfer_result = TransferOutSchema.model_validate(transfer)
  591. # 查询三方订单号
  592. open_transfer_crud = OpenTransferCRUD(auth)
  593. open_transfer_data = await open_transfer_crud.get(out_biz_no=transfer.out_biz_no)
  594. if open_transfer_data:
  595. transfer_result.third_biz_no = open_transfer_data.third_biz_no
  596. return transfer_result
  597. @classmethod
  598. async def transfer_list_service(
  599. cls,
  600. auth: AuthSchema,
  601. page_no: int = 1,
  602. page_size: int = 20,
  603. search: dict | None = None,
  604. ) -> dict:
  605. """
  606. 查询转账记录列表
  607. """
  608. log.info(f"查询转账记录列表: {page_no}, {page_size}, {search}")
  609. crud = TransferCRUD(auth)
  610. offset = (page_no - 1) * page_size
  611. return await crud.page(
  612. offset=offset,
  613. limit=page_size,
  614. order_by=[{"id": "desc"}],
  615. search=search or {},
  616. out_schema=TransferListOutSchema,
  617. )
  618. @classmethod
  619. async def transfer_export_service(
  620. cls,
  621. auth: AuthSchema,
  622. start_time: str,
  623. end_time: str,
  624. enterprise_id: Optional[str] = None,
  625. ) -> bytes:
  626. """
  627. 导出转账记录报表为Excel文件
  628. """
  629. log.info(f"导出转账记录报表: {start_time} -> {end_time}")
  630. crud = TransferCRUD(auth)
  631. search = {
  632. "created_time__gte": start_time,
  633. "created_time__lte": end_time,
  634. }
  635. if enterprise_id:
  636. search["enterprise_id"] = enterprise_id
  637. records = await crud.list(
  638. search=search,
  639. order_by=[{"id": "desc"}],
  640. )
  641. from app.utils.excel_util import ExcelUtil
  642. status_map = {
  643. "DEALING": "处理中",
  644. "SUCCESS": "成功",
  645. "FAIL": "失败",
  646. "REFUND": "退票",
  647. }
  648. payee_type_map = {
  649. "ALIPAY_ACCOUNT": "支付宝账户",
  650. "BANK_CARD": "银行卡",
  651. }
  652. list_data = []
  653. for i, record in enumerate(records, start=1):
  654. payee_info = record.payee_info or {}
  655. list_data.append({
  656. "序号": i,
  657. "订单号": record.out_biz_no or "",
  658. "商户订单号": record.order_no or "",
  659. "金额(元)": str(record.amount or 0),
  660. "收款方姓名": payee_info.get("name", ""),
  661. "收款方类型": payee_type_map.get(payee_info.get("identity_type", ""), ""),
  662. "状态": status_map.get(record.status, record.status),
  663. "转账标题": record.order_title or "",
  664. "创建时间": record.created_time.strftime("%Y-%m-%d %H:%M:%S") if record.created_time else "",
  665. })
  666. mapping_dict = {
  667. "序号": "序号",
  668. "订单号": "订单号",
  669. "商户订单号": "商户订单号",
  670. "金额(元)": "金额(元)",
  671. "收款方姓名": "收款方姓名",
  672. "收款方类型": "收款方类型",
  673. "状态": "状态",
  674. "转账标题": "转账标题",
  675. "创建时间": "创建时间",
  676. }
  677. return ExcelUtil.export_list2excel(list_data, mapping_dict)
  678. @classmethod
  679. async def apply_receipt_service(
  680. cls,
  681. auth: AuthSchema,
  682. redis: Redis,
  683. data: ReceiptApplySchema,
  684. ) -> str:
  685. """
  686. 申请转账业务回单
  687. 调用: alipay.commerce.ec.trans.receipt.apply
  688. 参数:
  689. - enterprise_id: 企业ID
  690. - order_no: 支付宝转账单号
  691. 返回: file_id
  692. """
  693. from app.core.redis_crud import RedisCURD
  694. redis_crud = RedisCURD(redis)
  695. cache_key = f"receipt:{data.enterprise_id}:{data.order_no}"
  696. cached_file_id = await redis_crud.get(cache_key)
  697. if cached_file_id:
  698. log.info(f"使用缓存的 file_id: {cached_file_id}")
  699. return cached_file_id
  700. crud = EnterpriseCRUD(auth)
  701. enterprise = await crud.get_by_enterprise_id(data.enterprise_id)
  702. if not enterprise:
  703. raise CustomException(msg="企业不存在")
  704. from alipay.aop.api.request.AlipayCommerceEcTransReceiptApplyRequest import (
  705. AlipayCommerceEcTransReceiptApplyRequest,
  706. )
  707. from alipay.aop.api.domain.AlipayCommerceEcTransReceiptApplyModel import (
  708. AlipayCommerceEcTransReceiptApplyModel,
  709. )
  710. from alipay.aop.api.response.AlipayCommerceEcTransReceiptApplyResponse import (
  711. AlipayCommerceEcTransReceiptApplyResponse,
  712. )
  713. model = AlipayCommerceEcTransReceiptApplyModel()
  714. model.enterprise_id = data.enterprise_id
  715. model.order_no = data.order_no
  716. request = AlipayCommerceEcTransReceiptApplyRequest()
  717. request.biz_model = model
  718. client = AlipayClient.get_client()
  719. response = client.execute(request)
  720. if not response:
  721. raise CustomException(msg="申请回单失败: 无响应")
  722. result = AlipayCommerceEcTransReceiptApplyResponse()
  723. result.parse_response_content(response)
  724. if not result.is_success():
  725. # 清除缓存
  726. await redis_crud.delete(cache_key)
  727. raise CustomException(msg=f"申请回单失败: {result.msg}")
  728. file_id = str(result.file_id)
  729. await redis_crud.set(cache_key, file_id, expire=172800)
  730. log.info(f"申请回单成功: order_no={data.order_no}, file_id={file_id}")
  731. return file_id
  732. @classmethod
  733. async def query_receipt_service(cls, enterprise_id: str, file_id: str) -> dict:
  734. """
  735. 查询回单状态
  736. 调用: alipay.commerce.ec.trans.receipt.query
  737. 参数:
  738. - file_id: 文件申请号
  739. 返回: {file_id, status, download_url, error_message}
  740. """
  741. from alipay.aop.api.request.AlipayCommerceEcTransReceiptQueryRequest import (
  742. AlipayCommerceEcTransReceiptQueryRequest,
  743. )
  744. from alipay.aop.api.response.AlipayCommerceEcTransReceiptQueryResponse import (
  745. AlipayCommerceEcTransReceiptQueryResponse,
  746. )
  747. from alipay.aop.api.domain.AlipayCommerceEcTransReceiptQueryModel import (
  748. AlipayCommerceEcTransReceiptQueryModel,
  749. )
  750. model = AlipayCommerceEcTransReceiptQueryModel()
  751. model.enterprise_id = enterprise_id
  752. model.file_id = file_id
  753. request = AlipayCommerceEcTransReceiptQueryRequest()
  754. request.biz_model = model
  755. client = AlipayClient.get_client()
  756. response = client.execute(request)
  757. if not response:
  758. raise CustomException(msg="查询回单失败: 无响应")
  759. result = AlipayCommerceEcTransReceiptQueryResponse()
  760. result.parse_response_content(response)
  761. if not result.is_success():
  762. raise CustomException(msg=f"查询回单失败: {result.msg}")
  763. data = {
  764. "file_id": file_id,
  765. "status": result.status,
  766. "download_url": result.download_url,
  767. "error_message": result.error_message,
  768. }
  769. return data
  770. @classmethod
  771. async def transfer_sync_status_service(
  772. cls,
  773. auth: AuthSchema,
  774. data: "TransferSyncStatusSchema",
  775. ) -> dict:
  776. """
  777. 手动同步转账状态(管理员补录)
  778. 用于修复因通知丢失而卡在 DEALING 的转账记录
  779. """
  780. from app.plugin.module_payment.account.crud import TransferCRUD
  781. from app.plugin.module_payment.account.schema import TransferSyncStatusSchema
  782. crud = TransferCRUD(auth)
  783. transfer = await crud.get_by_out_biz_no(data.out_biz_no)
  784. if not transfer:
  785. raise CustomException(msg=f"转账记录不存在: {data.out_biz_no}")
  786. if transfer.status != "DEALING" and data.status == "SUCCESS":
  787. raise CustomException(msg=f"转账记录当前状态为 {transfer.status},无需同步")
  788. update_data = {"status": data.status}
  789. if data.error_code:
  790. update_data["error_code"] = data.error_code
  791. if data.error_msg:
  792. update_data["error_msg"] = data.error_msg
  793. for key, value in update_data.items():
  794. if hasattr(transfer, key):
  795. setattr(transfer, key, value)
  796. await auth.db.flush()
  797. await auth.db.refresh(transfer)
  798. log.info(f"手动同步转账状态成功: out_biz_no={data.out_biz_no}, {transfer.status}")
  799. return {
  800. "out_biz_no": transfer.out_biz_no,
  801. "status": transfer.status,
  802. "error_code": transfer.error_code,
  803. "error_msg": transfer.error_msg,
  804. }
  805. @classmethod
  806. async def transfer_sync_all_service(
  807. cls,
  808. auth: AuthSchema,
  809. ) -> dict:
  810. """
  811. 全量同步转账状态
  812. 尝试调 fund.trans.common.query,如无权限则降级为列出 DEALING 记录供手动同步
  813. """
  814. from sqlalchemy import select
  815. from app.plugin.module_payment.account.model import TransferModel
  816. from app.plugin.module_payment.account.enums import TransferStatusEnum
  817. stmt = select(TransferModel).where(
  818. TransferModel.out_biz_no.isnot(None),
  819. ).order_by(TransferModel.id.asc())
  820. result = await auth.db.execute(stmt)
  821. all_transfers = result.scalars().all()
  822. synced = 0
  823. errors = 0
  824. details = []
  825. _has_permission = True
  826. for transfer in all_transfers:
  827. out_biz_no = transfer.out_biz_no
  828. eid = transfer.enterprise_id
  829. if not out_biz_no or not eid:
  830. continue
  831. try:
  832. result = await cls._sync_transfer_detail(auth, out_biz_no, eid)
  833. if result is False:
  834. # 两个方案都失败了(无权限),停止全量同步
  835. _has_permission = False
  836. break
  837. if isinstance(result, str):
  838. synced += 1
  839. details.append({"out_biz_no": out_biz_no, "old_status": transfer.status, "new_status": result})
  840. else:
  841. details.append({"out_biz_no": out_biz_no, "status": transfer.status, "action": "no_change"})
  842. except Exception as e:
  843. errors += 1
  844. details.append({"out_biz_no": out_biz_no, "status": transfer.status, "error": str(e)})
  845. log.warning(f"全量同步 - 查询失败: out_biz_no={out_biz_no}, err={e}")
  846. if not _has_permission:
  847. dealing = [t for t in all_transfers if t.status == TransferStatusEnum.DEALING.value]
  848. return {
  849. "total": len(all_transfers),
  850. "synced": synced,
  851. "no_permission": True,
  852. "dealing_count": len(dealing),
  853. "details": [{"out_biz_no": t.out_biz_no, "status": t.status} for t in dealing],
  854. "note": "无法通过支付宝 API 查询转账状态,请在开放平台开通 alipay.fund.trans.common.query 权限,或逐个使用 sync-status 手动补录",
  855. }
  856. if synced > 0:
  857. await auth.db.flush()
  858. return {
  859. "total": len(all_transfers),
  860. "synced": synced,
  861. "errors": errors,
  862. "details": details,
  863. }
  864. @classmethod
  865. async def _sync_transfer_detail(
  866. cls,
  867. auth: AuthSchema,
  868. out_biz_no: str,
  869. enterprise_id: str,
  870. ) -> str | bool | None:
  871. """查询单笔转账详情并更新本地记录
  872. 优先调 fund.trans.common.query,无权限时改用 consume.detail.query(用 order_no 当 pay_no 查)
  873. 返回: 新状态str / False(无权限) / None(失败/无变化)
  874. """
  875. from sqlalchemy import select, update as sa_update
  876. from app.plugin.module_payment.account.model import TransferModel
  877. from app.core.alipay import AlipayClient
  878. # 先查本地记录
  879. tf_stmt = select(TransferModel).where(TransferModel.out_biz_no == out_biz_no)
  880. tf_result = await auth.db.execute(tf_stmt)
  881. local_transfer = tf_result.scalar_one_or_none()
  882. # — 方案A: fund.trans.common.query —
  883. try:
  884. from alipay.aop.api.request.AlipayFundTransCommonQueryRequest import (
  885. AlipayFundTransCommonQueryRequest,
  886. )
  887. from alipay.aop.api.domain.AlipayFundTransCommonQueryModel import (
  888. AlipayFundTransCommonQueryModel,
  889. )
  890. from alipay.aop.api.response.AlipayFundTransCommonQueryResponse import (
  891. AlipayFundTransCommonQueryResponse,
  892. )
  893. model = AlipayFundTransCommonQueryModel()
  894. model.out_biz_no = out_biz_no
  895. model.product_code = "TRANS_ACCOUNT_NO_PWD"
  896. model.biz_scene = "DIRECT_TRANSFER"
  897. request = AlipayFundTransCommonQueryRequest()
  898. request.biz_model = model
  899. client = AlipayClient.get_client()
  900. response = client.execute(request)
  901. if response:
  902. result = AlipayFundTransCommonQueryResponse()
  903. result.parse_response_content(response)
  904. if result.is_success():
  905. alipay_status = getattr(result, 'status', None)
  906. if alipay_status and alipay_status != "DEALING":
  907. return await cls._apply_transfer_update(auth, out_biz_no, result, alipay_status)
  908. return None
  909. sub_msg = getattr(result, 'sub_msg', '') or ''
  910. if '权限' not in sub_msg and 'NO_PERMISSION' not in sub_msg:
  911. return None
  912. # 权限不足,继续方案B
  913. except ImportError:
  914. pass
  915. # — 方案B: consume.detail.query(用 order_no 当 pay_no 查) —
  916. order_no = local_transfer.order_no if local_transfer else None
  917. if not order_no:
  918. log.warning(f"无 order_no 可用于查询: out_biz_no={out_biz_no}")
  919. return False
  920. try:
  921. from alipay.aop.api.request.AlipayCommerceEcConsumeDetailQueryRequest import (
  922. AlipayCommerceEcConsumeDetailQueryRequest,
  923. )
  924. from alipay.aop.api.domain.AlipayCommerceEcConsumeDetailQueryModel import (
  925. AlipayCommerceEcConsumeDetailQueryModel,
  926. )
  927. from alipay.aop.api.response.AlipayCommerceEcConsumeDetailQueryResponse import (
  928. AlipayCommerceEcConsumeDetailQueryResponse,
  929. )
  930. model = AlipayCommerceEcConsumeDetailQueryModel()
  931. model.pay_no = order_no
  932. model.enterprise_id = enterprise_id
  933. request = AlipayCommerceEcConsumeDetailQueryRequest()
  934. request.biz_model = model
  935. client = AlipayClient.get_client()
  936. response = client.execute(request)
  937. if not response:
  938. return False
  939. result = AlipayCommerceEcConsumeDetailQueryResponse()
  940. result.parse_response_content(response)
  941. if not result.is_success():
  942. sub_code = getattr(result, 'sub_code', '') or ''
  943. sub_msg = getattr(result, 'sub_msg', '') or ''
  944. # 权限不足
  945. if '权限' in sub_msg or 'NO_PERMISSION' in sub_code:
  946. return False
  947. log.warning(f"consume.detail.query 查无记录: out_biz_no={out_biz_no}, err={sub_msg}")
  948. return None
  949. consume_info = getattr(result, 'consume_info', None)
  950. if not consume_info:
  951. return None
  952. consume_type = getattr(consume_info, 'consume_type', '')
  953. if consume_type != "TRANSFER":
  954. return None
  955. notify_reason = getattr(consume_info, 'notify_reason', '') or ''
  956. if 'SUCCESS' in notify_reason.upper():
  957. new_status = "SUCCESS"
  958. elif 'FAIL' in notify_reason.upper():
  959. new_status = "FAIL"
  960. else:
  961. return None
  962. update_data = {"status": new_status}
  963. pay_no = getattr(consume_info, 'pay_no', None)
  964. if pay_no and pay_no != order_no:
  965. update_data["order_no"] = pay_no
  966. upd = sa_update(TransferModel).where(
  967. TransferModel.out_biz_no == out_biz_no
  968. ).values(**update_data)
  969. await auth.db.execute(upd)
  970. log.info(f"转账同步(consume详情) - out_biz_no={out_biz_no}, status={new_status}")
  971. return new_status
  972. except ImportError:
  973. log.warning("consume.detail.query SDK 不可用")
  974. return False
  975. except Exception as e:
  976. log.warning(f"consume.detail.query 异常: out_biz_no={out_biz_no}, err={e}")
  977. return False
  978. @classmethod
  979. async def _apply_transfer_update(
  980. cls,
  981. auth: AuthSchema,
  982. out_biz_no: str,
  983. result: object,
  984. alipay_status: str,
  985. ) -> str | None:
  986. """根据 fund.trans.common.query 结果更新本地记录"""
  987. from sqlalchemy import update as sa_update
  988. from app.plugin.module_payment.account.model import TransferModel
  989. update_data = {"status": alipay_status}
  990. order_no = getattr(result, 'order_id', None)
  991. pay_fund_order_id = getattr(result, 'pay_fund_order_id', None)
  992. trans_amount = getattr(result, 'trans_amount', None)
  993. error_code = getattr(result, 'error_code', None)
  994. fail_reason = getattr(result, 'fail_reason', None)
  995. if order_no:
  996. update_data["order_no"] = order_no
  997. if pay_fund_order_id:
  998. update_data["fund_order_id"] = pay_fund_order_id
  999. if trans_amount:
  1000. update_data["amount"] = Decimal(str(trans_amount))
  1001. if error_code:
  1002. update_data["error_code"] = error_code
  1003. if fail_reason:
  1004. update_data["error_msg"] = fail_reason
  1005. if update_data.get("status") != "DEALING":
  1006. upd = sa_update(TransferModel).where(
  1007. TransferModel.out_biz_no == out_biz_no
  1008. ).values(**update_data)
  1009. await auth.db.execute(upd)
  1010. log.info(f"转账详情同步 - out_biz_no={out_biz_no}, status={alipay_status}")
  1011. return alipay_status
  1012. return None
  1013. @classmethod
  1014. async def update_transfer_status_service(
  1015. cls,
  1016. auth: AuthSchema,
  1017. order_no: str,
  1018. status: str,
  1019. ext_info: dict = {}
  1020. ) -> None:
  1021. """
  1022. 更新转账状态(由通知处理器调用)
  1023. """
  1024. crud = TransferCRUD(auth)
  1025. transfer = await crud.get_by_order_no(order_no)
  1026. if not transfer:
  1027. log.warning(f"转账记录不存在: {order_no}")
  1028. return
  1029. update_data = {}
  1030. update_data["status"] = status
  1031. if ext_info:
  1032. update_data["ext_info"] = ext_info
  1033. await crud.update_by_order_no(order_no, update_data)
  1034. @classmethod
  1035. async def update_deposit_status_service(
  1036. cls,
  1037. auth: AuthSchema,
  1038. out_biz_no: str,
  1039. status: str,
  1040. ) -> None:
  1041. """
  1042. 更新充值状态(由通知处理器调用)
  1043. """
  1044. crud = DepositCRUD(auth)
  1045. deposit = await crud.get_by_out_biz_no(out_biz_no)
  1046. if not deposit:
  1047. log.warning(f"充值记录不存在: {out_biz_no}")
  1048. return
  1049. update_data = {"status": status}
  1050. await crud.update_by_out_biz_no(out_biz_no, update_data)
  1051. @classmethod
  1052. async def update_withdraw_status_service(
  1053. cls,
  1054. auth: AuthSchema,
  1055. out_biz_no: str,
  1056. status: str,
  1057. error_code: str | None = None,
  1058. error_msg: str | None = None,
  1059. ) -> None:
  1060. """
  1061. 更新提现状态(由通知处理器调用)
  1062. """
  1063. crud = WithdrawCRUD(auth)
  1064. withdraw = await crud.get_by_out_biz_no(out_biz_no)
  1065. if not withdraw:
  1066. log.warning(f"提现记录不存在: {out_biz_no}")
  1067. return
  1068. update_data = {"status": status}
  1069. if error_code:
  1070. update_data["error_code"] = error_code
  1071. if error_msg:
  1072. update_data["error_msg"] = error_msg
  1073. await crud.update_by_out_biz_no(out_biz_no, update_data)
  1074. @classmethod
  1075. async def consume_detail_query_service(
  1076. cls,
  1077. auth: AuthSchema,
  1078. pay_no: str,
  1079. enterprise_id: str | None = None,
  1080. ant_shop_id: str | None = None,
  1081. query_options: list[str] | None = None,
  1082. ) -> dict:
  1083. """
  1084. 账单详情查询(✅)
  1085. 调用: alipay.commerce.ec.consume.detail.query
  1086. 用于查询企业码账单详情,支持查询关联退款、订单、票据等信息。
  1087. """
  1088. from alipay.aop.api.request.AlipayCommerceEcConsumeDetailQueryRequest import (
  1089. AlipayCommerceEcConsumeDetailQueryRequest,
  1090. )
  1091. from alipay.aop.api.domain.AlipayCommerceEcConsumeDetailQueryModel import (
  1092. AlipayCommerceEcConsumeDetailQueryModel,
  1093. )
  1094. from alipay.aop.api.response.AlipayCommerceEcConsumeDetailQueryResponse import (
  1095. AlipayCommerceEcConsumeDetailQueryResponse,
  1096. )
  1097. model = AlipayCommerceEcConsumeDetailQueryModel()
  1098. model.pay_no = pay_no
  1099. if enterprise_id:
  1100. model.enterprise_id = enterprise_id
  1101. if ant_shop_id:
  1102. model.ant_shop_id = ant_shop_id
  1103. if query_options:
  1104. model.query_options = query_options
  1105. request = AlipayCommerceEcConsumeDetailQueryRequest()
  1106. request.biz_model = model
  1107. client = AlipayClient.get_client()
  1108. response = client.execute(request)
  1109. if not response:
  1110. raise CustomException(msg="账单详情查询失败: 无响应")
  1111. result = AlipayCommerceEcConsumeDetailQueryResponse()
  1112. result.parse_response_content(response)
  1113. if not result.is_success():
  1114. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  1115. raise CustomException(msg=f"账单详情查询失败: {result.msg}")
  1116. consume_info = result.consume_info
  1117. if not consume_info:
  1118. raise CustomException(msg="账单详情查询失败: 无账单信息")
  1119. return {
  1120. "account_id": consume_info.account_id,
  1121. "pay_no": consume_info.pay_no,
  1122. "consume_type": consume_info.consume_type,
  1123. "gmt_biz_create": consume_info.gmt_biz_create,
  1124. "consume_biz_type": consume_info.consume_biz_type,
  1125. "consume_amount": consume_info.consume_amount,
  1126. "order_complete_label": consume_info.order_complete_label,
  1127. "refund_status": consume_info.refund_status,
  1128. "refund_amount": consume_info.refund_amount,
  1129. "peer_payer_card_name": consume_info.peer_payer_card_name,
  1130. "user_id": getattr(consume_info, 'user_id', None),
  1131. "open_id": getattr(consume_info, 'open_id', None),
  1132. "enterprise_id": consume_info.enterprise_id,
  1133. "employee_id": consume_info.employee_id,
  1134. "enterprise_name": getattr(consume_info, 'enterprise_name', None),
  1135. "employee_name": getattr(consume_info, 'employee_name', None),
  1136. "consume_scene_code": getattr(consume_info, 'consume_scene_code', None),
  1137. "consume_type_sub_category": getattr(consume_info, 'consume_type_sub_category', None),
  1138. "consume_title": getattr(consume_info, 'consume_title', None),
  1139. "gmt_pay": getattr(consume_info, 'gmt_pay', None),
  1140. "gmt_refund": getattr(consume_info, 'gmt_refund', None),
  1141. "pay_amount": getattr(consume_info, 'pay_amount', None),
  1142. "invoice_amount": getattr(consume_info, 'invoice_amount', None),
  1143. "peer_pay_amount": getattr(consume_info, 'peer_pay_amount', None),
  1144. "subsidy_amount": getattr(consume_info, 'subsidy_amount', None),
  1145. "ext_infos": getattr(consume_info, 'ext_infos', None),
  1146. }