feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
+300
View File
@@ -0,0 +1,300 @@
const express = require('express')
const Database = require('better-sqlite3')
const path = require('path')
const fs = require('fs')
const crypto = require('crypto')
const dataDir = path.join(process.cwd(), 'data')
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
const dbPath = path.join(dataDir, 'rebalance.db')
const db = new Database(dbPath)
// Initialize Tables
db.exec(`CREATE TABLE IF NOT EXISTS weeks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
position_json TEXT NOT NULL,
plan_json TEXT NOT NULL,
asset_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(start_date, end_date)
);`)
db.exec(`CREATE TABLE IF NOT EXISTS excluded_stocks (
code TEXT PRIMARY KEY,
name TEXT
);`)
db.exec(`CREATE TABLE IF NOT EXISTS fund_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
data_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(start_date, end_date)
);`)
db.exec(`CREATE TABLE IF NOT EXISTS fund_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
currency TEXT,
balance REAL,
available REAL,
reference_value REAL,
asset_value REAL,
profit REAL,
day_profit REAL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(start_date, end_date)
);`)
// Auth Helpers (Duplicated from index.js to avoid refactoring)
const readCookieCfg = () => {
const cfgPath = path.join(process.cwd(), 'public', 'tools', 'rebalance', 'assets', 'auth_config.json')
let name = 'rebalance_gate'
let maxAge = 90 * 24 * 3600 * 1000
try {
if (fs.existsSync(cfgPath)) {
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
if (cfg.cookieName) name = String(cfg.cookieName)
const days = Number(cfg.max_age_days || 0)
if (Number.isFinite(days) && days > 0) maxAge = Math.min(days, 365) * 24 * 3600 * 1000
}
} catch {}
return { name, maxAge }
}
const parseCookie = (cookieStr) => {
const out = {}
String(cookieStr || '').split(/;\s*/).forEach(p => { const idx = p.indexOf('='); if (idx > 0) out[p.slice(0, idx)] = decodeURIComponent(p.slice(idx + 1)) })
return out
}
const checkAuth = (req, res, next) => {
try {
const cookies = parseCookie(req.headers.cookie || '')
const { name, maxAge } = readCookieCfg()
if (cookies[name] === '1') {
res.cookie(name, '1', { httpOnly: true, sameSite: 'lax', maxAge })
return next()
}
} catch {}
return res.status(401).json({ error: 'unauthorized' })
}
const bindRoutes = (app) => {
// Rebalance API Routes
// Note: These routes are mounted at root level by bindRoutes to match original paths /api/...
// OR we can prefix them. The original app used /api/...
// Since Toolbox uses /api for other things, we need to be careful.
// Rebalance specific routes:
// /api/weekly-data
// /api/fund-shares
// /api/settings/excluded-stocks
// Auth endpoints for Rebalance frontend compatibility
app.get('/auth/handshake', (req, res) => {
try {
const cookies = parseCookie(req.headers.cookie || '')
const { name } = readCookieCfg()
if (cookies[name] === '1') return res.json({ ok: true })
return res.status(401).json({ error: 'unauthorized' })
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) })
}
})
// We rely on Toolbox's main auth mechanism for setting the cookie.
// But if the user hits this endpoint, we can just check if they are already auth'd.
app.get('/auth/token', (req, res) => {
// This is just a dummy to satisfy Rebalance frontend if it calls it.
// In simplified mode, we expect the user to be logged in via Toolbox.
const cookies = parseCookie(req.headers.cookie || '')
const { name } = readCookieCfg()
if (cookies[name] === '1') return res.json({ ok: true })
return res.status(401).json({ error: 'unauthorized' })
})
app.get('/api/weekly-data', checkAuth, (req, res) => {
const { start, end } = req.query
if (!start || !end) return res.status(400).json({ error: 'missing params' })
try {
const row = db.prepare('SELECT position_json, plan_json, asset_json FROM weeks WHERE start_date=? AND end_date=?').get(start, end)
if (!row) return res.status(404).json({ error: 'not found' })
res.json({
positionData: JSON.parse(row.position_json),
planData: JSON.parse(row.plan_json),
assetData: JSON.parse(row.asset_json)
})
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.post('/api/weekly-data', checkAuth, (req, res) => {
const { weekStart, weekEnd, positionData, planData, assetData, overwrite } = req.body
if (!weekStart || !weekEnd || !Array.isArray(positionData) || !Array.isArray(planData) || !Array.isArray(assetData)) {
return res.status(400).json({ error: 'invalid payload' })
}
const now = new Date().toISOString()
try {
const exists = db.prepare('SELECT id FROM weeks WHERE start_date=? AND end_date=?').get(weekStart, weekEnd)
if (exists && !overwrite) return res.status(409).json({ error: 'exists' })
if (exists && overwrite) {
db.prepare('UPDATE weeks SET position_json=?, plan_json=?, asset_json=?, updated_at=? WHERE id=?')
.run(JSON.stringify(positionData), JSON.stringify(planData), JSON.stringify(assetData), now, exists.id)
return res.json({ status: 'updated' })
}
db.prepare('INSERT INTO weeks (start_date, end_date, position_json, plan_json, asset_json, created_at, updated_at) VALUES (?,?,?,?,?,?,?)')
.run(weekStart, weekEnd, JSON.stringify(positionData), JSON.stringify(planData), JSON.stringify(assetData), now, now)
res.json({ status: 'saved' })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.get('/api/fund-shares', checkAuth, (req, res) => {
const { start, end } = req.query
if (!start || !end) return res.status(400).json({ error: 'missing params' })
try {
const row = db.prepare('SELECT data_json FROM fund_shares WHERE start_date=? AND end_date=?').get(start, end)
if (!row) return res.status(404).json({ error: 'not found' })
let payload = null
try { payload = JSON.parse(row.data_json) } catch (e) { payload = null }
let data = []
let summaryJson = null
if (Array.isArray(payload)) data = payload
else if (payload && typeof payload === 'object') { data = payload.data || []; summaryJson = payload.summary || null }
let summary = null
const srow = db.prepare('SELECT currency, balance, available, reference_value, asset_value, profit, day_profit FROM fund_summaries WHERE start_date=? AND end_date=?').get(start, end)
if (srow) {
summary = {
币种: srow.currency || null,
余额: typeof srow.balance === 'number' ? srow.balance : null,
可用: typeof srow.available === 'number' ? srow.available : null,
参考市值: typeof srow.reference_value === 'number' ? srow.reference_value : null,
资产: typeof srow.asset_value === 'number' ? srow.asset_value : null,
盈亏: typeof srow.profit === 'number' ? srow.profit : null,
当日盈亏: typeof srow.day_profit === 'number' ? srow.day_profit : null
}
}
if (!summary) summary = summaryJson || null
res.json({ data, summary })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.post('/api/fund-shares', checkAuth, (req, res) => {
const { weekStart, weekEnd, data, summary, overwrite } = req.body
if (!weekStart || !weekEnd || !Array.isArray(data)) {
return res.status(400).json({ error: 'invalid payload' })
}
const now = new Date().toISOString()
try {
const exists = db.prepare('SELECT id FROM fund_shares WHERE start_date=? AND end_date=?').get(weekStart, weekEnd)
if (exists && !overwrite) return res.status(409).json({ error: 'exists' })
if (exists && overwrite) {
db.prepare('UPDATE fund_shares SET data_json=?, updated_at=? WHERE id=?')
.run(JSON.stringify(data), now, exists.id)
// upsert summary
if (summary && typeof summary === 'object') {
const sexists = db.prepare('SELECT id FROM fund_summaries WHERE start_date=? AND end_date=?').get(weekStart, weekEnd)
const vals = [summary.币种 || null, summary.余额 ?? null, summary.可用 ?? null, summary.参考市值 ?? null, summary.资产 ?? null, summary.盈亏 ?? null, summary.当日盈亏 ?? null, now]
if (sexists) {
db.prepare('UPDATE fund_summaries SET currency=?, balance=?, available=?, reference_value=?, asset_value=?, profit=?, day_profit=?, updated_at=? WHERE id=?')
.run(...vals, sexists.id)
} else {
db.prepare('INSERT INTO fund_summaries (start_date, end_date, currency, balance, available, reference_value, asset_value, profit, day_profit, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)')
.run(weekStart, weekEnd, ...vals, now)
}
}
return res.json({ status: 'updated' })
}
db.prepare('INSERT INTO fund_shares (start_date, end_date, data_json, created_at, updated_at) VALUES (?,?,?,?,?)')
.run(weekStart, weekEnd, JSON.stringify(data), now, now)
if (summary && typeof summary === 'object') {
db.prepare('INSERT INTO fund_summaries (start_date, end_date, currency, balance, available, reference_value, asset_value, profit, day_profit, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)')
.run(weekStart, weekEnd, summary.币种 || null, summary.余额 ?? null, summary.可用 ?? null, summary.参考市值 ?? null, summary.资产 ?? null, summary.盈亏 ?? null, summary.当日盈亏 ?? null, now, now)
}
res.json({ status: 'saved' })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.delete('/api/fund-shares', checkAuth, (req, res) => {
const { start, end } = req.query
let password = null
try { password = (req.body && req.body.password) || null } catch (e) {}
if (!start || !end) return res.status(400).json({ error: 'missing params' })
if (password !== (process.env.SYSTEM_AUTH_PASS_93220 || '')) return res.status(403).json({ error: 'forbidden' })
try {
db.prepare('DELETE FROM fund_shares WHERE start_date=? AND end_date=?').run(start, end)
res.json({ status: 'deleted' })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.post('/api/fund-shares/delete', checkAuth, (req, res) => {
const { start, end, password } = req.body || {}
if (!start || !end) return res.status(400).json({ error: 'missing params' })
if (password !== (process.env.SYSTEM_AUTH_PASS_93220 || '')) return res.status(403).json({ error: 'forbidden' })
try {
db.prepare('DELETE FROM fund_shares WHERE start_date=? AND end_date=?').run(start, end)
res.json({ status: 'deleted' })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.post('/api/fund-shares/clear-all', checkAuth, (req, res) => {
const { password } = req.body || {}
if (password !== (process.env.SYSTEM_AUTH_PASS_93220 || '')) return res.status(403).json({ error: 'forbidden' })
try {
db.exec('DELETE FROM fund_shares')
db.exec('DELETE FROM fund_summaries')
res.json({ status: 'all_deleted' })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.get('/api/settings/excluded-stocks', checkAuth, (req, res) => {
try {
const rows = db.prepare('SELECT code FROM excluded_stocks').all()
res.json({ codes: rows.map((r) => r.code) })
} catch (e) {
res.status(500).json({ error: e && e.message ? e.message : 'db error' })
}
})
app.put('/api/settings/excluded-stocks', checkAuth, (req, res) => {
const { codes } = req.body
if (!Array.isArray(codes)) return res.status(400).json({ error: 'invalid payload' })
try {
db.transaction(() => {
db.exec('DELETE FROM excluded_stocks')
const stmt = db.prepare('INSERT OR REPLACE INTO excluded_stocks (code) VALUES (?)')
codes.forEach((code) => {
if (typeof code === 'string' && code.trim()) {
stmt.run(code.trim())
}
})
})()
res.json({ status: 'saved', codes })
} catch (e) {
res.status(500).json({ error: 'db error' })
}
})
}
module.exports = { bindRoutes }