ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的旧衣物捐赠系统开发实战:从数据库设计到前后端联调

基于SpringBoot+Vue的旧衣物捐赠系统开发实战:从数据库设计到前后端联调 旧衣物捐赠系统这类选题在最近的项目咨询里出现的频率是真的高。它表面看着就是个普普通通的信息管理系统但真到了动手写的阶段很多人才发现——涉及角色权限、图片上传、状态流转、前后端联调这些环节随便一个地方卡住整个项目就推进不下去。我最近刚好完整做完了一个基于 SpringBoot Vue 的旧衣物捐赠系统前端用了 Vue 全家桶后端走的是 SpringBoot MyBatis也就是标题里那个 ssm 容易让人困惑的组合。这篇就纯粹以我的实际开发经历为线索把这个系统从数据库设计到接口开发再到前端页面联调的完整过程拆开讲清楚希望能给正在做类似选题的朋友省下一些弯路。这套系统适合谁参考一是准备拿它当毕业设计或课程设计的同学二是刚接触前后端分离开发、想找一个完整业务练手的初级开发者。我会尽量把每一步为什么这么做也讲清楚而不是只给结论。代码示例、SQL 建表、踩坑记录都会有你照着思路改改就能用到自己的项目里。1. 项目到底做什么技术选型拆解与整体业务设计1.1 ssm和springboot到底是什么关系先解决一个最容易让人懵的问题题目里既有 ssm 又有 springboot到底该用哪一套老一批的 Java Web 项目ssm 指的是 Spring SpringMVC MyBatis 这三件套。Spring 管对象SpringMVC 管请求路由MyBatis 管数据库操作。SpringBoot 出现之后SpringMVC 的功能已经被 SpringBoot 的 web 模块直接整合了Spring 更是 SpringBoot 的地基所以实际开发中只要用 SpringBoot MyBatis 就等价于把原来的 ssm 能力全拿到手了而且配置还少了一大堆。我建议的做法是项目骨架直接用 SpringBoot持久层用 MyBatis前端用 Vue。这样既满足 ssm 的业务实质——Spring 容器 类 MVC 的分层结构 MyBatis 操作数据库又符合现代 Java Web 开发的主流习惯配置量也小得多。真用传统 SSM 的 XML 配置去写一个捐赠系统光是 springmvc.xml、applicationContext.xml 这些配置文件就够折腾一天完全没必要。1.2 角色权限与核心业务闭环旧衣物捐赠系统表面上是发布衣物、申请领取这么简单但把业务捋清楚之后至少要有三个角色参与捐赠者登录后发布旧衣物、查看自己发布的记录、处理他人的申请。受助者也可以叫申请人浏览在架衣物、提交捐赠申请、查看申请状态。管理员审核用户、审核衣物上下架、管理公告、查看所有捐赠记录。从用户发布一件旧衣服到它真正被送到需要的人手里中间要经过一条完整的状态链衣物上架 - 受助者发起申请 - 捐赠者确认 - 管理员审核 - 线下交接 - 完成捐赠。我最初设计时忽略了一个问题衣物状态和申请状态必须分开管理。衣物本身有审核中/在架/已下架的状态申请记录有待确认/已通过/已完成/已取消的状态。如果把这两个状态合并成单个字段后面业务逻辑根本没法写。这一点在数据库设计章节会详细展开。1.3 技术栈明细为什么前端选 Vue整个项目的技术栈相对主流没有为了炫技引入冷门框架后端SpringBoot 2.x MyBatis MySQL 5.7Java 8前端Vue 2 Element UI Axios Vue Router开发工具IDEA Navicat Postman环境Node 14 以上Maven 3.6为什么前端选 Vue 而不是其他框架核心原因有两个。第一Vue 的学习曲线比很多框架平缓对于要写管理后台和几个业务页面的项目来说非常合适第二Element UI 组件库几乎把后台管理需要的表格、表单、弹窗、上传组件全部封装好了不夸张地说页面开发效率至少提高一倍。而且 Vue 2 的生态非常成熟遇到问题几乎都能搜到解决方案非常适合项目开发周期紧张的场景。2. 数据库设计先把表结构想清楚后面代码能少改一半2.1 用户表设计用一个字段区分三种角色用户表是系统最基本的一张表设计得是否合理直接影响后续所有功能的开发。我的建议是不要建三张用户表管理员表、捐赠者表、受助者表一张用户表加一个角色字段就够了。这是因为三种角色本质上都属于系统用户很多字段是重叠的拆成三张表反而是给自己挖坑。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名/登录账号, password varchar(100) NOT NULL COMMENT 密码(MD5加密), nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 角色: 0管理员 1捐赠者 2受助者, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态: 1正常 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;这里特别提醒两个细节。密码字段长度建议设 100因为如果是 MD5 加盐或者其他加密方式32 位固定长度还行但如果后面要换成 BCrypt60 位是跑不掉的字段不够长就得改表。角色字段用 tinyint 而不是字符串是为了查询效率但代码里一定要写好枚举常量不然时间一长你自己都忘了 1 和 2 代表什么。2.2 衣物信息表图片存储不要用 base64衣物信息表是整个系统的核心业务表。设计的时候要考虑哪些字段衣物的名称、描述、分类上衣/裤子/鞋帽/其他、成色、图片、发布者、状态、发布时间还有一个容易被忽略的领取条件或备注字段。CREATE TABLE clothes ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 衣物名称, category varchar(20) DEFAULT NULL COMMENT 分类: 上衣/裤装/鞋靴/配饰/其他, condition_level varchar(20) DEFAULT NULL COMMENT 成色: 全新/九成新/七成新/有瑕疵, description text COMMENT 详细描述, image varchar(255) DEFAULT NULL COMMENT 图片路径, publisher_id int(11) NOT NULL COMMENT 发布者ID, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态: 0待审核 1在架 2已下架 3已捐出, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_publisher (publisher_id), KEY idx_status (status) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;有一个我踩过的坑必须讲图片字段千万不要用 base64 字符串往数据库里塞。当时我图省事前端直接把图片转成 base64 传到后端一张照片差不多能有几 MB 的文本数据库一下子就膨胀了页面加载也是肉眼可见的卡。正确的做法是把图片文件传到服务器的某个目录数据库里只存访问路径。后面在接口开发章节我会给出具体的文件上传方案。2.3 捐赠申请表状态机设计是业务核心捐赠申请表记录了谁申请了哪件衣物以及这笔申请现在的处理进度。这里的状态设计我建议至少要有四个待确认、已通过、已拒绝、已完成线下交接后由管理员或捐赠者确认完成。CREATE TABLE donation_apply ( id int(11) NOT NULL AUTO_INCREMENT, clothes_id int(11) NOT NULL COMMENT 衣物ID, applicant_id int(11) NOT NULL COMMENT 申请人ID, apply_reason varchar(500) DEFAULT NULL COMMENT 申请说明, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态: 0待确认 1已通过 2已拒绝 3已完成, apply_time datetime DEFAULT NULL, handover_time datetime DEFAULT NULL COMMENT 交接完成时间, PRIMARY KEY (id), KEY idx_clothes (clothes_id), KEY idx_applicant (applicant_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;这里还有一个业务上的关键点一件衣物被申请之后是不是要立刻下架让其他人不能再申请我的处理方式是不用立刻下架但同一件衣物在同一时刻只允许有一条待确认或已通过的申请记录。这个约束在代码里判断逻辑是查询这件衣物是否已有 status 为 0 或 1 的申请如果有就不允许再次申请。不然就会出现一件衣服被好几个人同时申请通过线下交接的时候打架的尴尬情况。2.4 辅助表公告与轮播图除了业务主表一个好的信息管理系统最好还有公告管理功能。公告表结构很简单标题、内容、发布时间、发布人。如果还有首页轮播图需求再加一张 banner 表字段就是图片路径、跳转链接、排序号、状态。这些辅助表不复杂但功能上有画龙点睛的作用也让系统的完整度更高。有需要就拿去用CREATE TABLE notice ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, content text, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3. 后端接口开发实录SpringBoot 的落地细节3.1 项目初始化和关键依赖配置用 IDEA 新建一个 SpringBoot 项目选择 Java 8勾选 Web、MyBatis、MySQL 驱动这样一个基础工程就出来了。如果你的 IDEA 创建项目时网速不给力可以直接去 Spring Initializr 网站下载压缩包再导入效果一样。pom.xml 里的关键依赖要特别注意版本匹配。SpringBoot 2.7.x 对应 MyBatis Starter 2.x对应 MySQL 连接器 8.0.x。我最初用 SpringBoot 3.x 搭的时候发现 MyBatis 官方 Starter 还没完全适配折腾了很久。所以稳妥起见直接上 SpringBoot 2.7.xparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency /dependencies3.2 application.yml 配置端口、数据库、上传路径一次配齐配置文件建议直接上 yml比 properties 可读性好很多。除了常规的数据源配置还有两个东西必须提前配好MyBatis 的 mapper 扫描路径、文件上传的路径。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/donation?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.donation.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置强烈建议开启。数据库字段 create_time对应 Java 实体类属性 createTime开启驼峰映射后 MyBatis 会自动转换不用在 XML 里写一堆 resultMap能少写很多代码。3.3 用户登录与 Token 验证用户登录是第一个要写的核心接口。我的方案是登录成功之后生成一个 Token 返回给前端前端存到 localStorage后续每次请求在请求头里带上这个 Token。后端用一个拦截器统一校验 Token。Token 的生成不需要引入特别复杂的框架用 UUID 加上用户 ID 和时间戳拼一个字符串再用 MD5 处理一下就够了毕竟这只是个课设/毕设级别的系统。如果你要追求更规范的做法可以用 JWT但对这个项目来说有点大材小用。PostMapping(/login) public Result login(RequestBody User loginUser) { User user userService.login(loginUser.getUsername(), loginUser.getPassword()); if (user null) { return Result.error(用户名或密码错误); } // 生成tokenmd5(uuid userId timestamp) String token Md5Util.md5(UUID.randomUUID() user.getId() System.currentTimeMillis()); // 存入数据库或Redis这里直接存一张token表简单可靠 tokenService.saveToken(user.getId(), token); MapString, Object data new HashMap(); data.put(token, token); data.put(userInfo, user); return Result.success(data); }这里我要分享一下为什么不用 JWTJWT 最大的问题是一旦签发在有效期内无法在服务端主动作废。比如管理员想封禁某个用户如果用的是 JWT除非把密钥换掉不然这个用户拿着旧 Token 还是能继续访问接口。自己维护一个 Token 表想做登录失效、强制下线都很容易实现。3.4 衣物发布与多条件查询接口衣物发布接口涉及文件上传和表单数据提交两部分。我推荐的做法是分成两个接口一个负责上传图片返回访问路径一个负责提交衣物表单数据。这样前端可以先把图片传完拿到路径再随表单一起提交逻辑清晰也方便图片上传失败时单独重试。衣物列表查询要支持多条件筛选按分类、按成色、按状态、按关键词搜索。MyBatis 的动态 SQL 在这里派上用场select idfindClothesList resultTypecom.donation.entity.Clothes SELECT * FROM clothes where if testcategory ! null and category ! AND category #{category} /if if testconditionLevel ! null and conditionLevel ! AND condition_level #{conditionLevel} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select使用where标签有个好处当所有条件都为空时它不会生成 WHERE 关键字SQL 不会出错。用CONCAT(%, #{keyword}, %)而不是直接在 Java 里拼好再传入是为了防止 SQL 注入。这些都是 MyBatis 开发中非常基础的但也很关键的细节。3.5 文件上传接口与静态资源映射文件上传这块我前面提过不要存 base64。后端的处理逻辑是接收 MultipartFile按日期分目录存储生成唯一文件名然后把可访问的 URL 返回给前端。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } // 生成存储目录: upload/2024/05/20/ String datePath new SimpleDateFormat(yyyy/MM/dd).format(new Date()); String uploadDir D:/donation-upload/ datePath; File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } // 生成唯一文件名 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(uploadDir / fileName)); String url /upload/ datePath / fileName; return Result.success(url); }这里有个关键问题上传的文件存在磁盘目录 D:/donation-upload/ 下浏览器访问 http://localhost:8080/upload/2024/05/20/xxx.jpg 能不能直接看到答案是不能因为 SpringBoot 默认只处理 classpath 下的静态资源。必须做一个静态资源映射把 /upload/** 的访问请求映射到磁盘目录Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/donation-upload/); } }这个映射配置是我在实战中见过被问得最多的问题之一。有人图片传上去了数据库路径也存对了就是页面上图片裂了十有八九是这个配置没写。4. 前端 Vue 实现从零搭建到接口联调4.1 初始化 Vue 项目与安装依赖前端我用 Vue 2 Element UI。创建项目用官方脚手架npm install -g vue/cli vue create donation-web创建的时候选择默认的 Vue 2 预设就行。进入项目目录后安装 Element UI、Axios、Vue Routernpm install element-ui npm install axios npm install vue-router3这里有个版本坑必须提醒Vue Router 4.x 是给 Vue 3 用的Vue 2 项目必须安装 vue-router3否则安装完了一启动就报错。Element UI 也一样Element Plus 是 Vue 3 的Vue 2 只能用 Element UI。我第一次搭的时候没注意版本装成了 Element Plus结果按钮都渲染不出来排查半天才发现是版本号的问题。在 main.js 里注册import Vue from vue import App from ./App.vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import router from ./router Vue.use(ElementUI) Vue.config.productionTip false new Vue({ router, render: h h(App) }).$mount(#app)4.2 Axios 封装与请求拦截器所有接口请求都需要携带 Token如果每次请求都要手动加请求头那就太痛苦了。正确的做法是封装一个统一的 Axios 实例用拦截器统一处理。import axios from axios import { Message } from element-ui import router from ../router 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[token] token } return config }) // 响应拦截器统一处理业务错误和登录失效 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { Message.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request统一封装的好处非常明显以后后端如果调整了接口返回格式或者要统一处理某种错误码只需要改拦截器一个地方所有页面都生效。这也是前后端分离项目的基本功。4.3 路由配置与登录守卫路由配置要区分哪些页面需要登录才能访问哪些是公开的。用 Vue Router 的导航守卫实现const routes [ { path: /login, component: Login }, { path: /register, component: Register }, { path: /, component: Layout, redirect: /home, children: [ { path: home, component: Home, meta: { title: 首页 } }, { path: clothes, component: ClothesList, meta: { title: 衣物广场 } }, { path: my/release, component: MyRelease, meta: { title: 我的发布, requiresAuth: true } }, { path: my/apply, component: MyApply, meta: { title: 我的申请, requiresAuth: true } }, { path: admin/user, component: AdminUser, meta: { title: 用户管理, requiresAuth: true, adminOnly: true } }, { path: admin/clothes, component: AdminClothes, meta: { title: 衣物审核, requiresAuth: true, adminOnly: true } } ] } ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.adminOnly) { // 校验用户角色 const role localStorage.getItem(role) if (role ! 0) { Message.error(无权访问该页面) next(/home) } else { next() } } else { next() } })adminOnly 这个思路是我后期补上的。最初我天真地以为后端接口校验了权限就够了后来发现前端页面照样能通过地址栏直接访问。虽然后端拦截器会挡住非法请求但前端做个权限控制用户在界面上体验会好很多不会看到一堆报错页面。4.4 衣物发布表单与图片上传组件前端发布衣物页面用 Element UI 的 el-form 和 el-upload 组件。el-upload 的关键配置是 action 属性指向后台上传接口headers 带上 Tokenon-success 回调里把返回的图片路径存到表单字段中el-form :modelform label-width80px el-form-item label衣物名称 el-input v-modelform.title placeholder请输入衣物名称 / /el-form-item el-form-item label分类 el-select v-modelform.category el-option label上衣 value上衣 / el-option label裤装 value裤装 / el-option label鞋靴 value鞋靴 / el-option label配饰 value配饰 / el-option label其他 value其他 / /el-select /el-form-item el-form-item label成色 el-radio-group v-modelform.conditionLevel el-radio label全新全新/el-radio el-radio label九成新九成新/el-radio el-radio label七成新七成新/el-radio el-radio label有瑕疵有瑕疵/el-radio /el-radio-group /el-form-item el-form-item label图片 el-upload actionhttp://localhost:8080/upload :headers{ token: getToken() } :on-successhandleUploadSuccess list-typepicture-card :limit1 i classel-icon-plus/i /el-upload /el-form-item el-form-item label描述 el-input typetextarea v-modelform.description rows5 / /el-form-item el-form-item el-button typeprimary clicksubmitForm发布/el-button /el-form-item /el-form图片上传成功后的回调处理handleUploadSuccess(response) { if (response.code 200) { this.form.image response.data this.$message.success(图片上传成功) } else { this.$message.error(图片上传失败) } }4.5 前后端联调中的跨域问题前后端分离开发必然遇到跨域问题。前端地址是 http://localhost:8081 后端接口是 http://localhost:8080 浏览器会拦截跨域请求。解决跨域最简单的方式是后端起一个全局 CORS 配置类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); } }但我建议实际开发时用更简单的方式前端在 vue.config.js 里配置 devServer 的 proxy把 /api 开头的请求都代理到后端地址。这样浏览器看到的请求还是同源的不存在跨域问题也就不用后端做额外配置。如果用代理方案注意 Axios 的 baseURL 要改成 /api后端接口地址保持不变。5. 常见问题与排查技巧实录5.1 前端请求一直报 404 或 405这个问题的排查思路先在浏览器 F12 里看 Network 面板确认请求 URL 和后端实际接口是否一致。最常见的原因是后端 Controller 的 RequestMapping 路径和前端 Axios 请求路径不一致或者 GET/POST 方法类型不匹配。注意检查路径时不要忽略 context-path。如果后端设置了server.servlet.context-path: /api那所有请求前缀都要带上。我见过有人在 application.yml 里加了 context-path前端却忘了加结果浪费半天时间排查。5.2 图片上传成功但页面显示裂图这个我前面已经埋了伏笔。核心排查步骤是三步第一确认上传接口返回的 URL 是什么第二在浏览器直接访问这个 URL看能不能打开第三如果打不开检查 SpringBoot 的静态资源映射是否配置正确。另外注意图片跨域防盗链的问题如果是前后端不同端口前端页面里 img 的 src 是 http://localhost:8080/upload/xxx.jpg这个属于跨域图片加载一般不会被拦截但如果后端加了图片鉴权拦截器就可能出问题。5.3 LocalDateTime 传到前端变成一串 T 格式的字符这个问题特别常见数据库字段是 datetimeJava 实体类是 LocalDateTimeJackson 默认序列化的格式是 2024-05-20T10:30:00而前端想要的是 2024-05-20 10:30:00。解决办法是在 application.yml 里配置全局时间格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT85.4 Maven 依赖冲突导致的启动失败典型场景是 spring-boot-starter 自带的日志框架和其他依赖自带的日志框架冲突。启动时报 SLF4J 相关的异常或者莫名的 ClassNotFoundException。排查方式是 mvn dependency:tree 查看依赖树找到冲突的依赖排除掉。用 IDEA 的话直接在 pom.xml 里右键 - Diagragrams - Show Dependencies 就能看到依赖冲突情况。另外 SpringBoot 版本太高也是个隐患。有些教程推荐直接用最新的 3.x但 3.x 要求 Java 17 起步很多老项目的代码和第三方库都不兼容。如果你只是做这个捐赠系统不要追求新版本稳定压倒一切。5.5 用户只能看自己的数据数据权限的 SQL 写法很多新手会忽略数据权限问题。比如我的发布页面查询结果不应该把别人的衣物也查出来。最容易实现的方案是在 SQL 里加 publisher_id 条件但如果每个查询都手动加容易漏。我的做法是 Service 层统一封装一个当前用户工具类在需要数据过滤的地方调用保证当前登录用户 ID 一定被拼进查询条件。public class UserContext { private static ThreadLocalInteger currentUser new ThreadLocal(); public static void set(Integer userId) { currentUser.set(userId); } public static Integer get() { return currentUser.get(); } public static void clear() { currentUser.remove(); } }登录拦截器里在请求进入 Controller 之前解析 Token拿到用户 ID 放入 UserContext请求结束后在 finally 里 clear。这样一个请求链路内任何 Service 层代码都能拿到当前登录用户 ID。但要注意Async 线程和定时任务里 ThreadLocal 会失效这个项目用不到知道即可。写在最后这个旧衣物捐赠系统我从数据库建表到前后端联调完成大概用了两周左右的业余时间。整体上不算难但小坑确实不少。我的体会是做这类系统最容易出问题的不是业务功能本身而是前后端衔接的那些细枝末节——跨域、Token 传递、文件路径、时间格式、状态同步。如果一开始就把技术方案定好基础设施搭扎实后面写业务代码反而很快。最后再分享一个小技巧联调时不要闷头写代码先拿 Postman 把所有后端接口全部自测通过再开始写前端页面。接口通了前端出问题就一定是前端的事排查范围缩小一大半这个习惯能帮你省掉大量无意义的联调时间。
返回列表