当前位置: 首页 > news >正文

Android12.0 SIM卡语言自适应

文章目录

        • 需求
        • 语言设定
        • Settings中语言切换流程
        • 检测到SIM卡,更新系统语言
        • 最终修改

需求

要求系统语言跟随SIM卡的语言变化。

语言设定

(1)系统预置语言, 即在makefile中指定的语言
(2)重启, 如果未插卡, 则系统语言为预置的语言
(3)重启插入SIM卡开机, 会自适应为SIM卡的语言
(4)如果有手动设置语言, 以后开机, 不管插入的是哪个国家的卡, 都会显示设置的语言, 不会根据SIM卡自适应变化.

Settings中语言切换流程

当在系统设置中手动设置语言拖拽结束后,会调用updateLocalesWhenAnimationStops(ll)方法

  • vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java
	public void updateLocalesWhenAnimationStops(final LocaleList localeList) {if (localeList.equals(mLocalesToSetNext)) {return;}// This will only update the Settings application to make things feel more responsive,// the system will be updated later, when animation stopped.LocaleList.setDefault(localeList);mLocalesToSetNext = localeList;final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {@Overridepublic void onAnimationsFinished() {if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {// All animations finished, but the locale list did not changereturn;}// 语言条目发生改变,调用到framework下的LocalePicker进行更新LocalePicker.updateLocales(mLocalesToSetNext);mLocalesSetLast = mLocalesToSetNext;new ShortcutsUpdateTask(mContext).execute();mLocalesToSetNext = null;mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());}});}

然后调用LocalePicker的updateLocales()方法进行更新

  • frameworks/base/core/java/com/android/internal/app/LocalePicker.java
    /*** Requests the system to update the list of system locales.* Note that the system looks halted for a while during the Locale migration,* so the caller need to take care of it.*/@UnsupportedAppUsagepublic static void updateLocales(LocaleList locales) {if (locales != null) {locales = removeExcludedLocales(locales);}// Note: the empty list case is covered by Configuration.setLocales().try {final IActivityManager am = ActivityManager.getService();final Configuration config = am.getConfiguration();// 切换后的语言信息更新到Configurationconfig.setLocales(locales);config.userSetLocale = true; // 手动设置的标志am.updatePersistentConfigurationWithAttribution(config,ActivityThread.currentOpPackageName(), null);// Trigger the dirty bit for the Settings Provider.BackupManager.dataChanged("com.android.providers.settings");} catch (RemoteException e) {// Intentionally left blank}}

又转入到ActivityManagerService中处理

  • frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
	@Overridepublic void updatePersistentConfigurationWithAttribution(Configuration values,String callingPackage, String callingAttributionTag) {enforceCallingPermission(CHANGE_CONFIGURATION, "updatePersistentConfiguration()");enforceWriteSettingsPermission("updatePersistentConfiguration()", callingPackage,callingAttributionTag);if (values == null) {throw new NullPointerException("Configuration must not be null");}int userId = UserHandle.getCallingUserId();// 这里的mActivityTaskManager就是ActivityTaskManagerServicemActivityTaskManager.updatePersistentConfiguration(values, userId);}

继续传递到ActivityTaskManagerService中处理updateConfigurationLocked()

  • frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
	public void updatePersistentConfiguration(Configuration values, @UserIdInt int userId) {final long origId = Binder.clearCallingIdentity();try {synchronized (mGlobalLock) {// 配置发生改变(尺寸,字体),都会执行updateConfigurationLocked(values, null, false, true, userId,false /* deferResume */);}} finally {Binder.restoreCallingIdentity(origId);}}

在ActivityTaskManagerService内部经过一系列处理,最终执行到updateGlobalConfigurationLocked()

	int updateGlobalConfigurationLocked(@NonNull Configuration values, boolean initLocale,boolean persistent, int userId) {mTempConfig.setTo(getGlobalConfiguration());// 判断是否发生变化final int changes = mTempConfig.updateFrom(values);if (changes == 0) {return 0;}...if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) {// 这里的locales包含所有已添加的语言,如果是第一次开机就是系统默认语言[en_US]final LocaleList locales = values.getLocales();int bestLocaleIndex = 0;if (locales.size() > 1) {if (mSupportedSystemLocales == null) {// 所有系统支持的语言mSupportedSystemLocales = Resources.getSystem().getAssets().getLocales();}bestLocaleIndex = Math.max(0, locales.getFirstMatchIndex(mSupportedSystemLocales));}// 如果是values.userSetLocale=true,设置系统属性SystemProperties.set("persist.sys.locale",locales.get(bestLocaleIndex).toLanguageTag());LocaleList.setDefault(locales, bestLocaleIndex);final Message m = PooledLambda.obtainMessage(ActivityTaskManagerService::sendLocaleToMountDaemonMsg, this,locales.get(bestLocaleIndex));mH.sendMessage(m);}mTempConfig.seq = increaseConfigurationSeqLocked();Slog.i(TAG, "Config changes=" + Integer.toHexString(changes) + " " + mTempConfig);...// Update stored global config and notify everyone about the change.mRootWindowContainer.onConfigurationChanged(mTempConfig); // 整个系统界面进行更新return changes;}
检测到SIM卡,更新系统语言

SIM卡ready后,会调用updateMccMncConfiguration()方法更新卡的MCC/MNC信息

  • frameworks/opt/telephony/src/java/com/android/internal/telephony/MccTable.java
 	/*** Updates MCC and MNC device configuration information for application retrieving* correct version of resources.  If MCC is 0, MCC and MNC will be ignored (not set).* @param context Context to act on.* @param mccmnc truncated imsi with just the MCC and MNC - MNC assumed to be from 4th to end*/public static void updateMccMncConfiguration(Context context, String mccmnc) {Rlog.d(LOG_TAG, "updateMccMncConfiguration mccmnc='" + mccmnc);if (TelephonyUtils.IS_DEBUGGABLE) {String overrideMcc = SystemProperties.get("persist.sys.override_mcc");if (!TextUtils.isEmpty(overrideMcc)) {mccmnc = overrideMcc;Rlog.d(LOG_TAG, "updateMccMncConfiguration overriding mccmnc='" + mccmnc + "'");}}if (!TextUtils.isEmpty(mccmnc)) {int mccInt;try {mccInt = Integer.parseInt(mccmnc.substring(0, 3));} catch (NumberFormatException | StringIndexOutOfBoundsException ex) {Rlog.e(LOG_TAG, "Error parsing mccmnc: " + mccmnc + ". ex=" + ex);return;}if (mccInt != 0) {ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);if (!activityManager.updateMccMncConfiguration(mccmnc.substring(0, 3), mccmnc.substring(3))) {Rlog.d(LOG_TAG, "updateMccMncConfiguration: update mccmnc="+ mccmnc + " failure");} else {Rlog.d(LOG_TAG, "updateMccMncConfiguration: update mccmnc="+ mccmnc + " success");}} else {Rlog.d(LOG_TAG, "updateMccMncConfiguration nothing to update");}}}

mcc参数不为0,继续往下执行到ActivityManagerService的updateMccMncConfiguration

  • frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
	@Overridepublic boolean updateConfiguration(Configuration values) {return mActivityTaskManager.updateConfiguration(values);}@Overridepublic boolean updateMccMncConfiguration(String mcc, String mnc) {int mccInt, mncInt;try {mccInt = Integer.parseInt(mcc);mncInt = Integer.parseInt(mnc);} catch (NumberFormatException | StringIndexOutOfBoundsException ex) {Slog.e(TAG, "Error parsing mcc: " + mcc + " mnc: " + mnc + ". ex=" + ex);return false;}Configuration config = new Configuration();config.mcc = mccInt;config.mnc = mncInt == 0 ? Configuration.MNC_ZERO : mncInt;return mActivityTaskManager.updateConfiguration(config);}

mcc/mnc参数没有问题更新Configuration,与Settings中设置语言一样执行到ActivityTaskManagerService中处理

  • frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@Overridepublic boolean updateConfiguration(Configuration values) {mAmInternal.enforceCallingPermission(CHANGE_CONFIGURATION, "updateConfiguration()");synchronized (mGlobalLock) {if (mWindowManager == null) {Slog.w(TAG, "Skip updateConfiguration because mWindowManager isn't set");return false;}if (values == null) {// sentinel: fetch the current configuration from the window managervalues = mWindowManager.computeNewConfiguration(DEFAULT_DISPLAY);}mH.sendMessage(PooledLambda.obtainMessage(ActivityManagerInternal::updateOomLevelsForDisplay, mAmInternal,DEFAULT_DISPLAY));final long origId = Binder.clearCallingIdentity();try {if (values != null) {Settings.System.clearConfiguration(values);}updateConfigurationLocked(values, null, false, false /* persistent */,UserHandle.USER_NULL, false /* deferResume */,mTmpUpdateConfigurationResult);return mTmpUpdateConfigurationResult.changes != 0;} finally {Binder.restoreCallingIdentity(origId);}}}

最终走updateConfigurationLocked(),与Settings中设置语言一样的流程。

最终修改
  • frameworks/opt/telephony/src/java/com/android/internal/telephony/MccTable.java
	// add for SIM language adaptiveimport android.content.res.Configuration;import android.os.LocaleList;import android.os.RemoteException;import android.app.ActivityManagerNative;// add end/*** Updates MCC and MNC device configuration information for application retrieving* correct version of resources.  If MCC is 0, MCC and MNC will be ignored (not set).* @param context Context to act on.* @param mccmnc truncated imsi with just the MCC and MNC - MNC assumed to be from 4th to end*/public static void updateMccMncConfiguration(Context context, String mccmnc) {Rlog.d(LOG_TAG, "updateMccMncConfiguration mccmnc='" + mccmnc);if (TelephonyUtils.IS_DEBUGGABLE) {String overrideMcc = SystemProperties.get("persist.sys.override_mcc");if (!TextUtils.isEmpty(overrideMcc)) {mccmnc = overrideMcc;Rlog.d(LOG_TAG, "updateMccMncConfiguration overriding mccmnc='" + mccmnc + "'");}}if (!TextUtils.isEmpty(mccmnc)) {int mccInt;int mncInt;try {mccInt = Integer.parseInt(mccmnc.substring(0, 3));// add for SIM language adaptivemncInt = Integer.parseInt(mccmnc.substring(3));// add end} catch (NumberFormatException | StringIndexOutOfBoundsException ex) {Rlog.e(LOG_TAG, "Error parsing mccmnc: " + mccmnc + ". ex=" + ex);return;}if (mccInt != 0) {// add for SIM language adaptivetry {Configuration config = new Configuration();config.mcc = mccInt;config.mnc = mncInt == 0 ? Configuration.MNC_ZERO : mncInt;// 根据MCC获取语言和国家码(对应的表是sTable)Locale mccLocale = LocaleUtils.getLocaleFromMcc(context, mccInt, null); // 根据sim卡的mcc参数获取的Locale不为空并且没有设置过语言,根据sim卡信息设置语言if (mccLocale != null && canUpdateLocale()){Configuration configLocal = new Configuration();configLocal = ActivityManagerNative.getDefault().getConfiguration();LocaleList userLocale = configLocal.getLocales();// sim卡语言置顶LocaleList newUserLocale = new LocaleList(mccLocale, userLocale);config.setLocales(newUserLocale);config.userSetLocale = true;config.fontScale = 1.0f;ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);if (!activityManager.updateConfiguration(config)) {Rlog.d(LOG_TAG, "updateConfiguration: update mccmnc="+ mccmnc + " failure");} else {Rlog.d(LOG_TAG, "updateConfiguration: update mccmnc="+ mccmnc + " success");}return;}} catch (RemoteException e) {throw e.rethrowFromSystemServer();}// add endActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);if (!activityManager.updateMccMncConfiguration(mccmnc.substring(0, 3), mccmnc.substring(3))) {Rlog.d(LOG_TAG, "updateMccMncConfiguration: update mccmnc="+ mccmnc + " failure");} else {Rlog.d(LOG_TAG, "updateMccMncConfiguration: update mccmnc="+ mccmnc + " success");}} else {Rlog.d(LOG_TAG, "updateMccMncConfiguration nothing to update");}}}// add for SIM language adaptiveprivate static boolean canUpdateLocale() {return !userHasPersistedLocale();}private static boolean userHasPersistedLocale() {String persistSysLanguage = SystemProperties.get("persist.sys.locale", "");return !(persistSysLanguage.isEmpty());}// add end
  • frameworks/base/core/java/android/app/ActivityManager.java
	import android.content.res.Configuration;// add for SIM language adaptivepublic boolean updateConfiguration(@NonNull Configuration values) {try {return getService().updateConfiguration(values);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}// add end

修改完之后
(1)系统预置语言, 即在makefile中指定的语言
(2)重启, 如果未插卡, 则系统语言为预置的语言
(3)重启插入SIM卡开机, 会自适应为SIM卡的语言
(4)如果有手动设置语言, 以后开机, 不管插入的是哪个国家的卡, 都会显示设置的语言, 不会根据SIM卡自适应变化.

相关文章:

Android12.0 SIM卡语言自适应

文章目录 需求语言设定Settings中语言切换流程检测到SIM卡,更新系统语言最终修改 需求 要求系统语言跟随SIM卡的语言变化。 语言设定 (1)系统预置语言, 即在makefile中指定的语言 (2)重启, 如果未插卡, 则系统语言为预置的语言 (3)重启插入SIM卡开机, 会自适应为…...

滴滴一季度营收同比增长14.9%至491亿元 经调整EBITA盈利9亿元

【头部财经】5月29日,滴滴在其官网发布2024年一季度业绩报告。一季度滴滴实现总收入491亿元,同比增长14.9%;经调整EBITA(非公认会计准则口径)盈利9亿元。其中,中国出行一季度实现收入445亿元,同…...

C语言 指针——指针变量的定义、初始化及解引用

目录 指针 内存如何编址? 如何对变量进行寻址? 用什么类型的变量来存放变量的地址? 如何显示变量的地址?​编辑 使用未初始化的指针会怎样? NULL是什么? 如何访问指针变量指向的存储单元中的数据? 指针变量的…...

详解 Spark 的运行架构

一、核心组件 1. Driver Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作主要负责: 将用户程序转化为作业 (job)在 Executor 之间调度任务 (task)跟踪 Executor 的执行情况通过 UI 展示查询运行情况 2. Exec…...

盲盒小程序开发,为市场带来的新机遇

近年来,盲盒市场一直处于热门行业中,发展非常快速。在互联网的支持下,也衍生出了线上盲盒小程序,实现了线上线下双发展的态势。 盲盒小程序作为一种新的盲盒购物方式,受到了盲盒消费者的喜爱,为盲盒行业的…...

stm32学习-流水灯

接线 注意:LED灯长一点的引脚是正极。 配置GPIO 1.使用RCC开启GPIO时钟 void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState); void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState); void RCC_APB1Perip…...

GIGE 协议摘录

系列文章目录 GIGE 学习笔记 GIGE 协议摘录 文章目录 系列文章目录引言第 1 章 设备发现1.1 链路选择1.1.1 单链路配置1.1.2 多链路配置1.1.3 链路聚合组配置 LAG 1.2 IP配置1.2.1 协议选择1.2.2 静态IP1.2.3 DHCP1.2.4 链接本地地址 LLA 1.3 设备枚举1.3.1 GVCP设备发现广播设…...

服务器的远程桌面无法连接,服务器远程桌面无法连接问题处理教程

服务器的远程桌面无法连接,服务器远程桌面无法连接问题处理教程。 一、问题概述 服务器远程桌面无法连接是日常运维中常见的问题之一。它可能由多种原因造成,如网络问题、服务器配置错误、远程桌面服务未启动等。本教程将指导您逐步排查并解决这些问题。…...

【机器学习300问】105、计算机视觉(CV)领域有哪些子任务?

计算机视觉作为人工智能的重要分支,发展至今已经在诸多领域取得显著的成果。在众多的计算机视觉任务中,图像分类、目标检测与定位、语义分割和实例分割是四个基本而关键的子任务,它们在不同的应用场景下扮演着重要角色。这四个子任务虽然各具…...

安卓手机APP开发__超宽带(UWB)通信

安卓手机APP开发__超宽带(UWB)通信 目录 概述 控制方/发起方与控制方/响应方 参数范围 后台测距 STS 配置 步骤 使用限制 代码示例 示例应用 UWB 范围 RxJava3 支持 生态系统支持 支持 UWB 的移动设备 第三方 SDK 概述 注意 :UWB 目前仅支持 Jetpac…...

儿童股骨干骨折用儿童悬吊如何进行康复

儿童股骨干骨折后的悬吊康复训练,应根据骨折的具体情况和儿童的年龄来制定个性化的康复计划。悬吊康复训练主要目的是通过减轻骨折部位的压力,促进骨折愈合,同时保持和增强儿童的肌肉力量和关节活动能力。 悬吊康复训练的方法 1.垂直悬吊皮牵…...

vscode plantuml插件安装使用(windows)

1、安装JDK,网址 https://www.oracle.com/java/technologies/,添加系统变量JAVA_HOME 2、安装graphviz,网址 Download | Graphviz, 并添加用户变量GRAPHVIZ_DOT 3、vscode安装插件plantuml 4、新增wsd文件,按照使用…...

Linux内核编译流程3.10

一、内核源代码编译流程 编译环境: cat /etc/redhat-release CentOS Linux release 7.4.1708 (Core) Linux内核版本: uname -r 3.10.0-693.el7.x86_64 编译内核源代码版本:linux-4.19.90-all-arch-master cp /boot/config-xxx到内核源代码目录/.configmake menuconfi…...

OSPF多区域组网实验(华为)

思科设备参考:OSPF多区域组网实验(思科) 技术简介 OSPF多区域功能通过划分网络为多个逻辑区域来提高网络的可扩展性和管理性能。每个区域内部运行独立的SPF计算,而区域之间通过区域边界路由器进行路由信息交换。这种划分策略适用…...

解密MySQL二进制日志:深度探究mysqlbinlog工具

欢迎来到我的博客,代码的世界里,每一行都是一个故事 🎏:你只管努力,剩下的交给时间 🏠 :小破站 解密MySQL二进制日志:深度探究mysqlbinlog工具 前言mysqlbinlog工具概述mysqlbinlog的…...

妙解设计模式之策略模式

目录 策略模式的概念生活中的例子编程中的例子 软件工程中的实际应用数据排序文件压缩支付方式图形绘制 策略模式的概念 策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,把它们一个个封装起来,并…...

Linux DHCP server 配置

参考:linux dhcp配置多vlan ip_linux 接口vlan-CSDN博客 配置静态IP地址: 给固定的MAC地址分配指定的IP地址,固定的IP地址不必包含在指定的IP池中,如果包含在IP地址池中,固定的IP地址会从IP地址池中移除 配置方法&…...

深入解析力扣166题:分数到小数(模拟长除法与字符串操作详解及模拟面试问答)

力扣166题:分数到小数 在本篇文章中,我们将详细解读力扣第166题“分数到小数”。通过学习本篇文章,读者将掌握如何使用多种方法来解决这一问题,并了解相关的复杂度分析和模拟面试问答。每种方法都将配以详细的解释和ASCII图解&am…...

新疆 | 金石商砼效率革命背后的逻辑

走进标杆企业,感受名企力量,探寻学习优秀企业领先之道。 本期要跟砼行们推介的标杆企业是新疆砼行业的龙头企业:新疆兵团建工金石商品混凝土有限责任公司(以下简称:新疆金石)。 从年产80万方到120万方&am…...

Dinky MySQLCDC 整库同步到 Doris

资源:flink 1.17.0、dinky 1.0.2、doris-2.0.1-rc04 问题:Cannot deserialize value of type int from String ,detailMessageunknowndatabases ,not a valid int value 2024-05-29 16:52:20.136 ERROR org.apache.doris.flink.…...

像素剧本圣殿惊艳案例:故障艺术标题下生成的赛博朋克短剧完整场次

像素剧本圣殿惊艳案例:故障艺术标题下生成的赛博朋克短剧完整场次 1. 像素剧本圣殿创作工具介绍 Pixel Script Temple(像素剧本圣殿)是一款基于Qwen2.5-14B-Instruct深度微调的专业剧本创作工具。这个独特的创作平台将先进的AI推理能力与复…...

大多数团队不是“用不好 PPO”,而是“用错了 PPO”

更多时候,你会听到的是: “PPO 太复杂了,算了”“调了一轮,模型变怪了”“感觉不如再多搞点 SFT 数据” 于是 PPO 很容易被贴上一个标签: “理论上很强,工程上很坑。” 但这个结论,其实并不公…...

开源AI翻译新范式:Pixel Language Portal镜像免配置+GPU算力适配教程

开源AI翻译新范式:Pixel Language Portal镜像免配置GPU算力适配教程 1. 产品概览:像素语言跨维传送门 Pixel Language Portal(像素语言跨维传送门)是一款基于Tencent Hunyuan-MT-7B大模型构建的创新翻译工具。与传统翻译软件不同…...

FASTDDS-Python 实战:从零构建分布式通信环境

1. 为什么选择Fast DDS-Python? 在物联网和机器人系统中,设备间的实时通信是个硬需求。想象一下,你正在开发一个智能仓储机器人系统,需要让多台机器人在复杂环境中协同工作。这时候,传统的HTTP请求-响应模式就显得力不…...

Windows系统维护新体验:告别繁琐手动操作,用WinUtil一键搞定所有

Windows系统维护新体验:告别繁琐手动操作,用WinUtil一键搞定所有 【免费下载链接】winutil Chris Titus Techs Windows Utility - Install Programs, Tweaks, Fixes, and Updates 项目地址: https://gitcode.com/GitHub_Trending/wi/winutil 你是…...

电子电路中的“心脏”:电源

一、语言特性:Java 26 与模式匹配进化 1.1 Java 26 语言级别支持 IDEA 2026.1 EAP 最引人注目的变化之一,就是新增 Java 26 语言级别支持。这意味着开发者可以提前体验和测试即将在 JDK 26 中正式发布的语言特性。 其中最重要的变化是对 JEP 530 的全面支…...

使用AIVideo实现LaTeX学术报告自动转视频教程

使用AIVideo实现LaTeX学术报告自动转视频教程 1. 引言 作为一名科研工作者,你是否曾经为了准备学术会议的视频报告而头疼?传统的视频制作需要录制、剪辑、配音等多个繁琐步骤,耗时耗力。现在,通过AIVideo这个强大的AI视频创作平…...

c++阿克曼函数详解

不爱吃饭的蓝胖子要开始整活了!!!大家好,我是蓝胖子!好久不见,倍感思念!今天带来的是--C阿克曼函数~~希望你能看到最后,有惊喜哈!正片开始 ——————————————…...

别再只记*#*#284#*#*了!揭秘小米手机日志抓取的‘售后模式’:CIT工具(*#*#6484#*#*)的隐藏用法与解读

解锁小米手机CIT工具的隐藏潜能:从硬件诊断到日志深度解析 在智能手机高度普及的今天,用户对设备问题的自主排查需求日益增长。小米手机内置的CIT工具(Customer Interface Test)作为售后服务的核心诊断利器,其实蕴藏着…...

基于SpringBoot + Vue的校园流浪动物救助平台

文章目录前言一、详细操作演示视频二、具体实现截图三、技术栈1.前端-Vue.js2.后端-SpringBoot3.数据库-MySQL4.系统架构-B/S四、系统测试1.系统测试概述2.系统功能测试3.系统测试结论五、项目代码参考六、数据库代码参考七、项目论文示例结语前言 💛博主介绍&#…...