添加数据库集成和用户认证功能
- 新增用户注册和登录系统 (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:
parent
58665f4bbf
commit
0d57273021
180
index.html
180
index.html
@ -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
146
web-app/DATABASE_SETUP.md
Normal 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
297
web-app/DEPLOYMENT_GUIDE.md
Normal 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
352
web-app/api.js
Normal 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
40
web-app/config.js
Normal 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
199
web-app/database-init.sql
Normal 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
187
web-app/database-schema.sql
Normal 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
65
web-app/get-api-keys.md
Normal 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
102
web-app/init-database.js
Normal 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
325
web-app/login.html
Normal 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
447
web-app/register.html
Normal 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>
|
223
web-app/test-connection.html
Normal file
223
web-app/test-connection.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user