Update .gitignore and add files
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
back/agriculture.db
|
||||
back/__pycache__/
|
8
back/.idea/.gitignore
generated
vendored
Normal file
8
back/.idea/.gitignore
generated
vendored
Normal 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
1
back/.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
exts.py
|
8
back/.idea/back.iml
generated
Normal file
8
back/.idea/back.iml
generated
Normal 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
8
back/.idea/backward_bg.iml
generated
Normal 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
12
back/.idea/dataSources.xml
generated
Normal 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>
|
12
back/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
back/.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
6
back/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
back/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
back/.idea/misc.xml
generated
Normal 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
8
back/.idea/modules.xml
generated
Normal 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
6
back/.idea/sqldialects.xml
generated
Normal 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
6
back/.idea/vcs.xml
generated
Normal 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>
|
BIN
back/DIY_gccpu_96_96/real_prediction.npy
Normal file
BIN
back/DIY_gccpu_96_96/real_prediction.npy
Normal file
Binary file not shown.
164
back/ai_processor.py
Normal file
164
back/ai_processor.py
Normal 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
168
back/app.py
Normal 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)
|
0
back/blueprints/__init__.py
Normal file
0
back/blueprints/__init__.py
Normal file
BIN
back/blueprints/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/aiask.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/aiask.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/aiask.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/aiask.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/aiask.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/aiask.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chohai.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/chohai.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chohai.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/chohai.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chohai.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/chohai.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chou1.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/chou1.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chou1.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/chou1.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chou2.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/chou2.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chou2.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/chou2.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chou3.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/chou3.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/chou3.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/chou3.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/device.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/device.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/device.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/device.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/device.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/device.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/device_warning.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/device_warning.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/device_warning.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/device_warning.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/device_warning.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/device_warning.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/guan.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/guan.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/guan.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/guan.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/liebiao.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/liebiao.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/liebiao.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/liebiao.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/login.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/login.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/login.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/login.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/login.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/login.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/personnel.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/personnel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/personnel.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/personnel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/personnel.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/personnel.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/ph_data.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/ph_data.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/ph_data.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/ph_data.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/ph_data.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/ph_data.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/register.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/register.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/register.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/register.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/register.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/register.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shebei.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/shebei.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shebei.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/shebei.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shebei.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/shebei.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shi1.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/shi1.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shi1.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/shi1.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shi1.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/shi1.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shi2.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/shi2.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shi2.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/shi2.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/shi2.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/shi2.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/tem.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/tem.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/tem.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/tem.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/tem.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/tem.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/temperature.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/temperature.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/temperature.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/temperature.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/temperature.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/temperature.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/weather.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/weather.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/weather.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/weather.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/weather.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/weather.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/wendu.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/wendu.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/wendu.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/wendu.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/wendu.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/wendu.cpython-39.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/yzm.cpython-311.pyc
Normal file
BIN
back/blueprints/__pycache__/yzm.cpython-311.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/yzm.cpython-38.pyc
Normal file
BIN
back/blueprints/__pycache__/yzm.cpython-38.pyc
Normal file
Binary file not shown.
BIN
back/blueprints/__pycache__/yzm.cpython-39.pyc
Normal file
BIN
back/blueprints/__pycache__/yzm.cpython-39.pyc
Normal file
Binary file not shown.
265
back/blueprints/aiask.py
Normal file
265
back/blueprints/aiask.py
Normal 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
91
back/blueprints/chohai.py
Normal 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
199
back/blueprints/chou1.py
Normal 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
172
back/blueprints/chou2.py
Normal 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
93
back/blueprints/chou3.py
Normal 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
614
back/blueprints/device.py
Normal 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()
|
325
back/blueprints/device_warning.py
Normal file
325
back/blueprints/device_warning.py
Normal 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
23
back/blueprints/guan.py
Normal file
@ -0,0 +1,23 @@
|
||||
from flask import Blueprint, jsonify, current_app, g
|
||||
|
||||
bp = Blueprint('guan', __name__)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
14
back/blueprints/liebiao.py
Normal file
14
back/blueprints/liebiao.py
Normal 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
177
back/blueprints/login.py
Normal 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
|
333
back/blueprints/personnel.py
Normal file
333
back/blueprints/personnel.py
Normal 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
531
back/blueprints/ph_data.py
Normal 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()
|
59
back/blueprints/register.py
Normal file
59
back/blueprints/register.py
Normal 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
64
back/blueprints/shebei.py
Normal 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
34
back/blueprints/shi1.py
Normal 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
52
back/blueprints/shi2.py
Normal 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
126
back/blueprints/tem.py
Normal 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
|
70
back/blueprints/temperature.py
Normal file
70
back/blueprints/temperature.py
Normal 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
110
back/blueprints/weather.py
Normal 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
85
back/blueprints/wendu.py
Normal 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
99
back/blueprints/yzm.py
Normal 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
13
back/config.py
Normal 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
5
back/exts.py
Normal 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
Reference in New Issue
Block a user