service.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. import asyncio
  2. from app.api.v1.module_system.auth.schema import AuthSchema
  3. from app.core.alipay import AlipayClient
  4. from app.core.exceptions import CustomException
  5. from app.core.logger import log
  6. from app.plugin.module_payment.expense.institution.schema import InstitutionListOutSchema, InstitutionCreateSchema
  7. from .crud import InstitutionCRUD
  8. from .enums import InstitutionStatusEnum
  9. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionCreateRequest import (
  10. AlipayEbppInvoiceInstitutionCreateRequest,
  11. )
  12. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
  13. AlipayEbppInvoiceInstitutionCreateModel,
  14. )
  15. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionCreateResponse import (
  16. AlipayEbppInvoiceInstitutionCreateResponse,
  17. )
  18. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionPageinfoQueryRequest import (
  19. AlipayEbppInvoiceInstitutionPageinfoQueryRequest,
  20. )
  21. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionPageinfoQueryModel import (
  22. AlipayEbppInvoiceInstitutionPageinfoQueryModel,
  23. )
  24. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionPageinfoQueryResponse import (
  25. AlipayEbppInvoiceInstitutionPageinfoQueryResponse,
  26. )
  27. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDetailinfoQueryRequest import (
  28. AlipayEbppInvoiceInstitutionDetailinfoQueryRequest,
  29. )
  30. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDetailinfoQueryModel import (
  31. AlipayEbppInvoiceInstitutionDetailinfoQueryModel,
  32. )
  33. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionDetailinfoQueryResponse import (
  34. AlipayEbppInvoiceInstitutionDetailinfoQueryResponse,
  35. )
  36. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDeleteRequest import (
  37. AlipayEbppInvoiceInstitutionDeleteRequest,
  38. )
  39. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDeleteModel import (
  40. AlipayEbppInvoiceInstitutionDeleteModel,
  41. )
  42. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionDeleteResponse import (
  43. AlipayEbppInvoiceInstitutionDeleteResponse,
  44. )
  45. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionModifyRequest import (
  46. AlipayEbppInvoiceInstitutionModifyRequest,
  47. )
  48. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionModifyModel import (
  49. AlipayEbppInvoiceInstitutionModifyModel,
  50. )
  51. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionModifyResponse import (
  52. AlipayEbppInvoiceInstitutionModifyResponse,
  53. )
  54. class InstitutionService:
  55. """费控制度服务层"""
  56. @classmethod
  57. def _execute_alipay(cls, request):
  58. """同步执行支付宝调用(通过线程池避免阻塞事件循环)"""
  59. client = AlipayClient.get_client()
  60. return client.execute(request)
  61. @classmethod
  62. async def create_institution_service(
  63. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionCreateModel
  64. ) -> AlipayEbppInvoiceInstitutionCreateResponse:
  65. """
  66. 创建费控制度(仅调 institution.create,不包含串联流程)
  67. 调用: alipay.ebpp.invoice.institution.create
  68. """
  69. if data.enterprise_id is None:
  70. raise CustomException(msg="创建费控制度失败: 企业ID不能为空")
  71. data.currency = 'CNY'
  72. request = AlipayEbppInvoiceInstitutionCreateRequest()
  73. request.biz_model = data
  74. response = await asyncio.to_thread(cls._execute_alipay, request)
  75. if not response:
  76. raise CustomException(msg="创建费控制度失败: 无响应")
  77. result = AlipayEbppInvoiceInstitutionCreateResponse()
  78. result.parse_response_content(response)
  79. if not result.is_success():
  80. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  81. raise CustomException(msg=f"创建费控制度失败: {result.msg}")
  82. return result
  83. @classmethod
  84. async def create_institution_full_flow(
  85. cls,
  86. auth: AuthSchema,
  87. institution_model: AlipayEbppInvoiceInstitutionCreateModel,
  88. enterprise_id: str,
  89. scope_data: dict | None = None,
  90. issuerule_data: dict | None = None,
  91. raw_data: dict | None = None,
  92. ) -> dict:
  93. """
  94. 创建费控制度(完整串联流程)
  95. 流程:
  96. 1. institution.create → 获取 institution_id
  97. 2. scope.modify ← 如有适用成员数据(scope_data)
  98. 3. issuerule.create ← 如为"按固定周期发放"(issuerule_data)
  99. 4. 保存到本地DB(制度 + 使用规则 + 发放规则)
  100. """
  101. # 第1步:创建制度
  102. institution_result = await cls.create_institution_service(auth=auth, data=institution_model)
  103. institution_id = institution_result.institution_id
  104. try:
  105. # 第2步:设置适用成员(如有)
  106. scope_modified = False
  107. if scope_data and scope_data.get("adapter_type") and scope_data.get("adapter_type") != "NONE":
  108. await InstitutionScopeService.scope_modify_service(
  109. auth=auth,
  110. institution_id=institution_id,
  111. data={
  112. "enterprise_id": enterprise_id,
  113. "adapter_type": scope_data["adapter_type"],
  114. "owner_type": scope_data.get("owner_type"),
  115. "add_owner_id_list": scope_data.get("add_owner_id_list"),
  116. },
  117. )
  118. scope_modified = True
  119. log.info(f"成员设置成功: institution_id={institution_id}")
  120. # 第3步:创建自动发放规则(如为"按固定周期发放")
  121. issue_rule_id = None
  122. if issuerule_data:
  123. issuerule_result = await IssueruleService.create_issuerule_service(
  124. auth=auth,
  125. institution_id=institution_id,
  126. enterprise_id=enterprise_id,
  127. quota_type=issuerule_data.get("quota_type", "CAP"),
  128. issue_type=issuerule_data.get("issue_type", "ISSUE_MONTH"),
  129. issue_amount_value=issuerule_data.get("issue_amount_value", "0"),
  130. outer_source_id=issuerule_data.get("outer_source_id"),
  131. issue_rule_name=issuerule_data.get("issue_rule_name"),
  132. effective_period=issuerule_data.get("effective_period"),
  133. invalid_mode=issuerule_data.get("invalid_mode", 0),
  134. share_mode=issuerule_data.get("share_mode", 0),
  135. )
  136. issue_rule_id = issuerule_result.get("issue_rule_id")
  137. log.info(f"发放规则创建成功: institution_id={institution_id}, issue_rule_id={issue_rule_id}")
  138. except Exception as e:
  139. # 子步骤失败:删除已创建的支付宝制度(补偿事务)
  140. log.error(f"创建串联流程失败: {e},开始回滚 institution_id={institution_id}")
  141. try:
  142. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDeleteRequest import (
  143. AlipayEbppInvoiceInstitutionDeleteRequest,
  144. )
  145. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDeleteModel import (
  146. AlipayEbppInvoiceInstitutionDeleteModel,
  147. )
  148. rollback_model = AlipayEbppInvoiceInstitutionDeleteModel()
  149. rollback_model.institution_id = institution_id
  150. rollback_model.enterprise_id = enterprise_id
  151. req = AlipayEbppInvoiceInstitutionDeleteRequest()
  152. req.biz_model = rollback_model
  153. await asyncio.to_thread(cls._execute_alipay, req)
  154. log.info(f"回滚成功: 已删除 institution_id={institution_id}")
  155. except Exception as rollback_err:
  156. log.error(f"回滚失败: {rollback_err}")
  157. raise
  158. # 第4步:保存到本地DB
  159. create_data = InstitutionCreateSchema(
  160. enterprise_id=enterprise_id,
  161. institution_id=institution_id,
  162. institution_name=getattr(institution_model, 'institution_name', None),
  163. institution_desc=getattr(institution_model, 'institution_desc', None),
  164. scene_type=getattr(institution_model, 'scene_type', None),
  165. expense_type=getattr(institution_model, 'expense_type', None),
  166. expense_sub_type=getattr(institution_model, 'expense_sub_type', None),
  167. status=InstitutionStatusEnum.INSTITUTION_CREATE.value,
  168. effective=getattr(institution_model, 'effective', None),
  169. effective_start_date=getattr(institution_model, 'effective_start_date', None),
  170. effective_end_date=getattr(institution_model, 'effective_end_date', None),
  171. consult_mode=getattr(institution_model, 'consult_mode', None),
  172. multi_employee_share_mode=getattr(institution_model, 'multi_employee_share_mode', None),
  173. currency=getattr(institution_model, 'currency', None),
  174. grant_mode=(raw_data or {}).get("grant_mode"),
  175. period_type=(raw_data or {}).get("period_type"),
  176. amount=(raw_data or {}).get("amount"),
  177. single_limit=(raw_data or {}).get("single_limit"),
  178. effective_time_type=(raw_data or {}).get("effective_time_type"),
  179. applicable_scope=(raw_data or {}).get("applicable_scope"),
  180. )
  181. create_data_dict = create_data.model_dump(exclude_unset=True)
  182. crud = InstitutionCRUD(auth)
  183. await crud.create(create_data_dict)
  184. # 第5步:保存使用规则到本地
  185. if raw_data and raw_data.get("standard_info_list") and hasattr(institution_result, 'standard_id_info_list') and institution_result.standard_id_info_list:
  186. from app.plugin.module_payment.expense.rule.crud import RuleCRUD
  187. from app.plugin.module_payment.expense.rule.service import RuleService
  188. standard_id_map = {}
  189. for info in institution_result.standard_id_info_list:
  190. if hasattr(info, 'outer_source_id') and hasattr(info, 'standard_id'):
  191. standard_id_map[info.outer_source_id] = info.standard_id
  192. for idx, std in enumerate(raw_data["standard_info_list"]):
  193. condition_list = std.get("standard_condition_info_list", [])
  194. single_limit_val = None
  195. for cond in (condition_list or []):
  196. if cond.get("rule_factor") == "QUOTA_TOTAL":
  197. try:
  198. single_limit_val = float(cond.get("rule_value", 0))
  199. except (ValueError, TypeError):
  200. pass
  201. std_data = {
  202. "out_biz_no": std.get("outer_source_id", f"std_{institution_id}_{idx}"),
  203. "institution_id": institution_id,
  204. "rule_id": standard_id_map.get(std.get("outer_source_id", "")),
  205. "standard_name": std.get("standard_name"),
  206. "standard_desc": std.get("standard_desc"),
  207. "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
  208. "enterprise_id": enterprise_id,
  209. "tenant_id": auth.user.tenant_id if auth.user else 1,
  210. "condition_info": condition_list,
  211. "single_limit": single_limit_val,
  212. }
  213. try:
  214. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  215. from sqlalchemy import insert
  216. stmt = insert(ExpenseRuleModel).values(**std_data)
  217. await auth.db.execute(stmt)
  218. await auth.db.flush()
  219. except Exception as e:
  220. log.warning(f"保存使用规则到本地失败: {e}")
  221. # 第6步:保存发放规则到本地
  222. if issuerule_data and issue_rule_id:
  223. from app.plugin.module_payment.expense.quota.model import QuotaModel
  224. quota_save_data = {
  225. "employee_id": "",
  226. "institution_id": institution_id,
  227. "out_biz_no": issuerule_data.get("outer_source_id", f"issue_{institution_id}"),
  228. "quota_id": issue_rule_id,
  229. "total_amount": float(issuerule_data.get("issue_amount_value", 0)),
  230. "available_amount": float(issuerule_data.get("issue_amount_value", 0)),
  231. "status": "QUOTA_ACTIVE",
  232. "enterprise_id": enterprise_id,
  233. "tenant_id": auth.user.tenant_id if auth.user else 1,
  234. }
  235. try:
  236. from sqlalchemy import insert
  237. stmt = insert(QuotaModel).values(**quota_save_data)
  238. await auth.db.execute(stmt)
  239. await auth.db.flush()
  240. except Exception as e:
  241. log.warning(f"保存发放规则到本地失败: {e}")
  242. return {
  243. "institution_id": institution_id,
  244. "scope_modified": scope_modified,
  245. "issue_rule_id": issue_rule_id,
  246. }
  247. @classmethod
  248. async def pageinfo_query_service(
  249. cls,
  250. auth: AuthSchema,
  251. enterprise_id: str,
  252. page_no: int = 1,
  253. page_size: int = 20,
  254. institution_name: str | None = None,
  255. ) -> dict:
  256. """
  257. 从支付宝查询费控制度列表
  258. 调用: alipay.ebpp.invoice.institution.pageinfo.query
  259. 失败时降级到本地DB
  260. """
  261. try:
  262. model = AlipayEbppInvoiceInstitutionPageinfoQueryModel()
  263. model.enterprise_id = enterprise_id
  264. model.page_num = page_no
  265. model.page_size = page_size
  266. if institution_name:
  267. model.institution_name = institution_name
  268. req = AlipayEbppInvoiceInstitutionPageinfoQueryRequest()
  269. req.biz_model = model
  270. response = await asyncio.to_thread(cls._execute_alipay, req)
  271. if response:
  272. result = AlipayEbppInvoiceInstitutionPageinfoQueryResponse()
  273. result.parse_response_content(response)
  274. if result.is_success():
  275. return {
  276. "page_no": getattr(result, 'page_num', page_no) or page_no,
  277. "page_size": getattr(result, 'page_size', page_size) or page_size,
  278. "total": getattr(result, 'total_page_count', 0) or 0,
  279. "list": getattr(result, 'institution_list', []) or [],
  280. }
  281. log.warning("支付宝 pageinfo.query 失败,降级到本地DB")
  282. except Exception as e:
  283. log.warning(f"支付宝 pageinfo.query 异常: {e},降级到本地DB")
  284. # 降级:查本地DB
  285. crud = InstitutionCRUD(auth)
  286. search = {"enterprise_id": enterprise_id}
  287. if institution_name:
  288. search["institution_name"] = institution_name
  289. offset = (page_no - 1) * page_size
  290. return await crud.page(
  291. offset=offset,
  292. limit=page_size,
  293. order_by=[{"id": "desc"}],
  294. search=search,
  295. out_schema=InstitutionListOutSchema,
  296. )
  297. @classmethod
  298. async def detailinfo_query_service(
  299. cls,
  300. auth: AuthSchema,
  301. institution_id: str,
  302. enterprise_id: str,
  303. ) -> dict | None:
  304. """
  305. 从支付宝查询费控制度详情,并补充本地规则和额度数据
  306. 调用: alipay.ebpp.invoice.institution.detailinfo.query
  307. 失败时降级到本地DB
  308. """
  309. result_dict = None
  310. try:
  311. model = AlipayEbppInvoiceInstitutionDetailinfoQueryModel()
  312. model.institution_id = institution_id
  313. model.enterprise_id = enterprise_id
  314. req = AlipayEbppInvoiceInstitutionDetailinfoQueryRequest()
  315. req.biz_model = model
  316. response = await asyncio.to_thread(cls._execute_alipay, req)
  317. if response:
  318. result = AlipayEbppInvoiceInstitutionDetailinfoQueryResponse()
  319. result.parse_response_content(response)
  320. if result.is_success():
  321. result_dict = result.to_alipay_dict()
  322. if not result_dict:
  323. log.warning("支付宝 detailinfo.query 失败,降级到本地DB")
  324. except Exception as e:
  325. log.warning(f"支付宝 detailinfo.query 异常: {e},降级到本地DB")
  326. # 降级:查本地DB
  327. if not result_dict:
  328. crud = InstitutionCRUD(auth)
  329. obj = await crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
  330. if obj:
  331. result_dict = InstitutionListOutSchema.model_validate(obj).model_dump()
  332. if not result_dict:
  333. return None
  334. # 合并本地DB的自定义字段(支付宝不包含的字段)
  335. try:
  336. crud = InstitutionCRUD(auth)
  337. local_obj = await crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
  338. if local_obj:
  339. local_dict = InstitutionListOutSchema.model_validate(local_obj).model_dump()
  340. for field in ("applicable_scope", "grant_mode", "period_type", "amount",
  341. "single_limit", "effective_time_type", "employee_ids",
  342. "period_type", "scope_owner_id_list"):
  343. if field in local_dict and local_dict[field] is not None:
  344. result_dict[field] = local_dict[field]
  345. except Exception as e:
  346. log.warning(f"合并本地DB字段失败: {e}")
  347. # 补充本地规则和额度
  348. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  349. from app.plugin.module_payment.expense.quota.model import QuotaModel
  350. from sqlalchemy import select
  351. # 查使用规则
  352. rule_stmt = select(ExpenseRuleModel).where(ExpenseRuleModel.institution_id == institution_id)
  353. rule_result = await auth.db.execute(rule_stmt)
  354. rules = rule_result.scalars().all()
  355. if rules:
  356. rule_list = []
  357. for r in rules:
  358. rule_item = {
  359. "rule_id": r.rule_id,
  360. "standard_name": r.standard_name,
  361. "standard_desc": r.standard_desc,
  362. }
  363. if hasattr(r, 'condition_info') and r.condition_info:
  364. rule_item["condition_info"] = r.condition_info
  365. if hasattr(r, 'single_limit') and r.single_limit:
  366. rule_item["single_limit"] = float(r.single_limit)
  367. rule_list.append(rule_item)
  368. result_dict["rule_list"] = rule_list
  369. # 查额度
  370. quota_stmt = select(QuotaModel).where(QuotaModel.institution_id == institution_id)
  371. quota_result = await auth.db.execute(quota_stmt)
  372. quotas = quota_result.scalars().all()
  373. if quotas:
  374. result_dict["quota_list"] = [
  375. {
  376. "quota_id": q.quota_id,
  377. "total_amount": float(q.total_amount) if q.total_amount else 0,
  378. "available_amount": float(q.available_amount) if q.available_amount else 0,
  379. "status": q.status,
  380. }
  381. for q in quotas
  382. ]
  383. return result_dict
  384. @classmethod
  385. async def list_service(
  386. cls,
  387. auth: AuthSchema,
  388. page_no: int = 1,
  389. page_size: int = 20,
  390. search: dict | None = None,
  391. ) -> dict:
  392. """
  393. 查询费控制度列表
  394. 优先调支付宝,失败降级到本地DB
  395. """
  396. enterprise_id = (search or {}).get("enterprise_id", "")
  397. institution_name = (search or {}).get("name") or (search or {}).get("institution_name")
  398. if enterprise_id:
  399. return await cls.pageinfo_query_service(
  400. auth=auth,
  401. enterprise_id=enterprise_id,
  402. page_no=page_no,
  403. page_size=page_size,
  404. institution_name=institution_name,
  405. )
  406. # 无 enterprise_id 时直接查本地
  407. crud = InstitutionCRUD(auth)
  408. offset = (page_no - 1) * page_size
  409. return await crud.page(
  410. offset=offset,
  411. limit=page_size,
  412. order_by=[{"id": "desc"}],
  413. search=search or {},
  414. out_schema=InstitutionListOutSchema,
  415. )
  416. @classmethod
  417. async def delete_institution_service(
  418. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionDeleteModel
  419. ) -> dict:
  420. """
  421. 删除费控制度
  422. 调用: alipay.ebpp.invoice.institution.delete
  423. 支付宝侧已删时忽略错误,始终清理本地关联表
  424. """
  425. institution_id = getattr(data, 'institution_id', None)
  426. # 调用支付宝删除(失败时仅告警,不影响本地清理)
  427. try:
  428. request = AlipayEbppInvoiceInstitutionDeleteRequest()
  429. request.biz_model = data
  430. response = await asyncio.to_thread(cls._execute_alipay, request)
  431. if response:
  432. result = AlipayEbppInvoiceInstitutionDeleteResponse()
  433. result.parse_response_content(response)
  434. if result.is_success():
  435. log.info(f"支付宝删除成功: institution_id={institution_id}")
  436. else:
  437. log.warning(f"支付宝删除失败(可能已删): {result.code} - {result.msg}")
  438. else:
  439. log.warning("支付宝删除无响应,继续清理本地")
  440. except Exception as e:
  441. log.warning(f"支付宝删除异常(忽略): {e}")
  442. # 清理本地关联表
  443. if institution_id:
  444. try:
  445. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  446. from app.plugin.module_payment.expense.quota.model import QuotaModel
  447. from app.plugin.module_payment.expense.institution.model import ExpenseInstitutionModel
  448. from sqlalchemy import delete as sa_delete
  449. # 删规则
  450. await auth.db.execute(sa_delete(ExpenseRuleModel).where(ExpenseRuleModel.institution_id == institution_id))
  451. # 删额度
  452. await auth.db.execute(sa_delete(QuotaModel).where(QuotaModel.institution_id == institution_id))
  453. # 删制度
  454. await auth.db.execute(sa_delete(ExpenseInstitutionModel).where(ExpenseInstitutionModel.institution_id == institution_id))
  455. await auth.db.flush()
  456. log.info(f"本地关联数据已清理: institution_id={institution_id}")
  457. except Exception as e:
  458. log.warning(f"本地清理失败: {e}")
  459. return {"institution_id": institution_id, "deleted": True}
  460. @classmethod
  461. async def modify_institution_service(
  462. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionModifyModel, raw_data: dict | None = None,
  463. scope_info: dict | None = None,
  464. ) -> dict:
  465. """
  466. 编辑费控制度
  467. 调用: alipay.ebpp.invoice.institution.modify
  468. 适用范围修改(scope_info)需单独调 scope.modify,与基础信息拆分两次请求
  469. 支付宝成功后同步更新本地DB:
  470. - 制度基本信息
  471. - 适用员工范围(scope)
  472. - 使用规则(standard_info_list → pay_expense_rule)
  473. - 额度(issuerule → pay_expense_quota)
  474. """
  475. if data.institution_id is None:
  476. raise CustomException(msg="编辑费控制度失败: 制度ID不能为空")
  477. institution_id = data.institution_id
  478. enterprise_id = getattr(data, 'enterprise_id', None) or (raw_data or {}).get("enterprise_id", "")
  479. raw_data = raw_data or {}
  480. # 第1步:修改支付宝制度基础信息(不含 scope)
  481. request = AlipayEbppInvoiceInstitutionModifyRequest()
  482. request.biz_model = data
  483. response = await asyncio.to_thread(cls._execute_alipay, request)
  484. if not response:
  485. raise CustomException(msg="编辑费控制度失败: 无响应")
  486. result = AlipayEbppInvoiceInstitutionModifyResponse()
  487. result.parse_response_content(response)
  488. if not result.is_success():
  489. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  490. raise CustomException(msg=f"编辑费控制度失败: {result.msg}")
  491. # 第1.5步:单独调用 scope.modify(不与基础修改在同一请求中)
  492. if scope_info:
  493. try:
  494. await InstitutionScopeService.scope_modify_service(
  495. auth=auth, institution_id=institution_id, data=scope_info
  496. )
  497. log.info(f"适用范围已单独同步: adapter_type={scope_info.get('adapter_type')}")
  498. except Exception as e:
  499. log.warning(f"适用范围同步失败(不影响基础修改): {e}")
  500. raise CustomException(msg=f"编辑费控制度失败: 适用范围修改失败: {e}")
  501. applicable_scope = raw_data.get("applicable_scope", "")
  502. # 第2步:同步更新本地数据库(scope 已在 Alipay modify 请求中通过 modify_scope_info 处理)
  503. try:
  504. crud = InstitutionCRUD(auth)
  505. update_data = {}
  506. if hasattr(data, 'institution_name') and data.institution_name:
  507. update_data['institution_name'] = data.institution_name
  508. if hasattr(data, 'institution_desc') and data.institution_desc:
  509. update_data['institution_desc'] = data.institution_desc
  510. if hasattr(data, 'effective') and data.effective is not None:
  511. update_data['effective'] = data.effective
  512. update_data['status'] = (
  513. InstitutionStatusEnum.INSTITUTION_EFFECTIVE.value
  514. if data.effective == "1"
  515. else InstitutionStatusEnum.INSTITUTION_INVALID.value
  516. )
  517. if hasattr(data, 'effective_start_date') and data.effective_start_date:
  518. update_data['effective_start_date'] = data.effective_start_date
  519. if hasattr(data, 'effective_end_date') and data.effective_end_date:
  520. update_data['effective_end_date'] = data.effective_end_date
  521. if applicable_scope:
  522. update_data['applicable_scope'] = applicable_scope
  523. # 同步额外配置字段
  524. for field in ("grant_mode", "period_type", "amount", "single_limit", "effective_time_type", "expense_type"):
  525. val = raw_data.get(field)
  526. if val is not None:
  527. update_data[field] = val
  528. if update_data:
  529. await crud.update_by_institution_id(institution_id, update_data)
  530. log.info(f"已更新本地制度: institution_id={institution_id}")
  531. # 同步标准规则(modify_standard_detail_info)
  532. std_detail = (raw_data or {}).get("modify_standard_detail_info") or {}
  533. if std_detail:
  534. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  535. from sqlalchemy import delete as sa_delete
  536. # 删除规则
  537. delete_ids = std_detail.get("delete_standard_id_list", [])
  538. if delete_ids:
  539. d_stmt = sa_delete(ExpenseRuleModel).where(
  540. ExpenseRuleModel.rule_id.in_(delete_ids)
  541. )
  542. await auth.db.execute(d_stmt)
  543. # 新增规则
  544. add_list = std_detail.get("add_standard_list", [])
  545. from sqlalchemy import insert as sa_insert
  546. for std in add_list:
  547. ins_data = {
  548. "out_biz_no": std.get("outer_source_id", f"std_{institution_id}"),
  549. "institution_id": institution_id,
  550. "rule_id": std.get("standard_id"),
  551. "standard_name": std.get("standard_name"),
  552. "standard_desc": std.get("standard_desc"),
  553. "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
  554. "enterprise_id": raw_data.get("enterprise_id", ""),
  555. "tenant_id": auth.user.tenant_id if auth.user else 1,
  556. }
  557. stmt = sa_insert(ExpenseRuleModel).values(**ins_data)
  558. await auth.db.execute(stmt)
  559. # 修改规则
  560. modify_list = std_detail.get("modify_standard_list", [])
  561. for std in modify_list:
  562. std_id = std.get("standard_id", "").strip('"')
  563. update_std = {}
  564. if std.get("standard_name"):
  565. update_std["standard_name"] = std["standard_name"]
  566. if std.get("standard_desc"):
  567. update_std["standard_desc"] = std["standard_desc"]
  568. if update_std and std_id:
  569. from sqlalchemy import update as sa_update
  570. u_stmt = sa_update(ExpenseRuleModel).where(
  571. ExpenseRuleModel.rule_id == std_id
  572. ).values(**update_std)
  573. await auth.db.execute(u_stmt)
  574. await auth.db.flush()
  575. log.info(f"已同步使用规则: institution_id={institution_id}")
  576. # 同步发放规则(modify_issue_rule_detail_info)
  577. issue_detail = (raw_data or {}).get("modify_issue_rule_detail_info") or {}
  578. if issue_detail:
  579. from app.plugin.module_payment.expense.quota.model import QuotaModel
  580. from sqlalchemy import delete as sa_delete
  581. # 删除发放规则
  582. delete_ids = issue_detail.get("delete_issue_rule_id_list", [])
  583. if delete_ids:
  584. d_stmt = sa_delete(QuotaModel).where(
  585. QuotaModel.quota_id.in_(delete_ids)
  586. )
  587. await auth.db.execute(d_stmt)
  588. # 新增发放规则
  589. add_list = issue_detail.get("add_issue_rule_list", [])
  590. for rule in add_list:
  591. amount = float(rule.get("issue_amount_value", 0))
  592. ins_data = {
  593. "employee_id": "",
  594. "institution_id": institution_id,
  595. "out_biz_no": rule.get("outer_source_id", f"issue_{institution_id}"),
  596. "quota_id": rule.get("issue_rule_id"),
  597. "total_amount": amount,
  598. "available_amount": amount,
  599. "status": "QUOTA_ACTIVE",
  600. "enterprise_id": raw_data.get("enterprise_id", ""),
  601. "tenant_id": auth.user.tenant_id if auth.user else 1,
  602. }
  603. stmt = sa_insert(QuotaModel).values(**ins_data)
  604. await auth.db.execute(stmt)
  605. # 修改发放规则
  606. modify_rule = issue_detail.get("modify_issue_rule_list") or {}
  607. if modify_rule.get("issue_rule_id"):
  608. q_id = modify_rule["issue_rule_id"].strip('"')
  609. update_q = {}
  610. if modify_rule.get("issue_amount_value"):
  611. update_q["total_amount"] = float(modify_rule["issue_amount_value"])
  612. update_q["available_amount"] = float(modify_rule["issue_amount_value"])
  613. if modify_rule.get("issue_rule_name"):
  614. update_q["standard_name"] = modify_rule["issue_rule_name"]
  615. if update_q and q_id:
  616. from sqlalchemy import update as sa_update
  617. u_stmt = sa_update(QuotaModel).where(
  618. QuotaModel.quota_id == q_id
  619. ).values(**update_q)
  620. await auth.db.execute(u_stmt)
  621. await auth.db.flush()
  622. log.info(f"已同步发放规则: institution_id={institution_id}")
  623. except Exception as e:
  624. log.warning(f"本地同步失败(不影响支付宝侧): {e}")
  625. return result
  626. class InstitutionScopeService:
  627. """费控制度成员范围服务层"""
  628. @classmethod
  629. def _execute_alipay(cls, request):
  630. """同步执行支付宝调用"""
  631. client = AlipayClient.get_client()
  632. return client.execute(request)
  633. @classmethod
  634. async def scope_modify_service(
  635. cls,
  636. auth: AuthSchema,
  637. institution_id: str,
  638. data: dict,
  639. ) -> dict:
  640. """
  641. 设置/修改制度成员范围
  642. 调用: alipay.ebpp.invoice.institution.scope.modify
  643. """
  644. try:
  645. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopeModifyRequest import (
  646. AlipayEbppInvoiceInstitutionScopeModifyRequest,
  647. )
  648. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopeModifyModel import (
  649. AlipayEbppInvoiceInstitutionScopeModifyModel,
  650. )
  651. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopeModifyResponse import (
  652. AlipayEbppInvoiceInstitutionScopeModifyResponse,
  653. )
  654. except ImportError:
  655. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scope-modify)")
  656. model = AlipayEbppInvoiceInstitutionScopeModifyModel()
  657. model.institution_id = institution_id
  658. model.enterprise_id = data.get("enterprise_id", "")
  659. model.adapter_type = data.get("adapter_type", "EMPLOYEE_ALL")
  660. if data.get("owner_type"):
  661. model.owner_type = data["owner_type"]
  662. if data.get("add_owner_id_list"):
  663. model.add_owner_id_list = data["add_owner_id_list"]
  664. if data.get("delete_owner_id_list"):
  665. model.delete_owner_id_list = data["delete_owner_id_list"]
  666. request = AlipayEbppInvoiceInstitutionScopeModifyRequest()
  667. request.biz_model = model
  668. response = await asyncio.to_thread(cls._execute_alipay, request)
  669. if not response:
  670. raise CustomException(msg="设置制度成员失败: 无响应")
  671. result = AlipayEbppInvoiceInstitutionScopeModifyResponse()
  672. result.parse_response_content(response)
  673. if not result.is_success():
  674. log.error(f"设置制度成员失败: {result.code} - {result.msg}")
  675. raise CustomException(msg=f"设置制度成员失败: {result.msg}")
  676. return {"result": True}
  677. @classmethod
  678. async def scopepageinfo_query_service(
  679. cls,
  680. auth: AuthSchema,
  681. institution_id: str,
  682. enterprise_id: str | None = None,
  683. page_num: int = 1,
  684. page_size: int = 20,
  685. owner_type: str | None = None,
  686. ) -> dict:
  687. """
  688. 查询制度成员范围
  689. 调用: alipay.ebpp.invoice.institution.scopepageinfo.query
  690. """
  691. try:
  692. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest import (
  693. AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest,
  694. )
  695. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopepageinfoQueryModel import (
  696. AlipayEbppInvoiceInstitutionScopepageinfoQueryModel,
  697. )
  698. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse import (
  699. AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse,
  700. )
  701. except ImportError:
  702. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scopepageinfo-query)")
  703. model = AlipayEbppInvoiceInstitutionScopepageinfoQueryModel()
  704. model.institution_id = institution_id
  705. model.page_num = page_num
  706. model.page_size = page_size
  707. if enterprise_id:
  708. model.enterprise_id = enterprise_id
  709. if owner_type:
  710. model.owner_type = owner_type
  711. request = AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest()
  712. request.biz_model = model
  713. response = await asyncio.to_thread(cls._execute_alipay, request)
  714. if not response:
  715. raise CustomException(msg="查询制度成员失败: 无响应")
  716. result = AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse()
  717. result.parse_response_content(response)
  718. if not result.is_success():
  719. log.error(f"查询制度成员失败: {result.code} - {result.msg}")
  720. raise CustomException(msg=f"查询制度成员失败: {result.msg}")
  721. return {
  722. "page_num": getattr(result, 'page_num', page_num) or page_num,
  723. "page_size": getattr(result, 'page_size', page_size) or page_size,
  724. "total_page_count": getattr(result, 'total_page_count', 0) or 0,
  725. "adapter_type": getattr(result, 'adapter_type', None),
  726. "owner_id_list": getattr(result, 'owner_id_list', []) or [],
  727. "owner_open_id_list": getattr(result, 'onwer_open_id_list', []) or [],
  728. "scope_info_list": [
  729. {
  730. "adapter_type": getattr(result, 'adapter_type', None),
  731. "owner_id_list": getattr(result, 'owner_id_list', []) or [],
  732. "owner_open_id_list": getattr(result, 'onwer_open_id_list', []) or [],
  733. }
  734. ] if getattr(result, 'adapter_type', None) else [],
  735. }
  736. class IssueruleService:
  737. """自动额度发放规则服务层"""
  738. ISSUE_TYPE_MAP = {
  739. "daily": "ISSUE_DAY",
  740. "weekly": "ISSUE_WEEK",
  741. "monthly": "ISSUE_MONTH",
  742. "quarterly": "ISSUE_QUARTER",
  743. "yearly": "ISSUE_YEAR",
  744. }
  745. @classmethod
  746. def _execute_alipay(cls, request):
  747. client = AlipayClient.get_client()
  748. return client.execute(request)
  749. @classmethod
  750. async def create_issuerule_service(
  751. cls,
  752. auth: AuthSchema,
  753. institution_id: str,
  754. enterprise_id: str,
  755. quota_type: str,
  756. issue_type: str,
  757. issue_amount_value: str,
  758. outer_source_id: str | None = None,
  759. issue_rule_name: str | None = None,
  760. effective_period: str | None = None,
  761. invalid_mode: int | None = None,
  762. share_mode: int | None = None,
  763. ) -> dict:
  764. """
  765. 创建自动额度发放规则
  766. 调用: alipay.ebpp.invoice.issuerule.create
  767. """
  768. try:
  769. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleCreateRequest import (
  770. AlipayEbppInvoiceIssueruleCreateRequest,
  771. )
  772. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleCreateModel import (
  773. AlipayEbppInvoiceIssueruleCreateModel,
  774. )
  775. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleCreateResponse import (
  776. AlipayEbppInvoiceIssueruleCreateResponse,
  777. )
  778. except ImportError:
  779. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-create)")
  780. # 参数约束校验
  781. if quota_type == "CAP" and invalid_mode is not None and invalid_mode != 1:
  782. raise CustomException(msg="余额类型(CP)的发放规则必须为可累计(invalid_mode=1)")
  783. if quota_type == "COUNT" and share_mode is not None and share_mode != 0:
  784. raise CustomException(msg="次卡类型(COUNT)的发放规则不可转赠(share_mode=0)")
  785. model = AlipayEbppInvoiceIssueruleCreateModel()
  786. model.target_type = "INSTITUTION"
  787. model.target_id = institution_id
  788. model.quota_type = quota_type
  789. model.issue_type = issue_type
  790. model.issue_amount_value = issue_amount_value
  791. model.enterprise_id = enterprise_id
  792. if outer_source_id:
  793. model.outer_source_id = outer_source_id
  794. if issue_rule_name:
  795. model.issue_rule_name = issue_rule_name
  796. if effective_period:
  797. model.effective_period = effective_period
  798. if invalid_mode is not None:
  799. model.invalid_mode = invalid_mode
  800. if share_mode is not None:
  801. model.share_mode = share_mode
  802. request = AlipayEbppInvoiceIssueruleCreateRequest()
  803. request.biz_model = model
  804. response = await asyncio.to_thread(cls._execute_alipay, request)
  805. if not response:
  806. raise CustomException(msg="创建发放规则失败: 无响应")
  807. result = AlipayEbppInvoiceIssueruleCreateResponse()
  808. result.parse_response_content(response)
  809. if not result.is_success():
  810. log.error(f"创建发放规则失败: {result.code} - {result.msg}")
  811. raise CustomException(msg=f"创建发放规则失败: {result.msg}")
  812. return {
  813. "issue_rule_id": getattr(result, 'issue_rule_id', None),
  814. }
  815. @classmethod
  816. async def modify_issuerule_service(
  817. cls,
  818. auth: AuthSchema,
  819. institution_id: str,
  820. issue_rule_id: str,
  821. enterprise_id: str,
  822. quota_type: str | None = None,
  823. issue_type: str | None = None,
  824. issue_amount_value: str | None = None,
  825. issue_rule_name: str | None = None,
  826. effective: str | None = None,
  827. effective_period: str | None = None,
  828. invalid_mode: int | None = None,
  829. share_mode: int | None = None,
  830. ) -> dict:
  831. """
  832. 编辑自动额度发放规则
  833. 调用: alipay.ebpp.invoice.issuerule.modify
  834. """
  835. try:
  836. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleModifyRequest import (
  837. AlipayEbppInvoiceIssueruleModifyRequest,
  838. )
  839. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleModifyModel import (
  840. AlipayEbppInvoiceIssueruleModifyModel,
  841. )
  842. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleModifyResponse import (
  843. AlipayEbppInvoiceIssueruleModifyResponse,
  844. )
  845. except ImportError:
  846. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-modify)")
  847. model = AlipayEbppInvoiceIssueruleModifyModel()
  848. model.target_type = "INSTITUTION"
  849. model.target_id = institution_id
  850. model.issue_rule_id = issue_rule_id
  851. model.action = "MODIFY_BASIC_INFO"
  852. model.enterprise_id = enterprise_id
  853. if issue_rule_name:
  854. model.issue_rule_name = issue_rule_name
  855. if quota_type:
  856. model.quota_type = quota_type
  857. if issue_type:
  858. model.issue_type = issue_type
  859. if issue_amount_value:
  860. model.issue_amount_value = issue_amount_value
  861. if effective is not None:
  862. model.effective = effective
  863. if effective_period:
  864. model.effective_period = effective_period
  865. if invalid_mode is not None:
  866. model.invalid_mode = invalid_mode
  867. if share_mode is not None:
  868. model.share_mode = share_mode
  869. request = AlipayEbppInvoiceIssueruleModifyRequest()
  870. request.biz_model = model
  871. response = await asyncio.to_thread(cls._execute_alipay, request)
  872. if not response:
  873. raise CustomException(msg="编辑发放规则失败: 无响应")
  874. result = AlipayEbppInvoiceIssueruleModifyResponse()
  875. result.parse_response_content(response)
  876. if not result.is_success():
  877. log.error(f"编辑发放规则失败: {result.code} - {result.msg}")
  878. raise CustomException(msg=f"编辑发放规则失败: {result.msg}")
  879. return {"result": True}
  880. @classmethod
  881. async def delete_issuerule_service(
  882. cls,
  883. auth: AuthSchema,
  884. institution_id: str,
  885. issue_rule_id_list: list[str],
  886. enterprise_id: str,
  887. ) -> dict:
  888. """
  889. 删除自动额度发放规则
  890. 调用: alipay.ebpp.invoice.issuerule.delete
  891. """
  892. try:
  893. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleDeleteRequest import (
  894. AlipayEbppInvoiceIssueruleDeleteRequest,
  895. )
  896. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleDeleteModel import (
  897. AlipayEbppInvoiceIssueruleDeleteModel,
  898. )
  899. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleDeleteResponse import (
  900. AlipayEbppInvoiceIssueruleDeleteResponse,
  901. )
  902. except ImportError:
  903. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-delete)")
  904. model = AlipayEbppInvoiceIssueruleDeleteModel()
  905. model.target_type = "INSTITUTION"
  906. model.target_id = institution_id
  907. model.issue_rule_id_list = issue_rule_id_list
  908. model.enterprise_id = enterprise_id
  909. request = AlipayEbppInvoiceIssueruleDeleteRequest()
  910. request.biz_model = model
  911. response = await asyncio.to_thread(cls._execute_alipay, request)
  912. if not response:
  913. raise CustomException(msg="删除发放规则失败: 无响应")
  914. result = AlipayEbppInvoiceIssueruleDeleteResponse()
  915. result.parse_response_content(response)
  916. if not result.is_success():
  917. log.error(f"删除发放规则失败: {result.code} - {result.msg}")
  918. raise CustomException(msg=f"删除发放规则失败: {result.msg}")
  919. return {"result": True}