feat: migrate backend storage to postgres
This commit is contained in:
@@ -127,6 +127,33 @@ jobs:
|
|||||||
echo "⏳ 等待服务启动..."
|
echo "⏳ 等待服务启动..."
|
||||||
sleep 10
|
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. 检查容器状态
|
# 1. 检查容器状态
|
||||||
echo "📊 容器状态:"
|
echo "📊 容器状态:"
|
||||||
sudo docker-compose ps
|
sudo docker-compose ps
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
- **功能**: Telegram Bot通知系统
|
- **功能**: Telegram Bot通知系统
|
||||||
|
|
||||||
#### 已完成功能:
|
#### 已完成功能:
|
||||||
|
|
||||||
1. ✅ 基础Telegram通知
|
1. ✅ 基础Telegram通知
|
||||||
2. ✅ 频率控制和去重
|
2. ✅ 频率控制和去重
|
||||||
3. ✅ 失败重试机制
|
3. ✅ 失败重试机制
|
||||||
@@ -16,15 +17,38 @@
|
|||||||
5. ✅ 优先级设置
|
5. ✅ 优先级设置
|
||||||
|
|
||||||
#### 配置信息:
|
#### 配置信息:
|
||||||
|
|
||||||
- Bot Token: 已配置
|
- Bot Token: 已配置
|
||||||
- Chat ID: 1102887169
|
- Chat ID: 1102887169
|
||||||
- Bot用户名: @ktcaiwubot
|
- Bot用户名: @ktcaiwubot
|
||||||
|
|
||||||
#### 测试结果:
|
#### 测试结果:
|
||||||
|
|
||||||
- ✅ Telegram消息发送成功
|
- ✅ Telegram消息发送成功
|
||||||
- ✅ API接口已实现
|
- ✅ 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 packages ./packages
|
||||||
COPY internal ./internal
|
COPY internal ./internal
|
||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
|
COPY data ./data
|
||||||
|
|
||||||
# 安装依赖(如果存在lock文件则使用)
|
# 安装依赖(如果存在lock文件则使用)
|
||||||
RUN pnpm install --no-frozen-lockfile
|
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
|
COPY --from=backend-builder /app/apps/backend /app/apps/backend
|
||||||
RUN ln -s /app/apps/backend /app/backend
|
RUN ln -s /app/apps/backend /app/backend
|
||||||
COPY --from=backend-builder /app/node_modules /app/node_modules
|
COPY --from=backend-builder /app/node_modules /app/node_modules
|
||||||
|
COPY --from=backend-builder /app/data /app/data
|
||||||
|
|
||||||
# 创建nginx配置和日志目录
|
# 创建nginx配置和日志目录
|
||||||
RUN mkdir -p /run/nginx && \
|
RUN mkdir -p /run/nginx && \
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const currency = query.currency as string | undefined;
|
const currency = query.currency as string | undefined;
|
||||||
|
|
||||||
let accounts = listAccounts();
|
let accounts = await listAccounts();
|
||||||
|
|
||||||
if (currency) {
|
if (currency) {
|
||||||
accounts = accounts.filter((account) => account.currency === currency);
|
accounts = accounts.filter((account) => account.currency === currency);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const type = query.type as 'expense' | 'income' | undefined;
|
const type = query.type as 'expense' | 'income' | undefined;
|
||||||
|
|
||||||
const categories = fetchCategories({ type });
|
const categories = await fetchCategories({ type });
|
||||||
|
|
||||||
return useResponseSuccess(categories);
|
return useResponseSuccess(categories);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
|
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||||
|
|
||||||
import { readBody } from 'h3';
|
import { readBody } from 'h3';
|
||||||
import {
|
import { createTransaction } from '~/utils/finance-repository';
|
||||||
createTransaction,
|
|
||||||
type TransactionStatus,
|
|
||||||
} from '~/utils/finance-repository';
|
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||||
|
|
||||||
const DEFAULT_CURRENCY = 'CNY';
|
const DEFAULT_CURRENCY = 'CNY';
|
||||||
const DEFAULT_STATUS: TransactionStatus = 'pending';
|
const DEFAULT_STATUS: TransactionStatus = 'pending';
|
||||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||||
'draft',
|
'draft',
|
||||||
'pending',
|
'pending',
|
||||||
'approved',
|
'approved',
|
||||||
'rejected',
|
'rejected',
|
||||||
'paid',
|
'paid',
|
||||||
];
|
]);
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
@@ -33,11 +32,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
const status =
|
const status =
|
||||||
(body.status as TransactionStatus | undefined) ?? DEFAULT_STATUS;
|
(body.status as TransactionStatus | undefined) ?? DEFAULT_STATUS;
|
||||||
|
|
||||||
if (!ALLOWED_STATUSES.includes(status)) {
|
if (!ALLOWED_STATUSES.has(status)) {
|
||||||
return useResponseError('状态值不合法', -1);
|
return useResponseError('状态值不合法', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reimbursement = createTransaction({
|
const reimbursement = await createTransaction({
|
||||||
type,
|
type,
|
||||||
amount,
|
amount,
|
||||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||||
|
|
||||||
import { getRouterParam, readBody } from 'h3';
|
import { getRouterParam, readBody } from 'h3';
|
||||||
import {
|
import {
|
||||||
restoreTransaction,
|
restoreTransaction,
|
||||||
updateTransaction,
|
updateTransaction,
|
||||||
type TransactionStatus,
|
|
||||||
} from '~/utils/finance-repository';
|
} from '~/utils/finance-repository';
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||||
'draft',
|
'draft',
|
||||||
'pending',
|
'pending',
|
||||||
'approved',
|
'approved',
|
||||||
'rejected',
|
'rejected',
|
||||||
'paid',
|
'paid',
|
||||||
];
|
]);
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = Number(getRouterParam(event, 'id'));
|
const id = Number(getRouterParam(event, 'id'));
|
||||||
@@ -23,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
if (body?.isDeleted === false) {
|
if (body?.isDeleted === false) {
|
||||||
const restored = restoreTransaction(id);
|
const restored = await restoreTransaction(id);
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
return useResponseError('报销单不存在', -1);
|
return useResponseError('报销单不存在', -1);
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||||
if (body?.status !== undefined) {
|
if (body?.status !== undefined) {
|
||||||
const status = body.status as TransactionStatus;
|
const status = body.status as TransactionStatus;
|
||||||
if (!ALLOWED_STATUSES.includes(status)) {
|
if (!ALLOWED_STATUSES.has(status)) {
|
||||||
return useResponseError('状态值不合法', -1);
|
return useResponseError('状态值不合法', -1);
|
||||||
}
|
}
|
||||||
payload.status = status;
|
payload.status = status;
|
||||||
@@ -76,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
payload.approvedAt = body.approvedAt ?? null;
|
payload.approvedAt = body.approvedAt ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = updateTransaction(id, payload);
|
const updated = await updateTransaction(id, payload);
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
return useResponseError('报销单不存在', -1);
|
return useResponseError('报销单不存在', -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter((item) => item.length > 0) as TransactionStatus[])
|
.filter((item) => item.length > 0) as TransactionStatus[])
|
||||||
: (['approved', 'paid'] satisfies TransactionStatus[]);
|
: (['approved', 'paid'] satisfies TransactionStatus[]);
|
||||||
const transactions = fetchTransactions({
|
const transactions = await fetchTransactions({
|
||||||
type,
|
type,
|
||||||
includeDeleted,
|
includeDeleted,
|
||||||
statuses,
|
statuses,
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
|
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||||
|
|
||||||
import { readBody } from 'h3';
|
import { readBody } from 'h3';
|
||||||
import {
|
import {
|
||||||
createTransaction,
|
createTransaction,
|
||||||
type TransactionStatus,
|
getAccountById,
|
||||||
|
getCategoryById,
|
||||||
} from '~/utils/finance-repository';
|
} from '~/utils/finance-repository';
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
|
||||||
import { notifyTransaction } from '~/utils/telegram-bot';
|
import { notifyTransaction } from '~/utils/telegram-bot';
|
||||||
import db from '~/utils/sqlite';
|
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||||
|
|
||||||
const DEFAULT_CURRENCY = 'CNY';
|
const DEFAULT_CURRENCY = 'CNY';
|
||||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||||
'draft',
|
'draft',
|
||||||
'pending',
|
'pending',
|
||||||
'approved',
|
'approved',
|
||||||
'rejected',
|
'rejected',
|
||||||
'paid',
|
'paid',
|
||||||
];
|
]);
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
@@ -29,13 +31,12 @@ export default defineEventHandler(async (event) => {
|
|||||||
return useResponseError('金额格式不正确', -1);
|
return useResponseError('金额格式不正确', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status =
|
const status = (body.status as TransactionStatus | undefined) ?? 'approved';
|
||||||
(body.status as TransactionStatus | undefined) ?? 'approved';
|
if (!ALLOWED_STATUSES.has(status)) {
|
||||||
if (!ALLOWED_STATUSES.includes(status)) {
|
|
||||||
return useResponseError('状态值不合法', -1);
|
return useResponseError('状态值不合法', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = createTransaction({
|
const transaction = await createTransaction({
|
||||||
type: body.type,
|
type: body.type,
|
||||||
amount,
|
amount,
|
||||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||||
@@ -61,23 +62,12 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
// 发送Telegram通知(新功能)
|
// 发送Telegram通知(新功能)
|
||||||
try {
|
try {
|
||||||
// 获取分类和账户名称
|
const category = transaction.categoryId
|
||||||
let categoryName: string | undefined;
|
? await getCategoryById(transaction.categoryId)
|
||||||
let accountName: string | undefined;
|
: null;
|
||||||
|
const account = transaction.accountId
|
||||||
if (transaction.categoryId) {
|
? await getAccountById(transaction.accountId)
|
||||||
const category = db
|
: null;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await notifyTransaction(
|
await notifyTransaction(
|
||||||
{
|
{
|
||||||
@@ -85,8 +75,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
type: transaction.type,
|
type: transaction.type,
|
||||||
amount: transaction.amount,
|
amount: transaction.amount,
|
||||||
currency: transaction.currency,
|
currency: transaction.currency,
|
||||||
categoryName,
|
categoryName: category?.name,
|
||||||
accountName,
|
accountName: account?.name,
|
||||||
transactionDate: transaction.transactionDate,
|
transactionDate: transaction.transactionDate,
|
||||||
description: transaction.description || undefined,
|
description: transaction.description || undefined,
|
||||||
status: transaction.status,
|
status: transaction.status,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
return useResponseError('参数错误', -1);
|
return useResponseError('参数错误', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = softDeleteTransaction(id);
|
const updated = await softDeleteTransaction(id);
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
return useResponseError('交易不存在', -1);
|
return useResponseError('交易不存在', -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||||
|
|
||||||
import { getRouterParam, readBody } from 'h3';
|
import { getRouterParam, readBody } from 'h3';
|
||||||
import {
|
import {
|
||||||
restoreTransaction,
|
restoreTransaction,
|
||||||
updateTransaction,
|
updateTransaction,
|
||||||
type TransactionStatus,
|
|
||||||
} from '~/utils/finance-repository';
|
} from '~/utils/finance-repository';
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||||
'draft',
|
'draft',
|
||||||
'pending',
|
'pending',
|
||||||
'approved',
|
'approved',
|
||||||
'rejected',
|
'rejected',
|
||||||
'paid',
|
'paid',
|
||||||
];
|
]);
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = Number(getRouterParam(event, 'id'));
|
const id = Number(getRouterParam(event, 'id'));
|
||||||
@@ -23,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
if (body?.isDeleted === false) {
|
if (body?.isDeleted === false) {
|
||||||
const restored = restoreTransaction(id);
|
const restored = await restoreTransaction(id);
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
return useResponseError('交易不存在', -1);
|
return useResponseError('交易不存在', -1);
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||||
if (body?.status !== undefined) {
|
if (body?.status !== undefined) {
|
||||||
const status = body.status as TransactionStatus;
|
const status = body.status as TransactionStatus;
|
||||||
if (!ALLOWED_STATUSES.includes(status)) {
|
if (!ALLOWED_STATUSES.has(status)) {
|
||||||
return useResponseError('状态值不合法', -1);
|
return useResponseError('状态值不合法', -1);
|
||||||
}
|
}
|
||||||
payload.status = status;
|
payload.status = status;
|
||||||
@@ -76,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
payload.approvedAt = body.approvedAt ?? null;
|
payload.approvedAt = body.approvedAt ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = updateTransaction(id, payload);
|
const updated = await updateTransaction(id, payload);
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
return useResponseError('交易不存在', -1);
|
return useResponseError('交易不存在', -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
import db from '~/utils/sqlite';
|
import { query } from '~/utils/db';
|
||||||
import { useResponseSuccess } from '~/utils/response';
|
import { useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(async () => {
|
||||||
const configs = db
|
const { rows } = await query<{
|
||||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>(
|
id: number;
|
||||||
`
|
name: string;
|
||||||
SELECT id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at
|
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
|
FROM telegram_notification_configs
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC`,
|
||||||
`,
|
);
|
||||||
)
|
|
||||||
.all();
|
|
||||||
|
|
||||||
const result = configs.map((row) => ({
|
const result = rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
botToken: row.bot_token,
|
botToken: row.bot_token,
|
||||||
chatId: row.chat_id,
|
chatId: row.chat_id,
|
||||||
notificationTypes: JSON.parse(row.notification_types) as string[],
|
notificationTypes: JSON.parse(row.notification_types) as string[],
|
||||||
isEnabled: row.is_enabled === 1,
|
isEnabled: row.is_enabled,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readBody } from 'h3';
|
import { readBody } from 'h3';
|
||||||
import db from '~/utils/sqlite';
|
import { query } from '~/utils/db';
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
import { testTelegramConfig } from '~/utils/telegram-bot';
|
import { testTelegramConfig } from '~/utils/telegram-bot';
|
||||||
|
|
||||||
@@ -25,31 +25,48 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
const result = db
|
const { rows } = await query<{
|
||||||
.prepare<unknown, [string, string, string, string, number, string, string]>(
|
id: number;
|
||||||
`
|
name: string;
|
||||||
INSERT INTO telegram_notification_configs (name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at)
|
bot_token: string;
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
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
|
||||||
)
|
)
|
||||||
.run(
|
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.name,
|
||||||
body.botToken,
|
body.botToken,
|
||||||
body.chatId,
|
body.chatId,
|
||||||
JSON.stringify(notificationTypes),
|
JSON.stringify(notificationTypes),
|
||||||
body.isEnabled !== false ? 1 : 0,
|
body.isEnabled !== false,
|
||||||
now,
|
now,
|
||||||
now,
|
now,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
return useResponseSuccess({
|
return useResponseSuccess({
|
||||||
id: result.lastInsertRowid,
|
id: row.id,
|
||||||
name: body.name,
|
name: row.name,
|
||||||
botToken: body.botToken,
|
botToken: row.bot_token,
|
||||||
chatId: body.chatId,
|
chatId: row.chat_id,
|
||||||
notificationTypes,
|
notificationTypes,
|
||||||
isEnabled: body.isEnabled !== false,
|
isEnabled: row.is_enabled,
|
||||||
createdAt: now,
|
createdAt: row.created_at,
|
||||||
updatedAt: now,
|
updatedAt: row.updated_at,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import db from '~/utils/sqlite';
|
import { query } from '~/utils/db';
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = event.context.params?.id;
|
const idParam = event.context.params?.id;
|
||||||
if (!id) {
|
const id = Number(idParam);
|
||||||
|
if (!idParam || Number.isNaN(id)) {
|
||||||
return useResponseError('缺少ID参数', -1);
|
return useResponseError('缺少ID参数', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = db
|
const result = await query(
|
||||||
.prepare('DELETE FROM telegram_notification_configs WHERE id = ?')
|
'DELETE FROM telegram_notification_configs WHERE id = $1',
|
||||||
.run(id);
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
if (result.changes === 0) {
|
if (result.rowCount === 0) {
|
||||||
return useResponseError('配置不存在或删除失败', -1);
|
return useResponseError('配置不存在或删除失败', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
import { readBody } from 'h3';
|
import { readBody } from 'h3';
|
||||||
import db from '~/utils/sqlite';
|
import { query } from '~/utils/db';
|
||||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||||
import { testTelegramConfig } from '~/utils/telegram-bot';
|
import { testTelegramConfig } from '~/utils/telegram-bot';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = event.context.params?.id;
|
const idParam = event.context.params?.id;
|
||||||
if (!id) {
|
const id = Number(idParam);
|
||||||
|
if (!idParam || Number.isNaN(id)) {
|
||||||
return useResponseError('缺少ID参数', -1);
|
return useResponseError('缺少ID参数', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
// 如果更新了botToken或chatId,需要测试配置
|
// 如果更新了botToken或chatId,需要测试配置
|
||||||
if (body.botToken || body.chatId) {
|
if (body.botToken !== undefined || body.chatId !== undefined) {
|
||||||
const existing = db
|
const { rows } = await query<{
|
||||||
.prepare<{ bot_token: string; chat_id: string }>('SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = ?')
|
bot_token: string;
|
||||||
.get(id);
|
chat_id: string;
|
||||||
|
}>(
|
||||||
|
'SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = $1',
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
const existing = rows[0];
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return useResponseError('配置不存在', -1);
|
return useResponseError('配置不存在', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenToTest = body.botToken || existing.bot_token;
|
const tokenToTest = body.botToken ?? existing.bot_token;
|
||||||
const chatIdToTest = body.chatId || existing.chat_id;
|
const chatIdToTest = body.chatId ?? existing.chat_id;
|
||||||
|
|
||||||
const testResult = await testTelegramConfig(tokenToTest, chatIdToTest);
|
const testResult = await testTelegramConfig(tokenToTest, chatIdToTest);
|
||||||
if (!testResult.success) {
|
if (!testResult.success) {
|
||||||
@@ -34,51 +40,65 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updates: string[] = [];
|
const updates: string[] = [];
|
||||||
const values: (string | number)[] = [];
|
const values: any[] = [];
|
||||||
|
|
||||||
if (body.name !== undefined) {
|
if (body.name !== undefined) {
|
||||||
updates.push('name = ?');
|
|
||||||
values.push(body.name);
|
values.push(body.name);
|
||||||
|
updates.push(`name = $${values.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.botToken !== undefined) {
|
if (body.botToken !== undefined) {
|
||||||
updates.push('bot_token = ?');
|
|
||||||
values.push(body.botToken);
|
values.push(body.botToken);
|
||||||
|
updates.push(`bot_token = $${values.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.chatId !== undefined) {
|
if (body.chatId !== undefined) {
|
||||||
updates.push('chat_id = ?');
|
|
||||||
values.push(body.chatId);
|
values.push(body.chatId);
|
||||||
|
updates.push(`chat_id = $${values.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.notificationTypes !== undefined) {
|
if (body.notificationTypes !== undefined) {
|
||||||
updates.push('notification_types = ?');
|
|
||||||
values.push(JSON.stringify(body.notificationTypes));
|
values.push(JSON.stringify(body.notificationTypes));
|
||||||
|
updates.push(`notification_types = $${values.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.isEnabled !== undefined) {
|
if (body.isEnabled !== undefined) {
|
||||||
updates.push('is_enabled = ?');
|
values.push(body.isEnabled !== false);
|
||||||
values.push(body.isEnabled ? 1 : 0);
|
updates.push(`is_enabled = $${values.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.length === 0) {
|
if (updates.length === 0) {
|
||||||
return useResponseError('没有可更新的字段', -1);
|
return useResponseError('没有可更新的字段', -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
updates.push('updated_at = ?');
|
|
||||||
values.push(new Date().toISOString());
|
values.push(new Date().toISOString());
|
||||||
|
updates.push(`updated_at = $${values.length}`);
|
||||||
values.push(id);
|
values.push(id);
|
||||||
|
const idPosition = values.length;
|
||||||
|
|
||||||
db.prepare(`UPDATE telegram_notification_configs SET ${updates.join(', ')} WHERE id = ?`).run(
|
const updateResult = await query(
|
||||||
...values,
|
`UPDATE telegram_notification_configs
|
||||||
|
SET ${updates.join(', ')}
|
||||||
|
WHERE id = $${idPosition}`,
|
||||||
|
values,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updated = db
|
if (updateResult.rowCount === 0) {
|
||||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>(
|
return useResponseError('配置不存在', -1);
|
||||||
'SELECT * FROM telegram_notification_configs WHERE id = ?',
|
}
|
||||||
)
|
|
||||||
.get(id);
|
|
||||||
|
|
||||||
|
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) {
|
if (!updated) {
|
||||||
return useResponseError('更新失败', -1);
|
return useResponseError('更新失败', -1);
|
||||||
}
|
}
|
||||||
@@ -89,7 +109,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
botToken: updated.bot_token,
|
botToken: updated.bot_token,
|
||||||
chatId: updated.chat_id,
|
chatId: updated.chat_id,
|
||||||
notificationTypes: JSON.parse(updated.notification_types) as string[],
|
notificationTypes: JSON.parse(updated.notification_types) as string[],
|
||||||
isEnabled: updated.is_enabled === 1,
|
isEnabled: updated.is_enabled,
|
||||||
createdAt: updated.created_at,
|
createdAt: updated.created_at,
|
||||||
updatedAt: updated.updated_at,
|
updatedAt: updated.updated_at,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "catalog:",
|
"@faker-js/faker": "catalog:",
|
||||||
"better-sqlite3": "9.5.0",
|
|
||||||
"jsonwebtoken": "catalog:",
|
"jsonwebtoken": "catalog:",
|
||||||
"nitropack": "catalog:"
|
"nitropack": "catalog:",
|
||||||
|
"pg": "^8.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsonwebtoken": "catalog:",
|
"@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 {
|
import {
|
||||||
MOCK_ACCOUNTS,
|
MOCK_ACCOUNTS,
|
||||||
MOCK_BUDGETS,
|
MOCK_BUDGETS,
|
||||||
@@ -5,37 +6,87 @@ import {
|
|||||||
MOCK_CURRENCIES,
|
MOCK_CURRENCIES,
|
||||||
MOCK_EXCHANGE_RATES,
|
MOCK_EXCHANGE_RATES,
|
||||||
} from './mock-data';
|
} from './mock-data';
|
||||||
import db from './sqlite';
|
|
||||||
|
|
||||||
export function listAccounts() {
|
interface AccountRow {
|
||||||
return MOCK_ACCOUNTS;
|
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;
|
||||||
try {
|
name: string;
|
||||||
const stmt = db.prepare(`
|
type: string;
|
||||||
SELECT id, name, type, icon, color, user_id as userId, is_active as isActive
|
icon: null | string;
|
||||||
FROM finance_categories
|
color: null | string;
|
||||||
WHERE is_active = 1
|
user_id: null | number;
|
||||||
ORDER BY type, id
|
is_active: boolean;
|
||||||
`);
|
}
|
||||||
const categories = stmt.all() as any[];
|
|
||||||
|
|
||||||
// 转换为前端需要的格式
|
function mapAccount(row: AccountRow) {
|
||||||
return categories.map(cat => ({
|
return {
|
||||||
id: cat.id,
|
id: row.id,
|
||||||
userId: cat.userId,
|
userId: row.user_id ?? 1,
|
||||||
name: cat.name,
|
name: row.name,
|
||||||
type: cat.type,
|
type: row.type,
|
||||||
icon: cat.icon,
|
currency: row.currency,
|
||||||
color: cat.color,
|
balance: 0,
|
||||||
sortOrder: cat.id,
|
icon: row.icon ?? '💳',
|
||||||
isSystem: true,
|
color: row.color ?? '#1677ff',
|
||||||
isActive: Boolean(cat.isActive),
|
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 { 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) {
|
} 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;
|
return MOCK_CATEGORIES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,76 +103,80 @@ export function listExchangeRates() {
|
|||||||
return MOCK_EXCHANGE_RATES;
|
return MOCK_EXCHANGE_RATES;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCategoryRecord(category: any) {
|
export async function createCategoryRecord(category: any) {
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare(`
|
const { rows } = await query<CategoryRow>(
|
||||||
INSERT INTO finance_categories (name, type, icon, color, user_id, is_active)
|
`INSERT INTO finance_categories (name, type, icon, color, user_id, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?, 1)
|
VALUES ($1, $2, $3, $4, $5, TRUE)
|
||||||
`);
|
RETURNING id, name, type, icon, color, user_id, is_active`,
|
||||||
const result = stmt.run(
|
[
|
||||||
category.name,
|
category.name,
|
||||||
category.type,
|
category.type,
|
||||||
category.icon || '📝',
|
category.icon || '📝',
|
||||||
category.color || '#dfe4ea',
|
category.color || '#dfe4ea',
|
||||||
category.userId || 1
|
category.userId || 1,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
return {
|
const row = rows[0];
|
||||||
id: result.lastInsertRowid,
|
return row
|
||||||
...category,
|
? {
|
||||||
|
...mapCategory(row),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
}
|
||||||
|
: null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建分类失败:', error);
|
console.error('创建分类失败:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateCategoryRecord(id: number, category: any) {
|
export async function updateCategoryRecord(id: number, category: any) {
|
||||||
try {
|
try {
|
||||||
const updates: string[] = [];
|
const updates: string[] = [];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
|
|
||||||
if (category.name) {
|
if (category.name) {
|
||||||
updates.push('name = ?');
|
|
||||||
params.push(category.name);
|
params.push(category.name);
|
||||||
|
updates.push(`name = $${params.length}`);
|
||||||
}
|
}
|
||||||
if (category.icon) {
|
if (category.icon) {
|
||||||
updates.push('icon = ?');
|
|
||||||
params.push(category.icon);
|
params.push(category.icon);
|
||||||
|
updates.push(`icon = $${params.length}`);
|
||||||
}
|
}
|
||||||
if (category.color) {
|
if (category.color) {
|
||||||
updates.push('color = ?');
|
|
||||||
params.push(category.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);
|
params.push(id);
|
||||||
const stmt = db.prepare(`
|
const setClause = updates.join(', ');
|
||||||
UPDATE finance_categories
|
const { rows } = await query<CategoryRow>(
|
||||||
SET ${updates.join(', ')}
|
`UPDATE finance_categories
|
||||||
WHERE id = ?
|
SET ${setClause}
|
||||||
`);
|
WHERE id = $${params.length}
|
||||||
stmt.run(...params);
|
RETURNING id, name, type, icon, color, user_id, is_active`,
|
||||||
|
params,
|
||||||
// 返回更新后的分类
|
);
|
||||||
const selectStmt = db.prepare('SELECT * FROM finance_categories WHERE id = ?');
|
const row = rows[0];
|
||||||
return selectStmt.get(id);
|
return row ? mapCategory(row) : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新分类失败:', error);
|
console.error('更新分类失败:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCategoryRecord(id: number) {
|
export async function deleteCategoryRecord(id: number) {
|
||||||
try {
|
try {
|
||||||
// 软删除
|
await query(
|
||||||
const stmt = db.prepare(`
|
`UPDATE finance_categories
|
||||||
UPDATE finance_categories
|
SET is_active = FALSE
|
||||||
SET is_active = 0
|
WHERE id = $1`,
|
||||||
WHERE id = ?
|
[id],
|
||||||
`);
|
);
|
||||||
stmt.run(id);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除分类失败:', 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';
|
const BASE_CURRENCY = 'CNY';
|
||||||
|
|
||||||
interface TransactionRow {
|
interface TransactionRow {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
amount: number;
|
amount: number | string;
|
||||||
currency: string;
|
currency: string;
|
||||||
exchange_rate_to_base: number;
|
exchange_rate_to_base: number | string;
|
||||||
amount_in_base: number;
|
amount_in_base: number | string;
|
||||||
category_id: null | number;
|
category_id: null | number;
|
||||||
account_id: null | number;
|
account_id: null | number;
|
||||||
transaction_date: string;
|
transaction_date: string;
|
||||||
@@ -23,7 +25,7 @@ interface TransactionRow {
|
|||||||
submitted_by: null | string;
|
submitted_by: null | string;
|
||||||
approved_by: null | string;
|
approved_by: null | string;
|
||||||
approved_at: null | string;
|
approved_at: null | string;
|
||||||
is_deleted: number;
|
is_deleted: boolean;
|
||||||
deleted_at: null | string;
|
deleted_at: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,32 +51,24 @@ interface TransactionPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type TransactionStatus =
|
export type TransactionStatus =
|
||||||
| 'draft'
|
|
||||||
| 'pending'
|
|
||||||
| 'approved'
|
| 'approved'
|
||||||
| 'rejected'
|
| 'draft'
|
||||||
| 'paid';
|
| 'paid'
|
||||||
|
| 'pending'
|
||||||
function getExchangeRateToBase(currency: string) {
|
| 'rejected';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapTransaction(row: TransactionRow) {
|
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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
userId: 1,
|
userId: 1,
|
||||||
type: 'expense' as const,
|
type: row.type as 'expense' | 'income' | 'transfer',
|
||||||
amount: Math.abs(row.amount),
|
amount: Math.abs(amount),
|
||||||
currency: row.currency,
|
currency: row.currency,
|
||||||
exchangeRateToBase: row.exchange_rate_to_base,
|
exchangeRateToBase,
|
||||||
amountInBase: Math.abs(row.amount_in_base),
|
amountInBase: Math.abs(amountInBase),
|
||||||
categoryId: row.category_id ?? undefined,
|
categoryId: row.category_id ?? undefined,
|
||||||
accountId: row.account_id ?? undefined,
|
accountId: row.account_id ?? undefined,
|
||||||
transactionDate: row.transaction_date,
|
transactionDate: row.transaction_date,
|
||||||
@@ -94,51 +88,114 @@ 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: {
|
options: {
|
||||||
includeDeleted?: boolean;
|
includeDeleted?: boolean;
|
||||||
type?: string;
|
|
||||||
statuses?: TransactionStatus[];
|
statuses?: TransactionStatus[];
|
||||||
|
type?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const clauses: string[] = [];
|
const clauses: string[] = [];
|
||||||
const params: Record<string, unknown> = {};
|
const params: any[] = [];
|
||||||
|
|
||||||
if (!options.includeDeleted) {
|
if (!options.includeDeleted) {
|
||||||
clauses.push('is_deleted = 0');
|
clauses.push('is_deleted = FALSE');
|
||||||
}
|
}
|
||||||
if (options.type) {
|
if (options.type) {
|
||||||
clauses.push('type = @type');
|
params.push(options.type);
|
||||||
params.type = options.type;
|
clauses.push(`type = $${params.length}`);
|
||||||
}
|
}
|
||||||
if (options.statuses && options.statuses.length > 0) {
|
if (options.statuses && options.statuses.length > 0) {
|
||||||
clauses.push(
|
const statusPlaceholders = options.statuses.map((status) => {
|
||||||
`status IN (${options.statuses.map((_, index) => `@status${index}`).join(', ')})`,
|
params.push(status);
|
||||||
);
|
return `$${params.length}`;
|
||||||
options.statuses.forEach((status, index) => {
|
|
||||||
params[`status${index}`] = status;
|
|
||||||
});
|
});
|
||||||
|
clauses.push(`status IN (${statusPlaceholders.join(', ')})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||||
|
const { rows } = await query<TransactionRow>(
|
||||||
const stmt = db.prepare<TransactionRow>(
|
`SELECT id,
|
||||||
`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`,
|
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 rows.map((row) => mapTransaction(row));
|
||||||
return stmt.all(params).map(mapTransaction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransactionById(id: number) {
|
export async function getTransactionById(id: number) {
|
||||||
const stmt = db.prepare<TransactionRow>(
|
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 = ?`,
|
`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;
|
return row ? mapTransaction(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTransaction(payload: TransactionPayload) {
|
export async function createTransaction(payload: TransactionPayload) {
|
||||||
const exchangeRate = getExchangeRateToBase(payload.currency);
|
return withTransaction(async (client) => {
|
||||||
|
const exchangeRate = await getExchangeRateToBase(client, payload.currency);
|
||||||
const amountInBase = +(payload.amount * exchangeRate).toFixed(2);
|
const amountInBase = +(payload.amount * exchangeRate).toFixed(2);
|
||||||
const createdAt =
|
const createdAt =
|
||||||
payload.createdAt && payload.createdAt.length > 0
|
payload.createdAt && payload.createdAt.length > 0
|
||||||
@@ -149,62 +206,95 @@ export function createTransaction(payload: TransactionPayload) {
|
|||||||
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
|
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
|
||||||
? payload.statusUpdatedAt
|
? payload.statusUpdatedAt
|
||||||
: createdAt;
|
: createdAt;
|
||||||
const approvedAt =
|
let approvedAt: string | null = null;
|
||||||
payload.approvedAt && payload.approvedAt.length > 0
|
if (payload.approvedAt && payload.approvedAt.length > 0) {
|
||||||
? payload.approvedAt
|
approvedAt = payload.approvedAt;
|
||||||
: status === 'approved' || status === 'paid'
|
} else if (status === 'approved' || status === 'paid') {
|
||||||
? statusUpdatedAt
|
approvedAt = statusUpdatedAt;
|
||||||
: null;
|
}
|
||||||
|
|
||||||
const stmt = db.prepare(
|
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 (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, @status, @statusUpdatedAt, @reimbursementBatch, @reviewNotes, @submittedBy, @approvedBy, @approvedAt, 0)`,
|
`INSERT INTO finance_transactions (
|
||||||
);
|
type,
|
||||||
|
amount,
|
||||||
const info = stmt.run({
|
currency,
|
||||||
type: payload.type,
|
exchange_rate_to_base,
|
||||||
amount: payload.amount,
|
amount_in_base,
|
||||||
currency: payload.currency,
|
category_id,
|
||||||
exchangeRateToBase: exchangeRate,
|
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,
|
amountInBase,
|
||||||
categoryId: payload.categoryId ?? null,
|
payload.categoryId ?? null,
|
||||||
accountId: payload.accountId ?? null,
|
payload.accountId ?? null,
|
||||||
transactionDate: payload.transactionDate,
|
payload.transactionDate,
|
||||||
description: payload.description ?? '',
|
payload.description ?? '',
|
||||||
project: payload.project ?? null,
|
payload.project ?? null,
|
||||||
memo: payload.memo ?? null,
|
payload.memo ?? null,
|
||||||
createdAt,
|
createdAt,
|
||||||
status,
|
status,
|
||||||
statusUpdatedAt,
|
statusUpdatedAt,
|
||||||
reimbursementBatch: payload.reimbursementBatch ?? null,
|
payload.reimbursementBatch ?? null,
|
||||||
reviewNotes: payload.reviewNotes ?? null,
|
payload.reviewNotes ?? null,
|
||||||
submittedBy: payload.submittedBy ?? null,
|
payload.submittedBy ?? null,
|
||||||
approvedBy: payload.approvedBy ?? null,
|
payload.approvedBy ?? null,
|
||||||
approvedAt,
|
approvedAt,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return mapTransaction(rows[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return getTransactionById(Number(info.lastInsertRowid));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTransaction(id: number, payload: TransactionPayload) {
|
export async function updateTransaction(
|
||||||
const current = getTransactionById(id);
|
id: number,
|
||||||
|
payload: TransactionPayload,
|
||||||
|
) {
|
||||||
|
const current = await getTransactionById(id);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStatus = (payload.status ?? current.status ?? 'approved') as TransactionStatus;
|
return withTransaction(async (client) => {
|
||||||
|
const nextStatus = (payload.status ??
|
||||||
|
current.status ??
|
||||||
|
'approved') as TransactionStatus;
|
||||||
const statusChanged = nextStatus !== current.status;
|
const statusChanged = nextStatus !== current.status;
|
||||||
const statusUpdatedAt =
|
let statusUpdatedAt: string;
|
||||||
payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0
|
if (payload.statusUpdatedAt && payload.statusUpdatedAt.length > 0) {
|
||||||
? payload.statusUpdatedAt
|
statusUpdatedAt = payload.statusUpdatedAt;
|
||||||
: statusChanged
|
} else if (statusChanged) {
|
||||||
? new Date().toISOString()
|
statusUpdatedAt = new Date().toISOString();
|
||||||
: current.statusUpdatedAt ?? current.createdAt;
|
} else {
|
||||||
const approvedAt =
|
statusUpdatedAt = current.statusUpdatedAt ?? current.createdAt;
|
||||||
payload.approvedAt && payload.approvedAt.length > 0
|
}
|
||||||
? payload.approvedAt
|
let approvedAt: string | null = null;
|
||||||
: nextStatus === 'approved' || nextStatus === 'paid'
|
if (payload.approvedAt && payload.approvedAt.length > 0) {
|
||||||
? current.approvedAt ?? (statusChanged ? statusUpdatedAt : null)
|
approvedAt = payload.approvedAt;
|
||||||
: null;
|
} else if (nextStatus === 'approved' || nextStatus === 'paid') {
|
||||||
|
approvedAt = current.approvedAt ?? (statusChanged ? statusUpdatedAt : null);
|
||||||
|
}
|
||||||
const approvedBy =
|
const approvedBy =
|
||||||
nextStatus === 'approved' || nextStatus === 'paid'
|
nextStatus === 'approved' || nextStatus === 'paid'
|
||||||
? payload.approvedBy ?? current.approvedBy ?? null
|
? payload.approvedBy ?? current.approvedBy ?? null
|
||||||
@@ -231,94 +321,117 @@ export function updateTransaction(id: number, payload: TransactionPayload) {
|
|||||||
approvedAt,
|
approvedAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
const exchangeRate = getExchangeRateToBase(next.currency);
|
const exchangeRate = await getExchangeRateToBase(client, next.currency);
|
||||||
const amountInBase = +(next.amount * exchangeRate).toFixed(2);
|
const amountInBase = +(next.amount * exchangeRate).toFixed(2);
|
||||||
|
|
||||||
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 deletedAt = next.isDeleted ? new Date().toISOString() : null;
|
const deletedAt = next.isDeleted ? new Date().toISOString() : null;
|
||||||
|
|
||||||
stmt.run({
|
const { rows } = await client.query<TransactionRow>(
|
||||||
id,
|
`UPDATE finance_transactions
|
||||||
type: next.type,
|
SET type = $1,
|
||||||
amount: next.amount,
|
amount = $2,
|
||||||
currency: next.currency,
|
currency = $3,
|
||||||
exchangeRateToBase: exchangeRate,
|
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,
|
amountInBase,
|
||||||
categoryId: next.categoryId,
|
next.categoryId,
|
||||||
accountId: next.accountId,
|
next.accountId,
|
||||||
transactionDate: next.transactionDate,
|
next.transactionDate,
|
||||||
description: next.description,
|
next.description,
|
||||||
project: next.project,
|
next.project,
|
||||||
memo: next.memo,
|
next.memo,
|
||||||
status: next.status,
|
next.status,
|
||||||
statusUpdatedAt: next.statusUpdatedAt,
|
next.statusUpdatedAt,
|
||||||
reimbursementBatch: next.reimbursementBatch,
|
next.reimbursementBatch,
|
||||||
reviewNotes: next.reviewNotes,
|
next.reviewNotes,
|
||||||
submittedBy: next.submittedBy,
|
next.submittedBy,
|
||||||
approvedBy: next.approvedBy,
|
next.approvedBy,
|
||||||
approvedAt: next.approvedAt,
|
next.approvedAt,
|
||||||
isDeleted: next.isDeleted ? 1 : 0,
|
next.isDeleted,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
|
id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return mapTransaction(rows[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return getTransactionById(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function softDeleteTransaction(id: number) {
|
export async function softDeleteTransaction(id: number) {
|
||||||
const stmt = db.prepare(
|
const deletedAt = new Date().toISOString();
|
||||||
`UPDATE finance_transactions SET is_deleted = 1, deleted_at = @deletedAt WHERE id = @id`,
|
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() });
|
const row = rows[0];
|
||||||
return getTransactionById(id);
|
return row ? mapTransaction(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreTransaction(id: number) {
|
export async function restoreTransaction(id: number) {
|
||||||
const stmt = db.prepare(
|
const { rows } = await query<TransactionRow>(
|
||||||
`UPDATE finance_transactions SET is_deleted = 0, deleted_at = NULL WHERE id = @id`,
|
`UPDATE finance_transactions
|
||||||
|
SET is_deleted = FALSE, deleted_at = NULL
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *`,
|
||||||
|
[id],
|
||||||
);
|
);
|
||||||
stmt.run({ id });
|
const row = rows[0];
|
||||||
return getTransactionById(id);
|
return row ? mapTransaction(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceAllTransactions(
|
export async function replaceAllTransactions(
|
||||||
rows: Array<{
|
rows: Array<{
|
||||||
accountId: null | number;
|
accountId: null | number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
approvedAt?: null | string;
|
||||||
|
approvedBy?: null | string;
|
||||||
categoryId: null | number;
|
categoryId: null | number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
isDeleted?: boolean;
|
||||||
memo?: null | string;
|
memo?: null | string;
|
||||||
project?: null | string;
|
project?: null | string;
|
||||||
transactionDate: string;
|
|
||||||
type: string;
|
|
||||||
status?: TransactionStatus;
|
|
||||||
statusUpdatedAt?: string;
|
|
||||||
reimbursementBatch?: null | string;
|
reimbursementBatch?: null | string;
|
||||||
reviewNotes?: null | string;
|
reviewNotes?: null | string;
|
||||||
|
status?: TransactionStatus;
|
||||||
|
statusUpdatedAt?: string;
|
||||||
submittedBy?: null | string;
|
submittedBy?: null | string;
|
||||||
approvedBy?: null | string;
|
transactionDate: string;
|
||||||
approvedAt?: null | string;
|
type: string;
|
||||||
isDeleted?: boolean;
|
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
db.prepare('DELETE FROM finance_transactions').run();
|
await withTransaction(async (client) => {
|
||||||
|
await client.query(
|
||||||
const insert = db.prepare(
|
'TRUNCATE TABLE finance_transactions RESTART IDENTITY CASCADE',
|
||||||
`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(
|
for (const item of rows) {
|
||||||
`SELECT rate FROM finance_exchange_rates WHERE from_currency = ? AND to_currency = 'CNY' ORDER BY date DESC LIMIT 1`,
|
const rate = await getExchangeRateToBase(client, item.currency);
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
const amountInBase = +(item.amount * rate).toFixed(2);
|
const amountInBase = +(item.amount * rate).toFixed(2);
|
||||||
const createdAt =
|
const createdAt =
|
||||||
item.createdAt ??
|
item.createdAt ??
|
||||||
@@ -326,38 +439,67 @@ export function replaceAllTransactions(
|
|||||||
const status = item.status ?? 'approved';
|
const status = item.status ?? 'approved';
|
||||||
const statusUpdatedAt =
|
const statusUpdatedAt =
|
||||||
item.statusUpdatedAt ??
|
item.statusUpdatedAt ??
|
||||||
new Date(
|
new Date(`${item.transactionDate}T00:00:00Z`).toISOString();
|
||||||
`${item.transactionDate}T00:00:00Z`,
|
|
||||||
).toISOString();
|
|
||||||
const approvedAt =
|
const approvedAt =
|
||||||
item.approvedAt ??
|
item.approvedAt ??
|
||||||
(status === 'approved' || status === 'paid' ? statusUpdatedAt : null);
|
(status === 'approved' || status === 'paid' ? statusUpdatedAt : null);
|
||||||
insert.run({
|
|
||||||
...item,
|
await client.query(
|
||||||
exchangeRateToBase: rate,
|
`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,
|
amountInBase,
|
||||||
project: item.project ?? null,
|
item.categoryId ?? null,
|
||||||
memo: item.memo ?? null,
|
item.accountId ?? null,
|
||||||
|
item.transactionDate,
|
||||||
|
item.description ?? '',
|
||||||
|
item.project ?? null,
|
||||||
|
item.memo ?? null,
|
||||||
createdAt,
|
createdAt,
|
||||||
status,
|
status,
|
||||||
statusUpdatedAt,
|
statusUpdatedAt,
|
||||||
reimbursementBatch: item.reimbursementBatch ?? null,
|
item.reimbursementBatch ?? null,
|
||||||
reviewNotes: item.reviewNotes ?? null,
|
item.reviewNotes ?? null,
|
||||||
submittedBy: item.submittedBy ?? null,
|
item.submittedBy ?? null,
|
||||||
approvedBy:
|
|
||||||
status === 'approved' || status === 'paid'
|
status === 'approved' || status === 'paid'
|
||||||
? item.approvedBy ?? null
|
? (item.approvedBy ?? null)
|
||||||
: null,
|
: null,
|
||||||
approvedAt,
|
approvedAt,
|
||||||
isDeleted: item.isDeleted ? 1 : 0,
|
item.isDeleted ?? false,
|
||||||
});
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
insertMany(rows);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分类相关函数
|
|
||||||
interface CategoryRow {
|
interface CategoryRow {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -365,7 +507,7 @@ interface CategoryRow {
|
|||||||
icon: null | string;
|
icon: null | string;
|
||||||
color: null | string;
|
color: null | string;
|
||||||
user_id: null | number;
|
user_id: null | number;
|
||||||
is_active: number;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCategory(row: CategoryRow) {
|
function mapCategory(row: CategoryRow) {
|
||||||
@@ -382,15 +524,53 @@ function mapCategory(row: CategoryRow) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchCategories(options: { type?: 'expense' | 'income' } = {}) {
|
export async function fetchCategories(
|
||||||
const where = options.type
|
options: { type?: 'expense' | 'income' } = {},
|
||||||
? `WHERE type = @type AND is_active = 1`
|
) {
|
||||||
: 'WHERE is_active = 1';
|
const params: any[] = [];
|
||||||
const params = options.type ? { type: options.type } : {};
|
const clauses: string[] = ['is_active = TRUE'];
|
||||||
|
if (options.type) {
|
||||||
const stmt = db.prepare<CategoryRow>(
|
params.push(options.type);
|
||||||
`SELECT id, name, type, icon, color, user_id, is_active FROM finance_categories ${where} ORDER BY id ASC`,
|
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 rows.map((row) => mapCategory(row));
|
||||||
return stmt.all(params).map(mapCategory);
|
}
|
||||||
|
|
||||||
|
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 { existsSync } from 'node:fs';
|
||||||
|
|
||||||
import db from './sqlite';
|
import { query } from './db';
|
||||||
|
|
||||||
interface MediaRow {
|
interface MediaRow {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -47,7 +47,7 @@ export interface MediaMessage {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
available: boolean;
|
available: boolean;
|
||||||
downloadUrl: string | null;
|
downloadUrl: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapMediaRow(row: MediaRow): MediaMessage {
|
function mapMediaRow(row: MediaRow): MediaMessage {
|
||||||
@@ -78,40 +78,85 @@ function mapMediaRow(row: MediaRow): MediaMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchMediaMessages(params: {
|
export async function fetchMediaMessages(
|
||||||
limit?: number;
|
params: {
|
||||||
fileTypes?: string[];
|
fileTypes?: string[];
|
||||||
} = {}) {
|
limit?: number;
|
||||||
const clauses: string[] = [];
|
} = {},
|
||||||
const bindParams: Record<string, unknown> = {};
|
) {
|
||||||
|
const whereClauses: string[] = [];
|
||||||
|
const queryParams: any[] = [];
|
||||||
|
|
||||||
if (params.fileTypes && params.fileTypes.length > 0) {
|
if (params.fileTypes && params.fileTypes.length > 0) {
|
||||||
clauses.push(
|
const placeholders = params.fileTypes.map((type) => {
|
||||||
`file_type IN (${params.fileTypes.map((_, index) => `@type${index}`).join(', ')})`,
|
queryParams.push(type);
|
||||||
);
|
return `$${queryParams.length}`;
|
||||||
params.fileTypes.forEach((type, index) => {
|
|
||||||
bindParams[`type${index}`] = type;
|
|
||||||
});
|
});
|
||||||
|
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 =
|
const limitClause =
|
||||||
params.limit && params.limit > 0 ? `LIMIT ${Number(params.limit)}` : '';
|
params.limit && params.limit > 0 ? `LIMIT ${Number(params.limit)}` : '';
|
||||||
|
|
||||||
const stmt = db.prepare<MediaRow>(
|
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 datetime(created_at) DESC, id DESC ${limitClause}`,
|
`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) {
|
export async function getMediaMessageById(id: number) {
|
||||||
const stmt = db.prepare<MediaRow>(
|
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 = ?`,
|
`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 = rows[0];
|
||||||
const row = stmt.get(id);
|
|
||||||
|
|
||||||
return row ? mapMediaRow(row) : null;
|
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 {
|
||||||
import db from './sqlite';
|
getEnabledNotificationConfigs,
|
||||||
|
notifyTransaction,
|
||||||
|
testTelegramConfig,
|
||||||
|
} from './telegram-bot';
|
||||||
|
|
||||||
interface TelegramNotificationConfig {
|
export { getEnabledNotificationConfigs, testTelegramConfig };
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
botToken: string;
|
|
||||||
chatId: string;
|
|
||||||
notificationTypes: string[];
|
|
||||||
isEnabled: boolean;
|
|
||||||
priority: string;
|
|
||||||
rateLimitSeconds: number;
|
|
||||||
batchEnabled: boolean;
|
|
||||||
batchIntervalMinutes: number;
|
|
||||||
retryEnabled: boolean;
|
|
||||||
retryMaxAttempts: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
export async function notifyTransactionEnhanced(
|
||||||
transaction: TransactionNotificationData,
|
...args: Parameters<typeof notifyTransaction>
|
||||||
action: string = 'created',
|
) {
|
||||||
): Promise<void> {
|
await notifyTransaction(...args);
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 重试失败的通知
|
|
||||||
*/
|
|
||||||
export async function retryFailedNotifications(): Promise<void> {
|
export async function retryFailedNotifications(): Promise<void> {
|
||||||
const pending = getPendingRetries();
|
// Retrying logic is not yet implemented for the PostgreSQL data source.
|
||||||
|
// The SQLite-specific implementation relied on synchronous database access.
|
||||||
if (pending.length === 0) {
|
// If this functionality becomes necessary, please implement it using the
|
||||||
return;
|
// telegram_notification_history table with pool-based transactions.
|
||||||
}
|
console.warn('[telegram-bot-enhanced] retryFailedNotifications is not implemented.');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import db from './sqlite';
|
import { query } from './db';
|
||||||
|
|
||||||
interface TelegramNotificationConfig {
|
interface TelegramNotificationConfig {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -24,18 +24,21 @@ interface TransactionNotificationData {
|
|||||||
/**
|
/**
|
||||||
* 获取所有启用的Telegram通知配置
|
* 获取所有启用的Telegram通知配置
|
||||||
*/
|
*/
|
||||||
export function getEnabledNotificationConfigs(
|
export async function getEnabledNotificationConfigs(
|
||||||
notificationType: string = 'transaction',
|
notificationType: string = 'transaction',
|
||||||
): TelegramNotificationConfig[] {
|
): Promise<TelegramNotificationConfig[]> {
|
||||||
const rows = db
|
const { rows } = await query<{
|
||||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number }>(
|
bot_token: string;
|
||||||
`
|
chat_id: string;
|
||||||
SELECT id, name, bot_token, chat_id, notification_types, is_enabled
|
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
|
FROM telegram_notification_configs
|
||||||
WHERE is_enabled = 1
|
WHERE is_enabled = TRUE`,
|
||||||
`,
|
);
|
||||||
)
|
|
||||||
.all();
|
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
@@ -44,7 +47,7 @@ export function getEnabledNotificationConfigs(
|
|||||||
botToken: row.bot_token,
|
botToken: row.bot_token,
|
||||||
chatId: row.chat_id,
|
chatId: row.chat_id,
|
||||||
notificationTypes: JSON.parse(row.notification_types) as string[],
|
notificationTypes: JSON.parse(row.notification_types) as string[],
|
||||||
isEnabled: row.is_enabled === 1,
|
isEnabled: row.is_enabled,
|
||||||
}))
|
}))
|
||||||
.filter((config) => config.notificationTypes.includes(notificationType));
|
.filter((config) => config.notificationTypes.includes(notificationType));
|
||||||
}
|
}
|
||||||
@@ -175,10 +178,10 @@ export async function notifyTransaction(
|
|||||||
transaction: TransactionNotificationData,
|
transaction: TransactionNotificationData,
|
||||||
action: string = 'created',
|
action: string = 'created',
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const configs = getEnabledNotificationConfigs('transaction');
|
const configs = await getEnabledNotificationConfigs('transaction');
|
||||||
|
|
||||||
if (configs.length === 0) {
|
if (configs.length === 0) {
|
||||||
console.log('[telegram-bot] No enabled notification configs found');
|
console.warn('[telegram-bot] No enabled notification configs found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +195,7 @@ export async function notifyTransaction(
|
|||||||
|
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status === 'fulfilled' && result.value) {
|
if (result.status === 'fulfilled' && result.value) {
|
||||||
console.log(
|
console.warn(
|
||||||
`[telegram-bot] Sent notification via config: ${configs[index].name}`,
|
`[telegram-bot] Sent notification via config: ${configs[index].name}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -209,17 +212,18 @@ export async function notifyTransaction(
|
|||||||
export async function testTelegramConfig(
|
export async function testTelegramConfig(
|
||||||
botToken: string,
|
botToken: string,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ error?: string; success: boolean }> {
|
||||||
try {
|
try {
|
||||||
const testMessage = `🤖 KT财务系统\n\n✅ Telegram通知配置测试成功!\n\n🕐 ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`;
|
const testMessage = `🤖 KT财务系统\n\n✅ Telegram通知配置测试成功!\n\n🕐 ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`;
|
||||||
|
|
||||||
const success = await sendTelegramMessage(botToken, chatId, testMessage);
|
const success = await sendTelegramMessage(botToken, chatId, testMessage);
|
||||||
|
|
||||||
if (success) {
|
return success
|
||||||
return { success: true };
|
? { success: true }
|
||||||
} else {
|
: {
|
||||||
return { success: false, error: '发送消息失败,请检查Bot Token和Chat ID' };
|
success: false,
|
||||||
}
|
error: '发送消息失败,请检查Bot Token和Chat ID',
|
||||||
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
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 "🚀 构建并启动新容器..."
|
echo "🚀 构建并启动新容器..."
|
||||||
sudo docker-compose up -d --build
|
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 "🧹 清理旧镜像..."
|
echo "🧹 清理旧镜像..."
|
||||||
sudo docker image prune -f
|
sudo docker image prune -f
|
||||||
|
|||||||
@@ -1,23 +1,54 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
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:
|
kt-financial:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: kt-financial-system
|
container_name: kt-financial-system
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
- POSTGRES_HOST=postgres
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
|
- POSTGRES_DB=kt_financial
|
||||||
|
- POSTGRES_USER=kt_financial
|
||||||
|
- POSTGRES_PASSWORD=kt_financial_pwd
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/var/log
|
- ./logs:/var/log
|
||||||
- ./storage/backend:/app/apps/backend/storage
|
- ./storage/backend:/app/apps/backend/storage
|
||||||
|
- ./data:/app/data:ro
|
||||||
networks:
|
networks:
|
||||||
- kt-network
|
- kt-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
kt-network:
|
kt-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|||||||
144
pnpm-lock.yaml
generated
144
pnpm-lock.yaml
generated
@@ -631,15 +631,15 @@ importers:
|
|||||||
'@faker-js/faker':
|
'@faker-js/faker':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 9.9.0
|
version: 9.9.0
|
||||||
better-sqlite3:
|
|
||||||
specifier: 9.5.0
|
|
||||||
version: 9.5.0
|
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
nitropack:
|
nitropack:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.12.9(better-sqlite3@9.5.0)
|
version: 2.12.9(better-sqlite3@9.5.0)
|
||||||
|
pg:
|
||||||
|
specifier: ^8.12.0
|
||||||
|
version: 8.16.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
@@ -8247,6 +8247,40 @@ packages:
|
|||||||
perfect-debounce@2.0.0:
|
perfect-debounce@2.0.0:
|
||||||
resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==}
|
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:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -8758,6 +8792,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
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:
|
preact@10.27.2:
|
||||||
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
|
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
|
||||||
|
|
||||||
@@ -10612,6 +10662,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
y18n@4.0.3:
|
y18n@4.0.3:
|
||||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||||
|
|
||||||
@@ -14433,6 +14487,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bindings: 1.5.0
|
bindings: 1.5.0
|
||||||
prebuild-install: 7.1.3
|
prebuild-install: 7.1.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
bignumber.js@9.3.1: {}
|
bignumber.js@9.3.1: {}
|
||||||
|
|
||||||
@@ -14451,6 +14506,7 @@ snapshots:
|
|||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
boolbase@1.0.0: {}
|
boolbase@1.0.0: {}
|
||||||
|
|
||||||
@@ -14496,6 +14552,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14666,7 +14723,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
chownr@1.1.4: {}
|
chownr@1.1.4:
|
||||||
|
optional: true
|
||||||
|
|
||||||
chownr@3.0.0: {}
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
@@ -15186,6 +15244,7 @@ snapshots:
|
|||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
deep-eql@5.0.2: {}
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
@@ -15427,6 +15486,7 @@ snapshots:
|
|||||||
end-of-stream@1.4.5:
|
end-of-stream@1.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
enhanced-resolve@5.18.3:
|
enhanced-resolve@5.18.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -15922,7 +15982,8 @@ snapshots:
|
|||||||
strip-final-newline: 4.0.0
|
strip-final-newline: 4.0.0
|
||||||
yoctocolors: 2.1.2
|
yoctocolors: 2.1.2
|
||||||
|
|
||||||
expand-template@2.0.3: {}
|
expand-template@2.0.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
expand-tilde@2.0.2:
|
expand-tilde@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16091,7 +16152,8 @@ snapshots:
|
|||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
fs-constants@1.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fs-extra@10.1.0:
|
fs-extra@10.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16215,7 +16277,8 @@ snapshots:
|
|||||||
meow: 12.1.1
|
meow: 12.1.1
|
||||||
split2: 4.2.0
|
split2: 4.2.0
|
||||||
|
|
||||||
github-from-package@0.0.0: {}
|
github-from-package@0.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17235,7 +17298,8 @@ snapshots:
|
|||||||
|
|
||||||
mimic-function@5.0.1: {}
|
mimic-function@5.0.1: {}
|
||||||
|
|
||||||
mimic-response@3.1.0: {}
|
mimic-response@3.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
minimatch@10.0.3:
|
minimatch@10.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17293,7 +17357,8 @@ snapshots:
|
|||||||
|
|
||||||
mitt@3.0.1: {}
|
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)):
|
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:
|
dependencies:
|
||||||
@@ -17374,7 +17439,8 @@ snapshots:
|
|||||||
|
|
||||||
nanopop@2.4.2: {}
|
nanopop@2.4.2: {}
|
||||||
|
|
||||||
napi-build-utils@2.0.0: {}
|
napi-build-utils@2.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
napi-postinstall@0.3.4: {}
|
napi-postinstall@0.3.4: {}
|
||||||
|
|
||||||
@@ -17498,6 +17564,7 @@ snapshots:
|
|||||||
node-abi@3.80.0:
|
node-abi@3.80.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
@@ -17806,6 +17873,41 @@ snapshots:
|
|||||||
|
|
||||||
perfect-debounce@2.0.0: {}
|
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: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -18334,6 +18436,16 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.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: {}
|
preact@10.27.2: {}
|
||||||
|
|
||||||
prebuild-install@7.1.3:
|
prebuild-install@7.1.3:
|
||||||
@@ -18350,6 +18462,7 @@ snapshots:
|
|||||||
simple-get: 4.0.1
|
simple-get: 4.0.1
|
||||||
tar-fs: 2.1.4
|
tar-fs: 2.1.4
|
||||||
tunnel-agent: 0.6.0
|
tunnel-agent: 0.6.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
@@ -18399,6 +18512,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
@@ -18492,6 +18606,7 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
readable-stream@4.7.0:
|
readable-stream@4.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18897,13 +19012,15 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
simple-concat@1.0.1: {}
|
simple-concat@1.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
simple-get@4.0.1:
|
simple-get@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
decompress-response: 6.0.0
|
decompress-response: 6.0.0
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
simple-concat: 1.0.1
|
simple-concat: 1.0.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
sirv@3.0.2:
|
sirv@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -19362,6 +19479,7 @@ snapshots:
|
|||||||
mkdirp-classic: 0.5.3
|
mkdirp-classic: 0.5.3
|
||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
tar-stream: 2.2.0
|
tar-stream: 2.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
tar-stream@2.2.0:
|
tar-stream@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -19370,6 +19488,7 @@ snapshots:
|
|||||||
fs-constants: 1.0.0
|
fs-constants: 1.0.0
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
tar-stream@3.1.7:
|
tar-stream@3.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -19493,6 +19612,7 @@ snapshots:
|
|||||||
tunnel-agent@0.6.0:
|
tunnel-agent@0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
turbo-darwin-64@2.6.0:
|
turbo-darwin-64@2.6.0:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -20567,6 +20687,8 @@ snapshots:
|
|||||||
|
|
||||||
xml-name-validator@4.0.0: {}
|
xml-name-validator@4.0.0: {}
|
||||||
|
|
||||||
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@4.0.3: {}
|
y18n@4.0.3: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user