随着人工智能技术的快速发展,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采用统一的响应格式,便于前端处理和接口监控。

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

Logo

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

更多推荐