4. 活期存款账户余额查询与明细

任务目标

实现活期存款账户的实时余额查询与交易明细分页展示。核心要求:余额由交易流水实时汇总得出(不依赖单独余额字段,避免不一致);明细按时间倒序分页,返回当前页数据与分页元信息。

数据模型与核心逻辑

账户表存开户信息,流水表存每笔交易(正数入账、负数出账)。余额 = 期初余额 + 所有流水金额之和。分页用 page(从1开始)与 pageSize,按 created_at DESC, id DESC 排序保证稳定。

查询请求 accountId/page 余额汇总 期初 + SUM(流水) 明细分页 LIMIT/OFFSET 返回结构 { balance, page, pageSize, total, totalPages, items[] } items: [{ id, amount, type, createdAt }]

完整实现(Node.js + SQLite)

依赖:npm i better-sqlite3。以下单文件可直接运行,含建表、造数、查询与断言验证。

const Database = require('better-sqlite3');
const db = new Database(':memory:');

db.exec(`
CREATE TABLE account (
  id INTEGER PRIMARY KEY,
  owner TEXT NOT NULL,
  opening_balance INTEGER NOT NULL DEFAULT 0  -- 单位:分
);
CREATE TABLE txn (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  account_id INTEGER NOT NULL,
  amount INTEGER NOT NULL,          -- 正入账 负出账, 单位:分
  type TEXT NOT NULL,
  created_at TEXT NOT NULL,
  FOREIGN KEY(account_id) REFERENCES account(id)
);
CREATE INDEX idx_txn_acc_time ON txn(account_id, created_at DESC, id DESC);
`);

// 造数
db.prepare(`INSERT INTO account(id,owner,opening_balance) VALUES (1,'张三',100000)`).run();
const ins = db.prepare(`INSERT INTO txn(account_id,amount,type,created_at) VALUES (?,?,?,?)`);
const rows = [
  [1,  50000, 'deposit',  '2024-05-01T09:00:00'],
  [1, -20000, 'withdraw', '2024-05-02T10:30:00'],
  [1,  30000, 'deposit',  '2024-05-03T14:15:00'],
  [1, -15000, 'withdraw', '2024-05-04T08:45:00'],
  [1,  10000, 'deposit',  '2024-05-05T16:20:00'],
];
for (const r of rows) ins.run(...r);

// 余额查询: 期初 + 流水汇总
function getBalance(accountId) {
  const acc = db.prepare(`SELECT opening_balance FROM account WHERE id=?`).get(accountId);
  if (!acc) throw new Error('账户不存在');
  const { sum } = db.prepare(`SELECT COALESCE(SUM(amount),0) AS sum FROM txn WHERE account_id=?`).get(accountId);
  return acc.opening_balance + sum;
}

// 明细分页: 时间倒序
function getStatement(accountId, page = 1, pageSize = 3) {
  if (page < 1 || pageSize < 1) throw new Error('分页参数非法');
  const { total } = db.prepare(`SELECT COUNT(*) AS total FROM txn WHERE account_id=?`).get(accountId);
  const offset = (page - 1) * pageSize;
  const items = db.prepare(
    `SELECT id, amount, type, created_at AS createdAt FROM txn
     WHERE account_id=? ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`
  ).all(accountId, pageSize, offset);
  return {
    balance: getBalance(accountId),
    page, pageSize, total,
    totalPages: Math.ceil(total / pageSize),
    items,
  };
}

// 验证
const r1 = getStatement(1, 1, 3);
console.log('余额(分):', r1.balance);          // 100000+50000-20000+30000-15000+10000 = 155000
console.log('第1页:', r1.items.map(i => i.id));
console.log('total/totalPages:', r1.total, r1.totalPages);

const r2 = getStatement(1, 2, 3);
console.log('第2页:', r2.items.map(i => i.id));

// 断言
const assert = require('assert');
assert.strictEqual(r1.balance, 155000);
assert.deepStrictEqual(r1.items.map(i => i.id), [5, 4, 3]);
assert.deepStrictEqual(r2.items.map(i => i.id), [2, 1]);
assert.strictEqual(r1.total, 5);
assert.strictEqual(r1.totalPages, 2);
console.log('全部断言通过');

运行与验证

  1. 保存为 demo.js,执行 node demo.js。
  2. 观察输出:余额应为 155000(分,即 1550.00 元);第1页 id 为 [5,4,3],第2页为 [2,1],说明按时间倒序且分页正确。
  3. 通过条件:脚本打印 全部断言通过,无异常抛出。

关键点

  • 余额实时汇总而非读缓存字段,保证与流水一致;生产环境可加对账任务校验。
  • 排序加 id DESC 兜底,避免同一时间戳导致分页错乱或重复。
  • 金额用整数分存储,避免浮点误差。