拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Android与Java服务端的物流管理系统实战:从建表到联调避坑

基于Android与Java服务端的物流管理系统实战:从建表到联调避坑

简介:这份资源是一套基于Android的物流管理系统完整项目源码,服务端采用Java技术栈实现,适合计算机专业学生、Java Web初学者及需要课程设计或毕业设计参考的开发者。项目以JSP、Servlet结合Android客户端与Ajax异步交互构建,数据库选用开源MySQL,整体按表现层、业务层、数据访问层分层设计,表现层遵循MVC结构,业务层为每个模块提供独立接口与实现类,代码结构清晰且便于适配不同数据库。压缩包共899个文件,约9.48MB,包含47个Java源文件与47个class编译文件、32个JSP页面、116个JavaScript脚本、24个CSS样式,以及大量gif、png、jpg图片资源和18个jar依赖包,另有properties、xml等配置文件,覆盖前后端与资源素材。目前已有506人学习下载。通过研读这套源码,读者可以掌握Android与服务端的数据交互流程、Servlet请求处理、DAO数据访问封装以及MVC分层组织方式,是理解物流业务系统从界面到数据库完整链路的实用参考。

1. 从一份 rar 说起:Android 端 + Java 服务端的物流管理系统到底怎么落地

很多人拿到「基于 Android 的物流管理系统,服务端 Java 实现.rar」这类压缩包,第一反应是解压、找 README、跑 main 方法,然后卡在数据库连不上、接口 404、Android 端 IP 写死这三件事上。它本质是一套典型的 C/S 架构练手项目:Android 客户端负责运单录入、扫码、状态查询,Java 服务端负责业务逻辑、数据持久化和接口暴露。适合谁?想从「只会写单机 demo」跨到「能跑通客户端和服务端联调」的 Java 工程师、Android 初学者,以及准备用物流场景做课程设计或面试项目的人。这一章先把这套系统的骨架讲清楚,后面几章再拆服务端接口、Android 端对接、数据库设计和避坑。

2. 服务端 Java 实现:从建表到接口跑通的最小闭环

服务端是这套系统的地基。物流业务的核心实体就那么几个:用户、运单、物流轨迹、网点。选型上,常见做法是 Spring Boot + MyBatis + MySQL,原因很直接——依赖少、启动快、SQL 可控,适合这种中小规模系统。如果你用的是纯 Servlet + JDBC 的老写法,逻辑一样成立,只是配置更啰嗦。这一章按「建表 → 实体 → Mapper → Service → Controller」的顺序走一遍,每一步都给可抄的代码。

2.1 物流核心表结构怎么设计

先建库建表。物流系统最怕的是运单状态和轨迹对不上,所以表设计阶段就要把状态字段和轨迹表分开,轨迹表只追加不修改。

CREATE DATABASE logistics DEFAULT CHARACTER SET utf8mb4; USE logistics; -- 用户表:区分管理员、网点操作员、普通客户 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存 MD5 或 BCrypt,别存明文 role TINYINT NOT NULL DEFAULT 2, -- 0管理员 1操作员 2客户 phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 运单表:一条运单一个当前状态 CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, -- 业务单号,对外暴露 sender_name VARCHAR(50), sender_phone VARCHAR(20), receiver_name VARCHAR(50), receiver_phone VARCHAR(20), address VARCHAR(255), status TINYINT NOT NULL DEFAULT 0, -- 0待揽收 1运输中 2派送中 3已签收 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 轨迹表:只追加,记录每次状态变更 CREATE TABLE t_track ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, status TINYINT NOT NULL, remark VARCHAR(255), op_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_order_no (order_no) );

order_no单独做业务单号而不是直接用自增 id,是为了对外查询时不暴露数据量。t_track上的idx_order_no索引必须有,否则轨迹查询在数据量上来后会全表扫描。status用 TINYINT 而不是字符串,省空间也方便前端做状态映射。

2.2 Spring Boot 里把运单接口写出来

实体和 Mapper 用 MyBatis 生成或用注解手写都行,这里直接给 Controller 和 Service 的关键部分,重点看参数校验和状态流转。

@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; // 创建运单:客户端提交收发件人信息 @PostMapping("/create") public Result create(@RequestBody OrderDTO dto) { if (dto.getReceiverPhone() == null || dto.getAddress() == null) { return Result.fail("收件人电话和地址不能为空"); } String orderNo = orderService.createOrder(dto); return Result.ok(orderNo); } // 按单号查轨迹:Android 端查询页调这个 @GetMapping("/track/{orderNo}") public Result track(@PathVariable String orderNo) { List<TrackVO> list = orderService.queryTrack(orderNo); return Result.ok(list); } // 更新状态:操作员扫码后调用 @PostMapping("/status") public Result updateStatus(@RequestParam String orderNo, @RequestParam Integer status, @RequestParam(required = false) String remark) { orderService.updateStatus(orderNo, status, remark); return Result.ok(); } }

create接口里做了最基础的非空校验,真实项目建议用@Valid+ JSR303 注解,但练手项目手写 if 更直观。track用@PathVariable而不是@RequestParam,是为了让 URL 语义更清晰,Android 端拼接也简单。updateStatus里状态和轨迹要在一个事务里写,Service 层加@Transactional,否则会出现运单状态改了但轨迹没记录的脏数据。

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private TrackMapper trackMapper; @Override @Transactional(rollbackFor = Exception.class) public void updateStatus(String orderNo, Integer status, String remark) { Order order = orderMapper.selectByOrderNo(orderNo); if (order == null) { throw new RuntimeException("运单不存在"); } // 状态只能往前走,防止误操作回退 if (status <= order.getStatus()) { throw new RuntimeException("状态不能回退"); } orderMapper.updateStatus(orderNo, status); Track track = new Track(); track.setOrderNo(orderNo); track.setStatus(status); track.setRemark(remark); trackMapper.insert(track); } }

@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省,默认只回滚 RuntimeException,受检异常不会回滚。状态回退校验是物流系统的血泪经验,没有这个判断,操作员点错一次就得手动改库。

2.3 接口测试:先用 curl 跑通再连 Android

服务端写完别急着开 Android Studio,先用 curl 或 Postman 把接口跑通,能省掉一半联调时间。

# 启动服务,默认 8080 mvn spring-boot:run # 创建运单 curl -X POST http://localhost:8080/api/order/create \ -H "Content-Type: application/json" \ -d '{"senderName":"张三","receiverName":"李四","receiverPhone":"13800000000","address":"北京市朝阳区"}' # 查轨迹 curl http://localhost:8080/api/order/track/SF1234567890 # 更新状态 curl -X POST "http://localhost:8080/api/order/status?orderNo=SF1234567890&status=1&remark=已揽收"

如果返回 404,先看 Controller 的@RequestMapping路径和端口;如果返回 500 且日志里有Unknown database,检查application.yml里的数据库名和账号密码。服务端接口测试这一步做扎实,Android 端就只是「发请求、解析 JSON」的体力活。

3. Android 客户端对接:网络请求、列表和扫码怎么串起来

Android 端是这套系统的门面,也是最容易翻车的地方。常见做法是 Retrofit + OkHttp + Gson 做网络层,RecyclerView 做运单列表,扫码用 ZXing 或华为 ScanKit。这一章按「网络层 → 列表页 → 扫码页」的顺序讲,重点说清楚 baseUrl 怎么配、异步回调怎么处理、权限怎么申请。

3.1 Retrofit 网络层与 baseUrl 的坑

Android 模拟器和真机访问本机服务端的地址不一样,这是新手第一个大坑。模拟器用10.0.2.2映射宿主机localhost,真机必须用电脑的局域网 IP。

public class ApiClient { // 模拟器用 10.0.2.2,真机改成电脑局域网 IP,如 192.168.1.100 private static final String BASE_URL = "http://10.0.2.2:8080/"; private static Retrofit retrofit; public static Retrofit getInstance() { if (retrofit == null) { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } }

BASE_URL必须以/结尾,否则 Retrofit 拼接路径时会丢掉最后一段,这是官方文档里都容易忽略的细节。超时时间设 10 秒,物流查询接口一般不会超过这个数,设太长会让用户以为卡死。Android 9 以后默认禁止明文 HTTP,需要在AndroidManifest.xml的application标签加android:usesCleartextTraffic="true",否则请求直接失败,日志里只有一句Cleartext HTTP traffic not permitted。

public interface OrderApi { @POST("api/order/create") Call<Result<String>> createOrder(@Body OrderDTO dto); @GET("api/order/track/{orderNo}") Call<Result<List<TrackVO>>> queryTrack(@PathVariable("orderNo") String orderNo); }

接口定义和 Java 服务端的路径一一对应,@Path的变量名要和 URL 里的占位符一致。返回类型用Result<T>包一层,和服务端的统一返回结构对齐,解析时不会因为字段缺失崩掉。

3.2 运单列表页:RecyclerView 与异步回调

列表页是 Android 端最核心的页面,用 RecyclerView + Adapter 的标准写法,重点看数据回来之后怎么刷新。

public class OrderListActivity extends AppCompatActivity { private OrderAdapter adapter; private List<OrderVO> dataList = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_order_list); RecyclerView rv = findViewById(R.id.rv_order); rv.setLayoutManager(new LinearLayoutManager(this)); adapter = new OrderAdapter(dataList); rv.setAdapter(adapter); loadOrders(); } private void loadOrders() { OrderApi api = ApiClient.getInstance().create(OrderApi.class); api.queryTrack("SF1234567890").enqueue(new Callback<Result<List<TrackVO>>>() { @Override public void onResponse(Call<Result<List<TrackVO>>> call, Response<Result<List<TrackVO>>> resp) { if (resp.isSuccessful() && resp.body() != null && resp.body().isOk()) { // 主线程更新 UI adapter.updateData(convert(resp.body().getData())); } } @Override public void onFailure(Call<Result<List<TrackVO>>> call, Throwable t) { Toast.makeText(OrderListActivity.this, "网络异常:" + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } }

enqueue是异步请求,回调默认在主线程,可以直接更新 UI,别在里面再开子线程。onFailure里把t.getMessage()打出来,常见的是Failed to connect to /10.0.2.2:8080,说明服务端没启动或地址不对。Adapter 的updateData里要调notifyDataSetChanged(),否则数据变了界面不动,这是 RecyclerView 最经典的「玄学」问题。

3.3 扫码录入与运行时权限

物流场景离不开扫码,用 ZXing 的CaptureActivity或自己集成zxing-android-embedded都行。关键是 Android 6.0 以后相机权限要动态申请。

private static final int REQ_CAMERA = 100; private void openScan() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQ_CAMERA); } else { startActivityForResult(new Intent(this, CaptureActivity.class), REQ_CAMERA); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQ_CAMERA && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startActivityForResult(new Intent(this, CaptureActivity.class), REQ_CAMERA); } else { Toast.makeText(this, "需要相机权限才能扫码", Toast.LENGTH_SHORT).show(); } }

权限申请要在使用时动态请求,不要在onCreate里一次性全申请,用户体验差还容易被拒。扫码结果在onActivityResult里拿data.getStringExtra("result"),拿到单号后直接调服务端的track接口查详情。如果扫码没反应,先确认权限是否真的授予,再看CaptureActivity是否在AndroidManifest.xml里注册。

4. 避坑与排查:联调阶段最容易翻车的 5 个点

这一章是纯踩坑记录,每条按「现象 → 原因 → 解决」写,都是这套系统联调时高频出现的问题。

现象一:Android 端请求一直转圈,最后超时。原因通常是 baseUrl 用了localhost或127.0.0.1,在 Android 设备上这两个地址指向设备自己,不是你的电脑。解决:模拟器改10.0.2.2,真机改电脑局域网 IP,并确保手机和电脑在同一网段。用ipconfig(Windows)或ifconfig(Mac/Linux)查电脑 IP。

现象二:服务端返回 200,但 Android 端解析出来全是 null。原因是 Gson 解析时字段名对不上,比如服务端返回order_no,Java 实体写的是orderNo。解决:在实体字段上加@SerializedName("order_no"),或者服务端统一用驼峰命名返回。建议后者,省得每个字段都加注解。

现象三:运单状态更新了,但轨迹列表没变化。原因是updateStatus没加事务,或者轨迹插入失败但状态更新成功了。解决:Service 方法加@Transactional(rollbackFor = Exception.class),并检查t_track表的order_no字段长度是否够,单号超长会被截断导致查不到。

现象四:Android 9 以上真机请求直接失败,日志提示明文流量被禁。原因是 Android 9 默认只允许 HTTPS。解决:在AndroidManifest.xml的application标签加android:usesCleartextTraffic="true",仅限开发阶段,上线要换 HTTPS。

现象五:扫码后返回的单号带换行或空格,查不到运单。原因是部分二维码内容末尾有不可见字符。解决:拿到扫码结果后先trim(),再传给服务端。这个坑很隐蔽,日志里单号看起来一模一样,但就是查不到。

5. 进阶技巧:用统一返回结构和日志把联调效率提上去

这套系统跑通之后,真正拉开效率差距的是两件事:统一返回结构和请求日志。先看统一返回结构,服务端所有接口都返回同一个壳,Android 端只写一次解析逻辑。

public class Result<T> { private int code; // 0 成功,非 0 失败 private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } public boolean isOk() { return code == 0; } }

Android 端拿到响应先判断isOk(),再取data,失败时直接弹msg,不用每个接口单独写错误处理。这个壳看起来简单,但能省掉大量重复代码。

第二件事是给 OkHttp 加日志拦截器,把请求 URL、参数、响应体全打出来。联调阶段 90% 的问题看日志就能定位,比猜快得多。

HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 开发阶段用 BODY,上线改 NONE OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(logging) .build();

Level.BODY会打印完整请求体和响应体,方便核对字段,但上线前必须改成NONE,否则用户敏感信息会进日志。我一般会在BuildConfig.DEBUG为 true 时才加这个拦截器,一行判断解决。

最后说一个验证方法:服务端接口改完后,先跑一遍 curl,再用 Android 端跑一遍,两边结果一致才算联调通过。别只信 Android 端界面显示正常,有时候是缓存数据在骗你。这套系统本身不复杂,难的是把「服务端接口测试」和「客户端和服务端」这两端的边界理清楚。我自己的习惯是每加一个接口,先在 Postman 里存一条用例,Android 端出问题时先跑用例确认服务端没问题,再去查客户端。这个习惯帮我省了无数个加班的晚上。希望帮到你。

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

返回列表