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
## 主要变更 ### 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>
227 lines
7.9 KiB
Python
227 lines
7.9 KiB
Python
"""
|
||
应用配置类
|
||
"""
|
||
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
|