体育活动成绩统计,协会经费

This commit is contained in:
2026-10-23 16:13:35 +08:00
parent 2ac7b0838a
commit 398dddc370
21 changed files with 608 additions and 251 deletions
@@ -295,6 +295,14 @@ public class SysUnitController {
}
}
/**
* 获取以指定单位为根的单位树。
*
* @param pid 根单位 ID;未传时使用系统根单位 0
* @param req 当前 HTTP 请求
* @return Result 对象;code 为 0 时 data 为树数组,节点包含 id、parentId、name、children、unitTypeCode;
* unitTypeCode 为 0 表示其他类、1 表示部门类,历史空值仅在返回时按 0 展示
*/
@At("/tree")
@Ok("json")
@SaCheckLogin
@@ -317,30 +325,45 @@ public class SysUnitController {
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(
Map.of(
"unitTypeCode", unit.getUnitTypeCode()
// 历史手工新建单位可能未设置类型,展示时按其他类处理,避免空值使整棵树加载失败。
"unitTypeCode", unit.getUnitTypeCode() == null ? 0 : unit.getUnitTypeCode()
)
));
}
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
return Result.success(treeList);
} catch (Exception e) {
log.error("加载单位树失败", e);
return Result.error();
}
}
/**
* 新建当前单位的下设单位。
*
* @param unit 表单数据;name 为单位名称,unitcode 为唯一编码,unitLevel 为单位等级,unionId 为所属工会;
* unitTypeCode 为部门类别,0 表示其他类、1 表示部门类,未传时由 service 按其他类保存
* @param parentId 当前选中上级单位的 ID,必须对应已存在的单位
* @param req 当前 HTTP 请求
* @return Result 对象;code 为 0 表示保存成功,非 0 表示失败,msg 为提示信息,data 为空
*/
@At
@Ok("json")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("sys.manager.unit.add")
@SLog(tag = "新建单位", msg = "单位名称:${args[0].name}")
public Object addDo(@Param("..") Sys_unit unit, @Param("parentId") String parentId, HttpServletRequest req) {
try {
if ("root".equals(parentId)) {
parentId = "";
if (Strings.isBlank(unit.getName()) || Strings.isBlank(unit.getUnitcode()) || Strings.isBlank(parentId) || "root".equals(parentId)) {
return Result.error("单位名称、单位编码和上级单位不能为空");
}
unit.setCreatedBy(SecurityUtil.getUserId());
sysUnitService.save(unit, parentId);
return Result.success();
} catch (BaseException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
log.error("新建单位失败", e);
return Result.error();
}
}
@@ -2,6 +2,7 @@ package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RedisConstant;
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.service.impl.BaseServiceImpl;
@@ -46,27 +47,36 @@ public class SysUnitServiceImpl extends BaseServiceImpl<Sys_unit> implements Sys
}
/**
* 新增单位
* 新增下设单位,校验上级关系并补齐影响树展示的字段。
*
* @param unit
* @param pid
* @param unit 单位表单;unitcode 同时作为单位 ID,unitTypeCode 未传时保存为其他类(0),不自动认定为部门类(1)
* @param pid 已存在的上级单位 ID,不允许为空,避免新增单位脱离当前单位树
* @throws BaseException 上级单位为空、不存在或部门类别不是 0/1 时拒绝保存
*/
@Aop(TransAop.READ_COMMITTED)
public void save(Sys_unit unit, String pid) {
String path = "";
if (!Strings.isEmpty(pid)) {
Sys_unit pp = this.fetch(pid);
path = pp.getPath();
if (StrUtil.isBlank(pid)) {
throw new BaseException("请选择上级单位");
}
if(StrUtil.isNotBlank(path)) {
unit.setPath(getSubPath("sys_unit", "path", path));
Sys_unit parentUnit = this.fetch(pid);
if (parentUnit == null) {
throw new BaseException("上级单位不存在,请刷新页面后重试");
}
// 手工创建不具备信息中心的部门分类依据,仅对缺失类型补其他类,保留明确传入的部门类设置。
if (unit.getUnitTypeCode() == null) {
unit.setUnitTypeCode(0);
} else if (!Integer.valueOf(0).equals(unit.getUnitTypeCode()) && !Integer.valueOf(1).equals(unit.getUnitTypeCode())) {
throw new BaseException("部门类别只能为其他类或部门类");
}
if (unit.getUnitLevel() == null && parentUnit.getUnitLevel() != null) {
unit.setUnitLevel(parentUnit.getUnitLevel() + 1);
}
// 与单位同步逻辑一致:上级路径缺失时也生成有效路径,避免新记录继续保留空路径。
unit.setPath(getSubPath("sys_unit", "path", parentUnit.getPath()));
unit.setId(unit.getUnitcode());
unit.setParentId(pid);
dao().fastInsert(unit);
if (!Strings.isEmpty(pid)) {
this.update(Chain.make("hasChildren", true), Cnd.where("id", "=", pid));
}
this.update(Chain.make("hasChildren", true), Cnd.where("id", "=", pid));
}
/**
@@ -10,10 +10,8 @@ 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_unit;
import com.budwk.app.sys.services.SysUnitService;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicUnitService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
@@ -28,8 +26,11 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @ClassName ActivityBasicUnitController
@@ -46,6 +47,8 @@ public class ActivityBasicUnitController {
private BaseService baseService;
@Inject
private Dao dao;
@Inject
private ActivityBasicUnitService activityBasicUnitService;
@At("")
@@ -62,6 +65,8 @@ public class ActivityBasicUnitController {
cnd.and("parentId", "=", "1");
} else {
cnd.and("parentId", "=", parentId);
// 系统根可能以自身为父级,列表只显示其下级单位,避免把根本身当作下级操作。
cnd.and("id", "!=", parentId);
}
if (StrUtil.isNotBlank(pageForm.getSearchName()) && StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
cnd.and(Cnd.likeEX(pageForm.getSearchName(), pageForm.getSearchKeyword()));
@@ -82,22 +87,39 @@ public class ActivityBasicUnitController {
cnd.asc("unitcode");
List<ActivityBasicUnit> list = baseService.dao().query(ActivityBasicUnit.class, cnd);
Set<String> unitIds = list.stream().map(ActivityBasicUnit::getId).collect(Collectors.toSet());
String virtualRootId = "_activity_unit_root_";
List<TreeNode<String>> nodeList = CollUtil.newArrayList();
for (int i = 0; i < list.size(); i++) {
nodeList.add(new TreeNode<>(list.get(i).getId(), list.get(i).getParentId(), list.get(i).getName(), i)
.setExtra(
Map.of(
"unitTypeCode", list.get(i).getUnitTypeCode()
)
));
ActivityBasicUnit unit = list.get(i);
String parentId = unit.getParentId();
// 系统根可能自引用或使用空父级;只在构树时挂到虚拟根,保留同步后的真实父级关系。
if (StrUtil.isBlank(parentId) || unit.getId().equals(parentId) || !unitIds.contains(parentId)) {
parentId = virtualRootId;
}
// 源单位分类允许为空,使用支持空值的Map避免同步后整棵单位树加载失败。
Map<String, Object> extra = new HashMap<>();
extra.put("unitTypeCode", unit.getUnitTypeCode());
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i).setExtra(extra));
}
List<Tree<String>> treeList = TreeUtil.build(nodeList, StrUtil.blankToDefault(pid, "0"));
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
return Result.success(treeList);
} catch (Exception e) {
return Result.error();
}
}
/**
* 无请求参数,按当前系统单位资料更新活动单位。
* 返回 Result,data 中 insertCount/updateCount/pendingUnionCount 分别表示新增、更新及新增待配置工会数量。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("activity.basic.unit")
public Result updateUnits() {
return Result.success(activityBasicUnitService.syncUnits());
}
private List<ActivityBasicUnit> child(String parentId) {
List<ActivityBasicUnit> units = dao.query(ActivityBasicUnit.class, Cnd.where("parentId", "=", parentId));
units.forEach(unit -> {
@@ -0,0 +1,19 @@
package com.budwk.app.zhgh.activity.basic.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import org.nutz.lang.util.NutMap;
/**
* 活动单位基础资料同步,活动工会归属仍由活动模块独立维护。
*/
public interface ActivityBasicUnitService extends BaseService<ActivityBasicUnit> {
/**
* 将系统单位同步到活动单位,不删除活动单位,不覆盖已有活动单位的工会归属。
*
* @return NutMap;insertCount 为新增单位数,updateCount 为更新基础资料的已有单位数,
* pendingUnionCount 为新增单位中未能匹配活动工会、需要配置归属的数量
*/
NutMap syncUnits();
}
@@ -0,0 +1,93 @@
package com.budwk.app.zhgh.activity.basic.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicUnitService;
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.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class ActivityBasicUnitServiceImpl extends BaseServiceImpl<ActivityBasicUnit> implements ActivityBasicUnitService {
public ActivityBasicUnitServiceImpl(Dao dao) {
super(dao);
}
/**
* 无请求参数,按 sys_unit.id 同步全部系统单位;返回新增数、更新数和新增单位待配置活动工会数。
* 已有记录只更新基础字段,连原有的空工会归属也保留,避免覆盖活动模块人工维护的组成单位关系。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap syncUnits() {
List<Sys_unit> sourceUnits = dao().query(Sys_unit.class, Cnd.NEW());
Set<String> existingIds = dao().query(ActivityBasicUnit.class, Cnd.NEW()).stream()
.map(ActivityBasicUnit::getId).collect(Collectors.toSet());
Set<String> activityUnionIds = dao().query(ActivityBasicUnion.class, Cnd.NEW()).stream()
.map(ActivityBasicUnion::getId).collect(Collectors.toSet());
int insertCount = 0;
int updateCount = 0;
int pendingUnionCount = 0;
String operatorId = SecurityUtil.getUserId();
long updateTime = System.currentTimeMillis();
for (Sys_unit sourceUnit : sourceUnits) {
if (StrUtil.isBlank(sourceUnit.getId())) {
throw new BaseException("系统单位存在空ID,本次同步已取消,请先检查系统单位资料");
}
Chain fields = buildBasicFields(sourceUnit)
.add("updatedBy", operatorId).add("updatedAt", updateTime);
if (existingIds.contains(sourceUnit.getId())) {
// 更新列中不包含 unionId、unionGroupId 和 id,保留现有归属及报名记录使用的单位ID。
dao().update(ActivityBasicUnit.class, fields, Cnd.where("id", "=", sourceUnit.getId()));
updateCount++;
} else {
// 新单位仅继承已存在于活动工会表的归属,不能将普通工会ID直接当作有效活动工会。
String unionId = StrUtil.isNotBlank(sourceUnit.getUnionId()) && activityUnionIds.contains(sourceUnit.getUnionId())
? sourceUnit.getUnionId() : null;
if (unionId == null) {
pendingUnionCount++;
}
fields.add("id", sourceUnit.getId()).add("unionId", unionId)
.add("createdBy", operatorId).add("createdAt", updateTime).add("delFlag", false);
// 按列插入避开实体的 UUID 生成拦截器,确保活动单位ID始终等于系统单位ID。
dao().insert(ActivityBasicUnit.class, fields);
existingIds.add(sourceUnit.getId());
insertCount++;
}
}
return NutMap.NEW().addv("insertCount", insertCount).addv("updateCount", updateCount)
.addv("pendingUnionCount", pendingUnionCount);
}
/**
* 只列出允许从系统单位更新的基础资料;工会、小组归属和审计创建信息不属于基础同步字段。
* 父级、路径与层级沿用源值,树展示需要的根节点适配在接口内存中完成,不改写单位关系。
*
* @param unit sys_unit 中的完整单位记录
* @return 可供 DAO 按列新增或更新的 Chain,不含单位ID和工会归属
*/
private Chain buildBasicFields(Sys_unit unit) {
return Chain.make("parentId", unit.getParentId()).add("path", unit.getPath())
.add("name", unit.getName()).add("aliasName", unit.getAliasName()).add("unitcode", unit.getUnitcode())
.add("note", unit.getNote()).add("address", unit.getAddress()).add("telephone", unit.getTelephone())
.add("email", unit.getEmail()).add("website", unit.getWebsite()).add("location", unit.getLocation())
.add("hasChildren", unit.isHasChildren()).add("logo", unit.getLogo()).add("unitLevel", unit.getUnitLevel())
.add("divisionCollegeCode", unit.getDivisionCollegeCode()).add("institutionType", unit.getInstitutionType())
.add("unitType", unit.getUnitType()).add("unitTypeCode", unit.getUnitTypeCode());
}
}
@@ -12,7 +12,7 @@ import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.services.SysUnionService;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchool;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolEvent;
import com.budwk.app.zhgh.activity.sports.service.ActivitySportsService;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
@@ -53,6 +53,9 @@ public class ActivitySportsScoreStatisticsController {
@Inject
private BaseService baseService;
@Inject
private ActivitySportsService activitySportsService;
// @Inject
// private OfficeTemplateUtil officeTemplateUtil;
@@ -246,7 +249,10 @@ public class ActivitySportsScoreStatisticsController {
@At
@SaCheckLogin
public Result getActivitys(Integer year) {
List<ActivitySchool> schoolList = baseService.dao().query(ActivitySchool.class, Cnd.where("YEAR(applyEndTime)", "=", year));
// 活动名称下拉只列出正式活动,与年度统计口径一致,过滤掉活动模板(模板标记 + 工作模板表双重判断)。
List<ActivitySchool> schoolList = baseService.dao().query(ActivitySchool.class, Cnd.where("YEAR(applyEndTime)", "=", year)
.and("templateStatus", "!=", ActivitySchool.TEMPLATE_STATUS_SET)
.and(new Static("id NOT IN (SELECT id FROM sys_home_template)")));
return Result.success(schoolList);
}
@@ -331,7 +337,10 @@ public class ActivitySportsScoreStatisticsController {
Dao dao = sysUnionService.dao();
List<Sys_union> unionList = sysUnionService.query();
List<ActivitySchool> activitySchoolList = dao.query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year));
// 年度统计只取正式活动:既排除模板标记,也排除工作模板表中登记的活动(历史模板未回填标记位)。
List<ActivitySchool> activitySchoolList = dao.query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year)
.and("templateStatus", "!=", ActivitySchool.TEMPLATE_STATUS_SET)
.and(new Static("id NOT IN (SELECT id FROM sys_home_template)")));
List<String> labelList = activitySchoolList.stream().map(v -> v.getName()).collect(Collectors.toList());
labelList.add(0, "分工会");
@@ -363,64 +372,16 @@ public class ActivitySportsScoreStatisticsController {
}
/**
* @param isMenWomen 1 为年度男子团体总分,2 为年度女子团体总分
* @param year 活动报名开始时间所属年度
* @return 成功结果中 label 为列名列表,score 为包含分工会、各活动积分和总分的行列表
*/
@At
@ApiOperation("年度男子团体总分/年度女子团体总分")
@SaCheckPermission("activity.score.statistics")
public Result getYear8(Integer isMenWomen, Integer year) {
ArrayList<Map> resultMap = new ArrayList<>();
// int isMenWomen = sex.equals("男") ? 1 : 2;
Dao dao = sysUnionService.dao();
List<Sys_union> unionList = sysUnionService.query();
List<ActivitySchool> activityList = dao.query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year));
List<String> activityIdList = activityList.stream().map(ActivitySchool::getId).collect(Collectors.toList());
List<String> activityNameList = activityList.stream().map(ActivitySchool::getName).collect(Collectors.toList());
List<String> eventIdList = dao.query(ActivitySchoolEvent.class, Cnd.where("activityId", "in", activityIdList)).stream().map(v -> v.getEventId()).collect(Collectors.toList());
activityNameList.add(0, "分工会");
Sql sql = Sqls.create("""
SELECT
ae.isMenWomen,
ae.isInterest,
ar.integral,
ar.unionId,
ar.activityId
FROM
activity_event ae
LEFT JOIN activity_results ar ON ar.eventId = ae.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("ae.id", "in", eventIdList);
cnd.and("ar.activityId", "in", activityIdList);
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
dao.execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
unionList.forEach(u -> {
NutMap unionMap = Lang.obj2nutmap(u);
AtomicReference<Double> scoreSum = new AtomicReference<>((double) 0);
activityList.forEach(a -> {
double sexScore = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getInt("isMenWomen") == isMenWomen && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
double qwScore = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getInt("isInterest") == 1 && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
scoreSum.updateAndGet(v -> v + sexScore + qwScore * 0.5);
unionMap.put(a.getName(), sexScore + (qwScore * 0.5));
});
unionMap.put("总分", scoreSum.get());
unionMap.put("分工会", unionMap.getString("name"));
resultMap.add(unionMap);
});
activityNameList.add("总分");
return Result.success(Map.of("label", activityNameList, "score", resultMap));
return Result.success(activitySportsService.getAnnualGenderResults(isMenWomen, year));
}
public static Double getDouble(NutMap o) {
return o.getDouble("score");
@@ -12,7 +12,7 @@ import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.services.SysUnionService;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchool;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolEvent;
import com.budwk.app.zhgh.activity.sports.service.ActivitySportsService;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
@@ -48,56 +48,24 @@ public class ActivitySportsStatisticsTypeExportController {
@Inject
private BaseService baseService;
@Inject
private ActivitySportsService activitySportsService;
/**
* 导出年度男子或女子团体积分,无返回值,文件直接写入 HTTP 响应。
*
* @param isMenWomen 1 导出年度男子团体总分,2 导出年度女子团体总分
* @param year 活动报名开始时间所属年度
* @param response 用于输出包含名次、分工会、各活动积分和总分的 XSSF Excel 文件
*/
@Ok("void")
@At
@ApiOperation("年度男子团体总分/年度女子团体总分")
@SaCheckPermission("activity.score.statistics")
public void getYear8(Integer isMenWomen, Integer year, HttpServletResponse response) {
ArrayList<Map> resultMap = new ArrayList<>();
List<Sys_union> unionList = sysUnionService.query();
List<ActivitySchool> activityList = baseService.dao().query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year));
List<String> activityIdList = activityList.stream().map(ActivitySchool::getId).collect(Collectors.toList());
List<String> activityNameList = activityList.stream().map(ActivitySchool::getName).collect(Collectors.toList());
List<String> eventIdList = baseService.dao().query(ActivitySchoolEvent.class, Cnd.where("activityId", "in", activityIdList)).stream().map(v -> v.getEventId()).collect(Collectors.toList());
activityNameList.add(0, "分工会");
Sql sql = Sqls.create("""
SELECT
ae.isMenWomen,
ae.isInterest,
ar.integral,
ar.unionId,
ar.activityId
FROM
activity_event ae
LEFT JOIN activity_results ar ON ar.eventId = ae.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("ae.id", "in", eventIdList);
cnd.and("ar.activityId", "in", activityIdList);
sql.setCondition(cnd);
List<NutMap> list = baseService.listMap(sql);
unionList.forEach(u -> {
NutMap unionMap = Lang.obj2nutmap(u);
AtomicReference<Double> scoreSum = new AtomicReference<>((double) 0);
activityList.forEach(a -> {
double sexScore = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getInt("isMenWomen") == isMenWomen && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
double qwScore = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getInt("isInterest") == 1 && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
scoreSum.updateAndGet(v -> v + (sexScore + (qwScore * 0.5)));
unionMap.put(a.getName(), sexScore + (qwScore * 0.5));
});
unionMap.put("总分", scoreSum.get());
unionMap.put("分工会", unionMap.getString("name"));
resultMap.add(unionMap);
});
activityNameList.add("总分");
NutMap annualResults = activitySportsService.getAnnualGenderResults(isMenWomen, year);
List<Map> resultMap = annualResults.getList("score", Map.class);
List<String> activityNameList = annualResults.getList("label", String.class);
resultMap.sort(Comparator.comparing((Map m) -> (new BigDecimal(m.get("总分").toString()))).reversed());
for (int i = 0; i < resultMap.size(); i++) {
@@ -131,7 +99,10 @@ public class ActivitySportsStatisticsTypeExportController {
Dao dao = sysUnionService.dao();
List<Sys_union> unionList = sysUnionService.query();
List<ActivitySchool> activitySchoolList = dao.query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year));
// 与页面查询保持一致,导出时同样过滤掉活动模板(标记位 + 工作模板表双重判断)。
List<ActivitySchool> activitySchoolList = dao.query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year)
.and("templateStatus", "!=", ActivitySchool.TEMPLATE_STATUS_SET)
.and(new Static("id NOT IN (SELECT id FROM sys_home_template)")));
List<String> labelList = activitySchoolList.stream().map(v -> v.getName()).collect(Collectors.toList());
labelList.add(0, "分工会");
@@ -4,6 +4,7 @@ package com.budwk.app.zhgh.activity.sports.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchool;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolEvent;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
@@ -33,5 +34,15 @@ public interface ActivitySportsService extends BaseService<ActivitySchool> {
*/
void cancelTemplate(String id);
/**
* 按老项目规则计算年度男子或女子团体积分,供页面查询和 Excel 导出共同使用。
*
* @param isMenWomen 统计性别,1 为男子、2 为女子;单一活动不按项目性别筛选
* @param year 报名开始时间所属年度,例如 2026
* @return NutMap:label 为“分工会、各活动名称、总分”的列名列表;score 为各分工会的积分行列表,
* 每行以活动名称存放该活动积分,以“分工会”和“总分”存放分工会名称及年度合计
*/
NutMap getAnnualGenderResults(Integer isMenWomen, Integer year);
void exportXlsx(String id, String unionId, HttpServletResponse response);
}
@@ -9,6 +9,7 @@ import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.DateUtil;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_home_template;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
@@ -26,15 +27,19 @@ 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.Static;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@@ -246,6 +251,69 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
Cnd.where("id", "=", id));
}
/**
* 按活动类型恢复老项目的年度积分口径,查询和导出使用同一份计算结果。
*
* @param isMenWomen 1 为男子年度积分,2 为女子年度积分
* @param year 按活动报名开始时间筛选的年度
* @return label 为列名列表,score 为以活动名称、分工会、总分作为字段的积分行列表
*/
@Override
public NutMap getAnnualGenderResults(Integer isMenWomen, Integer year) {
ArrayList<Map> resultMap = new ArrayList<>();
List<Sys_union> unionList = dao().query(Sys_union.class, null);
// 年度统计只取正式活动:既排除模板标记,也排除工作模板表中登记的活动(历史模板未回填标记位)。
List<ActivitySchool> activityList = dao().query(ActivitySchool.class, Cnd.where("YEAR(applyStartTime)", "=", year)
.and("templateStatus", "!=", ActivitySchool.TEMPLATE_STATUS_SET)
.and(new Static("id NOT IN (SELECT id FROM sys_home_template)")));
List<String> activityIdList = activityList.stream().map(ActivitySchool::getId).collect(Collectors.toList());
List<String> activityNameList = activityList.stream().map(ActivitySchool::getName).collect(Collectors.toList());
List<String> eventIdList = dao().query(ActivitySchoolEvent.class, Cnd.where("activityId", "in", activityIdList)).stream().map(v -> v.getEventId()).collect(Collectors.toList());
activityNameList.add(0, "分工会");
Sql sql = Sqls.create("""
SELECT
ae.isMenWomen,
ae.isInterest,
ar.integral,
ar.unionId,
ar.activityId
FROM
activity_event ae
LEFT JOIN activity_results ar ON ar.eventId = ae.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("ae.id", "in", eventIdList);
cnd.and("ar.activityId", "in", activityIdList);
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
unionList.forEach(u -> {
NutMap unionMap = Lang.obj2nutmap(u);
AtomicReference<Double> scoreSum = new AtomicReference<>((double) 0);
activityList.forEach(a -> {
if (Integer.valueOf(1).equals(a.getApplyType())) {
// 单一活动按分工会和活动累计全分,男子、女子年度榜均计入,不依赖项目性别或趣味标记。
double score = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
scoreSum.updateAndGet(v -> v + score);
unionMap.put(a.getName(), score);
} else {
// 综合类活动沿用原规则:对应性别积分,加上全部趣味项目积分的一半。
double sexScore = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getInt("isMenWomen") == isMenWomen && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
double qwScore = list.stream().filter(x -> x.getString("unionId").equals(u.getId()) && x.getInt("isInterest") == 1 && x.getString("activityId").equals(a.getId())).mapToDouble(h -> h.getDouble("integral")).sum();
scoreSum.updateAndGet(v -> v + sexScore + qwScore * 0.5);
unionMap.put(a.getName(), sexScore + (qwScore * 0.5));
}
});
unionMap.put("总分", scoreSum.get());
unionMap.put("分工会", unionMap.getString("name"));
resultMap.add(unionMap);
});
activityNameList.add("总分");
return NutMap.NEW().setv("label", activityNameList).setv("score", resultMap);
}
@Override
public void exportXlsx(String id, String unionId, HttpServletResponse response) {
try {
@@ -198,15 +198,26 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl<ClubPayRecord> imp
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name());
}
/** 普通菜单授权用户仅能查看其有效角色关联的社团;无关联时显式返回空范围。 */
/**
* 为缴费列表、协会下拉选项及状态设置追加当前用户所管理的协会范围,无返回值。
* 管理角色沿用项目现有口径:会长、副会长、秘书长、副秘书长、操作员;普通会员不纳入。
* @param cnd 当前业务查询条件,管理员保留原条件,无管理协会时追加恒假条件
* @param clubField 当前查询的协会ID字段,可包含表别名,如 cpr.clubId 或 c.id
*/
private void addPayClubScope(Cnd cnd, String clubField) {
if (!canManageAllPayClubs()) {
// 用户ID使用 SQL 参数绑定,社团集合交给 Cnd 处理,避免空集合被可选条件忽略。
// 用户ID和管理角色均使用 SQL 参数绑定;社团集合交给 Cnd 处理,空集合显式限制为无权限。
Sql scopeSql = Sqls.create("""
SELECT DISTINCT ur.clubId FROM sys_user_role ur
INNER JOIN sys_role r ON r.id = ur.roleId AND r.disabled = false
WHERE ur.userId = @userId AND ur.enable = true AND ur.clubId IS NOT NULL
""").setParam("userId", SecurityUtil.getUserId());
AND r.code IN (@presidentRole, @vicePresidentRole, @secretaryRole, @viceSecretaryRole, @operatorRole)
""").setParam("userId", SecurityUtil.getUserId())
.setParam("presidentRole", RoleConstant.CLUB_PRESIDENT.name())
.setParam("vicePresidentRole", RoleConstant.CLUB_VICE_PRESIDENT.name())
.setParam("secretaryRole", RoleConstant.CLUB_SECRETARY.name())
.setParam("viceSecretaryRole", RoleConstant.CLUB_VICE_SECRETARY.name())
.setParam("operatorRole", RoleConstant.CLUB_OPERATOR.name());
List<String> clubIds = listMap(scopeSql).stream()
.map(row -> row.getString("clubId"))
.filter(StrUtil::isNotBlank)
@@ -315,9 +315,9 @@ public class UnionReimburseApplyController {
}
/**
* 查询可申请社团经费报销的社团。
* 查询当前登录人所属的可申请报销协会,无请求参数,用户身份由服务端登录会话确定。
*
* @return 全部未解散社团列表
* @return Result.data 为已加入且未解散的协会列表,每项包含 id、clubName、clubCode;无所属协会时为空列表
*/
@At
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
@@ -63,9 +63,10 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
Result getBudgetBalance(String reimburseFundSource, String clubId, String unionId);
/**
* 查询未解散社团,供社团经费报销选择。
* 查询当前登录人已加入且未解散的协会,供协会经费报销选择和申请范围校验。
* 无请求参数,用户身份取自服务端登录会话。
*
* @return 全部未解散社团的ID、名称和编码列表
* @return 协会列表,每项包含 id、clubName、clubCode;未加入协会时返回空列表,同一协会只返回一次
*/
List<NutMap> listReimburseClubs();
@@ -894,6 +894,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
// return Result.error("请填写开户行");
// }
if (!needInvoiceDetails(unionReimburse)) {
// 慰问报销提交和重新提交时必须填写非空白事由,保存草稿仍沿用现有暂存规则。
if (StrUtil.isBlank(unionReimburse.getPaymentNotes())) {
return Result.error("请填写报销事由");
}
return validateInvoiceDetails(unionReimburse);
}
if (StrUtil.isBlank(unionReimburse.getActivityName())) {
@@ -1303,9 +1307,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
}
/**
* 查询未解散社团,社团选择和余额查询不按负责人归属过滤。
* 查询当前登录人已加入且未解散的协会,供所属协会选择、余额查询和申请校验共用。
* 无请求参数,会员归属使用服务端当前登录用户ID,不接受前端指定其他用户。
*
* @return 全部未解散社团的ID、名称和编码
* @return 协会列表,每项包含 id、clubName、clubCode;未加入协会时返回空列表,同一协会只返回一次
*/
@Override
public List<NutMap> listReimburseClubs() {
@@ -1313,8 +1318,13 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
SELECT c.id, c.clubName, c.clubCode
FROM sys_club c
WHERE c.dismiss = 0
AND EXISTS (
SELECT 1 FROM club_user cu
WHERE cu.clubId = c.id AND cu.userId = @userId
)
ORDER BY c.clubCode, c.id
""");
sql.setParam("userId", SecurityUtil.getUserId());
return listMap(sql);
}
@@ -1323,11 +1333,15 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (!"UNION_REIMBURSE_PROJECT_5".equals(form.getReimburseProject())) {
return Result.error("社团经费请选择社团活动");
}
// 协会经费必须选择本人已加入的有效协会,保存草稿也不能绕过所属协会范围。
if (StrUtil.isBlank(form.getClubId())) {
return Result.error("请选择所属协会");
}
NutMap club = listReimburseClubs().stream()
.filter(item -> Objects.equals(form.getClubId(), item.getString("id")))
.findFirst().orElse(null);
if (club == null) {
return Result.error("请选择有效的社团");
return Result.error("只能选择本人所在且未解散的协会");
}
form.setClubName(club.getString("clubName"));
} else if ("UNION_REIMBURSE_PROJECT_5".equals(form.getReimburseProject())) {
@@ -11,7 +11,7 @@
<el-table-column label="总分" prop="totalScore" header-align="center"
align="center" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" type="index" :index="indexMethod" label="名次"
<el-table-column align="center" header-align="center" prop="rank" label="名次"
width="80px"></el-table-column>
</el-table>
@@ -47,7 +47,21 @@ module.exports = {
this.tableData = []
const {data} = await this.$axios.post(loc() + "/isScoreTopEight", this.Form)
const table = data.score.sort(this.compare("totalScore"))
this.tableData = table.slice(0, 8)
// 按竞赛名次规则取前八:同分并列同名次,并列后名次不跳号(dense rank);
// 名次大于 8 的舍弃。不能直接 slice(0, 8),否则同分并列的工会会被切掉。
const ranked = []
let rank = 0
let prevScore = null
table.forEach(v => {
// 总分带 0.5 折半小数,比较用容差,避免浮点误差误判并列
if (prevScore === null || Math.abs(v.totalScore - prevScore) > 0.000001) {
// 每遇到一个新的分值,名次 +1;同分者沿用同一名次
rank += 1
prevScore = v.totalScore
}
ranked.push(Object.assign({}, v, {rank: rank}))
})
this.tableData = ranked.filter(v => v.rank <= 8)
} catch (e) {
this.$message.error(e.msg)
} finally {
@@ -77,6 +77,7 @@ layout("/layouts/platform.html"){
<el-input placeholder="输入关键字进行查找" v-model="filterText" clearable></el-input>
<div style="max-height: calc(100vh - 200px); overflow-y: auto">
<el-tree
v-loading="treeLoading"
:data="treeData"
ref="treeRef"
:expand-on-click-node="false"
@@ -151,13 +152,14 @@ layout("/layouts/platform.html"){
<!--#include("unitLeader.js"){}#-->
new Vue({
el: "#app",
data: function () {
data() {
return {
unitTabActive: "threeUnitManage",
filterText: null,
currentTreeNode: null,
currentTreeData: null,
defaultExpandedKeys: [],
treeLoading: false,
treeData: []
}
},
@@ -173,26 +175,45 @@ layout("/layouts/platform.html"){
},
methods: {
getTreeData() {
this.$axios.post(loc() + "/tree").then((res) => {
this.treeData = res.data
if (this.treeData) {
this.currentTreeData = this.treeData[0]
this.defaultExpandedKeys = this.treeData.length > 0 ? [this.treeData[0].id] : []
this.$set(this, "treeLoading", true)
// tree 返回单位树数组;失败时保留现有内容,成功后按 ID 恢复当前单位。
return this.$axios.post(loc() + "/tree").then((res) => {
if (res.code !== 0 || !Array.isArray(res.data)) return
const selectedId = this.currentTreeData?.id
this.$set(this, "treeData", res.data)
this.$set(this, "defaultExpandedKeys", res.data.length > 0 ? [res.data[0].id] : [])
if (res.data.length === 0) {
this.$set(this, "currentTreeData", null)
this.$set(this, "currentTreeNode", null)
return
}
this.$nextTick(() => {
const tree = this.$refs.treeRef
const node = (selectedId && tree.getNode(selectedId)) || tree.getNode(res.data[0].id)
// 将选中节点的祖先展开,避免刷新后当前单位隐藏在折叠的分支中。
let parent = node.parent
while (parent && parent.level > 0) {
parent.expanded = true
parent = parent.parent
}
tree.setCurrentKey(node.data.id)
tree.filter(this.filterText)
this.treeNodeClick(node.data, node)
})
}).finally(() => {
this.$set(this, "treeLoading", false)
})
},
treeNodeClick(data, node) {
this.currentTreeData = data
this.currentTreeNode = node
this.$nextTick(()=>{
if (data.unitTypeCode==1) {
this.$refs.bmlRef.pageData()
this.$refs.userRef.pageData()
this.$refs.leaderRef.pageData()
} else {
this.$refs.bzlRef.pageData()
}
})
this.$set(this, "currentTreeData", data)
this.$set(this, "currentTreeNode", node)
// 单位列表由 currentData 监听刷新,部门类另外更新人员和干部页签。
this.$nextTick(() => {
if (data.unitTypeCode == 1) {
this.$refs.userRef.pageData()
this.$refs.leaderRef.pageData()
}
})
},
filterNode(value, data, node) {
if (!value) return true
@@ -11,7 +11,7 @@ const UNIT_MANAGE_TEMPLATE = {
<table-tool label="单位列表">
<el-button @click="openAdd" type="primary" size="small">新建单位</el-button>
</table-tool>
<el-table :data="tableData" size="mini">
<el-table :data="tableData" v-loading="tableLoading" size="mini">
<el-table-column label="序号" type="index" width="60">
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column>
@@ -75,8 +75,8 @@ const UNIT_MANAGE_TEMPLATE = {
<!-- </el-form-item>-->
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doHandle">确 定</el-button>
<el-button :disabled="formLoading" @click="closeDialog">取 消</el-button>
<el-button type="primary" :loading="formLoading" @click="doHandle">确 定</el-button>
</span>
</el-dialog>
</div>
@@ -110,6 +110,8 @@ const UNIT_MANAGE_TEMPLATE = {
audit: false
},
tableData: [],
tableLoading: false,
formLoading: false,
dialogVisible: false,
formData: {},
formRules: {
@@ -128,96 +130,130 @@ const UNIT_MANAGE_TEMPLATE = {
}
},
computed: {},
watch: {
// 首次进入、切换节点以及刷新树后,均按最新上级单位从第一页重新查询。
currentData: {
immediate: true,
handler() {
this.doSearch()
}
}
},
methods: {
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
this.$set(this.pageForm, "pageNumber", 1)
return this.pageData()
},
openAdd() {
this.dialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
this.parentUnit = []
this.options = []
// 上级关系取当前单位 ID,与用户是否点击过树节点无关;手工新增默认其他类。
this.$set(this, "formData", {
parentId: this.currentData.id,
unitLevel: this.unitLevel,
unitTypeCode: 0
})
this.$set(this, "parentUnit", [])
this.$set(this, "options", [])
this.$set(this, "dialogVisible", true)
this.$nextTick(() => {
this.$refs.form.clearValidate()
})
},
closeDialog() {
// 提交过程中保留表单,等待请求完成后再允许取消。
if (!this.formLoading) {
this.$set(this, "dialogVisible", false)
}
},
doHandle() {
this.$confirm("确定要提交吗?", "提示", {
if (this.formLoading) return
this.$refs.form.validate((valid) => {
if (!valid) return
return this.$confirm("确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (this.formLoading) return
const url = this.formData.id ? "/platform/sys/unit/editDo" : "/platform/sys/unit/addDo"
// 新建始终携带当前上级;编辑沿用回显的原上级,不在保存时意外变更层级。
if (!this.formData.id) {
this.$set(this.formData, "parentId", this.currentData.id)
}
this.$set(this, "formLoading", true)
return this.$axios.post(url, this.formData).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$set(this, "dialogVisible", false)
// 父页面刷新并恢复当前树节点,currentData 监听统一刷新列表,避免重复查询。
this.$emit("refresh", null)
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {
// 取消确认直接结束;请求错误由全局 axios 拦截器提示,保留表单供重试。
})
})
},
// id 为待编辑单位 ID,接口 data 为单位实体;保留已有部门类别及原上级关系。
openEdit(id) {
return this.$axios.post("/platform/sys/unit/edit/" + id).then((resp) => {
if (resp.code === 0) {
this.$set(this, "formData", resp.data)
this.$set(this, "dialogVisible", true)
this.$nextTick(() => {
this.$refs.form.clearValidate()
})
} else {
this.$message.warning(resp.msg)
}
})
},
doDelete(id) {
return this.$confirm("此操作将永久删除, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const self = this
const url = this.formData.id ? "/platform/sys/unit/editDo" : "/platform/sys/unit/addDo"
this.$refs["form"].validate(async function (valid) {
if (valid) {
if (self.currentNode) {
self.formData.parentId = self.currentData.id
}
const resp = await self.$axios.post(url, self.formData)
if (resp.code === 0) {
self.$message.success(resp.msg)
self.doSearch()
self.dialogVisible = false
self.$emit("refresh", null)
} else {
self.$message.warning(resp.msg)
}
return this.$axios.post("/platform/sys/unit/delete/" + id).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.$emit("refresh", null)
} else {
this.$message.warning(resp.msg)
}
})
})
},
async openEdit(id) {
console.log(this.currentData)
const resp = await this.$axios.post("/platform/sys/unit/edit/" + id)
if (resp.code === 0) {
this.formData = resp.data
this.dialogVisible = true
} else {
this.$message.warning(resp.msg)
}
},
async doDelete(id) {
const confirm = await this.$confirm("此操作将永久删除, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
if (confirm === "confirm") {
const resp = await this.$axios.post("/platform/sys/unit/delete/" + id)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.$emit("refresh", null)
} else {
this.$message.warning(resp.msg)
}
}
},
pageNumberChange(val) {
this.pageForm.pageNumber = val
this.$set(this.pageForm, "pageNumber", val)
this.pageData()
},
pageSizeChange(val) {
this.pageForm.pageSize = val
this.$set(this.pageForm, "pageSize", val)
this.pageData()
},
pageData() {
this.pageForm.unitId = this.currentData.id
// this.pageForm.unitLevel = this.unitLevel
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (!this.currentData || !this.currentData.id) return
// unitId 限定当前上级,返回 data.list 为下设单位,data.totalCount 为分页总数。
this.$set(this.pageForm, "unitId", this.currentData.id)
this.$set(this, "tableLoading", true)
return this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
this.$set(this, "tableData", res.data.list)
this.$set(this.pageForm, "totalCount", res.data.totalCount)
}
}).finally(() => {
this.$set(this, "tableLoading", false)
})
}
},
async created() {
this.pageData()
if (this.$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN')) {
this.unions = await this.$businessTool.listUnion()
} else {
this.unions = await this.$businessTool.listUnion(this.store.state.user.union.id)
}
created() {
const unionId = this.$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN') ? null : this.store.state.user.union.id
this.$businessTool.listUnion(unionId).then((unions) => {
this.$set(this, "unions", unions)
})
}
}
@@ -9,10 +9,10 @@ layout("/layouts/platform.html"){
<div style="max-height: calc(100vh - 160px); overflow-y: auto">
<el-tree
:data="treeData"
:default-expanded-keys="['1']"
:default-expanded-keys="pageForm.parentId ? [pageForm.parentId] : []"
:expand-on-click-node="false"
:props="{
children: 'child',
children: 'children',
label: 'name'
}"
@node-click="treeNodeClick"
@@ -52,6 +52,7 @@ layout("/layouts/platform.html"){
</div>
<div class="search-item">
<el-button @click="doSearch" icon="el-icon-search" type="primary">搜索</el-button>
<el-button @click="updateUnits" :loading="formLoading" :disabled="formLoading" icon="el-icon-refresh" type="primary">更新单位</el-button>
</div>
</div>
</el-card>
@@ -104,6 +105,36 @@ layout("/layouts/platform.html"){
},
components: {},
methods: {
// 点击后同步一次系统单位;已有活动工会归属由后端保留,完成后刷新树和当前单位列表。
updateUnits() {
if (this.formLoading) {
return
}
this.$set(this, "formLoading", true)
this.$axios.post(loc() + "/updateUnits").then((res) => {
if (res.code !== 0) {
this.notifyWarning(res.msg)
return
}
const result = res.data
let message = "单位更新完成:新增 " + result.insertCount + " 个,更新 " + result.updateCount + " 个。"
if (result.pendingUnionCount > 0) {
message += "新增单位中有 " + result.pendingUnionCount + " 个待配置活动工会,请到活动工会的组成单位中维护。"
this.notifyWarning(message)
} else {
this.notifySuccess(message)
}
return this.getTreeData().then((loaded) => {
if (loaded) {
this.doSearch()
}
})
}).catch(() => {
this.notifyError("单位更新或刷新失败,请刷新页面后核对结果")
}).finally(() => {
this.$set(this, "formLoading", false)
})
},
async doDelete(id) {
const confirm = await this.$confirm("确定要删除该单位吗?", "提示", {
confirmButtonText: "确定",
@@ -122,24 +153,42 @@ layout("/layouts/platform.html"){
}
},
treeNodeClick(data, node) {
this.currentTreeData = data
this.currentTreeNode = node
this.pageForm.parentId = data.id
this.$set(this, "currentTreeData", data)
this.$set(this, "currentTreeNode", node)
this.$set(this.pageForm, "parentId", data.id)
this.doSearch()
},
getTreeData() {
this.$axios.post(loc() + "/tree").then((res) => {
this.treeData = res.data
if (this.treeData) {
this.currentTreeData = this.treeData[0]
return this.$axios.post(loc() + "/tree").then((res) => {
if (res.code !== 0) {
this.notifyWarning(res.msg)
return false
}
this.$set(this, "treeData", res.data || [])
// 同步后沿用当前选择,首次进入按真实树根查询,不再依赖旧的根单位ID常量。
if (!this.pageForm.parentId && this.treeData.length > 0) {
this.$set(this.pageForm, "parentId", this.treeData[0].id)
}
this.$nextTick(() => {
const tree = this.$refs.treeRef
const node = tree.getNode(this.pageForm.parentId)
this.$set(this, "currentTreeNode", node || null)
this.$set(this, "currentTreeData", node ? node.data : null)
tree.setCurrentKey(this.pageForm.parentId)
})
return true
})
},
openUnit() {}
},
created() {
this.pageData()
this.getTreeData()
this.getTreeData().then((loaded) => {
if (loaded) {
this.doSearch()
}
}).catch(() => {
this.notifyError("加载单位失败,请稍后重试")
})
}
})
</script>
@@ -474,7 +474,7 @@ layout("/layouts/platform.html"){
this.unionList = await this.$businessTool.listUnion()
if (this.unionList.length > 0) {
this.pageForm.unionId = this.unionList[0].id
this.unionName = this.unionList[0].unionname
this.unionName = this.unionList[0].name
}
await this.getPrizes()
}
@@ -567,16 +567,32 @@ layout("/layouts/platform.html"){
},
async openAdd() {
this.unitOptions = await this.$businessTool.listUnit()
this.$set(this.formData, "activityId", this.activityId)
this.$set(this.formData, "eventId", this.eventId)
this.$set(this.formData, "awardsMode", this.awardsMode)
this.$set(this.formData, "identity", ['1'])
this.$set(this.formData, "status", 2)
this.$set(this.formData, "sex", "男")
this.dialogVisible = true
if (this.$refs['form']) {
this.$refs['form'].resetFields()
// 每次打开都重建表单数据,避免上一次录入内容残留
this.formData = {
activityId: this.activityId,
eventId: this.eventId,
awardsMode: this.awardsMode,
identity: ['1'],
status: 2,
sex: "男",
userid: "",
username: "",
loginname: "",
mobile: "",
unitId: "",
unitname: "",
unionId: "",
unionname: ""
}
// 清空远程搜索候选,避免下拉仍显示上次搜索结果
this.userOptions = []
this.dialogVisible = true
// resetFields 会把值还原成首次渲染时的快照,不能用于清空;这里只清校验提示
this.$nextTick(() => {
if (this.$refs['form']) {
this.$refs['form'].clearValidate()
}
})
},
async doAddUser() {
@@ -73,7 +73,8 @@ layout("/layouts/platform.html"){
</el-select>
</search-item>
<search-item>
<el-button @click="doExcelCj" icon="el-icon-printer" type="primary">导出成绩excel
<el-button @click="doExcelCj" icon="el-icon-printer" type="primary"
:disabled="!pageForm.activityId">导出成绩excel
</el-button>
</search-item>
</search>
@@ -301,10 +302,18 @@ layout("/layouts/platform.html"){
},
doExcelCj() {
const {activityId, unionname} = this.pageForm
// 年度统计视图下 activityId 已被清空,此处导出会查不到数据
if (!activityId) {
this.$message.warning("当前为年度统计视图,无法导出单个活动的成绩明细,请先选择「运动会成绩统计」中的任一维度")
return
}
let unionId = ''
if (unionname) {
const unionlist = clone(this.unionList)
unionId = unionlist.find(v => v.unionname === unionname).id
const target = unionlist.find(v => v.name === unionname)
if (target) {
unionId = target.id
}
}
this.$downLoad(loc() + "/doExcelCj", {activityId: activityId, unionId: unionId})
@@ -818,6 +818,10 @@ layout("/layouts/platform.html"){
paymentWay: [{required: true, message: "请选择支付方式", trigger: ["change", "blur"]}],
reimburseProject: [{required: true, message: "请选择报销项目", trigger: ["change", "blur"]}],
reimburseFundSource: [{required: true, message: "请选择经费来源", trigger: ["change", "blur"]}],
// 报销事由仅在慰问项目显示,提交和重新提交时拒绝空值及纯空白。
paymentNotes: [{required: true, whitespace: true, message: "请填写报销事由", trigger: ["change", "blur"]}],
// 所属协会仅在协会经费显示,候选范围由后端当前会员关系确定。
clubId: [{required: true, message: "请选择所属协会", trigger: ["change", "blur"]}],
bankUserName: [{
required: true,
whitespace: true,
@@ -1890,10 +1894,14 @@ layout("/layouts/platform.html"){
this.normalizeFundBalance()
resolve(true)
} else {
// 支付信息已移至发票信息页,支付明细校验失败时跳转到对应页签。
// 新增必填项属于基本信息;同时存在支付错误时优先展示这两个字段的校验提示。
const basicInfoInvalid = Object.keys(invalidFields || {})
.some((field) => ["paymentNotes", "clubId"].includes(field))
const paymentDetailInvalid = Object.keys(invalidFields || {})
.some((field) => field.indexOf("paymentDetails.") === 0)
if (paymentDetailInvalid) {
if (basicInfoInvalid) {
this.$set(this, "activeTabName", "basic")
} else if (paymentDetailInvalid) {
this.$set(this, "activeTabName", "invoice")
}
this.$message.warning("请完善必填信息")