锁定缴费校验失败时整单回滚,避免样本半成功落库;前端禁止删除锁定缴费样本,改码走更正编码。同步落地样本更正、采集微信通知、OA提及未读、细胞产品套餐与相关迁移文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
245 lines
9.5 KiB
Python
245 lines
9.5 KiB
Python
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 = ""
|
||
# 基础实验交付日前一天提醒(出库提醒模板)
|
||
WX_MP_TEMPLATE_BASIC_EXPERIMENT_DELIVERY: str = (
|
||
"oQNU3t0Q5kwc4KPJ8Z6yPA-k6qlsH9pikSCIb4eAwgM"
|
||
)
|
||
# 协议新增样本采集通知(工单类模板)
|
||
WX_MP_TEMPLATE_SAMPLE_COLLECT: str = (
|
||
"iOnQ9k2mavAOwFbp_6yH4lD-RCNmM_OiwyTgrJfU-_A"
|
||
)
|
||
WX_MP_SAMPLE_COLLECT_ROLE_NAME: 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"
|
||
# 基础实验交付提醒:客户名称/产品名称/件数/收货联系人/出库时间
|
||
WX_MP_TPL_BE_DELIVERY_FIELD_CUSTOMER: str = "thing6"
|
||
WX_MP_TPL_BE_DELIVERY_FIELD_PRODUCT: str = "thing10"
|
||
WX_MP_TPL_BE_DELIVERY_FIELD_QTY: str = "character_string8"
|
||
WX_MP_TPL_BE_DELIVERY_FIELD_CONTACT: str = "thing7"
|
||
WX_MP_TPL_BE_DELIVERY_FIELD_TIME: str = "time14"
|
||
# 样本采集通知:工单标题/发起人/发起时间/任务类型
|
||
WX_MP_TPL_SC_FIELD_TITLE: str = "thing2"
|
||
WX_MP_TPL_SC_FIELD_APPLICANT: str = "thing8"
|
||
WX_MP_TPL_SC_FIELD_TIME: str = "time3"
|
||
WX_MP_TPL_SC_FIELD_TYPE: str = "phrase10"
|
||
|
||
# 与 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()
|