Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8f14e42
基于mysql数据库
dylanXM Feb 25, 2026
c9bcd97
合并main
dylanXM Feb 25, 2026
22672e4
feat: 合并main
dylanXM Feb 27, 2026
82365fa
feat: 设置smtp和mysql
dylanXM Feb 27, 2026
6ccedf0
feat: 解决冲突
dylanXM Feb 27, 2026
3e3f567
Merge branch 'main' into dev
dylanXM Feb 28, 2026
67868bf
feat: 合并main
dylanXM Feb 28, 2026
33de177
合并main
dylanXM Mar 1, 2026
1773730
Merge branch 'dev' of github.com:dylanXM/real-time-fund into dev
dylanXM Mar 1, 2026
9ca07d6
登录
dylanXM Mar 2, 2026
8cd03ce
更新基金
dylanXM Mar 3, 2026
fc23ec1
Merge branch 'main' into dev
dylanXM Mar 3, 2026
4f66e24
Merge branch 'main' into dev
dylanXM Mar 3, 2026
5cc0c4c
更新基金
dylanXM Mar 3, 2026
f26304e
删除打印语句
dylanXM Mar 3, 2026
d0e529a
修复listener
dylanXM Mar 4, 2026
831a505
Merge branch 'main' into dev
dylanXM Mar 4, 2026
7a7fad1
合并main
dylanXM Mar 4, 2026
e3fbfb4
合并main
dylanXM Mar 4, 2026
46ce22a
Merge branch 'main' into dev
dylanXM Mar 4, 2026
d579efa
更新docker
dylanXM Mar 5, 2026
6adbd7a
merge main
dylanXM Mar 5, 2026
00cbada
设置时区
dylanXM Mar 5, 2026
867070e
设置时区
dylanXM Mar 5, 2026
1206ce0
设置时区
dylanXM Mar 5, 2026
ebabe5e
合并main
dylanXM Mar 7, 2026
25d2754
更新付款码
dylanXM Mar 7, 2026
a80f804
feat: 更新项目名称
dylanXM Mar 7, 2026
dd67841
feat: 更新接口
dylanXM Mar 7, 2026
d6d4001
Merge branch 'main' into dev
dylanXM Mar 8, 2026
b28544e
删除通知
dylanXM Mar 8, 2026
5e5f923
Merge branch 'main' into dev
dylanXM Mar 8, 2026
9785771
合并main
dylanXM Mar 8, 2026
dcdffb2
删除原icon
dylanXM Mar 9, 2026
d74ab1b
合并main
dylanXM Mar 9, 2026
7471e8b
更换端口号
dylanXM Mar 10, 2026
6cdad5f
合并main
dylanXM Mar 10, 2026
c8a6def
更新图标
dylanXM Mar 10, 2026
f8dc779
更新port
dylanXM Mar 10, 2026
da5b95b
合并main
dylanXM Mar 11, 2026
a49b453
修复viewMode
dylanXM Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# MySQL 数据库配置
MYSQL_HOST=43.138.202.234
MYSQL_PORT=3306
MYSQL_USER=fund
MYSQL_PASSWORD=Yang19960223
MYSQL_DATABASE=fund

# JWT 密钥(生产环境请使用强密码)
JWT_SECRET=your-strong-jwt-secret-key-change-in-production

# SMTP 邮件配置(用于发送验证码)
SMTP_HOST=smtp.163.com
SMTP_PORT=465
SMTP_USER=dylan_xm@163.com
SMTP_PASS=PAdZa7x4X5nLftat

# 应用配置
NEXT_PUBLIC_APP_NAME=基金实时展示

# web3forms 配置
# 从 web3forms 中获取这些值 https://app.web3forms.com/dashboard
NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY=

# Google Analytics 配置
# 从 Google Analytics 中获取这些值 https://analytics.google.com/analytics/web/
NEXT_PUBLIC_GA_ID=

# GitHub Release 检查配置
# 若需要在页面中展示「发现新版本」更新提示,请配置为对应仓库的最新 Release 接口地址
# 例如本仓库默认值:
# https://api.github.com/repos/hzm0321/real-time-fund/releases/latest
NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL=
28 changes: 13 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
# ===== 构建阶段 =====
FROM node:22-bullseye AS builder

# 设置时区
ENV TZ=Asia/Shanghai

WORKDIR /app
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ARG NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY
ARG NEXT_PUBLIC_GA_ID
ARG NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY=$NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY
ENV NEXT_PUBLIC_GA_ID=$NEXT_PUBLIC_GA_ID
ENV NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL=$NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL
COPY package*.json ./
RUN npm install --legacy-peer-deps
COPY . .
RUN npx next build
# ===== 运行阶段 =====
FROM node:22-bullseye AS runner

# 设置时区
ENV TZ=Asia/Shanghai
RUN apt-get update && apt-get install -y tzdata && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
rm -rf /var/lib/apt/lists/*

WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY=$NEXT_PUBLIC_WEB3FORMS_ACCESS_KEY
ENV NEXT_PUBLIC_GA_ID=$NEXT_PUBLIC_GA_ID
ENV NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL=$NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL
ENV HOSTNAME=0.0.0.0
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000 || exit 1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ docker run -d -p 3000:3000 --name fund real-time-fund
#### docker-compose(会读取同目录 `.env` 作为 build-arg 与运行环境)
```bash
# 建议先:cp env.example .env 并编辑 .env
docker compose up -d
docker compose up -d --build
```

## 📖 使用说明
Expand Down
38 changes: 38 additions & 0 deletions app/api/auth/send-otp/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NextResponse } from 'next/server';
import { createOTP } from '../../../lib/auth';
import { sendOTPEmail } from '../../../lib/email';

export async function POST(request) {
try {
const { email } = await request.json();

if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json(
{ error: '请输入有效的邮箱地址' },
{ status: 400 }
);
}

const code = await createOTP(email);

try {
await sendOTPEmail(email, code);
return NextResponse.json({ success: true, message: '验证码已发送到您的邮箱' });
} catch (emailError) {
console.error('Failed to send OTP email:', emailError);
// 即使邮件发送失败,也返回成功,因为验证码已经生成并存储
// 这样用户可以继续使用验证码进行验证
return NextResponse.json({
success: true,
message: '验证码生成成功,但邮件发送失败,请检查邮箱配置',
code: process.env.NODE_ENV === 'development' ? code : undefined
});
}
} catch (error) {
console.error('Send OTP error:', error);
return NextResponse.json(
{ error: '服务器错误' },
{ status: 500 }
);
}
}
31 changes: 31 additions & 0 deletions app/api/auth/session/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { verifySession } from '../../../lib/auth';

export async function GET(request) {
try {
const token = request.cookies.get('auth_token')?.value;

if (!token) {
return NextResponse.json({ user: null });
}

const session = await verifySession(token);

if (!session) {
const response = NextResponse.json({ user: null });
response.cookies.delete('auth_token');
return response;
}

return NextResponse.json({
user: {
id: session.userId,
email: session.email,
emailVerified: session.emailVerified
}
});
} catch (error) {
console.error('Get session error:', error);
return NextResponse.json({ user: null });
}
}
29 changes: 29 additions & 0 deletions app/api/auth/signout/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { deleteSession, deleteUserSessions, verifySession } from '../../../lib/auth';

export async function POST(request) {
try {
const token = request.cookies.get('auth_token')?.value;
const { scope } = await request.json().catch(() => ({}));

if (token) {
if (scope === 'global') {
const payload = await verifySession(token);
if (payload?.userId) {
await deleteUserSessions(payload.userId);
}
} else {
await deleteSession(token);
}
}

const response = NextResponse.json({ success: true });
response.cookies.delete('auth_token');
return response;
} catch (error) {
console.error('Sign out error:', error);
const response = NextResponse.json({ success: true });
response.cookies.delete('auth_token');
return response;
}
}
48 changes: 48 additions & 0 deletions app/api/auth/verify-otp/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server';
import { verifyOTP, createOrUpdateUser, createSession } from '../../../lib/auth';

export async function POST(request) {
try {
const { email, code } = await request.json();

if (!email || !code) {
return NextResponse.json(
{ error: '请输入邮箱和验证码' },
{ status: 400 }
);
}

const isValid = await verifyOTP(email, code);

if (!isValid) {
return NextResponse.json(
{ error: '验证码无效或已过期' },
{ status: 400 }
);
}

const userId = await createOrUpdateUser(email);
const token = await createSession(userId);

const response = NextResponse.json({
success: true,
user: { id: userId, email }
});

response.cookies.set('auth_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 30 * 24 * 60 * 60,
path: '/',
});

return response;
} catch (error) {
console.error('Verify OTP error:', error);
return NextResponse.json(
{ error: '服务器错误' },
{ status: 500 }
);
}
}
131 changes: 131 additions & 0 deletions app/api/config/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { NextResponse } from 'next/server';
import { verifySession } from '../../lib/auth';
import { query } from '../../lib/db';

export async function GET(request) {
try {
const token = request.cookies.get('auth_token')?.value;

if (!token) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
}

const session = await verifySession(token);

if (!session) {
return NextResponse.json({ error: '会话已过期' }, { status: 401 });
}

const results = await query(
'SELECT data, updated_at FROM user_configs WHERE user_id = ?',
[session.userId]
);

if (results.length === 0) {
return NextResponse.json({ data: null });
}

return NextResponse.json({
data: results[0].data,
updatedAt: results[0].updated_at
});
} catch (error) {
console.error('Get config error:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}

export async function POST(request) {
try {
const token = request.cookies.get('auth_token')?.value;

if (!token) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
}

const session = await verifySession(token);

if (!session) {
return NextResponse.json({ error: '会话已过期' }, { status: 401 });
}

const { data } = await request.json();

if (!data) {
return NextResponse.json({ error: '数据不能为空' }, { status: 400 });
}

const existing = await query(
'SELECT id FROM user_configs WHERE user_id = ?',
[session.userId]
);

if (existing.length > 0) {
await query(
'UPDATE user_configs SET data = ?, updated_at = NOW() WHERE user_id = ?',
[JSON.stringify(data), session.userId]
);
} else {
await query(
'INSERT INTO user_configs (user_id, data) VALUES (?, ?)',
[session.userId, JSON.stringify(data)]
);
}

return NextResponse.json({ success: true });
} catch (error) {
console.error('Save config error:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}

export async function PATCH(request) {
try {
const token = request.cookies.get('auth_token')?.value;

if (!token) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
}

const session = await verifySession(token);

if (!session) {
return NextResponse.json({ error: '会话已过期' }, { status: 401 });
}

const { data: partialData } = await request.json();

if (!partialData) {
return NextResponse.json({ error: '数据不能为空' }, { status: 400 });
}

const existing = await query(
'SELECT data FROM user_configs WHERE user_id = ?',
[session.userId]
);

let mergedData = partialData;

if (existing.length > 0) {
const currentData = existing[0].data || {};
mergedData = { ...currentData, ...partialData };
}

if (existing.length > 0) {
await query(
'UPDATE user_configs SET data = ?, updated_at = NOW() WHERE user_id = ?',
[JSON.stringify(mergedData), session.userId]
);
} else {
await query(
'INSERT INTO user_configs (user_id, data) VALUES (?, ?)',
[session.userId, JSON.stringify(mergedData)]
);
}

return NextResponse.json({ success: true });
} catch (error) {
console.error('Patch config error:', error);
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
}
Loading