diff --git a/client/.env.production b/client/.env.production new file mode 100644 index 0000000..02fd38f --- /dev/null +++ b/client/.env.production @@ -0,0 +1,2 @@ +# 构建时设置API地址(内网穿透地址) +# VITE_API_BASE_URL=https://your-ngrok-url/api diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index d1b486b..5a1fc3e 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -1,7 +1,7 @@ import axios from 'axios' const api = axios.create({ - baseURL: '/api', + baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 30000, }) diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index a3a5140..f18a4f5 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -9,7 +9,7 @@ interface LoginPageProps { export function LoginPage({ onLogin }: LoginPageProps) { const navigate = useNavigate() const [username, setUsername] = useState('总部管理员') - const [password, setPassword] = useState('123') + const [password, setPassword] = useState('') const [error, setError] = useState('') const handleLogin = async () => { @@ -34,9 +34,11 @@ export function LoginPage({ onLogin }: LoginPageProps) { setUsername(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleLogin()} className="mt-1 w-full rounded-md border px-3 py-2 text-sm" /> + {error &&
{error}
}{error}
} diff --git a/client/vite.config.ts b/client/vite.config.ts index 092bbb6..d8d0867 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ port: 5173, proxy: { '/api': { - target: 'http://localhost:3001', + target: 'http://localhost:3333', changeOrigin: true, }, }, diff --git a/run.md b/run.md index 3bb963a..497a31d 100644 --- a/run.md +++ b/run.md @@ -1 +1,10 @@ -当前数据库是本机Homebrew PostgreSQL 15,数据库名为 bill_query,端口5432,仅监听 localhost;当前用户 freedak 是超级用户,本机认证方式为 trust。这适合本机开发,但不适合直接照搬到服务器生产环境。前端浏览器也不应直接连接PostgreSQL,必须通过后端API访问。 \ No newline at end of file +当前数据库是本机Homebrew PostgreSQL 15,数据库名为 bill_query,端口5432,仅监听 localhost;当前用户 freedak 是超级用户,本机认证方式为 trust。这适合本机开发,但不适合直接照搬到服务器生产环境。前端浏览器也不应直接连接PostgreSQL,必须通过后端API访问。 + +## 登录账号 + +| 用户名 | 密码 | 角色 | +|--------|------|------| +| 总部管理员 | 123 | hq | +| 区域经理 | 123 | regional | +| 潘家园店长 | 123 | store | +| 商品部 | 123 | dept | \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 8c1a28f..2d5ca25 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -8,10 +8,10 @@ import dataRoutes from './routes/data.js' import taskRoutes from './routes/tasks.js' const app = express() -const PORT = parseInt(process.env.PORT || '3001') +const PORT = parseInt(process.env.PORT || '3333') app.use(cors({ - origin: process.env.CLIENT_URL || 'http://localhost:5173', + origin: process.env.CLIENT_URL || true, credentials: true, })) app.use(express.json()) diff --git a/server/src/routes/data.ts b/server/src/routes/data.ts index 0a4f4f8..7879496 100644 --- a/server/src/routes/data.ts +++ b/server/src/routes/data.ts @@ -8,20 +8,7 @@ const router = Router() router.get('/overview', async (req: AuthRequest, res) => { try { const month = parseMonth(req) - const sql = ` - SELECT count(*) AS bill_count, - round(sum(received_total), 2) AS received, - round(sum(received_total) / count(*), 2) AS avg_bill_value, - round(sum(discount_total) / nullif(sum(consumption), 0) * 100, 2) AS discount_rate_pct, - round(sum(theoretical_profit) / nullif(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct, - count(*) FILTER (WHERE member_id IS NOT NULL) AS member_bills, - round(count(*) FILTER (WHERE member_id IS NOT NULL)::numeric / count(*) * 100, 2) AS member_share_pct - FROM analytics.bill_fact - WHERE closed_at >= $1::date - AND closed_at < ($1::date + interval '1 month') - AND closed_at IS NOT NULL - ` - const result = await query(sql, [month]) + const result = await query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month]) sendSuccess(res, result.rows[0]) } catch (err: any) { sendError(res, err.message) @@ -31,20 +18,7 @@ router.get('/overview', async (req: AuthRequest, res) => { router.get('/overview/daily', async (req: AuthRequest, res) => { try { const month = parseMonth(req) - const sql = ` - SELECT closed_at::date AS business_date, - count(*) AS bill_count, - round(sum(received_total), 2) AS received, - round(sum(received_total) / count(*), 2) AS avg_bill_value, - round(sum(discount_total) / nullif(sum(consumption), 0) * 100, 2) AS discount_rate_pct - FROM analytics.bill_fact - WHERE closed_at >= $1::date - AND closed_at < ($1::date + interval '1 month') - AND closed_at IS NOT NULL - GROUP BY closed_at::date - ORDER BY business_date - ` - const result = await query(sql, [month]) + const result = await query(`SELECT business_date, bill_count, received, avg_bill_value, discount_rate_pct FROM analytics.mv_overview_daily WHERE month = $1::date ORDER BY business_date`, [month]) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) @@ -63,7 +37,7 @@ router.get('/stores', async (req: AuthRequest, res) => { if (riskLevel) { sql = `SELECT s.* FROM analytics.v_store_scorecard s - JOIN analytics.v_store_risk_rating r ON s.store_code = r.store_code + JOIN analytics.mv_store_risk_rating r ON s.store_code = r.store_code WHERE r.risk_level = $1` params.push(riskLevel) } @@ -94,7 +68,7 @@ router.get('/stores', async (req: AuthRequest, res) => { router.get('/stores/risk', async (req: AuthRequest, res) => { try { - const result = await query(`SELECT * FROM analytics.v_store_risk_rating ORDER BY risk_level, received DESC`) + const result = await query(`SELECT * FROM analytics.mv_store_risk_rating ORDER BY risk_level, received DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) @@ -136,7 +110,7 @@ router.get('/stores/:code', async (req: AuthRequest, res) => { const code = req.params.code const [scorecard, risk, platform, benchmark, action, execution] = await Promise.all([ query(`SELECT * FROM analytics.v_store_scorecard WHERE store_code = $1`, [code]), - query(`SELECT * FROM analytics.v_store_risk_rating WHERE store_code = $1`, [code]), + query(`SELECT * FROM analytics.mv_store_risk_rating WHERE store_code = $1`, [code]), query(`SELECT * FROM analytics.v_store_platform_economics WHERE store_code = $1`, [code]), query(`SELECT * FROM analytics.v_store_benchmark WHERE store_code = $1`, [code]), query(`SELECT * FROM analytics.v_store_action_list WHERE store_code = $1`, [code]), @@ -313,7 +287,7 @@ router.get('/marketing/plans', async (req: AuthRequest, res) => { router.get('/benchmark/composite', async (req: AuthRequest, res) => { try { - const result = await query(`SELECT * FROM analytics.v_store_benchmark_composite ORDER BY benchmark_score DESC`) + const result = await query(`SELECT * FROM analytics.mv_store_benchmark_composite ORDER BY benchmark_score DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) @@ -445,7 +419,7 @@ router.get('/stores/:code/anomalies', async (req: AuthRequest, res) => { // 区域汇总 router.get('/region/summary', async (req, res) => { try { - const result = await query(`SELECT * FROM analytics.v_region_summary ORDER BY total_received DESC`) + const result = await query(`SELECT * FROM analytics.mv_region_summary ORDER BY total_received DESC`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) } }) diff --git a/server/src/routes/tasks.ts b/server/src/routes/tasks.ts index 92cb6ff..4294364 100644 --- a/server/src/routes/tasks.ts +++ b/server/src/routes/tasks.ts @@ -117,7 +117,7 @@ router.get('/monthly-review', async (req: AuthRequest, res) => { router.get('/followup', async (req: AuthRequest, res) => { try { - const result = await query(`SELECT * FROM analytics.v_store_monthly_followup ORDER BY priority, store_code`) + const result = await query(`SELECT * FROM analytics.mv_store_monthly_followup ORDER BY priority, store_code`) sendSuccess(res, result.rows) } catch (err: any) { sendError(res, err.message) @@ -247,7 +247,7 @@ router.post('/indicators', async (req: AuthRequest, res) => { router.get('/loop-health', async (req: AuthRequest, res) => { try { - const result = await query(`SELECT * FROM analytics.v_loop_health`) + const result = await query(`SELECT * FROM analytics.mv_loop_health`) sendSuccess(res, result.rows[0] || { task_generation_rate: 0, store_execution_rate: 0,