config.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. from pydantic import field_validator
  2. from pydantic_settings import BaseSettings
  3. from typing import Optional
  4. from pathlib import Path
  5. import os
  6. # 已知弱/占位 JWT 密钥:启动时一律拒绝(勿再当作默认值)
  7. _INSECURE_JWT_SECRET_KEYS = frozenset(
  8. {
  9. "",
  10. "secret",
  11. "changeme",
  12. "change-me",
  13. "your-secret-key",
  14. "your-secret-key-change-in-production",
  15. "your-secret-key-change-in-production-please-use-random-string",
  16. }
  17. )
  18. _MIN_JWT_SECRET_KEY_LEN = 32
  19. class Settings(BaseSettings):
  20. """应用配置"""
  21. # 应用基础配置
  22. APP_NAME: str = "zkxw 管理骨架"
  23. DEBUG: bool = True
  24. API_V1_PREFIX: str = "/api"
  25. # 数据库配置
  26. DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang"
  27. # JWT配置(必须通过环境变量配置;禁止弱默认值)
  28. SECRET_KEY: str = ""
  29. ALGORITHM: str = "HS256"
  30. ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30天
  31. @field_validator("SECRET_KEY")
  32. @classmethod
  33. def secret_key_must_be_strong(cls, value: str) -> str:
  34. key = (value or "").strip()
  35. if key.lower() in _INSECURE_JWT_SECRET_KEYS or key in _INSECURE_JWT_SECRET_KEYS:
  36. raise ValueError(
  37. "SECRET_KEY 未配置或仍为弱占位值。"
  38. "请在环境文件中设置至少 32 字符的随机密钥,例如:"
  39. 'python -c "import secrets; print(secrets.token_urlsafe(48))"'
  40. )
  41. if len(key) < _MIN_JWT_SECRET_KEY_LEN:
  42. raise ValueError(
  43. f"SECRET_KEY 长度不足(当前 {len(key)},至少 {_MIN_JWT_SECRET_KEY_LEN})。"
  44. "请使用 secrets.token_urlsafe(48) 等方式生成。"
  45. )
  46. return key
  47. # CORS配置
  48. BACKEND_CORS_ORIGINS: list = [
  49. "http://localhost:5173", # art-design-pro 开发服务器
  50. "http://localhost:8080",
  51. "http://localhost:8081", # uni-app H5 端口被占用时常用
  52. "http://localhost:8082",
  53. "http://127.0.0.1:5173",
  54. "http://127.0.0.1:8080",
  55. "http://127.0.0.1:8081",
  56. "http://127.0.0.1:8082",
  57. "http://localhost:3000", # Web开发
  58. "http://localhost:3006", # Web开发
  59. "http://127.0.0.1:3000",
  60. "http://127.0.0.1:3006",
  61. ]
  62. # 文件存储配置
  63. STORAGE_TYPE: str = "local" # local, qiniu, oss, minio
  64. # 本地存储配置
  65. LOCAL_UPLOAD_DIR: str = "uploads"
  66. LOCAL_STORAGE_URL: str = "/uploads"
  67. # Redis 配置(用于登录会话与 refreshToken 吊销)
  68. # 未配置 REDIS_HOST 时,后端会保持兼容:仍然只校验 JWT,不强制会话吊销。
  69. REDIS_HOST: str = ""
  70. REDIS_PORT: int = 6379
  71. REDIS_DB: int = 0
  72. REDIS_PASSWORD: str = ""
  73. REDIS_TIMEOUT_SEC: int = 30
  74. REDIS_SSL_ENABLED: bool = False
  75. # 七牛云存储配置 (生产环境推荐)
  76. QINIU_ACCESS_KEY: str = ""
  77. QINIU_SECRET_KEY: str = ""
  78. QINIU_BUCKET: str = ""
  79. QINIU_DOMAIN: str = ""
  80. # 阿里云OSS配置 (可选)
  81. OSS_ACCESS_KEY_ID: str = ""
  82. OSS_ACCESS_KEY_SECRET: str = ""
  83. OSS_BUCKET: str = ""
  84. OSS_ENDPOINT: str = ""
  85. OSS_DOMAIN: str = ""
  86. # MinIO配置
  87. MINIO_ENDPOINT: str = ""
  88. MINIO_ACCESS_KEY: str = ""
  89. MINIO_SECRET_KEY: str = ""
  90. MINIO_BUCKET: str = ""
  91. MINIO_SECURE: bool = False
  92. MINIO_BASE_URL: str = ""
  93. # 微信支付配置 (生产环境必需)
  94. WXPAY_MCHID: str = "" # 商户号
  95. WXPAY_APPID: str = "" # 公众号AppID
  96. WXPAY_MINI_APPID: str = "" # 小程序AppID
  97. WXPAY_API_KEY: str = "" # API密钥 (V2)
  98. WXPAY_SANDBOX: bool = False # 是否使用沙箱环境
  99. WXPAY_NOTIFY_URL: str = "https://your-domain.com/api/payment/notify" # 支付回调地址
  100. WXPAY_REFUND_NOTIFY_URL: str = "https://your-domain.com/api/payment/refund-notify" # 退款回调地址
  101. # V3 API 证书配置
  102. WXPAY_CERT_PATH: str = "" # API证书(apiclient_cert.pem)路径
  103. WXPAY_PRIVATE_KEY_PATH: str = "" # API私钥(apiclient_key.pem)路径
  104. WXPAY_SERIAL_NO: str = "" # 证书序列号
  105. # SSL证书配置 (退款必需)
  106. WXPAY_SSL_CERT: str = "" # SSL证书路径
  107. WXPAY_SSL_KEY: str = "" # SSL私钥路径
  108. # 微信小程序配置
  109. WXPAY_SECRET: str = "" # 小程序Secret
  110. # 微信模板消息ID配置
  111. WX_TEMPLATE_ORDER_STATUS: str = "" # 订单状态通知模板ID
  112. WX_TEMPLATE_DELIVERY_NOTICE: str = "" # 配送通知模板ID
  113. WX_TEMPLATE_PAYMENT_SUCCESS: str = "" # 支付成功通知模板ID
  114. WX_TEMPLATE_REFUND_RESULT: str = "" # 退款结果通知模板ID
  115. WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID
  116. # 微信服务号(OA 审批/抄送通知)
  117. # 总开关:false 时不发送审批/抄送/申请人进度模板消息(模板未就绪或微信能力不可用时关闭)
  118. WX_MP_NOTIFY_ENABLED: bool = False
  119. WX_MP_APPID: str = ""
  120. WX_MP_SECRET: str = ""
  121. WX_MP_TEMPLATE_APPROVAL: str = ""
  122. WX_MP_TEMPLATE_CC: str = ""
  123. WX_MP_TEMPLATE_APPLICANT_PROGRESS: str = ""
  124. # 审批模板:申请事项/操作人(申请人)/当前环节/操作时间/申请类型(流程名)
  125. # 生产字段:thing12 / thing18 / thing7 / time17 / thing8
  126. WX_MP_TPL_FIELD_TITLE: str = "thing12"
  127. WX_MP_TPL_FIELD_APPLICANT: str = "thing18"
  128. WX_MP_TPL_FIELD_NODE: str = "thing7"
  129. WX_MP_TPL_FIELD_TIME: str = "time17"
  130. WX_MP_TPL_FIELD_PROCESS: str = "thing8"
  131. # 抄送模板「OA抄送通知」:业务名称/申请人/申请时间/流程名称(无当前环节)
  132. WX_MP_TPL_CC_FIELD_TITLE: str = "thing5"
  133. WX_MP_TPL_CC_FIELD_APPLICANT: str = "thing2"
  134. WX_MP_TPL_CC_FIELD_TIME: str = "time3"
  135. WX_MP_TPL_CC_FIELD_PROCESS: str = "thing1"
  136. # 申请人进度:业务名称/流程名称/审批人/审批结果(const)/审批时间
  137. WX_MP_TPL_PROGRESS_FIELD_BUSINESS: str = "thing5"
  138. WX_MP_TPL_PROGRESS_FIELD_PROCESS: str = "thing1"
  139. WX_MP_TPL_PROGRESS_FIELD_APPROVER: str = "thing6"
  140. WX_MP_TPL_PROGRESS_FIELD_RESULT: str = "const4"
  141. WX_MP_TPL_PROGRESS_FIELD_TIME: str = "time11"
  142. # 与 docs/sql/zkxw.sql 基线一致:登录按租户 + sys_user.user_no
  143. DEFAULT_TENANT_NO: str = "zkxw"
  144. # 已废弃:无角色用户固定为 guest,不可通过环境变量提升为 super_admin
  145. WEB_AUTH_DEFAULT_ROLE: str = "guest"
  146. # 登录 RSA:私钥 PEM(多实例建议配置同一私钥;留空则各 Worker 共用内置默认密钥 login_rsa_default.pem)
  147. LOGIN_RSA_PRIVATE_KEY_PEM: str = ""
  148. # 是否允许明文 username/password 登录(仅建议本地排障临时开启;生产务必 false)
  149. LOGIN_ALLOW_PLAINTEXT_LOGIN: bool = False
  150. # 加密载荷内时间戳允许偏差(秒),防简单重放
  151. LOGIN_ENCRYPT_TS_SKEW_SEC: int = 300
  152. # 临时文件目录
  153. TEMP_FILE_DIR: str = "temp_files"
  154. # 全量 API 请求日志(中间件写入 sys_api_request_log)
  155. API_REQUEST_LOG_ENABLED: bool = True
  156. API_REQUEST_LOG_MAX_BODY: int = 65536
  157. API_REQUEST_LOG_MAX_BODY_BYTES: int = 131072
  158. # Web API 请求/响应加密配置
  159. API_ENCRYPT_ENABLED: bool = False
  160. API_ENCRYPT_FAIL_OPEN: bool = True
  161. API_ENCRYPT_FIXED_SECRET: str = ""
  162. API_ENCRYPT_TS_SKEW_SEC: int = 300
  163. API_ENCRYPT_NONCE_TTL_SEC: int = 300
  164. WX_QR_SESSION_TTL_SEC: int = 180
  165. WX_QR_ENV_VERSION: str = "release" # release | trial | develop
  166. WX_QR_PAGE: str = "pages/login/web-qr/index"
  167. API_ENCRYPT_EXCLUDE_PATHS: str = (
  168. "/,"
  169. "/health,"
  170. "/docs,"
  171. "/openapi.json,"
  172. "/api/web/auth/login-rsa-public-key,"
  173. "/api/web/auth/login,"
  174. "/api/web/auth/refresh-token,"
  175. "/api/mini/auth/wechat-login,"
  176. "/api/web/admin/attachment/upload,"
  177. "/api/web/admin/products/upload-image,"
  178. "/api/web/admin/categories/upload-image,"
  179. "/api/web/admin/users/upload-avatar,"
  180. "/api/web/auth/upload-avatar,"
  181. "/api/invoice/download,"
  182. "/api/web/auth/wx-qr/create,"
  183. "/api/web/auth/wx-qr/status,"
  184. "/api/mini/auth/web-qr-scanned,"
  185. "/api/mini/auth/web-qr-confirm"
  186. )
  187. # 中国节假日日历(ailcc)同步
  188. CN_HOLIDAY_API_BASE: str = "https://holiday.ailcc.com"
  189. CN_HOLIDAY_HTTP_TIMEOUT_SEC: int = 15
  190. CN_HOLIDAY_HTTP_RETRIES: int = 2
  191. class Config:
  192. # 支持通过 ENV_FILE 指定环境文件;未指定时回退到 backend/.env
  193. _default_env_file = Path(__file__).resolve().parents[2] / ".env"
  194. env_file = os.getenv("ENV_FILE", str(_default_env_file))
  195. case_sensitive = True
  196. settings = Settings()