SpringBoot数字藏品商城系统:从7张表设计到转赠防并发完整解析
简介针对数字藏品交易场景这套源码基于SpringBoot与HTML构建了完整的商城系统适合Java后端学习者、电商类课程设计以及数字藏品平台开发者借鉴。项目覆盖用户注册登录、商品浏览、购物车、订单处理与支付等核心流程并结合数字藏品的唯一性校验与版权归属设计体现出前后端整合与工程化实践。包体共61个文件压缩包约317KB以42个Java后端源文件为主辅以5个HTML页面、3张PNG图片资源以及SQL脚本、配置文件、Maven打包脚本和许可证等。Java文件承载业务逻辑与数据访问HTML负责商城界面展示SQL脚本提供数据库表结构与初始化数据内置的Maven Wrapper也便于快速构建。目前已有291人学习下载源码目录层次清晰包含依赖说明与基础文档便于快速理解项目整体脉络。通过研读可掌握SpringBoot分层设计、REST接口开发、前端整合及数据库脚本应用等实战技能适合作为课程设计或商城项目二次开发的基础。1. 基于SpringBoot和HTML的数字藏品商城系统设计源码能用、能跑、能被你改的完整闭环很多人找我要一套“能直接部署”的数字藏品商城源码我的建议恰恰是回头把这套 SpringBoot HTML 的组合吃透。它没有前后端分离那么花哨却已经把用户、藏品、订单、转赠这条核心链路收进同一个工程里换数据源、改页面、调接口都动手很快。这套源码解决的是一类很具体的诉求课程设计、校内项目、或者想低成本验证“藏品上架 购买 转赠”闭环的开发者。它适合手里有台能跑 Java 的机器、想在两天内把系统跑起来并且说清楚每一行在干什么的人。HTML 页面不代表“低端”数字藏品商城的复杂度远低于社交产品真正区分水平的是鉴权设计、订单状态机和防重复铸造这些才是后面要重点讲的地方。2. 把“数字藏品”翻译成数据表7 张表与 3 条设计原则2.1 商城的核心表结构最少需要建几张表数字藏品商城和普通电商最大的区别在于卖的不是“可复制的商品”而是“有唯一编号、可追溯归属”的数字凭证。我之前做过一版只建 4 张表的商城结果一到转赠环节就发现没有记录表可查最后只能重写。这里直接给出一套能跑通“铸造 - 购买 - 转赠”闭环的表设计共 7 张表名职责关键业务字段sys_user用户与后台管理员username, password(BCrypt), rolecollectible_series藏品系列series_name, description, cover_urldigital_collectible单件数字藏品series_id, issue_no, owner_id, price, stock, statuscollectible_image藏品多图collectible_id, image_url, sort_nocollectible_order订单order_no, collectible_id, buyer_id, statustransfer_log转赠记录collectible_id, from_user_id, to_user_id, transfer_timeoperate_log操作日志user_id, action, target_type, target_id, create_time核心表是digital_collectible它的 DDL 我一般这样写CREATE TABLE digital_collectible ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, series_id BIGINT NOT NULL COMMENT 所属系列ID, name VARCHAR(100) NOT NULL COMMENT 藏品名称, description VARCHAR(500) DEFAULT COMMENT 藏品描述, issue_no VARCHAR(32) NOT NULL COMMENT 铸造编号全局唯一, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 发行价格, stock INT NOT NULL DEFAULT 0 COMMENT 当前可售库存, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0下架 1上架 2已售罄, owner_id BIGINT DEFAULT NULL COMMENT 当前持有者用户ID, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0正常 1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_issue_no (issue_no), KEY idx_series_id (series_id), KEY idx_owner_id (owner_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT数字藏品表;这里有三点要特别说明。第一stock不能省虽然数字藏品理论上可以“卖完即止”但实际商城做活动时经常要分批次补库存留一个库存字段比直接删行更灵活。第二version是乐观锁字段购买时用它来防止超卖后面第 4 章会专门写实现。第三issue_no一定要加唯一索引它是藏品的“身份证”如果铸造逻辑有 bug 导致同一编号出现两次数据库直接报错兜底而不是让脏数据流到前端。还有一张容易漏的表是transfer_log。数字藏品最核心的合规动作是“转赠”也就是用户 A 把藏品转给用户 B这个过程必须有完整审计记录。transfer_log不要只存 from 和 to还要存发起转赠时的 IP、操作时间便于后续排查异常行为。2.2 统一返回体与分页查询为什么 REST 约定必须写在前面项目里如果每个接口返回格式都不一样前端 HTML 页面解析时就会写一堆兼容代码这基本是给自己埋雷。我习惯在一开始就定义统一返回体ReturnResult所有接口都走这一个外壳public class ReturnResultT { private int code; private String message; private T data; private long timestamp; public static T ReturnResultT ok(T data) { ReturnResultT r new ReturnResult(); r.code 200; r.message success; r.data data; r.timestamp System.currentTimeMillis(); return r; } public static T ReturnResultT error(int code, String message) { ReturnResultT r new ReturnResult(); r.code code; r.message message; r.timestamp System.currentTimeMillis(); return r; } }注意code只表示业务状态HTTP 状态码保持 200这样前端页面统一在ajax回调里判断res.code逻辑简单排查问题也直观。timestamp字段虽然前端不一定用但留着在做接口耗时排查时非常有用。分页是商城列表页面的刚需项目里用的 MyBatis-Plus 自带分页插件配置如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }maxLimit设置成 500 是为了防止有人恶意传size10000把数据库打爆超过上限时直接限制为 500。setOverflow(false)表示当页码超过总页数时不返回最后一页数据而是空列表这个行为更好排查分页 bug。很多项目翻车就翻在分页插件没配maxLimit线上被人用脚本扫接口一次拉全表数据库 CPU 直接飙满。2.3 把 HTML 页面放对位置不要和 Thymeleaf 抢资源目录这个项目的页面是纯 HTML不是 Thymeleaf 模板所以静态资源目录的配置必须和模板引擎分开。Spring Boot 默认静态资源目录是classpath:/static/HTML 页面直接放这里即可访问路径就是http://localhost:8080/index.html。但如果你同时引入了spring-boot-starter-thymeleaf要特别注意Spring Boot 的视图解析器会优先从templates/目录找模板如果你把index.html放在templates/下并且请求路径不带.html后缀就会出现“明明文件存在却 404”的诡异问题。正确做法是templates/目录留空或干脆不用所有 HTML 页面统一放static/下然后在application.yml里显式声明静态资源映射spring: mvc: static-path-pattern: /static/** web: resources: static-locations: classpath:/static/这里有一个选择static-path-pattern设成/static/**还是默认的/**。如果设为/static/**访问路径就要写http://localhost:8080/static/index.html路径长且容易记错。我一般保持默认/**不做修改这样页面引入 CSS 和 JS 时不用写static前缀。上面这段配置只是展示“你可以显式控制”实际项目里不写也行。真正要避免的是把 HTML 文件又放templates/又放static/两边各有一份改了一边另一边不生效后面排查时非常痛苦。3. 工程骨架与关键配置从拿到源码到能访问首页3.1 依赖与启动类固定版本才能少踩坑拿到一套源码第一步不是看业务代码而是先确认依赖版本。Spring Boot 2.x 和 3.x 的差异非常大尤其体现在javax.*到jakarta.*的包名迁移上。如果源码是基于 2.7 写的你硬用 3.x 打开连启动类都跑不起来。我建议直接用较稳妥的 2.7.x 作为父版本核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意到我加了 Redis原因是商城登录状态和转赠防重都需要一个分布式存储。如果你的学习环境装不了 Redis可以先把spring-boot-starter-data-redis注释掉登录 token 存数据库也能跑但是后面章节要讲的防重复提交方案就得换实现。启动类没什么特别标准的SpringBootApplication加MapperScanSpringBootApplication MapperScan(com.example.digitalmall.mapper) public class DigitalMallApplication { public static void main(String[] args) { SpringApplication.run(DigitalMallApplication.class, args); } }这里有个新手容易忽略的点MapperScan的包路径必须和Mapper接口所在的包完全一致差一层都不行。如果你看到启动时报Invalid bound statement (not found)八成是这个扫描路径写错了。3.2 application.yml 配置数据源、Redis 与上传目录这个文件是项目的“黑匣子”开关很多玄学问题最后都定位到配置上。下面这份配置是经过多次踩坑后稳定下来的版本server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/digital_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root redis: host: localhost port: 6379 timeout: 3000ms mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0serverTimezoneAsia/Shanghai必须写不写的话 JDBC 驱动会默认用服务器时区导致数据库时间和本地时间差 8 小时。allowPublicKeyRetrievaltrue是 MySQL 8.0 连接时常见的坑不配这个参数在某些加密认证方式下直接报Public Key Retrieval is not allowed。mybatis-plus的逻辑删除配置直接生效后所有 SQL 都会自动带上WHERE deleted 0这个机制非常省事但前提是每张表都有deleted字段。如果你建表时漏了MyBatis-Plus 执行时会报字段不存在的错误而不是静默失败这点对排查很友好。3.3 用 Filter 做一次全局 XSS 过滤数字藏品商城后台必然有“上传藏品描述”这类富文本入口如果不过滤 XSS别人在描述里塞一段script就能偷走管理员 cookie。Spring Boot 项目里最简单的做法是写一个全局 Filter 对参数做转义。实现思路是继承HttpServletRequestWrapper重写getParameter和getHeader把、、等字符转成 HTML 实体Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; XssHttpServletRequestWrapper wrapper new XssHttpServletRequestWrapper(req); chain.doFilter(wrapper, response); } static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return clean(value); } private String clean(String value) { if (value null) { return null; } return value.replaceAll(, lt;) .replaceAll(, gt;) .replaceAll(\, quot;) .replaceAll(, #39;); } } }这个 Filter 的粒度是整个项目但实际业务里“藏品描述”可能需要保留某些合法 HTML 标签比如加粗、换行。所以更精细的做法是只对“描述类”字段做过滤或者用白名单方式允许部分标签。我给的这份是“先能用”的版本真正上线前记得把图片上传接口单独放行否则 Base64 字符串里的和被过滤后上传的图片会直接损坏。4. 数字藏品核心链路实现铸造、购买、转赠三步走完闭环4.1 铸造/上架藏品接口编号生成与库存扣减“铸造”在业务上就是管理员把一件数字藏品录入系统并上架。为了支撑正向流程铸造时必须有藏品编号、价格、库存初始持有者可以是平台自己。先看 ControllerRestController RequestMapping(/api/collectible) public class CollectibleController { Resource private CollectibleService collectibleService; PostMapping(/publish) public ReturnResultLong publish(RequestBody CollectiblePublishRequest req) { Long id collectibleService.publish(req); return ReturnResult.ok(id); } }关键是 Service 里的实现这里有两个容易翻车的点编号生成和图片存储。Service public class CollectibleServiceImpl implements CollectibleService { Resource private DigitalCollectibleMapper collectibleMapper; Override Transactional(rollbackFor Exception.class) public Long publish(CollectiblePublishRequest req) { DigitalCollectible entity new DigitalCollectible(); entity.setSeriesId(req.getSeriesId()); entity.setName(req.getName()); entity.setDescription(req.getDescription()); entity.setPrice(req.getPrice()); entity.setStock(req.getStock()); entity.setStatus(1); // 编号生成平台编码 日期 随机串 String issueNo DC LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) UUID.randomUUID().toString().replace(-, ).substring(0, 8).toUpperCase(); entity.setIssueNo(issueNo); collectibleMapper.insert(entity); return entity.getId(); } }issue_no生成方式这里用的是“日期 随机子串”没有直接用数据库自增 ID。原因是藏品的编号如果按顺序递增用户很容易猜到下一个编号比如买到的编号是 10001正常脑子都会去试 10000 存在不存在这会暴露平台的发行总量和销售节奏。用 UUID 子串虽然可读性差但至少不可预测。如果你有分布式部署的需求可以换成雪花算法但单体项目用 UUID 足够。stock字段的初始值由管理员传这里有个边界要注意stock 最小是 1如果传 0 就相当于上架一个无法购买的藏品接口校验里必须拦截if (req.getStock() null || req.getStock() 0) { return ReturnResult.error(400, 库存必须大于0); }4.2 购买订单状态机如何保证不重不漏购买流程是数字藏品商城最核心的事务涉及订单创建和库存扣减两个动作必须在一个事务里完成。我见过很多初级写法是“先插入订单再 update 库存”中间一旦抛出异常订单还在但库存已经扣了数据就错乱了。正确的做法是把扣库存放在 SQL 层面做条件更新Transactional(rollbackFor Exception.class) public Long createOrder(Long collectibleId, Long buyerId) { // 1. 查藏品信息验证状态 DigitalCollectible collectible collectibleMapper.selectById(collectibleId); if (collectible null || collectible.getStatus() ! 1) { throw new BusinessException(藏品不存在或已下架); } // 2. 条件扣库存stock 0 才允许扣减 int rows collectibleMapper.deductStock(collectibleId); if (rows 0) { throw new BusinessException(库存不足); } // 3. 创建订单初始状态为待支付 CollectibleOrder order new CollectibleOrder(); order.setOrderNo(generateOrderNo()); order.setCollectibleId(collectibleId); order.setBuyerId(buyerId); order.setStatus(0); orderMapper.insert(order); return order.getId(); }对应的 Mapper SQL 是这行关键语句UPDATE digital_collectible SET stock stock - 1, version version 1 WHERE id #{id} AND stock 0这里没有用乐观锁的version字段去比对而是直接用stock 0作为条件。在高并发下update语句会锁定匹配的行第二个事务会等待第一个事务提交后再执行此时stock已经变成 0rows 0于是第二个请求直接报“库存不足”。这一条语句就解决了超卖问题比先查再判断的写法安全得多。订单状态机如下状态值含义可流转到0待支付1 已支付 / 5 已取消1已支付2 已铸造 / 5 已取消2已铸造3 已转赠3已转赠终态5已取消终态“已铸造”这一步在真实业务里对应的是把藏品正式发放到用户账户也就是更新digital_collectible.owner_id。在这个源码项目里可以简化创建订单成功且支付确认后直接调一次“发放藏品”的逻辑Transactional(rollbackFor Exception.class) public void confirmPay(Long orderId) { CollectibleOrder order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 0) { throw new BusinessException(订单状态不允许支付确认); } // 幂等更新状态 0 - 1 int rows orderMapper.updateStatusByOrderId(orderId, 0, 1); if (rows 0) { throw new BusinessException(订单已被处理); } // 发放藏品把 owner_id 从平台换成购买用户 collectibleMapper.updateOwner(order.getCollectibleId(), order.getBuyerId()); }注意updateStatusByOrderId带了WHERE status 0这样即使支付回调重复调用也不会把订单状态从“已支付”改回“已支付”而是直接报“订单已被处理”。这就是幂等。4.3 转赠接口校验身份与基本风控转赠是数字藏品区别于普通电商的关键能力但也是最容易被滥用的接口。一个合格的最小实现至少要校验“藏品当前所有者是否就是发起人”。Transactional(rollbackFor Exception.class) public void transfer(Long collectibleId, Long fromUserId, Long toUserId) { // 1. 校验接收人存在且不是自己 if (fromUserId.equals(toUserId)) { throw new BusinessException(不能转赠给自己); } SysUser toUser userMapper.selectById(toUserId); if (toUser null) { throw new BusinessException(接收人不存在); } // 2. 校验当前持有者 DigitalCollectible collectible collectibleMapper.selectById(collectibleId); if (collectible null || !fromUserId.equals(collectible.getOwnerId())) { throw new BusinessException(你不是该藏品的持有者); } // 3. 加锁防并发转赠 String lockKey TRANSFER_LOCK: collectibleId; boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(5)); if (!locked) { throw new BusinessException(该藏品正在转赠中请勿重复操作); } try { // 4. 写转赠记录 TransferLog log new TransferLog(); log.setCollectibleId(collectibleId); log.setFromUserId(fromUserId); log.setToUserId(toUserId); transferLogMapper.insert(log); // 5. 更新持有者 collectibleMapper.updateOwner(collectibleId, toUserId); } finally { redisTemplate.delete(lockKey); } }Redis 分布式锁在这里的价值是防并发用户 A 快速点了两次转赠第一次请求还没结束第二次请求已经进来了没有锁的话两次都会读到“持有者是 A”然后各自往transfer_log插一条记录把owner_id改两次。最终数据变成“持有者是 B”但是日志里多了两条无效记录。用setIfAbsent加锁后第二个请求会直接抛“正在转赠中”的提示虽然体验一般但数据是对的。这里锁的过期时间设成 5 秒正常转赠事务秒级完成5 秒足够如果业务重到超过 5 秒说明数据库或 Redis 网络有问题这时候宁可让用户重试也不要无限等待。单体部署时可以用synchronized替代 Redis 锁但既然项目里已经集成了 Redis直接用分布式锁后面上多实例也不用改代码。5. 避坑与排查数字藏品商城从开发到上线的六个常见问题5.1 现象HTML 页面拿到空 JSON 或图片全部裂开现象首页能打开但藏品列表接口返回空数组图片位置显示破图。原因藏品图片的 URL 被存成了绝对路径比如D:/upload/xx.jpg别人访问你的页面时浏览器去读的是他自己电脑上的D盘自然读不到。解决数据库里只存相对路径比如/upload/2025/01/xx.jpg再配置一个资源映射把本地目录映射成 URLspring: web: resources: static-locations: file:/data/digital-mall/upload/,classpath:/static/这样图片路径统一以/upload/...开头换服务器只要把目录拷过去即可不用改数据库。5.2 现象用户连点两次“购买”生成两条订单且库存变负数现象前端没有做按钮防抖用户快速双击后端收到两个请求两条订单都支付成功。原因创建订单和扣库存的代码没有加条件更新典型的“先 select 再 update”写法在并发下会读到同一个库存值。解决扣库存 SQL 改成UPDATE ... WHERE id? AND stock0同时检查affectRows为 0 时直接抛异常。这个坑一旦踩到库存数据基本没法手工修复只能找 DBA 对账属于“没有后悔药”的错。5.3 现象支付回调重复通知同一订单被处理两次现象支付平台因为网络重试推送了两次回调结果订单表里出现两条“已支付”记录。原因回调处理逻辑没有幂等每次收到回调都直接把订单状态从 0 改成 1第二次回调时订单已经变成 1又把它改成 1同时重复执行了“发放藏品”逻辑。解决状态更新 SQL 强制带上当前状态条件UPDATE collectible_order SET status1 WHERE id? AND status0。如果影响行数为 0说明订单已经被处理过直接返回成功给支付平台即可。5.4 现象接口能被脚本绕过页面直接调用现象用户 B 通过浏览器控制台调用“转赠接口”把用户 A 的藏品转到自己名下。原因只校验了请求参数没有校验“登录用户身份”和“资源归属”任何人都能伪造参数调接口。解决登录时发放 token前端每次请求在 header 带Authorization后端用拦截器统一校验Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录); } // 解析 token获取 userId 放入 request attribute return true; } }然后在转赠接口里把fromUserId从 request attribute 里取而不是由前端传。这是“身份校验和越权防护”的最低要求。5.5 现象数据库时间比本地时间早 8 小时或者乱跳现象日志里打印的create_time是凌晨 5 点明明操作发生在下午 1 点。原因application.yml里的 JDBC URL 没有加serverTimezoneAsia/Shanghai或者服务器系统时区是 UTC。解决数据库连接串固定写serverTimezoneAsia/Shanghai同时 MySQL 服务端时区用Asia/Shanghai两边保持一致。这个坑在单体本地开发时不会暴露因为 IDE 和 MySQL 在同一台机器一旦部署到云服务器就立刻现原形。5.6 现象Spring Boot 版本“太高”原来能跑的配置突然失效现象把 Spring Boot 从 2.7 升到 3.x 后静态资源 404、拦截器不生效、javax.servlet包名找不到。原因Spring Boot 3.x 基于 Spring Framework 6Servlet API 从javax迁移到了jakarta很多老项目代码直接编译报错。解决项目源码基于哪个大版本就用哪个大版本的依赖。2.7 的工程升 3.x 不是改个版本号就完事需要把javax.*的导入全部改成jakarta.*Interceptor、Filter、WebMvcConfigurer 的实现方式也有细节变化。对于课程设计或内部项目固定住 2.7.x 不升级是最省心的方案。6. 把源码变成你自己的商城一键验证脚本与三个改造方向拿到这套源码别急着改功能先用最小成本验证每一层都能跑通。我习惯写一个启动验证脚本把“后端启动、接口连通、数据库有数”三件事一次做完#!/bin/bash # 1. 启动项目后台运行 nohup java -jar digital-mall.jar --spring.profiles.activedev app.log 21 # 2. 等待服务启动 sleep 15 # 3. 检查健康状态 curl -s http://localhost:8080/api/health | grep -q code:200 echo 后端启动 OK || echo 后端启动失败 # 4. 调用藏品列表接口 curl -s http://localhost:8080/api/collectible/list?page1size10 | head -c 200如果健康检查通过但列表接口异常优先看app.log里的 SQL 日志MyBatis-Plus 配置了StdOutImpl后每次执行的 SQL 都会打印排查参数绑定问题最直接。验证通过后我建议按以下三个方向做二次开发把源码逐步改成自己的项目。第一把图片存储从本地目录迁移到 MinIO 或阿里云 OSS相对路径不变只替换上传逻辑这一步能解决“换服务器丢图片”的问题。第二引入 Spring Security 替换自定义拦截器把“登录认证 接口鉴权”纳入标准框架适合需要交论文的课程设计因为答辩老师通常对安全这一块比较感兴趣。第三如果你更熟悉前后端分离可以把前端 HTML 页面逐步改造成 Vue 单页应用后端接口完全不用动前后端分离后部署时把打包产物放到 Nginx 里接口地址通过代理转发。我个人的习惯是拿到任何一套源码先花半天把数据表结构画成一张 ER 图再动手改代码。数字藏品商城这类系统表结构理顺了后面写接口就是流水线作业表结构乱掉再强的后端也救不回来。希望这份拆解能帮到你动手跑一遍比读十遍文章更有效。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →