企业解决方案十一-各类小程序定制开发

随着人工智能技术的快速发展,AI图像处理已成为移动互联网领域的热门应用方向。本文将详细介绍一个基于Spring Boot后端框架和uni-app前端框架开发的AI图像处理小程序从架构设计到功能实现的全过程。文章涵盖了Spring Boot 2.7微服务架构、MyBatis-Plus ORM框架集成、JWT无状态认证机制、阿里云OSS文件存储服务、阿里云通义万相AI图像处理接口对接、uni-app跨平台开发等核心技术的实际应用。通过本文的学习,读者可以掌握构建一套完整的"AI图像处理小程序"技术方案,同时了解小程序定制开发的相关服务。

关键词:Spring Boot;uni-app;MyBatis-Plus;JWT认证;阿里云OSS;AI图像处理;微信小程序;定制开发


一、项目背景与需求分析

1.1 项目背景

在当今数字化时代,图像处理需求呈现出爆发式增长态势。从社交媒体用户对照片美化的需求,到电商平台对商品图片批量处理的需求,再到创意工作者对AI辅助设计工具的渴望,AI图像处理技术正在深刻改变着我们的工作和生活方式。特别是在微信小程序生态中,轻量化、即用即走的AI图像处理工具受到了广大用户的热烈欢迎。

本文所介绍的项目正是在这样的背景下诞生的。该项目是一款专注于AI图像处理的微信小程序,用户可以通过该小程序上传图片,并结合文本描述对图片进行智能编辑和创作。小程序后端采用成熟的Spring Boot框架构建,前端则采用支持多端运行的uni-app框架开发,实现了高性能、高可用、易扩展的技术架构。

1.2 核心功能需求

通过对市场需求的深入调研和分析,项目确定了以下核心功能需求:

用户管理模块是整个系统的基础模块。系统采用邮箱验证码的登录方式,用户无需注册复杂的账号密码,只需输入邮箱地址即可快速完成登录。首次登录的用户会自动创建账号,并获得一定次数的免费使用额度。用户可以在个人中心查看自己的使用记录、剩余次数,并进行联系客服等操作。

图片上传模块负责处理用户上传的图片文件。系统需要支持常见的图片格式,包括JPG、PNG等,同时对上传文件的大小进行限制,确保服务器资源的高效利用。系统还实现了基于MD5值的文件去重功能,对于相同内容的图片可以避免重复存储,有效节约阿里云OSS的存储成本。

AI图像处理模块是项目的核心功能模块。系统对接了阿里云通义万相AI服务,用户可以通过输入文字描述来指导AI对图片进行修改和创作。例如,用户可以输入"将背景替换为蓝天白云"、"添加卡通特效"等描述,AI模型会根据描述对图片进行智能处理,并返回处理后的结果图片。

文件管理模块提供了完整的文件生命周期管理功能。用户可以查看自己上传的所有图片文件,可以对文件进行重命名、删除等操作。系统采用软删除机制,删除的文件会标记为已删除状态,便于后续的数据恢复和审计。

1.3 技术选型原则

在技术选型过程中,项目团队遵循了以下核心原则:

成熟稳定优先:选择经过大量项目验证的技术框架和组件,确保系统的稳定性和可靠性。Spring Boot作为Java生态中最成熟的微服务框架之一,拥有活跃的社区支持和丰富的生态组件,非常适合作为企业级后端服务的开发框架。

开发效率至上:充分利用框架提供的自动化配置和开箱即用的功能,减少重复代码的编写。MyBatis-Plus作为MyBatis的增强框架,提供了丰富的CRUD操作封装和分页插件,可以显著提升数据库操作的开发效率。

跨平台兼容:前端采用uni-app框架,一套代码可以编译输出到微信小程序、H5页面、iOS App、Android App等多个平台,最大化代码复用价值。

云服务集成:充分利用阿里云提供的云服务能力,包括OSS对象存储服务、通义万相AI服务等,快速构建可扩展的云端应用。


二、系统架构设计

2.1 整体架构概述

系统采用经典的MVC三层架构设计,整体架构图如下所示:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        微信小程序客户端                           │
│                    (uni-app + Vue 2)                             │
└─────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼ HTTPS
┌─────────────────────────────────────────────────────────────────┐
│                        Nginx反向代理                             │
│                    (SSL证书 + 负载均衡)                          │
└─────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Spring Boot 后端服务                        │
│    ┌──────────────┬──────────────┬──────────────┐              │
│    │ Controller层  │  Service层   │   Mapper层    │              │
│    └──────────────┴──────────────┴──────────────┘              │
│    ┌──────────────┬──────────────┬──────────────┐              │
│    │  JWT拦截器    │  异常处理器   │   配置类      │              │
│    └──────────────┴──────────────┴──────────────┘              │
└─────────────────────────────────────────────────────────────────┘
         │                    │                    │
         ▼                    ▼                    ▼
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   MySQL     │      │   Redis     │      │  阿里云OSS   │
│  数据库服务   │      │   缓存服务   │      │  文件存储    │
└─────────────┘      └─────────────┘      └─────────────┘
                                                  │
                                                  ▼
                                         ┌─────────────┐
                                         │ 通义万相API  │
                                         │ AI图像处理   │
                                         └─────────────┘

2.2 后端技术架构

后端服务基于Spring Boot 2.7.9版本构建,采用Maven进行项目依赖管理。以下是pom.xml中的核心依赖配置:

Spring Boot核心框架是整个后端服务的基础。项目采用Spring Boot 2.7.9版本,这是一个长期支持版本(LTS),具有以下技术特性:

复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.9</version>
    <relativePath/>
</parent>

Web开发支持 通过spring-boot-starter-web依赖引入,该依赖包含了Spring MVC、Tomcat嵌入式服务器等核心组件,是构建RESTful API服务的基础。

数据库访问层采用MyBatis-Plus 3.5.1版本,这是一个强大的MyBatis增强工具包。MyBatis-Plus在MyBatis的基础上提供了以下增强功能:

复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

首先,MyBatis-Plus内置了通用的Mapper接口,开发者无需编写基本的CRUD SQL语句,通过继承BaseMapper接口即可获得增删改查的完整能力。其次,MyBatis-Plus提供了强大的分页插件,只需简单配置即可实现高性能的分页查询。此外,MyBatis-Plus还支持条件构造器、主动查询等功能,可以优雅地构建复杂的SQL查询语句。

身份认证采用JWT(JSON Web Token)技术,使用com.auth0:java-jwt:3.10.3依赖实现:

复制代码
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

JWT是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT由三部分组成:Header(头部)、Payload(负载)和Signature(签名)。相比传统的Session认证方式,JWT具有无状态、可扩展、支持跨域等优点,非常适合分布式系统和移动端应用。

云服务集成方面,项目集成了阿里云OSS文件存储服务:

复制代码
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.16.1</version>
</dependency>

阿里云OSS(Object Storage Service)是阿里云提供的海量、安全、低成本的云存储服务。通过OSS,开发者可以存储和访问任意类型的文件,非常适合用于存储用户上传的图片文件。

AI服务对接方面,项目通过HTTP请求调用阿里云通义万相API:

复制代码
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

通义万相是阿里云推出的大模型服务,提供了图像生成、图像编辑等多种AI能力。项目通过HTTP请求与通义万相API进行交互,实现AI图像处理功能。

2.3 前端技术架构

前端采用uni-app框架开发,这是一个基于Vue.js的跨平台应用开发框架。uni-app的核心优势在于"一套代码,多端发布",开发者只需编写一套Vue代码,即可发布到iOS、Android、H5、以及各种小程序平台。

项目采用Vue 2作为前端框架,主要原因如下:

第一,Vue 2拥有成熟的生态系统,社区资源丰富,技术文档完善,开发过程中遇到的问题可以很容易地找到解决方案。

第二,uni-app对Vue 2的支持最为完善,组件库和插件生态最为丰富,可以满足各种复杂的业务需求。

第三,相比Vue 3,Vue 2在微信小程序环境下的兼容性和性能表现更为稳定。

前端项目的核心依赖包括:

uni-ui组件库:这是DCloud官方提供的 uni-app 组件库,包含了表单组件(uni-forms、uni-easyinput)、图标组件(uni-icons)、弹窗组件(uni-popup)、过渡动画组件(uni-transition)等常用组件。这些组件针对移动端场景进行了优化,具有良好的用户体验和性能表现。

uni-file-picker组件:用于实现图片选择功能,支持从相册选择或拍照获取图片,并支持图片压缩处理。

axios封装:项目对uni.request进行了Promise封装,提供了统一的请求拦截、响应处理、错误处理等功能。

2.4 数据库设计

系统采用MySQL 5.7作为关系型数据库,根据业务需求设计了以下核心数据表:

**用户表(user)**是系统的核心实体表之一,主要字段设计如下:

字段名 数据类型 说明
id INT 主键,自增
email VARCHAR(255) 用户邮箱,唯一标识
code VARCHAR(255) 邮箱验证码
send_time DATETIME 验证码发送时间
avatar VARCHAR(255) 用户头像URL
balance VARCHAR(255) 用户剩余次数
use_time DATETIME 上一次使用时间
login_time DATETIME 上次成功登录时间
password VARCHAR(255) 密码(备用字段)

用户表的设计考虑了以下因素:邮箱作为用户的唯一标识,相比手机号更加轻量化,适合不需要强制实名认证的场景;balance字段采用VARCHAR类型存储用户剩余次数,便于灵活调整;use_time和login_time字段用于记录用户行为,便于后续的用户画像和运营分析。

**文档表(document)**用于存储用户上传的文件信息:

字段名 数据类型 说明
id INT 主键,自增
name VARCHAR(255) 文件名
type VARCHAR(255) 文件类型/扩展名
size BIGINT 文件大小,单位KB
preview VARCHAR(255) 预览链接
url VARCHAR(255) 文件存储URL
remove BOOLEAN 是否删除(软删除标记)
enable BOOLEAN 是否启用
md5 VARCHAR(255) 文件MD5值(用于去重)

文档表的设计采用了软删除机制,删除操作只是将remove字段标记为true,而不是真正从数据库中删除记录。这种设计可以保留数据便于审计和恢复,同时也不会影响正常的业务查询。

MD5字段的设计实现了文件去重功能。当用户上传文件时,系统会计算文件的MD5值并存储到数据库中。如果数据库中已存在相同MD5的记录,说明该文件内容已被存储过,系统可以直接返回已有文件的URL,避免重复上传和存储。


三、后端核心功能实现

3.1 JWT无状态认证机制

用户身份认证是系统安全的基础模块。项目采用JWT实现无状态认证,用户登录成功后服务器会生成一个Token返回给客户端,后续客户端的请求都需要携带这个Token进行身份验证。

Token生成是认证流程的第一步。在用户登录验证成功后,系统会调用Token工具类生成JWT Token:

复制代码
public class Token {
    
    /**
     * 生成用户Token
     * @param user 用户实体
     * @return JWT Token字符串
     */
    public static String createToken(User user) {
        // 创建Token过期时间,设置为8小时后过期
        Date expireDate = DateUtil.offsetHour(new Date(), 8);
        
        // 使用HMAC256算法,以用户密码作为密钥进行签名
        // 这样即使Token被截获,没有密码也无法伪造有效Token
        return JWT.create()
                .withAudience(String.valueOf(user.getId()))  // 设置Token载荷(用户ID)
                .withExpiresAt(expireDate)                   // 设置过期时间
                .sign(Algorithm.HMAC256(user.getPassword())); // 使用密码签名
    }
}

上述代码的关键设计点在于使用用户密码作为JWT签名的密钥。这种设计的好处是:即使攻击者截获了Token,由于不知道用户密码,也无法伪造有效的Token。同时,当用户修改密码后,原有的Token将自动失效,进一步提高了安全性。

Token验证是通过Spring MVC拦截器实现的。系统在WebMvcConfig中注册了JWTInterceptor拦截器,对需要认证的请求进行Token验证:

复制代码
@Component
public class JWTInterceptor implements HandlerInterceptor {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        
        // 从请求Header中获取Token
        String token = request.getHeader("token");
        
        if (StringUtils.isEmpty(token)) {
            response.setStatus(401);
            return false;
        }
        
        try {
            // 验证Token有效性和完整性
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("")).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            
            // 获取Token中存储的用户ID
            String userId = decodedJWT.getAudience().get(0);
            
            // 查询用户是否存在
            User user = userMapper.selectById(userId);
            if (user == null) {
                response.setStatus(401);
                return false;
            }
            
            // 使用用户密码重新验证Token签名
            // 这是防止Token被篡改的关键步骤
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
            verifier.verify(token);
            
            // 将用户信息存入请求属性,供后续处理使用
            request.setAttribute("user", user);
            
            return true;
            
        } catch (JWTVerificationException e) {
            response.setStatus(401);
            return false;
        }
    }
}

拦截器的验证逻辑包含三个层次:首先验证Token的基本格式和有效性;然后验证用户是否存在;最后使用用户密码重新验证Token签名,确保Token没有被篡改。只有通过全部验证,请求才会被放行。

统一响应封装是RESTful API设计的重要规范。系统定义了Res工具类来统一API的响应格式:

复制代码
public class Res {
    
    // 成功响应,携带数据
    public static Map<String, Object> ok(Object object) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", "200");
        result.put("message", "操作成功");
        result.put("object", object);
        return result;
    }
    
    // 成功响应,无数据
    public static Map<String, Object> ok() {
        return ok(null);
    }
    
    // 失败响应
    public static Map<String, Object> error(String message) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", "500");
        result.put("message", message);
        return result;
    }
    
    // 自定义状态码响应
    public static Map<String, Object> result(String code, String message, Object object) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", code);
        result.put("message", message);
        result.put("object", object);
        return result;
    }
}

这种统一的响应格式使得前端可以方便地进行统一处理,同时也便于进行接口监控和日志记录。

3.2 邮箱验证码登录流程

邮箱验证码登录是一种轻量级且安全的认证方式,特别适合小程序场景。用户无需记忆复杂的密码,只需拥有一个邮箱即可完成登录。

验证码发送接口的实现如下:

复制代码
@PostMapping("email_code")
public Res sendEmailCode(@RequestParam("email") String email) {
    // 校验邮箱格式
    if (!email.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")) {
        return Res.error("邮箱格式不正确");
    }
    
    // 生成4位数字验证码
    String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
    
    // 发送邮件(使用Hutool工具类)
    try {
        MailUtil.send(email, "您的验证码", "您的验证码是:" + code + ",5分钟内有效。");
    } catch (Exception e) {
        return Res.error("邮件发送失败,请检查邮箱地址");
    }
    
    // 将验证码存入数据库,设置5分钟有效期
    User user = userMapper.selectOne(new QueryWrapper<User>().eq("email", email));
    if (user == null) {
        // 新用户:创建账号
        user = new User();
        user.setEmail(email);
        user.setCode(code);
        user.setSendTime(new Date());
        user.setAvatar(Constants.DEFAULT_AVATAR);
        user.setBalance(Constants.DEFAULT_BALANCE);
        user.setPassword("123"); // 默认密码
        userMapper.insert(user);
    } else {
        // 老用户:更新验证码
        user.setCode(code);
        user.setSendTime(new Date());
        userMapper.updateById(user);
    }
    
    return Res.ok("验证码已发送");
}

验证码的有效期控制在5分钟内,通过send_time字段记录发送时间,后续验证时检查时间差即可。验证码存储在数据库中而非Session中,支持分布式部署场景。

登录验证接口的实现如下:

复制代码
@PostMapping("login")
public Res login(@RequestParam("email") String email, 
                 @RequestParam("code") String code) {
    
    // 查询用户
    User user = userMapper.selectOne(new QueryWrapper<User>().eq("email", email));
    if (user == null) {
        return Res.error("用户不存在");
    }
    
    // 检查验证码是否正确
    if (!code.equals(user.getCode())) {
        return Res.error("验证码错误");
    }
    
    // 检查验证码是否过期(5分钟)
    long diff = System.currentTimeMillis() - user.getSendTime().getTime();
    if (diff > 5 * 60 * 1000) {
        return Res.error("验证码已过期");
    }
    
    // 生成Token
    String token = Token.createToken(user);
    
    // 更新登录时间
    user.setLoginTime(new Date());
    userMapper.updateById(user);
    
    // 返回用户信息和Token
    user.setToken(token);
    user.setCode(null); // 不返回验证码
    return Res.ok(user);
}

登录成功后,系统会清除用户表中的验证码字段,防止验证码被重复使用。同时记录login_time便于追踪用户登录行为。

3.3 AI图像处理集成

AI图像处理是项目的核心功能模块。系统对接了阿里云通义万相API,实现了图片的智能编辑和创作功能。

异步任务处理机制是AI图像处理的核心设计。AI图像生成是一个耗时操作,无法在同步请求中完成,因此采用了异步轮询的方案:

复制代码
public class DealImage {
    
    // 通义万相API配置
    private static final String API_KEY = Constants.DASH_SCOPE_KEY;
    private static final String BASE_URL = "https://dashscope.aliy榜.com/api/v1/services/aigc/text2image/image-synthesis";
    
    /**
     * 执行AI图像处理
     * @param imageUrl 原图URL
     * @param prompt 用户描述
     * @param negativePrompt 负向提示词
     * @return 处理后的图片URL
     */
    public static String doWork(String imageUrl, String prompt, String negativePrompt) {
        try {
            // 步骤1:提交图像生成任务
            String taskId = submitTask(imageUrl, prompt, negativePrompt);
            
            // 步骤2:轮询任务状态(最多等待90秒)
            String resultUrl = pollTaskResult(taskId);
            
            return resultUrl;
            
        } catch (Exception e) {
            throw new ServiceException("AI处理失败:" + e.getMessage());
        }
    }
    
    /**
     * 提交图像生成任务
     */
    private static String submitTask(String imageUrl, String prompt, String negativePrompt) 
            throws Exception {
        
        // 构建请求体
        Map<String, Object> requestBody = new HashMap<>();
        Map<String, Object> model = new HashMap<>();
        model.put("name", "wan2.5-i2i-preview"); // 使用图像编辑模型
        requestBody.put("model", model);
        
        Map<String, Object> input = new HashMap<>();
        input.put("prompt", prompt);
        input.put("negative_prompt", negativePrompt);
        input.put("image_url", imageUrl);
        requestBody.put("input", input);
        
        // 发送HTTP POST请求
        HttpURLConnection conn = (HttpURLConnection) 
            new URL(BASE_URL).openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setDoOutput(true);
        
        // 写入请求体
        OutputStream os = conn.getOutputStream();
        os.write(new ObjectMapper().writeValueAsBytes(requestBody));
        os.flush();
        os.close();
        
        // 解析响应获取task_id
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(conn.getInputStream()));
        String response = reader.readLine();
        reader.close();
        
        // 解析JSON获取任务ID
        Map<String, Object> respMap = new ObjectMapper().readValue(response, Map.class);
        return (String) ((Map<String, Object>) respMap.get("output")).get("task_id");
    }
    
    /**
     * 轮询任务结果
     */
    private static String pollTaskResult(String taskId) throws Exception {
        String statusUrl = "https://dashscope.ali榜.com/api/v1/tasks/" + taskId;
        
        // 最多轮询30次,每次间隔3秒
        for (int i = 0; i < 30; i++) {
            Thread.sleep(3000);
            
            // 查询任务状态
            HttpURLConnection conn = (HttpURLConnection) 
                new URL(statusUrl).openConnection();
            conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
            
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream()));
            String response = reader.readLine();
            reader.close();
            
            // 解析响应
            Map<String, Object> respMap = new ObjectMapper().readValue(response, Map.class);
            Map<String, Object> output = (Map<String, Object>) respMap.get("output");
            String status = (String) output.get("task_status");
            
            if ("SUCCEEDED".equals(status)) {
                // 任务成功,返回结果图片URL
                return (String) ((Map<String, Object>) output.get("results"))
                    .get("image_url");
            } else if ("FAILED".equals(status)) {
                throw new ServiceException("AI处理任务失败");
            }
            // 否则继续轮询
        }
        
        throw new ServiceException("AI处理超时");
    }
}

异步任务处理机制的工作流程如下:第一步,用户提交处理请求后,系统调用通义万相API创建图像生成任务,并获取任务ID;第二步,系统进入轮询循环,每隔3秒查询一次任务状态;第三步,当任务状态变为"SUCCEEDED"时,获取处理结果图片URL并返回;第四步,如果超过90秒任务仍未完成,则返回超时错误。

图像处理接口是用户调用的入口:

复制代码
@PostMapping("modify_photo")
public Res modifyPhoto(HttpServletRequest request, 
                       @RequestParam("prompt") String prompt) {
    
    // 从请求属性中获取当前用户(由拦截器设置)
    User user = (User) request.getAttribute("user");
    
    // 检查用户余额
    int balance = Integer.parseInt(user.getBalance());
    if (balance <= 0) {
        return Res.error("余额不足,请联系客服充值");
    }
    
    // 获取用户上传的原图URL
    String imageUrl = user.getOriginImageUrl();
    if (StringUtils.isEmpty(imageUrl)) {
        return Res.error("请先上传图片");
    }
    
    // 调用AI处理
    String resultUrl = DealImage.doWork(imageUrl, prompt, "模糊、低质量、变形");
    
    // 扣减余额
    user.setBalance(String.valueOf(balance - 1));
    user.setUseTime(new Date());
    userMapper.updateById(user);
    
    return Res.ok(resultUrl);
}

3.4 文件上传与MD5去重

文件上传是AI图像处理的前置功能。系统实现了完整的文件上传流程,并基于MD5实现了文件去重功能。

文件上传接口的实现如下:

复制代码
@PostMapping("upload")
public Res upload(HttpServletRequest request, @RequestParam("file") MultipartFile file) {
    
    // 校验文件是否为空
    if (file.isEmpty()) {
        return Res.error("请选择文件");
    }
    
    // 校验文件大小(限制100MB)
    if (file.getSize() > 100 * 1024 * 1024) {
        return Res.error("文件大小不能超过100MB");
    }
    
    // 校验文件类型
    String originalFilename = file.getOriginalFilename();
    String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
    if (!Arrays.asList(".jpg", ".jpeg", ".png").contains(suffix)) {
        return Res.error("仅支持JPG、PNG格式图片");
    }
    
    try {
        // 计算文件MD5
        String md5 = DigestUtil.md5Hex(file.getBytes());
        
        // 查询是否已存在相同MD5的文件
        Document existDoc = documentMapper.selectOne(
            new QueryWrapper<Document>()
                .eq("md5", md5)
                .eq("remove", false)
        );
        
        if (existDoc != null) {
            // 文件已存在,直接返回已有URL(秒传逻辑)
            return Res.ok(existDoc.getUrl());
        }
        
        // 生成唯一文件名
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String newFilename = uuid + suffix;
        
        // 保存到本地服务器
        String localPath = "uploads/" + newFilename;
        File localFile = new File(Constants.UPLOAD_PATH, newFilename);
        file.transferTo(localFile);
        
        // 上传到阿里云OSS
        String ossUrl = OSSUploader.upload(localFile, "wx_clue_pic/" + newFilename);
        
        // 保存文件信息到数据库
        Document document = new Document();
        document.setName(originalFilename);
        document.setType(suffix);
        document.setSize(file.getSize() / 1024); // 转换为KB
        document.setUrl(ossUrl);
        document.setPreview(ossUrl);
        document.setMd5(md5);
        document.setEnable(true);
        document.setRemove(false);
        documentMapper.insert(document);
        
        // 删除本地临时文件
        localFile.delete();
        
        return Res.ok(ossUrl);
        
    } catch (Exception e) {
        return Res.error("上传失败:" + e.getMessage());
    }
}

文件上传流程的关键设计点包括:MD5去重机制可以有效节约存储空间,提升用户体验(秒传效果);文件首先保存到本地,然后异步上传到OSS,保证上传的可靠性;上传成功后删除本地临时文件,释放磁盘空间。


四、前端核心功能实现

4.1 uni-app项目结构

uni-app项目采用标准的前端项目结构,主要目录和文件说明如下:

pages.json 是uni-app项目的页面路由配置文件,定义了应用中所有页面的路径、样式和导航栏配置:

复制代码
{
    "pages": [
        {
            "path": "pages/begin_login/begin_login",
            "style": {
                "navigationBarTitleText": "登录",
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/left_home/left_home",
            "style": {
                "navigationBarTitleText": "AI图片修改",
                "navigationBarBackgroundColor": "#00BFFF"
            }
        },
        {
            "path": "pages/right_my/right_my",
            "style": {
                "navigationBarTitleText": "我的"
            }
        }
    ],
    "tabBar": {
        "color": "#7A7E83",
        "selectedColor": "#00BFFF",
        "borderStyle": "black",
        "backgroundColor": "#FFFFFF",
        "list": [
            {
                "pagePath": "pages/left_home/left_home",
                "text": "首页",
                "iconPath": "static/tabbar/home.png",
                "selectedIconPath": "static/tabbar/home-active.png"
            },
            {
                "pagePath": "pages/right_my/right_my",
                "text": "我的",
                "iconPath": "static/tabbar/my.png",
                "selectedIconPath": "static/tabbar/my-active.png"
            }
        ]
    }
}

manifest.json 是uni-app项目的应用配置文件,包含多端配置、权限声明、图标配置等信息。对于微信小程序,需要配置正确的AppID:

复制代码
{
    "mp-weixin": {
        "appid": "wx6d7daa5dfcc90260",
        "setting": {
            "urlCheck": false,
            "es6": true,
            "minified": true
        },
        "usingComponents": true
    }
}

4.2 HTTP请求封装

为了统一管理API请求,项目对uni.request进行了Promise封装,创建了axios.js工具类:

复制代码
const baseUrl = 'https://wdfgdzx.xyz';

const $http = (options = {}) => {
    return new Promise((resolve, reject) => {
        uni.request({
            url: baseUrl + options.url,
            method: options.method || 'GET',
            data: options.data || {},
            header: {
                "Content-Type": "application/json",
                "token": uni.getStorageSync('user')?.token || ''
            },
            success: (res) => {
                // 统一处理401未授权
                if (res.data.code === '401') {
                    uni.navigateTo({
                        url: '/pages/begin_login/begin_login'
                    });
                    return;
                }
                resolve(res.data);
            },
            fail: (err) => {
                uni.showToast({
                    title: '网络请求失败',
                    icon: 'none'
                });
                reject(err);
            }
        });
    });
};

// 导出便捷方法
const get = (url, data) => $http({ url, method: 'GET', data });
const post = (url, data) => $http({ url, method: 'POST', data });

export default { $http, get, post };

HTTP请求封装的核心功能包括:自动携带Token,从本地存储中读取用户Token并添加到请求Header中;统一响应处理,对于401未授权响应自动跳转到登录页面;错误处理,对于网络请求失败显示统一的错误提示。

4.3 登录页面实现

登录页面是用户进入小程序的第一个页面,采用邮箱验证码的登录方式。页面核心代码如下:

复制代码
<template>
    <view class="login-container">
        <view class="login-box">
            <view class="title">AI图片处理助手</view>
            <view class="subtitle">智能修改,一键完成</view>
            
            <uni-forms :modelValue="formData" :rules="rules" ref="formRef">
                <uni-forms-item name="email">
                    <uni-easyinput 
                        v-model="formData.email"
                        placeholder="请输入邮箱地址"
                        :disabled="codeSent"
                    />
                </uni-forms-item>
                
                <uni-forms-item name="code">
                    <view class="code-row">
                        <uni-easyinput 
                            v-model="formData.code"
                            placeholder="请输入验证码"
                            maxlength="6"
                        />
                        <button 
                            class="code-btn" 
                            :disabled="countdown > 0"
                            @click="sendCode"
                        >
                            {{ countdown > 0 ? countdown + 's' : '获取验证码' }}
                        </button>
                    </view>
                </uni-forms-item>
            </uni-forms>
            
            <button class="login-btn" @click="handleLogin">登录</button>
        </view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            formData: {
                email: '',
                code: ''
            },
            codeSent: false,
            countdown: 0,
            timer: null
        };
    },
    methods: {
        // 发送验证码
        async sendCode() {
            if (!this.formData.email) {
                uni.showToast({ title: '请输入邮箱', icon: 'none' });
                return;
            }
            
            const res = await this.$post('/big/email_code', {
                email: this.formData.email
            });
            
            if (res.code === '200') {
                this.codeSent = true;
                this.countdown = 60;
                this.timer = setInterval(() => {
                    this.countdown--;
                    if (this.countdown <= 0) {
                        clearInterval(this.timer);
                    }
                }, 1000);
                uni.showToast({ title: '验证码已发送', icon: 'success' });
            }
        },
        
        // 登录
        async handleLogin() {
            const res = await this.$post('/big/login', this.formData);
            
            if (res.code === '200') {
                // 保存用户信息
                uni.setStorageSync('user', res.object);
                uni.switchTab({ url: '/pages/left_home/left_home' });
            } else {
                uni.showToast({ title: res.message, icon: 'none' });
            }
        }
    },
    beforeDestroy() {
        // 组件销毁时清除定时器
        if (this.timer) {
            clearInterval(this.timer);
        }
    }
};
</script>

登录页面的设计考虑了以下用户体验要素:验证码按钮带有倒计时功能,防止用户频繁点击发送验证码;输入框在发送验证码后会被禁用,避免用户修改邮箱地址;登录按钮使用了渐变色设计,提升视觉吸引力;错误提示使用轻提示(Toast)方式,不打断用户操作流程。

4.4 首页图片处理实现

首页是小程序的核心功能页面,实现了图片上传、AI处理、结果展示等功能。页面核心代码如下:

复制代码
<template>
    <view class="home-container">
        <!-- 用户余额显示 -->
        <view class="balance-bar">
            <text>剩余次数:{{ user.balance }} 次</text>
            <text class="refresh" @click="refreshUser">刷新</text>
        </view>
        
        <!-- 图片选择区域 -->
        <view class="upload-section">
            <view 
                class="upload-box" 
                @click="chooseImage"
            >
                <image 
                    v-if="originImage" 
                    :src="originImage" 
                    mode="aspectFit"
                    class="preview-image"
                />
                <view v-else class="upload-hint">
                    <uni-icons type="camera" size="50" color="#999"></uni-icons>
                    <text>点击选择图片</text>
                </view>
            </view>
        </view>
        
        <!-- 修改描述输入 -->
        <view class="prompt-section">
            <textarea 
                v-model="prompt" 
                placeholder="请输入图片修改要求,如:'将背景替换为蓝天白云'"
                class="prompt-input"
            />
        </view>
        
        <!-- 处理按钮 -->
        <button 
            class="process-btn" 
            :disabled="processing"
            @click="processImage"
        >
            {{ processing ? '处理中...' : '立刻改图' }}
        </button>
        
        <!-- 结果展示 -->
        <view v-if="resultImage" class="result-section">
            <text class="section-title">处理结果</text>
            <image 
                :src="resultImage" 
                mode="aspectFit"
                class="result-image"
                @click="previewImage(resultImage)"
            />
            <view class="result-actions">
                <button @click="saveImage">保存到相册</button>
                <button @click="previewImage(resultImage)">预览大图</button>
            </view>
        </view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            user: {},
            originImage: '',
            prompt: '',
            processing: false,
            resultImage: ''
        };
    },
    onShow() {
        this.loadUserInfo();
    },
    methods: {
        // 加载用户信息
        loadUserInfo() {
            const user = uni.getStorageSync('user');
            if (user) {
                this.user = user;
                this.originImage = user.originImageUrl || '';
            }
        },
        
        // 选择图片
        chooseImage() {
            uni.chooseImage({
                count: 1,
                sizeType: ['compressed'],
                sourceType: ['album', 'camera'],
                success: async (res) => {
                    this.originImage = res.tempFilePaths[0];
                    // 上传到服务器
                    uni.showLoading({ title: '上传中...' });
                    const uploadRes = await this.uploadFile(res.tempFilePaths[0]);
                    uni.hideLoading();
                    
                    if (uploadRes.code === '200') {
                        // 保存原图URL到用户信息
                        this.user.originImageUrl = uploadRes.object;
                        uni.setStorageSync('user', this.user);
                    }
                }
            });
        },
        
        // 上传文件
        uploadFile(filePath) {
            return new Promise((resolve) => {
                uni.uploadFile({
                    url: this.$baseUrl + '/document/upload',
                    filePath: filePath,
                    name: 'file',
                    header: {
                        "token": uni.getStorageSync('user')?.token || ''
                    },
                    success: (res) => {
                        resolve(JSON.parse(res.data));
                    },
                    fail: () => {
                        uni.showToast({ title: '上传失败', icon: 'none' });
                        resolve({ code: '500' });
                    }
                });
            });
        },
        
        // 处理图片
        async processImage() {
            if (!this.originImage) {
                uni.showToast({ title: '请先选择图片', icon: 'none' });
                return;
            }
            if (!this.prompt) {
                uni.showToast({ title: '请输入修改要求', icon: 'none' });
                return;
            }
            
            this.processing = true;
            uni.showLoading({ title: 'AI处理中...' });
            
            try {
                const res = await this.$post('/user/modify_photo', {
                    prompt: this.prompt
                });
                
                uni.hideLoading();
                
                if (res.code === '200') {
                    this.resultImage = res.object;
                    // 更新用户余额
                    this.user.balance = parseInt(this.user.balance) - 1;
                    uni.setStorageSync('user', this.user);
                    uni.showToast({ title: '处理成功', icon: 'success' });
                } else {
                    uni.showToast({ title: res.message, icon: 'none' });
                }
            } catch (e) {
                uni.hideLoading();
                uni.showToast({ title: '处理失败', icon: 'none' });
            } finally {
                this.processing = false;
            }
        },
        
        // 保存图片到相册
        async saveImage() {
            try {
                // 下载图片
                const downloadRes = await uni.downloadFile({
                    url: this.resultImage
                });
                
                // 保存到相册
                await uni.saveImageToPhotosAlbum({
                    filePath: downloadRes.tempFilePath
                });
                
                uni.showToast({ title: '保存成功', icon: 'success' });
            } catch (e) {
                // 权限被拒绝
                if (e.errMsg.includes('auth deny')) {
                    uni.showModal({
                        title: '提示',
                        content: '需要您授权保存图片到相册',
                        success: (res) => {
                            if (res.confirm) {
                                uni.openSetting();
                            }
                        }
                    });
                }
            }
        },
        
        // 预览大图
        previewImage(url) {
            uni.previewImage({
                urls: [url],
                current: url
            });
        }
    }
};
</script>

首页功能实现的关键技术点包括:使用uni.chooseImage组件选择图片,支持从相册选择或拍照;使用uni.uploadFile上传文件,注意需要设置正确的content-type为multipart/form-data;图片下载和保存相册功能需要处理权限申请,对于权限被拒绝的情况引导用户去设置页面开启权限;使用uni.previewImage实现图片大图预览功能。


五、项目部署与运维

5.1 后端服务部署

后端服务部署在Linux服务器上,使用Docker容器化部署方案。主要配置如下:

Dockerfile定义了服务的容器镜像:

复制代码
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/black-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Nginx反向代理配置实现了HTTPS访问和请求转发:

复制代码
server {
    listen 443 ssl;
    server_name wdfgdzx.xyz;
    
    ssl_certificate /etc/nginx/ssl/wdfgdzx.xyz.pfx;
    ssl_certificate_key /etc/nginx/ssl/wdfgdzx.xyz.key;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

5.2 阿里云OSS配置

项目使用阿里云OSS存储用户上传的图片文件。OSS配置信息如下:

OSS客户端初始化

复制代码
@Configuration
public class OSSConfig {
    
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;
    
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;
    
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
    
    @Bean
    public OSS ossClient() {
        return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    }
}

文件上传工具类

复制代码
@Component
public class OSSUploader {
    
    @Autowired
    private OSS ossClient;
    
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
    
    /**
     * 上传文件到OSS
     * @param file 本地文件
     * @param objectName OSS对象名
     * @return OSS访问URL
     */
    public static String upload(File file, String objectName) {
        // 创建OSSClient实例
        OSS ossClient = new OSSClientBuilder().build(
            endpoint, accessKeyId, accessKeySecret);
        
        // 上传文件
        ossClient.putObject(bucketName, objectName, file);
        
        // 返回访问URL
        String url = "https://" + bucketName + ".oss-cn-beijing.aliyuncs.com/" + objectName;
        
        // 关闭OSSClient
        ossClient.shutdown();
        
        return url;
    }
}

六、小程序定制开发服务

6.1 为什么选择小程序定制开发

随着移动互联网的快速发展,微信小程序已成为企业触达用户的重要渠道。相比原生App,小程序具有以下显著优势:

即用即走的便捷性:用户无需下载安装,通过扫码或搜索即可使用,极大地降低了用户的使用门槛。对于AI图像处理这类轻量级工具类应用,小程序的便捷性尤为重要。

微信生态的流量红利:依托微信超过10亿的月活用户,小程序可以充分利用微信的社交关系链进行传播,获客成本相对较低。

开发成本的优势:相比iOS和Android双平台原生开发,小程序只需要开发一套代码即可覆盖多端场景,开发效率提升约50%。

完善的支付能力:小程序可以无缝集成微信支付,为商业化变现提供了便利条件。

6.2 定制开发服务内容

我们提供专业的小程序定制开发服务,服务内容包括但不限于:

需求分析与方案设计:深入了解客户的业务需求和目标用户,输出详细的需求文档和技术方案。我们会根据客户的行业特点和业务场景,提供针对性的解决方案。

UI/UX设计:提供专业的界面设计和交互体验优化。我们的设计团队会结合品牌调性和用户习惯,打造既美观又易用的产品界面。

功能开发与集成:完成小程序前后端的开发工作,并集成第三方服务(如支付、地图、AI等)。我们采用Spring Boot + uni-app的主流技术栈,确保项目的技术先进性和可维护性。

测试与上线:提供全面的功能测试、兼容性测试和性能优化服务,协助客户完成小程序的上线和审核工作。

运维支持:提供持续的运维支持和功能迭代服务,确保小程序的稳定运行和持续优化。

6.3 技术服务优势

选择我们的定制开发服务,您将获得以下技术优势:

成熟的架构设计:我们采用经过大量项目验证的成熟架构方案,包括Spring Boot微服务框架、MyBatis-Plus ORM框架、JWT身份认证、Redis缓存等,确保系统的稳定性和可扩展性。

丰富的组件积累:我们积累了大量可复用的业务组件和工具类,包括文件上传组件、分页查询组件、HTTP请求封装、统一的响应处理等,可以显著缩短开发周期。

完善的文档支持:每个项目都会提供完整的技术文档和操作手册,方便后续的维护和迭代工作。

持续的售后服务:项目交付后会提供一定期限的免费维保服务,对于客户反馈的问题会在第一时间响应和处理。


七、总结与展望

7.1 项目总结

本文详细介绍了一个基于Spring Boot + uni-app技术栈的AI图像处理小程序的完整开发过程。通过本文的学习,读者可以掌握以下核心技术的实际应用:

Spring Boot微服务开发:从项目结构设计、核心配置到业务逻辑实现,读者可以学习到如何使用Spring Boot快速构建企业级后端服务。

MyBatis-Plus ORM框架应用:通过实际案例展示了MyBatis-Plus的CRUD操作、分页查询、条件构造等核心功能的使用方法。

JWT无状态认证机制:详细讲解了JWT Token的生成、验证和拦截机制,帮助读者理解现代Web应用的认证体系。

阿里云OSS文件存储:展示了如何对接阿里云OSS实现文件上传、存储和访问,掌握云存储服务的集成方法。

AI图像处理接口对接:通过通义万相API的对接案例,读者可以学习到如何将第三方AI能力集成到自己的应用中。

uni-app跨平台开发:从项目结构、组件使用到业务逻辑实现,全面介绍了uni-app小程序的开发流程。

7.2 技术亮点回顾

本项目的技术亮点包括:

异步任务处理机制:AI图像生成是耗时操作,系统采用异步轮询方案处理,既保证了用户体验,又避免了请求超时问题。

MD5文件去重:通过计算和存储文件MD5值,实现了文件秒传功能,既节约了存储空间,又提升了用户体验。

JWT密码绑定签名:Token使用用户密码作为签名密钥,即使Token被截获也无法被伪造,大大提高了安全性。

统一响应封装:所有API采用统一的响应格式,便于前端处理和接口监控。

权限处理机制:图片保存相册功能完整处理了权限申请和拒绝场景,提供了良好的用户体验。

相关推荐
奔跑中的小象1 天前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南1 天前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang1 天前
LangGraph 状态机实战
人工智能
刹那芳华19921 天前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作1 天前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门1 天前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜1 天前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟1 天前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦1 天前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生1 天前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能