feat: 添加手机号、邀请好友、邮件通知功能

- 用户模型添加:手机号、邀请码、邀请统计、邮件通知设置
- 邀请好友系统:专属邀请码、邀请奖励(¥5/人)
- 邮件通知:翻译完成通知(含附件)、欢迎邮件、到期提醒
- 新增模型:UserInvitation, InviteRewardConfig, EmailNotification, EmailTemplateConfig
- 个人中心添加:手机号绑定、通知设置、邀请好友模块
- email_service.py:邮件发送服务(支持附件)

新用户注册奖励:¥2
邀请人奖励:¥5/人
This commit is contained in:
2026-04-14 18:58:40 +08:00
parent 4aac8ab04c
commit 71a613ff5f
4 changed files with 725 additions and 4 deletions

View File

@@ -134,7 +134,7 @@
</div>
<!-- 会员购买记录 -->
<div class="card">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎫 会员购买记录</h5>
</div>
@@ -157,6 +157,111 @@
</div>
</div>
</div>
<!-- 账户设置 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">⚙️ 账户设置</h5>
</div>
<div class="card-body">
<div class="row">
<!-- 手机号绑定 -->
<div class="col-md-6 mb-3">
<div class="border rounded p-3">
<h6 class="mb-3">📱 手机号</h6>
<div id="phoneStatus">
{% if user.phone %}
<p class="text-success">已绑定:{{ user.phone }}
{% if user.phone_verified %}<span class="badge bg-success">已验证</span>{% endif %}
</p>
<button class="btn btn-outline-secondary btn-sm" onclick="showPhoneModal()">更换手机号</button>
{% else %}
<p class="text-muted">未绑定手机号</p>
<button class="btn btn-primary btn-sm" onclick="showPhoneModal()">绑定手机号</button>
{% endif %}
</div>
</div>
</div>
<!-- 邮件通知设置 -->
<div class="col-md-6 mb-3">
<div class="border rounded p-3">
<h6 class="mb-3">📧 邮件通知</h6>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyComplete"
{% if user.notify_on_complete %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">翻译完成通知(含附件)</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="notifyExpire"
{% if user.notify_on_expire %}checked{% endif %}
onchange="updateNotifySettings()">
<label class="form-check-label">会员到期提醒</label>
</div>
<small class="text-muted">通知发送至:{{ user.email }}</small>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请好友 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🎁 邀请好友</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="border rounded p-3 bg-light">
<h6>您的专属邀请码</h6>
<div class="input-group mb-3">
<input type="text" class="form-control form-control-lg text-center fw-bold"
id="inviteCode" value="{{ user.invite_code or '生成中...' }}" readonly>
<button class="btn btn-outline-primary" onclick="copyInviteCode()">复制</button>
</div>
<p class="text-muted mb-0">
分享邀请码给好友,好友注册后您可获得 <strong class="text-success">¥5</strong> 奖励!
</p>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3">
<h6>邀请统计</h6>
<div class="d-flex justify-content-around text-center">
<div>
<div class="fs-4 fw-bold text-primary">{{ user.invite_count }}</div>
<small class="text-muted">已邀请</small>
</div>
<div>
<div class="fs-4 fw-bold text-success">¥{{ "%.2f"|format(user.invite_rewards) }}</div>
<small class="text-muted">累计奖励</small>
</div>
</div>
</div>
</div>
</div>
<!-- 邀请记录 -->
<div class="mt-3">
<h6>邀请记录</h6>
<table class="table table-sm">
<thead>
<tr>
<th>被邀请人</th>
<th>注册时间</th>
<th>奖励</th>
<th>状态</th>
</tr>
</thead>
<tbody id="inviteRecords">
<!-- 动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- 充值模态框 -->
@@ -340,10 +445,88 @@
// 初始化
loadTransactions();
loadPurchases();
loadInviteRecords();
document.getElementById('transactionFilter').addEventListener('change', function() {
loadTransactions(this.value);
});
// 加载邀请记录
function loadInviteRecords() {
fetch('/api/profile/invitations')
.then(r => r.json())
.then(data => {
const tbody = document.getElementById('inviteRecords');
if (data.invitations && data.invitations.length > 0) {
tbody.innerHTML = data.invitations.map(inv => {
const statusMap = { 'pending': '待注册', 'registered': '已注册', 'rewarded': '已奖励' };
const statusClass = { 'pending': 'text-muted', 'registered': 'text-info', 'rewarded': 'text-success' };
return `<tr>
<td>${inv.invitee_email || '用户' + inv.invitee_id}</td>
<td>${inv.created_at}</td>
<td>¥${inv.reward_amount.toFixed(2)}</td>
<td><span class="${statusClass[inv.status]}">${statusMap[inv.status]}</span></td>
</tr>`;
}).join('');
} else {
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">暂无邀请记录</td></tr>';
}
});
}
// 复制邀请码
function copyInviteCode() {
const code = document.getElementById('inviteCode').value;
navigator.clipboard.writeText(code).then(() => {
alert('邀请码已复制:' + code);
});
}
// 更新通知设置
function updateNotifySettings() {
const notifyComplete = document.getElementById('notifyComplete').checked;
const notifyExpire = document.getElementById('notifyExpire').checked;
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
notify_on_complete: notifyComplete,
notify_on_expire: notifyExpire
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
// 不刷新页面,只显示提示
console.log('设置已更新');
}
});
}
// 绑定手机号
function showPhoneModal() {
// 简单处理,直接弹输入框
const phone = prompt('请输入手机号:');
if (phone && phone.length >= 10) {
fetch('/api/profile/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phone })
})
.then(r => r.json())
.then(data => {
if (data.success) {
alert('手机号已绑定');
location.reload();
} else {
alert('绑定失败:' + data.error);
}
});
} else if (phone) {
alert('手机号格式不正确');
}
}
</script>
</body>
</html>