service.py 49 KB

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