ARTICLE DETAIL

资讯详情

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

SpringBoot2+Vue3免税商城系统源码实战:身份证校验、额度扣减与限购管控

SpringBoot2+Vue3免税商城系统源码实战:身份证校验、额度扣减与限购管控 做电商项目的朋友应该都有体会商城系统看似千篇一律但一旦挂上“免税商品”这个业务场景整个设计逻辑就完全不一样了。最近梳理了一套完整的Java Web免税商品优选购物商城源码后端用SpringBoot2MyBatis-Plus前端Vue3数据库MySQL8.0标准的前后端分离架构。项目本身不算重但业务上覆盖了身份证实名校验、免税额度扣减、限购管控、完税价与免税价对比展示这几个普通商城根本没有的模块对想入门电商或正在做毕业设计/练手项目的同学来说参考价值非常高。这篇博文我把整个项目从技术选型、数据库设计到环境搭建、核心代码实现、部署排查全捋一遍每个关键点都会解释“为什么这么做”也把实操过程中容易踩的坑列出来。整套代码自带数据库初始化脚本和开发文档拿到手之后跟着这篇走一遍基本能完整跑通。1. 技术选型思路为什么是SpringBoot2Vue3MyBatis-PlusMySQL8.0很多人拿到项目第一反应是“技术栈是不是过时了”毕竟现在SpringBoot3和JDK17都出来好几年了。但站在真实项目落地角度看这套组合恰恰是国内中小型电商系统里最稳、最普及的一套配置。1.1 后端底座SpringBoot2.7.18才是务实选择SpringBoot2.7.x是2.x系列的最后一个维护分支它最大的优势是生态兼容性。很多企业内部还在用JDK8老系统也都是基于SpringBoot2构建的在这个版本上无论接Redis、MQ还是对接各种支付SDK基本都不会有依赖冲突。换成SpringBoot3之后JDK最低要求直接拉高到17一堆第三方库的旧版本也会因为javax到jakarta的包名迁移而出问题。这个项目选的是SpringBoot2.7.18配合Maven做依赖管理核心starter就那几样spring-boot-starter-web、spring-boot-starter-validation、mybatis-plus-boot-starter、mysql-connector-j、jjwt登录鉴权用。整个依赖树非常干净没有那种“版本号地狱”的焦虑。如果你自己从零搭建议直接锁死SpringBoot版本别用start.spring.io默认的latest版本否则MyBatis-Plus和SpringBoot之间可能出现兼容性问题。1.2 数据访问层MyBatis-Plus如何减少样板代码为什么不用JPA或原生MyBatisJPA在简单CRUD上确实省事但免税商城这种业务有大量动态条件查询比如按商品分类筛选、按免税额度区间过滤、按备案号模糊搜索JPA写动态查询要么拼Specification要么写Query注解复杂度一上来就很难维护。原生MyBatis呢每个单表操作都要写一遍Mapper XML一个用户表一个商品表光基础增删改查就能写出上千行XML纯纯浪费时间。MyBatis-Plus正好卡在中间。单表CRUD直接用BaseMapper提供的现成方法动态条件用LambdaQueryWrapper链式构造分页有内置插件逻辑删除、自动填充也都是内置能力。项目中商品管理、购物车、订单明细这类单表操作占了六成以上用MyBatis-Plus基本不写XML只有订单统计这类复杂SQL才单独建Mapper XML。这不仅是开发效率问题也直接降低了新接手的人理解成本。1.3 前端与数据库Vue3带来的体验提升和MySQL8.0的隐形优势前端选Vue3而不是Vue2核心原因是组合式APIComposition API更符合中后台系统的组织逻辑。商城后台有商品管理、订单管理、会员管理、额度记录一大票页面用Vue2的Options API写每个页面的data/methods/computed是散的功能一多改起来难受。Vue3下可以把“某个功能涉及的所有逻辑”写在一个function里按业务聚合配合Pinia做状态管理代码组织明显清爽。UI组件库用的Element Plus它和Vue3是强绑定关系表格、表单、Tabs标签页、图片上传这些后台高频组件都有现成实现。项目里商品列表页的筛选区用的就是Element Plus的Tabs组件Vue3生态对这类动态切换场景支持得很顺。数据库选MySQL8.0表面看是“随大流”其实有几个实打实的好处。第一是默认字符集是utf8mb4存商品名称里的特殊符号不会乱码第二是支持窗口函数做订单统计、用户消费排行这类分析查询比老版本用临时表优雅太多第三是从8.0.17开始提供clone插件之后如果要把测试库复制到本地一条CLONE INSTANCE命令就能搞定。部署时唯一要注意的是JDBC驱动必须用com.mysql.cj.jdbc.Driver而不是旧版的com.mysql.jdbc.Driver后面部署章节会细说。2. 免税商城的业务模型与数据库设计普通商城项目直接把用户表、商品表、订单表建好就能开工但免税商品商城不行。免税品业务和跨境电商、口岸免税店高度相关业务规则比一般电商严得多数据库设计必须提前把这些规则落到表结构和字段约束里。2.1 和普通商城不同的四条业务规则这个系统在设计时明确了四条特殊规则每一条都会直接影响后端代码怎么写身份准入购买免税商品必须通过身份证实名校验一个证件号只能绑定一个商城账号注册时就要校验格式不能像普通商城那样随便填昵称。额度管控会员有年度免税购物额度每次下单会占用额度额度不足时提示用户且无法提交订单。这个额度既要在用户表里冗余存储一份“当前剩余值”又要留下完整的扣减流水方便对账。商品准入免税商品必须有备案编号、完税限价、免税价同一个商品在完税和免税两个状态下价格不同。数据库里商品表必须包含这些字段不能只存一个price了事。限购管控部分商品有单笔限购件数、每人限购总件数订单明细表里要能支持按商品维度做限制逻辑而不是只在页面上提示一下。这些规则说明了一件事做业务系统表和字段不是随便建的每一条约束都对应一段业务需求。拿到这套源码后建议先读sql初始化脚本里面的表结构基本就展示了全部业务边界。2.2 核心表结构与建表SQL所有业务围绕以下核心表展开表名用途关键字段sys_user商城用户账号id、username、password、phone、statusmember_info会员实名与额度信息user_id、id_number、member_type、annual_quota、remaining_quota、versionproduct_category商品分类id、parent_id、name、sortproduct_info商品信息id、category_id、product_code、record_no、duty_free_price、tax_included_price、limit_count、stock、statuscart_item购物车id、user_id、product_id、quantity、checkedorder_main订单主表id、order_no、user_id、total_amount、quota_used、status、create_timeorder_item订单明细id、order_id、product_id、product_name、price、quantityquota_record额度流水表id、user_id、order_no、amount、type、create_time我抽几个关键表的建表SQL放在下面方便对比学习CREATE TABLE member_info ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 关联sys_user.id, id_number varchar(32) NOT NULL COMMENT 身份证号, member_type tinyint NOT NULL DEFAULT 1 COMMENT 1-离岛旅客 2-口岸旅客, annual_quota decimal(12,2) NOT NULL DEFAULT 10000.00 COMMENT 年度免税总额度, remaining_quota decimal(12,2) NOT NULL DEFAULT 10000.00 COMMENT 剩余免税额度, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_user_id (user_id), UNIQUE KEY uk_id_number (id_number) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员实名与额度信息表; CREATE TABLE product_info ( id bigint NOT NULL AUTO_INCREMENT, category_id bigint NOT NULL, product_code varchar(64) NOT NULL COMMENT 商品编码, record_no varchar(64) NOT NULL COMMENT 免税商品备案编号, product_name varchar(128) NOT NULL, duty_free_price decimal(10,2) NOT NULL COMMENT 免税价, tax_included_price decimal(10,2) NOT NULL COMMENT 完税价, limit_count int NOT NULL DEFAULT 0 COMMENT 单笔限购件数0不限, stock int NOT NULL DEFAULT 0, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_id (category_id), KEY idx_product_code (product_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品信息表;member_info里加了乐观锁字段versionorder_main里加quota_used记录本次订单用了多少额度quota_record表专门存额度流水这些都是免税业务区别于普通商城的关键设计。2.3 索引和外键高并发下的取舍另一个值得学习的点是整套代码里几乎没建物理外键。外键看似能保证数据一致性但在电商场景下会给插入和删除带来额外的锁开销而且一旦订单量上来外键约束经常成为死锁的源头。这套项目的做法是“应用层保证一致性”数据库只建普通索引业务逻辑在Service层控制关联校验。实际开发中我也一直遵循这个原则订单表只存必要的冗余字段比如order_item里冗余product_name和price查询时少一大堆JOIN页面响应速度明显更快。索引方面订单表要按user_id和时间范围查询所以建(user_id, create_time)的联合索引购物车表则是单值索引user_id就够了。3. 环境搭建与项目初始化这部分是很多人卡住的地方尤其是MySQL8.0的安装和Vue3环境配置两大拦路虎。我按实际操作的顺序写尽量让每一步都能直接照着敲。3.1 安装MySQL8.0Windows和Docker两条路Windows本地开发推荐直接用官方zip解压版别用安装版能省很多环境变量上的坑。下载mysql-8.0.x-winx64.zip后解压到比如D:\mysql-8.0.40-winx64然后按下面几步走# 1. 创建data目录如果有就删掉旧的 mkdir data # 2. 初始化数据目录--console打印初始密码 mysqld --initialize-insecure --console # 3. 安装Windows服务 mysqld --install MySQL8 # 4. 启动服务 net start MySQL8用--initialize-insecure初始化root用户默认没有密码启动后直接登录再改密码ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这里把root的认证插件改成mysql_native_password是因为MySQL8.0默认的caching_sha2_password对客户端兼容性更差老版本的工具和驱动容易报错。改完之后用Navicat连接基本就不会有问题了。如果你本机装了Docker更推荐用容器方式跑干净且不会污染系统环境。实测下来的命令docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -v mysql-data:/var/lib/mysql \ --restartunless-stopped \ mysql:8.0注意容器默认时区是UTC启动时最好加一个--character-set-serverutf8mb4的参数避免后面查询时间字段时发现差了8小时。3.2 初始化数据库并导入官方文档脚本项目源码中的sql目录里提供了完整的初始化脚本包含建库、建表、初始化分类数据、初始化商品演示数据。用Navicat新建数据库名字建议和application.yml里一致比如duty_free_mall字符集选utf8mb4然后直接把sql文件拖进查询窗口执行不出意外几秒钟就能看到所有表和演示数据。执行完成之后可以用一条简单SQL验证一下SELECT p.product_name, p.duty_free_price, p.tax_included_price FROM product_info p WHERE p.status 1 LIMIT 10;能看到十几条商品数据就说明导入成功。3.3 创建SpringBoot后端工程后端工程可以直接从源码导入也可以自己按标准结构建。核心依赖pom.xml关键部分如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependencyIdea导入时建议用JDK8或JDK11Maven配国内镜像源否则依赖下载能等到怀疑人生。3.4 创建Vue3前端工程前端工程用Vite构建创建命令如下npm create vitelatest mall-web -- --template vue cd mall-web npm install npm install vue-router4 pinia axios element-plus装完依赖后用npm run dev启动默认在5173端口Vite的启动速度比Webpack快一个量级几乎秒开。然后需要安装几个Vue3生态的扩展包npm install -D unplugin-auto-import unplugin-vue-components这两个插件配合Element Plus可以做到组件按需自动引入不用在main.js里全量注册产物体积小不少。具体配置写在vite.config.js里代码在项目里已经是配好的状态直接copy使用即可。4. SpringBoot后端核心模块实现后端整体采用经典的Controller-Service-Mapper三层架构我挑了五个最值得学习的模块拆开讲。4.1 项目分层设计与application.yml配置包结构是一个成熟Java Web项目应该有的样子com.mall.dutyfree ├── controller // 接收参数返回统一结果 ├── service // 业务逻辑层 ├── mapper // MyBatis-Plus数据访问 ├── entity // 数据库实体 ├── common // 统一返回体、全局异常、工具类 └── config // 分页插件、跨域、拦截器等配置application.yml中比较关键的几处spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/duty_free_mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 servlet: multipart: max-file-size: 10MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0MySQL连接串里必须带serverTimezoneAsia/Shanghai否则会报“The server time zone value”的异常这是MySQL8.0最常见的坑之一。MyBatis-Plus的逻辑删除配置也要留意如果UTF数据表里没有deleted字段这一项就可以去掉否则运行会报字段不存在。项目里实际表结构选择了普通状态字段status来做商品上下架所以源码里没有启用logic-delete-field这点以你拿到的代码为准。4.2 商品分页查询MyBatis-Plus分页插件的正确姿势分页是商城列表页的刚需。MyBatis-Plus使用分页前先要注册分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后Service层就可以这样写public PageResultProductInfoVO pageProducts(ProductQuery query) { PageProductInfo page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperProductInfo wrapper new LambdaQueryWrapper(); wrapper.eq(query.getCategoryId() ! null, ProductInfo::getCategoryId, query.getCategoryId()) .like(StringUtils.hasText(query.getKeyword()), ProductInfo::getProductName, query.getKeyword()) .eq(ProductInfo::getStatus, 1) .orderByDesc(ProductInfo::getCreateTime); PageProductInfo result productMapper.selectPage(page, wrapper); return PageResult.of(result.getRecords(), result.getTotal()); }这里lambda条件构造器的核心优势就体现出来了eq方法第一个参数是boolean类型条件为true才拼接实现动态条件筛选又不需要手动写if拼SQL项目里的所有筛选接口都是这个模式。4.3 免税额度扣减如何用乐观锁防止超扣额度扣减是最容易出并发问题的地方。比如用户A和用户B同时下单都查到剩余额度1000元各自都判断“足够”然后同时扣款最后两个订单的额度总和可能超过1000。最简单的方案是给额度扣减加上数据库行锁但MyBatis-Plus项目里更常用的方案是乐观锁。member_info表里的version字段就是为此准备的。更新时带上版本号条件更新成功影响行数为0就说明版本变了直接重试Transactional(rollbackFor Exception.class) public boolean deductQuota(Long userId, BigDecimal amount) { int retryCount 0; while (retryCount 3) { MemberInfo member memberInfoMapper.selectOne( new LambdaQueryWrapperMemberInfo().eq(MemberInfo::getUserId, userId)); if (member.getRemainingQuota().compareTo(amount) 0) { throw new BusinessException(免税额度不足); } UpdateWrapperMemberInfo updateWrapper new UpdateWrapper(); updateWrapper.eq(id, member.getId()) .eq(version, member.getVersion()) .setSql(remaining_quota remaining_quota - amount) .setSql(version version 1); int update memberInfoMapper.update(null, updateWrapper); if (update 0) { insertQuotaRecord(userId, orderNo, amount); // 同步写流水 return true; } // 更新失败说明发生并发冲突重试 retryCount; } throw new BusinessException(系统繁忙请稍后重试); }这里有个细节扣减用setSql(remaining_quota remaining_quota - amount)而不是先把值查出来再set进去。前者是在数据库端原子自减后者是“读改写”两个并发请求可能读到同一个旧值导致更新丢失。关于amount拼接的方式真实项目建议使用参数绑定防止SQL注入源码里是使用UpdateWrapper的set方法传参数我这里为了展示写法用了setSql大家理解思路即可。4.4 订单提交事务边界怎么划订单创建涉及多个表的写入必须保证要么全部成功要么全部失败。核心是提交订单的Service方法上加Transactional把“扣库存、扣额度、生成主单、生成明细、清空购物车”放在同一个事务里Transactional(rollbackFor Exception.class) public OrderMain submitOrder(SubmitOrderDTO dto) { String orderNo generateOrderNo(); BigDecimal totalAmount BigDecimal.ZERO; BigDecimal totalQuota BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItemDTO item : dto.getItems()) { ProductInfo product productMapper.selectById(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(库存不足 product.getProductName()); } // 扣减库存使用update set stockstock-? 做数据库原子操作 int rows productMapper.deductStock(product.getId(), item.getQuantity()); if (rows 0) { throw new BusinessException(商品已售罄 product.getProductName()); } totalAmount totalAmount.add(product.getDutyFreePrice().multiply( BigDecimal.valueOf(item.getQuantity()))); totalQuota totalQuota.add(product.getDutyFreePrice().multiply( BigDecimal.valueOf(item.getQuantity()))); // 组装明细对象 } // 扣减额度 deductQuota(dto.getUserId(), totalQuota); // 保存订单主表和明细清空购物车... return orderMain; }事务边界怎么划这里我把额度扣减和订单创建放一起保证“额度减少但订单创建失败”这类脏数据不会出现。库存用数据库原子自减而不是先查后更新也是防止并发把库存扣成负数。4.5 登录鉴权JWT拦截器的小而美方案商城需要登录才能加购物车、下单后端鉴权用的是JWT加拦截器的方案没有引入Spring Security这种重框架。登录成功之后把userid和memberId放进JWT token拦截器在进入Controller之前解析token再把用户信息放进ThreadLocal供后续业务取用。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String authHeader request.getHeader(Authorization); if (StringUtils.hasText(authHeader) authHeader.startsWith(Bearer )) { String token authHeader.substring(7); try { Claims claims Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); Long userId Long.valueOf(claims.get(userId).toString()); Long memberId Long.valueOf(claims.get(memberId).toString()); UserContext.set(userId, memberId); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\登录已过期\}); return false; } } response.setStatus(401); return false; } }不引入Spring Security的考量很实际项目体量没那么大Security的filter链配置反而增加理解成本拦截器加白名单登录接口、商品列表、商品详情放行已经足够。之后真要加角色权限在拦截器里扩展角色判断即可。5. Vue3前端核心模块实现前端部分我挑几个和业务强相关的点讲项目结构、axios封装、商品列表与购物车、Pinia状态管理全程使用Vue3组合式API。5.1 前端目录结构与路由设计Vue3工程的目录结构是Vite初始化后按业务扩展的src ├── api // 按模块拆分的接口调用 ├── assets // 静态资源 ├── components // 通用组件 ├── router // 路由配置 ├── store // Pinia状态管理 ├── views // 页面首页、商品列表、购物车、订单、登录等 ├── utils // axios封装、工具方法 └── App.vue路由用vue-router4路由守卫里做了登录校验没带token想进购物车和订单页的一律重定向到登录页。商品列表和首页是公开路由适合做SEO和用户初次浏览。这样设计既保证了核心交易页面的安全又不至于把整个站点封死。5.2 封装axios请求层拦截器统一处理Token和错误码axios在项目里不是直接拿来就用的而是统一封装了一层。好处是所有接口都不用重复写token注入、错误处理、加载状态改一个地方就能全局生效。import axios from axios import { ElMessage } from element-plus import router from ../router import { useUserStore } from ../store/user const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token service.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer userStore.token } return config }) // 响应拦截器统一处理业务错误和HTTP错误 service.interceptors.response.use( response { const { code, message, data } response.data if (code 200) { return data } if (code 401) { userStore.logout() router.push(/login) } ElMessage.error(message || 请求失败) return Promise.reject(new Error(message)) }, error { ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default service这里有个细节Vite开发环境下前端请求的baseURL配成/api而实际接口是http://localhost:8080必须在vite.config.js里配代理把/api开头的请求转发到后端端口同时解决开发环境的跨域问题server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }5.3 商品列表与购物车组合式API的实战写法商品列表页核心逻辑就是两件事按条件查询商品、把商品加入购物车。用Vue3的ref和reactive组织数据computed计算状态代码比Vue2清爽很多。商品列表的关键部分script setup import { ref, reactive, onMounted, computed } from vue import { getProductPage } from /api/product import { useCartStore } from /store/cart const loading ref(false) const productList ref([]) const total ref(0) const queryParams reactive({ pageNum: 1, pageSize: 12, categoryId: null, keyword: }) const cartStore useCartStore() // 按分类筛选时重置页码并重新拉取 const tabsChange (value) { queryParams.categoryId value queryParams.pageNum 1 loadProducts() } const loadProducts async () { loading.value true try { const data await getProductPage(queryParams) productList.value data.records total.value data.total } finally { loading.value false } } const addToCart (product) { cartStore.addItem({ productId: product.id, productName: product.productName, price: product.dutyFreePrice, quantity: 1 }) } onMounted(loadProducts) /script对比Vue2里满屏的this.xxxusing组合式API的好处是逻辑清晰所有变量都是JavaScript原生变量类型推导也更好。这里特别提一下computed的用法购物车角标数量、购物车总金额这类展示型值别在模板里写复杂表达式封装成computed更合适const cartCount computed(() cartStore.cartList.length)模板里只用写{{ cartCount }}就行源数据一变它自动更新。5.4 Pinia状态管理购物车购物车状态用Pinia管理比组件之间props和emit层层传递省心太多。Pinia的store同时支持Options和Setup两种写法项目中用的是Setup风格import { defineStore } from pinia import { ref, computed } from vue export const useCartStore defineStore(cart, () { const cartList ref([]) const totalQuantity computed(() cartList.value.reduce((sum, item) sum item.quantity, 0) ) const totalAmount computed(() cartList.value.reduce((sum, item) sum item.price * item.quantity, 0) ) const addItem (item) { const existing cartList.value.find(i i.productId item.productId) if (existing) { existing.quantity item.quantity } else { cartList.value.push({ ...item }) } } const removeItem (productId) { cartList.value cartList.value.filter(i i.productId ! productId) } const clearCart () { cartList.value [] } return { cartList, totalQuantity, totalAmount, addItem, removeItem, clearCart } })组件里使用storeToRefs取响应式数据避免直接用解构丢失响应性import { storeToRefs } from pinia const cartStore useCartStore() const { cartList, totalQuantity, totalAmount } storeToRefs(cartStore)这个细节是Vue3/Pinia新手最容易踩的坑直接const { cartList } useCartStore()拿到的cartList只是一个普通值页面不会随store变化自动更新。6. 部署上线与踩坑实录代码本地跑通只是第一步部署到服务器上才是完整的Java Web项目流程。我把部署的两种路线和过程中最容易出的四类问题全部列出来。6.1 打包和部署流程jar包Tomcat双路线因为项目名带了“Java Web”关键词很多人会默认部署到Tomcat。SpringBoot2项目其实有两种部署方式二选一即可。方式一内嵌Tomcat打成jar包直接运行。这是SpringBoot最推荐的操作最简单mvn clean package -DskipTests java -jar duty-free-mall.jar生产环境可以加一个systemd服务或者轻量脚本守护进程代码里已经提供了start.sh和stop.sh直接改端口和jar包路径就能用。方式二传统的外置Tomcat部署。如果公司环境要求必须按war包发布修改三处pom里packaging改为war启动类继承SpringBootServletInitializer并重写configure方法然后打包出来放进Tomcat的webapps目录。这种方式启动会比较慢因为SpringBoot和Tomcat的类加载器需要磨合但胜在和运维体系兼容。前端构建后产物是静态文件部署到Nginx即可npm run build # dist目录下生成构建产物Nginx配置里需要特别注意Vue Router默认是history模式刷新非首页路由会404必须在server块里配置try_fileslocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这个配置我每次部署Vue项目都要检查一遍没有它用户一旦在订单页按F5请求会直接打到Nginx的静态目录找不到对应文件返回404。6.2 MySQL8.0四类致命问题我罗列一下MySQL8.0使用途中出现频率最高的四个错误全部亲身踩过报错信息原因解决方案The server time zone value is unrecognizedJDBC连接串没配时区url加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowed客户端不支持caching_sha2_passwordurl加allowPublicKeyRetrievaltrueAccess denied for user rootlocalhostroot密码或认证插件不对用mysql_native_password重新设置认证Unknown database xxx数据库没创建或名称不一致检查建库脚本和application.yml中的数据库名最后一个问题最隐蔽开发时Navicat连的是本地3306服务器上部署时改成服务器IP结果忘记改数据库名前后端接口全部报数据库连接失败。排查半天发现是名字大小写问题MySQL在Linux下默认表名区分大小写Windows下不区分这点跨平台部署时一定要提前约定好。6.3 前后端联调与Nginx部署的坑联调阶段最经典的坑是CORS跨域。开发环境用Vite代理解决了但部署到Nginx后如果没有正确配置反向代理前端请求后端的/api路径会被Nginx当成静态文件路径直接404。正确做法是Nginx里单独配置一个location把/api转发到后端服务location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }要注意proxy_pass后面的斜杠http://127.0.0.1:8080/带斜杠表示替换掉匹配的前缀/api/login会被转发到http://127.0.0.1:8080/login不带斜杠则是拼接成/api/login。这个细节错一处整个接口就全挂。另外一个Edge浏览器相关的坑也值得提Vue3项目在Edge里某些页面出现弹层关不掉或者Tabs标签页切换后样式残留大概率是Element Plus组件升级后和浏览器版本不匹配。解决办法是升级element-plus到最新版本如果公司固定要老版本就在全局样式里手动覆盖相应组件的动画和层级。6.4 Jenkins自动部署流水线参考现在稍微规范一点的团队都会用Jenkins做自动部署。这个项目配一条最简流水线只需要三个Stage拉代码、构建、发布。pipeline { agent any stages { stage(拉取代码) { steps { git credentialsId: gitlab-cred, url: https://gitlab.xxx.com/mall/duty-free-mall.git } } stage(后端构建) { steps { sh mvn clean package -DskipTests } } stage(部署到服务器) { steps { sshPublisher( publishers: [ sshPublisherDesc( hosts: [ sshHost(hostname: 192.168.1.10, username: deploy, password: ******) ], transfers: [ sshTransfer( sourceFiles: target/duty-free-mall.jar, remoteDirectory: /opt/mall, execCommand: systemctl restart mall ) ] ) ] ) } } } }实际做的时候建议用密钥认证代替密码executeCommand里加上备份旧包的命令回滚也方便。Jenkins流水线的好处是每次发布过程可追溯开发、测试、生产环境都走同一套逻辑不会出现“在我本机是好的”这类问题。6.5 常见问题速查表最后整理一张速查表遇到问题直接按图索骥场景现象排查思路后端启动报连接数据库失败根据报错信息判断是时区、认证还是网络问题先检查application.yml里的连接串、账号密码、远程授权前端接口全部401通常是登录过期或token没带打开浏览器调试面板看Authorization请求头没有就是axios拦截器没生效商品图片不显示图片路径或跨域确认上传目录正确、Nginx静态资源路径匹配、后端是否配置了资源的MIME类型购物车数量不更新Pinia store被解构后丢失响应性改用storeToRefs不要直接从store解构订单提交后库存没扣事务可能没生效检查Service方法是不是被同类调用this调用绕过代理、类是否被Spring管理写在最后这套项目我完整跑通下来最大的体会是“业务规则才是系统的灵魂”。同一个SpringBootVue3的架子做成普通商城和做成免税商城难度完全不是一个级别。身份证校验、额度扣减、限购管控这些模块每一条都要从数据库设计开始想清楚而不是先写代码再补逻辑。你拿到源码后我建议按“先读sql脚本→再读表结构注释→然后对着接口文档跑一遍→最后跟代码”的顺序来比直接翻Controller效率高很多。免税商城后续如果要扩展可以往两个方向走一是把额度统计做成定时任务每天自动对账生成报表二是引入Redis缓存商品分类和热门商品列表把首页查询压力降下来。但这些都是跑通基础版之后的事先把这套系统吃透对整个Java Web电商项目的理解会上一个台阶。
返回列表