news 2026/9/2 15:30:13

【时光清单|08】HarmonyOS ArkTS 备份服务实战:定义导出、恢复和版本兼容边界

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【时光清单|08】HarmonyOS ArkTS 备份服务实战:定义导出、恢复和版本兼容边界

【时光清单|08】HarmonyOS ArkTS 备份服务实战:定义导出、恢复和版本兼容边界

本地应用一旦允许用户积累纪念日、收藏语录、情侣空间和主题偏好,备份就不再是“把对象JSON.stringify()一下”这么简单。真正困难的是恢复:文件来自哪个版本、字段是否完整、数据能否迁移、旧数据与当前数据如何合并、写入一半失败怎么办、恢复后页面和桌面卡片何时刷新。若这些边界没有定义清楚,一个能成功生成 JSON 的按钮,反而可能给用户制造“数据已经安全”的错觉。

时光清单的真实源码提供了一个轻量BackupService:它在应用私有filesDir中写入带时间戳的 JSON 文件,能够读取、列出和删除备份;BackupData包含versiontimestamp、纪念日、语录、情侣空间和主题 ID。与此同时,当前importBackup()只解析文件并做最低格式判断,并没有把数据写回DataStore或各 Repository;项目声明的EntryBackupAbility也只记录系统备份与恢复回调,没有执行数据搬运。

因此本文不会把“读取成功”描述成“恢复完成”,而是以现有能力为起点,说明一个可审核、可测试、可演进的本地备份方案应该怎样划分导出、校验、迁移、预览、提交和回滚边界。

本文将完成以下真实复核:

  1. 解释BackupData的版本、时间戳与数据域。
  2. 还原私有目录导出、UTF-8 读取、列表和删除流程。
  3. 区分“导入文件”“解析备份”和“恢复业务数据”。
  4. 分析当前最低校验无法覆盖的类型、大小与路径风险。
  5. 设计版本迁移、原子恢复、冲突策略和 UI 状态。
  6. 区分应用内手动备份与系统BackupExtensionAbility

本文唯一标记:CSDN-SERIES:ALL-163207773

证据边界:哪些是当前事实,哪些只是建议

本文的当前事实来自BackupService.etsDataStore.ets、相关模型、ProfileView.etsEntryBackupAbility.etsmodule.json5backup_config.json的定向复核。源码可以证明接口、字段和静态调用边界,却不能单独证明构建已经通过、真机已经运行、系统迁移已经成功或用户数据已经恢复。本文没有运行构建、安装或真机恢复,也不会把这些结果补写成既成事实。

项目错误记录中没有找到备份服务的直接实现历史,定向 Git 历史查询也没有返回可引用的变更。因此“历史证据”只能写成没有发现直接记录,不能反推某个功能何时上线。下文凡是使用“建议”“应当”“可以设计”的段落,均属于未来实现方案,不代表当前工程已有这些能力。

一、先把“备份”拆成六个动作

一个完整备份流程至少有六个阶段:

采集业务快照 -> 序列化并写入文件 -> 让用户识别或导出文件 -> 读取并限制输入 -> 校验、迁移与预览 -> 原子写回并刷新应用状态

当前BackupService已覆盖“构造快照、写文件、读文件、列文件、删文件”,但没有接入文件选择、外部分享、迁移、仓库写回或事务回滚。工程分析必须尊重这个边界。

二、真实 BackupData 契约

源码把备份结构定义为:

export interface BackupData { version: number; timestamp: number; anniversaries: Anniversary[]; quotes: Quote[]; coupleSpace: CoupleSpace | null; themeId: string; }

字段语义如下:

字段用途恢复时的关注点
version备份格式版本决定迁移路径
timestamp备份生成时间展示、排序、审计
anniversaries纪念日集合ID、日期、枚举与重复项
quotes语录集合收藏状态与内容完整性
coupleSpace情侣空间可空、嵌套任务和留言
themeId主题身份合法 ID 与降级

version应表示备份 schema 版本,不是应用版本号。应用版本可能从1.0.0升到1.2.0,备份结构却没有变化;反过来,一次字段迁移也可能需要提升备份版本,即使应用营销版本只是补丁更新。

三、createBackupData:先构造明确快照

createBackupData()把多个数据域组合成版本 1:

createBackupData( anniversaries: Anniversary[], quotes: Quote[], coupleSpace: CoupleSpace | null, themeId: string ): BackupData { const bd: BackupData = { version: 1, timestamp: Date.now(), anniversaries, quotes, coupleSpace, themeId }; return bd; }

这个方法的价值在于统一备份包形状,但它并不负责从 Repository 采集数据。调用方必须保证这些集合属于同一逻辑时刻,否则可能出现纪念日已经更新、情侣空间仍是旧快照的情况。

对于当前本地小数据量,可以依次读取后立即构造;若未来迁移到 RDB 或多个异步数据源,应增加快照协调层。备份开始后还要禁用重复点击,避免同时生成多个内容几乎相同的文件。

四、导出:文件实际位于应用私有目录

构造函数使用context.filesDir

export class BackupService { private baseDir: string; constructor(context: Context) { this.baseDir = context.filesDir; } }

导出文件名由当前时间戳组成:

const fileName = `backup_${Date.now()}.json`; const filePath = `${this.baseDir}/${fileName}`;

随后序列化并写入:

const json = JSON.stringify(data, null, 2); const file = fileIo.openSync( filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY ); fileIo.writeSync(file.fd, json); fileIo.closeSync(file); return filePath;

这里的“导出”准确说是“在应用私有文件目录创建备份”。返回路径不等于用户已经把文件保存到下载目录,也不等于其他应用可以直接读取。若产品要支持用户手动保存或跨设备传输,还需要系统文档选择器、分享能力或平台支持的文件 URI,并遵循相应授权规则。

五、写文件需要补上的资源关闭保证

当前代码在正常路径调用closeSync()。若writeSync()抛出异常,文件可能没有进入关闭语句。更稳妥的结构是把关闭放入finally

let file: fileIo.File | null = null; try { file = fileIo.openSync( filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY ); fileIo.writeSync(file.fd, json); return filePath; } finally { if (file !== null) { fileIo.closeSync(file); } }

具体文件类型和 API 签名应以项目当前 SDK 为准。原则是:任何打开成功的句柄,都必须在成功和失败路径关闭。

另一个问题是覆盖模式。文件名精确到毫秒,正常操作很难冲突,但自动化或并发调用仍可能产生同名。可以在创建前检查,或增加随机后缀。更重要的是写入原子性:先写.tmp,完成后再重命名,避免列表里出现半写文件。

六、导入:当前只完成读取与解析

importBackup()打开指定路径、查询大小、分配缓冲区、读取并用 UTF-8 解码:

const file = fileIo.openSync( filePath, fileIo.OpenMode.READ_ONLY ); const stat = fileIo.statSync(filePath); const buf = new ArrayBuffer(stat.size); fileIo.readSync(file.fd, buf); fileIo.closeSync(file); const decoder = util.TextDecoder.create('utf-8'); const json = decoder.decodeWithStream( new Uint8Array(buf) ); const data = JSON.parse(json) as BackupData;

最后只检查两个条件:

if (!data.version || !data.anniversaries) { throw new Error('Invalid backup format'); }

成功后直接返回BackupData。它没有调用:

  • AnniversaryRepository.save()
  • DataStore.putJson()
  • 语录或情侣空间服务;
  • AppStorage数据版本通知;
  • 页面重载;
  • 桌面卡片更新。

所以当前方法更准确的命名是readBackup()parseBackup()。如果保留importBackup(),调用方也必须理解它只返回候选数据,不代表恢复提交成功。

七、类型断言不是运行时校验

这行代码:

const data = JSON.parse(json) as BackupData;

只告诉 ArkTS 编译器“把它当作 BackupData”,不会验证运行时字段。攻击者、旧版本或损坏文件都可能提供错误结构:

{ "version": 1, "anniversaries": "not-an-array", "quotes": null, "coupleSpace": [], "themeId": 123 }

当前!data.anniversaries对非空字符串会通过。恢复层若随后调用数组方法,就会异常。

最小校验应覆盖:

function isBackupData(value: Object): boolean { const data = value as BackupData; return Number.isInteger(data.version) && data.version > 0 && Number.isFinite(data.timestamp) && Array.isArray(data.anniversaries) && Array.isArray(data.quotes) && typeof data.themeId === 'string'; }

随后还要逐项检查纪念日 ID、类型、日期、重复规则和布尔字段。不能因为顶层是数组,就相信数组中的每一项。

八、导入前必须限制文件大小

当前实现根据stat.size一次性分配ArrayBuffer。应用私有备份通常很小,但若未来允许用户选择任意文件,超大文件可能造成内存压力甚至无响应。

应在分配前设置上限:

const MAX_BACKUP_BYTES = 5 * 1024 * 1024; const stat = fileIo.statSync(filePath); if (stat.size <= 0 || stat.size > MAX_BACKUP_BYTES) { throw new Error( 'Backup file size is invalid' ); }

上限要根据真实数据规模设定,并在 UI 中给出可理解提示。还应检查扩展名和内容,但扩展名只能作为初筛,真正可信的是解码、JSON 解析和 schema 校验。

九、路径边界与删除安全

deleteBackup()直接拼接:

fileIo.unlinkSync( `${this.baseDir}/${fileName}` );

fileName只来自listBackups()的结果,风险较低;若未来来自文本参数或路由参数,就必须拒绝../、绝对路径和路径分隔符。

可以只允许固定命名:

const BACKUP_NAME = /^backup_\d+\.json$/; if (!BACKUP_NAME.test(fileName)) { throw new Error( 'Invalid backup file name' ); }

导入外部路径时也不能把任意路径永久保存为应用配置。应通过官方文件选择能力获取临时可访问 URI,把内容复制到受控临时区后校验。

十、listBackups:文件名排序为什么可行

列表方法筛选固定前后缀,并倒序:

return entries .filter((fname: string) => fname.startsWith('backup_') && fname.endsWith('.json') ) .sort() .reverse();

因为中间部分是同长度毫秒时间戳,字符串排序与时间排序一致。该结论依赖命名规则不变。若以后加入手动标签、不同前缀或 schema 版本,应该读取文件元数据或解析文件名,而不是继续依赖裸字符串。

列表 UI 最好展示:

信息来源
生成时间文件名或timestamp
格式版本version
纪念日数量anniversaries.length
文件大小stat.size
校验状态预解析结果

只展示路径会把内部实现暴露给用户,也不利于识别目标备份。

十一、版本迁移必须先于恢复写入

当前version固定为 1,却没有分支处理。一个可扩展迁移器可以采用逐版本升级:

interface BackupV2 extends BackupData { version: 2; colorModePreference: string; } function migrateBackup( raw: BackupData ): BackupV2 { if (raw.version === 1) { return { ...raw, version: 2, colorModePreference: 'system' }; } if (raw.version === 2) { return raw as BackupV2; } throw new Error( 'Unsupported backup version' ); }

迁移只生成新的内存对象,不立即写入当前数据。所有版本转换、字段默认值和非法值修复完成后,再进入预览和提交阶段。

如果备份版本高于当前应用支持版本,应明确提示“请升级应用后恢复”,不能盲目忽略未知字段后继续写入,因为新版本可能改变了关键语义。

十二、恢复不是逐条 save

最直接的恢复方式是循环:

for (const item of backup.anniversaries) { await anniversaryRepo.save(item); }

这种做法有三个问题:

  1. 写到一半失败会留下部分恢复数据。
  2. 每条都刷新持久化,性能差。
  3. 相同 ID 的冲突策略被隐式交给save()

更可靠的方案是在 Repository 或恢复协调层提供批量替换:

interface RestorePlan { anniversaries: Anniversary[]; quotes: Quote[]; coupleSpace: CoupleSpace | null; themeId: string; } interface RestoreResult { success: boolean; restoredCounts: number[]; warnings: string[]; }

提交前先保存当前数据快照,按固定顺序写入所有数据域;任一步失败时尝试回滚。若底层改为 RDB,应使用事务。Preferences 跨多个键没有天然跨键事务,需要设计临时键、提交标记或双缓冲方案。

十三、覆盖、合并还是跳过

恢复必须先定义冲突策略。

覆盖

清空当前数据,完整替换为备份。结果容易理解,但风险最大,必须二次确认并展示将被替换的数量。

合并

保留当前数据,按 ID 合并备份。需要定义同 ID 时使用较新的updatedAt、优先当前值,还是让用户选择。

仅新增

只导入当前不存在的 ID。最安全,但无法恢复被错误修改的记录。

建议在恢复预览中明确显示:

备份版本:1 生成时间:2026-07-25 18:20 纪念日:18 条 语录:42 条 情侣空间:有 冲突:3 条 模式:覆盖 / 合并 / 仅新增

不可逆覆盖必须由用户明确确认,自动化流程也应停在最终确认之前。

十四、恢复后如何让页面同步

即使所有数据已写回,现有 Repository 可能仍缓存旧数组。恢复提交必须协调:

  1. 写入底层存储。
  2. 让 Repository 重新水合或替换内存集合。
  3. 更新当前主题。
  4. 递增StateKeys.DATA_VERSION
  5. 刷新桌面 Form 数据。
  6. 返回成功结果。

如果只直接调用DataStore.putJson()AnniversaryRepository.initialized仍为true,后续getAll()会继续返回旧缓存。因此恢复 API 应由 Repository 暴露,而不是绕过它修改 Preferences。

可设计:

async replaceAll( items: Anniversary[] ): Promise<void> { const normalized = items.map(normalizeAnniversary); await this.store.putJson( DataKeys.ANNIVERSARIES, normalized ); this.items = normalized; }

持久化成功后才替换内存,或者准备旧值用于失败回滚。恢复协调层完成所有域后,只发送一次全局数据版本通知。

十五、当前 BackupData 并未覆盖全部应用数据

DataKeys还包含:

static readonly WISHES = 'ds_wishes'; static readonly DIARY = 'ds_diary'; static readonly HABITS = 'ds_habits'; static readonly HABIT_WALL = 'ds_habit_wall'; static readonly COUPLE = 'ds_couple'; static readonly ALBUM = 'ds_album'; static readonly MOOD_BACKGROUND = 'ds_mood_background'; static readonly WIDGET_ANNIVERSARY_ID = 'ds_widget_anniversary_id';

BackupData只包含纪念日、语录、情侣空间和主题 ID。愿望、日记、习惯、相册元数据、心情背景和桌面卡片选择并未进入当前备份结构。

因此产品文案不能写“完整备份所有数据”。更准确的是“备份纪念日、语录、情侣空间和主题信息”,或者先扩充 schema 再声明完整备份。

相册还可能只保存媒体 URI,而不是图片二进制。跨设备恢复后,旧 URI 未必仍可访问。备份媒体需要单独的文件复制、容量预算和授权策略,不能只复制字符串。

十六、手动备份与系统备份是两条链路

模块声明了:

{ "name": "EntryBackupAbility", "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", "type": "backup", "exported": false }

对应 Ability 当前只记录回调:

export default class EntryBackupAbility extends BackupExtensionAbility { async onBackup() { hilog.info( DOMAIN, 'testTag', 'onBackup ok' ); await Promise.resolve(); } async onRestore( bundleVersion: BundleVersion ) { hilog.info( DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion) ); await Promise.resolve(); } }

这证明系统备份扩展已注册,但不能证明实际数据已被系统备份或恢复。应用内BackupService创建 JSON 文件,系统BackupExtensionAbility响应平台生命周期,两者目的和触发方式不同。

若要接通系统备份,应依据当前 HarmonyOS 官方 Core File Kit 文档确认备份目录、配置、版本与恢复时机,并测试卸载重装或设备迁移。不能简单在回调里调用私有 JSON 导出,就宣称跨设备恢复完成。

十七、敏感数据与隐私边界

备份可能包含纪念日标题、关系开始日期、情侣留言和个人收藏。这些信息未必属于系统定义的敏感权限数据,但对用户具有明显隐私价值。

本地私有目录的优势是默认受应用沙箱保护。若允许导出到公共位置或分享:

  • 明确提示文件包含哪些内容;
  • 不把绝对私有路径显示为“云备份成功”;
  • 不在日志打印完整 JSON;
  • 不自动上传;
  • 不把备份附加到反馈或分析请求;
  • 删除操作要说明只删除备份,不删除当前数据;
  • 恢复覆盖前明确说明影响。

如果加入密码保护,应使用平台安全能力和经过验证的加密方案,不能自行设计简单异或或把密钥写进代码。

十八、UI 状态必须完整

备份页面至少需要:

type BackupUiState = 'idle' | 'collecting' | 'writing' | 'reading' | 'validating' | 'preview' | 'restoring' | 'success' | 'error';

交互规则:

状态行为
writing禁用重复导出
reading显示文件读取进度或等待
preview展示版本、数量、冲突
restoring禁止离开或重复提交
error保留当前数据,提供可理解原因
success显示恢复数量并刷新页面

删除备份和覆盖当前数据是两种不同风险。删除单个备份应确认目标文件;覆盖恢复则应显示当前数据将如何变化,不能复用一个模糊的“确定”弹窗。

十九、测试矩阵

导出

  1. 空数据能生成合法备份。
  2. 中文、emoji、长备注经过 UTF-8 往返不损坏。
  3. 写入失败会关闭句柄并显示错误。
  4. 连续导出不会覆盖旧文件。
  5. 列表按最新时间优先。

解析

  1. 合法 v1 文件通过。
  2. 空文件、非 JSON、超大文件被拒绝。
  3. anniversaries不是数组时被拒绝。
  4. 非法枚举、日期、ID 被报告。
  5. 未知高版本提示升级应用。

恢复

  1. 覆盖、合并、仅新增结果符合预览。
  2. 中途失败不留下半恢复状态。
  3. Repository 内存与 Preferences 一致。
  4. 页面统计在一次版本通知后刷新。
  5. 当前主题和桌面卡片同步更新。
  6. 重启后数据与恢复完成时一致。

多设备与发布

当前模块只声明phone,不能虚构平板或 2in1 已支持。若未来扩大设备类型,要验证文件选择器、确认弹窗、长列表和安全区适配。发布前还要确认隐私说明与实际备份范围一致。

二十、渐进实现路线

基于现有源码,建议依次完成:

  1. importBackup()明确为读取与解析阶段。
  2. 增加文件大小、文件名和完整 schema 校验。
  3. 为 v1 建立规范化函数,为未来版本建立迁移器。
  4. 新增恢复预览与冲突策略。
  5. 给 Repository 增加批量替换或合并 API。
  6. 设计失败回滚,再接入页面确认。
  7. 恢复成功后统一刷新 AppStorage 和 Form。
  8. 扩展 BackupData 覆盖真实需要的数据域。
  9. 单独评估相册文件与 URI 的可迁移性。
  10. 按官方文档实现并验证系统备份扩展。

这条路线保留了当前BackupService的文件能力,又避免直接把解析结果写入业务仓库。

二十一、把“可解析”推进到“可恢复”的建议契约

这一节全部是建议实现,用于把前面的风险落到可检查的接口上,不是当前源码能力。第一步不是立刻写回数据,而是把导入结果分成明确状态。解析成功只能得到候选包;只有候选包通过版本、结构和业务规则检查,才允许进入预览。

type InspectResult = | { status: 'valid'; data: BackupData; warnings: string[] } | { status: 'unsupported_version'; version: number } | { status: 'invalid'; errors: string[] };

这个联合类型能阻止调用方用一个布尔值掩盖差异。未知高版本意味着当前应用看不懂,不等于文件损坏;字段缺失属于结构问题;日期范围或重复 ID 则是业务规则问题。三者对应的用户提示、重试方式和日志级别都不同。

第二步是建立明确的版本支持表。当前源码创建的是 v1,但导入逻辑只检查version是否为真值,没有拒绝未知版本。建议只接受列入支持表的版本,并且让迁移按相邻版本逐级执行,避免一个巨大的条件分支同时理解所有历史结构。

const CURRENT_BACKUP_VERSION: number = 2; function migrateToCurrent(raw: object, version: number): BackupData { if (version === 1) { return migrateV1ToV2(raw); } if (version === CURRENT_BACKUP_VERSION) { return normalizeV2(raw); } throw new Error('unsupported backup version'); }

这里的migrateV1ToV2normalizeV2是示意名称。真正实现时必须根据真实 schema 变化编写,不能为了让示例“看起来完整”而虚构字段。迁移后还要再次校验,因为迁移器本身也可能产生非法值。

第三步是限制输入资源。当前实现读取stat.size后直接申请同等大小的ArrayBuffer,并且没有检查一次readSync的返回字节数。建议先设定与业务数据规模相匹配的上限,再循环读取或核对已读字节。上限必须由真实数据量和测试决定,文章不编造一个“通用安全值”。

function assertReadableSize(size: number, maxBytes: number): void { if (!Number.isInteger(size) || size <= 0) { throw new Error('empty or invalid backup file'); } if (size > maxBytes) { throw new Error('backup file exceeds configured limit'); } }

第四步是把提交设计成一个可回滚边界。建议在用户确认之前只做读取、校验、迁移和预览;确认之后先保存当前数据快照,再通过服务层或仓库批量写入。任何数据域失败,都应停止后续写入并恢复旧快照。恢复完成后还要重新读取一次持久层并比较关键数量、ID 集合和版本,不能只相信写入 API 没有抛异常。

async function restoreCandidate(candidate: BackupData): Promise<void> { const before = await snapshotCurrentData(); try { await commitAllDomains(candidate); await verifyCommittedData(candidate); } catch (error) { await restoreSnapshot(before); throw error; } }

这段代码表达的是控制边界,不代表当前工程已有snapshotCurrentDatacommitAllDomainsrestoreSnapshot。如果 Preferences 无法提供原生事务,就要在应用服务层设计临时键、提交标记或双份快照;如果未来改用关系型存储,则应优先使用数据库事务。选择必须服从实际存储机制。

第五步是把路径和文件名当作输入验证的一部分。当前删除逻辑把传入文件名直接拼到filesDir,建议仅允许服务自身生成的命名格式,并在规范化后确认目标仍处于基准目录。列表接口也应区分“目录为空”和“读取失败”,否则 UI 会把权限或 I/O 错误误报成“暂无备份”。

function isGeneratedBackupName(name: string): boolean { return /^backup_[0-9]+\.json$/.test(name); }

文件名校验不是完整防线,真实实现还需要使用平台提供的规范化路径能力并验证父目录。删除前可以展示文件生成时间和包含的数据数量,但这些摘要必须来自已经校验过的备份,不能直接信任外部文件中的任意文本。

二十二、验收标准必须对应真实动作

建议把验收拆成四层。静态层检查 schema、支持版本表、资源关闭和路径约束;单元层覆盖合法、损坏、超大、未知版本和嵌套字段错误;集成层验证多数据域写入失败后的回滚;设备层才验证文件选择、应用重启、系统备份回调和真实恢复。某一层通过不能代替下一层。

对于当前文章,能确认的是源文件中存在手动 JSON 服务、系统备份扩展声明和空实现式回调;不能确认的是构建、设备、跨版本、卸载重装或系统迁移结果。后续实现若完成,也应记录实际命令、设备环境、备份版本、输入样本和失败注入点,再据此更新结论。

二十三、总结

时光清单当前备份能力可以准确概括为:

BackupData 定义 v1 快照 -> exportBackup 写入 filesDir -> listBackups 管理本地备份文件 -> importBackup 读取、解码和最低校验 -> 返回候选 BackupData

它已经具备本地 JSON 备份文件的基础设施,但尚未形成完整恢复闭环。真正可靠的恢复还需要运行时 schema 校验、大小和路径限制、版本迁移、冲突预览、Repository 批量提交、失败回滚、页面失效通知以及桌面卡片同步。

对本地数据应用而言,备份功能最重要的承诺不是“生成了一个文件”,而是:能说清备份了什么、能在支持的版本中完整验证、恢复失败不破坏现有数据,恢复成功后所有数据层和界面保持一致。


本文基于时光清单项目的BackupService.etsEntryBackupAbility.etsDataStore.ets和相关模型真实源码复核整理。文中明确区分了当前文件读写能力与尚未实现的完整恢复流程。

AI 辅助声明:本文在真实源码核验、结构梳理和文字编辑过程中使用了 AI 辅助;关键接口、调用边界和工程结论均以项目源码为依据进行人工复核。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 15:29:44

【单片机毕业设计】基于 STM32 单片机的多档位定量出水控制系统设计 基于 STM32 与传感器的智能热水管控系统设计与实现(012106)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/2 15:21:04

C++入门--STL库

1. 标准库中的string类 1.1 string类(了解) string类的文档介绍 在使用string类时&#xff0c;必须包含#include头文件以及using namespace std; 1.2 auto和范围for auto关键字 在这里补充2个C11的小语法&#xff0c;方便我们后面的学习。 • 在早期C/C中auto的含义是&#x…

作者头像 李华
网站建设 2026/9/2 15:19:04

单片机毕业设计-基于 STM32 或 51 单片机的坐姿提醒调光台灯装置设计与开发 基于 STM32 或 51 单片机的人体感应定时台灯控制系统设计与实现(011906)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/2 15:16:34

用Python实现精灵月签系统:抽奖、闪避、锁血与格挡机制原型

前几天在找同人游戏灵感的时候&#xff0c;刷到“穿越精灵世界&#xff0c;意外觉醒精灵月签系统”这个标题&#xff0c;点进去发现是一本宝可梦同人小说。标题里的“绝对闪避、锁血挂、徒手接白刃”&#xff0c;看起来只是网文爽点&#xff0c;但拆开看其实就是一套很完整的战…

作者头像 李华
网站建设 2026/9/2 15:13:19

两周快速掌握FreeRTOS:基于STM32CubeMX的任务创建与源码分析实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/2 15:09:12

MATLAB R2024a 永久授权安装指南:从授权类型识别到工程化配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华