简介:面向需要搭建成人用品线上零售业务的开发团队与个人开发者,该资源为一套完整的新版JAVA商城源码,涵盖Android与iOS双端原生APP和微信小程序前端,后台基于Java体系实现,可快速搭建支持商品分类、首页主推、购物流程、订单查看、促销优惠券等功能的零售商城。资源包共2021个文件,约346MB,以html、java、js、m、h、css、json等类型为主:java/m为服务端及原生逻辑,html/css/js对应前端与后台页面,json/xml承载数据与配置,sql可辅助初始化数据库;前端并集成bootstrap、layui、ueditor、font-awesome等常见开源库,方便二次开发与界面调整。资源未附带搭建教程,适合具备Java与移动端基础、能自行部署调试的读者研究。目前已有558人学习下载,可作为成人用品新零售项目起步的实用参考原型。
1. 一套Java电商源码能省半年工期?先把「商城APP源码」这五个字拆开看
成人用品零售商城,一个听着“敏感”实则很常规的垂直电商场景。很多从业者拿到这套“新版JAVA开源成人用品零售商城APP源码”时,第一反应是三端齐全、马上买服务器上线。但我在电商公司折腾过几套开源商城,必须说句实话:源码能帮你省下的是CRUD、商品管理和基础UI,省不下的是支付回调、库存扣减、售后状态机这些真正让人睡不着觉的部分。这篇文章就围绕这套源码讲透:拿到手先看什么、怎么跑起来、二次开发从哪里下手、哪些坑几乎天天有人踩。新手能照步骤复现,熟手也能对照参数和边界做取舍。
2. 源码里到底有什么:从前端到后端的目录解剖与选型判断
很多人解压“.rar”后只看一眼目录就开跑,这不对。你需要先回答三个问题:这是不是真原生源码?后端到底是单体还是微服务?接口设计有没有留出运营空间?我按三端逐一拆。
2.1 安卓与iOS原生工程:一眼分清是真源码还是套壳
判断双端源码是否“原生”的方法并不玄学。解开压缩包后,安卓端必须有.gradle工程结构,至少能看到app/src/main/java这样的包路径,以及build.gradle和AndroidManifest.xml。iOS 端如果是原生工程,应该出现.xcworkspace或.xcodeproj,配合Podfile管理第三方库。如果一个项目里只看到几个.html文件和一份webapp目录,那大概率是 WebView 套壳——上线后每次加载页面都走网络,体验和性能都受限。
我在本地验证时会先搜一遍代码里有没有高频的WebView、WKWebView加载逻辑。使用下面的命令可以直接在源码目录里检索:
grep -r "WebView\|WKWebView" --include="*.java" --include="*.swift" --include="*.js" ./android ./ios ./wechat逻辑说明:这条命令把安卓端、iOS端和小程序端的常见文件类型都覆盖了。如果搜索结果里大量出现 WebView 相关类名,那说明“原生”纯度存疑——原生商城应该把主要页面交给 RecyclerView/UITableView 渲染,只有 H5 活动页或商品详情富文本场景才需要内嵌浏览器。
参数说明:--include是常见做法,用来过滤文件类型,避免把整个 node_modules 或第三方库都搜出来。-r是递归目录。你还可以在安卓工程里再确认一下app/build.gradle的依赖列表,重点看有没有com.tencent.smtt:webview这类腾讯 X5 内核包——很多套壳项目会把 X5 作为依赖,因为它能缓解 WebView 的兼容性问题,但这恰恰说明业务层是用网页实现的。
另外,看包名也是一个有效线索。原生工程包名通常和业务相关,比如某个模拟项目X的包名是com.example.shop;而套壳工程则经常出现com.wps.wpswebview、com.tencent.smtt这类第三方内核包名。拿到源码后,用解压工具直接看apk结构也能判断,但本地源码里搜更直接。
2.2 小程序端:微信生态下的交易闭环
小程序端相对简单,但更考验业务理解。正常的微信小程序商城会使用原生wxml+wxss+js结构,同时包含app.json页面注册文件。它一定会调用wx.request请求后端接口,下单时会调用wx.requestPayment拉起微信支付。你还需要在project.config.json里确认appid字段是真实的还是占位符。
打开小程序目录后,常见做法是检查页面分包结构。比如pages/goods/list、pages/order/confirm、pages/pay/result。如果整个项目只有一个pages/index/index,那说明这套小程序只是把 H5 塞进了 web-view 组件里。工具层面,我一般会用find快速列出页面文件数量:
find ./wechat -name "*.wxml" | wc -l逻辑说明:这条命令统计wxml文件数量,能大致判断页面规模。一个能用的商城小程序,至少存在商品列表、详情、购物车、订单、支付结果这几个页面,所以数量在 10 个以上是常见入门线。如果只有 3 个以下,那基本可以断定是演示壳。
参数说明:wc -l用来统计行数,这里统计的是文件数量。你在 Windows 下可以用dir /b /s .\\wechat\\*.wxml加管道到find /c实现同样效果。注意这只是一个快速判断,不能替代真正的逻辑审查。
小程序端真正特殊的地方是“类目审核”。成人用品类目在微信小程序里属于特殊类目,需要营业执照和相应资质。源码自带的appid往往没有开通支付权限,所以你在联调时很可能遇到“支付不可用”的报错。这一点我们放到避坑章节细说。
2.3 Java后端:从包名到接口定义判断工程质量
后端是整个系统的心脏。打开源码根目录,先看pom.xml或build.gradle。常见的单体商城会基于 Spring Boot 构建,依赖里会出现spring-boot-starter-web、mybatis-plus或mybatis、spring-boot-starter-data-redis。如果你看到spring-cloud-starter-gateway、nacos-discovery、sentinel这类微服务组件,那说明作者当初是按分布式架构写的,落地时你的运维成本和出错概率也会更高。
我拿到源码后,第一步是看包名结构。一个干净的商城工程应该按照业务域分包,比如com.shop.order、com.shop.product、com.shop.user。如果所有代码都躺在com.shop.controller/com.shop.service这种按技术层次划分的包里,说明早期代码是简单的 CRUD 堆积,后续加需求时会很痛苦。做电商的人都明白:订单域和商品域必须分出独立的业务逻辑,因为库存、支付、售后这些状态流转各自复杂,混在一起就是事故现场。
除了包名,我还会看一眼接口返回值封装。优秀的商城会统一返回结果类型,常见的是Result包装对象,里面包含code、message、data三个字段。如果每个接口直接返回一个Map或者裸对象,那后续联调时前端会非常别扭。你可以用 grep 快速统计:
grep -r "class Result\|Result<T>" --include="*.java" ./backend | head -20逻辑说明:这条命令在backend目录里检索Result泛型类,目的是确认后端是否有统一的响应包装。如果搜索不到,说明接口返回结构不统一,前端每个请求都要单独做异常处理,二次开发时很容易出现“一个接口改了,另一个接口漏了”的情况。
参数说明:head -20限制显示前 20 行,避免输出太多刷屏。如果你看到多条符合,说明封装比较规范。配合查看具体的Result.java,需要确认它是否包含success标识、时间戳、分页数据承载字段。没有统一封装的源码不是说不能改,但你接手后第一件事就是建一个公共响应类,这个工作量我建议按 2~3 天来估。
后端工程质量还体现在数据库脚本上。源码根目录一般会带.sql文件,你要打开看表设计。一个靠谱的商城至少应有orders、order_items、products、sku_stock、users、pay_logs这几张表。如果只有寥寥几张表,说明功能裁剪得比较狠,上线前你得自己补售后、退款、优惠券这些表。
3. 把源码跑起来:环境准备与本地启动的最小可行路径
这套源码不是解压后就能跑。你需要依次准备后端、安卓、小程序三套运行环境,顺序别乱。我建议先起后端,因为双端都要依赖后端接口。下面是我整理的最小启动路径。
3.1 后端启动前的配置清单与数据库初始化
先从解压后的目录里找到后端工程,通常是backend或者server文件夹。你需要确认这几个基础环境已经装好:
| 组件 | 本地环境建议 | 用途 |
|---|---|---|
| JDK | JDK 8/11(看 pom.xml 里指定版本) | 编译运行 Spring Boot |
| MySQL | 5.7 或 8.0 | 持久化业务数据 |
| Redis | 3.2 以上 | 缓存、分布式锁、购物车临时数据 |
| Maven | 3.6 以上(或用 mvnw 包装器) | 拉取依赖并打包 |
确认环境后,把仓库根目录下类似doc/sql/init.sql的脚本导入数据库。常见做法是先用命令行创建数据库,再导入表结构:
mysql -uroot -p123456 -e "CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p123456 shop < init.sql逻辑说明:第一条命令创建shop数据库,指定utf8mb4字符集。做电商时商品名、用户收货地址都可能有表情符号,老旧的utf8存表情会报错,所以必须用utf8mb4。第二条命令把初始化脚本导入,脚本里通常包含建表和基础数据。
参数说明:-uroot -p123456是命令行连接 MySQL 的常见写法,实际环境请替换成你自己的账号密码。如果init.sql文件里本身有CREATE DATABASE语句,那你需要手动把文件里的库名改成你的本地库名,否则导入时会报数据库已存在或权限不足。
导入完成后,修改后端配置文件。常见位置是src/main/resources/application.yml或application.properties。至少改三处:数据库地址、Redis 地址、上传文件本地路径。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: 127.0.0.1 port: 6379 server: port: 8080 file: upload-dir: /data/upload逻辑说明:这个配置片段覆盖了最常见的三个连接点。serverTimezone参数非常关键,不设时 MySQL 驱动会拿 JVM 默认时区和你数据库时区做对照,可能产生“差 8 小时”的异步订单时间错乱。file.upload-dir是商品图片上传目录,Windows 下要改成D:/shop-upload这种盘符路径,否则启动后传图片会报路径不存在。
参数说明:url里的useUnicode=true和characterEncoding=utf8mb4缺一不可,特别是 MySQL 8.0 下,编码不对会导致中文乱码。serverTimezone=Asia/Shanghai建议固定,不要用UTC,否则商城后台看到的订单时间和你手机上的时间总是对不上。
最后在 IDEA 或命令行里启动:
mvn spring-boot:run -DskipTests启动后看到Started Application字样,说明后端就绪。你可以用浏览器访问http://127.0.0.1:8080验证,如果返回 404 没关系,只要不是 503/连接拒绝即可,说明端口和连接池没问题。
3.2 安卓端编译:改三个必改项就能链接测试服
安卓端用 Android Studio 打开android目录后,第一个坑是 Gradle 同步速度。常见做法是先检查gradle-wrapper.properties里的发行版地址,如果下载缓慢,可以替换为国内镜像,然后修改build.gradle中的仓库地址。这里不展开镜像细节,只想强调:不要一开始就追求使用最新版 Android Gradle Plugin,源码作者本来用的版本能编译过就最省事。
改到能连上你的本地后端,有三个必改项。第一项是网络权限,Android 工程里release包默认不允许明文 HTTP 请求,而本地测试后端往往是 HTTP。打开AndroidManifest.xml,在application节点加一行:
<application android:usesCleartextTraffic="true" ...>逻辑说明:usesCleartextTraffic设为true后,Android 9 以上系统才允许访问http://接口。上线时如果后端换成 HTTPS,这段配置可以移除。很多新手直接跑源码时遇到“网络请求失败”,90% 是这个配置没开。
参数说明:这个属性从 Android 8.0 开始生效,targetSdk 28 以下可能不强制。但它不是全局万能药,如果你只在debug下调试,更推荐在src/debug/AndroidManifest.xml里单独配,这样正式包不会被降级。
第二项是改接口地址。通常在utils/HttpConfig或Constants.java里定义BASE_URL,找到后用 Ctrl+R 全部替换为http://你的局域网IP:8080/api/:
public static final String BASE_URL = "http://192.168.31.5:8080/api/";逻辑说明:本地测试后端时,手机和电脑必须处于同一局域网。写127.0.0.1只对电脑上的模拟器有效,真机调试必须填电脑的局域网 IP。用ipconfig或ifconfig查看自己电脑的 IP 后填入即可。
参数说明:注意最后要带/api/或对应的前缀,否则很多请求会出现 404。如果源码里每个请求方法里都写死了/api/goods/list,而你的BASE_URL只到http://ip:8080,那地址就会变成/api/api/goods/list。改完地址后,建议直接搜索BASE_URL被引用处,确认拼接方式。
第三项是签名配置。很多开源源码会使用一个默认的debug.keystore,你可以直接靠它编译。但如果你想把包装到别人手机上测试,建议生成自己的签名文件,然后在build.gradle里配置:
android { signingConfigs { release { storeFile file("../release.jks") storePassword "your-password" keyAlias "shop" keyPassword "your-password" } } buildTypes { release { signingConfig signingConfigs.release } } }逻辑说明:这段配置把你的正式签名文件绑定到 release 包。商城应用涉及支付和登录,签名不一致会导致无法覆盖安装。如果你只是本地自测,用 debug 签名就没问题;但真正给别人测试时,debug 包的签名是固定的,可能与其他模块冲突。
参数说明:storeFile路径相对于app模块目录,所以../release.jks指项目根目录下。密码不要写死在代码里,正式团队会使用环境变量读取。这里为方便复现才使用明文。
编译时如果遇到 SDK 版本不匹配,把compileSdkVersion和minSdkVersion改成本机已安装的版本即可。不要神话版本号,能用跑的源码版本就是好版本。
3.3 iOS端与小程序端的联调要点
iOS 端打开.xcworkspace后,先执行pod install安装依赖。如果你的 Mac 上没有 CocoaPods,终端里sudo gem install cocoapods可以搞定。然后同样要改接口地址,常见位置是一个名为API.swift或NetworkConfig.swift的配置文件:
struct APIConfig { static let baseURL = "http://192.168.31.5:8080/api/" }逻辑说明:Swift 工程里的静态常量定义,使用方式是APIConfig.baseURL。iOS 对 HTTP 明文请求限制更严格,ATS(App Transport Security)会强制要求 HTTPS。本地调试时,需要在Info.plist里临时允许本地网络:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsLocalNetworking</key> <true/> </dict>逻辑说明:NSAllowsLocalNetworking是 iOS 10 以后提供的豁免项,它只允许本地网络的 HTTP 请求,不影响外部连接。注意这个键不是NSAllowsArbitraryLoads,后者会直接禁用 ATS,上线审核容易被拒。
参数说明:这里使用的是localhost、局域网 IP 时有效。如果你的后端部署在公网,还是建议直接用 HTTPS,别和 ATS 较劲。
小程序端联调最简单,微信开发者工具里导入wechat目录,然后把app.js里的globalData接口地址改掉。同时要在开发者工具的“详情 → 本地设置”里勾选“不校验合法域名”。因为本地请求是http://192.168.31.5:8080,小程序官方要求配置 HTTPS 域名,开发阶段只能用这个开关绕过去。
小程序端的另一个开关是支付。如果源码里已经配置了真实的商户号,你可以直接在开发者工具里模拟支付;如果没有,下单后支付会停留在“待支付”状态。你需要在pay.js里看到调用逻辑:
wx.requestPayment({ timeStamp: data.timeStamp, nonceStr: data.nonceStr, package: data.package, signType: 'MD5', paySign: data.paySign, success: (res) => { // 支付成功回调,跳转订单详情 }, fail: (err) => { // 支付失败或取消 } })逻辑说明:这段代码是小程序端拉起微信支付的固定格式。wx.requestPayment接收的参数全部来自后端接口返回,前端不能自行构造,否则支付流程不会成功。联调时如果fail回调里报requestPayment:fail,先检查后端返回的签名是否与商户密钥匹配。
参数说明:package字段一般是prepay_id=xxx,由统一下单接口返回。signType老代码用 MD5,新微信支付要求 HMAC-SHA256,如果一直报签名错误,翻一下后端PayService里的加密方式。
4. 二次开发怎么下手:订单、支付与会员体系的改造顺序
源码跑通只是第一步,你真正要接的是自己的支付商户、自己的商品逻辑、自己的会员规则。我建议按“支付 → 库存 → 多端打通”这个顺序来做,因为支付直接决定你能不能收款,库存直接决定你会不会被投诉,多端打通决定用户能不能跨端查订单。
4.1 支付回调:从下单到异步通知的链路改造
支付是电商里最容易“翻车”的模块。常见流程是:用户下单成功 → 后端生成支付单 → 小程序或APP发起支付 → 微信/支付宝服务器在支付成功后调用你的回调接口 → 后端修改订单状态。这套源码里一般会有一个PayController,你要重点改造的就是回调接口的验签逻辑。
先找到源码中的回调入口,常见路径是/api/pay/notify。微信的异步通知会把 XML 或 JSON 以 POST 方式发送过来,你必须先验证签名,再校验订单金额,然后才更新订单状态。下面是一个标准的校验骨架:
@PostMapping("/pay/notify") public String payNotify(HttpServletRequest request) { String body = readRequestBody(request); boolean signOk = WxPayUtil.verifySign(body, "你的商户API密钥"); if (!signOk) { return "sign error"; } PayNotifyData data = WxPayUtil.parseNotify(body); Order order = orderService.getByOrderNo(data.getOutTradeNo()); if (order.getStatus() != OrderStatus.WAIT_PAY) { // 幂等处理,重复回调直接返回成功 return "success"; } if (Math.abs(order.getPayAmount() - data.getTotalFee()) > 0.01) { // 金额不一致,记录告警并拒绝 return "amount error"; } orderService.markPaid(order, data.getTransactionId()); return "success"; }逻辑说明:verifySign负责验签,parseNotify负责把支付平台的回调请求体解析成对象。这里有两个细节容易被忽略:第一是幂等,同一个支付结果微信可能通知多次,你必须在更新状态前检查订单当前状态;第二是金额比对,回调里的金额必须和你数据库里订单金额完全一致,否则可能是恶意伪造或参数串改。
参数说明:WxPayUtil是源码里的工具类,不同版本可能叫WechatPayUtil。如果你的源码没有,你需要自己引入微信支付 SDK 或参考官方文档实现。totalFee单位是分,所以和订单金额比较时要换算。这段代码里Math.abs(...) > 0.01是元,实际业务建议把金额统一转为整数分再比较,避免浮点误差操蛋的问题。
改造支付回调的核心要点是:先验签,再查单,再改状态。顺序错了会出大问题,比如你没查单就改状态,攻击者伪造“支付成功”通知,订单就白白发货了。跑通回调后,建议用后端日志记录完整的请求体和响应,方便排查。
4.2 商品规格与库存扣减:别信“超卖已解决”的鬼话
商城源码里的库存逻辑往往是重灾区。很多开源作者会告诉你“使用同步锁来防止超卖”,但我在压测时见过不少源码里其实是这么写的:
public synchronized boolean reduceStock(Integer skuId, Integer count) { Stock stock = stockMapper.selectForUpdate(skuId); if (stock.getCount() < count) { return false; } stock.setCount(stock.getCount() - count); stockMapper.updateById(stock); return true; }逻辑说明:这段写法看起来没问题,但synchronized只对单个 JVM 进程内的单把锁有效。只要后端一部署多实例,或者存在多个手机同时请求,锁就失效了。selectForUpdate是数据库行锁,但你这行代码是否真的开启了事务?如果没有事务,行锁会立即释放,下一秒其他请求就还能读到旧数据。
我常用的库存扣减方案是 Redis + Lua 脚本,把“检查库存并扣减”这两个操作原子化。下面是 Lua 脚本的常见写法:
local key = KEYS[1] local current = tonumber(redis.call('get', key)) local decrement = tonumber(ARGV[1]) if current and current >= decrement then redis.call('decrby', key, decrement) return 1 end return -1逻辑说明:这段脚本先读取 Redis 中的库存数量,判断是否足够;足够才执行减库存操作。由于 Redis 是单线程执行 Lua 脚本,整个流程不存在并发交错。你在 Java 里调用时,把skuId对应的库存 key 传进去,把扣减数量传进ARGV。扣减成功后,再把操作异步同步到数据库。
参数说明:KEYS[1]是库存 key,比如sku:1001:stock。ARGV[1]是本次需扣减的数量。如果你的商品支持多规格,每个规格一个 key。注意 Redis 的库存 key 必须和数据库库存初始化时保持一致,否则会出现“Redis 显示有货,数据库没货”的不一致。
订单生成和库存扣减的事务是另一个坏点。常见正确做法是先扣库存,再创建订单,如果创建订单失败则回补库存。这个顺序能最大程度避免超卖。源码里如果先建订单再扣库存,你要反过来重构。改造时把库存扣减和订单创建放到同一个事务里,但不要把 Redis 操作嵌入数据库事务。这里用编程式事务会更稳:
@Transactional public Long createOrderWithStock(OrderDTO dto) { // 1.扣减数据库库存(乐观锁) int result = stockMapper.decreaseStockBySkuId(dto.getSkuId(), dto.getCount()); if (result == 0) { throw new StockNotEnoughException("库存不足"); } // 2.创建订单 return orderMapper.insert(dto); }逻辑说明:decreaseStockBySkuId的 SQL 里要带stock >= count条件,让 MySQL 本身承担判断,而不是先查后改。只有数据库更新行数为 0 时,说明库存不足,事务回滚。这里的乐观锁思路简单可靠,比synchronized强得多。
参数说明:@Transactional保证两步要么都成功要么都失败。但要注意:这个事务里不能包含远程调用,否则会长时间占用数据库连接。如果你的项目还需要在扣减库存后发送消息通知,建议放到事务提交后的事件监听器里。
4.3 小程序登录态与APP端的多端打通
一个零售商城几乎必然是小程序、安卓、iOS 三端同时上。用户在小程序里买了东西,再到 APP 里就看不到订单,这种体验会被骂死。源码里如果每一端各用一个登录表,那你要做的第一件事就是建立统一用户中心。
常见做法是维护一张users表,字段包括user_id、phone、wechat_openid、wechat_unionid。小程序登录走wx.login拿到code,后端用code换取openid;APP 端走手机号 + 短信验证码登录。登录成功后,后端统一签发 token:
public String loginByWechat(String code) { WechatSession session = wechatApi.code2Session(code); User user = userMapper.selectByOpenid(session.getOpenid()); if (user == null) { user = new User(); user.setWechatOpenid(session.getOpenid()); user.setWechatUnionid(session.getUnionid()); userMapper.insert(user); } return jwtUtil.createToken(user.getId()); }逻辑说明:code2Session调用微信接口换取openid和session_key。这里要注意,同一个用户在微信生态里有唯一的unionid,前提是你的小程序和公众号处于同一个开放平台账号下。仅靠openid无法跨小程序和 APP 识别用户,因为 APP 不调用微信登录时,你拿不到同一个openid。
参数说明:当用户在小程序用微信登录,之后在 APP 用手机号登录时,系统需要根据手机号去users表里匹配已经存在的wechat_openid。匹配逻辑一般设计为:用户绑定手机号时,如果phone还有空位,就把当前登录凭证所代表的用户手机号补上。如果你不做这一步,会员体系会分裂成“微信用户”和“手机用户”两套。
多端打通的硬骨头是购物车。小程序购物车、APP 购物车如果各存各的本地,用户换端就丢失。我建议把购物车数据同步到后端 Redis,key 格式为cart:{userId},value 用 JSON 存商品列表和数量。重写CartApiController时,取购物车永远从后端接口拉取,不再信任本地缓存。这样可以保证用户在 APP 里看到的购物车和小程序里完全一致。
5. 避坑:这套源码最常见的6个翻车现场与排查顺序
无论你是新手还是熟手,拿到一套开源商城后,大概率会在这几个地方栽跟头。我按从“启动时”到“上线后”的时间顺序,写一份排查手册。每一条都是我的血泪经验。
5.1 后端启动就报“端口被占”或“数据库连接失败”
现象:执行mvn spring-boot:run后,控制台出现Web server failed to start. Port 8080 was already in use.或者Communications link failure。原因:本地已经有其他进程占用 8080 端口,或者 MySQL 服务没启动,又或者连接地址里的库名写错。解决:在application.yml里把端口改成 8081,然后检查 MySQL 是否启动。排查顺序是先看 MySQL 是否能连上,再看库名和账号,最后看端口。常见做法是先用命令netstat -ano | grep 8080找出占用端口的进程,把这个 PID 杀掉或改端口。如果你是因为安装了多个 MySQL 版本导致服务未启动,Windows 下点击“服务”里启动 MySQL,macOS 下执行brew services start mysql。
5.2 安卓编译时Gradle依赖下载慢到发疯
现象:Android Studio 同步 Gradle 进度条一直卡在Downloading,半小时后提示Could not resolve com.android.support:appcompat-v7:28.0.0。原因:源码引用的第三方库要从 Google 的 Maven 仓库下载,国内网络连接不稳定。解决:修改根目录build.gradle里的仓库地址,把google()和mavenCentral()替换为国内镜像源,同时把distributionUrl里的distributionUrl换成阿里云镜像。注意不是把所有仓库都删掉,而是在原有仓库后面追加镜像仓库,这样既能优先走国内镜像,镜像没有的还能从源仓库拉取。改完后重新构建,如果还慢,就检查是否是代理设置导致。这里不建议把 Gradle 版本升到最新,很多老工程在高版本下编译会报类找不到。
5.3 iOS打包后登录接口全部超时
现象:模拟器上登录没问题,真机安装测试包后,一键登录/手机号登录全部转圈超时。原因:真机走的是局域网,电脑防火墙拦截了 App 访问的端口;或者你填的后端地址是127.0.0.1。解决:把接口地址改成电脑的局域网 IP,然后在系统防火墙里放行 8080 端口。iOS 真机连不上时,打开终端ping 电脑IP能通,说明网络通,但端口可能被墙。常见做法是临时关闭防火墙测试,确认是防火墙问题后再添加入站规则。另外检查Info.plist的 ATS 豁免项,如果你的后端是本地 HTTP,需要保持NSAllowsLocalNetworking。如果后端已经部署到 HTTPS,这条可以忽略。
5.4 小程序里微信支付总是返回“商户号未开通”
现象:开发者工具里点击支付,弹窗报错requestPayment:fail或后端日志里返回“商户号未开通”。原因:你用的支付商户号是源码作者自己在 demo 里写的,没有真实权限。解决:在微信商户平台申请自己的商户号,然后把源代码里的mch_id、api_key、appid全部替换成自己的。你还需要在小程序后台配置“支付商户号”关联,并且把开发者工具中的 appid 换成和小程序后台一致的 appid。需要特别注意的是:微信支付要求小程序与商户号关联的是同主体的账号,测试阶段如果主体不一致,即使参数填对了也会报错。这一步没有捷径,只有按微信官方指引逐步申请。
5.5 上线后订单状态卡在“已付款”不动
现象:用户支付成功,小程序跳回订单页,但订单状态依旧是“待付款”,后台一直收不到支付回调。原因:回调地址没有暴露到公网,或者回调地址没有配置到微信支付后台。解决:登录微信商户平台,在“产品中心 → 开发配置”里把支付回调域名设置为你服务器的公网 HTTPS 地址。然后用浏览器或 Postman 模拟 POST 到你回调地址,确认不返回 404。后端收到回调后只有返回字符串success,微信才会停止重试。如果回调域名是 IP 而不是域名,微信支付是不允许的,必须用备案域名。本地联调时可以用内网穿透工具,但上线前一定要换成正式域名,另外回调接口必须支持 HTTPS。
5.6 库存越卖越多:超卖的另一种姿势
现象:活动促销时,后台库存从 100 瞬间变成负数,或者用户明明看到“库存不足”,却仍然能下单。原因:源码使用了selectForUpdate但没加事务,或者使用updateById更新库存时没有携带stock >= count条件。解决:按我在 4.2 里的思路重构库存扣减。先检查stockMapper的 SQL,确认更新语句是UPDATE sku_stock SET stock = stock - #{count} WHERE sku_id = #{skuId} AND stock >= #{count}。如果源码里写的是SET stock = #{newStock},那就是读出来再写回去的经典错误,并发一高必然错乱。重构后做一次 100 并发下单压测,观察最终库存是否为 0。
6. 让商城活下来的进阶技巧:灰度发布、监控与促销引擎的取舍
源码跑起来只是开始,真正的考验是运营。我见过太多团队把精力浪费在自研促销引擎上,最后活动优惠计算错了,赔钱还得道歉。这里分享三个我更看重的进阶技巧。
第一,灰度发布别搞复杂。虽然源码支持微服务,你也不需要一上来就搞网关和注册中心。常见做法是在 Nginx 层按 IP 或 Cookie 分流,比如把 10% 的流量切到新版后端。命令上,用nginx.conf里的split_clients就能实现:
split_clients "${remote_addr}" $canary { 10% canary_upstream; * stable_upstream; }逻辑说明:这个配置根据客户端 IP 做一致性哈希,让同一用户固定访问新版本或旧版本。灰度期间,你在canary_upstream指向的服务器上部署新版代码,通过比对日志观察是否有异常。这样即使用户遇到 bug,也只会影响 10% 的测试人群,不至于全量崩盘。
参数说明:${remote_addr}是 Nginx 内置变量,表示客户端 IP。10% 可以改成 5% 或 20%,调灰度比例时只需要改这个数字并 reload Nginx。这个方案比微服务的全链路灰度简单得多,适合中小团队。
第二,订单监控必须在你睡觉时替你干活。写一个定时任务,每分钟扫一次“超过 15 分钟的待付款订单”和“已支付但超过 1 小时未发货的订单”,发送告警到企业微信或钉钉。源码里一般已经有spring-boot-starter-quartz,没有的话用@Scheduled也能搞定。定时任务的 SQL 可以设计成:
SELECT order_no FROM orders WHERE status = 'PAID' AND create_time < NOW() - INTERVAL 1 HOUR逻辑说明:这条 SQL 找出超过 1 小时还没发货的订单。如果你把这条查询放到定时任务里,一旦结果非空立即推送告警。这里要特别注意时区问题,订单表里的create_time如果存的是本地时间,而数据库时区是 UTC,那 INTERVAL 判断会出错。建议全局统一使用bigint或datetime且固定Asia/Shanghai存储。
参数说明:INTERVAL 1 HOUR可改为INTERVAL 30 MINUTE,根据你的发货时效调整。商品可以存在多个仓库时,最好再关联一个仓库名称字段,这样告警消息能告诉你是哪个仓漏发了。
第三,促销引擎能用现成就别自己写。开源商城自带的满减券、优惠券逻辑往往只覆盖了“满 X 减 Y”这一种场景,但运营经常要的“第2件半价”“会员日折扣”“多件折扣叠加”它都支持不了。如果你在商品详情页加一个复杂的优惠展示,建议先看代码里有没有DiscountStrategy之类的策略接口。有的话就实现一个策略类,没有的话就引入第三方独立促销服务,不要让促销逻辑和订单逻辑耦合在一起。我自己吃过这个亏:在模拟项目X上,运营要求“满 299 减 30 再叠加会员 95 折”,结果源码自带的CartService里优惠计算是硬编码的,我改了三天,最后还是用线程池加规则引擎才彻底搞明白优先级。
这三件事做完,一个商城系统才真正具备可运营的骨架。成熟度不是看功能多不多,而是看你会不会在突发故障时冷静查日志,能不能在流量进来之前找到隐患。商城开发没有一锤子买卖,每一次促销都是一次压测。
现在的习惯是:每次给商城加新功能之前,先问自己一句“如果数据错了,我怎么发现”。带着这个问题去写日志、加监控、做灰度,会比堆代码稳得多。希望这些经验能帮到你。
本文还有配套的精品资源,点击获取