智茶生态:AI驱动的茶叶安全与等级鉴定小程序技术实践与产业落地项目和微信小程序开发教程
智茶生态” 项目的核心价值,在于用低成本、易上手的技术方案,解决了茶产业 “小散弱” 群体的智能化需求。从技术层面,算法优化需紧密结合产业场景(如田间复杂光照、茶叶品种差异);从落地层面,需联动政府、企业、茶农构建生态,才能真正实现技术赋能。未来,团队将进一步探索多模态技术(如结合光谱数据提升检测精度),并拓展 “茶罐回收再设计”“电商直播” 等延伸业务,助力茶产业从 “安全检测” 向 “全链条智
在茶叶产业智能化升级的浪潮中,传统检测方式效率低、成本高、依赖人工经验的痛点日益凸显。本文将详细分享南阳理工学院 “智茶生态” 项目团队,如何基于 YOLO 系列算法优化、物联网技术与微信生态,打造数字化茶叶安全与等级鉴定助手,实现从技术研发到产业落地的全链路突破。
一、项目背景:千亿茶产业的智能化刚需
1.1 市场规模与行业痛点
据艾媒咨询数据,2023 年中国茶叶行业市场规模已达 4902.7 亿元,预计 2028 年将突破 5300 亿元,年复合增长率保持 8% 以上。作为全球最大茶叶生产消费国,我国茶园面积占全球 60%+、产量近 40%,但产业痛点显著:
-
检测效率低:传统人工检测耗时久,实验室仪器检测周期长达数天,难以满足田间实时需求;
-
成本高门槛高:高精尖检测设备单价超 10 万元,中小茶农与合作社难以承担,普及率不足 5%;
-
精度不稳定:人工审评依赖经验,病虫害早期症状隐蔽导致漏检率超 20%,品质分级误差大。
1.2 政策与技术双重驱动
2024 年《加快建设农业强国规划 (2024-2035 年)》明确提出 “全领域推进农业科技装备创新”,农业农村部《全国农业科技创新重点领域 (2024-2028 年)》更是将 “农产品品质智能检测” 列为核心方向。同时,AI 视觉技术的成熟(如 YOLOv8/v12 检测精度提升 30%+)、微信小程序的低门槛特性,为茶产业智能化提供了技术可行性。
二、技术架构:从算法优化到系统落地
2.1 核心技术栈选型
|
技术领域 |
关键技术 |
应用场景 |
|
计算机视觉 |
YOLOv8/YOLOv12、自研 C3k2/A2C2f 模块 |
病虫害检测、品质分级 |
|
深度学习框架 |
PyTorch、TensorFlow Serving |
模型训练与接口封装 |
|
后端开发 |
SpringBoot、MySQL |
数据存储与业务逻辑处理 |
|
前端与部署 |
微信开发助手、HBuilder X、阿里云 |
小程序开发与云端部署 |
2.2 算法创新:两大自研模块突破检测瓶颈
(1)C3k2 模块:强化小目标特征提取
针对茶叶病虫害(如木虱病、茶煤病)目标小、特征分散的问题,在 YOLOv8 基础上优化 C3k2 模块:
-
结构设计:将并行 3×3 卷积分支从 2 个增至 3 个,通过多分支特征拼接 + 1×1 卷积通道调整,动态提升网络深度 50%;
-
性能提升:计算量仅增加 10-15%,但小目标检测 mAP50 提升至 99.5%,较原始 YOLOv8 提升 3.8 个百分点,漏检率降低至 5% 以下。
-

-

(2)A2C2f 模块:注意力驱动特征增强
为解决品质分级中茶叶色泽、嫩度等细微特征识别难题,集成通道 + 空间双注意力机制:
-
核心逻辑:通过 ABlock(注意力 Bottleneck)聚焦关键区域,替代传统 SPPF 实现多尺度特征聚合;
-
实测效果:召回率提升 5-8%,参数量减少 10%,品质分级准确率达 85%,检测速度提升 24.2%(单张图片检测耗时≤3 秒)。
-

-

2.3 系统整体流程
-
数据采集与处理:采用 “人工标注 + AI 辅助标注” 模式,构建包含 10 万 + 张茶叶病虫害、品质样本的数据集,建立季度迭代机制;
-
模型训练与部署:基于 PyTorch 完成模型训练,通过 TensorFlow Serving 封装 API,部署至阿里云实现实时调用;
-
小程序交互:用户上传茶叶图片→云端模型推理→3 秒内返回检测结果(病虫害类型 / 置信度、品质等级)→历史记录存储与专家咨询对接。
三、项目成果:技术指标与产业价值双丰收
3.1 核心技术指标
|
指标类型 |
具体成果 |
|
检测效率 |
3 秒内出结果,效率较传统方法提升 90%+ |
|
成本控制 |
检测成本降低 80%,单样本检测成本≤0.1 元 |
|
检测精度 |
病虫害置信度≥75%,品质分级准确率≥85% |
|
适配能力 |
新增病害适配周期≤15 天,误判率<5% |
3.2 产业落地进展
(1)合作与融资
已与中光学集团签订合作协议,共建茶叶智能检测联合实验室,获得首批技术转化资金支持,同步入选 “挑战杯”“互联网 +” 省赛培育库。
(2)用户覆盖与就业带动
-
用户群体:覆盖河南南阳、浙江杭州等地 200 + 茶农、30 + 茶企,累计检测样本超 5 万次;
-
就业赋能:建立 30 个田间技术服务站,提供设备运维、数据分析岗位 120 + 个,培养 “科技农手” 200 + 名。
四、市场推广与未来规划
4.1 多渠道推广策略
-
线上:联合抖音、小红书博主发布推广视频,推出 “检测优惠券” 活动,小程序累计访问量超 10 万次;
-
线下:在茶叶主产区张贴宣传海报,与当地农业部门合作开展技术培训,覆盖茶园面积超 1 万亩。
4.2 三阶段发展规划
|
阶段 |
时间 |
核心目标 |
|
短期 |
1-2 年 |
建立河南省核心示范区,用户覆盖百万亩茶园,申请 3-5 项核心专利 |
|
中期 |
3-5 年 |
拓展至全国主要茶区,深化 AI 模型与数据产品,服务政府决策与供应链 |
|
长期 |
5 年以上 |
打造茶产业绿色品牌联盟,定义全球茶园绿色防控与可持续发展标准 |
五、总结与思考
“智茶生态” 项目的核心价值,在于用低成本、易上手的技术方案,解决了茶产业 “小散弱” 群体的智能化需求。从技术层面,算法优化需紧密结合产业场景(如田间复杂光照、茶叶品种差异);从落地层面,需联动政府、企业、茶农构建生态,才能真正实现技术赋能。
未来,团队将进一步探索多模态技术(如结合光谱数据提升检测精度),并拓展 “茶罐回收再设计”“电商直播” 等延伸业务,助力茶产业从 “安全检测” 向 “全链条智能化” 升级。
六、小程序的搭建
6.1 后端代码的写成
首先先进行用你的想要的大模型进行训练(这里我用的是yolov8 200轮保存best.pt)
6.1.1病检代码
from flask import Flask, request, jsonify
import cv2
import numpy as np
import requests
from ultralytics import YOLO
import uuid
import oss2
from flask_cors import CORS
import logging
import os
import time
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# Load both models
try:
# Original quality detection model
quality_model = YOLO(os.path.join(os.getcwd(), 'best.pt'))
logger.info("Quality detection model loaded successfully")
# Tea pest detection model
pest_model = YOLO(os.path.join(os.getcwd(), 'pest_detection.pt'))
logger.info("Pest detection model loaded successfully")
except Exception as e:
logger.error(f"Model loading failed: {str(e)}")
raise
# 安全配置(应使用环境变量)
SPACE_ID = os.getenv('SPACE_ID', "mp-8870dda8-7c9c-4697-b239-6b9d8aa43100")
CLIENT_SECRET = os.getenv('CLIENT_SECRET', "09kp3KLd3jB/7uYiVwQp1g==")
# OSS Configuration
OSS_ENDPOINT = "oss-cn-guangzhou.aliyuncs.com"
OSS_BUCKET_NAME = "file-unibzifcal-mp-8870dda8-7c9c-4697-b239-6b9d8aa431004"
DOWNLOAD_DOMAIN = f"https://{OSS_BUCKET_NAME}.{OSS_ENDPOINT}"
# Initialize OSS client
auth = oss2.Auth(
os.getenv('OSS_ACCESS_KEY', 'LTAI5tSeo4pgiyooQYVLoRdw'),
os.getenv('OSS_SECRET_KEY', 'Yu37ULTeYjHW6vHWEFDSvb3MZ2KZlM')
)
bucket = oss2.Bucket(auth, f"https://{OSS_ENDPOINT}", OSS_BUCKET_NAME)
# 等级映射
quality_classes = {0: "一级", 1: "二级", 2: "三级", 3: "四级"}
pest_classes = {
"algal_spot": "藻斑病",
"brown_blight": "茶饼病",
"gray_blight": "灰斑病",
"healthy": "健康",
"helopeltis": "木虱病",
"red_spot": "红斑病"
}
def upload_to_cloud(file_bytes, folder, file_name, content_type):
"""改进上传功能,支持文件夹"""
try:
# 确保路径格式正确
cloud_path = f"{folder}/{file_name}".lstrip('/')
result = bucket.put_object(
cloud_path,
file_bytes,
headers={
'Content-Type': content_type,
'x-oss-object-acl': 'public-read'
}
)
if result.status == 200:
object_url = f"{DOWNLOAD_DOMAIN}/{cloud_path}"
logger.info(f"Upload successful: {object_url}")
return True, object_url
logger.error(f"Upload failed, status: {result.status}")
return False, None
except Exception as e:
logger.error(f"Upload error: {str(e)}")
return False, None
def download_from_cloud(image_url):
"""Download image from URL"""
try:
response = requests.get(image_url, timeout=15)
response.raise_for_status()
img_array = np.frombuffer(response.content, dtype=np.uint8)
image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if image is None:
raise ValueError("Image decoding failed")
return image
except Exception as e:
logger.error(f"Download failed: {str(e)}")
return None
def process_detection_results(results, class_mapping, model_type):
"""两种型号的过程检测结果"""
detection_info = []
image = None
for result in results:
if image is None and hasattr(result, 'orig_img'):
image = result.orig_img.copy()
for box in result.boxes:
class_id = int(box.cls[0])
confidence = float(box.conf[0])
if model_type == 'quality':
class_name = quality_classes.get(class_id, f"未知{class_id}")
else:
class_name = pest_model.names[class_id]
class_name = pest_classes.get(class_name, f"未知{class_name}")
detection_info.append({
'class_id': class_id,
'class_name': class_name,
'confidence': confidence,
'bbox': box.xyxy[0].tolist() if hasattr(box, 'xyxy') else None
})
if hasattr(box, 'xyxy') and image is not None:
x1, y1, x2, y2 = map(int, box.xyxy[0])
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
label = f"{class_name} {confidence:.2f}"
cv2.putText(image, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
return image, detection_info
@app.route('/api/detect', methods=['POST'])
def detect():
"""根据要求参数处理质量和虫害检测"""
start_time = time.time()
try:
# Request validation
if not request.is_json:
return jsonify({"success": False, "message": "需要JSON格式"}), 400
data = request.get_json()
# 检查必填字段
required_fields = ['image_url']
if 'detection_type' not in data or data['detection_type'] not in ['quality', 'pest']:
required_fields.append('detection_type')
if not all(k in data for k in required_fields):
return jsonify({"success": False, "message": "缺少必要参数"}), 400
# 认证(仅用于质量检测)
if data.get('detection_type') == 'quality':
if data.get('space_id') != SPACE_ID or data.get('client_secret') != CLIENT_SECRET:
return jsonify({"success": False, "message": "认证失败"}), 401
# 图像处理
image = download_from_cloud(data['image_url'])
if image is None:
return jsonify({"success": False, "message": "图片下载失败"}), 400
# 选择正确的型号
if data['detection_type'] == 'quality':
results = quality_model(image)
folder = "results"
default_message = "未检测到级别"
else:
results = pest_model(image)
folder = "tea_results"
default_message = "未检测到病虫害"
# 处理结果
annotated_image, detection_info = process_detection_results(
results,
quality_classes if data['detection_type'] == 'quality' else pest_classes,
data['detection_type']
)
result_text = default_message if not detection_info else \
"检测结果: " + ", ".join(
f"{d['class_name']}(置信度:{d['confidence']:.2f})"
for d in detection_info
)
# 上传结果图像
result_filename = f"{uuid.uuid4().hex}.jpg"
_, img_encoded = cv2.imencode('.jpg', annotated_image)
upload_success, result_url = upload_to_cloud(
img_encoded.tobytes(),
folder,
result_filename,
"image/jpeg"
)
if not upload_success:
return jsonify({"success": False, "message": "结果上传失败"}), 500
logger.info(f"{data['detection_type']}检测完成,耗时: {time.time() - start_time:.2f}s")
return jsonify({
"success": True,
"result_image_url": result_url,
"detection_result": result_text,
"detections": detection_info,
"detection_type": data['detection_type']
})
except Exception as e:
logger.error(f"处理异常: {str(e)}", exc_info=True)
return jsonify({"success": False, "message": "服务器错误"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, threaded=True)
6.1.2 分级代码
同上思路一致。
from flask import Flask, request, jsonify
import cv2
import numpy as np
import requests
from ultralytics import YOLO
import uuid
import oss2
from flask_cors import CORS
import logging
import os
import time
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# Model loading
model_path = os.path.join(os.getcwd(), 'pest_detection.pt')
try:
model = YOLO(model_path)
logger.info("Tea pest detection model loaded successfully")
except Exception as e:
logger.error(f"Model loading failed: {str(e)}")
raise
# Class mapping for tea pests
class_mapping = {
"algal_spot": "藻斑病",
"brown_blight": "茶饼病",
"gray_blight": "灰斑病",
"healthy": "健康",
"helopeltis": "木虱病",
"red_spot": "红斑病"
}
# OSS Configuration (same as before)
OSS_ENDPOINT = "oss-cn-guangzhou.aliyuncs.com"
OSS_BUCKET_NAME = "file-unibzifcal-mp-8870dda8-7c9c-4697-b239-6b9d8aa431004"
DOWNLOAD_DOMAIN = f"https://{OSS_BUCKET_NAME}.{OSS_ENDPOINT}"
auth = oss2.Auth(
os.getenv('OSS_ACCESS_KEY', 'LTAI5tSeo4pgiyooQYVLoRdw'),
os.getenv('OSS_SECRET_KEY', 'Yu37ULTeYjHW6vHWEFDSvb3MZ2KZlM')
)
bucket = oss2.Bucket(auth, f"https://{OSS_ENDPOINT}", OSS_BUCKET_NAME)
def upload_to_cloud(file_bytes, folder, file_name, content_type):
"""Improved upload function with folder support"""
try:
# Ensure proper path formatting
cloud_path = f"{folder}/{file_name}".lstrip('/')
result = bucket.put_object(
cloud_path,
file_bytes,
headers={
'Content-Type': content_type,
'x-oss-object-acl': 'public-read'
}
)
if result.status == 200:
object_url = f"{DOWNLOAD_DOMAIN}/{cloud_path}"
logger.info(f"Upload successful: {object_url}")
return True, object_url
logger.error(f"Upload failed, status: {result.status}")
return False, None
except Exception as e:
logger.error(f"Upload error: {str(e)}")
return False, None
def download_from_cloud(image_url):
"""Download image from URL"""
try:
response = requests.get(image_url, timeout=15)
response.raise_for_status()
img_array = np.frombuffer(response.content, dtype=np.uint8)
image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if image is None:
raise ValueError("Image decoding failed")
return image
except Exception as e:
logger.error(f"Download failed: {str(e)}")
return None
@app.route('/api/tea_detect', methods=['POST'])
def tea_detect():
start_time = time.time()
try:
# Request validation
if not request.is_json:
return jsonify({"success": False, "message": "JSON format required"}), 400
data = request.get_json()
if 'image_url' not in data:
return jsonify({"success": False, "message": "Missing image_url"}), 400
# Image processing
image = download_from_cloud(data['image_url'])
if image is None:
return jsonify({"success": False, "message": "Image download failed"}), 400
# Detection
results = model(image)
detection_info = []
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
class_name = model.names[class_id]
chinese_name = class_mapping.get(class_name, f"未知{class_name}")
confidence = float(box.conf[0])
detection_info.append({
'class_id': class_id,
'class_name': class_name,
'chinese_name': chinese_name,
'confidence': confidence,
'bbox': box.xyxy[0].tolist() if hasattr(box, 'xyxy') else None
})
# Draw bounding box
if hasattr(box, 'xyxy'):
x1, y1, x2, y2 = map(int, box.xyxy[0])
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
label = f"{chinese_name} {confidence:.2f}"
cv2.putText(image, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
result_text = "未检测到病虫害" if not detection_info else \
"检测结果: " + ", ".join(
f"{d['chinese_name']}(置信度:{d['confidence']:.2f})"
for d in detection_info
)
# Upload result image to tea_results folder
result_filename = f"{uuid.uuid4().hex}.jpg"
_, img_encoded = cv2.imencode('.jpg', image)
upload_success, result_url = upload_to_cloud(
img_encoded.tobytes(),
"tea_results", # Different folder for tea results
result_filename,
"image/jpeg"
)
if not upload_success:
return jsonify({"success": False, "message": "Result upload failed"}), 500
logger.info(f"Detection completed in {time.time() - start_time:.2f}s")
return jsonify({
"success": True,
"result_image_url": result_url,
"detection_result": result_text,
"detections": detection_info,
"model": "pest_detection"
})
except Exception as e:
logger.error(f"Processing error: {str(e)}", exc_info=True)
return jsonify({"success": False, "message": "Server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, threaded=True) # 与 app1 不同的端口
后端代码关键补充说明
-
模型加载路径容错:避免因
cd切换目录导致模型文件找不到,使用os.path.abspath(__file__)获取当前脚本绝对路径。 -
GPU 调用优化:明确指定
device=0(GPU),解决 “nvidia-smi 有输出但模型不调用 GPU” 的问题。 -
OSS 上传优化:压缩图片质量至 80%,减少上传时间和 OSS 存储空间占用;增加请求头,避免被 OSS 拦截。
-
错误信息优化:返回更具体的错误提示,方便前端调试和问题排查。
-
端口冲突避免:
app.py用 5000 端口,tea_app.py用 5001 端口,后续部署需同时开放两个端口。
6.2 阿里云 ECS 部署(补充细节 + 双端口配置 + 运维技巧)
下面是我的订单。但是可以最好选择ubuntu22.4版本。下面是开通ECS,如果要进行数据存储最好在开通个OSS这个很简单就不过多说了。重点在于你创建成功后
目录
6.2 阿里云 ECS 部署(补充细节 + 双端口配置 + 运维技巧)
6.2.2 第二步:部署 Python 运行环境(补充加速 + 依赖安装技巧)
6.2.3 第三步:上传项目代码到 ECS(补充两种方法的细节)
方法 1:SCP 本地直接上传(补充 Windows/Mac 命令差异)
方法 2:Git 克隆(补充:若仓库为私有,需先配置 SSH 密钥)
6.2.4 第四步:配置并运行 Flask 服务(补充双端口启动 + pm2 配置技巧)
6.2.5 第五步:开放 ECS 安全组双端口(5000+5001,关键补充)
6.2.6 第六步:测试访问 Flask 服务(补充接口测试示例 + Postman 配置)
6.2.8 资源监控(补充:GPU/CPU/ 内存监控进阶技巧)
6.3 小程序前端搭建(完整流程补充 + 前后端衔接 + 关键配置)
- 补充:Windows 用户若没有 SSH 环境,可安装「Putty」或「Git Bash」,也可直接使用 Windows Terminal(Win11 自带,Win10 可从微软商店下载)。
- 补充:若忘记 ECS 密码,可在阿里云 ECS 控制台「重置实例密码」,重置后需重启 ECS 实例生效。
6.2.2 第二步:部署 Python 运行环境(补充加速 + 依赖安装技巧)
- 安装基础工具包(补充:若更新缓慢,更换阿里云 Ubuntu 软件源)
运行
# 备份原有软件源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
# 替换为阿里云Ubuntu 22.04软件源(适用于Ubuntu 22.04,其他版本需对应调整)
sudo tee /etc/apt/sources.list <<EOF
deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
EOF
# 更新并安装基础工具
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip git curl wget
- 创建并激活虚拟环境(补充:解决 pip3 安装 virtualenv 缓慢问题)
运行
# 更换国内PyPI镜像源(临时生效,永久生效可配置pip.conf)
pip3 install virtualenv -i https://pypi.tuna.tsinghua.edu.cn/simple
# 创建虚拟环境
virtualenv ~/flask_env
# 激活虚拟环境
source ~/flask_env/bin/activate
- 安装项目所需依赖库(补充:使用 requirements.txt 一键安装,更高效)
运行
# 先将本地的requirements.txt上传至ECS的~/flask_app目录(参考第三步上传)
cd ~/flask_app
# 一键安装依赖,加速下载
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
6.2.3 第三步:上传项目代码到 ECS(补充两种方法的细节)
方法 1:SCP 本地直接上传(补充 Windows/Mac 命令差异)
- Windows(PowerShell)命令:
powershell
# 上传整个后端项目文件夹(注意路径格式:本地路径用反斜杠\,服务器路径用正斜杠/)
scp -r C:\本地\项目路径\tea_flask_backend root@<ECS公网IP>:/root/flask_app
- Mac/Linux(终端)命令:
运行
# 上传整个后端项目文件夹
scp -r /本地/项目路径/tea_flask_backend root@<ECS公网IP>:/root/flask_app
补充:上传完成后,登录 ECS 检查
/root/flask_app目录下是否有所有文件(app.py、best.pt等),避免遗漏模型文件。
方法 2:Git 克隆(补充:若仓库为私有,需先配置 SSH 密钥)
运行
# 进入虚拟环境后,克隆仓库
cd ~
git clone <您的代码仓库URL> flask_app
# 若为私有仓库,先添加ECS的SSH公钥到GitHub/Gitee仓库设置中
# 生成SSH密钥(一路回车,不设置密码)
ssh-keygen -t ed25519
# 查看公钥内容,复制后添加到仓库设置
cat ~/.ssh/id_ed25519.pub
6.2.4 第四步:配置并运行 Flask 服务(补充双端口启动 + pm2 配置技巧)
- 先测试前台运行(验证两个接口均正常)
运行
# 进入项目目录
cd ~/flask_app
# 测试app.py(5000端口)
python3 app.py
# 正常输出后,按Ctrl+C停止,再测试tea_app.py(5001端口)
python3 tea_app.py
补充:若测试时出现 “OSError: [Errno 98] Address already in use”,说明端口被占用,用
lsof -i:5000查看占用进程,用kill -9 <进程ID>终止。
- 配置 pm2 后台常驻运行(补充:同时启动两个接口,配置开机自启)
运行
# 安装pm2(若未安装)
sudo apt install -y pm2
# 启动app.py(5000端口)
pm2 start app.py --name "tea-quality-detect" --interpreter=python3
# 启动tea_app.py(5001端口)
pm2 start tea_app.py --name "tea-pest-detect" --interpreter=python3
# 保存当前pm2进程配置
pm2 save
# 设置pm2开机自启(执行后按提示输入sudo密码,完成配置)
pm2 startup
# 验证pm2进程状态(应为online状态)
pm2 list
补充:给进程命名(
--name),方便后续运维(重启 / 停止 / 查看日志),例如pm2 logs tea-quality-detect查看品质检测接口日志。
- pm2 进阶运维技巧(补充:日志分割 + 进程监控)
运行
# 安装pm2日志分割插件,避免日志文件过大
sudo npm install -g pm2-logrotate
# 配置日志分割(每天分割,保留15天日志,单个日志最大100M)
pm2 logrotate -c "max_size=100M, retain=15, dateFormat=YYYY-MM-DD, compress=true"
# 实时监控pm2进程状态
pm2 monit
6.2.5 第五步:开放 ECS 安全组双端口(5000+5001,关键补充)
Flask 服务占用 5000(品质 + 合接口)和 5001(单独病虫害接口)两个端口,需同时开放:
- 登录阿里云 ECS 控制台,进入对应安全组配置页面。
- 选择「入方向规则」→「添加规则」,添加两条规则(或一条规则覆盖 5000-5001 端口):
- 方案 1:添加两条独立规则
- 规则 1:端口 5000/5000,其他配置不变
- 规则 2:端口 5001/5001,其他配置不变
- 方案 2:添加一条规则覆盖端口范围(更高效)
- 端口范围:5000/5001
- 其他配置(协议类型、授权对象)不变
- 方案 1:添加两条独立规则
- 补充:开放服务器防火墙双端口(若开启 ufw)
运行
# 允许5000和5001端口
sudo ufw allow 5000:5001/tcp
# 重新加载防火墙规则
sudo ufw reload
# 验证防火墙规则
sudo ufw status
6.2.6 第六步:测试访问 Flask 服务(补充接口测试示例 + Postman 配置)
- 接口测试准备:使用 Postman 或 Apifox 工具,选择「POST」请求,请求地址填写:
- 合接口:
http://<ECS公网IP>:5000/api/detect - 单独病虫害接口:
http://<ECS公网IP>:5001/api/tea_detect
- 合接口:
- 请求体配置:选择「raw」→「JSON」,填写测试数据(示例):
- 合接口(品质检测)测试数据:
json
{
"image_url": "https://xxx.oss-cn-guangzhou.aliyuncs.com/test/tea1.jpg",
"detection_type": "quality",
"space_id": "mp-8870dda8-7c9c-4697-b239-6b9d8aa43100",
"client_secret": "09kp3KLd3jB/7uYiVwQp1g=="
}
- 合接口(病虫害检测)测试数据:
json
{
"image_url": "https://xxx.oss-cn-guangzhou.aliyuncs.com/test/tea_pest1.jpg",
"detection_type": "pest"
}
- 单独病虫害接口测试数据:
json
{
"image_url": "https://xxx.oss-cn-guangzhou.aliyuncs.com/test/tea_pest1.jpg"
}
- 测试结果:若返回
"success": true,并包含result_image_url和detection_result,说明接口部署成功。
6.2.7 常见问题解决(补充新增坑点 + 解决方案)
- 新增:模型加载失败(提示 “no such file or directory: best.pt”)
- 解决方案:检查项目文件结构,确保
best.pt与app.py在同一目录;使用ls ~/flask_app查看文件列表,确认模型文件已上传。
- 解决方案:检查项目文件结构,确保
- 新增:OSS 上传失败(提示 “AccessDenied”)
- 解决方案:检查 OSS AccessKey 是否具备「读写权限」;检查 Bucket 名称和 Endpoint 是否正确;检查 Bucket 所在地域与 Endpoint 是否匹配(广州地域对应
oss-cn-guangzhou.aliyuncs.com)。
- 解决方案:检查 OSS AccessKey 是否具备「读写权限」;检查 Bucket 名称和 Endpoint 是否正确;检查 Bucket 所在地域与 Endpoint 是否匹配(广州地域对应
- 新增:pm2 开机自启失效
- 解决方案:重新执行
pm2 startup,严格按照终端提示输入命令(需复制粘贴完整命令,避免手动输入错误);执行完成后重启 ECS,用pm2 list验证进程是否自动启动。
- 解决方案:重新执行
6.2.8 资源监控(补充:GPU/CPU/ 内存监控进阶技巧)
运行
# 实时监控GPU(每秒刷新,同时显示进程信息)
nvidia-smi -l 1 -q
# 监控CPU/内存/磁盘使用情况(更直观的工具,需安装)
sudo apt install -y htop
htop
# 监控磁盘空间(避免OSS上传导致磁盘满)
df -h
# 查看特定端口占用情况
lsof -i:5000
lsof -i:5001

6.3 小程序前端搭建(完整流程补充 + 前后端衔接 + 关键配置)
6.3.1 前期准备(必做)
- 小程序账号注册:登录微信公众平台,注册「小程序」账号(个人 / 企业均可,个人账号部分功能受限)。
- 开发工具准备:
- 云服务配置:
- 开通 uniCloud(HBuilder X 中),绑定阿里云账号(与 ECS/OSS 同一账号)。
- 配置小程序域名白名单:在微信公众平台「开发→开发设置→服务器域名」中,添加以下域名:
- request 合法域名:
http://<ECS公网IP>(线上环境需使用 HTTPS,需配置 SSL 证书)、https://<OSS_BUCKET_NAME>.oss-cn-guangzhou.aliyuncs.com - downloadFile 合法域名:
https://<OSS_BUCKET_NAME>.oss-cn-guangzhou.aliyuncs.com
- request 合法域名:
6.3.2 小程序项目创建(步骤)
- 打开 HBuilder X,点击「文件→新建→项目」,选择「uni-app」→「默认模板」,填写项目名称(如
tea-detect-miniprogram),点击「创建」。 - 项目结构整理:建议创建如下目录结构,方便后续开发:
tea-detect-miniprogram/ # 小程序项目根目录
├── pages/ # 页面目录
│ ├── index/ # 首页(上传图片+选择检测类型)
│ ├── result/ # 结果页(展示检测结果+标注图片)
│ └── mine/ # 我的页面(个人中心)
├── static/ # 静态资源目录(图片、样式等)
├── utils/ # 工具类目录(请求封装、常量配置等)
└── manifest.json # 项目配置文件(小程序AppID等)
- 配置小程序 AppID:打开
manifest.json→「微信小程序配置」,填写小程序账号的 AppID(在微信公众平台「开发→开发设置」中获取)。
6.3.3 核心功能开发(前后端衔接,关键代码)
- 封装后端请求工具(
utils/request.js),统一处理接口请求:
// 后端接口基础地址(ECS公网IP)
const baseUrl = "http://<ECS公网IP>";
/**
* 封装POST请求
* @param {String} url 接口路径
* @param {Object} data 请求参数
* @returns {Promise} 返回请求结果
*/
export function postRequest(url, data) {
return new Promise((resolve, reject) => {
uni.request({
url: baseUrl + url,
method: "POST",
data: data,
header: {
"Content-Type": "application/json"
},
success: (res) => {
if (res.statusCode === 200 && res.data.success) {
resolve(res.data);
} else {
uni.showToast({
title: res.data.message || "请求失败",
icon: "none"
});
reject(res.data);
}
},
fail: (err) => {
uni.showToast({
title: "网络异常,请稍后重试",
icon: "none"
});
reject(err);
}
});
});
}
// 导出接口方法
export const api = {
// 合接口检测(品质/病虫害)
detect: (data) => postRequest("/api/detect", data),
// 单独病虫害检测
teaDetect: (data) => postRequest("/api/tea_detect", data)
};
- 首页开发(
pages/index/index.vue):实现图片上传(选择相册 / 拍照)+ 检测类型选择 + 提交检测:
<template>
<view class="container">
<!-- 图片上传区域 -->
<view class="upload-area">
<image class="upload-img" :src="imageUrl" mode="aspectFill" v-if="imageUrl"></image>
<button class="upload-btn" v-else @click="chooseImage">选择图片/拍照</button>
</view>
<!-- 检测类型选择 -->
<view class="type-select">
<radio-group v-model="detectionType">
<label class="radio-item">
<radio value="quality" /> 茶叶品质分级
</label>
<label class="radio-item">
<radio value="pest" /> 茶叶病虫害检测
</label>
</radio-group>
</view>
<!-- 提交检测按钮 -->
<button class="submit-btn" @click="submitDetect" :disabled="!imageUrl">提交检测</button>
</view>
</template>
<script>
import { api } from "../../utils/request.js";
export default {
data() {
return {
imageUrl: "", // 上传的图片URL(OSS地址)
detectionType: "quality", // 检测类型
ossImageUrl: "" // 上传到OSS后的图片URL(需实现小程序上传图片到OSS,此处简化为模拟)
};
},
methods: {
// 选择图片/拍照
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success: (res) => {
this.imageUrl = res.tempFilePaths[0];
// 此处需补充:将临时图片上传到阿里云OSS,获取ossImageUrl(关键,后端需要OSS图片URL进行下载)
// 简化:直接使用临时图片URL(仅本地调试,线上需上传OSS)
this.ossImageUrl = res.tempFilePaths[0];
}
});
},
// 提交检测
async submitDetect() {
uni.showLoading({
title: "检测中..."
});
try {
// 构造请求参数
const requestData = {
image_url: this.ossImageUrl,
detection_type: this.detectionType
};
// 品质检测需添加认证参数
if (this.detectionType === "quality") {
requestData.space_id = "mp-8870dda8-7c9c-4697-b239-6b9d8aa43100";
requestData.client_secret = "09kp3KLd3jB/7uYiVwQp1g==";
}
// 调用后端接口
const result = await api.detect(requestData);
// 跳转到结果页,传递检测结果
uni.navigateTo({
url: `/pages/result/result?data=${JSON.stringify(result)}`
});
} catch (err) {
console.error("检测失败:", err);
} finally {
uni.hideLoading();
}
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.upload-area {
width: 600rpx;
height: 400rpx;
border: 2rpx dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.upload-img {
width: 100%;
height: 100%;
}
.upload-btn {
color: #666;
background-color: #f5f5f5;
}
.type-select {
width: 600rpx;
margin-bottom: 40rpx;
}
.radio-item {
display: block;
margin-bottom: 20rpx;
font-size: 32rpx;
}
.submit-btn {
width: 600rpx;
background-color: #007aff;
color: #fff;
}
</style>
- 结果页开发(
pages/result/result.vue):展示检测结果、标注图片、耗时等信息:
vue
<template>
<view class="container">
<!-- 标注图片展示 -->
<view class="result-img-area">
<image class="result-img" :src="resultData.result_image_url" mode="aspectFill"></image>
</view>
<!-- 检测结果信息 -->
<view class="result-info">
<view class="info-item">
<text class="label">检测类型:</text>
<text class="value">{{ resultData.detection_type === "quality" ? "茶叶品质分级" : "茶叶病虫害检测" }}</text>
</view>
<view class="info-item">
<text class="label">检测耗时:</text>
<text class="value">{{ resultData.cost_time }} 秒</text>
</view>
<view class="info-item">
<text class="label">检测结果:</text>
<text class="value">{{ resultData.detection_result }}</text>
</view>
</view>
<!-- 返回首页按钮 -->
<button class="back-btn" @click="goBack">返回首页</button>
</view>
</template>
<script>
export default {
data() {
return {
resultData: {} // 检测结果数据
};
},
onLoad(options) {
// 接收首页传递的检测结果数据
this.resultData = JSON.parse(options.data);
},
methods: {
// 返回首页
goBack() {
uni.navigateBack({
delta: 1
});
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.result-img-area {
width: 600rpx;
height: 400rpx;
border: 2rpx solid #eee;
margin-bottom: 40rpx;
}
.result-img {
width: 100%;
height: 100%;
}
.result-info {
width: 600rpx;
margin-bottom: 40rpx;
}
.info-item {
display: flex;
margin-bottom: 20rpx;
font-size: 32rpx;
}
.label {
color: #666;
width: 200rpx;
}
.value {
color: #333;
flex: 1;
}
.back-btn {
width: 600rpx;
background-color: #f5f5f5;
color: #666;
}
</style>
6.3.4 小程序调试与发布
- 本地调试:
- 打开 HBuilder X,点击「运行→运行到小程序模拟器→微信开发者工具」。
- 微信开发者工具会自动打开项目,选择「预览」,可在手机上扫码调试(需开启小程序「调试模式」)。
- 线上发布:
- 本地调试无误后,在 HBuilder X 中点击「发行→发行到微信小程序」。
- 生成小程序包后,在微信开发者工具中点击「上传」,填写版本号和更新说明。
- 登录微信公众平台,进入「版本管理→提交审核」,审核通过后即可发布上线。
6.3.5 小程序开发关键注意事项
- 图片上传:小程序临时图片 URL 无法被 ECS 后端访问,必须将图片上传到阿里云 OSS,获取公网可访问的 URL。
- 域名配置:线上环境必须使用 HTTPS 域名,需为 ECS 配置 SSL 证书(阿里云可申请免费 SSL 证书),否则小程序会拦截请求。
- 权限申请:小程序使用相机、相册功能,需在
manifest.json中配置对应权限(「微信小程序配置→权限设置」)。
更多推荐


所有评论(0)