service.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. from datetime import datetime, timedelta
  2. from app.api.v1.module_system.auth.schema import AuthSchema
  3. from app.core.alipay import AlipayClient
  4. from app.core.exceptions import CustomException
  5. from app.core.logger import log
  6. from .crud import FacetofaceCRUD
  7. from .enums import FacetofaceOrderStatus
  8. from .schema import (
  9. FacetofaceApplySchema,
  10. FacetofaceOrderListOutSchema,
  11. FacetofaceOrderOutSchema,
  12. )
  13. class FacetofaceService:
  14. """当面付代开通服务"""
  15. @classmethod
  16. async def apply_service(
  17. cls,
  18. auth: AuthSchema,
  19. data: FacetofaceApplySchema,
  20. ) -> FacetofaceOrderOutSchema:
  21. """
  22. 提交当面付代开通申请
  23. 三步操作: agent.create → agent.facetoface.sign → agent.confirm
  24. """
  25. from alipay.aop.api.domain.AlipayOpenAgentCreateModel import AlipayOpenAgentCreateModel
  26. from alipay.aop.api.request.AlipayOpenAgentCreateRequest import AlipayOpenAgentCreateRequest
  27. from alipay.aop.api.response.AlipayOpenAgentCreateResponse import AlipayOpenAgentCreateResponse
  28. from alipay.aop.api.request.AlipayOpenAgentFacetofaceSignRequest import AlipayOpenAgentFacetofaceSignRequest
  29. from alipay.aop.api.response.AlipayOpenAgentFacetofaceSignResponse import AlipayOpenAgentFacetofaceSignResponse
  30. from alipay.aop.api.domain.AlipayOpenAgentConfirmModel import AlipayOpenAgentConfirmModel
  31. from alipay.aop.api.request.AlipayOpenAgentConfirmRequest import AlipayOpenAgentConfirmRequest
  32. from alipay.aop.api.response.AlipayOpenAgentConfirmResponse import AlipayOpenAgentConfirmResponse
  33. client = AlipayClient.get_client()
  34. crud = FacetofaceCRUD(auth)
  35. # 检查该企业是否已有进行中的申请
  36. existing = await crud.get_by_enterprise_id(data.enterprise_id)
  37. if existing and existing.order_status not in (
  38. FacetofaceOrderStatus.CLOSED.value,
  39. ):
  40. raise CustomException(msg="该企业已有进行中的当面付申请")
  41. # Step 1: 创建应用事务
  42. create_model = AlipayOpenAgentCreateModel()
  43. create_model.account = data.account
  44. # ContactModel
  45. from alipay.aop.api.domain.ContactModel import ContactModel
  46. create_model.contact_info = ContactModel()
  47. create_model.contact_info.contact_name = data.contact_name
  48. create_model.contact_info.contact_mobile = data.contact_mobile
  49. if data.contact_email:
  50. create_model.contact_info.contact_email = data.contact_email
  51. if data.order_ticket:
  52. create_model.order_ticket = data.order_ticket
  53. create_request = AlipayOpenAgentCreateRequest()
  54. create_request.biz_model = create_model
  55. response = client.execute(create_request)
  56. if not response:
  57. raise CustomException(msg="创建应用事务失败: 无响应")
  58. create_result = AlipayOpenAgentCreateResponse()
  59. create_result.parse_response_content(response)
  60. if not create_result.is_success():
  61. log.error(f"创建应用事务失败: {create_result.code} - {create_result.msg} - {create_result.sub_msg}")
  62. raise CustomException(msg=f"创建应用事务失败: {create_result.sub_msg or create_result.msg}")
  63. batch_no = create_result.batch_no
  64. if not batch_no:
  65. raise CustomException(msg="创建应用事务失败: 未返回 batch_no")
  66. log.info(f"当面付代开通 - Step1 创建事务成功: batch_no={batch_no}, account={data.account}")
  67. # Step 2: 提交当面付签约申请
  68. sign_request = AlipayOpenAgentFacetofaceSignRequest()
  69. sign_request.batch_no = batch_no
  70. if data.sign_and_auth:
  71. sign_request.sign_and_auth = "true"
  72. if data.rate:
  73. sign_request.rate = data.rate
  74. response = client.execute(sign_request)
  75. if not response:
  76. raise CustomException(msg="提交当面付签约失败: 无响应")
  77. sign_result = AlipayOpenAgentFacetofaceSignResponse()
  78. sign_result.parse_response_content(response)
  79. if not sign_result.is_success():
  80. log.error(f"提交当面付签约失败: {sign_result.code} - {sign_result.msg} - {sign_result.sub_msg}")
  81. raise CustomException(msg=f"提交当面付签约失败: {sign_result.sub_msg or sign_result.msg}")
  82. log.info(f"当面付代开通 - Step2 签约成功: batch_no={batch_no}")
  83. # Step 3: 确认提交事务
  84. confirm_model = AlipayOpenAgentConfirmModel()
  85. confirm_model.batch_no = batch_no
  86. confirm_request = AlipayOpenAgentConfirmRequest()
  87. confirm_request.biz_model = confirm_model
  88. response = client.execute(confirm_request)
  89. if not response:
  90. raise CustomException(msg="确认提交事务失败: 无响应")
  91. confirm_result = AlipayOpenAgentConfirmResponse()
  92. confirm_result.parse_response_content(response)
  93. if not confirm_result.is_success():
  94. log.error(f"确认提交事务失败: {confirm_result.code} - {confirm_result.msg} - {confirm_result.sub_msg}")
  95. raise CustomException(msg=f"确认提交事务失败: {confirm_result.sub_msg or confirm_result.msg}")
  96. log.info(f"当面付代开通 - Step3 确认事务成功: batch_no={batch_no}, order_no={confirm_result.order_no}")
  97. # 保存申请单
  98. now = datetime.now()
  99. create_data = {
  100. "enterprise_id": data.enterprise_id,
  101. "batch_no": batch_no,
  102. "order_no": confirm_result.order_no,
  103. "order_status": FacetofaceOrderStatus.SUBMITTED.value,
  104. "account": data.account,
  105. "contact_name": data.contact_name,
  106. "contact_mobile": data.contact_mobile,
  107. "contact_email": data.contact_email,
  108. "order_ticket": data.order_ticket,
  109. "sign_and_auth": data.sign_and_auth,
  110. "rate": data.rate,
  111. "remark": data.remark,
  112. "app_auth_token": confirm_result.app_auth_token,
  113. "app_refresh_token": confirm_result.app_refresh_token,
  114. "auth_app_id": confirm_result.auth_app_id,
  115. "user_id": confirm_result.user_id,
  116. "open_id": confirm_result.open_id,
  117. "expires_in": confirm_result.expires_in,
  118. "re_expires_in": confirm_result.re_expires_in,
  119. "next_query_time": now + timedelta(minutes=5),
  120. "query_count": 0,
  121. }
  122. if existing:
  123. obj = await crud.get(id=existing.id, preload=[])
  124. if obj:
  125. for key, value in create_data.items():
  126. if hasattr(obj, key):
  127. setattr(obj, key, value)
  128. await auth.db.flush()
  129. await auth.db.refresh(obj)
  130. return FacetofaceOrderOutSchema.model_validate(obj)
  131. order = await crud.create(create_data)
  132. if not order:
  133. raise CustomException(msg="保存申请单失败")
  134. return FacetofaceOrderOutSchema.model_validate(order)
  135. @classmethod
  136. async def query_order_service(
  137. cls, auth: AuthSchema, order_id: int
  138. ) -> FacetofaceOrderOutSchema:
  139. """手动查询单个申请单状态"""
  140. crud = FacetofaceCRUD(auth)
  141. order = await crud.get(id=order_id)
  142. if not order:
  143. raise CustomException(msg="申请单不存在")
  144. if not order.batch_no:
  145. raise CustomException(msg="申请单无事务编号,无法查询")
  146. terminal_statuses = [
  147. FacetofaceOrderStatus.SUCCESS.value,
  148. FacetofaceOrderStatus.CLOSED.value,
  149. ]
  150. if order.order_status in terminal_statuses:
  151. return FacetofaceOrderOutSchema.model_validate(order)
  152. await cls._query_and_update_order(crud, order)
  153. await auth.db.refresh(order)
  154. return FacetofaceOrderOutSchema.model_validate(order)
  155. @classmethod
  156. async def _query_and_update_order(cls, crud: FacetofaceCRUD, order) -> None:
  157. """查询支付宝接口并更新申请单状态"""
  158. from alipay.aop.api.domain.AlipayOpenAgentOrderQueryModel import AlipayOpenAgentOrderQueryModel
  159. from alipay.aop.api.request.AlipayOpenAgentOrderQueryRequest import AlipayOpenAgentOrderQueryRequest
  160. from alipay.aop.api.response.AlipayOpenAgentOrderQueryResponse import AlipayOpenAgentOrderQueryResponse
  161. client = AlipayClient.get_client()
  162. query_model = AlipayOpenAgentOrderQueryModel()
  163. query_model.batch_no = order.batch_no
  164. query_request = AlipayOpenAgentOrderQueryRequest()
  165. query_request.biz_model = query_model
  166. try:
  167. response = client.execute(query_request)
  168. except Exception as e:
  169. log.error(f"查询当面付申请单状态异常: batch_no={order.batch_no}, error={e}")
  170. return
  171. if not response:
  172. log.warning(f"查询当面付申请单无响应: batch_no={order.batch_no}")
  173. return
  174. result = AlipayOpenAgentOrderQueryResponse()
  175. result.parse_response_content(response)
  176. if not result.is_success():
  177. log.error(f"查询当面付申请单失败: batch_no={order.batch_no}, {result.code} - {result.sub_msg or result.msg}")
  178. return
  179. now = datetime.now()
  180. update_data: dict = {
  181. "last_query_time": now,
  182. "query_count": order.query_count + 1,
  183. }
  184. if result.order_no:
  185. update_data["order_no"] = result.order_no
  186. if result.confirm_url:
  187. update_data["confirm_url"] = result.confirm_url
  188. if result.merchant_pid:
  189. update_data["merchant_pid"] = result.merchant_pid
  190. if result.reject_reason:
  191. update_data["reject_reason"] = result.reject_reason
  192. alipay_status = result.order_status
  193. if alipay_status:
  194. if alipay_status == "MERCHANT_CONFIRM":
  195. update_data["order_status"] = FacetofaceOrderStatus.MERCHANT_CONFIRM.value
  196. update_data["next_query_time"] = now + timedelta(hours=4)
  197. elif alipay_status == "MERCHANT_AUDITING":
  198. update_data["order_status"] = FacetofaceOrderStatus.MERCHANT_AUDITING.value
  199. update_data["next_query_time"] = now + timedelta(hours=4)
  200. elif alipay_status == "MERCHANT_CONFIRM_SUCCESS":
  201. update_data["order_status"] = FacetofaceOrderStatus.SUCCESS.value
  202. update_data["next_query_time"] = None
  203. elif alipay_status in ("MERCHANT_APPLY_ORDER_CANCELED", "MERCHANT_CONFIRM_TIME_OUT"):
  204. update_data["order_status"] = FacetofaceOrderStatus.CLOSED.value
  205. update_data["next_query_time"] = None
  206. elif alipay_status == "MERCHANT_INFO_HOLD":
  207. # 暂存状态,继续轮询
  208. update_data["next_query_time"] = now + timedelta(hours=4)
  209. else:
  210. # 未识别状态,继续轮询
  211. update_data["next_query_time"] = now + timedelta(hours=4)
  212. log.info(
  213. f"当面付申请单状态更新: batch_no={order.batch_no}, "
  214. f"alipay_status={alipay_status}, "
  215. f"local_status={update_data.get('order_status', order.order_status)}"
  216. )
  217. obj = await crud.get(id=order.id, preload=[])
  218. if obj:
  219. for key, value in update_data.items():
  220. if hasattr(obj, key):
  221. setattr(obj, key, value)
  222. await crud.auth.db.flush()
  223. @classmethod
  224. async def get_by_enterprise_service(
  225. cls, auth: AuthSchema, enterprise_id: str
  226. ) -> FacetofaceOrderOutSchema | None:
  227. crud = FacetofaceCRUD(auth)
  228. order = await crud.get_by_enterprise_id(enterprise_id)
  229. if not order:
  230. return None
  231. return FacetofaceOrderOutSchema.model_validate(order)
  232. @classmethod
  233. async def batch_status_service(
  234. cls, auth: AuthSchema, enterprise_ids: list[str]
  235. ) -> dict[str, str | None]:
  236. crud = FacetofaceCRUD(auth)
  237. result: dict[str, str | None] = {}
  238. for eid in enterprise_ids:
  239. order = await crud.get_by_enterprise_id(eid)
  240. result[eid] = order.order_status if order else None
  241. return result
  242. @classmethod
  243. async def list_service(
  244. cls,
  245. auth: AuthSchema,
  246. page_no: int = 1,
  247. page_size: int = 20,
  248. search: dict | None = None,
  249. ) -> dict:
  250. crud = FacetofaceCRUD(auth)
  251. offset = (page_no - 1) * page_size
  252. return await crud.page(
  253. offset=offset,
  254. limit=page_size,
  255. order_by=[{"id": "desc"}],
  256. search=search or {},
  257. out_schema=FacetofaceOrderListOutSchema,
  258. )
  259. @classmethod
  260. async def detail_service(
  261. cls, auth: AuthSchema, order_id: int
  262. ) -> FacetofaceOrderOutSchema:
  263. crud = FacetofaceCRUD(auth)
  264. order = await crud.get(id=order_id)
  265. if not order:
  266. raise CustomException(msg="申请单不存在")
  267. return FacetofaceOrderOutSchema.model_validate(order)
  268. @classmethod
  269. async def poll_pending_orders(cls) -> int:
  270. """
  271. 轮询待处理的申请单(供定时任务调用)
  272. """
  273. from app.core.database import async_db_session
  274. async with async_db_session() as db:
  275. from app.api.v1.module_system.auth.schema import AuthSchema
  276. auth = AuthSchema(db=db, user=None, tenant_id=1)
  277. crud = FacetofaceCRUD(auth)
  278. orders = await crud.get_pending_orders()
  279. if not orders:
  280. return 0
  281. count = 0
  282. for order in orders:
  283. try:
  284. await cls._query_and_update_order(crud, order)
  285. count += 1
  286. except Exception as e:
  287. log.error(f"轮询当面付申请单异常: batch_no={order.batch_no}, error={e}")
  288. await db.commit()
  289. log.info(f"当面付申请单轮询完成: 处理 {count}/{len(orders)} 条")
  290. return count