403 lines
19 KiB
Python
403 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Database Service
|
||
数据库服务 - API v2
|
||
基于原项目的数据库服务实现
|
||
"""
|
||
|
||
import logging
|
||
import os
|
||
import json
|
||
import time
|
||
import traceback
|
||
from typing import Dict, Any, Optional, List
|
||
from pathlib import Path
|
||
|
||
try:
|
||
import mysql.connector
|
||
from mysql.connector import pooling
|
||
HAS_MYSQL = True
|
||
except ImportError:
|
||
HAS_MYSQL = False
|
||
mysql = None
|
||
pooling = None
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class DatabaseService:
|
||
"""数据库服务类"""
|
||
|
||
def __init__(self, db_config_path: str = None):
|
||
"""
|
||
初始化数据库服务
|
||
|
||
Args:
|
||
db_config_path: 数据库配置文件路径
|
||
"""
|
||
self.db_pool = None
|
||
self.config = None
|
||
|
||
if not HAS_MYSQL:
|
||
logger.warning("mysql-connector-python 未安装,将使用模拟数据")
|
||
self._init_mock_data()
|
||
return
|
||
|
||
# 加载数据库配置
|
||
self.config = self._load_db_config(db_config_path)
|
||
|
||
if self.config:
|
||
self.db_pool = self._init_db_pool()
|
||
|
||
# 如果数据库连接失败,回退到模拟数据
|
||
if not self.db_pool:
|
||
logger.warning("数据库连接失败,将使用模拟数据")
|
||
self._init_mock_data()
|
||
|
||
def _load_db_config(self, config_path: str = None) -> Optional[Dict[str, Any]]:
|
||
"""加载数据库配置"""
|
||
if not config_path:
|
||
# 默认配置路径
|
||
project_root = Path(__file__).parent.parent.parent
|
||
config_path = project_root / "config" / "database.json"
|
||
|
||
try:
|
||
if Path(config_path).exists():
|
||
with open(config_path, 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
# 处理环境变量
|
||
return self._process_env_vars(config)
|
||
else:
|
||
logger.warning(f"数据库配置文件不存在: {config_path}")
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"加载数据库配置失败: {e}")
|
||
return None
|
||
|
||
def _process_env_vars(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""处理环境变量"""
|
||
processed_config = {}
|
||
for key, value in config.items():
|
||
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
||
env_var = value[2:-1]
|
||
processed_config[key] = os.environ.get(env_var, "")
|
||
else:
|
||
processed_config[key] = value
|
||
|
||
return processed_config
|
||
|
||
def _init_db_pool(self):
|
||
"""初始化数据库连接池"""
|
||
if not self.config:
|
||
return None
|
||
|
||
# 连接尝试配置
|
||
connection_attempts = [
|
||
{"desc": "使用配置文件中的设置", "config": self.config},
|
||
{"desc": "使用明确的密码", "config": {**self.config, "password": "password"}},
|
||
{"desc": "使用空密码", "config": {**self.config, "password": ""}},
|
||
{"desc": "使用auth_plugin", "config": {**self.config, "auth_plugin": "mysql_native_password"}}
|
||
]
|
||
|
||
# 尝试不同的连接方式
|
||
for attempt in connection_attempts:
|
||
try:
|
||
# 打印连接信息(不包含密码)
|
||
connection_info = {k: v for k, v in attempt["config"].items() if k != 'password'}
|
||
logger.info(f"尝试连接数据库 ({attempt['desc']}): {connection_info}")
|
||
|
||
# 创建连接池
|
||
pool = pooling.MySQLConnectionPool(
|
||
pool_name=f"api_v2_pool_{int(time.time())}",
|
||
pool_size=10,
|
||
**attempt["config"]
|
||
)
|
||
|
||
# 测试连接
|
||
with pool.get_connection() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT 1")
|
||
cursor.fetchall()
|
||
|
||
logger.info(f"数据库连接池初始化成功 ({attempt['desc']})")
|
||
return pool
|
||
except Exception as e:
|
||
error_details = traceback.format_exc()
|
||
logger.error(f"数据库连接尝试 ({attempt['desc']}) 失败: {e}\n{error_details}")
|
||
|
||
logger.error("所有数据库连接尝试都失败了")
|
||
return None
|
||
|
||
def _init_mock_data(self):
|
||
"""初始化模拟数据"""
|
||
self.mock_data = {
|
||
"scenic_spots": [
|
||
{"id": 1, "name": "三亚亚龙湾", "address": "海南省三亚市", "description": "天下第一湾", "advantage": "海水清澈,沙滩细软", "highlight": "热带风情", "isPublic": True, "userId": 1},
|
||
{"id": 2, "name": "北京故宫", "address": "北京市东城区", "description": "明清皇宫", "advantage": "历史悠久", "highlight": "古建筑群", "isPublic": True, "userId": 1},
|
||
{"id": 3, "name": "杭州西湖", "address": "浙江省杭州市", "description": "人间天堂", "advantage": "风景秀丽", "highlight": "湖光山色", "isPublic": True, "userId": 1},
|
||
{"id": 4, "name": "桂林山水", "address": "广西桂林市", "description": "甲天下", "advantage": "山清水秀", "highlight": "喀斯特地貌", "isPublic": True, "userId": 1},
|
||
{"id": 5, "name": "黄山", "address": "安徽省黄山市", "description": "天下第一奇山", "advantage": "奇松怪石", "highlight": "云海日出", "isPublic": True, "userId": 1}
|
||
],
|
||
"products": [
|
||
{"id": 1, "productName": "亚龙湾度假套餐", "originPrice": 2999.0, "realPrice": 1999.0, "packageInfo": "3天2晚", "detailedDescription": "包含酒店住宿、早餐、接送服务", "keyAdvantages": "五星级酒店,私人海滩", "highlights": "海景房,无边泳池", "usageRules": "需提前3天预订", "surcharge": "节假日加价200元", "reservation": "电话预订", "userId": 1},
|
||
{"id": 2, "productName": "故宫门票", "originPrice": 60.0, "realPrice": 60.0, "packageInfo": "成人票", "detailedDescription": "故宫博物院参观门票", "keyAdvantages": "免排队入场", "highlights": "珍贵文物", "usageRules": "当日有效", "surcharge": "无", "reservation": "网上预订", "userId": 1},
|
||
{"id": 3, "productName": "西湖游船票", "originPrice": 55.0, "realPrice": 45.0, "packageInfo": "单程票", "detailedDescription": "西湖景区游船体验", "keyAdvantages": "湖心亭停靠", "highlights": "湖光山色", "usageRules": "当日有效", "surcharge": "无", "reservation": "现场购买", "userId": 1}
|
||
],
|
||
"styles": [
|
||
{"id": 1, "styleName": "活泼有趣", "styleDescription": "轻松愉快的语言风格,充满活力"},
|
||
{"id": 2, "styleName": "专业严谨", "styleDescription": "专业权威的表达方式,信息准确"},
|
||
{"id": 3, "styleName": "温馨感人", "styleDescription": "温暖感人的情感表达,富有人情味"},
|
||
{"id": 4, "styleName": "幽默风趣", "styleDescription": "诙谐幽默的表达方式,富有趣味性"},
|
||
{"id": 5, "styleName": "文艺清新", "styleDescription": "文艺范的表达方式,清新脱俗"}
|
||
],
|
||
"audiences": [
|
||
{"id": 1, "audienceName": "年轻情侣", "audienceDescription": "18-35岁的情侣群体,追求浪漫体验"},
|
||
{"id": 2, "audienceName": "亲子家庭", "audienceDescription": "有孩子的家庭群体,注重亲子互动"},
|
||
{"id": 3, "audienceName": "商务人士", "audienceDescription": "商务出行的专业人群,时间宝贵"},
|
||
{"id": 4, "audienceName": "年轻学生", "audienceDescription": "大学生群体,预算有限但充满活力"},
|
||
{"id": 5, "audienceName": "中老年群体", "audienceDescription": "50岁以上群体,注重舒适和安全"}
|
||
]
|
||
}
|
||
|
||
def is_available(self) -> bool:
|
||
"""检查数据库服务是否可用"""
|
||
if self.db_pool:
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT 1")
|
||
cursor.fetchall()
|
||
return True
|
||
except:
|
||
return False
|
||
return False
|
||
|
||
def get_scenic_spots(self, limit: int = 10, offset: int = 0, search: str = None) -> tuple[List[Dict[str, Any]], int]:
|
||
"""获取景区列表"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_scenic_spots_from_db(limit, offset, search)
|
||
else:
|
||
return self._get_scenic_spots_from_mock(limit, offset, search)
|
||
|
||
def _get_scenic_spots_from_db(self, limit: int, offset: int, search: str = None) -> tuple[List[Dict[str, Any]], int]:
|
||
"""从数据库获取景区列表"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 构建查询语句
|
||
base_query = "SELECT * FROM scenic_spots WHERE isPublic = 1"
|
||
count_query = "SELECT COUNT(*) as total FROM scenic_spots WHERE isPublic = 1"
|
||
params = []
|
||
|
||
if search:
|
||
base_query += " AND name LIKE %s"
|
||
count_query += " AND name LIKE %s"
|
||
search_param = f"%{search}%"
|
||
params.append(search_param)
|
||
|
||
# 获取总数
|
||
cursor.execute(count_query, params)
|
||
total = cursor.fetchone()['total']
|
||
|
||
# 获取分页数据
|
||
base_query += " ORDER BY id LIMIT %s OFFSET %s"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(base_query, params)
|
||
spots = cursor.fetchall()
|
||
|
||
return spots, total
|
||
except Exception as e:
|
||
logger.error(f"从数据库获取景区列表失败: {e}")
|
||
return self._get_scenic_spots_from_mock(limit, offset, search)
|
||
|
||
def _get_scenic_spots_from_mock(self, limit: int, offset: int, search: str = None) -> tuple[List[Dict[str, Any]], int]:
|
||
"""从模拟数据获取景区列表"""
|
||
spots = self.mock_data["scenic_spots"]
|
||
|
||
# 搜索过滤
|
||
if search:
|
||
spots = [spot for spot in spots if search.lower() in spot["name"].lower()]
|
||
|
||
# 分页
|
||
total = len(spots)
|
||
spots = spots[offset:offset + limit]
|
||
|
||
return spots, total
|
||
|
||
def get_scenic_spot_by_id(self, spot_id: int) -> Optional[Dict[str, Any]]:
|
||
"""根据ID获取景区详情"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_scenic_spot_by_id_from_db(spot_id)
|
||
else:
|
||
return self._get_scenic_spot_by_id_from_mock(spot_id)
|
||
|
||
def _get_scenic_spot_by_id_from_db(self, spot_id: int) -> Optional[Dict[str, Any]]:
|
||
"""从数据库根据ID获取景区详情"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
cursor.execute("SELECT * FROM scenic_spots WHERE id = %s AND isPublic = 1", (spot_id,))
|
||
return cursor.fetchone()
|
||
except Exception as e:
|
||
logger.error(f"从数据库获取景区详情失败: {e}")
|
||
return self._get_scenic_spot_by_id_from_mock(spot_id)
|
||
|
||
def _get_scenic_spot_by_id_from_mock(self, spot_id: int) -> Optional[Dict[str, Any]]:
|
||
"""从模拟数据根据ID获取景区详情"""
|
||
return next((s for s in self.mock_data["scenic_spots"] if s["id"] == spot_id), None)
|
||
|
||
def get_products(self, limit: int = 10, offset: int = 0, search: str = None) -> tuple[List[Dict[str, Any]], int]:
|
||
"""获取产品列表"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_products_from_db(limit, offset, search)
|
||
else:
|
||
return self._get_products_from_mock(limit, offset, search)
|
||
|
||
def _get_products_from_db(self, limit: int, offset: int, search: str = None) -> tuple[List[Dict[str, Any]], int]:
|
||
"""从数据库获取产品列表"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
base_query = "SELECT * FROM products"
|
||
count_query = "SELECT COUNT(*) as total FROM products"
|
||
params = []
|
||
|
||
if search:
|
||
base_query += " WHERE productName LIKE %s"
|
||
count_query += " WHERE productName LIKE %s"
|
||
search_param = f"%{search}%"
|
||
params.append(search_param)
|
||
|
||
# 获取总数
|
||
cursor.execute(count_query, params)
|
||
total = cursor.fetchone()['total']
|
||
|
||
# 获取分页数据
|
||
base_query += " ORDER BY id LIMIT %s OFFSET %s"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(base_query, params)
|
||
products = cursor.fetchall()
|
||
|
||
return products, total
|
||
except Exception as e:
|
||
logger.error(f"从数据库获取产品列表失败: {e}")
|
||
return self._get_products_from_mock(limit, offset, search)
|
||
|
||
def _get_products_from_mock(self, limit: int, offset: int, search: str = None) -> tuple[List[Dict[str, Any]], int]:
|
||
"""从模拟数据获取产品列表"""
|
||
products = self.mock_data["products"]
|
||
|
||
# 搜索过滤
|
||
if search:
|
||
products = [p for p in products if search.lower() in p["productName"].lower()]
|
||
|
||
# 分页
|
||
total = len(products)
|
||
products = products[offset:offset + limit]
|
||
|
||
return products, total
|
||
|
||
def get_product_by_id(self, product_id: int) -> Optional[Dict[str, Any]]:
|
||
"""根据ID获取产品详情"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_product_by_id_from_db(product_id)
|
||
else:
|
||
return self._get_product_by_id_from_mock(product_id)
|
||
|
||
def _get_product_by_id_from_db(self, product_id: int) -> Optional[Dict[str, Any]]:
|
||
"""从数据库根据ID获取产品详情"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
cursor.execute("SELECT * FROM products WHERE id = %s", (product_id,))
|
||
return cursor.fetchone()
|
||
except Exception as e:
|
||
logger.error(f"从数据库获取产品详情失败: {e}")
|
||
return self._get_product_by_id_from_mock(product_id)
|
||
|
||
def _get_product_by_id_from_mock(self, product_id: int) -> Optional[Dict[str, Any]]:
|
||
"""从模拟数据根据ID获取产品详情"""
|
||
return next((p for p in self.mock_data["products"] if p["id"] == product_id), None)
|
||
|
||
def get_styles(self) -> List[Dict[str, Any]]:
|
||
"""获取风格列表"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_styles_from_db()
|
||
else:
|
||
return self.mock_data["styles"]
|
||
|
||
def _get_styles_from_db(self) -> List[Dict[str, Any]]:
|
||
"""从数据库获取风格列表"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
cursor.execute("SELECT * FROM styles ORDER BY id")
|
||
return cursor.fetchall()
|
||
except Exception as e:
|
||
logger.error(f"从数据库获取风格列表失败: {e}")
|
||
return self.mock_data["styles"]
|
||
|
||
def get_audiences(self) -> List[Dict[str, Any]]:
|
||
"""获取受众列表"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_audiences_from_db()
|
||
else:
|
||
return self.mock_data["audiences"]
|
||
|
||
def _get_audiences_from_db(self) -> List[Dict[str, Any]]:
|
||
"""从数据库获取受众列表"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
cursor.execute("SELECT * FROM audiences ORDER BY id")
|
||
return cursor.fetchall()
|
||
except Exception as e:
|
||
logger.error(f"从数据库获取受众列表失败: {e}")
|
||
return self.mock_data["audiences"]
|
||
|
||
def get_scenic_spots_by_ids(self, spot_ids: List[int]) -> List[Dict[str, Any]]:
|
||
"""批量获取景区信息"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_scenic_spots_by_ids_from_db(spot_ids)
|
||
else:
|
||
return [s for s in self.mock_data["scenic_spots"] if s["id"] in spot_ids]
|
||
|
||
def _get_scenic_spots_by_ids_from_db(self, spot_ids: List[int]) -> List[Dict[str, Any]]:
|
||
"""从数据库批量获取景区信息"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
placeholders = ','.join(['%s'] * len(spot_ids))
|
||
query = f"SELECT * FROM scenic_spots WHERE id IN ({placeholders}) AND isPublic = 1"
|
||
cursor.execute(query, spot_ids)
|
||
return cursor.fetchall()
|
||
except Exception as e:
|
||
logger.error(f"从数据库批量获取景区信息失败: {e}")
|
||
return [s for s in self.mock_data["scenic_spots"] if s["id"] in spot_ids]
|
||
|
||
def get_products_by_ids(self, product_ids: List[int]) -> List[Dict[str, Any]]:
|
||
"""批量获取产品信息"""
|
||
if self.db_pool and self.is_available():
|
||
return self._get_products_by_ids_from_db(product_ids)
|
||
else:
|
||
return [p for p in self.mock_data["products"] if p["id"] in product_ids]
|
||
|
||
def _get_products_by_ids_from_db(self, product_ids: List[int]) -> List[Dict[str, Any]]:
|
||
"""从数据库批量获取产品信息"""
|
||
try:
|
||
with self.db_pool.get_connection() as conn:
|
||
cursor = conn.cursor(dictionary=True)
|
||
placeholders = ','.join(['%s'] * len(product_ids))
|
||
query = f"SELECT * FROM products WHERE id IN ({placeholders})"
|
||
cursor.execute(query, product_ids)
|
||
return cursor.fetchall()
|
||
except Exception as e:
|
||
logger.error(f"从数据库批量获取产品信息失败: {e}")
|
||
return [p for p in self.mock_data["products"] if p["id"] in product_ids] |