service.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336
  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. issue_rule_id=issue_rule_id,
  233. )
  234. except Exception as e:
  235. log.warning(f"创建员工额度记录失败(不影响支付宝侧): {e}")
  236. return {
  237. "institution_id": institution_id,
  238. "scope_modified": scope_modified,
  239. "issue_rule_id": issue_rule_id,
  240. }
  241. @classmethod
  242. async def _create_institution_quotas(
  243. cls,
  244. auth: AuthSchema,
  245. institution_id: str,
  246. enterprise_id: str,
  247. scope_data: dict,
  248. raw_data: dict | None = None,
  249. issue_rule_id: str | None = None,
  250. ):
  251. """按适用范围创建员工额度记录
  252. 规则:
  253. - 定额发放 + 制度在有效期内:total=定额值, available=定额值, ACTIVE
  254. - 定额发放 + 制度未生效/已过期:total=0, available=0, PENDING
  255. - 手工发放:total=0, available=0, PENDING
  256. """
  257. from app.plugin.module_payment.expense.quota.model import QuotaModel
  258. from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
  259. from sqlalchemy import insert, select
  260. grant_mode = (raw_data or {}).get("grant_mode", "manual")
  261. amount_val = float((raw_data or {}).get("amount", 0) or 0)
  262. tenant_id = auth.user.tenant_id if auth.user else 1
  263. # 判断制度是否在有效期内
  264. now = datetime.now()
  265. is_in_period = True
  266. try:
  267. start_str = (raw_data or {}).get("effective_start_date")
  268. end_str = (raw_data or {}).get("effective_end_date")
  269. if start_str:
  270. start_dt = datetime.fromisoformat(str(start_str).replace('Z', '').replace('T', ' ')[:19])
  271. if now < start_dt:
  272. is_in_period = False
  273. if end_str and is_in_period:
  274. end_dt = datetime.fromisoformat(str(end_str).replace('Z', '').replace('T', ' ')[:19])
  275. if now > end_dt:
  276. is_in_period = False
  277. except Exception:
  278. is_in_period = True
  279. # 定额+有效期内 → ACTIVE,否则 → PENDING
  280. is_active = (grant_mode == "period") and amount_val > 0 and is_in_period
  281. # 收集员工ID列表
  282. employee_ids: list[str] = []
  283. adapter_type = scope_data.get("adapter_type", "")
  284. add_ids = scope_data.get("add_owner_id_list") or []
  285. if adapter_type == "EMPLOYEE_SELECT":
  286. # 按员工选择 → 直接使用传入的员工ID
  287. employee_ids = [str(i) for i in add_ids if i]
  288. elif adapter_type in ("EMPLOYEE_DEPARTMENT", "DEPARTMENT_SELECT"):
  289. # 按部门 → 查该部门下的所有员工
  290. for dept_id in add_ids:
  291. dept_id_str = str(dept_id)
  292. from app.plugin.module_payment.employee.model import EmployeeModel
  293. emp_stmt = select(EmployeeModel).where(
  294. EmployeeModel.enterprise_id == enterprise_id,
  295. EmployeeModel.status == "EMPLOYEE_ACTIVATED",
  296. )
  297. emp_result = await auth.db.execute(emp_stmt)
  298. for emp in emp_result.scalars().all():
  299. if emp.department_ids and dept_id_str in emp.department_ids:
  300. employee_ids.append(emp.employee_id)
  301. elif adapter_type == "EMPLOYEE_ALL":
  302. # 全部员工
  303. from app.plugin.module_payment.employee.model import EmployeeModel
  304. emp_stmt = select(EmployeeModel).where(
  305. EmployeeModel.enterprise_id == enterprise_id,
  306. EmployeeModel.status == "EMPLOYEE_ACTIVATED",
  307. )
  308. emp_result = await auth.db.execute(emp_stmt)
  309. employee_ids = [emp.employee_id for emp in emp_result.scalars().all() if emp.employee_id]
  310. # 去重
  311. employee_ids = list(set(employee_ids))
  312. if not employee_ids:
  313. log.info(f"无员工需要创建额度记录: institution_id={institution_id}")
  314. return
  315. now = datetime.now()
  316. if is_active:
  317. # 定额+有效期内:直接赋值
  318. total = Decimal(str(amount_val))
  319. available = total
  320. status = QuotaStatusEnum.QUOTA_ACTIVE.value
  321. else:
  322. # 手工发放 或 定额但未到有效期:待发放
  323. total = Decimal("0")
  324. available = Decimal("0")
  325. status = QuotaStatusEnum.QUOTA_PENDING.value
  326. for emp_id in employee_ids:
  327. # 检查是否已有记录,避免重复
  328. check = select(QuotaModel).where(
  329. QuotaModel.employee_id == emp_id,
  330. QuotaModel.institution_id == institution_id,
  331. )
  332. existing = await auth.db.execute(check)
  333. if existing.scalar_one_or_none():
  334. continue
  335. stmt = insert(QuotaModel).values(
  336. employee_id=emp_id,
  337. institution_id=institution_id,
  338. quota_id=issue_rule_id,
  339. out_biz_no=f"inst_{institution_id}_{emp_id}",
  340. total_amount=total,
  341. available_amount=available,
  342. status=status,
  343. enterprise_id=enterprise_id,
  344. tenant_id=tenant_id,
  345. )
  346. await auth.db.execute(stmt)
  347. await auth.db.flush()
  348. log.info(
  349. f"创建员工额度记录完成: institution_id={institution_id}, "
  350. f"count={len(employee_ids)}, mode={'period' if (grant_mode == 'period') and amount_val > 0 else 'manual'}, "
  351. f"status={status}, amount={float(total)}, in_period={is_in_period}"
  352. )
  353. @classmethod
  354. async def _sync_modify_quotas_by_scope(
  355. cls,
  356. auth: AuthSchema,
  357. institution_id: str,
  358. enterprise_id: str,
  359. scope_info: dict,
  360. raw_data: dict | None = None,
  361. ):
  362. """修改制度时同步员工额度记录
  363. 新增员工:按发放模式+有效期创建额度记录
  364. 删除员工:删除对应额度记录
  365. 部门模式(EMPLOYEE_DEPARTMENT):add/delete_ids 是部门ID,
  366. 需要先展开为员工ID列表再操作额度。
  367. """
  368. from app.plugin.module_payment.expense.quota.model import QuotaModel
  369. from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
  370. from sqlalchemy import insert, delete as sa_delete, select
  371. grant_mode = (raw_data or {}).get("grant_mode", "manual")
  372. amount_val = float((raw_data or {}).get("amount", 0) or 0)
  373. tenant_id = auth.user.tenant_id if auth.user else 1
  374. adapter_type = scope_info.get("adapter_type", "")
  375. # 判断制度是否在有效期内
  376. now = datetime.now()
  377. is_in_period = True
  378. try:
  379. start_str = (raw_data or {}).get("effective_start_date")
  380. end_str = (raw_data or {}).get("effective_end_date")
  381. if start_str:
  382. start_dt = datetime.fromisoformat(str(start_str).replace('Z', '').replace('T', ' ')[:19])
  383. if now < start_dt:
  384. is_in_period = False
  385. if end_str and is_in_period:
  386. end_dt = datetime.fromisoformat(str(end_str).replace('Z', '').replace('T', ' ')[:19])
  387. if now > end_dt:
  388. is_in_period = False
  389. except Exception:
  390. is_in_period = True
  391. is_active = (grant_mode == "period") and amount_val > 0 and is_in_period
  392. # 如为部门模式,将部门ID展开为员工ID列表
  393. if adapter_type in ("EMPLOYEE_DEPARTMENT", "DEPARTMENT_SELECT"):
  394. from app.plugin.module_payment.employee.model import EmployeeModel
  395. emp_all = await auth.db.execute(
  396. select(EmployeeModel).where(
  397. EmployeeModel.enterprise_id == enterprise_id,
  398. EmployeeModel.status == "EMPLOYEE_ACTIVATED",
  399. )
  400. )
  401. emp_by_dept: dict[str, list[str]] = {}
  402. for emp in emp_all.scalars().all():
  403. if emp.employee_id and emp.department_ids:
  404. for did in emp.department_ids:
  405. emp_by_dept.setdefault(str(did), []).append(emp.employee_id)
  406. raw_delete = scope_info.get("delete_owner_id_list") or []
  407. raw_add = scope_info.get("add_owner_id_list") or []
  408. delete_ids = []
  409. for did in raw_delete:
  410. delete_ids.extend(emp_by_dept.get(str(did), []))
  411. add_ids = []
  412. for did in raw_add:
  413. add_ids.extend(emp_by_dept.get(str(did), []))
  414. else:
  415. delete_ids = scope_info.get("delete_owner_id_list") or []
  416. add_ids = scope_info.get("add_owner_id_list") or []
  417. # 删除被移除的员工额度
  418. if delete_ids:
  419. del_stmt = sa_delete(QuotaModel).where(
  420. QuotaModel.institution_id == institution_id,
  421. QuotaModel.employee_id.in_(delete_ids),
  422. )
  423. await auth.db.execute(del_stmt)
  424. log.info(f"删除已移除员工额度: count={len(delete_ids)}")
  425. # 新增员工的额度
  426. if add_ids:
  427. total = Decimal(str(amount_val)) if is_active else Decimal("0")
  428. available = total
  429. status = QuotaStatusEnum.QUOTA_ACTIVE.value if is_active else QuotaStatusEnum.QUOTA_PENDING.value
  430. created = 0
  431. for emp_id in set(add_ids):
  432. emp_id_str = str(emp_id)
  433. check = select(QuotaModel).where(
  434. QuotaModel.employee_id == emp_id_str,
  435. QuotaModel.institution_id == institution_id,
  436. )
  437. existing = await auth.db.execute(check)
  438. if existing.scalar_one_or_none():
  439. continue
  440. stmt = insert(QuotaModel).values(
  441. employee_id=emp_id_str,
  442. institution_id=institution_id,
  443. out_biz_no=f"inst_{institution_id}_{emp_id_str}",
  444. total_amount=total,
  445. available_amount=available,
  446. status=status,
  447. enterprise_id=enterprise_id,
  448. tenant_id=tenant_id,
  449. )
  450. await auth.db.execute(stmt)
  451. created += 1
  452. if created:
  453. await auth.db.flush()
  454. log.info(f"新增员工额度: count={created}, is_active={is_active}, status={status}")
  455. @classmethod
  456. async def pageinfo_query_service(
  457. cls,
  458. auth: AuthSchema,
  459. enterprise_id: str,
  460. page_no: int = 1,
  461. page_size: int = 20,
  462. institution_name: str | None = None,
  463. ) -> dict:
  464. """
  465. 从支付宝查询费控制度列表
  466. 调用: alipay.ebpp.invoice.institution.pageinfo.query
  467. 失败时降级到本地DB
  468. """
  469. try:
  470. model = AlipayEbppInvoiceInstitutionPageinfoQueryModel()
  471. model.enterprise_id = enterprise_id
  472. model.page_num = page_no
  473. model.page_size = page_size
  474. if institution_name:
  475. model.institution_name = institution_name
  476. req = AlipayEbppInvoiceInstitutionPageinfoQueryRequest()
  477. req.biz_model = model
  478. response = await asyncio.to_thread(cls._execute_alipay, req)
  479. if response:
  480. result = AlipayEbppInvoiceInstitutionPageinfoQueryResponse()
  481. result.parse_response_content(response)
  482. if result.is_success():
  483. return {
  484. "page_no": getattr(result, 'page_num', page_no) or page_no,
  485. "page_size": getattr(result, 'page_size', page_size) or page_size,
  486. "total": getattr(result, 'total_page_count', 0) or 0,
  487. "list": getattr(result, 'institution_list', []) or [],
  488. }
  489. log.warning("支付宝 pageinfo.query 失败,降级到本地DB")
  490. except Exception as e:
  491. log.warning(f"支付宝 pageinfo.query 异常: {e},降级到本地DB")
  492. # 降级:查本地DB
  493. crud = InstitutionCRUD(auth)
  494. search = {"enterprise_id": enterprise_id}
  495. if institution_name:
  496. search["institution_name"] = institution_name
  497. offset = (page_no - 1) * page_size
  498. return await crud.page(
  499. offset=offset,
  500. limit=page_size,
  501. order_by=[{"id": "desc"}],
  502. search=search,
  503. out_schema=InstitutionListOutSchema,
  504. )
  505. @classmethod
  506. async def detailinfo_query_service(
  507. cls,
  508. auth: AuthSchema,
  509. institution_id: str,
  510. enterprise_id: str,
  511. ) -> dict | None:
  512. """
  513. 从支付宝查询费控制度详情,并补充本地规则和额度数据
  514. 调用: alipay.ebpp.invoice.institution.detailinfo.query
  515. 失败时降级到本地DB
  516. """
  517. result_dict = None
  518. if not enterprise_id:
  519. from .crud import InstitutionCRUD as _InstitutionCRUD
  520. _inst_crud = _InstitutionCRUD(auth)
  521. _local_inst = await _inst_crud.get(institution_id=institution_id)
  522. if _local_inst and _local_inst.enterprise_id:
  523. enterprise_id = _local_inst.enterprise_id
  524. try:
  525. model = AlipayEbppInvoiceInstitutionDetailinfoQueryModel()
  526. model.institution_id = institution_id
  527. model.enterprise_id = enterprise_id
  528. req = AlipayEbppInvoiceInstitutionDetailinfoQueryRequest()
  529. req.biz_model = model
  530. response = await asyncio.to_thread(cls._execute_alipay, req)
  531. if response:
  532. result = AlipayEbppInvoiceInstitutionDetailinfoQueryResponse()
  533. result.parse_response_content(response)
  534. if result.is_success():
  535. result_dict = {k: getattr(result, k) for k in (
  536. "adapter_type", "consult_mode", "currency", "effective",
  537. "effective_end_date", "effective_start_date", "expense_type",
  538. "institution_desc", "institution_id", "institution_name",
  539. "issue_rule_info_list", "multi_employee_share_mode",
  540. "outer_source_id", "owner_id_list", "owner_open_id_list",
  541. "owner_type", "scene_type", "standard_info_detail_list",
  542. "standard_info_list",
  543. ) if getattr(result, k, None) is not None}
  544. if not result_dict:
  545. log.warning("支付宝 detailinfo.query 失败,降级到本地DB")
  546. except Exception as e:
  547. log.warning(f"支付宝 detailinfo.query 异常: {e},降级到本地DB")
  548. # 降级:查本地DB
  549. if not result_dict:
  550. crud = InstitutionCRUD(auth)
  551. obj = await crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
  552. if obj:
  553. result_dict = InstitutionListOutSchema.model_validate(obj).model_dump()
  554. if not result_dict:
  555. return None
  556. # 合并本地DB的自定义字段(支付宝不包含的字段)
  557. # 无条件覆盖,保证前端有数据
  558. try:
  559. from .crud import InstitutionCRUD as _crud_cls
  560. _crud = _crud_cls(auth)
  561. local_obj = await _crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
  562. if local_obj:
  563. local_dict = InstitutionListOutSchema.model_validate(local_obj).model_dump()
  564. for field in ("applicable_scope", "grant_mode", "period_type", "amount",
  565. "single_limit", "effective_time_type", "enterprise_id", "status",
  566. "created_time", "updated_time", "consult_mode",
  567. "effective_start_date", "effective_end_date", "institution_name",
  568. "institution_desc", "expense_type", "scene_type", "currency"):
  569. val = local_dict.get(field)
  570. if val is not None:
  571. result_dict[field] = val
  572. except Exception as e:
  573. log.warning(f"合并本地DB字段失败: {e}")
  574. # 从收入额度记录(quota records)获取员工ID列表
  575. if not result_dict.get("employee_ids"):
  576. from sqlalchemy import select as _select
  577. from app.plugin.module_payment.expense.quota.model import QuotaModel
  578. quota_stmt = _select(QuotaModel).where(
  579. QuotaModel.institution_id == institution_id,
  580. QuotaModel.employee_id.isnot(None),
  581. QuotaModel.employee_id != "",
  582. )
  583. quota_result = await auth.db.execute(quota_stmt)
  584. emp_ids = list(set(
  585. q.employee_id for q in quota_result.scalars().all() if q.employee_id
  586. ))
  587. if emp_ids:
  588. result_dict["employee_ids"] = emp_ids
  589. result_dict["scope_owner_id_list"] = emp_ids
  590. # 从支付宝 owner_id_list 映射(若以上均未取到)
  591. owner_ids = result_dict.get("owner_id_list")
  592. if not result_dict.get("employee_ids") and owner_ids and isinstance(owner_ids, list):
  593. result_dict["employee_ids"] = owner_ids
  594. result_dict["scope_owner_id_list"] = owner_ids
  595. # adapter_type → applicable_scope 兜底
  596. adapter_type = result_dict.get("adapter_type")
  597. if adapter_type and not result_dict.get("applicable_scope"):
  598. scope_map = {"EMPLOYEE_SELECT": "employee", "EMPLOYEE_DEPARTMENT": "department", "DEPARTMENT_SELECT": "department", "EMPLOYEE_ALL": "all"}
  599. result_dict["applicable_scope"] = scope_map.get(adapter_type, result_dict.get("applicable_scope", "none"))
  600. # 部门模式下,从 owner_id_list 提取 department_id
  601. if result_dict.get("applicable_scope") == "department" and not result_dict.get("department_id"):
  602. owner_ids = result_dict.get("owner_id_list") or result_dict.get("scope_owner_id_list")
  603. if owner_ids and isinstance(owner_ids, list) and len(owner_ids) > 0:
  604. result_dict["department_id"] = str(owner_ids[0])
  605. # 补充本地规则和额度
  606. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  607. from app.plugin.module_payment.expense.quota.model import QuotaModel
  608. from sqlalchemy import select
  609. # 查使用规则
  610. rule_stmt = select(ExpenseRuleModel).where(ExpenseRuleModel.institution_id == institution_id)
  611. rule_result = await auth.db.execute(rule_stmt)
  612. rules = rule_result.scalars().all()
  613. if rules:
  614. rule_list = []
  615. for r in rules:
  616. rule_item = {
  617. "rule_id": r.rule_id,
  618. "standard_name": r.standard_name,
  619. "standard_desc": r.standard_desc,
  620. }
  621. if hasattr(r, 'single_limit') and r.single_limit:
  622. rule_item["single_limit"] = float(r.single_limit)
  623. if hasattr(r, 'condition_info') and r.condition_info:
  624. rule_item["condition_info"] = r.condition_info
  625. for cond in r.condition_info:
  626. factor = cond.get("rule_factor")
  627. try:
  628. value = float(cond.get("rule_value", 0))
  629. except (ValueError, TypeError):
  630. continue
  631. if factor == "QUOTA_DAY":
  632. rule_item["max_day_amount"] = value
  633. elif factor == "QUOTA_MONTH":
  634. rule_item["max_month_amount"] = value
  635. elif factor == "QUOTA_QUARTER":
  636. rule_item["max_quarter_amount"] = value
  637. elif factor == "QUOTA_YEAR":
  638. rule_item["max_year_amount"] = value
  639. rule_list.append(rule_item)
  640. result_dict["rule_list"] = rule_list
  641. # 查额度
  642. quota_stmt = select(QuotaModel).where(QuotaModel.institution_id == institution_id).limit(1000)
  643. quota_result = await auth.db.execute(quota_stmt)
  644. quotas = quota_result.scalars().all()
  645. if quotas:
  646. result_dict["quota_list"] = [
  647. {
  648. "quota_id": q.quota_id,
  649. "employee_id": q.employee_id or "",
  650. "out_biz_no": q.out_biz_no,
  651. "total_amount": float(q.total_amount) if q.total_amount else 0,
  652. "available_amount": float(q.available_amount) if q.available_amount else 0,
  653. "status": q.status,
  654. }
  655. for q in quotas
  656. ]
  657. return result_dict
  658. @classmethod
  659. async def list_service(
  660. cls,
  661. auth: AuthSchema,
  662. page_no: int = 1,
  663. page_size: int = 20,
  664. search: dict | None = None,
  665. ) -> dict:
  666. """
  667. 查询费控制度列表
  668. 优先调支付宝,失败降级到本地DB
  669. """
  670. enterprise_id = (search or {}).get("enterprise_id", "")
  671. institution_name = (search or {}).get("name") or (search or {}).get("institution_name")
  672. if enterprise_id:
  673. return await cls.pageinfo_query_service(
  674. auth=auth,
  675. enterprise_id=enterprise_id,
  676. page_no=page_no,
  677. page_size=page_size,
  678. institution_name=institution_name,
  679. )
  680. # 无 enterprise_id 时直接查本地
  681. crud = InstitutionCRUD(auth)
  682. offset = (page_no - 1) * page_size
  683. return await crud.page(
  684. offset=offset,
  685. limit=page_size,
  686. order_by=[{"id": "desc"}],
  687. search=search or {},
  688. out_schema=InstitutionListOutSchema,
  689. )
  690. @classmethod
  691. async def delete_institution_service(
  692. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionDeleteModel
  693. ) -> dict:
  694. """
  695. 删除费控制度
  696. 调用: alipay.ebpp.invoice.institution.delete
  697. 支付宝侧已删时忽略错误,始终清理本地关联表
  698. """
  699. institution_id = getattr(data, 'institution_id', None)
  700. # 调用支付宝删除(失败时仅告警,不影响本地清理)
  701. try:
  702. request = AlipayEbppInvoiceInstitutionDeleteRequest()
  703. request.biz_model = data
  704. response = await asyncio.to_thread(cls._execute_alipay, request)
  705. if response:
  706. result = AlipayEbppInvoiceInstitutionDeleteResponse()
  707. result.parse_response_content(response)
  708. if result.is_success():
  709. log.info(f"支付宝删除成功: institution_id={institution_id}")
  710. else:
  711. log.warning(f"支付宝删除失败(可能已删): {result.code} - {result.msg}")
  712. else:
  713. log.warning("支付宝删除无响应,继续清理本地")
  714. except Exception as e:
  715. log.warning(f"支付宝删除异常(忽略): {e}")
  716. # 清理本地关联表
  717. if institution_id:
  718. try:
  719. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  720. from app.plugin.module_payment.expense.quota.model import QuotaModel
  721. from app.plugin.module_payment.expense.institution.model import ExpenseInstitutionModel
  722. from sqlalchemy import delete as sa_delete
  723. # 删规则
  724. await auth.db.execute(sa_delete(ExpenseRuleModel).where(ExpenseRuleModel.institution_id == institution_id))
  725. # 删额度
  726. await auth.db.execute(sa_delete(QuotaModel).where(QuotaModel.institution_id == institution_id))
  727. # 删制度
  728. await auth.db.execute(sa_delete(ExpenseInstitutionModel).where(ExpenseInstitutionModel.institution_id == institution_id))
  729. await auth.db.flush()
  730. log.info(f"本地关联数据已清理: institution_id={institution_id}")
  731. except Exception as e:
  732. log.warning(f"本地清理失败: {e}")
  733. return {"institution_id": institution_id, "deleted": True}
  734. @classmethod
  735. async def modify_institution_service(
  736. cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionModifyModel, raw_data: dict | None = None,
  737. scope_info: dict | None = None,
  738. ) -> dict:
  739. """
  740. 编辑费控制度
  741. 调用: alipay.ebpp.invoice.institution.modify
  742. 适用范围修改(scope_info)需单独调 scope.modify,与基础信息拆分两次请求
  743. 支付宝成功后同步更新本地DB:
  744. - 制度基本信息
  745. - 适用员工范围(scope)
  746. - 使用规则(standard_info_list → pay_expense_rule)
  747. - 额度(issuerule → pay_expense_quota)
  748. """
  749. if data.institution_id is None:
  750. raise CustomException(msg="编辑费控制度失败: 制度ID不能为空")
  751. institution_id = data.institution_id
  752. enterprise_id = getattr(data, 'enterprise_id', None) or (raw_data or {}).get("enterprise_id", "")
  753. raw_data = raw_data or {}
  754. # 第1步:修改支付宝制度基础信息(不含 scope)
  755. request = AlipayEbppInvoiceInstitutionModifyRequest()
  756. request.biz_model = data
  757. response = await asyncio.to_thread(cls._execute_alipay, request)
  758. if not response:
  759. raise CustomException(msg="编辑费控制度失败: 无响应")
  760. result = AlipayEbppInvoiceInstitutionModifyResponse()
  761. result.parse_response_content(response)
  762. if not result.is_success():
  763. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  764. raise CustomException(msg=f"编辑费控制度失败: {result.msg}")
  765. # 第1.5步:单独调用 scope.modify(不与基础修改在同一请求中)
  766. if scope_info:
  767. try:
  768. await InstitutionScopeService.scope_modify_service(
  769. auth=auth, institution_id=institution_id, data=scope_info
  770. )
  771. log.info(f"适用范围已单独同步: adapter_type={scope_info.get('adapter_type')}")
  772. except Exception as e:
  773. log.warning(f"适用范围同步失败(不影响基础修改,本地DB将更新为最新值): {e}")
  774. # scope 变动后同步员工额度记录
  775. try:
  776. await cls._sync_modify_quotas_by_scope(
  777. auth=auth,
  778. institution_id=institution_id,
  779. enterprise_id=enterprise_id,
  780. scope_info=scope_info,
  781. raw_data=raw_data,
  782. )
  783. except Exception as e:
  784. log.warning(f"同步员工额度记录失败(不影响主体操作): {e}")
  785. applicable_scope = raw_data.get("applicable_scope", "")
  786. # 第2步:同步更新本地数据库(scope 已在 Alipay modify 请求中通过 modify_scope_info 处理)
  787. try:
  788. crud = InstitutionCRUD(auth)
  789. update_data = {}
  790. if hasattr(data, 'institution_name') and data.institution_name:
  791. update_data['institution_name'] = data.institution_name
  792. if hasattr(data, 'institution_desc') and data.institution_desc:
  793. update_data['institution_desc'] = data.institution_desc
  794. if hasattr(data, 'effective') and data.effective is not None:
  795. update_data['effective'] = data.effective
  796. update_data['status'] = (
  797. InstitutionStatusEnum.INSTITUTION_EFFECTIVE.value
  798. if data.effective == "1"
  799. else InstitutionStatusEnum.INSTITUTION_INVALID.value
  800. )
  801. if hasattr(data, 'effective_start_date') and data.effective_start_date:
  802. val = data.effective_start_date
  803. update_data['effective_start_date'] = datetime.fromisoformat(val.replace(' ', 'T')) if isinstance(val, str) else val
  804. if hasattr(data, 'effective_end_date') and data.effective_end_date:
  805. val = data.effective_end_date
  806. update_data['effective_end_date'] = datetime.fromisoformat(val.replace(' ', 'T')) if isinstance(val, str) else val
  807. if applicable_scope:
  808. update_data['applicable_scope'] = applicable_scope
  809. # 同步额外配置字段
  810. for field in ("grant_mode", "period_type", "amount", "single_limit", "effective_time_type", "expense_type"):
  811. val = raw_data.get(field)
  812. if val is not None:
  813. update_data[field] = val
  814. if update_data:
  815. await crud.update_by_institution_id(institution_id, update_data)
  816. log.info(f"已更新本地制度: institution_id={institution_id}")
  817. # 制度生效/失效时同步更新额度状态
  818. if hasattr(data, 'effective') and data.effective is not None:
  819. from app.plugin.module_payment.expense.quota.model import QuotaModel
  820. from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
  821. from sqlalchemy import update as sa_quota_update
  822. new_quota_status = (
  823. QuotaStatusEnum.QUOTA_ACTIVE.value
  824. if data.effective == "1"
  825. else QuotaStatusEnum.QUOTA_FROZEN.value
  826. )
  827. quota_upd = sa_quota_update(QuotaModel).where(
  828. QuotaModel.institution_id == institution_id
  829. ).values(status=new_quota_status)
  830. await auth.db.execute(quota_upd)
  831. await auth.db.flush()
  832. log.info(f"已同步更新额度状态: institution_id={institution_id}, effective={data.effective}, quota_status={new_quota_status}")
  833. # 同步标准规则(modify_standard_detail_info)
  834. std_detail = (raw_data or {}).get("modify_standard_detail_info") or {}
  835. if std_detail:
  836. from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
  837. from sqlalchemy import delete as sa_delete
  838. # 删除规则
  839. delete_ids = std_detail.get("delete_standard_id_list", [])
  840. if delete_ids:
  841. d_stmt = sa_delete(ExpenseRuleModel).where(
  842. ExpenseRuleModel.rule_id.in_(delete_ids)
  843. )
  844. await auth.db.execute(d_stmt)
  845. # 新增规则
  846. add_list = std_detail.get("add_standard_list", [])
  847. from sqlalchemy import insert as sa_insert
  848. for std in add_list:
  849. ins_data = {
  850. "out_biz_no": std.get("outer_source_id", f"std_{institution_id}"),
  851. "institution_id": institution_id,
  852. "rule_id": std.get("standard_id"),
  853. "standard_name": std.get("standard_name"),
  854. "standard_desc": std.get("standard_desc"),
  855. "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
  856. "enterprise_id": raw_data.get("enterprise_id", ""),
  857. "tenant_id": auth.user.tenant_id if auth.user else 1,
  858. }
  859. stmt = sa_insert(ExpenseRuleModel).values(**ins_data)
  860. await auth.db.execute(stmt)
  861. # 修改规则
  862. modify_list = std_detail.get("modify_standard_list", [])
  863. for std in modify_list:
  864. std_id = std.get("standard_id", "").strip('"')
  865. update_std = {}
  866. if std.get("standard_name"):
  867. update_std["standard_name"] = std["standard_name"]
  868. if std.get("standard_desc"):
  869. update_std["standard_desc"] = std["standard_desc"]
  870. if update_std and std_id:
  871. from sqlalchemy import update as sa_update
  872. u_stmt = sa_update(ExpenseRuleModel).where(
  873. ExpenseRuleModel.rule_id == std_id
  874. ).values(**update_std)
  875. await auth.db.execute(u_stmt)
  876. await auth.db.flush()
  877. log.info(f"已同步使用规则: institution_id={institution_id}")
  878. # 同步发放规则(modify_issue_rule_detail_info)已去除
  879. # 发放规则对应的额度数据由外部消费同步时通过
  880. # alipay.ebpp.invoice.expensecomsue.outsource.notify 写入真实数据
  881. except Exception as e:
  882. log.warning(f"本地同步失败(不影响支付宝侧): {e}")
  883. return result
  884. class InstitutionScopeService:
  885. """费控制度成员范围服务层"""
  886. @classmethod
  887. def _execute_alipay(cls, request):
  888. """同步执行支付宝调用"""
  889. client = AlipayClient.get_client()
  890. return client.execute(request)
  891. @classmethod
  892. async def scope_modify_service(
  893. cls,
  894. auth: AuthSchema,
  895. institution_id: str,
  896. data: dict,
  897. ) -> dict:
  898. """
  899. 设置/修改制度成员范围
  900. 调用: alipay.ebpp.invoice.institution.scope.modify
  901. """
  902. try:
  903. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopeModifyRequest import (
  904. AlipayEbppInvoiceInstitutionScopeModifyRequest,
  905. )
  906. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopeModifyModel import (
  907. AlipayEbppInvoiceInstitutionScopeModifyModel,
  908. )
  909. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopeModifyResponse import (
  910. AlipayEbppInvoiceInstitutionScopeModifyResponse,
  911. )
  912. except ImportError:
  913. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scope-modify)")
  914. model = AlipayEbppInvoiceInstitutionScopeModifyModel()
  915. model.institution_id = institution_id
  916. model.enterprise_id = data.get("enterprise_id", "")
  917. model.adapter_type = data.get("adapter_type", "EMPLOYEE_ALL")
  918. if data.get("owner_type"):
  919. model.owner_type = data["owner_type"]
  920. if data.get("add_owner_id_list"):
  921. model.add_owner_id_list = data["add_owner_id_list"]
  922. if data.get("delete_owner_id_list"):
  923. model.delete_owner_id_list = data["delete_owner_id_list"]
  924. request = AlipayEbppInvoiceInstitutionScopeModifyRequest()
  925. request.biz_model = model
  926. response = await asyncio.to_thread(cls._execute_alipay, request)
  927. if not response:
  928. raise CustomException(msg="设置制度成员失败: 无响应")
  929. result = AlipayEbppInvoiceInstitutionScopeModifyResponse()
  930. result.parse_response_content(response)
  931. if not result.is_success():
  932. log.error(f"设置制度成员失败: {result.code} - {result.msg}")
  933. raise CustomException(msg=f"设置制度成员失败: {result.msg}")
  934. return {"result": True}
  935. @classmethod
  936. async def scopepageinfo_query_service(
  937. cls,
  938. auth: AuthSchema,
  939. institution_id: str,
  940. enterprise_id: str | None = None,
  941. page_num: int = 1,
  942. page_size: int = 20,
  943. owner_type: str | None = None,
  944. ) -> dict:
  945. """
  946. 查询制度成员范围
  947. 调用: alipay.ebpp.invoice.institution.scopepageinfo.query
  948. """
  949. try:
  950. from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest import (
  951. AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest,
  952. )
  953. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopepageinfoQueryModel import (
  954. AlipayEbppInvoiceInstitutionScopepageinfoQueryModel,
  955. )
  956. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse import (
  957. AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse,
  958. )
  959. except ImportError:
  960. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scopepageinfo-query)")
  961. model = AlipayEbppInvoiceInstitutionScopepageinfoQueryModel()
  962. model.institution_id = institution_id
  963. model.page_num = page_num
  964. model.page_size = page_size
  965. if not enterprise_id:
  966. # 从本地 DB 查找 enterprise_id
  967. from .crud import InstitutionCRUD
  968. inst_crud = InstitutionCRUD(auth)
  969. local_inst = await inst_crud.get(institution_id=institution_id)
  970. if local_inst and local_inst.enterprise_id:
  971. enterprise_id = local_inst.enterprise_id
  972. if enterprise_id:
  973. model.enterprise_id = enterprise_id
  974. if owner_type:
  975. model.owner_type = owner_type
  976. request = AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest()
  977. request.biz_model = model
  978. response = await asyncio.to_thread(cls._execute_alipay, request)
  979. if not response:
  980. raise CustomException(msg="查询制度成员失败: 无响应")
  981. result = AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse()
  982. result.parse_response_content(response)
  983. if not result.is_success():
  984. log.error(f"查询制度成员失败: {result.code} - {result.msg}")
  985. raise CustomException(msg=f"查询制度成员失败: {result.msg}")
  986. return {
  987. "page_num": getattr(result, 'page_num', page_num) or page_num,
  988. "page_size": getattr(result, 'page_size', page_size) or page_size,
  989. "total_page_count": getattr(result, 'total_page_count', 0) or 0,
  990. "adapter_type": getattr(result, 'adapter_type', None),
  991. "owner_id_list": getattr(result, 'owner_id_list', []) or [],
  992. "owner_open_id_list": getattr(result, 'owner_open_id_list', []) or [],
  993. "scope_info_list": [
  994. {
  995. "adapter_type": getattr(result, 'adapter_type', None),
  996. "owner_id_list": getattr(result, 'owner_id_list', []) or [],
  997. "owner_open_id_list": getattr(result, 'owner_open_id_list', []) or [],
  998. }
  999. ] if getattr(result, 'adapter_type', None) else [],
  1000. }
  1001. class IssueruleService:
  1002. """自动额度发放规则服务层"""
  1003. ISSUE_TYPE_MAP = {
  1004. "daily": "ISSUE_DAY",
  1005. "weekly": "ISSUE_WEEK",
  1006. "monthly": "ISSUE_MONTH",
  1007. "quarterly": "ISSUE_QUARTER",
  1008. "yearly": "ISSUE_YEAR",
  1009. }
  1010. @classmethod
  1011. def _execute_alipay(cls, request):
  1012. client = AlipayClient.get_client()
  1013. return client.execute(request)
  1014. @classmethod
  1015. async def create_issuerule_service(
  1016. cls,
  1017. auth: AuthSchema,
  1018. institution_id: str,
  1019. enterprise_id: str,
  1020. quota_type: str,
  1021. issue_type: str,
  1022. issue_amount_value: str,
  1023. outer_source_id: str | None = None,
  1024. issue_rule_name: str | None = None,
  1025. effective_period: str | None = None,
  1026. invalid_mode: int | None = None,
  1027. share_mode: int | None = None,
  1028. ) -> dict:
  1029. """
  1030. 创建自动额度发放规则
  1031. 调用: alipay.ebpp.invoice.issuerule.create
  1032. """
  1033. try:
  1034. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleCreateRequest import (
  1035. AlipayEbppInvoiceIssueruleCreateRequest,
  1036. )
  1037. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleCreateModel import (
  1038. AlipayEbppInvoiceIssueruleCreateModel,
  1039. )
  1040. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleCreateResponse import (
  1041. AlipayEbppInvoiceIssueruleCreateResponse,
  1042. )
  1043. except ImportError:
  1044. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-create)")
  1045. # 参数约束校验
  1046. if quota_type == "CAP" and invalid_mode is not None and invalid_mode != 1:
  1047. raise CustomException(msg="余额类型(CAP)的发放规则必须为可累计(invalid_mode=1)")
  1048. if quota_type == "COUNT" and share_mode is not None and share_mode != 0:
  1049. raise CustomException(msg="次卡类型(COUNT)的发放规则不可转赠(share_mode=0)")
  1050. model = AlipayEbppInvoiceIssueruleCreateModel()
  1051. model.target_type = "INSTITUTION"
  1052. model.target_id = institution_id
  1053. model.quota_type = quota_type
  1054. model.issue_type = issue_type
  1055. model.issue_amount_value = issue_amount_value
  1056. model.enterprise_id = enterprise_id
  1057. if outer_source_id:
  1058. model.outer_source_id = outer_source_id
  1059. if issue_rule_name:
  1060. model.issue_rule_name = issue_rule_name
  1061. if effective_period:
  1062. model.effective_period = effective_period
  1063. if invalid_mode is not None:
  1064. model.invalid_mode = invalid_mode
  1065. if share_mode is not None:
  1066. model.share_mode = share_mode
  1067. request = AlipayEbppInvoiceIssueruleCreateRequest()
  1068. request.biz_model = model
  1069. response = await asyncio.to_thread(cls._execute_alipay, request)
  1070. if not response:
  1071. raise CustomException(msg="创建发放规则失败: 无响应")
  1072. result = AlipayEbppInvoiceIssueruleCreateResponse()
  1073. result.parse_response_content(response)
  1074. if not result.is_success():
  1075. log.error(f"创建发放规则失败: {result.code} - {result.msg}")
  1076. raise CustomException(msg=f"创建发放规则失败: {result.msg}")
  1077. return {
  1078. "issue_rule_id": getattr(result, 'issue_rule_id', None),
  1079. }
  1080. @classmethod
  1081. async def modify_issuerule_service(
  1082. cls,
  1083. auth: AuthSchema,
  1084. institution_id: str,
  1085. issue_rule_id: str,
  1086. enterprise_id: str,
  1087. quota_type: str | None = None,
  1088. issue_type: str | None = None,
  1089. issue_amount_value: str | None = None,
  1090. issue_rule_name: str | None = None,
  1091. effective: str | None = None,
  1092. effective_period: str | None = None,
  1093. invalid_mode: int | None = None,
  1094. share_mode: int | None = None,
  1095. ) -> dict:
  1096. """
  1097. 编辑自动额度发放规则
  1098. 调用: alipay.ebpp.invoice.issuerule.modify
  1099. """
  1100. try:
  1101. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleModifyRequest import (
  1102. AlipayEbppInvoiceIssueruleModifyRequest,
  1103. )
  1104. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleModifyModel import (
  1105. AlipayEbppInvoiceIssueruleModifyModel,
  1106. )
  1107. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleModifyResponse import (
  1108. AlipayEbppInvoiceIssueruleModifyResponse,
  1109. )
  1110. except ImportError:
  1111. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-modify)")
  1112. model = AlipayEbppInvoiceIssueruleModifyModel()
  1113. model.target_type = "INSTITUTION"
  1114. model.target_id = institution_id
  1115. model.issue_rule_id = issue_rule_id
  1116. model.action = "MODIFY_BASIC_INFO"
  1117. model.enterprise_id = enterprise_id
  1118. if issue_rule_name:
  1119. model.issue_rule_name = issue_rule_name
  1120. if quota_type:
  1121. model.quota_type = quota_type
  1122. if issue_type:
  1123. model.issue_type = issue_type
  1124. if issue_amount_value:
  1125. model.issue_amount_value = issue_amount_value
  1126. if effective is not None:
  1127. model.effective = effective
  1128. if effective_period:
  1129. model.effective_period = effective_period
  1130. if invalid_mode is not None:
  1131. model.invalid_mode = invalid_mode
  1132. if share_mode is not None:
  1133. model.share_mode = share_mode
  1134. request = AlipayEbppInvoiceIssueruleModifyRequest()
  1135. request.biz_model = model
  1136. response = await asyncio.to_thread(cls._execute_alipay, request)
  1137. if not response:
  1138. raise CustomException(msg="编辑发放规则失败: 无响应")
  1139. result = AlipayEbppInvoiceIssueruleModifyResponse()
  1140. result.parse_response_content(response)
  1141. if not result.is_success():
  1142. log.error(f"编辑发放规则失败: {result.code} - {result.msg}")
  1143. raise CustomException(msg=f"编辑发放规则失败: {result.msg}")
  1144. return {"result": True}
  1145. @classmethod
  1146. async def delete_issuerule_service(
  1147. cls,
  1148. auth: AuthSchema,
  1149. institution_id: str,
  1150. issue_rule_id_list: list[str],
  1151. enterprise_id: str,
  1152. ) -> dict:
  1153. """
  1154. 删除自动额度发放规则
  1155. 调用: alipay.ebpp.invoice.issuerule.delete
  1156. """
  1157. try:
  1158. from alipay.aop.api.request.AlipayEbppInvoiceIssueruleDeleteRequest import (
  1159. AlipayEbppInvoiceIssueruleDeleteRequest,
  1160. )
  1161. from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleDeleteModel import (
  1162. AlipayEbppInvoiceIssueruleDeleteModel,
  1163. )
  1164. from alipay.aop.api.response.AlipayEbppInvoiceIssueruleDeleteResponse import (
  1165. AlipayEbppInvoiceIssueruleDeleteResponse,
  1166. )
  1167. except ImportError:
  1168. raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-delete)")
  1169. model = AlipayEbppInvoiceIssueruleDeleteModel()
  1170. model.target_type = "INSTITUTION"
  1171. model.target_id = institution_id
  1172. model.issue_rule_id_list = issue_rule_id_list
  1173. model.enterprise_id = enterprise_id
  1174. request = AlipayEbppInvoiceIssueruleDeleteRequest()
  1175. request.biz_model = model
  1176. response = await asyncio.to_thread(cls._execute_alipay, request)
  1177. if not response:
  1178. raise CustomException(msg="删除发放规则失败: 无响应")
  1179. result = AlipayEbppInvoiceIssueruleDeleteResponse()
  1180. result.parse_response_content(response)
  1181. if not result.is_success():
  1182. log.error(f"删除发放规则失败: {result.code} - {result.msg}")
  1183. raise CustomException(msg=f"删除发放规则失败: {result.msg}")
  1184. return {"result": True}