上个月刚把手头这套 leabo 私人西服定制系统交付出去,源码、数据库脚本、部署文档、演示账号都整理成包给了对方。这是一个非常典型的 Java Web 全栈项目,后端用的是 SpringBoot2 + MyBatis-Plus + MySQL8.0,前端是 Vue3 + Element Plus,整体结构清晰,功能覆盖也比较完整,很适合拿来当作毕业设计、课设项目或者中小企业内部定制业务的起步系统。
我最初接到这个项目需求时,对方给的信息并不算复杂,就是想把西服定制的流程搬到线上:用户选款式、选面料、填尺寸、下单,后台有人跟单、审单、安排制作。但真正落到系统设计后,牵扯出来的功能点非常多。今天这篇文章就是把这套系统从设计思路、技术选型、数据库建模、后端接口到前端页面实现,再到部署上线的完整过程拆开讲一遍。文章里写的都是实际操作中的真实做法,附带我踩过的坑和排查思路,适合正在做同类项目的同学参考,也更适合拿到源码后不知道怎么下手的读者照着走一遍。
1. 项目定位与功能设计思路
1.1 西服定制的业务痛点,系统到底要解决什么问题
西服定制不像普通服装电商,SKU 不是“尺码 + 颜色”就能定下来的。定制场景里,用户要确认的维度非常多:版型是修身还是宽松,单排扣还是双排扣,平驳领还是戗驳领,面料是羊毛还是混纺,里布颜色,扣子材质,袖口开叉几粒扣,要不要刺绣,甚至量体数据里包含胸围、腰围、肩宽、袖长、衣长等十几项数据。
传统线下的做法是门店顾问拿纸质表单手填,量体数据靠卷尺记录,下单后工厂再人工录入 ERP。问题在哪里?第一,数据容易抄错,尤其是批量订单;第二,用户看不到进度,只能打电话催;第三,价格计算不透明,顾问报价全凭口头;第四,历史订单难追溯,返单率高,客户第二次下单时又得重新量体。
所以这套系统在设计上就围绕三个核心关键词做文章:结构化、可追溯、状态化。结构化是指把定制的所有可变项拆成表单项,用字段存数据库,而不是塞在备注里;可追溯是指用户订单从提交到量体、制作、发货、完稿,每个环节都有状态记录和操作日志;状态化则是把订单整体流转串成一个状态机,前后台页面都以状态为核心去驱动交互。
1.2 用户端和管理端的功能拆解
整个系统分前台和后台两部分,前台面向 C 端用户,后台面向门店管理员或运营人员。前台模块包括用户注册登录、商品列表和详情、在线定制器、购物车、订单管理、个人资料编辑,以及一套贯穿所有页面的订单状态查询能力。
后台模块相对更重,包括会员列表、商品管理、面料库管理、订单审核、订单进度流转、价格规则配置、数据看板。这里值得说明的是,后台并没有做成复杂的权限多角色系统,只分了管理员和普通运营两种角色,因为西服定制门店内部通常就几个人:店长负责审单和排期,顾问负责对接用户,量体师负责填写量体数据,流程上各干各的。但权限控制上还是预留了扩展空间,基于角色字段做了接口级别的限制,后面要细分也不难。
1.3 线上定制流程如何串成一个闭环
这里我给订单定义了八个状态节点,严格按顺序流转:待付款、待确认、待量体、制作中、待发货、已发货、已完成、已取消。用户提交定制方案后生成订单,订单默认是待付款状态;支付成功后进入待确认,后台管理员确认面料和工艺无误后锁定订单;之后进入待量体,这一步可以由用户线下到店量体,也可以在页面填写历史量体数据,量体师在后台补充数据;量体数据确认后系统自动触发制作中状态;制作完成后发货,用户确认收货,订单完成。
每个状态节点都对应一张订单状态记录表,记录操作人、操作时间、操作前后的状态、备注内容。这个设计在后期处理售后纠纷时特别有用,比如用户说“我明明提交过刺绣内容”,后台一查记录表就知道是哪个环节漏了。
2. 技术栈选型解析
2.1 为什么选 SpringBoot2 而不是其他框架
后端技术栈选了 SpringBoot2,这几乎是当前 Java Web 项目里最稳妥的选择。SpringBoot2 虽然不像 SpringBoot3 那样紧跟 JDK17 和 Jakarta EE 新规范,但它的生态兼容性真的是太成熟了。大多数企业环境里 JDK8 仍然是主力,SpringBoot2 的 2.7.x 系列在 JDK8 上跑得非常稳定,第三方集成组件(比如各类云 SDK、支付 SDK、消息中间件客户端)对 JDK8 + SpringBoot2 的适配也是做的最完善的。
我强调这个选择的背后逻辑是“稳”字优先。系统要对接微信支付、支付宝、短信服务、物流查询,这些外部接口的技术文档很多还停留在旧版依赖包,用 SpringBoot3 去接入很容易遇到 javax 和 jakarta 命名空间不兼容的问题。SpringBoot2 在这方面的坑最少,遇到问题搜解决方案也最快。
另外一个重要因素是 SpringBoot2 内嵌 Tomcat 的配置和管理非常轻量,打出来的 jar 包可以直接java -jar跑,不需要单独装 Tomcat,这对中小企业服务器部署来说是最友好的方式。项目文档里我也特意把“如何用 Systemd 守护 Java 进程”写了进去,防止进程挂掉后没人管。
2.2 Vue3 组合式 API 带来的开发效率提升
前端我没有用 Vue2,直接从 Vue3 起步。Vue3 最大的变化是组合式 API,它让我可以把同一个业务逻辑的响应式数据、计算属性和方法集中在一个setup函数或者<script setup>区块里,而不是按照选项式 API 那样把 data、methods、computed 强行拆开。
举个例子,定制页面里我维护一个configForm对象,里面有版型、面料、尺寸、刺绣等字段。如果用 Vue2,这些数据在data里定义,联动逻辑在watch里写,提交逻辑在methods里写,三个区域割裂开;用 Vue3 的组合式 API,我可以把整个定制流程的所有逻辑封装到一个useTailorConfig的自定义 Hook 里,页面模板里只需要调用这个 Hook,返回表单数据和操作函数即可。
Vue3 的响应式系统基于 Proxy 重写,对数组下标变更、对象属性动态新增这类操作都能正确触发更新。这一点在动态表单的场景中特别关键。定制页面里用户随时可能添加一个测量数据项或者删除一个辅料选项,数组长度变化后视图能立刻刷新,用 Vue2 的时候就必须反复this.$set,这种细节对比在做大型表单时感受极其明显。
2.3 MyBatis-Plus:单表 CRUD 的瑞士军刀
Java 后端做数据库访问层,可选方案无非 MyBatis、MyBatis-Plus、JPA。这套系统选 MyBatis-Plus,核心原因有三个。
第一个原因是单表操作几乎不需要手写 SQL。MyBatis-Plus 内置了BaseMapper接口,提供selectById、selectList、insert、updateById、deleteById以及强大的QueryWrapper条件构造器。系统里的用户表、面料表、商品表、订单记录表这类常规实体,绝大多数数据操作一个 Mapper 接口加一个 XML 都不用写,直接继承BaseMapper<T>就能跑。
第二个原因是分页插件做得非常成熟。MyBatis-Plus 的PaginationInnerInterceptor可以无缝对接 MySQL 的LIMIT分页语法,查询时只需要传入Page对象,返回结果直接带出总数。相比自己手写分页 SQL,代码量减少一半,而且不用费心维护 count 查询。
第三个原因是它内置了逻辑删除、自动填充、乐观锁插件等实用功能。逻辑删除意味着订单和用户记录不会被物理删除,只会打上一个删除标记,这样后台做数据统计时还能看到历史数据的影子。自动填充可以在插入和更新时自动给createTime、updateTime赋值,省去了每个 Service 里手动 set 当前时间的工作。
2.4 MySQL8.0 用到的几个新特性
数据库选了 MySQL8.0,主要看中它的几个特性在项目里实际派上了用场。
MySQL8.0 默认字符集是utf8mb4,可以直接存储 emoji 字符,这看起来是小事,但在用户昵称、留言评价、收货地址这类自由输入字段里,如果没有 utf8mb4 很容易出现插入报错或者问号乱码。这套系统的数据库初始化脚本里,所有表结构都明确指定了utf8mb4_general_ci排序规则,从根源上规避了中文和特殊符号的兼容问题。
窗口函数也是 MySQL8.0 的强项。后台数据看板需要统计每月的订单数量、销售额、用户复购率,使用ROW_NUMBER()、SUM() OVER(PARTITION BY ...)这类窗口函数可以直接在 SQL 层完成复杂统计,不需要把数据拉到 Java 内存里再聚合。实际项目里复购率统计、订单状态分布统计都用窗口函数优化过,SQL 简洁清晰很多。
MySQL8.0 还支持公用表表达式(CTE),即WITH AS语法。我在做会员等级统计时,先算用户订单汇总,再关联会员等级表,一条 CTE 语句就把两层嵌套查询写完了,没有子查询那么难读,执行效率也更高。
3. 后端核心实现细节
3.1 工程结构与分层规范
后端项目的包结构如下,这也是我经过几次项目迭代后固定下来的标准姿势:
com.leabo ├── common # 通用返回结果、异常处理、常量定义 ├── config # Spring Boot 配置类,比如跨域、拦截器、分页插件 ├── controller # 接口层,只做参数接收和结果封装 ├── entity # 数据库实体类 ├── mapper # MyBatis-Plus 的数据访问接口 ├── service # 业务逻辑层 │ └── impl ├── dto # 前端交互的数据传输对象 └── util # 工具类,比如 JWT 工具、文件上传工具分层原则是 controller 层不写业务逻辑,service 层不直接操作数据库,mapper 层只做数据访问。有人会觉得这样很繁琐,多绕了一层,但实际项目里的收益很明显:当订单流程变复杂,比如同一个接口既需要修改订单状态又需要写状态记录表又需要扣减库存时,业务逻辑集中在 service 层,事务边界清晰,后续维护也轻松。
每个 Controller 接口统一返回Result<T>对象,结构是{ code, message, data },前端 Axios 拦截器收到响应后判断 code 是否为 200,再做对应处理。全局异常处理器负责捕获业务异常和数据库异常,将统一格式的错误信息返回给前端,这样用户看到的是友好提示而不是堆栈信息。
3.2 核心表结构设计
数据库表设计是整个系统里最重要的环节,我用了大概 15 张表支撑全部功能。下面挑出最重要的几张表做说明。
先看用户表,字段设计上除了常规的主键、昵称、手机号、密码、头像之外,还预留了member_level和total_spent两个字段。member_level用于标识普通会员和 VIP 会员,total_spent会随着订单完成自动累加,方便后台做会员等级判定。
订单表是最核心的一张表,字段包括订单号、用户 ID、定制方案 ID、订单金额、实付金额、订单状态、收货人信息、支付时间、发货时间、完成时间、逻辑删除标记。订单号我设计成 17 位:yyyyMMddHHmmss + 4 位随机数,比如202501151430251234,这样既保证唯一性,也可以从订单号直接看出下单时间,排查问题非常方便。
定制方案表负责存放用户在前端定制器里选择的所有参数,核心字段包括版型、领型、扣型、面料编号、里布颜色、刺绣文本、刺绣位置、胸围、腰围、肩宽、袖长、衣长等。这一张表把用户的个性化需求全部结构化存储,后端生成生产单时直接读取这些字段输出给工厂,工厂端打印一张工艺卡就可以开工。
面料表管理面料的基础信息,包含面料编号、名称、成分、克重、颜色、单价、库存数量、图片地址、状态。这里需要注意,面料单价会直接参与订单价格计算,所以字段类型我用DECIMAL(10,2),避免浮点数计算误差。
订单状态记录表也是一个重点表,字段包含订单 ID、操作人 ID、操作人类型、操作前状态、操作后状态、操作备注、创建时间。每次订单状态流转时,业务代码都会在这里插入一条流水。这个表在演示项目时效果很好,用户端可以看到“你的订单已进入制作中,操作人:王师傅”。
3.3 定制下单接口的实现思路
用户提交定制方案的下单接口,我按以下步骤处理。
接口接收前端传来的TailorOrderCreateDTO,里面包含定制参数对象、收货地址 ID、用户备注。第一步做参数校验,重点校验必填项:版型、面料、尺寸是否齐全,刺绣内容长度是否超限。第二步根据面料单价、版型工艺费等计算订单金额。价格计算规则我在商品配置里存了一个 JSON 字符串,包含基础价、加价项和计价规则,Service 层读取后逐项计算。第三步生成订单号并插入订单记录,状态置为待付款。第四步写入初始状态记录,备注内容是“用户提交定制订单”。
这里有个容易踩坑的点:订单金额计算时机应该在用户提交时完成,而不是在支付回调里算。因为支付回调里能使用的业务上下文太少,万一计算逻辑出错,钱收错了很难修正。提前算好并落库,支付回调里只需要比较实付金额和订单金额是否一致即可。
Controller 层的代码示意如下:
@PostMapping("/api/order/tailor/create") public Result<Long> createTailorOrder(@RequestBody @Valid TailorOrderCreateDTO dto) { Long userId = UserContext.getUserId(); Long orderId = orderService.createTailorOrder(userId, dto); return Result.success(orderId); }Service 层的核心逻辑是createTailorOrder,事务注解必须加上。因为这一步既要插入订单表,又要插入定制方案表,还要插入状态记录表,任何一个失败都需要回滚,不能出现订单创建了但方案没保存的脏数据。实际操作中我遇到过忘记加@Transactional导致状态记录缺失的情况,排查到凌晨才发现,这个坑印象太深了。
3.4 JWT 登录与权限拦截
用户端和管理端登录都使用 JWT 做鉴权。登录接口校验手机号和密码成功后,生成 token 返回给前端。token 里放了用户 ID 和角色,过期时间设置成 7 天,用户端每次请求在 Header 里携带Authorization: Bearer xxx。
后端拦截器统一校验 token,通过HandlerInterceptor实现,在preHandle方法里读取 Header,解析 JWT,然后通过ThreadLocal将当前用户信息放入UserContext。这里要指出一个细节:自定义拦截器只拦截/api/**的路径,登录注册接口和静态资源不拦截。配置类里注册拦截器的代码片段如下:
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register"); }角色权限我用一个简单的@RequireRole注解加拦截器实现。后台管理的敏感接口都加上这个注解,比如商品删除、订单状态流转。拦截器里拿到 token 中的角色字段做判断,没有权限直接返回 403 错误码。这种方案比 Shiro、Spring Security 轻量很多,对这类单系统项目非常合适,引入 Spring Security 在配置上付出的成本远大于收益。
4. 前端 Vue3 工程化实践
4.1 项目初始化与目录规划
前端部分我没有用 Vite 脚手架默认生成的平铺目录,而是按照模块化思路重新规划。项目创建命令用的 Vite,运行npm create vite@latest leabo-web -- --template vue,然后安装 Vue Router、Pinia、Axios、Element Plus 以及 Sass。
目录结构大概如下:
src ├── api # 接口请求定义,按业务模块拆分 ├── assets # 静态资源 ├── components # 通用组件 ├── hooks # 自定义组合式函数 ├── layout # 后台管理布局 ├── router # 路由配置 ├── stores # Pinia 状态管理 ├── views # 页面视图 │ ├── admin # 后台管理页面 │ └── mall # 前台商城页面 ├── utils # 工具类 └── App.vueapi目录下的每个文件都对应一个业务模块,比如order.js里只放订单相关的接口方法。这样做的好处是,当页面需要修改接口地址、新增字段时,只需要在一个文件里集中改动,不会出现同一个 URL 分散在多个组件里的情况。
4.2 Pinia 状态管理与用户登录态
Vue3 项目状态管理我选 Pinia,它比 Vuex 更轻,直接支持组合式 API 风格的定义方式。系统里的全局状态主要有用户信息、购物车数量、后台侧边栏折叠状态三类。
用户登录态管理在stores/user.js里实现,登录成功后调用接口拿 token,同时把用户基本信息缓存到 localStorage,页面刷新后重新从 localStorage 恢复用户状态。这个环节有个容易出错的地方:用户信息缓存和 token 缓存必须同步处理。我见过很多项目 token 存在一个 key、用户信息存在另一个 key,导致 token 有效但用户信息为空,页面到处报错。我直接把用户信息放进一个localStorage对象的userInfo字段,跟 token 放在同一块区域集中管理。
Pinia 的 store 定义代码如下:
export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null')) function setLoginInfo(data) { token.value = data.token userInfo.value = data.userInfo localStorage.setItem('token', data.token) localStorage.setItem('userInfo', JSON.stringify(data.userInfo)) } function logout() { token.value = '' userInfo.value = null localStorage.removeItem('token') localStorage.removeItem('userInfo') } return { token, userInfo, setLoginInfo, logout } })4.3 定制流程的动态表单实现
定制器是这套系统前端最复杂也最核心的页面。用户选择面料、版型、领型等选项时,后续的下拉项和输入项会根据选择动态变化。比如用户选择“双排扣”版型后,系统才显示扣子排数和扣子颜色选项;选择“带刺绣”后,才显示刺绣文字和刺绣位置的输入框。
Vue3 里实现这种联动,我用的是一个响应式对象configForm加多个计算属性:
const configForm = reactive({ version: 'slim', collarType: 'notch', fabricId: null, liningColor: 'black', embroideryText: '', embroideryPosition: 'leftCuff', hasEmbroidery: false }) const showEmbroidery = computed(() => configForm.hasEmbroidery) const showDoubleButton = computed(() => configForm.version === 'double')模板中用v-if="showEmbroidery"控制刺绣相关表单项是否显示,用v-model绑定configForm的字段。这种写法非常直观,不需要手动去监听每个字段的变化,也不需要维护一堆布尔标记。需要注意的是,当hasEmbroidery从 true 切回 false 时,要把embroideryText清空,否则用户选择刺绣后取消,提交时仍然会把旧的刺绣文本带上。
4.4 Axios 封装与请求拦截
前端请求封装是后台管理系统里必须做的一步。我在utils/request.js里创建了一个 Axios 实例,设置了基础 URL 为/api,然后配置了请求拦截器和响应拦截器。
请求拦截器主要做两件事:往请求头里塞 token,以及给请求加一个loading标识(可选)。响应拦截器的工作是统一处理接口返回结果,当response.data.code !== 200时,直接弹出 Element Plus 的ElMessage显示后端返回的错误信息,并且返回一个Promise.reject中断后续代码。当 code 为 401 时,清除本地登录信息并且跳转到登录页。
这个统一拦截在开发阶段帮了我大忙,前后端联调时后端只要返回正确的错误码,前端就能自动弹提示,不需要每个页面单独去写错误处理逻辑。如果后端返回的 code 是 500,我只在控制台打印详细信息,前端提示“系统繁忙,请稍后重试”,避免把数据库异常信息直接暴露给用户。
5. 环境搭建与部署实录
5.1 MySQL8.0 安装与数据初始化
拿到源码后第一步是准备数据库环境。MySQL8.0 的安装没有特别复杂,Linux 服务器上建议直接用系统包管理器安装,或者用 Docker 拉取官方镜像。Docker 方式最省心,一条命令就能跑起来:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e MYSQL_DATABASE=leabo_db \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0MySQL8.0 需要注意的事项是认证插件。默认的caching_sha2_password认证方式在某些老版本客户端和驱动上可能报认证失败,如果遇到这类问题,可以在创建用户时显式指定mysql_native_password:
CREATE USER 'leabo'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; GRANT ALL PRIVILEGES ON leabo_db.* TO 'leabo'@'%'; FLUSH PRIVILEGES;项目文档里的数据库初始化脚本会自动创建数据库和表结构,并且插入一条管理员账号、几条示例面料、示例商品。直接用命令行执行脚本即可:
mysql -u root -p < leabo_db.sql5.2 后端配置与实际打包
后端配置文件application.yml里有几个地方需要按实际环境修改。第一个是数据库连接地址和账号密码,第二个是 JWT 的签名密钥,第三个是文件上传的本地存储路径。
服务器上跑后端项目,我不建议把配置写在application.yml里写死,而是通过启动命令传参覆盖。比如启动 MySQL 的地址用环境变量DB_HOST来控制:
java -jar leabo-server.jar \ --spring.datasource.url=jdbc:mysql://${DB_HOST}:3306/leabo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai \ --spring.datasource.username=${DB_USER} \ --spring.datasource.password=${DB_PASSWORD}这样同一个 jar 包可以直接在开发环境、测试环境、生产环境之间切换,只需要改环境变量,不用重新打包。打包命令就是标准的 Maven 命令:mvn clean package -DskipTests,最终生成的 jar 在target目录下。如果服务器内存较小,启动时加上-Xms256m -Xmx512m限制堆内存,避免占用过多资源。
5.3 前端构建与 Nginx 部署
前端开发调试时执行npm run dev,部署上线则执行npm run build。构建产物是dist目录,里面是纯静态文件,可以直接交给 Nginx 托管。我的 Nginx 配置大概如下:
server { listen 80; server_name yourdomain.com; root /opt/leabo-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里有一个最关键的部分是try_files $uri $uri/ /index.html。Vue Router 如果使用 history 模式,刷新某个子页面时如果 Nginx 没有这个配置,会直接返回 404,因为服务器上根本不存在对应的静态文件路径。把这个配置加上后,所有未知路径都会回退到index.html,由前端路由接管。
如果你不想用 history 模式,也可以把 Vue Router 改成 hash 模式,URL 里会出现#符号,刷新时不会有 404 问题。但为了美观和 SEO,我还是推荐 history 模式加 Nginx 回退配置。
5.4 文档包里都包含了哪些内容
这套项目自带的文档是我整理交付时特意做的,包括需求分析说明书、数据库设计文档、接口文档、部署手册和演示数据说明。需求分析里写了详细的业务背景、角色分析、功能模块划分和用例图;数据库设计文档里画了 ER 图(用文字表格描述关系)并且逐表列出字段说明、索引和关联关系;接口文档按模块列出每个接口的 URL、请求方法、请求参数、返回示例和错误码。
对做毕业设计的同学来说,这套文档能省非常多时间,因为评审老师关注的重点基本都覆盖了。对二次开发的人来说,接口文档可以直接照着联调,部署手册里从装 JDK、装 MySQL、构建前端到 Nginx 反向代理全部写清,按步骤操作半个小时内能把整套系统跑起来。
6. 常见问题与排查速查
6.1 MySQL8.0 驱动与连接时区问题
这是新手最容易遇到的问题。使用 MySQL8.0 后,JDBC 驱动的类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,如果连接 URL 里没有加serverTimezone参数,启动时会直接报The server time zone value is unrecognized错误。解决办法是在连接 URL 中显式指定时区:
jdbc:mysql://localhost:3306/leabo_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8这里有个排查经验:如果你用的是 SpringBoot2.7,引入的 MySQL 驱动版本默认可能还是 8.0.x,请务必核对pom.xml里的依赖坐标,不要顺手引入老版本的 5.1.x 驱动,会引发一系列兼容问题。
6.2 MyBatis-Plus 分页插件不生效
MyBatis-Plus 使用分页功能时,需要手动配置分页插件,不能只依赖内置的Page对象。没有注册分页拦截器时,分页查询返回的total会是 0,所有数据也会一次性查出来,这就等于分页失效。在配置类里加一个 Bean 即可:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }这个坑我帮很多同事排查过,几乎都是因为少了这段配置。
6.3 跨域问题与 Session 登录态丢失
前后端分离部署时,如果前端域名是www.xxx.com,后端接口跑在另一台服务器上,浏览器跨域请求会失败。我的方案是在后端写一个全局跨域配置类,允许指定域名访问,并且允许携带凭证。Nginx 部署时如果使用反向代理,将/api/代理到后端,跨域问题就不会出现。但如果前端直接用 Axios 请求后端地址,则必须处理跨域。
另外要提醒的是,如果登录态用 Session 保存(不推荐,这套系统用的是 JWT),跨域时必须设置withCredentials: true,否则浏览器不会携带 Cookie,登录状态就会莫名其妙丢失。
6.4 Vue3 打包后页面空白
这个问题多数是资源路径不对。Vite 打包时默认资源路径是绝对路径/assets/xxx,如果前端部署在域名子目录下,资源会全部 404 导致页面空白。解决办法是在vite.config.js里设置:
export default defineConfig({ base: './' })把base改成相对路径后再打包,部署到任意子目录都能正常访问。我在实际部署时就因为这个配置问题浪费了大半天时间,也是排查到网络请求里的 JS 文件 404 才反应过来。
7. 这套系统的扩展方向与个人建议
我个人实际操作下来,这套系统最值得扩展的方向有几个。
第一个是接入真实的支付渠道。目前系统里是模拟支付逻辑,也就是用户点击支付后直接进入待确认状态,如果要落地商业运营,建议接入微信支付或支付宝。接入时重点处理的就是支付回调的幂等性问题,必须通过订单号和交易流水号双重校验,防止用户重复支付成功后订单状态被重复更新。这个项目在订单状态机上已经预留好了待付款到待确认的过渡路径,加支付网关并不难。
第二个是后台数据看板的增强。目前看板覆盖订单量、销售额、用户数这些基础指标,如果要做到更高级的运营分析,可以用 MySQL8.0 的窗口函数按周、按月统计趋势,甚至可以记录每个面料的被选择次数,用于指导采购备货。
第三个是消息通知。订单状态流转时,目前只有用户在网页端能看到,后续扩展可以考虑接入短信或者微信模板消息,用户不用天天刷新页面等进度。实现思路是在订单状态更新的地方发一个异步事件,由消息服务消费并推送。
最后再分享一个我做这个项目的体会:源码里的东西都是别人踩过坑后沉淀下来的,真正想学会这套系统的核心,一定要自己把数据库脚本执行一遍,从前到后点一遍功能,然后改一个功能点试试。我每次接手新项目都是这个流程,先跑起来,再改起来,最后才能理解当初的设计逻辑。西服定制这个领域需求很垂直,但它的订单流转和定制参数模型的思路,完全可以迁移到衬衫定制、礼服租赁、甚至任何需要多维度选配的商品售卖场景里。把这套模型吃透,扩展成其他行业系统只是时间问题。