简介:针对绘画学习场景,这套基于微信小程序的毕业设计项目提供了完整的Java开源实现。项目采用微信开发者工具进行前端开发,搭配WXML/WXSS/JS构建小程序界面与交互,后端以Java为核心,配合MySQL数据库存储数据,实现了绘画课程浏览、学习管理、用户操作等主要功能。资源包共1409个文件,压缩后约18.25MB,包含源码、数据库脚本、开题报告、论文、PPT及使用说明;其中图片与矢量图用于界面展示,Java与JavaScript文件构成前后端逻辑,JSON与XML存放配置数据,SQL为数据库初始化脚本,WXML与WXSS定义小程序页面结构。这套资料覆盖从需求分析到系统测试的完整开发流程,目录层级清晰,便于按模块查阅,从界面设计到后端接口、从数据库表结构到部署配置均有对应文件可供参考。已有106人学习下载,适合计算机相关专业学生参考与二次开发。
1. 绘画学习平台:一个把“教画画”完整搬到微信里的 Java 毕业设计
做过小程序毕业设计的人应该都有体会:很多项目要么只做了前端页面、后端是假接口,要么后端堆了一堆代码却跑不起来。这个基于微信小程序的绘画学习平台不一样,它是一套从前端微信端到后端 Java、再到 MYSQL 数据库的完整闭环,甚至把开题报告、论文、PPT、使用说明都给你备齐了。它的核心价值在于:你拿到的不是一堆零散文件,而是一个可以直接导入微信开发者工具、启动后端、连上数据库就能演示的成品。
这个平台解决的场景很具体:绘画初学者想找课程学习、想上传自己的作品、想记录学习进度,但不想装 App,打开微信就能用。适合三类人:做毕业设计需要完整项目参考的学生、想快速理解小程序 + Java 全栈开发流程的开发者、以及准备二次开发成在线艺术教育产品的团队。整套东西的关键词就三个:微信小程序、Java、MYSQL,技术栈不算新,但胜在完整、能跑、好改。
2. 项目架构与技术选型:从微信端到 Java 后端的完整链路
2.1 技术栈与文件结构:拿到资源后先认清这堆文件是什么
解压这份资源后,你会看到两类东西:一类是main.css.bak、update-password.vue.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak这样的备份文件,另一类是1-install.bat、2-run.bat、3-build.bat这三个批处理脚本,还有.classpath、org.eclipse.wst.common.component这类 Eclipse 工程配置文件。
先说.bak文件。很多人第一次看到会疑惑:这是不是多余的?不是,这些是开发者在改版过程中保留下来的备份。IndexAsideStatic.vue.bak和IndexHeader.vue.bak说明管理后台的前端用了 Vue + Element UI 这类组件库,侧边栏菜单和顶部导航都是独立组件;update-password.vue.bak对应后台管理员的改密页面;main.css.bak是全局样式备份。二次开发时这些文件能帮你快速找回旧版逻辑,但跑项目用不到它们。
三个.bat脚本是整个项目的“后悔药”:1-install.bat负责安装依赖,2-run.bat负责启动后端服务,3-build.bat负责打包构建。后端工程是标准的 Maven 结构,.classpath文件表明它曾经用 Eclipse 作为 IDE,org.eclipse.wst.common.component说明它是 Eclipse 的 Dynamic Web Project 或基于 Spring Boot 的 Web 工程。常见的做法是:后端用 Spring Boot + MyBatis 操作 MYSQL,控制层返回 JSON 给小程序端。
2.2 前后端分离设计:小程序端、管理后台、后端服务三条线怎么协同
这个平台从架构上分成三条线:微信小程序端(面向普通用户)、管理后台(面向管理员,就是 Vue 那套)、Java 后端(提供 API 接口)。小程序端用 WXML、WXS、JS 编写,通过微信开发者工具运行;管理后台用 Vue 组件拼装页面,通过 HTTP 调用后端接口;后端用 Java 处理业务逻辑,读写 MYSQL 数据库。
我一般会这样理解三者关系:小程序端拿到用户的点击操作,封装成请求参数,通过微信的wx.request发给后端;后端 Controller 层接收请求,调用 Service 层做业务判断(比如用户有没有权限看某个付费课程),Service 层再通过 Mapper 层操作数据库;返回结果统一打包成 JSON,小程序端拿到 JSON 之后用setData更新页面视图。整个过程无页面跳转,数据驱动视图,这也是小程序开发的核心思维。
2.3 运行环境要求:跑起来之前需要什么
需要准备的东西不复杂但版本要对上。JDK 1.8 是主流选择,和 Spring Boot 2.x 搭配最稳;MYSQL 5.7 或 8.0 都行,但要统一字符集为 utf8mb4,否则存中文绘画作品描述会乱码;微信开发者工具需要最新稳定版,导入小程序项目时 AppID 可以先用测试号。前端依赖管理用 npm,后端用 Maven,这两个装好之后,1-install.bat才能顺利执行。
常见做法是先把后端跑起来再跑小程序端。两个端之间用局域网 IP 或 localhost 通信,小程序端在app.js或配置文件里会有一个baseUrl变量,改成你本机的 IP 加端口就行。这一步踩坑率极高,后面避坑章节会专门讲。
3. 核心功能代码拆解:课程浏览、学习打卡与作品上传的实现
3.1 小程序端页面结构:从首页到课程详情的数据流
小程序端是用户接触最多的部分,页面结构围绕绘画学习的核心场景设计。首页展示课程分类和推荐课程,课程列表页支持按分类筛选,课程详情页展示课程图文介绍和学习进度,我的页面里包含个人信息、学习记录和作品管理。每个页面对应一个目录,目录下是.wxml文件(页面结构)、.wxss文件(页面样式)、.js文件(页面逻辑)、.json文件(页面配置)。
课程列表页的 JS 核心逻辑我拆给你看:
// pages/course/list.js const app = getApp(); const api = require('../../utils/api.js'); // 统一封装 wx.request Page({ data: { courseList: [], categoryId: 0, loading: false, page: 1, hasMore: true }, onLoad(options) { if (options.categoryId) { this.setData({ categoryId: options.categoryId }); } this.fetchCourses(); }, fetchCourses() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); const params = { categoryId: this.data.categoryId, page: this.data.page, pageSize: 10 }; api.get('/course/list', params) .then(res => { if (res.data && res.data.length > 0) { this.setData({ courseList: this.data.courseList.concat(res.data), page: this.data.page + 1 }); } else { this.setData({ hasMore: false }); } }) .finally(() => this.setData({ loading: false })); }, onReachBottom() { this.fetchCourses(); // 触底加载下一页 } });这段代码有四个关键参数要注意:categoryId是分类筛选条件,page和pageSize实现分页加载,hasMore是是否还有更多数据的开关。分页逻辑用的是经典的上拉加载更多模式:每次onReachBottom触发时请求下一页,数据拼接进courseList。api.get是对wx.request的 Promise 封装,返回res.data里是后端下发的 JSON 数组。这里的翻车点在于:如果后端返回的分页字段名不是page和pageSize,或者返回结构不是{ data: [...] },前端就要同步改,所以要前后端一起看。
3.2 后端 Controller 层:课程列表接口怎么把数据吐给小程序
后端对应处理课程列表的 Controller 长这样:
@RestController @RequestMapping("/course") public class CourseController { @Autowired private CourseService courseService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "0") Integer categoryId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize) { PageInfo<Course> coursePage = courseService.queryByCategory(categoryId, page, pageSize); return Result.success(coursePage); } @GetMapping("/detail") public Result detail(@RequestParam Integer courseId) { Course course = courseService.getById(courseId); return Result.success(course); } }@RequestParam(defaultValue = "1")里的defaultValue非常重要:小程序端如果没传page参数,后端不会报错而是默认第一页。Result是统一返回体,包含code、message、data三个字段,小程序端通过res.data.data拿到真正的列表数据。这种统一返回结构的习惯值得借鉴,它让所有接口的返回格式保持一致,前端不用每个接口单独处理异常。categoryId = 0表示查询全部分类,这是我见过比较普遍的设计方式,省去了前端传空值的麻烦。
Service 层的实现遵循 MyBatis 的分页插件或手工分页:
@Service public class CourseServiceImpl implements CourseService { @Autowired private CourseMapper courseMapper; @Override public PageInfo<Course> queryByCategory(Integer categoryId, int page, int pageSize) { PageHelper.startPage(page, pageSize); List<Course> list = categoryId == 0 ? courseMapper.selectAll() : courseMapper.selectByCategoryId(categoryId); return new PageInfo<>(list); } }PageHelper.startPage(page, pageSize)是 MyBatis 分页插件的核心方法,它会在下一次查询时自动拼接 LIMIT 语句。这里有个细节:categoryId等于 0 时走selectAll(),否则走selectByCategoryId,避免了在 SQL 里写动态判断。但要注意,PageHelper的生效范围是紧接着的一条 Mapper 查询,如果中间插了其他数据库操作,分页就会失效,这是 PageHelper 的经典坑。
3.3 学习打卡与作品上传:小程序端最实用的两个交互功能
学习打卡是绘画学习平台区别于普通课程网站的功能点。用户每学完一节视频课程,可以点击“打卡”按钮记录学习状态,后端会写入一条学习记录,同时更新课程的学习进度。这个功能的核心代码如下:
// pages/course/detail.js submitCheckIn() { const courseId = this.data.courseId; const userId = wx.getStorageSync('userId'); if (!userId) { wx.showToast({ title: '请先登录', icon: 'none' }); return; } api.post('/study/checkIn', { userId, courseId }) .then(res => { if (res.code === 200) { wx.showToast({ title: '打卡成功', icon: 'success' }); this.setData({ progress: res.data.progress }); } }); }wx.getStorageSync('userId')是小程序端本地存储的读取方式,登录成功时把用户 ID 存到本地,后续请求直接从 storage 里取。这种做法简单高效,但有个安全风险:本地存储可以被篡改,生产环境应该用wx.login获取 code 后换 session。对于毕业设计或学习演示场景,本地存 userId 是合格的做法。打卡成功后返回的progress是课程总进度百分比,前端拿到后可以刷新进度条展示。
作品上传功能需要处理图片文件。小程序端用wx.chooseMedia选图,然后通过wx.uploadFile上传到后端:
uploadWork() { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => { const filePath = res.tempFiles[0].tempFilePath; wx.uploadFile({ url: app.globalData.baseUrl + '/work/upload', filePath: filePath, name: 'file', formData: { userId: wx.getStorageSync('userId'), courseId: this.data.courseId }, success: (uploadRes) => { const data = JSON.parse(uploadRes.data); if (data.code === 200) { wx.showToast({ title: '上传成功', icon: 'success' }); } } }); } }); }name: 'file'这个参数名必须和后端 Controller 的@RequestParam("file")对应,否则后端收不到文件;formData里的userId和courseId是业务参数,和后端的MultipartFile file一起接收。后端的处理方式是存到本地磁盘或云存储,然后把访问 URL 存进数据库。这个功能我一般会建议扩展:增加缩略图生成和图片压缩,不然用户上传一张 5MB 的照片会把服务器带宽打满。
4. 数据库设计与接口联调:MYSQL 侧的 7 张核心表
4.1 数据库表结构与字段设计:画好 ER 图再落库
整个系统围绕用户、课程、学习记录、作品四个核心实体设计了一张关系网。我把它凝练成 7 张表,命名规范清晰,每张表都带前缀t_避免和关键字冲突。用户表t_user存微信用户的 openid、昵称、头像;管理员表t_admin存后台登录账号;课程分类表t_category存国画、素描、水彩这类分类;课程表t_course存课程标题、封面图、视频链接、讲师、简介;学习记录表t_study_record做打卡记录;作品表t_work存用户上传的绘画作品;评论表t_comment做用户对课程的反馈。
课程表t_course的核心字段设计如下:
CREATE TABLE `t_course` ( `course_id` INT NOT NULL AUTO_INCREMENT COMMENT '课程ID', `category_id` INT DEFAULT NULL COMMENT '分类ID', `course_title` VARCHAR(100) NOT NULL COMMENT '课程标题', `course_cover` VARCHAR(255) DEFAULT NULL COMMENT '课程封面图URL', `video_url` VARCHAR(255) DEFAULT NULL COMMENT '视频播放地址', `teacher` VARCHAR(50) DEFAULT NULL COMMENT '讲师', `course_desc` TEXT COMMENT '课程简介', `total_chapters` INT DEFAULT 0 COMMENT '总章节数(打卡约等于章节数)', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`course_id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';category_id加索引idx_category是因为课程列表页的筛选查询是最高频的数据库操作,没有索引会导致全表扫描。total_chapters的作用很巧妙:打卡进度的计算就是拿学习记录表里的打卡次数除以这个字段,不用额外维护进度字段。把这种计算字段冗余到主表里,是典型的空间换时间思路。
学习记录表和作品表是功能的核心:
CREATE TABLE `t_study_record` ( `record_id` INT NOT NULL AUTO_INCREMENT COMMENT '记录ID', `user_id` INT NOT NULL COMMENT '用户ID', `course_id` INT NOT NULL COMMENT '课程ID', `chapter_index` INT DEFAULT 1 COMMENT '打卡章节序号', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '打卡时间', PRIMARY KEY (`record_id`), UNIQUE KEY `uk_user_course_chapter` (`user_id`, `course_id`, `chapter_index`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习记录表'; CREATE TABLE `t_work` ( `work_id` INT NOT NULL AUTO_INCREMENT COMMENT '作品ID', `user_id` INT NOT NULL COMMENT '用户ID', `course_id` INT DEFAULT NULL COMMENT '关联课程ID', `image_url` VARCHAR(255) NOT NULL COMMENT '作品图片URL', `work_desc` VARCHAR(500) DEFAULT NULL COMMENT '作品描述', `like_count` INT DEFAULT 0 COMMENT '点赞数', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', PRIMARY KEY (`work_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作品表';uk_user_course_chapter唯一索引是防重打卡的关键:同一用户对同一课程同一章节只能打卡一次,重复提交会被数据库拦截,前端再做一次提示,这叫双保险。作品表的like_count是冗余计数,每次点赞直接UPDATE t_work SET like_count = like_count + 1,避免每次点赞都查一遍总数。这种设计在中小型项目里够用,但如果点赞量上来,就要拆成点赞流水表了。
4.2 管理后台的 Mapper 层与 SQL:列表查询和统计的常见写法
管理后台要展示课程列表、用户列表、学习统计,这些功能落在 Mapper 层的 XML 文件里。课程管理的分页查询 XML 是这样写的:
<select id="selectCoursePage" resultType="com.example.entity.Course"> SELECT course_id, category_id, course_title, course_cover, teacher, total_chapters, create_time FROM t_course <where> <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND course_title LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY create_time DESC </select><where>标签和<if>标签是 MyBatis 动态 SQL 的黄金组合:<where>会自动去掉第一个AND,避免 SQL 语法错误;categoryId != 0这个判断意味着前端传 0 的时候不参与筛选。LIKE CONCAT('%', #{keyword}, '%')是模糊搜索的标准写法,用CONCAT拼接通配符可以防止 SQL 注入,比直接拼接字符串安全得多。这里我一般会补一句:永远不要写LIKE '%${keyword}%',${}是直接字符串替换,会被注入。
4.3 前后端接口联调清单:小程序怎么拿到真数据
联调阶段的核心是弄清楚每个页面需要哪些接口。根据页面功能倒推,全平台大概需要 12 个核心接口:用户登录注册(/user/login、/user/register)、课程分类查询(/category/list)、课程列表与详情(/course/list、/course/detail)、学习打卡(/study/checkIn)、学习记录查询(/study/list)、作品上传(/work/upload)、作品列表与点赞(/work/list、/work/like)、管理员登录(/admin/login)、课程管理增删改(/course/add、/course/update、/course/delete)。接口返回统一用 Result 包装,状态码 200 成功,500 失败,401 未登录。
小程序端联调时最常遇到的状况是请求失败但后端日志没报错,多半是baseUrl配错了。小程序开发工具的本地请求默认走http://localhost:8080,但真机预览时 localhost 指向的是手机自己,必须改成电脑在局域网里的 IP。另一个注意点是:微信开发者工具默认不校验合法域名,正式发布时要把baseUrl换成 HTTPS 域名,并在小程序后台配置 request 合法域名,否则线上版本会请求失败。
5. 常见坑与排查记录:开发过程中的血泪经验
5.1 数据库中文乱码:存进去是好的,查出来变成问号
现象:小程序端提交课程名称或作品描述后,管理后台看到的中文变成一串???,但英文正常。原因:MYSQL 连接串里没指定characterEncoding=utf8,或者数据库表默认字符集是 latin1。解决:改 JDBC 连接串,在application.yml里加上jdbc:mysql://localhost:3306/painting?useUnicode=true&characterEncoding=utf8&useSSL=false,同时把建表语句里的DEFAULT CHARSET统一改成utf8mb4。从这以后,我每次新建数据库都先执行一遍ALTER DATABASE xxx CHARACTER SET utf8mb4,再开始建表。
5.2 PageHelper 分页失效:返回了全量数据
现象:接口返回的数据不按pageSize截断,列表一次性返回所有记录。原因:PageHelper.startPage()之后没有立即执行 Mapper 查询,中间穿插了其他数据库操作,导致分页线程变量被吞掉。解决:把startPage()放在 Mapper 查询的前一行,中间不要有任何数据库操作。另外很关键的一点是:不要在循环里调用分页查询,否则每个循环都会重新设置分页参数,结果互相覆盖。排查方法很简单:打开 MyBatis 的 SQL 日志,看一眼 LIMIT 子句有没有拼进去。
5.3 小程序真机请求不到后端:localhost 不认账
现象:开发者工具里一切正常,一用手机预览就请求失败。原因:小程序跑在手机上的时候,localhost指向的是手机自身,不是电脑。解决:把app.globalData.baseUrl里的localhost改成电脑的局域网 IP,比如http://192.168.1.8:8080,并确保电脑防火墙放行了 8080 端口。实战里,手机和电脑需要在同一个 Wi-Fi 下,且后端启动时监听0.0.0.0而不是默认的127.0.0.1。从那以后我每次联调真机,都会强制走一遍:改 IP、测防火墙、看后端启动日志。
5.4 图片上传后访问 404:路径没映射静态资源
现象:作品上传成功,但小程序端拿image_url访问图片时显示 404。原因:后端把文件存在了本地磁盘的某个目录,但没有配置静态资源映射,Spring Boot 默认只能访问classpath:/static/下的资源。解决:在 Spring Boot 配置类里加上 WebMvcConfigurer 的映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = System.getProperty("user.dir") + "/upload/"; registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); } }/upload/**是访问前缀,file:开头的路径是磁盘绝对路径,两个必须对齐,否则白配。这里最玄学的问题是 Windows 和 Linux 的路径分隔符不一样,建议用System.getProperty("user.dir")动态获取项目根目录,不要硬编码D:/xxx。
5.5 微信开发者工具导入项目报错:找不到 app.json
现象:用开发者工具导入某个目录时提示“app.json 未找到”或“项目文件不完整”。原因:导入的目录选错了,选了后端工程目录,或者选中了包含多个前端项目的父目录。解决:小程序源码一定是在含app.js、app.json、app.wxss的那个目录下导入。解压资源后,先找到这三个文件所在的文件夹,再在微信开发者工具里选择“导入项目”,目录指向那里。如果还是找不到,检查一下目录里是不是有隐藏的多余嵌套层。
6. 二次开发方向:给绘画学习平台加一个 AI 点评模块
6.1 从功能层面扩展:作品点评从人工到智能化
原始平台的作品模块只提供了上传、展示、点赞三个功能,用户上传绘画作品后得不到任何反馈,这是留存率低的最大瓶颈。我设想的改造是:在作品上传成功后,自动调用一个图像分析接口,返回作品的基础构图分析、色彩使用统计和明暗对比度评估,然后生成一段文字点评,和作品一起展示在作品详情页。这个功能不改变原有数据表结构,只需在t_work表里加两个字段:ai_comment TEXT存点评文字,score DECIMAL(3,1)存综合评分。
技术实现上,改造分为三个层次。第一层在后端:新增@PostMapping("/work/analyze")接口,接收workId,从数据库查到image_url,把图片地址传给一个预训练的绘画分析服务,拿到结果后写回t_work表;第二层在小程序端:在uploadWork成功回调里追加一次api.post('/work/analyze', { workId })调用,前端展示一个“AI 分析中”的 loading 状态;第三层在管理后台:课程表旁边加一个“AI 点评配置”入口,管理点评提示语的模板。
6.2 从代码层面改造:新增一个轻量级的调用服务
我一般会在后端service包下新增一个AiAnalyzeService,保持原有模块代码不动:
@Service public class AiAnalyzeService { @Value("${ai.service.url}") private String aiServiceUrl; @Value("${ai.service.token}") private String aiServiceToken; public WorkAnalysis analyzeImage(String imageUrl) { RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(aiServiceToken); Map<String, Object> body = new HashMap<>(); body.put("image_url", imageUrl); body.put("type", "painting_analysis"); HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers); ResponseEntity<Map> response = restTemplate.postForEntity( aiServiceUrl + "/v1/analyze", request, Map.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { return WorkAnalysis.fromMap(response.getBody()); } return WorkAnalysis.fallback("分析服务暂时不可用", 0.0); } }@Value注解把 AI 服务的地址和鉴权 token 放在application.yml里管理,换环境不用改代码。关键设计是最后的fallback兜底:AI 服务挂了的时候返回默认文案,不让用户的页面报错。这是生产环境该有的习惯——第三方服务永远可能挂,主流程不能被拖死。WorkAnalysis是一个 DTO,包含comment和score两个字段,内部用fromMap做类型转换,字段名和外部接口对齐。
小程序的uploadWork方法改造点很小:
success: (uploadRes) => { const data = JSON.parse(uploadRes.data); if (data.code === 200) { wx.showLoading({ title: 'AI 分析中...' }); const workId = data.data.workId; api.post('/work/analyze', { workId }).then((res) => { wx.hideLoading(); if (res.code === 200) { wx.showToast({ title: '点评生成成功', icon: 'success' }); } }); } }上传和 AI 点评串行执行,用户体验上会有 2 秒左右的等待,可以接受。如果想做得更细,可以把分析结果做成本地缓存,下次进入作品详情页直接展示,不用每次重复请求。
6.3 验证和收尾:改完怎么确定没把原来的功能弄坏
改造完成后,验证路径我习惯分三步走。第一步看接口:用 Postman 直接调/work/analyze,传一个已知的 workId,确认返回 JSON 里有comment和score字段;第二步看流程:在小程序真机里走一遍“上传作品 → AI 分析中 → 展示点评”的完整链路,确认上传功能没有被新增的串行请求影响;第三步看兜底:手动把ai.service.url配置改成错误地址,确认fallback文案正常返回,页面不白屏。
验证过程中最容易翻车的是 token 过期或无效,排查时先看后端日志里 RestTemplate 的响应状态码,401 就换 token,5xx 就查服务地址。从那以后,我每次新增一个依赖外部服务的模块,都会强制走一遍“接口可测、流程可走、失败可兜”这三步验证,不确定的时候宁可先 mock 掉外部服务把主流程跑通,再对接真实服务。希望这套改造思路也能帮你把这个绘画学习平台变成一个有自己特色的毕业设计。
本文还有配套的精品资源,点击获取