在茶叶产业智能化升级的浪潮中,传统检测方式效率低、成本高、依赖人工经验的痛点日益凸显。本文将详细分享南阳理工学院 “智茶生态” 项目团队,如何基于 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 系统整体流程

  1. 数据采集与处理:采用 “人工标注 + AI 辅助标注” 模式,构建包含 10 万 + 张茶叶病虫害、品质样本的数据集,建立季度迭代机制;

  2. 模型训练与部署:基于 PyTorch 完成模型训练,通过 TensorFlow Serving 封装 API,部署至阿里云实现实时调用;

  3. 小程序交互:用户上传茶叶图片→云端模型推理→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 不同的端口

后端代码关键补充说明

  1. 模型加载路径容错:避免因cd切换目录导致模型文件找不到,使用os.path.abspath(__file__)获取当前脚本绝对路径。

  2. GPU 调用优化:明确指定device=0(GPU),解决 “nvidia-smi 有输出但模型不调用 GPU” 的问题。

  3. OSS 上传优化:压缩图片质量至 80%,减少上传时间和 OSS 存储空间占用;增加请求头,避免被 OSS 拦截。

  4. 错误信息优化:返回更具体的错误提示,方便前端调试和问题排查。

  5. 端口冲突避免app.py用 5000 端口,tea_app.py用 5001 端口,后续部署需同时开放两个端口。

6.2 阿里云 ECS 部署(补充细节 + 双端口配置 + 运维技巧)

下面是我的订单。但是可以最好选择ubuntu22.4版本。下面是开通ECS,如果要进行数据存储最好在开通个OSS这个很简单就不过多说了。重点在于你创建成功后

目录

一、项目背景:千亿茶产业的智能化刚需

1.1 市场规模与行业痛点

1.2 政策与技术双重驱动

二、技术架构:从算法优化到系统落地

2.1 核心技术栈选型

2.2 算法创新:两大自研模块突破检测瓶颈

(1)C3k2 模块:强化小目标特征提取

(2)A2C2f 模块:注意力驱动特征增强

2.3 系统整体流程

三、项目成果:技术指标与产业价值双丰收

3.1 核心技术指标

3.2 产业落地进展

(1)合作与融资

(2)用户覆盖与就业带动

四、市场推广与未来规划

4.1 多渠道推广策略

4.2 三阶段发展规划

五、总结与思考

六、小程序的搭建

6.1 后端代码的写成

6.1.1病检代码

6.1.2 分级代码

6.2 阿里云 ECS 部署(补充细节 + 双端口配置 + 运维技巧)

6.2.1 第一步:连接 ECS 实例(SSH 登录)

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.7 常见问题解决(补充新增坑点 + 解决方案)

6.2.8 资源监控(补充:GPU/CPU/ 内存监控进阶技巧)

6.3 小程序前端搭建(完整流程补充 + 前后端衔接 + 关键配置)

6.3.1 前期准备(必做)

6.3.2 小程序项目创建(步骤)

6.3.3 核心功能开发(前后端衔接,关键代码)

6.3.4 小程序调试与发布

6.3.5 小程序开发关键注意事项


  • 补充:Windows 用户若没有 SSH 环境,可安装「Putty」或「Git Bash」,也可直接使用 Windows Terminal(Win11 自带,Win10 可从微软商店下载)。
  • 补充:若忘记 ECS 密码,可在阿里云 ECS 控制台「重置实例密码」,重置后需重启 ECS 实例生效。

6.2.2 第二步:部署 Python 运行环境(补充加速 + 依赖安装技巧)

  1. 安装基础工具包(补充:若更新缓慢,更换阿里云 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
  1. 创建并激活虚拟环境(补充:解决 pip3 安装 virtualenv 缓慢问题)

运行

# 更换国内PyPI镜像源(临时生效,永久生效可配置pip.conf)
pip3 install virtualenv -i https://pypi.tuna.tsinghua.edu.cn/simple

# 创建虚拟环境
virtualenv ~/flask_env

# 激活虚拟环境
source ~/flask_env/bin/activate
  1. 安装项目所需依赖库(补充:使用 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.pybest.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 配置技巧)

  1. 先测试前台运行(验证两个接口均正常)

运行

# 进入项目目录
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>终止。

  1. 配置 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查看品质检测接口日志。

  1. 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(单独病虫害接口)两个端口,需同时开放:

  1. 登录阿里云 ECS 控制台,进入对应安全组配置页面。
  2. 选择「入方向规则」→「添加规则」,添加两条规则(或一条规则覆盖 5000-5001 端口):
    • 方案 1:添加两条独立规则
      • 规则 1:端口 5000/5000,其他配置不变
      • 规则 2:端口 5001/5001,其他配置不变
    • 方案 2:添加一条规则覆盖端口范围(更高效)
      • 端口范围:5000/5001
      • 其他配置(协议类型、授权对象)不变
  3. 补充:开放服务器防火墙双端口(若开启 ufw)

运行

# 允许5000和5001端口
sudo ufw allow 5000:5001/tcp

# 重新加载防火墙规则
sudo ufw reload

# 验证防火墙规则
sudo ufw status

6.2.6 第六步:测试访问 Flask 服务(补充接口测试示例 + Postman 配置)

  1. 接口测试准备:使用 Postman 或 Apifox 工具,选择「POST」请求,请求地址填写:
    • 合接口:http://<ECS公网IP>:5000/api/detect
    • 单独病虫害接口:http://<ECS公网IP>:5001/api/tea_detect
  2. 请求体配置:选择「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"
}
  1. 测试结果:若返回"success": true,并包含result_image_urldetection_result,说明接口部署成功。

6.2.7 常见问题解决(补充新增坑点 + 解决方案)

  1. 新增:模型加载失败(提示 “no such file or directory: best.pt”)
    • 解决方案:检查项目文件结构,确保best.ptapp.py在同一目录;使用ls ~/flask_app查看文件列表,确认模型文件已上传。
  2. 新增:OSS 上传失败(提示 “AccessDenied”)
    • 解决方案:检查 OSS AccessKey 是否具备「读写权限」;检查 Bucket 名称和 Endpoint 是否正确;检查 Bucket 所在地域与 Endpoint 是否匹配(广州地域对应oss-cn-guangzhou.aliyuncs.com)。
  3. 新增: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 前期准备(必做)

  1. 小程序账号注册:登录微信公众平台,注册「小程序」账号(个人 / 企业均可,个人账号部分功能受限)。
  2. 开发工具准备:
    • 下载安装「HBuilder X」(官方下载地址),安装「uni-app」插件。
    • 下载安装「微信开发者工具」(官方下载地址),用于小程序调试和发布。
  3. 云服务配置:
    • 开通 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

6.3.2 小程序项目创建(步骤)

  1. 打开 HBuilder X,点击「文件→新建→项目」,选择「uni-app」→「默认模板」,填写项目名称(如tea-detect-miniprogram),点击「创建」。
  2. 项目结构整理:建议创建如下目录结构,方便后续开发:
tea-detect-miniprogram/  # 小程序项目根目录
├── pages/               # 页面目录
│   ├── index/           # 首页(上传图片+选择检测类型)
│   ├── result/          # 结果页(展示检测结果+标注图片)
│   └── mine/            # 我的页面(个人中心)
├── static/              # 静态资源目录(图片、样式等)
├── utils/               # 工具类目录(请求封装、常量配置等)
└── manifest.json        # 项目配置文件(小程序AppID等)
  1. 配置小程序 AppID:打开manifest.json→「微信小程序配置」,填写小程序账号的 AppID(在微信公众平台「开发→开发设置」中获取)。

6.3.3 核心功能开发(前后端衔接,关键代码)

  1. 封装后端请求工具(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)
};
  1. 首页开发(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>
  1. 结果页开发(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 小程序调试与发布

  1. 本地调试:
    • 打开 HBuilder X,点击「运行→运行到小程序模拟器→微信开发者工具」。
    • 微信开发者工具会自动打开项目,选择「预览」,可在手机上扫码调试(需开启小程序「调试模式」)。
  2. 线上发布:
    • 本地调试无误后,在 HBuilder X 中点击「发行→发行到微信小程序」。
    • 生成小程序包后,在微信开发者工具中点击「上传」,填写版本号和更新说明。
    • 登录微信公众平台,进入「版本管理→提交审核」,审核通过后即可发布上线。

6.3.5 小程序开发关键注意事项

  1. 图片上传:小程序临时图片 URL 无法被 ECS 后端访问,必须将图片上传到阿里云 OSS,获取公网可访问的 URL。
  2. 域名配置:线上环境必须使用 HTTPS 域名,需为 ECS 配置 SSL 证书(阿里云可申请免费 SSL 证书),否则小程序会拦截请求。
  3. 权限申请:小程序使用相机、相册功能,需在manifest.json中配置对应权限(「微信小程序配置→权限设置」)。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐