☰
基于Android的水果蔬菜销售APP复现与避坑指南
2026/10/1 15:37:55 网站建设 项目流程

简介:基于安卓的水果蔬菜销售应用程序设计与实现文档,是一份面向计算机相关专业学生及移动开发初学者的毕业设计参考材料,围绕选题目的、研究现状、可行性分析等方面,系统阐述了蔬菜水果销售应用从需求分析到技术选型的完整过程。资源为单个DOCX格式文档,大小892KB,内容涵盖中英文摘要、目录、引言、可行性分析及后续设计章节,结构完整,便于直接查阅与修改。已有79人学习下载,适合正在开展安卓项目开发或撰写相关论文的读者借鉴。通过该文档可以了解安卓客户端与网络后台配合实现商品展示、拍照上传等功能的思路,以及经济、技术、操作层面的可行性评估方法,对完成同类毕业设计或课程项目具有实际参考价值。

1. 一份能跑的安卓蔬果销售APP:它到底值不值得复现

做Android开发这些年,我拆过不少课程设计和毕业设计,像这份基于Android的水果蔬菜销售APP,反而是我建议新手和转行者认真复现的一类项目。理由很简单:它不炫技,但把电商类App最核心的业务闭环完整走通了——从用户登录注册、浏览商品、下单购买、评价反馈,到后台商品录入、类别管理、订单列表,该有的链路全都有。而且技术栈是Java、SQLite、HTTP、JSON这套经典组合,拿到手就能在Android Studio里打开改。它适合两类人:一类是马上要交毕业设计、需要完整可演示项目的学生;另一类是想弄懂一个安卓App从页面到数据库再到后台请求,整体是怎么咬合在一起的入门开发者。

2. 业务与功能模块:登录、购物车、订单和评价背后的设计取舍

拿到这份文档,我第一件事不是看代码,而是看需求分析和用例图。文档里的功能点其实很清晰:用户侧有登录注册、查看水果、购买商品、发表评价;管理侧有商品列表管理、用户管理、商品类别管理、订单列表和评价列表。很多人复现课程设计时容易犯的毛病是一上来就写页面,写到一半发现订单数据不知道存哪、评价和订单怎么关联也理不清。这份资源的价值就在于,它先把业务参与者和数据边界画出来了。

2.1 用例图里的参与者和权限边界

两张用例图值得先吃透。普通用户的动作是登录、注册、查看水果、购买、评价,管理员的动作是维护商品、管订单、管评价、管用户。你会发现两边几乎没有重叠,这就是权限边界的意义:用户能看到的商品列表来自后台录入,用户提交订单后由后台管理员处理发货,用户评价也只针对已购买的商品。

模块用户端管理端核心数据
商品管理查看列表、查看详情录入、修改、上架下架goods
商品类别按类别过滤增删类别category
订单创建订单、查看状态查看全部订单、更新状态orders
评价提交评价查看、可删除违规评价evaluation
用户注册、登录、维护地址查看用户列表users

我一般会在新建工程时就把这张表画出来,它决定了后面Activity怎么分、接口怎么拆。比如评价功能,如果用户没有购买记录就让它评价,数据上就不成立,所以评价表必须带order_id这个外键。权限边界不是一句空话,它会具体到代码里,控制按钮的显示和接口的校验。

2.2 商品、订单、评价三大表的关系设计

文档反复提到SQLite,这是Android自带的关系型数据库,整个销售系统的本地数据都靠它。复现时我会把表设计成下面这个样子。重点说订单表:它不会只存一个goods_id去关联商品表,而是直接把goods_name和price冗余一份进来做快照。为什么?因为后台改价改名称后,历史订单必须还保持当时的下单信息,这是电商业务的常识。

  • goods:id、name、category、price、stock、image_url、description
  • users:id、username、password、nickname、phone、address
  • orders:id、order_no、user_id、goods_name、price、quantity、status、create_time
  • evaluation:id、order_id、user_id、goods_id、content、rating、create_time

数据库设计到位之后,你会发现订单状态流转特别自然。文档里写的是“待发货”这类粗粒度状态,实际做个status字段,用一个int或者TEXT存都行。text的缺点是拼写错误难排查,优点是日志里直接可读,课程设计用TEXT更直观,比如“待支付”“待发货”“已发货”“已完成”。这些状态会写进流程图,也会直接体现在订单列表的UI上。

2.3 流程图里藏着的状态流转

文档里的系统流程图看起来简单:用户输入信息、系统添加数据、上传传输、存储读取、查看订单。但把流程图翻译成代码逻辑,它就不只是页面跳转了,而是一条数据链路。用户下单这个动作发生后,数据要同时走两条路:一条写在本地SQLite里,用于离线也能看到我的订单;另一条通过HTTP接口提交到后台,让管理员那边能处理发货。

食谱里最容易断的地方是“上传”这一步。拍照上传更是如此,文档里特别提到了照相、上传、图片处理,这背后牵涉相机调起、文件存储、网络传输、后台接收四个环节。任何一个环节断掉,用户看到的都是“上传失败”。我在复现时会把这一条完整链路单独画出来,从Android端调用相机拍照,到图片存到临时目录,再到multipart方式POST到后台接口,后台返回图片URL,最后客户端把这个URL和商品信息一起入库。业务设计阶段把这张图画清楚,后面写代码就不会返工。

3. Android客户端落地:Activity生命周期、SQLite本地存储与拍照上传

文档第四章把开发语言、Android框架、SQLite和工程环境讲得很细,这些是背景,真正动手时我建议按“工程结构→页面流转→本地存储→拍照上传”这个顺序去落地。很多人下载课程设计资源后第一件事是找MainActivity,我的习惯是先看AndroidManifest.xml,因为它相当于App的地基:页面、服务、权限、文件提供者全在这里声明。

3.1 工程结构:AndroidManifest.xml先注册后使用

一个典型的项目工程里,所有Activity、Service、ContentProvider都需要在AndroidManifest.xml里注册。四个组件里我尤其提醒Service和Provider,Activity漏注册会在启动时立刻崩,但Service漏注册会在调用startService时报错,Provider漏注册会在运行时找不到访问入口。Android 12之后还要求显式声明android:exported,不然直接安装失败。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.fruitsales"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:label="@string/app_name"> <activity android:name=".ui.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ui.LoginActivity" android:exported="false" /> <activity android:name=".ui.OrderListActivity" android:exported="false" /> <service android:name=".service.UploadService" android:exported="false" /> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application> </manifest>

逻辑说明:INTERNET权限用于访问后台接口,CAMERA和WRITE_EXTERNAL_STORAGE用于拍照和存图。需要注意,Android 6.0以上摄像机这类危险权限不能只写在Manifest里,还要在代码里动态申请,否则真机上调用相机时不会弹授权框。FileProvider的authorities要和应用包名对应,这里用${applicationId}.fileprovider,编译后会替换成真实的包名。

3.2 Activity生命周期与页面流转

页面流转是Android App的骨架。文档里提到了登录注册、查看商品、购买、评价这些界面需求,实际开发时我会用一个BaseActivity统一管理,再让LoginActivity、MainActivity、GoodsDetailActivity、OrderListActivity各自继承。页面之间跳转靠Intent,这是Android最核心的协作机制。

public void openGoodsDetail(View view) { int goodsId = Integer.parseInt(view.getTag().toString()); Intent intent = new Intent(this, GoodsDetailActivity.class); intent.putExtra("goods_id", goodsId); startActivity(intent); } public void submitOrder() { Intent serviceIntent = new Intent(this, UploadService.class); serviceIntent.putExtra("order_no", currentOrderNo); startService(serviceIntent); }

逻辑说明:first段是把商品id通过Intent传递到下个页面,接收方用getIntExtra("goods_id", -1)取回来,再按id查数据库或请求接口。第二段是启动上传服务,文档里提到“后台运行的复制信息传输”,指的就是Service在后台处理耗时上传任务。参数说明:putExtra能传基本类型和Serializable对象,但不能传太大的数据,复杂对象我建议转成JSON字符串或者直接传id再查一次,避免事务TooLargeException。

3.3 SQLite本地缓存:建表、增删改查与onUpgrade

后台数据到不了的时候,本地SQLite能不能扛住,决定了演示会不会翻车。文档里说SQLite在Android上是嵌入式关系型数据库,支持自动建表和对象化操作。我复现时会用SQLiteOpenHelper来管理数据库创建和版本升级,核心代码是这样。

public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "fruit_sales.db"; private static final int DB_VERSION = 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS goods (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name TEXT NOT NULL, " + "category TEXT, " + "price REAL, " + "stock INTEGER DEFAULT 0, " + "image_url TEXT)"); db.execSQL("CREATE TABLE IF NOT EXISTS orders (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "order_no TEXT UNIQUE, " + "user_id INTEGER, " + "goods_name TEXT, " + "price REAL, " + "quantity INTEGER, " + "status TEXT DEFAULT '待发货', " + "create_time TEXT)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 常见做法是按版本号逐段迁移,不要直接 DROP 重建 } }

逻辑说明:onCreate在数据库第一次创建时调用,里面建两张核心表。DATABASE_VERSION是结构版本号,注意它不是数据版本号,只要你的建表语句变了,这个值就必须加1,否则用户安装新版本后表结构还是旧的样子。参数说明:price用REAL是因为要存小数价格;order_no用UNIQUE约束能防止重复提交;onUpgrade里我习惯用switch匹配oldVersion,逐版本执行ALTER TABLE,而不是简单DROP TABLE重建,那会让用户本地数据清零,属于典型的“后悔药没得吃”的操作。

3.4 拍照上传:相机调用、文件路径与权限处理

文档里反复提到“照相、上传、图片出来”,这是系统里功能性最强也最容易出坑的部分。拍照首先要解决的是文件路径问题,Android 7.0之后直接用file:// Uri去调相机,会当场抛FileUriExposedException。正确做法是借助FileProvider把文件Uri暴露出去。

private void takePhoto() { Intent take = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File photo = new File(getExternalCacheDir(), "goods_" + System.currentTimeMillis() + ".jpg"); Uri photoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", photo); take.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); startActivityForResult(take, REQUEST_TAKE_PHOTO); } private void uploadImage(String filePath) { HttpURLConnection conn = (HttpURLConnection) new URL(BASE_URL + "/upload") .openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(8000); conn.setReadTimeout(8000); // 拼 multipart/form-data,把文件流写入请求体 // 后台返回 {"code":0,"data":{"url":"..."}} }

逻辑说明:拍照时指定输出文件,避免相机返回的缩略图太小;getExternalCacheDir()是App专属缓存目录,不需要额外申请存储权限,也比getExternalStorageDirectory()更符合分区存储规范。uploadImage用HttpURLConnection,这是文档那个时代最通用的方案,现在换OkHttp逻辑完全一样,换的只是请求构造方式。参数说明:connectTimeout和readTimeout都设8000毫秒,原因是弱网环境下如果无限等待,用户会以为App死了;超时后要在onFailure里给Toast提示,别让用户对着一个转圈的对话框发呆。

4. Web后台与数据流转:接口约定、JSON解析与网络切换

这份文档的后台部分没有展开讲,但它明确写了一个关键点:客户端和Web后台结合,用户下单后数据要传到后台,管理员在后台维护商品列表。这个模式决定了客户端和后台之间必须有一套稳定的接口约定。我会在复现时先定接口再写页面,顺序反了的话,前后端对不上参数,排查起来非常痛苦。

4.1 客户端与后台的接口约定

接口是客户端和后端的“合同”。我一般先定义下面这几个基础接口,覆盖主链路:登录、注册、商品列表、提交订单、图片上传、提交评价。

接口方法参数返回
/api/loginPOSTusername, passwordtoken, userInfo
/api/registerPOSTusername, password, phoneuserId
/api/goodsListGETcategory(可选)goods数组
/api/addOrderPOSTuserId, goodsId, quantityorderNo
/api/uploadImagePOSTmultipart/form-dataimageUrl
/api/addEvaluationPOSTorderId, userId, content, rating成功标记

为什么要在文档没有写全的情况下先补这一层?因为客户端页面能不能跑通,完全依赖接口返回的字段名。比如登录接口返回的是token还是userId,直接影响本地存什么。字段名大小写也要统一,JSON里userName和username是两回事,后台一旦返回了不一致,前端解析就会拿到null。

4.2 JSON解析与列表数据绑定

JSON是客户端和后台之间最常用的数据格式,Android自带的org.json包解析简单的数据结构完全够用。我在这个项目里不会急着上Gson,因为数据结构不复杂,手写解析反而更直观,还能让新手看清JSON和Java对象之间的映射关系。

protected void parseGoods(String jsonString) throws JSONException { JSONObject root = new JSONObject(jsonString); if (root.getInt("code") != 0) { throw new IllegalStateException("接口返回错误:" + root.getString("msg")); } JSONArray list = root.getJSONObject("data").getJSONArray("list"); List<GoodsBean> goodsList = new ArrayList<>(); for (int i = 0; i < list.length(); i++) { JSONObject item = list.getJSONObject(i); GoodsBean bean = new GoodsBean(); bean.setId(item.getInt("id")); bean.setName(item.getString("name")); bean.setPrice(item.getDouble("price")); bean.setImageUrl(item.getString("imageUrl")); goodsList.add(bean); } adapter.setData(goodsList); adapter.notifyDataSetChanged(); }

逻辑说明:先判断code字段,不等于0直接抛异常,这样错误能在日志里一眼看到;然后再取data里的list数组,逐条转实体类,最后交给Adapter刷新列表。参数说明:getDouble处理价格,如果用getInt会把8.8读成8;imageUrl字段名必须和后台对死,我踩过把imageUrl写成image_url的坑,后台返回下划线前端取驼峰,列表图片一整列出不来,排查了很久才发现是字段名不一致。

4.3 WiFi与数据流量:网络切换时的连接策略

文档里专门有一节叫“系统wifi连接与数据流量设计”,这是很多人容易忽略的点。真实场景是用户可能在WiFi和4G之间切换,切换瞬间网络请求会失败,App如果没有感知网络变化的能力,用户就只能在失败页面里干等。

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkRequest request = new NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build(); cm.registerNetworkCallback(request, new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { // 重试失败队列里的请求,重新拉取商品列表 } });

逻辑说明:registerNetworkCallback能监听网络恢复的时机,网络从断开恢复后立即重试之前的失败任务。参数说明:addCapability(NET_CAPABILITY_INTERNET)是筛选出真正能上网的网络,避免连上WiFi但没有外网时误判;这个回调API在Android 5.0以上可用,现在复现时不用再兼容老古董版本。实际使用时我还会加一个超时重试计数器,最多重试两次,避免反复请求把服务器打挂。

5. 避坑指南:真机联调、数据库升级与上传崩溃的五个坑

课程设计能跑模拟器,不等于能跑真机;能在自己的手机上跑,不等于换了别人的手机还能跑。这一章我集中写复现这份资源时最容易翻车的五个点,全是实际项目里的血泪经验,每一条都按现象、原因、解决来拆。

5.1 数据库版本升级翻车:忘记递增DATABASE_VERSION

现象:App在原版本基础上升级安装,打开后闪退,日志里报SQLiteException: no such column。

原因:改了建表语句,比如给goods表加了description字段,但SQLiteOpenHelper的DATABASE_VERSION还是原来的值。SQLite只有在版本号变化时才会触发onUpgrade,版本号没变,旧数据库就不会执行迁移,新代码却去查新字段,自然报错。

解决:每次表结构发生变化,必须把DB_VERSION加1,在onUpgrade里用switch匹配旧版本号,逐段执行ALTER TABLE。我一般还会把迁移语句包在try-catch里,防止中途失败导致整个升级流程异常终止。这里没有后悔药,已经发布出去的版本,用户数据是不能指望靠卸载重装解决的。

注意:发布后的App升级数据库,不要简单DROP TABLE重建,那会清空用户所有本地数据。正确姿势是ALTER TABLE ADD COLUMN,或者建临时表再拷贝数据。

5.2 真机拍照上传闪退:7.0以上FileUriExposedException

现象:模拟器上拍照上传一切正常,换到Android 8.0真机上,点拍照按钮直接崩,日志抛出FileUriExposedException。

原因:Android 7.0开始,App之间共享file:// Uri是被禁止的。相机的ACTION_IMAGE_CAPTURE通过Intent接收file:// Uri后无法访问该文件,系统直接抛异常。模拟器很多默认API版本较低,所以发现不了这个问题。

解决:按前面第三章的做法,配置FileProvider并声明file_paths.xml,代码里用FileProvider.getUriForFile生成content:// Uri传给相机。另外注意Android 10分区存储后,getExternalStorageDirectory访问受限,建议统一改用getExternalCacheDir或者getExternalFilesDir。

5.3 模拟器能登录、真机连不上后台

现象:模拟器里登录、商品列表、下单全部正常,装到真机上后所有网络请求超时。

原因:Android模拟器访问宿主机用10.0.2.2这个特殊地址,它映射的是开发机的localhost。代码里如果写死了http://10.0.2.2:8080,换到真机上自然找不到服务器。

解决:把接口baseUrl改成开发机的局域网IP,比如http://192.168.1.100:8080,并确保手机和电脑连同一个WiFi。临时调试还可以用adb reverse tcp:8080 tcp:8080把手机端口映射到电脑,这样代码可以继续写localhost。记得别把局域网IP提交到正式环境,那东西换个WiFi就失效。

5.4 商品图片一多就卡顿:直接加载原图扛不住

现象:商品列表滑起来掉帧,图片多的时候直接OOM,Logcat里频繁出现OutOfMemoryError。

原因:ListView或者RecyclerView的Item里直接加载了原始分辨率的图片。手机拍照的图动不动三四千万像素,原样加载进列表,内存瞬间就被撑爆。这是项目里常见的“为啥我的APP比别人的卡”的根源。

解决:两步走。一是加载时用BitmapFactory.Options的inSampleSize采样压缩,按需把图片缩小到几个像素再进内存;二是图片多的时候别手写加载,直接换Glide一行代码搞定。手写加载逻辑列表一长就露馅,这是踩过的坑。

BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 4; // 按 1/4 采样,宽高各缩到 1/4 Bitmap bitmap = BitmapFactory.decodeFile(imagePath, opts);

逻辑说明:inSampleSize设为4,采样后图片的内存占用大约变成原来的十六分之一。参数说明:值必须是2的幂,2、4、8、16,系统内部会向下取接近的2的幂值。商品列表场景用4比较合适,再小图会糊。

5.5 混淆打包后Activity找不到:keep规则没写

现象:debug包跑得好好的,release包一打开就闪退,日志报ClassNotFoundException或者NoSuchMethodException,点名某个Activity或实体Bean找不到。

原因:开启混淆后ProGuard把类名、方法名、字段名全部改成了a、b、c之类,而Android要求跳转Activity的类名必须保留原样,JSON解析库用反射读实体类字段,字段名被改了也解析不出来。debug包默认不开混淆,所以这个问题只在release包出现。

解决:在proguard-rules.pro里对ui包和bean包统一加keep规则,保结构不被混淆。实体类必须keep住,因为Gson、org.json这类库通过反射拿字段名,字段改名等于白解析。

-keep class com.example.fruitsales.bean.** { *; } -keep class com.example.fruitsales.ui.** { *; }

逻辑说明:第一行是保留所有实体Bean的类名、字段名和方法名,第二行是保留UI层的Activity和Adapter。参数说明:**表示包下所有子包也生效;{ *; }表示类里所有成员都不混淆。如果用了第三方库,还要按各自文档加对应规则,这个项目技术栈简单,这两行够用。

6. 从课程设计到可演示Demo:验证路径与一个实用技巧

文档能跑通主链路只是第一步,汇报演示能不能顺利演完,看的其实是准备功夫。我给一个固定的演示路径建议:登录注册 → 首页浏览商品 → 打开详情 → 下单 → 查看订单列表 → 提交评价。整个流程控制在十分钟内。演示前把代码里所有写死的测试账号、测试商品确认一遍,别在台上才发现登录密码不对。

还有一个我强烈安利的技巧:让演示不依赖网络。把SQLite数据库提前塞进assets目录,App首次启动时拷贝到data/data/包名/databases/,这样就算后台服务没起来,商品列表和订单数据也能完整展示。准备演示数据时用adb pull把数据库导出来:

adb shell run-as com.example.fruitsales cat databases/fruit_sales.db > seed.db

逻辑说明:run-as命令能以App身份读取私有目录里的数据库文件,导出的seed.db放进项目assets,App启动时用文件流覆盖到本地数据库目录。参数说明:包名必须和Manifest里的一致;数据库文件名为空的话,打开App会自动用DBHelper建一份新库,不会因为assets里没文件崩溃。这招我一直在用,它把演示风险从“网络、服务器、后台服务”三个变量降到了“一台有电的手机”一个变量。

从那以后,我每次拿到课程设计资源,都会强制自己先跑通这条最小闭环——登录、列表、下单、查看订单,再评估要不要继续深入。你也不用一上来就想着把所有功能写全,先把这一条主链路跑顺,再往里加评价、上传、后台管理,越往后的功能就越是在给骨架填肉。希望帮到你。

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

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

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

立即咨询