简介:基于Android的人才招聘平台设计PDF文档,是面向移动应用开发学习者、Android客户端程序员及计算机专业毕业生的专业参考文献。内容以期刊论文形式完整呈现人才招聘平台的设计方案,先从概述说明互联网招聘相对传统模式的优势,再从总体框架切入,围绕个人求职者与企业招聘方两类用户,逐一分析个人资料增删改查、简历管理、发布求职、投递简历、查看投递状态以及企业资料维护、招聘信息发布与修改等核心模块的设计思路,同时展示总体功能结构,为开发在线招聘类应用提供清晰的设计框架与流程参考。资源包内含1个PDF文件,大小1.72MB,排版规范、文字清晰,方便直接阅读或打印。已有58人学习,适合作为Android应用开发课程设计、毕业设计或求职招聘类项目的参考文献与专业指导资源。
1. 基于Android的人才招聘平台设计,先想清楚的三个问题
HR在微信里发来一句“简历发我下”,你从手机相册里翻出PDF版简历,却发现平台App上传失败;另一端,招聘专员在后台筛选了200份简历,却因为接口分页参数设计不合理,滑到第100条时直接卡死。基于Android的人才招聘平台设计,实际上面临三个基础问题:第一,招聘场景天然是双端多角色(求职者浏览职位、投递简历;招聘者发布职位、筛选简历、安排面试),客户端必须在这套角色模型之上设计;第二,简历、职位、投递记录是强结构化数据,本地缓存与弱网恢复直接影响留存;第三,附件文件访问、消息推送、面试日历提醒这类能力与Android系统机制强绑定,不能照搬Web端设计。这篇内容适合正在做招聘类App的Android工程师,也适合准备移动端系统设计面试的技术候选人。下文按业务建模、数据契约、核心功能落地、发布验证四个阶段展开,每一阶段都给出可执行的代码、参数和踩坑点。
2. 业务建模与Android端技术选型:先把招聘流程压缩成状态机
2.1 双端业务边界与模块划分:避免把角色塞进同一个Activity
招聘平台最常犯的设计错误,是把求职者端和招聘者端做成一个App里通过登录角色切换界面的形式。角色切换意味着两套业务状态、两套UI和两套接口都要耦合在同一个路由里,后期每加一个功能,回归成本翻倍。常见做法是单工程多模块,在构建期按角色维度拆开,代码层面共享核心模块,业务模块各自独立。
推荐的分层结构如下:
- :core:model:简历、职位、投递记录等纯数据模型
- :core:network:Retrofit与OkHttp封装,统一鉴权与解析
- :core:database:Room数据库,本地缓存与草稿箱
- :feature:login:登录注册,角色选择
- :feature:joblist:职位列表与搜索(求职者端)
- :feature:resume:简历编辑、解析与附件管理(求职者端)
- :feature:recruiter:职位发布、简历筛选、面试安排(招聘者端)
- :app:装配层,按flavor组合打包
模块化之后,Gradle配置里用flavor来区分双端。一个关键配置是buildConfigField,每个模块都要能感知当前构建的是哪一端:
android { flavorDimensions "role" productFlavors { candidate { dimension "role" applicationIdSuffix ".candidate" buildConfigField "String", "ROLE", "\"CANDIDATE\"" } recruiter { dimension "role" applicationIdSuffix ".recruiter" buildConfigField "String", "ROLE", "\"RECRUITER\"" } } }applicationIdSuffix的作用是让求职端和招聘端可以同时安装在同一台设备上,这对测试联调很重要。ROLE字段用于代码里控制Feature开关,例如职位列表页在求职端显示“投递”按钮,在招聘端显示“下线/编辑”按钮,用一份代码按不同角色渲染。Manifest合并时也要注意,:feature:recruiter里声明的Activity只会在recruiter flavor下被打进APK,Gradle的manifest merger会自动处理,不需要额外配置。
2.2 核心状态机定义:职位、投递与面试状态流转
招聘平台的业务核心是状态流转。如果不在一开始把状态机定义清楚,后续拿到服务端的状态值时,客户端会陷入到处when(status)判断的泥潭。建议把三个核心对象的状态设计成枚举,并写进core:model,这样求职端和招聘端引用同一份定义。
职位、投递、面试的推荐状态如下表:
| 对象 | 状态枚举 | 说明 |
|---|---|---|
| 职位 | DRAFT / PUBLISHED / OFFLINE / EXPIRED | 草稿、已发布、已下线、已过期 |
| 投递 | SUBMITTED / VIEWED / INTERVIEW / REJECTED / OFFER | 已投递、已查看、面试中、不合适、已录用 |
| 面试 | PENDING / CONFIRMED / COMPLETED / CANCELLED / ABSENT | 待确认、已确认、已完成、已取消、未出席 |
投递状态迁移时,客户端要根据前后状态差异刷新界面。举个例子,求职者看到“已查看”变成“面试中”,列表项要出现“面试时间”入口;招聘者在“已查看”状态下要展示“标记不合适”按钮。这个逻辑放在ViewModel里,用一个DeliveryStateMachine统一收口:
class DeliveryStateMachine { fun canTransition(current: DeliveryStatus, target: DeliveryStatus): Boolean { return when (current) { DeliveryStatus.SUBMITTED -> setOf( DeliveryStatus.VIEWED, DeliveryStatus.REJECTED ).contains(target) DeliveryStatus.VIEWED -> setOf( DeliveryStatus.INTERVIEW, DeliveryStatus.REJECTED ).contains(target) DeliveryStatus.INTERVIEW -> setOf( DeliveryStatus.OFFER, DeliveryStatus.REJECTED ).contains(target) else -> false } } }状态机不是额外抽象,而是防止UI按钮在非法状态下被点击。比如投递已经结束(OFFER/REJECTED),就不能再发起撤回操作。客户端每次点击“撤回投递”,先调canTransition(SUBMITTED, REJECTED)之类的判断,不行就弹Toast;服务端也会做同样的校验,两端状态不一致时以服务端为准。
2.3 技术栈选型:Kotlin + Jetpack Compose + MVVM + Repository
招聘平台这类的列表密集型应用,技术选型的核心是“列表渲染性能”和“业务状态可测试性”。我常用的组合是Kotlin + Jetpack Compose + MVVM + Repository。
Compose在列表场景下比XML布局有优势:LazyColumn的item组合函数天然支持key缓存,配合Paging 3可以做到滑动不卡顿;但要注意Compose的derivedStateOf和remember使用不当会带来多余重组,后续在实现章节细说。
架构上采用MVVM,ViewModel暴露StateFlow,UI只订阅状态不直接操作数据。Repository层屏蔽数据来源,网络和Room都从Reposity出入。
下面给出Repository层的接口定义,它解决了“数据从哪来”的问题:
interface JobRepository { fun pagedJobs( keyword: String, city: String? ): Flow<PagingData<JobItem>> fun cachedJobDetail(jobId: String): Flow<JobDetail?> suspend fun refreshJobDetail(jobId: String) } class JobRepositoryImpl( private val remote: JobApi, private val local: JobDao ) : JobRepository { override fun pagedJobs(keyword: String, city: String?): Flow<PagingData<JobItem>> { return Pager( config = PagingConfig(pageSize = 20, enablePlaceholders = false), pagingSourceFactory = { JobPagingSource(remote, local, keyword, city) } ).flow } }PagingConfig(pageSize = 20)是列表App的基础参数。enablePlaceholders = false关闭占位符,避免快速滑动时出现跳动。PageSize设为20能平衡首屏加载速度和流量消耗;大于30时弱网下首屏等待时间会明显拉长,小于10则滚动到底部时频繁触发加载,容易看到Loading。网络差时,客户端应优先展示Room里上一次缓存的数据,再静默刷新,这需要Room和PagingSource协同配合。
3. 数据模型与服务端接口约定:简历、职位、投递记录怎么建表
3.1 简历模型设计:结构化字段与附件URI分离
简历是招聘平台最核心的数据资产,设计上要把“结构化字段”和“附件文件”分开。结构化字段用于列表展示和搜索,附件文件用于查看原始内容。下面是一份可落地的ResumeEntity表结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | String | 简历ID,服务端生成 |
| user_id | String | 用户ID |
| name / phone / email | String | 联系方式,敏感字段加密后存储 |
| position / years | String / Int | 期望职位 / 工作年限 |
| education_json | String | 教育经历JSON数组 |
| experience_json | String | 工作经历JSON数组 |
| skill_tags | String | 技能标签,逗号分隔 |
| attachment_uri | String | 附件本地content:// URI,可为空 |
| updated_at | Long | 最后更新时间,时间戳毫秒 |
education_json和experience_json用JSON字符串存储,不单独建表,是因为教育和工作经历是简历的附属列表,极少被单独查询。单独拆三张表徒增关联查询成本,适合放在一起反范式存储。Room定义如下:
@Entity(tableName = "resume") data class ResumeEntity( @PrimaryKey val id: String, @ColumnInfo(name = "user_id") val userId: String, @ColumnInfo(name = "attachment_uri") val attachmentUri: String?, @ColumnInfo(name = "updated_at") val updatedAt: Long ) { val skills: List<String> get() = skillTags.split(",").filter { it.isNotBlank() } }注意attachment_uri存的是content://格式的URI字符串,不是文件绝对路径。Android 10开始的分区存储,应用访问其他应用共享的文件必须通过ContentResolver,直接存/storage/emulated/0/...这种绝对路径,在Android 11以上大概率读不到。这种路径在网络上被频繁搜索,比如content://com.tencent.wework.fileprovider/external_path/android/data/com...,本质上就是微信等应用通过FileProvider暴露文件时的授权URI,自己项目里不能按绝对路径来保存。
3.2 职位模型与筛选条件的表结构
职位表相对直接,但有两个字段要重点考虑:salary_min和salary_max用整数类型存储,单位K,不要用字符串“15K-20K”存,否则按薪资筛选时没办法比较。jd_content是长文本,列表接口里不返回,只返回摘要,详情接口再加载全量。
职位实体设计如下:
@Entity(tableName = "job") data class JobEntity( @PrimaryKey val jobId: String, val title: String, val companyName: String, val city: String, @ColumnInfo(name = "salary_min") val salaryMin: Int, @ColumnInfo(name = "salary_max") val salaryMax: Int, val tags: String, // "可远程,独角兽,周末双休" @ColumnInfo(name = "jd_content") val jdContent: String, val status: Int, // 状态枚举值 @ColumnInfo(name = "expire_at") val expireAt: Long )还有一个容易被忽略的字段:expireAt。职位过期后要停止在列表中展示,但已投递过的用户仍然需要看到该职位的历史记录。这里推荐的做法是列表查询时加条件WHERE expire_at > :now AND status = 1,详情接口则不做过滤,这样既保证列表干净,又不影响投递历史查看。
3.3 REST接口契约:分页、排序与错误码约定
服务端接口有两种风格可选:一是REST化资源接口,二是面向客户端场景的BFF接口。招聘平台建议选REST + 查询参数的方案,语义清晰,客户端也容易套Retrofit。核心接口如下:
| Method | Path | 关键参数 | 说明 |
|---|---|---|---|
| GET | /api/v1/jobs/search | keyword, city, salaryMin, page, pageSize, sort | 职位搜索分页 |
| GET | /api/v1/jobs/{jobId} | - | 职位详情 |
| POST | /api/v1/deliveries | jobId, resumeId | 投递简历 |
| GET | /api/v1/deliveries/mine | status, page, pageSize | 我投递的记录 |
| POST | /api/v1/resumes/parse | file(二进制) | 简历解析 |
| PUT | /api/v1/deliveries/{id}/status | status | 更新投递状态 |
分页参数统一约定page从1开始,pageSize默认20、最大50。响应体里必须返回hasMore字段,客户端据此判断是否还有下一页。这里有个容易被忽略的点:客户端下拉刷新时,服务端如果按page=1返回,会重复读取所有数据;建议刷新走since_id增量接口,或客户端在刷新请求里携带lastUpdatedAt参数,服务端只返回该时间戳之后变化的职位和状态。排序参数sort支持recommend(默认综合排序)、salary(新职级排序)、time(最新发布)。
错误码约定一套统一结构,例如:
{ "code": 40001, "message": "简历附件超出大小限制", "data": null }code为0表示成功,其他为失败。客户端根据code决定是否走特殊UI分支,不推荐用HTTP状态码直接做业务判断,因为服务端可能会返回200但业务code失败。
3.4 本地缓存策略与WorkManager同步机制
招聘App的典型使用场景在通勤路上、电梯里,弱网是常态。本地缓存分为两块:职位列表缓存和简历草稿缓存。职位列表缓存按页存储,下拉刷新后更新对应条目;简历草稿在用户每次编辑后直接插入或更新Room,网络恢复后再同步到服务端。
同步任务用WorkManager来实现,配合重试和退避策略:
class ResumeSyncWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val resumeDao = AppDatabase.get(context).resumeDao() val dirtyList = resumeDao.getDirtyResumes() if (dirtyList.isEmpty()) return Result.success() return try { dirtyList.forEach { resume -> api.uploadResume(resume.toRequest()) resumeDao.markSynced(resume.id) } Result.success() } catch (e: IOException) { Result.retry() } } }调度时设置约束条件和退避参数:
val syncRequest = OneTimeWorkRequestBuilder<ResumeSyncWorker>() .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() ) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) .build()退避策略中,LINEAR表示每次重试间隔线性递增:10秒、20秒、40秒,适合网络抖动恢复;EXPONENTIAL则翻倍增长,适合服务端过载场景。招聘平台的简历同步用LINEAR更合适,用户恢复网络后希望尽快看到“已同步”状态。同步失败后,UI上要显示“草稿未上传”的红点,点击后手动触发一次enqueue;如果连续失败超过3次,就停止自动重试,只保留手动入口,这是为了避免后台频繁唤醒消耗电量。
4. 职位列表、简历解析与消息推送:Android端核心功能的实现细节
4.1 职位列表分页与下拉刷新:Paging 3和状态管理
职位列表页是招聘App的门面,也是性能问题高发区。实现上采用Paging 3,自定义PagingSource从网络获取分页数据,配合RemoteMediator处理缓存,下沉到数据库。一个简化但稳定的方案:直接用PagingSource+ Room缓存,下拉刷新时清空缓存重新加载,避免脏数据残留。
关键的Compose列表实现:
@Composable fun JobListRoute( viewModel: JobListViewModel = viewModel() ) { val jobs = viewModel.jobPagingData.collectAsLazyPagingItems() LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp) ) { items( count = jobs.itemCount, key = { index -> jobs[index]?.jobId ?: index } ) { index -> val job = jobs[index] if (job != null) { JobCard(job) } else { PlaceholderCard() } } } }collectAsLazyPagingItems返回的是支持LazyColumn增量的数据流,key必须用职位ID而不能用index,否则职位状态变更(比如收藏)时,列表项会整体重建导致闪烁。列表底部加载状态用PagingDataAdapter的方式判断:当itemCount大于0且loadState.append为Loading时,在列表尾部渲染一个CircularProgressIndicator,同时保证列表第一屏加载时显示居中完整的ProgressBar。热搜里经常搜的“android进度条”,在招聘客户端里通常就这两个位置:首屏全屏Loading和底部加载更多,用LoadState区分,不要用同一个状态去渲染两处。
下拉刷新采用PullToRefreshBox(Compose Material 3):
PullToRefreshBox( isRefreshing = viewModel.isRefreshing, onRefresh = { viewModel.refresh() } ) { LazyColumn(...) }刷新时触发viewModel.refresh(),内部先清空Room缓存,再调用第一个page的网络请求。这里有一个经验参数:刷新超时设为10秒,超过则提示“网络开小差了”,但保留旧列表数据。旧数据清空会带来闪屏,不清空会带来“数据混乱”,一般会保留旧列表,等到新数据加载完成后一次性替换。做法是刷新请求里携带lastUpdatedAt,服务端返回增量,Room做upsert,然后PagingSource自动重新加载。
4.2 简历解析与附件加载:PDF转缩略图与FileProvider授权
简历解析是招聘平台的杀手级功能。移动端的常见做法是:先把PDF或Word文件上传到服务端,服务端解析出结构化文本,返回JSON字段;客户端本地只做两件事:附件下载缓存和解析状态轮询。
上传附件时要注意标准姿势:不要直接用contentResolver.openFileDescriptor读原始流,而是先读取文件大小、MIME类型,超过10MB的提前提示用户压缩。很多用户从微信/QQ保存的简历文件,content://URI的授权只在短期有效,超过一段时间(通常是2小时)再读取就会抛SecurityException。
推荐在进入编辑页时就把URI转成App私有目录的副本:
fun persistAttachment(context: Context, uri: Uri): Uri { val input = context.contentResolver.openInputStream(uri) ?: throw IOException() val dir = File(context.filesDir, "resume_attachments") if (!dir.exists()) dir.mkdirs() val target = File(dir, UUID.randomUUID().toString() + ".pdf") input.use { it.copyTo(target.outputStream()) } return FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", target) }FileProvider的authorities建议固定为${applicationId}.fileprovider,避免flavor切换后authority变化导致搜索不到。file_paths.xml配置:
<paths> <files-path name="resume_files" path="resume_attachments/" /> </paths>files-path指向context.filesDir下的子目录,不要把cache-path里的临时目录放进来,清理缓存时会导致正在编辑的简历附件丢失。对应日志里常见的content://com.tencent.mobileqq.sharefileprovide/external_files/android/d...这类路径,其实都是其他App的FileProvider暴露的授权URI,自己App里见到这种日志大多是跨App读取附件时grantUriPermission没有调,或调了但没有声明query权限。Android 13以上跨应用读取对方App的content URI,需要在onCreate里先获取ContentResolver的takePersistableUriPermission,否则随时可能失效。
4.3 消息推送通知栏设计与Channel参数
招聘平台最大的推送价值是“面试邀请”和“简历被查看”,这些属于高优通知。Android 8.0引入通知渠道后,必须为每条通知绑定Channel。要定义一个面试邀请专用的Channel:
| Channel属性 | 推荐值 | 说明 |
|---|---|---|
| id | interview_reminder | 客户端唯一标识 |
| importance | IMPORTANCE_HIGH | 弹窗+铃声+震动 |
| description | 面试安排与提醒 | 用户在系统设置里的可见说明 |
| lockscreenVisibility | VISIBILITY_PUBLIC | 锁屏展示摘要 |
通知时用NotificationCompat.Builder:
val channelId = "interview_reminder" val notification = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) .setContentTitle("面试提醒") .setContentText("明天10:00 字节跳动 Android开发岗视频面试") .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setContentIntent(pendingIntent) .build() NotificationManagerCompat.from(context).notify(1001, notification)推送本身会通过厂商通道到达客户端(如果用的是第三方推送服务),客户端收到推送后判断消息类型:如果是interview_reminder类型,就在Application层创建本地通知。注意不要为同一场面试重复创建通知,用interviewId作为notify()的id,重复到达时系统会更新已有通知而不是再叠加一条。面试的推送消息建议配置为“持续可见”,即用户从通知栏点击后进入面试详情页,此时setAutoCancel虽然把通知删除了,但面试日历视图里仍然要有未读红点,这属于服务端推送的ack回执逻辑,客户端要再上报一次确认动作。
4.4 面试日历提醒:WorkManager定时与时间参数
面试提醒是招聘平台的高频需求。做法是:用户确认面试时间后,客户端把startTime、timeZoneId、advanceMinutes传入WorkManager,做一次性延迟任务。需要注意WorkManager不保证在指定时刻精确触发,只会尽量在约束满足后的最早时间点执行;对面试提醒这种场景,提前5分钟和提前3分钟相差不大,误差可接受。
val delay = (startTime - System.currentTimeMillis()) - advanceMinutes * 60_000L val workRequest = OneTimeWorkRequestBuilder<InterviewReminderWorker>() .setInitialDelay(delay, TimeUnit.MILLISECONDS) .setInputData( workDataOf( "interview_id" to interviewId, "title" to companyName, "start_time" to startTime ) ) .build() WorkManager.getInstance(context).enqueue(workRequest)时区参数这里容易踩坑:面试时间是服务端返回的时间戳,在服务端时要转成客户端本地时区展示;如果面试双方跨时区,要使用timeZoneId字段,不要采用服务器默认时区。advanceMinutes按用户设置可以是5/10/30分钟,建议把5分钟设为默认值。WorkManager的setInitialDelay最小单位为毫秒,计算时注意延迟时间为负(已经过期)时直接触发通知。不要在InterviewReminderWorker.doWork()里做网络请求,Worker的存活时间不保证,网络请求失败会导致提醒直接丢失;正确做法是Worker里本地读库取面试信息,然后发本地通知。
4.5 常见崩溃与耗电排查:ADB定位问题
招聘App的崩溃主要集中在以下几个场景,用Android Debug Bridge(ADB)就能快速定位。
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| 白屏/ANR | 主线程IO操作 | adb shell am start -W 包名/Activity |
| 图片OOM | 缩略图加载过大 | adb shell dumpsys meminfo 包名 |
| 附件读取失败 | FileProvider权限问题 | adb logcat -s FileProvider |
| 列表卡顿 | DiffUtil没有设置key | adb shell screenrecord全屏录制分析帧率 |
| 电量耗电快 | 后台同步频繁重试 | adb shell dumpsys jobscheduler |
用adb logcat抓取崩溃日志时,推荐配合ActivityTaskManager和AndroidRuntime两个tag过滤,一次定位崩溃栈和Activity生命周期问题:
adb logcat -v threadtime -T 500 | grep -E "AndroidRuntime|ActivityTaskManager|FATAL"这里-T 500打印最近500条日志,threadtime显示线程和毫秒时间,方便对比用户操作时间点。招聘平台最常见的崩溃来源是图片列表缓存策略不对:职位Logo、公司封面图用Glide时,diskCacheStrategy要设成DATA而不是RESOURCE,否则弱网环境下图片会反复重新下载,导致列表滚动卡顿。
5. 发布前的构建配置与真机调试技巧
5.1 多环境配置与Android Studio构建参数
招聘平台通常要管理开发、测试、预发、生产四套环境。在build.gradle里统一配置,避免每个模块手改接口地址。debug和release的BuildConfig字段分离:
buildTypes { debug { buildConfigField "String", "API_BASE_URL", "\"https://dev-api.example.com/\"" buildConfigField "boolean", "LOG_ENABLED", "true" } release { buildConfigField "String", "API_BASE_URL", "\"https://api.example.com/\"" buildConfigField "boolean", "LOG_ENABLED", "false" minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile("proguard-android-optimize.txt") } }代码里用BuildConfig.API_BASE_URL初始化Retrofit。注意混淆配置:Retrofit和Room的数据模型需要保留、Gson/序列化相关的注解类不能混淆,否则release包解析JSON会抛空指针,debug包正常、release包异常,优先怀疑混淆规则。
5.2 用Android Debug Bridge在vivo手机等真机上无线调试
开发阶段频繁拔插USB线效率太低,Android 11以上可以用无线调试。以常见vivo手机为例,打开开发者选项里的“无线调试”,拿到IP:端口后:
# 首次连接使用USB线 adb devices adb tcpip 5555 # 拔掉USB,在同一Wi-Fi下连接 adb connect 192.168.1.100:5555 # 确认连接状态 adb devicesadb tcpip 5555让设备在5555端口上开启adb守护,adb connect指定设备IP和端口。如果连接成功但adb devices里显示offline,通常是开发者选项里的“USB安装”和“USB调试(安全设置)”没有同时开启,部分国产ROM还会额外要求登录系统账号。小米、vivo、OPPO的无线调试入口位置不同,vivo在设置-系统管理-开发者选项-无线调试,需要手动打开,命令行触发不了。
5.3 地图SDK的签名SHA1配置
招聘平台常需要在地图上展示职位位置,求职者看职位时能看到公司坐标。国内使用最常见的是高德地图Android SDK(热搜里大量出现“高德地图android离线包下载”,指的就是离线地图数据包需要单独在SDK里初始化配置)。高德SDK初始化时用AMapLocationClient.setApiKey(),这个Key与当前应用的SHA1签名绑定。获取签名信息:
keytool -list -v -keystore app.jks -alias release -storepass 你的密码输出中的SHA1字段就是高德控制台需要配置的值。注意debug和release使用不同的签名文件,需要分别申请两个Key。很多工程师在debug环境地图正常,一到release包就白屏,原因几乎都是控制台里只配置了debug的SHA1,忘记给release签名再申请一个Key。招聘平台这类App上线后启动定位时,还额外要检查Android 13的定位权限声明:ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION是危险权限,必须在代码里运行时申请,其中后台定位权限ACCESS_BACKGROUND_LOCATION如果App不需要求职者在后台上报位置,就不要申请,否则审核会被打回。
5.4 应用签名与版本号管理的最后一步
发布前最后一个检查点:签名文件和版本号。签名文件一旦丢失,应用无法升级覆盖安装,只能换包名重新上架,招聘平台如果已有用户,换包名会导致历史版本的地图Key和推送Channel全部失效。版本号versionCode是递增整数,上架后用脚本从Git Tag自动生成,避免人为疏忽导致同一个code重复。Android Studio里构建release包的常规入口是Build-Generate Signed APK,命令行方式为:
./gradlew assembleRelease生成的APK路径在app/build/outputs/apk/release/app-release.apk。上传应用市场前,用aapt dump badging检查包名和版本号:
aapt dump badging app-release.apk | grep -E "package|versionName|sdkVersion"aapt是Android SDK Build Tools里的工具,需要配置到环境变量ANDROID_HOME/build-tools/版本号/。多flavor构建时,assembleCandidateRelease对应求职端,assembleRecruiterRelease对应招聘端,别在流水线里打错包。
本文还有配套的精品资源,点击获取