service.py 46 KB

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