ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue的钱币收藏交流系统设计与实现

基于SpringBoot+Vue的钱币收藏交流系统设计与实现 1. 钱币收藏这个圈子系统真正要管好的三件事做这个项目之前我先花了几天时间泡在各种钱币论坛、钱币贴吧和线下交流会里看藏友到底在聊什么、争什么、担心什么。看完之后最大的感受是钱币收藏这个领域和普通二手交易、普通兴趣社区都不太一样它有一套非常特殊的话语体系。你随手拍一枚银元挂上去说卖价格私聊那基本没人理你。藏友关心的东西极其具体这枚币是什么版别比如袁大头的三年签字版和普通版价格能差几十倍、什么品相原光、包浆、弱打、清洗过没有、有没有评级PCGS还是NGC几分、边齿对不对、是不是同模伤。这些信息如果不结构化系统就是做一个花架子毫无实际价值。所以我把这个系统的核心价值拆成了三件事。1.1 藏品信息不是普通商品版别、品相、评级这些字段怎么落库普通电商系统里一个商品就是标题价格库存描述图片。但钱币不行。一枚币的价值判断高度依赖几个关键维度年代与朝代清、民国、新中国直接决定大的价格区间。材质银、铜、镍、纸币棉浆纸不同材质保存难度和鉴别逻辑完全不同。面值与版别同一年代的币版别差异是收藏研究的核心乐趣也是最容易产生价格分歧的地方。品相等级这是钱币圈最敏感的词。同样一枚币UNC未流通和XF极美的价格能差一倍以上。评级信息现在越来越多藏友认盒子评级封装评级机构和分数直接影响交易信心。参考价格区间这个字段不能做成固定值因为行情波动非常快系统只能给参考区间不能当真。基于这些分析我在藏品表里格外留了这几个字段版别描述、品相等级、评级机构、评级分数、发行年份、材质类型。发布藏品时这些字段做成必填项前端校验宁可让用户多填两步也不能让一条残缺的藏品信息流到列表页——因为那会破坏整个社区的信任感。1.2 交流场景的需求收敛从聊天到信息发布互动我最初的想法是做即时聊天IM后来砍掉了。原因有两个第一即时通讯对Web端网站来说技术成本高需要WebSocket长连接维护、在线状态管理、消息推送对一个交流系统的初期版本来说太重了第二钱币圈的交易和交流习惯是发帖公示大家更愿意在公开版块里鉴别真伪、讨论行情而不是私下一对一聊——公开反而能积累信任。所以我最后把交流拆成了四个模块藏品展示与分享帖藏友晒自己的藏品大家品评。求购与出售信息区类似信息撮合买方发布求购需求卖方发布出售帖。鉴别讨论区发图请人看真假、看版别这个需求极其高频。站内通知有人回复、有人点赞、有人回复了你的回复通过通知中心触达。这四个模块避开了即时通讯的复杂度又完整覆盖了收藏圈最核心的信息流转场景。等技术沉淀够了以后再加IM也不迟。1.3 权限与信任基础的搭建收藏圈有个特点新人和老手之间的信息差极大骗局也多。系统不能只做一个工具还要承载起角色带来的信任划分。我给系统设计了三层角色普通用户、版主/管理员、系统超级管理员。普通用户能发帖、能评论、能发布藏品版主能管理自己版块内的帖子置顶、加精、删帖甚至临时禁言超级管理员管用户、管全站内容、看统计数据。另外还做了一个比较重要的设计交易信息必须绑定藏品种类品相参考价格才能发布否则审核不通过。这就是在源头逼着发布者提供有效信息。事实证明这个设计很管用水帖数量大幅下降。2. 为什么这个题目用SpringBootVue是稳妥选择这个题目的技术选型其实没有太多悬念SpringBootVue在目前的Java全栈开发里就是一套非常成熟、社区资料极其丰富的组合特别适合快速搭建一个带管理后台的信息管理类系统。2.1 后端选型SpringBoot到底省了什么事如果回头去看SSMSpringMVCSpringMyBatis时代写一个Web项目最痛苦的其实是配置web.xml、spring-mvc.xml、spring-mybatis.xml、数据源配置、事务管理配置每个文件都要手写一堆bean定义跑起来还经常因为版本冲突报一堆稀奇古怪的错。SpringBoot的核心价值就是把这一坨配置全部约定化了。你需要什么功能引入对应的starter框架自动装配。举个例子我项目里要操作MySQL数据库用MyBatis-Plus做ORMdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency引入这个starter之后基础的CRUD方法就都内置了不需要自己写简单的单表Mapper方法。我只需要做一件事自定义Mapper接口继承BaseMapperT。public interface CollectionItemMapper extends BaseMapperCollectionItem { }然后Service里直接IPageCollectionItem page new Page(current, size); LambdaQueryWrapperCollectionItem wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(era), CollectionItem::getEra, era) .eq(StringUtils.hasText(material), CollectionItem::getMaterial, material) .orderByDesc(CollectionItem::getCreateTime); collectionItemMapper.selectPage(page, wrapper);这种开发效率是SSM时代没法比的。更别说SpringBoot自带内嵌Tomcat一个java -jar就能启动服务开发调试和部署都变得非常轻。2.2 前端选型Vue在快速迭代里的优势前端这边选Vue理由也很直接组件化开发模式对系统类项目太友好了。钱币交流系统的前端页面看起来多拆开来看其实就是几个核心组件在不同路由下的排列组合藏品卡片组件展示图片、品相、价格、帖子列表组件、评论组件、个人信息面板组件。Vue的单文件组件模式template script style让这些模块可以独立开发互不干扰后期维护时定位问题也快。状态管理方面我用的是PiniaVue 3生态。这里提一句如果你项目用的是Vue 2那就用Vuex如果新开项目用Vue 3Pinia更轻量写起来也更舒服。我的登录态保存逻辑很简单// stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null }), actions: { setLoginInfo(token, userInfo) { this.token token this.userInfo userInfo localStorage.setItem(token, token) }, logout() { this.token this.userInfo null localStorage.removeItem(token) } } })2.3 前后端分离的开发和调试流程前后端分离之后开发流程是这样的前端用Vite起一个开发服务默认端口5173后端SpringBoot跑在8080通过Vite的proxy配置把/api开头的请求转发到后端完美绕过开发环境的跨域问题。// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })后端Controller统一以/api开头写接口路径这样生产环境部署时Nginx只需要把/api的请求转发到SpringBoot端口之前端静态资源交给Nginx直接托管路径不用改任何代码。3. 数据库设计与核心接口把藏品和交易约束在代码边界内这个系统的数据模型是整个项目的重中之重。我第一次设计表结构时吃了不少亏反复改了三版最后沉淀出来一套相对稳定的方案直接分享最终版本。3.1 核心表结构设计我按照业务域把表分成了四组用户域、藏品域、内容域、互动域。用户域就是一张用户主表加一张角色表这个比较常规不展开。藏品域是整个系统的特色我单独设计了一张collection_item表字段名类型说明idbigint主键user_idbigint藏品所属用户titlevarchar(100)藏品标题dynastyvarchar(20)时代/朝代materialvarchar(20)材质银/铜/镍/纸等denominationvarchar(20)面值version_descvarchar(255)版别描述grade_levelvarchar(20)品相等级UNC/XF/VF等grading_orgvarchar(20)评级机构grading_scorevarchar(20)评级分数reference_pricedecimal(10,2)参考价格区间低位reference_price_highdecimal(10,2)参考价格区间高位descriptiontext详细描述cover_imagevarchar(255)封面图URLstatustinyint状态0草稿 1上架 2下架 3已售create_timedatetime创建时间update_timedatetime更新时间这些字段就是前面说的钱币话语体系的落库表达。这里特别提醒一件事版本号和版别千万不要混到一个字段里。我第一版图省事把版别描述和品相塞进描述栏结果列表页没法做筛选只能文本搜索体验很不好。后来拆开之后前端可以按朝代、材质、品相等级做条件筛选检索效率明显提升。内容域和互动域的表下面的社区运营部分再详细讲。3.2 发布藏品的事务与状态流转藏品发布这个接口看似简单但有几个细节必须处理到位不然用户在页面点了上架按钮心里是没底的。第一发布藏品时要处理封面图。用户是先传图拿到图片URL再提交表单这是一个异步过程。前端的逻辑是图片上传接口独立于表单提交接口图片传完返回URLURL随表单一起提交。这样后端接口就保持简单——只接收文本字段加图片URL不处理文件流。第二存reference_price时前端要传两个值low和high后端要做一次数据校验低价不能大于高价。这个校验后端一定要做不能只靠前端因为接口可以被绕过。我用了一个简单的自定义校验if (referencePrice ! null referencePriceHigh ! null referencePrice.compareTo(referencePriceHigh) 0) { throw new BusinessException(参考价格区间设置不合法); }第三状态机要提前定义清楚。藏品从发布到出售状态的流转是草稿-上架-已售/下架。管理员和版主没有权限直接改用户藏品状态只能通过举报处理触发强制下架。这样设计是为了保护用户对自己藏品数据的控制权。3.3 图片上传方案本地存储更适合毕设和中小项目图片上传是这类系统绕不开的模块。我对接过的方案有三种本地路径存储、FastDFS分布式存储、OSS云存储。如果是做毕业设计或课程项目我强烈建议用本地存储因为不依赖外部环境演示稳定。生产环境再考虑OSS。本地存储的核心逻辑就是让静态资源暴露到一个可访问的URL上。我当时的做法是在SpringBoot的静态资源配置里把本地的/uploads目录映射为/images/**这个URL路径用户上传的图片落到这个目录数据库存储/images/xxxx.jpg这种相对路径前端的图片URL由nginx或直接由SpringBoot暴露。这样做的最大好处是开发环境不需要外网本地跑起来就能看到图片效果。Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /uploads/; registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadPath); } }图片压缩这一块如果上传的是一些老藏品的扫描大图动辄几MB前端不做压缩体验会很难受。我在前端做了个小处理上传前用Canvas把图片宽度限制在1600像素以内质量压缩到0.8再上传几MB的图能压到300KB左右既保证细节清晰又不会拖慢页面。4. 交流系统的活水社区互动模块的细节实现藏品展示和发布功能做完之后系统只能算是一个数字收藏夹离交流系统还差得很远。真正让这个系统活起来的是社区互动模块。4.1 评论、点赞、收藏的数据模型与实现社区互动最核心的四张表帖子表post、评论表comment、点赞表like_record、收藏表favorite_record。表名关键字段说明postid, user_id, title, content, view_count, like_count, comment_count, status, top_flag帖子主表commentid, post_id, user_id, parent_id, content, create_time评论表parent_id支持楼中楼回复like_recordid, post_id, user_id, create_time点赞明细表favorite_recordid, post_id, user_id, create_time收藏明细表评论表用parent_id做楼中楼是个性价比很高的方案。一级评论的parent_id为0对某条评论的回复记录被回复评论的id。前端展示时按一级评论回复列表的结构分组渲染。点赞和收藏为什么要单独建明细表因为要做是否已点判断。每次渲染帖子列表时如果已经登录需要查当前用户对哪些帖子点过赞、收过藏然后高亮图标。这种情况下用冗余计数字段like_count、favorite_count外加明细表是最经典的做法防止每次统计都做count聚合查询又保留了用户粒度数据。4.2 热度排序一个简单又有效的小公式社区如果只是按时间排序老帖子永远沉底新帖子缺乏互动基础内容生态会很快僵化。参考一些成熟社区的做法我给帖子列表加了一个热门排序选项。热度值的计算方式非常简单核心就是给互动行为赋予不同权重并加入时间衰减public Double calcHotScore(Post post) { double score post.getViewCount() * 1 post.getCommentCount() * 5 post.getLikeCount() * 3 post.getFavoriteCount() * 4; // 时间衰减每过24小时热度衰减为原来的50% long hours ChronoUnit.HOURS.between(post.getCreateTime(), LocalDateTime.now()); double decay Math.pow(0.5, hours / 24.0); return score * decay; }这个逻辑没有另起一张表因为帖子数量不大时直接查询后内存计算完全扛得住。每页20条帖子每次请求最多算20个热度值性能开销可以忽略。等数据量真的起来了再把这个值固化到帖子表的hot_score字段用定时任务离线更新。4.3 站内通知为什么说不做就没人活跃通知模块的触发逻辑依托于用户在社区里的行为别人回复了你的帖子别人点赞了你的藏品别人关注了你的动态。我设计了一张通知表字段名说明id主键user_id接收通知的用户type通知类型1回复 2点赞 3收藏 4系统通知content通知内容摘要target_id跳转目标IDis_read是否已读create_time创建时间在前端布局里用户头像旁边显示一个未读消息的红色角标点击进去就是通知列表。每次进入页面时拉取未读数量已读状态通过点击全部已读接口批量更新。这个模块对用户的活跃度提升非常明显——有人回复你之后你收到通知再回访社区粘性就建立起来了。5. 开发过程中踩过的坑和最终采用的实践这部分我特意放在后面写因为踩坑的经验往往比功能列表更有参考价值。这几个坑我每一个都真实遇到过有的调试了大半天排查过程让人记忆深刻。5.1 MyBatis-Plus分页插件不配置就不会生效这是第一个坑。我按网上的教程引入了MyBatis-Plus依赖直接调用selectPage方法结果发现分页根本不管用查出来的是全量数据。排查了好久才发现MyBatis-Plus的分页功能需要显式配置PaginationInnerInterceptor否则分页SQL不会自动拼接limit语句。网上很多博客已经写了但版本更新后配置类写法略有差异。我用的最终版本是Configuration MapperScan(com.example.coin.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个坑值得单独说出来是因为它属于代码看起来没问题但就是跑不对的典型情况。5.2 前端登录态与路由守卫白屏问题的根源第二个坑出现在路由守卫。我期望的效果是未登录用户访问发布藏品个人中心等页面时自动跳转到登录页。最初我在router.beforeEach里做了判断从Pinia里读取token不存在就跳转登录。但上线前调试时发现一个诡异的问题用户刷新页面时虽然token存在localStorage里但Pinia的store是全新初始化的还没来得及从localStorage恢复token路由守卫就已经执行了结果把已登录用户又踢回了登录页。解决办法是在store初始化时同步读取localStorageexport const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }), ... })这个看起来细小的改动其实解决的是前端状态初始化时序这样一个经典问题。5.3 富文本内容的XSS防御帖子内容我规划的是允许用户写一些排版内容所以引入了富文本编辑器但这就带来了XSS注入风险。一个恶意用户如果能在帖子内容里插入一段脚本其他用户打开帖子时脚本就会执行这个危害是系统级的。后端处理XSS的思路是在JSON反序列化阶段做过滤。我实现了一个XssFilter继承HttpServletRequestWrapper重写getParameter和getInputStream把请求参数和body内容中的危险字符做HTML实体转义。Override public String getParameter(String name) { String value super.getParameter(name); return cleanXss(value); } private String cleanXss(String value) { if (value null || .equals(value)) { return value; } value value.replaceAll(, lt;).replaceAll(, gt;); value value.replaceAll(javascript\\s*:, ); value value.replaceAll(onerror\\s*, ); return value; }富文本场景下转义要更精细一些不能把用户帖子里的正常HTML标签全干掉否则排版内容全乱了。比较实用的做法是使用白名单过滤只允许p, img, h1, h2, a, blockquote这类安全标签存在其他标签和所有事件属性一律剔除。如果项目引了Spring Security也可以组合使用HtmlUtils.htmlEscape来做兜底。5.4 部署时的跨域和静态资源路径问题开发环境用Vite代理生产环境就不一样了。最终部署时我把前端打包好的dist目录交给了Nginx托管Nginx里同时配了两条location规则一条是/静态资源一条是把/api反向代理到Java服务。server { listen 80; server_name your.domain.com; root /var/www/coin-front/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; 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 / { try_files $uri $uri/ /index.html; } }这里有个非常关键的细节Vue Router如果开了history模式刷新页面时Nginx会去磁盘上找对应的静态路径找不到就404。try_files $uri $uri/ /index.html这条配置就是为了处理这种情况——所有匹配不到文件路径的请求都回退到index.html由前端路由接管。6. 冗余字段与查询性能以及上线前的注意事项系统功能能跑起来之后我开始关注一些偏工程化的细节。这些点虽然不影响功能完整性但直接影响用户感受和数据安全性。6.1 列表页的查询会越写越慢索引要提前规划社区类系统最常被访问的页面就是列表页。时间久了数据量上来之后不带条件的SELECT * FROM post ORDER BY create_time DESC一定会慢。我在核心查询字段上建了组合索引并在常用查询上做了explain分析。比如帖子列表的查询条件是status1 AND (title LIKE %关键词%)那索引就建(status, create_time)这个联合索引排序字段和过滤条件尽量走一个索引避免Using filesort。图片URL字段不要加索引长文本不要加索引这些是最基本的常识。另外LIKE %关键词%这种左模糊是肯定不走索引的如果搜索结果量很大建议引入Elasticsearch做全文检索而不是硬扣数据库。对于这个项目的体量左模糊搜索够用没必要上ES。6.2 登录安全与密码存储密码不能明文存这是底线。我用的加密方式是BCryptSpring Security里自带的BCryptPasswordEncoder它的好处是每次生成的盐值不同相同密码存出来的hash值也不一样有效抵抗彩虹表攻击。public String encodePassword(String rawPassword) { return new BCryptPasswordEncoder().encode(rawPassword); } public boolean matchPassword(String rawPassword, String encodedPassword) { return new BCryptPasswordEncoder().matches(rawPassword, encodedPassword); }登录成功后签发JWT token设置合理的过期时间前端把token存在localStorage里每个请求通过Axios拦截器自动加到Authorization请求头。后端通过拦截器校验token有效性并把用户信息塞到ThreadLocal里方便后续逻辑直接取当前用户。6.3 数据库初始化数据我做完所有表结构之后特意写了一个SQL脚本往库里插入了一些示例藏品和示例帖子。这个动作看着不起眼但对项目演示和测试意义很大第一能让评委或用户打开页面时立刻看到内容不用自己费劲发布第二能测试列表页在有一定数据量的情况下是否仍然渲染流畅。示例数据的编写要尽量真实比如民国三年的袁大头、1980年的长城币、第一套人民币的牧马图等这些藏品的版别和品相描述我都从真实收藏帖里参考过保证看起来是那么回事。7. 这个系统做完之后我复盘出的几点经验整个项目从需求分析到最终部署上线前前后后大概花了五周时间。这里最后聊几点复盘之后的真实感受。做了这个项目之后我更深刻地体会到技术本身不复杂SpringBoot和Vue都是工具真正的工作量在理解业务上。如果不理解钱币收藏圈的品相文化、不知道版别和评级的重要性做出来的系统就一定是个空壳。这和学习任何一个技术栈的逻辑是一样的——先理解要解决的真实问题再去选择合适的工具。第二个经验是前后端联调阶段一定要提前约定好接口文档。我用的是Swagger自动生成接口文档后端启动后访问/swagger-ui/index.html就能看到所有接口的定义前端开发照着文档调接口大大减少了互相问你的接口字段叫什么的沟通成本。第三件值得一提的事情是别怕功能做得少。我做这个系统时主动砍掉了很多看似高级的功能没有做即时通讯没有做拍卖竞价没有做复杂的推荐算法。核心功能做深做透才是一个系统能站得住的原因。最后如果你也想做一个类似的项目我的建议很直接一开始不要铺太开找一个小而真实的场景比如这里的钱币版别与品相交流把它做到流程闭环比十个半成品功能更打动人。后续想扩展的话可以往藏品拍卖、在线鉴定委托、行情趋势图表这些方向走技术和数据基础都已经铺好了。
返回列表