简介:一份面向校园场景的Java综合性跑腿系统源码,适合有Java基础的学习者,以及课程设计或毕业设计的学生参考。系统覆盖在线下单、接单、订单状态跟踪等互助服务流程,包含用户注册登录、任务发布、抢单接单、订单管理等模块。压缩包共213个文件,其中54个Java源文件承载核心业务逻辑,99个XML文件用于Spring与MyBatis配置及数据映射,32个PNG与10个WebP图片提供界面素材,另有Gradle构建脚本等,整体约1.05MB,轻巧规整,便于快速导入阅读,已有429人学习下载。源码涉及Spring依赖注入、MyBatis动态SQL、MySQL数据库设计、RESTful API、WebSocket实时通信、JWT权限验证等技术点,并包含密码加密、SQL注入防护、单元测试等实践。通过研读源码可掌握从持久层到控制层再到前端交互的完整链路,学习分层架构与模块解耦思路,借鉴通用工具类、配置文件和异常处理写法,为项目开发提供扎实参考。
1. Java综合性校园跑腿系统源码:先看清这份资源能干什么
先直接说结论:这份 Java 校园跑腿系统源码,目标用户不是想学“Hello World”的新手,而是已经有点 Java 基础、想拿一个完整项目练手,或者课程设计、毕设需要一套能跑通前后端流程的从业者。压缩包里的文件不多,但每一样都指向一个真实场景——学生需要代取快递、代购商品,另一头学生接单完成,中间牵扯下单、接单、状态同步、权限校验和支付回调,几乎把 Java 开发的核心环节都串起来了。
从里面列出的 build.gradle、gradlew.bat、RootMainActivity.java 这几个关键文件能看出来,这项目的技术选型不是老旧的 JSP Servlet 那一套,而是走了 Spring 系后端加 Android 客户端的路线。后端用 Spring 和 MyBatis 管理业务逻辑与数据库交互,前端有安卓端入口在 RootMainActivity 里承载下单和状态查看,这种分工在校园场景里非常常见。换句话说,你拿到的不只是一个堆代码的压缩包,而是一条完整的业务链路:从用户打开 App 下单,到请求打到后端,再到任务状态被写进 MySQL,每一步都有对应的实现可以参考。
这篇文章接下来会按照“资源是什么 → 怎么跑起来 → 核心模块怎么改 → 坑在哪”的顺序拆解,重点落在你能直接照着操作的步骤和参数上。
2. Spring Boot 与构建链路:从 gradlew 到可运行后端
2.1 先看懂项目骨架:gradle 配置里藏着什么
打开压缩包,你会看到 gradlew.bat、gradlew、gradle-wrapper.jar、build.gradle、settings.gradle 这一套文件。新手第一次见往往容易懵,但其实这套东西解决的是一个非常实际的问题:别人拿到你的源码,不需要自己装指定版本的 Gradle,只要系统里有 JDK,就能通过 gradlew 脚本把对应版本的 Gradle 自动下载并执行构建。这就叫 Gradle Wrapper,是团队协作和项目交付里保证环境一致性的标准做法。
看一下 build.gradle,核心配置大概长这样:
plugins { id 'java' id 'org.springframework.boot' version '2.7.14' id 'io.spring.dependency-management' version '1.1.3' } group = 'com.campus' version = '1.0.0' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1' implementation 'mysql:mysql-connector-java:8.0.33' testImplementation 'org.springframework.boot:spring-boot-starter-test' }这段配置解决的是“项目依赖哪些东西”的问题。Spring Boot 2.7.x 是目前生产环境里使用率非常高的稳定版本,starter-web 负责把内嵌的 Tomcat 和 Spring MVC 一起拉进来,这样你不需要额外装 Web 服务器,直接跑 main 方法就能启动服务。starter-validation 提供参数校验注解,比如下单接口里用户传的手机号格式,靠它就能在后端入口拦一道,不用把脏数据放进业务逻辑。mybatis-spring-boot-starter 是 MyBatis 和 Spring Boot 的桥接包,版本要跟 Spring Boot 匹配,2.3.1 搭配 2.7.x 是经过大量项目验证的组合,不建议擅自升到 3.x,因为 MyBatis 3 的自动配置机制改动很大。
2.2 启动入口与 Controller 层:看清请求怎么进后端
压缩包里虽然没有直接列出后端的主类文件,但按照 Spring Boot 的标准结构,通常会在src/main/java/com/campus/RunnerApplication.java写一个带@SpringBootApplication注解的主类。整个启动流程是:gradlew 编译代码,把编译结果打包成 jar,然后执行 main 方法,Spring 容器开始扫描包路径,自动装配各种 Bean,最后把 Tomcat 端口打开,默认 8080。
在跑腿系统里,最典型的一个接口是用户下单,对应的 Controller 层代码大致如下:
@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public ResultVO createOrder(@RequestBody @Valid OrderCreateDTO dto) { OrderDO order = orderService.create(dto); return ResultVO.success(order); } }这个接口做的事情很直接:接收前端传过来的 JSON 数据,通过 Spring MVC 自动把 JSON 反序列化成OrderCreateDTO对象,@Valid触发校验框架检查字段合法性,校验通过后进入 Service 层处理业务逻辑,最后把结果封装成统一的ResultVO返回给前端。ResultVO是这类项目里约定俗成的统一返回结构,里面至少会有 code、message、data 三个字段,这样前端不用管后端到底抛了什么异常,只要看 code 是 0 还是非 0 就能决定下一步操作。
2.3 为什么选 Spring 管理 Bean 而不是手动 new
校园跑腿系统里,OrderService 依赖 OrderMapper,OrderMapper 依赖 SqlSessionFactory,SqlSessionFactory 又依赖数据源,如果全用new来创建,一层层手动注入会让代码耦合到没法维护。Spring 的依赖注入解决的就是这个问题:把对象的创建和装配交给容器,需要哪个依赖就在构造器或字段上声明,容器会在合适的时机把对象塞进来。
下单场景里事务控制尤其重要:用户下单后要写订单表、扣减接单者的接单次数、可能还要生成一条消息通知,这三个操作任何一个失败,前面已经执行成功的操作就必须回滚。Spring 的@Transactional注解就是干这个的,加在 Service 方法上,默认遇到 RuntimeException 就回滚,像订单金额为负数这类业务异常,实践做法是抛出一个自定义的BusinessException继承 RuntimeException,事务就会被触发回滚,不会出现订单写了一半的脏数据。
3. MyBatis 与 MySQL:订单表设计、动态 SQL 与事务边界
3.1 数据库表设计:跑腿订单核心字段怎么定
校园跑腿系统的数据模型不难,但字段设计是否合理直接决定后续功能好不好扩展。订单表通常需要包含这些列:
CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单号,业务编号', user_id BIGINT NOT NULL COMMENT '下单人ID', runner_id BIGINT DEFAULT NULL COMMENT '接单跑腿员ID,未接单时为NULL', order_type TINYINT NOT NULL COMMENT '1-代取快递 2-代购 3-搬运', pickup_address VARCHAR(128) NOT NULL, delivery_address VARCHAR(128) NOT NULL, item_desc VARCHAR(512) COMMENT '物品描述', reward_amount DECIMAL(8,2) DEFAULT 0.00 COMMENT '跑腿费', status TINYINT DEFAULT 0 COMMENT '0-待接单 1-已接单 2-已取件 3-已完成 4-已取消', create_time DATETIME NOT NULL, accept_time DATETIME, finish_time DATETIME, KEY idx_status (status), KEY idx_user_id (user_id), KEY idx_runner_id (runner_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no不建议直接用自增 ID,因为订单号往往要暴露给用户,自增 ID 容易被遍历抓取,泄露订单总量。常见的做法是生成一个 20 位左右的业务单号,比如日期加随机数。runner_id允许为 NULL 的设计是有意的,表示订单还在等待接单池里,一旦有人接单就写入具体跑腿员 ID。状态字段用 TINYINT 而不是字符串,因为数据库存储和索引成本都更低,状态的语义在枚举类里统一管理。索引方面,status字段单独建索引就够用,因为最频繁的查询是“当前待接单的订单列表”,联合索引(status, create_time)在订单量大时更有优势。
3.2 Mapper 层:动态 SQL 处理多种查询条件
MyBatis 的 Mapper 不需要去写实现类,只要定义一个接口,再把 SQL 写在 XML 或注解里即可。校园跑腿系统里最常用的是“待接单订单列表”查询,这个查询有个特点:用户可能需要按类型过滤,也可能不传类型,这时候动态 SQL 就派上了用场。
<select id="listAvailableOrders" resultType="com.campus.entity.OrderDO"> SELECT id, order_no, order_type, pickup_address, delivery_address, reward_amount, create_time FROM t_order <where> <if test="orderType != null"> AND order_type = #{orderType} </if> AND status = 0 </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签会自动处理第一个条件前面的 AND,不会因为某个条件为空就产生语法错误。#{offset}和#{pageSize}用的是预编译占位符,MyBatis 会把它处理成?,再由 JDBC 的 PreparedStatement 传参,这一步能从根本上挡住 SQL 注入,写代码的时候一定要避免用${}直接拼接条件。
分页参数这里有一个实际坑:如果前端传的是第几页(pageNum),那查数据库时要自己换算成 offset,公式是offset = (pageNum - 1) * pageSize。很多第一次接触分页的开发者会直接拿前端传的 pageNum 当 offset,结果第一页数据就少了不是。
3.3 事务边界:跑腿接单场景的并发控制
接单这个操作在校园跑腿系统里是典型的并发竞争点:同一笔订单可能有多个跑腿员同时点接单,最后只有一个能成功。如果直接做三步操作——查询订单状态、更新订单状态为已接单、写入 runner_id——在并发情况下很容易出现两个跑腿员都查到“待接单”,然后都执行更新,最后订单的 runner_id 被覆盖成后提交的人。这个问题的解决方式是在更新语句里加条件:
@Update("UPDATE t_order SET status = 1, runner_id = #{runnerId}, accept_time = NOW() " + "WHERE id = #{orderId} AND status = 0") int takeOrder(@Param("orderId") Long orderId, @Param("runnerId") Long runnerId);这里的AND status = 0是核心。MySQL 的 UPDATE 语句在走主键索引时会锁住这行,第二个事务的更新必须等第一个事务提交后才能执行,而此时 status 已经变成 1,第二个事务的更新影响行数为 0,Service 层通过检查返回值就知道“接单失败”了。这是乐观锁思路在 SQL 层的直接落地,不需要引入分布式锁那样重的方案。
事务边界要放在 Service 层而不是 Mapper 层,因为一个完整业务往往涉及多个 Mapper 调用。比如接单成功后还要给下单人发一条 App 通知,如果通知写入失败而订单更新成功,那用户会看到订单已被接走但没收到通知。把这两步包在同一个@Transactional方法里,通知失败时订单也会一起回滚,保证数据一致。
4. Android 端 RootMainActivity:下单入口到实时状态的链路
4.1 单 Activity 架构:RootMainActivity 承载了什么
压缩包里的 RootMainActivity.java 是Android 端的核心入口。现在的 Android 应用普遍采用单 Activity 架构,也就是整个 App 只有一个 MainActivity,其他页面全用 Fragment 来切换,这样做的好处是避免每次 startActivity 都重新创建界面,页面切换更快,还能让 Fragment 的生命周期更可控。RootMainActivity 的角色更像是一个容器,负责把底部导航对应的各个 Fragment 加载进来。
RootMainActivity ├── HomeFragment // 浏览待接单列表 ├── PublishFragment // 发布跑腿订单 ├── OrderFragment // 查看我发布的/我接的订单 └── MineFragment // 个人信息与设置这种划分方式很贴近真实项目的组织逻辑。以发布订单为例,PublishFragment 收集用户输入的取件地址、送件地址、物品描述和悬赏金额,组装成一个 JSON 对象,通过 Retrofit 或 OkHttp 发送 POST 请求到后端接口。界面层不直接碰数据库,也不直接拼业务数据,职责边界清晰,代码维护起来不容易乱。
4.2 网络请求怎么写:Retrofit 接口定义与回调
Android 端与后端通信,业界标准的方案是 Retrofit。先定义一个接口来描述后端 API,声明请求方法和参数。
public interface OrderApi { @POST("order/create") Call<ResultVO<OrderDO>> createOrder(@Body OrderCreateRequest request); @GET("order/available") Call<ResultVO<List<OrderDO>>> getAvailableOrders( @Query("orderType") Integer orderType, @Query("page") int page, @Query("pageSize") int pageSize ); }Retrofit 会根据注解动态生成这个接口的实现,不需要手写 HTTP 客户端代码。@Body会把 OrderCreateRequest 这个对象序列化成 JSON,@Query则会把参数拼成 URL 中的查询字符串。创建 Retrofit 实例的时候有一个容易忽略的细节:Gson 或 Jackson 转换器里,后端返回的 create_time 如果是2024-01-15 10:30:00这种格式,默认会被解析成字符串而不是 Date 对象,所以前端要么约定好用时间戳传输,要么自定义一个 TypeAdapter,否则字段类型对不上会直接解析失败。
4.3 接口联调时总报错:先定位是后端还是前端的问题
Android 端联调最常见的现象是“明明后端接口用 Postman 测是好的,App 请求就报错”。这种情况通常不是接口本身坏了,而是以下几个方面没对齐:
第一,Android 9.0 及以上默认禁止明文 HTTP 请求,如果后端的接口地址是http://192.168.1.100:8080而不是 HTTPS,App 会直接报Cleartext HTTP traffic not permitted。解决方式是在 AndroidManifest.xml 里加android:usesCleartextTraffic="true",或者在 network_security_config.xml 里配置只对开发环境放开。
第二,后端接口返回的字段命名风格跟 Java 实体类不一致。后端如果返回的是order_no,而前端实体类字段写的是orderNo,Gson 默认会把下划线风格的字段映射不到 camelCase 字段上,除非加了@SerializedName("order_no")注解或者配置了命名策略。
第三,端口不通。后端的服务如果监听的是 127.0.0.1,App 从真机访问时永远连不上,因为手机上的 127.0.0.1 指向手机自己,不是电脑。开发环境里 Spring Boot 的 server.address 一定要配成0.0.0.0,同时确保电脑和手机在同一个局域网。
5. 权限、安全与踩坑排查:JWT 过期的三连坑与订单状态遗漏
5.1 JWT 登录与权限控制:拦截器该拦哪些路径
校园跑腿系统的用户角色很简单——普通学生就是下单人,另一部分学生注册成跑腿员,提现时可能需要管理员审核。JWT 在这种场景里足够用:用户登录成功后,后端生成一个带用户 ID 和角色的 token 返回给客户端,客户端存到本地,每次请求在 Header 里带上Authorization: Bearer <token>,后端用一个拦截器统一校验。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.replace("Bearer ", "")) .getBody(); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (ExpiredJwtException e) { response.setStatus(401); return false; } } }在 Spring MVC 里配置拦截器时,addPathPatterns和excludePathPatterns的选择是关键。/api/login、/api/register必须排除,否则用户没登录就拿不到 token,连注册都做不了。而/api/order/create、/api/order/take这些业务接口必须全部拦截。这里翻车频率最高的就是新人只拦截了部分路径,结果某个接口漏掉了,导致匿名用户能直接下单。
关于 JWT 还有一个经常被忽略的问题:默认生成的 token 里带的是exp过期时间,而这个过期时间往往写在用户登录那一刻。如果用户的 token 过期了,常规做法是让前端跳转回登录页重新登录,但体验很差。业界更顺滑的做法是引入 refresh token,拿一个短期有效的 access token 加一个长期有效的 refresh token,access token 过期后前端自动拿 refresh token 去换新的,用户无感续期。压缩包里如果没看到这块逻辑,可以自己补上。
5.2 安全机制:密码不能明文存,SQL 注入要堵死
校园跑腿系统虽然不涉及支付密码级别的敏感数据,但用户密码一旦泄露,等于用户的手机号、收货地址全部暴露。密码存储的正确姿势是加盐哈希,不要用 MD5,因为 MD5 彩虹表查起来太容易,部署成本几乎为零的更好的选择是 BCrypt。
public class PasswordUtil { public static String encode(String rawPassword) { return BCrypt.hashpw(rawPassword, BCrypt.gensalt()); } public static boolean matches(String rawPassword, String encodedPassword) { return BCrypt.checkpw(rawPassword, encodedPassword); } }每一次调用BCrypt.gensalt()都会生成不同的盐,所以同一密码两次加密后的结果不一样,但这不影响匹配。数据库设计上直接把哈希后的字符串存进 user 表的 password_hash 字段就行,长度至少 60 个字符。这个方案的最大好处是,数据库就算被拖走,攻击者想从哈希反推原始密码所付出的算力成本也远高于找回收益的期望值。
SQL 注入在 MyBatis 中只要坚持使用#{}就不会出问题,但有一种情况是误把表名或列名也放进#{}里,然后发现怎么都查不出数据。原因是#{}只能用于值,不能用于表名和列名,这种场景必须用${},而${}传入的内容绝对不能来自用户输入。如果业务确实需要让用户控制排序字段,正确做法是做一个白名单映射,比如前端传create_time,后端映射到真正的列名create_time,传其他值就拒绝。
5.3 避坑记录:从现象到原因到解决
坑一:本地跑起来了,但 App 一直连不上后端现象:Android 模拟器或真机访问后端接口超时,浏览器访问电脑 IP 的 8080 端口也是通不了。原因:电脑防火墙拦了 8080 端口,或者 Spring Boot 绑定的地址是 127.0.0.1。解决:确保 application.yml 里server.address=0.0.0.0,然后检查防火墙是否允许 8080 入站,局域网内用curl http://<电脑局域网IP>:8080/api/ping自测。
坑二:订单状态流转到一半就卡住现象:下单人取消了订单,但接单的人 App 里还显示“进行中”。原因:取消操作只更新了 status 字段,没有同步发通知,也没有校验接单人的订单是否真的需要更新。解决:把状态变更做成一个统一的方法,任何状态跳转都经过这个方法,里面有状态机的合法性校验,比如从“已取件”不能直接跳到“已取消”,并且状态变更后触发的通知逻辑也在这同一层执行。
坑三:JWT 过期时间设得太短或太长现象:设 30 分钟,用户写个跑腿单还没填完就掉线了;设 7 天,用户换手机后旧 token 还能用,安全风险大。原因:过期时间的选择没有结合业务场景来定。解决:access token 设 2 小时,refresh token 设 14 天,并且每个用户维护一个 token 版本号,用户改密码后版本号加一,旧 token 立即失效,不用等自然过期。
6. 进阶:把自己写的接口联调清单整理成可复用的验证模板
项目能跑通和项目能交付之间,差的是一套完整验证过的接口清单。拿到这份源码并且按前面几章改完自己的业务逻辑后,建议你整理一份针对跑腿核心链路的联调清单,下面是我个人习惯用的方案,可以直接抄作业:
| 验证场景 | 请求方式 | 预期结果 | 不通过的常见原因 |
|---|---|---|---|
| 用户注册 | POST/api/user/register | 返回成功且密码字段为密文 | 密码哈希工具没生效 |
| 用户登录 | POST/api/user/login | 返回 token 和用户角色 | 用户表里 role 字段没初始化 |
| 发布跑腿订单 | POST/api/order/create | 返回订单号,状态为 0 | 订单号生成器重复 |
| 查看待接单列表 | GET/api/order/available | 只返回状态为 0 的订单 | 查询条件漏了 status |
| 跑腿员接单 | POST/api/order/take | 返回成功,订单状态变 1 | 缺少AND status = 0导致重复接单 |
| 取消订单 | POST/api/order/cancel | 状态变 4,且接单人收到通知 | 通知服务异常被吞掉 |
| 订单完成 | POST/api/order/finish | 状态变 3,跑腿员累计金额更新 | 事务没加回滚导致金额不对 |
这份清单建议放在项目的doc/目录下,配合一个简单的 Postman Collection 或 curl 脚本,每次改完代码先拿清单把主链路跑一遍。curl 命令是一个最基本的落地方式:
# 注册用户 curl -X POST http://localhost:8080/api/user/register \ -H "Content-Type: application/json" \ -d '{"phone":"13800138000","password":"Test@123456"}' # 登录拿 token curl -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"phone":"13800138000","password":"Test@123456"}'拿到 token 后把它填入环境变量TOKEN,再测试下单和接单:
# 发布订单 curl -X POST http://localhost:8080/api/order/create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"type":1,"pickupAddr":"3号楼","deliveryAddr":"7号楼","desc":"京东快递","reward":5.00}' # 模拟接单(用另一个跑腿员账号执行) curl -X POST http://localhost:8080/api/order/take \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"orderId":123}'接口联调这件事,逻辑看着简单,实际项目里翻车都在细节上。我见过的最多的一类问题是:后端的接口文档和前端实际请求参数差了大小写或层级,两边各查各的代码查了半天,最后发现前端少了@SerializedName注解。从那以后我每次做联调,都强制自己先用 curl 把后端接口完全验证通过,再让前端接入。一整套跑下来,你能清楚地看到这份源码里哪些地方改得不彻底会出问题,哪些地方直接拿来用也完全没毛病。希望帮到你。
这里必须补一句:这份源码不是一个开箱即用的“生产环境应用”,它更像是一套完整的学习骨架,把跑腿业务中最关键的下单、接单、状态流转、权限控制都铺在了你面前。你拿到手后最应该做的事不是照抄它,而是按自己的需求去改造——压缩包里那些看起来不起眼的配置文件,往往才是真正值得琢磨的地方。希望你接下来的调试过程比我的第一版顺利一点。
本文还有配套的精品资源,点击获取