controller.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. from typing import Annotated
  2. import uuid
  3. from fastapi import APIRouter, Depends, Path, Query
  4. from fastapi.responses import JSONResponse
  5. from app.api.v1.module_system.auth.schema import AuthSchema
  6. from app.common.response import ResponseSchema, SuccessResponse
  7. from app.core.dependencies import AuthPermission
  8. from app.core.logger import log
  9. from app.core.router_class import OperationLogRoute
  10. from app.plugin.module_payment.expense.institution.schema import InstitutionListOutSchema
  11. from .service import InstitutionService, InstitutionScopeService, IssueruleService
  12. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
  13. AlipayEbppInvoiceInstitutionCreateModel,
  14. )
  15. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionCreateResponse import (
  16. AlipayEbppInvoiceInstitutionCreateResponse,
  17. )
  18. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDeleteModel import (
  19. AlipayEbppInvoiceInstitutionDeleteModel,
  20. )
  21. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionDeleteResponse import (
  22. AlipayEbppInvoiceInstitutionDeleteResponse,
  23. )
  24. from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionModifyModel import (
  25. AlipayEbppInvoiceInstitutionModifyModel,
  26. )
  27. from alipay.aop.api.response.AlipayEbppInvoiceInstitutionModifyResponse import (
  28. AlipayEbppInvoiceInstitutionModifyResponse,
  29. )
  30. InstitutionRouter = APIRouter(
  31. route_class=OperationLogRoute,
  32. prefix="/institution",
  33. tags=["费控制度"],
  34. )
  35. @InstitutionRouter.post(
  36. "",
  37. summary="创建费控制度",
  38. description="创建费控制度。支持串联调用:创建制度→设置成员→创建发放规则",
  39. )
  40. async def create_institution_controller(
  41. data: dict,
  42. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:create"]))],
  43. ) -> JSONResponse:
  44. """创建费控制度(含完整串联流程)"""
  45. enterprise_id = data.get("enterprise_id", "")
  46. if not enterprise_id:
  47. from app.plugin.module_payment.enterprise.model import EnterpriseModel
  48. from sqlalchemy import select
  49. tenant_id = auth.user.tenant_id if auth.user and auth.user.tenant_id else auth.tenant_id
  50. log.info(f"推导 enterprise_id: tenant_id={tenant_id}, user_tenant_id={getattr(auth.user, 'tenant_id', None)}")
  51. stmt = select(EnterpriseModel).where(EnterpriseModel.tenant_id == tenant_id).limit(1)
  52. result = await auth.db.execute(stmt)
  53. enterprise = result.scalar_one_or_none()
  54. log.info(f"查询 enterprise 结果: {enterprise.enterprise_id if enterprise else 'None'}")
  55. enterprise_id = enterprise.enterprise_id if enterprise else ""
  56. if enterprise_id:
  57. data["enterprise_id"] = enterprise_id
  58. # 字段映射:前端 name → Alipay institution_name
  59. if data.get("name") and not data.get("institution_name"):
  60. data["institution_name"] = data["name"]
  61. # Alipay 必填:商户外部单据号(唯一标识)
  62. if not data.get("outer_source_id"):
  63. data["outer_source_id"] = str(uuid.uuid4()).replace("-", "")
  64. # expense_type 映射:前端值 → 支付宝标准值
  65. EXPENSE_TYPE_MAP = {"GENERAL": "DEFAULT", "DEFAULT": "DEFAULT"}
  66. if data.get("expense_type") in EXPENSE_TYPE_MAP:
  67. data["expense_type"] = EXPENSE_TYPE_MAP[data["expense_type"]]
  68. # 时间格式补全:YYYY-MM-DD → YYYY-MM-DD HH:mm:ss
  69. if data.get("effective_start_date") and len(data["effective_start_date"]) == 10:
  70. data["effective_start_date"] = data["effective_start_date"] + " 00:00:00"
  71. if data.get("effective_end_date") and len(data["effective_end_date"]) == 10:
  72. data["effective_end_date"] = data["effective_end_date"] + " 23:59:59"
  73. elif not data.get("effective_end_date") and data.get("effective_time_type") == "unlimited":
  74. # 长期有效:设为2099年底
  75. data["effective_end_date"] = "2099-12-31 23:59:59"
  76. # 默认使用规则(支付宝必填)
  77. if not data.get("standard_info_list"):
  78. single_limit = data.get("single_limit", 0)
  79. period_type = data.get("period_type", "")
  80. amount = data.get("amount", 0)
  81. standard_info = {
  82. "standard_name": data.get("institution_name", "默认规则"),
  83. "standard_desc": f"单笔限额{single_limit}元" if single_limit else "通用规则",
  84. "consume_mode": "DEFAULT",
  85. "payment_policy": "PERSONAL",
  86. "personal_qrcode_mode": 0,
  87. "outer_source_id": str(uuid.uuid4()).replace("-", ""),
  88. }
  89. condition_list = []
  90. if single_limit:
  91. condition_list.append({"rule_factor": "QUOTA_TOTAL", "rule_name": "单次消费金额", "rule_value": str(single_limit)})
  92. # 定额发放时,将周期限额写入使用规则条件
  93. PERIOD_FACTOR_MAP = {
  94. "daily": "QUOTA_DAY", "weekly": "QUOTA_WEEK",
  95. "monthly": "QUOTA_MONTH", "quarterly": "QUOTA_QUARTER",
  96. "yearly": "QUOTA_YEAR",
  97. }
  98. if data.get("grant_mode") == "period" and period_type in PERIOD_FACTOR_MAP and amount:
  99. condition_list.append({
  100. "rule_factor": PERIOD_FACTOR_MAP[period_type],
  101. "rule_name": f"{period_type}限额",
  102. "rule_value": str(amount),
  103. })
  104. # 至少保证有一条条件规则(支付宝必填)
  105. if not condition_list:
  106. condition_list.append({
  107. "rule_factor": "QUOTA_TOTAL",
  108. "rule_name": "单次消费金额",
  109. "rule_value": "0",
  110. })
  111. standard_info["standard_condition_info_list"] = condition_list
  112. data["standard_info_list"] = [standard_info]
  113. institution_create_model = AlipayEbppInvoiceInstitutionCreateModel.from_alipay_dict(data)
  114. # 解析适用成员数据
  115. scope_data = None
  116. adapter_type = data.get("applicable_scope")
  117. if adapter_type and adapter_type not in ("NONE", "none"):
  118. ADAPTER_TYPE_MAP = {"all": "EMPLOYEE_ALL", "employee": "EMPLOYEE_SELECT", "department": "EMPLOYEE_DEPARTMENT"}
  119. mapped_adapter = ADAPTER_TYPE_MAP.get(adapter_type, adapter_type)
  120. scope_data = {
  121. "adapter_type": mapped_adapter,
  122. "owner_type": data.get("scope_owner_type", "EMPLOYEE"),
  123. "add_owner_id_list": data.get("scope_owner_id_list"),
  124. }
  125. # 全体员工时把 scope 写入创建请求(避免默认无scope导致支付宝后台不可操作)
  126. if adapter_type == "all":
  127. data["institution_scope_info"] = {
  128. "adapter_type": "ALL",
  129. "owner_type": "EMPLOYEE",
  130. }
  131. # 解析发放规则数据
  132. issuerule_data = None
  133. if data.get("grant_mode") == "period":
  134. period_type_raw = data.get("period_type", "monthly")
  135. # 映射前端period_type到支付宝枚举
  136. ISSUE_TYPE_MAP = {
  137. "daily": "ISSUE_DAY",
  138. "weekly": "ISSUE_WEEK",
  139. "monthly": "ISSUE_MONTH",
  140. "quarterly": "ISSUE_QUARTER",
  141. "yearly": "ISSUE_YEAR",
  142. }
  143. issue_type = ISSUE_TYPE_MAP.get(period_type_raw, "ISSUE_MONTH")
  144. amount = data.get("amount", 0)
  145. # 有效时间配置
  146. effective_time_type = data.get("effective_time_type", "unlimited")
  147. if effective_time_type == "unlimited":
  148. effective_period = '{"all": true}'
  149. elif effective_time_type == "workday":
  150. workday_start = data.get("workday_start_time", "00:00")
  151. workday_end = data.get("workday_end_time", "23:59")
  152. effective_period = f'{{"regular":{{"workday":[["{workday_start}","{workday_end}"]]}}}}'
  153. else:
  154. effective_period = '{"all": true}'
  155. issuerule_data = {
  156. "quota_type": "CAP",
  157. "issue_type": issue_type,
  158. "issue_amount_value": str(amount),
  159. "issue_rule_name": data.get("name", "") + "-发放规则",
  160. "effective_period": effective_period,
  161. "invalid_mode": 1 if data.get("effective_time_type") == "unlimited" else 0,
  162. "share_mode": 0,
  163. "outer_source_id": data.get("outer_source_id") or str(uuid.uuid4()),
  164. }
  165. result = await InstitutionService.create_institution_full_flow(
  166. auth=auth,
  167. institution_model=institution_create_model,
  168. enterprise_id=enterprise_id,
  169. scope_data=scope_data,
  170. issuerule_data=issuerule_data,
  171. raw_data=data,
  172. )
  173. log.info(f"创建费控制度成功: institution_id={result.get('institution_id')}")
  174. return SuccessResponse(data=result, msg="创建费控制度成功")
  175. @InstitutionRouter.get(
  176. "",
  177. summary="查询费控制度列表",
  178. description="分页查询费控制度列表",
  179. response_model=ResponseSchema[InstitutionListOutSchema],
  180. )
  181. async def list_institution_controller(
  182. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:list"]))],
  183. page_no: Annotated[int, Query(description="页码")] = 1,
  184. page_size: Annotated[int, Query(description="每页数量")] = 20,
  185. enterprise_id: Annotated[str | None, Query(description="企业ID")] = None,
  186. name: Annotated[str | None, Query(description="制度名称")] = None,
  187. expense_type: Annotated[str | None, Query(description="费用类型")] = None,
  188. status: Annotated[str | None, Query(description="状态")] = None,
  189. ) -> JSONResponse:
  190. """查询费控制度列表"""
  191. search = {}
  192. if enterprise_id:
  193. search["enterprise_id"] = enterprise_id
  194. if name:
  195. search["name"] = name
  196. if expense_type:
  197. search["expense_type"] = expense_type
  198. if status:
  199. search["status"] = status
  200. result = await InstitutionService.list_service(
  201. auth=auth, page_no=page_no, page_size=page_size, search=search
  202. )
  203. return SuccessResponse(data=result, msg="查询费控制度列表成功")
  204. @InstitutionRouter.get(
  205. "/{institution_id}",
  206. summary="查询费控制度详情",
  207. description="查询费控制度详情 (alipay.ebpp.invoice.institution.detailinfo.query),失败时降级到本地DB",
  208. )
  209. async def detail_institution_controller(
  210. institution_id: Annotated[str, Path(description="制度ID")],
  211. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:detail"]))],
  212. enterprise_id: Annotated[str | None, Query(description="企业ID")] = None,
  213. ) -> JSONResponse:
  214. """查询费控制度详情"""
  215. if not enterprise_id:
  216. from app.plugin.module_payment.enterprise.model import EnterpriseModel
  217. from sqlalchemy import select
  218. tenant_id = auth.user.tenant_id if auth.user and auth.user.tenant_id else auth.tenant_id
  219. stmt = select(EnterpriseModel).where(EnterpriseModel.tenant_id == tenant_id).limit(1)
  220. result = await auth.db.execute(stmt)
  221. ent = result.scalar_one_or_none()
  222. enterprise_id = ent.enterprise_id if ent else ""
  223. result = await InstitutionService.detailinfo_query_service(
  224. auth=auth,
  225. institution_id=institution_id,
  226. enterprise_id=enterprise_id,
  227. )
  228. if result is None:
  229. return SuccessResponse(data=None, msg="制度不存在")
  230. return SuccessResponse(data=result, msg="查询费控制度详情成功")
  231. @InstitutionRouter.delete(
  232. "",
  233. summary="删除费控制度",
  234. description="删除费控制度 (alipay.ebpp.invoice.institution.delete)",
  235. )
  236. async def delete_institution_controller(
  237. data: dict,
  238. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:delete"]))],
  239. ) -> JSONResponse:
  240. """删除费控制度"""
  241. institution_delete_model = AlipayEbppInvoiceInstitutionDeleteModel.from_alipay_dict(data)
  242. result = await InstitutionService.delete_institution_service(auth=auth, data=institution_delete_model)
  243. log.info(f"删除费控制度成功: institution_id={institution_delete_model.institution_id}, enterprise_id={institution_delete_model.enterprise_id}")
  244. return SuccessResponse(data=result, msg="删除费控制度成功")
  245. @InstitutionRouter.post(
  246. "/modify",
  247. summary="编辑费控制度",
  248. description="编辑费控制度 (alipay.ebpp.invoice.institution.modify)",
  249. )
  250. async def modify_institution_controller(
  251. data: dict,
  252. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:modify"]))],
  253. ) -> JSONResponse:
  254. """编辑费控制度"""
  255. institution_id = data.get("institution_id", "")
  256. # enterprise_id 推导
  257. if not data.get("enterprise_id"):
  258. from app.plugin.module_payment.enterprise.model import EnterpriseModel
  259. from sqlalchemy import select
  260. tenant_id = auth.user.tenant_id if auth.user and auth.user.tenant_id else auth.tenant_id
  261. stmt = select(EnterpriseModel).where(EnterpriseModel.tenant_id == tenant_id).limit(1)
  262. result = await auth.db.execute(stmt)
  263. enterprise = result.scalar_one_or_none()
  264. if enterprise:
  265. data["enterprise_id"] = enterprise.enterprise_id
  266. # name → institution_name
  267. if data.get("name") and not data.get("institution_name"):
  268. data["institution_name"] = data["name"]
  269. # 时间格式
  270. if data.get("effective_start_date") and len(data["effective_start_date"]) == 10:
  271. data["effective_start_date"] = data["effective_start_date"] + " 00:00:00"
  272. if data.get("effective_end_date") and len(data["effective_end_date"]) == 10:
  273. data["effective_end_date"] = data["effective_end_date"] + " 23:59:59"
  274. elif not data.get("effective_end_date") and data.get("effective_time_type") == "unlimited":
  275. data["effective_end_date"] = "2099-12-31 23:59:59"
  276. # expense_type 映射
  277. EXPENSE_TYPE_MAP = {"GENERAL": "DEFAULT", "DEFAULT": "DEFAULT"}
  278. if data.get("expense_type") in EXPENSE_TYPE_MAP:
  279. data["expense_type"] = EXPENSE_TYPE_MAP[data["expense_type"]]
  280. # 提取 scope 变更数据(需与基础修改分两次请求)
  281. applicable_scope = data.get("applicable_scope", "")
  282. scope_info = None
  283. enterprise_id = data.get("enterprise_id", "")
  284. if applicable_scope and applicable_scope not in ("NONE", "none"):
  285. ADAPTER_MAP = {"all": "EMPLOYEE_ALL", "employee": "EMPLOYEE_SELECT", "department": "EMPLOYEE_DEPARTMENT"}
  286. new_adapter = ADAPTER_MAP.get(applicable_scope, applicable_scope)
  287. # 查询当前scope:计算旧→新的差异
  288. old_ids = []
  289. try:
  290. scope_old = await InstitutionScopeService.scopepageinfo_query_service(
  291. auth=auth, institution_id=institution_id, enterprise_id=enterprise_id,
  292. page_num=1, page_size=100,
  293. )
  294. old_ids = [str(i) for i in (scope_old.get("owner_id_list") or []) if i]
  295. except Exception:
  296. log.warning(f"查询旧scope失败,将全量覆盖: institution_id={data.get('institution_id', '')}")
  297. new_ids_raw = data.get("scope_owner_id_list") or []
  298. new_ids = [str(i) for i in new_ids_raw if i is not None and str(i).strip()]
  299. # 计算差异
  300. old_set, new_set = set(old_ids), set(new_ids)
  301. add_ids = list(new_set - old_set)
  302. delete_ids = list(old_set - new_set)
  303. scope_info = {
  304. "enterprise_id": enterprise_id,
  305. "adapter_type": new_adapter,
  306. "owner_type": "EMPLOYEE",
  307. }
  308. if new_adapter == "EMPLOYEE_ALL":
  309. # 全体员工模式:不传员工ID给支付宝(但本地同步仍然需要)
  310. log.info(f"全体员工模式,跳过员工ID传参")
  311. else:
  312. if add_ids:
  313. scope_info["add_owner_id_list"] = add_ids
  314. if delete_ids:
  315. scope_info["delete_owner_id_list"] = delete_ids
  316. if not add_ids and not delete_ids:
  317. scope_info = None
  318. log.info("scope 无变化,跳过")
  319. elif applicable_scope in ("NONE", "none"):
  320. # 暂不设置:清空支付宝范围(调用 scope.modify 删除所有成员)
  321. try:
  322. scope_old = await InstitutionScopeService.scopepageinfo_query_service(
  323. auth=auth, institution_id=institution_id, enterprise_id=enterprise_id,
  324. page_num=1, page_size=100,
  325. )
  326. old_adapter = scope_old.get("adapter_type", "")
  327. old_ids = [str(i) for i in (scope_old.get("owner_id_list") or []) if i]
  328. # 对非NONE的旧范围,清空所有成员
  329. if old_adapter and old_adapter not in ("NONE",) and old_ids:
  330. scope_info = {
  331. "enterprise_id": enterprise_id,
  332. "adapter_type": old_adapter,
  333. "owner_type": "EMPLOYEE",
  334. "delete_owner_id_list": old_ids,
  335. }
  336. log.info(f"暂不设置 - 清空支付宝旧成员: {old_adapter}, count={len(old_ids)}")
  337. except Exception:
  338. log.warning(f"查询旧scope失败,跳过清空支付宝成员")
  339. # 从请求中移除 scope 数据,避免与基础修改冲突
  340. data.pop("modify_scope_info", None)
  341. # 构建金额/限额变更的标准规则信息(同步到支付宝)
  342. new_amount = data.get("amount")
  343. new_single_limit = data.get("single_limit")
  344. period_type = data.get("period_type", "")
  345. grant_mode = data.get("grant_mode", "")
  346. if institution_id and enterprise_id and (new_amount is not None or new_single_limit is not None):
  347. try:
  348. log.info(f"金额变更检测: grant_mode={grant_mode}, period_type={period_type}, new_amount={new_amount}, new_single_limit={new_single_limit}")
  349. def _to_dict(obj):
  350. """Alipay SDK 对象转 dict,已经是 dict 则直接返回"""
  351. if isinstance(obj, dict):
  352. return obj
  353. if hasattr(obj, 'to_alipay_dict'):
  354. return obj.to_alipay_dict()
  355. return {}
  356. # 从支付宝查询当前制度详情
  357. detail_dict = await InstitutionService.detailinfo_query_service(
  358. auth=auth, institution_id=institution_id, enterprise_id=enterprise_id
  359. )
  360. # 构建 modify_standard_detail_info
  361. modify_standard_list = []
  362. if detail_dict and detail_dict.get("standard_info_list"):
  363. std_list = detail_dict["standard_info_list"]
  364. log.info(f"找到 standard_info_list, 数量={len(std_list) if isinstance(std_list, list) else 1}")
  365. if not isinstance(std_list, list):
  366. std_list = [std_list]
  367. for std_obj in std_list:
  368. std = _to_dict(std_obj)
  369. std_id = std.get("standard_id")
  370. log.info(f"标准规则: std_id={std_id}, std={std}")
  371. if not std_id:
  372. continue
  373. conditions = std.get("standard_condition_info_list") or []
  374. if not isinstance(conditions, list):
  375. conditions = [conditions]
  376. modify_condition_list = []
  377. for cond_obj in conditions:
  378. cond = _to_dict(cond_obj)
  379. cond_factor = cond.get("rule_factor", "")
  380. cond_id = cond.get("rule_id", "")
  381. log.info(f"条件: rule_factor={cond_factor}, rule_id={cond_id}, cond={cond}")
  382. # 周期限额变更
  383. if grant_mode == "period" and period_type:
  384. PERIOD_FACTOR_MAP = {
  385. "daily": "QUOTA_DAY", "weekly": "QUOTA_WEEK",
  386. "monthly": "QUOTA_MONTH", "quarterly": "QUOTA_QUARTER",
  387. "yearly": "QUOTA_YEAR",
  388. }
  389. target_factor = PERIOD_FACTOR_MAP.get(period_type)
  390. if target_factor and cond_factor == target_factor and new_amount is not None:
  391. modify_condition_list.append({
  392. "rule_id": cond_id,
  393. "rule_factor": cond_factor,
  394. "rule_value": str(new_amount),
  395. })
  396. # 单笔限额变更
  397. if cond_factor == "QUOTA_TOTAL" and new_single_limit is not None:
  398. modify_condition_list.append({
  399. "rule_id": cond_id,
  400. "rule_factor": "QUOTA_TOTAL",
  401. "rule_value": str(new_single_limit),
  402. })
  403. if modify_condition_list:
  404. modify_standard_list.append({
  405. "standard_id": std_id,
  406. "modify_condition_list": modify_condition_list,
  407. })
  408. if modify_standard_list:
  409. data["modify_standard_detail_info"] = {
  410. "modify_standard_list": modify_standard_list
  411. }
  412. log.info(f"已构建金额变更信息: amount={new_amount}, single_limit={new_single_limit}, rules={len(modify_standard_list)}")
  413. else:
  414. log.warning(f"未找到可修改的标准规则: institution_id={institution_id}")
  415. # 周期发放制度:更新发放规则金额 — 独立调 issuerule.modify
  416. if grant_mode == "period" and new_amount is not None and detail_dict:
  417. issue_rule_list = detail_dict.get("issue_rule_info_list") or []
  418. if not isinstance(issue_rule_list, list):
  419. issue_rule_list = [issue_rule_list]
  420. for rule in issue_rule_list:
  421. rule_dict = _to_dict(rule)
  422. issue_rule_id = rule_dict.get("issue_rule_id", "")
  423. if issue_rule_id:
  424. try:
  425. await IssueruleService.modify_issuerule_service(
  426. auth=auth,
  427. institution_id=institution_id,
  428. issue_rule_id=issue_rule_id,
  429. enterprise_id=enterprise_id,
  430. issue_amount_value=str(new_amount),
  431. )
  432. log.info(f"已同步发放规则金额: issue_rule_id={issue_rule_id}, amount={new_amount}")
  433. break
  434. except Exception as e:
  435. log.warning(f"同步发放规则失败(不影响制度修改): {e}")
  436. else:
  437. log.warning(f"未查询到制度详情,跳过金额同步: institution_id={institution_id}")
  438. except Exception as e:
  439. log.warning(f"构建标准规则变更信息失败(将跳过金额同步): {e}")
  440. # 第1次请求:仅修改制度基础信息(不含 scope)
  441. base_data = {k: v for k, v in data.items() if k != "modify_scope_info"}
  442. institution_modify_model = AlipayEbppInvoiceInstitutionModifyModel.from_alipay_dict(base_data)
  443. try:
  444. result = await InstitutionService.modify_institution_service(
  445. auth=auth, data=institution_modify_model, raw_data=base_data, scope_info=scope_info
  446. )
  447. except Exception as e:
  448. err_msg = str(e)
  449. if "consult" in err_msg.lower() or "咨询" in err_msg or "发" in err_msg:
  450. raise CustomException(msg="制度下存在发放规则,咨询模式不允许修改为外部服务商,请先删除发放规则后再试")
  451. raise
  452. log.info(f"编辑费控制度成功: institution_id={institution_modify_model.institution_id}")
  453. return SuccessResponse(data=result, msg="编辑费控制度成功")
  454. # ========== 制度成员范围管理 ==========
  455. @InstitutionRouter.get(
  456. "/{institution_id}/scope",
  457. summary="查询制度成员范围",
  458. description="查询制度下成员范围 (alipay.ebpp.invoice.institution.scopepageinfo.query)",
  459. )
  460. async def list_scope_controller(
  461. institution_id: Annotated[str, Path(description="制度ID")],
  462. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:scope:list"]))],
  463. enterprise_id: Annotated[str | None, Query(description="企业ID")] = None,
  464. owner_type: Annotated[str | None, Query(description="适配ID类型")] = None,
  465. page_num: Annotated[int, Query(description="页码")] = 1,
  466. page_size: Annotated[int, Query(description="每页条数")] = 20,
  467. ) -> JSONResponse:
  468. """查询制度成员"""
  469. result = await InstitutionScopeService.scopepageinfo_query_service(
  470. auth=auth,
  471. institution_id=institution_id,
  472. enterprise_id=enterprise_id,
  473. page_num=page_num,
  474. page_size=page_size,
  475. owner_type=owner_type,
  476. )
  477. # 如果本地库标记为暂不设置,覆盖支付宝返回值
  478. try:
  479. from .crud import InstitutionCRUD
  480. crud = InstitutionCRUD(auth)
  481. local_inst = await crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
  482. if local_inst and getattr(local_inst, 'applicable_scope', '') in ('none', 'NONE'):
  483. result["adapter_type"] = "NONE"
  484. result["owner_id_list"] = []
  485. except Exception:
  486. pass
  487. return SuccessResponse(data=result, msg="查询成功")
  488. @InstitutionRouter.post(
  489. "/{institution_id}/scope",
  490. summary="设置制度成员范围",
  491. description="设置/修改制度成员范围 (alipay.ebpp.invoice.institution.scope.modify)",
  492. )
  493. async def modify_scope_controller(
  494. institution_id: Annotated[str, Path(description="制度ID")],
  495. data: dict,
  496. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:scope:modify"]))],
  497. ) -> JSONResponse:
  498. """设置制度成员"""
  499. from app.plugin.module_payment.employee.model import EmployeeModel
  500. from sqlalchemy import select
  501. enterprise_id = data.get("enterprise_id", "")
  502. new_adapter = data.get("adapter_type", "EMPLOYEE_ALL")
  503. # ====== 1. 查询旧适配类型和老员工ID列表 ======
  504. old_adapter = "NONE"
  505. old_employee_ids: set[str] = set()
  506. try:
  507. scope_old = await InstitutionScopeService.scopepageinfo_query_service(
  508. auth=auth, institution_id=institution_id, enterprise_id=enterprise_id,
  509. page_num=1, page_size=100,
  510. )
  511. old_adapter = scope_old.get("adapter_type", old_adapter)
  512. raw_old = [str(i) for i in (scope_old.get("owner_id_list") or []) if i]
  513. if old_adapter == "EMPLOYEE_ALL":
  514. # 全体员工模式 → 所有已签约员工
  515. stmt = select(EmployeeModel.employee_id).where(
  516. EmployeeModel.enterprise_id == enterprise_id,
  517. EmployeeModel.status == "ACTIVATED",
  518. )
  519. result = await auth.db.execute(stmt)
  520. old_employee_ids = {row[0] for row in result.fetchall() if row[0]}
  521. elif old_adapter == "EMPLOYEE_DEPARTMENT":
  522. # 按部门模式 → 部门下的所有已签约员工
  523. from app.plugin.module_payment.employee.model import EmployeeModel
  524. stmt = select(EmployeeModel.employee_id, EmployeeModel.department_ids).where(
  525. EmployeeModel.enterprise_id == enterprise_id,
  526. EmployeeModel.status == "ACTIVATED",
  527. )
  528. result = await auth.db.execute(stmt)
  529. dept_set = set(raw_old)
  530. for row in result.fetchall():
  531. if row[1] and dept_set.intersection(set(row[1])):
  532. if row[0]:
  533. old_employee_ids.add(row[0])
  534. else:
  535. old_employee_ids = set(raw_old)
  536. except Exception:
  537. old_adapter = "NONE" # 查不到旧范围时标记为NONE,确保走分步调用的逻辑
  538. log.warning(f"查询旧scope失败(将全量处理): institution_id={institution_id}")
  539. # ====== 2. 计算新员工ID列表 ======
  540. new_employee_ids: set[str] = set()
  541. if new_adapter == "EMPLOYEE_ALL":
  542. stmt = select(EmployeeModel.employee_id).where(
  543. EmployeeModel.enterprise_id == enterprise_id,
  544. EmployeeModel.status == "ACTIVATED",
  545. )
  546. result = await auth.db.execute(stmt)
  547. new_employee_ids = {row[0] for row in result.fetchall() if row[0]}
  548. elif new_adapter == "EMPLOYEE_DEPARTMENT":
  549. dept_ids = data.get("add_owner_id_list") or []
  550. dept_set = set(str(d) for d in dept_ids if d)
  551. stmt = select(EmployeeModel.employee_id, EmployeeModel.department_ids).where(
  552. EmployeeModel.enterprise_id == enterprise_id,
  553. EmployeeModel.status == "ACTIVATED",
  554. )
  555. result = await auth.db.execute(stmt)
  556. for row in result.fetchall():
  557. if row[1] and dept_set.intersection(set(row[1])):
  558. if row[0]:
  559. new_employee_ids.add(row[0])
  560. else:
  561. raw_new = data.get("add_owner_id_list") or []
  562. # EMPLOYEE_SELECT 模式:验证员工属于当前企业
  563. from sqlalchemy import select as _sa_select
  564. emp_verify = await auth.db.execute(
  565. _sa_select(EmployeeModel.employee_id).where(
  566. EmployeeModel.enterprise_id == enterprise_id,
  567. EmployeeModel.employee_id.in_([str(i) for i in raw_new if i]),
  568. )
  569. )
  570. validated = [row[0] for row in emp_verify.fetchall() if row[0]]
  571. new_employee_ids = set(validated)
  572. # ====== 3. 计算员工级差异(用于配额联动) ======
  573. add_emp_ids = list(new_employee_ids - old_employee_ids)
  574. delete_emp_ids = list(old_employee_ids - new_employee_ids)
  575. # 提取传给支付宝的原始ID(部门模式传部门ID,员工模式传员工ID) ======
  576. alipay_add_ids: list[str] = []
  577. alipay_delete_ids: list[str] = []
  578. if new_adapter == "EMPLOYEE_DEPARTMENT":
  579. dept_ids = data.get("add_owner_id_list") or []
  580. alipay_add_ids = [str(d) for d in dept_ids if d]
  581. elif new_adapter == "EMPLOYEE_SELECT":
  582. alipay_add_ids = list(new_employee_ids - old_employee_ids)
  583. alipay_delete_ids = list(old_employee_ids - new_employee_ids)
  584. # ====== 4. 调用支付宝 scope.modify ======
  585. # 部门模式:需一次性传部门ID,不支持分步
  586. if new_adapter == "EMPLOYEE_DEPARTMENT":
  587. scope_data = {
  588. "enterprise_id": enterprise_id,
  589. "adapter_type": "EMPLOYEE_DEPARTMENT",
  590. "owner_type": data.get("owner_type", "EMPLOYEE"),
  591. }
  592. if alipay_add_ids:
  593. scope_data["add_owner_id_list"] = alipay_add_ids
  594. result = await InstitutionScopeService.scope_modify_service(
  595. auth=auth, institution_id=institution_id, data=scope_data,
  596. )
  597. elif old_adapter != new_adapter and new_adapter != "EMPLOYEE_ALL":
  598. # SELECT模式 + 适配类型变更:先改类型,再加员工
  599. await InstitutionScopeService.scope_modify_service(
  600. auth=auth, institution_id=institution_id,
  601. data={
  602. "enterprise_id": enterprise_id,
  603. "adapter_type": new_adapter,
  604. "owner_type": data.get("owner_type", "EMPLOYEE"),
  605. },
  606. )
  607. if alipay_add_ids or alipay_delete_ids:
  608. scope_data = {
  609. "enterprise_id": enterprise_id,
  610. "adapter_type": new_adapter,
  611. "owner_type": data.get("owner_type", "EMPLOYEE"),
  612. }
  613. if alipay_add_ids:
  614. scope_data["add_owner_id_list"] = alipay_add_ids
  615. if alipay_delete_ids:
  616. scope_data["delete_owner_id_list"] = alipay_delete_ids
  617. result = await InstitutionScopeService.scope_modify_service(
  618. auth=auth, institution_id=institution_id, data=scope_data,
  619. )
  620. else:
  621. result = {"result": True}
  622. else:
  623. # ALL模式 或 类型不变
  624. if alipay_add_ids or alipay_delete_ids:
  625. scope_data = {
  626. "enterprise_id": enterprise_id,
  627. "adapter_type": new_adapter,
  628. "owner_type": data.get("owner_type", "EMPLOYEE"),
  629. }
  630. if alipay_add_ids:
  631. scope_data["add_owner_id_list"] = alipay_add_ids
  632. if alipay_delete_ids:
  633. scope_data["delete_owner_id_list"] = alipay_delete_ids
  634. result = await InstitutionScopeService.scope_modify_service(
  635. auth=auth, institution_id=institution_id, data=scope_data,
  636. )
  637. else:
  638. result = {"result": True}
  639. # ====== 5. 更新本地库 + 额度联动 ======
  640. try:
  641. from .crud import InstitutionCRUD
  642. crud = InstitutionCRUD(auth)
  643. scope_map = {"EMPLOYEE_ALL": "all", "EMPLOYEE_SELECT": "employee", "EMPLOYEE_DEPARTMENT": "department"}
  644. applicable_scope = scope_map.get(new_adapter, "all")
  645. update_data = {"applicable_scope": applicable_scope}
  646. if new_adapter == "EMPLOYEE_DEPARTMENT":
  647. dept_ids = data.get("add_owner_id_list") or []
  648. if dept_ids:
  649. update_data["department_id"] = str(dept_ids[0])
  650. await crud.update_by_institution_id(institution_id, update_data)
  651. if enterprise_id:
  652. from .service import InstitutionService
  653. scope_info = {
  654. "adapter_type": new_adapter,
  655. "owner_type": "EMPLOYEE",
  656. "add_owner_id_list": add_emp_ids,
  657. "delete_owner_id_list": delete_emp_ids,
  658. }
  659. await InstitutionService._sync_modify_quotas_by_scope(
  660. auth=auth,
  661. institution_id=institution_id,
  662. enterprise_id=enterprise_id,
  663. scope_info=scope_info,
  664. raw_data={},
  665. )
  666. except Exception as e:
  667. log.warning(f"本地scope同步失败(不影响支付宝侧): {e}")
  668. log.info(f"设置成员成功: {old_adapter}→{new_adapter}, "
  669. f"加{len(add_ids)}人, 减{len(delete_ids)}人")
  670. return SuccessResponse(data=result, msg="设置成功")
  671. # ========== 自动额度发放规则管理 ==========
  672. @InstitutionRouter.post(
  673. "/{institution_id}/issuerule",
  674. summary="创建自动发放规则",
  675. description="创建自动额度发放规则 (alipay.ebpp.invoice.issuerule.create)",
  676. )
  677. async def create_issuerule_controller(
  678. institution_id: Annotated[str, Path(description="制度ID")],
  679. data: dict,
  680. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:issuerule:create"]))],
  681. ) -> JSONResponse:
  682. """创建自动发放规则"""
  683. result = await IssueruleService.create_issuerule_service(
  684. auth=auth,
  685. institution_id=institution_id,
  686. enterprise_id=data.get("enterprise_id", ""),
  687. quota_type=data.get("quota_type", "CAP"),
  688. issue_type=data.get("issue_type", "ISSUE_MONTH"),
  689. issue_amount_value=data.get("issue_amount_value", "0"),
  690. outer_source_id=data.get("outer_source_id"),
  691. issue_rule_name=data.get("issue_rule_name"),
  692. effective_period=data.get("effective_period"),
  693. invalid_mode=data.get("invalid_mode"),
  694. share_mode=data.get("share_mode"),
  695. )
  696. log.info(f"创建自动发放规则成功: institution_id={institution_id}")
  697. return SuccessResponse(data=result, msg="创建自动发放规则成功")
  698. @InstitutionRouter.put(
  699. "/{institution_id}/issuerule/{issue_rule_id}",
  700. summary="编辑自动发放规则",
  701. description="编辑自动额度发放规则 (alipay.ebpp.invoice.issuerule.modify)",
  702. )
  703. async def modify_issuerule_controller(
  704. institution_id: Annotated[str, Path(description="制度ID")],
  705. issue_rule_id: Annotated[str, Path(description="发放规则ID")],
  706. data: dict,
  707. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:issuerule:modify"]))],
  708. ) -> JSONResponse:
  709. result = await IssueruleService.modify_issuerule_service(
  710. auth=auth,
  711. institution_id=institution_id,
  712. issue_rule_id=issue_rule_id,
  713. enterprise_id=data.get("enterprise_id", ""),
  714. quota_type=data.get("quota_type"),
  715. issue_type=data.get("issue_type"),
  716. issue_amount_value=data.get("issue_amount_value"),
  717. issue_rule_name=data.get("issue_rule_name"),
  718. effective=data.get("effective"),
  719. effective_period=data.get("effective_period"),
  720. invalid_mode=data.get("invalid_mode"),
  721. share_mode=data.get("share_mode"),
  722. )
  723. log.info(f"编辑自动发放规则成功: issue_rule_id={issue_rule_id}")
  724. return SuccessResponse(data=result, msg="编辑自动发放规则成功")
  725. @InstitutionRouter.delete(
  726. "/{institution_id}/issuerule",
  727. summary="删除自动发放规则",
  728. description="删除自动额度发放规则 (alipay.ebpp.invoice.issuerule.delete)",
  729. )
  730. async def delete_issuerule_controller(
  731. institution_id: Annotated[str, Path(description="制度ID")],
  732. data: dict,
  733. auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:issuerule:delete"]))],
  734. ) -> JSONResponse:
  735. result = await IssueruleService.delete_issuerule_service(
  736. auth=auth,
  737. institution_id=institution_id,
  738. issue_rule_id_list=data.get("issue_rule_id_list", []),
  739. enterprise_id=data.get("enterprise_id", ""),
  740. )
  741. log.info(f"删除自动发放规则成功: institution_id={institution_id}")
  742. return SuccessResponse(data=result, msg="删除自动发放规则成功")