This commit is contained in:
zhouhefeng
2026-07-14 09:15:41 +08:00
105 changed files with 4556 additions and 3279 deletions
@@ -1,17 +1,7 @@
package com.budwk.app.base.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.exception.BaseException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -33,19 +23,17 @@ public class DataCenterProperties {
@Inject
private PropertiesProxy conf;
@Inject
private RedisService redisService;
private String appId;
private String tokenUrl;
private Map<String, String> codes = new HashMap<>();
private String key;
private String secret;
private Map<String, String> urls = new HashMap<>();
public void init() {
Map<String, String> all = conf.toMap();
appId = all.get("data-center.app-id");
tokenUrl = all.get("data-center.token-url");
extract(all, "data-center.codes.", codes);
key = all.get("data-center.key");
secret = all.get("data-center.secret");
extract(all, "data-center.urls.", urls);
}
@@ -61,59 +49,4 @@ public class DataCenterProperties {
}
});
}
/**
* 读取配置
* @param key 接口类型枚举
* @return 相关配置
*/
public Credential credential(String key) {
String code = codes.get(key);
String url = urls.get(key);
if (code == null || url == null) {
throw new BaseException("未知业务: " + key);
}
String token = getToken(Integer.parseInt(code), key);
return new Credential(url, token);
}
@Getter
@AllArgsConstructor
public static class Credential {
private String url;
private String token;
}
/**
* 获取token
* @param code
* @param keyPrefix
* @return token
*/
private String getToken(Integer code, String keyPrefix) {
String dateCenterPrefix = RedisConstant.DATE_CENTER_PREFIX + keyPrefix;
String token = redisService.get(dateCenterPrefix);
if (StrUtil.isNotBlank(token)) {
return token;
} else {
HttpRequest httpRequest = HttpUtil.createPost(tokenUrl);
Map<String, Object> reqBody = Map.of(
"appId", appId,
"code", code
);
httpRequest.body(JSONUtil.toJsonStr(reqBody));
JSONObject resp = JSONUtil.parseObj(httpRequest.execute().body());
if (resp.getInt("code") != 200) {
throw new BaseException("获取token失败: " + resp.getStr("msg"));
} else {
JSONObject data = resp.getJSONObject("data");
String tokenStr = data.getStr("token");
Integer expire = data.getInt("expire");
redisService.setex(dateCenterPrefix, expire, tokenStr);
return tokenStr;
}
}
}
}
@@ -37,7 +37,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
@DataCenterColumn(name = "工号", key = "JGH")
@DataCenterColumn(name = "工号", key = "ZGH")
private String loginname;
@Column
@@ -49,7 +49,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 10)
@DataCenterColumn(name = "", key = "XBM", dict = "USER_SEX")
@DataCenterColumn(name = "", key = "XB", dict = "USER_SEX")
private String sex;
@Column
@@ -108,13 +108,12 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("手机号码")
@ColDefine(type = ColType.VARCHAR, width = 32)
@DataCenterColumn(name = "手机号码", key = "SJHM")
@DataCenterColumn(name = "手机号码", key = "YDDH")
private String mobile;
@Column
@Comment("学历")
@ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "学历", key = "ZGXLM", dict = "USER_EDUCATION")
private String education;
@Column
@@ -131,19 +130,16 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("岗位类别")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "岗位类别", key = "JRGWDJM",dict = "USER_JOB_CATEGORY")
private String jobCategory;
@Column
@Comment("职称")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "职称", key = "DQZC", dict = "USER_PROFESSIONAL_TITLE")
private String professionalTitle;
@Column
@Comment("职级")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "取得职称等级码", key = "QDZCDJM", dict = "USER_PROFESSIONAL_LEVEL")
private String professionalLevel;
@Column
@@ -179,7 +175,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("来校时间")
@ColDefine(type = ColType.VARCHAR, width = 10)
@DataCenterColumn(name = "来校时间", key = "LXRQ")
@DataCenterColumn(name = "来校时间", key = "LXNY")
private String arrivalAtSchoolDate;
@Column
@@ -195,7 +191,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("在职状态")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "当前状态码", key = "DQZTM", dict = "USER_STATE")
@DataCenterColumn(name = "当前状态码", key = "DQZT", dict = "USER_STATE")
private String userState;
@Column
@@ -207,19 +203,17 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("在岗情况")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "在岗情况码", key = "ZGQKM", dict = "USER_DUTY_SITUATION")
private String dutySituation;
@Column
@Comment("教职工类别码")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "教职工类别码", key = "JZGLBM", dict = "USER_PERSON_TYPE")
@DataCenterColumn(name = "教职工类别码", key = "RYLX", dict = "USER_PERSON_TYPE")
private String personType;
@Column
@Comment("编制类别码")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "编制类别码", key = "BZLBM", dict = "USER_PREPARED_BY_TYPE")
private String preparedBy;
@Column
@@ -269,7 +263,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
@DataCenterColumn(name = "单位", key = "DWH")
@DataCenterColumn(name = "单位", key = "SZDWH")
private String unitId;
@Column
@@ -1,28 +1,14 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.config.DataCenterProperties;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_data_dict;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDataDictPullService;
import com.budwk.app.sys.services.SysDictService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @version 1.0
@@ -35,14 +21,6 @@ import java.util.Map;
@IocBean(args = {"refer:dao"})
public class SysDataDictPullServiceImpl extends BaseServiceImpl<Sys_dict> implements SysDataDictPullService {
@Inject
private DataCenterProperties dcPro;
@Inject
private SysDictService dictService;
// code
private final static String KEY_PREFIX = "user_";
public SysDataDictPullServiceImpl(Dao dao) {
super(dao);
}
@@ -50,57 +28,7 @@ public class SysDataDictPullServiceImpl extends BaseServiceImpl<Sys_dict> implem
@Override
@Aop(TransAop.READ_COMMITTED)
public void pullDataDict() {
Map<String, String> urls = dcPro.getUrls();
List<String> list = urls.keySet().stream().filter(o -> o.startsWith(KEY_PREFIX.toUpperCase())).toList();
List<Sys_data_dict> dataDictList = new ArrayList<>();
for (String key : list) {
// 获取配置
DataCenterProperties.Credential credential = dcPro.credential(key);
String url = credential.getUrl();
String token = credential.getToken();
// 请求数据
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header("Content-Type", "application/json");
httpRequest.header("X-H3C-TOKEN", token);
httpRequest.body(JSONUtil.toJsonStr(Map.of()));
String resBody = httpRequest.execute().body();
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 200) {
log.error("获取{}字典数据失败,错误码: {};错误原因:{}", key, jsonBody.getInt("code"), jsonBody.getStr("msg"));
}
List<NutMap> data = Json.fromJsonAsList(NutMap.class, jsonBody.getStr("data"));
// 先删除
Sys_dict sysDict = dao().fetch(Sys_dict.class, Cnd.where(Sys_dict::getCode, "=", key));
dao().clear(Sys_dict.class, Cnd.where(Sys_dict::getParentId, "=", sysDict.getId()));
// 组合数据
data.forEach(o -> {
Sys_dict dict = new Sys_dict();
dict.setParentId(sysDict.getId());
dict.setPath(dictService.getSubPath("sys_dict", "path", sysDict.getPath()));
dict.setName(o.getString("xbname"));
dict.setCode(o.getString("xbname"));
dict.setDisabled(false);
dict.setHasChildren(false);
dao().insert(dict);
Sys_data_dict sysDataDict = new Sys_data_dict();
sysDataDict.setParentCode(key);
sysDataDict.setCode(o.getString("xbcode"));
sysDataDict.setName(o.getString("xbname"));
dataDictList.add(sysDataDict);
});
dao().insert(dataDictList);
}
// 旧数据中心字典接口已停用,避免继续误调旧token/code模式接口。
throw new BaseException("旧数据中心字典拉取接口已停用");
}
}
@@ -1,32 +1,33 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.config.DataCenterProperties;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.enums.Api2UnitFiledMap;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.services.SysDataUnitPullService;
import com.budwk.app.sys.utils.DataCenterUtil;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -40,8 +41,11 @@ import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
private static final String DMP_UNIT_KEY = "unit";
private static final int DMP_PAGE_SIZE = 1000;
@Inject
private DataCenterProperties dcPro;
private DataCenterProperties dataCenterProperties;
public SysDataUnitPullServiceImpl(Dao dao) {
super(dao);
@@ -51,11 +55,7 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateUnits() {
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);
updateUnits(unitIds);
updateUnits(List.of());
}
/**
@@ -64,65 +64,235 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateUnits(List<String> unitIds) {
// token url参数
Map<String, String> tokenParams = Map.of(
"grant_type", "password",
"scope", "read",
"username", "3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD",
"password", "3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD"
);
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = distinctDmpUnits(pullDmpUnits(getDmpUnitUrl(), accessToken));
Set<String> parentCodes = rawDataList.stream()
.map(this::getNormalizedParentCode)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
String params = URLUtil.buildQuery(tokenParams, StandardCharsets.UTF_8);
String tokenUrl = "https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/oauth/token" + (StrUtil.isBlank(params) ? "" : "?" + params);
HttpRequest tokenHttpRequest = HttpUtil.createPost(tokenUrl);
tokenHttpRequest.header("Content-Type", "application/json");
tokenHttpRequest.header("Authorization", "Basic MTE0OjU5M2UwNGI4YTE4NWRlMzkyNjY4ZmY0ZDJhZGMyZTEw");
rawDataList.stream()
.sorted(Comparator.comparingInt(this::getUnitLevel))
.forEach(raw -> saveOrUpdateUnit(raw, parentCodes));
log.info("请求token: {}", tokenHttpRequest);
String tokenResBody = tokenHttpRequest.execute().body();
log.info("信息中心单位数据同步完成,本次接口返回单位数量: {},待检查单位编码数量: {}", rawDataList.size(), unitIds == null ? 0 : unitIds.size());
}
log.info("请求token结果: {}", tokenResBody);
/**
* 获取信息中心开放平台访问令牌。
* 参数来自 data-center.token-url、data-center.key、data-center.secret
* 返回值为后续单位接口 body 中 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");
}
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);
// 请求数据
HttpRequest httpRequest = HttpUtil.createGet("https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/YXSDWJBSJZLB");
httpRequest.form(Map.of("access_token", tokenJsonBody.getStr("access_token"), "pageSize", 100, "page", 1));
log.info("请求单位数据: {}", httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求单位数据结果: {}", resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 200) {
throw new BaseException("获取单位数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("msg"));
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;
}
// 需要新增的单位
List<Sys_unit> insertList = new ArrayList<>();
// 需要更新的单位
List<Sys_unit> updateList = new ArrayList<>();
/**
* 读取信息中心单位接口地址。
* 配置项为 data-center.urls.unit,返回值为单位全量接口URL。
*/
private String getDmpUnitUrl() {
String url = dataCenterProperties.getUrls().get(DMP_UNIT_KEY);
if (StrUtil.isBlank(url)) {
throw new BaseException("未配置信息中心单位接口地址: data-center.urls." + DMP_UNIT_KEY);
}
return url;
}
// 拉取过来的数据,全部的单位,一万四千多条,这里只保留 “部门类”
List<JSONObject> data = jsonBody.getJSONArray("data").stream()
.map(o -> (JSONObject) o)
.toList();
/**
* 分页拉取单位接口数据。
* 入参 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;
Map<String, String> api2db = Arrays.stream(Api2UnitFiledMap.values())
.collect(Collectors.toMap(e -> e.apiField, e -> e.dbColumn));
for (JSONObject row : data) {
Sys_unit unit = DataCenterUtil.mapJsonToBean(row, Sys_unit.class, api2db);
unit.setUnitcode(unit.getId());
unit.setUnitTypeCode(1);
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)
)));
if (unitIds.contains(unit.getId())) {
// 存在则更新
updateList.add(unit);
} else {
// 不存在则新增
insertList.add(unit);
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<>();
for (JSONObject raw : rawDataList) {
String unitCode = raw.getStr("DWDM");
if (StrUtil.isBlank(unitCode)) {
continue;
}
unitMap.putIfAbsent(unitCode, raw);
}
log.info("本次拉取单位数据,新增{}条,更新{}条", insertList.size(), updateList.size());
dao().insert(insertList);
dao().update(updateList);
return new ArrayList<>(unitMap.values());
}
/**
* 保存或更新单位。
* DWDM 对应系统单位 id/unitcodeDWMC 对应名称,LSDWH 对应父级单位号,DWCC 对应信息中心原始单位层级。
*/
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes) {
String unitCode = raw.getStr("DWDM");
if (StrUtil.isBlank(unitCode)) {
return;
}
String parentId = getParentId(unitCode, getNormalizedParentCode(raw));
Sys_unit unit = fetch(unitCode);
if (unit == null) {
unit = new Sys_unit();
unit.setId(unitCode);
unit.setUnitcode(unitCode);
unit.setPath(getUnitPath(parentId));
setUnitValue(unit, raw, parentId, parentCodes);
dao().fastInsert(unit);
} else {
setUnitValue(unit, raw, parentId, parentCodes);
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath(parentId));
}
dao().updateIgnoreNull(unit);
}
}
/**
* 根据接口字段给系统单位赋值。
* DWYXBS 表示单位是否有效,DWCC 表示信息中心原始单位层次;
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
*/
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) {
String unitCode = raw.getStr("DWDM");
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));
}
/**
* 计算父级单位号。
* LSDWH 为空、等于自身或本地不存在该父级时按根级处理;系统根单位 0 允许直接作为父级。
*/
private String getParentId(String unitCode, String parentCode) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) {
return "";
}
if ("0".equals(parentCode)) {
return parentCode;
}
Sys_unit parentUnit = fetch(parentCode);
return parentUnit == null ? "" : parentCode;
}
/**
* 根据父级 path 生成当前单位 path。
* 父级为空时生成根节点 path,父级存在时在父级 path 下生成子节点 path。
*/
private String getUnitPath(String parentId) {
String parentPath = "";
if (StrUtil.isNotBlank(parentId)) {
Sys_unit parentUnit = fetch(parentId);
parentPath = parentUnit == null ? "" : parentUnit.getPath();
}
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);
}
}
@@ -5,6 +5,7 @@ import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.event.role.RoleEventMsg;
@@ -50,6 +51,8 @@ import java.util.stream.Collectors;
@Slf4j
@IocBean
public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService {
private static final int BATCH_SIZE = 500;
private static final int ROLE_DELETE_BATCH_SIZE = 50;
@Inject
private Dao dao;
@@ -180,6 +183,21 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
List<Sys_user_history> histories = new CopyOnWriteArrayList<>();
List<String> addMemberUserIds = Collections.synchronizedList(new ArrayList<>());
List<String> removeMemberUserIds = Collections.synchronizedList(new ArrayList<>());
Set<String> sourceLoginNames = sources.stream()
.map(Sys_user_source::getLoginname)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
List<String> leaveUserIds = sysUsers.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()))
.filter(user -> !sourceLoginNames.contains(user.getLoginname()))
.map(Sys_user::getId)
.toList();
List<String> leaveMemberUserIds = sysUsers.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()))
.filter(user -> !sourceLoginNames.contains(user.getLoginname()))
.filter(user -> Boolean.TRUE.equals(user.getMember()))
.map(Sys_user::getId)
.toList();
// 处理每条数据
for (Sys_user_source source : sources) {
@@ -235,7 +253,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
CompletableFuture<Void> insertTask = CompletableFuture.runAsync(() -> {
log.info("新增用户: {} 个", needInitUserList.size());
// 分批处理,每批200条
List<List<Sys_user>> batches = ListUtil.split(needInitUserList, 500);
List<List<Sys_user>> batches = ListUtil.split(needInitUserList, BATCH_SIZE);
batches.forEach(batch -> {
try {
dao.fastInsert(batch);
@@ -260,7 +278,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
if (!roleList.isEmpty()) {
// 分批处理角色分配
List<List<Sys_user_role>> roleBatches = ListUtil.split(roleList, 500);
List<List<Sys_user_role>> roleBatches = ListUtil.split(roleList, BATCH_SIZE);
roleBatches.forEach(batch -> {
try {
dao.fastInsert(batch);
@@ -282,7 +300,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
CompletableFuture<Void> updateTask = CompletableFuture.runAsync(() -> {
log.info("更新用户: {} 个", needDoUpdateList.size());
// 分批处理,每批200条
List<List<Sys_user>> batches = ListUtil.split(needDoUpdateList, 500);
List<List<Sys_user>> batches = ListUtil.split(needDoUpdateList, BATCH_SIZE);
batches.forEach(batch -> {
try {
dao.updateIgnoreNull(batch);
@@ -294,6 +312,65 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
updateTasks.add(updateTask);
}
if (Lang.isNotEmpty(leaveUserIds)) {
CompletableFuture<Void> leaveTask = CompletableFuture.runAsync(() -> {
log.info("数据源缺失用户转为不在岗并取消会员: {} 个", leaveUserIds.size());
List<List<String>> batches = ListUtil.split(leaveUserIds, BATCH_SIZE);
batches.forEach(batch -> {
try {
/*
* 本次拉取批次中没有出现、但系统用户表仍存在的人员,按离岗处理:
* 1. userState 写为“不在岗”,用于后续人员状态筛选和业务判断;
* 2. member 写为 false,避免仍按会员身份参与后续业务判断。
*/
dao.update(Sys_user.class, Chain.make("userState", "不在岗").add("member", false), Cnd.where("id", "in", batch));
} catch (Exception e) {
log.error("批量更新数据源缺失用户状态异常", e);
}
});
}, executorService);
updateTasks.add(leaveTask);
}
if (Lang.isNotEmpty(leaveMemberUserIds)) {
CompletableFuture<Void> leaveMemberRoleTask = CompletableFuture.runAsync(() -> {
log.info("数据源缺失会员移除会员角色: {} 个", leaveMemberUserIds.size());
List<List<String>> batches = ListUtil.split(leaveMemberUserIds, ROLE_DELETE_BATCH_SIZE);
batches.forEach(batch -> {
try {
/*
* 只对原本是会员的缺失人员移除会员角色,并缩小删除批次,
* 降低 sys_user_role 大批量 DELETE 时的锁等待概率。
*/
dao.clear(Sys_user_role.class, Cnd.where("userId", "in", batch).and("roleId", "=", memberRole.getId()));
} catch (Exception e) {
log.error("批量移除数据源缺失会员角色异常", e);
}
});
}, executorService);
updateTasks.add(leaveMemberRoleTask);
}
if (Lang.isNotEmpty(histories)) {
CompletableFuture<Void> historyTask = CompletableFuture.runAsync(() -> {
log.info("添加历史记录: {} 条", histories.size());
/*
* 历史记录基于更新前的 sys_user 与 sys_user_source 生成,必须纳入本次更新等待范围。
* 继续使用线程池和批量插入,避免主线程逐条写入拖慢更新接口。
*/
List<List<Sys_user_history>> batches = ListUtil.split(histories, 500);
batches.forEach(batch -> {
try {
dao.fastInsert(batch);
} catch (Exception e) {
log.error("批量添加历史记录异常", e);
throw new RuntimeException(e);
}
});
}, executorService);
updateTasks.add(historyTask);
}
// 等待用户数据更新完成
try {
// 设置超时时间,避免无限等待
@@ -307,22 +384,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return "更新失败: " + e.getMessage();
}
// 3. 异步添加历史记录 - 不等待完成
if (Lang.isNotEmpty(histories)) {
executorService.execute(() -> {
log.info("添加历史记录: {} 条", histories.size());
// 分批处理历史记录
List<List<Sys_user_history>> batches = ListUtil.split(histories, 500);
batches.forEach(batch -> {
try {
dao.fastInsert(batch);
} catch (Exception e) {
log.error("批量添加历史记录异常", e);
}
});
});
}
/* // 4. 异步添加会员角色 - 不等待完成
if (Lang.isNotEmpty(addMemberUserIds)) {
executorService.execute(() -> {
@@ -384,7 +445,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
log.info("全量更新用户数据完成,耗时: {} 毫秒", (endTime - startTime));
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 更新用户 " + needDoUpdateList.size()
+ " 个, 待添加会员 " + addMemberUserIds.size() + " 个, 待移除会员 " + removeMemberUserIds.size() + "";
+ " 个, 数据源缺失转不在岗 " + leaveUserIds.size() + " 个, 待添加会员 " + addMemberUserIds.size() + " 个, 待移除会员 " + removeMemberUserIds.size() + "";
}
/**
@@ -453,8 +514,8 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
NutMap change = NutMap.NEW();
change.put("fieldName", "单位");
change.put("field", "unitId");
change.put("sourceValue", unitIdNameMap.get(user.getUnitId()));
change.put("newValue", unitIdNameMap.get(user.getUnitId()));
change.put("sourceValue", getUnitChangeValue(user.getUnitId()));
change.put("newValue", getUnitChangeValue(source.getUnitId()));
changeList.add(change);
// 单位异动发送订阅消息,清空原单位的所有角色
@@ -470,8 +531,8 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// 生成变更信息描述
String changeInfos = changeList.stream()
.map(v -> v.getString("fieldName") + "" +
HtmlUtil.cleanHtmlTag(v.getString("sourceValue")) + "" +
HtmlUtil.cleanHtmlTag(v.getString("newValue")))
HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(v.getString("sourceValue"), "")) + "" +
HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(v.getString("newValue"), "")))
.collect(Collectors.joining(""));
// 设置历史记录信息
@@ -481,4 +542,15 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return history;
}
/**
* 获取单位变更记录展示值。
* 入参 unitId 为系统用户旧单位或数据源新单位ID;返回值优先使用单位名称,查不到名称时保留单位ID,避免变更记录为空导致更新中断。
*/
private String getUnitChangeValue(String unitId) {
if (StrUtil.isBlank(unitId)) {
return "";
}
return StrUtil.blankToDefault(unitIdNameMap.get(unitId), unitId);
}
}
@@ -21,6 +21,7 @@ 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_unit;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.models.Sys_user_source;
import com.budwk.app.sys.services.SysDataUnitPullService;
@@ -50,6 +51,10 @@ import java.util.stream.Collectors;
@Slf4j
public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService {
private static final String DMP_TEACHER_KEY = "teacher";
private static final String DMP_DISPATCH_KEY = "dispatch";
private static final int DMP_PAGE_SIZE = 1000;
@Inject
private SysDictService sysDictService;
@Inject
@@ -115,48 +120,14 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 字典码表Map,key为父级编码,value为子级字典列表
Map<String, List<Sys_data_dict>> dataDictMap = dataDictList.stream().collect(Collectors.groupingBy(Sys_data_dict::getParentCode));
// token url参数
Map<String, String> tokenParams = Map.of(
"grant_type", "password",
"scope", "read",
"username", "3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD",
"password", "3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD"
);
String params = URLUtil.buildQuery(tokenParams, StandardCharsets.UTF_8);
String tokenUrl = "https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/oauth/token" + (StrUtil.isBlank(params) ? "" : "?" + params);
HttpRequest tokenHttpRequest = HttpUtil.createPost(tokenUrl);
tokenHttpRequest.header("Content-Type", "application/json");
tokenHttpRequest.header("Authorization", "Basic MTE0OjU5M2UwNGI4YTE4NWRlMzkyNjY4ZmY0ZDJhZGMyZTEw");
log.info("请求token: {}", tokenHttpRequest);
String tokenResBody = tokenHttpRequest.execute().body();
log.info("请求token结果: {}", tokenResBody);
JSONObject tokenJsonBody = JSONUtil.parseObj(tokenResBody);
// 请求数据
HttpRequest httpRequest = HttpUtil.createGet("https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/JZGJCSJZLB");
httpRequest.form(Map.of("access_token", tokenJsonBody.getStr("access_token"), "pageSize", 5000, "page", 1));
log.info("请求人员数据: {}", httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求人员数据结果: {}", resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 200) {
throw new BaseException("获取人员数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("msg"));
}
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = new ArrayList<>();
JSONArray data = jsonBody.getJSONArray("data");
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工"));
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣"));
rawDataList = distinctDmpUsers(rawDataList);
if (!data.isEmpty()) {
// 先收集所有原始数据
rawDataList = data.stream().map(v -> (JSONObject) v).toList();
log.info("数据拉取进度: {}/{}", rawDataList.size(), jsonBody.getInt("total"));
} else {
log.warn("当前未获取到数据");
if (rawDataList.isEmpty()) {
log.warn("当前未获取到人员数据");
}
Date nowDate = new Date();
@@ -172,20 +143,24 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
if (mapping.field.getType() == String.class) {
if (StrUtil.isNotBlank(mapping.dict)) {
Sys_data_dict sysDataDict = dataDictMap.get(mapping.dict).stream()
.filter(v -> raw.getStr(mapping.key)
.equals(v.getCode())).findFirst().orElse(new Sys_data_dict());
mapping.field.set(sysUser, sysDataDict.getName());
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.field.getName())) {
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));
}
@@ -198,6 +173,9 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
}
}
sysUser.setUnitName(raw.getStr("SZDW"));
sysUser.setUnitId(resolveSourceUnitId(raw.getStr("SZDWH")));
// 设置拉取时间
sysUser.setPullTime(nowDate);
@@ -206,10 +184,12 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
log.info("用户数据初始化完成,等待插入,当前数据条数{}", latestSourceList.size());
// 插入到数据库
dao().insert(latestSourceList);
if (CollUtil.isNotEmpty(latestSourceList)) {
dao().insert(latestSourceList);
}
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).distinct().toList();
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);
@@ -225,6 +205,159 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
}
}
/**
* 规范化信息中心返回的来校时间。
* 入参 arrivalAtSchoolDate 对应信息中心 LXNY 字段,允许 yyyy-MM-dd 或 yyyy-MM
* 返回值用于 sys_user_source.arrivalAtSchoolDate 和后续 sys_user.arrivalAtSchoolDate,年月格式统一补为当月 01 日。
*/
private String normalizeArrivalAtSchoolDate(String arrivalAtSchoolDate) {
String dateText = StrUtil.trim(arrivalAtSchoolDate);
if (StrUtil.isBlank(dateText)) {
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)) {
throw new BaseException("未配置信息中心人员接口地址: data-center.urls." + 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<>();
for (JSONObject raw : rawDataList) {
String loginName = raw.getStr("ZGH");
if (StrUtil.isBlank(loginName)) {
loginName = raw.getStr("ID");
}
userMap.putIfAbsent(loginName, raw);
}
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
private void updateDict(List<Sys_user_source> userSources) {
//判断是否要更新在职状态字典
@@ -388,70 +521,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
@Override
public Map<String, NutMap> pullFinance() {
// 获取配置文件 接口地址 和 token
DataCenterProperties.Credential credential = dataCenterProperties.credential("finance");
String url = credential.getUrl();
String token = credential.getToken();
List<JSONObject> rawDataList = new ArrayList<>();
String year = String.valueOf(DateUtil.thisYear());
String month = String.valueOf(DateUtil.thisMonth() + 1);
int pageNum = 1;
int pageSize = 1000;
int totalPages = 1;
int totalCount = 0;
do {
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.header("Accept-Encoding", "gzip, deflate, br");
httpRequest.header("X-H3C-TOKEN", token);
Map<String, Object> reqBody = Map.of(
"nf", year,
"yf", month,
"pageNum", pageNum,
"pageSize", pageSize
);
httpRequest.body(JSONUtil.toJsonStr(reqBody));
JSONObject resp = JSONUtil.parseObj(httpRequest.execute().body());
if (resp.getInt("code") != 0) {
throw new BaseException("获取财务数据失败,错误码: " + resp.getInt("code") + ";错误原因:" + resp.getStr("msg"));
}
JSONObject data = resp.getJSONObject("data");
if (pageNum == 1) {
totalCount = data.getInt("total", 0);
// totalPages = data.getInt("pages", 0);
totalPages = (int) Math.ceil((double) totalCount / pageSize);
}
JSONArray records = data.getJSONArray("records");
if (CollUtil.isNotEmpty(records)) {
rawDataList.addAll(records.stream().map(v -> (JSONObject) v).toList());
log.info("数据拉取进度: {}/{}", rawDataList.size(), totalCount);
}
pageNum++; // 只加页码
} while (pageNum <= totalPages);
// 接口数据输出
log.info("数据拉取结果: {}", rawDataList);
// 最后结果数据
Map<String, NutMap> result = new HashMap<>();
Map<String, String> api2db = Arrays.stream(Api2FinanceFiledMap.values())
.collect(Collectors.toMap(e -> e.apiField, e -> e.dbColumn));
for (JSONObject row : rawDataList) {
NutMap nutMap = DataCenterUtil.mapJsonToBean(row, NutMap.class, api2db);
result.put(nutMap.getString("loginname"), nutMap);
}
// 输出结果
log.info("财务数据拉取转换结果: {}", result);
log.info("数据拉取完成,共拉取 {} 条数据", result.size());
return result;
// 旧数据中心财务接口已停用,避免继续调用旧token/code模式接口。
throw new BaseException("旧数据中心财务拉取接口已停用");
}
}
@@ -309,9 +309,9 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
String decodePwd = Base64Decoder.decodeStr(passowrd);
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
// if (Globals.sso) {
// throw new BaseException("用户名或者密码不正确");
// }
if (Globals.sso) {
throw new BaseException("用户名或者密码不正确");
}
}
user = this.fetchLinks(user, "unit");
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
@@ -169,16 +169,16 @@ public class ActivitySchoolApply extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50)
private String mobile;
// @Excel(name = "身份证号", width = 20)
@Excel(name = "身份证", width = 25)
private String idCard;
@Column
@Comment("职级")
@Excel(name = "职级", width = 20)
@ColDefine(type = ColType.VARCHAR, width = 50)
private String professionalLevel;
// @Excel(name = "身份证号", width = 20)
private String idCard;
@Column
@Comment("性别")
@Excel(name = "性别", width = 20)
@@ -10,6 +10,7 @@ import com.budwk.app.base.utils.DateUtil;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
import com.budwk.app.zhgh.activity.culture.models.ActivityTissue;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchool;
import com.budwk.app.zhgh.activity.sports.models.ActivitySchoolApply;
@@ -200,6 +201,7 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
app.activityUnionName unionname,
app.sex,
app.mobile,
u.idcard AS idCard,
app.professionalLevel,
u.userState,
ev.allName,
@@ -244,13 +246,25 @@ public class ActivitySportsServiceImpl extends BaseServiceImpl<ActivitySchool> i
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, ActivitySchoolApply.class, excels);
CommonDownloadUtil.download("报名表.xlsx", workbook, response);
CommonDownloadUtil.download(getUnionApplyExportFileName(unionId), workbook, response);
} catch (Exception e) {
e.printStackTrace();
}
}
private String getUnionApplyExportFileName(String unionId) {
String unionName = "分工会";
if (Strings.isNotBlank(unionId)) {
ActivityBasicUnion union = dao().fetch(ActivityBasicUnion.class, unionId);
if (union != null && Strings.isNotBlank(union.getName())) {
unionName = union.getName();
}
}
// 分工会名称可能已经包含“分工会”后缀,避免导出文件名重复拼接。
return unionName.endsWith("分工会") ? unionName + "报名表.xlsx" : unionName + "分工会报名表.xlsx";
}
private static class personType {
@@ -135,6 +135,7 @@ public class TrainSignUpApplyController {
tsuc.courseLocationCoordinates,
tsuc.courseReservedNumber,
tsuc.courseInstructor,
tsuc.courseInstructorMobile,
tsuc.campus,
tsuc.unionLimit,
tsuc.isMobileSign,
@@ -68,6 +68,11 @@ public class TrainSignUpCourse extends BaseModel implements Serializable {
@Comment("课程讲师")
private String courseInstructor;
@Column
@ColDefine(type = ColType.VARCHAR, width = 20)
@Comment("负责人手机号")
private String courseInstructorMobile;
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
@Comment("校区")
@@ -85,7 +85,7 @@ public class HonorBasicSettingsController {
} else {
dao.updateIgnoreNull(honorBasicSettings);
}
dao.update(Chain.make("hasChildren", true), Cnd.where("id", "=", honorBasicSettings.getParentId()));
dao.update(HonorBasicSettings.class, Chain.make("hasChildren", true), Cnd.where("id", "=", honorBasicSettings.getParentId()));
return Result.success();
} catch (Exception e) {
return Result.error();
@@ -103,7 +103,11 @@ public class HonorBatchController {
u.unionId,
u.unionName,
u.unitId,
u.unitName
u.unitName,
u.sex,
u.birthday,
u.userState,
u.political
FROM
`vw_user` u
""");
@@ -127,6 +131,10 @@ public class HonorBatchController {
user.ifPresent(sysUser -> v.setUnitId(sysUser.getString("unitId")));
user.ifPresent(sysUser -> v.setUnitName(sysUser.getString("unitName")));
user.ifPresent(sysUser -> v.setUserId(sysUser.getString("id")));
user.ifPresent(sysUser -> v.setUserSex(sysUser.getString("sex")));
user.ifPresent(sysUser -> v.setBirthday(sysUser.getString("birthday")));
user.ifPresent(sysUser -> v.setJobStatus(sysUser.getString("userState")));
user.ifPresent(sysUser -> v.setPolitical(sysUser.getString("political")));
} else {
v.setHonorType(getBasicIdByName("集体荣誉"));
//设置分工会
@@ -11,41 +11,41 @@ import java.math.BigDecimal;
import java.util.List;
/**
* 分工会季度额度分配服务
* 分工会分配项目额度分配服务
*
* <p>该服务统一处理季度额度分配批量分配和重置回滚
* 保证季度记录与年度预算表金额始终同步</p>
* <p>该服务统一处理会费评优奖励等分配项目的额度分配批量分配和重置回滚
* 保证分配项目记录与年度预算表金额始终同步</p>
*
* @author zhf
*/
public interface OutlayManageUnionAllocateService extends BaseService<OutLayAllocateUnion> {
/**
* 修改单个分工会的季度分配金额
* 修改单个分工会的分配项目金额
*
* @param id outlay_allocate_union 表主键用于定位当前编辑的季度分配记录
* @param allocateMoney 本次要设置的季度分配金额传入的是最终金额不是增量金额
* @param id outlay_allocate_union 表主键用于定位当前编辑的分配项目记录
* @param allocateMoney 本次要设置的分配项目金额传入的是最终金额不是增量金额
* @return Result 成功时返回 success失败时返回错误信息前端据此提示用户
*/
Result doEdit(String id, BigDecimal allocateMoney);
/**
* 批量设置某一季度所有分工会的分配金额
* 批量设置某一分配项目所有分工会的分配金额
*
* @param allocateMoney 本次批量设置的季度分配金额所有命中的分工会都会写入该金额
* @param quarterly 要操作的季度例如 1/2/3/4
* @param year 要操作的年份用于筛选对应季度的有效分配记录
* @param allocateMoney 本次批量设置的分配项目金额所有命中的分工会都会写入该金额
* @param quarterly 要操作的分配项目编码复用 OUTLAY_QUARTERLY 字典
* @param year 要操作的年份用于筛选对应分配项目的有效记录
* @return Result 成功时返回 success失败时返回错误信息
*/
Result doBatchAllocate(BigDecimal allocateMoney, Integer quarterly, Integer year);
/**
* 判断某年某季度是否已经做过额度分配
* 判断某年某分配项目是否已经做过额度分配
*
* <p>只要当前季度记录中存在分配额度大于 0 的有效记录
* 就认为该季度已经分配过额度前端据此决定是否提示再次分配</p>
* <p>只要当前分配项目记录中存在分配额度大于 0 的有效记录
* 就认为该分配项目已经分配过额度前端据此决定是否提示再次分配</p>
*
* @param quarterly 要检查的季度例如 1/2/3/4
* @param quarterly 要检查的分配项目编码取值由 OUTLAY_QUARTERLY 字典维护
* @param year 要检查的年份
* @return true 已分配过false 未分配
*/
@@ -60,15 +60,28 @@ public interface OutlayManageUnionAllocateService extends BaseService<OutLayAllo
List<OutlayUnionAllocateImportVo> readImportExcel(TempFile file);
/**
* 根据导入数据按分工会逐条分配季度额度
* 根据导入数据按分工会逐条分配项目额度
*
* @param importList 导入预览数据
* @param quarterly 当前季度
* @param quarterly 当前分配项目编码
* @param year 当前年度
* @return 导入分配结果
*/
Result doImportAllocate(List<OutlayUnionAllocateImportVo> importList, Integer quarterly, Integer year);
/**
* 给指定分工会新增评优奖励分配记录
*
* <p>评优奖励属于 OUTLAY_QUARTERLY 字典中的独立分配项目只针对单个分工会新增
* 新增后同步累加年度预算表总额度分工会经费分配详情会按分配项目展示该记录</p>
*
* @param year 当前预算年度
* @param unionId 要新增评优奖励的分工会 ID
* @param allocateMoney 本次新增的评优奖励金额
* @return Result 成功时返回 success失败时返回校验原因
*/
Result doAddAwardAllocate(Integer year, String unionId, BigDecimal allocateMoney);
/**
* 导出分工会额度导入模板
*
@@ -77,24 +90,24 @@ public interface OutlayManageUnionAllocateService extends BaseService<OutLayAllo
Workbook exportImportTemplate();
/**
* 重置指定年度季度的分配记录
* 重置指定年度分配项目的分配记录
*
* <p>页面支持切换年度和季度所以重置动作必须严格使用前端当前选择值
* 不能再按系统当前自然季度处理否则会出现页面筛选季度和实际重置季度不一致的问题</p>
* <p>页面支持切换年度和分配项目所以重置动作必须严格使用前端当前选择值
* 不能再按系统当前默认项目处理否则会出现页面筛选项目和实际重置项目不一致的问题</p>
*
* @param quarterly 要重置的季度例如 1/2/3/4
* @param quarterly 要重置的分配项目编码
* @param year 要重置的年份
* @return Result 成功时返回 success失败时返回错误信息
*/
Result deleteAllocateRecord(Integer quarterly, Integer year);
/**
* 按指定年度季度生成分工会预算记录
* 按指定年度分配项目生成分工会预算记录
*
* <p>生成前会先校验该年度季度是否已存在有效记录
* <p>生成前会先校验该年度分配项目是否已存在有效记录
* 若已存在则直接提示前端需要先重置后再重新生成</p>
*
* @param quarterly 要生成的季度例如 1/2/3/4
* @param quarterly 要生成的分配项目编码
* @param year 要生成的年份
* @return Result 成功时返回 success失败时返回错误信息
*/
@@ -33,13 +33,15 @@ import java.util.Map;
import java.util.Set;
/**
* 分工会季度额度分配实现
* 分工会分配项目额度分配实现
*
* @author zhf
*/
@IocBean(args = {"refer:dao"})
public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLayAllocateUnion> implements OutlayManageUnionAllocateService {
private static final int AWARD_PROJECT_CODE = 4;
public OutlayManageUnionAllocateServiceImpl(Dao dao) {
super(dao);
}
@@ -50,9 +52,9 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
if (Lang.isEmpty(allocateUnion)) {
return Result.error("分配记录不存在!");
}
int currentQuarter = getCurrentQuarter();
if (allocateUnion.getQuarterly() < currentQuarter) {
return Result.error("当前是第【" + currentQuarter + "季度】无法修改【第" + allocateUnion.getQuarterly() + "季度】的额度!");
int currentProject = getCurrentFeeProject();
if (allocateUnion.getQuarterly() < currentProject) {
return Result.error("当前仅允许修改当前会费项目额度!");
}
applyAllocateMoney(allocateUnion, allocateMoney);
return Result.success();
@@ -60,7 +62,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
@Override
public Result doBatchAllocate(BigDecimal allocateMoney, Integer quarterly, Integer year) {
Result validateResult = validateCurrentQuarterOperation(quarterly);
Result validateResult = validateCurrentPeriodOperation(quarterly);
if (validateResult != null) {
return validateResult;
}
@@ -69,7 +71,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
.and(OutLayAllocateUnion::getYear, "=", year)
.and(OutLayAllocateUnion::getDelFlag, "=", false));
if (allocateUnionList == null || allocateUnionList.isEmpty()) {
return Result.error("当前季度没有分配记录!");
return Result.error("当前分配项目没有分配记录!");
}
for (OutLayAllocateUnion allocateUnion : allocateUnionList) {
applyAllocateMoney(allocateUnion, allocateMoney);
@@ -143,7 +145,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
@Override
public Result doImportAllocate(List<OutlayUnionAllocateImportVo> importList, Integer quarterly, Integer year) {
Result validateResult = validateCurrentQuarterOperation(quarterly);
Result validateResult = validateCurrentPeriodOperation(quarterly);
if (validateResult != null) {
return validateResult;
}
@@ -155,7 +157,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
.and(OutLayAllocateUnion::getYear, "=", year)
.and(OutLayAllocateUnion::getDelFlag, "=", false));
if (allocateUnionList == null || allocateUnionList.isEmpty()) {
return Result.error("当前季度没有分配记录!");
return Result.error("当前分配项目没有分配记录!");
}
List<Sys_union> unionList = dao().query(Sys_union.class, Cnd.NEW());
Map<String, Sys_union> unionMap = new HashMap<>();
@@ -190,7 +192,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
continue;
}
if (!allocateUnionMap.containsKey(union.getId())) {
errorList.add("" + item.getRowNum() + "行:当前季度未生成该分工会分配记录");
errorList.add("" + item.getRowNum() + "行:当前分配项目未生成该分工会分配记录");
}
}
if (!errorList.isEmpty()) {
@@ -199,20 +201,62 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
for (OutlayUnionAllocateImportVo item : importList) {
Sys_union union = unionMap.get(item.getUnionCode());
OutLayAllocateUnion allocateUnion = allocateUnionMap.get(union.getId());
// 导入分配与手工分配保持同一套金额同步规则确保季度表和年度预算表数据一致
// 导入分配与手工分配保持同一套金额同步规则确保分配项目记录和年度预算表数据一致
applyAllocateMoney(allocateUnion, item.getAllocateMoney());
}
return Result.success("导入分配成功");
}
@Override
public Result doAddAwardAllocate(Integer year, String unionId, BigDecimal allocateMoney) {
if (year == null || StrUtil.isBlank(unionId) || allocateMoney == null) {
return Result.error("参数错误!");
}
if (allocateMoney.compareTo(BigDecimal.ZERO) <= 0) {
return Result.error("评优奖励金额必须大于0");
}
Sys_union union = dao().fetch(Sys_union.class, unionId);
if (Lang.isEmpty(union)) {
return Result.error("分工会不存在!");
}
int count = dao().count(OutLayAllocateUnion.class,
Cnd.where(OutLayAllocateUnion::getYear, "=", year)
.and(OutLayAllocateUnion::getUnionId, "=", unionId)
.and(OutLayAllocateUnion::getQuarterly, "=", AWARD_PROJECT_CODE)
.and(OutLayAllocateUnion::getDelFlag, "=", false));
if (count > 0) {
return Result.error("该分工会本年度已存在评优奖励记录,请直接编辑已有记录!");
}
OutLayAllocateUnion allocateUnion = new OutLayAllocateUnion();
allocateUnion.setYear(year);
allocateUnion.setUnionId(unionId);
allocateUnion.setQuarterly(AWARD_PROJECT_CODE);
allocateUnion.setAllocateHeadMoney(getCurrentYearSurplusMoney(year, unionId));
allocateUnion.setAllocateMoney(BigDecimal.ZERO);
insert(allocateUnion);
// 新增评优奖励后复用统一金额同步规则确保分配项目记录和年度预算表 totalQuota 同步
applyAllocateMoney(allocateUnion, allocateMoney);
return Result.success();
}
@Override
public Workbook exportImportTemplate() {
List<ExcelExportEntity> entityList = new ArrayList<>();
entityList.add(new ExcelExportEntity("工会名称", "unionName", 20));
entityList.add(new ExcelExportEntity("工会编码", "unionCode", 20));
entityList.add(new ExcelExportEntity("分配额度", "allocateMoney", 20));
List<Sys_union> unionList = dao().query(Sys_union.class, Cnd.NEW().asc("unionCode"));
List<OutlayUnionAllocateImportVo> templateList = new ArrayList<>();
for (Sys_union union : unionList) {
OutlayUnionAllocateImportVo item = new OutlayUnionAllocateImportVo();
item.setUnionName(union.getName());
item.setUnionCode(union.getUnionCode());
templateList.add(item);
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
return ExcelExportUtil.exportExcel(exportParams, entityList, new ArrayList<>());
return ExcelExportUtil.exportExcel(exportParams, entityList, templateList);
}
@Override
@@ -220,7 +264,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
if (quarterly == null || year == null) {
return Result.error("参数错误!");
}
Result validateResult = validateCurrentQuarterOperation(quarterly);
Result validateResult = validateCurrentPeriodOperation(quarterly);
if (validateResult != null) {
return validateResult;
}
@@ -229,7 +273,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
.and(OutLayAllocateUnion::getYear, "=", year)
.and(OutLayAllocateUnion::getDelFlag, "=", false));
if (allocateUnionList == null || allocateUnionList.isEmpty()) {
return Result.error("当前季度还未分配,无法重置!");
return Result.error("当前分配项目还未分配,无法重置!");
}
for (OutLayAllocateUnion allocateUnion : allocateUnionList) {
@@ -248,7 +292,7 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
if (quarterly == null || year == null) {
return Result.error("参数错误!");
}
Result validateResult = validateCurrentQuarterOperation(quarterly);
Result validateResult = validateCurrentPeriodOperation(quarterly);
if (validateResult != null) {
return validateResult;
}
@@ -257,10 +301,10 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
.and(OutLayAllocateUnion::getYear, "=", year)
.and(OutLayAllocateUnion::getDelFlag, "=", false));
if (count > 0) {
return Result.error("当前季度已分配,如需重新分配请点击重置分配记录!");
return Result.error("当前分配项目已分配,如需重新分配请点击重置分配记录!");
}
// 第一季度需要承接上一年度剩余额度其余季度承接当年预算额度确保生成记录时的分配前额度准确
// 第一个会费项目需要承接上一年度剩余额度其余项目承接当年预算额度确保生成记录时的分配前额度准确
Integer sourceYear = quarterly == 1 ? year - 1 : year;
List<OutlayManageUnion> outlayManageUnionList = dao().query(OutlayManageUnion.class,
Cnd.where(OutlayManageUnion::getYear, "=", sourceYear));
@@ -270,10 +314,10 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
}
/**
* 先回退旧额度再写入新额度的方式同步季度记录和年度预算表
* 先回退旧额度再写入新额度的方式同步分配项目记录和年度预算表
*
* @param allocateUnion 当前季度分配记录包含分工会年份旧分配额度等上下文
* @param newAllocateMoney 本次最终要保存的季度分配金额
* @param allocateUnion 当前分配项目记录包含分工会年份旧分配额度等上下文
* @param newAllocateMoney 本次最终要保存的分配项目金额
*/
private void applyAllocateMoney(OutLayAllocateUnion allocateUnion, BigDecimal newAllocateMoney) {
BigDecimal oldAllocateMoney = defaultValue(allocateUnion.getAllocateMoney());
@@ -285,10 +329,14 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
manageUnion = buildManageUnion(allocateUnion, newAllocateMoney);
insert(manageUnion);
} else {
// 当前 totalQuota 里已经包含了旧季度额度因此要先减旧值再加新值避免重复累加
// 当前 totalQuota 里已经包含了旧项目额度因此要先减旧值再加新值避免重复累加
BigDecimal newTotalQuota = defaultValue(manageUnion.getTotalQuota())
.subtract(oldAllocateMoney)
.add(newAllocateMoney);
BigDecimal lastYearSurplusMoney = getFirstProjectMissingHeadMoney(allocateUnion, manageUnion);
if (lastYearSurplusMoney.compareTo(BigDecimal.ZERO) != 0) {
newTotalQuota = newTotalQuota.add(lastYearSurplusMoney);
}
dao().update(OutlayManageUnion.class, Chain.make("totalQuota", newTotalQuota),
Cnd.where(OutlayManageUnion::getId, "=", manageUnion.getId()));
}
@@ -298,9 +346,9 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
}
/**
* 回滚当前季度已分配到年度预算表中的金额
* 回滚当前分配项目已分配到年度预算表中的金额
*
* @param allocateUnion 当前季度分配记录allocateMoney 表示本次需要从年度额度中扣回的金额
* @param allocateUnion 当前分配项目记录allocateMoney 表示本次需要从年度额度中扣回的金额
*/
private void rollbackAllocateMoney(OutLayAllocateUnion allocateUnion) {
OutlayManageUnion manageUnion = dao().fetch(OutlayManageUnion.class,
@@ -315,11 +363,43 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
Cnd.where(OutlayManageUnion::getId, "=", manageUnion.getId()));
}
/**
* 第一段会费分配记录的 allocateHeadMoney 保存上一年度剩余额度
* 如果当前年度预算表已提前生成导入/一键分配时 totalQuota 可能只包含各分配项目金额
* 此时需要补入这笔期初结余已补入过的年度预算再次导入时不重复累加
*
* @param allocateUnion 当前分配项目记录
* @param manageUnion 当前年度分工会预算记录
* @return BigDecimal 本次需要补入的上一年度剩余额度无需补入时返回 0
*/
private BigDecimal getFirstProjectMissingHeadMoney(OutLayAllocateUnion allocateUnion, OutlayManageUnion manageUnion) {
if (allocateUnion == null || manageUnion == null || !Integer.valueOf(1).equals(allocateUnion.getQuarterly())) {
return BigDecimal.ZERO;
}
BigDecimal headMoney = defaultValue(allocateUnion.getAllocateHeadMoney());
if (headMoney.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
List<OutLayAllocateUnion> allocateUnionList = dao().query(OutLayAllocateUnion.class,
Cnd.where(OutLayAllocateUnion::getUnionId, "=", allocateUnion.getUnionId())
.and(OutLayAllocateUnion::getYear, "=", allocateUnion.getYear())
.and(OutLayAllocateUnion::getDelFlag, "=", false));
BigDecimal allocatedTotalMoney = allocateUnionList.stream()
.map(OutLayAllocateUnion::getAllocateMoney)
.map(this::defaultValue)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal nonAllocateMoney = defaultValue(manageUnion.getTotalQuota()).subtract(allocatedTotalMoney);
if (nonAllocateMoney.compareTo(BigDecimal.ZERO) == 0) {
return headMoney;
}
return BigDecimal.ZERO;
}
/**
* 当前年份预算表不存在时分配前额度 + 本次分配额度创建年度预算记录
*
* @param allocateUnion 当前季度分配记录allocateHeadMoney 表示生成记录时的剩余额度快照
* @param allocateMoney 本次要设置的季度分配额度
* @param allocateUnion 当前分配项目记录allocateHeadMoney 表示生成记录时的剩余额度快照
* @param allocateMoney 本次要设置的分配项目额度
* @return OutlayManageUnion 新建的年度预算实体
*/
private OutlayManageUnion buildManageUnion(OutLayAllocateUnion allocateUnion, BigDecimal allocateMoney) {
@@ -347,38 +427,55 @@ public class OutlayManageUnionAllocateServiceImpl extends BaseServiceImpl<OutLay
}
/**
* 计算当前自然季度
* 计算当前年度指定分工会的可用余额作为新增评优奖励前的额度快照
*
* @return int 当前季度取值范围 1-4
* @param year 当前预算年度
* @param unionId 分工会 ID
* @return BigDecimal 当前年度总额度减已使用额度没有年度预算记录时返回 0
*/
private int getCurrentQuarter() {
return DateUtil.month(DateUtil.date()) / 3 + 1;
private BigDecimal getCurrentYearSurplusMoney(Integer year, String unionId) {
OutlayManageUnion manageUnion = dao().fetch(OutlayManageUnion.class,
Cnd.where(OutlayManageUnion::getYear, "=", year)
.and(OutlayManageUnion::getUnionId, "=", unionId));
if (Lang.isEmpty(manageUnion)) {
return BigDecimal.ZERO;
}
return defaultValue(manageUnion.getTotalQuota()).subtract(defaultValue(manageUnion.getUsedQuota()));
}
/**
* 预算季度记录相关操作只能针对当前自然季度执行
* 计算当前会费项目
*
* @param quarterly 前端传入的目标季度
* @return int 当前会费项目1 表示 1-4月会费2 表示 5-8月会费3 表示 9-12月会费
*/
private int getCurrentFeeProject() {
return DateUtil.month(DateUtil.date()) / 4 + 1;
}
/**
* 预算分配项目记录相关操作只能针对当前会费项目执行
*
* @param quarterly 前端传入的目标分配项目编码取值由 OUTLAY_QUARTERLY 字典维护
* @return Result 不允许操作时返回错误结果允许操作时返回 null
*/
private Result validateCurrentQuarterOperation(Integer quarterly) {
private Result validateCurrentPeriodOperation(Integer quarterly) {
if (quarterly == null) {
return Result.error("参数错误!");
}
int currentQuarter = getCurrentQuarter();
if (!quarterly.equals(currentQuarter)) {
return Result.error("当前仅允许操作" + currentQuarter + "季度数据,请切换后再操作!");
int currentProject = getCurrentFeeProject();
if (!quarterly.equals(currentProject)) {
return Result.error("当前仅允许操作当前会费项目数据,请切换后再操作!");
}
return null;
}
/**
* 按目标年度季度初始化要生成的分工会分配记录
* 按目标年度分配项目初始化要生成的分工会分配记录
*
* @param outlayManageUnionList 用于计算分配前额度的年度预算数据
* @param quarterly 目标季度
* @param quarterly 目标分配项目编码
* @param year 目标年度
* @return List<OutLayAllocateUnion> 待插入的季度分配记录
* @return List<OutLayAllocateUnion> 待插入的分配项目记录
*/
private List<OutLayAllocateUnion> buildAllocateUnionList(List<OutlayManageUnion> outlayManageUnionList, Integer quarterly, Integer year) {
List<Sys_union> unionList = dao().query(Sys_union.class, Cnd.NEW());
@@ -38,13 +38,13 @@ import java.util.List;
/**
* @author zhf
* @date 2026/2/3 11:12
* @description 分工会经费分配
* @description 分工会经费分配项目管理
*/
@IocBean
@At("/platform/outlay/outlayManage/unionAllocate")
@Ok("json:full")
@Slf4j
@Api(tags = "分工会经费分配")
@Api(tags = "分工会经费分配项目管理")
public class OutlayManageUnionAllocateController {
@Inject
@@ -100,11 +100,13 @@ public class OutlayManageUnionAllocateController {
@At
@ApiOperation("一键批量分配所有工会额度")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "分工会预算-季度预算分配", msg = "一键批量分配所有工会额度")
@SLog(tag = "分工会预算-分配项目预算分配", msg = "一键批量分配所有工会额度")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result doBatchAllocate(@Param("allocateMoney") BigDecimal allocateMoney,
@Param("quarterly") Integer quarterly,
@Param("year") Integer year) {
// 参数说明allocateMoney 为本次写入的分配额度quarterly 承载 OUTLAY_QUARTERLY 字典编码对应会费评优奖励等分配项目year 为预算年度
// 返回 Result成功时前端刷新列表失败时 msg 字段用于页面提示
if (allocateMoney == null || quarterly == null || year == null) {
return Result.error("参数错误!");
}
@@ -112,7 +114,7 @@ public class OutlayManageUnionAllocateController {
}
@At
@ApiOperation("校验当前季度是否已分配额度")
@ApiOperation("校验当前分配项目是否已分配额度")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result hasAllocated(@Param("quarterly") Integer quarterly,
@Param("year") Integer year) {
@@ -139,11 +141,13 @@ public class OutlayManageUnionAllocateController {
@At
@ApiOperation("导入分配分工会额度")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "分工会预算-季度预算分配", msg = "导入分配分工会额度")
@SLog(tag = "分工会预算-分配项目预算分配", msg = "导入分配分工会额度")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result doImportAllocate(String data,
@Param("quarterly") Integer quarterly,
@Param("year") Integer year) {
// 参数说明data 为导入预览行 JSONquarterly 为分配项目字典编码year 为预算年度
// 返回 Result成功表示导入额度已同步到分配项目记录和年度预算表失败时 msg 字段返回具体校验原因
if (StrUtil.isBlank(data) || quarterly == null || year == null) {
return Result.error("参数错误!");
}
@@ -151,6 +155,22 @@ public class OutlayManageUnionAllocateController {
return outlayManageUnionAllocateService.doImportAllocate(importList, quarterly, year);
}
@At
@ApiOperation("新增单个分工会评优奖励")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "分工会预算-分配项目预算分配", msg = "新增单个分工会评优奖励")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result doAddAwardAllocate(@Param("year") Integer year,
@Param("unionId") String unionId,
@Param("allocateMoney") BigDecimal allocateMoney) {
// 参数说明year 为预算年度unionId 为分工会 IDallocateMoney 为本次新增的评优奖励金额
// 返回 Result成功后前端刷新分配列表失败时 msg 字段返回参数或重复新增等校验原因
if (year == null || StrUtil.isBlank(unionId) || allocateMoney == null) {
return Result.error("参数错误!");
}
return outlayManageUnionAllocateService.doAddAwardAllocate(year, unionId, allocateMoney);
}
@At
@Ok("void")
@ApiOperation("下载导入分配模板")
@@ -166,12 +186,13 @@ public class OutlayManageUnionAllocateController {
@At
@ApiOperation("重置预算季度记录")
@ApiOperation("重置预算分配记录")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "分工会预算-季度预算分配", msg = "重置预算季度记录")
@SLog(tag = "分工会预算-分配项目预算分配", msg = "重置预算分配记录")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result deleteAllocateRecord(@Param("quarterly") Integer quarterly,
@Param("year") Integer year) {
// 参数说明quarterly 为当前要重置的分配项目编码year 为预算年度返回 Result 用于提示重置是否成功
if (quarterly == null || year == null) {
return Result.error("参数错误!");
}
@@ -179,11 +200,13 @@ public class OutlayManageUnionAllocateController {
}
@At
@ApiOperation("预算季度记录生成")
@SLog(tag = "分工会预算-季度预算分配", msg = "生成了预算分配记录")
@ApiOperation("预算分配记录生成")
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "分工会预算-分配项目预算分配", msg = "生成了预算分配记录")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result doAllocateRecord(@Param("quarterly") Integer quarterly,
@Param("year") Integer year) {
// 参数说明quarterly 为本次生成记录的分配项目编码year 为预算年度返回 Result 用于前端提示生成结果
if (quarterly == null || year == null) {
return Result.error("参数错误!");
}
@@ -0,0 +1,142 @@
package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.vo.OutlayUnionIncomeExpenseVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* 二级分工会经费收支使用情况表
*/
@IocBean
@At("/platform/outlay/outlayManage/unionIncomeExpense")
@Ok("json:full")
@Slf4j
@Api(tags = "二级分工会经费收支使用情况表")
public class OutlayManageUnionIncomeExpenseController {
@Inject
private BaseService baseService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/union/incomeExpense/index.html")
@SaCheckPermission("outlay.outlayManage.union.incomeExpense")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("outlay.outlayManage.union.incomeExpense")
public Result pageData(PageForm pageForm, Integer year, String unionId) {
Sql sql = buildIncomeExpenseSql(year, unionId);
Pagination pagination = baseService.listPageVO(pageForm, sql, OutlayUnionIncomeExpenseVO.class);
return Result.success(pagination);
}
@At
@Ok("void")
@ApiOperation("导出")
@SaCheckPermission("outlay.outlayManage.union.incomeExpense")
public void doExport(Integer year, String unionId, HttpServletResponse response) {
Integer queryYear = getQueryYear(year);
Sql sql = buildIncomeExpenseSql(queryYear, unionId);
List<OutlayUnionIncomeExpenseVO> list = baseService.listVO(sql, OutlayUnionIncomeExpenseVO.class);
ArrayList<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("单位", "unionName", 25));
entities.add(new ExcelExportEntity("年初数", "beginMoney", 20));
entities.add(new ExcelExportEntity("1-4月会费", "feeJanApr", 20));
entities.add(new ExcelExportEntity("5-8月会费", "feeMayAug", 20));
entities.add(new ExcelExportEntity("9-12月会费", "feeSepDec", 20));
entities.add(new ExcelExportEntity("评优奖励", "awardMoney", 20));
entities.add(new ExcelExportEntity("1-12月支出", "usedMoney", 20));
entities.add(new ExcelExportEntity("经费余额", "remainMoney", 20));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
CommonDownloadUtil.download(queryYear + "年二级分工会经费收支使用情况表.xlsx", workbook, response);
}
/**
* 统计参数说明
* year 为统计年度未传时默认当前年份unionId 为二级分工会主键管理员可为空查询全部
* 返回 VO beginMoney 是年初数feeJanApr/feeMayAug/feeSepDec 是三个会费收入段
* awardMoney 是评优奖励usedMoney 是年度支出remainMoney 是当前经费余额
*/
private Sql buildIncomeExpenseSql(Integer year, String unionId) {
Sql sql = Sqls.create("""
SELECT
mu.year AS year,
mu.unionId AS unionId,
COALESCE(mu.unionName, su.name) AS unionName,
COALESCE(mu.unionCode, su.unionCode) AS unionCode,
COALESCE(SUM(CASE WHEN au.quarterly = 1 THEN au.allocateHeadMoney ELSE 0 END), 0) AS beginMoney,
COALESCE(SUM(CASE WHEN au.quarterly = 1 THEN au.allocateMoney ELSE 0 END), 0) AS feeJanApr,
COALESCE(SUM(CASE WHEN au.quarterly = 2 THEN au.allocateMoney ELSE 0 END), 0) AS feeMayAug,
COALESCE(SUM(CASE WHEN au.quarterly = 3 THEN au.allocateMoney ELSE 0 END), 0) AS feeSepDec,
COALESCE(SUM(CASE WHEN au.quarterly = 4 THEN au.allocateMoney ELSE 0 END), 0) AS awardMoney,
COALESCE(mu.usedQuota, 0) AS usedMoney,
COALESCE(mu.totalQuota, 0) - COALESCE(mu.usedQuota, 0) AS remainMoney
FROM outlay_manage_union mu
LEFT JOIN sys_union su ON su.id = mu.unionId
LEFT JOIN outlay_allocate_union au ON au.year = mu.year
AND au.unionId = mu.unionId
AND au.delFlag = 0
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("mu.delFlag", "=", false);
cnd.and("mu.year", "=", getQueryYear(year));
cnd.and("mu.totalQuota", "IS NOT", null);
cnd.andEX("mu.unionId", "=", unionId);
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("mu.unionId", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("mu.year");
cnd.groupBy("mu.unionId");
cnd.groupBy("mu.unionName");
cnd.groupBy("mu.unionCode");
cnd.groupBy("su.name");
cnd.groupBy("su.unionCode");
cnd.groupBy("mu.usedQuota");
cnd.groupBy("mu.totalQuota");
cnd.asc("mu.unionCode");
sql.setCondition(cnd);
return sql;
}
private Integer getQueryYear(Integer year) {
if (year != null) {
return year;
}
return DateUtil.thisYear();
}
}
@@ -116,7 +116,7 @@ public class OutlayManageUnionUseDetailController {
}
@At
@ApiOperation("查询分工会今年每个季度的分配情况")
@ApiOperation("查询分工会今年每个分配项目的分配情况")
@SaCheckPermission("outlay.outlayManage.union.useDetail")
public Result queryQuarterlyList(Integer year, String unionId){
Sql sql = Sqls.create("""
@@ -37,7 +37,7 @@ public class OutLayAllocateUnion extends BaseModel implements Serializable {
private String unionId;
@Column
@Comment("季度")
@Comment("分配项目")
@ColDefine(type = ColType.INT)
private Integer quarterly;
@@ -6,22 +6,20 @@ import lombok.Data;
import java.math.BigDecimal;
/**
* 分工会季度额度导入行数据
* 分工会分配项目额度导入行数据
*/
@Data
public class OutlayUnionAllocateImportVo {
@Excel(name = "工会名称")
private String unionName;
@Excel(name = "工会编码")
private String unionCode;
@Excel(name = "分配额度")
private BigDecimal allocateMoney;
/**
* 预览时展示匹配到的工会名称
*/
private String unionName;
/**
* 预览时展示当前行错误信息
*/
@@ -0,0 +1,34 @@
package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 二级分工会经费收支使用情况表数据
*/
@Data
public class OutlayUnionIncomeExpenseVO {
private Integer year;
private String unionId;
private String unionName;
private String unionCode;
private BigDecimal beginMoney;
private BigDecimal feeJanApr;
private BigDecimal feeMayAug;
private BigDecimal feeSepDec;
private BigDecimal awardMoney;
private BigDecimal usedMoney;
private BigDecimal remainMoney;
}
@@ -307,6 +307,9 @@ public class SiteCugApplyController {
if (item == null || StrUtil.hasBlank(item.getReserveStartTime(), item.getReserveEndTime())) {
continue;
}
if (isOccupiedByCurrentUser(item)) {
continue;
}
DateTime reserveStart = DateUtil.parseDateTime(item.getReserveStartTime());
DateTime reserveEnd = DateUtil.parseDateTime(item.getReserveEndTime());
if (!reserveEnd.isAfter(reserveStart)) {
@@ -389,6 +392,13 @@ public class SiteCugApplyController {
}
}
// 当前登录人占用了该预约时该预约占用的时间块对当前登录人放开对其他人仍保持锁定
private boolean isOccupiedByCurrentUser(SiteCugApply apply) {
return apply != null
&& StrUtil.isNotBlank(apply.getOccupyUserId())
&& StrUtil.equals(apply.getOccupyUserId(), SecurityUtil.getUserId());
}
private boolean intersectsAny(int start, int end, List<int[]> ranges) {
for (int[] range : ranges) {
if (range != null && start < range[1] && end > range[0]) {
@@ -693,11 +703,11 @@ public class SiteCugApplyController {
@SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR)
public Result timeLimitConfig(@Param("siteId") String siteId) {
if (StrUtil.isBlank(siteId)) {
return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>()));
return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>()).addv("termStartDate", "").addv("termEndDate", ""));
}
SiteCugInfo info = infoService.fetch(siteId);
if (info == null) {
return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>()));
return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>()).addv("termStartDate", "").addv("termEndDate", ""));
}
List<String> holidayList = new ArrayList<>();
if (Boolean.TRUE.equals(info.getFilterHolidays())) {
@@ -713,7 +723,9 @@ public class SiteCugApplyController {
return Result.success(NutMap.NEW()
.addv("filterHolidays", Boolean.TRUE.equals(info.getFilterHolidays()))
.addv("holidayList", holidayList)
.addv("notApplyTimeList", info.getNotApplyTimeList() == null ? new ArrayList<>() : info.getNotApplyTimeList()));
.addv("notApplyTimeList", info.getNotApplyTimeList() == null ? new ArrayList<>() : info.getNotApplyTimeList())
.addv("termStartDate", info.getTermStartDate() != null ? info.getTermStartDate() : "")
.addv("termEndDate", info.getTermEndDate() != null ? info.getTermEndDate() : ""));
}
private List<SiteCugApply> buildYearlyApplyList(SiteCugApply apply) {
@@ -819,5 +831,22 @@ public class SiteCugApplyController {
for (ProcessTask task : doingTaskList) {
flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args);
}
if (applyService.shouldDirectFinishOccupiedApply(apply)) {
directFinishOccupiedApply(instance, args);
}
}
// 占用人提交与占用时间交叉的预约时复用已有流程引擎自动结束后续审核任务
private void directFinishOccupiedApply(ProcessInstance instance, Dict args) {
List<ProcessTask> doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
if (doingTaskList.isEmpty()) {
return;
}
Dict directArgs = args.clone();
directArgs.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.AGREE.getCode());
directArgs.set(FlowConst.APPROVAL_COMMENT, "占用预约自动通过");
for (ProcessTask task : doingTaskList) {
flowEngine.executeAndJumpToEnd(task.getId(), FlowConst.AUTO_ID, directArgs);
}
}
}
@@ -105,6 +105,13 @@ public class SiteCugManageController {
if ((fullDayOpenHour == null || fullDayOpenHour.isEmpty()) && info.getReserveTimeType() == 2 && info.getOpenHours() != null && !info.getOpenHours().isEmpty()) {
fullDayOpenHour = info.getOpenHours().get(0);
}
// 场地预约次数限制作为后续提交校验的依据保存场地时必须配置为正整数
if (info.getUnionYearReserveLimit() == null || info.getUnionYearReserveLimit() <= 0) {
return Result.error("分工会每年预约次数必须大于0");
}
if (info.getClubWeekReserveLimit() == null || info.getClubWeekReserveLimit() <= 0) {
return Result.error("社团每周预约次数必须大于0");
}
if (info.getReserveTimeType() == 1) {
if (segmentedOpenHours == null || segmentedOpenHours.isEmpty()) {
return Result.error("Please add at least one booking slot");
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.dayofficework.siteCug.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.FlowEngine;
@@ -24,6 +25,7 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
@IocBean
@Ok("json:full")
@@ -87,6 +89,28 @@ public class SiteCugRecordController {
return Result.success();
}
@At
@ApiOperation("占用预约记录")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.record")
public Result occupy(@Param("id") String id, @Param("message") String message) {
Map<Boolean, String> result = applyService.occupyRecord(id, message, false);
return result.containsKey(false) ? Result.error(result.get(false)) : Result.success(result.get(true));
}
@At
@ApiOperation("批量占用预约记录")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.record")
public Result occupyBatch(@Param("ids") String ids, @Param("message") String message) {
if (StrUtil.isBlank(ids)) {
return Result.error("请选择需要占用的预约");
}
List<String> idList = StrUtil.split(ids, ",").stream().filter(StrUtil::isNotBlank).toList();
Map<Boolean, String> result = applyService.occupyRecords(idList, message, false);
return result.containsKey(false) ? Result.error(result.get(false)) : Result.success(result.get(true));
}
@At
@Ok("void")
@ApiOperation("导出预约记录")
@@ -110,5 +110,15 @@ public class SiteCugApply extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 200)
private String backOption;
@Column
@Comment("占用人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String occupyUserId;
@Column
@Comment("占用通知消息")
@ColDefine(type = ColType.VARCHAR, width = 1000)
private String occupyMessage;
private String yearlyReserveEndDate;
}
@@ -73,6 +73,28 @@ public class SiteCugInfo extends BaseModel {
@ColDefine(type = ColType.INT)
private Integer sexLimit;
@Column
@Comment("本学期开始时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String termStartDate;
@Column
@Comment("本学期结束时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String termEndDate;
@Column
@Comment("分工会每年预约次数")
@ColDefine(type = ColType.INT)
@Default("2")
private Integer unionYearReserveLimit;
@Column
@Comment("社团每周预约次数")
@ColDefine(type = ColType.INT)
@Default("2")
private Integer clubWeekReserveLimit;
@Column
@Comment("场地的禁用时间")
@ColDefine(type = ColType.MYSQL_JSON)
@@ -7,6 +7,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.sql.Sql;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
public interface SiteCugApplyService extends BaseService<SiteCugApply> {
@@ -21,4 +22,13 @@ public interface SiteCugApplyService extends BaseService<SiteCugApply> {
// 场馆预约查询页的导出逻辑统一放在 servicecontroller 只负责接收请求
void exportRecord(SiteCugRecordPageForm pageForm, HttpServletResponse response);
// 占用单条预约记录保存占用人和通知内容真实发送消息代码当前保留为注释便于测试流程
Map<Boolean, String> occupyRecord(String id, String message, boolean sendMsg);
// 批量占用预约记录先统一校验再保存避免出现部分记录已占用部分记录失败的状态
Map<Boolean, String> occupyRecords(List<String> ids, String message, boolean sendMsg);
// 判断当前预约是否命中当前登录人的占用时间段命中后提交预约可自动通过审核
boolean shouldDirectFinishOccupiedApply(SiteCugApply apply);
}
@@ -10,10 +10,10 @@ import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.models.SysHoliday;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.model.SysClub;
import com.budwk.app.zhgh.club.service.SysClubService;
@@ -46,6 +46,8 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
@Inject
private SysClubService sysClubService;
@Inject
private SysMsgService sysMsgService;
public SiteCugApplyServiceImpl(Dao dao) {
super(dao);
@@ -78,6 +80,10 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
if (!Boolean.TRUE.equals(siteInfo.getState())) {
return Map.of(false, "该场地未开启预约");
}
Map<Boolean, String> termDateRangeValidate = validateTermDateRange(apply, siteInfo);
if (termDateRangeValidate.containsKey(false)) {
return termDateRangeValidate;
}
if (apply.getJoinCount() == null || apply.getJoinCount() <= 0) {
return Map.of(false, "预约人数必须大于0");
}
@@ -96,6 +102,10 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
if (intersectsDisabledTime(siteInfo, apply.getReserveStartTime(), apply.getReserveEndTime())) {
return Map.of(false, "预约时间落在禁用时间内");
}
Map<Boolean, String> reserveLimitValidate = validateReserveLimit(apply, siteInfo);
if (reserveLimitValidate.containsKey(false)) {
return reserveLimitValidate;
}
Sql sql = Sqls.create("""
select
@@ -108,8 +118,7 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
Cnd cnd = Cnd.NEW();
cnd.and(SiteCugApply::getSiteId, "=", apply.getSiteId());
cnd.andEX("sa.id", "!=", apply.getId());
List<Integer> stateList = List.of(ProcessInstanceStateEnum.DOING.getCode(), ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.PENDING.getCode());
cnd.and(ProcessInstance::getState, "in", stateList);
cnd.and("ins.state", "in", buildEffectiveProcessStateList());
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
@@ -118,6 +127,9 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
boolean overlap = DateUtil.parseDateTime(item.getReserveStartTime()).isBefore(DateUtil.parseDateTime(apply.getReserveEndTime()))
&& DateUtil.parseDateTime(item.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()));
if (overlap) {
if (isOccupiedByCurrentUser(item)) {
continue;
}
if (StrUtil.isBlank(apply.getId()) && StrUtil.equals(item.getApplyUserId(), SecurityUtil.getUserId())) {
return Map.of(false, "该时间段您已存在预约");
}
@@ -127,6 +139,236 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
return Map.of(true, "");
}
// 分工会预约必须落在后台配置的本学期时间内批量预约逐条复用该校验防止接口越权提交学期外日期
private Map<Boolean, String> validateTermDateRange(SiteCugApply apply, SiteCugInfo siteInfo) {
if (!StrUtil.equals(apply.getReserveType(), "union")
|| StrUtil.hasBlank(siteInfo.getTermStartDate(), siteInfo.getTermEndDate())) {
return Map.of(true, "");
}
try {
DateTime reserveStart = DateUtil.parseDateTime(apply.getReserveStartTime());
DateTime reserveEnd = DateUtil.parseDateTime(apply.getReserveEndTime());
Date termStart = DateUtil.beginOfDay(DateUtil.parseDate(siteInfo.getTermStartDate()));
Date termEnd = DateUtil.endOfDay(DateUtil.parseDate(siteInfo.getTermEndDate()));
if (reserveStart.isBefore(termStart) || reserveEnd.isAfter(termEnd)) {
return Map.of(false, "预约时间必须在本学期时间范围内");
}
return Map.of(true, "");
} catch (Exception e) {
return Map.of(false, "本学期时间配置有误,请联系管理员处理");
}
}
@Override
public Map<Boolean, String> occupyRecord(String id, String message, boolean sendMsg) {
if (StrUtil.isBlank(message)) {
return Map.of(false, "请填写通知消息");
}
SiteCugApply apply = fetch(id);
if (apply == null) {
return Map.of(false, "记录不存在");
}
Map<Boolean, String> validate = validateOccupyApply(apply);
if (validate.containsKey(false)) {
return validate;
}
saveOccupyApply(apply, message, sendMsg);
return Map.of(true, "占用成功");
}
// 已通过校验后统一保存占用信息测试阶段真实发送消息代码保留注释
private void saveOccupyApply(SiteCugApply apply, String message, boolean sendMsg) {
// 保存占用人和通知内容后续预约校验据此只放行当前占用人
apply.setOccupyUserId(SecurityUtil.getUserId());
apply.setOccupyMessage(message);
updateIgnoreNull(apply);
// 测试占用流程时先不真实发送消息保留日志用于确认保存和跳转链路是否正常
log.info("场地预约占用测试消息,applyId={}receiverLoginName={}message={}", apply.getId(), apply.getApplyLoginName(), message);
if (sendMsg) {
// sysMsgService.sendMsgInSys(List.of(apply.getApplyLoginName()), "场地预约占用通知", message, SecurityUtil.getUserId());
}
}
@Override
public Map<Boolean, String> occupyRecords(List<String> ids, String message, boolean sendMsg) {
if (ids == null || ids.isEmpty()) {
return Map.of(false, "请选择需要占用的预约");
}
if (StrUtil.isBlank(message)) {
return Map.of(false, "请填写通知消息");
}
List<SiteCugApply> applyList = new ArrayList<>();
for (String id : ids) {
SiteCugApply apply = fetch(id);
if (apply == null) {
return Map.of(false, "记录不存在");
}
Map<Boolean, String> validate = validateOccupyApply(apply);
if (validate.containsKey(false)) {
return validate;
}
applyList.add(apply);
}
for (SiteCugApply apply : applyList) {
saveOccupyApply(apply, message, sendMsg);
}
return Map.of(true, "占用成功");
}
// 占用前统一校验预约状态避免单条和批量接口出现不同判断
private Map<Boolean, String> validateOccupyApply(SiteCugApply apply) {
if (StrUtil.isBlank(apply.getApplyLoginName())) {
return Map.of(false, "预约人工号为空,无法发送消息");
}
if (StrUtil.isBlank(apply.getReserveStartTime()) || !DateUtil.parseDateTime(apply.getReserveStartTime()).isAfter(DateUtil.date())) {
return Map.of(false, "预约已开始,不能占用");
}
Integer state = queryInstanceState(apply.getId());
if (state == null || state != ProcessInstanceStateEnum.FINISHED.getCode()) {
return Map.of(false, "仅可占用已通过且未开始的预约记录");
}
if (StrUtil.isNotBlank(apply.getOccupyUserId()) && !StrUtil.equals(apply.getOccupyUserId(), SecurityUtil.getUserId())) {
return Map.of(false, "该预约已被其他人占用");
}
return Map.of(true, "");
}
private Integer queryInstanceState(String id) {
Sql sql = Sqls.create("""
SELECT
ins.state AS instanceState
FROM
site_cug_apply info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
WHERE info.id = @id
""");
sql.params().set("id", id);
sql.setCallback(Sqls.callback.maps());
dao().execute(sql);
List<NutMap> list = sql.getList(NutMap.class);
return list != null && !list.isEmpty() ? list.get(0).getInt("instanceState") : null;
}
// 当前登录人占用了该记录时允许其再次提交与该记录有交叉的预约
private boolean isOccupiedByCurrentUser(SiteCugApply apply) {
return apply != null
&& StrUtil.isNotBlank(apply.getOccupyUserId())
&& StrUtil.equals(apply.getOccupyUserId(), SecurityUtil.getUserId());
}
@Override
public boolean shouldDirectFinishOccupiedApply(SiteCugApply apply) {
if (apply == null || StrUtil.hasBlank(apply.getSiteId(), apply.getReserveStartTime(), apply.getReserveEndTime())) {
return false;
}
Sql sql = Sqls.create("""
select
sa.*
from
site_cug_apply sa
left join wf_process_instance ins on ins.businessNo = sa.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("sa.siteId", "=", apply.getSiteId());
cnd.andEX("sa.id", "!=", apply.getId());
cnd.and("sa.occupyUserId", "=", SecurityUtil.getUserId());
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
// 只要新预约时间和当前登录人占用的已通过预约有交叉就允许本次预约免审通过
return applyList.stream().anyMatch(item ->
DateUtil.parseDateTime(item.getReserveStartTime()).isBefore(DateUtil.parseDateTime(apply.getReserveEndTime()))
&& DateUtil.parseDateTime(item.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()))
);
}
// 按预约主体统计有效预约次数批量预约同一个批次号只计一次避免一次批量提交占用多次额度
private Map<Boolean, String> validateReserveLimit(SiteCugApply apply, SiteCugInfo siteInfo) {
if (StrUtil.equals(apply.getReserveType(), "union")) {
if (siteInfo.getUnionYearReserveLimit() == null || siteInfo.getUnionYearReserveLimit() <= 0) {
return Map.of(false, "请先配置分工会每年预约次数");
}
String[] range = buildYearRange(apply.getReserveStartTime());
long count = countEffectiveReserveTimes(apply, "sa.applyUnionId", apply.getApplyUnionId(), range[0], range[1]);
if (count >= siteInfo.getUnionYearReserveLimit()) {
return Map.of(false, "该分工会本年度预约次数已达" + siteInfo.getUnionYearReserveLimit() + "次,不能继续预约");
}
}
if (StrUtil.equals(apply.getReserveType(), "club")) {
if (siteInfo.getClubWeekReserveLimit() == null || siteInfo.getClubWeekReserveLimit() <= 0) {
return Map.of(false, "请先配置社团每周预约次数");
}
String[] range = buildWeekRange(apply.getReserveStartTime());
long count = countEffectiveReserveTimes(apply, "sa.clubId", apply.getClubId(), range[0], range[1]);
if (count >= siteInfo.getClubWeekReserveLimit()) {
return Map.of(false, "该协会本周预约次数已达" + siteInfo.getClubWeekReserveLimit() + "次,不能继续预约");
}
}
return Map.of(true, "");
}
// 分工会每年次数按预约开始时间所在自然年统计
private String[] buildYearRange(String reserveStartTime) {
DateTime reserveStart = DateUtil.parseDateTime(reserveStartTime);
String year = String.valueOf(reserveStart.year());
return new String[]{year + "-01-01 00:00:00", year + "-12-31 23:59:59"};
}
// 协会每周次数按自然周统计周一为开始周日为结束
private String[] buildWeekRange(String reserveStartTime) {
DateTime reserveStart = DateUtil.parseDateTime(reserveStartTime);
int week = reserveStart.dayOfWeek() - 1;
int offsetToMonday = week == 0 ? -6 : 1 - week;
DateTime weekStart = DateUtil.offsetDay(reserveStart, offsetToMonday);
DateTime weekEnd = DateUtil.offsetDay(weekStart, 6);
return new String[]{DateUtil.formatDate(weekStart) + " 00:00:00", DateUtil.formatDate(weekEnd) + " 23:59:59"};
}
// 统计预约主体在指定时间范围内的有效预约次数所有场地合计不按场地过滤
private long countEffectiveReserveTimes(SiteCugApply apply, String targetField, String targetId, String rangeStart, String rangeEnd) {
if (StrUtil.isBlank(targetId)) {
return 0;
}
Sql sql = Sqls.create("""
select
sa.*
from
site_cug_apply sa
left join wf_process_instance ins on ins.businessNo = sa.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("sa.reserveType", "=", apply.getReserveType());
cnd.andEX(targetField, "=", targetId);
cnd.andEX("sa.reserveStartTime", ">=", rangeStart);
cnd.andEX("sa.reserveStartTime", "<=", rangeEnd);
if (StrUtil.isNotBlank(apply.getId())) {
cnd.andEX("sa.id", "!=", apply.getId());
}
cnd.and("ins.state", "in", buildEffectiveProcessStateList());
sql.setCondition(cnd);
List<SiteCugApply> applyList = listEntity(sql);
return applyList.stream()
.map(this::buildReserveCountKey)
.distinct()
.count();
}
// 只有待审核办理中已通过的流程占用预约次数驳回记录不占额度
private List<Integer> buildEffectiveProcessStateList() {
return List.of(ProcessInstanceStateEnum.DOING.getCode(), ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.PENDING.getCode());
}
// 批量预约同一批次号只算一次普通预约按单条申请计算
private String buildReserveCountKey(SiteCugApply apply) {
if (apply != null && StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith("YEARLY_BATCH:")) {
return apply.getBackOption();
}
return apply == null ? "" : apply.getId();
}
@Override
public Sql buildRecordSql() {
// 场馆预约查询页和导出页共用同一套返回字段统一在 service 中维护避免 controller 重复拼 SQL
@@ -135,6 +377,7 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
info.*,
si.name AS siteName,
ins.id AS instanceId,
ins.state AS instanceState,
ins.processDefineId instanceProcessDefineId,
CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch,
CASE WHEN info.reserveType = 'club' THEN '协会预约' ELSE '分工会预约' END AS reserveTypeName,
@@ -315,7 +558,3 @@ public class SiteCugApplyServiceImpl extends BaseServiceImpl<SiteCugApply> imple
return Map.of(false, "预约类型不合法");
}
}
@@ -111,8 +111,6 @@ public class UnionReimburseCollectController {
cnd.and(seg);
}
cnd.and("info.stateId", "in", List.of(3, 2));
applyListSort(cnd, pageForm);
sql.setCondition(cnd);
Pagination<NutMap> pagination = unionReimburseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -185,29 +183,26 @@ public class UnionReimburseCollectController {
@Ok("void")
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
@ApiOperation("导出报销汇总表")
public void onExport(@Param(value = "year") Integer year,
public void onExport(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "reimburseProject") String reimburseProject,
@Param(value = "userName") String userName,
@Param(value = "searchKeyword") String searchKeyword,
HttpServletResponse response) {
//查询审核通过的数据
// 导出复用收集列表筛选口径保证下载结果与页面当前查询条件一致
Sql sql = Sqls.create("""
SELECT
t1.*,
ins.state instanceState
info.*
FROM
union_reimburse t1
LEFT JOIN `wf_process_instance` ins ON ins.businessNo = t1.id
union_reimburse info
$condition
""");
Cnd cnd = buildCondition(year, unionId, unitId, reimburseProject, userName, "t1.");
// 只查询流程实例状态为20的数据已完成状态
cnd.and("ins.state", "=", 20);
Cnd cnd = buildCondition(year, unionId, unitId, reimburseProject, searchKeyword, "info.");
if (!isAdmin()) {
cnd.andEX("t1.unionId", "=", SecurityUtil.getUnionId());
cnd.andEX("info.unionId", "=", SecurityUtil.getUnionId());
}
cnd.desc("t1.createTime");
applyListSort(cnd, pageForm);
sql.setCondition(cnd);
List<UnionReimburseCollectExcelVO> list = unionReimburseService.listVO(sql,UnionReimburseCollectExcelVO.class);
@@ -242,11 +237,13 @@ public class UnionReimburseCollectController {
item.setCreateTime(DateUtil.format(DateUtil.parse(item.getCreateTime()), "yyyy-MM-dd HH:mm:ss"));
}
// 拼接备注字段仿照系统代码逻辑使用activityName和condolenceName字段
item.setStateName(formatStateName(item.getStateId()));
// 备注字段与页面表格保持一致慰问显示被慰问人其他项目显示活动名称
if (cn.hutool.core.util.StrUtil.isNotBlank(item.getActivityName())) {
item.setRemark("活动名称:" + item.getActivityName());
} else if (cn.hutool.core.util.StrUtil.isNotBlank(item.getCondolenceName())) {
item.setRemark("慰问对象:" + item.getCondolenceName());
} else if (cn.hutool.core.util.StrUtil.isNotBlank(item.getCondolenceUserName())) {
item.setRemark("慰问:" + item.getCondolenceUserName());
}
});
@@ -263,12 +260,12 @@ public class UnionReimburseCollectController {
* @param unionId 工会ID
* @param unitId 单位ID
* @param reimburseProject 报销项目
* @param userName 经办人姓名
* @param searchKeyword 经办人姓名或工号
* @param prefix 表前缀
* @return 查询条件
*/
private Cnd buildCondition(Integer year, String unionId, String unitId,
String reimburseProject, String userName, String prefix) {
String reimburseProject, String searchKeyword, String prefix) {
Cnd cnd = Cnd.NEW();
// 年度查询条件
@@ -281,9 +278,12 @@ public class UnionReimburseCollectController {
// 报销项目查询条件
cnd.andEX(prefix + "reimburseProject", "=", reimburseProject);
// 经办人姓名查询条件
if (StrUtil.isNotBlank(userName)) {
cnd.and(prefix + "userName", "like", "%" + userName + "%");
// 经办人姓名或工号查询条件
if (StrUtil.isNotBlank(searchKeyword)) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike(prefix + "userName", searchKeyword);
seg.orLike(prefix + "loginName", searchKeyword);
cnd.and(seg);
}
return cnd;
@@ -318,4 +318,18 @@ public class UnionReimburseCollectController {
default -> null;
};
}
private String formatStateName(Integer stateId) {
if (stateId == null) {
return "";
}
return switch (stateId) {
case 1 -> "待提交";
case 2 -> "待审核确认";
case 3 -> "报销成功";
case 4 -> "拒绝";
case 5 -> "退回";
default -> String.valueOf(stateId);
};
}
}
@@ -175,6 +175,11 @@ public class UnionReimburse extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankOfDeposit;
@Column
@Comment("支付明细")
@ColDefine(type = ColType.MYSQL_JSON)
private List<JSONObject> paymentDetails;
/**
* 慰问
*/
@@ -369,13 +369,15 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (Integer.valueOf(3).equals(dbRecord.getStateId())) {
return Result.success();
}
Result validateResult = validateFundSourceBeforeSubmit(dbRecord);
if (validateResult != null) {
return validateResult;
}
Result deductResult = deductBudgetAndSaveUseDetail(dbRecord);
if (deductResult != null && deductResult.getCode() != 0) {
return deductResult;
if (!skipBudgetDeduct(dbRecord)) {
Result validateResult = validateFundSourceBeforeSubmit(dbRecord);
if (validateResult != null) {
return validateResult;
}
Result deductResult = deductBudgetAndSaveUseDetail(dbRecord);
if (deductResult != null && deductResult.getCode() != 0) {
return deductResult;
}
}
}
dbRecord.setReviewTime(new Date());
@@ -422,7 +424,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (dbRecord == null) {
return Result.error("未找到对应的报销记录");
}
if (Integer.valueOf(3).equals(dbRecord.getStateId())) {
if (Integer.valueOf(3).equals(dbRecord.getStateId()) && !skipBudgetDeduct(dbRecord)) {
OutlayUseDetail oldDetail = dao().fetch(OutlayUseDetail.class, Cnd.where("outlayReimburseId", "=", dbRecord.getId()));
BigDecimal oldMoney = oldDetail != null && oldDetail.getAdjustMoney() != null
? oldDetail.getAdjustMoney()
@@ -652,6 +654,16 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return Result.error("不支持的经费来源");
}
/**
* 慰问和专项活动费用不占用经费预算审核通过及后续调整实际金额时都不写预算台账
*/
private boolean skipBudgetDeduct(UnionReimburse unionReimburse) {
return unionReimburse != null && Set.of(
"UNION_REIMBURSE_PROJECT_1",
"UNION_REIMBURSE_PROJECT_4"
).contains(unionReimburse.getReimburseProject());
}
/**
* 已通过报销单修改实际金额时用差额同步预算主表和使用明细避免重复全量扣减
*/
@@ -22,23 +22,30 @@ public class UnionReimburseCollectExcelVO implements Serializable {
@Excel(name = "所属单位", width = 20)
private String unitName;
@Excel(name = "申请时间", width = 30)
private String createTime;
@Excel(name = "报销项目", width = 20)
private String reimburseProject;
@Excel(name = "备注", width = 20)
private String remark;
@Excel(name = "金额", width = 20)
@Excel(name = "申请状态", width = 20)
private String stateName;
private Double money;
@Excel(name = "申请时间", width = 30)
private String createTime;
private Integer stateId;
private String reimburseFundSource;
//活动名称
private String activityName;
//被慰问人姓名
private String condolenceUserName;
//被慰问人
private String condolenceName;
@@ -63,6 +63,11 @@ public class AidFundMemberHistory extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 20)
private String personType;
@Column
@Comment("人员属性")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String userAttribute;
@Column
@Comment("在职状态")
@ColDefine(type = ColType.VARCHAR, width = 10)
@@ -64,4 +64,9 @@ public class AidFundMemberPay extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 10)
private String userState;
@Column
@Comment("人员属性")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String userAttribute;
}