This commit is contained in:
2026-09-11 15:18:16 +08:00
parent 4353a62bb2
commit 0dd2d9d08f
46 changed files with 877 additions and 840 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.budwk</groupId> <groupId>com.budwk</groupId>
<artifactId>zhgh_cug_v4</artifactId> <artifactId>mini</artifactId>
<version>5.6.0-plus</version> <version>5.6.0-plus</version>
<build> <build>
<resources> <resources>
+1 -1
View File
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging> <packaging>jar</packaging>
<groupId>com.budwk</groupId> <groupId>com.budwk</groupId>
<artifactId>zhgh_cug_v4</artifactId> <artifactId>mini</artifactId>
<version>5.6.0-plus</version> <version>5.6.0-plus</version>
<properties> <properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version> <nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
@@ -27,6 +27,8 @@ public class DataCenterProperties {
private String tokenUrl; private String tokenUrl;
private String key; private String key;
private String secret; private String secret;
/** 人员和单位接口共用的请求头 apiKey。 */
private String apiKey;
private Map<String, String> urls = new HashMap<>(); private Map<String, String> urls = new HashMap<>();
public void init() { public void init() {
@@ -34,6 +36,7 @@ public class DataCenterProperties {
tokenUrl = all.get("data-center.token-url"); tokenUrl = all.get("data-center.token-url");
key = all.get("data-center.key"); key = all.get("data-center.key");
secret = all.get("data-center.secret"); secret = all.get("data-center.secret");
apiKey = all.get("data-center.api-key");
extract(all, "data-center.urls.", urls); extract(all, "data-center.urls.", urls);
} }
@@ -11,6 +11,8 @@ import com.budwk.app.sys.services.SysDataUserPullService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
@@ -64,6 +66,8 @@ public class SysDataUserPullController {
@At @At
@SaCheckPermission("sys.data.user.pull") @SaCheckPermission("sys.data.user.pull")
@ApiOperation("拉取用户数据") @ApiOperation("拉取用户数据")
// 无请求参数;service拉取完整批次,返回Result的code=0表示同步到本地数据源成功。
@Aop(TransAop.READ_COMMITTED)
public Result pullData() { public Result pullData() {
sysUserPullService.pull(); sysUserPullService.pull();
return Result.success(); return Result.success();
@@ -18,6 +18,8 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.ioc.Ioc; import org.nutz.ioc.Ioc;
@@ -118,10 +120,15 @@ public class SysDataUserUpdateController {
} }
/**
* 更新单位数据,无请求参数;由service读取接口配置并同步全部单位。
* 返回Result对象,code=0表示全部同步成功,失败时由统一异常处理返回错误信息。
*/
@At @At
@SaCheckPermission("sys.data.user.pull") @SaCheckPermission("sys.data.user.pull")
@SLog(tag = "更新单位数据", msg = "更新单位数据") @SLog(tag = "更新单位数据", msg = "更新单位数据")
@ApiOperation("更新单位数据") @ApiOperation("更新单位数据")
@Aop(TransAop.READ_COMMITTED)
public Result updateUnits(){ public Result updateUnits(){
sysDataUnitPullService.updateUnits(); sysDataUnitPullService.updateUnits();
return Result.success(); return Result.success();
@@ -12,25 +12,20 @@ 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_home_template;
import com.budwk.app.sys.models.Sys_menu; import com.budwk.app.sys.models.Sys_menu;
import com.budwk.app.sys.services.SysMenuService; import com.budwk.app.sys.services.SysMenuService;
import com.budwk.app.sys.services.SysHomeNewsService;
import com.budwk.app.sys.services.SysUserService; import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter; import org.nutz.dao.FieldFilter;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos; import org.nutz.dao.util.Daos;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -42,7 +37,6 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -58,7 +52,7 @@ public class SysHomeController {
@Inject @Inject
private SysUserService sysUserService; private SysUserService sysUserService;
@Inject @Inject
private RedisService redisService; private SysHomeNewsService sysHomeNewsService;
@Inject @Inject
private ActivityBasicScopeService activityBasicScopeService; private ActivityBasicScopeService activityBasicScopeService;
@@ -289,38 +283,14 @@ public class SysHomeController {
} }
/**
* 无请求参数;Result.data为六栏目列表,label为栏目名,value为标题、日期、原文链接及可选图片的列表。
*/
@At @At
@SaCheckLogin @SaCheckLogin
@ApiOperation("获取新闻") @ApiOperation("获取岭南工会首页新闻")
@Ok("json") @Ok("json")
// @CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "web_news", isHash = true)
// @CacheResult(cacheKey = "news", ignoreNull = true, cacheLiveTime = 60 * 60 * 24)
public Result getNews() { public Result getNews() {
try { return Result.success(sysHomeNewsService.getNews());
String url = "https://xgh.cug.edu.cn/";
Document document = Jsoup.connect(url).get();
Elements homeElements = document.select(".gh,.jc,.fc,.jg");
List<Map<String, Object>> news = homeElements.stream().map(homeElement -> {
//获取板块名称
String name = homeElement.selectFirst(".title").text();
Elements liElements = homeElement.select("li");
List<NutMap> ele = liElements.stream().map(liElement -> {
NutMap row = NutMap.NEW();
String href = liElement.selectFirst("a").attr("href");
String time = liElement.selectFirst("span").text();
String title = liElement.selectFirst("a").attr("title");
row.put("href", href);
row.put("time", time);
row.put("text", title);
return row;
}).toList();
return Map.of("label", name, "value", ele);
}).collect(Collectors.toList());
redisService.setex("gonghui_news", 60 * 60 * 24, Json.toJson(news));
return Result.success(news);
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
} }
@@ -3,10 +3,6 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
@@ -49,7 +45,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
; ;
@@ -295,34 +290,15 @@ public class SysUnitController {
} }
} }
/**
* pid为可选根单位ID,未传时使用学校单位1;返回Result包装的单位树,children表示下级单位。
*/
@At("/tree") @At("/tree")
@Ok("json") @Ok("json")
@SaCheckLogin @SaCheckLogin
public Object tree(@Param("pid") String pid, HttpServletRequest req) { public Object tree(@Param("pid") String pid, HttpServletRequest req) {
try { try {
String rootId = StrUtil.blankToDefault(pid, "0"); return Result.success(sysUnitService.getUnitTree(pid));
String virtualRootId = "root";
Cnd cnd = Cnd.NEW();
cnd.asc("unitcode");
List<Sys_unit> list = sysUnitService.query(cnd);
List<TreeNode<String>> nodeList = CollUtil.newArrayList();
for (int i = 0; i < list.size(); i++) {
Sys_unit unit = list.get(i);
/*
* 单位根节点存在 id 与 parentId 都为 0 的自引用数据。
* 构建树时把当前查询根挂到虚拟根下,避免“中国地质大学”和 parentId=0 的学院被构造成同级。
*/
String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId();
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(
Map.of(
"unitTypeCode", unit.getUnitTypeCode()
)
));
}
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
return Result.success(treeList);
} catch (Exception e) { } catch (Exception e) {
return Result.error(); return Result.error();
} }
@@ -37,7 +37,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("工号") @Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 120) @ColDefine(type = ColType.VARCHAR, width = 120)
@DataCenterColumn(name = "工号", key = "ZGH") @DataCenterColumn(name = "工号", key = "GH")
private String loginname; private String loginname;
@Column @Column
@@ -49,7 +49,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("性别") @Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 10) @ColDefine(type = ColType.VARCHAR, width = 10)
@DataCenterColumn(name = "性别", key = "XB", dict = "USER_SEX") @DataCenterColumn(name = "性别", key = "XB")
private String sex; private String sex;
@Column @Column
@@ -70,7 +70,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("政治面貌") @Comment("政治面貌")
@ColDefine(type = ColType.VARCHAR, width = 100) @ColDefine(type = ColType.VARCHAR, width = 100)
@DataCenterColumn(name = "政治面貌", key = "ZZMMM", dict = "USER_POLITICAL") @DataCenterColumn(name = "政治面貌", key = "ZZMM", dict = "USER_POLITICAL")
private String political; private String political;
@Column @Column
@@ -81,7 +81,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("民族") @Comment("民族")
@ColDefine(type = ColType.VARCHAR, width = 20) @ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "民族", key = "MZM", dict = "USER_NATION") @DataCenterColumn(name = "民族", key = "MZ", dict = "USER_NATION")
private String nation; private String nation;
@Column @Column
@@ -114,17 +114,19 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("学历") @Comment("学历")
@ColDefine(type = ColType.VARCHAR, width = 20) @ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "最高学历", key = "ZGXL")
private String education; private String education;
@Column @Column
@Comment("婚姻状况") @Comment("婚姻状况")
@ColDefine(type = ColType.VARCHAR, width = 20) @ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "婚姻状况", key = "HYZK")
private String marriage; private String marriage;
@Column @Column
@Comment("学位") @Comment("学位")
@ColDefine(type = ColType.VARCHAR, width = 50) @ColDefine(type = ColType.VARCHAR, width = 50)
// @DataCenterColumn(name = "学位", key = "zgxwmc") @DataCenterColumn(name = "学位", key = "ZGXW")
private String academicDegree; private String academicDegree;
@Column @Column
@@ -135,8 +137,16 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("职称") @Comment("职称")
@ColDefine(type = ColType.VARCHAR, width = 50) @ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "当前职称", key = "DQZC")
private String professionalTitle; private String professionalTitle;
/** 信息中心 DQZW 为当前职务,与职称、职级分别保存。 */
@Column
@Comment("当前职务")
@ColDefine(type = ColType.VARCHAR, width = 100)
@DataCenterColumn(name = "当前职务", key = "DQZW")
private String position;
@Column @Column
@Comment("职级") @Comment("职级")
@ColDefine(type = ColType.VARCHAR, width = 50) @ColDefine(type = ColType.VARCHAR, width = 50)
@@ -175,7 +185,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("来校时间") @Comment("来校时间")
@ColDefine(type = ColType.VARCHAR, width = 10) @ColDefine(type = ColType.VARCHAR, width = 10)
@DataCenterColumn(name = "来校时间", key = "LXNY") @DataCenterColumn(name = "来校时间", key = "LXRQ")
private String arrivalAtSchoolDate; private String arrivalAtSchoolDate;
@Column @Column
@@ -208,7 +218,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("教职工类别码") @Comment("教职工类别码")
@ColDefine(type = ColType.VARCHAR, width = 30) @ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "教职工类别码", key = "RYLX", dict = "USER_PERSON_TYPE") @DataCenterColumn(name = "教职工类别码", key = "JZGLB", dict = "USER_PERSON_TYPE")
private String personType; private String personType;
@Column @Column
@@ -220,6 +230,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("编制类别码") @Comment("编制类别码")
@ColDefine(type = ColType.VARCHAR, width = 30) @ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "编制类别", key = "BZLB")
private String preparedBy; private String preparedBy;
@Column @Column
@@ -269,12 +280,13 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@ColDefine(type = ColType.VARCHAR, width = 32) @ColDefine(type = ColType.VARCHAR, width = 32)
@DataCenterColumn(name = "单位", key = "SZDWH") @DataCenterColumn(name = "二级单位", key = "SZEJDWH")
private String unitId; private String unitId;
@Column @Column
@Comment("三级单位ID") @Comment("三级单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32) @ColDefine(type = ColType.VARCHAR, width = 32)
@DataCenterColumn(name = "三级单位", key = "SZDWH")
private String threeUnitId; private String threeUnitId;
@Column @Column
@@ -343,6 +355,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column @Column
@Comment("校区") @Comment("校区")
@ColDefine(type = ColType.VARCHAR, width = 20) @ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "校区", key = "XQH")
private String campus; private String campus;
@@ -0,0 +1,14 @@
package com.budwk.app.sys.services;
import org.nutz.lang.util.NutMap;
import java.util.List;
public interface SysHomeNewsService {
/**
* 无请求参数,读取岭南工会官网首页的六个栏目。
* 返回栏目列表:label 为栏目名,href 为更多链接,value 为文章列表;
* 文章 text 为标题、time 为官网显示日期、href 为原文链接、image 为可选图片地址。
*/
List<NutMap> getNews();
}
@@ -1,14 +1,25 @@
package com.budwk.app.sys.services; package com.budwk.app.sys.services;
import cn.hutool.core.lang.tree.Tree;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm; import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_unit; import com.budwk.app.sys.models.Sys_unit;
import java.util.List;
/** /**
* Created by wizzer on 2016/12/22. * Created by wizzer on 2016/12/22.
*/ */
public interface SysUnitService extends BaseService<Sys_unit> { public interface SysUnitService extends BaseService<Sys_unit> {
/**
* 获取包含指定单位自身及其下级的单位树。
*
* @param pid 根单位ID;为空或空白时使用信息中心学校单位ID“1”
* @return 单位树列表;节点包含id、parentId、name、unitTypeCode及children,根单位不存在时返回空列表
*/
List<Tree<String>> getUnitTree(String pid);
/** /**
* 分页查询指定分工会的组成单位。 * 分页查询指定分工会的组成单位。
* *
@@ -1,10 +1,7 @@
package com.budwk.app.sys.services.impl; package com.budwk.app.sys.services.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil; import cn.hutool.http.HttpResponse;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
@@ -21,7 +18,6 @@ import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -31,18 +27,14 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* @version 1.0 * 岭南师范学院单位同步:通过 apiKey 认证分页获取组织机构,并按单位编码更新本地单位。
* @Author zzr
* @nameSysDataUnitPullServiceImpl
* @Date 2025/9/15 17:52
* @注释
*/ */
@Slf4j @Slf4j
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService { public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
private static final String DMP_UNIT_KEY = "unit"; private static final int UNIT_PAGE_SIZE = 100;
private static final int DMP_PAGE_SIZE = 1000; private static final int REQUEST_TIMEOUT = 30000;
@Inject @Inject
private DataCenterProperties dataCenterProperties; private DataCenterProperties dataCenterProperties;
@@ -51,7 +43,9 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
super(dao); super(dao);
} }
/**
* 手动更新入口,无需参数、无返回值;全部分页拉取并校验成功后更新 sys_unit。
*/
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void updateUnits() { public void updateUnits() {
@@ -59,192 +53,167 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
} }
/** /**
* 拉取单位数据 * 人员拉取时复用的单位同步入口。
* unitIds 是调用方已有的本地单位编码列表,仅用于日志,不限制同步范围,以保证父级齐全;
* 无返回值,接口或入库失败时抛出异常,由事务回滚本次单位变更。
*/ */
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void updateUnits(List<String> unitIds) { public void updateUnits(List<String> unitIds) {
String accessToken = getDmpAccessToken(); List<JSONObject> units = pullUnits();
List<JSONObject> rawDataList = distinctDmpUnits(pullDmpUnits(getDmpUnitUrl(), accessToken)); Set<String> parentCodes = units.stream()
Set<String> parentCodes = rawDataList.stream()
.map(this::getNormalizedParentCode) .map(this::getNormalizedParentCode)
.filter(StrUtil::isNotBlank) .filter(StrUtil::isNotBlank)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
rawDataList.stream() // DWDJ 为外部单位等级,父级先入库后,子级才能获取正确的父级及树路径。
.sorted(Comparator.comparingInt(this::getUnitLevel)) units.sort(Comparator.comparingInt(raw -> raw.getInt("DWDJ")));
.forEach(raw -> saveOrUpdateUnit(raw, parentCodes)); for (JSONObject raw : units) {
saveOrUpdateUnit(raw, parentCodes);
log.info("信息中心单位数据同步完成,本次接口返回单位数量: {},待检查单位编码数量: {}", rawDataList.size(), unitIds == null ? 0 : unitIds.size()); }
log.info("单位数据同步完成,单位数量: {},调用方已有单位编码数量: {}",
units.size(), unitIds == null ? 0 : unitIds.size());
} }
/** /**
* 获取信息中心开放平台访问令牌 * 从配置读取 data-center.urls.unit 和 data-center.api-key,无方法参数
* 参数来自 data-center.token-url、data-center.key、data-center.secret * GET 查询参数 pageNum 从 1 开始,pageSize 为每页条数,请求头 apiKey 为接口密钥
* 返回值为后续单位接口 body 中 access_token 使用的字符串 * status=0 表示成功,payload 包含 pageNo、pageSize、totalCount 及 resultList 单位对象数组
* 返回全部分页按 DWMCDM 去重后的 JSON 列表;分页不完整或字段不合法时抛出异常,不写入部分数据。
*/ */
private String getDmpAccessToken() { private List<JSONObject> pullUnits() {
String tokenUrl = dataCenterProperties.getTokenUrl(); String url = dataCenterProperties.getUrls().get("unit");
if (StrUtil.isBlank(tokenUrl)) { String apiKey = dataCenterProperties.getApiKey();
throw new BaseException("未配置信息中心token地址: data-center.token-url");
}
String key = dataCenterProperties.getKey();
if (StrUtil.isBlank(key)) {
throw new BaseException("未配置信息中心token key: data-center.key");
}
String secret = dataCenterProperties.getSecret();
if (StrUtil.isBlank(secret)) {
throw new BaseException("未配置信息中心token secret: data-center.secret");
}
String params = URLUtil.buildQuery(Map.of("key", key, "secret", secret), StandardCharsets.UTF_8);
String tokenResBody = HttpUtil.createGet(tokenUrl + "?" + params).execute().body();
JSONObject tokenJsonBody = JSONUtil.parseObj(tokenResBody);
String accessToken = tokenJsonBody.getStr("access_token");
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("result") != null) {
accessToken = tokenJsonBody.getJSONObject("result").getStr("access_token");
}
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("data") != null) {
accessToken = tokenJsonBody.getJSONObject("data").getStr("access_token");
}
if (StrUtil.isBlank(accessToken)) {
accessToken = tokenJsonBody.getStr("token");
}
if (StrUtil.isBlank(accessToken)) {
throw new BaseException("获取信息中心token失败");
}
return accessToken;
}
/**
* 读取信息中心单位接口地址。
* 配置项为 data-center.urls.unit,返回值为单位全量接口URL。
*/
private String getDmpUnitUrl() {
String url = dataCenterProperties.getUrls().get(DMP_UNIT_KEY);
if (StrUtil.isBlank(url)) { if (StrUtil.isBlank(url)) {
throw new BaseException("未配置信息中心单位接口地址: data-center.urls." + DMP_UNIT_KEY); throw new BaseException("未配置单位接口地址: data-center.urls.unit");
}
if (StrUtil.isBlank(apiKey)) {
throw new BaseException("未配置单位接口密钥: data-center.api-key");
} }
return url;
}
/**
* 分页拉取单位接口数据。
* 入参 url 为单位接口地址,accessToken 为信息中心令牌;
* 请求 body 包含 access_token、per_page、page
* 返回值为 result.data 合并后的单位原始 JSON 列表。
*/
private List<JSONObject> pullDmpUnits(String url, String accessToken) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
int total = 0;
do {
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
log.info("请求信息中心单位数据,第{}页: {}", page, httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求信息中心单位数据第{}页结果: {}", page, resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 10000) {
throw new BaseException("获取信息中心单位数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
}
JSONObject result = jsonBody.getJSONObject("result");
if (result == null) {
break;
}
total = result.getInt("total", total);
maxPage = result.getInt("max_page", maxPage);
JSONArray data = result.getJSONArray("data");
if (CollUtil.isNotEmpty(data)) {
rawDataList.addAll(data.stream().map(v -> (JSONObject) v).toList());
}
log.info("信息中心单位数据拉取进度: {}/{}", rawDataList.size(), total);
page++;
} while (page <= maxPage);
return rawDataList;
}
/**
* 按单位代码去重,避免接口分页或源数据重复导致同一 DWDM 重复写入。
*/
private List<JSONObject> distinctDmpUnits(List<JSONObject> rawDataList) {
Map<String, JSONObject> unitMap = new LinkedHashMap<>(); Map<String, JSONObject> unitMap = new LinkedHashMap<>();
for (JSONObject raw : rawDataList) { int pageNum = 1;
String unitCode = raw.getStr("DWDM"); int pageSize = UNIT_PAGE_SIZE;
if (StrUtil.isBlank(unitCode)) { long totalCount = -1;
continue; long receivedCount = 0;
do {
JSONObject body;
// 响应使用完毕即关闭;日志仅记录进度,不输出含 apiKey 的请求对象。
try (HttpResponse response = HttpUtil.createGet(url)
.header("apiKey", apiKey)
.form("pageNum", pageNum)
.form("pageSize", pageSize)
.timeout(REQUEST_TIMEOUT)
.execute()) {
if (!response.isOk()) {
throw new BaseException("获取单位数据失败,第{}页HTTP状态码: {}", pageNum, response.getStatus());
}
String responseBody = response.body();
if (!JSONUtil.isTypeJSONObject(responseBody)) {
throw new BaseException("获取单位数据失败,第{}页返回内容不是JSON对象", pageNum);
}
body = JSONUtil.parseObj(responseBody);
} }
unitMap.putIfAbsent(unitCode, raw); if (!Integer.valueOf(0).equals(body.getInt("status"))) {
} throw new BaseException("获取单位数据失败,第{}页status: {}", pageNum, body.getInt("status"));
}
JSONObject payload = body.getJSONObject("payload");
if (payload == null) {
throw new BaseException("获取单位数据失败,第{}页缺少payload", pageNum);
}
Integer responsePage = payload.getInt("pageNo");
Integer responseSize = payload.getInt("pageSize");
Long responseTotal = payload.getLong("totalCount");
JSONArray records = payload.getJSONArray("resultList");
if (!Integer.valueOf(pageNum).equals(responsePage) || responseSize == null || responseSize <= 0
|| responseTotal == null || responseTotal < 0 || records == null) {
throw new BaseException("单位接口第{}页分页信息或resultList不合法", pageNum);
}
// 以首个响应的实际分页大小为准;后续总数或页大小变化时中止,避免漏同步。
if (pageNum == 1) {
totalCount = responseTotal;
pageSize = responseSize;
} else if (totalCount != responseTotal || pageSize != responseSize) {
throw new BaseException("单位接口分页总数或每页条数发生变化,请重新同步");
}
long expectedCount = Math.min(pageSize, totalCount - receivedCount);
if (records.size() != expectedCount) {
throw new BaseException("单位接口第{}页数据不完整,预期{}条,实际{}条", pageNum, expectedCount, records.size());
}
int previousSize = unitMap.size();
for (int i = 0; i < records.size(); i++) {
JSONObject raw = records.getJSONObject(i);
validateUnit(raw);
unitMap.putIfAbsent(raw.getStr("DWMCDM"), raw);
}
if (!records.isEmpty() && unitMap.size() == previousSize) {
throw new BaseException("单位接口第{}页未返回新的单位编码,请检查分页是否生效", pageNum);
}
receivedCount += records.size();
log.info("单位数据拉取进度: {}/{},当前第{}页", receivedCount, totalCount, pageNum);
pageNum++;
} while (receivedCount < totalCount);
return new ArrayList<>(unitMap.values()); return new ArrayList<>(unitMap.values());
} }
/** /**
* 保存或更新单位。 * 校验每条单位的编码、名称及正整数等级,避免错误主键或非法等级进入本地单位
* DWDM 对应系统单位 id/unitcodeDWMC 对应名称,LSDWH 对应父级单位号,DWCC 对应信息中心原始单位层级。
*/ */
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes) { private void validateUnit(JSONObject raw) {
String unitCode = raw.getStr("DWDM"); if (raw == null || StrUtil.isBlank(raw.getStr("DWMCDM")) || StrUtil.isBlank(raw.getStr("DWMC"))) {
if (StrUtil.isBlank(unitCode)) { throw new BaseException("单位接口存在缺少DWMCDM或DWMC的数据");
return;
} }
Integer level = raw.getInt("DWDJ");
if (level == null || level <= 0 || level == Integer.MAX_VALUE) {
throw new BaseException("单位{}的DWDJ不是有效的正整数等级", raw.getStr("DWMCDM"));
}
if ("0".equals(raw.getStr("DWMCDM"))) {
throw new BaseException("接口单位编码0与系统内置根单位冲突");
}
}
/**
* raw 为已校验的单位对象,parentCodes 为本次数据中的父单位编码集合,无返回值。
* DWMCDM 映射 id/unitcodeDWMC 映射 nameSJDWMCDM 映射 parentIdDWDJ 直接映射 unitLevel,相比原同步规则整体降低一级。
* 新增和已有单位均按同步等级设置部门类别:三级为1,其余为0;地址、简称等保留已有值,新增单位以名称作为简称。
*/
@Aop(TransAop.READ_COMMITTED)
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes) {
String unitCode = raw.getStr("DWMCDM");
String parentId = getParentId(unitCode, getNormalizedParentCode(raw)); String parentId = getParentId(unitCode, getNormalizedParentCode(raw));
Sys_unit unit = fetch(unitCode); Sys_unit unit = fetch(unitCode);
if (unit == null) { boolean insert = unit == null;
if (insert) {
unit = new Sys_unit(); unit = new Sys_unit();
unit.setId(unitCode); unit.setId(unitCode);
unit.setUnitcode(unitCode); unit.setAliasName(raw.getStr("DWMC"));
}
unit.setUnitcode(unitCode);
unit.setName(raw.getStr("DWMC"));
unit.setParentId(parentId);
unit.setUnitLevel(raw.getInt("DWDJ"));
// 每次按接口等级重算部门类别,确保已有单位同步后也能正确显示单位干部入口。
unit.setUnitTypeCode(unit.getUnitLevel() == 3 ? 1 : 0);
unit.setHasChildren(parentCodes.contains(unitCode));
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath(parentId)); unit.setPath(getUnitPath(parentId));
setUnitValue(unit, raw, parentId, parentCodes); }
if (insert) {
dao().fastInsert(unit); dao().fastInsert(unit);
} else { } else {
setUnitValue(unit, raw, parentId, parentCodes);
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath(parentId));
}
dao().updateIgnoreNull(unit); dao().updateIgnoreNull(unit);
} }
} }
/** /**
* 根据接口字段给系统单位赋值 * 一级单位挂到系统单位0,其余单位使用SJDWMCDM,避免学校记录的自引用形成环
* DWYXBS 表示单位是否有效,DWCC 表示信息中心原始单位层次;
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
*/ */
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) { private String getNormalizedParentCode(JSONObject raw) {
String unitCode = raw.getStr("DWDM"); return Integer.valueOf(1).equals(raw.getInt("DWDJ")) ? "0" : raw.getStr("SJDWMCDM");
String unitName = raw.getStr("DWMC");
String aliasName = StrUtil.blankToDefault(raw.getStr("DWJC"), unitName);
Integer rawUnitLevel = raw.getInt("DWCC");
Integer unitLevel = getNormalizedUnitLevel(raw);
boolean availableBusinessUnit = "".equals(raw.getStr("DWYXBS")) && Integer.valueOf(2).equals(rawUnitLevel);
unit.setParentId(parentId);
unit.setName(unitName);
unit.setAliasName(aliasName);
unit.setUnitcode(unitCode);
unit.setAddress(raw.getStr("DWDZ"));
unit.setUnitLevel(unitLevel);
unit.setUnitType(raw.getStr("DWLB"));
unit.setUnitTypeCode(availableBusinessUnit ? 1 : 0);
unit.setHasChildren(parentCodes.contains(unitCode));
} }
/** /**
* 计算父级单位号 * 沿用本地父级兜底规则:父编码为空、自引用或父级不存在时作为根级,系统根单位0可直接引用
* LSDWH 为空、等于自身或本地不存在该父级时按根级处理;系统根单位 0 允许直接作为父级。
*/ */
private String getParentId(String unitCode, String parentCode) { private String getParentId(String unitCode, String parentCode) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) { if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) {
@@ -253,13 +222,11 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
if ("0".equals(parentCode)) { if ("0".equals(parentCode)) {
return parentCode; return parentCode;
} }
Sys_unit parentUnit = fetch(parentCode); return fetch(parentCode) == null ? "" : parentCode;
return parentUnit == null ? "" : parentCode;
} }
/** /**
* 根据父级 path 生成当前单位 path * 新单位或缺失路径的单位复用项目getSubPath,在父级路径下生成唯一的四位子路径
* 父级为空时生成根节点 path,父级存在时在父级 path 下生成子节点 path。
*/ */
private String getUnitPath(String parentId) { private String getUnitPath(String parentId) {
String parentPath = ""; String parentPath = "";
@@ -269,30 +236,4 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
} }
return getSubPath("sys_unit", "path", parentPath); return getSubPath("sys_unit", "path", parentPath);
} }
/**
* 信息中心一级单位应挂在系统根单位 0 下,其余单位沿用接口返回的 LSDWH。
*/
private String getNormalizedParentCode(JSONObject raw) {
Integer rawUnitLevel = raw.getInt("DWCC");
if (Integer.valueOf(1).equals(rawUnitLevel)) {
return "0";
}
return raw.getStr("LSDWH");
}
/**
* 系统内置根单位占用一级层级,信息中心单位层次入库时统一加一。
*/
private Integer getNormalizedUnitLevel(JSONObject raw) {
Integer rawUnitLevel = raw.getInt("DWCC");
return rawUnitLevel == null ? null : rawUnitLevel + 1;
}
/**
* DWCC 为信息中心单位层次,排序时用于尽量先处理父级单位,便于后续子级找到父节点。
*/
private int getUnitLevel(JSONObject raw) {
return raw.getInt("DWCC", 0);
}
} }
@@ -23,7 +23,6 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin; import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType; import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
@@ -161,10 +160,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
for (Sys_user_source source : sources) { for (Sys_user_source source : sources) {
Sys_user user = userMap.get(source.getLoginname()); Sys_user user = userMap.get(source.getLoginname());
source.setMember(null); source.setMember(null);
String unitId = source.getUnitId(); // 数据源已分别保存二级和三级单位编码,正式更新时保留完整编码。
if (StringUtils.isNotBlank(unitId) && unitId.length() == 5) {
source.setUnitId(unitId.substring(0, 3));
}
// 创建用户对象 // 创建用户对象
Sys_user u = new Sys_user(); Sys_user u = new Sys_user();
@@ -1,14 +1,11 @@
package com.budwk.app.sys.services.impl; package com.budwk.app.sys.services.impl;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdcardUtil; import cn.hutool.core.util.IdcardUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.Header; import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
@@ -18,8 +15,6 @@ import com.budwk.app.base.config.DataCenterProperties;
import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.annotation.DataCenterColumn; import com.budwk.app.sys.annotation.DataCenterColumn;
import com.budwk.app.sys.enums.Api2FinanceFiledMap;
import com.budwk.app.sys.models.Sys_data_dict;
import com.budwk.app.sys.models.Sys_dict; import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.models.Sys_unit; import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
@@ -27,7 +22,6 @@ import com.budwk.app.sys.models.Sys_user_source;
import com.budwk.app.sys.services.SysDataUnitPullService; import com.budwk.app.sys.services.SysDataUnitPullService;
import com.budwk.app.sys.services.SysDataUserPullService; import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysDictService; import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.utils.DataCenterUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.async.Async; import org.nutz.aop.interceptor.async.Async;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
@@ -42,8 +36,6 @@ import org.nutz.lang.util.NutMap;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -51,9 +43,8 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService { public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService {
private static final String DMP_TEACHER_KEY = "teacher"; private static final int USER_PAGE_SIZE = 1000;
private static final String DMP_DISPATCH_KEY = "dispatch"; private static final int REQUEST_TIMEOUT = 30000;
private static final int DMP_PAGE_SIZE = 1000;
@Inject @Inject
private SysDictService sysDictService; private SysDictService sysDictService;
@@ -74,12 +65,10 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
private static class FieldMapping { private static class FieldMapping {
final Field field; final Field field;
final String key; final String key;
final String dict;
FieldMapping(Field field, String key, String dict) { FieldMapping(Field field, String key) {
this.field = field; this.field = field;
this.key = key; this.key = key;
this.dict = dict;
field.setAccessible(true); field.setAccessible(true);
} }
} }
@@ -101,7 +90,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
for (Field field : fields) { for (Field field : fields) {
DataCenterColumn annotation = field.getAnnotation(DataCenterColumn.class); DataCenterColumn annotation = field.getAnnotation(DataCenterColumn.class);
if (annotation != null) { if (annotation != null) {
mappings.add(new FieldMapping(field, annotation.key(), annotation.dict())); mappings.add(new FieldMapping(field, annotation.key()));
} }
} }
fieldMappings = mappings; fieldMappings = mappings;
@@ -111,253 +100,175 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
return fieldMappings; return fieldMappings;
} }
/**
* 拉取完整人员批次,无请求参数;返回本批次拉取时间,用于sys_user_source筛选。
* 接口及字段校验全部通过后补齐二、三级单位,再写入数据源;异常时由事务回滚。
*/
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Date pull() { public Date pull() {
List<JSONObject> records = pullUsers();
if (records.isEmpty()) {
throw new BaseException("人员接口未返回数据,本次未生成数据源批次");
}
Date pullTime = new Date();
List<Sys_user_source> sources = records.stream().map(raw -> mapUser(raw, pullTime)).toList();
Set<String> requiredUnits = new HashSet<>();
for (Sys_user_source source : sources) {
if (StrUtil.isNotBlank(source.getUnitId())) {
requiredUnits.add(source.getUnitId());
}
if (StrUtil.isNotBlank(source.getThreeUnitId())) {
requiredUnits.add(source.getThreeUnitId());
}
}
// 编码不再按长度截断,二级和三级单位均须存在,避免数据源保存悬空的单位关联。
Set<String> existingUnits = dao().query(Sys_unit.class, Cnd.NEW()).stream()
.map(Sys_unit::getId).collect(Collectors.toSet());
if (!existingUnits.containsAll(requiredUnits)) {
sysDataUnitPullService.updateUnits(new ArrayList<>(existingUnits));
existingUnits = dao().query(Sys_unit.class, Cnd.NEW()).stream()
.map(Sys_unit::getId).collect(Collectors.toSet());
if (!existingUnits.containsAll(requiredUnits)) {
throw new BaseException("单位同步后仍缺少人员所属单位,请核对人员与单位接口编码");
}
}
dao().insert(sources);
log.info("人员数据源拉取完成,本批次人数: {}", sources.size());
return pullTime;
}
/**
* raw为单条人员JSON,pullTime为本批次时间;返回待保存的Sys_user_source。
* 按Sys_user字段注解映射,XB直接保存中文,其余字典类字段保留接口值,不套用旧学校码表。
* SZEJDWH/SZEJDW为二级单位编码/名称,SZDWH为三级单位编码,DQZW为当前职务。
* 缺失或空白字段统一为null,避免后续updateIgnoreNull用空字符串覆盖已有人员信息。
*/
private Sys_user_source mapUser(JSONObject raw, Date pullTime) {
Sys_user_source source = new Sys_user_source();
for (FieldMapping mapping : getFieldMappings()) {
String value = StrUtil.trimToNull(raw.getStr(mapping.key));
if ("LXRQ".equals(mapping.key)) {
value = normalizeArrivalAtSchoolDate(value);
}
try {
mapping.field.set(source, value);
} catch (IllegalAccessException e) {
throw new BaseException("人员字段映射失败: {}", mapping.key);
}
}
source.setUnitName(StrUtil.trimToNull(raw.getStr("SZEJDW")));
source.setPullTime(pullTime);
// 接口没有出生日期,仅对有效身份证推导生日;其他证件不强行转换,也不推导性别。
if (StrUtil.isNotBlank(source.getIdCard()) && IdcardUtil.isValidCard(source.getIdCard())) {
source.setBirthday(IdcardUtil.getBirthDate(source.getIdCard()));
}
return source;
}
/**
* LXRQ为接口来校日期字符串,支持日期、以空格或T分隔的时间及年月;
* 返回yyyy-MM-dd,年月补为当月01日,空值返回null,非法日期中止当前批次。
*/
private String normalizeArrivalAtSchoolDate(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
String date = value.trim().split("[T ]", 2)[0];
try { try {
// 获取数据中心的字典 String[] parts = date.split("-");
List<Sys_data_dict> dataDictList = dao().query(Sys_data_dict.class, Cnd.NEW()); if (parts.length == 2 || parts.length == 3) {
// 字典码表Map,key为父级编码,value为子级字典列表 return java.time.LocalDate.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]),
Map<String, List<Sys_data_dict>> dataDictMap = dataDictList.stream().collect(Collectors.groupingBy(Sys_data_dict::getParentCode)); parts.length == 3 ? Integer.parseInt(parts[2]) : 1).toString();
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = new ArrayList<>();
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工"));
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣"));
rawDataList = distinctDmpUsers(rawDataList);
if (rawDataList.isEmpty()) {
log.warn("当前未获取到人员数据");
} }
} catch (RuntimeException e) {
Date nowDate = new Date(); throw new BaseException("人员接口LXRQ不是有效日期");
// 赋值
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
Sys_user_source sysUser = new Sys_user_source();
// 使用缓存的字段映射
for (FieldMapping mapping : getFieldMappings()) {
try {
// 根据字段类型设置值
if (mapping.field.getType() == String.class) {
if (StrUtil.isNotBlank(mapping.dict)) {
List<Sys_data_dict> sysDataDicts = dataDictMap.getOrDefault(mapping.dict, Collections.emptyList());
Sys_data_dict sysDataDict = sysDataDicts.stream()
.filter(v -> raw.getStr(mapping.key, "").equals(v.getCode()))
.findFirst().orElse(new Sys_data_dict());
mapping.field.set(sysUser, StrUtil.blankToDefault(sysDataDict.getName(), raw.getStr(mapping.key)));
} else {
// 特殊处理 根据身份证号获取性别和出生年月
if ("SFZJH".equals(mapping.key)) {
String idCard = raw.getStr(mapping.key);
mapping.field.set(sysUser, idCard);
// 设置出生年月
try {
sysUser.setBirthday(IdcardUtil.getBirthDate(idCard));
} catch (Exception e) {
sysUser.setBirthday(null);
}
} else if ("LXNY".equals(mapping.key)) {
mapping.field.set(sysUser, normalizeArrivalAtSchoolDate(raw.getStr(mapping.key)));
} else {
mapping.field.set(sysUser, raw.getStr(mapping.key));
}
}
} else if (mapping.field.getType() == Date.class) {
mapping.field.set(sysUser, raw.getDate(mapping.key));
}
} catch (IllegalAccessException e) {
log.error("设置字段值失败: {}", mapping.field.getName(), e);
}
}
sysUser.setUnitName(raw.getStr("SZDW"));
sysUser.setUnitId(resolveSourceUnitId(raw.getStr("SZDWH")));
// 设置拉取时间
sysUser.setPullTime(nowDate);
return sysUser;
}).toList();
log.info("用户数据初始化完成,等待插入,当前数据条数{}", latestSourceList.size());
// 插入到数据库
if (CollUtil.isNotEmpty(latestSourceList)) {
dao().insert(latestSourceList);
}
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).filter(StrUtil::isNotBlank).distinct().toList();
Sql sql = Sqls.create("select id from sys_unit group by id");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
List<String> unitIds = sql.getList(String.class);
if (!new HashSet<>(unitIds).containsAll(sourceUnitIds)) {
log.info("单位需要更新,正在同步更新");
sysDataUnitPullService.updateUnits(unitIds);
}
return nowDate;
} catch (Exception e) {
throw new BaseException("数据拉取失败,{}", e.getMessage());
} }
throw new BaseException("人员接口LXRQ日期格式不支持");
} }
/** /**
* 规范化信息中心返回的来校时间 * 配置data-center.urls.teacher为人员接口地址,data-center.api-key为人员和单位共用密钥
* 入参 arrivalAtSchoolDate 对应信息中心 LXNY 字段,允许 yyyy-MM-dd 或 yyyy-MM * GET查询参数pageNum从1开始、pageSize为每页数量,请求头apiKey用于认证。
* 返回值用于 sys_user_source.arrivalAtSchoolDate 和后续 sys_user.arrivalAtSchoolDate,年月格式统一补为当月 01 日 * status=0成功,payload.resultList为人员数组,pageNo/pageSize/totalCount用于校验分页完整性
* 返回全部分页按GH去重的人员JSON列表;分页异常时抛出异常,不保存部分批次。
*/ */
private String normalizeArrivalAtSchoolDate(String arrivalAtSchoolDate) { private List<JSONObject> pullUsers() {
String dateText = StrUtil.trim(arrivalAtSchoolDate); String url = dataCenterProperties.getUrls().get("teacher");
if (StrUtil.isBlank(dateText)) { String apiKey = dataCenterProperties.getApiKey();
return dateText;
}
if (dateText.matches("\\d{4}-\\d{1,2}-\\d{1,2}")) {
String[] parts = dateText.split("-");
return parts[0] + "-" + StrUtil.padPre(parts[1], 2, '0') + "-" + StrUtil.padPre(parts[2], 2, '0');
}
if (dateText.matches("\\d{4}-\\d{1,2}")) {
String[] parts = dateText.split("-");
return parts[0] + "-" + StrUtil.padPre(parts[1], 2, '0') + "-01";
}
return dateText;
}
/**
* 获取信息中心开放平台访问令牌。
* 返回值为接口后续调用使用的 access_token 字符串。
*/
private String getDmpAccessToken() {
String tokenUrl = dataCenterProperties.getTokenUrl();
if (StrUtil.isBlank(tokenUrl)) {
throw new BaseException("未配置信息中心token地址: data-center.token-url");
}
String key = dataCenterProperties.getKey();
if (StrUtil.isBlank(key)) {
throw new BaseException("未配置信息中心token key: data-center.key");
}
String secret = dataCenterProperties.getSecret();
if (StrUtil.isBlank(secret)) {
throw new BaseException("未配置信息中心token secret: data-center.secret");
}
// 信息中心token接口要求key和secret通过URL参数传入,配置中分开维护,调用时统一组装。
String params = URLUtil.buildQuery(Map.of("key", key, "secret", secret), StandardCharsets.UTF_8);
HttpRequest tokenHttpRequest = HttpUtil.createGet(tokenUrl + "?" + params);
log.info("请求信息中心token: {}", tokenHttpRequest);
String tokenResBody = tokenHttpRequest.execute().body();
log.info("请求信息中心token结果: {}", tokenResBody);
JSONObject tokenJsonBody = JSONUtil.parseObj(tokenResBody);
String accessToken = tokenJsonBody.getStr("access_token");
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("result") != null) {
accessToken = tokenJsonBody.getJSONObject("result").getStr("access_token");
}
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("data") != null) {
accessToken = tokenJsonBody.getJSONObject("data").getStr("access_token");
}
if (StrUtil.isBlank(accessToken)) {
accessToken = tokenJsonBody.getStr("token");
}
if (StrUtil.isBlank(accessToken)) {
throw new BaseException("获取信息中心token失败");
}
return accessToken;
}
/**
* 读取信息中心人员接口地址。
* 入参 key 对应 data-center.urls 下的配置项,返回值为具体接口URL。
*/
private String getDmpUrl(String key) {
String url = dataCenterProperties.getUrls().get(key);
if (StrUtil.isBlank(url)) { if (StrUtil.isBlank(url)) {
throw new BaseException("未配置信息中心人员接口地址: data-center.urls." + key); throw new BaseException("未配置人员接口地址: data-center.urls.teacher");
}
if (StrUtil.isBlank(apiKey)) {
throw new BaseException("未配置人员接口密钥: data-center.api-key");
} }
return url;
}
/**
* 分页拉取单个人员接口数据。
* 入参 url 为人员接口地址,accessToken 为信息中心令牌,sourceName 用于日志区分接口来源。
* 返回值为该接口所有分页合并后的原始人员 JSON 列表。
*/
private List<JSONObject> pullDmpUsers(String url, String accessToken, String sourceName) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
int total = 0;
do {
// 信息中心接口单页最多返回1000条,按page循环拉完当前接口全部数据。
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
log.info("请求{}人员数据,第{}页: {}", sourceName, page, httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求{}人员数据第{}页结果: {}", sourceName, page, resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 10000) {
throw new BaseException("获取" + sourceName + "人员数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
}
JSONObject result = jsonBody.getJSONObject("result");
if (result == null) {
break;
}
total = result.getInt("total", total);
maxPage = result.getInt("max_page", maxPage);
JSONArray data = result.getJSONArray("data");
if (CollUtil.isNotEmpty(data)) {
rawDataList.addAll(data.stream().map(v -> (JSONObject) v).toList());
}
log.info("{}人员数据拉取进度: {}/{}", sourceName, rawDataList.size(), total);
page++;
} while (page <= maxPage);
return rawDataList;
}
/**
* 合并两个接口数据时按职工号去重,避免同一批次出现重复人员。
* 返回值保持第一次出现的数据,用于后续统一映射入库。
*/
private List<JSONObject> distinctDmpUsers(List<JSONObject> rawDataList) {
Map<String, JSONObject> userMap = new LinkedHashMap<>(); Map<String, JSONObject> userMap = new LinkedHashMap<>();
for (JSONObject raw : rawDataList) { int pageNum = 1;
String loginName = raw.getStr("ZGH"); int pageSize = USER_PAGE_SIZE;
if (StrUtil.isBlank(loginName)) { long totalCount = -1;
loginName = raw.getStr("ID"); long receivedCount = 0;
do {
JSONObject body;
// 响应使用完毕即关闭;日志仅记录进度,不输出含 apiKey 的请求对象。
try (HttpResponse response = HttpUtil.createGet(url)
.header("apiKey", apiKey)
.form("pageNum", pageNum)
.form("pageSize", pageSize)
.timeout(REQUEST_TIMEOUT)
.execute()) {
if (!response.isOk()) {
throw new BaseException("获取人员数据失败,第{}页HTTP状态码: {}", pageNum, response.getStatus());
}
String responseBody = response.body();
if (!JSONUtil.isTypeJSONObject(responseBody)) {
throw new BaseException("获取人员数据失败,第{}页返回内容不是JSON对象", pageNum);
}
body = JSONUtil.parseObj(responseBody);
} }
userMap.putIfAbsent(loginName, raw); if (!Integer.valueOf(0).equals(body.getInt("status"))) {
} throw new BaseException("获取人员数据失败,第{}页status: {}", pageNum, body.getInt("status"));
}
JSONObject payload = body.getJSONObject("payload");
if (payload == null) {
throw new BaseException("获取人员数据失败,第{}页缺少payload", pageNum);
}
Integer responsePage = payload.getInt("pageNo");
Integer responseSize = payload.getInt("pageSize");
Long responseTotal = payload.getLong("totalCount");
JSONArray records = payload.getJSONArray("resultList");
if (!Integer.valueOf(pageNum).equals(responsePage) || responseSize == null || responseSize <= 0
|| responseTotal == null || responseTotal < 0 || records == null) {
throw new BaseException("人员接口第{}页分页信息或resultList不合法", pageNum);
}
// 以首个响应的实际分页大小为准;后续总数或页大小变化时中止,避免漏同步。
if (pageNum == 1) {
totalCount = responseTotal;
pageSize = responseSize;
} else if (totalCount != responseTotal || pageSize != responseSize) {
throw new BaseException("人员接口分页总数或每页条数发生变化,请重新同步");
}
long expectedCount = Math.min(pageSize, totalCount - receivedCount);
if (records.size() != expectedCount) {
throw new BaseException("人员接口第{}页数据不完整,预期{}条,实际{}条", pageNum, expectedCount, records.size());
}
int previousSize = userMap.size();
for (int i = 0; i < records.size(); i++) {
JSONObject raw = records.getJSONObject(i);
if (raw == null || StrUtil.isBlank(raw.getStr("GH")) || StrUtil.isBlank(raw.getStr("XM"))) {
throw new BaseException("人员接口存在缺少GH或XM的数据");
}
userMap.putIfAbsent(raw.getStr("GH").trim(), raw);
}
if (!records.isEmpty() && userMap.size() == previousSize) {
throw new BaseException("人员接口第{}页未返回新的人员编码,请检查分页是否生效", pageNum);
}
receivedCount += records.size();
log.info("人员数据拉取进度: {}/{},当前第{}页", receivedCount, totalCount, pageNum);
pageNum++;
} while (receivedCount < totalCount);
return new ArrayList<>(userMap.values()); return new ArrayList<>(userMap.values());
} }
/**
* 根据接口返回的所在单位号转换数据源用户单位。
* 入参 rawUnitId 对应人员接口 SZDWH:5位表示三级单位,需要写入其二级父单位;其他长度保持接口原值。
* 返回值为 sys_user_source.unitId 使用的单位ID,找不到三级单位或父级为空时保留接口原始单位号,避免人员数据丢失。
*/
private String resolveSourceUnitId(String rawUnitId) {
if (StrUtil.isBlank(rawUnitId) || rawUnitId.length() != 5) {
return rawUnitId;
}
Sys_unit unit = dao().fetch(Sys_unit.class, rawUnitId);
if (unit == null || StrUtil.isBlank(unit.getParentId())) {
return rawUnitId;
}
return unit.getParentId();
}
@Async @Async
private void updateDict(List<Sys_user_source> userSources) { private void updateDict(List<Sys_user_source> userSources) {
//判断是否要更新在职状态字典 //判断是否要更新在职状态字典
@@ -0,0 +1,171 @@
package com.budwk.app.sys.services.impl;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.services.SysHomeNewsService;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** 岭南工会首页新闻获取;只解析公开标题和链接,不抓取或执行文章脚本。 */
@IocBean
public class SysHomeNewsServiceImpl implements SysHomeNewsService {
private static final Log log = Logs.get();
private static final String HOME_URL = "https://d.lingnan.edu.cn/gh/index.htm";
private static final String CACHE_KEY = "budwk5mini:lingnan:home_news:v1";
private static final String LAST_SUCCESS_KEY = CACHE_KEY + ":last_success";
private static final Pattern IMAGE_PATTERN = Pattern.compile(
"u_u10_imgs\\.addImg\\(\\s*\"([^\"]+)\"\\s*,\\s*\"(\\d+)\"");
@Inject
private RedisService redisService;
private List<NutMap> lastSuccess;
private long nextRefreshAt;
/**
* 无入参;返回接口约定的六栏目列表。成功结果缓存30分钟,网络失败时使用最近成功数据。
* 同一实例串行刷新,有历史结果时失败后一分钟内不重复请求官网;Redis不可用时仍可使用实例内缓存。
*/
@Override
public synchronized List<NutMap> getNews() {
if (lastSuccess != null && System.currentTimeMillis() < nextRefreshAt) {
return lastSuccess;
}
List<NutMap> cached = readCache(CACHE_KEY);
if (cached != null) {
// Redis的剩余有效期可能不足30分钟,下一次请求仍以Redis是否过期为准。
lastSuccess = cached;
return cached;
}
try {
Document document = Jsoup.connect(HOME_URL).userAgent("Mozilla/5.0")
.timeout(10000).maxBodySize(2 * 1024 * 1024).get();
List<NutMap> news = parseHomepage(document);
lastSuccess = news;
nextRefreshAt = System.currentTimeMillis() + 30 * 60 * 1000L;
try {
String json = Json.toJson(news);
redisService.setex(LAST_SUCCESS_KEY, 7 * 24 * 60 * 60, json);
redisService.setex(CACHE_KEY, 30 * 60, json);
} catch (Exception e) {
log.warn("工会新闻缓存写入失败,使用本次获取结果", e);
}
return news;
} catch (Exception e) {
log.warn("岭南工会首页获取失败,尝试使用最近成功数据", e);
if (lastSuccess == null) {
lastSuccess = readCache(LAST_SUCCESS_KEY);
}
if (lastSuccess != null) {
nextRefreshAt = System.currentTimeMillis() + 60 * 1000L;
return lastSuccess;
}
throw new BaseException("工会官网新闻暂时无法获取,请稍后重试");
}
}
/** 缓存不存在、损坏或Redis不可用时返回null,不阻断官网抓取。 */
private List<NutMap> readCache(String key) {
try {
String json = redisService.get(key);
if (json != null && !json.isBlank()) {
List<NutMap> list = Json.fromJsonAsList(NutMap.class, json);
if (list != null && list.size() == 6) {
return list;
}
}
} catch (Exception e) {
log.warn("工会新闻缓存读取失败", e);
}
return null;
}
/** 按官网栏目顺序解析;任何栏目无法识别时不覆盖最近成功缓存,避免发布残缺数据。 */
private List<NutMap> parseHomepage(Document document) {
String[][] columns = {
{"新闻中心", "c123980", "index/xwzx__.htm"},
{"通知公告", "c123972", "index/tzgg__.htm"},
{"女工青工", "c123973", "index/ngqg_.htm"},
{"文体工作", "c123974", "index/wtgz__.htm"},
{"岭师教工", "c123975", "index/lsjg_.htm"}
};
List<NutMap> result = new ArrayList<>();
for (String[] column : columns) {
List<NutMap> articles = new ArrayList<>();
Set<String> links = new HashSet<>();
for (Element anchor : document.select("a." + column[1] + "[href]")) {
String href = absoluteUrl(anchor.attr("href"));
String title = anchor.attr("title").trim();
if (title.isEmpty()) {
title = anchor.text().trim();
}
if (href.isEmpty() || title.isEmpty() || !links.add(href)) {
continue;
}
Element row = anchor.closest("tr");
Element date = row == null ? null : row.selectFirst("td.timestyle" + column[1].substring(1));
// 官网仅给月日或未给日期时原样保留,不推算年份、不从文章标题猜日期。
articles.add(NutMap.NEW().addv("text", title).addv("href", href)
.addv("time", date == null ? "" : date.text().trim()));
}
if (articles.isEmpty()) {
throw new IllegalStateException("无法识别工会首页栏目:" + column[0]);
}
result.add(NutMap.NEW().addv("label", column[0]).addv("href", absoluteUrl(column[2])).addv("value", articles));
}
// 图片标签初始src为loading.gif;只读取预加载脚本中的地址和索引,不执行脚本。
Map<Integer, String> images = new HashMap<>();
Matcher matcher = IMAGE_PATTERN.matcher(document.html());
while (matcher.find()) {
images.put(Integer.parseInt(matcher.group(2)), absoluteUrl(matcher.group(1).replace("\\/", "/")));
}
List<NutMap> pictures = new ArrayList<>();
int index = 0;
for (Element image : document.select("#u_u10_demo1 img[name=u_u10_img]")) {
String source = images.get(index++);
Element anchor = image.closest("a[href]");
String href = anchor == null ? "" : absoluteUrl(anchor.attr("href"));
if (source == null || source.isEmpty() || href.isEmpty()) {
throw new IllegalStateException("工会首页图片地址解析失败");
}
pictures.add(NutMap.NEW().addv("text", image.attr("alt")).addv("href", href)
.addv("time", "").addv("image", source));
}
if (pictures.isEmpty()) {
throw new IllegalStateException("无法识别工会首页图片展示栏目");
}
result.add(NutMap.NEW().addv("label", "图片展示").addv("href", absoluteUrl("index/tpzs.htm")).addv("value", pictures));
return result;
}
/** 官网相对路径补全为绝对地址;仅返回HTTP(S)链接,保留原文外链及其查询参数。 */
private String absoluteUrl(String value) {
if (value == null || value.isBlank()) {
return "";
}
try {
URI uri = URI.create(HOME_URL).resolve(value.trim());
return (("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
&& uri.getHost() != null) ? uri.toString() : "";
} catch (IllegalArgumentException e) {
return "";
}
}
}
@@ -1,5 +1,8 @@
package com.budwk.app.sys.services.impl; package com.budwk.app.sys.services.impl;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RedisConstant; import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
@@ -19,6 +22,10 @@ import org.nutz.lang.Strings;
import org.nutz.plugins.wkcache.annotation.CacheDefaults; import org.nutz.plugins.wkcache.annotation.CacheDefaults;
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll; import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** /**
* Created by wizzer on 2016/12/22. * Created by wizzer on 2016/12/22.
*/ */
@@ -29,6 +36,29 @@ public class SysUnitServiceImpl extends BaseServiceImpl<Sys_unit> implements Sys
super(dao); super(dao);
} }
/**
* pid为空时以学校单位1为展示根;指定pid时返回该单位及其下级,不依赖实体单位0存在。
* 虚拟根仅用于组织返回结构,不修改数据库中的父级关系;缺少部门类别时按普通分类0展示。
*/
@Override
public List<Tree<String>> getUnitTree(String pid) {
String rootId = StrUtil.blankToDefault(pid, "1");
String virtualRootId = "root";
List<Sys_unit> units = query(Cnd.NEW().asc("unitcode"));
if (units.stream().noneMatch(unit -> rootId.equals(unit.getId()))) {
return List.of();
}
List<TreeNode<String>> nodes = new ArrayList<>();
for (int i = 0; i < units.size(); i++) {
Sys_unit unit = units.get(i);
// 仅将本次查询根挂到虚拟根下,其他节点保留实际父级,避免学校与下级并列。
String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId();
nodes.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(Map.of("unitTypeCode", unit.getUnitTypeCode() == null ? 0 : unit.getUnitTypeCode())));
}
return TreeUtil.build(nodes, virtualRootId);
}
/** /**
* 按单位编码或名称筛选当前分工会的组成单位,并按单位编码倒序分页。 * 按单位编码或名称筛选当前分工会的组成单位,并按单位编码倒序分页。
*/ */
+89 -9
View File
@@ -1,17 +1,85 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="false" scanPeriod="60000" debug="false"> <configuration scan="false" scanPeriod="60000" debug="false">
<!-- 定义日志文件的存储路径 --> <!-- 日志存储路径 -->
<property name="LOG_HOME" value="./logs"/> <property name="LOG_HOME" value="./logs"/>
<!-- 控制台输出配置 --> <!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<!-- INFO级别日志文件配置 -->
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)start #
# #
#################################################################################
-->
<!-- 所有级别日志写入同一个文件 -->
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<!-- 框架日志级别(按需调整) -->
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/>
<!-- root loggerDEBUG 级别,输出到控制台和全量文件 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ALL_FILE"/>
</root>
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)end #
# #
#################################################################################
-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 start #
# #
#################################################################################
-->
<!-- DEBUG级别日志文件配置 -->
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
&lt;!&ndash; INFO级别日志文件配置 &ndash;&gt;
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file> <file>${LOG_HOME}/info.log</file>
<encoder> <encoder>
@@ -24,11 +92,11 @@
</filter> </filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <maxHistory>300</maxHistory>
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<!-- ERROR级别日志文件配置 --> &lt;!&ndash; ERROR级别日志文件配置 &ndash;&gt;
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file> <file>${LOG_HOME}/error.log</file>
<encoder> <encoder>
@@ -41,19 +109,31 @@
</filter> </filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <maxHistory>300</maxHistory>
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<logger name="java" additivity="false" /> &lt;!&ndash; 框架日志级别 &ndash;&gt;
&lt;!&ndash; <logger name="java" additivity="false" />&ndash;&gt;
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/> <logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/> <logger name="org.nutz" level="DEBUG"/>
<!-- 日志级别和appender的关联 --> &lt;!&ndash; 日志级别和appender的关联 &ndash;&gt;
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/> <appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/> <appender-ref ref="ERROR_FILE"/>
</root> </root>-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 end #
# #
#################################################################################
-->
</configuration> </configuration>
@@ -153,7 +153,7 @@ layout("/layouts/v4/baseLayout.html"){
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template> <work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt :list="websiteNews"></jcdt> <jcdt :list="websiteNews" :loading="newsLoading" :error="newsError" @retry="getWebSiteNews"></jcdt>
</div> </div>
</div> </div>
@@ -172,6 +172,8 @@ layout("/layouts/v4/baseLayout.html"){
data(){ data(){
return{ return{
websiteNews: [], websiteNews: [],
newsLoading: false,
newsError: "",
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item) homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
} }
}, },
@@ -188,10 +190,20 @@ layout("/layouts/v4/baseLayout.html"){
}, },
methods: { methods: {
getWebSiteNews(){ getWebSiteNews(){
if (this.newsLoading) return
this.newsLoading = true
this.newsError = ""
// 无请求参数;data为六栏目数组,value包含标题、日期、原文链接和可选图片。
this.$axios.post('/platform/home/getNews').then(res => { this.$axios.post('/platform/home/getNews').then(res => {
if (res.code === 0) { if (res.code === 0) {
this.websiteNews = res.data this.websiteNews = res.data || []
} else {
this.newsError = res.msg || "工会新闻暂时无法获取"
} }
}).catch(() => {
this.newsError = "工会新闻暂时无法获取,请稍后重试"
}).finally(() => {
this.newsLoading = false
}) })
} }
} }
+168 -16
View File
@@ -1,18 +1,52 @@
const jcdt = { const jcdt = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<div class="jcdt-wrapper"> <div class="jcdt-wrapper">
<div class="jcdt-container"> <div class="jcdt-container" v-loading="loading">
<el-carousel height="360px" :interval="10000" indicator-position="none"> <div class="jcdt-tabs" v-if="list.length">
<el-carousel-item v-for="item,index in list" :key="index"> <el-button v-for="(column, index) in list" :key="column.label"
:type="activeIndex === index ? 'primary' : 'default'" size="small"
@click="selectColumn(index)">{{ column.label }}</el-button>
</div>
<div class="jcdt-empty" v-if="error">
{{ error }} <el-button type="text" @click="$emit('retry')">重试</el-button>
</div>
<el-empty v-if="!loading && !error && !list.length" description="暂无工会新闻"></el-empty>
<el-carousel v-if="list.length" ref="newsCarousel" height="430px" :interval="10000"
:autoplay="!isImageColumn(list[activeIndex])"
indicator-position="none" @change="onColumnChange">
<el-carousel-item v-for="item,index in displayColumns" :key="index">
<div class="jcdt-title"> <div class="jcdt-title">
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
<el-link :href="item.href" target="_blank" rel="noopener noreferrer" type="primary">更多</el-link>
</div> </div>
<div class="jcdt-content"> <div class="jcdt-content" :class="{ 'jcdt-image-content': isImageColumn(item) }">
<div class="jcdt-list"> <!-- 两组相同图片同步移动一个组宽,首尾衔接;隐藏栏目暂停动画。 -->
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)"> <div v-if="isImageColumn(item)" class="jcdt-image-track" :data-column="index">
<div class="date" v-if="o.time"> <div v-for="copy in 2" :key="copy" class="jcdt-image-group"
:style="{ '--image-count': item.scrollImages.length }">
<a class="jcdt-item" v-for="(o, i) in item.scrollImages" :key="i"
:href="o.href" target="_blank" rel="noopener noreferrer">
<div class="image">
<el-image :src="o.image" :alt="o.text" fit="cover">
<div slot="error">图片暂不可用</div>
</el-image>
</div>
<div class="title">{{ o.text }}</div>
</a>
</div>
</div>
<div v-else class="jcdt-list">
<a class="jcdt-item" v-for="o,i in item.value" :key="i" :href="o.href"
target="_blank" rel="noopener noreferrer">
<div class="image" v-if="o.image">
<el-image :src="o.image" :alt="o.text" fit="cover">
<div slot="error">图片暂不可用</div>
</el-image>
</div>
<!-- 源站未提供日期时仍保留时钟和行高,不补造日期。 -->
<div class="date" v-if="o.time || !o.image">
<i class="el-icon-alarm-clock"></i> <i class="el-icon-alarm-clock"></i>
<span>{{o.time}}</span> <span v-if="o.time">{{o.time}}</span>
</div> </div>
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<div> <div>
@@ -20,33 +54,149 @@ const jcdt = {
</div> </div>
<div class="title">{{o.text}}</div> <div class="title">{{o.text}}</div>
</div> </div>
</div> </a>
</div> </div>
</div> </div>
</el-carousel-item> </el-carousel-item>
</el-carousel> </el-carousel>
<slot></slot>
</div> </div>
</div> </div>
`, `,
data() { data() {
return {} return { activeIndex: 0 }
}, },
props: { props: {
list: { list: {
type: Array, type: Array,
default: [] default: () => []
},
loading: { type: Boolean, default: false },
error: { type: String, default: "" }
},
created() {
// 动画状态不参与 Vue 渲染,避免每一帧重新生成新闻卡片。
this._imageScroll = { frame: null, track: null, offset: 0, previousTime: null }
},
mounted() {
this.syncImageScroll()
},
beforeDestroy() {
this.stopImageScroll()
},
watch: {
activeIndex() {
this.syncImageScroll()
},
list: {
deep: true,
handler() {
this.syncImageScroll()
}
}
},
computed: {
// 图片不足四张时重复完整序列,保证宽屏滚动也不会露出空白;不修改接口原数据。
displayColumns() {
return this.list.map(item => {
if (!this.isImageColumn(item)) return item
const images = item.value || []
const scrollImages = []
if (images.length) {
const copies = Math.ceil(4 / images.length)
for (let i = 0; i < copies; i++) scrollImages.push(...images)
}
return Object.assign({}, item, { scrollImages })
})
} }
}, },
methods: { methods: {
onLink(item) { // 切栏、刷新数据后等待 DOM 更新;只为当前图片栏目启动一个动画循环。
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href syncImageScroll() {
window.open(url) this.stopImageScroll()
this.$nextTick(() => {
if (this._isDestroyed || this._isBeingDestroyed) return
this.stopImageScroll()
if (!this.isImageColumn(this.list[this.activeIndex])) return
const track = this.$el.querySelector('.jcdt-image-track[data-column="' + this.activeIndex + '"]')
if (!track || !track.querySelector('.jcdt-item')) return
const state = this._imageScroll
if (state.track !== track) {
state.track = track
state.offset = 0
}
state.frame = requestAnimationFrame(this.advanceImageScroll)
})
}, },
isHttpOrHttps(url) { // 浏览器帧时间戳为毫秒;按每秒 45 像素移动,超过首组宽度后无缝回绕。
return /^(http:\/\/|https:\/\/)/i.test(url) advanceImageScroll(timestamp) {
const state = this._imageScroll
state.frame = null
if (this._isDestroyed || this._isBeingDestroyed || !this.isImageColumn(this.list[this.activeIndex])) return
const group = state.track && state.track.firstElementChild
if (!group) return
const width = group.getBoundingClientRect().width
// 限制后台标签恢复后的单帧位移;实时读取组宽以适应窗口尺寸变化。
const elapsed = state.previousTime === null ? 0 : Math.min(timestamp - state.previousTime, 100)
state.previousTime = timestamp
if (width > 0) {
state.offset = (state.offset + elapsed * 0.045) % width
state.track.style.transform = 'translate3d(-' + state.offset + 'px, 0, 0)'
}
state.frame = requestAnimationFrame(this.advanceImageScroll)
},
// 离开栏目或销毁组件时取消待执行帧,保留当前位置供再次进入时续播。
stopImageScroll() {
const state = this._imageScroll
if (!state) return
if (state.frame !== null) cancelAnimationFrame(state.frame)
state.frame = null
state.previousTime = null
},
// 栏目对象来自新闻接口;仅图片展示使用连续滚动,并暂停外层自动切栏。
isImageColumn(item) {
return !!item && item.label === '图片展示'
},
// 手动栏目切换与轮播共用同一索引,避免标题按钮和内容不同步。
selectColumn(index) {
this.$refs.newsCarousel.setActiveItem(index)
},
onColumnChange(index) {
this.activeIndex = index
} }
}, },
style: /*language=CSS*/ ` style: /*language=CSS*/ `
.jcdt-tabs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.jcdt-empty { text-align: center; padding: 20px; }
/deep/ .jcdt-content { height: 330px; overflow-y: auto; padding: 8px; }
/deep/ .jcdt-image-content { overflow: hidden; --visible-cards: 4; }
/deep/ .jcdt-image-track { display: flex; width: 100%; will-change: transform; }
/* 每组含末尾间距,平移整组宽度后与下一组完全重合。 */
/deep/ .jcdt-image-group {
display: grid;
grid-template-columns: repeat(var(--image-count), minmax(0, 1fr));
gap: 20px;
padding-right: 20px;
box-sizing: border-box;
flex: 0 0 calc((100% + 20px) / var(--visible-cards) * var(--image-count));
}
/deep/ .jcdt-image-group .title { height: 45px; }
/deep/ .jcdt-title .el-link { margin-left: 16px; }
/deep/ .jcdt-item { color: inherit; text-decoration: none; min-width: 0; }
/deep/ .jcdt-item .el-image { width: 100%; height: 100%; }
@media (max-width: 900px) {
/deep/ .jcdt-image-content { --visible-cards: 2; }
/deep/ .jcdt-list { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}
@media (max-width: 520px) {
/deep/ .jcdt-image-content { --visible-cards: 1; }
/deep/ .jcdt-list { grid-template-columns: minmax(0, 1fr) !important; }
}
.jcdt-wrapper { .jcdt-wrapper {
width: 100%; width: 100%;
background-size: cover; background-size: cover;
@@ -122,6 +272,8 @@ const jcdt = {
} }
/deep/ .jcdt-item .date { /deep/ .jcdt-item .date {
min-height: 28px;
line-height: 28px;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
white-space: nowrap; white-space: nowrap;
@@ -4,7 +4,7 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<common-query ref="commonQueryRef" user-attribute-multiple @search="search"></common-query> <common-query ref="commonQueryRef" @search="search"></common-query>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
<table-tool label="申请列表"> <table-tool label="申请列表">
@@ -40,12 +40,6 @@
<dict-select v-model="pageForm.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE" <dict-select v-model="pageForm.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
style="width: 100%"></dict-select> style="width: 100%"></dict-select>
</search-item>--> </search-item>-->
<search-item label="人员属性">
<dict-select v-if="userAttributeMultiple" v-model="pageForm.userAttributes" code="USER_ATTRIBUTE"
multiple collapse-tags clearable style="width: 100%"></dict-select>
<dict-select v-else v-model="pageForm.userAttribute" code="USER_ATTRIBUTE"
clearable style="width: 100%"></dict-select>
</search-item>
</template> </template>
</search> </search>
</script> </script>
@@ -1,12 +1,5 @@
const COMMON_QUERY = { const COMMON_QUERY = {
template: "#member-apply-common-query-template", template: "#member-apply-common-query-template",
props: {
// 默认保持原单选行为,仅由明确传参的页面开启人员属性多选。
userAttributeMultiple: {
type: Boolean,
default: false
}
},
store, store,
mixins: [initTableMixins], mixins: [initTableMixins],
data() { data() {
@@ -21,8 +14,6 @@ const COMMON_QUERY = {
personTypes: [], personTypes: [],
preparedBys: [], preparedBys: [],
aidFundMemberUserType: '', aidFundMemberUserType: '',
userAttribute: '',
userAttributes: [],
} }
} }
}, },
@@ -32,7 +23,6 @@ const COMMON_QUERY = {
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys) pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$emit('search', pageForm) this.$emit('search', pageForm)
}, },
async initData(){ async initData(){
@@ -4,7 +4,7 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<common-query ref="commonQueryRef" user-attribute-multiple @search="search"></common-query> <common-query ref="commonQueryRef" @search="search"></common-query>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -4,7 +4,7 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<common-query ref="commonQueryRef" user-attribute-multiple @search="search"></common-query> <common-query ref="commonQueryRef" @search="search"></common-query>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -5,7 +5,7 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<common-query ref="commonQueryRef" user-attribute-multiple @search="search"></common-query> <common-query ref="commonQueryRef" @search="search"></common-query>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -29,12 +29,5 @@
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select> code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>--> </search-item>-->
<search-item label="人员属性">
<dict-select v-if="userAttributeMultiple" v-model="pageForm.userAttributes"
placeholder="请选择人员属性" @change="doSearch" code="USER_ATTRIBUTE"
multiple collapse-tags clearable></dict-select>
<dict-select v-else v-model="pageForm.userAttribute" placeholder="请选择人员属性"
@change="doSearch" code="USER_ATTRIBUTE"></dict-select>
</search-item>
</search> </search>
</script> </script>
@@ -1,12 +1,5 @@
const COMMON_QUERY = { const COMMON_QUERY = {
template: "#member-change-common-query-template", template: "#member-change-common-query-template",
props: {
// 未显式开启时保持原单选行为,避免影响未列入本次范围的页面。
userAttributeMultiple: {
type: Boolean,
default: false
}
},
store, store,
mixins: [initTableMixins], mixins: [initTableMixins],
data() { data() {
@@ -21,8 +14,6 @@ const COMMON_QUERY = {
personTypes: [], personTypes: [],
preparedBys: [], preparedBys: [],
aidFundMemberUserType: '', aidFundMemberUserType: '',
userAttribute: '',
userAttributes: [],
} }
} }
}, },
@@ -32,8 +23,6 @@ const COMMON_QUERY = {
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys) pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
// 多选人员属性沿用现有数组查询参数的JSON提交方式。
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$emit('search', pageForm) this.$emit('search', pageForm)
}, },
async initData(){ async initData(){
@@ -73,8 +73,8 @@ const INFO = {
</template> </template>
</el-descriptions> </el-descriptions>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="变更信息" name="changeInfo" v-if="viewData.changeInfos"> <el-tab-pane label="变更信息" name="changeInfo" v-if="visibleChangeInfos.length">
<el-table :data="viewData.changeInfos" :size="tableSize" @sort-change="pageOrder" ref="table" <el-table :data="visibleChangeInfos" :size="tableSize" @sort-change="pageOrder" ref="table"
row-key="id" style="width: 100%"> row-key="id" style="width: 100%">
<el-table-column :index="indexMethod" align="center" header-align="center" <el-table-column :index="indexMethod" align="center" header-align="center"
label="序号" type="index" width="80px"></el-table-column> label="序号" type="index" width="80px"></el-table-column>
@@ -144,6 +144,12 @@ const INFO = {
activeName: "basicInfo", activeName: "basicInfo",
} }
}, },
computed: {
// 人员属性不再展示,其他变更明细保持原有顺序,不改写后端返回的历史数据。
visibleChangeInfos() {
return (this.viewData.changeInfos || []).filter((item) => item.field !== "userAttribute" && item.fieldName !== "人员属性")
}
},
methods: { methods: {
// 打开 // 打开
onOpen(row) { onOpen(row) {
@@ -1,5 +1,6 @@
const MEMBER_ALL_CHANGE_INFO = { const MEMBER_ALL_CHANGE_INFO = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<div>
<el-tabs v-model="activeName"> <el-tabs v-model="activeName">
<el-tab-pane label="基础信息" name="basicInfo"> <el-tab-pane label="基础信息" name="basicInfo">
<el-descriptions :column="3" border class="descriptions-form"> <el-descriptions :column="3" border class="descriptions-form">
@@ -61,9 +62,9 @@ const MEMBER_ALL_CHANGE_INFO = {
</template> </template>
</el-descriptions> </el-descriptions>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="历史变更记录" name="changeInfo" v-if="viewData.allChangeInfo"> <el-tab-pane label="历史变更记录" name="changeInfo" v-if="visibleChangeHistory.length">
<el-timeline> <el-timeline>
<el-timeline-item placement="top" v-for="(changeData, index) in viewData.allChangeInfo" <el-timeline-item placement="top" v-for="(changeData, index) in visibleChangeHistory"
:key="index" :timestamp="changeData.timestamp"> :key="index" :timestamp="changeData.timestamp">
<el-card> <el-card>
<h4>{{ changeData.changeOriginName }}</h4> <h4>{{ changeData.changeOriginName }}</h4>
@@ -87,6 +88,8 @@ const MEMBER_ALL_CHANGE_INFO = {
</el-timeline> </el-timeline>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<slot></slot>
</div>
`, `,
store, store,
data(){ data(){
@@ -95,6 +98,16 @@ const MEMBER_ALL_CHANGE_INFO = {
activeName: "basicInfo" activeName: "basicInfo"
} }
}, },
computed: {
// 仅过滤展示的人员属性明细,保留原始历史;过滤后无明细的时间节点不展示。
visibleChangeHistory() {
return (this.viewData.allChangeInfo || []).map((entry) => {
return Object.assign({}, entry, {
changeInfos: (entry.changeInfos || []).filter((item) => item.field !== "userAttribute" && item.fieldName !== "人员属性")
})
}).filter((entry) => entry.changeInfos.length > 0)
}
},
methods:{ methods:{
onOpen(userId){ onOpen(userId){
this.$axios.post('/platform/member/change/manage/getAllChangeMemberInfo', {userId}) this.$axios.post('/platform/member/change/manage/getAllChangeMemberInfo', {userId})
@@ -149,12 +149,6 @@ const MEMBER_CHANGE = {
:disabled="allowFields('personType')" style="width: 100%"></dict-select> :disabled="allowFields('personType')" style="width: 100%"></dict-select>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="人员属性">
<el-form-item prop="userAttribute">
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
style="width: 100%"></dict-select>
</el-form-item>
</el-descriptions-item>
<!-- <el-descriptions-item label="人员分类">--> <!-- <el-descriptions-item label="人员分类">-->
<!-- <el-form-item prop="aidFundMemberUserType">--> <!-- <el-form-item prop="aidFundMemberUserType">-->
<!-- <dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"--> <!-- <dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"-->
@@ -530,6 +524,7 @@ const MEMBER_CHANGE = {
this.$set(this.formData, "userState", userState) this.$set(this.formData, "userState", userState)
this.$set(this.formData, "personType", personType) this.$set(this.formData, "personType", personType)
this.$set(this.formData, "preparedBy", preparedBy) this.$set(this.formData, "preparedBy", preparedBy)
// 仅保留原值随表单传递,避免移除控件后被变更比较误判为清空。
this.$set(this.formData, "userAttribute", userAttribute) this.$set(this.formData, "userAttribute", userAttribute)
this.$set(this.formData, "aidFundMemberUserType", aidFundMemberUserType) this.$set(this.formData, "aidFundMemberUserType", aidFundMemberUserType)
this.$set(this.formData, "unitName", unit ? unit.name : null) this.$set(this.formData, "unitName", unit ? unit.name : null)
@@ -50,10 +50,6 @@ layout("/layouts/platform.html"){
<!-- <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"--> <!-- <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"-->
<!-- code="AIDFUND_MEMBER_USER_TYPE"></dict-select>--> <!-- code="AIDFUND_MEMBER_USER_TYPE"></dict-select>-->
<!-- </search-item>--> <!-- </search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
<search-item label="更新日期"> <search-item label="更新日期">
<el-date-picker :picker-options="pickerOptions" @change="changeDateRangeChange" <el-date-picker :picker-options="pickerOptions" @change="changeDateRangeChange"
align="right" end-placeholder="结束日期" format="yyyy-MM-dd" align="right" end-placeholder="结束日期" format="yyyy-MM-dd"
@@ -76,10 +72,6 @@ layout("/layouts/platform.html"){
设置人员分类 设置人员分类
</el-button> </el-button>
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserAttribute" class="ml5" icon="el-icon-edit"
size="small" type="primary">
设置人员属性
</el-button>
<el-button :disabled="!checkUsers.length" @click="openBatchEditUserState" class="ml5" icon="el-icon-edit" <el-button :disabled="!checkUsers.length" @click="openBatchEditUserState" class="ml5" icon="el-icon-edit"
size="small" type="primary"> size="small" type="primary">
@@ -238,48 +230,6 @@ layout("/layouts/platform.html"){
<member-all-change ref="memberAllChangeRef"></member-all-change> <member-all-change ref="memberAllChangeRef"></member-all-change>
</template> </template>
<el-drawer
:before-close="handleClose"
:visible.sync="batchEditUserAttributeVisible"
size="1000px" title="批量设置人员属性">
<el-timeline style="margin-right: 40px">
<el-timeline-item placement="top" timestamp="">
<el-form :model="batchFormDataUserAttribute" label-width="100px" ref="batchFormUserAttribute">
<el-row :gutter="60">
<el-col :span="12">
<el-form-item label="人员属性">
<dict-select
@change="checkUsers.map(v => $set(v, 'userAttribute', batchFormDataUserAttribute.userAttribute))"
clearable code="USER_ATTRIBUTE"
placeholder="请选择人员属性"
style="width: 300px"
v-model="batchFormDataUserAttribute.userAttribute"></dict-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-timeline-item>
<el-timeline-item placement="top" timestamp="">
<el-table :data="checkUsers" class="vi-table" height="70vh" row-key="id" style="width: 100%">
<el-table-column align="center" header-align="center" label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="姓名" prop="username" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="所属单位" prop="unitName" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="所属工会" prop="unionName" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="人员属性" prop="userAttribute" show-overflow-tooltip width="300px">
<template scope="{row}">
<dict-select clearable code="USER_ATTRIBUTE" placeholder="请选择人员属性" style="width: 200px"
v-model="row.userAttribute"></dict-select>
</template>
</el-table-column>
</el-table>
</el-timeline-item>
</el-timeline>
<span style="position: absolute;bottom: 20px;right: 20px">
<el-button @click="batchEditUserAttributeVisible = false;checkUsers=[];$refs.table.clearSelection()">取消</el-button>
<el-button :loading="batchEditLoading" @click="doBatchEditUserAttribute" type="primary">确定</el-button>
</span>
</el-drawer>
<el-drawer <el-drawer
:before-close="handleClose" :before-close="handleClose"
@@ -523,7 +473,6 @@ layout("/layouts/platform.html"){
isMember: 1, isMember: 1,
changeTypes: [], changeTypes: [],
aidFundMemberUserType: "", aidFundMemberUserType: "",
userAttributes: [],
}, },
unions: [], unions: [],
units: [], units: [],
@@ -531,12 +480,10 @@ layout("/layouts/platform.html"){
id: "", id: "",
checkUsers: [], checkUsers: [],
batchEditLoading: false, batchEditLoading: false,
batchEditUserAttributeVisible: false,
batchEditMemberVisible: false, batchEditMemberVisible: false,
batchEditAidFundTypeVisible: false, batchEditAidFundTypeVisible: false,
batchEditUserStateVisible: false, batchEditUserStateVisible: false,
unitMoveVisible: false, unitMoveVisible: false,
batchFormDataUserAttribute: {},
batchFormDataMember: {member: false, welfareMember: false}, batchFormDataMember: {member: false, welfareMember: false},
batchFormDataAidFundType: {}, batchFormDataAidFundType: {},
batchFormDataUserState: {}, batchFormDataUserState: {},
@@ -569,7 +516,6 @@ layout("/layouts/platform.html"){
{ prop: "personType", label: "人员类型", width: 120, sortable: true }, { prop: "personType", label: "人员类型", width: 120, sortable: true },
// 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。 // 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true }, // { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 }, { prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 }, { prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 }, { prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
@@ -597,16 +543,6 @@ layout("/layouts/platform.html"){
"member-all-change": MEMBER_ALL_CHANGE_INFO, "member-all-change": MEMBER_ALL_CHANGE_INFO,
}, },
methods: { methods: {
// 分页和导出共用该参数构建方法,保证人员属性多选值始终按 JSON 数组提交。
buildQueryPageForm(source = this.pageForm) {
const pageForm = clone(source)
pageForm.userAttributes = JSON.stringify(source.userAttributes || [])
return pageForm
},
// 覆盖公共分页方法,使后端按人员属性集合执行查询。
pageData(data = null) {
return initTableMixins.methods.pageData.call(this, this.buildQueryPageForm(data || this.pageForm))
},
dropdownCommand(command) { dropdownCommand(command) {
const {type, data} = command const {type, data} = command
if (type === 'view') { if (type === 'view') {
@@ -667,7 +603,7 @@ layout("/layouts/platform.html"){
* 导出会员名单 * 导出会员名单
*/ */
exportExcel() { exportExcel() {
const pageForm = this.buildQueryPageForm() const pageForm = clone(this.pageForm)
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => { const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
return {prop: item.prop, label: item.label} return {prop: item.prop, label: item.label}
}) })
@@ -731,10 +667,6 @@ layout("/layouts/platform.html"){
this.$refs.table.clearSelection() this.$refs.table.clearSelection()
}).catch(() => {}) }).catch(() => {})
}, },
openBatchEditUserAttribute() {
this.batchFormDataUserAttribute = {}
this.batchEditUserAttributeVisible = true
},
openBatchEditMember() { openBatchEditMember() {
this.batchFormDataMember = {member: false, welfareMember: false} this.batchFormDataMember = {member: false, welfareMember: false}
this.batchEditMemberVisible = true this.batchEditMemberVisible = true
@@ -785,23 +717,6 @@ layout("/layouts/platform.html"){
this.$set(this.unitMoveFormData, "unitName", "") this.$set(this.unitMoveFormData, "unitName", "")
} }
}, },
async doBatchEditUserAttribute() {
const batch = this.checkUsers.map(v => {
return {id: v.id, userAttribute: v.userAttribute}
})
this.batchEditLoading = true
const resp = await this.$axios.post("/platform/member/change/manage/batchUpdateUserAttribute", {users: JSON.stringify(batch)})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
this.$refs.table.clearSelection()
this.batchEditUserAttributeVisible = false
this.checkUsers = []
} else {
this.$message.error(resp.msg)
}
this.batchEditLoading = false
},
async doBatchEditMember() { async doBatchEditMember() {
const batch = this.checkUsers.map(v => { const batch = this.checkUsers.map(v => {
return {id: v.id, member: v.member, welfareMember: v.welfareMember} return {id: v.id, member: v.member, welfareMember: v.welfareMember}
@@ -4,7 +4,7 @@ layout("/layouts/platform.html"){
<div id="app"> <div id="app">
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<common-query ref="commonQueryRef" user-attribute-multiple @search="search"></common-query> <common-query ref="commonQueryRef" @search="search"></common-query>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -42,10 +42,6 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select> code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>--> </search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
</search> </search>
</el-card> </el-card>
@@ -151,7 +147,6 @@ layout("/layouts/platform.html"){
personType: "", personType: "",
changeType: "", changeType: "",
aidFundMemberUserType: "", aidFundMemberUserType: "",
userAttributes: []
} }
} }
}, },
@@ -198,8 +193,6 @@ layout("/layouts/platform.html"){
pageForm.personTypes = JSON.stringify(pageForm.personTypes) pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.changeTypes = JSON.stringify(pageForm.changeTypes) pageForm.changeTypes = JSON.stringify(pageForm.changeTypes)
pageForm.changeOrigins = JSON.stringify(pageForm.changeOrigins) pageForm.changeOrigins = JSON.stringify(pageForm.changeOrigins)
// 人员属性为多选条件,提交前转换为后端可解析的 JSON 数组。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
this.$axios.post(loc() + '/pageData', pageForm) this.$axios.post(loc() + '/pageData', pageForm)
.then(res => { .then(res => {
@@ -5,7 +5,7 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak> <div id="app" v-cloak>
<guava ref="guava"> <guava ref="guava">
<el-card shadow="never"> <el-card shadow="never">
<common-query ref="commonQueryRef" user-attribute-multiple @search="search"></common-query> <common-query ref="commonQueryRef" @search="search"></common-query>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -59,8 +59,8 @@
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="变更信息" name="changeInfo" v-if="viewData.changeInfos"> <el-tab-pane label="变更信息" name="changeInfo" v-if="visibleChangeInfos.length">
<el-table :data="viewData.changeInfos" :size="tableSize" @sort-change="pageOrder" ref="table" row-key="id" style="width: 100%"> <el-table :data="visibleChangeInfos" :size="tableSize" @sort-change="pageOrder" ref="table" row-key="id" style="width: 100%">
<el-table-column :index="indexMethod" align="center" header-align="center" <el-table-column :index="indexMethod" align="center" header-align="center"
label="序号" type="index" width="80px"></el-table-column> label="序号" type="index" width="80px"></el-table-column>
<el-table-column prop="fieldName" label="变更字段" header-align="center"></el-table-column> <el-table-column prop="fieldName" label="变更字段" header-align="center"></el-table-column>
@@ -119,6 +119,12 @@
doneTasks: [] doneTasks: []
} }
}, },
computed: {
// 人员属性不再展示,其他变更明细保持原有顺序,不改写后端返回的历史数据。
visibleChangeInfos() {
return (this.viewData.changeInfos || []).filter((item) => item.field !== "userAttribute" && item.fieldName !== "人员属性")
}
},
methods: { methods: {
info() { info() {
this.$axios.post("/platform/member/change/view/findMemberChangeRecord", { id: this.businessId }).then((res) => { this.$axios.post("/platform/member/change/view/findMemberChangeRecord", { id: this.businessId }).then((res) => {
@@ -73,7 +73,6 @@ const MEMBER_INFO = {
<el-descriptions-item label="人员类型">{{ viewData.personType }}</el-descriptions-item> <el-descriptions-item label="人员类型">{{ viewData.personType }}</el-descriptions-item>
<!-- 第 4 行 --> <!-- 第 4 行 -->
<el-descriptions-item label="人员属性">{{ viewData.userAttribute }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item> <el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item>
<el-descriptions-item label="职级">{{ viewData.professionalLevel }}</el-descriptions-item> <el-descriptions-item label="职级">{{ viewData.professionalLevel }}</el-descriptions-item>
@@ -85,6 +84,7 @@ const MEMBER_INFO = {
<el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item> <el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item>
<el-descriptions-item label="人员分类" v-if="false">{{ viewData.aidFundMemberUserType }}</el-descriptions-item> <el-descriptions-item label="人员分类" v-if="false">{{ viewData.aidFundMemberUserType }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<slot></slot>
</div> </div>
`, `,
store, store,
@@ -43,9 +43,5 @@
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select> code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>--> </search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
</search> </search>
</script> </script>
@@ -22,7 +22,6 @@ const COMMON_QUERY = {
personTypes: [], personTypes: [],
preparedBys: [], preparedBys: [],
aidFundMemberUserType: '', aidFundMemberUserType: '',
userAttributes: [],
} }
} }
}, },
@@ -37,8 +36,6 @@ const COMMON_QUERY = {
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys) pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
// 人员属性多选值按现有数组查询参数口径提交。
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$emit('search', pageForm) this.$emit('search', pageForm)
}, },
handleUnionChange() { handleUnionChange() {
@@ -102,12 +102,6 @@
style="width: 100%"></dict-select> style="width: 100%"></dict-select>
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="人员属性">
<el-form-item prop="userAttribute">
<dict-select v-model="formData.userAttribute" code="USER_ATTRIBUTE"
style="width: 100%"></dict-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="人员分类"> <el-descriptions-item label="人员分类">
<el-form-item prop="aidFundMemberUserType"> <el-form-item prop="aidFundMemberUserType">
<dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE" <dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
@@ -252,7 +246,7 @@
`, `,
store, store,
dicts: ["USER_NATION", "USER_POLITICAL", "USER_EDUCATION", "USER_ACADEMIC_DEGREE", "USER_CAMPUS", dicts: ["USER_NATION", "USER_POLITICAL", "USER_EDUCATION", "USER_ACADEMIC_DEGREE", "USER_CAMPUS",
"USER_STATE", "USER_PREPARED_BY_TYPE", "USER_PERSON_TYPE", "USER_MARRIAGE","AIDFUND_MEMBER_USER_TYPE","USER_ATTRIBUTE"], "USER_STATE", "USER_PREPARED_BY_TYPE", "USER_PERSON_TYPE", "USER_MARRIAGE","AIDFUND_MEMBER_USER_TYPE"],
data() { data() {
return { return {
formData: {}, formData: {},
@@ -292,7 +292,6 @@ layout("/layouts/platform.html"){
search(pageForm) { search(pageForm) {
if (pageForm) { if (pageForm) {
// 当前页的在职状态、人员类型使用标签数组筛选,公共查询组件回传的是字符串, // 当前页的在职状态、人员类型使用标签数组筛选,公共查询组件回传的是字符串,
// 这里保留当前页数组,避免选择人员属性后把标签筛选状态覆盖掉。
const currentUserStates = this.pageForm.userStates const currentUserStates = this.pageForm.userStates
const currentPersonTypes = this.pageForm.personTypes const currentPersonTypes = this.pageForm.personTypes
this.pageForm = {...this.pageForm, ...pageForm} this.pageForm = {...this.pageForm, ...pageForm}
@@ -458,7 +458,6 @@ layout("/layouts/platform.html"){
{ prop: "unionName", label: "所属工会", sortable: true }, { prop: "unionName", label: "所属工会", sortable: true },
{ prop: "unitName", label: "所属单位",sortable: true }, { prop: "unitName", label: "所属单位",sortable: true },
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true }, // { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
// { prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 }, { prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 }, { prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 }, { prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
@@ -49,10 +49,6 @@ layout("/layouts/platform.html"){
<search-item label="人员分类" v-if="false"> <search-item label="人员分类" v-if="false">
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select> <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item> </search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
</search> </search>
</el-card> </el-card>
@@ -102,7 +98,7 @@ layout("/layouts/platform.html"){
new Vue({ new Vue({
el: "#app", el: "#app",
mixins: [initTableMixins], mixins: [initTableMixins],
dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE", "AIDFUND_MEMBER_USER_TYPE", "USER_ATTRIBUTE"], dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE", "AIDFUND_MEMBER_USER_TYPE"],
data() { data() {
return { return {
pageForm: { pageForm: {
@@ -110,7 +106,6 @@ layout("/layouts/platform.html"){
memberSearchName: "", memberSearchName: "",
memberSearchKeyWord: "", memberSearchKeyWord: "",
aidFundMemberUserType: "", aidFundMemberUserType: "",
userAttributes: []
}, },
userId: "", userId: "",
unions: [], unions: [],
@@ -133,16 +128,6 @@ layout("/layouts/platform.html"){
"member-info": MEMBER_INFO "member-info": MEMBER_INFO
}, },
methods: { methods: {
// 分页和导出共用该参数构建方法,保证人员属性多选值始终按 JSON 数组提交。
buildQueryPageForm() {
const pageForm = clone(this.pageForm)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes || [])
return pageForm
},
// 覆盖公共分页方法,使后端按人员属性集合执行查询。
pageData() {
return initTableMixins.methods.pageData.call(this, this.buildQueryPageForm())
},
openView(row) { openView(row) {
this.$refs.guava.view(() => { this.$refs.guava.view(() => {
this.$refs.memberInfoRef.onOpen(row.id) this.$refs.memberInfoRef.onOpen(row.id)
@@ -152,7 +137,7 @@ layout("/layouts/platform.html"){
this.$downLoad("/platform/member/info/history/doExportByUnion", { year: this.pageForm.year }) this.$downLoad("/platform/member/info/history/doExportByUnion", { year: this.pageForm.year })
}, },
doExport() { doExport() {
this.$downLoad("/platform/member/info/history/doExport", this.buildQueryPageForm()) this.$downLoad("/platform/member/info/history/doExport", clone(this.pageForm))
}, },
async initData() { async initData() {
if (this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])) { if (this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])) {
@@ -49,10 +49,6 @@ layout("/layouts/platform.html"){
<search-item label="人员分类" v-if="false"> <search-item label="人员分类" v-if="false">
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select> <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch" code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item> </search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
</search> </search>
</el-card> </el-card>
@@ -109,11 +105,10 @@ layout("/layouts/platform.html"){
new Vue({ new Vue({
el: "#app", el: "#app",
mixins: [initTableMixins], mixins: [initTableMixins],
dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE","AIDFUND_MEMBER_USER_TYPE","USER_ATTRIBUTE"], dicts: ["USER_STATE", "USER_PERSON_TYPE", "MEMBER_CHANGE_TYPE","AIDFUND_MEMBER_USER_TYPE"],
data() { data() {
return { return {
pageForm: { pageForm: {
userAttributes: [],
year: new Date().getFullYear() + "", year: new Date().getFullYear() + "",
memberSearchName: "", memberSearchName: "",
memberSearchKeyWord: "" memberSearchKeyWord: ""
@@ -141,12 +136,6 @@ layout("/layouts/platform.html"){
"member-add-form": MEMBER_ADD_FORM "member-add-form": MEMBER_ADD_FORM
}, },
methods: { methods: {
// 人员属性为多选条件,分页查询前统一转换为后端可接收的 JSON 数组。
pageData(data = null) {
const pageForm = clone(data || this.pageForm)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes || [])
return initTableMixins.methods.pageData.call(this, pageForm)
},
openAdd() { openAdd() {
this.$refs.guava.edit(() => { this.$refs.guava.edit(() => {
this.$refs.memberAddFormRef.onOpen() this.$refs.memberAddFormRef.onOpen()
@@ -129,29 +129,6 @@ layout("/layouts/platform.html"){
</span> </span>
</el-col> </el-col>
</el-row> </el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员属性:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag class="query-tag-item"
v-for="item in userAttributeOptions"
:key="item.code"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<span class="query-tag-actions">
<el-link type="danger"
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
:underline="false"
@click="pageForm.userAttributes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</span>
</el-col>
</el-row>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -237,12 +214,10 @@ layout("/layouts/platform.html"){
units: [], units: [],
personTypeOptions: [], personTypeOptions: [],
aidFundMemberUserTypeOptions: [], aidFundMemberUserTypeOptions: [],
userAttributeOptions: [],
pageForm: { pageForm: {
currentYear: moment().format("YYYY"), currentYear: moment().format("YYYY"),
queryType: "fgh", queryType: "fgh",
personTypes: [], personTypes: [],
userAttributes: [],
aidFundMemberUserTypes: [] aidFundMemberUserTypes: []
}, },
fullTableData: [] fullTableData: []
@@ -271,16 +246,11 @@ layout("/layouts/platform.html"){
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => { this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
this.aidFundMemberUserTypeOptions = data this.aidFundMemberUserTypeOptions = data
}) })
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
this.userAttributeOptions = data
})
}, },
pageData() { pageData() {
// 统计接口按 JSON 字符串接收多选条件,这里统一在提交前做转换,保证人员分类和人员属性都能正确过滤。
const pageForm = clone(this.pageForm) const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes) pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$axios.post(loc() + "/pageData", pageForm).then((res) => { this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.tableData = res.data this.tableData = res.data
@@ -296,13 +266,6 @@ layout("/layouts/platform.html"){
this.pageForm.personTypes.push(code) this.pageForm.personTypes.push(code)
} }
} }
if (key === "userAttributes") {
if (this.pageForm.userAttributes.includes(code)) {
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
} else {
this.pageForm.userAttributes.push(code)
}
}
if (key === "aidFundMemberUserTypes") { if (key === "aidFundMemberUserTypes") {
if (this.pageForm.aidFundMemberUserTypes.includes(code)) { if (this.pageForm.aidFundMemberUserTypes.includes(code)) {
this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1) this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1)
@@ -165,7 +165,6 @@ layout("/layouts/platform.html"){
{ prop: "unitName", label: "所属单位", width: 120, sortable: true }, { prop: "unitName", label: "所属单位", width: 120, sortable: true },
// 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。 // 临时屏蔽“人员分类”列表字段,不删除配置,后续需要可恢复。
// { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true }, // { prop: "aidFundMemberUserType", label: "人员分类", width: 120, sortable: true },
{ prop: "userAttribute", label: "人员属性", width: 120, sortable: true },
{ prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 }, { prop: "idCardType", label: "证件类型", width: 120, sortable: true, checked: 0 },
{ prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 }, { prop: "idCard", label: "证件号码", width: 120, sortable: true, checked: 0 },
{ prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 }, { prop: "marriage", label: "婚姻状况", width: 120, sortable: true, checked: 0 },
@@ -205,7 +204,6 @@ layout("/layouts/platform.html"){
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes) pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes) pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => { const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
return {prop: item.prop, label: item.label} return {prop: item.prop, label: item.label}
@@ -230,7 +228,6 @@ layout("/layouts/platform.html"){
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes) pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes) pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys) pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus) pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus)
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
@@ -113,30 +113,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
</el-col> </el-col>
</el-row> </el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员属性:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag
style="margin-right: 10px;cursor: pointer"
v-for="item in userAttributeOptions"
:key="item.code"
:type="item.name"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<el-link type="danger"
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
:underline="false"
@click="pageForm.userAttributes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="query-row"> <el-row type="flex" align="middle" class="query-row">
<el-col class="query-row-content"> <el-col class="query-row-content">
@@ -235,7 +211,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
personTypes: [], personTypes: [],
preparedBys: [], preparedBys: [],
aidFundMemberUserTypes: [], aidFundMemberUserTypes: [],
userAttributes: [],
userStates: [], userStates: [],
memberStatus: [], memberStatus: [],
year: moment().format("YYYY"), year: moment().format("YYYY"),
@@ -253,7 +228,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
personTypeOptions: [], personTypeOptions: [],
preparedByOptions: [], preparedByOptions: [],
aidFundMemberUserTypeOptions: [], aidFundMemberUserTypeOptions: [],
userAttributeOptions: [],
userStateOptions: [], userStateOptions: [],
memberStatusOptions: [], memberStatusOptions: [],
unions: [], unions: [],
@@ -346,7 +320,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
this.pageForm.personTypes = [] this.pageForm.personTypes = []
this.pageForm.preparedBys = [] this.pageForm.preparedBys = []
this.pageForm.aidFundMemberUserTypes = [] this.pageForm.aidFundMemberUserTypes = []
this.pageForm.userAttributes = []
this.pageForm.memberStatus = [] this.pageForm.memberStatus = []
this.pageForm.userStates = [] this.pageForm.userStates = []
this.pageForm.unionId = [] this.pageForm.unionId = []
@@ -387,9 +360,6 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => { this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
this.aidFundMemberUserTypeOptions = data this.aidFundMemberUserTypeOptions = data
}) })
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
this.userAttributeOptions = data
})
this.$businessTool.getDictOptions("USER_STATE").then((data) => { this.$businessTool.getDictOptions("USER_STATE").then((data) => {
this.userStateOptions = data this.userStateOptions = data
}) })
@@ -129,29 +129,6 @@ layout("/layouts/platform.html"){
</span> </span>
</el-col> </el-col>
</el-row> </el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">人员属性:</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag class="query-tag-item"
v-for="item in userAttributeOptions"
:key="item.code"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<span class="query-tag-actions">
<el-link type="danger"
v-if="userAttributeOptions.length&&pageForm.userAttributes.length"
:underline="false"
@click="pageForm.userAttributes=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.userAttributes=userAttributeOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</span>
</el-col>
</el-row>
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
@@ -198,12 +175,10 @@ layout("/layouts/platform.html"){
unions: [], unions: [],
units: [], units: [],
personTypeOptions: [], personTypeOptions: [],
userAttributeOptions: [],
pageForm: { pageForm: {
currentYear: moment().format("YYYY"), currentYear: moment().format("YYYY"),
queryType: "fgh", queryType: "fgh",
personTypes: [], personTypes: [],
userAttributes: [],
aidFundMemberUserTypes: [] aidFundMemberUserTypes: []
}, },
aidFundMemberUserTypeOptions: [], aidFundMemberUserTypeOptions: [],
@@ -232,9 +207,6 @@ layout("/layouts/platform.html"){
this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => { this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE").then((data) => {
this.aidFundMemberUserTypeOptions = data this.aidFundMemberUserTypeOptions = data
}) })
this.$businessTool.getDictOptions("USER_ATTRIBUTE").then((data) => {
this.userAttributeOptions = data
})
}, },
tagClick(key,code) { tagClick(key,code) {
if (key === "personTypes") { if (key === "personTypes") {
@@ -244,13 +216,6 @@ layout("/layouts/platform.html"){
this.pageForm.personTypes.push(code) this.pageForm.personTypes.push(code)
} }
} }
if (key === "userAttributes") {
if (this.pageForm.userAttributes.includes(code)) {
this.pageForm.userAttributes.splice(this.pageForm.userAttributes.indexOf(code), 1)
} else {
this.pageForm.userAttributes.push(code)
}
}
if (key === "aidFundMemberUserTypes") { if (key === "aidFundMemberUserTypes") {
if (this.pageForm.aidFundMemberUserTypes.includes(code)) { if (this.pageForm.aidFundMemberUserTypes.includes(code)) {
this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1) this.pageForm.aidFundMemberUserTypes.splice(this.pageForm.aidFundMemberUserTypes.indexOf(code), 1)
@@ -262,10 +227,8 @@ layout("/layouts/platform.html"){
}, },
pageData() { pageData() {
const pageForm = clone(this.pageForm) const pageForm = clone(this.pageForm)
// 统计接口按 JSON 字符串接收多选条件,这里把人员分类和人员属性统一成同一提交口径。
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes) pageForm.aidFundMemberUserTypes = JSON.stringify(this.pageForm.aidFundMemberUserTypes)
pageForm.userAttributes = JSON.stringify(this.pageForm.userAttributes)
this.$axios.post(loc() + "/pageData", pageForm).then((res) => { this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.tableData = res.data this.tableData = res.data