☰
SpringBoot2+Vue3民宿租赁系统:全栈源码、数据库设计与部署指南
2026/10/2 14:02:35 网站建设 项目流程

懒人看源码最怕什么?不是代码看不懂,而是看完不知道往哪儿改。这套民宿租赁系统我整理出来的时候,刻意把 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 全栈常踩的坑都留了注释,同时把文档补全到"照着文档就能把项目跑起来"的程度。文章会从技术选型、数据库设计、核心业务流程、环境搭建、部署上线以及常见问题几个方面展开,尽量把我在搭这套系统时的真实思路说清楚。

民宿租赁这个场景其实很适合做全栈练手项目。它的业务闭环完整,从房源发布、房间浏览、下单预订、支付回调到订单管理、评价体系、经营统计,每一环都是真实的业务逻辑,不是简单的增删改查。系统拆成前台用户端、民宿老板端、平台管理端三种角色,权限边界清晰,数据关系也够复杂。无论你是毕设选型、想学前后端分离项目,还是准备用这套源码快速做自己的民宿 SaaS 原型,拿它当底座都不会觉得虚。

1. 项目定位与技术选型思考

1.1 这套系统到底做了什么

整个系统按角色分为三个入口:游客或注册用户可以搜索房源、浏览房间详情、提交订单、在线支付、入住后退房评价;民宿经营者可以管理自己的民宿信息、房型价格、每日库存、处理订单、查看经营统计;平台管理员负责审核民宿、管理用户、处理投诉评价、系统运营。三端共用一套后端 API,通过 JWT 做的用户身份标识来区分权限。

业务上最核心的闭环是"下单-支付-核销-评价"这条链路。下单时系统要校验所选日期范围内房间是否有库存,下单后锁定库存,支付成功后减少可售库存,到店入住后状态变为已入住,退房后释放库存并进入评价环节。我见过很多人做民宿系统时把这一步跳过,只在订单表里存一个房间ID,不考虑日期冲突,结果同一个房间同一晚被订出去两次,这就是典型的库存设计没想清楚。

1.2 后端为什么锁定 SpringBoot2 + MyBatis-Plus

现在 Spring Boot 3 已经出了很长时间,我还是坚持用 SpringBoot2.7.x,原因很实际:国内大量线上环境和教程资料还停留在 JDK8 + Spring Boot 2 的组合,部署环境不用折腾,网上报错也能搜到非常成熟的解决方案。SpringBoot2.7 属于 2.x 的最后一个稳定小版本,既能享受多年迭代后的稳定性,又保留了大家最熟悉的配置习惯,拿来跑业务系统完全够用。

MyBatis-Plus 在这个项目里承担了几乎所有单表 CRUD 的底层实现。它内置 BaseMapper,像selectById、selectList、insert这类方法不需要手写 SQL;条件构造器LambdaQueryWrapper能避免在 Java 代码里拼字符串 SQL,配合@TableLogic逻辑删除和@TableField(fill = FieldFill.INSERT)自动填充,能让代码量减少一半以上。对于业务开发者来说,重点应该放在库存、订单状态这些复杂逻辑上,而不是花时间写大量重复的 mapper 方法。

1.3 前端选择 Vue3 + Vite 的真实理由

前端选 Vue3 不是因为"新",是因为 Vue3 的组合式 API(Composition API)确实更适合中后台系统。页面逻辑按业务功能组织,比如"房间筛选逻辑"是一个函数,"订单状态管理"是另一个函数,不再像 Vue2 的 Options API 那样把数据、方法、生命周期硬拆成三块。配合<script setup>,代码量能明显变少。

构建工具用 Vite 替代 Webpack,开发环境下冷启动基本在几百毫秒级,热更新更快,配置也比 Webpack 直观。状态管理选 Pinia 而不是 Vuex,因为 Pinia 对 TypeScript 支持更好,而且取消了 mutation 概念,store 里直接改 state,心智负担低很多。UI 组件库用的 Element Plus,表单、表格、分页、对话框这些后台管理高频组件开箱即用,不用自己拿 div 死磕样式。

1.4 MySQL8.0 带来哪些便利和限制

MySQL 8.0 相比 5.7,默认字符集已经是utf8mb4,不用再担心 emoji 存不进去。更重要的是它支持窗口函数、公用表表达式(CTE),做营收统计、环比计算这类需求时,SQL 可以写得更简洁。窗口函数在我这个系统的经营统计模块里帮了大忙,比如计算每个房源近三个月的订单量排名,用ROW_NUMBER() OVER (PARTITION BY homestay_id ORDER BY ...)一下子就能搞定。

但 MySQL 8.0 也埋了不少坑。8.0 默认身份认证插件是caching_sha2_password,如果用的 JDBC 驱动还是 5.x,启动后端会直接报Unable to load authentication plugin,所以项目里必须把mysql-connector-java升到 8.0.3x 版本。此外 8.0 对时区更敏感,连接串里忘了设置serverTimezone,控制台就会蹦出比业务报错更吓人的一长串异常。这些细节在后续章节会专门展开。

1.5 附带的文档里藏着哪些关键信息

这个源码里的docs目录不是凑数的,里面有四部分内容:数据库初始化脚本homestay.sql、接口文档(Postman 导出集合 + Markdown 接口说明)、环境搭建指南、部署上线手册。文档里还会写明初始账号和密码,比如管理员账号admin、测试房东账号owner001、普通用户账号user001,方便拿到代码后第一时间登录体验完整流程。

我整理文档的习惯是"按操作顺序写",从安装 JDK8、Docker 装 MySQL、导入 SQL、启动后端、启动前端,一直到 Nginx 部署,每一步都对应具体命令和预期结果。这样的文档对新手很友好,照着走一遍就能跑起来;对有经验的开发者来说,也能很快定位到项目里各个模块对应哪些文件,不用靠猜。

2. 数据库设计:先把状态表和库存表想清楚

2.1 核心表关系与字段设计

系统主要表我整理成下面的关系,每个表都加了逻辑删除字段deleted、创建时间create_time、更新时间update_time三个公共字段。

表名核心作用关键字段
sys_user用户、房东、管理员统一存储username, password, role, phone, status
homestay民宿基础信息owner_id, name, city, address, status, images
homestay_room民宿下的房间/房型homestay_id, room_name, price_per_night, max_people
homestay_schedule房间每日库存与价格room_id, biz_date, inventory, locked_inventory
order_info预订订单主表order_no, room_id, check_in_date, check_out_date, total_amount, status
comment订单完成后的评价order_id, homestay_id, user_id, content, rating
sys_dict数据字典,存城市、房型等dict_type, dict_label, dict_value

主键我统一用的 BIGINT 雪花 ID,由 MyBatis-Plus 的ASSIGN_ID策略自动生成。之所以不用数据库自增 ID,是因为订单号、民宿ID会出现在前端的 URL 和接口参数里,自增 ID 容易让竞争对手直接从订单号推断出平台每天的单量。金额字段必须用DECIMAL(10,2),严禁用double,这是所有涉及钱的系统的铁律。状态字段用TINYINT,通过数据字典维护含义,避免在代码里散落一堆魔法数字。

2.2 民宿房间库存:为什么需要一张“每日库存表”

很多初学做民宿系统的做法是在房间里放一个stock字段,下单就stock - 1,退房就stock + 1。这在酒店场景勉强能用,但民宿预订通常按"入住日期-离店日期"来选,一套房源今天有库存不代表明天也有,要支持连续入住 N 晚,就必须知道每一天的库存情况。

所以我在homestay_room和order_info之间加了一张homestay_schedule每日库存表,每条记录对应某个房间某一天的可售数量。比如(room_id=101, biz_date='2025-05-01', inventory=3)表示 101 号房在 5 月 1 日还剩 3 间可售。用户下单 5月1日到5月3日,系统就把 5月1日、5月2日两天的记录都锁住。这样做带来的好处是,以后想支持"同一个房型拆成多间出售"或者"周末涨价"都很方便,只需在 schedule 表里按日期设置价格即可。

下单锁定库存的关键操作是用一条 UPDATE 语句原子扣减,而不是先在 Java 里查询再更新:

UPDATE homestay_schedule SET locked_inventory = locked_inventory + 2 WHERE room_id = 101 AND biz_date BETWEEN '2025-05-01' AND '2025-05-02' AND (inventory - locked_inventory) >= 2

如果影响行数不等于 2(因为跨了两晚),说明至少有一天库存不足,直接在 Service 层抛异常回滚。这条 SQL 是在数据库层面完成的,天然能做到并发安全,比我加分布式锁简单可靠得多。

2.3 订单状态机与金额精度控制

订单状态我设计了六种,分别对应一套明确的流转规则:

状态码含义进入条件允许流转到
0待支付用户提交订单成功1、4
1已支付支付回调成功2、5
2已入住到店核销/房东操作3
3已退房到期退房/房东操作已结束
4已取消用户取消/超时未支付已结束
5退款中支付后退款申请6
6已退款退款处理完成已结束

为什么要把状态机写清楚?因为后续做接口时,几乎所有业务校验都要基于当前状态判断。比如已取消的订单就不能再支付,已入住的订单不能申请退款,已退房的订单才能发起评价。如果不提前梳理状态流转,后端 Service 每个方法里都散落一堆if (order.getStatus() == 1),时间一长必然出现状态错乱。

金额部分,订单总价在创建时根据price_per_night * 入住晚数计算,支付回调后只做金额比对,绝不直接使用前端传过来的total_amount。支付表单独记录支付流水号、回调原始报文、支付状态,保证后续对账时有据可查。

3. 核心业务流程与后端实现要点

3.1 登录认证:BCrypt + JWT 守卫接口

密码存储用 BCrypt 加密,不要用 MD5。MD5 加不加盐都挡不住现在的大字典跑表,BCrypt 每次加密结果不同,自带 salt,破解成本高出几个量级。Spring Security 里抽出BCryptPasswordEncoder单独用即可,不需要把整个 Spring Security 引进来,避免学习成本和配置复杂度失控。

用户登录成功后签发 JWT,我把用户 ID 和角色放进 token 的 claim 里,有效期设为 24 小时:

Algorithm algorithm = Algorithm.HMAC256("homestay-secret"); String token = JWT.create() .withSubject(String.valueOf(user.getId())) .withClaim("role", user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) .sign(algorithm);

后端写一个 HandlerInterceptor,在preHandle里从Authorization: Bearer xxx取出 token,解析成功就把用户 ID 放入当前请求上下文,解析失败直接返回 401。拦截器注册时配置白名单,比如/api/auth/login、/api/homestay/list、/api/room/detail这些公开接口不需要登录也能访问,但下单、支付回调、后台管理等接口必须校验身份。

3.2 房源搜索:条件构造器与日期冲突查询

首页搜索逻辑是典型的多条件动态查询。用户可能只选了城市,也可能选了城市+入住日期+人数+价格区间,还可能按价格排序、按评分筛选。这种场景用 MyBatis-Plus 的LambdaQueryWrapper最合适,条件按需拼接:

LambdaQueryWrapper<Homestay> wrapper = Wrappers.lambdaQuery(); wrapper.eq(StringUtils.hasText(query.getCity()), Homestay::getCity, query.getCity()) .ge(query.getMinPrice() != null, Homestay::getPrice, query.getMinPrice()) .le(query.getMaxPrice() != null, Homestay::getPrice, query.getMaxPrice());

真正复杂的是"搜索指定日期区间内有房"的民宿。我的做法是先查出所有满足基础筛选的民宿 ID,再把民宿 ID 和日期范围传给一个自定义 SQL,查询这些民宿在目标日期区间内是否还有库存:

SELECT r.id FROM homestay_room r WHERE r.homestay_id IN (...) AND NOT EXISTS ( SELECT 1 FROM homestay_schedule s WHERE s.room_id = r.id AND s.biz_date BETWEEN #{checkIn} AND DATE_SUB(#{checkOut}, INTERVAL 1 DAY) AND s.inventory - s.locked_inventory &lt;= 0 )

用NOT EXISTS的含义是:只要目标日期区间内有一天没库存,这个房间就不能出现在搜索结果里。注意这里离店日期要减一天,因为住 5月1日到5月3日,实际占用的只是 1号、2号两晚。

3.3 下单锁定库存:事务、更新条件与并发

下单接口是系统里并发压力最大、也最容易出 bug 的地方。正确流程分几步:

  1. 前端把房间ID、入住日期、离店日期、联系人信息POST到后端;
  2. 后端查到房间,计算晚数、总价;
  3. 开事务,先执行前文那条UPDATE homestay_schedule原子扣减库存;
  4. 更新影响行数不对则抛异常回滚;
  5. 插入order_info订单记录;
  6. 提交事务,返回订单号。

这里最关键的是第三步必须独立塞进事务里,并且 UPDATE 语句的 WHERE 条件里带上inventory - locked_inventory >= #{nights}作为乐观判断。如果两个用户同时下单,数据库的行锁会保证只有一个 UPDATE 先执行成功,第二个执行时库存已经被扣掉,影响行数为 0,事务回滚,请求直接失败。不需要引入 Redis 分布式锁,也避免了锁过期导致的超卖。

订单号生成我也顺便说下:不要用时间戳+随机数,容易撞。我用"日期 + 用户ID后四位 + 雪花算法后八位"拼出来,比如202505011012330123456,看起来长但可读性好,还能从订单号反推下单日期。

3.4 后台管理模块的统计怎么实现

后台管理端包含民宿审核、用户管理、订单管理、评价管理,全做出来是标准的 CRUD + 状态筛选,这里不展开。重点想分享统计查询。经营统计里有一个需求:民宿老板打开首页要看到三个数字——本月营收、本月订单量、当前待处理订单,还要能看到最近七天订单趋势。

本月营收用一条聚合 SQL 就能查出来:

SELECT IFNULL(SUM(total_amount), 0) FROM order_info WHERE status IN (1,2,3,5) AND pay_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')

订单趋势则按日期分组,注意 MySQL8.0 可以直接用DATE(pay_time)分组,配合DATE_FORMAT输出前端需要的YYYY-MM-DD格式。如果要做同比环比,窗口函数LAG()很香,它可以方便地取到上一周期的数据,这些在 MySQL5.7 里都得靠自连接实现,8.0 直接搞定。

4. 从零跑通前后端:环境搭建与配置实录

4.1 Docker 一条命令装好 MySQL 8.0

我推荐用 Docker 装 MySQL8.0,省去安装 MySQL 服务的各种环境冲突。一条命令就能跑起来:

docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e TZ=Asia/Shanghai \ -v /opt/mysql8/data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci

这里有两个细节:一是-e TZ=Asia/Shanghai必须加,不然容器默认 UTC,数据库里的now()会跟北京时间差 8 小时;二是字符集参数建议显式指定,虽然 MySQL8.0 默认是 utf8mb4,但collation如果不指定,表默认的排序规则可能是utf8mb4_0900_ai_ci,跟老库迁移出来的utf8mb4_general_ci不一致时 join 会报字符集冲突。启动完成后用docker exec -it mysql8 mysql -uroot -proot123验证连接是否正常。

4.2 后端项目搭建与 MyBatis-Plus 配置

后端用 Spring Initializr 创建,注意打包方式选 jar,Java 版本选 8。核心依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>

application.yml里三个地方容易写错。第一是数据库连接串必须带serverTimezone=Asia/Shanghai和useUnicode=true&characterEncoding=utf8;第二是要写driver-class-name: com.mysql.cj.jdbc.Driver,注意中间的cj,老教程写的com.mysql.jdbc.Driver在 8.0 驱动下虽然能兼容但不推荐;第三是配置好mapper-locations指向 XML 文件位置,默认如果放在resources/mapper下就要显式声明:

mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true

分页插件配置也是一个独立@Configuration类:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

这个配置容易漏掉,漏掉后最典型的表现是调用page()方法不报错,但返回的数据是全量的,根本没有分页。

4.3 Vue3 前端初始化与请求封装

前端我用 Vite 创建:

npm create vite@latest homestay-web -- --template vue cd homestay-web npm install npm install vue-router@4 pinia axios element-plus

目录结构按业务拆模块,不按文件类型堆一起:

src/ api/ // 接口封装,按模块分文件 components/ // 公共组件 layout/ // 登录后主框架(侧边栏+顶栏) router/ // 路由配置和守卫 stores/ // Pinia 状态 views/ // 页面 auth/ // 登录、注册 home/ // 前台首页、民宿列表、详情 order/ // 订单流程 admin/ // 后台管理

axios 封装是所有前后端项目的地基。请求拦截器里统一加 token,响应拦截器里处理状态码:

service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) service.interceptors.response.use( res => { // 约定后端返回 { code: 0, data: ..., message: 'ok' } if (res.data.code === 0) return res.data.data ElMessage.error(res.data.message) return Promise.reject(res.data) }, err => { if (err.response?.status === 401) { localStorage.clear() router.push('/login') } return Promise.reject(err) } )

这里的 401 跳登录逻辑必须配合路由守卫双保险,因为 token 过期时,接口先返回 401,前端跳登录页;而用户直接访问需要登录的页面时,路由守卫在进入页面前就要拦截校验,否则会闪一下页面。

4.4 前后端联调:跨域、Token 与接口规范

联调阶段最大的坑是跨域。我在后端统一设置了server.servlet.context-path=/api,也就是所有接口地址都带/api前缀。前端开发环境只需要在 Vite 里做代理:

// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求/api/order/create会被代理到http://localhost:8080/api/order/create,浏览器里没有跨域问题,前端代码里也只用写相对路径/api/order/create,生产环境部署后由 Nginx 做同样转发。后端的 CORS 配置可以不用加,开发和线上全部靠代理解决。

Token 传递规范建议统一:登录接口返回 token 后存入 localStorage,请求头统一用Authorization: Bearer <token>。不要图省事把 token 放在 URL 参数里,否则日志系统会记到完整 token,安全隐患很大。

5. 打包部署上线:Nginx + Jar 的经典组合

5.1 前端构建与 Nginx 路由配置

前端执行npm run build,产物是dist目录,放到服务器/opt/homestay-web下。Nginx 配置里有两个关键点。

第一,要用 history 模式路由必须配try_files,否则刷新页面就会出现 404:

server { listen 80; server_name your-domain.com; root /opt/homestay-web; index index.html; location / { try_files $uri $uri/ /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; } }

第二,前端资源建议开启 gzip,Vite 生产构建默认会压缩 JS 体积,但网络传输层再配一层 gzip 能让首屏加载快不少:

gzip on; gzip_min_length 1k; gzip_types text/plain text/css application/json application/javascript;

5.2 后端 Jar 包运行与进程守护

后端打包用mvn clean package -DskipTests,产物在target/下。简单运行是java -jar homestay-system.jar,但服务器重启或者 SSH 断开后进程就可能没了,所以我建议配一个 systemd 服务:

[Unit] Description=Homestay Server After=network.target [Service] ExecStart=/usr/local/jdk8/bin/java -jar /opt/homestay/homestay-system.jar Restart=on-failure User=homestay [Install] WantedBy=multi-user.target

启用后systemctl enable homestay实现开机自启,systemctl status homestay看运行状态,日志统一交给 journald 管理。生产环境如果机器内存有限,记得在启动命令上调过 JVM 参数,比如-Xms256m -Xmx512m,不要一把梭让 JVM 默认吃满内存。

5.3 数据库导入与日常备份

首次部署时执行:

mysql -uroot -p homestay < /opt/homestay/docs/homestay.sql

日常备份建议用计划任务,每天凌晨把 MySQL 数据导出来并保留最近 7 天:

#!/bin/bash DATE=$(date +%Y%m%d) mysqldump -uroot -p'root123' homestay > /opt/backup/homestay_$DATE.sql find /opt/backup -type f -mtime +7 -delete

备份脚本的密码写在命令行里会出现在进程列表里,更稳妥的做法是把账号密码写进~/.my.cnf,这样备份命令不用暴露密码,这个问题在生产环境里很值得留意。

6. 常见问题速查与避坑经验

6.1 数据库与后端问题

问题现象原因解决办法
启动报Unable to load authentication plugin 'caching_sha2_password'JDBC 驱动版本低于 8.0升级mysql-connector-java到 8.0.33
连接报The server time zone value is unrecognized未设置 serverTimezoneJDBC URL 加serverTimezone=Asia/Shanghai
报Public Key Retrieval is not allowedMySQL8 连接时未允许公钥检索JDBC URL 加allowPublicKeyRetrieval=true
分页查询返回全量数据缺少分页拦截器配置PaginationInnerInterceptor
SQL 一直提示deleted字段不存在实体类加了@TableLogic但表没加字段数据库表补deleted字段
接口返回的 LocalDateTime 是数组没配置 JSON 序列化引入 jackson-datatype-jsr310 并配置格式

这里重点说下allowPublicKeyRetrieval=true。MySQL8.0 的caching_sha2_password在 SSL 未开启的情况下需要获取服务器公钥才能完成认证,有的驱动出于安全考虑默认不允许自动获取,于是报错。本地开发直接加这个参数最省事,生产环境如果强制 SSL,那这个参数就不需要了。

6.2 前端与部署问题

问题现象原因解决办法
刷新路由页面 404前端用的是 history 模式但 Nginx 没配 try_files加try_files $uri $uri/ /index.html;
接口 404,但本地联调正常生产代理路径与后端 context-path 不一致确认location /api/的 proxy_pass 最终转发到http://127.0.0.1:8080/api/...
前端请求跨域生产环境未通过 Nginx 代理,直接访问后端统一走 Nginx 转发,避免后端开全局 CORS
上传的图片刷新后 404静态资源路径没做映射Nginx 增加location /upload/ { alias /opt/homestay/upload/; }
订单重复提交生成两条前端连点按钮 + 后端未做幂等前端请求期间禁用按钮,后端用订单号唯一索引兜底

6.3 几个没写在文档里的坑

有一部分问题,接口文档里不会告诉你,但是实际跑项目时一定会遇到。第一个是 HikariCP 连接池在 MySQLwait_timeout默认 8 小时的情况下,如果项目半夜没请求,第二天早上第一个请求经常会报"连接已关闭"。解决办法是配置spring.datasource.hikari.max-lifetime=1800000,让连接池在 MySQL 主动断开之前回收连接。

第二个是数据库字段命名一定要全部用下划线风格,比如check_in_date,配合 MyBatis-Plus 的map-underscore-to-camel-case: true自动映射。我在初始化表的时候有一张表偷懒用了checkInDate,结果查询结果里这个字段永远是 null,排查了很久才发现是映射问题。

第三个是把编译器和参考教程的版本统一。如果照着文档里 Spring Boot 2.7 的配置去搜网上的报错,却搜到一篇 Spring Boot 3 的解决方案,很容易把依赖坐标抄错。我建议遇到问题先看项目实际用的版本,再定向搜索,不要盲目复制。

最后再多说一句

源码拿到手,建议先按文档把环境跑通,然后用演示账号完整走一遍"搜索-下单-支付-评价-后台统计"这条链路。我在整理这套系统时的体会是,业务代码本身不复杂,真正花时间的是把库存、状态、金额这些边界条件想完整。二次开发时如果要做民宿拼团、会员折扣、多门店这些扩展,底层的每日库存表和订单状态机已经预留了足够空间,不会推倒重来。希望这套代码能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询