Android简易播放器开发实战:MediaPlayer、Service与音频焦点全解析

发布时间:2026/10/19 4:15:09

Android简易播放器开发实战:MediaPlayer、Service与音频焦点全解析 简介面向安卓移动开发初学者的简易音乐播放器完整工程覆盖登录页面、音乐列表展示、播放控制、进度同步与后台通知栏控制等典型功能模块。资源包共 68 个文件压缩后 3.22 MB主要包括 Java 源码、XML 布局与配置、APK 安装包、安卓资源文件及少量 JAR 依赖库编译生成的 class 文件、工程配置文件均保留便于直接导入开发环境进行调试与二次开发。目前已有 264 人学习下载。借助该资源可系统掌握本地音乐扫描、权限申请、列表组件复用、播放状态保存与恢复等关键知识点同时通过自带安装包快速验证功能效果是入门安卓多媒体开发的实用参考。1. Android简易播放器别小看这个练手项目服务、焦点、通知全在一条链路上这个android 简易播放器项目我第一次拆的时候以为只是个“MediaPlayer RecyclerView”的练手 Demo实际跑起来才发现能稳定播完一首歌背后要绕的弯比想象中多得多后台保活要用前台服务通知栏不能黑块来电时音频焦点怎么让位Android 13 以后本地音乐权限体系整个换了还有进度记忆这种容易被忽略的小功能。项目功能虽然简单但把移动开发里播放器这条主链路完整串了一遍MediaStore 扫歌、Binder 连接 Service、音频焦点抢占、通知栏同步、进度存档。适合三类人刚学完四大组件想练 Service 的、要做本地音频扫描的、以及打算把通知栏控制做成标配的音频类应用开发者。2. 技术选型MediaPlayer、前台Service与权限适配的取舍2.1 MediaPlayer 够用吗简易项目的选型边界只播放本地 mp3、wav、aac 这类常规编码MediaPlayer 完全够用。它是系统组件底层走 AudioFlinger不占应用包体积也不需要像 ExoPlayer 那样引入大模块。ExoPlayer 的优势在 HLS/DASH 流媒体、字幕、变速、自定义渲染管线对“扫本地歌单—播放—记忆进度”这个场景来说就是杀鸡用牛刀。MediaPlayer 真正难用的不是 API 本身而是它那张严格的状态机。Idle、Initialized、Prepared、Started、Paused、PlaybackCompleted、Error 之间必须按顺序转移连续快速切歌时如果忘了 reset 直接 setDataSource很容易抛 IllegalStateException。这个项目把 MediaPlayer 的生命周期放在 Service 里而不是 Activity 里是挺明智的做法——屏幕旋转、Activity 重建都不会打断播放。很多教程习惯在 Activity 里 new MediaPlayer、setDataSource、start一退出页面音乐就没了。这个项目做了一个正确示范播放器的宿主是 Service界面只是通过 Binder 发指令的遥控器。这也是我拆项目时最先注意到的一个点它没有把播放状态放在界面层。2.2 前台服务 Binder后台播放的最小骨架要让音乐在 App 退到后台后继续响Service 必须以前台服务的方式运行。项目里 MainActivity 同时做了两件事startForegroundService() 和 bindService()。前者给服务一个常驻前提后者给 Activity 一条双向通信管道。Intent intent new Intent(this, PlayerService.class); startForegroundService(intent); // 先启动保证后台存活 bindService(intent, connection, Context.BIND_AUTO_CREATE); // 再绑定拿操作句柄这里的逻辑是只绑不启进程在 Activity 销毁后可能被回收只启不绑Activity 拿不到 LocalBinder 对象没法调用 play。两个要配合使用。绑定是异步的ServiceConnection.onServiceConnected 回调之前不要点播放按钮否则拿到的 mService 还是 null。2.3 Android 13 前后的权限差异一次适配两个世界这个项目的权限适配坑了不少人。Android 12 及以下读本地音乐用 READ_EXTERNAL_STORAGEAndroid 13 开始被拆分成细粒度媒体权限读音频必须用 READ_MEDIA_AUDIO。如果只声明旧权限在 Android 13 手机上列表永远是空的。目标版本需要声明的权限作用Android 12 及以下READ_EXTERNAL_STORAGE读取本地媒体文件Android 13 及以上READ_MEDIA_AUDIO仅读取音频文件Android 13 及以上POST_NOTIFICATIONS通知栏展示、前台服务通知Android 14 及以上FOREGROUND_SERVICE FOREGROUND_SERVICE_MEDIA_PLAYBACK启动媒体播放类型前台服务Manifest 里把新旧权限都声明代码里按版本分支申请if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { requestPermissions(new String[]{ Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.POST_NOTIFICATIONS}, PERMISSION_REQ); } else { requestPermissions(new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQ); }还有一个容易漏的配置targetSdk 34 时Service 标签里要加 android:foregroundServiceTypemediaPlayback并且 Manifest 里声明 FOREGROUND_SERVICE_MEDIA_PLAYBACK 权限否则前台服务启动时会直接 SecurityException。我一般会在 AndroidManifest 的 application 节点下把这一组权限和 service 属性一起核对一遍这属于“漏了不会编译报错、跑了才崩”的隐蔽问题。3. 核心链路拆解从点击歌单到声音从扬声器传出3.1 一次播放请求从 Activity 到 MediaPlayer 的完整路径列表点击事件的完整链路是这样的用户在 RecyclerView 里点某一首 → MusicAdapter 的 OnSongClickListener 回调触发 → MainActivity 拿到 Song 对象 → 调用 mService.play(song) → PlayerService 里先向 AudioManager 申请音频焦点 → 焦点拿到后 reset 旧播放器 → 用 ContentUri 做 setDataSource → prepareAsync() 异步准备 → onPrepared 回调里 start() → 通知栏更新标题。Override public void onSongClick(Song song, int position) { if (mService null) { Toast.makeText(this, 服务还没连接稍等一下, Toast.LENGTH_SHORT).show(); return; } mService.play(song); // position 暂时没用到后面做播放队列可以直接取下标 }这里有个关键细节setDataSource 用的是 ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId) 生成的 uri而不是把 MediaStore 查出来的 DATA 字段当文件路径传进去。Android 10 之后分区存储收紧直接用文件路径访问外部存储很容易 Permission DeniedContentUri 是 MediaStore 授权给应用的合法通道MediaPlayer 天然支持。这个选择给以后适配高版本系统省了很多事。3.2 MusicService 的 onBind 与 ServiceConnection 设计服务端用 LocalBinder 是本进程绑定的标准做法代码量最少public class PlayerService extends Service { private final IBinder binder new LocalBinder(); public class LocalBinder extends Binder { public PlayerService getService() { return PlayerService.this; } } Override public IBinder onBind(Intent intent) { return binder; } public void play(Song song) { if (!audioFocusManager.requestFocus()) { return; // 焦点没拿到先不播 } releasePlayer(); try { player.setDataSource(getApplicationContext(), ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, song.getId())); player.prepareAsync(); } catch (IOException e) { Log.e(TAG, setDataSource failed: song.getTitle(), e); } } }LocalBinder 这种方式只适合同进程服务端和 Activity 在同一个 APK 里直接强转 IBinder 就行。如果以后要做成跨进程的播放服务就得换 Messenger 或者 AIDL但简易播放器项目没必要。Activity 侧在 ServiceConnection.onServiceConnected 里把返回的 binder 转成 PlayerService 保存到成员变量之后所有操作都走 mService.play()、mService.pause() 这套方法。3.3 AudioFocus来电时音乐该停还是该降音量不处理音频焦点来电时音乐和铃声会同时响。Android 8 以后推荐用 AudioFocusRequest.Builder 来申请焦点AudioFocusRequest focusRequest new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()) .setOnAudioFocusChangeListener(focusListener) .build(); int result audioManager.requestAudioFocus(focusRequest);焦点回调里要分三种情况处理AUDIOFOCUS_LOSS 表示永久失去焦点此时应该暂停并 abandonFocusAUDIOFOCUS_LOSS_TRANSIENT 是临时失去比如来电暂停即可AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 是短暂让位但允许降低音量继续播放这时候把音量调低而不是暂停。private final AudioManager.OnAudioFocusChangeListener focusListener change - { switch (change) { case AudioManager.AUDIOFOCUS_LOSS: pause(); abandonFocus(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: pause(); break; case AudioManager.AUDIOFOCUS_GAIN: resume(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // 压低音量继续播系统回调给的是一个 0~1 的系数 break; } };注意 Android 8 以下用老接口 requestAudioFocus(listener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)项目里需要做一次版本分支。焦点回调的宿主应该是 Service 或全局单例放进 Activity 的话Activity 一回收回调就失效了这是很典型的隐性 bug。3.4 通知栏与锁屏控制进度条和播放状态怎么同步前台服务通知必须走 NotificationChannelAndroid 8 以后不建 channel 直接 notify 会静默失败。媒体播放的通知建议把 importance 设为 IMPORTANCE_LOW避免播放音乐时通知还响一声。NotificationManager nm getSystemService(NotificationManager.class); NotificationChannel channel new NotificationChannel( CHANNEL_ID, Playback, NotificationManager.IMPORTANCE_LOW); nm.createNotificationChannel(channel); Notification notification new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification_music) .setContentTitle(song.getTitle()) .setContentText(song.getArtist()) .setOngoing(true) .setShowWhen(false) .build(); startForeground(1001, notification);播放进度的实时刷新常见做法是 Service 里用一个 Handler 每隔 500 毫秒调一次 notify() 更新进度条但频繁重建 Notification 对象开销不小。我一般会先把 Notification 构建好setProgress 的调用放在同一实例上更新减少系统 UI 刷新压力。锁屏控制则需要 MediaSessionCompat 配合 MediaStyle简易播放器可以不先做但改造方向要预留。4. 关键代码实读MediaStore扫描、Binder通信与进度记忆4.1 项目目录结构每个类只干一件事app/src/main/ java/com/example/player/ MainActivity.java // 列表页 底部控制条 权限申请 服务绑定 PlayerService.java // 播放核心MediaPlayer、前台服务、Binder 管道 Song.java // 歌曲实体id、标题、歌手、时长 MusicLoader.java // MediaStore 扫描返回 ListSong MusicAdapter.java // RecyclerView 适配器点击回调 AudioFocusManager.java // 音频焦点申请与释放封装 ProgressKeeper.java // SharedPreferences 进度记忆 res/layout/ activity_main.xml // RecyclerView 底部控制栏 item_song.xml // 歌名 歌手 时长 AndroidManifest.xml这个结构的好处是职责分离每个类都能单独测试。最容易写乱的是 MusicLoader 和 MainActivity扫歌逻辑如果直接塞在 Activity 的 onCreate 里后面想做“刷新歌单”“搜索过滤”就要大改。独立成 Loader 类以后扫描结果可以直接喂给任意界面。4.2 MediaStore 扫描查歌曲列表的完整写法MusicLoader 用 ContentResolver 查询系统媒体库这是读取本地音乐的标准入口public static ListSong scan(ContentResolver resolver) { ListSong result new ArrayList(); Uri collection MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String[] projection new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.DURATION }; String selection MediaStore.Audio.Media.IS_MUSIC ! 0; String sortOrder MediaStore.Audio.Media.TITLE COLLATE NOCASE ASC; try (Cursor c resolver.query(collection, projection, selection, null, sortOrder)) { int idCol c.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); int titleCol c.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); int artistCol c.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); int durationCol c.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); while (c.moveToNext()) { Song song new Song(); song.setId(c.getLong(idCol)); song.setTitle(c.getString(titleCol)); song.setArtist(c.getString(artistCol)); song.setDuration(c.getInt(durationCol)); result.add(song); } } return result; }selection 里 IS_MUSIC ! 0 会把铃声、通知音排除掉避免列表混进乱七八糟的音频文件。sortOrder 用了 COLLATE NOCASE这样英文歌名排序时大小写不分离中文标题的排序表现也更正常。这个查询一定要放到子线程执行Cursor 遍历在歌曲数量大时会有明显卡顿我一般用线程池跑完再通过 Handler 回主线程 submit 到 Adapter。4.3 MusicAdapter 与列表点击把数据源和控件解耦Adapter 不直接持有 Activity只负责把 Song 数据渲染到 item 上点击行为通过接口回调抛出去public class MusicAdapter extends RecyclerView.AdapterMusicAdapter.VH { private ListSong songs new ArrayList(); private OnSongClickListener listener; public interface OnSongClickListener { void onSongClick(Song song, int position); } public void submit(ListSong list) { songs list ! null ? list : new ArrayList(); notifyDataSetChanged(); // 数据量小时够用大了可以换 DiffUtil } Override public void onBindViewHolder(VH holder, int position) { Song song songs.get(position); holder.title.setText(song.getTitle()); holder.artist.setText(song.getArtist()); holder.duration.setText(formatDuration(song.getDuration())); holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onSongClick(song, holder.getBindingAdapterPosition()); } }); } }这里要注意用 getBindingAdapterPosition() 而不是已经废弃的 getPosition()否则在列表插入删除后点击容易取到错位下标。submit 方法在数据源更新时会触发全量刷新对简易播放器足够如果列表以后要做增量更新再考虑换 DiffUtil。4.4 SharedPreferences 记录播放进度下次打开不用从头听进度记忆是本项目很实用的一个小功能。我一般会单独封装一个 ProgressKeeper而不是散落在 Activity 里public class ProgressKeeper { private final SharedPreferences sp; public ProgressKeeper(Context context) { sp context.getSharedPreferences(player_status, Context.MODE_PRIVATE); } public void save(long songId, int positionMs) { sp.edit() .putLong(last_song_id, songId) .putInt(last_position, positionMs) .apply(); } public long getLastSongId() { return sp.getLong(last_song_id, -1L); } public int getLastPosition() { return sp.getInt(last_position, 0); } }save 的时机要选对onPause 暂停、播放完成、Service onDestroy 时都会写一次。apply() 是异步落盘不阻塞 UI 线程。恢复进度的正确姿势是等 MediaPlayer 的 onPrepared 回调触发后再 seekTo否则在 prepare 之前 seek 会被状态机拒绝这就是很多人进度恢复“写了等于没写”的原因。5. 避坑笔记权限、服务、焦点与通知的四类翻车现场5.1 前台服务启动即崩溃现象第一次从 MainActivity 进入表现正常但杀掉进程后从最近任务恢复或者在某些国产 ROM 上重启应用时Logcat 直接报 “Context.startForegroundService() did not then call Service.startForeground()”紧接着 Service 被系统杀死。原因startForegroundService 启动的服务必须在创建后的短时间窗口内调用 startForeground()。如果 Service 在 onStartCommand 里先做了耗时初始化再慢慢创建通知窗口期一过系统就认定违规。解决把 NotificationChannel 和通知对象的构建放在 onCreate 里onStartCommand 的第一个动作就执行 startForeground(NOTIFICATION_ID, notification)。真正的业务逻辑放到子线程跑。我重构时直接把 startForeground 提到了 onStartCommand 第一行问题立刻消失。5.2 通知栏图标变成灰色方块现象通知能弹出来但小图标显示成一坨纯色块或者干脆是透明的空洞特别丑。原因通知栏小图标 setSmallIcon 要求传入纯 alpha 位图系统会把图标按单色模板渲染。如果用了带彩色背景的 png系统只会保留透明度信息其余全部涂成系统主题色看起来就是一个色块。解决把图标资源换成 24x24 的纯白单通道 png 或者矢量 drawable。矢量图在通知栏的表现通常更稳定我习惯直接把 ic_notification_music 做成 VectorDrawable省去多分辨率适配。5.3 快速点击播放按钮时偶发空指针现象App 刚启动用户马上点列表里的某首歌程序直接 NPELogcat 指向 mService.play() 这一行。原因bindService 是异步流程ServiceConnection.onServiceConnected 回调需要时间而用户点击事件可能比回调来得更早此时 mService 还是 null。解决界面上加一个“服务未就绪”的状态onServiceConnected 回调里再放开播放按钮。也可以在下发指令前判空像前面代码里那样 Toast 提示“服务还没连接稍等一下”。这两招我都用了第一个改善体验第二个兜底防崩溃。5.4 AudioFocus 写了但来电时音乐没反应现象明明在代码里调了 requestAudioFocus但来电话时音乐照响不误或者电话挂断后音乐不会自动恢复。原因最常见的是焦点监听器写在 Activity 里Activity 按 Home 键退到后台、被系统回收后监听器跟着失效。另一个原因是只调了 request 没有检查返回值焦点申请失败也照样执行 player.start()。解决把音频焦点逻辑封装成 AudioFocusManager 单例生命周期对齐 Service在 Service onCreate 时申请onDestroy 时 abandon。播放前先判断 request 结果等于 AUDIOFOCUS_REQUEST_GRANTED 才继续这属于正规军做法。5.5 Android 13 手机上扫不到任何歌曲现象用户升级到 Android 13 之后打开应用列表一直空白旧版本上却正常。原因targetSdk 升级到 33 以后系统不再把 READ_EXTERNAL_STORAGE 当作音乐读取权限必须申请 READ_MEDIA_AUDIO。如果 Manifest 里只声明了旧权限应用在 13 上拿不到任何音频文件的读取能力。解决Manifest 同时声明新旧权限代码按版本分支申请。还有一点要提醒用户拒绝权限两次后系统不会再弹窗必须跳转到 App 设置页手动开启。这个坑我吃过后面在权限被拒的提示框里加了一个 Settings 跳转动作问题才彻底解决。6. 二次开发把简易播放器改造成带播放队列的播放器拿到这个源码后最推荐做的第一项改造是加播放队列。现在的接口是 play(Song song) 单曲播放改成 play(List queue, int startIndex)Service 内部维护 currentIndex 和循环模式所有播放入口都收敛到一个方法里。public void switchTo(int index) { if (queue null || index 0 || index queue.size()) return; currentIndex index; player.reset(); player.setDataSource(context, queue.get(index).contentUri()); player.prepareAsync(); // onPrepared 里再 start并更新通知栏标题 }切歌逻辑只有这一处还不够通知栏的“下一首”按钮、锁屏 MediaSession 的 “next” 指令、列表点击事件最终都要调到 switchTo 方法。我改造时专门加了一个统一的 TAGPlaybackFlow 日志每个入口进出都打点验证时手动走一遍“列表点歌 → 通知栏下一首 → 锁屏控制 → 来电打断 → 挂断恢复”这条链路日志能清晰看出流程是否一致。验证方法很简单打开 Android Studio 的 Logcat 过滤 PlaybackFlow依次触发上述五个场景。如果在通知栏点了下一首但日志显示走的是旧接口而没有经过 switchTo说明入口没收敛干净还要继续改。从那以后我每次改播放器都会强制走一遍这五步链路再收工这个习惯帮我拦下了不少“看起来能跑、实际上状态不同步”的问题。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/19 4:15:09

open-code-review:基于CLI的本地化代码评审新范式

1. “open-code-review”不是工具名,而是正在发生的协作范式迁移你最近在技术社区、GitHub讨论区、甚至团队内部文档里反复看到“open-code-review”这个词——它不像npm install那样是个可执行命令,也不像VS Code插件那样点几下就能装上。它没有官网、没…

2026/10/19 4:15:09

个人介绍怎么写?从需求匹配到模板避坑,一份完整指南

"个人介绍"这四个字,大概是最常被写、最容易被写砸的一段文字。在简历里,它要出现在最前面;在社交主页上,它是陌生人认识你的第一屏;在项目的README里,它是用户判断"这人靠不靠谱"的依…

2026/10/19 5:30:16

MySQL日期函数DATE_ADD与DATE_SUB深度解析:语法、边界与索引优化

1. 为什么 DATE_ADD 和 DATE_SUB 值得单独拿出来写做后端开发的,几乎没有不跟日期时间打交道的。订单要算超时时间,会员要算到期日,报表要按天、按周、按月汇总,这些场景全绕不开一个操作——在某个时间点上加一段间隔&#xff0c…

2026/10/19 5:30:16

从互联网泡沫到AGI热潮:技术突破与资本炒作的分界线

我入行那一年,正好赶上上一轮科技周期的尾声。那时候互联网的故事讲得天花乱坠,仿佛谁不站上这趟车,谁就要被时代淘汰。结果泡沫破裂的时候,大家才发现,真正留下来的不是造概念最多的那批公司,而是把网络变…

2026/10/19 5:30:16

深入理解继承:从核心概念到实战避坑指南

1. 为什么继承这件事值得单独拎出来讲刚入行那会儿,我对继承的理解就停留在“子类 extends 父类,然后就能用父类的方法了”这个层面。直到有一次做一个订单系统的重构,我才真正意识到继承这件事远没有想象中那么简单。当时系统里有普通订单、…

2026/10/19 5:30:16

Spark+HBase地铁客流分析:从CSV到分时报表完整链路

简介:一套基于Spark的地铁大数据客流分析系统毕业设计源码包,面向计算机相关专业学生、大数据初学者及毕业设计选题者,围绕地铁客流统计、预测与调度优化场景展开。压缩包共194个文件,约42.6MB,按功能分为Java/Scala核…

2026/10/19 5:30:16

VS工具集实战:环境配置、脚本使用与工程模板生成指南

简介:VS平台工具集.zip是一份面向Visual Studio开发者的平台工具集整合包,用于补充和更新VS的构建与编译组件,适合需要在Windows环境开展.NET/C开发、自定义MSBuild构建流程或调试工具链的工程师。压缩包共收录2000个文件,以1158个…

2026/10/19 5:25:16

C++享元模式实战:从原理到线程安全实现与内存优化

1. 内存告警背后的元凶:上千种弹道特效的重复建模先说个我遇到过的真实场景。某客户端的战斗模块里有一批弹道特效,每个子弹发射后都会在场景里生成一条轨迹,轨迹上挂着一串光点数据。最初功能跑得好好的,但等特效种类从十几个涨到…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/19 0:04:56

悟空开源无代码平台实战:从实体建模到应用设计避坑指南

简介:基于悟空开源无代码平台的多种应用设计源码,面向希望在不编写代码的情况下快速搭建CRM、HRM、财务系统、ERP、SRM等业务应用的企业与技术人员,通过拖放式界面和内置模板即可完成应用定制与部署。压缩包共697个文件,约10.74MB…

2026/10/19 0:04:56

基于.NET9的多租户权限系统:数据权限与动态API落地实践

简介:这份资源是一套面向 .NET 与 Vue 全栈开发者的中台权限管理系统,后端基于 .Net9,前端采用 Vue3 Element Plus,并配套 uniapp uviewui 移动端方案,实现前后端分离。系统覆盖多租户隔离、数据权限、动态 Api、任务…

2026/10/19 0:04:56

场地预约小程序毕设实战:SSM+微信小程序+MySQL数据库设计到避坑

简介:面向毕业设计场景的场地预约小程序完整项目,基于微信小程序SSMMySql技术栈开发,适合计算机相关专业学生作为选题参考、功能扩展或二次开发起点。系统实现场地类型管理、场地管理、预约管理、取消申请与押金退回等功能,分为管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑