一、项目背景与意义
山东剪纸作为国家级非物质文化遗产,承载着齐鲁大地的历史记忆与民间智慧。然而,传统的剪纸技艺传承主要依赖师徒口传心授和线下展览,面临着传播范围有限、教学资源匮乏、年轻一代兴趣不足等挑战。在数字化浪潮下,构建一个集展示、教学、互动于一体的线上平台,对于非遗的保护、传承与创新具有深远意义。
本项目的意义主要体现在以下几个方面:
- 文化保护与传播:通过高清图片、视频、3D模型等数字化手段,永久性保存剪纸精品,打破地域限制,向全球展示山东剪纸的艺术魅力。
- 创新教学模式:提供在线教程、分步图解、视频演示、虚拟仿真等多元化教学资源,降低学习门槛,激发公众尤其是青少年的学习兴趣。
- 社区互动与活化:搭建创作者与爱好者交流社区,鼓励用户上传作品、分享心得,形成活态传承的生态圈。
- 学术研究支持:系统化整理剪纸的历史脉络、流派风格、技法特点,为研究者提供宝贵的数字资料库。
二、技术栈选型
本项目采用前后端分离的架构,旨在构建一个高性能、易维护、可扩展的现代化Web应用。
1. 后端技术栈 (SpringBoot)
- 核心框架:Spring Boot 2.7+,提供快速的项目构建和自动配置。
- Web层:Spring MVC,处理HTTP请求和RESTful API设计。
- 数据持久层:
- Spring Data JPA:简化数据库操作。
- MySQL 8.0:关系型数据库,存储用户、作品、教程、评论等结构化数据。
- 安全与权限:Spring Security + JWT (JSON Web Token),实现用户认证、授权和接口安全防护。
- 文件存储:
- 本地存储/MinIO:用于存储用户上传的图片、视频等多媒体资源。
- FastDFS/阿里云OSS(可选):用于分布式或云存储方案,提升大文件访问性能。
- 缓存:Redis,缓存热点数据(如首页作品、热门教程),提升系统响应速度。
- 搜索:Elasticsearch,实现剪纸作品、教程的全文检索和复杂条件筛选。
- 任务调度:Spring Scheduler/Quartz,定时执行数据备份、资源清理等任务。
- API文档:SpringDoc OpenAPI (Swagger UI),自动生成并可视化后端API接口文档。
2. 前端技术栈 (Vue.js)
- 核心框架:Vue 3 (Composition API) + TypeScript,提升开发体验和代码可维护性。
- 构建工具:Vite,提供极速的冷启动和模块热更新。
- UI组件库:Element Plus / Ant Design Vue,快速搭建美观、一致的用户界面。
- 状态管理:Pinia,替代Vuex,提供更简洁、类型安全的状态管理。
- 路由:Vue Router 4,实现单页面应用(SPA)的路由管理。
- HTTP客户端:Axios,封装请求拦截、响应处理和错误统一管理。
- 可视化与交互:
- ECharts:用于数据统计图表展示(如用户增长、作品分类占比)。
- PhotoSwipe:实现图片画廊与灯箱效果。
- Video.js:用于教学视频的播放与控制。
- 3D展示(进阶):Three.js,实现剪纸作品的3D模型旋转、缩放查看。
3. 开发与部署
- 版本控制:Git + GitHub/GitLab。
- 项目管理:Maven (后端) / npm/pnpm (前端)。
- 容器化:Docker + Docker Compose,实现环境标准化和快速部署。
- 持续集成/持续部署 (CI/CD):Jenkins/GitHub Actions,自动化构建、测试和部署流程。
- 服务器:Linux (CentOS/Ubuntu) + Nginx(反向代理、静态资源服务)。
三、核心功能模块与数据库设计
1. 核心功能模块
- 用户中心:注册、登录、个人信息管理、我的作品、我的收藏、学习记录。
- 非遗展示馆:剪纸作品分类浏览(按地区、流派、题材)、精品推荐、高清大图查看、3D模型互动、作品详情(作者、技法、故事)。
- 在线教学系统:教程分类(入门、进阶、专题)、图文/视频教程、分步图解、虚拟剪纸模拟器、学习进度跟踪、课后练习与提交。
- 社区互动:作品发布与分享、评论、点赞、关注、话题讨论、活动报名。
- 后台管理系统:用户管理、内容审核(作品/教程/评论)、数据统计、系统配置、轮播图管理。
2. 核心数据库表设计(部分)
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `user_type` tinyint DEFAULT '0' COMMENT '用户类型(0:普通用户,1:剪纸艺人,2:管理员)', `status` tinyint DEFAULT '1' COMMENT '状态(0:禁用,1:正常)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 剪纸作品表 CREATE TABLE `artwork` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '作品标题', `description` text COMMENT '作品描述', `cover_image` varchar(500) NOT NULL COMMENT '封面图URL', `image_urls` json DEFAULT NULL COMMENT '作品多图URL数组', `video_url` varchar(500) DEFAULT NULL COMMENT '制作过程视频URL', `model_3d_url` varchar(500) DEFAULT NULL COMMENT '3D模型文件URL', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `region` varchar(100) DEFAULT NULL COMMENT '所属地区(如:烟台、潍坊)', `style` varchar(100) DEFAULT NULL COMMENT '流派风格', `technique` varchar(200) DEFAULT NULL COMMENT '使用技法', `artist_id` bigint DEFAULT NULL COMMENT '关联艺人用户ID', `view_count` int DEFAULT '0' COMMENT '浏览量', `like_count` int DEFAULT '0' COMMENT '点赞数', `collect_count` int DEFAULT '0' COMMENT '收藏数', `status` tinyint DEFAULT '0' COMMENT '状态(0:待审核,1:已发布,2:已下架)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_artist` (`artist_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='剪纸作品表'; -- 在线教程表 CREATE TABLE `tutorial` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL, `cover_image` varchar(500) DEFAULT NULL, `video_url` varchar(500) DEFAULT NULL COMMENT '教学视频URL', `content` longtext COMMENT '教程详细内容(富文本HTML)', `difficulty` tinyint DEFAULT '1' COMMENT '难度等级(1:入门,2:进阶,3:高级)', `step_count` int DEFAULT '0' COMMENT '步骤数', `duration` int DEFAULT NULL COMMENT '预计学习时长(分钟)', `author_id` bigint NOT NULL COMMENT '作者用户ID', `view_count` int DEFAULT '0', `like_count` int DEFAULT '0', `collect_count` int DEFAULT '0', `status` tinyint DEFAULT '0' COMMENT '状态(0:草稿,1:已发布)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_author` (`author_id`), KEY `idx_difficulty` (`difficulty`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='在线教程表';四、核心代码实现示例
1. SpringBoot后端核心代码
a. 统一RESTful API响应封装
package com.shandong.papercut.common; import lombok.Data; import java.io.Serializable; @Data public class Result<T> implements Serializable { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("success"); result.setData(data); return result; } public static <T> Result<T> success(String msg, T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg(msg); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); return result; } }b. 作品分页查询接口 (Controller层)
package com.shandong.papercut.controller; import com.shandong.papercut.common.PageResult; import com.shandong.papercut.common.Result; import com.shandong.papercut.entity.Artwork; import com.shandong.papercut.service.ArtworkService; import com.shandong.papercut.vo.ArtworkVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/artwork") public class ArtworkController { @Autowired private ArtworkService artworkService; @GetMapping("/page") public Result<PageResult<ArtworkVO>> pageQuery( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String region) { PageResult<ArtworkVO> pageResult = artworkService.pageQuery(pageNum, pageSize, keyword, categoryId, region); return Result.success(pageResult); } @GetMapping("/{id}") public Result<ArtworkVO> getDetail(@PathVariable Long id) { // 增加浏览量 artworkService.incrementViewCount(id); ArtworkVO detail = artworkService.getDetailById(id); return Result.success(detail); } @PostMapping("/{id}/like") public Result<String> like(@PathVariable Long id) { artworkService.like(id); return Result.success("点赞成功"); } }c. 作品服务层实现 (Service层)
package com.shandong.papercut.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.shandong.papercut.common.PageResult; import com.shandong.papercut.entity.Artwork; import com.shandong.papercut.mapper.ArtworkMapper; import com.shandong.papercut.service.ArtworkService; import com.shandong.papercut.vo.ArtworkVO; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.List; import java.util.stream.Collectors; @Service public class ArtworkServiceImpl implements ArtworkService { @Autowired private ArtworkMapper artworkMapper; @Override @Cacheable(value = "artworkPage", key = "#pageNum + '-' + #pageSize + '-' + #keyword + '-' + #categoryId + '-' + #region") public PageResult<ArtworkVO> pageQuery(Integer pageNum, Integer pageSize, String keyword, Long categoryId, String region) { LambdaQueryWrapper<Artwork> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Artwork::getStatus, 1); // 只查询已发布的作品 if (StringUtils.hasText(keyword)) { queryWrapper.like(Artwork::getTitle, keyword).or().like(Artwork::getDescription, keyword); } if (categoryId != null) { queryWrapper.eq(Artwork::getCategoryId, categoryId); } if (StringUtils.hasText(region)) { queryWrapper.eq(Artwork::getRegion, region); } queryWrapper.orderByDesc(Artwork::getCreateTime); // 按创建时间倒序 Page<Artwork> page = new Page<>(pageNum, pageSize); Page<Artwork> artworkPage = artworkMapper.selectPage(page, queryWrapper); List<ArtworkVO> voList = artworkPage.getRecords().stream().map(artwork -> { ArtworkVO vo = new ArtworkVO(); BeanUtils.copyProperties(artwork, vo); // 可在此处补充关联查询,如作者昵称、分类名称等 return vo; }).collect(Collectors.toList()); return new PageResult<>(artworkPage.getTotal(), voList); } @Override public void incrementViewCount(Long id) { Artwork artwork = artworkMapper.selectById(id); if (artwork != null) { artwork.setViewCount(artwork.getViewCount() + 1); artworkMapper.updateById(artwork); } } @Override public void like(Long id) { // 实际业务需结合用户ID,防止重复点赞,此处简化 Artwork artwork = artworkMapper.selectById(id); if (artwork != null) { artwork.setLikeCount(artwork.getLikeCount() + 1); artworkMapper.updateById(artwork); } } }2. Vue3前端核心代码示例
a. 作品列表页面组件 (ArtworkList.vue)
<template> <div class="artwork-list"> <div class="filter-bar"> <el-input v-model="queryParams.keyword" placeholder="搜索作品标题或描述" clearable @clear="handleSearch" @keyup.enter="handleSearch" > <template #prefix> <el-icon><Search /></el-icon> </template> </el-input> <el-select v-model="queryParams.region" placeholder="选择地区" clearable @change="handleSearch"> <el-option label="烟台" value="烟台" /> <el-option label="潍坊" value="潍坊" /> <el-option label="青岛" value="青岛" /> </el-select> <el-button type="primary" @click="handleSearch">搜索</el-button> </div> <el-row :gutter="20" v-loading="loading"> <el-col :span="6" v-for="item in artworkList" :key="item.id"> <el-card class="artwork-card" @click="goDetail(item.id)"> <img :src="item.cov