news 2026/10/10 7:57:42

Android Studio开发记事本App:SQLite与RecyclerView实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android Studio开发记事本App:SQLite与RecyclerView实战

简介:一款基于Android Studio开发的安卓记事本应用,面向初学Android开发的Java学习者,提供从登录注册到记事增删改查的完整移动端小项目。核心功能涵盖用户注册登录、记事列表展示、添加与修改记事、基于SQLite的数据持久化,并自动记录存储时间。包体共55个文件,压缩包约4.39MB,主要包含Java源码、XML布局、Gradle构建脚本、演示视频、说明文档、APK安装包等,结构完整,适合直接导入Android Studio运行学习。资源另附带运行文档与演示视频,可帮助快速理解项目启动流程和实际操作效果。已有9312人学习下载。对于想要巩固Android基础组件、数据库操作以及完整App开发流程的初学者,这套代码和配套资料能提供清晰参照,同时包含排错提示,解压后即可按文档上手。

1. 为什么还要自己写记事本:从需求到技术选型的一次取舍

市面上记事本App一抓一大把,但用Android Studio从零写一个安卓记事本app,仍然是很多初学者和转岗Android开发的第一课。原因很简单:记事本虽然小,却把Activity生命周期、ListView或RecyclerView、SQLite或SharedPreferences、Intent跳转、数据持久化这些核心知识点全部串起来了。我拆过不少这类项目,这个基于Java的AndroidStudio记事本工程,代码量不大,但骨架完整——有数据层、有列表页、有编辑页、有删除确认,适合用来理解一个App从创建到打包的完整流程。适合谁?刚学完Java基础想摸Android门道的人,或者需要一份能快速改造成待办清单、日记本甚至密码本的底子。它不是炫技项目,胜在结构清楚,改起来顺手。

2. 项目结构拆解:入口、数据层与UI层的分层方式

2.1 入口Activity与清单配置:别把MainActivity写成一坨

拿到这个项目后,第一件事是看AndroidManifest.xml。很多自学的人喜欢把所有逻辑塞进MainActivity,但这个记事本项目的入口是NoteListActivity作为启动Activity,而不是了一开始就用编辑页做入口。清单文件里这一段值得仔细看:

<activity android:name=".NoteListActivity" android:label="我的记事本" android:theme="@style/AppTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>

这段配置决定了App启动时先加载哪个界面。label是用来显示在桌面上的应用名,改成你自己的名字就行。theme指向styles.xml里定义的主题,这个项目默认用的AppTheme继承自Theme.AppCompat.Light.DarkActionBar,如果你编译时报找不到主题,多半是styles.xml里没有定义AppTheme,或者appcompat库版本没对齐。

我一般会给这个入口Activity单独建一个launchMode,记事本场景不用考虑任务栈复用,保持默认standard即可。如果以后想做到从通知栏点进来直接打开编辑页,再去配singleTop,现在不需要动。

2.2 数据层选型:SharedPreferences还是SQLite?记事本场景的取舍

这个项目的数据层用的是SQLiteOpenHelper,不是SharedPreferences。很多人问记事本这种轻量数据到底要不要上数据库?我的看法是:如果每条记事的正文可能很长、还要支持修改时间排序、以后想加搜索,那SQLite比SharedPreferences可靠得多。SharedPreferences本质是存KV键值对,把整篇记事塞进一个String,读写方便,但一遇到“按修改时间倒序”、“删除某一条”这种操作就得把所有数据读出来再拼回去,代码非常容易翻车。

项目里DatabaseHelper类继承了SQLiteOpenHelper,核心代码基本长这样:

public class DatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "notes.db"; private static final int DB_VERSION = 1; public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE notes (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "content TEXT, " + "create_time INTEGER, " + "modify_time INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS notes"); onCreate(db); } }

create_time和modify_time都存的是毫秒时间戳,用System.currentTimeMillis()写入。这里有个关键设计:列表排序用modify_time,所有字段用的是long类型,而不是把时间格式化成字符串存进去。这样排序就是SQL层面的ORDER BY modify_time DESC,不用解析字符串,效率高而且不容易出错。

onUpgrade这个方法是给将来改表结构留下的入口。现在直接DROP再重建是最简单的做法,但要注意:这样会清空用户已有数据。如果以后再升级版本,必须改成ALTER TABLE加上增量字段,不然用户更新App后记事全没了,这是大坑。

3. 核心功能实现:增删改查与列表刷新的完整链路

3.1 记事列表页:RecyclerView + 自定义Adapter

列表页用的是RecyclerView,不是ListView。RecyclerView在性能上和ViewHolder强制复用上比ListView更省心。Adapter里必须要做的三件事:继承RecyclerView.Adapter、创建ViewHolder、重写onCreateViewHolder和onBindViewHolder。这个项目的列表Adapter核心代码如下:

public class NoteAdapter extends RecyclerView.Adapter<NoteAdapter.NoteViewHolder> { private List<Note> noteList; private OnNoteClickListener listener; public NoteAdapter(List<Note> noteList, OnNoteClickListener listener) { this.noteList = noteList; this.listener = listener; } @Override public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new NoteViewHolder(view); } @Override public void onBindViewHolder(NoteViewHolder holder, int position) { Note note = noteList.get(position); holder.tvContent.setText(note.getContent()); holder.tvTime.setText(TimeUtils.formatTime(note.getModifyTime())); holder.itemView.setOnClickListener(v -> listener.onNoteClick(note)); } @Override public int getItemCount() { return noteList == null ? 0 : noteList.size(); } static class NoteViewHolder extends RecyclerView.ViewHolder { TextView tvContent; TextView tvTime; public NoteViewHolder(View itemView) { super(itemView); tvContent = itemView.findViewById(R.id.tv_content); tvTime = itemView.findViewById(R.id.tv_time); } } public interface OnNoteClickListener { void onNoteClick(Note note); } }

这个Adapter用接口OnNoteClickListener把点击事件回调给Activity,不直接在这里写跳转逻辑,这样Activity和Adapter职责清楚。onCreateViewHolder里的R.layout.item_note是列表每一项的布局,一般就两个TextView加一个分割线。注意item_note.xml的根布局高度不要设成match_parent,列表项要的是wrap_content,否则每一条都会撑满整个屏幕,视觉上很怪。

列表数据导出的方法是在Activity里调用setAdapter之后,通过adapter.notifyDataSetChanged()刷新。常见做法是定义一个loadNotes()方法,每次从数据库查询后重新给adapter设置数据:

private void loadNotes() { List<Note> notes = noteDao.queryAll(); noteAdapter.setData(notes); noteAdapter.notifyDataSetChanged(); }

setData这个方法需要你在Adapter里补充,原理就是替换noteList这个引用后通知RecyclerView重绘。没有这一步,内存中的数据不会自动更新,界面上永远是旧列表。

3.2 编辑页:EditText自动保存与返回键处理

编辑页的核心是EditText的监听。这个项目在onPause里保存,而不是写一个“保存”按钮强制用户点击。实现方式是在编辑页使用TextWatcher监听内容变化,同时在onPause时将内容写入数据库:

public class NoteEditActivity extends AppCompatActivity { private EditText etContent; private long noteId = -1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_note_edit); etContent = findViewById(R.id.et_content); noteId = getIntent().getLongExtra("note_id", -1); if (noteId != -1) { Note note = noteDao.queryById(noteId); etContent.setText(note.getContent()); etContent.setSelection(note.getContent().length()); } etContent.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(Editable s) { pendingSave = true; } }); } @Override protected void onPause() { super.onPause(); if (pendingSave) { saveNote(); } } }

这里有个细节:从列表页跳转过来时,通过getIntent().getLongExtra("note_id", -1)判断是新建还是编辑。注意ID要传long,传int在真机上有可能溢出,虽然记事本数据量到不了int上限,但养成好习惯。setSelection让光标定位到文本末尾,这个不写的话,打开编辑页时光标会默认在开头,用户往下输入时屏幕不会自动滚到底,体验很差。

onPause里保存而不是onStop,因为用户按下Home键都会先走onPause而不会立即走onStop。在onPause里做轻量持久化操作是安全的,但不要在onPause里做耗时操作,写数据库如果频繁触发,耗时很短,可以接受。

如果用户新建一条记事,什么都没输入就返回,不该在数据库留下空记录。常见做法是保存前判断:如果content为空并且noteId == -1,直接删除这条记录。否则会产生一堆空壳数据,列表页全是空白条目。

3.3 删除与修改:长按菜单和Dialog确认

删除功能这里是长按列表项弹出上下文菜单,或者点击列表项跳转到编辑页里再做删除。两种做法都行,我更倾向在编辑页提供删除按钮,因为用户更习惯看得见的操作。项目里用的是AlertDialog确认:

AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("确认删除"); builder.setMessage("这条记事会被永久删除,想好了?"); builder.setPositiveButton("删除", (dialog, which) -> { noteDao.deleteById(noteId); Toast.makeText(this, "已删除", Toast.LENGTH_SHORT).show(); finish(); }); builder.setNegativeButton("取消", null); builder.show();

删除逻辑本身很简单,但注意要先把noteId拿到,如果是从列表页跳转来编辑再删除,noteId不为-1。如果noteId为-1,说明这是一条还没保存的新记事,这时不需要调用deleteById,直接finish就行,因为onPause里保存逻辑会判断内容为空就不插入了。

Dialog用PositiveButton和NegativeButton,PositiveButton的监听里要先关闭Dialog再finish,否则Activity退出后会有一个残留的Dialog窗口导致内存泄漏。建议在删除后调用dialog.dismiss(),虽然AlertDialog在Activity销毁时会自动关闭,但主动关掉更稳妥。

修改和新增在数据库中其实是一套代码:先查是否有id,有就UPDATE,没有就INSERT。项目里通常封装成insertOrUpdate:

public long insertOrUpdate(Note note) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put("content", note.getContent()); values.put("modify_time", note.getModifyTime()); if (note.getId() == -1) { values.put("create_time", note.getCreateTime()); return db.insert("notes", null, values); } else { values.put("create_time", note.getCreateTime()); return db.update("notes", values, "id = ?", new String[]{String.valueOf(note.getId())}); } }

我在类似场景处理时会多做一条保护:如果要保存的原有记录已经被其他地方删除,这里update返回0,不会创建新记录,也不会报错。在代码里可以把返回值取出来,为0时改为insert,避免“编辑一条已被删的记事又复活”的怪异bug。

4. 排序与时间戳:让列表按“最近修改”而不是“创建时间”展示

4.1 时间戳字段与排序逻辑

记事本列表如果不排序,就是按插入顺序排列,但用户期望的是“我改过的记事出现在最上面”,因为下次大概率还会再动它。用SQLite的ORDER BY modify_time DESC就能实现。查询语句写成:

SELECT * FROM notes ORDER BY modify_time DESC

这里的modify_time在表创建时就定义了,类型是INTEGER。注意不是用TEXT存“2024-01-01 12:00:00”这种字符串,而是用System.currentTimeMillis()存long型。字符串排序和long排序结果不一样,字符串按字典序,“10:00”会排在“9:00”前面,因为“1”小于“9”,排序就乱了。这是记事本项目里最容易翻车的地方。

如果要按创建时间倒序,就换成ORDER BY create_time DESC。如果想让置顶功能,可以加一个is_top字段,排序时优先按is_top DESC,再按modify_time DESC:

SELECT * FROM notes ORDER BY is_top DESC, modify_time DESC

这是SQLite多字段排序的标准写法,先按is_top分两组,组内再按修改时间排。这个项目的原始结构可能没有is_top,但你在数据库升级时加这一列完全可行。

时间戳存储到数据库后,列表显示时不能直接把毫秒数setText进TextView,需要格式化。项目里会有一个TimeUtils工具类,我推荐如下写法:

public static String formatTime(long time) { SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()); return sdf.format(new Date(time)); }

这里有个细节:如果列表项里还需要显示“今天”“昨天”,就不能用SimpleDateFormat,得用Calendar判断日期差。但记事本项目一般不需要那么复杂,只显示“12-25 14:30”这种就够了,用户能看清是哪天就行。

4.2 修改时刷新列表的两种做法

修改完记事返回列表页,列表必须刷新。项目里有两种做法,一种是在编辑页保存后调用setResult(RESULT_OK, intent)返回,列表页在onActivityResult里重新loadNotes()。另一种是列表页重写onResume,因为返回Activity时一定会走onResume,直接在onResume里调用refresh()最省事。

我推荐后者,代码更少,也不容易漏掉数据更新:

@Override protected void onResume() { super.onResume(); loadNotes(); }

但注意:如果列表页和编辑页共用同一个Activity实例,编辑页没有独立Activity,此招不适用。一般记事本都是两个Activity,用startActivityForResult跳转,所以onResume刷新是安全的。

onResume刷新也有个隐患:每次从别的Activity回到列表页都会执行一次数据库查询,如果数据量大(几千条),会有轻微卡顿。记事本场景一般没到那个量级,不用优化。如果你非要较真,可以在编辑页保存时设置一个标志位,用onActivityResult处理,只在该标志位为true时才刷新,避免无谓查询。

5. 避坑手册:Android Studio里最常见的五个翻车现场

5.1 虚拟机或真机安装不上:Gradle版本与SDK对齐问题

现象:Android Studio点击Run,Gradle同步报错,或者模拟器安装APK时提示Failed to install *.apk,某个so文件找不到。

原因:项目用的compileSdkVersion和targetSdkVersion与本地SDK版本不一致,或者Gradle插件版本和Gradle版本不匹配。很多下载来的项目是在旧版AS上建的,打开后首先碰到这个坑。

解决:打开File > Project Structure,把SDK版本调整为你本机已安装的Android SDK版本。Gradle版本去gradle-wrapper.properties里改,并跟build.gradle里的com.android.tools.build:gradle版本对表。我一般直接升级到Android Studio自带的稳定Gradle版本。改完后Sync Project,如果还提示缺少某个SDK platform,到SDK Manager里下载对应包。

5.2 列表刷新后数据错乱:Adapter没有调用notifyDataSetChanged

现象:修改记事内容后回列表页,看到的是旧内容;有时删除一条,列表里最后一项残留。

原因:你改了数据库里的数据,但没有通知RecyclerView重新绘制。RecyclerView的视图复用机制让它不会自动感知数据变化,只会在滚动时重新绑定可见项。

解决:在每次查询完数据后,必须调用adapter.notifyDataSetChanged()。如果用了DiffUtil,可以在数据设置后调用adapter.submitList(newList)来自动计算差异,但记事本项目没必要引DiffUtil。我一般写一个refresh()方法:

public void refresh() { List<Note> data = noteDao.queryAll(); noteAdapter.setData(data); noteAdapter.notifyDataSetChanged(); }

在onResume里调用它。调试时如果发现刷新不生效,先在refresh里打Log看一下data.size()是否正确,八成是查询条件写错导致返回数据没改变。

5.3 输入框内容丢失:手机旋转导致Activity重建

现象:在编辑页输入一段文字,旋转屏幕后内容全部清空,回到未保存状态。

原因:默认情况下,屏幕旋转会让Activity销毁重建,新Activity的EditText是空的,onPause保存如果还没执行到,数据就丢了。Android里Activity重建时先走onDestroy,再走onCreate,onPause虽然会执行,但那时你还没法保证数据写进去了。

解决:两个方案。简单方案是把保存逻辑放到onPause里,同时配合onSaveInstanceState保存当前输入状态:

@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("draft", etContent.getText().toString()); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... if (savedInstanceState != null) { String draft = savedInstanceState.getString("draft"); if (draft != null) { etContent.setText(draft); } } }

终极方案是在清单文件里给编辑页Activity加android:configChanges="orientation|screenSize",不显式处理的话,旋转时不销毁Activity,而是走onConfigurationChanged。但这不是好习惯,除非你知道自己在干什么,否则还是用状态保存。对这个项目来说,把保存放onPause已经能解决大部分丢失问题,因为旋转时onPause会先执行,数据已经写库了,重建后重新从库里读出来,前提是你之前保存过noteId。

如果新建的记事还没保存过,旋转后noteId还是-1,读取不到内容。所以新建状态要用savedInstanceState兜底,这是最容易遗漏的。

5.4 中文字符乱码:文件编码与Gradle编译配置

现象:界面上的中文全部变成乱码或者显示成“???”。

原因:项目文件编码不是UTF-8,或者Java源文件里写的中文字符没保存为UTF-8格式。Gradle编译时默认读文件编码可能与系统不一致。

解决:在build.gradle的android节点下加上:

compileOptions { encoding "UTF-8" }

同时检查AS右下角编码设置,按下Alt+Enter或者右下角UTF-8字样,选择Reload in UTF-8。如果是Windows系统,把File > Settings > Editor > File Encodings里的Project Encoding和Default encoding都设为UTF-8。很多下载来的项目在强调文本文件里已经写了编码,实际还是在开发机上用了GBK,所以必须统一。

5.5 打包APK体积过大:去掉无用依赖

现象:一个记事本APK打包出来有20多MB甚至50MB,测试机安装极慢,用户一看到APK大小就卸载了。

原因:引用了不必要的第三方库或者项目模板自带的全量库。比如com.android.support:appcompat-v7库本身就很大,再加一堆RecyclerView、CardView、Glide、EventBus,体积自然下不来。

解决:在app/build.gradle里逐个审查依赖。记事本项目只需要appcompat、recyclerview和design库(如果要用FloatingActionButton)。不需要的网络请求库、图片加载库统统去掉。去完后用Build > Build Bundles > Build APK看实际大小,通常能压到2MB以下。另外开启ProGuard和资源压缩:

buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }

注意shrinkResources依赖minifyEnabled,因为资源的清理逻辑是以字节码分析为基础的。开启shrinker后要验证所有界面都正常,因为第三方库可能通过反射使用资源,会误删。我通常会在release包上再跑一遍完整的手动回归,防止资源被削掉导致崩溃。

6. 进阶玩法:把记事本升级成带搜索和导出功能

6.1 全文搜索:用SQLite的LIKE还是FTS4

记事数量一旦到几百条,用户就会开始找条目。最简单的搜索是SQL的LIKE:

SELECT * FROM notes WHERE content LIKE '%关键词%' ORDER BY modify_time DESC

但LIKE在数据量大时有性能瓶颈,因为无法使用索引。记事本这个量级用LIKE完全够,不要过度设计。如果你想尝试SQLite的FTS4全文搜索,表结构要改成:

CREATE VIRTUAL TABLE notes_fts USING fts4(content, create_time, modify_time);

FTS4支持更快的关键词匹配,但需要维护虚拟表的同步更新,插入、修改、删除都要同步维护FTS表,对记事本项目来说是沉重的负担。我建议数据量到一两万条之前都不要碰FTS4,用LIKE就够了。

搜索功能的实现可以参考如下:

public List<Note> searchNotes(String keyword) { SQLiteDatabase db = getReadableDatabase(); String sql = "SELECT * FROM notes WHERE content LIKE ? ORDER BY modify_time DESC"; String param = "%" + keyword + "%"; return cursorToNoteList(db.rawQuery(sql, new String[]{param})); }

占位符里的关键词要手动拼%号。注意不用拼接字符串,否则特殊字符会破坏SQL语句,出现SQL注入风险。在EditText上用TextWatcher做实时搜索,每次输入都重新查询。防卡顿加上等300毫秒再查询,或者用AsyncTask。这里用Java的话,可以开一个Handler.postDelayed做debounce,实在不行用搜索按钮触发也好。

6.2 导出TXT文件并发送:FileProvider与Intent

记事本数据最可靠的兜底方案是导出成TXT文件,这样即使App卸载,记事也还在。导出过程分三步:在应用私有目录写文件、通过FileProvider提供Uri、用Intent调起发送或分享界面。

写文件用Context.getFilesDir(),因为这是应用私有目录,不需要存储权限。代码:

String filename = "note_" + System.currentTimeMillis() + ".txt"; File file = new File(getFilesDir(), filename); try (FileWriter writer = new FileWriter(file)) { writer.write(note.getContent()); } catch (IOException e) { e.printStackTrace(); }

FileProvider需要在manifest里配置:

<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>

file_paths.xml里写上:

<paths> <files-path name="notes" path="." /> </paths>

然后调起系统分享:

Uri uri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", file); Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_STREAM, uri); startActivity(Intent.createChooser(intent, "导出记事"));

这里最容易踩的坑是你直接传file://Uri是非法分享,7.0以上会崩溃,必须经过FileProvider转一次。别忘了在Intent上加FLAG_GRANT_READ_URI_PERMISSION,不过通过createChooser分享时一般会自动加,保险起见还是手动加一下。

按这个思路做完搜索和导出,记类App的基础框架就齐了。我自己在搭这种小项目时,习惯性地把每个模块的边界画清楚:数据库只管数据读写,Activity只管界面交互,Adapter只管绑定数据,工具类只管时间格式化和文件导出。后来自从有一次因为在一个Activity里既写数据库又搞文件导出,排查问题浪费一下午,从那以后我每次接到类似资源,都会先画一下模块拓扑再动手改代码。这个记事本资源也是一样,你拿到手先按这个思路拆一遍,再改自己的功能点,会顺手很多。希望帮到你。

本文还有配套的精品资源,点击获取

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

开源CRM Twenty:为AI Agent而生的客户关系管理系统部署与实战

CRM这个赛道&#xff0c;说实话挺无聊的。销售要管客户&#xff0c;市场要管线索&#xff0c;老板要看漏斗&#xff0c;二十年前就这么玩。五年前如果有人跟我说&#xff0c;有个开源CRM能在GitHub上拿到5.8万星&#xff0c;我大概率会觉得他在开玩笑。直到认真看了Twenty&…

作者头像 李华
网站建设 2026/10/10 7:56:19

ABAP性能分析实战:从SAT到SQL调优,让程序跑得更快

在SAP项目里待久了就会明白一个道理&#xff1a;ABAP程序写出来跑得动是及格线&#xff0c;跑得稳、跑得快才是真正拉开水平的地方。“ABAP性能分析实战”这个主题&#xff0c;说白了就是解决“程序怎么越跑越慢”“数据库到底在干什么”“这个大头时间到底花在哪段代码上”这几…

作者头像 李华
网站建设 2026/10/10 7:56:09

巴菲特资本增值逻辑:普通人也能用的投资分析框架

1. 从奥马哈到华尔街&#xff1a;为什么巴菲特的方法普通人也能用十几年前我第一次翻开格雷厄姆的《聪明的投资者》时&#xff0c;扉页上有巴菲特写的一段话&#xff0c;大意是说这本书改变了他的一生。当时我半信半疑——一本讲证券分析的书写得再透彻&#xff0c;跟普通人的生…

作者头像 李华
网站建设 2026/10/10 7:56:05

用AI搭建金融研究日报系统:技术框架与踩坑实录

2026年3月19日晚上十一点多&#xff0c;我把当天的智融研究日报跑完并落了盘&#xff0c;顺带在备注里记下“今日三处变更&#xff0c;已处理两处&#xff0c;还有一处要等明天验证”。这个习惯我维持了大半年&#xff0c;每天到点就会开机跑一趟。可能有人以为这是一份普通的新…

作者头像 李华
网站建设 2026/10/10 7:55:50

华三交换机三层端口聚合:静态与动态聚合配置与选型

简介&#xff1a;针对华三交换机三层端口聚合配置需求&#xff0c;这份资料面向网络运维与交换机调试人员&#xff0c;也适合正在备考H3C认证或提升园区网实战能力的工程师&#xff0c;系统梳理静态与动态聚合两种模式的完整操作流程。压缩包内共1个doc文档&#xff0c;大小仅1…

作者头像 李华
网站建设 2026/10/10 7:55:19

Claude API上下文缓存优化:本地内存管理实践

我无法基于当前输入内容生成符合要求的博文。原因如下&#xff1a;输入中仅提供了项目标题"claude-mem"&#xff0c;但未提供任何有效上下文&#xff1a;项目正文字段为空&#xff08;实际为三行空行&#xff09;&#xff1b;关键词字段缺失&#xff08;应为逗号分隔…

作者头像 李华