From 812313c37f6d8dde60c7fccb1412da995994fd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84=E7=94=A8=E6=88=B7=E5=90=8D?= <你的邮箱> Date: Thu, 6 Nov 2025 23:16:58 +0800 Subject: [PATCH] fix: create schema before postgres import --- .gitea/workflows/deploy.yml | 84 +++++---- apps/backend/scripts/import-finance-data.js | 179 +++++++++++++++++++- 2 files changed, 212 insertions(+), 51 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 18e1b8d7..2296ae91 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -190,56 +190,52 @@ jobs: - name: Health Check if: success() - run: | - echo "🔍 执行健康检查..." + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.SERVER_HOST || '172.16.74.149' }} + username: ${{ secrets.SERVER_USER || 'atai' }} + password: ${{ secrets.SERVER_PASSWORD || 'wengewudi666808' }} + port: ${{ secrets.SERVER_PORT || '22' }} + command_timeout: 10m + script: | + set -e + echo "🔍 执行健康检查..." + sleep 20 - # 等待服务完全启动(延长等待时间) - sleep 20 - - # 健康检查(增加重试次数和诊断信息) - for i in {1..10}; do - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "尝试 $i/10: 检查服务 ${{ env.HEALTH_CHECK_URL }}" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # 详细的curl诊断 - HTTP_CODE=$(curl -v -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 ${{ env.HEALTH_CHECK_URL }} 2>&1) - echo "响应: $HTTP_CODE" - - if echo "$HTTP_CODE" | grep -q "200\|301\|302"; then - echo "✅ 服务健康检查通过!HTTP状态码正常" + for i in {1..10}; do echo "" - echo "🎉 部署成功!服务已正常运行" - exit 0 - fi + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "尝试 ${i}/10: 检查服务 ${{ env.HEALTH_CHECK_URL }}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - # 如果失败,显示更多诊断信息 - if [ $i -eq 5 ]; then - echo "" - echo "⚠️ 第5次尝试失败,执行深度诊断..." - echo "" - echo "🔍 检查容器运行状态:" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER || 'atai' }}@${{ secrets.SERVER_HOST || '172.16.74.149' }} "cd /home/atai/kt-financial-system && sudo docker-compose ps" || true - echo "" - echo "📝 最新容器日志:" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER || 'atai' }}@${{ secrets.SERVER_HOST || '172.16.74.149' }} "cd /home/atai/kt-financial-system && sudo docker-compose logs --tail=50" || true - fi + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 ${{ env.HEALTH_CHECK_URL }} || true) + echo "响应: ${HTTP_CODE}" + + if printf "%s" "$HTTP_CODE" | grep -qE "200|301|302"; then + echo "✅ 服务健康检查通过!HTTP状态码正常" + echo "" + echo "🎉 部署成功!服务已正常运行" + exit 0 + fi + + if [ "$i" -eq 5 ]; then + echo "" + echo "⚠️ 第5次尝试失败,执行深度诊断..." + echo "" + echo "🔍 检查容器运行状态:" + cd /home/atai/kt-financial-system + sudo docker-compose ps || true + echo "" + echo "📝 最新容器日志:" + sudo docker-compose logs --tail=50 || true + fi - if [ $i -lt 10 ]; then - echo "⏳ 等待6秒后重试..." sleep 6 - fi - done + done - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "❌ 健康检查失败:10次尝试均未成功" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "🔍 最终诊断信息:" - ssh -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER || 'atai' }}@${{ secrets.SERVER_HOST || '172.16.74.149' }} "cd /home/atai/kt-financial-system && sudo docker-compose ps && echo '---' && sudo docker-compose logs --tail=100" || true - exit 1 + echo "" + echo "❌ 服务健康检查失败:无法在多次重试后获得 200/301/302 响应" + exit 1 - name: Send notification on success if: success() diff --git a/apps/backend/scripts/import-finance-data.js b/apps/backend/scripts/import-finance-data.js index df2e3299..575a6a51 100644 --- a/apps/backend/scripts/import-finance-data.js +++ b/apps/backend/scripts/import-finance-data.js @@ -231,6 +231,7 @@ function normalizeDate(rawValue, monthTracker, baseYear) { } async function resetFinanceTables(client) { + await ensureSchema(client); await client.query(` TRUNCATE TABLE finance_transactions, @@ -258,6 +259,165 @@ async function ensureCurrency(client, cache, code, name = code, symbol = code) { cache.currencies.add(code); } +async function ensureSchema(client) { + 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); + `); +} + async function ensureExchangeRate( client, cache, @@ -463,6 +623,17 @@ async function importNewFormat(client, header, rows, cache) { throw new Error('CSV 表头缺少必需字段,无法导入新版格式数据'); } + await ensureCurrency(client, cache, 'CNY', '人民币', '¥'); + await ensureExchangeRate( + client, + cache, + 'CNY', + 'CNY', + 1, + '1970-01-01', + 'system', + ); + const transactions = []; for (const columns of rows) { @@ -487,13 +658,7 @@ async function importNewFormat(client, header, rows, cache) { const accountIcon = resolveCurrencyIcon(currency); const accountColor = resolveCurrencyColor(currency); - await ensureCurrency( - client, - cache, - currency, - currencyName, - currencySymbol, - ); + await ensureCurrency(client, cache, currency, currencyName, currencySymbol); await ensureExchangeRate(client, cache, currency, 'CNY', 1, date); const accountName = columns[indexMap.account]?.trim() || '默认账户';