service.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. from typing import Optional, cast
  2. from app.api.v1.module_system.auth.schema import AuthSchema
  3. from app.api.v1.module_system.user.crud import UserCRUD
  4. from app.api.v1.module_system.user.schema import UserCreateSchema
  5. from app.core.alipay import AlipayClient
  6. from app.core.exceptions import CustomException
  7. from app.core.logger import log
  8. from app.utils.hash_bcrpy_util import PwdUtil
  9. from .crud import EmployeeCRUD
  10. from .schema import (
  11. EmployeeCreateOrUpdateSchema,
  12. EmployeeListOutSchema,
  13. EmployeeOperationOutSchema,
  14. EmployeeInviteQuerySchema,
  15. EmployeeInviteQueryOutSchema,
  16. EmployeeOutSchema,
  17. )
  18. from alipay.aop.api.domain.EmployeeInfoDTO import EmployeeInfoDTO
  19. class EmployeeService:
  20. """员工服务层"""
  21. @classmethod
  22. async def add_employee_service(
  23. cls, auth: AuthSchema, data: EmployeeCreateOrUpdateSchema
  24. ) -> EmployeeOperationOutSchema:
  25. """
  26. 添加员工
  27. 调用: alipay.commerce.ec.employee.add
  28. """
  29. crud = EmployeeCRUD(auth)
  30. from alipay.aop.api.request.AlipayCommerceEcEmployeeAddRequest import (
  31. AlipayCommerceEcEmployeeAddRequest,
  32. )
  33. from alipay.aop.api.domain.AlipayCommerceEcEmployeeAddModel import (
  34. AlipayCommerceEcEmployeeAddModel,
  35. )
  36. from alipay.aop.api.response.AlipayCommerceEcEmployeeAddResponse import (
  37. AlipayCommerceEcEmployeeAddResponse,
  38. )
  39. model = AlipayCommerceEcEmployeeAddModel()
  40. # 必选
  41. model.enterprise_id = data.enterprise_id
  42. model.employee_name = data.employee_name
  43. # 身份标识(identity_type+identity)、身份证(employee_cert_type+employee_cert_no)、
  44. # 手机号、邮箱四者必选其一; 当传入多个时,优先级为:身份标识>身份证>手机号>邮箱
  45. model.identity_type = data.identity_type
  46. model.identity = data.identity
  47. model.identity_open_id = data.identity_open_id
  48. model.employee_mobile = data.employee_mobile
  49. model.employee_email = data.employee_email
  50. model.employee_cert_type = data.employee_cert_type
  51. model.employee_cert_no = data.employee_cert_no
  52. model.iot_check_type = data.iot_check_type
  53. model.employee_no = data.employee_no
  54. model.department_ids = data.department_ids
  55. model.accounting_entity_ids = data.accounting_entity_ids
  56. model.label_names = data.label_names
  57. model.sign_return_url = "alipays://platformapi/startapp?appId=2021006145621147"
  58. model.create_share_code = data.create_share_code
  59. model.sign_url_carry_info = data.sign_url_carry_info
  60. model.profiles = data.profiles
  61. request = AlipayCommerceEcEmployeeAddRequest()
  62. request.biz_model = model
  63. client = AlipayClient.get_client()
  64. response = client.execute(request)
  65. if not response:
  66. raise CustomException(msg="添加员工失败: 无响应")
  67. result = AlipayCommerceEcEmployeeAddResponse()
  68. result.parse_response_content(response)
  69. if not result.is_success():
  70. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  71. raise CustomException(msg=f"添加员工失败: {result.sub_msg or result.msg or result.code}")
  72. result_data = EmployeeOperationOutSchema(
  73. employee_id=result.employee_id,
  74. sign_url=result.sign_url,
  75. share_code=result.share_code,
  76. iot_unique_id=result.iot_unique_id,
  77. )
  78. create_data_dict = result_data.model_dump()
  79. create_data_dict.update(data.model_dump(exclude_none=True))
  80. # 自动创建系统用户记录
  81. user_crud = UserCRUD(auth)
  82. # 检查用户是否已存在
  83. username = str(result.employee_id)
  84. existing_user = await user_crud.get_by_username_crud(username=username)
  85. if not existing_user:
  86. # 创建新用户
  87. user_data = UserCreateSchema(
  88. username=username,
  89. password=PwdUtil.set_password_hash(password="123456"), # 默认密码
  90. name=data.employee_name,
  91. mobile=data.employee_mobile,
  92. email=data.employee_email,
  93. tenant_id=auth.tenant_id,
  94. # role_ids=[11],
  95. # status="0", # 启用状态
  96. description=f"e:{data.enterprise_id}:{result.employee_id}"
  97. )
  98. user_dict = user_data.model_dump(exclude_unset=True)
  99. new_user = await user_crud.create(data=user_dict, skip_tenant_id=True)
  100. create_data_dict["user_id"] = new_user.id
  101. else:
  102. create_data_dict["user_id"] = existing_user.id
  103. await crud.create(data=create_data_dict)
  104. return result_data
  105. @classmethod
  106. async def list_service(
  107. cls,
  108. auth: AuthSchema,
  109. page_no: int = 1,
  110. page_size: int = 20,
  111. search: dict | None = None,
  112. ) -> dict:
  113. """
  114. 查询员工列表
  115. """
  116. crud = EmployeeCRUD(auth)
  117. offset = (page_no - 1) * page_size
  118. result = await crud.page(
  119. offset=offset,
  120. limit=page_size,
  121. order_by=[{"id": "desc"}],
  122. search=search or {},
  123. out_schema=EmployeeListOutSchema,
  124. preload=["user"]
  125. )
  126. # 补充部门名称
  127. if result.get("items"):
  128. from app.plugin.module_payment.department.model import DepartmentModel
  129. from sqlalchemy import select
  130. dept_ids = set()
  131. for item in result["items"]:
  132. dept_ids_list = item.get("department_ids") or []
  133. if dept_ids_list:
  134. dept_ids.update(str(d) for d in dept_ids_list if d)
  135. if dept_ids:
  136. dept_stmt = select(DepartmentModel.department_id, DepartmentModel.department_name).where(
  137. DepartmentModel.department_id.in_(list(dept_ids)),
  138. )
  139. dept_result = await auth.db.execute(dept_stmt)
  140. dept_map = {row.department_id: row.department_name for row in dept_result.fetchall() if row.department_id}
  141. for item in result["items"]:
  142. dept_ids_list = item.get("department_ids") or []
  143. names = [dept_map.get(str(d), "") for d in dept_ids_list if dept_map.get(str(d))]
  144. item["department_name"] = "、".join(filter(None, names)) or None
  145. return result
  146. @classmethod
  147. async def info_service(
  148. cls, auth: AuthSchema, employee_id: Optional[str], employee_email: Optional[str], employee_mobile: Optional[str], enterprise_id: str
  149. ) -> EmployeeOutSchema:
  150. crud = EmployeeCRUD(auth)
  151. out_data = await crud.get(employee_id=employee_id, employee_email=employee_email, employee_mobile=employee_mobile, enterprise_id=enterprise_id)
  152. if not out_data:
  153. raise CustomException(msg="员工不存在")
  154. result = EmployeeOutSchema.model_validate(out_data)
  155. # 补充 account_id
  156. if enterprise_id:
  157. from app.plugin.module_payment.enterprise.model import EnterpriseModel
  158. from sqlalchemy import select
  159. ent_stmt = select(EnterpriseModel).where(EnterpriseModel.enterprise_id == enterprise_id).limit(1)
  160. ent_result = await auth.db.execute(ent_stmt)
  161. ent = ent_result.scalar_one_or_none()
  162. if ent and ent.account_id:
  163. result.account_id = ent.account_id
  164. # 补充 user_name / avatar
  165. user = getattr(out_data, 'user', None)
  166. if user:
  167. result.user_id = user.id
  168. result.user_name = user.name or user.username
  169. result.avatar = getattr(user, 'avatar', None)
  170. return result
  171. @classmethod
  172. async def detail_service(
  173. cls, auth: AuthSchema, employee_id: Optional[str], employee_email: Optional[str], employee_mobile: Optional[str], enterprise_id: str
  174. ) -> dict:
  175. """
  176. 查询员工详情
  177. 调用: alipay.commerce.ec.employee.info.query
  178. 合并本地DB数据补全字段
  179. """
  180. from alipay.aop.api.request.AlipayCommerceEcEmployeeInfoQueryRequest import AlipayCommerceEcEmployeeInfoQueryRequest
  181. from alipay.aop.api.domain.AlipayCommerceEcEmployeeInfoQueryModel import AlipayCommerceEcEmployeeInfoQueryModel
  182. from alipay.aop.api.response.AlipayCommerceEcEmployeeInfoQueryResponse import AlipayCommerceEcEmployeeInfoQueryResponse
  183. model = AlipayCommerceEcEmployeeInfoQueryModel()
  184. model.enterprise_id = enterprise_id
  185. model.employee_id = employee_id
  186. model.employee_email = employee_email
  187. model.mobile = employee_mobile
  188. request = AlipayCommerceEcEmployeeInfoQueryRequest()
  189. request.biz_model = model
  190. client = AlipayClient.get_client()
  191. response = client.execute(request)
  192. if not response:
  193. raise CustomException(msg="查询员工详情失败: 无响应")
  194. result = AlipayCommerceEcEmployeeInfoQueryResponse()
  195. result.parse_response_content(response)
  196. if not result.is_success():
  197. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  198. raise CustomException(msg=f"查询员工详情失败: {result.msg}")
  199. # Alipay 原始字段 → 本地 schema 字段映射
  200. raw = EmployeeInfoDTO.to_alipay_dict(cast(EmployeeInfoDTO, result.employee_info))
  201. mapped = {
  202. "employee_id": raw.get("employee_id"),
  203. "employee_name": raw.get("employee_name"),
  204. "employee_no": raw.get("employee_no"),
  205. "employee_email": raw.get("email"),
  206. "employee_mobile": raw.get("mobile"),
  207. "employee_cert_type": raw.get("employee_cert_type"),
  208. "employee_cert_no": raw.get("employee_cert_no"),
  209. "identity_open_id": raw.get("open_id"),
  210. "created_time": raw.get("gmt_create"),
  211. "updated_time": raw.get("gmt_modified"),
  212. "department_ids": raw.get("department_list"),
  213. "label_names": raw.get("label_names"),
  214. "enterprise_id": enterprise_id,
  215. }
  216. # 合并本地DB数据(补全 Alipay 不返回的字段)
  217. try:
  218. from .crud import EmployeeCRUD
  219. local = await EmployeeCRUD(auth).get(
  220. employee_id=employee_id,
  221. employee_email=employee_email,
  222. employee_mobile=employee_mobile,
  223. enterprise_id=enterprise_id
  224. )
  225. if local:
  226. for f in ("status", "identity_type", "identity", "iot_check_type",
  227. "withholding_sign_str", "free_sign_token", "share_code",
  228. "sign_url", "expire_time", "profiles", "department_ids",
  229. "accounting_entity_ids", "role_list", "employee_cert_type",
  230. "employee_cert_no", "employee_email", "employee_mobile"):
  231. val = getattr(local, f, None)
  232. if val is not None:
  233. mapped[f] = val
  234. except Exception as e:
  235. log.warning(f"合并本地员工数据失败: {e}")
  236. return mapped
  237. @classmethod
  238. async def delete_employee_service(
  239. cls, auth: AuthSchema, employee_id: str, enterprise_id: str
  240. ) -> EmployeeOperationOutSchema:
  241. """
  242. 删除员工
  243. 调用: alipay.commerce.ec.employee.delete
  244. """
  245. from alipay.aop.api.request.AlipayCommerceEcEmployeeDeleteRequest import AlipayCommerceEcEmployeeDeleteRequest
  246. from alipay.aop.api.domain.AlipayCommerceEcEmployeeDeleteModel import AlipayCommerceEcEmployeeDeleteModel
  247. from alipay.aop.api.response.AlipayCommerceEcEmployeeDeleteResponse import AlipayCommerceEcEmployeeDeleteResponse
  248. model = AlipayCommerceEcEmployeeDeleteModel()
  249. model.enterprise_id = enterprise_id
  250. model.employee_id = employee_id
  251. request = AlipayCommerceEcEmployeeDeleteRequest()
  252. request.biz_model = model
  253. client = AlipayClient.get_client()
  254. response = client.execute(request)
  255. if not response:
  256. raise CustomException(msg="删除员工失败: 无响应")
  257. result = AlipayCommerceEcEmployeeDeleteResponse()
  258. result.parse_response_content(response)
  259. if not result.is_success():
  260. log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
  261. raise CustomException(msg=f"删除员工失败: {result.sub_msg or result.msg or result.code}")
  262. # 从本地数据库删除, 并删除关联的用户
  263. # 先查询员工是否存在
  264. crud = EmployeeCRUD(auth)
  265. employee = await crud.get(employee_id=employee_id, enterprise_id=enterprise_id, preload=["user"])
  266. if not employee:
  267. raise CustomException(msg=f"员工 {employee_id} 不存在")
  268. # 解约联动:从所有引用该员工的费控制度中移除
  269. try:
  270. from app.plugin.module_payment.expense.institution.scope_sync import remove_employee_from_institution_scopes
  271. await remove_employee_from_institution_scopes(
  272. auth=auth, enterprise_id=enterprise_id, employee_id=employee_id
  273. )
  274. except Exception as e:
  275. log.warning(f"从制度移除解约员工失败(不影响主体操作): {e}")
  276. # 先删除关联的用户
  277. if employee.user_id:
  278. user_service = UserCRUD(auth)
  279. await user_service.delete(ids=[employee.user_id])
  280. await crud.delete_by_employee_id(employee_id)
  281. return EmployeeOperationOutSchema(
  282. employee_id=employee_id,
  283. )
  284. @classmethod
  285. async def invite_query_service(
  286. cls, auth: AuthSchema, data: EmployeeInviteQuerySchema
  287. ) -> EmployeeInviteQueryOutSchema:
  288. """
  289. 获取员工签约激活链接
  290. 调用: alipay.commerce.ec.employee.invite.query
  291. """
  292. from alipay.aop.api.request.AlipayCommerceEcEmployeeInviteQueryRequest import AlipayCommerceEcEmployeeInviteQueryRequest
  293. from alipay.aop.api.domain.AlipayCommerceEcEmployeeInviteQueryModel import AlipayCommerceEcEmployeeInviteQueryModel
  294. from alipay.aop.api.response.AlipayCommerceEcEmployeeInviteQueryResponse import AlipayCommerceEcEmployeeInviteQueryResponse
  295. model = AlipayCommerceEcEmployeeInviteQueryModel()
  296. model.enterprise_id = data.enterprise_id
  297. model.employee_id = data.employee_id
  298. model.page_content_code = data.page_content_code
  299. model.withholding_sign_str = data.withholding_sign_str
  300. model.create_share_code = data.create_share_code
  301. request = AlipayCommerceEcEmployeeInviteQueryRequest()
  302. request.biz_model = model
  303. client = AlipayClient.get_client()
  304. response = client.execute(request)
  305. if not response:
  306. raise CustomException(msg="获取员工签约激活链接失败: 无响应")
  307. result = AlipayCommerceEcEmployeeInviteQueryResponse()
  308. result.parse_response_content(response)
  309. if not result.is_success():
  310. log.error(f"支付宝接口调用失败: {result.code} - {result.sub_msg}")
  311. raise CustomException(msg=f"获取员工签约激活链接失败: {result.sub_msg}")
  312. return EmployeeInviteQueryOutSchema(
  313. enterprise_id=result.enterprise_id or data.enterprise_id,
  314. sign_url=result.sign_url or "",
  315. mini_app_sign_url=result.mini_app_sign_url or "",
  316. share_code=getattr(result, 'share_code', None)
  317. )
  318. @classmethod
  319. async def update_employee_from_alipay(cls, auth: AuthSchema, data: EmployeeCreateOrUpdateSchema):
  320. """
  321. 从支付宝更新员工信息
  322. """
  323. # 先查询支付宝员工信息
  324. employee = await cls.detail_service(
  325. auth=auth,
  326. employee_id=data.employee_id,
  327. employee_email=data.employee_email,
  328. employee_mobile=data.employee_mobile,
  329. enterprise_id=data.enterprise_id
  330. )
  331. if not employee :
  332. raise CustomException(msg=f"员工 {data.employee_id} 不存在")
  333. if hasattr(employee, 'employee_id') and employee['employee_id'] != data.employee_id:
  334. raise CustomException(msg=f"员工 {data.employee_id} 不存在")
  335. crud = EmployeeCRUD(auth)
  336. await crud.update_by(
  337. employee_mobile=data.employee_mobile,
  338. employee_email=data.employee_email,
  339. identity_open_id=data.identity_open_id,
  340. enterprise_id=data.enterprise_id,
  341. data=data.model_dump(exclude_none=True)
  342. )