简介:这份资源是面向Android初学者的课程设计项目,基于Android Studio开发,完整实现了人员管理系统的用户登录与增删改查功能。包内为85.15MB的RAR压缩包,共3291个文件,涵盖39个java源码、525个xml布局与配置、11个可安装apk、91个dex字节码,以及webp图片、json配置和gradle构建文件等,既可直接查看核心代码,也可导入工程学习完整项目结构。已有1425人学习下载。内容围绕登录界面设计、数据校验、SQLiteOpenHelper建表、CRUD操作、RecyclerView列表展示、Material Design界面优化与Gradle构建部署展开,并附带编译产物与调试文件,便于对照实际运行效果理解Android开发全流程。对于需要完成人员管理类课程设计或入门Android数据存储与界面开发的学习者,这是一份可运行、可参考、可二次改造的实用资料。
1. 基于Android Studio的人员管理系统:为什么说它比你想的更简单
把“人员管理系统”做成一个Android应用,很多人的第一反应是:要搭服务器、要搞网络交互、要上数据库中间件。但你拿到的这份课程设计资源里,核心只有两样东西——Android Studio和一个本地SQLite数据库文件。人员数据的增删改查、关键字搜索、部门筛选、导出备份,全都跑在手机本地,不需要外网,不需要后端,甚至不需要一个真实的服务器接口。这种两层架构的好处很直白:逻辑清晰、演示稳定、答辩时几乎不会因为网络问题翻车。适合刚学完安卓四大组件、想拿一个完整项目练手,或者课程设计需要快速落地成型的同学。这篇文章会从数据库层拆到界面层,再到参数设置和坑点,全程按我拆项目时实际会动的代码来写。
2. SQLite数据层:先从人员表结构和DAO层搭起
2.1 数据库表结构:字段别贪多,够用还要能扩展
人员管理系统最忌讳一上来就把表设计得很复杂,比如“员工绩效表”“考勤表”“薪资表”全塞进一张表。我经手的模拟项目X里,最初版本只落四张表,但真正跑通后维护成本最高的反而是字段最杂的那张。这次的人员管理系统,我一般只保留一张主表,字段控制在六个以内:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 自增主键 |
| name | TEXT | NOT NULL | 姓名 |
| phone | TEXT | NOT NULL | 手机号 |
| department | TEXT | 默认“未分配” | 部门 |
| position | TEXT | 可空 | 岗位 |
| remark | TEXT | 可空 | 备注 |
为什么不做多张表?因为课程设计阶段的检索需求大多是“按名字搜”“按部门列”,一张宽表配合SQL的WHERE条件就足够覆盖。真到了要扩展,SQLite的ALTER TABLE也可以后续动态加列。表结构设计的第一原则是:当前功能能跑通,未来改起来不难。
2.2 SQLiteOpenHelper实现:版本号不要乱动
DAO层第一步是写一个继承SQLiteOpenHelper的帮助类。常见的坑是很多人在onCreate里建表,然后每次改表结构就卸载App重装,这样连错误现场都找不到。正确的做法是把数据库版本号当成一个设计参数,每次表结构变动就升版本号,让onUpgrade兜底。
public class DatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "person_manager.db"; private static final int DB_VERSION = 1; public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { String sql = "CREATE TABLE IF NOT EXISTS person (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name TEXT NOT NULL, " + "phone TEXT NOT NULL, " + "department TEXT DEFAULT '未分配', " + "position TEXT, " + "remark TEXT)"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 低版本直接重建,实际项目可在这里做迁移 db.execSQL("DROP TABLE IF EXISTS person"); onCreate(db); } }这段代码里DB_VERSION = 1是初始版本,后面如果要加字段,不要改这个数,而是改成2并重写onUpgrade里的ALTER逻辑。onCreate只在数据库文件第一次生成时执行,你的所有建表语句都必须放在这里。getWritableDatabase和getReadableDatabase的调用则统一放在DAO层,不在Activity里直接执行SQL。
2.3 DAO层实现:五个基础方法加一个模糊搜索
DAO是连接SQLite和业务逻辑的桥,我习惯把增删改查全部封装成独立方法。下面这个PersonDao实现了项目中最核心的五个操作:插入人员、按ID删除、全量更新、查询全部、按姓名或手机号模糊搜索。
public class PersonDao { private final DatabaseHelper helper; public PersonDao(Context context) { helper = new DatabaseHelper(context); } public long addPerson(Person person) { SQLiteDatabase db = helper.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("name", person.getName()); cv.put("phone", person.getPhone()); cv.put("department", person.getDepartment()); cv.put("position", person.getPosition()); cv.put("remark", person.getRemark()); long id = db.insert("person", null, cv); db.close(); return id; } public int deleteById(int id) { SQLiteDatabase db = helper.getWritableDatabase(); int rows = db.delete("person", "id=?", new String[]{String.valueOf(id)}); db.close(); return rows; } public int updatePerson(Person person) { SQLiteDatabase db = helper.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("name", person.getName()); cv.put("phone", person.getPhone()); cv.put("department", person.getDepartment()); cv.put("position", person.getPosition()); cv.put("remark", person.getRemark()); int rows = db.update("person", cv, "id=?", new String[]{String.valueOf(person.getId())}); db.close(); return rows; } public List<Person> queryAll() { SQLiteDatabase db = helper.getReadableDatabase(); List<Person> list = new ArrayList<>(); Cursor cursor = db.rawQuery("SELECT * FROM person ORDER BY id DESC", null); while (cursor.moveToNext()) { Person p = new Person(); p.setId(cursor.getInt(cursor.getColumnIndexOrThrow("id"))); p.setName(cursor.getString(cursor.getColumnIndexOrThrow("name"))); p.setPhone(cursor.getString(cursor.getColumnIndexOrThrow("phone"))); p.setDepartment(cursor.getString(cursor.getColumnIndexOrThrow("department"))); p.setPosition(cursor.getString(cursor.getColumnIndexOrThrow("position"))); p.setRemark(cursor.getString(cursor.getColumnIndexOrThrow("remark"))); list.add(p); } cursor.close(); db.close(); return list; } public List<Person> searchPersons(String keyword) { SQLiteDatabase db = helper.getReadableDatabase(); List<Person> list = new ArrayList<>(); String sql = "SELECT * FROM person WHERE name LIKE ? OR phone LIKE ? ORDER BY id DESC"; String like = "%" + keyword + "%"; Cursor cursor = db.rawQuery(sql, new String[]{like, like}); while (cursor.moveToNext()) { // 与queryAll中相同的读取逻辑 } cursor.close(); db.close(); return list; } }逻辑说明:addPerson使用ContentValues传参,避免了字符串拼接SQL注入;deleteById和updatePerson的WHERE条件用?占位符,这是Android SQLite操作的安全习惯。searchPersons里LIKE ?配合%keyword%实现模糊匹配,同时覆盖姓名和手机号两个维度。每个方法结束后一定要close,SQLiteDatabase的连接数很有限,不关连接在多次操作后会直接报“数据库被锁定”的运行时异常。
2.4 数据库文件去哪了:路径和调试入口
模拟器或真机上这个库文件实际存储在/data/data/你的包名/databases/person_manager.db。调试阶段可以开启Android Studio的Device File Explorer直接拉出来看,也可以用命令行sqlite3工具打开检查表结构。实际项目中这个目录需要root权限才能访问,但Android Studio的调试器不受限制。这是在开发期验证数据是否真的写进去了的最快手段,不用依赖打印日志。
3. UI交互层:RecyclerView列表与Activity间数据传递
3.1 列表展示:RecyclerView比ListView更合适
旧式写法多用ListView配ArrayAdapter,但到了Android Studio的新版本模板,RecyclerView已经是市场主流。人员列表这种动态刷新频繁、删除后需要局部更新的场景,RecyclerView配合自定义Adapter的体验好很多。
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.PersonViewHolder> { private List<Person> dataset = new ArrayList<>(); @NonNull @Override public PersonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_person, parent, false); return new PersonViewHolder(view); } @Override public void onBindViewHolder(@NonNull PersonViewHolder holder, int position) { Person person = dataset.get(position); holder.nameText.setText(person.getName()); holder.phoneText.setText(person.getPhone()); holder.deptText.setText(person.getDepartment()); holder.itemView.setOnClickListener(v -> { if (listener != null) { listener.onItemClick(person); } }); } @Override public int getItemCount() { return dataset.size(); } public void setData(List<Person> newData) { dataset.clear(); dataset.addAll(newData); notifyDataSetChanged(); } static class PersonViewHolder extends RecyclerView.ViewHolder { TextView nameText, phoneText, deptText; PersonViewHolder(View itemView) { super(itemView); nameText = itemView.findViewById(R.id.person_name); phoneText = itemView.findViewById(R.id.person_phone); deptText = itemView.findViewById(R.id.person_dept); } } }这里的setData方法先清理再整体填充,配合notifyDataSetChanged是最不容易出错的刷新方式。局部刷新notifyItemInserted虽然效率高,但新人阶段很容易因为位置计算错误导致界面闪现脏数据。列表点击回调onItemClick在Activity里统一处理跳转,不要在ViewHolder内部直接startActivity。
3.2 新增与编辑:同一个Activity复用两种模式
新增人员界面和编辑人员界面在课程设计里往往是同一个布局文件,区别只在Intent里多传了一个人员ID。常规做法是在跳转时用Intent.putExtra("person_id", id),新增时传null或-1。编辑器里做判断:如果传了ID,就调用dao的queryAll或单独queryById回填表单;没有就是空白表单。
// 在MainActivity中启动编辑页 Intent intent = new Intent(this, EditActivity.class); intent.putExtra("person_id", person.getId()); startActivity(intent); // 在EditActivity的onCreate中 int id = getIntent().getIntExtra("person_id", -1); if (id != -1) { dao = new PersonDao(this); Person person = dao.findPersonById(id); // 回填姓名、电话、部门等输入框 }表单保存时先校验必填项,再判断id是否为-1决定调用addPerson还是updatePerson。这种“一个页面处理新增和编辑”的模式,在答辩时要多说一句设计理由:减少代码重复,新增和编辑的界面结构天然一致。
3.3 搜索栏:实时搜索与防抖
人员管理系统的搜索框建议放在列表Activity顶部,用EditText监听文本变化,每变化一次就调一次dao.searchPersons。性能上SQLite对本地小数据量查询毫秒级完成,不需要做防抖,但如果后续数据量上万,要考虑加一个Handler延时300毫秒再查。搜索后拿返回的List直接调adapter.setData,列表就会自动收窄。注意SearchView在这里不是必要的,普通EditText加两个属性就够了。
4. 五个低频却致命的坑:版本升级、校验与列表刷新边界
4.1 现象:改了表结构但新字段一直报“no such column”
原因:SQLiteOpenHelper的onUpgrade没有触发,因为数据库文件已经存在,版本号也没升级。很多人只会建表不会改表,遇到这种情况的第一反应是卸载App,但答辩时不能这么干。 解决:把DB_VERSION从1改成2,在onUpgrade里写ALTER TABLE语句:
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2) { db.execSQL("ALTER TABLE person ADD COLUMN age INTEGER DEFAULT 0"); } if (oldVersion < 3) { db.execSQL("ALTER TABLE person ADD COLUMN email TEXT"); } }每一次版本迁移单独写一个if分支,不要合并成一条命令,这样历史版本升级路径才可控。
4.2 现象:输入了汉字姓名,点击搜索后结果为空
原因:EditText拿到的字符串包含不可见空格,常见于复制粘贴或输入法自动补全。LIKE查询时前面的空格被当成关键字的一部分。 解决:所有搜索关键字统一做trim处理,代码写成String keyword = editText.getText().toString().trim()。保存数据时姓名和手机号同样做trim。这条在真机演示时特别容易翻车,务必处理。
4.3 现象:删除最后一条数据后列表少了一行,但再次删除又报数组越界
原因:删除后直接调notifyDataSetChanged还好,但如果你用了notifyItemRemoved(position),而这个position是基于删除前的旧数据集计算的,在连续删除时会拿到越界的索引。 解决:删除后重新查一次数据库,再调adapter.setData(newList)。不要手动维护列表里的Item位置。正确姿势是:数据库删除成功 → 重新queryAll → setData刷新。
4.4 现象:EditText只判断isEmpty,输入全角空格仍然通过验证
原因:中英文空格、全角空格在isEmpty判断下都认为有内容,但保存后看起来像没填一样。 解决:校验用TextUtils.isEmpty(phone)结合phone.trim().length() == 0。再进一步,手机号字段用正则校验,11位数字且以1开头,我会写一个简单校验方法:
public static boolean isValidPhone(String phone) { String regex = "^1[3-9]\\d{9}$"; return Pattern.matches(regex, phone); }格式不对时在输入框setError直接提示,不要在保存时才弹Toast。
4.5 现象:数据库“locked”异常,多出现在快速滑动列表时
原因:每次查询都new一个DAO或者每次都拿getWritableDatabase,SQLite单写多读,但同一时刻写操作冲突会抛异常。常见于主线程频繁开数据库连接同时还有一个异步任务在写。 解决:项目内统一一个DAO实例,查询用getReadableDatabase,写操作用getWritableDatabase,用完后立刻close。如果后续需要优化,可以线程加锁,但课程设计阶段做到“所有数据库访问走同一DAO”就足够了。
5. 导出与扩展:让人员管理系统的演示更完整
这个章节落到实处是“版本升级与扩展能力”。本地数据库也有生命周期,一块SQLite数据库文件可以导出,换了设备也能恢复,这在答辩中是一个充分体现工程意识的加分项。
5.1 导出CSV文件:一行代码备份人员数据
SQLite的备份最简单的方式不是拷贝db文件,而是把内容导出成CSV,方便用Excel打开。工具类里写一个export方法,遍历DAO查出的List写入文本文件:
public static boolean exportCsv(Context context, List<Person> list, String filePath) { File file = new File(filePath); try (FileOutputStream fos = new FileOutputStream(file); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { osw.write("\uFEFF"); // 写入BOM,防止Excel打开中文乱码 osw.write("姓名,手机号,部门,岗位,备注\n"); for (Person p : list) { osw.write(String.format("%s,%s,%s,%s,%s\n", p.getName(), p.getPhone(), p.getDepartment(), p.getPosition(), p.getRemark())); } osw.flush(); return true; } catch (IOException e) { e.printStackTrace(); return false; } }那段\uFEFF在很多教程里是隐形的,没有它Excel双击打开CSV时中文会全变乱码,这是我实际踩过的坑。文件路径在Android 10以上要注意分区存储规则,简单做法是写到getExternalFilesDir(null)下面,不会触发权限问题。
5.2 数据恢复的思路
导入CSV解析文本然后逐条插入person表,相比直接拷贝db文件更安全,因为拷贝db文件常因为版本不一致或数据库被占用而失败。如果确实要拷贝,先调用SQLiteDatabase的close方法,再从/data/data路径读取。这个功能在课程设计的“系统扩展”部分提一下即可,不需要做得太重。
5.3 按部门折叠展示:一个查询搞定分组效果
扩展能力里效果最明显的改动,不是加第三个Activity,而是把已有的查询加一个GROUP BY部门的分组列表。RecyclerView里用getItemViewType区分“部门头”和“人员行”两种视图,Adapter内部的viewType逻辑:
@Override public int getItemViewType(int position) { return dataList.get(position).isHeader() ? TYPE_HEADER : TYPE_ITEM; }这个实现思路的价值在于说明你对RecyclerView的Adapter机制有深度理解,而不仅仅是套用模板。写到这里想起一个习惯:从那以后我每次改表结构,都会先在升级脚本里跑一遍从旧到新版本的全路径,再回来动数据访问代码。早期做某图像处理Demo时,直接改了字段忘了升版本号,害得导师演示时当场报错,这份尴尬让我养成了“先管数据库版本、再写查询逻辑”的习惯。希望帮到你。
本文还有配套的精品资源,点击获取