
做这个项目的起因比题面看上去要接地气得多。我当初在选毕业设计题目时翻到一组关于北方人口较少民族的数据鄂温克、鄂伦春、赫哲、达斡尔……有的民族语言使用者仅剩百余人桦树皮制作技艺、兽皮服饰加工、伊玛堪说唱这些非遗项目面临非常严重的传承断代。数据本身不会说话但看完那组材料我把毕设方向直接定成了基于SpringBoot和Vue.js的北方消逝民族网站。简单来说这是一个把北方少数民族的历史源流、文化遗存、口述视频、非遗技艺集中起来做数字化展示的平台前台面向访客提供浏览和检索后台给管理员做内容维护技术栈采用前后端分离架构SpringBoot提供REST APIVue.js负责前端交互渲染。如果你正在选毕业设计题目、第一次打算做前后端分离全栈项目或者对文化数字化方向感兴趣这篇内容可以拿来直接落地。我尽量把从建表、写接口、写页面到打包部署的完整链路都拆开讲包括开发时踩过的坑和排查思路参考价值比单纯甩一个demo要高得多。1. 项目定位与业务需求拆解1.1 这个“濒危民族文化网站”到底要做什么很多同学看到“北方消逝民族”这几个字会以为这是一个纯展示型的静态网站顶多加几个详情页。实际上只要往深处梳理一遍业务需求就会发现它本质上是一个“内容管理系统 多媒体播放 分类检索 后台权限管理”的全栈项目复杂程度并不比常见的商品管理系统低。先把我整理过后的用户角色和对应需求列出来普通访客不需要登录访问首页按民族分类浏览文化专题查看民族档案播放口述历史视频使用关键词搜索内容在联系页面留言。内容管理员需要登录后台对民族信息、文化条目、视频资源、图文资讯做增删改查处理访客留言设置首页推荐位。研究者或文化工作者更关心资料的系统性所以平台必须有清晰的分类体系和全文检索能力而不是一堆内容堆在一起。围绕这些角色功能模块可以拆成这么几块民族档案模块每个民族一个档案页包含民族名称、人口数据、语言现状、主要分布区、历史源流、文化概况等字段。文化遗存模块这是内容量最大的模块。我建议拆成物质文化遗产和非物质文化遗产两个维度前者覆盖服饰、器具、民居、手工艺品后者覆盖仪式、口头传统、音乐舞蹈、节庆习俗。视频与口述历史模块通过视频资源表记录口述历史、非遗技艺演示、民俗活动录像前台支持在线播放。资讯动态模块发布文化保护动态、展览活动信息。检索模块支持按关键词在民族名、文化标题、内容摘要、正文中做模糊查询。后台管理模块管理员登录、内容管理、分类管理、留言审核。留言交互模块访客提交留言管理员审核后在前台展示形成低门槛的互动反馈闭环。这样拆完之后你会发现它和常见的“XX管理系统”最大的区别在于文化内容的字段更复杂多媒体资源占比更高而且需要在分类、民族、推荐位之间维护多层关联。数据库设计不能再按“单表CRUD”的思路走必须在一开始就把关联关系设计清楚。1.2 为什么选择SpringBoot Vue.js前后端分离这个选型不是一个随大流的选择而是有明确理由的。如果你拿相同的题目去问十个人可能八个人会给你推荐SpringBoot Vue因为这在目前Java技术栈里是组合成本最低、资料最多的方案。但我要说的是更深层的考量第一内容型网站天然适合前后端分离。传统文化展示页面有大量交互筛选、分页、轮播、视频播放、滚动加载、富文本渲染。这些交互如果靠JSP模板渲染每一个点击都要刷新一次页面体感非常割裂。Vue把视图和状态管理放在浏览器端后端只需要返回结构化JSON数据前端拿来做渲染用户体验会好很多。第二项目的开发效率高。后端只管接口前端只管页面两边可以并行推进。我当时的节奏是先用两周把后端的表结构和接口跑通用Postman做接口自测与此同时前端按接口文档搭好页面骨架等后端联调时替换真实数据。这个协作模式比传统单体模板开发效率高很多。第三方便后续扩展。如果以后要做小程序端、移动端App后端的REST API可以原封不动复用只需要新做前端壳子。这个价值在做文化数字化平台时尤其明显将来面向不同受众完全可以衍生出不同前端形态但底层的民族档案和文化资源数据是不需要重复建设的。也有人问过为什么不直接用Next.js或Nuxt.js做服务端渲染如果这个网站的核心诉求是搜索引擎收录SSR确实有优势。但作为毕业设计和教学型项目我们更关注的是把Java后端能力、数据库设计能力、前端工程化能力完整地串联起来SPA REST API这条路更贴近企业里最常见的开发模式所以我不推荐在毕设阶段上太重型的SSR架构。2. 业务模块与数据库设计一张民族表如何撑起整个平台2.1 从实体关系到表设计数据库设计是这类项目的重头戏。我的做法是先梳理实体关系再落到具体表结构。核心实体有这些民族ethnic_group、文化分类cultural_category、文化遗产条目cultural_item、图文资讯article、视频资源video_resource、留言message、后台用户sys_user。它们之间的关键关系是一个民族有多个文化遗产条目一个文化遗产条目可以同时属于多个民族吗实际场景中有些文化事象是几个民族共有的比如萨满文化在鄂温克、鄂伦春、达斡尔等民族中都存在。为了简化毕设的复杂度我采用了一个条目只关联一个主要民族的设计把共有文化的问题通过内容正文里的交叉描述来解决。文化遗产条目必须归属到一个分类下这里的分类是两级先分物质/非物质再往下细分具体类型。视频资源和民族关联和文化遗产条目也可以关联但为了少一张中间表我直接在video_resource表里冗余了ethnic_id和item_id两个字段允许为空。这样设计的好处是前台查询时只需要一次JOIN或者两次简单查询就能拿到展示数据链路过短不容易出现性能问题。2.2 核心表结构详解先看民族档案表这是整个平台的主表CREATE TABLE ethnic_group ( id int NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(50) NOT NULL COMMENT 民族名称, alias_name varchar(100) DEFAULT NULL COMMENT 别名/曾用名, population varchar(50) DEFAULT NULL COMMENT 人口概况, language_status varchar(255) DEFAULT NULL COMMENT 语言使用现状, distribution_area varchar(255) DEFAULT NULL COMMENT 主要分布区域, history_desc text COMMENT 历史源流, culture_summary text COMMENT 文化概况, banner_img varchar(255) DEFAULT NULL COMMENT 封面图, sort_order int DEFAULT 0 COMMENT 排序值, status tinyint DEFAULT 1 COMMENT 状态 1显示 0隐藏, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT北方民族档案表;这里有几个值得注意的设计细节population字段用varchar而不是int是因为民族人口数据往往是一个范围描述比如“约3万人”或“3000余人”存数字反而要额外维护单位直接用字符串更省心。language_status单独拎出来做字段是因为语言濒危程度是这个网站的重要展示信息放在显眼位置更能突出主题。banner_img存的是图片URL路径不存二进制数据这是所有内容型项目必须遵守的约定。其他核心表的字段设计我直接整理成表格表名核心字段用途说明cultural_categoryid, category_name, parent_id, sort_order二级分类parent_id为0表示一级分类cultural_itemid, category_id, ethnic_id, title, cover_img, digest, content, era, source, view_count, is_recommended文化条目主体content存富文本articleid, title, cover_img, summary, content, author, view_count, is_top, status图文资讯video_resourceid, title, ethnic_id, item_id, video_url, cover_img, duration, intro, status视频资源支持与民族和条目关联messageid, nickname, content, is_approved, reply_content, create_time访客留言sys_userid, username, password, nickname, avatar, role, status后台管理员账号补充说明一下cultural_item表它的category_id直接指向二级分类省去了一次关联查询。比如一条“桦树皮制作技艺”数据category_id指向“传统技艺”而上级分类“非遗”通过category表的parent_id关联。前台的筛选逻辑是查某个一级分类下的所有内容时先查出该分类下所有二级分类id列表再用in条件查询条目表。2.3 长文本、图片和视频的存储策略文化条目表里的content字段我用的是longtext里面存的是富文本HTML。前端展示时用v-html直接渲染。可能有人担心XSS注入我在后端加了一层简单的HTML过滤只允许常见的p、img、h2、h3、strong、em、ul、li等标签其余一律剥掉。图片和视频的存储需要提前规划。我没有用对象存储因为毕设环境里没有云服务预算直接采用本地磁盘存放加Web服务器静态映射的方式项目根目录下建upload文件夹内部按images、videos两个子目录分类。文件上传接口统一写到upload目录下文件名用UUID重命名避免中文文件名带来的乱码问题。后端通过配置类把upload目录映射成静态资源路径这样访问http://localhost:8080/upload/images/xxx.jpg就能直接拿到文件。前端保存的图片URL就是这个相对路径。视频存储同理但有两点要注意一是大视频上传时要调大SpringBoot的spring.servlet.multipart.max-file-size参数默认1MB肯定不够二是视频要做转码后再上传的话对毕设来说过重直接用H.264编码的MP4文件兼容性最好。如果一定要支持m3u8流媒体前端播放器用video.js加HLS插件即可后端只需要把ts文件和m3u8清单放在静态目录下原理并不复杂。3. 后端SpringBoot核心实现与接口设计3.1 工程结构与依赖清单后端项目的目录结构按常见的Controller-Service-Mapper三层分包src/main/java/com/example/ethnic/ ├── config // 跨域配置、静态资源配置、拦截器注册 ├── controller // 前端接口和后台管理接口 ├── service // 业务逻辑层 ├── mapper // MyBatis-Plus数据访问层 ├── entity // 数据库实体类 ├── dto // 接口入参封装 ├── vo // 接口出参封装 ├── common // 统一返回结果、异常处理、常量 ├── utils // JWT工具类、文件上传工具类 └── interceptor // 后端登录拦截器依赖方面我推荐使用SpringBoot 2.7.x MyBatis-Plus 3.5.x MySQL 8.0 JWT Hutool。重点提醒一下不要一上来就选SpringBoot 3.x因为3.x要求JDK17很多同学的电脑上还是JDK8而且部分老教程里的依赖写法在3.x里会报错。毕设求稳JDK8 SpringBoot 2.7.16是最不容易出问题的组合。pom.xml里的关键依赖大致是这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.18/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency3.2 统一返回结果与分页结构接口设计必须从一开始就统一数据格式否则前后端联调会陷入混乱。我自定义了一个通用返回类Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(success); result.setData(data); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMsg(msg); return result; } }分页接口返回的格式也做了统一直接用MyBatis-Plus的Page对象再封装一层Data public class PageResultT { private ListT records; private long total; private long current; private long size; private long pages; }这样前端拿到数据后可以直接用records填充表格用total渲染分页组件不需要自己去解析Map结构。3.3 民族详情与文化条目列表的查询实现民族详情页的数据来源相对简单按id查主表。但文化条目列表需要考虑筛选我当时的查询逻辑做到了三个维度组合按民族id筛选按分类id筛选按关键词模糊筛选标题和摘要实现代码如下Override public PageResultCulturalItemVO queryList(CulturalQueryDTO dto) { PageCulturalItem page new Page(dto.getPage(), dto.getSize()); LambdaQueryWrapperCulturalItem wrapper new LambdaQueryWrapper(); if (dto.getEthnicId() ! null) { wrapper.eq(CulturalItem::getEthnicId, dto.getEthnicId()); } if (dto.getCategoryId() ! null) { wrapper.eq(CulturalItem::getCategoryId, dto.getCategoryId()); } if (StringUtils.hasText(dto.getKeyword())) { wrapper.and(q - q.like(CulturalItem::getTitle, dto.getKeyword()) .or().like(CulturalItem::getDigest, dto.getKeyword())); } wrapper.eq(CulturalItem::getStatus, 1) .orderByDesc(CulturalItem::getIsRecommended) .orderByDesc(CulturalItem::getCreateTime); PageCulturalItem result culturalItemMapper.selectPage(page, wrapper); // 转VO并填充民族名称 ListCulturalItemVO records result.getRecords().stream().map(item - { CulturalItemVO vo new CulturalItemVO(); BeanUtils.copyProperties(item, vo); EthnicGroup ethnic ethnicGroupMapper.selectById(item.getEthnicId()); if (ethnic ! null) { vo.setEthnicName(ethnic.getName()); } return vo; }).collect(Collectors.toList()); PageResultCulturalItemVO pageResult new PageResult(); pageResult.setRecords(records); pageResult.setTotal(result.getTotal()); // ... 省略其余字段赋值 return pageResult; }这里有一个设计心得实体类里的字段名和前端要展示的字段往往不完全一致。比如文化条目表里只有ethnic_id但前端详情页需要显示民族名称所以我在VO层加了一个ethnicName字段通过一次主表查询填充。这种“实体入库、VO出参”的模式非常推荐它避免了你为了展示一个多余字段就去改表结构的尴尬。3.4 后台登录鉴权JWT拦截器怎么落地后台管理不能裸奔我用了JWT做无状态登录认证整体链路是管理员在登录页输入账号密码后端校验通过后签发一个有效期为24小时的token返回。前端把token存在localStorage中每次请求经过axios拦截器自动塞进Authorization请求头。后端写一个LoginInterceptor拦截所有/admin开头的接口解析并校验token非法请求直接返回401状态码和JSON错误信息。核心拦截器代码如下Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录状态失效请重新登录\}); return false; } } }密码加密用的是BCrypt不要明文存密码。Hutool工具类里有BCrypt实现直接把密码加密后的hash存到sys_user表的password字段登录校验时用BCrypt.checkpw比对即可。3.5 文件上传接口与静态资源映射上传接口是内容项目的刚需后台需要上传民族封面、文化条目图片、视频文件。我用MultipartFile接收文件统一保存到upload目录PostMapping(/admin/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; // 按日期分目录存储避免单目录文件过多 String dateDir new SimpleDateFormat(yyyyMMdd).format(new Date()); File dir new File(uploadPath / dateDir); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(dir.getAbsolutePath() / fileName)); return Result.ok(/upload/ dateDir / fileName); } catch (IOException e) { return Result.error(上传失败); } }配置文件里需要加spring: servlet: multipart: max-file-size: 200MB max-request-size: 200MB然后写一个WebMvcConfigurer把本地磁盘路径映射成URLConfiguration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); } }注意Linux环境下路径结尾必须有斜杠Windows环境下要写成file:D:/project/upload/这种格式。这个细节当时坑了我将近一天后面在排查章节里详细说。4. 前端Vue.js实现从页面骨架到细节呈现4.1 Vue项目初始化与目录组织前端我用的是Vue 3 Vite Vue Router Pinia Element Plus。Vite相比Webpack的优势是启动速度快依赖安装也简单npm create vuelatest一行命令就能拉好脚手架。建议在src下按功能模块拆分目录src/ ├── api/ // 接口请求统一封装按模块拆分文件 ├── assets/ // 静态资源 ├── components/ // 通用组件 ├── router/ // 路由配置 ├── stores/ // 全局状态管理 ├── views/ // 页面组件 │ ├── home/ // 首页 │ ├── ethnic/ // 民族列表与详情 │ ├── culture/ // 文化遗存列表与详情 │ ├── video/ // 视频专题 │ ├── about/ // 关于我们 │ └── admin/ // 后台管理页面 └── utils/ // 工具函数4.2 前端路由设计静态页面与动态页面并存路由设计是整个前端架构的地基。我采用的方式是常规页面用静态路由注册民族详情页、文化详情页、视频详情页这类带参数的页面用动态路由。Vue Router 4的配置长这样const routes [ { path: /, name: home, component: () import(/views/home/HomeView.vue) }, { path: /ethnic, name: ethnicList, component: () import(/views/ethnic/EthnicList.vue) }, { path: /ethnic/:id, name: ethnicDetail, component: () import(/views/ethnic/EthnicDetail.vue) }, { path: /culture, name: cultureList, component: () import(/views/culture/CultureList.vue) }, { path: /culture/:id, name: cultureDetail, component: () import(/views/culture/CultureDetail.vue) }, { path: /video, name: videoList, component: () import(/views/video/VideoList.vue) }, { path: /about, name: about, component: () import(/views/about/AboutView.vue) }, { path: /admin/login, name: adminLogin, component: () import(/views/admin/AdminLogin.vue) }, { path: /admin, component: () import(/views/admin/AdminLayout.vue), children: [ { path: ethnic, component: () import(/views/admin/AdminEthnic.vue) }, { path: culture, component: () import(/views/admin/AdminCulture.vue) }, { path: video, component: () import(/views/admin/AdminVideo.vue) }, { path: message, component: () import(/views/admin/AdminMessage.vue) } ] } ]前台页面采用懒加载模式点击某个民族档案时通过动态路由的id参数请求详情数据。这里有个经验文化详情页的id不要直接用数据库自增id暴露在URL里虽然对毕设来说问题不大但如果后续要做更多运营建议统一使用随机字符串或雪花ID避免被恶意遍历爬取全部内容。4.3 axios封装与拦截器前端请求必须统一封装不然每个页面对请求头、错误处理、loading状态各写一套项目会变得极其难维护。我封装了一个api模块import axios from axios const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(admin_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data // 统一处理业务码 if (res.code 200) { return res } if (res.code 401) { // 未登录或过期跳转登录页 localStorage.removeItem(admin_token) window.location.href /admin/login return Promise.reject(new Error(res.msg)) } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default servicebaseURL为什么要配置成/api而不是直接写localhost:8080这个问题很关键。如果直接写死后端地址就会在开发环境和生产环境之间来回切换还会遇到跨域问题。我的做法是开发环境在vite.config.js里配置代理把/api的请求转发到后端端口。生产环境用Nginx把/api路径反向代理到后端服务前端代码里不用改任何地址。vite.config.js里的代理配置长这样export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这样后端接口的Controller里不需要加/api前缀前后端分离得干净彻底。同时开发环境浏览器里不会出现跨域CORS错误因为代理转发是同源的。4.4 文化展示页的视觉呈现视觉风格对文化类网站的重要性不亚于技术实现。在设计页面时我没有用默认的花哨模板而是围绕“北方”“民族”“历史文化感”这三个关键词做设计主色调选定深赭石色和米白色模仿桦树皮和兽皮的颜色质感避免使用过于鲜艳的现代配色。首页用大尺寸民族封面图做轮播配以民族名称和简介文字让访客第一眼就能感知到这个平台的主题。民族详情页使用时间线形式展示历史源流用标签形式展示语言现状、分布区域、人口概况。文化遗存列表页采用卡片式布局封面图上叠加分类标签鼠标悬停显示摘要交互偏轻量。视频页面参考视频网站布局左侧主播放区右侧推荐列表播放器使用原生video标签配自定义控制栏。Vue在实现这些交互时非常有优势用transitions-group做列表动画、用懒加载指令做图片渐进加载这些细节虽然工作量不大但会让整个站点的质感提升一个档次。我建议做这类文化展示项目时前端花的时间至少要和后端持平因为内容好不好看是用户对平台的第一印象。5. 打包部署与高频问题排查实录5.1 前端构建产物怎么和后端整合前后端分离项目在开发环境是两条链路跑但要交付成一个可以运行的系统时有两种处理方式。方式一前端构建后单独部署后端单独部署中间用Nginx联通。前端执行npm run build产物生成在dist文件夹。Nginx配置大致如下server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /opt/ethnic/dist; index index.html; try_files $uri $uri/ /index.html; # 解决history路由刷新404 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1: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; } # 上传文件静态映射 location /upload/ { alias /opt/ethnic/upload/; } }方式二把前端构建产物复制到SpringBoot的static目录下打包成一个jar直接运行。操作方法是npm run build生成dist后把dist里的所有文件复制到src/main/resources/static/下然后正常打包SpringBoot。这样访问8080端口时SpringBoot会自动处理静态资源和动态接口。但要注意如果前端用的是history模式路由刷新时会出现404解决办法是在后端加一个转发Controller把非/api路径全部重定向到index.html。两种方式里我更推荐方式一因为Nginx更贴近实际生产环境以后要加独立静态资源目录也方便。但方式二更适合演示一个jar包拷过去就能跑。你可以按交付场景二选一。5.2 我踩过的高频问题与排查清单做这个项目的过程中我记录了一批高频问题和对应的排查思路整理成表格供大家直接参考问题现象根本原因解决方案前端请求后端接口报CORS跨域前后端端口不同且后端未开启跨域后端配置CorsConfig或前端使用Vite代理Vue history模式部署后刷新404刷新时Nginx按路径找静态文件找不到Nginx配try_files或后端转发到index.html数据库中文乱码创建库时未指定utf8mb4连接串缺少编码参数建库用utf8mb4连接串加useUnicodetruecharacterEncodingutf8上传图片后无法访问静态资源映射路径配置错误Linux路径末尾加斜杠Windows用file:D:/path/格式SpringBoot启动提示端口占用本地8080被其他进程占用先查占用杀掉进程或配置server.port切换端口管理员密码登录失败前端可能传了加密串后端未正确解密或BCrypt校验失败统一登录逻辑确认密码字段传递链和加密方式富文本编辑后样式丢失Element Plus的编辑器输出可能有额外样式标签后端过滤过于严格检查内容存储格式调整HTML白名单视频大文件上传超时Nginx默认文件上传大小限制Nginx配置client_max_body_size 200mSpringBoot启动时MyBatis-Plus找不到mapper启动类没加MapperScan或mapper.xml位置不对启动类加MapperScan保证xml在resources/mapper下Vite工程发给别人后npm install报错依赖版本不一致或node环境问题提交package.json和package-lock.json统一node版本5.3 避坑经验与后续扩展思路几个从实操中沉淀下来的经验我觉得必须单独提一下第一接口文档一定要先写。哪怕只是简单的Excel表格把每个接口的URL、请求方式、入参、出参列清楚前后端联调能省一半时间。不要一边写后端一边口头描述给前端数据格式一变就容易出bug。第二CORS配置不要和代理配置同时写死。如果开发环境用Vite代理后端可以不配CORS这样更接近生产环境但如果和我一样喜欢直接用Postman调接口后端还是配一个宽松的CORS方便开发测试。第三重要数据做好初始化SQL。民族档案、文化分类这类基础数据一定整理成init.sql文件随着项目一起提交到Git仓库。不要只在运行界面上手点添加否则换一台电脑部署时数据全靠手工录入不仅慢还容易漏。第四注意数据查询的N1问题。我在写列表页时一开始图省事循环调用selectById查民族名称结果数据量到几百条时明显变慢。后来改成查出来后按id批量查询一次用Map做映射性能提升非常明显。这个思路对任何前端列表页都适用你可以直接复用。至于这个项目后续可以怎么发展我在做完后也想过几条路一是加入数据可视化模块用ECharts绘制各民族人口变化、语言濒危等级分布图让平台从展示型升级为分析型二是增加多语言支持因为传统文化平台面向的受众可能包括海外研究人员英文版的价值完全不同三是把视频内容从MP4升级为m3u8切片方案在弱网环境下播放更加流畅。但这些都是后面的事了。文化数字化的核心从来不只是技术堆叠而是让那些正在从世界上慢慢消失的声音、技艺和记忆有一个可以被更多人看见和检索的窗口。技术是这个窗口的骨架内容才是它真正的灵魂。如果你正在做类似的文化保护方向毕设希望你也能用自己的代码为那些仍在坚持传承的人留下一点痕迹。