添加数据库集成和用户认证功能

- 新增用户注册和登录系统 (login.html, register.html)
- 集成Supabase数据库连接 (config.js, api.js)
- 完善数据库架构设计 (database-schema.sql)
- 添加部署指南和配置文档 (DEPLOYMENT_GUIDE.md)
- 修复主页面结构和功能完善 (index.html)
- 支持通话记录保存到数据库
- 完整的账单管理和用户认证流程
- 集成OpenAI、Twilio、Stripe等API服务
This commit is contained in:
mars 2025-06-30 19:34:58 +08:00
parent 58665f4bbf
commit 0d57273021
12 changed files with 2559 additions and 4 deletions

View File

@ -6,6 +6,12 @@
<title>翻译服务应用</title>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#4285f4">
<!-- 引入必要的脚本 -->
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<script src="web-app/config.js"></script>
<script src="web-app/api.js"></script>
<style>
* {
margin: 0;
@ -37,9 +43,57 @@
position: relative;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.login-btn-header {
color: white;
text-decoration: none;
padding: 8px 16px;
border: 1px solid white;
border-radius: 20px;
font-size: 14px;
transition: all 0.3s ease;
}
.login-btn-header:hover {
background: white;
color: #4285f4;
}
.user-name {
font-size: 14px;
font-weight: 500;
}
.logout-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.5);
padding: 6px 12px;
border-radius: 15px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.header h1 {
font-size: 20px;
font-weight: 600;
margin: 0;
}
.content {
@ -665,7 +719,16 @@
<body>
<div class="app-container">
<div class="header">
<h1>翻译服务应用</h1>
<div class="header-content">
<h1>翻译服务应用</h1>
<div class="user-info login-required" style="display: none;">
<a href="web-app/login.html" class="login-btn-header">登录</a>
</div>
<div class="user-info user-only" style="display: none;">
<span class="user-name">用户名</span>
<button class="logout-btn" onclick="handleLogout()">登出</button>
</div>
</div>
</div>
<div class="content">
@ -912,6 +975,92 @@
</div>
<script>
// 等待API管理器初始化
let apiManagerReady = false;
// 初始化应用
async function initApp() {
// 等待API管理器初始化
await new Promise(resolve => {
const checkInit = () => {
if (window.apiManager && window.apiManager.supabase) {
apiManagerReady = true;
resolve();
} else {
setTimeout(checkInit, 100);
}
};
checkInit();
});
// 检查登录状态
await checkLoginStatus();
// 如果用户已登录,加载用户数据
if (apiManager.currentUser) {
await loadUserData();
}
}
// 检查登录状态
async function checkLoginStatus() {
if (!apiManagerReady) return;
try {
await apiManager.checkAuthStatus();
} catch (error) {
console.error('检查登录状态失败:', error);
}
}
// 加载用户数据
async function loadUserData() {
try {
// 加载通话记录
const callRecords = await apiManager.getCallRecords();
if (callRecords) {
billHistory = callRecords.map(record => ({
date: new Date(record.created_at).toLocaleString('zh-CN'),
type: record.call_type === 'voice' ? '语音通话' : '视频通话',
duration: Math.ceil(record.duration / 60), // 转换为分钟
amount: record.total_amount,
paid: record.status === 'completed',
hasTranslator: record.has_translator
}));
updateBillHistory();
}
// 加载预约记录
const appointments = await apiManager.getAppointments();
if (appointments) {
// 更新预约数据
console.log('预约记录:', appointments);
}
} catch (error) {
console.error('加载用户数据失败:', error);
}
}
// 登出处理
async function handleLogout() {
try {
const result = await apiManager.logout();
if (result.success) {
// 清空本地数据
billHistory = [];
updateBillHistory();
// 显示登录提示
alert('已成功登出');
} else {
alert('登出失败,请重试');
}
} catch (error) {
console.error('登出失败:', error);
alert('登出失败:' + error.message);
}
}
// 全局变量
let currentTab = 'call';
let isCallActive = false;
@ -1174,7 +1323,7 @@
startCall();
}
function startCall() {
async function startCall() {
if (!currentCallType) return;
isCallActive = true;
@ -1205,7 +1354,7 @@
}, 1000);
}
function endCall() {
async function endCall() {
if (!isCallActive) return;
isCallActive = false;
@ -1234,6 +1383,26 @@
hasTranslator: hasTranslator
};
// 如果用户已登录,保存通话记录到数据库
if (apiManagerReady && apiManager.currentUser) {
try {
const callData = {
type: currentCallType,
duration: callDuration * 60, // 转换为秒
hasTranslator: hasTranslator,
baseRate: baseRate,
translatorRate: translatorRate,
totalAmount: currentBill.amount,
status: 'completed'
};
await apiManager.createCallRecord(callData);
console.log('通话记录已保存到数据库');
} catch (error) {
console.error('保存通话记录失败:', error);
}
}
// 显示账单
showBillModal();
@ -1326,9 +1495,12 @@
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', async function() {
updateBillHistory();
generateCalendar();
// 初始化应用(包括数据库连接和用户状态检查)
await initApp();
});
</script>
</body>

146
web-app/DATABASE_SETUP.md Normal file
View File

@ -0,0 +1,146 @@
# 🗄️ Twilio-project 数据库设置指南
## 📋 概述
本指南将帮你为翻译服务应用设置 Supabase 数据库,包括创建所有必要的表结构、安全策略和索引。
## 🔧 快速设置步骤
### 方法一:手动设置(推荐)
1. **访问 Supabase 控制台**
```
https://supabase.com/dashboard/project/poxwjzdianersitpnvdy
```
2. **进入 SQL Editor**
- 在左侧菜单中点击 "SQL Editor"
- 点击 "New query"
3. **执行初始化脚本**
- 复制 `database-init.sql` 文件的全部内容
- 粘贴到 SQL Editor 中
- 点击 "Run" 执行
4. **验证设置**
- 在左侧菜单点击 "Table Editor"
- 确认以下表已创建:
- ✅ user_profiles
- ✅ translator_profiles
- ✅ call_records
- ✅ appointments
- ✅ document_translations
- ✅ payments
- ✅ system_settings
### 方法二:自动化脚本
1. **安装依赖**
```bash
npm install @supabase/supabase-js
```
2. **获取 Service Role Key**
- 在 Supabase 控制台 → Settings → API
- 复制 "service_role" 密钥
3. **更新脚本配置**
- 编辑 `init-database.js`
- 替换 `YOUR_SERVICE_ROLE_KEY_HERE` 为实际密钥
4. **运行初始化脚本**
```bash
node web-app/init-database.js
```
## 📊 数据库结构
### 核心表结构
| 表名 | 用途 | 主要字段 |
|------|------|----------|
| `user_profiles` | 用户档案 | username, full_name, email, account_balance |
| `translator_profiles` | 翻译员信息 | specializations, languages, hourly_rate, rating |
| `call_records` | 通话记录 | call_type, duration_minutes, total_amount |
| `appointments` | 预约管理 | appointment_date, service_type, status |
| `document_translations` | 文档翻译 | original_filename, status, completion_percentage |
| `payments` | 支付记录 | amount, payment_status, payment_method |
| `system_settings` | 系统配置 | setting_key, setting_value, setting_type |
### 🔒 安全特性
- **行级安全 (RLS)**: 所有表都启用了 RLS
- **用户隔离**: 用户只能访问自己的数据
- **角色权限**: 不同角色有不同的访问权限
- **数据验证**: 表约束确保数据完整性
### 📈 性能优化
- **索引优化**: 为常用查询字段创建索引
- **触发器**: 自动更新时间戳
- **约束检查**: 确保数据有效性
## 🎯 默认系统设置
初始化后会自动创建以下系统设置:
| 设置项 | 值 | 说明 |
|--------|-----|------|
| `voice_call_rate` | 80.00 | 语音通话费率(元/小时) |
| `video_call_rate` | 120.00 | 视频通话费率(元/小时) |
| `translator_rate` | 50.00 | 翻译员费率(元/小时) |
| `min_call_duration` | 1 | 最小通话时长(分钟) |
| `supported_languages` | [多语言数组] | 支持的语言列表 |
| `max_file_size` | 10485760 | 最大文件大小10MB |
| `supported_file_types` | [文件类型数组] | 支持的文件类型 |
## 🔍 验证检查
执行以下 SQL 来验证设置是否成功:
```sql
-- 检查表是否存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public';
-- 检查系统设置
SELECT * FROM system_settings;
-- 检查 RLS 策略
SELECT schemaname, tablename, policyname
FROM pg_policies
WHERE schemaname = 'public';
```
## 🚨 故障排除
### 常见问题
1. **权限错误**
- 确保使用正确的 API 密钥
- 检查用户权限设置
2. **表创建失败**
- 检查 SQL 语法
- 确认没有重复的表名
3. **RLS 策略问题**
- 验证策略语法
- 检查用户认证状态
### 联系支持
如果遇到问题,请检查:
- Supabase 项目状态
- 网络连接
- API 密钥有效性
## ✅ 完成确认
数据库设置完成后,你应该能够:
- ✅ 在 Supabase 控制台看到所有表
- ✅ 系统设置表包含默认值
- ✅ RLS 策略正确应用
- ✅ 应用可以正常连接数据库
现在你可以开始使用翻译服务应用了!🎉

297
web-app/DEPLOYMENT_GUIDE.md Normal file
View File

@ -0,0 +1,297 @@
# 翻译服务应用 - 数据库集成部署指南
## 概述
本指南将帮助您完成翻译服务应用的数据库集成和部署,包括 Supabase 数据库设置、API 配置和应用部署。
## 前置要求
- Supabase 账户
- Stripe 账户(用于支付处理)
- Twilio 账户(用于视频通话)
- OpenAI 账户用于AI翻译
## 1. Supabase 数据库设置
### 1.1 创建 Supabase 项目
1. 访问 [Supabase Dashboard](https://supabase.com/dashboard)
2. 点击 "New Project"
3. 填写项目信息:
- 项目名称:`twilio-translation-app`
- 数据库密码:选择一个强密码
- 区域:选择最近的区域
### 1.2 执行数据库迁移
1. 在 Supabase Dashboard 中,进入 "SQL Editor"
2. 复制 `web-app/database-schema.sql` 文件的内容
3. 粘贴到 SQL Editor 中并执行
4. 确认所有表和触发器创建成功
### 1.3 配置认证设置
1. 进入 "Authentication" → "Settings"
2. 启用 "Enable email confirmations"
3. 设置重定向URL
- Site URL: `http://localhost:8080`
- Redirect URLs: `http://localhost:8080/index.html`
### 1.4 获取 API 密钥
1. 进入 "Settings" → "API"
2. 复制以下信息:
- Project URL
- anon (public) key
- service_role (secret) key
## 2. API 服务配置
### 2.1 Stripe 配置
1. 登录 [Stripe Dashboard](https://dashboard.stripe.com/)
2. 获取 API 密钥:
- Publishable key (用于前端)
- Secret key (用于后端)
3. 配置 Webhook 端点(如需要)
### 2.2 Twilio 配置
1. 登录 [Twilio Console](https://console.twilio.com/)
2. 获取以下信息:
- Account SID
- Auth Token
- API Key SID
- API Key Secret
### 2.3 OpenAI 配置
1. 访问 [OpenAI API](https://platform.openai.com/api-keys)
2. 创建新的 API Key
3. 记录 API Key 和 Organization ID
## 3. 应用配置
### 3.1 更新配置文件
编辑 `web-app/config.js` 文件,填入您的 API 密钥:
```javascript
const CONFIG = {
// Supabase 配置
SUPABASE: {
URL: 'your-supabase-url',
ANON_KEY: 'your-supabase-anon-key'
},
// Stripe 配置
STRIPE: {
PUBLISHABLE_KEY: 'your-stripe-publishable-key'
},
// Twilio 配置
TWILIO: {
ACCOUNT_SID: 'your-twilio-account-sid',
API_KEY_SID: 'your-twilio-api-key-sid'
},
// OpenAI 配置
OPENAI: {
API_KEY: 'your-openai-api-key',
ORGANIZATION_ID: 'your-openai-org-id'
}
};
```
### 3.2 环境变量设置
对于生产环境,建议使用环境变量:
```bash
# Supabase
SUPABASE_URL=your-supabase-url
SUPABASE_ANON_KEY=your-supabase-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-key
# Stripe
STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key
STRIPE_SECRET_KEY=your-stripe-secret-key
# Twilio
TWILIO_ACCOUNT_SID=your-twilio-account-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_API_KEY_SID=your-twilio-api-key-sid
TWILIO_API_KEY_SECRET=your-twilio-api-key-secret
# OpenAI
OPENAI_API_KEY=your-openai-api-key
OPENAI_ORGANIZATION_ID=your-openai-org-id
```
## 4. 本地开发环境
### 4.1 启动本地服务器
```bash
# 使用 Python 启动简单的 HTTP 服务器
python -m http.server 8080
# 或使用 Node.js
npx http-server -p 8080
# 或使用 PHP
php -S localhost:8080
```
### 4.2 访问应用
1. 打开浏览器访问 `http://localhost:8080`
2. 点击 "立即体验" 进入应用
3. 测试注册和登录功能
## 5. 生产环境部署
### 5.1 静态文件托管
推荐使用以下平台之一:
- **Vercel**:
```bash
npx vercel --prod
```
- **Netlify**:
```bash
netlify deploy --prod --dir .
```
- **GitHub Pages**:
推送到 GitHub 仓库并启用 Pages
### 5.2 域名配置
1. 购买域名并配置 DNS
2. 在 Supabase 中更新重定向 URL
3. 更新 CORS 设置
### 5.3 HTTPS 配置
确保生产环境使用 HTTPS
- 大多数托管平台自动提供 SSL 证书
- 更新所有 API 配置使用 HTTPS URL
## 6. 功能测试
### 6.1 用户认证测试
1. 测试用户注册流程
2. 验证邮箱确认功能
3. 测试登录和登出
### 6.2 通话功能测试
1. 测试语音通话启动
2. 测试视频通话启动
3. 验证计费功能
4. 检查数据库记录
### 6.3 数据同步测试
1. 验证通话记录保存
2. 测试账单历史显示
3. 检查用户档案更新
## 7. 监控和维护
### 7.1 日志监控
- 监控 Supabase 日志
- 检查 API 调用错误
- 设置错误告警
### 7.2 性能优化
- 监控数据库查询性能
- 优化图片和资源加载
- 实施缓存策略
### 7.3 备份策略
- 定期备份 Supabase 数据库
- 备份用户上传的文档
- 制定灾难恢复计划
## 8. 故障排除
### 8.1 常见问题
**问题**: 无法连接到 Supabase
- 检查 API URL 和密钥是否正确
- 验证网络连接
- 检查 CORS 设置
**问题**: 用户注册失败
- 检查邮箱格式验证
- 验证 Supabase 认证设置
- 检查密码强度要求
**问题**: 通话记录未保存
- 检查用户登录状态
- 验证数据库连接
- 检查 RLS 策略设置
### 8.2 调试技巧
1. 打开浏览器开发者工具
2. 检查控制台错误信息
3. 监控网络请求状态
4. 验证数据库操作日志
## 9. 安全考虑
### 9.1 API 密钥安全
- 永远不要在前端暴露 secret keys
- 使用环境变量存储敏感信息
- 定期轮换 API 密钥
### 9.2 数据安全
- 启用 RLS (行级安全)
- 实施数据加密
- 定期安全审计
### 9.3 用户隐私
- 遵守数据保护法规
- 实施数据删除功能
- 提供隐私政策
## 10. 扩展功能
### 10.1 移动应用
- 使用 React Native 或 Flutter
- 集成相同的 Supabase 后端
- 实现推送通知
### 10.2 管理后台
- 创建管理员界面
- 实施用户管理功能
- 添加数据分析面板
### 10.3 API 扩展
- 创建 RESTful API
- 实施 GraphQL 接口
- 添加第三方集成
---
## 支持
如需技术支持,请联系:
- 邮箱: support@translation-app.com
- 文档: https://docs.translation-app.com
- GitHub: https://github.com/your-org/translation-app

352
web-app/api.js Normal file
View File

@ -0,0 +1,352 @@
// API 管理文件
class APIManager {
constructor() {
this.supabase = null;
this.currentUser = null;
this.init();
}
// 初始化 Supabase 客户端
async init() {
try {
// 加载 Supabase 客户端
const { createClient } = supabase;
this.supabase = createClient(CONFIG.SUPABASE.URL, CONFIG.SUPABASE.ANON_KEY);
// 检查用户登录状态
await this.checkAuthStatus();
console.log('API Manager 初始化成功');
} catch (error) {
console.error('API Manager 初始化失败:', error);
}
}
// 检查认证状态
async checkAuthStatus() {
try {
const { data: { user } } = await this.supabase.auth.getUser();
this.currentUser = user;
if (user) {
console.log('用户已登录:', user.email);
this.updateUIForLoggedInUser(user);
} else {
console.log('用户未登录');
this.updateUIForLoggedOutUser();
}
} catch (error) {
console.error('检查认证状态失败:', error);
}
}
// 用户注册
async register(email, password, userData = {}) {
try {
const { data, error } = await this.supabase.auth.signUp({
email: email,
password: password,
options: {
data: {
full_name: userData.fullName || '',
phone: userData.phone || '',
...userData
}
}
});
if (error) throw error;
// 创建用户档案
if (data.user) {
await this.createUserProfile(data.user, userData);
}
return { success: true, data: data };
} catch (error) {
console.error('注册失败:', error);
return { success: false, error: error.message };
}
}
// 用户登录
async login(email, password) {
try {
const { data, error } = await this.supabase.auth.signInWithPassword({
email: email,
password: password
});
if (error) throw error;
this.currentUser = data.user;
this.updateUIForLoggedInUser(data.user);
return { success: true, data: data };
} catch (error) {
console.error('登录失败:', error);
return { success: false, error: error.message };
}
}
// 用户登出
async logout() {
try {
const { error } = await this.supabase.auth.signOut();
if (error) throw error;
this.currentUser = null;
this.updateUIForLoggedOutUser();
return { success: true };
} catch (error) {
console.error('登出失败:', error);
return { success: false, error: error.message };
}
}
// 创建用户档案
async createUserProfile(user, userData) {
try {
const { data, error } = await this.supabase
.from('user_profiles')
.insert([
{
id: user.id,
email: user.email,
full_name: userData.fullName || '',
phone: userData.phone || '',
avatar_url: userData.avatarUrl || '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
]);
if (error) throw error;
return data;
} catch (error) {
console.error('创建用户档案失败:', error);
throw error;
}
}
// 获取用户档案
async getUserProfile(userId = null) {
try {
const id = userId || this.currentUser?.id;
if (!id) throw new Error('用户未登录');
const { data, error } = await this.supabase
.from('user_profiles')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
return data;
} catch (error) {
console.error('获取用户档案失败:', error);
throw error;
}
}
// 更新用户档案
async updateUserProfile(updates) {
try {
if (!this.currentUser) throw new Error('用户未登录');
const { data, error } = await this.supabase
.from('user_profiles')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', this.currentUser.id);
if (error) throw error;
return data;
} catch (error) {
console.error('更新用户档案失败:', error);
throw error;
}
}
// 创建通话记录
async createCallRecord(callData) {
try {
if (!this.currentUser) throw new Error('用户未登录');
const { data, error } = await this.supabase
.from('call_records')
.insert([
{
user_id: this.currentUser.id,
call_type: callData.type, // 'voice' or 'video'
duration: callData.duration, // 通话时长(秒)
has_translator: callData.hasTranslator || false,
base_rate: callData.baseRate, // 基础费率
translator_rate: callData.translatorRate || 0, // 翻译费率
total_amount: callData.totalAmount, // 总金额
status: callData.status || 'completed', // 'pending', 'completed', 'cancelled'
created_at: new Date().toISOString()
}
]);
if (error) throw error;
return data;
} catch (error) {
console.error('创建通话记录失败:', error);
throw error;
}
}
// 获取通话记录
async getCallRecords(limit = 50) {
try {
if (!this.currentUser) throw new Error('用户未登录');
const { data, error } = await this.supabase
.from('call_records')
.select('*')
.eq('user_id', this.currentUser.id)
.order('created_at', { ascending: false })
.limit(limit);
if (error) throw error;
return data;
} catch (error) {
console.error('获取通话记录失败:', error);
throw error;
}
}
// 创建预约记录
async createAppointment(appointmentData) {
try {
if (!this.currentUser) throw new Error('用户未登录');
const { data, error } = await this.supabase
.from('appointments')
.insert([
{
user_id: this.currentUser.id,
translator_id: appointmentData.translatorId,
appointment_date: appointmentData.date,
appointment_time: appointmentData.time,
service_type: appointmentData.serviceType, // 'voice', 'video', 'document'
language_pair: appointmentData.languagePair, // '中文-英文'
duration: appointmentData.duration || 60, // 预约时长(分钟)
notes: appointmentData.notes || '',
status: 'pending', // 'pending', 'confirmed', 'completed', 'cancelled'
created_at: new Date().toISOString()
}
]);
if (error) throw error;
return data;
} catch (error) {
console.error('创建预约失败:', error);
throw error;
}
}
// 获取预约记录
async getAppointments() {
try {
if (!this.currentUser) throw new Error('用户未登录');
const { data, error } = await this.supabase
.from('appointments')
.select(`
*,
translator_profiles (
full_name,
avatar_url,
languages,
rating
)
`)
.eq('user_id', this.currentUser.id)
.order('appointment_date', { ascending: true });
if (error) throw error;
return data;
} catch (error) {
console.error('获取预约记录失败:', error);
throw error;
}
}
// 获取翻译员列表
async getTranslators() {
try {
const { data, error } = await this.supabase
.from('translator_profiles')
.select('*')
.eq('is_active', true)
.order('rating', { ascending: false });
if (error) throw error;
return data;
} catch (error) {
console.error('获取翻译员列表失败:', error);
throw error;
}
}
// 更新UI - 已登录用户
updateUIForLoggedInUser(user) {
// 显示用户信息
const userNameElements = document.querySelectorAll('.user-name');
userNameElements.forEach(el => {
el.textContent = user.user_metadata?.full_name || user.email;
});
// 显示/隐藏相关元素
const loginElements = document.querySelectorAll('.login-required');
loginElements.forEach(el => el.style.display = 'none');
const userElements = document.querySelectorAll('.user-only');
userElements.forEach(el => el.style.display = 'block');
}
// 更新UI - 未登录用户
updateUIForLoggedOutUser() {
// 隐藏/显示相关元素
const loginElements = document.querySelectorAll('.login-required');
loginElements.forEach(el => el.style.display = 'block');
const userElements = document.querySelectorAll('.user-only');
userElements.forEach(el => el.style.display = 'none');
}
// Stripe 支付处理
async processPayment(amount, callRecordId) {
try {
// 这里应该调用后端API来处理Stripe支付
// 由于安全考虑Stripe的secret key不应该在前端使用
console.log('处理支付:', amount, callRecordId);
// 模拟支付成功
return { success: true, paymentId: 'pi_test_' + Date.now() };
} catch (error) {
console.error('支付处理失败:', error);
return { success: false, error: error.message };
}
}
// Twilio 视频通话初始化
async initVideoCall() {
try {
// 这里应该调用后端API获取Twilio访问令牌
console.log('初始化视频通话');
return { success: true, token: 'twilio_token_placeholder' };
} catch (error) {
console.error('视频通话初始化失败:', error);
return { success: false, error: error.message };
}
}
}
// 创建全局API管理器实例
window.apiManager = new APIManager();

40
web-app/config.js Normal file
View File

@ -0,0 +1,40 @@
// 应用配置文件
const CONFIG = {
// Supabase 配置 - Twilio-project
SUPABASE: {
URL: 'https://poxwjzdianersitpnvdy.supabase.co',
ANON_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBveHdqemRpYW5lcnNpdHBudmR5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTExNjk4MjMsImV4cCI6MjA2Njc0NTgyM30.FkgCCSHK0_i8bNFIhhN3k6dEbP5PpE52IggcVJC4Aj8'
},
// Stripe 配置(测试环境)
STRIPE: {
PUBLISHABLE_KEY: 'pk_test_51RTwLuDWamLO9gYlv7ZX0Jj2aLBkADGWmTC3NP0aoez3nEdnLlQiWH3KUie1C45CSa1ho3DvTm0GqR59X0sNTnqN00Q15Fq0zw',
SECRET_KEY: 'sk_test_51RTwLuDWamLO9gYliBCJFtPob28ttoTtvsglGtyXrHkrnuppY2ScnVz7BRh1hCHzvOXcOyvMejBRVsx5vMpgKLVE0065W8VOU8'
},
// OpenAI 配置
OPENAI: {
API_KEY: 'sk-live-o_pqmR3A26poD7ltpYgZ1aoDZEOaAJr8lUlv'
},
// Twilio 配置
TWILIO: {
ACCOUNT_SID: 'AC0123456789abcdef0123456789abcdef',
API_KEY_SID: 'SK0123456789abcdef0123456789abcdef',
API_KEY_SECRET: '0123456789abcdef0123456789abcdef'
},
// 应用配置
APP: {
NAME: '翻译服务应用',
VERSION: '1.0.0',
DEBUG: true
}
};
// 导出配置
if (typeof module !== 'undefined' && module.exports) {
module.exports = CONFIG;
} else {
window.CONFIG = CONFIG;
}

199
web-app/database-init.sql Normal file
View File

@ -0,0 +1,199 @@
-- 翻译服务应用数据库初始化脚本
-- 适用于 Supabase PostgreSQL
-- 1. 创建更新时间触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- 2. 创建用户档案表
CREATE TABLE IF NOT EXISTS user_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
username VARCHAR(50) UNIQUE NOT NULL,
full_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20),
avatar_url TEXT,
preferred_language VARCHAR(10) DEFAULT 'zh-CN',
timezone VARCHAR(50) DEFAULT 'Asia/Shanghai',
account_balance DECIMAL(10,2) DEFAULT 0.00,
is_verified BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 3. 创建翻译员档案表
CREATE TABLE IF NOT EXISTS translator_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
specializations TEXT[] DEFAULT '{}',
languages TEXT[] DEFAULT '{}',
hourly_rate DECIMAL(8,2) DEFAULT 50.00,
rating DECIMAL(3,2) DEFAULT 0.00,
total_reviews INTEGER DEFAULT 0,
is_available BOOLEAN DEFAULT true,
certification_level VARCHAR(20) DEFAULT 'basic',
experience_years INTEGER DEFAULT 0,
bio TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 4. 创建通话记录表
CREATE TABLE IF NOT EXISTS call_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
translator_id UUID REFERENCES translator_profiles(id) ON DELETE SET NULL,
call_type VARCHAR(20) NOT NULL CHECK (call_type IN ('voice', 'video')),
duration_minutes INTEGER NOT NULL DEFAULT 0,
base_rate DECIMAL(8,2) NOT NULL,
translator_rate DECIMAL(8,2) DEFAULT 0.00,
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'completed' CHECK (status IN ('active', 'completed', 'cancelled')),
quality_rating INTEGER CHECK (quality_rating >= 1 AND quality_rating <= 5),
feedback TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
ended_at TIMESTAMP WITH TIME ZONE
);
-- 5. 创建预约表
CREATE TABLE IF NOT EXISTS appointments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
translator_id UUID REFERENCES translator_profiles(id) ON DELETE CASCADE,
appointment_date TIMESTAMP WITH TIME ZONE NOT NULL,
duration_minutes INTEGER DEFAULT 60,
service_type VARCHAR(50) NOT NULL,
languages TEXT[] NOT NULL,
special_requirements TEXT,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled', 'completed')),
total_amount DECIMAL(10,2) NOT NULL,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 6. 创建文档翻译表
CREATE TABLE IF NOT EXISTS document_translations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
translator_id UUID REFERENCES translator_profiles(id) ON DELETE SET NULL,
original_filename VARCHAR(255) NOT NULL,
translated_filename VARCHAR(255),
file_size INTEGER NOT NULL,
file_type VARCHAR(50) NOT NULL,
source_language VARCHAR(10) NOT NULL,
target_language VARCHAR(10) NOT NULL,
word_count INTEGER DEFAULT 0,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
total_amount DECIMAL(10,2) NOT NULL,
completion_percentage INTEGER DEFAULT 0,
estimated_completion TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 7. 创建支付记录表
CREATE TABLE IF NOT EXISTS payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
related_table VARCHAR(50) NOT NULL,
related_id UUID NOT NULL,
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'CNY',
payment_method VARCHAR(20) NOT NULL,
payment_status VARCHAR(20) DEFAULT 'pending' CHECK (payment_status IN ('pending', 'processing', 'completed', 'failed', 'refunded')),
stripe_payment_intent_id TEXT,
transaction_id VARCHAR(100),
paid_at TIMESTAMP WITH TIME ZONE,
refunded_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 8. 创建系统设置表
CREATE TABLE IF NOT EXISTS system_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT NOT NULL,
setting_type VARCHAR(20) DEFAULT 'string' CHECK (setting_type IN ('string', 'number', 'boolean', 'json')),
description TEXT,
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 9. 创建触发器
CREATE TRIGGER update_user_profiles_updated_at BEFORE UPDATE ON user_profiles FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_translator_profiles_updated_at BEFORE UPDATE ON translator_profiles FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_appointments_updated_at BEFORE UPDATE ON appointments FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_document_translations_updated_at BEFORE UPDATE ON document_translations FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TRIGGER update_system_settings_updated_at BEFORE UPDATE ON system_settings FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
-- 10. 启用行级安全 (RLS)
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE translator_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE call_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE appointments ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_translations ENABLE ROW LEVEL SECURITY;
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;
-- 11. 创建 RLS 策略
-- 用户档案策略
CREATE POLICY "用户只能查看自己的档案" ON user_profiles FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "用户只能更新自己的档案" ON user_profiles FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "用户可以插入自己的档案" ON user_profiles FOR INSERT WITH CHECK (auth.uid() = user_id);
-- 翻译员档案策略
CREATE POLICY "翻译员只能查看自己的档案" ON translator_profiles FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "翻译员只能更新自己的档案" ON translator_profiles FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "翻译员可以插入自己的档案" ON translator_profiles FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "所有用户可以查看翻译员档案" ON translator_profiles FOR SELECT USING (true);
-- 通话记录策略
CREATE POLICY "用户只能查看自己的通话记录" ON call_records FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "用户可以插入自己的通话记录" ON call_records FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "用户可以更新自己的通话记录" ON call_records FOR UPDATE USING (auth.uid() = user_id);
-- 预约策略
CREATE POLICY "用户只能查看自己的预约" ON appointments FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "用户可以插入自己的预约" ON appointments FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "用户可以更新自己的预约" ON appointments FOR UPDATE USING (auth.uid() = user_id);
-- 文档翻译策略
CREATE POLICY "用户只能查看自己的文档翻译" ON document_translations FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "用户可以插入自己的文档翻译" ON document_translations FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "用户可以更新自己的文档翻译" ON document_translations FOR UPDATE USING (auth.uid() = user_id);
-- 支付记录策略
CREATE POLICY "用户只能查看自己的支付记录" ON payments FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "用户可以插入自己的支付记录" ON payments FOR INSERT WITH CHECK (auth.uid() = user_id);
-- 12. 插入默认系统设置
INSERT INTO system_settings (setting_key, setting_value, setting_type, description, is_public) VALUES
('voice_call_rate', '80.00', 'number', '语音通话费率(元/小时)', true),
('video_call_rate', '120.00', 'number', '视频通话费率(元/小时)', true),
('translator_rate', '50.00', 'number', '翻译员费率(元/小时)', true),
('min_call_duration', '1', 'number', '最小通话时长(分钟)', true),
('supported_languages', '["zh-CN", "en-US", "ja-JP", "ko-KR", "fr-FR", "de-DE", "es-ES", "it-IT", "pt-PT", "ru-RU"]', 'json', '支持的语言列表', true),
('max_file_size', '10485760', 'number', '最大文件大小(字节)', true),
('supported_file_types', '["pdf", "doc", "docx", "txt", "rtf"]', 'json', '支持的文件类型', true)
ON CONFLICT (setting_key) DO NOTHING;
-- 13. 创建索引以提高查询性能
CREATE INDEX IF NOT EXISTS idx_user_profiles_email ON user_profiles(email);
CREATE INDEX IF NOT EXISTS idx_user_profiles_username ON user_profiles(username);
CREATE INDEX IF NOT EXISTS idx_call_records_user_id ON call_records(user_id);
CREATE INDEX IF NOT EXISTS idx_call_records_created_at ON call_records(created_at);
CREATE INDEX IF NOT EXISTS idx_appointments_user_id ON appointments(user_id);
CREATE INDEX IF NOT EXISTS idx_appointments_date ON appointments(appointment_date);
CREATE INDEX IF NOT EXISTS idx_document_translations_user_id ON document_translations(user_id);
CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id);
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(payment_status);
-- 完成初始化
SELECT 'Database initialization completed successfully!' as status;

187
web-app/database-schema.sql Normal file
View File

@ -0,0 +1,187 @@
-- 翻译服务应用数据库表结构
-- 使用 Supabase PostgreSQL 数据库
-- 1. 用户档案表
CREATE TABLE IF NOT EXISTS user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
full_name TEXT,
phone TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 2. 翻译员档案表
CREATE TABLE IF NOT EXISTS translator_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
full_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
avatar_url TEXT,
languages TEXT[] NOT NULL, -- 支持的语言对
specialties TEXT[], -- 专业领域
rating DECIMAL(3,2) DEFAULT 5.00, -- 评分 (0.00-5.00)
hourly_rate DECIMAL(10,2) DEFAULT 50.00, -- 小时费率
is_active BOOLEAN DEFAULT TRUE,
bio TEXT, -- 个人简介
experience_years INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 3. 通话记录表
CREATE TABLE IF NOT EXISTS call_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
translator_id UUID REFERENCES translator_profiles(id) ON DELETE SET NULL,
call_type TEXT NOT NULL CHECK (call_type IN ('voice', 'video')),
duration INTEGER NOT NULL, -- 通话时长(秒)
has_translator BOOLEAN DEFAULT FALSE,
base_rate DECIMAL(10,2) NOT NULL, -- 基础费率
translator_rate DECIMAL(10,2) DEFAULT 0, -- 翻译员费率
total_amount DECIMAL(10,2) NOT NULL, -- 总金额
status TEXT DEFAULT 'completed' CHECK (status IN ('pending', 'completed', 'cancelled')),
payment_status TEXT DEFAULT 'unpaid' CHECK (payment_status IN ('unpaid', 'paid', 'refunded')),
payment_id TEXT, -- Stripe 支付ID
twilio_call_sid TEXT, -- Twilio 通话ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 4. 预约表
CREATE TABLE IF NOT EXISTS appointments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
translator_id UUID REFERENCES translator_profiles(id) ON DELETE SET NULL,
appointment_date DATE NOT NULL,
appointment_time TIME NOT NULL,
service_type TEXT NOT NULL CHECK (service_type IN ('voice', 'video', 'document')),
language_pair TEXT NOT NULL, -- 语言对,如 '中文-英文'
duration INTEGER DEFAULT 60, -- 预约时长(分钟)
notes TEXT, -- 备注
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'completed', 'cancelled')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 5. 文档翻译表
CREATE TABLE IF NOT EXISTS document_translations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
translator_id UUID REFERENCES translator_profiles(id) ON DELETE SET NULL,
original_filename TEXT NOT NULL,
original_file_url TEXT NOT NULL, -- 原文件存储URL
translated_file_url TEXT, -- 翻译后文件存储URL
source_language TEXT NOT NULL,
target_language TEXT NOT NULL,
file_size INTEGER, -- 文件大小(字节)
word_count INTEGER, -- 字数统计
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
estimated_completion TIMESTAMP WITH TIME ZONE,
actual_completion TIMESTAMP WITH TIME ZONE,
amount DECIMAL(10,2), -- 翻译费用
payment_status TEXT DEFAULT 'unpaid' CHECK (payment_status IN ('unpaid', 'paid', 'refunded')),
payment_id TEXT, -- Stripe 支付ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 6. 支付记录表
CREATE TABLE IF NOT EXISTS payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
stripe_payment_id TEXT UNIQUE NOT NULL,
amount DECIMAL(10,2) NOT NULL,
currency TEXT DEFAULT 'cny',
status TEXT NOT NULL CHECK (status IN ('pending', 'succeeded', 'failed', 'cancelled')),
payment_method TEXT, -- 支付方式
description TEXT, -- 支付描述
metadata JSONB, -- 额外的支付信息
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 7. 系统设置表
CREATE TABLE IF NOT EXISTS system_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key TEXT UNIQUE NOT NULL,
value JSONB NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 创建索引以提高查询性能
CREATE INDEX IF NOT EXISTS idx_user_profiles_email ON user_profiles(email);
CREATE INDEX IF NOT EXISTS idx_translator_profiles_languages ON translator_profiles USING GIN(languages);
CREATE INDEX IF NOT EXISTS idx_call_records_user_id ON call_records(user_id);
CREATE INDEX IF NOT EXISTS idx_call_records_created_at ON call_records(created_at);
CREATE INDEX IF NOT EXISTS idx_appointments_user_id ON appointments(user_id);
CREATE INDEX IF NOT EXISTS idx_appointments_date ON appointments(appointment_date);
CREATE INDEX IF NOT EXISTS idx_document_translations_user_id ON document_translations(user_id);
CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id);
CREATE INDEX IF NOT EXISTS idx_payments_stripe_id ON payments(stripe_payment_id);
-- 创建更新时间触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- 为所有表添加更新时间触发器
CREATE TRIGGER update_user_profiles_updated_at BEFORE UPDATE ON user_profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_translator_profiles_updated_at BEFORE UPDATE ON translator_profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_call_records_updated_at BEFORE UPDATE ON call_records FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_appointments_updated_at BEFORE UPDATE ON appointments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_document_translations_updated_at BEFORE UPDATE ON document_translations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_payments_updated_at BEFORE UPDATE ON payments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_system_settings_updated_at BEFORE UPDATE ON system_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 插入默认系统设置
INSERT INTO system_settings (key, value, description) VALUES
('call_rates', '{"voice": 80, "video": 120, "translator": 50}', '通话费率设置(元/小时)'),
('supported_languages', '["中文", "英文", "日文", "韩文", "法文", "德文", "西班牙文", "俄文"]', '支持的语言列表'),
('document_formats', '["pdf", "doc", "docx", "txt", "rtf"]', '支持的文档格式'),
('max_file_size', '10485760', '最大文件上传大小(字节)')
ON CONFLICT (key) DO NOTHING;
-- 插入示例翻译员数据
INSERT INTO translator_profiles (full_name, email, phone, languages, specialties, rating, hourly_rate, bio, experience_years) VALUES
('张译文', 'zhang.yiwen@example.com', '13800138001', ARRAY['中文', '英文'], ARRAY['商务', '法律', '技术'], 4.8, 80.00, '资深英语翻译具有10年商务翻译经验', 10),
('李法兰', 'li.falan@example.com', '13800138002', ARRAY['中文', '法文'], ARRAY['法律', '文学', '艺术'], 4.9, 90.00, '法语翻译专家,巴黎大学文学硕士', 12),
('田中太郎', 'tanaka.taro@example.com', '13800138003', ARRAY['中文', '日文'], ARRAY['技术', '制造', '动漫'], 4.7, 75.00, '日语翻译,专注于技术和制造业翻译', 8),
('金智慧', 'kim.jihye@example.com', '13800138004', ARRAY['中文', '韩文'], ARRAY['娱乐', '时尚', '美容'], 4.6, 70.00, '韩语翻译,熟悉韩国文化和娱乐产业', 6)
ON CONFLICT DO NOTHING;
-- 设置行级安全策略 (RLS)
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE translator_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE call_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE appointments ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_translations ENABLE ROW LEVEL SECURITY;
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;
-- 用户只能访问自己的数据
CREATE POLICY "Users can view own profile" ON user_profiles FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update own profile" ON user_profiles FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "Users can view own call records" ON call_records FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own call records" ON call_records FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can view own appointments" ON appointments FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own appointments" ON appointments FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own appointments" ON appointments FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can view own document translations" ON document_translations FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own document translations" ON document_translations FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can view own payments" ON payments FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own payments" ON payments FOR INSERT WITH CHECK (auth.uid() = user_id);
-- 翻译员档案可以被所有用户查看
CREATE POLICY "Anyone can view active translators" ON translator_profiles FOR SELECT USING (is_active = true);

65
web-app/get-api-keys.md Normal file
View File

@ -0,0 +1,65 @@
# 🔑 获取 Supabase API 密钥指南
## ❌ 当前问题
你遇到了 "Invalid API key" 错误,这是因为配置文件中的 API 密钥不正确或已过期。
## 🔧 解决步骤
### 1. 访问 Supabase 控制台
```
https://supabase.com/dashboard/project/poxwjzdianersitpnvdy
```
### 2. 获取 API 密钥
1. 在项目控制台中,点击左侧菜单的 **"Settings"**
2. 选择 **"API"** 选项
3. 在 **"Project API keys"** 部分找到:
- **anon public** 密钥(这是我们需要的)
- **service_role** 密钥(用于服务端操作)
### 3. 更新配置文件
复制 **anon public** 密钥,然后更新 `web-app/config.js` 文件:
```javascript
// 应用配置文件
const CONFIG = {
// Supabase 配置 - Twilio-project
SUPABASE: {
URL: 'https://poxwjzdianersitpnvdy.supabase.co',
ANON_KEY: '你的_anon_public_密钥_在这里' // 替换为实际密钥
},
// ... 其他配置保持不变
};
```
### 4. 验证连接
更新密钥后:
1. 刷新浏览器页面
2. 尝试重新注册用户
3. 检查浏览器控制台是否还有错误
## 🚨 重要提示
- **不要分享 service_role 密钥**:这个密钥有完全的数据库访问权限
- **anon public 密钥是安全的**:可以在前端代码中使用
- **检查密钥格式**:应该是以 `eyJ` 开头的长字符串
## 🔍 常见问题
### Q: 找不到 API 密钥?
A: 确保你已经登录到正确的 Supabase 账户,并且有访问该项目的权限。
### Q: 密钥看起来正确但仍然报错?
A: 检查项目状态是否为 "Active",并且确保没有复制错误(没有额外的空格或字符)。
### Q: 如何知道密钥是否正确?
A: 正确的 anon public 密钥应该:
- 以 `eyJ` 开头
- 包含三个部分,用 `.` 分隔
- 长度通常在 100-200 个字符之间
## 📞 需要帮助?
如果你仍然遇到问题,请:
1. 确认你能正常访问 Supabase 控制台
2. 检查项目状态是否正常
3. 确保复制的密钥完整且正确

102
web-app/init-database.js Normal file
View File

@ -0,0 +1,102 @@
// 数据库初始化脚本
// 使用 Node.js 运行: node init-database.js
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
const path = require('path');
// 配置信息
const SUPABASE_URL = 'https://poxwjzdianersitpnvdy.supabase.co';
const SUPABASE_SERVICE_KEY = 'YOUR_SERVICE_ROLE_KEY_HERE'; // 需要替换为实际的 Service Role Key
async function initializeDatabase() {
console.log('🚀 开始初始化 Twilio-project 数据库...');
try {
// 创建 Supabase 客户端(使用 Service Role Key
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
// 读取 SQL 初始化脚本
const sqlScript = fs.readFileSync(path.join(__dirname, 'database-init.sql'), 'utf8');
// 将 SQL 脚本分割成单独的语句
const statements = sqlScript
.split(';')
.map(stmt => stmt.trim())
.filter(stmt => stmt.length > 0 && !stmt.startsWith('--'));
console.log(`📝 准备执行 ${statements.length} 条 SQL 语句...`);
// 逐条执行 SQL 语句
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (statement.includes('SELECT') && statement.includes('status')) {
continue; // 跳过状态检查语句
}
console.log(`⏳ 执行语句 ${i + 1}/${statements.length}...`);
try {
const { data, error } = await supabase.rpc('exec_sql', {
sql_query: statement + ';'
});
if (error) {
console.warn(`⚠️ 语句 ${i + 1} 执行警告:`, error.message);
} else {
console.log(`✅ 语句 ${i + 1} 执行成功`);
}
} catch (err) {
console.warn(`⚠️ 语句 ${i + 1} 执行出错:`, err.message);
}
}
// 验证表是否创建成功
console.log('\n🔍 验证数据库表...');
const { data: tables, error: tablesError } = await supabase
.from('information_schema.tables')
.select('table_name')
.eq('table_schema', 'public');
if (tablesError) {
console.error('❌ 无法获取表列表:', tablesError);
} else {
console.log('✅ 数据库表创建成功:');
tables.forEach(table => {
console.log(` - ${table.table_name}`);
});
}
console.log('\n🎉 数据库初始化完成!');
console.log('\n📋 创建的表包括:');
console.log(' • user_profiles - 用户档案');
console.log(' • translator_profiles - 翻译员档案');
console.log(' • call_records - 通话记录');
console.log(' • appointments - 预约管理');
console.log(' • document_translations - 文档翻译');
console.log(' • payments - 支付记录');
console.log(' • system_settings - 系统设置');
} catch (error) {
console.error('❌ 数据库初始化失败:', error);
process.exit(1);
}
}
// 手动执行 SQL 的替代方法
async function manualInit() {
console.log('\n📖 手动初始化指南:');
console.log('1. 访问 Supabase 控制台: https://supabase.com/dashboard/project/poxwjzdianersitpnvdy');
console.log('2. 进入 SQL Editor');
console.log('3. 复制并执行 database-init.sql 文件中的内容');
console.log('4. 确认所有表都创建成功');
}
// 检查是否提供了 Service Role Key
if (SUPABASE_SERVICE_KEY === 'YOUR_SERVICE_ROLE_KEY_HERE') {
console.log('⚠️ 请先在脚本中设置正确的 SUPABASE_SERVICE_KEY');
manualInit();
} else {
initializeDatabase();
}

325
web-app/login.html Normal file
View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 翻译服务平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
text-align: center;
}
.logo {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
margin: 0 auto 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
font-weight: bold;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
font-weight: 600;
}
.subtitle {
color: #666;
margin-bottom: 40px;
font-size: 16px;
}
.form-group {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 15px 20px;
border: 2px solid #e1e5e9;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: #f8f9fa;
}
input[type="email"]:focus,
input[type="password"]:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.login-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.login-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.forgot-password {
color: #667eea;
text-decoration: none;
font-size: 14px;
margin-bottom: 30px;
display: inline-block;
}
.forgot-password:hover {
text-decoration: underline;
}
.divider {
margin: 30px 0;
position: relative;
text-align: center;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e1e5e9;
}
.divider span {
background: white;
padding: 0 20px;
color: #666;
font-size: 14px;
}
.register-link {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.register-link:hover {
text-decoration: underline;
}
.error-message {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid #fcc;
}
.success-message {
background: #efe;
color: #3c3;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid #cfc;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 480px) {
.login-container {
padding: 30px 20px;
margin: 10px;
}
h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo"></div>
<h1>欢迎回来</h1>
<p class="subtitle">登录您的翻译服务账户</p>
<div id="message-container"></div>
<form id="loginForm">
<div class="form-group">
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="login-btn" id="loginBtn">
<span class="btn-text">登录</span>
</button>
</form>
<a href="#" class="forgot-password">忘记密码?</a>
<div class="divider">
<span>还没有账户?</span>
</div>
<a href="register.html" class="register-link">立即注册</a>
</div>
<!-- 引入必要的脚本 -->
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<script src="config.js"></script>
<script src="api.js"></script>
<script>
// 登录表单处理
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
const messageContainer = document.getElementById('message-container');
// 显示加载状态
loginBtn.disabled = true;
loginBtn.innerHTML = '<span class="loading"></span>登录中...';
try {
const result = await apiManager.login(email, password);
if (result.success) {
showMessage('登录成功!正在跳转...', 'success');
// 延迟跳转,让用户看到成功消息
setTimeout(() => {
window.location.href = '../index.html';
}, 1500);
} else {
showMessage(result.error || '登录失败,请检查邮箱和密码', 'error');
}
} catch (error) {
showMessage('登录失败:' + error.message, 'error');
} finally {
// 恢复按钮状态
loginBtn.disabled = false;
loginBtn.innerHTML = '<span class="btn-text">登录</span>';
}
});
// 显示消息
function showMessage(message, type) {
const messageContainer = document.getElementById('message-container');
const messageClass = type === 'success' ? 'success-message' : 'error-message';
messageContainer.innerHTML = `<div class="${messageClass}">${message}</div>`;
// 3秒后自动清除消息
setTimeout(() => {
messageContainer.innerHTML = '';
}, 3000);
}
// 检查是否已登录
window.addEventListener('load', async () => {
// 等待API管理器初始化
await new Promise(resolve => {
const checkInit = () => {
if (window.apiManager && window.apiManager.supabase) {
resolve();
} else {
setTimeout(checkInit, 100);
}
};
checkInit();
});
// 如果已登录,直接跳转到主页
if (apiManager.currentUser) {
window.location.href = '../index.html';
}
});
// 回车键登录
document.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('loginForm').dispatchEvent(new Event('submit'));
}
});
</script>
</body>
</html>

447
web-app/register.html Normal file
View File

@ -0,0 +1,447 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册 - 翻译服务平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.register-container {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 450px;
text-align: center;
}
.logo {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
margin: 0 auto 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
font-weight: bold;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
font-weight: 600;
}
.subtitle {
color: #666;
margin-bottom: 40px;
font-size: 16px;
}
.form-group {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"] {
width: 100%;
padding: 15px 20px;
border: 2px solid #e1e5e9;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: #f8f9fa;
}
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="tel"]:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.password-strength {
margin-top: 8px;
font-size: 12px;
}
.strength-weak { color: #e74c3c; }
.strength-medium { color: #f39c12; }
.strength-strong { color: #27ae60; }
.register-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.register-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.register-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.terms {
font-size: 12px;
color: #666;
margin-bottom: 30px;
line-height: 1.5;
}
.terms a {
color: #667eea;
text-decoration: none;
}
.terms a:hover {
text-decoration: underline;
}
.divider {
margin: 30px 0;
position: relative;
text-align: center;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e1e5e9;
}
.divider span {
background: white;
padding: 0 20px;
color: #666;
font-size: 14px;
}
.login-link {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.login-link:hover {
text-decoration: underline;
}
.error-message {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid #fcc;
}
.success-message {
background: #efe;
color: #3c3;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid #cfc;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.form-row {
display: flex;
gap: 15px;
}
.form-row .form-group {
flex: 1;
}
@media (max-width: 480px) {
.register-container {
padding: 30px 20px;
margin: 10px;
}
h1 {
font-size: 24px;
}
.form-row {
flex-direction: column;
gap: 0;
}
}
</style>
</head>
<body>
<div class="register-container">
<div class="logo"></div>
<h1>创建账户</h1>
<p class="subtitle">加入我们的翻译服务平台</p>
<div id="message-container"></div>
<form id="registerForm">
<div class="form-group">
<label for="fullName">姓名</label>
<input type="text" id="fullName" name="fullName" required>
</div>
<div class="form-group">
<label for="email">邮箱地址</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="phone">手机号码</label>
<input type="tel" id="phone" name="phone" placeholder="请输入手机号码">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
<div id="passwordStrength" class="password-strength"></div>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input type="password" id="confirmPassword" name="confirmPassword" required>
</div>
<button type="submit" class="register-btn" id="registerBtn">
<span class="btn-text">创建账户</span>
</button>
</form>
<div class="terms">
注册即表示您同意我们的
<a href="#">服务条款</a>
<a href="#">隐私政策</a>
</div>
<div class="divider">
<span>已有账户?</span>
</div>
<a href="login.html" class="login-link">立即登录</a>
</div>
<!-- 引入必要的脚本 -->
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<script src="config.js"></script>
<script src="api.js"></script>
<script>
// 密码强度检查
document.getElementById('password').addEventListener('input', function(e) {
const password = e.target.value;
const strengthDiv = document.getElementById('passwordStrength');
if (password.length === 0) {
strengthDiv.textContent = '';
return;
}
let strength = 0;
let feedback = [];
// 长度检查
if (password.length >= 8) strength++;
else feedback.push('至少8个字符');
// 包含数字
if (/\d/.test(password)) strength++;
else feedback.push('包含数字');
// 包含小写字母
if (/[a-z]/.test(password)) strength++;
else feedback.push('包含小写字母');
// 包含大写字母或特殊字符
if (/[A-Z]/.test(password) || /[^A-Za-z0-9]/.test(password)) strength++;
else feedback.push('包含大写字母或特殊字符');
// 显示强度
if (strength < 2) {
strengthDiv.className = 'password-strength strength-weak';
strengthDiv.textContent = '密码强度:弱 - ' + feedback.join('、');
} else if (strength < 3) {
strengthDiv.className = 'password-strength strength-medium';
strengthDiv.textContent = '密码强度:中等 - ' + feedback.join('、');
} else {
strengthDiv.className = 'password-strength strength-strong';
strengthDiv.textContent = '密码强度:强';
}
});
// 注册表单处理
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const fullName = formData.get('fullName');
const email = formData.get('email');
const phone = formData.get('phone');
const password = formData.get('password');
const confirmPassword = formData.get('confirmPassword');
const registerBtn = document.getElementById('registerBtn');
// 验证密码匹配
if (password !== confirmPassword) {
showMessage('两次输入的密码不一致', 'error');
return;
}
// 验证密码强度
if (password.length < 6) {
showMessage('密码长度至少6个字符', 'error');
return;
}
// 显示加载状态
registerBtn.disabled = true;
registerBtn.innerHTML = '<span class="loading"></span>注册中...';
try {
const result = await apiManager.register(email, password, {
fullName: fullName,
phone: phone
});
if (result.success) {
showMessage('注册成功!请检查您的邮箱进行验证,然后登录。', 'success');
// 清空表单
document.getElementById('registerForm').reset();
document.getElementById('passwordStrength').textContent = '';
// 延迟跳转到登录页面
setTimeout(() => {
window.location.href = '../index.html';
}, 3000);
} else {
showMessage(result.error || '注册失败,请重试', 'error');
}
} catch (error) {
showMessage('注册失败:' + error.message, 'error');
} finally {
// 恢复按钮状态
registerBtn.disabled = false;
registerBtn.innerHTML = '<span class="btn-text">创建账户</span>';
}
});
// 显示消息
function showMessage(message, type) {
const messageContainer = document.getElementById('message-container');
const messageClass = type === 'success' ? 'success-message' : 'error-message';
messageContainer.innerHTML = `<div class="${messageClass}">${message}</div>`;
// 成功消息显示更长时间
const timeout = type === 'success' ? 5000 : 3000;
setTimeout(() => {
messageContainer.innerHTML = '';
}, timeout);
}
// 检查是否已登录
window.addEventListener('load', async () => {
// 等待API管理器初始化
await new Promise(resolve => {
const checkInit = () => {
if (window.apiManager && window.apiManager.supabase) {
resolve();
} else {
setTimeout(checkInit, 100);
}
};
checkInit();
});
// 如果已登录,直接跳转到主页
if (apiManager.currentUser) {
window.location.href = '../index.html';
}
});
// 实时验证确认密码
document.getElementById('confirmPassword').addEventListener('input', function(e) {
const password = document.getElementById('password').value;
const confirmPassword = e.target.value;
if (confirmPassword && password !== confirmPassword) {
e.target.style.borderColor = '#e74c3c';
} else {
e.target.style.borderColor = '#e1e5e9';
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Supabase 连接测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 15px;
margin: 10px 0;
border-radius: 5px;
font-weight: bold;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
.loading {
display: none;
}
pre {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>🔧 Supabase 连接测试</h1>
<div class="info">
<strong>说明:</strong> 这个页面用于测试 Supabase 数据库连接是否正常。
</div>
<h2>当前配置</h2>
<div id="currentConfig"></div>
<h2>连接测试</h2>
<button onclick="testConnection()" id="testBtn">测试连接</button>
<div class="loading" id="loading">正在测试连接...</div>
<div id="result"></div>
<h2>数据库表检查</h2>
<button onclick="checkTables()" id="tablesBtn">检查数据库表</button>
<div id="tablesResult"></div>
<h2>解决方案</h2>
<div class="info">
<p><strong>如果连接失败,请按以下步骤操作:</strong></p>
<ol>
<li>访问 <a href="https://supabase.com/dashboard/project/poxwjzdianersitpnvdy" target="_blank">Supabase 控制台</a></li>
<li>进入 Settings → API</li>
<li>复制 "anon public" 密钥</li>
<li>更新 config.js 文件中的 ANON_KEY</li>
<li>刷新页面重新测试</li>
</ol>
</div>
</div>
<!-- 引入 Supabase 客户端 -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="config.js"></script>
<script>
// 显示当前配置
document.getElementById('currentConfig').innerHTML = `
<pre>
URL: ${CONFIG.SUPABASE.URL}
ANON_KEY: ${CONFIG.SUPABASE.ANON_KEY.substring(0, 50)}...
</pre>
`;
let supabase;
async function testConnection() {
const resultDiv = document.getElementById('result');
const testBtn = document.getElementById('testBtn');
const loading = document.getElementById('loading');
testBtn.disabled = true;
loading.style.display = 'block';
resultDiv.innerHTML = '';
try {
// 创建 Supabase 客户端
const { createClient } = supabase;
supabase = createClient(CONFIG.SUPABASE.URL, CONFIG.SUPABASE.ANON_KEY);
// 测试连接
const { data, error } = await supabase.auth.getSession();
if (error) {
throw error;
}
resultDiv.innerHTML = `
<div class="success">
✅ 连接成功API 密钥有效。
</div>
<pre>响应数据: ${JSON.stringify(data, null, 2)}</pre>
`;
} catch (error) {
resultDiv.innerHTML = `
<div class="error">
❌ 连接失败: ${error.message}
</div>
<pre>错误详情: ${JSON.stringify(error, null, 2)}</pre>
`;
} finally {
testBtn.disabled = false;
loading.style.display = 'none';
}
}
async function checkTables() {
const resultDiv = document.getElementById('tablesResult');
const tablesBtn = document.getElementById('tablesBtn');
if (!supabase) {
resultDiv.innerHTML = `
<div class="error">
❌ 请先测试连接!
</div>
`;
return;
}
tablesBtn.disabled = true;
resultDiv.innerHTML = '<div class="info">正在检查数据库表...</div>';
try {
// 尝试查询系统设置表
const { data, error } = await supabase
.from('system_settings')
.select('*')
.limit(5);
if (error) {
throw error;
}
resultDiv.innerHTML = `
<div class="success">
✅ 数据库表访问正常!找到 ${data.length} 条系统设置记录。
</div>
<pre>示例数据: ${JSON.stringify(data, null, 2)}</pre>
`;
} catch (error) {
if (error.message.includes('relation "system_settings" does not exist')) {
resultDiv.innerHTML = `
<div class="error">
❌ 数据库表不存在!请先执行数据库初始化脚本。
</div>
<div class="info">
<p><strong>解决方案:</strong></p>
<ol>
<li>访问 <a href="https://supabase.com/dashboard/project/poxwjzdianersitpnvdy" target="_blank">Supabase SQL Editor</a></li>
<li>执行 database-init.sql 脚本</li>
<li>重新测试</li>
</ol>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="error">
❌ 数据库访问失败: ${error.message}
</div>
<pre>错误详情: ${JSON.stringify(error, null, 2)}</pre>
`;
}
} finally {
tablesBtn.disabled = false;
}
}
// 页面加载时自动测试连接
window.addEventListener('load', () => {
setTimeout(testConnection, 1000);
});
</script>
</body>
</html>