注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Skills 是什么?如何用于 Agent 开发?

大家好,我是双越。wangEditor 作者,前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP,前端面试派 作者。 我正致力于两个项目的开发和升级,感兴趣的可以私信我,加入项目小组。 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、多人协同...
继续阅读 »

大家好,我是双越。wangEditor 作者,前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP,前端面试派 作者。


我正致力于两个项目的开发和升级,感兴趣的可以私信我,加入项目小组。



  • 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、多人协同编辑。复杂业务,真实上线。

  • 智语 AI Agent 智能体项目。一个智能面试官,可以优化简历、模拟面试、解答题目等。


本文总结了我最近调研和学习 SKILLS 的一些记录,帮助大家对 SKILLS 全面的学习和理解。


SKILLS 是什么


SKILLS 本质上就是组织 prompt 提示词。


不仅 SKILLS,很多包装得很复杂的 Agent 框架,剥开来看确实都在做"往 Prompt 里塞什么、怎么塞"这件事。


SKILLS 比传统 prompt 多了一个:按需注入。这样就能极大减少 token 使用量,关键是能省钱


基于什么发展而来?


Skills 的概念来源于以下几个演进脉络:


1. Prompt Engineering 的沉淀


早期开发者发现,同样的任务,提示词写法不同,结果差异巨大。经过大量试错后,好的提示策略需要被复用和共享,Skills 就是这种沉淀的容器。


2. RAG(检索增强生成)的思路迁移


RAG 是在运行时动态注入外部知识,Skills 借鉴了这个思路——在任务执行前动态注入过程性知识(How-to),而不仅仅是事实性知识。


3. Tool Use / Function Calling 的延伸


LLM 有了调用工具的能力后,下一步自然是让 Agent 知道**"什么场景用什么工具、怎么用得好"**,这正是 Skill 要解决的事。


4. ReAct / Chain-of-Thought 框架的落地需求


ReAct 等框架让 Agent 有了规划-行动-观察的循环能力,但每个领域的规划策略不同,Skills 为不同领域提供了领域专属的推理引导


Agent Skill = 将人类专家经验编码为 Agent 可读的操作手册,在运行时动态注入,让 Agent 像专家一样执行特定领域任务。


单一任务 Agent 使用 SKILLS 的价值


对于单一任务类型的 Agent,Skill 的核心价值——"按需选择合适的知识"——确实没有用武之地。


但 Skill 还有另一个价值维度值得考虑:知识的组织与维护,而不仅仅是"选择"。


即使是单任务 Agent,你仍然面临这些问题:



  • 这个任务的最佳实践怎么沉淀?

  • Prompt 写在哪、谁来维护、怎么迭代?

  • 新同事怎么快速理解这个 Agent 的设计意图?


这时候 Skill 文档的形式本身是有价值的——它是对 System Prompt 的结构化管理方式,而不只是动态检索的载体。


所以



  • 多任务 Agent:Skill 的动态检索能力是核心价值

  • 单任务 Agent:Skill 作为动态检索机制价值有限,但作为Prompt 知识库的组织形式仍有一定工程价值


如果单任务 Agent 的 Prompt 本身也很简单:那 Skill 机制确实基本没必要引入,直接写死在 System Prompt 里最省事


看几个 SKILLS 的例子


SKILLS 怎么写,可以看看几个官方的例子。


第一,在 Claude desktop 中内置了很多 SKILLS examples ,例如这个 MCP-builder


image.png


第二,看 SKILLS 官方给出的例子 github.com/anthropics/…


例如这个是 python 处理 PDF 文档的 SKILL


image.png


第三,Vercel 推出的 react best practice skills github.com/vercel-labs…


规定了 AI 编程工具如何正确的编写 React 代码。同理,现在 vue nextjs svelte 也都有了自己的 skills


image.png


实际体验 SKILLS


在 Claude desktop 中试一试,既然它有 MCP-builder SKill 那就让它开发一个 MCP server


使用 nodejs 技术栈帮我开发一个 MCP server ,
用于在 cursor 中连接 notion ,
可以查看和搜索我在 notion 中的文档和内容。

它会主动搜索 skills ,并识别合适的 skill ,然后按照 skill 里的步骤来操作


image.png


然后就卡在了这里,我一度以为这是卡死了。结果等待了大概 10-15 分钟,终于做完了


image.png


它说明了制作的步骤,使用方法,其中带有哪些 tools ,最后可以下载代码。


image.png


如果没有 skills ,纯靠笨拙的、混在一起的 system prompt ,很难想象 AI 能如此精细的开发一个任务。


SKILLS 和 Tools


Skills 中会定义很多脚本和命令,python 脚本,js 脚本,或者 shell 命令行等。


这些代码和 skill 其他文字一样,也会作为 prompt 被传递给 LLM ,而 LLM 无法直接执行这些代码。它们需要调用 agent 相关的 tools 来执行。


Skill.md 里的 Python 脚本

只是文本/模板

Agent 通过 Tool 执行

才真正产生结果

如果要自己开发一个 agent ,你需要自己定义执行 python 代码的 tool ,skill 中的 python 脚本才能被执行。


const executePythonTool = tool(
async ({ code }) => {
const tmpFile = `/tmp/script_${Date.now()}.py`;
fs.writeFileSync(tmpFile, code);

return new Promise((resolve) => {
exec(`python3 ${tmpFile}`, (error, stdout, stderr) => {
fs.unlinkSync(tmpFile);
resolve(error ? `执行失败: ${stderr}` : stdout);
});
});
},
{
name: 'execute_python',
description: '执行 Python 脚本',
schema: z.object({
code: z.string().describe('要执行的 Python 代码'),
}),
}
);

SKILL 中的“中间数据”


例如 SKILL 中有如下描述,这里的 JSON 数据就是一个中间数据,不是最终输出。


1. 先把文档转换为一个 JSON 格式;2. 再把 JSON 格式生成 表格 。

skill 的“中间数据”一般不需要管理,会把这个描述作为 prompt 一起传递个 LLM 统一由 LLM 处理。


但如果中间数据过大,需要提供一个 write_file tool ,让 LLM 调用,写入本地/服务器的临时文件。


Skill"写入 /tmp/intermediate.json"

LLM 理解这个意图,生成如下代码:
fs.writeFileSync('/tmp/intermediate.json', JSON.stringify(data))

LLM 调用 execute_nodejs Tool 执行这段代码

Tool 在服务器/沙箱环境里真正创建了这个文件

SKILLS 和 RAG


RAG 本质上也是一种信息注入机制,和 Skill 类似,但目的不同:


Skill   → 注入"怎么做"(操作规范、流程)
RAG → 注入"知识内容"(业务数据、文档库)

所以 RAG 在架构里的位置是:


用户输入

[RAG 检索] ──→ 从知识库找相关内容 ──→ 注入 Prompt
[Skill 检索] ─→ 找对应操作规范 ────→ 注入 Prompt

[LLM] 结合两者生成计划

[Tool 执行]

RAG 和 Skill 是同一层次的东西,都在 LLM 推理前注入 Prompt。Skill 给方法,RAG 给数据,Tool 负责执行。


async function runAgent(userInput) {

// Step 1: 并行检索(RAG + Skill)
const [ragContext, skill] = await Promise.all([
retrieveFromRAG(userInput), // 检索业务知识
retrieveSkill(userInput), // 检索操作规范
]);

// Step 2: 组装 Prompt
const systemPrompt = `
你是一个智能助手。

<knowledge>
${ragContext}
</knowledge>

<skill>
${skill}
</skill>
`
;

// Step 3: LLM 推理 + Tool 执行
const agent = createReactAgent({ llm, tools, prompt: systemPrompt });
return agent.invoke({ input: userInput });
}

说一个真实的例子,用户问"把我们公司 Q3 销售数据生成 Word 报告"


RAG 检索
→ 从公司文档库找到 Q3 销售数据.csv 的内容
→ 注入 Prompt:"以下是相关数据:销售额 300万,同比+15%..."

Skill 检索
→ 匹配到 docx Skill
→ 注入 Prompt:"生成 Word 文档请遵循以下规范:..."

LLM 结合两者
→ 知道数据内容(来自 RAG)
→ 知道怎么生成文档(来自 Skill)
→ 生成代码调用 Tool 执行

没有 RAG,LLM 不知道 Q3 数据是什么。 没有 Skill,LLM 不知道怎么规范地生成 Word。 两者缺一不可。


如何在 Agent 中实现 SKILLS 功能


SKILLS 说白了就是一堆文本,而且除了 name description 之外没有其他约定。如果你自己开发 Agent 是需要自己实现 SKILLS 功能的。


以 langChain 框架为例子,要开发 agent 实现 SKILLS 主要有如下步骤。


第一,skills 存储,可以是本地文件,可以是数据库,反正都是文本。


/skills
/docx.md
/text-parser.md
/excel.md

第二,按需获取 Skill(按需动态检索),推荐使用向量检索,这样更适合配置多种 skill


const { MemoryVectorStore } = require('langchain/vectorstores/memory');
const { OpenAIEmbeddings } = require('@langchain/openai');

// 启动时把所有 Skill 索引进向量库
async function indexSkills() {
const skills = [
{ name: 'docx', content: loadSkill('docx'), desc: '生成Word文档' },
{ name: 'excel', content: loadSkill('excel'), desc: '生成Excel表格' },
];

const vectorStore = await MemoryVectorStore.fromTexts(
skills.map(s => s.desc),
skills.map(s => ({ name: s.name })),
new OpenAIEmbeddings()
);

return vectorStore;
}

// 按需检索最相关的 Skill
async function retrieveSkill(userInput, vectorStore) {
const results = await vectorStore.similaritySearch(userInput, 1);
return loadSkill(results[0].metadata.name);
}

第三,把检索出来的 Skill 注入到 prompt


const { ChatPromptTemplate } = require('@langchain/core/prompts');

async function buildPromptWithSkill(userInput, skill) {
const prompt = ChatPromptTemplate.fromMessages([
['system', `你是一个文档生成助手。

以下是你完成任务需要遵循的操作规范:
<skill>
{skill}
</skill>

请严格按照规范中的步骤和要求完成任务。`
],
['human', '{input}']
]);

return prompt.formatMessages({
skill: skill,
input: userInput
});
}

第四,定义必要的 tools 。上文有介绍。


第五,完成 agent 串联,包括 LLM toos prompt ,这就是 agent 最基础的结构了,


const { createReactAgent } = require('langchain/agents');
const { ChatOpenAI } = require('@langchain/openai');

async function runDocumentAgent(userInput) {
const llm = new ChatOpenAI({ model: 'gpt-4o' });

// 1. 按需加载 Skill
const skill = await retrieveSkill(userInput, vectorStore);

// 2. 构建带 Skill 的 System Prompt
const systemPrompt = `你是文档生成助手,严格遵循以下规范:
<skill>${skill}</skill>`
;

// 3. 创建带 Tool 的 Agent
const agent = createReactAgent({
llm,
tools: [generateDocxTool, installDepTool],
prompt: systemPrompt,
});

// 4. 执行
const result = await agent.invoke({ input: userInput });
return result;
}

// 调用
runDocumentAgent('把这段文字转成 Word 文档:...');

这个 agent 整体的工作流程如下:


用户输入

[Skill 检索] ──→ 读取 .md 文件 ──→ 注入 System Prompt

[LangChain Agent] ──→ LLM 规划步骤

[Tool 调用循环]
├── install_npm_package (环境准备)
├── generate_docx (执行代码)
└── 验证结果 / 自我修正

返回文件路径 / 下载链接

最后


以上就是我对于 SKILLS 及其周边功能的理解,如果你有新的见解欢迎留言补充。


作者:前端双越老师
来源:juejin.cn/post/7614331029458026531
收起阅读 »

OpenClaw 很爆火,但没人敢聊它的权限安全🤷‍♂️

最近掘金首页和排行榜基本被 OpenClaw 刷屏了😒。 大家都在发教程: 教你怎么用 腾讯云轻量服务器几分钟搭好环境 怎么连上 飞书 / 钉钉 / QQ Bot 甚至怎么让它自动处理工作流 多 Agent 打通小红书 作为开源框架,OpenClaw 确实...
继续阅读 »

image.png


image.png


最近掘金首页和排行榜基本被 OpenClaw 刷屏了😒。


大家都在发教程:



  • 教你怎么用 腾讯云轻量服务器几分钟搭好环境

  • 怎么连上 飞书 / 钉钉 / QQ Bot

  • 甚至怎么让它自动处理工作流

  • 多 Agent 打通小红书


作为开源框架,OpenClaw 确实做得不错的。尤其是升级到 v2026.3.2 之后:底层TypeScript 架构, ACP 调度系统,让它具备了非常强的扩展能力。很多人把它当做 个人效率工具用,其实完全没有问题。




但让我担忧的是:


很多文章都在引导开发者, 把它直接接入公司的业务流和内网里。


但我作为一个在 生产环境写了 9 年代码的开发者。我翻了热门的几十篇教程,发现一个非常奇怪的现象:


大家都在教 如何快速跑通流程 ,但却没人提醒这套系统进入企业内网之后的安全风险🤷‍♂️。


所以今天这篇文章:不聊配置,不聊提示词


只聊一件事:当你准备把 OpenClaw 接入团队业务时,必须想清楚的几个现实问题。




资源隔离与内存消耗


很多教程为了降低上手门槛,推荐的机器配置基本都是:2核 1G


image.png


但要注意一点:那只是跑一个空壳服务的成本🤷‍♂️。




当你真正开始在公司环境里使用时,比如:开启多 Agent 协作,并发解析内部 Excel 报表,批量处理长文档。


这时候会发生什么?Node 进程内存占用会直线上升!!!


如果你只是按照教程:



  • 直接部署在内网共享服务器

  • 没有容器化

  • 没有Cgroup 资源限制


那么在业务峰值的时候, CPU 和内存会被直接吃满。最终可能导致:



  • 同机部署的测试服务直接宕机

  • CI / 内部工具全部异常


所以正确做法应该是:Docker / K8s容器化隔离,配置 CPU / Memory 限额,不要和其他业务混跑。




危险的运行权限


为了让 Agent 拥有真正的执行能力。很多部署教程会默认做一件事:直接用 root 账户运行。


这是一个非常危险的操作。因为 OpenClaw 本身具备:读写本地文件,甚者可以调用外部脚本,执行系统命令等。


如果它同时还接入了:



  • 飞书

  • 钉钉

  • Webhook

  • 外部 API


而用户输入没有严格过滤。攻击者可能绕过模型的意图识别,直接让 Agent 执行:


rm -rf /
curl xxx
wget xxx

换句话说:攻击者有机会在你的内网服务器上执行 任何 Shell 命令!!!


所以使用沙箱环境,严格限制可访问目录,例如:


/data/ai_sandbox



插件市场的盲区


OpenClaw 的生态发展非常快。


插件市场Skills 里现在已经有:



  • 发票识别

  • Excel解析

  • CRM同步

  • 邮件处理

  • 自动报表


很多教程都在鼓励:去插件市场直接搜索安装。🤷‍♂️


但在企业开发环境里,这是一个非常典型的安全雷区。因为第三方插件的代码质量参差不齐。大部分插件没有经过安全审计, 没有经过任何企业级代码 review。


举个真实的风险例子:


你装了一个 发票解析插件Skills。它在读取公司财务数据的同时,完全可以偷偷做一件事, POST 数据到外部服务器。


如果企业没有源码审查,出站网络限制(尤其是在内网环境下), 那么公司敏感数据可能已经悄悄泄露。




公网暴露风险


很多团队希望 OpenClaw,接管这些通讯工具:



  • 钉钉

  • 飞书

  • QQ

  • 微信


这时候必须接收Webhook 回调。这意味着,服务器端口必须暴露在公网。然而我看了大量爆款教程从头到尾,几乎没人提到这些东西:防火墙策略,IP来源限制,反向代理,DDoS 防护


把一个内部自动化框架,直接暴露在公网,这是一个非常草率的决定😖。如果一旦发生恶意扫描 -> 漏洞利用 -> DDoS 攻击,这台机器就会变成内网被攻破的跳板🤷‍♂️。




OpenClaw 是一个非常优秀的开源项目。


但技术人最容易犯的一个错误就是,被工具的新鲜感冲昏头脑😁。


当你准备把它从个人玩具升级为团队基础设施,就必须把它当作高风险后台服务来对待。


我们真正需要做的事情包括:



  • 网络白名单

  • 权限降级

  • 数据沙箱隔离

  • 插件源码审查


这些后端运维的安全常识,一个都不能少。


如果团队没有精力做这些兜底工作,我其实更建议直接使用大厂的合规托管服务。


而不是盲目跟风把一个AI 自动化框架随便部署进公司内网环境😖。


你们怎么看?


谢谢大家.gif


作者:ErpanOmer
来源:juejin.cn/post/7615470791221395483
收起阅读 »

2026年的IT圈,看看谁在“裸泳”,谁在“吃肉”

Hello,兄弟们,我是V哥! 最近不少粉丝私信问我:“V哥,现在这行情卷得跟麻花似的,35岁危机就在眼前,你说咱们搞IT的,到了2026年还有出路吗?这技术迭代快得像坐火箭,我到底该往哪边押注?” V哥我就一句话:焦虑个屁!机会全是给有准备的人留着的。 你们...
继续阅读 »

Hello,兄弟们,我是V哥!


最近不少粉丝私信问我:“V哥,现在这行情卷得跟麻花似的,35岁危机就在眼前,你说咱们搞IT的,到了2026年还有出路吗?这技术迭代快得像坐火箭,我到底该往哪边押注?”


V哥我就一句话:焦虑个屁!机会全是给有准备的人留着的。


你们现在看是“寒冬”,V哥我看是“洗牌”。等到2026年,IT行业的格局早就翻天覆地了。那些只会写重复代码的“代码搬运工”确实该慌,但懂趋势、会借力的兄弟,那会儿绝对是香饽饽。


今天,V哥我就掏心窝子地聊聊,2026年咱们这行的几大“风口”。特别是最后两块大肉,听进去了,你下半年的年终奖就稳了。




一、 AI智能体开发:2026年的“新物种”


兄弟们,先把“ChatGPT”这种对话机器人放一边。V哥告诉你,2026年是AI智能体爆发的一年。


啥叫智能体?现在的AI像个博学的书呆子,你问它答。而智能体,那是带着“脑子”和“手脚”的打工人。它不仅能理解你的意图,还能自己拆解任务、自己去调用工具、自己反思纠错,最后把活儿干完了给你交差。



  • 现在是: 你写代码,AI帮你补全一行。

  • 2026年是: 你说“帮我做个电商后台”,智能体自己写代码、自己测、自己部署、甚至自己写文档。


V哥的研判:
到了2026年,不会开发智能体的程序员,就像2010年不会用智能手机的人一样落伍。你不需要自己去造一个大模型(那是大厂的事儿),你需要做的是做中间的“Controller”(控制器)。怎么用LangChain(或者那时候更牛的框架)把大模型串起来?怎么给智能体挂载API接口?怎么设计它的“记忆”和“规划”能力?


这块儿目前还是蓝海,谁能率先把“数字员工”搞定,谁就是那个省下百万人力成本的老板眼里的红人。





二、 鸿蒙开发:国产操作系统的“成年礼”


这块儿,V哥必须得敲黑板!这可能是未来几年里,中国普通程序员最大的红利期


别总盯着Android和iOS卷了,那是存量市场,杀得头破血流。你看华为现在的动作,HarmonyOS NEXT(纯血鸿蒙) 已经切断了对安卓代码的依赖。这意味什么?意味着这不仅仅是换个皮肤,这是一套全新的、独立的生态!


V哥的预言:
到了2026年,鸿蒙不再是手机的配角,而是全场景(手机、车机、家电、工控)的霸主



  • 技术栈: 赶紧把ArkTS(Ark TypeScript)学熟了,ArkUI这套声明式开发范式非常顺手。

  • 机会在哪? 现在市面上大量的APP都需要重构鸿蒙原生版。这中间有一个巨大的缺口!前两年进去的那批人,现在都成技术总监了。2026年,随着万物互联真正落地,鸿蒙开发者的薪资会比同级别的安卓开发高出至少30%。


V哥我一直说,技术要跟着国运走。鸿蒙这条路,不仅是写代码,更是在参与基础设施建设。这碗饭,香!





三、 后端开发:告别“CRUD”,拥抱“编排”


兄弟们,别再笑话写Java/Go的后端枯燥了。虽然简单的增删改查(CRUD)真的会被AI干掉,但后端的逻辑核心地位永远不会动摇


2026年的后端,不再是单纯的写接口,而是做“AI时代的管家”


以前你的服务是给前端APP用的,2026年,你的服务大部分是给上面的“AI智能体”用的。智能体需要调用你的数据库、调用你的业务逻辑。你的接口设计得更规范、更原子化、响应更快。


V哥建议:
Go语言和Rust会在后端越来越火(因为性能好、并发强)。而且,后端得懂点云原生,容器化、Service Mesh(服务网格)这些都是标配。你得学会怎么把一个庞大的系统拆得碎碎的,还能用AI把它们管得服服帖帖。





四、 前端开发:从“画页面”到“造体验”


前端死了吗?V哥告诉你,前端才刚刚开始“性感”起来。


写HTML/CSS这种活儿,2026年估计UI设计师直接说一句话,AI就生成了。那前端干嘛?前端负责“交互的灵魂”


随着WebGPU的普及,浏览器里能跑3D大作、能跑复杂的物理引擎。鸿蒙的ArkUI也是跨端的前端技术。未来的前端,更多是图形学、人机交互和3D可视化。你打开一个网页,不再是看图文,而是进入一个虚拟空间,这背后全是前端工程师的功力。


V哥一句话: 放下jQuery,搞深Three.js,搞透React/Vue原理,往图形学和全栈方向发展。





五、 嵌入式开发:软硬件结合的“硬核浪漫”


以前搞嵌入式感觉是“修收音机的”,2026年搞嵌入式那是“造智能机器人”的。


因为上面说的鸿蒙AI,最后都要落脚到硬件上。智能眼镜、智能家电、自动驾驶,哪个离得开嵌入式?


重点来了: 嵌入式未来会和AI深度融合,叫TinyML(微型机器学习)。在芯片上跑小型的AI模型,让摄像头能识别人脸,让传感器能听懂声音。如果你既懂C语言底层,又懂一点AI算法部署,你是各大硬件厂抢着要的“国宝”。





六、 大数据开发:从“存数据”到“喂AI”


大数据没凉,只是换了个活法。


前几年大家搞Hadoop、Spark,是为了存日志、做报表。2026年,搞大数据主要是为了给AI当“饲养员”


AI需要高质量的数据清洗、向量化处理。这就涉及到向量数据库、数据湖、实时计算流。怎么把企业的几十亿条数据,变成AI能看懂的“知识”,这是大数据工程师的新活儿。不懂AI的数据工程师,未来路会越走越窄。





七、 AI运维与 AI测试:机器管机器


最后说说这两个容易被忽视的领域。



  • AI运维: 以前服务器报警了,运维兄弟半夜爬起来看日志。2026年,AI运维系统会自动定位故障、自动修复、自动扩容。运维工程师不需要敲那么多命令了,而是负责训练这个“运维AI”,制定策略。这叫SRE(站点可靠性工程)的进化版

  • AI测试: 测试不仅是找Bug,更是“攻防演练”。用AI去生成几万条变态测试用例去轰炸你的系统,甚至用AI去对抗AI生成的代码。只有AI才能测出AI写的Bug。




V哥总结一下


兄弟们,2026年其实并不远。


V哥我看了一圈,未来的趋势就两个字:融合



  • 鸿蒙是万物互联的底座,必须要抓;

  • AI智能体是提升效率的神器,必须要懂;

  • 其他所有的后端、前端、嵌入式、数据,都要围绕着这两者去进化。


别再纠结Java还是Python,Go还是Rust了。语言只是工具,解决问题的思路才是王道。从今天起,试着用AI去帮你干活,试着去了解一下鸿蒙的ArkTS,试着把你的工作流程“智能化”。


等到了2026年,当别人还在为裁员瑟瑟发抖时,V哥希望看到你已经站在风口上,笑傲江湖!


我是V哥,带你不仅看懂技术,更看懂未来。


作者:威哥爱编程
来源:juejin.cn/post/7593139476839874566
收起阅读 »

阿里人的 2025 年终总结:买房、晋升、订婚、投资,遇见更清晰的自己

一、引言 又到了一年一度的年终复盘时刻。 复盘,从来不只是回看已经发生的事情,更重要的是——为尚未发生的未来,提前铺路、校准方向。 回望 2025 年,其实很长一段时间里,我始终没有真正找到自己的方向。工作之外,谈不上热爱,也谈不上笃定,只是在惯性中前行。 直...
继续阅读 »


一、引言


又到了一年一度的年终复盘时刻。


复盘,从来不只是回看已经发生的事情,更重要的是——为尚未发生的未来,提前铺路、校准方向。


回望 2025 年,其实很长一段时间里,我始终没有真正找到自己的方向。工作之外,谈不上热爱,也谈不上笃定,只是在惯性中前行。


直到 2025 年的尾声,才终于看清了一些东西:哪些是必须坚持的,哪些是可以放下的,也逐渐摸索出几条更属于自己的路径。这一年,并非突然开悟,而是反复碰壁后的沉淀与取舍。


依旧按照惯例,沿着时间的轨迹,回到 2025 年的起点,梳理这一整年里的得与失、进与退,也为下一阶段的自己,留下些什么。


二、技术(AI驱动)



2025 年,是 AI 加速演进的一年。


这一年里,我写博客明显少了,最直接的原因,正是 AI 带来的冲击。曾经需要熬夜查资料、翻文档、啃源码的事情,在今天的 AI 面前显得异常高效——只需要给出合适的关键词,答案往往已经被系统性地整理好。


从宏观来看,2025 年的主旋律,几乎全部围绕着大模型基础设施展开。各大厂商持续投入巨额资金,用于模型规模、算力和训练能力的迭代升级。但与之形成鲜明对比的是:​大模型的应用落地,依然是一个尚未被彻底解决的问题​。


这就像高速公路已经修好,但真正决定胜负的,是谁家的车最先跑起来、跑得最稳。


基于这一判断,我越来越坚定地认为:2026 年的主旋律,一定是 AI 的落地与应用。


对技术人而言,未来不再只是把业务“做深做熟”,而是在既有业务基础上,思考如何与大模型更好地结合、适配、放大价值——这或许才是更长远的终局。


纯业务型技术路线,势必会遭遇一定冲击。原因并不复杂:当业务格局趋于稳定,公司往往不会再在传统方向上投入重资产。努力当然重要,但在技术浪潮面前,​方向的优先级,永远高于努力本身​。风口之上,选择往往比埋头苦干更关键。


坦白说,我曾长期对 AI 保持距离。并非否定它的价值,而是因为它太新——标准未定、路径未明、变量太多。这种不确定性,本能地让人抗拒。


真正的转折,来自一次和老板的绩效沟通。在 AI 飞速演进的节点,如果只是站在一旁观望,等别人把位置站稳、红利吃完,再回头跟进,我们注定只能咀嚼别人剩下的成果,且永远慢一步。


从那次谈话之后,我逐渐接受了一个事实:AI 不是选修,而是必须正视的方向。


客观来说,2025 年我并没有系统性地学习太多 AI 知识。但我也越来越确信,学习本身并不是最难的事,真正重要的,是先选对方向。


立下 2026 年的第一个 Flag:持续系统性地学习 AI,构建完整的技术体系,至少让自己始终站在行业前列。


三、工作(Agent/晋升)



2025 年,几乎可以被视为 ​Agent 的元年​。


对风控工程师而言,这是一个难得的历史窗口期。大模型天然擅长图文理解,而内容风控的核心风险,恰恰集中在图文与多模态层面,二者之间的契合度,几乎是为内容风控量身定做。


在过去一年里,我们更多是在​夯实底层能力​:包括大模型与小模型的协同、向量检索体系、敏感词与规则匹配等基础设施建设。而今年,则顺应技术演进的方向,在此之上,开始系统性地推进​内容风控 Agent 的建设​。


从系统设计角度看,传统架构往往以同步模式为主。但在大模型时代,推理延迟不可避免地成为整体架构的瓶颈,在高 QPS 场景下,单纯依赖同步调用已难以支撑规模化落地。因此,架构形态也不得不随之演进,从同步模式逐步向异步化、任务化转变。


不过,相比架构调整,Agent 本身长期存在的两个核心问题更加关键:幻觉问题与​成本问题​。


对于幻觉问题,业界较为成熟的做法,是通过 Workflow 对大模型的行为路径进行约束,让其在预设的流程和边界内完成推理,从而降低不确定性。


而成本问题则更具挑战性。目前主流思路可以拆解为两个方向:一是降低大模型​​的调用频次​,二是​降低单次调用成本​。前者往往引入搜广推体系中的粗排 / 精排架构,由小模型或规则先行过滤,仅将必要样本交由大模型处理;后者则更多依赖自建推理服务,通过部署开源模型(如千问、DeepSeek 等)来降低整体成本。


更进一步来看,自建模型还有一个明显优势:当前开源模型大多是通用基模,而真实业务更需要的是​领域大模型​。这也意味着,必须通过微调、蒸馏等手段,构建并持续演进真正贴合业务的模型体系。


整体来看,这一阶段的工作,相比传统开发,多了更多的不确定性,但也因此更具挑战性和成长空间,是真正与 AI 深度接轨的一段经历。


也幸运地得到了老板的认可,拿到了来之不易的晋升名额,完成了职业生涯中的第二次晋升——上一次是在上一家公司。回头看,确实有运气成分,也更需要珍惜。


立下新年的第二个 Flag:2026 年,持续精进 Agent 架构能力,从整体设计到细节实现全面吃透,建立真正的体系化掌控力,同时对自己保持足够严格的技术要求,始终心存技术敬畏。


四、健康(运动/旅游)



又到了这个多少有些老生常谈的栏目。


坦率地说,2025 年的运动状态可以用全面崩盘来形容。不仅没有瘦下来,反而稳步增重,这一项只能毫不留情地给自己打一个最低分,甚至有点不忍直视。唯一能做的,大概只能把希望寄托在 2026 年,重新做人。


相比之下,生活并非全然失守。这一年,还是走了不少地方。公司团建去了昆明、大理,看到了心心念念的洱海;个人也陆续去了深圳、香港、天津、北戴河、南京等城市,在不同的节奏里切换视角,多少算是给自己留了一些喘息的空间。


也期待明年能继续把“在路上”这件事延续下去——更重要的是,明年对象终于有年假了。


立下新年的第三个 Flag:坚持运动,至少别再继续发胖;同时走进几个尚未抵达过的城市,让生活保持一点新的变量。


五、家庭(买房/订婚)



去年看过的房子,最终还是和对象商量后定了下来。位置在县城里算是比较成熟的小区,四面都有学校,整体价格也在可承受范围内。房贷已经还了一年,对日常生活的影响并不大。


唯一的遗憾是交付时间要到 2027 年,目前仍在建设中。不过好在并不着急,慢一点也无妨。地理位置也和去年设想的一样——我和亲姐选择了同一个小区、同一个单元、上下楼,只需要一分钟,就能“传送”到她家蹭饭。考虑到我和对象都不太擅长做饭,这个选择现在看起来,格外明智。


另一件重要的事情,是订婚。和对象谈了七年恋爱,最终还是给了彼此一个相对笃定、也让双方家庭都满意的答案。整个过程比预想中顺利得多,几乎没有在流程或观念上产生大的分歧。订婚前和订婚后,心态确实发生了一些变化——一种更明确的“我们已经是一家人了”的心理确认。


当然,现实层面仍然存在异地的问题。对象在老家县医院工作,我则在北京从事互联网相关工作。所幸的是,老家县城今年将开通直达北京的高铁,往返的成本和难度都会降低,这也算是一个不小的利好。


整体来看,家庭这一块在 2025 年算是比较稳定的一年,没有突发的波折,一切都在预期内向前推进。


立下新年的第三个 Flag:家庭层面继续顺其自然、稳步发展;如果房子能提前交付,也不排除提前去拍个婚纱照——不强制,慢慢来。


六、投资(美股/港股/A股)



2025 年,也算是我个人投资生涯的起点。


在长桥关门前开通了账户,在汇丰调整政策前办好了银彳亍卡,时间点刚好卡在一个“来得及”的窗口期。12 月份正式进入美股市场后,才真正意识到:投资这件事,本身就像一门需要长期修炼的技术。


这一阶段,最大的收获并不在于盈亏,而是对投资这件事的​认知重构​。至少有几件事,是必须补上的基础能力。


第一,是​仓位控制​。如何在不踏空的前提下,避免一次性打光子弹,是非常难的一件事。仓位过低,行情来了只能旁观;仓位过高,下跌时却失去补仓空间。合理的仓位,本质上是为了​尽可能长时间地留在牌桌上​。


第二,是​买卖策略​。不同股票对应不同策略:有的适合长期持有,有的更偏交易属性。对陌生标的,更需要分批建仓,区分观察仓、交易仓与核心仓,而不是一次性下注。


第三,是​情绪管理​。这一点的冲击远超预期。下跌时容易冲动补仓,情绪持续低落;上涨时又容易过度乐观,放松纪律。回头看,绝大多数错误操作,都不是判断失误,而是典型的“情绪杀”。


第四,是​交易复盘​。 每一笔交易都值得被复盘:究竟是基于清晰逻辑做出的决策,还是随市场情绪跟风的结果。如果连自己为什么买、为什么卖都说不清楚,那么这笔交易本身就是不合格的。


事实上,这些问题最终都可以归结为一个核心:是否拥有一套成熟、可执行、可复盘的交易系统。


只有在系统的约束下交易,才能尽可能屏蔽情绪干扰,让结果更多由概率而非情绪决定。


某种程度上,这和前面提到的技术路径并无本质区别——就像 2026 年是 Agent 的关键一年一样,投资领域同样需要系统化思维。在美股市场中,也有不少值得长期观察的优质标的,新的一年,我会重点关注:​谷歌、英伟达、台积电、特斯拉​。


立下新年的第四个 Flag:建立并持续迭代属于自己的交易系统,在控制风险的前提下,争取 2026 年实现 10% 的年度收益目标。


七、总结



2025 年过得很快,一转眼就到了年末。回头算了一下,自己已经毕业四年半了。


还记得刚毕业的时候,对周围的一切都充满好奇,对技术保持着最纯粹的热情。那时知道的不多,需要承担的事情也不多,反而简单、专注,也更容易感到快乐。


随着年龄和阅历的增长,不得不考虑的事情越来越多,责任与压力也随之而来,这是成长过程中无法回避的一部分。


回看从毕业到现在的这几年,多少还是带着一些运气。整体来看,很少在关键节点上做出错误选择,生活与事业也在持续向更好的方向推进。正因如此,也愈发珍惜当下所拥有的一切。


但运气从来不是全部。对我而言,更重要的是始终保持一种行动上的自觉—想到就去做,决定了方向,就尽力把事情做成。


接下来,也对 2025 年年初立下的 Flag 做一次完整复盘,给自己这一年的选择与投入,留下一份相对坦诚的答案。



回到终点,再看起点,会发现很多事情并非一蹴而就,而是在一次次选择中逐渐变得清晰。


这一年,没有奇迹,也没有偏航。有的,只是在关键问题上站对方向,在重要事情上持续投入。


花有重开日,人无再少年。2026,让我们顶峰相见。


如果你也对 后端架构中间件源码 感兴趣,欢迎添加博主微信:​hls1793929520​,一起学习,一起成长。


我是 ​爱敲代码的小黄,​阿里巴巴 Java 开发工程师,CSDN 博客专家,专注后端架构与中间件源码。



我从清晨走过,也拥抱夜晚的星辰。人生没有捷径,你我皆平凡。你好,陌生人,一起共勉。



作者:爱敲代码的小黄
来源:juejin.cn/post/7590699877630378022
收起阅读 »

2025 年: 一半无业游民、一半外包牛马

引言 「2025」 年就这么稀里糊涂的过去咯, 前不久正巧听到这期播客 《请收下这枚记录 2025 年的声音时光胶囊》 不禁感慨, 于世界而言「2025」这一年属实精彩: 年初的 Deepseek、宇树机器人爆火 特朗普上台后各种折腾, 关税战一度引起全球股...
继续阅读 »

引言


「2025」 年就这么稀里糊涂的过去咯, 前不久正巧听到这期播客 《请收下这枚记录 2025 年的声音时光胶囊》 不禁感慨, 于世界而言「2025」这一年属实精彩:



  • 年初的 Deepseek、宇树机器人爆火

  • 特朗普上台后各种折腾, 关税战一度引起全球股市暴跌, 而最后又好像啥也没发生一样

  • 后面泡泡马特爆火、小红书出圈...

  • 之后又有雷军塌房、西贝塌房、俞敏洪塌房...

  • 再之后又是美团、京东、淘宝三方外卖大战...

  • 最后高德闭关整了个用脚投票, 最后好像也没声了

  • 然后就是各大厂商的 AI 争夺战了

  • ...


然而这一切又和我有啥关系, 各种风口并没有让我踩到, AI 的出现也只会让我更难找到合适的工作, 2025 我依然过得平庸, 比起 2024 也没啥起色....


2026, 我的生活大概率还是不会有起色? 谁知道呢? 随便吧....


一、无业游民这半年


从去年九月份被裁后就一直在 GAP, 属实没想到这一 GAPGAP 了近一年....


不幸的 2024 又是失业又是车祸, 所以年初一直在处理工伤的事; 从劳动能力鉴定、出劳动能力鉴定结果、提交工伤赔偿申请到最后 💰 到账。折腾了三四个月, 跑了好几次社保局, 捡钱好像也没那么容易


image


当然最后结果是好的, 小赚了一笔。 钱到账前还计划着出去奢侈一把, 到账后还是不舍得花钱, 还是老老实实存着吧....


image


上半年除了折腾工伤这事, 其余时间就是准备面试咯, 这期间还尝试写日记、做 TODO...


image


然鹅并没有什么用, 回头过了下上半年写的内容, 每天写的最多的就是:



  • 去健身房

  • 煮饭

  • 没有面试机会

  • 焦虑

  • 今天又啥都没干

  • ....


是的失业这半年基本上也没干啥, 有面试机会就面试, 没有就宅家里和 “自律” 抗争...


最后自然也不会有出乎意料的结果, 破罐子破摔罢了。最后入职了一家外包公司, 主要是看给的薪资也还行就去了


image


二、 外包这半年


人生第一次外包工作, 好像也没想象🤔中那么差


image


有免费的零食吃、无限自助咖啡, 偶尔一天干它个三杯 😂)。过节也有各种礼盒、去了半年 T 恤、衣服、帽子、鼠标垫都不知道领了多少个了, 同时外包公司过节偶尔也有礼盒, 这过一次节领双份礼盒了都 👍


对了, 还有免费的早中晚餐。刚开始去还自己带饭, 后面杭州这边可以集体点餐了, 自此三餐就全包咯。晚餐不吃, 就带回去给对象, 第二天她带去公司当午饭吃, 一家人全靠公司养活了


唯一不好的就是得加班, 每天得 8.30 下班, 这样就更不想去健身了。下班到家都 9 点了, 如果去健身那么到家都得 10 点多了。不对, 还有就是没有年假, 这坑爹的外包公司必须入职满一年才给年假....


没有绩效压力, 纯牛马好像过得也挺爽的, 运气好的是还有机会接触到 Agent 相关的工作(虽然只是边缘的对接工作)


二、好好生活


2.1 见家长


人生进程 +1: 51 去了对象老家, 10 月份趁着周末带对象也回了趟我的老家, 一切都还挺顺利的。实际上好像也没想象中那么紧张, 在对象老家这一待就待了 5 天, 每天就是吃吃喝喝...


image


2.2 旅游


上半年一直忙着工伤和工作的事, 下半年也一直在上班, 所以这一年也没去几个地方。


image


五月份大学舍长结婚, 就和对象一起去了趟福建, 之后就顺便就绕道去厦门玩了几天。厦门不错, 就是物价忒贵了, 如果去鼓浪屿千万自带吃食, 要不然就等着被宰吧。住的酒店就挺不错的, 服务周到、还有个大天台...


image


之后就是十月份, 去了心心念念的重庆。不愧是山城啊, 有被惊到, 刚去就按错电梯了 🐶。和美食荒漠的杭州相比, 那重庆简直是美食天堂了吧。可惜了, 上了年纪了真真感觉没以前能吃了, 一吃多肚子就难受得很....


image


再之后又去了趟桐庐, 当然主要目的是对象在支付宝上白嫖了医美项目(只有那边能预约到), 顺带着就想着溜达溜达。景点就去了「石舍村」, 主要是淡季很多景点都闭园修整了, 在富春江溜达了两天就溜了, 江边一路风景倒是挺不错的。


2.3 健康


是不是人一到 30 往后, 身体毛病就开始多起来了。 上半年差点不能入职了, 主要就是体检「心电图」有点异常, 吓坏了都。后面第二天又跑医院重新做了一次, 还是有一点点「异常」又补了其他检查, 最后没发现问题, 让医生开了个证明, 才顺利入职。


image


可能是那几天生活作息不良引起的「心电图」波动, 但是那时才感觉真的老咯, 以前可不这样。并且还检查出血压偏高, 当然我血压偏高这事其实早就知道, 只是一直不重视而已。这次检查医生说我的一个心房好像偏大, 问是不是血压高, 我说是的, 然后就建议我吃药、减肥...


image


好咯, 谨遵医嘱。 现在就是每天食用降压药, 还给自己安排上了血压仪。


后面就开始尝试减肥了, 到目前为止已瘦 20 斤吧。大部分都是前面几个月减下来的, 主要最开始上班都是自己带饭, 做的也干净(基本水煮), 所以瘦得还挺快的。


后面公司有免费的早中晚餐, 就放弃自己带饭咯。 慢慢的就反弹了点, 人啊一破防就容易破罐子破摔, 现在也不关注体重了, 现在估计又反弹不少....


image


2.4 理财


今年大家都回本了吧 🐶。 毕竟今年大 A 表现这么好, 再不回本也太说不过去了。


image


今年我也可算回本了, 感谢大 A 🙇。往年我的理财账户是另一个支付宝, 只为了眼不见为净毕竟亏了那么多。今年做了个改变:



  • 将所有基金都转托管到常用的那个支付宝上

  • 追求稳健、分散, 并卖掉亏损大的

  • 大仓位放在债劵、固收、红利上

  • 还配置了点纳指

  • 最后只有一小部分放在科技上


这种做法是稳, 但注定不能赚太多。在今年行情这么好的情况下, 今年收益也就 8.25% 远远跑输大盘。慢慢来吧, 起码今年的收益可以把我一年的房租给 cover 掉, 还是挺满意的。


image


今年半场还进了股市, 折腾了大半年, 小赚 400 大洋 😆。小资金, 玩得不亦说乎....


image


今年黄金、白银也有点给力。黄金入场了, 可惜中场没拿住卖了, 后面又进场, 少赚了(虽然也没买多少)。后面白银出现套利的机会, 没整明白就瞎炒作, 导致耽误了好几天, 少套利了不少。我对象场外买了不少, 也因此错过了最高点(就差那么一天), 可惜了...


image


2.5 其他


七月份大热天搬了个家, 在这小区已经换了三个窝了, 第一个主要是楼上太吵了, 第二个就是有点贵外加顶楼夏天实在是热得不行。


这一个也不知道能坚持多久呢, 主要问题就是二房东总是要房东催好几次才肯给他房租, 房东都找上我了让我催二房东给钱, 还说明年不租给他了 😭


image


双十二对象整了个烘干机, 那是真后悔没早点整, 简直太香了。特别是对我这种租房党, 晚上洗个衣服, 白天出门上班前把衣服丢烘干机就完事咯。主要还能收集灰尘和毛絮, 烘出来的衣服看起来就和新买的一样。强力推荐, 免去晒衣服的烦扰, 而且这样常年也只需要买 2~3 套衣服就行 🐶


image


两个崽: 妹妹越吃越胖、就像头猪; 姐姐倒是自律得很, 这一年身材管理起码比我好太多了, 没胖没瘦一切如旧。年初二狗子还了场病, 一直流鼻涕(怀疑是鼻炎), 被整的够呛的。还担心今年冬天会不会复发, 现在看着应该不会了吧, 对了这家伙年初应该还被俺们给噶了。


image


一年 365 天, 说长不长说短不短


翻翻照片总会有些收获的...


image


三、工作与成长


3.1 工作


新的工作, 工作难度一般吧, 反正轻松驾驭! 就是整体会更难? 或者说是更乱, 工期赶加上一群外包一起开发的项目, 虽然说有正式员工在把控, 但是项目质量也很难起来。正式员工也只是参与初期的项目开发, 后面的维护基本也就不管了, 毕竟只有新项目、新玩意才能出绩效。


image


但总的来说还是有点新东西可以学到, 毕竟不同的团队他们的开发习惯、开发范式都是不同的。总是能看到一些新玩意, 即便有些玩意个人也不太喜欢, 但是也是可以玩一玩的。


后面也有幸参与了 vscode 插件相关工作, 这里也涉及到 Agent, 虽然只是一些简单的对接工作, 但是我可以接触到 Agent 代码呀。明年一大目标就是好好研究研究大佬们写的代码, 应该是会有所收获的吧。


这大半年工作唯一的感受就是, 太多工作都没啥意义, 都是用一群人内卷换来一小撮人的自嗨罢了。但是又能咋样, 牛马嘛给钱就干。先把钱赚到了再说。


image


3.2 读书 & 播客


今年相比去年阅读量少了些, 当然去年其实也没读多少书。书嘛, 都是随便瞎看的, 看过了 26 本书, 但真正看完的也就 10 本。本着先培养习惯的目的, 所以很多书看几眼不感兴趣也就不会强迫自己去读。也不会抱着太强的目的去看书, 也不要求一定会有啥收获。


image


今年还入坑了博客, 平常做家务、坐地铁、撸铁、上班... 无聊时就会听听博客。大部分都是财经类的、科技类的, 再则就是几个脱口秀平台。


image


3.3 开源? 独立作品


今年 小绿点还行, 出乎意料的多....


image


今年主要新开了两个坑, 当然也都没有填上:



  • 智账: 一个记账的 APP, 为此还去研究了下 React Native 还输出了五篇博客, 整了个专栏, 实际上连项目都还没搭建起来, 就研究了下 React Native

  • writeflow: 基于 prosemirror 写的一个富文本编辑器吧, 整体架构主要参考 Tiptap 全当是学习了。之所以要写这么一个东西一为折腾, 二是 昆仑虚 中文章编辑器用的是代码编辑器, 不是很方便想要换成富文本编辑器。


其他的, 昆仑虚 今年应该也做了些调整, 没做记录也不知道改了啥, 也懒得查了, 就这样吧....


3.4 写文 & 视频


今天拢共就写了 11 篇文章, 主要还是发布在掘金、公众号上, 掘金上整体数据也就一般般吧。大部分都是上半年失业时闲得无聊写的, 下半年基本就荒废了, 毕竟当牛马的日子那么累, 谁还写文章啊 🐶


image


今年公众号粉丝从 8301394 涨了 500 多吧, 主要也都是上半年涨的, 下半年也都没怎么更新了。


image


对了, 油管B 站还发了一期视频, 配套文章 还原 Mac Dock 栏动效: 一步步打造流畅的波形缩放动画


image


内容创作收入: 掘金金石 300+、公众号累计收入 47


image


四、展望 2026


照例, 定定 2026 目标, 至于能不能实现另说....



  • 好好生活



    • 身材管理(减肥、撸铁): 撸铁每周四次(下雨可以不去)、体重维持在 80~82 (目前 2025.12.31 体重 90)

    • 吃喝玩乐: 好好吃饭(不吃饱)、养生(少喝饮料)、多出去逛逛(去次新疆或韩国)

    • 财务理财: 保护钱袋子、稳定理财(别浪)

    • 人生体验: 整个人生第一辆小车(10w 左右)、人生大事安排起来、学习打网球



  • 工作与成长



    • 保住饭碗: 简历常更新(6月12月)、保持学习

    • 通过软考: 为了 E 类人才、为了买车

    • AI 学习: 把公司 Agent 那一套研究明白、侧重应用端、AI Coding 技巧掌握

    • 算法学习

    • 多输出: 写文(每月 2 篇)、公众号(每月 2 篇)、视频(每月 1 个)

    • 独立作品: 昆仑虚(增加两个 APP、开放注册)、智帐(完成开发)



  • 习惯养成:



    • 坚持写日记、做月度总结、季度总结

    • 阅读: 每天 30 分钟, 微信读书打卡

    • 早起(给自己找个活干)




作者:墨渊君
来源:juejin.cn/post/7592996072705474610
收起阅读 »

2025年终总结:再次选择、沪漂、第一次演讲、相亲无果

选择大于努力 友友们,我是卷福同学,上次写2024年终总结的时候还在武汉,谁能想到一年之后会在上海写2025的年终总结。今年下半年经历的事情比较多,总结来说就是,人生经历又丰富了 1.再次选择 去一线大城市闯荡人生还是留在武汉岁月静好呢? 1月 1月时...
继续阅读 »

选择大于努力



友友们,我是卷福同学,上次写2024年终总结的时候还在武汉,谁能想到一年之后会在上海写2025的年终总结。今年下半年经历的事情比较多,总结来说就是,人生经历又丰富了


1.png


1.再次选择



去一线大城市闯荡人生还是留在武汉岁月静好呢?



1月


1月时候还在武汉国企里呢,彼时因为项目变少了,武汉人员要重新分配,没分到项目组的人要进资源池等候下一步安排。而我这个小组之前武汉是有2个人的,北京1个项目经理,给我分配了半个人的工时,另一个人直接让去资源池。关于这个项目经理,去年也写过吐槽的帖子。这个人在武汉的名声非常不好,就完全是对待牛马一样对待底下干活的人。


我想着以后的日子可能过得更难受,还不如直接进资源池算了。于是就和他说了,他倒也爽快,想着再从武汉随便捞个人进来呗,反正还有很多人没安排项目组的。没想到的是,他接连找了两个人,但是因为武汉的人都听说过他的名号,都表示不想去他的项目组。最后,他把项目给外包人员做了。武汉的人,宁愿待池子里,也不想跟着他干。。。


这让我想起以前上小学的时候,以前农村小学的老师,打学生都很厉害的。而打的原因不仅仅是因为调皮捣蛋,我那个班教数学的老师,就是其中打人最厉害的。上课的讲台两边会有两个座位嘛,每次她讲课的时候,都会从这两个座位的学生手里拿课后作业讲,要是讲的时候发现写错了,直接冲过去打头,提耳朵等等。有一次因为班上写错作业的人太多了,直接一节课没上,轮流上去扎马步。而我呢,又恰巧有一学期是坐在讲台旁边的座位,于是这学期每逢她的课必挨打,打到后面,居然在课上说,卷福坐在这,已经被我打肿了,你们再敢做错题试试。到第二学期开学的时候,大家选座位直接把前面两个位置空着了,有两个人宁愿在后面站着也不坐那位置。


插图.png


我感觉是不是历史又一次重演了呢?


现在回头看,当时没继续跟着他的选择是对的。在武汉,也没有岁月静好啊。


再次选择


虽然5月份的时候拿了上海的offer,但是等真的要走的时候,还是会纠结的。就和刚毕业的时候去北京一样,一切都要重头开始了。走的那天,出租屋里只有个保洁阿姨在打扫卫生,就和我刚回武汉的时候一样。不同的是,上次阿姨说的是房子很快就打扫好了,这次说的是,祝老板以后去上海了发大财啊


2.jpg


2.沪漂


探索新事物


上海就是机会多啊,休息日都会出去逛逛,探索些新事物。想想来上海之后,去周边城市参加徒步、参加ChinaJoy漫展、看了开心麻花的话剧《疯狂理发店》、还有市区内的一些公园、大学、图书馆、动物园、演唱活动等等,生活非常丰富多彩。



  • 徒步活动在小红书上找个团报就行,很多都是一天游,一半时间都在路上

  • 漫展里的coser都美如画,非常适合集邮


3_1.jpg


3.jpg


3.第一次演讲


3月


今年参加的线下活动还比较多呢,3月份的时候受腾讯云社区的邀请去杭州参加线下的技术训练营活动,主要也是想趁机会多认识些大佬,说不定大佬招人,有内推机会。倒也认识了不少人,喵喵、小智,还有社区的泽敏姐。晚上一起吃饭交流的时候,泽敏姐说下次有机会让我上去演讲。当时只以为是说说而已,毕竟社区里大佬太多了


4.jpg


9月


9月份的时候收到社区的邀请,去深圳参加腾讯全球数字生态大会,作为讲师上去做分享。我是非常想去的,这样就能达成从学生到老师角色转变的目标,输入变输出。比较纠结的是分享什么内容比较好呢,想了一晚上,最后觉得分享自己用AI两年的经历、沉淀的一些使用心得体、还有变现方式会比较好。


现场的分享也是比较顺利,讲完下来的时候和小智老师沟通上台演讲体验,小智说我讲的非常干货,讲的很稳,刚才他自己讲的时候非常紧张,腿都在抖。我说,我也是啊,腿都在打颤,反而看你讲一点也不慌的样子。。。


第二天又和喵喵一起去腾讯数码大厦找泽敏姐,非常感谢泽敏姐的邀请,第一次到腾讯的大楼参观。期间见识到了超豪华的二次元工位,满墙的手办,非常震惊。


5.jpg


4.AI探索


选择适合自己的方向比较重要,2024年投入了很多时间在AI视频、绘画上,虽然也有涨粉嘛,但是变现不行,一年下来也就三位数的收益。今年主要在写作还有AI编程方向投入,因为换工作的原因,其实投入精力没去年那么多。反而收益还更多了,有四位数的收益。也产出了百万阅读的文章和10w+播放量的视频


出爆款的诀窍就是追热点,这是普通人出爆款最容易的方式了。比如年初的Deepseek,国庆期间的sora2,趁着刚出来热度最高的时候,随便写点东西或者做个视频,流量都非常好的。那像现在再去写Deepseek,流量肯定不如之前了。


6.png


7.png


11月


11月看到小智老师发的华为鸿蒙线下编程活动的信息,拉着在上海的一个前同事一起去参加玩玩,前同事是我在北京阿里工作时同组的,后来来了上海后,我居然在一个公交站碰到他了,也是十分震惊,居然在上海遇到曾在北京的前同事。鸿蒙的编程活动都是基础的操作,正好也买了Codebuddy的会员,用AI编程轻松解决了,拿到个小礼品


8.jpg


12月


年底了,AI破局俱乐部在深圳举办行动家大会,我看分享嘉宾和内容挺干货的,也报名跑去深圳参加了,到现场才发现,高手云集,天下英雄如过江之鲫。我把这次参会了解的东西整理了下:



  • AIPPT.com :赵充老师以肯德基为例分享做产品不要做全家桶,用户只想要个甜筒(不要做大而全的产品,而是在垂直领域找到需求,在单点,堵上一切)

  • AI编程出海:老外付费意愿更强,大公司不愿意做的垂直小市场,才是个人最大的机会。remove.bg网站仅有去除背景这一项功能,流量却非常高

  • 搜索流量比推荐流量更值钱:用户主动搜的,说明有明确需求。而平台推的,用户只是随便看看。获取搜索流量的方法:研究用户搜索的关键词,围绕关键词输出内容,时间久了,用户搜索这个关键词,自然就找到你了


活动分享的内容挺多的啊,这里就不继续写了。


同样的听课,结果可能完全不同,会场的1000人,参加完会回去后,可能大部分人就是感慨一下,然后继续原来的生活


9.jpg


10.jpg


11.jpg


5.相亲


离开武汉前,和大学同学聚餐,聊了下发现同学要准备去女方家提亲了,问对象是从哪里找的,说了个相亲软件。不过也很难,同学相亲了十几个女生,才和现在这个走到谈结婚这一步。知道了相亲软件(青藤)后,我来上海也开始了相亲之旅,到目前为止,还没有一个相上的,简单说下相亲的几个女生吧:



  • 91年,初中老师。其实是在武汉相亲的,家里给介绍的。感觉年龄差的太大了,差不多5岁了,不过因为是家里介绍的,还是得见上一面。3月的时候,武大樱花还开着,便想去武大里逛逛聊聊。让她把身-份-证号发我,用校友通道预约。然后犹犹豫豫半天没有发我,说是个人隐私等等之类的,要自己预约。结果没约上,想再用校友通道时已经约满了,无奈只好约着去学校旁边一家冷锅鱼店吃饭好了,计划约的12点见面,我预估12点半应该能吃上饭(不知道为什么,武汉相亲的女生都迟到),没想到她直到1点多才到,迟到1个多小时,期间也就各种尬聊,回去后两人都没再发消息了,凉凉

  • 93年,上海公务员。家里的亲戚介绍的,是我相亲过的最优秀的女生了。以前的高考文科状元,武大校友,长得像袁咏仪,聊天说话情商也很高。接触了一个月吧,期间也一起吃过饭,看过话剧。最后一次见面聊天说起她前男友,海归,年入百万,金融行业。长相没说啊,应该也不差,妥妥高富帅。我一听这条件,心里顿时凉凉,差距太大了。问起为什么分了呢,说是男的虽然有钱,但是不给女的花,什么事都要AA。就是网上那种观念:钱是给女人看的,不是给女人花的。这次聊完之后就结束了,又凉凉。

  • 95年,幼师。在上海相亲的,青藤上找的,见面后感觉非常漂亮啊。不过性格比较强势,刚开始聊的还是开心的话题,突然画风一转,她就开始吐槽模式,吐槽支教的山里小学的领导等等,后半段全是听她吐槽,没再聊相亲的话题了。回去后又聊了几周,但是幼师可能时间太紧,也可能她同时聊的人太多,后面想再约见面就没时间了,于是凉凉

  • 97年,互联网数据分析师。来上海后相亲的第一个女生,也是非常漂亮,加上好友,看了我动态后,说非常崇拜技术大佬(多写博客还是有好处的嘛),想请我吃饭。于是爽快赴约,期间聊分布式、高并发等等,最后吃完饭结束的时候,说感觉身高不够

  • 00年,主业机械设计,副业自媒体。遇到同行了,聊天话题特别多,情绪价值拉满。约线下去CJ漫展玩,让她把身-份-证号发我来买票,本来还想着怎么解释说明的,下一秒就把身-份-证号和手机号发来了,没有任何犹豫。约好了早上9点半去会展中心,没想到她早早到我这边来等我一起过去,连零食和水都买好了。遂开心前往,妹子是第一次逛漫展,逛的非常开心。只不过回去后还是说了性格不合,只好当朋友了。后续还是保持联系,偶尔见面


这里列出不同年龄段的相亲女生,其他还有一些都是软件上匹配了没说话,或者说两句话就不继续聊了的。给兄弟们做个负面教材的参考啊,今年的相亲就到此为止,明年再说吧。。。


6.2026目标


最后不都得展望下未来吗,2026年你们又给自己制定了哪些目标和规划呢,我给自己定下的目标,希望明年都能做到



  • 神山转山

  • 樱花巡礼

  • 新手上路

  • 恰老外米


最后,感兴趣的朋友可以关注我的公众号:卷福同学


作者:卷福同学
来源:juejin.cn/post/7590309337861046313
收起阅读 »

告别996!Claude Code 6个实用工作流程

效率翻倍!Claude Code 6个实用工作流程,让我开发效率提升300% 从代码小白到团队效率担当,掌握这些工作流程后,我终于告别了996 前言 还记得刚加入新公司时的那种无助感吗?面对一个几万行代码的项目,光是理解架构就要花上好几周,遇到 Bug 更...
继续阅读 »

效率翻倍!Claude Code 6个实用工作流程,让我开发效率提升300%



从代码小白到团队效率担当,掌握这些工作流程后,我终于告别了996



前言


还记得刚加入新公司时的那种无助感吗?面对一个几万行代码的项目,光是理解架构就要花上好几周,遇到 Bug 更是焦头烂额。直到半年前,我开始使用 Claude Code,一切都变了。


起初,我只把它当作一个"更聪明的代码补全工具"。但随着深入了解,我发现它真正强大的是一套完整的工作流程体系。从理解代码库到并行开发,从错误修复到架构决策,每个环节都能找到对应的最佳实践。


这篇文章,我想和你分享我摸索出来的6个最实用的工作流程。 这些都是我在实战中反复验证过的,如果你也是有经验的开发者,相信看完后会有相见恨晚的感觉。




一、快速理解新代码库 - 告别"看代码看到眼花"


1.1 项目概览三板斧


刚接手一个新项目时,很多人的第一反应是打开编辑器,从入口文件开始一行行看。别这样做! 这样看一周也理不清头绪。


我的方法是"从宏观到微观"的三板斧:


第一步:获取高级概览


cd /path/to/project
claude

然后直接问:


> 给我这个代码库的概览

Claude 会分析整个项目结构,告诉你:



  • 这是什么类型的项目(Web应用、API服务、CLI工具等)

  • 使用了哪些主要技术栈

  • 核心模块有哪些

  • 项目的目录结构组织方式


第二步:理解架构模式


> 解释这里使用的主要架构模式

这个问题太关键了!我曾经接手一个微服务项目,看了一周都没搞清楚服务间的调用关系。问了这个问题后,Claude 直接告诉我:



  • 采用的是事件驱动架构

  • 使用了 CQRS 模式

  • 服务间通过消息队列通信

  • 有清晰的分层结构


瞬间豁然开朗!


第三步:深入关键细节


有了架构理解后,再针对性地深入:


> 关键的数据模型有哪些?
> 认证是如何处理的?

1.2 精准定位代码


理解了整体架构后,接下来是快速定位具体功能的实现代码。


场景1:找功能实现


> 找出处理用户认证的文件

Claude 不仅会列出相关文件,还会解释每个文件的作用。比如:



  • auth.service.ts - 核心认证逻辑

  • auth.guard.ts - 路由守卫

  • auth.middleware.ts - 请求预处理


场景2:理解组件交互


> 这些认证文件是如何协同工作的?

这会得到一个清晰的调用链路图,比看代码注释高效100倍。


场景3:追踪执行流程


> 追踪从前端到数据库的登录流程

从用户点击登录按钮,到前端发送请求,到后端验证,再到数据库查询,整个流程一清二楚。


💡 实战经验分享


经验1:使用项目术语


不同团队有自己的命名习惯。如果你们把"用户"叫"Member",那就问:


> 找出处理成员认证的文件

这样得到的结果更准确。


经验2:从测试入手


如果项目有完善的测试,我会先问:


> 显示支付模块的测试文件

测试文件通常能快速了解模块的功能和用法。


经验3:建立词汇表


大型项目往往有自己的术语。我会让 Claude 帮我整理:


> 创建项目特定术语的词汇表

这样后续交流更顺畅。




二、高效修复错误 - 不再为 Bug 掉头发


2.1 错误诊断的正确姿势


遇到错误时,很多开发者的第一反应是复制错误信息到 Google。但很多时候,同样的错误信息可能有完全不同的原因。


我的方法是把完整上下文给 Claude:


> 运行 npm test 时遇到了错误

然后把错误堆栈粘贴给 Claude。关键是提供:



  1. 完整的错误信息

  2. 执行的命令

  3. 重现步骤(如果知道的话)


让 Claude 分析后,再问:


> 建议几种修复方法

注意,我故意问"几种方法",而不是"怎么修复"。这样可以:



  • 看到不同的解决思路

  • 理解每种方案的优缺点

  • 选择最适合当前项目的方案


选定方案后:


> 更新 user.ts 添加你建议的空值检查

2.2 从修复到预防


修复一个 Bug 不难,难的是避免类似问题再次出现。


我的做法是:


第一步:根因分析


> 这个错误的根本原因是什么?
> 代码的其他部分是否可能存在相同问题?

第二步:添加防护措施


> 添加验证以防止此类错误

第三步:补充测试


> 编写能够捕获此错误的测试用例

💡 实战经验分享


经验1:区分错误类型


告诉 Claude 错误的特性:


> 这个错误间歇性发生,大约10次里有1次

间歇性错误和持续错误的分析方法完全不同。


经验2:分享环境信息


> 我在 macOS 上使用 Node 18.17.0

环境差异可能导致的问题,Claude 能帮你考虑到。


经验3:让 Claude 解释


修复后,我会问:


> 解释为什么这个修复有效

理解原理,下次遇到类似问题就能自己解决了。




三、代码重构 - 让旧代码焕发新生


3.1 识别重构目标


代码重构最难的不是怎么改,而是改什么。项目大了,到处都是"历史遗留代码",从哪里开始?


我的方法是让 Claude 帮我扫描:


> 查找代码库中已弃用的 API 使用

或者更具体:


> 查找所有使用 moment.js 的地方并建议替代方案

Claude 会列出所有使用旧 API 的地方,并给出现代化的替代方案。


3.2 安全重构策略


找到了重构目标,接下来是安全地执行。我的原则是:小步快跑,每步验证。


第一步:获取重构建议


> 建议如何重构 utils.js 以使用现代 JavaScript 特性

第二步:明确行为不变


> 重构 utils.js 以使用 ES2024 特性,同时保持相同的行为

重点强调"保持相同的行为",避免 Claude 引入破坏性变更。


第三步:立即验证


> 为重构后的代码运行测试

第四步:如果没有测试?


> 在重构前为 utils.js 编写测试

先补测试,再重构,安全系数翻倍。


💡 实战经验分享


经验1:明确兼容性要求


如果项目需要支持旧环境:


> 重构时保持 IE11 兼容性

经验2:请求解释收益


> 解释这种重构方法的好处

不是为了用新语法而重构,而是为了更好的性能、可维护性。


经验3:分批次重构


大型重构不要一次性做完:


> 先只重构日期处理函数

减少风险,便于代码审查。




四、扩展思考 - 处理复杂架构决策


4.1 深度思考模式


有些问题不是简单问答能解决的。比如:



  • 设计一个新的认证系统

  • 评估技术选型的利弊

  • 规划数据库分片策略


这时候,我会触发 Claude 的扩展思考模式:


> 我需要使用 OAuth2 实现一个新的认证系统。
> 深入思考在我们代码库中的最佳方案。

关键触发词:



  • think

  • think more / think harder / think longer

  • think a lot


触发后,你会看到 Claude 的思考过程以斜体灰色文本显示。这个过程可能持续几十秒甚至更久,不要中断它! 这正是深度分析的价值所在。


4.2 最佳使用场景


场景1:架构规划


> 我们正在从单体应用迁移到微服务。 
> 思考拆分用户模块的最佳策略。

场景2:复杂调试


> 我们有一个只在高负载下出现的内存泄漏。
> 思考所有可能的原因和调查方法。

场景3:权衡分析


> 思考在我们的新日志系统中使用 PostgreSQL 与 MongoDB 的权衡。

💡 实战经验分享


经验1:提供充分上下文


扩展思考的效果取决于你提供的信息:


> 思考如何优化我们的 API 响应时间。 
> 目前平均是 2 秒,我们需要降到 200 毫秒以下。
> 我们使用的是 Node.js + PostgreSQL + Redis。

经验2:追问和深化


第一次思考后,继续深入:


> 思考这种方法中潜在的安全漏洞
> 更深入地思考我们应该处理的边缘情况

经验3:保存思考过程


Claude 的思考过程本身很有价值。我会复制出来,作为设计文档的一部分。




五、Git Worktrees 并行开发 - 多任务处理神器


5.1 理解 Worktrees


作为开发者,你是不是经常遇到这种情况:



  • 正在开发新功能,突然来了一个紧急 Bug

  • 不得不 stash 当前修改,切换分支修 Bug

  • 修完回来,恢复 stash,结果各种冲突


Git Worktrees 就是为了解决这个问题而生的。


简单说,Worktrees 允许你在同一台机器上,同时检出同一个仓库的多个分支到不同目录。每个目录都是独立的工作区,互不干扰。


5.2 实战操作


创建新的 Worktree:


# 为新功能创建 worktree
git worktree add ../my-project-feature-a -b feature-a

# 或者用现有分支创建
git worktree add ../my-project-bugfix bugfix-123

在不同 Worktree 中运行 Claude Code:


# 终端1:开发新功能
cd ../my-project-feature-a
claude

# 终端2:修复 Bug
cd ../my-project-bugfix
claude

两个 Claude 实例完全隔离! 一个在写新功能,一个在修 Bug,互不影响。


管理 Worktrees:


# 查看所有 worktrees
git worktree list

# 完成后删除
git worktree remove ../my-project-feature-a

5.3 环境初始化注意事项


重要! 新 Worktree 是干净的代码目录,需要初始化开发环境:


JavaScript 项目:


cd ../my-project-feature-a
npm install # 或 yarn / pnpm

Python 项目:


cd ../my-project-feature-a
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

💡 实战经验分享


经验1:命名规范


用描述性的目录名:


git worktree add ../myproject-auth-refactor -b auth-refactor
git worktree add ../myproject-urgent-fix -b hotfix-123

一眼就知道每个 worktree 是做什么的。


经验2:长期任务隔离


对于需要几天才能完成的任务,单独一个 worktree:


# 早上继续开发
cd ../myproject-big-feature
claude --continue

经验3:PR 准备区


专门用一个 worktree 来准备 PR:


git worktree add ../myproject-pr-prep -b pr-prep
cd ../myproject-pr-prep
claude
> 帮我准备一个干净的 PR



六、自定义斜杠命令 - 打造专属工具箱


6.1 项目级命令


团队协作时,有些操作是固定的流程。与其每次手动输入,不如封装成命令。


创建命令目录:


mkdir -p .claude/commands

创建优化命令:


echo "分析这段代码的性能并建议三个具体的优化措施:" > .claude/commands/optimize.md

使用命令:


> /project:optimize

就这么简单!


6.2 参数化命令 - 更灵活的利器


固定命令很好,但有时候需要动态参数。使用 $ARGUMENTS 占位符:


创建 Fix Issue 命令:


cat > .claude/commands/fix-issue.md <<'EOF'
查找并修复问题 #$ARGUMENTS。按以下步骤操作:
1. 理解工单中描述的问题
2. 在代码库中定位相关代码
3. 实现解决根本原因的方案
4. 添加适当的测试
5. 准备简洁的 PR 描述
EOF

使用命令:


> /project:fix-issue 123

$ARGUMENTS 会被替换为 123


更多应用场景:


# 生成测试
echo "为 $ARGUMENTS 函数生成全面的测试" > .claude/commands/test.md

# 代码审查
echo "审查 $ARGUMENTS 的安全漏洞" > .claude/commands/security-review.md

# 文档生成
echo "为 $ARGUMENTS 添加带示例的文档" > .claude/commands/document.md

6.3 个人命令库


有些命令是通用的,适合所有项目。放在个人目录:


mkdir -p ~/.claude/commands

创建个人命令:


echo "审查这段代码的常见安全问题:
- SQL 注入
- XSS 漏洞
- CSRF 保护
- 认证缺陷
- 敏感数据泄露"
> ~/.claude/commands/security-audit.md

在任何项目中使用:


> /user:security-audit

个人命令 vs 项目命令:



  • /user:xxx - 个人命令,所有项目可用

  • /project:xxx - 项目命令,团队成员共享


💡 实战经验分享


经验1:建立团队命令库


我们团队创建了这些常用命令:



  • /project:optimize - 性能优化分析

  • /project:fix-issue - 修复 Issue 流程

  • /project:review-pr - PR 审查清单

  • /project:update-deps - 依赖更新检查


新人入职,克隆仓库就能用,大大降低上手成本。


经验2:命令模板化


把常用的 Prompt 模板化:


# code-review.md
审查这段代码的:
1. **代码质量**: 可读性、命名、结构
2. **性能**: 识别瓶颈
3. **安全性**: 检查漏洞
4. **测试**: 覆盖率和质量

提供具体、可操作的建议。

经验3:版本控制命令


.claude/commands 目录加入 Git,团队共享:


git add .claude/commands
git commit -m "添加团队 Claude 命令"



其他实用功能速览


除了上面重点介绍的6个工作流程,Claude Code 还有很多实用功能。这里快速过一遍:


测试覆盖


> 查找未被测试覆盖的函数
> 为边缘情况添加测试
> 运行新测试并修复任何失败

PR 创建


> 总结我做的修改
> 创建一个 PR
> 用更多上下文增强 PR 描述

文档管理


> 查找没有适当 JSDoc 注释的函数
> 添加带示例的文档
> 检查文档是否符合项目标准

图像处理


可以直接把图片拖进 CLI,然后问:


> 这个错误截图显示了什么?
> 生成 CSS 以匹配这个设计稿

会话恢复


# 继续最近的对话
claude --continue

# 选择特定对话
claude --resume



总结与心得


回顾这半年使用 Claude Code 的经历,我的效率提升至少在 300% 以上。这不是夸张,而是实实在在的数据:


量化收益:



  • 新项目上手时间:从2周缩短到2天

  • Bug 修复时间:平均减少60%

  • 代码审查效率:提升3倍

  • 文档编写时间:减少80%


更重要的是思维方式的转变:


以前遇到问题,我的第一反应是"我去查查"。现在是"我问问 Claude"。


以前写代码,我要自己规划每一步。现在是我告诉 Claude 目标,它给我几个方案,我来选最优的。


但也要注意:


Claude Code 不是万能的。它:



  • 不能替代你的技术判断

  • 不能跳过代码审查

  • 不能盲目信任它的输出


它更像一个超级助手,帮你更快地探索、验证、实现。最终决策权还是在你手里。


学习曲线:


说实话,前两周我也很迷茫。不知道怎么问问题,不知道什么时候用什么命令。但随着使用,慢慢就找到了感觉。


我的建议:



  1. 先从简单场景开始 - 找代码、修 Bug

  2. 逐步尝试高级功能 - Worktrees、自定义命令

  3. 建立自己的命令库 - 积累常用 Prompt

  4. 团队共享最佳实践 - 提升 Team 整体效率


未来展望:


AI 编程助手还在快速进化。今天的"黑科技",明天可能就是标配。保持学习,保持好奇,保持对新工具的开放态度。


希望这篇文章对你有帮助。如果你也在使用 Claude Code,欢迎在评论区分享你的经验和心得!




如果这篇文章对你有帮助,请点赞 👍 收藏 ⭐ 关注 💖,这对我非常重要!


你在使用 Claude Code 时有什么独特的技巧?欢迎评论区交流!





Happy Coding! 🚀



作者:小碗细面
来源:juejin.cn/post/7610676768316801074
收起阅读 »

5 分钟打造你的“幽灵搭档”终端-Ghostty

什么是 Ghostty?为什么它这么香? Ghostty 是由 HashiCorp 联合创始人 Mitchell Hashimoto(@mitchellh) 从 2021 年开始用业余时间开发的终端模拟器,核心用 Zig 语言编写,于 2024 年底正式开源...
继续阅读 »

什么是 Ghostty?为什么它这么香?


image.png


Ghostty 是由 HashiCorp 联合创始人 Mitchell Hashimoto(@mitchellh) 从 2021 年开始用业余时间开发的终端模拟器,核心用 Zig 语言编写,于 2024 年底正式开源


三大优势(开发者狂喜):


优势说明
超级快GPU 加速(macOS 用 Metal),滚动丝滑如丝绸,Claude 输出千行不卡顿
超级美原生 macOS 界面 + 毛玻璃透明 + Catppuccin Mocha 紫色主题 + 完美连字字体
超级智能支持 Kitty 图形协议(Claude 画图直接显示)、一键分屏、布局永久保存


💡 一句话总结:Ghostty 不逼你“要么快要么丑”,它全都要!

免费开源,跨平台,还在疯狂迭代。

官网:ghostty.org/





🛠️ 第一步:安装 Ghostty(3 分钟搞定)


在终端执行:


brew install --cask ghostty

安装后 Spotlight 搜索 Ghostty 打开。



⚠️ 第一次启动可能弹出两个窗口(主窗口 + 下拉幽灵窗口),忽略它,我们稍后配置。





⌨️ 第二步:基础命令(记住这 5 个就够了)


快捷键功能
Cmd + D左右分屏(左 Claude 写码,右调试)
Cmd + Shift + Enter放大当前窗格(看长输出超爽)
Cmd + W关闭当前窗格
Cmd + Shift + ,重载配置(改完 config 必按!)
Cmd + Q完全退出 Ghostty


🖱️ 切换窗格:直接用鼠标点击即可!





🌈 第三步:美化升级 — Starship 彩虹状态栏


安装并配置 Starship(终端显示 Git、CPU、时间等):


brew install starship
starship preset catppuccin-powerline -o ~/.config/starship.toml

~/.zshrc 末尾添加一行:


eval "$(starship init zsh)"

保存后完全退出 Ghostty(Cmd + Q)并重启,即可看到彩虹状态栏!




🎮 第四步:打造你的“快乐开发现场”


安装监控工具:


brew install fastfetch btop

布局操作(全部在 Ghostty 内完成):



  1. 主窗口:运行 claude(或你的 AI 编程助手)

  2. Cmd + D → 右侧窗格:运行 fastfetch(炫酷系统信息)

  3. Cmd + Shift + D → 下方窗格:运行 btop(实时 CPU/内存监控)

  4. 任意窗格按 Cmd + Shift + Enter 放大 Claude 输出



效果

左侧 Claude 生成代码 + 右侧 fastfetch + 底部 btop 监控

紫色毛玻璃背景 + 连字字体 + 彩虹状态栏 → 开发浪漫到窒息!



image.png




💎 第五步:终极配置(直接复制粘贴,零报错!)


以下配置已包含所有功能



  • Catppuccin Mocha 紫色主题

  • Cmd + D 左右分屏

  • Cmd + Shift + Enter 一键放大

  • 布局永久保存 + 零报错


请直接复制下方全部内容,覆盖你的 Ghostty 配置文件:


# --- Typography ---
font-family = "Maple Mono NF CN"
font-size = 14
adjust-cell-height = 2

# --- Theme and Colors ---
theme = Catppuccin Mocha

# --- Window and Appearance ---
background-opacity = 0.85
background-blur-radius = 30
macos-titlebar-style = transparent
window-padding-x = 10
window-padding-y = 8
window-save-state = always
window-theme = auto

# --- Cursor ---
cursor-style = bar
cursor-style-blink = true
cursor-opacity = 0.8

# --- Mouse ---
mouse-hide-while-typing = true
copy-on-select = clipboard

# --- Quick Terminal ---
quick-terminal-position = top
quick-terminal-screen = mouse
quick-terminal-autohide = true
quick-terminal-animation-duration = 0.15

# --- Security ---
clipboard-paste-protection = true
clipboard-paste-bracketed-safe = true

# --- Shell Integration ---
shell-integration = zsh

# --- Claude 专属优化 ---
# initial-command = /opt/homebrew/bin/claude
initial-window = true
quit-after-last-window-closed = true
notify-on-command-finish = always

# --- Performance ---
scrollback-limit = 25000000

# --- 基础分屏(左右添加屏幕)---
keybind = cmd+d=new_split:right
keybind = cmd+shift+enter=toggle_split_zoom
keybind = cmd+shift+f=toggle_split_zoom

✅ 操作步骤:



  1. 打开终端,执行:


    open ~/.config/ghostty/config


  2. 全选删除原内容粘贴上方配置 → 保存

  3. 在 Ghostty 中按 Cmd + Shift + , 重载配置


然后就可以得到这样的效果:


image.png




💫 结语:你,已是“幽灵开发者”


闭上眼睛,想象这一刻:



你按下 Cmd + D,屏幕裂开新世界。

左侧 Claude 生成优雅代码,

右侧 fastfetch 彩虹跳动,

底部 btop 实时监控 CPU,

你再按 Cmd + Shift + Enter

Claude 的千行输出铺满全屏——

连字字体闪烁,紫色毛玻璃温柔发光



那一刻,你会笑出声:原来开发,可以这么爽!




🚀 现在就行动!



  1. 安装 Ghosttybrew install --cask ghostty

  2. 复制上方配置 → 覆盖 ~/.config/ghostty/config

  3. Cmd + D,创建你人生第一个左右分屏!



Claude 负责思考

Ghostty 负责鬼混

而你,只需 收割快乐与效率



从今天起,你的 Mac 不再是冷冰冰的终端,

而是一个会分屏、陪鬼混的 AI 搭档。


作者:树獭非懒
来源:juejin.cn/post/7616681500684419099
收起阅读 »

Skills 实战:让 AI 成为你的领域专家

引言:从通用助手到领域专家 想象一下这些场景: 场景 1: 重复的上下文说明 你: "帮我分析这个 BigQuery 数据,记住要排除测试账户,使用 user_metrics 表..." Claude: "好的,我来分析..." [第二天] 你: "再帮我分...
继续阅读 »

引言:从通用助手到领域专家


想象一下这些场景:


场景 1: 重复的上下文说明


你: "帮我分析这个 BigQuery 数据,记住要排除测试账户,使用 user_metrics 表..."
Claude: "好的,我来分析..."

[第二天]
你: "再帮我分析一次销售数据,还是那个表,记得排除测试账户..."
Claude: "好的,我来分析..." # 😓 又要重复一遍

场景 2: 领域知识的重复传授


你: "帮我处理这个 PDF 表单,PDF 的表单字段结构是..."
Claude: "明白了"

[一周后]
你: "再处理一个 PDF 表单..."
Claude: "请告诉我 PDF 表单的结构" # 😓 忘记了

场景 3: 工作流程的不一致


你: "生成 API 文档,记得包含请求示例、响应格式、错误码..."
Claude: "好的" # ✅ 这次做得很好

[下次]
你: "再生成一份 API 文档"
Claude: [生成的文档] # ❌ 这次忘记了错误码部分

这些问题的根源是:每次对话都是全新的开始,Claude 无法记住你的领域知识、偏好和工作流程。


💡 Skills 系统的价值


Skills 就是解决这个问题的方案——它让你能够:



  1. 📦 封装领域知识: 把你反复向 Claude 解释的专业知识打包成 Skill

  2. 🔄 自动加载: 当任务相关时,Skill 自动激活,无需重复说明

  3. ♻️ 持续复用: 创建一次,跨所有对话自动使用

  4. 🎯 专业能力: 让 Claude 从通用助手进化为领域专家


本文核心内容:



  1. Skills 的核心概念与工作原理

  2. 渐进式披露架构:三级加载机制

  3. 创建自定义 Skills:从入门到精通

  4. 最佳实践:简洁、结构化、可验证

  5. 实战案例:PDF 处理、BigQuery 分析、代码审查

  6. 评估与迭代:如何持续优化 Skills



"把你反复向 Claude 解释的偏好、流程、领域知识打包成 Skills,让 AI 成为你的领域专家"





一、什么是 Skills?


1.1 核心概念


Agent Skills(智能体技能)是一种模块化的能力扩展系统,它为 Claude 提供了:



  • 领域专业知识: 如 PDF 处理技巧、数据库 schema、业务规则

  • 工作流程: 如代码审查流程、文档生成流程、数据分析流程

  • 最佳实践: 如命名规范、代码风格、错误处理模式


1.2 Skills vs 普通 Prompt


维度普通 PromptSkills
作用范围单次对话跨所有相关对话
加载方式每次手动提供相关任务时自动加载
上下文占用每次都占用按需加载,未使用时零占用
知识管理分散在多次对话中集中管理,持续优化
一致性依赖人工记忆标准化,确保一致

类比理解:



  • 普通 Prompt 像是每次都要"现场培训"新员工

  • Skills 像是给员工提供"岗位手册",需要时自己查阅


1.3 Skills 遵循开放标准


Claude Code Skills 基于 Agent Skills 开放标准,这意味着:



  • ✅ 标准化格式,跨 AI 工具兼容

  • ✅ 社区生态,可以使用他人创建的 Skills

  • ✅ 长期支持,不会因产品升级而失效


Claude Code 在标准基础上扩展了:



  • 🔧 调用控制机制

  • 🤖 子代理执行能力

  • 📥 动态上下文注入




二、Skills 工作原理:渐进式披露架构


2.1 为什么需要渐进式披露?


问题:如果把所有 Skills 的详细内容都加载到上下文中会怎样?


假设你有 10 个 Skills,每个包含 5000 tokens 的详细指导...
总共: 50,000 tokens

但你可能只需要使用其中 1-2 个 Skill!
浪费: 40,000+ tokens(80% 的上下文窗口!)

解决方案:渐进式披露——只加载需要的内容,按需展开详细信息


2.2 三级加载机制


09-01-progressive-disclosure.png


第一级:元数据(Metadata)- 始终加载


---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
Use when working with PDF files or when the user mentions PDFs, forms,
or document extraction.
---


  • 加载时机: Claude 启动时

  • Token 消耗: 每个 Skill 约 100 tokens

  • 作用: 让 Claude 知道有哪些 Skills 可用,以及何时触发


关键字段解析:



  • name: Skill 标识符(小写字母、数字、连字符)

  • description: 功能说明 + 触发场景(最重要的字段!)



⚠️ 重要: description 是 Skill 触发的关键。Claude 根据用户请求与 description 的匹配度决定是否加载该 Skill。



第二级:指令(Instructions)- 触发时加载


# PDF Processing

## Quick start

Use pdfplumber to extract text from PDFs:

\`\`\`python
import pdfplumber

with pdfplumber.open("document.pdf") as pdf:
text = pdf.pages[0].extract_text()
\`\`\`

For advanced form filling, see [FORMS.md](FORMS.md).


  • 加载时机: 当用户请求匹配 Skill 描述时

  • Token 消耗: 通常少于 5k tokens

  • 作用: 提供具体的操作指导和工作流程


第三级:资源和代码(Resources & Code)- 按需访问


pdf-skill/
├── SKILL.md # 主指令文件(第二级)
├── FORMS.md # 表单填写指南(按需读取)
├── REFERENCE.md # 详细 API 参考(按需读取)
└── scripts/
└── fill_form.py # 工具脚本(执行时不加载代码)


  • 加载时机: 仅当 SKILL.md 中引用时

  • Token 消耗: 脚本执行时只有输出占用 tokens

  • 作用: 提供专业参考材料和可执行工具


2.3 实例演示:从触发到加载


场景: 用户请求"帮我提取 PDF 中的文本"


┌─────────────────────────────────────────┐
步骤 1: Claude 检查所有 Skill 的元数据
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
匹配到 pdf-processing Skill
description 包含 "Extract text from PDF"
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
步骤 2: 加载 SKILL.md 的指令内容
(~3k tokens)
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
步骤 3: Claude 发现需要表单填写
读取 FORMS.md (~2k tokens)
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
Token 消耗: 5k tokens
其他 9 Skills: 0 tokens(未加载)
└─────────────────────────────────────────┘

对比无渐进式披露:


❌ 传统方式: 10 个 Skills × 5k = 50k tokens
✅ 渐进式披露: 只加载 1 个 Skill = 5k tokens
节省: 45k tokens (90% 的上下文!)



三、Skills 的文件结构


3.1 最小化 Skill


最简单的 Skill 只需要一个文件:


my-skill/
└── SKILL.md # 唯一必需的文件

SKILL.md 示例:


---
name: code-review-checklist
description: Provides a code review checklist for pull requests. Use when reviewing code or when the user asks for code review guidelines.
---


# Code Review Checklist

When reviewing code, check:

1. **Functionality**: Does the code do what it's supposed to?
2. **Readability**: Is the code easy to understand?
3. **Tests**: Are there appropriate tests?
4. **Performance**: Are there any obvious performance issues?
5. **Security**: Are there any security vulnerabilities?

For each item, provide specific feedback with examples.

3.2 完整 Skill 结构


对于复杂的 Skills,可以组织成多文件结构:


pdf-processing-skill/
├── SKILL.md # 核心指令(必需)
├── FORMS.md # 表单填写详细指南
├── REFERENCE.md # PDF 库 API 参考
├── EXAMPLES.md # 常见用例示例
└── scripts/
├── analyze_form.py # 分析表单工具
├── fill_form.py # 填写表单工具
└── validate.py # 验证输出工具

3.3 YAML Frontmatter 规范


必填字段:


---
name: skill-name # 必填
description: Skill description # 必填
---

字段要求:


字段要求示例
name小写字母、数字、连字符
最多 64 字符
禁止 "anthropic"、"claude"
pdf-processing
bigquery-analytics
code-reviewer
description非空
最多 1024 字符
包含功能 + 触发场景
第三人称描述
Extract text from PDFs. Use when...
Analyze BigQuery data. Use when...

命名规范:


推荐: 动名词形式(Gerund Form)


processing-pdfs
analyzing-spreadsheets
reviewing-code
managing-databases

避免: 过于模糊


helper          # 太模糊
utils # 不知道干什么
tool # 功能不明确

3.4 Description 字段的重要性


⚠️ 警告: description 是 Skill 触发的关键,必须用第三人称!


为什么必须第三人称?


description 会被注入到系统提示中,视角不一致会导致困惑:


系统提示: "You are Claude, an AI assistant..."
Skill description: "I can help you process PDFs" # ❌ 第一人称,视角冲突!

正确示例:


---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
Use when working with PDF files or when the user mentions PDFs, forms,
or document extraction.
---

不正确示例:


 description: I can help you process Excel files  # 第一人称
description: You can use this to process Excel # 第二人称
description: Helps with documents # 过于模糊

编写技巧:



  1. 明确功能: 说清楚 Skill 能做什么

  2. 包含关键词: 用户可能使用的术语(PDF、Excel、BigQuery 等)

  3. 触发场景: 明确何时使用("Use when...")

  4. 简洁精准: 1-2 句话说清楚




四、创建你的第一个 Skill


4.1 确定需求


问题导向:


问自己:



  1. 我反复向 Claude 解释什么内容?

  2. 哪些领域知识 Claude 不太了解?

  3. 哪些工作流程需要标准化?


示例场景:


场景 1: BigQuery 数据分析



  • ❌ 每次都要说明表结构

  • ❌ 每次都要强调"排除测试账户"

  • ❌ 每次都要说明查询模式

  • ✅ 创建一个 BigQuery Skill!


场景 2: 公司文档规范



  • ❌ 每次都要说明文档模板

  • ❌ 每次都要强调格式要求

  • ❌ 每次都要纠正不符合规范的部分

  • ✅ 创建一个文档规范 Skill!


4.2 编写 SKILL.md


步骤 1: 创建目录和文件


mkdir my-bigquery-skill
cd my-bigquery-skill
touch SKILL.md

步骤 2: 编写 YAML Frontmatter


---
name: bigquery-analytics
description: Analyze BigQuery data from the user_metrics and sales tables. Use when the user asks about data analysis, metrics, or BigQuery queries. Always exclude test accounts and apply standard date filters.
---

步骤 3: 编写核心指令


# BigQuery Analytics

## Database Schema

### user_metrics table
- user_
id (STRING): Unique user identifier

- event_date (DATE): Event date
- metrics_
value (FLOAT): Metric value
- account_type (STRING): "production" or "test"

### sales table
- order_
id (STRING): Order identifier
- user_id (STRING): User ID (foreign key to user_metrics)
- amount (FLOAT): Order amount
- order_date (DATE): Order date

## Standard Filtering Rules

**Always apply these filters:**
1. Exclude test accounts: `WHERE account_
type = 'production'`
2. Date range: Default to last 30 days unless specified
3. Remove null values: `WHERE metrics_value IS NOT NULL`

## Query Patterns

### Pattern 1: User activity analysis
\`\`\`sql
SELECT
event_date,
COUNT(DISTINCT user_
id) as active_users,
AVG(metrics_
value) as avg_metric
FROM user_
metrics
WHERE account_type = 'production'
AND event_
date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GR0UP BY event_date
ORDER BY event_
date;
\`\`\`

### Pattern 2: Sales analysis
\`\`\`sql
SELECT
DATE_TRUNC(order_date, MONTH) as month,
COUNT(*) as order_count,
SUM(amount) as total_revenue
FROM sales s
JOIN user_metrics u ON s.user_id = u.user_id
WHERE u.account_type = 'production'
GR0UP BY month
ORDER BY month;
\`\`\`

## Important Notes

- **Performance**: Always use partitioned date fields in WHERE clause
- **Costs**: Preview query cost before running on large datasets
- **Timezone**: All dates are in UTC



五、核心最佳实践


5.1 简洁为王(Conciseness is Key)


核心原则: 上下文窗口是公共资源,你的 Skill 要与系统提示、对话历史、其他 Skills 共享。


✅ 好的示例(约 50 tokens)


## Extract PDF Text

Use pdfplumber for text extraction:

\`\`\`python
import pdfplumber

with pdfplumber.open("file.pdf") as pdf:
text = pdf.pages[0].extract_text()
\`\`\`

❌ 糟糕的示例(约 150 tokens)


## Extract PDF Text

PDF(便携式文档格式)是一种常见的文件格式,包含文本、图像等内容。
要从 PDF 中提取文本,你需要使用一个库。有很多 PDF 处理库可用,
但我们推荐 pdfplumber,因为它易于使用且能处理大多数情况。
首先,你需要使用 pip 安装它。然后你可以使用下面的代码...

为什么简洁版更好?



  • ✅ 假设 Claude 已经知道 PDF 是什么

  • ✅ 假设 Claude 知道库的工作原理

  • ✅ 直接提供关键信息:用什么库、怎么用

  • ✅ 节省 100 tokens,留给其他 Skills 使用



⚠️ 记住: 不要低估 Claude 的智能!它是通用 AI,不需要你解释基础概念。



5.2 设置适当的自由度


根据任务的脆弱性和可变性,选择合适的指导程度。


🌟 高自由度(基于文本的指令)


适用场景:



  • 多种方法都可行

  • 决策依赖上下文

  • 启发式方法指导


示例:代码审查流程


## Code Review Process

1. Analyze code structure and organization
2. Check for potential bugs or edge cases
3. Suggest improvements for readability and maintainability
4. Verify compliance with project standards

特点: 给出大方向,信任 Claude 根据具体情况调整。


🎯 中等自由度(伪代码或带参数的脚本)


适用场景:



  • 存在首选模式

  • 允许一定变化

  • 配置影响行为


示例:生成报告


## Generate Report

Use this template and customize as needed:

\`\`\`python
def generate_report(data, format="markdown", include_charts=True):
# Process data
# Generate output in specified format
# Optionally include visualizations
\`\`\`

特点: 提供模板和参数,允许根据需求调整。


🔒 低自由度(特定脚本,少量或无参数)


适用场景:



  • 操作易错且脆弱

  • 一致性至关重要

  • 必须遵循特定顺序


示例:数据库迁移


## Database Migration

Execute this script strictly:

\`\`\`bash
python scripts/migrate.py --verify --backup
\`\`\`

Do not modify the command or add extra parameters.

特点: 精确指令,不允许偏离。


🌉 类比理解


把 Claude 想象成在不同地形上探索的机器人:



  • 悬崖边的窄桥(低自由度): 只有一条安全路径 → 提供详细护栏和精确指令

  • 丘陵地带(中等自由度): 几条推荐路径 → 提供地图和指南针

  • 无障碍的开阔草地(高自由度): 多条路径都能成功 → 给出大致方向,信任 Claude 找到最佳路线


5.3 渐进式披露模式


模式 1:高层指南 + 引用


结构:


SKILL.md (简要指南)
↓ 引用
[FORMS.md] [REFERENCE.md] [EXAMPLES.md]

示例:


# PDF Processing

## Quick Start

Use pdfplumber to extract text:
\`\`\`python
import pdfplumber
with pdfplumber.open("file.pdf") as pdf:
text = pdf.pages[0].extract_text()
\`\`\`

## Advanced Features

**Form Filling**: See [FORMS.md](FORMS.md) for complete guide
**API Reference**: See [REFERENCE.md](REFERENCE.md) for all methods
**Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns

优势:



  • Claude 只在需要时才读取 FORMS.md、REFERENCE.md 或 EXAMPLES.md

  • 未使用的文件 = 0 tokens 消耗


模式 2:按领域组织


适用场景: 多领域的 Skills,避免加载无关上下文


结构:


bigquery-skill/
├── SKILL.md # 概述和导航
└── reference/
├── finance.md # 财务指标
├── sales.md # 销售数据
├── product.md # 产品分析
└── marketing.md # 营销活动

示例:


# BigQuery Analytics

## Domain Reference

- **Finance Metrics**: See [reference/finance.md](reference/finance.md)
- **Sales Data**: See [reference/sales.md](reference/sales.md)
- **Product Analytics**: See [reference/product.md](reference/product.md)
- **Marketing Campaigns**: See [reference/marketing.md](reference/marketing.md)

优势:



  • 用户询问销售指标时,只读取 sales.md

  • finance.md 和其他文件保持在文件系统中,消耗 0 tokens


模式 3:条件细节


# DOCX Processing

## Create Documents

Use docx-js to create new documents. See [DOCX-JS.md](DOCX-JS.md).

## Edit Documents

For simple edits, modify XML directly.

**Track Changes**: See [REDLINING.md](REDLINING.md)
**OOXML Details**: See [OOXML.md](OOXML.md)

优势:



  • 常见操作(创建文档)在主文件中

  • 高级功能(追踪更改)按需引用


💡 重要: 保持引用层级为一级深度。避免 SKILL.md → advanced.md → details.md 这样的深层嵌套。


5.4 工作流和反馈循环


复杂任务的工作流模式


为多步骤任务提供清晰的检查清单:


## PDF Form Filling Workflow

Copy this checklist and track progress:

\`\`\`
Task Progress:
- [ ] Step 1: Analyze form (run analyze_form.py)
- [ ] Step 2: Create field mapping (edit fields.json)
- [ ] Step 3: Validate mapping (run validate_
fields.py)
- [ ] Step 4: Fill form (run fill_form.py)
- [ ] Step 5: Verify output (run verify_
output.py)
\`\`\`

**Step 1: Analyze Form**

Run: `python scripts/analyze_form.py input.pdf`

This extracts form fields and their locations, saving to `fields.json`.

**Step 2: Create Field Mapping**

Edit `fields.json` to add values for each field.

**Step 3: Validate Mapping**

Run: `python scripts/validate_fields.py fields.json`

Fix any validation errors before proceeding.

**Step 4: Fill Form**

Run: `python scripts/fill_form.py input.pdf fields.json output.pdf`

**Step 5: Verify Output**

Run: `python scripts/verify_output.py output.pdf`

If validation fails, return to Step 2.

实现反馈循环


常见模式: 运行验证器 → 修复错误 → 重复


这种模式极大提高输出质量。


示例:文档编辑流程


## Document Editing Flow

1. Make edits to `word/document.xml`
2. **Validate immediately**: `python ooxml/scripts/validate.py unpacked_dir/`
3. If validation fails:
- Review error messages carefully
- Fix issues in XML
- Run validation again
4. **Only proceed when validation passes**
5. Repack: `python ooxml/scripts/pack.py unpacked_dir/ output.docx`
6. Test output document

为什么反馈循环重要?



  • ✅ 及早发现错误(在应用更改前)

  • ✅ 机器可验证(脚本提供客观验证)

  • ✅ 可逆计划(Claude 可以迭代而不破坏原始文件)

  • ✅ 清晰调试(错误消息指向具体问题)


5.5 内容指南


避免时间敏感信息


糟糕示例(会过时):


如果你在 2025 年 8 月之前做这件事,使用旧 API。
2025 年 8 月之后,使用新 API。

好的示例(使用"旧模式"部分):


## Current Method

Use v2 API endpoint: `api.example.com/v2/messages`

## Legacy Patterns

<details>
<summary>Legacy v1 API (deprecated 2025-08)</summary>

v1 API uses: `api.example.com/v1/messages`

This endpoint is no longer supported.
</details>

使用一致的术语


在整个 Skill 中选择一个术语并坚持使用:


一致性好:



  • 始终使用 "API endpoint"

  • 始终使用 "field"

  • 始终使用 "extract"


不一致:



  • 混用 "API endpoint"、"URL"、"API route"、"path"

  • 混用 "field"、"box"、"element"、"control"

  • 混用 "extract"、"pull"、"get"、"retrieve"




六、高级技巧


6.1 包含可执行代码的 Skills


解决问题,而非推卸责任


编写 Skills 脚本时,显式处理错误情况,而非推卸给 Claude。


好的示例:显式处理错误


def process_file(path):
"""处理文件,如果不存在则创建。"""
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
# 创建默认内容而非失败
print(f"文件 {path} 未找到,创建默认文件")
with open(path, 'w') as f:
f.write('')
return ''
except PermissionError:
# 提供替代方案而非失败
print(f"无法访问 {path},使用默认值")
return ''

糟糕示例:推卸给 Claude


def process_file(path):
# 直接失败,让 Claude 自己想办法
return open(path).read()

提供工具脚本


即使 Claude 可以编写脚本,预制脚本也有优势:


工具脚本的好处:



  • 比生成代码更可靠

  • 节省 tokens(无需在上下文中包含代码)

  • 节省时间(无需代码生成)

  • 确保使用的一致性


09-02-skills-file-structure.png


示例:


## Tool Scripts

**analyze_form.py**: Extract all form fields from PDF

\`\`\`bash
python scripts/analyze_
form.py input.pdf > fields.json
\`\`\`

Output format:
\`\`\`json
{
"field_name": {"type": "text", "x": 100, "y": 200},
"signature": {"type": "sig", "x": 150, "y": 500}
}
\`\`\`

**validate_
boxes.py**
: Check for boundary box overlaps

\`\`\`bash
python scripts/validate_boxes.py fields.json
# Returns: "OK" or lists conflicts
\`\`\`

**fill_form.py**: Apply field values to PDF

\`\`\`bash
python scripts/fill_
form.py input.pdf fields.json output.pdf
\`\`\`

💡 重要区分: 在指令中明确说明 Claude 应该:



  • 执行脚本(最常见): "运行 analyze_form.py 以提取字段"

  • 读取作为参考(用于复杂逻辑): "参见 analyze_form.py 了解字段提取算法"


6.2 创建可验证的中间输出


当 Claude 执行复杂、开放式任务时,可能会出错。"计划-验证-执行"模式通过让 Claude 首先创建结构化格式的计划,然后在执行前用脚本验证该计划,从而及早发现错误。


示例场景


要求: Claude 根据电子表格更新 PDF 中的 50 个表单字段。


没有验证:Claude 可能:



  • ❌ 引用不存在的字段

  • ❌ 创建冲突的值

  • ❌ 遗漏必填字段

  • ❌ 错误应用更新


有验证:工作流变为:


分析 → 创建计划文件 → 验证计划 → 执行 → 验证输出

添加一个中间 changes.json 文件,在应用更改前进行验证。


实现示例


## Bulk Form Update Workflow

**Step 1: Analyze**
- Extract current form fields
- Save to `current_fields.json`

**Step 2: Create Change Plan**
- Based on spreadsheet, create `changes.json`:
\`\`\`json
{
"field_updates": [
{"field": "customer_name", "value": "John Doe"},
{"field": "order_total", "value": "1250.00"}
]
}
\`\`\`

**Step 3: Validate Plan**
- Run: `python scripts/validate_changes.py changes.json`
- Script checks:
- All referenced fields exist
- Values are in correct format
- No conflicts
- **Only proceed if validation passes**

**Step 4: Execute**
- Apply changes: `python scripts/apply_changes.py changes.json`

**Step 5: Verify Output**
- Run: `python scripts/verify_output.py output.pdf`

为什么此模式有效



  • 及早发现错误: 在应用更改前验证发现问题

  • 机器可验证: 脚本提供客观验证

  • 可逆计划: Claude 可以在不触及原始文件的情况下迭代计划

  • 清晰调试: 错误消息指向具体问题


使用时机



  • 批量操作

  • 破坏性更改

  • 复杂验证规则

  • 高风险操作


💡 实现技巧: 让验证脚本输出详细的错误消息:


❌ 模糊: "Validation failed"
✅ 清晰: "Field 'signature_date' not found. Available fields: customer_name, order_total, signature_date_signed"

这帮助 Claude 快速修复问题。


6.3 MCP 工具引用


如果你的 Skill 使用 MCP(Model Context Protocol)工具,始终使用完全限定的工具名称以避免"工具未找到"错误。


格式


ServerName:tool_name

示例


## Query Database Schema

Use the BigQuery:bigquery_schema tool to retrieve table schema.

\`\`\`
Use tool: BigQuery:bigquery_
schema
Parameters: {"table": "user_metrics"}
\`\`\`

## Create GitHub Issue

Use the GitHub:create_
issue tool to create an issue.

\`\`\`
Use tool: GitHub:create_issue
Parameters: {"title": "Bug report", "body": "Description"}
\`\`\`

说明



  • BigQueryGitHub 是 MCP 服务器名称

  • bigquery_schemacreate_issue 是这些服务器中的工具名称


没有服务器前缀,Claude 可能无法找到工具,特别是当有多个 MCP 服务器可用时。




七、常见反模式


❌ 反模式 1:Windows 风格路径


问题:使用反斜杠 \ 作为路径分隔符


错误:


参见 scripts\helper.py
参见 reference\guide.md

正确:


参见 scripts/helper.py
参见 reference/guide.md

原因:



  • Unix 风格路径跨所有平台工作

  • Windows 风格路径在 Unix 系统上会导致错误


❌ 反模式 2:提供太多选项


问题:列出所有可能的方法,让 Claude 困惑


错误:


你可以使用 pypdf,或 pdfplumber,或 PyMuPDF,或 pdf2image,
或 pikepdf,或 PyPDF2,或 pdfrw,或 pdfminer...

正确:


使用 pdfplumber 进行文本提取:
\`\`\`python
import pdfplumber
\`\`\`

对于需要 OCR 的扫描 PDF,改用 pdf2image 配合 pytesseract。

原则:



  • 提供默认推荐方法

  • 只在特殊情况下提供替代方案

  • 不要列出所有可能性


❌ 反模式 3:深层嵌套引用


问题:引用链太长,Claude 难以跟踪


错误:


SKILL.md → advanced.mddetails.md → examples.md

正确:


SKILL.md
↓ 直接引用
[ADVANCED.md] [DETAILS.md] [EXAMPLES.md]

原则:



  • 保持从 SKILL.md 的引用为一级深度

  • 所有引用文件应直接从 SKILL.md 链接


❌ 反模式 4:过度解释基础概念


问题:解释 Claude 已经知道的内容


错误:


PDF(Portable Document Format,便携式文档格式)是 Adobe 公司
开发的一种文件格式,可以在不同操作系统上保持一致的显示效果。
PDF 文件包含文本、图像、矢量图形等多种内容类型...

正确:


使用 pdfplumber 提取 PDF 文本。

原则:



  • 假设 Claude 的智能

  • 只提供 Claude 不知道的领域特定知识


❌ 反模式 5:第一人称描述


问题:使用"我"、"你"等人称


错误:


description: I can help you process Excel files and generate reports.

正确:


description: Process Excel files and generate reports. Use when working with spreadsheets or when the user mentions Excel, CSV, or data analysis.

原因:



  • description 被注入系统提示

  • 第一人称会导致视角冲突




八、总结与行动


8.1 核心收益


通过 Skills 系统,你可以:



  1. ⏱️ 节省时间: 不用每次重复说明领域知识

  2. ✅ 保证质量: 标准化流程,减少错误

  3. 📚 积累知识: 把最佳实践封装成 Skills,团队共享

  4. 🚀 提升专业性: 让 Claude 从通用助手进化为领域专家

  5. 🔧 持续优化: 基于使用反馈不断改进 Skills


8.2 Skills 与其他功能的关系


功能作用与 Skills 的关系
Agent处理复杂、多步骤任务Skills 为 Agent 提供领域知识
MCP连接外部工具和数据源Skills 可以引用 MCP 工具
claude.md项目级配置和规范Skills 是跨项目的能力扩展
Hook事件触发的自动化Hook 可以在特定时机加载 Skills

8.3 实践建议


对于个人开发者:



  1. 从一个简单的 Skill 开始(如代码审查清单)

  2. 识别自己反复解释的内容

  3. 逐步添加更多 Skills

  4. 持续优化基于实际使用


对于团队:



  1. 建立团队 Skills 仓库

  2. 统一 Skills 开发规范

  3. 定期分享优秀 Skills

  4. 建立 Skills 评审机制


对于技术 Leader:



  1. 推广 Skills 使用文化

  2. 组织 Skills 开发培训

  3. 激励团队贡献 Skills

  4. 建立 Skills 质量标准


8.4 未来展望


Skills 系统的发展方向:



  1. 可视化 Skill Builder: 通过图形界面创建 Skills

  2. Skill 市场: 官方 Skills 商店,一键安装分享

  3. AI 生成 Skills: 描述需求,AI 自动生成 Skills

  4. Skill 编排: 多个 Skills 组合成工作流

  5. 实时协作: 团队实时共享和更新 Skills



"把你反复向 Claude 解释的偏好、流程、领域知识打包成 Skills,让 AI 成为你的领域专家"





实用资源



🔗 相关文章:





如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题或建议,欢迎在评论区留言讨论。让我们一起学习,一起成长!


也欢迎访问我的个人主页发现更多宝藏资源


作者:冬奇Lab
来源:juejin.cn/post/7608382961723555890
收起阅读 »

消耗 760万 Token 后,一文看懂了“小龙虾” OpenClaw 和 OpenCode 的区别

前言 网上最近关于 OpenClaw 和 OpenCode 的讨论异常火爆,很多普通用户都在关注它们是否真的适合日常使用。OpenClaw 以稳定性和特定场景下的高可靠性吸引了一部分技术用户,但其操作体验和灵活性常常成为争议点。OpenCode 则因为能够读取...
继续阅读 »

前言


网上最近关于 OpenClaw 和 OpenCode 的讨论异常火爆,很多普通用户都在关注它们是否真的适合日常使用。OpenClaw 以稳定性和特定场景下的高可靠性吸引了一部分技术用户,但其操作体验和灵活性常常成为争议点。OpenCode 则因为能够读取 OpenClaw 的人格文件并进行学习而受到关注,它在文件处理和个性化学习方面显示出更大的潜力。本文将从灵活性、文件处理能力、对话消耗以及学习能力四个方面对两者进行深入分析,帮助读者判断哪一款更适合自己的需求。




所以呢,我个人 消耗了接近7500K的Token 专门深度的使用了下OpenClaw和OpenCode,为的就是探究下到底咋样。他们俩都适合干什么。


760万token
image.png


image.png
image.png

他俩的区别到底是啥




OpenClaw 主要面向快速生成内容和执行自动化任务,它在简单场景下表现稳定,但在文件处理和个性化学习上有一定局限。根据用户测试数据,每次完整对话消耗的 token 数通常在一万起步,对于频繁使用的普通用户来说成本较高。OpenCode 则更偏向代码管理和智能学习,它不仅可以读取多种文件格式,包括 OpenClaw 的人格文件,还能学习用户操作习惯,提高后续交互的效率和准确性。在同样的对话长度下,OpenCode 的 token 消耗相对更低,同时支持更复杂的文件处理和多轮学习。


对比维度OpenClawOpenCode
核心功能内容生成、自动化任务 、智能学习代码管理、文件处理
文件处理能力有时无法读取部分文件(或者报错,要不然就很卡)可读取多种文件格式,包括 OpenClaw 人格文件
灵活性较低,受限于固定流程高,可根据用户操作和文件内容进行学习
对话 token 消耗高,每次对话通常 15000+较低,同样长度对话消耗更少
学习能力支持学习用户操作习惯,实现个性化优化无持续学习能力

OpenClaw 在长期记忆和人格一致性方面表现更强,它能够通过人格文件和历史上下文维持稳定的角色行为,因此在连续对话中往往更接近真实的人类交流方式。随着使用时间增加,它的表达风格和行为习惯会逐渐固定下来,这让很多用户感觉它更像一个具有持续人格的助手。OpenCode 在这一点上的定位有所不同,它更偏向工具型架构,重点放在文件读取、代码处理和系统扩展能力上。虽然它可以读取 OpenClaw 的人格文件并进行学习,但整体设计仍然以效率和任务执行为主,而不是强调人格连续性。


对!!你没听错,他可以读取OpenClaw的文件!!
dc83609b74d8e6c2c01055b8e677e483.png
05047f192cbb4faf464da48e2f192b9a.png


编码速度对比


OpenCode


我相信,我们国内大多数人不可能去使用OpenClaw去处理文件和邮件,等待的时间成本太高太高了。
在编码速度方面,OpenCode 的表现通常明显快于 OpenClaw,这一点在实际开发场景中非常容易感知。OpenCode 本身就是为开发者设计的终端代码助手,它的交互模式、上下文管理以及模型调用方式都围绕“实时编程”进行优化,因此响应延迟非常低。在很多编码任务中,例如函数实现、代码补全或者简单重构,OpenCode 通常可以在 1到10秒内返回结果,复杂一点的多文件修改也大多在 10到30秒之间完成
(如图)




OpenClaw


相比之下,OpenClaw 的架构更偏向 自主代理系统(autonomous agent) 。当用户提出一个任务时,它往往需要经历多个步骤,例如分析任务、调用模型、读取数据、执行工具、再进行决策。每一步都可能触发新的模型调用,因此整个流程是串行执行的。在这种多步骤代理架构下,一个完整任务通常需要 30到90秒 才能完成(如图)


这种速度差异本质上来自两者的设计目标不同。OpenCode 是一个实时编码助手,强调低延迟交互和快速迭代。OpenClaw 更像一个自动化代理,它会自主规划任务步骤,因此在执行复杂流程时需要更多推理和调度时间


我们可以看到,OpenClaw 21秒的时候。OpenCode就已经开始修改文件了(也可能Claw没展示)
image.png


时间来到 1分10秒左右,OpenCode已经修改完成,而这边的Claw还在思考。 二者都用的GLM-4.7模型
image.png


总结


综上来看,OpenClaw 和 OpenCode 的定位虽然都属于 AI 编程工具,但两者在设计理念和使用体验上存在明显差异。OpenClaw 更接近一个具有长期记忆和人格特征的 AI 代理,它能够通过人格文件维持稳定的行为方式,在连续对话中表现出较强的一致性。对于希望构建 AI 助手、自动化任务或者进行长期交互的用户来说,这种能力具有一定吸引力。与此同时,OpenClaw 在执行复杂任务时通常会进行多步骤规划,因此在编码速度和 token 消耗方面往往更高,这在高频开发场景中会逐渐放大成本。


而且OpenClaw 更像一个具有记忆和人格的 AI 代理,而 OpenCode 更像一个高效率的开发助手。随着 AI 编程工具不断演进,未来很可能会出现同时兼具人格记忆、低成本对话以及高速编码能力的新一代工具,但在当前阶段,选择哪一个更多取决于个人的使用习惯以及具体的开发需求。


安装


OpenClaw 的安装方式。最常见的方式是通过 npm 进行全局安装:


npm install -g openclaw@latest

安装完成后,可以运行以下命令验证是否安装成功:


openclaw --version

OpenCode 的安装方式则相对更简单,大多数发行版本同样通过 npm 安装。安装命令通常为:


npm install -g opencode-ai

安装完成后可以通过以下命令启动:


opencode

在第一次运行时,OpenCode 会扫描当前项目目录并建立基础索引,从而让 AI 能够快速理解项目结构并参与代码编写。


作者:狗头大军之江苏分军
来源:juejin.cn/post/7615202491505754146
收起阅读 »

从爆红到被嫌弃,MCP 为什么开始失宠了

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优...
继续阅读 »

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。



如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。



如果你对 AI全栈 感兴趣,也欢迎添加我微信,我拉你进交流群



MCP 出生时,被捧得很高。


2024 年 11 月,Anthropic 发布"模型上下文协议",几乎所有 AI 开发者社区都在讨论这件事。它的定位很诱人,要成为大模型和外部工具之间通信的"通用标准",有点像当年 HTTP 对 Web 的意义。一时间,MCP server 满天飞,各种集成教程、开源实现层出不穷。


但时间只过了一年多。


上周,Perplexity 的联合创始人兼 CTO Denis Yarats 在内部表示,他们正在放弃 MCP,转而改用 APICLI。这个消息扩散出来后,引发了一波讨论,但讨论的内容不是"为什么",而是"早该如此"。


Y Combinator 的总裁兼 CEO Garry Tan 甚至直接说了一句话:"MCP sucks。"


MCP 的问题从来都不是技术实现不够好


很多人对 MCP 的质疑,停留在"不稳定"、"认证烦"这些体感上的抱怨。这些问题确实存在,但它们只是表象。MCP 真正的困境,是一个结构性问题。


MCP 的工作方式是,把工具的名称、描述、参数结构(Schema)以及使用示例,全部注入到 Agent 的上下文窗口里。Agent 读完这些信息,再决定要调用哪个工具。


这个设计在工具数量少时还可以接受。但你一旦接入 10 个服务,每个服务有 5 个工具,光是工具定义本身就已经烧掉了几千个 token。Agent 还没开始干活,上下文就已经塞满了一半。


上下文窗口是 Agent 最宝贵的资源,它决定了 Agent 能看见多少对话历史,能保留多少工作记忆,能有多大的推理空间。MCP 的代价,是把这个资源拿来"列菜单"。


面对这个问题,现有的出路只有三条:



  • 一次性加载所有工具,接受推理性能下降

  • 限制接入工具数量,接受 Agent 能力边界收窄

  • 构建动态工具加载机制,接受额外的延迟和复杂度


三条路都不好走。这不是"实现质量"的问题,而是协议设计本身的代价。


除此之外,日常使用中的痛点也不少。MCP server 启动失败是家常便饭,有时重试能解决,有时必须推倒重来。接入多个服务就要在每个服务上重新认证一遍。权限管理也只有"允许"和"不允许"两档,没有办法把某个工具限制为只读,也没有办法约束它可以传什么参数。


CLI 是更好的答案,不是因为它新,而是因为它够旧


工程师 Eric Holmes 写过一篇文章,观点直接:MCP 没有带来任何实际价值,LLM 完全可以自己搞懂怎么用 CLI


这话有点刺,但它说的是实情。


大模型在训练时看过海量的 man 手册、Stack Overflow 回答和 GitHub 上的 Shell 脚本。它们对 CLI 的理解,远比对某个 MCP server 的理解深得多。给它一个命令行工具和一份文档,它就能上手,不需要特殊适配。


CLI 在几个关键点上,比 MCP 天然占优。


第一是可调试性。当 Claude 对 Jira 执行了一个出乎意料的操作,你可以直接跑同一条 jira issue view 命令,看看它看到了什么。输入一致,输出一致,没有谜团。但 MCP 的调用只发生在 LLM 的对话内部,出问题了只能去翻复杂的 JSON 传输日志。


第二是可组合性。这是 CLI 的核心竞争力。你可以用 jq 过滤数据,用 grep 串联逻辑,把输出重定向到文件。这不只是方便,很多时候这是唯一可行的路。MCP 没有这个能力,你要么把完整数据塞进上下文,要么在 server 端自己写过滤逻辑,两种方式都在用更多的精力换取更差的结果。


第三是认证。CLI 复用的是系统级别的认证体系,这套东西已经经过几十年的打磨。MCP 需要你重新为每个工具搭一遍认证流程。


这件事说明了什么


Perplexity 放弃 MCP,以及其他工具陆续移除 MCP 支持,这件事背后有一个更值得思考的信号。


给 AI 构建工具链,不需要发明一套新的协议。AI 需要的工具,和人类需要的工具,在很多时候是同一套。最好的工具是对人类和机器都好用的工具。


CLI 存在了几十年,设计上一直遵循一个哲学,每个工具做好一件事,然后把工具组合起来解决复杂问题。这套哲学放到 Agent 身上,依然成立。


MCP 想构建一个更"现代"的抽象层,但它解决的问题,现有工具已经解决得够好了。在不需要额外抽象的地方强行加一层,带来的只有额外的成本和复杂度。


当然,MCP 不会完全消失。在某些特定场景,比如需要强类型 Schema、有严格访问控制要求的企业内部系统,它依然有它的位置。但作为"AI 工具集成的通用标准",这个定位恐怕很难站稳了。


参考:



作者:Moment
来源:juejin.cn/post/7617816762886881286
收起阅读 »

程序员,你使用过灰度发布吗?

大家好呀,我是猿java。 在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。 1. 什么是灰度发布? 简单来说,灰度发布也叫做渐进式发布或金丝雀发布,它是一种逐步将新版本应用到生产...
继续阅读 »

大家好呀,我是猿java


在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。


1. 什么是灰度发布?


简单来说,灰度发布也叫做渐进式发布金丝雀发布,它是一种逐步将新版本应用到生产环境中的策略。相比于一次性全量发布,灰度发布可以让我们在小范围内先行测试新功能,监控其表现,再决定是否全面推开。这样做的好处是显而易见的:



  1. 降低风险:新版本如果存在 bug,只影响少部分用户,减少了对整体用户体验的冲击。

  2. 快速回滚:在小范围内发现问题,可以更快地回到旧版本。

  3. 收集反馈:可以在真实环境中收集用户反馈,优化新功能。


2. 原理解析


要理解灰度发布,我们需要先了解一下它的基本流程:



  1. 准备阶段:在生产环境中保留旧版本,同时引入新版本。

  2. 小范围发布:将新版本先部署到一小部分用户,例如1%-10%。

  3. 监控与评估:监控新版本的性能和稳定性,收集用户反馈。

  4. 逐步扩展:如果一切正常,将新版本逐步推广到更多用户。

  5. 全面切换:当确认新版本稳定后,全面替换旧版本。


在这个过程中,关键在于如何切分流量,确保新旧版本平稳过渡。常见的切分方式包括:



  • 基于用户ID:根据用户的唯一标识,将部分用户指向新版本。

  • 基于地域:先在特定地区进行发布,观察效果后再扩展到其他地区。

  • 基于设备:例如,先在Android或iOS用户中进行发布。


3. 示例演示


为了更好地理解灰度发布,接下来,我们通过一个简单的 Java示例来演示基本的灰度发布策略。假设我们有一个简单的 Web应用,有两个版本的登录接口/login/v1/login/v2,我们希望将百分之十的流量引导到v2,其余流量继续使用v1


3.1 第一步:引入灰度策略


我们可以通过拦截器(Interceptor)来实现流量的切分。以下是一个基于Spring Boot的简单实现:


import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Random;

@Component
public class GrayReleaseInterceptor implements HandlerInterceptor {

private static final double GRAY_RELEASE_PERCENT = 0.1; // 10% 流量

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if ("/login".equals(uri)) {
if (isGrayRelease()) {
// 重定向到新版本接口
response.sendRedirect("/login/v2");
return false;
} else {
// 使用旧版本接口
response.sendRedirect("/login/v1");
return false;
}
}
return true;
}

private boolean isGrayRelease() {
Random random = new Random();
return random.nextDouble() < GRAY_RELEASE_PERCENT;
}
}

3.2 第二步:配置拦截器


在Spring Boot中,我们需要将拦截器注册到应用中:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private GrayReleaseInterceptor grayReleaseInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(grayReleaseInterceptor).addPathPatterns("/login");
}
}

3.3 第三步:实现不同版本的登录接口


import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/login")
public class LoginController {

@GetMapping("/v1")
public String loginV1(@RequestParam String username, @RequestParam String password) {
// 旧版本登录逻辑
return "登录成功 - v1";
}

@GetMapping("/v2")
public String loginV2(@RequestParam String username, @RequestParam String password) {
// 新版本登录逻辑
return "登录成功 - v2";
}
}

在上面三个步骤之后,我们就实现了登录接口地灰度发布:



  • 当用户访问/login时,拦截器会根据设定的灰度比例(10%)决定请求被重定向到/login/v1还是/login/v2

  • 大部分用户会体验旧版本接口,少部分用户会体验新版本接口。


3.4 灰度发布优化


上述示例,我们只是一个简化的灰度发布实现,实际生产环境中,我们可能需要更精细的灰度策略,例如:



  1. 基于用户属性:不仅仅是随机切分,可以根据用户的地理位置、设备类型等更复杂的条件。

  2. 动态配置:通过配置中心动态调整灰度比例,无需重启应用。

  3. 监控与告警:集成监控系统,实时监控新版本的性能指标,异常时自动回滚。

  4. A/B 测试:结合A/B测试,进一步优化用户体验和功能效果。


grayscale-release.png


4. 为什么需要灰度发布?


在实际工作中,为什么我们要使用灰度发布?这里我们总结了几个重要的原因。


4.1 降低发布风险


每次发布新版本,尤其是功能性更新或架构调整,都会伴随着一定的风险。即使经过了充分的测试,实际生产环境中仍可能出现意想不到的问题。灰度发布通过将新版本逐步推向部分用户,可以有效降低全量发布可能带来的风险。


举个例子,假设你上线了一个全新的支付功能,直接面向所有用户开放。如果这个功能存在严重 bug,可能导致大量用户无法完成支付,甚至影响公司声誉。而如果采用灰度发布,先让10%的用户体验新功能,发现问题后只需影响少部分用户,修复起来也更为迅速和容易。


4.2 快速回滚


在传统的全量发布中,一旦发现问题,回滚到旧版本可能需要耗费大量时间和精力,尤其是在高并发系统中,数据状态的同步与恢复更是复杂。而灰度发布由于新版本只覆盖部分流量,问题定位和回滚变得更加简单和快速。


比如说,你在灰度发布阶段发现新版本的某个功能在某些特定条件下会导致系统崩溃,立即可以停止向新用户推送这个版本,甚至只针对受影响的用户进行回滚操作,而不用影响全部用户的正常使用。


4.3 实时监控与反馈


灰度发布让你有机会在真实的生产环境中监控新版本的表现,并收集用户的反馈。这些数据对于评估新功能的实际效果至关重要,有助于做出更明智的决策。


举个具体的场景,你新增了一个推荐算法,希望提升用户的点击率。在灰度发布阶段,你可以监控新算法带来的点击率变化、服务器负载情况等指标,确保新算法确实带来了预期的效果,而不是引入了新的问题。


4.4 提升用户体验


通过灰度发布,你可以在推出新功能时,逐步优化用户体验。先让一部分用户体验新功能,收集他们的使用反馈,根据反馈不断改进,最终推出一个更成熟、更符合用户需求的版本。


举个例子,你开发了一项新的用户界面设计,直接全量发布可能会让一部分用户感到不适应或不满意。灰度发布允许你先让一部分用户体验新界面,收集他们的意见,进行必要的调整,再逐步扩大使用范围,确保最终发布的版本能获得更多用户的认可和喜爱。


4.5 支持A/B测试


灰度发布是实现A/B测试的基础。通过将用户随机分配到不同的版本,你可以比较不同版本的表现,选择最优方案进行全面推行。这对于优化产品功能和提升用户体验具有重要意义。


比如说,你想测试两个不同的推荐算法,看哪个能带来更高的转化率。通过灰度发布,将用户随机分配到使用算法A和算法B的版本,比较它们的表现,最终选择效果更好的算法进行全面部署。


4.6 应对复杂的业务需求


在一些复杂的业务场景中,全量发布可能无法满足灵活的需求,比如分阶段推出新功能、针对不同用户群体进行差异化体验等。灰度发布提供了更高的灵活性和可控性,能够更好地适应多变的业务需求。


例如,你正在开发一个面向企业用户的新功能,希望先让部分高价值客户试用,收集他们的反馈后再决定是否全面推广。灰度发布让这一过程变得更加顺畅和可控。


5. 总结


本文,我们详细地分析了灰度发布,它是一种强大而灵活的部署策略,能有效降低新版本上线带来的风险,提高系统的稳定性和用户体验。作为Java开发者,掌握灰度发布的原理和实现方法,不仅能提升我们的技术能力,还能为团队的项目成功保驾护航。


对于灰度发布,如果你有更多的问题或想法,欢迎随时交流!


6. 学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7488321730764603402
收起阅读 »

项目经理被裁那天,没人替他说话

简单写个自我介绍。 我在团队里算不上的管理层,没有人事权,也不决定谁走谁留,但我参与的项目一旦出问题,最后都会落到我这里。进度卡住、需求反复、线上事故,会议上可以绕一圈再绕一圈,最终还是要有人把代码改完、把锅兜住。我清楚自己的位置,用武之地比较多的一个“多功能...
继续阅读 »

简单写个自我介绍。


我在团队里算不上的管理层,没有人事权,也不决定谁走谁留,但我参与的项目一旦出问题,最后都会落到我这里。进度卡住、需求反复、线上事故,会议上可以绕一圈再绕一圈,最终还是要有人把代码改完、把锅兜住。我清楚自己的位置,用武之地比较多的一个“多功能开发者”而已。还依稀记得,当初总部另外一个项目的入侵测,找的还是我,而不是再招一个人。


所以我很少参与评价人,只谈事,谈事实,谈项目是怎么一步步偏离轨道的。


先直接告诉大家结果吧,他被辞退了。


当初公司决定要不要这个人的时候,无意中跟我提到过。我只讲述了几个项目事故,以及其他同事和他合作时的态度。至于留不留他,我不想决定,也决定不了。


那个项目经理,其实并不“坏”。他不吼人,也不甩脸色,会议纪要写得很勤,群里回复永远是“收到”“我跟一下”。问题出在另一层面:需求变更没有留痕,风险评估永远是“可控”,节点延期总能找到外部理由。


上面看到的是一条被不断抚平的曲线,下面看到的是每天被推翻重来的开发计划。我们不是没提醒过,只是提醒被整理成了更好看的版本,再往上递的时候,已经失去了原本的锋利。当然,这些最后都会被归结为一句话——开发同学多努努力,多扩展下思维,补补这个缺点就好了。


但我认为,有些问题其实在内部一直被反复提起,只是从来没有被真正放到台面上说过。


团队里的其他项目经理,大多都有过开发背景。哪怕代码早就不写了,对功能复杂度、实现成本、技术边界心里都是有尺度的。评估的时候会留余量,也知道什么时候该踩刹车。


只有他完全没有开发经验,对一个需求的理解停留在“看起来不难”的层面。既怕自己显得不专业,又怕在会上被认为拖进度,于是每次评估都偏向最激进的版本,功能报得满,时间压到极限。


开发这边明知道不现实,那又怎么办呢?你能说得过他吗?况且领导也是只看结果,活干得快,公司赚得多,干得慢赚得少。所以开发也只能硬着头皮往前推。


一次延期还能解释成意外,两次三次之后,延期就成了默认选项。项目表面上在跑,实际上每一步都在透支客户的耐心。


他甚至能把一个月的功能,压成 7 个工作日。


结果显而易见。项目连夜上线,第二天直接崩溃:APP、小程序白屏,数据无法保存,ToC 的用户一个都打不开。我们凌晨 4 点发完版本,早上 6 点半问题出现,7 点钟起床开始处理。


我起床的时候就已经料到了。项目有他管控着,您就放一万个心吧,麻烦肯定少不了。


当时写功能的时候,有个同事请了丧假。他来了句逆天发言:“到时候你能把电脑带上吗?有事可以找你。”


我当时真想告诉他,兄弟,全公司不是只有他一个前端,这个项目也不是只有他一个前端。人家就请假 3 天,已经很紧张了,还让人把电脑带着,真特么丧良心。


真正的转折点,是那次 A 项目上线。


我没有提任何人的名字,也没有用情绪化的词,只是把时间线拉直:哪一天确认需求,哪一天推翻,哪一天出 PRD,哪一天出 UI,最终导致了什么结果。那份文档写得很长,不好读,也不“体面”,但它有一个特点——每一个问题,都自然地指向了同一个岗位职责。


我提交的时候,甚至没多想,只觉得这次总算把事情说清楚了。


事后我想过,如果我当初不写那份复盘,不跟领导说这些事,会不会结果不同。答案大概是否定的。项目不会因为沉默变好,问题也不会因为不点名而消失。


那天没人替他说话,并不是因为他人缘差,而是因为在那个位置上,他已经很久没有为任何人、任何结果,真正说过一句“这是我的责任”。


系统从来不需要情绪,它只是在某个时刻,停止了包容。


我后来也明白了一件事:在很多公司里,项目经理这个角色,本质上是一个缓冲层。缓冲需求、缓冲压力、缓冲管理层的焦虑。


但一旦缓冲只剩下过滤,没有承担,系统就会重新校准。


那天被裁的不是一个人,而是一种失效的角色设计。而这件事,迟早会发生在任何一个不再为结果站出来的位置上。


作者:狗头大军之江苏分军
来源:juejin.cn/post/7598174154665623587
收起阅读 »

北京回长沙了,简单谈谈感受!

大家好呀,我是飞鱼 我今年已经从北京回长沙了,这里谈谈感受。 ❝ 首先我回长沙不是逃离,而是换一种更舒服、更可持续的生活方式。 北京给了我视野和能力,长沙给了我生活和归属。 最直观的变化 节奏慢了:不用挤早高峰了,走路不用小跑,回家路更短。 心态稳了:不...
继续阅读 »

大家好呀,我是飞鱼


我今年已经从北京回长沙了,这里谈谈感受。




首先我回长沙不是逃离,而是换一种更舒服、更可持续的生活方式。


北京给了我视野和能力,长沙给了我生活和归属。



最直观的变化



  • 节奏慢了:不用挤早高峰了,走路不用小跑,回家路更短。

  • 心态稳了:不再天天赶进度、追KPI,人也没那么紧绷了。


生活成本:压力明显小了




房租、通勤、日常开销都降了不少,以前在北京工资高,但大头都被生活成本吃掉了。


现在收入可能少些,但心里踏实很多。



个人生活:更松弛也更有边界




回来后作息更规律了,能早点睡、早点起,周内也会留出时间运动或散步。


以前下班只想躺着刷手机,现在会给自己留一点空白时间,用来读书、整理思路或者陪家人聊天。


生活变简单,但心里更笃定,能把注意力放在真正重要的人和事上。



城市气息:更有生活感




长沙烟火气足,我现在每周都会去爬一次岳麓山(离得近)。


周末也能随时约上朋友一起吃饭聊天,不用再掐着时间赶路。



个人成长:从外部驱动到自我驱动




以前在北京,节奏和环境会推着我走,事情一件接一件,来不及想太多。


回长沙后,外部推力小了,但我开始主动搭自己的节奏:给自己设目标、做复盘、安排学习计划。


慢下来之后,反而更能看清自己擅长什么、缺什么,也更容易把工作和生活都经营得更稳。



给同样选择的人一点建议



  • 先想清楚你想要什么:是离家近、生活压力小,还是职业成长更快?别只因为累了就决定,要有明确的取舍。

  • 提前做资源准备:无论去哪,职业发展都得靠自己,技能储备、作品、圈子都要主动经营。

  • 规划现金流:收入变化要提前算清楚,别让生活压力反过来影响判断。

  • 给自己一个过渡期:回去不是立刻完美适应,给自己几个月调整节奏,别太焦虑。


最后想说


适合自己的地方,不一定是机会最多的地方,而是能让你活得更从容、有力量的地方。




最后想看技术文章的,可以去我的个人网站:hardyfish.top/



作者:程序员飞鱼
来源:juejin.cn/post/7603781883973763091
收起阅读 »

用 Trae + MCP 让 AI 自动测试页面

前言 想必大家在前段时间关于 “豆包手机” 一定有所耳闻,说实话在看过了不少博主的体验和测评之后呀,不禁有点感叹现代的AI发展了🤯。 但是有一个让我比较好奇的功能🤔,那就是 AI Agent是如何通过用户提供的提示词来直接操控应用的呢? 毕竟对于大多数人来说 ...
继续阅读 »

前言


想必大家在前段时间关于 “豆包手机” 一定有所耳闻,说实话在看过了不少博主的体验和测评之后呀,不禁有点感叹现代的AI发展了🤯。


但是有一个让我比较好奇的功能🤔,那就是 AI Agent是如何通过用户提供的提示词来直接操控应用的呢?
毕竟对于大多数人来说 AI 还停留在聊天对话的阶段,是什么东西让 AI 突然拥有了自己的 “手”与“眼” 呢?


因此我就去稍微了解了一下与这方面有关的一些知识,结果碰到了 mcp协议playwright


于是我抱着一颗学徒的心,来试试在目前很火的 AI 编译器 Trae 来看看会摩擦出怎样的火花🔥。


一、什么是 MCP(Model Context Protocol)?


定义:


MCP 协议是一个开放、标准化的通信协议,而它的核心作用是:



让大语言模型能够安全、结构化地调用外部工具、访问上下文数据,并与真实世界交互。



说白了,MCP 是给 AI 装上“手”和“眼”的接口。


在 MCP 出现之前,AI 编程助手存在严重局限:



  • 只能沟通,不能实施:虽然 AI 可以帮助生成代码,但是不能直接运行、测试或验证它是否真的达到了需要的效果。

  • 信息获取步骤复杂:AI 并不知道我们本地有哪些文件、依赖等等,大多数时候需要手动操作来提供给它。


但是通过 MCP 这个“桥梁”,让 AI 作为客户端通过标准协议,连接一个或多个 MCP Server从而把外部程序(比如 Playwright、Shell 脚本等)接入到 AI 编译器中,让 AI 能调用它们。


例如:“模型 + 工具生态”协同
tongyi-mermaid-2025-12-15-184415.png


在这整个过程中,用户仅仅只是向 AI Agent 下达了测试登录的命令,在此之后的过程都无需人工干预,AI 可以自主协调多个工具完成闭环。


AI 就像公司的 CEO,通过 MCP 协议向多个部门(MCP Server)下达命令


如果你想得到更官方的解释,可以去官网What is the Model Context Protocol (MCP)?


二、什么是 Playwright?


Playwright 是由 Microsoft 开发的 现代化端到端(E2E)Web 测试工具,用于自动化 Chromium、WebKit 和 Firefox 浏览器的 Node.js/Python/Java/.NET 库。


但是由于 AI + MCP 的存在,我们并不需要精通这个工具,把事情交给AI,让我们更注重于功能。


三、MCP + Playwright 的协同


下述示例都是通过使用 Trae 来实现的


前期工作:


打开我们的 Trae 编辑器


image.png


在设置页面找到 MCP


image.png


点击右边的 添加 按钮,这里有两种方法添加,从市场添加 / 手动添加,这里我们点击从市场添加即可


image.png


在市场这里可以添加任何你需要的 MCP Server,并且可以输入你需要配置的 MCP Server 信息。这里我们添加 playwright


2025-12-15.gif


安装好后回到主页面上,在 TRAE 智能体聊天框中选择 @Builder with MCP


image.png



注:


不要忘记 playwright 只适配 Chromium、WebKit 和 Firefox浏览器,各位只需提前安装好其一即可



示例一:让 AI 自动访问页面


给 AI 提示词:


请帮我测试掘金网站的插件页面:
1. 打开 https://juejin.cn/
2. 点击顶部导航栏的"插件"按钮
3. 等待页面加载完成
4. 截图并保存到当前目录

image.png


最后实现结果:
image.png


示例二:让 AI 自动测试页面


现在我test文件夹下有一个 1.html文件,给 AI 提示词让它帮我测试页面的基本效果。


请确认 1.html 文件的以下能力:
1. 测试是否能正确通过文件路径加载本地 HTML。
2. 测试 fill和click是否精准。
3. 通过验证结果文本,确保页面逻辑正常运行且被测试工具正确捕获。
4. 通过截图提供最终的视觉证据。

动画.gif


四、这代表未来


传统开发AI + MCP + Playwright
写代码 → 手动测试 → 调试 → 修复描述需求 → AI 生成 + 自动测试 + 自动修复
测试是“事后”行为测试是“内建”能力
人做机械性验证AI 做闭环验证


这正是 “Agentic AI”(智能体式开发) 的核心:AI 不再只是生成代码,而是能自主执行、验证、迭代



作者:谎言西西里
来源:juejin.cn/post/7583898823921008682
收起阅读 »

中国四大软件外包公司

在程序员的职业字典里,每次提到“外包”这两个字,似乎往往带着一种复杂的况味,不知道大家对于这个问题是怎么看的? 包括我们在逛职场社区时,也会经常刷到一些有关外包公司讨论或选择的求职帖子。 的确,在如今的 IT 职场大环境里,对于许多刚入行的年轻人,或者很多寻求...
继续阅读 »

在程序员的职业字典里,每次提到“外包”这两个字,似乎往往带着一种复杂的况味,不知道大家对于这个问题是怎么看的?


包括我们在逛职场社区时,也会经常刷到一些有关外包公司讨论或选择的求职帖子。


的确,在如今的 IT 职场大环境里,对于许多刚入行的年轻人,或者很多寻求机会的开发者来说,外包公司或许也是求职过程中的一个绕不开的备选项。


今天这篇文章,我们先来聊一聊 IT 江湖里经常被大家所提起的“四大软件外包公司”,每次打开招聘软件,相信不少同学都刷到过他们的招聘信息。


他们在业内也一度曾被大家戏称为“外包四大金刚”,可能不少同学也能猜到个大概。


1、中软国际


中软可以说是国内软件外包行业的“老大哥”之一,拥有约 8 万名员工,年收入规模高达 170 亿。


而且中软的业务版图确实很大,在国内外 70 个城市重点布局,在北京、西安、深圳、南京等地均拥有自有产权的研发基地。


提起中软,很多同学的第一反应是它和华为的“深度绑定”。


的确,华为算是中软比较大的合作伙伴之一,同样,这种紧密的合作关系,让中软在通信、政企数字化等领域获得了不少份额。


在中软的体系里,经常能看到一种非常典型的“正规化”打法。它的流程比较规范,制度也非常完善。这对于刚毕业的大学生或者想要转行进入 IT 的人来说,算是一个不错的“练兵场”。


不过近年来,中软也在拼命转型,试图摆脱单纯的外包标签,在 AIGC 和鸿蒙生态上投入了不少精力。


2、软通动力


如果说上面的中软是“稳扎稳打”的代表,那么软通给人的感觉就是“迅猛扩张”。


软通虽然成立时间比中软晚了几年,但发展势头却非常迅猛。


根据第三方机构的数据显示,软通动力在 IT 服务市场的份额已经名列前茅,甚至在某些年份拔得头筹。


软通这家公司一直给人的印象是“大而全”。它的总部在北京,员工规模甚至达到了 90000 人。


而软通动力的上市,一度给行业打了一剂强心针。它的业务线覆盖了从咨询到 IT 服务的全生命周期,包含了金融、能源、智能制造、ICT 软硬件、智能化产品等诸多方面。


3、东软集团


如果说前两家是后来居上的代表,那么东软就是老牌子软件公司的代表。


成立于 1991 年的东软,是中国上市较早的软件公司之一,早在 1996 年就上市了。


东软最初创立于东北大学,后来通过国际合作进入汽车电子领域,并逐渐踏上产业化发展之路,其创始人刘积仁博士也算是软件行业的先驱大佬了。


东软的业务重心很早就放在了医疗健康、智慧城市和汽车电子等这几个领域。


说不定现在很多城市的医院里,跑着的 HIS 系统有可能就是东软做的。


虽然近年来东软也面临着转型阵痛,但它在医疗和智慧城市等领域的积淀,依然是其他外包公司难以撼动的。


4、文思海辉(中电金信)


这家公司的发展历程比较特殊,它经历过文思创新和海辉软件的合并,后来又加入了中国电子(CEC)的阵营,成为中国电子旗下的一员,并且后来又进一步整合为了中电金信。


所以它现在更多地以“中电金信”的身份出现。


文思海辉的强项在于金融和数智化领域,尤其银行业 IT 项目这一块做了非常多,市场份额也很大。


那除了上面这几个“外包巨头”之外,其实很多领域还有很多小型外包公司,有的是人力资源外包,有的则是项目外包。


每次提到「外包」这个词,可能不少同学都会嗤之以鼻,那这里我也来聊聊我自己对于外包的一些个人看法和感受


说实话,我没有进过外包公司干过活,但是呢,我和不少外包公司的工作人员共事过,一起参与过项目。


记得老早之前我在通信公司工作时,我们团队作为所谓的“甲方”,就和外包员工共事过有大半年的样子,一起负责公司的核心网子项目。


有一说一,我们团队整体对外包同事都是非常友好的。


我看网上有那种什么外包抢了红包要退钱、什么提醒外包注意素质不要偷吃的零食的事情,有点太离谱、太夸张了,这在我们团队那会是从来没有发生过的。


大家平时在一起上班的氛围也挺融洽,大家一起该聊天聊天,该开玩笑开玩笑,该一起吃饭一起吃饭,在相处方面并没有什么区别。


但是,不同地方的确也有。


比方说,他们上班时所带的工牌带子颜色就和我们不太一样,这一眼就能看出来,另外平时做的事情也有点不太一样。


我记得当时项目的一些抓包任务、测试任务、包括一些标注任务等等都是丢给外包同事那边来完成,我们需要的是结果、是报告。


另外对于项目文档库和代码库的权限也的确有所不同,核心项目代码和文档确实是不对外包同事那边开放的。


除此之外,我倒并没有觉得有什么太多的不同。


那作为程序员,我们到底该如何看待这些外包公司呢


这就好比是一个围城,城外的人有的想进去,城里的人有的想出来。


每次一提到外包,很多人的建议都是不要进,打亖别去。但是,这里有个前提是,首先得在你有的选的情况下,再谈要不要选的问题


不可否认的是,外包公司确实有它的短板。最被人诟病的两点,一个“职业天花板”问题、一个“归属感缺失”问题。


但是在当下的就业环境里,我们不得不承认的是,外包公司也承担了 IT 行业“蓄水池”的角色。


毕竟并不是每个人一毕业就能拿到互联网大厂的 offer,也并不是每个人都有勇气去创业公司搏一把。


对于有些学历一般、技术基础一般或者刚转行的程序员来说,外包也提供了另外一个选择。


而如果你现在正在外包或者正在考虑加入外包,那这里我也想说几句肺腑之言


第一,不要把外包作为职业生涯的终点,而应该把它看作一个跳板或过渡。


如果你刚毕业进不去大厂,或者在一二线城市没有更好的选择,那外包可以为你提供一个接触正规项目流程的机会(当然前提是要进那种正规的外包),我们也可以把它看昨一个特殊的职场驿站。


在那里的每一天,你都要问问自己:我学到了什么?我的技术有没有长进?我的视野有没有开阔?


第二,一定要警惕“舒适区”。


很多同学在外包待久了,可能会陷入一种拿工资办事的机械式工作中,看起来很舒适,实际上很危险。


注意,一定要利用能接触到的资源,去学习项目的技术架构和业务流程,去想办法提升自己的核心竞争力,而不是仅仅为了完成工时。


最后我想说的是,无论你是在大厂做正式员工,还是在小团队里打拼,亦或是在外包公司里默默耕耘,最终决定职业高度的,并不是工牌上公司的名字,而是会多少技术,懂多少业务,能解决多少问题,大家觉得呢?


好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7585839411122454574
收起阅读 »

AI 做公众号漫画,竟然日入 400+

0. 前言 在 2025年的尾巴上,发生了一件非常有趣的事,我在微信公众号上的 AI 四格漫画 意外爆火。之前公众号上发的技术文章,基本上阅读量不过 300,每天广告收益也就几毛钱。目前最火的美杜莎,浏览量已经达到惊人的 5W。这样让我不禁感叹: 十年技术无...
继续阅读 »

0. 前言


在 2025年的尾巴上,发生了一件非常有趣的事,我在微信公众号上的 AI 四格漫画 意外爆火。之前公众号上发的技术文章,基本上阅读量不过 300,每天广告收益也就几毛钱。目前最火的美杜莎,浏览量已经达到惊人的 5W。这样让我不禁感叹:



十年技术无人问,一曲漫笑广人闻。






火爆之后,带来的最直接价值就是迎来了泼天富贵。从未想过有一天,我的日广告收益能达到 400+ ,目前已经连续四天高位。除了金钱,自己的作品受到欢迎,以及大家在评论区的吐槽、讨论,也为我带来了很大的情绪价值。


--
7cb241368f7743365383b9a1ea1a90ed.jpgIMG_4554.PNG



1. 缘起


先简单介绍一下:我是一个籍籍无名的编程技术小博主,全网统一名号 张风捷特烈编程之王 是我维护的公众号,一直是输出编程技术的文章,主要以 Flutter 技术为主。

但技术文章更新的不是非常频繁,而公众号每天有一篇发文的机会。本着 不想浪费 的优良传统,在 AI 重塑一切的浪潮中,我想用 AI 画些四格漫画的笑话试试。于是开启了 慧心一笑 专栏, 《小火柴的倒霉日常》 就是第一篇,现在还没火。大家也可以点开看看,内容非常精简,就是一幅图+提示词。



这个系列整体是诙谐幽默的,下面是第一篇的内容:



一开始我是用自然语言的提示词,感觉效果并不是太好,四格漫画有着连续的信息和一致性的人物、场景等。由于编程出身,在 结构一致性 方面有着天然的敏锐嗅觉。于是基于 yaml 文件来定义统一的场景、角色、样式、色调等信息:


comic_info:
type: "四格漫画"
style: "手绘简笔画、柔软线条、轻松冷幽默、统一角色"
color_scheme: "暖黄主色调,红橙色点缀,柔和明暗层次"
character:
name: "小火柴"
appearance: "细长圆柱身体、红色火柴头、两根短竖眉毛、圆点眼睛、呆萌可爱"
personality: "迷糊、天真、略倒霉"
background_style: "白色简约背景,搭配少量手绘街景或物件增强生活感"

面板列表放在 panels 节点下,每个宫格由 panel[x] 固定场景内容。包括描述、场景、动作、表情、细节、文本等:


panels:
panel1:
description: "第一格:日常铺垫"
scene: "温暖的手绘街道:地面为淡黄色纹理,简单的路灯、几株小草、远处一座小房子,空气里飘着幾颗小亮点"
action: "小火柴双手背在身后,踩着轻快的小步子前进"
expression: "轻松微笑,眼睛微弯"
details: "路灯用细线勾勒,小草三两稀疏点缀,天空加几朵柔软的白云"
text: "今天天气真好呀~"

定义完结构,一个 yaml 文件就对应了一个四格故事,把这个内容丢给 AI 生图的工具,就能得到对应的图片。





2. 关于 AI 生图工具与质量


我的理念是: 文本是一种序列的约定:



它可以视为一个四格漫画的 基因,而 AI 工具会将基因 实例化 为个体。



所以,生成图的好坏取决于两个因素:基因序列成长环境。也就是提示词好不好,以及 AI 工具厉不厉害。 AI 生图的工具有很多,单目前大多数,对于标准的四格漫画都无法准确输出,下面列举几个:



  • 即梦 AI




  • 豆包




  • Nano Banana



目前来看,国产的 AI 仍有很大的进步空间,Nano Banana 能符合我对图片产品的预期。但是 AI 正在蓬勃发展中, AI 生图也是最近一两年才逐渐可用的,我对他们的未来持有乐观的态度,包括我们国产的大模型。所以如果 成长环境 将会越来越好,那么 基因序列 本身将会成为非常重要的因素。

目前我只是简单设计了一下 yaml,按照版本控制,称为 v0.0.1 吧,后续随着创作需求的升级,我也会逐步迭代整体结构,设计更合理的 DNA 结构 😁




3. 选定方向? Flow Heart



有人问我,你是怎么想到这些稀奇古怪的方向的,而且你是怎么坚持下来的。



对于一个创作者来说,拓宽自己的边界是一个很必要的事。特别是对一个编程创作者,广泛涉猎是家常便饭。使用一切手段,解决自己遇到的问题;没有问题时就去发展自己,在新的领域中寻找问题。至于坚持嘛,遵循内心的指引,做自己喜欢的事,是不需要坚持的,就像你每天都要喝水一样自然。


可能有人会问,如果 AI 的笑话漫画没有火,你还会坚持下去吗?刚做前两个漫画文章时,还没有火,一天收入 1 块钱,我已经觉得很美滋滋了。投入的产出符合我的预期,毕竟只需要准备个笑话雏形,其他都交给 AI 写就行了。我还和女朋友炫耀:


--

最后还是想强调一点:如果一件事,对社会、对他人没有危害,自己做着觉得开心,起来没有负担和压力,就会大胆去做。反之,可以在其他方面继续延伸,找到自己喜欢的那个领域。AI 工具的加持,让个体拥有了前所未有的能力,个人的边界可以极度拓宽。




4. 为什么会火?


第一次感觉会火,是因为擎天柱 这篇,浏览量异常上升:



从数据统计来看,发布第一天只有 102 个浏览量,和往常没什么区别。持续一周,没有任何波澜,突然在 12-20 号,增加了近 5000 的浏览量,第二天持续上涨过万,然后逐渐平息:





在第一篇爆火的后一天,慧心一笑#03 | 爸爸去钓鱼~ 数据开始上升,感觉像是连带效应:






为了验证一下是不是偶然火爆,我在 20号和 21 号又发表了两篇小笑话。结果不温不火,似乎感觉也不是必然的。
在 23 号,我发布了 慧心一笑#06 | 被美杜莎石化...,这篇在当晚直接火爆,



从数据来看,第二天浏览量直接过 2.6W,后面还有持续几天的流量:



至于为什么火爆,从阅读渠道构成来看 98.7% 的阅读量来自于公众号推荐。只能说是老天喂饭吃 ~





5. 小结一下


接下来几天的 慧心一笑#07 | 爸爸回来了...慧心一笑#09 | 农夫与蛇 也阅读过 3万。目前慧心一笑系列发布了 9 篇,阅读量超过 2.5W 的爆款有 5 篇,比例算是很高了。


感觉微信公众号的推荐阅读机制应该有所变化。另外也不是每篇都会火爆,应该和作品本身质量、流传度也有关系。这个有趣的现象让我非常欣喜,后续我还会继续创作更有意思的四格漫画,来继续验证数据。大家也可以关注 《编程之王》 公众号和我一起见证。等到第 30 篇后,我会再写一个复盘报告,和大家分享。


另外可能会有人问,你发这个就不怕别人也抄你的模式,跟你竞争吗。我只想说:





更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。让我们一起成长,变得更强。我们下次再见~



作者:张风捷特烈
来源:juejin.cn/post/7588004601392529442
收起阅读 »

国企三年我现在怎么样了

前言 我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题! 23年年初来到现在公司,已经三年了。没错公司的薪资稳定得离谱。从来没涨过,你说我能力不行表现不好嘛,我也拿过优秀员工。现在明显感觉到35岁危机的压力了,买房、结...
继续阅读 »

前言



我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!



23年年初来到现在公司,已经三年了。没错公司的薪资稳定得离谱。从来没涨过,你说我能力不行表现不好嘛,我也拿过优秀员工。现在明显感觉到35岁危机的压力了,买房、结婚、生子现在也是我最近要考虑的问题了。要想收入提高一个等级对我来说现在很难了.........


因为目前对我来说想要收入提高一个小等级比如收入提高(50%),要么996,要么拥抱AI冲一波然后跳槽。这两条路都不在我的规划中,放弃技术 实现睡后收入😂才是我的目标(不撞南墙不回头)。


三年我都在干嘛


这三年,每一年我的状态还是有很大区别的。第一年努力工作站稳脚跟,第二年闲暇时间写文章开始接触短视频,第三年(25年)年初公司裁员庆幸自己没有被优化。第三年裁员之后,也是过得最舒服的一年。


今年(25年)开始把更多的精力花在了个人探索上,做自媒体(粉丝3600😭)。因为长期的写文章做视频,也算是接了两个广子。不怕大家笑话一共赚了不到2000块。


在工作期间我也花了很多的时间去思考以后的路怎么走,既然来了这养老公司,就得利用闲暇时间探索出一条路来!然而三年过去了,没有什么起色!




最大的收获: 就是行动起来了,通过坚持写文章,做视频让自己有了独立思考,有了独立做事儿的能力,有了独立做事儿的信心。




我的牺牲:
对我的工作影响其实还是有的。因为在上班期间我的思路的是并行的,没法专注的干一件事儿,好在工作上的事儿,借助现在的AI 能够轻松高效的完成。我现在写接口基本上都不会自测了😂。最大的影响就是自己干的活儿、做的业务记不住,因为思维没有深度参与其中,有时候看着代码自己都没有印象,因为代码基本也都是AI写的加上自己注意力不够专注。


无所谓了既然做了决定来了这养老公司,并且没有涨薪升职的机会,工作上的事儿能完成就行了。


26年怎么做


3年了,还是没有找到一个方向坚定的做下去!今年的目标抖音粉丝先破1万,然后探索独立开发,利用AI 快速形成自己的一套全栈开发流程。以后能不能独立接单或者维护一些自己的小产品。如果能找到志同道合的兄弟一起干那就是最好的呢。


总结


在职场当一个混子,向着目标出发!聚焦一个方向!



每年一篇年终总结:

在国企待了一年了,吐槽一下吧

入职国企快2年了,分享一下我最近的状态吧



作者:提前退休的java猿
来源:juejin.cn/post/7599868109086867498
收起阅读 »

2026年了,前端到底算不算“夕阳行业”?

web
你有没有在朋友圈或者知乎上看到过这样的声音:“前端这行是不是快没前途了?”、“前端是夕阳行业,学不起来就晚了”。听起来很吓人吧?今天周五公司不忙~ 所以就想就想聊聊,为什么这些说法有点夸张,而且,实际上,前端比你想的要活跃、要有意思得多。 前端行业现状与就业...
继续阅读 »

你有没有在朋友圈或者知乎上看到过这样的声音:“前端这行是不是快没前途了?”、“前端是夕阳行业,学不起来就晚了”。听起来很吓人吧?今天周五公司不忙~ 所以就想就想聊聊,为什么这些说法有点夸张,而且,实际上,前端比你想的要活跃、要有意思得多。



前端行业现状与就业趋势深入分析


其他废话少说,我先列出一组数据。


市场数据说明:招聘活跃度与求职热度


在判定某个岗位是否是“夕阳行业”前,我们得看看实实在在的数据,而不是空谈。虽然我们没有官方完整的每月统计数据,但从招聘平台侧面指标可以窥见市场动态:


BOSS直聘平台整体使用频次趋势(2024 年)

数据来自行业研究监测,反映招聘平台月度活跃度(平台月访问次数,单位为万次)。它可以折射出用户在找工作和发布岗位的活跃程度:


月份Boss直聘(万次)前程无忧(万次)智联招聘(万次)
2024‑011212.8503.3381.6
2024‑032271.8958.5660.3
2024‑051892.9730.1496.5
2024‑091861.9695.1465.5
2024‑121492.8665.7432.8

从这张表可以看到几个趋势:



  • 春节前后及 3 月、4 月经常会有求职与招聘高峰,这与校园招聘和年终奖金兑现周期有关。

  • Boss直聘的整体使用频次明显高于其他招聘平台,表明它在人才市场中具有更高的活跃度。


这说明整体就业市场并没有冷却到技术岗位“没市场”的程度,但伴随着整体求职竞争压力也在增加(尤其毕业季之后)。


2024–2025 前端岗位薪资与供需情况(综合公开数据)


下面给出一个简要的薪资与供需趋势对比,是基于公开行业报告和招聘平台上职位薪资调研整理的(单位:人民币):


前端薪资水平(2024–2025)


类型数据来源平均薪资(月)说明
全国前端平均薪资招聘求职网站综合数据~20,877 元/月2024 年全国平均数据,样本规模较大
数字前端工程师高薪技术岗位脉脉高聘年度报告~67,728 元/月仅针对极高端职位薪资榜首人才
BOSS直聘高级前端岗位示例招聘岗位样例20K–50K /月典型一线城市高级薪资范围
企业大厂前端薪资公司薪资水平数据~57–65 万/年P6(技术中高级)年薪典型值


小结:大厂或高级岗位薪资明显高于平均,而整体前端岗薪资按城市和经验差异明显(北上深等一线城市更高)。中高级工程师薪资已进入较高收入层。





前端岗位供需趋势(24 年–25 年)


真实可公开的按月份招聘/求职人数统计不容易直接获得(需付费或数据授权),但我们可以根据人才供需比报告和其他间接指标构建趋势理解:


人才供需比(供给 vs 需求)变化


数据年份/区间人才供需比(整体技术类)解读
2022 全年1.29约 1.3 求职者争一岗
2023 全年2.00竞争更激烈
2024 1‑10 月2.06职位竞争仍然紧张

供需比上升意味着“求职者数量增速快于岗位数量”,这反映就业市场总体竞争压力上升,但这主要是整体技术类岗位,不仅限前端。技术类岗位中核心和稀缺型(例如 AI、架构方向)仍然紧缺。 开源中国


招聘/求职活跃度趋势示意


timeline
title
2024 : 招聘需求 ↑, 求职人数 ↑
2025 : 招聘需求 ↓, 求职人数 ↑↑



  • 招聘需求在 2024/2025 年虽整体活跃,但增长略收敛。

  • 求职人数增速仍然高(尤其高校毕业生和转行人才增多)。 PDF 文档助手+1


“前端到底是做什么的”


以前的前端,其实很简单——写页面。你写几个 HTML、CSS,再加上点 JS,页面能跑就算完成任务。大部分人只要会写代码,基本就能找到工作。那时候,技术门槛不高,但随之而来的问题是:大家都能做,稀缺性不强。


到了现在,前端已经不是单纯写页面那么简单了。现在你需要考虑性能优化、工程化、架构设计,甚至还得会和 AI 工具配合来提高效率。也就是说,前端的工作量和复杂度已经大幅升级了,光会写代码,已经不再稀缺。


普通前端 / 工程型前端 / 架构型前端


我一般把前端分成三类:



  1. 普通前端

    就是那种把设计稿转成页面的人,写页面、调样式、搞交互。以前,这类岗位很吃香,因为企业只要有人能把界面做出来就行。现在,普通前端的门槛低,但成长空间有限。

  2. 工程型前端

    这类前端不仅会写页面,还懂打包工具、模块化、性能优化、测试、CI/CD,甚至前端安全。他们能把一个项目从零到一搞成可以高效运转的系统。你可以把他们想象成“能写代码,也懂流程的人”,在团队里很吃香。

  3. 架构型前端

    架构型前端更厉害,他们关注的是整个平台的稳定性、可维护性和扩展性。他们设计组件库、微前端架构、前端性能监控体系,甚至参与后端接口设计。换句话说,他们更像“产品工程师”,不仅懂技术,还懂业务。


会写代码不再稀缺,会“用 AI 写代码”才是门槛


你可能注意到了,现在很多人说“前端会写代码不稀缺了”。这是真的。基础的 JS、CSS、HTML 很多人都会,但如果你能用 AI 辅助写代码、自动生成模板、快速优化性能,那才是真正的核心竞争力。就像以前会打字的人很多,但会用 Excel 做财务建模的人少,差距就出来了。


举个例子,现在有些大型项目,我们用 AI 帮忙生成表单验证逻辑,或者做自动化测试脚本,效率能提高好几倍。这种能力,不是简单敲几行代码能替代的。


前端未来,更像产品工程师


所以,到底前端是不是夕阳行业?我觉得恰恰相反。未来的前端,更像产品工程师——你不仅要写代码,还要思考性能、用户体验、架构设计、工程化流程,甚至要和 AI、云端、数据打交道。前端的职业宽度比以前更大,技能组合也更加稀缺。


换句话说,前端不再只是写界面的小伙伴,而是能把技术和产品结合起来,创造可落地系统的人。


总结


不是前端“夕阳”,只是门槛提高了


从薪资和招聘活跃度看:



  • 前端岗位依旧铺开在招聘平台上,高薪职位数量没有消失,只是分布更广、更分层。

  • 高端工程师、架构型前端、全栈/AI 前端人才仍然供不应求。

  • 竞争压力主要来自技术同质化人才与行业整体求职人数增长的趋势(特别是毕业季)。 开源中国


真实情形是:前端并非夕阳,而是在职业形态和薪资结构上出现了更明显的分层


你看到普通前端岗位薪资增长缓慢,是因为市场供给大,但 高技术、高工程化能力者反而更加吃香,门槛变了,而不是需求消失




总结:结合数据再看“前端是否夕阳”


既然有数据支撑,我们再回到那个问题:


前端是否是夕阳行业?结论是:



  1. 前端需求仍在增长 ——招聘平台活跃度高,技术转型需求仍旧带来岗位。

  2. 薪资仍然维持在行业中上水平 ——尤其中高级、工程化岗位。

  3. 市场竞争更激烈 ——求职人数持续增长使得低门槛岗位更难突围。

  4. 分层明显 ——普通前端增长较缓,高技能人才仍稀缺。


所以说:前端不是夕阳行业,前端职业更像是正经历升级版的“技术工程”方向,更接近综合产品工程师,而不是单纯的页面写手。


要在这个岗位上活得更好,与 AI 协作、提升工程化能力、掌握架构与性能优化,成为未来核心竞争力。


数据来源说明


本文涉及的前端薪资、招聘人数、求职人数及市场趋势数据,主要来源公开渠道:




数据仅供行业分析参考,实际薪资及岗位信息可能随城市、公司和岗位等级变化。



作者:狗头大军之江苏分军
来源:juejin.cn/post/7587684397530595355
收起阅读 »

一个Java工程师的17个日常效率工具

作为一名Java工程师,效率就是生产力。那些能让你少写代码、少改BUG、少加班的工具,往往能为你节省大量时间,让你专注于解决真正有挑战性的问题。 下面分享的这些工具几乎覆盖了Java开发全流程,从编码、调试到构建、部署,每一个环节都能大幅提升你的工作效率。 一...
继续阅读 »

作为一名Java工程师,效率就是生产力。那些能让你少写代码、少改BUG、少加班的工具,往往能为你节省大量时间,让你专注于解决真正有挑战性的问题。


下面分享的这些工具几乎覆盖了Java开发全流程,从编码、调试到构建、部署,每一个环节都能大幅提升你的工作效率。


一、IDE增强类工具


1. IntelliJ IDEA终极版 + 精选插件


作为Java开发的首选IDE,IntelliJ IDEA本身已经非常强大,但配合以下插件,效率可以再提升一个档次:



  • Key Promoter X: 显示你手动操作的快捷键,帮助你养成使用快捷键的习惯

  • AiXcoder Code Completer: 基于AI的代码补全,比IDEA自带的更智能

  • Maven Helper: 解决Maven依赖冲突的神器

  • Lombok: 减少模板代码编写

  • Rainbow Brackets: 彩色括号,让嵌套结构一目了然


实用技巧:创建多个Live Templates(代码模板),比如定义日志、常用异常处理、单例模式等。每天能节省几十次重复输入。


2. Lombok


虽然这是一个库,但它堪称效率工具。通过注解的方式,自动生成getter/setter、构造函数、equals/hashCode等方法,大幅减少模板代码量。


@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private Long id;
private String username;
private String email;
// 无需编写getter/setter/构造函数/toString等
}

注意事项:使用@EqualsAndHashCode时,注意排除可能造成循环引用的字段;使用@Builder时,考虑添加@NoArgsConstructor满足序列化需求。


二、调试与性能分析工具


3. Arthas


阿里开源的Java诊断工具,它能在线排查问题,无需重启应用。最强大的是它能够实时观察方法的入参、返回值,统计方法执行耗时,甚至动态修改类的行为。


常用命令:



  • watch 监控方法调用

  • trace 跟踪方法调用链路

  • jad 反编译类

  • sc 查找加载的类

  • redefine 热更新类


实战示例:线上问题排查,不方便加日志时,用watch命令观察方法执行:


watch com.example.service.UserService queryUser "{params,returnObj}" -x 3

4. JProfiler


Java剖析工具的王者,能够分析CPU热点、内存泄漏、线程阻塞等问题。与其他分析工具相比,JProfiler的UI更友好,数据呈现更直观。


核心功能



  • 内存视图:找出占用内存最多的对象

  • CPU视图:定位热点方法

  • 线程视图:发现死锁和阻塞

  • 实时遥测:监控线上应用,无需重启


技巧:养成定期对自己负责的服务做性能分析的习惯,很多问题在上线前就能发现。


5. Charles/Fiddler


抓包工具是API调试的必备利器。Charles(Mac)或Fiddler(Windows)能够拦截、查看和修改HTTP/HTTPS请求和响应。


实用功能



  • 模拟网络延迟

  • 请求重写

  • 断点调试HTTP请求

  • 反向代理


在前后端分离开发和调试第三方API时,这类工具能节省大量时间。


三、代码质量工具


6. SonarQube + SonarLint


SonarQube是静态代码分析工具,可以检测代码中的漏洞、坏味道和潜在bug。而SonarLint是其IDE插件版,能在你编码时实时提供反馈。


最佳实践



  • 在CI流程中集成SonarQube

  • 为团队制定"质量门"标准

  • 使用SonarLint实时检查,避免代码审查时返工


技巧:自定义规则集,忽略对特定项目不适用的规则,避免"过度洁癖"。


7. ArchUnit


用代码的方式测试架构规则,确保项目架构不会随着时间推移而腐化。


@Test
public void servicesAndRepositoriesShouldNotDependOnControllers() {
ArchRule rule = noClasses()
.that().resideInAPackage("..service..")
.or().resideInAPackage("..repository..")
.should().dependOnClassesThat().resideInAPackage("..controller..");

rule.check(importedClasses);
}

将架构约束加入单元测试,比写文档更有效,因为违反规则会导致测试失败。


8. JaCoCo


代码覆盖率工具,与Maven/Gradle集成,生成直观的HTML报告。它不仅统计单元测试覆盖了哪些代码,还能显示哪些分支没有测试到。


实用配置:在Maven中设置覆盖率阈值,低于阈值则构建失败:


<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>

四、API开发与测试工具


9. Postman + Newman


Postman是API开发和测试的标准工具,而Newman是其命令行版本,适合集成到CI/CD流程中。


高级用法



  • 环境变量管理不同测试环境

  • 请求前/后脚本自动化测试

  • 导出集合到Newman在CI中执行

  • 团队共享API集合


技巧:为每个项目创建环境变量集合,包含测试环境、开发环境、生产环境配置,一键切换。


10. OpenAPI Generator


从OpenAPI(Swagger)规范自动生成API客户端和服务器端代码。


openapi-generator generate -i swagger.json -g spring -o my-spring-server

前后端并行开发时,通过API优先设计,让前端可以基于Swagger UI与Mock服务器工作,而后端则基于生成的接口实现业务逻辑。


五、数据库工具


11. DBeaver


全能型数据库客户端,支持几乎所有主流数据库,功能强大且开源免费。


必备功能



  • ER图可视化

  • 数据导出/导入

  • SQL格式化

  • 数据库比较

  • 执行计划分析


技巧:使用其"SQL模板"功能,保存常用查询模板,提高重复查询效率。


12. Flyway/Liquibase


数据库版本控制工具,将数据库结构变更纳入版本管理,确保开发、测试和生产环境的数据库结构一致性。


以Flyway为例:


@Bean
public Flyway flyway() {
return Flyway.configure()
.dataSource(dataSource)
.locations("classpath:db/migration")
.load();
}

最佳实践



  • 每个变更一个脚本文件

  • 脚本文件命名规范化

  • 脚本必须是幂等的

  • 将验证步骤集成到CI流程


六、构建与部署工具


13. Gradle + Kotlin DSL


虽然Maven仍是Java构建工具的主流,但Gradle的灵活性和性能优势明显。使用Kotlin DSL而非Groovy可以获得更好的IDE支持和类型安全。


plugins {
id("org.springframework.boot") version "2.7.0"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.21"
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

优势



  • 增量构建更快

  • 依赖缓存更智能

  • 自定义任务更灵活

  • 多项目构建更高效


14. Docker + Docker Compose


容器化是现代Java开发的标配,Docker让环境一致性问题成为历史。


实用命令


# 启动开发环境所需的所有服务
docker-compose up -d
# 查看容器日志
docker logs -f container_name
# 进入容器内部
docker exec -it container_name bash

技巧:创建一个包含常用中间件(MySQL、Redis、RabbitMQ等)的docker-compose.yml,一键启动开发环境。


15. GitHub Actions/Jenkins


CI/CD是提高团队效率的关键环节。GitHub Actions适合开源项目,Jenkins则更适合企业内部构建流程。


GitHub Actions示例:


name: Java CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
- name: Build with Gradle
run: ./gradlew build

最佳实践:将代码风格检查、单元测试、集成测试、安全扫描全部纳入CI流程,确保代码质量。


七、辅助工具


16. PlantUML


用代码生成UML图,比拖拽式画图工具更高效,特别是需要频繁修改图表时。可以和版本控制系统无缝集成。


@startuml
package "Customer Domain" {
class Customer
class Address
Customer "1" *-- "n" Address
}
package "Order Domain" {
class Order
class LineItem
Order "1" *-- "n" LineItem
Order "*" -- "1" Customer
}
@enduml

IDEA集成:安装PlantUML插件,编写代码时实时预览图表。


17. Obsidian/Logseq


知识管理工具,基于Markdown文件的本地知识库。对于需要持续学习的Java工程师来说,构建个人知识体系至关重要。


推荐用法



  • 每学习一个新技术,创建一个页面

  • 记录常见错误和解决方案

  • 构建项目文档和架构决策记录

  • 使用日常笔记捕捉想法和灵感


技巧:利用双向链接功能,将知识点相互关联,构建知识网络,而非简单的知识树。


总结


最后,工具再好,也需要时间精力去掌握。建议每次只引入1-2个新工具,熟练后再考虑扩展。


毕竟,真正的效率来源于熟练度,而非工具数量。


作者:风象南
来源:juejin.cn/post/7506414257399939111
收起阅读 »

同志们,我去外包了

同志们,我去外包了 同志们,经历了漫长的思想斗争,我决定回老家发展,然后就是简历石沉大海,还好外包拯救了我,我去外包了! 都是自己人,说这些伤心话干嘛;下面说下最近面试的总结地方,小小弱鸡,图一乐吧。 首先随着工作年限的增加,越来越多公司并不会去和你抠八股文...
继续阅读 »

同志们,我去外包了


同志们,经历了漫长的思想斗争,我决定回老家发展,然后就是简历石沉大海,还好外包拯救了我,我去外包了!


Xbw8OtYtcYAVZ0dCwFJzXwc8bad653b209f07472ec09fd8e712492.jpg


都是自己人,说这些伤心话干嘛;下面说下最近面试的总结地方,小小弱鸡,图一乐吧。

首先随着工作年限的增加,越来越多公司并不会去和你抠八股文了(那阵八股风好像停了),只是象征性的问几个问题,然后会对照着项目去问些实际的问题以及你的处理办法。
(ps:(坐标合肥)突然想到某鑫面试官问我你知道亿级流量吗?你怎么处理的,听到这个问题我就想呼过去,也许读书读傻了,他根本不知道亿级流量是个什么概念,最主要的是它是个制造业公司啊,你哪来的亿级流量啊,也不知道问这个问题时他在想啥,还有某德(不是高德),一场能面一个小时,人裂开)


好了,言归正传,咱说点入职这家公司我了解到的一点东西,我分为两部分:代码和sql;


代码上


首先传统的web项目也会分前端后端,这点不错;


1.获取昨天日期


可以使用jdk自带的LocalDate.now().minusDays(-1)
这个其实内部调用的是plusDays(1)方法,所以不如直接就用plusDays方法,这样少一层判断;



PS:有多少人和我之前一样直接new Date()的。



2.字符填充


apache.common下的StringUtils的rightPad方法用于字符串填充使用方法是StringUtils.rightPad(str,len,fillStr)
大概意思就是str长度如果小于len,就用fillStr填充;



PS:有多少人之前是String.format或者StringBuilder用循环实现的。



3.获取指定年指定月的某天


获取指定年指定月的某天可以用localDate.of(year,month,day),如果我们想取2025年的五月一号,可以写成LocalDate.of(2025, 5, 1),那有人可能就想到了如果月尾呢,LocalDate.of(2025, 5, 31)也是可以的,但是我们需要清楚知道这个月有多少天,比如说你2月给个30天,那就会抛异常;
麻烦;


12.jpg
更好的办法就是先获取第一天,然后调用localDate.with(TemporalAdjusters.lastDayOfMonth());方法获取最后一天,TemporalAdjusters.lastDayOfMonth()会自动处理不同月份和闰年的情况;


sql层面的


有言在先,说实话我不建议在sql层面写这种复杂的东西,毕竟我们这么弱的人看到那么长的且复杂的sql会很无力,那种无力感你懂吗?打工人不为难打工人;不过既然别人写了,咱们就学习一下嘛;


1.获取系统日期


首先获取系统日期可以试用TRUNC(SYSDATE)进行截取,这样返回的时分秒是00:00:00,比如2025-05-29 00:00:00,它也可以截取数字,想知道就去自行科普下,不建议掌握,学习了下,有点搞;


2.返回date当前月份的最后一天


LAST_DAY(date)这个返回的是date当前月份的最后一天,比如今天是2025-05-29,那么返回的是2025-05-31
ADD_MONTH(date,11)表示当前日期加上11个月,比如2025-01-02,最终返回的是2025-12-02;


3.左连接的知识点


最后再提个左连接的知识点,最近看懵了,图一乐哈,A left join B,就是on的条件是在join生成临时表时起作用的,而where是对生成的临时表进行过滤;
两者过滤的时机不一样。我想了很久我觉得可以这么理解,on它虽然可以添加条件,但他的条件只是一个匹配条件比如B.age>10;它是不会对A表查询出来的数据量产生一个过滤效果;
而where是一个实打实的过滤条件,不管怎么说都会影响最终结果,对于inner join这个特例,on和where的最终效果一样,因为B.age>10会导致B的匹配数据减少,由于是交集,故会对整体数据产生影响。


好了,晚安,外包打工仔。。。


作者:小红帽的大灰狼
来源:juejin.cn/post/7510055871465308212
收起阅读 »

给大龄(35岁+)程序员的绝地求生计划书

不要侥幸,35 岁以上的程序员不好找工作, 这是一个既定事实 首先无论是什么渠道, 对于普通人来说 35+ 的程序员, 不好就业, 就是一个既定事实。 甚至都不一定与自己的工作经历、学历 有多大的关系。 甚至我知道很多 35+ 的老哥们, 经验丰富, 985 ...
继续阅读 »

image.png


不要侥幸,35 岁以上的程序员不好找工作, 这是一个既定事实


首先无论是什么渠道, 对于普通人来说 35+ 的程序员, 不好就业, 就是一个既定事实。 甚至都不一定与自己的工作经历、学历 有多大的关系。


甚至我知道很多 35+ 的老哥们, 经验丰富, 985 大学毕业, 依然不好找工作, 这个不是个例。


我们不过多探究为何 35+ 的程序员不好就业, 我们可能需要更多关注, 怎么在这种大背景下「绝地求生」


这些方向可以让 35+ 程序员依然抢手


“35 岁危机”并非绝对,大量 35 岁以上的程序员仍能保持职业竞争力,甚至更受青睐,核心在于是否具备“不可替代性”:



  • 技术深度型:在某一细分领域(如底层架构、算法优化、安全攻防)有深耕,成为行业公认的技术专家。例如,专注于分布式系统设计、AI 大模型工程化的资深工程师,35 岁后反而因经验稀缺而抢手。

  • 业务融合型:熟悉特定行业(如金融、医疗、制造业)的业务逻辑,能将技术与行业需求深度结合。例如,懂银行业务的支付系统架构师、懂医疗流程的医疗信息化专家,年龄增长带来的业务经验反而成为优势。

  • 管理转型型:从技术岗转型为技术管理(如 CTO、技术总监、团队负责人),具备带团队、做决策、对接业务的能力。这类岗位更看重“经验沉淀”和“资源整合能力”,35-45 岁往往是黄金期。


技术管理型 - 有坑


首先看看「管理型」, 我感觉上面三个「绝地求生」方向, 管理方向, 反而是最不考虑的, 其实很简单, 现在大社会都是紧缩模式,只有出局的业务,没有新业务开展了。 那么这个时候, 就出现一个更加严重的问题, 「技术管理系」岗位, 一个萝卜一个坑, 甚至可以说, 你无论技术有多牛逼, 但是没有那个坑位, 可能永远都上不去。


甚至还有一个比较搞笑的现象,都是很多中小公司离开一线很久的技术 leader , 找不到坑位了, 再想着来投递技术岗, 技术上基本上生疏很久了, 基本上很难再就业。 这种人真不在少数。


深耕技术性 - 有利有弊


这个其实是一个非常好的方向, 但是这种人往往都是大头兵, 或者叫做高级工具人。 首先需要花非常多的时间和精力去做深耕技术, 要时刻保持最前沿的技术储备, 最充沛的精力, 最丰富的热情。然后要去干最累的活儿, 干最难的事儿, 但是不一定有好结果。 很简单, 这个业务线没了, 那也只能去找下一份工作。 而且大头兵, 很容易为业务背锅。


都是高级打工仔了, 做的好, 是应该的, 做的不好就得背锅。


而且还要想办法跟 AI 做差异性竞争。 很简单, 做了一个非常好的工作架构, 然后 AI 可以用非常低的成本做替代, 那就白干了。


上面说了那么多缺点, 这个方向就真的那么不堪吗?其实也不是, 只要努力, 肯吃苦, 至少下限还是很高的。 因为这个路子, 就跟上大学一样,你只要一直读书, 肯吃苦, 就能上到 博士 。 做深耕技术也是一样的, 只要肯努力, 耐得住寂寞, 一直死磕下去, 基本上在一个方向都能有几刷子的。 对于迷茫型和努力型同学,这个也是最佳直选。


所以有利有弊, 各位同学可自行斟酌。


业务融合型 - 性价比之王


技术的价值最终要落地到业务中,30 + 程序员若能将技术能力与具体行业的业务逻辑深度绑定,会比 “纯技术专家” 更难被替代 —— 因为年轻人可以快速学会技术,但吃透一个行业的业务规则(如金融风控逻辑、医疗流程规范、制造业供应链协同)往往需要 5 年以上的沉淀。


这个才是我真正想跟大家聊一聊的方向。


精通技术的业务专家成长之路


“技术 + 业务” 复合岗,核心是 让技术能力成为 “解读业务、解决业务痛点” 的工具,而非终点


这种转型的价值在于:业务逻辑的沉淀周期长(5-10 年),年轻人可快速学会技术,但难以短期吃透行业规则,这正是 30 + 程序员的经验红利。以下从 “有价值的业务方向”“业务理解训练方法”“避坑要点” 三个维度展开,附具体实操步骤:


一、值得深耕的“技术+业务”方向(附核心业务逻辑与技术结合点)


选择业务方向的关键标准:业务逻辑复杂(有门槛)、监管严格(需经验规避风险)、技术与业务深度绑定(技术优化能直接带来业务收益)。以下是几个高价值领域:


1. 金融科技(银行/保险/证券)


核心业务逻辑:金融行业的本质是“风险定价+资金流转”,涉及复杂的监管规则(如央行反洗钱、银保监会合规要求)、用户分层(高净值客户vs大众客户)、业务流程(信贷审批、理赔核保、交易清算)。

技术结合点



  • 信贷领域:用AI模型优化风控(需理解“逾期率”“不良率”等业务指标,以及征信数据、行为数据如何影响授信);

  • 交易领域:低延迟交易系统(需理解股票/期货的“撮合规则”“涨跌停限制”,技术优化直接影响交易成功率);

  • 保险领域:智能核保系统(需理解“健康告知”“免责条款”等业务规则,技术需实现“用户输入→规则匹配→核保结论”的自动化)。


    为什么值得做:金融监管政策每年更新(如2025年央行新规对“消费贷资金用途监控”的要求),技术方案必须跟着业务规则调整,经验越丰富越能快速响应,年轻人易因不懂合规踩坑。



2. 医疗健康(医院信息化/互联网医疗)


核心业务逻辑:医疗行业的核心是“患者诊疗全流程”,涉及医院内部流程(挂号、分诊、问诊、检查、缴费、取药)、医保政策(医保目录、报销比例、异地结算规则)、医疗安全(病历隐私、药品溯源)。

技术结合点



  • 医院信息系统(HIS):需理解“门诊/住院流程”(如门诊的“医生开单→药房发药”环节,技术需对接收费系统、药品库存系统);

  • 互联网医疗:在线问诊平台需符合《互联网诊疗管理办法》(如“首诊不能线上”“电子处方流转规则”),技术架构要支持“医患身份核验→问诊记录留存→处方合规性校验”;

  • 医疗大数据:医疗影像AI辅助诊断(需理解“CT/MRI影像的临床意义”,技术模型训练需结合医生诊断逻辑,而非纯数据拟合)。


    为什么值得做:医疗流程标准化程度低(不同医院流程差异大),且涉及生命安全,技术方案容错率极低,需要“技术+临床经验”双重积累,30+的耐心和细致更具优势。



3. 智能制造(工业互联网/工厂数字化)


核心业务逻辑:制造业的核心是“生产效率提升+成本控制”,涉及生产流程(订单排产、物料采购、车间加工、质量检测、物流配送)、设备管理(设备故障率、OEE设备综合效率)、供应链协同(供应商交付周期、库存周转率)。


技术结合点



  • 工业物联网(IIoT):设备数据采集与分析(需理解“数控机床的主轴温度、转速与产品精度的关系”,技术需将数据转化为“设备维护预警”等业务动作);

  • MES系统(制造执行系统):生产排产优化(需理解“订单优先级、物料齐套率、设备产能”的制约关系,技术算法要平衡“交付时效”与“生产成本”);

  • 质量追溯系统:需理解“产品不良品的产生环节”(如焊接工艺参数异常导致的缺陷),技术需实现“生产数据→不良原因”的反向追溯。


    为什么值得做:制造业数字化转型依赖“懂生产的技术人”,纯技术人员易陷入“为数字化而数字化”(比如盲目上物联网设备却不会分析数据),而有车间经验的技术人员能精准定位痛点(如某环节停机1小时损失5万元,技术优化需优先解决)。



4. 跨境电商(平台型/品牌型)


核心业务逻辑:跨境电商的核心是“跨区域供需匹配”,涉及海外市场规则(如亚马逊的A+页面规则、TikTok Shop的物流时效要求)、跨境链路(报关、清关、海外仓配送)、本地化运营(语言、支付习惯、合规要求,如欧盟增值税VAT)。


技术结合点



  • 选品系统:需理解“海外市场需求”(如东南亚雨季对雨具的需求波动),技术通过爬虫+数据分析预测“潜力商品”;

  • 跨境ERP:需对接“多国物流商API”“海关报关系统”,技术需处理“汇率换算”“多语言订单”“合规申报”等业务细节;

  • 本地化营销工具:如TikTok直播带货的“实时翻译+弹幕互动”功能,技术需结合“海外用户互动习惯”(如欧美用户更关注产品参数,东南亚用户更关注价格)。


为什么值得做:跨境业务涉及“多国家、多规则、多链路”,技术方案需灵活适配(比如某国突然调整进口关税,系统需快速支持税率更新),经验能减少试错成本,年轻人易因不了解海外规则导致系统“水土不服”。


二、训练“业务理解能力”的5个实操步骤(从0到1建立业务思维)


技术人员常陷入“只懂代码不懂业务”的误区,核心问题是:习惯用“技术实现”倒推“业务需求”,而非从“业务目标”推导“技术价值”。以下步骤帮你系统性建立业务思维:


步骤1:从“被动接需求”到“主动问目标”——搞懂“业务为什么需要这个功能”



  • 具体做法:每次接需求时,多问3个问题:

    1. “这个功能要解决用户的什么痛点?”(如“用户反馈支付失败率高”,而非只接“开发新支付渠道”);

    2. “这个功能的业务指标是什么?”(如“支付成功率从90%提升到99%”,而非“完成开发即可”);

    3. “如果这个功能上线后不达预期,备选方案是什么?”(理解业务的优先级和容错空间)。



  • 案例:若业务方提“开发一个优惠券系统”,技术人员不应直接设计表结构,而是先问:“发优惠券是为了拉新还是促活?目标是提升客单价10%还是复购率20%?预算多少?”——这些决定了系统是否需要支持“新用户专属券”“满减叠加规则”等细节。


步骤2:画“业务流程图”——用可视化方式梳理业务环节(比写代码更重要)



  • 工具:Figma(画流程图)、Visio(复杂流程)、甚至手绘;

  • 核心要素:每个流程节点包含“谁(角色)→做什么(动作)→输入/输出什么(信息)→遇到异常怎么办(分支)”;

  • 案例:画“电商退款流程”时,需明确:

    • 角色:用户、客服、财务、仓库;

    • 动作:用户发起退款→客服审核(是否符合7天无理由)→财务确认退款金额→仓库确认是否收到退货→系统打款;

    • 异常分支:“用户已拆封商品”是否支持退款?“仓库未收到货但用户说已寄出”如何处理?



  • 价值:流程图能帮你发现“技术设计的盲区”(如漏考虑“退款失败后重试机制”),也能让你在和业务方沟通时“用他们的语言对话”(而非只说“接口、数据库”)。


步骤3:“泡在业务场景里”——亲身体验业务,而非只听业务方描述



  • 具体做法

    • 若做电商:自己下单、退货、咨询客服,记录每个环节的体验(如“退款到账时间长”可能是技术链路太长);

    • 若做医疗系统:去医院门诊“蹲点”,看医生如何开单、护士如何分诊、患者如何缴费(你会发现“医生开单时频繁切换系统”是真实痛点,技术可做集成优化);

    • 若做金融:假扮客户打电话给银行客服,咨询“信用卡逾期如何处理”(理解业务方常说的“催收流程”实际是怎样的)。



  • 关键:技术人员容易“坐在办公室想当然”,而业务的真相往往藏在一线操作中。比如某团队开发“外卖骑手App”时,程序员亲自骑了3天车,才发现“高峰期导航频繁卡顿”是比“界面美观”更重要的问题。


步骤4:建立“业务知识体系”——像学技术一样系统化学习业务



  • 方法

    1. 行业基础术语库:整理业务常用词(如金融的“拨备率”“LPR”,医疗的“DRG/DIP”“电子病历互联互通”),每个词注明“定义+业务意义”(如“DRG”是“按疾病诊断分组付费”,影响医院的收费和成本控制);

    2. 监管规则清单:收集行业相关政策(如跨境电商的《跨境电子商务零售进口商品清单》,金融的《个人信息保护法》对数据采集的要求),标注“哪些规则会影响技术方案”(如数据本地化存储要求决定服务器部署位置);

    3. 业务指标公式:搞懂核心KPI的计算逻辑(如“电商GMV=流量×转化率×客单价”,“银行不良率=不良带款余额/总带款余额”),理解技术优化如何影响这些指标(如“页面加载速度提升1秒→转化率提升2%→GMV增加X万元”)。



  • 工具:用Notion或Excel整理,定期更新(如政策变动时),避免“业务术语听不懂”的尴尬。


步骤5:输出“业务-技术关联报告”——证明你能“用技术解决业务问题”



  • 核心动作:每完成一个项目,写一份“技术方案如何支撑业务目标”的报告,包含:

    • 业务背景:项目要解决什么业务痛点(如“工厂因排产不合理,订单交付延迟率达15%”);

    • 技术方案:用了什么技术(如APS高级排产算法),为什么选这个技术(对比其他方案,该算法在“多品种小批量”场景下更优);

    • 业务效果:技术上线后,业务指标有何变化(如“交付延迟率从15%降至5%,每月减少违约金100万元”);

    • 经验沉淀:如果再遇到类似业务问题,技术方案可复用哪些部分(如“排产算法可适配其他工厂的生产模式”)。



  • 价值:这份报告不仅是你“业务+技术”能力的证明(跳槽时可作为案例),更能倒逼你在项目中主动思考“技术的业务价值”,而非只关注“代码写得漂不漂亮”。


三、转型避坑:这3个误区会让你“既不像技术,也不像业务”



  1. 误区1:放弃技术深度,单纯“转业务”

    复合岗的核心是“技术为根,业务为翼”,而非变成纯业务岗。比如做金融科技,若不懂分布式系统,就无法设计高并发的交易系统;若不懂AI,就无法优化风控模型。保留技术深度,同时叠加业务理解,才是不可替代的关键

  2. 误区2:只学“表面业务”,不懂“业务本质”

    比如做电商,知道“优惠券能促单”是表面,理解“不同面额的优惠券对不同客群(新用户vs老用户)的转化差异”才是本质;做医疗,知道“电子病历要存数据”是表面,理解“病历数据如何支持医生诊断决策”才是本质。多问“为什么”,穿透业务动作看目标

  3. 误区3:等待“别人教业务”,而非主动获取

    业务方通常很忙,不会系统性教你业务知识。要主动“找信息”:看行业报告(艾瑞、易观)、读专业书籍(如《支付战争》懂支付业务,《精益生产》懂制造流程)、加行业社群(如医疗信息化的“HIT专家网”)、甚至考行业证书(如PMP学项目管理,CFA基础懂金融)。


作者:晴小篆
来源:juejin.cn/post/7543976401176985643
收起阅读 »

微服务正在悄然消亡:这是一件美好的事

最近在做的事情正好需要系统地研究微服务与单体架构的取舍与演进。读到这篇文章《Microservices Are Quietly Dying — And It’s Beautiful》,许多观点直击痛点、非常启发,于是我顺手把它翻译出来,分享给大家,也希望能给同...
继续阅读 »

最近在做的事情正好需要系统地研究微服务与单体架构的取舍与演进。读到这篇文章《Microservices Are Quietly Dying — And It’s Beautiful》,许多观点直击痛点、非常启发,于是我顺手把它翻译出来,分享给大家,也希望能给同样在复杂性与效率之间权衡的团队一些参考。


微服务正在悄然消亡:这是一件美好的事


为了把我们的创业产品扩展到数百万用户,我们搭建了 47 个微服务。


用户从未达到一百万,但我们达到了每月 23,000 美元的 AWS 账单、长达 14 小时的故障,以及一个再也无法高效交付新功能的团队。


那一刻我才意识到:我们并没有在构建产品,而是在搭建一座分布式的自恋纪念碑。


image.png


我们都信过的谎言


五年前,微服务几乎是教条。Netflix 用它,Uber 用它。每一场技术大会、每一篇 Medium 文章、每一位资深架构师都在高喊同一句话:单体不具备可扩展性,微服务才是答案。


于是我们照做了。我们把 Rails 单体拆成一个个服务:用户服务、认证服务、支付服务、通知服务、分析服务、邮件服务;然后是子服务,再然后是调用服务的服务,层层套叠。


到第六个月,我们已经在 12 个 GitHub 仓库里维护 47 个服务。我们的部署流水线像一张地铁图,架构图需要 4K 显示器才能看清。


当“最佳实践”变成“最差实践”


我们不断告诫自己:一切都在运转。我们有 Kubernetes,有服务网格,有用 Jaeger 的分布式追踪,有 ELK 的日志——我们很“现代”。


但那些光鲜的微服务文章从不提的一点是:分布式的隐性税


每一个新功能都变成跨团队的协商。想给用户资料加一个字段?那意味着要改五个服务、提三个 PR、协调两周,并进行一次像劫案电影一样精心编排的数据库迁移。


我们的预发布环境成本甚至高于生产环境,因为想测试任何东西,都需要把一切都跑起来。47 个服务在 Docker Compose 里同时启动,内存被疯狂吞噬。


那个彻夜崩溃的夜晚


凌晨 2:47,Slack 被消息炸翻。


生产环境宕了。不是某一个服务——是所有服务。支付服务连不上用户服务,通知服务不断超时,API 网关对每个请求都返回 503。


我打开分布式追踪面板:一万五千个 span,全线飘红。瀑布图像抽象艺术。我花了 40 分钟才定位出故障起点。


结果呢?一位初级开发在认证服务上发布了一个配置变更,只是一个环境变量。它让令牌校验多了 2 秒延迟,这个延迟在 11 个下游服务间层层传递,超时叠加、断路器触发、重试逻辑制造请求风暴,整个系统在自身重量下轰然倒塌。


我们搭了一座纸牌屋,却称之为“容错架构”。


我们花了六个小时才修复。并不是因为 bug 复杂——它只是一个配置的单行改动,而是因为排查分布式系统就像破获一桩谋杀案:每个目击者说着不同的语言,而且有一半在撒谎。


那个被忽略的低语


一周后,在复盘会上,我们的 CTO 说了句让所有人不自在的话:


“要不我们……回去?”


回到单体。回到一个仓库。回到简单。


会议室一片沉默。你能感到认知失调。我们是工程师,我们很“高级”。单体是给传统公司和训练营毕业生用的,不是给一家正打造未来的 A 轮初创公司用的。


但随后有人把指标展开:平均恢复时间 4.2 小时;部署频率每周 2.3 次(从单体时代的每周 12 次一路下滑);云成本增长速度比营收快 40%。


数字不会说谎。是架构在拖垮我们。


美丽的回归


我们用了三个月做整合。47 个服务归并成一个模块划分清晰的 Rails 应用;Kubernetes 变成负载均衡后面的三台 EC2;12 个仓库的工作流收敛成一个边界明确的仓库。


结果简直让人尴尬。


部署时间从 25 分钟降到 90 秒;AWS 账单从 23,000 美元降到 3,800 美元;P95 延迟提升了 60%,因为我们消除了 80% 的网络调用。更重要的是——我们又开始按时交付功能了。


开发者不再说“我需要和三个团队协调”,而是开始说“午饭前给你”。


我们的“分布式系统”变回了结构良好的应用。边界上下文变成 Rails 引擎,服务调用变成方法调用,Kafka 变成后台任务,“编排层”……就是 Rails 控制器。


它更快,它更省,它更好。


我们真正学到的是什么


这是真相:我们为此付出两年时间和 40 万美元才领悟——


微服务不是一种纯粹的架构模式,而是一种组织模式。Netflix 需要它,因为他们有 200 个团队。你没有。Uber 需要它,因为他们一天发布 4,000 次。你没有。


复杂性之所以诱人,是因为它看起来像进步。 拥有 47 个服务、Kubernetes、服务网格和分布式追踪,看起来很“专业”;而一个单体加一套 Postgres,看起来很“业余”。


但复杂性是一种税。它以认知负担、运营开销、开发者幸福感和交付速度为代价。


而大多数初创公司根本付不起这笔税。


我们花了两年时间为并不存在的规模做优化,同时牺牲了能让我们真正达到规模的简单性。


你不需要 50 个微服务,你需要的是自律


软件架构的“肮脏秘密”是:好的设计在任何规模都奏效。


一个结构良好的单体,拥有清晰的模块、明确的边界上下文和合理的关注点分离,比一团由希望和 YAML 勉强粘合在一起的微服务乱麻走得更远。


微服务并不是因为“糟糕”而式微,而是因为我们出于错误的理由使用了它。我们选择了分布式的复杂性而不是本地的自律,选择了运营的负担而不是价值的交付。


那些悄悄回归单体的公司并非承认失败,而是在承认更难的事实:我们一直在解决错误的问题。


所以我想问一个问题:你构建微服务,是在逃避什么?


如果答案是“一个凌乱的代码库”,那我有个坏消息——分布式系统不会修好坏代码,它只会让问题更难被发现。


作者:程序猿DD
来源:juejin.cn/post/7563860666349649970
收起阅读 »

百度智能云开工采购季助力低成本解锁AI生产力

当“赛博养虾”成为一种新晋社交货币,一场关于AI落地的范式革命已然开启。近期,开源AI智能体OpenClaw因其酷似龙虾的图标和强大的自动化能力火爆全球,被开发者们亲昵地称为“小龙虾”,掀起了一场“全民养虾”的热潮 。在这场“养虾”运动的背后,是海量的算力消耗...
继续阅读 »

当“赛博养虾”成为一种新晋社交货币,一场关于AI落地的范式革命已然开启。近期,开源AI智能体OpenClaw因其酷似龙虾的图标和强大的自动化能力火爆全球,被开发者们亲昵地称为“小龙虾”,掀起了一场“全民养虾”的热潮 。

在这场“养虾”运动的背后,是海量的算力消耗与高昂的Token成本。如何让每一位“养虾人”和企业用户都能低成本、高效率地拥抱这波技术红利?据悉,2026年2月25日,百度智能云正式启动“云启惠聚·企业采购季”开工季大促活动,不仅将OpenClaw的部署门槛降至冰点,更以极致的价格和丰厚的权益,为企业和开发者们开年复工的智能升级“囤好粮”。

极简部署“养虾”,9.9元开启AI助理时代

“养虾”虽火,但环境配置、模型接入等技术门槛曾让不少爱好者望而却步,甚至催生了付费“上门安装”的生意 。为了让AI普惠至每一位用户,百度智能云在本次采购季中推出了针对性的极简部署方案与超低折扣。

针对近期火爆的OpenClaw“养虾”热潮,百度智能云推出轻量应用服务器9.9元/月起,可实现AI助手极简部署;同时面向中小网站建设、开发测试等传统场景,推出云服务器经济型E2 19.9元/年惊爆价。两款服务器分别瞄准AI极速上手与通用业务上云,以极致性价比满足复工季的多样化算力需求。

“OpenClaw的爆火,折射出市场对‘能干活’的AI的迫切期待。”百度智能云相关负责人表示,“本次采购季,我们整合了从算力、模型到部署工具的全链路资源,希望让企业和个人都能零门槛迈入‘代理型AI’应用的新阶段。”

企业级“粮仓”全面升级,万元券包助跑复工季

除了面向开发者的“养虾”盛宴,本次“开工采购季”更为广大企业客户准备了丰厚的“开工红包”,直击企业数字化转型中的成本痛点。

邀请企业认证,得1999元红利津贴:作为本次活动中力度最大的满减福利,活动期间,邀请百度云用户完成企业实名认证,双方均可获得1999元的专属红利津贴。该津贴可用于云服务器BCC、对象存储BOS、人脸识别等多种核心产品,极大降低企业上云试错成本。

  • 领万元新购/续费券包,至高立减6000元:针对不同企业需求,百度智能云推出多梯度满减券。新用户可享满200减30至满1500减525元不等的专享券;而对于有批量采购需求的企业,最高可领取满20000减6000元的超值续费券,覆盖计算、存储、网络、安全及AI全栈产品。

  • 限时秒杀与100%中奖锦鲤池:每周不同主题的秒杀日将持续点燃采购热情。通用文字识别低至1元、文档解析5000页仅199元、数字员工套餐9.9元起 。活动期间,只要完成实名认证或订单满额,即可参与抽奖,京东卡、小度智能屏等好礼100%中奖,更有高额惊喜券随机放送。

技术普惠,重构AI生产力“新成本”

在OpenClaw引发的“Token经济学”讨论中,国产模型凭借极致的性价比成为全球“养虾人”的热门选择 。百度智能云此次采购季也深度呼应了这一趋势。

以千帆大模型平台相关产品为例,大模型Tokens量包低至20元/年(产品首购) 。配合云服务器、CDN等基础资源的超低折扣,百度智能云正在构建一个从算力基座到AI应用层的“高性价比创新闭环”。

业内分析认为,随着OpenClaw等智能体框架的普及,AI的商业模式正从“让更多人对话”转向“让更多智能体持续做事” 。百度智能云此次“开工采购季”敏锐地捕捉到了这一节点,通过精准的优惠组合,不仅解决了开发者“养虾”的燃眉之急,更为广大企业在AI时代的组织变革和效率升级,提供了坚实的“粮草”后盾。

收起阅读 »

BOE(京东方)联合TÜV莱茵发布《自然光显示技术白皮书》 以“晨午暮夜”系统仿生定义健康显示新标杆

2026年3月11日,在AWE 2026(中国家电及消费电子博览会)举办前夕,BOE(京东方)“自然光”显示技术品鉴会于TÜV莱茵InnoHub隆重举行。本次活动以“朝夕自然·光合焕新”为主题,基于“晨午暮夜”四时自然光所提出的“系统仿生学”设计理念,BOE(...
继续阅读 »

2026年3月11日,在AWE 2026(中国家电及消费电子博览会)举办前夕,BOE(京东方)“自然光”显示技术品鉴会于TÜV莱茵InnoHub隆重举行。本次活动以“朝夕自然·光合焕新”为主题,基于“晨午暮夜”四时自然光所提出的“系统仿生学”设计理念,BOE(京东方)深刻解读并展示了其独创的“自然光”显示技术(BNL)的创新实践。会上,BOE(京东方)携手国际权威检测认证机构TÜV莱茵共同发布《自然光显示技术白皮书》,不仅为健康显示领域提供了系统性、可量化的指引,更将为全球显示产业树立健康护眼的可测量基准,推动显示产业从“看得清”迈向“看得舒服、看得健康”的新纪元。BOE(京东方)与TÜV莱茵大中华区相关领导,以及联想全球显示器事业部、业务&产品总监芦智勇,复旦大学附属眼耳鼻喉科医院副主任医师许烨,深圳创维显示科技有限公司产品规划部总监薛海啸等各界嘉宾出席了本次活动。

京东方科技集团高级副总裁、联席首席技术官邵喜斌在致辞中深度阐释了“自然光”显示技术(BNL)的核心设计逻辑与创新价值。“在行业还聚焦于分辨率、刷新率等硬性参数竞逐时,BOE(京东方)就已前瞻布局视觉健康研究,成为最早进入该领域的显示企业。十余年间,我们推出了低蓝光、PWM低频闪、类纸护眼等多项行业领先的护眼技术,率先实现量产并广泛应用于手机、平板、笔记本、电视等全品类终端。BOE(京东方)发布的‘自然光’显示技术是健康护眼领域持续深耕的集大成之作。”他在致辞中还强调,BOE(京东方)“自然光”显示技术(BNL)的核心差异在于其“系统仿生学”设计理念:“它不是单一功能的修补,而是从自然光中提取‘有益’精华——即BNL中的Beneficial——通过光谱优化、偏振调节、光形优化、时变适配四大维度,精准复刻自然光的健康舒适特性,好比是在重构一天中‘晨、午、暮、夜’四个时间段的舒适光环境,实现从‘单点减害’到‘系统增益’的跨越。”

本次活动的核心环节是BOE(京东方)与TÜV莱茵联合发布《自然光显示技术白皮书》。该白皮书基于最新的视觉健康研究和自然光特性在显示产业的应用实践,将自然光对人体身心健康的有益特性进行详细分解,并对现有的科学实践和理论研究对自然光有益特性的支持证据进行了分级,为未来建立科学有序的自然光技术评价体系提供了依据。TÜV莱茵大中华区电子电气产品服务区域总经理、全球显示技术总监刘喜强表示:“这份白皮书的发布代表着我们对于未来显示设备如何才能确保人类的长期视觉健康与发展的重新审视,相信将会有越来越多的产品应用自然光技术,为广大消费者带来更健康和舒适的体验。”

品鉴会现场,BOE(京东方)精心打造了 “晨、午、暮、夜”技术体验展区,多款全球首发的基于“自然光”显示技术(BNL)产品与生态伙伴产品集中亮相,生动诠释了BNL技术如何解决用户在真实场景中的视觉健康痛点。

· 晨 · 光形优化展区:模拟清晨柔和漫射光,通过光形优化中的低眩光、低反射、广视角技术,大幅抑制屏幕反光和眩光。创维壁纸电视A7F Pro系列通过极低的DGR(显示眩光值)使得眩光反光几乎不存在,不仅显示艺术画作细腻真实,观影效果亦媲美真实世界;ROG枪神9 Plus超竞版搭载ACR技术减少约55%光线反射,实现至高4.5倍环境光对比度,让用户在室内复杂光环境下也能获得如阅读纸质书般的舒适体验。

· 午 · 偏振调节展区:借鉴正午阳光经由大气层散射后偏振光含量最少,最接近自然光自然无偏振的特性,突破传统屏幕线偏振局限,推出圆偏振、随机偏振等技术。EVNIA弈威全球首款舒视蓝4.0圆偏光护眼显示器兼顾护眼与高刷;BOE(京东方)全球首发的10.95英寸RDF平板实现屏幕光线全域均匀振动,从物理根源预防因对眼底视网膜叶黄素的局部刺激导致的视疲劳。

· 暮 · 光谱优化展区:复刻傍晚霞光中红光比例高、蓝光比例低的特质,通过低蓝光、有益红光、红外光等技术,在削减有害蓝光的同时主动补充有益红光、增加红外光。另外通过全光谱、节律调节等光谱优化技术,补全宽光谱、调节人体睡眠。BOE(京东方)14英寸全光谱笔记本拥有63%业界领先光谱分布匹配度,14英寸有益红光笔记本实现50%有益红光能量占比,可改善血液循环,并对近视抑制有一定作用,从“被动减害”升级为“主动增益”。联想ThinkVision P27QD-40以“好屏看得见,性能再跃升”为核心理念,可以将有害蓝光降低到35%以下,并获得TÜV莱茵眼部舒适度(5星)认证、EyeSafe 2.0认证,为企业办公定制视觉新标准。

· 夜 · 时变适配展区:对标暗夜微光稳定无频闪、光强连续变化的特性,融合超高刷、超高频PWM等低频闪技术以及跟随环境光自适应的光感技术。一加15搭载BOE(京东方)第三代东方屏,实现1nit超低亮暗夜显示;荣耀Magic6 RSR采用超高频4320Hz PWM调光,能减少频闪对眼睛的刺激,缓解视觉疲劳,获得了TÜV莱茵无频闪认证。

· 中心展区:BOE(京东方)13.8英寸“自然光”显示平板作为全球首款BNL综合集成产品,集BNL四大维度于一身,拥有BOE(京东方)首创的红外光、RDF解偏技术,业界最高光谱分布匹配度的全光谱技术及低蓝光、低频闪等BNL技术,并荣获CES 2026护眼显示全球创新金奖,成为BNL技术系统级能力的完美缩影。

在“光·合作用”圆桌论坛环节,BOE(京东方)、TÜV莱茵、联想及行业权威专家从技术、标准、应用、产业四大维度展开深入探讨。专家们一致认为,BNL技术的推出标志着显示产业实现从“参数导向”向“用户健康需求导向”的根本性重构;而TÜV莱茵白皮书的发布则为行业提供了从“无序宣传”走向“规范认证”的路径。

活动尾声,BOE(京东方)正式启动“光合作用”健康护眼计划,以 BOE (京东方)领先的“自然光”显示技术为基石,定义健康显示新标准,构建协同创新的全场景生态,照亮全民健康的智慧未来。这一计划与BOE(京东方)可持续发展品牌ONE的核心理念高度契合,彰显了BOE(京东方)作为显示产业龙头企业的社会责任与担当。

从定义“自然光”标准到健康显示白皮书发布,BOE(京东方)正通过“技术创新+标准制定”,让每一块屏幕都成为用户视觉健康的守护者。在“屏之物联”战略指引下,BOE(京东方)将继续携手全球伙伴,推动显示产业向更健康、更舒适、更规范的方向迈进,让“好屏认准京东方”成为显示行业公认的价值标杆。

收起阅读 »

一大波危险的“龙虾”来袭,绿盟君助您安全“养虾”

近期,工业和信息化部网络安全威胁和漏洞信息共享平台(NVDB)发布了一则重磅预警:OpenClaw开源AI智能体部分实例在默认或不当配置下存在严重安全风险,极易引发网络攻击、信息泄露等安全问题。这一预警犹如一石激起千层浪,引发了业界对AI智能体安全的高度关注。...
继续阅读 »

近期,工业和信息化部网络安全威胁和漏洞信息共享平台(NVDB)发布了一则重磅预警:OpenClaw开源AI智能体部分实例在默认或不当配置下存在严重安全风险,极易引发网络攻击、信息泄露等安全问题。这一预警犹如一石激起千层浪,引发了业界对AI智能体安全的高度关注。


OpenClaw引领智能体全面爆发,

安全问题频发

2026年,AI智能体技术迎来全面爆发。作为其中的代表性项目,OpenClaw(曾用名Clawdbot、Moltbot)凭借其强大的能力备受青睐——它能够整合多渠道通信能力与大语言模型,构建具备持久记忆、主动执行能力的定制化AI助手,支持本地私有化部署。

然而,正是这样一位“能干的助手”,却可能成为潜伏在您网络中的“定时炸弹”。    


OpenClaw三大核心风险,不容忽视

OpenClaw由个人程序员编写,从发布到爆火仅仅几个月时间,由于自身设计特点,存在天然的“信任边界模糊”问题。它具备持续运行、自主决策、调用系统和外部资源等能力,在缺乏有效权限控制、审计机制和安全加固的情况下,将面临三重严重风险:


安全风险1:代码安全堪忧——三天两高危RCE,系统可被恶意接管

OpenClaw的代码库在短期内连续曝出两个高危远程代码执行漏洞(RCE)。攻击者无需复杂操作,即可利用漏洞在目标主机上执行任意代码,实现从“入侵”到“接管”的一步跨越。一旦得手,OpenClaw所在的主机将成为攻击者的“肉鸡”,企业核心数据、内部网络将完全暴露在风险之下。这不是危言耸听,而是已在野外被积极利用的真实威胁。


安全风险2: 盲目信任放大风险——“以安全换便捷”,Agent沦为攻击跳板

OpenClaw的设计理念强调“自主性”,默认配置往往为了便捷而牺牲安全。许多用户在部署时,为了能让工作更便捷,需要赋予它极高的权限,甚至让其直接访问敏感系统或数据库。这种对Agent的“盲目信任”,让攻击者能够通过诱导式指令,轻松操纵OpenClaw执行越权操作——比如读取机密文件、发送恶意邮件、横向移动攻击内网其他主机。你以为它是你的得力助手,实际上它可能正在被敌人遥控。


安全风险3: 插件系统成供应链突破口——隔离机制缺失,投毒威胁放大

OpenClaw支持通过Skills插件系统扩展功能,但这片“沃土”也成为攻击者的乐园。第三方插件来源不明、供应链环节缺乏审查,加之OpenClaw自身对插件运行缺乏有效的隔离机制,使得一个被投毒的插件就能成为“特洛伊木马”。一旦插件被安装,恶意代码便能随OpenClaw权限肆意横行,窃取数据、植入后门,甚至通过插件更新机制将投毒扩散至更多用户。供应链安全的薄弱环节,在此被无限放大。

谷歌在2月份就已经连夜封禁OpenClaw,Facebook、Mata、微软等几家巨头也不允许员工在公司内部使用OpenClaw。微软安全团队已将此情形定性为“具有持久凭据的不可信代码执行环境”,这一评价值得每一个正在或计划使用AI智能体的企业深思。


客户真实痛点,您是否感同身受?

在与众多企业用户的交流中,我们听到了两种典型的担忧:

客户声音1:“我们公司有员工自己偷偷部署了OpenClaw,我担心这些‘影子AI’导致本地主机端口暴露,引发信息泄露。但我连它们在哪里都不知道,更别提管控了。”

客户声音2:“我们业务部门正式部署了OpenClaw,但我想知道它到底做了哪些外部访问?这些访问是否合法合规?是否存在被利用的风险?”

传统安全方案为何失效?

面对OpenClaw这类新型AI智能体,传统安全方案显得力不从心:

流量内容不可见:OpenClaw用户侧API主要进行常规HTTPS调用,流量加密传输,传统应用识别方式完全失效。

端口识别易绕过:OpenClaw虽有默认端口,但极易被修改,单纯依赖端口识别不仅准确率低,还容易被攻击者绕过。


绿盟科技OpenClaw安全防护方案:

给AI装上“安全护栏”

针对OpenClaw带来的新型安全挑战,绿盟科技创新性地推出低成本“AI安全一体机+绿盟防火墙NF联动”解决方案,帮助用户实现对AI智能体的精准识别与全面管控。


方案核心能力

精准识别:AI安全一体机内置AI智能体发现能力,可主动扫描内网环境,精确识别哪些主机部署了OpenClaw等AI智能体。

灵活管控:根据企业策略,可对非法部署的OpenClaw进行网络隔离,对合法部署的OpenClaw进行全程行为跟踪。

纵深防御:防火墙对OpenClaw的会话访问进行实时分析,识别恶意URL、入侵威胁、病毒等风险,确保每一次访问都安全可控。


方案优势

识别精准:从资产纬度对AI智能体进行主动识别,告别传统方案的误报与漏报。

部署便捷:不影响原有组网,快速接入,即插即用。

全面可视:对OpenClaw的访问行为进行全程跟踪分析,让安全风险无处遁形。


两大应用场景,全方位守护

场景1:对非法OpenClaw进行精准封堵

当员工私自部署OpenClaw,AI安全一体机第一时间发现并定位,将非法安装的PC信息同步给防火墙。防火墙自动生成黑名单策略——无论外部试图访问该OpenClaw,还是OpenClaw主动外联,都将被实时阻断,将风险扼杀在萌芽状态。

 

场景2:对合法OpenClaw进行安全管控

对于业务部门正式部署的OpenClaw,AI安全一体机将其纳入管控范围。防火墙生成精细化管控策略,对OpenClaw的所有访问会话记录流量日志,进行全方位安全检测,避免恶意URL、入侵威胁、病毒等威胁趁虚而入,让业务在安全轨道上平稳运行。

 

两会强调“发展与安全并重”,

绿盟科技如何解读?

近期全国两会多次强调“发展与安全并重”,这释放了一个清晰信号:安全不是AI创新的刹车,而是AI落地的护栏和加速器。

作为安全厂商,我们的目标就是让客户在AI的高速公路上既能跑得快,又能刹得住、不跑偏。具体到企业落地,要避免“安全拖慢AI”或“AI裸奔”,我们主要通过“双引擎驱动”和“原生融合”的策略来解决。


以智增效:让安全成为创新加速器

很多企业担心:上了安全措施,AI应用会不会变慢?我们的答案是:用更聪明的AI去解决安全效率问题。

绿盟“风云卫”AI安全能力平台,本质上就是一个“安全效率倍增器”。它将AI能力“原子化”地嵌入安全运营的每一个环节,让过去需要大量人工的重复性工作变得自动化、智能化。面对海量安全告警,我们的AI安全运营中心可以实现“AI优化AI”的能效革命,大幅提升安全事件的处置效率。


为AI“上险”:拒绝“裸奔”式创新

我们坚决反对“先狂奔、再补胎”的裸奔式创新。当企业把核心业务交给大模型时,大模型本身的幻觉、数据泄露、提示词注入等风险,就成了企业的“阿喀琉斯之踵”。

绿盟的做法是构建一套从 “事前评估、事中防护、事后审计”的AI原生纵深防御体系,形象地说就是给大模型穿上“金钟罩”。我们推出的清风卫”系列安全防护产品,能在模型运行时实时防御各类新型攻击。同时,针对合规要求,绿盟大模型备案服务能够帮助企业构建“可自证”的合规体系,提前预见风险,规避千万级罚款风险。

OpenClaw的“虾”来了,别让您的网络成为坏虾的“养殖场”。立即行动,为您的AI应用加上一道“安全护栏”!

凭借二十多年的攻防实战经验,绿盟科技致力于成为您智能化转型路上最可靠的“副驾驶”——帮您看清路况、预警风险,让您可以更安心地手握方向盘,专注于业务创新的加速与超越,共同驶向智能化的未来。


收起阅读 »

e签宝对话中建四局|产业为基,AI为翼,解码建筑央企的数字化章法

建筑业的转型逻辑正在被时代重写。行业共识日趋清晰:数字化不再是锦上添花的可选项,而是关乎企业核心竞争力的必答题。而当AI浪潮席卷各行各业,这道必答题又增添了新的难度与想象空间。在此背景下,作为体量庞大、链条复杂的央企代表,中建四局的数字化转型实践尤为引人关注。...
继续阅读 »

建筑业的转型逻辑正在被时代重写。行业共识日趋清晰:数字化不再是锦上添花的可选项,而是关乎企业核心竞争力的必答题。而当AI浪潮席卷各行各业,这道必答题又增添了新的难度与想象空间。

在此背景下,作为体量庞大、链条复杂的央企代表,中建四局的数字化转型实践尤为引人关注。这家拥有数万工人的建筑巨头,将如何拥抱AI?又将如何走出国企不同于民企的转型路径?当自身的数字基建初具规模,它又如何将能力向外辐射,推动产业链从单点提效走向系统升级?本期e签宝《对话》栏目走进中建四局,与其数字化部总经理符和清展开深度对谈,共同解码这家建筑央企在数字时代的破局思路与实践。

拥抱AI从具体场景做起AI落地的“小成果”逻辑,让优化叠加

AI是这场对话的时代背景,也是绕不开的开场话题。行业自上而下的共识正在凝聚:AI不再是遥不可及的未来概念,而是建筑业提质增效的现实变量。但“如何用、用在哪儿”仍是普遍困惑。有行业调研显示,建筑企业在推进AI时存在三类典型误区:重技术基座轻应用场景、试图全自建导致门槛过高、初期成效未达预期便选择观望。

正是在这样的背景下,e签宝联合创始人、高级副总裁张晋直言,AI浪潮席卷各行各业,对国央企影响大吗?中建四局在落地应用上又做了哪些探索?

中建四局数字化部总经理 符和清坦言,国央企对AI的重视程度出奇一致,首先是响应国家战略的需要。但在具体落地上,他的判断颇为清醒:AI不会是某个巨无霸式的综合大平台,而应是底层平台支撑、上层场景开花的格局——真正能够改变生产效率的,往往是那些像智能体一样聚焦具体环节的小众应用。

循着这个思路,中建四局已经在多个场景上展开探索。得益于两年前推动的合同结构化,他们在合同智能审查上有了用武之地;施工图纸审查、商务算量、目标成本测算等环节,AI也开始介入那些繁琐的重复劳动。符和清把这些称为“小成果”——没有惊天动地的大平台,但在知识库搜索、制度查询这些细碎场景里,效率的提升是实实在在的。

他打了个比方:AI不像是造出一辆全新的汽车,更像是优化了某个齿轮、改进了某个发动机零件。未来的数字化,一定是无数个这样的“小优化”叠加起来,最终让整个系统跑得更顺、更高效。

深耕场景、务实推进的这种思路,也将贯穿于中建四局对数字化每一个命题的思考与实践之中。

从蓝图到一线国央企数字化的慢功夫与真问题

同样的数字化,国央企与民企的两样走法

随后张晋便抛出了一个很基础但是许多人关心的问题:国央企与民营企业在推进数字化转型的过程中,到底有哪些不同?中建四局数字化部总经理符和清结合自己从阿里到国央企的多年经历,从三个维度给出了洞察。

第一重差异在于组织能力。民企极少设置三级及以上机构,扁平化的组织让战略能直达一线。而在中建四局这样的大型国央企,从集团、工程局、公司、分公司到项目部,层级纵深长达五级。同样一句话从顶层发出,穿透层层组织后,不同节点的理解和执行难免产生偏差。

第二重差异体现在实践思路上。民企的数字化往往自下而上生长,一个部门的尝试、一个场景的创新,都可能快速迭代成燎原之火。而国央企更倾向于顶层设计先行——先做蓝图规划,再分解为项目群、项目、产品,层层落地。对于体量庞大、业务复杂的国央企而言,没有清晰的蓝图,就谈不上有序的推进。

第三重差异落在细节管理上。在国央企,业务人员懂业务却难以精准表达需求,技术人员懂技术却无法真正理解业务场景,中间横亘着巨大的认知裂缝。如何有效整合资源、转化需求、把控架构,是国央企在数字化落地中需要投入更多精力的地方。

这三重差异,勾勒出国企数字化转型的独特底色:它注定不是一场快速突围战,而是一场需要穿透层级、统筹规划、弥合裂缝的体系工程。

数字化深水区中的“两张皮”现象

谈及业务与数字化“两张皮”,符和清没有回避。这是建筑行业的老话题,也是真问题——这些年行业上了不少系统,云端工厂、智慧工地、数字看板,一个个新名词落地成屏,挂满了项目部的墙面和电视。但系统是不是真的在用?数据是不是真的在跑?还是说,只是把原来的线下表格搬到了大屏上,看上去热闹,业务该怎么干还是怎么干?这不仅是四局的考题,也是整个行业在数字化深水区绕不开的一道坎。

在符和清看来,这个问题在国央企尤为突出:体系庞大、层级多、角色杂,认知不统一是天然难题。但在四局的实践中,他们摸索出了几个关键的抓手。

首先是“一把手工程”的决心。数字化是一把双刃剑——系统上线意味着流程透明、审计严格,过去能含糊过去的问题都会浮出水面。高层如果没有“认账”的魄力,遇到阻力就容易动摇。“数字化本质是业务重塑,既然要改,就得有从上到下的改革决心。”

其次是IT与业务组织的深度协同。符和清反复强调,数字化绝不能是技术部门的一厢情愿,必须由业务部门来推动。“系统第一版能用就行,关键是业务愿意用起来。”而IT部门要做的,是持续优化、快速响应,确保大家“用得动、改得好”。“只要业务在用,一年不行,两年三年总能磨出来。”

最后是培训与认知的普及。他打了个比方:给农村老太太一个苹果手机,不教不用,最后还是躺在抽屉里。再好的数字化产品,如果员工不理解、不会用,数据不准、流程空转,“两张皮”就自然而生。符和清提到,像四局这种三万人左右的体量,只要数字化系统上线,基本都要组织封闭式的大规模培训,给大家“交底”。通过这种持续渗透,让不同层级的人真正理解系统、用透系统。

这多重解法背后,是一个朴素的认知:数字化从来不是系统上线即告捷,而是人与流程、组织与工具长期磨合、逐步深化的过程。如今在新鸿基广州南站的项目上,数字化已经实打实地用起来了——在中建四局的转型实践中,“务实”正成为越来越清晰的注脚。

不是零敲碎打而是体系推进,数字化路径有章可循

从单点到全链,中建四局率先出招产业互联网先手棋

“建筑产业互联网”,这是一个近年来在行业内热度渐起的词汇——从行业协会连续三年举办产业互联网发展大会,到各地纷纷布局区域级平台,行业共识正在凝聚:建筑业的数字化,正在从单点突破走向全链协同。

符和清介绍到,其实“建筑产业互联网”这个概念,中建早在2020年做“136规划”时就已写入目标——那个“1”,就是要构建产业互联网。

在他的解读里,产业互联网不是新名词包装,而是围绕建筑全生命周期的数字化主线:从勘察、设计、施工,到交付、运维、运营,每一个环节都要有数据贯穿。而作为全球最大的建筑商之一,中建四局有这个体量去整合上中下游的资源协同。

他细数了中建四局目前的进展:内部施工环节已经通过DMP平台实现了数字化,对下游的分包、劳务、供应链协同也基本跑通,上游与政府监管系统也有零星连接。未来的远景目标,是以建筑为单元,连接城市运营,让数据在整个生命周期里真正流转起来。

“中间环节我们自己做通了,上下游也在逐步打通。”符和清说,产业互联网的蓝图,正在从规划一步步走向现实。

从规划到落地:DMP平台承载四局数字化蓝图

在不久前举办的智能建造观摩会上,中建四局正式发布了DMP数字化管理平台,引起行业关注。谈及这个平台的来龙去脉,符和清把它放进了四局数字化转型的整体框架里。

他介绍,三年前做顶层规划时,四局明确了五个数字化方向:管理数字化、生产体系数字化、供应链数字化、项目现场数字化,以及面向未来的全生命周期运营。而DMP平台,正是承载这五大方向的统一载体。

目前DMP的发力点主要有两个维度:横向上,从项目投标开始,贯穿施工、结算到最终运营,让经济数据全链条跑通;纵向上,基于生产现场的管理,把设计图纸、清单量与施工进度绑定,自动算量、算成本、算收入,最终自动呈现真实的利润。

“这个工程难度相当大,在全行业来看,中建也是走在前面的。”符和清说。这套平台不仅是对内提效的工具,更是四局构建产业互联网蓝图的关键一步。

不止于提效,e签宝电子签成为四局数字化协同关键

对于体系庞大的建筑行业来说,供应链的数字化非常具有典型性。据符和清介绍,中建四局在供应商资源整合、招采、物流体系及订单管理方面已经相当成熟。从前两年开始,对下游分包环节,尤其是施工队伍,通过DMP平台实现了深度协同;在上游也取得突破,劳务管理、农民工工资发放等环节已基本实现数字化对接。

谈及具体落地成效,符和清特别提到了e签宝电子签章系统带来的变化。目前,e签宝电子用印不仅覆盖了对上游的承包合同和对下游的分包合同这两类核心业务,还在内部管理体系中全面铺开——全局3万人的行政办公,从发文、收文到红头文件用印,已全部实现电子化;下属地产公司等多家单位的对外业务合同也陆续上线使用。

“今年局里已经发了制度和文件,要求未来电子用印全部实现数字化。”符和清说。这套系统不仅解决了传统用印的流程痛点,更成为四局打通上下游协同的“连接器”——从内部办公到外部合同,从上游甲方到下游分包,电子签章让每一份文件的流转都留下了清晰的数字化足迹。

而随着AI能力的深度融入,电子签章正在从“连接器”进化为更智能的合同中枢——从智能审查到风险预警,从合同结构化到履约跟踪,AI让每一枚电子印章背后都有了更强大的支撑。这也正是符和清所说的“小优化叠加”:当AI赋能每一个细碎场景,系统自然跑得更顺、更高效。

数字化不是一场立竿见影的技术革命,而是一场需要穿透层级、统筹规划、弥合裂缝的体系工程。它既需要顶层设计的定力,也需要一线落地的韧性;既需要一把手工程的决心,也需要业务与IT的长期磨合。而AI的加入,正在让每一个“齿轮”的优化变得更快、更准。

在这场建筑行业深刻的数字化变革中,e签宝很荣幸成为中建四局数字化拼图中的一块——从内部办公到外部合同,从上游甲方到下游分包,电子签章已成为打通产业链协同的“连接器”。从电子签章到智能合同Agent,e签宝正从电子签名服务商进化为AI驱动型数字信任基础设施提供商。未来,e签宝将继续以AI赋能、以可信筑基,助力更多企业从单点提效走向全链协同,共同见证产业互联网从蓝图走向现实。

收起阅读 »

ZeroClaw 实战:Rust 重构版 OpenClaw,7.8MB 内存秒启动的 AI 助手

OpenClaw 功能强大,但在内存占用和启动速度方面存在挑战。针对这些问题,主打极速、轻量的 Rust 重构版 ZeroClaw 应运而生。 整篇文章的目标只有一个: 让你看完后,能在本地服务器上部署 ZeroClaw,体验 7.8MB 内存、秒启动的 A...
继续阅读 »

OpenClaw 功能强大,但在内存占用和启动速度方面存在挑战。针对这些问题,主打极速、轻量的 Rust 重构版 ZeroClaw 应运而生。


整篇文章的目标只有一个:



让你看完后,能在本地服务器上部署 ZeroClaw,体验 7.8MB 内存、秒启动的 AI 助手,并在实际项目中发挥它的价值。





一、ZeroClaw 是什么?为什么值得一试?


zeroclaw.png


1.1 性能对比


如果把它和 OpenClaw 放在一起对比,ZeroClaw 可以说是个妥妥的性能怪兽


根据官方基准测试(macOS arm64,2026年2月,针对 0.8GHz 边缘硬件标准化),ZeroClaw 的表现如下:


指标OpenClawZeroClaw 🦀
编程语言TypeScriptRust
内存占用> 1GB< 5MB
启动速度(0.8GHz 核心)> 500s< 10ms
二进制大小~28MB (dist)3.4 MB
硬件成本Mac Mini $599任意硬件 $10

zero-claw-comparison.jpeg


这个项目用 Rust 编写,ZeroClaw 运行时内存不到 5MB、启动时间小于 10ms、二进制体积仅约 3.4MB,支持在树莓派或者低配云主机上部署 Agent。


从性能角度看,它具备几个关键特性:



  • 🏎️ 极致精简:< 5MB 内存占用,比 OpenClaw 核心小 99%

  • 💰 成本极低:高效到可以在 $10 硬件上运行,比 Mac mini 便宜 98%

  • 闪电启动:启动速度提升 400 倍,在 < 10ms 内启动(即使在 0.6GHz 核心上也能在 1 秒内启动)

  • 🌍 真正可移植:跨 ARM、x86 和 RISC-V 的单一自包含二进制文件


1.2 适用场景


场景 A:资源受限环境


如果你需要在树莓派、低配云主机(1GB 内存)上部署 AI Agent,ZeroClaw 无疑是最优选。


它那极低的资源占用,能大幅减少服务器资源的浪费。用省下的内存,来运行多一点其他业务,不香吗?


场景 B:自动化流水线与服务器运维


如果需求是每天定时抓取博客、监控服务器日志,或者在配置较低的云服务器上部署,ZeroClaw 的轻量特性让它成为理想选择。


场景 C:批量部署


对于需要在一人企业中批量部署多个 AI Agent 的场景,ZeroClaw 的小体积和低资源占用,让批量部署成为可能。


1.3 架构设计:一切都是 Trait


ZeroClaw 的核心设计理念是:每个子系统都是一个 trait,只需更改配置即可交换实现,无需修改代码。


architecture.svg
ZeroClaw 架构图,展示各个子系统(Provider、Channel、Memory、Tools 等)的 trait 设计和可插拔架构


核心子系统


子系统Trait内置实现可扩展
AI 模型Provider22+ providers(OpenRouter、Anthropic、OpenAI、Ollama、Venice、Groq、Mistral、xAI、DeepSeek、Together、Fireworks、Perplexity、Cohere、Bedrock 等)custom:https://your-api.com — 任何 OpenAI 兼容的 API
通信渠道ChannelCLI、Telegram、Discord、Slack、iMessage、Matrix、WhatsApp、Webhook任何消息 API
记忆系统MemorySQLite 混合搜索(FTS5 + 向量余弦相似度)、Markdown任何持久化后端
工具Toolshell、file_read、file_write、memory_store、memory_recall、memory_forget、browser_open(Brave + 白名单)、composio(可选)任何能力
可观测性ObserverNoop、Log、MultiPrometheus、OTel
运行时RuntimeAdapterNative(Mac/Linux/Pi)Docker、WASM(计划中)
安全策略SecurityPolicyGateway 配对、沙箱、白名单、速率限制、文件系统作用域、加密密钥
身份系统IdentityConfigOpenClaw(markdown)、AIEOS v1.1(JSON)任何身份格式
隧道Tunnel、Cloudflare、Tailscale、ngrok、Custom任何隧道二进制文件

这种设计让 ZeroClaw 具有极强的可扩展性和灵活性,你可以根据实际需求替换任何组件,而无需修改核心代码。




二、开始前你需要准备好的东西


动手之前,先确认这几样已经就绪。



  1. 一台 Linux/macOS 服务器(Windows 需要 WSL)

    ZeroClaw 是纯 Rust 项目,主要支持 Linux 和 macOS。Windows 用户需要先安装 WSL。

  2. Rust 环境(如果还没安装,下面会带你安装)

    由于 ZeroClaw 是纯 Rust 项目,需要先安装 Rust 编译环境。

  3. LLM API Key(OpenAI、Anthropic、DeepSeek、OpenRouter 等)

    用于配置 AI 模型,支持主流的大模型服务。




三、手把手安装:10 分钟搞定 ZeroClaw


3.1 环境准备:安装 Rust


由于 ZeroClaw 是纯 Rust 项目,如果我们电脑里还没安装 Rust,需要先把 Rust 环境准备好。


Linux / macOS:一条命令安装


curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

rustc --version
cargo --version

看到版本号后,说明 Rust 安装成功,可以继续后面的 ZeroClaw 编译和安装步骤了。


开始安装


architecture.svg


安装成功


git-installl1-1.png


Windows:通过 WSL 安装(推荐 ZeroClaw 场景)


ZeroClaw 更适合跑在 Linux 环境里,所以在 Windows 下,推荐先装好 WSL(如 Ubuntu),然后在 WSL 终端里执行和 Linux 一样的命令,看到版本号后,就可以在这个 WSL 环境中继续后面的 ZeroClaw 编译和安装步骤了。


3.2 编译安装 ZeroClaw


环境装好后,我们把代码拉下来,就能开始编译安装了。


这里推荐 Release 版,因为它体积最小、速度最快:


# 克隆仓库
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw

# 编译 Release 版本(优化后的版本)
cargo build --release

# 安装到系统路径
cargo install --path . --force

cargo-install21.png


cargo-install22.png



⚠️ 编译时间较长

首次编译 Rust 项目可能需要 5 - 10 分钟,取决于你的机器性能。这是正常现象,因为 Rust 需要编译所有依赖。

解决:耐心等待,或者使用 cargo build --release -j $(nproc) 来并行编译加速。



编译完成后,验证安装:


zeroclaw --version

如果显示版本号,说明安装成功。


3.3 基础配置:交互式向导


这个项目不仅安装快,配置也极其人性化。交互式向导:


zeroclaw onboard --interactive

zeroclaw3.png


这是一个完整的 7 步交互式向导,会引导你完成所有配置。


配置向导会引导你完成



  1. 输入 LLM 的 API Key

    支持 OpenAI, Anthropic, DeepSeek, OpenRouter 等主流模型。

    根据你选择的模型服务,输入对应的 API Key。

  2. 选择想连接的渠道

    比如 Slack, Discord, Telegram, WhatsApp 等。

    如果暂时不确定,可以先跳过,后续再配置。

  3. 安全设置:强制设置一个 "配对码"

    防止陌生人乱连我们的 Agent。

    这个配对码很重要,后续连接时需要用到。

  4. 配置渠道白名单

    为了安全,建议配置允许列表,只允许特定用户连接。

  5. 其他高级配置

    包括内存后端、隧道配置等。


配置完成后,配置文件通常保存在:



  • Linux/macOS~/.zeroclaw/config.toml

  • Windows(WSL)~/.zeroclaw/config.toml



⚠️ 渠道白名单配置

如果配置后收到消息但 ZeroClaw 没有响应,可能是白名单配置问题。

解决



  1. 查看日志,找到发送者的身份标识

  2. 运行 zeroclaw onboard --channels-only 重新配置白名单

  3. 或者临时使用 "*" 允许所有(仅用于测试)



3.4 启动和使用 ZeroClaw


启动守护进程


如果你希望 ZeroClaw 长期运行,处理定时任务和自动响应:


zeroclaw daemon

此时,它就在后台默默运行了。


zero-start.png


我们可以随时用以下命令查看它的状态:


zeroclaw status

zero-start2.png


其他实用命令


# 运行系统诊断
zeroclaw doctor

# 检查渠道健康状态
zeroclaw channel doctor

# 查看集成信息(如 Telegram)
zeroclaw integrations info Telegram

# 管理后台服务
zeroclaw service install
zeroclaw service status

现在,你就拥有一个 24 小时待命的全功能 AI 助手




四、配置文件详解:定制你的 ZeroClaw


配置文件位置:~/.zeroclaw/config.toml(由 onboard 命令创建)


4.1 基础配置


# API 密钥(支持加密存储)
api_key = "sk-..."
default_provider = "openrouter" # 默认 provider
default_model = "anthropic/claude-sonnet-4-20250514" # 默认模型
default_temperature = 0.7 # 默认温度参数

4.2 内存配置


[memory]
backend = "sqlite" # "sqlite", "markdown", "none"
auto_save = true # 自动保存
embedding_provider = "openai" # "openai", "noop"
vector_weight = 0.7 # 向量搜索权重
keyword_weight = 0.3 # 关键词搜索权重

4.3 Gateway 配置


[gateway]
require_pairing = true # 首次连接需要配对码
allow_public_bind = false # 拒绝 0.0.0.0 绑定(无隧道时)

4.4 自主性配置


[autonomy]
level = "supervised" # "readonly", "supervised", "full"
workspace_only = true # 限制在工作区范围内
allowed_commands = ["git", "npm", "cargo", "ls", "cat", "grep"] # 允许的命令
forbidden_paths = ["/etc", "/root", "/proc", "/sys", "~/.ssh", "~/.gnupg", "~/.aws"] # 禁止访问的路径

4.5 其他配置


[runtime]
kind = "native" # 当前仅支持 "native"

[heartbeat]
enabled = false # 是否启用定时任务
interval_minutes = 30 # 任务执行间隔

[tunnel]
provider = "none" # "none", "cloudflare", "tailscale", "ngrok", "custom"

[secrets]
encrypt = true # API 密钥加密存储

[browser]
enabled = false # 是否启用浏览器工具
allowed_domains = ["docs.rs"] # 允许访问的域名

[composio]
enabled = false # 是否启用 Composio(1000+ OAuth 应用)


📌 提示:配置文件支持热重载,修改后重启服务即可生效。建议使用 zeroclaw doctor 检查配置是否正确。





五、OpenClaw 和 ZeroClaw,怎么选?


简单说,可以按场景来选:



  • 如果你更关注交互体验、家庭中枢、可视化能力,已经在用 Mac mini 等环境做本地 AI 中控,那继续用 OpenClaw 会更顺手。

  • 如果你更在意资源占用、启动速度、批量部署,尤其是打算在树莓派、低配云服务器上长期跑 Agent,那 ZeroClaw 会是更合适的选择。


很多时候,两者是可以并存的:用 OpenClaw 做「大中枢」,用 ZeroClaw 覆盖「边缘节点」和自动化脚本,各自发挥所长。




六、相关资源


GitHub 项目地址

github.com/zeroclaw-la…


官方文档



我的其他相关文章





结语:ZeroClaw 作为 OpenClaw 的 Rust 重构版,在保持核心功能的同时,大幅降低了资源占用和启动时间。对于需要在资源受限环境或批量部署场景下使用 AI Agent 的朋友来说,ZeroClaw 无疑是一个值得尝试的选择。


希望本文能够帮助你顺利完成部署,在实际项目中发挥 ZeroClaw 的价值。如果在部署过程中遇到问题,欢迎查阅官方文档或相关社区获取帮助。


作者:星浩AI
来源:juejin.cn/post/7610997893576376354
收起阅读 »

Skills 实战:让 AI 成为你的领域专家

引言:从通用助手到领域专家 想象一下这些场景: 场景 1: 重复的上下文说明 你: "帮我分析这个 BigQuery 数据,记住要排除测试账户,使用 user_metrics 表..." Claude: "好的,我来分析..." [第二天] 你: "再帮我分...
继续阅读 »

引言:从通用助手到领域专家


想象一下这些场景:


场景 1: 重复的上下文说明


你: "帮我分析这个 BigQuery 数据,记住要排除测试账户,使用 user_metrics 表..."
Claude: "好的,我来分析..."

[第二天]
你: "再帮我分析一次销售数据,还是那个表,记得排除测试账户..."
Claude: "好的,我来分析..." # 😓 又要重复一遍

场景 2: 领域知识的重复传授


你: "帮我处理这个 PDF 表单,PDF 的表单字段结构是..."
Claude: "明白了"

[一周后]
你: "再处理一个 PDF 表单..."
Claude: "请告诉我 PDF 表单的结构" # 😓 忘记了

场景 3: 工作流程的不一致


你: "生成 API 文档,记得包含请求示例、响应格式、错误码..."
Claude: "好的" # ✅ 这次做得很好

[下次]
你: "再生成一份 API 文档"
Claude: [生成的文档] # ❌ 这次忘记了错误码部分

这些问题的根源是:每次对话都是全新的开始,Claude 无法记住你的领域知识、偏好和工作流程。


💡 Skills 系统的价值


Skills 就是解决这个问题的方案——它让你能够:



  1. 📦 封装领域知识: 把你反复向 Claude 解释的专业知识打包成 Skill

  2. 🔄 自动加载: 当任务相关时,Skill 自动激活,无需重复说明

  3. ♻️ 持续复用: 创建一次,跨所有对话自动使用

  4. 🎯 专业能力: 让 Claude 从通用助手进化为领域专家


本文核心内容:



  1. Skills 的核心概念与工作原理

  2. 渐进式披露架构:三级加载机制

  3. 创建自定义 Skills:从入门到精通

  4. 最佳实践:简洁、结构化、可验证

  5. 实战案例:PDF 处理、BigQuery 分析、代码审查

  6. 评估与迭代:如何持续优化 Skills



"把你反复向 Claude 解释的偏好、流程、领域知识打包成 Skills,让 AI 成为你的领域专家"





一、什么是 Skills?


1.1 核心概念


Agent Skills(智能体技能)是一种模块化的能力扩展系统,它为 Claude 提供了:



  • 领域专业知识: 如 PDF 处理技巧、数据库 schema、业务规则

  • 工作流程: 如代码审查流程、文档生成流程、数据分析流程

  • 最佳实践: 如命名规范、代码风格、错误处理模式


1.2 Skills vs 普通 Prompt


维度普通 PromptSkills
作用范围单次对话跨所有相关对话
加载方式每次手动提供相关任务时自动加载
上下文占用每次都占用按需加载,未使用时零占用
知识管理分散在多次对话中集中管理,持续优化
一致性依赖人工记忆标准化,确保一致

类比理解:



  • 普通 Prompt 像是每次都要"现场培训"新员工

  • Skills 像是给员工提供"岗位手册",需要时自己查阅


1.3 Skills 遵循开放标准


Claude Code Skills 基于 Agent Skills 开放标准,这意味着:



  • ✅ 标准化格式,跨 AI 工具兼容

  • ✅ 社区生态,可以使用他人创建的 Skills

  • ✅ 长期支持,不会因产品升级而失效


Claude Code 在标准基础上扩展了:



  • 🔧 调用控制机制

  • 🤖 子代理执行能力

  • 📥 动态上下文注入




二、Skills 工作原理:渐进式披露架构


2.1 为什么需要渐进式披露?


问题:如果把所有 Skills 的详细内容都加载到上下文中会怎样?


假设你有 10 个 Skills,每个包含 5000 tokens 的详细指导...
总共: 50,000 tokens

但你可能只需要使用其中 1-2 个 Skill!
浪费: 40,000+ tokens(80% 的上下文窗口!)

解决方案:渐进式披露——只加载需要的内容,按需展开详细信息


2.2 三级加载机制


09-01-progressive-disclosure.png


第一级:元数据(Metadata)- 始终加载


---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
Use when working with PDF files or when the user mentions PDFs, forms,
or document extraction.
---


  • 加载时机: Claude 启动时

  • Token 消耗: 每个 Skill 约 100 tokens

  • 作用: 让 Claude 知道有哪些 Skills 可用,以及何时触发


关键字段解析:



  • name: Skill 标识符(小写字母、数字、连字符)

  • description: 功能说明 + 触发场景(最重要的字段!)



⚠️ 重要: description 是 Skill 触发的关键。Claude 根据用户请求与 description 的匹配度决定是否加载该 Skill。



第二级:指令(Instructions)- 触发时加载


# PDF Processing

## Quick start

Use pdfplumber to extract text from PDFs:

\`\`\`python
import pdfplumber

with pdfplumber.open("document.pdf") as pdf:
text = pdf.pages[0].extract_text()
\`\`\`

For advanced form filling, see [FORMS.md](FORMS.md).


  • 加载时机: 当用户请求匹配 Skill 描述时

  • Token 消耗: 通常少于 5k tokens

  • 作用: 提供具体的操作指导和工作流程


第三级:资源和代码(Resources & Code)- 按需访问


pdf-skill/
├── SKILL.md # 主指令文件(第二级)
├── FORMS.md # 表单填写指南(按需读取)
├── REFERENCE.md # 详细 API 参考(按需读取)
└── scripts/
└── fill_form.py # 工具脚本(执行时不加载代码)


  • 加载时机: 仅当 SKILL.md 中引用时

  • Token 消耗: 脚本执行时只有输出占用 tokens

  • 作用: 提供专业参考材料和可执行工具


2.3 实例演示:从触发到加载


场景: 用户请求"帮我提取 PDF 中的文本"


┌─────────────────────────────────────────┐
步骤 1: Claude 检查所有 Skill 的元数据
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
匹配到 pdf-processing Skill
description 包含 "Extract text from PDF"
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
步骤 2: 加载 SKILL.md 的指令内容
(~3k tokens)
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
步骤 3: Claude 发现需要表单填写
读取 FORMS.md (~2k tokens)
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
Token 消耗: 5k tokens
其他 9 Skills: 0 tokens(未加载)
└─────────────────────────────────────────┘

对比无渐进式披露:


❌ 传统方式: 10 个 Skills × 5k = 50k tokens
✅ 渐进式披露: 只加载 1 个 Skill = 5k tokens
节省: 45k tokens (90% 的上下文!)



三、Skills 的文件结构


3.1 最小化 Skill


最简单的 Skill 只需要一个文件:


my-skill/
└── SKILL.md # 唯一必需的文件

SKILL.md 示例:


---
name: code-review-checklist
description: Provides a code review checklist for pull requests. Use when reviewing code or when the user asks for code review guidelines.
---


# Code Review Checklist

When reviewing code, check:

1. **Functionality**: Does the code do what it's supposed to?
2. **Readability**: Is the code easy to understand?
3. **Tests**: Are there appropriate tests?
4. **Performance**: Are there any obvious performance issues?
5. **Security**: Are there any security vulnerabilities?

For each item, provide specific feedback with examples.

3.2 完整 Skill 结构


对于复杂的 Skills,可以组织成多文件结构:


pdf-processing-skill/
├── SKILL.md # 核心指令(必需)
├── FORMS.md # 表单填写详细指南
├── REFERENCE.md # PDF 库 API 参考
├── EXAMPLES.md # 常见用例示例
└── scripts/
├── analyze_form.py # 分析表单工具
├── fill_form.py # 填写表单工具
└── validate.py # 验证输出工具

3.3 YAML Frontmatter 规范


必填字段:


---
name: skill-name # 必填
description: Skill description # 必填
---

字段要求:


字段要求示例
name小写字母、数字、连字符
最多 64 字符
禁止 "anthropic"、"claude"
pdf-processing
bigquery-analytics
code-reviewer
description非空
最多 1024 字符
包含功能 + 触发场景
第三人称描述
Extract text from PDFs. Use when...
Analyze BigQuery data. Use when...

命名规范:


推荐: 动名词形式(Gerund Form)


processing-pdfs
analyzing-spreadsheets
reviewing-code
managing-databases

避免: 过于模糊


helper          # 太模糊
utils # 不知道干什么
tool # 功能不明确

3.4 Description 字段的重要性


⚠️ 警告: description 是 Skill 触发的关键,必须用第三人称!


为什么必须第三人称?


description 会被注入到系统提示中,视角不一致会导致困惑:


系统提示: "You are Claude, an AI assistant..."
Skill description: "I can help you process PDFs" # ❌ 第一人称,视角冲突!

正确示例:


---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
Use when working with PDF files or when the user mentions PDFs, forms,
or document extraction.
---

不正确示例:


 description: I can help you process Excel files  # 第一人称
description: You can use this to process Excel # 第二人称
description: Helps with documents # 过于模糊

编写技巧:



  1. 明确功能: 说清楚 Skill 能做什么

  2. 包含关键词: 用户可能使用的术语(PDF、Excel、BigQuery 等)

  3. 触发场景: 明确何时使用("Use when...")

  4. 简洁精准: 1-2 句话说清楚




四、创建你的第一个 Skill


4.1 确定需求


问题导向:


问自己:



  1. 我反复向 Claude 解释什么内容?

  2. 哪些领域知识 Claude 不太了解?

  3. 哪些工作流程需要标准化?


示例场景:


场景 1: BigQuery 数据分析



  • ❌ 每次都要说明表结构

  • ❌ 每次都要强调"排除测试账户"

  • ❌ 每次都要说明查询模式

  • ✅ 创建一个 BigQuery Skill!


场景 2: 公司文档规范



  • ❌ 每次都要说明文档模板

  • ❌ 每次都要强调格式要求

  • ❌ 每次都要纠正不符合规范的部分

  • ✅ 创建一个文档规范 Skill!


4.2 编写 SKILL.md


步骤 1: 创建目录和文件


mkdir my-bigquery-skill
cd my-bigquery-skill
touch SKILL.md

步骤 2: 编写 YAML Frontmatter


---
name: bigquery-analytics
description: Analyze BigQuery data from the user_metrics and sales tables. Use when the user asks about data analysis, metrics, or BigQuery queries. Always exclude test accounts and apply standard date filters.
---

步骤 3: 编写核心指令


# BigQuery Analytics

## Database Schema

### user_metrics table
- user_
id (STRING): Unique user identifier

- event_date (DATE): Event date
- metrics_
value (FLOAT): Metric value
- account_type (STRING): "production" or "test"

### sales table
- order_
id (STRING): Order identifier
- user_id (STRING): User ID (foreign key to user_metrics)
- amount (FLOAT): Order amount
- order_date (DATE): Order date

## Standard Filtering Rules

**Always apply these filters:**
1. Exclude test accounts: `WHERE account_
type = 'production'`
2. Date range: Default to last 30 days unless specified
3. Remove null values: `WHERE metrics_value IS NOT NULL`

## Query Patterns

### Pattern 1: User activity analysis
\`\`\`sql
SELECT
event_date,
COUNT(DISTINCT user_
id) as active_users,
AVG(metrics_
value) as avg_metric
FROM user_
metrics
WHERE account_type = 'production'
AND event_
date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GR0UP BY event_date
ORDER BY event_
date;
\`\`\`

### Pattern 2: Sales analysis
\`\`\`sql
SELECT
DATE_TRUNC(order_date, MONTH) as month,
COUNT(*) as order_count,
SUM(amount) as total_revenue
FROM sales s
JOIN user_metrics u ON s.user_id = u.user_id
WHERE u.account_type = 'production'
GR0UP BY month
ORDER BY month;
\`\`\`

## Important Notes

- **Performance**: Always use partitioned date fields in WHERE clause
- **Costs**: Preview query cost before running on large datasets
- **Timezone**: All dates are in UTC



五、核心最佳实践


5.1 简洁为王(Conciseness is Key)


核心原则: 上下文窗口是公共资源,你的 Skill 要与系统提示、对话历史、其他 Skills 共享。


✅ 好的示例(约 50 tokens)


## Extract PDF Text

Use pdfplumber for text extraction:

\`\`\`python
import pdfplumber

with pdfplumber.open("file.pdf") as pdf:
text = pdf.pages[0].extract_text()
\`\`\`

❌ 糟糕的示例(约 150 tokens)


## Extract PDF Text

PDF(便携式文档格式)是一种常见的文件格式,包含文本、图像等内容。
要从 PDF 中提取文本,你需要使用一个库。有很多 PDF 处理库可用,
但我们推荐 pdfplumber,因为它易于使用且能处理大多数情况。
首先,你需要使用 pip 安装它。然后你可以使用下面的代码...

为什么简洁版更好?



  • ✅ 假设 Claude 已经知道 PDF 是什么

  • ✅ 假设 Claude 知道库的工作原理

  • ✅ 直接提供关键信息:用什么库、怎么用

  • ✅ 节省 100 tokens,留给其他 Skills 使用



⚠️ 记住: 不要低估 Claude 的智能!它是通用 AI,不需要你解释基础概念。



5.2 设置适当的自由度


根据任务的脆弱性和可变性,选择合适的指导程度。


🌟 高自由度(基于文本的指令)


适用场景:



  • 多种方法都可行

  • 决策依赖上下文

  • 启发式方法指导


示例:代码审查流程


## Code Review Process

1. Analyze code structure and organization
2. Check for potential bugs or edge cases
3. Suggest improvements for readability and maintainability
4. Verify compliance with project standards

特点: 给出大方向,信任 Claude 根据具体情况调整。


🎯 中等自由度(伪代码或带参数的脚本)


适用场景:



  • 存在首选模式

  • 允许一定变化

  • 配置影响行为


示例:生成报告


## Generate Report

Use this template and customize as needed:

\`\`\`python
def generate_report(data, format="markdown", include_charts=True):
# Process data
# Generate output in specified format
# Optionally include visualizations
\`\`\`

特点: 提供模板和参数,允许根据需求调整。


🔒 低自由度(特定脚本,少量或无参数)


适用场景:



  • 操作易错且脆弱

  • 一致性至关重要

  • 必须遵循特定顺序


示例:数据库迁移


## Database Migration

Execute this script strictly:

\`\`\`bash
python scripts/migrate.py --verify --backup
\`\`\`

Do not modify the command or add extra parameters.

特点: 精确指令,不允许偏离。


🌉 类比理解


把 Claude 想象成在不同地形上探索的机器人:



  • 悬崖边的窄桥(低自由度): 只有一条安全路径 → 提供详细护栏和精确指令

  • 丘陵地带(中等自由度): 几条推荐路径 → 提供地图和指南针

  • 无障碍的开阔草地(高自由度): 多条路径都能成功 → 给出大致方向,信任 Claude 找到最佳路线


5.3 渐进式披露模式


模式 1:高层指南 + 引用


结构:


SKILL.md (简要指南)
↓ 引用
[FORMS.md] [REFERENCE.md] [EXAMPLES.md]

示例:


# PDF Processing

## Quick Start

Use pdfplumber to extract text:
\`\`\`python
import pdfplumber
with pdfplumber.open("file.pdf") as pdf:
text = pdf.pages[0].extract_text()
\`\`\`

## Advanced Features

**Form Filling**: See [FORMS.md](FORMS.md) for complete guide
**API Reference**: See [REFERENCE.md](REFERENCE.md) for all methods
**Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns

优势:



  • Claude 只在需要时才读取 FORMS.md、REFERENCE.md 或 EXAMPLES.md

  • 未使用的文件 = 0 tokens 消耗


模式 2:按领域组织


适用场景: 多领域的 Skills,避免加载无关上下文


结构:


bigquery-skill/
├── SKILL.md # 概述和导航
└── reference/
├── finance.md # 财务指标
├── sales.md # 销售数据
├── product.md # 产品分析
└── marketing.md # 营销活动

示例:


# BigQuery Analytics

## Domain Reference

- **Finance Metrics**: See [reference/finance.md](reference/finance.md)
- **Sales Data**: See [reference/sales.md](reference/sales.md)
- **Product Analytics**: See [reference/product.md](reference/product.md)
- **Marketing Campaigns**: See [reference/marketing.md](reference/marketing.md)

优势:



  • 用户询问销售指标时,只读取 sales.md

  • finance.md 和其他文件保持在文件系统中,消耗 0 tokens


模式 3:条件细节


# DOCX Processing

## Create Documents

Use docx-js to create new documents. See [DOCX-JS.md](DOCX-JS.md).

## Edit Documents

For simple edits, modify XML directly.

**Track Changes**: See [REDLINING.md](REDLINING.md)
**OOXML Details**: See [OOXML.md](OOXML.md)

优势:



  • 常见操作(创建文档)在主文件中

  • 高级功能(追踪更改)按需引用


💡 重要: 保持引用层级为一级深度。避免 SKILL.md → advanced.md → details.md 这样的深层嵌套。


5.4 工作流和反馈循环


复杂任务的工作流模式


为多步骤任务提供清晰的检查清单:


## PDF Form Filling Workflow

Copy this checklist and track progress:

\`\`\`
Task Progress:
- [ ] Step 1: Analyze form (run analyze_form.py)
- [ ] Step 2: Create field mapping (edit fields.json)
- [ ] Step 3: Validate mapping (run validate_
fields.py)
- [ ] Step 4: Fill form (run fill_form.py)
- [ ] Step 5: Verify output (run verify_
output.py)
\`\`\`

**Step 1: Analyze Form**

Run: `python scripts/analyze_form.py input.pdf`

This extracts form fields and their locations, saving to `fields.json`.

**Step 2: Create Field Mapping**

Edit `fields.json` to add values for each field.

**Step 3: Validate Mapping**

Run: `python scripts/validate_fields.py fields.json`

Fix any validation errors before proceeding.

**Step 4: Fill Form**

Run: `python scripts/fill_form.py input.pdf fields.json output.pdf`

**Step 5: Verify Output**

Run: `python scripts/verify_output.py output.pdf`

If validation fails, return to Step 2.

实现反馈循环


常见模式: 运行验证器 → 修复错误 → 重复


这种模式极大提高输出质量。


示例:文档编辑流程


## Document Editing Flow

1. Make edits to `word/document.xml`
2. **Validate immediately**: `python ooxml/scripts/validate.py unpacked_dir/`
3. If validation fails:
- Review error messages carefully
- Fix issues in XML
- Run validation again
4. **Only proceed when validation passes**
5. Repack: `python ooxml/scripts/pack.py unpacked_dir/ output.docx`
6. Test output document

为什么反馈循环重要?



  • ✅ 及早发现错误(在应用更改前)

  • ✅ 机器可验证(脚本提供客观验证)

  • ✅ 可逆计划(Claude 可以迭代而不破坏原始文件)

  • ✅ 清晰调试(错误消息指向具体问题)


5.5 内容指南


避免时间敏感信息


糟糕示例(会过时):


如果你在 2025 年 8 月之前做这件事,使用旧 API。
2025 年 8 月之后,使用新 API。

好的示例(使用"旧模式"部分):


## Current Method

Use v2 API endpoint: `api.example.com/v2/messages`

## Legacy Patterns

<details>
<summary>Legacy v1 API (deprecated 2025-08)</summary>

v1 API uses: `api.example.com/v1/messages`

This endpoint is no longer supported.
</details>

使用一致的术语


在整个 Skill 中选择一个术语并坚持使用:


一致性好:



  • 始终使用 "API endpoint"

  • 始终使用 "field"

  • 始终使用 "extract"


不一致:



  • 混用 "API endpoint"、"URL"、"API route"、"path"

  • 混用 "field"、"box"、"element"、"control"

  • 混用 "extract"、"pull"、"get"、"retrieve"




六、高级技巧


6.1 包含可执行代码的 Skills


解决问题,而非推卸责任


编写 Skills 脚本时,显式处理错误情况,而非推卸给 Claude。


好的示例:显式处理错误


def process_file(path):
"""处理文件,如果不存在则创建。"""
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
# 创建默认内容而非失败
print(f"文件 {path} 未找到,创建默认文件")
with open(path, 'w') as f:
f.write('')
return ''
except PermissionError:
# 提供替代方案而非失败
print(f"无法访问 {path},使用默认值")
return ''

糟糕示例:推卸给 Claude


def process_file(path):
# 直接失败,让 Claude 自己想办法
return open(path).read()

提供工具脚本


即使 Claude 可以编写脚本,预制脚本也有优势:


工具脚本的好处:



  • 比生成代码更可靠

  • 节省 tokens(无需在上下文中包含代码)

  • 节省时间(无需代码生成)

  • 确保使用的一致性


09-02-skills-file-structure.png


示例:


## Tool Scripts

**analyze_form.py**: Extract all form fields from PDF

\`\`\`bash
python scripts/analyze_
form.py input.pdf > fields.json
\`\`\`

Output format:
\`\`\`json
{
"field_name": {"type": "text", "x": 100, "y": 200},
"signature": {"type": "sig", "x": 150, "y": 500}
}
\`\`\`

**validate_
boxes.py**
: Check for boundary box overlaps

\`\`\`bash
python scripts/validate_boxes.py fields.json
# Returns: "OK" or lists conflicts
\`\`\`

**fill_form.py**: Apply field values to PDF

\`\`\`bash
python scripts/fill_
form.py input.pdf fields.json output.pdf
\`\`\`

💡 重要区分: 在指令中明确说明 Claude 应该:



  • 执行脚本(最常见): "运行 analyze_form.py 以提取字段"

  • 读取作为参考(用于复杂逻辑): "参见 analyze_form.py 了解字段提取算法"


6.2 创建可验证的中间输出


当 Claude 执行复杂、开放式任务时,可能会出错。"计划-验证-执行"模式通过让 Claude 首先创建结构化格式的计划,然后在执行前用脚本验证该计划,从而及早发现错误。


示例场景


要求: Claude 根据电子表格更新 PDF 中的 50 个表单字段。


没有验证:Claude 可能:



  • ❌ 引用不存在的字段

  • ❌ 创建冲突的值

  • ❌ 遗漏必填字段

  • ❌ 错误应用更新


有验证:工作流变为:


分析 → 创建计划文件 → 验证计划 → 执行 → 验证输出

添加一个中间 changes.json 文件,在应用更改前进行验证。


实现示例


## Bulk Form Update Workflow

**Step 1: Analyze**
- Extract current form fields
- Save to `current_fields.json`

**Step 2: Create Change Plan**
- Based on spreadsheet, create `changes.json`:
\`\`\`json
{
"field_updates": [
{"field": "customer_name", "value": "John Doe"},
{"field": "order_total", "value": "1250.00"}
]
}
\`\`\`

**Step 3: Validate Plan**
- Run: `python scripts/validate_changes.py changes.json`
- Script checks:
- All referenced fields exist
- Values are in correct format
- No conflicts
- **Only proceed if validation passes**

**Step 4: Execute**
- Apply changes: `python scripts/apply_changes.py changes.json`

**Step 5: Verify Output**
- Run: `python scripts/verify_output.py output.pdf`

为什么此模式有效



  • 及早发现错误: 在应用更改前验证发现问题

  • 机器可验证: 脚本提供客观验证

  • 可逆计划: Claude 可以在不触及原始文件的情况下迭代计划

  • 清晰调试: 错误消息指向具体问题


使用时机



  • 批量操作

  • 破坏性更改

  • 复杂验证规则

  • 高风险操作


💡 实现技巧: 让验证脚本输出详细的错误消息:


❌ 模糊: "Validation failed"
✅ 清晰: "Field 'signature_date' not found. Available fields: customer_name, order_total, signature_date_signed"

这帮助 Claude 快速修复问题。


6.3 MCP 工具引用


如果你的 Skill 使用 MCP(Model Context Protocol)工具,始终使用完全限定的工具名称以避免"工具未找到"错误。


格式


ServerName:tool_name

示例


## Query Database Schema

Use the BigQuery:bigquery_schema tool to retrieve table schema.

\`\`\`
Use tool: BigQuery:bigquery_
schema
Parameters: {"table": "user_metrics"}
\`\`\`

## Create GitHub Issue

Use the GitHub:create_
issue tool to create an issue.

\`\`\`
Use tool: GitHub:create_issue
Parameters: {"title": "Bug report", "body": "Description"}
\`\`\`

说明



  • BigQueryGitHub 是 MCP 服务器名称

  • bigquery_schemacreate_issue 是这些服务器中的工具名称


没有服务器前缀,Claude 可能无法找到工具,特别是当有多个 MCP 服务器可用时。




七、常见反模式


❌ 反模式 1:Windows 风格路径


问题:使用反斜杠 \ 作为路径分隔符


错误:


参见 scripts\helper.py
参见 reference\guide.md

正确:


参见 scripts/helper.py
参见 reference/guide.md

原因:



  • Unix 风格路径跨所有平台工作

  • Windows 风格路径在 Unix 系统上会导致错误


❌ 反模式 2:提供太多选项


问题:列出所有可能的方法,让 Claude 困惑


错误:


你可以使用 pypdf,或 pdfplumber,或 PyMuPDF,或 pdf2image,
或 pikepdf,或 PyPDF2,或 pdfrw,或 pdfminer...

正确:


使用 pdfplumber 进行文本提取:
\`\`\`python
import pdfplumber
\`\`\`

对于需要 OCR 的扫描 PDF,改用 pdf2image 配合 pytesseract。

原则:



  • 提供默认推荐方法

  • 只在特殊情况下提供替代方案

  • 不要列出所有可能性


❌ 反模式 3:深层嵌套引用


问题:引用链太长,Claude 难以跟踪


错误:


SKILL.md → advanced.mddetails.md → examples.md

正确:


SKILL.md
↓ 直接引用
[ADVANCED.md] [DETAILS.md] [EXAMPLES.md]

原则:



  • 保持从 SKILL.md 的引用为一级深度

  • 所有引用文件应直接从 SKILL.md 链接


❌ 反模式 4:过度解释基础概念


问题:解释 Claude 已经知道的内容


错误:


PDF(Portable Document Format,便携式文档格式)是 Adobe 公司
开发的一种文件格式,可以在不同操作系统上保持一致的显示效果。
PDF 文件包含文本、图像、矢量图形等多种内容类型...

正确:


使用 pdfplumber 提取 PDF 文本。

原则:



  • 假设 Claude 的智能

  • 只提供 Claude 不知道的领域特定知识


❌ 反模式 5:第一人称描述


问题:使用"我"、"你"等人称


错误:


description: I can help you process Excel files and generate reports.

正确:


description: Process Excel files and generate reports. Use when working with spreadsheets or when the user mentions Excel, CSV, or data analysis.

原因:



  • description 被注入系统提示

  • 第一人称会导致视角冲突




八、总结与行动


8.1 核心收益


通过 Skills 系统,你可以:



  1. ⏱️ 节省时间: 不用每次重复说明领域知识

  2. ✅ 保证质量: 标准化流程,减少错误

  3. 📚 积累知识: 把最佳实践封装成 Skills,团队共享

  4. 🚀 提升专业性: 让 Claude 从通用助手进化为领域专家

  5. 🔧 持续优化: 基于使用反馈不断改进 Skills


8.2 Skills 与其他功能的关系


功能作用与 Skills 的关系
Agent处理复杂、多步骤任务Skills 为 Agent 提供领域知识
MCP连接外部工具和数据源Skills 可以引用 MCP 工具
claude.md项目级配置和规范Skills 是跨项目的能力扩展
Hook事件触发的自动化Hook 可以在特定时机加载 Skills

8.3 实践建议


对于个人开发者:



  1. 从一个简单的 Skill 开始(如代码审查清单)

  2. 识别自己反复解释的内容

  3. 逐步添加更多 Skills

  4. 持续优化基于实际使用


对于团队:



  1. 建立团队 Skills 仓库

  2. 统一 Skills 开发规范

  3. 定期分享优秀 Skills

  4. 建立 Skills 评审机制


对于技术 Leader:



  1. 推广 Skills 使用文化

  2. 组织 Skills 开发培训

  3. 激励团队贡献 Skills

  4. 建立 Skills 质量标准


8.4 未来展望


Skills 系统的发展方向:



  1. 可视化 Skill Builder: 通过图形界面创建 Skills

  2. Skill 市场: 官方 Skills 商店,一键安装分享

  3. AI 生成 Skills: 描述需求,AI 自动生成 Skills

  4. Skill 编排: 多个 Skills 组合成工作流

  5. 实时协作: 团队实时共享和更新 Skills



"把你反复向 Claude 解释的偏好、流程、领域知识打包成 Skills,让 AI 成为你的领域专家"





实用资源



🔗 相关文章:





如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题或建议,欢迎在评论区留言讨论。让我们一起学习,一起成长!


也欢迎访问我的个人主页发现更多宝藏资源


作者:冬奇Lab
来源:juejin.cn/post/7608382961723555890
收起阅读 »

OpenClaw安装

前置条件 环境 openclaw需要Node.js 22或者更高版本 笔者在CentOS 7虚拟机上使用源码包安装或者fnm安装Node.js会提示这样那样的错误; 源码包安装会有环境的问题:python、g++版本太老…… 用fnm安装后提示 node: /...
继续阅读 »

前置条件


环境


openclaw需要Node.js 22或者更高版本


笔者在CentOS 7虚拟机上使用源码包安装或者fnm安装Node.js会提示这样那样的错误;


源码包安装会有环境的问题:python、g++版本太老……


用fnm安装后提示


node: /lib64/libstdc++.so.6: version `CXXABI_1.3.11' not found (required by node) node: /lib64/libstdc++.so.6: version `CXXABI_1.3.8' not found (required by node) node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by node) node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by node) node: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by node) node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node) node: /lib64/libc.so.6: version `GLIBC_2.27' not found (required by node) node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node) node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by node)

所以在自己的云服务器上搭建了


node安装


nodejs github地址


nodejs 网站地址可查看LTS版本


file-20260227100922402.png
官方推荐这三种方式


nvm


官方给出的安装方式


# 下载并安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

#
代替重启 shell
\. "$HOME/.nvm/nvm.sh"

#
下载并安装 Node.js:
nvm install 24

#
验证 Node.js 版本:
node -v # Should print "v24.14.0".

#
验证 npm 版本:
npm -v # Should print "11.9.0".


但是这里会提示网络不可达,所以nvm我们采用离线的方式进行安装


nvm GitHub地址


解压


配置环境变量



  • vi ~/.bashrc


export NVM_DIR="/usr/local/nvm-0.40.4"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion

#
使其生效
source ~/.bashrc

验证


nvm -v
显示对应的版本号

查看可用的node版本


# 查看可安装的 node 版本
nvm ls-remote

安装对应的版本


# 安装指定版本的 node
nvm install 24.14.0

fnm


官方给出的安装方式


# 下载并安装 fnm:
curl -o- https://fnm.vercel.app/install | bash
# 下载并安装 Node.js:
fnm install 24
# 验证 Node.js 版本:
node -v # Should print "v24.14.0".
# 验证 npm 版本:npm -v # Should print "11.9.0".

但是这里会提示网络不可达,所以fnm我们采用离线的方式进行安装


fnmGitHub地址


下载后解压


添加环境变量



  • vi /etc/profile


# /usr/local/fnm-1.38.1为fnm解压后的存放位置并不是fnm文件的绝对路径,而是存放位置
export PATH=$PATH:/usr/local/fnm-1.38.1
# 使其生效
source /etc/profile


  • vi ~/.bashrc


eval "$(fnm env --use-on-cd --shell bash)"
# 使其生效
source ~/.bashrc

验证


fnm --version
返回对应的版本号

Docker


官方给出的安装方式


# Docker 对每个操作系统都有特定的安装指导。
# 请参考 https://docker.com/get-started/ 给出的官方文档

#
拉取 Node.js Docker 镜像:
docker pull node:24-alpine

#
创建 Node.js 容器并启动一个 Shell 会话:
docker run -it --rm --entrypoint sh node:24-alpine

#
验证 Node.js 版本:
node -v # Should print "v24.14.0".

#
验证 npm 版本:
npm -v # Should print "11.9.0".

安装docker

阿里云文档


非阿里云服务器,需将mirrors.cloud.aliyuncs.com替换为https://mirrors.al…


#添加Docker软件包源
sudo wget -O /etc/yum.repos.d/docker-ce.repo http://mirrors.cloud.aliyuncs.com/docker-ce/linux/centos/docker-ce.repo

# 【非阿里云服务区不需要执行这一步】执行后,该文件中的所有https://mirrors.aliyun.com都会被替换为http://mirrors.cloud.aliyuncs.com
sudo sed -i 's|https://mirrors.aliyun.com|http://mirrors.cloud.aliyuncs.com|g' /etc/yum.repos.d/docker-ce.repo

#安装Docker社区版本,容器运行时containerd.io,以及Docker构建和Compose插件
sudo yum -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

配置阿里云加速镜像

sudo mkdir -p /etc/docker 
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://xxxx.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

阿里云docker加速器地址


直接拉取镜像由于网络的问题可能导致镜像拉取失败


下载好镜像上传到服务器进行加载


docker load -i node_24-alpine.tar

运行


# 创建 Node.js 容器并启动一个 Shell 会话:
docker run -it --rm --entrypoint sh node:24-alpine


  1. docker run:

    • 这是创建并启动一个新容器的命令。



  2. -it:

    • 这是两个参数的组合:

      • -i (interactive): 保持标准输入(STDIN)打开,即使没有附加到容器上。这让你能输入命令。

      • -t (tty): 分配一个伪终端(pseudo-TTY)。这让你拥有一个类似真实终端的交互界面(支持颜色、自动补全等)。



    • 作用:合起来就是让你能交互式地在容器里敲命令。



  3. --rm:

    • 自动清理。当容器停止运行(退出)时,Docker 会自动删除这个容器实例及其写入层。

    • 作用:防止产生大量“僵尸”容器占用磁盘空间,非常适合临时调试或测试。



  4. --entrypoint sh:

    • 覆盖入口点

    • 通常,node:24-alpine 镜像的默认入口点(Entrypoint)是 node 命令。如果你直接运行 docker run node:24-alpine,它会尝试运行 node 但没有脚本文件,通常会报错或直接退出。

    • 这里指定为 sh (Shell),告诉 Docker:“别运行默认的 node 程序,而是运行 sh (Shell)”。

    • 作用:让你进入容器的命令行环境,以便检查文件系统、安装临时包、测试网络或调试环境问题。



  5. node:24-alpine:

    • 这是使用的镜像名称

    • node: 官方 Node.js 镜像。

    • 24: 指定 Node.js 的版本为 v24(注意:截至2026年2月,Node.js 24 应该是当前的最新稳定版或新版)。

    • alpine: 基于 Alpine Linux 构建。这是一个非常轻量级的 Linux 发行版,因此生成的镜像体积很小(通常只有几十 MB),但里面预装的工具较少(比如没有 bash,只有 sh,没有 curl 或 wget 除非手动安装)。
      验证




# 验证 Node.js 版本:
node -v # Should print "v24.14.0".
# 验证 npm 版本:
npm -v # Should print "11.9.0".

源码包安装


v24.14.0 LTS GitHub地址


下载后解压


tar -zxaf node-24.14.0.tar.gz
cd node-24.14.0
# 检查依赖并生成 Makefile(可添加自定义参数,如 --prefix=/usr/local/node)
./configure --prefix=/usr/local/node
# 编译(-j4 表示使用 4 核 CPU 加速,根据实际核心数调整)
make -j4
# 安装到系统目录(默认 /usr/local/bin)
make install

问题



  • python


./configure --prefix=/usr/local/node/ --python=/usr/local/python_3.14.3/bin/python3.14 Node.js configure: Found Python 3.6.8... Please use python3.14 or python3.13 or python3.12 or python3.11 or python3.10 or python3.9.


  • bz2


[root@localhost node-24.14.0]# ./configure --prefix=/usr/local/node Node.js configure: Found Python 3.14.3... Traceback (most recent call last): File "/usr/local/node-24.14.0/./configure", line 28, in <module> import configure File "/usr/local/node-24.14.0/configure.py", line 13, in <module> import bz2 File "/usr/local/python_3.14.3/lib/python3.14/bz2.py", line 17, in <module> from _bz2 import BZ2Compressor, BZ2Decompressor ModuleNotFoundError: No module named '_bz2'


  • g++


Node.js configure: Found Python 3.14.3... WARNING: C++ compiler (CXX=g++, 4.8.5) too old, need g++ 12.2.0 or clang++ 8.0.0 WARNING: warnings were emitted in the configure phase INFO: configure completed successfully

个人不太推荐


npm 包管理器安装


npm i -g openclaw

安装完成后


openclaw onboard

访问Dashboard


由于程序运行在linux服务器上没有GUI界面所以想要可视化访问需要做一系列的操作


运行 SSH 隧道命令


ssh -N -L 18789:127.0.0.1:18789 root@xx.x.x.xxx

file-20260227100922400.png


光标在下方就表示隧道建立了


浏览器访问


http://localhost:18789 直接访问会报身份认证失败
http://localhost:18789/#token=c74d1xxxxdccaxxx8bdcxxx 回车即可进入可视化控制台
file-20260227100922401.png


一键安装


官网安装教程这里不再赘述


源码编译


官网安装教程这里不再赘述


Docker


官网安装教程这里不再赘述


常用命令



  • 查看版本


openclaw --version


  • 交互式设置向导


openclaw onboard [--install-daemon]


  • 启动网关


openclaw gateway [--port 18789]


  • 重启网关


openclaw gateway restart


  • 健康检查与快速修复


openclaw doctor [--deep]


  • 交互式配置


openclaw configure

链接


官网安装


OpenClaw 官方文档


作者:透明人_x
来源:juejin.cn/post/7610981820638691368
收起阅读 »

说说 HTTP 和 RPC 的区别是什么?

说说 HTTP 和 RPC 的区别是什么? 2026年02月02日 面试考察点 面试官提出这个问题,主要想考察以下几个层面: 对通信方式本质的理解:不仅仅是背诵概念,而是能否清晰地说出 HTTP 和 RPC 在通信模型、协议栈上的根本性差异。 序列化与性能的...
继续阅读 »

说说 HTTP 和 RPC 的区别是什么?


2026年02月02日


面试考察点


面试官提出这个问题,主要想考察以下几个层面:



  1. 对通信方式本质的理解:不仅仅是背诵概念,而是能否清晰地说出 HTTP 和 RPC 在通信模型、协议栈上的根本性差异。

  2. 序列化与性能的权衡:是否了解它们背后不同的序列化方式(如 JSON/XML vs Protobuf/Hessian)及其对性能、体积和开发效率的影响。

  3. 设计哲学与适用场景:能否理解它们不同的设计目标(通用 Web 标准 vs 高效内部服务通信),并据此分析各自的适用场景。

  4. 架构视野:在微服务或分布式系统架构的背景下,能否结合实际,阐述技术选型的思考,体现将理论知识应用于工程实践的能力。


核心答案


HTTP 和 RPC 的核心区别在于:HTTP 是一个通用的、无状态的、应用层的网络协议标准,而 RPC 是一种旨在实现像调用本地方法一样调用远程服务的框架或设计模式


更直接地说:



  • HTTP 是一种协议,定义了客户端与服务器之间通信的通用格式和规则(如 URL、Method、Header、Body),其设计初衷是为了万维网(Web)的超文本传输,现已广泛用于构建 RESTful API。

  • RPC 是一种概念/框架,其核心目标是让开发者无感知地调用远程服务。为了实现这个目标,一个完整的 RPC 框架通常会自定义或封装底层通信协议(可能基于 TCP,也可能基于 HTTP) ,并集成高效的二进制序列化、服务发现、负载均衡、熔断降级等分布式服务治理能力。


简言之,你可以  “用 HTTP 协议来实现一种 RPC” (如 gRPC over HTTP/2),但并非所有 RPC 都必须使用 HTTP 协议。


深度解析


原理/机制



  • HTTP:基于经典的 请求-响应 (Request-Response)  模型。通常使用文本格式(如 JSON/XML)序列化数据,协议头(Header)庞大且冗余(如 Cookie、Cache-Control 等 Web 特性字段),但其无状态和标准化的特点使其非常适合跨网络、跨语言的开放 API 场景。

  • RPC:目标是实现  “透明远程过程调用” 。一个完整的 RPC 调用过程包括:



    1. 客户端代理(Stub)  将方法名和参数序列化;

    2. 通过网络传输到服务器;

    3. 服务端骨架(Skeleton)  反序列化并调用实际方法;

    4. 将结果序列化返回。其底层通信协议通常追求更高的性能和紧凑性,例如使用自定义的二进制协议。




对比分析


维度HTTP (以 RESTful API 为例)RPC (以典型框架如 Dubbo, gRPC 为例)
通信协议主要基于应用层的 HTTP/1.1 或 HTTP/2 协议。通常基于传输层的 TCP 自定义二进制协议,或基于 HTTP/2 (如 gRPC)。
序列化通常使用人类可读的 JSON、XML 等文本格式。序列化/反序列化开销较大。通常使用高效的二进制格式,如 Protobuf、Hessian、Kryo。体积小,速度快。
性能协议头较大,序列化效率较低,性能开销相对较高。HTTP/2 通过多路复用等特性大幅改善了性能。专为高效内部通信设计,协议精简,序列化高效,性能通常优于 HTTP/1.1
连接与交互传统的 HTTP/1.1 是 “一问一答”,多个请求需要多个连接或串行。HTTP/2 支持连接复用和流。通常支持连接复用、异步调用和流式处理,交互模式更灵活高效。
服务治理需要额外集成组件(如客户端负载均衡器 Ribbon、服务发现 Eureka)来实现完整的治理。框架原生集成了服务发现、负载均衡、熔断、限流等治理能力,开箱即用。
适用场景对外的开放 API、需要被多种异构客户端(浏览器、移动端、第三方)调用的服务、简单快速的微服务原型。大规模的内部微服务集群、对性能有极高要求的系统、需要复杂服务治理的分布式系统。

代码示例


一个简单的感受:调用一个 “获取用户信息” 的服务。


// 使用 HTTP (RestTemplate) 调用
// 开发者需要关注 URL、HTTP 方法、请求体/参数的组装
User user = restTemplate.getForObject("http://user-service/users/123", User.class);

// 使用 RPC (以 Dubbo 接口为例)
// 开发者像调用本地接口一样直接调用,框架隐藏了所有网络细节
@Reference
private UserService userService; // 远程服务的本地代理

public User getUser() {
return userService.getUserById(123L); // 看起来和本地调用无异
}

最佳实践与常见误区



  • 最佳实践



    • 内外有别:对公网暴露的 API 优先使用 HTTP (RESTful) ,因其标准、通用、易于调试(用 curl 或浏览器即可)、防火墙友好。内部服务间调用,尤其是性能敏感、调用链路长的场景,优先考虑 RPC 以获得更好的性能和治理能力。

    • 不唯技术论:技术选型需权衡团队技术栈、维护成本、生态集成度。Spring Cloud 生态的 OpenFeign(基于 HTTP)在中小规模下,凭借其与 Spring 的无缝集成,开发体验和效率可能优于引入一套独立的 RPC 框架。



  • 常见误区



    1. 误区一:HTTP 和 RPC 是完全对立的。实际上,gRPC 就是一个完美的反例,它既是强大的 RPC 框架,又使用 HTTP/2 作为传输协议,结合了二者的优势。

    2. 误区二:HTTP 性能一定差HTTP/2 在性能上有了质的飞跃(头部压缩、多路复用、服务端推送),使其在不少场景下足以替代传统的 RPC 协议。

    3. 误区三:RPC 一定比 HTTP 复杂。对于调用方开发者而言,RPC 的接口式编程模型反而更简单直观。复杂性主要转移到了框架的部署和维护上。




总结


HTTP 是通用网络协议,适合构建开放、标准化的 Web API;而 RPC 是远程调用框架模式,旨在为内部服务提供高效、透明、治理完善的调用体验。在现代架构中,二者边界正在模糊(如 gRPC),关键在于根据  “场景”(内外网、性能要求)  和  “生态”(团队、基础设施)  做出最合适的选择。


作者:Vin_evail
来源:juejin.cn/post/7601444617695543306
收起阅读 »

字节2面:为了性能,你会违反数据库三范式吗?

大家好,我是猿java。 数据库的三大范式,它是数据库设计中最基本的三个规范,那么,三大范式是什么?在实际开发中,我们一定要严格遵守三大范式吗?这篇文章,我们一起来聊一聊。 1. 三大范式 1. 第一范式(1NF,确保每列保持原子性) 第一范式要求数据库中的每...
继续阅读 »

大家好,我是猿java


数据库的三大范式,它是数据库设计中最基本的三个规范,那么,三大范式是什么?在实际开发中,我们一定要严格遵守三大范式吗?这篇文章,我们一起来聊一聊。


1. 三大范式


1. 第一范式(1NF,确保每列保持原子性)


第一范式要求数据库中的每个表格的每个字段(列)都具有原子性,即字段中的值不可再分割。换句话说,每个字段只能存储一个单一的值,不能包含集合、数组或重复的组。


如下示例: 假设有一个学生表 Student,结构如下:


学生ID姓名电话号码
1张三123456789, 987654321
2李四555555555

在这个表中,电话号码字段包含多个号码,违反了1NF的原子性要求。为了满足1NF,需要将电话号码拆分为单独的记录或创建一个新的表。


满足 1NF后的设计:


学生表 Student


学生ID姓名
1张三
2李四

电话表 Phone


电话ID学生ID电话号码
11123456789
21987654321
32555555555

1.2 第二范式(2NF,确保表中的每列都和主键相关)


第二范式要求满足第一范式,并且消除表中的部分依赖,即非主键字段必须完全依赖于主键,而不是仅依赖于主键的一部分。这主要适用于复合主键的情况。


如下示例:假设有一个订单详情表 OrderDetail,结构如下:


订单ID商品ID商品名称数量单价
1001A01苹果102.5
1001A02橙子53.0
1002A01苹果72.5

在上述表中,主键是复合主键 (订单ID, 商品ID)商品名称单价只依赖于复合主键中的商品ID,而不是整个主键,存在部分依赖,违反了2NF。


满足 2NF后的设计:


订单详情表 OrderDetail


订单ID商品ID数量
1001A0110
1001A025
1002A017

商品表 Product


商品ID商品名称单价
A01苹果2.5
A02橙子3.0

1.3 第三范式(3NF,确保每列都和主键列直接相关,而不是间接相关)


第三范式要求满足第二范式,并且消除表中的传递依赖,即非主键字段不应依赖于其他非主键字段。换句话说,所有非主键字段必须直接依赖于主键,而不是通过其他非主键字段间接依赖。


如下示例:假设有一个员工表 Employee,结构如下:


员工ID员工姓名部门ID部门名称
E01王五D01销售部
E02赵六D02技术部
E03孙七D01销售部

在这个表中,部门名称依赖于部门ID,而部门ID依赖于主键员工ID,形成了传递依赖,违反了3NF。


满足3NF后的设计:


员工表 Employee


员工ID员工姓名部门ID
E01王五D01
E02赵六D02
E03孙七D01

部门表 Department


部门ID部门名称
D01销售部
D02技术部

通过将部门信息移到单独的表中,消除了传递依赖,使得数据库结构符合第三范式。


最后,我们总结一下数据库设计的三大范式:



  • 第一范式(1NF): 确保每个字段的值都是原子性的,不可再分。

  • 第二范式(2NF): 在满足 1NF的基础上,消除部分依赖,确保非主键字段完全依赖于主键。

  • 第三范式(3NF): 在满足 2NF的基础上,消除传递依赖,确保非主键字段直接依赖于主键。


2. 破坏三范式


在实际工作中,尽管遵循数据库的三大范式(1NF、2NF、3NF)有助于提高数据的一致性和减少冗余,但在某些情况下,为了满足性能、简化设计或特定业务需求,我们可能需要违反这些范式。


下面列举了一些常见的破坏三范式的原因及对应的示例。


2.1 性能优化


在高并发、大数据量的应用场景中,严格遵循三范式可能导致频繁的联表查询,增加查询时间和系统负载。为了提高查询性能,设计者可能会通过冗余数据来减少联表操作。


假设有一个电商系统,包含订单表 Orders 和用户表 Users。在严格 3NF设计中,订单表只存储 用户ID,需要通过联表查询获取用户的详细信息。


但是,为了查询性能,我们通常会在订单表中冗余存储 用户姓名用户地址等信息,因此,查询订单信息时无需联表查询 Users 表,从而提升查询速度。


破坏 3NF后的设计:


订单ID用户ID用户姓名用户地址订单日期总金额
1001U01张三北京市2023-10-01500元
1002U02李四上海市2023-10-02300元

2.2 简化查询和开发


严格规范化可能导致数据库结构过于复杂,增加开发和维护的难度,为了简化查询逻辑和减少开发复杂度,我们也可能会选择适当的冗余。


比如,在内容管理系统(CMS)中,文章表 Articles 和分类表 Categories 通常是独立的,如果频繁需要显示文章所属的分类名称,联表查询可能增加复杂性。因此,通过在 Articles 表中直接存储 分类名称,可以简化前端展示逻辑,减少开发工作量。


破坏 3NF后的设计:


文章ID标题内容分类ID分类名称
A01文章一C01技术
A02文章二C02生活

2.3 报表和数据仓库


在数据仓库和报表系统中,通常需要快速读取和聚合大量数据。为了优化查询性能和数据分析,可能会采用冗余的数据结构,甚至使用星型或雪花型模式,这些模式并不完全符合三范式。


在销售数据仓库中,为了快速生成销售报表,可能会创建一个包含维度信息的事实表。


破坏 3NF后的设计:


销售ID产品ID产品名称类别销售数量销售金额销售日期
S01P01手机电子10050000元2023-10-01
S02P02书籍教育20020000元2023-10-02

在事实表中直接存储 产品名称类别,避免了需要联表查询维度表,提高了报表生成的效率。


2.4 特殊业务需求


在某些业务场景下,可能需要快速响应特定的查询或操作,这时通过适当的冗余设计可以满足业务需求。


比如,在实时交易系统中,为了快速计算用户的账户余额,可能会在用户表中直接存储当前余额,而不是每次交易时都计算。


破坏 3NF后的设计:


用户ID用户名当前余额
U01王五10000元
U02赵六5000元

在交易记录表中存储每笔交易的增减,但直接在用户表中维护 当前余额,避免了每次查询时的复杂计算。


2.5 兼顾读写性能


在某些应用中,读操作远多于写操作。为了优化读性能,可能会通过数据冗余来提升查询速度,而接受在数据写入时需要额外的维护工作。


社交媒体平台中,用户的好友数常被展示在用户主页上。如果每次请求都计算好友数量,效率低下。可以在用户表中维护一个 好友数 字段。


破坏3NF后的设计:


用户ID用户名好友数
U01Alice150
U02Bob200

通过在 Users 表中冗余存储 好友数,可以快速展示,无需实时计算。


2.6 快速迭代和灵活性


在快速发展的产品或初创企业中,数据库设计可能需要频繁调整。过度规范化可能导致设计不够灵活,影响迭代速度。适当的冗余设计可以提高开发的灵活性和速度。


一个初创电商平台在初期快速上线,数据库设计时为了简化开发,可能会将用户的收货地址直接存储在订单表中,而不是单独创建地址表。


破坏3NF后的设计:


订单ID用户ID用户名收货地址订单日期总金额
O1001U01李雷北京市海淀区…2023-10-01800元
O1002U02韩梅梅上海市浦东新区…2023-10-021200元

这样设计可以快速上线,后续根据需求再进行规范化和优化。


2.7 降低复杂性和提高可理解性


有时,过度规范化可能使数据库结构变得复杂,难以理解和维护。适度的冗余可以降低设计的复杂性,提高团队对数据库结构的理解和沟通效率。


在一个学校管理系统中,如果将学生的班级信息独立为多个表,可能增加理解难度。为了简化设计,可以在学生表中直接存储班级名称。


破坏3NF后的设计:


学生ID姓名班级ID班级名称班主任
S01张三C01三年级一班李老师
S02李四C02三年级二班王老师

通过在学生表中直接存储 班级名称班主任,减少了表的数量,简化了设计。


3. 总结


本文,我们分析了数据库的三范式以及对应的示例,它是数据库设计的基本规范。但是,在实际工作中,为了满足性能、简化设计、快速迭代或特定业务需求,我们很多时候并不会严格地遵守三范式。


所以说,架构很多时候都是业务需求、数据一致性、系统性能、开发效率等各种因素权衡的结果,我们需要根据具体应用场景做出合理的设计选择。


4. 学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7455635421529145359
收起阅读 »

最低成本使用最强模型编程方案

模型是:Codex+Gemini+GLM5+Kimik2.5+MiniMax2.5 先说明一下:这篇只分享我自己长期在用的公开方案,不是广告,也不是“理论推荐”。 核心标准就三个:能稳定用、能真落地、成本尽量低。 另外我现在的原则是:尽量走官方渠道,不再折腾反...
继续阅读 »

模型是:Codex+Gemini+GLM5+Kimik2.5+MiniMax2.5


先说明一下:这篇只分享我自己长期在用的公开方案,不是广告,也不是“理论推荐”。

核心标准就三个:能稳定用、能真落地、成本尽量低


另外我现在的原则是:尽量走官方渠道,不再折腾反代。账号和接口都建议按平台规则来。


先说结论:我现在的主力分工



  1. Codex:后端和大部分功能实现主力

  2. Gemini + Antigravity:前端和浏览器侧任务主力

  3. 国产免费模型:查资料、国内信息检索、API 调试补位

  4. 其他工具(如 Cloud Code Opus 4.6):额度见底时兜底


1)第一名:Codex(免费账号可用,但额度偏低)


这个“第一名”我给得很直接:写代码时最稳,尤其后端和复杂功能


免费账号能用它的模型,但额度确实不高。我的观察是,不同账号之间免费额度会有差异:



  • 早期注册的账号,额度通常更高一点

  • Google 邮箱注册的账号,额度通常也会稍高

  • 其他邮箱注册的部分账号,额度会明显低一些


所以我会准备多个免费账号轮换,但整体还是优先把主链路放在 Codex 上。


2)第二名:Gemini + Antigravity(我现在的前端主力)


Google 这套我最近重新用了起来。之前被封的一批账号后来都解封了,我自己之前的 5 个号也都恢复了。


但现在我不准备再用反代,直接走官方工具 Antigravity。我看中它的点主要是:



  • 对 Chrome 控制比较顺手

  • 内部集成了 Playwright MCP

  • Gemini 在前端页面和交互生成上表现不错


所以我现在是:Gemini 负责前端,Codex 负责后端和大多数功能实现


3)Cloud Code Opus 4.6:有,但不是常用位


Antigravity 里还有 Cloud Code Opus 4.6,我基本只在别的模型额度打满时才会切过去。


原因很简单:给到它的免费用量比较少,放在主流程里不太稳,适合作为兜底。


4)国内白嫖方案:白山智算 + zo computer


这两个是我现在固定会留着的国内补位方案。


白山智算(API 平台)


这个活动期挺给力:实名 + 首次调用后,能拿到 450 左右额度,日常够用。

里面我主要关注的是 GLM-5MiniMax-2.5


它本身走的是 OpenAI 兼容路由,所以能比较方便接进各种编程工具里,改个接口配置就能跑。


我之前推荐过了,注册链接:白山智算


image.png


zo computer(在线使用)


这个相对小众一点,但很省心:注册账号就能用内置免费模型,包含 GLM-5Kimi-2.5MiniMax-2.5


限制也很明确:更适合在线对话和临时任务,不太适合直接塞进 IDE 作为长期开发链路。


image.png


官网:zo.computer/


OpenCode:我现在已经没咋用了


以前有 Kimi 相关模型时我还会用一下。现在免费模型能力和覆盖变化后,我基本不再用它做主力。


再加上我有更顺手的 Codex + Gemini 组合,就没有必要在后面的方案上投入太多时间了。


我自己的使用策略(很实用)



  1. 主开发链路固定:Codex + Gemini

  2. 国内模型主要用于:资料检索、中文语境信息、API 测试

  3. 所有“免费”方案都按兜底思路准备,不把单一渠道当唯一依赖

  4. 不追求模型数量,追求“每天都能稳定出活”


最后


以上 4 个就是我现在公开在用的白嫖 AI 方案。

如果你有更稳、更省钱、还能长期跑的组合,欢迎留言交流。


作者:小兵张健
来源:juejin.cn/post/7611732636969615375
收起阅读 »

谁是OpenClaw?这个一夜爆火的“AI打工人”,正在悄悄接管你的电脑!

仅仅几个月狂揽 22 万 Star,经历三次改名(Clawdbot -> Moltbot -> OpenClaw)依然热度不减。它不是简单的聊天机器人,而是真正运行在你本地、接管你所有通讯软件(WhatsApp, Telegram, 飞书...)的...
继续阅读 »

仅仅几个月狂揽 22 万 Star,经历三次改名(Clawdbot -> Moltbot -> OpenClaw)依然热度不减。它不是简单的聊天机器人,而是真正运行在你本地、接管你所有通讯软件(WhatsApp, Telegram, 飞书...)的超级私人管家。本文手把手教你部署这只“全能龙虾”!


Github:github.com/openclaw/op…



image.png


最近,GitHub 上有一个项目杀疯了。


如果你关注 AI 圈,一定被一只红色的龙虾(Lobster) 刷屏过。没错,就是 OpenClaw


截至目前(2026 年 2 月),OpenClaw 在 GitHub 上的 Star 数已经突破了 228k!这是什么概念?即使是当年的爆款项目,也没有如此夸张的增长速度。


它到底是什么?为什么连退隐多年的技术圈大佬 Peter Steinberger(PSPDFKit 创始人)和 Mario Zechner(libGDX 之父)都要复出亲自操刀?


今天,我们就来扒一扒这个“当红炸子鸡”,并手把手教你把它装进电脑里。


什么是 OpenClaw?


简单来说,OpenClaw 是一个运行在你本地设备上的“个人 AI 代理(Agent)中枢”


别把它和 ChatGPT 网页版搞混了。OpenClaw 的核心逻辑是: “流水的模型,铁的管家”



  • 它没有 App(或者说不需要 App): 它直接寄生在你最常用的聊天软件里——WhatsApp、飞书、Slack、Discord、Signal,甚至是 iMessage。

  • 它极度聪明: 它不仅能陪聊,还能执行任务。它内置了强大的工具链,可以浏览网页、操作日历、根据你的指令写代码、甚至通过 ElevenLabs 实现语音对话。

  • 它绝对隐私: 所有的记忆、配置、对话历史都存储在你本地。你可以自由切换底层模型(Claude Opus, GPT-4, Llama 等),但“管家”本身是你的。


大家之所以疯狂追捧它,是因为它终于实现了我们对“贾维斯”的幻想:一个永远在线、了解你一切背景、并且听命于你的私人助理。


核心特点:为什么它能“爆”?



  1. 全渠道打通(Omnichannel):


    你不需要打开特定的 AI App。在 飞书 给它发消息:“帮我查下明天的天气并把会议发邮件给老板”,它就去做了。它就像是你通讯录里的一个全能好友。


  2. 本地优先与“记忆”机制:


    OpenClaw 拥有基于 Markdown 和 SQLite 的本地记忆系统。它记得你上周说想买的耳机,也记得你老板的忌讳。这些数据不会被上传到云端大厂的服务器。


  3. 强大的 Agent 能力:


    它不只是生成文本,它是来干活的。



    • Browsing: 给它一个 URL,它能读完并总结。

    • Coding: 它能在本地环境写代码并执行(Sandboxing 机制保障安全)。

    • Vision: 它可以“看”屏幕,或者通过摄像头看到你展示的东西。



  4. 大佬背书与“更名梗”:


    OpenClaw 的前身叫 Clawdbot,后来因为名字太像 Claude 被 Anthropic 警告,改名 Moltbot,最后定名 OpenClaw。每一次改名都伴随着一波热度,加上两位“退休”大佬为了它重出江湖,代码质量极高,被誉为“工程学的艺术品”。



如何安装与使用


好消息是,经过几个版本的迭代,现在安装 OpenClaw 已经非常简单了(推荐使用 MacOS 或 Linux/WSL2)。


1. 环境准备


你需要安装 Node.js(版本 ≥ 22)。


2. 一键安装


打开终端(Terminal),输入以下命令:


npm install -g openclaw@latest
# pnpm add -g openclaw@latest

3. 启动向导(Onboarding)


OpenClaw 最人性化的地方就在于它的交互式配置向导。在终端输入:


openclaw onboard --install-daemon

接下来,你只需要像填问卷一样:



  1. 选择模型服务商: 输入你的 Anthropic Key 或 授权 Qwen 服务。

  2. 设置连接渠道: 选择你想在哪里使用它(比如 飞书)。向导会提示你下载 openclaw/feishu ,下载后按照提示的信息进行配置(比如 登入 open.feishu.cn 平台创建 APP)。

  3. 安装技能: 选择是否开启浏览器控制、文件系统访问等权限。

  4. 按照提示配置 Gateway 自启动,打开 web 地址 127.0.0.1:18789,能够正常打开网页说明 OpenClaw 本地安装完成了,对于飞书端的其他配置可以参考文档参考视频


image.png


4. 开始使用


现在,拿起你的手机,打开你配置的渠道(例如:飞书),给你刚刚绑定的 Bot 发送一句:


你会发现,一个新的世界大门打开了。


7c4aed22c8e1eae2375ce2f62455b857.png


作者:GetcharZp
来源:juejin.cn/post/7610580125619093530
收起阅读 »

写了 10 年 MyBatis,一直以为“去 XML”=写注解,直到看到了这个项目

一直对 MyBatis 有个刻板印象:Mapper 接口负责声明方法,Mapper.xml 负责写 SQL。 改条件就去 XML 里 <if test="">,调参数就切换不同文件,从刚开始学到现在用了很久,熟悉得不能再熟悉。 直到最近看到一个项目...
继续阅读 »


一直对 MyBatis 有个刻板印象:Mapper 接口负责声明方法,Mapper.xml 负责写 SQL


改条件就去 XML 里 <if test="">,调参数就切换不同文件,从刚开始学到现在用了很久,熟悉得不能再熟悉。


直到最近看到一个项目:我把 resources/mapper 翻了个底朝天,愣是没找到一份 XML。


我第一反应:



“这项目肯定是全用 @Select 之类的注解硬写 SQL 了吧。”



结果打开 Mapper:我人傻了。


1)去 XML:@Select


单表按主键查,注解很舒服:


@Select("select * from tb_user where id = #{id}")UserDO selectById(Long id);

但一旦要动态条件,很多人会写成这种:


@Select("" +        "select * from tb_user " +        "" +        "   and name like concat('%', #{name}, '%') " +        "   and age >= #{age} " +        "" +        "")List list(UserQuery req);

这么些的体验基本是:



  • • 字符串拼接看得眼疼

  • • 没有 SQL 高亮、格式化也很难受

  • • 复杂一点直接维护灾难


所以绝大多数团队最终都会回到 XML——至少 XML 里写动态 SQL 还能接受。


2)去 XML:@SelectProvider


这个项目里 Mapper 长这样:


@SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")List selectByCondition(UserQuery req);

我当时心里一句话:



“Provider?这是什么东东?”



点进 UserSqlProvider,看到的是这种代码:


public class UserSqlProvider {  public String selectByCondition(UserQuery req) {    return new SQL() {{      SELECT("id, name, age, status, create_time");      FROM("tb_user");      if (req.getName() != null && !req.getName().isBlank()) {        WHERE("name like concat('%', #{name}, '%')");      }      if (req.getMinAge() != null) {        WHERE("age >= #{minAge}");      }      if (req.getStatus() != null) {        WHERE("status = #{status}");      }      ORDER_BY("create_time desc");    }}.toString();  }}

当时我有点惊讶:
SQL()SELECT()WHERE() 这些不是自定义工具类,而是 MyBatis 自带的 SQL Builder


这类写法的本质是:



  • XML 动态 SQL 的能力不变

  • • 但“拼 SQL 的载体”从 XML 变成 Java Provider 方法

  • • 最终 MyBatis 仍然执行一段 SQL 字符串(只是这段字符串由 builder 组装出来)



3)Provider


解决了什么?


动态条件 + 可读性


不用在注解字符串里写 <script>、不用手动拼 AND、也不用在 Java/XML 之间跳来跳去。


再比如:动态排序字段(注意做白名单防注入)


public String list(UserQuery req) {  return new SQL() {{    SELECT("*");    FROM("tb_user");    if (req.getName() != null && !req.getName().isBlank()) {      WHERE("name like concat('%', #{name}, '%')");    }    // 排序字段做白名单,避免 order by 注入    if ("create_time".equals(req.getOrderBy())) {      ORDER_BY("create_time desc");    } else if ("age".equals(req.getOrderBy())) {      ORDER_BY("age desc");    } else {      ORDER_BY("id desc");    }  }}.toString();}

未能解决


复杂 SQL 的“表达力”问题


子查询、复杂 join、窗口函数、CTE……你用 builder 也能写,但写着写着就会变成“在 Java 里造 SQL AST”,维护成本可能并不比 XML 低。


我的建议:



  • 中等复杂度动态查询:Provider 很合适

  • 复杂报表 / 多层嵌套:直接写原生 SQL(放 XML 或统一的 SQL 文件)更直观



4)Provider 最容易踩的坑:参数绑定(90% 的报错在这)


4.1 单参数对象:最舒服


List list(UserQuery req);

Provider 里直接 #{name}#{minAge},对应 req 的属性名即可。


4.2 多参数一定要 @Param,不然会看到奇怪的参数名


@SelectProvider(type = UserSqlProvider.class, method = "get")UserDO get(@Param("id") Long id, @Param("status") Integer status);

Provider 可以收 Map


public String get(Map p) {  return new SQL() {{    SELECT("*");    FROM("tb_user");    WHERE("id = #{id}");    if (p.get("status") != null) {      WHERE("status = #{status}");    }  }}.toString();}

如果不写 @Param,参数名可能变成 param1/param2arg0/arg1,然后你就开始“有bug,明明传了值怎么为空”。


5)再懒一下:MyBatis-Plus


如果主要场景是单表 CRUD + 条件筛选,MyBatis-Plus 的思路是:尽量别写 SQL,让 Wrapper 来表达条件。


LambdaQueryWrapper w = Wrappers.lambdaQuery();w.like(StringUtils.isNotBlank(req.getName()), UserDO::getName, req.getName()) .ge(req.getMinAge() != null, UserDO::getAge, req.getMinAge()) .eq(req.getStatus() != null, UserDO::getStatus, req.getStatus()); List list = userMapper.selectList(w);

这套东西的价值很明确:



  • • 字段引用是方法引用,改字段/重构更安全

  • • 大量单表查询不需要写 SQL

  • • 团队统一风格之后,开发效率很高


但边界也很明确:复杂 SQL 仍然要回到原生 SQL(XML/Provider/自定义 mapper 都行),Wrapper 不适合硬扛报表类需求。


6)组装 SQL:MyBatis-Flex


如果连 join 都不想写 SQL,更希望用 Java 结构来表达,MyBatis-Flex 这类框架会提供更强的 QueryWrapper/Join 能力。


简单 join 确实很直观:


QueryWrapper q = QueryWrapper.create()    .select(ACCOUNT.ID, ACCOUNT.USER_NAME, ROLE.ROLE_NAME)    .from(ACCOUNT)    .leftJoin(ROLE).on(ACCOUNT.ROLE_ID.eq(ROLE.ID))    .where(ACCOUNT.AGE.ge(18)); List list = accountMapper.selectListByQueryAs(q, AccountDTO.class);

但当你开始写多层子查询/嵌套条件时,可读性很容易被“对象套对象”拉低。


比如“订单金额 > 用户 1 平均订单金额”这种:


// 子查询QueryWrapper sub = QueryWrapper.create()    .select(avg(ORDER.TOTAL_PRICE))    .from(ORDER)    .where(ORDER.USER_ID.eq(1)); // 主查询QueryWrapper main = QueryWrapper.create()    .select(ORDER.ALL_COLUMNS)    .from(ORDER)    .where(ORDER.TOTAL_PRICE.gt(sub)); List list = orderMapper.selectListByQuery(main);

能写、也类型更安全,但维护者往往需要在脑子里把它“还原成 SQL”再理解意图。嵌套层级越深,这个成本越高。


7)到底怎么选?


参考落地策略:



  • 固定 SQL / 简单单表@Select 足够

  • 中等动态 SQL(条件多、拼接多,但逻辑清晰)@SelectProvider + SQL Builder

  • 单表 CRUD 为主,追求少写 SQL:MyBatis-Plus

  • Join 多、希望 Java 化表达更强:MyBatis-Flex(嵌套复杂时要克制)

  • 复杂报表 / 多层子查询 / 强声明式:直接原生 SQL(XML/SQL 文件),通常最清晰



Provider 这条路最让我意外:不靠第三方,也不把动态 SQL 写成字符串炼狱,但它也不是用来替代所有 SQL 的。把边界定好,用起来会更舒服。


总之就是在不同场景下面选择合适的技术并确定合理的规范,然后统一按照规范执行就可以啦!


作者:程序员布吉岛
来源:juejin.cn/post/7603656494904737798
收起阅读 »

北京回长沙了,简单谈谈感受!

大家好呀,我是飞鱼 我今年已经从北京回长沙了,这里谈谈感受。 ❝ 首先我回长沙不是逃离,而是换一种更舒服、更可持续的生活方式。 北京给了我视野和能力,长沙给了我生活和归属。 最直观的变化 节奏慢了:不用挤早高峰了,走路不用小跑,回家路更短。 心态稳了:不...
继续阅读 »

大家好呀,我是飞鱼


我今年已经从北京回长沙了,这里谈谈感受。




首先我回长沙不是逃离,而是换一种更舒服、更可持续的生活方式。


北京给了我视野和能力,长沙给了我生活和归属。



最直观的变化



  • 节奏慢了:不用挤早高峰了,走路不用小跑,回家路更短。

  • 心态稳了:不再天天赶进度、追KPI,人也没那么紧绷了。


生活成本:压力明显小了




房租、通勤、日常开销都降了不少,以前在北京工资高,但大头都被生活成本吃掉了。


现在收入可能少些,但心里踏实很多。



个人生活:更松弛也更有边界




回来后作息更规律了,能早点睡、早点起,周内也会留出时间运动或散步。


以前下班只想躺着刷手机,现在会给自己留一点空白时间,用来读书、整理思路或者陪家人聊天。


生活变简单,但心里更笃定,能把注意力放在真正重要的人和事上。



城市气息:更有生活感




长沙烟火气足,我现在每周都会去爬一次岳麓山(离得近)。


周末也能随时约上朋友一起吃饭聊天,不用再掐着时间赶路。



个人成长:从外部驱动到自我驱动




以前在北京,节奏和环境会推着我走,事情一件接一件,来不及想太多。


回长沙后,外部推力小了,但我开始主动搭自己的节奏:给自己设目标、做复盘、安排学习计划。


慢下来之后,反而更能看清自己擅长什么、缺什么,也更容易把工作和生活都经营得更稳。



给同样选择的人一点建议



  • 先想清楚你想要什么:是离家近、生活压力小,还是职业成长更快?别只因为累了就决定,要有明确的取舍。

  • 提前做资源准备:无论去哪,职业发展都得靠自己,技能储备、作品、圈子都要主动经营。

  • 规划现金流:收入变化要提前算清楚,别让生活压力反过来影响判断。

  • 给自己一个过渡期:回去不是立刻完美适应,给自己几个月调整节奏,别太焦虑。


最后想说


适合自己的地方,不一定是机会最多的地方,而是能让你活得更从容、有力量的地方。




最后想看技术文章的,可以去我的个人网站:hardyfish.top/



作者:程序员飞鱼
来源:juejin.cn/post/7603781883973763091
收起阅读 »

索引夺命10连问,你能顶住第几问?

前言 今天我们来聊聊让无数开发者又爱又恨的——数据库索引。 相信不少小伙伴在工作中都遇到过这样的场景: 明明已经加了索引,为什么查询还是慢? 为什么有时候索引反而导致性能下降? 联合索引到底该怎么设计才合理? 别急,今天我就通过10个问题,带你彻底搞懂索引...
继续阅读 »

前言


今天我们来聊聊让无数开发者又爱又恨的——数据库索引


相信不少小伙伴在工作中都遇到过这样的场景:



  • 明明已经加了索引,为什么查询还是慢?

  • 为什么有时候索引反而导致性能下降?

  • 联合索引到底该怎么设计才合理?


别急,今天我就通过10个问题,带你彻底搞懂索引的奥秘!


希望对你会有所帮助。


最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、场景设计题、面试真题、7个项目实战、工作内推什么都有


一、什么是索引?为什么需要索引?


1.1 索引的本质


简单来说,索引就是数据的目录


就像一本书的目录能帮你快速找到内容一样,数据库索引能帮你快速定位数据。


-- 没有索引的查询(全表扫描)
SELECT * FROM users WHERE name = '苏三'; -- 需要遍历所有记录

-- 有索引的查询(索引扫描)
CREATE INDEX idx_name ON users(name);
SELECT * FROM users WHERE name = '苏三'; -- 通过索引快速定位

1.2 索引的工作原理



索引的底层结构(B+树)


二、索引的10个常见问题


1.为什么我加了索引,查询还是慢?


场景还原


CREATE INDEX idx_name ON users(name);
SELECT * FROM users WHERE name LIKE '%苏三%'; -- 还是很慢!

原因分析



  1. 前导通配符LIKE '%苏三% 导致索引失效

  2. 索引选择性差:如果name字段大量重复,索引效果不佳

  3. 回表代价高:索引覆盖不全,需要回表查询


解决方案


-- 方案1:避免前导通配符
SELECT * FROM users WHERE name LIKE '苏三%';

-- 方案2:使用覆盖索引
CREATE INDEX idx_name_covering ON users(name, id, email);
SELECT name, id, email FROM users WHERE name LIKE '苏三%'; -- 不需要回表

-- 方案3:使用全文索引(对于文本搜索)
CREATE FULLTEXT INDEX ft_name ON users(name);
SELECT * FROM users WHERE MATCH(name) AGAINST('苏三');

2.索引是不是越多越好?


绝对不是! 索引需要维护代价:


-- 每个索引都会影响写性能
INSERT INTO users (name, email, age) VALUES ('苏三', 'susan@example.com', 30);
-- 需要更新:
-- 1. 主键索引
-- 2. idx_name索引(如果存在)
-- 3. idx_email索引(如果存在)
-- 4. idx_age索引(如果存在)

索引的代价



  1. 存储空间:每个索引都需要额外的磁盘空间

  2. 写操作变慢:INSERT/UPDATE/DELETE需要维护所有索引

  3. 优化器负担:索引太多会增加查询优化器的选择难度


黄金法则:一般建议表的索引数量不超过5-7个


3.联合索引的最左前缀原则是什么?


最左前缀原则:联合索引只能从最左边的列开始使用


-- 创建联合索引
CREATE INDEX idx_name_age ON users(name, age);

-- 能使用索引的查询
SELECT * FROM users WHERE name = '苏三'; -- √ 使用索引
SELECT * FROM users WHERE name = '苏三' AND age = 30; -- √ 使用索引
SELECT * FROM users WHERE age = 30 AND name = '苏三'; -- √ 优化器会调整顺序

-- 不能使用索引的查询
SELECT * FROM users WHERE age = 30; -- × 不符合最左前缀

联合索引结构



4.如何选择索引字段的顺序?


选择原则



  1. 高选择性字段在前:选择性高的字段能更快过滤数据

  2. 经常查询的字段在前:优先满足常用查询场景

  3. 等值查询在前,范围查询在后


-- 计算字段选择性
SELECT
COUNT(DISTINCT name) / COUNT(*) as name_selectivity,
COUNT(DISTINCT age) / COUNT(*) as age_selectivity,
COUNT(DISTINCT city) / COUNT(*) as city_selectivity
FROM users;

-- 根据选择性决定索引顺序
CREATE INDEX idx_name_city_age ON users(name, city, age); -- name选择性最高

5.什么是覆盖索引?为什么重要?


覆盖索引:索引包含了查询需要的所有字段,不需要回表查询


-- 不是覆盖索引(需要回表)
CREATE INDEX idx_name ON users(name);
SELECT * FROM users WHERE name = '苏三'; -- 需要回表查询其他字段

-- 覆盖索引(不需要回表)
CREATE INDEX idx_name_covering ON users(name, email, age);
SELECT name, email, age FROM users WHERE name = '苏三'; -- 所有字段都在索引中

覆盖索引的优势



  1. 避免回表:减少磁盘IO

  2. 减少内存占用:只需要读取索引页

  3. 提升性能:查询速度更快


6.NULL值对索引有什么影响?


NULL值的问题


-- 创建索引
CREATE INDEX idx_email ON users(email);

-- 查询NULL值
SELECT * FROM users WHERE email IS NULL; -- 可能不使用索引
SELECT * FROM users WHERE email IS NOT NULL; -- 可能不使用索引

NULL值可能不使用索引。


解决方案



  1. 避免NULL值:设置默认值

  2. 使用函数索引(MySQL 8.0+)


-- 使用函数索引处理NULL值
CREATE INDEX idx_email_null ON users((COALESCE(email, '')));
SELECT * FROM users WHERE COALESCE(email, '') = '';

7.索引对排序和分组有什么影响?


索引优化排序和分组


-- 创建索引
CREATE INDEX idx_age_name ON users(age, name);

-- 索引优化排序
SELECT * FROM users ORDER BY age, name; -- √ 使用索引避免排序

-- 索引优化分组
SELECT age, COUNT(*) FROM users GR0UP BY age; -- √ 使用索引优化分组

-- 无法使用索引排序的情况
SELECT * FROM users ORDER BY name, age; -- × 不符合最左前缀
SELECT * FROM users ORDER BY age DESC, name ASC; -- × 排序方向不一致

最近为了帮助大家找工作,专门建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:掘金+所在城市,即可进群。


8.如何发现索引失效的场景?


常见索引失效场景



  1. 函数操作WHERE YEAR(create_time) = 2023

  2. 类型转换WHERE phone = 13800138000(phone是varchar)

  3. 数学运算WHERE age + 1 > 30

  4. 前导通配符WHERE name LIKE '%苏三'


使用EXPLAIN分析


EXPLAIN SELECT * FROM users WHERE name = '苏三';

-- 查看关键指标:
-- type: const|ref|range|index|ALL(性能从好到坏)
-- key: 实际使用的索引
-- rows: 预估扫描行数
-- Extra: Using index(覆盖索引)| Using filesort(需要排序)| Using temporary(需要临时表)

9.如何维护和优化索引?


定期索引维护


-- 查看索引使用情况(MySQL)
SELECT * FROM sys.schema_index_statistics
WHERE table_schema = 'your_database' AND table_name = 'users';

-- 重建索引(优化索引碎片)
ALTER TABLE users REBUILD INDEX idx_name;

-- 分析索引使用情况
ANALYZE TABLE users;

索引监控


-- 开启索引监控(Oracle)
ALTER INDEX idx_name MONITORING USAGE;

-- 查看索引使用情况
SELECT * FROM v$object_usage WHERE index_name = 'IDX_NAME';

10.不同数据库的索引有什么差异?


MySQL vs PostgreSQL索引差异


特性MySQLPostgreSQL
索引类型B+Tree, Hash, FulltextB+Tree, Hash, GiST, SP-GiST
覆盖索引支持支持(使用INCLUDE)
函数索引8.0+支持支持
部分索引支持支持
索引组织表聚簇索引堆表

PostgreSQL示例


-- 创建包含索引(Covering Index)
CREATE INDEX idx_users_covering ON users (name) INCLUDE (email, age);

-- 创建部分索引(Partial Index)
CREATE INDEX idx_active_users ON users (name) WHERE is_active = true;

-- 创建表达式索引(Expression Index)
CREATE INDEX idx_name_lower ON users (LOWER(name));

三、索引设计最佳实践


3.1 索引设计原则



  1. 按需创建:只为经常查询的字段创建索引

  2. 选择合适类型:根据场景选择B-Tree、Hash、全文索引等

  3. 考虑复合索引:使用复合索引减少索引数量

  4. 避免过度索引:每个索引都有维护成本

  5. 定期维护:重建索引,优化索引碎片


3.2 索引设计检查清单



四、实战案例:电商系统索引设计


4.1 用户表索引设计


-- 用户表结构
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20),
age INT,
city VARCHAR(50),
created_at TIMESTAMP,
is_active BOOLEAN
);

-- 推荐索引
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_city_age ON users(city, age);
CREATE INDEX idx_users_created ON users(created_at) WHERE is_active = true;

4.2 订单表索引设计


-- 订单表结构
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status VARCHAR(20),
amount DECIMAL(10,2),
created_at TIMESTAMP,
updated_at TIMESTAMP
);

-- 推荐索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
CREATE INDEX idx_orders_created_amount ON orders(created_at, amount);

总结



  1. 理解原理:掌握B+树索引的工作原理和特性。

  2. 合理设计:遵循最左前缀原则,选择合适的索引顺序。

  3. 避免失效:注意索引失效的常见场景。

  4. 覆盖索引:尽可能使用覆盖索引减少回表。

  5. 定期维护:监控索引使用情况,定期优化重建。

  6. 权衡利弊:索引不是越多越好,要权衡查询性能和写成本。



好的索引设计是数据库性能的基石。



不要盲目添加索引,要基于实际查询需求和数据分布来科学设计。


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。


求一键三连:点赞、转发、在看。


关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。


作者:苏三说技术
来源:juejin.cn/post/7578402574652850228
收起阅读 »

CSS 也要支持 if 了 !!!CSS if() 函数来了!

web
CSS 也要支持 if 了 !!!CSS if() 函数来了! CSS if() 函数允许在纯 CSS 中基于条件为属性赋值,无需 JavaScript 或预处理器。该函数已在 Chrome 137 发布。 过去常用的做法包括通过 JavaScript 切换类...
继续阅读 »

CSS 也要支持 if 了 !!!CSS if() 函数来了!


CSS if() 函数允许在纯 CSS 中基于条件为属性赋值,无需 JavaScript 或预处理器。该函数已在 Chrome 137 发布。


过去常用的做法包括通过 JavaScript 切换类名、使用预处理器 mixin 或编写大量媒体查询。if() 将条件逻辑引入 CSS,使写法更直接、性能稳定。


原文 CSS 也要支持 if 了 !!!CSS if() 函数来了!


工作原理


property: if(condition-1: value-1; condition-2: value-2; condition-3: value-3; else: default-value);

函数按顺序检查条件并应用第一个匹配的值;若没有条件匹配,则使用 else 的值。这一语义与常见编程语言一致,但实现于纯 CSS。


if() 的三种能力


样式查询(Style queries)


使用 style() 可响应 CSS 自定义属性:


.card {
--status: attr(data-status type(<custom-ident>));

border-color: if(style(--status: pending): royalblue; style(--status: complete): seagreen; style(--status: error): crimson; else: gray);
}

一个 data-status 属性即可驱动对应样式,无需额外工具类。


媒体查询(Media queries)


使用 media() 可以在属性内联定义响应式值,无需嵌套媒体查询块:


h1 {
font-size: if(media(width >= 1200px): 3rem; media(width >= 768px): 2.5rem; media(width >= 480px): 2rem; else: 1.75rem);
}

特性检测(Feature detection)


使用 supports() 可在属性中直接进行特性检测,并提供明确回退:


.element {
border-color: if(supports(color: lch(0 0 0)): lch(50% 100 150) ; supports(color: lab(0 0 0)): lab(50 100 -50) ; else: rgb(200, 100, 50));
}

真实用例


暗色模式示例


body {
--theme: 'dark'; /* 通过 JavaScript 或用户偏好切换 */

background: if(style(--theme: 'dark'): #1a1a1a; else: white);

color: if(style(--theme: 'dark'): #e4e4e4; else: #333);
}

设计系统状态组件


.alert {
--type: attr(data-type type(<custom-ident>));

background: if(style(--type: success): #d4edda; style(--type: warning): #fff3cd; style(--type: danger): #f8d7da; style(--type: info): #d1ecf1; else: #f8f9fa);

border-left: 4px solid if(style(--type: success): #28a745; style(--type: warning): #ffc107; style(--type: danger): #dc3545; style(--type: info): #17a2b8; else: #6c757d);
}

容器尺寸示例(简化媒体查询)


.container {
width: if(media(width >= 1400px): 1320px; media(width >= 1200px): 1140px; media(width >= 992px): 960px; media(width >= 768px): 720px; media(width >= 576px): 540px; else: 100%);

padding-inline: if(media(width >= 768px): 2rem; else: 1rem);
}

与现代 CSS 特性结合


.element {
/* 搭配新的 light-dark() 函数 */
color: if(style(--high-contrast: true): black; else: light-dark(#333, #e4e4e4));

/* 搭配 CSS 自定义函数(@function) */
padding: if(style(--spacing: loose): --spacing-function(2) ; style(--spacing: tight): --spacing-function(0.5) ; else: --spacing-function(1));
}

浏览器支持


支持情况(截至 2025 年 8 月):



  • ✅ Chrome/Edge:自 137 版起

  • ✅ Chrome Android:自 139 版起

  • ❌ Firefox:开发中

  • ❌ Safari:在规划中

  • ❌ Opera:尚未支持


在尚未完全支持的环境中,可采用如下写法:


.button {
/* 所有浏览器的回退 */
padding: 1rem 2rem;
background: #007bff;

/* 现代浏览器会自动覆盖 */
padding: if(style(--size: small): 0.5rem 1rem; style(--size: large): 1.5rem 3rem; else: 1rem 2rem);

background: if(style(--variant: primary): #007bff; style(--variant: success): #28a745; style(--variant: danger): #dc3545; else: #6c757d);
}

未来展望


CSS 工作组已经在推进扩展能力:



  • 范围查询:if(style(--value > 100): ...)

  • 逻辑运算符:if(style(--a: true) and style(--b: false): ...)

  • 容器查询集成:更强的上下文感知


在使用前建议评估目标浏览器版本,并准备相应回退方案。


作者:BingoGo
来源:juejin.cn/post/7571758212472897587
收起阅读 »

公司来的新人用字符串存储日期,被组长怒怼了...

在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可...
继续阅读 »

在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。


本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。


不要用字符串存储日期


和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。


但是,这是不正确的做法,主要会有下面两个问题:



  1. 空间效率:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。

  2. 查询与计算效率低下

    • 比较操作复杂且低效:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。

    • 计算功能受限:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。

    • 索引性能不佳:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。




DATETIME 和 TIMESTAMP 选择


DATETIMETIMESTAMP 是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?


下面我们从几个关键维度对它们进行对比:


时区信息


DATETIME 类型存储的是字面量的日期和时间值,它本身不包含任何时区信息。当你插入一个 DATETIME 值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。


这样就会有什么问题呢? 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 DATETIME 时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。


TIMESTAMP 和时区有关。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 TIMESTAMP 字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。


这意味着,对于同一条记录的 TIMESTAMP 字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。


下面实际演示一下!


建表 SQL 语句:


CREATE TABLE `time_zone_test` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`date_time` datetime DEFAULT NULL,
`time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入一条数据(假设当前会话时区为系统默认,例如 UTC+0)::


INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());

查询数据(在同一时区会话下):


SELECT date_time, time_stamp FROM time_zone_test;

结果:


+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |
+---------------------+---------------------+

现在,修改当前会话的时区为东八区 (UTC+8):


SET time_zone = '+8:00';

再次查询数据:


# TIMESTAMP 的值自动转换为 UTC+8 时间
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 |
+---------------------+---------------------+

扩展:MySQL 时区设置常用 SQL 命令


# 查看当前会话时区
SELECT @@session.time_zone;
# 设置当前会话时区
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
# 数据库全局时区设置
SELECT @@global.time_zone;
# 设置全局时区
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';

占用空间


下图是 MySQL 日期类型所占的存储空间(官方文档传送门:dev.mysql.com/doc/refman/…):



在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 58 字节,TIMESTAMP 的范围是 47 字节。


表示范围


TIMESTAMP 表示的时间范围更小,只能到 2038 年:



  • DATETIME:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'

  • TIMESTAMP:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC


性能


由于 TIMESTAMP 在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,DATETIME 因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。


为了获得可预测的行为并可能减少 TIMESTAMP 的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 time_zone 参数,而不是依赖服务器的默认或操作系统时区。


数值时间戳是更好的选择吗?


除了上述两种类型,实践中也常用整数类型(INTBIGINT)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。


这种存储方式的具有 TIMESTAMP 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。


时间戳的定义如下:



时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。



数据库中实际操作:


-- 将日期时间字符串转换为 Unix 时间戳 (秒)
mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
| 1578707612 |
+---------------------------------------+
1 row in set (0.00 sec)

-- 将 Unix 时间戳 (秒) 转换为日期时间格式
mysql> SELECT FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
| 2020-01-11 09:53:32 |
+---------------------------+
1 row in set (0.01 sec)

PostgreSQL 中没有 DATETIME


由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:http://www.postgresql.org/docs/curren…


PostgreSQL 时间类型总结


可以看到,PG 没有名为 DATETIME 的类型:



  • PG 的 TIMESTAMP WITHOUT TIME ZONE在功能上最接近 MySQL 的 DATETIME。它存储日期和时间,但不包含任何时区信息,存储的是字面值。

  • PG 的TIMESTAMP WITH TIME ZONE (或 TIMESTAMPTZ) 相当于 MySQL 的 TIMESTAMP。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。


对于绝大多数需要记录精确发生时间点的应用场景,TIMESTAMPTZ是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。


总结


MySQL 中时间到底怎么存储才好?DATETIME?TIMESTAMP?还是数值时间戳?


并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。


《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:



每种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:


类型存储空间日期格式日期范围是否带时区信息
DATETIME5~8 字节YYYY-MM-DD hh:mm:ss[.fraction]1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999]
TIMESTAMP4~7 字节YYYY-MM-DD hh:mm:ss[.fraction]1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999]
数值型时间戳4 字节全数字如 15787076121970-01-01 00:00:01 之后的时间

选择建议小结:



  • TIMESTAMP 的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,TIMESTAMP 是自然的选择(注意其时间范围限制,也就是 2038 年问题)。

  • 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,DATETIME 是更稳妥的选择。

  • 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。


作者:JavaGuide
来源:juejin.cn/post/7488927722774937609
收起阅读 »

这 10 个 MySQL 高级用法,让你的代码又快又好看

大家好,我是大华! MySQL 有很多高级但实用的功能,能让你的查询变得更简洁、更高效。 今天分享 10 个我在工作中经常使用的 SQL 技巧,不用死记硬背,掌握了就能立刻提升你的数据库操作水平! 1. CTE(WITH 子句)——让复杂查询变清晰 -- 传统...
继续阅读 »

大家好,我是大华!


MySQL 有很多高级但实用的功能,能让你的查询变得更简洁、更高效。


今天分享 10 个我在工作中经常使用的 SQL 技巧,不用死记硬背,掌握了就能立刻提升你的数据库操作水平!


1. CTE(WITH 子句)——让复杂查询变清晰


-- 传统子查询,难以阅读
SELECT nickname
FROM system_users
WHERE dept_id IN (
SELECT id FROM system_dept WHERE `name` = 'IT部'
);

-- 使用CTE,逻辑清晰
WITH ny_depts AS (
SELECT id FROM system_dept WHERE `name` = 'IT部'
)
SELECT u.nickname
FROM system_users u
JOIN ny_depts nd ON u.dept_id = nd.id;

解释:



  • WITH ny_depts AS (...):先创建一个临时结果集,叫 ny_depts,里面只包含“IT部”的部门名称。

  • SELECT u.nickname FROM system_users u JOIN ny_depts...:再从用户表中找出那些部门ID在ny_depts里的员工昵称。


好处:把找部门和找人分成两步,逻辑更清楚,比嵌套子查询好读多了。




2. 窗口函数 —— 不分组也能统计


SELECT 
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept,
AVG(salary) OVER (PARTITION BY department) AS avg_salary
FROM employees;

解释:



  • PARTITION BY department:按部门“分组”,但不合并行,每行仍然保留。

  • RANK() OVER (...):在每个部门内部,按薪水从高到低排名(相同薪水并列)。

  • AVG(salary) OVER (...):计算每个部门的平均工资,并显示在每一行里。


对比 GR0UP BYGR0UP BY 会把多行合并成一行,而窗口函数保留原始行,同时加上统计值。




3. 条件聚合 —— 一行查出多个统计


SELECT 
YEAR(created_at) AS year,
COUNT(*) AS total,
COUNT(CASE WHEN status = 'completed' THEN 1 END) AS completed,
SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) AS revenue
FROM orders
GR0UP BY YEAR(created_at);

解释:



  • YEAR(created_at):提取订单年份。

  • COUNT(*):该年总订单数。

  • COUNT(CASE WHEN status = 'completed' THEN 1 END)
    如果状态是 'completed',就返回 1,否则返回 NULL

  • COUNT() 只统计非 NULL 值,所以这行就是“完成的订单数”。

  • SUM(CASE WHEN ... THEN amount ELSE 0 END):只对完成的订单求金额总和。


关键:不用写多个子查询,一条语句搞定全年报表!




4. 自连接 —— 同一张表自己连自己


SELECT e1.name, e2.name
FROM employees e1
JOIN employees e2
ON e1.department = e2.department
AND e1.id < e2.id
AND ABS(e1.salary - e2.salary) <= e1.salary * 0.1;

解释:



  • employees e1 JOIN employees e2:把员工表当成两个副本(e1 和 e2)来连接。

  • e1.department = e2.department:只找同一个部门的人。

  • e1.id < e2.id:避免重复配对(比如 Alice-Bob 和 Bob-Alice 只保留一个)。

  • ABS(...):计算两人薪水差是否 ≤ 10%。


用途:找“相似记录”“配对关系”“上下级”等场景非常有用。




5. EXISTS 替代 IN —— 更高效的存在判断


SELECT name FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.amount > 1000
);

解释:



  • 对每一位客户 c,检查是否存在一笔订单满足:

    • 订单的 customer_id 等于这个客户的 id

    • 订单金额 > 1000



  • SELECT 1:这里不需要返回具体字段,只要知道“有没有”就行,所以用 1 最轻量。

  • 为什么快?:一旦找到一条匹配订单,就立刻停止搜索,不像 IN 可能要加载全部订单 ID。


注意:如果子查询可能返回 NULLIN 会失效(因为 x IN (..., NULL) 永远为 UNKNOWN),而 EXISTS 不受影响。




6. JSON 函数 —— 轻松读取 JSON 字段


SELECT 
name,
profile->>'$.address.city' AS city,
JSON_EXTRACT(profile, '$.age') AS age
FROM users
WHERE profile->>'$.city' = 'Beijing';

解释:



  • profile 是一个 JSON 类型字段,比如:{"address": {"city": "Beijing"}, "age": 30}

  • profile->>'$.address.city'

    • ->> 是简写,等价于 JSON_UNQUOTE(JSON_EXTRACT(...))

    • 返回字符串 "Beijing"(去掉引号)



  • JSON_EXTRACT(profile, '$.age'):返回 30(带类型,可能是数字)

  • WHERE profile->>'$.city' = 'Beijing':筛选城市是北京的用户。


适用场景:用户偏好、动态表单、日志等结构不固定的字段。




7. 生成列 —— 数据库自动帮你算


CREATE TABLE products (
id INT PRIMARY KEY,
width DECIMAL(10,2),
height DECIMAL(10,2),
area DECIMAL(10,2) AS (width * height) STORED
);

INSERT INTO products (id, width, height) VALUES (1, 5, 10);

解释:



  • area DECIMAL(...) AS (width * height) STORED

    • 这是一个“存储型生成列”,数据库会自动计算 width * height 并存下来。

    • 如果不加 STORED,就是“虚拟列”(每次查询时计算,不占存储)。



  • 插入时只需给 widthheightarea 自动变成 50


优势:避免应用层重复计算,还能给 area 加索引加速查询!




8. 多表更新 —— 一条语句更新关联数据


UPDATE customers c
JOIN (
SELECT customer_id, SUM(amount) AS total
FROM orders
GR0UP BY customer_id
) o ON c.id = o.customer_id
SET c.total_spent = o.total;

解释:



  • 子查询 o:先按客户 ID 统计每个人的总消费。

  • UPDATE customers c JOIN o ...:把客户表和统计结果连接起来。

  • SET c.total_spent = o.total:直接把统计值写回客户表。


好处:不用在程序里循环“查一个、改一个”,减少网络开销,保证原子性。




9. GR0UP_CONCAT —— 多行变一行


SELECT 
department,
GR0UP_CONCAT(name ORDER BY salary DESC SEPARATOR ', ') AS members
FROM employees
GR0UP BY department;

解释:



  • GR0UP BY department:按部门分组。

  • GR0UP_CONCAT(name ...):把每个部门的所有员工名字拼成一个字符串。

  • ORDER BY salary DESC:按薪水从高到低排序后再拼接。

  • SEPARATOR ', ':用逗号加空格分隔名字。


典型用途:导出名单、展示标签、汇总明细等。


默认最多拼 1024 字符,可通过 SET SESSION group_concat_max_len = 1000000; 调大。




10. INSERT ... ON DUPLICATE KEY UPDATE —— 智能插入/更新


INSERT INTO page_views (page_url, view_date, view_count)
VALUES ('/home', CURDATE(), 1)
ON DUPLICATE KEY UPDATE
view_count = view_count + 1;

解释:



  • 尝试插入一条新记录:页面 /home,今天日期,访问次数为 1。

  • 如果因为唯一索引冲突(比如 (page_url, view_date) 是唯一键)导致插入失败:

    • 就执行 ON DUPLICATE KEY UPDATE 部分

    • 把原有的 view_count 加 1



  • 效果:第一次访问创建记录,之后每次访问自动 +1,完美实现计数器!


前提:表必须有主键或唯一索引,否则不会触发更新。





本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!



📌往期精彩


《async/await 到底要不要加 try-catch?异步错误处理最佳实践》


《如何查看 SpringBoot 当前线程数?3 种方法亲测有效》


《Java 开发必看:什么时候用 for,什么时候用 Stream?》


《别再乱 new ArrayList!8 大 Java 容器选型案例,一篇看懂》


作者:程序员大华
来源:juejin.cn/post/7584266184882552866
收起阅读 »

Vue3 后台分页写腻了?我用 1 个 Hook 删掉 90% 重复代码(附源码)

web
实战推荐: 不仅免费,还开源?这个 AI Mock 神器我必须曝光它 ⚡ 一个Vue自定义指令搞定丝滑拖拽列表,告别复杂组件封装 🔥 这才是 Vue 驱动的 Chrome 插件工程化正确打开方式 女朋友又给我出难题了:解锁网页禁用复制 + 一键提取图片文...
继续阅读 »

实战推荐:





还在为每个列表页写重复的分页代码而烦恼吗? 还在复制粘贴 currentPage、pageSize、loading 等状态吗? 一个 Hook 帮你解决所有分页痛点,减少90%重复代码



背景与痛点


在后台管理系统开发中,分页列表查询非常常见,我们通常需要处理:



  • 当前页、页大小、总数等分页状态

  • 加载中、错误处理等请求状态

  • 搜索、刷新、翻页等分页操作

  • 数据缓存和重复请求处理


这些重复逻辑分散在各个组件中,维护起来很麻烦。


为了解决这个烦恼,我专门封装了分页数据管理 Hook。现在只需要几行代码,就能轻松实现分页查询,省时又高效,减少了大量重复劳动


使用前提 - 接口格式约定


查询接口返回的数据格式:


{
list: [ // 当前页数据数组
{ id: 1, name: 'user1' },
{ id: 2, name: 'user2' }
],
total: 100 // 数据总条数
}

先看效果:分页查询只需几行代码!


import usePageFetch from '@/hooks/usePageFetch' // 引入分页查询 Hook,封装了分页逻辑和状态管理
import { getUserList } from '@/api/user' // 引入请求用户列表的 API 方法

// 使用 usePageFetch Hook 实现分页数据管理
const {
currentPage, // 当前页码
pageSize, // 每页条数
total, // 数据总数
data, // 当前页数据列表
isFetching, // 加载状态,用于控制 loading 效果
search, // 搜索方法
onSizeChange, // 页大小改变事件处理方法
onCurrentChange // 页码改变事件处理方法
} = usePageFetch(
getUserList, // 查询API
{ initFetch: false } // 是否自动请求一次(组件挂载时自动拉取第一页数据)
)

这样子每次分页查询只需要引入hook,然后传入查询接口就好了,减少了大量重复劳动


解决方案


我设计了两个相互配合的 Hook:



  • useFetch:基础请求封装,处理请求状态和缓存

  • usePageFetch:分页逻辑封装,专门处理分页相关的状态和操作


usePageFetch (分页业务层)
├── 管理 page / pageSize / total 状态
├── 处理搜索、刷新、翻页逻辑
├── 统一错误处理和用户提示
└── 调用 useFetch (请求基础层)
├── 管理 loading / data / error 状态
├── 可选缓存机制(避免重复请求)
└── 成功回调适配不同接口格式

核心实现


useFetch - 基础请求封装


// hooks/useFetch.js
import { ref } from 'vue'

const Cache = new Map()

/**
* 基础请求 Hook
* @param {Function} fn - 请求函数
* @param {Object} options - 配置选项
* @param {*} options.initValue - 初始值
* @param {string|Function} options.cache - 缓存配置
* @param {Function} options.onSuccess - 成功回调
*/

function useFetch(fn, options = {}) {
const isFetching = ref(false)
const data = ref()
const error = ref()

// 设置初始值
if (options.initValue !== undefined) {
data.value = options.initValue
}

function fetch(...args) {
isFetching.value = true
let promise

if (options.cache) {
const cacheKey = typeof options.cache === 'function'
? options.cache(...args)
: options.cache || `${fn.name}_${args.join('_')}`

promise = Cache.get(cacheKey) || fn(...args)
Cache.set(cacheKey, promise)
} else {
promise = fn(...args)
}

// 成功回调处理
if (options.onSuccess) {
promise = promise.then(options.onSuccess)
}

return promise
.then(res => {
data.value = res
isFetching.value = false
error.value = undefined
return res
})
.catch(err => {
isFetching.value = false
error.value = err
return Promise.reject(err)
})
}

return {
fetch,
isFetching,
data,
error
}
}

export default useFetch


usePageFetch - 分页逻辑封装


// hooks/usePageFetch.js
import { ref, onMounted, toRaw, watch } from 'vue'
import useFetch from './useFetch' // 即上面的hook ---> useFetch
import { ElMessage } from 'element-plus'

/**
* 分页数据管理 Hook
* @param {Function} fn - 请求函数
* @param {Object} options - 配置选项
* @param {Object} options.params - 默认参数
* @param {boolean} options.initFetch - 是否自动初始化请求
* @param {Ref} options.formRef - 表单引用
*/

function usePageFetch(fn, options = {}) {
// 分页状态
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const data = ref([])
const params = ref()
const pendingCount = ref(0)

// 初始化参数
params.value = options.params

// 使用基础请求 Hook
const { isFetching, fetch: fetchFn, error, data: originalData } = useFetch(fn)

// 核心请求方法
const fetch = async (searchParams, pageNo, size) => {
try {
// 更新分页状态
page.value = pageNo
pageSize.value = size
params.value = searchParams

// 发起请求
await fetchFn({
page: pageNo,
pageSize: size,
// 使用 toRaw 避免响应式对象问题
...(searchParams ? toRaw(searchParams) : {})
})

// 处理响应数据
data.value = originalData.value?.list || []
total.value = originalData.value?.total || 0
pendingCount.value = originalData.value?.pendingCounts || 0
} catch (e) {
console.error('usePageFetch error:', e)
ElMessage.error(e?.msg || e?.message || '请求出错')
// 清空数据,提供更好的用户体验
data.value = []
total.value = 0
}
}

// 搜索 - 重置到第一页
const search = async (searchParams) => {
await fetch(searchParams, 1, pageSize.value)
}

// 刷新当前页
const refresh = async () => {
await fetch(params.value, page.value, pageSize.value)
}

// 改变页大小
const onSizeChange = async (size) => {
await fetch(params.value, 1, size) // 重置到第一页
}

// 切换页码
const onCurrentChange = async (pageNo) => {
await fetch(params.value, pageNo, pageSize.value)
}

// 组件挂载时自动请求
onMounted(() => {
if (options.initFetch !== false) {
search(params.value)
}
})

// 监听表单引用变化(可选功能)
watch(
() => options.formRef,
(formRef) => {
if (formRef) {
console.log('Form ref updated:', formRef)
}
}
)

return {
// 分页状态
currentPage: page,
pageSize,
total,
pendingCount,

// 数据状态
data,
originalData,
isFetching,
error,

// 操作方法
search,
refresh,
onSizeChange,
onCurrentChange
}
}

export default usePageFetch

完整使用示例


用element ui举例


<template>
<el-form :model="searchForm" >
<el-form-item label="用户名">
<el-input v-model="searchForm.username" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>

<!-- 表格数据展示,绑定 data 和 loading 状态 -->
<el-table :data="data" v-loading="isFetching">
<!-- ...表格列定义... -->
</el-table>


<!-- 分页组件,绑定当前页、页大小、总数,并响应切换事件 -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
@size-change="onSizeChange"
@current-change="onCurrentChange"
/>

</template>
<script setup>
import { ref } from 'vue'
import usePageFetch from '@/hooks/usePageFetch' // 引入分页查询 Hook,封装了分页逻辑和状态管理
import { getUserList } from '@/api/user' // 引入请求用户列表的 API 方法

// 搜索表单数据,响应式声明
const searchForm = ref({
username: ''
})

// 使用 usePageFetch Hook 实现分页数据管理
const {
currentPage, // 当前页码
pageSize, // 每页条数
total, // 数据总数
data, // 当前页数据列表
isFetching, // 加载状态,用于控制 loading 效果
search, // 搜索方法
onSizeChange, // 页大小改变事件处理方法
onCurrentChange // 页码改变事件处理方法
} = usePageFetch(
getUserList,
{ initFetch: false } // 是否自动请求一次(组件挂载时自动拉取第一页数据)
)

/**
* 处理搜索操作
*/

const handleSearch = () => {
search({ username: searchForm.value.username })
}

</script>


高级用法


带缓存


const {
data,
isFetching,
search
} = usePageFetch(getUserList, {
cache: (params) => `user-list-${JSON.stringify(params)}` // 自定义缓存 key
})

设计思路解析



  • 职责分离:useFetch 专注请求状态管理,usePageFetch 专注分页逻辑

  • 统一错误处理:在 usePageFetch 层统一处理错误

  • 智能缓存机制:支持多种缓存策略

  • 生命周期集成:自动在组件挂载时请求数据


总结


这套分页管理 Hook 的优势:



  • 开发效率高,减少90%的重复代码,新增列表页从 30 分钟缩短到 5 分钟

  • 状态管理完善,自动处理加载、错误、数据状态

  • 缓存机制,避免重复请求

  • 错误处理统一,用户体验一致

  • 易于扩展,支持自定义配置和回调



如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下!



作者:不一样的少年_
来源:juejin.cn/post/7549096640340426802
收起阅读 »

📢 程序员注意!这些代码可能会让你"吃牢饭"!

关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 不知道你有没有听过面向监狱编程,可能你好好的码着代码,就就被帽子叔叔带走了。 "我只是个写代码的,关我什么事?" 这是深圳某P2P平台架构师在法庭上崩溃大喊,但代码提交记录中的"反爬优化注释"...
继续阅读 »

关注我的公众号:【编程朝花夕拾】,可获取首发内容。




01 引言


不知道你有没有听过面向监狱编程,可能你好好的码着代码,就就被帽子叔叔带走了。


"我只是个写代码的,关我什么事?" 这是深圳某P2P平台架构师在法庭上崩溃大喊,但代码提交记录中的"反爬优化注释"成了铁证——他因非法吸收公众存款罪获刑5年。这不是电影,而是2023年真实判决!


程序员早已不是免责职业,你的键盘可能正在敲响监狱的大门!


02 血泪案例


⚠️ 这些代码真的会"坐牢"!


2.1 爬虫爬进铁窗


爬虫其实是最容易爬进铁窗的代码。当时可能只是为了解决一个业务痛点,一旦被非法使用,就可能触犯法律的红线。开发者无意,使用者有心,莫名其名的就背了锅。


案例:


浙江某程序员用分布式爬虫狂扫10亿条个人信息,庭审时辩解:"技术无罪!"。


法官怒怼:"每秒突破5次反爬验证,这叫'技术中立'?" —— 6人团队全员获刑!


所以,我们在日常工作中开发爬虫就应该得到启示:



  • ✅数据是否涉个人隐私?

  • ✅是否突破网站防护?

  • ✅对方是否知情同意?


实在拿不准,公司一般都会有法务,可以咨询。


2.2 权限变"凶器"


我们在开发过程中为处理决异常数据的问题,可能会在代码里面留后门。正常的业务功能本身没有问题,但是涉及支付、数据安全等行为,就要注意了。被他人恶意使用,不仅会造成财产损失,可能会还会勉励牢狱之灾。


案例:


杭州前程序员偷偷植入"定时转账代码",21万公款秒变私人财产。检察机关以马某涉嫌盗窃罪、妨害公务罪向法院提起公诉,经法院审理作出一审判决,马某被判处有期徒刑四年二个月,并处罚金。


该起事件也为程序员们敲响了警钟。玩归玩,闹归闹,法律红线不可碰。


🛑 高位操作清单:



  • ❌ 私留系统后门

  • ❌ 超权限访问数据

  • ❌ 删除/篡改数据或日志


2.3 "技术黑产"陷阱


程序员除了工作之外,很多人可能还会通过接私活,如猪八戒网等。以此增加自己的收入。没有公司的严格审核,很多程序员就会掉如技术黑产 的陷阱。


案例:


湖北大学生接私活开发"诈骗APP",庭审播放需求录音:"要能后台改赌局结果"。可能当时你只想着:"我只负责技术实现" 最后却成诈骗案从犯!


尤其一些关于支付的似乎要尤为谨慎,特别是支付成功后,限制体现的时间,很有可能就会用于非法洗钱的黑坑里。


🔥 接私活避坑指南



  • 👉 要求签署书面合同

  • 👉 录音确认需求合法性

  • 👉 转账账户必须对公


03 为什么程序员总成背锅侠


其实大多数程序员都是很单纯的,尤其那些喜欢挑战的程序员。他可能只为表现自己的实力,仅仅只是按照需求开发了功能,但是被恶意利用,最终成为背锅侠


程序员以为法官认定
突破反爬是技术挑战非法侵入计算机系统
按需求开发无过错明知违法仍提供帮助
删除代码就没事电子证据完整链锁定"

血泪真相:你的Git提交记录、代码注释、甚至TODO列表都可能成为呈堂证供!


04 IT人保命三件套


4.1 代码防坐牢套餐


敏感功能增加法律注释。开发的功能以及项目的沟通都要留档尤其需求的变更。因为接到需求的时候可能没有问题,改着改着就出问题了。


拒绝口头需求,落实文档记录,需求、会议、项目事件以邮件的方式存档。


4.2 权限管理生死线


权限管理是保护数据安全的重要措施,但是可能为了调试方便,预留逃逸后门。被人利用轻则数据信息泄露、重则踩缝纫机。


三方对接中,加强公钥、私钥的管理,防止恶意推送或者拉取数据。敏感信息是否脱敏,都是开发中需要考虑的要点。


如果有必要,增加埋点记录,日志记录。收集用户的操作行为。


4.3 法律意识


每个IT公司都会面临网络安全的检查,可以多了解一些相关的法律发条。至少了解那些数据可能会属于需要保护的数据,引起重视。


如果公司允许,可以多参加一些《数据安全法》《网络安全》等的培训。


05 技术向善


代码改变世界,这个不是一句虚话。运用好技术,代码也可以是光。


阿里巴巴的支付宝硬是借助技术,将全国带入数字支付的时代;疫情期间的随申码、一码通等,为战胜疫情作出了巨大贡献;大模型的火爆推送了智能时代的到来等等。


真正的大神不仅代码能跑通,人生更不能"跑偏"!


你在工作中遇到过哪些"法律边缘"的需求?评论区说出你的故事。


作者:SimonKing
来源:juejin.cn/post/7506417928788836362
收起阅读 »

招行2面:为什么需要序列化和反序列?为什么不能直接使用对象?

Hi,你好,我是猿java。 工作中,我们经常听到序列化和反序列化,那么,什么是序列化?什么又是反序列化?这篇文章,我们来分析一个招商的面试题:为什么需要序列化和反序列化? 1. 什么是序列化和反序列化? 简单来说,序列化就是把一个Java对象转换成一系列字节...
继续阅读 »

Hi,你好,我是猿java。


工作中,我们经常听到序列化反序列化,那么,什么是序列化?什么又是反序列化?这篇文章,我们来分析一个招商的面试题:为什么需要序列化反序列化


1. 什么是序列化和反序列化?


简单来说,序列化就是把一个Java对象转换成一系列字节的过程,这些字节可以被存储到文件、数据库,或者通过网络传输。反过来,反序列化则是把这些字节重新转换成Java对象的过程。


想象一下,你有一个手机应用中的用户对象(比如用户的名字、年龄等信息)。如果你想将这个用户对象存储起来,或者发送给服务器,你就需要先序列化它。等到需要使用的时候,再通过反序列化把它恢复成原来的对象。


2. 为什么需要序列化?


“为什么需要序列化?为什么不能直接使用对象呢?”这确实是一个好问题,而且很多工作多年的程序员不一定能回答清楚。综合来看:需要序列化的主要原因有以下三点:



  1. 持久化存储:当你需要将对象的数据保存到磁盘或数据库中时,必须把对象转换成一系列字节。

  2. 网络传输:在分布式系统中,不同的机器需要交换对象数据,序列化是实现这一点的关键。

  3. 深拷贝:有时候需要创建对象的副本,序列化和反序列化可以帮助你实现深拷贝。


更直白的说,序列化是为了实现持久化和网络传输,对象是应用层的东西,不同的语言(比如:java,go,python)创建的对象还不一样,实现持久化和网络传输的载体不认这些对象。


3. 序列化的原理分析


Java中的序列化是通过实现java.io.Serializable接口来实现的。这个接口是一个标记接口,意味着它本身没有任何方法,只是用来标记这个类的对象是可序列化的。


当你序列化一个对象时,Java会将对象的所有非瞬态(transient)和非静态字段的值转换成字节流。这包括对象的基本数据类型、引用类型,甚至是继承自父类的字段。


序列化的步骤



  1. 实现Serializable接口:你的类需要实现这个接口。

  2. 创建ObjectOutputStream:用于将对象转换成字节流。

  3. 调用writeObject方法:将对象写入输出流。

  4. 关闭流:别忘了关闭流以释放资源。


反序列化的步骤大致相同,只不过是使用ObjectInputStreamreadObject方法。


4. 示例演示


让我们通过一个简单的例子来看看实际操作是怎样的。


定义一个可序列化的类


import java.io.Serializable;

public class User implements Serializable {
private static final long serialVersionUID = 1L; // 推荐定义序列化版本号
private String name;
private int age;
private transient String password; // transient字段不会被序列化

public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}

// 省略getter和setter方法

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}

序列化对象


import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30, "secret123");

try (FileOutputStream fileOut = new FileOutputStream("user.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {

out.writeObject(user);
System.out.println("对象已序列化到 user.ser 文件中.");
} catch (IOException i) {
i.printStackTrace();
}
}
}

运行上述代码后,你会发现当前目录下生成了一个名为user.ser的文件,这就是序列化后的字节流。


反序列化对象


import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeDemo {
public static void main(String[] args) {
User user = null;

try (FileInputStream fileIn = new FileInputStream("user.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {

user = (User) in.readObject();
System.out.println("反序列化后的对象: " + user);
} catch (IOException | ClassNotFoundException i) {
i.printStackTrace();
}
}
}

运行这段代码,你会看到输出:


反序列化后的对象: User{name='Alice', age=30, password='null'}

注意到password字段为空,这是因为它被声明为transient,在序列化过程中被忽略了。


5. 常见问题与注意事项


serialVersionUID是干嘛的?


serialVersionUID是序列化时用来验证版本兼容性的一个标识符。如果你不显式定义它,Java会根据类的结构自动生成。但为了避免类结构变化导致序列化失败,建议手动定义一个固定的值。


继承关系中的序列化


如果一个类的父类没有实现Serializable接口,那么在序列化子类对象时,父类的字段不会被序列化。反序列化时,父类的构造函数会被调用初始化父类部分。


处理敏感信息


使用transient关键字可以防止敏感信息被序列化,比如密码字段。此外,你也可以自定义序列化逻辑,通过实现writeObjectreadObject方法来更精细地控制序列化过程。


6. 总结


本文,我们深入浅出地探讨了Java中的序列化和反序列化,从基本概念到原理分析,再到实际的代码示例,希望你对这两个重要的技术点有了更清晰的理解。


为什么需要序列化和反序列化?


最直白的说,如果不进行持久化和网络传输,根本不需要序列化和反序列化。如果需要实现持久化和网络传输,就必须序列化和反序列化,因为对象是应用层的东西,不同的语言(比如:java,go,python)创建的对象还不一样,实现持久化和网络传输的载体根本不认这些对象。


7. 交流学习


最后,把猿哥的座右铭送给你:投资自己才是最大的财富。 如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7499078331708932134
收起阅读 »

vue文件自动生成路由会成为主流

web
vue-router悄悄发布了5.0版本,用官方的话说,V5 是一个过渡版本,它将unplugin-vue-router(基于文件的路由)合并到了核心包中,就是说V5版本直接支持基于文件自动生成路由了,无需再引入unplugin-vue-router。 这一变...
继续阅读 »

vue-router悄悄发布了5.0版本,用官方的话说,V5 是一个过渡版本,它将unplugin-vue-router(基于文件的路由)合并到了核心包中,就是说V5版本直接支持基于文件自动生成路由了,无需再引入unplugin-vue-router


这一变化标志着前端开发模式的一个重要转折点。过去,开发者需要手动定义路由配置,这种方式虽然灵活,但随着项目规模增大,维护成本也随之增加。现在,Vue Router 5.0内置了基于文件的路由系统,使得路由管理变得更加直观和高效。


传统路由配置与基于文件路由的对比


在传统的Vue Router使用方式中,我们需要手动创建路由:


import { createRouter, createWebHistory } from "vue-router";
import Home from "./views/Home.vue";
import About from "./views/About.vue";

const routes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/about",
name: "about",
component: About,
},
];

const router = createRouter({
history: createWebHistory(),
routes,
});

而基于文件的路由系统允许我们通过目录结构自动生成路由,例如:


src/
├── pages/
│ ├── index.vue # -> /
│ ├── about.vue # -> /about
│ ├── user/
│ │ └── index.vue # -> /user
│ └── user-[id].vue # -> /user/:id

无需手动创建,直接导入即可:


import { routes } from "vue-router/auto-routes";

const router = createRouter({
history: createWebHistory(),
routes,
});

省去了手动定义路由的繁琐步骤。


基于文件路由的优势



  1. 减少样板代码:无需手动编写大量路由配置

  2. 约定优于配置:通过文件名和目录结构确定路由路径

  3. 提高开发效率:添加新页面只需创建对应文件

  4. 易于维护:路由结构一目了然,便于团队协作

  5. 类型化路由: 使用ts能够获得更好的提示,比如router.push(xxx)现在会有提示了


缺点



  1. 路由的meta等额外数据必须在.vue文件使用definePageroute标签声明,点此查看教程

  2. 增加了额外的学习成本


快速入门


安装


pnpm add vue-router@5

vite.config.ts


import VueRouter from "vue-router/vite";

export default defineConfig({
plugins: [
VueRouter({
dts: "typed-router.d.ts",
}),
// ⚠️ Vue must be placed after VueRouter()
Vue(),
],
});

tsconfig.json


// tsconfig.json
{
"include": [ "typed-router.d.ts" ],
"vueCompilerOptions": {
"plugins": [
"vue-router/volar/sfc-typed-router",
"vue-router/volar/sfc-route-blocks"
]
}
}

src/router/index.ts


import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";

export const router = createRouter({
history: createWebHistory(),
routes,
});

// This will update routes at runtime without reloading the page
if (import.meta.hot) {
handleHotUpdate(router);
}

详细的路由生成规则


根据官方文档,基于文件的路由系统有以下具体规则:


索引路由:任何 index.vue 文件(必须全小写)将生成空路径,类似于 index.html 文件:



  • src/pages/index.vue 生成 / 路由

  • src/pages/users/index.vue 生成 /users 路由


嵌套路由:当在同一层级同时存在同名文件夹和 .vue 文件时,会自动生成嵌套路由。例如:


src/pages/
├── users/
│ └── index.vue
└── users.vue

这将生成如下路由配置:


const routes = [
{
path: "/users",
component: () => import("src/pages/users.vue"),
children: [
{ path: "", component: () => import("src/pages/users/index.vue") },
],
},
];

不带布局嵌套的路由:有时候你可能想在URL中添加斜杠形式的嵌套,但不想影响UI层次结构。可以使用点号(.)分隔符:


src/pages/
├── users/
│ ├── [id].vue
│ └── index.vue
└── users.vue

要添加 /users/create 路由而不将其嵌套在 users.vue 组件内,可以创建 src/pages/users.create.vue 文件,. 会被转换为 /


const routes = [
{
path: "/users",
component: () => import("src/pages/users.vue"),
children: [
{ path: "", component: () => import("src/pages/users/index.vue") },
{ path: ":id", component: () => import("src/pages/users/[id].vue") },
],
},
{
path: "/users/create",
component: () => import("src/pages/users.create.vue"),
},
];

路由组:有时候需要组织文件结构而不改变URL。路由组允许你逻辑性地组织路由,不影响实际URL:


src/pages/
├── (admin)/
│ ├── dashboard.vue
│ └── settings.vue
└── (user)/
├── profile.vue
└── order.vue

生成的URL:



  • /dashboard -> 渲染 src/pages/(admin)/dashboard.vue

  • /settings -> 渲染 src/pages/(admin)/settings.vue

  • /profile -> 渲染 src/pages/(user)/profile.vue

  • /order -> 渲染 src/pages/(user)/order.vue


命名视图:可以通过在文件名后附加 @ + 名称来定义命名视图,如 src/pages/index@aux.vue 将生成:


{
path: '/',
component: {
aux: () => import('src/pages/index@aux.vue')
}
}

默认情况下,未命名的路由被视为 default,即使有其他命名视图也不需要将文件命名为 index@default.vue


动态路由:使用方括号语法定义动态参数:



  • [id].vue -> /users/:id

  • [category]-details.vue -> /electronics-details

  • [...all].vue -> 通配符路由 /all/*


对开发工作流的影响


这一变化将显著改变Vue应用的开发流程:



  • 新功能页面的添加变得更加简单

  • 团队成员更容易理解项目的路由结构

  • 减少了因手动配置错误导致的路由问题

  • 更好的IDE集成和自动补全支持


迁移策略


对于现有项目,Vue Router 5.0提供了平滑的迁移路径:



  • 旧的路由配置方式依然有效

  • 可以逐步采用基于文件的路由

  • 混合使用两种方式以适应不同场景


配置选项和高级功能


Vue Router 5.0的基于文件路由系统提供了丰富的配置选项,可以根据项目需求进行定制:


自定义路由目录:默认情况下,系统会在 src/pages 目录中查找 .vue 文件,但可以通过配置更改此行为。


命名路由:所有生成的路由都会自动获得名称属性,避免意外将用户引导至父路由。默认情况下,名称使用文件路径生成,但可以通过自定义 getRouteName() 函数覆盖此行为。


类型安全:系统会自动生成类型声明文件(如 typed-router.d.ts),提供几乎无处不在的 TypeScript 验证。


配置示例


// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueRouter from "unplugin-vue-router/vite";

export default defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages", // 自定义路由目录
extensions: [".vue"], // 指定路由文件扩展名
dts: "typed-router.d.ts", // 生成类型声明文件
importMode: (filename) => "async", // 自定义导入模式
}),
vue(),
],
});

实际应用建议


在实际项目中采用基于文件的路由时,建议遵循以下最佳实践:



  1. 清晰的目录结构:保持一致的目录结构,便于团队成员理解

  2. 有意义的文件名:使用描述性的文件名,使路由意图明确

  3. 合理使用路由组:利用路由组组织相关的页面,而不影响URL结构

  4. 渐进式采用:对于大型项目,可以逐步迁移部分路由到新的系统


总结


Vue Router 5.0引入的基于文件的路由系统代表了前端开发模式的重要演进。它将 Nuxt.js 等框架成功的路由理念整合到了 Vue 的核心生态中,使开发者能够以更简洁、更直观的方式管理应用路由。


这一变化不仅减少了样板代码,提高了开发效率,还促进了更一致的项目结构。随着更多开发者采用这一新模式,我们可以期待看到更高质量、更易维护的 Vue 应用程序出现,这将为整个前端社区带来积极的影响。


作者:ashuicoder
来源:juejin.cn/post/7610253888646119467
收起阅读 »

🦞OpenClaw 让 MacMini 脱销了,而我拿出了6年陈的安卓机

前言 OpenClaw 作为 AI 界 2026 年的新宠,第一波受益的居然是 Mac Mini,原因是 Mac Mini 功耗低,适合 7x24 小时运行。这就让我很不李姐,作为一个十年 Linux 手,我坚信只要能运行 Linux 的设备上就可以运行 Op...
继续阅读 »

前言


OpenClaw 作为 AI 界 2026 年的新宠,第一波受益的居然是 Mac Mini,原因是 Mac Mini 功耗低,适合 7x24 小时运行。这就让我很不李姐,作为一个十年 Linux 手,我坚信只要能运行 Linux 的设备上就可以运行 OpenClaw。像树莓派这种小众玩具就算了,我们的安卓手机也完全可以,这手机不比 Mac Mini 的功耗低吗,而且手机自带 UPS+移动网络,即使你家断电断网,都不影响 OpenClaw 为你干活。


好巧不巧我的 2019 年的包浆三星,昨晚音量键也坏了,这是完全没法用了,但是我觉得它的处理器还能打,这不正好适合干这个吗。


当我在安卓手机上安装了 Termux(终端模拟器) + nodejs + cmake 等古法炼丹的组件之后,意外发现这是 AI 时代了,GitHub 上已经有一个完善的项目openclaw-termux,它不仅提供了基于 Termux 环境的安装指令,还提供了一个 Flutter App,大大降低了安装门槛。下面将一步步介绍我的安装过程。


Step 0:准备工作


0.1 准备大模型API key


我选用的是 MiniMax,CodingPlan 可以无限 token 使用,实名认证之后也可以获得体验额度。



点击链接获得 CodingPlan 9 折优惠 platform.minimaxi.com/subscribe/c…



注意这里有两种 key,上面的接口密钥是消耗余额和体验券的,点击“创建新的 API key”。有CodingPlan的朋友点击“重置并复制”使用下面的key。


Image


0.2 创建聊天机器人


我用的是飞书,这个创建操作流程较长,没有操作过的朋友直接转到文末参考附录:创建飞书聊天机器人


0.3 安装 OpenClaw 安卓 App


github.com/mithun50/op…


安装之后打开 App,就进入了下面的界面。因为许多访问地址都是国外的,所以建议安装过程挂🪜,会比较顺利。这个界面视网络情况,可能需要等待比较久时间,我大概花了 15 分钟。 期间尽量让 app 保持在前台,防止被杀掉。


后面完成后会有两个可选安装项,不懂可以跳过。


Image


Step 1:运行配置向导


第一段是风险提示,默认选项是“No”,我们点下面的⬅️按钮选到“Yes”,并在键盘按“↩︎”键


Image


继续选择“QuickStart”,就来到最关键的模型配置了。


1.1  Model 模型配置


Image


国内的供应商可选的有 MiniMax 和 DeepSeek。MiniMax 可以看到专门的选项,但是 DeepSeek 没有,因为 DeepSeek 执行了 OpenAI 的标准,因此我们可以用 OpenAI 或者 Custom 选兼容 OpenAI 的方式。


关于这两个平台的对比,可以参考下图:


Image


这里我选择了 MiniMax,第二个 auth method 不要选 Oauth。然后提示输入 MiniMax 后台的 API key。模型选择见下图,建议使用-cn 后缀的,利于国内访问。


Image


1.2 Channel 会话频道配置


完成模型配置就来到了 Channel 配置,这里的 Channel 是指用什么即时通讯工具与 OpenClaw 交互,国内的只有飞书。我们就选飞书。


题外话是飞书的配置属于较复杂的,如果有条件优先使用 Telegram会比较简单。


Image
Image


这里就需要输入飞书的 AppID 以及 Secret 了。后文会介绍如何创建飞书机器人。


Image


1.3 Skill技能配置


配置完 Channel 之后,就来到了 Skills,建议配置 mcporter,MCP好比 OpenClaw 的手臂,能大大增强它做事的能力。其余技能按需即可。


Image


这里会有一些配置API KEY 的提示,通通选 No 即可,如果有需求,可以后配。


Image


再然后看到这个界面就告诉你配置结束了。


Image


Step 2:仪表盘界面


这个仪表盘界面最重要的功能是管理Gateway的状态,只有当Gateway运行状态时,OpenClaw 才可以正常工作。因此这里要点击“Start Gateway”。


Image


然后我们看到绿色的“运行中”就万事毕备了。


Image


Step 3:工作节点配置


点击 Node 进入工作节点配置,点击 Enable,会提示你进行各种授权,这也意味着 OpenClaw 可以接管你手机的相关能力,比如拍照,这都属于敏感操作,一定要想清楚再开。


Image


Step 4:飞书后台设置事件回调


如果你这个时候迫不及待的去找机器人聊天,你会发现连输入框都没有。我们需要继续配置事件回调。


Image


选择使用长连接接收事件:


Image


可以看到添加事件按钮由原来的灰色不可点击变为可点击:


Image


添加接收消息事件:


Image


给应用开通获取通讯录基本信息的权限:


Image


重新发布版本:


Image


跟前面的步骤一样,发布为在线应用即可。


现在可以看到飞书机器人有输入框了!


但是如果你发消息给机器人,会收到一条配对提示


Image


我们找到with:后面那一行指令到 OpenClaw App 主界面的Terminal中执行。这是唯一需要输入命令的地方。





openclaw pairing approve feishu A5T7L53A



附录:创建飞书聊天机器人


1. 来到飞书开发者后台


飞书开放平台地址:



没有飞书账号的,需要自己注册账号



点击右上角进入开发者后台:


Image


2. 创建应用


Image


3. 填写应用信息


Image


4. 获取自己的应用凭证


Image


5. 给应用添加机器人


Image
Image


6. 给应用配置权限


Image


把即时通讯相关的权限全部开通:


Image


7. 创建版本并发布


Image
Image
Image


如果当前账号不是管理员会多一个审批流程。


作者:西门老铁
来源:juejin.cn/post/7611386394679459874
收起阅读 »

为什么Django这么慢,却还是Python后端第一梯队呢?

学习Web框架的Python玩家大多应该都听过:Django 性能不行”、“高并发场景根本用不了 Django”。但有趣的是,在TIOBE、PyPI下载量、企业技术栈选型中,Django始终稳居Python后端框架第一梯队,甚至是很多公司的首选。 这背后的矛盾...
继续阅读 »

学习Web框架的Python玩家大多应该都听过:Django 性能不行”、“高并发场景根本用不了 Django”。但有趣的是,在TIOBE、PyPI下载量、企业技术栈选型中,Django始终稳居Python后端框架第一梯队,甚至是很多公司的首选。


这背后的矛盾,恰恰折射出工业级开发的核心逻辑:性能从来不是唯一的衡量标准,生产力和工程化能力才是


一、先澄清:Django 的“慢”,到底慢在哪?


首先要纠正一个认知偏差:Django的 “慢” 是相对的,而非绝对的。


1. 所谓“慢”的本质


Django被吐槽“慢”,主要集中在这几个点:



  • 全栈特性的代价:Django是“电池已内置”的全栈框架,ORM、表单验证、认证授权、Admin后台、缓存、国际化等功能开箱即用,这些内置组件会带来一定的性能开销,对比Flask、FastAPI这类轻量框架,纯接口响应速度确实稍慢(基准测试中,简单接口QPS约为FastAPI的1/3-1/2)。

  • 同步 IO 的天然限制:Django默认是同步架构,在高并发IO密集型场景(如大量请求等待数据库/第三方接口响应)下,线程/进程池容易被打满,吞吐量受限。

  • ORM 的 “便利税” :自动生成的SQL可能不够优化,新手容易写出N+1查询,进一步放大性能问题。


2. 但这些“慢”,大多是“伪问题”


绝大多数业务场景下,Django的性能完全够用:



  • 普通中小网站(日活10万以内):Django+合理缓存+数据库优化,能轻松支撑业务,性能瓶颈根本不在框架本身。

  • 所谓 “慢” 的对比场景:大多是“裸框架接口跑分”,而真实业务中,接口响应时间的80%以上消耗在数据库、缓存、网络IO上,框架本身的耗时占比不足5%。

  • 性能可优化空间大:通过异步改造(Django 3.2+原生支持ASGI)、缓存层(Redis)、数据库读写分离、CDN、Gunicorn+Nginx部署等方式,完全能把Django的性能提升到满足中高并发的水平。


二、Django能稳居第一梯队,核心是“降本增效”


企业选框架,本质是选“性价比”——开发效率、维护成本、团队协作成本,远比单点性能重要。而这正是Django的核心优势。


1. 极致的开发效率:“开箱即用” 的工业级体验


Django的设计哲学是 “不要重复造轮子”,一个命令就能生成完整的项目骨架,几行代码就能实现核心功能:


# 5行代码实现带权限的REST接口(Django+DRF)
from rest_framework import viewsets
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticated]


  • 内置Admin后台:无需写一行前端代码,就能实现数据的增删改查,调试和运营效率拉满。

  • 完善的认证授权:Session、Token、OAuth2等认证方式开箱即用,不用自己造权限轮子。

  • 表单验证&CSRF防护:自动处理表单校验、跨站请求伪造,减少安全漏洞。

  • ORM的价值:虽然有性能损耗,但大幅降低了数据库操作的学习成本和出错概率,新手也能快速写出安全的数据库逻辑。


对于创业公司或快速迭代的业务,“快上线、少踩坑”比“多10%的性能” 重要得多——Django能让团队用最少的人力,在最短时间内搭建起稳定的业务系统。


2. 成熟的工程化体系:适合团队协作


个人项目可以用Flask自由发挥,但团队项目需要“规范”。Django的“约定优于配置”理念,强制规范了项目结构、代码组织、数据库迁移等流程:



  • 统一的项目结构:新人接手项目,不用花时间理解自定义的目录结构,直接就能上手。

  • 内置的数据库迁移工具:makemigrations/migrate 完美解决数据库版本管理问题,避免团队协作中的数据结构混乱。

  • 丰富的中间件和扩展生态:缓存中间件、跨域中间件、日志中间件等开箱即用,DRF(Django REST Framework)、Celery、Django Channels等扩展几乎能覆盖所有业务场景。

  • 完善的文档和社区:官方文档堪称 “教科书级别”,遇到问题能快速找到解决方案,招聘时也更容易找到有经验的开发者。


3. 稳定可靠:经得起生产环境的考验


Django 诞生于2005年,经过近20年的迭代,已经成为一个极其稳定的框架:



  • 长期支持版本(LTS):每2-3年发布一个LTS版本,提供3年以上的安全更新和bug修复,企业不用频繁升级框架。

  • 安全特性完善:自动防御XSS、CSRF、SQL注入等常见攻击,官方会及时修复安全漏洞,这对企业来说是“刚需”。

  • 大量知名案例背书:Instagram、Pinterest、Mozilla、Spotify、国内的知乎(早期)、豆瓣等,都在用 Django支撑核心业务——这些产品的规模,足以证明Django的可靠性。


三、Django 的 “破局之路”:性能短板正在被补齐


面对性能吐槽,Django团队也一直在迭代优化:



  • 异步支持:Django 3.0引入ASGI,3.2+完善异步视图、异步ORM,能直接对接WebSocket、长连接,IO密集型场景的并发能力大幅提升。

  • 性能优化:新版本持续优化ORM、模板引擎、中间件,减少不必要的开销,比如Django 4.0+的ORM支持批量更新/插入,性能提升显著。

  • 生态适配:可以和FastAPI混合部署(比如核心接口用FastAPI,管理后台用Django),兼顾性能和生产力;也可以通过Gunicorn+Uvicorn+异步工作进程,充分利用多核CPU。


四、总结:选框架,本质是选 “适配性”


Django的 “慢”,是为 “全栈、工程化、生产力” 付出的合理代价;而它能稳居第一梯队,核心原因是:



  1. 匹配绝大多数业务场景:90%的中小业务不需要 “极限性能”,但都需要 “快速开发、稳定运行、易维护”。

  2. 降低团队成本:统一的规范、丰富的内置功能、完善的文档,能大幅降低招聘、培训、协作成本。

  3. 生态和稳定性兜底:成熟的生态能解决几乎所有业务问题,长期支持版本让企业不用频繁重构。


最后想说:框架没有好坏,只有适配与否。如果是做高并发的API服务(如直播、秒杀),FastAPI/Tornado 可能更合适;但如果是做内容管理、电商、企业后台等需要快速落地、长期维护的业务,Django依然是Python后端的最优解之一。


这也是为什么,即便有层出不穷的新框架,Django依然能稳坐第一梯队——因为它抓住了工业级开发的核心:让开发者把精力放在业务上,而非重复造轮子


作者:Sheffield
来源:juejin.cn/post/7606182668330303524
收起阅读 »

别吹了,AI写Java代码到底能省多少时间?一个后端仔的真实记录

场景一:CRUD 生成 | 提效 70-80% | ⭐⭐⭐⭐⭐ 这是 AI 最能打的场景,没有之一。 传统写法:手写 Entity → Mapper XML → Service → ServiceImpl → Controller,一套标准的增删改查,从建表到...
继续阅读 »

场景一:CRUD 生成 | 提效 70-80% | ⭐⭐⭐⭐⭐


这是 AI 最能打的场景,没有之一。


传统写法:手写 Entity → Mapper XML → Service → ServiceImpl → Controller,一套标准的增删改查,从建表到联调,至少 40 分钟。如果加上参数校验、分页查询、统一返回体,一个小时也不夸张。


用 AI 之后:给 Cursor 一段需求描述加上数据库表结构,5-10 分钟出完整代码。而且生成的代码结构很规范——注解没漏、字段映射没错、甚至连 Swagger 文档注解都给你加上了。


但别高兴太早。


我踩过的坑:AI 生成的 CRUD 代码表面上能跑,细节经常有问题。举几个典型的:



  • 字段校验粗糙——@NotNull 加了,但 @Size@Pattern 这种业务校验不会主动加

  • 分页查询用的是内存分页而不是数据库分页(这个坑特别隐蔽)

  • 异常处理一律 catch Exception,没有细分业务异常


所以我的做法是:让 AI 先生成骨架,然后自己过一遍核心逻辑。这样比从零手写快得多,又不会因为盲信 AI 埋雷。



CRUD 是 AI 的主场,这事没争议。但你还是得 review 每一行——AI 写的 bug 比你手写的更隐蔽。





场景二:Bug 排查 | 提效 40-60% | ⭐⭐⭐⭐


先说好用的部分。


一个 NullPointerException,传统排查流程:看堆栈 → 找到报错行 → 往上追调用链 → 打断点 → 复现 → 定位 → 修复。顺利的话 15 分钟,不顺利半小时起步。


现在我直接把报错日志和相关代码丢给 Claude,通常 2-3 分钟就能给出准确定位。不仅告诉你哪里报错,还能分析为什么会走到这个分支。效率差距是数量级的。


SQL 慢查询也类似。把 EXPLAIN 结果丢给 AI,它能分析出索引缺失、全表扫描、JOIN 顺序不合理等问题,给出的优化建议大部分是靠谱的。


但是。


复杂业务逻辑的 bug——比如跨服务的数据不一致、分布式事务异常、线程安全问题——AI 基本帮不上忙。原因很简单:它不了解你的业务上下文,不知道你的系统架构长什么样,不清楚各个服务之间的调用关系。


你可以把全部代码都喂给它,但在上下文窗口有限的情况下,效果也很一般。



AI debug 像个经验丰富的实习生——标准问题秒解,但真正棘手的问题还得靠你自己。





场景三:代码重构 | 提效 30-50% | ⭐⭐⭐


这个场景我体验比较复杂,分层来说。


方法级重构——好用。 提取公共方法、消除重复代码、简化多层嵌套的 if-else,这些 AI 干得又快又好。你说一句「把这段 if-else 用策略模式重构」,它真能给你一套结构清晰的策略类出来。


模块级重构——凑合。 比如给一个老旧的 Service 类做职责拆分,AI 能给出拆分方案,但命名经常不太合适,边界划分也需要自己调整。它不太懂你的团队命名规范,也不清楚哪些逻辑在你的系统里属于同一个领域。


架构级重构——别想了。 单体拆微服务、数据库分库分表这种事,涉及太多业务决策和技术权衡,AI 给的方案顶多算个参考。你要是照着它的方案直接改,后果自负。



重构的本质是对业务的理解。AI 只能帮你搬砖,不能帮你画图纸。





场景四:写单元测试 | 提效 50-70% | ⭐⭐⭐⭐


Java 程序员最不爱写但又不能不写的东西——单元测试。


传统写法:一个 Service 方法的单测,先 mock 一堆依赖,再写正常返回用例、空值用例、异常用例,30-40 分钟很正常。而且坦白说,很多人(包括我)写单测就是为了应付覆盖率检查,质量堪忧。


现在我把 Service 代码丢给 AI,说一句「用 JUnit 5 + Mockito 生成单元测试,覆盖正常和异常场景」,3-5 分钟出一整套。


举个真实例子。比如有这么一个订单查询方法:


public OrderDetailVO getOrderDetail(Long orderId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new BizException(ErrorCode.ORDER_NOT_FOUND);
}
// 查关联的商品信息
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
OrderDetailVO vo = OrderConverter.toDetailVO(order);
vo.setItems(OrderConverter.toItemVOList(items));
vo.setTotalAmount(calcTotalAmount(items));
return vo;
}

AI 生成的测试长这样(节选关键部分):


@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

@Mock
private OrderMapper orderMapper;
@Mock
private OrderItemMapper orderItemMapper;
@InjectMocks
private OrderServiceImpl orderService;

@Test
@DisplayName("查询存在的订单-返回完整详情")
void getOrderDetail_success() {
Order mockOrder = buildMockOrder(1001L);
List<OrderItem> mockItems = buildMockItems(1001L, 2);

when(orderMapper.selectById(1001L)).thenReturn(mockOrder);
when(orderItemMapper.selectByOrderId(1001L)).thenReturn(mockItems);

OrderDetailVO result = orderService.getOrderDetail(1001L);

assertNotNull(result);
assertEquals(1001L, result.getOrderId());
assertEquals(2, result.getItems().size());
verify(orderMapper).selectById(1001L);
}

@Test
@DisplayName("查询不存在的订单-抛出业务异常")
void getOrderDetail_notFound() {
when(orderMapper.selectById(9999L)).thenReturn(null);

BizException ex = assertThrows(BizException.class,
() -> orderService.getOrderDetail(9999L));
assertEquals(ErrorCode.ORDER_NOT_FOUND, ex.getErrorCode());
}
}

说实话,这个质量是能直接用的。Mock 层级对、断言清晰、方法命名规范。如果手写,这两个测试方法至少要 15 分钟,AI 生成只要 2 分钟。


但我手动补了什么呢?


@Test
@DisplayName("订单存在但商品列表为空-金额应为0")
void getOrderDetail_emptyItems() {
when(orderMapper.selectById(1001L)).thenReturn(buildMockOrder(1001L));
when(orderItemMapper.selectByOrderId(1001L)).thenReturn(Collections.emptyList());

OrderDetailVO result = orderService.getOrderDetail(1001L);

assertEquals(BigDecimal.ZERO, result.getTotalAmount());
assertTrue(result.getItems().isEmpty());
}

这种业务边界用例——商品列表为空时金额计算是否正确——AI 不会主动想到,因为它不知道 calcTotalAmount 对空列表的处理逻辑是不是你期望的。


AI 生成测试的优点:



  • Mockito 用得很熟练,@Mock@InjectMockswhen().thenReturn() 一气呵成

  • 正常路径和基本异常覆盖得不错

  • 断言写得清晰,方法命名规范


缺点也明显:



  • 边界条件经常遗漏——并发场景、数据库连接异常、超时这种不会主动想到

  • 有时 mock 的层级不对——该 mock Repository 的地方 mock 了 Service

  • 对业务语义理解有限——不知道空列表、金额为 0 这种场景需不需要专门测


我现在的套路是:让 AI 先生成 80% 的测试用例,自己再补上那 20% 的关键边界。这比从零手写效率高太多了。



80/20 法则在 AI 写单测这件事上体现得淋漓尽致。





场景五:写技术文档 | 提效 80-90% | ⭐⭐⭐⭐⭐


这是我觉得最被低估的 AI 提效场景。


Java 程序员写代码还行,写文档真的要命。一份接口文档写半天,一份方案设计磨一整天,部署文档更是能拖就拖。


举个例子。前阵子我写了一个用户收货地址的 Controller,大概长这样:


@PostMapping("/address/add")
public Result<Long> addAddress(@RequestBody @Valid AddressAddReq req) { ... }

@GetMapping("/address/list")
public Result<List<AddressVO>> listAddress() { ... }

@PutMapping("/address/update")
public Result<Void> updateAddress(@RequestBody @Valid AddressUpdateReq req) { ... }

@DeleteMapping("/address/{id}")
public Result<Void> deleteAddress(@PathVariable Long id) { ... }

把这段代码丢给 AI,说「生成接口文档,Markdown 格式,包含请求参数、返回值、错误码」,2 分钟后它给我的文档(节选):


POST /address/add — 新增收货地址


参数类型必填说明
receiverNameString收货人姓名,2-20个字符
phoneString手机号,11位
provinceString省份
cityString城市
districtString区/县
detailAddressString详细地址,最长200字符
isDefaultInteger是否默认地址,0-否 1-是,默认0

返回值: Result<Long> — 新地址ID


错误码:



  • ADDRESS_LIMIT_EXCEEDED — 收货地址数量超过上限(20个)

  • PARAM_INVALID — 参数校验失败


它连字段的校验规则都从 @Valid 注解里推断出来了。手写这份文档至少要 30 分钟,现在 2 分钟生成,再花 5 分钟检查补充,总共不到 10 分钟。


方案设计文档也一样。给出需求描述和技术方案要点,AI 能在 10 分钟内生成一份结构完整的设计文档——背景、目标、方案对比、技术选型、风险评估,该有的都有。当然具体技术细节需要自己补,但框架已经省了你 80% 的时间。


README 就更不用说了,几乎不用改,直接能用。



如果你还在手写文档,那你是真的在浪费生命。这话可能有点绝对,但我是认真的。





我的真实结论


一张表总结


场景提效幅度推荐度一句话
CRUD 生成70-80%⭐⭐⭐⭐⭐AI 的主场,没争议
Bug 排查40-60%⭐⭐⭐⭐标准问题秒杀,复杂问题不行
代码重构30-50%⭐⭐⭐能搬砖不能画图纸
单元测试50-70%⭐⭐⭐⭐AI 出 80% + 人工补 20%
技术文档80-90%⭐⭐⭐⭐⭐最被低估的提效场景

关于「效率反降 19%」


最近那个「AI 写代码效率反降 19%」的研究火了,很多人拿来当 AI 没用的证据。但你仔细看就会发现:研究用的是大型开源项目(平均 110 万行代码),上下文极其复杂。


日常业务开发完全不是这个量级。我们的代码库上下文更可控,需求也更明确。在这种条件下,AI 发挥的空间大得多。


说白了,关键不是 AI 行不行,而是你选对了场景没有。用 AI 写 CRUD 能省 70% 时间,你非要让它帮你做架构设计——那确实会「提效为负」。


我的使用原则



  1. AI 擅长的事全交给它:CRUD、文档、测试框架、标准 bug 排查

  2. AI 不擅长的事别勉强:复杂业务逻辑、架构设计、跨服务问题

  3. 永远 review:AI 写的代码比你手写的更需要审查——因为它写的 bug 更隐蔽

  4. 积累自己的 Prompt:好的 Prompt 比好的工具更重要




写在最后


我是栈外,写了几年 Java,现在用 AI 多赚一份钱。


不会告诉你用 AI 就能月入十万。但它确实能让你每天省出 1-2 小时——前提是你用对场景。


下一篇聊聊这省出来的时间,我拿来干了什么。省下来的时间如果不变成钱,那提效就是个伪命题。


如果觉得有用,点个赞收个藏,后面还有更硬的干货。





这是「栈外」的第 1 篇文章。不画饼,只算账。



作者:栈外
来源:juejin.cn/post/7610580125618929690
收起阅读 »

一个 Java 老兵转 Go 后,终于理解了“简单”的力量

之前写的文章《信不信?一天让你从Java工程师变成Go开发者》很受关注,很多读者对 Go 的学习很感兴趣。今天就再写一篇,聊聊 Java 程序员写 Go 时最常见的思维误区。 核心观点: Go 不需要 Spring 式的依赖注入框架,因为它的设计哲学是"显式优...
继续阅读 »

之前写的文章《信不信?一天让你从Java工程师变成Go开发者》很受关注,很多读者对 Go 的学习很感兴趣。今天就再写一篇,聊聊 Java 程序员写 Go 时最常见的思维误区。


核心观点: Go 不需要 Spring 式的依赖注入框架,因为它的设计哲学是"显式优于隐式"。手动构造依赖看似啰嗦,实则更清晰、更快、更易调试。


从 Java 转 Go,第一天就会被这个问题困扰:"@Autowired 在哪?依赖注入框架用哪个?IoC 容器怎么配?"


答案很直接:Go 里没有,也不需要。 不是 Go 做不到,而是 Go 压根不想这么干。这不是功能缺失,而是设计哲学的根本性差异。




第一反应:Go 怎么这么"原始"?


刚开始写 Go,看到的代码是这样的:


func main() {
// 手动创建数据库连接
db := NewDB("localhost:3306", "user", "password")

// 手动创建各种 Service
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)

paymentSvc := NewPaymentService(db)
inventorySvc := NewInventoryService(db)

orderSvc := NewOrderService(
orderRepo,
paymentSvc,
inventorySvc,
)

userSvc := NewUserService(userRepo)

// 手动创建 HTTP Handler
handler := NewHandler(orderSvc, userSvc)

// 启动服务
http.ListenAndServe(":8080", handler)
}

第一反应:这种写法让人想起早期的 Java 或 PHP


在 Java 里,这些全是框架干的事:


@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// 就这一行,所有对象都帮你创建好了
}
}

@RestController
public class OrderController {
@Autowired
private OrderService orderService;

// 框架自动注入,你根本看不到对象怎么创建的
}

Java 开发者心里 OS:



  • "Go 是不是太简陋了?"

  • "难道要我手动 new 几十个对象?"

  • "这不是倒退吗?"


先别急着下结论,听我说完。




为什么 Go 要这么"原始"?


Go 的设计哲学就一句话:



显式优于隐式,简单优于复杂。



这不是口号,而是实实在在的取舍。


对比1:依赖是怎么传递的?


Java/Spring 的做法:


// 你写这个
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;

@Autowired
private InventoryService inventoryService;

@Autowired
private NotificationService notificationService;
}

// 框架在背后做了:
// 1. 扫描所有类
// 2. 分析依赖关系
// 3. 构建依赖图
// 4. 按顺序创建对象
// 5. 通过反射注入字段
// 6. 处理循环依赖
// 7. 管理生命周期

这些魔法看起来很方便,但:



  • 你不知道对象什么时候创建的

  • 你不知道注入顺序是什么

  • 出问题了,调试要靠猜

  • 启动慢(要扫描、要反射)

  • 内存大(要维护容器)


Go 的做法:


type OrderService struct {
paymentSvc *PaymentService
inventorySvc *InventoryService
notificationSvc *NotificationService
}

func NewOrderService(
paymentSvc *PaymentService,
inventorySvc *InventoryService,
notificationSvc *NotificationService,
)
*OrderService {
return &OrderService{
paymentSvc: paymentSvc,
inventorySvc: inventorySvc,
notificationSvc: notificationSvc,
}
}

// 在 main 里
paymentSvc := NewPaymentService(db)
inventorySvc := NewInventoryService(db)
notificationSvc := NewNotificationService(queue)

orderSvc := NewOrderService(
paymentSvc,
inventorySvc,
notificationSvc,
)

这些代码看起来很啰嗦,但:



  • 你清楚地看到每个对象怎么创建的

  • 你清楚地看到依赖关系是什么

  • 出问题了,一眼就能定位

  • 启动快(没有扫描、没有反射)

  • 内存小(没有容器)


对比2:遇到问题怎么调试?


Java/Spring 遇到问题:


报错:Could not autowire. No beans of 'PaymentService' type found.

你要做的:
1. 检查 PaymentService 有没有 @Service
2. 检查包扫描路径对不对
3. 检查有没有循环依赖
4. 检查 @Conditional 条件是否满足
5. 检查配置文件有没有禁用
6. Google 半天
7. 还不行,看 Spring 源码

根本原因可能是:配置文件里有个 typo

Go 遇到问题:


编译报错:undefined: paymentSvc

你要做的:
1. 看报错的那一行
2. 发现没有传 paymentSvc 参数
3. 改完,搞定

5 秒钟解决

对比3:新人上手难度


Java/Spring 新人:


"这个对象哪来的?"
"@Autowired@Resource 有什么区别?"
"为什么我的 Bean 没有注入?"
"循环依赖怎么解决?"
"什么是 BeanPostProcessor?"

要学的概念:
- IoC 容器
- 依赖注入
- Bean 生命周期
- AOP
- 代理模式
- ...

Go 新人:


"这个对象哪来的?"
"看 main 函数,就是在那 New 出来的。"
"哦,明白了。"

要学的概念:
- 函数
- 指针



Go 为什么说自己"像脚本语言"?


Go 的设计目标就是:



写起来像脚本语言一样简单,跑起来像编译型语言一样快。



什么叫"像脚本语言"?


PHP 的写法:


<?php
// 直接开始写逻辑
$db = new PDO('mysql:host=localhost', 'user', 'pass');
$userRepo = new UserRepository($db);
$user = $userRepo->find(1);
echo $user->name;

Python 的写法:


# 直接开始写逻辑
db = connect_db('localhost', 'user', 'pass')
user_repo = UserRepository(db)
user = user_repo.find(1)
print(user.name)

Go 的写法:


func main() {
// 直接开始写逻辑
db := NewDB("localhost", "user", "pass")
userRepo := NewUserRepository(db)
user := userRepo.Find(1)
fmt.Println(user.Name)
}

看出来了吗?Go 就是想让你像写脚本一样写代码。


不需要:



  • 复杂的配置文件

  • 注解魔法

  • 框架黑盒

  • 反射黑魔法


只需要:



  • 创建对象

  • 调用方法

  • 传递参数


但是,它不是脚本语言:



  • 有强类型检查(写错了编译不过)

  • 编译成二进制(部署一个文件)

  • 性能接近 C(比 Java 快很多)

  • 启动秒开(没有 JVM 预热)




这种差异带来的实际影响


理论说完了,看看实际项目中的差异。


场景1:启动速度


Java/Spring 项目:


启动流程:
1. JVM 启动(1-2秒)
2. 加载类(2-3秒)
3. 扫描注解(3-5秒)
4. 构建依赖图(2-3秒)
5. 初始化 Bean(5-10秒)
6. AOP 代理(2-3秒)

总计:15-30秒

项目大了:1-2分钟

Go 项目:


启动流程:
1. 执行 main 函数
2. 创建对象
3. 启动服务

总计:0.1-0.5秒

项目再大:也就几秒

这就是为什么 Go 适合做 CLI 工具、K8s 组件:启动快


场景2:内存占用


Java/Spring 项目:


启动后内存:
- JVM 基础:100-200MB
- Spring 容器:50-100MB
- 对象缓存:100-200MB

最小内存:300-500MB
实际运行:1-2GB

Go 项目:


启动后内存:
- 没有虚拟机
- 没有容器
- 只有你创建的对象

最小内存:10-20MB
实际运行:50-200MB

这就是为什么 Go 适合做微服务、容器应用:省资源


场景3:调试体验


Java/Spring 遇到空指针:


// 报错
NullPointerException at OrderService.process()

// 原因可能是:
1. paymentService 没有注入成功
2. 某个 @Conditional 条件不满足
3. 循环依赖导致代理失败
4. 配置文件写错了

// 排查过程:
- 看日志,找不到原因
- 打断点,发现字段是 null
- Google,找到类似问题
- 尝试各种方案
- 1小时后,发现是配置文件拼写错误

Go 遇到空指针:


// 报错
panic: runtime error: invalid memory address

// 看代码
orderSvc := NewOrderService(
paymentSvc,
nil, // 这里忘了传
notificationSvc,
)

// 排查过程:
- 看报错行号
- 看代码
- 发现 nil
- 改完,搞定

// 5 秒钟解决



Java 开发者常犯的错误


看几个 Java 开发者写 Go 时常犯的错误。


错误1:找依赖注入框架


错误想法:
"Go 的依赖注入框架哪个好?Wire?Dig?"

正确做法:
别找了,手动传参就够了

有些 Go 项目确实用了 Wire、Dig,但那是因为:



  • 项目太大(100+ 个 Service)

  • 自动生成代码,减少重复


大部分项目,手动传参就够了。


错误2:过度抽象


// 错误做法:照搬 Java 那套
type ServiceFactory interface {
CreateUserService() UserService
CreateOrderService() OrderService
}

type ServiceFactoryImpl struct {
db *DB
}

func (f *ServiceFactoryImpl) CreateUserService() UserService {
return NewUserService(f.db)
}

// 正确做法:直接创建
func main() {
db := NewDB()
userSvc := NewUserService(db)
orderSvc := NewOrderService(db)
}

错误3:到处用接口


// 错误做法:每个 struct 都配个 interface
type UserService interface {
GetUser(id int) (*User, error)
}

type UserServiceImpl struct {
repo *UserRepository
}

// 正确做法:需要 mock 时才定义 interface
type UserService struct {
repo *UserRepository
}

// 测试时才定义
type UserRepository interface {
Find(id int) (*User, error)
}

Go 的接口是隐式实现的,不需要到处声明。


错误4:配置文件过度使用


# 错误做法:把所有配置都写 YAML
database:
host: localhost
port: 3306
user: root

services:
user:
enabled: true
timeout: 5s
order:
enabled: true
timeout: 10s

// 正确做法:代码即配置
func main() {
db := NewDB("localhost:3306", "root", "password")

userSvc := NewUserService(db, 5*time.Second)
orderSvc := NewOrderService(db, 10*time.Second)
}

Go 的理念是:代码就是最好的配置




什么时候该用依赖注入框架?


话说回来,真的完全不需要 DI 框架吗?也不是。


适合手动传参的场景(大部分情况)


小型项目(<50 个组件)


// 清晰、直接、易调试
func main() {
db := NewDB()
cache := NewCache()

userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)

userSvc := NewUserService(userRepo, cache)
orderSvc := NewOrderService(orderRepo, userSvc)

// 10-20 个组件,完全可控
}

中型项目(50-100 个组件)


// 可以考虑分组管理
type Services struct {
User *UserService
Order *OrderService
// ...
}

func InitServices(db *DB) *Services {
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)

return &Services{
User: NewUserService(userRepo),
Order: NewOrderService(orderRepo),
}
}

适合用 DI 框架的场景


大型微服务(>100 个组件)


当你的项目有 100+ 个 Service、Repository、Client 时,手动传参确实会很繁琐。这时可以考虑:


Wire(Google 官方推荐)



  • 编译时生成代码,不是运行时反射

  • 性能无损耗

  • 类型安全

  • 适合大型项目


// wire.go
//go:build wireinject

func InitializeApp() (*App, error) {
wire.Build(
NewDB,
NewUserRepository,
NewUserService,
NewApp,
)
return nil, nil
}

// wire 会自动生成代码

Dig(Uber 出品)



  • 运行时依赖注入

  • 更灵活,但有性能开销

  • 适合需要动态配置的场景


判断标准:


组件数 < 50 个      → 手动传参
组件数 50-100 个 → 手动传参 + 分组管理
组件数 > 100 个 → 考虑 Wire
需要插件化/动态加载 → 考虑 Dig

CLI 工具/脚本类应用 → 绝对不需要 DI 框架

记住一个原则:

不要为了"看起来像企业级架构"而引入 DI 框架。大部分 Go 项目,手动传参就够了。




澄清一个误解:Go 不是"反对抽象"


看到这里,有些人可能会想:"Go 这么简单粗暴,是不是就是写面条代码?"


不是的。


Go 的设计哲学不是"反对抽象",而是**"反对过早抽象、反对过度抽象"**。


Go 鼓励的抽象方式


1. 需要解耦时才引入接口


// 错误做法:提前抽象
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(user *User) error
}

type UserServiceImpl struct { }

// 正确做法:需要 mock 时才抽象
type UserService struct {
repo UserRepository // 这里才用接口
}

type UserRepository interface {
Find(id int) (*User, error)
Save(user *User) error
}

2. 真正需要多态时才用接口


// 有多个实现时才抽象
type Storage interface {
Save(key string, value []byte) error
Load(key string) ([]byte, error)
}

// 文件存储实现
type FileStorage struct { }

// Redis 存储实现
type RedisStorage struct { }

// S3 存储实现
type S3Storage struct { }

Go 的理念是:



  • 先用具体类型写代码

  • 发现真正需要抽象时(测试、多实现),再引入接口

  • 不要为了"看起来专业"而提前抽象


这不是反对抽象,而是在正确的时机做正确的事。




什么时候该用框架?


话说回来,难道 Go 就完全不需要框架了?


也不是。


适合用框架的场景


1. HTTP 路由:Gin、Echo


// 标准库的 http.ServeMux 太简陋
// 用 Gin 处理路由、中间件更方便

r := gin.Default()
r.GET("/users/:id", getUser)
r.POST("/orders", createOrder)

2. ORM:GORM


// 标准库的 database/sql 写 SQL 太麻烦
// 用 GORM 处理关联查询更方便

db.Where("age > ?", 18).Find(&users)

3. 配置管理:Viper


// 管理多环境配置
viper.SetConfigName("config")
viper.ReadInConfig()

不适合用框架的场景


1. 依赖注入


不需要 Wire、Dig,手动传参就够了。


2. 业务逻辑


不要用框架包装业务逻辑,直接写代码。


3. 简单功能


不要为了"看起来专业"而引入框架。




给 Java 开发者的建议


如果你是 Java 开发者,开始写 Go,记住这几点:


1. 忘掉 Spring 那套


别想着:
- 在哪配置注解
- 怎么注入依赖
- 怎么用 AOP

直接写代码就行

2. 拥抱"啰嗦"


Java 开发者看 Go:
"怎么要手动 new 这么多对象?太啰嗦了!"

写一段时间后:
"原来清晰明了比简洁更重要。"

3. 代码即文档


Java 项目:
- 要看 XML 配置
- 要看注解定义
- 要看框架文档

Go 项目:
- 看 main 函数
- 看 NewXXX 函数
- 看代码就够了

4. 简单优于复杂


遇到问题:
第一反应不是"找个框架"
而是"能不能写100行代码搞定"

90%的情况,100行代码就够了



一个实际例子


最后用一个例子,对比一下两种风格。


场景:订单服务


需要:



  • 数据库操作

  • 支付服务调用

  • 库存服务调用

  • 通知服务调用


Java/Spring 实现


// Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

// OrderController.java
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;

@PostMapping
public Order create(@RequestBody CreateOrderRequest req) {
return orderService.create(req);
}
}

// OrderService.java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;

@Autowired
private PaymentService paymentService;

@Autowired
private InventoryService inventoryService;

@Autowired
private NotificationService notificationService;

public Order create(CreateOrderRequest req) {
// 业务逻辑
}
}

配置文件 application.yml:


spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: password

代码文件: 4个

配置文件: 1个

看起来: 很简洁

实际运行: 一堆魔法


Go 实现


// main.go
func main() {
// 创建依赖
db := NewDB("localhost:3306", "root", "password")
defer db.Close()

orderRepo := NewOrderRepository(db)
paymentSvc := NewPaymentService()
inventorySvc := NewInventoryService()
notificationSvc := NewNotificationService()

orderSvc := NewOrderService(
orderRepo,
paymentSvc,
inventorySvc,
notificationSvc,
)

// 创建 HTTP Handler
r := gin.Default()
r.POST("/orders", func(c *gin.Context) {
var req CreateOrderRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

order, err := orderSvc.Create(req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}

c.JSON(200, order)
})

// 启动服务
r.Run(":8080")
}

// order_service.go
type OrderService struct {
orderRepo *OrderRepository
paymentSvc *PaymentService
inventorySvc *InventoryService
notificationSvc *NotificationService
}

func NewOrderService(
orderRepo *OrderRepository,
paymentSvc *PaymentService,
inventorySvc *InventoryService,
notificationSvc *NotificationService,
)
*OrderService {
return &OrderService{
orderRepo: orderRepo,
paymentSvc: paymentSvc,
inventorySvc: inventorySvc,
notificationSvc: notificationSvc,
}
}

func (s *OrderService) Create(req CreateOrderRequest) (*Order, error) {
// 业务逻辑
}

代码文件: 2个

配置文件: 0个

看起来: 有点啰嗦

实际运行: 一目了然




最后的思考:从 Spring 到 main(),是一次思维升级


从 Java 转 Go,最大的障碍不是语法,而是思维方式。


Java/Spring 的思维:



  • 框架帮你管理一切

  • 抽象层次越高越好

  • 配置优于代码

  • "我不需要知道对象怎么创建的,框架会处理"


Go 的思维:



  • 你自己管理一切

  • 简单直接就够了

  • 代码即配置

  • "我清楚地知道每个对象是怎么来的"


这不是谁对谁错,而是不同的设计哲学,适合不同的场景。


给 Java 开发者的建议


如果你从 Java 转 Go,记住这几点:


1. 拥抱"啰嗦",它带来的是清晰


刚开始:
"怎么要手动 new 这么多对象?太麻烦了!"

一个月后:
"原来看一眼 main 函数就知道整个系统是怎么组装的。"

2. 别急着找"Go 的 Spring"


Go 生态里有很多框架,但:
- 不要为了"看起来专业"而引入框架
- 不要为了"企业级架构"而过度设计
- 先写代码解决问题,再考虑是否需要框架

3. 代码即文档


Java 项目理解成本:
- 看配置文件
- 看注解定义
- 看框架文档
- 猜测对象是怎么创建的

Go 项目理解成本:
- 看 main 函数
- 看 NewXXX 函数
- 就这么简单

4. 简单优于复杂


遇到问题时:
第一反应不是"有没有框架能解决"
而是"能不能写 100 行代码搞定"

90% 的情况,100 行代码就够了

从 Spring 到 main(),不是倒退,而是升级


你失去的是:



  • 自动注入的"魔法"

  • 复杂的抽象层次

  • 庞大的框架依赖


你获得的是:



  • 对系统的完全掌控

  • 清晰可见的执行流程

  • 快速的启动和调试

  • 简单直接的代码组织


这不是倒退,而是一次返璞归真的旅程


最后的鼓励


从 Spring 的"魔法"转到 Go 的"手工",一开始可能会不适应。


你可能会觉得:



  • "怎么这么原始?"

  • "怎么要写这么多重复代码?"

  • "没有框架怎么办?"


但坚持一周,你会发现:



  • 代码更清晰了

  • 调试更简单了

  • 启动更快了

  • 部署更轻了


再过一个月,当你回头看 Spring 项目时,你会想:



  • "这个对象是怎么创建的?"

  • "这个注解背后做了什么?"

  • "为什么启动要 30 秒?"


那时候,你就真正理解了 Go 的设计哲学。


记住:



Go 的哲学是:显式优于隐式,简单优于复杂。

从 Spring 到 main(),你失去的是魔法,获得的是掌控。



适应这个哲学,你就适应了 Go。


欢迎来到 Go 的世界。


就这样。


作者:踏浪无痕
来源:juejin.cn/post/7587712328826224676
收起阅读 »

告别满屏 v-if:用一个自定义指令搞定 Vue 前端权限控制

web
在企业级应用开发中,权限控制是一个绑不开的话题。前端权限控制虽然不能替代后端校验,但能极大提升用户体验——让用户只看到自己能操作的内容,避免无效点击和困惑。 本文将分享一个 Vue 2 自定义指令的设计思路,实现了声明式的权限控制方案。 设计目标 在动手写代码...
继续阅读 »

在企业级应用开发中,权限控制是一个绑不开的话题。前端权限控制虽然不能替代后端校验,但能极大提升用户体验——让用户只看到自己能操作的内容,避免无效点击和困惑。


本文将分享一个 Vue 2 自定义指令的设计思路,实现了声明式的权限控制方案。


设计目标


在动手写代码之前,我先梳理了几个核心诉求:



  1. 使用简单:一行代码搞定权限控制,不需要写一堆 v-if

  2. 性能友好:同一权限不重复请求,利用缓存

  3. 灵活可控:支持隐藏、禁用、提示等多种交互方式

  4. 支持多场景:既能用在模板里,也能在 JS 逻辑中调用


核心实现


1. 权限缓存设计


权限校验通常需要请求后端接口,如果每个按钮都单独请求一次,那页面性能会非常糟糕。这里采用了 Promise 缓存 的方式:


Vue.prototype.$getPerm = (options) => {
const argId = options.type + '-' + (options.type === 'space' ? options.spaceId : options.wikiId)

// 如果缓存中没有,创建新的 Promise
if (!spaceInfoStore.permissionMap[argId]) {
spaceInfoStore.permissionMap[argId] = new Promise((resolve, reject) => {
// 请求后端获取权限...
Auth.getUserPermBySpaceId(options.spaceId).then((res) => {
const permissions = res.reduce((acc, category) => {
category.permissions.forEach(permission => {
acc.push(`${category.value}.${permission.authorityKey}`)
})
return acc
}, [])
resolve({ permissions })
})
})
}
return spaceInfoStore.permissionMap[argId]
}

这个设计的巧妙之处在于:缓存的是 Promise 本身,而不是结果


这样做的好处是,即使多个组件同时调用 $getPerm,也只会发出一次请求。后续的调用会直接拿到同一个 Promise,等待第一次请求的结果。


2. 双重使用方式


为了覆盖不同的使用场景,我设计了两种调用方式:


方式一:指令式(模板中使用)


<button v-perm:[permOptions]="'SPACE.EDIT'">编辑</button>

方式二:编程式(JS 中使用)


this.$hasPerm({ spaceId: 123 }, 'SPACE.EDIT').then(() => {
// 有权限,执行操作
}).catch(() => {
// 无权限
})

指令式适合静态权限控制,编程式适合需要在逻辑中判断的场景。两者底层共用同一套缓存机制。


3. DOM 处理策略


无权限时如何处理 DOM?这里提供了三种策略:


function domHandler (el, binding) {
let placeholderDom = null

if (binding?.arg?.showTips || binding?.arg?.disabled) {
// 策略1&2:保留元素,但禁用或添加点击提示
placeholderDom = el.cloneNode(true)

if (binding?.arg?.showTips) {
placeholderDom.onclick = function () {
Vue.prototype.$bkMessage({
message: binding?.arg?.tipsText || '没有权限',
theme: 'warning'
})
}
}

if (binding?.arg?.disabled) {
placeholderDom.classList.add('disabled')
}
} else {
// 策略3:完全隐藏,用注释节点占位
placeholderDom = document.createComment('permission-placeholder')
}

el.placeholderDom = placeholderDom
el.parentNode.replaceChild(placeholderDom, el)
}

为什么用注释节点而不是直接 display: none


因为注释节点不会影响布局,也不会被 CSS 选择器选中。更重要的是,我们需要保留一个"锚点",方便权限变化时把原始元素恢复回去。


4. 响应式更新


权限可能会动态变化(比如用户被授权后刷新),所以指令需要同时监听 insertedupdate 钩子:


Vue.directive('perm', {
inserted (el, binding) {
handlerPerm(el, binding)
},
update (el, binding) {
handlerPerm(el, binding)
}
})

恢复元素的逻辑也很简单:


function restoreElement (el) {
if (el.placeholderDom && el.placeholderDom.parentNode) {
el.placeholderDom.parentNode.replaceChild(el, el.placeholderDom)
}
el.placeholderDom = null
return true
}

5. 支持布尔值快捷方式


有时候权限结果已经在外部计算好了,不需要再走一遍接口校验。这种场景下,支持直接传布尔值会更方便:


<button v-perm:[options]="hasPermission">操作</button>

if (typeof binding.value === 'boolean') {
if (binding.value === false) {
domHandler(el, binding)
} else {
restoreElement(el)
}
return
}

使用示例


基础用法


<template>
<div>
<!-- 无权限时隐藏 -->
<button v-perm:[permConfig]="'SPACE.DELETE'">删除</button>

<!-- 无权限时禁用并提示 -->
<button v-perm:[permConfigWithTips]="'SPACE.EDIT'">编辑</button>
</div>
</template>

<script>
export default {
computed: {
permConfig() {
return {
spaceId: this.currentSpaceId,
type: 'space'
}
},
permConfigWithTips() {
return {
spaceId: this.currentSpaceId,
type: 'space',
disabled: true,
showTips: true,
tipsText: '您没有编辑权限,请联系管理员'
}
}
}
}
</script>

编程式调用


// 在执行敏感操作前校验
async handleDelete() {
try {
await this.$hasPerm({ spaceId: this.spaceId }, 'SPACE.DELETE')
// 有权限,继续执行删除逻辑
await this.doDelete()
} catch {
// 无权限,$hasPerm 内部已经弹出提示
}
}

清除缓存重新加载


当权限发生变化时(比如管理员授权后),可以清除缓存重新加载:


this.$getPerm({ 
spaceId: this.spaceId,
clearCache: true
})

设计总结


特性实现方式
性能优化Promise 缓存,同一权限只请求一次
使用方式指令式 + 编程式双重支持
DOM 处理隐藏 / 禁用 / 提示三种策略
响应式inserted + update 钩子联动
灵活性支持布尔值、清除缓存、自定义提示

可以优化的点



  1. Vue 3 适配:Vue 3 的指令钩子函数名称有变化(mountedupdated),迁移时需要调整

  2. TypeScript 支持:可以为 PermissionOptions 添加完整的类型定义

  3. 批量权限查询:如果页面上有大量权限点,可以考虑合并成一次批量查询

  4. 权限预加载:在路由守卫中预加载权限数据,减少页面白屏时间




以上就是这个权限指令的完整设计思路。核心思想是:用缓存换性能,用指令换简洁。希望对你有所启发,欢迎交流讨论 🙌


完整代码


最后贴一下完整代码,可以直接拿去用,根据自己项目的接口改一下就行:


// permission.js
import { useSpaceInfoStore } from '@/store/modules/spaceInfo'
import Auth from '@/api/modules/auth'

let spaceInfoStore = null
setTimeout(() => {
spaceInfoStore = useSpaceInfoStore()
}, 40)

/**
* @typedef {Object} PermissionOptions
* @property {string|number} [wikiId] - 文档id
* @property {string|number} [spaceId] - 空间id
* @property {string} [type] - 权限类型:空间/文档 space/wiki
* @property {boolean} [disabled] - 是否禁用元素
* @property {boolean} [showTips] - 是否显示提示信息
* @property {string} [tipsText] - 提示文本内容
* @property {boolean} [clearCache] - 是否清除缓存
*/


function install (Vue) {
/**
* 获取权限信息
* @param {PermissionOptions} options - 权限选项
* @returns {Promise<any>}
*/

Vue.prototype.$getPerm = (options) => {
if (!options.spaceId) return
// 如果没有传type,则根据是否有文档id判断
options.type = options.type || (options.wikiId ? 'wiki' : 'space')
const argId = options.type + '-' + (options.type === 'space' ? options.spaceId : options.wikiId)
// 清除缓存权限,可重新加载
if (options.clearCache) {
spaceInfoStore.permissionMap[argId] = false
}
if (!spaceInfoStore.permissionMap[argId]) {
spaceInfoStore.permissionMap[argId] = new Promise((resolve, reject) => {
if (options.type === 'space') {
Auth.getUserPermBySpaceId(options.spaceId).then((res) => {
// 组合权限生成唯一key
const permissions = res.reduce((acc, category) => {
category.permissions.forEach(permission => {
acc.push(`${category.value}.${permission.authorityKey}`)
})
return acc
}, [])
resolve({ permissions })
})
} else {
Auth.getWikiPermissionDetail(options.spaceId, options.wikiId).then((res) => {
// 组合权限生成唯一key
const permissions = res.map(item => item.authorityKey)
resolve({ permissions })
})
}
})
}
return spaceInfoStore.permissionMap[argId]
}

/**
* 检查是否有权限
* @param {PermissionOptions} options - 权限选项
* @param {string} perm - 权限码
* @returns {Promise<boolean>}
*/

Vue.prototype.$hasPerm = (options, perm) => {
if (!Object.prototype.hasOwnProperty.call(options, 'showTips')) {
options.showTips = true
}
return new Promise((resolve, reject) => {
if (!options.spaceId) {
resolve(true)
return
}
const promise = Vue.prototype.$getPerm(options)
promise.then((res) => {
if (res.isAdmin) {
resolve(true)
return
}
if (res.permissions.includes(perm)) {
resolve(true)
return
}
if (options.showTips) {
Vue.prototype.$bkMessage({
message: options.tipsText || '没有权限',
theme: 'warning'
})
}
reject(new Error(''))
})
})
}

/**
* DOM 处理函数 - 处理无权限时的元素显示
* @param {HTMLElement} el - DOM 元素
* @param {Object} binding - 指令绑定对象
*/

function domHandler (el, binding) {
let placeholderDom = null
if (binding?.arg?.showTips || binding?.arg?.disabled) {
placeholderDom = el.cloneNode(true)
if (binding?.arg?.showTips) {
placeholderDom.onclick = function () {
Vue.prototype.$bkMessage({
message: binding?.arg?.tipsText || '没有权限',
theme: 'warning'
})
}
}
if (binding?.arg?.disabled) {
placeholderDom.classList.add('disabled')
}
} else {
placeholderDom = document.createComment('permission-placeholder')
}
if (el.parentNode) {
el.placeholderDom = placeholderDom
el.parentNode.replaceChild(placeholderDom, el)
}
}

/**
* 将元素恢复到原始位置
* @param {HTMLElement} el - DOM 元素
* @returns {boolean}
*/

function restoreElement (el) {
el.placeholderDom && el.placeholderDom.parentNode.replaceChild(el, el.placeholderDom)
el.placeholderDom = null
return true
}

/**
* 权限处理函数
* @param {HTMLElement} el - DOM 元素
* @param {Object} binding - 指令绑定对象
*/

function handlerPerm (el, binding) {
// 通过直接传递boolean值,也可以进行权限校验
if (typeof binding.value === 'boolean') {
if (binding.value === false) {
domHandler(el, binding)
} else {
restoreElement(el)
}
return
}
// 判断权限入参是否完善
if (!binding?.arg?.spaceId || !binding?.value) return restoreElement(el)
const promise = Vue.prototype.$getPerm({ ...binding.arg })
promise.then((res) => {
if (res.isAdmin) return restoreElement(el)
if (res.permissions.includes(binding.value)) return restoreElement(el)
domHandler(el, binding)
})
}

Vue.directive('perm', {
inserted (el, binding) {
handlerPerm(el, binding)
},
update (el, binding) {
handlerPerm(el, binding)
}
})
}

export default { install }

main.js 里注册一下就能用了:


import permission from '@/directives/permission'
Vue.use(permission)

作者:keyV
来源:juejin.cn/post/7585758163436011526
收起阅读 »

当上传不再只是 /upload,我们是怎么设计大文件上传的

web
业务背景在正式讲之前,先看一个我们做的大文件上传demo。下面这个视频演示的是上传一个 1GB 的压缩包,整个过程支持分片上传、断点续传、暂停和恢复。可以看到速度不是特别快,这个是我故意没去优化的。前端那边计算文件 MD5、以及最后合并文件的时间我都保留了,主...
继续阅读 »

业务背景

在正式讲之前,先看一个我们做的大文件上传demo。
下面这个视频演示的是上传一个 1GB 的压缩包,整个过程支持分片上传、断点续传、暂停和恢复。

可以看到速度不是特别快,这个是我故意没去优化的。
前端那边计算文件 MD5、以及最后合并文件的时间我都保留了,
主要是想让大家能看到整个流程是怎么跑通的。

output1111.gif

平时我们在做一些 SaaS 系统的时候,文件上传这块其实基本上都设计的挺简单的。
前端做个分片上传,后端把分片合并起来,最后存 OSS 或者服务器某个路径上,再返回一个 URL 就完事了。
大多数情况下,这样的方案也确实够用。

但是最近我在做一个私有化项目,场景完全不一样。
项目是给政企客户部署的内部系统,里面有 AI 大模型客服问答的功能。
客户需要把他们内部的文档、手册、规范、图纸、流程等资料打包上传到服务器,用来做后续的向量化、知识检索或者模型训练。

这类场景如果还沿用之前 SaaS 系统那种上传方式,往往就不太适用了。
因为这些文件往往有几个共同点:

  1. 文件数量多,动辄几百上千份(Word、PDF、PPT、Markdown 都有);
  2. 文件体积大,打成 zip 动不动就是几个 G,甚至十几二十个 G;
  3. 上传环境复杂,客户一般在内网或局域网,有的甚至完全断网;
  4. 有安全要求,文件不能经过云端 OSS,里面可能有保密资料;
  5. 需要审计,要能知道是谁上传的、什么时候传的、文件现在存哪;
  6. 上传完之后还要进一步处理,比如自动解压、解析文本、拆页、向量化,然后再存入 Milvus 或 pgvector。

所以这种情况还用 SaaS 系统那种“简单上传+云存储”方案的话,那可能问题就一堆:

  • 上传中断后用户一刷新浏览器就得重传整个包;
  • 集群部署时分片打到不同机器上根本无法合并;
  • 多人同时上传可能会发生文件覆盖或路径冲突;
  • 没有任何上传记录,也追踪不到是谁传的;
  • 对政企来说,审计、合规、保密全都不达标。

所以,我们需要重新设计文件上传的功能逻辑。
目的是让它不仅能支持大文件、断点续传、集群部署,还能同时适配内网环境、权限管控,以及后续的 AI 文档解析和知识向量化等处理流程。


为什么很多项目只需要一个 upload 接口

如果我们回头看一下自己平常做过的一些常规 Web 项目,尤其是各种 SaaS 系统或者后台管理系统,
其实大多数时候后端只会提供一个 /upload 接口, 前端拿到文件后直接调用这个接口,后端保存文件再返回一个 URL 就结束了。

甚至我们在很多项目里,前端都不会把文件传给业务服务,
而是直接通过前端 SDK(比如阿里云 OSS、腾讯云 COS、七牛云等)上传到云存储,
上传完后拿到文件地址,再把这个地址回传给后端保存。

这种方式在 SaaS 系统或者轻量级的业务里非常普遍,也非常高效。 主要原因有几个:

  1. 文件都比较小,大多数就是几 MB 的图片、PDF 或 Excel;
  2. 云存储足够稳定,上传、下载、访问都有完整的 SDK 支撑;
  3. 系统是公网部署,不需要考虑局域网、内网断网这些问题;
  4. 对安全和审计的要求不高,文件内容也不是涉密数据;
  5. 用户体验优先,所以直接把文件上传到云端是最省事的方案。

换句话说,这种“一个 upload 接口”或“前端直传 OSS”模式,其实是面向通用型 SaaS 场景的。
对于绝大多数互联网业务来说,它既够快又够省心。

但一旦项目换成政企、私有化部署或者 AI 训练平台这种环境,
就完全不是一个量级的问题了。
这里的关键不在“能不能上传”,
而在于文件上传之后的可控性、可追溯性和安全性


前端常见的大文件上传方式

在重新设计后端接口之前,我们先来看看现在前端常见的大文件上传思路。
其实近几年前端这块已经比较成熟了,主流方案大体都是围绕几个核心点展开的:
秒传检测、分片上传、断点续传、并发控制、进度展示。

一般来说,前端拿到文件后,会先计算一个文件哈希值,比如用 MD5。
这样做的目的是为了做秒传检测
如果服务器上已经存在这个文件,就可以直接跳过上传,节省时间和带宽。

接下来是分片上传
文件太大时,前端会把文件拆成多个固定大小的小块(比如每块 5MB 或 10MB),
然后一片一片地上传。这样做可以避免一次性传输大文件导致浏览器卡顿或网络中断。

然后就是断点续传
前端会记录哪些分片已经上传成功,如果上传过程中网络中断或浏览器刷新,
下次只需要从未完成的分片继续上传,不用重新传整包文件。

在性能方面,前端还会做并发控制
比如同时上传三到五个分片,上传完一个就立刻补下一个,
这样整体速度比单线程串行上传要快很多。

最后是进度展示
通过监听每个分片的上传状态,前端可以计算整体进度,
给用户展示一个实时的上传百分比或进度条,让体验更可控。

可以看到,前端的大文件上传方案已经形成了一套相对标准的模式。
所以这次我在重新设计后端的时候,就打算基于这种前端逻辑,
去构建一套更贴合企业私有化环境的上传接口控制体系。
目标是让前后端的职责划分更清晰:
前端负责切片、控制与恢复;后端负责存储、校验与合并。


后端接口设计思路

前端的大文件上传流程其实已经相对固定了,我们只要让后端的接口和它配合得上,就能把整个上传链路打通。
所以我这次重新设计时,把上传接口拆成了几个比较独立的阶段:
秒传检查、初始化任务、上传分片、合并文件、暂停任务、取消任务、任务列表。
每个接口都只负责一件事,这样接口的职责会更清晰,也方便后期扩展。

一、/upload/check —— 秒传检查

这个接口是整个流程的第一步,用来判断文件是否已经上传过。
前端在计算完文件的全局 MD5(或其他 hash)后,会先调这个接口。
如果后端发现数据库里已经有相同 hash 的文件,就直接返回“已存在”,前端就不用再上传了。

请求示例:

POST /api/upload/check
{
"fileHash": "md5_abc123def456",
"fileName": "training-docs.zip",
"fileSize": 5342245120
}

返回示例:

{
"success": true,
"data": {
"exists": false
}
}

如果 exists = true,说明服务端已经有这个文件,可以直接走“秒传成功”的逻辑。

伪代码示例:

@PostMapping("/check")
public Result checkFile(@RequestBody Map body) {
// 1. 校验 fileHash 参数是否为空
// 2. 查询 file_info 表是否已有该文件
// 3. 如果文件已存在,直接返回秒传成功(exists = true)
// 4. 如果文件不存在,查询 upload_task 表中是否有未完成任务(支持断点续传)
}

二、/upload/init —— 初始化上传任务

如果文件不存在,就要先初始化一个新的上传任务。
这个接口的作用是创建一条 upload_task 记录,同时返回一个唯一的 uploadId
前端会用这个 uploadId 来标识整个上传过程。

请求示例:

POST /api/upload/init
{
"fileHash": "md5_abc123def456",
"fileName": "training-docs.zip",
"totalChunks": 320,
"chunkSize": 5242880
}

返回示例:

{
"success": true,
"data": {
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"uploadedChunks": []
}
}

uploadedChunks 用来支持断点续传,如果之前有部分分片上传过,就会在这里返回索引数组。

伪代码示例:

@PostMapping("/init")
public Result initUpload(@RequestBody UploadInitRequest request) {
// 1. 检查是否已有同 fileHash 的任务,若有则返回旧任务信息(支持断点续传)
// 2. 否则创建新的 upload_task 记录,生成 uploadId
// 3. 初始化分片数量、大小、状态等信息
// 4. 返回 uploadId 与已上传分片索引列表
}

三、/upload/chunk —— 上传单个分片

这是整个上传过程里调用次数最多的接口。
每个分片都会单独上传一次,并在服务端保存为临时文件,同时写入 upload_chunk 表。
上传成功后,后端会更新 upload_task 的进度信息。

请求示例(表单上传):

POST /api/upload/chunk
Content-Type: multipart/form-data

formData:
uploadId: b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b
chunkIndex: 0
chunkSize: 5242880
chunkHash: md5_001
file: (二进制分片数据)

返回示例:

{
"success": true,
"data": {
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"chunkIndex": 0,
"chunkSize": 5242880
}
}

伪代码示例:

@PostMapping(value = "/chunk", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result uploadChunk(@ModelAttribute UploadChunkRequest req) {
// 1. 校验任务状态,禁止上传已取消或已完成的任务
// 2. 检查本地目录(或云端存储桶)是否存在,不存在则创建
// 3. 接收当前分片文件并写入临时路径
// 4. 写入 upload_chunk 表,标记状态为 “已上传”
// 5. 更新 upload_task 的 uploaded_chunks 数量
}

四、/upload/merge —— 合并分片

当前端确认所有分片都上传完后,会调用 /upload/merge
后端收到这个请求后,去检查所有分片是否完整,然后按照索引顺序依次合并。
合并成功后,会删除临时分片文件,并更新 upload_task 状态为“完成”。
如果启用了云存储,这一步也可以直接把合并后的文件上传到 OSS。

请求示例:

POST /api/upload/merge
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"fileHash": "md5_abc123def456"
}

返回示例:

{
"success": true,
"message": "文件合并成功",
"data": {
"storagePath": "/data/uploads/training-docs.zip"
}
}

伪代码示例:

@PostMapping("/merge")
public Result mergeFile(@RequestBody UploadMergeRequest req) {
// 1. 检查 upload_task 状态是否允许合并
// 2. 校验所有分片是否都上传完成
// 3. 如果是本地存储:按 chunk_index 顺序流式合并文件
// 4. 如果是云存储:调用云端分片合并 API(如 OSS、COS)
// 5. 校验文件 hash 完整性,更新任务状态为 COMPLETED
// 6. 将最终文件信息写入 file_info 表
}

五、/upload/pause —— 暂停任务

这个接口用于在上传过程中手动暂停任务。
前端可能会在网络波动或用户主动点击暂停时调用。
后端会更新任务状态为“已暂停”,并记录当前已上传的分片数。

请求示例:

POST /api/upload/pause
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b"
}

返回示例:

{
"success": true,
"message": "任务已暂停"
}

伪代码示例:

@PostMapping("/pause")
public Result pauseUpload(@RequestBody UploadPauseRequest req) {
// 1. 查找对应的 upload_task
// 2. 更新任务状态为 “已暂停”
// 3. 返回任务状态确认信息
}

六、/upload/cancel —— 取消任务

如果用户想放弃本次上传,可以调用 /cancel
后端会把任务状态标记为“已取消”,并清理对应的临时分片文件。
这样能避免磁盘上堆积无用数据。

请求示例:

POST /api/upload/cancel
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b"
}

返回示例:

{
"success": true,
"message": "任务已取消"
}

伪代码示例:

@PostMapping("/cancel")
public Result cancelUpload(@RequestBody UploadCancelRequest req) {
// 1. 查找对应的 upload_task
// 2. 更新任务状态为 “已取消”
// 3. 删除或标记已上传的分片文件为待清理
// 4. 返回操作结果
}

七、/upload/list —— 查询任务列表

这个接口我们用于管理后台查看当前上传任务的整体情况。
可以展示每个任务的文件名、大小、进度、状态、上传人等信息,方便追踪和审计。

请求示例:

GET /api/upload/list

返回示例:

{
"success": true,
"data": [
{
"uploadId": "b4f8e3a7-1a0c-4a1d-88af-61e98d91a49b",
"fileName": "training-docs.zip",
"status": "COMPLETED",
"uploadedChunks": 320,
"totalChunks": 320,
"uploader": "admin",
"createdAt": "2025-10-20 14:30:12"
}
]
}

伪代码示例:

@GetMapping("/list")
public Result> listUploadTasks() {
// 1. 查询所有上传任务
// 2. 按创建时间或状态排序
// 3. 返回任务摘要信息(任务名、状态、进度、上传人等)
}

接口调用顺序小结

那我们这整个上传过程的调用顺序就是:

1. /upload/check     → 秒传检测
2. /upload/init → 初始化上传任务
3. /upload/chunk → 循环上传所有分片
4. /upload/merge → 所有分片完成后合并
(可选)/upload/pause、/upload/cancel 用于控制任务
(可选)/upload/list 用于任务追踪与审计

接口调用顺序示意图

下面这张时序图展示了前端、后端、数据库在整个上传过程中的交互关系。

Untitled diagram-2025-11-10-031845.png

这样安排有几个好处:

  1. 逻辑衔接顺:上面刚讲完每个接口的职责,下面立刻用图总结;
  2. 视觉节奏平衡:读者读到这里已经看了不少文字,用图能缓解阅读疲劳;
  3. 承上启下:这张图既总结接口流程,又能自然引出下一节“数据库表设计”。

这套接口设计基本能覆盖大文件上传在企业项目中的常见需求。
接下来,我们再来看看支撑这套接口背后的数据库表设计。
数据库的作用是让上传任务的状态可追踪、可恢复,也能在集群部署时保持一致性。


数据库表设计思路

前面说的那一套接口,要真正稳定地跑起来,
后端必须有一套能记录任务状态、分片信息、文件存储路径的数据库结构。
因为上传这种场景不是“一次请求就结束”的操作,它往往会持续几分钟甚至几个小时,
所以我们需要让任务状态可以追踪、可以恢复,还要能支撑集群部署。

我这次主要设计了三张核心表:
upload_task(上传任务表)、upload_chunk(分片表)、file_info(文件信息表)。
它们分别负责记录任务、分片和最终文件三层的数据关系。

一、upload_task —— 上传任务表

这张表是整个上传过程的“总账”,
每一个文件上传任务,不管分成多少片,都会在这里生成一条记录。
它主要用来保存任务的全局信息,比如文件名、大小、上传进度、状态、存储方式等。

CREATE TABLE `upload_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`upload_id` varchar(64) NOT NULL COMMENT '任务唯一ID(UUID)',
`file_hash` varchar(64) NOT NULL COMMENT '文件哈希(用于秒传与断点续传)',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_size` bigint(20) NOT NULL COMMENT '文件总大小(字节)',
`chunk_size` bigint(20) NOT NULL COMMENT '每个分片大小(字节)',
`total_chunks` int(11) NOT NULL COMMENT '分片总数',
`uploaded_chunks` int(11) DEFAULT '0' COMMENT '已上传分片数量',
`status` tinyint(4) DEFAULT '0' COMMENT '任务状态:0-待上传 1-上传中 2-合并中 3-完成 4-取消 5-失败 6-已合并 7-已暂停',
`storage_type` varchar(32) DEFAULT 'local' COMMENT '存储类型:local/oss/cos/minio/s3等',
`storage_url` varchar(512) DEFAULT NULL COMMENT '文件最终存储地址(云端或本地路径)',
`local_path` varchar(512) DEFAULT NULL COMMENT '本地临时文件或合并文件路径',
`remark` varchar(255) DEFAULT NULL COMMENT '备注信息',
`uploader` varchar(64) DEFAULT NULL COMMENT '上传人',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `upload_id` (`upload_id`),
KEY `idx_hash` (`file_hash`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上传任务表(支持多种云存储)';

设计要点:

  • upload_id 是前端初始化任务后由后端生成的唯一标识;
  • file_hash 用来支持秒传逻辑;
  • status 控制任务生命周期(等待、上传中、合并中、完成等);
  • storage_typestorage_url 可以兼容多种存储方案(本地、OSS、COS、MinIO);
  • uploaded_chunks 字段让任务能随时恢复,适配断点续传。

二、upload_chunk —— 分片表

这张表对应每个上传任务下的所有分片。
每一个分片都会单独在这里占一条记录,用来追踪它的上传状态。
这张表的存在让我们能做断点续传、进度统计、以及合并前的完整性检查。

CREATE TABLE `upload_chunk` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`upload_id` varchar(64) NOT NULL COMMENT '所属上传任务ID',
`chunk_index` int(11) NOT NULL COMMENT '分片索引(从0开始)',
`chunk_size` bigint(20) NOT NULL COMMENT '实际分片大小(字节)',
`chunk_hash` varchar(64) DEFAULT NULL COMMENT '可选:分片hash(用于高级去重)',
`status` tinyint(4) DEFAULT '0' COMMENT '状态:0-待上传 1-已上传 2-已合并',
`local_path` varchar(512) DEFAULT NULL COMMENT '分片本地路径',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_task_chunk` (`upload_id`,`chunk_index`),
KEY `idx_upload_id` (`upload_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上传分片表';

设计要点:

  • upload_id 是任务外键,和 upload_task 一一对应;
  • chunk_index 代表分片顺序,合并文件时会按这个排序;
  • chunk_hash 可选字段,用来在上传前后做完整性校验;
  • status 字段控制上传进度(待上传、已上传、已合并);
  • 唯一索引 (upload_idchunk_index) 避免重复插入分片。

通过这张表,我们可以轻松实现断点续传:
当用户重新开始上传时,后端只返回未完成的分片索引,前端跳过已上传的部分。

三、file_info —— 文件信息表

这张表记录的是上传完成后的“最终文件信息”,
相当于系统的文件索引表。只要文件合并成功并通过校验,
后端就会往这里写入一条记录。

这张表支撑秒传功能,也能被后续的文档解析或向量化任务使用。

CREATE TABLE `file_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`file_hash` varchar(64) NOT NULL COMMENT '文件hash,用于秒传',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`file_size` bigint(20) NOT NULL COMMENT '文件大小',
`storage_type` varchar(32) DEFAULT 'local' COMMENT '存储类型:local/oss/cos/minio/s3等',
`storage_url` varchar(512) DEFAULT NULL COMMENT '文件最终存储地址(云端或本地路径)',
`uploader` varchar(64) DEFAULT NULL COMMENT '上传人',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1-正常,2-删除中,3-已归档',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `file_hash` (`file_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='已上传文件信息表(支持多云存储)';

设计要点:

  • file_hash 是全局唯一标识,用于秒传和查重;
  • storage_url 记录最终可访问路径;
  • status 可扩展为删除、归档等后续操作;
  • 这张表和业务系统中的“文档解析”、“知识库构建”可以直接关联。

四、三张表之间的关系

这三张表之间的关系我们可以简单理解为:

upload_task  (上传任务)
├── upload_chunk (分片详情)
└── file_info (最终文件)
  • upload_task 管理任务生命周期;
  • upload_chunk 跟踪每个分片的上传进度;
  • file_info 保存最终文件索引,用于秒传与后续 AI 处理。

这样设计的好处是:

  • 上传状态可追踪;
  • 上传任务可恢复;
  • 文件信息可统一管理;
  • 多节点部署也能保证一致性。

上传状态流转与任务恢复机制

有了前面的三张核心表,整个上传的过程就能被“状态机化”管理。
简单来说,我们希望每一个上传任务从创建、上传、合并到完成,都能有一个明确的状态,
系统也能在任意阶段中断后恢复,不需要用户重新来一遍。

我们把整个上传任务的生命周期划分成几个关键状态:

WAITING(0, "待上传"),
UPLOADING(1, "上传中"),
MERGING(2, "合并中"),
COMPLETED(3, "已完成"),
CANCELED(4, "已取消"),
FAILED(5, "上传失败"),
CHUNK_MERGED(6, "已合并"),
PAUSED(7, "已暂停");

一、WAITING(待上传)

当用户在前端发起上传、文件切片还没真正传上来之前,
系统会先生成一个上传任务记录(也就是 /upload/init 接口那一步)。
这个时候任务只是“登记”在数据库里,还没开始传数据。

我们可以理解为:

任务刚创建,还没开始跑。

此时前端拿到 uploadId,就可以开始逐片上传了。

在数据库层面,upload_task.status = 0,所有的分片表里还没有数据。

二、UPLOADING(上传中)

当第一个分片开始上传时,系统会把任务状态更新为 上传中
这时候每上传一块分片,都会往 upload_chunk 表里写入一条记录,
并且更新任务的 uploaded_chunks 字段。

我们会周期性地根据分片上传数量去更新进度条,
比如已上传 35 / 100 块,系统就知道这部分可以恢复。

这个阶段是任务生命周期里最活跃的一段:
用户可能暂停、断网、刷新页面、甚至浏览器崩溃。
但是没关系,因为分片信息都落地到数据库了,
我们能随时通过 upload_chunk 的状态重新恢复上传。

三、PAUSED(已暂停)

如果用户主动点击“暂停上传”,
系统就会把任务状态标记为 PAUSED

暂停并不会删除分片,只是告诉系统“不要再继续发请求”。
这样当用户重新点击“继续上传”时,
前端只需从后端拿到“哪些分片还没上传”,就能断点续传。

这个状态一般只在用户控制的情况下出现,
比如网络不好、或者中途切换网络时暂停。

四、CANCELED(已取消)

取消和暂停不同,取消意味着用户彻底放弃了这个上传任务。
任务会被标记为 CANCELED,同时系统可以选择:

  • 删除已经上传的临时分片文件;
  • 或者保留一段时间等待清理任务。

在后台日志中,这个状态主要用于审计:
记录谁取消了任务、在什么时间、上传了多少进度。

五、MERGING(合并中)

当所有分片都上传完成后,
后端会自动或手动触发文件合并逻辑(调用 /upload/merge)。
此时任务状态会切换为 MERGING,表示系统正在进行最后一步。

在这一步里:

  • 如果是本地存储,会逐个读取分片文件并拼接为完整文件;
  • 如果是云存储(比如 OSS、MinIO),则会触发服务端的分片合并 API。

合并过程通常比较耗时,尤其是几 GB 的文件,
所以单独拿出来作为一个明确状态是必要的。

六、CHUNK_MERGED(已合并)

有些系统会把合并成功但未做后续处理的状态单独标出来,
比如文件已经合并,但还没入库、还没解析。
这个状态可以让我们在合并之后还有机会做文件校验或后处理。

不过在实际项目里,也可以直接跳过这一步,
合并完后立刻进入下一状态——COMPLETED

七、COMPLETED(已完成)

文件合并完成、验证通过、存储路径落地、写入 file_info 表,
这时候任务就算彻底完成了。

在这个状态下:

  • 用户可以正常访问文件;
  • 系统可以执行后续的解析任务(比如文档拆页、向量化等);
  • 文件具备秒传条件,下次再上传同样的文件会直接跳过。

COMPLETED 是整个生命周期的终点状态。
在数据库中,任务记录会更新最终路径、存储类型、完成时间等字段。

八、FAILED(上传失败)

上传过程中如果出现异常,比如网络中断、磁盘写入异常、OSS 上传失败等,
系统会标记任务为 FAILED
这一状态不会自动清理,
方便管理员事后追踪错误原因或人工恢复。

失败任务在设计上一般允许“重新启动”,
也就是通过任务 ID 重新触发上传,从未完成的分片继续。

我们可以通过下面这张图可以更直观地看到整个上传任务的生命周期:

Untitled diagram-2025-11-10-033341.png

九、任务恢复机制

在这套机制下,任务恢复就变得非常自然。
前端每次进入上传页面时,只要传入文件的 hash,
后端就能通过 upload_task 和 upload_chunk 判断:

  1. 这个文件有没有上传任务;
  2. 如果有,哪些分片已经上传;
  3. 任务当前状态是什么(暂停、失败还是上传中)。

然后前端只需补传那些未上传的分片即可。
这就是我们常说的 断点续传(Resumable Upload) 。

在集群环境中,这套逻辑同样成立,
因为任务与分片状态都落在数据库,不依赖单台服务器。
无论请求打到哪一台机器,上传进度都是统一可见的。

十、中断后如何续传

在实际使用中,用户上传中断是很常见的。
比如文件太大上传到一半,浏览器突然关了;
或者公司网络断了,机器重启了;
甚至有人直接换了电脑继续操作。

如果系统没有任务恢复机制,那用户每次都得重新传一遍,
尤其是那种几个 G 的文件,不但浪费时间,还容易出错。
所以我们在设计这套上传中心时,
一开始就考虑了“断点续传”和“任务恢复”的问题。

1. 恢复上传靠的其实是数据库里的状态

断点续传的核心逻辑,其实很简单:
我们让任务和分片的状态都写进数据库。

每当用户重新进入上传页面、选中同一个文件时,
前端会先计算出文件的 hash,然后调用 /upload/check 接口。
后端收到 hash 后,会依次去查三张表:

  1. 先查 file_info
    如果能查到,说明文件之前已经上传并合并成功,
    这时候直接返回“文件已存在”,前端就能实现“秒传”,不需要重新上传。
  2. 查不到 file_info,就去查 upload_task
    如果找到了对应任务,就说明这个文件上传到一半被中断了。
    这时我们会返回这个任务的 uploadId。
  3. 再查 `upload_chunk``
    系统会统计出哪些分片已经上传成功,哪些还没传。
    然后返回一个“未完成的分片索引列表”给前端。

前端拿到这些信息后,就能从中断的地方继续往下传,
不用再重复上传已经完成的部分。

2. 前端续传时的流程

前端拿到旧的 uploadId 和未完成分片列表后,
只需要跳过那些已经上传成功的分片,
然后照常调用 /upload/chunk 去上传剩下的部分。

上传过程中,每个分片的状态都会被实时更新到 upload_chunk 表中,
upload_task 表的 uploaded_chunks 也会跟着同步增加。
当所有分片都上传完后,任务状态自动进入 MERGING(合并中)阶段。

所以整个续传过程,其实就是**“基于数据库状态的增量上传”**。
用户不需要额外操作,系统自己就能恢复上次的进度。

3. 任务状态和恢复判断

任务是否允许恢复,系统会根据 upload_task.status 来判断。
大致逻辑是这样的:

状态是否可恢复说明
WAITING可以任务刚创建,还没开始传
UPLOADING可以正在上传中,可以继续
PAUSED可以用户主动暂停,可以恢复
FAILED可以上传失败,可以重新尝试
CANCELED不可以用户主动取消,不再恢复
COMPLETED不需要已经完成,直接秒传
MERGING等待中系统正在合并,前端等待即可

这套判断逻辑让任务的行为更清晰。
比如用户暂停上传再回来时,可以直接恢复;
如果任务已经取消,那就算用户重启也不会再自动续传。

4. 多机器部署下的恢复问题

有些人会担心:如果我们的系统是集群部署的,
上传时中断后再续传,万一请求打到另一台机器上,
还能恢复吗?

其实没问题。
因为我们所有任务和分片的状态都是写进数据库的,
不依赖内存或本地文件。

也就是说,哪怕用户上次上传在 A 机器,这次续传到了 B 机器,
系统仍然能根据数据库的记录知道:
这个 uploadId 下的哪些分片已经上传完,哪些还没传。

所以集群部署下也能无缝续传,不会出现“不同机器不认任务”的情况。

5. 小结

整个任务恢复机制靠的就是两张表:upload_task 和 upload_chunk
upload_task 负责记录任务总体进度,
upload_chunk 负责记录每个分片的上传状态。

当用户重新上传时,我们查表判断进度,
前端从未完成的地方继续传,就能实现真正意义上的“断点续传”。

这套机制有几个显著的好处:

  • 上传进度可追踪;
  • 中断后可恢复;
  • 支持集群部署;
  • 不依赖浏览器缓存或 Session。

所以,只要数据库没丢,任务记录还在,
上传进度就能恢复,哪怕换机器、重启系统都没问题。


文件合并与完整性校验

前面的所有步骤,其实都是在为这一刻做准备。
当用户的所有分片都上传完成后,接下来最重要的工作就是:
把这些分片拼成一个完整的文件,并且确保文件内容没有出错。

这一步看似简单,但其实是整个大文件上传流程里最容易出问题的地方。
尤其在集群部署下,如果不同分片分布在不同机器上,
那合并逻辑就不能只靠本地文件路径去拼接,否则根本找不到所有分片。

所以我们先来理一理整个思路。

一、合并的触发时机

前端在检测到所有分片都上传完成后,会调用 /upload/merge 接口。
这个接口的作用就是通知后端:
“这个任务的所有分片都传完了,现在可以开始合并了。”

后端接收到请求后,会先去查数据库确认几个关键信息:

  1. 这个任务对应的 uploadId 是否存在;
  2. upload_chunk 表里所有分片是否都处于 “已上传” 状态;
  3. 当前任务状态是否允许合并(例如不是暂停、取消或失败)。

确认无误后,任务状态会从 UPLOADING 变成 MERGING
正式进入文件合并阶段。

二、本地合并逻辑

如果系统配置的是本地存储(也就是 cloud.enable = false),
那所有分片文件都保存在服务器的临时目录中。

合并逻辑大致是这样的:

  1. 后端按分片的 chunk_index 顺序,依次读取每个分片文件。
  2. 逐个写入到一个新的目标文件中,比如 merge.zip
  3. 每合并一个分片,就更新数据库中的状态。
  4. 合并完成后,把任务状态更新为 COMPLETED,并写入最终路径。

整个过程看起来很直观,
但这里有两个要点需要特别注意:

  • 写入顺序要严格按照分片索引,否则文件内容会错乱;
  • 文件 IO 要用流式写入(Stream) ,避免内存一次性读取所有分片导致溢出。

合并完成后,我们会计算整个文件的 MD5,与原始 fileHash 对比,
如果不一致,就说明合并过程中数据丢失或出错。
这种情况任务会被标记为 FAILED,并在日志中留下异常记录。

三、云端合并逻辑

如果我们配置了云存储(比如 OSS、COS、MinIO 等),
那分片文件就不是存在本地磁盘,而是上传到云端的对象存储桶里。

在这种情况下,合并逻辑就不需要我们自己拼文件了,
因为大部分云存储服务都提供了“分片合并”的 API。

比如以 OSS 为例,上传时我们调用的是 uploadPart 接口,
合并时只需要调用 completeMultipartUpload
它会根据上传时的分片顺序自动合并为一个完整对象。

整个过程的优点是:

  • 不占用本地磁盘;
  • 不受单机 IO 限制;
  • 云端自动校验每个分片的完整性。

所以在云存储场景下,我们只需要做两件事:

  1. 通知云服务去执行合并;
  2. 成功后记录最终的文件地址(storage_url)到数据库。

这样整个流程就闭环了。

四、集群部署下的合并问题

单机情况下,合并很简单,因为所有分片都在本地。
但如果系统是集群部署的,分片请求可能打到了不同机器,
这时候分片文件就会分散在多个节点上。

我们在设计时考虑了三种解决方案:

方案 1:共享存储(私有化部署下比较推荐)

最常见的做法是把所有机器的上传目录指向同一个共享路径,
比如通过 NFS、NAS、或对象存储挂载到 /data/uploads
这样无论用户上传的分片打到哪台机器,
最终都会写入同一个物理目录。

当合并请求发起时,任意一台机器都能访问到完整的分片文件。
这是目前在企业部署中最稳定、最通用的方案。

方案 2:云存储中转

如果机器之间没有共享目录,那我们可以让每个分片先上传到云端,
合并时再调用云服务的 API 进行分片合并。
这种方式适合公网可访问的 SaaS 环境。
但对于政企内网部署,就不一定行得通。

方案 3:统一调度节点

还有一种是我们自己维护一个“合并调度节点”,
所有分片上传完后,系统会把合并任务分配到一个指定节点执行,
这个节点会从其他机器拉取分片(比如通过 HTTP 内部传输或 RPC)。
这种方式更复杂,适合大规模分布式存储场景。

在私有化项目中,我们一般采用第一种方式——共享目录 + 本地合并
既能保证性能,也能兼顾安全性。

五、完整性校验

文件合并完成后,最后一步是完整性校验。
我们会重新计算合并后文件的 MD5,与前端最初上传的 fileHash 对比。

如果一致,就说明文件合并成功,内容没有丢失;
如果不一致,就说明某个分片损坏或顺序错误,
任务会被标记为 FAILED,并自动记录错误日志。

这样可以确保文件数据的安全性,
避免在后续 AI 解析或向量化阶段出现内容异常。

六、异步处理与性能优化

开头的视频里我们也看到了,整个上传和合并过程我们是同步执行的。
从前端开始上传分片,到最后文件合并完成,都在等待同一个流程走完。
这种方式在演示时很直观,但在真实项目中其实问题不少。

最明显的一个问题就是——时间太长。
像我们刚才那个 1GB 的文件,即使网络稳定、服务器性能还可以,
整个流程也要几分钟甚至更久。
如果我们让前端一直等待响应,接口超时、连接断开、前端刷新这些问题就都会冒出来。

所以,在真正的业务系统里,我们一般会把合并、校验、迁移 OSS 或解析入库这些操作改成异步任务来做。
接口只负责接收分片、登记状态,然后立刻返回“任务已创建”或“上传完成,正在处理中”的提示。
后续的合并、校验、清理临时文件这些工作交给后台的异步线程、任务队列或者调度器去跑。

这样做的好处有几个:

  • 前端体验更流畅,不用卡在“等待合并”阶段;
  • 后端可以批量处理任务,减少高峰期的 IO 压力;
  • 如果任务失败或中断,也能通过任务表重试或补偿;
  • 对接外部存储或 AI 解析流程时,也能自然衔接后续任务链。

简单来说,上传只是第一步,
而合并、校验、转存这些操作本质上更像是后台任务。
我们在系统设计时只要把这些环节分开,让接口尽量“轻”,
这套上传系统就能在面对更大文件、更复杂场景时依然稳定可靠。

七、小结

整个合并与校验阶段,是把前面所有分片上传工作“收尾”的过程。
我们通过以下机制保证了稳定性:

  • 本地存储场景下:顺序读取 + 流式写入 + hash 校验;
  • 云存储场景下:依赖云端分片合并 API;
  • 集群环境下:通过共享存储或统一调度节点解决文件分散问题;
  • 数据库层面:实时记录状态,便于追踪和审计。

最终,当文件合并成功、校验通过后,
系统会将结果写入 file_info 表,
整条上传链路就算是完整闭环。


最后

我们平常做的项目,大多数时候文件上传都挺简单的。
前端传到 OSS,后端接个地址存起来就行。
但等真正做私有化项目的时候,也就会发现很多地方都不一样了。
要求更多,考虑的细节也多得多。

像这次做的大文件上传就是个很典型的例子。
以前那种简单方案,放在这种环境下就完全不够用了。
得考虑断点续传、任务恢复、集群部署、权限、审计这些东西,
一步没想好,后面全是坑。

我们现在这套设计,其实就是在解决这些“现实问题”。
接口虽然多一点,但每个职责都很清晰,
任务状态能追踪,上传中断能恢复,
甚至以后如果我们想单独抽出来做一个文件系统模块也完全没问题。
不管是拿来给知识库用,还是 AI 向量化、文档解析,这套逻辑都能复用。

其实很多以前觉得“简单”的功能,
一旦遇到复杂场景,其实都得重新想。
但好处是,一旦做通了,这套东西就能稳定用很久。

到这里,大文件上传这块我们算是完整走了一遍。
以后再遇到类似需求,我们就有经验了,
不用再从头掉坑里爬出来一次哈。

更多架构实战、工程化经验和踩坑复盘,我会在公众号 「洛卡卡了」 持续更新。
如果内容对你有帮助,欢迎关注我,我们一起每天学一点,一起进步。


作者:洛卡卡了
来源:juejin.cn/post/7571355989133099023

收起阅读 »