手工编织非遗平台这类项目,最近在毕设选题里确实越来越常见。它既不像纯商城系统那样“千篇一律”,又比单纯的内容管理系统更有技术含量——核心在于引入了推荐算法。本文围绕一个基于 Spring Boot + Vue 的“手工编织非遗平台”完整项目,从前端页面到后端接口,从数据库增删改查到推荐算法落地,把整套实现拆开讲清楚。如果你正在找前后端分离的毕设项目,或者想了解如何给业务系统加入推荐逻辑,这篇文章可以作为你的完整参考。项目附带完整源码与教学资料,文中会给出核心代码、设计思路、排错方式和工程建议,方便你照着配置运行,也能在答辩时把技术点讲明白。
1. 项目背景与核心概念
1.1 非遗手工编织平台解决什么问题
手工编织作为非物质文化遗产,具有很强的地方特色和手工属性。但在传统的线下场景里,它的传播渠道窄,受众群体小,很多传承人和手工艺人的作品无法触达真正感兴趣的用户。与此同时,手工编织品又不同于普通商品:用户对材料、工艺、风格、用途都有比较细分的偏好,单纯的“浏览商品列表”体验很差。
这个项目要解决的,就是两个核心问题:
- 信息展示问题:用手工编织非遗平台把非遗项目、手工艺人、编织作品、文化资讯集中管理和展示,替换掉线下的静态介绍。
- 个性化推荐问题:不同用户对编织风格(比如绳编、毛线编、竹编、藤编)偏好不同,平台需要根据用户行为(浏览记录、收藏、评论),给用户推荐更符合其兴趣的编织作品。
所以这不仅仅是一个增删改查的“管理后台”,而是一个带用户行为分析、带推荐逻辑的前后端分离应用,这也是它适合做毕设、适合写进简历项目的原因。
1.2 推荐算法选型:为什么用内容推荐而非协同过滤
一提到推荐算法,很多同学第一反应是协同过滤(Collaborative Filtering)。但在实际落地时,这个项目选择了基于内容的推荐(Content-based Recommendation),原因很简单:
- 非遗平台项目的数据量不大,用户数和作品数都在百级到千级,协同过滤的稀疏矩阵问题会很突出。
- 协同过滤需要大量用户行为数据来训练相似度矩阵,而毕设项目启动阶段用户行为很少,效果反而不稳定。
- 内容推荐不需要训练过程,它直接利用“物品特征”和“用户偏好标签”做相似度计算,可解释性强,代码量适中,答辩时容易讲清楚原理。
内容推荐的核心思路是:
- 给每个编织作品打上标签,比如“绳编”“杯垫”“中国结”“红绳手链”。
- 根据用户的浏览、收藏记录,累计用户对各标签的兴趣权重。
- 计算每个作品标签向量和用户偏好向量之间的相似度,选最高的前 N 个推荐给用户。
这个思路和现在很多非遗传习平台的落地方式一致,简单、有效、易于维护。
1.3 前后端分离架构解读
本项目采用前后端分离架构,区别于传统的单体 JSP/Thymeleaf 项目。
后端只负责提供 RESTful API,返回 JSON 数据,不关心页面渲染。前端通过 Axios 调用接口,拿到 JSON 后动态渲染页面。两者通过 HTTP 通信,开发时可以完全解耦。
前后端分离带来的好处是:
- 后端接口可以被多个端复用,比如 Web 端、移动端、管理后台。
- 前端开发和后端开发可以并行进行,只需要提前约定好接口文档。
- 部署灵活,前端可以打包为静态文件,后端打成 jar 包独立运行。
在毕设项目中,前端我们可以用 Vue 2/3 + Element UI 搭建,后端用 Spring Boot + MyBatis Plus,数据库使用 MySQL。
2. 技术选型与开发环境准备
2.1 技术栈清单
这个项目的技术栈选型会充分考虑“毕设实用性”和“答辩可解释性”,不希望引入过于复杂而难以说明的组件。
| 层次 | 技术选型 | 作用说明 |
|---|---|---|
| 前端框架 | Vue 2 + Element UI | 构建页面组件与后台管理界面 |
| 前端构建 | npm / Vue CLI | 本地开发调试与打包 |
| 后端框架 | Spring Boot | 提供 RESTful API 与业务逻辑 |
| ORM | MyBatis Plus | 简化单表增删改查,分页查询 |
| 权限认证 | JWT(jjwt) | 用户登录令牌与接口鉴权 |
| 数据库 | MySQL 8.0 | 存储用户、作品、标签、行为数据 |
| 推荐算法 | 基于内容 + 余弦相似度 | 首页个性化推荐 |
版本说明:Spring Boot 使用 2.5.x / 2.6.x / 2.7.x 均可,如果你使用 Spring Boot 3.x,注意需要 JDK 17 及以上,同时 MyBatis Plus 也要切换为 3.5.5+ 版本。本文示例以 Spring Boot 2.x 为主,实际项目请按照自己的 JDK 版本调整依赖。
2.2 后端环境准备
- JDK:1.8 或 11(Spring Boot 2.x 推荐 JDK 8/11)
- Maven:3.6以上
- IDE:IntelliJ IDEA
- MySQL:5.7 / 8.0
- Navicat 或 DBeaver:用于数据库管理
创建一个 Spring Boot 项目,核心依赖如下:
<!-- 文件路径:pom.xml --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>2.3 前端环境准备
前端使用 Vue CLI 快速初始化项目。如果你使用 Vue 2,运行以下命令:
# 全局安装 vue-cli npm install -g @vue/cli # 创建项目 vue create weaving-front # 进入项目 cd weaving-front # 安装 Element UI 和 Axios npm install element-ui axios如果使用 Vue 3,安装的是element-plus,但本文示例为了保持兼容性以 Vue 2 语法为主。两种版本核心逻辑相通,差异在个别标签和导入方式。
3. 数据库设计与增删改查
数据库设计是任何管理系统的基础。这个项目包含用户、作品、分类、标签、收藏、浏览记录六张核心表。
3.1 核心表结构设计
先创建数据库:
CREATE DATABASE IF NOT EXISTS weaving_db DEFAULT CHARACTER SET utf8mb4; USE weaving_db;用户表:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';编织作品表:
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '作品名称', `category_id` int(11) NOT NULL COMMENT '分类ID', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图', `description` text COMMENT '作品描述', `craftsman` varchar(50) DEFAULT NULL COMMENT '手工艺人', `material` varchar(100) DEFAULT NULL COMMENT '材料', `price` decimal(10,2) DEFAULT '0.00' COMMENT '参考价格', `tags` varchar(255) DEFAULT NULL COMMENT '标签,逗号分隔', `view_count` int(11) DEFAULT '0' COMMENT '浏览量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='手工编织作品表';收藏表:
CREATE TABLE `favorite` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `product_id` int(11) NOT NULL COMMENT '作品ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_product` (`user_id`, `product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';浏览记录表:
CREATE TABLE `browse_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `product_id` int(11) NOT NULL, `browse_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='浏览记录表';注意:作品表中的
tags字段使用逗号分隔字符串,在推荐计算时需要拆分为标签数组。这种方式牺牲了部分关系型设计规范,但换取业务实现的简便,适合毕设量级,也便于推荐算法快速取标签。
3.2 MyBatis Plus 增删改查
引入 MyBatis Plus 后,单表 CRUD 大多数情况不需要写 SQL。实体类继承BaseMapper<T>即可获得通用方法。
作品实体类:
// 文件路径:src/main/java/com/weaving/entity/Product.java @Data @TableName("product") public class Product { @TableId(type = IdType.AUTO) private Integer id; private String title; @TableField("category_id") private Integer categoryId; @TableField("cover_image") private String coverImage; private String description; private String craftsman; private String material; private BigDecimal price; private String tags; @TableField("view_count") private Integer viewCount; @TableField("create_time") private LocalDateTime createTime; }Mapper 接口:
// 文件路径:src/main/java/com/weaving/mapper/ProductMapper.java @Mapper public interface ProductMapper extends BaseMapper<Product> { }Service 层写法:
// 文件路径:src/main/java/com/weaving/service/ProductService.java @Service public class ProductService { @Autowired private ProductMapper productMapper; // 新增 public boolean addProduct(Product product) { return productMapper.insert(product) > 0; } // 删除 public boolean deleteProduct(Integer id) { return productMapper.deleteById(id) > 0; } // 修改 public boolean updateProduct(Product product) { return productMapper.updateById(product) > 0; } // 查询单个 public Product getById(Integer id) { return productMapper.selectById(id); } // 分页查询 public Page<Product> pageProducts(int page, int size, String keyword) { Page<Product> pageInfo = new Page<>(page, size); QueryWrapper<Product> wrapper = new QueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like("title", keyword); } wrapper.orderByDesc("create_time"); return productMapper.selectPage(pageInfo, wrapper); } }这是典型的增删改查代码,几乎所有毕设后台管理部分都是这个套路。需要注意的是,MyBatis Plus 的Page对象返回时,直接往 Controller 返回即可,它会包含records、total、current、size等分页字段。
3.3 遍历查询与动态条件拼接
在实际业务场景中,前端列表页通常带有搜索条件。我们可以使用QueryWrapper动态拼接 SQL,避免写大量 if 判断。
public Page<Product> searchProducts(int page, int size, Integer categoryId, String keyword, String tag) { Page<Product> pageInfo = new Page<>(page, size); QueryWrapper<Product> wrapper = new QueryWrapper<>(); wrapper.eq(categoryId != null, "category_id", categoryId); wrapper.like(StringUtils.hasText(keyword), "title", keyword); wrapper.like(StringUtils.hasText(tag), "tags", tag); wrapper.orderByDesc("view_count"); return productMapper.selectPage(pageInfo, wrapper); }QueryWrapper的第一个参数是 boolean 条件,只有条件为 true 时,后面的 SQL 片段才会拼接到最终执行的语句中。这种写法可以避免页面传空值时产生无意义的where category_id = null。
4. 内容推荐算法实现
推荐部分是整个项目最有技术亮点的模块,也是答辩时最容易获得加分的地方。下面用一个可运行的简化版说明整套实现。
4.1 推荐算法实现思路
整个推荐模块分为三步:
- 获取当前用户的兴趣偏好向量。
- 获取所有待推荐创作作品的标签向量。
- 计算余弦相似度,排序后取 Top N。
用户兴趣向量来自两部分:
- 浏览记录:每浏览一次,该作品的所有标签权重 + 1。
- 收藏记录:每收藏一次,该作品的所有标签权重 + 3(收藏比浏览更能代表兴趣)。
这样我们就得到一个类似下面的用户偏好表:
| 标签 | 权重 |
|---|---|
| 绳编 | 5 |
| 中国结 | 3 |
| 挂饰 | 2 |
| 毛线 | 1 |
同时每个作品也有标签向量,比如一件作品 tags 为“绳编,挂饰,中国结”,则该作品的向量为{绳编:1, 挂饰:1, 中国结:1}。
接下来计算用户向量与作品向量的余弦相似度:
similarity = cos(θ) = (A·B) / (|A| × |B|)A 表示用户偏好向量,B 表示作品标签向量。两者点积越大,说明用户对该作品标签的偏好越强;模长用于归一化,避免多标签作品天然获得高分。
这种做法的好处是简单、可解释性强,且不需要离线训练模型,项目跑起来就能看到推荐效果。
4.2 用户行为数据准备
为了计算用户偏好,先定义两个查询方法,分别查询用户浏览和收藏过的作品标签列表。
// 文件路径:src/main/java/com/weaving/service/RecommendService.java @Service public class RecommendService { @Autowired private ProductMapper productMapper; @Autowired private BrowseRecordMapper browseRecordMapper; @Autowired private FavoriteMapper favoriteMapper; /** * 构建用户标签偏好向量 */ private Map<String, Double> buildUserPreferenceVector(Integer userId) { Map<String, Double> vector = new HashMap<>(); // 1. 查询用户浏览过的所有作品ID List<Integer> browseProductIds = browseRecordMapper.selectProductIdsByUserId(userId); if (CollectionUtils.isNotEmpty(browseProductIds)) { List<Product> products = productMapper.selectBatchIds(browseProductIds); for (Product product : products) { for (String tag : splitTags(product.getTags())) { vector.put(tag, vector.getOrDefault(tag, 0.0) + 1.0); } } } // 2. 查询用户收藏过的所有作品ID List<Integer> favoriteProductIds = favoriteMapper.selectProductIdsByUserId(userId); if (CollectionUtils.isNotEmpty(favoriteProductIds)) { List<Product> products = productMapper.selectBatchIds(favoriteProductIds); for (Product product : products) { for (String tag : splitTags(product.getTags())) { vector.put(tag, vector.getOrDefault(tag, 0.0) + 3.0); } } } return vector; } private List<String> splitTags(String tags) { if (StringUtils.hasText(tags)) { return Arrays.asList(tags.split(",")); } return Collections.emptyList(); } }这里给收藏行为更高的权重(3 vs 1),是因为收藏行为比一次浏览更能反映用户兴趣。权重值不是固定死的,你可以根据平台数据实际情况调整,比如加入评论行为 +2、加入分享行为 +4 等。
4.3 余弦相似度核心算法实现
有了用户的偏好向量,接下来就可以计算每个待推荐作品与用户偏好之间的相似度。
/** * 计算向量余弦相似度 */ private double cosineSimilarity(Map<String, Double> userVector, Map<String, Double> itemVector) { if (userVector.isEmpty() || itemVector.isEmpty()) { return 0.0; } // 点积 double dotProduct = 0.0; for (Map.Entry<String, Double> entry : itemVector.entrySet()) { String tag = entry.getKey(); if (userVector.containsKey(tag)) { dotProduct += entry.getValue() * userVector.get(tag); } } // 模长 double userNorm = 0.0; for (Double value : userVector.values()) { userNorm += value * value; } double itemNorm = 0.0; for (Double value : itemVector.values()) { itemNorm += value * value; } if (userNorm == 0.0 || itemNorm == 0.0) { return 0.0; } return dotProduct / (Math.sqrt(userNorm) * Math.sqrt(itemNorm)); }4.4 推荐接口完整实现
接下来把上面的方法串起来,实现完整的推荐逻辑。推荐结果有两个要求:
- 用户已收藏过的作品不再推荐。
- 若用户没有行为数据,则按浏览量降序返回热门作品(冷启动策略)。
/** * 获取用户推荐作品 */ public List<Product> recommendProducts(Integer userId, int topN) { // 1. 获取用户偏好向量 Map<String, Double> userVector = buildUserPreferenceVector(userId); // 2. 如果用户偏好为空,使用热门作品兜底 if (userVector.isEmpty()) { QueryWrapper<Product> hotWrapper = new QueryWrapper<>(); hotWrapper.orderByDesc("view_count"); hotWrapper.last("LIMIT " + topN); return productMapper.selectList(hotWrapper); } // 3. 查询所有作品 List<Product> allProducts = productMapper.selectList(null); // 4. 查询用户已收藏作品ID,用于排除 List<Integer> favoriteIds = favoriteMapper.selectProductIdsByUserId(userId); // 5. 遍历计算相似度 List<RecommendItem> recommendList = new ArrayList<>(); for (Product product : allProducts) { if (favoriteIds.contains(product.getId())) { continue; } Map<String, Double> itemVector = new HashMap<>(); for (String tag : splitTags(product.getTags())) { itemVector.put(tag, 1.0); } double score = cosineSimilarity(userVector, itemVector); if (score > 0) { recommendList.add(new RecommendItem(product, score)); } } // 6. 按相似度降序取前 topN recommendList.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); List<Product> result = new ArrayList<>(); for (int i = 0; i < Math.min(topN, recommendList.size()); i++) { result.add(recommendList.get(i).getProduct()); } return result; } @Data @AllArgsConstructor class RecommendItem { private Product product; private double score; }4.5 推荐效果说明
运行后,如果某个用户浏览过多个“绳编、中国结”类作品,那么首页“猜你喜欢”模块会优先推荐同样带这些标签的作品,效果符合直觉。
如果系统检测到用户从未浏览或收藏任何作品,推荐接口不会崩溃,而是返回全站浏览量最高的作品——这就是推荐系统中的“冷启动”处理。毕设答辩时,面试官特别喜欢问“如果新用户没有行为数据怎么办”,这一套代码给出了合理答案。
5. 前后端分离核心实现
5.1 后端接口设计
后端 Controller 采用 RESTful 风格,统一返回Result<T>包装类。
// 文件路径:src/main/java/com/weaving/common/Result.java @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }作品模块的 Controller:
// 文件路径:src/main/java/com/weaving/controller/ProductController.java @RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @Autowired private RecommendService recommendService; @GetMapping("/list") public Result<Page<Product>> list( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String keyword) { return Result.success(productService.pageProducts(page, size, keyword)); } @GetMapping("/detail/{id}") public Result<Product> detail(@PathVariable Integer id) { return Result.success(productService.getById(id)); } @PostMapping("/add") public Result<?> add(@RequestBody Product product) { return productService.addProduct(product) ? Result.success(null) : Result.error("添加失败"); } @PutMapping("/update") public Result<?> update(@RequestBody Product product) { return productService.updateProduct(product) ? Result.success(null) : Result.error("修改失败"); } @DeleteMapping("/delete/{id}") public Result<?> delete(@PathVariable Integer id) { return productService.deleteProduct(id) ? Result.success(null) : Result.error("删除失败"); } @GetMapping("/recommend") public Result<List<Product>> recommend( @RequestParam Integer userId, @RequestParam(defaultValue = "8") int topN) { return Result.success(recommendService.recommendProducts(userId, topN)); } }5.2 前端 Axios 请求封装
前端先封装统一的 axios 实例:
// 文件路径:src/utils/request.js import axios from 'axios'; import { Message } from 'element-ui'; const request = axios.create({ baseURL: 'http://localhost:8080', timeout: 10000 }); // 请求拦截器:携带token request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; } return config; }); // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res; }, error => { Message.error(error.message || '网络异常'); return Promise.reject(error); } ); export default request;5.3 前端推荐模块页面
首页“猜你喜欢”模块用 Vue 渲染:
<!-- 文件路径:src/views/home/RecommendProducts.vue --> <template> <div class="recommend-container"> <h2 class="section-title">猜你喜欢</h2> <el-row :gutter="20"> <el-col :span="6" v-for="item in recommendList" :key="item.id"> <el-card :body-style="{ padding: '10px' }" class="product-card" shadow="hover"> <img :src="item.coverImage" class="product-image" /> <div class="product-title">{{ item.title }}</div> <div class="product-tags"> <el-tag v-for="tag in splitTags(item.tags)" :key="tag" size="mini">{{ tag }}</el-tag> </div> <div class="product-footer"> <span class="price">¥{{ item.price }}</span> <el-button type="text" @click="goDetail(item.id)">查看</el-button> </div> </el-card> </el-col> </el-row> </div> </template> <script> import request from '@/utils/request'; export default { name: 'RecommendProducts', data() { return { recommendList: [] }; }, created() { this.loadRecommend(); }, methods: { splitTags(tags) { return tags ? tags.split(',') : []; }, checkLogin() { const token = localStorage.getItem('token'); if (!token) { this.$message.warning('登录后才能获取个性化推荐'); return false; } return true; }, loadRecommend() { if (!this.checkLogin()) { return; } const userId = localStorage.getItem('userId'); request.get('/api/product/recommend', { params: { userId } }).then(res => { this.recommendList = res.data; }); }, goDetail(id) { this.$router.push({ path: `/product/${id}` }); } } }; </script>未登录时,前端不做请求,而是提示用户登录。这样既避免后端对 anonymous 用户返回空推荐,也符合业务预期。
5.4 跨域问题处理
前后端分离开发中最常见的坑就是跨域。后端需要允许前端域名访问接口,常见方案是在 Spring Boot 中配置 CORS。
// 文件路径:src/main/java/com/weaving/config/CorsConfig.java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }需要注意的是,allowedOriginPatterns("*")与allowCredentials(true)必须搭配使用,这样前端携带 cookie 或 token 请求时浏览器才不会拦截。
6. 核心源码目录结构
整个项目分为后端weaving-server和前端weaving-front两个目录。
weaving-server/ # 后端 Spring Boot 项目 ├── src/main/java/com/weaving/ │ ├── config/ # 全局配置(CORS、MyBatisPlus分页插件) │ ├── common/ # 统一返回结果、全局异常处理 │ ├── controller/ # 控制层(Product、User、Favorite、Recommend) │ ├── entity/ # 数据库实体类 │ ├── mapper/ # MyBatis Plus Mapper接口 │ ├── service/ # 业务逻辑层 │ │ ├── ProductService.java │ │ ├── UserService.java │ │ └── RecommendService.java # 推荐算法核心 │ └── utils/ # JWT工具类、字符串工具类 └── src/main/resources/ ├── application.yml # 数据源、端口等配置 └── sql/weaving_db.sql # 建库建表脚本 weaving-front/ # 前端 Vue 项目 ├── src/ │ ├── api/ # 接口请求模块 │ ├── views/ │ │ ├── home/Home.vue # 首页+猜你喜欢 │ │ ├── product/ProductList.vue # 作品列表 │ │ ├── product/ProductDetail.vue # 作品详情 │ │ ├── admin/AdminProduct.vue # 后台作品管理 │ │ └── user/Login.vue # 登录注册 │ ├── router/index.js # 前端路由 │ ├── store/ # Vuex状态管理 │ └── utils/request.js # Axios封装 └── package.json这种目录结构把前后端完全分离开,代码职责清晰,答辩时讲解项目结构会非常顺畅。
7. 常见问题与排查思路
7.1 数据库连接报错
错误现象:
Access denied for user 'root'@'localhost' Communications link failure常见原因:
- MySQL 密码或用户写错。
- 数据库尚未启动。
- 没有提前创建 weaving_db 数据库。
排查步骤:
- 检查 application.yml 中 username 和 password 是否正确。
- 使用命令行或 Navicat 连接测试。
- 确认数据库是否已导入 SQL 脚本。
# 文件路径:src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/weaving_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 07.2 前端请求接口跨域
错误现象:
浏览器控制台出现:
Access to XMLHttpRequest at 'http://localhost:8080/api/...' from origin 'http://localhost:8081' has been blocked by CORS policy处理方案:
- 后端添加 CorsConfig 配置。
- 确认前端 axios baseURL 指向后端 8080 端口。
- 如果使用部署后的 Nginx 反向代理,可以由 Nginx 统一解决跨域。
7.3 推荐结果为空
错误现象:接口返回[]。
可能原因:
- 用户没有任何浏览和收藏记录,且所有作品 view_count 都为 0。
- 所有作品 tags 字段为空。
- 用户已收藏了全部作品,待推荐集合为空。
解决思路:
- 确认是否有浏览/收藏数据,没有可以手工造几条测试。
- 确保作品 tags 字段有值,推荐算法依赖标签。
- 冷启动兜底逻辑除了 view_count,还可以加“最新发布”作为第二排序条件。
7.4 常见错误汇总表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Spring Boot 启动失败 | 端口被占用 | 修改 server.port 或关闭占用进程 |
| MyBatis Plus 实体字段映射失败 | 数据库字段名与实体属性名不一致 | 使用 @TableField 指定列名 |
| 前端打包后接口 404 | baseURL 路径写错 | 检查 axios baseURL 配置 |
| 用户登录后接口 401 | token 过期或未携带 | 检查请求拦截器是否注入 token |
| 上传图片不显示 | 静态资源映射未配置 | 在 Spring Boot 中配置资源映射或使用绝对路径 |
7.5 静态资源无法访问
如果作品图片上传到了本地磁盘目录,而前端访问不了,需要在后端配置静态资源映射:
// 文件路径:src/main/java/com/weaving/config/WebMvcConfig.java @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = "file:" + System.getProperty("user.dir") + "/upload/"; registry.addResourceHandler("/upload/**") .addResourceLocations(uploadPath); } }这样访问http://localhost:8080/upload/example.jpg就能定位到本地磁盘目录的图片。
8. 最佳实践与工程建议
8.1 代码分层要严格
这个项目建议严格遵循 Controller → Service → Mapper 三层结构,不建议在 Controller 里直接调用 Mapper。原因有三:
- 后续加缓存、加事务、加日志时,只需要改 Service,Controller 和 Mapper 都不动。
- 推荐算法逻辑放在 Service 层,方便单元测试。
- 答辩时讲解“三层架构”是加分项,代码组织直观。
8.2 密码安全与权限控制
用户登录密码不要明文存储,使用 BCrypt 加密:
// 文件路径:src/main/java/com/weaving/service/UserService.java @Service public class UserService { @Autowired private UserMapper userMapper; public User login(String username, String password) { User user = userMapper.selectOne( new QueryWrapper<User>().eq("username", username) ); if (user == null) { throw new RuntimeException("用户不存在"); } // 使用 BCrypt 校验密码 if (!BCrypt.checkpw(password, user.getPassword())) { throw new RuntimeException("密码错误"); } return user; } public boolean register(User user) { String encodedPwd = BCrypt.hashpw(user.getPassword(), BCrypt.gensalt()); user.setPassword(encodedPwd); return userMapper.insert(user) > 0; } }这样即使数据库泄露,攻击者也无法直接拿到明文密码。
8.3 操作类接口需要权限校验
管理后台的增删改查接口不能允许普通用户调用。建议使用简单拦截器,校验请求头中的 JWT,如果是管理员role才能进入/api/admin/**。
// 文件路径:src/main/java/com/weaving/config/AuthInterceptor.java @Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); String requestUri = request.getRequestURI(); // 管理端接口需要校验管理员角色 if (requestUri.startsWith("/api/admin/")) { if (token == null || !JwtUtils.isAdmin(token)) { response.setStatus(401); return false; } } return true; } }8.4 推荐算法埋点与评估
推荐模块上线后,要想知道推荐效果好不好,不能只靠主观感觉。建议做两件事:
- 记录“推荐位点击率”:前端统计每次推荐位展示了哪些作品、用户点击了哪个,生成曝光-点击日志。
- 做效果对比:一部分用户使用推荐页,另一部分用户使用热门页,比较收藏转化率。
虽然不是毕设必须项,但如果能在答辩时提到这个评估思路,会让老师觉得你不只是写了功能,而是有数据思维。
8.5 部署注意事项
生产部署时,推荐使用前后端分离的部署方式:
- 前端
npm run build生成 dist 静态文件。 - 用 Nginx 托管 dist 目录。
- 后端
mvn clean package打成 jar 包。 - jar 包通过
java -jar weaving-server.jar启动。
Nginx 配置可以这样写:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html/weaving-front/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样/api/会被代理到后端服务,前端静态文件由 Nginx 直接返回,解决前后端跨域问题的同时,也避免用history路由刷新页面 404。
9. 项目运行步骤与后续扩展
9.1 从源码运行项目的步骤
拿到源码后,按以下顺序操作即可快速跑通:
# 1. 导入数据库 mysql -u root -p < weaving-server/src/main/resources/sql/weaving_db.sql # 2. 启动后端 cd weaving-server mvn clean package java -jar target/weaving-server-0.0.1-SNAPSHOT.jar # 3. 启动前端 cd weaving-front npm install npm run serve启动完成后,浏览器访问http://localhost:8081即可看到系统首页。
9.2 功能扩展方向
如果想让毕设项目更进一步,可以从以下几个方向扩展:
- 改进推荐算法:在内容推荐的基础上,引入协同过滤,把“看了这个作品的人还看了”也加入推荐流,让整个推荐系统从单路变成双路融合。
- 增加社区模块:增加手工艺人的动态发布,用户可以关注手工艺人,平台根据关注关系推送动态。
- 增加视频教程:非遗编织很适合短视频展示,增加视频上传与在线播放模块。
- 接入对象存储:把本地图片存储换成 OSS 或 MinIO,提升图片访问速度。
- 引入 Redis 缓存:热门作品列表、推荐结果都可以缓存到 Redis,降低数据库压力。
其中第 1 点和第 5 点最值得做。推荐算法从单路到双路,是算法能力最直观的体现;Redis 缓存则是 Spring Boot 项目面试高频考点,加进去之后项目含金量会明显提升。
9.3 手工编织非遗平台项目复盘
回过头来看这个项目,它的核心价值在于三点:
- 业务定位准确:非遗手工编织有明确的目标用户和内容特色,比泛泛的“校园二手交易平台”更有辨识度。
- 技术栈完整:Spring Boot + Vue 前后端分离、MyBatis Plus 增删改查、JWT 权限控制、推荐算法落地,覆盖了企业开发的常见要求。
- 推荐算法有加分点:虽然没有用大数据平台,但用余弦相似度实现个性化推荐,并在冷启动、权重设计、效果评估等方面做了合理考量,已经超出了大部分毕设项目的深度。
如果你拿到这套源码,建议先不要急着改页面,而是把推荐模块的代码完整读一遍。理解buildUserPreferenceVector如何把用户行为转成向量,理解cosineSimilarity如何计算相似度,再把权重值自己调一调,看看推荐结果有什么变化。亲手改过算法参数之后,你才真正掌握了这个项目最核心的知识点,答辩时也更有底气。
项目里的源码和视频教学资料都包含了完整注释,遇到不懂的地方可以对照源码逐行调试。动手跑通一个前后端分离的完整项目,比只读十篇教程都管用。