修改主页工作台布局以及数据显示, 改成预报名报表统计页面

This commit is contained in:
mirage 2026-02-26 09:39:22 +08:00
parent 737c7a1ff1
commit c3a8f590e8
18 changed files with 570 additions and 391 deletions

View File

@ -6,8 +6,8 @@ export function getConfig() {
}
// 工作台主页
export function getWorkbench() {
return request.get({ url: '/workbench/index' })
export function getWorkbench(params?: Record<string, any>) {
return request.get({ url: '/workbench/index', params })
}
//字典数据

View File

@ -12,20 +12,10 @@
<el-form-item label="性别" prop="gender">
<el-select v-model="queryParams.gender" class="w-[280px]" clearable>
<el-option label="男" value="1" />
<el-option label="女" value="2" />
<el-option label="女" value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="身份证号" prop="idCard">
<el-input class="w-[280px]" v-model="queryParams.idCard" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="毕业学校" prop="previousSchool">
<el-input class="w-[280px]" v-model="queryParams.previousSchool" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="报名专业" prop="majorId">
<el-select
@ -63,21 +53,11 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="接待老师" prop="receptionTeacherId">
<el-select
v-model="queryParams.receptionTeacherId"
class="w-[280px]"
clearable
filterable
placeholder="请选择接待老师"
>
<el-option
v-for="item in teacherOptions"
:key="item.id"
:label="item.name"
:value="item.id"
<el-form-item label="创建时间">
<daterange-picker
v-model:startTime="queryParams.createTimeStart"
v-model:endTime="queryParams.createTimeEnd"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
@ -90,7 +70,19 @@
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
<div class="flex justify-start mb-2">
<el-button type="success" plain @click="handleBatchEnroll" :disabled="!multipleSelection.length">
批量入学
</el-button>
</div>
<el-table
class="mt-4"
size="large"
v-loading="pager.loading"
:data="pager.lists"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="姓名" prop="name" min-width="100" />
<el-table-column
label="性别"
@ -98,71 +90,28 @@
min-width="100"
:formatter="formatGender"
/>
<el-table-column
label="身份证号"
prop="idCard"
min-width="200"
>
<el-table-column label="身份证号" prop="idCard" min-width="200">
<template #default="{ row }">
<span style="white-space: nowrap;">{{ row.idCard }}</span>
<span style="white-space: nowrap">{{ row.idCard }}</span>
</template>
</el-table-column>
<el-table-column
label="毕业学校"
prop="previousSchool"
min-width="150"
/>
<el-table-column
label="身高(cm)"
prop="height"
min-width="100"
/>
<el-table-column
label="体重(kg)"
prop="weight"
min-width="100"
/>
<el-table-column
label="鞋码"
prop="shoeSize"
min-width="100"
/>
<el-table-column
label="中考成绩"
prop="highSchoolScore"
min-width="120"
/>
<el-table-column label="毕业学校" prop="previousSchool" min-width="150" />
<el-table-column label="报名专业" prop="majorName" min-width="120" />
<el-table-column
label="招生老师"
prop="recruitmentTeacherId"
prop="recruitmentTeacherName"
min-width="120"
:formatter="formatRecruitmentTeacher"
/>
<el-table-column
label="接待老师"
prop="receptionTeacherId"
min-width="120"
:formatter="formatReceptionTeacher"
/>
<el-table-column
label="预报名金额"
prop="preRegistrationAmount"
min-width="120"
/>
<el-table-column label="创建时间" prop="createTime" min-width="180">
<template #default="{ row }">
<span style="white-space: nowrap;">{{ row.createTime }}</span>
<span style="white-space: nowrap">{{ row.createTime }}</span>
</template>
</el-table-column>
<el-table-column label="更新时间" prop="updateTime" min-width="180">
<template #default="{ row }">
<span style="white-space: nowrap;">{{ row.updateTime }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">详情</el-button>
<el-button type="success" link @click="handleEnroll(row)">入学</el-button>
</template>
</el-table-column>
</el-table>
@ -174,115 +123,50 @@
</div>
</template>
<script lang="ts" setup name="info">
import { infoLists } from '@/api/info/info'
import { infoLists, infoDetail, infoEdit } from '@/api/info/info'
import { majorLists } from '@/api/major'
import { teacherLists } from '@/api/teacher'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import { snakeToCamel } from '@/utils/format'
import request from '@/utils/request'
import EditPopup from '../student/info/edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const multipleSelection = ref<any[]>([])
const majorOptions = ref<Array<{ id: number; majorName: string }>>([])
const teacherOptions = ref<Array<{ id: number; name: string }>>([])
const queryParams = reactive({
id: null,
userId: null,
studentNumber: '',
name: '',
gender: '',
college: '',
majorId: undefined as number | undefined,
className: '',
grade: '',
enrollmentYear: '',
expectedGraduationYear: '',
studentStatus: 0,
idCard: '',
birthday: '',
politicalStatus: '',
nativePlace: '',
homeAddress: '',
emergencyContact: '',
emergencyPhone: '',
dormitory: '',
counselorId: '',
totalCredits: '',
gpa: '',
scholarshipLevel: '',
academicWarnings: '',
isVerified: '',
verifiedBy: '',
verifiedTime: '',
previousSchool: '',
recruitmentTeacherId: undefined as number | undefined,
receptionTeacherId: undefined as number | undefined
//
recruitmentTeacherId: '' as unknown as number | undefined,
createTimeStart: '',
createTimeEnd: ''
})
const formatGender = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'1': '男',
'2': '女'
}
return statusMap[String(cellValue)] || cellValue
}
const formatStudentStatus = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'1': '在读',
'2': '休学',
'3': '毕业',
'4': '退学'
}
return statusMap[String(cellValue)] || cellValue
}
const formatPoliticalStatus = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'1': '党员',
'2': '团员',
'3': '群众'
}
return statusMap[String(cellValue)] || cellValue
}
const formatScholarshipLevel = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'0': ' ',
'1': '一等奖',
'2': '二等奖',
'3': '三等奖'
}
return statusMap[String(cellValue)] || cellValue
}
const formatAcademicWarnings = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'0': ' ',
'1': '一级预警',
'2': '二级预警',
'3': '三级预警'
}
return statusMap[String(cellValue)] || cellValue
}
const formatIsVerified = (row: any, column: any, cellValue: any) => {
const statusMap: Record<string | number, string> = {
'0': '未认证',
'1': '已认证'
'0': '女'
}
return statusMap[String(cellValue)] || cellValue
}
const formatRecruitmentTeacher = (row: any, column: any, cellValue: any) => {
if (!cellValue) return ''
const teacher = teacherOptions.value.find((t) => t.id === cellValue)
return teacher ? teacher.name : cellValue
// 使
if (row.recruitmentTeacherName != null && row.recruitmentTeacherName !== '') {
return row.recruitmentTeacherName
}
const formatReceptionTeacher = (row: any, column: any, cellValue: any) => {
if (!cellValue) return ''
const teacher = teacherOptions.value.find((t) => t.id === cellValue)
return teacher ? teacher.name : cellValue
const teacherId = row.recruitmentTeacherId
if (teacherId == null || teacherId === '') return ''
const teacher = teacherOptions.value.find(
(t) => t.id == teacherId || String(t.id) === String(teacherId)
)
return teacher ? teacher.name : ''
}
const fetchMajorOptions = async () => {
@ -303,6 +187,10 @@ const fetchTeacherOptions = async () => {
}
}
const handleSelectionChange = (val: any[]) => {
multipleSelection.value = val || []
}
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: async (params: any) => {
//
@ -314,24 +202,26 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
}
// undefined null null undefined
if (processedParams.majorId === '' || processedParams.majorId === null || processedParams.majorId === undefined) {
if (
processedParams.majorId === '' ||
processedParams.majorId === null ||
processedParams.majorId === undefined
) {
processedParams.majorId = null
} else if (typeof processedParams.majorId === 'string') {
processedParams.majorId = parseInt(processedParams.majorId)
}
if (processedParams.recruitmentTeacherId === '' || processedParams.recruitmentTeacherId === null || processedParams.recruitmentTeacherId === undefined) {
if (
processedParams.recruitmentTeacherId === '' ||
processedParams.recruitmentTeacherId === null ||
processedParams.recruitmentTeacherId === undefined
) {
processedParams.recruitmentTeacherId = null
} else if (typeof processedParams.recruitmentTeacherId === 'string') {
processedParams.recruitmentTeacherId = parseInt(processedParams.recruitmentTeacherId)
}
if (processedParams.receptionTeacherId === '' || processedParams.receptionTeacherId === null || processedParams.receptionTeacherId === undefined) {
processedParams.receptionTeacherId = null
} else if (typeof processedParams.receptionTeacherId === 'string') {
processedParams.receptionTeacherId = parseInt(processedParams.receptionTeacherId)
}
return infoLists(processedParams)
},
params: queryParams
@ -349,5 +239,43 @@ const handleView = async (data: any) => {
editRef.value?.getDetail(data)
}
const handleEnroll = async (row: any) => {
await feedback.confirm('是否确认入学?')
//
const detail = await infoDetail({
id: row.studentId || row.student_id || row.id
})
const data = snakeToCamel(detail)
//
if (!data.studentId) {
data.studentId = row.studentId || row.student_id || row.id
}
data.id = data.studentId
//
data.studentStatus = 2
await infoEdit(data)
feedback.msgSuccess('入学成功')
getLists()
}
const handleBatchEnroll = async () => {
if (!multipleSelection.value.length) return
await feedback.confirm('是否确认批量入学?')
const ids = multipleSelection.value
.map((item) => item.studentId || item.student_id || item.id)
.filter((id) => id != null)
if (!ids.length) {
feedback.msgError('未获取到有效的学生ID')
return
}
// GET /info/batchUpdateStudentStatus?studentIdList=1&studentIdList=2...
const query = ids.map((id) => `studentIdList=${encodeURIComponent(id)}`).join('&')
await request.get({
url: `/info/batchUpdateStudentStatus?${query}`
})
feedback.msgSuccess('批量入学成功')
getLists()
}
getLists()
</script>

View File

@ -497,7 +497,7 @@
v-model="studentFormData.preRegistrationTime"
type="datetime"
placeholder="请选择预报名时间"
value-format="X"
value-format="YYYY-MM-DD HH:mm:ss"
class="w-full"
:disabled="isViewMode"
/>
@ -717,7 +717,7 @@ const studentFormData = reactive({
preRegistrationAmount: '',
recruitmentTeacherId: undefined as number | undefined,
receptionTeacherId: undefined as number | undefined,
preRegistrationTime: undefined as number | undefined,
preRegistrationTime: undefined as string | undefined,
invitationCode: '',
isVerified: undefined as number | undefined,
verifiedBy: '',

View File

@ -43,59 +43,83 @@
<div class="flex flex-wrap">
<div class="w-1/2 md:w-1/4">
<div class="leading-10">销售额</div>
<div class="text-6xl">{{ workbenchData.today.today_sales }}</div>
<div class="leading-10">总招生数量</div>
<div class="text-6xl">{{ workbenchData.today.total_enroll_count || 0 }}</div>
<div class="text-tx-secondary text-xs">
{{ workbenchData.today.total_sales }}
学生状态为"预报名""报名"的学生总数
</div>
</div>
<div class="w-1/2 md:w-1/4">
<div class="leading-10">成交订单</div>
<div class="text-6xl">{{ workbenchData.today.order_num }}</div>
<div class="leading-10">今日招生数量</div>
<div class="text-6xl">{{ workbenchData.today.today_enroll_count || 0 }}</div>
<div class="text-tx-secondary text-xs">
{{ workbenchData.today.order_sum }}
今天新增的招生人数
</div>
</div>
<div class="w-1/2 md:w-1/4">
<div class="leading-10">新增用户</div>
<div class="text-6xl">{{ workbenchData.today.today_new_user }}</div>
<div class="leading-10">本周招生数量</div>
<div class="text-6xl">{{ workbenchData.today.week_enroll_count || 0 }}</div>
<div class="text-tx-secondary text-xs">
{{ workbenchData.today.total_new_user }}
最近 7 天的招生人数
</div>
</div>
<div class="w-1/2 md:w-1/4">
<div class="leading-10">新增访问</div>
<div class="text-6xl">{{ workbenchData.today.today_visitor }}</div>
<div class="leading-10">本月招生数</div>
<div class="text-6xl">{{ workbenchData.today.month_enroll_count || 0 }}</div>
<div class="text-tx-secondary text-xs">
{{ workbenchData.today.total_visitor }}
最近 30 天的招生人数
</div>
</div>
</div>
</el-card>
</div>
<div class="function mb-4">
<el-card class="flex-1 !border-none" shadow="never">
<template #header>
<span>常用功能</span>
</template>
<div class="flex flex-wrap">
<div
v-for="item in workbenchData.menu"
class="md:w-[12.5%] w-1/4 flex flex-col items-center"
:key="item"
>
<router-link :to="item.url" class="mb-3 flex flex-col items-center">
<image-contain width="40px" height="40px" :src="item?.image" />
<div class="mt-2">{{ item.name }}</div>
</router-link>
</div>
</div>
</el-card>
</div>
<!-- <div class="function mb-4">-->
<!-- <el-card class="flex-1 !border-none" shadow="never">-->
<!-- <template #header>-->
<!-- <span>常用功能</span>-->
<!-- </template>-->
<!-- <div class="flex flex-wrap">-->
<!-- <div-->
<!-- v-for="item in workbenchData.menu"-->
<!-- class="md:w-[12.5%] w-1/4 flex flex-col items-center"-->
<!-- :key="item"-->
<!-- >-->
<!-- <router-link :to="item.url" class="mb-3 flex flex-col items-center">-->
<!-- <image-contain width="40px" height="40px" :src="item?.image" />-->
<!-- <div class="mt-2">{{ item.name }}</div>-->
<!-- </router-link>-->
<!-- </div>-->
<!-- </div>-->
<!-- </el-card>-->
<!-- </div>-->
<div class="lg:flex gap-4">
<el-card class="!border-none w-full lg:w-3/3" shadow="never">
<template #header>
<span>访问量趋势图</span>
<div class="flex justify-between items-center">
<span>招生趋势图</span>
<div class="flex items-center space-x-4">
<el-select
v-model="filterForm.teacherId"
class="w-[220px]"
clearable
filterable
placeholder="请选择招生老师"
@change="handleFilterChange"
>
<el-option
v-for="item in teacherOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-radio-group v-model="filterForm.rangeType" @change="handleFilterChange">
<el-radio-button label="day"></el-radio-button>
<el-radio-button label="week"></el-radio-button>
<el-radio-button label="month"></el-radio-button>
</el-radio-group>
</div>
</div>
</template>
<div>
<v-charts
@ -125,6 +149,8 @@
import vCharts from 'vue-echarts'
import { getWorkbench } from '@/api/app'
import { teacherLists } from '@/api/teacher'
//
const workbenchData: any = reactive({
version: {
@ -137,9 +163,9 @@ const workbenchData: any = reactive({
}
},
support: [],
today: {}, //
today: {}, // /
menu: [], //
visitor: [], // 访
visitor: [], //
article: [], //
visitorOption: {
@ -148,151 +174,81 @@ const workbenchData: any = reactive({
data: []
},
yAxis: {
type: 'value'
type: 'value',
name: '单位(人)'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['访问量']
},
itemStyle: {
//
color: 'red'
},
tooltip: {
trigger: 'axis'
},
series: [
{
name: '访问量',
data: [],
type: 'line',
smooth: true,
lineStyle: {
color: '#4A5DFF',
width: 2
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#4A5DFF'
},
{
offset: 1,
color: '#5777ff'
}
]
},
opacity: 0.1
}
}
]
},
saleOption: {
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value',
name: '单位(万)'
},
tooltip: {
trigger: 'axis'
},
series: [
{
data: [],
type: 'bar',
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
borderRadius: [10, 10, 0, 0]
},
barWidth: '40%',
itemStyle: {
borderRadius: [10, 10, 0, 0],
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#4A5DFF'
},
{
offset: 1,
color: '#5777ff'
}
]
}
}
}
]
series: []
}
})
//
const filterForm = reactive<{
teacherId: number | null
rangeType: 'day' | 'week' | 'month'
}>({
teacherId: null,
rangeType: 'week'
})
//
const teacherOptions = ref<Array<{ id: number; name: string }>>([])
const fetchTeacherOptions = async () => {
try {
const res: any = await teacherLists()
teacherOptions.value = res.lists || []
} catch (e) {
//
console.error('获取招生老师列表失败', e)
}
}
//
const getData = () => {
getWorkbench()
const params: Record<string, any> = {
rangeType: filterForm.rangeType
}
if (filterForm.teacherId) {
params.teacherId = filterForm.teacherId
}
getWorkbench(params)
.then((res: any) => {
workbenchData.version = res.version
workbenchData.today = res.today
workbenchData.menu = res.menu
workbenchData.visitor = res.visitor
workbenchData.today = res.today || {}
workbenchData.menu = res.menu || []
workbenchData.visitor = res.enrollmentTrend || {}
workbenchData.support = res.support
// echarts
workbenchData.visitorOption.xAxis.data = []
workbenchData.visitorOption.series[0].data = []
workbenchData.saleOption.xAxis.data = []
workbenchData.saleOption.series[0].data = []
const trend = res.enrollmentTrend || {}
const dates: string[] = Array.isArray(trend.date) ? trend.date : []
const list: any[] = Array.isArray(trend.list) ? trend.list : []
//
res.visitor.date.reverse().forEach((item: any) => {
workbenchData.visitorOption.xAxis.data.push(item)
// ECharts
workbenchData.visitorOption.xAxis.data = []
workbenchData.visitorOption.series = []
workbenchData.visitorOption.legend.data = []
// 线
workbenchData.visitorOption.xAxis.data = dates
list.forEach((item: any) => {
const name = item.name || '未知老师'
const data = Array.isArray(item.data) ? item.data : []
workbenchData.visitorOption.legend.data.push(name)
workbenchData.visitorOption.series.push({
name,
type: 'line',
smooth: true,
data
})
res.visitor.list[0].data.forEach((item: any) => {
workbenchData.visitorOption.series[0].data.push(item)
})
res.sale.date.reverse().forEach((item: any) => {
workbenchData.saleOption.xAxis.data.push(item)
})
res.sale.list[0].data.forEach((item: any) => {
if (item <= 50) {
item = {
value: item,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#ff8729'
},
{
offset: 1,
color: '#ff8729'
}
]
}
}
}
}
workbenchData.saleOption.series[0].data.push(item)
})
})
.catch((err: any) => {
@ -300,7 +256,12 @@ const getData = () => {
})
}
const handleFilterChange = () => {
getData()
}
onMounted(() => {
fetchTeacherOptions()
getData()
})
</script>

View File

@ -7,6 +7,7 @@ import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@ -23,8 +24,10 @@ public class IndexController {
@GetMapping("/index")
@ApiOperation(value="控制台")
public AjaxResult<Map<String, Object>> index() {
Map<String, Object> map = iIndexService.index();
public AjaxResult<Map<String, Object>> index(
@RequestParam(value = "teacherId", required = false) Integer teacherId,
@RequestParam(value = "rangeType", required = false) String rangeType) {
Map<String, Object> map = iIndexService.index(teacherId, rangeType);
return AjaxResult.success(map);
}

View File

@ -18,6 +18,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("adminapi/info")
@ -58,6 +59,14 @@ public class StudentInfoController {
return AjaxResult.success();
}
@Log(title = "学生批量入学")
@GetMapping("/batchUpdateStudentStatus")
@ApiOperation(value = "学生批量入学")
public AjaxResult<Object> batchUpdateStudentStatus(@RequestParam("studentIdList") List<Integer> studentIdList) {
iStudentInfoService.batchUpdateStudentStatus(studentIdList);
return AjaxResult.success();
}
@Log(title = "学生信息删除")
@PostMapping("/del")
@ApiOperation(value = "学生信息删除")

View File

@ -11,9 +11,11 @@ public interface IIndexService {
* 控制台数据
*
* @author fzr
* @param teacherId 招生老师ID可选
* @param rangeType 时间区间类型day / week / month可选默认 week
* @return Map<String, Object>
*/
Map<String, Object> index();
Map<String, Object> index(Integer teacherId, String rangeType);
/**
* 公共配置

View File

@ -9,6 +9,7 @@ import com.mdd.admin.vo.StudentInfoDetailVo;
import com.mdd.common.core.PageResult;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 学生信息服务接口类
@ -58,4 +59,6 @@ public interface IStudentInfoService {
* @param id 主键ID
*/
void del(Integer id);
void batchUpdateStudentStatus(List<Integer> studentIdList);
}

View File

@ -4,9 +4,21 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.mdd.admin.service.IIndexService;
import com.mdd.common.config.GlobalConfig;
import com.mdd.common.util.*;
import com.mdd.common.entity.StudentInfo;
import com.mdd.common.entity.Teacher;
import com.mdd.common.mapper.StudentInfoMapper;
import com.mdd.common.mapper.TeacherMapper;
import com.mdd.common.util.ConfigUtils;
import com.mdd.common.util.ListUtils;
import com.mdd.common.util.TimeUtils;
import com.mdd.common.util.UrlUtils;
import com.mdd.common.util.YmlUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
@ -15,14 +27,22 @@ import java.util.*;
@Service
public class IndexServiceImpl implements IIndexService {
@Resource
private StudentInfoMapper studentInfoMapper;
@Resource
private TeacherMapper teacherMapper;
/**
* 控制台数据
*
* @author fzr
* @param teacherId 招生老师ID可选
* @param rangeType 时间区间类型day / week / month可选默认 week
* @return Map<String, Object>
*/
@Override
public Map<String, Object> index() {
public Map<String, Object> index(Integer teacherId, String rangeType) {
Map<String, Object> console = new LinkedHashMap<>();
// 版本信息
@ -37,30 +57,15 @@ public class IndexServiceImpl implements IIndexService {
version.put("channel", channel);
console.put("version", version);
// 今日数据
Map<String, Object> today = new LinkedHashMap<>();
today.put("time", "2022-08-11 15:08:29");
today.put("today_visitor", 10); // 访问量()
today.put("total_visitor", 100); // 总访问量
today.put("today_sales", 30); // 销售额()
today.put("total_sales", 65); // 总销售额
today.put("order_num", 12); // 订单量()
today.put("order_sum", 255); // 总订单量
today.put("today_new_user", 120); // 新增用户
today.put("total_new_user", 360); // 总访用户
console.put("today", today);
// 招生统计数据
Map<String, Object> enrollmentStats = buildEnrollmentStats();
console.put("today", enrollmentStats);
// 访客图表
Map<String, Object> visitor = new LinkedHashMap<>();
visitor.put("date", TimeUtils.daysAgoDate(15));
visitor.put("list", new JSONArray() {{
add(new JSONObject() {{
put("name", "访客数");
put("data", Arrays.asList(12,13,11,5,8,22,14,9,456,62,78,12,18,22,46));
}});
}});
console.put("visitor", visitor);
// 招生趋势数据带筛选条件
Map<String, Object> enrollmentTrend = buildEnrollmentTrend(teacherId, rangeType);
console.put("enrollmentTrend", enrollmentTrend);
// 常用功能菜单
console.put("menu", new JSONArray() {{
add(new JSONObject() {{
@ -113,10 +118,190 @@ public class IndexServiceImpl implements IIndexService {
}});
return console;
}
/**
* 构建招生统计数据
* - 总招生数量
* - 今日招生数量
* - 本周招生数量
* - 本月招生数量
*
* 招生数量定义student_status = 0 1 的学生数量
*/
private Map<String, Object> buildEnrollmentStats() {
Map<String, Object> stats = new LinkedHashMap<>();
// 当前时间
Date now = new Date();
stats.put("time", new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(now));
// 总招生数量所有时间
long totalCount = studentInfoMapper.selectCount(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<StudentInfo>()
.in("student_status", 0, 1)
.isNull("delete_time")
);
// 今日本周本月时间范围
LocalDate today = LocalDate.now();
Date todayStart = toDate(today.atStartOfDay());
Date tomorrowStart = toDate(today.plusDays(1).atStartOfDay());
LocalDate weekStartDate = today.minusDays(6); // 最近7天
Date weekStart = toDate(weekStartDate.atStartOfDay());
LocalDate monthStartDate = today.minusDays(29); // 最近30天
Date monthStart = toDate(monthStartDate.atStartOfDay());
long todayCount = countEnrollmentInRange(todayStart, tomorrowStart);
long weekCount = countEnrollmentInRange(weekStart, tomorrowStart);
long monthCount = countEnrollmentInRange(monthStart, tomorrowStart);
stats.put("total_enroll_count", totalCount);
stats.put("today_enroll_count", todayCount);
stats.put("week_enroll_count", weekCount);
stats.put("month_enroll_count", monthCount);
return stats;
}
/**
* 计算时间区间内的招生数量 pre_registration_time 筛选
*/
private long countEnrollmentInRange(Date startTime, Date endTime) {
return studentInfoMapper.selectCount(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<StudentInfo>()
.in("student_status", 0, 1)
.isNull("delete_time")
.isNotNull("pre_registration_time")
.ge("pre_registration_time", startTime)
.lt("pre_registration_time", endTime)
);
}
/**
* 构建招生趋势数据
*
* @param teacherId 招生老师ID为空则统计所有老师
* @param rangeType 时间区间类型day / week / month
*/
private Map<String, Object> buildEnrollmentTrend(Integer teacherId, String rangeType) {
Map<String, Object> result = new LinkedHashMap<>();
if (rangeType == null || rangeType.trim().isEmpty()) {
rangeType = "week";
}
LocalDate today = LocalDate.now();
LocalDate startDate;
switch (rangeType) {
case "day":
startDate = today;
break;
case "month":
startDate = today.minusDays(29);
break;
case "week":
default:
startDate = today.minusDays(6);
break;
}
LocalDate endDate = today;
Date startTime = toDate(startDate.atStartOfDay());
Date endTime = toDate(endDate.plusDays(1).atStartOfDay());
// 生成日期列表
List<String> dateList = new ArrayList<>();
Map<String, Integer> dateIndexMap = new LinkedHashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
int idx = 0;
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
String dateStr = d.format(formatter);
dateList.add(dateStr);
dateIndexMap.put(dateStr, idx++);
}
// 查询数据库中的统计数据按日期 + 招生老师分组
List<Map<String, Object>> rows = studentInfoMapper.selectEnrollmentTrend(startTime, endTime, teacherId);
// 收集涉及到的老师ID
Set<Integer> teacherIds = new LinkedHashSet<>();
for (Map<String, Object> row : rows) {
Object tIdObj = row.get("teacherId");
if (tIdObj != null) {
teacherIds.add(((Number) tIdObj).intValue());
}
}
Map<Integer, String> teacherNameMap = new LinkedHashMap<>();
if (!teacherIds.isEmpty()) {
List<Teacher> teachers = teacherMapper.selectBatchIds(teacherIds);
for (Teacher t : teachers) {
teacherNameMap.put(t.getTeacherId(), t.getTeacherName());
}
}
// 构建每个老师的时间序列数据
Map<Integer, List<Long>> teacherSeriesMap = new LinkedHashMap<>();
for (Integer tId : teacherIds) {
List<Long> initList = new ArrayList<>();
for (int i = 0; i < dateList.size(); i++) {
initList.add(0L);
}
teacherSeriesMap.put(tId, initList);
}
for (Map<String, Object> row : rows) {
Object dateObj = row.get("statDate");
Object tIdObj = row.get("teacherId");
Object countObj = row.get("enrollCount");
if (dateObj == null || tIdObj == null || countObj == null) {
continue;
}
String dateStr = String.valueOf(dateObj);
Integer tId = ((Number) tIdObj).intValue();
Integer index = dateIndexMap.get(dateStr);
if (index == null) {
continue;
}
List<Long> dataList = teacherSeriesMap.get(tId);
if (dataList == null) {
dataList = new ArrayList<>();
for (int i = 0; i < dateList.size(); i++) {
dataList.add(0L);
}
teacherSeriesMap.put(tId, dataList);
}
long count = ((Number) countObj).longValue();
dataList.set(index, count);
}
// 组装返回给前端的 list 数据
JSONArray seriesList = new JSONArray();
for (Map.Entry<Integer, List<Long>> entry : teacherSeriesMap.entrySet()) {
Integer tId = entry.getKey();
List<Long> data = entry.getValue();
JSONObject item = new JSONObject();
item.put("teacherId", tId);
item.put("name", teacherNameMap.getOrDefault(tId, "未知老师"));
item.put("data", data);
seriesList.add(item);
}
result.put("date", dateList);
result.put("list", seriesList);
result.put("rangeType", rangeType);
return result;
}
private Date toDate(java.time.LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* 公共配置
*

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mdd.admin.validate.commons.PageValidate;
import com.mdd.admin.service.IStudentInfoService;
import com.mdd.admin.validate.StudentInfoCreateValidate;
@ -12,18 +13,12 @@ import com.mdd.admin.validate.StudentInfoSearchValidate;
import com.mdd.admin.vo.StudentInfoListedVo;
import com.mdd.admin.vo.StudentInfoDetailVo;
import com.mdd.common.core.PageResult;
import com.mdd.common.entity.*;
import com.mdd.common.entity.Class;
import com.mdd.common.entity.College;
import com.mdd.common.entity.Major;
import com.mdd.common.entity.StudentInfo;
import com.mdd.common.entity.StudentBaseInfo;
import com.mdd.common.entity.admin.Admin;
import com.mdd.common.entity.user.User;
import com.mdd.common.mapper.ClassMapper;
import com.mdd.common.mapper.CollegeMapper;
import com.mdd.common.mapper.MajorMapper;
import com.mdd.common.mapper.StudentInfoMapper;
import com.mdd.common.mapper.StudentBaseInfoMapper;
import com.mdd.common.exception.OperateException;
import com.mdd.common.mapper.*;
import com.mdd.common.mapper.admin.AdminMapper;
import com.mdd.common.mapper.user.UserMapper;
@ -33,6 +28,7 @@ import java.text.SimpleDateFormat;
import com.mdd.common.util.*;
import io.netty.util.internal.ThreadLocalRandom;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -47,7 +43,7 @@ import java.util.*;
* @author gyp
*/
@Service
public class StudentInfoServiceImpl implements IStudentInfoService {
public class StudentInfoServiceImpl extends ServiceImpl<StudentInfoMapper, StudentInfo> implements IStudentInfoService {
@Resource
StudentInfoMapper studentInfoMapper;
@ -63,6 +59,8 @@ public class StudentInfoServiceImpl implements IStudentInfoService {
private AdminMapper adminMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private TeacherMapper teacherMapper;
/**
* 学生信息列表
@ -95,6 +93,7 @@ public class StudentInfoServiceImpl implements IStudentInfoService {
"=:verifiedBy@verified_by:int",
"=:recruitmentTeacherId@recruitment_teacher_id:int",
"=:receptionTeacherId@reception_teacher_id:int",
"datetime:createTimeStart-createTimeEnd@create_time:str",
});
IPage<StudentInfo> iPage = studentInfoMapper.selectPage(new Page<>(page, limit), queryWrapper);
@ -133,12 +132,14 @@ public class StudentInfoServiceImpl implements IStudentInfoService {
Class clazz = classMapper.selectById(item.getClassId());
Admin counselor = adminMapper.selectById(item.getCounselorId());
Admin verifier = adminMapper.selectById(item.getVerifiedBy());
Teacher teacher = teacherMapper.selectById(item.getRecruitmentTeacherId());
vo.setCollegeName(college != null ? college.getCollegeName() : "");
vo.setMajorName(major != null ? major.getMajorName() : "");
vo.setClassName(clazz != null ? clazz.getClassName() : "");
vo.setCounselorName(counselor != null ? counselor.getName() : "");
vo.setVerifierName(verifier != null ? verifier.getName() : "");
vo.setRecruitmentTeacherName(teacher != null ? teacher.getTeacherName() : null);
list.add(vo);
}
@ -232,6 +233,10 @@ public class StudentInfoServiceImpl implements IStudentInfoService {
studentInfo.setIsVerified(createValidate.getIsVerified() != null ? createValidate.getIsVerified() : 0);
studentInfo.setVerifiedBy(createValidate.getVerifiedBy());
studentInfo.setVerifiedTime(createValidate.getVerifiedTime());
studentInfo.setPreRegistrationTime(createValidate.getPreRegistrationTime());
studentInfo.setRecruitmentTeacherId(createValidate.getRecruitmentTeacherId());
studentInfo.setReceptionTeacherId(createValidate.getReceptionTeacherId());
studentInfo.setInvitationCode(createValidate.getInvitationCode());
studentInfoMapper.insert(studentInfo);
// 创建基本信息
@ -384,6 +389,15 @@ public class StudentInfoServiceImpl implements IStudentInfoService {
studentInfoMapper.delete(new QueryWrapper<StudentInfo>().eq("student_id", id));
}
@Override
public void batchUpdateStudentStatus(List<Integer> studentIdList) {
if (CollectionUtils.isEmpty(studentIdList)) {
throw new OperateException("批量入学失败, 学生idList丢失");
}
studentInfoMapper.batchUpdateStudentStatus(studentIdList);
}
private <T, M extends BaseMapper<T>> T getRandomEntity(M mapper) {
try {
QueryWrapper<T> wrapper = new QueryWrapper<>();

View File

@ -1,5 +1,6 @@
package com.mdd.admin.validate;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -75,7 +76,8 @@ public class StudentInfoCreateValidate implements Serializable {
private Integer receptionTeacherId;
@ApiModelProperty(value = "预报名时间")
private Long preRegistrationTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private java.util.Date preRegistrationTime;
@ApiModelProperty(value = "邀请码")
private String invitationCode;

View File

@ -56,4 +56,10 @@ public class StudentInfoSearchValidate implements Serializable {
@ApiModelProperty(value = "接待老师ID")
private Integer receptionTeacherId;
@ApiModelProperty(value = "创建时间开始")
private String createTimeStart;
@ApiModelProperty(value = "创建时间结束")
private String createTimeEnd;
}

View File

@ -1,5 +1,6 @@
package com.mdd.admin.validate;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -25,7 +26,7 @@ public class StudentInfoUpdateValidate implements Serializable {
@ApiModelProperty(value = "主键")
private Long studentId;
@NotNull(message = "studentNumber参数缺失")
// @NotNull(message = "studentNumber参数缺失")
@ApiModelProperty(value = "学号")
private String studentNumber;
@ -41,19 +42,19 @@ public class StudentInfoUpdateValidate implements Serializable {
@ApiModelProperty(value = "班级ID")
private Integer classId;
@NotNull(message = "grade参数缺失")
// @NotNull(message = "grade参数缺失")
@ApiModelProperty(value = "年级")
private Integer grade;
@NotNull(message = "enrollmentYear参数缺失")
// @NotNull(message = "enrollmentYear参数缺失")
@ApiModelProperty(value = "入学年份")
private Integer enrollmentYear;
@NotNull(message = "expectedGraduationYear参数缺失")
// @NotNull(message = "expectedGraduationYear参数缺失")
@ApiModelProperty(value = "预计毕业年份")
private Integer expectedGraduationYear;
@NotNull(message = "studentStatus参数缺失")
// @NotNull(message = "studentStatus参数缺失")
@ApiModelProperty(value = "学生状态: [0=预报名, 1=报名, 2=在读, 3=休学, 4=毕业, 5=退学]")
private Integer studentStatus;
@ -85,7 +86,8 @@ public class StudentInfoUpdateValidate implements Serializable {
private Integer receptionTeacherId;
@ApiModelProperty(value = "预报名时间")
private Long preRegistrationTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private java.util.Date preRegistrationTime;
@ApiModelProperty(value = "邀请码")
private String invitationCode;

View File

@ -1,5 +1,6 @@
package com.mdd.admin.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -69,7 +70,8 @@ public class StudentInfoDetailVo implements Serializable {
private Integer receptionTeacherId;
@ApiModelProperty(value = "预报名时间")
private Long preRegistrationTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private java.util.Date preRegistrationTime;
@ApiModelProperty(value = "邀请码")
private String invitationCode;

View File

@ -99,6 +99,9 @@ public class StudentInfoListedVo implements Serializable {
@ApiModelProperty(value = "招生老师ID")
private Integer recruitmentTeacherId;
@ApiModelProperty(value = "招生老师名字")
private String recruitmentTeacherName;
@ApiModelProperty(value = "接待老师ID")
private Integer receptionTeacherId;

View File

@ -72,7 +72,7 @@ public class StudentInfo implements Serializable {
private Integer receptionTeacherId;
@ApiModelProperty(value = "预报名时间")
private Long preRegistrationTime;
private Date preRegistrationTime;
@ApiModelProperty(value = "邀请码")
private String invitationCode;

View File

@ -3,6 +3,11 @@ package com.mdd.common.mapper;
import com.mdd.common.core.basics.IBaseMapper;
import com.mdd.common.entity.StudentInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 学生信息Mapper
@ -10,4 +15,18 @@ import org.apache.ibatis.annotations.Mapper;
*/
@Mapper
public interface StudentInfoMapper extends IBaseMapper<StudentInfo> {
void batchUpdateStudentStatus(@Param("studentIdList") List<Integer> studentIdList);
/**
* 按日期和招生老师统计招生数量student_status = 0 1
*
* @param startTime 开始时间
* @param endTime 结束时间不含
* @param teacherId 招生老师ID可选
* @return 每行包含 statDateyyyy-MM-ddteacherIdenrollCount
*/
List<Map<String, Object>> selectEnrollmentTrend(@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("teacherId") Integer teacherId);
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mdd.common.mapper.StudentInfoMapper">
<!-- 批量更新学生状态为在读(2) -->
<update id="batchUpdateStudentStatus">
UPDATE la_student_info
SET student_status = 2
WHERE student_id IN
<foreach collection="studentIdList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 按日期和招生老师统计招生数量student_status = 0 或 1时间区间按 pre_registration_time -->
<select id="selectEnrollmentTrend" resultType="map">
SELECT
DATE(pre_registration_time) AS statDate,
recruitment_teacher_id AS teacherId,
COUNT(*) AS enrollCount
FROM la_student_info
WHERE delete_time IS NULL
AND student_status IN (0, 1)
AND pre_registration_time IS NOT NULL
<if test="startTime != null">
AND pre_registration_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND pre_registration_time <![CDATA[<]]> #{endTime}
</if>
<if test="teacherId != null">
AND recruitment_teacher_id = #{teacherId}
</if>
GROUP BY DATE(pre_registration_time), recruitment_teacher_id
ORDER BY statDate ASC
</select>
</mapper>