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,27 +1,48 @@
const api = require('../../utils/api');
const auth = require('../../utils/auth');
// 计划类型常量
const TYPES = ['weekly', 'monthly', 'yearly'];
const TYPE_LABELS = ['周计划', '月计划', '年计划'];
Page({
data: {
// 类型切换相关
types: TYPE_LABELS,
typeIndex: 0,
view: 'overview', // overview | detail
// 视图模式:overview 总览 / detail 小组详情(仅老师)
view: 'overview',
// 加载状态
loading: false,
refreshing: false,
// 当前周期
periodKey: '',
// 提交相关
canSubmit: false,
message: '',
// 我的计划状态
myChecked: false,
myStatus: '',
myRecord: null,
myThumbs: [],
// 未分组人数(仅老师可见)
unassignedCount: 0,
// 小组列表
groups: [],
// 当前选中的小组
selectedGroupName: '',
members: []
// 小组成员列表(老师详情 & 学员小组 tab)
members: [],
// 用户角色
isTeacher: false,
myGroup: '',
// 学员视图 tab:group / personal
studentTab: 'group',
// 个人历史列表
history: [],
// 周期切换相关
periodList: [], // 可用周期列表
showPeriodPicker: false,
},
onShow() {
@@ -33,53 +54,141 @@ Page({
}
wx.removeStorageSync('plan_overview_dirty');
this.setData({ view: 'overview' });
this.loadOverview();
}).catch(() => {});
// 先加载周期列表,再加载总览(总览接口会返回 is_teacher)
this.loadPeriods().then(() => {
this.loadOverview();
});
}).catch((err) => {
console.error('plan page init error:', err);
});
},
/** 切换计划类型(周/月/年) */
onTypeChange(e) {
const typeIndex = Number(e.currentTarget.dataset.index);
if (typeIndex === this.data.typeIndex) {
return;
}
this.setData({ typeIndex, view: 'overview', selectedGroupName: '', members: [] });
this.loadOverview();
this.setData({ typeIndex, view: 'overview', selectedGroupName: '', members: [], history: [], studentTab: 'group' });
// 切换类型后重新加载周期列表和总览
this.loadPeriods().then(() => {
this.loadOverview();
});
},
/** 下拉刷新 */
onRefresh() {
this.setData({ refreshing: true });
this.loadOverview();
this.loadPeriods().then(() => {
return this.loadOverview();
}).then(() => {
// 学员个人 tab 也刷一下历史
if (!this.data.isTeacher && this.data.studentTab === 'personal') {
return this.loadHistory();
}
}).then(() => {
this.setData({ refreshing: false });
});
},
/** 加载可用周期列表 */
loadPeriods() {
const type = TYPES[this.data.typeIndex];
return api.request({ url: '/api/v1/plan/periods', data: { type } })
.then((res) => {
const periods = res.periods || [];
// 默认选第一个(最新)周期,或者后端返回的当前周期
let periodKey = periods[0] || res.current_period || '';
this.setData({
periodList: periods,
periodKey: periodKey || this.data.periodKey,
});
})
.catch(() => {
// 获取失败不影响主流程
console.warn('获取周期列表失败');
});
},
/** 打开周期选择器 */
openPeriodPicker() {
if (!this.data.periodList.length) {
return;
}
this.setData({ showPeriodPicker: true });
},
/** 关闭周期选择器 */
closePeriodPicker() {
this.setData({ showPeriodPicker: false });
},
/** 选择某个周期 */
onPeriodSelect(e) {
const period = e.currentTarget.dataset.period;
if (!period || period === this.data.periodKey) {
this.setData({ showPeriodPicker: false });
return;
}
this.setData({
periodKey: period,
showPeriodPicker: false,
});
if (this.data.view === 'detail') {
// 详情页:重新加载当前小组的该周期数据
this.loadDetail();
} else if (!this.data.isTeacher && this.data.studentTab === 'group' && this.data.myGroup) {
// 学员总览 + 小组 tab:刷新本组成员
this.loadOverview();
} else {
// 老师总览 / 学员个人 tab:重新加载总览
this.loadOverview();
}
},
/** 加载总览数据 */
loadOverview() {
const type = TYPES[this.data.typeIndex];
const periodKey = this.data.periodKey;
this.setData({ loading: true });
api.request({ url: '/api/v1/plan/groups', data: { type } })
return api.request({ url: '/api/v1/plan/groups', data: { type, period_key: periodKey } })
.then((res) => {
const groups = (res.groups || []).map((g) => ({
...g,
percent: g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0
}));
const groups = (res.groups || []).map((g) => {
const percent = g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0;
return {
...g,
percent,
progressStyle: 'width:' + percent + '%;'
};
});
const isTeacher = !!res.is_teacher;
this.setData({
loading: false,
refreshing: false,
periodKey: res.period_key || '',
periodKey: res.period_key || periodKey,
canSubmit: !!res.can_submit,
message: res.message || '',
myChecked: !!res.my_checked,
myStatus: res.my_status || '',
myRecord: res.my_record || null,
myThumbs: ((res.my_record && res.my_record.attachments) || []).map((p) => this.planFileUrl(p)),
myThumbs: ((res.my_record && res.my_record.attachments) || []).map((p) => api.planFileUrl(p)),
unassignedCount: res.unassigned_count || 0,
groups
isTeacher,
myGroup: res.my_group || '',
groups,
});
// 学员:如果在总览 + 小组 tab,加载本组成员
if (!isTeacher && this.data.view === 'overview' && this.data.studentTab === 'group' && res.my_group) {
return this.loadDetailForGroup(res.my_group);
}
})
.catch(() => {
this.setData({ loading: false, refreshing: false });
wx.showToast({ title: '加载小组失败', icon: 'none' });
wx.showToast({ title: '加载失败', icon: 'none' });
});
},
/** 打开小组详情 */
openGroup(e) {
const groupName = e.currentTarget.dataset.group;
if (!groupName) {
@@ -89,12 +198,29 @@ Page({
this.loadDetail();
},
/** 学员直接查看自己小组(只有一个小组时点击卡片直接进) */
openMyGroup() {
const groupName = this.data.myGroup;
if (!groupName) {
return;
}
this.setData({ selectedGroupName: groupName, view: 'detail', members: [] });
this.loadDetail();
},
/** 加载小组详情 */
loadDetail() {
const type = TYPES[this.data.typeIndex];
return this.loadDetailForGroup(this.data.selectedGroupName, type);
},
/** 加载指定小组的成员数据(不改变视图状态) */
loadDetailForGroup(groupName, type) {
type = type || TYPES[this.data.typeIndex];
this.setData({ loading: true });
api.request({
return api.request({
url: '/api/v1/plan/group',
data: { type, group_name: this.data.selectedGroupName, period_key: this.data.periodKey }
data: { type, group_name: groupName, period_key: this.data.periodKey }
}).then((res) => {
this.setData({
loading: false,
@@ -109,19 +235,44 @@ Page({
});
},
/** 学员切换 tab */
switchStudentTab(e) {
const tab = e.currentTarget.dataset.tab;
if (tab !== 'group' && tab !== 'personal') return;
this.setData({ studentTab: tab });
if (tab === 'personal') {
// 每次切到个人 tab 都刷新历史数据
this.loadHistory();
} else if (tab === 'group' && this.data.myGroup) {
// 每次切回小组 tab 都刷新小组成员数据
this.loadDetailForGroup(this.data.myGroup);
}
},
/** 加载个人计划历史 */
loadHistory() {
const type = TYPES[this.data.typeIndex];
return api.request({
url: '/api/v1/plan/history',
data: { type, page: 1, size: 20 }
}).then((res) => {
const history = (res.list || []).map((item) => ({
...item,
thumbs: (item.attachments || []).map((p) => api.planFileUrl(p)).slice(0, 3),
statusColor: item.status === '未完成' ? '#C67B30' : '#2E7D32',
statusBg: item.status === '未完成' ? '#FFF3E6' : '#E8F4EB',
}));
this.setData({ history });
}).catch(() => {});
},
/** 返回总览 */
backToOverview() {
this.setData({ view: 'overview', selectedGroupName: '', members: [] });
this.loadOverview();
},
planFileUrl(path) {
if (!path) return '';
if (/^https?:\/\//i.test(path)) return path;
const parts = String(path).replace(/^attachments\//, '').split('/');
if (parts.length < 4) return '';
return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/');
},
/** 预览我的图片 */
previewMyImage(e) {
const index = Number(e.currentTarget.dataset.index);
const urls = this.data.myThumbs;
@@ -130,10 +281,11 @@ Page({
wx.previewImage({ current: src, urls });
},
/** 去提交计划 */
goSubmit() {
const type = TYPES[this.data.typeIndex];
if (type === 'weekly' && !this.data.canSubmit) {
wx.showToast({ title: this.data.message || '当前不可打卡', icon: 'none' });
if (!this.data.canSubmit) {
wx.showToast({ title: this.data.message || '当前不可提交', icon: 'none' });
return;
}
const params = [
@@ -142,5 +294,26 @@ Page({
'group_name='
].join('&');
wx.navigateTo({ url: '/pages/plan-submit/plan-submit?' + params });
},
// ---- 分享 ----
onShareAppMessage() {
const type = this.data.types[this.data.typeIndex];
const checked = this.data.myChecked;
const title = checked
? '我的' + type + '已提交,来明缘云道场一起成长吧'
: '明缘云道场 · ' + type + '管理工具';
return {
title,
path: '/pages/plan/plan'
};
},
onShareTimeline() {
return {
title: '明缘云道场 · 周/月/年计划,让成长有迹可循',
query: 'from=timeline'
};
}
});
});