Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-07-08 09:26:37 +08:00
12 changed files with 798 additions and 356 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
@@ -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);
}
}
@@ -50,6 +50,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 +119,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,14 +142,16 @@ 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));
@@ -206,10 +178,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 +199,122 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
}
}
/**
* 获取信息中心开放平台访问令牌。
* 返回值为接口后续调用使用的 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());
}
@Async
private void updateDict(List<Sys_user_source> userSources) {
//判断是否要更新在职状态字典
@@ -388,70 +478,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("旧数据中心财务拉取接口已停用");
}
}
@@ -80,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");
}
@@ -135,6 +139,26 @@ 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)) {
@@ -175,9 +175,9 @@ const apply = {
<el-col :span="24">
<el-form-item label="批量预约">
<div style="display: flex; align-items: flex-start; column-gap: 12px; flex-wrap: wrap; line-height: 1.7;">
<el-checkbox v-model="yearlyReserve">预约本后续每周同一时段</el-checkbox>
<el-checkbox v-model="yearlyReserve">预约本后续每周同一时段</el-checkbox>
<span style="color: #909399;">
例如先选某个周一 09:00-11:00,勾选后会自动预约本剩余所有周一的这个时段。
例如先选某个周一 09:00-11:00,勾选后会自动预约本剩余所有周一的这个时段。
</span>
</div>
<div v-if="yearlyReserve" style="margin-top: 10px; max-width: 320px;">
@@ -344,7 +344,10 @@ const apply = {
if (!this.formData.reserveStartTime) {
return false
}
return this.$moment(time).format('YYYY-MM-DD') < this.formData.reserveStartTime.slice(0, 10)
const targetDay = this.$moment(time).format('YYYY-MM-DD')
const startDay = this.formData.reserveStartTime.slice(0, 10)
const monthEndDay = this.$moment(this.formData.reserveStartTime).endOf('month').format('YYYY-MM-DD')
return targetDay < startDay || targetDay > monthEndDay
},
}
},
@@ -397,10 +400,10 @@ const apply = {
return ''
}
if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) {
return '请先选择开始和结束时间后,再批量预约本同星期时段'
return '请先选择开始和结束时间后,再批量预约本同星期时段'
}
if (!this.yearlyReserveDates.length) {
return '当前时间段无法生成本批量预约日期'
return '当前时间段无法生成本批量预约日期'
}
return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.yearlyReserveDates[this.yearlyReserveDates.length - 1] + ',共 ' + this.yearlyReserveDates.length + ' 个时段'
},
@@ -498,6 +501,10 @@ const apply = {
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
return
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate > defaultDate) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
}
},
getInitialScheduleDate() {
@@ -1020,12 +1027,16 @@ const apply = {
this.$message.warning('批量预约截止日期不能早于开始日期')
return false
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate > this.$moment(this.formData.reserveStartTime).endOf('month').format('YYYY-MM-DD')) {
this.$message.warning('批量预约截止日期不能超过预约开始时间所在月份')
return false
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('当前选择的时间段在截止日期内无法生成可批量预约的日期')
return false
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('当前选择的时间段无法生成本批量预约日期')
this.$message.warning('当前选择的时间段无法生成本批量预约日期')
return false
}
return true
@@ -1038,7 +1049,7 @@ const apply = {
if (!this.yearlyReserve) {
return baseText + ',是否确定提交预约?'
}
return baseText + '。系统将继续预约本后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,是否确定提交?'
return baseText + '。系统将继续预约本后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,是否确定提交?'
},
validateYearlyReserve() {
if (!this.yearlyReserve) {
@@ -1052,6 +1063,10 @@ const apply = {
this.$message.warning('批量预约截止日期不能早于开始日期')
return false
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate > this.$moment(this.formData.reserveStartTime).endOf('month').format('YYYY-MM-DD')) {
this.$message.warning('批量预约截止日期不能超过预约开始时间所在月份')
return false
}
if (!this.yearlyReserveDates.length) {
this.$message.warning('当前选择的时间段在截止日期内无法生成可批量预约的日期')
return false
@@ -288,7 +288,7 @@ layout("/layouts/platform.html"){
return
}
const message = this.formData.yearlyBatch
? ('该记录属于“预约本”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
? ('该记录属于“预约本”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
@@ -317,7 +317,7 @@ layout("/layouts/platform.html"){
return
}
const message = this.normalizeBatchFlag(row)
? ('该记录属于“预约本”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || row._batchCount || 0) + ' 条审核任务,是否继续?')
? ('该记录属于“预约本”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || row._batchCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?'
this.$confirm(message, '提示', {
confirmButtonText: '确定',
@@ -142,7 +142,10 @@ new Vue({
return base.startOf('day').toDate()
},
yearlyReserveMaxDate() {
return this.$moment().add(2, 'year').endOf('year').toDate()
const base = this.formData.reserveStartTime
? this.$moment(this.formData.reserveStartTime)
: (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment())
return base.endOf('month').toDate()
},
yearlyReserveDates() {
if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.formData.yearlyReserveEndDate) {
@@ -255,6 +258,10 @@ new Vue({
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
return
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate > defaultDate) {
this.$set(this.formData, 'yearlyReserveEndDate', defaultDate)
}
},
openReserveTypePicker() {
@@ -488,6 +495,20 @@ new Vue({
result.push('slot-block--closed')
return result
},
formatSlotLabel(block) {
const label = block && block.label ? block.label : ''
const match = label.match(/^(.+?)\s+(\d{2}:\d{2}(?::\d{2})?\s*-\s*\d{2}:\d{2}(?::\d{2})?)$/)
if (match) {
return {
name: match[1],
time: match[2],
}
}
return {
name: label,
time: '',
}
},
onAvailabilityBlockClick(block) {
if (!block || !block.key) {
return
@@ -847,6 +868,10 @@ new Vue({
this.$toast('批量预约截止日期不能早于开始日期')
return false
}
if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate > this.$moment(this.formData.reserveStartTime).endOf('month').format('YYYY-MM-DD')) {
this.$toast('批量预约截止日期不能超过预约开始时间所在月份')
return false
}
if (!this.yearlyReserveDates.length) {
this.$toast('当前截止日期内没有可批量预约的同星期时段')
return false
@@ -165,21 +165,30 @@ layout("/layouts/platform_h5.html"){
.availability-popup__tabs {
display: flex;
gap: 8px;
padding: 12px 16px 0;
padding: 12px 16px 8px;
overflow-x: auto;
background: #f8fafc;
scrollbar-width: none;
-ms-overflow-style: none;
}
.availability-popup__tabs::-webkit-scrollbar {
display: none;
}
.availability-tab {
flex: 0 0 auto;
min-width: 72px;
height: 34px;
padding: 0 14px;
min-height: 34px;
padding: 6px 14px;
border: 1px solid #cbd5e1;
border-radius: 999px;
background: #ffffff;
color: #475569;
font-size: 13px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
box-sizing: border-box;
}
@@ -244,12 +253,13 @@ layout("/layouts/platform_h5.html"){
.slot-block {
min-height: 48px;
padding: 0 12px;
padding: 8px 10px;
border: 1px solid #dbe4ee;
border-radius: 14px;
background: #ffffff;
color: #475569;
display: inline-flex;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
@@ -259,6 +269,21 @@ layout("/layouts/platform_h5.html"){
box-sizing: border-box;
}
.slot-block__name,
.slot-block__time {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.slot-block__time {
margin-top: 2px;
font-size: 12px;
line-height: 1.2;
}
.slot-block--available {
border-color: #86efac;
background: rgba(34, 197, 94, 0.1);
@@ -464,7 +489,7 @@ layout("/layouts/platform_h5.html"){
</template>
</van-cell>
<div v-if="yearlyReserve" class="batch-panel">
<div class="section-tip">将自动预约截止日期内每周同一时段,默认到本月底。</div>
<div class="section-tip">预约本月后续每周同一时段,默认到本月底。</div>
<van-field
:value="formData.yearlyReserveEndDate"
label="截至日期"
@@ -548,7 +573,8 @@ layout("/layouts/platform_h5.html"){
type="button"
:class="getBlockClass(block)"
@click="onAvailabilityBlockClick(block)">
<span>{{ block.label }}</span>
<span class="slot-block__name">{{ formatSlotLabel(block).name }}</span>
<span class="slot-block__time">{{ formatSlotLabel(block).time }}</span>
</button>
</div>
</div>
@@ -3,7 +3,54 @@ layout("/layouts/platform_h5.html"){
#-->
<style scoped>
.site-cug-card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.site-cug-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #eef6ff;
color: #0b75bd;
font-size: 12px;
}
.site-cug-tag.gray {
background: #f1f5f9;
color: #64748b;
}
.site-cug-tag.warn {
background: #fff3e5;
color: #d97706;
}
.site-cug-tag.info {
background: #eef2ff;
color: #2563eb;
}
.site-cug-tag.muted {
background: #f8fafc;
color: #94a3b8;
}
.site-cug-batch-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 22px;
padding: 0 8px;
border: 0;
border-radius: 4px;
background: #e0f2fe;
color: #0369a1;
font-size: 12px;
}
.site-cug-batch-child {
padding-left: 8px;
border-left: 3px solid #dbeafe;
}
</style>
<div id="app" v-cloak>
@@ -18,18 +65,35 @@ layout("/layouts/platform_h5.html"){
<table-list api="/platform/siteCug/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="siteName" @ready="onReady">
<template v-slot="{index,row}">
<div :class="row._isBatchChild ? 'site-cug-batch-child' : ''">
<div class="site-cug-card-tags">
<span v-if="row.applyLoginName" class="site-cug-tag">{{ row.applyLoginName }}</span>
<span class="site-cug-tag gray">{{ row.reserveType === 'club' ? '协会预约' : '分工会预约' }}</span>
<span v-if="row.yearlyBatch" class="site-cug-tag warn">预约本月</span>
<span v-if="row._isBatchChild" class="site-cug-tag muted">明细</span>
<span v-if="row._batchCount > 1 && !row._isBatchChild" class="site-cug-tag info">共 {{ row._batchCount }} 条</span>
<button
v-if="row._hasFoldChildren && !row._isBatchChild"
type="button"
class="site-cug-batch-toggle"
@click.stop="toggleBatchGroup(row)">
<i :class="row._expanded ? 'fa fa-angle-up' : 'fa fa-angle-down'"></i>
<span>{{ row._expanded ? '收起' : '展开' }}</span>
</button>
</div>
<table-column label="预约人">{{row.applyUserName || '-'}}</table-column>
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.taskName || '-'}}</table-column>
</div>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn delete" @click="onDelete(row)" v-if="row.canCancel">
<div class="action-btn delete" @click="onDelete(row)" v-if="!row._isBatchChild && row.canCancel">
<i class="fa fa-trash"></i>
<span>取消预约</span>
</div>
@@ -68,14 +132,113 @@ layout("/layouts/platform_h5.html"){
value: null,
}
],
expandedBatchKeys: {},
}
},
methods: {
async onReady() {
this.installTableListFoldHook()
await this.querySiteType()
await this.querySites()
this.doSearch()
},
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
return 'batch_' + (row.backOption || row.id) + '_' + (row.createdBy || '')
},
buildFoldedTableData(rows) {
const groups = {}
;(rows || []).forEach((row) => {
const key = this.getBatchGroupKey(row)
if (!groups[key]) {
groups[key] = []
}
groups[key].push(Object.assign({}, row))
})
const displayRows = []
Object.keys(groups).forEach((key) => {
const sortedRows = groups[key].slice().sort((a, b) => {
return (a.reserveStartTime || '').localeCompare(b.reserveStartTime || '')
})
const parent = Object.assign({}, sortedRows[0])
const children = sortedRows.slice(1).map((item) => {
return Object.assign({}, item, {
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
})
})
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = parent.batchDeleteCount || sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
children.forEach((child) => displayRows.push(child))
}
})
return displayRows
},
refreshFoldedTableData() {
const tableList = this.$refs.tableListRef
if (!tableList) {
return
}
tableList.tableData = this.buildFoldedTableData(tableList._rawTableData || [])
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.refreshFoldedTableData()
},
installTableListFoldHook() {
const tableList = this.$refs.tableListRef
if (!tableList || tableList._siteCugBatchFoldHooked) {
return
}
tableList._siteCugBatchFoldHooked = true
tableList._rawTableData = []
tableList.pageData = () => {
return this.loadFoldedMinePageData(tableList)
}
tableList.doSearch = () => {
tableList.tableFinished = false
tableList.tableData = []
tableList._rawTableData = []
tableList.localPageForm.pageNumber = 1
tableList.$emit('update:page_form', Object.assign({}, tableList.localPageForm))
return this.loadFoldedMinePageData(tableList)
}
},
loadFoldedMinePageData(tableList) {
tableList.tableLoading = true
const loading = createListLoading()
return this.$axios.post(tableList.api, tableList.json ? { pageForm: JSON.stringify(tableList.localPageForm) } : tableList.localPageForm).then((res) => {
if (res.code === 0) {
const list = res.data && Array.isArray(res.data.list) ? res.data.list : []
tableList._rawTableData = (tableList._rawTableData || []).concat(list)
tableList.tableData = this.buildFoldedTableData(tableList._rawTableData)
tableList.localPageForm.totalCount = res.data ? res.data.totalCount : 0
if (tableList._rawTableData.length >= tableList.localPageForm.totalCount) {
tableList.tableFinished = true
}
tableList.$emit('update:page_form', Object.assign({}, tableList.localPageForm))
}
}).finally(() => {
loading.close()
tableList.tableLoading = false
tableList.tableRefreshing = false
})
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
@@ -83,7 +246,7 @@ layout("/layouts/platform_h5.html"){
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || 0) + ' 条预约记录,是否继续?')
? ('该记录属于“预约本”批量预约,确认后将一并删除同批次的 ' + (row.batchDeleteCount || row._batchCount || 0) + ' 条预约记录,是否继续?')
: '您确定要取消该预约吗?',
}).then(() => {
this.$axios.post('/platform/siteCug/mine/delete', { id: row.id }).then((res) => {
@@ -134,6 +297,7 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.$nextTick(() => {
this.expandedBatchKeys = {}
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
@@ -145,4 +309,4 @@ layout("/layouts/platform_h5.html"){
<!--#
}
#-->
#-->
@@ -52,6 +52,30 @@ layout("/layouts/platform_h5.html"){
background: #fff3e5;
color: #d97706;
}
.site-cug-tag.info {
background: #eef2ff;
color: #2563eb;
}
.site-cug-tag.muted {
background: #f8fafc;
color: #94a3b8;
}
.site-cug-batch-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 22px;
padding: 0 8px;
border: 0;
border-radius: 4px;
background: #e0f2fe;
color: #0369a1;
font-size: 12px;
}
.site-cug-batch-child {
padding-left: 8px;
border-left: 3px solid #dbeafe;
}
.action-btn.disabled {
opacity: 0.45;
pointer-events: none;
@@ -94,10 +118,21 @@ layout("/layouts/platform_h5.html"){
<div class="site-cug-audit-list-scroll">
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
<template v-slot="{index,row}">
<div :class="row._isBatchChild ? 'site-cug-batch-child' : ''">
<div class="site-cug-card-tags">
<span v-if="row.applyLoginName" class="site-cug-tag">{{ row.applyLoginName }}</span>
<span class="site-cug-tag gray">{{ reserveTypeText(row) }}</span>
<span v-if="row.yearlyBatch" class="site-cug-tag warn">预约本</span>
<span v-if="row.yearlyBatch" class="site-cug-tag warn">预约本</span>
<span v-if="row._isBatchChild" class="site-cug-tag muted">明细</span>
<span v-if="row._batchCount > 1 && !row._isBatchChild" class="site-cug-tag info">共 {{ row._batchCount }} 条</span>
<button
v-if="row._hasFoldChildren && !row._isBatchChild"
type="button"
class="site-cug-batch-toggle"
@click.stop="toggleBatchGroup(row)">
<i :class="row._expanded ? 'fa fa-angle-up' : 'fa fa-angle-down'"></i>
<span>{{ row._expanded ? '收起' : '展开' }}</span>
</button>
</div>
<table-column label="场地名称">{{row.siteName}}</table-column>
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
@@ -105,17 +140,18 @@ layout("/layouts/platform_h5.html"){
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '-'}}</table-column>
</div>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)">
<div class="action-btn" v-if="!row._isBatchChild && row.taskState === 10" @click="onAudit(row)">
<i class="fa fa-edit"></i>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<div class="action-btn delete" v-if="!row._isBatchChild && canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
<i v-else class="fa fa-reply"></i>
<span>撤回</span>
@@ -184,11 +220,13 @@ layout("/layouts/platform_h5.html"){
showApprovalForm: false,
auditLoading: false,
revokeLoading: false,
expandedBatchKeys: {},
}
},
methods: {
historyBack,
async onReady() {
this.installTableListFoldHook()
await this.querySiteType()
await this.querySites()
this.doSearch()
@@ -203,6 +241,103 @@ layout("/layouts/platform_h5.html"){
reserveTypeText(row) {
return row && row.reserveType === 'club' ? '协会预约' : '分工会预约'
},
normalizeBatchFlag(row) {
return row && (row.yearlyBatch === 1 || row.yearlyBatch === '1' || row.yearlyBatch === true)
},
getBatchGroupKey(row) {
if (!this.normalizeBatchFlag(row)) {
return 'single_' + row.id
}
return 'batch_' + (row.backOption || row.id) + '_' + (row.createdBy || '')
},
buildFoldedTableData(rows) {
const groups = {}
;(rows || []).forEach((row) => {
const key = this.getBatchGroupKey(row)
if (!groups[key]) {
groups[key] = []
}
groups[key].push(Object.assign({}, row))
})
const displayRows = []
Object.keys(groups).forEach((key) => {
const sortedRows = groups[key].slice().sort((a, b) => {
return (a.reserveStartTime || '').localeCompare(b.reserveStartTime || '')
})
const parent = Object.assign({}, sortedRows[0])
const children = sortedRows.slice(1).map((item) => {
return Object.assign({}, item, {
_isBatchChild: true,
_groupKey: key,
_hasFoldChildren: false,
})
})
parent._groupKey = key
parent._isBatchChild = false
parent._hasFoldChildren = children.length > 0
parent._expanded = !!this.expandedBatchKeys[key]
parent._batchCount = parent.batchAuditCount || sortedRows.length
parent._foldedRecords = children
displayRows.push(parent)
if (parent._expanded) {
children.forEach((child) => displayRows.push(child))
}
})
return displayRows
},
refreshFoldedTableData() {
const tableList = this.$refs.tableListRef
if (!tableList) {
return
}
tableList.tableData = this.buildFoldedTableData(tableList._rawTableData || [])
},
toggleBatchGroup(row) {
if (!row || !row._groupKey || !row._hasFoldChildren) {
return
}
this.$set(this.expandedBatchKeys, row._groupKey, !this.expandedBatchKeys[row._groupKey])
this.refreshFoldedTableData()
},
installTableListFoldHook() {
const tableList = this.$refs.tableListRef
if (!tableList || tableList._siteCugBatchFoldHooked) {
return
}
tableList._siteCugBatchFoldHooked = true
tableList._rawTableData = []
tableList.pageData = () => {
return this.loadFoldedAuditPageData(tableList)
}
tableList.doSearch = () => {
tableList.tableFinished = false
tableList.tableData = []
tableList._rawTableData = []
tableList.localPageForm.pageNumber = 1
tableList.$emit('update:page_form', Object.assign({}, tableList.localPageForm))
return this.loadFoldedAuditPageData(tableList)
}
},
loadFoldedAuditPageData(tableList) {
tableList.tableLoading = true
const loading = createListLoading()
return this.$axios.post(tableList.api, tableList.json ? { pageForm: JSON.stringify(tableList.localPageForm) } : tableList.localPageForm).then((res) => {
if (res.code === 0) {
const list = res.data && Array.isArray(res.data.list) ? res.data.list : []
tableList._rawTableData = (tableList._rawTableData || []).concat(list)
tableList.tableData = this.buildFoldedTableData(tableList._rawTableData)
tableList.localPageForm.totalCount = res.data ? res.data.totalCount : 0
if (tableList._rawTableData.length >= tableList.localPageForm.totalCount) {
tableList.tableFinished = true
}
tableList.$emit('update:page_form', Object.assign({}, tableList.localPageForm))
}
}).finally(() => {
loading.close()
tableList.tableLoading = false
tableList.tableRefreshing = false
})
},
onView(row) {
this.showApprovalForm = false
this.$refs.infoRef.onOpen(row)
@@ -228,7 +363,7 @@ layout("/layouts/platform_h5.html"){
this.$dialog.confirm({
title: '提示',
message: this.formData.yearlyBatch
? ('该记录属于“预约本”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
? ('该记录属于“预约本”批量预约,提交后将一并审核同批次的 ' + (this.formData.batchAuditCount || 0) + ' 条预约记录,是否继续?')
: '您确定要提交吗?',
}).then(() => {
this.auditLoading = true
@@ -267,7 +402,7 @@ layout("/layouts/platform_h5.html"){
this.$dialog.confirm({
title: '提示',
message: row.yearlyBatch
? ('该记录属于“预约本”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
? ('该记录属于“预约本”批量预约,撤回后将一并撤回同批次的 ' + (row.batchAuditCount || 0) + ' 条审核任务,是否继续?')
: '您确定要撤回吗?',
}).then(() => {
this.revokeLoading = true
@@ -321,6 +456,7 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.$nextTick(() => {
this.expandedBatchKeys = {}
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()