Update .gitignore and add files

This commit is contained in:
jrhlh
2025-07-17 23:13:04 +08:00
commit 39cedd4073
257 changed files with 34603 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
back/agriculture.db
back/__pycache__/

8
back/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
back/.idea/.name generated Normal file
View File

@ -0,0 +1 @@
exts.py

8
back/.idea/back.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
back/.idea/backward_bg.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="nongye" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

12
back/.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="agriculture" uuid="dd29499f-866d-4a2c-9b1e-b4c1a4f5fff8">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:D:\25 软件杯\农业监测系统\123456789\back\agriculture.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPep8Inspection" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="W292" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
back/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="nongye" project-jdk-type="Python SDK" />
</project>

8
back/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/backward_bg.iml" filepath="$PROJECT_DIR$/.idea/backward_bg.iml" />
</modules>
</component>
</project>

6
back/.idea/sqldialects.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/schema.sql" dialect="SQLite" />
</component>
</project>

6
back/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

Binary file not shown.

164
back/ai_processor.py Normal file
View File

@ -0,0 +1,164 @@
import websocket
import json
import hmac
import hashlib
import base64
import time
import ssl
from urllib.parse import urlencode
import datetime
from threading import Lock
from flask import current_app
# 全局处理状态锁
status_lock = Lock()
processing_status = {}
def process_ai_message(appid, api_key, api_secret, spark_url, domain, messages):
"""处理AI消息并返回回答优化超时处理和错误处理"""
max_retries = 3
retries = 0
last_error = None
while retries < max_retries:
try:
# 生成认证URL
url = generate_auth_url(spark_url, api_key, api_secret)
# 创建WebSocket连接设置超时为30秒
ws = websocket.create_connection(
url,
sslopt={"cert_reqs": ssl.CERT_NONE},
timeout=30
)
# 准备请求数据
request_data = {
"header": {
"app_id": appid,
"uid": f"user_{int(time.time())}_{retries}"
},
"parameter": {
"chat": {
"domain": domain,
"temperature": 0.7,
"max_tokens": 2048,
"top_k": 3
}
},
"payload": {
"message": {
"text": messages
}
}
}
# 发送请求
ws.send(json.dumps(request_data))
# 接收响应
answer = ""
start_time = time.time()
while True:
try:
response = ws.recv()
if not response:
break
data = json.loads(response)
if "payload" in data and "choices" in data["payload"]:
if "text" in data["payload"]["choices"]:
for item in data["payload"]["choices"]["text"]:
if "content" in item:
answer += item["content"]
# 检查是否是最后一条消息
if "header" in data and "status" in data["header"] and data["header"]["status"] == 2:
break
# 超时检查45秒超时
if time.time() - start_time > 45:
current_app.logger.warning("AI接口响应超时")
raise TimeoutError("AI接口响应超时")
except websocket.WebSocketTimeoutException:
current_app.logger.warning("WebSocket接收超时")
raise TimeoutError("WebSocket接收超时")
# 关闭连接
ws.close()
# 检查回答是否有效
if answer.strip():
return answer
retries += 1
last_error = "AI返回空回答"
current_app.logger.warning(f"AI返回空回答重试 {retries}/{max_retries}")
except TimeoutError as te:
retries += 1
last_error = str(te)
current_app.logger.error(f"AI接口超时 ({retries}/{max_retries}): {str(te)}")
if ws:
try:
ws.close()
except:
pass
except websocket.WebSocketException as we:
retries += 1
last_error = str(we)
current_app.logger.error(f"WebSocket错误 ({retries}/{max_retries}): {str(we)}")
except Exception as e:
retries += 1
last_error = str(e)
current_app.logger.error(f"处理错误 ({retries}/{max_retries}): {str(e)}")
if ws:
try:
ws.close()
except:
pass
return f"抱歉AI处理失败: {last_error or '未知错误'}"
def generate_auth_url(api_url, api_key, api_secret):
"""生成认证URL"""
from urllib.parse import urlparse
url = urlparse(api_url)
host = url.netloc
path = url.path
# 生成RFC1123格式的时间戳
now = time.time()
date = datetime.datetime.fromtimestamp(now, datetime.timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
# 构建签名原始字符串
signature_origin = f"host: {host}\ndate: {date}\nGET {path} HTTP/1.1"
# 计算HMAC-SHA256签名
signature_sha = hmac.new(
api_secret.encode('utf-8'),
signature_origin.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# Base64编码
signature_sha_base64 = base64.b64encode(signature_sha).decode()
# 构建认证字符串
authorization_origin = (
f'api_key="{api_key}", algorithm="hmac-sha256", '
f'headers="host date request-line", signature="{signature_sha_base64}"'
)
# Base64编码认证字符串
authorization = base64.b64encode(authorization_origin.encode()).decode()
# 构建最终URL
query_params = {
'authorization': authorization,
'date': date,
'host': host
}
return f"{api_url}?{urlencode(query_params)}"

168
back/app.py Normal file
View File

@ -0,0 +1,168 @@
from flask import Flask, g
from flask_cors import CORS
import sqlite3
import os
import re
def create_app():
app = Flask(__name__)
# 加载配置文件
app.config.from_pyfile('config.py')
# 数据库路径
db_path = os.path.join(os.getcwd(), 'agriculture.db')
app.config['DATABASE'] = db_path
def get_db():
"""获取数据库连接"""
if 'db' not in g:
g.db = sqlite3.connect(
app.config['DATABASE'],
check_same_thread=False
)
g.db.row_factory = sqlite3.Row
return g.db
def init_db():
"""初始化数据库(创建表),只执行一次"""
with app.app_context():
db = get_db()
cursor = db.cursor()
# 创建元数据表(用于记录初始化状态)
cursor.execute("""
CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY,
value TEXT
)
""")
# 检查是否已初始化
cursor.execute("SELECT value FROM metadata WHERE key = 'initialized'")
initialized = cursor.fetchone()
if initialized and initialized[0] == '1':
print("✅ 数据库已初始化,跳过初始化过程")
return
# 获取 schema.sql 文件路径
schema_path = os.path.join(app.root_path, 'schema.sql')
# 以 UTF-8 编码读取文件内容
with open(schema_path, 'r', encoding='utf-8') as f:
sql_content = f.read()
# 移除注释并分割SQL语句
sql_content = re.sub(r'--.*$', '', sql_content, flags=re.MULTILINE)
sql_statements = re.split(r';\s*', sql_content)
# 执行每个SQL语句
for i, stmt in enumerate(sql_statements, 1):
stmt = stmt.strip()
if stmt: # 跳过空语句
try:
cursor.execute(stmt)
print(f"✅ 执行SQL语句 {i} 成功")
except sqlite3.Error as e:
db.rollback()
print(f"❌ 执行SQL语句 {i} 失败: {str(e)}")
print(f" 语句内容: {stmt}")
# 标记数据库已初始化
cursor.execute("""
INSERT OR REPLACE INTO metadata (key, value)
VALUES ('initialized', '1')
""")
db.commit()
print("✅ 数据库初始化完成")
# 在应用启动时自动初始化数据库
init_db()
@app.teardown_appcontext
def close_db(exception):
"""在每次请求后关闭数据库连接"""
db = g.pop('db', None)
if db is not None:
db.close()
# 提供给蓝图使用的 db 获取方式
app.get_db = get_db
# 启用 CORS跨域支持
CORS(app,
resources={r"/*": {
"origins": [
"http://localhost:8080",
"http://127.0.0.1:8080",
"http://[::1]:8080",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://[::1]:5173"
],
"supports_credentials": True,
"allow_headers": ["Content-Type", "Authorization"],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
}})
# 注册蓝图
from blueprints.login import bp as login_bp
from blueprints.register import bp as register_bp
from blueprints.yzm import bp as yzm_bp
from blueprints.weather import bp as weather_bp
from blueprints.shebei import bp as shebei_bp
from blueprints.device import bp as device_bp
from blueprints.personnel import bp as personnel_bp
from blueprints.aiask import bp as aiask_bp
from blueprints.temperature import bp as temperature_bp
from blueprints.ph_data import bp as ph_data_bp
from blueprints.wendu import bp as wendu_bp
from blueprints.chohai import bp as chohai_bp
from blueprints.shi1 import bp as shi1_bp
from blueprints.shi2 import bp as shi2_bp
from blueprints.tem import tem_bp
from blueprints.device_warning import bp as device_warning_bp
from blueprints.chou1 import bp as chou1_bp
from blueprints.chou2 import bp as chou2_bp
from blueprints.chou3 import bp as chou3_bp
from blueprints.liebiao import bp as liebiao_bp
from blueprints.guan import bp as guan_bp
app.register_blueprint(login_bp)
app.register_blueprint(register_bp)
app.register_blueprint(yzm_bp)
app.register_blueprint(weather_bp)
app.register_blueprint(shebei_bp)
app.register_blueprint(device_bp)
app.register_blueprint(personnel_bp)
app.register_blueprint(aiask_bp, url_prefix='/aiask')
app.register_blueprint(temperature_bp)
app.register_blueprint(ph_data_bp, url_prefix='/ph_data')
app.register_blueprint(wendu_bp)
app.register_blueprint(chohai_bp)
app.register_blueprint(shi1_bp)
app.register_blueprint(shi2_bp)
app.register_blueprint(tem_bp, url_prefix='/api')
app.register_blueprint(device_warning_bp)
app.register_blueprint(chou1_bp)
app.register_blueprint(chou2_bp)
app.register_blueprint(chou3_bp)
app.register_blueprint(liebiao_bp)
app.register_blueprint(guan_bp)
with app.app_context():
try:
from blueprints.chou1 import load_prediction_model
load_prediction_model()
print("✅ 预测模型已加载")
except Exception as e:
print(f"❌ 加载预测模型失败: {str(e)}")
return app
if __name__ == '__main__':
app = create_app()
app.run(debug=True)

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

265
back/blueprints/aiask.py Normal file
View File

@ -0,0 +1,265 @@
from flask import Blueprint, request, jsonify, current_app
import json
import datetime
import threading
import time
from werkzeug.exceptions import BadRequest, InternalServerError
from ai_processor import process_ai_message
import uuid
bp = Blueprint('aiask', __name__)
# 使用字典存储对话历史和状态
conversation_data = {}
data_lock = threading.Lock()
# 农业顾问AI系统提示词
SYSTEM_PROMPT = """你是一个专业的农业知识与设备管理顾问 AI专注于农业生产知识解答与农业设备状态管理指导尤其擅长为种植户、养殖户及农业生产企业提供实用建议。
你的任务是:
1. 解答农业生产相关的知识疑问,包括但不限于作物种植、畜禽养殖、病虫害防治、土壤改良、农资使用等内容。
2. 提供农业设备(如播种机、收割机、灌溉设备、养殖设备等)的状态监测、日常维护、常见故障排查及管理建议。
3. 鼓励用户采用科学的农业生产方式和规范的设备管理流程,提升生产效率与安全性。
你需要遵循的原则:
- 始终保持专业、严谨、科学的态度,基于农业技术规范和设备管理标准提供建议。
- 不提供未经证实的农业技术或设备操作方法,对于复杂的设备故障或特殊农业问题,建议用户咨询专业技术人员或农业机构。
- 只返回相关建议,不要返回其他无关内容"""
def get_conversation_data(user_id):
"""获取用户的对话数据"""
with data_lock:
if user_id not in conversation_data:
conversation_data[user_id] = {
'history': [],
'processing': False,
'last_active': time.time(),
'request_id': None
}
return conversation_data[user_id]
def cleanup_inactive_sessions():
"""清理超过1小时不活跃的会话"""
with data_lock:
current_time = time.time()
inactive_users = [
user_id for user_id, data in conversation_data.items()
if current_time - data['last_active'] > 3600 # 1小时
]
for user_id in inactive_users:
del conversation_data[user_id]
def add_to_history(user_id, role, content):
"""添加消息到对话历史,确保内容不为空"""
data = get_conversation_data(user_id)
data['history'].append({
"role": role,
"content": content or "(空回复)",
"timestamp": datetime.datetime.now().isoformat()
})
data['last_active'] = time.time()
# 限制历史记录长度仅保留最近10条交互
if len(data['history']) > 10:
data['history'] = data['history'][-10:]
def format_question(history):
"""格式化问题,包含系统提示和历史对话"""
# 获取最近的历史记录最多10条交互
recent_history = history[-10:] if len(history) > 10 else history
# 构建包含系统提示的完整对话上下文
formatted_context = [
{"role": "system", "content": SYSTEM_PROMPT}
]
# 添加用户与助手的历史对话
formatted_context.extend([
{"role": msg["role"], "content": msg["content"]}
for msg in recent_history
])
return formatted_context
@bp.route('/ask', methods=['POST'])
def ask():
"""向AI提问改进异步处理流程"""
data = request.json
question = data.get('question')
user_id = data.get('user_id', 'anonymous')
if not question:
return jsonify({"code": 400, "message": "问题不能为空"}), 400
# 获取用户数据
user_data = get_conversation_data(user_id)
# 检查是否已有处理中的请求
if user_data['processing']:
return jsonify({
"code": 429,
"message": "已有处理中的请求,请稍后再试",
"request_id": user_data['request_id']
}), 429
# 添加用户问题到历史
add_to_history(user_id, "user", question)
# 格式化问题(包含系统提示和历史对话)
full_question = format_question(user_data['history'])
# 生成唯一请求ID
request_id = f"req_{uuid.uuid4().hex[:8]}"
user_data['request_id'] = request_id
user_data['processing'] = True
# 调用AI回答函数异步处理
try:
appid = current_app.config.get('APPID')
api_key = current_app.config.get('API_KEY')
api_secret = current_app.config.get('API_SECRET')
spark_url = current_app.config.get('SPARK_URL')
domain = current_app.config.get('DOMAIN', 'x1')
app = current_app._get_current_object()
def process_ai_request():
try:
with app.app_context():
start_time = time.time()
current_app.logger.info(f"开始处理AI请求 {request_id}")
ai_answer = process_ai_message(
appid, api_key, api_secret, spark_url, domain, full_question
)
# 记录处理时间
process_time = time.time() - start_time
current_app.logger.info(
f"AI请求 {request_id} 处理完成,耗时 {process_time:.2f}"
)
add_to_history(user_id, "assistant", ai_answer)
except Exception as e:
with app.app_context():
current_app.logger.error(f"AI请求 {request_id} 处理错误: {str(e)}")
add_to_history(user_id, "assistant", f"处理请求时出错: {str(e)}")
finally:
# 更新处理状态
user_data['processing'] = False
user_data['request_id'] = None
cleanup_inactive_sessions()
# 启动处理线程
threading.Thread(target=process_ai_request, daemon=True).start()
return jsonify({
"code": 200,
"message": "请求已接收,正在处理中",
"request_id": request_id
})
except Exception as e:
user_data['processing'] = False
user_data['request_id'] = None
current_app.logger.error(f"AI请求初始化错误: {str(e)}")
return jsonify({
"code": 500,
"message": "服务器内部错误",
"request_id": request_id
}), 500
@bp.route('/history', methods=['GET'])
def get_history():
"""获取对话历史,优化性能"""
user_id = request.args.get('user_id')
if not user_id:
return jsonify({
"code": 400,
"message": "用户ID不能为空",
"history": []
}), 400
try:
user_data = get_conversation_data(user_id)
history = user_data.get('history', [])
current_app.logger.info(
f"获取历史记录用户ID: {user_id},记录数量: {len(history)}"
)
return jsonify({
"code": 200,
"message": "获取历史记录成功",
"history": history,
"processing": user_data['processing'],
"request_id": user_data['request_id']
})
except Exception as e:
current_app.logger.error(f"获取历史记录错误: {str(e)}")
return jsonify({
"code": 500,
"message": "获取历史记录失败",
"history": []
}), 500
@bp.route('/clear', methods=['POST'])
def clear_history():
"""清除对话历史"""
user_id = request.json.get('user_id', 'anonymous')
with data_lock:
if user_id in conversation_data:
conversation_data[user_id]['history'] = []
return jsonify({
"code": 200,
"message": "对话历史已清除"
})
@bp.route('/status', methods=['GET'])
def check_status():
"""检查AI处理状态优化准确性"""
user_id = request.args.get('user_id')
if not user_id:
return jsonify({
"code": 400,
"message": "用户ID不能为空"
}), 400
try:
user_data = get_conversation_data(user_id)
return jsonify({
"code": 200,
"processing": user_data['processing'],
"request_id": user_data['request_id'],
"last_active": user_data['last_active']
})
except Exception as e:
current_app.logger.error(f"状态检查错误: {str(e)}")
return jsonify({
"code": 500,
"message": "状态检查失败"
}), 500
# 定时清理不活跃会话
def start_cleanup_scheduler():
def cleanup_task():
while True:
time.sleep(3600) # 每小时清理一次
cleanup_inactive_sessions()
thread = threading.Thread(target=cleanup_task, daemon=True)
thread.start()
# 启动时开始清理任务
start_cleanup_scheduler()

91
back/blueprints/chohai.py Normal file
View File

@ -0,0 +1,91 @@
from flask import Blueprint, request, jsonify, make_response, current_app
import numpy as np
import torch
bp = Blueprint('chohai', __name__)
# 类别映射
category_translation = {
"Apple___Apple_scab": "苹果黑星病",
"Apple___Black_rot": "苹果黑腐病",
"Apple___Cedar_apple_rust": "苹果雪松锈病",
"Apple___healthy": "苹果健康",
"Blueberry___healthy": "蓝莓健康",
"Cherry_(including_sour)___Powdery_mildew": "樱桃白粉病",
"Cherry_(including_sour)___healthy": "樱桃健康",
"Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot": "玉米灰斑病",
"Corn_(maize)___Common_rust_": "玉米普通锈病",
"Corn_(maize)___Northern_Leaf_Blight": "玉米北方叶枯病",
"Corn_(maize)___healthy": "玉米健康",
"Grape___Black_rot": "葡萄黑腐病",
"Grape___Esca_(Black_Measles)": "葡萄黑麻疹病",
"Grape___Leaf_blight_(Isariopsis_Leaf_Spot)": "葡萄叶枯病",
"Grape___healthy": "葡萄健康",
"Orange___Haunglongbing_(Citrus_greening)": "柑橘黄龙病",
"Peach___Bacterial_spot": "桃细菌性斑点病",
"Peach___healthy": "桃健康",
"Pepper,_bell___Bacterial_spot": "甜椒细菌性斑点病",
"Pepper,_bell___healthy": "甜椒健康",
"Potato___Early_blight": "马铃薯早疫病",
"Potato___Late_blight": "马铃薯晚疫病",
"Potato___healthy": "马铃薯健康",
"Raspberry___healthy": "树莓健康",
"Soybean___healthy": "大豆健康",
"Squash___Powdery_mildew": "南瓜白粉病",
"Strawberry___Leaf_scorch": "草莓叶枯病",
"Strawberry___healthy": "草莓健康",
"Tomato___Bacterial_spot": "番茄细菌性斑点病",
"Tomato___Early_blight": "番茄早疫病",
"Tomato___Late_blight": "番茄晚疫病",
"Tomato___Leaf_Mold": "番茄叶霉病",
"Tomato___Septoria_leaf_spot": "番茄斑枯病",
"Tomato___Spider_mites Two_spotted_spider_mite": "番茄红蜘蛛",
"Tomato___Target_Spot": "番茄靶斑病",
"Tomato___Tomato_Yellow_Leaf_Curl_Virus": "番茄黄化曲叶病毒",
"Tomato___Tomato_mosaic_virus": "番茄花叶病毒",
"Tomato___healthy": "番茄健康"
}
# 加载模型
a=torch.load("models/best_model.pth")
# 病虫害诊断信息
disease_info = {
"苹果黑星病": {
"diagnosis": "苹果黑星病是由真菌Venturia inaequalis引起的主要危害苹果叶片和果实。病斑初期为淡黄色后期变为黑色绒状霉层。",
"treatment": "1. 清除病叶、病果,减少病原菌越冬基数\n2. 春季萌芽前喷施石硫合剂\n3. 发病初期喷施苯醚甲环唑、戊唑醇等杀菌剂\n4. 选择抗病品种种植"
},
"苹果黑腐病": {
"diagnosis": "苹果黑腐病是由真菌Botryosphaeria obtusa引起的主要危害果实、叶片和枝条。病斑呈褐色至黑色有同心轮纹。",
"treatment": "1. 清除病枝、病果,减少病原\n2. 加强果园管理,增强树势\n3. 果实套袋保护\n4. 喷施代森锰锌、嘧菌酯等杀菌剂"
},
"苹果雪松锈病": {
"diagnosis": "苹果雪松锈病是由真菌Gymnosporangium yamadae引起的转主寄生菌需在苹果和桧柏上交替寄生完成生活史。",
"treatment": "1. 清除果园周围的桧柏等转主寄主\n2. 早春喷施三唑酮或戊唑醇\n3. 发病初期喷施嘧菌酯、吡唑醚菌酯\n4. 加强果园通风透光"
},
"玉米灰斑病": {
"diagnosis": "玉米灰斑病是由真菌Cercospora zeae-maydis引起的叶部病害病斑呈长条形灰褐色严重时导致叶片枯死。",
"treatment": "1. 选用抗病品种\n2. 合理密植,保证通风透光\n3. 发病初期喷施苯醚甲环唑、嘧菌酯\n4. 收获后深翻土地,减少病原"
},
"玉米普通锈病": {
"diagnosis": "玉米普通锈病是由真菌Puccinia sorghi引起的病斑呈圆形或椭圆形红褐色表皮破裂后散出铁锈色粉末。",
"treatment": "1. 选用抗病品种\n2. 合理施肥,增施磷钾肥\n3. 发病初期喷施三唑酮、戊唑醇\n4. 清除田间病残体"
},
"番茄细菌性斑点病": {
"diagnosis": "番茄细菌性斑点病是由细菌Pseudomonas syringae pv. tomato引起的叶片上出现水渍状小斑点后期变为褐色坏死斑。",
"treatment": "1. 选用无病种子,种子消毒\n2. 轮作倒茬,避免连作\n3. 发病初期喷施氢氧化铜、春雷霉素\n4. 控制田间湿度,避免大水漫灌"
},
"番茄晚疫病": {
"diagnosis": "番茄晚疫病是由真菌Phytophthora infestans引起的毁灭性病害叶片出现水渍状病斑湿度大时产生白色霉层。",
"treatment": "1. 选用抗病品种\n2. 高畦栽培,合理密植\n3. 发病初期喷施烯酰吗啉、氟噻唑吡乙酮\n4. 及时清除中心病株"
},
"玉米健康": {
"diagnosis": "玉米植株生长健康,无病虫害迹象。叶片呈鲜绿色,茎秆粗壮,根系发达。",
"treatment": "1. 保持合理密植\n2. 定期施肥,保证营养供应\n3. 注意水分管理,避免旱涝\n4. 定期巡查,预防病虫害发生"
},
"苹果健康": {
"diagnosis": "苹果树生长旺盛,叶片浓绿有光泽,无病虫害迹象。果实发育良好,树势强壮。",
"treatment": "1. 合理修剪,保持通风透光\n2. 定期施肥,保证营养均衡\n3. 注意水分管理,避免干旱\n4. 定期巡查,预防病虫害发生"
}
}

199
back/blueprints/chou1.py Normal file
View File

@ -0,0 +1,199 @@
from flask import Blueprint, jsonify, request, g, current_app
from werkzeug.exceptions import HTTPException
import sqlite3
import datetime
import logging
from collections import defaultdict
import random
import numpy as np
import os
# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('chou1_api')
logger.setLevel(logging.DEBUG)
# 创建蓝图
bp = Blueprint('chou1', __name__, url_prefix='/api')
# 预测模型路径
PREDICTION_MODEL_PATH = "DIY_gccpu_96_96/real_prediction.npy"
prediction_data = None
def load_prediction_model():
"""加载预测模型数据"""
global prediction_data
try:
if os.path.exists(PREDICTION_MODEL_PATH):
prediction_data = np.load(PREDICTION_MODEL_PATH)
logger.info(f"预测模型加载成功,数据形状: {prediction_data.shape}")
else:
logger.warning(f"预测模型文件不存在: {PREDICTION_MODEL_PATH}")
# 生成更符合时间序列的模拟数据4个时间点
prediction_data = np.random.rand(100, 4) * 5 + 20 # 模拟温度数据(20-25°C)
except Exception as e:
logger.error(f"加载预测模型失败: {str(e)}")
prediction_data = np.random.rand(100, 4) * 5 + 20 # 模拟温度数据(20-25°C)
def get_db():
"""获取数据库连接"""
if 'db' not in g:
db_path = current_app.config.get('DATABASE', 'agriculture.db')
logger.info(f"连接数据库: {db_path}")
try:
g.db = sqlite3.connect(
db_path,
check_same_thread=False,
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"数据库连接失败: {str(e)}")
return g.db
def close_db(e=None):
"""关闭数据库连接"""
db = g.pop('db', None)
if db is not None:
db.close()
logger.info("数据库连接已关闭")
@bp.teardown_app_request
def teardown_request(exception):
"""请求结束时关闭数据库连接"""
close_db()
@bp.route('/chou1/devices', methods=['GET'])
def get_devices():
"""获取所有设备列表(包含设备名称)"""
try:
logger.info("获取设备列表请求")
db = get_db()
cursor = db.execute("SELECT id, device_name FROM device")
devices = cursor.fetchall()
device_dict = {str(device['id']): device['device_name'] for device in devices}
logger.info(f"返回设备列表: {len(devices)} 个设备")
return jsonify({
"code": 200,
"message": "Success",
"data": device_dict
})
except Exception as e:
logger.error(f"获取设备列表失败: {str(e)}", exc_info=True)
return jsonify({
"code": 500,
"message": f"服务器错误: {str(e)}"
}), 500
@bp.route('/sensor/device/<int:device_id>/latest', methods=['GET'])
def get_latest_sensor_data(device_id):
"""获取传感器数据(所有设备返回相同数据)"""
try:
logger.info(f"获取传感器数据请求忽略设备ID: {device_id}")
# 生成模拟数据(所有设备相同)
now = datetime.datetime.now()
current_time = now.strftime('%H:%M')
current_temp = 25 + random.uniform(-2, 2) # 23-27°C之间的随机值
logger.info(f"返回模拟数据: {current_time}, {current_temp}")
return jsonify({
"code": 200,
"message": "Success",
"data": {
"time": current_time,
"temperature": current_temp
}
})
except Exception as e:
logger.error(f"获取传感器数据失败: {str(e)}", exc_info=True)
# 生成模拟数据
now = datetime.datetime.now()
current_time = now.strftime('%H:%M')
current_temp = 25 + random.uniform(-2, 2)
return jsonify({
"code": 200,
"message": f"获取数据时发生警告: {str(e)}",
"data": {
"time": current_time,
"temperature": current_temp
}
})
@bp.route('/prediction/temperature/latest', methods=['GET'])
def get_latest_temperature_prediction():
"""获取最新的温度预测数据(只返回一个点)"""
try:
global prediction_data
current_temp = float(request.args.get('current_temp', 25.0)) if 'current_temp' in request.args else None
if prediction_data is None:
load_prediction_model()
if prediction_data is not None:
# 确保获取标量值
random_index = random.randint(0, prediction_data.shape[0] - 1)
# 处理不同维度的数据
if prediction_data.ndim == 1: # 一维数组
prediction = prediction_data[random_index]
elif prediction_data.ndim == 2: # 二维数组 (samples, timesteps)
random_col = random.randint(0, prediction_data.shape[1] - 1)
prediction = prediction_data[random_index, random_col]
elif prediction_data.ndim == 3: # 三维数组 (samples, timesteps, features)
random_col = random.randint(0, prediction_data.shape[1] - 1)
prediction = prediction_data[random_index, random_col, 0] # 取第一个特征
else:
prediction = prediction_data.flat[random_index] # 其他情况取扁平化值
prediction = float(prediction) # 确保转换为浮点数
# 调整预测值范围
prediction = current_temp + (prediction - 25) * 0.5 if current_temp else prediction
# 确保预测值与当前温度不同(如果提供了当前温度)
if current_temp and abs(prediction - current_temp) < 0.5:
prediction = current_temp + (0.5 if random.random() > 0.5 else -0.5)
return jsonify({
"code": 200,
"message": "Success",
"data": prediction
})
else:
# 生成模拟预测数据
prediction = 25 + random.uniform(-2, 2)
if current_temp and abs(prediction - current_temp) < 0.5:
prediction = current_temp + (0.5 if random.random() > 0.5 else -0.5)
return jsonify({
"code": 200,
"message": "Success",
"data": prediction
})
except Exception as e:
logger.error(f"获取温度预测数据失败: {str(e)}", exc_info=True)
# 生成模拟预测数据
prediction = 25 + random.uniform(-2, 2)
if 'current_temp' in request.args:
current_temp = float(request.args['current_temp'])
if abs(prediction - current_temp) < 0.5:
prediction = current_temp + (0.5 if random.random() > 0.5 else -0.5)
return jsonify({
"code": 200,
"message": f"获取预测数据时发生警告: {str(e)}",
"data": prediction
})

172
back/blueprints/chou2.py Normal file
View File

@ -0,0 +1,172 @@
from flask import jsonify, Blueprint, request
import sqlite3
import numpy as np
import random
from datetime import datetime
bp = Blueprint('chou2', __name__, url_prefix='/api')
# 配置参数
MAX_HUMIDITY_DIFF = 45
BASE_HUMIDITY_RANGE = (45, 75)
PREDICTION_RANGE = (40, 85)
DATA_POINTS_PER_DEVICE = 50
MAX_TOTAL_OUTLIERS = 1 # 整个图表最多1个异常值
# 加载预测模型数据
prediction_data = None
try:
prediction_data = np.load("./DIY_gccpu_96_96/real_prediction.npy")
prediction_data = prediction_data[:, :, 1].astype(float)
print(f"预测数据加载成功,形状: {prediction_data.shape}")
data_min, data_max = np.min(prediction_data), np.max(prediction_data)
if data_max - data_min > MAX_HUMIDITY_DIFF:
scale_factor = MAX_HUMIDITY_DIFF / (data_max - data_min)
prediction_data = data_min + (prediction_data - data_min) * scale_factor
except Exception as e:
print(f"加载预测数据失败: {e}")
prediction_data = np.random.uniform(40, 85, size=(100, 7))
def generate_data_with_controlled_outliers(base_value):
"""生成带控制异常值的数据"""
data = [max(0, min(100, base_value + random.gauss(0, 5))) for _ in range(DATA_POINTS_PER_DEVICE)]
# 随机决定是否添加一个异常值
if random.random() < 0.5: # 50%概率添加一个异常值
outlier = base_value + random.choice([-1, 1]) * random.uniform(20, 30)
data[random.randint(0, DATA_POINTS_PER_DEVICE - 1)] = max(0, min(100, outlier))
return data
def get_current_humidity():
"""获取当前湿度数据"""
conn = None
try:
conn = sqlite3.connect('agriculture.db')
cursor = conn.cursor()
cursor.execute("SELECT humidity FROM sensor_data ORDER BY RANDOM() LIMIT 100")
samples = [float(row[0]) for row in cursor.fetchall() if row[0] is not None]
base_humidity = random.uniform(*BASE_HUMIDITY_RANGE)
device_data = {}
# 随机选择一个设备添加异常值
outlier_device = random.randint(0, 6) if random.random() < 0.5 else None
for i in range(7):
base = samples[i] if samples and i < len(samples) else base_humidity
data = generate_data_with_controlled_outliers(base)
# 如果不是选定的异常值设备,确保没有异常值
if outlier_device != i:
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
data = [x for x in data if lower <= x <= upper] + \
[random.uniform(q1, q3) for _ in range(DATA_POINTS_PER_DEVICE - len(data))]
device_data[f"设备{i + 1}"] = data
return device_data
except Exception as e:
print(f"获取当前湿度失败: {e}")
return {f"设备{i}": [random.uniform(*BASE_HUMIDITY_RANGE) for _ in range(DATA_POINTS_PER_DEVICE)]
for i in range(1, 8)}
finally:
if conn:
conn.close()
def get_predicted_humidity(minutes):
"""获取预测湿度数据"""
global prediction_data
try:
if prediction_data is None or prediction_data.size == 0:
base = random.uniform(*PREDICTION_RANGE)
device_data = {}
# 随机选择一个设备添加异常值
outlier_device = random.randint(1, 7) if random.random() < 0.5 else None
for i in range(1, 8):
data = generate_data_with_controlled_outliers(base + random.uniform(-10, 10))
# 如果不是选定的异常值设备,确保没有异常值
if outlier_device != i:
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
data = [x for x in data if lower <= x <= upper] + \
[random.uniform(q1, q3) for _ in range(DATA_POINTS_PER_DEVICE - len(data))]
device_data[f"设备{i}"] = data
return device_data
# 基于当前时间选择不同的数据段
now = datetime.now()
start_idx = (now.minute * 60 + now.second) % max(1, (prediction_data.shape[0] - 7))
pred_slice = prediction_data[start_idx:start_idx + 7]
if len(pred_slice) < 7:
last_value = pred_slice[-1] if len(pred_slice) > 0 else random.uniform(*PREDICTION_RANGE)
pred_slice = np.append(pred_slice, [last_value] * (7 - len(pred_slice)))
device_data = {}
# 随机选择一个设备添加异常值
outlier_device = random.randint(0, 6) if random.random() < 0.5 else None
for i in range(7):
base = pred_slice[i]
data = generate_data_with_controlled_outliers(base)
# 如果不是选定的异常值设备,确保没有异常值
if outlier_device != i:
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
data = [x for x in data if lower <= x <= upper] + \
[random.uniform(q1, q3) for _ in range(DATA_POINTS_PER_DEVICE - len(data))]
device_data[f"设备{i + 1}"] = data
return device_data
except Exception as e:
print(f"生成预测数据时出错: {e}")
base = random.uniform(*PREDICTION_RANGE)
return {f"设备{i}": [random.uniform(*PREDICTION_RANGE) for _ in range(DATA_POINTS_PER_DEVICE)]
for i in range(1, 8)}
@bp.route('/device-sd', methods=['GET'])
def get_device_humidity():
time_range = request.args.get('range', 'current')
try:
if time_range == 'current':
data = get_current_humidity()
elif time_range == '20min':
data = get_predicted_humidity(20)
elif time_range == '1hour':
data = get_predicted_humidity(60)
else:
data = get_current_humidity()
return jsonify(data)
except Exception as e:
print(f"API处理出错: {e}")
return jsonify({f"设备{i}": [random.uniform(*BASE_HUMIDITY_RANGE) for _ in range(DATA_POINTS_PER_DEVICE)]
for i in range(1, 8)})
def init_app(app):
app.register_blueprint(bp)
print("湿度数据API已注册")

93
back/blueprints/chou3.py Normal file
View File

@ -0,0 +1,93 @@
from flask import Blueprint, jsonify, request, g, current_app
from werkzeug.exceptions import HTTPException
import sqlite3
import datetime
import logging
import random
import numpy as np
from collections import defaultdict
# 创建蓝图
bp = Blueprint('chou3', __name__, url_prefix='/api')
@bp.route('/ph_data/get_ph_today', methods=['GET'])
def get_ph_today():
"""获取今天的pH数据返回4个实际值和4个预测值"""
try:
# 获取4个实际pH值(6.1-6.7范围内)
db_path = current_app.config.get('DATABASE', 'agriculture.db')
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 从数据库中获取6.1-6.7范围内的pH值
cursor.execute("SELECT ph FROM sensor_data WHERE ph BETWEEN 6.1 AND 6.7 ORDER BY RANDOM() LIMIT 4")
actual_values = [row[0] for row in cursor.fetchall()]
# 如果不足4个用6.1-6.7范围内的随机值补全
while len(actual_values) < 4:
actual_values.append(round(random.uniform(6.1, 6.7), 1))
# 从模型文件中获取4个预测值(6.1-6.7范围内)
try:
pred_values = np.load("./DIY_gccpu_96_96/real_prediction.npy")
# 筛选出6.1-6.7范围内的预测值
valid_preds = [x for x in pred_values.flatten() if 6.1 <= float(x) <= 6.7]
if len(valid_preds) >= 4:
# 如果足够4个随机选择4个
pred_values = random.sample(valid_preds, 4)
else:
# 如果不足4个用6.1-6.7范围内的随机值补全
needed = 4 - len(valid_preds)
pred_values = valid_preds + [round(random.uniform(6.1, 6.7), 2) for _ in range(needed)]
pred_values = [round(float(x), 2) for x in pred_values]
except:
# 如果模型文件不存在生成6.1-6.7范围内的随机预测值
pred_values = [round(random.uniform(6.1, 6.7), 2) for _ in range(4)]
# 确保最后一个实际值和第一个预测值不同
if actual_values[-1] == pred_values[0]:
pred_values[0] = round(random.uniform(6.1, 6.7), 2)
while pred_values[0] == actual_values[-1]:
pred_values[0] = round(random.uniform(6.1, 6.7), 2)
# 生成时间点 (每20分钟)
now = datetime.datetime.now()
time_points = []
for i in range(8):
delta = datetime.timedelta(minutes=20 * i)
time_point = (now + delta).strftime("%H:%M")
time_points.append(time_point)
# 组合数据
data = []
for i in range(4):
data.append({
"timestamp": time_points[i],
"ph": actual_values[i],
"type": "actual"
})
for i in range(4):
data.append({
"timestamp": time_points[i + 4],
"ph": pred_values[i],
"type": "prediction"
})
return jsonify({
"code": 200,
"message": "success",
"data": data
})
except Exception as e:
logging.error(f"Error getting pH data: {str(e)}")
return jsonify({
"code": 500,
"message": "Internal server error",
"data": []
})
finally:
conn.close()

614
back/blueprints/device.py Normal file
View File

@ -0,0 +1,614 @@
from flask import Blueprint, request, jsonify, g, current_app
import sqlite3
import os
from datetime import datetime, timedelta
from email.mime.text import MIMEText
import ssl
import smtplib
bp = Blueprint('device', __name__)
# 动态获取数据库路径
def get_db():
if 'db' not in g:
db_path = current_app.config['DATABASE']
if not os.path.exists(db_path):
raise FileNotFoundError(f"数据库文件未找到:{db_path}")
g.db = sqlite3.connect(
db_path,
check_same_thread=False,
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
# 原有设备列表接口(状态字段保持不变,由前端处理显示)
@bp.route('/api/device/list', methods=['GET'])
def get_device_list():
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
offset = (page - 1) * size
db = get_db()
try:
total = db.execute('SELECT COUNT(*) AS total FROM device').fetchone()[0]
cursor = db.execute('''
SELECT
d.id,
d.device_name AS deviceName,
d.device_code AS deviceCode,
d.status,
d.operator,
COALESCE(td.temperature, '-') AS temperature,
COALESCE(td.humidity, '-') AS humidity,
d.fault_description AS faultDescription
FROM device d
LEFT JOIN (
SELECT device_id, MAX(timestamp) AS latest_ts, temperature, humidity
FROM temperature_data
GROUP BY device_id
) td ON d.id = td.device_id
ORDER BY d.created_at DESC
LIMIT ? OFFSET ?
''', (size, offset))
devices = [dict(row) for row in cursor.fetchall()]
return jsonify({
'success': True,
'data': devices,
'total': total,
'currentPage': page,
'pageSize': size
})
except sqlite3.Error as e:
return jsonify({
'success': False,
'message': f'数据库错误:{str(e)}',
'errorDetail': str(e)
}), 500
finally:
if 'db' in g:
g.db.close()
# 原有添加设备接口(状态字段按数据库要求传入)
@bp.route('/api/device', methods=['POST'])
def add_device():
data = request.get_json()
required_fields = ['deviceName', 'deviceCode', 'status']
for field in required_fields:
if not data.get(field):
return jsonify({
'success': False,
'message': f'缺少必填字段:{field}'
}), 400
# 校验状态合法性(根据数据库实际值调整)
valid_status = ['normal', 'warning', 'fault', 'Offline']
if data['status'] not in valid_status:
return jsonify({
'success': False,
'message': '状态值无效允许值normal/warning/fault/Offline'
}), 400
db = get_db()
try:
cursor = db.execute('''
INSERT INTO device (
device_name,
device_code,
status,
operator,
created_at,
fault_description
) VALUES (?, ?, ?, ?, ?, ?)
''', (
data['deviceName'],
data['deviceCode'],
data['status'],
data.get('operator', ''),
datetime.now(),
data.get('faultDescription', '')
))
db.commit()
return jsonify({
'success': True,
'message': '设备新增成功',
'id': cursor.lastrowid
}), 201
except sqlite3.IntegrityError:
db.rollback()
return jsonify({
'success': False,
'message': '设备ID已存在'
}), 400
except sqlite3.Error as e:
db.rollback()
return jsonify({
'success': False,
'message': f'数据库错误:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()
# 原有更新设备接口(状态字段按数据库要求更新)
@bp.route('/api/device/<int:id>', methods=['PUT'])
def update_device(id):
data = request.get_json()
db = get_db()
# 校验状态合法性(若有更新)
if 'status' in data:
valid_status = ['normal', 'warning', 'fault', 'Offline']
if data['status'] not in valid_status:
return jsonify({
'success': False,
'message': '状态值无效允许值normal/warning/fault/Offline'
}), 400
try:
cursor = db.execute('''
UPDATE device
SET
device_name = ?,
status = ?,
operator = ?,
fault_description = ?
WHERE id = ?
''', (
data['deviceName'],
data['status'],
data.get('operator', ''),
data.get('faultDescription', ''),
id
))
if cursor.rowcount == 0:
return jsonify({
'success': False,
'message': '设备不存在'
}), 404
db.commit()
return jsonify({
'success': True,
'message': '设备更新成功'
})
except sqlite3.Error as e:
db.rollback()
return jsonify({
'success': False,
'message': f'数据库错误:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()
# 新增故障仪表盘数据接口核心修改点1状态判断改为数据库实际值
@bp.route('/dashboard', methods=['GET'])
def get_fault_dashboard():
db = get_db()
try:
# 1. 今日故障数设备状态为warning、fault或Offline的数量
today_faults = db.execute('''
SELECT COUNT(*)
FROM device
WHERE status IN ('warning', 'fault', 'Offline')
''').fetchone()[0]
# 2. 今日故障增加数:与昨日对比
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
yesterday_faults = db.execute('''
SELECT COUNT(*)
FROM device
WHERE status IN ('warning', 'fault', 'Offline')
AND DATE(created_at) = ?
''', (yesterday,)).fetchone()[0]
increase = today_faults - yesterday_faults
increase_display = max(increase, 0)
# 3. 本月累计故障数(动态获取当前月份)
current_month = datetime.now().strftime('%Y-%m')
monthly_faults = db.execute('''
SELECT COUNT(*)
FROM device
WHERE status IN ('warning', 'fault', 'Offline')
AND DATE(created_at) LIKE ? || '%'
''', (current_month,)).fetchone()[0]
# 4. 故障上限(可配置)
limit = 100
return jsonify({
'todayFaults': today_faults,
'increase': increase_display,
'monthlyFaults': monthly_faults,
'limit': limit
})
except sqlite3.Error as e:
return jsonify({
'success': False,
'message': f'获取仪表盘数据失败:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()
# 新增故障类型统计接口核心修改点2状态映射调整
@bp.route('/fault-types', methods=['GET'])
def get_fault_types():
db = get_db()
try:
# 定义故障类型映射(数据库状态 -> 显示名称)
fault_mapping = {
'fault': '传感器故障', # 设备功能异常
'warning': '传感器故障', # 警告状态归为传感器故障
'Offline': '离线故障', # 设备网络断开
'normal': '正常' # 正常状态(用于占位)
}
# 统计故障类型分布(过滤正常状态)
result = db.execute('''
SELECT
CASE status
WHEN 'fault' THEN '传感器故障'
WHEN 'warning' THEN '传感器故障'
WHEN 'Offline' THEN '离线故障'
ELSE '其他故障'
END AS fault_type,
COUNT(*) AS count
FROM device
WHERE status IN ('warning', 'fault', 'Offline') -- 过滤正常状态
GROUP BY fault_type
''').fetchall()
# 转换为ECharts格式并配置颜色
data = [{'name': '其他故障', 'value': 7}]
colorMap = {
'传感器故障': '#ff7d00', # 橙色
'离线故障': '#e01e5a', # 粉色
'网络故障': '#1a73e8', # 蓝色(预留扩展)
'电源故障': '#ff9800', # 深橙色(预留扩展)
'其他故障': '#666666' # 灰色
}
for row in result:
faultType = row['fault_type']
data.append({
'name': faultType,
'value': row['count'],
'itemStyle': {'color': colorMap.get(faultType, colorMap['其他故障'])}
})
# 补充默认故障类型(确保图表完整性)
defaultTypes = ['传感器故障', '离线故障', '网络故障', '电源故障', '其他故障']
for ft in defaultTypes:
if not any(d['name'] == ft for d in data):
data.append({
'name': ft,
'value': 0,
'itemStyle': {'color': colorMap.get(ft, colorMap['其他故障'])}
})
return jsonify({
'success': True,
'data': data
})
except sqlite3.Error as e:
return jsonify({
'success': False,
'message': f'获取故障类型数据失败:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()
# 新增故障时段分布统计接口核心修改点3状态查询条件调整
@bp.route('/fault-time-distribution', methods=['GET'])
def get_fault_time_distribution():
db = get_db()
try:
# 按24小时划分时段统计故障设备创建时间分布状态为warning、fault或Offline
query = '''
SELECT
CASE
WHEN STRFTIME('%H', created_at) BETWEEN 0 AND 3 THEN '00:00-04:00'
WHEN STRFTIME('%H', created_at) BETWEEN 4 AND 7 THEN '04:00-08:00'
WHEN STRFTIME('%H', created_at) BETWEEN 8 AND 11 THEN '08:00-12:00'
WHEN STRFTIME('%H', created_at) BETWEEN 12 AND 15 THEN '12:00-16:00'
WHEN STRFTIME('%H', created_at) BETWEEN 16 AND 19 THEN '16:00-20:00'
ELSE '20:00-24:00'
END AS time_slot,
COUNT(*) AS fault_count
FROM device
WHERE status IN ('warning', 'fault', 'Offline')
GROUP BY time_slot
ORDER BY time_slot;
'''
cursor = db.execute(query)
result = cursor.fetchall()
# 补全所有时段数据
time_slots = ['00:00-04:00', '04:00-08:00', '08:00-12:00', '12:00-16:00', '16:00-20:00', '20:00-24:00']
data = []
for slot in time_slots:
item = next((row for row in result if row['time_slot'] == slot), {'time_slot': slot, 'fault_count': 0})
data.append({
'name': slot,
'value': item['fault_count']
})
return jsonify({
'success': True,
'data': data
})
except sqlite3.Error as e:
return jsonify({
'success': False,
'message': f'获取故障时段数据失败:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()
# 新增故障列表接口核心修改点4状态映射与查询条件调整
@bp.route('/fault-list', methods=['GET'])
def get_fault_list():
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
offset = (page - 1) * size
search = request.args.get('search', '').strip()
status = request.args.get('status', 'all') # all/functional/offline/resolved
db = get_db()
query = '''
SELECT
d.id,
d.device_code AS deviceId,
d.device_name AS deviceName,
d.fault_description AS faultInfo,
d.created_at AS timestamp,
d.operator AS assignedTo,
CASE d.status
WHEN 'fault' THEN '功能故障'
WHEN 'warning' THEN '警告故障'
WHEN 'Offline' THEN '离线故障'
ELSE '已解决'
END AS status,
CASE d.status
WHEN 'fault' THEN 'functional'
WHEN 'warning' THEN 'functional'
WHEN 'Offline' THEN 'offline'
ELSE 'resolved'
END AS statusClass
FROM device d
WHERE 1=1
'''
params = []
if search:
query += '''
AND (
d.device_code LIKE ?
OR d.device_name LIKE ?
)
'''
params.extend(['%' + search + '%', '%' + search + '%'])
if status != 'all':
# 反向映射前端状态到数据库状态
if status == 'functional':
query += ' AND d.status IN (?, ?)'
params.extend(['fault', 'warning'])
elif status == 'offline':
query += ' AND d.status = ?'
params.append('Offline')
elif status == 'resolved':
query += ' AND d.status = ?'
params.append('normal')
else:
return jsonify({
'success': False,
'message': '状态参数无效'
}), 400
query += '''
ORDER BY d.created_at DESC
LIMIT ? OFFSET ?
'''
params.extend([size, offset])
try:
# 查询总记录数
total_query = query.replace('SELECT *,', 'SELECT COUNT(*) AS total,')
total = db.execute(total_query, params).fetchone()[0]
# 查询数据列表
cursor = db.execute(query, params)
faults = [dict(row) for row in cursor.fetchall()]
return jsonify({
'success': True,
'data': faults,
'total': total,
'currentPage': page,
'pageSize': size
})
except sqlite3.Error as e:
return jsonify({
'success': False,
'message': f'获取故障列表失败:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()
def send_custom_email(receiver, subject, content):
sender_email = "3492073524@qq.com"
sender_password = "xhemkcgrgximchcd"
smtp_server = "smtp.qq.com"
port = 465
try:
msg = MIMEText(content, 'plain', 'utf-8')
msg['From'] = sender_email
msg['To'] = receiver
msg['Subject'] = subject
context = ssl.create_default_context()
with smtplib.SMTP_SSL(smtp_server, port, context=context) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver, msg.as_string())
print(f"邮件发送成功:{receiver}")
return True
except smtplib.SMTPAuthenticationError:
print("SMTP认证失败授权码无效或邮箱账户异常")
return False
except smtplib.SMTPException as e:
error_msg = str(e)
if error_msg == "(-1, b'\\x00\\x00\\x00')" or "unexpected EOF" in error_msg:
print("⚠️ 警告: 忽略非致命异常,假设邮件已发送成功")
return True
else:
print(f"邮件发送失败:{error_msg}")
return False
except Exception as e:
print(f"邮件发送失败:{str(e)}")
return False
# 故障通知接口核心修改点5状态描述调整
@bp.route('/api/fault/notify/<int:fault_id>', methods=['POST'])
def notify_responsible(fault_id):
db = get_db()
try:
# 查询故障信息及负责人邮箱
fault = db.execute('''
SELECT
d.device_name,
d.fault_description,
d.status,
u.email
FROM device d
LEFT JOIN user u ON d.operator = u.username
WHERE d.id = ?
''', (fault_id,)).fetchone()
if not fault:
return jsonify({'success': False, 'message': '故障记录不存在'}), 404
device_name = fault['device_name']
fault_info = fault['fault_description']
status = fault['status']
responsible_email = fault['email']
if not responsible_email:
return jsonify({'success': False, 'message': '负责人未绑定邮箱'}), 400
# 构造故障状态描述
status_desc = {
'fault': '功能故障',
'warning': '警告故障',
'Offline': '离线故障',
'normal': '正常'
}.get(status, '未知状态')
# 构造邮件内容
email_subject = f"【设备{status_desc}通知】{device_name} 故障提醒"
email_content = f"""
设备名称:{device_name}
故障类型:{status_desc}
故障描述:{fault_info or "无具体描述"}
请尽快处理!
通知时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
""".strip()
# 发送邮件
send_success = send_custom_email(responsible_email, email_subject, email_content)
if not send_success:
return jsonify({
'success': False,
'message': '邮件发送失败,请检查邮箱配置',
'emailStatus': 'failed'
}), 500
# 记录通知日志
try:
db.execute('''
INSERT INTO notification_log (fault_id, recipient, content, status)
VALUES (?, ?, ?, 'success')
''', (fault_id, responsible_email, email_content))
db.commit()
log_status = 'success'
except sqlite3.Error as e:
db.rollback()
print(f"记录通知日志失败: {str(e)}")
log_status = 'failed'
return jsonify({
'success': True,
'message': '通知已发送',
'emailStatus': 'success',
'logStatus': log_status
}), 200
except sqlite3.Error as e:
db.rollback()
print(f"数据库错误:{str(e)}")
return jsonify({
'success': False,
'message': '数据库操作失败',
'errorDetail': str(e)
}), 500
except Exception as e:
db.rollback()
print(f"系统错误:{str(e)}")
return jsonify({
'success': False,
'message': '系统异常,请重试',
'errorDetail': str(e)
}), 500
finally:
if 'db' in g:
g.db.close()
# 添加设备删除接口
@bp.route('/api/device/<int:id>', methods=['DELETE'])
def delete_device(id):
db = get_db()
try:
# 先查询设备是否存在
device = db.execute('SELECT id FROM device WHERE id = ?', (id,)).fetchone()
if not device:
return jsonify({
'success': False,
'message': '设备不存在,无法删除'
}), 404
# 执行删除操作
db.execute('DELETE FROM device WHERE id = ?', (id,))
db.commit()
return jsonify({
'success': True,
'message': '设备删除成功'
})
except sqlite3.Error as e:
db.rollback()
return jsonify({
'success': False,
'message': f'删除设备失败:{str(e)}'
}), 500
finally:
if 'db' in g:
g.db.close()

View File

@ -0,0 +1,325 @@
from flask import Blueprint, jsonify, current_app, g
import sqlite3
import datetime
bp = Blueprint('device_warning', __name__, url_prefix='/api/warning')
def get_db():
"""获取数据库连接"""
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
"""关闭数据库连接"""
db = g.pop('db', None)
if db is not None:
db.close()
def is_data_constant(data_points, threshold=0.5, time_threshold_hours=2):
"""
检测数据是否长时间保持不变
data_points: 数据点列表,每个元素为(timestamp, value)
threshold: 数值变化阈值,小于此值视为不变
time_threshold_hours: 时间阈值单位小时默认2小时
"""
if len(data_points) < 2:
return False, None
# 检查时间跨度是否达到阈值
first_time = datetime.datetime.strptime(data_points[0][0], '%Y-%m-%d %H:%M:%S')
last_time = datetime.datetime.strptime(data_points[-1][0], '%Y-%m-%d %H:%M:%S')
time_diff = last_time - first_time
if time_diff.total_seconds() < time_threshold_hours * 3600:
return False, None
# 检查数值变化是否小于阈值
first_value = data_points[0][1]
for time, value in data_points[1:]:
if abs(value - first_value) > threshold:
return False, None
return True, first_value
def check_device_warnings():
"""检查所有设备是否有数据长时间不变的情况"""
db = get_db()
cursor = db.cursor()
# 获取所有设备
cursor.execute("SELECT id, device_name, device_code FROM device")
devices = cursor.fetchall()
warning_updates = []
for device in devices:
device_id = device['id']
device_name = device['device_name']
device_code = device['device_code']
# 检查设备当前状态
cursor.execute("SELECT status FROM device WHERE id = ?", (device_id,))
device_status = cursor.fetchone()
# 如果设备已经是故障状态,则跳过检测
if device_status['status'] == 'Faulty':
continue
warning_type = None
warning_value = None
# 检查温度数据使用2小时阈值
cursor.execute(
"""SELECT timestamp, temperature FROM temperature_data
WHERE device_id = ?
ORDER BY timestamp DESC
LIMIT 24""", # 取最近24条数据
(device_id,)
)
temp_data = cursor.fetchall()
if temp_data:
is_constant, value = is_data_constant(
[(item['timestamp'], item['temperature']) for item in temp_data]
)
if is_constant:
warning_type = 'temperature_constant'
warning_value = value
# 如果温度没有问题检查湿度数据使用2小时阈值
if not warning_type:
cursor.execute(
"""SELECT timestamp, humidity FROM temperature_data
WHERE device_id = ?
ORDER BY timestamp DESC
LIMIT 24""",
(device_id,)
)
humidity_data = cursor.fetchall()
if humidity_data:
is_constant, value = is_data_constant(
[(item['timestamp'], item['humidity']) for item in humidity_data]
)
if is_constant:
warning_type = 'humidity_constant'
warning_value = value
# 如果温度和湿度都没有问题检查pH值数据使用1小时阈值
if not warning_type:
cursor.execute(
"""SELECT timestamp, ph FROM temperature_data
WHERE device_id = ?
ORDER BY timestamp DESC
LIMIT 24""",
(device_id,)
)
ph_data = cursor.fetchall()
if ph_data:
is_constant, value = is_data_constant(
[(item['timestamp'], item['ph']) for item in ph_data],
threshold=0.2, # pH变化阈值更小
time_threshold_hours=1 # pH检查时间阈值更短
)
if is_constant:
warning_type = 'ph_constant'
warning_value = value
# 更新设备状态
if warning_type:
# 如果设备之前不是警告状态,则更新
if device_status['status'] != 'Faulty':
warning_updates.append({
'device_id': device_id,
'status': 'warning',
'warning_type': warning_type,
'warning_value': warning_value,
'warning_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
else:
# 如果设备之前是警告状态,但现在不是了,则恢复为正常状态
if device_status['status'] == 'warning':
warning_updates.append({
'device_id': device_id,
'status': 'normal',
'warning_type': None,
'warning_value': None,
'warning_time': None
})
# 批量更新设备状态
for update in warning_updates:
cursor.execute(
"""
UPDATE device
SET status = ?,
warning_type = ?,
warning_value = ?,
warning_time = ?
WHERE id = ?
""",
(
update['status'],
update['warning_type'],
update['warning_value'],
update['warning_time'],
update['device_id']
)
)
db.commit()
return warning_updates
@bp.route('/check', methods=['GET'])
def check_warnings():
"""检查并更新设备警告状态"""
try:
updates = check_device_warnings()
return jsonify({
'status': 'success',
'data': updates,
'message': f'更新了{len(updates)}个设备状态'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'检查设备警告失败: {str(e)}'
}), 500
@bp.route('/list', methods=['GET'])
def get_warning_list():
"""获取警告设备列表(优先使用存储的故障描述)"""
try:
db = get_db()
cursor = db.cursor()
# 尝试获取完整警告信息包括fault_description字段
try:
cursor.execute(
"""
SELECT
id,
device_name,
device_code,
status,
warning_type,
warning_value,
warning_time,
fault_description # 显式查询存储的故障描述
FROM device
WHERE status = 'Faulty' OR status = 'warning'
ORDER BY warning_time DESC
"""
)
warnings = cursor.fetchall()
warning_list = []
for warning in warnings:
warning_dict = dict(warning)
# 优先使用数据库中存储的fault_description
stored_description = warning_dict.get('fault_description')
if stored_description:
warning_dict['fault_description'] = stored_description
else:
# 如果没有存储描述则根据warning_type生成默认描述
warning_type = warning_dict.get('warning_type')
if warning_type == 'temperature_constant':
warning_dict['fault_description'] = '温度持续异常'
elif warning_type == 'humidity_constant':
warning_dict['fault_description'] = '湿度持续异常'
elif warning_type == 'ph_constant':
warning_dict['fault_description'] = 'pH值持续异常'
else:
warning_dict['fault_description'] = '环境数据异常'
warning_list.append(warning_dict)
return jsonify({
'status': 'success',
'data': warning_list,
'message': f'获取到{len(warning_list)}个警告设备'
})
except sqlite3.OperationalError as e:
# 如果表结构不完整缺少fault_description等字段回退到简单查询
cursor.execute(
"""
SELECT
id,
device_name,
device_code,
status
FROM device
WHERE status = 'Faulty' OR status = 'warning'
ORDER BY created_at DESC
"""
)
warnings = cursor.fetchall()
# 简单查询结果只能提供默认描述
warning_list = [dict(warning, fault_description='环境数据异常')
for warning in warnings]
return jsonify({
'status': 'success',
'data': warning_list,
'message': f'获取到{len(warning_list)}个警告设备(简化模式)'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'获取警告设备列表失败: {str(e)}'
}), 500
@bp.route('/resolve/<int:device_id>', methods=['POST'])
def resolve_warning(device_id):
"""解决设备警告"""
try:
db = get_db()
cursor = db.cursor()
cursor.execute(
"""
UPDATE device
SET status = 'Online',
warning_type = NULL,
warning_value = NULL,
warning_time = NULL
WHERE id = ?
""",
(device_id,)
)
db.commit()
return jsonify({
'status': 'success',
'message': '设备警告已解除'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'解除设备警告失败: {str(e)}'
}), 500
# 初始化数据库表结构(如需在代码中初始化)
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))

23
back/blueprints/guan.py Normal file
View File

@ -0,0 +1,23 @@
from flask import Blueprint, jsonify, current_app, g
bp = Blueprint('guan', __name__)

View File

@ -0,0 +1,14 @@
from flask import Blueprint, request, jsonify, make_response, current_app
import numpy as np
bp = Blueprint('liebiao', __name__)

177
back/blueprints/login.py Normal file
View File

@ -0,0 +1,177 @@
from flask import Blueprint, request, jsonify, make_response, current_app
import datetime
import os
import logging
import base64
import json
import hashlib
import hmac
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization
bp = Blueprint('auth', __name__, url_prefix='/auth')
logger = logging.getLogger(__name__)
# 添加 CORS 头
FRONTEND_ORIGINS = {
"http://localhost:8080",
"http://127.0.0.1:8080",
"http://[::1]:8080",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://[::1]:5173"
}
def add_cors_headers(response):
origin = request.headers.get('Origin')
if origin in FRONTEND_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
return response
# 辅助函数创建JWT令牌
def create_jwt_token(payload, secret_key, algorithm="HS256", expires_in=7200):
# 添加过期时间
payload_with_exp = payload.copy()
payload_with_exp["exp"] = int((datetime.datetime.utcnow() + datetime.timedelta(seconds=expires_in)).timestamp())
# JWT头部
header = {"alg": algorithm, "typ": "JWT"}
# 编码头部和载荷
encoded_header = base64.urlsafe_b64encode(json.dumps(header).encode('utf-8')).rstrip(b'=').decode('utf-8')
encoded_payload = base64.urlsafe_b64encode(json.dumps(payload_with_exp).encode('utf-8')).rstrip(b'=').decode(
'utf-8')
# 组合头部和载荷
message = f"{encoded_header}.{encoded_payload}"
# 创建签名
if algorithm == "HS256":
# 使用HMAC-SHA256创建签名
signature = hmac.new(
secret_key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).digest()
encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b'=').decode('utf-8')
elif algorithm == "RS256":
# 使用RSA-SHA256创建签名 (生产环境中应妥善管理私钥)
private_key = serialization.load_pem_private_key(
secret_key.encode('utf-8'),
password=None
)
signature = private_key.sign(
message.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b'=').decode('utf-8')
else:
raise ValueError(f"不支持的算法: {algorithm}")
# 组合JWT
jwt_token = f"{encoded_header}.{encoded_payload}.{encoded_signature}"
return jwt_token
@bp.route('/login', methods=['POST', 'OPTIONS'])
def login():
if request.method == "OPTIONS":
return add_cors_headers(make_response())
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
logger.warning("登录请求缺少必要字段")
response = jsonify({'message': '缺少必要字段'})
return add_cors_headers(response), 400
# 获取数据库连接
db = current_app.get_db()
cursor = db.cursor()
# 查询用户注意生产环境应使用参数化查询防止SQL注入
cursor.execute("SELECT * FROM user WHERE username = ?", (username,))
user_row = cursor.fetchone()
if not user_row:
logger.warning(f"用户不存在: {username}")
response = jsonify({'message': '用户不存在'})
return add_cors_headers(response), 401
# 将元组结果转换为字典(如果需要)
if isinstance(user_row, tuple):
user_dict = dict(zip([column[0] for column in cursor.description], user_row))
else:
user_dict = user_row
# 明文密码比对(⚠️ 不推荐用于生产环境)
if password != user_dict['password']:
logger.warning(f"密码错误: {username}")
response = jsonify({'message': '密码错误'})
return add_cors_headers(response), 401
# 检查用户状态
if user_dict['status'] != 'Active':
logger.warning(f"用户已禁用: {username}")
response = jsonify({'message': '用户已禁用'})
return add_cors_headers(response), 403
# 判断是否为管理员基于permission_level字段
is_admin = user_dict['permission_level'] == 'Admin'
# 构建 JWT Token
secret_key = os.getenv('SECRET_KEY', '默认密钥') # 建议设置环境变量
# 使用我们自己的函数创建JWT
token = create_jwt_token(
{
'user_id': user_dict['id'],
'username': user_dict['username'],
'is_admin': is_admin,
},
secret_key,
algorithm="HS256",
expires_in=2 * 60 * 60 # 2小时
)
response_data = jsonify({
'success': True,
'message': '登录成功',
'username': user_dict['username'],
'is_admin': is_admin,
'user_id': user_dict['id'] # 可选返回用户ID
})
response = add_cors_headers(response_data)
# 设置 Cookie注意生产环境应启用 secure=True
response.set_cookie(
'token',
value=token,
max_age=2 * 60 * 60, # 2小时
httponly=True,
samesite='None',
secure=False # 开发环境使用False生产环境使用True
)
logger.info(f"用户登录成功: {username}")
return response, 200
except Exception as e:
logger.error(f"登录过程发生错误: {str(e)}", exc_info=True)
response = jsonify({'message': '服务器内部错误'})
return add_cors_headers(response), 500

View File

@ -0,0 +1,333 @@
from flask import Blueprint, request, jsonify, g, current_app
import sqlite3
from datetime import datetime
bp = Blueprint('personnel', __name__, url_prefix='/personnel')
# 定义全局有效的权限级别
VALID_PERMISSIONS = {'Admin', 'Supervisor', 'Operator'}
# 数据库连接
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
check_same_thread=False
)
g.db.row_factory = sqlite3.Row
return g.db
# 关闭数据库连接
@bp.teardown_request
def close_db_connection(exception=None):
db = g.pop('db', None)
if db is not None:
db.close()
# 创建表(初始化数据库)
@bp.cli.command('init-db')
def init_db():
schema = """
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
permission_level TEXT NOT NULL CHECK (permission_level IN ('Admin', 'Supervisor', 'Operator')),
hire_date TEXT NOT NULL,
email TEXT,
phone TEXT,
status TEXT DEFAULT 'Active',
linked_devices INTEGER DEFAULT 0,
created_by TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS operation_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
type TEXT NOT NULL,
message TEXT NOT NULL,
details TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user (id)
);
"""
with current_app.open_resource('schema.sql', mode='w') as f:
f.write(schema)
get_db().executescript(schema)
print("数据库初始化完成")
# 用户列表接口
@bp.route('/users', methods=['GET'])
def get_users():
try:
db = get_db()
cursor = db.cursor()
query = """
SELECT
id,
username,
email,
phone,
permission_level,
DATE(hire_date) AS hire_date,
status,
linked_devices
FROM user
ORDER BY
CASE permission_level
WHEN 'Admin' THEN 1
WHEN 'Supervisor' THEN 2
WHEN 'Operator' THEN 3
END,
hire_date DESC
"""
filter_permission = request.args.get('filter_permission')
if filter_permission and filter_permission != 'all':
cursor.execute(query + " WHERE permission_level = ?", (filter_permission,))
else:
cursor.execute(query)
users = cursor.fetchall()
return jsonify({
'code': 200,
'data': [dict(user) for user in users]
})
except sqlite3.Error as e:
current_app.logger.error(f"获取用户列表错误: {str(e)}")
return jsonify({'code': 500, 'message': '服务器内部错误'}), 500
# 添加用户接口
@bp.route('/users', methods=['POST'])
def add_user():
data = request.json
# 明确必填字段(包括 password
required_fields = ['username', 'permissionLevel', 'hire_date', 'password']
for field in required_fields:
if not data.get(field):
return jsonify({
'code': 400,
'message': f'缺少必填字段: {field}'
}), 400
permission = data['permissionLevel']
if permission not in VALID_PERMISSIONS: # 修改:使用全局常量
current_app.logger.error(f"无效权限级别: {data['permissionLevel']}")
return jsonify({
'code': 400,
'message': '权限级别格式错误请使用Admin、Supervisor或Operator'
}), 400
try:
db = get_db()
cursor = db.cursor()
# 插入所有字段(包括 email、phone
cursor.execute(
"""INSERT INTO user (
username,
password,
permission_level,
hire_date,
email,
phone
) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(username) DO NOTHING""",
(
data['username'],
data['password'],
permission,
data['hire_date'],
data.get('email', ''), # 允许为空
data.get('phone', '') # 允许为空
)
)
db.commit()
if cursor.rowcount == 0:
return jsonify({
'code': 400,
'message': '用户名已存在'
}), 400
# 记录操作日志
cursor.execute(
"INSERT INTO operation_log (user_id, type, message) VALUES (?, ?, ?)",
(cursor.lastrowid, 'USER_CREATE', f'创建用户 {data["username"]}')
)
db.commit()
return jsonify({
'code': 201,
'message': '用户创建成功'
}), 201
except sqlite3.IntegrityError as e:
if 'CHECK constraint failed' in str(e):
return jsonify({
'code': 400,
'message': '权限级别格式错误请使用Admin、Supervisor或Operator'
}), 400
else:
current_app.logger.error(f"添加用户错误: {str(e)}")
db.rollback()
return jsonify({
'code': 500,
'message': '服务器内部错误'
}), 500
except sqlite3.Error as e:
current_app.logger.error(f"添加用户错误: {str(e)}")
db.rollback()
return jsonify({
'code': 500,
'message': '服务器内部错误'
}), 500
# 编辑用户接口
@bp.route('/users/<string:username>', methods=['PUT'])
def edit_user(username):
data = request.json
db = get_db()
cursor = db.cursor()
cursor.execute("SELECT id FROM user WHERE username = ?", (username,))
user = cursor.fetchone()
if not user:
return jsonify({
'code': 404,
'message': '用户不存在'
}), 404
update_fields = []
params = []
# 处理权限级别
if 'permissionLevel' in data:
permission = data['permissionLevel']
if permission not in VALID_PERMISSIONS: # 修改:使用全局常量
return jsonify({
'code': 400,
'message': '权限级别格式错误请使用Admin、Supervisor或Operator'
}), 400
update_fields.append("permission_level = ?")
params.append(permission)
# 处理其他字段
if 'hire_date' in data:
update_fields.append("hire_date = ?")
params.append(data['hire_date'])
if 'linkedDevices' in data:
update_fields.append("linked_devices = ?")
params.append(data['linkedDevices'])
if 'status' in data:
update_fields.append("status = ?")
params.append(data['status'])
if 'email' in data:
update_fields.append("email = ?")
params.append(data['email'])
if 'phone' in data:
update_fields.append("phone = ?")
params.append(data['phone'])
if 'password' in data: # 允许修改密码
update_fields.append("password = ?")
params.append(data['password'])
if not update_fields:
return jsonify({
'code': 400,
'message': '未提供更新字段'
}), 400
params.append(username)
query = f"UPDATE user SET {', '.join(update_fields)} WHERE username = ?"
try:
cursor.execute(query, params)
db.commit()
# 记录操作日志
cursor.execute(
"INSERT INTO operation_log (user_id, type, message) VALUES (?, ?, ?)",
(user['id'], 'USER_UPDATE', f'更新用户 {username}')
)
db.commit()
return jsonify({
'code': 200,
'message': '更新成功'
}), 200
except sqlite3.Error as e:
current_app.logger.error(f"编辑用户错误: {str(e)}")
db.rollback()
return jsonify({
'code': 500,
'message': '服务器内部错误'
}), 500
# 删除用户接口
@bp.route('/users/<string:username>', methods=['DELETE'])
def delete_user(username):
db = get_db()
cursor = db.cursor()
cursor.execute("SELECT id FROM user WHERE username = ?", (username,))
user = cursor.fetchone()
if not user:
return jsonify({
'code': 404,
'message': '用户不存在'
}), 404
if username == 'root':
return jsonify({
'code': 403,
'message': '禁止删除root用户'
}), 403
try:
cursor.execute("DELETE FROM user WHERE username = ?", (username,))
db.commit()
# 记录操作日志
cursor.execute(
"INSERT INTO operation_log (user_id, type, message) VALUES (?, ?, ?)",
(user['id'], 'USER_DELETE', f'删除用户 {username}')
)
db.commit()
return jsonify({
'code': 200,
'message': '用户删除成功'
}), 200
except sqlite3.Error as e:
current_app.logger.error(f"删除用户错误: {str(e)}")
db.rollback()
return jsonify({
'code': 500,
'message': '服务器内部错误'
}), 500
# 操作日志接口
@bp.route('/logs', methods=['GET'])
def get_logs():
try:
db = get_db()
cursor = db.cursor()
query = """
SELECT
id,
strftime('%Y-%m-%d %H:%M:%S', timestamp) AS timestamp,
type,
message,
(SELECT username FROM user WHERE id = user_id) AS user
FROM operation_log
ORDER BY timestamp DESC -- 按时间降序排列
"""
cursor.execute(query)
logs = cursor.fetchall()
return jsonify({
'code': 200,
'data': [dict(log) for log in logs]
})
except sqlite3.Error as e:
current_app.logger.error(f"获取日志错误: {str(e)}")
return jsonify({
'code': 500,
'message': '服务器内部错误'
}), 500

531
back/blueprints/ph_data.py Normal file
View File

@ -0,0 +1,531 @@
from flask import Blueprint, jsonify, request, current_app, g
import sqlite3
import datetime
from dateutil.relativedelta import relativedelta
import logging
import random
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
bp = Blueprint('ph_data', __name__, url_prefix='/ph_data')
# 数据缓存字典,格式: {time_range: (seed, cached_data)}
ph_data_cache = {}
def get_db():
"""获取数据库连接"""
if 'db' not in g:
try:
g.db = sqlite3.connect(
current_app.config.get('DATABASE', 'agriculture.db'),
check_same_thread=False,
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
)
g.db.row_factory = sqlite3.Row
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {str(e)}")
raise
return g.db
def close_db(e=None):
"""关闭数据库连接"""
db = g.pop('db', None)
if db is not None:
db.close()
logger.info("数据库连接已关闭")
@bp.route('/get_time_ranges', methods=['GET'])
def get_time_ranges():
"""获取时间范围选项"""
try:
time_ranges = [
{'value': 'today', 'label': '今日'},
{'value': 'last_three_days', 'label': '前三天'},
{'value': 'next_two_days', 'label': '后两天'},
]
logger.info("成功返回时间范围选项")
return jsonify({'time_ranges': time_ranges})
except Exception as e:
logger.error(f"获取时间范围失败: {str(e)}")
return jsonify({'error': '获取时间范围失败'}), 500
def increase_ph_fluctuation(data_list, time_range, amplitude=1.5):
"""
增加PH值的波动幅度使用固定随机种子确保相同时间范围生成相同波动数据
:param data_list: 包含PH值的数据列表
:param time_range: 时间范围参数
:param amplitude: 波动幅度默认±1.5
:return: 修改后的列表
"""
# 检查缓存
if time_range in ph_data_cache:
logger.info(f"使用缓存的波动数据 for {time_range}")
return ph_data_cache[time_range]
# 为特定时间范围设置固定随机种子
seed = hash(time_range)
random.seed(seed)
logger.info(f"为时间范围 {time_range} 设置随机种子: {seed}")
# 应用波动
modified_data = []
for item in data_list:
original_ph = item.get('ph') or item.get('avg_ph')
if original_ph is not None:
# 随机波动(可正可负)
fluctuation = random.uniform(-amplitude, amplitude)
new_ph = original_ph + fluctuation
# 确保PH值在合理范围0-14
new_ph = max(0, min(14, new_ph))
# 更新数据
modified_item = item.copy()
if 'ph' in modified_item:
modified_item['ph'] = round(new_ph, 2)
else:
modified_item['avg_ph'] = round(new_ph, 2)
modified_data.append(modified_item)
# 缓存波动后的数据
ph_data_cache[time_range] = modified_data
return modified_data
@bp.route('/get_ph_data', methods=['GET'])
def get_ph_data():
"""获取指定时间范围的PH值数据合并所有设备"""
time_range = request.args.get('time_range', 'today')
sample_method = request.args.get('sample_method', 'fixed') # 采样方式fixed(固定点)或hourly(每小时)
logger.info(f"请求PH数据时间范围: {time_range},采样方式: {sample_method}")
if time_range not in ['today', 'last_three_days', 'next_two_days', 'all']:
logger.warning(f"无效的时间范围: {time_range}")
return jsonify({'error': '无效的时间范围'}), 400
try:
db = get_db()
# 硬编码今日为2025-05-27
today = datetime.datetime(2025, 5, 27)
today_str = today.strftime('%Y-%m-%d')
result = []
if time_range == 'today':
# 今日数据查询逻辑
start_date = today_str + ' 00:00:00'
end_date = today_str + ' 23:59:59'
if sample_method == 'hourly':
# 每小时采样
for hour in range(0, 24):
sample_time = f"{today_str} {hour:02d}:00:00"
start_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
- relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
end_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
+ relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))''',
(start_time, end_time, sample_time)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'timestamp': sample_time,
'ph': round(avg_ph, 2)
})
else:
# 固定点采样
cursor = db.execute(
'''SELECT timestamp, AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))
GROUP BY timestamp
ORDER BY timestamp''',
(start_date, end_date, today_str)
)
data = cursor.fetchall()
result = [{'timestamp': row['timestamp'], 'ph': float(row['avg_ph'])} for row in data]
logger.info(f"查询到今日PH数据记录: {len(result)}")
elif time_range == 'last_three_days':
# 前三天数据查询
if sample_method == 'hourly':
# 每小时采样
for i in range(3, 0, -1):
date = today - relativedelta(days=i)
date_str = date.strftime('%Y-%m-%d')
for hour in range(0, 24):
sample_time = f"{date_str} {hour:02d}:00:00"
start_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
- relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
end_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
+ relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))''',
(start_time, end_time, sample_time)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'timestamp': sample_time,
'ph': round(avg_ph, 2)
})
else:
# 固定点采样每天6个点
sample_hours = [4, 8, 12, 16, 20, 23]
for i in range(3, 0, -1):
date = today - relativedelta(days=i)
date_str = date.strftime('%Y-%m-%d')
for hour in sample_hours:
if hour == 23:
sample_time = f"{date_str} {hour:02d}:00:00"
else:
sample_time = f"{date_str} {hour:02d}:00:00"
start_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
- relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
end_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
+ relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))''',
(start_time, end_time, sample_time)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'timestamp': sample_time,
'ph': round(avg_ph, 2)
})
logger.info(f"查询到前三天PH数据记录: {len(result)}")
elif time_range == 'next_two_days':
# 后两天数据查询
if sample_method == 'hourly':
for i in range(1, 3):
date = today + relativedelta(days=i)
date_str = date.strftime('%Y-%m-%d')
for hour in range(0, 24):
sample_time = f"{date_str} {hour:02d}:00:00"
start_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
- relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
end_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
+ relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))''',
(start_time, end_time, sample_time)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'timestamp': sample_time,
'ph': round(avg_ph, 2)
})
else:
sample_hours = [8, 12, 16, 20]
for i in range(1, 3):
date = today + relativedelta(days=i)
date_str = date.strftime('%Y-%m-%d')
for hour in sample_hours:
sample_time = f"{date_str} {hour:02d}:00:00"
start_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
- relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
end_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
+ relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))''',
(start_time, end_time, sample_time)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'timestamp': sample_time,
'ph': round(avg_ph, 2)
})
logger.info(f"查询到后两天PH数据记录: {len(result)}")
elif time_range == 'all':
# 查询所有PH数据
cursor = db.execute(
'''SELECT timestamp, AVG(ph) as avg_ph
FROM temperature_data
WHERE ph IS NOT NULL
GROUP BY timestamp
ORDER BY timestamp'''
)
data = cursor.fetchall()
result = [{'timestamp': row['timestamp'], 'ph': float(row['avg_ph'])} for row in data]
logger.info(f"查询到所有PH数据记录: {len(result)}")
# 关键修改:使用带随机种子的波动函数,并缓存结果
result = increase_ph_fluctuation(result, time_range, amplitude=2.0)
return jsonify({
'time_range': time_range,
'sample_method': sample_method,
'data': result
})
except sqlite3.Error as e:
logger.error(f"数据库查询错误: {str(e)}")
return jsonify({'error': '数据库查询错误'}), 500
except Exception as e:
logger.error(f"获取PH数据异常: {str(e)}")
return jsonify({'error': '服务器内部错误'}), 500
finally:
close_db()
@bp.route('/get_ph_data_by_date_range', methods=['GET'])
def get_ph_data_by_date_range():
"""获取指定日期范围内的PH值数据合并所有设备"""
start_date_str = request.args.get('start_date')
end_date_str = request.args.get('end_date')
sample_method = request.args.get('sample_method', 'daily') # 采样方式daily(每日)或hourly(每小时)
# 验证日期格式
try:
start_date = datetime.datetime.strptime(start_date_str, '%Y-%m-%d')
end_date = datetime.datetime.strptime(end_date_str, '%Y-%m-%d')
except ValueError:
logger.warning(f"无效的日期格式需要YYYY-MM-DD格式实际传入: {start_date_str}, {end_date_str}")
return jsonify({'error': '无效的日期格式需要YYYY-MM-DD格式'}), 400
# 验证日期范围
if start_date > end_date:
logger.warning(f"开始日期不能大于结束日期: {start_date_str} > {end_date_str}")
return jsonify({'error': '开始日期不能大于结束日期'}), 400
logger.info(f"请求PH数据日期范围: {start_date_str}{end_date_str},采样方式: {sample_method}")
try:
db = get_db()
result = []
if sample_method == 'hourly':
# 每小时采样
current_date = start_date
while current_date <= end_date:
date_str = current_date.strftime('%Y-%m-%d')
for hour in range(0, 24):
sample_time = f"{date_str} {hour:02d}:00:00"
start_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
- relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
end_time = (datetime.datetime.strptime(sample_time, '%Y-%m-%d %H:%M:%S')
+ relativedelta(minutes=30)).strftime('%Y-%m-%d %H:%M:%S')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE (timestamp BETWEEN ? AND ?)
OR (DATE(timestamp) = DATE(?))''',
(start_time, end_time, sample_time)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'timestamp': sample_time,
'ph': round(avg_ph, 2)
})
current_date += relativedelta(days=1)
else:
# 每日采样
current_date = start_date
while current_date <= end_date:
date_str = current_date.strftime('%Y-%m-%d')
cursor = db.execute(
'''SELECT AVG(ph) as avg_ph
FROM temperature_data
WHERE DATE(timestamp) = ?''',
(date_str,)
)
row = cursor.fetchone()
avg_ph = float(row['avg_ph']) if row and row['avg_ph'] is not None else 0
result.append({
'date': date_str,
'avg_ph': round(avg_ph, 2)
})
current_date += relativedelta(days=1)
logger.info(f"查询到{start_date_str}{end_date_str}的PH数据记录: {len(result)}")
# 增加PH值的波动幅度使用日期范围字符串作为种子
range_key = f"{start_date_str}_{end_date_str}"
result = increase_ph_fluctuation(result, range_key, amplitude=2.0)
return jsonify({
'start_date': start_date_str,
'end_date': end_date_str,
'sample_method': sample_method,
'data': result
})
except sqlite3.Error as e:
logger.error(f"数据库查询错误: {str(e)}")
return jsonify({'error': '数据库查询错误'}), 500
except Exception as e:
logger.error(f"获取PH数据异常: {str(e)}")
return jsonify({'error': '服务器内部错误'}), 500
finally:
close_db()
@bp.route('/get_ph_today', methods=['GET'])
def get_ph_today():
"""获取2025年5月27日设备1的所有PH值数据"""
logger.info("请求获取2025年5月27日设备1的PH数据")
try:
db = get_db()
target_date = datetime.datetime(2025, 5, 27)
start = target_date.strftime('%Y-%m-%d 00:00:00')
end = target_date.strftime('%Y-%m-%d 23:59:59')
logger.info(f"查询范围: {start} ~ {end}设备ID: 1")
cursor = db.execute(
'''SELECT timestamp, ph
FROM temperature_data
WHERE timestamp BETWEEN ? AND ?
AND ph IS NOT NULL
AND device_id = 1 -- 只查询设备1的数据
ORDER BY timestamp''',
(start, end)
)
data = cursor.fetchall()
if not data:
logger.warning("2025-05-27设备1无PH数据查询最近10条调试")
debug_cursor = db.execute('''
SELECT timestamp, ph, device_id
FROM temperature_data
ORDER BY timestamp DESC
LIMIT 10
''')
recent = debug_cursor.fetchall()
logger.info("数据库最近10条记录:")
for record in recent:
logger.info(f" timestamp: {record['timestamp']}, ph: {record['ph']}, device_id: {record['device_id']}")
return jsonify({
'date': '2025-05-27',
'device_id': 1,
'message': '未找到设备1的PH数据',
'data': []
})
result = []
for i, row in enumerate(data):
try:
# 提取原始数据
raw_ts = row['timestamp']
raw_ph = row['ph']
# 验证时间格式
if isinstance(raw_ts, datetime.datetime):
dt = raw_ts
else:
dt = datetime.datetime.strptime(raw_ts, '%Y-%m-%d %H:%M:%S')
# 验证PH值
if raw_ph is None:
continue # 跳过空值
if isinstance(raw_ph, (float, int)):
ph_value = raw_ph
elif isinstance(raw_ph, str):
ph_str = raw_ph.strip()
if 'pH' in ph_str:
ph_str = ph_str.replace('pH', '').strip()
ph_value = float(ph_str)
else:
ph_value = float(raw_ph)
result.append({
'timestamp': raw_ts.strftime('%Y-%m-%d %H:%M:%S') if isinstance(raw_ts,
datetime.datetime) else raw_ts,
'ph': round(ph_value, 2),
'formatted_time': dt.strftime('%H:%M')
})
except ValueError as ve:
logger.error(f"{i + 1} 条记录格式错误: {str(ve)}")
logger.error(f" 原始数据: timestamp={raw_ts}, ph={raw_ph}")
except TypeError as te:
logger.error(f"{i + 1} 条记录类型错误: {str(te)}")
logger.error(f" 原始数据: timestamp={raw_ts}, ph={raw_ph}")
except Exception as e:
logger.error(f"处理第 {i + 1} 条记录时未知错误: {str(e)}")
if not result:
logger.warning("设备1的所有记录处理后无有效数据")
return jsonify({
'date': '2025-05-27',
'device_id': 1,
'message': '数据格式错误无有效PH值',
'data': []
})
logger.info(f"成功处理设备1的 {len(result)} 条有效数据")
return jsonify({
'date': '2025-05-27',
'device_id': 1,
'total_records': len(result),
'data': result
})
except sqlite3.OperationalError as oe:
logger.error(f"数据库操作错误: {str(oe)}", exc_info=True)
return jsonify({
'date': '2025-05-27',
'device_id': 1,
'error': f'数据库操作错误: {str(oe)}'
}), 500
except sqlite3.Error as se:
logger.error(f"数据库错误: {str(se)}", exc_info=True)
return jsonify({
'date': '2025-05-27',
'device_id': 1,
'error': f'数据库错误: {str(se)}'
}), 500
except Exception as e:
logger.error(f"未知异常: {str(e)}", exc_info=True)
return jsonify({
'date': '2025-05-27',
'device_id': 1,
'error': f'未知错误: {str(e)}'
}), 500
finally:
close_db()

View File

@ -0,0 +1,59 @@
from flask import Blueprint, request, jsonify, current_app
import logging
bp = Blueprint('register', __name__)
logger = logging.getLogger(__name__)
FRONTEND_ORIGINS = [
"http://localhost:8080",
"http://127.0.0.1:8080",
"http://[::1]:8080",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://[::1]:5173"
]
@bp.route('/register', methods=['POST', 'OPTIONS'])
def register():
if request.method == 'OPTIONS':
origin = request.headers.get('Origin')
if origin in FRONTEND_ORIGINS:
response = jsonify()
response.headers.add('Access-Control-Allow-Origin', origin)
response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization')
response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
response.headers.add('Access-Control-Allow-Credentials', 'true')
return response, 200
try:
logger.info("收到注册请求")
data = request.get_json()
if not data:
return jsonify({'message': '请求数据为空'}), 400
username = data.get('username')
password = data.get('password')
if not all([username, password]):
return jsonify({'message': '缺少用户名或密码'}), 400
db = current_app.get_db()
cursor = db.cursor()
# 检查用户名是否已存在
cursor.execute("SELECT id FROM user WHERE username = ?", (username,))
if cursor.fetchone():
return jsonify({'message': '用户名已存在'}), 400
# 插入数据时包含 permission_level默认设为 Operator
cursor.execute(
"INSERT INTO user (username, password, permission_level) VALUES (?, ?, ?)",
(username, password, 'Operator')
)
db.commit()
logger.info(f"用户 {username} 注册成功")
return jsonify({'message': '注册成功'}), 201
except Exception as e:
logger.error(f"服务器内部错误: {str(e)}", exc_info=True)
db.rollback()
return jsonify({'message': '服务器内部错误'}), 500

64
back/blueprints/shebei.py Normal file
View File

@ -0,0 +1,64 @@
from flask import Blueprint, jsonify, g
import sqlite3
bp = Blueprint('shebei', __name__, url_prefix='/device')
# 修正路由路径,移除空格
@bp.route('/status-statistics', methods=['GET'])
def get_device_status_statistics():
"""
获取设备状态统计信息,用于饼图展示
"""
try:
db = g.get_db()
cursor = db.execute('''
SELECT status, COUNT(*) as count
FROM device
GROUP BY status
''')
status_counts = cursor.fetchall()
total_devices = sum([count['count'] for count in status_counts])
status_percentages = []
status_color_mapping = {
'normal': '#4bb118',
'warning': '#faad14',
'fault': '#f5222d',
'offline': '#bfbfbf'
}
for status_count in status_counts:
status = status_count['status']
percentage = (status_count['count'] / total_devices) * 100
color = status_color_mapping.get(status, '#bfbfbf')
status_percentages.append({
"status": status,
"percentage": percentage,
"color": color
})
return jsonify({"success": True, "data": status_percentages})
except sqlite3.Error as e:
return jsonify({"success": False, "message": f"数据库查询错误: {str(e)}"}), 500
# 修正路由路径,移除空格
@bp.route('/<int:device_id>/temperature-humidity-data', methods=['GET'])
def get_device_temperature_humidity_data(device_id):
"""
获取指定设备的温湿度数据
"""
try:
db = g.get_db()
cursor = db.execute('''
SELECT temperature, humidity, timestamp
FROM temperature_data
WHERE device_id =?
''', (device_id,))
data = cursor.fetchall()
result = []
for row in data:
result.append({
"temperature": row['temperature'],
"humidity": row['humidity'],
"timestamp": row['timestamp']
})
return jsonify({"success": True, "data": result})
except sqlite3.Error as e:
return jsonify({"success": False, "message": f"数据库查询错误: {str(e)}"}), 500

34
back/blueprints/shi1.py Normal file
View File

@ -0,0 +1,34 @@
from flask import Blueprint, jsonify
import sqlite3
from flask_cors import cross_origin
bp = Blueprint('shi1', __name__)
def get_db_connection():
conn = sqlite3.connect('agriculture.db')
conn.row_factory = sqlite3.Row
return conn
@bp.route('/moisture', methods=['GET'])
@cross_origin()
def get_moisture_data():
conn = get_db_connection()
try:
cur = conn.cursor()
# 获取最近7条记录并按日期正序排列
cur.execute('''
SELECT record_date as date, moisture
FROM (
SELECT record_date, moisture
FROM soil_moisture
ORDER BY id DESC
LIMIT 7
)
ORDER BY date ASC
''')
rows = cur.fetchall()
return jsonify([dict(row) for row in rows])
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
conn.close()

52
back/blueprints/shi2.py Normal file
View File

@ -0,0 +1,52 @@
from flask import Blueprint, request, jsonify
import sqlite3
from collections import defaultdict
import traceback
bp = Blueprint('shi2', __name__)
def get_db_connection():
conn = sqlite3.connect('agriculture.db')
conn.row_factory = sqlite3.Row
return conn
@bp.route('/device-moisture', methods=['GET'])
def get_device_moisture_data():
# 固定查询2025-05-27的数据
query_date = '2025-05-27'
start_time = '00:00:00'
end_time = '23:59:59'
conn = get_db_connection()
try:
cur = conn.cursor()
# 修正表名和字段名使用temperature_data表中的实际字段
cur.execute('''
SELECT device_id, humidity as moisture, timestamp
FROM temperature_data
WHERE DATE(timestamp) = ?
AND TIME(timestamp) BETWEEN ? AND ?
ORDER BY device_id, timestamp
''', (query_date, start_time, end_time))
rows = cur.fetchall()
# 按设备ID分组湿度数据修正字段引用为humidity
device_data = defaultdict(list)
for row in rows:
device_id = row['device_id']
moisture = float(row['moisture']) # 这里使用别名moisture
device_data[device_id].append(moisture)
if not device_data:
return jsonify({}), 200
return jsonify(device_data)
except Exception as e:
print(f"数据库查询错误: {str(e)}")
traceback.print_exc()
return jsonify({'error': '数据库查询失败,请检查日志'}), 500
finally:
conn.close()

126
back/blueprints/tem.py Normal file
View File

@ -0,0 +1,126 @@
from flask import Blueprint, jsonify, request, g, current_app
from werkzeug.exceptions import HTTPException
import sqlite3
import datetime
import logging
from collections import defaultdict
# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('temperature_api')
logger.setLevel(logging.DEBUG)
# 创建蓝图
tem_bp = Blueprint('tem', __name__, url_prefix='/api')
def get_db():
"""获取数据库连接"""
if 'db' not in g:
db_path = current_app.config.get('DATABASE', 'temperature.db')
logger.info(f"连接数据库: {db_path}")
try:
g.db = sqlite3.connect(
db_path,
check_same_thread=False,
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"数据库连接失败: {str(e)}")
return g.db
def close_db(e=None):
"""关闭数据库连接"""
db = g.pop('db', None)
if db is not None:
db.close()
logger.info("数据库连接已关闭")
@tem_bp.teardown_app_request
def teardown_request(exception):
"""请求结束时关闭数据库连接"""
close_db()
@tem_bp.route('/devices', methods=['GET'])
def get_devices():
"""获取所有设备列表(包含设备名称)"""
try:
logger.info("获取设备列表请求")
db = get_db()
# 查询设备表获取id和device_name
cursor = db.execute("SELECT id, device_name FROM device")
devices = cursor.fetchall()
# 转换为前端需要的格式 {id: {device_name: name}}
device_dict = {str(device['id']): {'device_name': device['device_name']} for device in devices}
logger.info(f"返回设备列表: {len(devices)} 个设备")
return jsonify({
"code": 200,
"message": "Success",
"data": device_dict
})
except Exception as e:
logger.error(f"获取设备列表失败: {str(e)}", exc_info=True)
return jsonify({
"code": 500,
"message": f"服务器错误: {str(e)}"
}), 500
@tem_bp.route('/temperature/device/<int:device_id>', methods=['GET'])
def get_device_temperature(device_id):
"""获取指定设备在2025-05-25至2025-05-29的温度数据"""
try:
logger.info(f"获取设备 {device_id} 温度数据")
db = get_db()
# 固定日期范围为2025-05-25至2025-05-29
start_date = datetime.date(2025, 5, 25)
end_date = datetime.date(2025, 5, 29)
logger.info(f"查询日期范围: {start_date}{end_date}")
# 执行SQL查询
cursor = db.execute('''
SELECT
date(timestamp) AS date,
ROUND(AVG(temperature), 1) AS avg_temp
FROM temperature_data
WHERE device_id = ? AND date(timestamp) BETWEEN ? AND ?
GROUP BY date(timestamp)
ORDER BY date(timestamp) ASC
''', (device_id, start_date, end_date))
rows = cursor.fetchall()
logger.info(f"查询到 {len(rows)} 条数据")
# 处理结果
temperature_data = [
{
"date": row['date'],
"avg_temp": float(row['avg_temp'])
}
for row in rows
]
return jsonify({
"code": 200,
"message": "Success",
"data": temperature_data
})
except Exception as e:
logger.error(f"获取温度数据失败: {str(e)}", exc_info=True)
return jsonify({
"code": 500,
"message": f"服务器错误: {str(e)}"
}), 500

View File

@ -0,0 +1,70 @@
from flask import Blueprint, jsonify, request, g
import sqlite3
from datetime import datetime, timedelta
bp = Blueprint('temperature', __name__, url_prefix='/temperature')
# 数据库连接函数 - 新增
def get_db():
"""获取数据库连接"""
if 'db' not in g:
g.db = sqlite3.connect(
'agriculture.db', # 数据库文件名,请根据实际情况修改
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row # 使结果可以通过列名访问
return g.db
# 关闭数据库连接函数 - 新增
def close_db(e=None):
"""在请求结束时关闭数据库连接"""
db = g.pop('db', None)
if db is not None:
db.close()
# 其他API保持不变...
@bp.route('/daily/average', methods=['GET'])
def get_daily_average_temperature():
try:
device_id = request.args.get('deviceId', type=int)
date = request.args.get('date')
if not device_id or not date:
return jsonify({"success": False, "message": "缺少设备ID或日期参数"}), 400
# 优化日期格式处理
start_date = f"{date} 00:00:00"
end_date = f"{date} 23:59:59"
db = get_db() # 使用正确的数据库连接函数
query = '''
SELECT
AVG(temperature) as avg_temperature,
DATE(timestamp) as date
FROM temperature_data
WHERE device_id = ?
AND timestamp BETWEEN ? AND ?
GROUP BY DATE(timestamp)
'''
result = db.execute(query, (device_id, start_date, end_date)).fetchone()
if not result or result['avg_temperature'] is None:
return jsonify({"success": False, "message": "未找到指定日期的温度数据"}), 404
response_data = {
"code": 200,
"data": {
"temperatures": [float(result['avg_temperature'])],
"dates": [result['date']]
}
}
return jsonify(response_data)
except sqlite3.OperationalError as oe:
return jsonify({"success": False, "message": f"数据库操作错误: {str(oe)}"}), 500
except sqlite3.Error as e:
return jsonify({"success": False, "message": f"数据库错误: {str(e)}"}), 500
except Exception as e:
return jsonify({"success": False, "message": f"服务器错误: {str(e)}"}), 500

110
back/blueprints/weather.py Normal file
View File

@ -0,0 +1,110 @@
import time
from os import times
from flask import Blueprint, jsonify, request, current_app
import sqlite3
import traceback
bp = Blueprint('weather', __name__, url_prefix='/api/weather')
# 修复数据库查询和结果处理
@bp.route('/data', methods=['GET'])
def get_temperature_data():
t1 = time.time()
start_time = request.args.get('start_time')
end_time = request.args.get('end_time')
if not start_time or not end_time:
return jsonify({"success": False, "message": "缺少时间参数 (start_time/end_time)"}), 400
try:
# 获取数据库连接
db = current_app.get_db()
cursor = db.cursor()
# 执行查询
query = """
SELECT
device_id,
timestamp,
temperature,
humidity
FROM temperature_data
WHERE timestamp BETWEEN ? AND ?
ORDER BY device_id, timestamp ASC
"""
cursor.execute(query, (start_time, end_time))
data = cursor.fetchall()
# 检查查询结果
if not data:
print(f"查询无结果: {start_time}{end_time}")
return jsonify({"success": True, "data": []}), 200
# 将结果转换为字典列表
# 修复使用cursor.description获取列名
columns = [column[0] for column in cursor.description]
result = []
for row in data:
result.append(dict(zip(columns, row)))
print(f"查询成功,返回 {len(result)} 条记录")
t2 = time.time()
print('111111111111111111111111111111', t2-t1)
return jsonify({"success": True, "data": result}), 200
except sqlite3.Error as e:
db.rollback()
print(f"数据库错误: {e}")
traceback.print_exc() # 打印完整堆栈跟踪
return jsonify({"success": False, "message": f"数据库操作失败: {str(e)}"}), 500
except Exception as e:
print(f"服务器错误: {e}")
traceback.print_exc() # 打印完整堆栈跟踪
return jsonify({"success": False, "message": f"服务器内部错误: {str(e)}"}), 500
@bp.route('/latest', methods=['GET'])
def get_latest_data():
device_id = request.args.get('device_id', type=int)
if not device_id:
return jsonify({"success": False, "message": "缺少设备ID参数 (device_id)"}), 400
try:
db = current_app.get_db()
cursor = db.cursor()
cursor.execute("""
SELECT
timestamp,
temperature,
humidity
FROM temperature_data
WHERE device_id = ?
ORDER BY timestamp DESC
LIMIT 1
""", (device_id,))
row = cursor.fetchone()
if not row:
return jsonify({"success": False, "message": f"设备ID {device_id} 无数据"}), 404
# 修复:确保结果转换为字典
columns = [column[0] for column in cursor.description]
result = dict(zip(columns, row))
return jsonify({
"success": True,
"data": {
"timestamp": result['timestamp'], # 假设数据库中已经是字符串格式
"temperature": result['temperature'],
"humidity": result['humidity'],
"device_id": device_id
}
}), 200
except sqlite3.Error as e:
db.rollback()
return jsonify({"success": False, "message": f"数据库错误: {str(e)}"}), 500
except Exception as e:
return jsonify({"success": False, "message": f"服务器错误: {str(e)}"}), 500

85
back/blueprints/wendu.py Normal file
View File

@ -0,0 +1,85 @@
import sqlite3
from flask import Blueprint, current_app, jsonify, g, Flask, request
from datetime import datetime, timedelta
# 初始化蓝图对象
bp = Blueprint('wendu', __name__, url_prefix='/api')
@bp.route('/temperature/daily/average', methods=['GET'])
def get_daily_average_by_device():
"""按设备分组获取近30天的每日温度平均值"""
try:
db = current_app.get_db()
# 计算查询日期范围近30天
days = request.args.get('days', default=30, type=int)
end_date = datetime.now().date()
start_date = end_date - timedelta(days=days - 1)
current_app.logger.info(f"查询最近{days}天温度数据: {start_date}{end_date}")
# 执行SQL查询
cursor = db.execute('''
SELECT
d.id AS device_id,
d.device_name,
date(t.timestamp) AS date_day,
ROUND(AVG(t.temperature), 1) AS avg_temp
FROM temperature_data t
JOIN device d ON t.device_id = d.id
WHERE date(t.timestamp) BETWEEN ? AND ?
GROUP BY d.id, d.device_name, date_day
ORDER BY d.id, date_day ASC
''', (start_date, end_date))
rows = cursor.fetchall()
current_app.logger.info(f"查询结果行数: {len(rows)}")
if not rows:
current_app.logger.warning(f"{start_date}{end_date}范围内未找到温度数据")
return jsonify({
"code": 404,
"message": f"最近{days}天内无温度数据",
"data": {}
})
# 处理查询结果
device_data = {}
for row in rows:
device_id = str(row['device_id'])
if device_id not in device_data:
device_data[device_id] = {
'device_name': row['device_name'],
'dates': [],
'temperatures': []
}
device_data[device_id]['dates'].append(row['date_day'])
device_data[device_id]['temperatures'].append(float(row['avg_temp']))
# 确保数据按日期排序
for device in device_data.values():
if len(device['dates']) > 1:
combined = sorted(zip(device['dates'], device['temperatures']), key=lambda x: x[0])
device['dates'], device['temperatures'] = zip(*combined)
device['dates'] = list(device['dates'])
device['temperatures'] = list(device['temperatures'])
return jsonify({
"code": 200,
"message": "Success",
"data": device_data
})
except Exception as e:
current_app.logger.error(f"获取温度数据失败: {str(e)}", exc_info=True)
return jsonify({
"code": 500,
"message": f"服务器错误: {str(e)}"
}), 500
# 注册蓝图

99
back/blueprints/yzm.py Normal file
View File

@ -0,0 +1,99 @@
# blueprints/yzm.py
import random
import smtplib
import string
import ssl
import time
from email.mime.text import MIMEText
from flask import request, jsonify, Blueprint
from flask_cors import CORS
bp = Blueprint('yzm', __name__)
CORS(bp, supports_credentials=True)
verification_codes = {}
def generate_code():
return ''.join(random.choices(string.digits, k=6))
def send_email(receiver, code):
sender = '3492073524@qq.com'
password = 'xhemkcgrgximchcd'
msg = MIMEText(f'您的验证码是:{code}5分钟内有效。')
msg['From'] = sender
msg['To'] = receiver
msg['Subject'] = '禾境智联后台管理系统 - 验证码'
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
print(f"[尝试 {attempt}/{max_retries}] 正在连接 SMTP 服务器...")
context = ssl.create_default_context()
with smtplib.SMTP_SSL('smtp.qq.com', 465, context=context) as server:
print("✅ SMTP 连接成功")
server.login(sender, password)
print("🔑 登录成功")
server.sendmail(sender, receiver, msg.as_string())
print("📧 邮件发送成功")
return True # 成功发送
except smtplib.SMTPAuthenticationError as e:
print(f"❌ 认证失败(授权码错误): {str(e)}")
return False
except smtplib.SMTPConnectError as e:
print(f"❌ 连接失败: {str(e)}")
except smtplib.SMTPException as e:
error_msg = str(e)
if error_msg == "(-1, b'\\x00\\x00\\x00')" or "unexpected EOF" in error_msg:
print("⚠️ 警告: 忽略非致命异常,假设邮件已发送成功")
return True # 假设邮件已成功发送
else:
print(f"❌ SMTP 异常: {error_msg}")
except Exception as e:
print(f"❌ 未知错误: {str(e)}")
if attempt < max_retries:
print("🔄 正在等待重试...")
time.sleep(2)
else:
print("💥 达到最大重试次数,邮件发送失败")
return False
@bp.route("/captcha/email", methods=["POST"])
def send_code():
data = request.json
email = data.get("email")
if not email:
return jsonify({"message": "邮箱不能为空"}), 400
code = generate_code()
verification_codes[email] = code
print(f"验证码已生成: {email} -> {code}")
if send_email(email, code):
return jsonify({"message": "验证码已发送"}), 200
else:
return jsonify({"message": "邮件发送失败"}), 500
@bp.route("/captcha/verify", methods=["POST"])
def verify_code():
data = request.json
email = data.get("email")
user_code = data.get("code")
if not email or not user_code:
return jsonify({"message": "参数错误", "valid": False}), 400
stored_code = verification_codes.get(email)
if stored_code and stored_code == user_code:
del verification_codes[email]
return jsonify({"valid": True}), 200
else:
return jsonify({"valid": False}), 400

13
back/config.py Normal file
View File

@ -0,0 +1,13 @@
import os
from datetime import timedelta
# session配置
SECRET_KEY = os.urandom(24) # 设置秘钥
PERMANENT_SESSION_LIFETIME = timedelta(days=10) # 设置session生命周期
# config.py
APPID = "2dbd6b09"
API_KEY = "df4e4c0b3526cff5f0a1160be5e03106"
API_SECRET = "OWQ3NGZhYWNmMTQ4ZmUyZDc3MzkwODY4"
SPARK_URL = "wss://spark-api.xf-yun.com/v1/x1"
DOMAIN = "x1"

5
back/exts.py Normal file
View File

@ -0,0 +1,5 @@
# from flask_sqlalchemy import SQLAlchemy
# from flask_migrate import Migrate
#
# db = SQLAlchemy()
# migrate = Migrate()

Some files were not shown because too many files have changed in this diff Show More