Fix API compatibility and add user/role/permission and asset import/export

This commit is contained in:
2026-01-25 23:36:23 +08:00
commit 501d11e14e
371 changed files with 68853 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
"""
工具模块
"""
from app.utils.redis_client import redis_client, init_redis, close_redis, RedisClient
__all__ = ["redis_client", "init_redis", "close_redis", "RedisClient"]

View File

@@ -0,0 +1,97 @@
"""
资产编码生成工具
使用PostgreSQL Advisory Lock保证并发安全
"""
from datetime import datetime
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
async def generate_asset_code(db: AsyncSession) -> str:
"""
生成资产编码
格式: AS + YYYYMMDD + 流水号(4位)
示例: AS202501240001
使用PostgreSQL Advisory Lock保证并发安全
Args:
db: 数据库会话
Returns:
资产编码
"""
# 获取当前日期字符串
date_str = datetime.now().strftime("%Y%m%d")
prefix = f"AS{date_str}"
# 使用Advisory Lock保证并发安全
# 使用日期作为锁ID避免不同日期的锁冲突
lock_id = int(date_str)
try:
# 获取锁
await db.execute(text(f"SELECT pg_advisory_lock({lock_id})"))
# 查询今天最大的序号
result = await db.execute(
text("""
SELECT CAST(SUBSTRING(asset_code FROM 13 FOR 4) AS INTEGER) as max_seq
FROM assets
WHERE asset_code LIKE :prefix
AND deleted_at IS NULL
ORDER BY asset_code DESC
LIMIT 1
"""),
{"prefix": f"{prefix}%"}
)
row = result.fetchone()
max_seq = row[0] if row and row[0] else 0
# 生成新序号
new_seq = max_seq + 1
seq_str = f"{new_seq:04d}" # 补零到4位
# 组合编码
asset_code = f"{prefix}{seq_str}"
return asset_code
finally:
# 释放锁
await db.execute(text(f"SELECT pg_advisory_unlock({lock_id})"))
def validate_asset_code(asset_code: str) -> bool:
"""
验证资产编码格式
Args:
asset_code: 资产编码
Returns:
是否有效
"""
if not asset_code or len(asset_code) != 14:
return False
# 检查前缀
if not asset_code.startswith("AS"):
return False
# 检查日期部分
date_str = asset_code[2:10]
try:
datetime.strptime(date_str, "%Y%m%d")
except ValueError:
return False
# 检查序号部分
seq_str = asset_code[10:]
if not seq_str.isdigit():
return False
return True

View File

@@ -0,0 +1,86 @@
"""
二维码生成工具
"""
import os
import qrcode
from datetime import datetime
from pathlib import Path
from app.core.config import settings
def generate_qr_code(asset_code: str, save_path: str = None) -> str:
"""
生成资产二维码
Args:
asset_code: 资产编码
save_path: 保存路径(可选)
Returns:
二维码文件相对路径
"""
# 如果未指定保存路径,使用默认路径
if not save_path:
qr_dir = Path(settings.QR_CODE_DIR)
else:
qr_dir = Path(save_path)
# 确保目录存在
qr_dir.mkdir(parents=True, exist_ok=True)
# 生成文件名
filename = f"{asset_code}.png"
file_path = qr_dir / filename
# 创建二维码
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=settings.QR_CODE_BORDER,
)
qr.add_data(asset_code)
qr.make(fit=True)
# 生成图片
img = qr.make_image(fill_color="black", back_color="white")
# 保存文件
img.save(str(file_path))
# 返回相对路径
return f"{settings.QR_CODE_DIR}/{filename}"
def get_qr_code_url(asset_code: str) -> str:
"""
获取二维码URL
Args:
asset_code: 资产编码
Returns:
二维码URL
"""
filename = f"{asset_code}.png"
return f"/static/{settings.QR_CODE_DIR}/{filename}"
def delete_qr_code(asset_code: str) -> bool:
"""
删除二维码文件
Args:
asset_code: 资产编码
Returns:
是否删除成功
"""
try:
file_path = Path(settings.QR_CODE_DIR) / f"{asset_code}.png"
if file_path.exists():
file_path.unlink()
return True
return False
except Exception:
return False

View File

@@ -0,0 +1,219 @@
"""
Redis客户端工具类
"""
import json
import asyncio
import hashlib
from functools import wraps
from typing import Optional, Any, List, Callable
from redis.asyncio import Redis, ConnectionPool
from app.core.config import settings
class RedisClient:
"""Redis客户端"""
def __init__(self):
"""初始化Redis客户端"""
self.pool: Optional[ConnectionPool] = None
self.redis: Optional[Redis] = None
async def connect(self):
"""连接Redis"""
if not self.pool:
self.pool = ConnectionPool.from_url(
settings.REDIS_URL,
max_connections=settings.REDIS_MAX_CONNECTIONS,
decode_responses=True
)
self.redis = Redis(connection_pool=self.pool)
async def close(self):
"""关闭连接"""
if self.redis:
await self.redis.close()
if self.pool:
await self.pool.disconnect()
async def get(self, key: str) -> Optional[str]:
"""获取缓存"""
if not self.redis:
await self.connect()
return await self.redis.get(key)
async def set(
self,
key: str,
value: str,
expire: Optional[int] = None
) -> bool:
"""设置缓存"""
if not self.redis:
await self.connect()
return await self.redis.set(key, value, ex=expire)
async def delete(self, key: str) -> int:
"""删除缓存"""
if not self.redis:
await self.connect()
return await self.redis.delete(key)
async def exists(self, key: str) -> bool:
"""检查键是否存在"""
if not self.redis:
await self.connect()
return await self.redis.exists(key) > 0
async def expire(self, key: str, seconds: int) -> bool:
"""设置过期时间"""
if not self.redis:
await self.connect()
return await self.redis.expire(key, seconds)
async def keys(self, pattern: str) -> List[str]:
"""获取匹配的键"""
if not self.redis:
await self.connect()
return await self.redis.keys(pattern)
async def delete_pattern(self, pattern: str) -> int:
"""删除匹配的键"""
keys = await self.keys(pattern)
if keys:
return await self.redis.delete(*keys)
return 0
async def setex(self, key: str, time: int, value: str) -> bool:
"""设置缓存并指定过期时间(秒)"""
if not self.redis:
await self.connect()
return await self.redis.setex(key, time, value)
# JSON操作辅助方法
async def get_json(self, key: str) -> Optional[Any]:
"""获取JSON数据"""
value = await self.get(key)
if value:
try:
return json.loads(value)
except json.JSONDecodeError:
return value
return None
async def set_json(
self,
key: str,
value: Any,
expire: Optional[int] = None
) -> bool:
"""设置JSON数据"""
json_str = json.dumps(value, ensure_ascii=False)
return await self.set(key, json_str, expire)
# 缓存装饰器
def cache(self, key_prefix: str, expire: int = 300):
"""
Redis缓存装饰器改进版
Args:
key_prefix: 缓存键前缀
expire: 过期时间默认300秒5分钟
Example:
@redis_client.cache("device_types", expire=1800)
async def get_device_types(...):
pass
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 使用MD5生成更稳定的缓存键
key_data = f"{key_prefix}:{str(args)}:{str(kwargs)}"
cache_key = f"cache:{hashlib.md5(key_data.encode()).hexdigest()}"
# 尝试从缓存获取
cached = await self.get_json(cache_key)
if cached is not None:
return cached
# 执行函数
result = await func(*args, **kwargs)
# 存入缓存
await self.set_json(cache_key, result, expire)
return result
return wrapper
return decorator
# 统计缓存辅助方法
async def cache_statistics(
self,
key: str,
data: Any,
expire: int = 600
):
"""缓存统计数据"""
return await self.set_json(key, data, expire)
async def get_cached_statistics(self, key: str) -> Optional[Any]:
"""获取缓存的统计数据"""
return await self.get_json(key)
async def invalidate_statistics_cache(self, pattern: str = "statistics:*"):
"""清除统计数据缓存"""
return await self.delete_pattern(pattern)
# 同步函数的异步缓存包装器
def cached_async(self, key_prefix: str, expire: int = 300):
"""
为同步函数提供异步缓存包装的装饰器
Args:
key_prefix: 缓存键前缀
expire: 过期时间默认300秒5分钟
Example:
@redis_client.cached_async("device_types", expire=1800)
async def cached_get_device_types(db, skip, limit, ...):
return device_type_service.get_device_types(...)
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 使用MD5生成更稳定的缓存键
key_data = f"{key_prefix}:{str(args)}:{str(kwargs)}"
cache_key = f"cache:{hashlib.md5(key_data.encode()).hexdigest()}"
# 尝试从缓存获取
cached = await self.get_json(cache_key)
if cached is not None:
return cached
# 执行函数
result = await func(*args, **kwargs)
# 存入缓存
await self.set_json(cache_key, result, expire)
return result
return wrapper
return decorator
# 创建全局实例
redis_client = RedisClient()
async def init_redis():
"""初始化Redis连接"""
await redis_client.connect()
async def close_redis():
"""关闭Redis连接"""
await redis_client.close()