解决超话签到中选择性签到的逻辑问题
This commit is contained in:
@@ -304,7 +304,34 @@ async def list_topics(
|
||||
return error_response("Cookie 解密失败", "COOKIE_ERROR", status_code=400)
|
||||
|
||||
topics = await _get_super_topics(cookie_str, account.weibo_user_id)
|
||||
return success_response({"topics": topics, "total": len(topics)})
|
||||
return success_response({
|
||||
"topics": topics,
|
||||
"total": len(topics),
|
||||
"selected_topics": account.selected_topics,
|
||||
})
|
||||
|
||||
|
||||
@router.put("/{account_id}/topics")
|
||||
async def save_selected_topics(
|
||||
account_id: str,
|
||||
body: dict = Body(...),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""保存用户选择的签到超话列表。空列表或 null 表示签到全部。"""
|
||||
account = await _get_owned_account(account_id, user, db)
|
||||
selected = body.get("selected_topics")
|
||||
# null 或空列表都表示全部签到
|
||||
if selected and isinstance(selected, list) and len(selected) > 0:
|
||||
account.selected_topics = selected
|
||||
else:
|
||||
account.selected_topics = None
|
||||
await db.commit()
|
||||
await db.refresh(account)
|
||||
return success_response(
|
||||
_account_to_dict(account),
|
||||
f"已保存 {len(selected) if selected else 0} 个超话" if selected else "已设为签到全部超话",
|
||||
)
|
||||
|
||||
|
||||
# ---- MANUAL SIGNIN ----
|
||||
|
||||
@@ -27,6 +27,7 @@ class AccountResponse(BaseModel):
|
||||
weibo_user_id: str
|
||||
remark: Optional[str]
|
||||
status: str
|
||||
selected_topics: Optional[list] = None
|
||||
last_checked_at: Optional[datetime]
|
||||
created_at: Optional[datetime]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, JSON, String, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
@@ -19,6 +19,7 @@ class Account(Base):
|
||||
encrypted_cookies = Column(Text, nullable=False)
|
||||
iv = Column(String(32), nullable=False)
|
||||
status = Column(String(20), default="pending")
|
||||
selected_topics = Column(JSON, nullable=True) # 用户选择的签到超话列表,null=全部
|
||||
last_checked_at = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
@@ -369,6 +369,7 @@ async def _async_do_signin(account_id: str, cron_expr: str = ""):
|
||||
return {"status": "failed", "reason": "cookie decryption failed"}
|
||||
|
||||
acc_id = str(account.id)
|
||||
acc_selected_topics = account.selected_topics # 用户选择的超话列表
|
||||
except Exception as e:
|
||||
await eng.dispose()
|
||||
raise e
|
||||
@@ -387,6 +388,16 @@ async def _async_do_signin(account_id: str, cron_expr: str = ""):
|
||||
await session.commit()
|
||||
return {"status": "completed", "signed": 0, "message": "no topics"}
|
||||
|
||||
# 如果用户选择了特定超话,只签选中的
|
||||
if acc_selected_topics and isinstance(acc_selected_topics, list):
|
||||
selected_cids = {t.get("containerid") for t in acc_selected_topics if t.get("containerid")}
|
||||
if selected_cids:
|
||||
topics = [t for t in topics if t["containerid"] in selected_cids]
|
||||
logger.info(f"📌 按用户选择过滤: {len(topics)} 个超话 (共 {len(selected_cids)} 个已选)")
|
||||
|
||||
if not topics:
|
||||
return {"status": "completed", "signed": 0, "message": "no selected topics"}
|
||||
|
||||
signed = already = failed = 0
|
||||
log_entries = []
|
||||
|
||||
|
||||
@@ -1085,7 +1085,9 @@ def account_topics(account_id):
|
||||
try:
|
||||
resp = api_request('GET', f'{API_BASE_URL}/api/v1/accounts/{account_id}/topics')
|
||||
data = resp.json()
|
||||
topics = data.get('data', {}).get('topics', []) if data.get('success') else []
|
||||
payload = data.get('data', {}) if data.get('success') else {}
|
||||
topics = payload.get('topics', [])
|
||||
selected_topics = payload.get('selected_topics') or []
|
||||
|
||||
# 获取账号信息
|
||||
acc_resp = api_request('GET', f'{API_BASE_URL}/api/v1/accounts/{account_id}')
|
||||
@@ -1096,12 +1098,32 @@ def account_topics(account_id):
|
||||
flash('账号不存在', 'danger')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
return render_template('topics.html', account=account, topics=topics, user=session.get('user'))
|
||||
return render_template('topics.html', account=account, topics=topics, selected_topics=selected_topics, user=session.get('user'))
|
||||
except Exception as e:
|
||||
flash(f'获取超话列表失败: {str(e)}', 'danger')
|
||||
return redirect(url_for('account_detail', account_id=account_id))
|
||||
|
||||
|
||||
@app.route('/accounts/<account_id>/topics/save', methods=['POST'])
|
||||
@login_required
|
||||
def save_topics(account_id):
|
||||
"""保存用户选择的签到超话"""
|
||||
try:
|
||||
body = request.json
|
||||
resp = api_request(
|
||||
'PUT',
|
||||
f'{API_BASE_URL}/api/v1/accounts/{account_id}/topics',
|
||||
json=body,
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get('success'):
|
||||
return jsonify({'success': True, 'message': data.get('message', '保存成功')})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': data.get('message', '保存失败')}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/accounts/<account_id>/signin-selected', methods=['POST'])
|
||||
@login_required
|
||||
def signin_selected(account_id):
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.topics-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
.topics-header h1 { font-size: 24px; font-weight: 700; color: #1e293b; }
|
||||
.topics-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 10px; }
|
||||
.topics-header h1 { font-size: 22px; font-weight: 700; color: #1e293b; }
|
||||
.topic-list { display: flex; flex-direction: column; gap: 0; }
|
||||
.topic-item {
|
||||
display: flex; align-items: center; gap: 14px; padding: 14px 16px;
|
||||
@@ -19,22 +19,25 @@
|
||||
.select-bar {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 16px 0; border-bottom: 1px solid #e2e8f0; margin-bottom: 8px;
|
||||
flex-wrap: wrap; gap: 8px;
|
||||
}
|
||||
.select-bar label { font-weight: 600; color: #475569; font-size: 14px; cursor: pointer; }
|
||||
.signin-btn {
|
||||
padding: 14px 32px; border-radius: 16px; border: none; font-size: 16px;
|
||||
font-weight: 600; cursor: pointer; color: white;
|
||||
background: linear-gradient(135deg, #6366f1, #818cf8);
|
||||
box-shadow: 0 2px 12px rgba(99,102,241,0.3); transition: all 0.2s;
|
||||
}
|
||||
.signin-btn:hover { box-shadow: 0 4px 20px rgba(99,102,241,0.4); transform: translateY(-1px); }
|
||||
.signin-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
||||
.result-box {
|
||||
margin-top: 20px; padding: 16px; border-radius: 14px; display: none;
|
||||
font-size: 14px; font-weight: 500;
|
||||
.action-btns { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.signin-btn, .save-btn {
|
||||
padding: 10px 20px; border-radius: 12px; border: none; font-size: 14px;
|
||||
font-weight: 600; cursor: pointer; color: white; transition: all 0.2s;
|
||||
}
|
||||
.signin-btn { background: linear-gradient(135deg, #6366f1, #818cf8); box-shadow: 0 2px 8px rgba(99,102,241,0.25); }
|
||||
.save-btn { background: linear-gradient(135deg, #10b981, #059669); box-shadow: 0 2px 8px rgba(16,185,129,0.25); }
|
||||
.signin-btn:disabled, .save-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.result-box { margin-top: 16px; padding: 14px; border-radius: 12px; display: none; font-size: 13px; font-weight: 500; }
|
||||
.result-success { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
|
||||
.result-error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
|
||||
.tip-box { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; border-radius: 12px; padding: 12px 16px; margin-bottom: 16px; font-size: 13px; }
|
||||
@media (max-width: 768px) {
|
||||
.action-btns { width: 100%; }
|
||||
.signin-btn, .save-btn { flex: 1; text-align: center; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -42,24 +45,35 @@
|
||||
<div style="max-width: 720px; margin: 0 auto;">
|
||||
<div class="topics-header">
|
||||
<div>
|
||||
<h1>🔥 选择签到超话</h1>
|
||||
<div style="color:#94a3b8; font-size:14px; margin-top:4px;">
|
||||
<h1>🔥 超话签到管理</h1>
|
||||
<div style="color:#94a3b8; font-size:13px; margin-top:4px;">
|
||||
{{ account.remark or account.weibo_user_id }} · 共 {{ topics|length }} 个超话
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('account_detail', account_id=account.id) }}" class="btn btn-secondary">← 返回</a>
|
||||
</div>
|
||||
|
||||
<div class="tip-box">
|
||||
💡 勾选要参与定时签到的超话,点击「保存选择」后,定时任务和手动签到都只签选中的超话。不选则签到全部。
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if topics %}
|
||||
<div class="select-bar">
|
||||
<label><input type="checkbox" id="selectAll" class="topic-cb" checked onchange="toggleAll()"> 全选 (<span id="selectedCount">{{ topics|length }}</span>/{{ topics|length }})</label>
|
||||
<button class="signin-btn" id="signinBtn" onclick="doSignin()">🚀 签到选中超话</button>
|
||||
<label><input type="checkbox" id="selectAll" class="topic-cb" onchange="toggleAll()"> 全选 (<span id="selectedCount">0</span>/{{ topics|length }})</label>
|
||||
<div class="action-btns">
|
||||
<button class="save-btn" id="saveBtn" onclick="saveSelection()">💾 保存选择</button>
|
||||
<button class="signin-btn" id="signinBtn" onclick="doSignin()">🚀 立即签到选中</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-list" id="topicList">
|
||||
{% for topic in topics %}
|
||||
<label class="topic-item">
|
||||
<input type="checkbox" class="topic-cb topic-check" data-index="{{ loop.index0 }}" checked onchange="updateCount()">
|
||||
<input type="checkbox" class="topic-cb topic-check"
|
||||
data-index="{{ loop.index0 }}"
|
||||
data-title="{{ topic.title }}"
|
||||
data-cid="{{ topic.containerid }}"
|
||||
onchange="updateCount()">
|
||||
<div>
|
||||
<div class="topic-name">{{ topic.title }}</div>
|
||||
<div class="topic-id">{{ topic.containerid }}</div>
|
||||
@@ -78,6 +92,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 已保存的选中超话
|
||||
const savedTopics = {{ (selected_topics or [])|tojson }};
|
||||
const savedCids = new Set(savedTopics.map(t => t.containerid));
|
||||
|
||||
// 页面加载时恢复选中状态
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const checks = document.querySelectorAll('.topic-check');
|
||||
if (savedCids.size > 0) {
|
||||
checks.forEach(cb => {
|
||||
cb.checked = savedCids.has(cb.dataset.cid);
|
||||
});
|
||||
} else {
|
||||
// 没有保存过 = 全部选中
|
||||
checks.forEach(cb => cb.checked = true);
|
||||
}
|
||||
updateCount();
|
||||
});
|
||||
|
||||
function toggleAll() {
|
||||
const checked = document.getElementById('selectAll').checked;
|
||||
document.querySelectorAll('.topic-check').forEach(cb => cb.checked = checked);
|
||||
@@ -91,6 +123,49 @@ function updateCount() {
|
||||
document.getElementById('selectAll').checked = (checked === total);
|
||||
}
|
||||
|
||||
function getSelectedTopics() {
|
||||
const selected = [];
|
||||
document.querySelectorAll('.topic-check:checked').forEach(cb => {
|
||||
selected.push({ title: cb.dataset.title, containerid: cb.dataset.cid });
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
async function saveSelection() {
|
||||
const btn = document.getElementById('saveBtn');
|
||||
const resultBox = document.getElementById('resultBox');
|
||||
const selected = getSelectedTopics();
|
||||
const total = document.querySelectorAll('.topic-check').length;
|
||||
|
||||
btn.disabled = true; btn.textContent = '⏳ 保存中...';
|
||||
try {
|
||||
// 全选时传 null(签到全部)
|
||||
const body = (selected.length === total)
|
||||
? { selected_topics: null }
|
||||
: { selected_topics: selected };
|
||||
|
||||
const resp = await fetch('/accounts/{{ account.id }}/topics/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await resp.json();
|
||||
resultBox.style.display = 'block';
|
||||
if (data.success) {
|
||||
resultBox.className = 'result-box result-success';
|
||||
resultBox.textContent = '✅ ' + data.message;
|
||||
} else {
|
||||
resultBox.className = 'result-box result-error';
|
||||
resultBox.textContent = data.message || '保存失败';
|
||||
}
|
||||
} catch(e) {
|
||||
resultBox.className = 'result-box result-error';
|
||||
resultBox.style.display = 'block';
|
||||
resultBox.textContent = '请求失败: ' + e.message;
|
||||
}
|
||||
btn.disabled = false; btn.textContent = '💾 保存选择';
|
||||
}
|
||||
|
||||
async function doSignin() {
|
||||
const btn = document.getElementById('signinBtn');
|
||||
const resultBox = document.getElementById('resultBox');
|
||||
@@ -98,18 +173,14 @@ async function doSignin() {
|
||||
document.querySelectorAll('.topic-check:checked').forEach(cb => {
|
||||
indices.push(parseInt(cb.dataset.index));
|
||||
});
|
||||
|
||||
if (indices.length === 0) {
|
||||
resultBox.className = 'result-box result-error';
|
||||
resultBox.style.display = 'block';
|
||||
resultBox.textContent = '请至少选择一个超话';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ 签到中...';
|
||||
btn.disabled = true; btn.textContent = '⏳ 签到中...';
|
||||
resultBox.style.display = 'none';
|
||||
|
||||
try {
|
||||
const resp = await fetch('{{ url_for("signin_selected", account_id=account.id) }}', {
|
||||
method: 'POST',
|
||||
@@ -117,7 +188,6 @@ async function doSignin() {
|
||||
body: JSON.stringify({topic_indices: indices}),
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
resultBox.style.display = 'block';
|
||||
if (data.success) {
|
||||
resultBox.className = 'result-box result-success';
|
||||
@@ -126,14 +196,12 @@ async function doSignin() {
|
||||
resultBox.className = 'result-box result-error';
|
||||
resultBox.textContent = data.message || '签到失败';
|
||||
}
|
||||
} catch (e) {
|
||||
} catch(e) {
|
||||
resultBox.className = 'result-box result-error';
|
||||
resultBox.style.display = 'block';
|
||||
resultBox.textContent = '请求失败: ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🚀 签到选中超话';
|
||||
}
|
||||
btn.disabled = false; btn.textContent = '🚀 立即签到选中';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS accounts (
|
||||
encrypted_cookies TEXT NOT NULL,
|
||||
iv VARCHAR(32) NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
selected_topics JSON DEFAULT NULL,
|
||||
last_checked_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_accounts_user_id (user_id),
|
||||
|
||||
4
migrate_add_selected_topics.sql
Normal file
4
migrate_add_selected_topics.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 给 accounts 表添加 selected_topics 字段
|
||||
-- 用法: mysql -u weibo -p123456 weibo_hotsign < migrate_add_selected_topics.sql
|
||||
|
||||
ALTER TABLE accounts ADD COLUMN selected_topics JSON DEFAULT NULL AFTER status;
|
||||
Reference in New Issue
Block a user