Java微信小程序商城系统:Spring Boot双端闭环开发实战
2026/9/23 2:12:50 网站建设 项目流程

简介:这是一套面向Java后端开发者与全栈工程师的企业级B2C商城系统源码,基于微服务架构设计,完整支持微信小程序、H5、APP多端接入,适用于电商类项目快速落地与二次开发。资源包含2000个文件,主体为1010个Java后端服务代码、889个JS前端逻辑、371个Vue组件及300个TypeScript文件,辅以CSS/SCSS/LESS样式、WXML/WXSS小程序模板及Dockerfile、Jenkinsfile等部署脚本,包体仅13.86MB,结构紧凑且具备集群部署能力。已有1326人学习下载,体现其在真实业务场景中的高复用价值。开发者可直接获取百万用户验证过的Smart Shop核心模块——包括商品中心、订单引擎、支付对接、用户权限体系及小程序交互层,同时通过清晰的目录划分与标准化配置(如yml、properties、gitignore)快速理解整体架构与工程规范。

1. Java开发B2C商城 + 微信小程序商城系统,不是拼凑两个独立项目,而是用一套后端支撑双端流量闭环

很多刚接触电商系统的开发者看到“Java开发B2C商城 微信小程序商城系统源码.zip”这个标题,第一反应是:这不就是把Spring Boot写的后台 + 微信小程序前端打包在一起?但实际落地时才发现——后端接口设计是否适配小程序的登录态、支付回调、消息推送、商品SKU规格联动、订单状态机流转,直接决定上线后能否扛住300人同时下单而不超卖。这不是简单的前后端分离,而是围绕微信生态重构了传统B2C的业务链路:用户从微信授权登录开始,到微信支付回调验签、微信模板消息触发发货提醒、小程序内嵌H5页承载复杂营销页,全部依赖Java后端提供强一致性服务。适合已有Java Web开发经验(熟悉Spring Boot + MyBatis + Redis),正要快速交付一个可商用、带完整微信侧能力的中小商家自营商城的技术负责人或全栈工程师。它解决的不是“能不能跑起来”,而是“能不能在微信里合规、稳定、可扩展地跑”。

2. 用Spring Boot 2.7.x + MyBatis-Plus构建高并发商品与订单核心,避开微信小程序调用时的典型数据一致性陷阱

2.1 为什么选MyBatis-Plus而非JPA,以及它如何降低小程序高频查询下的SQL误写风险

微信小程序首页轮播图、分类导航、商品列表页均需毫秒级响应,而JPA默认的N+1查询在商品关联品牌、分类、规格值时极易引发数据库连接池耗尽。MyBatis-Plus通过@TableName@TableField显式声明映射关系,配合LambdaQueryWrapper构建类型安全的条件查询,避免字符串拼接SQL导致的小程序请求参数注入(如用户传入?category_id=1' OR '1'='1)。更重要的是,其IService接口内置的listByIds()getOne()等方法自动处理空指针,防止小程序端因后端返回null而触发页面白屏。实际项目中,我们用@Select("SELECT * FROM pms_product WHERE id IN #{ids}")手写批量查商品详情,并在Mapper层加@ResultMap("ProductWithSkuMap")预加载SKU列表,将单次商品页请求的DB查询从7次压至2次。

// ProductServiceImpl.java @Override public List<ProductDTO> listByCategory(Long categoryId, Integer pageNum, Integer pageSize) { Page<Product> page = new Page<>(pageNum, pageSize); // 避免使用QueryWrapper的like模糊查询,小程序搜索框输入实时触发,易拖垮DB LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getCategoryId, categoryId) .eq(Product::getStatus, 1) // 仅上架商品 .orderByDesc(Product::getSort); return this.page(page, wrapper).getRecords().stream() .map(this::convertToDTO) // 转DTO时异步查SKU,非阻塞 .collect(Collectors.toList()); }

提示:小程序端分页参数pageNumpageSize必须校验范围(如pageNum > 0 && pageSize <= 50),否则恶意请求?pageNum=1&pageSize=10000会触发全表扫描。MyBatis-Plus的Page对象本身不校验,需在Controller层用@Min(1) @Max(100)注解约束。

2.2 订单创建的分布式事务控制:用本地消息表+定时任务替代RocketMQ,适配中小项目部署成本

微信小程序下单流程包含:校验库存 → 扣减库存 → 创建订单 → 发送模板消息 → 更新用户积分。若用RocketMQ实现最终一致性,需额外部署MQ集群,对单台服务器部署的中小项目不现实。本方案采用本地消息表+补偿任务:在同一个MySQL事务中,插入订单记录的同时,向msg_order_log表写入一条状态为pending的消息;另起一个@Scheduled(fixedDelay = 30000)定时任务,扫描pending消息并调用下游服务(如积分服务、消息推送服务)。关键点在于msg_order_log表必须与订单表同库同事务,且INSERT ... SELECT语句确保原子性:

-- 创建本地消息表 CREATE TABLE `msg_order_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(64) NOT NULL COMMENT '订单号', `service_type` varchar(32) NOT NULL COMMENT '服务类型:points/integration', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-pending,1-success,2-failed', `retry_count` int NOT NULL DEFAULT '0', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no_type` (`order_no`,`service_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
// OrderService.java @Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderDTO dto) { // 1. 库存校验(Redis Lua脚本保证原子性) String luaScript = "if redis.call('get', KEYS[1]) >= tonumber(ARGV[1]) then " + " return redis.call('decrby', KEYS[1], ARGV[1]) " + "else return -1 end"; Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList("stock:" + dto.getProductId()), String.valueOf(dto.getQuantity())); if (result == null || result < 0) { throw new BusinessException("库存不足"); } // 2. 创建订单(含主订单+订单项) Order order = buildOrder(dto); orderMapper.insert(order); // 3. 写入本地消息表(同事务) MsgOrderLog log = new MsgOrderLog(); log.setOrderNo(order.getOrderNo()); log.setServiceType("points"); log.setStatus(MsgOrderLog.STATUS_PENDING); msgOrderLogMapper.insert(log); return order; }

注意:Redis库存扣减必须用Lua脚本,避免GET+DECRBY之间的竞态。小程序用户点击“立即购买”时,前端需禁用按钮并显示loading,防止重复提交——后端仅靠幂等性(订单号唯一索引)无法覆盖前端误操作。

2.3 微信支付回调验签:用官方SDK解析XML并校验签名,拒绝任何自定义签名逻辑

小程序调起支付后,微信服务器会向/api/pay/notify发送XML格式回调。常见错误是开发者自行拼接参数验签,但微信签名规则要求:① 参数名ASCII升序排列;② 过滤空值和sign字段;③ 拼接key=value&后末尾加商户密钥;④ MD5转小写。官方WXPayUtil类已封装此逻辑,必须使用:

// PayController.java @PostMapping(value = "/notify", produces = MediaType.APPLICATION_XML_VALUE) public String handlePayNotify(HttpServletRequest request) { try { // 1. 读取原始XML流(不能用@RequestBody,会丢失CDATA) String xmlData = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 2. 解析并验签(使用微信官方SDK) Map<String, String> notifyMap = WXPayUtil.xmlToMap(xmlData); if (!WXPayUtil.isSignatureValid(notifyMap, wxPayConfig.getMchKey())) { return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>"; } // 3. 业务处理:更新订单状态、发模板消息、扣库存 if ("SUCCESS".equals(notifyMap.get("result_code"))) { String outTradeNo = notifyMap.get("out_trade_no"); orderService.handlePaySuccess(outTradeNo); } return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } catch (Exception e) { log.error("微信支付回调异常", e); return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[系统错误]]></return_msg></xml>"; } }
参数说明小程序侧对应
out_trade_no商户订单号createOrder()返回的orderNo
transaction_id微信订单号用于后续退款、查询
total_fee支付金额(分)必须与下单时一致,防篡改
openid用户OpenID用于发送模板消息

3. 微信小程序前端对接Java后端的关键配置与调试技巧,绕过request:fail net::ERR_CONNECTION_REFUSED

3.1 小程序app.jsonproject.config.json的必填项设置,确保开发工具能直连本地Java服务

微信开发者工具默认不允许访问http://localhost:8080,必须在project.config.json中开启"miniprogramRoot": "./"并配置"setting"

{ "description": "项目配置文件", "setting": { "urlCheck": false, // 关键!关闭合法域名检查 "es6": true, "enhance": true, "postcss": true, "preloadBackgroundComponents": true, "uploadWithSourceMap": true, "ignoreUploadUnusedFiles": true, "compileHotReLoad": false, "useMultiFrameRuntime": true, "useApiHook": true, "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" }, "enableEngineNative": false, "bundle": false }, "miniprogramRoot": "./" }

同时,在小程序app.js中全局配置wx.request的baseURL:

// app.js App({ globalData: { baseUrl: 'http://127.0.0.1:8080/api' // 开发时指向本地Java服务 } })

提示:真机调试时,手机与电脑必须在同一局域网,且Java服务绑定0.0.0.0:8080(而非127.0.0.1)。在application.yml中配置:

server: address: 0.0.0.0 port: 8080

3.2 登录态管理:用code2Session换取session_key,再由Java后端生成自定义token

小程序wx.login()获取临时code后,不能直接将session_key传给前端(存在泄露风险)。正确流程是:小程序将code发给Java后端 → 后端调用微信auth.code2Session接口 → 用返回的openid查库或新建用户 → 生成JWT token返回:

// LoginController.java @PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO dto) { // 1. 调用微信接口换取openid String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + wxConfig.getAppId() + "&secret=" + wxConfig.getAppSecret() + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; String response = restTemplate.getForObject(url, String.class); JSONObject json = JSONObject.parseObject(response); String openid = json.getString("openid"); String sessionKey = json.getString("session_key"); // 2. 根据openid查用户,不存在则创建 User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + RandomStringUtils.randomAlphanumeric(6)); userService.save(user); } // 3. 生成JWT(含userid、openid、exp) String token = JwtUtil.generateToken(user.getId(), openid); return Result.success(new LoginVO(token, user.getAvatar(), user.getNickname())); }

小程序端存储token并设置请求头:

// utils/request.js const request = (url, method = 'GET', data = {}) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: getApp().globalData.baseUrl + url, method, data, header: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, success: resolve, fail: reject }) }) }

3.3 商品SKU选择器的后端数据结构设计,支撑小程序端“颜色-尺码”二维联动

小程序商品详情页的SKU选择器需支持:选中“红色”后,“L码”变灰不可选(库存为0),且价格实时变化。Java后端不能只返回扁平化SKU列表,而应提供树形结构:

{ "specs": [ { "name": "颜色", "values": ["红色", "蓝色", "黑色"] }, { "name": "尺码", "values": ["S", "M", "L", "XL"] } ], "skus": [ { "id": 1001, "specs": ["红色", "S"], "price": 9900, "stock": 10, "skuCode": "RED-S" }, { "id": 1002, "specs": ["红色", "M"], "price": 9900, "stock": 0, "skuCode": "RED-M" } ] }

后端用Map<String, Sku>缓存skuCode→Sku映射,前端选中规格后拼skuCode(如“红色”+“M”→“RED-M”)查库存与价格:

// ProductController.java @GetMapping("/detail/{id}") public Result<ProductDetailVO> detail(@PathVariable Long id) { Product product = productService.getById(id); List<Sku> skus = skuService.listByProductId(id); // 构建规格树 List<SpecVO> specs = buildSpecTree(skus); // 构建SKU映射 Map<String, Sku> skuMap = skus.stream() .collect(Collectors.toMap( s -> s.getSpecs().replace(",", "-").toUpperCase(), Function.identity() )); return Result.success(new ProductDetailVO(product, specs, skuMap)); }

4. 生产环境部署避坑指南:Nginx反向代理配置、HTTPS证书自动续期、MySQL慢查询定位

4.1 Nginx配置必须包含proxy_set_header X-Forwarded-For $remote_addr,否则Java后端获取不到真实IP

小程序用户通过微信客户端访问,请求经微信服务器中转,再由Nginx代理到Java服务。若Nginx未透传原始IP,request.getRemoteAddr()将返回Nginx内网地址(如127.0.0.1),导致风控系统失效。正确配置如下:

upstream java_backend { server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=30s; } server { listen 443 ssl; server_name mall.example.com; ssl_certificate /etc/nginx/ssl/mall.pem; ssl_certificate_key /etc/nginx/ssl/mall.key; location /api/ { proxy_pass http://java_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 关键 proxy_set_header X-Forwarded-For $remote_addr; # 关键 proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location / { root /var/www/mall-miniprogram; try_files $uri $uri/ /index.html; } }

Java后端获取真实IP时,必须优先读X-Forwarded-For

// 工具类 public static String getClientIp(HttpServletRequest request) { String xff = request.getHeader("X-Forwarded-For"); if (StringUtils.isNotBlank(xff) && !"unKnown".equalsIgnoreCase(xff)) { // 多级代理时取第一个非unknown的IP return xff.split(",")[0].trim(); } xff = request.getHeader("X-Real-IP"); if (StringUtils.isNotBlank(xff) && !"unKnown".equalsIgnoreCase(xff)) { return xff; } return request.getRemoteAddr(); }

4.2 用Certbot自动续期Let's Encrypt证书,避免小程序因HTTPS过期白屏

微信小程序强制HTTPS,证书过期会导致request:fail ssl hand shake error。手动更新极不可靠,必须自动化:

# 安装Certbot sudo apt update && sudo apt install certbot python3-certbot-nginx -y # 获取证书(首次) sudo certbot --nginx -d mall.example.com # 自动续期(添加到crontab) echo "0 2 * * 1 /usr/bin/certbot renew --quiet --post-hook \"systemctl reload nginx\"" | sudo tee -a /etc/crontab

验证续期是否生效:

sudo certbot renew --dry-run

注意--post-hook中的systemctl reload nginx必须存在,否则新证书不会生效。reload比restart更安全,不中断现有连接。

4.3 定位MySQL慢查询:开启慢日志并用pt-query-digest分析,聚焦小程序首页加载瓶颈

小程序首页加载慢,90%源于未优化的SQL。先在MySQL中开启慢日志:

-- 开启慢查询日志 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 0.5; -- 超过500ms即记录 SET GLOBAL log_output = 'FILE'; SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';

然后用Percona Toolkit分析:

# 安装pt-query-digest sudo apt install percona-toolkit -y # 分析最近1小时慢查询 pt-query-digest /var/log/mysql/mysql-slow.log --since "2024-06-01 10:00:00" \ --limit 10 \ --filter '$event->{db} && $event->{db} =~ m/your_db_name/' \ > /tmp/slow-report.txt

典型问题及修复:

慢查询特征原因修复方案
SELECT * FROM pms_product WHERE category_id = ? ORDER BY sort DESC LIMIT 20category_id无索引ALTER TABLE pms_product ADD INDEX idx_category_sort (category_id, sort);
SELECT COUNT(*) FROM pms_order WHERE user_id = ? AND status IN (1,2,3)user_id+status未联合索引ALTER TABLE pms_order ADD INDEX idx_user_status (user_id, status);
SELECT * FROM pms_sku WHERE product_id = ? AND stock > 0product_id索引未覆盖stockALTER TABLE pms_sku ADD INDEX idx_product_stock (product_id, stock);

5. 微信小程序商城的三个高价值进阶技巧:模板消息动态内容渲染、Redis布隆过滤器防刷券、订单超时自动关单

5.1 模板消息内容动态拼接:用Thymeleaf预编译模板,避免Java代码硬编码JSON

小程序支付成功后需发送模板消息:“您购买的【{product_name}】已发货,预计{days}天后送达”。若每次都在Java中拼JSON,易出错且难维护。用Thymeleaf模板统一管理:

<!-- templates/msg/order_shipped.html --> { "touser": "#{${openId}}", "template_id": "TEMPLATE_ID_HERE", "data": { "thing1": { "value": "#{${productName}}" }, "time4": { "value": "#{${deliveryTime}}" } } }

Java调用:

@Service public class TemplateMsgService { @Autowired private TemplateEngine templateEngine; public String buildShippedMsg(String openId, String productName, String deliveryTime) { Context context = new Context(); context.setVariable("openId", openId); context.setVariable("productName", productName); context.setVariable("deliveryTime", deliveryTime); return templateEngine.process("msg/order_shipped", context); } }

优势:模板与代码分离,运营人员可直接修改order_shipped.html调整文案,无需发版;Thymeleaf自动HTML转义,防XSS注入。

5.2 用Redis布隆过滤器拦截恶意领券请求,单机QPS从200提升至1500+

小程序“新人专享券”活动常被脚本刷券。传统SETNX方案在高并发下仍可能超发。布隆过滤器(Bloom Filter)以极小内存代价提供“存在性概率判断”:

// 初始化布隆过滤器(100万用户,误判率0.01) RedisBloom redisBloom = new RedisBloom("coupon_bloom", 1000000, 0.01); // 领券前校验 String userId = "user_123456"; if (redisBloom.mightContain(userId)) { // 可能已领,直接拒绝 throw new BusinessException("每人限领1张"); } else { // 未领过,尝试领取 boolean success = couponService.grantCoupon(userId, "NEW_USER_10"); if (success) { redisBloom.add(userId); // 加入过滤器 } }

布隆过滤器内存占用估算:m = -n * ln(p) / (ln(2)^2),其中n=1000000p=0.01m≈9.6MB,远低于缓存100万用户ID的内存开销。

5.3 订单超时自动关单:用Redis ZSet实现精准延迟任务,替代低效的定时轮询

传统方案用@Scheduled每分钟扫status=0 and create_time < now()-30min,随着订单量增长,扫描压力剧增。ZSet方案将订单过期时间作为score存入:

// 创建订单时,将订单号加入ZSet long expireAt = System.currentTimeMillis() + 30 * 60 * 1000; // 30分钟后过期 redisTemplate.opsForZSet().add("order:timeout", orderNo, (double) expireAt); // 启动守护线程,监听过期订单 while (!Thread.currentThread().isInterrupted()) { Set<String> timeoutOrders = redisTemplate.opsForZSet() .rangeByScore("order:timeout", 0, System.currentTimeMillis()); if (CollectionUtils.isNotEmpty(timeoutOrders)) { for (String orderNo : timeoutOrders) { // 关单逻辑 orderService.closeOrder(orderNo); // 从ZSet移除 redisTemplate.opsForZSet().remove("order:timeout", orderNo); } } Thread.sleep(1000); // 每秒检查一次 }

该方案时间复杂度O(1),10万订单ZSet内存占用约2MB,且无锁设计避免定时任务竞争。

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

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

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

立即咨询