service.py 33 KB

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