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

Android Studio课堂点名App开发实战:从设计到代码全解析

简介移动应用开发中原生Android技术栈凭借高效的系统调用能力与成熟的开发环境成为众多工具类应用的首选。Android Studio作为官方IDE提供了从界面设计到调试打包的完整工作流。在数据存储层面SQLite以轻量级、免配置的优势支撑本地数据管理通过合理设计表结构与唯一约束可有效保障考勤记录的幂等性与一致性。结合RecyclerView的高效列表刷新机制开发者能够实现流畅的点名操作与实时状态反馈。此类技术组合广泛应用于课堂考勤、会议签到、活动登记等场景尤其适合课程设计与毕业设计项目。围绕使用Android Studio开发课堂点名签到App的完整过程涵盖数据库建模、随机点名算法、考勤统计导出等核心实现与常见问题排查为开发者提供一条可直接落地的实践路径。 课堂上点名签到这件事看起来简单真做起来比想象中麻烦得多。纸质名单点一遍要两三分钟代答、漏记、课后统计考勤更是让人头大。所以我才决定用 Android Studio 从零写了一个安卓课堂点名签到 app把“随机点名 手动签到 考勤统计”整套流程塞进一部手机里。这篇文章就把我整个开发过程、核心代码、数据库设计、还有踩过的那些坑全部拆开讲清楚希望能给正在做安卓课程设计、毕设或者自己练手的同学一条能直接走下去的路。这个项目最适合两类人一类是刚学完 Android 四大组件和 SQLite想做个完整综合练手的安卓开发初学者另一类是准备交课程设计或毕业设计需要一个功能完整、逻辑清晰、还能现场演示的“点名签到”项目的同学。不管你是哪种看完这篇都能搞清楚一个完整的 app 是怎么从一个想法变成一行行代码的。1. 项目立项从需求到技术选型1.1 为什么选择原生 Android Studio 开发在动手写代码之前先把技术路线定下来。目前安卓 app 的主流开发方案无非三种原生 AndroidJava/Kotlin、跨平台框架Flutter/React Native/Uni-app、以及低代码平台。我最终选择了 Android Studio 原生开发核心原因有三个。第一课堂点名签到 app 的核心场景是“打开即用、快速操作”涉及的 UI 交互不算复杂但需要稳定调用系统数据库、通知栏、震动、语音播报等本地能力原生开发在这些方面有天然优势不需要引入额外桥接层。第二Android Studio 是谷歌官方的 IDE对国内学习者和课程设计场景最友好。调试工具、模拟器、布局编辑器、Gradle 构建系统都集成在一个环境里出现问题搜索解决方案时资料也最全。用原生方式写出来的代码可读性高老师检查代码时也更容易理解你的设计思路。第三这个项目不需要复杂的网络通信不是云同步那种架构。单机本地存储就够用原生 SQLite 直接搞定没必要为了一个本地应用去包一层 Flutter 或 React Native。技术选型讲究“够用就好”过度设计反而是负担。1.2 功能清单与用例设计需求分析阶段我列了一个功能清单按优先级从高到低排。第一优先级是点名核心链路老师选择课程、发起一次点名、学生名单展示、标记到课状态出勤/迟到/请假/缺勤、结束点名后生成本次考勤结果。第二优先级是名单管理支持手动添加学生、批量导入学生、多门课程切换。第三优先级是统计功能按学生维度统计出勤率、按日期维度查看历史考勤记录。实际的用例设计其实比想象中复杂。例如“一次点名”到底是什么概念我一开始直接用一个布尔字段记录学生是否出勤后来发现不够用——同一个人可能上下午各有一堂课必须用“课程ID 日期 节次”共同定位一条考勤记录。这个看似不起眼的设计决定了后面数据库表结构的长相。所以做用例时就要想清楚业务边界不然写代码的时候大概率要重构。1.3 技术栈选择的取舍逻辑具体到项目内部的技术选型我做了几个关键决定。界面层用传统的 XML 布局没有上 Jetpack Compose因为这个项目的 UI 以列表和简单表单为主XML 布局在 Android Studio 上写了五年以上的教程都是这么教的遇到问题网上答案最全课程设计答辩时也好解释。数据存储用 SQLite 原生数据库配合 SQLiteOpenHelper没有引入 Room 框架。Room 确实好用但需要额外写注解和 DAO 接口对新手来说多了一层概念而且课程设计项目里“手写 SQL 建表”反而是一个加分项能体现你对数据库的理解。不过我也在代码里做了一定封装不把 SQL 语句散落在 Activity 里而是统一放进 DatabaseHelper 类后期维护很舒服。列表展示用 RecyclerView适配器和 ViewHolder 手写。虽然 ListView 更简单但 RecyclerView 是目前的主流而且它的点击事件处理、数据刷新机制更现代学一遍在未来其他项目里也用得上。至于网络请求、图片加载这些库这个项目完全用不上我就没有引入保持工程干净。2. 数据库设计与点名逻辑的底层思考2.1 三张核心表的结构设计点名签到 app 的数据模型我设计了三个核心表课程表、学生表、考勤记录表。这里直接把我最终定稿的建表语句贴出来已经过多个版本迭代字段都细化到可以直接用的程度。-- 课程表 CREATE TABLE course ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, teacher TEXT, create_time TEXT DEFAULT (datetime(now, localtime)) ); -- 学生表 CREATE TABLE student ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, name TEXT NOT NULL, course_id INTEGER NOT NULL, phone TEXT, remark TEXT, FOREIGN KEY(course_id) REFERENCES course(id) ON DELETE CASCADE ); -- 考勤记录表 CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_id INTEGER NOT NULL, student_id INTEGER NOT NULL, date TEXT NOT NULL, period INTEGER NOT NULL, -- 第几节课可取1,2,3,4... status INTEGER NOT NULL, -- 0缺勤 1出勤 2迟到 3请假 sign_time TEXT, UNIQUE(course_id, student_id, date, period), FOREIGN KEY(course_id) REFERENCES course(id) ON DELETE CASCADE, FOREIGN KEY(student_id) REFERENCES student(id) ON DELETE CASCADE );这里最值得说的是考勤记录表的唯一约束UNIQUE(course_id, student_id, date, period)。这个约束保证了同一个学生同一门课同一天同一节次只会有一条考勤记录。如果没有这个约束用户重复点击“开始点名”就会生成重复数据统计出勤率时就全乱套了。有了唯一约束之后配合INSERT OR REPLACE或INSERT OR IGNORE幂等性问题就彻底解决了。外键的ON DELETE CASCADE也很关键。删除一门课程或者一个学生时关联的考勤记录会自动清理不然界面上数据删了数据库里还残留一堆孤儿数据以后统计的时候查出来一堆从前的幽灵记录特别诡异。2.2 点名状态的流转与“一次点名只记一次”的实现点名状态的业务逻辑是整个 app 里最绕的地方。我一开始天真地以为点击“开始点名”按钮后在内存里维护一个列表标记每个人的出勤状态结束点名时一次性写入数据库就行。但实际操作中经常遇到这种情况老师正点到一半手机来电、切后台、甚至 app 被杀掉了结果这半截点名数据全丢了。所以我把方案改成了“边点边存”每次点击某个学生的状态按钮立即将这条考勤记录写入或更新到数据库。开始点名时先检查该课程该日期该节次是否已有考勤记录如果有就加载已有的记录状态没有就为所有学生初始化成“缺勤”状态。这个“初始化就是写入修改也是写入”的设计让整个点名过程天然具有断电恢复能力。状态流转上我用整数常量来标记0 缺勤、1 出勤、2 迟到、3 请假。四个状态用一种分段切换的逻辑点击学生条目时在“出勤 → 迟到 → 请假 → 缺勤”之间循环切换长按则直接切换为“出勤”。交互上考虑得很细——点按循环适合快速操作长按直接标记出勤适合那种“一大批人到齐了”的场景。每次状态变化立即更新数据库中的 status 字段和 sign_time 时间戳。2.3 界面层拆解三个页面管住整个流程整个 app 的界面从用户操作路径来看只需要三个核心页面首页课程列表页、课程详情学生列表页也是点名执行页、统计页。这三个页面之间的跳转逻辑我梳理得很清楚不走多余流程。首页是课程的横向卡片列表右上角有“添加课程”按钮。点击某张卡片进入课程详情页这里显示该课程的花名册和一个醒目的“开始点名”按钮。点击“开始点名”后页面底部弹出本次点名的控制条显示当前签到人数、缺勤人数并提供“结束点名”按钮。结束点名后本页面的学生列表恢复成只读模式每个学生后面显示本次考勤状态颜色区分一目了然。统计页放在首页顶部的一个图表入口里按课程选择后展示最近七次考勤的汇总表格。这里有一个界面细节特别想分享点名进行中的学生列表行背景色会随状态实时变化——绿色出勤、橙色迟到、蓝色请假、灰色缺勤。这样做的好处是即便屏幕显示不下所有学生老师滚动列表时也能通过颜色快速定位还没处理的人极大提升点名效率。实际演示的时候这个设计效果特别好老师一眼就能看到谁的考勤状态还没被确认。3. 实操环节核心模块的落地实现3.1 Android Studio 环境与工程配置第一个落地环节是环境搭建。我用的是 Android Studio 最新稳定版这里不纠结具体小版本号只要不低于 Flamingo 2022.2.1 就可以。项目 minSdk 设为 21覆盖 Android 5.0 及以上设备targetSdk 设为 34适配 Android 14 的要求compileSdk 对应也设为 34。android { namespace com.example.attendance compileSdk 34 defaultConfig { applicationId com.example.attendance minSdk 21 targetSdk 34 versionCode 1 versionName 1.0.0 } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android.txt), proguard-rules.pro } } }这里提醒新手一个常见坑namespace和applicationId是两回事。applicationId是应用的唯一标识决定你在应用市场上架时的包名namespace是代码里 R 类和相关 BuildConfig 的包路径。如果你从旧项目迁移这俩经常不一致编译时会报出各种找不到 R 类的问题。正确做法是让 namespace 和应用的根包名一致applicationId 可以单独设置成你自己的域名反写。3.2 学生名单批量导入的两种姿势学生名单录入是使用门槛的关键。如果一个一个手动添加50 人的班级要操作五分钟体验极其糟糕。我做了两种批量导入方式一种是从剪贴板粘贴另一种是从 CSV 文件导入。剪贴板导入的实现逻辑很简单用户从 Excel 里复制“学号,姓名”两列内容到 app 里点击“批量导入”弹出一个文本框粘贴后程序逐行解析用英文逗号兼容中文逗号分割第一列作为学号第二列作为姓名循环插入数据库。每插入一条前先查询是否已存在同课程同学号如果存在则跳过并计数最后用 Toast 提示“成功导入 48 人跳过 2 个重复项”。private int importStudentsFromText(String courseId, String rawText) { String[] lines rawText.trim().split(\\r?\\n); int successCount 0; for (String line : lines) { String[] parts line.split([,]); if (parts.length 2) continue; String no parts[0].trim(); String name parts[1].trim(); ContentValues values new ContentValues(); values.put(student_no, no); values.put(name, name); values.put(course_id, courseId); long newId db.insert(student, null, values); if (newId ! -1) successCount; } return successCount; }这段代码看起来短但里面藏着几个细节。正则[,]同时匹配英文和中文逗号用户从微信或 Excel 复制的数据经常混着中英文标点这个问题不处理就会出现“学号后带一堆空格”的脏数据。trim()必须调用否则姓名前后隐藏的空格会显示出来很别扭。插入时没有显式查重而是靠数据库里 student_no 和 course_id 的联合唯一约束来决定insert是否失败返回 -1 就说明是重复数据直接跳过。这样既省了一次查询又保证了数据唯一性。CSV 文件导入稍微复杂一点需要用到OpenDocument文件选择器。Android 的ActivityResultContracts.OpenDocument不需要申请存储权限这是目前最推荐的方案。选择文件后通过contentResolver.openInputStream(uri)读取内容然后复用上面这段解析逻辑。注意读取 CSV 时统一按 UTF-8 编码解析有些 Windows 上生成的 CSV 文件是 GBK 编码读出来全是乱码这个坑我踩过一次。解决办法是读取前先判断 BOM 和尝试用字节流检测编码或者干脆在导入界面放一个“编码选择”下拉框。3.3 随机点名与手动签到双模式实现点名功能我做了两种模式手动签到模式和随机点名模式。手动签到模式就是列表上挨个点选状态适合全班人数少或者有充分时间课前的场景随机点名模式则是我这个 app 的亮点每次点击“随机点名”按钮程序会从所有未标记为“出勤”的学生中随机选出一个放大显示其姓名和学号并伴有震动反馈和语音播报。随机点名的核心是“从剩余未出勤学生中抽取”不是从全班学生中抽取。这个逻辑差异很重要如果从全班抽很可能抽中已经点过名的人老师得重新抽体验很差。实现方法是查询数据库时加一个条件status ! 1把未出勤学生放在一个列表中然后调用Collections.shuffle()打乱顺序后取第一个或者用Random.nextInt(list.size())取值。private Student pickRandomAbsentStudent(int courseId, String date, int period) { ListStudent absentList new ArrayList(); Cursor cursor db.rawQuery( SELECT s.id, s.student_no, s.name FROM student s LEFT JOIN attendance a ON a.student_id s.id AND a.course_id ? AND a.date ? AND a.period ? WHERE s.course_id ? AND (a.id IS NULL OR a.status ! 1), new String[]{String.valueOf(courseId), date, String.valueOf(period), String.valueOf(courseId)}); // 遍历 cursor组装 absentList ... if (absentList.isEmpty()) return null; int index new Random().nextInt(absentList.size()); return absentList.get(index); }这个 SQL 用了一次 LEFT JOIN核心逻辑是找出所有“没有考勤记录”或“考勤状态不是出勤”的学生。这里a.id IS NULL就是那些从未被初始化过考勤记录的学生理论上如果每次点名开始都初始化了记录就不该出现但为了防止意外情况这个条件一定要保留。语音播报我用的是TextToSpeech系统接口初始化成功后调用speak(name, QUEUE_FLUSH, null, utteranceId)即可注意在onDestroy里调用shutdown()释放资源不然会出现播完一次后 app 退出发出杂音的诡异 bug。3.4 考勤统计与导出逻辑考勤统计页是老师最关心的部分。我用一个组合查询按学生聚合出勤率、迟到率、缺勤率并按出勤率从高到低排序。这个查询看似简单但聚合条件一定要写对。SELECT s.student_no, s.name, COUNT(CASE WHEN a.status 1 THEN 1 END) AS attend_count, COUNT(CASE WHEN a.status 2 THEN 1 END) AS late_count, COUNT(CASE WHEN a.status 3 THEN 1 END) AS leave_count, COUNT(CASE WHEN a.status 0 THEN 1 END) AS absent_count, COUNT(a.id) AS total_count FROM student s LEFT JOIN attendance a ON a.student_id s.id WHERE s.course_id ? GROUP BY s.id, s.student_no, s.name ORDER BY attend_count DESC, absent_count ASC;这里用COUNT(CASE WHEN ... THEN 1 END)比SUM(CASE WHEN ... THEN 1 ELSE 0 END)更直观且不会因为 null 值产生错误。COUNT(a.id)统计的是该学生参与过的点名次数注意这里不能写成COUNT(*)因为如果该学生没有考勤记录LEFT JOIN 出来的所有字段都是 NULLCOUNT(*)会给一个 1而COUNT(a.id)会正确给 0。这种细节一旦错了统计表上就会出现“0 次考勤但出勤率 100%”这种反直觉数据。导出功能我把统计结果生成 CSV 文件用MediaStore.Downloads写入公共下载目录这样不需要权限就能在 Android 10 上实现导出。生成 CSV 时要注意如果姓名或备注里有逗号需要给该字段加上双引号并且字段内部的双引号要转义成两个双引号。这个规则不处理的话导出的文件用 Excel 打开时列会错位看起来特别业余。导出完成后弹一个 Snackbar点击“打开”可以跳转到文件管理器查看文件点击“分享”则通过系统分享面板发送方便老师直接发到班级群或教学秘书邮箱。private void exportAttendanceToCsv(int courseId, File csvFile) { // 按课程查询所有学生的考勤汇总遍历写入CSV StringBuilder sb new StringBuilder(); sb.append(学号,姓名,出勤次数,迟到次数,请假次数,缺勤次数,出勤率\n); for (AttendanceStat stat : getAttendanceStat(courseId)) { sb.append(escapeCsvField(stat.studentNo)).append(,) .append(escapeCsvField(stat.name)).append(,) .append(stat.attendCount).append(,) .append(stat.lateCount).append(,) .append(stat.leaveCount).append(,) .append(stat.absentCount).append(,) .append(stat.attendRate).append(\n); } // 写入文件 ... }4. 常见问题与排查技巧实录4.1 SQLite 版本升级别只改版本号不写迁移逻辑开发过程中最头疼的问题之一就是数据库版本升级。我的 app 经历了三个迭代第一次只有考勤表和课程表第二次加了学生表第三次又在考勤表里加了 period 字段。每次升级如果不处理onUpgrade用户手机上安装旧版本后直接覆盖安装新版app 打开就会崩溃报错信息通常是no such column: period或者no such table: student。解决这个问题的标准姿势是在onUpgrade里按版本号分段写迁移逻辑而不是简单粗暴地DROP TABLE重建。你想想如果直接删表重建用户之前辛辛苦苦记录的考勤数据就全没了这在课程设计答辩演示时反而是减分项。我推荐的形式是这样Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(CREATE TABLE student (...)); if (oldVersion 3) { db.execSQL(ALTER TABLE attendance ADD COLUMN period INTEGER DEFAULT 1); } } }注意这里用了if (oldVersion 2)而不是switch (oldVersion)因为用户可能直接从版本 1 跳升到版本 3如果是 switch 写法版本 1 直接跳到 3 时中间那段修改版本的逻辑就不会执行数据库结构就残缺了。分段递增判断的写法保证任何版本的升级路径都会经历完整的迁移过程。4.2 RecyclerView 刷新时数据不更新的经典坑我在做点名列表的实时刷新时遇到过一个问题点击学生条目修改了数据库状态但 RecyclerView 列表显示的一直是旧状态必须重新进入页面才能看到变化。排查半天发现是适配器用的notifyDataSetChanged()位置不对我在子线程修改数据库后直接调用了刷新但 RecyclerView 的刷新操作必须要回到主线程执行。解决办法是在修改数据库后立即调用adapter.notifyItemChanged(position)并传入当前列表中的准确位置而不是整个列表刷新。进一步说为了提升点名时的流畅度我做了局部刷新优化。notifyDataSetChanged()本质上会重建所有可见的 ViewHolder如果列表有 60 个学生每次点一个就重建 60 个条目视觉上会有轻微闪烁。而notifyItemChanged(position)只重建对应的一个条目点击时反应更跟手。要做到这个优化列表的数据源学生对象必须实时同步数据库中的状态我在每个学生实体上绑定了一个课程和日期在点击回调中用数据库查询返回的最新状态去更新列表项。如果你的列表结构简单直接用 DiffUtil 也可以但那样引入的计算复杂度对这个场景来说有点过度设计。4.3 运行时权限与机型适配的注意事项点名 app 涉及的功能不多但有一个权限绕不开读外部存储导入名单、写外部存储导出 CSV以及 TextToSpeech 引擎。在 Android 6.0 之后运行时权限机制下如果不在代码里动态申请直接访问存储会静默失败。我最终采用ActivityResultContracts.RequestPermission来申请权限在用户拒绝时给出理性说明。如果你只是通过OpenDocument选择文件完全可以不申请读权限但如果使用传统READ_EXTERNAL_STORAGE读取就必须动态申请。Android 10 之后还有一种更优雅的存储策略是Scoped Storageapp 只能访问自己的专属目录和公共媒体目录所以我建议直接放弃老旧的绝对路径存储统一采用getExternalFilesDir()或者 MediaStore 接口。这样既免去了复杂的权限处理又符合 Google Play 的上架政策。机型适配方面遇到最多的是全面屏手势区遮挡底部按钮的问题。点名界面底部有一个“结束点名”按钮在 iPhone 式全面屏手机上会被系统手势条挡住一部分触发不了点击。解决办法是在布局的最外层或者按钮下方加上android:fitsSystemWindowstrue或者在根布局设置View.SYSTEM_UI_FLAG_LAYOUT_STABLE和WindowInsets监听。我最后是给底部按钮上方留出16dp navigation bar height的 padding用WindowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars())动态获取导航栏高度这样在所有机型上都能正确显示。4.4 打包发布与代码混淆的注意事项App 开发完面临的是打包发布问题。课程设计一般不需要上架应用市场但至少要能打出一个 APK 包在手机上运行。Android Studio 的 Build - Generate APK 走的是 release 签名包流程。这里提醒一个新手容易踩的坑不要直接使用 debug 签名打包给老师演示。debug APK 的签名是 Android Studio 自动生成的过期时间是有限的过一段时间后签名文件会过期老用户升级时会提示签名不一致无法覆盖安装。正确做法是创建一个自己的 release 签名 keystore妥善保存密码这个 keystore 文件千万不能丢失一旦丢失你后续再也无法更新这个 app。开启minifyEnabled true后ProGuard 混淆规则也要注意。我的项目使用 SQLite 原生 API不涉及反射框架所以混淆配置比较简单主要确保不混淆实体类即可。如果你的考勤统计模块引入了 Gson 等 JSON 解析库实体类上加Keep注解或者配置 keep 规则否则运行时会因为字段被混淆而导致解析出 null排查半天找不到原因。我后来统一给所有 model 类加了Keep一劳永逸。还有一个细节是 release 包的标志可以在onCreate里用BuildConfig.DEBUG判断当前是 debug 还是 release 环境如果是 release可以隐藏开发调试入口比如清空数据库的按钮。这样打包出来的演示版不会误触清空数据避免现场演示的尴尬。4.5 语音播报与屏幕常亮的两个小技巧最后补充两个提升演示体验的小技巧。课堂场景老师经常把手机放在讲台上点名时如果屏幕自己锁了整个业务就中断了。我的做法是在点名进行状态时调用getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)强制屏幕保持常量结束点名后移除该标志。如果用FLAG_KEEP_SCREEN_ON而不是通过唤醒锁实现系统会智能地识别当前是否插电、是否低电量不会太耗电。语音播报的坑在于TextToSpeech的初始化是异步的。你在onCreate里调用new TextToSpeech(context, listener)如果立刻点击随机点名监听器可能还没回调onInit成功的通知直接调用speak()时会静默失败。解决办法是初始化时加一个 boolean 标志ttsReady只有等到onInit返回TextToSpeech.SUCCESS才置为 true随机点名时若标志为 false 则跳过语音播报、只做震动提示。这样即使设备没有安装语音引擎点名功能也不会因此崩溃只是少了播报这个锦上添花的功能。在AndroidManifest.xml里还要声明android.permission.VIBRATE否则震动不生效。别嫌我啰嗦这种小权限最容易忽略很多同学写完代码发现随机点名时不震动折腾半天才发现没有在清单文件里加这一行。写在最后的一些体会开发这个点名 app 对我来说最大的收获不是“完成了任务”而是亲身经历了从需求分析、数据库设计、UI 开发到打包发布的完整闭环。课堂上再顺手的点名工具也不可能完全满足每个老师的习惯自己掌握开发能力后可以根据实际教学场景随时增加功能比如接入 Wi-Fi 考勤、二维码签到、多设备同步这些都是这个项目未来的扩展方向。回看整个过程最值得坚持的其实是“先把数据结构想清楚再写代码”这个习惯。很多功能实现到最后发现很别扭本质上都是表结构设计时漏了约束或者漏了字段。如果现在让我重新做一遍这个项目我会在写第一行代码之前先把实体关系图、表结构、页面跳转关系都画在纸上确认没有逻辑漏洞再开工。这套方法论在任何软件项目中都能复用比单纯掌握几个 API 有价值得多。如果你正在做一个类似的点名签到 app遇到具体问题卡住了或者在某些实现细节上有更好的方案欢迎在评论区交流。我会第一时间回复。本文还有配套的精品资源点击获取
分享:

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

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