service.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  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. TransferListOutSchema,
  28. TransferOutSchema,
  29. TenantTransferCreate,
  30. TenantTransferResponse,
  31. )
  32. from ..openapi.crud import OpenTransferCRUD
  33. class AccountService:
  34. """资金专户服务层"""
  35. @classmethod
  36. async def authorize_apply_service(
  37. cls,
  38. auth: AuthSchema,
  39. data: AccountAuthorizeApplySchema
  40. ) -> AccountAuthorizeApplyOutSchema:
  41. """
  42. 申请转账授权签约(✅)
  43. 调用: alipay.commerce.ec.trans.authorize.apply
  44. """
  45. from alipay.aop.api.request.AlipayCommerceEcTransAuthorizeApplyRequest import (
  46. AlipayCommerceEcTransAuthorizeApplyRequest,
  47. )
  48. from alipay.aop.api.domain.AlipayCommerceEcTransAuthorizeApplyModel import (
  49. AlipayCommerceEcTransAuthorizeApplyModel,
  50. )
  51. from alipay.aop.api.response.AlipayCommerceEcTransAuthorizeApplyResponse import (
  52. AlipayCommerceEcTransAuthorizeApplyResponse,
  53. )
  54. model = AlipayCommerceEcTransAuthorizeApplyModel()
  55. model.enterprise_id = data.enterprise_id
  56. request = AlipayCommerceEcTransAuthorizeApplyRequest()
  57. request.biz_model = model
  58. client = AlipayClient.get_client()
  59. response = client.execute(request)
  60. if not response:
  61. raise CustomException(msg="申请转账授权失败: 无响应")
  62. result = AlipayCommerceEcTransAuthorizeApplyResponse()
  63. result.parse_response_content(response)
  64. if not result.is_success():
  65. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  66. raise CustomException(msg=f"申请转账授权失败: {result.msg}")
  67. return AccountAuthorizeApplyOutSchema(
  68. sign_url=result.sign_url,
  69. )
  70. @classmethod
  71. async def create_account_service(
  72. cls,
  73. auth: AuthSchema,
  74. data: AccountCreateSchema
  75. ) -> AccountOperationOutSchema:
  76. """
  77. 开通资金专户(✅)
  78. 调用: alipay.commerce.ec.trans.account.create
  79. """
  80. from alipay.aop.api.request.AlipayCommerceEcTransAccountCreateRequest import (
  81. AlipayCommerceEcTransAccountCreateRequest,
  82. )
  83. from alipay.aop.api.domain.AlipayCommerceEcTransAccountCreateModel import (
  84. AlipayCommerceEcTransAccountCreateModel,
  85. )
  86. from alipay.aop.api.response.AlipayCommerceEcTransAccountCreateResponse import (
  87. AlipayCommerceEcTransAccountCreateResponse,
  88. )
  89. model = AlipayCommerceEcTransAccountCreateModel()
  90. model.enterprise_id = data.enterprise_id
  91. # model.account_type = data.account_type or "ALL" # 收支全能户
  92. # model.scene = data.scene or "B2B_TRANS" # ToB转账场景
  93. model.account_type = "ALL"
  94. model.scene = "B2B_TRANS"
  95. request = AlipayCommerceEcTransAccountCreateRequest()
  96. request.biz_model = model
  97. client = AlipayClient.get_client()
  98. response = client.execute(request)
  99. if not response:
  100. raise CustomException(msg="开通资金专户失败: 无响应")
  101. result = AlipayCommerceEcTransAccountCreateResponse()
  102. result.parse_response_content(response)
  103. if not result.is_success():
  104. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  105. raise CustomException(msg=f"开通资金专户失败: {result.msg}")
  106. account_data = AccountCreateSchema(
  107. enterprise_id=model.enterprise_id,
  108. account_book_id=result.account_book_id,
  109. account_type=model.account_type,
  110. scene=model.scene,
  111. )
  112. if result.account_book_id:
  113. account_data.account_book_id = result.account_book_id
  114. await AccountCRUD(auth).create(account_data)
  115. return AccountOperationOutSchema(
  116. enterprise_id=account_data.enterprise_id,
  117. account_book_id=account_data.account_book_id,
  118. )
  119. @classmethod
  120. async def deposit_service(
  121. cls,
  122. auth: AuthSchema,
  123. data: AccountDepositSchema
  124. ) -> AccountDepositOutSchema:
  125. """
  126. 资金专户充值(✅)
  127. 调用: alipay.commerce.ec.trans.account.deposit
  128. """
  129. from alipay.aop.api.request.AlipayCommerceEcTransAccountDepositRequest import (
  130. AlipayCommerceEcTransAccountDepositRequest,
  131. )
  132. from alipay.aop.api.domain.AlipayCommerceEcTransAccountDepositModel import (
  133. AlipayCommerceEcTransAccountDepositModel,
  134. )
  135. from alipay.aop.api.response.AlipayCommerceEcTransAccountDepositResponse import (
  136. AlipayCommerceEcTransAccountDepositResponse,
  137. )
  138. model = AlipayCommerceEcTransAccountDepositModel()
  139. model.enterprise_id = data.enterprise_id
  140. model.account_book_id = data.account_book_id
  141. model.amount = str(data.amount)
  142. model.out_biz_no = get_snowflake_id_str(auth.tenant_id)
  143. request = AlipayCommerceEcTransAccountDepositRequest()
  144. request.biz_model = model
  145. client = AlipayClient.get_client()
  146. response = client.execute(request)
  147. if not response:
  148. raise CustomException(msg="充值失败: 无响应")
  149. result = AlipayCommerceEcTransAccountDepositResponse()
  150. result.parse_response_content(response)
  151. if not result.is_success():
  152. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  153. raise CustomException(msg=f"充值失败: {result.msg}")
  154. deposit_crud = DepositCRUD(auth)
  155. deposit_data = {
  156. "enterprise_id": data.enterprise_id,
  157. "out_biz_no": model.out_biz_no,
  158. "account_book_id": data.account_book_id,
  159. "amount": data.amount,
  160. "url": result.url,
  161. "status": DepositStatusEnum.DEALING.value,
  162. "remark": data.remark,
  163. }
  164. await deposit_crud.create(deposit_data)
  165. return AccountDepositOutSchema(
  166. url=result.url,
  167. )
  168. @classmethod
  169. async def transfer_service(
  170. cls,
  171. auth: AuthSchema,
  172. data: AccountTransferSchema
  173. ) -> AccountTransferOutSchema:
  174. """
  175. 资金专户转账(✅)
  176. 调用: alipay.commerce.ec.trans.account.transfer
  177. """
  178. from alipay.aop.api.request.AlipayCommerceEcTransAccountTransferRequest import (
  179. AlipayCommerceEcTransAccountTransferRequest,
  180. )
  181. from alipay.aop.api.domain.AlipayCommerceEcTransAccountTransferModel import (
  182. AlipayCommerceEcTransAccountTransferModel,
  183. )
  184. from alipay.aop.api.response.AlipayCommerceEcTransAccountTransferResponse import (
  185. AlipayCommerceEcTransAccountTransferResponse,
  186. )
  187. from alipay.aop.api.domain.TransParticipant import (
  188. TransParticipant,
  189. )
  190. from alipay.aop.api.domain.BankCardExtInfoDTO import (
  191. BankCardExtInfoDTO,
  192. )
  193. # 检查资金专户是否存在
  194. account = await AccountCRUD(auth).get_by_account_book_id(data.account_book_id)
  195. if not account:
  196. raise CustomException(msg="资金账户不存在")
  197. if account.tenant_id != auth.tenant_id:
  198. raise CustomException(msg="无权限操作")
  199. if data.enterprise_id and account.enterprise_id != data.enterprise_id:
  200. raise CustomException(msg="参数错误")
  201. if not data.order_title and account.enterprise_id:
  202. enterprise = await EnterpriseCRUD(auth).get_by_enterprise_id(account.enterprise_id)
  203. if not enterprise:
  204. raise CustomException(msg="资金账户所属企业不存在")
  205. data.order_title = f"来自{enterprise.name}转账"
  206. model = AlipayCommerceEcTransAccountTransferModel()
  207. model.enterprise_id = account.enterprise_id
  208. model.account_book_id = account.account_book_id
  209. model.out_biz_no = get_snowflake_id_str(auth.tenant_id)
  210. # 转账总金额,单位为元,精确到小数点后两位
  211. model.amount = str(data.amount)
  212. model.order_title = data.order_title
  213. payee_info = TransParticipant()
  214. payee_info.identity_type = data.payee_info.identity_type
  215. payee_info.name = data.payee_info.name
  216. payee_info.identity = data.payee_info.identity
  217. if data.payee_info.bankcard_ext_info:
  218. payee_info.bankcard_ext_info = BankCardExtInfoDTO.from_alipay_dict(
  219. data.payee_info.bankcard_ext_info.model_dump(exclude_none=True)
  220. )
  221. model.payee_info = payee_info
  222. request = AlipayCommerceEcTransAccountTransferRequest()
  223. request.biz_model = model
  224. client = AlipayClient.get_client()
  225. response = client.execute(request)
  226. if not response:
  227. raise CustomException(msg="转账失败: 无响应")
  228. result = AlipayCommerceEcTransAccountTransferResponse()
  229. result.parse_response_content(response)
  230. if not result.is_success():
  231. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  232. raise CustomException(msg=f"转账失败: {result.sub_msg or result.msg or result.code}")
  233. transfer_crud = TransferCRUD(auth)
  234. transfer_data = {
  235. "enterprise_id": model.enterprise_id,
  236. "out_biz_no": model.out_biz_no,
  237. "account_book_id": model.account_book_id,
  238. "amount": model.amount,
  239. "order_title": model.order_title,
  240. "payee_info": data.payee_info.model_dump() if data.payee_info else None,
  241. "status": result.status,
  242. "order_no": result.order_no,
  243. "fund_order_id": result.fund_order_id,
  244. }
  245. await transfer_crud.create(transfer_data)
  246. return AccountTransferOutSchema(
  247. status=result.status,
  248. order_no=result.order_no,
  249. fund_order_id=result.fund_order_id,
  250. out_biz_no=model.out_biz_no,
  251. )
  252. @classmethod
  253. async def tenant_transfer_service(
  254. cls,
  255. auth: AuthSchema,
  256. tenant_id: int,
  257. data: TenantTransferCreate,
  258. request_ip: str,
  259. api_key_id: int | None = None,
  260. ) -> TenantTransferResponse:
  261. """
  262. 租户API转账(通过API Key认证)
  263. 调用: alipay.commerce.ec.trans.account.transfer
  264. """
  265. from alipay.aop.api.request.AlipayCommerceEcTransAccountTransferRequest import (
  266. AlipayCommerceEcTransAccountTransferRequest,
  267. )
  268. from alipay.aop.api.domain.AlipayCommerceEcTransAccountTransferModel import (
  269. AlipayCommerceEcTransAccountTransferModel,
  270. )
  271. from alipay.aop.api.response.AlipayCommerceEcTransAccountTransferResponse import (
  272. AlipayCommerceEcTransAccountTransferResponse,
  273. )
  274. from alipay.aop.api.domain.TransParticipant import (
  275. TransParticipant,
  276. )
  277. from alipay.aop.api.domain.BankCardExtInfoDTO import (
  278. BankCardExtInfoDTO,
  279. )
  280. # 检查资金专户是否存在
  281. account = await AccountCRUD(auth).get_by_account_book_id(data.account_book_id)
  282. if not account:
  283. raise CustomException(msg="资金账户不存在")
  284. if account.tenant_id != tenant_id:
  285. raise CustomException(msg="无权限操作")
  286. if data.enterprise_id and account.enterprise_id != data.enterprise_id:
  287. raise CustomException(msg="参数错误")
  288. if not data.order_title and account.enterprise_id:
  289. enterprise = await EnterpriseCRUD(auth).get_by_enterprise_id(account.enterprise_id)
  290. if not enterprise:
  291. raise CustomException(msg="资金账户所属企业不存在")
  292. data.order_title = f"来自{enterprise.name}转账"
  293. model = AlipayCommerceEcTransAccountTransferModel()
  294. model.enterprise_id = account.enterprise_id
  295. model.account_book_id = account.account_book_id
  296. model.out_biz_no = get_snowflake_id_str(tenant_id)
  297. # 转账总金额,单位为元,精确到小数点后两位
  298. model.amount = str(data.amount)
  299. model.order_title = data.order_title
  300. payee_info = TransParticipant()
  301. payee_info.identity_type = data.payee_info.identity_type
  302. payee_info.name = data.payee_info.name
  303. payee_info.identity = data.payee_info.identity
  304. if data.payee_info.bankcard_ext_info:
  305. payee_info.bankcard_ext_info = BankCardExtInfoDTO.from_alipay_dict(
  306. data.payee_info.bankcard_ext_info.model_dump(exclude_none=True)
  307. )
  308. model.payee_info = payee_info
  309. request = AlipayCommerceEcTransAccountTransferRequest()
  310. request.biz_model = model
  311. client = AlipayClient.get_client()
  312. response = client.execute(request)
  313. if not response:
  314. raise CustomException(msg="转账失败: 无响应")
  315. result = AlipayCommerceEcTransAccountTransferResponse()
  316. result.parse_response_content(response)
  317. if not result.is_success():
  318. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  319. raise CustomException(msg=f"转账失败: {result.sub_msg or result.msg or result.code}")
  320. transfer_crud = TransferCRUD(auth)
  321. transfer_data = {
  322. "enterprise_id": model.enterprise_id,
  323. "out_biz_no": model.out_biz_no,
  324. "account_book_id": model.account_book_id,
  325. "amount": model.amount,
  326. "order_title": model.order_title,
  327. "payee_info": data.payee_info.model_dump() if data.payee_info else None,
  328. "status": result.status,
  329. "order_no": result.order_no,
  330. "fund_order_id": result.fund_order_id,
  331. }
  332. await transfer_crud.create(transfer_data)
  333. return TenantTransferResponse(
  334. status=result.status,
  335. order_no=result.order_no,
  336. fund_order_id=result.fund_order_id,
  337. )
  338. @classmethod
  339. async def withdraw_service(
  340. cls,
  341. auth: AuthSchema,
  342. data: AccountWithdrawSchema
  343. ) -> AccountOperationOutSchema:
  344. """
  345. 资金专户提现
  346. 调用: alipay.commerce.ec.trans.account.withdraw
  347. 接口文档: https://opendocs.alipay.com/pre-open/d651859b_alipay.commerce.ec.trans.account.withdraw
  348. 参数说明:
  349. - enterprise_id: 企业ID
  350. - account_book_id: 资金专户号
  351. - amount: 提现金额
  352. - out_biz_no: 商家侧订单号(唯一)
  353. """
  354. from alipay.aop.api.request.AlipayCommerceEcTransAccountWithdrawRequest import (
  355. AlipayCommerceEcTransAccountWithdrawRequest,
  356. )
  357. from alipay.aop.api.domain.AlipayCommerceEcTransAccountWithdrawModel import (
  358. AlipayCommerceEcTransAccountWithdrawModel,
  359. )
  360. from alipay.aop.api.response.AlipayCommerceEcTransAccountWithdrawResponse import (
  361. AlipayCommerceEcTransAccountWithdrawResponse,
  362. )
  363. crud = AccountCRUD(auth)
  364. enterprise = await crud.get_by_enterprise_id(data.enterprise_id)
  365. if not enterprise:
  366. raise CustomException(msg="企业不存在")
  367. model = AlipayCommerceEcTransAccountWithdrawModel()
  368. model.enterprise_id = enterprise.enterprise_id
  369. model.account_book_id = data.account_book_id
  370. model.amount = str(data.amount)
  371. model.out_biz_no = get_snowflake_id_str(auth.tenant_id)
  372. request = AlipayCommerceEcTransAccountWithdrawRequest()
  373. request.biz_model = model
  374. client = AlipayClient.get_client()
  375. response = client.execute(request)
  376. if not response:
  377. raise CustomException(msg="提现失败: 无响应")
  378. result = AlipayCommerceEcTransAccountWithdrawResponse()
  379. result.parse_response_content(response)
  380. if not result.is_success():
  381. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  382. raise CustomException(msg=f"提现失败: {result.msg}")
  383. withdraw_crud = WithdrawCRUD(auth)
  384. withdraw_data = {
  385. "enterprise_id": data.enterprise_id,
  386. "out_biz_no": model.out_biz_no,
  387. "account_book_id": data.account_book_id,
  388. "amount": data.amount,
  389. # 专户提现到余额户是同步操作,要么执行成功,要么执行异常,
  390. # 出参status设计多余,遵循规范使用业务码区分成功与失败
  391. "status": WithdrawStatusEnum.SUCCESS.value,
  392. "order_no": result.order_no,
  393. }
  394. await withdraw_crud.create(withdraw_data)
  395. log.info(f"资金专户提现发起成功: 企业: {data.enterprise_id}, 金额: {data.amount}")
  396. return AccountOperationOutSchema(
  397. enterprise_id=data.enterprise_id,
  398. account_book_id=data.account_book_id,
  399. )
  400. @classmethod
  401. async def query_account_service(
  402. cls,
  403. auth: AuthSchema,
  404. data: AccountQuerySchema
  405. ) -> list[Any]:
  406. """
  407. 查询资金专户(调用支付宝接口)
  408. 调用: alipay.commerce.ec.trans.account.query
  409. """
  410. from alipay.aop.api.request.AlipayCommerceEcTransAccountQueryRequest import (
  411. AlipayCommerceEcTransAccountQueryRequest,
  412. )
  413. from alipay.aop.api.domain.AlipayCommerceEcTransAccountQueryModel import (
  414. AlipayCommerceEcTransAccountQueryModel,
  415. )
  416. from alipay.aop.api.response.AlipayCommerceEcTransAccountQueryResponse import (
  417. AlipayCommerceEcTransAccountQueryResponse,
  418. )
  419. from alipay.aop.api.domain.FundAccountApiDTO import (
  420. FundAccountApiDTO,
  421. )
  422. model = AlipayCommerceEcTransAccountQueryModel()
  423. model.enterprise_id = data.enterprise_id
  424. request = AlipayCommerceEcTransAccountQueryRequest()
  425. request.biz_model = model
  426. client = AlipayClient.get_client()
  427. response = client.execute(request)
  428. if not response:
  429. raise CustomException(msg="查询资金专户失败: 无响应")
  430. result = AlipayCommerceEcTransAccountQueryResponse()
  431. result.parse_response_content(response)
  432. if not result.is_success():
  433. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  434. raise CustomException(msg=f"查询资金专户失败: {result.msg}")
  435. collect = []
  436. for v in list(result.account_list or []):
  437. if not hasattr(v, "account_book_id"):
  438. continue
  439. if not hasattr(v, "scene") or v.scene != "B2B_TRANS":
  440. continue
  441. account = FundAccountApiDTO.to_alipay_dict(v)
  442. collect.append(account)
  443. return collect
  444. @classmethod
  445. async def transfer_detail_service(
  446. cls,
  447. auth: AuthSchema,
  448. out_biz_no: str
  449. ) -> TransferOutSchema:
  450. """
  451. 查询转账记录详情
  452. """
  453. crud = TransferCRUD(auth)
  454. transfer = await crud.get_by_out_biz_no(out_biz_no)
  455. if not transfer:
  456. raise CustomException(msg="转账记录不存在")
  457. transfer_result = TransferOutSchema.model_validate(transfer)
  458. # 查询三方订单号
  459. open_transfer_crud = OpenTransferCRUD(auth)
  460. open_transfer_data = await open_transfer_crud.get(out_biz_no=transfer.out_biz_no)
  461. if open_transfer_data:
  462. transfer_result.third_biz_no = open_transfer_data.third_biz_no
  463. return transfer_result
  464. @classmethod
  465. async def transfer_list_service(
  466. cls,
  467. auth: AuthSchema,
  468. page_no: int = 1,
  469. page_size: int = 20,
  470. search: dict | None = None,
  471. ) -> dict:
  472. """
  473. 查询转账记录列表
  474. """
  475. log.info(f"查询转账记录列表: {page_no}, {page_size}, {search}")
  476. crud = TransferCRUD(auth)
  477. offset = (page_no - 1) * page_size
  478. return await crud.page(
  479. offset=offset,
  480. limit=page_size,
  481. order_by=[{"id": "desc"}],
  482. search=search or {},
  483. out_schema=TransferListOutSchema,
  484. )
  485. @classmethod
  486. async def transfer_export_service(
  487. cls,
  488. auth: AuthSchema,
  489. start_time: str,
  490. end_time: str,
  491. enterprise_id: Optional[str] = None,
  492. ) -> bytes:
  493. """
  494. 导出转账记录报表为Excel文件
  495. """
  496. log.info(f"导出转账记录报表: {start_time} -> {end_time}")
  497. crud = TransferCRUD(auth)
  498. search = {
  499. "created_time__gte": start_time,
  500. "created_time__lte": end_time,
  501. }
  502. if enterprise_id:
  503. search["enterprise_id"] = enterprise_id
  504. records = await crud.list(
  505. search=search,
  506. order_by=[{"id": "desc"}],
  507. )
  508. from app.utils.excel_util import ExcelUtil
  509. status_map = {
  510. "DEALING": "处理中",
  511. "SUCCESS": "成功",
  512. "FAIL": "失败",
  513. "REFUND": "退票",
  514. }
  515. payee_type_map = {
  516. "ALIPAY_ACCOUNT": "支付宝账户",
  517. "BANK_CARD": "银行卡",
  518. }
  519. list_data = []
  520. for i, record in enumerate(records, start=1):
  521. payee_info = record.payee_info or {}
  522. list_data.append({
  523. "序号": i,
  524. "订单号": record.out_biz_no or "",
  525. "商户订单号": record.order_no or "",
  526. "金额(元)": str(record.amount or 0),
  527. "收款方姓名": payee_info.get("name", ""),
  528. "收款方类型": payee_type_map.get(payee_info.get("identity_type", ""), ""),
  529. "状态": status_map.get(record.status, record.status),
  530. "转账标题": record.order_title or "",
  531. "创建时间": record.created_time.strftime("%Y-%m-%d %H:%M:%S") if record.created_time else "",
  532. })
  533. mapping_dict = {
  534. "序号": "序号",
  535. "订单号": "订单号",
  536. "商户订单号": "商户订单号",
  537. "金额(元)": "金额(元)",
  538. "收款方姓名": "收款方姓名",
  539. "收款方类型": "收款方类型",
  540. "状态": "状态",
  541. "转账标题": "转账标题",
  542. "创建时间": "创建时间",
  543. }
  544. return ExcelUtil.export_list2excel(list_data, mapping_dict)
  545. @classmethod
  546. async def update_transfer_status_service(
  547. cls,
  548. auth: AuthSchema,
  549. order_no: str,
  550. status: str,
  551. ext_info: dict = {}
  552. ) -> None:
  553. """
  554. 更新转账状态(由通知处理器调用)
  555. """
  556. crud = TransferCRUD(auth)
  557. transfer = await crud.get_by_order_no(order_no)
  558. if not transfer:
  559. log.warning(f"转账记录不存在: {order_no}")
  560. return
  561. update_data = {}
  562. update_data["status"] = status
  563. if ext_info:
  564. update_data["ext_info"] = ext_info
  565. await crud.update_by_order_no(order_no, update_data)
  566. @classmethod
  567. async def update_deposit_status_service(
  568. cls,
  569. auth: AuthSchema,
  570. out_biz_no: str,
  571. status: str,
  572. ) -> None:
  573. """
  574. 更新充值状态(由通知处理器调用)
  575. """
  576. crud = DepositCRUD(auth)
  577. deposit = await crud.get_by_out_biz_no(out_biz_no)
  578. if not deposit:
  579. log.warning(f"充值记录不存在: {out_biz_no}")
  580. return
  581. update_data = {"status": status}
  582. await crud.update_by_out_biz_no(out_biz_no, update_data)
  583. @classmethod
  584. async def update_withdraw_status_service(
  585. cls,
  586. auth: AuthSchema,
  587. out_biz_no: str,
  588. status: str,
  589. error_code: str | None = None,
  590. error_msg: str | None = None,
  591. ) -> None:
  592. """
  593. 更新提现状态(由通知处理器调用)
  594. """
  595. crud = WithdrawCRUD(auth)
  596. withdraw = await crud.get_by_out_biz_no(out_biz_no)
  597. if not withdraw:
  598. log.warning(f"提现记录不存在: {out_biz_no}")
  599. return
  600. update_data = {"status": status}
  601. if error_code:
  602. update_data["error_code"] = error_code
  603. if error_msg:
  604. update_data["error_msg"] = error_msg
  605. await crud.update_by_out_biz_no(out_biz_no, update_data)
  606. @classmethod
  607. async def consume_detail_query_service(
  608. cls,
  609. auth: AuthSchema,
  610. pay_no: str,
  611. enterprise_id: str | None = None,
  612. ant_shop_id: str | None = None,
  613. query_options: list[str] | None = None,
  614. ) -> dict:
  615. """
  616. 账单详情查询(✅)
  617. 调用: alipay.commerce.ec.consume.detail.query
  618. 用于查询企业码账单详情,支持查询关联退款、订单、票据等信息。
  619. """
  620. from alipay.aop.api.request.AlipayCommerceEcConsumeDetailQueryRequest import (
  621. AlipayCommerceEcConsumeDetailQueryRequest,
  622. )
  623. from alipay.aop.api.domain.AlipayCommerceEcConsumeDetailQueryModel import (
  624. AlipayCommerceEcConsumeDetailQueryModel,
  625. )
  626. from alipay.aop.api.response.AlipayCommerceEcConsumeDetailQueryResponse import (
  627. AlipayCommerceEcConsumeDetailQueryResponse,
  628. )
  629. model = AlipayCommerceEcConsumeDetailQueryModel()
  630. model.pay_no = pay_no
  631. if enterprise_id:
  632. model.enterprise_id = enterprise_id
  633. if ant_shop_id:
  634. model.ant_shop_id = ant_shop_id
  635. if query_options:
  636. model.query_options = query_options
  637. request = AlipayCommerceEcConsumeDetailQueryRequest()
  638. request.biz_model = model
  639. client = AlipayClient.get_client()
  640. response = client.execute(request)
  641. if not response:
  642. raise CustomException(msg="账单详情查询失败: 无响应")
  643. result = AlipayCommerceEcConsumeDetailQueryResponse()
  644. result.parse_response_content(response)
  645. if not result.is_success():
  646. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  647. raise CustomException(msg=f"账单详情查询失败: {result.msg}")
  648. consume_info = result.consume_info
  649. if not consume_info:
  650. raise CustomException(msg="账单详情查询失败: 无账单信息")
  651. return {
  652. "account_id": consume_info.account_id,
  653. "pay_no": consume_info.pay_no,
  654. "consume_type": consume_info.consume_type,
  655. "gmt_biz_create": consume_info.gmt_biz_create,
  656. "consume_biz_type": consume_info.consume_biz_type,
  657. "consume_amount": consume_info.consume_amount,
  658. "order_complete_label": consume_info.order_complete_label,
  659. "refund_status": consume_info.refund_status,
  660. "refund_amount": consume_info.refund_amount,
  661. "peer_payer_card_name": consume_info.peer_payer_card_name,
  662. "user_id": getattr(consume_info, 'user_id', None),
  663. "open_id": getattr(consume_info, 'open_id', None),
  664. "enterprise_id": consume_info.enterprise_id,
  665. "employee_id": consume_info.employee_id,
  666. "enterprise_name": getattr(consume_info, 'enterprise_name', None),
  667. "employee_name": getattr(consume_info, 'employee_name', None),
  668. "consume_scene_code": getattr(consume_info, 'consume_scene_code', None),
  669. "consume_type_sub_category": getattr(consume_info, 'consume_type_sub_category', None),
  670. "consume_title": getattr(consume_info, 'consume_title', None),
  671. "gmt_pay": getattr(consume_info, 'gmt_pay', None),
  672. "gmt_refund": getattr(consume_info, 'gmt_refund', None),
  673. "pay_amount": getattr(consume_info, 'pay_amount', None),
  674. "invoice_amount": getattr(consume_info, 'invoice_amount', None),
  675. "peer_pay_amount": getattr(consume_info, 'peer_pay_amount', None),
  676. "subsidy_amount": getattr(consume_info, 'subsidy_amount', None),
  677. "ext_infos": getattr(consume_info, 'ext_infos', None),
  678. }