service.py 33 KB

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