
最近有一种现象很值得注意很多人把 Claude Code 这类 Agent 工具当作“聊天窗口的加强版”丢一句“帮我写一个网站”进去然后就等着验收。结果往往很一致代码确实能跑起来但离“商业级”还很远——没有登录注册数据库密码直接写在源码里上传接口不校验文件类型部署靠手敲命令。更麻烦的是AI 生成的代码会在某个隐蔽的环节出问题例如数据库连接没有关闭、JWT 密钥写死、上传的文件可以被当作 HTML 执行这些问题单独看都是小事合在一起就是安全事故。这篇文章想表达的核心判断是Claude Code 类工具改变的是“编码执行”的效率而不是“架构设计”的责任。工具的权限越大使用它的人越需要先建立一套清晰的工程边界——鉴权怎么做、数据怎么存、多媒体文件怎么处理、发布流程怎么自动化。我会以一个“用户可注册登录、媒体文件可上传压缩、数据库可查询、可自动化部署”的全栈网站为主线拆解 Claude Code 在实际项目中的接入方式和工程化方法。你可以把这篇文章当作一份“AI 辅助构建商业级站点”的设计笔记来读也可以直接按里面的代码和配置开始搭建。1. 为什么 AI 生成的网站容易“能跑但不敢上线”在讨论技术细节之前先给 Claude Code 一个准确的角色定位它不是代码补全插件而是能长时间驻留在终端里、跨文件工作的编码 Agent。它能做的事情包括读取项目文件、搜索代码、修改多个文件、执行终端命令、运行测试、根据报错信息再次修改。这带来一个质变过去程序员写一个模块要经历“写代码—编译—看报错—修复”的多次循环现在 Agent 可以在内部把这个循环完成多轮然后交出一个相对完整的功能。但问题也随之而来。Agent 对“功能是否实现”的判断很敏感对“这个功能是否安全、是否适合生产环境”的判断却不总是可靠。它常常倾向给出最短路径不用鉴权框架手写一个简陋的 token不用迁移工具直接建表不做文件类型校验允许任何文件上传不设计滚动发布要求所有人登录服务器改代码。“能跑”和“可持续运行”是两种完全不同的标准。商业级网站至少要回答下面这些问题用户密码怎么存是明文、可逆加密还是单向哈希用户访问受限资源时后端靠什么判断身份数据库连接放在哪里查询是否使用参数化能否防住注入用户上传的图片经过压缩和重编码吗文件访问权限是否控制到用户维度测试环境、预发环境、生产环境的配置怎么隔离发布流程是否可重复、可回滚这些问题恰好构成一条完整的技术主线后端鉴权、数据库设计、多媒体处理、自动化部署。接下来我会用一套最小但五脏俱全的“用户媒体库”系统逐个模块展开。2. Claude Code 的工程定位与适用边界2.1 它真正改变的是“代码交付闭环”如果只是让 Claude Code 写一个函数那么它的价值和传统 AI 补全工具差别不大。真正有差别的是下面这个工作流你提出需求实现用户注册、登录、上传头像。Agent 先扫描项目现有代码结构。Agent 选择合适的文件位置创建路由、服务、数据库访问代码。Agent 运行后端服务或执行测试。Agent 从报错信息中定位问题修改代码再次验证。你把改动 review 后提交。这个流程最大的价值不是“少敲了几个字母”而是把“写代码—验证—排错”的小循环从人工变成了自动。开发者可以把精力集中到更有判断力的事情上模块怎么拆分、鉴权方案是否合理、数据库表结构是否需要调整、上线流程是否安全。2.2 传统 AI 编码工具与 Agent 工具的差异从工程实践角度看可以把工具分成三类工具类型典型交互是否跨文件改代码是否执行命令适合任务聊天式代码助手在网页/IDE 对话框提问部分支持通常不执行解释代码、生成片段、单文件修改终端 AgentClaude Code在项目目录内下达任务是是多文件功能开发、重构、排错、写测试人工编写阅读上下文后手动修改是是复杂架构设计、关键安全代码、Code ReviewClaude Code 的价值来自“桥梁”它把 AI 的理解能力和终端执行力结合在了一起但是这种结合意味着风险面也在扩大。它的执行范围不再局限于代码编辑器内部而是覆盖了项目目录、依赖安装、命令执行等操作。2.3 Claude Code 不适合做什么先说结论Claude Code 并不适合在需求完全模糊的情况下替你构思产品。它更适合“已经明确目标和约束”的工程实现。举个例子如果你直接说“帮我做一个类似电商的后台”它可能会生成一份常见的后台模板。但如果你的诉求是“做一个支持 3 种角色权限、媒体文件需要鉴权访问、部署在内部服务器上的工具平台”那么它就能在明确边界里发挥很大作用。让 AI 写功能前人应该先想清楚这几个问题用户角色有哪些谁能看哪些数据数据库表结构以什么字段作为唯一标识文件上传后是否需要压缩、转格式生产环境谁负责发布是否要用 CI 工具这就是我在文章开头强调的判断Claude Code 是效率放大器不是架构决策器。下面进入正题看看如何用它构建一个带鉴权和多媒体处理的全栈项目。3. 先从架构层面设计这个网站我以一个“用户媒体库”系统为例。用户可以注册、登录、上传图片、查看自己的图片列表、删除自己的图片。管理员可以查看所有图片普通用户只能操作自己的文件。这个系统覆盖了三类最常见的后端问题鉴权用户身份验证和权限控制。数据用户数据、媒体元数据的持久化。多媒体文件上传、图片校验、压缩与访问控制。3.1 系统模块划分client浏览器/前端 ↓ HTTPS API 服务Express ├── /api/auth 注册、登录、刷新 token、退出 ├── /api/media 上传、列表、删除需要登录 └── /static/uploads 静态文件真实项目中建议由网关控制访问 数据库PostgreSQL ├── users ├── refresh_tokens └── media_files之所以把模块拆开而不是让 Agent 在一个文件里完成所有逻辑是为了让每个模块拥有独立的职责也便于后续审阅。如果你在 Claude Code 里下达任务建议把上面的模块结构直接写在需求里。例如请在后端项目 src/routes 下创建 auth 路由和 media 路由。 auth 路由负责注册、登录、刷新 token、退出。 media 路由负责上传图片、列出本人图片、删除本人图片。 所有 media 路由必须经过 authRequired 中间件。 数据库访问统一使用参数化查询禁止字符串拼接 SQL。这种请求方式比“帮我写个完整后端”更接近 AI 可以高效执行的粒度。3.2 技术选型建议在实际项目中你可以把下面的技术栈整体交给 Claude Code 初始化也可以只让它完成其中的若干模块。模块技术选型选择理由API 层Node.js Express轻量生态成熟Agent 生成代码不易出错数据库PostgreSQL支持事务、行锁、JSON适合业务型全栈网站鉴权bcryptjs JWT httpOnly Cookie常见方案资料多便于后续开发维护文件上传multer sharpmulter 处理 multipartsharp 负责重编码压缩部署Docker Compose CI 脚本本地开发与服务器环境一致发布流程可重复AI 协作Claude Code终端内跨文件编码与验证这套选型不是唯一的答案但胜在“通用”。你团队如果使用 Java/Spring、Go、Python/FastAPI也可以沿用同样的模块思路只是语言层面的中间件写法要相应调整。下面以 Node.js 为例演示。4. 环境准备与 Claude Code 接入4.1 Node.js 与数据库环境开发环境建议使用 Node.js 18 及以上版本。这里用 Docker 启动 PostgreSQL 比较省事具体命令可以交给 Claude Code 生成但你要能看懂和 review。推荐使用一个干净的项目目录mkdir media-platform cd media-platform npm init -y然后安装核心依赖npm install express helmet cors morgan dotenv pg bcryptjs jsonwebtoken multer sharp cookie-parser npm install -D nodemon各依赖的作用分别是Express 提供路由能力helmet 设置常见安全响应头cors 控制跨域morgan 记录请求日志dotenv 读取环境变量pg 是 Node.js 连接 PostgreSQL 的客户端bcryptjs 负责密码哈希jsonwebtoken 负责签发和验证 JWTmulter 处理 multipart 文件上传sharp 做图片处理cookie-parser 解析 httpOnly Cookie。4.2 Claude Code 的安装与配置Claude Code 是终端工具安装方式会随着版本变化。以目前比较常见的路径为例可以通过 npm 全局安装npm install -g anthropic-ai/claude-code安装完成后在项目根目录启动claude如果你是第一次运行工具会引导完成账号或 API 密钥配置。不同版本的引导入口可能不同以工具自身的提示为准。配置完成、进入交互界面后可以先让它分析一下当前项目的结构请先看一下当前目录的项目结构告诉我你打算怎么实现用户上传图片的接口不要急着写代码先给方案。这一步很重要。先要求 Agent“只给方案、不写代码”能让你在写代码之前就看出它是否理解业务。如果你的项目已经有了后端框架它会直接结合现有代码进行说明。也有很多人把 Claude Code 配置在 VS Code 里使用更方便边看代码边下指令。配置方式本身不复杂核心是让终端工具能够读取当前打开的文件夹。如果项目使用了容器开发环境还要确认工具在容器内可以正常运行。4.3 环境变量文件任何环境变量都不应进入 Git 仓库。项目里需要创建.env.example作为模板把真实密钥放在.env文件中并加入.gitignore。# .env.example PORT3000 DATABASE_URLpostgres://app_user:app_passwordlocalhost:5432/media_platform JWT_ACCESS_SECRETchange_me_access JWT_REFRESH_SECRETchange_me_refresh NODE_ENVdevelopment# .gitignore node_modules/ .env dist/ coverage/从项目第一天开始就做配置隔离是一个商业级项目的底线。5. 后端鉴权模块的实现后端鉴权是网站从“演示品”走向“产品”的第一道门槛。下面来实现一个比较典型的方案用户注册时密码使用 bcryptjs 做哈希不存明文。登录成功后签发短期 Access Token 和长期 Refresh Token。Refresh Token 的哈希值存入数据库便于撤销和退出登录。受保护接口通过 JWT 中间件判断当前用户。5.1 数据库中的用户与会话表先用 SQL 设计两张核心表。如果你希望 Agent 帮你生成迁移脚本需要先把表结构定义清楚再让它动手。-- src/migrations/001_init.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, password_hash TEXT NOT NULL, role VARCHAR(20) NOT NULL DEFAULT member, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_users_email UNIQUE (email), CONSTRAINT chk_users_role CHECK (role IN (member, admin)) ); CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, token_hash CHAR(64) NOT NULL, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT fk_refresh_tokens_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);这里把 Refresh Token 的 SHA-256 摘要存入数据库而不是直接存原始 Token。这样即使数据库泄露攻击者也无法直接拿摘要冒充用户。用户表里还加了role字段后续管理员权限判断可以直接用。5.2 身份验证核心代码下面的代码是鉴权模块的最小实现。建议让 Claude Code 按这个逻辑生成然后由你逐行 review。// src/auth.js const bcrypt require(bcryptjs); const jwt require(jsonwebtoken); const crypto require(crypto); const SALT_ROUNDS 12; function hashPassword(plainPassword) { return bcrypt.hash(plainPassword, SALT_ROUNDS); } function verifyPassword(plainPassword, hashedPassword) { return bcrypt.compare(plainPassword, hashedPassword); } function signAccessToken(user) { return jwt.sign( { sub: String(user.id), role: user.role, type: access }, process.env.JWT_ACCESS_SECRET, { expiresIn: 15m } ); } function sha256(value) { return crypto.createHash(sha256).update(value).digest(hex); } function signRefreshToken(user) { const token jwt.sign( { sub: String(user.id), type: refresh }, process.env.JWT_REFRESH_SECRET, { expiresIn: 7d } ); return { token, tokenHash: sha256(token), expiresAt: new Date(Date.now() 7 * 24 * 60 * 60 * 1000) }; } function authRequired(req, res, next) { const header req.headers.authorization || ; const [scheme, token] header.split( ); if (scheme ! Bearer || !token) { return res.status(401).json({ message: 缺少访问凭证 }); } try { const payload jwt.verify(token, process.env.JWT_ACCESS_SECRET); if (payload.type ! access) { return res.status(401).json({ message: Token 类型错误 }); } req.user { id: payload.sub, role: payload.role }; return next(); } catch (err) { return res.status(401).json({ message: Token 无效或已过期 }); } } function adminRequired(req, res, next) { if (!req.user) { return res.status(401).json({ message: 请先登录 }); } if (req.user.role ! admin) { return res.status(403).json({ message: 权限不足 }); } return next(); } module.exports { hashPassword, verifyPassword, signAccessToken, signRefreshToken, authRequired, adminRequired, };这段代码有几个关键点必须理解bcrypt.hash的盐值轮数设置为 12。轮数越高计算越慢暴力破解成本越高但也需要权衡服务器性能。Access Token 有效期短15 分钟。这样即使 Token 泄露影响窗口也比较小。中间件只解析 Bearer Token不依赖前端传用户 ID。这是一个安全底线后端绝不能通过请求参数决定“我是谁”身份必须来自签发过的 Token。5.3 注册与登录路由// src/routes/auth.js const express require(express); const { pool } require(../pool); const { hashPassword, verifyPassword, signAccessToken, signRefreshToken, } require(../auth); const router express.Router(); router.post(/register, async (req, res) { const { email, password } req.body || {}; if (!email || !password) { return res.status(400).json({ message: 邮箱和密码不能为空 }); } const emailPattern /^[^\s][^\s]\.[^\s]$/; if (!emailPattern.test(email)) { return res.status(400).json({ message: 邮箱格式不正确 }); } if (password.length 8) { return res.status(400).json({ message: 密码至少 8 位 }); } try { const passwordHash await hashPassword(password); const result await pool.query( INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email, role, [email.toLowerCase(), passwordHash] ); return res.status(201).json({ user: result.rows[0] }); } catch (err) { if (err.code 23505) { return res.status(409).json({ message: 该邮箱已注册 }); } console.error(register error:, err); return res.status(500).json({ message: 服务器内部错误 }); } }); router.post(/login, async (req, res) { const { email, password } req.body || {}; if (!email || !password) { return res.status(400).json({ message: 邮箱和密码不能为空 }); } const result await pool.query( SELECT id, email, password_hash, role FROM users WHERE email $1, [email.toLowerCase()] ); const user result.rows[0]; if (!user) { return res.status(401).json({ message: 邮箱或密码错误 }); } const ok await verifyPassword(password, user.password_hash); if (!ok) { return res.status(401).json({ message: 邮箱或密码错误 }); } const accessToken signAccessToken(user); const { token: refreshToken, tokenHash, expiresAt } signRefreshToken(user); await pool.query( INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3), [user.id, tokenHash, expiresAt] ); return res.json({ accessToken, refreshToken, user: { id: user.id, email: user.email, role: user.role }, }); }); router.post(/logout, async (req, res) { const { refreshToken } req.body || {}; if (!refreshToken) { return res.status(400).json({ message: 缺少 refreshToken }); } const tokenHash require(crypto).createHash(sha256).update(refreshToken).digest(hex); await pool.query( UPDATE refresh_tokens SET revoked_at now() WHERE token_hash $1 AND revoked_at IS NULL, [tokenHash] ); return res.json({ message: 退出成功 }); }); module.exports router;这里的 SQL 全部使用$1、$2参数占位而不是拼接字符串这是防止 SQL 注入的最核心手段。如果 Claude Code 在生成代码时把参数直接拼进 SQL你要在 Review 时立刻指出来。5.4 用 curl 验证注册登录流程启动服务后先用 curl 验证最基础的两个接口curl -s -X POST http://localhost:3000/api/auth/register \ -H Content-Type: application/json \ -d {email:demoexample.com,password:password123}返回内容应当是一个用户对象。再调用登录curl -s -X POST http://localhost:3000/api/auth/login \ -H Content-Type: application/json \ -d {email:demoexample.com,password:password123}如果登录接口返回了accessToken和refreshToken说明最小闭环已经跑通。这里要特意提醒一句真实商业项目还需要补充登录限速、刷新 Token 接口、验证码、设备管理、异常登录提醒等能力。上面这段代码是架构骨架不是完整的安全解决方案。6. 数据库连接设计与常见并发问题Chapters 5 Already included db. 独立 database section covers pool, common pitfall.6. 数据库连接设计与并发、死锁避坑Claude Code 在写数据库代码时最容易出现两种倾向一是喜欢把所有逻辑堆在server.js里二是完全不考虑连接复用导致每次请求都新建连接。下面先说连接池的标准做法。6.1 PostgreSQL 连接池// src/pool.js const { Pool } require(pg); const pool new Pool({ connectionString: process.env.DATABASE_URL, max: 10, idleTimeoutMillis: 30000, }); module.exports { pool };创建连接池之后不要再写new Client()去请求数据库。连接池会复用连接避免频繁握手带来的性能开销。如果你的数据库是在 Docker 容器里启动的DATABASE_URL可以用类似下面的值postgres://app_user:app_passwordlocalhost:5432/media_platform数据库的账号权限应当遵循最小权限原则应用账号只具备它需要的增删改查权限不要使用 PostgreSQL 超级用户。6.2 入门级增删改查示例下面是一个简单的媒体文件查询接口展示了 SELECT、DELETE 和参数化的用法。// src/routes/media.js const express require(express); const { pool } require(../pool); const { authRequired } require(../auth); const router express.Router(); router.get(/media, authRequired, async (req, res) { try { const result await pool.query( SELECT id, original_name, mime_type, size_bytes, width, height, created_at FROM media_files WHERE owner_id $1 ORDER BY id DESC LIMIT 100, [req.user.id] ); return res.json({ files: result.rows }); } catch (err) { console.error(list media error:, err); return res.status(500).json({ message: 服务器内部错误 }); } }); router.delete(/media/:id, authRequired, async (req, res) { const { id } req.params; try { const result await pool.query( DELETE FROM media_files WHERE id $1 AND owner_id $2 RETURNING id, [id, req.user.id] ); if (result.rowCount 0) { return res.status(404).json({ message: 文件不存在或无权删除 }); } return res.json({ message: 删除成功 }); } catch (err) { console.error(delete media error:, err); return res.status(500).json({ message: 服务器内部错误 }); } }); module.exports router;delete 语句中同时包含id和owner_id是保证数据隔离的关键。很多“越权删除”漏洞就是因为只按id删除没有校验资源归属。6.3 从 SQL 注入到并发锁的理解数据库部分最容易踩的坑不只是安装和连接还包括并发问题。热搜里经常出现“数据库死锁”“数据库并发锁”说明不少同学在做实验或课程设计时遇到了行锁等待。先看一个典型的并发问题用户 A 上传图片需要更新media_files和users.file_count。用户 B 删除图片也需要更新users.file_count。两个事务没有按一致的顺序加锁就可能死锁。数据库死锁的本质是事务 X 持有锁 1 等待锁 2事务 Y 持有锁 2 等待锁 1双方都不释放数据库必须主动回滚一方才能继续。开发时可以用下面这个事务模板让 Claude Code 生成“先查后更新”的安全代码BEGIN; SELECT id FROM users WHERE id $1 FOR UPDATE; INSERT INTO media_files (owner_id, original_name, storage_path, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5); UPDATE users SET file_count file_count 1 WHERE id $1; COMMIT;在实际项目里不要在一笔事务里做耗时的外部请求或图片压缩。事务应越短越好。图片压缩等耗时操作应该放到事务之外等文件处理完成后再更新数据库状态。对大多数 Node.js PostgreSQL 项目的建议是用连接池不要手动频繁创建连接。涉及多表更新时使用事务。如果要对某一行做“先查后改”使用SELECT ... FOR UPDATE。多个事务总是按相同的顺序访问表减少死锁概率。给查询频繁的字段加上索引比如owner_id created_at。7. 多媒体上传与图片处理的工程做法多媒体文件是网站开发里很容易被忽略的一个模块。很多人让 Claude Code 写完上传接口发现能传文件就收工了结果图片没压缩、原图横竖方向不对、文件类型伪造甚至服务上存储了大量用户上传的恶意脚本。这一章集中解决这类问题。7.1 media_files 表设计为了让数据库可以记录文件的处理和访问信息可以新建一张媒体表CREATE TABLE media_files ( id BIGSERIAL PRIMARY KEY, owner_id BIGINT NOT NULL, original_name VARCHAR(255) NOT NULL, storage_path TEXT NOT NULL, mime_type VARCHAR(100) NOT NULL, size_bytes BIGINT NOT NULL, width INT, height INT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT fk_media_files_user FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_media_files_owner_created ON media_files(owner_id, created_at DESC);原始文件名只用于展示不应该作为服务器的存储文件名。真实存储名应使用随机字符串或 UUID避免路径穿越问题。7.2 上传接口限制类型、重编码、压缩在 Node.js 项目里文件上传最容易碰到三个问题只检查扩展名不检查真实文件内容。攻击者可以把恶意文件改名成.jpg上传。把原始文件直接存放在静态目录导致可执行脚本被访问。没有限制体积大文件耗尽磁盘和带宽。更稳妥做法是接收文件后先用 sharp 读取文件内容尝试按图片解析。解析失败就直接拒绝解析成功则统一转成 JPEG 或 WebP 格式并压缩再写盘。这样即使原始文件里含有异常代码经过重编码后也会丢失执行能力。// src/routes/upload.js const express require(express); const multer require(multer); const sharp require(sharp); const crypto require(crypto); const path require(path); const fs require(fs/promises); const { pool } require(../pool); const { authRequired } require(../auth); const router express.Router(); const UPLOAD_DIR path.join(__dirname, ../../uploads); const upload multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, }); router.post(/upload, authRequired, upload.single(file), async (req, res) { if (!req.file) { return res.status(400).json({ message: 未接收到文件 }); } try { // 1. 尝试解析图片内容识别真实格式 let metadata; try { metadata await sharp(req.file.buffer).metadata(); } catch (err) { return res.status(400).json({ message: 不是有效的图片文件 }); } // 2. 统一转码压缩 const processedBuffer await sharp(req.file.buffer) .rotate() .resize({ width: 1920, withoutEnlargement: true }) .jpeg({ quality: 80 }) .toBuffer(); const fileId crypto.randomUUID(); const ext jpg; const filename ${fileId}.${ext}; const storagePath path.join(UPLOAD_DIR, filename); // 3. 写入磁盘 await fs.mkdir(UPLOAD_DIR, { recursive: true }); await fs.writeFile(storagePath, processedBuffer); // 4. 元数据写入数据库 const result await pool.query( INSERT INTO media_files (owner_id, original_name, storage_path, mime_type, size_bytes, width, height) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, original_name, mime_type, size_bytes, width, height, [ req.user.id, req.file.originalname, filename, image/jpeg, processedBuffer.length, metadata.width || null, metadata.height || null, ] ); return res.status(201).json({ file: result.rows[0] }); } catch (err) { console.error(upload error:, err); return res.status(500).json({ message: 服务器内部错误 }); } }); module.exports router;这段代码值得留意的设计点使用multer.memoryStorage()文件先进内存。优点是便于先处理再写盘缺点是 10MB 以上的大文件会占用内存。如果项目经常接收超大文件建议改成磁盘临时存储或分片上传。.rotate()会根据 EXIF 方向信息自动旋转图片避免手机拍摄图片在网页上显示颠倒。.jpeg({ quality: 80 })会把 PNG、WebP、GIF 都统一转成 JPEG。如果业务需要无背景透明图可以改用webp格式压缩率和清晰度都更好。数据库里的storage_path只保存文件名不保存绝对路径。这样便于后续把文件迁移到对象存储。在真实项目里文件落地到本机磁盘并不是终态。更常见的是上传到对象存储或云存储然后通过 CDN 提供服务。这个替换不影响上面的处理流程只是把“写入磁盘”换成“写入存储服务”而已。8. 自动化部署让发布从“手敲命令”变成“可重复流程”完成了鉴权、数据库和媒体处理之后网站还要解决最后一公里问题怎么发布。手动发布的最大问题不是慢而是不可复制——今天执行了哪些命令、依赖版本是不是一致、配置有没有遗漏都靠记忆一旦出错只能靠人肉回滚。用 Docker 和 CI 管道可以让整个发布过程固定下来。这里以 Docker Compose Jenkins 为例。如果你的团队使用 GitHub Actions 或其他 CI 工具思路是相通的。8.1 编写 Dockerfile先让项目可以被构建成镜像# Dockerfile FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omitdev COPY src ./src RUN mkdir -p /app/uploads EXPOSE 3000 CMD [node, src/server.js]注意npm ci会严格按照package-lock.json安装依赖比npm install更适合构建流程。如果项目里没有锁文件建议先运行一次 npm install 生成。8.2 使用 Docker Compose 编排数据库与应用# docker-compose.yml version: 3.8 services: db: image: postgres:16-alpine container_name: media-platform-db restart: unless-stopped environment: POSTGRES_DB: media_platform POSTGRES_USER: app_user POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - db_data:/var/lib/postgresql/data networks: - backend api: build: . container_name: media-platform-api restart: unless-stopped environment: DATABASE_URL: postgres://app_user:${DB_PASSWORD}db:5432/media_platform JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} NODE_ENV: production ports: - 3000:3000 depends_on: - db volumes: - upload_data:/app/uploads networks: - backend volumes: db_data: upload_data: networks: backend:这里的 db 服务没有向外映射端口。外部程序无法直接访问 5432只能通过容器内部网络访问这是很重要的安全边界。如果你需要本机查看数据库可以用数据库管理工具连接宿主机的某个临时映射端口但生产环境不要长期暴露数据库端口。8.3 使用 Jenkins 实现流水线发布下面是一个通用的 Jenkinsfile 示例pipeline { agent any environment { DB_PASSWORD credentials(media-db-password) JWT_ACCESS_SECRET credentials(media-jwt-access) JWT_REFRESH_SECRET credentials(media-jwt-refresh) } stages { stage(checkout) { steps { git branch: main, url: https://your-git-server/example/media-platform.git } } stage(install) { steps { sh npm ci } } stage(test) { steps { sh npm test } } stage(build image) { steps { sh docker compose build api } } stage(deploy) { steps { sh docker compose down || true docker compose up -d api } } } }在这个流程里密钥存放在 Jenkins 的凭据管理中不会写进仓库。流水线里的down || true用于处理第一次部署时还没有容器的情况。真实项目如果追求更高可用性应该用滚动发布而不是先 down 再 up否则部署窗口期服务会中断。如果你在本地开发时希望让 Claude Code 帮你写 CI 配置可以给出一个明确的约束请为当前项目编写一个 Jenkinsfile它需要完成以下步骤 1. 从 main 分支拉代码。 2. npm ci 安装依赖。 3. 运行测试。 4. 构建 Docker 镜像。 5. 通过 docker compose up -d 部署到服务器。 所有密钥使用 Jenkins credentials 注入不能出现在仓库文件里。9. Claude Code 使用中的常见问题与排查方法在实际使用中不管是安装、配置还是模型设置容易出现下面这些问题。这里按“现象—可能原因—排查方式—解决方案”的格式整理成一张表值得收藏备用。问题现象可能原因排查方式解决方案安装后找不到claude命令全局安装路径不在系统 PATH运行npm config get prefix查看全局目录将 npm 全局目录加入 PATH或改用原生安装方式首次启动提示登录或 API 配置未完成没有配置账号凭据或 API Key查看工具输出提示确认进入配置页按官方指引完成身份配置后再使用工具提示某模型标识符不受当前版本识别配置了当前版本不支持的模型名称或模型标识符写错打开配置文件检查模型名与实际所用模型是否一致改为当前版本支持的模型标识或升级工具版本在 VS Code 集成环境中无法读取项目文件打开目录与工作区目录不一致查看工具是否在项目根目录启动在项目根目录打开终端确认工作区路径一致Agent 修改了授权范围外的文件启动时权限设置过宽检查权限配置确认自动批准范围只授权当前项目目录涉及系统命令时逐条确认数据库连接失败.env中DATABASE_URL错误或数据库未启动检查数据库容器状态和环境变量修复连接字符串启动数据库容器鉴权接口返回 401Token 过期或请求头格式错误检查请求头是否存在Authorization: Bearer xxx重新登录获取 accessToken上传文件返回 400 “不是有效的图片文件”文件类型伪造或文件损坏打开文件确认真实格式统一通过 sharp 转码拒绝无法解析的文件CI 部署后服务起不来环境变量未注入或端口冲突查看容器日志docker compose logs api修正凭据、检查端口占用这里最值得展开的是“工具提示模型标识符不受当前版本识别”这个问题。很多用户在配置模型时会看到一段形如“xxx” is not a model this version of claude code recognizes的报错。这类问题的根源通常是当前 Claude Code 版本内置的可用