news 2026/10/14 4:40:41

HarmonyOS智慧农业地块管理:增删改查一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS智慧农业地块管理:增删改查一次讲透

HarmonyOS智慧农业管理应用开发这个系列写到第7篇,上篇我们把“高高种地”的地块管理页面从零搭好,数据库和列表框架已经立起来了。这篇接着把地块管理系统的增删改查一次讲透。增删改查是智慧农业项目里最基础也最绕不开的一组动作:新增一个地块、查看地块信息、修改种植数据、删除废弃地块。用HarmonyOS的ArkTS配合关系型数据库(RDB),四个动作串起来就是一个完整的信息管理闭环。

这篇教程适合两类人:一类是正在从零写HarmonyOS应用的初学者,跟着把代码敲一遍就能跑通全流程;另一类是已经会基础UI、想搞清楚RDB怎么真正落地到业务场景的开发者。我会把每一步为什么这么做、踩过什么坑都写出来,而不是丢一段代码就完事。代码基于API 12版本,老版本项目可能需要小改导入语句,后面会专门提。

1. 设计先行:先把地块数据表立起来

1.1 字段设计决定了后面少返工

做增删改查之前,第一步不是写代码,而是想清楚一个地块需要存哪些信息。我最早做这个模块时偷懒只存了名称和面积,结果后来要按作物筛选、要按状态展示,只能回头改表结构,连带着所有查询代码都要动。这次“高高种地”的地块表,我建议直接按下面的字段来:

字段名类型说明约束
idINTEGER地块唯一标识主键,自增
nameTEXT地块名称非空
areaREAL面积,单位亩非空
cropTypeTEXT当前种植作物可空
soilTypeTEXT土壤类型可空
statusINTEGER状态:1正常,0暂停默认1
createTimeTEXT创建时间非空
updateTimeTEXT更新时间非空

面积字段我特别用REAL而不是TEXT,是因为后面做统计、做排序都会用到数字计算,存文本到时候还得转型,纯给自己找麻烦。状态字段用INTEGER而不是TEXT,也是同理——判断状态时直接比较数字比比较字符串更稳妥,而且后续如果想扩展更多状态,加数字枚举就行。

时间字段用TEXT存ISO格式的字符串,比如“2025-01-08T10:30:00”,在ArkTS里生成和比较都很方便。真要做时间区间查询,字符串比较在ISO格式下也能正常工作,前期够用。

1.2 为什么选关系型数据库而不是Preferences

HarmonyOS本地存储有好几种方案,最简单的是Preferences,键值对存储,用起来像Map,存个开关状态、用户配置很合适。但地块数据是结构化数据,而且需要按名称查、按状态过滤、按时间排序,这些操作如果用Preferences实现,得把所有数据读出来自己写循环过滤,代码量大不说,性能也不如SQL。

关系型数据库RDB(Relational Database)在HarmonyOS里封装的是SQLite,支持SQL、事务、索引,这些正是列表类业务需要的。地块以后还可能跟农事记录、物资投入数据做关联查询,有RDB打底,后面扩展起来不慌。所以这个项目里,地块模块直接用RDB,Preferences只留给登录状态这类简单配置。

1.3 建表与数据库初始化

RDB初始化在HarmonyOS里是通过getRdbStore完成的。一个应用可以建多个数据库文件,也可以一个库多张表。地块管理我建议单独用smart_farm.db,后面农事记录、库存管理都放在这个库里,方便统一管理。

import { relationalStore } from '@kit.ArkData'; import { hilog } from '@kit.PerformanceKit'; const TAG = 'FieldDB'; const STORE_CONFIG: relationalStore.StoreConfig = { name: 'smart_farm.db', securityLevel: relationalStore.SecurityLevel.S1, }; const CREATE_FIELD_TABLE_SQL: string = ` CREATE TABLE IF NOT EXISTS field ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, area REAL NOT NULL, cropType TEXT, soilType TEXT, status INTEGER DEFAULT 1, createTime TEXT, updateTime TEXT ) `; export async function initRdb(context: Context): Promise<relationalStore.RdbStore> { const store = await relationalStore.getRdbStore(context, STORE_CONFIG); await store.executeSql(CREATE_FIELD_TABLE_SQL); hilog.info(TAG, 'field table ready'); return store; }

这里有个细节:建表SQL一定要加IF NOT EXISTS,否则每次启动应用都执行一次会直接报“表已存在”的错。getRdbStore本身只要传入的StoreConfig一样,重复调用也不会重复创建文件,但executeSql不是幂等的,得靠SQL语句自身保证安全。

关于导入路径,API 12以后推荐用import { relationalStore } from '@kit.ArkData',老一点的项目里也常见import relationalStore from '@ohos.data.relationalStore'。这两种写法本质上是同一个模块,只是导出方式不同,遇到编译错误换一下导入方式就行。

2. 查询加载:从数据表到界面渲染

2.1 用RdbPredicates构造查询条件

增删改查里,“查”是使用频率最高的操作,页面一打开就要执行。RDB查询的核心是RdbPredicates,它相当于SQL里的WHERE子句,用来描述查询条件、排序规则和limit限制。

export class FieldInfo { id: number = -1; name: string = ''; area: number = 0; cropType: string = ''; soilType: string = ''; status: number = 1; } export async function queryFieldList(store: relationalStore.RdbStore): Promise<FieldInfo[]> { const predicates = new relationalStore.RdbPredicates('field'); predicates.orderByDesc('updateTime'); const resultSet = await store.query(predicates); const list: FieldInfo[] = []; while (resultSet.goToNextRow()) { const item = new FieldInfo(); item.id = resultSet.getLong(resultSet.getColumnIndex('id')); item.name = resultSet.getString(resultSet.getColumnIndex('name')); item.area = resultSet.getDouble(resultSet.getColumnIndex('area')); item.cropType = resultSet.getString(resultSet.getColumnIndex('cropType')); item.soilType = resultSet.getString(resultSet.getColumnIndex('soilType')); item.status = resultSet.getLong(resultSet.getColumnIndex('status')); list.push(item); } resultSet.close(); return list; }

这段代码有几个关键点。第一,orderByDesc('updateTime')会让最近更新的地块排最前面,用户改完数据重新加载列表时,一眼就能看到改动过的记录。第二,query返回的是ResultSet结果集,它像游标一样指向当前行,必须调用goToNextRow()逐行遍历。第三,getColumnIndex拿索引、再通过对应类型的get方法取值,这个操作是常规操作,但取错类型会抛异常,比如area是REAL就得用getDouble,用getString就会崩。

ResultSet用完一定要close()。我见过不少新手漏掉这一步,短时间看不出问题,连续增删改查几次之后数据库连接耗尽,页面直接卡死。这是RDB开发里最常见的坑之一,后面排坑部分还会细讲。

2.2 数据模型与列表卡片

把查询结果封装成FieldInfo这样的数据模型类,UI层操作起来就很清爽。ArkTS对属性类型要求比较严格,类属性必须初始化,所以id默认-1、name默认空字符串,避免出现“未初始化属性”的编译问题。

列表UI我建议直接用List + ForEach来实现。每个地块用一张卡片展示,卡片里放名称、面积、当前作物,右侧放状态标识。卡片式展示比纯文本列表更符合农业管理场景,地块信息一眼扫过去就能看清。

@Builder FieldCard(item: FieldInfo) { Row() { Column({ space: 4 }) { Text(item.name) .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${item.area} 亩 · ${item.cropType ? item.cropType : '未种植'}`) .fontSize(13) .fontColor('#666666') } .alignItems(HorizontalAlign.Start) Blank() Text(item.status === 1 ? '进行中' : '暂停') .fontSize(12) .fontColor(item.status === 1 ? '#2E7D32' : '#999999') .padding({ left: 8, right: 8, top: 2, bottom: 2 }) .backgroundColor(item.status === 1 ? '#E8F5E9' : '#F5F5F5') .borderRadius(4) } .padding(16) .backgroundColor(Color.White) .borderRadius(12) .width('100%') }

这里用@Builder封装卡片组件,主页面build里通过ForEach循环调用。每张卡片的key用item.id.toString(),保证了列表项的唯一性,ArkTS的ForEach要求key必须唯一且是string类型,这是规范。

2.3 空状态和页面刷新时机

列表页最容易被忽略的是空状态。地块刚初始化时数据库一张表可能是空的,没有数据时页面直接白茫茫一片,用户还以为应用出bug了。我习惯在List外层判断:

if (this.fieldList.length === 0) { Column({ space: 8 }) { Text('暂无地块数据') .fontSize(16) .fontColor('#999999') Text('点击右上角“新增地块”开始添加') .fontSize(13) .fontColor('#BBBBBB') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) } else { List({ space: 12 }) { ForEach(this.fieldList, (item: FieldInfo) => { ListItem() { this.FieldCard(item) } }, (item: FieldInfo) => item.id.toString()) } .layoutWeight(1) .width('100%') .padding({ left: 12, right: 12, top: 4 }) }

刷新时机也很讲究。页面onPageShow生命周期里重新加载数据是最稳妥的做法,因为用户从编辑弹窗返回、从详情页返回时都需要重新拉一遍列表。如果你只在aboutToAppear里加载一次,改完数据再返回就只能看到旧数据,体验很差。我之前就犯过这个错,后来统一改成onPageShow里刷新,问题直接消失。

3. 新增与编辑:一个弹窗两套逻辑

3.1 表单弹窗的复用设计

新增和编辑在UI层面高度相似:都要填地块名称、面积、作物、土壤类型,只是编辑时表单里要先回填数据,保存时走update而不是insert。与其写两个弹窗,不如用一个弹窗组件加两个模式状态。我习惯定义三个状态变量:

@State isEdit: boolean = false; @State editingId: number = -1; @State showFormDialog: boolean = false;

新增时,isEdit设为false,editingId重置为-1,表单字段全部清空。编辑时,先从列表里取出要编辑的地块信息,回填到name、area这些输入框里,然后isEdit设为true、editingId设为该地块的id。

弹窗里用TextInput、TextArea、Select这几个组件就够了。TextInput用于名称和面积,Select搞一个下拉选择土壤类型,可选值像“壤土”“沙土”“黏土”这些,作物类型用TextArea或者TextInput都行,看自己输入习惯。

if (this.showFormDialog) { AlertDialog.show({ title: this.isEdit ? '编辑地块' : '新增地块', message: '', autoCancel: false, alignment: DialogAlignment.Center, customStyle: true, builder: () => { Column({ space: 12 }) { TextInput({ placeholder: '地块名称', text: this.fieldName }) .onChange((value: string) => { this.fieldName = value; }) .width('100%') TextInput({ placeholder: '面积(亩)', text: this.fieldArea }) .type(InputType.NUMBER_DECIMAL) .onChange((value: string) => { this.fieldArea = value; }) .width('100%') TextInput({ placeholder: '种植作物', text: this.cropType }) .onChange((value: string) => { this.cropType = value; }) .width('100%') Row({ space: 10 }) { Button('取消') .backgroundColor('#EEEEEE') .fontColor('#333333') .layoutWeight(1) .onClick(() => { this.showFormDialog = false; }) Button('保存') .backgroundColor('#2E7D32') .layoutWeight(1) .onClick(() => { this.handleSave(); }) } .width('100%') } .padding(20) .width('100%') } }) }

AlertDialog用builder加载自定义内容时,customStyle要设为true,否则弹窗背景和默认样式可能会打架。autoCancel设为false是为了防止用户点弹窗外区域误关弹窗,表单填一半被关掉确实挺烦人的。

3.2 输入校验与数字处理

校验是表单里绝不能省的一步。地块名称是必填,面积必须是大于0的数字,我见过用户直接输入中文或负数的情况。校验逻辑我单独提取成函数,方便复用:

validateField(name: string, areaStr: string): boolean { if (!name.trim()) { promptAction.showToast({ message: '请输入地块名称' }); return false; } if (areaStr.trim() === '') { promptAction.showToast({ message: '请输入面积' }); return false; } const area = Number.parseFloat(areaStr); if (Number.isNaN(area) || area <= 0) { promptAction.showToast({ message: '面积必须是大于0的数字' }); return false; } return true; }

调用promptAction需要import { promptAction } from '@kit.ArkUI'。TextInput的类型设置成InputType.NUMBER_DECIMAL之后,弹出来的键盘是数字键盘,Shift键是减号,但用户还是可能输入乱码,所以parseFloat之后的NaN判断必须保留。

这里还有一个容易忽略的点:parseFloat("12abc")不会报NaN,它会解析出12。也就是说用户输入“12亩”这样的内容,parseFloat也能拿到12。看起来是好事,但对业务来说其实是脏数据进入系统的漏洞。我在处理时会在校验前先判断正则:/^\d+(.\d+)?$/,只允许纯数字格式。不过普通场景下parseFloat配合NaN判断也够用,看你对数据的洁癖程度。

3.3 保存逻辑:insert和update的分流

保存按钮的onClick统一走handleSave方法,内部根据isEdit分流。新增走insert,返回的是新记录的rowId;编辑走update,通过predicates限定id更新对应行。

async handleSave() { if (!this.validateField(this.fieldName, this.fieldArea)) { return; } const area = Number.parseFloat(this.fieldArea); const now = new Date().toISOString(); const values: relationalStore.ValuesBucket = { 'name': this.fieldName.trim(), 'area': area, 'cropType': this.cropType, 'soilType': this.soilType, 'status': this.isEdit ? this.editingStatus : 1, 'updateTime': now, }; try { if (this.isEdit) { const predicates = new relationalStore.RdbPredicates('field'); predicates.equalTo('id', this.editingId); await this.rdbStore.update(values, predicates); } else { values['createTime'] = now; await this.rdbStore.insert('field', values); } this.showFormDialog = false; this.refreshList(); promptAction.showToast({ message: this.isEdit ? '修改成功' : '新增成功' }); } catch (err) { hilog.error(TAG, `save field failed: ${JSON.stringify(err)}`); promptAction.showToast({ message: '保存失败,请重试' }); } }

注意ValuesBucket的key必须和表的字段名完全一致,大小写也不能错。之前有同事把name拼成nmae,编译不报错,跑起来才报“column nmae does not exist”,排查半天。insert方法返回的是自增的rowId,如果表里有自增之外的特殊需求还可以拿到这个id做后续关联,但地块这个场景不太用得上。

保存成功之后要做什么?关闭弹窗、重新拉列表、Toast提示。这三个动作顺序不能乱:先关弹窗,再刷新,最后提示。你先刷新再关弹窗,用户会看到弹窗底下列表闪了一下;你先提示再刷新,提示文字还没消失列表变了,视觉上很突兀。按顺序来体验最顺。

4. 删除操作:确认、执行、刷新三步走

4.1 为什么删除前要加确认弹窗

删除操作是四个动作里破坏性最强的。用户手指一抖点了删除,一整块地从数据库里就没了,没有后悔药。所以在删除前加一个确认弹窗是必须的,这既是交互规范,也是数据安全底线。

常见做法是卡片右侧放一个“更多”按钮,点开用ActionSheet弹出菜单,菜单里有“编辑”和“删除”两个选项。这样既避免了误触,又能把编辑入口和删除入口收拢在一起,页面看起来干净。如果直接在每个卡片上放一个大红色删除按钮,视觉上太难看了,而且误触概率很高。

4.2 删除确认弹窗的实现

HarmonyOS里弹确认框用AlertDialog.show最方便,支持两个按钮:取消和删除。删除按钮用红色字体,给用户足够的视觉警示。

confirmDelete(item: FieldInfo) { AlertDialog.show({ title: '删除确认', message: `确定要删除地块“${item.name}”吗?删除后不可恢复。`, autoCancel: true, primaryButton: { value: '取消', action: () => {} }, secondaryButton: { value: '删除', fontColor: Color.Red, action: () => { this.handleDelete(item.id); } } }); } async handleDelete(id: number) { try { const predicates = new relationalStore.RdbPredicates('field'); predicates.equalTo('id', id); await this.rdbStore.delete(predicates); this.refreshList(); promptAction.showToast({ message: '删除成功' }); } catch (err) { hilog.error(TAG, `delete field failed: ${JSON.stringify(err)}`); promptAction.showToast({ message: '删除失败,请重试' }); } }

message里拼接了地块名称,这个细节很重要。用户看到“确定要删除地块‘东区一号地’吗”,比看到干巴巴的“确认删除”要能唤起警惕心,误操作概率小得多。

autoCancel这里我设置为true,因为删除确认弹窗不需要强制用户做选择,点外部关闭相当于取消,符合用户直觉。

4.3 删除后的列表同步

删除成功后有两种刷新方式:一种是调用refreshList重新查询全表,简单粗暴但数据量大了会浪费性能;另一种是在本地数组里过滤出被删的那条,然后整体赋值触发UI刷新。

我用的方案是感知型的:先拿到被删除的id,然后把this.fieldList里不等于该id的项过滤出来,重新赋值。这样列表更新是即时的,不用等数据库重新查询。

refreshListAfterDelete(deletedId: number) { const newList = this.fieldList.filter((item: FieldInfo) => item.id !== deletedId); this.fieldList = [...newList]; }

这里有一个ArkTS状态管理的经典坑:直接this.fieldList.filter再赋值能触发刷新,但如果你在filter之后直接this.fieldList = filteredList,在某些版本上UI不刷新。最稳妥的写法是展开成一个新数组再赋值,让@State感知到数组引用发生了变化。我后面的排坑部分会展开说。

4.4 删除操作的扩展思考

当前删除是物理删除,数据直接没了。如果后续要支持“回收站”功能,可以在表里加一个deleted字段做逻辑删除,delete操作变成update操作,把deleted置为1。查询时统一过滤deleted=0的数据。这个改造并不难,关键是表结构设计时最好提前留好deleted字段,或者给现有表做一次表结构升级。

另外,如果地块下面还挂着农事记录、投入品记录,块删掉之后这些关联数据会变成孤儿数据。我在“高高种地”的规划里,后续会把农事记录表也建起来,届时删除地块时就要考虑关联数据的处理方案:要么联动删除,要么提示用户“该地块有X条农事记录,删除后将一并清理”。这个现在不用做,但设计时心里要有数。

5. 现场排坑:地块系统开发中遇到的6个典型问题

5.1 问题速查表

这块内容是我自己实际调试过程中踩过的坑,整理成表方便大家对照。

问题现象根本原因解决方案
连续增删改查后页面卡死ResultSet未关闭,数据库连接耗尽每次query后finally里close,或封装到工具类统一释放
修改数据后返回列表,UI不更新@State数组是直接修改而非整体替换用[...newList]或splice重新赋值,让数组引用变化
编辑弹窗回填的数据不显示弹窗内容builder里对@State的赋值时机不对回填逻辑必须在showFormDialog=true之前完成
数字输入框能输入字母TextInput未设置type为NUMBER_DECIMAL设置InputType.NUMBER_DECIMAL,并加入NaN校验
快速连续点击保存,插入多条重复数据异步插入未加防抖或保存中状态保存时加isSaving标志位,提交完成前禁用按钮
建表SQL反复执行报错未使用IF NOT EXISTS建表语句加上IF NOT EXISTS

5.2 三个最多人踩的坑详细展开

ResultSet不关闭的问题,我在刚用RDB时遇到过。当时是做一个简单的查询列表功能,写完了自测感觉没问题,但频繁切换页面、反复下拉刷新之后,应用开始越来越卡,最后直接无响应。排查日志才发现是query返回的结果集没关,底层连接一直被占用。这个问题的隐蔽性在于,小数据量下不关闭结果集可能运行几十次都不报错,但一旦多了就暴露。而且ResultSet是同步API,在异步方法里调用就得特别小心,异常分支也要记得关。我现在的习惯是建一个工具类封装所有query调用,用try/finally把close写死,从根上杜绝这个问题。

@State数组不刷新的问题,本质是ArkTS状态管理的对象引用机制。@State监听到的是属性或数组引用的变化,如果你直接修改原始数组的某个元素,比如this.fieldList[0].name = 'test',UI是不会动的。正确做法是生成一个新数组,然后整体赋给@State变量。我在项目里统一用this.fieldList = [...tempList]这种写法,Array的展开运算符会创建新数组,触发刷新。这个点很多教程没讲透,等你自己写列表业务时就会遇到。

连续点击保存按钮导致重复插入的问题,交互层就能解决。我习惯给保存按钮额外加一个isSaving状态,进入保存逻辑时置为true,保存完成或失败后再置为false。按钮在isSaving时禁用并且变灰,这样即使用户狂点,第二次点击也会被拦截。数据库层当然可以用唯一索引来防重,但地块名称允许重名的话,唯一索引不适用,所以前端拦截是性价比最高的方案。

还有一个编译层面的坑:API 12的RDB批量操作接口有些是老API已废弃,比如旧版本的executeSql传参方式,如果你是从网上抄的老代码,编译时可能会提示deprecated。遇到这种情况不要慌,看编译警告里的替代方案,改成新的store.executeSql(sql, args)写法就行。

6. 地块模块还能往哪扩展

增删改查做完,地块管理系统的骨架就完整了。但一个智慧农业应用的地块模块,可以做的远不止这些。我整理几个接下来值得扩展的方向,按优先级排序。

搜索和筛选项是性价比最高的扩展。地块多了以后,用户从几十个地块里找一个,光靠滚动列表会很累。加一个搜索栏,按名称模糊查询;再加一个状态筛选Tab,比如“进行中”“暂停”“全部”。实现上RDB原生就支持,RdbPredicates有like方法和equalTo方法,组合起来就是一条带条件的查询语句。这个功能我已经在下一版计划里了。

分页加载也是地块数量过百之后必须面对的。现在这个实现是一次性把整个表查出来,几百条数据还好,上千条就会明显拖慢列表滑动。RdbPredicates里可以用limit和offset,实现上拉加载更多。注意跟搜索条件、排序条件组合使用,避免分页状态下数据顺序错乱。

统计和图表是我个人最看重的扩展。地块数据攒起来之后,可以统计总面积、每种作物的种植面积占比、各土壤类型的分布情况。HarmonyOS有图表组件库可用,把查询结果聚合一下,往图表组件里一塞就是可视化大屏的雏形。这部分我打算等农事记录模块做完再一起搞,因为单看地块静态数据,统计维度还是太单薄,等跟产量、投入数据关联起来,统计才有实际价值。

还有一环是跟农事记录联动。未来每个地块可以挂多条农事记录,浇水、施肥、打药都记一笔。要支撑这个,现在的地块表结构是够用的,因为农事记录表通过fieldId关联到地块表,不需要改表。这也印证了开头那句话,建表时字段设计多想一步,后面功能扩展就能少改好几处。

写在最后

这块写到最后,我想分享一个自己养成的习惯:凡是涉及数据库读写的方法,我都会把结果封装成数据模型返回,而不是直接把ResultSet抛给UI层。这次地块模块的增删改查能写得这么顺,靠的就是这个习惯。UI层永远面对干净的FieldInfo数组,数据库细节全部收在数据操作层里,后面换存储方案、改表结构,UI代码基本不用动。下一篇我们开始做农事记录管理,那块会比地块系统更复杂一些,但套路是相通的——先设计好表结构,再写增删改查,最后补交互细节。感兴趣的可以先把手里的地块模块多点点、多跑跑,有报错的欢迎把日志贴出来,我们一起看看。

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

PixVerse广告视频生成:语义保真度与运镜控制实战指南

1. 项目概述&#xff1a;这不是一个“邀请链接”&#xff0c;而是一次广告创意工作流的深度重构“PixVerse 邀你探索广告杰作世界”——看到这个标题&#xff0c;我第一反应不是点开链接&#xff0c;而是立刻在笔记本上画了三栏表格&#xff1a;左边写“传统广告制作流程”&…

作者头像 李华
网站建设 2026/10/14 4:40:16

PyTorch模型创建与nn.Module核心机制详解

说真的&#xff0c;深度学习做到第9篇&#xff0c;大部分人的“分水岭”就出现在这一步&#xff1a;模型到底是怎么被PyTorch创建、管理&#xff0c;又真正跑起来的。前面用现成网络训练还好&#xff0c;一旦要自己改结构、加层、写自定义模块&#xff0c;就绕不开nn.Module这个…

作者头像 李华
网站建设 2026/10/14 4:39:13

Java操作Domino实战:Session、视图遍历与资源回收避坑指南

简介&#xff1a;《Domino开发指南精华》面向企业级应用开发者与Lotus Notes/Domino初学者&#xff0c;聚焦Java集成开发这一核心方向&#xff0c;帮助读者解决数据库操作、文档管理、视图控制与代理自动化等实际开发问题。资源包内含1个PDF文件&#xff0c;整体约3.82MB&#…

作者头像 李华
网站建设 2026/10/14 4:38:47

PHP反序列化漏洞入门:从源码泄露到__wakeup绕过实战

拿到这道题&#xff0c;我先说结论&#xff1a;它几乎是Web方向新手接触PHP反序列化漏洞最顺滑的一道练手题。标题里“PHP”三个字已经剧透了主要考点&#xff0c;但真正做完你会发现&#xff0c;它还顺带把源码泄露排查、魔术方法利用、任意文件读取这几件事串成了一条完整的学…

作者头像 李华
网站建设 2026/10/14 4:37:37

Unity应用加密实战:三层防御体系与工程化落地指南

1. 项目概述&#xff1a;为什么Unity App加密不是“加个壳”就完事了“Unity app加密”这五个字&#xff0c;最近在开发者社区里出现频率陡增——不是因为技术突飞猛进&#xff0c;而是因为踩坑的人实在太多了。我接触过不下二十个团队&#xff0c;有刚毕业的独立开发者&#x…

作者头像 李华