Android租房APP开发:从数据库设计到图片上传与VIP排序
2026/9/14 15:09:00 网站建设 项目流程

简介:基于安卓的租房信息发布平台应用是一份面向计算机专业毕业设计开发的完整项目资源,覆盖管理员、房东与租户三类核心角色。管理员端可管理房东与客户信息、删除不合理房源、发布修改删除行业政策;房东支持注册登录、修改个人资料并发布带图租房信息;租户区分普通与VIP,能够按条件搜索房源、修改个人资料、提交租房申请,VIP租户还享有预约优先展示和房源通知。压缩包采用zip格式,共1055个文件,包括242个png图片、239个class字节码、192个xml布局配置、152个java源码、78个jpg图片及jsp、js等辅助文件,其中java与xml构成应用核心代码和界面,png/jpg/gif为界面素材,整体约13.14MB,已有522人学习下载。资源包含多个Dao与Action类,项目结构完整,适合正在准备毕业设计或希望掌握安卓租房平台业务逻辑的开发者阅读,可从源码层面梳理房屋管理、订单处理、用户权限等模块思路,并直接复用界面与功能代码。

1. 从一个安卓租房APP的class清单说起

看到这个项目的class列表时,第一反应是它绝对不是纯CRUD。里面有PhotoViewAttacher,说明图片浏览做了手势缩放;有WheelView,大概率是城市或价格选择器;有HouseAction、StudentAction这种带Action后缀的类,说明部分业务逻辑没有直接堆在Activity里。基于Android Studio开发这类租房信息发布平台APP,最难的不是登录注册,而是三件事:房屋状态怎么流转、图片怎么从相册安全进到服务端、VIP租户的预约排序怎么不把SQL写乱。我会按这三条线拆,最后落到管理员审核和政策发布,适合那些想把毕业设计做得能演示、也能应付答辩的人。

2. DAO层与数据库设计:从HouseDao、OrderDao反推表结构

打开构建产物里的class文件能够发现,项目里同时存在ShopDao、HouseDao、OrderDao,但业务流程跟“Shop”没什么关系。这类情况在毕业设计里很常见,从别的项目复制过来的代码没删干净。我通常的做法是先忽略它们,以HouseDao和OrderDao为入口,重画ER图。

2.1 用户单表还是分表:role字段解决80%的问题

这个平台有管理员、房东、租户三种角色,租户又分普通和VIP。很多同学一开始会建三张用户表:admin表、landlord表、tenant表。但这里有一个问题:房东也能注册,租户也能登录,如果三张表分开,登录时得依次查三次,或者维护一张login表,代码量立刻膨胀。更合理的做法是建一张user表,用role字段标识当前身份。

CREATE TABLE t_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role INTEGER NOT NULL DEFAULT 2, -- 0:管理员 1:房东 2:租户 phone TEXT, avatar TEXT, vip_level INTEGER NOT NULL DEFAULT 0, deal_count INTEGER NOT NULL DEFAULT 0, register_time TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) );

这里把管理员也放进同一张表,虽然管理员不需要注册,但我习惯在初始化数据库时通过INSERT OR IGNORE写入一个默认管理员账号,账号密码用admin/admin123,后面改密码走同一个update逻辑。vip_leveldeal_count是VIP升级的两个条件:交易数量达到阈值、注册时间达到规定年数。对应到代码,就是每次租房申请成功后更新deal_count,登录时检查julianday('now') - julianday(register_time)是否大于规定值。

分表的缺点是联表查询极多。比如房源列表要显示房东昵称和联系方式,单表设计里只需要join t_user一次;如果是分表,你得判断发布者到底是房东还是店铺,反而麻烦。所以这个项目选单表,角色权限完全靠role字段控制,够用。

2.2 房源表和订单表:状态字段是设计核心

房源的增删改查集中在HouseDao里,订单/预约的逻辑在OrderDao里。这两张表决定了整个项目能不能转起来。

表名用途关键状态字段
t_user所有用户role, vip_level
t_house房源信息status
t_order预约与租房申请order_type, status

三张表的关系是:t_user是房东和租户的公共表,t_house用landlord_id关联t_user,t_order分别用house_id和user_id关联。注意t_order表里order_type用0表示预约看房,用1表示提交租房申请,这样VIP预约和普通申请共用一张表,查询时不需要二选一。

CREATE TABLE t_house ( id INTEGER PRIMARY KEY AUTOINCREMENT, landlord_id INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, price REAL NOT NULL, area REAL DEFAULT 0, address TEXT, image_list TEXT, -- 用JSON字符串保存多张图片路径 status INTEGER NOT NULL DEFAULT 0, -- 0:待审核 1:已发布 2:已下架 3:已删除 create_time TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), update_time TEXT ); CREATE TABLE t_order ( id INTEGER PRIMARY KEY AUTOINCREMENT, house_id INTEGER NOT NULL, user_id INTEGER NOT NULL, order_type INTEGER NOT NULL, -- 0:预约看房 1:提交租房申请 status INTEGER NOT NULL DEFAULT 0, -- 0:待处理 1:已通过 2:已拒绝 3:已取消 remark TEXT, create_time TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) );

为什么状态字段必须用Integer而不用字符串?因为字符串状态适合人看,不适合程序走分支。比如房东下架房源,只需UPDATE t_house SET status=2 WHERE id=?;管理员删除不合理房源,不直接delete而是改status=3,这样房东的发布历史里能看到自己哪些房源被下架,也能知道原因。t_order里一个房源允许被多个VIP预约,但最终提交租房申请时,会先检查该user是否已经对该house存在type=1且status=0的申请,防止重复提交。

2.2.1 DAO实现:HouseDao的插入与条件查询

有了表结构,DAO层就好写多了。下面是一个简化版的HouseDao:

public class HouseDao { private SQLiteDatabase db; public HouseDao(DatabaseHelper helper) { this.db = helper.getReadableDatabase(); } public long insertHouse(House house) { ContentValues cv = new ContentValues(); cv.put("landlord_id", house.getLandlordId()); cv.put("title", house.getTitle()); cv.put("description", house.getDescription()); cv.put("price", house.getPrice()); cv.put("area", house.getArea()); cv.put("address", house.getAddress()); cv.put("image_list", new JSONArray(house.getImageList()).toString()); cv.put("status", house.getStatus()); // 传入0表示待审核,传入1表示已上架 return db.insert("t_house", null, cv); } }

JSONArray存入image_list字段,取出时再new JSONArray(json).toList(),这样就不需要额外建图片子表。好处是查询一次就能拿到全部图片,坏处是没法针对单张图片做操作,但对这个项目足够。插入时通过status参数决定是否直接上架:如果后台开启审核,就传0;如果希望演示时发布即展示,就传1。实际答辩演示时,我一般建议直接传1,不然列表一直看不到新数据会很尴尬。

2.3 SQLiteOpenHelper:版本升级时别只drop表

如果你用Android Studio创建项目,SQLiteOpenHelper的模板方法里通常建议在onUpgrade里DROP TABLE IF EXISTS再重建,但这样做会导致用户数据全部丢失。毕业设计虽然不用考虑线上升级,但答辩老师通常会问这个问题,所以还是要有最小可用的升级方案:

class DatabaseHelper extends SQLiteOpenHelper { private static final int DB_VERSION = 2; @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE t_user (...)"); db.execSQL("CREATE TABLE t_house (...)"); db.execSQL("CREATE TABLE t_order (...)"); initAdmin(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldV, int newV) { if (oldV < 2) { db.execSQL("ALTER TABLE t_house ADD COLUMN update_time TEXT"); } } }

这里需要注意,ALTER TABLE只能加字段,不能改已有字段类型。如果非要改,就只能建临时表,把数据拷贝过去再删旧表。还有个坑:在onCreate里执行多条CREATE TABLE时,要保证表和表之间没有外键约束,因为SQLite默认外键是关闭的。一旦开启外键,删除房东时就会各种报错。我的建议是不在SQL语句里写FOREIGN KEY,全部在Java层控制,省去外键维护成本。

3. 图片选择与上传:从相册到服务端的完整链路

房东发布房源时,除了文字还要图片。class列表里的PhotoViewAttacher说明项目里已经集成了图片缩放预览的依赖,但真正的难点在权限和压缩。Android 6.0以后,运行时权限把很多人的第一次启动流程打断了;Android 10以后又有了分区存储限制,不能随便把图片路径传给服务端。所以这里把图片链路拆成三步。

3.1 相册选择与Android运行时权限适配

首先要明白:使用系统相册选择图片,不需要存储权限。最低限度只需要ACTION_GET_CONTENTACTION_PICK的Intent,系统会临时授予读取该图片的URI权限。只有你自己把图片保存到应用外部目录时才需要请求WRITE_EXTERNAL_STORAGE。所以推荐直接使用ActivityResultLauncher:

private ActivityResultLauncher<String> pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> { if (uri != null) { imgPath = handleImage(uri); ivPreview.setImageURI(uri); } }); btnSelectImage.setOnClickListener(v -> pickImageLauncher.launch("image/*"));

GetContent是AndroidX Activity库提供的契约,不需要手动写onActivityResult回调,也不需要在AndroidManifest里声明任何存储权限。这是目前最干净的方案。如果你还在用旧的startActivityForResult,请尽快迁移到registerForActivityResult,否则代码会散落在各个Activity里,很难维护。但还有另一个问题:用户从相册选完图之后,我们拿到的URI可能是content://media格式,也可能是content://com.tencent.wework.fileprovider这类来自第三方应用的URI。此时不能直接new File(uri.getPath()),因为你没有那个文件的直接路径权限。首先要判断uri的scheme,如果是content,就用ContentResolver.openInputStream读取。

3.2 图片压缩:不能把原图直接扔进接口

从相册拿到的原图少说2MB,多则10MB。直接以Base64塞进JSON会让接口超时,甚至会溢出内存。常见做法是把图片先采样压缩到1200px宽度,再按质量压缩到80%,最后转成Base64上传。下面是一个工具方法:

public static byte[] compressImage(Context context, Uri uri, int targetWidth) throws IOException { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; InputStream is = context.getContentResolver().openInputStream(uri); BitmapFactory.decodeStream(is, null, opts); is.close(); int width = opts.outWidth; int sample = 1; while (width / (sample * 2) >= targetWidth) { sample *= 2; } BitmapFactory.Options compressOpts = new BitmapFactory.Options(); compressOpts.inSampleSize = sample; InputStream is2 = context.getContentResolver().openInputStream(uri); Bitmap bitmap = BitmapFactory.decodeStream(is2, null, compressOpts); is2.close(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); bitmap.recycle(); return baos.toByteArray(); }

这里inSampleSize必须是2的整数幂,Android底层会按这个值跳过像素,从而避免大图OOM。targetWidth建议设为1200,因为房源图片在手机端展示最宽不过1080px,再大只会浪费流量。质量80%是图片大小和清晰度的平衡点。压缩完的字节数组如果还是太大,就降低质量为60%,或者把targetWidth改成800。如果你的设备是高端机一次选9张图,建议用线程池执行压缩,不要在UI线程里循环处理,否则界面会直接卡死。

3.3 发布房源页:图文数据怎么组装

发布房源页面需要把title、description、price、address、多张图片一起提交。我的做法是先用一个HouseDraft对象在内存里收集数据,再统一构建请求。如果后端是Java的Servlet,前端用OkHttp的MultipartBody上传最合适:

MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", title) .addFormDataPart("price", price) .addFormDataPart("description", desc); for (int i = 0; i < imgList.size(); i++) { byte[] imgBytes = compressImage(context, imgList.get(i), 1200); builder.addFormDataPart("image" + i, "img" + i + ".jpg", RequestBody.create(MediaType.parse("image/jpeg"), imgBytes)); }

addFormDataPart("image" + i, ...)这里命名成image0、image1,后端解析时用request.getPart("image" + i)接收。如果你用List<String>直接传Base64,也可以,但JSON体积会膨胀30%左右,因为Base64编码会增加约33%的长度。我一般只在服务端接口不支持multipart时才会退回到Base64。注意RequestBody.create在OkHttp 4.x已经改成了静态方法,旧项目里如果还在用RequestBody.create(mediaType, file)需要加上@JvmStatic调用,否则编译不过。上传时还要给进度条设置可见性,等待OkHttp的enqueue回调返回后再隐藏,避免用户误以为APP卡死。

3.3.1 图片预览与PhotoViewAttacher的作用

缩略图选完之后,点击预览可以打开大图。项目里出现PhotoViewAttacher,就是一个老牌的图片缩放库,用法很简单:new PhotoViewAttacher(ivPreview).update();。这个库内部通过Matrix处理双指缩放,所以我们要确保ivPreview使用ScaleType.FIT_CENTER,否则缩放时的锚点会错乱。另外,WheelView.class这个类很多项目是用来做城市选择器的,也可以用NumberPicker替代,但自定义WheelView手感更好。发布页的城市、租金范围选择,用它正好。

4. 租户端搜索、预约与VIP优先级排序

租户端的核心功能有三个:按条件搜索房源、预约房源、提交租房申请。最难的是VIP租户看到自己预约的房源排在最前面。先说搜索。

4.1 条件搜索:SQL拼接不是concat

页面上的搜索条件通常是价格区间、面积、地址关键字、排序方式。很多人喜欢拼SQL字符串时直接String sql = "select * from t_house where " + condition,这在SQLite里能跑,但后面要改成?占位符会非常痛苦。正确做法是把可能为空的参数动态拼接:

public List<House> searchHouse(double minPrice, double maxPrice, String address) { StringBuilder sb = new StringBuilder("SELECT * FROM t_house WHERE status = 1"); List<String> args = new ArrayList<>(); if (minPrice > 0) { sb.append(" AND price >= ?"); args.add(String.valueOf(minPrice)); } if (maxPrice > 0) { sb.append(" AND price <= ?"); args.add(String.valueOf(maxPrice)); } if (address != null && !address.isEmpty()) { sb.append(" AND address LIKE ?"); args.add("%" + address + "%"); } sb.append(" ORDER BY create_time DESC"); return db.rawQuery(sb.toString(), args.toArray(new String[0])); }

rawQuery的第二个参数是String数组,里面每个?依次对应数组中的元素。注意LIKE子句里的通配符%要放在参数里,而不是拼在SQL模板里,不然?占位符会被破坏。地址模糊查询有一个性能隐患:LIKE '%xx%'无法使用B树索引,房源量少无所谓,量大时建议再加一个全文检索表,或者用FTS4虚拟表,但那是加分项,不是必做。

4.2 预约流程:OrderDao写入与状态流转

VIP租户预约看房,前端点击“预约”按钮后,调用OrderDao.insertOrder。这里要注意:预约不是按钮点击一下就完事,要防重复提交。

public boolean insertOrder(int houseId, int userId, int orderType) { if (orderType == 1) { Cursor cursor = db.rawQuery( "SELECT id FROM t_order WHERE house_id=? AND user_id=? AND order_type=1 AND status != 3", new String[]{String.valueOf(houseId), String.valueOf(userId)}); if (cursor.moveToFirst()) { cursor.close(); return false; // 已存在有效申请,不允许重复提交 } cursor.close(); } ContentValues cv = new ContentValues(); cv.put("house_id", houseId); cv.put("user_id", userId); cv.put("order_type", orderType); cv.put("status", 0); long rowId = db.insert("t_order", null, cv); return rowId != -1; }

status != 3表示排除已取消的记录。这里用status=0作为“待处理”,房东端看到待处理预约后可以联系租户,然后把状态改成1或者2。很多团队会再加一个pay_amount字段,但租房申请在未签约前不涉及金额,不建议提前引入。如果答辩老师问你交易数量怎么算,答案就是:每当一个order_type=1的记录被房东标记为已完成后,deal_count++,再更新user表的vip_level。

4.3 VIP优先排序:一条SQL还是Java排序?

回到开头的问题:VIP租户看到的房源列表,自己预约过的排在最前。最简单的方法是查询时带着当前userId,用LEFT JOIN把预约状态带出来:

SELECT h.*, CASE WHEN o.id IS NOT NULL THEN 1 ELSE 0 END AS is_reserved FROM t_house h LEFT JOIN t_order o ON o.house_id = h.id AND o.user_id = ? AND o.order_type = 0 AND o.status != 3 WHERE h.status = 1 ORDER BY is_reserved DESC, h.create_time DESC;

这里有个很容易错的点:LEFT JOIN的条件要写在ON子句里,不能放在WHERE子句里。如果写成WHERE o.user_id = ?,LEFT JOIN就退化成了INNER JOIN,没预约过的房源全没了。is_reserved是一个计算列,值为1或0,ORDER BY先按它倒序,再按发布时间倒序。这样VIP租户预约过的房源天然排在最前面,且不需要在Java端做二次排序。普通租户没有预约功能,前端不传userId,就换成一个简单查询:WHERE status=1 ORDER BY create_time DESC

4.3.1 为什么不在Java层做排序

有人觉得SQL这么复杂,不如查询后遍历一遍,根据一个hash集合标记排序。如果列表只有几十条,两种方式性能都行;但分页阶段就不行了。比如一页20条,可能这20条里预约过的房源一条都没有,因为它们排在第二页,Java排序就没法把它提到第一页。这个问题只有在SQL里一次性把排序条件表达清楚才能解决。所以在数据库层面做排序列是最正确的设计。同时,这个方案也兼容了价格排序、时间排序等其他排序条件,你只需要在ORDER BY后面追加字段即可,不会破坏is_reserved的优先级。

5. 管理员审核与行业政策模块:用最少代码撑起后台功能

管理员端的功能集中在HouseAction和StudentAction之类的类里。这里说几个能直接用的技巧。

5.1 删除房源:软删除比DELETE安全

管理员看到不合理房源,删除时执行UPDATE t_house SET status=3 WHERE id=?,不要执行DELETE。因为t_order表里可能有针对该房源的预约记录,如果删除了house行,这些记录就变成孤儿数据,关联查询时会遇到空指针。软删除后,房东端可以看到“已下架”状态,如果导师做成申诉流程,还能重置回1。列表查询始终带上WHERE status=1,基本不会影响性能。

5.2 行业政策发布:HTML富文本的适配

行业政策如果只是纯文本,页面会很干。管理员端发布政策时,可以传一段HTML片段。Android端渲染时,用Html.fromHtml比引入WebView更轻量:

String content = policy.getContent(); Spanned spanned; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { spanned = Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY); } else { spanned = Html.fromHtml(content); } tvPolicyContent.setText(spanned);

FROM_HTML_MODE_LEGACY是为了兼容Android 7.0以上的行为差异,不加这个参数,很多自定义标签会被丢弃。还要注意,如果政策内容包含<img>标签,fromHtml默认不会加载网络图片,需要自定义Html.ImageGetter,这是很多新手会踩的坑。政策列表刷新时,新增了政策用notifyDataSetChanged()就好,数据量小,没必要用DiffUtil。

5.3 验证VIP升级条件是否可复现

最后说一个演示时最容易被问到的点:VIP租户怎么升级。这个项目里有交易数量和注册时间两个条件,建议在登录后调用一个refreshVipStatus方法,用SQL把两个条件一起判断,而不是在注册时就固定vip_level。比如UPDATE t_user SET vip_level=1 WHERE deal_count >= threshold OR julianday('now') - julianday(register_time) >= years。这样,答辩时可以通过修改系统时间验证注册时间条件,或者通过设置deal_count阈值验证交易条件,整个过程不依赖服务端定时任务,纯客户端也能演示完整闭环。

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

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

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

立即咨询