☰
基于Java的农村电商系统实战:从RBAC权限到并发扣库存
2026/10/9 1:01:47 网站建设 项目流程

简介:这是一份基于Java的农村电子商务系统设计与实现的完整学位论文PDF,面向学习Java Web开发或需要毕业设计参考的开发者。资源针对农村电商基础设施薄弱、信息入村困难的问题,提出以O2O模式为主、PC端与电子货柜终端相结合的电商平台方案。包体内包含1个PDF文件,压缩包大小11.39MB,已有758人学习/下载。论文详细阐述技术选型与系统实现:在Java语言下整合Spring、SpringMVC、MyBatis与Shiro安全框架,选用SQL Server 2008数据库;系统分为前台和后台,前台涵盖缴费支付、商品展示、购物车、订单生成、会员登录注册及便民服务、同城购物、采购配送、终端管理等,后台包含商户管理、商品管理、订单管理、信息管理与系统设置;同时完整呈现需求分析、数据库设计以及实体层、持久层、业务层、控制层、显示层的分层架构和实现流程。读者可据此复现系统,也可作为毕业设计、课程设计或农业信息化项目的技术方案参考,并了解系统在县域村域超市、零售店中的测试运营情况。

1. 基于Java的农村电子商务系统:为什么值得自己动手做一遍

有人问我,基于Java的农村电子商务系统的设计与实现,难在哪?我的答案不是功能,而是并发和状态。真实的农村电商场景,往往是一个村几个收购点,买家在微信里下单,运营用Excel记账,订单一多就乱了。用Java做一套系统,管住商品、订单、库存、补贴和物流,是毕设里常报的题,也是很多县域电商项目的最小原型。它适合正在做毕设的Java工程师,也适合想快速搭出可验证demo的小团队。别急着写代码,先把边界画清楚,后面会少踩一半坑。

2. 系统设计:角色与表结构先定好,后面才不返工

农村电商系统表面是商品展示加下单,实际上一动手就会发现,每个角色都要不同权限,每笔订单都牵扯商品、库存、支付、物流,关系很复杂。我一般会先花两天画E-R图、定状态,再写代码。常见做法是先用RBAC模型解权限,再用业务表解订单和库存,最后用状态机约束订单流转。这一章把设计讲清楚,后面实现才有据可依。

2.1 角色与权限:买家、卖家、运营、物流的权限模型

最稳妥的是RBAC,也就是用户-角色-权限三张核心表。买家登录后只能看商品、下单、查自己的订单;卖家是农户或合作社,可以上架商品、处理自己的订单;运营人员负责审核商品、发优惠券、看全站数据;物流人员只能更新运单轨迹。权限不能直接挂在用户身上,否则加一个人就要改代码,维护成本很高。

表设计可以精简成下面这套:

  • sys_user:id, username, password, phone, real_name, status
  • sys_role:id, role_code, role_name
  • sys_user_role:id, user_id, role_id
  • sys_permission:id, perm_code, perm_name, parent_id
  • sys_role_permission:id, role_id, permission_id

权限粒度控制在接口级别,比如order:create、product:audit。小项目不用一上来就引入Spring Security,自己写一个HandlerInterceptor,每次请求校验当前用户是否拥有对应权限即可。用Spring Security也可以,但配置重,毕设答辩时容易被追问底层细节。

这里有一个设计要点:把“运营”和“平台管理员”分开。运营只能处理商品审核和营销券,平台管理员才有系统配置、日志查看等权限。这样权限模型有层次,后续加功能不会乱。密码也不要存明文,常见做法是用BCrypt加密,Java侧用spring-security-crypto的BCryptPasswordEncoder。

2.2 商品、订单、支付、物流的核心表结构

商品是典型的SPU/SKU模型。很多课程设计把商品信息全塞进一张表,刚开始没事,一旦卖不同规格的鸡蛋、礼盒,库存就分不清。建议至少拆四张表:

  • 商品分类表category:id, parent_id, name, sort
  • 商品表product:id, category_id, seller_id, title, main_image, detail, status
  • 商品SKU表product_sku:id, product_id, spec_name, price, stock, presale_stock
  • 库存流水表stock_log:id, sku_id, change_type, change_qty, order_id, create_time

订单这块最少需要订单主表和订单明细表。订单主表存用户、卖家、总金额、实付金额、优惠券ID、补贴金额和状态;订单明细表存SKU、单价、数量、商品标题、商品图片。注意“商品标题、商品图片”是冗余字段,因为下单后商品改名、换图不能影响历史订单。

订单主表核心字段大致如下:

字段类型说明
idBIGINT主键
order_noVARCHAR(32)订单号
user_idBIGINT买家ID
seller_idBIGINT卖家ID
total_amountDECIMAL(10,2)商品总金额
actual_amountDECIMAL(10,2)实付金额
coupon_idBIGINT优惠券ID
subsidy_amountDECIMAL(10,2)助农补贴金额
statusTINYINT订单状态
create_timeDATETIME下单时间

支付记录单独建表,记录支付渠道、流水号、回调内容、状态。物流表记录运单号、快递公司、轨迹JSON。这样做的好处是每笔订单都能追溯到支付和物流链路,出问题能查。别忘了给订单表加idx_user_id和idx_seller_id,因为农村电商运营查询经常按买家或卖家筛选,没索引到十万级数据就开始卡。

订单号生成不要用数据库自增id,会暴露单量。常见做法是时间戳加随机数再加用户id,比如yyMMddHHmmss加4位随机数,数据库里设为唯一索引。金额字段要存total_amount、actual_amount、subsidy_amount三个值,分别表示原价、实付、补贴。有些人只存实付金额,后面对账时补贴去哪了都查不到。

2.3 用 MyBatis-Plus 根据 Java 实体类生成建表 SQL:给实体类就出 DDL

很多人选型时愿意用MyBatis-Plus而不是原生MyBatis,因为它能根据Java实体类省掉大部分单表CRUD。这里有一个实用技巧:写一个小工具,读取实体类上的MyBatis-Plus注解,自动拼装CREATE TABLE语句。热搜词“mybatisplus根据java实体类生成创建表的sql语句”,说的就是这个事。

先看实体类,比如ProductSku:

import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.math.BigDecimal; import java.time.LocalDateTime; @TableName("product_sku") public class ProductSku { @TableId(type = IdType.AUTO) private Long id; @TableField("product_id") private Long productId; @TableField("spec_name") private String specName; @TableField("price") private BigDecimal price; @TableField("stock") private Integer stock; @TableField("presale_stock") private Integer presaleStock; @TableField(value = "create_time", fill = FieldFill.INSERT) private LocalDateTime createTime; }

@TableName指定表名,@TableId(type = IdType.AUTO)表示主键自增,@TableField("product_id")把Java字段的驼峰名映射到下划线列名。如果你不写@TableField,MyBatis-Plus默认开启驼峰命名映射,也能匹配到product_id。

然后是生成DDL的工具类:

import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDateTime; public class DdlGenerator { public static String generate(Class<?> entityClass) { TableName tableName = entityClass.getAnnotation(TableName.class); StringBuilder sql = new StringBuilder(); sql.append("CREATE TABLE IF NOT EXISTS `") .append(tableName.value()).append("` (\n"); Field[] fields = entityClass.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; if (field.isAnnotationPresent(TableField.class) && !field.getAnnotation(TableField.class).exist()) { continue; } String columnName = camelToUnderscore(field.getName()); if (field.isAnnotationPresent(TableId.class)) { String idValue = field.getAnnotation(TableId.class).value(); columnName = idValue.isEmpty() ? columnName : idValue; } else if (field.isAnnotationPresent(TableField.class) && !field.getAnnotation(TableField.class).value().isEmpty()) { columnName = field.getAnnotation(TableField.class).value(); } sql.append(" `").append(columnName).append("` ") .append(mapType(field.getType())) .append(i == fields.length - 1 ? "\n" : ",\n"); } sql.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='") .append(tableName.value()).append("';\n"); return sql.toString(); } private static String camelToUnderscore(String camel) { return camel.replaceAll("([A-Z])", "_$1").toLowerCase(); } private static String mapType(Class<?> type) { if (type == Long.class || type == long.class) return "BIGINT"; if (type == Integer.class || type == int.class) return "INT"; if (type == BigDecimal.class) return "DECIMAL(10,2)"; if (type == LocalDateTime.class) return "DATETIME"; if (type == String.class) return "VARCHAR(255)"; return "VARCHAR(255)"; } }

生成逻辑很直接:遍历实体类字段,先跳过@TableField(exist=false)的字段,再用@TableId或@TableField里的value决定列名,最后用mapType把Java数据类型映射成MySQL类型。生成的DDL带IF NOT EXISTS,重复执行不报错。

使用时在单元测试里循环所有实体类即可:

List<Class<?>> entities = List.of(ProductSku.class, Product.class, Order.class); entities.forEach(e -> System.out.println(DdlGenerator.generate(e)));

这个工具只覆盖常用Java类型,LocalDate、Boolean还需要自己补。它也不生成索引和约束,所以我的习惯是:用它生成大表的基础DDL,再人工补索引和注释。生产环境更推荐用Flyway管理版本化脚本,不要在启动时直接执行这个工具生成的SQL,否则字段变更没法追溯。

3. 用 Spring Boot + MyBatis-Plus 把后端跑通:核心代码与最小启动命令

设计完成之后就是落地。我用Spring Boot加MyBatis-Plus加MySQL这套组合,因为它最接近农村电商项目的主流技术栈,招聘和答辩都不容易被挑战。这一章按照依赖配置、核心接口、启动命令三步走,跟着抄就能在本地跑出一个最小订单流程。

3.1 Maven 依赖与基础配置:Spring Boot + MyBatis-Plus + MySQL

先看pom.xml里的关键依赖:

<properties> <mybatis-plus.version>3.5.3</mybatis-plus.version> </properties> <dependencies> <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>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

MyBatis-Plus的版本号要和你用的Spring Boot对齐,3.5.3是我常用的版本,如果你用Spring Boot 3.x,需要换对应的starter和JDK版本。Lombok可以省掉getter/setter,但实体类里用了继承时要小心,Lombok不会自动生成父类字段的setter。

然后是application.yml:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/rural_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

JDBC URL里的characterEncoding=utf8防乱码,serverTimezone=Asia/Shanghai防时区差8小时。map-underscore-to-camel-case必须为true,否则product_name映射不到productName。逻辑删除字段deleted是实体类里的一个字段,MyBatis-Plus会把删除操作变成UPDATE,查询自动带上deleted=0。

3.2 从登录到下单:Controller-Service-Mapper 三段式怎么落地

农村电商的核心链路是登录、浏览、下单、支付。这里用订单创建接口展示三层结构。先写Controller:

@RestController @RequestMapping("/api/order") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping("/create") public Result<OrderVO> create(@RequestBody CreateOrderDTO dto) { OrderVO orderVO = orderService.createOrder( dto.getUserId(), dto.getSkuId(), dto.getQuantity(), dto.getCouponId()); return Result.ok(orderVO); } }

Controller只接收参数并调用Service,不写业务逻辑。CreateOrderDTO里应有userId、skuId、quantity、couponId四个字段。这里用构造器注入而不是@Autowired字段注入,后面写单元测试时更容易替换依赖。

Service实现是重点:

@Service public class OrderServiceImpl implements OrderService { @Override @Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, Long skuId, Integer quantity, Long couponId) { User user = userMapper.selectById(userId); ProductSku sku = skuMapper.selectById(skuId); if (user == null || sku == null) { throw new BizException("用户或商品不存在"); } int rows = skuMapper.deductStock(skuId, quantity); if (rows == 0) { throw new BizException("库存不足"); } Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setSellerId(sku.getSellerId()); order.setTotalAmount(sku.getPrice().multiply(BigDecimal.valueOf(quantity))); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setSkuId(skuId); item.setQuantity(quantity); item.setPrice(sku.getPrice()); item.setProductTitle(sku.getTitle()); orderItemMapper.insert(item); return OrderVO.from(order); } }

@Transactional(rollbackFor = Exception.class)很关键,Spring默认只回滚RuntimeException,如果抛检查异常,事务是不回滚的。扣库存用的是自定义deductStock,SQL长这样:

UPDATE product_sku SET stock = stock - #{quantity} WHERE id = #{skuId} AND stock >= #{quantity}

返回受影响行数,0表示库存不足。这比先查库存再update安全,能防并发超卖。订单明细里的productTitle是冗余快照,后续商品改名不影响历史订单。事务边界放在Service,不是Controller,因为一个Service方法往往包含多次数据库操作,放在Controller异常会被提前捕获,事务就不回滚了。

3.3 面向对象编程思想:用枚举管状态,用 BigDecimal 管金额

“面向对象编程java”是面试题高频词,放到项目里最直接的体现就是不让业务状态散落成魔法数字。订单状态我建议定义枚举:

public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public boolean canCancel() { return this == PENDING_PAYMENT; } }

状态判断都走枚举方法,而不是到处写if (order.getStatus() == 0)。后续加状态,改动集中在枚举一个类里。Java数据类型也要注意:金额不要用double或float,用BigDecimal。数据库里商品单价、订单金额都是DECIMAL(10,2),Java实体用BigDecimal对应。

为什么这点容易被忽略?因为很多购物车demo用double算总价,0.1加0.2会出现精度问题。农村电商有补贴和优惠券叠加,金额算错一分都会被运营找上门。从实体类、DTO到计算逻辑,统一用BigDecimal,这是Java基础里最便宜的一课。

3.4 本地跑通最小命令:从建库到 curl 验证

代码结构搭好后,本地验证流程建议写成README。我一般分四步走。

先建库:

mysql -u root -p -e "CREATE DATABASE rural_mall DEFAULT CHARACTER SET utf8mb4;"

再启动项目。如果你用Maven:

mvn spring-boot:run

或者打包运行:

mvn clean package -DskipTests java -jar target/rural-mall-0.0.1-SNAPSHOT.jar

-DskipTests只是跳过测试执行,不是跳过编译测试代码。如果测试代码编译不过,打包还是会失败。想完全跳过可以写-Dmaven.test.skip=true。

启动后在另一个终端验证:

curl -X POST http://localhost:8080/api/order/create \ -H "Content-Type: application/json" \ -d '{"userId":1,"skuId":1001,"quantity":2,"couponId":null}'

如果返回{"code":0,"data":{"orderNo":"..."}},说明链路通了。如果报错,先看日志里的SQL。我通常会在application.yml里加这段,把SQL打印出来:

logging: level: com.example.rural.mapper: debug

这里的包名com.example.rural.mapper要换成你实际的Mapper包路径。打印出SQL后,看参数有没有传对,排查效率会高很多。

提示:启动前确认JDK版本和Maven版本一致,常见问题是本机JDK8但依赖用的JDK11,启动直接报UnsupportedClassVersionError。

4. 农村电商的差异化功能:预售、补贴、物流怎么实现

农村电商和城市电商最大的区别在业务模式:货还没种,订单已经下了;政府补贴要跟着订单走;物流链路长,快递可能要转好几次。这三个点如果按照普通电商做,后面会补得很痛苦。

4.1 预售模式与批次库存:先卖后种,怎么防止超卖

农产品预售很常见,比如春季预售大米,农户按预订量决定种植面积。这种需求不能只靠普通库存,要在SKU上加一个presale_stock字段。预售下单时扣presale_stock,不扣stock。到了约定发货时间,批量把预售订单转成普通订单,再走正常物流。

扣减预售价用同样的条件更新,防止并发超卖:

UPDATE product_sku SET presale_stock = presale_stock - #{quantity} WHERE id = #{skuId} AND presale_stock >= #{quantity}

如果预售件数超过种植能力,后续发不出货。所以运营后台要设置“可预售上限”,等于预估产量乘一个保险系数,比如预估3吨,预售上限设80%。预售订单取消后,要回补预售库存,同时写库存流水,否则对不上账。

等预售到期,定时任务扫描预售订单,把状态改成待发货,并把预售库存转成实际库存。这里要注意,转单时不能重复扣预售库存,只做状态流转和物流运单创建。常见做法是加一张presale_batch表,记录批次、预计发货日期、预售数量、实际发货数量。

4.2 助农补贴与优惠券:金额计算要留审计痕迹

政府给农产品发补贴是常见玩法。注意补贴不能直接改商品价格,而是生成一张补贴记录,关联订单。我一般这样设计:

subsidy_flow表字段:id, order_id, user_id, subsidy_type, amount, status, create_time, audit_user

补贴金额在订单创建时计算,写入订单表的subsidy_amount。订单取消时,补贴和优惠券都要回滚。最容易犯的错是只把用户实付金额改了,补贴流水没记,月底对账差一大截。

计算逻辑不要散落在Service里,抽一个PriceCalculator:

public class PriceCalculator { public PayDetail calculate(BigDecimal totalAmount, BigDecimal couponAmount, BigDecimal subsidyAmount) { BigDecimal payAmount = totalAmount .subtract(couponAmount) .subtract(subsidyAmount) .max(BigDecimal.ZERO); return PayDetail.builder() .totalAmount(totalAmount) .couponAmount(couponAmount) .subsidyAmount(subsidyAmount) .payAmount(payAmount) .build(); } }

使用BigDecimal.subtract和max,保证实付金额不会变负数。业务上如果规定补贴和优惠券不能叠加,可以在外面加规则判断。这个类是纯函数,方便写单元测试,比如“补贴大于订单总额时,实付为0”。

4.3 物流信息对接:手写回调接口比轮询更可靠

农村电商物流经常是邮政、通达系混合。对接物流轨迹时,新手最容易想到定时任务每半小时调一次第三方接口。但查询接口有频率限制,单量大时很浪费。常见做法是提供回调接口,让物流公司把轨迹推给你,你只负责更新数据库。下面是一个简化回调接口:

@PostMapping("/api/logistics/callback") public Result<String> receiveCallback(@RequestBody LogisticsCallbackDTO dto, @RequestHeader("X-Sign") String sign) { // 1. 验签,防止伪造回调 if (!LogisticsSignUtil.verify(sign, dto)) { return Result.error("签名不合法"); } // 2. 更新运单轨迹 logisticsTraceService.saveTrace(dto.getTrackingNo(), dto.getTrace()); // 3. 更新订单状态为已发货 if ("SHIPPED".equals(dto.getStatus())) { orderService.markShipped(dto.getOrderNo()); } return Result.ok("已接收"); }

X-Sign是签名,保证请求来自合作的物流公司。回调接口必须幂等,同一运单重复推送不能产生重复轨迹,所以我会在logistics_trace表加(tracking_no, trace_time)唯一索引。处理完要返回成功,物流方收到成功响应就不会重推;如果接口抛异常,物流方会重试,容易造成重复消费。这个设计比轮询及时,也省流量,唯一代价是要在对接时和物流方约定好签名规则。

5. 避坑指南:从事务失效到并发超卖的 5 个血泪教训

这一章写我做类似系统时真实踩过的坑,每一条都浪费过一天时间。按现象、原因、解决来记,照着排查能少走弯路。

5.1 下单成功但库存没减:事务失效的第一现场

现象:接口返回“下单成功”,数据库订单存在,商品库存却纹丝不动;更诡异的是,扣库存日志也打印了。

原因:最常见的是在OrderServiceImpl内部调用事务方法。比如类里有一个createOrder,另一个方法handleBuyNow又调用this.createOrder。Spring事务走的是代理,this调用不会经过代理,所以@Transactional失效。其次是方法不是public,或者异常被catch住后没有抛出,事务自然不回滚。

解决:不要同类内部调用。把事务方法放在独立Bean里,或者让Controller直接调用createOrder。@Transactional的rollbackFor要设成Exception.class,否则抛出检查异常时可能不回滚。排查时打开MyBatis SQL日志,看扣库存的update有没有真正执行到。

5.2 同一张优惠券被领两次:唯一索引比查询判断更有用

现象:用户在活动开始瞬间连续点击“领取”,后台出现两条user_coupon记录。

原因:代码里先selectCount判断有没有领过,再insert。两个请求同时查到count为0,都进入insert。判断逻辑没有约束,数据库也不会阻止重复数据。

解决:在user_coupon表加唯一索引(user_id, coupon_id),让MySQL兜底。插入前保留查询判断,是为了减少无用插入,但真正的防线是唯一索引。插入时用INSERT IGNORE或者捕获DuplicateKeyException:

try { userCouponMapper.insert(record); } catch (DuplicateKeyException e) { throw new BizException("您已领过这张券"); }

如果是毕设,别只把唯一索引写在文档里,一定在初始化SQL里加上UNIQUE KEY uk_user_coupon (user_id, coupon_id),否则答辩老师反问“并发怎么办”就露怯了。

5.3 MyBatis-Plus 自动填充失效:create_time 一直是 null

现象:实体类里设置了@TableField(fill = FieldFill.INSERT),插入后create_time字段在数据库里还是null。

原因:MyBatis-Plus的自动填充不是加了注解就生效,还需要实现MetaObjectHandler。很多初学者只加注解,没有处理器,注解永远不会工作。

解决:写一个填充处理器:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

同时要确认实体字段名和这里一致,strictInsertFill按字段名匹配。如果字段名是create_time而不是createTime,就填不进去。如果实体类已经手动给createTime赋值,strict模式不会覆盖。

5.4 订单列表越查越慢:下单时做商品快照,少 join 一张表

现象:订单列表页每次都要关联商品表、用户表,数据量到十万级后接口耗时从200毫秒涨到2秒。

原因:订单明细只存了SKU id,查询时join了太多表。更隐蔽的是,时间字段还在用java.util.Date,序列化、比较都不舒服。

解决:下单时把商品标题、图片、价格快照到订单明细表,查询订单列表只查订单表加明细表,不join商品表。时间类型统一用java.time.LocalDateTime,和MySQL的DATETIME对应很顺。Java常用包里的java.util.Date不建议在新代码里使用。订单表还要建好idx_user_id和idx_seller_id,不然再冗余也没用。

5.5 本地能跑,部署后乱码、时间差8小时

现象:本地查询中文正常,部署到服务器后输出变成问号,时间比数据库快了8小时。

原因:JDBC连接串没指定字符集和时区;数据库表默认字符集不是utf8mb4;服务器JVM默认编码不是UTF-8。三个原因叠加,表现就很迷惑。

解决:三点一起处理。建库用DEFAULT CHARACTER SET utf8mb4;JDBC URL写成jdbc:mysql://localhost:3306/rural_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai;JVM启动参数带上-Dfile.encoding=UTF-8。另外,不要用System.out.println打印中文,换成SLF4J日志,并在日志配置里指定UTF-8。

6. 把系统讲成能过答辩、能上线的方案:验证与表达技巧

6.1 答辩时先讲订单状态流转,再讲接口文档

我第一次做系统时只演示代码,被评委问“订单在哪个流转点会被取消”,当场卡住。后来学乖了,准备一张订单状态表格,说明每个动作由谁触发:待支付可被买家取消或超时自动关闭;已支付只能由卖家发货;发货后买家确认收货进入完成。你不需要画得多复杂,但要把边界说清楚。接口文档用Swagger或SpringDoc生成,至少覆盖登录、商品列表、创建订单、物流回调四个核心接口。写文档时把参数类型、是否必填、失败响应放在一起,会比只贴代码更有说服力。

6.2 上线前验证三件事:压测、金额用例、日志监控

上线前我一般做三件小事。一是用JMeter模拟500个并发同时买同一个SKU,看库存会不会变负数;二是跑一组金额测试用例,包括优惠券大于订单总额、补贴叠加、取消后退券;三是把日志级别调到info,确认每次扣库存和创建订单都有orderNo与skuId可追踪。如果是毕设,至少完成第一件,并把压测结果截图放到PPT里。千万不要只写“系统实现了”五个字,评委要的是数据。

就像我常对身边人说的:项目能不能打动人,不在功能列表有多长,而在于你踩过坑之后,能不能把解决方案说明白。这个基于Java的农村电商系统,从表结构到并发扣库存,每一步都有可验证的细节。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询