from pydantic import field_validator from pydantic_settings import BaseSettings from typing import Optional from pathlib import Path import os # 已知弱/占位 JWT 密钥:启动时一律拒绝(勿再当作默认值) _INSECURE_JWT_SECRET_KEYS = frozenset( { "", "secret", "changeme", "change-me", "your-secret-key", "your-secret-key-change-in-production", "your-secret-key-change-in-production-please-use-random-string", } ) _MIN_JWT_SECRET_KEY_LEN = 32 class Settings(BaseSettings): """应用配置""" # 应用基础配置 APP_NAME: str = "zkxw 管理骨架" DEBUG: bool = True API_V1_PREFIX: str = "/api" # 数据库配置 DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang" # JWT配置(必须通过环境变量配置;禁止弱默认值) SECRET_KEY: str = "" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30天 @field_validator("SECRET_KEY") @classmethod def secret_key_must_be_strong(cls, value: str) -> str: key = (value or "").strip() if key.lower() in _INSECURE_JWT_SECRET_KEYS or key in _INSECURE_JWT_SECRET_KEYS: raise ValueError( "SECRET_KEY 未配置或仍为弱占位值。" "请在环境文件中设置至少 32 字符的随机密钥,例如:" 'python -c "import secrets; print(secrets.token_urlsafe(48))"' ) if len(key) < _MIN_JWT_SECRET_KEY_LEN: raise ValueError( f"SECRET_KEY 长度不足(当前 {len(key)},至少 {_MIN_JWT_SECRET_KEY_LEN})。" "请使用 secrets.token_urlsafe(48) 等方式生成。" ) return key # CORS配置 BACKEND_CORS_ORIGINS: list = [ "http://localhost:5173", # art-design-pro 开发服务器 "http://localhost:8080", "http://localhost:8081", # uni-app H5 端口被占用时常用 "http://localhost:8082", "http://127.0.0.1:5173", "http://127.0.0.1:8080", "http://127.0.0.1:8081", "http://127.0.0.1:8082", "http://localhost:3000", # Web开发 "http://localhost:3006", # Web开发 "http://127.0.0.1:3000", "http://127.0.0.1:3006", ] # 文件存储配置 STORAGE_TYPE: str = "local" # local, qiniu, oss, minio # 本地存储配置 LOCAL_UPLOAD_DIR: str = "uploads" LOCAL_STORAGE_URL: str = "/uploads" # Redis 配置(用于登录会话与 refreshToken 吊销) # 未配置 REDIS_HOST 时,后端会保持兼容:仍然只校验 JWT,不强制会话吊销。 REDIS_HOST: str = "" REDIS_PORT: int = 6379 REDIS_DB: int = 0 REDIS_PASSWORD: str = "" REDIS_TIMEOUT_SEC: int = 30 REDIS_SSL_ENABLED: bool = False # 七牛云存储配置 (生产环境推荐) QINIU_ACCESS_KEY: str = "" QINIU_SECRET_KEY: str = "" QINIU_BUCKET: str = "" QINIU_DOMAIN: str = "" # 阿里云OSS配置 (可选) OSS_ACCESS_KEY_ID: str = "" OSS_ACCESS_KEY_SECRET: str = "" OSS_BUCKET: str = "" OSS_ENDPOINT: str = "" OSS_DOMAIN: str = "" # MinIO配置 MINIO_ENDPOINT: str = "" MINIO_ACCESS_KEY: str = "" MINIO_SECRET_KEY: str = "" MINIO_BUCKET: str = "" MINIO_SECURE: bool = False MINIO_BASE_URL: str = "" # 微信支付配置 (生产环境必需) WXPAY_MCHID: str = "" # 商户号 WXPAY_APPID: str = "" # 公众号AppID WXPAY_MINI_APPID: str = "" # 小程序AppID WXPAY_API_KEY: str = "" # API密钥 (V2) WXPAY_SANDBOX: bool = False # 是否使用沙箱环境 WXPAY_NOTIFY_URL: str = "https://your-domain.com/api/payment/notify" # 支付回调地址 WXPAY_REFUND_NOTIFY_URL: str = "https://your-domain.com/api/payment/refund-notify" # 退款回调地址 # V3 API 证书配置 WXPAY_CERT_PATH: str = "" # API证书(apiclient_cert.pem)路径 WXPAY_PRIVATE_KEY_PATH: str = "" # API私钥(apiclient_key.pem)路径 WXPAY_SERIAL_NO: str = "" # 证书序列号 # SSL证书配置 (退款必需) WXPAY_SSL_CERT: str = "" # SSL证书路径 WXPAY_SSL_KEY: str = "" # SSL私钥路径 # 微信小程序配置 WXPAY_SECRET: str = "" # 小程序Secret # 微信模板消息ID配置 WX_TEMPLATE_ORDER_STATUS: str = "" # 订单状态通知模板ID WX_TEMPLATE_DELIVERY_NOTICE: str = "" # 配送通知模板ID WX_TEMPLATE_PAYMENT_SUCCESS: str = "" # 支付成功通知模板ID WX_TEMPLATE_REFUND_RESULT: str = "" # 退款结果通知模板ID WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID # 微信服务号(OA 审批/抄送通知) # 总开关:false 时不发送审批/抄送/申请人进度模板消息(模板未就绪或微信能力不可用时关闭) WX_MP_NOTIFY_ENABLED: bool = False WX_MP_APPID: str = "" WX_MP_SECRET: str = "" WX_MP_TEMPLATE_APPROVAL: str = "" WX_MP_TEMPLATE_CC: str = "" WX_MP_TEMPLATE_APPLICANT_PROGRESS: str = "" # 审批模板:申请事项/操作人(申请人)/当前环节/操作时间/申请类型(流程名) # 生产字段:thing12 / thing18 / thing7 / time17 / thing8 WX_MP_TPL_FIELD_TITLE: str = "thing12" WX_MP_TPL_FIELD_APPLICANT: str = "thing18" WX_MP_TPL_FIELD_NODE: str = "thing7" WX_MP_TPL_FIELD_TIME: str = "time17" WX_MP_TPL_FIELD_PROCESS: str = "thing8" # 抄送模板「OA抄送通知」:业务名称/申请人/申请时间/流程名称(无当前环节) WX_MP_TPL_CC_FIELD_TITLE: str = "thing5" WX_MP_TPL_CC_FIELD_APPLICANT: str = "thing2" WX_MP_TPL_CC_FIELD_TIME: str = "time3" WX_MP_TPL_CC_FIELD_PROCESS: str = "thing1" # 申请人进度:业务名称/流程名称/审批人/审批结果(const)/审批时间 WX_MP_TPL_PROGRESS_FIELD_BUSINESS: str = "thing5" WX_MP_TPL_PROGRESS_FIELD_PROCESS: str = "thing1" WX_MP_TPL_PROGRESS_FIELD_APPROVER: str = "thing6" WX_MP_TPL_PROGRESS_FIELD_RESULT: str = "const4" WX_MP_TPL_PROGRESS_FIELD_TIME: str = "time11" # 与 docs/sql/zkxw.sql 基线一致:登录按租户 + sys_user.user_no DEFAULT_TENANT_NO: str = "zkxw" # 已废弃:无角色用户固定为 guest,不可通过环境变量提升为 super_admin WEB_AUTH_DEFAULT_ROLE: str = "guest" # 登录 RSA:私钥 PEM(多实例建议配置同一私钥;留空则各 Worker 共用内置默认密钥 login_rsa_default.pem) LOGIN_RSA_PRIVATE_KEY_PEM: str = "" # 是否允许明文 username/password 登录(仅建议本地排障临时开启;生产务必 false) LOGIN_ALLOW_PLAINTEXT_LOGIN: bool = False # 加密载荷内时间戳允许偏差(秒),防简单重放 LOGIN_ENCRYPT_TS_SKEW_SEC: int = 300 # 临时文件目录 TEMP_FILE_DIR: str = "temp_files" # 全量 API 请求日志(中间件写入 sys_api_request_log) API_REQUEST_LOG_ENABLED: bool = True API_REQUEST_LOG_MAX_BODY: int = 65536 API_REQUEST_LOG_MAX_BODY_BYTES: int = 131072 # Web API 请求/响应加密配置 API_ENCRYPT_ENABLED: bool = False API_ENCRYPT_FAIL_OPEN: bool = True API_ENCRYPT_FIXED_SECRET: str = "" API_ENCRYPT_TS_SKEW_SEC: int = 300 API_ENCRYPT_NONCE_TTL_SEC: int = 300 WX_QR_SESSION_TTL_SEC: int = 180 WX_QR_ENV_VERSION: str = "release" # release | trial | develop WX_QR_PAGE: str = "pages/login/web-qr/index" API_ENCRYPT_EXCLUDE_PATHS: str = ( "/," "/health," "/docs," "/openapi.json," "/api/web/auth/login-rsa-public-key," "/api/web/auth/login," "/api/web/auth/refresh-token," "/api/mini/auth/wechat-login," "/api/web/admin/attachment/upload," "/api/web/admin/products/upload-image," "/api/web/admin/categories/upload-image," "/api/web/admin/users/upload-avatar," "/api/web/auth/upload-avatar," "/api/invoice/download," "/api/web/auth/wx-qr/create," "/api/web/auth/wx-qr/status," "/api/mini/auth/web-qr-scanned," "/api/mini/auth/web-qr-confirm" ) # 中国节假日日历(ailcc)同步 CN_HOLIDAY_API_BASE: str = "https://holiday.ailcc.com" CN_HOLIDAY_HTTP_TIMEOUT_SEC: int = 15 CN_HOLIDAY_HTTP_RETRIES: int = 2 class Config: # 支持通过 ENV_FILE 指定环境文件;未指定时回退到 backend/.env _default_env_file = Path(__file__).resolve().parents[2] / ".env" env_file = os.getenv("ENV_FILE", str(_default_env_file)) case_sensitive = True settings = Settings()