Merge remote-tracking branch 'origin/main'

This commit is contained in:
=
2026-07-14 16:36:44 +08:00
155 changed files with 3161 additions and 715 deletions
@@ -209,7 +209,7 @@ public class TourLineController {
return Result.error("创建年度不能为空");
}
if (StrUtil.isBlank(line.getTravelAgencyId())) {
return Result.error("旅行社名称不能为空");
return Result.error("服务单位名称不能为空");
}
if (StrUtil.isBlank(line.getLineName())) {
return Result.error("线路名称不能为空");
@@ -466,7 +466,13 @@ public class TourMySignupController {
ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime()));
fillStaffInfo(ledger);
tourLedgerService.updateIgnoreNull(ledger);
// 修改报名也重新锁定并校验当前分工会名额,兼容人员所属分工会发生变化的场景。
String quotaMessage = tourLedgerService.saveWithUnionSignupQuota(
ledger, matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), true);
if (StrUtil.isNotBlank(quotaMessage)) {
return Result.error(quotaMessage);
}
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
if (Lang.isNotEmpty(familyList)) {
@@ -69,6 +69,8 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
// 编辑时同时返回分工会人员数量,确保页面按当前组织和人员总数完整回显。
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id));
return Result.success(tourSetting);
}
@@ -89,15 +91,30 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
// 延用上一年配置时带出原分配数量,前端会清除子表ID后作为新配置提交。
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId()));
return Result.success(tourSetting);
}
/**
* 查询人员分配页签所需的全部分工会、当前人员总数和已保存人员数量。
*
* @param settingId 疗休养配置ID,新增时为空
* @return 分工会人员分配行
*/
@At
@SaCheckPermission("tour.setting")
public Result unionQuotaRows(String settingId) {
return Result.success(tourSettingService.listUnionQuotaRows(settingId));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.setting")
@SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置")
public Result doSubmit(TourSetting tourSetting,
@Param(value = "lots") String lots,
@Param(value = "unionQuotas") String unionQuotas,
@Param(value = "lotDeleteList") String[] lotDeleteList) {
Result checkResult = check(tourSetting);
if (checkResult != null) {
@@ -113,6 +130,14 @@ public class TourSettingController {
return Result.error("同年度下配置名称已存在");
}
// 后端再次校验分配总量,避免绕过页面直接提交超出出行人数指标的数据。
int travelPeopleQuota = tourSetting.getTravelPeopleQuota() == null
? 0 : tourSetting.getTravelPeopleQuota();
String quotaLimitMessage = tourSettingService.checkUnionQuotaLimit(unionQuotas, travelPeopleQuota);
if (StrUtil.isNotBlank(quotaLimitMessage)) {
return Result.error(quotaLimitMessage);
}
// 布尔值给默认值,避免前端未传时出现空状态。
if (tourSetting.getEnabled() == null) {
tourSetting.setEnabled(true);
@@ -148,6 +173,7 @@ public class TourSettingController {
} else {
tourSettingService.insertWith(tourSetting, "lots");
}
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
} else {
// 编辑时先处理页面删除的标段,再保存配置和当前标段行。
if (Lang.isNotEmpty(lotDeleteList)) {
@@ -155,6 +181,7 @@ public class TourSettingController {
}
tourSettingService.updateIgnoreNull(tourSetting);
saveLots(tourSetting);
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
}
return Result.success();
}
@@ -168,6 +195,8 @@ public class TourSettingController {
return Result.error("参数错误");
}
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
// 配置删除时同步清理人员分配,避免遗留不可达的子表数据。
tourSettingService.clearUnionQuotas(id);
tourSettingService.delete(id);
return Result.success();
}
@@ -205,6 +234,9 @@ public class TourSettingController {
if (StrUtil.isBlank(tourSetting.getConfigName())) {
return Result.error("配置名称不能为空");
}
if (tourSetting.getTravelPeopleQuota() == null || tourSetting.getTravelPeopleQuota() < 0) {
return Result.error("出行人数指标必须为非负整数");
}
if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
@@ -699,12 +699,16 @@ public class TourSignupController {
ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed()));
fillStaffInfo(ledger);
// PC和移动端共用本接口;在写入台账前锁定分工会名额,防止并发报名超过人员分配数量。
String quotaMessage = tourLedgerService.saveWithUnionSignupQuota(
ledger, matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), update);
if (StrUtil.isNotBlank(quotaMessage)) {
return Result.error(quotaMessage);
}
if (update) {
tourLedgerService.updateIgnoreNull(ledger);
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
} else {
tourLedgerService.insert(ledger);
}
if (Lang.isNotEmpty(familyList)) {
familyList.forEach(item -> {
@@ -58,13 +58,13 @@ public class TourTravelAgencyController {
return Result.error("参数错误");
}
TourTravelAgency agency = travelAgencyService.fetch(id);
return agency == null ? Result.error("旅行社不存在") : Result.success(agency);
return agency == null ? Result.error("服务单位不存在") : Result.success(agency);
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.travelAgency")
@SLog(type = "tour", tag = "旅行社管理", msg = "保存旅行社信息")
@SLog(type = "tour", tag = "服务单位管理", msg = "保存服务单位信息")
public Result doSubmit(TourTravelAgency agency) {
Result checkResult = check(agency);
if (checkResult != null) {
@@ -77,7 +77,7 @@ public class TourTravelAgencyController {
sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId());
}
if (travelAgencyService.count(sameCodeCnd) > 0) {
return Result.error("同年度下旅行社编号已存在");
return Result.error("同年度下服务单位编号已存在");
}
if (agency.getEnabled() == null) {
agency.setEnabled(true);
@@ -93,7 +93,7 @@ public class TourTravelAgencyController {
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.travelAgency")
@SLog(type = "tour", tag = "旅行社管理", msg = "删除旅行社信息")
@SLog(type = "tour", tag = "服务单位管理", msg = "删除服务单位信息")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
@@ -110,10 +110,10 @@ public class TourTravelAgencyController {
return Result.error("年度不能为空");
}
if (StrUtil.isBlank(agency.getAgencyName())) {
return Result.error("旅行社名称不能为空");
return Result.error("服务单位名称不能为空");
}
if (StrUtil.isBlank(agency.getAgencyCode())) {
return Result.error("旅行社编号不能为空");
return Result.error("服务单位编号不能为空");
}
if (StrUtil.isBlank(agency.getContactName())) {
return Result.error("联系人不能为空");
@@ -102,12 +102,12 @@ public class TourLedger extends BaseModel implements Serializable {
private String hotelName;
@Column
@Comment("旅行社ID")
@Comment("服务单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@Column
@Comment("旅行社")
@Comment("服务单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@@ -42,7 +42,7 @@ public class TourLine extends BaseModel implements Serializable {
private String lineName;
@Column
@Comment("旅行社ID")
@Comment("服务单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@@ -43,6 +43,12 @@ public class TourSetting extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50)
private String tourType;
@Column
@Comment("出行人数指标")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer travelPeopleQuota;
@Column
@Comment("可参加人员范围ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -122,11 +128,16 @@ public class TourSetting extends BaseModel implements Serializable {
private Boolean enabled;
/**
* 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。
* 标段管理沿用老疗休养配置的子表设计,后续线路、服务单位等模块可通过标段ID继续关联。
*/
@Many(field = "settingId")
private List<TourSettingLot> lots;
/**
* 分工会人员分配,仅用于配置弹窗回显与提交,不作为 tour_setting 表字段保存。
*/
private List<TourSettingUnionQuota> unionQuotas;
@Column
@Comment("服务须知")
@ColDefine(type = ColType.TEXT)
@@ -0,0 +1,60 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养配置下的分工会人员数量分配。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_setting_union_quota")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养配置分工会人员分配")
public class TourSettingUnionQuota extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("分工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("人员数量")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer peopleQuota;
/**
* 当前分工会人员总数,仅用于页面分配参考,不写入数据库。
*/
private Integer personCount;
}
@@ -9,14 +9,14 @@ import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 旅行社管理。
* 先维护疗休养线路创建会复用的旅行社基础信息,后续线路模块可通过旅行社ID关联。
* 服务单位管理。
* 先维护疗休养线路创建会复用的服务单位基础信息,后续线路模块可通过服务单位ID关联。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_travel_agency")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养旅行社")
@Comment("普惠疗休养服务单位")
public class TourTravelAgency extends BaseModel implements Serializable {
@Column
@@ -32,12 +32,12 @@ public class TourTravelAgency extends BaseModel implements Serializable {
private Integer year;
@Column
@Comment("旅行社编号")
@Comment("服务单位编号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String agencyCode;
@Column
@Comment("旅行社名称")
@Comment("服务单位名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String agencyName;
@@ -3,5 +3,24 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
/**
* 疗休养报名台账服务,统一处理报名记录及并发名额校验。
*/
public interface TourLedgerService extends BaseService<TourLedger> {
/**
* 在同一事务中锁定分工会名额、校验并发容量并写入报名台账。
*
* @param ledger 待新增或修改的报名台账
* @param settingId 疗休养配置ID
* @param matterId 当前报名事项ID
* @param userId 当前登录用户ID
* @param update true 表示修改已有报名,false 表示新增报名
* @return 保存成功返回空字符串,否则返回业务提示
*/
String saveWithUnionSignupQuota(TourLedger ledger,
String settingId,
String matterId,
String userId,
boolean update);
}
@@ -2,6 +2,44 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import java.util.List;
/**
* 疗休养配置服务,统一处理配置及分工会人员数量分配业务。
*/
public interface TourSettingService extends BaseService<TourSetting> {
/**
* 查询全部分工会在指定配置下的人员数量,并实时补充分工会人员总数。
*
* @param settingId 疗休养配置ID,新增配置时允许为空
* @return 按分工会编码排序的人员分配行
*/
List<TourSettingUnionQuota> listUnionQuotaRows(String settingId);
/**
* 保存指定配置的分工会人员数量,提交内容为 JSON 数组字符串。
*
* @param settingId 疗休养配置ID
* @param unionQuotas 分工会人员分配 JSON
*/
void saveUnionQuotas(String settingId, String unionQuotas);
/**
* 校验各分工会人员数量是否均为非负整数且合计不超过出行人数指标。
*
* @param unionQuotas 分工会人员分配 JSON
* @param travelPeopleQuota 出行人数指标
* @return 校验通过返回空字符串,否则返回业务提示
*/
String checkUnionQuotaLimit(String unionQuotas, int travelPeopleQuota);
/**
* 清除指定配置下的全部分工会人员分配。
*
* @param settingId 疗休养配置ID
*/
void clearUnionQuotas(String settingId);
}
@@ -1,15 +1,136 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import org.nutz.aop.interceptor.ioc.TransAop;
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.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
/**
* 疗休养报名台账服务实现。
*/
@IocBean(args = {"refer:dao"})
public class TourLedgerServiceImpl extends BaseServiceImpl<TourLedger> implements TourLedgerService {
public TourLedgerServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public String saveWithUnionSignupQuota(TourLedger ledger,
String settingId,
String matterId,
String userId,
boolean update) {
if (ledger == null) {
return "报名数据不能为空";
}
String quotaMessage = lockAndCheckUnionSignupQuota(
settingId, matterId, userId, update ? ledger.getId() : null);
if (StrUtil.isNotBlank(quotaMessage)) {
return quotaMessage;
}
// 名额校验通过后立即在同一事务内写入台账,事务提交前始终持有分工会名额行锁。
if (update) {
updateIgnoreNull(ledger);
} else {
insert(ledger);
}
return "";
}
/**
* 锁定当前配置和登录人所属分工会的名额行,并校验报名后是否超过人员数量。
*/
private String lockAndCheckUnionSignupQuota(String settingId,
String matterId,
String userId,
String excludeLedgerId) {
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(matterId)) {
return "疗休养配置或报名事项不存在";
}
// 分工会信息从用户视图读取,不接受报名表单传入的工会ID,防止跨工会占用名额。
View_user user = dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", userId));
if (user == null || StrUtil.isBlank(user.getUnionId()) || StrUtil.isBlank(user.getLoginname())) {
return "未查询到您所属的分工会,不能报名";
}
// FOR UPDATE 串行化同一配置、同一分工会的抢名额请求,不同分工会之间互不阻塞。
Sql quotaSql = Sqls.create("""
SELECT peopleQuota
FROM tour_setting_union_quota
WHERE settingId = @settingId
AND unionId = @unionId
AND delFlag = 0
LIMIT 1
FOR UPDATE
""");
quotaSql.setParam("settingId", settingId);
quotaSql.setParam("unionId", user.getUnionId());
quotaSql.setCallback(Sqls.callback.map());
dao().execute(quotaSql);
NutMap quotaRow = quotaSql.getObject(NutMap.class);
if (quotaRow == null || quotaRow.isEmpty()) {
return "您所在分工会未分配报名名额";
}
int peopleQuota = quotaRow.getInt("peopleQuota", 0);
if (peopleQuota <= 0) {
return "您所在分工会的报名名额已满";
}
// 取得名额锁后再次检查同事项重复报名,关闭双击或并发请求绕过前置查询的时间窗口。
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId";
Sql duplicateSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger
WHERE delFlag = 0
AND matterId = @matterId
AND jobNo = @jobNo
""" + excludeSql);
duplicateSql.setParam("matterId", matterId);
duplicateSql.setParam("jobNo", user.getLoginname());
if (StrUtil.isNotBlank(excludeLedgerId)) {
duplicateSql.setParam("excludeLedgerId", excludeLedgerId);
}
duplicateSql.setCallback(Sqls.callback.integer());
dao().execute(duplicateSql);
if (duplicateSql.getInt() > 0) {
return "您已报名当前出行时段,请勿重复提交";
}
// 名额按配置统计,覆盖该配置下的全部事项;待审核报名也占用名额,取消后因台账删除自动释放。
String countExcludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND ledger.id <> @excludeLedgerId";
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger ledger
INNER JOIN tour_matter matter
ON matter.id = ledger.matterId
AND matter.delFlag = 0
WHERE ledger.delFlag = 0
AND matter.settingId = @settingId
AND ledger.unionId = @unionId
""" + countExcludeSql);
countSql.setParam("settingId", settingId);
countSql.setParam("unionId", user.getUnionId());
if (StrUtil.isNotBlank(excludeLedgerId)) {
countSql.setParam("excludeLedgerId", excludeLedgerId);
}
countSql.setCallback(Sqls.callback.integer());
dao().execute(countSql);
int signupCount = countSql.getInt();
if (signupCount + 1 > peopleQuota) {
return "您所在分工会的报名名额已满(已报名 " + signupCount
+ " 人,分配 " + peopleQuota + " 人)";
}
return "";
}
}
@@ -1,15 +1,172 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
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.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 疗休养配置服务实现,负责分工会人员分配的查询、校验和持久化。
*/
@IocBean(args = {"refer:dao"})
public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> implements TourSettingService {
public TourSettingServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<TourSettingUnionQuota> listUnionQuotaRows(String settingId) {
// 一次查询分工会、已保存数量和实时人员总数,避免逐个工会统计产生 N+1 查询。
Sql sql = Sqls.create("""
SELECT
quota.id AS id,
@settingId AS settingId,
un.id AS unionId,
un.name AS unionName,
COALESCE(quota.peopleQuota, 0) AS peopleQuota,
COALESCE(user_count.personCount, 0) AS personCount
FROM sys_union un
LEFT JOIN tour_setting_union_quota quota
ON quota.unionId = un.id
AND quota.settingId = @settingId
AND COALESCE(quota.delFlag, 0) = 0
LEFT JOIN (
SELECT unionId, COUNT(1) AS personCount
FROM vw_user
GROUP BY unionId
) user_count ON user_count.unionId = un.id
WHERE COALESCE(un.delFlag, 0) = 0
ORDER BY un.unionCode ASC
""");
sql.setParam("settingId", StrUtil.blankToDefault(settingId, ""));
List<NutMap> rows = listMap(sql);
return rows.stream().map(row -> {
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setId(row.getString("id"));
quota.setSettingId(row.getString("settingId"));
quota.setUnionId(row.getString("unionId"));
quota.setUnionName(row.getString("unionName"));
quota.setPeopleQuota(toNonNegativeInteger(row.getInt("peopleQuota")));
quota.setPersonCount(toNonNegativeInteger(row.getInt("personCount")));
return quota;
}).collect(Collectors.toList());
}
@Override
public void saveUnionQuotas(String settingId, String unionQuotas) {
if (StrUtil.isBlank(settingId)) {
return;
}
clearUnionQuotas(settingId);
if (StrUtil.isBlank(unionQuotas)) {
return;
}
List<TourSettingUnionQuota> quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
if (Lang.isEmpty(quotaList)) {
return;
}
// 工会名称以当前组织表为准,防止前端篡改或历史名称继续写入。
Map<String, Sys_union> unionMap = dao().query(Sys_union.class,
Cnd.where(Sys_union::getDelFlag, "=", false)).stream()
.collect(Collectors.toMap(Sys_union::getId, item -> item, (first, second) -> first));
List<TourSettingUnionQuota> saveList = quotaList.stream()
.filter(item -> item != null
&& StrUtil.isNotBlank(item.getUnionId())
&& unionMap.containsKey(item.getUnionId()))
.map(item -> normalizeUnionQuota(settingId, item, unionMap))
.filter(item -> item.getPeopleQuota() > 0)
.collect(Collectors.toList());
if (Lang.isNotEmpty(saveList)) {
dao().insert(saveList);
}
}
@Override
public String checkUnionQuotaLimit(String unionQuotas, int travelPeopleQuota) {
if (StrUtil.isBlank(unionQuotas)) {
return "";
}
final List<TourSettingUnionQuota> quotaList;
try {
quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
} catch (Exception e) {
return "人员分配数据格式不正确";
}
if (Lang.isEmpty(quotaList)) {
return "";
}
long total = 0L;
Set<String> unionIds = new HashSet<>();
Set<String> validUnionIds = dao().query(Sys_union.class,
Cnd.where(Sys_union::getDelFlag, "=", false)).stream()
.map(Sys_union::getId)
.collect(Collectors.toSet());
for (int index = 0; index < quotaList.size(); index++) {
TourSettingUnionQuota quota = quotaList.get(index);
if (quota == null || quota.getPeopleQuota() == null) {
continue;
}
// 工会ID必须来自当前组织表且不能重复,防止伪造行绕过总量和组织范围校验。
if (StrUtil.isBlank(quota.getUnionId())
|| !validUnionIds.contains(quota.getUnionId())
|| !unionIds.add(quota.getUnionId())) {
return "" + (index + 1) + "行分工会信息无效或重复";
}
if (quota.getPeopleQuota() < 0) {
return "" + (index + 1) + "行人员数量必须为非负整数";
}
total += quota.getPeopleQuota();
}
if (total > travelPeopleQuota) {
return "当前人员数量合计 " + total + ",不能超过出行人数指标 " + travelPeopleQuota;
}
return "";
}
@Override
public void clearUnionQuotas(String settingId) {
if (StrUtil.isNotBlank(settingId)) {
dao().clear(TourSettingUnionQuota.class,
Cnd.where(TourSettingUnionQuota::getSettingId, "=", settingId));
}
}
/**
* 将前端分配行转换为可信的持久化对象,只接受当前组织表中的工会名称。
*/
private TourSettingUnionQuota normalizeUnionQuota(String settingId,
TourSettingUnionQuota item,
Map<String, Sys_union> unionMap) {
Sys_union union = unionMap.get(item.getUnionId());
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setSettingId(settingId);
quota.setUnionId(item.getUnionId());
quota.setUnionName(union.getName());
quota.setPeopleQuota(toNonNegativeInteger(item.getPeopleQuota()));
return quota;
}
/**
* 将空值或负数统一转换为零,保证页面统计和数据库保存使用相同口径。
*/
private Integer toNonNegativeInteger(Integer value) {
return value == null || value < 0 ? 0 : value;
}
}
@@ -8,6 +8,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalType;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +34,8 @@ public class ProposalConfigTypeController {
private Dao dao;
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/config/type/index.html")
@@ -47,6 +50,8 @@ public class ProposalConfigTypeController {
Sql sql = Sqls.create("select * from proposal_type $condition");
Cnd cnd = Cnd.NEW();
cnd.and(Cnd.likeEX("name", name));
// 提案类型列表只允许名称和编码参与排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "configType");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageVO(pageForm, sql, ProposalType.class);
return Result.success(pagination);
@@ -19,6 +19,7 @@ import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
@@ -54,6 +55,8 @@ public class ProposalConfigUnitController {
private BaseService baseService;
@Inject
private SysRoleService sysRoleService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/config/unit/index.html")
@@ -99,7 +102,10 @@ public class ProposalConfigUnitController {
cnd.where().orLike("u2.loginname", pageForm.getSearchKeyword());
cnd.where().orLike("t1.name", pageForm.getSearchKeyword());
}
cnd.asc("t1.code");
// 承办单位列表未指定有效排序时继续按单位编码升序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "configUnit")) {
cnd.asc("t1.code");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -28,6 +28,7 @@ import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@IocBean
@At("/platform/proposal/dashboard")
@@ -35,6 +36,23 @@ import java.util.List;
@Ok("json:full")
public class ProposalDashboardController {
/**
* 工作台表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> DASHBOARD_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("isConsolidation", "info.isConsolidation"),
Map.entry("masterUnitName", "masterUnitName"),
Map.entry("slaveUnitNames", "slaveUnitNames"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private ProposalCommonService proposalCommonService;
@Inject
@@ -178,6 +196,8 @@ public class ProposalDashboardController {
}
}
// 根据工作台字段白名单追加排序,未选择排序时保持原有查询顺序。
proposalCommonService.applySafePageOrder(cnd, pageForm, DASHBOARD_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -7,6 +7,7 @@ import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalExportService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
@@ -29,6 +30,8 @@ public class ProposalExportComprehensiveController {
@Inject
private ProposalExportService proposalExportService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/export/comprehensive/index.html")
@@ -73,6 +76,8 @@ public class ProposalExportComprehensiveController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 综合导出主列表的聚合字段仅通过固定查询别名排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "exportComprehensive");
sql.setCondition(cnd);
Pagination pagination = proposalExportService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -17,6 +17,7 @@ import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -63,6 +64,8 @@ public class ProposalExportSingleCustomController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private FlowEngine flowEngine;
@@ -100,7 +103,10 @@ public class ProposalExportSingleCustomController {
Cnd cnd = Cnd.NEW();
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
cnd.desc("info.createdAt");
// 自定义导出列表未指定有效排序时保留提案创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "exportSingleCustom")) {
cnd.desc("info.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -6,11 +6,13 @@ import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -31,6 +33,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean
@@ -40,10 +43,26 @@ import java.util.stream.Collectors;
@Api(tags = "提案统计分析")
public class ProposalQueryAnalysisController {
/**
* 统计数据表允许排序的字段白名单。
*/
private static final Set<String> STATISTICS_ORDER_COLUMNS = Set.of(
"dimension", "itemName", "count", "rate"
);
/**
* 分析数据表允许排序的字段白名单。
*/
private static final Set<String> ANALYSIS_ORDER_COLUMNS = Set.of(
"dimension", "total", "categoryCount", "topItem", "topCount", "topRate", "analysis"
);
@Inject
private BaseService baseService;
@Inject
private SysDictService sysDictService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/analysis/index.html")
@@ -51,36 +70,54 @@ public class ProposalQueryAnalysisController {
public void index() {
}
/**
* 查询提案统计数据。
*
* @param sessionId 教代会届次ID
* @param dimension 统计维度,为空时返回全部维度
* @param statisticsOrderName 统计表排序字段
* @param statisticsOrderBy 统计表排序方向
* @return 统计行列表,包含维度、分类项、数量和占比
*/
@At
@ApiOperation("统计数据")
@SaCheckPermission("proposal.query.analysis")
public Result statisticsData(String sessionId, String dimension) {
/*
* 查询参数说明:
* sessionId:教代会届次ID,用于限定统计提案范围;
* dimension:统计维度,可传“提案人单位、提案类型、代表团、立案结果、满意度”,为空时返回全部维度。
* 返回值:Result.data 为统计行列表,包含 dimension、itemName、count、rate。
*/
public Result statisticsData(String sessionId, String dimension, String statisticsOrderName, String statisticsOrderBy) {
if (StrUtil.isBlank(sessionId)) {
return Result.error("请先选择教代会届次");
}
return Result.success(buildStatisticsRows(sessionId, dimension));
List<NutMap> rows = buildStatisticsRows(sessionId, dimension);
PageForm orderForm = new PageForm();
orderForm.setPageOrderName(statisticsOrderName);
orderForm.setPageOrderBy(statisticsOrderBy);
// 统计数据生成后按独立白名单执行后端排序。
proposalCommonService.sortStatisticsRows(rows, orderForm, STATISTICS_ORDER_COLUMNS);
return Result.success(rows);
}
/**
* 查询提案分析数据。
*
* @param sessionId 教代会届次ID
* @param dimension 统计维度,为空时返回全部维度
* @param analysisOrderName 分析表排序字段
* @param analysisOrderBy 分析表排序方向
* @return 分析行列表,包含总量、最高项、最高占比和分析结论
*/
@At
@ApiOperation("分析数据")
@SaCheckPermission("proposal.query.analysis")
public Result analysisData(String sessionId, String dimension) {
/*
* 查询参数说明:
* sessionId:教代会届次ID,用于限定分析提案范围;
* dimension:统计维度,可传“提案人单位、提案类型、代表团、立案结果、满意度”,为空时返回全部维度。
* 返回值:Result.data 为分析行列表,包含 dimension、total、categoryCount、topItem、topCount、topRate、analysis。
*/
public Result analysisData(String sessionId, String dimension, String analysisOrderName, String analysisOrderBy) {
if (StrUtil.isBlank(sessionId)) {
return Result.error("请先选择教代会届次");
}
return Result.success(buildAnalysisRows(sessionId, dimension));
List<NutMap> rows = buildAnalysisRows(sessionId, dimension);
PageForm orderForm = new PageForm();
orderForm.setPageOrderName(analysisOrderName);
orderForm.setPageOrderBy(analysisOrderBy);
// 分析数据生成后按独立白名单执行后端排序。
proposalCommonService.sortStatisticsRows(rows, orderForm, ANALYSIS_ORDER_COLUMNS);
return Result.success(rows);
}
@At
@@ -94,9 +131,9 @@ public class ProposalQueryAnalysisController {
entities.add(new ExcelExportEntity("分类项", "itemName", 30));
entities.add(new ExcelExportEntity("数量", "count", 12));
entities.add(new ExcelExportEntity("占比", "rate", 12));
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, entities, list);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
CommonDownloadUtil.download("提案统计数据.xlsx", workbook, response);
}
@@ -114,9 +151,9 @@ public class ProposalQueryAnalysisController {
entities.add(new ExcelExportEntity("最高数量", "topCount", 12));
entities.add(new ExcelExportEntity("最高占比", "topRate", 12));
entities.add(new ExcelExportEntity("分析结论", "analysis", 60));
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, entities, list);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
CommonDownloadUtil.download("提案分析数据.xlsx", workbook, response);
}
@@ -27,6 +27,7 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@IocBean
@@ -36,6 +37,20 @@ import java.util.List;
@Api(tags = "征集进度查询")
public class ProposalQueryCollectProgressController {
/**
* 征集进度表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("delegationName", "delegationName"),
Map.entry("inviteCount", "inviteCount"),
Map.entry("finishCount", "finishCount"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
static List<NutMap> states = new ArrayList<>() {{
add(NutMap.NEW().addv("code", 10).addv("id", 10).addv("name", "撰写提案"));
add(NutMap.NEW().addv("code", 20).addv("id", 20).addv("name", "附议提案"));
@@ -83,8 +98,9 @@ public class ProposalQueryCollectProgressController {
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
$condition
""");
sql.setParam("sessionId", pageForm.getSessionId());
Cnd cnd = Cnd.NEW();
// 征集进度必须按页面选中的教代会过滤,避免共享参数未处理sessionId导致跨届次查询。
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
SqlExpressionGroup seg = new SqlExpressionGroup();
@@ -106,6 +122,8 @@ public class ProposalQueryCollectProgressController {
cnd.and(seg);
}
cnd.groupBy("info.id");
// 征集进度包含关联字段和人数统计别名,只允许白名单字段进入排序条件。
proposalCommonService.applySafePageOrder(cnd, pageForm, COLLECT_PROGRESS_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
List<NutMap> list = (List<NutMap>) pagination.getList();
@@ -25,6 +25,7 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/comprehensive")
@@ -33,6 +34,23 @@ import java.util.Arrays;
@Api(tags = "提案综合查询")
public class ProposalQueryComprehensiveController {
/**
* 综合查询表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> COMPREHENSIVE_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("caseFilingType", "info.caseFilingType"),
Map.entry("merge", "merge"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private BaseService baseService;
@Inject
@@ -62,7 +80,8 @@ public class ProposalQueryComprehensiveController {
ins.processDefineId instanceProcessDefineId,
t.displayName curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
@@ -120,6 +139,8 @@ public class ProposalQueryComprehensiveController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 综合查询包含多表字段及聚合别名,只允许白名单字段进入排序条件。
proposalCommonService.applySafePageOrder(cnd, pageForm, COMPREHENSIVE_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -1,13 +1,16 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -19,8 +22,10 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/delegation")
@@ -29,9 +34,34 @@ import java.util.stream.Collectors;
@Api(tags = "代表团提案统计")
public class ProposalQueryDelegationController {
/**
* 代表团汇总表固定列的安全排序映射,动态立案结果列在查询时追加。
*/
private static final Map<String, String> DELEGATION_SUMMARY_ORDER_COLUMNS = Map.of(
"dbtName", "dbt.name",
"TOTAL", "TOTAL",
"caseRate", "caseRateOrder"
);
/**
* 代表团提案明细表允许排序的字段映射。
*/
private static final Map<String, String> DELEGATION_PROPOSAL_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("sessionName", "sessionName"),
Map.entry("delegationName", "delegationName"),
Map.entry("curTaskName", "curTaskName")
);
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private SysDictService sysDictService;
@@ -46,28 +76,45 @@ public class ProposalQueryDelegationController {
@At
@ApiOperation("代表团提案统计")
@SaCheckPermission("proposal.query.delegation")
public Result pageData(String sessionId) {
public Result pageData(PageForm pageForm, String sessionId) {
List<Sys_dict> dictList = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT");
Sql sql = Sqls.create("""
SELECT
dbt.id,
dbt.`name` AS dbtName,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId ) as TOTAL,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.isSubmit = 1 ) as SUBMIT_COUNT,
$resultSql
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.isSubmit = 1 ) as SUBMIT_COUNT
$resultSql,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = @confirmFiling)
/ NULLIF((select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId), 0) AS caseRateOrder
FROM
teacher_congress_delegation dbt
$condition
""");
String resultStr = dictList.stream().map(v -> {
return "( select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult= '%s' ) as `%s`".formatted(v.getCode(), v.getCode());
}).collect(Collectors.joining(","));
sql.setVar("resultSql", resultStr);
Map<String, String> summaryOrderColumns = new HashMap<>(DELEGATION_SUMMARY_ORDER_COLUMNS);
List<String> resultSqlParts = new ArrayList<>();
int resultIndex = 0;
for (Sys_dict dict : dictList) {
String resultCode = dict.getCode();
// 动态别名仅允许字母、数字和下划线,查询值使用参数绑定,避免字典内容进入SQL结构。
if (StrUtil.isBlank(resultCode) || !resultCode.matches("[A-Za-z0-9_]+")) {
continue;
}
String paramName = "caseFilingResult" + resultIndex++;
resultSqlParts.add(", (select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = @" + paramName + ") AS `" + resultCode + "`");
sql.setParam(paramName, resultCode);
summaryOrderColumns.put(resultCode, "`" + resultCode + "`");
}
sql.setVar("resultSql", String.join("", resultSqlParts));
sql.setParam("confirmFiling", "CONFIRM_FILING");
Cnd cnd = Cnd.NEW();
cnd.andEX("dbt.sessionId", "=", sessionId);
if (!AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) {
}
cnd.asc("dbt.`code`");
// 未选择排序时保留原有代表团编码升序,选择后仅应用白名单字段。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, summaryOrderColumns)) {
cnd.asc("dbt.`code`");
}
sql.setCondition(cnd);
return Result.success(baseService.listMap(sql));
}
@@ -101,7 +148,18 @@ public class ProposalQueryDelegationController {
$condition
""");
Cnd cnd = Cnd.NEW();
ProposalSearchParam.buildSearch(cnd, pageForm);
// 先应用明细字段白名单,再隔离公共搜索中的原始排序处理,阻止任意字段进入SQL。
proposalCommonService.applySafePageOrder(cnd, pageForm, DELEGATION_PROPOSAL_ORDER_COLUMNS);
String requestedOrderName = pageForm.getPageOrderName();
String requestedOrderBy = pageForm.getPageOrderBy();
pageForm.setPageOrderName(null);
pageForm.setPageOrderBy(null);
try {
ProposalSearchParam.buildSearch(cnd, pageForm);
} finally {
pageForm.setPageOrderName(requestedOrderName);
pageForm.setPageOrderBy(requestedOrderBy);
}
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -5,6 +5,7 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -18,6 +19,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/history")
@@ -26,8 +28,27 @@ import javax.validation.Valid;
@Api(tags = "提案管理系统-查询统计-历史提案查询")
public class ProposalQueryHistoryController {
/**
* 历史提案表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> HISTORY_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("sessionName", "sessionName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("caseFilingType", "info.caseFilingType"),
Map.entry("merge", "merge"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/history/index.html")
@@ -53,7 +74,8 @@ public class ProposalQueryHistoryController {
ins.processDefineId instanceProcessDefineId,
t.displayName curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
@@ -69,6 +91,8 @@ public class ProposalQueryHistoryController {
Cnd cnd = Cnd.NEW();
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 历史查询包含多表字段及聚合别名,只允许白名单字段进入排序条件。
proposalCommonService.applySafePageOrder(cnd, pageForm, HISTORY_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask;
@@ -31,6 +32,15 @@ import java.util.stream.Collectors;
@Slf4j
@Ok("json:full")
public class ProposalQueryUnitReplyController {
/**
* 承办单位办理统计允许排序的字段白名单。
*/
private static final Set<String> UNIT_REPLY_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "masterReplySum", "masterNoReplySum",
"slaveSum", "slaveReplySum", "slaveNoReplySum"
);
@Inject
private Dao dao;
@Inject
@@ -45,9 +55,10 @@ public class ProposalQueryUnitReplyController {
@At
@SaCheckPermission("proposal.query.unitReply")
public Result data(String sessionId, String undertakeUnitId, String caseFilingResult) {
public Result data(PageForm pageForm, String sessionId, String undertakeUnitId, String caseFilingResult) {
/*
* 查询参数说明:
* pageForm:表格排序字段和排序方向;
* sessionId:教代会届次ID,用于限定统计的提案范围;
* undertakeUnitId:承办单位ID,用于只统计某个承办单位,为空时统计全部承办单位;
* caseFilingResult:立案结果字典值,对应 proposal_info.caseFilingResult,为空时不限制立案结果。
@@ -139,6 +150,8 @@ public class ProposalQueryUnitReplyController {
tableData.add(tableRow);
});
// 统计结果生成后在服务层按白名单字段排序,避免前端字段参与任意业务处理。
proposalCommonService.sortStatisticsRows(tableData, pageForm, UNIT_REPLY_ORDER_COLUMNS);
return Result.success().addData(Map.of("tableData", tableData, "slaveNeedReply", slaveNeedReply));
}
}
@@ -21,6 +21,7 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/yearReport")
@@ -29,6 +30,22 @@ import javax.validation.Valid;
@Api(tags = "提案年度报告")
public class ProposalQueryYearReportController {
/**
* 年度报告表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> YEAR_REPORT_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("isConsolidation", "info.isConsolidation"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private ProposalCommonService proposalCommonService;
@@ -56,7 +73,8 @@ public class ProposalQueryYearReportController {
ins.processDefineId instanceProcessDefineId,
t.displayName curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
@@ -70,8 +88,12 @@ public class ProposalQueryYearReportController {
$condition
""");
Cnd cnd = Cnd.NEW();
// 年度报告必须按页面选中的教代会过滤,避免共享参数未处理sessionId导致跨届次查询。
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 年度报告包含多表字段及聚合别名,只允许白名单字段进入排序条件。
proposalCommonService.applySafePageOrder(cnd, pageForm, YEAR_REPORT_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql);
return Result.success(pagination);
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance;
@@ -10,6 +12,7 @@ import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
@@ -30,10 +33,19 @@ import java.util.stream.Collectors;
@Api(tags = "提案承办单位满意度")
public class ProposalUnderTakeSatisfactionController {
/**
* 满意度统计固定列的排序白名单,动态满意度列在查询时追加。
*/
private static final Set<String> SATISFACTION_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "slaveSum"
);
@Inject
private Dao dao;
@Inject
private SysDictService sysDictService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/underTakeSatisfaction/index.html")
@@ -42,10 +54,24 @@ public class ProposalUnderTakeSatisfactionController {
}
/**
* 查询指定教代会和承办单位的满意度统计结果。
*
* @param pageForm 表格排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID
* @return 满意度统计结果
*/
@At
@SaCheckPermission("proposal.query.underTake.satisfaction")
public Result data(String sessionId, String undertakeUnitId) {
public Result data(PageForm pageForm, String sessionId, String undertakeUnitId) {
List<Sys_dict> feedbackCodes = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK");
Set<String> orderColumns = new HashSet<>(SATISFACTION_ORDER_COLUMNS);
// 动态满意度列仅从后端字典加入白名单,不接受前端自行扩展字段。
feedbackCodes.stream()
.map(Sys_dict::getCode)
.filter(StrUtil::isNotBlank)
.forEach(orderColumns::add);
// 当前届次所有的提案ID
Sql sql = Sqls.create("select id from proposal_info where sessionId = @sessionId").setParam("sessionId", sessionId);
@@ -109,6 +135,8 @@ public class ProposalUnderTakeSatisfactionController {
tableData.add(tableRow);
});
// 汇总完成后在服务层执行白名单排序,数字列按数值顺序处理。
proposalCommonService.sortStatisticsRows(tableData, pageForm, orderColumns);
return Result.success(tableData);
}
@@ -111,7 +111,10 @@ public class ProposalExpeditingController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 提案催办列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "expediting")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -78,6 +78,8 @@ public class ProposalSeniorBasicController {
Cnd cnd = Cnd.NEW();
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 内容修改列表仅允许页面展示字段参与排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorBasic");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -78,6 +78,8 @@ public class ProposalSeniorDeleteController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 高级删除列表仅允许页面展示字段参与排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorDelete");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -93,6 +93,8 @@ public class ProposalSeniorFeedBackController {
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
// cnd.and("latestFeedBack.id","is not",null);
cnd.groupBy("info.id");
// 反馈评分修改列表包含反馈计算列,使用页面独立白名单排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorFeedback");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -82,6 +82,8 @@ public class ProposalSeniorTypeController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 类型修改列表仅允许页面展示字段参与排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorType");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -5,23 +5,23 @@ import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.validation.Valid;
import java.util.List;
/**
* @version 1.0
@@ -39,6 +39,8 @@ public class ProposalAllFinishedController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/allFinished/index.html")
@@ -62,7 +64,9 @@ public class ProposalAllFinishedController {
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId
FROM wf_process_instance ins
LEFT JOIN proposal_info info ON info.id = ins.businessNo
/* Limit all-finished data to proposal workflow instances and discard other module instances. */
INNER JOIN wf_process_define def ON def.id = ins.processDefineId AND def.name = 'JDHTA_NC'
INNER JOIN proposal_info info ON info.id = ins.businessNo
LEFT JOIN proposal_type type on type.id = info.typeId
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
@@ -72,9 +76,12 @@ public class ProposalAllFinishedController {
cnd.and("ins.state", "=", "20");
if(!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())){
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
// Non-admin users can only see proposal flows where they participated in at least one task.
cnd.and(new Static("EXISTS (SELECT 1 FROM wf_process_task wt INNER JOIN wf_process_task_actor wta ON wta.processTaskId = wt.id WHERE wt.processInstanceId = ins.id AND wta.actorId = '" + SecurityUtil.getUserId() + "')"));
}
ProposalSearchParam.buildSearch(cnd, pageForm);
// 已办结列表仅允许页面展示字段参与排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "allFinished");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -10,6 +10,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCaseCheckService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -42,6 +43,8 @@ public class ProposalCaseCheckController {
private BaseService baseService;
@Inject
private ProposalCaseCheckService proposalCaseCheckService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/caseCheck/index.html")
@@ -104,7 +107,10 @@ public class ProposalCaseCheckController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 委员会审查列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "caseCheck")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -85,7 +85,10 @@ public class ProposalCommissionerController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.and("pco.id", approval ? "is not" : "is", null);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 委员查询包含统计列,未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "commissioner")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -10,6 +10,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -40,6 +41,8 @@ public class ProposalCommitteeFilingController {
private Dao dao;
@Inject
private ProposalCommitteeFilingService proposalCommitteeFilingService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/committeeFiling/index.html")
@@ -102,7 +105,10 @@ public class ProposalCommitteeFilingController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 立案审核列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "committeeFiling")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommitteeFilingService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -118,7 +118,10 @@ public class ProposalCommitteeFilingUnitController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 确认承办单位列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "committeeFilingUnit")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommitteeFilingUnitService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -10,6 +10,7 @@ import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +34,8 @@ public class ProposalControlController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/control/index.html")
@@ -84,7 +87,10 @@ public class ProposalControlController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 状态调整列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "control")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -118,7 +118,10 @@ public class ProposalDelegationController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 团长审核列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "delegation")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -107,7 +107,10 @@ public class ProposalFeedbackEvaluationController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 反馈评分列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "feedbackEvaluation")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -6,7 +6,6 @@ import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -99,10 +98,9 @@ public class ProposalInviteController {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
// 页面排序仅使用邀请列表白名单,未指定有效字段时保留创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "invite")) {
cnd.desc("info.createdAt");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
@@ -62,6 +62,28 @@ import java.util.Objects;
@Ok("json:full")
@Api(tags = "提案管理-我的提案")
public class ProposalMineController {
/** 我的提案列表允许排序的页面字段与 SQL 字段映射。 */
private static final Map<String, String> MINE_ORDER_COLUMNS = Map.of(
"code", "info.code",
"name", "info.name",
"createTime", "info.createTime",
"typeName", "type.name",
"sourceName", "sd.name",
"sessionName", "tcs.fullName",
"taskName", "t.displayName",
"instanceState", "ins.state"
);
/** 邀请附议人列表允许排序的页面字段与 SQL 字段映射。 */
private static final Map<String, String> SECONDER_ORDER_COLUMNS = Map.of(
"loginName", "t1.loginName",
"userName", "t1.userName",
"sex", "t1.sex",
"unitName", "t1.unitName",
"unionName", "t1.unionName",
"delegationName", "t2.name"
);
@Inject
private Dao dao;
@Inject
@@ -128,6 +150,8 @@ public class ProposalMineController {
cnd.and("info.createdBy", "=", SecurityUtil.getUserId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.andEX("info.sessionId", "=", sessionId);
// 排序字段必须经过白名单映射,避免前端参数直接参与 SQL 排序。
proposalCommonService.applySafePageOrder(cnd, pageForm, MINE_ORDER_COLUMNS);
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination<NutMap> pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -255,6 +279,8 @@ public class ProposalMineController {
seg.orLike("t1.userName", pageForm.getSearchKeyword());
cnd.and(seg);
}
// 邀请列表与主列表使用独立白名单,确保关联表字段排序准确且安全。
proposalCommonService.applySafePageOrder(cnd, pageForm, SECONDER_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination<ProposalInviteSeconderVO> pagination = proposalCommonService.listPageVO(pageForm, sql, ProposalInviteSeconderVO.class);
return Result.success(pagination);
@@ -9,6 +9,7 @@ import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -39,6 +40,8 @@ public class ProposalPreAuditController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/preAudit/index.html")
@@ -102,7 +105,10 @@ public class ProposalPreAuditController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 预审核列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "preAudit")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -135,7 +135,10 @@ public class ProposalSchoolLeaderApprovalController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 分管领导审批列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "schoolLeaderApproval")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -21,6 +21,7 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalSecond;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalSecondedService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -41,6 +42,7 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 附议提案
@@ -51,6 +53,17 @@ import java.util.List;
@Ok("json:full")
@Api(tags = "提案管理系统-提案附议")
public class ProposalSecondedController {
/** 提案附议列表允许排序的页面字段与 SQL 字段或查询别名映射。 */
private static final Map<String, String> ORDER_COLUMNS = Map.of(
"code", "info.code",
"name", "info.name",
"createUserName", "info.createUserName",
"typeName", "type.name",
"delegationName", "tcd.name",
"taskActorName", "taskActorName",
"curTaskName", "curTaskName",
"instanceState", "ins.state"
);
@Inject
private Dao dao;
@@ -59,6 +72,8 @@ public class ProposalSecondedController {
@Inject
private ProposalSecondedService proposalSecondedService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private FlowCommonService flowCommonService;
@At("")
@@ -141,7 +156,10 @@ public class ProposalSecondedController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 用户未指定有效排序字段时,继续沿用原有任务创建时间倒序。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, ORDER_COLUMNS)) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -96,6 +96,8 @@ public class ProposalUnSubmitQueryController {
cnd.and("info.sessionId", "=", pageForm.getSessionId());
}
// 未提交查询包含子查询统计列,统一通过页面独立白名单排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unSubmitQuery");
sql.setCondition(cnd);
Pagination<NutMap> pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -137,6 +137,8 @@ public class ProposalUnderTakeReadController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id","task.id","JSON_EXTRACT( unit_data.DATA, '$.underTakeId' )");
// 初步阅览列表的计算字段使用固定查询别名排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unitRead");
// cnd.groupBy("task.id");
// cnd.groupBy("JSON_EXTRACT( unit_data.DATA, '$.underTakeId' ) ");
sql.setCondition(cnd);
@@ -148,7 +148,10 @@ public class ProposalUnderTakeReplyController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 承办答复列表未指定有效排序时,继续按任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unitReply")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -100,7 +100,10 @@ public class ProposalUndertakeSuggestionController {
cnd.groupBy("t.id");
cnd.having(Cnd.where("count", approval ? ">" : "=", 0));
cnd.desc("t.createdAt");
// 承办意见列表未指定有效排序时,继续按任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "suggestion")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -80,6 +80,8 @@ public class ProposalViceDelegationController {
cnd.and("info.delegationId", "in", proposalCommonService.getSelfManageDelegationIds());
}
ProposalSearchParam.buildSearch(cnd, pageForm);
// 副团长查阅列表仅允许页面已展示字段参与排序。
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "viceDelegation");
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -21,6 +21,8 @@ public class ProposalQueryComprehensiveParam extends PageForm {
private String code;
@ApiModelProperty(name = "教代会ID")
private String sessionId;
@ApiModelProperty(name = "search session id")
private String searchSessionId;
@ApiModelProperty(name = "教代会ID")
private String[] sessionIds;
@ApiModelProperty(name = "代表团ID")
@@ -79,6 +81,8 @@ public class ProposalQueryComprehensiveParam extends PageForm {
} else {
// cnd.andEX("info.sessionId", "=", searchParam.getSessionId());
}
// Isolated single-session filter for pages that should not reuse sessionId semantics.
cnd.andEX("info.sessionId", "=", searchParam.getSearchSessionId());
//提案名称
if (StrUtil.isNotBlank(searchParam.getName())) {
@@ -1,17 +1,49 @@
package com.budwk.app.zhgh.democratic.proposal.service.common;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.nutz.dao.Cnd;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface ProposalCommonService extends BaseService<ProposalInfo> {
/**
* 按页面允许的字段白名单添加排序条件避免前端字段直接进入SQL
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param allowedOrderColumns 前端字段与数据库字段或查询别名的映射
* @return 是否成功添加安全排序条件
*/
boolean applySafePageOrder(Cnd cnd, PageForm pageForm, Map<String, String> allowedOrderColumns);
/**
* 按提案列表页面注册的独立字段白名单添加排序条件
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param pageCode 提案列表页面代码
* @return 是否成功添加安全排序条件
*/
boolean applySafeProposalListOrder(Cnd cnd, PageForm pageForm, String pageCode);
/**
* 按白名单字段对已汇总的统计结果进行后端排序
*
* @param rows 统计结果行
* @param pageForm 排序字段及方向参数
* @param allowedOrderColumns 允许排序的统计字段
*/
void sortStatisticsRows(List<NutMap> rows, PageForm pageForm, Set<String> allowedOrderColumns);
/**
* 查询提案信息
*/
@@ -9,8 +9,10 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
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.base.utils.PageUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
@@ -58,7 +60,9 @@ import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.Collator;
import java.util.*;
import java.util.stream.Collectors;
@@ -66,6 +70,9 @@ import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService {
/** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */
private static final Map<String, Map<String, String>> PROPOSAL_LIST_ORDER_COLUMNS = createProposalListOrderColumns();
//找出富文本里面上传的图片
static String IMG_SRC_REGEX = "<img\\s+[^>]*src\\s*=\\s*([\"'])(/platform/sys/file/download\\?id=.*?)\\1[^>]*>";
@@ -88,6 +95,178 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao);
}
/**
* 使用业务页面提供的字段白名单构建排序条件非法字段或非法排序方向将被忽略
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param allowedOrderColumns 前端字段与数据库字段或查询别名的映射
* @return 是否成功添加安全排序条件
*/
@Override
public boolean applySafePageOrder(Cnd cnd, PageForm pageForm, Map<String, String> allowedOrderColumns) {
if (cnd == null || pageForm == null || allowedOrderColumns == null) {
return false;
}
// 仅允许白名单中的字段参与排序防止构造任意SQL排序字段
String orderColumn = allowedOrderColumns.get(pageForm.getPageOrderName());
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
if (StrUtil.isBlank(orderColumn) || StrUtil.isBlank(orderBy)) {
return false;
}
cnd.orderBy(orderColumn, orderBy);
return true;
}
/**
* 根据页面代码读取独立白名单并应用排序未注册页面不会接受前端排序字段
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param pageCode 提案列表页面代码
* @return 是否成功添加安全排序条件
*/
@Override
public boolean applySafeProposalListOrder(Cnd cnd, PageForm pageForm, String pageCode) {
if (StrUtil.isBlank(pageCode)) {
return false;
}
return applySafePageOrder(cnd, pageForm, PROPOSAL_LIST_ORDER_COLUMNS.get(pageCode));
}
/**
* 创建各列表页独立排序白名单显示字段与真实SQL字段或固定查询别名一一对应
*
* @return 不可变的页面排序白名单
*/
private static Map<String, Map<String, String>> createProposalListOrderColumns() {
Map<String, Map<String, String>> pages = new HashMap<>();
pages.put("invite", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("viceDelegation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("unSubmitQuery", orderColumns("proposalCode", "info.code", "proposalName", "info.name", "username", "vu.username", "mobile", "vu.mobile", "unitName", "vu.unitName", "delegationName", "tcd.name", "typeName", "pt.name", "mannerName", "manner.name", "secondedNum", "secondedNum", "secondedAgreeNum", "secondedAgreeNum", "delegationAudit", "delegationAudit"));
pages.put("unitReply", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "caseFilingResult", "info.caseFilingResult", "merge", "merge", "underTakeName", "underTakeName", "underTakeIsMaster", "underTakeIsMaster", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state", "transfer", "transferUserName"));
pages.put("unitRead", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "isConsolidation", "isMasterUnderTake", "isMasterUnderTake", "underTakeName", "underTakeName", "processInstanceNodeName", "inst.processInstanceNodeName"));
pages.put("suggestion", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "suggestUnits", "info.suggestUnits", "count", "count", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("commissioner", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "CONFIRM_FILING_COUNT", "CONFIRM_FILING_COUNT", "SUGGESTION_COUNT", "SUGGESTION_COUNT", "NOT_COUNT", "NOT_COUNT", "taskName", "taskName", "instanceState", "ins.state"));
pages.put("feedbackEvaluation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "merge", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("delegation", standardOrderColumns());
pages.put("control", standardOrderColumns());
pages.put("schoolLeaderApproval", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "undertakeUnits", "masterUnitName", "merge", "merge", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state"));
pages.put("committeeFilingUnit", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("allFinished", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "instanceState", "ins.state"));
pages.put("preAudit", standardOrderColumns());
pages.put("committeeFiling", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("caseCheck", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state", "finishTime", "t.finishTime"));
pages.put("exportComprehensive", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("exportSingleCustom", standardOrderColumns());
pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username"));
pages.put("configType", orderColumns("name", "name", "code", "code"));
pages.put("seniorBasic", seniorOrderColumns());
pages.put("expediting", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "curTaskName", "curTaskName", "underTakeName", "underTakeName", "taskName", "t.displayName", "actorName", "ta.actorName", "instanceState", "ins.state"));
pages.put("seniorDelete", seniorOrderColumns());
pages.put("seniorFeedback", mergeOrderColumns(seniorOrderColumns(), orderColumns("tf_feedback", "tf_feedback")));
pages.put("seniorType", seniorOrderColumns());
return Collections.unmodifiableMap(pages);
}
/** 创建常规流程列表共用的字段映射。 */
private static Map<String, String> standardOrderColumns() {
return orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state");
}
/** 创建高级管理列表共用的字段映射。 */
private static Map<String, String> seniorOrderColumns() {
return orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state");
}
/**
* 按前端字段SQL字段成对创建不可变映射
*
* @param mappings 交替排列的前端字段和SQL字段
* @return 不可变字段映射
*/
private static Map<String, String> orderColumns(String... mappings) {
if (mappings == null || mappings.length % 2 != 0) {
throw new IllegalArgumentException("排序字段映射必须成对配置");
}
Map<String, String> columns = new LinkedHashMap<>();
for (int i = 0; i < mappings.length; i += 2) {
columns.put(mappings[i], mappings[i + 1]);
}
return Collections.unmodifiableMap(columns);
}
/** 合并基础字段和页面扩展字段,并返回不可变映射。 */
private static Map<String, String> mergeOrderColumns(Map<String, String> base, Map<String, String> extension) {
Map<String, String> columns = new LinkedHashMap<>(base);
columns.putAll(extension);
return Collections.unmodifiableMap(columns);
}
/**
* 按白名单字段对统计结果执行后端排序数字按数值比较文本按中文区域规则比较
*
* @param rows 统计结果行
* @param pageForm 排序字段及方向参数
* @param allowedOrderColumns 允许排序的统计字段
*/
@Override
public void sortStatisticsRows(List<NutMap> rows, PageForm pageForm, Set<String> allowedOrderColumns) {
if (rows == null || pageForm == null || allowedOrderColumns == null) {
return;
}
String orderName = pageForm.getPageOrderName();
String orderBy = pageForm.getPageOrderBy();
boolean ascending = "ascending".equals(orderBy);
boolean descending = "descending".equals(orderBy);
if (StrUtil.isBlank(orderName) || !allowedOrderColumns.contains(orderName) || (!ascending && !descending)) {
return;
}
// 空值始终排在末尾降序只反转有效值的比较结果
rows.sort((leftRow, rightRow) -> {
Object leftValue = leftRow.get(orderName);
Object rightValue = rightRow.get(orderName);
if (leftValue == null && rightValue == null) {
return 0;
}
if (leftValue == null) {
return 1;
}
if (rightValue == null) {
return -1;
}
int result = compareStatisticsValue(leftValue, rightValue);
return descending ? -result : result;
});
}
/**
* 比较统计字段值兼容不同整数类型以及中文文本
*
* @param leftValue 左侧字段值
* @param rightValue 右侧字段值
* @return 标准比较结果
*/
private int compareStatisticsValue(Object leftValue, Object rightValue) {
if (leftValue instanceof Number && rightValue instanceof Number) {
BigDecimal leftNumber = new BigDecimal(leftValue.toString());
BigDecimal rightNumber = new BigDecimal(rightValue.toString());
return leftNumber.compareTo(rightNumber);
}
String leftText = String.valueOf(leftValue);
String rightText = String.valueOf(rightValue);
String leftPercent = StrUtil.removeSuffix(leftText, "%");
String rightPercent = StrUtil.removeSuffix(rightText, "%");
// 百分比展示值按实际数值比较避免字符串顺序导致10%排在9%之前
if (leftText.endsWith("%") && rightText.endsWith("%")
&& NumberUtil.isNumber(leftPercent) && NumberUtil.isNumber(rightPercent)) {
return new BigDecimal(leftPercent).compareTo(new BigDecimal(rightPercent));
}
return Collator.getInstance(Locale.CHINA).compare(leftText, rightText);
}
@Override
public NutMap info(String id) {
if (StrUtil.isBlank(id)) {
@@ -118,7 +118,11 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
@Override
public List<Sys_dict> listSourceByCode(String code) {
Sys_dict dict = sysDictService.fetch(Cnd.where("code", "=", code));
return dict == null ? new ArrayList<>() : sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(dict.getId())).asc("location"));
return dict == null ? new ArrayList<>() : sysDictService.query(
Cnd.NEW().where("parentId", "=", Strings.sNull(dict.getId()))
.and("disabled","=",0)
.asc("location")
);
}
@Override
@@ -7,7 +7,6 @@ import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
@@ -20,15 +19,14 @@ import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation_union;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation_unit;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.service.TeacherCongressDelegationService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
@@ -43,7 +41,6 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@IocBean
@At("/platform/teacherCongress/delegation")
@@ -63,6 +60,9 @@ public class TeacherCongressDelegationController {
@Inject
private SysRoleService sysRoleService;
@Inject
private TeacherCongressDelegationService teacherCongressDelegationService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/teachercongress/delegation/index.html")
@SaCheckPermission("tc.delegation")
@@ -288,97 +288,50 @@ public class TeacherCongressDelegationController {
}
/**
* 查询团长
* 查询代表团负责人副团长返回多条以兼容V3多个副团长
*
* @param sessionId
* @param delegationId
* @return
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param type 负责人角色编码
* @return 负责人列表
*/
@At
@SaCheckPermission("tc.delegation")
public Result headUser(@Valid String sessionId, @Valid String delegationId, @Param(value = "type", df = "TEACHER_CONGRESS_DELEGATION_HEAD") String type) {
Sys_role role = null;
if (type.equals(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD.name())) {
role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
} else if (type.equals(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD.name())) {
role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
} else if (type.equals(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT.name())) {
role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
} else {
throw new BaseException("参数错误");
}
Record record = dao.fetch("sys_user_role", Cnd.where("tcSessionId", "=", sessionId).and("tcDelegationId", "=", delegationId).and("roleId", "=", role.getId()));
String userId = Optional.ofNullable(record).map(r -> r.getString("userId")).orElse(null);
Sql sql = Sqls.create("""
SELECT
u.id as userId,
u.loginname as loginName,
u.username as userName,
u.mobile,
n.name as unitName
FROM
sys_user u
LEFT JOIN sys_unit n on n.id = u.unitId
WHERE
u.id = @userId
""");
sql.setParam("userId", userId);
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
NutMap user = (NutMap) sql.getResult();
return Result.success().addData(user);
List<NutMap> users = teacherCongressDelegationService.listHeadUsers(sessionId, delegationId, type);
return Result.success().addData(users);
}
/**
* 设置团长
* 设置代表团团长副团长和联络人
*
* @param delegationId 代表团ID
* @param sessionId 届次ID
* @param userId 团长id
* @param viceUserId 副团长id
* @param contactUserId 联络人
* @return
* @param userId 团长ID
* @param viceUserIds 副团长ID数组
* @param contactUserId 联络人ID
* @return 操作结果
*/
@At
@SaCheckPermission("tc.delegation")
@Aop(TransAop.READ_COMMITTED)
public Result insertHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId, @Valid String viceUserId, String contactUserId) {
// 团长
Sys_role role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role.getId()));
dao.insert("sys_user_role", Chain.make("userId", userId).add("roleId", role.getId()).add("tcDelegationId", delegationId).add("tcSessionId", sessionId));
// 副团长
Sys_role role2 = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role2.getId()));
dao.insert("sys_user_role", Chain.make("userId", viceUserId).add("roleId", role2.getId()).add("tcDelegationId", delegationId).add("tcSessionId", sessionId));
// 联络人
Sys_role role3 = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role3.getId()));
dao.insert("sys_user_role", Chain.make("userId", contactUserId).add("roleId", role3.getId()).add("tcDelegationId", delegationId).add("tcSessionId", sessionId));
sysUserService.clearCache();
public Result insertHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId, @Param("viceUserIds") String[] viceUserIds, String contactUserId) {
teacherCongressDelegationService.setHeadUsers(sessionId, delegationId, userId, viceUserIds, contactUserId);
return Result.success();
}
/**
* 删除团长角色
* 删除代表团负责人角色
*
* @param delegationId
* @param sessionId
* @param userId
* @return
* @param delegationId 代表团ID
* @param sessionId 届次ID
* @param userId 用户ID
* @param type 负责人角色编码
* @return 操作结果
*/
@At
@SaCheckPermission("tc.delegation")
public Result deleteHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId) {
Sys_role role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
//先删除角色
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role.getId()).and(Sys_user_role::getUserId, "=", userId));
sysUserService.clearCache();
public Result deleteHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId, @Param(value = "type", df = "TEACHER_CONGRESS_DELEGATION_HEAD") String type) {
teacherCongressDelegationService.deleteHeadUser(sessionId, delegationId, userId, type);
return Result.success();
}
}
@@ -0,0 +1,44 @@
package com.budwk.app.zhgh.democratic.teachercongress.delegation.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 教代会代表团业务服务
*/
public interface TeacherCongressDelegationService extends BaseService<Teacher_congress_delegation> {
/**
* 按角色查询代表团负责人信息
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param type 负责人角色编码
* @return 负责人用户列表
*/
List<NutMap> listHeadUsers(String sessionId, String delegationId, String type);
/**
* 设置代表团团长副团长和联络人
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 团长用户ID
* @param viceUserIds 副团长用户ID列表
* @param contactUserId 联络人用户ID
*/
void setHeadUsers(String sessionId, String delegationId, String userId, String[] viceUserIds, String contactUserId);
/**
* 删除代表团指定负责人角色
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 用户ID
* @param type 负责人角色编码
*/
void deleteHeadUser(String sessionId, String delegationId, String userId, String type);
}
@@ -0,0 +1,164 @@
package com.budwk.app.zhgh.democratic.teachercongress.delegation.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
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.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.service.TeacherCongressDelegationService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.Arrays;
import java.util.List;
/**
* 教代会代表团业务服务实现
*/
@IocBean(args = {"refer:dao"})
public class TeacherCongressDelegationServiceImpl extends BaseServiceImpl<Teacher_congress_delegation> implements TeacherCongressDelegationService {
@Inject
private SysRoleService sysRoleService;
@Inject
private SysUserService sysUserService;
public TeacherCongressDelegationServiceImpl(Dao dao) {
super(dao);
}
/**
* 按角色查询代表团负责人信息副团长允许返回多条记录以兼容V3多副团长数据
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param type 负责人角色编码
* @return 负责人用户列表
*/
@Override
public List<NutMap> listHeadUsers(String sessionId, String delegationId, String type) {
Sys_role role = getHeadRole(type);
Sql sql = Sqls.create("""
SELECT
u.id as userId,
u.loginname as loginName,
u.username as userName,
u.mobile,
n.name as unitName
FROM
sys_user_role sur
LEFT JOIN sys_user u ON u.id = sur.userId
LEFT JOIN sys_unit n ON n.id = u.unitId
WHERE
sur.tcSessionId = @sessionId
AND sur.tcDelegationId = @delegationId
AND sur.roleId = @roleId
ORDER BY u.loginname
""");
sql.setParam("sessionId", sessionId);
sql.setParam("delegationId", delegationId);
sql.setParam("roleId", role.getId());
return listMap(sql);
}
/**
* 设置代表团团长副团长和联络人保存前按角色清理旧值避免同一角色残留脏数据
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 团长用户ID
* @param viceUserIds 副团长用户ID列表
* @param contactUserId 联络人用户ID
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void setHeadUsers(String sessionId, String delegationId, String userId, String[] viceUserIds, String contactUserId) {
// 团长仍然保持单人设置
Sys_role headRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
replaceRoleUsers(sessionId, delegationId, headRole.getId(), new String[]{userId});
// 副团长支持多人设置完整保留V3迁移后的多个副团长关系
Sys_role viceHeadRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
replaceRoleUsers(sessionId, delegationId, viceHeadRole.getId(), viceUserIds);
// 联络人保持单人设置未选择时只清理旧联络人
Sys_role contactRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
replaceRoleUsers(sessionId, delegationId, contactRole.getId(), new String[]{contactUserId});
sysUserService.clearCache();
}
/**
* 删除代表团指定负责人角色按角色类型精确删除避免删除副团长时影响团长
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 用户ID
* @param type 负责人角色编码
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteHeadUser(String sessionId, String delegationId, String userId, String type) {
Sys_role role = getHeadRole(type);
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUserId, "=", userId));
sysUserService.clearCache();
}
/**
* 根据负责人类型解析系统角色集中校验避免 controller 层出现业务分支
*
* @param type 负责人角色编码
* @return 系统角色
*/
private Sys_role getHeadRole(String type) {
if (RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD.name().equals(type)) {
return sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
}
if (RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD.name().equals(type)) {
return sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
}
if (RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT.name().equals(type)) {
return sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
}
throw new BaseException("参数错误");
}
/**
* 替换指定角色用户关系先清理再按去重后的用户ID批量插入
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param roleId 角色ID
* @param userIds 用户ID数组
*/
private void replaceRoleUsers(String sessionId, String delegationId, String roleId, String[] userIds) {
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", roleId));
if (userIds == null || userIds.length == 0) {
return;
}
Arrays.stream(userIds)
.filter(StrUtil::isNotBlank)
.distinct()
.forEach(userId -> dao().insert("sys_user_role", Chain.make("userId", userId)
.add("roleId", roleId)
.add("tcDelegationId", delegationId)
.add("tcSessionId", sessionId)));
}
}
@@ -98,6 +98,7 @@ public class MemberApplyBranchUnionApprovalController {
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN member_apply_record info ON info.id = ins.businessNo
LEFT JOIN vw_user u ON u.id = info.userId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
@@ -113,6 +113,7 @@ public class MemberApplySchoolUnionApprovalController {
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN member_apply_record info ON info.id = ins.businessNo
LEFT JOIN vw_user u ON u.id = info.userId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
@@ -87,6 +87,7 @@ public class MemberApplyUnionGroupApprovalController {
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN member_apply_record info ON info.id = ins.businessNo
LEFT JOIN vw_user u ON u.id = info.userId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
@@ -34,6 +34,9 @@ public class MemberApplyPageForm extends PageForm {
private String userAttribute; //人员属性
/** 人员属性多选值,供开启多选查询的页面使用。 */
private List<String> userAttributes;
public static void buildSearch(Cnd cnd, MemberApplyPageForm pageForm) {
if (cnd == null) {
cnd = Cnd.NEW();
@@ -50,6 +53,11 @@ public class MemberApplyPageForm extends PageForm {
cnd.andEX("info.personType","in",pageForm.getPersonTypes());
cnd.andEX("info.preparedBy","in",pageForm.getPreparedBys());
cnd.andEX("u.aidFundMemberUserType","=",pageForm.getAidFundMemberUserType());
cnd.andEX("u.userAttribute","=",pageForm.getUserAttribute());
// 多选值优先使用IN查询未传多选值时保留原单值查询兼容其他调用页面
if (pageForm.getUserAttributes() != null && !pageForm.getUserAttributes().isEmpty()) {
cnd.and("u.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("u.userAttribute", "=", pageForm.getUserAttribute());
}
}
}
@@ -47,6 +47,9 @@ public class MemberChangePageForm extends PageForm {
@ApiModelProperty("人员属性")
private String userAttribute;
@ApiModelProperty("人员属性组")
private List<String> userAttributes;
public void buildSearch(Cnd cnd, String prefix){
if (cnd == null) {
@@ -66,6 +69,11 @@ public class MemberChangePageForm extends PageForm {
cnd.andEX(prefix + "preparedBy", "in", this.getPreparedBys());
cnd.andEX(prefix + "personType", "in", this.getPersonTypes());
cnd.andEX(prefix + "aidFundMemberUserType", "=", this.getAidFundMemberUserType());
cnd.andEX(prefix + "userAttribute", "=", this.getUserAttribute());
// 多选条件优先兼容仍按单值提交人员属性的旧页面
if (this.getUserAttributes() != null && !this.getUserAttributes().isEmpty()) {
cnd.and(prefix + "userAttribute", "in", this.getUserAttributes());
} else {
cnd.andEX(prefix + "userAttribute", "=", this.getUserAttribute());
}
}
}
@@ -162,10 +162,11 @@ public class MemberInfoPageForm extends PageForm {
} else if (Lang.isNotEmpty(this.getPersonTypes())) {
cnd.and(prefix + "personType", "in", this.getPersonTypes());
}
if (StrUtil.isNotBlank(this.getUserAttribute())) {
cnd.and(prefix + "userAttribute", "=", this.getUserAttribute());
} else if (Lang.isNotEmpty(this.getUserAttributes())) {
// 多选条件优先兼容仍按单值提交人员属性的旧页面
if (Lang.isNotEmpty(this.getUserAttributes())) {
cnd.and(prefix + "userAttribute", "in", this.getUserAttributes());
} else if (StrUtil.isNotBlank(this.getUserAttribute())) {
cnd.and(prefix + "userAttribute", "=", this.getUserAttribute());
}
if (StrUtil.isNotBlank(this.getAidFundMemberUserType())) {
@@ -42,6 +42,9 @@ public class MemberManagePageForm extends PageForm {
private String userAttribute;
/** 人员属性多选查询条件。 */
private List<String> userAttributes;
private String changeType;
//变更状态数组
@@ -93,7 +96,12 @@ public class MemberManagePageForm extends PageForm {
cnd.andEX("u.sex", "=", pageForm.getSex());
cnd.andEX("u.personType", "=", pageForm.getPersonType());
cnd.andEX("u.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("u.userAttribute", "=", pageForm.getUserAttribute());
// 多选条件优先兼容仍按单值提交人员属性的旧页面
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("u.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("u.userAttribute", "=", pageForm.getUserAttribute());
}
if (StrUtil.isNotBlank(pageForm.getChangeDateBefore()) && StrUtil.isNotBlank(pageForm.getChangeDateEnd())) {
cnd.and(new Static(String.format("Date(his.changeTime) >= '%s' and Date(his.changeTime) <= '%s'", pageForm.getChangeDateBefore(), pageForm.getChangeDateEnd())));
@@ -148,7 +156,12 @@ public class MemberManagePageForm extends PageForm {
cnd.andEX("record.preparedBy", "=", pageForm.getPreparedBy());
cnd.andEX("record.personType", "=", pageForm.getPersonType());
cnd.andEX("record.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("record.userAttribute", "=", pageForm.getUserAttribute());
// 历史记录查询与当前会员查询使用一致的人员属性多选规则
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("record.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("record.userAttribute", "=", pageForm.getUserAttribute());
}
cnd.groupBy("record.id,task.id");
}
@@ -396,6 +396,12 @@ public class MemberInfoServiceImpl extends BaseServiceImpl<Sys_user> implements
cnd.andEX("mh.personType", "=", pageForm.getPersonType());
cnd.andEX("mh.preparedBy", "=", pageForm.getPreparedBy());
cnd.andEX("mh.userState", "=", pageForm.getUserState());
// 历史台账未复用通用 buildSearch需要在此显式处理人员属性多选条件
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("mh.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("mh.userAttribute", "=", pageForm.getUserAttribute());
}
if (StrUtil.isAllNotBlank(pageForm.getMemberSearchName(), pageForm.getMemberSearchKeyWord())) {
cnd.and(new SqlExpressionGroup().andLike("u." + pageForm.getMemberSearchName(), pageForm.getMemberSearchKeyWord()));
}
@@ -175,6 +175,12 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
cnd.andEX("his.unitId", "=", pageForm.getUnitId());
cnd.andEX("his.personType", "=", pageForm.getPersonType());
cnd.andEX("his.userState", "=", pageForm.getUserState());
// 变更记录查询自行拼接历史表条件需要单独补充人员属性多选及旧单值兼容
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("his.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("his.userAttribute", "=", pageForm.getUserAttribute());
}
if (StrUtil.isNotBlank(pageForm.getChangeType())){
cnd.and(new Static("JSON_CONTAINS(his.changeTypes, JSON_ARRAY('%s'), '$')".formatted(pageForm.getChangeType())));
}
@@ -0,0 +1,4 @@
-- 评优评先申请表新增历史导入来源和类型名称字段。
ALTER TABLE `evaluate_apply`
ADD COLUMN `source` int(2) DEFAULT 0 COMMENT '来源 0.流程申请 1.历史导入' AFTER `honorTypeId`,
ADD COLUMN `typeName` varchar(200) DEFAULT NULL COMMENT '类型名称' AFTER `source`;
@@ -0,0 +1,122 @@
-- 普惠疗休养平台电脑端菜单。
-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT
'b7c63f0c73a44a9a8c4f3d3bb1a10001',
'',
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
'普惠疗休养',
'Tour',
'menu',
'',
'',
'ti-map-alt',
1,
0,
'tour',
NULL,
991,
1,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
NULL,
NULL,
'p',
0,
0
FROM sys_menu
WHERE (parentId = '' OR parentId IS NULL)
AND CHAR_LENGTH(path) = 4
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10002', p.id, CONCAT(p.path, '0001'), '疗休养设置', 'Tour Setting', 'menu', '/platform/tour/setting', 'data-pjax', '', 1, 0, 'tour.setting', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.setting') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10003', p.id, CONCAT(p.path, '0002'), '服务单位管理', 'Service Unit', 'menu', '/platform/tour/travelAgency', 'data-pjax', '', 1, 0, 'tour.travelAgency', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.travelAgency') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10004', p.id, CONCAT(p.path, '0003'), '线路管理', 'Route Manage', 'menu', '/platform/tour/route', 'data-pjax', '', 1, 0, 'tour.route', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.route') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10005', p.id, CONCAT(p.path, '0004'), '疗休养事项', 'Tour Matter', 'menu', '/platform/tour/matter', 'data-pjax', '', 1, 0, 'tour.matter', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.matter') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10006', p.id, CONCAT(p.path, '0005'), '疗休养报名', 'Tour Signup', 'menu', '/platform/tour/signup', 'data-pjax', '', 1, 0, 'tour.signup', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.signup') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10008', p.id, CONCAT(p.path, '0006'), '我的报名', 'My Signup', 'menu', '/platform/tour/mysignup', 'data-pjax', '', 1, 0, 'tour.mysignup', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.mysignup') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10009', p.id, CONCAT(p.path, '0007'), '分工会查询', 'Union Ledger', 'menu', '/platform/tour/unionledger', 'data-pjax', '', 1, 0, 'tour.unionledger', NULL, 7, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionledger') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10011', p.id, CONCAT(p.path, '0010'), '分工会审核', 'Union Approval', 'menu', '/platform/tour/unionApproval', 'data-pjax', '', 1, 0, 'tour.unionApproval', NULL, 8, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionApproval') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10012', p.id, CONCAT(p.path, '0011'), '校工会审核', 'School Union Approval', 'menu', '/platform/tour/schoolUnionApproval', 'data-pjax', '', 1, 0, 'tour.schoolUnionApproval', NULL, 9, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.schoolUnionApproval') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10007', p.id, CONCAT(p.path, '0008'), '疗休养台账', 'Tour Ledger', 'menu', '/platform/tour/ledger', 'data-pjax', '', 1, 0, 'tour.ledger', NULL, 10, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.ledger') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10010', p.id, CONCAT(p.path, '0009'), '线路成团', 'Tour Group', 'menu', '/platform/tour/group', 'data-pjax', '', 1, 0, 'tour.group', NULL, 11, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.group') t);
-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
FROM sys_role r
JOIN sys_menu m ON m.permission IN (
'tour',
'tour.setting',
'tour.travelAgency',
'tour.route',
'tour.matter',
'tour.signup',
'tour.group',
'tour.mysignup',
'tour.unionledger',
'tour.unionApproval',
'tour.schoolUnionApproval',
'tour.ledger'
)
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code = 'SYSADMIN'
AND rm.roleId IS NULL;
@@ -0,0 +1,3 @@
-- 疗休养配置增加出行人数指标,历史配置默认按 0 处理。
ALTER TABLE `tour_setting`
ADD COLUMN `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标' AFTER `tourType`;
@@ -0,0 +1,183 @@
-- 普惠疗休养基础配置表。
-- 若生产环境未开启 Nutz 自动建表,请先执行本脚本再使用“疗休养设置”菜单。
CREATE TABLE IF NOT EXISTS `tour_setting` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`configName` varchar(100) DEFAULT NULL COMMENT '疗休养配置名称',
`tourType` varchar(50) DEFAULT NULL COMMENT '疗休养类型',
`activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID',
`sortNo` int DEFAULT NULL COMMENT '排序编号',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`outProvinceYears` int DEFAULT NULL COMMENT '省外几年去一次',
`outProvinceRatio` decimal(10,2) DEFAULT NULL COMMENT '省外人数占比',
`outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型',
`outProvinceFixedPeople` int DEFAULT 0 COMMENT '省外固定人数',
`cycleStartYear` int DEFAULT NULL COMMENT '周期开始年度',
`cycleEndYear` int DEFAULT NULL COMMENT '周期结束年度',
`cycleTotalCost` int DEFAULT NULL COMMENT '周期内总费用',
`cycleAllowedTimes` int DEFAULT NULL COMMENT '周期允许次数',
`allowFamily` tinyint(1) DEFAULT 0 COMMENT '是否允许携带家属',
`fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息',
`enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`serviceNotice` text COMMENT '服务须知',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_setting_year` (`year`),
KEY `idx_tour_setting_activity_group` (`activityGroupId`),
KEY `idx_tour_setting_sort` (`sortNo`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置';
-- 疗休养配置标段表。
-- 标段先挂在配置上维护,后续线路、旅行社、报名等模块可继续通过 lotId 做业务关联。
CREATE TABLE IF NOT EXISTS `tour_setting_lot` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属配置ID',
`lotName` varchar(50) DEFAULT NULL COMMENT '标段名称',
`lotValue` varchar(50) DEFAULT NULL COMMENT '标段值',
`activityCost` int DEFAULT NULL COMMENT '标段费用',
`allowOverReimbursement` tinyint(1) DEFAULT 0 COMMENT '允许超出报销',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_setting_lot_setting` (`settingId`),
KEY `idx_tour_setting_lot_value` (`lotValue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置标段';
-- 旅行社管理表。
CREATE TABLE IF NOT EXISTS `tour_travel_agency` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`agencyCode` varchar(50) DEFAULT NULL COMMENT '旅行社编号',
`agencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系人手机',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`remark` text COMMENT '备注',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '激活状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_travel_agency_year_code` (`year`, `agencyCode`),
KEY `idx_tour_travel_agency_name` (`agencyName`),
KEY `idx_tour_travel_agency_contact` (`contactName`, `contactPhone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养旅行社';
-- 线路管理表。
CREATE TABLE IF NOT EXISTS `tour_line` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '创建年度',
`lineCode` varchar(50) DEFAULT NULL COMMENT '线路编号',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID',
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`lineType` varchar(50) DEFAULT NULL COMMENT '线路类型',
`lotId` varchar(32) DEFAULT NULL COMMENT '时间标段ID',
`lineContent` text COMMENT '线路内容',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`openFlag` tinyint(1) DEFAULT 1 COMMENT '是否对外开放',
`directFamilyUnitLine` tinyint(1) DEFAULT 0 COMMENT '是否直系亲属线路',
`enabled` tinyint(1) DEFAULT 1 COMMENT '激活状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_line_year_code` (`year`, `lineCode`),
KEY `idx_tour_line_year` (`year`),
KEY `idx_tour_line_name` (`lineName`),
KEY `idx_tour_line_agency` (`travelAgencyId`),
KEY `idx_tour_line_creator` (`creatorUserId`),
KEY `idx_tour_line_unit` (`unitId`),
KEY `idx_tour_line_lot` (`lotId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养线路';
-- 疗休养事项表。
CREATE TABLE IF NOT EXISTS `tour_matter` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`year` int DEFAULT NULL COMMENT '年度',
`matterName` varchar(100) DEFAULT NULL COMMENT '事项名称',
`settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID',
`creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`creatorName` varchar(100) DEFAULT NULL COMMENT '创建人',
`unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID',
`organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式',
`mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图',
`enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_matter_year_name` (`year`, `matterName`),
KEY `idx_tour_matter_year` (`year`),
KEY `idx_tour_matter_creator` (`creatorUserId`),
KEY `idx_tour_matter_setting` (`settingId`),
KEY `idx_tour_matter_union` (`unionId`),
KEY `idx_tour_matter_org_type` (`organizationType`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项';
-- 疗休养事项批次表。
CREATE TABLE IF NOT EXISTS `tour_matter_batch` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`matterId` varchar(32) DEFAULT NULL COMMENT '事项ID',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`batchName` varchar(100) DEFAULT NULL COMMENT '批次名称',
`signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间',
`signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间',
`changeDeadline` varchar(20) DEFAULT NULL COMMENT '变更截止时间',
`contactName` varchar(30) DEFAULT NULL COMMENT '联系人',
`contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式',
`minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数',
`maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数',
`estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用',
`enabled` tinyint(1) DEFAULT 1 COMMENT '状态',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_matter_batch_matter` (`matterId`),
KEY `idx_tour_matter_batch_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项批次';
-- 直系亲属线路报名信息表。
CREATE TABLE IF NOT EXISTS `tour_ledger_direct_relative` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`ledgerId` varchar(32) DEFAULT NULL COMMENT '台账ID',
`relativeName` varchar(100) DEFAULT NULL COMMENT '亲属姓名',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`relationshipCode` varchar(50) DEFAULT NULL COMMENT '亲属关系编码',
`relationshipName` varchar(50) DEFAULT NULL COMMENT '亲属关系',
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始日期',
`travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束日期',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_tour_ledger_direct_relative_ledger` (`ledgerId`),
KEY `idx_tour_ledger_direct_relative_line` (`lineId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养直系亲属线路报名信息';
@@ -0,0 +1,17 @@
-- 疗休养配置分工会人员分配表,人员数量合计受配置的出行人数指标约束。
CREATE TABLE IF NOT EXISTS `tour_setting_union_quota` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称',
`peopleQuota` int DEFAULT 0 COMMENT '人员数量',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_setting_union_quota_setting_union` (`settingId`, `unionId`),
KEY `idx_tour_setting_union_quota_setting` (`settingId`),
KEY `idx_tour_setting_union_quota_union` (`unionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会人员分配';
@@ -170,6 +170,16 @@ module.exports = {
type: Number,
default: 300
},
// wangEditor
placeholder: {
type: String,
default: ""
},
//
topTip: {
type: String,
default: ""
},
menus: {
type: Array,
default: () => {
@@ -205,13 +215,18 @@ module.exports = {
data() {
return {
editor: null,
cursorPos: 0
cursorPos: 0,
topTipElement: null
}
},
mounted() {
this.initEditor()
},
beforeDestroy() {
this.removeTopTip()
},
watch: {
value: {
handler: function (newValue) {
@@ -229,6 +244,13 @@ module.exports = {
})
},
immediate: true
},
topTip: {
handler: function () {
this.$nextTick(() => {
this.renderTopTip()
})
}
}
},
@@ -266,7 +288,43 @@ module.exports = {
this.editor.config.pasteFilterStyle = false
this.editor.config.height = this.height
this.editor.config.menus = this.menus
// wangEditor placeholder create
this.editor.config.placeholder = this.placeholder
this.editor.create()
this.renderTopTip()
},
renderTopTip() {
if (!this.editor || !this.$refs.editorRef) {
return
}
const textContainer = this.$refs.editorRef.querySelector(".w-e-text-container")
if (!textContainer) {
return
}
if (!this.topTip) {
this.removeTopTip()
return
}
if (!this.topTipElement) {
this.topTipElement = document.createElement("div")
this.topTipElement.setAttribute("contenteditable", "false")
this.topTipElement.style.color = "#f56c6c"
this.topTipElement.style.fontSize = "12px"
this.topTipElement.style.lineHeight = "20px"
this.topTipElement.style.padding = "6px 10px 0"
this.topTipElement.style.userSelect = "none"
this.topTipElement.style.pointerEvents = "none"
}
//
this.topTipElement.innerText = this.topTip
if (textContainer.firstChild !== this.topTipElement) {
textContainer.insertBefore(this.topTipElement, textContainer.firstChild)
}
},
removeTopTip() {
if (this.topTipElement && this.topTipElement.parentNode) {
this.topTipElement.parentNode.removeChild(this.topTipElement)
}
},
uploadFiles(resultFiles) {
return resultFiles.map(async (file) => {
@@ -133,6 +133,12 @@ module.exports = {
default: 1024 * 1024 * 10,
required: false
},
//
limit_upload_size: {
type: Boolean,
default: true,
required: false
},
//
upload_text: {
type: String,
@@ -216,7 +222,7 @@ module.exports = {
} else {
tips = tips + '不限格式;'
}
if (fileSize) {
if (this.limit_upload_size && fileSize) {
tips = tips + `单个文件大小不能超过<span style="color: red">${fileSize / 1024 / 1024}</span>M`
}
return tips
@@ -313,7 +319,8 @@ module.exports = {
// image
beforeUpload(file) {
if (file.size > this.upload_size) {
//
if (this.limit_upload_size && file.size > this.upload_size) {
this.$message.error("文件过大,请上传小于" + (this.upload_size / 1024 / 1024).toFixed(0) + "M的文件!")
return false
}
@@ -419,6 +419,25 @@ layout("/layouts/v4/baseLayout.html"){
},
defaultSelect() {
// 应用中心点击根菜单时会缓存完整应用信息,根菜单配置了href则应优先进入根菜单页面。
try {
const rootApp = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
const currentAppId = GetQueryString("appId")
const hasRootHref = rootApp && typeof rootApp.href === "string" && rootApp.href.trim() !== ""
// 校验缓存应用与当前URL一致,避免复用旧标签页缓存时跳转到其他应用。
if (hasRootHref && String(rootApp.id) === currentAppId) {
this.activeMenuIndex = rootApp.id
this.openedMenus = []
this.$nextTick(() => {
this.menuSelect(this.activeMenuIndex, this.openedMenus)
commonUtil.pjaxPush(getBaseSubAppPath(rootApp.href))
})
return
}
} catch (e) {
// 根菜单缓存缺失或格式异常时,继续使用子菜单href作为默认入口。
}
// 查找第一个有href的子菜单
function findFirstChildWithHref(menus) {
for (const menu of menus) {
@@ -19,7 +19,7 @@ const branchUnionGroupManage = {
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" border>
<el-table :data="tableData" :height="table_height ? Math.max(200, table_height - 90) : null" border>
<el-table-column type="index" width="50" label="序号"></el-table-column>
<el-table-column prop="code" label="小组编码" width="120px"></el-table-column>
<el-table-column prop="name" label="小组名称" width="200px"></el-table-column>
@@ -85,6 +85,11 @@ const branchUnionGroupManage = {
union_id: {
type: String,
required: true
},
// 工会小组列表包含分页,父页面传入高度后预留分页区域。
table_height: {
type: Number,
default: null
}
},
data() {
@@ -16,7 +16,7 @@ const branchUnionPartUnitManage = {
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" border>
<el-table :data="tableData" :height="table_height ? Math.max(200, table_height - 40) : null" border>
<el-table-column type="index" width="50" label="序号"></el-table-column>
<el-table-column prop="unitcode" label="单位编码"></el-table-column>
<el-table-column prop="name" label="单位名称"></el-table-column>
@@ -48,6 +48,11 @@ const branchUnionPartUnitManage = {
union_id: {
type: String,
required: true
},
// 父页面可传入可用高度;默认值保持组件在其他页面的原有展示能力。
table_height: {
type: Number,
default: null
}
},
data() {
@@ -45,7 +45,7 @@ const branchUnionUserManage = {
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" border size="small">
<el-table :data="tableData" :height="table_height ? Math.max(200, table_height - 40) : null" border size="small">
<el-table-column prop="loginName" label="工号"></el-table-column>
<el-table-column prop="userName" label="姓名"></el-table-column>
<el-table-column prop="mobile" label="联系方式"></el-table-column>
@@ -123,6 +123,11 @@ const branchUnionUserManage = {
union_id: {
type: String,
required: true
},
// 父页面可传入可用高度;默认值保持组件在其他页面的原有展示能力。
table_height: {
type: Number,
default: null
}
},
data() {
@@ -174,7 +174,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名服务单位">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
@@ -114,7 +114,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名服务单位">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="detailFillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="detailFillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
@@ -255,7 +255,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="报名旅行社">
<el-form-item label="报名服务单位">
<el-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
</el-col>
@@ -112,8 +112,8 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旅行社名称" prop="travelAgencyId">
<el-select v-model="formData.travelAgencyId" clearable filterable placeholder="请选择旅行社" style="width: 100%">
<el-form-item label="服务单位名称" prop="travelAgencyId">
<el-select v-model="formData.travelAgencyId" clearable filterable placeholder="请选择服务单位" style="width: 100%">
<el-option v-for="item in travelAgencyOptions" :key="item.id" :label="item.agencyName" :value="item.id"></el-option>
</el-select>
</el-form-item>
@@ -187,7 +187,7 @@ layout("/layouts/platform.html"){
<text-editor v-model="formData.lineContent"></text-editor>
</el-form-item>
<el-form-item label="移动端缩略图" prop="mobileThumb">
<!--旅行社管理保持一致:移动端缩略图使用单图上传并保存URL。 -->
<!--服务单位管理保持一致:移动端缩略图使用单图上传并保存URL。 -->
<file-upload
style="--upload-width: 200px;--upload-height:108px"
:upload_number="1"
@@ -191,7 +191,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="服务单位">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
@@ -83,6 +83,7 @@ layout("/layouts/platform.html"){
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="64.8%"
custom-class="tour-setting-dialog"
@closed="destroyEditor">
<el-form :model="formData" :rules="formRules" label-width="120px" ref="form">
<div v-if="title === '新建疗休养配置'" class="tour-setting-inherit">
@@ -145,6 +146,11 @@ layout("/layouts/platform.html"){
<el-input-number v-model="formData.maxGroupPeople" :controls="false" :min="0" :precision="0" placeholder="请输入最多成团人数" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出行人数指标" prop="travelPeopleQuota">
<el-input-number v-model="formData.travelPeopleQuota" :controls="false" :min="0" :precision="0" placeholder="请输入出行人数指标" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="tour-setting-basic-divider"></div>
</el-col>
@@ -267,6 +273,47 @@ layout("/layouts/platform.html"){
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="人员分配" name="unionQuota">
<div class="tour-tab-fill">
<div class="tour-quota-toolbar">
<div class="tour-quota-stat">
<span>出行人数指标</span>
<strong>{{ travelPeopleQuota }}</strong>
</div>
<div class="tour-quota-stat">
<span>分工会总人数</span>
<strong>{{ branchTotalPersonCount }}</strong>
</div>
<el-input-number
v-model="quotaAllocate.ratio"
:controls="false"
:min="0"
:precision="2"
placeholder="比例"
class="tour-quota-input">
</el-input-number>
<el-button type="primary" size="medium" @click="allocateQuotaByRatio">按比例分配</el-button>
</div>
<el-table
:data="formData.unionQuotas"
border
class="vi-table tour-quota-table tour-tab-table"
empty-text="暂无分工会"
height="100%"
show-summary
:summary-method="unionQuotaSummary"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="分工会" prop="unionName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="当前人数" prop="personCount" width="140" align="center" header-align="center"></el-table-column>
<el-table-column label="人员数量" width="180" align="center" header-align="center">
<template slot-scope="{row}">
<el-input-number v-model="row.peopleQuota" :controls="false" :min="0" :precision="0" style="width: 100%"></el-input-number>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="服务须知" name="notice">
<el-form-item prop="serviceNotice" label-width="0">
<text-editor v-model="formData.serviceNotice"></text-editor>
@@ -331,10 +378,14 @@ layout("/layouts/platform.html"){
activityGroupList: [],
formData: {},
lotDeleteList: [],
quotaAllocate: {
ratio: null
},
inheritLoading: false,
formRules: {
year: [{required: true, message: "必填", trigger: ["blur", "change"]}],
configName: [{required: true, message: "必填", trigger: ["blur", "change"]}],
travelPeopleQuota: [{required: true, message: "必填", trigger: ["blur", "change"]}],
minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}],
@@ -347,12 +398,117 @@ layout("/layouts/platform.html"){
components: {
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
},
computed: {
// 出行人数指标统一按非负整数参与页面统计和分配上限校验。
travelPeopleQuota() {
return this.toNonNegativeInteger(this.formData.travelPeopleQuota)
},
// 人员总数使用后端实时统计结果,仅作为按比例分配的计算基数。
branchTotalPersonCount() {
const rows = this.formData.unionQuotas || []
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.personCount), 0)
}
},
watch: {
"formData.travelPeopleQuota": function() {
this.refreshQuotaRatio()
}
},
methods: {
async getActivityGroup() {
const {data} = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup")
this.activityGroupList = (data || []).map((item) => {
// 加载分工会人员分配行;新增时传空配置ID,编辑时合并已保存数量。
loadUnionQuotaRows(settingId) {
this.$axios.post(loc() + "/unionQuotaRows", {settingId: settingId || ""}).then((res) => {
if (res.code === 0) {
this.$set(this.formData, "unionQuotas", this.normalizeUnionQuotaRows(res.data || []))
this.refreshQuotaRatio()
} else {
this.$message.warning(res.msg || "查询人员分配失败")
}
})
},
// 将接口和历史数据中的空值规范为非负整数,避免合计结果出现 NaN。
normalizeUnionQuotaRows(rows) {
return (rows || []).map(item => {
return Object.assign({}, item, {
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
peopleQuota: this.toNonNegativeInteger(item.peopleQuota),
personCount: this.toNonNegativeInteger(item.personCount)
})
})
},
toNonNegativeInteger(value) {
const numberValue = parseInt(value, 10)
if (isNaN(numberValue) || numberValue < 0) {
return 0
}
return numberValue
},
toNumber(value) {
const numberValue = parseFloat(value)
return isNaN(numberValue) ? null : numberValue
},
truncateDecimal(value, precision) {
const times = Math.pow(10, precision)
return Math.floor(value * times) / times
},
// 默认比例为出行人数指标除以分工会总人数,并按参考项目保留两位后直接截断。
refreshQuotaRatio() {
if (this.branchTotalPersonCount <= 0 || this.travelPeopleQuota <= 0) {
this.quotaAllocate.ratio = null
return
}
this.quotaAllocate.ratio = this.truncateDecimal(this.travelPeopleQuota / this.branchTotalPersonCount, 2)
},
unionQuotaSummary(param) {
const columns = param.columns || []
const rows = this.formData.unionQuotas || []
return columns.map((column, index) => {
if (index === 0) {
return "合计"
}
if (column.property === "personCount") {
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.personCount), 0)
}
if (column.label === "人员数量") {
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.peopleQuota), 0)
}
return ""
})
},
allocateQuotaByRatio() {
const rows = this.formData.unionQuotas || []
const ratio = this.toNumber(this.quotaAllocate.ratio)
if (!rows.length) {
this.$message.warning("暂无分工会数据")
return
}
if (ratio === null || this.quotaAllocate.ratio === "" || this.quotaAllocate.ratio === undefined) {
this.$message.warning("比例为空,未分配")
return
}
if (ratio < 0) {
this.$message.warning("比例不能为负数")
return
}
// 按比例分配时小数直接舍去,不补余数,保持与参考项目一致。
const nextQuotas = rows.map(row => {
return Math.floor(ratio * this.toNonNegativeInteger(row.personCount))
})
const quotaTotal = nextQuotas.reduce((sum, value) => sum + value, 0)
if (quotaTotal > this.travelPeopleQuota) {
this.$message.warning("当前人员数量合计 " + quotaTotal + ",不能超过出行人数指标 " + this.travelPeopleQuota)
return
}
nextQuotas.forEach((peopleQuota, index) => {
this.$set(rows[index], "peopleQuota", peopleQuota)
})
},
getActivityGroup() {
return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => {
const data = res.data
this.activityGroupList = (data || []).map((item) => {
return Object.assign({}, item, {
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
})
})
})
},
@@ -378,6 +534,7 @@ layout("/layouts/platform.html"){
year: moment().format("YYYY"),
configName: "",
tourType: "",
travelPeopleQuota: 0,
activityGroupId: "",
sortNo: 0,
minGroupPeople: 0,
@@ -394,6 +551,7 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
enabled: true,
lots: [],
unionQuotas: [],
serviceNotice: ""
}
},
@@ -404,6 +562,7 @@ layout("/layouts/platform.html"){
// 新建时给出默认年度和开关值,减少校工会管理员录入成本。
this.formData = this.defaultFormData()
this.dialogVisible = true
this.loadUnionQuotaRows("")
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
inheritPreviousYearInfo() {
@@ -428,11 +587,19 @@ layout("/layouts/platform.html"){
allowOverReimbursement: !!item.allowOverReimbursement
}
})
// 延用上一年分配数量,但清除原配置和子表ID,保存时生成当前年度数据。
const inheritedUnionQuotas = this.normalizeUnionQuotaRows(previous.unionQuotas || []).map(item => {
return Object.assign({}, item, {
id: "",
settingId: ""
})
})
this.formData = Object.assign(this.defaultFormData(), previous, {
year: String(currentYear),
cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "",
cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "",
lots: inheritedLots
lots: inheritedLots,
unionQuotas: inheritedUnionQuotas
})
delete this.formData.id
delete this.formData.createdAt
@@ -463,6 +630,7 @@ layout("/layouts/platform.html"){
this.$axios.post(loc() + "/detail", {id: row.id}).then((res) => {
if (res.code === 0) {
this.formData = Object.assign({
travelPeopleQuota: 0,
activityGroupId: "",
outProvinceRatioType: "当年报名人数",
outProvinceFixedPeople: 0,
@@ -474,6 +642,7 @@ layout("/layouts/platform.html"){
fillBedInfo: true,
enabled: true,
lots: [],
unionQuotas: [],
serviceNotice: ""
}, res.data || {})
this.formData.year = this.formData.year ? String(this.formData.year) : ""
@@ -491,6 +660,7 @@ layout("/layouts/platform.html"){
this.formData.lots = (this.formData.lots || []).map(item => Object.assign({}, item, {
allowOverReimbursement: !!item.allowOverReimbursement
}))
this.formData.unionQuotas = this.normalizeUnionQuotaRows(this.formData.unionQuotas || [])
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
} else {
@@ -502,6 +672,7 @@ layout("/layouts/platform.html"){
this.$refs.form.validate((valid) => {
if (!valid) return
if (!this.validateLots()) return
if (!this.validateUnionQuotas()) return
this.submitLoading = true
// Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。
const submitData = JSON.parse(JSON.stringify(this.formData))
@@ -509,6 +680,7 @@ layout("/layouts/platform.html"){
submitData.outProvinceFixedPeople = 0
}
submitData.lots = JSON.stringify(this.formData.lots || [])
submitData.unionQuotas = JSON.stringify(this.formData.unionQuotas || [])
submitData.lotDeleteList = JSON.stringify(this.lotDeleteList)
this.$axios.post(loc() + "/doSubmit", submitData).then((res) => {
this.submitLoading = false
@@ -562,6 +734,29 @@ layout("/layouts/platform.html"){
}
return true
},
// 提交前统一人员数量格式,并校验分配合计不超过出行人数指标。
validateUnionQuotas() {
const rows = this.formData.unionQuotas || []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
if (row.peopleQuota === null || row.peopleQuota === undefined || row.peopleQuota === "") {
row.peopleQuota = 0
}
if (!/^\d+$/.test(String(row.peopleQuota))) {
this.$message.warning("第" + (i + 1) + "行人员数量必须为非负整数")
return false
}
this.$set(row, "peopleQuota", this.toNonNegativeInteger(row.peopleQuota))
}
const quotaTotal = rows.reduce((sum, row) => {
return sum + this.toNonNegativeInteger(row.peopleQuota)
}, 0)
if (quotaTotal > this.travelPeopleQuota) {
this.$message.warning("当前人员数量合计 " + quotaTotal + ",不能超过出行人数指标 " + this.travelPeopleQuota)
return false
}
return true
},
deleteLot(index, row) {
this.$confirm("确定删除该标段吗?", "提示", {
confirmButtonText: "确定",
@@ -593,6 +788,9 @@ layout("/layouts/platform.html"){
destroyEditor() {
this.formData = {}
this.lotDeleteList = []
this.quotaAllocate = {
ratio: null
}
this.inheritLoading = false
this.activeTab = "basic"
}
@@ -627,6 +825,94 @@ layout("/layouts/platform.html"){
border-top: 1px dashed #dcdfe6;
margin: 2px 0 18px;
}
.tour-quota-toolbar {
align-items: center;
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
.tour-quota-input {
width: 160px;
}
.tour-quota-stat {
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: inline-flex;
gap: 8px;
height: 36px;
padding: 0 10px;
}
.tour-quota-stat span {
color: #606266;
}
.tour-quota-stat strong {
color: #303133;
font-weight: 600;
}
.tour-tab-fill {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.tour-tab-table {
flex: 1;
min-height: 0;
}
.tour-setting-dialog {
display: flex;
flex-direction: column;
height: 85vh;
max-height: 85vh;
}
.tour-setting-dialog .el-dialog__header,
.tour-setting-dialog .el-dialog__footer,
.tour-setting-dialog .el-tabs__header {
flex-shrink: 0;
}
.tour-setting-dialog .el-dialog__body {
flex: 1;
min-height: 0;
overflow: hidden;
padding-bottom: 0;
}
.tour-setting-dialog .el-form,
.tour-setting-dialog .el-tabs {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.tour-setting-dialog .el-tabs {
flex: 1;
}
.tour-setting-dialog .el-tabs__content {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding: 0 6px 18px 0;
}
.tour-setting-dialog .el-tab-pane {
height: 100%;
}
</style>
<!--#
@@ -154,7 +154,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="报名旅行社">
<el-form-item label="报名服务单位">
<el-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
</el-col>
@@ -360,7 +360,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="时间标段">{{ lineDetail.lotName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ lineDetail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="直系亲属线路">{{ isDirectFamilyLine(lineDetail) ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ lineDetail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="服务单位">{{ lineDetail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ lineDetail.creatorName || '' }}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{ lineDetail.unitName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否对外开放">{{ lineDetail.openFlag ? '是' : '否' }}</el-descriptions-item>
@@ -15,11 +15,11 @@ layout("/layouts/platform.html"){
style="width: 100%">
</el-date-picker>
</search-item>
<search-item label="旅行社名称">
<search-item label="服务单位名称">
<el-input
v-model="pageForm.agencyName"
clearable
placeholder="请输入旅行社名称"
placeholder="请输入服务单位名称"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
@@ -50,7 +50,7 @@ layout("/layouts/platform.html"){
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="旅行社列表">
<table-tool :app="this" label="服务单位列表">
<el-button @click="openAdd" size="medium" type="primary">
<i class="el-icon-plus"></i>
新增
@@ -65,8 +65,8 @@ layout("/layouts/platform.html"){
@sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="旅行社编号" prop="agencyCode" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="旅行社名称" prop="agencyName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="服务单位编号" prop="agencyCode" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="服务单位名称" prop="agencyName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="联系电话" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="激活状态" prop="enabled" width="160" align="center" header-align="center">
@@ -111,15 +111,15 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旅行社名称" prop="agencyName">
<el-input v-model="formData.agencyName" maxlength="30" show-word-limit placeholder="请输入旅行社名称"></el-input>
<el-form-item label="服务单位名称" prop="agencyName">
<el-input v-model="formData.agencyName" maxlength="30" show-word-limit placeholder="请输入服务单位名称"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="旅行社编号" prop="agencyCode">
<el-input v-model="formData.agencyCode" maxlength="50" placeholder="请输入旅行社编号"></el-input>
<el-form-item label="服务单位编号" prop="agencyCode">
<el-input v-model="formData.agencyCode" maxlength="50" placeholder="请输入服务单位编号"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -248,19 +248,19 @@ layout("/layouts/platform.html"){
}
},
openAdd() {
this.title = "新增旅行社信息"
this.title = "新增服务单位信息"
this.viewMode = false
this.formData = this.emptyForm()
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
openEdit(row) {
this.title = "编辑旅行社信息"
this.title = "编辑服务单位信息"
this.viewMode = false
this.loadDetail(row.id)
},
openView(row) {
this.title = "查看旅行社信息"
this.title = "查看服务单位信息"
this.viewMode = true
this.loadDetail(row.id)
},
@@ -191,7 +191,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="服务单位">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="是否携带家属">
{{ familyText() }}
</el-descriptions-item>
@@ -164,7 +164,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="报名服务单位">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
@@ -12,6 +12,7 @@ const PROPOSAL_INFO = {
</h3>
<el-descriptions :column="3" border>
<el-descriptions-item label="提案编号">{{ viewData.code }}</el-descriptions-item>
<el-descriptions-item label="立案编号">{{ viewData.caseFilingCode || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="提案人">{{viewData.createUserName }}</el-descriptions-item>
<el-descriptions-item label="提案时间">{{ viewData.createTime }}</el-descriptions-item>
<el-descriptions-item label="教代会届次"> {{ viewData.sessionName || '暂无' }}</el-descriptions-item>
@@ -87,7 +88,8 @@ const PROPOSAL_INFO = {
<div style="display: flex;justify-content: center">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
:value="row.caseFilingResult"></dict-tag>
<template v-if="row.caseFilingType">
<!-- 普通提案/重点提案暂不展示历史数据仍保留 -->
<template v-if="false">
(
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="row.caseFilingType"></dict-tag>
@@ -107,9 +109,22 @@ const PROPOSAL_INFO = {
</div>
</el-dialog>
<template v-for="task in doneTasks">
<div class="mt10">
<div class="process-title">{{ task.displayName }}</div>
<!--审核信息采用手风琴展示同一时间最多展开一条记录避免流程较长时页面内容一次性全部铺开-->
<template v-if="doneTasks.length">
<div class="process-title">审核信息</div>
<el-collapse v-model="activeTaskId" accordion>
<el-collapse-item v-for="task in doneTasks" :key="task.id" :name="task.id">
<template slot="title">
<!--折叠标题使用独立的单行布局避免复用 process-title 后产生外边距和高度冲突-->
<div style="display: flex;align-items: center;justify-content: space-between;width: 100%;min-width: 0;padding-left: 10px">
<span style="overflow: hidden;text-overflow: ellipsis;white-space: nowrap;font-size: 15px;font-weight: 600">
{{ task.displayName }}
</span>
<span style="flex-shrink: 0;margin-right: 8px;color: var(--color-primary);font-weight: 400">
{{ activeTaskId === task.id ? '收起' : '展开' }}
</span>
</div>
</template>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName
@@ -154,7 +169,8 @@ const PROPOSAL_INFO = {
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
:value="task.ext.tf_caseFilingResult"></dict-tag>
<template v-if="task.ext.tf_caseFilingType">
<!-- 普通提案/重点提案暂不展示历史流程变量仍保留 -->
<template v-if="false">
(
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="task.ext.tf_caseFilingType"></dict-tag>
@@ -200,7 +216,8 @@ const PROPOSAL_INFO = {
<div v-html="task.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
</div>
</el-collapse-item>
</el-collapse>
</template>
<slot></slot>
@@ -215,6 +232,7 @@ const PROPOSAL_INFO = {
return {
viewData: {},
doneTasks: [],
activeTaskId: null,
row: null,
viewDialogVisible: false
}
@@ -224,6 +242,9 @@ const PROPOSAL_INFO = {
onOpen(row) {
this.row = row
this.visible = true
// 切换提案时先清空上一条提案的审核记录和展开状态,避免异步加载期间展示旧数据。
this.doneTasks = []
this.activeTaskId = null
this.getInfo()
this.getDoneTasks()
},
@@ -249,9 +270,12 @@ const PROPOSAL_INFO = {
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
const tasks = res.data || []
this.doneTasks = tasks
// 审核记录加载完成后默认全部折叠,由用户按需展开查看。
this.activeTaskId = null
// 通知外层页面已办节点数据已加载完成,便于当前环节做表单回显。
this.$emit("done-tasks", res.data)
this.$emit("done-tasks", tasks)
}
})
},
@@ -15,10 +15,10 @@ layout("/layouts/platform.html"){
<table-tool>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="openAdd">新增</el-button>
</table-tool>
<el-table :data="tableData" border stripe>
<el-table :data="tableData" border stripe @sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="50px"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="code" label="编码"></el-table-column>
<el-table-column prop="name" label="名称" sortable="custom"></el-table-column>
<el-table-column prop="code" label="编码" sortable="custom"></el-table-column>
<el-table-column label="操作" width="150px">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
@@ -23,12 +23,12 @@ layout("/layouts/platform.html"){
<table-tool>
<el-button type="primary" size="small" icon="el-icon-refresh" @click="syncSysUnit">同步系统单位</el-button>
</table-tool>
<el-table :data="tableData" border ref="tableRef" height="100%">
<el-table :data="tableData" border ref="tableRef" height="100%" @sort-change="pageOrder">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column prop="name" label="单位名称" show-overflow-tooltip></el-table-column>
<el-table-column prop="code" label="单位编码" width="150px"></el-table-column>
<el-table-column prop="name" label="单位名称" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column prop="code" label="单位编码" sortable="custom" width="150px"></el-table-column>
<!-- <el-table-column prop="branchSchoolLeader" label="分管校领导及联系方式" width="180px"></el-table-column>-->
<el-table-column prop="unitLeader" label="单位领导及联系方式" width="180px" show-overflow-tooltip></el-table-column>
<el-table-column prop="unitLeader" label="单位领导及联系方式" sortable="custom" width="180px" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="350px">
<template slot-scope="{row}">
<!-- <el-button size="mini" type="primary" @click="$refs.userRef.onOpen(row.id,true)">设置分管校领导</el-button>-->
@@ -196,26 +196,26 @@ layout("/layouts/platform.html"){
</div>
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%" row-key="id">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="提案编号" prop="code" width="100px" show-overflow-tooltip></el-table-column>
<el-table-column label="提案名称" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column label="提案人" prop="createUserName"></el-table-column>
<el-table-column label="提案类别" prop="typeName"></el-table-column>
<el-table-column label="代表团" prop="delegationName"></el-table-column>
<el-table-column label="立案结果" prop="caseFilingResult">
<el-table-column label="提案编号" prop="code" sortable="custom" width="100px" show-overflow-tooltip></el-table-column>
<el-table-column label="提案名称" prop="name" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="提案人" prop="createUserName" sortable="custom"></el-table-column>
<el-table-column label="提案类别" prop="typeName" sortable="custom"></el-table-column>
<el-table-column label="代表团" prop="delegationName" sortable="custom"></el-table-column>
<el-table-column label="立案结果" prop="caseFilingResult" sortable="custom">
<template scope="{row}">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT" :value="row.caseFilingResult"></dict-tag>
</template>
</el-table-column>
<el-table-column label="是否并案" prop="isConsolidation" width="100">
<el-table-column label="是否并案" prop="isConsolidation" sortable="custom" width="100">
<template scope="{row}">
<el-tag size="mini" v-if="row.isConsolidation" type="success"></el-tag>
<el-tag size="mini" v-else type="danger"></el-tag>
</template>
</el-table-column>
<el-table-column label="主办单位" prop="masterUnitName" show-overflow-tooltip></el-table-column>
<el-table-column label="协办单位" prop="slaveUnitNames" show-overflow-tooltip></el-table-column>
<el-table-column prop="curTaskName" label="当前节点"></el-table-column>
<el-table-column prop="instanceState" label="流程状态">
<el-table-column label="主办单位" prop="masterUnitName" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="协办单位" prop="slaveUnitNames" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column prop="curTaskName" label="当前节点" sortable="custom"></el-table-column>
<el-table-column prop="instanceState" label="流程状态" sortable="custom">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
size="small"></enum-tag>
@@ -137,6 +137,7 @@ layout("/layouts/platform.html"){
:key="column.key"
:min-width="column.width"
:fixed="column.fixed"
sortable="custom"
show-overflow-tooltip
v-for="column in tableColumns"
v-if="column.visible !== false"
@@ -1,4 +1,4 @@
<!--#
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
@@ -31,7 +31,8 @@ layout("/layouts/platform.html"){
:prop="column.prop"
:key="column.key"
:min-width="column.width"
:fixed="column.fixed"
:fixed="column.fixed"
sortable="custom"
show-overflow-tooltip
v-for="column in tableColumns"
v-if="column.visible !== false"
@@ -1,4 +1,4 @@
<!--#
<!--#
layout("/layouts/platform.html"){
#-->
<style>
@@ -204,11 +204,12 @@ layout("/layouts/platform.html"){
</div>
<el-button size="mini" type="primary" @click="exportStatistics">导出统计数据</el-button>
</div>
<el-table :data="statisticsRows" border stripe style="width: 100%" v-loading="tableLoading">
<el-table-column prop="dimension" label="统计维度" width="180"></el-table-column>
<el-table-column prop="itemName" label="分类项" min-width="260" show-overflow-tooltip></el-table-column>
<el-table-column prop="count" label="数量" width="120"></el-table-column>
<el-table-column prop="rate" label="占比" width="120"></el-table-column>
<el-table :data="statisticsRows" border stripe style="width: 100%" v-loading="tableLoading"
@sort-change="statisticsOrder">
<el-table-column prop="dimension" label="统计维度" sortable="custom" width="180"></el-table-column>
<el-table-column prop="itemName" label="分类项" sortable="custom" min-width="260" show-overflow-tooltip></el-table-column>
<el-table-column prop="count" label="数量" sortable="custom" width="120"></el-table-column>
<el-table-column prop="rate" label="占比" sortable="custom" width="120"></el-table-column>
</el-table>
</el-card>
@@ -220,14 +221,15 @@ layout("/layouts/platform.html"){
</div>
<el-button size="mini" type="primary" @click="exportAnalysis">导出分析数据</el-button>
</div>
<el-table :data="analysisRows" border stripe style="width: 100%" v-loading="tableLoading">
<el-table-column prop="dimension" label="统计维度" width="160"></el-table-column>
<el-table-column prop="total" label="总量" width="100"></el-table-column>
<el-table-column prop="categoryCount" label="分类数量" width="120"></el-table-column>
<el-table-column prop="topItem" label="最高项" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="topCount" label="最高数量" width="120"></el-table-column>
<el-table-column prop="topRate" label="最高占比" width="120"></el-table-column>
<el-table-column prop="analysis" label="分析结论" min-width="320" show-overflow-tooltip></el-table-column>
<el-table :data="analysisRows" border stripe style="width: 100%" v-loading="tableLoading"
@sort-change="analysisOrder">
<el-table-column prop="dimension" label="统计维度" sortable="custom" width="160"></el-table-column>
<el-table-column prop="total" label="量" sortable="custom" width="100"></el-table-column>
<el-table-column prop="categoryCount" label="分类数量" sortable="custom" width="120"></el-table-column>
<el-table-column prop="topItem" label="最高项" sortable="custom" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="topCount" label="最高数量" sortable="custom" width="120"></el-table-column>
<el-table-column prop="topRate" label="最高占比" sortable="custom" width="120"></el-table-column>
<el-table-column prop="analysis" label="分析结论" sortable="custom" min-width="320" show-overflow-tooltip></el-table-column>
</el-table>
</el-card>
@@ -283,7 +285,11 @@ layout("/layouts/platform.html"){
return {
pageForm: {
sessionId: "",
dimension: ""
dimension: "",
statisticsOrderName: "",
statisticsOrderBy: "",
analysisOrderName: "",
analysisOrderBy: ""
},
dimensionOptions: [
{label: "提案人单位", value: "提案人单位"},
@@ -320,6 +326,18 @@ layout("/layouts/platform.html"){
this.listSession()
},
methods: {
// 统计数据表使用独立排序参数,避免与分析数据表的排序状态相互覆盖。
statisticsOrder(column) {
this.pageForm.statisticsOrderName = column.prop
this.pageForm.statisticsOrderBy = column.order
this.doSearch()
},
// 分析数据表使用独立排序参数,两个接口刷新时分别应用各自白名单。
analysisOrder(column) {
this.pageForm.analysisOrderName = column.prop
this.pageForm.analysisOrderBy = column.order
this.doSearch()
},
toggleChartView() {
this.showChart = !this.showChart
if (this.showChart) {
@@ -89,6 +89,7 @@ layout("/layouts/platform.html"){
:key="column.key"
:min-width="column.width"
:fixed="column.fixed"
sortable="custom"
show-overflow-tooltip
v-for="column in tableColumns"
v-if="column.visible !== false"
@@ -206,6 +206,7 @@ layout("/layouts/platform.html"){
:key="column.key"
:min-width="column.width"
:fixed="column.fixed"
sortable="custom"
show-overflow-tooltip
v-for="column in tableColumns"
v-if="column.visible !== false"
@@ -214,6 +215,11 @@ layout("/layouts/platform.html"){
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
:value="row.caseFilingResult"></dict-tag>
</template>
<!-- 立案类型在数据库中保存字典编码,列表统一转换为中文名称展示。 -->
<template v-else-if="column.prop === 'caseFilingType'" scope="{row}">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="row.caseFilingType"></dict-tag>
</template>
<template v-else-if="column.prop === 'merge'" scope="{row}">
<el-tag v-if="row.merge" size="small"></el-tag>
<el-tag v-else type="warning" size="small"></el-tag>
@@ -14,7 +14,8 @@ layout("/layouts/platform.html"){
<el-card shadow="never" class="mt10">
<table-tool label="代表团列表" :columns.sync="tableColumns"></table-tool>
<el-table :data="tableData" ref="tableRef" header-align="center" style="width: 100%" row-key="id" show-summary>
<el-table :data="tableData" ref="tableRef" header-align="center" style="width: 100%" row-key="id"
show-summary @sort-change="pageOrder">
<el-table-column
:index="indexMethod"
label="序号"
@@ -28,6 +29,7 @@ layout("/layouts/platform.html"){
:key="column.key"
:min-width="column.width"
:fixed="column.fixed"
sortable="custom"
show-overflow-tooltip
v-for="column in tableColumns"
v-if="column.visible !== false"
@@ -61,6 +63,30 @@ layout("/layouts/platform.html"){
</div>
</el-dialog>
</div>
<!-- 提案明细组件模板独立放在页面中,避免在JavaScript中使用模板字符串。 -->
<script type="text/x-template" id="proposal-table-template" nonce="${cspNonce!}">
<div>
<el-table :data="tableData" ref="tableRef" header-align="center" style="width: 100%" row-key="id"
@sort-change="pageOrder">
<el-table-column label="序号" width="50px" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="提案编号" prop="code" sortable="custom" width="200px"></el-table-column>
<el-table-column label="提案名称" prop="name" sortable="custom"></el-table-column>
<el-table-column label="提案人" prop="createUserName" sortable="custom"></el-table-column>
<el-table-column label="提案类别" prop="typeName" sortable="custom"></el-table-column>
<el-table-column label="届次" prop="sessionName" sortable="custom"></el-table-column>
<el-table-column label="代表团" prop="delegationName" sortable="custom"></el-table-column>
<el-table-column label="当前节点" prop="curTaskName" sortable="custom"></el-table-column>
<el-table-column label="操作" fixed="right" width="100px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="$emit('view-single-proposal',row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div>
</script>
<script nonce="${cspNonce!}">
<!--#include("proposalTable.js"){}#-->
<!--#include("../../common/info.js"){}#-->
@@ -128,7 +154,9 @@ layout("/layouts/platform.html"){
pageData() {
this.$axios
.post(loc() + "/pageData", {
sessionId: this.sessionId
sessionId: this.sessionId,
pageOrderName: this.pageForm.pageOrderName,
pageOrderBy: this.pageForm.pageOrderBy
})
.then((res) => {
if (res.code === 0) {
@@ -1,24 +1,6 @@
const PROPOSAL_TABLE = {
template: `
<div>
<el-table :data="tableData" ref="tableRef" header-align="center" style="width: 100%" row-key="id" @sort-change="pageOrder">
<el-table-column label="序号" width="50px" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="提案编号" prop="code" width="200px"></el-table-column>
<el-table-column label="提案名称" prop="name"></el-table-column>
<el-table-column label="提案人" prop="createUserName"></el-table-column>
<el-table-column label="提案类别" prop="typeName"></el-table-column>
<el-table-column label="届次" prop="sessionName"></el-table-column>
<el-table-column label="代表团" prop="delegationName"></el-table-column>
<el-table-column label="当前节点" prop="curTaskName"></el-table-column>
<el-table-column label="操作" fixed="right" width="100px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="$emit('view-single-proposal',row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div>
`,
// 组件模板由页面中的text/x-template节点提供,避免使用JavaScript模板字符串。
template: "#proposal-table-template",
props: {
sessionId: {
type: String,
@@ -94,35 +94,35 @@ layout("/layouts/platform.html"){
<table-tool></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%" row-key="id">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="提案编号" prop="code" width="120px" sortable></el-table-column>
<el-table-column label="提案名称" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column label="提案人" prop="createUserName"></el-table-column>
<el-table-column label="届次" prop="sessionName"></el-table-column>
<el-table-column label="代表团" prop="delegationName"></el-table-column>
<el-table-column label="立案结果" prop="caseFilingResult">
<el-table-column label="提案编号" prop="code" width="120px" sortable="custom"></el-table-column>
<el-table-column label="提案名称" prop="name" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="提案人" prop="createUserName" sortable="custom"></el-table-column>
<el-table-column label="届次" prop="sessionName" sortable="custom"></el-table-column>
<el-table-column label="代表团" prop="delegationName" sortable="custom"></el-table-column>
<el-table-column label="立案结果" prop="caseFilingResult" sortable="custom">
<template scope="{row}">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT" :value="row.caseFilingResult"></dict-tag>
</template>
</el-table-column>
<el-table-column label="立案类型" prop="caseFilingType">
<el-table-column label="立案类型" prop="caseFilingType" sortable="custom">
<template scope="{row}">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="row.caseFilingType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="是否并案" prop="merge">
<el-table-column label="是否并案" prop="merge" sortable="custom">
<template slot-scope="{row}">
<el-tag v-if="row.merge" size="small"></el-tag>
<el-tag v-else type="warning" size="small"></el-tag>
</template>
</el-table-column>
<el-table-column label="承办单位" prop="undertakeUnits" show-overflow-tooltip>
<el-table-column label="承办单位" prop="undertakeUnits" sortable="custom" show-overflow-tooltip>
<template slot-scope="{row}">
{{ getUndertakeDisplay(row) }}
</template>
</el-table-column>
<el-table-column prop="curTaskName" label="当前节点"></el-table-column>
<el-table-column prop="instanceState" label="流程状态">
<el-table-column prop="curTaskName" label="当前节点" sortable="custom"></el-table-column>
<el-table-column prop="instanceState" label="流程状态" sortable="custom">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
size="small"></enum-tag>
@@ -24,19 +24,19 @@ layout("/layouts/platform.html"){
</el-card>
<el-card shadow="never">
<el-table :data="tableData">
<el-table :data="tableData" @sort-change="pageOrder">
<el-table-column label="序号" type="index" width="50"></el-table-column>
<el-table-column label="单位名称" prop="unitName" width="400" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="提案总数" prop="sum" sortable></el-table-column>
<el-table-column label="单位名称" prop="unitName" width="400" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="提案总数" prop="sum" sortable="custom"></el-table-column>
<el-table-column label="主办提案">
<el-table-column label="提案数量" prop="masterSum" sortable></el-table-column>
<el-table-column label="已答复" prop="masterReplySum" sortable></el-table-column>
<el-table-column label="未答复" prop="masterNoReplySum" sortable></el-table-column>
<el-table-column label="提案数量" prop="masterSum" sortable="custom"></el-table-column>
<el-table-column label="已答复" prop="masterReplySum" sortable="custom"></el-table-column>
<el-table-column label="未答复" prop="masterNoReplySum" sortable="custom"></el-table-column>
</el-table-column>
<el-table-column label="协办提案">
<el-table-column label="提案数量" prop="slaveSum" sortable></el-table-column>
<el-table-column label="已答复" prop="slaveReplySum" sortable v-if="slaveNeedReply"></el-table-column>
<el-table-column label="未答复" prop="slaveNoReplySum" sortable v-if="slaveNeedReply"></el-table-column>
<el-table-column label="提案数量" prop="slaveSum" sortable="custom"></el-table-column>
<el-table-column label="已答复" prop="slaveReplySum" sortable="custom" v-if="slaveNeedReply"></el-table-column>
<el-table-column label="未答复" prop="slaveNoReplySum" sortable="custom" v-if="slaveNeedReply"></el-table-column>
</el-table-column>
</el-table>
</el-card>
@@ -22,14 +22,14 @@ layout("/layouts/platform.html"){
</el-card>
<el-card shadow="never">
<el-table :data="tableData">
<el-table :data="tableData" @sort-change="pageOrder">
<el-table-column label="序号" type="index" width="50"></el-table-column>
<el-table-column label="单位名称" prop="unitName" width="400" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="提案总数" prop="sum" sortable></el-table-column>
<el-table-column label="主办提案数" prop="masterSum" sortable></el-table-column>
<el-table-column label="协办提案数" prop="slaveSum" sortable></el-table-column>
<el-table-column label="单位名称" prop="unitName" width="400" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="提案总数" prop="sum" sortable="custom"></el-table-column>
<el-table-column label="主办提案数" prop="masterSum" sortable="custom"></el-table-column>
<el-table-column label="协办提案数" prop="slaveSum" sortable="custom"></el-table-column>
<template v-for="dict in dict.type.PROPOSAL_FEEDBACK">
<el-table-column :label="dict.label" :prop="dict.code" sortable></el-table-column>
<el-table-column :label="dict.label" :prop="dict.code" sortable="custom"></el-table-column>
</template>
</el-table>
</el-card>

Some files were not shown because too many files have changed in this diff Show More