增加组装推送消息的逻辑

This commit is contained in:
mark.tian
2025-12-08 09:49:36 +08:00
parent 038363a17e
commit 43ff4aa5f9
6 changed files with 1096 additions and 53 deletions

281
REDIS_SETUP.md Normal file
View File

@@ -0,0 +1,281 @@
# Redis 配置指南
## 概述
本应用使用 Redis 作为 Celery 的消息代理和结果后端。在启动应用前,需要确保 Redis 服务正常运行。
## Redis 安装
### Windows
1. **下载 Redis for Windows**
```bash
# 访问 https://github.com/microsoftarchive/redis/releases
# 下载最新的 .msi 文件并安装
```
2. **使用 WSL (推荐)**
```bash
# 在 WSL 中安装
sudo apt update
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server
```
### Linux
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server
# CentOS/RHEL
sudo yum install redis
sudo systemctl start redis
sudo systemctl enable redis
```
### macOS
```bash
# 使用 Homebrew
brew install redis
brew services start redis
```
## Redis 配置
### 基本配置
编辑 Redis 配置文件 `/etc/redis/redis.conf`:
```ini
# 设置密码(可选)
requirepass your_redis_password
# 设置最大内存
maxmemory 256mb
maxmemory-policy allkeys-lru
# 持久化配置
save 900 1
save 300 10
save 60 10000
```
### 环境变量配置
在 `.env` 文件中配置:
```bash
# Redis 配置
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0
# 如果设置了密码
# CELERY_BROKER_URL=redis://:your_password@localhost:6379/0
# CELERY_RESULT_BACKEND=redis://:your_password@localhost:6379/0
```
## Redis 服务管理
### 启动 Redis
```bash
# Windows
redis-server
# Linux (Systemd)
sudo systemctl start redis-server
# macOS (Homebrew)
brew services start redis
```
### 停止 Redis
```bash
# Linux
sudo systemctl stop redis-server
# macOS
brew services stop redis
# 手动停止
redis-cli shutdown
```
### 重启 Redis
```bash
# Linux
sudo systemctl restart redis-server
# macOS
brew services restart redis
```
## 连接测试
### 测试基本连接
```bash
# 使用 redis-cli
redis-cli ping
# 应该返回 PONG
# 使用项目测试脚本
python test_redis.py
```
### 检查 Redis 状态
```bash
# 查看连接信息
redis-cli info clients
# 查看内存使用
redis-cli info memory
# 查看数据库信息
redis-cli info keyspace
```
## 应用中的 Redis 集成
### 启动时检查
应用启动时会自动:
1. 验证 Redis 连接
2. 创建 Redis 客户端
3. 存储到应用状态中供其他组件使用
### 健康检查
访问 `/health` 端点可以查看 Redis 连接状态:
```json
{
"status": "healthy",
"redis": "connected"
}
```
### 故障排除
如果 Redis 连接失败,应用将无法启动。检查:
1. Redis 服务是否运行
2. 连接配置是否正确
3. 防火墙设置
4. 密码配置
## 生产环境建议
### 安全配置
1. **设置密码**
```ini
requirepass your_strong_password
```
2. **绑定特定 IP**
```ini
bind 127.0.0.1 10.0.0.1
```
3. **禁用危险命令**
```ini
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command KEYS ""
rename-command CONFIG ""
```
### 性能优化
1. **设置最大内存**
```ini
maxmemory 1gb
maxmemory-policy allkeys-lru
```
2. **持久化配置**
```ini
save 900 1
save 300 10
save 60 10000
```
3. **连接限制**
```ini
maxclients 1000
```
### 监控
1. **使用 INFO 命令**
```bash
redis-cli info server
redis-cli info memory
redis-cli info stats
```
2. **监控工具**
- RedisInsight
- Redis Commander
- 自定义监控脚本
## 常见问题
### Q: 应用启动失败,提示 Redis 连接错误
**A:** 检查以下几点:
1. Redis 服务是否启动:`redis-cli ping`
2. 配置文件中的 URL 是否正确
3. 防火墙是否阻止连接
4. Redis 密码配置是否匹配
### Q: Redis 内存占用过高
**A:**
1. 检查键的数量:`redis-cli dbsize`
2. 设置过期策略:`CONFIG SET maxmemory-policy allkeys-lru`
3. 清理无用键:`redis-cli FLUSHDB`
### Q: Celery 任务不执行
**A:** 检查:
1. Redis 连接状态
2. Celery Worker 是否启动
3. 队列配置是否正确
## 日志位置
- 应用日志:`logs/app.log`
- Redis 日志:`/var/log/redis/redis.log` (Linux)
- Celery 日志:控制台输出或配置的日志文件
## 相关命令
```bash
# 查看所有键
redis-cli keys "*"
# 查看特定键
redis-cli get key_name
# 删除键
redis-cli del key_name
# 清空数据库
redis-cli flushdb
# 查看信息
redis-cli info
# 监控命令
redis-cli monitor
```

71
REFACTOR_GUIDE.md Normal file
View File

@@ -0,0 +1,71 @@
# 代码重构指南
## Service 模块重构
### 重构内容
将以下业务逻辑函数从 `app/celery_tasks.py` 移动到独立的 `app/callback_service.py` 模块中:
#### 移动的函数
1. `save_callback_data_items` - 保存回调数据到数据库
2. `get_uncompleted_callback_log` - 获取未完成的回调日志
3. `get_callback_log_data` - 获取回调日志数据
4. `check_phone_number_threshold` - 检查手机号阈值
5. `call_external_api_with_retry` - 带重试的外部API调用
6. `mark_callback_log_completed` - 标记回调日志为完成
7. `_log_dtc_push_call` - 记录API调用日志
### 重构优势
#### 1. 代码组织优化
- **关注点分离**:Celery 任务专注于任务调度,业务逻辑独立到 Service 层
- **代码复用**:Service 函数可以被其他模块直接调用,不限于 Celery 任务
- **维护性提升**:业务逻辑集中管理,便于测试和维护
#### 2. 模块职责清晰
- `celery_tasks.py`: 负责任务定义和调度逻辑
- `callback_service.py`: 负责回调处理的核心业务逻辑
#### 3. 可测试性增强
- Service 函数可以独立进行单元测试
- 不依赖 Celery 环境,测试更加便捷
### 使用方式
#### 在 Celery 任务中使用
```python
from app.callback_service import (
save_callback_data_items,
get_uncompleted_callback_log,
call_external_api_with_retry
)
# 直接调用服务函数
callback_log_data = get_uncompleted_callback_log(conn)
if callback_log_data:
save_callback_data_items(conn, data_list, callback_log_id)
```
#### 在其他模块中使用
```python
from app.callback_service import check_phone_number_threshold
# 直接使用业务逻辑
exceeds, is_valid = check_phone_number_threshold(conn, phone_number)
```
### 向后兼容性
- ✅ 所有现有功能保持不变
- ✅ Celery 任务正常工作
- ✅ API 接口行为一致
- ✅ 数据库操作不变
### 文件结构
```
app/
├── callback_service.py # 新增:回调处理服务
├── celery_tasks.py # 重构:仅包含任务定义
├── celery_app.py # 保持不变
└── ... # 其他文件保持不变
```
这次重构提高了代码的可维护性和可测试性,同时保持了完全的向后兼容性。

59
START_GUIDE.md Normal file
View File

@@ -0,0 +1,59 @@
# 启动说明
## 启动模式
现在所有启动都通过 `main.py` 完成,支持以下模式:
### 1. 仅启动 API 服务
```bash
python main.py --mode api
# 或者
python main.py
```
### 2. 仅启动 Celery Worker
```bash
python main.py --mode worker
```
### 3. 仅启动 Celery Beat 调度器
```bash
python main.py --mode beat
```
### 4. 启动完整服务栈 (API + Worker + Beat)
```bash
python main.py --mode all
```
## 开发环境启动
开发环境下推荐使用 `all` 模式:
```bash
python main.py --mode all
```
这将在同一个进程中启动:
- FastAPI 应用 (端口 8000)
- Celery Worker (后台线程)
- Celery Beat 调度器 (后台线程)
## 生产环境启动
生产环境下建议分别启动各个组件:
```bash
# 终端1: 启动 API
python main.py --mode api
# 终端2: 启动 Worker
python main.py --mode worker
# 终端3: 启动 Beat
python main.py --mode beat
```
## 注意事项
- 使用 `--mode all` 时,Celery Worker 和 Beat 运行在后台线程中,适合开发和测试
- 生产环境建议使用进程管理工具 (如 systemd, supervisor) 分别管理各个进程
- 日志会统一输出到配置的日志文件中

497
app/callback_service.py Normal file
View File

@@ -0,0 +1,497 @@
"""
回调处理服务模块
包含与回调处理相关的业务逻辑函数
"""
import json
import time
import httpx
from datetime import datetime
from typing import Optional, Tuple, Dict, Any
from sqlalchemy import text
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.logger import get_logger
from app.database import CallbackFailureLog
logger = get_logger("callback_service")
async def log_callback_request(
db: AsyncSession,
request: Request,
site_id: str,
callback_data: dict
) -> bool:
"""记录回调请求到数据库,返回操作是否成功"""
import json
# 获取客户端IP地址
client_ip = request.client.host if request.client else None
client_port = request.client.port if request.client else None
# 获取服务器IP地址
server_ip = None
server_port = None
if hasattr(request, 'scope') and 'server' in request.scope:
server_host, server_port_info = request.scope['server']
server_ip = server_host
server_port = server_port_info
# 准备请求头信息(直接记录原始请求头)
request_headers = dict(request.headers)
# 准备请求体信息(直接记录原始请求体)
request_body = callback_data
# 记录请求URL(JSON格式)
logger.info(f"🌐 请求URL: {request.url}")
# 记录site_id(JSON格式)
logger.info(f"📝 site_id: {site_id}")
# 记录server_ip(JSON格式)
server_info = {
"ip": server_ip,
"port": server_port
}
logger.info(f"🏠 server_ip: {server_info}")
# 记录client_ip(JSON格式)
client_info = {
"ip": client_ip,
"port": client_port
}
logger.info(f"🖥️ client_ip: {client_info}")
# 记录请求头(JSON格式)
logger.info(f"📋 请求头: {json.dumps(request_headers, ensure_ascii=False, indent=2)}")
# 记录请求体(JSON格式)
logger.info(f"📄 请求体: {json.dumps(callback_data, ensure_ascii=False, indent=2)}")
try:
# 保存到数据库
callback_log = CallbackFailureLog(
site_id=site_id,
remote_address=f"{client_ip}:{client_port}" if client_ip and client_port else client_ip,
server_ip=f"{server_ip}:{server_port}" if server_ip and server_port else server_ip,
request_url=str(request.url),
request_headers=request_headers, # 保存原始请求头
request_body=request_body # 保存从request.body获取的原始请求体
)
db.add(callback_log)
await db.commit()
logger.info(f"✅ 回调请求记录成功保存到数据库,ID: {callback_log.id}")
# 返回成功标识
return True
except Exception as e:
logger.error(f"❌ 保存回调请求到数据库失败: {e}", exc_info=True)
# 返回失败标识
return False
def save_callback_data_items(
conn,
callback_data_items: list,
callback_failure_log_id: int
) -> bool:
"""保存callback_data.data中的数据到数据库
Returns:
bool: 保存是否成功,True表示成功,False表示失败或跳过保存
"""
try:
logger.info(f"📊 共传入callback_log_id {callback_failure_log_id} 的 {len(callback_data_items)} 条数据")
# 检查是否已经存在该callback_log_id的数据
existing_data_result = conn.execute(
text("SELECT * FROM callback_failure_data WHERE callback_failure_log_id = :log_id"),
{"log_id": callback_failure_log_id}
)
existing_data_list = existing_data_result.fetchall()
if existing_data_list:
logger.warning(f"⚠️ callback_log_id {callback_failure_log_id} 已存在 {len(existing_data_list)} 条数据,跳过保存")
return False
for item in callback_data_items:
# 获取手机号
number_data = item.get('number_data', {})
phone_number = number_data.get('number')
if not phone_number:
continue
# 获取任务ID
task = item.get('task', {})
task_id = task.get('id', '')
# 获取用户ID
user_id = item.get('user_id', '')
# 获取状态信息
status = item.get('status', 0)
status_description = item.get('status_str', '')
# 获取通话日期,默认使用当前时间
calldate = datetime.now()
if 'calldate' in item:
try:
# 如果calldate是字符串,尝试解析为datetime
if isinstance(item['calldate'], str):
calldate = datetime.fromisoformat(item['calldate'].replace('Z', '+00:00'))
elif isinstance(item['calldate'], (int, float)):
# 如果是时间戳,转换为datetime
calldate = datetime.fromtimestamp(item['calldate'])
except (ValueError, TypeError) as e:
logger.warning(f"⚠️ 解析calldate失败: {item.get('calldate')}, 使用当前时间, 错误: {e}")
calldate = datetime.now()
# 将整个item转换为JSON字符串保存
raw_data_json = json.dumps(item, ensure_ascii=False)
# 直接插入数据库
conn.execute(
text("""
INSERT INTO callback_failure_data
(callback_failure_log_id, phone_number, task_id, user_id, status, status_description, raw_data, calldate)
VALUES (:log_id, :phone_number, :task_id, :user_id, :status, :status_description, :raw_data, :calldate)
"""),
{
"log_id": callback_failure_log_id,
"phone_number": phone_number,
"task_id": task_id,
"user_id": user_id,
"status": status,
"status_description": status_description,
"raw_data": raw_data_json,
"calldate": calldate
}
)
conn.commit()
logger.info(f"✅ 成功保存 {len(callback_data_items)} 条callback_data记录到数据库")
return True
except Exception as e:
logger.error(f"❌ 保存callback_data到数据库失败: {e}", exc_info=True)
# 不重新抛出异常,避免影响主业务流程
return False
def get_uncompleted_callback_log(conn) -> Tuple[bool, Optional[Tuple[int, str, str, str]]]:
"""获取一条未完成的回调请求(按创建时间取最小值)
Returns:
Tuple[bool, Optional[Tuple[int, str, str, str]]]:
- 第一个值表示查询是否成功(True表示成功,False表示失败)
- 第二个值为回调日志数据元组或None
"""
try:
# 查询一条未完成的回调日志(按创建时间升序排列,取第一条)
result = conn.execute(
text("""
SELECT id, site_id, request_headers, request_body
FROM callback_failure_logs
WHERE is_completed = false
ORDER BY created_at ASC
LIMIT 1
""")
)
row = result.fetchone()
if not row:
logger.info("📋 没有找到未完成的回调日志")
return True, None
callback_log_id, site_id, request_headers, request_body = row
request_headers_json = json.dumps(request_headers, ensure_ascii=False)
request_body_json = json.dumps(request_body, ensure_ascii=False)
return True, (callback_log_id, site_id, request_headers_json, request_body_json)
except Exception as e:
logger.error(f"❌ 查询未完成回调日志失败: {e}", exc_info=True)
return False, None
def get_callback_log_data(conn, callback_log_id: int) -> Tuple[bool, Optional[str], Optional[str]]:
"""获取回调日志数据"""
try:
# 查询回调日志
result = conn.execute(
text("SELECT request_headers, request_body FROM callback_failure_logs WHERE id = :log_id"),
{"log_id": callback_log_id}
)
row = result.fetchone()
if not row:
logger.error(f"❌ 未找到回调日志: {callback_log_id}")
return False, None, None
request_headers_json = json.dumps(row[0], ensure_ascii=False)
request_body_json = json.dumps(row[1], ensure_ascii=False)
return True, request_headers_json, request_body_json
except Exception as e:
logger.error(f"❌ 获取回调日志数据失败: {e}", exc_info=True)
return False, None, None
def call_external_api_with_retry(
conn,
request_body: dict,
request_headers: Dict[str, str],
max_retries: int,
callback_failure_log_id: int
) -> Tuple[bool, int]:
"""带重试的调用外部API接口"""
for attempt in range(1, max_retries + 1):
try:
logger.info(f"🌐 尝试调用外部API接口,第{attempt}次")
with httpx.Client(timeout=30.0) as client:
response = client.post(
settings.external_api_url,
json=request_body,
headers=request_headers
)
# 记录推送日志
_log_dtc_push_call(
conn=conn,
callback_failure_log_id=callback_failure_log_id,
request_url=settings.external_api_url,
request_headers=request_headers,
request_body=request_body,
response_status=response.status_code,
response_headers=dict(response.headers),
response_body=response.text,
retry_count=attempt - 1
)
if response.status_code == 200:
logger.info(f"✅ 调用外部API接口成功,状态码: {response.status_code}")
return True, attempt - 1
else:
logger.warning(f"⚠️ 外部API返回非成功状态码: {response.status_code}")
# 如果是客户端错误(4xx),不重试
if 400 <= response.status_code < 500:
logger.error(f"❌ 客户端错误,不重试: {response.status_code}")
return False, attempt - 1
except httpx.TimeoutException:
logger.warning(f"⏰ 调用外部API接口超时,第{attempt}次尝试")
except httpx.RequestError as e:
logger.warning(f"🌐 调用外部API接口请求错误,第{attempt}次尝试: {e}")
except Exception as e:
logger.error(f"❌ 调用外部API接口异常,第{attempt}次尝试: {e}", exc_info=True)
# 如果不是最后一次尝试,等待一段时间再重试
if attempt < max_retries:
time.sleep(2 ** attempt) # 指数退避
# 所有重试都失败了
logger.error(f"❌ 调用外部API接口失败,已重试{max_retries}次")
return False, max_retries
def mark_callback_log_completed(conn, callback_log_id: int) -> bool:
"""标记CallbackFailureLog记录为已完成"""
try:
# 先检查推送日志中是否有响应成功的记录
success_result = conn.execute(
text("""
SELECT COUNT(*) as success_count
FROM external_api_logs
WHERE callback_failure_log_id = :log_id
AND response_status = 200
"""),
{"log_id": callback_log_id}
)
success_count = success_result.fetchone()[0]
if success_count == 0:
logger.warning(f"⚠️ 回调日志 {callback_log_id} 没有成功的推送记录,不标记为完成")
return False
logger.info(f"✅ 回调日志 {callback_log_id} 找到 {success_count} 条成功推送记录,开始标记为完成")
# 更新指定日志记录为已完成
result = conn.execute(
text("""
UPDATE callback_failure_logs
SET is_completed = true
WHERE id = :log_id
AND is_completed = false
"""),
{"log_id": callback_log_id}
)
# 检查是否真的更新了记录
if result.rowcount == 0:
logger.warning(f"⚠️ 回调日志 {callback_log_id} 已标记为完成或不存在")
return False
conn.commit()
logger.info(f"✅ 回调日志 {callback_log_id} 已标记为完成")
return True
except Exception as e:
logger.error(f"❌ 标记回调日志 {callback_log_id} 完成失败: {e}", exc_info=True)
conn.rollback()
return False
def get_related_records_by_unique_data_list(
conn,
unique_data_list: list,
callback_log_id: int,
limit_count: int
) -> Tuple[bool, list]:
"""根据unique_data_list中的数据查询相关记录,按创建时间排序
Args:
conn: 数据库连接
unique_data_list: 包含手机号、task_id、user_id的数据项列表
callback_log_id: 回调日志ID,作为过滤条件
limit_count: 获取记录数量限制
Returns:
Tuple[bool, list]:
- 第一个值表示查询是否成功(True表示成功,False表示失败)
- 第二个值为查询到的相关记录列表,失败时返回空列表
"""
related_records = []
try:
# 遍历unique_data_list查询相关记录
for item in unique_data_list:
# 提取手机号
number_data = item.get('number_data', {})
phone_number = number_data.get('number')
# 提取task_id
task = item.get('task', {})
task_id = task.get('id', '')
# 提取user_id
user_id = item.get('user_id', '')
if phone_number and task_id and user_id:
result = conn.execute(
text("""
SELECT phone_number, task_id, user_id, created_at
FROM callback_failure_data
WHERE phone_number = :phone_number
AND task_id = :task_id
AND user_id = :user_id
AND callback_failure_log_id = :callback_log_id
ORDER BY created_at DESC
LIMIT :limit_count
"""),
{
"phone_number": phone_number,
"task_id": task_id,
"user_id": user_id,
"callback_log_id": callback_log_id,
"limit_count": limit_count
}
)
records = result.fetchall()
if records:
related_records.extend([{
"phone_number": record[0],
"task_id": record[1],
"user_id": record[2],
"created_at": record[3]
} for record in records])
logger.info(f"📊 查询到相关记录数量: {len(related_records)} (阈值: {limit_count})")
if related_records:
for i, record in enumerate(related_records):
logger.info(f"📋 记录{i+1}: 手机号={record['phone_number']}, task_id={record['task_id']}, 用户ID={record['user_id']}, 创建时间={record['created_at']}")
return True, related_records
except Exception as e:
logger.error(f"❌ 查询相关记录失败: {e}", exc_info=True)
return False, []
def _log_dtc_push_call(
conn,
callback_failure_log_id: int,
request_url: str,
request_headers: Dict[str, Any],
request_body: Dict[str, Any],
response_status: int,
response_headers: Dict[str, str],
response_body: str,
retry_count: int
) -> bool:
"""记录DTC推送调用日志"""
try:
# 提取手机号从请求体中
phone_number = None
if 'data' in request_body and isinstance(request_body['data'], list) and len(request_body['data']) > 0:
first_item = request_body['data'][0]
if 'number_data' in first_item and 'number' in first_item['number_data']:
phone_number = first_item['number_data']['number']
# 记录到external_api_logs表
conn.execute(
text("""
INSERT INTO external_api_logs (
callback_failure_log_id,
phone_number,
request_url,
request_headers,
request_body,
response_status,
response_headers,
response_body,
retry_count,
created_at
) VALUES (
:callback_failure_log_id,
:phone_number,
:request_url,
:request_headers,
:request_body,
:response_status,
:response_headers,
:response_body,
:retry_count,
datetime('now')
)
"""),
{
"callback_failure_log_id": callback_failure_log_id,
"phone_number": phone_number,
"request_url": request_url,
"request_headers": json.dumps(request_headers, ensure_ascii=False),
"request_body": json.dumps(request_body, ensure_ascii=False),
"response_status": response_status,
"response_headers": json.dumps(response_headers, ensure_ascii=False),
"response_body": response_body,
"retry_count": retry_count
}
)
conn.commit()
logger.debug(f"📝 DTC推送调用日志记录成功,状态码: {response_status}")
return True
except Exception as e:
logger.error(f"❌ 记录DTC推送调用日志失败: {e}", exc_info=True)
conn.rollback()
return False

View File

@@ -2,20 +2,17 @@
Celery任务定义
"""
from celery import current_task
from sqlalchemy import create_engine, text
from sqlalchemy import create_engine
import json
from app.celery_app import celery_app
from app.config import settings
from app.logger import get_logger
from app.database import CallbackFailureLog, ExternalApiLog, CallbackFailureData
from app.callback_service import (
save_callback_data_items,
get_uncompleted_callback_log,
get_callback_log_data,
check_phone_number_threshold,
call_external_api_with_retry,
mark_callback_log_completed,
_log_dtc_push_call
get_related_records_by_unique_data_list
)
from app.redis_lock import redis_manager
@@ -29,7 +26,7 @@ engine = create_engine(
)
@celery_app.task(bind=True, name='push_data_to_dtc')
def push_data_to_dtc_task(self):
def push_data_to_dtc_task():
"""
推送数据给DTC的Celery任务
自动获取一条未完成的回调请求进行处理
@@ -50,6 +47,12 @@ def push_data_to_dtc_task(self):
try:
with engine.connect() as conn:
# 更新任务状态
current_task.update_state(
state='PROGRESS',
meta={'current': 0, 'total': 100, 'status': f'正在获取下一条回调请求日志...'}
)
# 获取一条未完成的回调请求(按创建时间取最小值)
query_success, callback_log_data = get_uncompleted_callback_log(conn)
if not query_success:
@@ -65,10 +68,11 @@ def push_data_to_dtc_task(self):
callback_log_id, site_id, request_headers_json, request_body_json = callback_log_data
logger.info(f"📋 获取到未完成的回调请求: ID={callback_log_id}, site_id={site_id}")
# 更新任务状态
current_task.update_state(
state='PROGRESS',
meta={'current': len(valid_phone_numbers), 'total': len(phone_numbers), 'status': f'检查手机号: {phone_number}'}
meta={'current': 25, 'total': 100, 'status': f'获取回调请求成功(ID={callback_log_id}, site_id={site_id}),开始分析处理...'}
)
# 解析请求头和请求体
@@ -83,78 +87,104 @@ def push_data_to_dtc_task(self):
logger.error(f"❌ 保存callback_data:{callback_log_id}失败,停止任务执行")
raise Exception(f"保存callback_data:{callback_log_id}失败,任务停止执行")
# 提取所有手机号并去重
phone_numbers_set = set()
data_list = request_body.get('data', [])
for item in data_list:
number_data = item.get('number_data', {})
phone_number = number_data.get('number')
if phone_number:
phone_numbers_set.add(phone_number)
phone_numbers = list(phone_numbers_set)
logger.info(f"📱 提取到的手机号列表(去重后): {phone_numbers}")
# 过滤出需要处理的手机号(不超过阈值的)
valid_phone_numbers = []
for phone_number in phone_numbers:
# 更新任务状态
current_task.update_state(
state='PROGRESS',
meta={'current': len(valid_phone_numbers), 'total': len(phone_numbers), 'status': f'检查手机号: {phone_number}'}
meta={'current': 50, 'total': 100, 'status': f'请求体中通话明细处理成功(ID={callback_log_id}, site_id={site_id}),开始过滤需要转发的通话记录...'}
)
# 检查手机号是否超过阈值
exceeds_threshold, query_success = check_phone_number_threshold(conn, phone_number)
# 根据手机号、task_id、user_id给data_list去重
data_list = request_body.get('data', [])
unique_data_list = []
seen_records = set()
original_count = len(data_list)
for item in data_list:
# 提取手机号
number_data = item.get('number_data', {})
phone_number = number_data.get('number', '')
# 提取task_id
task = item.get('task', {})
task_id = task.get('id', '')
# 提取user_id
user_id = item.get('user_id', '')
# 创建唯一标识
unique_key = (phone_number, task_id, user_id)
# 如果这个组合没见过,则添加到去重列表中
if unique_key not in seen_records:
seen_records.add(unique_key)
unique_data_list.append(item)
logger.info(f"🔄 数据去重完成: 原始数据 {original_count} 条,去重后 {len(unique_data_list)} 条 - callback_log_id: {callback_log_id}, site_id: {site_id}")
if len(unique_data_list) < original_count:
logger.info(f"🗑️ 移除了 {original_count - len(unique_data_list)} 条重复数据 - callback_log_id: {callback_log_id}, site_id: {site_id}")
# 查询相关数据:根据手机号、task_id、user_id作为条件,按创建时间排序获取前三条记录
query_success, related_records = get_related_records_by_unique_data_list(
conn, unique_data_list, callback_log_id, settings.count_threshold
)
if not query_success:
logger.error(f"❌ 查询手机号 {phone_number} 失败,跳过处理")
continue
logger.error("❌ 查询相关记录失败,停止任务执行")
raise Exception("查询相关记录失败,任务停止执行")
if exceeds_threshold:
logger.info(f"✅ 手机号 {phone_number} 出现次数超过阈值,跳过处理")
continue
# 判断是否有相关记录需要处理
if not related_records:
logger.info(f"📋 没有有效的通过记录需要处理,直接返回 - callback_log_id: {callback_log_id}, site_id={site_id}")
return {"status": "completed", "message": "没有有效的通过记录需要处理"}
valid_phone_numbers.append(phone_number)
logger.info(f"📱 需要处理的手机号数量: {len(valid_phone_numbers)}")
# 推送数据给DTC(在循环外执行一次)
processed_count = 0
skipped_count = len(phone_numbers) - len(valid_phone_numbers)
if valid_phone_numbers:
# 更新任务状态
current_task.update_state(
state='PROGRESS',
meta={'current': 0, 'total': len(valid_phone_numbers), 'status': '开始推送数据给DTC'}
meta={'current': 70, 'total': 100, 'status': f'需要推送的通过记录已获取成功(ID={callback_log_id}, site_id={site_id}),准备转发...'}
)
if related_records:
# 创建只包含有效数据项的请求体
filtered_request_body = request_body.copy()
filtered_request_body['data'] = related_records
# 更新任务状态
current_task.update_state(
state='PROGRESS',
meta={'current': 85, 'total': 100, 'status': '开始推送数据给DTC(ID={callback_log_id}, site_id={site_id})...'}
)
success, retry_count = call_external_api_with_retry(
conn=conn,
request_body=request_body,
request_body=filtered_request_body,
request_headers=request_headers,
max_retries=settings.external_api_retry_max,
callback_failure_log_id=callback_log_id
)
if success:
logger.info(f"✅ 推送数据给DTC成功,处理的手机号数量: {len(valid_phone_numbers)}, 重试次数: {retry_count}")
processed_count = len(valid_phone_numbers)
logger.info(f"✅ 推送数据给DTC成功,处理的数据项数量: {len(related_records)}, 重试次数: {retry_count} - callback_log_id: {callback_log_id}, site_id: {site_id}")
else:
logger.error(f"❌ 推送数据给DTC失败,重试次数: {retry_count}")
logger.error(f"❌ 推送数据给DTC失败,重试次数: {retry_count} - callback_log_id: {callback_log_id}, site_id: {site_id}")
else:
logger.info("📋 没有有效的手机号需要处理")
logger.info(f"📋 没有有效的通过记录需要处理 - callback_log_id: {callback_log_id}, site_id: {site_id}")
logger.info(f"🎉 推送数据给DTC处理完成,成功: {processed_count}, 跳过: {skipped_count}")
logger.info(f"🎉 推送数据给DTC处理完成 - callback_log_id: {callback_log_id}, site_id: {site_id}")
# 标记CallbackFailureLog为已完成
mark_callback_log_completed(conn, callback_log_id)
# 更新任务状态
current_task.update_state(
state='PROGRESS',
meta={'current': 100, 'total': 100, 'status': '标记回调请求日志为已完成(ID={callback_log_id}, site_id={site_id})'}
)
return {
"status": "completed",
"processed": processed_count,
"skipped": skipped_count,
"total": len(phone_numbers)
"message": "任务完成"
}
except Exception as e:

105
test_redis.py Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Redis 连接测试脚本
"""
import os
import sys
import asyncio
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app.config import settings
from app.logger import get_logger
logger = get_logger("redis_test")
async def test_redis_connection():
"""测试 Redis 连接"""
logger.info("🔴 测试 Redis 连接...")
try:
import redis.asyncio as redis
# 创建 Redis 客户端
redis_client = redis.from_url(settings.celery_broker_url)
# 测试连接
await redis_client.ping()
logger.info("✅ Redis 连接成功")
# 测试基本操作
test_key = "test_connection"
test_value = "Hello Redis!"
await redis_client.set(test_key, test_value, ex=60) # 设置60秒过期
retrieved_value = await redis_client.get(test_key)
if retrieved_value == test_value:
logger.info("✅ Redis 读写操作正常")
else:
logger.warning("⚠️ Redis 读写操作异常")
# 清理测试数据
await redis_client.delete(test_key)
# 关闭连接
await redis_client.close()
return True
except Exception as e:
logger.error(f"❌ Redis 连接失败: {e}")
return False
async def test_celery_redis():
"""测试 Celery 相关的 Redis 操作"""
logger.info("🌿 测试 Celery Redis 配置...")
try:
import redis.asyncio as redis
# 测试 Broker 连接
broker_client = redis.from_url(settings.celery_broker_url)
await broker_client.ping()
logger.info(f"✅ Celery Broker (Redis) 连接成功: {settings.celery_broker_url}")
# 测试 Backend 连接
backend_client = redis.from_url(settings.celery_result_backend)
await backend_client.ping()
logger.info(f"✅ Celery Backend (Redis) 连接成功: {settings.celery_result_backend}")
# 关闭连接
await broker_client.close()
await backend_client.close()
return True
except Exception as e:
logger.error(f"❌ Celery Redis 测试失败: {e}")
return False
async def main():
"""主函数"""
logger.info("🧪 开始 Redis 连接测试...")
# 显示配置信息
logger.info(f"📡 Celery Broker: {settings.celery_broker_url}")
logger.info(f"💾 Celery Backend: {settings.celery_result_backend}")
# 测试基础连接
basic_ok = await test_redis_connection()
# 测试 Celery 连接
celery_ok = await test_celery_redis()
if basic_ok and celery_ok:
logger.info("🎉 所有 Redis 测试通过!")
return 0
else:
logger.error("❌ Redis 测试失败")
return 1
if __name__ == "__main__":
sys.exit(asyncio.run(main()))