☰
SpringBoot数字藏品商城系统:防超卖、支付回调与订单状态流转实战
2026/10/2 22:06:06 网站建设 项目流程

简介:这是一份基于SpringBoot与HTML实现的数字藏品商城系统源码,面向Java全栈学习者与数字藏品交易平台开发者,可用于理解前后端整合、商城核心业务逻辑及权限校验等关键设计。整个压缩包共61个文件,其中以42个Java后端源文件为主,覆盖数据增删改查、用户鉴权与服务端渲染,另有5个HTML前端页面、3张PNG图片资源,以及properties/yml配置、SQL初始化脚本等辅助文件,整体大小仅317KB,适合中小型项目入门研读。已有291人浏览学习,说明该代码具备一定参考价值。通过学习,使用者可以拿到一套可直接运行的完整工程结构,并围绕登录注册、商品展示、购物车、订单处理、数字资产唯一性校验等场景,扩展支付结算、版权归属验证等功能,同时可借鉴响应式页面布局与边界校验、缓存策略等工程实践,为后续二次开发或毕业设计打下基础。

1. 数字藏品商城系统:为什么是 SpringBoot + HTML 而不是前后端分离

如果你接手过数字藏品或者文创电商这类项目,大概率遇到过这种场景:几百人同时抢一个限量盲盒,数据库底层在超卖;订单状态在待支付和已铸造之间反复横跳;支付回调到了,前端页面还停在倒计时。这套基于 SpringBoot 和 HTML 构建的数字藏品商城系统设计源码,把这类项目最核心的骨架直接整理好了——藏品增发、用户下单、支付回调、订单状态流转,全部走通。它不是前后端分离的重型工程,而是后端 SpringBoot 提供接口、前端 HTML 页面直出渲染的经典架构,适合做 Java 课程设计案例源码参考,也适合想用最小成本复现一个可演示电商闭环的开发者。下面我从数据模型开始,把每个关键模块的实际落地方式拆开讲。

2. 数据模型与接口分层:先把用户、藏品、订单三张主表的关系理清

很多拿到源码的人第一件事是启动项目,然后对着页面点来点去。我习惯先打开数据库脚本,把表结构读完再碰代码。原因很简单:数字藏品商城的业务规则几乎全部落在表和状态上,表设计看不明白,后面所有接口都是黑匣子。

2.1 三张核心表的结构设计与字段选型

这套系统最核心的就是用户表、藏品表、订单表。下面是按常见设计方案还原的建表核心语句,字段名和注释都保留在实际项目中常见的写法:

CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'BCrypt加密后的密码', balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE t_collectible ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '藏品ID', name VARCHAR(100) NOT NULL COMMENT '藏品名称', image_url VARCHAR(255) COMMENT '藏品图片地址', total_supply INT NOT NULL COMMENT '发行总量', sold_count INT NOT NULL DEFAULT 0 COMMENT '已售数量', price DECIMAL(12,2) NOT NULL COMMENT '发行价格', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在售 0下架 2售罄', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字藏品表'; CREATE TABLE t_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID', order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_id BIGINT NOT NULL COMMENT '下单用户ID', collectible_id BIGINT NOT NULL COMMENT '藏品ID', amount DECIMAL(12,2) NOT NULL COMMENT '订单金额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已铸造 2已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', pay_time DATETIME DEFAULT NULL COMMENT '支付时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

三个字段选型值得展开说。金额我全部用 DECIMAL(12,2) 而不是 FLOAT 或 DOUBLE——浮点数在电商场景下做累加会出精度问题,这不是玄学,是 IEEE 754 的二进制表示决定的。订单号 order_no 建了唯一索引,这不仅是查询优化,更重要的是给支付回调和重复下单做幂等兜底。藏品表的 version 字段是乐观锁标记,解决超卖就靠它,下一章会具体写用法。status 字段用 TINYINT 存数字字典,而不是直接存中文,这样状态流转在代码里可控,前端再映射成文案。

提示:order 是 MySQL 的保留字,直接建表会报语法错误。用 t_order 或者在 SQL 里加反引号都可以,但规范做法是避开保留字。

2.2 后端分层与统一返回体:Controller 薄、Service 重、SQL 清晰

这套源码的分层是标准的 Controller → Service → Mapper 四层结构,实体类 Entity 只做字段映射,不放业务逻辑。看代码时先找统一返回体,它决定了前端 fetch 之后怎么解数据。常见的实现是这样:

public class Result<T> { private Integer code; // 0成功,非0失败 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "ok"; r.data = data; return r; } public static <T> Result<T> error(int code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }

统一返回体的作用在 HTML 页面里体现得最明显。前端不用关心 HTTP 状态码是 200 还是 500,只看 body 里的 code:code 为 0 走成功分支,非 0 统一弹 message。对比直接返回裸对象或者把异常堆栈抛给前端,这种封装让页面上的错误处理逻辑少掉一大半。

Controller 层的写法遵循一个原则:参数校验和权限判断做完,直接调 Service,不在 Controller 里写业务计算。比如藏品列表接口,Controller 只做两件事——拿到分页参数,调用 service.pageList(),然后把 Result.success(pageResult) 返回。真正的库存判断、状态过滤、金额计算全在 Service。我之前见过有人把 SQL 拼接写在 Controller 里,后面加一个字段要改三层代码,维护成本直接翻倍。

2.3 登录鉴权:JWT 令牌生成与拦截器配置

数字藏品商城不是完全开放的,登录才能下单,所以鉴权环节必须有。这套源码采用 JWT 方案,不依赖服务端 Session,适合接口被 HTML 和移动端共用的场景。核心逻辑分两块。

public class JwtUtil { private static final String SECRET = "your-256-bit-secret"; private static final long EXPIRE_MS = 2 * 60 * 60 * 1000L; // 2小时过期 public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_MS)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

生成令牌时把 userId 放在 subject 里,username 作为附加 claim;解析时只要密钥一致就能还原出登录身份。服务端不存 token,所以服务重启不会踢人下线,这是无状态鉴权的优点。

拦截器负责拦截除白名单外的所有 /api/** 请求:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", claims.getSubject()); return true; } response.setStatus(401); return false; } }

注册拦截器时要特别处理白名单:

registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/collectible/list", "/static/**", "/html/**", "/error");

注意最后的 /static/** 和 /html/**。HTML 页面是静态资源直出,如果拦截器把静态页面的请求也拦了,登录页能打开但页面里的 CSS 和 JS 全部返回 401,样式全丢。这个坑后面避坑章还会再提一次。

另外,HTML 直出渲染这种架构天然要面对存储型 XSS——藏品名称、用户签名这类字段如果直接回显到页面上,

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询