简介:在当今数字化浪潮中,构建一个安全、可扩展的在线交易平台是后端开发的核心挑战之一。其底层原理依赖于一套严谨的数据模型与事务处理机制,以确保数据的一致性与业务的原子性。这项技术的核心价值在于,它能够为数字资产(如NFT)的铸造、确权与流转提供可靠的技术支撑,广泛应用于数字藏品、知识产权交易等新兴场景。本文聚焦于如何利用Spring Boot框架与MySQL数据库,设计并实现一个具备完整交易流程的数字藏品商城原型。文中将详细解析如何通过数据表结构(如用户表、藏品表、交易记录表)定义数字藏品的唯一性与所有权,并深入探讨在高并发场景下,如何运用数据库悲观锁(SELECT ... FOR UPDATE)与事务管理来保障交易的安全与一致性,避免超卖等典型问题。
1. 项目缘起:从“数字藏品”热潮到技术落地
最近几年,数字藏品这个概念火得一塌糊涂,从艺术圈到科技圈,再到各种品牌营销,几乎无处不在。作为一个后端开发,我身边不少朋友和客户都来问,这东西到底是怎么做出来的?能不能自己搭一个?问得多了,我就琢磨着,与其一遍遍解释,不如动手搞一个能跑起来的、麻雀虽小五脏俱全的“数字藏品商城”原型系统。一来可以梳理清楚这里面的技术门道,二来也能给想入行的朋友一个实实在在的参考。
这个项目,我选择用Spring Boot作为后端框架,前端则用最朴素的HTML、CSS 和 JavaScript 来实现。你可能会问,现在不都流行前后端分离用 Vue 或 React 吗?为啥还用 HTML?我的考虑很简单:聚焦核心业务逻辑。对于一个原型系统或者一个需要快速验证想法的项目,前端用纯 HTML 能让我们把绝大部分精力放在后端业务逻辑、藏品流转、交易安全这些更核心、更复杂的问题上,避免被前端框架的配置和生态分散注意力。而且,纯 HTML 页面足够轻量,部署简单,对于理解整个系统的数据流和交互逻辑反而更直观。
所以,这篇内容,我会带你从零开始,一步步拆解如何用 Spring Boot 和 HTML 搭建一个具备基础功能的数字藏品商城。我会重点讲清楚几个核心问题:数字藏品到底是什么数据模型?如何确保它的唯一性和所有权?交易流程怎么设计才安全?以及,那些看似简单的 HTML 页面,如何与 Spring Boot 后端优雅地交互,完成从浏览、购买到查看个人资产的全过程。这不是一个简单的“Hello World”教程,而是包含了我在设计过程中遇到的真实坑点、技术选型的思考,以及如何让代码结构更清晰、更易于扩展的实战经验。
2. 核心概念与数据模型设计:数字藏品的“身份证”
在动手写代码之前,我们必须先搞清楚我们要处理的核心“实体”是什么。数字藏品,虽然名字里带“数字”,但它和我们平时说的“一张图片”、“一个视频”文件有本质区别。它的核心价值在于可验证的稀缺性、唯一性和所有权。因此,我们的数据模型设计必须围绕这些特性展开。
2.1 理解数字藏品的核心属性
一个数字藏品(通常对应一个 NFT,Non-Fungible Token)至少包含以下几个关键属性:
- 唯一标识符 (Token ID):这是藏品在链上或我们系统内的唯一身份证号。即使两个藏品看起来一模一样(比如同一系列的 001 号和 002 号),它们的 Token ID 也必须是不同的。
- 元数据 (Metadata):描述藏品具体信息的数据,比如藏品的名称、创作者、描述、创建日期、所属系列等。这部分数据通常以 JSON 格式存储。
- 媒体文件地址 (Asset URI):指向藏品实际内容(图片、音频、3D模型等)的链接。这里有一个重要实践:媒体文件本身通常不存储在区块链上(因为成本极高),而是存储在去中心化存储(如 IPFS)或中心化云存储中,但存储后的内容哈希(Hash)会记录在元数据或链上,用于验证文件是否被篡改。
- 所有权信息 (Owner):当前拥有该藏品的用户地址(在区块链项目中是钱包地址,在我们这个中心化演示系统中,可以是对应用户的 ID)。
- 智能合约地址 (Contract Address, 可选):在真正的区块链应用中,这会指向部署该藏品系列的智能合约。在我们的模拟系统中,可以用一个“系列ID”来替代,标识藏品属于哪个发行项目。
在我们的 Spring Boot 项目中,这些属性需要被映射为数据库中的表和字段。
2.2 数据库表结构设计
我选择 MySQL 作为关系型数据库,因为它足够成熟,生态完善,对于这种包含复杂关系和事务的业务非常合适。以下是核心表的设计思路:
1. 用户表 (user)这是系统的基础。除了常规的账号、密码(加密存储)、邮箱、昵称,我还增加了两个字段来模拟区块链钱包的概念:
wallet_address:一个虚拟的“钱包地址”,可以用 UUID 生成,代表用户在系统内的唯一资产账户。balance:用户余额,用于平台内交易(比如用积分或模拟的法币购买藏品)。
2. 藏品系列表 (collection_series)数字藏品通常是按系列发行的。这个表记录系列信息。
CREATE TABLE `collection_series` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `series_name` varchar(255) NOT NULL COMMENT '系列名称', `creator_id` bigint NOT NULL COMMENT '创建者用户ID', `description` text COMMENT '系列描述', `cover_image` varchar(500) COMMENT '系列封面图', `total_supply` int NOT NULL DEFAULT 0 COMMENT '计划发行总量', `minted_count` int NOT NULL DEFAULT 0 COMMENT '已铸造数量', `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-准备中,1-发售中,2-已售罄,3-已结束', `created_at` datetime DEFAULT CURRENT_TIMESTAMP );这里total_supply和minted_count是关键,用于控制系列的稀缺性。status字段管理系列的生命周期。
3. 数字藏品表 (digital_collection)这是最核心的表,对应每一个独一无二的藏品。
CREATE TABLE `digital_collection` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `token_id` varchar(100) NOT NULL UNIQUE COMMENT '藏品唯一Token ID,可自定义规则生成', `series_id` bigint NOT NULL COMMENT '所属系列ID', `metadata_uri` varchar(500) NOT NULL COMMENT '元数据JSON文件存储地址', `asset_uri` varchar(500) NOT NULL COMMENT '藏品媒体文件(图片等)地址', `owner_id` bigint NOT NULL COMMENT '当前所有者用户ID', `creator_id` bigint NOT NULL COMMENT '铸造者/创作者用户ID', `price` decimal(18,2) DEFAULT NULL COMMENT '当前挂单价格,NULL表示非卖品', `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1-正常,2-交易中(锁定),3-已转移', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_owner` (`owner_id`), KEY `idx_series` (`series_id`) );设计要点与踩坑经验:
token_id的生成:不能简单用数据库自增id,因为自增ID有规律且可能暴露发行量信息。我采用的方案是:系列ID + 时间戳 + 随机数,然后取 MD5 或 SHA-256 的前几位,确保唯一且无规律。例如:SERIES_001_20231027123045_5A3F。metadata_uri和asset_uri:在原型阶段,我们可以先把元数据 JSON 和图片文件放在服务器的某个目录下,uri存储相对路径,如/metadata/1.json和/assets/1.png。生产环境务必考虑使用对象存储(如阿里云OSS、AWS S3)并开启 CDN,同时记录文件哈希。status字段的重要性:在用户发起购买、藏品转移过程中,必须通过status字段(例如设置为2-交易中)结合数据库事务来锁定该藏品,防止同一藏品被同时卖给两个人,这是实现“原子性”交易的关键,后面交易流程会详细讲。- 索引优化:
owner_id和series_id是高频查询字段,必须加索引。根据业务,如果经常按价格排序,可能还需要对price加索引。
4. 交易记录表 (transaction_record)所有藏品所有权的变更都必须有迹可循,这是数字藏品“链上”特性的核心体现。即使我们这是中心化系统,也要模拟出不可篡改的交易流水。
CREATE TABLE `transaction_record` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `transaction_hash` varchar(255) NOT NULL UNIQUE COMMENT '交易哈希,模拟区块链交易ID', `collection_id` bigint NOT NULL COMMENT '藏品ID', `from_user_id` bigint COMMENT '转出方用户ID(首次铸造时为NULL)', `to_user_id` bigint NOT NULL COMMENT '接收方用户ID', `transaction_type` tinyint NOT NULL COMMENT '交易类型:1-铸造,2-购买,3-转账', `amount` decimal(18,2) DEFAULT NULL COMMENT '交易金额', `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0- pending,1- confirmed,2- failed', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, KEY `idx_collection` (`collection_id`), KEY `idx_user` (`to_user_id`, `from_user_id`) );设计要点:
transaction_hash:模拟区块链的 TxHash。可以用UUID + 时间戳再哈希一次生成,确保全局唯一。每次所有权的真实变更,都必须先插入一条交易记录并标记为pending,在后续业务逻辑(如支付确认)完成后,再更新为confirmed,并同步更新digital_collection表的owner_id。这是一个典型的“事件溯源”思想的简化应用。- 记录所有状态:即使是失败(
failed)的交易也要记录,便于对账和排查问题。
通过这样的数据模型设计,我们为数字藏品系统搭建了坚实、清晰且可扩展的数据基础。接下来,我们就要让 Spring Boot 来驱动这些数据模型,实现业务逻辑。
3. Spring Boot后端架构与核心业务实现
有了清晰的数据模型,后端的工作就是围绕这些模型构建 RESTful API,并实现核心的业务流程。我采用经典的分层架构:Controller(控制层) -> Service(业务逻辑层) -> Mapper/Repository(数据访问层)。这里我重点讲几个核心服务的实现逻辑和避坑点。
3.1 项目结构与基础配置
使用 Spring Initializr 快速生成项目,依赖主要包含:Spring Web,Spring Data JPA(或 MyBatis-Plus, 我更喜欢后者,更灵活),MySQL Driver,Lombok。
一个关键的配置项是事务管理。数字藏品的交易涉及多个表的更新(用户余额、藏品状态、交易记录),必须保证原子性。在 Service 层的方法上,务必使用@Transactional(rollbackFor = Exception.class)注解。
# application.yml 部分配置 spring: datasource: url: jdbc:mysql://localhost:3306/nft_market?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword hikari: maximum-pool-size: 10 minimum-idle: 5 jpa: # 如果使用JPA hibernate: ddl-auto: update show-sql: true servlet: multipart: max-file-size: 10MB # 上传藏品图片大小限制 max-request-size: 20MB3.2 藏品铸造服务:从零到一的诞生
“铸造”是数字藏品诞生的过程。在我们的系统里,就是管理员或创作者创建一个新的、独一无二的藏品记录。
核心接口:POST /api/collection/mint
Service层逻辑 (CollectionService.mintCollection):
- 参数校验:验证传入的系列ID是否存在且处于可铸造状态,验证创作者身份。
- 生成唯一标识:调用工具类,根据系列ID等信息生成全局唯一的
token_id。 - 处理文件:接收上传的藏品媒体文件(如图片),保存到指定目录(或上传到OSS),生成
asset_uri。同时,根据藏品信息(名称、描述、图片哈希等)生成一个 JSON 格式的元数据文件,同样保存并生成metadata_uri。踩坑提示:文件存储路径不要用中文,且最好按日期(如
yyyy/MM/dd)或用户ID进行分目录存储,避免单个目录文件过多。存储后,应立即计算文件的 MD5 或 SHA-256 哈希值,并将这个哈希值写入元数据 JSON 中。这是未来验证藏品真伪的关键。 - 数据库操作(在一个事务内): a. 向
digital_collection表插入新记录,owner_id和creator_id设为创作者ID,status为1-正常。 b. 向transaction_record表插入一条类型为1-铸造的记录,from_user_id为null,to_user_id为创作者ID,状态为1-confirmed。 c. 更新对应collection_series表的minted_count(已铸造数量),并检查是否已达到total_supply,如果达到则更新系列状态为2-已售罄。 - 返回结果:返回包含新藏品
token_id、metadata_uri等信息的 DTO 对象。
这里的一个技术细节是元数据 JSON 的结构,它应该遵循一定的社区规范(如 OpenSea 的元数据标准),以增加兼容性。
// metadata/1.json { "name": "数字熊猫 #001", "description": "这是首个数字熊猫系列的首个藏品。", "image": "https://your-oss-domain.com/assets/2023/10/27/panda_001.png", "image_hash": "0xabc123...", // 图片文件的哈希值 "attributes": [ {"trait_type": "背景", "value": "星空"}, {"trait_type": "稀有度", "value": "传说"} ], "creator": "张三", "series": "数字熊猫系列" }3.3 藏品购买/交易服务:确保安全与一致性
这是商城系统的核心,也是最容易出并发问题的地方。流程设计必须保证:同一藏品在同一时间只能被一个成功购买,且钱货两清。
核心接口:POST /api/transaction/buy
请求参数:collectionId(藏品ID),userId(购买者ID)。
Service层逻辑 (TransactionService.buyCollection),这是一个需要高并发考虑的典型场景:
- 初步校验:检查购买者余额是否充足,检查藏品是否存在、是否属于可出售状态(
status = 1且price不为 null)。 - 悲观锁或乐观锁:这是防止超卖的关键。
- 方案A(悲观锁,推荐用于此场景):在查询藏品信息时,使用
SELECT ... FOR UPDATE锁定这条记录。这样,在本次事务提交前,其他任何事务都无法修改或锁定这条藏品记录。MyBatis-Plus 可以通过在 Mapper 方法上添加@Select("... FOR UPDATE")实现。 - 方案B(乐观锁):为
digital_collection表增加一个version字段。更新时带上版本号条件UPDATE ... SET ..., version=version+1 WHERE id=#{id} AND version=#{oldVersion}。如果更新影响行数为0,说明版本冲突,购买失败。这种方式在高并发下失败率可能较高,用户体验不如悲观锁直接返回“已售出”。我的选择:对于数字藏品这种绝对稀缺、唯一性的商品,我倾向于使用悲观锁。虽然会降低一点并发度,但保证了强一致性,业务逻辑更清晰简单。
- 方案A(悲观锁,推荐用于此场景):在查询藏品信息时,使用
- 在事务内执行核心操作: a.锁定藏品:使用
FOR UPDATE查询出藏品信息,此时该行记录已被锁定。 b.创建交易记录:向transaction_record插入一条类型为2-购买的记录,状态为0-pending。 c.扣减买家余额:UPDATE user SET balance = balance - #{price} WHERE id = #{buyerId} AND balance >= #{price}。这里WHERE条件再次校验余额,防止并发情况下余额不足。 d.增加卖家余额:UPDATE user SET balance = balance + #{price} WHERE id = #{sellerId}。 e.转移藏品所有权并锁定状态:UPDATE digital_collection SET owner_id = #{buyerId}, status = 2 WHERE id = #{collectionId}。注意,此时状态变为2-交易中,这是一个中间状态,防止在后续步骤失败时藏品处于不确定状态。 f.更新交易记录状态:将刚才插入的交易记录状态更新为1-confirmed。 - 事务提交:如果以上所有步骤成功,Spring 的事务管理器会提交事务,释放锁。此时,买家余额减少,卖家余额增加,藏品所有权完成变更。
- 后置操作(可异步):事务提交后,可以发送消息通知(如站内信、邮件)给买卖双方。也可以异步地将最终的藏品所有权变更事件(藏品ID,新老Owner)记录到另一个“事件表”或发送到消息队列,供其他系统(如数据分析、风控)消费。
- 异常处理:如果任何一步失败,事务会回滚,数据库状态恢复到操作前。需要给前端返回明确的错误信息,如“余额不足”、“藏品已售出”、“系统繁忙”等。
这个流程的严谨性在于,通过数据库事务和行锁,将“检查库存(藏品状态)”、“扣款”、“转移物权”这几个操作绑定成了一个不可分割的原子操作,是金融级交易系统的基本设计思路。
3.4 用户资产与交易历史查询
这两个功能是面向用户的核心查询,性能优化很重要。
1. 查询用户拥有的藏品 (GET /api/user/{userId}/collections): 直接关联digital_collection和collection_series表查询即可。注意分页,避免一次拉取过多数据。如果藏品图片很多,前端可以采用懒加载。
2. 查询用户的交易历史 (GET /api/user/{userId}/transactions): 查询transaction_record表,关联digital_collection和user表(获取对方用户信息)。这里数据量可能增长很快,需要做好分页。可以根据transaction_type进行过滤。
性能优化点:
- 为
transaction_record表的created_at字段建立索引,因为历史查询通常按时间倒序。 - 在 Service 层使用 MyBatis-Plus 的
Page对象进行物理分页,而不是内存分页。 - 对于复杂的联表查询,要关注 SQL 执行计划,避免全表扫描。
后端 API 实现后,我们就需要一个简单但功能完整的前端界面来与用户交互了。
4. 前端HTML页面与后端交互实战
前端我们采用最基础的 HTML + CSS + JavaScript,配合Fetch API或Axios与后端通信。目的是演示如何在不依赖复杂前端框架的情况下,完成一个动态数据应用。我会重点讲几个核心页面的逻辑。
4.1 项目首页:藏品列表展示
首页 (index.html) 主要展示正在出售的藏品列表。关键点在于如何从后端获取数据并动态渲染到页面上。
HTML 结构骨架:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>数字藏品商城</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <header>...</header> <main> <div class="collection-list" id="collectionList"> <!-- 藏品卡片将通过JS动态插入到这里 --> </div> <div id="pagination"></div> <!-- 分页控件 --> </main> <script src="/js/api.js"></script> <script src="/js/index.js"></script> </body> </html>JavaScript 逻辑 (/js/index.js):
// 定义当前页码和每页大小 let currentPage = 1; const pageSize = 12; // 页面加载完成后,获取并渲染藏品列表 document.addEventListener('DOMContentLoaded', function() { loadCollections(currentPage); }); // 加载藏品列表的函数 async function loadCollections(page) { try { // 调用封装好的API函数 const response = await api.getCollections(page, pageSize); if (response.code === 200) { renderCollectionList(response.data.records); renderPagination(response.data.total, page); } else { alert('加载藏品列表失败:' + response.message); } } catch (error) { console.error('请求出错:', error); alert('网络请求失败,请检查网络连接。'); } } // 渲染藏品列表到页面 function renderCollectionList(collections) { const container = document.getElementById('collectionList'); container.innerHTML = ''; // 清空现有内容 if (!collections || collections.length === 0) { container.innerHTML = '<p class="empty-tip">暂无在售藏品。</p>'; return; } collections.forEach(item => { const card = document.createElement('div'); card.className = 'collection-card'; card.innerHTML = ` <img src="${item.assetUri}" alt="${item.name}" onerror="this.src='/images/default.png'"> <h3>${item.name}</h3> <p class="series">系列:${item.seriesName}</p> <p class="price">价格:¥${item.price}</p> <button onclick="viewDetail(${item.id})">查看详情</button> ${item.isOwner ? '<button disabled>我的藏品</button>' : `<button onclick="buyCollection(${item.id})">立即购买</button>`} `; container.appendChild(card); }); }交互细节:
- 图片加载失败处理:
onerror事件设置一个默认图片,提升用户体验。 - API 封装:我将所有与后端交互的
fetch调用封装在api.js中,统一处理请求头(如添加AuthorizationToken)、基础URL和错误响应,使业务逻辑更清晰。 - 分页:
renderPagination函数会根据总条数生成上一页/下一页或数字页码按钮,并绑定点击事件重新调用loadCollections。
4.2 藏品详情与购买页
当用户点击“查看详情”时,跳转到detail.html?id=xxx。这个页面需要做两件事:展示藏品完整信息、处理购买逻辑。
详情页数据加载:
// detail.js const urlParams = new URLSearchParams(window.location.search); const collectionId = urlParams.get('id'); async function loadDetail() { if (!collectionId) { alert('无效的藏品ID'); window.history.back(); return; } const resp = await api.getCollectionDetail(collectionId); // ... 渲染详细信息到页面 document.getElementById('buyBtn').onclick = () => handleBuy(collectionId, resp.data.price); }购买处理 (handleBuy函数): 这是前端最需要谨慎处理的地方,涉及用户资金。
async function handleBuy(collectionId, price) { if (!confirm(`确定要花费 ¥${price} 购买此藏品吗?`)) { return; } const buyBtn = document.getElementById('buyBtn'); buyBtn.disabled = true; buyBtn.textContent = '处理中...'; try { const resp = await api.buyCollection(collectionId); // 调用购买API if (resp.code === 200) { alert('购买成功!'); // 跳转到用户的资产页面或订单详情页 window.location.href = `/my-collections.html`; } else { alert(`购买失败:${resp.message}`); buyBtn.disabled = false; buyBtn.textContent = '立即购买'; } } catch (error) { console.error('购买请求失败:', error); alert('网络异常,请重试。'); buyBtn.disabled = false; buyBtn.textContent = '立即购买'; } }关键点:
- 用户确认:购买前必须有明确的二次确认。
- 按钮状态管理:请求发出后,立即禁用按钮并改变文字,防止用户重复点击,造成重复提交订单。
- 错误友好提示:后端返回的错误信息(如“余额不足”、“藏品已售出”)要清晰展示给用户。
- 成功跳转:购买成功后,引导用户到相关页面,提供明确的反馈。
4.3 用户登录与状态管理
由于没有使用前端框架,我们需要一个简单的方式来管理用户登录状态(如 Token)。我采用localStorage来存储登录后后端返回的token。
登录成功后:
// 在登录API回调中 localStorage.setItem('auth_token', response.data.token); localStorage.setItem('user_info', JSON.stringify(response.data.user)); // 然后跳转到首页或用户中心API 封装层 (api.js) 统一添加 Token:
const API_BASE = 'http://localhost:8080/api'; const authToken = localStorage.getItem('auth_token'); async function request(url, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers, }; if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`; const response = await fetch(fullUrl, { ...options, headers }); // ... 统一处理响应,如检查HTTP状态码,解析JSON等 return await response.json(); } // 封装具体的API调用 export const api = { login: (username, password) => request('/user/login', { method: 'POST', body: JSON.stringify({username, password}) }), getCollections: (page, size) => request(`/collection/list?page=${page}&size=${size}`), buyCollection: (id) => request(`/transaction/buy`, { method: 'POST', body: JSON.stringify({collectionId: id}) }), // ... 其他API };页面权限控制:在需要登录的页面(如my-collections.html),可以在页面加载时检查localStorage中是否有auth_token,如果没有则重定向到登录页。
通过以上前端实践,我们完成了一个无需编译、直接由浏览器运行的动态应用。它虽然简单,但清晰地展示了前后端分离架构下数据流动的完整闭环。
5. 部署、测试与核心问题排查
系统开发完成后,部署和测试是验证其稳定性的关键环节。这里分享一些我的实操经验和常见问题。
5.1 本地运行与测试
- 数据库初始化:运行设计好的 SQL 脚本,创建表和初始化必要数据(如一个管理员用户)。
- 启动后端:在 IDE 中直接运行 Spring Boot 主类,或使用命令
mvn spring-boot:run。 - 启动前端:由于是纯静态文件,可以直接用 Nginx 托管,或者在项目里用 Spring Boot 的静态资源映射。更简单的方法是,在
src/main/resources/static目录下放置你的 HTML、CSS、JS 文件,Spring Boot 会自动提供静态资源服务。这样访问http://localhost:8080/index.html即可。 - 接口测试:使用 Postman 或 Apifox 等工具,对所有后端 API 进行测试,特别是购买接口,要模拟并发场景。
5.2 部署到服务器
对于生产环境,建议将前后端分离部署,以获得更好的性能和灵活性。
后端部署:
- 使用
mvn clean package打包生成可执行的 JAR 文件。 - 在服务器上安装 Java 运行环境。
- 使用
nohup java -jar your-app.jar --spring.profiles.active=prod > app.log 2>&1 &后台运行。更优的方案是使用 systemd 或 Docker 容器化部署。 - 生产环境配置文件:务必创建一个
application-prod.yml,配置生产数据库地址、Redis连接(如果用了缓存)、以及关闭开发工具(如spring.jpa.show-sql: false)。
前端部署:
- 将
index.html,detail.html,css/,js/,images/等静态资源文件夹,上传到 Nginx 或 Apache 的网站根目录。 - 配置 Nginx,将
/api/路径的请求反向代理到后端 Spring Boot 应用。server { listen 80; server_name your-domain.com; root /path/to/your/static/files; index index.html; location / { try_files $uri $uri/ /index.html; # 支持前端路由(如果用了的话) } location /api/ { proxy_pass http://localhost:8080; # 后端服务地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
5.3 核心问题排查与优化
在开发和测试中,我遇到了几个典型问题:
问题一:购买时出现“藏品已售出”,但日志显示库存还有。
- 排查:这是典型的并发问题。检查购买服务的数据库事务隔离级别(默认是
REPEATABLE_READ,在 MySQL 中配合FOR UPDATE是可行的)。确保在查询藏品信息时正确使用了SELECT ... FOR UPDATE进行行锁。可以通过 JMeter 或写一个简单的多线程测试程序来模拟并发购买,验证锁机制是否生效。 - 解决:在 Service 方法上添加
@Transactional注解,并在查询藏品的 Mapper 方法中明确使用FOR UPDATE。这是最可靠的方案。
问题二:上传图片后,前端页面访问不到。
- 排查:
- 检查后端文件保存的路径是否正确,文件是否成功写入。
- 检查 Spring Boot 的静态资源映射。如果文件保存在项目目录外(如
/data/upload),需要在配置中添加资源映射:@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:/data/upload/"); } } - 检查前端
img标签的src路径是否正确拼接(如应该是/upload/2023/10/27/abc.png)。
- 解决:统一文件访问路径规则,使用绝对路径或相对于网站根目录的路径。强烈建议生产环境使用云对象存储,直接返回完整的 HTTPS URL,省去本地映射的麻烦。
问题三:列表页加载速度慢,尤其是图片多的时候。
- 优化:
- 数据库层面:确保查询语句使用了索引(
owner_id,status,price等),避免SELECT *,只查询需要的字段。 - 后端层面:对藏品列表查询结果进行分页,避免一次性拉取大量数据。
- 前端层面:
- 图片懒加载:使用
loading="lazy"属性,或监听滚动事件动态加载进入视口的图片。 - 图片优化:后端在上传时生成缩略图,列表页使用小尺寸缩略图,详情页再加载原图。
- 浏览器缓存:为静态资源(图片、CSS、JS)设置合适的
Cache-Control头,利用浏览器缓存。
- 图片懒加载:使用
- 数据库层面:确保查询语句使用了索引(
问题四:交易记录表数据量巨大,查询用户历史变慢。
- 优化:
- 分库分表:如果数据量真的非常大,可以考虑按用户ID哈希或按时间(如每月)对
transaction_record表进行分表。 - 读写分离:交易记录写入频繁,但历史查询是读多写少。可以考虑使用数据库主从复制,将历史查询的请求路由到从库。
- 归档历史数据:将很早以前(如一年前)的已完成交易记录迁移到历史归档表,保持主表的数据量在一个可接受的范围。
- 分库分表:如果数据量真的非常大,可以考虑按用户ID哈希或按时间(如每月)对
通过这个从设计到部署的完整流程,我们构建了一个具备核心功能的数字藏品商城原型。它虽然简化了区块链的底层细节(如真正的智能合约和链上确认),但完整地模拟了数字藏品系统的关键业务流程和数据一致性要求,对于理解此类系统的架构和开发要点,是一个非常好的起点。你可以在此基础上,继续探索集成真正的区块链钱包、实现跨平台交易、或者加入更复杂的社区功能。
本文还有配套的精品资源,点击获取