service.py 28 KB

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