记账网站-post功能

加 POST 新增账单功能

改文件:

app/api/transactions/route.ts

现在这个文件里已经有 GET(),我们就在它下面继续加 POST()

把下面这段放到 GET() 后面:

export async function POST(request: Request) {
  try {
    const body = await request.json();

    const {
      type,
      amount,
      category,
      note,
      transaction_date,
    } = body;

    const [result] = await pool.query(
      `
      INSERT INTO transactions
      (type, amount, category, note, transaction_date)
      VALUES (?, ?, ?, ?, ?)
      `,
      [
        type,
        amount,
        category,
        note || null,
        transaction_date,
      ]
    );

    return NextResponse.json(
      {
        message: "新增账单成功",
        result,
      },
      { status: 201 }
    );
  } catch (error) {
    console.error("新增账单失败:", error);

    return NextResponse.json(
      { error: "新增账单失败" },
      { status: 500 }
    );
  }
}

这样你的这个接口就同时支持:

GET  /api/transactions
→ 查询账单

POST /api/transactions
→ 新增账单

测试 POST 接口本身能不能成功插入数据

保持 npm run dev 终端运行,然后新终端执行:

Invoke-RestMethod `
  -Uri "http://localhost:3000/api/transactions" `
  -Method POST `
  -ContentType "application/json" `
  -Body '{
    "type": "expense",
    "amount": 18.5,
    "category": "餐饮",
    "note": "测试晚饭",
    "transaction_date": "2026-08-14"
  }'

如果成功,你应该会看到类似:

message
-------
新增账单成功

然后再打开:

http://localhost:3000/api/transactions

刷新。

如果里面多出一条:

{
  "type": "expense",
  "amount": "18.50",
  "category": "餐饮",
  "note": "测试晚饭"
}

那就代表:

POST → Next.js → SQL INSERT → MySQL

整条新增链路正式跑通了。

浏览器中文写入 MySQL

打开:

app/page.tsx

原来的:

const handleSave = () => {
  const parsedAmount = parseFloat(amount);

  if (isNaN(parsedAmount) || parsedAmount <= 0) {
    return;
  }

  const newTransaction: Transaction = {
    id: Date.now(),
    title: note.trim() !== "" ? note.trim() : category,
    amount: type === "income" ? parsedAmount : -parsedAmount,
    date: "今天",
    category,
    note: note.trim() !== "" ? note.trim() : undefined,
  };

  setTransactions((prev) => [newTransaction, ...prev]);
  setIsModalOpen(false);
  resetForm();
};

目前只是:

点击保存 → 改 React 内存里的 transactions

所以刷新网页以后数据就没了。

我们要把它改成:

点击保存 → POST /api/transactions → MySQL 真正保存

可以改成这样:

const handleSave = async () => {
  const parsedAmount = parseFloat(amount);

  if (isNaN(parsedAmount) || parsedAmount <= 0) {
    return;
  }

  const today = new Date().toISOString().slice(0, 10);

  const response = await fetch("/api/transactions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type,
      amount: parsedAmount,
      category,
      note,
      transaction_date: today,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    console.error("新增账单失败:", data);
    return;
  }

  const newTransaction: Transaction = {
    id: Date.now(),
    title: note.trim() !== "" ? note.trim() : category,
    amount: type === "income" ? parsedAmount : -parsedAmount,
    date: "今天",
    category,
    note: note.trim() !== "" ? note.trim() : undefined,
  };

  setTransactions((prev) => [newTransaction, ...prev]);

  setIsModalOpen(false);
  resetForm();
};

这样就同时做两件事:

点击保存
   ↓
POST 到后端
   ↓
MySQL 真正新增一条账单
   ↓
前端也立即显示这条账单

点一次「+ 记一笔」,输入一笔中文账单测试。成功后再去 /api/transactions 看它是不是真的进入 MySQL。

首页 GET MySQL 真实账单,而不是 mock 数据。

前端 Transaction 类型还没完全对齐,先对齐

真实数据库是:

id               bigint(20)
type             enum('income','expense')
amount           decimal(10,2)
category         varchar(50)
note             varchar(255) / 可为空
transaction_date date
created_at       timestamp

所以app/page.tsx现在这段:

type Transaction = {
  id: number;
  title: string;
  amount: number;
  date: string;
  category: string;
  note?: string;
};

需要逐步改成更接近数据库真实结构的版本。

你先把它替换成:

type Transaction = {
  id: number;
  type: "income" | "expense";
  amount: number;
  category: string;
  note: string | null;
  transaction_date: string;
  created_at: string;
};

这样对应关系就是:

数据库                前端
id             ↔      id
type           ↔      type
amount         ↔      amount
category       ↔      category
note           ↔      note
transaction_date ↔    transaction_date
created_at     ↔      created_at

把模拟数据 mockTransactions 去掉。

在同一个 app/page.tsx 里,找到这一段:

const mockTransactions: Transaction[] = [
  { id: 1, title: "工资", amount: 300, date: "今天", category: "工资" },
  { id: 2, title: "吃饭", amount: -25, date: "今天", category: "餐饮" },
  { id: 3, title: "日用品", amount: -48, date: "昨天", category: "日用品" },
  { id: 4, title: "交通", amount: -20, date: "昨天", category: "交通" },
];

整段删掉。

然后再找到:

const [transactions, setTransactions] =
  useState<Transaction[]>(mockTransactions);

改成:

const [transactions, setTransactions] =
  useState<Transaction[]>([]);

意思就是:

页面刚打开时先没有账单,后面我们再从 MySQL 真实读取。

让页面打开时去 GET /api/transactions,把 MySQL 里的真实账单读进来。

还是改 app/page.tsx

先把最上面的:

import { useState } from "react";

改成:

import { useEffect, useState } from "react";

然后在 Home() 里面,放在这段后面:

const [transactions, setTransactions] =
  useState<Transaction[]>([]);

加上:

useEffect(() => {
  async function loadTransactions() {
    const response = await fetch("/api/transactions");
    const data = await response.json();

    const formatted = data.map((item: Transaction) => ({
      ...item,
      amount: Number(item.amount),
    }));

    setTransactions(formatted);
  }

  loadTransactions();
}, []);

因为 Transaction 类型已经改成数据库真实字段了,下面还有几处旧逻辑没跟着改,所以接下来要修这几块。

第一处,handleSave() 里这个 newTransaction 现在还是旧结构:

const newTransaction: Transaction = {
  id: Date.now(),
  title: note.trim() !== "" ? note.trim() : category,
  amount: type === "income" ? parsedAmount : -parsedAmount,
  date: "今天",
  category,
  note: note.trim() !== "" ? note.trim() : undefined,
};

这里要改成数据库字段对应的结构:

const newTransaction: Transaction = {
  id: Date.now(),
  type,
  amount: parsedAmount,
  category,
  note: note.trim() !== "" ? note.trim() : null,
  transaction_date: today,
  created_at: new Date().toISOString(),
};

第二处,你现在收入支出统计还在根据正负金额判断:

const monthlyIncome = transactions
  .filter((t) => t.amount > 0)
  .reduce((sum, t) => sum + t.amount, 0);

const monthlyExpense = transactions
  .filter((t) => t.amount < 0)
  .reduce((sum, t) => sum + Math.abs(t.amount), 0);

但我们数据库的规则是:

金额永远存正数,收入支出靠 type 区分。

所以改成:

const monthlyIncome = transactions
  .filter((t) => t.type === "income")
  .reduce((sum, t) => sum + t.amount, 0);

const monthlyExpense = transactions
  .filter((t) => t.type === "expense")
  .reduce((sum, t) => sum + t.amount, 0);

第三处,最近账单这里还在使用数据库里根本不存在的:

t.title
t.date

把:

<p className="font-medium text-gray-800">{t.title}</p>
<p className="text-xs text-gray-400">
  {t.category} · {t.date}
</p>

改成:

<p className="font-medium text-gray-800">
  {t.note || t.category}
</p>

<p className="text-xs text-gray-400">
  {t.category} · {t.transaction_date}
</p>

第四处,金额颜色和正负号也要改。

现在是:

className={`font-semibold ${
  t.amount > 0 ? "text-emerald-600" : "text-rose-600"
}`}

改成:

className={`font-semibold ${
  t.type === "income" ? "text-emerald-600" : "text-rose-600"
}`}

然后:

{t.amount > 0 ? "+" : "-"}¥{Math.abs(t.amount).toFixed(2)}

改成:

{t.type === "income" ? "+" : "-"}¥{t.amount.toFixed(2)}

这样整个前端就真正和数据库统一了:

一个小问题修改

handleSave() 成功之后现在是自己构造:

const newTransaction: Transaction = {
  id: Date.now(),
  ...
};

这里的 id: Date.now() 只是前端临时造的,不是数据库真正的自增 id。短期能显示,但以后做删除/修改时会出问题。

第一步:把读取账单单独变成一个函数

你现在有:

useEffect(() => {
  async function loadTransactions() {
    const response = await fetch("/api/transactions");
    const data = await response.json();

    const formatted = data.map((item: Transaction) => ({
      ...item,
      amount: Number(item.amount),
    }));

    setTransactions(formatted);
  }

  loadTransactions();
}, []);

把它整段替换成

const loadTransactions = async () => {
  const response = await fetch("/api/transactions");
  const data = await response.json();

  const formatted = data.map((item: Transaction) => ({
    ...item,
    amount: Number(item.amount),
  }));

  setTransactions(formatted);
};

useEffect(() => {
  loadTransactions();
}, []);

为什么这么改?

以前 loadTransactions() 被关在 useEffect 里面:

useEffect
└── loadTransactions

外面的 handleSave() 用不了它。

现在把它拿出来:

Home
├── loadTransactions()
├── useEffect()
└── handleSave()

这样:

  • 页面第一次打开 → 可以调用它
  • 新增成功 → 也可以调用它
  • 以后删除成功 → 还可以调用它

相当于我们把“刷新账单列表”做成了一个公共小工具。


第二步:修改 handleSave

你现在 handleSave 里面 POST 成功之后,有这一大段:

const newTransaction: Transaction = {
  id: Date.now(),
  type,
  amount: parsedAmount,
  category,
  note: note.trim() !== "" ? note.trim() : null,
  transaction_date: today,
  created_at: new Date().toISOString(),
};

setTransactions((prev) => [newTransaction, ...prev]);

setIsModalOpen(false);
resetForm();

把这里面的:

const newTransaction...

和:

setTransactions(...)

全部删掉。

改成:

await loadTransactions();

setIsModalOpen(false);
resetForm();

所以完整的 handleSave() 最后应该变成:

const handleSave = async () => {
  const parsedAmount = parseFloat(amount);

  if (isNaN(parsedAmount) || parsedAmount <= 0) {
    return;
  }

  const today = new Date().toISOString().slice(0, 10);

  const response = await fetch("/api/transactions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type,
      amount: parsedAmount,
      category,
      note,
      transaction_date: today,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    console.error("新增账单失败:", data);
    return;
  }

  await loadTransactions();

  setIsModalOpen(false);
  resetForm();
};

测试

如果开发服务器没开:

npm run dev

然后打开:

http://localhost:3000

先观察首页。

正常情况下,你应该直接看到数据库里的真实账单,而不是之前的「工资300、吃饭25、日用品48、交通20」那四条 mock 数据。

目前数据库里我们知道至少有:

25.00  餐饮  午饭
18.50  ??    ????
3.00   餐饮

所以如果看到这些,说明 GET → MySQL → 首页 成功。

然后再点:

「+ 记一笔」

这次建议故意测试一条收入,因为之前基本都在测试支出:

类型:收入
金额:100
分类:兼职
备注:测试兼职收入

点「保存」。

正常应该马上出现:

测试兼职收入
兼职 · 日期

+¥100.00

而且应该是绿色。

同时顶部:

本月收入
+¥100.00

然后按 F5 刷新网页

如果刷新之后这条 100 元兼职收入 还在,这就非常关键了——说明它不是 React 临时显示,而是真的进了 MySQL。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值