service.py 55 KB

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