1. 星之语这个项目到底在做什么:明星周边电商的真实业务拆解
先聊点实际的。很多人看到"明星周边产品销售网站"第一反应是:这不就是个普通商城吗?商品管理、购物车、下单、支付四大件,找个开源商城改改不就行了?
我当初也这么想,真动手做"星之语"之后才发现,明星周边这个垂直品类跟卖3C数码、卖服装完全是两套玩法。最核心的差异有三个:
第一,商品形态极度非标准化。专辑、海报、手办、应援棒、小卡、舞台服装复刻,每种周边的属性维度完全不同。手办有材质、高度、限定编号,小卡有版本、拍摄场次、成员单人还是团体,光一张专辑就能拆出普通版、特别版、签售版、平台特典版。你用"名称+价格+库存"这种通用字段去建表,后台录入员能崩溃。
第二,预售和限时抢购是常态。明星周边不像普通商品那样稳定有货,大部分是粉丝团购、预售、按销量分批出货。订单系统必须支持先付款后发货、尾款补款、批次发货这种电商里相对边缘的流程。
第三,围绕"人"的营销属性极强。普通商城按分类逛,周边商城按明星逛。首页要有人气明星榜、新周边速递、粉丝热推专区,商品详情页必须展示关联明星的全部周边。这直接决定了前端信息架构和数据库表结构的设计方向。
回到"星之语"这个项目本身,它是一个标准的前后端分离单体应用,技术栈是SpringBoot + Vue3 + MyBatis + MySQL。整个系统的业务边界可以拆成两条线:
前台用户端:
- 用户注册登录(手机号+验证码,JWT鉴权)
- 明星专区浏览、商品按分类/关键词检索
- 商品详情(多图展示、规格选择、库存展示)
- 购物车管理、下单结算、订单状态跟踪
- 个人中心(收货地址、我的订单、我的收藏)
后台管理端:
- 明星管理(绑定商品、维护热度排序)
- 商品管理(多规格SKU、库存、上下架)
- 订单管理(发货、退款、修改收货信息)
- 用户管理(角色权限、账户状态)
- 轮播图/运营位管理、销售统计
这套功能清单定下来,整个项目的工作量其实已经超过80%的开源电商模板了。它不是应付毕业设计那种"能跑就行"的东西,而是按真实上线标准来设计的。
2. 为什么选SpringBoot + Vue3 + MyBatis + MySQL这套组合
技术选型这部分,网上争论很多,我直接说结论:这套组合不是最潮的,但它是当前中小型前后端分离项目里试错成本最低、最不容易把自己绕进去的组合。
2.1 后端用SpringBoot而不是Spring MVC,省掉的是配置心智
SpringBoot的价值不在于功能上比SpringMVC多什么,而在于它把"让项目跑起来"这件事的成本降到了最低。内置Tomcat、自动配置、starter机制,让开发人员能把精力放到业务代码上,而不是折腾web.xml、配置文件里那些繁琐的bean声明。
我在一些用SSM框架的老项目里被虐过,最崩溃的是环境搭建阶段就花了一整天:Spring版本和Jackson版本冲突、MyBatis的mapper扫描路径配错、事务管理器忘了注入。SpringBoot通过统一版本管理(BOM)和自动配置把这些问题基本消灭了。比如引入spring-boot-starter-web后,Jackson、Tomcat、参数校验这些基础组件全都自动装配好,你只管写接口就行。
"星之语"项目使用的是SpringBoot 2.7.x版本。这里有一个很实际的经验:别一上来就追最新的SpringBoot 3.x。3.x底层是Jakarta EE规范,很多老牌第三方库(包括部分MyBatis增强插件、代码生成器)还在适配期,遇到问题网上的解决方案也多基于2.x。等3.x生态彻底成熟了再迁移也不迟。
2.2 Vue3的价值是组合式API带来的组织效率
Vue2时代,一个复杂的商品详情页组件会变得很长,data、methods、computed、watch混在一起,不同功能的代码逻辑被拆散到各自的选项中。Vue3的组合式API(Composition API)把"同一个业务功能的所有逻辑"收拢到一块儿,可读性和复用性都好了很多。
我在"星之语"项目里用setup语法糖编写组件,最典型的例子是购物车逻辑:把购物车的数据加载、增删改数量、选中状态、结算勾选抽成一个useCart()组合函数。页面组件里只需要一行const { cartList, updateCount, checkedItems } = useCart(),代码量肉眼可见地减少,而且这个逻辑可以被购物车页、商品详情页、结算页共用。
当然,Vue3搭配Vite开发服务器的热更新体验,比Vue2配Webpack快得不是一点半点,"秒级刷新"在开发体验上优势很明显。
2.3 MyBatis的半自动ORM特性,写SQL不被框架绑架
项目里没用MyBatis-Plus,刻意保留了原生MyBatis。原因很简单:这个系统的查询场景足够复杂,明星周边网站的多条件动态查询非常多——按明星筛选、按品类筛选、按价格区间筛选、按销量排序、组合条件叠加,这些场景下原生MyBatis的<if>动态SQL极其灵活,你能精确控制每一条SQL的生成逻辑。
MyBatis-Plus那种QueryWrapper链式查询确实省代码,但在多表关联、子查询、复杂统计的场景里,你会发现最终还是要手写SQL,而且模板方法查出来的字段映射容易出坑。反过来,原生MyBatis配合<resultMap>做结果映射,任何查询都能精准控制。
有一个体会很关键:写业务查询前,先用SQL直接在Navicat里验证,确认无误后再搬进Mapper.xml。这样排错成本最低,也容易发现SQL本身的性能问题。
2.4 MySQL 5.7/8.0的选择没有悬念
这个项目用的MySQL 5.7有一批人建议直接上8.0。我的看法是:独立开发项目直接用8.0就好,如果部署环境是老服务器担心兼容性,5.7也完全够用。重点是创建连接串时必须显式指定serverTimezone=Asia/Shanghai,否则Java连接MySQL时会出现时区报错。另外8.0默认的caching_sha2_password认证插件,老版的mysql-connector-java驱动连不上,记得使用com.mysql.cj.jdbc.Driver并且驱动版本要大于5.1.47。
3. 后端架构与核心模块设计:从分层到鉴权的落地细节
3.1 包结构与分层逻辑
"星之语"后端是单模块Maven项目,包结构这样划分:
com.xingzhiyu ├── common │ ├── result(统一返回结果Result、ResultCode枚举) │ ├── exception(业务异常类、全局异常处理器) │ ├── utils(JwtUtil、MD5加密、日期工具) ├── config(跨域配置、WebMvcConfig、MyBatis配置) ├── controller ├── service(接口+impl) ├── mapper ├── entity ├── dto(接收前端参数的专用对象) ├── vo(返回前端数据的专用对象)这里的核心思想是实体分层:entity对应数据库表结构,dto接收前端入参,vo定义接口返回结构。一定不要混用。很多新手图省事,前端传什么全用一个Map接收,接口返回也直接丢实体类,短期速度快,后期维护简直是灾难——字段暴露安全隐患、文档无法自动生成、前端对接全靠猜。我在项目里连地址修改这种小接口都单独建了AddressDTO,时间长了你会发现前期这点"麻烦"是值得的。
3.2 统一返回结果与全局异常处理
前后端分离项目,接口返回格式必须统一,否则前端Axios响应拦截器没法做统一处理。"星之语"项目的统一返回结构是这样设计的:
@Data public class Result<T> { private Integer code; // 200成功,其他为失败 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }与之配套的是全局异常处理器。我在ControllerAdvice里捕获三类异常:业务异常(手动抛出的,提示给用户的友好信息)、参数校验异常(@Validated触发的MethodArgumentNotValidException)、兜底Exception(记录日志并返回"系统繁忙")。
这样做最大的好处是Controller层代码极其清爽,业务逻辑里发现了问题直接throw new BizException("库存不足"),不用每个方法写try-catch,前端拿到的永远是结构化错误信息。
3.3 JWT登录鉴权的完整链路
"星之语"项目的用户鉴权用的是JWT(JSON Web Token),流程是:登录成功 → 后端生成Token并返回 → 前端存入localStorage → 每次请求在请求头带Authorization: Bearer xxx→ 拦截器解析校验 → 放行或拒绝。
关键实现点有三个:
第一个,Token里存什么。我设计的是:用户ID、用户名、角色(user/admin)、过期时间。一定不要存敏感信息(手机号、密码),因为JWT的Payload只是Base64编码,任何人拿到Token都能解码看到内容。但这个Token本身是签名防篡改的。
第二个,拦截器怎么排除白名单。登录注册接口、首页轮播图、商品列表、商品详情这些不需要登录就能访问的接口需要放行。我的做法是在配置类里维护一个白名单list,配合HandlerInterceptor使用。这里有个经验:静态资源也要放行,否则前端通过后端代理图片时会被拦截器拦下来。
第三个,解析Token后怎么获取用户信息。我在拦截器中解析出用户ID后,用ThreadLocal存储当前登录用户对象,在Controller的任意位置通过UserContext.getUserId()获取。请求结束后在afterCompletion中务必执行UserContext.clear(),否则高并发下用户数据会串。
3.4 MyBatis的配置与手写SQL的核心场景
"星之语"项目在application.yml里MyBatis配置如下:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xingzhiyu.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImplmap-underscore-to-camel-case: true这个配置强烈建议开启,数据库字段create_time可以自动映射到实体类的createTime,省掉一堆手动resultMap。
最复杂的SQL集中在商品多条件检索上。明星周边的筛选条件经常是动态组合的——用户选了"王一博"、"手办"、"价格300-500"、"按销量排序",对应的Mapper就是多条件动态查询:
<select id="selectProductByCondition" resultType="com.xingzhiyu.vo.ProductVO"> SELECT p.id, p.name, p.main_image, p.price, p.sales_count, s.name AS star_name, c.name AS category_name FROM product p LEFT JOIN star s ON p.star_id = s.id LEFT JOIN category c ON p.category_id = c.id <where> <if test="starId != null"> AND p.star_id = #{starId} </if> <if test="categoryId != null"> AND p.category_id = #{categoryId} </if> <if test="minPrice != null"> AND p.price >= #{minPrice} </if> <if test="maxPrice != null"> AND p.price <= #{maxPrice} </if> <if test="keyword != null and keyword != ''"> AND (p.name LIKE CONCAT('%', #{keyword}, '%') OR s.name LIKE CONCAT('%', #{keyword}, '%')) </if> </where> <if test="sortType == 'sales'"> ORDER BY p.sales_count DESC </if> <if test="sortType == 'price_asc'"> ORDER BY p.price ASC </if> <if test="sortType == 'newest'"> ORDER BY p.create_time DESC </if> </select>这段SQL有两个细节要注意:一是<where>标签会自动处理第一个条件前缀的AND,不用手动where 1=1这种丑写法;二是排序字段不能用#{},因为预编译占位符只适用于值,所以排序走<if>判断,防止SQL注入。
分页方面,项目使用的是PageHelper插件,配置了reasonable: true,这样页码越界时插件会自动修正,不用自己写边界逻辑。
4. 前端Vue3工程化实践:从Vite构建到Axios封装的完整链路
4.1 项目初始化的版本坑
创建Vue3项目我建议用Vite而不是Vue CLI。Vite 2.x要求Node.js版本>=12.2.0,但如果用Vite 4.x或5.x,Node版本最好在16+。我在开发"星之语"时用的是Vite 4 + Vue 3.3 + Element Plus 2.3,整体生态稳定。
一个实际的坑:如果服务器上Node版本较老(比如12.x),执行npm install会出现大量依赖报错,不要硬扛,直接装个nvm进行多版本管理,项目目录里维护好.nvmrc文件指明版本号。
4.2 前端目录结构怎么组织
"星之语"前端结构如下:
src ├── api(按业务模块拆分的接口请求:auth.js、product.js、cart.js、order.js) ├── assets(静态资源) ├── components(通用组件:GoodsCard、Pagination、StarAvatar等) ├── router(路由配置+路由守卫) ├── stores(Pinia状态:userStore、cartStore) ├── utils(request.js、auth.js) ├── views(页面级组件) │ ├── home(首页) │ ├── product(商品列表/详情) │ ├── cart │ ├── order │ ├── user(个人中心) │ └── admin(后台管理) ├── App.vue ├── main.js4.3 Axios封装与请求拦截器
前后端分离项目里,Axios封装是第一道基础设施。我在utils/request.js里做了这三件事:
第一,统一baseURL和环境区分。开发环境用Vite代理转发到后端8080端口,生产环境通过环境变量VITE_API_BASE_URL配置,避免把接口地址写死在代码里。
第二,请求拦截器注入Token。从localStorage读取Token,如果存在就设置请求头Authorization: Bearer ${token}。
第三,响应拦截器统一处理返回。约定后端返回结构为{code, message, data},code===200直接返回data给业务代码;code===401表示Token过期,清空本地用户信息并跳转登录页;其他code统一ElMessage.error(message)提示。
登录接口的调用因此变得非常清爽:
const login = async (loginForm) => { const res = await api.post('/auth/login', loginForm) userStore.setToken(res.token) userStore.setUserInfo(res.userInfo) router.push('/') }4.4 路由守卫控制页面访问权限
"星之语"项目有普通用户和管理员两种角色,路由守卫需要做双重判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) return } if (to.meta.requiresAdmin) { const role = localStorage.getItem('role') if (role !== 'admin') { next('/') return } } next() })meta.requiresMeta标记需要登录的路由,meta.requiresAdmin标记管理端。这里我踩过的一个坑是:刷新页面时Pinia的store会被清空,而localStorage还留着用户信息,所以刷新后需要重新从localStorage恢复用户状态,否则路由守卫会误判登录态。
4.5 购物车与订单的状态流转设计
购物车模块是前端里最容易写乱的部分。我设计的状态流转是:加入购物车时只把商品ID、规格、数量、选中状态存在前端(用Pinia + localStorage持久化),真正下单时把购物车选中项一次性提交到后端生成订单。
这种设计的好处是购物车本身不需要后端表,减轻了服务端压力,而且用户未登录状态下也能加购物车,登录后依然保留。等用户提交订单的时候,后端再校验库存、计算金额、生成订单记录。
5. 数据库设计:明星周边商城的数据模型到底该怎么建模
5.1 核心表结构拆解
"星之语"项目的数据库一共12张表,我把最核心的几张讲清楚。
用户表(user)
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名/手机号', `password` varchar(255) NOT NULL COMMENT 'MD5加密密码', `nickname` varchar(50) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `role` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0普通用户 1管理员', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常 0封禁', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码加密用的是MD5加盐方案,即MD5(password + 固定盐值)。我知道有人会说BCrypt更安全,但中小型项目MD5加盐已经够用,重点是绝不能明文存密码。
商品表(product)
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `star_id` int(11) NOT NULL COMMENT '关联明星ID', `category_id` int(11) NOT NULL COMMENT '关联分类ID', `name` varchar(100) NOT NULL, `subtitle` varchar(200) DEFAULT NULL COMMENT '副标题', `main_image` varchar(255) NOT NULL COMMENT '主图', `detail` text COMMENT '富文本详情', `price` decimal(10,2) NOT NULL COMMENT '售价', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价', `stock` int(11) NOT NULL DEFAULT '0', `sales_count` int(11) NOT NULL DEFAULT '0', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_star_id` (`star_id`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意两个设计:一是金额用decimal(10,2),不用double或float,浮点数在计算时会产生精度误差,这在订单金额计算里是致命的;二是对star_id和category_id建了索引,因为首页和列表页的查询都是按明星和分类过滤的。
商品规格表(product_sku)
明星周边商品的规格很杂,我在"星之语"里设计了独立的SKU表:
CREATE TABLE `product_sku` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL, `spec_info` varchar(255) NOT NULL COMMENT '规格描述,如"普通版/单张海报"', `price` decimal(10,2) NOT NULL, `stock` int(11) NOT NULL DEFAULT '0', `sku_image` varchar(255) DEFAULT NULL COMMENT '规格专属图片', PRIMARY KEY (`id`), KEY `idx_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;商品详情页选择不同规格时,实时切换价格和库存,前端数据都由这个SKU表提供。
订单主表(order)与订单明细表(order_item)
订单采用主表+明细表的经典设计。主表只存订单编号、用户ID、总金额、状态、收货信息快照;明细表存商品ID、SKU、单价、数量、小计金额。
这里有一个很多人忽略的点:收货地址要在订单表里做快照。因为用户下单后可能修改默认地址,如果订单查询时去读最新的用户地址,历史订单信息会被篡改。我把收货人姓名、电话、地址直接冗余进订单表,订单就变成了不可变记录。
订单状态我用整数类型存储:0待付款 1待发货 2已发货 3已完成 4已取消,后端用状态机约束流转,比如只有0待付款状态才能取消,只有1待发货才能发货。
明星表(star)与分类表(category)
明星表和分类表是周边商城区别于普通商城的设计核心。明星表有name、avatar、fans_count(热度,用于首页排序)、description。分类表做了parent_id自关联,支持"专辑-普通版-特别版"这种多级分类。
这种"以明星为第一入口,以分类为第二入口"的信息架构,体现在首页设计上就是明星排行+分类导航两个模块平级展示。
5.2 购物车为什么不建表
前面提到购物车数据存在前端,数据库里没有购物车表。很多人会质疑这一点,我的理由很实在:
- 周边电商的购物车只是临时存储,用户未必会结算,存数据库纯属浪费空间
- 前端用Pinia + localStorage持久化,体验更流畅,加购不经过网络请求
- 真正需要落库的信息(下单商品、数量)在订单明细表里完整保留
但是前端localStorage存购物车有个边界条件:用户分页刷新时Token过期会导致强退,购物车数据不会丢(localStorage),但会失去登录状态,下单时需要重新登录,购物车数据不会丢(localStorage),但会失去登录状态,下单时需要重新登录。这个体验可以在购物车结算按钮上提示用户"登录后结算",再做一次引导即可。
6. 前后端联调与部署中的真实踩坑记录
6.1 跨域问题:不是配一次就能一劳永逸
前后端分离项目最经典的坑就是跨域。我在"星之语"项目里开发环境采用Vite代理解决:
// vite.config.js server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }开发环境建议用代理而不是在后端开启全放行的CORS配置。原因有两点:一是代理对前端代码零侵入,接口路径保持/api/xxx;二是生产环境一旦忘了关闭后端的CORS全放行,安全风险很大。
但生产环境还是要考虑后端的CORS配置。我的做法是在后端配置类里addCorsMappings,只放行指定的前端域名来源,并限定allowedMethods为GET,POST,PUT,DELETE,OPTIONS。
6.2 图片上传与静态资源映射
明星周边网站图片量大,商品主图、规格图、轮播图、用户头像。开发阶段我把图片存在本地服务器目录,通过一个上传接口接收MultipartFile,落到D:/upload/目录,然后配置了静态资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourcePaths("file:D:/upload/"); }这样图片访问路径就是http://localhost:8080/upload/xxx.jpg,数据库里只存相对路径即可。这个方案有一个隐患:服务器重启后如果上传目录配置错误,图片会404。所以建议把上传路径抽到配置项里,部署时改配置文件就行。
真要上线,建议把图片迁到云OSS/COS,配合CDN加速,本地存储方案只适合开发和内网环境。
6.3 Nginx部署配置与history路由404问题
Vue3项目打包后是纯静态文件,部署到Nginx时最大的坑是前端路由用history模式,刷新页面会404。因为前端路由是JS虚拟的,刷新时浏览器直接请求了/product/detail/123,但服务器上根本没这个物理路径。
Nginx配置需要加上try_files:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; } location /upload/ { proxy_pass http://localhost:8080/upload/; } }这套配置同时处理了三件事:前端history路由回退到index.html、后端接口转发、图片资源转发。其中location /api/的proxy_pass末尾要带/,表示把请求原样转发给后端,否则会丢掉/api前缀导致404。
6.4 MySQL连接串里的两个经典参数
第一次把后端部署到服务器时,如果Java连不上MySQL,九成是这两个问题:
第一,时区问题。报错信息类似The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,解决办法是在连接串上加serverTimezone=Asia/Shanghai。
第二,SSL警告。MySQL 8默认开启SSL,但本地测试环境没有配置证书,连接时会报SSL connection error或大量警告日志。解决办法是连接串加useSSL=false。注意这里如果数据库配置了SSL证书,强制关掉会影响安全,所以更推荐的写法是useSSL=false&allowPublicKeyRetrieval=true,后者解决8.0版本的公钥检索认证问题。
下面是我项目里最终使用的连接串,可以直接抄:
jdbc:mysql://localhost:3306/xingzhiyu?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true6.5 金额精度与库存超卖的隐性坑
订单金额计算如果全用Java的double或float,总金额会出现类似199.99999999的值。我在后端计算金额时统一用BigDecimal,加法用add,乘法用multiply,并且divide时指定精度和舍入模式。
库存扣减是高并发的另一个雷区。我的做法是更新时加条件判断:
UPDATE product SET stock = stock - 1, sales_count = sales_count + 1 WHERE id = #{productId} AND stock > 0;stock > 0这个条件保证不会超卖。同时检测受影响行数,如果为0说明库存不足,抛出"手速太快啦,库存不足"的业务异常。这就是典型的乐观锁思想,不用分布式锁也能扛住常规的并发压力。
7. 从"能跑"到"好用":这个系统的进级优化方向
如果只是复现"星之语",当前的功能和架构已经完整,但要真正运营起来,有几个明显的优化方向值得投入。
第一个方向是热搜榜和推荐逻辑。当前首页明星热度是人工维护的fans_count字段,实际上可以基于搜索关键词频率、商品收藏量、订单销量做一个热度分值,用定时任务每天凌晨计算一次,让榜单更真实。
第二个方向是Redis缓存热点数据。商品详情页是访问最密集的接口,每次都查MySQL虽然能扛住,但浪费资源。可以把商品详情、明星列表、首页轮播这些读多写少的数据缓存到Redis,设置合理的过期时间,接口响应能从200ms降到20ms级别。注意缓存更新策略要处理,商品修改后删除对应缓存即可。
第三个方向是订单超时自动取消。当前待付款订单需要用户手动取消,真实运营场景下加一个定时任务,扫描超过30分钟未付款的订单自动置为取消状态,并回滚库存。这个功能用Spring的@Scheduled就能实现,不需要引入消息队列那么重的组件。
第四个方向是前后端分离之后的文档管理。接口多了以后,前后端对接全靠口头沟通效率极低。可以引入SpringDoc(OpenAPI 3)生成接口文档,配合Knife4j的界面,后端写完接口自动生成文档,前端直接照着对接,省掉反复询问字段含义的沟通成本。
我个人在实际操作中体会最深的一点是:这个系统的代码量并不算大,真正花了最多时间的是字段设计和状态流转。明星周边这种强垂直领域的电商,业务规则比技术实现更值得花心思。如果你拿这套源码去学习,建议不要急着跑通功能,而是先把数据库表结构、订单状态机、前后端数据流这三条主线理清楚,再动手去改需求,收获会大很多。
最后再分享一个小技巧:本地开发时后端接口地址和前端代理配置,尽量做成环境变量驱动,不要在每个文件里硬编码。把VITE_API_BASE_URL、server.port、upload.path这类配置全部集中管理,部署到不同环境时只改一份配置,能少掉不少头发。