资讯动态

Spring Boot构建B2B商旅小程序:订单、库存与权限设计实战

发布时间:2026/10/19 14:02:32 来源:尧图企业网站定制
出差商旅这个方向在毕业设计选题里算是比较少见的“既有业务逻辑、又有技术深度”的题目。我刚拿到的是“基于Spring Boot的出差商旅B2B商务出行住行订购小程序”这个题目后面还跟着“源码文档远程调试全包定制”这些交付要求。看起来就是一套常规管理系统真往下做才发现难点根本不在 Spring Boot 怎么写而在 B2B 这个前缀带来的业务约束企业协议价怎么算、员工差标怎么校验、订单和结算怎么拆、小程序端三个角色怎么共用一套入口。这篇文章我想把整个项目的动手过程完整拆开来讲。后面涉及的所有表结构、状态机、接口约定和调试经验都是模拟项目X从零开发过程中实际验证过的方案。适合三类人看正在选毕业设计题目的同学接手了类似“源码文档远程调试”交付内容的开发者以及想了解 B2B 商旅系统和小程序端如何打通的初学者。1. 先搞清楚 B2B 商旅和 C 端订票到底差在哪1.1 同样是“订酒店机票”业务目标完全不同C 端出行产品的用户是自己决策链路很薄搜一下、比个价、自己付钱、自己报销。页面做得够快、价格展示够直白基本就跑通了。但 B2B 商务出行的用户是企业员工只是发起人真正掏钱和看账单的是企业财务。这个变化带来几个核心问题第一价格不能随便展示。每个企业跟平台签的协议可能完全不同同一间房A 企业看到的协议价是 380B 企业可能是 420员工个人根本看不到门市价。所以小程序端每次查价格都必须带着企业身份去实时计算。第二下单不一定立刻成立。员工选完酒店和车次还要判断是否符合企业的差旅标准。超出差标怎么办有的企业允许“超标自付”有的企业要求走特批流程。这个判断必须落在后端不能只在前端提示一下。第三住和行往往是一个行程。员工去外地出差大概率需要“一晚酒店一张车票/机票”两样东西属于同一次出差申请。如果做成两个独立模块订单对账和行程管理就会很乱。所以“住行一体”不是功能堆叠而是订单从创建开始就要有分组逻辑。这套逻辑决定了技术方案不能用“酒店预订管理系统”的思路去套而是要做成一整条企业消费链路。1.2 坐在屏幕对面的三类人员工、企业管理员、平台运营我一开始以为小程序就是给员工用的管理员做一个 Web 后台就行。真梳理需求之后发现这套系统至少有三类使用者而且他们看的东西完全不同。员工端发起出差申请查行程选酒店和车次提交订单查看自己的订单记录和报销状态。员工最在意的是“便不便宜”“合不合规”“有没有票”。企业管理员维护企业信息设置部门结构审批出差申请设定差旅标准查看对账账单。管理员在意的是“花了多少”“在哪花的”“有没有超标”。平台运营管理供应商的产品酒店房型、车次班次、维护企业协议价、处理订单异常和退款。运营在意的是“库存够不够”“结算对不对”“异常多不多”。这三类角色不是三个独立的小程序而是共用同一个入口。登录时后端返回当前用户的角色标识前端根据角色渲染不同的首页和功能菜单。权限模型用简单的 RBAC 就够了没必要引入过重的框架但数据库设计必须一开始就把“企业维度”和“角色维度”都考虑进去。2. 后端设计第一步不是写接口而是把订单和结算拆开2.1 主链路从出差申请到订单完成要经过哪些状态很多同学拿到这种题目会直接开始建表写 CRUD结果写到一半就会发现问题订单有酒店有交通退款和结算交织在一起完全分不清主次。我建议先画业务主链路再倒推数据表。这个项目的主链路是这样的员工提交出差申请 → 企业管理员审批可跳过看企业配置 → 员工在小程序里按申请单查询可用产品 → 选择酒店/车次并提交订单 → 后端校验差标和库存 → 生成订单并锁定库存 → 行程完成 → 订单进入结算池 → 生成账单 → 财务确认或系统自动结算。这里面最容易被忽略的是“审批”和“预订”的关系。如果员工还没有通过出差申请就不能进入下单页。所以出差申请单是订单的前置条件订单必须携带 apply_id 字段而不是各订各的。状态流转也要提前定义清楚。出差申请单的状态我定义为草稿(0) → 审批中(1) → 已通过(2) → 已驳回(3) → 已撤销(4)。订单主表状态定义为待确认(0) → 已确认(1) → 已完成(2) → 已取消(3) → 已退款(4)。这里没有做太复杂的支付状态机因为 B2B 场景下很多是月结或者协议预付不跟个人支付一样实时强校验。但这个设计会被答辩老师问到时候要能说清楚“什么类型的订单会进入待确认”“退款之后库存怎么释放”。2.2 核心表结构与容易忽略的字段整个项目的核心表大致可以分为四组。企业组织组t_company企业信息、结算方式、t_department部门、t_employee员工信息、t_travel_standard差旅标准。产品资源组t_product_hotel酒店产品、t_product_flight交通产品、t_inventory_calendar按日期维护的库存日历、t_company_agreement企业与平台的协议价。流程订单组t_travel_apply出差申请单、t_order订单主表、t_order_item订单明细、t_order_status_log状态变更日志。结算账单组t_settlement_bill结算单、t_refund_order退款单。有几张表的字段值得单独说一下。t_order 里除了 order_no、employee_id、company_id、order_type酒店/交通、total_amount、status 之外一定要有 apply_id 和 order_group_id。apply_id 把订单和出差申请绑起来order_group_id 把同一次出差里的酒店订单和交通订单绑成一个组方便前端展示“一趟行程”而不是两个孤立订单。t_order_item 里要有 unit_price、quantity、amount、standard_amount 四个金额字段。standard_amount 表示差标上限下单时后端会拿协议价跟 standard_amount 比较如果超标则标记 status 为“待审批”。为什么要单独存 standard_amount因为差标未来可能变订单一旦生成就不能受后续标准调整的影响。t_inventory_calendar 是容易被做成“普通库存数字”的表真正做起来应该长这样product_id、biz_date业务日期)、total_stock、locked_stock、available_stock。酒店和交通资源是按日期存在的不能用简单的总数去扣否则跨天订单会把库存扣错。建表字段我直接给出一个参考片段其中订单主表是这样CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, apply_id BIGINT NOT NULL COMMENT 出差申请单ID, order_group_id VARCHAR(32) NOT NULL COMMENT 订单组ID, company_id BIGINT NOT NULL COMMENT 企业ID, employee_id BIGINT NOT NULL COMMENT 员工ID, order_type TINYINT NOT NULL COMMENT 1-酒店 2-交通, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态, request_id VARCHAR(64) DEFAULT NULL COMMENT 幂等请求ID, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_request_id (request_id), KEY idx_apply (apply_id), KEY idx_group (order_group_id) ) COMMENT 订单主表;这里 request_id 是幂等控制用的下面讲并发的时候会细说。先记住一条原则所有金额字段都用 DECIMAL绝不用 DOUBLE因为机票、住宿这类价格一旦出现精度误差对账的时候会很头疼。2.3 Service 层要承载业务Controller 只做转发Spring Boot 项目最常见的反面教材就是 Controller 里写一大堆业务代码查询都走 JPA Repository 直接拼。B2B 订单场景的业务复杂度集中在下单链路这样做很快会失控。我自己用的分层是Controller 负责参数接收、简单校验、调用 ServiceService 负责完整业务编排比如下单时要做差标校验、价格计算、库存锁定、生成订单、写日志这五步Repository或者 Mapper只负责数据库读写。这样拆分后测试和远程调试都会方便很多。简单列一下下单接口的 Controller 和 Service 大致形态RestController RequestMapping(/api/order) public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } PostMapping(/create) public ResultString create(RequestBody OrderCreateDTO dto) { return Result.success(orderService.createOrder(dto)); } }Service 内部虽然代码多一些但逻辑可以被测试覆盖Service public class OrderServiceImpl implements OrderService { Override Transactional(rollbackFor Exception.class) public String createOrder(OrderCreateDTO dto) { // 1. 校验员工和企业状态 // 2. 根据产品ID和出差日期计算协议价 // 3. 比较差标判断是否需要特批 // 4. 锁定库存锁定失败则抛出异常 // 5. 生成订单主表和明细表 return orderNo; } }整个下单操作必须在同一个事务里完成。因为库存锁定和订单生成要么一起成功要么一起失败不能出现“订单生成了但库存没扣”或者反过来。3. 小程序端的功能落点不止是几个列表页3.1 三个角色共用一个入口首页按身份渲染小程序端首先要解决的不是页面多好看而是“一个微信小程序怎么装下三种角色”。微信小程序应用本身只有一个入口所以登录之后要做一次路由分发。登录流程是用户点微信授权 → 前端拿到 code → 调后端 login 接口 → 后端用 code 换取用户身份信息查出用户属于哪个企业、什么角色 → 返回 token 和 userInfo → 前端存入本地缓存 → 根据 role 字段跳到不同首页。具体到页面设计员工进入的是“出差工作台”管理员进入的是“企业账单和审批工作台”运营进入的是“产品管理工作台”。同一套代码里做三个页面靠登录态分流不要搞三个小程序那样的话部署和证书都会非常麻烦。首页划分可以参考下面的 tabBar 设计员工端是“出差申请”“订单”“我的”三个 tab管理端是“审批”“账单”“企业设置”三个 tab运营端是“产品”“订单管理”“协议价”三个 tab。底部的 tab 在微信小程序里是全局配置做不到按用户动态变化所以我的做法是首页占一个 tab内部用组件来切换角色面板订单和个人中心作为公共页面保留。这里有个很实用的经验不要试图在 tabBar 上做完全动态配置微信小程序的 tabBar 是静态的。要么把所有角色都需要的“首页”“订单”“我的”保留在首页里分身份展示不同区块要么把工作台做成一个普通页面首页 tab 只是一个入口进入后再分发。3.2 出差申请和订单的绑定关系员工去出差不是直接搜酒店下单而是先填一张出差申请单。申请单要包含出发城市、目的城市、预计出差日期、返程日期、随行人员、出差事由。提交之后管理员在“审批”面板里看到申请点击通过员工才可以在“我的出差申请”里看到“去预订”按钮。这看起来多了一步但它是 B2B 商旅的核心卖点企业通过申请单提前知道员工要去哪、什么时候去从而控制成本。前端交互上两个地方要特别注意。第一员工进入“去预订”之后系统只能展示与出差日期匹配的库存。比如出差日期是 6 月 1 日到 6 月 3 日那就只查 6 月 1 日入住的酒店、6 月 1 日和 6 月 3 日的车次。不能给出一个自由选日期的搜索框否则申请单就形同虚设。第二酒店和交通要放在同一个确认页里。员工先选酒店再选车次最后在一个订单确认页里看到“住行”的合计金额。提交按钮写的是“提交本次行程”而不是“提交酒店订单”。这样前端跳转简单后端也能用一个 order_group_id 把两笔订单串起来。小程序端提交订单的核心逻辑其实很轻就是把用户选好的产品和日期交给后端wx.request({ url: ${baseUrl}/api/order/create, method: POST, data: { applyId: applyId, items: [ { productId: 101, productType: 1, bizDate: 2025-06-01 }, { productId: 202, productType: 2, bizDate: 2025-06-01 } ] }, success(res) { if (res.data.code 0) { wx.redirectTo({ url: /pages/order/detail?orderGroupId res.data.data }); } } });真正的业务判断全部在后端前端只有一个原则服务端返回什么就展示什么。3.3 在企业差标下“可选”才是核心文案C 端小程序会把最便宜的产品标成“推荐”但 B2B 商旅小程序里最核心的文案是“可选”或“超标需特批”。前端展示产品列表时要请求一个接口product/list?applyIdxxx。后端返回每个房型或车次时同时带上 price、standardAmount 和 status 字段。status 分三种OPTIONAL符合差标可直接下单、OVER_STANDARD超出差标需走特批、NO_ACCESS企业协议不包含员工无权限预订。前端不要自己比较价格是否超差标必须信任后端返回的 status。原因很简单差标规则可能有“一线城市 500二线城市 400”这种复杂判断前端写死容易出错而且协议价是敏感数据前端只要拿到一个“当前员工可见价格”就好不需要知道企业全部价格规则。如果员工选了“超标需特批”的产品提交订单时要弹一个说明框要求填写超标理由。这个理由会存到订单表里的 special_approval_reason 字段管理员在审批列表里能看到。这个小功能很容易被忽略但在企业的财务合规场景下非常关键答辩时拿出来讲会显得你对业务理解是到位的。4. 价格、库存、幂等住行订购链路的三道坎4.1 价格不要预存要在下单时算刚开发这个项目时踩过一个很典型的坑为了查询快在产品表里直接存一个 price 字段前端一查就显示出来了。后来做企业协议价时发现问题了同一个房型A 企业的协议价可能跟 B 企业完全不一样存到这个字段里到底存谁的如果存门市价那 B 企业看到的就不对如果存最低价其他企业又会有纠纷。正确做法是价格在下单时实时计算把“产品基础价”和“企业协议折扣”分开存储。产品表存一个基础价 base_price企业协议表 t_company_agreement 存协议折扣或者一个固定的协议价。计算逻辑是协议价 产品基础价 × 企业协议折扣 服务费如果某个企业跟平台签了特殊价比如某酒店房型直接约定 380 元/晚那协议表里保存 agreement_price计算时直接取这个值。整个计算发生在 Service 层前端完全没有必要知道内部规则。价格计算的代码要注意精度问题。Java 里计算金额必须用 BigDecimal我用一段示例说明这个坑// 反例不要用 double 计算金额 double base 289.00; double discount 0.85; double price base * discount; // 输出可能带一堆小数 // 正例用 BigDecimal BigDecimal base new BigDecimal(289.00); BigDecimal discount new BigDecimal(0.85); BigDecimal price base.multiply(discount).setScale(2, RoundingMode.HALF_UP);除了价格本身还要注意订单生成后所有金额字段要原样快照到订单明细表里不能下单之后再回头去查协议价。因为企业协议折扣可能随时被运营调整如果订单已经生成后续退款、结算都要以订单快照为准。4.2 库存扣减的顺序决定会不会超卖库存这块我一开始想得很简单查到 available_stock 0就在内存里减一然后插入订单。后来并发测试结果直接打脸两个请求同时读到 available_stock1都认为有货结果两个订单都生成成功卖出了两间房。要解决这个问题不能用“先查再改”的方式要用条件更新来保证原子性。直接执行一条 UPDATE 语句库存大于零才扣减UPDATE t_inventory_calendar SET locked_stock locked_stock 1 WHERE product_id ? AND biz_date ? AND available_stock 0;如果这条 UPDATE 影响行数为 1说明锁定成功影响行数为 0说明库存已被占用直接抛异常提示“库存不足”。这个做法在数据量不大的毕业设计里完全够用不需要引入分布式锁。再补充一点locked_stock 是已经锁定但还没完成的库存available_stock 是剩余可订库存。订单确认后可以把 locked_stock 转成实际消耗也可以简单地把 available_stock 减一。建议把 available_stock 设计成虚拟字段表中只存 total_stock 和 locked_stockavailable 用减法得到这样可以避免两处维护不一致。我实测过一个对比场景并发 20 个请求同时抢最后 5 间房没有条件更新时生成了 9 个成功订单有条件更新后只成功 5 个其他全部正常失败。这个测试结果直接写进论文的测试章节比空口说“性能好”有说服力得多。4.3 幂等处理重复点击和回调重放小程序端网络不稳定时用户可能重复点击“提交订单”同一个请求被发了两次。如果没有幂等控制数据库里会出现两笔一模一样的订单这种情况特别尴尬。解决方案是在请求里带一个 requestId前端每次进入订单确认页时生成一个 uuid提交时放进请求体。后端在 t_order 表上给 request_id 建唯一索引插入订单前先按 request_id 查一次如果已经存在直接返回已生成的订单号不重复创建。这里有个细节如果 requestId 查不到但插入时唯一索引冲突了说明并发下另一个请求已经创建成功此时要捕获 DuplicateKeyException然后重新查询并返回已有订单。不要在这个异常情况下直接往外抛错否则用户会看到一个“系统繁忙”的报错。这三个问题价格、库存、幂等是把 B2B 商旅订单链路做扎实的关键。做完这一层项目就从“增删改查”提升到了“有业务深度”的层面这也是答辩时可以重点展开的部分。5. 远程调试与交付演示代码能跑不等于演示能过5.1 联调环境怎么搭本机、局域网、服务器三种模式“远程调试”这套服务实际上就是把项目从开发机搬到可演示环境的过程。我自己实际走过的步骤可以分成三种模式。第一种本机联调。小程序开发者工具里可以直接把后端地址填成 http://localhost:8080但微信开发者工具默认会校验合法域名所以要勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。这只是开发调试阶段的做法不要在正式环境这么干。第二种局域网联调。后端跑在自己电脑上小程序预览时用真机调试把请求地址改成电脑的局域网 IP比如 http://192.168.1.100:8080。手机和电脑必须在同一个网络里而且需要注意小程序真机预览对 HTTP 接口同样有域名校验所以需要点开开发工具的“详情-本地设置”把不校验合法域名的选项打开。第三种服务器部署。这是最接近交付验收的形式。后端打成 jar 包部署到一台云服务器数据库也放上去小程序请求地址改成服务器的公网地址同时需要配置 HTTPS 证书。此时“远程调试”就可以通过日志查看问题而不必坐在开发机旁边。我自己推荐直接走第三种模式因为毕业设计演示通常发生在答辩教室网络环境不可控不可能依赖本机或局域网。代码提前部署到公网现场只要打开小程序就能演示稳定性最好。5.2 最容易卡住的接口问题清单远程调试中遇到的大部分问题不是代码逻辑而是环境连接。我把高频问题整理成一张排查表接手“源码文档远程调试”这类任务时可以直接按表查。现象可能原因解决办法小程序请求一直转圈后端地址写的是 localhost改成服务器公网地址或局域网 IP接口返回 404Controller 路径写错或项目没启动先在本机用 curl 测一下接口路径接口返回 403部署环境开了权限拦截检查拦截器白名单是否包含登录接口数据库连接失败MySQL 没开远程访问或密码错误检查 application.yml 配置和数据库授权中文乱码数据库连接串没指定编码jdbc 参数加 characterEncodingutf-8时间差 8 小时数据库时区设置不对连接串加 serverTimezoneAsia/Shanghai排查的时候不要只盯着小程序端先用 Postman 或 curl 直接请求后端接口能快速定位问题在前端还是后端。日志更是重要Spring Boot 默认的日志级别下SQL 语句不一定打印出来我习惯在开发环境把 MyBatis 的日志级别调成 DEBUGlogging: level: com.example.project.mapper: DEBUG这样每一条 SQL 都能在控制台看到参数和返回行数一目了然很多联调问题瞬间就有答案了。5.3 部署验收前必须过一遍的检查项很多项目做完了第一遍部署总是失败原因都出在低级配置上。我列了一个部署前的检查清单照着走一遍基本能稳数据库初始化脚本是否完整是否包含测试数据application.yml 里的数据库密码是否为部署环境真实密码后端端口是否被防火墙或云安全组放行小程序 request 合法域名是否在微信公众平台配置是否配置了 HTTPS 证书且证书未过期服务器上的 jar 包是否有执行权限Java 版本是否与打包版本一致是否关闭了开发调试模式避免接口暴露敏感错误信息把这七项走完基本上就能达到“演示环境可稳定运行”的状态。远程调试这个交付承诺说白了就是让对方能在异地看到一个真实跑起来的系统而不是一堆源码和一张架构图。6. 毕业论文和答辩别把文档写成接口文档6.1 论文目录怎么定评委更愿意看什么代码做完了论文还占一半工作量。我见过太多毕设论文写得跟接口文档一样一个接口一个接口地贴代码评委翻两页就不想看了。论文结构不要按代码模块写要按业务主线写。我建议的目录顺序是绪论背景与意义、国内外研究现状、论文结构相关技术介绍Spring Boot、微信小程序、MySQL、相关开发工具系统需求分析可行性分析、功能需求、非功能需求系统概要设计总体架构、功能模块划分、数据库设计系统详细设计与实现核心业务流程、关键实现类、页面实现系统测试测试环境、功能测试、并发测试、测试结论总结与展望这里最值得花时间的是“系统详细设计与实现”这章。不要把每个表的每个字段都抄一遍而要挑两三个核心场景讲深下单场景怎么处理价格和库存、审批场景怎么流转状态、结算场景怎么汇总账单。每个场景配一张流程图论文里可以画图但这里不放 flow 图和一段关键代码。我在写“库存并发控制”时把并发测试对比结果做成了表格列出不同并发数下超卖订单数量。这一页很加分因为评委一眼就能看出你真的做了实验验证而不是抄代码。6.2 十分钟演示脚本和常被追问的问题答辩演示一般不超过十分钟很多同学败在“从头到尾点了一遍但没把亮点讲出来”。我建议按这个脚本走先花 30 秒讲清楚系统是给谁用的企业员工出差、管理员审批、运营管理产品。然后花 2 分钟演示员工路径提交出差申请 → 模拟管理员审批通过 → 员工进入预订页 → 选择一个符合差标的产品 → 提交订单 → 查看订单详情。再用 1 分钟演示一个特殊场景比如选一个超额产品走特批流程突出系统的业务规则。最后用 1 分钟打开管理员的账单页面说明住行订单如何汇总成结算单。这个脚本的关键是把“业务闭环”讲出来而不是逐个页面罗列功能。答辩时高频出现的问题我也提前准备好了回答思路问为什么选择 Spring Boot而不是 Spring MVC 或其他框架答Spring Boot 简化了配置和部署自带内嵌 Tomcat适合快速交付生态成熟。问订单并发冲突你怎么解决答库存通过条件 UPDATE 保证原子性订单通过 requestId 唯一索引保证幂等。问如果企业协议价调整已经生成的订单怎么办答下单时金额已经快照到订单表后续结算和退款不受协议价调整影响。问这个系统如果要做权限控制怎么防止员工越权查看别的企业订单答所有查询在 Service 层强制带上 companyId不能只依赖前端传参。这几个问题的回答不用背但要保证自己能流畅地讲出来。毕业设计答辩本质上就是“把你做的东西讲清楚”代码能跑只是入场券能讲明白设计取舍才是真正的验收标准。最后再分享一个我自己的实操习惯拿到这类项目不管是自己写还是接手现成源码第一件事一定先把订单状态流转和权限边界理清楚。状态流转理不清后面做结算、退款、对账都会反复返工权限边界理不清演示时很容易翻车。只要这两条主线稳住了Spring Boot 和小程序端的技术细节反而都是可以快速补上的部分。这套思路做完这个商旅项目之后再回头看其他管理系统基本都能快速抓住主干这也是这个毕业设计给我最大的收获。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑