拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android租房APP开发:从数据库设计到图片上传与VIP排序

简介基于安卓的租房信息发布平台应用是一份面向计算机专业毕业设计开发的完整项目资源覆盖管理员、房东与租户三类核心角色。管理员端可管理房东与客户信息、删除不合理房源、发布修改删除行业政策房东支持注册登录、修改个人资料并发布带图租房信息租户区分普通与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_level和deal_count是VIP升级的两个条件交易数量达到阈值、注册时间达到规定年数。对应到代码就是每次租房申请成功后更新deal_count登录时检查julianday(now) - julianday(register_time)是否大于规定值。分表的缺点是联表查询极多。比如房源列表要显示房东昵称和联系方式单表设计里只需要join t_user一次如果是分表你得判断发布者到底是房东还是店铺反而麻烦。所以这个项目选单表角色权限完全靠role字段控制够用。2.2 房源表和订单表状态字段是设计核心房源的增删改查集中在HouseDao里订单/预约的逻辑在OrderDao里。这两张表决定了整个项目能不能转起来。表名用途关键状态字段t_user所有用户role, vip_levelt_house房源信息statust_order预约与租房申请order_type, status三张表的关系是t_user是房东和租户的公共表t_house用landlord_id关联t_usert_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 status2 WHERE id?管理员删除不合理房源不直接delete而是改status3这样房东的发布历史里能看到自己哪些房源被下架也能知道原因。t_order里一个房源允许被多个VIP预约但最终提交租房申请时会先检查该user是否已经对该house存在type1且status0的申请防止重复提交。2.2.1 DAO实现HouseDao的插入与条件查询有了表结构DAO层就好写多了。下面是一个简化版的HouseDaopublic 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_CONTENT或ACTION_PICK的Intent系统会临时授予读取该图片的URI权限。只有你自己把图片保存到应用外部目录时才需要请求WRITE_EXTERNAL_STORAGE。所以推荐直接使用ActivityResultLauncherprivate ActivityResultLauncherString 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)接收。如果你用ListString直接传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 ListHouse searchHouse(double minPrice, double maxPrice, String address) { StringBuilder sb new StringBuilder(SELECT * FROM t_house WHERE status 1); ListString 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_type1 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表示排除已取消的记录。这里用status0作为“待处理”房东端看到待处理预约后可以联系租户然后把状态改成1或者2。很多团队会再加一个pay_amount字段但租房申请在未签约前不涉及金额不建议提前引入。如果答辩老师问你交易数量怎么算答案就是每当一个order_type1的记录被房东标记为已完成后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或0ORDER BY先按它倒序再按发布时间倒序。这样VIP租户预约过的房源天然排在最前面且不需要在Java端做二次排序。普通租户没有预约功能前端不传userId就换成一个简单查询WHERE status1 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 status3 WHERE id?不要执行DELETE。因为t_order表里可能有针对该房源的预约记录如果删除了house行这些记录就变成孤儿数据关联查询时会遇到空指针。软删除后房东端可以看到“已下架”状态如果导师做成申诉流程还能重置回1。列表查询始终带上WHERE status1基本不会影响性能。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_level1 WHERE deal_count threshold OR julianday(now) - julianday(register_time) years。这样答辩时可以通过修改系统时间验证注册时间条件或者通过设置deal_count阈值验证交易条件整个过程不依赖服务端定时任务纯客户端也能演示完整闭环。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门