feat(database): 迁移用户关联字段并优化打卡系统

- 修改数据库配置将MYSQL_DB从mys_info_v3改为mys_info_test
- 新增auth_users表wechat_openid字段并建立唯一索引
- 为plan_checkins表添加计划性能索引(idx_plan_period_student)
- 将checkin_records_old表关联方式从student_id改为user_id
- 将plan_checkins表关联方式从student_id改为user_id并更新附件路径
- 移除routes/auth.py中的uuid导入使用安全token生成函数
- 重构routes/checkin.py打卡逻辑去除course_type字段并优化返回结构
- 添加打卡时间限制验证功能防止过期打卡操作
- 实现小组统计功能支持老师查看各组打卡情况
- 更新主页index.html界面适配新的打卡和计划功能
- 调整计划提交时间规则为周五至周四周期
- 添加月度和年度计划提交状态验证机制
This commit is contained in:
王宏豪
2026-09-10 14:11:59 +08:00
parent 4fbcff2bb4
commit 9a7681112b
17 changed files with 876 additions and 168 deletions

258
index.html Normal file
View File

@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="expires" content="0">
<title>明缘进阶场 · 薪火相传</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--gold:#C67B30;--gold-light:#E8C99B;--warm-white:#FDFBF7;--ink:#2C1810;--ink-l:#5A4A3A;--fs:"Inter","PingFang SC","Noto Sans SC",sans-serif;--fser:"Noto Serif SC",serif}
html{scroll-behavior:smooth}
body{font-family:var(--fs);background:var(--warm-white);color:var(--ink-l);line-height:1.6;min-height:100vh;display:flex;justify-content:center;background-image:radial-gradient(circle at 10% 20%, rgba(198,123,48,0.06) 0%, transparent 50%),radial-gradient(circle at 90% 80%, rgba(192,57,43,0.04) 0%, transparent 50%)}
.wrap{max-width:960px;width:100%;margin:0 auto;padding:28px 24px 20px;display:flex;flex-direction:column;min-height:100vh}
header{text-align:center;margin-bottom:32px;padding:20px 0 16px;position:relative}
header::after{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:80px;height:2px;background:linear-gradient(90deg,transparent,var(--gold),transparent)}
header h1{font-family:var(--fser);font-size:40px;font-weight:900;color:var(--ink);margin-bottom:6px;letter-spacing:8px;text-shadow:0 2px 4px rgba(0,0,0,0.02)}
header .greeting-wrap{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:10px;flex-wrap:wrap}
header .greeting-wrap .name{font-size:20px;font-weight:700;color:var(--gold)}
header .greeting-wrap .msg{font-size:14px;color:#888;background:rgba(198,123,48,0.08);padding:2px 14px;border-radius:20px;border:1px solid rgba(198,123,48,0.12)}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px;align-content:start}
.choice{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;padding:20px 22px;border-radius:16px;background:rgba(255,255,255,0.75);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);text-decoration:none;transition:all 0.35s cubic-bezier(0.25,0.46,0.45,0.94);gap:8px;box-shadow:0 4px 20px rgba(0,0,0,0.04),0 1px 3px rgba(0,0,0,0.02);border:1px solid rgba(255,255,255,0.9);min-height:120px;position:relative;overflow:hidden}
.choice::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,transparent 60%,rgba(198,123,48,0.03));pointer-events:none;border-radius:16px}
.choice:hover{transform:translateY(-4px);box-shadow:0 20px 40px rgba(44,24,16,0.08),0 4px 12px rgba(198,123,48,0.10);border-color:rgba(198,123,48,0.3);background:rgba(255,255,255,0.92)}
.choice .row{display:flex;align-items:center;gap:14px;width:100%}
.choice .icon{font-size:34px;flex-shrink:0;line-height:1;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.04));margin-bottom:2px}
.choice .label{font-size:18px;font-weight:700;color:var(--ink);letter-spacing:1px;transition:color .2s}
.choice:hover .label{color:var(--gold)}
.choice .desc{font-size:13px;color:#999;line-height:1.4;letter-spacing:0.3px}
.choice .badge{display:inline-block;background:rgba(192,57,43,0.06);color:#c0392b;font-size:11px;padding:0 10px;border-radius:12px;font-weight:500;border:1px solid rgba(192,57,43,0.08);margin-left:8px;line-height:20px}
.choice.virtual{background:linear-gradient(135deg,#fdfcf9,#f7f0e6);border:1px dashed rgba(198,123,48,0.3)}
.choice.virtual:hover{border-color:var(--gold);background:#fcf8f0}
.choice.hidden{display:none}
.bottom-links{text-align:center;padding:18px 0 4px;font-size:13px;color:#bbb;border-top:1px solid rgba(0,0,0,0.04);margin-top:auto}
.bottom-links a{color:#8B6B4A;text-decoration:none;margin:0 8px;transition:color .2s;font-weight:450}
.bottom-links a:hover{color:var(--gold);text-decoration:none}
.bottom-links .sep{color:#ddd;margin:0 2px}
.bottom-links .ver{color:#ccc;font-size:12px;margin-left:8px}
@media(max-width:800px){.grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:550px){.grid{grid-template-columns:1fr}.choice{min-height:auto;padding:16px 18px}.choice .icon{font-size:28px}.choice .label{font-size:16px}.wrap{padding:16px 14px}header h1{font-size:30px;letter-spacing:4px}}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>明缘进阶场</h1>
<div class="greeting-wrap">
<span class="name" id="welcome">加载中...</span>
<span class="msg" id="greeting"></span>
<span class="enc-line" id="encLine" style="font-size:13px;color:#8B6B4A;margin-top:4px;font-style:italic;opacity:0.85;display:block"></span><p class="enc-line" id="encLine" style="font-size:13px;color:#8B6B4A;margin-top:6px;font-style:italic;opacity:0.85"></p>
</div>
</header>
<div class="grid" id="grid">
<a href="/xinhuo/" class="choice" id="btn_xh">
<div class="row">
<div class="icon">🔥</div>
<div>
<div class="label">薪火相传</div>
<div class="desc">年课 / 终身学习小组</div>
</div>
</div>
</a>
<a href="/19qi/" class="choice hidden" id="btn_19">
<div class="row">
<div class="icon">📖</div>
<div>
<div class="label">19期研修</div>
<div class="desc">小班课 · 第19期</div>
</div>
</div>
</a>
<a href="/18qi/" class="choice hidden" id="btn_18">
<div class="row">
<div class="icon">📖</div>
<div>
<div class="label">18期研修</div>
<div class="desc">小班课 · 第18期</div>
</div>
</div>
</a>
<a href="/20qi/" class="choice hidden" id="btn_20">
<div class="row">
<div class="icon">📖</div>
<div>
<div class="label">20期小班课</div>
<div class="desc">小班课 · 第20期</div>
</div>
</div>
</a>
<a href="/xinhuo/upgrade-info.html" class="choice" id="btn_teacher">
<div class="row">
<div class="icon">🎓</div>
<div>
<div class="label">老师大班授权</div>
<div class="desc">年课 / 长期学员入口</div>
</div>
</div>
</a>
</a>
<a href="/xinhuo360/" class="choice" id="btn_360"><div class="row"><div class="icon">🧬</div><div><div class="label">学员360</div><div class="desc">薪火相传 · 个人成长档案</div></div></div></a><a href="/quant-courses.html" class="choice" id="btn_quant"><div class="row"><div class="icon">📊</div><div><div class="label">量化课程</div><div class="desc">量化课程内容 · 随课程方上传更新</div></div></div></a><a href="/reading-club.html" class="choice" id="btn_reading"><div class="row"><div class="icon">📚</div><div><div class="label">读书会</div><div class="desc">经典原著 · 共读课程</div></div></div></a><a href="/course-assistant.html" class="choice hidden" id="btn_assistant"><div class="row"><div class="icon">🎓</div><div><div class="label">课程助理</div><div class="desc">老师专属 · 逐页讲解</div></div></div></a>
<a href="/xinhuo/virtual-group.html" class="choice virtual" id="btn_demo">
<div class="row">
<div class="icon">🧭</div>
<div>
<div class="label">功能演示</div>
<div class="desc">先看看讨论和量化报告的样子</div>
</div>
</div>
</a>
<a href="yijing/" class="choice" id="btn_yijing"><div class="row"><div class="icon">☯</div><div><div class="label">易经学习</div><div class="desc">周易六十四卦 · 经文详解</div></div></div></a>
<a href="homework/" class="choice hidden" id="btn_hw"><div class="row"><div class="icon">📝</div><div><div class="label">20期作业评价</div><div class="desc">回嗔作喜 · 观过思仁 · 温故知新</div></div></div></a>
<a href="checkin/index.html" class="choice" id="btn_checkin">
<div class="row">
<div class="icon">&#128203;</div>
<div>
<div class="label">上课打卡</div>
<div class="desc">会员课程 · 小组讨论考勤</div>
</div>
</div>
</a>
<a href="plan/index.html" class="choice" id="btn_plan">
<div class="row">
<div class="icon">&#9989;</div>
<div>
<div class="label">计划打卡</div>
<div class="desc">周计划 · 月计划 · 年计划</div>
</div>
</div>
</a>
</div>
<div class="bottom-links">
<a href="/feedback.html">问题反馈</a><span class="sep">|</span>
<a href="/changelog.html">更新日志</a><span class="sep">|</span>
<a href="/help.html">使用帮助</a><span class="sep">|</span>
<a href="#" id="logoutBtn">退出登录</a>
<span class="ver" id="versionInfo">V3.1</span>
</div>
</div>
<script>
(async function(){
try{
var vr=await fetch("/api/auth/verify");var vd=await vr.json();
if(!vd.success){window.location.href="/auth/login.html";return;}
var myName=vd.name||"";var displayName=vd.display_name||myName;var myRole=vd.role||"student";var isTeacher=(myRole==="teacher");var sid=vd.student_id||"";
document.getElementById("welcome").textContent=displayName||"欢迎回来";
var h=new Date().getHours();var greet="";
if(h<6)greet="夜深了,还在用功";
else if(h<9)greet="早上好,今天又起了一个大早";
else if(h<12)greet="上午好";
else if(h<14)greet="中午好,记得吃饭";
else if(h<18)greet="下午好";
else if(h<21)greet="晚上好";
else greet="夜深了,早点休息";
document.getElementById("greeting").textContent=greet;
var ps=[{n:"薪火相传",b:document.getElementById("btn_xh"),d:"xinhuo"},{n:"19期",b:document.getElementById("btn_19"),d:"19qi"},{n:"18期",b:document.getElementById("btn_18"),d:"18qi"},{n:"20期",b:document.getElementById("btn_20"),d:"20qi"}];
ps.forEach(function(p){
var mg=null;
if(vd.class_groups){for(var j=0;j<vd.class_groups.length;j++){if(vd.class_groups[j].period===p.n){mg=vd.class_groups[j];break;}}}
if(!mg&&p.n==="薪火相传"&&vd.group){mg={group:vd.group,period:"薪火相传",role:"组员"};}
if(isTeacher){p.b.classList.remove("hidden");}
else if(p.n==="薪火相传"&&(sid.indexOf("N")===0||sid.indexOf("0")===0)){p.b.classList.remove("hidden");p.b.href="/xinhuo/index.html";}
else if(mg){p.b.classList.remove("hidden");var g=(mg.group||"").replace("组","").trim();if(g.length===1)g="0"+g;if(/^\d+$/.test(g)){p.b.href="/"+p.d+"/"+"No"+parseInt(g,10)+".html";}else{p.b.href="/"+p.d+"/free-group.html";}}
else{p.b.classList.add("hidden");}
});
var hwBtn=document.getElementById("btn_hw");
if(hwBtn){
var is20=false;
if(vd.class_groups){for(var j=0;j<vd.class_groups.length;j++){if(vd.class_groups[j].period==="20期"){is20=true;break;}}}
if(isTeacher||is20){hwBtn.classList.remove("hidden");}
}
var assistBtn=document.getElementById("btn_assistant");
if(assistBtn){
if(isTeacher){assistBtn.classList.remove("hidden");}
else{assistBtn.classList.add("hidden");}
}
var bt=document.getElementById("btn_teacher");
if(bt){
if(isTeacher||sid.startsWith("N")||sid.startsWith("0")){bt.href="/teacher/";}
else{bt.href="/xinhuo/upgrade-info.html";}
}
// 功能演示:仅自由学习组/周计划组成员可见(自由学习组含薪火相传与小班课)
var demoBtn=document.getElementById("btn_demo");
if(demoBtn){
var isFreeOrPlan=false;
if(vd.class_groups){for(var j=0;j<vd.class_groups.length;j++){
var gg=(vd.class_groups[j].group||"");
if(gg==="自由学习组"||gg==="自由"||gg==="周计划"){isFreeOrPlan=true;break;}
}}
if(isTeacher||isFreeOrPlan){demoBtn.classList.remove("hidden");}
else{demoBtn.classList.add("hidden");}
}
// 学员360:老师进索引,学生直接进自己的报告/吸引力页
var b360=document.getElementById("btn_360");
if(b360){
if(isTeacher){b360.href="/xinhuo360/index.html";}
else{
fetch("/xinhuo360/_user360_versions.json?t="+Date.now(),{cache:"no-store"})
.then(function(r){return r.json();})
.then(function(d){
var ks=Object.keys(d.versions||{});
for(var i=0;i<ks.length;i++){if(ks[i].indexOf(sid+"-")===0){b360.href="/xinhuo360/"+ks[i]+"?t="+Date.now();return;}}
}).catch(function(){});
}
}
// Random encouragement
var periods = ["xinhuo","19qi","18qi","20qi"];
var rp = periods[Math.floor(Math.random()*periods.length)];
var encUrl = "";
var grp = vd.group || "";
if (grp === "自由学习组" || grp === "周计划") {
encUrl = "/" + rp + "/_encouragements_free.json?t=" + Date.now();
} else if (isTeacher) {
var rg = ("0"+Math.floor(Math.random()*33+1)).slice(-2);
encUrl = "/xinhuo/reports/" + rg + "组/_encouragements.json?t=" + Date.now();
} else {
var ownG = (grp||"").replace("组","").trim();
if (ownG && /^\d+$/.test(ownG)) {
if (ownG.length===1) ownG="0"+ownG;
encUrl = "/" + rp + "/reports/" + ownG + "组/_encouragements.json?t=" + Date.now();
}
}
if (encUrl) {
fetch(encUrl).then(function(r){return r.ok?r.json():null}).then(function(d){
if(!d) return;
var pool = (d.team||[]).slice();
if (d.intro) pool = pool.concat(d.intro);
var myMsgs = (d.members||{})[myName] || (d.members||{})[sid] || [];
pool = pool.concat(myMsgs);
if (pool.length) {
var msg = pool[Math.floor(Math.random()*pool.length)];
var memberNames = Object.keys(d.members || {});
for (var ni = 0; ni < memberNames.length; ni++) {
if (msg.indexOf(memberNames[ni]) >= 0) { msg = '有同学说:' + msg; break; }
}
document.getElementById("encLine").textContent = msg;
}
}).catch(function(){});
}
document.getElementById("logoutBtn").onclick=function(e){e.preventDefault();window.location.href="/api/auth/logout?redirect=/auth/login.html";};
}catch(e){window.location.href="/auth/login.html";}
})();
</script>
</body>
</html>