Files
ljwx-health/ljwx-bigscreen/config.py
T
brunoandClaude Sonnet 4.5 6fcf4b4d83
CI/CD Pipeline / build-push (ljwx-admin) (push) Blocked by required conditions
CI/CD Pipeline / build-push (ljwx-bigscreen) (push) Blocked by required conditions
CI/CD Pipeline / build-push (ljwx-boot) (push) Blocked by required conditions
CI/CD Pipeline / update-deploy (push) Blocked by required conditions
CI/CD Pipeline / security-scan (push) Successful in 54s
CI/CD Pipeline / test-build-boot (push) Successful in 5m15s
CI/CD Pipeline / build-pr (ljwx-admin) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-bigscreen) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-boot) (push) Has been skipped
CI/CD Pipeline / test-admin (push) Successful in 7m16s
CI/CD Pipeline / test-bigscreen (push) Failing after 29m38s
docs: 深度分析 API 架构并准备激进式重构
## 主要变更

### 1. API 架构分析文档
- 创建 api-architecture-analysis.md:深度分析当前 API 架构混乱问题
- 发现核心问题:93% API 无版本号,命名规范混乱,功能重复
- 提供渐进式和激进式两种重构方案

### 2. OpenAPI 契约维护文档
- 创建 openapi-maintenance-workflow.md:OpenAPI 维护流程规范
- 创建 v1-api-contract-validation.md:V1 API 契约验证报告
- 创建 v1-api-quick-reference.md:V1 API 快速参考指南

### 3. ljwx-bigscreen API 迁移
- 修复 BigscreenApiV1Controller 中 userId 参数 required 问题
- 更新 ljwx-bigscreen 调用正确的 API 路径
- 修复 session key 不一致问题(customer_id vs customerId)
- 添加 boot_api_client 统一 HTTP 客户端

### 4. 日志和配置优化
- 添加 StaticFileFilter 过滤 304 日志
- 修复 SQLAlchemy ROLLBACK 警告
- 优化 ljwx-bigscreen 日志格式

### 5. 数据库迁移
- 添加 V1.9.0 Flyway 迁移脚本(geofence alert 字段)
- 创建本地数据库迁移指南

### 6. Claude Code 配置
- 添加 context-guard.md 和 task-type-detector.md
- 创建 QUICK_REFERENCE.md 决策流程图
- 优化 CLAUDE.md 文档管理规范

## 下一步
- 启动激进式 API 重构
- 统一 ljwx-admin、ljwx-watch、ljwx-phone API
- 维护 OpenAPI 契约

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 23:27:02 +08:00

227 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
应用配置类
"""
import os
import sys
import logging
import json
from datetime import datetime
from urllib.parse import quote_plus
class JSONFormatter(logging.Formatter):
"""JSON 格式日志 Formatter(用于 Loki 收集)"""
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat() + 'Z',
'level': record.levelname,
'service': 'ljwx-bigscreen',
'logger': record.name,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno,
'environment': os.getenv('ENV', 'production')
}
# 添加 OpenTelemetry trace context (如果存在)
if hasattr(record, 'otelTraceID'):
log_data['trace_id'] = record.otelTraceID
if hasattr(record, 'otelSpanID'):
log_data['span_id'] = record.otelSpanID
# 添加异常信息
if record.exc_info:
log_data['exception'] = self.formatException(record.exc_info)
# 添加自定义字段
if hasattr(record, 'extra_data'):
log_data['extra'] = record.extra_data
return json.dumps(log_data, ensure_ascii=False)
class StaticFileFilter(logging.Filter):
"""过滤静态文件请求日志(304 Not Modified)"""
def filter(self, record):
# 过滤 werkzeug 的静态文件 304 响应日志
if record.name == 'werkzeug':
message = record.getMessage()
# 过滤静态资源的 304 日志
if ' 304 ' in message and ('/static/' in message or '.js' in message or '.css' in message or '.png' in message):
return False
return True
def setup_logging(app, use_json=None):
"""
配置应用日志
:param app: Flask app 实例
:param use_json: 是否使用 JSON 格式(None 表示根据环境自动判断)
"""
# 自动判断是否使用 JSON 格式
if use_json is None:
env = os.getenv('ENV', 'development').lower()
use_json = env in ['production', 'prod', 'docker', 'k8s']
# 清除默认 handlers
app.logger.handlers.clear()
# 创建 handler
handler = logging.StreamHandler(sys.stdout)
if use_json:
# 生产环境:使用 JSON 格式(便于 Loki 解析)
handler.setFormatter(JSONFormatter())
else:
# 开发环境:使用易读的纯文本格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
handler.setFormatter(formatter)
# 添加静态文件过滤器
handler.addFilter(StaticFileFilter())
# 设置日志级别
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
handler.setLevel(getattr(logging, log_level, logging.INFO))
app.logger.setLevel(getattr(logging, log_level, logging.INFO))
app.logger.addHandler(handler)
# 配置 werkzeug 日志(Flask 内置服务器日志)
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.handlers.clear()
werkzeug_logger.addHandler(handler)
werkzeug_logger.addFilter(StaticFileFilter())
werkzeug_logger.setLevel(getattr(logging, log_level, logging.INFO))
# 配置 SQLAlchemy 日志(减少 ROLLBACK 日志)
sqlalchemy_logger = logging.getLogger('sqlalchemy.engine.Engine')
sqlalchemy_logger.handlers.clear()
# 只在 DEBUG 模式下显示 SQLAlchemy 日志
if log_level == 'DEBUG':
sqlalchemy_logger.addHandler(handler)
sqlalchemy_logger.setLevel(logging.INFO)
else:
sqlalchemy_logger.setLevel(logging.WARNING)
# 日志配置完成提示
if use_json:
app.logger.info('Logging configured with JSON format for production')
else:
app.logger.info('Logging configured with text format for development')
class Config:
"""基础配置"""
# 密钥
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
# 数据库配置
# 支持两种环境变量命名:DB_* (K8s) 和 MYSQL_* (本地)
MYSQL_HOST = os.getenv('DB_HOST', os.getenv('MYSQL_HOST', '127.0.0.1'))
MYSQL_PORT = os.getenv('DB_PORT', os.getenv('MYSQL_PORT', '3306'))
MYSQL_USER = os.getenv('SPRING_DATASOURCE_USERNAME', os.getenv('MYSQL_USER', 'root'))
MYSQL_PASSWORD = os.getenv('SPRING_DATASOURCE_PASSWORD', os.getenv('MYSQL_PASSWORD', '123456'))
MYSQL_DATABASE = os.getenv('DB_NAME', os.getenv('MYSQL_DATABASE', 'test'))
# URL编码密码
MYSQL_PASSWORD_ENCODED = quote_plus(MYSQL_PASSWORD)
# SQLAlchemy配置
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD_ENCODED}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 30,
'pool_recycle': 3600,
'pool_pre_ping': True,
'max_overflow': 50,
'pool_timeout': 30
}
# Redis配置
REDIS_HOST = os.getenv('REDIS_HOST', '127.0.0.1')
REDIS_PORT = os.getenv('REDIS_PORT', '6379')
REDIS_DB = os.getenv('REDIS_DB', '0')
REDIS_PASSWORD = os.getenv('SPRING_REDIS_PASSWORD', os.getenv('REDIS_PASSWORD', ''))
if REDIS_PASSWORD:
REDIS_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
else:
REDIS_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
# 应用配置
APP_HOST = os.getenv('APP_HOST', '0.0.0.0')
APP_PORT = os.getenv('APP_PORT', '5225')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
# UI定制
BIGSCREEN_TITLE = os.getenv('BIGSCREEN_TITLE', '智能健康监测大屏')
COMPANY_NAME = os.getenv('COMPANY_NAME', '灵境万象')
COMPANY_LOGO_URL = os.getenv('COMPANY_LOGO_URL', '/static/images/logo.png')
THEME_COLOR = os.getenv('THEME_COLOR', '#00ff9d')
BACKGROUND_COLOR = os.getenv('BACKGROUND_COLOR', '#0a1525')
BIGSCREEN_VERSION = os.getenv('BIGSCREEN_VERSION', 'v1.0.0-lite')
PERSONAL_SCREEN_VERSION = os.getenv('PERSONAL_SCREEN_VERSION', 'v2.0')
# 微信告警配置
WECHAT_ALERT_ENABLED = os.getenv('WECHAT_ALERT_ENABLED', 'False') == 'True'
WECHAT_APP_ID = os.getenv('WECHAT_APP_ID', '')
WECHAT_APP_SECRET = os.getenv('WECHAT_APP_SECRET', '')
WECHAT_TEMPLATE_ID = os.getenv('WECHAT_TEMPLATE_ID', '')
# 企业微信配置
CORP_WECHAT_ENABLED = os.getenv('CORP_WECHAT_ENABLED', 'False') == 'True'
CORP_ID = os.getenv('CORP_ID', '')
CORP_SECRET = os.getenv('CORP_SECRET', '')
CORP_AGENT_ID = os.getenv('CORP_AGENT_ID', '')
# 性能配置
BATCH_SIZE = 200
BATCH_TIMEOUT = 3
ASYNC_WORKERS = 20
REDIS_PIPELINE_SIZE = 100
CACHE_TTL = 300 # 5分钟
# Flask-Session 配置(用于 SSO 认证)
SESSION_TYPE = 'redis' # 使用 Redis 存储 session
SESSION_REDIS = None # 将在 app 初始化时设置
SESSION_KEY_PREFIX = 'bigscreen:session:' # Session key 前缀
SESSION_PERMANENT = True # Session 持久化
PERMANENT_SESSION_LIFETIME = 28800 # 8 小时(秒)
SESSION_USE_SIGNER = True # 签名 session cookie
SESSION_COOKIE_NAME = 'bigscreen_session' # Cookie 名称
SESSION_COOKIE_HTTPONLY = True # HttpOnly cookie(防止 XSS)
SESSION_COOKIE_SECURE = False # 开发环境使用 HTTP(生产环境需要 HTTPS)
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF 保护
# ljwx-boot API 配置
BOOT_API_URL = os.getenv('BOOT_API_URL', 'http://localhost:9998')
class DevelopmentConfig(Config):
"""开发环境配置"""
DEBUG = True
SQLALCHEMY_ECHO = True
class ProductionConfig(Config):
"""生产环境配置"""
DEBUG = False
SQLALCHEMY_ECHO = False
SESSION_COOKIE_SECURE = True # 生产环境强制 HTTPS
class TestingConfig(Config):
"""测试环境配置"""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
SQLALCHEMY_ENGINE_OPTIONS = {} # SQLite doesn't support pool settings