【claude产品经理系列12】接入数据库——让数据永久保存

「产品经理用 Claude 实现产品」系列 · 第12篇

上一篇我们搭好了后端接口,但数据还存在内存里------重启服务器,数据就消失了。这一篇我们接入 SQLite 数据库,给数据找一个"永久的家"。完成后,你的前端、后端、数据库三层架构全部打通,这才是一个真正能用的完整系统。


一、为什么数据重启就没了?

上一篇跑通了接口------你能创建需求、获取列表,一切看起来很好。

但有个问题:重启服务器,所有数据消失了。

为什么?

因为我们把数据存在了 JavaScript 的数组里:

javascript 复制代码
let requirements = [];  // 数据在这里

这个数组存在内存里。内存是什么?是电脑的临时工作区。

对比 内存(数组) 数据库(文件)
速度 极快
持久性 程序关闭即消失 永久保存
容量 有限 几乎无限
查询能力 需要自己写代码遍历 SQL 语言,非常强大

数据库就是给数据找了一个"永久的家"------不管服务器重启多少次,数据都在。

这一篇,我们接入 SQLite 数据库,把所有内存操作替换成数据库操作。

💡 本系列全程使用 weelinking 访问 Claude,国内可稳定使用


二、SQLite 入门------最简单的数据库

2.1 为什么选 SQLite

市面上有很多数据库:MySQL、PostgreSQL、MongoDB......为什么我们选 SQLite?

数据库 特点 适合场景
MySQL 需要单独安装服务器 生产环境、大型项目
PostgreSQL 功能最全,配置最复杂 企业级项目
MongoDB 文档型数据库,配置复杂 非结构化数据
SQLite 一个文件,零配置 个人项目、原型、学习

SQLite 的核心优势:整个数据库就是一个 .db 文件,不需要安装任何服务,不需要配置,直接用。

对于我们这个"产品经理用 Claude 做的需求管理平台",SQLite 完全够用。等以后产品规模大了,迁移到 MySQL 也很简单------Claude 能帮你做。

2.2 SQL 语言------就像跟 Excel 说话

数据库用 SQL 语言来操作。SQL 其实很直观,你看一眼就能猜出来:

增(INSERT)------新增一行数据:

sql 复制代码
INSERT INTO requirements (title, priority, status)
VALUES ('用户可以筛选需求', 'P1', '待评审');

翻译:往 requirements 表里,插入一行,标题是"用户可以筛选需求",优先级 P1,状态"待评审"。

删(DELETE)------删除数据:

sql 复制代码
DELETE FROM requirements WHERE id = 1;

翻译:从 requirements 表里,删除 id=1 的那一行。

改(UPDATE)------修改数据:

sql 复制代码
UPDATE requirements SET status = '开发中' WHERE id = 1;

翻译:更新 requirements 表里 id=1 的行,把 status 改成"开发中"。

查(SELECT)------查询数据:

sql 复制代码
SELECT * FROM requirements WHERE priority = 'P0';

翻译:从 requirements 表里,查出所有 priority='P0' 的行。

你不需要自己写这些 SQL------Claude 会帮你写。 你只需要能大致看懂,知道它在做什么就够了。

2.3 数据表 = 你定义的字段结构

你在 PRD 里定义过字段:需求标题、优先级、状态、创建人、创建时间......

这就是数据表的结构。每个字段对应表里的一"列",每条需求数据对应表里的一"行"。

就像这样:

id title priority status createdAt
1 支持筛选需求 P1 待评审 2024-03-20
2 导出数据为Excel P2 开发中 2024-03-21
3 添加评论功能 P1 已完成 2024-03-22

这就是你脑子里的"字段定义"变成了真实存储的数据。


三、用 Claude 接入数据库

3.1 创建数据库连接

打开 Claude,新建对话:

你: 帮我在 Express 项目里接入 SQLite 数据库(使用 better-sqlite3 库),创建 src/models/db.js 数据库连接文件。

要求:

  1. 数据库文件存在项目根目录的 data/database.db
  2. 建立连接后,打印"数据库连接成功"
  3. 导出 db 实例,供其他模块使用

Claude 会生成:

javascript 复制代码
// src/models/db.js
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');

// 确保 data 目录存在
const dataDir = path.join(__dirname, '../../data');
if (!fs.existsSync(dataDir)) {
  fs.mkdirSync(dataDir, { recursive: true });
}

// 创建数据库连接
const db = new Database(path.join(dataDir, 'database.db'));

console.log('数据库连接成功');

module.exports = db;

3.2 创建数据表

你: 帮我创建数据库初始化脚本 src/models/init.js,创建以下数据表:

users 表(用户):

  • id:自增主键
  • username:用户名,唯一,不能为空
  • password:密码(加密后存储),不能为空
  • email:邮箱,可以为空
  • role:角色,默认 'user',可选值 'admin'/'user'/'guest'
  • createdAt:创建时间,默认当前时间

requirements 表(需求):

  • id:自增主键
  • title:需求标题,不能为空
  • priority:优先级,P0/P1/P2/P3,不能为空
  • type:需求类型,不能为空
  • description:详细描述,可以为空
  • status:状态,默认'待评审'
  • assignee:负责人,可以为空
  • tags:标签,JSON字符串存储,可以为空
  • dueDate:期望完成日期,可以为空
  • createdBy:创建人ID,关联 users.id
  • createdAt:创建时间,默认当前时间
  • updatedAt:更新时间,默认当前时间
  • deletedAt:软删除时间,可以为空

在 app.js 启动时调用这个初始化脚本。

Claude 会生成建表 SQL:

javascript 复制代码
// src/models/init.js
const db = require('./db');

const initDB = () => {
  // 创建用户表
  db.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      username TEXT UNIQUE NOT NULL,
      password TEXT NOT NULL,
      email TEXT,
      role TEXT DEFAULT 'user',
      createdAt TEXT DEFAULT (datetime('now', 'localtime'))
    )
  `);

  // 创建需求表
  db.exec(`
    CREATE TABLE IF NOT EXISTS requirements (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      priority TEXT NOT NULL,
      type TEXT NOT NULL,
      description TEXT,
      status TEXT DEFAULT '待评审',
      assignee TEXT,
      tags TEXT,
      dueDate TEXT,
      createdBy INTEGER,
      createdAt TEXT DEFAULT (datetime('now', 'localtime')),
      updatedAt TEXT DEFAULT (datetime('now', 'localtime')),
      deletedAt TEXT,
      FOREIGN KEY (createdBy) REFERENCES users(id)
    )
  `);

  console.log('数据表初始化完成');
};

module.exports = initDB;

启动服务器,终端显示:

复制代码
数据库连接成功
数据表初始化完成
Server running on port 3001

data/database.db 文件出现了------这就是你的数据库,一个文件存所有数据。

3.3 改造需求控制器

现在把上一篇内存操作的代码,全部替换成数据库操作。

你: 帮我改造 src/controllers/requirementController.js,把原来用数组存数据的方式,全部改成操作 SQLite 数据库:

  • 创建需求:用 INSERT 语句插入
  • 获取列表:用 SELECT 语句查询,支持 status/priority/keyword 筛选和分页
  • 获取详情:用 SELECT WHERE id=? 查询
  • 更新需求:用 UPDATE 语句更新,同时更新 updatedAt
  • 删除需求:软删除,UPDATE 设置 deletedAt

使用 better-sqlite3 的同步 API(prepare + run/get/all)

tags 字段用 JSON.stringify/JSON.parse 处理

Claude 会给你改造后的完整代码,核心部分类似这样:

javascript 复制代码
const db = require('../models/db');

// 创建需求
const createRequirement = (req, res) => {
  const { title, priority, type, description, assignee, tags, dueDate } = req.body;

  if (!title || !priority || !type) {
    return res.status(400).json({ success: false, message: '标题、优先级和类型为必填项' });
  }

  const stmt = db.prepare(`
    INSERT INTO requirements (title, priority, type, description, assignee, tags, dueDate, createdBy)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  `);

  const result = stmt.run(
    title,
    priority,
    type,
    description || null,
    assignee || null,
    tags ? JSON.stringify(tags) : null,
    dueDate || null,
    req.user?.id || 1
  );

  // 查询刚插入的数据返回
  const requirement = db.prepare('SELECT * FROM requirements WHERE id = ?').get(result.lastInsertRowid);
  requirement.tags = requirement.tags ? JSON.parse(requirement.tags) : [];

  res.status(201).json({ success: true, data: requirement });
};

// 获取列表(带筛选和分页)
const getRequirements = (req, res) => {
  const { status, priority, keyword, page = 1, pageSize = 20 } = req.query;

  let where = 'WHERE deletedAt IS NULL';
  const params = [];

  if (status) { where += ' AND status = ?'; params.push(status); }
  if (priority) { where += ' AND priority = ?'; params.push(priority); }
  if (keyword) { where += ' AND title LIKE ?'; params.push(`%${keyword}%`); }

  const total = db.prepare(`SELECT COUNT(*) as count FROM requirements ${where}`).get(...params).count;
  const list = db.prepare(`
    SELECT * FROM requirements ${where}
    ORDER BY createdAt DESC
    LIMIT ? OFFSET ?
  `).all(...params, Number(pageSize), (Number(page) - 1) * Number(pageSize));

  // 解析 tags JSON
  list.forEach(item => {
    item.tags = item.tags ? JSON.parse(item.tags) : [];
  });

  res.json({ success: true, data: { list, total, page: Number(page), pageSize: Number(pageSize) } });
};

同样,改造用户控制器:

你: 参考需求控制器的改造方式,帮我改造用户认证控制器,注册和登录都改成操作 users 数据库表。

Claude 给你改造后的代码,复制粘贴替换。

3.4 数据验证

改造完成后,验证数据库是否真的在工作:

  1. 用 Apifox 发送"创建需求"请求
  2. 创建成功后,重启服务器
  3. 再发送"获取列表"请求
  4. 数据还在!

这一刻很重要------数据不再消失了,这才是一个真正能"存东西"的系统。


四、用可视化工具查看数据库

命令行看数据库太抽象,我们用可视化工具。

4.1 安装 DB Browser for SQLite

下载地址:sqlitebrowser.org(免费)

安装后打开,选择"打开数据库",找到你的 data/database.db 文件。

你会看到:

  • 左侧:所有数据表(users、requirements)
  • 点击表名:看到所有字段
  • 点击"浏览数据":看到所有存储的记录

这就是你的数据,真实存储在文件里的。

你在 Apifox 里创建的那条需求,在这里清清楚楚展示:id、title、priority、status......和你设计的字段完全一致。

这个工具相当于数据库的"查看器",很直观。以后想知道"数据库里到底存了什么",打开它就行。


五、前后端联调------打通完整链路

数据库接好了,现在把前端也接起来,形成完整的数据流。

5.1 修改前端 API 调用

前端现在用的是假数据,要改成调用真实的后端接口。

打开 Claude 前端项目的对话:

你: 帮我在前端项目里配置 axios,用于调用后端接口:

  1. src/utils/request.js 里创建 axios 实例
  2. baseURL 设置为 http://localhost:3001
  3. 请求拦截器:从 localStorage 读取 token,加入 Header(Authorization: Bearer token
  4. 响应拦截器:统一处理错误,401 时跳转登录页
  5. 导出这个 axios 实例

Claude 生成:

javascript 复制代码
// src/utils/request.js
import axios from 'axios';

const request = axios.create({
  baseURL: 'http://localhost:3001',
  timeout: 10000,
});

// 请求拦截器:自动带上 token
request.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器:统一错误处理
request.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default request;

5.2 改造需求列表页

你: 帮我改造需求列表页(src/pages/Requirements.jsx),把原来的假数据改成调用后端接口:

  • 页面加载时调用 GET /api/requirements 获取列表
  • 搜索/筛选条件变化时重新请求
  • 创建成功后刷新列表
  • 显示加载状态(Loading)

使用刚才创建的 request 工具类

Claude 会修改列表页代码,关键部分:

javascript 复制代码
const [loading, setLoading] = useState(false);
const [requirements, setRequirements] = useState([]);
const [total, setTotal] = useState(0);

const fetchRequirements = async () => {
  setLoading(true);
  try {
    const res = await request.get('/api/requirements', {
      params: { status, priority, keyword, page, pageSize }
    });
    setRequirements(res.data.list);
    setTotal(res.data.total);
  } finally {
    setLoading(false);
  }
};

useEffect(() => {
  fetchRequirements();
}, [status, priority, keyword, page]);

5.3 改造登录页

你: 改造登录页,点击"登录"按钮时调用 POST /api/auth/login,登录成功后把 token 存到 localStorage,然后跳转到首页。

Claude 修改登录逻辑:

javascript 复制代码
const handleLogin = async (values) => {
  try {
    const res = await request.post('/api/auth/login', values);
    localStorage.setItem('token', res.data.token);
    localStorage.setItem('user', JSON.stringify(res.data.user));
    navigate('/home');
  } catch (error) {
    message.error(error.response?.data?.message || '登录失败');
  }
};

5.4 验证完整链路

现在做一次完整的端到端测试:

  1. 打开前端 http://localhost:5173,跳转到登录页
  2. 注册账号(如果没有),或直接登录
  3. 跳转到需求列表页------看到从数据库加载的真实数据
  4. 点击"创建需求",填写表单,提交
  5. 回到列表页------新需求出现了
  6. 重启后端服务器,刷新前端页面------数据还在!

从用户操作到页面显示,数据流完整打通了:

复制代码
用户点击"创建需求"
  → 前端发 POST 请求 → 后端接收
  → 后端校验数据 → 写入 SQLite 数据库
  → 返回创建成功 → 前端收到响应
  → 列表页刷新 → 显示新需求

这就是你在 PRD 里画的"数据流图",现在真实地跑起来了。


六、常见联调问题排查

问题1:跨域(CORS)

现象: 浏览器控制台报 CORS policy 错误,页面请求失败。

解决: 后端已配置 cors,检查 origin 是否正确:

javascript 复制代码
app.use(cors({
  origin: 'http://localhost:5173',  // 必须匹配前端地址
  credentials: true
}));

问题2:数据格式不一致

现象: 列表页报错 Cannot read property 'map' of undefined

原因: 前端期望 data.list 是数组,但后端返回格式不对。

解决: 把完整错误和前后端代码给 Claude:

"前端报错:Cannot read property 'map' of undefined,后端接口返回的数据是 [粘贴返回数据],前端代码是 [粘贴代码]。请帮我找出格式不一致的地方并修复。"

问题3:Token 没有带上

现象: 请求列表正常,但创建需求时报 401 未认证。

原因: 前端请求没带 token,或 token 存储的 key 不对。

解决: 打开浏览器 Network 面板,查看请求 Headers 里有没有 Authorization 字段。没有的话检查 localStorage.getItem('token') 是否有值。

问题4:SQLite 字段类型问题

现象: 存入的 tags 字段,取出来是字符串不是数组。

原因: SQLite 没有数组类型,tags 用 JSON 字符串存储,读出来需要 parse。

解决: 确认控制器里处理了 JSON.parse(item.tags)


七、阶段成果

完成本篇后,你的系统三层架构全部打通:

复制代码
前端(React)
    ↕ HTTP 请求(axios)
后端(Express)
    ↕ SQL 操作(better-sqlite3)
数据库(SQLite)
    → 存储在 data/database.db 文件
  • ✅ 数据持久化------重启不丢失
  • ✅ 前端真实调用后端接口
  • ✅ 后端真实读写数据库
  • ✅ 完整登录流程(含 token)
  • ✅ 需求数据真实存储和查询

这已经是一个"能用"的系统了。 虽然还有些功能没完善,但核心链路通了。

bash 复制代码
git add .
git commit -m "接入SQLite数据库,前后端联调完成"
git push

八、总结与下期预告

🎯 本篇核心要点

1. 数据库就是给数据找个"永久的家"。 内存是临时的,数据库是持久的。这是一个根本性的区别,任何真实的产品都需要数据库。

2. SQLite 是新手的最佳选择。 一个文件,零配置,立刻能用。等产品规模大了,再迁移 MySQL 也不难,Claude 能帮你做。

3. SQL 不需要背,Claude 会写。 你只需要能看懂,知道"这段 SQL 在查什么数据"就够了。

4. 前后端联调是真正的里程碑。 打通了完整链路,才算是一个真实的产品。从这一刻起,你操作的数据会真实保存、真实显示。

5. 遇到联调问题,描述清楚给 Claude。 把前端代码、后端代码、报错信息、网络请求详情都给它,它能帮你找到问题所在。

📌 记住这句话

三层架构打通的那一刻,你会感受到一种奇妙的满足感------你在前端创建了一条数据,它经过后端处理,真实地存进了数据库。这就是"做出一个产品"的感觉。

📣 下期预告

第13篇:《核心功能实现------需求的增删改查全流程》

数据库接好了,链路打通了,下一篇我们完善所有核心功能------让需求管理平台真正"好用"。

你会看到:

  • 完整的需求创建表单(优先级、状态、标签、富文本)
  • 列表筛选和搜索
  • 可拖拽的看板视图
  • 需求详情与编辑
  • 软删除与归档

完成后,作为产品经理,你要对自己的产品做一次完整的验收走查。

💡 本系列全程使用 weelinking 访问 Claude,国内可稳定使用


📎 配套资源

📋 数据库接入检查清单

复制代码
□ 数据库配置
  □ 安装 better-sqlite3
  □ 创建 src/models/db.js 数据库连接
  □ 创建 data/ 目录和 database.db 文件

□ 数据表创建
  □ users 表(含 role 字段)
  □ requirements 表(含 deletedAt 软删除字段)
  □ 启动时自动初始化数据表

□ 控制器改造
  □ requirementController:所有操作改为数据库操作
  □ authController:注册/登录改为数据库操作
  □ 处理 tags 的 JSON 序列化/反序列化

□ 前端联调
  □ 创建 axios 请求实例(src/utils/request.js)
  □ 配置请求拦截器(自动带 token)
  □ 配置响应拦截器(401 跳转登录)
  □ 需求列表页改为真实 API 调用
  □ 登录页改为真实 API 调用

□ 验证完整链路
  □ 登录成功,token 存入 localStorage
  □ 创建需求成功,列表刷新显示
  □ 重启服务器,数据不丢失
  □ 用 DB Browser 查看数据库内容

📋 SQL 速查表

sql 复制代码
-- 插入一条数据
INSERT INTO 表名 (字段1, 字段2) VALUES (值1, 值2);

-- 查询所有数据
SELECT * FROM 表名;

-- 条件查询
SELECT * FROM 表名 WHERE 字段 = 值;

-- 模糊查询
SELECT * FROM 表名 WHERE 字段 LIKE '%关键词%';

-- 多条件查询
SELECT * FROM 表名 WHERE 状态 = '待评审' AND 优先级 = 'P0';

-- 排序
SELECT * FROM 表名 ORDER BY 创建时间 DESC;

-- 分页
SELECT * FROM 表名 LIMIT 20 OFFSET 0;  -- 第1页,每页20条

-- 更新数据
UPDATE 表名 SET 字段 = 新值 WHERE id = 1;

-- 统计数量
SELECT COUNT(*) FROM 表名 WHERE 条件;

-- 按字段分组统计
SELECT 状态, COUNT(*) as 数量 FROM 表名 GROUP BY 状态;

📋 联调排查流程

复制代码
遇到联调问题时:

1. 先看浏览器控制台(F12 → Console)
   → 有没有 JS 错误

2. 看浏览器 Network 面板(F12 → Network)
   → 找到失败的请求
   → 看 Request Headers(有没有 Authorization)
   → 看 Response(后端返回的是什么)

3. 看后端终端
   → 有没有报错日志
   → 请求有没有打到后端

4. 把以上信息给 Claude
   → "前端报错:[...],Network 显示请求返回:[...],后端日志:[...],请帮我分析问题"

📌 系列导航: 产品经理用 Claude 实现产品 · 系列目录

上一篇: 第11篇:实现后端接口------数据在背后如何流动

下一篇: 第13篇:核心功能实现------需求的增删改查全流程
💡 本系列全程使用 weelinking 访问 Claude,国内可稳定使用

🚀 整个系列的核心理念:你不需要变成程序员,你只需要从"找人做"变成"自己能做"。

相关推荐
用户5191495848452 小时前
Linux Mint或Ubuntu上安装Nmap的三种不同方法
人工智能·aigc
追随者永远是胜利者2 小时前
(LeetCode-Hot100)283. 移动零
java·算法·leetcode·职场和发展·go
TAPD敏捷研发2 小时前
腾讯TAPD × CNB 联合赋能,开通TAPD项目管理工具就送价值1万元CNB云原生构建资源包!
人工智能·云原生·项目管理·代码管理·腾讯云ai代码助手·mcp·ai代码助手
咚咚王者2 小时前
人工智能之视觉领域 计算机视觉 第十二章 视频目标跟踪
人工智能·计算机视觉·音视频
Elastic 中国社区官方博客2 小时前
Elasticsearch:通过最小分数确保语义精度
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
陈广亮2 小时前
2026年 AI Agent 开发工具生态全景
人工智能
陈广亮2 小时前
AI Agent Skills 完全指南:让你的 AI 助手拥有超能力
人工智能
用户3231069488282 小时前
OpenSpec 综合指南:使用动机与实践
人工智能
一个天蝎座 白勺 程序猿2 小时前
Jenkins X + AI:重塑云原生时代的持续交付范式
人工智能·云原生·jenkins