This commit is contained in:
2026-07-23 18:30:30 +08:00
parent 86ab0142da
commit 785b9a4b80
57 changed files with 1795 additions and 340 deletions
@@ -245,7 +245,8 @@ public class ActivityBasicScopeController {
COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'sys_user'
AND TABLE_SCHEMA = 'budwk_v5_mini'
-- 使用当前连接数据库,避免环境切换后查询不到字段元数据。
AND TABLE_SCHEMA = DATABASE()
""");
List<NutMap> sqlDataList = activityBasicScopeService.listMap(sql);
// sqlDataList.forEach(v -> v.put("DATA_TYPE", new String((byte[]) v.get("DATA_TYPE"))));
@@ -3,7 +3,7 @@ package com.budwk.app.zhgh.dayofficework.healthCheckup.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.ManyAddOrRenewUtil;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.dayofficework.healthCheckup.model.*;
import com.budwk.app.zhgh.dayofficework.healthCheckup.service.HealthCheckupProjectService;
import org.nutz.aop.interceptor.ioc.TransAop;
@@ -36,6 +36,9 @@ public class HealthCheckupProjectServiceImpl extends BaseServiceImpl<HealthCheck
@Inject
private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
public HealthCheckupProjectServiceImpl(Dao dao) {
super(dao);
}
@@ -97,9 +100,10 @@ public class HealthCheckupProjectServiceImpl extends BaseServiceImpl<HealthCheck
public List<HealthCheckupUser> getHealthCheckupUserListByProject(HealthCheckupProject healthCheckupProject) {
String projectId = healthCheckupProject.getId();
List<ActivityUserScope> groupUsers = dao().query(ActivityUserScope.class, Cnd.where("groupId", "=", healthCheckupProject.getActivityGroupId()));
List<String> userIds = groupUsers.stream().map(ActivityUserScope::getUserId).collect(Collectors.toList());
List<View_user> userMapList = dao().query(View_user.class, Cnd.where("id", "in", userIds));
// 可报名人员范围同时支持人员结果分组和 SQL 条件分组,统一通过分组服务动态解析人员,
// 避免 SQL 条件分组因未保存 userId 明细而生成空名单。
List<View_user> userMapList = dao().query(View_user.class, Cnd.where("id", "in",
activityBasicScopeService.buildGroupUserIdSubSql(healthCheckupProject.getActivityGroupId())));
return userMapList.stream().map(v -> {
HealthCheckupUser user = new HealthCheckupUser();
user.setId(R.UU32());
@@ -97,6 +97,21 @@ public class OutlayManageClubQueryController {
return Result.success(pagination);
}
/**
* 导出社团拨付核对汇总表。
*
* @param year 统计年度,未传时默认当前年度
* @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团
* @param response 下载响应,返回 XSSF Excel 文件流
*/
@At
@Ok("void")
@ApiOperation("导出社团拨付核对汇总表")
@SaCheckPermission("outlay.outlayManage.club.query")
public void exportSummary(Integer year, String clubId, HttpServletResponse response) {
clubQueryService.exportSummary(year, clubId, response);
}
@At
@ApiOperation("提交核对")
@@ -29,6 +29,7 @@ import org.nutz.lang.Strings;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
@@ -90,6 +91,21 @@ public class OutlayManageClubUseDetailController {
return Result.success(pagination);
}
/**
* 导出社团预算使用情况汇总表。
*
* @param year 统计年度,未传时默认当前年度
* @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团
* @param response 下载响应,返回 XSSF Excel 文件流
*/
@At
@Ok("void")
@ApiOperation("导出社团预算使用情况")
@SaCheckPermission("outlay.outlayManage.club.useDetail")
public void doExport(Integer year, String clubId, HttpServletResponse response) {
outlayUseDetailService.exportClubUseDetail(year, clubId, response);
}
@At
@ApiOperation("某个社团预算使用详情")
@@ -5,9 +5,20 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import org.nutz.dao.sql.Sql;
import javax.servlet.http.HttpServletResponse;
public interface OutlayManageClubQueryService extends BaseService {
void doMoney(String clubId);
Sql backUserSql(PageForm pageForm, String clubId, String type, Integer year, String payed);
/**
* 导出社团拨付核对汇总表。
*
* @param year 统计年度,未传时默认当前年度
* @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团
* @param response 下载响应,用于向浏览器输出 XSSF Excel 文件
*/
void exportSummary(Integer year, String clubId, HttpServletResponse response);
}
@@ -3,6 +3,8 @@ package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail;
import javax.servlet.http.HttpServletResponse;
public interface OutlayUseDetailService extends BaseService<OutlayUseDetail> {
@@ -13,4 +15,13 @@ public interface OutlayUseDetailService extends BaseService<OutlayUseDetail> {
*/
void doDeleteDetail(String id,String outlayType);
void doEditDetail(OutlayUseDetail outlayUseDetail,String outlayType);
/**
* 导出社团预算使用情况汇总表。
*
* @param year 统计年度
* @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团
* @param response 下载响应,用于向浏览器输出 XSSF Excel 文件
*/
void exportClubUseDetail(Integer year, String clubId, HttpServletResponse response);
}
@@ -1,5 +1,9 @@
package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
@@ -7,6 +11,11 @@ import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.service.impl.SysClubUserServiceImpl;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayClubUserBackHistory;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
@@ -19,10 +28,15 @@ import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.apache.poi.ss.usermodel.Workbook;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -36,6 +50,10 @@ import java.util.stream.Collectors;
*/
@IocBean(args = {"refer:dao"})
public class OutlayManageClubQueryServiceImpl extends BaseServiceImpl implements OutlayManageClubQueryService {
@Inject
private SysRoleService sysRoleService;
public OutlayManageClubQueryServiceImpl(Dao dao) {
super(dao);
}
@@ -189,4 +207,66 @@ public class OutlayManageClubQueryServiceImpl extends BaseServiceImpl implements
sql.setCondition(cnd);
return sql;
}
/**
* 导出当前用户可查询的社团拨付核对汇总数据。
* 年审人数、可拨付人数和不可拨付人数分别取当前年度记录中的历史统计值;
* 拨付标准依据现有规则计算:0 人为 0 元,1 至 49 人为 3000 元,50 人及以上为 5000 元。
*
* @param year 统计年度,未传时使用当前年度
* @param clubId 社团主键,可为空
* @param response 下载响应,用于输出 Excel 文件
*/
@Override
public void exportSummary(Integer year, String clubId, HttpServletResponse response) {
Integer queryYear = year == null ? DateUtil.thisYear() : year;
Cnd cnd = Cnd.NEW();
cnd.and("year", "=", queryYear);
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name())) {
List<String> roleIds = sysRoleService.getRoleIdsByCode(List.of(RoleConstant.CLUB_PRESIDENT, RoleConstant.CLUB_SECRETARY));
List<Sys_user_role> userRoles = dao().query(Sys_user_role.class,
Cnd.where("userId", "=", SecurityUtil.getUserId())
.and("roleId", "in", roleIds));
List<String> myClubIds = userRoles.stream().map(Sys_user_role::getClubId).toList();
cnd.andEX("clubId", "in", myClubIds);
}
cnd.andEX("clubId", "=", clubId);
cnd.asc("clubCode");
Sql summarySql = Sqls.create("SELECT * FROM `outlay_manage_club` $condition");
summarySql.setCondition(cnd);
List<NutMap> summaryList = listMap(summarySql);
for (NutMap summary : summaryList) {
int giveMoneyNum = summary.getInt("historyGiveMoneyNum");
summary.put("standard", getAllocateStandard(giveMoneyNum));
summary.put("isCheckName", summary.getBoolean("isCheck") ? "已核对" : "未核对");
}
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("社团名称", "clubName", 25));
exportEntities.add(new ExcelExportEntity("年审人数", "historyUserNum", 15));
exportEntities.add(new ExcelExportEntity("可拨付人数", "historyGiveMoneyNum", 15));
exportEntities.add(new ExcelExportEntity("不可拨付人数", "historyNotGiveMoneyNum", 15));
exportEntities.add(new ExcelExportEntity("标准", "standard", 15));
exportEntities.add(new ExcelExportEntity("拨付总额", "totalQuota", 15));
exportEntities.add(new ExcelExportEntity("是否核对", "isCheckName", 15));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, summaryList);
CommonDownloadUtil.download(queryYear + "年社团拨付核对汇总.xlsx", workbook, response);
}
/**
* 根据可拨付人数返回社团年度拨付标准。
*
* @param giveMoneyNum 可拨付人数
* @return 对应的拨付标准金额
*/
private BigDecimal getAllocateStandard(int giveMoneyNum) {
if (giveMoneyNum == 0) {
return BigDecimal.ZERO;
}
return giveMoneyNum >= 50 ? BigDecimal.valueOf(5000) : BigDecimal.valueOf(3000);
}
}
@@ -1,13 +1,35 @@
package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.school.model.OutlayManageSchool;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayUseDetailService;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* @author zhf
@@ -16,6 +38,10 @@ import org.nutz.ioc.loader.annotation.IocBean;
*/
@IocBean(args = {"refer:dao"})
public class OutlayUseDetailServiceImpl extends BaseServiceImpl<OutlayUseDetail> implements OutlayUseDetailService {
@Inject
private SysRoleService sysRoleService;
public OutlayUseDetailServiceImpl(Dao dao) {
super(dao);
}
@@ -34,6 +60,54 @@ public class OutlayUseDetailServiceImpl extends BaseServiceImpl<OutlayUseDetail>
update(outlayUseDetail);
}
/**
* 按社团预算使用页面的查询口径导出汇总数据。
* 剩余额度由分配总额度减已使用额度计算,避免导出内容与页面展示不一致。
*
* @param year 统计年度,未传时使用当前年度
* @param clubId 社团主键,可为空
* @param response 下载响应,用于输出 Excel 文件
*/
@Override
public void exportClubUseDetail(Integer year, String clubId, HttpServletResponse response) {
Integer queryYear = year == null ? DateUtil.thisYear() : year;
Cnd cnd = Cnd.NEW();
cnd.and("totalQuota", "IS NOT", null);
cnd.and("year", "=", queryYear);
if (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.andEX("clubId", "=", clubId);
} else {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.CLUB_PRESIDENT);
List<Sys_user_role> userRoles = dao().query(Sys_user_role.class,
Cnd.where("userId", "=", SecurityUtil.getUserId())
.and("roleId", "=", sysRole.getId()));
List<String> myClubIds = userRoles.stream().map(Sys_user_role::getClubId).toList();
cnd.andEX("clubId", "in", myClubIds);
}
cnd.asc("clubCode");
Sql sql = Sqls.create("""
SELECT *, COALESCE(totalQuota, 0) - COALESCE(usedQuota, 0) AS surplusQuota
FROM outlay_manage_club $condition
""");
sql.setCondition(cnd);
List<NutMap> clubUseDetailList = listMap(sql);
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("年度", "year", 15));
exportEntities.add(new ExcelExportEntity("社团名称", "clubName", 25));
exportEntities.add(new ExcelExportEntity("社团编码", "clubCode", 20));
exportEntities.add(new ExcelExportEntity("分配总额度(元)", "totalQuota", 18));
exportEntities.add(new ExcelExportEntity("已使用额度(元)", "usedQuota", 18));
exportEntities.add(new ExcelExportEntity("剩余额度(元)", "surplusQuota", 18));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, clubUseDetailList);
CommonDownloadUtil.download(queryYear + "年社团预算使用情况.xlsx", workbook, response);
}
/**
* 同步预算主表的已使用额度,保证校工会、分工会、协会三类台账口径一致。
*
@@ -265,6 +265,7 @@ public class ProposalMineController {
Cnd cnd = Cnd.where("t1.sessionId", "=", sessionId);
cnd.andEX("t1.delegationId", "=", delegationId);
cnd.and("t1.roleId", "=", "72e24b5b4c4f4e90a6641e6af8487e42");
cnd.and("t1.delFlag", "!=", 1);
cnd.and("t1.loginName", "!=", SecurityUtil.getUserLoginname());
if (isInvite) {
@@ -286,11 +287,33 @@ public class ProposalMineController {
return Result.success(pagination);
}
/**
* 邀请当前提案所属教代会届次内的正式代表参与附议。
*
* @param seconders 附议人工号数组,数组中的每个工号必须属于当前提案届次的有效正式代表
* @param proposalId 提案ID,用于确定流程实例及可信的教代会届次
* @return Result;邀请成功返回 success,提案不存在、代表信息无效或人数不足时返回错误信息
*/
@At
@SaCheckPermission("proposal.mine")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "提案", msg = "邀请附议人")
public Result inviteSeconder(@Param("seconders") String[] seconders, @Valid String proposalId) {
if (seconders == null || seconders.length == 0) {
return Result.error("请选择附议人");
}
ProposalInfo proposalInfo = dao.fetch(ProposalInfo.class, proposalId);
if (proposalInfo == null || StrUtil.isBlank(proposalInfo.getSessionId())) {
return Result.error("提案信息不存在");
}
List<String> requestedLoginNames = List.of(seconders).stream()
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (requestedLoginNames.isEmpty()) {
return Result.error("请选择附议人");
}
// 查询已经邀请的附议人
ProcessInstance processInstance = dao.fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", proposalId));
@@ -319,11 +342,7 @@ public class ProposalMineController {
ProposalConfig proposalConfig = dao.fetch(ProposalConfig.class, Cnd.NEW());
if (tasks.size() + seconders.length < proposalConfig.getSeconderNum()) {
return Result.error("至少邀请" + proposalConfig.getSeconderNum() + "个附议人");
}
// 必须使用提案自身的届次查询,不能只按工号匹配,否则同一代表的历史届次记录会污染代表团等快照信息。
Sql sql = Sqls.create("""
SELECT
t1.id,
@@ -340,8 +359,20 @@ public class ProposalMineController {
LEFT JOIN teacher_congress_delegation t2 ON t2.id = t1.delegationId
$condition
""");
sql.setCondition(Cnd.where("t1.loginName", "in", seconders).groupBy("t1.loginName"));
Cnd seconderCnd = Cnd.where("t1.sessionId", "=", proposalInfo.getSessionId())
.and("t1.loginName", "in", requestedLoginNames)
.and("t1.roleId", "=", "72e24b5b4c4f4e90a6641e6af8487e42")
.and("t1.delFlag", "!=", 1);
sql.setCondition(seconderCnd);
List<NutMap> list = proposalCommonService.listMap(sql);
// 每个工号在当前届次必须且只能匹配一条有效代表记录,防止无效工号或重复基础数据进入流程。
if (list.size() != requestedLoginNames.size()) {
return Result.error("部分附议人不属于当前提案届次或代表信息重复,请重新选择");
}
if (tasks.size() + list.size() < proposalConfig.getSeconderNum()) {
return Result.error("至少邀请" + proposalConfig.getSeconderNum() + "个附议人");
}
// 附议人ID
List<String> userIds = list.stream().map(item -> item.getString("userId")).toList();
// 获取正在执行的任务(邀请附议人||提案附议)
@@ -45,6 +45,7 @@ import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
@@ -76,14 +77,24 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
throw new BaseException("没有进行中的流程任务");
}
String proposalId = StrUtil.trim(args.getStr("proposalId"));
if (StrUtil.isBlank(proposalId) || dao().fetch(ProposalInfo.class, proposalId) == null) {
throw new BaseException("提案信息不存在");
}
args.put("proposalId", proposalId);
validateAndNormalizeCaseFilingCode(args);
String proposalId = args.getStr("proposalId");
List<String> proposalIds = proposalCommonService.mergeProposal(proposalId);
if (ObjectUtil.isEmpty(proposalIds)) {
proposalIds = List.of(proposalId);
}
/*
* 先同步 proposal_info 和承办单位,再执行 WF 流转,使查看页面及下一节点读取到的都是本次最新数据。
* 当前方法处于同一事务中,后续流程执行失败时这里的业务数据会一并回滚。
*/
syncCommitteeFilingUnitData(proposalIds, args);
/*
* 并案场景下,同一个节点会在每条提案实例上各有一条待办。
* 这里统一按 taskName 找出整组正在办理中的任务,逐条执行,确保并案提案的流程状态保持一致。
@@ -97,8 +108,6 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
cloneArgs.put(FlowConst.PROCESS_TASK_ID_KEY, executeTask.getId());
flowCommonService.executeTask(cloneArgs);
}
syncCommitteeFilingUnitData(proposalIds, args);
}
/**
@@ -110,8 +119,10 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
private void validateAndNormalizeCaseFilingCode(Dict args) {
String caseFilingResultKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult";
String caseFilingCodeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode";
String caseFilingTypeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType";
if (!"CONFIRM_FILING".equals(args.getStr(caseFilingResultKey))) {
args.put(caseFilingCodeKey, "");
args.put(caseFilingTypeKey, "");
return;
}
@@ -139,6 +150,7 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
.add("caseFilingResult", caseFilingResult)
.add("caseFilingType", caseFilingType),
Cnd.where(ProposalInfo::getId, "in", proposalIds));
verifyCommitteeFilingUnitData(proposalIds, caseFilingCode, caseFilingResult, caseFilingType);
dao().clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds));
if (!List.of("CONFIRM_FILING", "SUGGESTION").contains(caseFilingResult)) {
@@ -175,6 +187,28 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
}
}
/**
* 校验本次立案字段是否已同步到全部提案,避免流程变量更新成功但 info 基础信息仍保留旧值。
*/
private void verifyCommitteeFilingUnitData(List<String> proposalIds, String caseFilingCode,
String caseFilingResult, String caseFilingType) {
List<ProposalInfo> proposalInfos = dao().query(ProposalInfo.class,
Cnd.where(ProposalInfo::getId, "in", proposalIds));
if (proposalInfos.size() != new LinkedHashSet<>(proposalIds).size()) {
throw new BaseException("部分提案信息不存在,无法同步立案结果");
}
boolean syncFailed = proposalInfos.stream().anyMatch(proposalInfo ->
!Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingCode(), ""),
StrUtil.nullToDefault(caseFilingCode, ""))
|| !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingResult(), ""),
StrUtil.nullToDefault(caseFilingResult, ""))
|| !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingType(), ""),
StrUtil.nullToDefault(caseFilingType, "")));
if (syncFailed) {
throw new BaseException("立案结果同步失败,请稍后重试");
}
}
/**
* 一个提案可以配置多个主办单位和多个协办单位。
* 这里按页面提交顺序逐条生成承办单位记录,兼容多主办的业务场景。
@@ -95,6 +95,7 @@ public class SuggestionBoxMineController {
}
cnd.andEX("YEAR(info.submitTime)", "=", year);
cnd.desc("info.submitTime");
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination<NutMap> pagination = suggestionBoxService.listPageMap(pageNumber, pageSize, sql);
return Result.success(pagination);
@@ -20,6 +20,7 @@ import com.budwk.app.zhgh.learning.models.LearningCourseOutline;
import com.budwk.app.zhgh.learning.models.LearningCourseType;
import com.budwk.app.zhgh.learning.models.LearningOutlineResource;
import com.budwk.app.zhgh.learning.models.LearningStudyRecord;
import com.budwk.app.zhgh.learning.models.LearningStudyRule;
import com.google.common.net.HttpHeaders;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -29,6 +30,7 @@ import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
@@ -59,7 +61,7 @@ public class LearningCourseDisplayController {
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/index.html")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/indexV2.html")
@SaCheckPermission("learning.course.display")
public void index() {
}
@@ -71,7 +73,7 @@ public class LearningCourseDisplayController {
}
@At("/study")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/study.html")
@Ok("beetl:/platform/zhgh/Learning/courseDisplay/studyV2.html")
@SaCheckPermission("learning.course.display")
public void study() {
}
@@ -79,7 +81,12 @@ public class LearningCourseDisplayController {
@At
@ApiOperation("课程展示列表")
@SaCheckPermission("learning.course.display")
public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag) {
/**
* 查询课程展示列表。
* pageForm 传分页页码和每页条数;keyword、courseTypeId、recommendFlag 分别用于关键词、分类和推荐标识筛选;
* sortType 支持 default(综合排序)和 view_count(观看次数倒序)。返回值为 Pagination,list 中包含课程基础字段、课节数、总时长和观看次数。
*/
public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag, String sortType) {
Cnd cnd = Cnd.where("c.status", "=", "published");
if (StrUtil.isNotBlank(keyword)) {
cnd.and(Cnd.exps("c.courseName", "like", "%" + keyword + "%")
@@ -103,13 +110,22 @@ public class LearningCourseDisplayController {
Sql listSql = Sqls.create("""
SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover,
c.recommendFlags, c.courseTypeId, c.lecturerName, c.sortNum, t.typeName AS courseTypeName
c.recommendFlags, c.courseTypeId, c.lecturerName, c.sortNum,
COALESCE(c.viewCount, 0) AS viewCount, t.typeName AS courseTypeName,
(SELECT COUNT(1) FROM learning_outline_resource r
WHERE r.courseId = c.id AND r.status = 'enabled') AS resourceCount,
COALESCE((SELECT SUM(r.durationSeconds) FROM learning_outline_resource r
WHERE r.courseId = c.id AND r.status = 'enabled'), 0) AS totalDurationSeconds
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
$condition
ORDER BY c.sortNum ASC, c.createdAt DESC
ORDER BY $orderBy
""");
listSql.setCondition(cnd);
// 排序字段由固定选项映射,禁止将前端参数直接拼接进SQL。
listSql.setVar("orderBy", "view_count".equals(sortType)
? "COALESCE(c.viewCount, 0) DESC, c.sortNum ASC, c.createdAt DESC"
: "c.sortNum ASC, c.createdAt DESC");
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
dao.execute(listSql);
@@ -126,13 +142,24 @@ public class LearningCourseDisplayController {
@At
@ApiOperation("课程学习基础信息")
@SaCheckPermission("learning.course.display")
/**
* 查询学习页课程信息。
* id 传课程主键;返回 NutMap,包含课程介绍、讲师、分类、课节数、总时长和后台配置的观看次数。
*/
public Result courseInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择课程");
}
// 学习页复用课程配置和资源统计数据,观看次数由课程管理后台配置,不在访问时自动累加。
Sql sql = Sqls.create("""
SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover,
c.recommendFlags, c.lecturerName, t.typeName AS courseTypeName
SELECT c.id, c.courseName, c.courseIntro, c.suitablePeople, c.learningGoal, c.coursePeriod,
c.courseTags, c.startTime, c.endTime, c.openType, c.cover, c.recommendFlags,
c.lecturerName, c.lecturerInfo, COALESCE(c.viewCount, 0) AS viewCount,
t.typeName AS courseTypeName,
(SELECT COUNT(1) FROM learning_outline_resource r
WHERE r.courseId = c.id AND r.status = 'enabled') AS resourceCount,
COALESCE((SELECT SUM(r.durationSeconds) FROM learning_outline_resource r
WHERE r.courseId = c.id AND r.status = 'enabled'), 0) AS totalDurationSeconds
FROM learning_course c
LEFT JOIN learning_course_type t ON t.id = c.courseTypeId
WHERE c.id = @id
@@ -161,12 +188,17 @@ public class LearningCourseDisplayController {
List<LearningCourseOutline> outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt"));
List<LearningOutlineResource> resources = dao.query(LearningOutlineResource.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt"));
List<LearningStudyRecord> records = dao.query(LearningStudyRecord.class, Cnd.where("courseId", "=", courseId).and("userId", "=", SecurityUtil.getUserId()));
List<LearningStudyRule> outlineRules = dao.query(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", "outline").and("status", "=", "enabled"));
Map<String, List<NutMap>> childrenMap = new HashMap<>();
List<NutMap> roots = new ArrayList<>();
Map<String, LearningStudyRecord> recordMap = new HashMap<>();
Map<String, LearningStudyRule> outlineRuleMap = new HashMap<>();
for (LearningStudyRecord record : records) {
recordMap.put(record.getOutlineId(), record);
}
for (LearningStudyRule outlineRule : outlineRules) {
outlineRuleMap.put(outlineRule.getTargetId(), outlineRule);
}
Map<String, NutMap> outlineNodeMap = new HashMap<>();
for (LearningCourseOutline outline : outlines) {
@@ -194,6 +226,9 @@ public class LearningCourseDisplayController {
}
}
for (LearningOutlineResource resource : resources) {
// 章节学习规则控制其下媒体播放行为;未配置规则时保持原有的可拖动行为。
LearningStudyRule outlineRule = outlineRuleMap.get(resource.getOutlineId());
boolean allowDrag = outlineRule == null || !Boolean.FALSE.equals(outlineRule.getAllowDrag());
NutMap node = NutMap.NEW()
.addv("id", resource.getId())
.addv("type", "resource")
@@ -207,6 +242,8 @@ public class LearningCourseDisplayController {
.addv("progressPercent", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getProgressPercent() : 0)
.addv("studySeconds", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getStudySeconds() : 0)
.addv("lastPositionSeconds", recordMap.containsKey(resource.getOutlineId()) ? value(recordMap.get(resource.getOutlineId()).getLastPositionSeconds()) : 0)
.addv("maxPositionSeconds", resourceMaxPosition(recordMap.get(resource.getOutlineId()), resource.getId()))
.addv("allowDrag", allowDrag)
.addv("completeStatus", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getCompleteStatus() : "not_started");
childrenMap.computeIfAbsent(resource.getOutlineId(), key -> new ArrayList<>()).add(node);
}
@@ -294,4 +331,23 @@ public class LearningCourseDisplayController {
private int value(Integer value) {
return value == null ? 0 : value;
}
/**
* 读取指定资源的最高已观看秒数。该值用于两端限制快进,最近播放位置仍单独用于续播。
*/
private int resourceMaxPosition(LearningStudyRecord record, String resourceId) {
if (record == null || StrUtil.isBlank(resourceId)) {
return 0;
}
if (StrUtil.isBlank(record.getResourceMaxPositionData())) {
return value(record.getLastPositionSeconds());
}
try {
NutMap positions = Json.fromJson(NutMap.class, record.getResourceMaxPositionData());
return Math.max(0, positions.getInt(resourceId, 0));
} catch (Exception ignored) {
// 历史记录未保存资源级位置时,从零开始建立最高观看位置。
return 0;
}
}
}
@@ -18,6 +18,8 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
@@ -113,6 +115,8 @@ public class LearningCourseManageController {
@ApiOperation("新增课程")
@SaCheckPermission("learning.course.manage")
@SLog(tag = "新增课程", msg = "课程名称:${args[0].courseName}")
@Aop(TransAop.READ_COMMITTED)
/** course 传课程表单字段,viewCount 为非负整数;返回 Result 表示新增是否成功。 */
public Result doAdd(LearningCourse course) {
Result checkResult = checkCourse(course, null);
if (checkResult != null) {
@@ -121,6 +125,9 @@ public class LearningCourseManageController {
if (StrUtil.isBlank(course.getStatus())) {
course.setStatus("draft");
}
if (course.getViewCount() == null) {
course.setViewCount(0);
}
dao.insert(course);
return Result.success();
}
@@ -129,6 +136,8 @@ public class LearningCourseManageController {
@ApiOperation("编辑课程")
@SaCheckPermission("learning.course.manage")
@SLog(tag = "编辑课程", msg = "课程ID:${args[0].id}")
@Aop(TransAop.READ_COMMITTED)
/** course 传课程主键及需要更新的表单字段;返回 Result 表示编辑是否成功。 */
public Result doEdit(LearningCourse course) {
if (StrUtil.isBlank(course.getId())) {
return Result.error("请选择要编辑的数据");
@@ -145,6 +154,8 @@ public class LearningCourseManageController {
@ApiOperation("删除课程")
@SaCheckPermission("learning.course.manage")
@SLog(tag = "删除课程", msg = "课程ID:${args[0]}")
@Aop(TransAop.READ_COMMITTED)
/** id 传课程主键;返回 Result,存在章节内容时返回不可删除提示。 */
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("请选择要删除的数据");
@@ -200,6 +211,9 @@ public class LearningCourseManageController {
if (course.getSortNum() == null) {
return Result.error("排序编码不能为空");
}
if (course.getViewCount() != null && course.getViewCount() < 0) {
return Result.error("观看次数不能小于0");
}
if (!"long_term".equals(course.getOpenType()) && (course.getStartTime() == null || course.getEndTime() == null)) {
return Result.error("请选择开课时间");
}
@@ -227,6 +241,7 @@ public class LearningCourseManageController {
columns.put("status", "c.status");
columns.put("recommendFlags", "c.recommendFlags");
columns.put("sortNum", "c.sortNum");
columns.put("viewCount", "c.viewCount");
return columns.get(prop);
}
}
@@ -24,8 +24,11 @@ import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
@@ -151,6 +154,11 @@ public class LearningStudyRecordController {
@At
@ApiOperation("开始学习")
@SaCheckPermission("learning.course.display")
@Aop(TransAop.READ_COMMITTED)
/**
* courseId、resourceId 分别传课程和资源主键,positionSeconds 传当前播放秒数;
* 返回学习记录、学习时段、学习进度和本次自增后的课程观看次数。
*/
public Result start(@Param("courseId") String courseId,
@Param("resourceId") String resourceId,
@Param("positionSeconds") Integer positionSeconds) {
@@ -176,18 +184,30 @@ public class LearningStudyRecordController {
.and("outlineId", "=", outline.getId()));
if (record == null) {
record = buildRecord(userId, course, outline, now);
record.setLastPositionSeconds(sanitizePosition(positionSeconds, 0));
int startPositionSeconds = sanitizePosition(positionSeconds, 0);
record.setLastPositionSeconds(startPositionSeconds);
record.setResourceMaxPositionData(resourceMaxPositionData(resourceId, startPositionSeconds));
dao.insert(record);
} else {
int requiredSeconds = requiredSeconds(outline.getId());
dao.update(LearningStudyRecord.class, Chain.make("latestStudyTime", now)
int startPositionSeconds = sanitizePosition(positionSeconds, 0);
Map<String, Integer> resourceMaxPositions = resourceMaxPositions(record.getResourceMaxPositionData());
int maxPositionSeconds = value(resourceMaxPositions.get(resourceId));
boolean needUpdateMaxPosition = !resourceMaxPositions.containsKey(resourceId) || startPositionSeconds > maxPositionSeconds;
if (needUpdateMaxPosition) {
resourceMaxPositions.put(resourceId, Math.max(startPositionSeconds, maxPositionSeconds));
}
Chain chain = Chain.make("latestStudyTime", now)
.add("courseName", course.getCourseName())
.add("outlineName", outline.getTitle())
.add("lastPositionSeconds", sanitizePosition(positionSeconds, record.getLastPositionSeconds()))
.add("requiredSeconds", requiredSeconds)
.add("progressPercent", progress(record.getStudySeconds(), requiredSeconds))
.add("completeStatus", "completed".equals(record.getCompleteStatus()) ? "completed" : "studying"),
Cnd.where("id", "=", record.getId()));
.add("completeStatus", "completed".equals(record.getCompleteStatus()) ? "completed" : "studying");
if (needUpdateMaxPosition) {
chain.add("resourceMaxPositionData", Json.toJson(resourceMaxPositions));
}
dao.update(LearningStudyRecord.class, chain, Cnd.where("id", "=", record.getId()));
record = dao.fetch(LearningStudyRecord.class, record.getId());
}
@@ -204,19 +224,28 @@ public class LearningStudyRecordController {
segment.setState("studying");
dao.insert(segment);
// 每次成功创建学习时段记为一次观看,使用原子自增避免并发开始学习时丢失计数。
dao.update(LearningCourse.class,
Chain.makeSpecial("viewCount", "IFNULL(viewCount, 0) + 1"),
Cnd.where("id", "=", courseId));
LearningCourse updatedCourse = dao.fetch(LearningCourse.class, courseId);
return Result.success(NutMap.NEW()
.addv("recordId", record.getId())
.addv("segmentId", segment.getId())
.addv("studySeconds", record.getStudySeconds())
.addv("lastPositionSeconds", record.getLastPositionSeconds())
.addv("maxPositionSeconds", resourceMaxPosition(record, resourceId))
.addv("requiredSeconds", record.getRequiredSeconds())
.addv("progressPercent", record.getProgressPercent())
.addv("completeStatus", record.getCompleteStatus()));
.addv("completeStatus", record.getCompleteStatus())
.addv("viewCount", value(updatedCourse == null ? null : updatedCourse.getViewCount())));
}
@At
@ApiOperation("学习心跳")
@SaCheckPermission("learning.course.display")
@Aop(TransAop.READ_COMMITTED)
public Result heartbeat(@Param("segmentId") String segmentId,
@Param("activeSeconds") Integer activeSeconds,
@Param("positionSeconds") Integer positionSeconds) {
@@ -225,21 +254,27 @@ public class LearningStudyRecordController {
return Result.error("学习时段已结束,请重新开始学习");
}
int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS));
return Result.success(addActiveSeconds(segment, seconds, false, positionSeconds));
return Result.success(addActiveSeconds(segment, seconds, false, false, positionSeconds));
}
@At
@ApiOperation("结束学习")
@SaCheckPermission("learning.course.display")
@Aop(TransAop.READ_COMMITTED)
/**
* segmentId 传当前学习时段主键,activeSeconds 传本次尚未上报的有效学习秒数,positionSeconds 传当前播放位置;
* completed 仅在视频或音频自然播放结束时传 true。返回值为 NutMap,包含累计学习时长、进度和完成状态。
*/
public Result finish(@Param("segmentId") String segmentId,
@Param("activeSeconds") Integer activeSeconds,
@Param("positionSeconds") Integer positionSeconds) {
@Param("positionSeconds") Integer positionSeconds,
@Param("completed") Boolean completed) {
LearningStudySegment segment = fetchOwnStudyingSegment(segmentId);
if (segment == null) {
return Result.success();
}
int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS));
NutMap result = addActiveSeconds(segment, seconds, true, positionSeconds);
NutMap result = addActiveSeconds(segment, seconds, true, Boolean.TRUE.equals(completed), positionSeconds);
dao.update(LearningStudySegment.class, Chain.make("endTime", new Date()).add("state", "finished"), Cnd.where("id", "=", segmentId));
return Result.success(result);
}
@@ -247,6 +282,7 @@ public class LearningStudyRecordController {
@At
@ApiOperation("保存播放位置")
@SaCheckPermission("learning.course.display")
@Aop(TransAop.READ_COMMITTED)
public Result position(@Param("courseId") String courseId,
@Param("resourceId") String resourceId,
@Param("positionSeconds") Integer positionSeconds) {
@@ -321,20 +357,28 @@ public class LearningStudyRecordController {
record.setLatestStudyTime(now);
record.setStudySeconds(0);
record.setLastPositionSeconds(0);
record.setResourceMaxPositionData("{}");
record.setRequiredSeconds(requiredSeconds);
record.setProgressPercent(0);
record.setCompleteStatus("studying");
return record;
}
private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, Integer positionSeconds) {
private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, boolean completed, Integer positionSeconds) {
Date now = new Date();
int segmentSeconds = value(segment.getActiveSeconds()) + activeSeconds;
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, segment.getRecordId());
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, segment.getResourceId());
int studyIncrementSeconds = effectiveStudySeconds(record, resource, segment.getResourceId(), activeSeconds, positionSeconds);
int segmentSeconds = value(segment.getActiveSeconds()) + studyIncrementSeconds;
dao.update(LearningStudySegment.class, Chain.make("activeSeconds", segmentSeconds).add("lastHeartbeatTime", now), Cnd.where("id", "=", segment.getId()));
LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, segment.getRecordId());
int studySeconds = value(record.getStudySeconds()) + activeSeconds;
int studySeconds = value(record.getStudySeconds()) + studyIncrementSeconds;
int requiredSeconds = requiredSeconds(record.getOutlineId());
int maxPositionSeconds = resourceMaxPosition(record, segment.getResourceId());
// 自然播放结束且当前章节仅有一个媒体资源时,补齐定时器未统计到的尾部秒数,避免跨端状态不一致。
if (completed && canCompleteByNaturalEnd(segment, positionSeconds, requiredSeconds)) {
studySeconds = Math.max(studySeconds, requiredSeconds);
}
int progressPercent = progress(studySeconds, requiredSeconds);
String completeStatus = completeStatus(studySeconds, requiredSeconds, finish);
if ("completed".equals(completeStatus)) {
@@ -350,6 +394,13 @@ public class LearningStudyRecordController {
lastPositionSeconds = positionSeconds;
chain.add("lastPositionSeconds", positionSeconds);
}
if (resource != null && isMediaResource(resource)) {
Map<String, Integer> resourceMaxPositions = resourceMaxPositions(record.getResourceMaxPositionData());
int currentPositionSeconds = Math.max(0, positionSeconds == null ? 0 : positionSeconds);
maxPositionSeconds = Math.max(value(resourceMaxPositions.get(segment.getResourceId())), currentPositionSeconds);
resourceMaxPositions.put(segment.getResourceId(), maxPositionSeconds);
chain.add("resourceMaxPositionData", Json.toJson(resourceMaxPositions));
}
if ("completed".equals(completeStatus) && record.getCompletedAt() == null) {
chain.add("completedAt", now);
}
@@ -358,6 +409,7 @@ public class LearningStudyRecordController {
.addv("studySeconds", studySeconds)
.addv("studyTimeText", formatStudySeconds(studySeconds))
.addv("lastPositionSeconds", lastPositionSeconds)
.addv("maxPositionSeconds", maxPositionSeconds)
.addv("requiredSeconds", requiredSeconds)
.addv("progressPercent", progressPercent)
.addv("completeStatus", completeStatus);
@@ -396,6 +448,85 @@ public class LearningStudyRecordController {
return Math.max(0, sql.getInt());
}
/**
* 校验媒体自然播放结束能否完成当前章节。
* segment 表示当前用户的学习时段;positionSeconds 必须到达资源末尾附近;requiredSeconds 为章节要求学习时长。
* 仅单资源章节且配置时长不超过媒体时长时返回 true,防止一个资源结束误完成包含多个资源或设置了更长时长的章节。
*/
private boolean canCompleteByNaturalEnd(LearningStudySegment segment, Integer positionSeconds, int requiredSeconds) {
if (positionSeconds == null || requiredSeconds <= 0) {
return false;
}
LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, segment.getResourceId());
if (resource == null || !("video".equals(resource.getResourceType()) || "audio".equals(resource.getResourceType()))) {
return false;
}
int durationSeconds = value(resource.getDurationSeconds());
if (durationSeconds <= 0 || positionSeconds < Math.max(0, durationSeconds - 2) || requiredSeconds > durationSeconds) {
return false;
}
Sql countSql = Sqls.create("SELECT COUNT(1) FROM learning_outline_resource WHERE outlineId = @outlineId AND status = 'enabled'");
countSql.params().set("outlineId", segment.getOutlineId());
countSql.setCallback(Sqls.callback.integer());
dao.execute(countSql);
return countSql.getInt() == 1;
}
/**
* 仅统计当前资源超过历史最高观看位置的新增区间,回退复习不会重复累计学习时长。
* record 保存各资源最高位置,resource 用于区分媒体和非媒体资料,positionSeconds 为本次心跳时的播放器位置。
*/
private int effectiveStudySeconds(LearningStudyRecord record, LearningOutlineResource resource, String resourceId, int activeSeconds, Integer positionSeconds) {
if (!isMediaResource(resource)) {
return activeSeconds;
}
int currentPositionSeconds = Math.max(0, positionSeconds == null ? 0 : positionSeconds);
int maxPositionSeconds = resourceMaxPosition(record, resourceId);
return Math.min(activeSeconds, Math.max(0, currentPositionSeconds - maxPositionSeconds));
}
/**
* 读取学习记录中按资源保存的最高已观看秒数。历史记录或异常 JSON 视为未保存,避免影响已有记录的使用。
*/
private int resourceMaxPosition(LearningStudyRecord record, String resourceId) {
return value(resourceMaxPositions(record == null ? null : record.getResourceMaxPositionData()).get(resourceId));
}
/**
* 将资源 ID 与最高观看秒数转换为可持久化数据;键为资源 ID,值为该资源累计到达过的最远秒数。
*/
private String resourceMaxPositionData(String resourceId, int positionSeconds) {
Map<String, Integer> positions = new HashMap<>();
positions.put(resourceId, Math.max(0, positionSeconds));
return Json.toJson(positions);
}
/**
* 解析资源最高位置 JSON。返回值为资源 ID 到秒数的映射,空值或异常数据返回空映射。
*/
private Map<String, Integer> resourceMaxPositions(String data) {
Map<String, Integer> positions = new HashMap<>();
if (StrUtil.isBlank(data)) {
return positions;
}
try {
NutMap source = Json.fromJson(NutMap.class, data);
for (String resourceId : source.keySet()) {
positions.put(resourceId, Math.max(0, source.getInt(resourceId, 0)));
}
} catch (Exception ignored) {
// 历史记录可能没有该字段或保存了异常内容,按未记录最高位置处理。
}
return positions;
}
/**
* 判断资料是否使用播放器计时。视频和音频按未重复观看区间累计,其他资料沿用活跃时长累计。
*/
private boolean isMediaResource(LearningOutlineResource resource) {
return resource != null && ("video".equals(resource.getResourceType()) || "audio".equals(resource.getResourceType()));
}
private int progress(Integer studySeconds, int requiredSeconds) {
if (requiredSeconds <= 0) {
return 0;
@@ -110,6 +110,11 @@ public class LearningCourse extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer sortNum;
@Column
@Comment("观看次数")
@ColDefine(type = ColType.INT)
private Integer viewCount;
@Column
@Comment("课程封面")
@ColDefine(type = ColType.VARCHAR, width = 500)
@@ -97,6 +97,11 @@ public class LearningStudyRecord extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer lastPositionSeconds;
@Column
@Comment("各资源最高已观看位置(JSON)")
@ColDefine(type = ColType.TEXT)
private String resourceMaxPositionData;
@Column
@Comment("要求学习时长(秒)")
@ColDefine(type = ColType.INT)
@@ -17,6 +17,7 @@ import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import com.budwk.app.zhgh.staffmanage.member.models.MemberChangeRecord;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberChangePageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
import com.budwk.app.sys.models.Sys_unit;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
@@ -155,6 +156,12 @@ public class MemberChangeMineController {
}
/**
* 查询会员变更记录详情。
*
* @param id 会员变更记录 ID。
* @return 包含变更记录基础字段、三级单位名称及变更明细的 JSON 对象。
*/
@At
@ApiOperation("根据id查询变更记录")
@SaCheckPermission(value = {"member.change.mine", "member.change.unionGroup.audit"}, mode = SaMode.OR)
@@ -162,6 +169,12 @@ public class MemberChangeMineController {
MemberChangeRecord record = dao.fetch(MemberChangeRecord.class, id);
NutMap nutMap = Lang.obj2nutmap(record);
// 变更记录只保存三级单位 ID,查看时补充当前单位名称供页面展示。
if (StrUtil.isNotBlank(record.getThreeUnitId())) {
Sys_unit threeUnit = dao.fetch(Sys_unit.class, record.getThreeUnitId());
nutMap.put("threeUnitName", threeUnit == null ? "" : threeUnit.getName());
}
List<NutMap> changeInfos = memberCommonService.getChangeInfos(nutMap.getString("id"));
nutMap.put("changeInfos", changeInfos);
return Result.success().addData(nutMap);
@@ -177,6 +177,11 @@ public class MemberChangeRecord extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 20)
private String campus;
@Column
@Comment("三级单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String threeUnitId;
@Column
@Comment("退休日期")
@ColDefine(type = ColType.DATE)
@@ -30,6 +30,9 @@ public class MemberManagePageForm extends PageForm {
private String unitId;
/** 所属二级单位下的三级单位 ID。 */
private String threeUnitId;
private String sex;
private String personType;
@@ -91,6 +94,7 @@ public class MemberManagePageForm extends PageForm {
cnd.andEX("u.member", "=", pageForm.getIsMember());
cnd.andEX("u.welfareMember", "=", pageForm.getIsWelfareMember());
cnd.andEX("u.unitId", "=", pageForm.getUnitId());
cnd.andEX("u.threeUnitId", "=", pageForm.getThreeUnitId());
cnd.andEX("u.userState", "=", pageForm.getUserState());
cnd.andEX("u.preparedBy", "=", pageForm.getPreparedBy());
cnd.andEX("u.sex", "=", pageForm.getSex());
@@ -257,6 +257,12 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
Set<String> allowChangeFieldNames = map.getAs("allowChangeFieldNames", Set.class);
Map<String, String> dictMap = map.getAs("dictMap", Map.class);
// 三级单位仅由高级管理页面传入,未传入时不参与比较,避免其他共用页面误判为清空。
if (record.getThreeUnitId() != null) {
allowChangeFieldNames.add("threeUnitId");
dictMap.put("threeUnitId", "三级单位");
}
// 新数据
NutMap newMap = Lang.obj2nutmap(record);
// 原数据
@@ -11,6 +11,7 @@ import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import com.budwk.app.zhgh.welfare.param.WelfareSelectionSituationPageForm;
import com.budwk.app.zhgh.welfare.service.WelfareListService;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
import com.budwk.app.zhgh.welfare.service.WelfareSelectionSituationService;
import com.budwk.app.zhgh.welfare.service.WelfareStatisticsService;
import io.swagger.annotations.Api;
@@ -47,6 +48,8 @@ public class WelfareSelectionSituationController {
private WelfareStatisticsService welfareStatisticsService;
@Inject
private WelfareSelectionSituationService situationService;
@Inject
private WelfareProjectService welfareProjectService;
@At("")
@Ok("beetl:/platform/zhgh/welfare/selectionSituation/index.html")
@@ -86,6 +89,11 @@ public class WelfareSelectionSituationController {
return Result.error("此用户没有选择的权限");
}
String defaultOptionValidationMessage = welfareProjectService.validateSystemDefaultOptionSelection(projectId, selections);
if (defaultOptionValidationMessage != null) {
return Result.error(defaultOptionValidationMessage);
}
//删除上次选择的
dao.clear(WelfareUserSelection.class, Cnd.where(WelfareUserSelection::getWelfareId, "=", projectId).and(WelfareUserSelection::getSelectUserId, "=", userId));
for (WelfareUserSelection welfareUserSelection : selections) {
@@ -89,6 +89,11 @@ public class WelfareUserSelectController {
}
WelfareProject project = dao.fetch(WelfareProject.class, projectId);
String defaultOptionValidationMessage = welfareProjectService.validateSystemDefaultOptionSelection(projectId, selections);
if (defaultOptionValidationMessage != null) {
return Result.error(defaultOptionValidationMessage);
}
int sum = Arrays.stream(selections).mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0)).sum();
if (sum > project.getMultiSelectNum()) {
return Result.error("选择的数量不能超过" + project.getMultiSelectNum());
@@ -37,8 +37,11 @@ public class WelfareFilterUserPageForm extends PageForm {
@ApiModelProperty("工会名称")
private String unionName;
@ApiModelProperty("单位id")
private String[] unitIds;
@ApiModelProperty("所属单位ID")
private String unitId;
@ApiModelProperty("三级单位ID")
private String threeUnitId;
@ApiModelProperty("单位名称")
private String unitName;
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.welfare.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import java.util.List;
@@ -16,6 +17,15 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
*/
WelfareProject projectInfo(String projectId);
/**
* 校验福利选择是否保留系统默认福利。
*
* @param projectId 福利项目 ID,用于查询该项目配置的系统默认福利
* @param selections 用户提交的福利选择数组,每项需包含 selectOptionId 和大于 0 的 selectNum
* @return 校验通过返回 {@code null};未保留系统默认福利时返回错误提示文本
*/
String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections);
/**
* 保存项目信息
* @param project
@@ -471,7 +471,8 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
cnd.andEX("MONTH(u.birthday)", "in", pageForm.getBirthMonths());
}
cnd.andEX("u.unionId", "=", pageForm.getUnionId());
cnd.andEX("u.unitId", "in", pageForm.getUnitIds());
cnd.andEX("u.unitId", "=", pageForm.getUnitId());
cnd.andEX("u.threeUnitId", "=", pageForm.getThreeUnitId());
cnd.andEX("u.personType", "in", pageForm.getPersonTypes());
cnd.andEX("u.userAttribute", "in", pageForm.getUserAttributes());
cnd.andEX("u.preparedBy", "in", pageForm.getPreparedBys());
@@ -524,7 +525,8 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
cnd.andEX("MONTH(birthday)", "in", pageForm.getBirthMonths());
}
cnd.andEX("unionId", "=", pageForm.getUnionId());
cnd.andEX("unitId", "in", pageForm.getUnitIds());
cnd.andEX("unitId", "=", pageForm.getUnitId());
cnd.andEX("threeUnitId", "=", pageForm.getThreeUnitId());
cnd.andEX("personType", "in", pageForm.getPersonTypes());
cnd.andEX("userAttribute", "in", pageForm.getUserAttributes());
cnd.andEX("preparedBy", "in", pageForm.getPreparedBys());
@@ -49,6 +49,30 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
return project;
}
@Override
public String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections) {
List<WelfareProjectSubjectOption> systemDefaultOptions = dao().query(WelfareProjectSubjectOption.class,
Cnd.where("welfareId", "=", projectId).and("isSystemDefault", "=", true));
if (systemDefaultOptions.isEmpty()) {
return null;
}
Set<String> selectedOptionIds = Arrays.stream(selections == null ? new WelfareUserSelection[0] : selections)
.filter(Objects::nonNull)
.filter(selection -> selection.getSelectNum() != null && selection.getSelectNum() > 0)
.map(WelfareUserSelection::getSelectOptionId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
List<String> missingOptionNames = systemDefaultOptions.stream()
.filter(option -> !selectedOptionIds.contains(option.getId()))
.map(WelfareProjectSubjectOption::getOptionName)
.collect(Collectors.toList());
if (missingOptionNames.isEmpty()) {
return null;
}
return "系统默认福利“" + String.join("", missingOptionNames) + "”不可取消";
}
private void fillOptionRank(String projectId, List<WelfareProjectSubjectOption> options) {
if (options == null || options.isEmpty()) {
return;