From 20643b366fbafce122849b580bf62ae47b4b69f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E7=85=9C?= <1871263099@qq.com>
Date: Sat, 3 Oct 2026 12:06:36 +0800
Subject: [PATCH] feat: show liability account credit balances as deposits
---
apps/api/package.json | 2 +-
apps/api/prisma/schema.prisma | 1 +
apps/api/src/backup.ts | 7 +-
apps/api/src/calculation.ts | 1 +
apps/api/src/openapi.ts | 6 +
apps/api/src/portfolio.ts | 8 +-
apps/api/src/replay.ts | 2 +-
apps/api/src/validation.ts | 4 +-
apps/api/test/credit-balance.test.ts | 193 +++++++++++++++++++++++
apps/api/test/update.test.ts | 85 +++++++++-
apps/web/src/App.tsx | 81 ++++------
apps/web/src/api.ts | 15 +-
apps/web/src/locales/en.json | 6 +-
apps/web/src/locales/zh-Hant.json | 6 +-
docs/acceptance.md | 4 +
docs/architecture.md | 2 +
docs/credit-balance-preview.png | Bin 0 -> 50780 bytes
docs/update-credit-balance-2026-10-03.md | 26 +++
update.md | 4 +
19 files changed, 385 insertions(+), 68 deletions(-)
create mode 100644 apps/api/test/credit-balance.test.ts
create mode 100644 docs/credit-balance-preview.png
create mode 100644 docs/update-credit-balance-2026-10-03.md
diff --git a/apps/api/package.json b/apps/api/package.json
index 079dd3d..fbfb05d 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -9,7 +9,7 @@
"db:generate": "prisma generate",
"db:migrate": "node scripts/database.cjs deploy",
"db:status": "node scripts/database.cjs status",
- "test:integration": "tsx --test --test-concurrency=1 test/integration.test.ts test/privacy.test.ts test/icons.test.ts test/transfers.test.ts test/queries.test.ts test/debts.test.ts test/update-integration.test.ts test/record-edit.test.ts test/security-backup.test.ts",
+ "test:integration": "tsx --test --test-concurrency=1 test/integration.test.ts test/privacy.test.ts test/icons.test.ts test/transfers.test.ts test/queries.test.ts test/debts.test.ts test/update-integration.test.ts test/record-edit.test.ts test/security-backup.test.ts test/credit-balance.test.ts",
"test:performance": "tsx scripts/performance.ts after",
"icons:seed": "node scripts/seed-icons.cjs"
},
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 5e4679e..a25e5e1 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -103,6 +103,7 @@ model Revision {
positionId String @db.Char(36)
position Position @relation(fields:[positionId],references:[id],onDelete:Cascade)
/// 变更后的原币绝对余额,不是收支增量,禁止对全部历史求和
+ /// 负债账户正数为欠款,负数为溢缴存款;前端显示符号相反
amount Decimal @db.Decimal(24,8)
/// 北京时间业务时间对应的 UTC,精确到分钟
effectiveDate DateTime @db.DateTime(3)
diff --git a/apps/api/src/backup.ts b/apps/api/src/backup.ts
index 369cd87..1099823 100644
--- a/apps/api/src/backup.ts
+++ b/apps/api/src/backup.ts
@@ -161,11 +161,8 @@ export function validateBackup(raw: unknown) {
if (!b.currencies.includes(p.currency)) throw new BadRequestException('币种清单不完整');
}
for (const p of b.positions)
- if (
- (p.kind !== 'account' || p.side !== 'asset') &&
- p.revisions.some((r) => r.amount.startsWith('-'))
- )
- throw new BadRequestException('仅资产账户支持负余额');
+ if (p.kind !== 'account' && p.revisions.some((r) => r.amount.startsWith('-')))
+ throw new BadRequestException('仅账户支持负余额');
const planIds = new Set();
for (const plan of b.schedules || []) {
const source = ids.get(plan.sourceId),
diff --git a/apps/api/src/calculation.ts b/apps/api/src/calculation.ts
index 8b2f4e2..5df990a 100644
--- a/apps/api/src/calculation.ts
+++ b/apps/api/src/calculation.ts
@@ -75,6 +75,7 @@ export function totals(positions: Holding[], rates: Rate[], base: string, date:
const converted = fx ? amount.mul(fx.value) : null;
if (converted) {
if (p.side === 'asset') assets = assets.plus(converted);
+ else if (p.kind === 'account' && converted.isNegative()) assets = assets.minus(converted);
else liabilities = liabilities.plus(converted);
}
return {
diff --git a/apps/api/src/openapi.ts b/apps/api/src/openapi.ts
index 4c2f0da..fb602e6 100644
--- a/apps/api/src/openapi.ts
+++ b/apps/api/src/openapi.ts
@@ -60,6 +60,12 @@ export function setupOpenApi(app: INestApplication) {
const operation = entry[method];
if (!operation) continue;
operation.summary = `${method.toUpperCase()} ${path}`;
+ if (
+ (path === '/api/positions' && method === 'post') ||
+ (path.startsWith('/api/positions/{id}/revisions') && ['post', 'put'].includes(method))
+ )
+ operation.description =
+ 'amount 为数据库原币余额:资产账户正数为存款、负数为透支;负债账户正数为欠款、负数为溢缴存款。网页负债账户输入和显示使用相反符号(负数欠款、正数存款)。独立资产和借入借出债务本金须非负。';
if (
method === 'get' &&
['/api/history', '/api/positions/{id}/history', '/api/transfers'].includes(path)
diff --git a/apps/api/src/portfolio.ts b/apps/api/src/portfolio.ts
index 70ac284..84ab6f8 100644
--- a/apps/api/src/portfolio.ts
+++ b/apps/api/src/portfolio.ts
@@ -159,8 +159,8 @@ export class PortfolioController {
});
if (!p) throw new NotFoundException('项目不存在');
if (p.archived) throw new ConflictException('请先恢复归档项目');
- if (v.amount.startsWith('-') && (p.kind !== 'account' || p.side !== 'asset'))
- throw new BadRequestException('仅资产账户支持负余额');
+ if (v.amount.startsWith('-') && p.kind !== 'account')
+ throw new BadRequestException('仅账户支持负余额');
const lastTransfer = await tx.revision.findFirst({
where: { positionId: p.id, reason: { in: pairedReasons } },
orderBy: [{ effectiveDate: 'desc' }, { sequence: 'desc' }],
@@ -203,8 +203,8 @@ export class PortfolioController {
});
if (!p) throw new NotFoundException('项目不存在');
if (p.archived) throw new ConflictException('请先恢复归档项目');
- if (v.amount.startsWith('-') && (p.kind !== 'account' || p.side !== 'asset'))
- throw new BadRequestException('仅资产账户支持负余额');
+ if (v.amount.startsWith('-') && p.kind !== 'account')
+ throw new BadRequestException('仅账户支持负余额');
const original = await tx.revision.findFirst({ where: { id: revisionId, positionId: p.id } });
if (!original) throw new NotFoundException('历史记录不存在');
if (pairedReasons.includes(original.reason))
diff --git a/apps/api/src/replay.ts b/apps/api/src/replay.ts
index 4aa38f6..90c5cfb 100644
--- a/apps/api/src/replay.ts
+++ b/apps/api/src/replay.ts
@@ -55,7 +55,7 @@ export async function captureReplay(tx: Prisma.TransactionClient, userId: string
const amount = delta
? (balances.get(row.positionId) || new Decimal(0)).plus(delta)
: new Decimal(row.amount.toString());
- if (amount.isNegative() && (p.kind !== 'account' || p.side !== 'asset'))
+ if (amount.isNegative() && p.kind !== 'account')
throw new BadRequestException('修改后债务或资产金额不能为负数');
if (amount.abs().gte('10000000000000000'))
throw new BadRequestException('重算后的金额超出支持范围');
diff --git a/apps/api/src/validation.ts b/apps/api/src/validation.ts
index c0ae557..30316b8 100644
--- a/apps/api/src/validation.ts
+++ b/apps/api/src/validation.ts
@@ -98,8 +98,8 @@ export const positionInput = positionMeta
})
.strict()
.superRefine((p, c) => {
- if (p.amount.startsWith('-') && (p.kind !== 'account' || p.side !== 'asset'))
- c.addIssue({ code: 'custom', message: '仅资产账户支持负余额' });
+ if (p.amount.startsWith('-') && p.kind !== 'account')
+ c.addIssue({ code: 'custom', message: '仅账户支持负余额' });
if (
(p.kind === 'asset' && p.side !== 'asset') ||
(p.kind === 'account' &&
diff --git a/apps/api/test/credit-balance.test.ts b/apps/api/test/credit-balance.test.ts
new file mode 100644
index 0000000..aab1f19
--- /dev/null
+++ b/apps/api/test/credit-balance.test.ts
@@ -0,0 +1,193 @@
+import 'dotenv/config';
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID, randomBytes, createHash } from 'node:crypto';
+import { PrismaClient } from '@prisma/client';
+import { readBackupZip } from '../src/zip';
+const base = process.env.TEST_API_URL || 'http://127.0.0.1:3100/api';
+test('liability account credit balances survive revisions, replay, calendar, privacy and ZIP restore', async () => {
+ const db = new PrismaClient(),
+ ids: string[] = [];
+ const origin = process.env.WEB_ORIGIN === '*' ? 'http://localhost:5173' : process.env.WEB_ORIGIN!;
+ async function user() {
+ const u = await db.user.create({
+ data: { username: 'wp_credit_' + randomUUID(), passwordHash: 'unused' },
+ });
+ ids.push(u.id);
+ const token = randomBytes(32).toString('hex');
+ await db.session.create({
+ data: {
+ id: createHash('sha256').update(token).digest('hex'),
+ userId: u.id,
+ expiresAt: new Date(Date.now() + 3600000),
+ },
+ });
+ return { id: u.id, cookie: 'wp_session=' + token };
+ }
+ async function call(path: string, cookie: string, method = 'GET', body?: unknown) {
+ const res = await fetch(base + path, {
+ method,
+ headers: {
+ Cookie: cookie,
+ Origin: origin,
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
+ },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ return { status: res.status, data: await res.json() };
+ }
+ try {
+ const a = await user(),
+ b = await user();
+ const initial = {
+ name: 'Card',
+ kind: 'account',
+ side: 'liability',
+ category: 'credit_card',
+ currency: 'CNY',
+ amount: '100',
+ date: '2026-09-01T10:00',
+ };
+ const create = await call('/positions', a.cookie, 'POST', initial);
+ assert.equal(create.status, 201);
+ const id = create.data.id;
+ const balance = async (amount: string, date = '2026-09-02T10:00') =>
+ call('/positions/' + id + '/revisions', a.cookie, 'POST', {
+ amount,
+ date,
+ reason: 'balance',
+ });
+ const credit = await balance('-25');
+ assert.equal(credit.status, 201);
+ assert.equal((await call('/positions/' + id, a.cookie)).data.amount, '-25');
+ let totals = (await call('/overview', a.cookie)).data;
+ assert.equal(totals.assets, '25.00');
+ assert.equal(totals.liabilities, '0.00');
+ assert.equal(totals.net, '25.00');
+ let day = (await call('/calendar/day?date=2026-09-02', a.cookie)).data;
+ assert.equal(day.items[0].amount, '125');
+ assert.equal(day.items[0].direction, 'income');
+ assert.equal(
+ (
+ await call('/positions/' + id + '/revisions/' + credit.data.id, b.cookie, 'PUT', {
+ amount: '-30',
+ date: '2026-09-02T10:00',
+ })
+ ).status,
+ 404,
+ );
+ assert.equal(
+ (
+ await call('/positions/' + id + '/revisions/' + credit.data.id, a.cookie, 'PUT', {
+ amount: '-30',
+ date: '2026-09-02T10:00',
+ })
+ ).status,
+ 200,
+ );
+ assert.equal((await call('/overview', a.cookie)).data.assets, '30.00');
+ assert.equal(
+ (await call('/calendar/day?date=2026-09-02', a.cookie)).data.items[0].amount,
+ '130',
+ );
+ assert.equal(
+ (await call('/positions/' + id + '/revisions/' + credit.data.id, a.cookie, 'DELETE')).status,
+ 200,
+ );
+ assert.equal((await call('/overview', a.cookie)).data.liabilities, '100.00');
+ await balance('-20');
+ await balance('10', '2026-09-03T10:00');
+ day = (await call('/calendar/day?date=2026-09-03', a.cookie)).data;
+ assert.equal(day.items[0].direction, 'expense');
+ assert.equal(day.items[0].amount, '30');
+ const history = (await call('/positions/' + id + '/history', a.cookie)).data.items;
+ await call('/positions/' + id + '/revisions/' + history[0].id, a.cookie, 'DELETE');
+ assert.equal((await call('/positions/' + id, a.cookie)).data.amount, '-20');
+ assert.equal(
+ (
+ await call('/positions', a.cookie, 'POST', {
+ ...initial,
+ name: 'Initial credit',
+ amount: '-5.01',
+ })
+ ).status,
+ 201,
+ );
+ assert.equal(
+ (
+ await call('/positions', a.cookie, 'POST', {
+ ...initial,
+ name: 'Debt',
+ kind: 'debt',
+ category: 'personal',
+ amount: '10',
+ })
+ ).status,
+ 201,
+ );
+ assert.equal(
+ (
+ await call('/positions', a.cookie, 'POST', {
+ ...initial,
+ name: 'Invalid debt',
+ kind: 'debt',
+ category: 'personal',
+ amount: '-1',
+ })
+ ).status,
+ 400,
+ );
+ assert.equal(
+ (
+ await call('/positions', a.cookie, 'POST', {
+ ...initial,
+ name: 'Invalid asset',
+ kind: 'asset',
+ side: 'asset',
+ category: 'other',
+ amount: '-1',
+ })
+ ).status,
+ 400,
+ );
+ totals = (await call('/overview', a.cookie)).data;
+ assert.equal(totals.assets, '25.01');
+ assert.equal(totals.liabilities, '10.00');
+ assert.equal(totals.net, '15.01');
+ const trend = (await call('/trend?from=2026-09-01&to=2026-09-03', a.cookie)).data.items;
+ assert.equal(trend[0].assets, '5.01');
+ assert.equal(trend[0].liabilities, '110.00');
+ assert.equal(trend[1].assets, '25.01');
+ assert.equal(trend[1].liabilities, '10.00');
+ const zip = await fetch(base + '/backup', { headers: { Cookie: a.cookie } });
+ assert.equal(zip.status, 200);
+ const backup: any = await readBackupZip(Buffer.from(await zip.arrayBuffer()));
+ assert.equal(
+ (await call('/backup/import', b.cookie, 'POST', { confirmed: true, backup })).status,
+ 201,
+ );
+ const restored = (await call('/overview', b.cookie)).data;
+ assert.equal(restored.assets, '25.01');
+ assert.equal(restored.liabilities, '10.00');
+ assert.equal(restored.net, '15.01');
+ assert.equal(
+ (await call('/calendar/day?date=2026-09-02', b.cookie)).data.items[0].amount,
+ '120',
+ );
+ assert.equal(
+ (
+ await call('/positions/' + id, a.cookie, 'PATCH', {
+ name: 'Card',
+ category: 'credit_card',
+ hidden: true,
+ })
+ ).status,
+ 200,
+ );
+ assert.equal((await call('/positions/' + id, a.cookie)).status, 404);
+ assert.equal((await call('/overview', a.cookie)).data.assets, '5.01');
+ } finally {
+ await db.user.deleteMany({ where: { id: { in: ids } } });
+ await db.$disconnect();
+ }
+});
diff --git a/apps/api/test/update.test.ts b/apps/api/test/update.test.ts
index 7f66461..8a143a5 100644
--- a/apps/api/test/update.test.ts
+++ b/apps/api/test/update.test.ts
@@ -7,7 +7,7 @@ import { executeMovement } from '../src/transfers';
import { transferInput, positionInput, revisionInput } from '../src/validation';
import { scheduleInput, occurrenceId, nextOccurrence, SchedulesController } from '../src/schedules';
import { trend, totals, type Holding } from '../src/calculation';
-import { positionAmount, money, type Position } from '../../web/src/api';
+import { positionAmount, accountInputAmount, money, type Position } from '../../web/src/api';
const now = new Date(Date.now() - 60000);
const date = new Date(+now + 8 * 3600000).toISOString().slice(0, 16);
function fixture(
@@ -114,7 +114,7 @@ test('transfer into an overdrawn account is allowed; debt overpayment still fail
assert.equal(debt.balances.get(debt.sourceId), '-11');
assert.equal(debt.balances.get(debt.targetId), '0');
});
-test('negative balances are confined to asset accounts; group names and principal remain validated', () => {
+test('negative balances are confined to accounts; group names and debt principal remain validated', () => {
const data = {
name: 'test',
category: 'bank',
@@ -127,7 +127,7 @@ test('negative balances are confined to asset accounts; group names and principa
};
assert.equal(positionInput.parse(data).groupName, '日常');
assert.equal(positionInput.safeParse({ ...data, kind: 'debt' }).success, false);
- assert.equal(positionInput.safeParse({ ...data, side: 'liability' }).success, false);
+ assert.equal(positionInput.safeParse({ ...data, side: 'liability' }).success, true);
assert.equal(positionInput.safeParse({ ...data, groupName: 'a'.repeat(61) }).success, false);
assert.equal(revisionInput.parse({ amount: '-0.00000001', date }).amount, '-0.00000001');
assert.equal(transferInput.safeParse({ ...fixture().request, amount: '-1' }).success, false);
@@ -343,3 +343,82 @@ test('calendar replays only the range, preserves fractional totals, and hides in
assert.equal(month.items[2].complete, false);
assert.equal(month.items.length, 30);
});
+
+test('liability account credit balances invert display and input without losing decimal precision', () => {
+ const card = {
+ kind: 'account',
+ side: 'liability',
+ amount: '-9999999999999999.12345678',
+ } as Position;
+ assert.equal(positionAmount(card), '9999999999999999.12345678');
+ assert.equal(positionAmount(card, '12.12345678'), '-12.12345678');
+ assert.equal(positionAmount(card, '-0.00000000'), '0.00000000');
+ assert.equal(accountInputAmount(card, '12.12345678'), '-12.12345678');
+ assert.equal(accountInputAmount(card, '-12.12345678'), '12.12345678');
+ assert.equal(accountInputAmount({ ...card, kind: 'debt' }, '12.12345678'), '12.12345678');
+});
+test('credit balances count as assets across zero, retain FX attribution and do not offset other liabilities', () => {
+ const card: Holding = {
+ id: randomUUID(),
+ name: 'Card',
+ kind: 'account',
+ side: 'liability',
+ currency: 'USD',
+ revisions: [
+ {
+ id: 'a',
+ amount: '100',
+ effectiveDate: new Date('2026-09-01'),
+ reason: 'initial',
+ notes: '',
+ },
+ {
+ id: 'b',
+ amount: '-25',
+ effectiveDate: new Date('2026-09-02'),
+ reason: 'balance',
+ notes: '',
+ },
+ ],
+ };
+ const rates = [
+ {
+ currency: 'USD',
+ baseCurrency: 'CNY',
+ date: new Date('2026-09-01'),
+ rate: '7',
+ source: 'manual',
+ },
+ {
+ currency: 'USD',
+ baseCurrency: 'CNY',
+ date: new Date('2026-09-02'),
+ rate: '8',
+ source: 'manual',
+ },
+ ];
+ const owing = totals([card], rates, 'CNY', '2026-09-01');
+ assert.equal(owing.assets, '0.00');
+ assert.equal(owing.liabilities, '700.00');
+ assert.equal(owing.net, '-700.00');
+ const credit = totals([card], rates, 'CNY', '2026-09-02');
+ assert.equal(credit.assets, '200.00');
+ assert.equal(credit.liabilities, '0.00');
+ assert.equal(credit.net, '200.00');
+ const rows = trend([card], rates, 'CNY', '2026-09-01', '2026-09-02');
+ assert.equal(rows[1].fxChange, '-100.00');
+ assert.equal(rows[1].balanceChange, '1000.00');
+ const debt = {
+ ...card,
+ id: randomUUID(),
+ kind: 'debt',
+ currency: 'CNY',
+ revisions: [{ ...card.revisions[0], amount: '20' }],
+ };
+ const mixed = totals([card, debt], rates, 'CNY', '2026-09-02');
+ assert.equal(mixed.assets, '200.00');
+ assert.equal(mixed.liabilities, '20.00');
+ assert.equal(mixed.net, '180.00');
+ assert.equal(cashflowDelta('liability', 'balance', '100', '-25').toFixed(), '125');
+ assert.equal(cashflowDelta('liability', 'balance', '-25', '10').toFixed(), '-35');
+});
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 1676651..6e25c60 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -25,6 +25,7 @@ import {
currencies,
money,
positionAmount,
+ accountInputAmount,
today,
nowMinute,
displayTime,
@@ -1157,16 +1158,14 @@ export default function App() {
? tr(p.side === 'asset' ? '借款(待收)' : '欠款(待还)')
: p.side === 'asset'
? tr('资产')
- : tr('负债')}
+ : tr(
+ p.kind === 'account' && p.amount.startsWith('-')
+ ? '溢缴存款'
+ : '负债',
+ )}
{p.archived ? tr(' · 已归档') : ''}
-
+
{money(positionAmount(p), p.currency)}
{p.hidden && {tr('隐藏项目')}}
@@ -1368,14 +1367,16 @@ export default function App() {
? tr(p.side === 'asset' ? '借款(待收)' : '欠款(待还)')
: p.side === 'asset'
? tr('资产')
- : tr('负债')}
+ : tr(
+ p.kind === 'account' && p.amount.startsWith('-')
+ ? '溢缴存款'
+ : '负债',
+ )}
{p.archived ? tr(' · 已归档') : ''}
{money(positionAmount(p), p.currency)}
@@ -2262,7 +2263,7 @@ export default function App() {
api(
'/positions/' + mp!.id + '/revisions' + (modal.h ? '/' + modal.h.id : ''),
modal.h ? 'PUT' : 'POST',
- v,
+ { ...v, amount: accountInputAmount(mp!, String(v.amount)) },
),
tr('金额历史已保存,总览已重新计算'),
);
@@ -2292,6 +2293,7 @@ export default function App() {
...(f.has('iconId') ? { iconId: f.get('iconId') || null } : {}),
kind,
side,
+ amount: accountInputAmount({ kind, side }, String(v.amount)),
hidden: f.get('hidden') === 'on',
}),
tr('项目已添加'),
@@ -2360,7 +2362,7 @@ export default function App() {
{modal.kind === 'account' && (
- {tr('信用卡、贷款账户自动计入负债,其余账户计入资产。')}
+ {tr('信用卡、贷款账户:负数为欠款,正数为溢缴存款;其余账户按实际余额填写。')}
)}
>
@@ -2406,13 +2408,16 @@ export default function App() {
inputMode="decimal"
required
pattern={
- (modal.p?.side === 'asset' && modal.p?.kind === 'account') ||
- modal.kind === 'account'
+ modal.p?.kind === 'account' || modal.kind === 'account'
? '-?(0|[1-9][0-9]{0,15})(\\.[0-9]{1,8})?'
: '(0|[1-9][0-9]{0,15})(\\.[0-9]{1,8})?'
}
defaultValue={
- modal.h?.after || modal.p?.amount || (modal.kind === 'debt' ? '0' : '')
+ modal.p
+ ? accountInputAmount(modal.p, modal.h?.after || modal.p.amount)
+ : modal.kind === 'debt'
+ ? '0'
+ : ''
}
placeholder="0.00"
/>
@@ -2430,6 +2435,13 @@ export default function App() {
/>
)}
+ {['revision', 'correction'].includes(modal.kind) &&
+ modal.p?.kind === 'account' &&
+ modal.p.side === 'liability' && (
+
+ {tr('负数表示欠款,正数表示溢缴存款;溢缴部分计入总资产。')}
+
+ )}
{modal.kind === 'revision' && (
<>
@@ -2575,42 +2587,19 @@ function HistoryTable({
- {money(
- h.kind === 'debt' && h.side === 'liability' && Number(h.before) !== 0
- ? '-' + h.before
- : h.before,
- h.currency,
- )}
+ {money(positionAmount(h, h.before), h.currency)}
|
- {money(
- h.kind === 'debt' && h.side === 'liability' && Number(h.after) !== 0
- ? '-' + h.after
- : h.after,
- h.currency,
- )}
+ {money(positionAmount(h, h.after), h.currency)}
|
- {money(
- h.kind === 'debt' && h.side === 'liability'
- ? h.delta.startsWith('-')
- ? h.delta.slice(1)
- : Number(h.delta) !== 0
- ? '-' + h.delta
- : h.delta
- : h.delta,
- h.currency,
- )}
+ {money(positionAmount(h, h.delta), h.currency)}
|
{h.notes || '—'} |
{correct && (
diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts
index cb991f7..27feb08 100644
--- a/apps/web/src/api.ts
+++ b/apps/web/src/api.ts
@@ -140,8 +140,15 @@ export function requestUuid() {
export type PageResult = { items: T[]; nextCursor: string | null; revealed: boolean };
-export function positionAmount(p: Position, value = p.amount) {
- return p.kind === 'debt' && p.side === 'liability' && !/^[-]?0(?:\.0+)?$/.test(value)
- ? '-' + value.replace(/^-/, '')
- : value;
+// Stored liability balances are amounts owed; negative account balances are credit.
+export function positionAmount(
+ p: Pick & { amount?: string },
+ value = p.amount || '0',
+) {
+ if (p.side !== 'liability' || /^-?0(?:\.0+)?$/.test(value))
+ return value.replace(/^-0(?=\.0+$|$)/, '0');
+ return value.startsWith('-') ? value.slice(1) : '-' + value;
+}
+export function accountInputAmount(p: Pick, value: string) {
+ return p.kind === 'account' ? positionAmount(p, value) : value;
}
diff --git a/apps/web/src/locales/en.json b/apps/web/src/locales/en.json
index 92a35d9..309b8f9 100644
--- a/apps/web/src/locales/en.json
+++ b/apps/web/src/locales/en.json
@@ -463,5 +463,9 @@
"备份下载失败": "Backup download failed.",
"备份文件格式错误": "Invalid backup file format.",
"备份文件为空": "The backup file is empty.",
- "备份已生成,请确认文件已保存": "Backup generated. Please confirm the file has been saved."
+ "备份已生成,请确认文件已保存": "Backup generated. Please confirm the file has been saved.",
+ "溢缴存款": "Credit balance",
+ "信用卡、贷款账户:负数为欠款,正数为溢缴存款;其余账户按实际余额填写。": "Credit cards and loan accounts: negative means debt, positive means credit. Enter the actual balance for other accounts.",
+ "负数表示欠款,正数表示溢缴存款;溢缴部分计入总资产。": "Negative means debt, positive means credit. Credit balances count towards total assets.",
+ "仅账户支持负余额": "Only accounts support negative balances."
}
diff --git a/apps/web/src/locales/zh-Hant.json b/apps/web/src/locales/zh-Hant.json
index d8f301a..df6b946 100644
--- a/apps/web/src/locales/zh-Hant.json
+++ b/apps/web/src/locales/zh-Hant.json
@@ -463,5 +463,9 @@
"备份下载失败": "備份下載失敗",
"备份文件格式错误": "備份檔案格式錯誤",
"备份文件为空": "備份檔案為空",
- "备份已生成,请确认文件已保存": "備份已產生,請確認檔案已保存"
+ "备份已生成,请确认文件已保存": "備份已產生,請確認檔案已保存",
+ "溢缴存款": "溢繳存款",
+ "信用卡、贷款账户:负数为欠款,正数为溢缴存款;其余账户按实际余额填写。": "信用卡、貸款賬戶:負數為欠款,正數為溢繳存款;其餘賬戶按實際餘額填寫。",
+ "负数表示欠款,正数表示溢缴存款;溢缴部分计入总资产。": "負數表示欠款,正數表示溢繳存款;溢繳部分計入總資產。",
+ "仅账户支持负余额": "僅賬戶支援負餘額"
}
diff --git a/docs/acceptance.md b/docs/acceptance.md
index 6af4887..f50144c 100644
--- a/docs/acceptance.md
+++ b/docs/acceptance.md
@@ -66,3 +66,7 @@
## 2026-10-03 账号、删除与备份修复
28 项单元测试、11 项真实 MySQL 集成检查、前后端类型检查和生产构建通过。新增账号凭据变更与会话撤销、配对历史直接删除、零历史项目及 ZIP 上传恢复验证;浏览器确认删除弹窗和修改后的自动刷新。详情与验证边界见 [更新说明](update-2026-10-03.md)。无数据库结构变更。
+
+## 2026-10-03 负债账户溢缴存款
+
+30 项单元测试、12 项真实 MySQL 集成检查、类型检查及构建通过。浏览器验证信用卡从欠款转为正数溢缴存款并计入总资产,余额历史与总览自动刷新;补充金额精度、趋势、日历、权限和备份恢复断言。无数据库结构变更。详见 [更新说明](update-credit-balance-2026-10-03.md)。
diff --git a/docs/architecture.md b/docs/architecture.md
index 81013bc..0eff0a7 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -45,3 +45,5 @@ User.hiddenMenus 保存受枚举校验的菜单键,settings 不可隐藏;Use
2026-10-02 记录编辑更新:允许更正转账之前的独立余额历史。`replay.ts` 重放涉及项目的历史,绝对余额保留为锚点,配对资金往来与定时支出按增量更新;整笔修改/删除与两侧余额在同一事务提交。单侧配对历史禁止独立更正;2026-10-03 更新后,从任一侧删除会原子删除完整配对记录。日历读取保存后的账户历史重算。完整边界见 [更新说明](update-2026-10-02.md)。
2026-10-03 修复:历史重放允许没有绝对余额锚点时从零计算增量,空历史项目金额为零;备份校验、转账和计划支出使用相同规则。账号凭据变更锁定用户行、验证当前密码、更新凭据并在同一事务撤销旧会话和创建新会话;无需新增字段。/auth/me 返回完整隐藏授权状态和会话时间。前端 mutation 更新数据版本,以当前页面重新读取数据,旧视图请求通过 generation 检查丢弃。见 [更新说明](update-2026-10-03.md)。
+
+2026-10-03 负债账户溢缴更新:负债账户存储正数欠款、负数溢缴;前端显示和输入取相反符号。统计将负债账户的负余额按汇率转换为正资产,正欠款计入负债,两类分别汇总。独立资产和借入借出债务仍要求非负本金,旧数据和 API 金额含义保留。详见 [溢缴更新说明](update-credit-balance-2026-10-03.md)。
diff --git a/docs/credit-balance-preview.png b/docs/credit-balance-preview.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca59bc5e79b81fd9b22e6efa9b04ca7b06d9e735
GIT binary patch
literal 50780
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
aqa)j=vWR0lbZxXf|E5^=ve@z8>Hi7jV=TS^
literal 0
HcmV?d00001
diff --git a/docs/update-credit-balance-2026-10-03.md b/docs/update-credit-balance-2026-10-03.md
new file mode 100644
index 0000000..a36eb02
--- /dev/null
+++ b/docs/update-credit-balance-2026-10-03.md
@@ -0,0 +1,26 @@
+# 2026-10-03 负债账户溢缴存款
+
+## 使用方式
+
+信用卡和贷款等负债账户的余额输入、列表、详情及历史统一使用有符号余额:负数表示欠款,正数表示溢缴存款。例如欠款 100 填写 -100,溢缴存款 100 填写 100。零表示无欠款或存款。快速记账、余额更新、历史更正和新建账户使用相同规则,表单说明已同步更新。
+
+溢缴账户标记为“溢缴存款”,正数不显示为负债红色;欠款和负向变化显示红色。溢缴款按原币和可用历史汇率计入总资产,欠款计入总负债,不用溢缴款抵消其他账户的负债。净资产始终按总资产减总负债计算,趋势和汇率归因也按同一逻辑重放。
+
+收支日历沿用负债方向计算。例如从欠款 100 到溢缴存款 25,余额净增加 125;从溢缴存款 25 到欠款 10,余额净减少 35。初始余额、估值和配对资金操作仍按原规则排除。
+
+## 数据与兼容性
+
+数据库和现有 API 保留原来的负债本金符号:正数为欠款,负数为溢缴存款;网页输入和显示进行无损字符串符号转换。已有正数欠款数据仍表示同一欠款,原币金额没有批量改写。资产账户仍按原币余额存储和显示;独立资产及借入借出债务仍要求非负本金。
+
+创建、更新、更正、删除后的历史重放和备份校验均允许负债账户的负余额。ZIP 恢复保存负债账户类型、原币和历史,不把项目永久改为资产类型;同一账户可以再次从溢缴转为欠款。此次未扩大账户转账或计划支持的账户范围。
+
+复用现有字段,无数据库结构变更和新迁移。数据库已有表、字段注释保留;Prisma 的余额字段注释补充了负债符号含义,OpenAPI 补充了网页与 API 的符号约定。
+
+## 验证
+
+- 30 项单元测试、12 项真实 MySQL 集成检查通过;前后端类型检查和生产构建通过。
+- 新增断言覆盖八位小数和大金额字符串无损转换、跨越零余额、溢缴款与其他债务分别汇总、历史趋势及 FX 归因、日历方向、更正和删除重算、跨用户和隐藏权限、ZIP 导出与恢复。
+- 浏览器使用临时模拟账号,将欠款 -100 更新为溢缴存款 120.12345678,详情、历史和总览自动刷新;总资产 120.12、总负债 0.00、净资产 120.12。未操作实际用户账户或进行真实资金转移。
+- 临时账号、模拟数据和预览配置在验收后清理。保留用户原有 Vite 配置修改,仅本地提交,不 push。
+
+
diff --git a/update.md b/update.md
index 3ba82b1..3dced8b 100644
--- a/update.md
+++ b/update.md
@@ -32,6 +32,8 @@
- ~~删除记录有问题,有时候无法删除,UI也不对~~ — 已完成并验证:2026-10-03 11:22
- ~~删除记录后部分UI需要手动刷新~~ — 已完成并验证:2026-10-03 11:22
- ~~备份功能出现问题~~ — 已完成并验证:2026-10-03 11:22
+- ~~负债(如信用卡)也可以显示为正数变成存款~~ — 已完成并验证:2026-10-03 12:05
+- 分组可以排序
验证与部署边界见 [更新说明](docs/update-2026-10-02.md)。
@@ -40,3 +42,5 @@
本轮补充验收完成:2026-10-02 18:44。13 个迁移状态最新,34 个内置图标已初始化;28 项单元测试、10 项真实数据库集成检查通过,9 张表与 88 个字段注释已核验。重新检查文档,无新增未完成要求。
本轮验收完成:2026-10-03 11:22。28 项单元测试、11 项真实 MySQL 集成检查、前后端类型检查与生产构建通过;未新增数据库表或字段。账号、删除与备份更新详情见 [更新说明](docs/update-2026-10-03.md)。重新检查文档,无新增未完成要求。
+
+负债账户溢缴验收完成:2026-10-03 12:05。30 项单元测试、12 项真实 MySQL 集成检查、类型检查和构建通过;支持信用卡等负债账户正数存款显示及总资产分类,已有数据符号保留。详见 [更新说明](docs/update-credit-balance-2026-10-03.md)。复查发现新增“分组可以排序”,继续处理。