ARTICLE DETAIL

资讯详情

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

基于Spring Boot的3D虚拟人物外观售卖系统设计与实现

基于Spring Boot的3D虚拟人物外观售卖系统设计与实现 现在虚拟形象经济越来越热游戏皮肤、VTuber模型、元宇宙场景里的数字人装扮都在往“可售卖”的方向走。基于Spring Boot的3D虚拟人物外观售卖系统正是典型的电商虚拟资产交付场景前端卖的是外观、模型或皮肤后端要解决的是商品管理、订单流转、文件存储与安全交付这一整套链路。这个题目非常适合做毕业设计、课程项目或练手实战业务不复杂但涉及面完整Spring Boot生态里的Web开发、数据库设计、缓存、文件服务、权限校验全都能串起来。我按实际做过的方案从整体设计、表结构、核心代码到踩坑记录把整个系统的实现思路完整捋一遍尽量做到拿过去就能直接参考。1. 项目背景与整体设计思路1.1 这个系统到底在解决什么问题表面上这是一个“卖3D模型”的商城但往深了想它和普通电商有本质区别。普通电商卖的是实物或标准化的虚拟商品SKU是固定的而3D虚拟人物外观本身是文件资产包含模型文件本身、贴图、骨骼绑定、材质参数甚至还有预览图、版权声明这些附属信息。所以它的核心逻辑不是“下单-发货-物流”而是“下单-支付-授权下载-资产交付”。我把需求拆成几个核心痛点模型商品信息如何结构化管理。外观不只是一张图片它可能是一个GLB文件加若干贴图还可能分不同精度版本低模、高模。购买后如何自动交付数字资产。不能靠手动发链接用户付完钱应该立刻拿到下载地址同时又要防止未购买的人直接盗链。订单状态如何严谨流转。支付回调、超时关闭、已支付待下载、售后退款每个状态都要有明确的触发条件和边界。后台如何维护这批虚拟商品。上传模型、设置价格、上下架、查看订单整套管理端也不能落下。理解了这句话系统的基本盘就定下来了用户的注册登录、模型商品的浏览与详情、购物车与下单、支付回调、模型文件下载、后台商品管理外加一个3D预览页。这就是一个典型的Spring Boot全栈项目能给评审讲清楚业务闭环。1.2 技术选型为什么这么定这类系统最常见的技术栈就是Spring Boot MySQL Redis 文件存储。我把每个部件的角色说清楚Spring Boot负责业务接口和整体调度。内置Tomcat自动配置能力强对比传统SSM省去了大量XML配置项目结构清晰也方便写单元测试。MySQL负责存储用户、商品、订单等核心业务数据。表结构是关系型的事务支持完善购买流程必须保证一致性。Redis负责缓存热点商品数据、存储验证码、实现分布式锁防并发重复下单时也要用。MinIO或云OSS如阿里云OSS负责存放模型文件。生产环境不建议把大文件直接塞服务器本地磁盘单体部署时磁盘空间和备份都是问题。Spring Security JWT负责认证和授权。管理接口和用户接口要分离权限文件下载接口也要校验身份。这里要特别解释一个选型思路为什么不用微服务很多毕设一上来就想拆注册中心、网关、多个服务但3D虚拟人物售卖系统本质上是一个单体业务用户量也没有大到需要分布式架构的程度。微服务带来的服务拆分、链路追踪、部署复杂度对于这个场景属于过度设计。我的建议是单体 合理分层把controller、service、mapper的边界理清楚等真有并发压力再拆也来得及。答辩时如果被问“能不能扩展成微服务”你能说出商品服务、订单服务、用户服务各自的划分边界比直接硬上微服务更加分。1.3 角色与功能模块划分系统分成两个角色、三个端端角色主要功能前台用户端普通用户注册登录、浏览3D模型列表、查看模型详情与在线预览、加入购物车、下单支付、下载已购模型、查看订单后台管理端管理员模型商品上传与编辑、上下架管理、订单列表与状态处理、用户管理、文件管理公共服务系统层文件上传下载、支付回调处理、验证码/Token服务前台我建议做成前后端分离Vue 3 Element Plus或者React都可以接口全部走RESTful API后台可以用同一套前端拆分路由和权限也可以直接用Spring Boot Thymeleaf做服务端渲染。如果是为了快速出成果Thymeleaf更省事如果你本身前端熟悉分离式更“现代”答辩也更有的讲。我的实际建议是核心精力放在后端接口的完整性和稳定性上前端保证能跑通主流程就够了。评审老师真正会细看的往往是订单状态流程、支付回调逻辑、文件权限控制这些有业务深度的部分。2. 核心表结构与订单状态机设计2.1 数据库设计要抠哪些细节数据库是这套系统的地基。很多新手上来就建一张商品表、一张订单表等到写代码才发现字段不够用。我梳理了一套相对完整且不过度设计的表结构user表用户基本信息包含用户名、密码密文、昵称、头像、手机号、创建时间。model_info表3D模型商品的主表包含模型名称、描述、封面图URL、价格、是否上架、模型分类、浏览量。model_asset表模型的资产文件表一个模型对应多个文件主模型、贴图、预览图、低模/高模包含文件类型、存储路径、文件哈希值、文件大小、上传时间。category表模型分类比如“写实角色”“二次元”“动物形态”等。orders表订单主表包含订单号、用户ID、总金额、状态、支付时间、支付流水号、关闭时间。order_item表订单明细表记录订单里的每个模型商品包含商品ID、单价、数量。download_log表下载记录表记录哪个用户在什么时间下载了哪个模型文件方便追溯版权问题。这里有一个我特别想提醒的点订单号和数据库自增主键是两回事。自增主键只能表示“第几行”不能暴露给外部因为可以被人遍历猜到订单数量订单号需要单独用一个字段生成格式可以设计成“日期前缀用户ID后四位随机数时间戳”这样既不容易重复又能在日志里快速定位。2.2 订单状态机别到处散写状态判断3D虚拟人物外观是数字商品交付靠文件下载而不是物流所以订单状态比普通电商要精简但边界必须严谨。我把订单状态定义成以下几种状态值状态名触发动作后续可能0待支付用户下单成功用户支付 / 超时自动关闭1已支付支付回调成功系统开通下载权限2已关闭超时未支付或用户主动取消订单结束3已完成用户下载文件或管理员标记完成订单结束4退款中用户申请退款管理员同意/拒绝实际开发中最容易翻车的是状态流转判断写乱。比如在controller里直接用if判断订单状态状态散的到处都是后面改逻辑要翻一堆代码。更推荐的状态机做法是在OrderService里维护一个safeTransition(order, targetStatus)方法用一个Map或枚举记录合法的流转路径非法流转直接抛业务异常。private static final MapInteger, SetInteger ALLOWED_TRANSITIONS new HashMap(); static { ALLOWED_TRANSITIONS.put(0, Set.of(1, 2)); // 待支付 - 已支付 / 已关闭 ALLOWED_TRANSITIONS.put(1, Set.of(3, 4)); // 已支付 - 已完成 / 退款中 } public void safeTransition(Order order, int targetStatus) { SetInteger allowed ALLOWED_TRANSITIONS.get(order.getStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new BizException(非法订单状态流转); } order.setStatus(targetStatus); }这套思路能直接避免大量奇葩bug。比如用户已支付但系统由于网络问题没收到回调时有人会手动把订单从“待支付”改成“已完成”这就跳过了支付确认属于非法流转状态机直接拦截。2.3 模型文件存储与版权保护方案3D模型文件通常不小一个GLB模型从几MB到几十MB都很常见贴图、材质、预览图算在一起存储方案不能随便处理。我的做法是文件全部走对象存储配置一个MinIO服务本地开发可以用Docker起。对象存储的好处是扩展性强、有访问控制、能生成临时签名URL比把文件写在本地磁盘更适合类电商场景。版权保护是这个项目一个值得展开的亮点。普通电商发货是寄快递数字资产发货是给下载链接但如果所有下载都用一个永久公开的URL那用户把链接分享出去就等于白嫖了。解决方案就是预签名URLpresigned URL。它的原理可以这样理解对象存储的桶设置为私有读写外部任何匿名访问都会被拒绝。但当后端通过密钥对某个文件路径生成一个带签名和过期时间的URL时用户便能在有效期内凭这个URL访问该文件。过期后链接自动失效即使被别人转发也无法使用。String url minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(model-assets) .object(filePath) .expiry(10, TimeUnit.MINUTES) .build() );我把过期时间设成10分钟。太短用户可能还没下载完就失效了太长又增加盗链风险。下载完成后写入download_log表管理员能看到谁下载过哪个模型这在售后争议时是重要凭证。3. 关键功能实操实现3.1 项目依赖与初始化配置直接用Spring Initializr生成骨架Java版本我选17Spring Boot版本选2.7.x或3.x都可以。Spark Boot 3.x搭配Jakarta EE命名空间网上教程很多还是javax的如果照抄老代码会报包名错误这一点要注意。核心依赖这样配置dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.x/version /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.x/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependencyapplication.yml的核心配置项如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/avatar_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 redis: host: localhost port: 6379 servlet: multipart: max-file-size: 100MB max-request-size: 200MB minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: model-assets这里要特别强调一下multipart配置。Spring Boot默认上传文件大小限制是1MB如果不调大上传10MB的3D模型文件会直接报MaxUploadSizeExceededException。我一开始没注意折腾了半天才发现是默认限制在拦路。3.2 模型文件上传接口与临时下载地址模型上传接口的设计思路是管理员在后台选择模型文件可能是多个文件后端逐文件上传到MinIO并把返回的路径、大小、哈希值写入model_asset表。上传接口的Controller核心逻辑PostMapping(/admin/model/upload) public ResultString uploadModel(RequestParam(file) MultipartFile file, RequestParam(modelId) Long modelId) { // 1. 生成对象存储中的唯一路径 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String objectPath models/ modelId / UUID.randomUUID() suffix; // 2. 上传到MinIO minioService.upload(objectPath, file.getInputStream(), file.getContentType()); // 3. 记录资产信息到model_asset表 modelAssetService.saveModelAsset(modelId, objectPath, file.getSize(), computeMd5(file)); return Result.success(objectPath); }路径设计里加UUID是有讲究的。如果不加直接用“models/1/model.glb”那用户只要猜到命名规则就能构造同一模型的路径去拼接签名链接虽然签名URL需要后端密钥生成无法伪造但路径可预测终归多一层风险。加上随机UUID之后文件路径本身就是不可枚举的。下载接口则要对购买状态做校验这个逻辑大家写项目时容易忽略直接放行GetMapping(/api/download/{assetId}) public ResultString getDownloadUrl(PathVariable Long assetId, RequestAttribute(userId) Long userId) { // 1. 校验用户是否购买过该模型 boolean purchased orderService.checkPurchased(userId, assetId); if (!purchased) { throw new BizException(请先购买后再下载); } // 2. 生成10分钟有效的预签名URL ModelAsset asset modelAssetService.getById(assetId); String signedUrl minioService.presignedGetUrl(asset.getFilePath(), 10); // 3. 写入下载日志 downloadLogService.record(userId, assetId, signedUrl); return Result.success(signedUrl); }这个下载接口是整套系统的安全工作重点。直接放行意味着任何拿到接口路径的人都能扫出所有模型文件下载地址这属于严重设计缺陷答辩很容易被挑出来。校验购买状态是底线再加上签名URL的时效性双层保护才算合格。3.3 下单、支付回调与并发幂等用户下单流程看起来简单但并发场景下容易产生重复订单。比如用户在购物车页面手速快连续点了两次“去支付”如果后端不做限制就会生成两笔相同内容的订单。我的处理方式提交订单的接口用Redis分布式锁以“order_create:{userId}”为锁键设置2秒过期防止同一用户的瞬时重复提交。String lockKey order_create: userId; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(2)); if (!locked) { throw new BizException(操作太频繁请稍后再试); }这只是第一层保护。还有一种情况更隐蔽锁过期了或者用户故意用脚本并发下单。这时候需要在数据库层面加约束。我的方案是订单表增加一个业务幂等字段比如request_id下单前前端生成一个请求ID后端在订单表中对该字段建唯一索引。同一请求ID插入两次第二次数据库直接报主键冲突事务回滚从根源杜绝重复下单。支付回调是另一个细节点。对接微信支付或支付宝时回调接口是支付平台主动调用你后端的可能因为网络抖动重复推送多次所以回调处理必须幂等。我的做法是先按支付流水号查订单如果订单已经是“已支付”状态直接返回成功给支付平台不再重复修改。PostMapping(/payment/notify) public String paymentNotify(RequestBody PayNotifyDTO notify) { String transactionId notify.getTransactionId(); Order order orderService.getByTransactionId(transactionId); if (order null) { return fail; } // 幂等已支付的订单直接确认 if (order.getStatus() 1) { return success; } // 校验签名和金额 if (!payService.verifySignature(notify)) { return fail; } // 状态机安全流转并给用户开通下载权限 orderService.safeTransition(order, 1); return success; }这里的验签步骤不能省。如果直接相信回调参数里的金额和订单号攻击者可以伪造一个回调通知让你的订单号匹配、金额匹配然后系统就给未付款的用户开了下载权限相当于白嫖。验签一般是在服务端用支付平台公钥对回调签名做校验平台文档都写得比较清楚照做即可。3.4 3D模型在线预览打通前后端售卖3D外观的系统商品详情页不能只有一张封面图用户看不到模型长什么样很难下单所以在线预览是很重要的一环。我采用的是前端Three.js加载GLB模型的方式后端只需要提供一个获取模型资源的URL列表。前端核心思路详情页展示model-info里的元数据预览区域初始化Three.js场景加载GLB文件模型文件通过后端预签名URL获取不直接暴露静态资源路径预览时如果模型太大可以先加载低模版本高模在用户购买后再提供下载。这里有一个坑Three.js直接请求后端返回的预签名URL时如果后端和前端域名不一致会触发跨域问题。而MinIO返回的预签名URL默认响应头里没有CORS相关配置浏览器就会拦截。解决方法是进入MinIO控制台给对应的存储桶配置CORS规则。实际上我在开发时配置的MinIO CORS规则大致是这样{ AllowedOrigins: [*], AllowedMethods: [GET, PUT], AllowedHeaders: [*], ExposeHeaders: [ETag] }注意AllowedOrigins虽然配了*但生产环境应该收紧到前端自己的域名防止其他网站直接引用你的模型下载链接。如果不想让前端直接处理Three.js加载还有一个更省事的方案用model-viewer这个Web组件。它封装了模型加载、光照、相机控制等一大堆细节一行HTML就能在详情页嵌入3D预览model-viewer src签名URL alt3D模型预览 auto-rotate camera-controls/model-viewer这里要提醒model-viewer存在浏览器兼容性问题老版浏览器不支持WebGL,需要在项目里做好降级提示。4. 常见问题与排查技巧实录4.1 上传大文件报错现象管理员上传一个大十几MB的GLB模型后端返回错误日志里出现MaxUploadSizeExceededException。原因就是前面提到的Spring Boot默认1MB上传限制没有去改配置。当时排查时我还绕了一圈先看了Nginx配置又看了MinIO配置最后才发现根本没有传到MinIO在Spring MVC入口就被拦了。正确做法配置文件上传大小后重启应用。如果前面还挂了Nginx还需要同时把Nginx的client_max_body_size调大否则文件会在Nginx层直接被拒绝。这个坑属于环境联动问题本地开发可能没问题但部署到服务器就暴露了。4.2 并发下单出现重复订单现象压测时同一用户快速下单订单表出现两条内容一模一样的记录。排查思路发现接口里没有幂等控制。我第一步加了Redis分布式锁用用户维度锁住了提交操作瞬时重复请求被拦截但极端情况锁过期或并发来自不同设备仍然可能漏掉。最后在订单表加了request_id唯一索引从数据库层面兜底。这里有个经验分布式锁和唯一索引不是二选一而是双保险。锁能拦住绝大多数正常场景的误操作唯一索引能拦住极端并发和恶意请求。答辩时你能主动说出这一层设计绝对加分。4.3 模型预览跨域失败现象前端页面加载模型时控制台报跨域错误模型始终旋转不出来。排查思路首先确认前端请求的是预签名URL且URL指向的是MinIO域名然后再检查MinIO桶的CORS配置。我当时把MinIO的CORS漏配了导致Three.js的loader请求被浏览器拦截。配置好之后还有一个细节如果MinIO通过Nginx反向代理暴露那Nginx也要把Access-Control-Allow-Origin相关头配好我在这上面又折腾了半小时。4.4 Spring Security把接口全拦截了现象登录接口、上传接口、预览接口全部返回401。原因Spring Security默认拦截一切请求如果你的SecurityConfig没有配置放行规则那系统根本没法访问。排查思路不是把所有接口都放行而是按职责区分路径权限设置/api/auth/**登录注册放行/admin/model/**后台上传需要管理员角色/api/model/list、/api/model/detail放行游客也能看/api/download/**下载模型需要登录静态资源、预览资源放行制作一个快速排查表方便大家回头对照异常现象可能原因排查手段文件上传超过限制Spring MVC/网关/容器上传大小限制检查multipart配置、Nginx client_max_body_size重复订单缺少幂等控制加Redis锁并在订单表建request_id唯一索引支付回调重复处理回调幂等没做好根据支付流水号判断是否已处理直接返回成功预签名URL跨域加载不了MinIO桶或Nginx缺少CORS头配置桶的CORS规则检查反向代理头接口全部401Security放行规则遗漏按路径分角色配置filter chain放行规则模型文件链接被分享后仍可访问下载URL是永久URL改用预签名URL设置短时过期时间5. 项目扩展方向与答辩加分思路这套系统做到能跑通前台下单、后台管理、文件下载毕设的框架已经完整。但如果还想往深做有几个方向非常契合当前热点第一个方向是模型渲染与展示优化。现在很多3D外观售卖平台支持用户查看不同光照、换背景、旋转角度的效果本质是把Three.js的展示能力发挥到极致。后端可以增加预览图自动生成服务比如部署一个Blender Python脚本定时任务渲染商品的预览图这样商品列表页展示的封面图就能自动更新。第二个方向是零售后的“外观试穿”场景。如果3D模型是角色类外观用户可以上传自己的头像照片或身体参数系统通过简单的参数化变形来模拟穿戴效果。这个听起来复杂但用现有的人体参数化模型做基础可行性已经不低而且很能唬住答辩评委。第三个方向是订单维度的数据分析和推荐。基于购买记录算用户偏好做个简单的协同过滤推荐给详情页加一个“猜你喜欢”列表。数据量不大算法实现也不复杂但能让系统从纯交易工具升级成带运营属性的平台。我个人在实际操作中的体会是这类题目最怕的是把“商城”做成“CRUD堆砌”。3D模型售卖和普通商品卖货最大的不同在于文件交付、版权保护、模型展示这三个环节都是数字资产特有链路。你把这三个环节讲透系统设计的目的性和深度立刻就不一样了。如果让我给一个最直接的扩展建议那就是把支付回调的安全性打磨到极致——数字商品本身就是钱模型文件就是货货在被下载的瞬间安全保障没做好商品再好看都白搭。这部分值得你在答辩前专门花时间设计一个边界场景考验自己不发消息直接把接口地址发出去看看能不能用假回调获得下载权限。这一关过了你的系统才算真正扛得住检查。
返回列表