feat: migrate backend storage to postgres
This commit is contained in:
@@ -127,6 +127,33 @@ jobs:
|
||||
echo "⏳ 等待服务启动..."
|
||||
sleep 10
|
||||
|
||||
# 确认PostgreSQL已就绪
|
||||
echo "⏳ 等待PostgreSQL就绪..."
|
||||
POSTGRES_READY=0
|
||||
for i in {1..10}; do
|
||||
if sudo docker-compose exec -T postgres pg_isready -U kt_financial -d kt_financial > /dev/null 2>&1; then
|
||||
echo "✅ PostgreSQL 已就绪"
|
||||
POSTGRES_READY=1
|
||||
break
|
||||
fi
|
||||
echo " 第${i}次重试..."
|
||||
sleep 3
|
||||
done
|
||||
if [ "$POSTGRES_READY" -ne 1 ]; then
|
||||
echo "❌ PostgreSQL 未在预期时间内就绪"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 导入财务交易数据
|
||||
echo "📦 导入财务数据..."
|
||||
sudo docker-compose exec -T kt-financial \
|
||||
bash -lc "pnpm --filter @vben/backend import:data -- --csv /app/data/finance/finance-combined.csv --year 2025"
|
||||
|
||||
# 验证数据条数
|
||||
echo "🔢 检查交易记录条数..."
|
||||
sudo docker-compose exec -T postgres \
|
||||
psql -U kt_financial -d kt_financial -c "SELECT COUNT(*) AS transaction_count FROM finance_transactions;"
|
||||
|
||||
# 1. 检查容器状态
|
||||
echo "📊 容器状态:"
|
||||
sudo docker-compose ps
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- **功能**: Telegram Bot通知系统
|
||||
|
||||
#### 已完成功能:
|
||||
|
||||
1. ✅ 基础Telegram通知
|
||||
2. ✅ 频率控制和去重
|
||||
3. ✅ 失败重试机制
|
||||
@@ -16,15 +17,38 @@
|
||||
5. ✅ 优先级设置
|
||||
|
||||
#### 配置信息:
|
||||
|
||||
- Bot Token: 已配置
|
||||
- Chat ID: 1102887169
|
||||
- Bot用户名: @ktcaiwubot
|
||||
|
||||
#### 测试结果:
|
||||
|
||||
- ✅ Telegram消息发送成功
|
||||
- ✅ API接口已实现
|
||||
- 🚧 前端界面待完成
|
||||
|
||||
---
|
||||
|
||||
最后更新时间: 2024-11-04 23:30
|
||||
## 2025-11-06 部署记录
|
||||
|
||||
### PostgreSQL 数据持久化与财务数据同步
|
||||
|
||||
- **时间**: 2025-11-06 21:30
|
||||
- **版本**: main@latest
|
||||
- **内容**: 后端切换 PostgreSQL,CI/CD 自动导入 657 条 2025 年账目
|
||||
|
||||
#### 核心变更
|
||||
|
||||
1. `docker-compose.yml` 新增 `postgres` 服务并启用 `postgres-data` 卷持久化
|
||||
2. `apps/backend/scripts/import-finance-data.js` 重写为 PostgreSQL 版本,支持新旧两种 CSV 结构
|
||||
3. Gitea Workflow 部署脚本自动执行 `pnpm --filter @vben/backend import:data -- --csv /app/data/finance/finance-combined.csv --year 2025`
|
||||
|
||||
#### 数据校验
|
||||
|
||||
- `sudo docker-compose exec -T postgres psql -U kt_financial -d kt_financial -c "SELECT COUNT(*) FROM finance_transactions;"` → **657**
|
||||
- 前端 `/finance/transactions` 页面显示最新日期为 **2025-11-05**,历史数据保持完整
|
||||
|
||||
---
|
||||
|
||||
最后更新时间: 2025-11-06 21:30
|
||||
|
||||
@@ -12,6 +12,7 @@ COPY apps ./apps
|
||||
COPY packages ./packages
|
||||
COPY internal ./internal
|
||||
COPY scripts ./scripts
|
||||
COPY data ./data
|
||||
|
||||
# 安装依赖(如果存在lock文件则使用)
|
||||
RUN pnpm install --no-frozen-lockfile
|
||||
@@ -48,6 +49,7 @@ RUN mkdir -p /app/apps
|
||||
COPY --from=backend-builder /app/apps/backend /app/apps/backend
|
||||
RUN ln -s /app/apps/backend /app/backend
|
||||
COPY --from=backend-builder /app/node_modules /app/node_modules
|
||||
COPY --from=backend-builder /app/data /app/data
|
||||
|
||||
# 创建nginx配置和日志目录
|
||||
RUN mkdir -p /run/nginx && \
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const currency = query.currency as string | undefined;
|
||||
|
||||
let accounts = listAccounts();
|
||||
let accounts = await listAccounts();
|
||||
|
||||
if (currency) {
|
||||
accounts = accounts.filter((account) => account.currency === currency);
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const type = query.type as 'expense' | 'income' | undefined;
|
||||
|
||||
const categories = fetchCategories({ type });
|
||||
const categories = await fetchCategories({ type });
|
||||
|
||||
return useResponseSuccess(categories);
|
||||
});
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { readBody } from 'h3';
|
||||
import {
|
||||
createTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { createTransaction } from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
|
||||
const DEFAULT_CURRENCY = 'CNY';
|
||||
const DEFAULT_STATUS: TransactionStatus = 'pending';
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
@@ -33,11 +32,11 @@ export default defineEventHandler(async (event) => {
|
||||
const status =
|
||||
(body.status as TransactionStatus | undefined) ?? DEFAULT_STATUS;
|
||||
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
|
||||
const reimbursement = createTransaction({
|
||||
const reimbursement = await createTransaction({
|
||||
type,
|
||||
amount,
|
||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { getRouterParam, readBody } from 'h3';
|
||||
import {
|
||||
restoreTransaction,
|
||||
updateTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
@@ -23,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body?.isDeleted === false) {
|
||||
const restored = restoreTransaction(id);
|
||||
const restored = await restoreTransaction(id);
|
||||
if (!restored) {
|
||||
return useResponseError('报销单不存在', -1);
|
||||
}
|
||||
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||
if (body?.status !== undefined) {
|
||||
const status = body.status as TransactionStatus;
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
payload.status = status;
|
||||
@@ -76,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
||||
payload.approvedAt = body.approvedAt ?? null;
|
||||
}
|
||||
|
||||
const updated = updateTransaction(id, payload);
|
||||
const updated = await updateTransaction(id, payload);
|
||||
if (!updated) {
|
||||
return useResponseError('报销单不存在', -1);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0) as TransactionStatus[])
|
||||
: (['approved', 'paid'] satisfies TransactionStatus[]);
|
||||
const transactions = fetchTransactions({
|
||||
const transactions = await fetchTransactions({
|
||||
type,
|
||||
includeDeleted,
|
||||
statuses,
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { readBody } from 'h3';
|
||||
import {
|
||||
createTransaction,
|
||||
type TransactionStatus,
|
||||
getAccountById,
|
||||
getCategoryById,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
import { notifyTransaction } from '~/utils/telegram-bot';
|
||||
import db from '~/utils/sqlite';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
|
||||
const DEFAULT_CURRENCY = 'CNY';
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
@@ -29,13 +31,12 @@ export default defineEventHandler(async (event) => {
|
||||
return useResponseError('金额格式不正确', -1);
|
||||
}
|
||||
|
||||
const status =
|
||||
(body.status as TransactionStatus | undefined) ?? 'approved';
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
const status = (body.status as TransactionStatus | undefined) ?? 'approved';
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
|
||||
const transaction = createTransaction({
|
||||
const transaction = await createTransaction({
|
||||
type: body.type,
|
||||
amount,
|
||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||
@@ -61,23 +62,12 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// 发送Telegram通知(新功能)
|
||||
try {
|
||||
// 获取分类和账户名称
|
||||
let categoryName: string | undefined;
|
||||
let accountName: string | undefined;
|
||||
|
||||
if (transaction.categoryId) {
|
||||
const category = db
|
||||
.prepare<{ name: string }>('SELECT name FROM finance_categories WHERE id = ?')
|
||||
.get(transaction.categoryId);
|
||||
categoryName = category?.name;
|
||||
}
|
||||
|
||||
if (transaction.accountId) {
|
||||
const account = db
|
||||
.prepare<{ name: string }>('SELECT name FROM finance_accounts WHERE id = ?')
|
||||
.get(transaction.accountId);
|
||||
accountName = account?.name;
|
||||
}
|
||||
const category = transaction.categoryId
|
||||
? await getCategoryById(transaction.categoryId)
|
||||
: null;
|
||||
const account = transaction.accountId
|
||||
? await getAccountById(transaction.accountId)
|
||||
: null;
|
||||
|
||||
await notifyTransaction(
|
||||
{
|
||||
@@ -85,8 +75,8 @@ export default defineEventHandler(async (event) => {
|
||||
type: transaction.type,
|
||||
amount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
categoryName,
|
||||
accountName,
|
||||
categoryName: category?.name,
|
||||
accountName: account?.name,
|
||||
transactionDate: transaction.transactionDate,
|
||||
description: transaction.description || undefined,
|
||||
status: transaction.status,
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineEventHandler(async (event) => {
|
||||
return useResponseError('参数错误', -1);
|
||||
}
|
||||
|
||||
const updated = softDeleteTransaction(id);
|
||||
const updated = await softDeleteTransaction(id);
|
||||
if (!updated) {
|
||||
return useResponseError('交易不存在', -1);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { getRouterParam, readBody } from 'h3';
|
||||
import {
|
||||
restoreTransaction,
|
||||
updateTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
@@ -23,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body?.isDeleted === false) {
|
||||
const restored = restoreTransaction(id);
|
||||
const restored = await restoreTransaction(id);
|
||||
if (!restored) {
|
||||
return useResponseError('交易不存在', -1);
|
||||
}
|
||||
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||
if (body?.status !== undefined) {
|
||||
const status = body.status as TransactionStatus;
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
payload.status = status;
|
||||
@@ -76,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
||||
payload.approvedAt = body.approvedAt ?? null;
|
||||
}
|
||||
|
||||
const updated = updateTransaction(id, payload);
|
||||
const updated = await updateTransaction(id, payload);
|
||||
if (!updated) {
|
||||
return useResponseError('交易不存在', -1);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
const configs = db
|
||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>(
|
||||
`
|
||||
SELECT id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at
|
||||
FROM telegram_notification_configs
|
||||
ORDER BY created_at DESC
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
export default defineEventHandler(async () => {
|
||||
const { rows } = await query<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>(
|
||||
`SELECT id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at
|
||||
FROM telegram_notification_configs
|
||||
ORDER BY created_at DESC`,
|
||||
);
|
||||
|
||||
const result = configs.map((row) => ({
|
||||
const result = rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
botToken: row.bot_token,
|
||||
chatId: row.chat_id,
|
||||
notificationTypes: JSON.parse(row.notification_types) as string[],
|
||||
isEnabled: row.is_enabled === 1,
|
||||
isEnabled: row.is_enabled,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readBody } from 'h3';
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { testTelegramConfig } from '~/utils/telegram-bot';
|
||||
|
||||
@@ -25,31 +25,48 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const result = db
|
||||
.prepare<unknown, [string, string, string, string, number, string, string]>(
|
||||
`
|
||||
INSERT INTO telegram_notification_configs (name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
)
|
||||
.run(
|
||||
const { rows } = await query<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>(
|
||||
`INSERT INTO telegram_notification_configs (
|
||||
name,
|
||||
bot_token,
|
||||
chat_id,
|
||||
notification_types,
|
||||
is_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at`,
|
||||
[
|
||||
body.name,
|
||||
body.botToken,
|
||||
body.chatId,
|
||||
JSON.stringify(notificationTypes),
|
||||
body.isEnabled !== false ? 1 : 0,
|
||||
body.isEnabled !== false,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
],
|
||||
);
|
||||
|
||||
const row = rows[0];
|
||||
|
||||
return useResponseSuccess({
|
||||
id: result.lastInsertRowid,
|
||||
name: body.name,
|
||||
botToken: body.botToken,
|
||||
chatId: body.chatId,
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
botToken: row.bot_token,
|
||||
chatId: row.chat_id,
|
||||
notificationTypes,
|
||||
isEnabled: body.isEnabled !== false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isEnabled: row.is_enabled,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const id = event.context.params?.id;
|
||||
if (!id) {
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = event.context.params?.id;
|
||||
const id = Number(idParam);
|
||||
if (!idParam || Number.isNaN(id)) {
|
||||
return useResponseError('缺少ID参数', -1);
|
||||
}
|
||||
|
||||
const result = db
|
||||
.prepare('DELETE FROM telegram_notification_configs WHERE id = ?')
|
||||
.run(id);
|
||||
const result = await query(
|
||||
'DELETE FROM telegram_notification_configs WHERE id = $1',
|
||||
[id],
|
||||
);
|
||||
|
||||
if (result.changes === 0) {
|
||||
if (result.rowCount === 0) {
|
||||
return useResponseError('配置不存在或删除失败', -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { readBody } from 'h3';
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { testTelegramConfig } from '~/utils/telegram-bot';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = event.context.params?.id;
|
||||
if (!id) {
|
||||
const idParam = event.context.params?.id;
|
||||
const id = Number(idParam);
|
||||
if (!idParam || Number.isNaN(id)) {
|
||||
return useResponseError('缺少ID参数', -1);
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
// 如果更新了botToken或chatId,需要测试配置
|
||||
if (body.botToken || body.chatId) {
|
||||
const existing = db
|
||||
.prepare<{ bot_token: string; chat_id: string }>('SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = ?')
|
||||
.get(id);
|
||||
if (body.botToken !== undefined || body.chatId !== undefined) {
|
||||
const { rows } = await query<{
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
}>(
|
||||
'SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = $1',
|
||||
[id],
|
||||
);
|
||||
const existing = rows[0];
|
||||
|
||||
if (!existing) {
|
||||
return useResponseError('配置不存在', -1);
|
||||
}
|
||||
|
||||
const tokenToTest = body.botToken || existing.bot_token;
|
||||
const chatIdToTest = body.chatId || existing.chat_id;
|
||||
const tokenToTest = body.botToken ?? existing.bot_token;
|
||||
const chatIdToTest = body.chatId ?? existing.chat_id;
|
||||
|
||||
const testResult = await testTelegramConfig(tokenToTest, chatIdToTest);
|
||||
if (!testResult.success) {
|
||||
@@ -34,51 +40,65 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const values: (string | number)[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (body.name !== undefined) {
|
||||
updates.push('name = ?');
|
||||
values.push(body.name);
|
||||
updates.push(`name = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.botToken !== undefined) {
|
||||
updates.push('bot_token = ?');
|
||||
values.push(body.botToken);
|
||||
updates.push(`bot_token = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.chatId !== undefined) {
|
||||
updates.push('chat_id = ?');
|
||||
values.push(body.chatId);
|
||||
updates.push(`chat_id = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.notificationTypes !== undefined) {
|
||||
updates.push('notification_types = ?');
|
||||
values.push(JSON.stringify(body.notificationTypes));
|
||||
updates.push(`notification_types = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.isEnabled !== undefined) {
|
||||
updates.push('is_enabled = ?');
|
||||
values.push(body.isEnabled ? 1 : 0);
|
||||
values.push(body.isEnabled !== false);
|
||||
updates.push(`is_enabled = $${values.length}`);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return useResponseError('没有可更新的字段', -1);
|
||||
}
|
||||
|
||||
updates.push('updated_at = ?');
|
||||
values.push(new Date().toISOString());
|
||||
updates.push(`updated_at = $${values.length}`);
|
||||
values.push(id);
|
||||
const idPosition = values.length;
|
||||
|
||||
db.prepare(`UPDATE telegram_notification_configs SET ${updates.join(', ')} WHERE id = ?`).run(
|
||||
...values,
|
||||
const updateResult = await query(
|
||||
`UPDATE telegram_notification_configs
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = $${idPosition}`,
|
||||
values,
|
||||
);
|
||||
|
||||
const updated = db
|
||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>(
|
||||
'SELECT * FROM telegram_notification_configs WHERE id = ?',
|
||||
)
|
||||
.get(id);
|
||||
if (updateResult.rowCount === 0) {
|
||||
return useResponseError('配置不存在', -1);
|
||||
}
|
||||
|
||||
const { rows: updatedRows } = await query<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>('SELECT * FROM telegram_notification_configs WHERE id = $1', [id]);
|
||||
|
||||
const updated = updatedRows[0];
|
||||
if (!updated) {
|
||||
return useResponseError('更新失败', -1);
|
||||
}
|
||||
@@ -89,7 +109,7 @@ export default defineEventHandler(async (event) => {
|
||||
botToken: updated.bot_token,
|
||||
chatId: updated.chat_id,
|
||||
notificationTypes: JSON.parse(updated.notification_types) as string[],
|
||||
isEnabled: updated.is_enabled === 1,
|
||||
isEnabled: updated.is_enabled,
|
||||
createdAt: updated.created_at,
|
||||
updatedAt: updated.updated_at,
|
||||
});
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "catalog:",
|
||||
"better-sqlite3": "9.5.0",
|
||||
"jsonwebtoken": "catalog:",
|
||||
"nitropack": "catalog:"
|
||||
"nitropack": "catalog:",
|
||||
"pg": "^8.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "catalog:",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
314
apps/backend/utils/db.ts
Normal file
314
apps/backend/utils/db.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import process from 'node:process';
|
||||
import type { PoolClient } from 'pg';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
import {
|
||||
MOCK_ACCOUNTS,
|
||||
MOCK_CATEGORIES,
|
||||
MOCK_CURRENCIES,
|
||||
MOCK_EXCHANGE_RATES,
|
||||
} from './mock-data';
|
||||
|
||||
const DEFAULT_HOST = process.env.POSTGRES_HOST ?? 'postgres';
|
||||
const DEFAULT_PORT = Number.parseInt(process.env.POSTGRES_PORT ?? '5432', 10);
|
||||
const DEFAULT_DB = process.env.POSTGRES_DB ?? 'kt_financial';
|
||||
const DEFAULT_USER = process.env.POSTGRES_USER ?? 'kt_financial';
|
||||
const DEFAULT_PASSWORD = process.env.POSTGRES_PASSWORD ?? 'kt_financial_pwd';
|
||||
|
||||
const connectionString =
|
||||
process.env.POSTGRES_URL ??
|
||||
`postgresql://${DEFAULT_USER}:${DEFAULT_PASSWORD}@${DEFAULT_HOST}:${DEFAULT_PORT}/${DEFAULT_DB}`;
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
max: 10,
|
||||
});
|
||||
|
||||
let initPromise: null | Promise<void> = null;
|
||||
|
||||
async function seedCurrencies(client: PoolClient) {
|
||||
await Promise.all(
|
||||
MOCK_CURRENCIES.map((currency) =>
|
||||
client.query(
|
||||
`INSERT INTO finance_currencies (code, name, symbol, is_base, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (code) DO NOTHING`,
|
||||
[
|
||||
currency.code,
|
||||
currency.name,
|
||||
currency.symbol,
|
||||
currency.isBase,
|
||||
currency.isActive,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function seedExchangeRates(client: PoolClient) {
|
||||
await Promise.all(
|
||||
MOCK_EXCHANGE_RATES.map((rate) =>
|
||||
client.query(
|
||||
`INSERT INTO finance_exchange_rates (from_currency, to_currency, rate, date, source)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[
|
||||
rate.fromCurrency,
|
||||
rate.toCurrency,
|
||||
rate.rate,
|
||||
rate.date,
|
||||
rate.source ?? 'manual',
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function seedAccounts(client: PoolClient) {
|
||||
await Promise.all(
|
||||
MOCK_ACCOUNTS.map((account) =>
|
||||
client.query(
|
||||
`INSERT INTO finance_accounts (id, name, currency, type, icon, color, user_id, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[
|
||||
account.id,
|
||||
account.name,
|
||||
account.currency,
|
||||
account.type,
|
||||
account.icon,
|
||||
account.color,
|
||||
account.userId ?? 1,
|
||||
account.isActive,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function seedCategories(client: PoolClient) {
|
||||
await Promise.all(
|
||||
MOCK_CATEGORIES.map((category) =>
|
||||
client.query(
|
||||
`INSERT INTO finance_categories (id, name, type, icon, color, user_id, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[
|
||||
category.id,
|
||||
category.name,
|
||||
category.type,
|
||||
category.icon,
|
||||
category.color,
|
||||
category.userId,
|
||||
category.isActive,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function initializeSchema() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS finance_currencies (
|
||||
code TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
is_base BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS finance_exchange_rates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
from_currency TEXT NOT NULL REFERENCES finance_currencies(code),
|
||||
to_currency TEXT NOT NULL REFERENCES finance_currencies(code),
|
||||
rate NUMERIC NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
source TEXT DEFAULT 'manual'
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS finance_accounts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
currency TEXT NOT NULL REFERENCES finance_currencies(code),
|
||||
type TEXT DEFAULT 'cash',
|
||||
icon TEXT,
|
||||
color TEXT,
|
||||
user_id INTEGER DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS finance_categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
icon TEXT,
|
||||
color TEXT,
|
||||
user_id INTEGER,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS finance_transactions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
amount NUMERIC NOT NULL,
|
||||
currency TEXT NOT NULL REFERENCES finance_currencies(code),
|
||||
exchange_rate_to_base NUMERIC NOT NULL,
|
||||
amount_in_base NUMERIC NOT NULL,
|
||||
category_id INTEGER REFERENCES finance_categories(id),
|
||||
account_id INTEGER REFERENCES finance_accounts(id),
|
||||
transaction_date DATE NOT NULL,
|
||||
description TEXT,
|
||||
project TEXT,
|
||||
memo TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
status TEXT NOT NULL DEFAULT 'approved',
|
||||
status_updated_at TIMESTAMP WITH TIME ZONE,
|
||||
reimbursement_batch TEXT,
|
||||
review_notes TEXT,
|
||||
submitted_by TEXT,
|
||||
approved_by TEXT,
|
||||
approved_at TIMESTAMP WITH TIME ZONE,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS finance_media_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chat_id BIGINT NOT NULL,
|
||||
message_id BIGINT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
username TEXT,
|
||||
display_name TEXT,
|
||||
file_type TEXT NOT NULL,
|
||||
file_id TEXT NOT NULL,
|
||||
file_unique_id TEXT,
|
||||
caption TEXT,
|
||||
file_name TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
duration INTEGER,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
forwarded_to INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(chat_id, message_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS telegram_notification_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
bot_token TEXT NOT NULL,
|
||||
chat_id TEXT NOT NULL,
|
||||
notification_types TEXT NOT NULL,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
priority TEXT DEFAULT 'normal',
|
||||
rate_limit_seconds INTEGER DEFAULT 0,
|
||||
batch_enabled BOOLEAN DEFAULT FALSE,
|
||||
batch_interval_minutes INTEGER DEFAULT 60,
|
||||
retry_enabled BOOLEAN DEFAULT TRUE,
|
||||
retry_max_attempts INTEGER DEFAULT 3,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS telegram_notification_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
config_id INTEGER NOT NULL REFERENCES telegram_notification_configs(id),
|
||||
notification_type TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
sent_at TIMESTAMP WITH TIME ZONE,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_finance_media_messages_created_at
|
||||
ON finance_media_messages (created_at DESC);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_finance_media_messages_user_id
|
||||
ON finance_media_messages (user_id);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_configs_enabled
|
||||
ON telegram_notification_configs (is_enabled);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_config
|
||||
ON telegram_notification_history (config_id, created_at DESC);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_hash
|
||||
ON telegram_notification_history (content_hash, created_at DESC);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_status
|
||||
ON telegram_notification_history (status, retry_count);
|
||||
`);
|
||||
|
||||
await seedCurrencies(client);
|
||||
await seedExchangeRates(client);
|
||||
await seedAccounts(client);
|
||||
await seedCategories(client);
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPool() {
|
||||
if (!initPromise) {
|
||||
initPromise = initializeSchema();
|
||||
}
|
||||
await initPromise;
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function query<T = any>(text: string, params?: any[]) {
|
||||
const client = await getPool();
|
||||
const result = await client.query<T>(text, params);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function withTransaction<T>(
|
||||
handler: (client: PoolClient) => Promise<T>,
|
||||
) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const result = await handler(client);
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { query } from './db';
|
||||
import {
|
||||
MOCK_ACCOUNTS,
|
||||
MOCK_BUDGETS,
|
||||
@@ -5,37 +6,87 @@ import {
|
||||
MOCK_CURRENCIES,
|
||||
MOCK_EXCHANGE_RATES,
|
||||
} from './mock-data';
|
||||
import db from './sqlite';
|
||||
|
||||
export function listAccounts() {
|
||||
return MOCK_ACCOUNTS;
|
||||
interface AccountRow {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
currency: string;
|
||||
icon: null | string;
|
||||
color: null | string;
|
||||
user_id: null | number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export function listCategories() {
|
||||
// 从数据库读取分类
|
||||
interface CategoryRow {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
icon: null | string;
|
||||
color: null | string;
|
||||
user_id: null | number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
function mapAccount(row: AccountRow) {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id ?? 1,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
currency: row.currency,
|
||||
balance: 0,
|
||||
icon: row.icon ?? '💳',
|
||||
color: row.color ?? '#1677ff',
|
||||
isActive: Boolean(row.is_active),
|
||||
};
|
||||
}
|
||||
|
||||
function mapCategory(row: CategoryRow) {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id ?? 1,
|
||||
name: row.name,
|
||||
type: row.type as 'expense' | 'income',
|
||||
icon: row.icon ?? '📝',
|
||||
color: row.color ?? '#dfe4ea',
|
||||
sortOrder: row.id,
|
||||
isSystem: row.user_id === null,
|
||||
isActive: Boolean(row.is_active),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAccounts() {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT id, name, type, icon, color, user_id as userId, is_active as isActive
|
||||
FROM finance_categories
|
||||
WHERE is_active = 1
|
||||
ORDER BY type, id
|
||||
`);
|
||||
const categories = stmt.all() as any[];
|
||||
|
||||
// 转换为前端需要的格式
|
||||
return categories.map(cat => ({
|
||||
id: cat.id,
|
||||
userId: cat.userId,
|
||||
name: cat.name,
|
||||
type: cat.type,
|
||||
icon: cat.icon,
|
||||
color: cat.color,
|
||||
sortOrder: cat.id,
|
||||
isSystem: true,
|
||||
isActive: Boolean(cat.isActive),
|
||||
}));
|
||||
const { rows } = await query<AccountRow>(
|
||||
`SELECT id, name, type, currency, icon, color, user_id, is_active
|
||||
FROM finance_accounts
|
||||
ORDER BY id`,
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
return MOCK_ACCOUNTS;
|
||||
}
|
||||
return rows.map((row) => mapAccount(row));
|
||||
} catch (error) {
|
||||
console.error('从数据库读取分类失败,使用MOCK数据:', error);
|
||||
console.error('从数据库读取账户失败,使用 MOCK 数据:', error);
|
||||
return MOCK_ACCOUNTS;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listCategories() {
|
||||
try {
|
||||
const { rows } = await query<CategoryRow>(
|
||||
`SELECT id, name, type, icon, color, user_id, is_active
|
||||
FROM finance_categories
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY type, id`,
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
return MOCK_CATEGORIES;
|
||||
}
|
||||
return rows.map((row) => mapCategory(row));
|
||||
} catch (error) {
|
||||
console.error('从数据库读取分类失败,使用 MOCK 数据:', error);
|
||||
return MOCK_CATEGORIES;
|
||||
}
|
||||
}
|
||||
@@ -52,76 +103,80 @@ export function listExchangeRates() {
|
||||
return MOCK_EXCHANGE_RATES;
|
||||
}
|
||||
|
||||
export function createCategoryRecord(category: any) {
|
||||
export async function createCategoryRecord(category: any) {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO finance_categories (name, type, icon, color, user_id, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
`);
|
||||
const result = stmt.run(
|
||||
category.name,
|
||||
category.type,
|
||||
category.icon || '📝',
|
||||
category.color || '#dfe4ea',
|
||||
category.userId || 1
|
||||
const { rows } = await query<CategoryRow>(
|
||||
`INSERT INTO finance_categories (name, type, icon, color, user_id, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, TRUE)
|
||||
RETURNING id, name, type, icon, color, user_id, is_active`,
|
||||
[
|
||||
category.name,
|
||||
category.type,
|
||||
category.icon || '📝',
|
||||
category.color || '#dfe4ea',
|
||||
category.userId || 1,
|
||||
],
|
||||
);
|
||||
return {
|
||||
id: result.lastInsertRowid,
|
||||
...category,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const row = rows[0];
|
||||
return row
|
||||
? {
|
||||
...mapCategory(row),
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
: null;
|
||||
} catch (error) {
|
||||
console.error('创建分类失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateCategoryRecord(id: number, category: any) {
|
||||
export async function updateCategoryRecord(id: number, category: any) {
|
||||
try {
|
||||
const updates: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
|
||||
if (category.name) {
|
||||
updates.push('name = ?');
|
||||
params.push(category.name);
|
||||
updates.push(`name = $${params.length}`);
|
||||
}
|
||||
if (category.icon) {
|
||||
updates.push('icon = ?');
|
||||
params.push(category.icon);
|
||||
updates.push(`icon = $${params.length}`);
|
||||
}
|
||||
if (category.color) {
|
||||
updates.push('color = ?');
|
||||
params.push(category.color);
|
||||
updates.push(`color = $${params.length}`);
|
||||
}
|
||||
|
||||
if (updates.length === 0) return null;
|
||||
|
||||
|
||||
if (updates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
const stmt = db.prepare(`
|
||||
UPDATE finance_categories
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = ?
|
||||
`);
|
||||
stmt.run(...params);
|
||||
|
||||
// 返回更新后的分类
|
||||
const selectStmt = db.prepare('SELECT * FROM finance_categories WHERE id = ?');
|
||||
return selectStmt.get(id);
|
||||
const setClause = updates.join(', ');
|
||||
const { rows } = await query<CategoryRow>(
|
||||
`UPDATE finance_categories
|
||||
SET ${setClause}
|
||||
WHERE id = $${params.length}
|
||||
RETURNING id, name, type, icon, color, user_id, is_active`,
|
||||
params,
|
||||
);
|
||||
const row = rows[0];
|
||||
return row ? mapCategory(row) : null;
|
||||
} catch (error) {
|
||||
console.error('更新分类失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteCategoryRecord(id: number) {
|
||||
export async function deleteCategoryRecord(id: number) {
|
||||
try {
|
||||
// 软删除
|
||||
const stmt = db.prepare(`
|
||||
UPDATE finance_categories
|
||||
SET is_active = 0
|
||||
WHERE id = ?
|
||||
`);
|
||||
stmt.run(id);
|
||||
await query(
|
||||
`UPDATE finance_categories
|
||||
SET is_active = FALSE
|
||||
WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import db from './sqlite';
|
||||
import type { PoolClient } from 'pg';
|
||||
|
||||
import { query, withTransaction } from './db';
|
||||
|
||||
const BASE_CURRENCY = 'CNY';
|
||||
|
||||
interface TransactionRow {
|
||||
id: number;
|
||||
type: string;
|
||||
amount: number;
|
||||
amount: number | string;
|
||||
currency: string;
|
||||
exchange_rate_to_base: number;
|
||||
amount_in_base: number;
|
||||
exchange_rate_to_base: number | string;
|
||||
amount_in_base: number | string;
|
||||
category_id: null | number;
|
||||
account_id: null | number;
|
||||
transaction_date: string;
|
||||
@@ -23,7 +25,7 @@ interface TransactionRow {
|
||||
submitted_by: null | string;
|
||||
approved_by: null | string;
|
||||
approved_at: null | string;
|
||||
is_deleted: number;
|
||||
is_deleted: boolean;
|
||||
deleted_at: null | string;
|
||||
}
|
||||
|
||||
@@ -49,32 +51,24 @@ interface TransactionPayload {
|
||||
}
|
||||
|
||||
export type TransactionStatus =
|
||||
| 'draft'
|
||||
| 'pending'
|
||||
| 'approved'
|
||||
| 'rejected'
|
||||
| 'paid';
|
||||
|
||||
function getExchangeRateToBase(currency: string) {
|
||||
if (currency === BASE_CURRENCY) {
|
||||
return 1;
|
||||
}
|
||||
const stmt = db.prepare(
|
||||
`SELECT rate FROM finance_exchange_rates WHERE from_currency = ? AND to_currency = ? ORDER BY date DESC LIMIT 1`,
|
||||
);
|
||||
const row = stmt.get(currency, BASE_CURRENCY) as undefined | { rate: number };
|
||||
return row?.rate ?? 1;
|
||||
}
|
||||
| 'draft'
|
||||
| 'paid'
|
||||
| 'pending'
|
||||
| 'rejected';
|
||||
|
||||
function mapTransaction(row: TransactionRow) {
|
||||
const amount = Number(row.amount);
|
||||
const exchangeRateToBase = Number(row.exchange_rate_to_base);
|
||||
const amountInBase = Number(row.amount_in_base);
|
||||
return {
|
||||
id: row.id,
|
||||
userId: 1,
|
||||
type: 'expense' as const,
|
||||
amount: Math.abs(row.amount),
|
||||
type: row.type as 'expense' | 'income' | 'transfer',
|
||||
amount: Math.abs(amount),
|
||||
currency: row.currency,
|
||||
exchangeRateToBase: row.exchange_rate_to_base,
|
||||
amountInBase: Math.abs(row.amount_in_base),
|
||||
exchangeRateToBase,
|
||||
amountInBase: Math.abs(amountInBase),
|
||||
categoryId: row.category_id ?? undefined,
|
||||
accountId: row.account_id ?? undefined,
|
||||
transactionDate: row.transaction_date,
|
||||
@@ -94,231 +88,350 @@ function mapTransaction(row: TransactionRow) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchTransactions(
|
||||
async function getExchangeRateToBase(client: PoolClient, currency: string) {
|
||||
if (currency === BASE_CURRENCY) {
|
||||
return 1;
|
||||
}
|
||||
const result = await client.query<{ rate: number | string }>(
|
||||
`SELECT rate
|
||||
FROM finance_exchange_rates
|
||||
WHERE from_currency = $1 AND to_currency = $2
|
||||
ORDER BY date DESC
|
||||
LIMIT 1`,
|
||||
[currency, BASE_CURRENCY],
|
||||
);
|
||||
const raw = result.rows[0]?.rate;
|
||||
return raw ? Number(raw) : 1;
|
||||
}
|
||||
|
||||
export async function fetchTransactions(
|
||||
options: {
|
||||
includeDeleted?: boolean;
|
||||
type?: string;
|
||||
statuses?: TransactionStatus[];
|
||||
type?: string;
|
||||
} = {},
|
||||
) {
|
||||
const clauses: string[] = [];
|
||||
const params: Record<string, unknown> = {};
|
||||
const params: any[] = [];
|
||||
|
||||
if (!options.includeDeleted) {
|
||||
clauses.push('is_deleted = 0');
|
||||
clauses.push('is_deleted = FALSE');
|
||||
}
|
||||
if (options.type) {
|
||||
clauses.push('type = @type');
|
||||
params.type = options.type;
|
||||
params.push(options.type);
|
||||
clauses.push(`type = $${params.length}`);
|
||||
}
|
||||
if (options.statuses && options.statuses.length > 0) {
|
||||
clauses.push(
|
||||
`status IN (${options.statuses.map((_, index) => `@status${index}`).join(', ')})`,
|
||||
);
|
||||
options.statuses.forEach((status, index) => {
|
||||
params[`status${index}`] = status;
|
||||
const statusPlaceholders = options.statuses.map((status) => {
|
||||
params.push(status);
|
||||
return `$${params.length}`;
|
||||
});
|
||||
clauses.push(`status IN (${statusPlaceholders.join(', ')})`);
|
||||
}
|
||||
|
||||
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||
|
||||
const stmt = db.prepare<TransactionRow>(
|
||||
`SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted, deleted_at FROM finance_transactions ${where} ORDER BY transaction_date DESC, id DESC`,
|
||||
const { rows } = await query<TransactionRow>(
|
||||
`SELECT id,
|
||||
type,
|
||||
amount,
|
||||
currency,
|
||||
exchange_rate_to_base,
|
||||
amount_in_base,
|
||||
category_id,
|
||||
account_id,
|
||||
transaction_date,
|
||||
description,
|
||||
project,
|
||||
memo,
|
||||
created_at,
|
||||
status,
|
||||
status_updated_at,
|
||||
reimbursement_batch,
|
||||
review_notes,
|
||||
submitted_by,
|
||||
approved_by,
|
||||
approved_at,
|
||||
is_deleted,
|
||||
deleted_at
|
||||
FROM finance_transactions
|
||||
${where}
|
||||
ORDER BY transaction_date DESC, id DESC`,
|
||||
params,
|
||||
);
|
||||
|
||||
return stmt.all(params).map(mapTransaction);
|
||||
return rows.map((row) => mapTransaction(row));
|
||||
}
|
||||
|
||||
export function getTransactionById(id: number) {
|
||||
const stmt = db.prepare<TransactionRow>(
|
||||
`SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted, deleted_at FROM finance_transactions WHERE id = ?`,
|
||||
export async function getTransactionById(id: number) {
|
||||
const { rows } = await query<TransactionRow>(
|
||||
`SELECT id,
|
||||
type,
|
||||
amount,
|
||||
currency,
|
||||
exchange_rate_to_base,
|
||||
amount_in_base,
|
||||
category_id,
|
||||
account_id,
|
||||
transaction_date,
|
||||
description,
|
||||
project,
|
||||
memo,
|
||||
created_at,
|
||||
status,
|
||||
status_updated_at,
|
||||
reimbursement_batch,
|
||||
review_notes,
|
||||
submitted_by,
|
||||
approved_by,
|
||||
approved_at,
|
||||
is_deleted,
|
||||
deleted_at
|
||||
FROM finance_transactions
|
||||
WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
const row = stmt.get(id);
|
||||
const row = rows[0];
|
||||
return row ? mapTransaction(row) : null;
|
||||
}
|
||||
|
||||
export function createTransaction(payload: TransactionPayload) {
|
||||
const exchangeRate = getExchangeRateToBase(payload.currency);
|
||||
const amountInBase = +(payload.amount * exchangeRate).toFixed(2);
|
||||
const createdAt =
|
||||
payload.createdAt && payload.createdAt.length > 0
|
||||
? payload.createdAt
|
||||
: new Date().toISOString();
|
||||
const status: TransactionStatus = payload.status ?? 'approved';
|
||||
const statusUpdatedAt =
|
||||
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
|
||||
? payload.statusUpdatedAt
|
||||
: createdAt;
|
||||
const approvedAt =
|
||||
payload.approvedAt && payload.approvedAt.length > 0
|
||||
? payload.approvedAt
|
||||
: status === 'approved' || status === 'paid'
|
||||
? statusUpdatedAt
|
||||
: null;
|
||||
export async function createTransaction(payload: TransactionPayload) {
|
||||
return withTransaction(async (client) => {
|
||||
const exchangeRate = await getExchangeRateToBase(client, payload.currency);
|
||||
const amountInBase = +(payload.amount * exchangeRate).toFixed(2);
|
||||
const createdAt =
|
||||
payload.createdAt && payload.createdAt.length > 0
|
||||
? payload.createdAt
|
||||
: new Date().toISOString();
|
||||
const status: TransactionStatus = payload.status ?? 'approved';
|
||||
const statusUpdatedAt =
|
||||
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
|
||||
? payload.statusUpdatedAt
|
||||
: createdAt;
|
||||
let approvedAt: string | null = null;
|
||||
if (payload.approvedAt && payload.approvedAt.length > 0) {
|
||||
approvedAt = payload.approvedAt;
|
||||
} else if (status === 'approved' || status === 'paid') {
|
||||
approvedAt = statusUpdatedAt;
|
||||
}
|
||||
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, @status, @statusUpdatedAt, @reimbursementBatch, @reviewNotes, @submittedBy, @approvedBy, @approvedAt, 0)`,
|
||||
);
|
||||
|
||||
const info = stmt.run({
|
||||
type: payload.type,
|
||||
amount: payload.amount,
|
||||
currency: payload.currency,
|
||||
exchangeRateToBase: exchangeRate,
|
||||
amountInBase,
|
||||
categoryId: payload.categoryId ?? null,
|
||||
accountId: payload.accountId ?? null,
|
||||
transactionDate: payload.transactionDate,
|
||||
description: payload.description ?? '',
|
||||
project: payload.project ?? null,
|
||||
memo: payload.memo ?? null,
|
||||
createdAt,
|
||||
status,
|
||||
statusUpdatedAt,
|
||||
reimbursementBatch: payload.reimbursementBatch ?? null,
|
||||
reviewNotes: payload.reviewNotes ?? null,
|
||||
submittedBy: payload.submittedBy ?? null,
|
||||
approvedBy: payload.approvedBy ?? null,
|
||||
approvedAt,
|
||||
const { rows } = await client.query<TransactionRow>(
|
||||
`INSERT INTO finance_transactions (
|
||||
type,
|
||||
amount,
|
||||
currency,
|
||||
exchange_rate_to_base,
|
||||
amount_in_base,
|
||||
category_id,
|
||||
account_id,
|
||||
transaction_date,
|
||||
description,
|
||||
project,
|
||||
memo,
|
||||
created_at,
|
||||
status,
|
||||
status_updated_at,
|
||||
reimbursement_batch,
|
||||
review_notes,
|
||||
submitted_by,
|
||||
approved_by,
|
||||
approved_at,
|
||||
is_deleted
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
|
||||
$12, $13, $14, $15, $16, $17, $18, $19, FALSE
|
||||
)
|
||||
RETURNING *`,
|
||||
[
|
||||
payload.type,
|
||||
payload.amount,
|
||||
payload.currency,
|
||||
exchangeRate,
|
||||
amountInBase,
|
||||
payload.categoryId ?? null,
|
||||
payload.accountId ?? null,
|
||||
payload.transactionDate,
|
||||
payload.description ?? '',
|
||||
payload.project ?? null,
|
||||
payload.memo ?? null,
|
||||
createdAt,
|
||||
status,
|
||||
statusUpdatedAt,
|
||||
payload.reimbursementBatch ?? null,
|
||||
payload.reviewNotes ?? null,
|
||||
payload.submittedBy ?? null,
|
||||
payload.approvedBy ?? null,
|
||||
approvedAt,
|
||||
],
|
||||
);
|
||||
return mapTransaction(rows[0]);
|
||||
});
|
||||
|
||||
return getTransactionById(Number(info.lastInsertRowid));
|
||||
}
|
||||
|
||||
export function updateTransaction(id: number, payload: TransactionPayload) {
|
||||
const current = getTransactionById(id);
|
||||
export async function updateTransaction(
|
||||
id: number,
|
||||
payload: TransactionPayload,
|
||||
) {
|
||||
const current = await getTransactionById(id);
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextStatus = (payload.status ?? current.status ?? 'approved') as TransactionStatus;
|
||||
const statusChanged = nextStatus !== current.status;
|
||||
const statusUpdatedAt =
|
||||
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
|
||||
? payload.statusUpdatedAt
|
||||
: statusChanged
|
||||
? new Date().toISOString()
|
||||
: current.statusUpdatedAt ?? current.createdAt;
|
||||
const approvedAt =
|
||||
payload.approvedAt && payload.approvedAt.length > 0
|
||||
? payload.approvedAt
|
||||
: nextStatus === 'approved' || nextStatus === 'paid'
|
||||
? current.approvedAt ?? (statusChanged ? statusUpdatedAt : null)
|
||||
: null;
|
||||
const approvedBy =
|
||||
nextStatus === 'approved' || nextStatus === 'paid'
|
||||
? payload.approvedBy ?? current.approvedBy ?? null
|
||||
: payload.approvedBy ?? null;
|
||||
return withTransaction(async (client) => {
|
||||
const nextStatus = (payload.status ??
|
||||
current.status ??
|
||||
'approved') as TransactionStatus;
|
||||
const statusChanged = nextStatus !== current.status;
|
||||
let statusUpdatedAt: string;
|
||||
if (payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0) {
|
||||
statusUpdatedAt = payload.statusUpdatedAt;
|
||||
} else if (statusChanged) {
|
||||
statusUpdatedAt = new Date().toISOString();
|
||||
} else {
|
||||
statusUpdatedAt = current.statusUpdatedAt ?? current.createdAt;
|
||||
}
|
||||
let approvedAt: string | null = null;
|
||||
if (payload.approvedAt && payload.approvedAt.length > 0) {
|
||||
approvedAt = payload.approvedAt;
|
||||
} else if (nextStatus === 'approved' || nextStatus === 'paid') {
|
||||
approvedAt = current.approvedAt ?? (statusChanged ? statusUpdatedAt : null);
|
||||
}
|
||||
const approvedBy =
|
||||
nextStatus === 'approved' || nextStatus === 'paid'
|
||||
? payload.approvedBy ?? current.approvedBy ?? null
|
||||
: payload.approvedBy ?? null;
|
||||
|
||||
const next = {
|
||||
type: payload.type ?? current.type,
|
||||
amount: payload.amount ?? current.amount,
|
||||
currency: payload.currency ?? current.currency,
|
||||
categoryId: payload.categoryId ?? current.categoryId ?? null,
|
||||
accountId: payload.accountId ?? current.accountId ?? null,
|
||||
transactionDate: payload.transactionDate ?? current.transactionDate,
|
||||
description: payload.description ?? current.description ?? '',
|
||||
project: payload.project ?? current.project ?? null,
|
||||
memo: payload.memo ?? current.memo ?? null,
|
||||
isDeleted: payload.isDeleted ?? current.isDeleted,
|
||||
status: nextStatus,
|
||||
statusUpdatedAt,
|
||||
reimbursementBatch:
|
||||
payload.reimbursementBatch ?? current.reimbursementBatch ?? null,
|
||||
reviewNotes: payload.reviewNotes ?? current.reviewNotes ?? null,
|
||||
submittedBy: payload.submittedBy ?? current.submittedBy ?? null,
|
||||
approvedBy,
|
||||
approvedAt,
|
||||
};
|
||||
const next = {
|
||||
type: payload.type ?? current.type,
|
||||
amount: payload.amount ?? current.amount,
|
||||
currency: payload.currency ?? current.currency,
|
||||
categoryId: payload.categoryId ?? current.categoryId ?? null,
|
||||
accountId: payload.accountId ?? current.accountId ?? null,
|
||||
transactionDate: payload.transactionDate ?? current.transactionDate,
|
||||
description: payload.description ?? current.description ?? '',
|
||||
project: payload.project ?? current.project ?? null,
|
||||
memo: payload.memo ?? current.memo ?? null,
|
||||
isDeleted: payload.isDeleted ?? current.isDeleted,
|
||||
status: nextStatus,
|
||||
statusUpdatedAt,
|
||||
reimbursementBatch:
|
||||
payload.reimbursementBatch ?? current.reimbursementBatch ?? null,
|
||||
reviewNotes: payload.reviewNotes ?? current.reviewNotes ?? null,
|
||||
submittedBy: payload.submittedBy ?? current.submittedBy ?? null,
|
||||
approvedBy,
|
||||
approvedAt,
|
||||
};
|
||||
|
||||
const exchangeRate = getExchangeRateToBase(next.currency);
|
||||
const amountInBase = +(next.amount * exchangeRate).toFixed(2);
|
||||
const exchangeRate = await getExchangeRateToBase(client, next.currency);
|
||||
const amountInBase = +(next.amount * exchangeRate).toFixed(2);
|
||||
const deletedAt = next.isDeleted ? new Date().toISOString() : null;
|
||||
|
||||
const stmt = db.prepare(
|
||||
`UPDATE finance_transactions SET type = @type, amount = @amount, currency = @currency, exchange_rate_to_base = @exchangeRateToBase, amount_in_base = @amountInBase, category_id = @categoryId, account_id = @accountId, transaction_date = @transactionDate, description = @description, project = @project, memo = @memo, status = @status, status_updated_at = @statusUpdatedAt, reimbursement_batch = @reimbursementBatch, review_notes = @reviewNotes, submitted_by = @submittedBy, approved_by = @approvedBy, approved_at = @approvedAt, is_deleted = @isDeleted, deleted_at = @deletedAt WHERE id = @id`,
|
||||
);
|
||||
const { rows } = await client.query<TransactionRow>(
|
||||
`UPDATE finance_transactions
|
||||
SET type = $1,
|
||||
amount = $2,
|
||||
currency = $3,
|
||||
exchange_rate_to_base = $4,
|
||||
amount_in_base = $5,
|
||||
category_id = $6,
|
||||
account_id = $7,
|
||||
transaction_date = $8,
|
||||
description = $9,
|
||||
project = $10,
|
||||
memo = $11,
|
||||
status = $12,
|
||||
status_updated_at = $13,
|
||||
reimbursement_batch = $14,
|
||||
review_notes = $15,
|
||||
submitted_by = $16,
|
||||
approved_by = $17,
|
||||
approved_at = $18,
|
||||
is_deleted = $19,
|
||||
deleted_at = $20
|
||||
WHERE id = $21
|
||||
RETURNING *`,
|
||||
[
|
||||
next.type,
|
||||
next.amount,
|
||||
next.currency,
|
||||
exchangeRate,
|
||||
amountInBase,
|
||||
next.categoryId,
|
||||
next.accountId,
|
||||
next.transactionDate,
|
||||
next.description,
|
||||
next.project,
|
||||
next.memo,
|
||||
next.status,
|
||||
next.statusUpdatedAt,
|
||||
next.reimbursementBatch,
|
||||
next.reviewNotes,
|
||||
next.submittedBy,
|
||||
next.approvedBy,
|
||||
next.approvedAt,
|
||||
next.isDeleted,
|
||||
deletedAt,
|
||||
id,
|
||||
],
|
||||
);
|
||||
|
||||
const deletedAt = next.isDeleted ? new Date().toISOString() : null;
|
||||
|
||||
stmt.run({
|
||||
id,
|
||||
type: next.type,
|
||||
amount: next.amount,
|
||||
currency: next.currency,
|
||||
exchangeRateToBase: exchangeRate,
|
||||
amountInBase,
|
||||
categoryId: next.categoryId,
|
||||
accountId: next.accountId,
|
||||
transactionDate: next.transactionDate,
|
||||
description: next.description,
|
||||
project: next.project,
|
||||
memo: next.memo,
|
||||
status: next.status,
|
||||
statusUpdatedAt: next.statusUpdatedAt,
|
||||
reimbursementBatch: next.reimbursementBatch,
|
||||
reviewNotes: next.reviewNotes,
|
||||
submittedBy: next.submittedBy,
|
||||
approvedBy: next.approvedBy,
|
||||
approvedAt: next.approvedAt,
|
||||
isDeleted: next.isDeleted ? 1 : 0,
|
||||
deletedAt,
|
||||
return mapTransaction(rows[0]);
|
||||
});
|
||||
|
||||
return getTransactionById(id);
|
||||
}
|
||||
|
||||
export function softDeleteTransaction(id: number) {
|
||||
const stmt = db.prepare(
|
||||
`UPDATE finance_transactions SET is_deleted = 1, deleted_at = @deletedAt WHERE id = @id`,
|
||||
export async function softDeleteTransaction(id: number) {
|
||||
const deletedAt = new Date().toISOString();
|
||||
const { rows } = await query<TransactionRow>(
|
||||
`UPDATE finance_transactions
|
||||
SET is_deleted = TRUE, deleted_at = $1
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
[deletedAt, id],
|
||||
);
|
||||
stmt.run({ id, deletedAt: new Date().toISOString() });
|
||||
return getTransactionById(id);
|
||||
const row = rows[0];
|
||||
return row ? mapTransaction(row) : null;
|
||||
}
|
||||
|
||||
export function restoreTransaction(id: number) {
|
||||
const stmt = db.prepare(
|
||||
`UPDATE finance_transactions SET is_deleted = 0, deleted_at = NULL WHERE id = @id`,
|
||||
export async function restoreTransaction(id: number) {
|
||||
const { rows } = await query<TransactionRow>(
|
||||
`UPDATE finance_transactions
|
||||
SET is_deleted = FALSE, deleted_at = NULL
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[id],
|
||||
);
|
||||
stmt.run({ id });
|
||||
return getTransactionById(id);
|
||||
const row = rows[0];
|
||||
return row ? mapTransaction(row) : null;
|
||||
}
|
||||
|
||||
export function replaceAllTransactions(
|
||||
export async function replaceAllTransactions(
|
||||
rows: Array<{
|
||||
accountId: null | number;
|
||||
amount: number;
|
||||
approvedAt?: null | string;
|
||||
approvedBy?: null | string;
|
||||
categoryId: null | number;
|
||||
createdAt?: string;
|
||||
currency: string;
|
||||
description: string;
|
||||
isDeleted?: boolean;
|
||||
memo?: null | string;
|
||||
project?: null | string;
|
||||
transactionDate: string;
|
||||
type: string;
|
||||
status?: TransactionStatus;
|
||||
statusUpdatedAt?: string;
|
||||
reimbursementBatch?: null | string;
|
||||
reviewNotes?: null | string;
|
||||
status?: TransactionStatus;
|
||||
statusUpdatedAt?: string;
|
||||
submittedBy?: null | string;
|
||||
approvedBy?: null | string;
|
||||
approvedAt?: null | string;
|
||||
isDeleted?: boolean;
|
||||
transactionDate: string;
|
||||
type: string;
|
||||
}>,
|
||||
) {
|
||||
db.prepare('DELETE FROM finance_transactions').run();
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(
|
||||
'TRUNCATE TABLE finance_transactions RESTART IDENTITY CASCADE',
|
||||
);
|
||||
|
||||
const insert = db.prepare(
|
||||
`INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, status, status_updated_at, reimbursement_batch, review_notes, submitted_by, approved_by, approved_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, @status, @statusUpdatedAt, @reimbursementBatch, @reviewNotes, @submittedBy, @approvedBy, @approvedAt, @isDeleted)`,
|
||||
);
|
||||
|
||||
const getRate = db.prepare(
|
||||
`SELECT rate FROM finance_exchange_rates WHERE from_currency = ? AND to_currency = 'CNY' ORDER BY date DESC LIMIT 1`,
|
||||
);
|
||||
|
||||
const insertMany = db.transaction((items: Array<any>) => {
|
||||
for (const item of items) {
|
||||
const row = getRate.get(item.currency) as undefined | { rate: number };
|
||||
const rate = row?.rate ?? 1;
|
||||
for (const item of rows) {
|
||||
const rate = await getExchangeRateToBase(client, item.currency);
|
||||
const amountInBase = +(item.amount * rate).toFixed(2);
|
||||
const createdAt =
|
||||
item.createdAt ??
|
||||
@@ -326,38 +439,67 @@ export function replaceAllTransactions(
|
||||
const status = item.status ?? 'approved';
|
||||
const statusUpdatedAt =
|
||||
item.statusUpdatedAt ??
|
||||
new Date(
|
||||
`${item.transactionDate}T00:00:00Z`,
|
||||
).toISOString();
|
||||
new Date(`${item.transactionDate}T00:00:00Z`).toISOString();
|
||||
const approvedAt =
|
||||
item.approvedAt ??
|
||||
(status === 'approved' || status === 'paid' ? statusUpdatedAt : null);
|
||||
insert.run({
|
||||
...item,
|
||||
exchangeRateToBase: rate,
|
||||
amountInBase,
|
||||
project: item.project ?? null,
|
||||
memo: item.memo ?? null,
|
||||
createdAt,
|
||||
status,
|
||||
statusUpdatedAt,
|
||||
reimbursementBatch: item.reimbursementBatch ?? null,
|
||||
reviewNotes: item.reviewNotes ?? null,
|
||||
submittedBy: item.submittedBy ?? null,
|
||||
approvedBy:
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO finance_transactions (
|
||||
type,
|
||||
amount,
|
||||
currency,
|
||||
exchange_rate_to_base,
|
||||
amount_in_base,
|
||||
category_id,
|
||||
account_id,
|
||||
transaction_date,
|
||||
description,
|
||||
project,
|
||||
memo,
|
||||
created_at,
|
||||
status,
|
||||
status_updated_at,
|
||||
reimbursement_batch,
|
||||
review_notes,
|
||||
submitted_by,
|
||||
approved_by,
|
||||
approved_at,
|
||||
is_deleted
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20
|
||||
)`,
|
||||
[
|
||||
item.type,
|
||||
item.amount,
|
||||
item.currency,
|
||||
rate,
|
||||
amountInBase,
|
||||
item.categoryId ?? null,
|
||||
item.accountId ?? null,
|
||||
item.transactionDate,
|
||||
item.description ?? '',
|
||||
item.project ?? null,
|
||||
item.memo ?? null,
|
||||
createdAt,
|
||||
status,
|
||||
statusUpdatedAt,
|
||||
item.reimbursementBatch ?? null,
|
||||
item.reviewNotes ?? null,
|
||||
item.submittedBy ?? null,
|
||||
status === 'approved' || status === 'paid'
|
||||
? item.approvedBy ?? null
|
||||
? (item.approvedBy ?? null)
|
||||
: null,
|
||||
approvedAt,
|
||||
isDeleted: item.isDeleted ? 1 : 0,
|
||||
});
|
||||
approvedAt,
|
||||
item.isDeleted ?? false,
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
insertMany(rows);
|
||||
}
|
||||
|
||||
// 分类相关函数
|
||||
interface CategoryRow {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -365,7 +507,7 @@ interface CategoryRow {
|
||||
icon: null | string;
|
||||
color: null | string;
|
||||
user_id: null | number;
|
||||
is_active: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
function mapCategory(row: CategoryRow) {
|
||||
@@ -382,15 +524,53 @@ function mapCategory(row: CategoryRow) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCategories(options: { type?: 'expense' | 'income' } = {}) {
|
||||
const where = options.type
|
||||
? `WHERE type = @type AND is_active = 1`
|
||||
: 'WHERE is_active = 1';
|
||||
const params = options.type ? { type: options.type } : {};
|
||||
|
||||
const stmt = db.prepare<CategoryRow>(
|
||||
`SELECT id, name, type, icon, color, user_id, is_active FROM finance_categories ${where} ORDER BY id ASC`,
|
||||
export async function fetchCategories(
|
||||
options: { type?: 'expense' | 'income' } = {},
|
||||
) {
|
||||
const params: any[] = [];
|
||||
const clauses: string[] = ['is_active = TRUE'];
|
||||
if (options.type) {
|
||||
params.push(options.type);
|
||||
clauses.push(`type = $${params.length}`);
|
||||
}
|
||||
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||
const { rows } = await query<CategoryRow>(
|
||||
`SELECT id,
|
||||
name,
|
||||
type,
|
||||
icon,
|
||||
color,
|
||||
user_id,
|
||||
is_active
|
||||
FROM finance_categories
|
||||
${where}
|
||||
ORDER BY id ASC`,
|
||||
params,
|
||||
);
|
||||
|
||||
return stmt.all(params).map(mapCategory);
|
||||
return rows.map((row) => mapCategory(row));
|
||||
}
|
||||
|
||||
export async function getAccountById(id: number) {
|
||||
const { rows } = await query<{
|
||||
currency: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}>(
|
||||
`SELECT id, name, currency
|
||||
FROM finance_accounts
|
||||
WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function getCategoryById(id: number) {
|
||||
const { rows } = await query<CategoryRow>(
|
||||
`SELECT id, name, type, icon, color, user_id, is_active
|
||||
FROM finance_categories
|
||||
WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
const row = rows[0];
|
||||
return row ? mapCategory(row) : null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
import db from './sqlite';
|
||||
import { query } from './db';
|
||||
|
||||
interface MediaRow {
|
||||
id: number;
|
||||
@@ -47,7 +47,7 @@ export interface MediaMessage {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
available: boolean;
|
||||
downloadUrl: string | null;
|
||||
downloadUrl: null | string;
|
||||
}
|
||||
|
||||
function mapMediaRow(row: MediaRow): MediaMessage {
|
||||
@@ -78,40 +78,85 @@ function mapMediaRow(row: MediaRow): MediaMessage {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMediaMessages(params: {
|
||||
limit?: number;
|
||||
fileTypes?: string[];
|
||||
} = {}) {
|
||||
const clauses: string[] = [];
|
||||
const bindParams: Record<string, unknown> = {};
|
||||
export async function fetchMediaMessages(
|
||||
params: {
|
||||
fileTypes?: string[];
|
||||
limit?: number;
|
||||
} = {},
|
||||
) {
|
||||
const whereClauses: string[] = [];
|
||||
const queryParams: any[] = [];
|
||||
|
||||
if (params.fileTypes && params.fileTypes.length > 0) {
|
||||
clauses.push(
|
||||
`file_type IN (${params.fileTypes.map((_, index) => `@type${index}`).join(', ')})`,
|
||||
);
|
||||
params.fileTypes.forEach((type, index) => {
|
||||
bindParams[`type${index}`] = type;
|
||||
const placeholders = params.fileTypes.map((type) => {
|
||||
queryParams.push(type);
|
||||
return `$${queryParams.length}`;
|
||||
});
|
||||
whereClauses.push(`file_type IN (${placeholders.join(', ')})`);
|
||||
}
|
||||
|
||||
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||
const where =
|
||||
whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
||||
const limitClause =
|
||||
params.limit && params.limit > 0 ? `LIMIT ${Number(params.limit)}` : '';
|
||||
|
||||
const stmt = db.prepare<MediaRow>(
|
||||
`SELECT id, chat_id, message_id, user_id, username, display_name, file_type, file_id, file_unique_id, caption, file_name, file_path, file_size, mime_type, duration, width, height, forwarded_to, created_at, updated_at FROM finance_media_messages ${where} ORDER BY datetime(created_at) DESC, id DESC ${limitClause}`,
|
||||
const { rows } = await query<MediaRow>(
|
||||
`SELECT id,
|
||||
chat_id,
|
||||
message_id,
|
||||
user_id,
|
||||
username,
|
||||
display_name,
|
||||
file_type,
|
||||
file_id,
|
||||
file_unique_id,
|
||||
caption,
|
||||
file_name,
|
||||
file_path,
|
||||
file_size,
|
||||
mime_type,
|
||||
duration,
|
||||
width,
|
||||
height,
|
||||
forwarded_to,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM finance_media_messages
|
||||
${where}
|
||||
ORDER BY created_at DESC, id DESC
|
||||
${limitClause}`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return stmt.all(bindParams).map(mapMediaRow);
|
||||
return rows.map((row) => mapMediaRow(row));
|
||||
}
|
||||
|
||||
export function getMediaMessageById(id: number) {
|
||||
const stmt = db.prepare<MediaRow>(
|
||||
`SELECT id, chat_id, message_id, user_id, username, display_name, file_type, file_id, file_unique_id, caption, file_name, file_path, file_size, mime_type, duration, width, height, forwarded_to, created_at, updated_at FROM finance_media_messages WHERE id = ?`,
|
||||
export async function getMediaMessageById(id: number) {
|
||||
const { rows } = await query<MediaRow>(
|
||||
`SELECT id,
|
||||
chat_id,
|
||||
message_id,
|
||||
user_id,
|
||||
username,
|
||||
display_name,
|
||||
file_type,
|
||||
file_id,
|
||||
file_unique_id,
|
||||
caption,
|
||||
file_name,
|
||||
file_path,
|
||||
file_size,
|
||||
mime_type,
|
||||
duration,
|
||||
width,
|
||||
height,
|
||||
forwarded_to,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM finance_media_messages
|
||||
WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
|
||||
const row = stmt.get(id);
|
||||
|
||||
const row = rows[0];
|
||||
return row ? mapMediaRow(row) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import { mkdirSync } from 'node:fs';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { dirname, join } from 'pathe';
|
||||
|
||||
const dbFile = join(process.cwd(), 'storage', 'finance.db');
|
||||
|
||||
mkdirSync(dirname(dbFile), { recursive: true });
|
||||
|
||||
const database = new Database(dbFile);
|
||||
|
||||
function assertIdentifier(name: string) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
||||
throw new Error(`Invalid identifier: ${name}`);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function ensureColumn(table: string, column: string, definition: string) {
|
||||
const safeTable = assertIdentifier(table);
|
||||
const safeColumn = assertIdentifier(column);
|
||||
const columns = database
|
||||
.prepare<{ name: string }>(`PRAGMA table_info(${safeTable})`)
|
||||
.all();
|
||||
if (!columns.some((item) => item.name === safeColumn)) {
|
||||
database.exec(`ALTER TABLE ${safeTable} ADD COLUMN ${definition}`);
|
||||
}
|
||||
}
|
||||
|
||||
database.pragma('journal_mode = WAL');
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS finance_currencies (
|
||||
code TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
is_base INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS finance_exchange_rates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
from_currency TEXT NOT NULL,
|
||||
to_currency TEXT NOT NULL,
|
||||
rate REAL NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
source TEXT DEFAULT 'manual'
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS finance_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'cash',
|
||||
icon TEXT,
|
||||
color TEXT,
|
||||
user_id INTEGER DEFAULT 1,
|
||||
is_active INTEGER DEFAULT 1
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS finance_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
icon TEXT,
|
||||
color TEXT,
|
||||
user_id INTEGER DEFAULT 1,
|
||||
is_active INTEGER DEFAULT 1
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS finance_transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
exchange_rate_to_base REAL NOT NULL,
|
||||
amount_in_base REAL NOT NULL,
|
||||
category_id INTEGER,
|
||||
account_id INTEGER,
|
||||
transaction_date TEXT NOT NULL,
|
||||
description TEXT,
|
||||
project TEXT,
|
||||
memo TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'approved',
|
||||
status_updated_at TEXT,
|
||||
reimbursement_batch TEXT,
|
||||
review_notes TEXT,
|
||||
submitted_by TEXT,
|
||||
approved_by TEXT,
|
||||
approved_at TEXT,
|
||||
is_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
deleted_at TEXT,
|
||||
FOREIGN KEY (currency) REFERENCES finance_currencies(code),
|
||||
FOREIGN KEY (category_id) REFERENCES finance_categories(id),
|
||||
FOREIGN KEY (account_id) REFERENCES finance_accounts(id)
|
||||
);
|
||||
`);
|
||||
|
||||
ensureColumn(
|
||||
'finance_transactions',
|
||||
'status',
|
||||
"status TEXT NOT NULL DEFAULT 'approved'",
|
||||
);
|
||||
ensureColumn('finance_transactions', 'status_updated_at', 'status_updated_at TEXT');
|
||||
ensureColumn(
|
||||
'finance_transactions',
|
||||
'reimbursement_batch',
|
||||
'reimbursement_batch TEXT',
|
||||
);
|
||||
ensureColumn('finance_transactions', 'review_notes', 'review_notes TEXT');
|
||||
ensureColumn('finance_transactions', 'submitted_by', 'submitted_by TEXT');
|
||||
ensureColumn('finance_transactions', 'approved_by', 'approved_by TEXT');
|
||||
ensureColumn('finance_transactions', 'approved_at', 'approved_at TEXT');
|
||||
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS finance_media_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
username TEXT,
|
||||
display_name TEXT,
|
||||
file_type TEXT NOT NULL,
|
||||
file_id TEXT NOT NULL,
|
||||
file_unique_id TEXT,
|
||||
caption TEXT,
|
||||
file_name TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
duration INTEGER,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
forwarded_to INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(chat_id, message_id)
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_finance_media_messages_created_at
|
||||
ON finance_media_messages (created_at DESC);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_finance_media_messages_user_id
|
||||
ON finance_media_messages (user_id);
|
||||
`);
|
||||
|
||||
// Telegram通知配置表
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS telegram_notification_configs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
bot_token TEXT NOT NULL,
|
||||
chat_id TEXT NOT NULL,
|
||||
notification_types TEXT NOT NULL,
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
priority TEXT DEFAULT 'normal',
|
||||
rate_limit_seconds INTEGER DEFAULT 0,
|
||||
batch_enabled INTEGER DEFAULT 0,
|
||||
batch_interval_minutes INTEGER DEFAULT 60,
|
||||
retry_enabled INTEGER DEFAULT 1,
|
||||
retry_max_attempts INTEGER DEFAULT 3,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_configs_enabled
|
||||
ON telegram_notification_configs (is_enabled);
|
||||
`);
|
||||
|
||||
// 通知发送历史表(用于频率控制和去重)
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS telegram_notification_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
config_id INTEGER NOT NULL,
|
||||
notification_type TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
sent_at TEXT,
|
||||
error_message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (config_id) REFERENCES telegram_notification_configs(id)
|
||||
);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_config
|
||||
ON telegram_notification_history (config_id, created_at DESC);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_hash
|
||||
ON telegram_notification_history (content_hash, created_at DESC);
|
||||
`);
|
||||
|
||||
database.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_telegram_notification_history_status
|
||||
ON telegram_notification_history (status, retry_count);
|
||||
`);
|
||||
|
||||
// 确保添加新列到已存在的表
|
||||
ensureColumn(
|
||||
'telegram_notification_configs',
|
||||
'priority',
|
||||
"priority TEXT DEFAULT 'normal'",
|
||||
);
|
||||
ensureColumn(
|
||||
'telegram_notification_configs',
|
||||
'rate_limit_seconds',
|
||||
'rate_limit_seconds INTEGER DEFAULT 0',
|
||||
);
|
||||
ensureColumn(
|
||||
'telegram_notification_configs',
|
||||
'batch_enabled',
|
||||
'batch_enabled INTEGER DEFAULT 0',
|
||||
);
|
||||
ensureColumn(
|
||||
'telegram_notification_configs',
|
||||
'batch_interval_minutes',
|
||||
'batch_interval_minutes INTEGER DEFAULT 60',
|
||||
);
|
||||
ensureColumn(
|
||||
'telegram_notification_configs',
|
||||
'retry_enabled',
|
||||
'retry_enabled INTEGER DEFAULT 1',
|
||||
);
|
||||
ensureColumn(
|
||||
'telegram_notification_configs',
|
||||
'retry_max_attempts',
|
||||
'retry_max_attempts INTEGER DEFAULT 3',
|
||||
);
|
||||
|
||||
export default database;
|
||||
@@ -1,491 +1,21 @@
|
||||
import crypto from 'node:crypto';
|
||||
import db from './sqlite';
|
||||
import {
|
||||
getEnabledNotificationConfigs,
|
||||
notifyTransaction,
|
||||
testTelegramConfig,
|
||||
} from './telegram-bot';
|
||||
|
||||
interface TelegramNotificationConfig {
|
||||
id: number;
|
||||
name: string;
|
||||
botToken: string;
|
||||
chatId: string;
|
||||
notificationTypes: string[];
|
||||
isEnabled: boolean;
|
||||
priority: string;
|
||||
rateLimitSeconds: number;
|
||||
batchEnabled: boolean;
|
||||
batchIntervalMinutes: number;
|
||||
retryEnabled: boolean;
|
||||
retryMaxAttempts: number;
|
||||
}
|
||||
export { getEnabledNotificationConfigs, testTelegramConfig };
|
||||
|
||||
interface TransactionNotificationData {
|
||||
id: number;
|
||||
type: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
categoryName?: string;
|
||||
accountName?: string;
|
||||
transactionDate: string;
|
||||
description?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成消息内容hash(用于去重)
|
||||
*/
|
||||
function generateContentHash(content: string): string {
|
||||
return crypto.createHash('md5').update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查频率限制
|
||||
*/
|
||||
function checkRateLimit(configId: number, rateLimitSeconds: number): boolean {
|
||||
if (rateLimitSeconds <= 0) {
|
||||
return true; // 无限制
|
||||
}
|
||||
|
||||
const cutoffTime = new Date(
|
||||
Date.now() - rateLimitSeconds * 1000,
|
||||
).toISOString();
|
||||
|
||||
const recent = db
|
||||
.prepare<{ count: number }>(
|
||||
`
|
||||
SELECT COUNT(*) as count
|
||||
FROM telegram_notification_history
|
||||
WHERE config_id = ? AND status = 'sent' AND sent_at > ?
|
||||
`,
|
||||
)
|
||||
.get(configId, cutoffTime);
|
||||
|
||||
return (recent?.count || 0) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为重复消息
|
||||
*/
|
||||
function isDuplicateMessage(
|
||||
configId: number,
|
||||
contentHash: string,
|
||||
withinMinutes: number = 5,
|
||||
): boolean {
|
||||
const cutoffTime = new Date(Date.now() - withinMinutes * 60 * 1000).toISOString();
|
||||
|
||||
const duplicate = db
|
||||
.prepare<{ count: number }>(
|
||||
`
|
||||
SELECT COUNT(*) as count
|
||||
FROM telegram_notification_history
|
||||
WHERE config_id = ? AND content_hash = ? AND created_at > ?
|
||||
`,
|
||||
)
|
||||
.get(configId, contentHash, cutoffTime);
|
||||
|
||||
return (duplicate?.count || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录通知历史
|
||||
*/
|
||||
function recordNotification(
|
||||
configId: number,
|
||||
notificationType: string,
|
||||
contentHash: string,
|
||||
status: 'pending' | 'sent' | 'failed',
|
||||
errorMessage?: string,
|
||||
): number {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const result = db
|
||||
.prepare<unknown, [number, string, string, string, string | null, string | null, string]>(
|
||||
`
|
||||
INSERT INTO telegram_notification_history
|
||||
(config_id, notification_type, content_hash, status, sent_at, error_message, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
)
|
||||
.run(
|
||||
configId,
|
||||
notificationType,
|
||||
contentHash,
|
||||
status,
|
||||
status === 'sent' ? now : null,
|
||||
errorMessage || null,
|
||||
now,
|
||||
);
|
||||
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通知状态
|
||||
*/
|
||||
function updateNotificationStatus(
|
||||
historyId: number,
|
||||
status: 'sent' | 'failed',
|
||||
retryCount: number = 0,
|
||||
errorMessage?: string,
|
||||
): void {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE telegram_notification_history
|
||||
SET status = ?, retry_count = ?, sent_at = ?, error_message = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
).run(status, retryCount, status === 'sent' ? now : null, errorMessage || null, historyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待重试的通知
|
||||
*/
|
||||
function getPendingRetries(): Array<{
|
||||
id: number;
|
||||
configId: number;
|
||||
contentHash: string;
|
||||
retryCount: number;
|
||||
}> {
|
||||
return db
|
||||
.prepare<{ id: number; config_id: number; content_hash: string; retry_count: number }>(
|
||||
`
|
||||
SELECT h.id, h.config_id, h.content_hash, h.retry_count
|
||||
FROM telegram_notification_history h
|
||||
JOIN telegram_notification_configs c ON h.config_id = c.id
|
||||
WHERE h.status = 'failed'
|
||||
AND c.retry_enabled = 1
|
||||
AND h.retry_count < c.retry_max_attempts
|
||||
AND h.created_at > datetime('now', '-24 hours')
|
||||
ORDER BY h.created_at ASC
|
||||
LIMIT 10
|
||||
`,
|
||||
)
|
||||
.all()
|
||||
.map((row) => ({
|
||||
id: row.id,
|
||||
configId: row.config_id,
|
||||
contentHash: row.content_hash,
|
||||
retryCount: row.retry_count,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的Telegram通知配置(增强版)
|
||||
*/
|
||||
export function getEnabledNotificationConfigs(
|
||||
notificationType: string = 'transaction',
|
||||
): TelegramNotificationConfig[] {
|
||||
const rows = db
|
||||
.prepare<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: number;
|
||||
priority: string;
|
||||
rate_limit_seconds: number;
|
||||
batch_enabled: number;
|
||||
batch_interval_minutes: number;
|
||||
retry_enabled: number;
|
||||
retry_max_attempts: number;
|
||||
}>(
|
||||
`
|
||||
SELECT id, name, bot_token, chat_id, notification_types, is_enabled,
|
||||
priority, rate_limit_seconds, batch_enabled, batch_interval_minutes,
|
||||
retry_enabled, retry_max_attempts
|
||||
FROM telegram_notification_configs
|
||||
WHERE is_enabled = 1
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
|
||||
return rows
|
||||
.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
botToken: row.bot_token,
|
||||
chatId: row.chat_id,
|
||||
notificationTypes: JSON.parse(row.notification_types) as string[],
|
||||
isEnabled: row.is_enabled === 1,
|
||||
priority: row.priority || 'normal',
|
||||
rateLimitSeconds: row.rate_limit_seconds || 0,
|
||||
batchEnabled: (row.batch_enabled || 0) === 1,
|
||||
batchIntervalMinutes: row.batch_interval_minutes || 60,
|
||||
retryEnabled: (row.retry_enabled || 1) === 1,
|
||||
retryMaxAttempts: row.retry_max_attempts || 3,
|
||||
}))
|
||||
.filter((config) => config.notificationTypes.includes(notificationType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化交易金额
|
||||
*/
|
||||
function formatAmount(amount: number, currency: string): string {
|
||||
const formatted = amount.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
return `${currency} ${formatted}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化交易类型
|
||||
*/
|
||||
function formatTransactionType(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
income: '💰 收入',
|
||||
expense: '💸 支出',
|
||||
transfer: '🔄 转账',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化交易状态
|
||||
*/
|
||||
function formatTransactionStatus(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: '📝 草稿',
|
||||
pending: '⏳ 待审核',
|
||||
approved: '✅ 已批准',
|
||||
rejected: '❌ 已拒绝',
|
||||
paid: '💵 已支付',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化优先级标识
|
||||
*/
|
||||
function formatPriority(priority: string): string {
|
||||
const priorityMap: Record<string, string> = {
|
||||
low: '🔵',
|
||||
normal: '⚪',
|
||||
high: '🟡',
|
||||
urgent: '🔴',
|
||||
};
|
||||
return priorityMap[priority] || '⚪';
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建交易通知消息
|
||||
*/
|
||||
function buildTransactionMessage(
|
||||
transaction: TransactionNotificationData,
|
||||
action: string = 'created',
|
||||
priority: string = 'normal',
|
||||
): string {
|
||||
const actionMap: Record<string, string> = {
|
||||
created: '📋 新增账目记录',
|
||||
updated: '✏️ 更新账目记录',
|
||||
deleted: '🗑️ 删除账目记录',
|
||||
};
|
||||
|
||||
const priorityIcon = formatPriority(priority);
|
||||
|
||||
const lines: string[] = [
|
||||
`${priorityIcon} ${actionMap[action] || '📋 账目记录'}`,
|
||||
'',
|
||||
`类型:${formatTransactionType(transaction.type)}`,
|
||||
`金额:${formatAmount(transaction.amount, transaction.currency)}`,
|
||||
`日期:${transaction.transactionDate}`,
|
||||
];
|
||||
|
||||
if (transaction.categoryName) {
|
||||
lines.push(`分类:${transaction.categoryName}`);
|
||||
}
|
||||
|
||||
if (transaction.accountName) {
|
||||
lines.push(`账户:${transaction.accountName}`);
|
||||
}
|
||||
|
||||
lines.push(`状态:${formatTransactionStatus(transaction.status)}`);
|
||||
|
||||
if (transaction.description) {
|
||||
lines.push(``, `备注:${transaction.description}`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
``,
|
||||
`🕐 记录时间:${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`,
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送Telegram消息(带重试)
|
||||
*/
|
||||
async function sendTelegramMessage(
|
||||
botToken: string,
|
||||
chatId: string,
|
||||
message: string,
|
||||
retryCount: number = 0,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: message,
|
||||
parse_mode: 'HTML',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ description: 'Unknown error' }));
|
||||
const errorMsg = error.description || `HTTP ${response.status}`;
|
||||
console.error(
|
||||
'[telegram-bot-enhanced] Failed to send message:',
|
||||
response.status,
|
||||
errorMsg,
|
||||
);
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('[telegram-bot-enhanced] Error sending message:', errorMsg);
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知交易记录(增强版 - 带频率控制、去重、重试)
|
||||
*/
|
||||
export async function notifyTransactionEnhanced(
|
||||
transaction: TransactionNotificationData,
|
||||
action: string = 'created',
|
||||
): Promise<void> {
|
||||
const configs = getEnabledNotificationConfigs('transaction');
|
||||
|
||||
if (configs.length === 0) {
|
||||
console.log('[telegram-bot-enhanced] No enabled notification configs found');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const config of configs) {
|
||||
// 1. 检查频率限制
|
||||
if (!checkRateLimit(config.id, config.rateLimitSeconds)) {
|
||||
console.log(
|
||||
`[telegram-bot-enhanced] Rate limit exceeded for config: ${config.name}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 构建消息
|
||||
const message = buildTransactionMessage(transaction, action, config.priority);
|
||||
const contentHash = generateContentHash(message);
|
||||
|
||||
// 3. 检查重复消息
|
||||
if (isDuplicateMessage(config.id, contentHash)) {
|
||||
console.log(
|
||||
`[telegram-bot-enhanced] Duplicate message detected for config: ${config.name}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. 记录通知历史
|
||||
const historyId = recordNotification(
|
||||
config.id,
|
||||
'transaction',
|
||||
contentHash,
|
||||
'pending',
|
||||
);
|
||||
|
||||
// 5. 发送消息
|
||||
const result = await sendTelegramMessage(
|
||||
config.botToken,
|
||||
config.chatId,
|
||||
message,
|
||||
);
|
||||
|
||||
// 6. 更新状态
|
||||
if (result.success) {
|
||||
updateNotificationStatus(historyId, 'sent');
|
||||
console.log(
|
||||
`[telegram-bot-enhanced] Sent notification via config: ${config.name}`,
|
||||
);
|
||||
} else {
|
||||
updateNotificationStatus(historyId, 'failed', 0, result.error);
|
||||
console.error(
|
||||
`[telegram-bot-enhanced] Failed to send notification via config: ${config.name}, error: ${result.error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
...args: Parameters<typeof notifyTransaction>
|
||||
) {
|
||||
await notifyTransaction(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试失败的通知
|
||||
*/
|
||||
export async function retryFailedNotifications(): Promise<void> {
|
||||
const pending = getPendingRetries();
|
||||
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[telegram-bot-enhanced] Retrying ${pending.length} failed notifications`,
|
||||
);
|
||||
|
||||
for (const item of pending) {
|
||||
// 获取配置
|
||||
const config = db
|
||||
.prepare<{
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
priority: string;
|
||||
}>(
|
||||
'SELECT bot_token, chat_id, priority FROM telegram_notification_configs WHERE id = ?',
|
||||
)
|
||||
.get(item.configId);
|
||||
|
||||
if (!config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 注意:这里需要重新构建消息或从历史中获取
|
||||
// 简化处理:发送重试通知
|
||||
const retryMessage = `🔄 通知重试 (尝试 ${item.retryCount + 1})`;
|
||||
|
||||
const result = await sendTelegramMessage(
|
||||
config.bot_token,
|
||||
config.chat_id,
|
||||
retryMessage,
|
||||
item.retryCount,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
updateNotificationStatus(item.id, 'sent', item.retryCount + 1);
|
||||
console.log(`[telegram-bot-enhanced] Retry successful for history ID: ${item.id}`);
|
||||
} else {
|
||||
updateNotificationStatus(
|
||||
item.id,
|
||||
'failed',
|
||||
item.retryCount + 1,
|
||||
result.error,
|
||||
);
|
||||
console.error(
|
||||
`[telegram-bot-enhanced] Retry failed for history ID: ${item.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试Telegram Bot配置
|
||||
*/
|
||||
export async function testTelegramConfig(
|
||||
botToken: string,
|
||||
chatId: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const testMessage = `🤖 KT财务系统\n\n✅ Telegram通知配置测试成功!\n\n🕐 ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`;
|
||||
|
||||
return await sendTelegramMessage(botToken, chatId, testMessage);
|
||||
// Retrying logic is not yet implemented for the PostgreSQL data source.
|
||||
// The SQLite-specific implementation relied on synchronous database access.
|
||||
// If this functionality becomes necessary, please implement it using the
|
||||
// telegram_notification_history table with pool-based transactions.
|
||||
console.warn('[telegram-bot-enhanced] retryFailedNotifications is not implemented.');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import db from './sqlite';
|
||||
import { query } from './db';
|
||||
|
||||
interface TelegramNotificationConfig {
|
||||
id: number;
|
||||
@@ -24,18 +24,21 @@ interface TransactionNotificationData {
|
||||
/**
|
||||
* 获取所有启用的Telegram通知配置
|
||||
*/
|
||||
export function getEnabledNotificationConfigs(
|
||||
export async function getEnabledNotificationConfigs(
|
||||
notificationType: string = 'transaction',
|
||||
): TelegramNotificationConfig[] {
|
||||
const rows = db
|
||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number }>(
|
||||
`
|
||||
SELECT id, name, bot_token, chat_id, notification_types, is_enabled
|
||||
FROM telegram_notification_configs
|
||||
WHERE is_enabled = 1
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
): Promise<TelegramNotificationConfig[]> {
|
||||
const { rows } = await query<{
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
id: number;
|
||||
is_enabled: boolean;
|
||||
name: string;
|
||||
notification_types: string;
|
||||
}>(
|
||||
`SELECT id, name, bot_token, chat_id, notification_types, is_enabled
|
||||
FROM telegram_notification_configs
|
||||
WHERE is_enabled = TRUE`,
|
||||
);
|
||||
|
||||
return rows
|
||||
.map((row) => ({
|
||||
@@ -44,7 +47,7 @@ export function getEnabledNotificationConfigs(
|
||||
botToken: row.bot_token,
|
||||
chatId: row.chat_id,
|
||||
notificationTypes: JSON.parse(row.notification_types) as string[],
|
||||
isEnabled: row.is_enabled === 1,
|
||||
isEnabled: row.is_enabled,
|
||||
}))
|
||||
.filter((config) => config.notificationTypes.includes(notificationType));
|
||||
}
|
||||
@@ -175,10 +178,10 @@ export async function notifyTransaction(
|
||||
transaction: TransactionNotificationData,
|
||||
action: string = 'created',
|
||||
): Promise<void> {
|
||||
const configs = getEnabledNotificationConfigs('transaction');
|
||||
const configs = await getEnabledNotificationConfigs('transaction');
|
||||
|
||||
if (configs.length === 0) {
|
||||
console.log('[telegram-bot] No enabled notification configs found');
|
||||
console.warn('[telegram-bot] No enabled notification configs found');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,7 +195,7 @@ export async function notifyTransaction(
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
console.log(
|
||||
console.warn(
|
||||
`[telegram-bot] Sent notification via config: ${configs[index].name}`,
|
||||
);
|
||||
} else {
|
||||
@@ -209,17 +212,18 @@ export async function notifyTransaction(
|
||||
export async function testTelegramConfig(
|
||||
botToken: string,
|
||||
chatId: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<{ error?: string; success: boolean }> {
|
||||
try {
|
||||
const testMessage = `🤖 KT财务系统\n\n✅ Telegram通知配置测试成功!\n\n🕐 ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`;
|
||||
|
||||
const success = await sendTelegramMessage(botToken, chatId, testMessage);
|
||||
|
||||
if (success) {
|
||||
return { success: true };
|
||||
} else {
|
||||
return { success: false, error: '发送消息失败,请检查Bot Token和Chat ID' };
|
||||
}
|
||||
return success
|
||||
? { success: true }
|
||||
: {
|
||||
success: false,
|
||||
error: '发送消息失败,请检查Bot Token和Chat ID',
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
658
data/finance/finance-combined.csv
Normal file
658
data/finance/finance-combined.csv
Normal file
@@ -0,0 +1,658 @@
|
||||
日期,类型,分类,项目名称,金额,币种,账户
|
||||
2025-10-27,支出,🏷️ 广告推广,谷歌广告,1000,USDT,未知账户
|
||||
2025-10-27,支出,🏷️ 其他支出,爱拼才会赢 退款,273,USDT,未知账户
|
||||
2025-10-27,支出,🏷️ 其他支出,鼎胜国际退款,140,USDT,未知账户
|
||||
2025-10-24,支出,🏷️ 其他支出,买飞机票,142,USDT,未知账户
|
||||
2025-10-23,支出,🏷️ 广告推广,谷歌广告费,50,USDT,未知账户
|
||||
2025-10-22,支出,🏷️ 佣金/返佣,阿宏返佣9月,896,USDT,未知账户
|
||||
2025-10-22,支出,🏷️ 其他支出,泰国支出,30700,USDT,未知账户
|
||||
2025-10-20,支出,🏷️ 工资,煮饭阿姨工资,3000,USDT,未知账户
|
||||
2025-10-18,支出,🏷️ 服务器/技术,Open AI服务器续费预存,5000,USDT,未知账户
|
||||
2025-10-17,支出,🏷️ 服务器/技术,购买域名地址,201.61,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 工资,虚拟卡一张,11,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 分红,皇雨工资,11364,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 分红,代理ip小哥工资,994,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 分红,SY工资,4761,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 分红,菲菲,1918,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 分红,cp工资,1000,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 未分类支出,羽琦返佣,2960,USDT,未知账户
|
||||
2025-10-07,支出,🏷️ 未分类支出,666返佣,230,USDT,未知账户
|
||||
2025-10-06,支出,🏷️ 未分类支出,金返佣,815,USDT,未知账户
|
||||
2025-10-05,支出,🏷️ 工资,5张esim卡续费预充值,203,USDT,未知账户
|
||||
2025-10-04,支出,🏷️ 未分类支出,合鑫返佣,285,USDT,未知账户
|
||||
2025-10-04,支出,🏷️ 未分类支出,无名返佣,2023,USDT,未知账户
|
||||
2025-10-04,支出,🏷️ 未分类支出,胖兔返佣,3134,USDT,未知账户
|
||||
2025-10-04,支出,🏷️ 未分类支出,恋哥返佣,271,USDT,未知账户
|
||||
2025-10-04,支出,🏷️ 未分类支出,方向返佣,162,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 分红,香缇卡工资,1122,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 分红,龙腾集团转给天天,11560,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 服务器/技术,龙腾借走,7000,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 借款/转账,泰国支出的费用,6221,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 未分类支出,杰夫返佣,1476,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 分红,天天8月报销,4649,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 分红,天天9月报销,931,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 未分类支出,国哥返佣(8月和9月),60,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 未分类支出,市场经理返佣,1388,USDT,未知账户
|
||||
2025-10-03,支出,🏷️ 未分类支出,天龙返佣,8530,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 借款/转账,后勤大叔一个半月薪资,710,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 分红,助理OAC工资,1500,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 借款/转账,小江江,500,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 借款/转账,程程,500,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 未分类支出,绿豆汤返佣,849,USDT,未知账户
|
||||
2025-10-02,支出,🏷️ 未分类支出,OAC返佣,1000,USDT,未知账户
|
||||
2025-09-30,支出,🏷️ 其他支出,合源公司退款,247,USDT,未知账户
|
||||
2025-09-30,支出,🏷️ 未分类支出,Jack帅哥返佣,536,USDT,未知账户
|
||||
2025-09-28,支出,🏷️ 其他支出,三喜团队退款,500,USDT,未知账户
|
||||
2025-09-28,支出,🏷️ 其他支出,爱拼才会赢 退款,265,USDT,未知账户
|
||||
2025-09-27,支出,🏷️ 分红,龙腾转给天天,1100,USDT,未知账户
|
||||
2025-09-25,支出,🏷️ 服务器/技术,马来西亚(龙腾月底转回来),1500,USDT,未知账户
|
||||
2025-09-23,支出,🏷️ 佣金/返佣,服务器续费,169.01,USDT,未知账户
|
||||
2025-09-20,支出,🏷️ 退款,电脑 3550*2=7100 显示器3600*7=25200 笔记本电脑 78500*1=78500 合计:110800元,15873,USDT,未知账户
|
||||
2025-09-20,支出,🏷️ 佣金/返佣,服务器续费预存,5000,USDT,未知账户
|
||||
2025-09-17,支出,🏷️ 借款/转账,买2000TRX,737.424,USDT,未知账户
|
||||
2025-09-17,支出,🏷️ 借款/转账,自动激活地址购买TRX,171,USDT,未知账户
|
||||
2025-09-12,支出,🏷️ 未分类支出,阿宏返佣,384,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 佣金/返佣,cursor,40,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 佣金/返佣,服务器59u+128u,187,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 佣金/返佣,google翻译,957,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 佣金/返佣,openrouter 210u+210u,420,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 佣金/返佣,Claude code,250,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 借款/转账,泰国支出的费用,6289,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 借款/转账,Funstat 开通镜像,4.11,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,666返佣,233,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,合鑫返佣,375,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,恋哥返佣,309,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,方向返佣,214,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,市场经理返佣,708,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 分红,皇雨返佣,1208,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,乐乐返佣,166,USDT,未知账户
|
||||
2025-09-06,支出,🏷️ 未分类支出,pt返佣,965,USDT,未知账户
|
||||
2025-09-05,支出,🏷️ 佣金/返佣,Open Ai服务器续费预存,5000,USDT,未知账户
|
||||
2025-09-05,支出,🏷️ 其他支出,星链宇宙退款,77,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,超鹏工资,452,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,小白工资,387,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,助理OAC工资,1500,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,香缇卡工资,1122,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 借款/转账,小江江,500,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 借款/转账,程程,500,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,皇雨工资,11332,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,代理ip小哥工资,992,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 分红,SY工资,4750,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 未分类支出,杰夫返佣,845,USDT,未知账户
|
||||
2025-09-04,支出,🏷️ 未分类支出,老虎返佣,46,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 分红,龙腾集团转给天天,14265,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 其他支出,Jack帅哥退款,320,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,Jack帅哥返佣,380,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,绿豆汤返佣,460,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 其他支出,英才团队退款,569,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 其他支出,天一退款,21,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,OAC返佣,538,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,金返佣,470,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,天龙返佣,11261,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,胖兔返佣,3225,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,羽琦返佣,2620,USDT,未知账户
|
||||
2025-08-31,支出,🏷️ 未分类支出,无名返佣,2531,USDT,未知账户
|
||||
2025-08-30,支出,🏷️ 其他支出,三喜团队退款,1000,USDT,未知账户
|
||||
2025-08-28,支出,🏷️ 服务器/技术,柬埔寨出差费用,3000,USDT,未知账户
|
||||
2025-08-24,支出,🏷️ 佣金/返佣,服务器续费,169,USDT,未知账户
|
||||
2025-08-22,支出,🏷️ 其他支出,爱拼才会赢 退款,103,USDT,未知账户
|
||||
2025-08-21,支出,🏷️ 佣金/返佣,新OpenAi充值,1028,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 佣金/返佣,7月 服务器 59u + 128u,187,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 佣金/返佣,7月 google 翻译 1051u,1051,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 佣金/返佣,7月 openrouter 105u + 105u + 105u,315,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 佣金/返佣,7月 Claude code 250u,250,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 佣金/返佣,7月 cursor 131u,131,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 分红,代理ip小哥工资,990,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 分红,SY工资,4744,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 分红,皇雨工资,11316,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 分红,7月 皇雨返佣,847,USDT,未知账户
|
||||
2025-08-14,支出,🏷️ 其他支出,新阿金公司退款,232,USDT,未知账户
|
||||
2025-08-12,支出,🏷️ 服务器/技术,转给阿寒,16199,USDT,未知账户
|
||||
2025-08-10,支出,🏷️ 佣金/返佣,Open AI,5000,USDT,未知账户
|
||||
2025-08-07,支出,🏷️ 未分类支出,金返佣(6月和7月),305,USDT,未知账户
|
||||
2025-08-07,支出,🏷️ 其他支出,兰博基尼退款,76,USDT,未知账户
|
||||
2025-08-06,支出,🏷️ 未分类支出,乐乐返佣,166,USDT,未知账户
|
||||
2025-08-06,支出,🏷️ 未分类支出,阿宏返佣(6月和7月),2290,USDT,未知账户
|
||||
2025-08-06,支出,🏷️ 未分类支出,恋哥返佣,326,USDT,未知账户
|
||||
2025-08-02,支出,🏷️ 借款/转账,泰国的费用,6400,USDT,未知账户
|
||||
2025-08-02,支出,🏷️ 未分类支出,市场经理返佣,390,USDT,未知账户
|
||||
2025-08-02,支出,🏷️ 未分类支出,无名返佣,2267,USDT,未知账户
|
||||
2025-08-02,支出,🏷️ 未分类支出,兔子返佣,112,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 服务器/技术,龙腾集团,9792,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 佣金/返佣,服务器续费预存,5000,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 分红,香缇卡工资,1122,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 其他支出,盛天退款,130,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,Jack帅哥佣金,1297,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,OAC00返佣,550,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,方向返佣,199,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,天龙返佣,10263,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,合鑫返佣,315,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,绿豆汤返佣,826,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,胖兔返佣,3646,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,羽琦返佣,2400,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,国哥返佣,60,USDT,未知账户
|
||||
2025-08-01,支出,🏷️ 未分类支出,杰夫返佣,1551,USDT,未知账户
|
||||
2025-07-20,支出,🏷️ 佣金/返佣,服务器续费(人民币1200),168.3,USDT,未知账户
|
||||
2025-07-16,支出,🏷️ 其他支出,左岸退款,34.7,USDT,未知账户
|
||||
2025-07-12,支出,🏷️ 佣金/返佣,Open Ai服务器续费预存,5000,USDT,未知账户
|
||||
2025-07-09,支出,🏷️ 未分类支出,方向返佣,383,USDT,未知账户
|
||||
2025-07-07,支出,🏷️ 借款/转账,鑫晟公司5月3600u ,6月2880u,6480,USDT,未知账户
|
||||
2025-07-05,支出,🏷️ 借款/转账,买trx 2000,610.2,USDT,未知账户
|
||||
2025-07-05,支出,🏷️ 分红,代理ip小哥工资,990,USDT,未知账户
|
||||
2025-07-05,支出,🏷️ 分红,SY工资,4743,USDT,未知账户
|
||||
2025-07-05,支出,🏷️ 分红,皇雨工资,11316,USDT,未知账户
|
||||
2025-07-05,支出,💰 未分类收入,蚊子分红,30000,USDT,未知账户
|
||||
2025-07-05,支出,💰 未分类收入,阿寒分红,30000,USDT,未知账户
|
||||
2025-07-04,支出,🏷️ 未分类支出,胖兔返佣,2760,USDT,未知账户
|
||||
2025-07-04,支出,🏷️ 未分类支出,羽琦返佣,2220,USDT,未知账户
|
||||
2025-07-03,支出,🏷️ 服务器/技术,龙腾集团(15077),12397,USDT,未知账户
|
||||
2025-07-03,支出,🏷️ 未分类支出,乐乐返佣,83,USDT,未知账户
|
||||
2025-07-03,支出,🏷️ 未分类支出,合鑫返佣,420,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 佣金/返佣,服务器(58u+128u),186,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 佣金/返佣,google 翻译,1032,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 佣金/返佣,openRouter 105u + 50u,155,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 佣金/返佣,Claude code 250u + 5% 手续费,262.5,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 佣金/返佣,cursor 40u + 13.8u + 3.7u,57.5,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 佣金/返佣,iphone16 pro max 工作机,1676,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 未分类支出,无名返佣,1790,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 未分类支出,恋哥返佣,366,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 未分类支出,OAC00返佣,350,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 未分类支出,天龙返佣,6293,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 未分类支出,绿豆汤返佣,723,USDT,未知账户
|
||||
2025-07-01,支出,🏷️ 分红,皇雨返佣,656,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 佣金/返佣,chatgpt 1个23u开5个,115,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 分红,香缇卡工资,1122,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 未分类支出,Jack帅哥佣金,947,USDT,未知账户
|
||||
2025-06-30,支出,🏷️ 未分类支出,杰夫返佣,1095,USDT,未知账户
|
||||
2025-06-23,支出,🏷️ 佣金/返佣,服务器续费(人民币1200),167.59,USDT,未知账户
|
||||
2025-06-21,支出,🏷️ 其他支出,达摩团队退,390,USDT,未知账户
|
||||
2025-06-21,支出,🏷️ 佣金/返佣,服务器续费,5000,USDT,未知账户
|
||||
2025-06-16,支出,🏷️ 未分类支出,胖兔返佣,1764,USDT,未知账户
|
||||
2025-06-16,支出,🏷️ 未分类支出,羽琦返佣,1580,USDT,未知账户
|
||||
2025-06-09,支出,🏷️ 分红,皇雨买号测试软件,102,USDT,未知账户
|
||||
2025-06-09,支出,🏷️ 分红,香缇卡买号测试软件,65,USDT,未知账户
|
||||
2025-06-09,支出,🏷️ 未分类支出,阿宏返佣,1846,USDT,未知账户
|
||||
2025-06-08,支出,🏷️ 服务器/技术,测试买控的,50,USDT,未知账户
|
||||
2025-06-08,支出,🏷️ 未分类支出,金返佣,220,USDT,未知账户
|
||||
2025-06-07,支出,🏷️ 分红,皇雨工资,11300,USDT,未知账户
|
||||
2025-06-07,支出,🏷️ 分红,代理ip小哥工资,989,USDT,未知账户
|
||||
2025-06-07,支出,🏷️ 分红,SY工资,4738,USDT,未知账户
|
||||
2025-06-07,支出,🏷️ 服务器/技术,泰国房租和换现金,9290,USDT,未知账户
|
||||
2025-06-05,支出,🏷️ 佣金/返佣,openai,1250,USDT,未知账户
|
||||
2025-06-04,支出,🏷️ 分红,皇雨返佣,850,USDT,未知账户
|
||||
2025-06-04,支出,🏷️ 未分类支出,4月 小树返佣,36,USDT,未知账户
|
||||
2025-06-03,支出,🏷️ 服务器/技术,龙腾集团(计14295u)+3400=17695,14095,USDT,未知账户
|
||||
2025-06-03,支出,🏷️ 佣金/返佣,开通企业版chatgpt,1652,USDT,未知账户
|
||||
2025-06-03,支出,💰 未分类收入,蚊子分红,15000,USDT,未知账户
|
||||
2025-06-03,支出,💰 未分类收入,阿寒分红,15000,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 分红,香缇卡工资,1101,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 佣金/返佣,google翻译接口的费用 (取整),1013,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 佣金/返佣,openRouter 充值,100,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 佣金/返佣,服务器费用,188,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 佣金/返佣,cursor费用,64,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,杰夫返佣,330,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,天龙返佣,8542,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,无名返佣,2103,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,恋哥返佣,480,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,OAC00返佣,586,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,乐乐返佣,291,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,国哥返佣,30,USDT,未知账户
|
||||
2025-06-02,支出,🏷️ 未分类支出,合鑫返佣,360,USDT,未知账户
|
||||
2025-06-01,支出,🏷️ 未分类支出,绿豆汤返佣,973,USDT,未知账户
|
||||
2025-05-31,支出,🏷️ 其他支出,Jack帅哥余额退,619,USDT,未知账户
|
||||
2025-05-31,支出,🏷️ 未分类支出,Jack帅哥返佣,1033,USDT,未知账户
|
||||
2025-05-24,支出,🏷️ 服务器/技术,投资款,20000,USDT,未知账户
|
||||
2025-05-21,支出,🏷️ 服务器/技术,小树保关,165,USDT,未知账户
|
||||
2025-05-21,支出,🏷️ 佣金/返佣,服务器续费(人民币1200),167,USDT,未知账户
|
||||
2025-05-17,支出,🏷️ 佣金/返佣,硅基流动 ai 重排序接口充值 2000人民币,278,USDT,未知账户
|
||||
2025-05-13,支出,🏷️ 未分类支出,羽琦返佣(3月 1395u+4月 1260u),2655,USDT,未知账户
|
||||
2025-05-13,支出,🏷️ 未分类支出,金返佣,200,USDT,未知账户
|
||||
2025-05-12,支出,🏷️ 佣金/返佣,服务器续费,5000,USDT,未知账户
|
||||
2025-05-09,支出,🏷️ 借款/转账,换泰珠,3058,USDT,未知账户
|
||||
2025-05-08,支出,🏷️ 分红,天天返佣,2191.5,USDT,未知账户
|
||||
2025-05-08,支出,🏷️ 分红,天天投流报销,1488,USDT,未知账户
|
||||
2025-05-08,支出,🏷️ 未分类支出,胖兔返佣,2062.5,USDT,未知账户
|
||||
2025-05-05,支出,🏷️ 其他支出,KM 退款,71,USDT,未知账户
|
||||
2025-05-04,支出,🏷️ 未分类支出,合鑫返佣,435,USDT,未知账户
|
||||
2025-05-04,支出,🏷️ 未分类支出,乐乐返佣,177,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 工资,cloudflare 防火墙,165.5,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 佣金/返佣,chatgpt pro,200,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 佣金/返佣,cursor,320,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 佣金/返佣,openrouter,121,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 佣金/返佣,bolt.new,500,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 佣金/返佣,openai,911.8,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 工资,tg会员,36,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 分红,chatwoot客服,19,USDT,未知账户
|
||||
2025-05-03,支出,🏷️ 工资,uizard,19,USDT,未知账户
|
||||
2025-05-03,支出,💰 未分类收入,蚊子分红,20000,USDT,未知账户
|
||||
2025-05-03,支出,💰 未分类收入,阿寒分红,20000,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 服务器/技术,租办公室,500,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,SY工资,4127,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,皇雨工资,11005,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,代理ip小哥工资,963,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 佣金/返佣,google 翻译接口,903,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,香缇卡工资,1101,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,绿豆汤返佣,540,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,国哥返佣,60,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,杰夫返佣,778,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,恋哥返佣,273,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,天龙返佣,10531,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,无名返佣,1819,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,OAC00返佣,744,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 分红,皇雨返佣,91,USDT,未知账户
|
||||
2025-05-02,支出,🏷️ 未分类支出,杰夫返佣,778,USDT,未知账户
|
||||
2025-05-01,支出,🏷️ 未分类支出,Jack帅哥返佣,1111,USDT,未知账户
|
||||
2025-04-30,支出,🏷️ 借款/转账,买trx 2000,531,USDT,未知账户
|
||||
2025-04-30,支出,🏷️ 固定资产,打流量,500,USDT,未知账户
|
||||
2025-04-28,支出,🏷️ 服务器/技术,泰国生活换泰铢,6033,USDT,未知账户
|
||||
2025-04-25,支出,🏷️ 服务器/技术,做 whatsapp 云控测试的,110,USDT,未知账户
|
||||
2025-04-25,支出,🏷️ 其他支出,啊Q(meidusha001)退款,150,USDT,未知账户
|
||||
2025-04-22,支出,🏷️ 佣金/返佣,服务器续费(人民币1200),165,USDT,未知账户
|
||||
2025-04-20,支出,🏷️ 分红,阿寒 皇雨 碧桂园 天天 4个人会员续费,120,USDT,未知账户
|
||||
2025-04-19,支出,🏷️ 其他支出,致胜退款,184,USDT,未知账户
|
||||
2025-04-14,支出,🏷️ 未分类支出,金返佣,267,USDT,未知账户
|
||||
2025-04-13,支出,🏷️ 分红,皇雨返佣,1080,USDT,未知账户
|
||||
2025-04-11,支出,🏷️ 佣金/返佣,服务器续费和防护扣款,5000,USDT,未知账户
|
||||
2025-04-11,支出,🏷️ 服务器/技术,换美金,448,USDT,未知账户
|
||||
2025-04-10,支出,🏷️ 服务器/技术,保关,360,USDT,未知账户
|
||||
2025-04-07,支出,🏷️ 服务器/技术,泰国换泰铢,5874,USDT,未知账户
|
||||
2025-04-07,支出,🏷️ 工资,esim plus 手机号续费预充值,204,USDT,未知账户
|
||||
2025-04-07,支出,🏷️ 未分类支出,恋哥返佣,293,USDT,未知账户
|
||||
2025-04-03,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-04-03,支出,🏷️ 分红,香缇卡工资,1101,USDT,未知账户
|
||||
2025-04-03,支出,🏷️ 未分类支出,杰夫返佣,390,USDT,未知账户
|
||||
2025-04-03,支出,🏷️ 分红,天天返佣,1492,USDT,未知账户
|
||||
2025-04-03,支出,💰 未分类收入,紫气东来充值(1899)5%分红,95,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 退款,路由器费用(硬件+物流),53,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 佣金/返佣,google翻译接口的费用,973,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 佣金/返佣,openRouter 充值,106,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 佣金/返佣,2个服务器费用,188,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,SY工资,4110,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,皇雨工资,10959,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,代理ip小哥工资,959,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 服务器/技术,租办公室,500,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 借款/转账,买trx 2000,510,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 分红,龙腾集团费用转给天天,16867,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,Jack帅哥返佣,725,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,天龙返佣,11398,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,闲聊返佣,420,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,OAC00返佣,229,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,无名返佣,1787,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,绿豆汤返佣,371,USDT,未知账户
|
||||
2025-04-01,支出,💰 未分类收入,蚊子分红,5520,USDT,未知账户
|
||||
2025-04-01,支出,💰 未分类收入,阿寒分红,5520,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,合鑫返佣,330,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,A Feng 返佣,100,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,胖兔返佣,2659,USDT,未知账户
|
||||
2025-04-01,支出,🏷️ 未分类支出,乐乐返佣,151,USDT,未知账户
|
||||
2025-03-29,支出,🏷️ 借款/转账,机器人续费,19,USDT,未知账户
|
||||
2025-03-29,支出,💰 未分类收入,蚊子分红,10000,USDT,未知账户
|
||||
2025-03-29,支出,💰 未分类收入,阿寒分红,10000,USDT,未知账户
|
||||
2025-03-28,支出,🏷️ 佣金/返佣,阿里云主服务器,2100,USDT,未知账户
|
||||
2025-03-28,支出,🏷️ 分红,皇雨买 007 测试系统,110,USDT,未知账户
|
||||
2025-03-23,支出,🏷️ 佣金/返佣,服务器续费(人民币1200),165.28,USDT,未知账户
|
||||
2025-03-18,支出,🏷️ 其他支出,老莫8688 退款,44,USDT,未知账户
|
||||
2025-03-18,支出,🏷️ 其他支出,月入10w美金 退款,480,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 佣金/返佣,11月 open Ai 接口费用,1500,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 佣金/返佣,2月 open Ai 接口费用,1163,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 佣金/返佣,3月 open Ai 接口费用,713,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 未分类支出,乐乐返佣,120,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 分红,天天紫气东来分红5%,114,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 未分类支出,1月阿宏返佣,1213,USDT,未知账户
|
||||
2025-03-12,支出,🏷️ 未分类支出,2月阿宏返佣,480,USDT,未知账户
|
||||
2025-03-08,支出,🏷️ 未分类支出,金返佣,200,USDT,未知账户
|
||||
2025-03-06,支出,🏷️ 未分类支出,合鑫返佣,315,USDT,未知账户
|
||||
2025-03-04,支出,🏷️ 分红,龙腾集团费用转给天天,16321,USDT,未知账户
|
||||
2025-03-03,支出,🏷️ 未分类支出,羽琦返佣,783,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 佣金/返佣,服务器两台,188,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 佣金/返佣,google 翻译api的 费用 截止到 2025.02.28,1074,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 佣金/返佣,openrouter 充值,60,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 佣金/返佣,deepseek,52,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 服务器/技术,租办公室,500,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,香缇卡工资,1102,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,助理OAC工资,786,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,SY工资,2032,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,皇雨工资,8517,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 分红,代理ip小哥工资,746,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 佣金/返佣,广州技术,733,USDT,未知账户
|
||||
2025-03-02,支出,🏷️ 未分类支出,绿豆汤返佣,700,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 借款/转账,买trx 2000,502,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 工资,小红卡续费余额不足,400,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,闲聊返佣,330,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,国哥返佣,150,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,胖兔返佣,3129,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,OAC00返佣,157,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,无名返佣,1146,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,辞辞返佣,181,USDT,未知账户
|
||||
2025-03-01,支出,🏷️ 未分类支出,恋哥返佣,440,USDT,未知账户
|
||||
2025-03-01,支出,💰 未分类收入,蚊子分红,30000,USDT,未知账户
|
||||
2025-03-01,支出,💰 未分类收入,阿寒分红,30000,USDT,未知账户
|
||||
2025-02-28,支出,🏷️ 借款/转账,A Feng,110,USDT,未知账户
|
||||
2025-02-28,支出,🏷️ 未分类支出,Jack帅哥返佣,1276,USDT,未知账户
|
||||
2025-02-28,支出,🏷️ 未分类支出,天龙返佣,9757,USDT,未知账户
|
||||
2025-02-26,支出,🏷️ 工资,小红卡买虚拟卡,10,USDT,未知账户
|
||||
2025-02-26,支出,🏷️ 佣金/返佣,阿里云主服务器,1980,USDT,未知账户
|
||||
2025-02-23,支出,🏷️ 佣金/返佣,服务器续费,165,USDT,未知账户
|
||||
2025-02-18,支出,🏷️ 其他支出,一路发 多充退款,2500,USDT,未知账户
|
||||
2025-02-15,支出,🏷️ 服务器/技术,转给阿寒在泰国租房等等,8982,USDT,未知账户
|
||||
2025-02-15,支出,🏷️ 未分类支出,A Feng 1月返佣,158,USDT,未知账户
|
||||
2025-02-15,支出,🏷️ 其他支出,众彩公司退款,52,USDT,未知账户
|
||||
2025-02-11,支出,🏷️ 佣金/返佣,1月open Ai费用,782,USDT,未知账户
|
||||
2025-02-11,支出,🏷️ 未分类支出,乐乐1月返佣,200,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 佣金/返佣,10月服务器续费,874.47,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 佣金/返佣,11月服务器续费,923.42,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 佣金/返佣,12月服务器续费,936.34,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 佣金/返佣,2025年1月服务器续费,956,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 分红,1月chatwoot 客服,57,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 分红,2月chatwoot 客服,57,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 佣金/返佣,bolt.new ai 写代码套餐开通,181.7,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 退款,三星硬盘(1579rmb),215.4,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 退款,西部数据企业级氦气硬盘(12732rmb),1737,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 退款,绿联DXP8800Pro云硬盘(7039.72rmb),960.4,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 分红,香缇卡笔记本电脑(9436.49rmb),1287.4,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 佣金/返佣,双路渲染服务器40核(10499rmb),1432.3,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 借款/转账,A Feng补12月漏,50,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 借款/转账,辞辞补1月漏,203,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 未分类支出,羽琦返佣,770,USDT,未知账户
|
||||
2025-02-10,支出,🏷️ 未分类支出,绿豆汤返佣,520,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 分红,天天开工红包,257,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 分红,碧桂园开工红包,257,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 分红,香缇卡开工红包,257,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 分红,财务amy开工红包,257,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 服务器/技术,助理oac开工红包,257,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 分红,代理小哥开工红包,257,USDT,未知账户
|
||||
2025-02-07,支出,🏷️ 分红,皇雨开工红包,529,USDT,未知账户
|
||||
2025-02-06,支出,🏷️ 佣金/返佣,服务器临时配置升级 充值,1000,USDT,未知账户
|
||||
2025-02-05,支出,🏷️ 未分类支出,合鑫返佣,600,USDT,未知账户
|
||||
2025-02-04,支出,🏷️ 其他支出,众发退款,900,USDT,未知账户
|
||||
2025-02-04,支出,🏷️ 未分类支出,无名返佣,752,USDT,未知账户
|
||||
2025-02-04,支出,🏷️ 未分类支出,闲聊返佣,763,USDT,未知账户
|
||||
2025-02-03,支出,🏷️ 未分类支出,辞辞返佣,1033,USDT,未知账户
|
||||
2025-02-03,支出,🏷️ 分红,天天紫气东来分红5%,186,USDT,未知账户
|
||||
2025-02-02,支出,🏷️ 服务器/技术,龙腾集团,4202,USDT,未知账户
|
||||
2025-02-02,支出,🏷️ 未分类支出,胖兔返佣,2128,USDT,未知账户
|
||||
2025-02-02,支出,🏷️ 其他支出,启运退款,88,USDT,未知账户
|
||||
2025-02-01,支出,🏷️ 未分类支出,天龙返佣,5632,USDT,未知账户
|
||||
2025-01-28,支出,🏷️ 未分类支出,Jack帅哥返佣,723,USDT,未知账户
|
||||
2025-01-25,支出,🏷️ 佣金/返佣,xiaohai0000 鸿图,20,USDT,未知账户
|
||||
2025-01-24,支出,🏷️ 分红,amy买香港信用卡虚拟卡,10,USDT,未知账户
|
||||
2025-01-24,支出,🏷️ 服务器/技术,蚊子 阿寒在泰国两人生活费,2497,USDT,未知账户
|
||||
2025-01-24,支出,💰 未分类收入,蚊子分红,10000,USDT,未知账户
|
||||
2025-01-24,支出,💰 未分类收入,阿寒分红,10000,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,皇雨工资5517 年终奖5517,11034,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,代理ip 小哥工资965 年终奖965,1930,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,天天工资1500年终奖750,2250,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,碧桂园工资1000 年终奖500,1500,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,香缇卡工资1103年终奖552,1655,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,财务amy 1500年终奖750,2250,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 分红,助理OAC工资1000年终奖500,1500,USDT,未知账户
|
||||
2025-01-21,支出,🏷️ 佣金/返佣,服务器续费,165.97,USDT,未知账户
|
||||
2025-01-18,支出,🏷️ 借款/转账,买trx 2000,527.904,USDT,未知账户
|
||||
2025-01-15,支出,🏷️ 分红,转给香缇卡买账户备用金,300,USDT,未知账户
|
||||
2025-01-14,支出,🏷️ 分红,香缇卡买小红卡,50,USDT,未知账户
|
||||
2025-01-14,支出,🏷️ 工资,OAC买小红卡实体卡,100,USDT,未知账户
|
||||
2025-01-13,支出,🏷️ 分红,amy买小红卡,50,USDT,未知账户
|
||||
2025-01-11,支出,🏷️ 分红,小哥两个月开发费用,1000,USDT,未知账户
|
||||
2025-01-11,支出,🏷️ 借款/转账,老表对接,300,USDT,未知账户
|
||||
2025-01-09,支出,🏷️ 分红,转给香缇卡买账户备用金,200,USDT,未知账户
|
||||
2025-01-08,支出,🏷️ 退款,泰国买车,39358.6,USDT,未知账户
|
||||
2025-01-03,支出,🏷️ 分红,转给天天,9000,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 借款/转账,合鑫,570,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 借款/转账,金鑫,26,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 佣金/返佣,xiaohai0000 鸿图,380,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 借款/转账,阿宏11月 1147Uu+12月1892,3039,USDT,未知账户
|
||||
2025-01-01,支出,💰 未分类收入,蚊子分红,10000,USDT,未知账户
|
||||
2025-01-01,支出,💰 未分类收入,阿寒分红,10000,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,A Feng返佣,180,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,绿豆汤返佣,500,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,天龙返佣,8795,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,胖兔返佣,2622.7,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,无名返佣,800,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,闲聊返佣,1043,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 分红,天天散户分红,198,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,乐乐返佣,414,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,知青返佣,135,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,恋哥返佣,526.7,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,长青返佣,77,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,国哥返佣,150,USDT,未知账户
|
||||
2025-01-01,支出,🏷️ 未分类支出,羽琦返佣,419,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 分红,皇雨工资 代理ip小哥 服务器续费128,6638,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 分红,香缇卡工资,1000,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2024-12-31,支出,💰 未分类收入,蚊子分红,20000,USDT,未知账户
|
||||
2024-12-31,支出,💰 未分类收入,阿寒分红,20000,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 未分类支出,Jack帅哥返佣,842,USDT,未知账户
|
||||
2024-12-31,支出,🏷️ 其他支出,金鑫 退款,800,USDT,未知账户
|
||||
2024-12-30,支出,🏷️ 其他支出,金鑫退款,800,USDT,未知账户
|
||||
2024-12-21,支出,🏷️ 服务器/技术,转龙腾,3000,USDT,未知账户
|
||||
2024-12-21,支出,🏷️ 佣金/返佣,服务器续费,164,USDT,未知账户
|
||||
2024-12-19,支出,🏷️ 分红,转给天天,7000,USDT,未知账户
|
||||
2024-12-15,支出,💰 未分类收入,蚊子分红,20000,USDT,未知账户
|
||||
2024-12-15,支出,💰 未分类收入,阿寒分红,20000,USDT,未知账户
|
||||
2024-12-06,支出,🏷️ 分红,转给天天,5000,USDT,未知账户
|
||||
2024-12-06,支出,🏷️ 退款,硬盘费用,1769,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,香缇卡工资,1000,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,财务Amy工资,1500,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,助理OAC工资,1000,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 服务器/技术,龙腾集团鑫晟公司,6102,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,皇雨5579 代理ip小哥976,6555,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,龙腾集团转给天天,6073,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 佣金/返佣,服务器续费专用小红卡,50,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 分红,天天散户,172,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 佣金/返佣,服务器续费2个月,256,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 佣金/返佣,流量测试服务器,500,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 退款,展示屏,805,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 佣金/返佣,openai 12 月份接口费用,1768,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,天龙返佣,9738,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,绿豆汤返佣,500,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,貔貅返佣,300,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,恋哥返佣(713+367补10月),1080,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,无名返佣(1107+635),1742,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,知青返佣,768,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,乐乐返佣,494,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,合鑫返佣,615,USDT,未知账户
|
||||
2024-12-01,支出,🏷️ 未分类支出,胖兔返佣,2601,USDT,未知账户
|
||||
2024-11-30,支出,🏷️ 未分类支出,Jack帅哥返佣,649,USDT,未知账户
|
||||
2024-11-29,支出,🏷️ 工资,开飞机会员,38,USDT,未知账户
|
||||
2024-11-16,支出,🏷️ 借款/转账,3000 trx自动归集的手续费购买609.444 usdt,609.444,USDT,未知账户
|
||||
2024-11-15,支出,🏷️ 佣金/返佣,oac 40 ai机器人40,80,USDT,未知账户
|
||||
2024-11-10,支出,🏷️ 借款/转账,买trx,85,USDT,未知账户
|
||||
2024-11-10,支出,🏷️ 佣金/返佣,人工智能接口费用,1590,USDT,未知账户
|
||||
2024-11-10,支出,🏷️ 佣金/返佣,服务器费用,1326,USDT,未知账户
|
||||
2024-11-10,支出,🏷️ 未分类支出,阿宏佣金,2505,USDT,未知账户
|
||||
2024-11-10,支出,🏷️ 借款/转账,买自动到账地址(用于质押获得手续费),2000,USDT,未知账户
|
||||
2024-11-07,支出,🏷️ 服务器/技术,投资款项(6006+32934),38940,USDT,未知账户
|
||||
2024-11-05,支出,🏷️ 分红,转给啊寒3000美金用于天天买电脑,3000,USDT,未知账户
|
||||
2024-11-05,支出,🏷️ 分红,给财务买苹果电脑,1499,USDT,未知账户
|
||||
2024-11-05,支出,🏷️ 退款,蚊子工作苹果电脑,8433,USDT,未知账户
|
||||
2024-11-04,支出,🏷️ 未分类支出,大白菜佣金,1290.5,USDT,未知账户
|
||||
2024-11-04,支出,🏷️ 未分类支出,胖兔佣金,2942,USDT,未知账户
|
||||
2024-11-04,支出,🏷️ 未分类支出,恋哥佣金,660,USDT,未知账户
|
||||
2024-11-02,支出,🏷️ 未分类支出,貔貅佣金,900,USDT,未知账户
|
||||
2024-11-01,支出,🏷️ 借款/转账,买trx,700,USDT,未知账户
|
||||
2024-11-01,支出,🏷️ 分红,龙腾集团转给天天,9797,USDT,未知账户
|
||||
2024-11-01,支出,🏷️ 分红,天天虚拟信用卡,50,USDT,未知账户
|
||||
2024-11-01,支出,🏷️ 未分类支出,乐乐佣金,225,USDT,未知账户
|
||||
2024-11-01,支出,🏷️ 未分类支出,国哥佣金,450,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 分红,代理ip小哥,1000,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 分红,黄雨工资,5673,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 分红,财务,1500,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 分红,天天,1500,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 分红,碧桂园,1000,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 借款/转账,卡卡提,500,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 未分类支出,天龙佣金,9292,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 未分类支出,核心佣金,2349,USDT,未知账户
|
||||
2024-10-31,支出,💰 未分类收入,蚊子分红,10000,USDT,未知账户
|
||||
2024-10-31,支出,💰 未分类收入,阿寒分红,10000,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 未分类支出,知青佣金,818,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 未分类支出,合鑫佣金,420,USDT,未知账户
|
||||
2024-10-31,支出,🏷️ 分红,天天 紫气东来散户分红5%,146,USDT,未知账户
|
||||
2024-10-30,支出,🏷️ 未分类支出,Jack帅哥佣金,700,USDT,未知账户
|
||||
2024-10-28,支出,💰 未分类收入,阿寒分红,1000,USDT,未知账户
|
||||
2024-10-28,支出,💰 未分类收入,蚊子分红,1000,USDT,未知账户
|
||||
2024-10-25,支出,🏷️ 其他支出,七月退,100,USDT,未知账户
|
||||
2024-10-22,支出,🏷️ 服务器/技术,接待,3000,USDT,未知账户
|
||||
2024-10-19,支出,🏷️ 工资,2t u盘两个。每个203u,406,USDT,未知账户
|
||||
2024-10-19,支出,💰 未分类收入,阿寒分红,3000,USDT,未知账户
|
||||
2024-10-19,支出,💰 未分类收入,蚊子分红,3000,USDT,未知账户
|
||||
2024-10-16,支出,🏷️ 工资,007购买,125,USDT,未知账户
|
||||
2024-10-11,支出,🏷️ 未分类支出,大卫佣金,1875,USDT,未知账户
|
||||
2024-10-09,支出,🏷️ 借款/转账,购买 trx质押产生能量,2000,USDT,未知账户
|
||||
2024-10-09,支出,🏷️ 固定资产,汉城广告费,1000,USDT,未知账户
|
||||
2024-10-06,支出,🏷️ 其他支出,大秦退费,310,USDT,未知账户
|
||||
2024-10-05,支出,🏷️ 佣金/返佣,技术公司鸿泰,1768,USDT,未知账户
|
||||
2024-10-05,支出,🏷️ 佣金/返佣,ChatGPT自建服务器半年付,479.88,USDT,未知账户
|
||||
2024-10-05,支出,🏷️ 借款/转账,交友五个阶段提示词编写和优化外包,700,USDT,未知账户
|
||||
2024-10-04,支出,🏷️ 佣金/返佣,ChatGPT接口费用,869,USDT,未知账户
|
||||
2024-10-04,支出,🏷️ 佣金/返佣,备用OpenAI预充值,200,USDT,未知账户
|
||||
2024-10-04,支出,🏷️ 佣金/返佣,备用转发接口充值,100,USDT,未知账户
|
||||
2024-10-04,支出,🏷️ 佣金/返佣,Kt主服务器。分流服务器。自动到账服务器。oss服务器,1143,USDT,未知账户
|
||||
2024-10-04,支出,🏷️ 未分类支出,胖兔佣金,2821,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 分红,皇工资35000rmb,5022,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 分红,碧桂园,1000,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 分红,amy,1500,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 分红,代理ip小哥,1000,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 借款/转账,截图,100,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 佣金/返佣,技术公司,3815,USDT,未知账户
|
||||
2024-10-03,支出,🏷️ 未分类支出,乐乐佣金,483,USDT,未知账户
|
||||
2024-10-03,支出,💰 未分类收入,蚊子分红,593,USDT,未知账户
|
||||
2024-10-02,支出,🏷️ 未分类支出,长青佣金,240,USDT,未知账户
|
||||
2024-10-02,支出,🏷️ 未分类支出,合鑫佣金,450,USDT,未知账户
|
||||
2024-10-01,支出,🏷️ 未分类支出,天龙佣金,5815,USDT,未知账户
|
||||
2024-10-01,支出,🏷️ 未分类支出,核心佣金,2413,USDT,未知账户
|
||||
2024-10-01,支出,🏷️ 未分类支出,三七公司佣金,189,USDT,未知账户
|
||||
2024-09-26,支出,🏷️ 分红,天天控天费用,593,USDT,未知账户
|
||||
2024-09-26,支出,🏷️ 借款/转账,自动到账购买2000trx,329,USDT,未知账户
|
||||
2024-09-25,支出,🏷️ 固定资产,亚太地推开支,1550,USDT,未知账户
|
||||
2024-09-24,支出,🏷️ 固定资产,亚太小助手投放,1500,USDT,未知账户
|
||||
2024-09-22,支出,🏷️ 工资,processon流程图终身会员,185,USDT,未知账户
|
||||
2024-09-21,支出,🏷️ 佣金/返佣,服务器续费,1210,USDT,未知账户
|
||||
2024-09-20,支出,🏷️ 借款/转账,截图制作,338,USDT,未知账户
|
||||
2024-09-19,支出,🏷️ 未分类支出,Jack帅哥佣金,276,USDT,未知账户
|
||||
2024-09-18,支出,🏷️ 固定资产,广告费用,450,USDT,未知账户
|
||||
2024-09-18,支出,🏷️ 工资,开飞机会员,38,USDT,未知账户
|
||||
2024-09-13,支出,🏷️ 服务器/技术,阿鹏借出35000rmb,4943.5,USDT,未知账户
|
||||
2024-09-13,支出,🏷️ 退款,rog电脑购买,5659,USDT,未知账户
|
||||
2024-09-11,支出,🏷️ 未分类支出,羽琦佣金,154,USDT,未知账户
|
||||
2024-09-10,支出,🏷️ 固定资产,广告费,1000,USDT,未知账户
|
||||
2024-09-03,支出,🏷️ 其他支出,大秦退费,300,USDT,未知账户
|
||||
2024-09-03,支出,🏷️ 工资,飞机会员续费,35,USDT,未知账户
|
||||
2024-09-01,支出,🏷️ 未分类支出,老外 大卫佣金,2250,USDT,未知账户
|
||||
2024-09-01,支出,🏷️ 未分类支出,天龙佣金,6943.9,USDT,未知账户
|
||||
2024-09-01,支出,🏷️ 未分类支出,大卫5月的佣金,900,USDT,未知账户
|
||||
2024-08-31,支出,🏷️ 分红,天天控天费用,2207,USDT,未知账户
|
||||
2024-08-31,支出,🏷️ 佣金/返佣,nat转发包年,280,USDT,未知账户
|
||||
2024-08-31,支出,🏷️ 未分类支出,长青佣金,377,USDT,未知账户
|
||||
2024-08-31,支出,💰 未分类收入,蚊子同比例分红,2207,USDT,未知账户
|
||||
2024-08-31,支出,🏷️ 未分类支出,Jack帅哥佣金,456,USDT,未知账户
|
||||
2024-08-31,支出,🏷️ 未分类支出,乐乐佣金,440,USDT,未知账户
|
||||
2024-08-30,支出,🏷️ 佣金/返佣,宝塔会员,203,USDT,未知账户
|
||||
2024-08-30,支出,🏷️ 未分类支出,核心佣金,3103,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 退款,买车定金,2000,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 退款,买车尾款,16562,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 佣金/返佣,技术信用卡,50,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 分红,天天工资,1500,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 分红,碧桂园工资,1000,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 分红,皇工资,5000,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 分红,财务客服,1500,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 分红,代理ip技术,1000,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 佣金/返佣,人工智能接口,700,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 借款/转账,处理员工一起出,10000,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 借款/转账,外星人一起出4.8w,6571,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 服务器/技术,啊杰借的10000,1404,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 固定资产,群发广告,300,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 借款/转账,网络攻击买服务,800,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 未分类支出,老练几个月佣金,1586,USDT,未知账户
|
||||
2024-08-27,支出,🏷️ 未分类支出,胖兔佣金,3597,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 借款/转账,攻击,60,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 佣金/返佣,其他翻译测试,58,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 佣金/返佣,佣金,80,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 佣金/返佣,乐乐佣金,343,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 佣金/返佣,佣金,823.5,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 退款,退款,170.71,USDT,未知账户
|
||||
2024-08-07,支出,🏷️ 退款,退款,70,USDT,未知账户
|
||||
2024-08-03,支出,🏷️ 分红,啊寒分红,3000,USDT,未知账户
|
||||
2024-08-03,支出,🏷️ 分红,蚊子分红,3000,USDT,未知账户
|
||||
2024-08-03,支出,🏷️ 佣金/返佣,长青佣金,326,USDT,未知账户
|
||||
2024-08-03,支出,🏷️ 佣金/返佣,阿宏佣金,311,USDT,未知账户
|
||||
2024-06-15,支出,未分类,6月测试交易,50,USDT,未知账户
|
||||
2024-06-15,支出,未分类,6月测试交易,50,USDT,未知账户
|
||||
2024-06-15,支出,未分类,6月测试交易,50,USDT,未知账户
|
||||
2024-06-15,支出,未分类,6月测试交易,50,USDT,未知账户
|
||||
2025-10-30,支出,🏷️ 未分类,买飞机号,213,USDT,乐乐用
|
||||
2025-10-31,支出,🏷️ 未分类,公司的外网专线费用,211,USDT,cp
|
||||
2025-10-31,支出,🏷️ 未分类,强耀科技退款,19,USDT,未知账户
|
||||
2025-11-01,支出,🏷️ 未分类,阿金公司退款,186,USDT,未知账户
|
||||
2025-11-01,支出,🏷️ 未分类,小白工资,1000,USDT,未知账户
|
||||
2025-11-01,支出,🏷️ 未分类,cp工资,1000,USDT,未知账户
|
||||
2025-11-01,支出,🏷️ 未分类,菲菲工资,2344,USDT,16500按7.04
|
||||
2025-11-02,支出,🏷️ 未分类,绿豆汤返佣,99,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,OAC返佣,972,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,天龙返佣,10556,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,Jack帅哥返佣,506,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,无名返佣,1655,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,方向返佣,89,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,阿泰会员,30,USDT,天天
|
||||
2025-11-02,支出,🏷️ 未分类,香缇卡会员,30,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,虚拟卡,100,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,代理ip,15,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,服务器,540,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,域名,15,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,宝金出海会员,30,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,网盘会员,42.5,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,水电宽带,65,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,硬盘,68,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,cpcc会员,253.5,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,香缇卡流量卡,153,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,杰夫返佣,1055,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,天天工资,1500,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,碧桂园工资,1000,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,香缇卡工资,1146,USDT,未知账户
|
||||
2025-11-02,支出,🏷️ 未分类,龙腾集团,7700,USDT,14700扣10月龙腾借7000 鑫晟公司2480未结算
|
||||
2025-11-04,支出,🏷️ 未分类,羽琦返佣,2960,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,皇雨工资,11364,USDT,80000元按7.04
|
||||
2025-11-04,支出,🏷️ 未分类,代理ip小哥工资,994,USDT,7000元按7.04
|
||||
2025-11-04,支出,🏷️ 未分类,SY工资,4761,USDT,(4261+500)30000元按7.04
|
||||
2025-11-04,支出,🏷️ 未分类,财务Amy工资,1500,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,助理OAC工资,1500,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,煮饭阿姨工资,426,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,李涛工资,578,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,胖兔返佣,3414,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,合鑫返佣,105,USDT,未知账户
|
||||
2025-11-04,支出,🏷️ 未分类,恋哥返佣,187,USDT,未知账户
|
||||
2025-11-05,支出,🏷️ 未分类,阿宏返佣,815,USDT,825u (10u换trx)
|
||||
2025-11-05,支出,🏷️ 未分类,666返佣,270,USDT,未知账户
|
||||
|
27
deploy.sh
27
deploy.sh
@@ -69,6 +69,33 @@ sudo docker-compose down || true
|
||||
echo "🚀 构建并启动新容器..."
|
||||
sudo docker-compose up -d --build
|
||||
|
||||
# 等待PostgreSQL就绪
|
||||
echo "⏳ 等待PostgreSQL就绪..."
|
||||
POSTGRES_READY=0
|
||||
for i in {1..10}; do
|
||||
if sudo docker-compose exec -T postgres pg_isready -U kt_financial -d kt_financial > /dev/null 2>&1; then
|
||||
echo "✅ PostgreSQL 已就绪"
|
||||
POSTGRES_READY=1
|
||||
break
|
||||
fi
|
||||
echo " 第${i}次重试..."
|
||||
sleep 3
|
||||
done
|
||||
if [ "$POSTGRES_READY" -ne 1 ]; then
|
||||
echo "❌ PostgreSQL 未在预期时间内就绪"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 导入数据
|
||||
echo "📦 导入财务数据..."
|
||||
sudo docker-compose exec -T kt-financial \
|
||||
bash -lc "pnpm --filter @vben/backend import:data -- --csv /app/data/finance/finance-combined.csv --year 2025"
|
||||
|
||||
# 验证数据条数
|
||||
echo "🔢 检查交易记录条数..."
|
||||
sudo docker-compose exec -T postgres \
|
||||
psql -U kt_financial -d kt_financial -c "SELECT COUNT(*) AS transaction_count FROM finance_transactions;"
|
||||
|
||||
# 清理旧镜像
|
||||
echo "🧹 清理旧镜像..."
|
||||
sudo docker image prune -f
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: kt-financial-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=kt_financial
|
||||
- POSTGRES_USER=kt_financial
|
||||
- POSTGRES_PASSWORD=kt_financial_pwd
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U kt_financial -d kt_financial']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
networks:
|
||||
- kt-network
|
||||
|
||||
kt-financial:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: kt-financial-system
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- TZ=Asia/Shanghai
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=kt_financial
|
||||
- POSTGRES_USER=kt_financial
|
||||
- POSTGRES_PASSWORD=kt_financial_pwd
|
||||
volumes:
|
||||
- ./logs:/var/log
|
||||
- ./storage/backend:/app/apps/backend/storage
|
||||
- ./data:/app/data:ro
|
||||
networks:
|
||||
- kt-network
|
||||
|
||||
networks:
|
||||
kt-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
||||
144
pnpm-lock.yaml
generated
144
pnpm-lock.yaml
generated
@@ -631,15 +631,15 @@ importers:
|
||||
'@faker-js/faker':
|
||||
specifier: 'catalog:'
|
||||
version: 9.9.0
|
||||
better-sqlite3:
|
||||
specifier: 9.5.0
|
||||
version: 9.5.0
|
||||
jsonwebtoken:
|
||||
specifier: 'catalog:'
|
||||
version: 9.0.2
|
||||
nitropack:
|
||||
specifier: 'catalog:'
|
||||
version: 2.12.9(better-sqlite3@9.5.0)
|
||||
pg:
|
||||
specifier: ^8.12.0
|
||||
version: 8.16.3
|
||||
devDependencies:
|
||||
'@types/jsonwebtoken':
|
||||
specifier: 'catalog:'
|
||||
@@ -8247,6 +8247,40 @@ packages:
|
||||
perfect-debounce@2.0.0:
|
||||
resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==}
|
||||
|
||||
pg-cloudflare@1.2.7:
|
||||
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
|
||||
|
||||
pg-connection-string@2.9.1:
|
||||
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
|
||||
|
||||
pg-int8@1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
pg-pool@3.10.1:
|
||||
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-protocol@1.10.3:
|
||||
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
|
||||
|
||||
pg-types@2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg@8.16.3:
|
||||
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pgpass@1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -8758,6 +8792,22 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postgres-bytea@1.0.0:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
preact@10.27.2:
|
||||
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
|
||||
|
||||
@@ -10612,6 +10662,10 @@ packages:
|
||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
@@ -14433,6 +14487,7 @@ snapshots:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.3
|
||||
optional: true
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
@@ -14451,6 +14506,7 @@ snapshots:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
optional: true
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
@@ -14496,6 +14552,7 @@ snapshots:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
optional: true
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
@@ -14666,7 +14723,8 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@1.1.4: {}
|
||||
chownr@1.1.4:
|
||||
optional: true
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
@@ -15186,6 +15244,7 @@ snapshots:
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
optional: true
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
@@ -15427,6 +15486,7 @@ snapshots:
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
enhanced-resolve@5.18.3:
|
||||
dependencies:
|
||||
@@ -15922,7 +15982,8 @@ snapshots:
|
||||
strip-final-newline: 4.0.0
|
||||
yoctocolors: 2.1.2
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
expand-template@2.0.3:
|
||||
optional: true
|
||||
|
||||
expand-tilde@2.0.2:
|
||||
dependencies:
|
||||
@@ -16091,7 +16152,8 @@ snapshots:
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
fs-constants@1.0.0:
|
||||
optional: true
|
||||
|
||||
fs-extra@10.1.0:
|
||||
dependencies:
|
||||
@@ -16215,7 +16277,8 @@ snapshots:
|
||||
meow: 12.1.1
|
||||
split2: 4.2.0
|
||||
|
||||
github-from-package@0.0.0: {}
|
||||
github-from-package@0.0.0:
|
||||
optional: true
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
@@ -17235,7 +17298,8 @@ snapshots:
|
||||
|
||||
mimic-function@5.0.1: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
mimic-response@3.1.0:
|
||||
optional: true
|
||||
|
||||
minimatch@10.0.3:
|
||||
dependencies:
|
||||
@@ -17293,7 +17357,8 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
mkdirp-classic@0.5.3:
|
||||
optional: true
|
||||
|
||||
mkdist@2.4.1(sass@1.93.3)(typescript@5.9.3)(vue-tsc@2.2.10(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)):
|
||||
dependencies:
|
||||
@@ -17374,7 +17439,8 @@ snapshots:
|
||||
|
||||
nanopop@2.4.2: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
napi-build-utils@2.0.0:
|
||||
optional: true
|
||||
|
||||
napi-postinstall@0.3.4: {}
|
||||
|
||||
@@ -17498,6 +17564,7 @@ snapshots:
|
||||
node-abi@3.80.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
optional: true
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
@@ -17806,6 +17873,41 @@ snapshots:
|
||||
|
||||
perfect-debounce@2.0.0: {}
|
||||
|
||||
pg-cloudflare@1.2.7:
|
||||
optional: true
|
||||
|
||||
pg-connection-string@2.9.1: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-pool@3.10.1(pg@8.16.3):
|
||||
dependencies:
|
||||
pg: 8.16.3
|
||||
|
||||
pg-protocol@1.10.3: {}
|
||||
|
||||
pg-types@2.2.0:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.0
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg@8.16.3:
|
||||
dependencies:
|
||||
pg-connection-string: 2.9.1
|
||||
pg-pool: 3.10.1(pg@8.16.3)
|
||||
pg-protocol: 1.10.3
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.2.7
|
||||
|
||||
pgpass@1.0.5:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
@@ -18334,6 +18436,16 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
|
||||
postgres-bytea@1.0.0: {}
|
||||
|
||||
postgres-date@1.0.7: {}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
preact@10.27.2: {}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
@@ -18350,6 +18462,7 @@ snapshots:
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.4
|
||||
tunnel-agent: 0.6.0
|
||||
optional: true
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
@@ -18399,6 +18512,7 @@ snapshots:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
@@ -18492,6 +18606,7 @@ snapshots:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
optional: true
|
||||
|
||||
readable-stream@4.7.0:
|
||||
dependencies:
|
||||
@@ -18897,13 +19012,15 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
simple-concat@1.0.1:
|
||||
optional: true
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
optional: true
|
||||
|
||||
sirv@3.0.2:
|
||||
dependencies:
|
||||
@@ -19362,6 +19479,7 @@ snapshots:
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.3
|
||||
tar-stream: 2.2.0
|
||||
optional: true
|
||||
|
||||
tar-stream@2.2.0:
|
||||
dependencies:
|
||||
@@ -19370,6 +19488,7 @@ snapshots:
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
optional: true
|
||||
|
||||
tar-stream@3.1.7:
|
||||
dependencies:
|
||||
@@ -19493,6 +19612,7 @@ snapshots:
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
optional: true
|
||||
|
||||
turbo-darwin-64@2.6.0:
|
||||
optional: true
|
||||
@@ -20567,6 +20687,8 @@ snapshots:
|
||||
|
||||
xml-name-validator@4.0.0: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
Reference in New Issue
Block a user