service.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834
  1. from datetime import datetime
  2. from decimal import Decimal
  3. from app.api.v1.module_system.auth.schema import AuthSchema
  4. from app.core.alipay import AlipayClient
  5. from app.core.exceptions import CustomException
  6. from app.core.logger import log
  7. from app.utils.snowflake import get_snowflake_id
  8. from .enums import QuotaStatusEnum
  9. from .schema import (
  10. ExpenseQuotaCreateSchema,
  11. ExpenseQuotaDeleteSchema,
  12. ExpenseQuotaModifySchema,
  13. ExpenseQuotaQueryOutSchema,
  14. ExpenseQuotaQuerySchema,
  15. IssueBatchCancelOutSchema,
  16. IssueBatchCancelSchema,
  17. IssueBatchCreateOutSchema,
  18. IssueBatchCreateSchema,
  19. IssueBatchListOutSchema,
  20. IssueBatchRecordsQueryOutSchema,
  21. IssueBatchRecordsQuerySchema,
  22. IssueQuotaCheckFailedItem,
  23. IssueRecordInfoItem,
  24. QuotaCreateSchema,
  25. QuotaDetailInfoSchema,
  26. QuotaListOutSchema,
  27. QuotaOperationOutSchema,
  28. QuotaOutSchema,
  29. QuotaUpdateSchema,
  30. )
  31. from .crud import IssueBatchCRUD, QuotaCRUD
  32. from .model import IssueBatchModel
  33. class QuotaService:
  34. """额度服务层"""
  35. @classmethod
  36. async def create_expense_quota_service(
  37. cls, auth: AuthSchema, data: ExpenseQuotaCreateSchema
  38. ) -> QuotaOperationOutSchema:
  39. """
  40. 创建余额/点券
  41. 调用: alipay.ebpp.invoice.expensecontrol.quota.create
  42. """
  43. crud = QuotaCRUD(auth)
  44. out_biz_no = data.outer_source_id or str(get_snowflake_id())
  45. try:
  46. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaCreateRequest import (
  47. AlipayEbppInvoiceExpensecontrolQuotaCreateRequest,
  48. )
  49. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaCreateModel import (
  50. AlipayEbppInvoiceExpensecontrolQuotaCreateModel,
  51. )
  52. from alipay.aop.api.domain.IssueQuotaTarget import (
  53. IssueQuotaTarget,
  54. )
  55. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaCreateResponse import (
  56. AlipayEbppInvoiceExpensecontrolQuotaCreateResponse,
  57. )
  58. except ImportError:
  59. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  60. model = AlipayEbppInvoiceExpensecontrolQuotaCreateModel()
  61. model.target_type = data.target_type
  62. model.target_id = data.target_id
  63. model.enterprise_id = data.enterprise_id
  64. model.outer_source_id = out_biz_no
  65. model.quota_type = data.quota_type or "CAP"
  66. model.share_mode = data.share_mode or "0"
  67. if data.effective_start_date:
  68. model.effective_start_date = data.effective_start_date.strftime("%Y-%m-%d %H:%M:%S")
  69. if data.effective_end_date:
  70. model.effective_end_date = data.effective_end_date.strftime("%Y-%m-%d %H:%M:%S")
  71. if data.issue_name:
  72. model.issue_name = data.issue_name
  73. if data.issue_desc:
  74. model.issue_desc = data.issue_desc
  75. if data.issue_quota_target_list:
  76. target_list = []
  77. for item in data.issue_quota_target_list:
  78. target = IssueQuotaTarget()
  79. target.owner_type = item.owner_type
  80. target.owner_id = item.owner_id
  81. target.quota = item.quota
  82. if item.amount is not None:
  83. target.amount = item.amount
  84. target_list.append(target)
  85. model.issue_quota_target_list = target_list
  86. request = AlipayEbppInvoiceExpensecontrolQuotaCreateRequest()
  87. request.biz_model = model
  88. client = AlipayClient.get_client()
  89. response = client.execute(request)
  90. if not response:
  91. raise CustomException(msg="创建余额/点券失败: 无响应")
  92. result = AlipayEbppInvoiceExpensecontrolQuotaCreateResponse()
  93. result.parse_response_content(response)
  94. if not result.is_success():
  95. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  96. raise CustomException(msg=f"创建余额/点券失败: {result.msg}")
  97. quota_data = data.model_dump(exclude_none=True)
  98. quota_data["out_biz_no"] = out_biz_no
  99. quota_data["status"] = QuotaStatusEnum.QUOTA_ACTIVE.value
  100. if result.quota_id:
  101. quota_data["quota_id"] = result.quota_id
  102. quota = await crud.create(quota_data)
  103. if not quota:
  104. raise CustomException(msg="创建额度记录失败")
  105. return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=result.quota_id)
  106. @classmethod
  107. async def create_quota_service(
  108. cls, auth: AuthSchema, data: QuotaCreateSchema
  109. ) -> QuotaOperationOutSchema:
  110. """创建额度"""
  111. crud = QuotaCRUD(auth)
  112. out_biz_no = str(get_snowflake_id())
  113. quota_data = data.model_dump(exclude_none=True)
  114. quota_data["out_biz_no"] = out_biz_no
  115. quota_data["status"] = QuotaStatusEnum.QUOTA_ACTIVE.value
  116. if quota_data.get("available_amount") is None:
  117. quota_data["available_amount"] = quota_data.get("total_amount", 0)
  118. quota = await crud.create(quota_data)
  119. if not quota:
  120. raise CustomException(msg="创建额度记录失败")
  121. return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=quota.quota_id)
  122. @classmethod
  123. async def query_expense_quota_service(
  124. cls, auth: AuthSchema, data: ExpenseQuotaQuerySchema
  125. ) -> ExpenseQuotaQueryOutSchema:
  126. """
  127. 查询余额/点券
  128. 调用: alipay.ebpp.invoice.expensecontrol.quota.query
  129. """
  130. try:
  131. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaQueryRequest import (
  132. AlipayEbppInvoiceExpensecontrolQuotaQueryRequest,
  133. )
  134. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaQueryModel import (
  135. AlipayEbppInvoiceExpensecontrolQuotaQueryModel,
  136. )
  137. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaQueryResponse import (
  138. AlipayEbppInvoiceExpensecontrolQuotaQueryResponse,
  139. )
  140. except ImportError:
  141. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  142. model = AlipayEbppInvoiceExpensecontrolQuotaQueryModel()
  143. model.owner_type = data.owner_type
  144. model.page_size = data.page_size
  145. model.page_num = data.page_num
  146. if data.target_type:
  147. model.target_type = data.target_type
  148. if data.target_id:
  149. model.target_id = data.target_id
  150. if data.owner_id:
  151. model.owner_id = data.owner_id
  152. if data.owner_open_id:
  153. model.owner_open_id = data.owner_open_id
  154. if data.enterprise_id:
  155. model.enterprise_id = data.enterprise_id
  156. if data.quota_id_list:
  157. model.quota_id_list = data.quota_id_list
  158. if data.quota_type:
  159. model.quota_type = data.quota_type
  160. request = AlipayEbppInvoiceExpensecontrolQuotaQueryRequest()
  161. request.biz_model = model
  162. client = AlipayClient.get_client()
  163. response = client.execute(request)
  164. if not response:
  165. raise CustomException(msg="查询余额/点券失败: 无响应")
  166. result = AlipayEbppInvoiceExpensecontrolQuotaQueryResponse()
  167. result.parse_response_content(response)
  168. if not result.is_success():
  169. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  170. raise CustomException(msg=f"查询余额/点券失败: {result.msg}")
  171. return ExpenseQuotaQueryOutSchema(
  172. page_num=result.page_num or data.page_num,
  173. page_size=result.page_size or data.page_size,
  174. total_page_count=result.total_page_count or 0,
  175. )
  176. @classmethod
  177. async def modify_expense_quota_service(
  178. cls, auth: AuthSchema, out_biz_no: str, data: ExpenseQuotaModifySchema
  179. ) -> QuotaOperationOutSchema:
  180. """
  181. 修改余额/点券
  182. 调用: alipay.ebpp.invoice.expensecontrol.quota.modify
  183. """
  184. crud = QuotaCRUD(auth)
  185. quota = await crud.get_by_out_biz_no(out_biz_no)
  186. if not quota:
  187. raise CustomException(msg="额度不存在")
  188. try:
  189. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaModifyRequest import (
  190. AlipayEbppInvoiceExpensecontrolQuotaModifyRequest,
  191. )
  192. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaModifyModel import (
  193. AlipayEbppInvoiceExpensecontrolQuotaModifyModel,
  194. )
  195. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaModifyResponse import (
  196. AlipayEbppInvoiceExpensecontrolQuotaModifyResponse,
  197. )
  198. except ImportError:
  199. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  200. model = AlipayEbppInvoiceExpensecontrolQuotaModifyModel()
  201. model.quota_id = data.quota_id
  202. model.action = data.action
  203. model.outer_source_id = data.outer_source_id
  204. model.enterprise_id = data.enterprise_id
  205. if data.amount is not None:
  206. model.amount = str(data.amount)
  207. if data.share_mode:
  208. model.share_mode = data.share_mode
  209. request = AlipayEbppInvoiceExpensecontrolQuotaModifyRequest()
  210. request.biz_model = model
  211. client = AlipayClient.get_client()
  212. response = client.execute(request)
  213. if not response:
  214. raise CustomException(msg="修改余额/点券失败: 无响应")
  215. result = AlipayEbppInvoiceExpensecontrolQuotaModifyResponse()
  216. result.parse_response_content(response)
  217. if not result.is_success():
  218. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  219. raise CustomException(msg=f"修改余额/点券失败: {result.msg}")
  220. return QuotaOperationOutSchema(
  221. out_biz_no=out_biz_no,
  222. quota_id=data.quota_id,
  223. result=result.success,
  224. )
  225. @classmethod
  226. async def delete_expense_quota_service(
  227. cls, auth: AuthSchema, out_biz_no: str, data: ExpenseQuotaDeleteSchema
  228. ) -> QuotaOperationOutSchema:
  229. """
  230. 删除额度
  231. 调用: alipay.ebpp.invoice.expensecontrol.quota.delete
  232. """
  233. crud = QuotaCRUD(auth)
  234. quota = await crud.get_by_out_biz_no(out_biz_no)
  235. if not quota:
  236. raise CustomException(msg="额度不存在")
  237. try:
  238. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest import (
  239. AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest,
  240. )
  241. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaDeleteModel import (
  242. AlipayEbppInvoiceExpensecontrolQuotaDeleteModel,
  243. )
  244. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse import (
  245. AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse,
  246. )
  247. except ImportError:
  248. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  249. model = AlipayEbppInvoiceExpensecontrolQuotaDeleteModel()
  250. model.enterprise_id = data.enterprise_id
  251. if data.quota_id:
  252. model.quota_id = data.quota_id
  253. if data.issue_batch_id:
  254. model.issue_batch_id = data.issue_batch_id
  255. request = AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest()
  256. request.biz_model = model
  257. client = AlipayClient.get_client()
  258. response = client.execute(request)
  259. if not response:
  260. raise CustomException(msg="删除额度失败: 无响应")
  261. result = AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse()
  262. result.parse_response_content(response)
  263. if not result.is_success():
  264. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  265. raise CustomException(msg=f"删除额度失败: {result.msg}")
  266. await crud.delete(id=quota.id)
  267. return QuotaOperationOutSchema(out_biz_no=out_biz_no)
  268. # ========================
  269. # 手工批量发放额度
  270. # ========================
  271. @classmethod
  272. async def issue_batch_create_service(
  273. cls, auth: AuthSchema, data: IssueBatchCreateSchema
  274. ) -> IssueBatchCreateOutSchema:
  275. """
  276. 手工批量发放额度
  277. 调用: alipay.ebpp.invoice.expensecontrol.issuebatch.create
  278. """
  279. try:
  280. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolIssuebatchCreateRequest import (
  281. AlipayEbppInvoiceExpensecontrolIssuebatchCreateRequest,
  282. )
  283. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolIssuebatchCreateModel import (
  284. AlipayEbppInvoiceExpensecontrolIssuebatchCreateModel,
  285. )
  286. from alipay.aop.api.domain.IssueTargetInfoContent import (
  287. IssueTargetInfoContent,
  288. )
  289. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolIssuebatchCreateResponse import (
  290. AlipayEbppInvoiceExpensecontrolIssuebatchCreateResponse,
  291. )
  292. except ImportError:
  293. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  294. # 本地检查批次号是否已存在,避免无效调用支付宝
  295. try:
  296. issue_batch_crud = IssueBatchCRUD(auth)
  297. existing_batch = await issue_batch_crud.get_by_batch_no(data.batch_no)
  298. if existing_batch:
  299. raise CustomException(msg=f"批次号 {data.batch_no} 已存在,请勿重复创建")
  300. except CustomException:
  301. raise
  302. except Exception:
  303. pass
  304. model = AlipayEbppInvoiceExpensecontrolIssuebatchCreateModel()
  305. model.enterprise_id = data.enterprise_id
  306. model.issue_name = data.issue_name
  307. model.quota_type = data.quota_type
  308. model.effective_start_date = data.effective_start_date
  309. model.effective_end_date = data.effective_end_date
  310. model.institution_id = data.institution_id
  311. model.batch_no = data.batch_no
  312. model.share_mode = data.share_mode
  313. if data.issue_desc:
  314. model.issue_desc = data.issue_desc
  315. if data.issue_target_info_list:
  316. target_list = []
  317. for item in data.issue_target_info_list:
  318. target = IssueTargetInfoContent()
  319. target.issue_quota = item.issue_quota
  320. if item.owner_open_id:
  321. target.owner_open_id = item.owner_open_id
  322. if item.owner_id:
  323. target.owner_id = item.owner_id
  324. if item.user_name:
  325. target.user_name = item.user_name
  326. if item.owner_type:
  327. target.owner_type = item.owner_type
  328. target_list.append(target)
  329. model.issue_target_info_list = target_list
  330. request = AlipayEbppInvoiceExpensecontrolIssuebatchCreateRequest()
  331. request.biz_model = model
  332. client = AlipayClient.get_client()
  333. response = client.execute(request)
  334. if not response:
  335. raise CustomException(msg="手工批量发放额度失败: 无响应")
  336. result = AlipayEbppInvoiceExpensecontrolIssuebatchCreateResponse()
  337. result.parse_response_content(response)
  338. if not result.is_success():
  339. sub_msg = getattr(result, 'sub_msg', '') or ''
  340. err_detail = f"{result.msg}" + (f" - {sub_msg}" if sub_msg else "")
  341. log.error(f"支付宝接口调用失败: {result.code} - {err_detail}")
  342. raise CustomException(msg=f"手工批量发放额度失败: {sub_msg or result.msg}")
  343. # 保存批次记录到本地
  344. try:
  345. issue_batch_crud = IssueBatchCRUD(auth)
  346. total_amount = Decimal("0")
  347. if data.issue_target_info_list:
  348. for item in data.issue_target_info_list:
  349. try:
  350. total_amount += Decimal(item.issue_quota)
  351. except Exception:
  352. pass
  353. batch_data = {
  354. "issue_batch_id": result.issue_batch_id,
  355. "batch_no": data.batch_no,
  356. "institution_id": data.institution_id,
  357. "issue_name": data.issue_name,
  358. "quota_type": data.quota_type,
  359. "share_mode": data.share_mode,
  360. "total_count": len(data.issue_target_info_list or []),
  361. "total_amount": total_amount,
  362. "status": "ACTIVE",
  363. "effective_start_date": datetime.strptime(data.effective_start_date, "%Y-%m-%d %H:%M:%S"),
  364. "effective_end_date": datetime.strptime(data.effective_end_date, "%Y-%m-%d %H:%M:%S"),
  365. "issue_desc": data.issue_desc,
  366. }
  367. await issue_batch_crud.create(batch_data)
  368. except Exception as e:
  369. log.warning(f"保存发放批次记录失败(不影响发放): {e}")
  370. # 组装校验失败列表(在更新本地记录前获取,用于过滤)
  371. failed_owner_ids: set[str] = set()
  372. if hasattr(result, 'issue_quota_check_failed_list') and result.issue_quota_check_failed_list:
  373. failed_owner_ids = {
  374. str(getattr(f, 'owner_id', ''))
  375. for f in result.issue_quota_check_failed_list
  376. if getattr(f, 'owner_id', None)
  377. }
  378. # 更新本地额度记录(跳过校验失败的员工)
  379. try:
  380. from app.plugin.module_payment.expense.quota.model import QuotaModel
  381. from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
  382. from sqlalchemy import insert, select, update as sa_update
  383. tenant_id = auth.user.tenant_id if auth.user else 1
  384. effective_start = datetime.strptime(data.effective_start_date, "%Y-%m-%d %H:%M:%S") if data.effective_start_date else None
  385. effective_end = datetime.strptime(data.effective_end_date, "%Y-%m-%d %H:%M:%S") if data.effective_end_date else None
  386. updated_count = 0
  387. inserted_count = 0
  388. if data.issue_target_info_list:
  389. for item in data.issue_target_info_list:
  390. emp_id = (item.owner_id or "")
  391. # 跳过支付宝校验失败的员工
  392. if emp_id in failed_owner_ids:
  393. log.warning(f"手工发放 - 跳过校验失败员工: owner_id={emp_id}")
  394. continue
  395. try:
  396. quota_amount = Decimal(item.issue_quota)
  397. except Exception:
  398. quota_amount = Decimal("0")
  399. # 查询是否已有该员工在该制度下的额度记录
  400. check = select(QuotaModel).where(
  401. QuotaModel.employee_id == emp_id,
  402. QuotaModel.institution_id == data.institution_id,
  403. )
  404. existing = await auth.db.execute(check)
  405. existing_quota = existing.scalar_one_or_none()
  406. if existing_quota:
  407. # 已有记录 → 更新金额、状态、有效期
  408. upd = sa_update(QuotaModel).where(
  409. QuotaModel.id == existing_quota.id
  410. ).values(
  411. total_amount=quota_amount,
  412. available_amount=quota_amount,
  413. status=QuotaStatusEnum.QUOTA_ACTIVE.value,
  414. quota_type=data.quota_type,
  415. valid_from=effective_start,
  416. valid_to=effective_end,
  417. target_type="INSTITUTION",
  418. target_id=data.institution_id,
  419. )
  420. await auth.db.execute(upd)
  421. updated_count += 1
  422. else:
  423. # 没有记录 → 新增
  424. stmt = insert(QuotaModel).values(
  425. employee_id=emp_id,
  426. institution_id=data.institution_id,
  427. quota_type=data.quota_type,
  428. target_type="INSTITUTION",
  429. target_id=data.institution_id,
  430. out_biz_no=f"batch_{data.batch_no}_{emp_id}",
  431. total_amount=quota_amount,
  432. available_amount=quota_amount,
  433. status=QuotaStatusEnum.QUOTA_ACTIVE.value,
  434. valid_from=effective_start,
  435. valid_to=effective_end,
  436. enterprise_id=data.enterprise_id,
  437. tenant_id=tenant_id,
  438. )
  439. await auth.db.execute(stmt)
  440. inserted_count += 1
  441. await auth.db.flush()
  442. log.info(f"手工发放 - 更新 {updated_count} 条、新增 {inserted_count} 条额度记录到本地(跳过 {len(failed_owner_ids)} 条校验失败)")
  443. # 查询支付宝端的发放记录,获取每个员工的 real quota_id 并更新本地记录
  444. try:
  445. from alipay.aop.api.request.AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryRequest import (
  446. AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryRequest,
  447. )
  448. from alipay.aop.api.domain.AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryModel import (
  449. AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryModel,
  450. )
  451. from alipay.aop.api.response.AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryResponse import (
  452. AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryResponse,
  453. )
  454. records_model = AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryModel()
  455. records_model.enterprise_id = data.enterprise_id
  456. records_model.institution_id = data.institution_id
  457. records_model.issue_batch_id = result.issue_batch_id
  458. records_model.page_size = len(data.issue_target_info_list)
  459. records_model.page_num = 1
  460. records_request = AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryRequest()
  461. records_request.biz_model = records_model
  462. records_response = client.execute(records_request)
  463. if records_response:
  464. records_result = AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryResponse()
  465. records_result.parse_response_content(records_response)
  466. if records_result.is_success() and hasattr(records_result, 'issue_record_info_list') and records_result.issue_record_info_list:
  467. updated_quota_id_count = 0
  468. for r in records_result.issue_record_info_list:
  469. quota_id = getattr(r, 'quota_id', None)
  470. owner_id = getattr(r, 'owner_id', None)
  471. if quota_id and owner_id:
  472. upd_q = sa_update(QuotaModel).where(
  473. QuotaModel.employee_id == owner_id,
  474. QuotaModel.institution_id == data.institution_id,
  475. ).values(quota_id=quota_id)
  476. await auth.db.execute(upd_q)
  477. updated_quota_id_count += 1
  478. if updated_quota_id_count:
  479. await auth.db.flush()
  480. log.info(f"手工发放 - 已从支付宝查询并更新 {updated_quota_id_count} 条记录的 quota_id")
  481. except Exception as e:
  482. log.warning(f"查询支付宝发放记录获取 quota_id 失败(不影响发放): {e}")
  483. except Exception as e:
  484. log.warning(f"保存额度记录到本地失败(不影响发放): {e}")
  485. # 组装校验失败列表
  486. failed_list = None
  487. if hasattr(result, 'issue_quota_check_failed_list') and result.issue_quota_check_failed_list:
  488. failed_list = []
  489. for f in result.issue_quota_check_failed_list:
  490. failed_list.append(IssueQuotaCheckFailedItem(
  491. user_name=getattr(f, 'user_name', None),
  492. owner_type=getattr(f, 'owner_type', None),
  493. owner_id=getattr(f, 'owner_id', None),
  494. owner_open_id=getattr(f, 'owner_open_id', None),
  495. issue_quota=getattr(f, 'issue_quota', None),
  496. message=getattr(f, 'message', None),
  497. result=getattr(f, 'result', None),
  498. ))
  499. return IssueBatchCreateOutSchema(
  500. issue_batch_id=result.issue_batch_id,
  501. issue_quota_check_failed_list=failed_list,
  502. )
  503. @classmethod
  504. async def issue_batch_cancel_service(
  505. cls, auth: AuthSchema, data: IssueBatchCancelSchema
  506. ) -> IssueBatchCancelOutSchema:
  507. """
  508. 作废手工发放批次
  509. 调用: alipay.ebpp.invoice.expensecontrol.issuebatch.cancel
  510. """
  511. try:
  512. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolIssuebatchCancelRequest import (
  513. AlipayEbppInvoiceExpensecontrolIssuebatchCancelRequest,
  514. )
  515. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolIssuebatchCancelModel import (
  516. AlipayEbppInvoiceExpensecontrolIssuebatchCancelModel,
  517. )
  518. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolIssuebatchCancelResponse import (
  519. AlipayEbppInvoiceExpensecontrolIssuebatchCancelResponse,
  520. )
  521. except ImportError:
  522. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  523. model = AlipayEbppInvoiceExpensecontrolIssuebatchCancelModel()
  524. model.enterprise_id = data.enterprise_id
  525. model.institution_id = data.institution_id
  526. model.issue_batch_id = data.issue_batch_id
  527. request = AlipayEbppInvoiceExpensecontrolIssuebatchCancelRequest()
  528. request.biz_model = model
  529. client = AlipayClient.get_client()
  530. response = client.execute(request)
  531. if not response:
  532. raise CustomException(msg="作废手工发放批次失败: 无响应")
  533. result = AlipayEbppInvoiceExpensecontrolIssuebatchCancelResponse()
  534. result.parse_response_content(response)
  535. if not result.is_success():
  536. sub_msg = getattr(result, 'sub_msg', '') or ''
  537. err_detail = f"{result.msg}" + (f" - {sub_msg}" if sub_msg else "")
  538. log.error(f"支付宝接口调用失败: {result.code} - {err_detail}")
  539. raise CustomException(msg=f"作废手工发放批次失败: {sub_msg or result.msg}")
  540. # 更新本地批次记录状态
  541. try:
  542. issue_batch_crud = IssueBatchCRUD(auth)
  543. batch = await issue_batch_crud.get_by_issue_batch_id(data.issue_batch_id)
  544. if batch:
  545. setattr(batch, 'status', 'CANCELLED')
  546. await issue_batch_crud.update(batch.id, {"status": "CANCELLED"})
  547. except Exception as e:
  548. log.warning(f"更新批次本地状态失败(不影响作废): {e}")
  549. # 作废批次后,将涉及员工的额度记录回退为待发放状态
  550. try:
  551. from app.plugin.module_payment.expense.quota.model import QuotaModel
  552. from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
  553. from sqlalchemy import select, update as sa_update
  554. records_resp = await cls.issue_batch_records_query_service(
  555. auth,
  556. IssueBatchRecordsQuerySchema(
  557. enterprise_id=data.enterprise_id,
  558. institution_id=data.institution_id,
  559. issue_batch_id=data.issue_batch_id,
  560. page_size=100,
  561. page_num=1,
  562. ),
  563. )
  564. if records_resp.issue_record_info_list:
  565. reset_count = 0
  566. for record in records_resp.issue_record_info_list:
  567. if record.issue_status != 1:
  568. continue
  569. owner_id = record.owner_id
  570. quota_id = record.quota_id
  571. if not owner_id:
  572. continue
  573. upd = sa_update(QuotaModel).where(
  574. QuotaModel.employee_id == owner_id,
  575. QuotaModel.institution_id == data.institution_id,
  576. ).values(
  577. total_amount=0,
  578. available_amount=0,
  579. status=QuotaStatusEnum.QUOTA_PENDING.value,
  580. quota_id=None,
  581. )
  582. await auth.db.execute(upd)
  583. reset_count += 1
  584. if reset_count:
  585. await auth.db.flush()
  586. log.info(f"批次作废 - 已回退 {reset_count} 条额度记录为待发放状态")
  587. except Exception as e:
  588. log.warning(f"回退额度记录失败(不影响作废): {e}")
  589. return IssueBatchCancelOutSchema(
  590. result=getattr(result, 'result', False),
  591. )
  592. @classmethod
  593. async def issue_batch_records_query_service(
  594. cls, auth: AuthSchema, data: IssueBatchRecordsQuerySchema
  595. ) -> IssueBatchRecordsQueryOutSchema:
  596. """
  597. 查询手工发放发放明细
  598. 调用: alipay.ebpp.invoice.issuebatch.issuerecords.batchquery
  599. """
  600. try:
  601. from alipay.aop.api.request.AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryRequest import (
  602. AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryRequest,
  603. )
  604. from alipay.aop.api.domain.AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryModel import (
  605. AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryModel,
  606. )
  607. from alipay.aop.api.response.AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryResponse import (
  608. AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryResponse,
  609. )
  610. except ImportError:
  611. raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
  612. model = AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryModel()
  613. model.enterprise_id = data.enterprise_id
  614. model.institution_id = data.institution_id
  615. model.issue_batch_id = data.issue_batch_id
  616. model.page_size = data.page_size
  617. model.page_num = data.page_num
  618. request = AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryRequest()
  619. request.biz_model = model
  620. client = AlipayClient.get_client()
  621. response = client.execute(request)
  622. if not response:
  623. raise CustomException(msg="查询手工发放明细失败: 无响应")
  624. result = AlipayEbppInvoiceIssuebatchIssuerecordsBatchqueryResponse()
  625. result.parse_response_content(response)
  626. if not result.is_success():
  627. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  628. raise CustomException(msg=f"查询手工发放明细失败: {result.msg}")
  629. record_list = None
  630. if hasattr(result, 'issue_record_info_list') and result.issue_record_info_list:
  631. record_list = []
  632. for r in result.issue_record_info_list:
  633. record_list.append(IssueRecordInfoItem(
  634. quota_id=getattr(r, 'quota_id', None),
  635. issue_quota=getattr(r, 'issue_quota', None),
  636. issue_status=getattr(r, 'issue_status', None),
  637. owner_type=getattr(r, 'owner_type', None),
  638. owner_id=getattr(r, 'owner_id', None),
  639. owner_open_id=getattr(r, 'owner_open_id', None),
  640. user_name=getattr(r, 'user_name', None),
  641. currency=getattr(r, 'currency', None),
  642. ))
  643. return IssueBatchRecordsQueryOutSchema(
  644. page_num=getattr(result, 'page_num', data.page_num),
  645. page_size=getattr(result, 'page_size', data.page_size),
  646. total_page_count=getattr(result, 'total_page_count', 0),
  647. issue_record_info_list=record_list,
  648. )
  649. @classmethod
  650. async def list_batch_service(
  651. cls,
  652. auth: AuthSchema,
  653. page_no: int = 1,
  654. page_size: int = 20,
  655. search: dict | None = None,
  656. ) -> dict:
  657. """分页查询手工发放批次列表(本地DB)"""
  658. crud = IssueBatchCRUD(auth)
  659. offset = (page_no - 1) * page_size
  660. return await crud.page(
  661. offset=offset,
  662. limit=page_size,
  663. order_by=[{"id": "desc"}],
  664. search=search or {},
  665. out_schema=IssueBatchListOutSchema,
  666. )
  667. @classmethod
  668. async def list_service(
  669. cls,
  670. auth: AuthSchema,
  671. page_no: int = 1,
  672. page_size: int = 20,
  673. search: dict | None = None,
  674. ) -> dict:
  675. crud = QuotaCRUD(auth)
  676. offset = (page_no - 1) * page_size
  677. result = await crud.page(
  678. offset=offset,
  679. limit=page_size,
  680. order_by=[{"id": "desc"}],
  681. search=search or {},
  682. out_schema=QuotaListOutSchema,
  683. )
  684. # 如果指定了 institution_id,同时查询支付宝端自动发放的额度
  685. if search and search.get("institution_id"):
  686. try:
  687. from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaQueryRequest import (
  688. AlipayEbppInvoiceExpensecontrolQuotaQueryRequest,
  689. )
  690. from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaQueryModel import (
  691. AlipayEbppInvoiceExpensecontrolQuotaQueryModel,
  692. )
  693. from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaQueryResponse import (
  694. AlipayEbppInvoiceExpensecontrolQuotaQueryResponse,
  695. )
  696. alipay_model = AlipayEbppInvoiceExpensecontrolQuotaQueryModel()
  697. # owner_type 为必填字段,查询制度下所有额度时使用通用类型
  698. alipay_model.owner_type = "ENTERPRISE_PAY_UID"
  699. alipay_model.target_type = "INSTITUTION"
  700. alipay_model.target_id = search["institution_id"]
  701. alipay_model.page_size = 100
  702. alipay_model.page_num = 1
  703. request = AlipayEbppInvoiceExpensecontrolQuotaQueryRequest()
  704. request.biz_model = alipay_model
  705. client = AlipayClient.get_client()
  706. response = client.execute(request)
  707. if response:
  708. alipay_result = AlipayEbppInvoiceExpensecontrolQuotaQueryResponse()
  709. alipay_result.parse_response_content(response)
  710. if alipay_result.is_success() and hasattr(alipay_result, 'quota_detail_info_list') and alipay_result.quota_detail_info_list:
  711. # 将支付宝端额度合并到结果中(去重)
  712. existing_ids = {item.get("quota_id") for item in result.get("items", []) if item.get("quota_id")}
  713. for q in alipay_result.quota_detail_info_list:
  714. qid = getattr(q, 'quota_id', None)
  715. if qid and qid not in existing_ids:
  716. result["items"].append({
  717. "quota_id": qid,
  718. "target_type": getattr(q, 'target_type', None),
  719. "target_id": getattr(q, 'target_id', None),
  720. "quota_type": getattr(q, 'quota_type', None),
  721. "employee_id": getattr(q, 'owner_id', None),
  722. "total_amount": getattr(q, 'total_amount', None),
  723. "available_amount": getattr(q, 'available_amount', None),
  724. "status": getattr(q, 'status', "QUOTA_ACTIVE"),
  725. "created_time": getattr(q, 'effective_start_date', None),
  726. })
  727. existing_ids.add(qid)
  728. result["total"] = len(result["items"])
  729. except Exception as e:
  730. log.warning(f"查询支付宝端额度失败(不影响本地数据): {e}")
  731. return result