1. 项目整体设计与思路拆解
做乡村智慧社区服务平台,核心不是把技术栈堆得多高,而是先想清楚在乡村场景里什么功能会被真正用起来。我见过不少同类项目把重点放在“智慧”两个字上——做大屏、做AI识别、做一堆花哨的可视化图表,结果村民根本不用,村干部也觉得麻烦。这个项目我一开始就把重心放在:公告与村务公开、事项预约与办理、事件上报与反馈、乡村积分、农产品展示这几个最贴近日常的模块上。技术选型用SpringBoot搭后端、Vue搭前端、MySQL存业务数据、MinIO存文件和视频、JWT管登录态。这套组合在中小型系统里非常成熟,会的人也最多,无论你是拿来做毕业设计还是真实部署到村委会,后续维护都不至于抓瞎。
整个平台按角色分成三条线:管理员、村干部、村民。村民只管看公告、上报问题、预约办事;村干部负责处理事件、受理预约、反馈结果;管理员管账号、菜单、数据字典和系统配置。权限边界清晰之后,前后端可以并行开发,前端写页面不用等后端接口等得心慌。平台的功能模块划分我列成一张表:
| 模块 | 主要功能 | 涉及角色 |
|---|---|---|
| 公告村务 | 通知发布、公告轮播、详情展示 | 管理员、村干部、村民 |
| 事件上报 | 问题拍照上报、进度跟踪、处理反馈 | 村民、村干部 |
| 办事预约 | 服务事项、预约时间段、预约记录 | 村民、村干部 |
| 乡村积分 | 积分规则、积分流水、积分排行 | 村民、管理员 |
| 农产品展示 | 农产品列表、详情、联系农户 | 村民 |
| 系统管理 | 用户、角色、菜单、数据字典 | 管理员 |
这张表定下来之后,后端表结构就好设计了。每个模块可以独立拆成几张简单的表,模块之间尽量少做深层join。乡村场景的数据量本身不会特别大,但逻辑一定要独立,后面加需求才不会牵一发动全身。
1.1 技术选型:为什么是SpringBoot + Vue而不是别的
这个问题每次答辩都会被问到,索性一次说清楚。SpringBoot能火这么多年,核心就在于把Spring生态里那些繁琐的XML配置变成了自动装配和starter依赖。创建新项目,引入一个spring-boot-starter-web,写一个启动类,一个Hello接口就跑起来了。很多人对SpringBoot自动装配原理一知半解,其实原理并不复杂:SpringBoot通过AutoConfiguration.imports文件加载一堆自动配置类,再配合@ConditionalOnClass、@ConditionalOnMissingBean这些条件注解按需装配Bean。你在配置文件里放一个spring.datasource.url,数据源自动就配好了,背后就是这条路。
前端选Vue也类似。Vue的学习曲线比React平缓,模板语法对后端出身的人特别友好。这个项目里要处理路由守卫、页面跳转、表单校验、数据绑定,Vue + Vue Router + Pinia这套全家桶完全够用。而且Vue的组件化很舒服,公告列表做一个组件,事件上报做一个组件,登录注册做一个组件,同一套组件改改props就能在好几个页面复用,对乡村场景这种页面风格统一的系统再合适不过。
一句话总结选型逻辑:成熟、文档多、社区大、出问题搜得到、后续招人容易。技术项目最怕用的就是冷门框架,写代码的时候很爽,后面维护的人想骂娘。
1.2 功能模块划分与数据表设计
表的划分我采用了“一模块一核心表”的思路。比如公告模块就一张notice表,字段有id、title、content、cover_url、publish_time、status、create_by。事件上报就一张event_report表,字段有id、user_id、type、description、image_urls、status、feedback、create_time、update_time。办事预约需要两张表:appointment_item和appointment_record。农产品展示需要product表和category表。用户相关的是sys_user、sys_role、sys_menu、sys_user_role,标准RBAC五件套,再加上一张user_info存放村民的详细信息。
这里有一个我写了很多项目之后才养成的习惯:所有表都要有create_time、update_time、deleted三个字段。create_time和update_time用MyBatis-Plus的MetaObjectHandler自动填充,deleted做逻辑删除。线上系统的用户数据、业务流程数据一定要留痕迹,尤其是乡村社区这种可能涉及村务公开、事件追责的场景,一条投诉记录如果被物理删了,后面就说不清了。这个点在答辩时说出来是加分项。
用户表的设计我简化成下面的结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录名 |
| password | varchar(100) | BCrypt加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| phone | varchar(20) | 手机号 |
| role_id | bigint | 角色ID |
| village_id | bigint | 所属村ID |
| status | tinyint | 0禁用1启用 |
| deleted | tinyint | 逻辑删除 |
| create_time | datetime | 创建时间 |
这种表结构非常朴素,但胜在稳定、好理解、易扩展。不要一上来就设计复杂的微服务拆分方案,单体应用加几张表就能跑得非常舒服,先让系统真正落地,比过度设计重要得多。
2. 后端SpringBoot实现复盘
后端部分是整个平台的“大脑”,我从环境准备、工程初始化、登录鉴权、MinIO文件上传、接口统一返回格式五个方向来复盘实现过程。每个环节都有我实际踩过或者身边人反复踩的坑,写出来给大家省点时间。
2.1 环境准备与工程初始化
本地开发环境我用的是JDK 8 + Maven 3.8 + MySQL 5.7,没有刻意追高版本。SpringBoot 2.7.x在JDK 8下的兼容性最稳,部署到服务器上也省心。这里插一句,很多新手一上来装JDK 17甚至JDK 21,然后发现SpringBoot老版本启动直接报错,跑过去问“SpringBoot版本太高怎么办”,其实就是版本匹配问题。SpringBoot 2.x配JDK 8或11都很稳,SpringBoot 3.x才开始要求JDK 17。不要盲目追新版本,能用、稳定才是第一位的。
工程用IDEA自带的Spring Initializr生成Maven项目,核心依赖不多:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.4.6</version> </dependency>MyBatis-Plus帮我省掉了绝大部分Mapper XML的工作,单表CRUD直接调用IService和BaseMapper提供的方法,需要自定义SQL时再在Mapper接口里写@Select注解。这里要说一句,MyBatis-Plus用得再顺手,基础SQL能力不能丢。这个项目里查询条件动态组装要用LambdaQueryWrapper,不懂SQL的话调试起来会非常吃力。
工程目录我按controller、service、mapper、entity、dto、vo、common、config、util九个包划分。common放统一返回结果类Result和全局异常处理器,util放JwtUtil、MinioUtil,config放跨域配置、MyBatis-Plus分页配置、MinIO配置。application.yml分成三个:application-dev.yml、application-prod.yml、application.yml,用spring.profiles.active切换环境。这里我没有上Redis,因为整个平台并发量根本到不了需要Redis的程度,少一个依赖就少一个可能出问题的点。能用简单方案解决的事情,就不要为了技术而技术。
IDEA里配置SpringBoot启动项也很简单,用Spring Boot的启动配置,选好主类,Program arguments里加上--spring.profiles.active=dev,就可以指定启动环境了。第一次跑项目如果报端口冲突,在application.yml里改server.port就行,不需要去系统层面折腾。
2.2 登录鉴权与权限控制
登录鉴权选的是JWT方案,没有上Spring Security加OAuth2那套,因为对乡村社区这种场景来说那套太重了。JWT的思路不复杂:用户登录成功后,后端生成一个token返回给前端,前端保存token,之后每次请求在Header里带上,后端用一个HandlerInterceptor解析token,拿到用户ID和角色决定是否放行。
JwtUtil的核心方法就三个:生成token、解析token、校验token是否过期。生成的时候把用户ID、用户名、角色塞进claims里,过期时间我给了7天。太短了用户要频繁登录,太长不安全。关键代码如下:
public String generateToken(Long userId, String username, String role) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); claims.put("role", role); return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); }拦截器这里有一个重点:前端所有请求都走/api前缀,白名单包括登录、注册、首页公告列表等公开接口,其余全部拦截校验。校验通过后把userId和role放进request的attribute里,后面的Controller直接从request里取,不需要再查一次数据库。这个细节能减少一次数据库IO,虽然不多,但整个系统调优就是靠一个点一个点抠出来的。
权限控制我用了一个最简单但有效的方式:在Controller方法上加自定义注解@RequireRole("admin"),拦截器判断当前用户角色是否匹配,不匹配直接返回403。这种方式和Spring Security的@PreAuthorize比不够优雅,但逻辑直观、代码量小,答辩时把实现原理讲清楚也很加分。有空可以深入探索Spring Security,但项目里用不上就不硬上,保持方案精简。
2.3 MinIO文件上传与访问
乡村智慧社区平台最重的I/O就是图片和短视频。村民上报事件要拍照上传,农产品要传图,公告要传封面。文件存储我用的MinIO,这是一个开源对象存储服务,兼容S3协议,部署非常简单,下载一个可执行文件一条命令就起来了。
MinIO在SpringBoot里的集成分三步:第一,创建配置类读取配置文件的endpoint、accessKey、secretKey、bucketName;第二,写一个MinioUtil工具类,封装上传、删除、生成访问链接的方法;第三,在上传接口里接收MultipartFile,转换成InputStream,调用util上传,返回文件URL。
这里有一个非常容易踩的坑:MinIO的endpoint地址。本地开发写的是http://localhost:9000,部署到服务器后要改成http://服务器IP:9000,如果做了Nginx反向代理,文件访问路径也要处理好。我一开始就是没注意这个,本地好好的,部署到服务器后上传成功但图片加载不出来,排查了半天发现前端拿到的URL是localhost,浏览器当然打不开。解决方案是:把MinIO返回的URL拼成可配置的,在application里加一个file.access-url,上传成功后用access-url加文件名拼出完整URL返回给前端。
还有一个容易被忽略的坑:bucket的访问权限。想让图片直接通过URL公开访问,需要给bucket设置公共读策略。否则每次都要生成临时的presigned URL,虽然更安全,但前端显示图片要带上临时参数,很麻烦。这个项目用的是公共读策略,毕竟村务公告、农产品图片这些内容本来就是要公开的。
2.4 接口设计与统一返回格式
前后端分离项目统一返回格式是必须的,不然前端对接的时候每个人写一个样,能把人逼疯。我在common包里定义了一个Result类:
public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { ... } public static <T> Result<T> error(Integer code, String msg) { ... } }成功的时候code是200,业务失败code是400或者500,未登录是401,没有权限是403。前端axios在拦截器里判断code统一处理,弹一次提示,不会出现一个错误弹三次框的情况。
分页接口的返回我自定义了一个PageResult类,封装total、pages、records、current字段。虽然可以直接用MyBatis-Plus的Page对象返回,但Controller里最好做个转换,避免把MyBatis-Plus的底层对象暴露给前端。接口层的数据结构属于对外契约,要尽量稳定,不要和底层框架绑死。这个意识越早养成越好,后面系统大了之后接口版本管理才不会崩。
3. 前端Vue实现复盘
前端我用的是Vue 3 + Vite + Pinia + Vue Router + Axios + Element Plus这套组合。如果你很熟悉Vue 2,转Vue 3需要一个适应过程,但Vue 3的Composition API写起来确实更顺手,逻辑复用性也更强,新项目直接上Vue 3不要犹豫。
3.1 Vue环境搭建与基础配置
Node环境装好之后,用npm create vite@latest初始化项目,交互式命令行里选Vue,再选JavaScript。这个平台我选的是JavaScript而不是TypeScript,不是TS不好,而是考虑到项目体量和上手成本,纯JS开发速度更快。Vite比Vue CLI快太多,热更新几乎是秒级,开发体验提升明显。Vite 4要求Node 14.18以上,建议用Node 16以上的稳定版本。
工程里我建了views、components、router、store、api、utils、assets这些目录。views放页面组件,components放公共组件,router放路由配置,store放Pinia状态,api放所有接口请求封装,utils放axios实例和工具函数,assets放静态资源。
环境配置用.env.development和.env.production两个文件,分别配置VITE_API_BASE_URL。开发环境指向http://localhost:8080/api,生产环境指向http://服务器IP:8080/api。前端所有请求走相对路径/api开头,开发阶段由后端CORS配置处理跨域,生产环境用Nginx反向代理转发,两边都不折腾。
Vue入门的同学容易卡在依赖安装上,npm install慢或者报错,多半是镜像源问题,换一下taobao镜像地址就能解决。另外npm install之后node_modules目录特别大,这是正常的,不要慌。
3.2 路由设计与动态路由权限
路由是前端的一个重头戏。平台角色有三类,不可能把所有页面都塞给所有人。管理员能看到用户管理、菜单管理;村干部能看到事件处理、预约管理;村民只能看到公告、上报、预约等功能。这种场景用动态路由最合适。
我的做法是:前端先定义一套静态路由,只有登录页、注册页、首页三条。登录成功后,后端根据角色返回该角色能访问的菜单列表和路由路径,前端用router.addRoute()动态添加路由。同时把菜单列表存到Pinia里,侧边栏渲染用的就是这份动态菜单。
这里有个坑:Vue Router的动态路由在刷新页面时会丢失。刷新时Pinia里的状态重置了,路由守卫里需要重新从后端拉取菜单列表再重新addRoute。我的解决方案是在全局前置守卫router.beforeEach里做判断:如果Pinia里有用户信息但没有菜单列表,就调用后端接口重新拉取菜单,然后next({...to, replace: true})重新进入目标路由。这个判断一定要做好,否则会出现刷新后跳404的问题。
路由传参也是实操中经常出问题的地方。query方式传参,参数拼在URL里,刷新页面不会丢;params方式如果不用动态路由占位,刷新就丢了。事件详情的跳转我用的是query方式:router.push({path: '/event/detail', query: {id: row.id}}),详情页刷新也不丢。
3.3 登录态管理与Axios封装
Axios封装做了请求拦截器和响应拦截器。请求拦截器统一从localStorage取出token设置到Authorization头。响应拦截器统一处理code,401时跳转到登录页,其他错误码弹出提示。为了防止重复弹Toast,拦截器里用一个状态标记来控制。
service.interceptors.response.use( (response) => { const res = response.data if (res.code === 200) { return res } if (res.code === 401) { router.push('/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) } )token存在localStorage里,没进cookie也没用pinia持久化插件。token本身由后端控制过期时间,前端只需要存起来每次请求带上就行。localStorage有XSS风险,但这个项目没有引入用户自定义内容渲染页面,风险基本可控。
这类平台里,如果最终要嵌入到微信生态使用,那企业微信和微信公众号的JS-SDK也许还要做一层适配。前端项目集成JS-SDK实际上就是引入@wecom/jssdk这个包,在入口文件里初始化,然后调用register方法。不过这些属于后续扩展的内容,需求一旦明确,再往项目里加不迟。
3.4 核心页面:公告列表、事件上报、办事预约
公告列表页面是典型的列表加详情组合。列表用Element Plus的el-card展示标题、发布时间、封面图,点击进入详情。详情页用el-descriptions展示内容,支持富文本。富文本编辑器用的wangEditor,正经项目里够用,接入也简单。这里有个小细节:el-upload上传文件时,action属性要设置成后端的真实上传地址,同时要带上Authorization头,不然会401。直接写action="http://localhost:8080/api/file/upload"是初学者最常犯的错误,axios实例里配了baseURL但el-upload不会自动用,必须单独把完整地址和headers写清楚。
事件上报页面是最有“乡村场景感”的页面。村民要选事件类型(道路损坏、垃圾堆积、公共设施故障等)、填描述、上传照片。照片上传用el-upload组件,对接后端上传接口,支持多选、预览、删除。这里要注意el-upload的组件校验:文件大小限制、图片格式限制要在beforeUpload钩子里做,否则大图片传上去后端还要处理半天。
办事预约页面分两步:先选事项,再选时间段。事项列表来自后端配置表,时间段是固定的几个档位,比如上午9:00-11:00、下午14:00-16:00。提交的时候后端要校验这个时间段是否已被预约过,用数据库唯一索引加业务校验双重保障。唯一索引的字段是appointment_item_id、appointment_date、time_slot,这样同一个事项同一天同一个时间段只能被预约一次。
4. 平台核心业务流程串讲
如果功能模块只是CRUD就太水了,真正体现平台价值的是核心业务流的闭环。我挑三个最有代表性的展开讲。
4.1 事件上报到处理反馈的完整闭环
先说场景:村民张三发现村口垃圾桶旁的路坑坑洼洼,打开手机进平台,点“事件上报”,选“道路损坏”,拍一张现场照,提交。数据落到event_report表,状态为pending。村干部李四登录后台,待办列表里看到这条事件,点进去看描述和照片,点击“受理”,状态变为processing,系统自动给张三发一条站内信通知“您上报的事件已被受理”。李四处理完之后填写处理结果,上传处理后的照片,点击“完成”,状态变为resolved。张三在“我的上报”里能看到处理反馈和前后对比照片,还能对结果打分评价。这个闭环走完,村民觉得平台有用,才会有下一次使用。
事件状态我用了一个Java枚举EventStatus,而不是在代码里散落一堆字符串常量。PENDING("待处理")、PROCESSING("处理中")、RESOLVED("已解决")、REJECTED("已驳回")。状态流转写在枚举里,方法签名可读性会好很多,Swagger展示枚举含义也方便。这种细节在评审时是加分项。
4.2 办事预约与提醒机制
办事预约的流程是:村干部在后台配置可预约事项,比如“宅基地审批咨询”“低保申请材料预审”“养老保险认证”,每个事项配置可预约的日期范围、时间段和每时段可预约人数。村民在前端选择事项和时间段,填备注信息,提交预约。系统校验时间段余量,扣减余量并生成预约记录,状态为已预约。快到时间了,系统提前一天给用户发送提醒。
提醒功能用了SpringBoot自带的@Scheduled定时任务,每天早上9点扫一遍第二天的预约记录,给用户发站内信。@Scheduled用法很简单,配置类上加@EnableScheduling,定时任务方法上标注@Scheduled(cron = "0 0 9 * * ?")就行。需要注意,如果项目部署在多节点,定时任务会重复执行,需要用分布式锁或者配置只允许主节点执行。这个项目是单节点部署,所以暂时没这个问题。
4.3 乡村积分体系的设计思路
乡村积分是目前智慧乡村项目的常见亮点,本质是激励体系。村民参与环保活动、参与村务会议、上报有效事件、垃圾分类正确都能获得积分,积分可以在积分商城兑换生活用品或抵扣部分费用。
积分实现拆成两部分:积分规则表和积分流水表。规则表存放可获得的积分行为,比如上报事件加5分、事件被采纳加10分、参与会议加2分。流水表记录每次积分变动明细,包括用户ID、变动值、来源、关联业务ID、时间。每次增加积分时先查规则表拿到分值,再插入流水表,同时更新用户表的总积分字段。这三个操作放在一个事务里,保证数据一致性。页面端展示积分排行,用SQL的ORDER BY score DESC LIMIT 10查出前10名。排行榜这个功能虽然简单,但在乡村场景里能实实在在激发大家参与公共事务的热情。
5. 常见问题与排查技巧实录
这一节把实际开发中遇到、有共性的问题列出来,给后来的人提个醒。每个问题都是调试过的,照着排查能省不少时间。
5.1 前后端联调的跨域问题
前后端分离项目跨域是第一道坎。我在后端配置类里写了一个CorsConfig,允许的前端源地址是http://localhost:5173(Vite默认端口),允许方法包括GET、POST、PUT、DELETE、OPTIONS,允许Header包括Authorization、Content-Type。请求头里如果带了自定义header,要在allowedHeaders里声明,否则预检请求会失败。
如果加了CORS配置还是跨域,排查两步:一是后端allowedOrigins写没写对,二是前端有没有走代理。生产环境我其实不建议靠跨域配置,更稳妥的是用Nginx把前端静态资源和后端接口挂同一个域名下:
location /api/ { proxy_pass http://127.0.0.1:8080/api/; }这样前端请求的是同一个源,根本没有跨域问题。
5.2 文件上传成功后图片不显示的问题
这个问题本质是URL拼接错误或访问权限错误。MinIO返回的URL中endpoint地址要配置化,通过配置文件控制访问域名。另一个可能的坑是文件名重复导致覆盖,自己加UUID前缀加当前时间戳保证唯一性,实测这个方案最稳。
还有一点,很多人在本地测试时用绝对路径“D:/uploads/xxx.png”直接存数据库,前端拿到这个路径在浏览器根本打不开。正确做法是:数据库只存相对路径或者文件名,访问时由后端拼上完整访问地址返回。
5.3 播放器无法播放M3U8视频流
乡村社区偶尔会有直播或录像回放需求,后端输出M3U8流。前端播放器用的video.js,播放M3U8格式需要安装videojs-contrib-hls插件。配置不复杂:
import videojs from 'video.js' import 'videojs-contrib-hls' const player = videojs('video', { autoplay: true, controls: true, sources: [{ src: 'http://xxx/stream/index.m3u8', type: 'application/x-mpegURL' }] })测试时发现,有些视频源地址会带鉴权参数,或者后端对M3U8请求做了token校验,导致播放黑屏。我的建议是:如果只是展示录像回放,开发阶段用一个公开可访问的存储桶,能直接播放最省事;如果后续要求安全,再考虑给视频地址做临时签名。千万别一上来就上高安全方案,联调的时候会痛不欲生。
5.4 SpringBoot版本过高带来的依赖兼容问题
“SpringBoot版本太高”是很多新手反复问的问题。我最早新建项目时选了SpringBoot 3.0,结果发现很多旧版本依赖不支持Jakarta命名空间,javax全改jakarta了,MyBatis-Plus旧版本也报错。后来老老实实换回SpringBoot 2.7.16配JDK 8,依赖从Maven仓库拉下来,一点都不折腾。
经验就是:除非项目有硬性需要,比如必须跑在GraalVM或必须用SpringBoot 3的新特性,否则用2.x的最终版是最稳妥的选择。Maven依赖版本冲突也一样,先用mvn dependency:tree看依赖树,搞清楚谁把谁升级了,再决定是排除依赖还是调整版本。一定不要用IDEA里“update all dependencies”一键升级,升级一时爽,排查火葬场。
5.5 前端路由参数丢失问题
Vue Router传参有query和params两种方式。query拼在URL里,刷新参数还在;params如果不是动态路由占位,刷新就丢。前面已经说过,项目里统一用query方式传id。如果你用了params发现刷新后数据没了,先查路由配置里有没有定义动态路径段。
还有一个容易被忽视的问题是动态路由刷新后404。登录后动态addRoute的路由在刷新时会丢失,需要在路由守卫里重新拉取菜单再addRoute,然后next({...to, replace: true})。如果不加replace,可能会出现重复导航或者白屏。这个坑踩的人非常多,建议项目一上来就把动态路由的刷新处理写完整,别等上线了再补。
6. 部署上线与后续可持续扩展
平台开发完总要部署。我的部署方案是:后端打成jar包部署在服务器,服务器装JDK 8和MySQL,前端用Vite构建生成dist目录,交给Nginx托管。后端服务用nohup启动:
nohup java -jar community-platform.jar --spring.profiles.active=prod > app.log 2>&1 &第一次部署时踩了一个坑:服务器MySQL的连接时区没配置,后端日志一直报The server time zone value is unrecognized。解决方案是在jdbc连接串上加serverTimezone=Asia/Shanghai。还有,创建数据库时一定要指定utf8mb4字符集,否则中文存进去会出现乱码。这两点都是部署环节必踩的经典坑,提前配好能省很多排查时间。
如果团队有Docker环境,用Docker部署SpringBoot项目也很方便。写一个Dockerfile,基础镜像选eclipse-temurin:8-jre,把jar包COPY进去,ENTRYPOINT指定启动命令,构建镜像后run起来即可。用Docker的好处是环境隔离,本地怎么跑服务器就怎么跑,不会出现“本地好好的,服务器就是不行”的玄学问题。
6.1 服务器部署的完整流程
具体流程可以归纳为六步:第一,服务器安装JDK和MySQL,初始化数据库,导入sql脚本;第二,后端项目打包,mvn clean package -DskipTests,生成jar;第三,把jar上传到服务器指定目录,用nohup启动;第四,前端项目构建,npm run build,生成dist;第五,配置Nginx,root指向dist目录,location /api/反向代理到后端端口;第六,访问服务器IP验证页面,测试登录、上传、预约等功能。整个流程走完,确实有一种“尘埃落定”的感觉。
这里建议,部署前先检查一遍application-prod.yml里的数据库地址、MinIO endpoint、文件访问域名是否都改成了服务器地址。我在帮朋友排查部署问题时,十次有八次是配置没改全,或者数据库密码不对,真正代码问题反而少见。
6.2 面向后续迭代的经验建议
这个平台上线后可以扩展的方向我梳理过两个:第一,接入政务服务接口,让村民在平台上直接查看办事进度,把平台从村内工具升级成政务服务的延伸触点;第二,做小程序端,现在Web端虽然做了响应式适配,但真正在村里推广,小程序比浏览器更顺手。技术上后端接口完全复用,前端用uni-app套一层壳就行,工作量不大。
我个人实际操作中的体会是:做这类乡村信息化项目,不要贪多求全,先把一个核心业务闭环做到位,让村民和村干部真实用起来,再迭代下一个功能。技术永远是工具,真正解决实际问题才是平台活下去的根本。把基础打牢了,后面来什么需求都不怕。