
简介基于微信小程序的水果销售系统毕业设计论文doc格式面向计算机相关专业毕业生及需要完成同类课题的开发者是一份可直接参考的完整文档。系统以Java语言编写MySQL存储数据SpringBoot构建后端服务微信小程序承担前端交互实现水果销售信息的集中管理与查看。资源共1个文件类型为doc文档大小1.41MB内容包含论文摘要、绪论、开发环境与技术MYSQL、JAVA、微信小程序、SPRINGBOOT、B/S架构、TOMCAT以及系统设计与实现章节结构较为完整。文档还涉及信息管理重要性、数据库选型等知识点并附有英文摘要和关键词适合用于本科毕业设计参考、开题报告撰写以及论文格式仿写。目前已有131人学习浏览读者可借鉴其中的数据库表设计思路、SpringBoot与微信小程序结合的接口实现方式以及水果销售业务模块的划分方法有助于快速完成自己的系统设计与论文排版。1. 基于微信小程序的水果销售系统Spring Boot 撑起一个毕设项目的全部技术点把微信小程序、Spring Boot、MySQL 拼成一个水果销售系统是毕业设计里出现频率很高的组合小程序端能真实跑在手机上后端接口能独立测试数据库表结构也足以撑起需求分析、设计、测试一整条论文逻辑链。标题指的正是这类“能跑通、能演示、能写进论文”的完整项目不是只剩页面截图的 Demo。做得多了会发现系统难点不在页面渲染也不在增删改查而在三处数据表的字段提前想清楚没有、登录态怎么在两端维持、下单扣库存会不会被并发打穿。任何一处没处理好演示当场就可能翻车。这篇内容按一线开发视角把这套系统重新拆一遍先讲架构与数据表再给 Spring Boot 的登录、商品、订单接口实现接着落到小程序端的数据流最后集中排联调坑、整理论文素材适合正准备复现或写配套毕业论文的读者。2. 系统架构与数据库设计先定五张核心表和事务边界代码才不会翻车2.1 前后端分离架构里小程序端、Spring Boot 服务端、管理后台怎么分工前端选微信小程序是因为它天然解决了“怎么展示商品、怎么加购、怎么下单”的交互问题用户不需要安装额外 App。Spring Boot 在服务端单独提供 RESTFUL 接口小程序通过 wx.request 发起 HTTP 请求后端返回 JSON。整个链路用一句话概括页面只负责渲染和采集动作所有真实业务规则都收拢到后端 service 层数据库只存结果。如果还想加一个管理后台常见做法是再搭一个 Vue 管理端或者直接在 Spring Boot 里渲染几个简单页面。我一般建议让管理端也复用同一套后端接口只开放商品上下架、库存调整、订单状态修改这三个操作减少重复开发。对论文而言这样就足以画出完整的功能模块图了。端技术选型职责典型页面或接口小程序端WXML WXSS JavaScript商品展示、购物车、下单、地址管理pages/index、pages/cart、pages/order后端服务Spring Boot MyBatis-Plus微信登录、商品查询、库存扣减、订单生成/api/auth/login、/api/product/list、/api/order/create数据库MySQL 8.x持久化用户、商品、订单数据app_user、product、order_info、order_item开发环境里小程序通过局域网 IP 访问本机 Spring Boot真机预览则要把接口域名换成 HTTPS 合法域名。后端接口建议统一以 /api 开头这样以后加 Nginx 反向代理很方便在拦截器里做登录态校验也只需要拦一个路径前缀。2.2 水果销售系统的核心数据表用户、商品、购物车、订单、地址水果销售和普通电商的差别不大但有几个字段是水果场景特有的商品要有“单位”字段因为水果按斤、按份、按盒卖商品要有“产地”字段用于列表页展示商品还建议加“销量”字段做默认排序比用时间排序更贴近真实商城。表拆分上我通常建六张表用户表 app_user、商品表 product、购物车表 cart_item、订单主表 order_info、订单明细表 order_item、收货地址表 delivery_address。购物车表放在后端而不是只存小程序本地缓存原因是用户换手机登录后购物车还在后端也能直接统计“哪些用户加购了但没下单”。核心表和关键字段如下表名用途关键字段app_user微信用户openid、nickname、avatar、phone、create_timeproduct水果商品name、category、origin、price、unit、stock、sold、image_url、statuscart_item购物车user_id、product_id、quantity、selectedorder_info订单主表order_no、user_id、total_amount、status、address_id、create_timeorder_item订单明细order_id、product_id、product_name、price、quantitydelivery_address收货地址user_id、receiver_name、receiver_phone、province、city、district、detail、is_default两点设计必须提前定好。第一app_user 的 openid 要加唯一索引微信登录时用 openid 作为用户唯一标识同一个微信账号只能对应一个用户。第二order_item 里要把 product_name 和 price 冗余保存不能下单后只留 product_id否则商品表改价或删除后历史订单数据就完全失真了。金额字段统一用 DECIMAL(10,2)Java 端对应 BigDecimal这一点后面避坑章节还会再提。2.3 直接可跑的建库脚本把五张表和初始水果数据一次建好下面是基于 MySQL 8 的完整建库脚本我按“先建用户和商品再建购物车和订单”的顺序执行避免外键关系出现在还没有主表的 DDL 里CREATE DATABASE IF NOT EXISTS fruit_sales DEFAULT CHARACTER SET utf8mb4; USE fruit_sales; CREATE TABLE app_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信用户唯一标识, nickname VARCHAR(64) DEFAULT , avatar VARCHAR(512) DEFAULT , phone VARCHAR(20) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL DEFAULT 应季水果, origin VARCHAR(64) DEFAULT COMMENT 产地, price DECIMAL(10,2) NOT NULL, unit VARCHAR(16) NOT NULL DEFAULT 斤 COMMENT 售卖单位斤/份/盒, stock INT NOT NULL DEFAULT 0, sold INT NOT NULL DEFAULT 0 COMMENT 销量用于列表排序, image_url VARCHAR(512) DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE cart_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, selected TINYINT NOT NULL DEFAULT 1 COMMENT 是否勾选结算, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB; CREATE TABLE order_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务单号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已支付 2已发货 3已完成 4已取消, address_id BIGINT NOT NULL, remark VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, KEY idx_user (user_id) ) ENGINEInnoDB; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name VARCHAR(64) NOT NULL, product_image VARCHAR(512) DEFAULT , price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) ENGINEInnoDB; CREATE TABLE delivery_address ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, receiver_name VARCHAR(32) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, province VARCHAR(32) NOT NULL, city VARCHAR(32) NOT NULL, district VARCHAR(32) NOT NULL, detail VARCHAR(255) NOT NULL, is_default TINYINT NOT NULL DEFAULT 0, KEY idx_user (user_id) ) ENGINEInnoDB; INSERT INTO product (name, category, origin, price, unit, stock, image_url, status) VALUES (红富士苹果, 苹果类, 温带产区, 6.80, 斤, 200, /images/apple.jpg, 1), (巨峰葡萄, 葡萄类, 温带产区, 12.50, 斤, 120, /images/grape.jpg, 1), (麒麟西瓜, 瓜类, 温带产区, 2.99, 斤, 300, /images/watermelon.jpg, 1), (砂糖橘, 柑橘类, 亚热带产区, 5.20, 斤, 180, /images/orange.jpg, 1), (红心火龙果, 热带水果, 热带产区, 9.90, 个, 80, /images/dragonfruit.jpg, 1);脚本里有几个细节解释一下。字符集用 utf8mb4 而不是 utf8因为用户昵称可能包含 emojiutf8mb4 才是完整四字节编码。所有表引擎用 InnoDB只有 InnoDB 支持事务订单创建时“扣库存 插入订单 插入明细”必须在一个事务里MyISAM 做不到。order_no 我单独设了唯一索引业务单号逻辑上不能重复数据库层面再兜底一次。提示如果论文里要体现“软删除”可以给 product 和 order_info 加一个 deleted 字段并在 MyBatis-Plus 全局配置里开启逻辑删除。不做也不影响功能演示但做了能让系统设计章节多一个可写的点。3. Spring Boot 后端落地从微信登录到下单扣库存的核心接口3.1 工程骨架与依赖pom.xml、application.yml 和包结构后端工程推荐用 Spring Boot 2.7.x 搭配 MyBatis-Plus 3.5.x这是毕业设计里最稳的组合。包结构按功能分包即可controller、service、mapper、entity、common、config避免按表分包导致后续代码乱成一团。pom.xml 里的关键依赖这样写parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 起步依赖提供 Spring MVC 与内嵌 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 简化单表 CRUD内置分页插件适合中小型系统 -- 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 !-- JWT 令牌用于小程序登录态维持 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesmybatis-plus-boot-starter 的版本要和 Spring Boot 版本对齐Boot 2.x 用 3.5.xBoot 3.x 必须换用 mybatis-plus-spring-boot3-starter否则启动直接报版本冲突。jjwt 则需要三个包同时引入api 负责编译impl 和 jackson 在运行时提供解密和 JSON 序列化。application.yml 的配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/fruit_sales?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true wx: appid: wx1234567890abcdef secret: abcdef1234567890abcdef1234567890 jwt: # 本地开发密钥上线必须替换并放到环境变量里 secret: fruit-sales-demo-secret-key-2024-change-me expire-hours: 7map-underscore-to-camel-case 必须开启数据库字段 order_no 才能自动映射到 Java 实体属性 orderNo。wx 配置项存放小程序的 appid 和 secret这两个值在小程序控制台里拿别写进代码常量。3.2 微信登录接口用 code 换 openid 再签发 JWT 令牌小程序端 wx.login() 拿到的不是身份只是一个临时凭证 code。后端要拿这个 code 去微信的登录凭证校验接口换取 openid流程是小程序发 code → 后端请求微信接口 → 拿到 openid → 查库或建用户 → 生成 token 返回小程序。AuthController 接口这样写RestController RequestMapping(/api/auth) public class AuthController { private final WxService wxService; public AuthController(WxService wxService) { this.wxService wxService; } PostMapping(/login) public ResultString login(RequestBody LoginRequest request) { // request.getCode() 来自小程序 wx.login 返回的临时登录凭证 String openid wxService.code2Openid(request.getCode()); // 根据 openid 查用户不存在则创建新用户 AppUser user wxService.findOrCreateUser(openid); // 签发 JWT后续接口通过 token 识别用户身份 String token JwtUtil.generateToken(user.getId(), openid); return Result.success(token); } }service 层里调用微信接口的核心逻辑public String code2Openid(String code) { // code 只能用一次有效期约 5 分钟 String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code code grant_typeauthorization_code; String resp HttpUtil.get(url); JSONObject json JSONObject.parseObject(resp); // 正常返回包含 openid 和 session_key失败时带 errcode/errmsg if (json.getInteger(errcode) ! null) { throw new BizException(微信登录失败 json.getString(errmsg)); } // 同一微信用户每次登录拿到的 openid 是固定不变的 return json.getString(openid); }这里的 HttpUtil 来自 hutool或者用 Spring 的 RestTemplate 代替效果一样。注意 code 是一次性的重放会报 errcode所以前端登录成功后不能反复调这个接口。JWT 工具类需要特别注意签名密钥长度public class JwtUtil { // 密钥必须不少于 32 字节否则 jjwt 会抛 WeakKeyException private static final String SECRET fruit-sales-demo-secret-key-2024-change-me; private static final long EXPIRE_MS 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String openid) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(openid, openid) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_MS)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8))) .compact(); } public static Long parseUserId(String token) { Claims claims Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }注意JWT 的 HS256 算法要求密钥至少 32 字节写一个很短的测试密钥会直接抛 WeakKeyException这个初始化的坑很隐蔽。expire-hours 设 7 天答辩演示期间不用频繁重新登录。3.3 水果商品与订单接口分页查询、条件更新扣库存、事务边界商品列表接口是最简单的部分但有几个参数要提前约定好RestController RequestMapping(/api/product) public class ProductController { private final ProductMapper productMapper; public ProductController(ProductMapper productMapper) { this.productMapper productMapper; } GetMapping(/list) public ResultIPageProduct list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int pageSize) { PageProduct p new Page(page, pageSize); LambdaQueryWrapperProduct qw new LambdaQueryWrapper(); qw.eq(Product::getStatus, 1) .orderByDesc(Product::getSold) .orderByAsc(Product::getId); return Result.success(productMapper.selectPage(p, qw)); } }这里的 page 从 1 开始MyBatis-Plus 的 Page 也是从 1 开始小程序端传参必须保持一致。排序用销量倒序商品列表第一批加载的是销量最好的水果演示效果比按创建时间排序好看得多。订单创建是整个系统的核心库存扣减是这个接口的成败点。ProductMapper 里定义一个条件更新方法Update(UPDATE product SET stock stock - #{quantity}, sold sold #{quantity} WHERE id #{productId} AND stock #{quantity}) int deductStock(Param(productId) Long productId, Param(quantity) Integer quantity);然后在 orderService 里用事务串起来Transactional(rollbackFor Exception.class) public String createOrder(Long userId, OrderCreateRequest req) { // 1. 扣库存受影响行数为 0 说明库存不足或商品已被下架 for (OrderItemRequest item : req.getItems()) { int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(商品已售罄或库存不足 item.getProductId()); } } // 2. 以数据库实时价格重新计算总价不信任前端传过来的金额 BigDecimal total BigDecimal.ZERO; for (OrderItemRequest item : req.getItems()) { Product product productMapper.selectById(item.getProductId()); total total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 生成订单主表记录 OrderInfo order new OrderInfo(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(1); // 演示环境直接置为已支付真实项目在这里接支付回调 order.setAddressId(req.getAddressId()); orderInfoMapper.insert(order); // 4. 插入订单明细商品名称和单价做冗余快照 for (OrderItemRequest item : req.getItems()) { Product product productMapper.selectById(item.getProductId()); OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(product.getId()); oi.setProductName(product.getName()); oi.setProductImage(product.getImageUrl()); oi.setPrice(product.getPrice()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); } return order.getOrderNo(); }关键点有两个一是扣库存用条件更新的受影响行数判断不用“先 select 再判断再 update”否则并发下库存会变成负数二是 Transactional 保证整个方法要么全部成功要么抛异常回滚包括已经扣掉的库存。订单状态我这里直接设置为 1跳过真实支付论文里按“模拟支付成功”处理就行。4. 微信小程序前端从商品列表到购物车下单的完整数据流4.1 app.js 里配置全局参数baseUrl、token 与登录逻辑小程序端的第一步不是写页面而是先把全局配置和登录流程定好。app.js 是所有页面启动的入口baseUrl、token、登录逻辑放这里是最合理的App({ globalData: { // 开发环境用局域网 IP真机调试和上线必须换成 HTTPS 域名 baseUrl: http://192.168.1.100:8080/api, token: , userInfo: null }, onLaunch() { // 启动时先尝试从本地缓存恢复登录态 const token wx.getStorageSync(token); if (token) { this.globalData.token token; } else { this.login(); } }, login() { wx.login({ success: (res) { wx.request({ url: this.globalData.baseUrl /auth/login, method: POST, data: { code: res.code }, success: (loginRes) { const token loginRes.data.data; this.globalData.token token; wx.setStorageSync(token, token); } }); } }); } });baseUrl 有两层含义开发时是本机局域网地址真机调试时手机必须和电脑在同一 WiFi 下才能访问上线后必须改成已备案的 HTTPS 域名否则所有请求都会被拦。token 写入 storage冷启动时直接恢复避免每次打开小程序都重新走微信登录。4.2 商品列表页wx.request 拉数据、渲染页面、触底加载商品列表页是用户看到的第一屏代码里要处理分页、加载态、触底加载三件事。推荐的页面结构是 index.js 负责数据index.wxml 负责渲染const request require(../../utils/request); Page({ data: { list: [], page: 1, pageSize: 10, loading: false, finished: false }, onLoad() { this.loadProducts(); }, async loadProducts() { if (this.data.loading || this.data.finished) return; this.setData({ loading: true }); try { const res await request.get(/product/list, { page: this.data.page, pageSize: this.data.pageSize }); const records res.data.records || []; this.setData({ list: this.data.list.concat(records), loading: false, finished: records.length this.data.pageSize, page: this.data.page 1 }); } catch (e) { this.setData({ loading: false }); wx.showToast({ title: 商品加载失败, icon: none }); } }, onReachBottom() { this.loadProducts(); } });wxml 里最基本的结构是把商品卡片的图片、名称、价格、单位渲染出来view classproduct-card wx:for{{list}} wx:keyid image src{{item.imageUrl}} modeaspectFill / view classname{{item.name}}/view view classprice text¥{{item.price}}/text text/{{item.unit}}/text /view button bindtapaddCart>const request require(../../utils/request); Page({ data: { cartList: [], totalPrice: 0.00, addressId: null }, onShow() { this.loadCart(); }, async loadCart() { const res await request.get(/cart/list); const list res.data || []; // 只汇总已勾选的条目金额 const selected list.filter(item item.selected); let total 0; selected.forEach(item { total parseFloat(item.price) * item.quantity; }); this.setData({ cartList: list, totalPrice: total.toFixed(2) }); }, async checkout() { const selected this.data.cartList.filter(item item.selected); if (!selected.length) { wx.showToast({ title: 请先勾选商品, icon: none }); return; } if (!this.data.addressId) { wx.showToast({ title: 请选择收货地址, icon: none }); return; } const res await request.post(/order/create, { addressId: this.data.addressId, items: selected.map(item ({ productId: item.productId, quantity: item.quantity })) }); wx.showToast({ title: 下单成功 }); this.loadCart(); } });前端只负责展示和组织参数不对金额做最终判断。这里的 totalPrice 用了 toFixed(2) 做展示格式化但后端下单接口并不信任这个值而是按数据库商品价格重新计算这个信任边界必须清晰不然改个前端页面就能把订单金额改掉。5. 微信小程序 Spring Boot 联调避坑五个高频翻车现场与修复5.1 现象开发者工具图片正常真机上一片空白开发工具里水果图片显示正常一用手机预览商品图全部白屏控制台报 downloadFile 的域名不在合法域名列表里。原因小程序对网络图片的加载有域名白名单限制。开发者工具默认不校验域名所以本地开发看不出问题真机上 image 组件加载的所有图片都属于 downloadFile 合法域名必须配置且要求 HTTPS。解决图片放到支持 HTTPS 的云端存储或 CDN然后在微信公众平台的小程序管理后台配置 downloadFile 合法域名。如果是答辩前的临时演示可以在开发者工具的“本地设置”里勾选“不校验合法域名、web-view 业务域名、TLS 版本以及 HTTPS 证书”再用“真机调试”而不是“预览”的方式打开这样能绕过域名限制跑通演示。5.2 现象并发下单后库存变成负数用两个账号同时下单同一个水果最终数据库里 stock 变成负数订单记录却还在。原因service 层代码写的是“先查库存if 库存大于 0 再执行 update”。两个请求同时查到库存是 5都通过了判断分别执行减库存 update后执行的把先执行的结果覆盖了库存就变成负数。解决把扣库存改成带条件的 UPDATE 语句让数据库在更新时判断库存是否足够UPDATE product SET stock stock - 1, sold sold 1 WHERE id 1 AND stock 1;受影响行数为 0 就抛异常配合 Transactional 回滚整个订单。这是防超卖最直接的手段不用引入分布式锁论文里也能解释清楚。5.3 现象token 过期后接口批量 401小程序端毫无感知演示过程中购物车、下单接口陆续返回 401页面却一直停留着用户重进小程序才能恢复。原因JWT 过期后后端拦截器把请求拦截掉但小程序端没有统一处理 401也没有重新登录的逻辑导致后面的请求全部失败。解决在 utils/request.js 封装里统一判断响应状态码遇到 401 就清除本地 token引导用户重新调用 wx.loginif (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); return Promise.reject(new Error(登录已过期)); }答辩前把 JWT 过期时间调长到 7 到 30 天能减少演示中突然掉线的概率要想做得更规范可以在每次请求成功时重新签发 token实现滑动续期。5.4 现象wx.request 请求本机 Spring Boot 接口直接失败前端把 baseUrl 配成 http://localhost:8080开发工具里请求报 request:fail url not in domain list。原因localhost 在小程序真机里指向手机本机不是开发电脑而且生产环境小程序要求接口必须是 HTTPS 合法域名HTTP 明文接口默认禁止。解决开发阶段在开发者工具详情面板的“本地设置”勾选“不校验合法域名”baseUrl 改成开发机的局域网 IP比如 http://192.168.1.100:8080/api。真机预览时手机和电脑连同一个 WiFi确认防火墙放行 8080 端口。正式发布则必须申请 HTTPS 域名并配置合法域名否则真机无法访问。5.5 现象金额两位小数在结算时变成 29.999999订单表里的 total_amount 显示 29.999999前端明明展示的是 30.00对不上账。原因Java 端用 float 或 double 做金额运算二进制浮点表示 29.9 本身就是近似值多次乘加后误差累积写入数据库就成了 29.999999。解决Java 实体、数据库字段、DTO 参数全部统一用 BigDecimal除法计算时要指定 scale 和舍入模式。前端展示用 toFixed(2) 只是最后一层包装根子上的计算必须发生在 BigDecimal 上否则修复展示格式治标不治本。6. 从可运行系统到可答辩论文截图、测试用例和图表三件套6.1 答辩演示前必录的五个页面流程系统跑通后先别急着写论文正文把演示流程录成素材再动笔。我的习惯是固定录五段微信授权登录进入首页、商品列表下拉刷新、加入购物车并修改数量、勾选商品提交订单、后端数据库刷新出现新订单记录。这五段正好对应论文里从系统实现到系统测试的关键画面录的时候顺手把每段的页面截图存好。6.2 把接口日志整理成功能测试用例表论文的“系统测试”章节最容易被问细节最好用一张功能测试用例表撑起来。把接口日志导出来按模块整理成字段用例编号、测试功能、操作步骤、预期结果、实际结果、是否通过。比如“商品列表分页查询”的用例前端传 page2 和 pageSize10预期返回第 11 到第 20 条记录实际结果对照接口日志填写。写论文时数据库设计里面放 ER 图系统设计里面放架构图系统实现里面放截图系统测试里面放用例表四块素材齐了整篇论文的骨架就立住了。答辩评委最常问的四个问题我也提前列一下为什么购物车数据放后端、token 过期怎么处理、并发扣库存怎么防、订单金额为什么以服务端为准。把这些问题对应的代码逻辑整理成一段文字答辩时直接拿出来讲。我做这套方案时最深的教训是测试数据不能太干净价格、库存、订单号都要像真实业务一样带一点随机感评阅老师一眼就能看出系统是真跑过还是只贴了截图。先把系统跑顺再把素材沉淀下来论文就是水到渠成的事。希望帮到你。本文还有配套的精品资源点击获取