# db_utils.py
import pymysql
from config import DB_CONFIG, TABLE_NAME, LINE_NUMBER_FIELD, KEY_FIELD
def get_db_connection():
"""创建并返回数据库连接"""
try:
conn = pymysql.connect(**DB_CONFIG)
return conn
except Exception as e:
print(f"❌ 数据库连接失败: {e}")
return None
def fetch_all_status():
"""
查询 ai_devicestatus 表中所有线路的状态
返回: [{ 'linenumber': '10', 'nextwaybillid': '25560071' }, ...]
"""
conn = get_db_connection()
if not conn:
return []
try:
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
sql = f"SELECT `{LINE_NUMBER_FIELD}`, `{KEY_FIELD}` FROM `{TABLE_NAME}`"
cursor.execute(sql)
result = cursor.fetchall()
# 构建字典,过滤空值
data = {}
for line_num, next_id in result:
line_str = str(line_num).strip()
id_str = str(next_id).strip() if next_id else ""
if id_str: # 只保留非空 nextwaybillid
data[line_str] = id_str
return data
except Exception as e:
print(f"❌ 查询数据失败: {e}")
return []
finally:
conn.close()
"""
全局配置文件
"""
# ==================== 数据库连接配置 ====================
DB_CONFIG = {
"host": "192.168.110.204",
"port": 3306,
"user": "Gapinyc",
"password": "Gapinyc_2025",
"database": "gapinyc",
"charset": "utf8mb4",
"autocommit": True, # 自动提交事务
}
# ==================== 表和字段名 ====================
TABLE_NAME = "ai_devicestatus"
LINE_NUMBER_FIELD = "linenumber" # 线号字段
KEY_FIELD = "nextwaybillid" # 监控的关键字段
# ==================== 轮询间隔(秒)====================
POLLING_INTERVAL = 5
# ==================== PushPlus 配置 ====================
PUSHPLUS_TOKEN = "22315b28092842b48623325593451f46" # 主 token(一对多模板)
PUSHPLUS_SECRET_KEY = "ogIU756cpMOrJizUpIk5_ocuDDGA"
# 用户列表:当前使用的是关注 token(已失效于私聊),仅用于广播测试
# ⚠️ 必须替换为 new_xxx 开头的一对一 token 才能实现精准推送
PUSHPLUS_USERS = [
"2e16af713e6d4432b99863df67c32938", # Mr.Hui
# "ead8950287e44193b35901504dc3c6c5", # 小明
# "4a7a2ea811e64b72af1f41352591dacc", # 王玉龙
]
# ==================== 日志配置 ====================
LOG_FILE = "app.log"
ENABLE_LOG_FILE = False # 是否写入日志文件(默认打印到控制台)
# ==================== 状态持久化文件 ====================
STATE_FILE = "last_waybill_state.json" # 记录每条线最后推送的 nextwaybillid
# main.py
import time
import json
import os
from config import POLLING_INTERVAL, STATE_FILE
from db_utils import fetch_all_status
from push_utils import notify_waybill_change, log
def load_last_state():
"""
从 JSON 文件加载上次推送状态
返回: {'10': '25560071', '20': '25560072'}
"""
if not os.path.exists(STATE_FILE):
return {}
try:
with open(STATE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# 转换 key 为字符串,value 去前导零
cleaned = {str(k): str(v).lstrip('0') for k, v in data.items() if v}
return cleaned
except Exception as e:
log(f"⚠️ 读取历史状态失败,将重新开始: {e}")
return {}
def save_last_state(state):
"""
保存当前状态到文件
"""
try:
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
except Exception as e:
log(f"⚠️ 保存状态失败: {e}")
def is_valid_new_value(val):
"""判断是否是有效的新值(长度 > 0)"""
return val is not None and isinstance(val, str) and len(val.strip()) > 0
def normalize_value(val):
"""标准化值用于比较"""
if not val:
return ""
return str(val).strip().lstrip("0")
def main():
log("🟢 系统启动,开始监控 ai_devicestatus 表...")
last_state = load_last_state()
log(f"📌 已加载上次状态: {last_state}")
while True:
try:
rows = fetch_all_status()
current_state = {}
for row in rows:
lineno = row['linenumber']
waybill_id = row['nextwaybillid']
if not lineno:
continue
# 标准化当前值
norm_waybill = normalize_value(waybill_id)
norm_last = normalize_value(last_state.get(lineno))
current_state[lineno] = norm_waybill
# 判断是否是从“空”变为“非空”
if not last_state.get(lineno) and is_valid_new_value(waybill_id):
log(f"🔥 检测到 {lineno} 号线首次出现波次: {waybill_id}")
notify_waybill_change(lineno, waybill_id)
last_state[lineno] = norm_waybill # 更新本地状态
save_last_state(last_state) # 持久化保存
time.sleep(30)
# 可选:打印当前总览
# log(f"🔍 当前状态: {current_state}")
except KeyboardInterrupt:
log("🛑 用户中断程序")
break
except Exception as e:
log(f"🚨 主循环异常: {e}")
time.sleep(5)
time.sleep(POLLING_INTERVAL)
if __name__ == "__main__":
main()
# push_utils.py
import requests
import json
from config import PUSHPLUS_TOKEN, PUSHPLUS_USERS, LOG_FILE, ENABLE_LOG_FILE
def log(message):
formatted = f"[{__import__('datetime').datetime.now()}] {message}"
if ENABLE_LOG_FILE:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(formatted + "\n")
else:
print(formatted)
def send_pushplus_message(token, title, content, template="html"):
"""
发送广播消息(例如发到主频道)
"""
url = "https://www.pushplus.plus/send"
payload = {
"token": token,
"title": title,
"content": content,
"template": template
}
headers = {"Content-Type": "application/json"}
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
result = response.json()
if result.get("code") == 200:
trace_id = result.get("data")
log(f"✅ 推送成功 -> token尾部:{token[-6:]}, 流水号: {trace_id}")
return True, trace_id
else:
msg = result.get("msg", "未知错误")
log(f"❌ 推送失败 [{token[-6:]}]: {msg}")
return False, None
except Exception as e:
log(f"🚨 请求异常 [{token[-6:]}]: {e}")
return False, None
def send_pushplus_message_to_user(target_token, title, content, template="html"):
"""
向指定用户发送私信(使用主 token + to 参数)
"""
url = "https://www.pushplus.plus/send"
payload = {
"token": PUSHPLUS_TOKEN, # 使用你的主 token
"title": title,
"content": content,
"template": template,
"channel": "wechat", # 固定为微信通道
"to": target_token # 指定接收者
}
headers = {"Content-Type": "application/json"}
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
result = response.json()
if result.get("code") == 200:
trace_id = result.get("data")
log(f"✅ 私信推送成功 -> 用户:{target_token[-6:]}, 流水号: {trace_id}")
return True, trace_id
else:
msg = result.get("msg", "未知错误")
log(f"❌ 私信推送失败 [{target_token[-6:]}]: {msg}")
return False, None
except Exception as e:
log(f"🚨 请求异常 [{target_token[-6:]}]: {e}")
return False, None
def notify_waybill_change(lineno, waybill_id):
"""
向主频道和所有用户推送通知
"""
title = content = f"{lineno} 号线: {waybill_id}"
# 1. 推送到主频道(广播)
# success, trace_id = send_pushplus_message(PUSHPLUS_TOKEN, title, content)
# if not success:
# log("⚠️ 主频道推送失败")
# 2. 推送给每个指定用户(使用 to 参数)
for user_token in PUSHPLUS_USERS:
success, _ = send_pushplus_message_to_user(user_token, title, content)
if not success:
log(f"⚠️ 向用户 {user_token[-6:]} 推送失败")
D:\PushPlus>python main.py
[2025-11-17 14:22:12.302732] 🟢 系统启动,开始监控 ai_devicestatus 表...
[2025-11-17 14:22:12.302732] 📌 已加载上次状态: {}
[2025-11-17 14:22:12.302732] 🚨 主循环异常: string indices must be integers
[2025-11-17 14:22:22.334862] 🚨 主循环异常: string indices must be integers
[2025-11-17 14:22:32.355265] 🚨 主循环异常: string indices must be integers