package com.tcm.training.admin.live.service;
|
|
import cn.idev.excel.FastExcel;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.tcm.training.admin.common.DateTimes;
|
import com.tcm.training.admin.common.PageResponse;
|
import com.tcm.training.admin.common.PhoneMasker;
|
import com.tcm.training.admin.live.entity.LiveSession;
|
import com.tcm.training.admin.live.excel.LiveReserveExportRow;
|
import com.tcm.training.admin.live.excel.LiveWatchExportRow;
|
import com.tcm.training.admin.live.mapper.LiveCourseMapper;
|
import com.tcm.training.admin.live.mapper.LiveReserveMapper;
|
import com.tcm.training.admin.live.mapper.LiveSessionMapper;
|
import com.tcm.training.admin.live.mapper.LiveSessionQuizRecordMapper;
|
import com.tcm.training.admin.live.mapper.LiveWatchRecordMapper;
|
import com.tcm.training.admin.live.vo.LiveQuizItemVO;
|
import com.tcm.training.admin.live.vo.LiveQuizRow;
|
import com.tcm.training.admin.live.vo.LiveReserveItemVO;
|
import com.tcm.training.admin.live.vo.LiveReservePageVO;
|
import com.tcm.training.admin.live.vo.LiveReserveRow;
|
import com.tcm.training.admin.live.vo.LiveReserveSummaryRow;
|
import com.tcm.training.admin.live.vo.LiveReserveSummaryVO;
|
import com.tcm.training.admin.live.vo.LiveWatchItemVO;
|
import com.tcm.training.admin.live.vo.LiveWatchPageVO;
|
import com.tcm.training.admin.live.vo.LiveWatchRow;
|
import com.tcm.training.admin.live.vo.LiveWatchSummaryRow;
|
import com.tcm.training.admin.live.vo.LiveWatchSummaryVO;
|
import com.tcm.training.admin.live.vo.UserVO;
|
import org.springframework.stereotype.Service;
|
|
import javax.servlet.http.HttpServletResponse;
|
import java.io.IOException;
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.net.URLEncoder;
|
import java.time.LocalDateTime;
|
import java.util.ArrayList;
|
import java.util.List;
|
|
@Service
|
public class LiveRecordAppService {
|
|
private final LiveCourseMapper liveCourseMapper;
|
private final LiveSessionMapper liveSessionMapper;
|
private final LiveReserveMapper liveReserveMapper;
|
private final LiveWatchRecordMapper liveWatchRecordMapper;
|
private final LiveSessionQuizRecordMapper liveSessionQuizRecordMapper;
|
|
public LiveRecordAppService(LiveCourseMapper liveCourseMapper,
|
LiveSessionMapper liveSessionMapper,
|
LiveReserveMapper liveReserveMapper,
|
LiveWatchRecordMapper liveWatchRecordMapper,
|
LiveSessionQuizRecordMapper liveSessionQuizRecordMapper) {
|
this.liveCourseMapper = liveCourseMapper;
|
this.liveSessionMapper = liveSessionMapper;
|
this.liveReserveMapper = liveReserveMapper;
|
this.liveWatchRecordMapper = liveWatchRecordMapper;
|
this.liveSessionQuizRecordMapper = liveSessionQuizRecordMapper;
|
}
|
|
public PageResponse<LiveReserveItemVO> pageReserves(Long courseId, Long sessionId, int page, int pageSize, String keyword, Integer isCancel) {
|
requireSession(courseId, sessionId);
|
|
IPage<LiveReserveRow> p = liveReserveMapper.selectReservePage(new Page<LiveReserveRow>(page, pageSize),
|
courseId, sessionId, keyword, isCancel);
|
|
return PageResponse.of(toReserveItems(p.getRecords()), p.getTotal(), p.getCurrent(), p.getSize());
|
}
|
|
public LiveReservePageVO pageGlobalReserves(int page,
|
int pageSize,
|
Long courseId,
|
Long sessionId,
|
String phone,
|
String courseName,
|
Integer liveStatus,
|
Integer hasWatched,
|
LocalDateTime reserveFrom,
|
LocalDateTime reserveTo) {
|
IPage<LiveReserveRow> result = liveReserveMapper.selectGlobalReservePage(
|
new Page<LiveReserveRow>(page, pageSize),
|
courseId,
|
sessionId,
|
trimToNull(phone),
|
trimToNull(courseName),
|
liveStatus,
|
hasWatched,
|
reserveFrom,
|
reserveTo
|
);
|
LiveReservePageVO vo = new LiveReservePageVO();
|
vo.setList(toReserveItems(result.getRecords()));
|
vo.setTotal(result.getTotal());
|
vo.setPage(result.getCurrent());
|
vo.setPageSize(result.getSize());
|
vo.setSummary(buildReserveSummary(courseId, sessionId, phone, courseName, liveStatus, hasWatched, reserveFrom, reserveTo));
|
return vo;
|
}
|
|
public void exportGlobalReserves(HttpServletResponse response,
|
Long courseId,
|
Long sessionId,
|
String phone,
|
String courseName,
|
Integer liveStatus,
|
Integer hasWatched,
|
LocalDateTime reserveFrom,
|
LocalDateTime reserveTo) throws IOException {
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
response.setCharacterEncoding("utf-8");
|
String filename = URLEncoder.encode("直播预约记录", "UTF-8").replace("+", "%20");
|
response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
|
|
List<LiveReserveRow> rows = liveReserveMapper.selectGlobalReserveList(
|
courseId,
|
sessionId,
|
trimToNull(phone),
|
trimToNull(courseName),
|
liveStatus,
|
hasWatched,
|
reserveFrom,
|
reserveTo
|
);
|
List<LiveReserveExportRow> exports = new ArrayList<LiveReserveExportRow>();
|
for (int i = 0; i < rows.size(); i++) {
|
LiveReserveRow row = rows.get(i);
|
LiveReserveExportRow export = new LiveReserveExportRow();
|
export.setSerialNo(i + 1);
|
export.setNickname(defaultText(row.getUserNickname()));
|
export.setPhone(PhoneMasker.mask(row.getUserPhone()));
|
export.setCourseTitle(defaultText(row.getCourseTitle()));
|
export.setCategoryName(defaultText(row.getCategoryName()));
|
export.setStartTime(formatMinute(row.getStartTime()));
|
export.setLiveStatus(liveStatusText(row.getLiveStatus()));
|
export.setReserveTime(DateTimes.format(row.getReserveTime()));
|
export.setHasWatched(boolText(row.getHasWatched()));
|
exports.add(export);
|
}
|
FastExcel.write(response.getOutputStream(), LiveReserveExportRow.class)
|
.sheet("直播预约记录")
|
.doWrite(exports);
|
}
|
|
public PageResponse<LiveWatchItemVO> pageWatchRecords(Long courseId, Long sessionId, int page, int pageSize, String keyword, Integer isCompleted, Integer isEffective) {
|
requireSession(courseId, sessionId);
|
|
IPage<LiveWatchRow> p = liveWatchRecordMapper.selectWatchPage(new Page<LiveWatchRow>(page, pageSize),
|
courseId, sessionId, keyword, isCompleted, isEffective);
|
|
return PageResponse.of(toWatchItems(p.getRecords()), p.getTotal(), p.getCurrent(), p.getSize());
|
}
|
|
public LiveWatchPageVO pageGlobalWatchRecords(int page,
|
int pageSize,
|
Long courseId,
|
Long sessionId,
|
String regionCode,
|
String clinicName,
|
String phone,
|
String courseName,
|
Integer hasReserve,
|
Integer isCompleted,
|
Integer isEffective,
|
LocalDateTime watchFrom,
|
LocalDateTime watchTo) {
|
IPage<LiveWatchRow> result = liveWatchRecordMapper.selectGlobalWatchPage(
|
new Page<LiveWatchRow>(page, pageSize),
|
courseId,
|
sessionId,
|
trimToNull(regionCode),
|
trimToNull(clinicName),
|
trimToNull(phone),
|
trimToNull(courseName),
|
hasReserve,
|
isCompleted,
|
isEffective,
|
watchFrom,
|
watchTo
|
);
|
LiveWatchPageVO vo = new LiveWatchPageVO();
|
vo.setList(toWatchItems(result.getRecords()));
|
vo.setTotal(result.getTotal());
|
vo.setPage(result.getCurrent());
|
vo.setPageSize(result.getSize());
|
vo.setSummary(buildWatchSummary(courseId, sessionId, regionCode, clinicName, phone, courseName, hasReserve, isCompleted, isEffective, watchFrom, watchTo));
|
return vo;
|
}
|
|
public void exportGlobalWatchRecords(HttpServletResponse response,
|
Long courseId,
|
Long sessionId,
|
String regionCode,
|
String clinicName,
|
String phone,
|
String courseName,
|
Integer hasReserve,
|
Integer isCompleted,
|
Integer isEffective,
|
LocalDateTime watchFrom,
|
LocalDateTime watchTo) throws IOException {
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
response.setCharacterEncoding("utf-8");
|
String filename = URLEncoder.encode("直播观看记录", "UTF-8").replace("+", "%20");
|
response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
|
|
List<LiveWatchRow> rows = liveWatchRecordMapper.selectGlobalWatchList(
|
courseId,
|
sessionId,
|
trimToNull(regionCode),
|
trimToNull(clinicName),
|
trimToNull(phone),
|
trimToNull(courseName),
|
hasReserve,
|
isCompleted,
|
isEffective,
|
watchFrom,
|
watchTo
|
);
|
List<LiveWatchExportRow> exports = new ArrayList<LiveWatchExportRow>();
|
for (int i = 0; i < rows.size(); i++) {
|
LiveWatchRow row = rows.get(i);
|
LiveWatchExportRow export = new LiveWatchExportRow();
|
export.setSerialNo(i + 1);
|
export.setNickname(defaultText(row.getUserNickname()));
|
export.setPhone(PhoneMasker.mask(row.getUserPhone()));
|
export.setClinicName(defaultText(row.getClinicName()));
|
export.setClinicArea(defaultText(row.getClinicArea()));
|
export.setCourseTitle(defaultText(row.getCourseTitle()));
|
export.setCategoryName(defaultText(row.getCategoryName()));
|
export.setStartTime(formatMinute(row.getStartTime()));
|
export.setHasReserve(boolText(row.getHasReserve()));
|
export.setWatchTime(DateTimes.format(row.getWatchStart()));
|
export.setIsCompleted(boolText(row.getIsCompleted()));
|
exports.add(export);
|
}
|
FastExcel.write(response.getOutputStream(), LiveWatchExportRow.class)
|
.sheet("直播观看记录")
|
.doWrite(exports);
|
}
|
|
public PageResponse<LiveQuizItemVO> pageQuizRecords(Long courseId, Long sessionId, int page, int pageSize, String keyword, Integer isCorrect) {
|
requireSession(courseId, sessionId);
|
|
IPage<LiveQuizRow> p = liveSessionQuizRecordMapper.selectQuizPage(new Page<LiveQuizRow>(page, pageSize),
|
sessionId, keyword, isCorrect);
|
|
List<LiveQuizItemVO> list = new ArrayList<LiveQuizItemVO>();
|
for (LiveQuizRow r : p.getRecords()) {
|
LiveQuizItemVO vo = new LiveQuizItemVO();
|
vo.setId(r.getId());
|
vo.setUser(new UserVO(r.getUserId(), r.getUserNickname(), PhoneMasker.mask(r.getUserPhone()), null));
|
vo.setQuestionSort(r.getQuestionSort());
|
vo.setStem(r.getStem());
|
vo.setChosenOption(r.getChosenOption());
|
vo.setCorrectOption(r.getCorrectOption());
|
vo.setIsCorrect(r.getIsCorrect());
|
vo.setPointsEarned(r.getPointsEarned());
|
vo.setAnsweredAt(r.getAnsweredAt());
|
list.add(vo);
|
}
|
|
return PageResponse.of(list, p.getTotal(), p.getCurrent(), p.getSize());
|
}
|
|
private void requireSession(Long courseId, Long sessionId) {
|
if (liveCourseMapper.selectById(courseId) == null) {
|
throw new IllegalArgumentException("课程不存在");
|
}
|
LiveSession s = liveSessionMapper.selectById(sessionId);
|
if (s == null) {
|
throw new IllegalArgumentException("场次不存在");
|
}
|
if (!courseId.equals(s.getLiveCourseId())) {
|
throw new IllegalArgumentException("场次不属于该课程");
|
}
|
}
|
|
private List<LiveReserveItemVO> toReserveItems(List<LiveReserveRow> rows) {
|
List<LiveReserveItemVO> list = new ArrayList<LiveReserveItemVO>();
|
for (LiveReserveRow row : rows) {
|
LiveReserveItemVO vo = new LiveReserveItemVO();
|
vo.setId(row.getId());
|
vo.setCourseId(row.getCourseId());
|
vo.setSessionId(row.getSessionId());
|
vo.setUser(new UserVO(row.getUserId(), row.getUserNickname(), PhoneMasker.mask(row.getUserPhone()), row.getUserAvatar()));
|
vo.setCourseTitle(row.getCourseTitle());
|
vo.setCategoryName(row.getCategoryName());
|
vo.setStartTime(row.getStartTime());
|
vo.setLiveStatus(row.getLiveStatus());
|
vo.setReserveTime(row.getReserveTime());
|
vo.setIsCancel(row.getIsCancel());
|
vo.setHasWatched(row.getHasWatched());
|
list.add(vo);
|
}
|
return list;
|
}
|
|
private List<LiveWatchItemVO> toWatchItems(List<LiveWatchRow> rows) {
|
List<LiveWatchItemVO> list = new ArrayList<LiveWatchItemVO>();
|
for (LiveWatchRow row : rows) {
|
LiveWatchItemVO vo = new LiveWatchItemVO();
|
vo.setId(row.getId());
|
vo.setCourseId(row.getCourseId());
|
vo.setSessionId(row.getSessionId());
|
vo.setUser(new UserVO(row.getUserId(), row.getUserNickname(), PhoneMasker.mask(row.getUserPhone()), row.getUserAvatar()));
|
vo.setClinicName(row.getClinicName());
|
vo.setClinicArea(row.getClinicArea());
|
vo.setCourseTitle(row.getCourseTitle());
|
vo.setCategoryName(row.getCategoryName());
|
vo.setStartTime(row.getStartTime());
|
vo.setHasReserve(row.getHasReserve());
|
vo.setWatchStart(row.getWatchStart());
|
vo.setWatchEnd(row.getWatchEnd());
|
vo.setDurationSeconds(row.getDurationSeconds());
|
vo.setIsCompleted(row.getIsCompleted());
|
vo.setIsEffective(row.getIsEffective());
|
list.add(vo);
|
}
|
return list;
|
}
|
|
private LiveReserveSummaryVO buildReserveSummary(Long courseId,
|
Long sessionId,
|
String phone,
|
String courseName,
|
Integer liveStatus,
|
Integer hasWatched,
|
LocalDateTime reserveFrom,
|
LocalDateTime reserveTo) {
|
LiveReserveSummaryRow row = liveReserveMapper.selectGlobalReserveSummary(
|
courseId,
|
sessionId,
|
trimToNull(phone),
|
trimToNull(courseName),
|
liveStatus,
|
hasWatched,
|
reserveFrom,
|
reserveTo
|
);
|
LiveReserveSummaryVO vo = new LiveReserveSummaryVO();
|
long reserveTotal = row == null || row.getReserveTotal() == null ? 0L : row.getReserveTotal();
|
long watchedTotal = row == null || row.getWatchedTotal() == null ? 0L : row.getWatchedTotal();
|
vo.setReserveTotal(reserveTotal);
|
vo.setWatchedTotal(watchedTotal);
|
vo.setReserveConversionRate(percent(watchedTotal, reserveTotal));
|
return vo;
|
}
|
|
private LiveWatchSummaryVO buildWatchSummary(Long courseId,
|
Long sessionId,
|
String regionCode,
|
String clinicName,
|
String phone,
|
String courseName,
|
Integer hasReserve,
|
Integer isCompleted,
|
Integer isEffective,
|
LocalDateTime watchFrom,
|
LocalDateTime watchTo) {
|
LiveWatchSummaryRow row = liveWatchRecordMapper.selectGlobalWatchSummary(
|
courseId,
|
sessionId,
|
trimToNull(regionCode),
|
trimToNull(clinicName),
|
trimToNull(phone),
|
trimToNull(courseName),
|
hasReserve,
|
isCompleted,
|
isEffective,
|
watchFrom,
|
watchTo
|
);
|
LiveWatchSummaryVO vo = new LiveWatchSummaryVO();
|
long watchUserTotal = row == null || row.getWatchUserTotal() == null ? 0L : row.getWatchUserTotal();
|
long completedUserTotal = row == null || row.getCompletedUserTotal() == null ? 0L : row.getCompletedUserTotal();
|
long effectiveUserTotal = row == null || row.getEffectiveUserTotal() == null ? 0L : row.getEffectiveUserTotal();
|
vo.setWatchUserTotal(watchUserTotal);
|
vo.setCompletedUserTotal(completedUserTotal);
|
vo.setEffectiveUserTotal(effectiveUserTotal);
|
vo.setCompletionRate(percent(completedUserTotal, watchUserTotal));
|
vo.setEffectiveWatchRate(percent(effectiveUserTotal, watchUserTotal));
|
return vo;
|
}
|
|
private BigDecimal percent(long numerator, long denominator) {
|
if (denominator <= 0) {
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
}
|
return BigDecimal.valueOf(numerator)
|
.multiply(BigDecimal.valueOf(100))
|
.divide(BigDecimal.valueOf(denominator), 2, RoundingMode.HALF_UP);
|
}
|
|
private String trimToNull(String value) {
|
if (value == null) {
|
return null;
|
}
|
String trimmed = value.trim();
|
return trimmed.isEmpty() ? null : trimmed;
|
}
|
|
private String defaultText(String value) {
|
return value == null || value.trim().isEmpty() ? "-" : value;
|
}
|
|
private String boolText(Integer value) {
|
return value != null && value == 1 ? "是" : "否";
|
}
|
|
private String liveStatusText(Integer value) {
|
if (value == null) {
|
return "-";
|
}
|
if (value == 1) {
|
return "直播中";
|
}
|
if (value == 2) {
|
return "已结束";
|
}
|
return "未开始";
|
}
|
|
private String formatMinute(LocalDateTime value) {
|
if (value == null) {
|
return "-";
|
}
|
String text = DateTimes.format(value);
|
return text == null || text.length() < 16 ? defaultText(text) : text.substring(0, 16);
|
}
|
}
|