简介这是一套面向Android开发者进阶后端技能的全栈小商城实战项目涵盖客户端与服务端完整实现适合具备Java基础、希望掌握SpringBootMybatis主流后端技术栈的初学者系统学习。资源包含Android端MVC架构与Java后端SpringBootMybatisRedis分布式缓存双模块源码支持下单、购物车、支付宝支付等核心电商功能并附带数据库脚本与详细项目说明文档。压缩包共270个文件以164个Java源码含Controller/Service/Mapper层、74个XML配置MyBatis映射与Generator模板、4个properties/yml配置文件及SQL建表脚本为主辅以Gradle构建文件、SDK依赖jar包与UI资源整体4.63MB结构清晰、模块解耦便于分层理解与二次开发。目前已有2260人学习下载提供从网络请求OkGo、屏幕适配AutoSize、权限管理RxPermissions到分页PageHelper、代码生成MyBatisGenerator等一线开发实践细节是少有的兼顾Android与后端协同开发的轻量级教学级项目。1. 一个能跑通、能调试、能改需求的小商店全栈项目为什么值得你花20分钟搭一遍很多刚学完 Java 基础和 Android 开发的同学卡在「知道每个技术点但拼不出完整系统」这一步。你可能写过 Spring Boot 的 HelloController也用 RecyclerView 展示过商品列表但当「用户登录 → 查看商品 → 加入购物车 → 提交订单 → 后端扣库存 → Android 端显示支付成功」这一整条链路要串起来时就发现数据库字段对不上、API 返回格式不一致、Token 传丢了、MyBatis 的 resultMap 没配对、Android 端 Retrofit 接口定义和后端 Controller 方法签名不匹配……这些不是理论问题是工程落地的毛细血管级断点。这个标题里的「AndroidJava后端SpringbootMybatis小商店项目」本质是一个最小可行闭环MVCRESTSQLite/MySQL 双端协同它不追求高并发或微服务但强制覆盖了身份认证JWT、商品CRUD、购物车本地服务端同步、订单状态机、前后端时间戳与ID生成一致性等真实业务中绕不开的细节。尤其适合两类人一是准备 Java 或 Android 方向校招面试者可直接拆解为「MyBatis 动态SQL怎么写」「Spring Boot 如何统一返回体」「Android 如何安全存储 Token」等高频考点二是想快速验证自己技术栈整合能力的中级开发者——它不藏私所有接口契约明确定义在api.md或 Controller 注释里数据库建表语句完整连application.yml中 MySQL 连接池的max-active: 8都写了注释说明依据。提示这不是玩具项目。它包含真实业务约束比如「下单时校验商品库存是否大于等于购买数量」在 Service 层有显式if (stock quantity) throw new BusinessException(库存不足)且该异常被全局ControllerAdvice捕获并转为标准 JSON 错误响应Android 端对应地做了 Toast 提示和按钮置灰防重复提交。这种「错误路径全覆盖」的设计才是工程化项目的分水岭。2. 从零启动本地运行这个小商店项目的5个关键步骤与参数含义要让这个 ZIP 包里的项目真正活起来不能只解压双击运行。必须理解每个环节的职责边界和协作契约。下面按执行顺序拆解每步都给出命令、配置项说明及失败时的定位线索。2.1 数据库初始化用 SQL 脚本创建表结构而非依赖 Hibernate 自动建表Spring Boot MyBatis 组合默认不开启自动建表区别于 JPA这是刻意为之的工程实践生产环境严禁 ORM 自动变更 DDL。项目提供的shop_db.sql是唯一可信的数据源。-- shop_db.sql 片段MySQL 8.0 兼容 CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(64) NOT NULL COMMENT 用户名, password varchar(128) NOT NULL COMMENT BCrypt 加密后的密码, phone varchar(16) DEFAULT NULL COMMENT 手机号, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;注意脚本中ENGINEInnoDB和CHARSETutf8mb4是硬性要求。若用 MySQL 5.7 且未开启innodb_file_per_tableON建表可能静默失败若字符集设为utf8非utf8mb4后续插入 emoji 或四字节中文会报错Incorrect string value。执行前务必确认mysql --version # 必须 ≥ 5.7 mysql -u root -p -e SHOW VARIABLES LIKE character_set%;2.1.1 创建数据库并导入数据# 1. 登录 MySQL 创建数据库注意指定字符集 mysql -u root -p -e CREATE DATABASE shop_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 导入 SQL 脚本确保路径正确 mysql -u root -p shop_db /path/to/shop_db.sql # 3. 验证表是否创建成功关键 mysql -u root -p -D shop_db -e SHOW TABLES; # 应输出t_cart, t_goods, t_order, t_order_item, t_user 等 5 张表提示如果SHOW TABLES为空不要急着重试。先检查shop_db.sql文件头是否有USE shop_db;—— 若有需删除该行再导入否则在非 shop_db 数据库下执行会静默失败。这是新手最常踩的坑。2.2 后端启动Spring Boot 配置文件中的 3 个必调参数解压后端模块通常为server/或springboot-server/目录打开src/main/resources/application.yml。以下三个参数必须根据你的本地环境修改参数默认值必须修改原因示例值spring.datasource.urljdbc:mysql://localhost:3306/shop_db?useSSLfalseserverTimezoneAsia/Shanghai若 MySQL 端口不是 3306或数据库名不是shop_db连接直接失败jdbc:mysql://127.0.0.1:3307/shop_db?...spring.datasource.usernameroot若 MySQL 设置了非 root 用户此处需同步shop_usermybatis.configuration.map-underscore-to-camel-casetrue决定 MyBatis 是否自动转换下划线字段名到驼峰属性名。若设为 falset_user.username就无法映射到User.getUsername()所有查询返回 nulltrue保持默认# application.yml 关键片段 spring: datasource: url: jdbc:mysql://localhost:3306/shop_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_mysql_password # 明文密码仅限本地开发 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: map-underscore-to-camel-case: true # 此开关影响所有实体类字段映射 mapper-locations: classpath:mapper/*.xml注意password字段若含特殊字符如,/,:需用 URL 编码。例如密码为pss/w0rd应写为p%40ss%2Fw0rd。否则 Spring Boot 解析 JDBC URL 时会将误认为主机分隔符。2.2.2 启动后端并验证 API 可达性# 在 server/ 目录下执行确保已安装 JDK 11 mvn clean spring-boot:run # 启动成功后终端应出现 # Tomcat started on port(s): 8080 (http) with context path # Started ShopApplication in X.XXX seconds (JVM running for Y.YYY) # 验证基础接口用 curl 或浏览器访问 curl -X GET http://localhost:8080/api/goods/list # 正常响应{code:200,msg:success,data:[{id:1,name:iPhone 15,price:5999.0}]}提示若返回Whitelabel Error Page首先检查http://localhost:8080/actuator/health是否返回{status:UP}。若健康检查失败说明数据库连接未通回溯 2.1 步骤若健康检查通过但业务接口 404检查RestController类是否在SpringBootApplication扫描包路径内默认扫描启动类所在包及子包。2.3 Android 端配置替换 BaseUrl 与启用网络权限Android 项目通常为app/模块需两处硬编码修改否则请求全部超时2.3.1 修改 API 基地址打开app/src/main/java/com/example/shop/network/ApiService.java或类似路径找到 Retrofit 实例构建处// 错误写法写死 IP无法跨设备调试 public static final String BASE_URL http://192.168.1.100:8080/; // 正确写法适配本机回环 USB 调试 public static final String BASE_URL http://10.0.2.2:8080/; // Android 模拟器访问宿主机 // 或 public static final String BASE_URL http://192.168.x.x:8080/; // 真机调试时填电脑局域网 IP提示10.0.2.2是 Android Studio 模拟器预设的宿主机别名真机调试必须用电脑的局域网 IP如192.168.31.123。在电脑终端执行ipconfigWindows或ifconfig | grep inet Mac/Linux获取。切勿用localhost或127.0.0.1Android 设备无法解析。2.3.2 声明网络权限并允许 HTTP 明文流量在app/src/main/AndroidManifest.xml的application标签内添加application android:usesCleartextTraffictrue !-- 允许 HTTP 请求开发阶段必需 -- ... uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / /application注意android:usesCleartextTraffictrue是开发阶段的妥协。上线前必须切换为 HTTPS并在res/xml/network_security_config.xml中配置证书固定Certificate Pinning。此配置仅用于本地联调。2.4 运行 Android App解决 Gradle 同步与依赖冲突Android Studio 打开项目后首次同步常因依赖版本不兼容报错。核心矛盾在于项目使用较老的com.android.tools.build:gradle:3.6.4对应 Gradle 5.6.4而新版本 AS 默认推荐 8.x。2.4.1 强制降级 Gradle 版本修改项目根目录下的gradle/wrapper/gradle-wrapper.properties# 将 distributionUrl 改为项目兼容版本 distributionUrlhttps\://services.gradle.org/distributions/gradle-5.6.4-all.zip2.4.2 处理常见的依赖冲突若同步时报错Duplicate class android.arch.lifecycle...说明引入了 AndroidX 与旧 Support Library 混用。在app/build.gradle中添加android { compileSdkVersion 29 // 项目通常基于 Android 10 defaultConfig { targetSdkVersion 29 // 添加以下两行强制迁移至 AndroidX android.useAndroidXtrue android.enableJetifiertrue } } dependencies { // 替换所有 support 库为 androidx 对应版本 implementation androidx.appcompat:appcompat:1.1.0 implementation androidx.recyclerview:recyclerview:1.1.0 // 移除所有以 com.android.support: 开头的依赖 }提示若Build Clean Project后仍报Cannot resolve symbol R右键app模块 →Load project强制重新索引资源。这是 AS 缓存导致的假性错误。3. 深度调试MyBatis 与 Android 网络层的 3 个典型问题定位方法当界面空白、按钮无响应、日志无报错时问题往往藏在数据流转的缝隙里。这里给出针对本项目架构的精准排查路径。3.1 后端 MyBatis 查询结果为空先查 SQL 日志与实体映射MyBatis 默认不打印 SQL需主动开启。在application.yml中添加logging: level: com.example.shop.mapper: debug # 指定 Mapper 接口包路径 org.springframework.jdbc.core.JdbcTemplate: debug启动后控制台将输出类似内容 Preparing: SELECT id, name, price, stock FROM t_goods WHERE status ? Parameters: 1(Integer) Columns: id, name, price, stock Row: 1, iPhone 15, 5999.0, 100 Total: 1注意若看到Parameters但无Row输出说明 SQL 执行了但没查到数据。此时检查WHERE条件如status 1是否与数据库中t_goods.status值一致若连Preparing都没有说明 Mapper 接口方法未被调用检查 Controller 层Autowired的 Service 是否注入成功IDE 会标黄提示。3.1.1 实体类字段与数据库列名不匹配的 2 种修复方式假设数据库列名为goods_name而 Java 实体类属性为goodsName方式一推荐用 Results 注解显式映射Select(SELECT id, goods_name, price FROM t_goods) Results({ Result(property goodsName, column goods_name), Result(property id, column id) }) ListGoods selectAll();方式二在全局配置中开启自动映射需确保 mybatis.configuration.map-underscore-to-camel-case: true// Goods.java public class Goods { private Long id; private String goodsName; // 自动匹配 goods_name private BigDecimal price; }提示map-underscore-to-camel-case: true仅对列名到属性名生效对Param(goodsName)中的参数名无效。若 DAO 方法用Param传参SQL 中仍需写#{goodsName}。3.2 Android 端 Retrofit 接口返回 null检查泛型擦除与 JSON 解析Retrofit 默认用 Gson 解析 JSON。若后端返回{code:200,data:[{id:1,name:A}]}而 Android 端定义为// 错误泛型被擦除Gson 不知 data 内部是 Goods 列表 CallBaseResponse getGoodsList(); // 正确用 TypeToken 保留泛型信息 Type type new TypeTokenBaseResponseListGoods(){}.getType(); CallBaseResponseListGoods call apiService.getGoodsList();更稳妥的做法是定义具体泛型响应类public class BaseResponseT { private int code; private String msg; private T data; // getter/setter } // 接口定义 GET(api/goods/list) CallBaseResponseListGoods getGoodsList();3.2.1 查看真实网络请求与响应无需抓包工具在OkHttpClient构建时添加日志拦截器HttpLoggingInterceptor logging new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 打印请求/响应体 OkHttpClient client new OkHttpClient.Builder() .addInterceptor(logging) .build();运行 App 后Logcat 中搜索OkHttp可见-- GET http://10.0.2.2:8080/api/goods/list -- END GET -- 200 http://10.0.2.2:8080/api/goods/list (123ms) {code:200,msg:success,data:[{id:1,name:iPhone 15,price:5999.0}]} -- END HTTP注意若看到{code:500,msg:Internal Server Error}说明后端抛出未捕获异常回溯 3.1 步骤查日志若看到java.net.ConnectException: Failed to connect to /10.0.2.2:8080检查后端是否运行、防火墙是否放行 8080 端口、IP 地址是否填错。3.3 购物车数据不同步理解本地 SQLite 与服务端 REST 的协同逻辑本项目购物车采用「混合存储」未登录时数据存 Android 本地 SQLiteCartDao登录后同步到服务端t_cart表。关键逻辑在CartManager.javapublic void addToCart(Goods goods) { if (isLogin()) { // 已登录调用 API 添加到服务端 apiService.addCart(goods.getId(), goods.getCount()) .enqueue(new CallbackResponseBody() { Override public void onResponse(CallResponseBody call, ResponseResponseBody response) { syncLocalCartToServer(); // 同步完成后再拉取最新服务端数据覆盖本地 } }); } else { // 未登录存入本地 SQLite cartDao.insert(new CartItem(goods)); } }3.3.1 调试购物车同步的黄金三步法确认本地 SQLite 是否写入在 Android Studio 的Device File Explorer中路径/data/data/com.example.shop/databases/cart.db→ 右键Save As导出用 DB Browser for SQLite 打开查看cart_items表。确认服务端 t_cart 表是否更新直接执行 SQLSELECT * FROM t_cart WHERE user_id 1;user_id 为当前登录用户 ID。比对两端数据一致性若本地有 3 条服务端只有 1 条说明syncLocalCartToServer()方法未执行或执行失败。在该方法首行加断点观察cartDao.getAll()返回值是否为空。提示syncLocalCartToServer()内部会先清空服务端购物车再批量插入这是为避免重复商品。若网络中断导致清空成功但插入失败会造成数据丢失。生产环境应改为「幂等更新」如用INSERT ... ON DUPLICATE KEY UPDATE。4. 进阶实战为小商店项目添加「微信支付回调通知」的完整链路支付功能是电商类项目的标志性能力。本节以微信支付沙箱环境为例演示如何在现有 Spring Boot MyBatis 架构上安全接入支付回调不改动原有订单核心逻辑。4.1 微信支付沙箱环境配置与密钥生成微信支付沙箱无需企业资质专为开发测试设计。登录 微信支付商户平台 →「开发配置」→「沙箱环境」→「APIv3 密钥」下载apiclient_key.pem私钥和apiclient_cert.pem证书在application.yml中新增配置wechat: pay: mch-id: 1900000109 # 沙箱商户号 app-id: wx8888888888888888 # 沙箱 AppID notify-url: http://your-ngrok-domain.com/api/pay/callback # 外网可访问地址 key-path: classpath:apiclient_key.pem cert-path: classpath:apiclient_cert.pem注意notify-url必须是公网地址。本地开发用ngrok或localtunnel映射ngrok http 8080→ 获取https://abc123.ngrok.io填入notify-url。4.2 编写支付回调 Controller验证签名 更新订单状态微信支付回调是 POST 请求Body 为加密 JSON。Spring Boot 需用RequestBody接收原始流PostMapping(/api/pay/callback) public ResponseEntityString handlePayCallback(HttpServletRequest request) throws Exception { // 1. 读取原始请求体关键不能用 RequestBody会破坏签名验证 String body StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 2. 验证签名微信提供官方 SDK boolean isValid WechatPayValidatorForV3.verify( request.getHeader(Wechatpay-Signature), request.getHeader(Wechatpay-Timestamp), request.getHeader(Wechatpay-Nonce), request.getHeader(Wechatpay-Serial), body ); if (!isValid) { return ResponseEntity.status(401).body(Invalid signature); } // 3. 解密回调内容微信用 AES-GCM 加密 String decryptData AesUtil.decryptToString( Files.readAllBytes(Paths.get(classpath:apiclient_key.pem)), MD5, // 加密算法 body ); // 4. 解析 JSON 并更新订单 JSONObject json new JSONObject(decryptData); String outTradeNo json.getJSONObject(resource).getString(out_trade_no); String transactionId json.getJSONObject(resource).getString(transaction_id); orderService.updateOrderStatus(outTradeNo, OrderStatus.PAID, transactionId); return ResponseEntity.ok(SUCCESS); // 必须返回纯文本 SUCCESS }4.2.1 订单状态更新的 MyBatis 实现在OrderMapper.xml中编写条件更新语句确保幂等性update idupdateOrderStatus UPDATE t_order SET status #{status}, pay_time NOW(), transaction_id #{transactionId} WHERE order_no #{orderNo} AND status #{oldStatus} !-- 防止重复支付导致状态回滚 -- /update提示AND status #{oldStatus}是关键。若订单已是PAID此 SQL 影响行数为 0updateOrderStatus()返回0可记录告警日志但不抛异常保证回调接口高可用。4.3 Android 端发起支付调用统一下单 API 并唤起微信支付流程分两步后端调用微信统一下单 API 获取prepay_idAndroid 端用该 ID 唤起微信 App。4.3.1 后端统一下单接口供 Android 调用PostMapping(/api/pay/unifiedorder) public ResultMapString, String unifiedOrder(RequestBody PayRequest request) { // 1. 校验订单合法性是否存在、未支付 Order order orderService.getByOrderNo(request.getOrderNo()); if (order null || !order.getStatus().equals(OrderStatus.UNPAID)) { return Result.fail(订单不存在或已支付); } // 2. 构造微信统一下单参数 MapString, String params new HashMap(); params.put(appid, wechatPayProperties.getAppId()); params.put(mch_id, wechatPayProperties.getMchId()); params.put(nonce_str, UUID.randomUUID().toString().replace(-, )); params.put(body, 小商店订单); params.put(out_trade_no, request.getOrderNo()); params.put(total_fee, String.valueOf(order.getTotalPrice().multiply(new BigDecimal(100)).longValue())); // 单位分 params.put(spbill_create_ip, 127.0.0.1); params.put(notify_url, wechatPayProperties.getNotifyUrl()); params.put(trade_type, APP); // 3. 签名并发送请求微信 SDK String sign WXPayUtil.generateSignature(params, wechatPayProperties.getKey()); params.put(sign, sign); String resultXml WXPayUtil.postXml(https://api.mch.weixin.qq.com/pay/unifiedorder, params); MapString, String respMap WXPayUtil.xmlToMap(resultXml); if (SUCCESS.equals(respMap.get(return_code)) SUCCESS.equals(respMap.get(result_code))) { MapString, String payParams new HashMap(); payParams.put(appid, respMap.get(appid)); payParams.put(partnerid, respMap.get(mch_id)); payParams.put(prepayid, respMap.get(prepay_id)); payParams.put(package, SignWXPay); payParams.put(noncestr, respMap.get(nonce_str)); payParams.put(timestamp, String.valueOf(System.currentTimeMillis() / 1000)); payParams.put(sign, WXPayUtil.generateSignature(payParams, wechatPayProperties.getKey())); return Result.success(payParams); } else { return Result.fail(respMap.get(err_code_des)); } }4.3.2 Android 端集成微信 SDK 唤起支付在app/build.gradle中添加依赖implementation com.tencent.mm.opensdk:wechat-sdk-android-without-mta:调用逻辑// 1. 调用后端 /api/pay/unifiedorder 获取 payParams apiService.unifiedOrder(orderNo).enqueue(new CallbackMapString, String() { Override public void onResponse(CallMapString, String call, ResponseMapString, String response) { if (response.isSuccessful()) { MapString, String payParams response.body(); // 2. 构造 PayReq PayReq req new PayReq(); req.appId payParams.get(appid); req.partnerId payParams.get(partnerid); req.prepayId payParams.get(prepayid); req.packageValue payParams.get(package); req.nonceStr payParams.get(noncestr); req.timeStamp payParams.get(timestamp); req.sign payParams.get(sign); // 3. 唤起微信 IWXAPI api WXAPIFactory.createWXAPI(this, req.appId); api.registerApp(req.appId); api.sendReq(req); } } });提示微信 SDK 要求appid必须与 AndroidManifest 中注册的com.tencent.mm.sdk.openapi.WXEntryActivity的android:exportedtrue一致且WXEntryActivity必须继承WXCallbackActivity。若唤起失败检查AndroidManifest.xml中activity声明是否遗漏exportedtrueAndroid 12 强制要求。至此一个具备真实支付闭环的小商店项目已完全打通。你不仅跑通了 ZIP 包里的代码更掌握了从数据库建模、前后端联调、异常定位到第三方服务集成的全链路工程能力。本文还有配套的精品资源点击获取
