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:58 +08:00
parent 1dd4fa3f41
commit cd04c1b5ef
19 changed files with 1457 additions and 407 deletions

View File

@@ -1,6 +1,7 @@
<view class="page">
<block wx:if="{{view === 'overview'}}">
<view class="view-root">
<!-- 顶部类型切换 -->
<view class="type-tabs">
<view
class="type-tab {{typeIndex === index ? 'active' : ''}}"
@@ -19,14 +20,20 @@
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onRefresh"
>
<!-- 头部:类型 + 我的状态 -->
<view class="overview-head">
<view class="head-title-wrap">
<view class="head-title">{{types[typeIndex]}}</view>
<view class="head-sub">{{periodKey}}</view>
<view class="head-sub" wx:if="{{isTeacher}}" bindtap="{{periodList.length > 0 ? 'openPeriodPicker' : ''}}" hover-class="{{periodList.length > 0 ? 'head-hover' : ''}}">
{{periodKey}}
<text class="head-arrow" wx:if="{{periodList.length > 0}}"> ▾</text>
</view>
<view class="head-sub" wx:else>{{periodKey}}</view>
</view>
<view class="head-pill">{{myChecked ? '已提交' : '待提交'}}</view>
<view class="head-pill {{myChecked ? 'checked' : ''}}">{{myChecked ? '已提交' : '待提交'}}</view>
</view>
<!-- 我的计划卡片 -->
<view class="card my-status {{myChecked ? 'checked' : 'unchecked'}}">
<view class="my-state-icon {{myChecked ? 'done' : 'todo'}}">{{myChecked ? '✓' : '●'}}</view>
<view class="my-status-left">
@@ -37,59 +44,141 @@
class="btn-mystatus {{myChecked ? 'outline' : ''}}"
bindtap="goSubmit"
hover-class="btn-hover"
disabled="{{typeIndex === 0 && !canSubmit}}"
>{{myChecked ? '修改' : '新增打卡'}}</button>
disabled="{{!canSubmit}}"
>{{myChecked ? '修改' : '提交计划'}}</button>
</view>
<view class="card my-plan" wx:if="{{myChecked && myRecord}}">
<view class="my-plan-head">
<text class="my-plan-title">我上传的计划</text>
<text class="my-plan-status {{myRecord.status === '未完成' ? 'pending' : ''}}">{{myRecord.status}}</text>
</view>
<view class="my-plan-meta" wx:if="{{myRecord.checked_at}}">{{myRecord.checked_at}} 提交</view>
<view class="my-plan-content" wx:if="{{myRecord.content}}">{{myRecord.content}}</view>
<view class="my-plan-thumbs" wx:if="{{myThumbs.length}}">
<image class="my-plan-thumb" wx:for="{{myThumbs}}" wx:key="*this" src="{{item}}" mode="aspectFill" data-index="{{index}}" bindtap="previewMyImage"></image>
</view>
</view>
<view class="window-msg" wx:if="{{typeIndex === 0 && message}}">{{message}}</view>
<!-- 提交时间窗口提示 -->
<view class="window-msg" wx:if="{{message}}">{{message}}</view>
<!-- 加载状态 -->
<view class="state-block" wx:if="{{loading && groups.length === 0}}">
<view class="state-icon">◌</view>
<view>加载中...</view>
</view>
<view class="group-card" hover-class="group-card-hover" wx:for="{{groups}}" wx:key="group_name" wx:for-item="group">
<view class="group-main" data-group="{{group.group_name}}" bindtap="openGroup">
<view class="group-row">
<view class="group-avatar">{{index + 1}}</view>
<view class="group-name">{{group.group_name}}</view>
<view class="group-counts">
<text class="count done">已打卡 {{group.checked_count}}</text>
<text class="count total">{{group.total}}人</text>
<!-- 老师:显示所有小组列表 -->
<block wx:if="{{isTeacher}}">
<view class="section-title">小组统计</view>
<view class="group-card" hover-class="group-card-hover" wx:for="{{groups}}" wx:key="group_name" wx:for-item="group">
<view class="group-main" data-group="{{group.group_name}}" bindtap="openGroup">
<view class="group-row">
<view class="group-avatar">{{index + 1}}</view>
<view class="group-name">{{group.group_name}}</view>
<view class="group-counts">
<text class="count done">已提交 {{group.checked_count}}</text>
<text class="count total">{{group.total}}人</text>
</view>
<view class="chevron">›</view>
</view>
<view class="progress-track">
<view class="progress-bar" style="{{group.progressStyle}}"></view>
</view>
<view class="chevron">›</view>
</view>
<view class="progress-track">
<view class="progress-bar" style="width: {{group.percent}}%;"></view>
</view>
</view>
</view>
<view class="unassigned" wx:if="{{unassignedCount > 0}}">另有未分组 {{unassignedCount}} 人</view>
<view class="empty" wx:if="{{!loading && groups.length === 0}}">暂无小组</view>
<view class="unassigned" wx:if="{{unassignedCount > 0}}">另有未分组 {{unassignedCount}} 人</view>
<view class="empty" wx:if="{{!loading && groups.length === 0}}">暂无小组</view>
</block>
<!-- 学员:tab 切换(小组/个人) -->
<block wx:else>
<view class="student-tabs">
<view class="student-tab {{studentTab === 'group' ? 'active' : ''}}" data-tab="group" hover-class="tab-hover" bindtap="switchStudentTab">小组</view>
<view class="student-tab {{studentTab === 'personal' ? 'active' : ''}}" data-tab="personal" hover-class="tab-hover" bindtap="switchStudentTab">个人</view>
</view>
<!-- 小组 tab:本组成员列表 -->
<view wx:if="{{studentTab === 'group'}}" class="student-group-panel">
<view
class="group-period-row"
wx:if="{{myGroup}}"
bindtap="{{periodList.length > 0 ? 'openPeriodPicker' : ''}}"
hover-class="{{periodList.length > 0 ? 'head-hover' : ''}}"
>
<text class="group-period-text">{{myGroup}} · {{periodKey}}</text>
<text class="head-arrow" wx:if="{{periodList.length > 0}}"> ▾</text>
</view>
<view class="state-block" wx:if="{{loading && members.length === 0}}">
<view class="state-icon">◌</view>
<view>加载中...</view>
</view>
<view class="member-list" wx:elif="{{members.length > 0}}">
<view class="member" wx:for="{{members}}" wx:key="user_id" wx:for-item="member">
<view class="member-left">
<view class="member-avatar {{member.filled ? 'done' : 'unchecked'}}">{{index + 1}}</view>
<view class="member-info">
<view class="member-name">{{member.name}}</view>
<view class="member-meta" wx:if="{{member.filled && member.record}}">{{member.record.checked_at}} 提交</view>
</view>
</view>
<view class="badge {{member.filled ? 'done' : 'unchecked'}}">{{member.filled ? '已完成' : '未完成'}}</view>
</view>
</view>
<view class="empty" wx:elif="{{!loading && !myGroup}}">
<view class="empty-icon">👥</view>
<view class="empty-text">暂无小组,请联系管理员分配</view>
</view>
<view class="empty" wx:elif="{{!loading && members.length === 0}}">
<view class="empty-icon">📭</view>
<view class="empty-text">暂无成员数据</view>
</view>
</view>
<!-- 个人 tab:历史记录 -->
<view wx:else class="student-history-panel">
<view class="empty" wx:if="{{history.length === 0}}">
<view class="empty-icon">📒</view>
<view class="empty-text">暂无{{types[typeIndex]}}记录</view>
</view>
<view class="history-card" wx:for="{{history}}" wx:key="id">
<view class="history-head">
<view class="history-period">{{item.period_key}}</view>
<view class="history-status" style="color:{{item.statusColor}};background:{{item.statusBg}}">{{item.status}}</view>
</view>
<view class="history-meta" wx:if="{{item.checked_at}}">{{item.checked_at}} 提交</view>
<view class="history-content" wx:if="{{item.content}}">{{item.content}}</view>
<view class="history-thumbs" wx:if="{{item.thumbs.length > 0}}">
<image class="history-thumb" wx:for="{{item.thumbs}}" wx:for-item="thumb" wx:key="*this" src="{{thumb}}" mode="aspectFill"></image>
</view>
</view>
</view>
</block>
<!-- 底部留白 -->
<view class="bottom-space"></view>
</scroll-view>
<!-- 周期选择弹层 -->
<view class="picker-mask" wx:if="{{showPeriodPicker}}" bindtap="closePeriodPicker"></view>
<view class="picker-panel {{showPeriodPicker ? 'show' : ''}}" wx:if="{{showPeriodPicker}}">
<view class="picker-title">选择周期</view>
<scroll-view class="picker-list" scroll-y>
<view
class="picker-item {{item === periodKey ? 'active' : ''}}"
wx:for="{{periodList}}"
wx:key="*this"
data-period="{{item}}"
bindtap="onPeriodSelect"
>
<text>{{item}}</text>
<text class="picker-check" wx:if="{{item === periodKey}}">✓</text>
</view>
</scroll-view>
</view>
</view>
</block>
<!-- 小组详情页 -->
<block wx:else>
<view class="view-root">
<view class="detail-head">
<view class="back" hover-class="back-hover" bindtap="backToOverview">‹ 返回</view>
<view class="detail-title-wrap">
<view class="head-title">{{selectedGroupName}}</view>
<view class="head-sub">{{periodKey}}</view>
<view class="head-sub" bindtap="{{periodList.length > 0 ? 'openPeriodPicker' : ''}}" hover-class="{{periodList.length > 0 ? 'head-hover' : ''}}">
{{periodKey}} <text class="head-arrow" wx:if="{{periodList.length > 0}}"> ▾</text>
</view>
</view>
</view>
<scroll-view class="view-scroll" scroll-y>
@@ -111,6 +200,24 @@
</view>
<view class="empty" wx:if="{{!loading && members.length === 0}}">暂无成员</view>
</scroll-view>
<!-- 周期选择弹层(详情页也可用) -->
<view class="picker-mask" wx:if="{{showPeriodPicker}}" bindtap="closePeriodPicker"></view>
<view class="picker-panel {{showPeriodPicker ? 'show' : ''}}" wx:if="{{showPeriodPicker}}">
<view class="picker-title">选择周期</view>
<scroll-view class="picker-list" scroll-y>
<view
class="picker-item {{item === periodKey ? 'active' : ''}}"
wx:for="{{periodList}}"
wx:key="*this"
data-period="{{item}}"
bindtap="onPeriodSelect"
>
<text>{{item}}</text>
<text class="picker-check" wx:if="{{item === periodKey}}">✓</text>
</view>
</scroll-view>
</view>
</view>
</block>
</view>
</view>