schemas.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import json
  2. from datetime import datetime
  3. from fastapi import Request
  4. from fastapi.datastructures import FormData
  5. from pydantic import BaseModel, Field, model_validator
  6. from app.core.logger import log
  7. async def parse_alipay_notify_form(request: Request) -> "AlipayNotifyBase":
  8. """解析支付宝通知表单数据"""
  9. form_data = await request.form()
  10. # 确保所有值都是字符串类型
  11. return parse_alipay_notify_form_data(form_data)
  12. def parse_alipay_notify_form_data(form_data: FormData) -> "AlipayNotifyBase":
  13. """解析支付宝通知表单数据"""
  14. form_dict = {}
  15. for key, value in form_data.items():
  16. if isinstance(value, str):
  17. form_dict[key] = value
  18. else:
  19. # 对于非字符串类型(如UploadFile),获取其字符串表示
  20. form_dict[key] = str(value)
  21. log.info(f"收到支付宝通知表单数据: {form_dict}")
  22. return AlipayNotifyBase(**form_dict)
  23. class AlipayNotifyBase(BaseModel):
  24. """支付宝通知基础模型"""
  25. notify_id: str = Field(..., description="通知ID")
  26. utc_timestamp: str = Field(..., description="消息发送时的服务端时间")
  27. msg_method: str | None = Field(None, description="消息接口名称(优先取此值,若为空则用 notify_type)")
  28. app_id: str = Field(..., description="消息接受方的应用id")
  29. version: str = Field(..., description="版本号")
  30. sign: str = Field(..., description="签名")
  31. sign_type: str = Field(..., description="签名类型")
  32. charset: str = Field(..., description="编码集")
  33. notify_type: str | None = Field(None, description="通知类型(msg_method 为空时使用)")
  34. biz_content: str = Field(..., description="消息报文")
  35. def parse_biz_content(self) -> dict:
  36. """解析 biz_content JSON 字符串"""
  37. try:
  38. return json.loads(self.biz_content)
  39. except:
  40. raise Exception(f"支付宝通知消息 - 解析 biz_content 失败")
  41. @property
  42. def method(self) -> str:
  43. """获取消息接口标识,优先 msg_method,其次 notify_type"""
  44. return self.msg_method or self.notify_type or ""
  45. class EnterpriseChangeContent(BaseModel):
  46. """企业变更通知内容"""
  47. enterprise_id: str = Field(..., description="企业id")
  48. out_biz_no: str = Field(..., description="外部业务号/幂等号")
  49. action: str = Field(..., description="变更动作")
  50. change_time: str = Field(..., description="变更时间")
  51. account_id: str | None = Field(None, description="账户id")
  52. remark: str | None = Field(None, description="备注")
  53. tenant_id: int | None = Field(None, description="租户id")
  54. class EmployeeChangeContent(BaseModel):
  55. """
  56. 员工变更通知内容
  57. {'gmt_create': '2026-04-24 11:15:49', 'role_list': ['USER'], 'mobile': '13874410370', 'employee_name': '胡森林', 'gmt_modified': '2026-04-24 11:15:49', 'ext_info': '{}', 'enterprise_id': '2088480767913636', 'iot_face_status': '2', 'department_list': [{'department_id': '1001163032541324', 'department_name': '湖南花米惠科技有限公司'}], 'employee_id': '2300063240699375', 'activate': 'UNACTIVATED', 'action': 'EMPLOYEE_ADD', 'change_time': '2026-04-24 11:15:49'}
  58. """
  59. enterprise_id: str = Field(..., description="企业id")
  60. employee_id: str = Field(..., description="员工id")
  61. action: str | None = Field(None, description="变更动作")
  62. change_time: str | None = Field(None, description="变更时间")
  63. activate: str | None = Field(None, description="激活状态")
  64. department_list: list | None = Field(None, description="部门列表")
  65. role_list: list | None = Field(None, description="角色列表")
  66. mobile: str | None = Field(None, description="手机号")
  67. email: str | None = Field(None, description="邮箱")
  68. open_id: str | None = Field(None, description="open_id")
  69. employee_name: str | None = Field(None, description="员工姓名")
  70. iot_face_status: str | None = Field(None, description="人脸状态状态")
  71. class VoucherChangeContent(BaseModel):
  72. """凭证变动通知内容 (alipay.commerce.ec.voucher.change.notify)"""
  73. # 必选字段
  74. account_id: str = Field(..., description="共同账户ID")
  75. pay_no: str = Field(..., description="交易流水号(支付宝账单号)")
  76. voucher_type: str = Field(..., description="凭证类型: Ticket/Multimedia/Compliance")
  77. notify_reason: str = Field(..., description="通知原因: INVOICE_CREATE/INVOICE_INVALID/TICKET_BIND")
  78. notify_desc: str = Field(..., description="通知描述")
  79. # 二选一
  80. user_id: str | None = Field(None, description="用户支付宝UID")
  81. open_id: str | None = Field(None, description="用户open_id")
  82. # 可选
  83. ext_infos: str | None = Field(None, description="扩展参数,JSON格式")
  84. enterprise_id: str | None = Field(None, description="企业ID")
  85. employee_id: str | None = Field(None, description="员工ID")
  86. voucher_id: str | None = Field(None, description="凭证ID")
  87. # 本地补充
  88. action: str | None = Field(None, description="变动动作")
  89. out_biz_no: str | None = Field(None, description="外部业务号")
  90. change_time: str | None = Field(None, description="变动时间")
  91. class FundChangeContent(BaseModel):
  92. """资金变动通知内容"""
  93. enterprise_id: str = Field(..., description="企业id")
  94. account_book_id: str = Field(..., description="资金专户号")
  95. out_biz_no: str = Field(..., description="外部业务号")
  96. change_type: str = Field(..., description="变动类型: DEPOSIT/WITHDRAW/TRANSFER")
  97. status: str = Field(..., description="状态")
  98. amount: str | None = Field(None, description="金额")
  99. order_no: str | None = Field(None, description="支付宝订单号")
  100. error_code: str | None = Field(None, description="错误码")
  101. error_message: str | None = Field(None, description="错误信息")
  102. change_time: str = Field(..., description="变动时间")
  103. class ConsumeChangeContent(BaseModel):
  104. """企业码账单消息通知内容"""
  105. # 必选字段
  106. account_id: str = Field(..., description="共同账户ID")
  107. enterprise_id: str = Field(..., description="企业ID")
  108. employee_id: str = Field(..., description="员工ID")
  109. pay_no: str = Field(..., description="支付宝账单号")
  110. consume_type: str = Field(..., description="账单类型: CONSUME(消费账单)/REFUND(退款账单)")
  111. gmt_biz_create: str = Field(..., description="账单创建时间,格式:yyyy-MM-dd HH:mm:ss")
  112. consume_amount: str = Field(..., description="账单金额,2位小数,单位:元")
  113. notify_reason: str = Field(..., description="通知原因")
  114. notify_msg: str = Field(..., description="通知描述")
  115. # 二选一字段
  116. user_id: str | None = Field(None, description="员工支付宝UID")
  117. open_id: str | None = Field(None, description="员工open_id")
  118. # 条件必选字段(退款账单才有值)
  119. related_pay_no: str | None = Field(None, description="关联消费账单交易流水号")
  120. # 可选字段
  121. gmt_recieve_pay: str | None = Field(None, description="账单支付时间,格式:yyyy-MM-dd HH:mm:ss")
  122. peer_pay_amount: str | None = Field(None, description="企业代付金额,2位小数,单位:元")
  123. expense_rule_group_id: str | None = Field(None, description="费控规则ID")
  124. expense_scene_code: str | None = Field(None, description="费用场景")
  125. expense_type: str | None = Field(None, description="费用类型")
  126. expense_type_sub_category: str | None = Field(None, description="费用类型子类目")
  127. ext_infos: str | None = Field(None, description="扩展参数,JSON格式")
  128. class OrderChangeContent(BaseModel):
  129. """订单变动通知内容
  130. 接口名: alipay.ebpp.invoice.ecorder.order.changed
  131. 参考文档: https://opendocs.alipay.com/apis/03iu5y
  132. """
  133. order_no: str = Field(..., description="支付宝订单号")
  134. biz_out_no: str = Field(..., description="商户订单号")
  135. enterprise_id: str = Field(..., description="企业ID")
  136. employee_id: str | None = Field(None, description="员工ID")
  137. out_biz_no: str | None = Field(None, description="外部业务号")
  138. action: str | None = Field(None, description="变动动作")
  139. change_time: str | None = Field(None, description="变动时间")
  140. order_status: str | None = Field(None, description="订单状态")
  141. # 补充字段
  142. pay_no: str | None = Field(None, description="账单号")
  143. order_title: str | None = Field(None, description="订单标题")
  144. order_amount: str | None = Field(None, description="订单金额")
  145. trade_no: str | None = Field(None, description="交易号")
  146. merchant_id: str | None = Field(None, description="商户ID")
  147. merchant_name: str | None = Field(None, description="商户名称")
  148. shop_id: str | None = Field(None, description="店铺ID")
  149. shop_name: str | None = Field(None, description="店铺名称")
  150. gmt_create: str | None = Field(None, description="创建时间")
  151. gmt_payment: str | None = Field(None, description="支付时间")
  152. fund_channel: str | None = Field(None, description="资金渠道")
  153. order_type: str | None = Field(None, description="订单类型")
  154. invoice_status: str | None = Field(None, description="发票状态")
  155. ext_info: str | None = Field(None, description="扩展信息")
  156. # 把order_no设置为pay_no的值
  157. @model_validator(mode="before")
  158. def set_order_no(cls, values):
  159. if not values.get("order_no") and values.get("pay_no"):
  160. values["order_no"] = values["pay_no"]
  161. return values
  162. class BillDetailQueryOut(BaseModel):
  163. """账单详情查询响应"""
  164. pay_no: str = Field(..., description="支付宝账单号")
  165. enterprise_id: str = Field(..., description="企业ID")
  166. employee_id: str = Field(..., description="员工ID")
  167. employee_name: str | None = Field(None, description="员工姓名")
  168. consume_amount: str = Field(..., description="账单金额")
  169. peer_pay_amount: str | None = Field(None, description="企业代付金额")
  170. employee_pay_amount: str | None = Field(None, description="员工自付金额")
  171. order_info: dict | None = Field(None, description="订单详情")
  172. voucher_list: list[dict] | None = Field(None, description="凭证列表")