service.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  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. )
  175. create_data_dict = create_data.model_dump(exclude_unset=True)
  176. crud = InstitutionCRUD(auth)
  177. await crud.create(create_data_dict)
  178. # 第5步:保存使用规则到本地
  179. 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:
  180. from app.plugin.module_payment.expense.rule.crud import RuleCRUD
  181. from app.plugin.module_payment.expense.rule.service import RuleService
  182. standard_id_map = {}
  183. for info in institution_result.standard_id_info_list:
  184. if hasattr(info, 'outer_source_id') and hasattr(info, 'standard_id'):
  185. standard_id_map[info.outer_source_id] = info.standard_id
  186. for idx, std in enumerate(raw_data["standard_info_list"]):
  187. std_data = {
  188. "out_biz_no": std.get("outer_source_id", f"std_{institution_id}_{idx}"),
  189. "institution_id": institution_id,
  190. "rule_id": standard_id_map.get(std.get("outer_source_id", "")),
  191. "standard_name": std.get("standard_name"),
  192. "standard_desc": std.get("standard_desc"),
  193. "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
  194. "enterprise_id": enterprise_id,
  195. "tenant_id": auth.user.tenant_id if auth.user else 1,
  196. }
  197. try:
  198. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  199. from sqlalchemy import insert
  200. stmt = insert(ExpenseRuleModel).values(**std_data)
  201. await auth.db.execute(stmt)
  202. await auth.db.flush()
  203. except Exception as e:
  204. log.warning(f"保存使用规则到本地失败: {e}")
  205. # 第6步:保存发放规则到本地
  206. if issuerule_data and issue_rule_id:
  207. from app.plugin.module_payment.expense.quota.model import QuotaModel
  208. quota_save_data = {
  209. "employee_id": "",
  210. "institution_id": institution_id,
  211. "out_biz_no": issuerule_data.get("outer_source_id", f"issue_{institution_id}"),
  212. "quota_id": issue_rule_id,
  213. "total_amount": float(issuerule_data.get("issue_amount_value", 0)),
  214. "available_amount": float(issuerule_data.get("issue_amount_value", 0)),
  215. "status": "QUOTA_ACTIVE",
  216. "enterprise_id": enterprise_id,
  217. "tenant_id": auth.user.tenant_id if auth.user else 1,
  218. }
  219. try:
  220. from sqlalchemy import insert
  221. stmt = insert(QuotaModel).values(**quota_save_data)
  222. await auth.db.execute(stmt)
  223. await auth.db.flush()
  224. except Exception as e:
  225. log.warning(f"保存发放规则到本地失败: {e}")
  226. return {
  227. "institution_id": institution_id,
  228. "scope_modified": scope_modified,
  229. "issue_rule_id": issue_rule_id,
  230. }
  231. @classmethod
  232. async def pageinfo_query_service(
  233. cls,
  234. auth: AuthSchema,
  235. enterprise_id: str,
  236. page_no: int = 1,
  237. page_size: int = 20,
  238. institution_name: str | None = None,
  239. ) -> dict:
  240. """
  241. 从支付宝查询费控制度列表
  242. 调用: alipay.ebpp.invoice.institution.pageinfo.query
  243. 失败时降级到本地DB
  244. """
  245. try:
  246. model = AlipayEbppInvoiceInstitutionPageinfoQueryModel()
  247. model.enterprise_id = enterprise_id
  248. model.page_num = page_no
  249. model.page_size = page_size
  250. if institution_name:
  251. model.institution_name = institution_name
  252. req = AlipayEbppInvoiceInstitutionPageinfoQueryRequest()
  253. req.biz_model = model
  254. response = await asyncio.to_thread(cls._execute_alipay, req)
  255. if response:
  256. result = AlipayEbppInvoiceInstitutionPageinfoQueryResponse()
  257. result.parse_response_content(response)
  258. if result.is_success():
  259. return {
  260. "page_no": getattr(result, 'page_num', page_no) or page_no,
  261. "page_size": getattr(result, 'page_size', page_size) or page_size,
  262. "total": getattr(result, 'total_page_count', 0) or 0,
  263. "list": getattr(result, 'institution_list', []) or [],
  264. }
  265. log.warning("支付宝 pageinfo.query 失败,降级到本地DB")
  266. except Exception as e:
  267. log.warning(f"支付宝 pageinfo.query 异常: {e},降级到本地DB")
  268. # 降级:查本地DB
  269. crud = InstitutionCRUD(auth)
  270. search = {"enterprise_id": enterprise_id}
  271. if institution_name:
  272. search["institution_name"] = institution_name
  273. offset = (page_no - 1) * page_size
  274. return await crud.page(
  275. offset=offset,
  276. limit=page_size,
  277. order_by=[{"id": "desc"}],
  278. search=search,
  279. out_schema=InstitutionListOutSchema,
  280. )
  281. @classmethod
  282. async def detailinfo_query_service(
  283. cls,
  284. auth: AuthSchema,
  285. institution_id: str,
  286. enterprise_id: str,
  287. ) -> dict | None:
  288. """
  289. 从支付宝查询费控制度详情
  290. 调用: alipay.ebpp.invoice.institution.detailinfo.query
  291. 失败时降级到本地DB
  292. """
  293. try:
  294. model = AlipayEbppInvoiceInstitutionDetailinfoQueryModel()
  295. model.institution_id = institution_id
  296. model.enterprise_id = enterprise_id
  297. req = AlipayEbppInvoiceInstitutionDetailinfoQueryRequest()
  298. req.biz_model = model
  299. response = await asyncio.to_thread(cls._execute_alipay, req)
  300. if response:
  301. result = AlipayEbppInvoiceInstitutionDetailinfoQueryResponse()
  302. result.parse_response_content(response)
  303. if result.is_success():
  304. return result.to_alipay_dict()
  305. log.warning("支付宝 detailinfo.query 失败,降级到本地DB")
  306. except Exception as e:
  307. log.warning(f"支付宝 detailinfo.query 异常: {e},降级到本地DB")
  308. # 降级:查本地DB
  309. crud = InstitutionCRUD(auth)
  310. obj = await crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
  311. if obj:
  312. return InstitutionListOutSchema.model_validate(obj).model_dump()
  313. return None
  314. @classmethod
  315. async def list_service(
  316. cls,
  317. auth: AuthSchema,
  318. page_no: int = 1,
  319. page_size: int = 20,
  320. search: dict | None = None,
  321. ) -> dict:
  322. """
  323. 查询费控制度列表
  324. 优先调支付宝,失败降级到本地DB
  325. """
  326. enterprise_id = (search or {}).get("enterprise_id", "")
  327. institution_name = (search or {}).get("name") or (search or {}).get("institution_name")
  328. if enterprise_id:
  329. return await cls.pageinfo_query_service(
  330. auth=auth,
  331. enterprise_id=enterprise_id,
  332. page_no=page_no,
  333. page_size=page_size,
  334. institution_name=institution_name,
  335. )
  336. # 无 enterprise_id 时直接查本地
  337. crud = InstitutionCRUD(auth)
  338. offset = (page_no - 1) * page_size
  339. return await crud.page(
  340. offset=offset,
  341. limit=page_size,
  342. order_by=[{"id": "desc"}],
  343. search=search or {},
  344. out_schema=InstitutionListOutSchema,
  345. )
  346. @classmethod
  347. async def delete_institution_service(
  348. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionDeleteModel
  349. ) -> AlipayEbppInvoiceInstitutionDeleteResponse:
  350. """
  351. 删除费控制度
  352. 调用: alipay.ebpp.invoice.institution.delete
  353. """
  354. request = AlipayEbppInvoiceInstitutionDeleteRequest()
  355. request.biz_model = data
  356. response = await asyncio.to_thread(cls._execute_alipay, request)
  357. if not response:
  358. raise CustomException(msg="删除费控制度失败: 无响应")
  359. result = AlipayEbppInvoiceInstitutionDeleteResponse()
  360. result.parse_response_content(response)
  361. if not result.is_success():
  362. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  363. raise CustomException(msg=f"删除费控制度失败: {result.msg}")
  364. # 同步删除本地记录
  365. try:
  366. crud = InstitutionCRUD(auth)
  367. institution_id = getattr(data, 'institution_id', None)
  368. if institution_id:
  369. obj = await crud.get(institution_id=institution_id)
  370. if obj:
  371. await crud.delete(ids=[obj.id])
  372. log.info(f"已删除本地记录: institution_id={institution_id}")
  373. except Exception as e:
  374. log.warning(f"删除本地记录失败(不影响支付宝侧): {e}")
  375. return result
  376. @classmethod
  377. async def modify_institution_service(
  378. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionModifyModel, raw_data: dict | None = None
  379. ) -> AlipayEbppInvoiceInstitutionModifyResponse:
  380. """
  381. 编辑费控制度
  382. 调用: alipay.ebpp.invoice.institution.modify
  383. 支付宝成功后同步更新本地DB:
  384. - 制度基本信息
  385. - 使用规则(standard_info_list → pay_expense_rule)
  386. - 额度(issuerule → pay_expense_quota)
  387. """
  388. if data.institution_id is None:
  389. raise CustomException(msg="编辑费控制度失败: 制度ID不能为空")
  390. request = AlipayEbppInvoiceInstitutionModifyRequest()
  391. request.biz_model = data
  392. response = await asyncio.to_thread(cls._execute_alipay, request)
  393. if not response:
  394. raise CustomException(msg="编辑费控制度失败: 无响应")
  395. result = AlipayEbppInvoiceInstitutionModifyResponse()
  396. result.parse_response_content(response)
  397. if not result.is_success():
  398. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  399. raise CustomException(msg=f"编辑费控制度失败: {result.msg}")
  400. # 同步更新本地数据库
  401. institution_id = getattr(data, 'institution_id', None)
  402. if not institution_id:
  403. return result
  404. try:
  405. crud = InstitutionCRUD(auth)
  406. update_data = {}
  407. if hasattr(data, 'institution_name') and data.institution_name:
  408. update_data['institution_name'] = data.institution_name
  409. if hasattr(data, 'institution_desc') and data.institution_desc:
  410. update_data['institution_desc'] = data.institution_desc
  411. if hasattr(data, 'effective') and data.effective is not None:
  412. update_data['effective'] = data.effective
  413. update_data['status'] = (
  414. InstitutionStatusEnum.INSTITUTION_EFFECTIVE.value
  415. if data.effective == "1"
  416. else InstitutionStatusEnum.INSTITUTION_INVALID.value
  417. )
  418. if hasattr(data, 'effective_start_date') and data.effective_start_date:
  419. update_data['effective_start_date'] = data.effective_start_date
  420. if hasattr(data, 'effective_end_date') and data.effective_end_date:
  421. update_data['effective_end_date'] = data.effective_end_date
  422. if update_data:
  423. await crud.update_by_institution_id(institution_id, update_data)
  424. log.info(f"已更新本地制度: institution_id={institution_id}")
  425. # 同步标准规则(modify_standard_detail_info)
  426. std_detail = (raw_data or {}).get("modify_standard_detail_info") or {}
  427. if std_detail:
  428. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  429. from sqlalchemy import delete as sa_delete
  430. # 删除规则
  431. delete_ids = std_detail.get("delete_standard_id_list", [])
  432. if delete_ids:
  433. d_stmt = sa_delete(ExpenseRuleModel).where(
  434. ExpenseRuleModel.rule_id.in_(delete_ids)
  435. )
  436. await auth.db.execute(d_stmt)
  437. # 新增规则
  438. add_list = std_detail.get("add_standard_list", [])
  439. from sqlalchemy import insert as sa_insert
  440. for std in add_list:
  441. ins_data = {
  442. "out_biz_no": std.get("outer_source_id", f"std_{institution_id}"),
  443. "institution_id": institution_id,
  444. "rule_id": std.get("standard_id"),
  445. "standard_name": std.get("standard_name"),
  446. "standard_desc": std.get("standard_desc"),
  447. "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
  448. "enterprise_id": raw_data.get("enterprise_id", ""),
  449. "tenant_id": auth.user.tenant_id if auth.user else 1,
  450. }
  451. stmt = sa_insert(ExpenseRuleModel).values(**ins_data)
  452. await auth.db.execute(stmt)
  453. # 修改规则
  454. modify_list = std_detail.get("modify_standard_list", [])
  455. for std in modify_list:
  456. std_id = std.get("standard_id", "").strip('"')
  457. update_std = {}
  458. if std.get("standard_name"):
  459. update_std["standard_name"] = std["standard_name"]
  460. if std.get("standard_desc"):
  461. update_std["standard_desc"] = std["standard_desc"]
  462. if update_std and std_id:
  463. from sqlalchemy import update as sa_update
  464. u_stmt = sa_update(ExpenseRuleModel).where(
  465. ExpenseRuleModel.rule_id == std_id
  466. ).values(**update_std)
  467. await auth.db.execute(u_stmt)
  468. await auth.db.flush()
  469. log.info(f"已同步使用规则: institution_id={institution_id}")
  470. # 同步发放规则(modify_issue_rule_detail_info)
  471. issue_detail = (raw_data or {}).get("modify_issue_rule_detail_info") or {}
  472. if issue_detail:
  473. from app.plugin.module_payment.expense.quota.model import QuotaModel
  474. from sqlalchemy import delete as sa_delete
  475. # 删除发放规则
  476. delete_ids = issue_detail.get("delete_issue_rule_id_list", [])
  477. if delete_ids:
  478. d_stmt = sa_delete(QuotaModel).where(
  479. QuotaModel.quota_id.in_(delete_ids)
  480. )
  481. await auth.db.execute(d_stmt)
  482. # 新增发放规则
  483. add_list = issue_detail.get("add_issue_rule_list", [])
  484. for rule in add_list:
  485. amount = float(rule.get("issue_amount_value", 0))
  486. ins_data = {
  487. "employee_id": "",
  488. "institution_id": institution_id,
  489. "out_biz_no": rule.get("outer_source_id", f"issue_{institution_id}"),
  490. "quota_id": rule.get("issue_rule_id"),
  491. "total_amount": amount,
  492. "available_amount": amount,
  493. "status": "QUOTA_ACTIVE",
  494. "enterprise_id": raw_data.get("enterprise_id", ""),
  495. "tenant_id": auth.user.tenant_id if auth.user else 1,
  496. }
  497. stmt = sa_insert(QuotaModel).values(**ins_data)
  498. await auth.db.execute(stmt)
  499. # 修改发放规则
  500. modify_rule = issue_detail.get("modify_issue_rule_list") or {}
  501. if modify_rule.get("issue_rule_id"):
  502. q_id = modify_rule["issue_rule_id"].strip('"')
  503. update_q = {}
  504. if modify_rule.get("issue_amount_value"):
  505. update_q["total_amount"] = float(modify_rule["issue_amount_value"])
  506. update_q["available_amount"] = float(modify_rule["issue_amount_value"])
  507. if modify_rule.get("issue_rule_name"):
  508. update_q["standard_name"] = modify_rule["issue_rule_name"]
  509. if update_q and q_id:
  510. from sqlalchemy import update as sa_update
  511. u_stmt = sa_update(QuotaModel).where(
  512. QuotaModel.quota_id == q_id
  513. ).values(**update_q)
  514. await auth.db.execute(u_stmt)
  515. await auth.db.flush()
  516. log.info(f"已同步发放规则: institution_id={institution_id}")
  517. except Exception as e:
  518. log.warning(f"本地同步失败(不影响支付宝侧): {e}")
  519. return result
  520. class InstitutionScopeService:
  521. """费控制度成员范围服务层"""
  522. @classmethod
  523. def _execute_alipay(cls, request):
  524. """同步执行支付宝调用"""
  525. client = AlipayClient.get_client()
  526. return client.execute(request)
  527. @classmethod
  528. async def scope_modify_service(
  529. cls,
  530. auth: AuthSchema,
  531. institution_id: str,
  532. data: dict,
  533. ) -> dict:
  534. """
  535. 设置/修改制度成员范围
  536. 调用: alipay.ebpp.invoice.institution.scope.modify
  537. """
  538. try:
  539. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopeModifyRequest import (
  540. AlipayEbppInvoiceInstitutionScopeModifyRequest,
  541. )
  542. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopeModifyModel import (
  543. AlipayEbppInvoiceInstitutionScopeModifyModel,
  544. )
  545. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopeModifyResponse import (
  546. AlipayEbppInvoiceInstitutionScopeModifyResponse,
  547. )
  548. except ImportError:
  549. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scope-modify)")
  550. model = AlipayEbppInvoiceInstitutionScopeModifyModel()
  551. model.institution_id = institution_id
  552. model.enterprise_id = data.get("enterprise_id", "")
  553. model.adapter_type = data.get("adapter_type", "EMPLOYEE_ALL")
  554. if data.get("owner_type"):
  555. model.owner_type = data["owner_type"]
  556. if data.get("add_owner_id_list"):
  557. model.add_owner_id_list = data["add_owner_id_list"]
  558. if data.get("delete_owner_id_list"):
  559. model.delete_owner_id_list = data["delete_owner_id_list"]
  560. request = AlipayEbppInvoiceInstitutionScopeModifyRequest()
  561. request.biz_model = model
  562. response = await asyncio.to_thread(cls._execute_alipay, request)
  563. if not response:
  564. raise CustomException(msg="设置制度成员失败: 无响应")
  565. result = AlipayEbppInvoiceInstitutionScopeModifyResponse()
  566. result.parse_response_content(response)
  567. if not result.is_success():
  568. log.error(f"设置制度成员失败: {result.code} - {result.msg}")
  569. raise CustomException(msg=f"设置制度成员失败: {result.msg}")
  570. return {"result": True}
  571. @classmethod
  572. async def scopepageinfo_query_service(
  573. cls,
  574. auth: AuthSchema,
  575. institution_id: str,
  576. enterprise_id: str | None = None,
  577. page_num: int = 1,
  578. page_size: int = 20,
  579. owner_type: str | None = None,
  580. ) -> dict:
  581. """
  582. 查询制度成员范围
  583. 调用: alipay.ebpp.invoice.institution.scopepageinfo.query
  584. """
  585. try:
  586. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest import (
  587. AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest,
  588. )
  589. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopepageinfoQueryModel import (
  590. AlipayEbppInvoiceInstitutionScopepageinfoQueryModel,
  591. )
  592. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse import (
  593. AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse,
  594. )
  595. except ImportError:
  596. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scopepageinfo-query)")
  597. model = AlipayEbppInvoiceInstitutionScopepageinfoQueryModel()
  598. model.institution_id = institution_id
  599. model.page_num = page_num
  600. model.page_size = page_size
  601. if enterprise_id:
  602. model.enterprise_id = enterprise_id
  603. if owner_type:
  604. model.owner_type = owner_type
  605. request = AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest()
  606. request.biz_model = model
  607. response = await asyncio.to_thread(cls._execute_alipay, request)
  608. if not response:
  609. raise CustomException(msg="查询制度成员失败: 无响应")
  610. result = AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse()
  611. result.parse_response_content(response)
  612. if not result.is_success():
  613. log.error(f"查询制度成员失败: {result.code} - {result.msg}")
  614. raise CustomException(msg=f"查询制度成员失败: {result.msg}")
  615. return {
  616. "page_num": getattr(result, 'page_num', page_num) or page_num,
  617. "page_size": getattr(result, 'page_size', page_size) or page_size,
  618. "total_page_count": getattr(result, 'total_page_count', 0) or 0,
  619. "adapter_type": getattr(result, 'adapter_type', None),
  620. "owner_id_list": getattr(result, 'owner_id_list', []) or [],
  621. "owner_open_id_list": getattr(result, 'onwer_open_id_list', []) or [],
  622. "scope_info_list": [
  623. {
  624. "adapter_type": getattr(result, 'adapter_type', None),
  625. "owner_id_list": getattr(result, 'owner_id_list', []) or [],
  626. "owner_open_id_list": getattr(result, 'onwer_open_id_list', []) or [],
  627. }
  628. ] if getattr(result, 'adapter_type', None) else [],
  629. }
  630. class IssueruleService:
  631. """自动额度发放规则服务层"""
  632. ISSUE_TYPE_MAP = {
  633. "daily": "ISSUE_DAY",
  634. "weekly": "ISSUE_WEEK",
  635. "monthly": "ISSUE_MONTH",
  636. "quarterly": "ISSUE_QUARTER",
  637. "yearly": "ISSUE_YEAR",
  638. }
  639. @classmethod
  640. def _execute_alipay(cls, request):
  641. client = AlipayClient.get_client()
  642. return client.execute(request)
  643. @classmethod
  644. async def create_issuerule_service(
  645. cls,
  646. auth: AuthSchema,
  647. institution_id: str,
  648. enterprise_id: str,
  649. quota_type: str,
  650. issue_type: str,
  651. issue_amount_value: str,
  652. outer_source_id: str | None = None,
  653. issue_rule_name: str | None = None,
  654. effective_period: str | None = None,
  655. invalid_mode: int | None = None,
  656. share_mode: int | None = None,
  657. ) -> dict:
  658. """
  659. 创建自动额度发放规则
  660. 调用: alipay.ebpp.invoice.issuerule.create
  661. """
  662. try:
  663. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleCreateRequest import (
  664. AlipayEbppInvoiceIssueruleCreateRequest,
  665. )
  666. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleCreateModel import (
  667. AlipayEbppInvoiceIssueruleCreateModel,
  668. )
  669. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleCreateResponse import (
  670. AlipayEbppInvoiceIssueruleCreateResponse,
  671. )
  672. except ImportError:
  673. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-create)")
  674. # 参数约束校验
  675. if quota_type == "CAP" and invalid_mode is not None and invalid_mode != 1:
  676. raise CustomException(msg="余额类型(CP)的发放规则必须为可累计(invalid_mode=1)")
  677. if quota_type == "COUNT" and share_mode is not None and share_mode != 0:
  678. raise CustomException(msg="次卡类型(COUNT)的发放规则不可转赠(share_mode=0)")
  679. model = AlipayEbppInvoiceIssueruleCreateModel()
  680. model.target_type = "INSTITUTION"
  681. model.target_id = institution_id
  682. model.quota_type = quota_type
  683. model.issue_type = issue_type
  684. model.issue_amount_value = issue_amount_value
  685. model.enterprise_id = enterprise_id
  686. if outer_source_id:
  687. model.outer_source_id = outer_source_id
  688. if issue_rule_name:
  689. model.issue_rule_name = issue_rule_name
  690. if effective_period:
  691. model.effective_period = effective_period
  692. if invalid_mode is not None:
  693. model.invalid_mode = invalid_mode
  694. if share_mode is not None:
  695. model.share_mode = share_mode
  696. request = AlipayEbppInvoiceIssueruleCreateRequest()
  697. request.biz_model = model
  698. response = await asyncio.to_thread(cls._execute_alipay, request)
  699. if not response:
  700. raise CustomException(msg="创建发放规则失败: 无响应")
  701. result = AlipayEbppInvoiceIssueruleCreateResponse()
  702. result.parse_response_content(response)
  703. if not result.is_success():
  704. log.error(f"创建发放规则失败: {result.code} - {result.msg}")
  705. raise CustomException(msg=f"创建发放规则失败: {result.msg}")
  706. return {
  707. "issue_rule_id": getattr(result, 'issue_rule_id', None),
  708. }
  709. @classmethod
  710. async def modify_issuerule_service(
  711. cls,
  712. auth: AuthSchema,
  713. institution_id: str,
  714. issue_rule_id: str,
  715. enterprise_id: str,
  716. quota_type: str | None = None,
  717. issue_type: str | None = None,
  718. issue_amount_value: str | None = None,
  719. issue_rule_name: str | None = None,
  720. effective: str | None = None,
  721. effective_period: str | None = None,
  722. invalid_mode: int | None = None,
  723. share_mode: int | None = None,
  724. ) -> dict:
  725. """
  726. 编辑自动额度发放规则
  727. 调用: alipay.ebpp.invoice.issuerule.modify
  728. """
  729. try:
  730. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleModifyRequest import (
  731. AlipayEbppInvoiceIssueruleModifyRequest,
  732. )
  733. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleModifyModel import (
  734. AlipayEbppInvoiceIssueruleModifyModel,
  735. )
  736. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleModifyResponse import (
  737. AlipayEbppInvoiceIssueruleModifyResponse,
  738. )
  739. except ImportError:
  740. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-modify)")
  741. model = AlipayEbppInvoiceIssueruleModifyModel()
  742. model.target_type = "INSTITUTION"
  743. model.target_id = institution_id
  744. model.issue_rule_id = issue_rule_id
  745. model.action = "MODIFY_BASIC_INFO"
  746. model.enterprise_id = enterprise_id
  747. if issue_rule_name:
  748. model.issue_rule_name = issue_rule_name
  749. if quota_type:
  750. model.quota_type = quota_type
  751. if issue_type:
  752. model.issue_type = issue_type
  753. if issue_amount_value:
  754. model.issue_amount_value = issue_amount_value
  755. if effective is not None:
  756. model.effective = effective
  757. if effective_period:
  758. model.effective_period = effective_period
  759. if invalid_mode is not None:
  760. model.invalid_mode = invalid_mode
  761. if share_mode is not None:
  762. model.share_mode = share_mode
  763. request = AlipayEbppInvoiceIssueruleModifyRequest()
  764. request.biz_model = model
  765. response = await asyncio.to_thread(cls._execute_alipay, request)
  766. if not response:
  767. raise CustomException(msg="编辑发放规则失败: 无响应")
  768. result = AlipayEbppInvoiceIssueruleModifyResponse()
  769. result.parse_response_content(response)
  770. if not result.is_success():
  771. log.error(f"编辑发放规则失败: {result.code} - {result.msg}")
  772. raise CustomException(msg=f"编辑发放规则失败: {result.msg}")
  773. return {"result": True}
  774. @classmethod
  775. async def delete_issuerule_service(
  776. cls,
  777. auth: AuthSchema,
  778. institution_id: str,
  779. issue_rule_id_list: list[str],
  780. enterprise_id: str,
  781. ) -> dict:
  782. """
  783. 删除自动额度发放规则
  784. 调用: alipay.ebpp.invoice.issuerule.delete
  785. """
  786. try:
  787. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleDeleteRequest import (
  788. AlipayEbppInvoiceIssueruleDeleteRequest,
  789. )
  790. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleDeleteModel import (
  791. AlipayEbppInvoiceIssueruleDeleteModel,
  792. )
  793. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleDeleteResponse import (
  794. AlipayEbppInvoiceIssueruleDeleteResponse,
  795. )
  796. except ImportError:
  797. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-delete)")
  798. model = AlipayEbppInvoiceIssueruleDeleteModel()
  799. model.target_type = "INSTITUTION"
  800. model.target_id = institution_id
  801. model.issue_rule_id_list = issue_rule_id_list
  802. model.enterprise_id = enterprise_id
  803. request = AlipayEbppInvoiceIssueruleDeleteRequest()
  804. request.biz_model = model
  805. response = await asyncio.to_thread(cls._execute_alipay, request)
  806. if not response:
  807. raise CustomException(msg="删除发放规则失败: 无响应")
  808. result = AlipayEbppInvoiceIssueruleDeleteResponse()
  809. result.parse_response_content(response)
  810. if not result.is_success():
  811. log.error(f"删除发放规则失败: {result.code} - {result.msg}")
  812. raise CustomException(msg=f"删除发放规则失败: {result.msg}")
  813. return {"result": True}