Compare commits
56
Commits
ae37d48657
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f51b24686f | ||
|
|
3f7b5dc27a | ||
|
|
e944ebf85f | ||
|
|
a0732fdb08 | ||
|
|
db26813635 | ||
|
|
d5f1c642c4 | ||
|
|
a09cd7b8fd | ||
|
|
d18d9bda6d | ||
|
|
919f8c9dec | ||
|
|
0fb779ff5a | ||
|
|
55e94de004 | ||
|
|
d452d4ad92 | ||
|
|
6ea864cb64 | ||
|
|
56cc3fee91 | ||
|
|
4c03b389d4 | ||
|
|
fcb6ba7e2d | ||
|
|
65fac78cfb | ||
|
|
2d20959355 | ||
|
|
1a1946b1f8 | ||
|
|
7f2ac97dea | ||
|
|
82713327db | ||
|
|
d598f1bf0f | ||
|
|
01e2443db8 | ||
|
|
bb4c44443d | ||
|
|
15d3ec0ddd | ||
|
|
713e174650 | ||
|
|
6e351c6829 | ||
|
|
5d160f660f | ||
|
|
57d84724cb | ||
|
|
018ee96402 | ||
|
|
98c7804c28 | ||
|
|
e257f4bae2 | ||
|
|
054cf966c3 | ||
|
|
c497c0c0cc | ||
|
|
fa9ff1e9d5 | ||
|
|
19a2a6d63a | ||
|
|
33818d0972 | ||
|
|
0a93a71346 | ||
|
|
10b9a5ba95 | ||
|
|
b19996f57e | ||
|
|
88d624172c | ||
|
|
6b5d27c68d | ||
|
|
7c5bc65fa0 | ||
|
|
fbdf38389d | ||
|
|
48329f933b | ||
|
|
c262c8e715 | ||
|
|
17369c90e6 | ||
|
|
426c389d5b | ||
|
|
24f7267fbc | ||
|
|
b4d6c4bfa3 | ||
|
|
82cbea973f | ||
|
|
d039559f47 | ||
|
|
123a73e5ca | ||
|
|
1650383670 | ||
|
|
20bac2e078 | ||
|
|
4cd992fb28 |
@@ -49,3 +49,4 @@ Thumbs.db
|
|||||||
.trae/pr-creator/SKILL.md
|
.trae/pr-creator/SKILL.md
|
||||||
.trae/update-docs/SKILL.md
|
.trae/update-docs/SKILL.md
|
||||||
.trae/webapp-testing/SKILL.md
|
.trae/webapp-testing/SKILL.md
|
||||||
|
outputs
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.budwk</groupId>
|
<groupId>com.budwk</groupId>
|
||||||
<artifactId>zhgh_zjnu</artifactId>
|
<artifactId>mini</artifactId>
|
||||||
<version>5.6.0-plus</version>
|
<version>5.6.0-plus</version>
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<groupId>com.budwk</groupId>
|
<groupId>com.budwk</groupId>
|
||||||
<artifactId>zhgh_zjnu</artifactId>
|
<artifactId>mini</artifactId>
|
||||||
<version>5.6.0-plus</version>
|
<version>5.6.0-plus</version>
|
||||||
<properties>
|
<properties>
|
||||||
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
|
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import com.budwk.app.web.commons.base.Globals;
|
|||||||
import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
|
import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
|
||||||
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
|
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
|
||||||
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
|
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.beetl.core.GroupTemplate;
|
import org.beetl.core.GroupTemplate;
|
||||||
import org.nutz.boot.NbApp;
|
import org.nutz.boot.NbApp;
|
||||||
@@ -81,6 +83,16 @@ public class MainLauncher {
|
|||||||
Globals.AppBase = Mvcs.getServletContext().getContextPath();
|
Globals.AppBase = Mvcs.getServletContext().getContextPath();
|
||||||
Globals.AppRoot = Mvcs.getServletContext().getRealPath("/");
|
Globals.AppRoot = Mvcs.getServletContext().getRealPath("/");
|
||||||
init_sys();
|
init_sys();
|
||||||
|
ioc.get(com.budwk.app.zhgh.staffbenefit.therapyRecuperation.TherapyRecuperationModule.class);
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:先升级会员表及补齐历史资料,再处理缴费快照和启动计划任务;不依赖 DEBUG。
|
||||||
|
SysClubUserService members = ioc.get(SysClubUserService.class, "sysClubUserServiceImpl");
|
||||||
|
members.upgradeMemberSchema();
|
||||||
|
members.backfillMemberSnapshots();
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:缴费表升级独立于 DEBUG,先完成字段检查和旧数据补齐再启动任务。
|
||||||
|
// 2026年10月3日 09:28:37;操作人:wxg:按实际注册的实现类 Bean 名称获取服务,避免接口默认名称不存在导致启动失败。
|
||||||
|
ClubPayRecordService payRecords = ioc.get(ClubPayRecordService.class, "clubPayRecordServiceImpl");
|
||||||
|
payRecords.upgradePayRecordSchema();
|
||||||
|
payRecords.backfillPayRecordSnapshots();
|
||||||
init_task();
|
init_task();
|
||||||
init_auth();
|
init_auth();
|
||||||
ioc.get(Globals.class);
|
ioc.get(Globals.class);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class DataCenterProperties {
|
|||||||
@Inject
|
@Inject
|
||||||
private PropertiesProxy conf;
|
private PropertiesProxy conf;
|
||||||
|
|
||||||
|
private boolean tokenEnabled;
|
||||||
private String tokenUrl;
|
private String tokenUrl;
|
||||||
private String key;
|
private String key;
|
||||||
private String secret;
|
private String secret;
|
||||||
@@ -31,6 +32,7 @@ public class DataCenterProperties {
|
|||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
Map<String, String> all = conf.toMap();
|
Map<String, String> all = conf.toMap();
|
||||||
|
tokenEnabled = conf.getBoolean("data-center.token-enabled", true);
|
||||||
tokenUrl = all.get("data-center.token-url");
|
tokenUrl = all.get("data-center.token-url");
|
||||||
key = all.get("data-center.key");
|
key = all.get("data-center.key");
|
||||||
secret = all.get("data-center.secret");
|
secret = all.get("data-center.secret");
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ public class RedisConstant {
|
|||||||
*/
|
*/
|
||||||
public static final String UCENTER_SMSCODE = PLATFORM_REDIS_PREFIX + "ucenter:smscode:";
|
public static final String UCENTER_SMSCODE = PLATFORM_REDIS_PREFIX + "ucenter:smscode:";
|
||||||
|
|
||||||
|
/** 拼图挑战与凭证按匿名会话隔离,频率键单独存放。 */
|
||||||
|
public static final String REDIS_SLIDER_CAPTCHA_KEY = PLATFORM_REDIS_PREFIX + "login:slider:";
|
||||||
|
public static final String REDIS_SLIDER_RATE_KEY = PLATFORM_REDIS_PREFIX + "login:slider-rate:";
|
||||||
|
|
||||||
|
/** 登录短信专用缓存前缀,避免验证码与其他业务场景交叉使用。 */
|
||||||
|
public static final String LOGIN_SMSCODE = PLATFORM_REDIS_PREFIX + "login:sms:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接口重复提交前缀
|
* 接口重复提交前缀
|
||||||
*/
|
*/
|
||||||
@@ -72,4 +79,10 @@ public class RedisConstant {
|
|||||||
* 数据接口锁前缀
|
* 数据接口锁前缀
|
||||||
*/
|
*/
|
||||||
public static final String RSA_KEY_PREFIX = PLATFORM_REDIS_PREFIX + "rsa:private:";
|
public static final String RSA_KEY_PREFIX = PLATFORM_REDIS_PREFIX + "rsa:private:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人员治理第三方数据查询结果缓存前缀。
|
||||||
|
*/
|
||||||
|
public static final String REDIS_KEY_PERSON_API_DATA_QUERY_RESULT =
|
||||||
|
PLATFORM_REDIS_PREFIX + "personGovernance:dataQuery:";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ RoleConstant {
|
|||||||
SCHOOL_UNION_PROPOSAL_ADMIN("校工会提案管理员"),
|
SCHOOL_UNION_PROPOSAL_ADMIN("校工会提案管理员"),
|
||||||
SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
|
SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
|
||||||
SCHOOL_UNION_ACTIVITY_ADMIN("校工会活动管理员"),
|
SCHOOL_UNION_ACTIVITY_ADMIN("校工会活动管理员"),
|
||||||
|
/** 品牌活动统计、人员调整管理员,数据范围按 trainSignUp 子活动负责人关系控制。 */
|
||||||
|
SCHOOL_UNION_TRAIN_SIGN_UP_ADMIN("校工会品牌活动管理员"),
|
||||||
SCHOOL_UNION_ARTICLE_CLUB_ADMIN("校工会社团新闻审批管理员"),
|
SCHOOL_UNION_ARTICLE_CLUB_ADMIN("校工会社团新闻审批管理员"),
|
||||||
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
|
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
|
||||||
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
|
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
|
||||||
@@ -48,6 +50,11 @@ RoleConstant {
|
|||||||
BRANCH_UNION_SHENGGHUO_WY("分工会生活委员"),
|
BRANCH_UNION_SHENGGHUO_WY("分工会生活委员"),
|
||||||
BRANCH_UNION_TIAOJIE_WY("分工会调解委员"),
|
BRANCH_UNION_TIAOJIE_WY("分工会调解委员"),
|
||||||
BRANCH_UNION_WENTI_SPORTS("分工会文体福利委员"),
|
BRANCH_UNION_WENTI_SPORTS("分工会文体福利委员"),
|
||||||
|
BRANCH_UNION_YOUTH_WORK_COMMITTEE("分工会青工委委员"),
|
||||||
|
BRANCH_UNION_WOMEN_WORK_COMMITTEE("分工会女工委委员"),
|
||||||
|
BRANCH_UNION_WELFARE_COMMITTEE("分工会福利委员"),
|
||||||
|
BRANCH_UNION_SPORTS_COMMITTEE("分工会文体委员"),
|
||||||
|
BRANCH_UNION_PUBLICITY_COMMITTEE("分工会宣传委员"),
|
||||||
|
|
||||||
UNIT_PARTY_SECRETARY("单位党委书记"),
|
UNIT_PARTY_SECRETARY("单位党委书记"),
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public enum LoginType {
|
|||||||
QI_YE_WECHAT("企业微信"),
|
QI_YE_WECHAT("企业微信"),
|
||||||
WECHAT("微信"),
|
WECHAT("微信"),
|
||||||
PC_LOCAL("PC本地"),
|
PC_LOCAL("PC本地"),
|
||||||
|
SMS_CODE("手机验证码"),
|
||||||
MOBILE_LOCAL("手机本地"),
|
MOBILE_LOCAL("手机本地"),
|
||||||
CAS("CAS");
|
CAS("CAS");
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ public class SmsServiceImpl implements SmsService {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private PropertiesProxy conf;
|
private PropertiesProxy conf;
|
||||||
|
@Inject
|
||||||
|
private ZjnuMsgService zjnuMsgService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送短信消息。
|
* 发送短信消息。
|
||||||
@@ -59,6 +61,9 @@ public class SmsServiceImpl implements SmsService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
|
public MsgPlatformResponse sendSms(MsgPlatformSmsRequest request) {
|
||||||
|
if (Strings.isNotBlank(conf.get("custom.msg.platform.app-id"))) {
|
||||||
|
return zjnuMsgService.send(request, conf.getBoolean("msg.platform.IntelligenceMode", false) ? "IntelligenceMode" : "SMS");
|
||||||
|
}
|
||||||
validateCommonRequest(request);
|
validateCommonRequest(request);
|
||||||
JSONObject payload = buildCommonPayload(request, true);
|
JSONObject payload = buildCommonPayload(request, true);
|
||||||
return doPost(SMS_API_PATH, payload);
|
return doPost(SMS_API_PATH, payload);
|
||||||
@@ -107,6 +112,9 @@ public class SmsServiceImpl implements SmsService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request) {
|
public MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request) {
|
||||||
|
if (Strings.isNotBlank(conf.get("custom.msg.platform.app-id"))) {
|
||||||
|
return zjnuMsgService.send(request, "WeChat");
|
||||||
|
}
|
||||||
validateCommonRequest(request);
|
validateCommonRequest(request);
|
||||||
if (Strings.isBlank(request.getWechatType())) {
|
if (Strings.isBlank(request.getWechatType())) {
|
||||||
request.setWechatType("text");
|
request.setWechatType("text");
|
||||||
@@ -342,6 +350,9 @@ public class SmsServiceImpl implements SmsService {
|
|||||||
* @param request 平台请求参数
|
* @param request 平台请求参数
|
||||||
*/
|
*/
|
||||||
private void fillDefaultSenderInfo(MsgPlatformBaseRequest request) {
|
private void fillDefaultSenderInfo(MsgPlatformBaseRequest request) {
|
||||||
|
if (Strings.isNotBlank(conf.get("custom.msg.platform.app-id"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
request.setSendUserId(getRequiredConfig("msg.platform.default-send-user-id", "请先配置 msg.platform.default-send-user-id"));
|
request.setSendUserId(getRequiredConfig("msg.platform.default-send-user-id", "请先配置 msg.platform.default-send-user-id"));
|
||||||
request.setSendUserName(getRequiredConfig("msg.platform.default-send-user-name", "请先配置 msg.platform.default-send-user-name"));
|
request.setSendUserName(getRequiredConfig("msg.platform.default-send-user-name", "请先配置 msg.platform.default-send-user-name"));
|
||||||
request.setSendUnitId(conf.get("msg.platform.default-send-unit-id", ""));
|
request.setSendUnitId(conf.get("msg.platform.default-send-unit-id", ""));
|
||||||
@@ -351,7 +362,7 @@ public class SmsServiceImpl implements SmsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用地大通讯平台 REST 接口。
|
* 调用浙江师范大学通讯平台 REST 接口。
|
||||||
*
|
*
|
||||||
* @param apiPath 具体接口路径
|
* @param apiPath 具体接口路径
|
||||||
* @param payload 发送报文
|
* @param payload 发送报文
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package com.budwk.app.base.sms.impl;
|
||||||
|
|
||||||
|
import cn.hutool.http.HttpRequest;
|
||||||
|
import cn.hutool.http.HttpResponse;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.base.sms.model.*;
|
||||||
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
|
import com.budwk.app.web.commons.base.Globals;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
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;
|
||||||
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.log.Log;
|
||||||
|
import org.nutz.log.Logs;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
public class ZjnuMsgService {
|
||||||
|
private static final Log log = Logs.get();
|
||||||
|
@Inject
|
||||||
|
private PropertiesProxy conf;
|
||||||
|
@Inject
|
||||||
|
private RedisService redisService;
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
|
||||||
|
public MsgPlatformResponse send(MsgPlatformBaseRequest request, String channel) {
|
||||||
|
MsgPlatformResponse result = new MsgPlatformResponse();
|
||||||
|
if (!conf.getBoolean("msg.platform.enabled", false)
|
||||||
|
|| !Globals.MyConfig.getBoolean("SendMsg", Globals.MyConfig.getBoolean("AppSms", false))) {
|
||||||
|
result.setCode(0);
|
||||||
|
result.setResult(true);
|
||||||
|
result.setMsg("通讯平台未启用,已跳过实际发送");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (request == null || Strings.isBlank(request.getInfo())) {
|
||||||
|
throw new BaseException("浙师大消息内容不能为空");
|
||||||
|
}
|
||||||
|
List<MsgPlatformRecipient> recipients = request.getRecipients();
|
||||||
|
if (Strings.isNotBlank(request.getPersonInfo())) {
|
||||||
|
recipients = new ArrayList<>();
|
||||||
|
for (String line : request.getPersonInfo().split("\\^@\\^")) {
|
||||||
|
String[] fields = line.split("\\|", -1);
|
||||||
|
MsgPlatformRecipient recipient = new MsgPlatformRecipient();
|
||||||
|
recipient.setName(fields[0]);
|
||||||
|
recipient.setAccount(fields.length > 1 ? fields[1] : "");
|
||||||
|
recipient.setMobile(fields.length > 4 ? fields[4] : "");
|
||||||
|
recipients.add(recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (recipients == null || recipients.isEmpty()) {
|
||||||
|
throw new BaseException("接收人列表不能为空");
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> receivers = new ArrayList<>();
|
||||||
|
for (MsgPlatformRecipient recipient : recipients) {
|
||||||
|
if (recipient == null) {
|
||||||
|
throw new BaseException("接收人信息不能为空");
|
||||||
|
}
|
||||||
|
String account = recipient.getAccount();
|
||||||
|
String mobile = recipient.getMobile();
|
||||||
|
if (Strings.isBlank(mobile) && Strings.isNotBlank(account) && account.matches("^\\d{11}$")) {
|
||||||
|
mobile = account;
|
||||||
|
account = null;
|
||||||
|
}
|
||||||
|
if (Strings.isBlank(account) && Strings.isNotBlank(mobile)) {
|
||||||
|
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where("mobile", "=", mobile));
|
||||||
|
if (users.size() != 1) {
|
||||||
|
throw new BaseException("无法通过手机号唯一确定浙师大消息接收人工号");
|
||||||
|
}
|
||||||
|
account = users.get(0).getLoginname();
|
||||||
|
}
|
||||||
|
if (Strings.isBlank(account)) {
|
||||||
|
throw new BaseException("浙师大消息接收人工号不能为空");
|
||||||
|
}
|
||||||
|
receivers.add(Map.of("type", "User", "userId", account, "name", Strings.sNull(recipient.getName())));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("token", getToken());
|
||||||
|
payload.put("msgtitle", Globals.AppName);
|
||||||
|
payload.put("msgContent", request.getInfo().replace("<", "<").replace(">", ">"));
|
||||||
|
payload.put("msgType", "0");
|
||||||
|
payload.put("sendStatus", "1");
|
||||||
|
payload.put("sendChannel", channel);
|
||||||
|
payload.put("sendMode", "normal");
|
||||||
|
payload.put("receivers", JSONUtil.toJsonStr(receivers));
|
||||||
|
payload.put("summary", "");
|
||||||
|
payload.put("sendTime", Strings.sNull(request.getSendTime()));
|
||||||
|
payload.put("isReply", "0");
|
||||||
|
payload.put("sendMsgType", "1");
|
||||||
|
try (HttpResponse response = HttpRequest.post(baseUrl() + "/message/sendMessageApi")
|
||||||
|
.form(payload).timeout(conf.getInt("msg.platform.read-timeout", 10000)).execute()) {
|
||||||
|
JSONObject body = JSONUtil.parseObj(response.body());
|
||||||
|
Integer code = body.getInt("rspCode");
|
||||||
|
result.setHttpStatus(response.getStatus());
|
||||||
|
result.setCode(code);
|
||||||
|
result.setResult(response.getStatus() == 200 && Integer.valueOf(0).equals(code));
|
||||||
|
result.setMsg(body.getStr("rspMsg"));
|
||||||
|
if (!Boolean.TRUE.equals(result.getResult())) {
|
||||||
|
log.warn("浙师大消息发送失败,状态码:" + response.getStatus() + ",业务码:" + code);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("浙师大消息接口调用失败:" + e.getClass().getSimpleName());
|
||||||
|
result.setCode(-1);
|
||||||
|
result.setResult(false);
|
||||||
|
result.setMsg("浙师大消息接口调用失败");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getToken() {
|
||||||
|
String appId = conf.get("custom.msg.platform.app-id");
|
||||||
|
String cacheKey = "zjnu:msg:token:" + appId;
|
||||||
|
String token = redisService.get(cacheKey);
|
||||||
|
if (Strings.isNotBlank(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
try (HttpResponse response = HttpRequest.get(baseUrl() + "/msg/getThirdAPIToken")
|
||||||
|
.form("appId", appId, "appPassword", conf.get("custom.msg.platform.app-password"))
|
||||||
|
.timeout(conf.getInt("msg.platform.read-timeout", 10000)).execute()) {
|
||||||
|
JSONObject body = JSONUtil.parseObj(response.body());
|
||||||
|
JSONObject data = body.getJSONObject("resultData");
|
||||||
|
token = data == null ? null : data.getStr("token");
|
||||||
|
if (response.getStatus() != 200 || Strings.isBlank(token)) {
|
||||||
|
throw new BaseException("获取浙师大消息平台token失败");
|
||||||
|
}
|
||||||
|
redisService.setex(cacheKey, 1200, token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String baseUrl() {
|
||||||
|
return conf.get("msg.platform.base-url").replaceAll("/+$", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,6 +243,27 @@ public class FlowDesignController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置流程设计在流程中心的展示状态。
|
||||||
|
* @param id 流程设计 ID
|
||||||
|
* @param centerEnabled 传 1 表示显示,传 0 表示隐藏
|
||||||
|
* @return Result;code 为 0 表示保存成功,非 0 时 msg 说明错误,data 无业务数据
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@ApiOperation("设置流程中心显示状态")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@SaCheckPermission("flow.design")
|
||||||
|
public Result updateCenterEnabled(@Param("id") Long id, @Param("centerEnabled") Integer centerEnabled) {
|
||||||
|
if (id == null || centerEnabled == null || (centerEnabled != 0 && centerEnabled != 1)) {
|
||||||
|
return Result.error("流程设计或显示状态无效");
|
||||||
|
}
|
||||||
|
int updated = processDesignService.update(Chain.make("centerEnabled", centerEnabled), Cnd.where(ProcessDesign::getId, "=", id));
|
||||||
|
if (updated == 0 && processDesignService.fetch(id) == null) {
|
||||||
|
return Result.error("流程设计不存在");
|
||||||
|
}
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@ApiOperation("获取流程设计任务参与者处理类")
|
@ApiOperation("获取流程设计任务参与者处理类")
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public class ProcessDesign extends BaseModel {
|
|||||||
@Column
|
@Column
|
||||||
private Integer isDeployed;
|
private Integer isDeployed;
|
||||||
|
|
||||||
|
@Comment("是否在流程中心显示(1显示,0隐藏)")
|
||||||
|
@Column
|
||||||
|
@Default("1")
|
||||||
|
private Integer centerEnabled;
|
||||||
|
|
||||||
@Comment("排序编码")
|
@Comment("排序编码")
|
||||||
@Column
|
@Column
|
||||||
@Default("0")
|
@Default("0")
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package com.budwk.app.flow.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||||
|
import com.budwk.app.flow.engine.core.Execution;
|
||||||
|
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||||
|
import com.budwk.app.flow.engine.model.TaskModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlowBranchUnionPublicityCommitteeHandler implements AssignmentHandler {
|
||||||
|
@Override
|
||||||
|
public List<String> assign(TaskModel model, Execution execution) {
|
||||||
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
|
||||||
|
Sys_role role = sysRoleService.getByCode(RoleConstant.BRANCH_UNION_PUBLICITY_COMMITTEE);
|
||||||
|
if (role == null || role.isDisabled()) {
|
||||||
|
throw new BaseException("分工会宣传委员角色未配置或已禁用,请联系校工会!");
|
||||||
|
}
|
||||||
|
String unionId = StrUtil.blankToDefault(execution.getArgs().getStr(FlowConst.INITIATOR_UNIT_UNION_ID), SecurityUtil.getUnionId());
|
||||||
|
if (StrUtil.isBlank(unionId)) {
|
||||||
|
throw new BaseException("无法确定所属分工会,请联系校工会!");
|
||||||
|
}
|
||||||
|
List<Sys_user_role> relations = dao.query(Sys_user_role.class,
|
||||||
|
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
|
||||||
|
.and(Sys_user_role::getUnionId, "=", unionId)
|
||||||
|
.and(Sys_user_role::getEnable, "=", true));
|
||||||
|
List<String> userIds = relations.stream().map(Sys_user_role::getUserId)
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
throw new BaseException("分工会宣传委员没有设置,请联系校工会!");
|
||||||
|
}
|
||||||
|
return userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "流程发起人所属分工会宣传委员";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.budwk.app.flow.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||||
|
import com.budwk.app.flow.engine.core.Execution;
|
||||||
|
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||||
|
import com.budwk.app.flow.engine.model.TaskModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlowBranchUnionSportsCommitteeHandler implements AssignmentHandler {
|
||||||
|
@Override
|
||||||
|
public List<String> assign(TaskModel model, Execution execution) {
|
||||||
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
|
||||||
|
Sys_role role = sysRoleService.getByCode(RoleConstant.BRANCH_UNION_SPORTS_COMMITTEE);
|
||||||
|
if (role == null || role.isDisabled()) {
|
||||||
|
throw new BaseException("分工会文体委员角色未配置或已禁用,请联系校工会!");
|
||||||
|
}
|
||||||
|
String unionId = StrUtil.blankToDefault(execution.getArgs().getStr(FlowConst.INITIATOR_UNIT_UNION_ID), SecurityUtil.getUnionId());
|
||||||
|
if (StrUtil.isBlank(unionId)) {
|
||||||
|
throw new BaseException("无法确定所属分工会,请联系校工会!");
|
||||||
|
}
|
||||||
|
List<Sys_user_role> relations = dao.query(Sys_user_role.class,
|
||||||
|
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
|
||||||
|
.and(Sys_user_role::getUnionId, "=", unionId)
|
||||||
|
.and(Sys_user_role::getEnable, "=", true));
|
||||||
|
List<String> userIds = relations.stream().map(Sys_user_role::getUserId)
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
throw new BaseException("分工会文体委员没有设置,请联系校工会!");
|
||||||
|
}
|
||||||
|
return userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "流程发起人所属分工会文体委员";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.budwk.app.flow.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||||
|
import com.budwk.app.flow.engine.core.Execution;
|
||||||
|
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||||
|
import com.budwk.app.flow.engine.model.TaskModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlowBranchUnionWelfareCommitteeHandler implements AssignmentHandler {
|
||||||
|
@Override
|
||||||
|
public List<String> assign(TaskModel model, Execution execution) {
|
||||||
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
|
||||||
|
Sys_role role = sysRoleService.getByCode(RoleConstant.BRANCH_UNION_WELFARE_COMMITTEE);
|
||||||
|
if (role == null || role.isDisabled()) {
|
||||||
|
throw new BaseException("分工会福利委员角色未配置或已禁用,请联系校工会!");
|
||||||
|
}
|
||||||
|
String unionId = StrUtil.blankToDefault(execution.getArgs().getStr(FlowConst.INITIATOR_UNIT_UNION_ID), SecurityUtil.getUnionId());
|
||||||
|
if (StrUtil.isBlank(unionId)) {
|
||||||
|
throw new BaseException("无法确定所属分工会,请联系校工会!");
|
||||||
|
}
|
||||||
|
List<Sys_user_role> relations = dao.query(Sys_user_role.class,
|
||||||
|
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
|
||||||
|
.and(Sys_user_role::getUnionId, "=", unionId)
|
||||||
|
.and(Sys_user_role::getEnable, "=", true));
|
||||||
|
List<String> userIds = relations.stream().map(Sys_user_role::getUserId)
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
throw new BaseException("分工会福利委员没有设置,请联系校工会!");
|
||||||
|
}
|
||||||
|
return userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "流程发起人所属分工会福利委员";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 13;
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package com.budwk.app.flow.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||||
|
import com.budwk.app.flow.engine.core.Execution;
|
||||||
|
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||||
|
import com.budwk.app.flow.engine.model.TaskModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlowBranchUnionWomenWorkCommitteeHandler implements AssignmentHandler {
|
||||||
|
@Override
|
||||||
|
public List<String> assign(TaskModel model, Execution execution) {
|
||||||
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
|
||||||
|
Sys_role role = sysRoleService.getByCode(RoleConstant.BRANCH_UNION_WOMEN_WORK_COMMITTEE);
|
||||||
|
if (role == null || role.isDisabled()) {
|
||||||
|
throw new BaseException("分工会女工委委员角色未配置或已禁用,请联系校工会!");
|
||||||
|
}
|
||||||
|
String unionId = StrUtil.blankToDefault(execution.getArgs().getStr(FlowConst.INITIATOR_UNIT_UNION_ID), SecurityUtil.getUnionId());
|
||||||
|
if (StrUtil.isBlank(unionId)) {
|
||||||
|
throw new BaseException("无法确定所属分工会,请联系校工会!");
|
||||||
|
}
|
||||||
|
List<Sys_user_role> relations = dao.query(Sys_user_role.class,
|
||||||
|
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
|
||||||
|
.and(Sys_user_role::getUnionId, "=", unionId)
|
||||||
|
.and(Sys_user_role::getEnable, "=", true));
|
||||||
|
List<String> userIds = relations.stream().map(Sys_user_role::getUserId)
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
throw new BaseException("分工会女工委委员没有设置,请联系校工会!");
|
||||||
|
}
|
||||||
|
return userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "流程发起人所属分工会女工委委员";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package com.budwk.app.flow.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||||
|
import com.budwk.app.flow.engine.core.Execution;
|
||||||
|
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||||
|
import com.budwk.app.flow.engine.model.TaskModel;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlowBranchUnionYouthWorkCommitteeHandler implements AssignmentHandler {
|
||||||
|
@Override
|
||||||
|
public List<String> assign(TaskModel model, Execution execution) {
|
||||||
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
|
||||||
|
Sys_role role = sysRoleService.getByCode(RoleConstant.BRANCH_UNION_YOUTH_WORK_COMMITTEE);
|
||||||
|
if (role == null || role.isDisabled()) {
|
||||||
|
throw new BaseException("分工会青工委委员角色未配置或已禁用,请联系校工会!");
|
||||||
|
}
|
||||||
|
String unionId = StrUtil.blankToDefault(execution.getArgs().getStr(FlowConst.INITIATOR_UNIT_UNION_ID), SecurityUtil.getUnionId());
|
||||||
|
if (StrUtil.isBlank(unionId)) {
|
||||||
|
throw new BaseException("无法确定所属分工会,请联系校工会!");
|
||||||
|
}
|
||||||
|
List<Sys_user_role> relations = dao.query(Sys_user_role.class,
|
||||||
|
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
|
||||||
|
.and(Sys_user_role::getUnionId, "=", unionId)
|
||||||
|
.and(Sys_user_role::getEnable, "=", true));
|
||||||
|
List<String> userIds = relations.stream().map(Sys_user_role::getUserId)
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
throw new BaseException("分工会青工委委员没有设置,请联系校工会!");
|
||||||
|
}
|
||||||
|
return userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "流程发起人所属分工会青工委委员";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,12 @@ import com.budwk.app.base.result.Result;
|
|||||||
import com.budwk.app.sys.models.Sys_config;
|
import com.budwk.app.sys.models.Sys_config;
|
||||||
import com.budwk.app.sys.services.SysConfigService;
|
import com.budwk.app.sys.services.SysConfigService;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.integration.jedis.pubsub.PubSubService;
|
import org.nutz.integration.jedis.pubsub.PubSubService;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
@@ -139,12 +141,15 @@ public class SysConfController {
|
|||||||
@At
|
@At
|
||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
@SaCheckPermission("sys.manager.conf")
|
@SaCheckPermission("sys.manager.conf")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Object data(@Param("pageNumber") int pageNumber,
|
public Object data(@Param("pageNumber") int pageNumber,
|
||||||
@Param("pageSize") int pageSize,
|
@Param("pageSize") int pageSize,
|
||||||
@Param("pageOrderName") String pageOrderName,
|
@Param("pageOrderName") String pageOrderName,
|
||||||
@Param("pageOrderBy") String pageOrderBy,
|
@Param("pageOrderBy") String pageOrderBy,
|
||||||
@Param("configKey") String configKey) {
|
@Param("configKey") String configKey) {
|
||||||
try {
|
try {
|
||||||
|
// 由 service 补充默认关闭的入口参数,管理员可通过现有“是/否”控件修改。
|
||||||
|
sysConfigService.ensureSmsLoginVisibleConfig(SecurityUtil.getUserId());
|
||||||
ensureAppImageConfig("AppHomeImg", "PC首页轮播图");
|
ensureAppImageConfig("AppHomeImg", "PC首页轮播图");
|
||||||
ensureAppImageConfig("H5AppHomeImg", "移动端首页轮播图");
|
ensureAppImageConfig("H5AppHomeImg", "移动端首页轮播图");
|
||||||
ensureAppImageConfig("AppFeaturedActivityImg", "精彩活动页顶部图片");
|
ensureAppImageConfig("AppFeaturedActivityImg", "精彩活动页顶部图片");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.budwk.app.sys.controller;
|
|||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.sys.models.Sys_dict;
|
import com.budwk.app.sys.models.Sys_dict;
|
||||||
import com.budwk.app.sys.services.SysDictService;
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
@@ -119,6 +120,8 @@ public class SysDictController {
|
|||||||
sysDictService.save(dict, parentId);
|
sysDictService.save(dict, parentId);
|
||||||
sysDictService.clearCache();
|
sysDictService.clearCache();
|
||||||
return Result.success("system.success");
|
return Result.success("system.success");
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Result.error("system.error");
|
return Result.error("system.error");
|
||||||
}
|
}
|
||||||
@@ -142,9 +145,11 @@ public class SysDictController {
|
|||||||
public Object editDo(@Param("..") Sys_dict dict, @Param("parentId") String parentId, HttpServletRequest req) {
|
public Object editDo(@Param("..") Sys_dict dict, @Param("parentId") String parentId, HttpServletRequest req) {
|
||||||
try {
|
try {
|
||||||
dict.setUpdatedBy(SecurityUtil.getUserId());
|
dict.setUpdatedBy(SecurityUtil.getUserId());
|
||||||
sysDictService.updateIgnoreNull(dict);
|
sysDictService.updateDict(dict);
|
||||||
sysDictService.clearCache();
|
sysDictService.clearCache();
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Result.error();
|
return Result.error();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.budwk.app.sys.services.SysConfigService;
|
|||||||
import com.budwk.app.sys.services.SysFileService;
|
import com.budwk.app.sys.services.SysFileService;
|
||||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
import com.budwk.app.zhgh.learning.service.LearningFileAccessService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -52,6 +53,9 @@ public class SysFileController {
|
|||||||
private SysConfigService sysConfigService;
|
private SysConfigService sysConfigService;
|
||||||
private SysFileMinIoUtil sysFileMinIoUtil;
|
private SysFileMinIoUtil sysFileMinIoUtil;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private LearningFileAccessService learningFileAccessService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/sys/file/index.html")
|
@Ok("beetl:/platform/sys/file/index.html")
|
||||||
@SaCheckPermission("sys.manager.file")
|
@SaCheckPermission("sys.manager.file")
|
||||||
@@ -124,6 +128,7 @@ public class SysFileController {
|
|||||||
@Ok("void")
|
@Ok("void")
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
if (!learningFileAccessService.check(id, true, response)) return;
|
||||||
sysFileService.download(id, request, response);
|
sysFileService.download(id, request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +137,7 @@ public class SysFileController {
|
|||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@ApiOperation("文件管理-转换PDF")
|
@ApiOperation("文件管理-转换PDF")
|
||||||
public void convertPDF(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
public void convertPDF(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
if (!learningFileAccessService.check(id, true, response)) return;
|
||||||
sysFileService.convertPDF(id, request, response);
|
sysFileService.convertPDF(id, request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,11 +176,82 @@ public class SysFileController {
|
|||||||
return Result.success(files);
|
return Result.success(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckLogin
|
||||||
|
public void preview(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
if (!learningFileAccessService.check(id, false, response)) return;
|
||||||
|
Sys_file file = sysFileService.detail(id);
|
||||||
|
String contentType = switch (StrUtil.blankToDefault(file.getSuffix(), "").toLowerCase()) {
|
||||||
|
case "jpg", "jpeg" -> "image/jpeg";
|
||||||
|
case "png" -> "image/png";
|
||||||
|
case "gif" -> "image/gif";
|
||||||
|
case "webp" -> "image/webp";
|
||||||
|
case "bmp" -> "image/bmp";
|
||||||
|
case "mp3" -> "audio/mpeg";
|
||||||
|
case "aac" -> "audio/aac";
|
||||||
|
case "wav" -> "audio/wav";
|
||||||
|
case "ogg" -> "audio/ogg";
|
||||||
|
case "m4a" -> "audio/mp4";
|
||||||
|
case "pdf" -> "application/pdf";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (contentType == null) {
|
||||||
|
response.sendError(415, "该资料不支持在线预览");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] bytes = SysFileEngineTypeEnum.LOCAL.getValue().equals(file.getEngine())
|
||||||
|
? FileUtil.readBytes(file.getStoragePath())
|
||||||
|
: SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
|
||||||
|
response.setContentType(contentType);
|
||||||
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline");
|
||||||
|
if (contentType.startsWith("audio/")) {
|
||||||
|
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||||
|
String range = request.getHeader(HttpHeaders.RANGE);
|
||||||
|
if (StrUtil.isNotBlank(range) && range.startsWith("bytes=") && !range.contains(",")) {
|
||||||
|
long start;
|
||||||
|
long end;
|
||||||
|
try {
|
||||||
|
String specification = range.substring(6).trim();
|
||||||
|
if (!specification.matches("[0-9]*-[0-9]*")) throw new NumberFormatException();
|
||||||
|
String[] bounds = specification.split("-", -1);
|
||||||
|
if (bounds[0].isEmpty()) {
|
||||||
|
long length = Long.parseLong(bounds[1]);
|
||||||
|
if (length <= 0) throw new NumberFormatException();
|
||||||
|
start = Math.max(0, bytes.length - length);
|
||||||
|
end = bytes.length - 1;
|
||||||
|
} else {
|
||||||
|
start = Long.parseLong(bounds[0]);
|
||||||
|
end = bounds[1].isEmpty() ? bytes.length - 1 : Math.min(Long.parseLong(bounds[1]), bytes.length - 1);
|
||||||
|
}
|
||||||
|
if (start >= bytes.length || end < start) throw new NumberFormatException();
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + bytes.length);
|
||||||
|
response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int length = (int) (end - start + 1);
|
||||||
|
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||||
|
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + bytes.length);
|
||||||
|
response.setContentLength(length);
|
||||||
|
try (OutputStream out = response.getOutputStream()) {
|
||||||
|
out.write(bytes, (int) start, length);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.setContentLength(bytes.length);
|
||||||
|
try (OutputStream out = response.getOutputStream()) {
|
||||||
|
out.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@ApiOperation("文件管理-视频播放")
|
@ApiOperation("文件管理-视频播放")
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
public void videoPlay(String id, HttpServletRequest request, HttpServletResponse response) {
|
public void videoPlay(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
if (!learningFileAccessService.check(id, false, response)) return;
|
||||||
Sys_file sys_file = sysFileService.detail(id);
|
Sys_file sys_file = sysFileService.detail(id);
|
||||||
byte[] bytes = SysFileMinIoUtil.getFileBytes(sys_file.getBucket(), sys_file.getStoragePath());
|
byte[] bytes = SysFileMinIoUtil.getFileBytes(sys_file.getBucket(), sys_file.getStoragePath());
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,29 @@
|
|||||||
package com.budwk.app.sys.controller;
|
package com.budwk.app.sys.controller;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.http.useragent.UserAgent;
|
import cn.hutool.http.useragent.UserAgent;
|
||||||
import cn.hutool.http.useragent.UserAgentUtil;
|
import cn.hutool.http.useragent.UserAgentUtil;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.sys.models.Sys_home_activity;
|
|
||||||
import com.budwk.app.sys.models.Sys_home_template;
|
import com.budwk.app.sys.models.Sys_home_template;
|
||||||
import com.budwk.app.sys.models.Sys_menu;
|
import com.budwk.app.sys.models.Sys_menu;
|
||||||
|
import com.budwk.app.sys.services.SysHomeActivityService;
|
||||||
|
import com.budwk.app.sys.services.SysHomeNewsService;
|
||||||
import com.budwk.app.sys.services.SysMenuService;
|
import com.budwk.app.sys.services.SysMenuService;
|
||||||
import com.budwk.app.sys.services.SysUserService;
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.FieldFilter;
|
import org.nutz.dao.FieldFilter;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
|
||||||
import org.nutz.dao.util.Daos;
|
import org.nutz.dao.util.Daos;
|
||||||
import org.nutz.integration.jedis.RedisService;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
|
||||||
import org.nutz.lang.Lang;
|
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
import org.nutz.lang.util.NutMap;
|
|
||||||
import org.nutz.log.Log;
|
import org.nutz.log.Log;
|
||||||
import org.nutz.log.Logs;
|
import org.nutz.log.Logs;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
@@ -42,7 +32,6 @@ import org.nutz.mvc.annotation.Param;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -58,9 +47,9 @@ public class SysHomeController {
|
|||||||
@Inject
|
@Inject
|
||||||
private SysUserService sysUserService;
|
private SysUserService sysUserService;
|
||||||
@Inject
|
@Inject
|
||||||
private RedisService redisService;
|
private SysHomeActivityService sysHomeActivityService;
|
||||||
@Inject
|
@Inject
|
||||||
private ActivityBasicScopeService activityBasicScopeService;
|
private SysHomeNewsService sysHomeNewsService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("re")
|
@Ok("re")
|
||||||
@@ -207,71 +196,16 @@ public class SysHomeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* includeEnded可选:true供PC首页展示已结束活动,缺省false保持既有调用行为。
|
||||||
|
* 返回Result,data为当前登录用户可见的活动实体列表,含名称、封面、链接及起止时间,不含内部SQL和类路径。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@ApiOperation("首页推送活动")
|
@ApiOperation("首页推送活动")
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
public Result listHomeActivity() {
|
public Result listHomeActivity(@Param(value = "includeEnded", df = "false") boolean includeEnded) {
|
||||||
FieldFilter fieldFilter = FieldFilter.locked(Sys_home_activity.class, "classPath");
|
return Result.success(sysHomeActivityService.listHomeActivity(SecurityUtil.getUserId(), includeEnded));
|
||||||
List<Sys_home_activity> list = Daos.ext(dao, fieldFilter).query(Sys_home_activity.class, Cnd.where("enable", "=", 1).desc("top"));
|
|
||||||
String userId = SecurityUtil.getUserId();
|
|
||||||
List<Sys_home_activity> allowActivityList = new ArrayList<>();
|
|
||||||
// 今天的时间
|
|
||||||
Date today = DateUtil.date();
|
|
||||||
for (Sys_home_activity activity : list) {
|
|
||||||
// 该活动的时间范围
|
|
||||||
Date startDate = activity.getStartDate();
|
|
||||||
Date endDate = activity.getEndDate();
|
|
||||||
// 判断当前时间是否在活动范围内,不在就过滤掉
|
|
||||||
// if (!DateUtil.isIn(today, startDate, endDate)) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// 只过滤结束的
|
|
||||||
if (DateUtil.compare(today, endDate) > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Integer allowUserGroupId = activity.getAllowUserGroupId();
|
|
||||||
String allowUserSql = activity.getAllowUserSql();
|
|
||||||
activity.setAllowUserSql(null);
|
|
||||||
if (allowUserGroupId == null && StrUtil.isBlank(allowUserSql)) {
|
|
||||||
allowActivityList.add(activity);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowUserGroupId != null) {
|
|
||||||
if (activityBasicScopeService.isUserInGroup(allowUserGroupId, userId)) {
|
|
||||||
allowActivityList.add(activity);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StrUtil.isNotBlank(allowUserSql)) {
|
|
||||||
Sql sql = Sqls.create(allowUserSql).setParam("userId", userId);
|
|
||||||
sql.setCallback(Sqls.callback.map());
|
|
||||||
dao.execute(sql);
|
|
||||||
NutMap nutMap = (NutMap) sql.getResult();
|
|
||||||
if (Lang.isNotEmpty(nutMap) && StrUtil.isNotBlank(nutMap.getString("userId"))) {
|
|
||||||
allowActivityList.add(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.success(allowActivityList);
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// FieldFilter fieldFilter = FieldFilter.locked(Sys_home_activity.class, "allowUserSql|classPath");
|
|
||||||
// List<Sys_home_activity> list = Daos.ext(dao, fieldFilter).query(Sys_home_activity.class, Cnd.where("enable", "=", 1).desc("top"));
|
|
||||||
//
|
|
||||||
// List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
|
|
||||||
// Set<String> hrefs = menus.stream().map(Sys_menu::getHref).collect(Collectors.toSet());
|
|
||||||
//
|
|
||||||
// List<Sys_home_activity> hasPermisiionList = list.stream().filter(activity -> {
|
|
||||||
// String url = activity.getUrl();
|
|
||||||
// String h5Url = activity.getH5Url();
|
|
||||||
// return hrefs.contains(url) || hrefs.contains(h5Url);
|
|
||||||
// }).toList().stream().toList();
|
|
||||||
// return Result.success(hasPermisiionList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@@ -293,34 +227,17 @@ public class SysHomeController {
|
|||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@ApiOperation("获取新闻")
|
@ApiOperation("获取新闻")
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
// @CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "web_news", isHash = true)
|
|
||||||
// @CacheResult(cacheKey = "news", ignoreNull = true, cacheLiveTime = 60 * 60 * 24)
|
|
||||||
public Result getNews() {
|
public Result getNews() {
|
||||||
try {
|
return Result.success(sysHomeNewsService.getWebsiteNews());
|
||||||
String url = "https://xgh.cug.edu.cn/";
|
}
|
||||||
Document document = Jsoup.connect(url).get();
|
|
||||||
Elements homeElements = document.select(".gh,.jc,.fc,.jg");
|
|
||||||
|
|
||||||
List<Map<String, Object>> news = homeElements.stream().map(homeElement -> {
|
/**
|
||||||
//获取板块名称
|
* href 传首页新闻项原始链接;成功结果 data 为图片地址字符串,空字符串表示没有可用图片。
|
||||||
String name = homeElement.selectFirst(".title").text();
|
*/
|
||||||
Elements liElements = homeElement.select("li");
|
@At
|
||||||
List<NutMap> ele = liElements.stream().map(liElement -> {
|
@SaCheckLogin
|
||||||
NutMap row = NutMap.NEW();
|
@Ok("json")
|
||||||
String href = liElement.selectFirst("a").attr("href");
|
public Result getNewsImage(@Param("href") String href) {
|
||||||
String time = liElement.selectFirst("span").text();
|
return Result.success(sysHomeNewsService.getNewsImage(href));
|
||||||
String title = liElement.selectFirst("a").attr("title");
|
|
||||||
row.put("href", href);
|
|
||||||
row.put("time", time);
|
|
||||||
row.put("text", title);
|
|
||||||
return row;
|
|
||||||
}).toList();
|
|
||||||
return Map.of("label", name, "value", ele);
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
redisService.setex("gonghui_news", 60 * 60 * 24, Json.toJson(news));
|
|
||||||
return Result.success(news);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,20 @@ import com.budwk.app.base.utils.RsaUtils;
|
|||||||
import com.budwk.app.sys.models.Sys_log;
|
import com.budwk.app.sys.models.Sys_log;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
import com.budwk.app.sys.services.SysUserService;
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
|
import com.budwk.app.sys.services.SysConfigService;
|
||||||
import com.budwk.app.web.commons.auth.service.ValidateService;
|
import com.budwk.app.web.commons.auth.service.ValidateService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.message.service.SliderCaptchaService;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.web.commons.base.Globals;
|
import com.budwk.app.web.commons.base.Globals;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.jasig.cas.client.util.AbstractCasFilter;
|
import org.jasig.cas.client.util.AbstractCasFilter;
|
||||||
import org.jasig.cas.client.validation.Assertion;
|
import org.jasig.cas.client.validation.Assertion;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.integration.jedis.RedisService;
|
import org.nutz.integration.jedis.RedisService;
|
||||||
import org.nutz.ioc.impl.PropertiesProxy;
|
import org.nutz.ioc.impl.PropertiesProxy;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
@@ -35,6 +39,7 @@ import org.nutz.mvc.annotation.At;
|
|||||||
import org.nutz.mvc.annotation.Filters;
|
import org.nutz.mvc.annotation.Filters;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -55,19 +60,19 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
public class SysLoginController {
|
public class SysLoginController {
|
||||||
private static final Log log = Logs.get();
|
private static final Log log = Logs.get();
|
||||||
private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing();
|
private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing();
|
||||||
/**
|
|
||||||
* TODO: 临时屏蔽 localhost 平台登录验证码,恢复时改为 false。
|
|
||||||
*/
|
|
||||||
private static final boolean DISABLE_LOGIN_CAPTCHA = true;
|
|
||||||
@Inject
|
@Inject
|
||||||
private SysUserService sysUserService;
|
private SysUserService sysUserService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private SysConfigService sysConfigService;
|
||||||
|
@Inject
|
||||||
private RedisService redisService;
|
private RedisService redisService;
|
||||||
@Inject
|
@Inject
|
||||||
private SLogService sLogService;
|
private SLogService sLogService;
|
||||||
@Inject
|
@Inject
|
||||||
private ValidateService validateService;
|
private ValidateService validateService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private SliderCaptchaService sliderCaptchaService;
|
||||||
|
@Inject
|
||||||
protected PropertiesProxy conf;
|
protected PropertiesProxy conf;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@@ -75,13 +80,17 @@ public class SysLoginController {
|
|||||||
@ApiOperation("用户本地登录页面")
|
@ApiOperation("用户本地登录页面")
|
||||||
@Filters
|
@Filters
|
||||||
public String login(HttpServletRequest req, HttpSession session) {
|
public String login(HttpServletRequest req, HttpSession session) {
|
||||||
|
// 只向模板提供入口显示状态,短信发送及验证码登录接口继续使用原有规则。
|
||||||
|
req.setAttribute("smsLoginVisible", sysConfigService.isSmsLoginVisible());
|
||||||
return "beetl:/platform/sys/login.html";
|
return "beetl:/platform/sys/login.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("re")
|
@Ok("re")
|
||||||
@Filters
|
@Filters
|
||||||
public String h5() {
|
public String h5(HttpServletRequest req) {
|
||||||
|
// 手机端复用同一个登录模板,读取相同的系统参数。
|
||||||
|
req.setAttribute("smsLoginVisible", sysConfigService.isSmsLoginVisible());
|
||||||
return "beetl:/platform/sys/login.html";
|
return "beetl:/platform/sys/login.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +102,13 @@ public class SysLoginController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号密码登录:username 为账号,password 为 RSA 密文,keyId 为公钥编号;
|
||||||
|
* platformKey/platformCaptcha 为当前会话、当前账号的滑块一次性凭证。
|
||||||
|
* 返回 Result:code=0 表示登录成功,msg 为成功提示或失败原因。
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@At("/doLogin")
|
@At("/doLogin")
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@ApiOperation("用户本地账号密码登录")
|
@ApiOperation("用户本地账号密码登录")
|
||||||
@@ -114,23 +130,19 @@ public class SysLoginController {
|
|||||||
return Result.error("参数缺失");
|
return Result.error("参数缺失");
|
||||||
}
|
}
|
||||||
|
|
||||||
String lockKey = RedisConstant.USER_LOGIN_LOCK_PREFIX + username;
|
// TODO: 临时停用所有账号的登录失败次数和 5 分钟锁定限制;恢复时同步取消下方失败计数写入的注释。
|
||||||
int errCount = Convert.toInt(StrUtil.blankToDefault(redisService.get(lockKey), "0"));
|
// String lockKey = RedisConstant.USER_LOGIN_LOCK_PREFIX + username;
|
||||||
log.info("用户名:" + username + "登录失败次数:" + errCount);
|
// int errCount = Convert.toInt(StrUtil.blankToDefault(redisService.get(lockKey), "0"));
|
||||||
|
// log.info("用户名:" + username + "登录失败次数:" + errCount);
|
||||||
|
|
||||||
if (errCount > 5) {
|
// if (errCount > 5) {
|
||||||
redisService.setex(lockKey, 5 * 60, String.valueOf(errCount + 1));
|
// redisService.setex(lockKey, 5 * 60, String.valueOf(errCount + 1));
|
||||||
return Result.error("登录失败次数过多,请5分钟后再试");
|
// return Result.error("登录失败次数过多,请5分钟后再试");
|
||||||
}
|
// }
|
||||||
try {
|
try {
|
||||||
// 验证码校验
|
// 所有访问地址均需消费绑定当前账号的滑块凭证,旧图形码不能用于登录。
|
||||||
if (!isLoginCaptchaDisabled(req)) {
|
sliderCaptchaService.consume(session.getId(), captchaKey, captchaCode,
|
||||||
try {
|
SliderCaptchaService.PASSWORD, username);
|
||||||
validateService.checkCode(captchaKey, captchaCode);
|
|
||||||
} catch (BaseException e) {
|
|
||||||
return Result.error(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密密码
|
// 解密密码
|
||||||
LoginRsaKey rsaKey = LOGIN_RSA_KEY_RING.getPrivateKey(keyId);
|
LoginRsaKey rsaKey = LOGIN_RSA_KEY_RING.getPrivateKey(keyId);
|
||||||
@@ -154,36 +166,47 @@ public class SysLoginController {
|
|||||||
return Result.success("login.success");
|
return Result.success("login.success");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
redisService.set(lockKey, Convert.toStr(errCount + 1));
|
// TODO: 临时暂停累计登录失败次数;恢复限制时与上方计数读取、锁定拦截一并恢复。
|
||||||
|
// redisService.set(lockKey, Convert.toStr(errCount + 1));
|
||||||
String message = e.getMessage();
|
String message = e.getMessage();
|
||||||
return Result.error(e.getMessage());
|
return Result.error(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLoginCaptchaDisabled(HttpServletRequest req) {
|
@At(value = {"/platform/sso/login", "/sso/login"}, top = true)
|
||||||
if (!DISABLE_LOGIN_CAPTCHA || req == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String serverName = req.getServerName();
|
|
||||||
return "localhost".equalsIgnoreCase(serverName)
|
|
||||||
|| "127.0.0.1".equals(serverName)
|
|
||||||
|| "0:0:0:0:0:0:0:1".equals(serverName)
|
|
||||||
|| "::1".equals(serverName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@At(value = "/platform/sso/login", top = true)
|
|
||||||
@Ok("re")
|
@Ok("re")
|
||||||
@ApiOperation("用户cas登录统一入口")
|
@ApiOperation("用户cas登录统一入口")
|
||||||
public String ssoLogin(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response) {
|
public String ssoLogin(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response) {
|
||||||
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
|
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
|
||||||
|
String tokenName = StpUtil.getTokenName();
|
||||||
|
boolean tokenCookiePresent = request.getCookies() != null && java.util.Arrays.stream(request.getCookies())
|
||||||
|
.anyMatch(cookie -> tokenName.equals(cookie.getName()) && StrUtil.isNotBlank(cookie.getValue()));
|
||||||
|
log.infof("[CAS诊断] 回调进入 uri=%s, ticketPresent=%s, sessionNew=%s, requestedSessionValid=%s, sessionAssertion=%s, requestAssertion=%s, tokenCookiePresent=%s, secure=%s",
|
||||||
|
request.getRequestURI(), StrUtil.isNotBlank(request.getParameter("ticket")), httpSession.isNew(),
|
||||||
|
request.isRequestedSessionIdValid(), assertion != null && assertion.getPrincipal() != null,
|
||||||
|
request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) != null, tokenCookiePresent, request.isSecure());
|
||||||
if (null == assertion || assertion.getPrincipal() == null) {
|
if (null == assertion || assertion.getPrincipal() == null) {
|
||||||
|
log.warn("[CAS诊断] Session 中没有有效认证结果,未执行本地登录,即将跳转首页");
|
||||||
return ">>:/platform/home";
|
return ">>:/platform/home";
|
||||||
}
|
}
|
||||||
String loginName = assertion.getPrincipal().getName();
|
String loginName = assertion.getPrincipal().getName();
|
||||||
sysUserService.checkThirdPlatformLoginName(loginName);
|
String stage = "查询本地用户";
|
||||||
Sys_user sysUser = sysUserService.loginByLoginName(loginName);
|
try {
|
||||||
|
sysUserService.checkThirdPlatformLoginName(loginName);
|
||||||
return ">>:" + sysUserService.loginPlus(sysUser, LoginType.CAS, request);
|
Sys_user sysUser = sysUserService.loginByLoginName(loginName);
|
||||||
|
stage = "建立本地登录态";
|
||||||
|
log.info("[CAS诊断] 本地用户查询成功,开始执行 loginPlus");
|
||||||
|
String redirect = sysUserService.loginPlus(sysUser, LoginType.CAS, request);
|
||||||
|
boolean tokenCookieWritten = response.getHeaders("Set-Cookie").stream()
|
||||||
|
.anyMatch(header -> header.startsWith(tokenName + "="));
|
||||||
|
log.infof("[CAS诊断] 本地登录完成 loggedIn=%s, tokenCookieWritten=%s, responseCommitted=%s, appDomain=%s, redirectPath=%s",
|
||||||
|
StpUtil.isLogin(), tokenCookieWritten, response.isCommitted(), Globals.AppDomain,
|
||||||
|
redirect == null ? null : redirect.split("[?#]", 2)[0]);
|
||||||
|
return ">>:" + redirect;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warnf("[CAS诊断] 回调失败 stage=%s, exceptionType=%s", stage, e.getClass().getName());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@At(value = "/platform/wxwork/oauth2/callback", top = true)
|
@At(value = "/platform/wxwork/oauth2/callback", top = true)
|
||||||
@@ -273,6 +296,120 @@ public class SysLoginController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送系统绑定手机号的登录验证码,滑块凭证和账号可用性由 service 再次校验。
|
||||||
|
*
|
||||||
|
* @param mobile 已绑定系统账号的 11 位大陆手机号
|
||||||
|
* @param captchaKey /checkSliderCaptcha 返回的 platformKey
|
||||||
|
* @param captchaCode /checkSliderCaptcha 返回的一次性 platformCaptcha
|
||||||
|
* @param request 当前请求,供后端按连接来源限流
|
||||||
|
* @return Result,成功 data 包含 retryAfter 重发等待秒数、expireIn 验证码有效秒数;失败 msg 为原因
|
||||||
|
*/
|
||||||
|
@At("/mobileCaptcha")
|
||||||
|
@POST
|
||||||
|
@Ok("json")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("发送手机登录验证码")
|
||||||
|
public Object mobileCaptcha(@Param("mobile") String mobile,
|
||||||
|
@Param("platformKey") String captchaKey,
|
||||||
|
@Param("platformCaptcha") String captchaCode,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
if (StrUtil.isBlank(mobile) || !mobile.trim().matches("^1[3-9]\\d{9}$")) {
|
||||||
|
return Result.error("请输入正确的手机号码");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(captchaKey) || StrUtil.isBlank(captchaCode)) {
|
||||||
|
return Result.error("请先完成滑块验证");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Result.success("短信验证码已发送", sysUserService.sendLoginSmsCode(mobile.trim(), captchaKey, captchaCode, request));
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送登录短信验证码失败", e);
|
||||||
|
return Result.error("短信发送失败,请稍后重试或联系管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用与手机号绑定的一次性短信验证码登录,不接收密码或 RSA 公钥参数。
|
||||||
|
*
|
||||||
|
* @param mobile 获取短信时填写的 11 位大陆手机号
|
||||||
|
* @param smsCode 用户收到的六位数字验证码
|
||||||
|
* @param request 当前登录请求,供 service 创建原有 Sa-Token 会话和登录日志
|
||||||
|
* @return Result,code 为 0 表示完成登录;失败 msg 为校验或登录失败原因
|
||||||
|
*/
|
||||||
|
@At("/doSmsLogin")
|
||||||
|
@POST
|
||||||
|
@Ok("json")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@ApiOperation("手机验证码登录")
|
||||||
|
public Object doSmsLogin(@Param("mobile") String mobile, @Param("smsCode") String smsCode,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
if (StrUtil.isBlank(mobile) || !mobile.trim().matches("^1[3-9]\\d{9}$")) {
|
||||||
|
return Result.error("请输入正确的手机号码");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(smsCode) || !smsCode.trim().matches("^\\d{6}$")) {
|
||||||
|
return Result.error("请输入六位短信验证码");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sysUserService.loginBySmsCode(mobile.trim(), smsCode.trim(), request);
|
||||||
|
return Result.success("login.success");
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("手机验证码登录失败", e);
|
||||||
|
return Result.error("登录失败,请稍后重试或联系管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* scene 传 password(账号登录)或 sms(发送短信),subject 传对应账号或手机号。
|
||||||
|
* 返回 Result.data:challengeId、background/piece 图片、width/height/pieceSize/pieceY 像素信息及 expiresIn 秒数。
|
||||||
|
* 会话 ID 仅从服务器读取,业务校验、图片生成和频控均由 service 完成。
|
||||||
|
*/
|
||||||
|
@At("/sliderCaptcha")
|
||||||
|
@POST
|
||||||
|
@Ok("json")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Object sliderCaptcha(@Param("scene") String scene, @Param("subject") String subject,
|
||||||
|
HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
response.setHeader("Cache-Control", "no-store");
|
||||||
|
try {
|
||||||
|
return Result.success(sliderCaptchaService.create(request.getSession().getId(),
|
||||||
|
request.getRemoteAddr(), scene, subject));
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成滑块验证码失败", e);
|
||||||
|
return Result.error("验证码加载失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* challengeId 传挑战编号,offset 传拼图左侧在原始 320px 图片中的横坐标。
|
||||||
|
* 返回 Result.data:platformKey、platformCaptcha 为一次性凭证,expiresIn 为有效秒数。
|
||||||
|
*/
|
||||||
|
@At("/checkSliderCaptcha")
|
||||||
|
@POST
|
||||||
|
@Ok("json")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Object checkSliderCaptcha(@Param("challengeId") String challengeId, @Param("offset") String offset,
|
||||||
|
HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
response.setHeader("Cache-Control", "no-store");
|
||||||
|
if (offset == null || !offset.matches("[0-9]{1,3}(\\.[0-9]{1,3})?")) {
|
||||||
|
return Result.error("滑块位置不正确,请重新验证");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Result.success(sliderCaptchaService.verify(request.getSession().getId(),
|
||||||
|
challengeId, Double.parseDouble(offset)));
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("校验滑块验证码失败", e);
|
||||||
|
return Result.error("验证失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@At("/captcha")
|
@At("/captcha")
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@ApiOperation("获取验证码")
|
@ApiOperation("获取验证码")
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.nutz.mvc.annotation.At;
|
|||||||
import org.nutz.mvc.annotation.GET;
|
import org.nutz.mvc.annotation.GET;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
@@ -499,6 +500,24 @@ public class SysRoleController {
|
|||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为全部 member=1 的用户设置会员角色,无需传入用户 ID、角色 ID 或列表筛选参数。
|
||||||
|
*
|
||||||
|
* @return JSON Result,data.addedCount 为新增关联数,data.enabledCount 为恢复启用关联数
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@Ok("json")
|
||||||
|
@SaCheckPermission("sys.manager.role.user")
|
||||||
|
@SLog(tag = "一键设置会员角色", msg = "为全部member=1的人员补齐会员角色")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result doSettingMemberRole() {
|
||||||
|
NutMap counts = sysRoleService.settingMemberRole();
|
||||||
|
sysRoleService.clearCache();
|
||||||
|
sysUserService.clearCache();
|
||||||
|
return Result.success("会员角色设置完成", counts);
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@SaCheckPermission("sys.manager.role")
|
@SaCheckPermission("sys.manager.role")
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
package com.budwk.app.sys.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
|
import com.budwk.app.sys.models.*;
|
||||||
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
|
import com.budwk.app.sys.views.View_user;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.lang.random.R;
|
||||||
|
import org.nutz.trans.Atom;
|
||||||
|
import org.nutz.trans.Trans;
|
||||||
|
import org.nutz.mvc.annotation.AdaptBy;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.upload.TempFile;
|
||||||
|
import org.nutz.mvc.upload.UploadAdaptor;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/sys/unionCadreImport")
|
||||||
|
@Ok("json:full")
|
||||||
|
public class SysUnionCadreImportController {
|
||||||
|
private static final String[] HEADERS = {"工会编码", "工号", "角色编码", "届次编码(选填)", "所属单位编码(书记必填)"};
|
||||||
|
private static final int MAX_ROWS = 1000;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
@Inject
|
||||||
|
private SysDictService sysDictService;
|
||||||
|
@Inject
|
||||||
|
private SysUnionController sysUnionController;
|
||||||
|
@Inject
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
@Inject
|
||||||
|
private SysUserService sysUserService;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<NutMap> roleOptions() {
|
||||||
|
return (List<NutMap>) sysUnionController.branchUnionRoleOptions().getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckRole("SYSADMIN")
|
||||||
|
@SaCheckPermission("sys.manager.union.branchOfficer")
|
||||||
|
public void downloadTemplate(HttpServletResponse response) {
|
||||||
|
Workbook workbook = new XSSFWorkbook();
|
||||||
|
Sheet data = createSheet(workbook, "干部导入", HEADERS);
|
||||||
|
CellStyle textStyle = workbook.createCellStyle();
|
||||||
|
textStyle.setDataFormat(workbook.createDataFormat().getFormat("@"));
|
||||||
|
for (int i = 0; i < HEADERS.length; i++) {
|
||||||
|
data.setDefaultColumnStyle(i, textStyle);
|
||||||
|
}
|
||||||
|
Sheet notes = createSheet(workbook, "填写说明", "字段", "填写要求");
|
||||||
|
notes.setColumnWidth(1, 100 * 256);
|
||||||
|
append(notes, "导入范围", "仅导入第一个工作表;每行一个人员角色,最多1000行;请保留表头,工号和编码按文本填写。");
|
||||||
|
append(notes, "工会编码", "必填,参考工会对照表。人员必须属于该分工会。");
|
||||||
|
append(notes, "工号", "必填,填写系统中已存在的人员工号,不填写姓名。");
|
||||||
|
append(notes, "角色编码", "必填,参考角色对照表,不填写角色名称。");
|
||||||
|
append(notes, "届次编码(选填)", "可留空;填写时使用届次对照表中的编码,与单个添加一致。");
|
||||||
|
append(notes, "所属单位编码(书记必填)", "仅二级党委书记填写,多个单位用英文逗号分隔,必须为当前分工会的二级单位;其他角色留空。");
|
||||||
|
append(notes, "重复记录", "普通干部按人员和角色跳过;二级党委书记按人员、角色和工会跳过。禁用的已有关系也跳过,不覆盖。");
|
||||||
|
append(notes, "保存方式", "系统管理员直接授权,批量校验并保存干部及角色记录,统一清理权限缓存,不发起审批。校验失败行不写入;保存异常时拆分定位失败行,其他有效行继续导入。");
|
||||||
|
append(notes, "已有干部记录", "已有在任或待审核干部记录但未分配角色时跳过,避免重复创建或绕过已有审批。");
|
||||||
|
Sheet unions = createSheet(workbook, "工会对照", "工会编码", "工会名称");
|
||||||
|
List<Sys_union> unionList = dao.query(Sys_union.class, Cnd.NEW().asc("unionCode"));
|
||||||
|
for (Sys_union union : unionList) {
|
||||||
|
append(unions, union.getUnionCode(), union.getName());
|
||||||
|
}
|
||||||
|
Sheet roles = createSheet(workbook, "角色对照", "角色编码", "角色名称");
|
||||||
|
for (NutMap role : roleOptions()) {
|
||||||
|
append(roles, role.getString("code"), role.getString("name"));
|
||||||
|
}
|
||||||
|
Sheet terms = createSheet(workbook, "届次对照", "届次编码", "届次名称");
|
||||||
|
for (Sys_dict term : sysDictService.getSubListByCode("TEACHER_CONGRESS_J")) {
|
||||||
|
append(terms, term.getCode(), term.getName());
|
||||||
|
}
|
||||||
|
Sheet units = createSheet(workbook, "所属单位对照", "单位编码", "单位名称", "工会编码", "工会名称");
|
||||||
|
for (Sys_unit unit : dao.query(Sys_unit.class, Cnd.where("unitLevel", "=", 2).asc("unitcode"))) {
|
||||||
|
for (Sys_union union : unionList) {
|
||||||
|
if (union.getId().equals(unit.getUnionId())) {
|
||||||
|
append(units, unit.getUnitcode(), unit.getName(), union.getUnionCode(), union.getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CommonDownloadUtil.download("批量添加分工会干部模板.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckRole("SYSADMIN")
|
||||||
|
@SaCheckPermission("sys.manager.union.branchOfficer")
|
||||||
|
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||||
|
public Result doImport(@Param("file") TempFile file) {
|
||||||
|
if (file == null) {
|
||||||
|
return Result.error("请选择Excel文件");
|
||||||
|
}
|
||||||
|
List<NutMap> rows = new ArrayList<>();
|
||||||
|
// 先完整读取文件,避免文件格式错误时已写入部分数据却无法返回逐行结果。
|
||||||
|
try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) {
|
||||||
|
if (workbook.getNumberOfSheets() == 0) {
|
||||||
|
return Result.error("文件没有工作表,请使用下载的模板");
|
||||||
|
}
|
||||||
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
DataFormatter formatter = new DataFormatter();
|
||||||
|
for (int i = 0; i < HEADERS.length; i++) {
|
||||||
|
if (!HEADERS[i].equals(cellText(sheet.getRow(0), i, formatter))) {
|
||||||
|
return Result.error("模板表头不正确,请使用下载的模板并保留第一行表头");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Row row : sheet) {
|
||||||
|
if (row.getRowNum() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] values = new String[HEADERS.length];
|
||||||
|
for (int j = 0; j < values.length; j++) {
|
||||||
|
values[j] = cellText(row, j, formatter);
|
||||||
|
}
|
||||||
|
if (Arrays.stream(values).allMatch(StrUtil::isBlank)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rows.size() >= MAX_ROWS) {
|
||||||
|
return Result.error("每次最多导入1000行有效数据,请拆分文件后导入");
|
||||||
|
}
|
||||||
|
rows.add(NutMap.NEW().addv("rowNumber", row.getRowNum() + 1).addv("unionCode", values[0])
|
||||||
|
.addv("loginname", values[1]).addv("roleCode", values[2])
|
||||||
|
.addv("j", values[3]).addv("unitCodes", values[4]));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("读取分工会干部导入文件失败", e);
|
||||||
|
return Result.error("无法读取文件,请上传未加密的xls或xlsx模板文件,编码列不要使用公式");
|
||||||
|
}
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
return Result.error("文件中没有需要导入的数据");
|
||||||
|
}
|
||||||
|
Set<String> roleCodes = sysDictService.getSubListByCode("BRANCH_UNION_ROLES").stream()
|
||||||
|
.map(Sys_dict::getCode).collect(Collectors.toSet());
|
||||||
|
roleCodes.add(RoleConstant.UNIT_PARTY_SECRETARY.name());
|
||||||
|
Set<String> terms = sysDictService.getSubListByCode("TEACHER_CONGRESS_J").stream()
|
||||||
|
.map(Sys_dict::getCode).collect(Collectors.toSet());
|
||||||
|
List<String> unionCodes = values(rows, "unionCode");
|
||||||
|
List<String> loginnames = values(rows, "loginname");
|
||||||
|
List<String> inputRoleCodes = values(rows, "roleCode");
|
||||||
|
List<String> unitCodes = rows.stream().flatMap(row -> splitUnitCodes(row.getString("unitCodes")).stream())
|
||||||
|
.distinct().toList();
|
||||||
|
// 各类基础数据及已有关系按表一次查询,逐行校验时不再访问数据库。
|
||||||
|
Map<String, List<Sys_union>> unions = group(unionCodes.isEmpty() ? List.of()
|
||||||
|
: dao.query(Sys_union.class, Cnd.where("unionCode", "in", unionCodes)), Sys_union::getUnionCode);
|
||||||
|
Map<String, List<View_user>> users = group(loginnames.isEmpty() ? List.of()
|
||||||
|
: dao.query(View_user.class, Cnd.where("loginname", "in", loginnames)), View_user::getLoginname);
|
||||||
|
Map<String, List<Sys_role>> roles = group(inputRoleCodes.isEmpty() ? List.of()
|
||||||
|
: dao.query(Sys_role.class, Cnd.where("code", "in", inputRoleCodes)), Sys_role::getCode);
|
||||||
|
Map<String, List<Sys_unit>> units = group(unitCodes.isEmpty() ? List.of()
|
||||||
|
: dao.query(Sys_unit.class, Cnd.where("unitcode", "in", unitCodes)), Sys_unit::getUnitcode);
|
||||||
|
List<String> userIds = users.values().stream().flatMap(List::stream).map(View_user::getId).distinct().toList();
|
||||||
|
List<String> roleIds = roles.values().stream().flatMap(List::stream).map(Sys_role::getId).distinct().toList();
|
||||||
|
Set<List<String>> existingRoles = new HashSet<>();
|
||||||
|
if (!userIds.isEmpty() && !roleIds.isEmpty()) {
|
||||||
|
for (Sys_user_role relation : dao.query(Sys_user_role.class,
|
||||||
|
Cnd.where("userId", "in", userIds).and("roleId", "in", roleIds))) {
|
||||||
|
existingRoles.add(roleKey(relation.getUserId(), relation.getRoleId(), relation.getUnionId(),
|
||||||
|
roles.getOrDefault(RoleConstant.UNIT_PARTY_SECRETARY.name(), List.of()).stream()
|
||||||
|
.anyMatch(role -> role.getId().equals(relation.getRoleId()))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<List<String>> existingCadres = new HashSet<>();
|
||||||
|
if (!userIds.isEmpty() && !inputRoleCodes.isEmpty() && !unionCodes.isEmpty()) {
|
||||||
|
List<String> unionIds = unions.values().stream().flatMap(List::stream).map(Sys_union::getId).distinct().toList();
|
||||||
|
if (!unionIds.isEmpty()) {
|
||||||
|
for (Sys_union_cadre cadre : dao.query(Sys_union_cadre.class, Cnd.where("userId", "in", userIds)
|
||||||
|
.and("roleCode", "in", inputRoleCodes).and("unionId", "in", unionIds).and("isServing", "=", true))) {
|
||||||
|
existingCadres.add(Arrays.asList(cadre.getUserId(), cadre.getRoleCode(), cadre.getUnionId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<ImportEntry> entries = new ArrayList<>();
|
||||||
|
Map<List<String>, Integer> fileRoles = new java.util.HashMap<>();
|
||||||
|
String operator = SecurityUtil.getUserId();
|
||||||
|
for (NutMap row : rows) {
|
||||||
|
ImportEntry entry = prepareRow(row, roleCodes, terms, unions, users, roles, units,
|
||||||
|
existingRoles, existingCadres, fileRoles, operator);
|
||||||
|
if (entry != null) {
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveBatch(entries);
|
||||||
|
int success = (int) rows.stream().filter(row -> "成功".equals(row.getString("status"))).count();
|
||||||
|
int skipped = (int) rows.stream().filter(row -> "跳过".equals(row.getString("status"))).count();
|
||||||
|
if (success > 0) {
|
||||||
|
sysRoleService.clearCache();
|
||||||
|
sysUserService.clearCache();
|
||||||
|
}
|
||||||
|
return Result.success(NutMap.NEW().addv("success", success).addv("skipped", skipped)
|
||||||
|
.addv("failed", rows.size() - success - skipped).addv("rows", rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> values(List<NutMap> rows, String field) {
|
||||||
|
return rows.stream().map(row -> row.getString(field)).filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> splitUnitCodes(String codes) {
|
||||||
|
return Arrays.stream(codes.split("[,,]")).map(String::trim).filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Map<String, List<T>> group(List<T> items, Function<T, String> key) {
|
||||||
|
return items.stream().collect(Collectors.groupingBy(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> roleKey(String userId, String roleId, String unionId, boolean secretary) {
|
||||||
|
return Arrays.asList(userId, roleId, secretary ? unionId : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T unique(Map<String, List<T>> records, String code, String label, List<String> errors) {
|
||||||
|
if (StrUtil.isBlank(code)) {
|
||||||
|
errors.add(label + "不能为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<T> matches = records.getOrDefault(code, List.of());
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
errors.add(label + "【" + code + "】不存在");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (matches.size() > 1) {
|
||||||
|
errors.add(label + "【" + code + "】匹配到" + matches.size() + "条记录,需先处理系统中的重复编码");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return matches.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImportEntry prepareRow(NutMap row, Set<String> roleCodes, Set<String> terms,
|
||||||
|
Map<String, List<Sys_union>> unions, Map<String, List<View_user>> users,
|
||||||
|
Map<String, List<Sys_role>> roles, Map<String, List<Sys_unit>> units,
|
||||||
|
Set<List<String>> existingRoles, Set<List<String>> existingCadres,
|
||||||
|
Map<List<String>, Integer> fileRoles, String operator) {
|
||||||
|
String unionCode = row.getString("unionCode");
|
||||||
|
String loginname = row.getString("loginname");
|
||||||
|
String roleCode = row.getString("roleCode");
|
||||||
|
String j = row.getString("j");
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
Sys_union union = unique(unions, unionCode, "工会编码", errors);
|
||||||
|
View_user user = unique(users, loginname, "工号", errors);
|
||||||
|
Sys_role role = unique(roles, roleCode, "角色编码", errors);
|
||||||
|
if (StrUtil.isNotBlank(roleCode) && !roleCodes.contains(roleCode)) {
|
||||||
|
errors.add("角色编码【" + roleCode + "】不在允许导入的分工会干部角色范围内,请参照角色对照表");
|
||||||
|
}
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
outcome(row, "失败", String.join(";", errors));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean secretary = RoleConstant.UNIT_PARTY_SECRETARY.name().equals(roleCode);
|
||||||
|
List<String> key = roleKey(user.getId(), role.getId(), union.getId(), secretary);
|
||||||
|
if (existingRoles.contains(key)) {
|
||||||
|
outcome(row, "跳过", "工号【" + loginname + "】已分配角色【" + roleCode + "】"
|
||||||
|
+ (secretary ? "(工会编码【" + unionCode + "】)" : "") + ",保留原有记录(含禁用关系)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!union.getId().equals(user.getUnionId())) {
|
||||||
|
errors.add("工号【" + loginname + "】所属工会为【" + StrUtil.blankToDefault(user.getUnionName(), "未分配工会")
|
||||||
|
+ "】,与填写的工会【" + union.getName() + "(" + unionCode + ")】不一致");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(j) && !terms.contains(j)) {
|
||||||
|
errors.add("届次编码【" + j + "】不存在,请参照届次对照表或留空");
|
||||||
|
}
|
||||||
|
if (existingCadres.contains(Arrays.asList(user.getId(), roleCode, union.getId()))) {
|
||||||
|
outcome(row, "跳过", "工号【" + loginname + "】在工会【" + unionCode + "】已有角色【" + roleCode
|
||||||
|
+ "】的在任或待审核干部记录,请通过原记录处理");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> unitIds = new ArrayList<>();
|
||||||
|
String unitCodes = row.getString("unitCodes");
|
||||||
|
if (secretary) {
|
||||||
|
List<String> codes = splitUnitCodes(unitCodes);
|
||||||
|
if (codes.isEmpty()) {
|
||||||
|
errors.add("角色【二级党委书记】必须填写所属单位编码,请参照所属单位对照表");
|
||||||
|
}
|
||||||
|
for (String code : codes) {
|
||||||
|
List<Sys_unit> matches = units.getOrDefault(code, List.of());
|
||||||
|
List<Sys_unit> valid = matches.stream().filter(unit -> union.getId().equals(unit.getUnionId())
|
||||||
|
&& Integer.valueOf(2).equals(unit.getUnitLevel())).toList();
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
errors.add("所属单位编码【" + code + "】不存在");
|
||||||
|
} else if (valid.isEmpty()) {
|
||||||
|
boolean inUnion = matches.stream().anyMatch(unit -> union.getId().equals(unit.getUnionId()));
|
||||||
|
errors.add(inUnion ? "所属单位编码【" + code + "】属于工会【" + unionCode + "】,但单位级别不是二级"
|
||||||
|
: "所属单位编码【" + code + "】不属于填写的工会【" + union.getName() + "(" + unionCode + ")】");
|
||||||
|
} else if (valid.size() > 1) {
|
||||||
|
errors.add("所属单位编码【" + code + "】在工会【" + unionCode + "】下匹配到" + valid.size() + "个二级单位,编码不唯一");
|
||||||
|
} else {
|
||||||
|
unitIds.add(valid.get(0).getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (StrUtil.isNotBlank(unitCodes)) {
|
||||||
|
errors.add("角色【" + roleCode + "】不是二级党委书记,所属单位编码【" + unitCodes + "】应留空");
|
||||||
|
}
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
outcome(row, "失败", String.join(";", errors));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Integer previousRow = fileRoles.putIfAbsent(key, row.getInt("rowNumber"));
|
||||||
|
if (previousRow != null) {
|
||||||
|
outcome(row, "跳过", "与本文件第" + previousRow + "行的人员角色授权重复");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Sys_union_cadre cadre = new Sys_union_cadre();
|
||||||
|
cadre.setId(R.UU32());
|
||||||
|
cadre.setUserId(user.getId());
|
||||||
|
cadre.setUnionId(union.getId());
|
||||||
|
cadre.setMobile(user.getMobile());
|
||||||
|
cadre.setUnionName(user.getUnionName());
|
||||||
|
cadre.setLoginName(user.getLoginname());
|
||||||
|
cadre.setUserName(user.getUsername());
|
||||||
|
cadre.setRoleCode(roleCode);
|
||||||
|
cadre.setJ(j);
|
||||||
|
cadre.setApplyDate(new Date());
|
||||||
|
cadre.setIsServing(true);
|
||||||
|
cadre.setIsJoin(true);
|
||||||
|
// fastInsert不执行实体插入拦截器,显式补齐主键及审计字段。
|
||||||
|
cadre.setCreatedBy(operator);
|
||||||
|
cadre.setUpdatedBy(operator);
|
||||||
|
cadre.setCreatedAt(System.currentTimeMillis());
|
||||||
|
cadre.setUpdatedAt(cadre.getCreatedAt());
|
||||||
|
cadre.setDelFlag(false);
|
||||||
|
List<Sys_user_role> relations = new ArrayList<>();
|
||||||
|
for (String unitId : secretary ? unitIds : java.util.Collections.singletonList((String) null)) {
|
||||||
|
Sys_user_role relation = new Sys_user_role();
|
||||||
|
relation.setUserId(user.getId());
|
||||||
|
relation.setRoleId(role.getId());
|
||||||
|
relation.setUnionId(union.getId());
|
||||||
|
relation.setUnitId(unitId);
|
||||||
|
relations.add(relation);
|
||||||
|
}
|
||||||
|
return new ImportEntry(row, cadre, relations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveBatch(List<ImportEntry> entries) {
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 两张表各批量写入一次,同一事务保证干部记录和角色授权同时生效。
|
||||||
|
Trans.exec((Atom) () -> {
|
||||||
|
dao.fastInsert(entries.stream().map(entry -> entry.cadre).toList());
|
||||||
|
dao.fastInsert(entries.stream().flatMap(entry -> entry.relations.stream()).toList());
|
||||||
|
});
|
||||||
|
entries.forEach(entry -> outcome(entry.row, "成功", "添加成功"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (entries.size() > 1) {
|
||||||
|
// 仅保存异常时拆分批次定位坏行,正常导入不产生逐行写入。
|
||||||
|
int middle = entries.size() / 2;
|
||||||
|
saveBatch(entries.subList(0, middle));
|
||||||
|
saveBatch(entries.subList(middle, entries.size()));
|
||||||
|
} else {
|
||||||
|
NutMap row = entries.get(0).row;
|
||||||
|
log.error("导入分工会干部保存失败,行号:{},工号:{},角色编码:{}",
|
||||||
|
row.getInt("rowNumber"), row.getString("loginname"), row.getString("roleCode"), e);
|
||||||
|
outcome(row, "失败", saveFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String saveFailure(Exception error) {
|
||||||
|
for (Throwable cause = error; cause != null; cause = cause.getCause()) {
|
||||||
|
if (cause instanceof SQLException) {
|
||||||
|
SQLException sql = (SQLException) cause;
|
||||||
|
String state = sql.getSQLState();
|
||||||
|
if ("23505".equals(state) || sql.getErrorCode() == 1062) {
|
||||||
|
return "保存时发生唯一键冲突,可能已有相同干部或角色授权记录;本行已回滚";
|
||||||
|
}
|
||||||
|
if ("22001".equals(state) || sql.getErrorCode() == 1406) {
|
||||||
|
return "干部信息字段超出数据库长度限制,请检查工号、姓名、手机号、工会名称及届次长度;本行已回滚";
|
||||||
|
}
|
||||||
|
if ("23502".equals(state) || sql.getErrorCode() == 1048 || sql.getErrorCode() == 1364) {
|
||||||
|
return "干部记录或角色授权缺少数据库要求的必填值,请检查该人员的基础信息及干部表必填字段;本行已回滚";
|
||||||
|
}
|
||||||
|
if ("23503".equals(state) || sql.getErrorCode() == 1452) {
|
||||||
|
return "人员、工会、角色或所属单位的关联记录已不存在,请核对对应系统数据;本行已回滚";
|
||||||
|
}
|
||||||
|
if (state != null && state.startsWith("23")) {
|
||||||
|
return "干部记录或角色授权违反数据库必填、关联或唯一性约束,请核对人员、工会和角色数据;本行已回滚";
|
||||||
|
}
|
||||||
|
if (state != null && state.startsWith("08")) {
|
||||||
|
return "保存干部及角色授权时数据库连接异常,本行未完成导入,可稍后重新导入";
|
||||||
|
}
|
||||||
|
if (state != null && state.startsWith("40")) {
|
||||||
|
return "保存干部及角色授权时发生事务冲突,本行已回滚,可重新导入";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "干部记录或角色授权写入异常,本行未完成导入;请管理员按Excel行号、工号及角色编码查看导入日志";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ImportEntry {
|
||||||
|
private final NutMap row;
|
||||||
|
private final Sys_union_cadre cadre;
|
||||||
|
private final List<Sys_user_role> relations;
|
||||||
|
|
||||||
|
private ImportEntry(NutMap row, Sys_union_cadre cadre, List<Sys_user_role> relations) {
|
||||||
|
this.row = row;
|
||||||
|
this.cadre = cadre;
|
||||||
|
this.relations = relations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outcome(NutMap row, String status, String message) {
|
||||||
|
row.setv("status", status).setv("message", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellText(Row row, int column, DataFormatter formatter) {
|
||||||
|
Cell cell = row == null ? null : row.getCell(column);
|
||||||
|
if (cell != null && cell.getCellType() == CellType.FORMULA) {
|
||||||
|
throw new IllegalArgumentException("导入内容不支持公式");
|
||||||
|
}
|
||||||
|
return formatter.formatCellValue(cell).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Sheet createSheet(Workbook workbook, String name, String... headers) {
|
||||||
|
Sheet sheet = workbook.createSheet(name);
|
||||||
|
Row header = sheet.createRow(0);
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
header.createCell(i).setCellValue(headers[i]);
|
||||||
|
sheet.setColumnWidth(i, 30 * 256);
|
||||||
|
}
|
||||||
|
sheet.createFreezePane(0, 1);
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void append(Sheet sheet, String... values) {
|
||||||
|
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
row.createCell(i).setCellValue(values[i] == null ? "" : values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -556,6 +556,28 @@ public class SysUnionController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@ApiOperation("修改分工会干部届次")
|
||||||
|
@SaCheckPermission("sys.manager.union.branchOfficer")
|
||||||
|
public Result updateBranchUnionUserJ(String id, String j) {
|
||||||
|
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
|
||||||
|
return Result.error("仅超级管理员可编辑");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(id)) {
|
||||||
|
return Result.error("请选择干部记录");
|
||||||
|
}
|
||||||
|
Sys_union_cadre unionCadre = dao.fetch(Sys_union_cadre.class, id);
|
||||||
|
if (Lang.isEmpty(unionCadre)) {
|
||||||
|
return Result.error("未找到对应干部记录");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(j) && sysDictService.getSubListByCode("TEACHER_CONGRESS_J").stream()
|
||||||
|
.noneMatch(item -> j.equals(item.getCode()))) {
|
||||||
|
return Result.error("请选择有效届次");
|
||||||
|
}
|
||||||
|
dao.update(Sys_union_cadre.class, Chain.make("j", StrUtil.isBlank(j) ? "" : j), Cnd.where("id", "=", id));
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("分工会干部离任")
|
@ApiOperation("分工会干部离任")
|
||||||
@SaCheckPermission("sys.manager.union.branchOfficer")
|
@SaCheckPermission("sys.manager.union.branchOfficer")
|
||||||
|
|||||||
@@ -295,6 +295,14 @@ public class SysUnitController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取以指定单位为根的单位树。
|
||||||
|
*
|
||||||
|
* @param pid 根单位 ID;未传时使用系统根单位 0
|
||||||
|
* @param req 当前 HTTP 请求
|
||||||
|
* @return Result 对象;code 为 0 时 data 为树数组,节点包含 id、parentId、name、children、unitTypeCode;
|
||||||
|
* unitTypeCode 为 0 表示其他类、1 表示部门类,历史空值仅在返回时按 0 展示
|
||||||
|
*/
|
||||||
@At("/tree")
|
@At("/tree")
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@@ -317,30 +325,45 @@ public class SysUnitController {
|
|||||||
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
|
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
|
||||||
.setExtra(
|
.setExtra(
|
||||||
Map.of(
|
Map.of(
|
||||||
"unitTypeCode", unit.getUnitTypeCode()
|
// 历史手工新建单位可能未设置类型,展示时按其他类处理,避免空值使整棵树加载失败。
|
||||||
|
"unitTypeCode", unit.getUnitTypeCode() == null ? 0 : unit.getUnitTypeCode()
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
|
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
|
||||||
return Result.success(treeList);
|
return Result.success(treeList);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.error("加载单位树失败", e);
|
||||||
return Result.error();
|
return Result.error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建当前单位的下设单位。
|
||||||
|
*
|
||||||
|
* @param unit 表单数据;name 为单位名称,unitcode 为唯一编码,unitLevel 为单位等级,unionId 为所属工会;
|
||||||
|
* unitTypeCode 为部门类别,0 表示其他类、1 表示部门类,未传时由 service 按其他类保存
|
||||||
|
* @param parentId 当前选中上级单位的 ID,必须对应已存在的单位
|
||||||
|
* @param req 当前 HTTP 请求
|
||||||
|
* @return Result 对象;code 为 0 表示保存成功,非 0 表示失败,msg 为提示信息,data 为空
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission("sys.manager.unit.add")
|
@SaCheckPermission("sys.manager.unit.add")
|
||||||
@SLog(tag = "新建单位", msg = "单位名称:${args[0].name}")
|
@SLog(tag = "新建单位", msg = "单位名称:${args[0].name}")
|
||||||
public Object addDo(@Param("..") Sys_unit unit, @Param("parentId") String parentId, HttpServletRequest req) {
|
public Object addDo(@Param("..") Sys_unit unit, @Param("parentId") String parentId, HttpServletRequest req) {
|
||||||
try {
|
try {
|
||||||
if ("root".equals(parentId)) {
|
if (Strings.isBlank(unit.getName()) || Strings.isBlank(unit.getUnitcode()) || Strings.isBlank(parentId) || "root".equals(parentId)) {
|
||||||
parentId = "";
|
return Result.error("单位名称、单位编码和上级单位不能为空");
|
||||||
}
|
}
|
||||||
unit.setCreatedBy(SecurityUtil.getUserId());
|
unit.setCreatedBy(SecurityUtil.getUserId());
|
||||||
sysUnitService.save(unit, parentId);
|
sysUnitService.save(unit, parentId);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
} catch (BaseException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.error("新建单位失败", e);
|
||||||
return Result.error();
|
return Result.error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.nutz.dao.Cnd;
|
|||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.Static;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -57,6 +58,8 @@ public class SysV4ServController {
|
|||||||
$condition
|
$condition
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
|
// 只隐藏流程中心入口;历史流程实例及已有流程定义保持原有状态。
|
||||||
|
cnd.and(new Static("COALESCE(d.centerEnabled, 1) = 1"));
|
||||||
if (StrUtil.isNotBlank(keyword)) {
|
if (StrUtil.isNotBlank(keyword)) {
|
||||||
cnd.where().andLike("t.disPlayName", keyword);
|
cnd.where().andLike("t.disPlayName", keyword);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ public class Sys_dict extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("关联人员类型编码")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String personType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("人员属性业务编码")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String userAttributeCode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("启用状态")
|
@Comment("启用状态")
|
||||||
@ColDefine(type = ColType.BOOLEAN)
|
@ColDefine(type = ColType.BOOLEAN)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Column
|
@Column
|
||||||
@Comment("出生日期")
|
@Comment("出生日期")
|
||||||
@ColDefine(type = ColType.DATE)
|
@ColDefine(type = ColType.DATE)
|
||||||
|
@DataCenterColumn(name = "出生日期", key = "CSRQ")
|
||||||
private Date birthday;
|
private Date birthday;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -114,6 +115,7 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Column
|
@Column
|
||||||
@Comment("学历")
|
@Comment("学历")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
@DataCenterColumn(name = "学历", key = "ZGXLMC")
|
||||||
private String education;
|
private String education;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -125,6 +127,7 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Comment("学位")
|
@Comment("学位")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
// @DataCenterColumn(name = "学位", key = "zgxwmc")
|
// @DataCenterColumn(name = "学位", key = "zgxwmc")
|
||||||
|
@DataCenterColumn(name = "学位", key = "ZGXWMC")
|
||||||
private String academicDegree;
|
private String academicDegree;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -135,6 +138,7 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Column
|
@Column
|
||||||
@Comment("职称")
|
@Comment("职称")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
@DataCenterColumn(name = "职称", key = "ZYJSZWDMMC")
|
||||||
private String professionalTitle;
|
private String professionalTitle;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -206,9 +210,9 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
private String dutySituation;
|
private String dutySituation;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("教职工类别码")
|
@Comment("人员类型")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 30)
|
@ColDefine(type = ColType.VARCHAR, width = 30)
|
||||||
@DataCenterColumn(name = "教职工类别码", key = "RYLX", dict = "USER_PERSON_TYPE")
|
@DataCenterColumn(name = "人员类型", key = "RYLX", dict = "USER_PERSON_TYPE")
|
||||||
private String personType;
|
private String personType;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -220,6 +224,7 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Column
|
@Column
|
||||||
@Comment("编制类别码")
|
@Comment("编制类别码")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 30)
|
@ColDefine(type = ColType.VARCHAR, width = 30)
|
||||||
|
@DataCenterColumn(name = "编制类别", key = "BZLBDMMC")
|
||||||
private String preparedBy;
|
private String preparedBy;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@@ -237,6 +242,11 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.DATE)
|
@ColDefine(type = ColType.DATE)
|
||||||
private Date retireDate;
|
private Date retireDate;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("福利享受截止时间")
|
||||||
|
@ColDefine(type = ColType.DATE)
|
||||||
|
private Date welfareStopDate;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("博士后进站时间")
|
@Comment("博士后进站时间")
|
||||||
@ColDefine(type = ColType.DATE)
|
@ColDefine(type = ColType.DATE)
|
||||||
@@ -454,4 +464,13 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@Comment("基金会员退出时间")
|
@Comment("基金会员退出时间")
|
||||||
@ColDefine(type = ColType.DATETIME)
|
@ColDefine(type = ColType.DATETIME)
|
||||||
private Date aidFundMemberQuitTime;
|
private Date aidFundMemberQuitTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人员数据治理同步保护标识:1 表示人工确认后的数据,自动同步时不得覆盖。
|
||||||
|
*/
|
||||||
|
@Column
|
||||||
|
@Comment("人员数据治理同步保护标识")
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Default(value = "0")
|
||||||
|
private Integer flag;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ import java.util.List;
|
|||||||
* Created by wizzer on 2016/12/23.
|
* Created by wizzer on 2016/12/23.
|
||||||
*/
|
*/
|
||||||
public interface SysConfigService extends BaseService<Sys_config> {
|
public interface SysConfigService extends BaseService<Sys_config> {
|
||||||
|
/** 仅控制登录页面短信入口的可见性,不作为后端短信接口的访问开关。 */
|
||||||
|
String SMS_LOGIN_VISIBLE_KEY = "AppSmsLoginEnabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补充缺失的短信登录入口参数,初始值为 false,已有管理员配置保持不变。
|
||||||
|
*
|
||||||
|
* @param createdBy 当前打开系统参数列表的管理员 ID,用于记录参数创建人
|
||||||
|
*/
|
||||||
|
void ensureSmsLoginVisibleConfig(String createdBy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取短信登录入口显示配置,不依赖异步更新的全局缓存。
|
||||||
|
*
|
||||||
|
* @return 仅参数值为 true 时返回 true;参数缺失、为空或非法时返回 false
|
||||||
|
*/
|
||||||
|
boolean isSmsLoginVisible();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询所有数据
|
* 查询所有数据
|
||||||
* @return
|
* @return
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ public interface SysDictService extends BaseService<Sys_dict> {
|
|||||||
*/
|
*/
|
||||||
void save(Sys_dict dict, String pid);
|
void save(Sys_dict dict, String pid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据字典,并对人员属性直属子项的人员类型和业务编码进行校验。
|
||||||
|
*
|
||||||
|
* @param dict 待更新字典;id 为必填项,personType 与 userAttributeCode 可为空
|
||||||
|
*/
|
||||||
|
void updateDict(Sys_dict dict);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存数据字典
|
* 保存数据字典
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,5 +3,17 @@ package com.budwk.app.sys.services;
|
|||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.sys.models.Sys_home_activity;
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface SysHomeActivityService extends BaseService<Sys_home_activity> {
|
public interface SysHomeActivityService extends BaseService<Sys_home_activity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定用户有权查看的已启用首页活动。
|
||||||
|
*
|
||||||
|
* @param userId 当前登录用户ID,用于人员分组及自定义SQL的可见范围校验
|
||||||
|
* @param includeEnded true包含已结束活动并按状态、置顶和开始时间排序;false保持原有过滤和排序
|
||||||
|
* @return 活动实体列表,包含id、name、cover、url、h5Url、startDate、endDate等展示字段,
|
||||||
|
* 不返回classPath和allowUserSql内部配置
|
||||||
|
*/
|
||||||
|
List<Sys_home_activity> listHomeActivity(String userId, boolean includeEnded);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.budwk.app.sys.services;
|
||||||
|
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页风采墙新闻服务。
|
||||||
|
*/
|
||||||
|
public interface SysHomeNewsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取风采墙可见栏目下的已发布新闻。
|
||||||
|
*
|
||||||
|
* @return 新闻栏目集合;每个栏目包含 label 栏目名称和 value 新闻列表,
|
||||||
|
* 新闻项包含 href 跳转地址、time 发布时间、text 新闻标题和 thumbUrl 缩略图地址(无图时为空)
|
||||||
|
*/
|
||||||
|
List<NutMap> getWebsiteNews();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* href 必须是首页返回的风采墙新闻链接;返回封面或正文首图地址,无图时为空字符串。
|
||||||
|
*/
|
||||||
|
String getNewsImage(String href);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.budwk.app.base.service.BaseService;
|
|||||||
import com.budwk.app.sys.models.Sys_menu;
|
import com.budwk.app.sys.models.Sys_menu;
|
||||||
import com.budwk.app.sys.models.Sys_role;
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
import com.budwk.app.sys.models.Sys_unit;
|
import com.budwk.app.sys.models.Sys_unit;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -128,6 +129,13 @@ public interface SysRoleService extends BaseService<Sys_role> {
|
|||||||
*/
|
*/
|
||||||
List<String> getRoleIdsByCode(List<RoleConstant> roleConstantList);
|
List<String> getRoleIdsByCode(List<RoleConstant> roleConstantList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为用户表中 member=1 的全部人员补齐启用的 MEMBER 角色,不受列表筛选影响。
|
||||||
|
*
|
||||||
|
* @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数;已有启用关联不重复添加
|
||||||
|
*/
|
||||||
|
NutMap settingMemberRole();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空缓存
|
* 清空缓存
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.budwk.app.base.service.BaseService;
|
|||||||
import com.budwk.app.sys.models.Sys_menu;
|
import com.budwk.app.sys.models.Sys_menu;
|
||||||
import com.budwk.app.sys.models.Sys_role;
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -155,14 +156,37 @@ public interface SysUserService extends BaseService<Sys_user> {
|
|||||||
Sys_user loginByPassword(String loginname, String passowrd) throws BaseException;
|
Sys_user loginByPassword(String loginname, String passowrd) throws BaseException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过短信验证码登录
|
* 根据手机号获取唯一可登录用户,并加载所属单位及分工会。
|
||||||
*
|
*
|
||||||
* @param mobile 手机号
|
* @param mobile 已在系统人员资料中登记的 11 位大陆手机号,允许首尾空格
|
||||||
* @return
|
* @return Sys_user,包含用户基本资料、unit 单位及 union 分工会关联
|
||||||
* @throws BaseException
|
* @throws BaseException 手机号格式不正确、未绑定用户、关联多条用户或账号已禁用
|
||||||
*/
|
*/
|
||||||
Sys_user loginByMobile(String mobile) throws BaseException;
|
Sys_user loginByMobile(String mobile) throws BaseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为已登记且唯一有效的用户发送登录短信验证码。
|
||||||
|
*
|
||||||
|
* @param mobile 系统人员资料中登记的 11 位大陆手机号
|
||||||
|
* @param captchaKey 滑块验证接口返回的 platformKey
|
||||||
|
* @param captchaCode 滑块验证接口返回的一次性 platformCaptcha 凭证
|
||||||
|
* @param request 当前 HTTP 请求,用于取得服务端接收到的客户端 IP 并限制发送频率
|
||||||
|
* @return NutMap,retryAfter 为再次发送前应等待的秒数,expireIn 为短信验证码有效秒数
|
||||||
|
* @throws BaseException 用户不可登录、滑块凭证错误、请求过于频繁或短信发送失败
|
||||||
|
*/
|
||||||
|
NutMap sendLoginSmsCode(String mobile, String captchaKey, String captchaCode, HttpServletRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验登录短信验证码并建立与账号密码登录一致的 Sa-Token 会话。
|
||||||
|
*
|
||||||
|
* @param mobile 系统人员资料中登记的 11 位大陆手机号
|
||||||
|
* @param smsCode 发送到该手机号的登录短信验证码
|
||||||
|
* @param request 当前 HTTP 请求,供既有登录流程记录 IP 及取得跳转参数
|
||||||
|
* @return String,登录成功后应访问的项目页面地址
|
||||||
|
* @throws BaseException 用户不可登录、关联资料缺失或短信验证码校验失败
|
||||||
|
*/
|
||||||
|
String loginBySmsCode(String mobile, String smsCode, HttpServletRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过用户名获取用户信息
|
* 通过用户名获取用户信息
|
||||||
* @param loginname
|
* @param loginname
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import com.budwk.app.base.service.impl.BaseServiceImpl;
|
|||||||
import com.budwk.app.base.utils.PageUtil;
|
import com.budwk.app.base.utils.PageUtil;
|
||||||
import com.budwk.app.sys.models.Sys_config;
|
import com.budwk.app.sys.models.Sys_config;
|
||||||
import com.budwk.app.sys.services.SysConfigService;
|
import com.budwk.app.sys.services.SysConfigService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.DaoException;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
@@ -24,6 +27,35 @@ public class SysConfigServiceImpl extends BaseServiceImpl<Sys_config> implements
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public synchronized void ensureSmsLoginVisibleConfig(String createdBy) {
|
||||||
|
if (fetch(SMS_LOGIN_VISIBLE_KEY) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Sys_config config = new Sys_config();
|
||||||
|
config.setConfigKey(SMS_LOGIN_VISIBLE_KEY);
|
||||||
|
config.setConfigValue("false");
|
||||||
|
config.setNote("是否开启短信登录(仅控制登录页入口显示)");
|
||||||
|
config.setCreatedBy(createdBy);
|
||||||
|
try {
|
||||||
|
insert(config);
|
||||||
|
} catch (DaoException e) {
|
||||||
|
// 多实例同时首次打开列表时,另一个实例可能已插入;已有值不能被默认值覆盖。
|
||||||
|
if (fetch(SMS_LOGIN_VISIBLE_KEY) == null) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public boolean isSmsLoginVisible() {
|
||||||
|
Sys_config config = fetch(SMS_LOGIN_VISIBLE_KEY);
|
||||||
|
return config != null && "true".equals(Strings.trim(config.getConfigValue()));
|
||||||
|
}
|
||||||
|
|
||||||
public List<Sys_config> getAllList() {
|
public List<Sys_config> getAllList() {
|
||||||
return this.query(Cnd.where("delFlag", "=", false));
|
return this.query(Cnd.where("delFlag", "=", false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
package com.budwk.app.sys.services.impl;
|
package com.budwk.app.sys.services.impl;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.core.collection.ListUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
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.JSONObject;
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import com.budwk.app.base.config.DataCenterProperties;
|
import com.budwk.app.base.config.DataCenterProperties;
|
||||||
import com.budwk.app.base.exception.BaseException;
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.sys.models.Sys_unit;
|
import com.budwk.app.sys.models.Sys_unit;
|
||||||
import com.budwk.app.sys.services.SysDataUnitPullService;
|
import com.budwk.app.sys.services.SysDataUnitPullService;
|
||||||
|
import com.budwk.app.sys.utils.DataCenterUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -42,7 +41,7 @@ import java.util.stream.Collectors;
|
|||||||
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
|
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
|
||||||
|
|
||||||
private static final String DMP_UNIT_KEY = "unit";
|
private static final String DMP_UNIT_KEY = "unit";
|
||||||
private static final int DMP_PAGE_SIZE = 1000;
|
private static final int BATCH_SIZE = 500;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private DataCenterProperties dataCenterProperties;
|
private DataCenterProperties dataCenterProperties;
|
||||||
@@ -64,58 +63,61 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void updateUnits(List<String> unitIds) {
|
public void updateUnits(List<String> unitIds) {
|
||||||
String accessToken = getDmpAccessToken();
|
List<Sys_unit> localUnits = query(Cnd.NEW());
|
||||||
List<JSONObject> rawDataList = distinctDmpUnits(pullDmpUnits(getDmpUnitUrl(), accessToken));
|
Sys_unit root = DataCenterUtil.findRootUnit(localUnits);
|
||||||
|
Map<String, Sys_unit> unitMap = localUnits.stream()
|
||||||
|
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
|
||||||
|
Set<String> existingUnitIds = new HashSet<>(unitMap.keySet());
|
||||||
|
Map<String, Sys_unit> changedUnits = new LinkedHashMap<>();
|
||||||
|
List<Sql> pathUpdates = new ArrayList<>();
|
||||||
|
List<JSONObject> rawDataList = distinctDmpUnits(DataCenterUtil.pullData(getDmpUnitUrl()));
|
||||||
|
if (rawDataList.isEmpty()) {
|
||||||
|
log.warn("当前未获取到有效单位数据,跳过本次机构同步");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(root.getPath())) {
|
||||||
|
root.setPath(getUnitPath("", unitMap));
|
||||||
|
changedUnits.put(root.getId(), root);
|
||||||
|
}
|
||||||
Set<String> parentCodes = rawDataList.stream()
|
Set<String> parentCodes = rawDataList.stream()
|
||||||
.map(this::getNormalizedParentCode)
|
.map(raw -> getNormalizedParentCode(raw, root.getId()))
|
||||||
.filter(StrUtil::isNotBlank)
|
.filter(StrUtil::isNotBlank)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
rawDataList.stream()
|
rawDataList.stream()
|
||||||
.sorted(Comparator.comparingInt(this::getUnitLevel))
|
.sorted(Comparator.comparingInt(this::getUnitLevel))
|
||||||
.forEach(raw -> saveOrUpdateUnit(raw, parentCodes));
|
.forEach(raw -> {
|
||||||
|
try {
|
||||||
|
prepareUnit(raw, parentCodes, root, unitMap, changedUnits, pathUpdates);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("单位同步失败,跳过当前机构,单位编码: {},上级编码: {},原因: {}", raw.getStr("DWDM"), raw.getStr("LSDWDM"), e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Sys_unit> insertUnits = new ArrayList<>();
|
||||||
|
List<Sys_unit> updateUnits = new ArrayList<>();
|
||||||
|
for (Sys_unit unit : changedUnits.values()) {
|
||||||
|
if (existingUnitIds.contains(unit.getId())) {
|
||||||
|
updateUnits.add(unit);
|
||||||
|
} else {
|
||||||
|
insertUnits.add(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (List<Sys_unit> batch : ListUtil.split(insertUnits, BATCH_SIZE)) {
|
||||||
|
dao().fastInsert(batch);
|
||||||
|
}
|
||||||
|
for (List<Sys_unit> batch : ListUtil.split(updateUnits, BATCH_SIZE)) {
|
||||||
|
dao().updateIgnoreNull(batch);
|
||||||
|
}
|
||||||
|
for (List<Sql> batch : ListUtil.split(pathUpdates, BATCH_SIZE)) {
|
||||||
|
dao().execute(batch.toArray(new Sql[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
dao().execute(Sqls.create("update sys_unit u left join (select parentId, count(*) as childCount from sys_unit where parentId is not null and id<>parentId group by parentId) children on children.parentId=u.id set u.hasChildren=(children.childCount is not null)"));
|
||||||
|
|
||||||
log.info("信息中心单位数据同步完成,本次接口返回单位数量: {},待检查单位编码数量: {}", rawDataList.size(), unitIds == null ? 0 : unitIds.size());
|
log.info("信息中心单位数据同步完成,本次接口返回单位数量: {},待检查单位编码数量: {}", rawDataList.size(), unitIds == null ? 0 : unitIds.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取信息中心开放平台访问令牌。
|
|
||||||
* 参数来自 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);
|
|
||||||
String accessToken = tokenJsonBody.getStr("access_token");
|
|
||||||
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("result") != null) {
|
|
||||||
accessToken = tokenJsonBody.getJSONObject("result").getStr("access_token");
|
|
||||||
}
|
|
||||||
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("data") != null) {
|
|
||||||
accessToken = tokenJsonBody.getJSONObject("data").getStr("access_token");
|
|
||||||
}
|
|
||||||
if (StrUtil.isBlank(accessToken)) {
|
|
||||||
accessToken = tokenJsonBody.getStr("token");
|
|
||||||
}
|
|
||||||
if (StrUtil.isBlank(accessToken)) {
|
|
||||||
throw new BaseException("获取信息中心token失败");
|
|
||||||
}
|
|
||||||
return accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取信息中心单位接口地址。
|
* 读取信息中心单位接口地址。
|
||||||
* 配置项为 data-center.urls.unit,返回值为单位全量接口URL。
|
* 配置项为 data-center.urls.unit,返回值为单位全量接口URL。
|
||||||
@@ -128,108 +130,88 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 分页拉取单位接口数据。
|
|
||||||
* 入参 url 为单位接口地址,accessToken 为信息中心令牌;
|
|
||||||
* 请求 body 包含 access_token、per_page、page;
|
|
||||||
* 返回值为 result.data 合并后的单位原始 JSON 列表。
|
|
||||||
*/
|
|
||||||
private List<JSONObject> pullDmpUnits(String url, String accessToken) {
|
|
||||||
List<JSONObject> rawDataList = new ArrayList<>();
|
|
||||||
int page = 1;
|
|
||||||
int maxPage = 1;
|
|
||||||
int total = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
HttpRequest httpRequest = HttpUtil.createPost(url);
|
|
||||||
httpRequest.header(Header.CONTENT_TYPE, "application/json");
|
|
||||||
httpRequest.body(JSONUtil.toJsonStr(Map.of(
|
|
||||||
"access_token", accessToken,
|
|
||||||
"per_page", String.valueOf(DMP_PAGE_SIZE),
|
|
||||||
"page", String.valueOf(page)
|
|
||||||
)));
|
|
||||||
|
|
||||||
log.info("请求信息中心单位数据,第{}页: {}", page, httpRequest);
|
|
||||||
String resBody = httpRequest.execute().body();
|
|
||||||
log.info("请求信息中心单位数据第{}页结果: {}", page, resBody);
|
|
||||||
JSONObject jsonBody = JSONUtil.parseObj(resBody);
|
|
||||||
|
|
||||||
if (jsonBody.getInt("code") != 10000) {
|
|
||||||
throw new BaseException("获取信息中心单位数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject result = jsonBody.getJSONObject("result");
|
|
||||||
if (result == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
total = result.getInt("total", total);
|
|
||||||
maxPage = result.getInt("max_page", maxPage);
|
|
||||||
JSONArray data = result.getJSONArray("data");
|
|
||||||
if (CollUtil.isNotEmpty(data)) {
|
|
||||||
rawDataList.addAll(data.stream().map(v -> (JSONObject) v).toList());
|
|
||||||
}
|
|
||||||
log.info("信息中心单位数据拉取进度: {}/{}", rawDataList.size(), total);
|
|
||||||
page++;
|
|
||||||
} while (page <= maxPage);
|
|
||||||
|
|
||||||
return rawDataList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按单位代码去重,避免接口分页或源数据重复导致同一 DWDM 重复写入。
|
* 按单位代码去重,避免接口分页或源数据重复导致同一 DWDM 重复写入。
|
||||||
*/
|
*/
|
||||||
private List<JSONObject> distinctDmpUnits(List<JSONObject> rawDataList) {
|
private List<JSONObject> distinctDmpUnits(List<JSONObject> rawDataList) {
|
||||||
Map<String, JSONObject> unitMap = new LinkedHashMap<>();
|
Map<String, JSONObject> unitMap = new LinkedHashMap<>();
|
||||||
for (JSONObject raw : rawDataList) {
|
for (JSONObject raw : rawDataList) {
|
||||||
String unitCode = raw.getStr("DWDM");
|
try {
|
||||||
if (StrUtil.isBlank(unitCode)) {
|
String unitCode = raw.getStr("DWDM");
|
||||||
continue;
|
if (StrUtil.isBlank(unitCode)) {
|
||||||
|
log.warn("单位接口记录缺少 DWDM,跳过当前机构,ROW_ID: {}", raw.get("ROW_ID"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (raw.getInt("DWCC") == null || raw.getInt("DWCC") < 1) {
|
||||||
|
log.warn("单位{}的 DWCC 无效,跳过当前机构,DWCC: {}", unitCode, raw.get("DWCC"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unitMap.putIfAbsent(unitCode, raw);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("单位接口记录解析失败,跳过当前机构,单位编码: {},原因: {}", raw == null ? null : raw.get("DWDM"), e.getMessage());
|
||||||
}
|
}
|
||||||
unitMap.putIfAbsent(unitCode, raw);
|
|
||||||
}
|
}
|
||||||
return new ArrayList<>(unitMap.values());
|
return new ArrayList<>(unitMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存或更新单位。
|
* 在内存中整理待新增、更新的单位及关联人员路径变更。
|
||||||
* DWDM 对应系统单位 id/unitcode,DWMC 对应名称,LSDWH 对应父级单位号,DWCC 对应信息中心原始单位层级。
|
* DWDM 对应系统单位 id/unitcode,DWMC 对应名称,LSDWDM 对应父级单位号,DWCC 对应信息中心原始单位层级。
|
||||||
*/
|
*/
|
||||||
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes) {
|
private void prepareUnit(JSONObject raw, Set<String> parentCodes, Sys_unit root,
|
||||||
|
Map<String, Sys_unit> unitMap, Map<String, Sys_unit> changedUnits, List<Sql> pathUpdates) {
|
||||||
String unitCode = raw.getStr("DWDM");
|
String unitCode = raw.getStr("DWDM");
|
||||||
if (StrUtil.isBlank(unitCode)) {
|
if (StrUtil.isBlank(unitCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String parentId = getParentId(unitCode, getNormalizedParentCode(raw));
|
if (root.getId().equals(unitCode)) {
|
||||||
Sys_unit unit = fetch(unitCode);
|
throw new BaseException("接口单位{}与本地顶级机构编号冲突,中止同步", unitCode);
|
||||||
if (unit == null) {
|
}
|
||||||
unit = new Sys_unit();
|
String parentId = getParentId(unitCode, getNormalizedParentCode(raw, root.getId()), root.getId(), unitMap);
|
||||||
|
Sys_unit existing = unitMap.get(unitCode);
|
||||||
|
Sys_unit unit = existing == null ? new Sys_unit() : BeanUtil.copyProperties(existing, Sys_unit.class);
|
||||||
|
setUnitValue(unit, raw, parentId, parentCodes);
|
||||||
|
if (existing == null) {
|
||||||
unit.setId(unitCode);
|
unit.setId(unitCode);
|
||||||
unit.setUnitcode(unitCode);
|
unit.setUnitcode(unitCode);
|
||||||
unit.setPath(getUnitPath(parentId));
|
unit.setPath(getUnitPath(parentId, unitMap));
|
||||||
setUnitValue(unit, raw, parentId, parentCodes);
|
|
||||||
dao().fastInsert(unit);
|
|
||||||
} else {
|
} else {
|
||||||
setUnitValue(unit, raw, parentId, parentCodes);
|
if (!parentId.equals(existing.getParentId()) || StrUtil.isBlank(existing.getPath())) {
|
||||||
if (StrUtil.isBlank(unit.getPath())) {
|
String oldPath = existing.getPath();
|
||||||
unit.setPath(getUnitPath(parentId));
|
String newPath = getUnitPath(parentId, unitMap);
|
||||||
|
if (StrUtil.isNotBlank(oldPath)) {
|
||||||
|
if (newPath.startsWith(oldPath)) {
|
||||||
|
throw new BaseException("单位{}不能移动到自身的下级单位", unitCode);
|
||||||
|
}
|
||||||
|
pathUpdates.add(Sqls.create("update sys_user set unitPath=concat(@newPath, substring(unitPath, @start)) where unitPath like @prefix")
|
||||||
|
.setParam("newPath", newPath).setParam("start", oldPath.length() + 1).setParam("prefix", oldPath + "%"));
|
||||||
|
pathUpdates.add(Sqls.create("update sys_user_source set unitPath=concat(@newPath, substring(unitPath, @start)) where unitPath like @prefix")
|
||||||
|
.setParam("newPath", newPath).setParam("start", oldPath.length() + 1).setParam("prefix", oldPath + "%"));
|
||||||
|
for (Sys_unit child : unitMap.values()) {
|
||||||
|
if (StrUtil.isNotBlank(child.getPath()) && child.getPath().startsWith(oldPath)) {
|
||||||
|
child.setPath(newPath + child.getPath().substring(oldPath.length()));
|
||||||
|
changedUnits.put(child.getId(), child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unit.setPath(newPath);
|
||||||
}
|
}
|
||||||
dao().updateIgnoreNull(unit);
|
|
||||||
}
|
}
|
||||||
|
unitMap.put(unitCode, unit);
|
||||||
|
changedUnits.put(unitCode, unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据接口字段给系统单位赋值。
|
* 根据接口字段给系统单位赋值。
|
||||||
* DWYXBS 表示单位是否有效,DWCC 表示信息中心原始单位层次;
|
* DWCC 表示信息中心原始单位层次;
|
||||||
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
|
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
|
||||||
*/
|
*/
|
||||||
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) {
|
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) {
|
||||||
String unitCode = raw.getStr("DWDM");
|
String unitCode = raw.getStr("DWDM");
|
||||||
String unitName = raw.getStr("DWMC");
|
String unitName = raw.getStr("DWMC");
|
||||||
String aliasName = StrUtil.blankToDefault(raw.getStr("DWJC"), unitName);
|
String aliasName = StrUtil.blankToDefault(raw.getStr("DWJC"), unitName);
|
||||||
Integer rawUnitLevel = raw.getInt("DWCC");
|
|
||||||
Integer unitLevel = getNormalizedUnitLevel(raw);
|
Integer unitLevel = getNormalizedUnitLevel(raw);
|
||||||
boolean availableBusinessUnit = "是".equals(raw.getStr("DWYXBS")) && Integer.valueOf(2).equals(rawUnitLevel);
|
|
||||||
|
|
||||||
unit.setParentId(parentId);
|
unit.setParentId(parentId);
|
||||||
unit.setName(unitName);
|
unit.setName(unitName);
|
||||||
@@ -237,48 +219,71 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
|||||||
unit.setUnitcode(unitCode);
|
unit.setUnitcode(unitCode);
|
||||||
unit.setAddress(raw.getStr("DWDZ"));
|
unit.setAddress(raw.getStr("DWDZ"));
|
||||||
unit.setUnitLevel(unitLevel);
|
unit.setUnitLevel(unitLevel);
|
||||||
unit.setUnitType(raw.getStr("DWLB"));
|
unit.setUnitType(raw.getStr("DWLBDM"));
|
||||||
unit.setUnitTypeCode(availableBusinessUnit ? 1 : 0);
|
unit.setUnitTypeCode(1);
|
||||||
unit.setHasChildren(parentCodes.contains(unitCode));
|
unit.setHasChildren(parentCodes.contains(unitCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算父级单位号。
|
* 计算父级单位号。
|
||||||
* LSDWH 为空、等于自身或本地不存在该父级时按根级处理;系统根单位 0 允许直接作为父级。
|
* 异常父级仅允许沿用本地已修正的关系,并确认父级链最终到达顶级机构。
|
||||||
*/
|
*/
|
||||||
private String getParentId(String unitCode, String parentCode) {
|
private String getParentId(String unitCode, String parentCode, String rootId, Map<String, Sys_unit> unitMap) {
|
||||||
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) {
|
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode) || !unitMap.containsKey(parentCode)) {
|
||||||
return "";
|
Sys_unit existing = unitMap.get(unitCode);
|
||||||
|
parentCode = existing == null ? null : existing.getParentId();
|
||||||
}
|
}
|
||||||
if ("0".equals(parentCode)) {
|
String ancestorId = parentCode;
|
||||||
return parentCode;
|
Set<String> visited = new HashSet<>();
|
||||||
|
visited.add(unitCode);
|
||||||
|
while (!rootId.equals(ancestorId)) {
|
||||||
|
if (StrUtil.isBlank(ancestorId) || !visited.add(ancestorId)) {
|
||||||
|
throw new BaseException("单位{}的上级机构无效或存在循环,无法确定归属", unitCode);
|
||||||
|
}
|
||||||
|
Sys_unit ancestor = unitMap.get(ancestorId);
|
||||||
|
if (ancestor == null) {
|
||||||
|
throw new BaseException("单位{}的上级机构{}不存在", unitCode, ancestorId);
|
||||||
|
}
|
||||||
|
ancestorId = ancestor.getParentId();
|
||||||
}
|
}
|
||||||
Sys_unit parentUnit = fetch(parentCode);
|
return parentCode;
|
||||||
return parentUnit == null ? "" : parentCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据父级 path 生成当前单位 path。
|
* 根据父级 path 生成当前单位 path。
|
||||||
* 父级为空时生成根节点 path,父级存在时在父级 path 下生成子节点 path。
|
* 父级为空时生成根节点 path,父级存在时在父级 path 下生成子节点 path。
|
||||||
*/
|
*/
|
||||||
private String getUnitPath(String parentId) {
|
private String getUnitPath(String parentId, Map<String, Sys_unit> unitMap) {
|
||||||
String parentPath = "";
|
String parentPath = "";
|
||||||
if (StrUtil.isNotBlank(parentId)) {
|
if (StrUtil.isNotBlank(parentId)) {
|
||||||
Sys_unit parentUnit = fetch(parentId);
|
Sys_unit parentUnit = unitMap.get(parentId);
|
||||||
parentPath = parentUnit == null ? "" : parentUnit.getPath();
|
if (parentUnit == null || StrUtil.isBlank(parentUnit.getPath())) {
|
||||||
|
throw new BaseException("父机构{}不存在或路径为空", parentId);
|
||||||
|
}
|
||||||
|
parentPath = parentUnit.getPath();
|
||||||
}
|
}
|
||||||
return getSubPath("sys_unit", "path", parentPath);
|
int maxIndex = 0;
|
||||||
|
for (Sys_unit unit : unitMap.values()) {
|
||||||
|
String path = unit.getPath();
|
||||||
|
if (StrUtil.isNotBlank(path) && path.startsWith(parentPath) && path.length() == parentPath.length() + 4) {
|
||||||
|
maxIndex = Math.max(maxIndex, Integer.parseInt(path.substring(parentPath.length())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxIndex >= 9999) {
|
||||||
|
throw new BaseException("父机构{}的子级路径编码已用尽", parentId);
|
||||||
|
}
|
||||||
|
return parentPath + StrUtil.padPre(String.valueOf(maxIndex + 1), 4, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 信息中心一级单位应挂在系统根单位 0 下,其余单位沿用接口返回的 LSDWH。
|
* 信息中心一级单位挂在本地顶级机构下,其余单位沿用接口返回的 LSDWDM。
|
||||||
*/
|
*/
|
||||||
private String getNormalizedParentCode(JSONObject raw) {
|
private String getNormalizedParentCode(JSONObject raw, String rootId) {
|
||||||
Integer rawUnitLevel = raw.getInt("DWCC");
|
Integer rawUnitLevel = raw.getInt("DWCC");
|
||||||
if (Integer.valueOf(1).equals(rawUnitLevel)) {
|
if (Integer.valueOf(1).equals(rawUnitLevel)) {
|
||||||
return "0";
|
return rootId;
|
||||||
}
|
}
|
||||||
return raw.getStr("LSDWH");
|
return raw.getStr("LSDWDM");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+15
-15
@@ -23,7 +23,6 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
|||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -134,7 +133,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
|
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
|
||||||
|
|
||||||
// 获取单位信息,后续判断单位变更
|
// 获取单位信息,后续判断单位变更
|
||||||
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1"));
|
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.NEW());
|
||||||
unitIdNameMap = unitList.stream().collect(Collectors.toMap(Sys_unit::getId, Sys_unit::getName));
|
unitIdNameMap = unitList.stream().collect(Collectors.toMap(Sys_unit::getId, Sys_unit::getName));
|
||||||
|
|
||||||
// 查询数据源
|
// 查询数据源
|
||||||
@@ -161,10 +160,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
for (Sys_user_source source : sources) {
|
for (Sys_user_source source : sources) {
|
||||||
Sys_user user = userMap.get(source.getLoginname());
|
Sys_user user = userMap.get(source.getLoginname());
|
||||||
source.setMember(null);
|
source.setMember(null);
|
||||||
String unitId = source.getUnitId();
|
|
||||||
if (StringUtils.isNotBlank(unitId) && unitId.length() == 5) {
|
|
||||||
source.setUnitId(unitId.substring(0, 3));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建用户对象
|
// 创建用户对象
|
||||||
Sys_user u = new Sys_user();
|
Sys_user u = new Sys_user();
|
||||||
@@ -180,12 +175,15 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
} else {
|
} else {
|
||||||
// 修改现有用户
|
// 修改现有用户
|
||||||
u.setId(user.getId());
|
u.setId(user.getId());
|
||||||
|
// 拉取数据未分配三级单位时写入空字符串,避免 updateIgnoreNull 保留人员原有的三级单位。
|
||||||
|
if (StrUtil.isBlank(source.getThreeUnitId())) {
|
||||||
|
u.setThreeUnitId("");
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
|
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
|
||||||
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
|
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
|
||||||
*/
|
*/
|
||||||
u.setAidFundMember(null);
|
u.setAidFundMember(null);
|
||||||
u.setPersonType(null);
|
|
||||||
needDoUpdateList.add(u);
|
needDoUpdateList.add(u);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +351,12 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
// 6. 处理其他任务
|
// 6. 处理其他任务
|
||||||
// 6.1 更新提案的校领导角色,发送订阅
|
// 6.1 更新提案的校领导角色,发送订阅
|
||||||
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
|
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
|
||||||
for (String unitId : config.getSchoolLeaderUnitIds()) {
|
if (config == null || config.getSchoolLeaderUnitIds() == null || config.getSchoolLeaderUnitIds().isEmpty()) {
|
||||||
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
|
log.warn("未配置提案校领导单位,跳过校领导角色刷新,继续完成人员全量更新");
|
||||||
|
} else {
|
||||||
|
for (String unitId : config.getSchoolLeaderUnitIds()) {
|
||||||
|
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -383,14 +385,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
Sql resetStatusSql = Sqls.create("""
|
Sql resetStatusSql = Sqls.create("""
|
||||||
UPDATE sys_user userInfo
|
UPDATE sys_user userInfo
|
||||||
SET userInfo.pendingConfirmStatus = @noneStatus
|
SET userInfo.pendingConfirmStatus = @noneStatus
|
||||||
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
|
WHERE userInfo.pendingConfirmStatus <> @noneStatus
|
||||||
AND userInfo.pendingConfirmStatus <> @noneStatus
|
AND userInfo.member='1'
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM sys_user_source sourceInfo
|
FROM sys_user_source sourceInfo
|
||||||
WHERE sourceInfo.pullTime = @pullTime
|
WHERE sourceInfo.pullTime = @pullTime
|
||||||
AND sourceInfo.loginname = userInfo.loginname
|
AND sourceInfo.loginname = userInfo.loginname
|
||||||
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
|
|
||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
resetStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
|
resetStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
|
||||||
@@ -400,14 +401,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
Sql pendingStatusSql = Sqls.create("""
|
Sql pendingStatusSql = Sqls.create("""
|
||||||
UPDATE sys_user userInfo
|
UPDATE sys_user userInfo
|
||||||
SET userInfo.pendingConfirmStatus = @pendingStatus
|
SET userInfo.pendingConfirmStatus = @pendingStatus
|
||||||
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
|
WHERE IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
|
||||||
AND IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
|
AND userInfo.member='1'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM sys_user_source sourceInfo
|
FROM sys_user_source sourceInfo
|
||||||
WHERE sourceInfo.pullTime = @pullTime
|
WHERE sourceInfo.pullTime = @pullTime
|
||||||
AND sourceInfo.loginname = userInfo.loginname
|
AND sourceInfo.loginname = userInfo.loginname
|
||||||
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
|
|
||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
pendingStatusSql.setParam("pendingStatus", PENDING_CONFIRM_STATUS_PENDING);
|
pendingStatusSql.setParam("pendingStatus", PENDING_CONFIRM_STATUS_PENDING);
|
||||||
|
|||||||
+78
-40
@@ -3,8 +3,6 @@ package com.budwk.app.sys.services.impl;
|
|||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.collection.ListUtil;
|
import cn.hutool.core.collection.ListUtil;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.thread.AsyncUtil;
|
|
||||||
import cn.hutool.core.thread.ThreadUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.event.role.RoleEventMsg;
|
import com.budwk.app.base.event.role.RoleEventMsg;
|
||||||
@@ -15,9 +13,12 @@ import com.budwk.app.sys.models.*;
|
|||||||
import com.budwk.app.sys.param.SysDataUserUpdateParam;
|
import com.budwk.app.sys.param.SysDataUserUpdateParam;
|
||||||
import com.budwk.app.sys.services.SysDataUserUpdateService;
|
import com.budwk.app.sys.services.SysDataUserUpdateService;
|
||||||
import com.budwk.app.sys.services.SysRoleService;
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.sys.services.SysUnitService;
|
||||||
|
import com.budwk.app.sys.utils.DataCenterUtil;
|
||||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -28,18 +29,21 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.lang.random.R;
|
import org.nutz.lang.random.R;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增量更新系统用户数据
|
* 增量更新系统用户数据
|
||||||
*/
|
*/
|
||||||
@IocBean
|
@IocBean
|
||||||
|
@Slf4j
|
||||||
public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateService {
|
public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateService {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
@Inject
|
@Inject
|
||||||
private SysRoleService sysRoleService;
|
private SysRoleService sysRoleService;
|
||||||
|
@Inject
|
||||||
|
private SysUnitService sysUnitService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@@ -58,6 +62,64 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
|||||||
// 执行查询
|
// 执行查询
|
||||||
List<Sys_user_source> userSources = dao.query(Sys_user_source.class, cnd);
|
List<Sys_user_source> userSources = dao.query(Sys_user_source.class, cnd);
|
||||||
|
|
||||||
|
// 先补齐机构,再写入人员;补建机构统一挂到本地唯一顶级机构。
|
||||||
|
List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.NEW());
|
||||||
|
var unitMap = sysUnits.stream().collect(java.util.stream.Collectors.toMap(Sys_unit::getId, v -> v));
|
||||||
|
List<Sys_unit> insertUnits = new ArrayList<>();
|
||||||
|
Sys_unit root = null;
|
||||||
|
int nextUnitPathIndex = 0;
|
||||||
|
for (var iterator = userSources.iterator(); iterator.hasNext();) {
|
||||||
|
Sys_user_source source = iterator.next();
|
||||||
|
if (StrUtil.isBlank(source.getUnitId())) {
|
||||||
|
log.warn("人员{}缺少所属机构编号,跳过当前人员增量更新", source.getLoginname());
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sys_unit unit = unitMap.get(source.getUnitId());
|
||||||
|
if (unit == null) {
|
||||||
|
if (StrUtil.isBlank(source.getUnitName())) {
|
||||||
|
log.warn("人员{}所属机构{}缺少名称,无法补建,跳过当前人员增量更新", source.getLoginname(), source.getUnitId());
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (root == null) {
|
||||||
|
root = DataCenterUtil.findRootUnit(sysUnits);
|
||||||
|
if (StrUtil.isBlank(root.getPath())) {
|
||||||
|
root.setPath(sysUnitService.getSubPath("sys_unit", "path", ""));
|
||||||
|
}
|
||||||
|
String nextUnitPath = sysUnitService.getSubPath("sys_unit", "path", root.getPath());
|
||||||
|
nextUnitPathIndex = Integer.parseInt(nextUnitPath.substring(root.getPath().length()));
|
||||||
|
}
|
||||||
|
unit = new Sys_unit();
|
||||||
|
unit.setId(source.getUnitId());
|
||||||
|
unit.setUnitcode(source.getUnitId());
|
||||||
|
unit.setName(source.getUnitName());
|
||||||
|
unit.setUnitLevel(2);
|
||||||
|
unit.setUnitTypeCode(1);
|
||||||
|
unit.setParentId(root.getId());
|
||||||
|
unit.setPath(root.getPath() + StrUtil.padPre(String.valueOf(nextUnitPathIndex++), 4, '0'));
|
||||||
|
unitMap.put(unit.getId(), unit);
|
||||||
|
insertUnits.add(unit);
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(unit.getPath())) {
|
||||||
|
log.warn("人员{}所属机构{}的路径为空,跳过当前人员增量更新", source.getLoginname(), unit.getId());
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
source.setUnitName(unit.getName());
|
||||||
|
source.setUnitPath(unit.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!insertUnits.isEmpty()) {
|
||||||
|
dao.fastInsert(insertUnits);
|
||||||
|
root.setHasChildren(true);
|
||||||
|
dao.updateIgnoreNull(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSources.isEmpty() && insertUnits.isEmpty()) {
|
||||||
|
return "本次没有新增人员或单位。";
|
||||||
|
}
|
||||||
|
|
||||||
//加入到系统用户表
|
//加入到系统用户表
|
||||||
List<Sys_user> sysUsers = BeanUtil.copyToList(userSources, Sys_user.class);
|
List<Sys_user> sysUsers = BeanUtil.copyToList(userSources, Sys_user.class);
|
||||||
for (Sys_user sysUser : sysUsers) {
|
for (Sys_user sysUser : sysUsers) {
|
||||||
@@ -72,9 +134,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<List<Sys_user>> splitSysUsers = ListUtil.split(sysUsers, 500);
|
List<List<Sys_user>> splitSysUsers = ListUtil.split(sysUsers, 500);
|
||||||
List<CompletableFuture<List<Sys_user>>> splitSysUsersFutures = splitSysUsers.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList();
|
for (List<Sys_user> batch : splitSysUsers) {
|
||||||
//等待任务全部完成
|
dao.fastInsert(batch);
|
||||||
AsyncUtil.waitAll(CompletableFuture.allOf(splitSysUsersFutures.toArray(new CompletableFuture[0])));
|
}
|
||||||
|
|
||||||
// 增量更新只处理新增人员,需要同步写入用户历史记录;列表的变更类型、变更时间通过 sys_user_history 最新记录展示。
|
// 增量更新只处理新增人员,需要同步写入用户历史记录;列表的变更类型、变更时间通过 sys_user_history 最新记录展示。
|
||||||
List<Sys_user_history> histories = BeanUtil.copyToList(userSources, Sys_user_history.class);
|
List<Sys_user_history> histories = BeanUtil.copyToList(userSources, Sys_user_history.class);
|
||||||
@@ -85,8 +147,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
|||||||
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
|
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
|
||||||
}
|
}
|
||||||
List<List<Sys_user_history>> splitHistories = ListUtil.split(histories, 500);
|
List<List<Sys_user_history>> splitHistories = ListUtil.split(histories, 500);
|
||||||
List<CompletableFuture<List<Sys_user_history>>> splitHistoryFutures = splitHistories.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList();
|
for (List<Sys_user_history> batch : splitHistories) {
|
||||||
AsyncUtil.waitAll(CompletableFuture.allOf(splitHistoryFutures.toArray(new CompletableFuture[0])));
|
dao.fastInsert(batch);
|
||||||
|
}
|
||||||
|
|
||||||
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
|
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
|
||||||
|
|
||||||
@@ -97,42 +160,17 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
|||||||
sysUserRole.setRoleId(publicRole.getId());
|
sysUserRole.setRoleId(publicRole.getId());
|
||||||
return sysUserRole;
|
return sysUserRole;
|
||||||
}).toList();
|
}).toList();
|
||||||
ThreadUtil.execAsync(() -> {
|
dao.insert(sysUserRoles);
|
||||||
dao.insert(sysUserRoles);
|
|
||||||
});
|
|
||||||
sysRoleService.clearCache();
|
sysRoleService.clearCache();
|
||||||
|
|
||||||
//比较单位数据
|
|
||||||
List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitLevel, "=", 2));
|
|
||||||
List<String> sysUnitIds = sysUnits.stream().map(Sys_unit::getId).toList();
|
|
||||||
|
|
||||||
//信息中心的数据 转为单位代码 ->单位名称 map
|
|
||||||
var unitIdNameMap = userSources.stream()
|
|
||||||
.filter(v -> StrUtil.isAllNotBlank(v.getUnitId(), v.getUnitName()))
|
|
||||||
.collect(java.util.stream.Collectors.toMap(
|
|
||||||
Sys_user_source::getUnitId,
|
|
||||||
Sys_user_source::getUnitName,
|
|
||||||
(existingValue, newValue) -> newValue));
|
|
||||||
|
|
||||||
//需要新增的单位
|
|
||||||
List<Sys_unit> insertUnits = unitIdNameMap.entrySet().stream()
|
|
||||||
.filter(entry -> !sysUnitIds.contains(entry.getKey()))
|
|
||||||
.map(entry -> {
|
|
||||||
Sys_unit sysUnit = new Sys_unit();
|
|
||||||
sysUnit.setId(entry.getKey());
|
|
||||||
sysUnit.setName(entry.getValue());
|
|
||||||
sysUnit.setParentId("1");
|
|
||||||
sysUnit.setUnitLevel(2);
|
|
||||||
return sysUnit;
|
|
||||||
}).toList();
|
|
||||||
ThreadUtil.execAsync(() -> {
|
|
||||||
dao.insert(insertUnits);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6.1 更新提案的校领导角色,发送订阅
|
// 6.1 更新提案的校领导角色,发送订阅
|
||||||
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
|
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
|
||||||
for (String unitId : config.getSchoolLeaderUnitIds()) {
|
if (config == null || config.getSchoolLeaderUnitIds() == null || config.getSchoolLeaderUnitIds().isEmpty()) {
|
||||||
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
|
log.warn("未配置提案校领导单位,跳过校领导角色刷新,继续完成人员增量更新");
|
||||||
|
} else {
|
||||||
|
for (String unitId : config.getSchoolLeaderUnitIds()) {
|
||||||
|
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sysUsers.isEmpty() && insertUnits.isEmpty()) {
|
if (sysUsers.isEmpty() && insertUnits.isEmpty()) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.budwk.app.sys.services.SysDataUserPullService;
|
|||||||
import com.budwk.app.sys.services.SysDictService;
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
import com.budwk.app.sys.utils.DataCenterUtil;
|
import com.budwk.app.sys.utils.DataCenterUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.nutz.aop.interceptor.async.Async;
|
import org.nutz.aop.interceptor.async.Async;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
@@ -120,18 +121,62 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
// 字典码表Map,key为父级编码,value为子级字典列表
|
// 字典码表Map,key为父级编码,value为子级字典列表
|
||||||
Map<String, List<Sys_data_dict>> dataDictMap = dataDictList.stream().collect(Collectors.groupingBy(Sys_data_dict::getParentCode));
|
Map<String, List<Sys_data_dict>> dataDictMap = dataDictList.stream().collect(Collectors.groupingBy(Sys_data_dict::getParentCode));
|
||||||
|
|
||||||
String accessToken = getDmpAccessToken();
|
|
||||||
List<JSONObject> rawDataList = new ArrayList<>();
|
List<JSONObject> rawDataList = new ArrayList<>();
|
||||||
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工"));
|
if (dataCenterProperties.isTokenEnabled()) {
|
||||||
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣"));
|
String accessToken = getDmpAccessToken();
|
||||||
|
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工"));
|
||||||
|
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣"));
|
||||||
|
} else {
|
||||||
|
rawDataList.addAll(DataCenterUtil.pullData(getDmpUrl(DMP_TEACHER_KEY)));
|
||||||
|
// 将浙师大字段转为现有中间表映射使用的键,保留Token模式的字段协议。
|
||||||
|
Map<String, String> fieldAliases = Map.of("XBMC", "XB", "SJHM", "YDDH",
|
||||||
|
"LXRQ", "LXNY", "DQZTMC", "DQZT", "YRFSDM", "RYLX", "SZDWDM", "SZDWH", "MZDM", "MZM");
|
||||||
|
for (JSONObject raw : rawDataList) {
|
||||||
|
fieldAliases.forEach((source, target) -> raw.set(target, raw.get(source)));
|
||||||
|
}
|
||||||
|
}
|
||||||
rawDataList = distinctDmpUsers(rawDataList);
|
rawDataList = distinctDmpUsers(rawDataList);
|
||||||
|
|
||||||
if (rawDataList.isEmpty()) {
|
if (rawDataList.isEmpty()) {
|
||||||
log.warn("当前未获取到人员数据");
|
throw new BaseException("当前未获取到人员数据,中止本次同步");
|
||||||
}
|
}
|
||||||
|
|
||||||
Date nowDate = new Date();
|
Date nowDate = new Date();
|
||||||
|
|
||||||
|
// 人员单位号先与单位表批量比对,缺失单位只同步一次,避免逐人查询单位表。
|
||||||
|
Set<String> sourceUnitIds = rawDataList.stream()
|
||||||
|
.map(raw -> raw.getStr("SZDWH"))
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<String, Sys_unit> unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
|
||||||
|
if (!unitMap.keySet().containsAll(sourceUnitIds)) {
|
||||||
|
log.info("单位需要更新,正在同步更新");
|
||||||
|
sysDataUnitPullService.updateUnits(new ArrayList<>(unitMap.keySet()));
|
||||||
|
unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按单位表的实际等级判断:三级单位为3、直属二级单位为2;单位号位数不参与判断。
|
||||||
|
Map<String, String> tertiaryParentUnitIds = new HashMap<>();
|
||||||
|
for (String sourceUnitId : sourceUnitIds) {
|
||||||
|
Sys_unit unit = unitMap.get(sourceUnitId);
|
||||||
|
if (unit == null) {
|
||||||
|
log.warn("人员单位号{}在单位表中不存在,保留信息中心原始单位号", sourceUnitId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Integer.valueOf(3).equals(unit.getUnitLevel())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sys_unit parentUnit = unitMap.get(unit.getParentId());
|
||||||
|
if (parentUnit == null || !Integer.valueOf(2).equals(parentUnit.getUnitLevel())) {
|
||||||
|
log.warn("三级单位{}缺少有效的二级父单位,保留信息中心原始单位号", sourceUnitId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tertiaryParentUnitIds.put(sourceUnitId, parentUnit.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Sys_unit> resolvedUnitMap = unitMap;
|
||||||
// 赋值
|
// 赋值
|
||||||
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
|
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
|
||||||
Sys_user_source sysUser = new Sys_user_source();
|
Sys_user_source sysUser = new Sys_user_source();
|
||||||
@@ -149,35 +194,53 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
.findFirst().orElse(new Sys_data_dict());
|
.findFirst().orElse(new Sys_data_dict());
|
||||||
mapping.field.set(sysUser, StrUtil.blankToDefault(sysDataDict.getName(), raw.getStr(mapping.key)));
|
mapping.field.set(sysUser, StrUtil.blankToDefault(sysDataDict.getName(), raw.getStr(mapping.key)));
|
||||||
} else {
|
} else {
|
||||||
// 特殊处理 根据身份证号获取性别和出生年月
|
if ("LXNY".equals(mapping.key)) {
|
||||||
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)));
|
mapping.field.set(sysUser, normalizeArrivalAtSchoolDate(raw.getStr(mapping.key)));
|
||||||
} else {
|
} else {
|
||||||
mapping.field.set(sysUser, raw.getStr(mapping.key));
|
mapping.field.set(sysUser, raw.getStr(mapping.key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (mapping.field.getType() == Date.class) {
|
} else if (mapping.field.getType() == Date.class) {
|
||||||
mapping.field.set(sysUser, raw.getDate(mapping.key));
|
String dateText = StrUtil.trim(raw.getStr(mapping.key));
|
||||||
|
if (StrUtil.isNotBlank(dateText)) {
|
||||||
|
mapping.field.set(sysUser, dateText.matches("\\d{8}")
|
||||||
|
? DateUtil.parse(dateText, "yyyyMMdd") : DateUtil.parse(dateText));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
log.error("设置字段值失败: {}", mapping.field.getName(), e);
|
log.error("设置字段值失败: {}", mapping.field.getName(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sysUser.getBirthday() == null && StrUtil.isNotBlank(sysUser.getIdCard())) {
|
||||||
|
try {
|
||||||
|
sysUser.setBirthday(IdcardUtil.getBirthDate(sysUser.getIdCard()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
sysUser.setBirthday(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
sysUser.setUnitName(raw.getStr("SZDW"));
|
sysUser.setUnitName(raw.getStr("SZDW"));
|
||||||
sysUser.setUnitId(resolveSourceUnitId(raw.getStr("SZDWH")));
|
String sourceUnitId = raw.getStr("SZDWH");
|
||||||
|
String parentUnitId = tertiaryParentUnitIds.get(sourceUnitId);
|
||||||
|
sysUser.setUnitId(StrUtil.blankToDefault(parentUnitId, sourceUnitId));
|
||||||
|
sysUser.setThreeUnitId(parentUnitId == null ? null : sourceUnitId);
|
||||||
|
Sys_unit resolvedUnit = resolvedUnitMap.get(sysUser.getUnitId());
|
||||||
|
if (resolvedUnit != null) {
|
||||||
|
sysUser.setUnitName(resolvedUnit.getName());
|
||||||
|
sysUser.setUnitPath(resolvedUnit.getPath());
|
||||||
|
}
|
||||||
|
if (resolvedUnit == null || StrUtil.isBlank(resolvedUnit.getPath())) {
|
||||||
|
log.warn("人员{}所属机构{}不存在或路径为空,保留当前机构号并继续拉取", sysUser.getLoginname(), sysUser.getUnitId());
|
||||||
|
}
|
||||||
|
|
||||||
// 设置拉取时间
|
// 设置拉取时间
|
||||||
sysUser.setPullTime(nowDate);
|
sysUser.setPullTime(nowDate);
|
||||||
|
if(StringUtils.isNotBlank(sysUser.getPersonType()) && sysUser.getPersonType().equals("教职工")){
|
||||||
|
sysUser.setUserAttribute("新增“教职工”");
|
||||||
|
}
|
||||||
|
if(StringUtils.isNotBlank(sysUser.getPersonType()) && sysUser.getPersonType().equals("劳务派遣人员")){
|
||||||
|
sysUser.setUserAttribute("劳务派遣人员");
|
||||||
|
}
|
||||||
|
|
||||||
return sysUser;
|
return sysUser;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -188,17 +251,6 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
dao().insert(latestSourceList);
|
dao().insert(latestSourceList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
|
|
||||||
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).filter(StrUtil::isNotBlank).distinct().toList();
|
|
||||||
Sql sql = Sqls.create("select id from sys_unit group by id");
|
|
||||||
sql.setCallback(Sqls.callback.strList());
|
|
||||||
dao().execute(sql);
|
|
||||||
List<String> unitIds = sql.getList(String.class);
|
|
||||||
|
|
||||||
if (!new HashSet<>(unitIds).containsAll(sourceUnitIds)) {
|
|
||||||
log.info("单位需要更新,正在同步更新");
|
|
||||||
sysDataUnitPullService.updateUnits(unitIds);
|
|
||||||
}
|
|
||||||
return nowDate;
|
return nowDate;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BaseException("数据拉取失败,{}", e.getMessage());
|
throw new BaseException("数据拉取失败,{}", e.getMessage());
|
||||||
@@ -223,7 +275,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
String[] parts = dateText.split("-");
|
String[] parts = dateText.split("-");
|
||||||
return parts[0] + "-" + StrUtil.padPre(parts[1], 2, '0') + "-01";
|
return parts[0] + "-" + StrUtil.padPre(parts[1], 2, '0') + "-01";
|
||||||
}
|
}
|
||||||
return dateText;
|
return DateUtil.formatDate(dateText.matches("\\d{8}")
|
||||||
|
? DateUtil.parse(dateText, "yyyyMMdd") : DateUtil.parse(dateText));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -337,27 +390,14 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
if (StrUtil.isBlank(loginName)) {
|
if (StrUtil.isBlank(loginName)) {
|
||||||
loginName = raw.getStr("ID");
|
loginName = raw.getStr("ID");
|
||||||
}
|
}
|
||||||
|
if (StrUtil.isBlank(raw.getStr("ZGH"))) {
|
||||||
|
throw new BaseException("人员接口存在缺少职工号 ZGH 的记录");
|
||||||
|
}
|
||||||
userMap.putIfAbsent(loginName, raw);
|
userMap.putIfAbsent(loginName, raw);
|
||||||
}
|
}
|
||||||
return new ArrayList<>(userMap.values());
|
return new ArrayList<>(userMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据接口返回的所在单位号转换数据源用户单位。
|
|
||||||
* 入参 rawUnitId 对应人员接口 SZDWH:5位表示三级单位,需要写入其二级父单位;其他长度保持接口原值。
|
|
||||||
* 返回值为 sys_user_source.unitId 使用的单位ID,找不到三级单位或父级为空时保留接口原始单位号,避免人员数据丢失。
|
|
||||||
*/
|
|
||||||
private String resolveSourceUnitId(String rawUnitId) {
|
|
||||||
if (StrUtil.isBlank(rawUnitId) || rawUnitId.length() != 5) {
|
|
||||||
return rawUnitId;
|
|
||||||
}
|
|
||||||
Sys_unit unit = dao().fetch(Sys_unit.class, rawUnitId);
|
|
||||||
if (unit == null || StrUtil.isBlank(unit.getParentId())) {
|
|
||||||
return rawUnitId;
|
|
||||||
}
|
|
||||||
return unit.getParentId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
private void updateDict(List<Sys_user_source> userSources) {
|
private void updateDict(List<Sys_user_source> userSources) {
|
||||||
//判断是否要更新在职状态字典
|
//判断是否要更新在职状态字典
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import java.util.Map;
|
|||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
@CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "sys_dict", isHash = true)
|
@CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "sys_dict", isHash = true)
|
||||||
public class SysDictServiceImpl extends BaseServiceImpl<Sys_dict> implements SysDictService {
|
public class SysDictServiceImpl extends BaseServiceImpl<Sys_dict> implements SysDictService {
|
||||||
|
private static final String USER_ATTRIBUTE_DICT_CODE = "USER_ATTRIBUTE";
|
||||||
|
private static final String USER_PERSON_TYPE_DICT_CODE = "USER_PERSON_TYPE";
|
||||||
|
|
||||||
public SysDictServiceImpl(Dao dao) {
|
public SysDictServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
@@ -132,18 +135,76 @@ public class SysDictServiceImpl extends BaseServiceImpl<Sys_dict> implements Sys
|
|||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void save(Sys_dict dict, String pid) {
|
public void save(Sys_dict dict, String pid) {
|
||||||
String path = "";
|
String path = "";
|
||||||
|
Sys_dict parentDict = null;
|
||||||
if (!Strings.isEmpty(pid)) {
|
if (!Strings.isEmpty(pid)) {
|
||||||
Sys_dict pp = this.fetch(pid);
|
parentDict = this.fetch(pid);
|
||||||
path = pp.getPath();
|
path = parentDict.getPath();
|
||||||
}
|
}
|
||||||
dict.setPath(getSubPath("sys_dict", "path", path));
|
dict.setPath(getSubPath("sys_dict", "path", path));
|
||||||
dict.setParentId(pid);
|
dict.setParentId(pid);
|
||||||
|
validateUserAttribute(dict, parentDict);
|
||||||
dao().insert(dict);
|
dao().insert(dict);
|
||||||
if (!Strings.isEmpty(pid)) {
|
if (!Strings.isEmpty(pid)) {
|
||||||
this.update(Chain.make("hasChildren", true), Cnd.where("id", "=", pid));
|
this.update(Chain.make("hasChildren", true), Cnd.where("id", "=", pid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新字典记录。人员属性关联字段允许为空;填写后校验人员类型有效性和业务编码唯一性。
|
||||||
|
*
|
||||||
|
* @param dict 待更新字典,id 用于定位原记录并在唯一性校验中排除自身
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void updateDict(Sys_dict dict) {
|
||||||
|
Sys_dict current = this.fetch(dict.getId());
|
||||||
|
if (current == null) {
|
||||||
|
throw new BaseException("字典数据不存在");
|
||||||
|
}
|
||||||
|
Sys_dict parentDict = Strings.isBlank(current.getParentId()) ? null : this.fetch(current.getParentId());
|
||||||
|
dict.setParentId(current.getParentId());
|
||||||
|
validateUserAttribute(dict, parentDict);
|
||||||
|
this.updateIgnoreNull(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 USER_ATTRIBUTE 直属子项的扩展关系。
|
||||||
|
* personType 为空时允许保存;有值时必须属于 USER_PERSON_TYPE 的启用子项。
|
||||||
|
* userAttributeCode 为空时允许保存;有值时在 USER_ATTRIBUTE 直属子项中必须唯一。
|
||||||
|
*/
|
||||||
|
private void validateUserAttribute(Sys_dict dict, Sys_dict parentDict) {
|
||||||
|
if (parentDict == null || !USER_ATTRIBUTE_DICT_CODE.equals(parentDict.getCode())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String personType = Strings.sNull(dict.getPersonType()).trim();
|
||||||
|
String userAttributeCode = Strings.sNull(dict.getUserAttributeCode()).trim();
|
||||||
|
dict.setPersonType(personType);
|
||||||
|
dict.setUserAttributeCode(userAttributeCode);
|
||||||
|
|
||||||
|
if (!Strings.isBlank(personType)) {
|
||||||
|
Sys_dict personTypeRoot = this.fetch(Cnd.where("code", "=", USER_PERSON_TYPE_DICT_CODE));
|
||||||
|
boolean personTypeValid = personTypeRoot != null && this.count(
|
||||||
|
Cnd.where("parentId", "=", personTypeRoot.getId())
|
||||||
|
.and("code", "=", personType)
|
||||||
|
.and("disabled", "=", false)
|
||||||
|
) > 0;
|
||||||
|
if (!personTypeValid) {
|
||||||
|
throw new BaseException("人员类型不存在或已禁用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Strings.isBlank(userAttributeCode)) {
|
||||||
|
Cnd codeCondition = Cnd.where("parentId", "=", parentDict.getId())
|
||||||
|
.and("userAttributeCode", "=", userAttributeCode);
|
||||||
|
if (!Strings.isBlank(dict.getId())) {
|
||||||
|
codeCondition.and("id", "!=", dict.getId());
|
||||||
|
}
|
||||||
|
if (this.count(codeCondition) > 0) {
|
||||||
|
throw new BaseException("人员属性编码已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void saveByParentCode(Sys_dict dict, String parentCode) {
|
public void saveByParentCode(Sys_dict dict, String parentCode) {
|
||||||
Sys_dict parentDict = fetch(Cnd.where("code", "=", parentCode));
|
Sys_dict parentDict = fetch(Cnd.where("code", "=", parentCode));
|
||||||
|
|||||||
@@ -1,15 +1,87 @@
|
|||||||
package com.budwk.app.sys.services.impl;
|
package com.budwk.app.sys.services.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.sys.models.Sys_home_activity;
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
import com.budwk.app.sys.services.SysHomeActivityService;
|
import com.budwk.app.sys.services.SysHomeActivityService;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.dao.FieldFilter;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.Daos;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.Lang;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class SysHomeActivityServiceImpl extends BaseServiceImpl<Sys_home_activity> implements SysHomeActivityService {
|
public class SysHomeActivityServiceImpl extends BaseServiceImpl<Sys_home_activity> implements SysHomeActivityService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ActivityBasicScopeService activityBasicScopeService;
|
||||||
|
|
||||||
public SysHomeActivityServiceImpl(Dao dao) {
|
public SysHomeActivityServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沿用首页原有的启用及人员可见范围校验,仅在调用方明确要求时展示已结束活动。
|
||||||
|
* 参数和返回结构见服务接口;查询过程不更新活动配置。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Sys_home_activity> listHomeActivity(String userId, boolean includeEnded) {
|
||||||
|
FieldFilter fieldFilter = FieldFilter.locked(Sys_home_activity.class, "classPath");
|
||||||
|
List<Sys_home_activity> list = Daos.ext(dao(), fieldFilter).query(Sys_home_activity.class,
|
||||||
|
Cnd.where("enable", "=", 1).desc("top"));
|
||||||
|
List<Sys_home_activity> allowActivityList = new ArrayList<>();
|
||||||
|
// 同一次请求使用同一个时间点,保证过滤与排序对活动是否结束的判断一致。
|
||||||
|
Date today = DateUtil.date();
|
||||||
|
for (Sys_home_activity activity : list) {
|
||||||
|
// 未传includeEnded的既有调用继续过滤已结束活动,保留原来的日期比较口径。
|
||||||
|
if (!includeEnded && DateUtil.compare(today, activity.getEndDate()) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer allowUserGroupId = activity.getAllowUserGroupId();
|
||||||
|
String allowUserSql = activity.getAllowUserSql();
|
||||||
|
// 自定义SQL仅用于服务端判断可见范围,禁止随活动列表返回前端。
|
||||||
|
activity.setAllowUserSql(null);
|
||||||
|
if (allowUserGroupId == null && StrUtil.isBlank(allowUserSql)) {
|
||||||
|
allowActivityList.add(activity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowUserGroupId != null) {
|
||||||
|
if (activityBasicScopeService.isUserInGroup(allowUserGroupId, userId)) {
|
||||||
|
allowActivityList.add(activity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(allowUserSql)) {
|
||||||
|
Sql sql = Sqls.create(allowUserSql).setParam("userId", userId);
|
||||||
|
sql.setCallback(Sqls.callback.map());
|
||||||
|
dao().execute(sql);
|
||||||
|
NutMap nutMap = (NutMap) sql.getResult();
|
||||||
|
if (Lang.isNotEmpty(nutMap) && StrUtil.isNotBlank(nutMap.getString("userId"))) {
|
||||||
|
allowActivityList.add(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (includeEnded) {
|
||||||
|
// 未结束活动整体在前,已结束置顶活动也不能越组;组内置顶优先,再按开始时间倒序,空开始时间排后。
|
||||||
|
allowActivityList.sort(Comparator
|
||||||
|
.comparing((Sys_home_activity activity) -> DateUtil.compare(today, activity.getEndDate()) > 0)
|
||||||
|
.thenComparing(activity -> Boolean.TRUE.equals(activity.getTop()), Comparator.reverseOrder())
|
||||||
|
.thenComparing(Sys_home_activity::getStartDate, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
|
}
|
||||||
|
return allowActivityList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.budwk.app.sys.services.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.budwk.app.sys.services.SysHomeNewsService;
|
||||||
|
import com.budwk.app.zhgh.spread.models.SpreadCategory;
|
||||||
|
import com.budwk.app.zhgh.spread.models.SpreadContent;
|
||||||
|
import com.budwk.app.zhgh.spread.service.SpreadCategoryService;
|
||||||
|
import com.budwk.app.zhgh.spread.service.SpreadContentService;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
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.Map;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页风采墙新闻服务实现,沿用首页新闻数据结构。
|
||||||
|
*/
|
||||||
|
@IocBean
|
||||||
|
public class SysHomeNewsServiceImpl implements SysHomeNewsService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SpreadCategoryService categoryService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SpreadContentService contentService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按风采墙顶级栏目返回已发布内容,保留首页现有新闻数据结构。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<NutMap> getWebsiteNews() {
|
||||||
|
List<SpreadCategory> categories = categoryService.listCategories(true);
|
||||||
|
List<NutMap> sections = new ArrayList<>();
|
||||||
|
for (SpreadCategory category : categories) {
|
||||||
|
if (!"0".equals(category.getParentId())) continue;
|
||||||
|
List<String> categoryIds = new ArrayList<>();
|
||||||
|
categoryIds.add(category.getId());
|
||||||
|
for (SpreadCategory child : categories) {
|
||||||
|
if (category.getId().equals(child.getParentId())) categoryIds.add(child.getId());
|
||||||
|
}
|
||||||
|
List<SpreadContent> contents = contentService.query(Cnd.where("delFlag", "=", false)
|
||||||
|
.and("isPublished", "=", 1).and("categoryId", "in", categoryIds)
|
||||||
|
.desc("isTop").desc("publishedAt").asc("sortOrder").desc("createdAt"));
|
||||||
|
List<NutMap> rows = new ArrayList<>();
|
||||||
|
for (SpreadContent content : contents) {
|
||||||
|
Long publishedAt = content.getPublishedAt();
|
||||||
|
rows.add(NutMap.NEW()
|
||||||
|
.addv("href", "/platform/spread/display?id=" + content.getId())
|
||||||
|
.addv("time", publishedAt == null ? "" : DateUtil.format(new java.util.Date(publishedAt), "yyyy-MM-dd"))
|
||||||
|
.addv("text", content.getTitle())
|
||||||
|
.addv("thumbUrl", getContentImage(content)));
|
||||||
|
}
|
||||||
|
sections.add(NutMap.NEW().addv("label", category.getName()).addv("value", rows));
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getContentImage(SpreadContent content) {
|
||||||
|
String image = "";
|
||||||
|
if (StrUtil.isNotBlank(content.getThumbUrl())) {
|
||||||
|
Object value = content.getThumbUrl().trim();
|
||||||
|
if (value.toString().startsWith("[") || value.toString().startsWith("{")) {
|
||||||
|
try {
|
||||||
|
value = Json.fromJson(value.toString());
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value instanceof List) {
|
||||||
|
List<?> files = (List<?>) value;
|
||||||
|
value = files.isEmpty() ? "" : files.get(0);
|
||||||
|
}
|
||||||
|
if (value instanceof Map) {
|
||||||
|
Map<?, ?> file = (Map<?, ?>) value;
|
||||||
|
value = file.get("src") != null ? file.get("src") : file.get("url");
|
||||||
|
}
|
||||||
|
image = value == null ? "" : value.toString().split(",", 2)[0].trim();
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(image) && StrUtil.isNotBlank(content.getContentBody())) {
|
||||||
|
Element first = Jsoup.parse(content.getContentBody()).selectFirst("img[src]");
|
||||||
|
if (first != null) image = first.attr("src");
|
||||||
|
}
|
||||||
|
return image.startsWith("/") && !image.startsWith("//")
|
||||||
|
|| image.startsWith("https://") || image.startsWith("http://") ? image : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅返回首页已发布风采内容的图片,不再请求外部新闻网站。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getNewsImage(String href) {
|
||||||
|
if (StrUtil.isBlank(href)) return "";
|
||||||
|
for (NutMap section : getWebsiteNews()) {
|
||||||
|
for (Object value : (List<?>) section.get("value")) {
|
||||||
|
NutMap news = (NutMap) value;
|
||||||
|
if (href.equals(news.getString("href"))) return news.getString("thumbUrl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import org.nutz.integration.jedis.JedisAgent;
|
|||||||
import org.nutz.integration.jedis.RedisService;
|
import org.nutz.integration.jedis.RedisService;
|
||||||
import org.nutz.integration.jedis.pubsub.PubSubService;
|
import org.nutz.integration.jedis.pubsub.PubSubService;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.impl.PropertiesProxy;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
import org.nutz.json.Json;
|
||||||
@@ -62,6 +63,8 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
|||||||
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
private ManyAddOrRenewUtil manyAddOrRenewUtil;
|
||||||
@Inject
|
@Inject
|
||||||
private SmsService smsService;
|
private SmsService smsService;
|
||||||
|
@Inject
|
||||||
|
private PropertiesProxy conf;
|
||||||
|
|
||||||
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
|
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
|
||||||
|
|
||||||
@@ -106,7 +109,10 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
|
|||||||
ThreadUtil.execute(() -> {
|
ThreadUtil.execute(() -> {
|
||||||
for (String loginName : loginNames) {
|
for (String loginName : loginNames) {
|
||||||
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
|
||||||
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
if (!(Strings.isNotBlank(conf.get("custom.msg.platform.app-id"))
|
||||||
|
&& conf.getBoolean("msg.platform.IntelligenceMode", false))) {
|
||||||
|
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.nutz.ioc.aop.Aop;
|
|||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheResult;
|
import org.nutz.plugins.wkcache.annotation.CacheResult;
|
||||||
@@ -284,4 +285,44 @@ public class SysRoleServiceImpl extends BaseServiceImpl<Sys_role> implements Sys
|
|||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量补齐会员角色。仅处理 member=1 的用户,保留其他角色及账号禁用状态。
|
||||||
|
*
|
||||||
|
* @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap settingMemberRole() {
|
||||||
|
// 锁定同一会员角色,使本接口的并发请求依次执行,避免重复插入用户角色关系。
|
||||||
|
Sql roleSql = Sqls.create("SELECT * FROM sys_role WHERE code=@code FOR UPDATE");
|
||||||
|
roleSql.setParam("code", RoleConstant.MEMBER.name());
|
||||||
|
roleSql.setEntity(dao().getEntity(Sys_role.class));
|
||||||
|
roleSql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(roleSql);
|
||||||
|
List<Sys_role> roles = roleSql.getList(Sys_role.class);
|
||||||
|
if (roles.isEmpty()) {
|
||||||
|
throw new BaseException("会员角色(MEMBER)不存在,请先配置会员角色");
|
||||||
|
}
|
||||||
|
Sys_role role = roles.get(0);
|
||||||
|
if (role.isDisabled()) {
|
||||||
|
throw new BaseException("会员角色(MEMBER)已禁用,请先启用会员角色");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史关联可能已停用或未填写 enable;恢复关联即可,无需重新插入。
|
||||||
|
Sql enableSql = Sqls.create("UPDATE sys_user_role SET enable=1 WHERE roleId=@roleId "
|
||||||
|
+ "AND (enable IS NULL OR enable<>1) "
|
||||||
|
+ "AND EXISTS (SELECT 1 FROM sys_user u WHERE u.id=sys_user_role.userId AND u.member=1)");
|
||||||
|
enableSql.setParam("roleId", role.getId());
|
||||||
|
dao().execute(enableSql);
|
||||||
|
|
||||||
|
// 按用户、角色判重,只为缺少会员关联的人员添加记录;不删除任何已有角色。
|
||||||
|
Sql insertSql = Sqls.create("INSERT INTO sys_user_role (userId,roleId,enable) "
|
||||||
|
+ "SELECT u.id,@roleId,1 FROM sys_user u WHERE u.member=1 "
|
||||||
|
+ "AND NOT EXISTS (SELECT 1 FROM sys_user_role ur WHERE ur.userId=u.id AND ur.roleId=@roleId)");
|
||||||
|
insertSql.setParam("roleId", role.getId());
|
||||||
|
dao().execute(insertSql);
|
||||||
|
return NutMap.NEW().addv("addedCount", insertSql.getUpdateCount())
|
||||||
|
.addv("enabledCount", enableSql.getUpdateCount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.budwk.app.sys.services.SysMenuService;
|
|||||||
import com.budwk.app.sys.services.SysRoleService;
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
import com.budwk.app.sys.services.SysUserService;
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
import com.budwk.app.sys.views.View_user;
|
import com.budwk.app.sys.views.View_user;
|
||||||
|
import com.budwk.app.web.commons.auth.service.ValidateService;
|
||||||
import com.budwk.app.web.commons.base.Globals;
|
import com.budwk.app.web.commons.base.Globals;
|
||||||
import com.budwk.app.zhgh.staffmanage.specialstaff.model.SpecialStaff;
|
import com.budwk.app.zhgh.staffmanage.specialstaff.model.SpecialStaff;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
@@ -29,11 +30,13 @@ import org.nutz.dao.Sqls;
|
|||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.integration.jedis.RedisService;
|
import org.nutz.integration.jedis.RedisService;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.impl.PropertiesProxy;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
import org.nutz.lang.random.R;
|
import org.nutz.lang.random.R;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheRemove;
|
import org.nutz.plugins.wkcache.annotation.CacheRemove;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
||||||
@@ -63,6 +66,10 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
|||||||
private SLogService sLogService;
|
private SLogService sLogService;
|
||||||
@Inject
|
@Inject
|
||||||
private RedisService redisService;
|
private RedisService redisService;
|
||||||
|
@Inject
|
||||||
|
private ValidateService validateService;
|
||||||
|
@Inject
|
||||||
|
private PropertiesProxy conf;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@CacheResult(cacheKey = "${userId}_getPermissionList")
|
@CacheResult(cacheKey = "${userId}_getPermissionList")
|
||||||
@@ -355,10 +362,9 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
|||||||
// base64解码密码
|
// base64解码密码
|
||||||
String decodePwd = Base64Decoder.decodeStr(passowrd);
|
String decodePwd = Base64Decoder.decodeStr(passowrd);
|
||||||
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
|
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
|
||||||
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
if (!"dev".equals(conf.get("nutz.profiles.active"))
|
||||||
if (Globals.sso) {
|
&& !Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
||||||
throw new BaseException("用户名或者密码不正确");
|
throw new BaseException("用户名或者密码不正确");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
user = this.fetchLinks(user, "unit");
|
user = this.fetchLinks(user, "unit");
|
||||||
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
|
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
|
||||||
@@ -368,16 +374,90 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按登记手机号确认唯一且未禁用的账号,补齐后续登录会话使用的组织关系。
|
||||||
|
*
|
||||||
|
* @param mobile 已登记的 11 位大陆手机号,查询前移除首尾空格
|
||||||
|
* @return Sys_user,包含 unit 单位和按单位关联的 union 分工会信息
|
||||||
|
* @throws BaseException 手机号无效、未登记、关联多条用户或账号已禁用
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Sys_user loginByMobile(String mobile) throws BaseException {
|
public Sys_user loginByMobile(String mobile) throws BaseException {
|
||||||
Sys_user user = this.fetch(Cnd.where("mobile", "=", mobile));
|
String normalizedMobile = Strings.sNull(mobile).trim();
|
||||||
if (user == null) {
|
if (!normalizedMobile.matches("^1[3-9]\\d{9}$")) {
|
||||||
throw new BaseException("用户不存在");
|
throw new BaseException("请输入正确的手机号码");
|
||||||
|
}
|
||||||
|
// 手机号不是唯一字段;任何重复关联都拒绝登录,避免任取账号或绕过禁用状态。
|
||||||
|
List<Sys_user> users = this.query(Cnd.where("mobile", "=", normalizedMobile));
|
||||||
|
if (users.isEmpty()) {
|
||||||
|
throw new BaseException("该手机号未绑定系统账号,请联系管理员");
|
||||||
|
}
|
||||||
|
if (users.size() > 1) {
|
||||||
|
throw new BaseException("该手机号关联多个账号,请联系管理员");
|
||||||
|
}
|
||||||
|
Sys_user user = users.get(0);
|
||||||
|
if (user.isDisabled()) {
|
||||||
|
throw new BaseException("用户被禁用");
|
||||||
}
|
}
|
||||||
user = this.fetchLinks(user, "unit");
|
user = this.fetchLinks(user, "unit");
|
||||||
|
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
|
||||||
|
user.setUnion(dao().fetch(Sys_union.class, user.getUnit().getUnionId()));
|
||||||
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 先确认手机号唯一且账号可用,再交由验证码服务校验滑块凭证、限流并实际发送短信。
|
||||||
|
*
|
||||||
|
* @param mobile 系统人员资料中登记的 11 位大陆手机号
|
||||||
|
* @param captchaKey 滑块验证接口返回的 platformKey
|
||||||
|
* @param captchaCode 滑块验证接口返回的一次性 platformCaptcha 凭证
|
||||||
|
* @param request 当前请求,以 getRemoteAddr 取得的地址作为服务端发送限流依据
|
||||||
|
* @return NutMap,retryAfter 为重发等待秒数,expireIn 为短信验证码有效秒数
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap sendLoginSmsCode(String mobile, String captchaKey, String captchaCode, HttpServletRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new BaseException("登录请求无效");
|
||||||
|
}
|
||||||
|
Sys_user user = this.loginByMobile(mobile);
|
||||||
|
return validateService.sendLoginSmsCode(user.getMobile(), captchaKey, captchaCode, request.getRemoteAddr(), request.getSession().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用短信验证码登录既有账号,验证成功后复用原有会话、日志和跳转处理。
|
||||||
|
*
|
||||||
|
* @param mobile 已登记且只关联一个账号的 11 位大陆手机号
|
||||||
|
* @param smsCode 登录短信中的验证码,成功校验后一次性消费
|
||||||
|
* @param request 当前请求,供既有登录流程记录客户端 IP 并读取跳转参数
|
||||||
|
* @return String,原有登录流程生成的登录成功跳转地址
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public String loginBySmsCode(String mobile, String smsCode, HttpServletRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new BaseException("登录请求无效");
|
||||||
|
}
|
||||||
|
Sys_user user = this.loginByMobile(mobile);
|
||||||
|
// loginPlus 会读取视图中的工会小组等资料,必须在建立登录态之前确认关联资料存在。
|
||||||
|
if (dao().fetch(View_user.class, Cnd.where("id", "=", user.getId())) == null) {
|
||||||
|
throw new BaseException("账号关联资料不完整,请联系管理员");
|
||||||
|
}
|
||||||
|
validateService.checkLoginSmsCode(user.getMobile(), smsCode);
|
||||||
|
try {
|
||||||
|
return this.loginPlus(user, LoginType.SMS_CODE, request);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// 仅在进入创建登录态的流程后清理;手机号或验证码校验失败不会注销原有登录。
|
||||||
|
try {
|
||||||
|
StpUtil.logout();
|
||||||
|
} catch (RuntimeException cleanupException) {
|
||||||
|
e.addSuppressed(cleanupException);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Sys_user loginByLoginName(String loginname) {
|
public Sys_user loginByLoginName(String loginname) {
|
||||||
Sys_user user = this.fetch(Cnd.where("loginname", "=", loginname));
|
Sys_user user = this.fetch(Cnd.where("loginname", "=", loginname));
|
||||||
@@ -446,6 +526,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public String loginPlus(Sys_user user, LoginType loginType, HttpServletRequest request) {
|
public String loginPlus(Sys_user user, LoginType loginType, HttpServletRequest request) {
|
||||||
String wxOpenId = request.getParameter("wxOpenId");
|
String wxOpenId = request.getParameter("wxOpenId");
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,18 @@ package com.budwk.app.sys.utils;
|
|||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
import cn.hutool.core.util.ClassUtil;
|
import cn.hutool.core.util.ClassUtil;
|
||||||
import cn.hutool.core.util.ReflectUtil;
|
import cn.hutool.core.util.ReflectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpResponse;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import cn.hutool.json.JSONArray;
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.sys.models.Sys_unit;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +26,52 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class DataCenterUtil {
|
public class DataCenterUtil {
|
||||||
|
|
||||||
|
public static Sys_unit findRootUnit(List<Sys_unit> units) {
|
||||||
|
boolean hasZeroUnit = units.stream().anyMatch(unit -> "0".equals(unit.getId()));
|
||||||
|
List<Sys_unit> roots = units.stream().filter(unit ->
|
||||||
|
StrUtil.isBlank(unit.getParentId())
|
||||||
|
|| ("0".equals(unit.getParentId()) && !hasZeroUnit)
|
||||||
|
|| (unit.getId().equals(unit.getParentId())
|
||||||
|
&& (unit.getUnitLevel() == null || unit.getUnitLevel() <= 1))).toList();
|
||||||
|
if (roots.size() != 1) {
|
||||||
|
throw new BaseException("本地顶级机构数量为{},无法确定唯一的学校根机构", roots.size());
|
||||||
|
}
|
||||||
|
return roots.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<JSONObject> pullData(String url) {
|
||||||
|
JSONObject firstResponse = requestData(url, Map.of("", ""));
|
||||||
|
JSONObject pageInfo = firstResponse.getJSONObject("pageInfo");
|
||||||
|
Integer pages = pageInfo == null ? null : pageInfo.getInt("pages");
|
||||||
|
if (pages == null || pages < 0) {
|
||||||
|
throw new BaseException("数据中心接口未返回有效的 pageInfo.pages");
|
||||||
|
}
|
||||||
|
List<JSONObject> records = new ArrayList<>();
|
||||||
|
for (int page = 1; page <= pages; page++) {
|
||||||
|
JSONObject response = requestData(url, Map.of("pageNum", page));
|
||||||
|
JSONArray data = response.getJSONArray("dataSet");
|
||||||
|
if (data == null) {
|
||||||
|
throw new BaseException("数据中心接口第{}页缺少 dataSet", page);
|
||||||
|
}
|
||||||
|
records.addAll(data.toList(JSONObject.class));
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject requestData(String url, Map<String, ?> body) {
|
||||||
|
try (HttpResponse response = HttpUtil.createPost(url)
|
||||||
|
.body(JSONUtil.toJsonStr(body)).execute()) {
|
||||||
|
if (!response.isOk()) {
|
||||||
|
throw new BaseException("数据中心接口请求失败,HTTP状态码:{}", response.getStatus());
|
||||||
|
}
|
||||||
|
JSONObject result = JSONUtil.parseObj(response.body());
|
||||||
|
if (!Integer.valueOf(1).equals(result.getInt("returnStatus"))) {
|
||||||
|
throw new BaseException("数据中心接口返回失败:{}", result.getStr("errorMsg"));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 映射字段
|
* 映射字段
|
||||||
* @param obj
|
* @param obj
|
||||||
|
|||||||
@@ -1,60 +1,35 @@
|
|||||||
package com.budwk.app.task.job.club;
|
package com.budwk.app.task.job.club;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.json.JSONObject;
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import org.quartz.DisallowConcurrentExecution;
|
||||||
import org.nutz.dao.Cnd;
|
|
||||||
import org.nutz.dao.Dao;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.quartz.Job;
|
import org.quartz.Job;
|
||||||
import org.quartz.JobExecutionContext;
|
import org.quartz.JobExecutionContext;
|
||||||
import org.quartz.JobExecutionException;
|
import org.quartz.JobExecutionException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubGeneratePayRecordJob
|
* @ClassName ClubGeneratePayRecordJob
|
||||||
* @Author JyuHsin
|
* @Author JyuHsin
|
||||||
* @Date 2026/1/22 16:56
|
* @Date 2026/1/22 16:56
|
||||||
* @Version 1.0
|
* @Version 1.0
|
||||||
* @Description 每年的1月1日生成社团成员下年的缴费记录
|
* @Description 每年的1月1日生成社团成员当年的缴费记录
|
||||||
*/
|
*/
|
||||||
@IocBean
|
@IocBean
|
||||||
|
@DisallowConcurrentExecution
|
||||||
public class ClubGeneratePayRecordJob implements Job {
|
public class ClubGeneratePayRecordJob implements Job {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private ClubPayRecordService payRecordService;
|
||||||
|
|
||||||
@Override
|
/** 2026年10月3日 09:05:37;操作人:wxg:生成当年完整快照名单,service 批量去重并在事务中保存。 */
|
||||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
@Override
|
||||||
// 当前年份
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
int thisYear = DateUtil.thisYear();
|
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||||
// 获取最新的社团成员信息
|
payRecordService.generateYearPayRecords(DateUtil.thisYear());
|
||||||
List<ClubUser> clubUserList = dao.query(ClubUser.class, Cnd.NEW());
|
}
|
||||||
// 生成缴费记录对象
|
|
||||||
List<ClubPayRecord> list = new ArrayList<>();
|
|
||||||
for (ClubUser clubUser : clubUserList) {
|
|
||||||
ClubPayRecord clubPayRecord = new ClubPayRecord();
|
|
||||||
clubPayRecord.setYear(thisYear);
|
|
||||||
clubPayRecord.setClubId(clubUser.getClubId());
|
|
||||||
clubPayRecord.setUserId(clubUser.getUserId());
|
|
||||||
clubPayRecord.setPayed(false);
|
|
||||||
|
|
||||||
JSONObject log = new JSONObject();
|
|
||||||
log.set("operatorUserId", "task");
|
|
||||||
log.set("operatorUserName", "定时任务");
|
|
||||||
log.set("operatorTime", DateUtil.now());
|
|
||||||
log.set("operatorType", "缴费管理-定时任务自动设置");
|
|
||||||
log.set("oldValue", null);
|
|
||||||
log.set("newValue", false);
|
|
||||||
clubPayRecord.setOperateLogs(List.of(log));
|
|
||||||
|
|
||||||
list.add(clubPayRecord);
|
|
||||||
}
|
|
||||||
dao.insert(list);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,30 @@ package com.budwk.app.web.commons.auth.service;
|
|||||||
import com.budwk.app.base.constant.RedisConstant;
|
import com.budwk.app.base.constant.RedisConstant;
|
||||||
import com.budwk.app.base.exception.BaseException;
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.utils.RsaUtil;
|
import com.budwk.app.base.utils.RsaUtil;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.message.service.TencentSmsService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.message.service.SliderCaptchaService;
|
||||||
import com.wf.captcha.ArithmeticCaptcha;
|
import com.wf.captcha.ArithmeticCaptcha;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.integration.jedis.JedisAgent;
|
||||||
import org.nutz.integration.jedis.RedisService;
|
import org.nutz.integration.jedis.RedisService;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.impl.PropertiesProxy;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
import org.nutz.lang.random.R;
|
import org.nutz.lang.random.R;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author wizzer@qq.com
|
* @author wizzer@qq.com
|
||||||
@@ -23,14 +34,45 @@ import java.util.Base64;
|
|||||||
@IocBean
|
@IocBean
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ValidateService {
|
public class ValidateService {
|
||||||
|
private static final int SMS_EXPIRE_SECONDS = 300;
|
||||||
|
// 同一手机号的重发等待时间与短信验证码有效期一致,发送成功后五分钟内禁止重发。
|
||||||
|
private static final int SMS_RETRY_SECONDS = 300;
|
||||||
|
private static final SecureRandom SMS_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
// 计数与有效期原子设置,避免并发发送绕过限流或产生永不过期的计数键。
|
||||||
|
private static final String HOUR_LIMIT_SCRIPT = "local count=redis.call('incr',KEYS[1]); "
|
||||||
|
+ "if count==1 then redis.call('expire',KEYS[1],3600) end; return count";
|
||||||
|
// 成功发送后才保存验证码;带号码 hash tag 的键可在同一个 Redis 分片原子更新。
|
||||||
|
private static final String SAVE_SMS_SCRIPT = "if redis.call('get',KEYS[1])~=ARGV[1] then return 0 end; "
|
||||||
|
+ "redis.call('set',KEYS[2],ARGV[2],'EX',ARGV[3]); redis.call('del',KEYS[3]); "
|
||||||
|
+ "redis.call('expire',KEYS[1],ARGV[4]); return 1";
|
||||||
|
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get',KEYS[1])==ARGV[1] "
|
||||||
|
+ "then return redis.call('del',KEYS[1]) else return 0 end";
|
||||||
|
// 校验、错误计数和消费必须原子完成,同一码只能有一个请求成功,五次输错后作废。
|
||||||
|
private static final String VERIFY_SMS_SCRIPT = "local code=redis.call('get',KEYS[1]); "
|
||||||
|
+ "if not code then return 0 end; if code~=ARGV[1] then "
|
||||||
|
+ "local count=redis.call('incr',KEYS[2]); local ttl=redis.call('ttl',KEYS[1]); "
|
||||||
|
+ "redis.call('expire',KEYS[2],math.max(ttl,1)); "
|
||||||
|
+ "if count>=5 then redis.call('del',KEYS[1],KEYS[2]); return -2 end; return -1 end; "
|
||||||
|
+ "redis.call('del',KEYS[1],KEYS[2]); return 1";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private RedisService redisService;
|
private RedisService redisService;
|
||||||
|
@Inject
|
||||||
|
private JedisAgent jedisAgent;
|
||||||
|
@Inject
|
||||||
|
private PropertiesProxy conf;
|
||||||
|
@Inject
|
||||||
|
private TencentSmsService tencentSmsService;
|
||||||
|
@Inject
|
||||||
|
private SliderCaptchaService sliderCaptchaService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取验证码
|
* 获取验证码
|
||||||
*
|
*
|
||||||
* @return
|
* @return key 为图形验证码标识,codeUrl 为可直接展示的 Base64 图片;验证码有效期为三分钟
|
||||||
*/
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public NutMap getCaptcha() {
|
public NutMap getCaptcha() {
|
||||||
String uuid = R.UU32();
|
String uuid = R.UU32();
|
||||||
ArithmeticCaptcha captcha = new ArithmeticCaptcha(120, 40);
|
ArithmeticCaptcha captcha = new ArithmeticCaptcha(120, 40);
|
||||||
@@ -41,21 +83,66 @@ public class ValidateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送短信验证码
|
* 校验发送条件后发送登录短信,只有腾讯云确认发送成功才写入有效验证码。
|
||||||
*
|
*
|
||||||
* @param mobile 手机号码
|
* @param mobile 已由用户 service 确认唯一绑定、未禁用的 11 位大陆手机号
|
||||||
* @throws BaseException
|
* @param captchaKey 滑块验证接口返回的 platformKey
|
||||||
|
* @param captchaCode 滑块验证接口返回的一次性 platformCaptcha 凭证
|
||||||
|
* @param sessionId 服务端当前 Servlet 会话 ID,不接受前端指定
|
||||||
|
* @param clientIp 请求连接的来源 IP,用于后端发送频控
|
||||||
|
* @return retryAfter 为重发等待秒数,expireIn 为本次短信验证码有效秒数,不返回验证码或 Redis 键
|
||||||
|
* @throws BaseException 参数无效、滑块凭证不正确、频控触发或短信发送失败
|
||||||
*/
|
*/
|
||||||
public void getSmsCode(String mobile) throws BaseException {
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
String text = R.captchaNumber(4);
|
public NutMap sendLoginSmsCode(String mobile, String captchaKey, String captchaCode, String clientIp, String sessionId) {
|
||||||
String codeFromRedis = redisService.get(RedisConstant.UCENTER_SMSCODE + mobile + ":LOCK");
|
if (mobile == null || !mobile.matches("^1[3-9]\\d{9}$") || Strings.isBlank(clientIp)) {
|
||||||
if (Strings.isNotBlank(codeFromRedis)) {
|
throw new BaseException("手机号或请求来源不正确");
|
||||||
throw new BaseException("请1分钟之后再试");
|
}
|
||||||
|
sliderCaptchaService.consume(sessionId, captchaKey, captchaCode, SliderCaptchaService.SMS, mobile);
|
||||||
|
tencentSmsService.checkLoginConfiguration();
|
||||||
|
String mobileKey = RedisConstant.LOGIN_SMSCODE + "{" + mobile + "}";
|
||||||
|
String lockKey = mobileKey + ":LOCK";
|
||||||
|
String lockToken = R.UU32();
|
||||||
|
try (Jedis jedis = jedisAgent.getResource()) {
|
||||||
|
if (!"OK".equals(jedis.set(lockKey, lockToken, "NX", "EX", SMS_RETRY_SECONDS))) {
|
||||||
|
throw new BaseException("验证码发送频繁,请5分钟后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean saved = false;
|
||||||
|
try {
|
||||||
|
try (Jedis jedis = jedisAgent.getResource()) {
|
||||||
|
long mobileCount = ((Number) jedis.eval(HOUR_LIMIT_SCRIPT,
|
||||||
|
Collections.singletonList(mobileKey + ":HOUR"), Collections.emptyList())).longValue();
|
||||||
|
long ipCount = ((Number) jedis.eval(HOUR_LIMIT_SCRIPT,
|
||||||
|
Collections.singletonList(RedisConstant.LOGIN_SMSCODE + "ip:" + clientIp), Collections.emptyList())).longValue();
|
||||||
|
if (mobileCount > Math.max(1, conf.getInt("sms.login.mobile-hour-limit", 10))
|
||||||
|
|| ipCount > Math.max(1, conf.getInt("sms.login.ip-hour-limit", 30))) {
|
||||||
|
throw new BaseException("验证码请求次数过多,请稍后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String code = String.format(Locale.ROOT, "%06d", SMS_RANDOM.nextInt(1000000));
|
||||||
|
tencentSmsService.sendLoginCode(mobile, code);
|
||||||
|
try (Jedis jedis = jedisAgent.getResource()) {
|
||||||
|
Number saveResult = (Number) jedis.eval(SAVE_SMS_SCRIPT,
|
||||||
|
Arrays.asList(lockKey, mobileKey, mobileKey + ":FAIL"),
|
||||||
|
Arrays.asList(lockToken, code, String.valueOf(SMS_EXPIRE_SECONDS), String.valueOf(SMS_RETRY_SECONDS)));
|
||||||
|
if (saveResult.longValue() != 1) {
|
||||||
|
throw new BaseException("短信验证码处理超时,请稍后重新获取");
|
||||||
|
}
|
||||||
|
saved = true;
|
||||||
|
}
|
||||||
|
return NutMap.NEW().addv("retryAfter", SMS_RETRY_SECONDS).addv("expireIn", SMS_EXPIRE_SECONDS);
|
||||||
|
} finally {
|
||||||
|
// 失败只释放本请求持有的锁,不覆盖旧验证码,也不删除其他请求的新发送锁。
|
||||||
|
if (!saved) {
|
||||||
|
try (Jedis jedis = jedisAgent.getResource()) {
|
||||||
|
jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(lockToken));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warn("登录短信发送锁释放失败,锁将在五分钟内自动过期");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// smsSendServer.sendCode(mobile, text);
|
|
||||||
log.debug("sms code:::" + text);
|
|
||||||
redisService.setex(RedisConstant.UCENTER_SMSCODE + mobile, 300, text);
|
|
||||||
redisService.setex(RedisConstant.UCENTER_SMSCODE + mobile + ":LOCK", 60, text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +152,7 @@ public class ValidateService {
|
|||||||
* @param code 验证码的值
|
* @param code 验证码的值
|
||||||
* @throws BaseException
|
* @throws BaseException
|
||||||
*/
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void checkCode(String key, String code) throws BaseException {
|
public void checkCode(String key, String code) throws BaseException {
|
||||||
String codeFromRedis = redisService.get(RedisConstant.UCENTER_CAPTCHA + key);
|
String codeFromRedis = redisService.get(RedisConstant.UCENTER_CAPTCHA + key);
|
||||||
|
|
||||||
@@ -81,27 +169,34 @@ public class ValidateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核验短信验证码
|
* 按服务端收到的手机号核验登录短信验证码,成功后原子删除,输错五次作废。
|
||||||
*
|
*
|
||||||
* @param mobile 手机号码
|
* @param mobile 本次登录的 11 位大陆手机号,与发送时的手机号完全一致
|
||||||
* @param code 验证码值
|
* @param code 用户输入的六位数字短信验证码
|
||||||
* @throws BaseException
|
* @throws BaseException 验证码为空、格式错误、不匹配、过期或已使用
|
||||||
*/
|
*/
|
||||||
public void checkSMSCode(String mobile, String code) throws BaseException {
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
String codeFromRedis = redisService.get(RedisConstant.UCENTER_SMSCODE + mobile);
|
public void checkLoginSmsCode(String mobile, String code) throws BaseException {
|
||||||
|
if (mobile == null || !mobile.matches("^1[3-9]\\d{9}$") || code == null || !code.matches("^\\d{6}$")) {
|
||||||
if (Strings.isBlank(code)) {
|
throw new BaseException("请输入正确的手机号和六位短信验证码");
|
||||||
throw new BaseException("请输入短信验证码");
|
|
||||||
}
|
}
|
||||||
if (Strings.isEmpty(codeFromRedis)) {
|
String mobileKey = RedisConstant.LOGIN_SMSCODE + "{" + mobile + "}";
|
||||||
throw new BaseException("短信验证码已过期");
|
try (Jedis jedis = jedisAgent.getResource()) {
|
||||||
|
long result = ((Number) jedis.eval(VERIFY_SMS_SCRIPT,
|
||||||
|
Arrays.asList(mobileKey, mobileKey + ":FAIL"), Collections.singletonList(code))).longValue();
|
||||||
|
if (result == 0) {
|
||||||
|
throw new BaseException("短信验证码已过期或已使用,请重新获取");
|
||||||
|
}
|
||||||
|
if (result == -2) {
|
||||||
|
throw new BaseException("短信验证码错误次数过多,请重新获取");
|
||||||
|
}
|
||||||
|
if (result != 1) {
|
||||||
|
throw new BaseException("短信验证码不正确");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!Strings.equalsIgnoreCase(code, codeFromRedis)) {
|
|
||||||
throw new BaseException("短信验证码不正确");
|
|
||||||
}
|
|
||||||
redisService.del(RedisConstant.UCENTER_SMSCODE + mobile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public String decryptPwd(String keyId, String password) throws Exception {
|
public String decryptPwd(String keyId, String password) throws Exception {
|
||||||
// 从 Redis 获取私钥
|
// 从 Redis 获取私钥
|
||||||
String rsaKey = RedisConstant.RSA_KEY_PREFIX + keyId;
|
String rsaKey = RedisConstant.RSA_KEY_PREFIX + keyId;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.budwk.app.web.commons.ext.handler;
|
|||||||
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
|
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
|
||||||
import org.apache.catalina.core.StandardContext;
|
import org.apache.catalina.core.StandardContext;
|
||||||
import org.apache.catalina.Executor;
|
import org.apache.catalina.Executor;
|
||||||
|
import org.apache.catalina.Wrapper;
|
||||||
import org.apache.catalina.core.StandardThreadExecutor;
|
import org.apache.catalina.core.StandardThreadExecutor;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
import org.apache.catalina.valves.ErrorReportValve;
|
import org.apache.catalina.valves.ErrorReportValve;
|
||||||
@@ -51,6 +52,11 @@ public class WkTomcatConfiguration implements NbAppEventListener {
|
|||||||
|
|
||||||
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
|
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
|
||||||
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
|
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
|
||||||
|
// 静态文本资源统一按 UTF-8 读取,避免服务器默认 GBK 与 UTF-8 响应编码不一致时破坏中文和正则。
|
||||||
|
Wrapper defaultServlet = (Wrapper) context.findChild("default");
|
||||||
|
defaultServlet.addInitParameter("fileEncoding", "UTF-8");
|
||||||
|
// 为静态 SVG 补齐响应类型,避免浏览器收到文件后仍无法按图片解码。
|
||||||
|
context.addMimeMapping("svg", "image/svg+xml");
|
||||||
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
|
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
|
||||||
context.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
|
context.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
|
||||||
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
|
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.log.Log;
|
import org.nutz.log.Log;
|
||||||
import org.nutz.log.Logs;
|
import org.nutz.log.Logs;
|
||||||
import org.nutz.plugins.mvc.websocket.AbstractWsEndpoint;
|
import org.nutz.plugins.mvc.websocket.AbstractWsEndpoint;
|
||||||
import org.nutz.plugins.mvc.websocket.NutWsConfigurator;
|
|
||||||
import org.nutz.plugins.mvc.websocket.WsHandler;
|
import org.nutz.plugins.mvc.websocket.WsHandler;
|
||||||
import redis.clients.jedis.*;
|
import redis.clients.jedis.*;
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ import javax.websocket.server.ServerEndpoint;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ServerEndpoint(value = "/websocket", configurator = NutWsConfigurator.class)
|
@ServerEndpoint(value = "/websocket", configurator = WkWsConfigurator.class)
|
||||||
@IocBean(create = "init") // 使用NutWsConfigurator的必备条件
|
@IocBean(create = "init") // 使用NutWsConfigurator的必备条件
|
||||||
public class WkWebSocket extends AbstractWsEndpoint implements PubSub {
|
public class WkWebSocket extends AbstractWsEndpoint implements PubSub {
|
||||||
protected static final Log log = Logs.get();
|
protected static final Log log = Logs.get();
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.budwk.app.web.commons.ext.websocket;
|
||||||
|
|
||||||
|
import org.nutz.plugins.mvc.websocket.NutWsConfigurator;
|
||||||
|
|
||||||
|
import javax.websocket.HandshakeResponse;
|
||||||
|
import javax.websocket.server.HandshakeRequest;
|
||||||
|
import javax.websocket.server.ServerEndpointConfig;
|
||||||
|
|
||||||
|
public class WkWsConfigurator extends NutWsConfigurator {
|
||||||
|
@Override
|
||||||
|
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
|
||||||
|
// WebSocket升级不一定经过Nutz请求链,只使用当前握手的会话,避免读取已回收的MVC请求。
|
||||||
|
Object session = request.getHttpSession();
|
||||||
|
if (session == null) {
|
||||||
|
config.getUserProperties().remove("HttpSession");
|
||||||
|
} else {
|
||||||
|
config.getUserProperties().put("HttpSession", session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,6 +163,15 @@ public class WkFailProcessor extends ViewProcessor {
|
|||||||
*/
|
*/
|
||||||
private void notLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws Throwable {
|
private void notLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws Throwable {
|
||||||
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
|
Assertion assertion = (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
|
||||||
|
if (Globals.sso) {
|
||||||
|
String tokenName = cn.dev33.satoken.stp.StpUtil.getTokenName();
|
||||||
|
boolean tokenCookiePresent = request.getCookies() != null && java.util.Arrays.stream(request.getCookies())
|
||||||
|
.anyMatch(cookie -> tokenName.equals(cookie.getName()) && StrUtil.isNotBlank(cookie.getValue()));
|
||||||
|
log.infof("[CAS诊断] 未登录跳转 uri=%s, sessionAssertion=%s, requestedSessionValid=%s, tokenCookiePresent=%s, tokenHeaderPresent=%s, tokenParameterPresent=%s, secure=%s, appDomain=%s",
|
||||||
|
request.getRequestURI(), assertion != null && assertion.getPrincipal() != null,
|
||||||
|
request.isRequestedSessionIdValid(), tokenCookiePresent, StrUtil.isNotBlank(request.getHeader(tokenName)),
|
||||||
|
StrUtil.isNotBlank(request.getParameter(tokenName)), request.isSecure(), Globals.AppDomain);
|
||||||
|
}
|
||||||
if (assertion != null && assertion.getPrincipal() != null) {
|
if (assertion != null && assertion.getPrincipal() != null) {
|
||||||
// //判断cas里面是否有用户名 那么造成的原因就是系统中没他的信息
|
// //判断cas里面是否有用户名 那么造成的原因就是系统中没他的信息
|
||||||
// //就是cas已经认证了 但是本地系统没有认证
|
// //就是cas已经认证了 但是本地系统没有认证
|
||||||
@@ -188,7 +197,10 @@ public class WkFailProcessor extends ViewProcessor {
|
|||||||
|
|
||||||
if (Globals.sso) {
|
if (Globals.sso) {
|
||||||
//1.判断是否为企业微信浏览器并且对接了企业微信
|
//1.判断是否为企业微信浏览器并且对接了企业微信
|
||||||
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
|
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)
|
||||||
|
&& "/platform/qywechat/getCode".equals(propertiesProxy.get("wxwork.redirectUri"))) {
|
||||||
|
new ServerRedirectView("/platform/qywechat/oauth?redirect=" + redirect).render(request, response, null);
|
||||||
|
} else if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
|
||||||
//进行企业微信授权
|
//进行企业微信授权
|
||||||
// String token = getWxWorkToken();
|
// String token = getWxWorkToken();
|
||||||
// if (StrUtil.isNotBlank(token)) {
|
// if (StrUtil.isNotBlank(token)) {
|
||||||
@@ -203,7 +215,7 @@ public class WkFailProcessor extends ViewProcessor {
|
|||||||
// new ServerRedirectView(authUrl).render(request, response, null);
|
// new ServerRedirectView(authUrl).render(request, response, null);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 武汉城市职业学院 跳转到企业微信cas登录页
|
// 浙江师范大学 跳转到企业微信cas登录页
|
||||||
String callBackUrl = propertiesProxy.get("wecom.wecom-client-host-url") + propertiesProxy.get("wecom.wecom-client-call-back-url") + "?redirect=" + redirect;
|
String callBackUrl = propertiesProxy.get("wecom.wecom-client-host-url") + propertiesProxy.get("wecom.wecom-client-call-back-url") + "?redirect=" + redirect;
|
||||||
String authUrl = propertiesProxy.get("wecom.wecom-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
|
String authUrl = propertiesProxy.get("wecom.wecom-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
|
||||||
log.info("auth:"+authUrl);
|
log.info("auth:"+authUrl);
|
||||||
|
|||||||
+10
-2
@@ -54,9 +54,8 @@ public class ActivityBasicSettingsController {
|
|||||||
if (parentId != null) {
|
if (parentId != null) {
|
||||||
parentId.setChild(child(parentId.getId()));
|
parentId.setChild(child(parentId.getId()));
|
||||||
list.add(parentId);
|
list.add(parentId);
|
||||||
return Result.success().addData(list);
|
|
||||||
}
|
}
|
||||||
return Result.success();
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ActivityBasicSettings> child(String parentId) {
|
private List<ActivityBasicSettings> child(String parentId) {
|
||||||
@@ -73,6 +72,9 @@ public class ActivityBasicSettingsController {
|
|||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
if (StrUtil.isBlank(parentId)) {
|
if (StrUtil.isBlank(parentId)) {
|
||||||
ActivityBasicSettings rootSetting = dao.fetch(ActivityBasicSettings.class, Cnd.where("parentId", "=", 0));
|
ActivityBasicSettings rootSetting = dao.fetch(ActivityBasicSettings.class, Cnd.where("parentId", "=", 0));
|
||||||
|
if (rootSetting == null) {
|
||||||
|
return Result.success(new Pagination<ActivityBasicSettings>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<ActivityBasicSettings>()));
|
||||||
|
}
|
||||||
cnd.and("parentId", "=", rootSetting.getId());
|
cnd.and("parentId", "=", rootSetting.getId());
|
||||||
} else {
|
} else {
|
||||||
cnd.and("parentId", "=", parentId);
|
cnd.and("parentId", "=", parentId);
|
||||||
@@ -116,7 +118,13 @@ public class ActivityBasicSettingsController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
public Result getActivityTwoLevelType(String code) {
|
public Result getActivityTwoLevelType(String code) {
|
||||||
|
if (StrUtil.isBlank(code)) {
|
||||||
|
return Result.success(new ArrayList<ActivityBasicSettings>());
|
||||||
|
}
|
||||||
ActivityBasicSettings fetch = dao.fetch(ActivityBasicSettings.class, Cnd.where("code", "=", code));
|
ActivityBasicSettings fetch = dao.fetch(ActivityBasicSettings.class, Cnd.where("code", "=", code));
|
||||||
|
if (fetch == null) {
|
||||||
|
return Result.success(new ArrayList<ActivityBasicSettings>());
|
||||||
|
}
|
||||||
List<ActivityBasicSettings> query = dao.query(ActivityBasicSettings.class, Cnd.where("parentId", "=", fetch.getId()).asc("code"));
|
List<ActivityBasicSettings> query = dao.query(ActivityBasicSettings.class, Cnd.where("parentId", "=", fetch.getId()).asc("code"));
|
||||||
return Result.success(query);
|
return Result.success(query);
|
||||||
}
|
}
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.basic.service;
|
||||||
|
|
||||||
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动工会资料同步及组成单位维护,保留活动模块独立维护的临时分工会。
|
||||||
|
*/
|
||||||
|
public interface ActivityBasicUnionService extends BaseService<ActivityBasicUnion> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无请求参数,按 sys_union.id 新增缺少的活动工会,仅更新已有非临时工会的名称,不删除记录。
|
||||||
|
*
|
||||||
|
* @return NutMap;insertCount 为新增工会数,updateCount 为名称实际变更的工会数
|
||||||
|
*/
|
||||||
|
NutMap syncUnions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全部二级活动单位,不按现有分工会归属过滤候选项。
|
||||||
|
*
|
||||||
|
* @param unionId 当前活动分工会ID,用于回显已选组成单位
|
||||||
|
* @return NutMap;allUnits 为二级单位列表,selectUnitIds 为当前分工会已选二级单位ID列表
|
||||||
|
*/
|
||||||
|
NutMap getPartUnitTransferData(String unionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前活动分工会的组成单位,选中单位允许从其他活动分工会转入。
|
||||||
|
*
|
||||||
|
* @param unionId 当前活动分工会ID
|
||||||
|
* @param unitIds 二级活动单位ID数组,传空数组或 null 表示清空当前工会组成单位
|
||||||
|
* @return 无返回值;包含不存在或非二级单位的ID时抛出异常,不修改归属
|
||||||
|
*/
|
||||||
|
void setPartUnits(String unionId, String[] unitIds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.basic.service;
|
||||||
|
|
||||||
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动单位基础资料同步,活动工会归属仍由活动模块独立维护。
|
||||||
|
*/
|
||||||
|
public interface ActivityBasicUnitService extends BaseService<ActivityBasicUnit> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将系统单位同步到活动单位,不删除活动单位,不覆盖已有活动单位的工会归属。
|
||||||
|
*
|
||||||
|
* @return NutMap;insertCount 为新增单位数,updateCount 为更新基础资料的已有单位数,
|
||||||
|
* pendingUnionCount 为新增单位中未能匹配活动工会、需要配置归属的数量
|
||||||
|
*/
|
||||||
|
NutMap syncUnits();
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.basic.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.sys.models.Sys_union;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicUnionService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@IocBean(args = {"refer:dao"})
|
||||||
|
public class ActivityBasicUnionServiceImpl extends BaseServiceImpl<ActivityBasicUnion> implements ActivityBasicUnionService {
|
||||||
|
|
||||||
|
public ActivityBasicUnionServiceImpl(Dao dao) {
|
||||||
|
super(dao);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无请求参数,按来源工会ID同步全部工会组织;返回新增数和名称实际变更数。
|
||||||
|
* 已有工会只更新名称及修改审计信息,不覆盖编码、联系方式、校区、组成单位和管理员关系。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap syncUnions() {
|
||||||
|
List<Sys_union> sourceUnions = dao().query(Sys_union.class, Cnd.NEW());
|
||||||
|
Map<String, ActivityBasicUnion> existingUnions = dao().query(ActivityBasicUnion.class, Cnd.NEW()).stream()
|
||||||
|
.collect(Collectors.toMap(ActivityBasicUnion::getId, union -> union));
|
||||||
|
String operatorId = SecurityUtil.getUserId();
|
||||||
|
long updateTime = System.currentTimeMillis();
|
||||||
|
int insertCount = 0;
|
||||||
|
int updateCount = 0;
|
||||||
|
|
||||||
|
for (Sys_union sourceUnion : sourceUnions) {
|
||||||
|
if (StrUtil.isBlank(sourceUnion.getId())) {
|
||||||
|
throw new BaseException("工会组织存在空ID,本次同步已取消,请先检查工会组织资料");
|
||||||
|
}
|
||||||
|
ActivityBasicUnion existingUnion = existingUnions.get(sourceUnion.getId());
|
||||||
|
if (existingUnion == null) {
|
||||||
|
// 按列插入以避开实体UUID生成拦截器,保留来源ID,保证重复同步不会重复新增。
|
||||||
|
dao().insert(ActivityBasicUnion.class, Chain.make("id", sourceUnion.getId())
|
||||||
|
.add("name", sourceUnion.getName()).add("unionCode", sourceUnion.getUnionCode())
|
||||||
|
.add("telephone", sourceUnion.getTelephone()).add("campus", sourceUnion.getCampus())
|
||||||
|
.add("source", "sys").add("createdBy", operatorId).add("createdAt", updateTime)
|
||||||
|
.add("updatedBy", operatorId).add("updatedAt", updateTime).add("delFlag", false));
|
||||||
|
insertCount++;
|
||||||
|
} else if (!"activity".equals(existingUnion.getSource())
|
||||||
|
&& !Objects.equals(existingUnion.getName(), sourceUnion.getName())) {
|
||||||
|
// 临时分工会即使与来源ID意外相同也不覆盖;来源中已不存在的工会自然保留。
|
||||||
|
dao().update(ActivityBasicUnion.class, Chain.make("name", sourceUnion.getName())
|
||||||
|
.add("updatedBy", operatorId).add("updatedAt", updateTime),
|
||||||
|
Cnd.where("id", "=", sourceUnion.getId()));
|
||||||
|
updateCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NutMap.NEW().addv("insertCount", insertCount).addv("updateCount", updateCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按单位层级查询全部二级活动单位,单位类别和现有工会归属不影响候选范围。
|
||||||
|
*
|
||||||
|
* @param unionId 当前活动分工会ID,仅用于计算回显的选中项
|
||||||
|
* @return NutMap;allUnits 为候选单位实体列表,selectUnitIds 为当前工会已选二级单位ID列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NutMap getPartUnitTransferData(String unionId) {
|
||||||
|
List<ActivityBasicUnit> units = dao().query(ActivityBasicUnit.class,
|
||||||
|
Cnd.where("unitLevel", "=", 2).asc("unitcode"));
|
||||||
|
List<String> selectUnitIds = units.stream()
|
||||||
|
.filter(unit -> Objects.equals(unit.getUnionId(), unionId))
|
||||||
|
.map(ActivityBasicUnit::getId).toList();
|
||||||
|
return NutMap.NEW().addv("selectUnitIds", selectUnitIds).addv("allUnits", units);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验全部选中单位均为二级单位后,替换当前活动分工会的组成单位关系。
|
||||||
|
*
|
||||||
|
* @param unionId 当前活动分工会ID
|
||||||
|
* @param unitIds 二级活动单位ID数组,空数组或 null 表示清空组成单位
|
||||||
|
* @return 无返回值;无效单位参数在修改归属之前抛出异常
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void setPartUnits(String unionId, String[] unitIds) {
|
||||||
|
List<String> selectedIds = unitIds == null ? List.of() : Arrays.stream(unitIds).distinct().toList();
|
||||||
|
if (!selectedIds.isEmpty()) {
|
||||||
|
// 必须先完整校验再解除原归属,防止提交三级、空ID或不存在的单位时丢失已有组成单位。
|
||||||
|
if (selectedIds.stream().anyMatch(StrUtil::isBlank)
|
||||||
|
|| dao().count(ActivityBasicUnit.class,
|
||||||
|
Cnd.where("id", "in", selectedIds).and("unitLevel", "=", 2)) != selectedIds.size()) {
|
||||||
|
throw new BaseException("组成单位只能选择有效的二级单位,请刷新后重新选择");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 保存时替换当前组成单位,历史误选的三级单位也随本次重新设置解除关联。
|
||||||
|
dao().update(ActivityBasicUnit.class, Chain.make("unionId", null), Cnd.where("unionId", "=", unionId));
|
||||||
|
if (!selectedIds.isEmpty()) {
|
||||||
|
// 允许将已属于其他活动分工会的二级单位转入,不增加原工会归属限制。
|
||||||
|
dao().update(ActivityBasicUnit.class, Chain.make("unionId", unionId),
|
||||||
|
Cnd.where("id", "in", selectedIds).and("unitLevel", "=", 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.basic.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
import com.budwk.app.sys.models.Sys_unit;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnion;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.models.ActivityBasicUnit;
|
||||||
|
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicUnitService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@IocBean(args = {"refer:dao"})
|
||||||
|
public class ActivityBasicUnitServiceImpl extends BaseServiceImpl<ActivityBasicUnit> implements ActivityBasicUnitService {
|
||||||
|
|
||||||
|
public ActivityBasicUnitServiceImpl(Dao dao) {
|
||||||
|
super(dao);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无请求参数,按 sys_unit.id 同步全部系统单位;返回新增数、更新数和新增单位待配置活动工会数。
|
||||||
|
* 已有记录只更新基础字段,连原有的空工会归属也保留,避免覆盖活动模块人工维护的组成单位关系。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap syncUnits() {
|
||||||
|
List<Sys_unit> sourceUnits = dao().query(Sys_unit.class, Cnd.NEW());
|
||||||
|
Set<String> existingIds = dao().query(ActivityBasicUnit.class, Cnd.NEW()).stream()
|
||||||
|
.map(ActivityBasicUnit::getId).collect(Collectors.toSet());
|
||||||
|
Set<String> activityUnionIds = dao().query(ActivityBasicUnion.class, Cnd.NEW()).stream()
|
||||||
|
.map(ActivityBasicUnion::getId).collect(Collectors.toSet());
|
||||||
|
int insertCount = 0;
|
||||||
|
int updateCount = 0;
|
||||||
|
int pendingUnionCount = 0;
|
||||||
|
String operatorId = SecurityUtil.getUserId();
|
||||||
|
long updateTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (Sys_unit sourceUnit : sourceUnits) {
|
||||||
|
if (StrUtil.isBlank(sourceUnit.getId())) {
|
||||||
|
throw new BaseException("系统单位存在空ID,本次同步已取消,请先检查系统单位资料");
|
||||||
|
}
|
||||||
|
Chain fields = buildBasicFields(sourceUnit)
|
||||||
|
.add("updatedBy", operatorId).add("updatedAt", updateTime);
|
||||||
|
if (existingIds.contains(sourceUnit.getId())) {
|
||||||
|
// 更新列中不包含 unionId、unionGroupId 和 id,保留现有归属及报名记录使用的单位ID。
|
||||||
|
dao().update(ActivityBasicUnit.class, fields, Cnd.where("id", "=", sourceUnit.getId()));
|
||||||
|
updateCount++;
|
||||||
|
} else {
|
||||||
|
// 新单位仅继承已存在于活动工会表的归属,不能将普通工会ID直接当作有效活动工会。
|
||||||
|
String unionId = StrUtil.isNotBlank(sourceUnit.getUnionId()) && activityUnionIds.contains(sourceUnit.getUnionId())
|
||||||
|
? sourceUnit.getUnionId() : null;
|
||||||
|
if (unionId == null) {
|
||||||
|
pendingUnionCount++;
|
||||||
|
}
|
||||||
|
fields.add("id", sourceUnit.getId()).add("unionId", unionId)
|
||||||
|
.add("createdBy", operatorId).add("createdAt", updateTime).add("delFlag", false);
|
||||||
|
// 按列插入避开实体的 UUID 生成拦截器,确保活动单位ID始终等于系统单位ID。
|
||||||
|
dao().insert(ActivityBasicUnit.class, fields);
|
||||||
|
existingIds.add(sourceUnit.getId());
|
||||||
|
insertCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NutMap.NEW().addv("insertCount", insertCount).addv("updateCount", updateCount)
|
||||||
|
.addv("pendingUnionCount", pendingUnionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只列出允许从系统单位更新的基础资料;工会、小组归属和审计创建信息不属于基础同步字段。
|
||||||
|
* 父级、路径与层级沿用源值,树展示需要的根节点适配在接口内存中完成,不改写单位关系。
|
||||||
|
*
|
||||||
|
* @param unit sys_unit 中的完整单位记录
|
||||||
|
* @return 可供 DAO 按列新增或更新的 Chain,不含单位ID和工会归属
|
||||||
|
*/
|
||||||
|
private Chain buildBasicFields(Sys_unit unit) {
|
||||||
|
return Chain.make("parentId", unit.getParentId()).add("path", unit.getPath())
|
||||||
|
.add("name", unit.getName()).add("aliasName", unit.getAliasName()).add("unitcode", unit.getUnitcode())
|
||||||
|
.add("note", unit.getNote()).add("address", unit.getAddress()).add("telephone", unit.getTelephone())
|
||||||
|
.add("email", unit.getEmail()).add("website", unit.getWebsite()).add("location", unit.getLocation())
|
||||||
|
.add("hasChildren", unit.isHasChildren()).add("logo", unit.getLogo()).add("unitLevel", unit.getUnitLevel())
|
||||||
|
.add("divisionCollegeCode", unit.getDivisionCollegeCode()).add("institutionType", unit.getInstitutionType())
|
||||||
|
.add("unitType", unit.getUnitType()).add("unitTypeCode", unit.getUnitTypeCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.sports.models;
|
||||||
|
|
||||||
|
import com.budwk.app.base.model.BaseModel;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.nutz.dao.entity.annotation.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/** 一条获奖成绩的奖品人数归属,与积分分配独立;一人一次获奖对应一份奖品。 */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Table("activity_results_prize")
|
||||||
|
@Comment("活动成绩奖品人数分配")
|
||||||
|
@TableIndexes({@Index(name = "ARP_RESULT_UNION", fields = {"resultId", "prizeUnionId"}, unique = true)})
|
||||||
|
public class ActivityResultsPrize extends BaseModel {
|
||||||
|
@Name
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("成绩主记录ID")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String resultId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("领取奖品的正式分工会ID,对应sys_union")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String prizeUnionId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("奖品领取工会名称快照")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String prizeUnionName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("实际获奖人数,个人成绩合计为1,团体成绩合计等于获奖总人数")
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
// 接收时保留原始数值精度,避免JSON直接绑定Integer时把3.5截断为3;保存前严格转为int范围整数。
|
||||||
|
private BigDecimal numberOfPeople;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.sports.models;
|
||||||
|
|
||||||
|
import com.budwk.app.base.model.BaseModel;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.nutz.dao.entity.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/** 一次团体获奖在正式分工会之间的积分分配,不代表新增获奖或人数分配。 */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Table("activity_results_score")
|
||||||
|
@Comment("活动成绩积分分配")
|
||||||
|
@TableIndexes({@Index(name = "ARS_RESULT_UNION", fields = {"resultId", "scoreUnionId"}, unique = true)})
|
||||||
|
public class ActivityResultsScore extends BaseModel {
|
||||||
|
@Name
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("成绩主记录ID")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String resultId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("正式计分工会ID,对应sys_union")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
private String scoreUnionId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("计分工会名称快照")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String scoreUnionName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("分配积分,合计必须等于主成绩总积分")
|
||||||
|
@ColDefine(customType = "decimal(10,2)")
|
||||||
|
private BigDecimal integral;
|
||||||
|
}
|
||||||
+48
-99
@@ -1,32 +1,22 @@
|
|||||||
package com.budwk.app.zhgh.activity.trainSignUp.controller.manage;
|
package com.budwk.app.zhgh.activity.trainSignUp.controller.manage;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.service.BaseService;
|
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Sqls;
|
|
||||||
import org.nutz.dao.sql.Sql;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author JyuHsin
|
* @Author JyuHsin
|
||||||
* @Date 2022/9/21
|
* @Date 2022/9/21
|
||||||
@@ -39,12 +29,6 @@ import java.util.List;
|
|||||||
@At("/platform/trainSignUp/adjust")
|
@At("/platform/trainSignUp/adjust")
|
||||||
public class TrainSignUpAdjustController {
|
public class TrainSignUpAdjustController {
|
||||||
|
|
||||||
@Inject
|
|
||||||
private Dao dao;
|
|
||||||
@Inject
|
|
||||||
private BaseService baseService;
|
|
||||||
@Inject
|
|
||||||
private TrainSignUpActivityService trainSignUpActivityManageService;
|
|
||||||
@Inject
|
@Inject
|
||||||
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
|
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
|
||||||
|
|
||||||
@@ -55,122 +39,87 @@ public class TrainSignUpAdjustController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动list
|
* @param year 查询年度,为空时不限制年度
|
||||||
* @param year 年度
|
* @return Result.data 为当前登录人可管理的已启用主活动数组,id/activityName 用于下拉框
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("活动列表")
|
@ApiOperation("活动列表")
|
||||||
@SaCheckPermission("trainSignUp.adjust")
|
@SaCheckPermission("trainSignUp.adjust")
|
||||||
public Result activityList(@Param(value = "year") Integer year) {
|
public Result activityList(@Param("year") Integer year) {
|
||||||
List<TrainSignUpActivity> activityList = dao.query(TrainSignUpActivity.class, Cnd.NEW().andEX("year", "=", year).andEX("isDisabled", "=", false).desc("activityStartTime"));
|
return Result.success(trainSignUpActivityStatisticsService.activityList(year, true));
|
||||||
return Result.success(activityList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||||
|
* @param activityId 主活动ID,为空时返回空分页
|
||||||
|
* @return Result.data 为 Pagination,list 和 totalCount 均按负责人范围查询
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("分页查询")
|
@ApiOperation("分页查询")
|
||||||
@SaCheckPermission("trainSignUp.adjust")
|
@SaCheckPermission("trainSignUp.adjust")
|
||||||
public Result pageData(PageForm pageForm,
|
public Result pageData(PageForm pageForm, @Param("activityId") String activityId) {
|
||||||
@Param(value = "activityId") String activityId) {
|
|
||||||
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
|
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activityId 主活动ID
|
||||||
|
* @return Result.data 为可调整的子活动数组,id 为目标ID,registerNum/hasWaitingNum 为已报及候补人数
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("子活动查询")
|
@ApiOperation("子活动查询")
|
||||||
@SaCheckPermission("trainSignUp.adjust")
|
@SaCheckPermission("trainSignUp.adjust")
|
||||||
public Result getCourse(String activityId) {
|
public Result getCourse(String activityId) {
|
||||||
Sql sql = Sqls.create("""
|
return Result.success(trainSignUpActivityStatisticsService.getCourse(activityId));
|
||||||
SELECT
|
|
||||||
tsuc.id,
|
|
||||||
tsuc.courseName,
|
|
||||||
tsuc.coursePeopleNumber,
|
|
||||||
tsuc.courseType,
|
|
||||||
tsuc.courseLocation,
|
|
||||||
tsuc.courseInstructor,
|
|
||||||
tsuc.courseReservedNumber,
|
|
||||||
tsuc.waitingNum,
|
|
||||||
tsuc.reserveMode
|
|
||||||
FROM
|
|
||||||
`train_sign_up_course` tsuc
|
|
||||||
WHERE
|
|
||||||
tsuc.activityId = @activityId
|
|
||||||
ORDER BY courseName asc
|
|
||||||
""");
|
|
||||||
sql.setParam("activityId", activityId);
|
|
||||||
List<NutMap> courseList = baseService.listMap(sql);
|
|
||||||
courseList.forEach(c -> {
|
|
||||||
c.put("registerNum", trainSignUpActivityStatisticsService.queryCourseCount(c.getString("id"), c.getString("courseType")));
|
|
||||||
c.put("hasWaitingNum", trainSignUpActivityStatisticsService.queryCourseWaitCount(c.getString("id"), c.getString("courseType")));
|
|
||||||
});
|
|
||||||
return Result.success(courseList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 有管理权限的子活动ID
|
||||||
|
* @param unionId 工会ID,可为空
|
||||||
|
* @param unitId 单位ID,可为空
|
||||||
|
* @param searchName 保留现有接口参数,姓名和工号搜索统一使用 searchKeyword
|
||||||
|
* @param searchKeyword 姓名或工号关键字,可为空
|
||||||
|
* @return Result.data 为报名人员数组,userId 为报名人员ID,state 为报名状态
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("报名用户列表")
|
@ApiOperation("报名用户列表")
|
||||||
@SaCheckPermission("trainSignUp.adjust")
|
@SaCheckPermission("trainSignUp.adjust")
|
||||||
public Result registerUserList(@Param("courseId") String courseId,
|
public Result registerUserList(@Param("courseId") String courseId, @Param("unionId") String unionId,
|
||||||
@Param(value = "unionId") String unionId,
|
@Param("unitId") String unitId, @Param("searchName") String searchName,
|
||||||
@Param(value = "unitId") String unitId,
|
@Param("searchKeyword") String searchKeyword) {
|
||||||
@Param(value = "searchName") String searchName,
|
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId, unionId, unitId, searchName, searchKeyword));
|
||||||
@Param(value = "searchKeyword") String searchKeyword) {
|
|
||||||
List<NutMap> list = trainSignUpActivityStatisticsService.registerUserList(courseId, unionId, unitId, searchName, searchKeyword);
|
|
||||||
return Result.success(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activityId 主活动ID,来源和目标子活动都必须属于该活动
|
||||||
|
* @param oldCourseId 来源子活动ID,必须有管理权限
|
||||||
|
* @param newCourseId 目标子活动ID,必须有管理权限
|
||||||
|
* @param userId 被调整的报名人员ID,操作人身份由登录会话确定
|
||||||
|
* @return Result,code=0 表示报名及签到记录已在同一事务中调整
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("人员调整")
|
@ApiOperation("人员调整")
|
||||||
@SaCheckPermission("trainSignUp.adjust")
|
@SaCheckPermission("trainSignUp.adjust")
|
||||||
@SLog(tag = "品牌活动-人员调整", msg = "人员调整")
|
@SLog(tag = "品牌活动-人员调整", msg = "人员调整")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||||
|
trainSignUpActivityStatisticsService.adjust(activityId, oldCourseId, newCourseId, userId);
|
||||||
Cnd oldCnd = Cnd.where("activityId", "=", activityId)
|
|
||||||
.and("courseId", "=", oldCourseId).and("userId", "=", userId);
|
|
||||||
|
|
||||||
Cnd newCnd = Cnd.where("activityId", "=", activityId)
|
|
||||||
.and("courseId", "=", newCourseId).and("userId", "=", userId);
|
|
||||||
|
|
||||||
//旧的报名信息
|
|
||||||
TrainSignUpUser oldTrainSignUpUser = dao.fetch(TrainSignUpUser.class, oldCnd);
|
|
||||||
oldTrainSignUpUser.setCourseId(newCourseId);
|
|
||||||
oldTrainSignUpUser.setSignUpTime(DateUtil.date());
|
|
||||||
dao.update(oldTrainSignUpUser);
|
|
||||||
|
|
||||||
//新的课程的信息,上课时间
|
|
||||||
List<TrainSignUpActivityCourse> activityCourseList = dao.query(TrainSignUpActivityCourse.class, Cnd.where("activityId", "=", activityId)
|
|
||||||
.and("courseId", "=", newCourseId));
|
|
||||||
//先清楚旧的信息
|
|
||||||
dao.clear(TrainSignUpUserCourse.class, oldCnd);
|
|
||||||
//添加新的信息
|
|
||||||
List<TrainSignUpUserCourse> trainSignUpUserCourseList = new ArrayList<>();
|
|
||||||
activityCourseList.forEach(item -> {
|
|
||||||
TrainSignUpUserCourse course = new TrainSignUpUserCourse();
|
|
||||||
course.setActivityCourseId(activityId);
|
|
||||||
course.setCourseId(newCourseId);
|
|
||||||
course.setUserId(userId);
|
|
||||||
course.setCourseStartTime(item.getCourseStartTime());
|
|
||||||
course.setCourseEndTime(item.getCourseEndTime());
|
|
||||||
course.setActivityCourseId(item.getId());
|
|
||||||
trainSignUpUserCourseList.add(course);
|
|
||||||
});
|
|
||||||
dao.insert(trainSignUpUserCourseList);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activityId 主活动ID
|
||||||
|
* @param courseId 属于该主活动且有管理权限的子活动ID
|
||||||
|
* @param userId 被删除的报名人员ID
|
||||||
|
* @return Result,code=0 表示该子活动的报名及签到记录已删除
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("删除报名人员")
|
@ApiOperation("删除报名人员")
|
||||||
@SaCheckPermission("trainSignUp.adjust")
|
@SaCheckPermission("trainSignUp.adjust")
|
||||||
@SLog(tag = "品牌活动-人员调整", msg = "删除报名人员")
|
@SLog(tag = "品牌活动-人员调整", msg = "删除报名人员")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result deleteSignUser(String activityId, String courseId, String userId) {
|
public Result deleteSignUser(String activityId, String courseId, String userId) {
|
||||||
|
trainSignUpActivityStatisticsService.deleteSignUser(activityId, courseId, userId);
|
||||||
//删除
|
|
||||||
Cnd cnd = Cnd.NEW();
|
|
||||||
cnd.and("activityId", "=", activityId);
|
|
||||||
cnd.and("courseId", "=", courseId);
|
|
||||||
cnd.and("userId", "=", userId);
|
|
||||||
|
|
||||||
dao.clear(TrainSignUpUser.class, cnd);
|
|
||||||
dao.clear(TrainSignUpUserCourse.class, cnd);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -128,6 +128,7 @@ public class TrainSignUpApplyController {
|
|||||||
@Param(value = "assortTypes") String[] assortTypes,
|
@Param(value = "assortTypes") String[] assortTypes,
|
||||||
@Param(value = "searchKeyword") String searchKeyword,
|
@Param(value = "searchKeyword") String searchKeyword,
|
||||||
@Param(value = "dataType") String dataType) {
|
@Param(value = "dataType") String dataType) {
|
||||||
|
// 返回课程签到半径 radius(米),供 H5 地图按实际配置绘制签到范围。
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
tsuc.id,
|
tsuc.id,
|
||||||
@@ -144,6 +145,7 @@ public class TrainSignUpApplyController {
|
|||||||
tsuc.unionLimit,
|
tsuc.unionLimit,
|
||||||
tsuc.isMobileSign,
|
tsuc.isMobileSign,
|
||||||
tsuc.signType,
|
tsuc.signType,
|
||||||
|
tsuc.radius,
|
||||||
tsuc.isReceiveGift,
|
tsuc.isReceiveGift,
|
||||||
tsuc.giftType,
|
tsuc.giftType,
|
||||||
tsuc.reserveMode,
|
tsuc.reserveMode,
|
||||||
|
|||||||
+45
-19
@@ -10,8 +10,6 @@ import com.budwk.app.base.param.PageForm;
|
|||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.flow.entity.ProcessInstance;
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||||
import com.budwk.app.sys.models.Sys_home_activity;
|
|
||||||
import com.budwk.app.sys.models.Sys_home_template;
|
|
||||||
import com.budwk.app.sys.models.Sys_union;
|
import com.budwk.app.sys.models.Sys_union;
|
||||||
import com.budwk.app.sys.models.Sys_unit;
|
import com.budwk.app.sys.models.Sys_unit;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
@@ -22,18 +20,18 @@ import com.budwk.app.zhgh.club.model.SysClub;
|
|||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Chain;
|
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
import org.nutz.trans.Trans;
|
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -66,6 +64,37 @@ public class TrainSignUpManageController {
|
|||||||
public void index() {
|
public void index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** activityId 为主活动ID,courseName 为可选名称;返回 Result.data 分页对象 list、totalCount。 */
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
public Result courseManagePageData(PageForm pageForm, @Param("activityId") String activityId,
|
||||||
|
@Param("courseName") String courseName) {
|
||||||
|
return Result.success(trainSignUpActivityManageService.courseManagePageData(pageForm, activityId, courseName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** activityId、courseId 为所属主活动和子活动ID;返回 Result.data 的 course、activityTime、canEditBasic、courseTypes。 */
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
public Result courseManageDetail(@Param("activityId") String activityId, @Param("courseId") String courseId) {
|
||||||
|
return Result.success(trainSignUpActivityManageService.courseManageDetail(activityId, courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* course 接收单个子活动及签到字段,负责人、时段列表按 JSON 传入;updateCourseTimes 仅实际修改时段时为 true。
|
||||||
|
* id、activityId 为子活动和主活动ID;isMobileSign 为签到开关,signType 为1扫码、2被扫、3定位。
|
||||||
|
* signUnit 为非负整数分钟,radius 为正整数米;courseLocationCoordinates、transPosition 分别为地图、WGS-84坐标数组[纬度,经度]。
|
||||||
|
* 管理员可另传名称、类型、人数、预留名额、地点、校区、分类标识;responsibleList 每项包含 userId、mobile。
|
||||||
|
* service 按登录角色决定允许保存的字段,返回 Result,code=0 表示保存成功,msg 为操作结果。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
@SLog(tag = "品牌活动-子活动管理", msg = "保存子活动配置")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result saveManagedCourse(TrainSignUpCourse course, @Param("updateCourseTimes") boolean updateCourseTimes) {
|
||||||
|
trainSignUpActivityManageService.saveManagedCourse(course, updateCourseTimes);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("分页查询")
|
@ApiOperation("分页查询")
|
||||||
@SaCheckPermission("trainSignUp.manage")
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
@@ -110,31 +139,22 @@ public class TrainSignUpManageController {
|
|||||||
@ApiOperation("活动删除")
|
@ApiOperation("活动删除")
|
||||||
@SaCheckPermission("trainSignUp.manage")
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
@SLog(tag = "品牌活动-活动管理", msg = "删除活动")
|
@SLog(tag = "品牌活动-活动管理", msg = "删除活动")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result onDelete(String id) {
|
public Result onDelete(String id) {
|
||||||
if (dao.fetch(Sys_home_template.class, id) != null) {
|
// id 为主活动ID;service 在同一事务中清理子活动负责人及报名关联。
|
||||||
return Result.error("该活动已设为模板,请先取消模板后再删除");
|
if (StrUtil.isBlank(id)) {
|
||||||
|
return Result.error("活动ID不能为空");
|
||||||
}
|
}
|
||||||
Trans.exec(() -> {
|
trainSignUpActivityManageService.deleteActivity(id);
|
||||||
trainSignUpActivityManageService.delete(id);
|
|
||||||
dao.clear(TrainSignUpCourse.class, Cnd.where("activityId", "=", id));
|
|
||||||
dao.clear(TrainSignUpActivityCourse.class, Cnd.where("activityId", "=", id));
|
|
||||||
dao.clear(TrainSignUpUser.class, Cnd.where("activityId", "=", id));
|
|
||||||
dao.clear(TrainSignUpUserCourse.class, Cnd.where("activityId", "=", id));
|
|
||||||
dao.clear(TrainSignUpActivity.class, Cnd.where("id", "=", id));
|
|
||||||
dao.clear(TrainSignUpTypeLimit.class, Cnd.where("activityId", "=", id));
|
|
||||||
dao.delete(Sys_home_activity.class, id);
|
|
||||||
});
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("活动状态变更")
|
@ApiOperation("活动状态变更")
|
||||||
@SaCheckPermission("trainSignUp.manage")
|
@SaCheckPermission("trainSignUp.manage")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result activityStatusChange(TrainSignUpActivity activity) {
|
public Result activityStatusChange(TrainSignUpActivity activity) {
|
||||||
trainSignUpActivityManageService.updateActivityStatus(activity);
|
trainSignUpActivityManageService.updateActivityStatus(activity);
|
||||||
dao.update(Sys_home_activity.class,
|
|
||||||
Chain.make("enable", !activity.isDisabled()),
|
|
||||||
Cnd.where("id", "=", activity.getId()));
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,11 +205,17 @@ public class TrainSignUpManageController {
|
|||||||
return Result.success().addData(dataMap);
|
return Result.success().addData(dataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收主活动及 courseList 子活动列表;每个 responsibleList 元素传 userId、mobile。
|
||||||
|
* responsibleList 为 null 保留历史联系人,空列表清空,多人关系由 service 校验并保存。
|
||||||
|
* 返回 Result,code=0 表示主活动、子活动和负责人已在同一事务中保存成功。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
@ApiOperation("品牌活动新增/修改")
|
@ApiOperation("品牌活动新增/修改")
|
||||||
@SaCheckPermission(value = {"trainSignUp.manage", "trainSignUp.applyActivity"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"trainSignUp.manage", "trainSignUp.applyActivity"}, mode = SaMode.OR)
|
||||||
@SLog(tag = "品牌活动-活动管理", msg = "新增/修改活动")
|
@SLog(tag = "品牌活动-活动管理", msg = "新增/修改活动")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result doHandle(TrainSignUpActivity activity) {
|
public Result doHandle(TrainSignUpActivity activity) {
|
||||||
if (StrUtil.isBlank(activity.getId())) {
|
if (StrUtil.isBlank(activity.getId())) {
|
||||||
cleanNewActivityData(activity);
|
cleanNewActivityData(activity);
|
||||||
|
|||||||
+61
-212
@@ -1,55 +1,26 @@
|
|||||||
package com.budwk.app.zhgh.activity.trainSignUp.controller.statistics;
|
package com.budwk.app.zhgh.activity.trainSignUp.controller.statistics;
|
||||||
|
|
||||||
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.afterturn.easypoi.excel.export.ExcelExportService;
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.json.JSONObject;
|
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.sys.models.Sys_file;
|
|
||||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Cnd;
|
|
||||||
import org.nutz.dao.Dao;
|
|
||||||
import org.nutz.dao.Sqls;
|
|
||||||
import org.nutz.dao.sql.Sql;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
|
||||||
import org.nutz.lang.Lang;
|
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author zxy
|
* @author zxy
|
||||||
@@ -63,12 +34,8 @@ import java.util.zip.ZipOutputStream;
|
|||||||
@At("/platform/trainSignUp/statistics")
|
@At("/platform/trainSignUp/statistics")
|
||||||
public class TrainSignUpActivityStatisticsController {
|
public class TrainSignUpActivityStatisticsController {
|
||||||
|
|
||||||
@Inject
|
|
||||||
private TrainSignUpActivityService trainSignUpActivityManageService;
|
|
||||||
@Inject
|
@Inject
|
||||||
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
|
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
|
||||||
@Inject
|
|
||||||
private Dao dao;
|
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
@@ -76,231 +43,113 @@ public class TrainSignUpActivityStatisticsController {
|
|||||||
public void index() {
|
public void index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||||
|
* @param activityId 主活动ID,为空时返回空分页
|
||||||
|
* @return Result.data 为 Pagination,list 和 totalCount 均受当前登录人的负责人范围限制
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("分页查询")
|
@ApiOperation("分页查询")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
public Result pageData(PageForm pageForm,
|
public Result pageData(PageForm pageForm, @Param("activityId") String activityId) {
|
||||||
@Param(value = "activityId") String activityId) {
|
|
||||||
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
|
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
|
||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动list
|
* @param year 查询年度,为空时不限制年度
|
||||||
*
|
* @return Result.data 为可管理的主活动列表,id/activityName 用于活动下拉框
|
||||||
* @param year 年度
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("活动列表")
|
@ApiOperation("活动列表")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
public Result activityList(@Param(value = "year") Integer year) {
|
public Result activityList(@Param("year") Integer year) {
|
||||||
List<TrainSignUpActivity> list = dao.query(TrainSignUpActivity.class, Cnd.NEW().andEX("year", "=", year).desc("activityStartTime"));
|
return Result.success(trainSignUpActivityStatisticsService.activityList(year, false));
|
||||||
if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CHAIRMAN.name(), RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name())) {
|
|
||||||
// 获取当前登录用户的工会
|
|
||||||
String unionId = SecurityUtil.getUnionId();
|
|
||||||
// 获取当前登录用户的社团
|
|
||||||
List<String> clubIds = SecurityUtil.getClubIds();
|
|
||||||
|
|
||||||
List<TrainSignUpActivity> result = new ArrayList<>();
|
|
||||||
for (TrainSignUpActivity activity : list) {
|
|
||||||
if(AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
|
|
||||||
if(activity.getHostUnits().contains(unionId) || activity.getHelpUnits().contains(unionId)) {
|
|
||||||
result.add(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(AuthUtil.hasRoleOr(RoleConstant.CLUB_PRESIDENT.name(), RoleConstant.CLUB_OPERATOR.name())) {
|
|
||||||
if(clubIds.stream().anyMatch(activity.getHostUnits()::contains) || clubIds.stream().anyMatch(activity.getHelpUnits()::contains)) {
|
|
||||||
result.add(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.success(result);
|
|
||||||
}
|
|
||||||
return Result.success(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 培训班报名人员list
|
* @param courseId 子活动ID,必须在当前登录人的管理范围内
|
||||||
*
|
* @return Result.data 为报名人员数组,包含姓名、工号、单位、联系方式、报名状态及动态报名字段
|
||||||
* @param courseId
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("报名人员列表")
|
@ApiOperation("报名人员列表")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
public Result registerUserList(@Param(value = "courseId") String courseId) {
|
public Result registerUserList(@Param("courseId") String courseId) {
|
||||||
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId));
|
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 子活动ID,必须在当前登录人的管理范围内
|
||||||
|
* @return Result.data 为动态列配置数组,label 为列标题,prop 为报名数据字段名
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("报名动态列")
|
@ApiOperation("报名动态列")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
public Object getTaleColumnInfo(@Param(value = "courseId") String courseId) {
|
public Object getTaleColumnInfo(@Param("courseId") String courseId) {
|
||||||
return Result.success(trainSignUpActivityStatisticsService.getTaleColumnInfo(courseId));
|
return Result.success(trainSignUpActivityStatisticsService.getTaleColumnInfo(courseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
/**
|
||||||
@ApiOperation("获取时间信息")
|
* @param courseId 子活动ID,必须在当前登录人的管理范围内
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
* @return Result.data 为时段数组,id 为时段ID,courseStartTime/courseEndTime 为起止时间
|
||||||
public Result selectTimes(@Param("courseId") String courseId) {
|
*/
|
||||||
List<TrainSignUpActivityCourse> activityCourses = dao.query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", courseId).asc("courseStartTime"));
|
@At
|
||||||
return Result.success(activityCourses);
|
@ApiOperation("获取时间信息")
|
||||||
}
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
|
public Result selectTimes(@Param("courseId") String courseId) {
|
||||||
|
return Result.success(trainSignUpActivityStatisticsService.selectTimes(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 培训班上课签到信息
|
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||||
*
|
* @param courseId 有管理权限的子活动ID
|
||||||
* @param courseId
|
* @param timeId 子活动时段ID
|
||||||
* @return
|
* @return Result.data 为签到分页,list 中 isAttend/attendTime 为签到状态及时间
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("获取签到信息")
|
@ApiOperation("获取签到信息")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
public Result getSignInfo(PageForm pageForm,
|
public Result getSignInfo(PageForm pageForm, @Param("courseId") String courseId, String timeId) {
|
||||||
@Param("courseId") String courseId,
|
|
||||||
String timeId) {
|
|
||||||
return Result.success(trainSignUpActivityStatisticsService.getSignInfo(pageForm, courseId, timeId));
|
return Result.success(trainSignUpActivityStatisticsService.getSignInfo(pageForm, courseId, timeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 有管理权限的子活动ID
|
||||||
|
* @param openOtherUnion true 开放给其他工会报名,false 关闭
|
||||||
|
* @return Result,code=0 表示状态已更新
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("开放报名")
|
@ApiOperation("开放报名")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result signChange(@Param("courseId") String courseId, @Param("openOtherUnion") Boolean openOtherUnion) {
|
public Result signChange(@Param("courseId") String courseId, @Param("openOtherUnion") Boolean openOtherUnion) {
|
||||||
dao.update(TrainSignUpCourse.class, Chain.make("openOtherUnion", openOtherUnion)
|
trainSignUpActivityStatisticsService.signChange(courseId, openOtherUnion);
|
||||||
, Cnd.where("id", "=", courseId));
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
/**
|
||||||
@ApiOperation("设置签到")
|
* @param id 签到记录ID,service 根据记录所属子活动校验当前登录人的权限
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
* @return Result,code=0 表示签到状态已切换
|
||||||
public Result adjustSign(@Param("id") String id) {
|
*/
|
||||||
TrainSignUpUserCourse userCourse = dao.fetch(TrainSignUpUserCourse.class, id);
|
@At
|
||||||
if(userCourse.isAttend()) {
|
@ApiOperation("设置签到")
|
||||||
userCourse.setAttend(false);
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
userCourse.setAttendTime(null);
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
} else {
|
public Result adjustSign(@Param("id") String id) {
|
||||||
userCourse.setAttend(true);
|
trainSignUpActivityStatisticsService.adjustSign(id);
|
||||||
userCourse.setAttendTime(userCourse.getCourseStartTime());
|
return Result.success();
|
||||||
}
|
}
|
||||||
dao.update(userCourse);
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activityId 主活动ID,仅导出当前登录人有管理权限的子活动报名记录
|
||||||
|
* @param response 文件下载响应,输出 .xlsx 文件而非 JSON
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
@ApiOperation("导出签到名单")
|
@ApiOperation("导出签到名单")
|
||||||
@SaCheckPermission("trainSignUp.statistics")
|
@SaCheckPermission("trainSignUp.statistics")
|
||||||
public void exportSignUser(@Param(value = "activityId") String activityId,
|
public void exportSignUser(@Param("activityId") String activityId, HttpServletResponse response) throws IOException {
|
||||||
HttpServletResponse response) throws IOException {
|
NutMap exportData = trainSignUpActivityStatisticsService.buildSignUserExport(activityId);
|
||||||
try {
|
CommonDownloadUtil.download(exportData.getString("fileName"), exportData.getAs("workbook", Workbook.class), response);
|
||||||
TrainSignUpActivity activity = dao.fetch(TrainSignUpActivity.class, activityId);
|
|
||||||
Sql sql = Sqls.create("""
|
|
||||||
SELECT
|
|
||||||
ts.*,
|
|
||||||
CONCAT(DATE_FORMAT(ac.courseStartTime, '%H:%i:%s'),'至',DATE_FORMAT(ac.courseEndTime, '%H:%i:%s')) AS courseTime,
|
|
||||||
u.username,
|
|
||||||
u.loginname,
|
|
||||||
u.sex,
|
|
||||||
ifnull(ts.mobile, u.mobile) as mobile,
|
|
||||||
u.birthday,
|
|
||||||
tsc.courseName
|
|
||||||
FROM
|
|
||||||
train_sign_up_user ts
|
|
||||||
left join train_sign_up_activity_course ac on ts.activityCourseId = ac.id
|
|
||||||
left join `vw_user` u on u.id = ts. userId
|
|
||||||
left join train_sign_up_course tsc on tsc.id = ts.courseId
|
|
||||||
WHERE
|
|
||||||
ts.activityId = @activityId
|
|
||||||
""").setParam("activityId", activityId);
|
|
||||||
List<NutMap> userList = trainSignUpActivityManageService.listMap(sql);
|
|
||||||
|
|
||||||
List<TrainSignUpCourse> courseList = dao.query(TrainSignUpCourse.class, Cnd.where("activityId", "=", activityId));
|
|
||||||
|
|
||||||
List<TrainSignUpType> trainSignUpTypeList = dao.query(TrainSignUpType.class, Cnd.NEW());
|
|
||||||
dao.fetchLinks(trainSignUpTypeList, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
|
||||||
Map<String, TrainSignUpType> typeMap = trainSignUpTypeList.stream().collect(Collectors.toMap(TrainSignUpType::getId, o -> o));
|
|
||||||
|
|
||||||
List<Map<String, Object>> sheetsList = new ArrayList<>();
|
|
||||||
|
|
||||||
List<ExcelExportEntity> excelCommonExportEntity = new ArrayList<>();
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("姓名", "username", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("工号", "loginname", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("单位", "unitName", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("分工会", "unionName", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("性别", "sex", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("手机号", "mobile", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("生日", "birthday", 20));
|
|
||||||
excelCommonExportEntity.add(new ExcelExportEntity("报名时段", "courseTime", 20));
|
|
||||||
|
|
||||||
for (TrainSignUpCourse c : courseList) {
|
|
||||||
String k = c.getCourseName();
|
|
||||||
List<NutMap> v = userList.stream().filter(x -> StrUtil.isNotBlank(x.getString("courseName")) && x.getString("courseName").equals(k)).collect(Collectors.toList());
|
|
||||||
ExportParams userExportParams = new ExportParams();
|
|
||||||
userExportParams.setSheetName(k);
|
|
||||||
userExportParams.setType(ExcelType.HSSF);
|
|
||||||
|
|
||||||
List<ExcelExportEntity> currentEntities = new ArrayList<>();
|
|
||||||
currentEntities.addAll(excelCommonExportEntity);
|
|
||||||
|
|
||||||
TrainSignUpType signUpType = typeMap.get(c.getCourseType());
|
|
||||||
if (Lang.isNotEmpty(signUpType.getTrainMobileSignColumnList())) {
|
|
||||||
for (TrainMobileSignColumn column : signUpType.getTrainMobileSignColumnList()) {
|
|
||||||
ExcelExportEntity entity = new ExcelExportEntity();
|
|
||||||
entity.setName(column.getColumnName());
|
|
||||||
entity.setKey(column.getColumnCode());
|
|
||||||
entity.setWidth(20);
|
|
||||||
if ("FILE".equals(column.getColumnFormType())) {
|
|
||||||
entity.setType(2);
|
|
||||||
entity.setExportImageType(2);
|
|
||||||
}
|
|
||||||
currentEntities.add(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (NutMap userSignData : v) {
|
|
||||||
String mobileColumnsValueStr = userSignData.getString("mobileColumnsValue");
|
|
||||||
if (StrUtil.isNotBlank(mobileColumnsValueStr)) {
|
|
||||||
List<NutMap> mobileColumnsValue = Json.fromJsonAsList(NutMap.class, mobileColumnsValueStr);
|
|
||||||
for (NutMap cv : mobileColumnsValue) {
|
|
||||||
if (!"FILE".equals(cv.getString("columnFormType"))) {
|
|
||||||
userSignData.put(cv.getString("columnCode"), cv.getString("columnValue"));
|
|
||||||
} else {
|
|
||||||
if (StrUtil.isNotBlank(cv.getString("columnValue"))) {
|
|
||||||
List<NutMap> columnValue = cv.getAsList("columnValue", NutMap.class);
|
|
||||||
if (columnValue.size() == 1) {
|
|
||||||
NutMap sysFile = columnValue.get(0);
|
|
||||||
Sys_file file = dao.fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", sysFile.get("url")));
|
|
||||||
byte[] imageBytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
|
|
||||||
if (imageBytes.length > 0) {
|
|
||||||
userSignData.put(cv.getString("columnCode"), imageBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> userExportMap = new HashMap<>();
|
|
||||||
userExportMap.put("name", k);
|
|
||||||
userExportParams.setType(ExcelType.HSSF);
|
|
||||||
userExportMap.put("title", userExportParams);
|
|
||||||
userExportMap.put("entity", currentEntities);
|
|
||||||
userExportMap.put("data", v);
|
|
||||||
|
|
||||||
sheetsList.add(userExportMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
Workbook workbook = new HSSFWorkbook();
|
|
||||||
for (Map<String, Object> map : sheetsList) {
|
|
||||||
ExcelExportService service = new ExcelExportService();
|
|
||||||
service.createSheetForMap(workbook, (ExportParams) map.get("title"), (List<ExcelExportEntity>) map.get("entity"), (Collection<?>) map.get("data"));
|
|
||||||
}
|
|
||||||
CommonDownloadUtil.download(activity.getActivityName() + "报名人员名单" + ".xls", workbook, response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ public class TrainSignUpCourse extends BaseModel implements Serializable {
|
|||||||
@Many(field = "courseId")
|
@Many(field = "courseId")
|
||||||
private List<TrainSignUpActivityCourse> courseTimeList;
|
private List<TrainSignUpActivityCourse> courseTimeList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整负责人关系。null 表示旧数据尚未设置,空列表表示主动清空负责人。
|
||||||
|
* 关系单独保存,后续可按 userId 精确查询,无需解析姓名或 JSON。
|
||||||
|
*/
|
||||||
|
@Many(field = "courseId")
|
||||||
|
private List<TrainSignUpCourseResponsible> responsibleList;
|
||||||
|
|
||||||
//已报人数
|
//已报人数
|
||||||
private Integer hasRegisterNum;
|
private Integer hasRegisterNum;
|
||||||
|
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package com.budwk.app.zhgh.activity.trainSignUp.models;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import org.nutz.dao.entity.annotation.*;
|
||||||
|
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子活动负责人关系:一人一条记录,业务控制应以 userId 为准,姓名仅用于展示。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@Table("train_sign_up_course_responsible")
|
||||||
|
@Comment("品牌活动子活动负责人")
|
||||||
|
@TableIndexes({
|
||||||
|
@Index(name = "TSU_RESP_COURSE_USER", fields = {"courseId", "userId"}, unique = true),
|
||||||
|
@Index(name = "TSU_RESP_USER_ACTIVITY", fields = {"userId", "activityId"}, unique = false),
|
||||||
|
@Index(name = "TSU_RESP_ACTIVITY", fields = {"activityId"}, unique = false)
|
||||||
|
})
|
||||||
|
public class TrainSignUpCourseResponsible implements Serializable {
|
||||||
|
|
||||||
|
@Name
|
||||||
|
@PrevInsert(uu32 = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
@Comment("主活动ID,由后端根据所属子活动设置")
|
||||||
|
private String activityId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
@Comment("子活动ID,由后端在子活动保存后设置")
|
||||||
|
private String courseId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
@Comment("系统用户ID,用于负责人查询和业务控制")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
@Comment("负责人姓名,由后端读取系统用户信息")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
|
@Comment("子活动联系号码,可填写手机号或办公电话")
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Comment("负责人顺序,第一位兼容原有联系人展示")
|
||||||
|
private int orderNum;
|
||||||
|
|
||||||
|
/** 工号仅用于多选回显,不重复存储系统用户资料。 */
|
||||||
|
private String loginname;
|
||||||
|
}
|
||||||
+36
@@ -20,6 +20,35 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public interface TrainSignUpActivityService extends BaseService<TrainSignUpActivity> {
|
public interface TrainSignUpActivityService extends BaseService<TrainSignUpActivity> {
|
||||||
|
|
||||||
|
/** 当前登录人是否具有活动关键内容编辑权限(三类管理员任意一种)。 */
|
||||||
|
boolean canEditActivityBasic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前主活动中可管理的子活动,普通用户仅返回本人负责的记录。
|
||||||
|
* @param pageForm 页码、每页条数;排序固定使用子活动序号
|
||||||
|
* @param activityId 主活动ID,必填
|
||||||
|
* @param courseName 子活动名称,可为空,按包含匹配
|
||||||
|
* @return 分页对象,list 包含子活动字段、courseTypeName、responsibleNames、courseTimeName
|
||||||
|
*/
|
||||||
|
Pagination courseManagePageData(PageForm pageForm, String activityId, String courseName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取单个子活动及表单所需选项,同时校验当前登录人的负责人范围。
|
||||||
|
* @param activityId 主活动ID,必须与子活动实际归属一致
|
||||||
|
* @param courseId 子活动ID,必填
|
||||||
|
* @return course 为子活动及负责人、时段;activityTime 为主活动日期范围;
|
||||||
|
* canEditBasic 为关键内容编辑权限,courseTypes 为管理员使用的类型选项
|
||||||
|
*/
|
||||||
|
NutMap courseManageDetail(String activityId, String courseId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存单个子活动,普通负责人仅更新签到字段,管理员可更新基本信息及负责人。
|
||||||
|
* @param course id、activityId 必填;签到参数为 isMobileSign、signType、signUnit、radius、
|
||||||
|
* courseLocationCoordinates、transPosition,坐标均按纬度、经度顺序传入
|
||||||
|
* @param updateCourseTimes 仅管理员实际修改时段后传 true;false 不改动任何时段及签到记录
|
||||||
|
*/
|
||||||
|
void saveManagedCourse(TrainSignUpCourse course, boolean updateCourseTimes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加活动
|
* 添加活动
|
||||||
*
|
*
|
||||||
@@ -35,6 +64,13 @@ public interface TrainSignUpActivityService extends BaseService<TrainSignUpActiv
|
|||||||
*/
|
*/
|
||||||
void edit(TrainSignUpActivity activity);
|
void edit(TrainSignUpActivity activity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除活动及其子活动、负责人关系、报名和首页记录。
|
||||||
|
*
|
||||||
|
* @param id 主活动ID;已经设为模板的活动需先取消模板
|
||||||
|
*/
|
||||||
|
void deleteActivity(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新活动状态
|
* 更新活动状态
|
||||||
*
|
*
|
||||||
|
|||||||
+96
-16
@@ -3,6 +3,8 @@ package com.budwk.app.zhgh.activity.trainSignUp.service;
|
|||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpActivity;
|
||||||
|
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpActivityCourse;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpUser;
|
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpUser;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
@@ -17,44 +19,122 @@ import java.util.Map;
|
|||||||
public interface TrainSignUpActivityStatisticsService extends BaseService<TrainSignUpUser> {
|
public interface TrainSignUpActivityStatisticsService extends BaseService<TrainSignUpUser> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计分页
|
* 查询当前用户可管理的主活动,只包含至少一个本人负责子活动的主活动。
|
||||||
|
* 系统管理员、校工会管理员、校工会活动管理员不受负责人范围限制,仍保留年度及启用状态条件。
|
||||||
*
|
*
|
||||||
* @param pageForm
|
* @param year 年度,为空时不限制年度
|
||||||
* @param activityId
|
* @param enabledOnly true 仅查询已启用活动,人员调整页面使用;false 保留统计页面原范围
|
||||||
* @return
|
* @return 主活动实体列表;没有负责记录时返回空列表
|
||||||
|
*/
|
||||||
|
List<TrainSignUpActivity> activityList(Integer year, boolean enabledOnly);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询人员调整的目标子活动,限制为当前用户负责的子活动。
|
||||||
|
*
|
||||||
|
* @param activityId 主活动ID,为空时返回空列表
|
||||||
|
* @return 子活动列表,id/courseName 为标识及名称,registerNum/hasWaitingNum 为已报及候补人数
|
||||||
|
*/
|
||||||
|
List<NutMap> getCourse(String activityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询有管理权限的子活动时段。
|
||||||
|
*
|
||||||
|
* @param courseId 子活动ID,必须存在且在当前用户的管理范围内
|
||||||
|
* @return 时段实体列表,id 为时段ID,courseStartTime/courseEndTime 为起止时间
|
||||||
|
*/
|
||||||
|
List<TrainSignUpActivityCourse> selectTimes(String courseId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改子活动对外报名状态,只允许操作本人负责的子活动或由全量管理员操作。
|
||||||
|
*
|
||||||
|
* @param courseId 子活动ID
|
||||||
|
* @param openOtherUnion true 开放给其他工会报名,false 关闭,不能传 null
|
||||||
|
*/
|
||||||
|
void signChange(String courseId, Boolean openOtherUnion);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换签到状态,依据签到记录实际所属子活动校验负责人权限。
|
||||||
|
*
|
||||||
|
* @param id TrainSignUpUserCourse 签到记录ID
|
||||||
|
*/
|
||||||
|
void adjustSign(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移报名记录及签到时段,来源、目标子活动均需属于指定主活动且在当前用户管理范围内。
|
||||||
|
*
|
||||||
|
* @param activityId 主活动ID
|
||||||
|
* @param oldCourseId 原子活动ID
|
||||||
|
* @param newCourseId 目标子活动ID
|
||||||
|
* @param userId 被调整的报名人员用户ID,不能用来指定操作人的身份
|
||||||
|
*/
|
||||||
|
void adjust(String activityId, String oldCourseId, String newCourseId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定子活动的报名及签到记录,先验证实际子活动归属和当前登录人的负责人权限。
|
||||||
|
*
|
||||||
|
* @param activityId 主活动ID
|
||||||
|
* @param courseId 子活动ID
|
||||||
|
* @param userId 被删除的报名人员用户ID
|
||||||
|
*/
|
||||||
|
void deleteSignUser(String activityId, String courseId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建当前用户管理范围内的报名人员 Excel,不能导出同一主活动下其他人的子活动。
|
||||||
|
*
|
||||||
|
* @param activityId 主活动ID
|
||||||
|
* @return 内部 NutMap,fileName 为 .xlsx 文件名,workbook 为 XSSF Workbook,由 controller 输出并关闭
|
||||||
|
*/
|
||||||
|
NutMap buildSignUserExport(String activityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按当前登录人的负责人范围查询子活动分页,系统管理员、校工会管理员、校工会活动管理员可查询全部。
|
||||||
|
*
|
||||||
|
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||||
|
* @param activityId 主活动ID,为空时返回空分页
|
||||||
|
* @return Pagination,list 为子活动数据,totalCount 为相同负责人范围下的记录总数
|
||||||
*/
|
*/
|
||||||
Pagination pageData(PageForm pageForm, String activityId);
|
Pagination pageData(PageForm pageForm, String activityId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 该课程下的报名人员信息
|
* 查询有管理权限的子活动报名明细,并展开动态报名字段。
|
||||||
*
|
*
|
||||||
* @param courseId
|
* @param courseId 子活动ID,不能为空,必须在当前登录人的管理范围内
|
||||||
* @return
|
* @return 报名人员数组,包含姓名、工号、单位、联系方式、state 报名状态及动态字段
|
||||||
*/
|
*/
|
||||||
List<NutMap> registerUserList(String courseId);
|
List<NutMap> registerUserList(String courseId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 有管理权限的子活动ID
|
||||||
|
* @return 动态列配置数组,label 为列标题,prop 为报名明细对应字段名
|
||||||
|
*/
|
||||||
List<NutMap> getTaleColumnInfo(String courseId);
|
List<NutMap> getTaleColumnInfo(String courseId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 该课程下的报名人员信息
|
* 按工会、单位和姓名/工号搜索有管理权限的子活动报名人员。
|
||||||
*
|
*
|
||||||
* @param courseId
|
* @param courseId 有管理权限的子活动ID,不能为空
|
||||||
* @return
|
* @param unionId 工会ID,可为空
|
||||||
|
* @param unitId 单位ID,可为空
|
||||||
|
* @param searchName 保留现有接口参数,姓名和工号统一使用 searchKeyword 搜索
|
||||||
|
* @param searchKeyword 姓名或工号关键字,可为空
|
||||||
|
* @return 报名人员数组,userId 为报名人员用户ID,state 为报名状态
|
||||||
*/
|
*/
|
||||||
List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword);
|
List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取每个课程的签到情况
|
* 查询有管理权限的子活动指定时段签到分页。
|
||||||
*
|
*
|
||||||
* @param courseId
|
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||||
* @return k->每个培训班每节课的上课时间 v->上课记录list
|
* @param courseId 有管理权限的子活动ID
|
||||||
|
* @param timeId 子活动时段ID
|
||||||
|
* @return Pagination,list 为签到人员,isAttend/attendTime 为签到状态及时间
|
||||||
*/
|
*/
|
||||||
Pagination getSignInfo(PageForm pageForm, String courseId, String timeId);
|
Pagination getSignInfo(PageForm pageForm, String courseId, String timeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 报名人员list 导出
|
* 查询主活动下可管理子活动的基础报名导出数据。
|
||||||
* @param activityId
|
* @param activityId 主活动ID
|
||||||
* @return
|
* @return 报名人员数组,包含姓名、工号、联系方式及子活动名称,不包含管理范围以外的子活动
|
||||||
*/
|
*/
|
||||||
List<NutMap> baoMingUserList(String activityId);
|
List<NutMap> baoMingUserList(String activityId);
|
||||||
|
|
||||||
|
|||||||
+410
-3
@@ -1,13 +1,18 @@
|
|||||||
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.sys.models.Sys_home_activity;
|
import com.budwk.app.sys.models.Sys_home_activity;
|
||||||
import com.budwk.app.sys.models.Sys_home_template;
|
import com.budwk.app.sys.models.Sys_home_template;
|
||||||
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
import com.budwk.app.sys.views.View_user;
|
import com.budwk.app.sys.views.View_user;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.zhgh.activity.family.models.FamilyActivity;
|
import com.budwk.app.zhgh.activity.family.models.FamilyActivity;
|
||||||
import com.budwk.app.zhgh.activity.family.models.FamilyCourse;
|
import com.budwk.app.zhgh.activity.family.models.FamilyCourse;
|
||||||
import com.budwk.app.zhgh.activity.family.models.FamilyType;
|
import com.budwk.app.zhgh.activity.family.models.FamilyType;
|
||||||
@@ -51,9 +56,227 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canEditActivityBasic() {
|
||||||
|
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||||
|
RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整场编辑、删除和启停均会修改关键内容,不能仅凭页面菜单权限放行。 */
|
||||||
|
private void requireActivityBasicPermission() {
|
||||||
|
if (!canEditActivityBasic()) {
|
||||||
|
throw new BaseException("活动关键信息仅管理员可修改,请通过子活动管理维护签到设置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 每次读写都重新检查实际归属和负责人关系,禁止信任前端传入的角色或负责人名单。 */
|
||||||
|
private TrainSignUpCourse requireManagedCourse(String activityId, String courseId) {
|
||||||
|
if (StrUtil.isBlank(activityId) || StrUtil.isBlank(courseId)) {
|
||||||
|
throw new BaseException("主活动和子活动ID不能为空");
|
||||||
|
}
|
||||||
|
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
||||||
|
if (course == null || !activityId.equals(course.getActivityId()) || fetch(activityId) == null) {
|
||||||
|
throw new BaseException("子活动不存在或不属于当前活动");
|
||||||
|
}
|
||||||
|
if (!canEditActivityBasic()) {
|
||||||
|
String userId = SecurityUtil.getUserId();
|
||||||
|
if (StrUtil.isBlank(userId) || dao().count(TrainSignUpCourseResponsible.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId)
|
||||||
|
.and("userId", "=", userId)) == 0) {
|
||||||
|
throw new BaseException("无权管理该子活动,请检查负责人设置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pagination courseManagePageData(PageForm pageForm, String activityId, String courseName) {
|
||||||
|
if (StrUtil.isBlank(activityId) || fetch(activityId) == null) {
|
||||||
|
throw new BaseException("主活动不存在");
|
||||||
|
}
|
||||||
|
Cnd cnd = Cnd.where("course.activityId", "=", activityId).and(Cnd.likeEX("course.courseName", courseName));
|
||||||
|
if (!canEditActivityBasic()) {
|
||||||
|
String userId = SecurityUtil.getUserId();
|
||||||
|
List<TrainSignUpCourseResponsible> relations = StrUtil.isBlank(userId) ? Collections.emptyList()
|
||||||
|
: dao().query(TrainSignUpCourseResponsible.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("userId", "=", userId));
|
||||||
|
if (relations.isEmpty()) {
|
||||||
|
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>());
|
||||||
|
}
|
||||||
|
// 范围条件在分页前进入数据库查询,记录数和分页总数使用同一负责人范围。
|
||||||
|
cnd.and("course.id", "in", relations.stream().map(TrainSignUpCourseResponsible::getCourseId).toList());
|
||||||
|
}
|
||||||
|
cnd.asc("course.orderNum").asc("course.id");
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT course.*, type.typeName AS courseTypeName,
|
||||||
|
(SELECT GROUP_CONCAT(r.username ORDER BY r.orderNum SEPARATOR '、')
|
||||||
|
FROM train_sign_up_course_responsible r
|
||||||
|
WHERE r.courseId=course.id AND r.activityId=course.activityId) AS responsibleNames,
|
||||||
|
(SELECT GROUP_CONCAT(CONCAT(DATE_FORMAT(t.courseStartTime, '%Y-%m-%d %H:%i'),
|
||||||
|
' 至 ', DATE_FORMAT(t.courseEndTime, '%H:%i')) ORDER BY t.courseStartTime SEPARATOR ';')
|
||||||
|
FROM train_sign_up_activity_course t WHERE t.courseId=course.id) AS courseTimeName
|
||||||
|
FROM train_sign_up_course course
|
||||||
|
LEFT JOIN train_sign_up_type type ON type.id=course.courseType
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap courseManageDetail(String activityId, String courseId) {
|
||||||
|
TrainSignUpCourse course = requireManagedCourse(activityId, courseId);
|
||||||
|
fillCourseResponsibles(List.of(course));
|
||||||
|
dao().fetchLinks(course, "courseTimeList", Cnd.NEW().asc("courseStartTime"));
|
||||||
|
TrainSignUpActivity activity = fetch(activityId);
|
||||||
|
NutMap data = Lang.obj2nutmap(course);
|
||||||
|
// 时间编辑组件使用日期和 HH:mm 两部分;签到记录仍由 service 按完整日期时间保存。
|
||||||
|
List<NutMap> times = course.getCourseTimeList().stream().map(time -> Lang.obj2nutmap(time)
|
||||||
|
.setv("courseDate", DateUtil.formatDate(time.getCourseDate()))
|
||||||
|
.setv("courseStartTime", DateUtil.format(time.getCourseStartTime(), "HH:mm"))
|
||||||
|
.setv("courseEndTime", DateUtil.format(time.getCourseEndTime(), "HH:mm"))).toList();
|
||||||
|
data.setv("courseTimeList", times).setv("setUpCourseData",
|
||||||
|
times.stream().map(time -> time.getString("courseDate")).distinct().toList());
|
||||||
|
TrainSignUpType type = StrUtil.isBlank(course.getCourseType()) ? null
|
||||||
|
: dao().fetch(TrainSignUpType.class, course.getCourseType());
|
||||||
|
data.setv("courseTypeName", type == null ? "" : type.getTypeName());
|
||||||
|
List<String> activityTime = activity.getActivityStartTime() == null || activity.getActivityEndTime() == null
|
||||||
|
? Collections.emptyList() : List.of(DateUtil.formatDateTime(activity.getActivityStartTime()),
|
||||||
|
DateUtil.formatDateTime(activity.getActivityEndTime()));
|
||||||
|
return NutMap.NEW().setv("course", data).setv("activityTime", activityTime)
|
||||||
|
.setv("canEditBasic", canEditActivityBasic())
|
||||||
|
.setv("courseTypes", canEditActivityBasic() ? dao().query(TrainSignUpType.class, Cnd.NEW()) : Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void saveManagedCourse(TrainSignUpCourse course, boolean updateCourseTimes) {
|
||||||
|
if (course == null) {
|
||||||
|
throw new BaseException("子活动信息不能为空");
|
||||||
|
}
|
||||||
|
TrainSignUpCourse stored = requireManagedCourse(course.getActivityId(), course.getId());
|
||||||
|
boolean usesSignLocation = validateManagedSign(course);
|
||||||
|
String fields = "isMobileSign";
|
||||||
|
if (course.isMobileSign()) {
|
||||||
|
fields += "|signType";
|
||||||
|
}
|
||||||
|
// 被扫或关闭签到时不更新隐藏配置,即使请求携带这些字段也保留数据库原值。
|
||||||
|
if (usesSignLocation) {
|
||||||
|
fields += "|signUnit|radius|courseLocationCoordinates|transPosition";
|
||||||
|
}
|
||||||
|
if (canEditActivityBasic()) {
|
||||||
|
validateManagedBasic(course, stored);
|
||||||
|
validateCourseResponsibles(List.of(course));
|
||||||
|
fields += "|courseName|courseType|coursePeopleNumber|courseReservedNumber|courseLocation|campus|assort";
|
||||||
|
if (course.getResponsibleList() != null) {
|
||||||
|
fields += "|courseInstructor|courseInstructorMobile";
|
||||||
|
}
|
||||||
|
if (updateCourseTimes) {
|
||||||
|
validateManagedTimes(course, fetch(stored.getActivityId()));
|
||||||
|
fields += "|courseIsLimitApply";
|
||||||
|
}
|
||||||
|
} else if (updateCourseTimes) {
|
||||||
|
throw new BaseException("活动时间仅管理员可修改");
|
||||||
|
}
|
||||||
|
// 明确列出允许更新的列,普通负责人提交其他字段也无法覆盖关键内容或其他子活动。
|
||||||
|
if (dao().update(course, "^(" + fields + ")$") != 1) {
|
||||||
|
throw new BaseException("子活动已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
if (canEditActivityBasic()) {
|
||||||
|
saveCourseResponsibles(course);
|
||||||
|
if (updateCourseTimes) {
|
||||||
|
handleCourseTimes(fetch(stored.getActivityId()), course);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示扫码或 GPS 签到,需校验并更新时间、位置配置,默认误差0分钟、半径100米。
|
||||||
|
* 被扫使用固定签到时间规则,关闭签到也不使用这些字段,返回 false 跳过校验且不更新原配置。
|
||||||
|
*/
|
||||||
|
private boolean validateManagedSign(TrainSignUpCourse course) {
|
||||||
|
if (!course.isMobileSign()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!List.of(1, 2, 3).contains(course.getSignType() == null ? 0 : course.getSignType())) {
|
||||||
|
throw new BaseException("请选择有效的签到方式");
|
||||||
|
}
|
||||||
|
if (Objects.equals(course.getSignType(), 2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (course.getSignUnit() == null) course.setSignUnit(0);
|
||||||
|
if (course.getRadius() == null) course.setRadius(100);
|
||||||
|
if (course.getSignUnit() < 0 || course.getRadius() <= 0) {
|
||||||
|
throw new BaseException("签到时间误差不能为负数,签到半径必须大于0");
|
||||||
|
}
|
||||||
|
validateManagedPosition(course.getCourseLocationCoordinates());
|
||||||
|
validateManagedPosition(course.getTransPosition());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 地图坐标和签到坐标均为 [纬度, 经度],拒绝不完整、非有限值及超出地理范围的坐标。 */
|
||||||
|
private void validateManagedPosition(List<Double> point) {
|
||||||
|
if (point == null || point.size() != 2 || point.stream().anyMatch(value -> value == null || !Double.isFinite(value))
|
||||||
|
|| Math.abs(point.get(0)) > 90 || Math.abs(point.get(1)) > 180) {
|
||||||
|
throw new BaseException("请重新设置有效的签到点");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 管理员基本信息沿用原字段范围;人数为0表示不限制,已报名后禁止切换报名类型。 */
|
||||||
|
private void validateManagedBasic(TrainSignUpCourse course, TrainSignUpCourse stored) {
|
||||||
|
if (StrUtil.isBlank(course.getCourseName()) || course.getCourseName().length() > 50
|
||||||
|
|| StrUtil.isBlank(course.getCourseLocation()) || course.getCourseLocation().length() > 50
|
||||||
|
|| StrUtil.isBlank(course.getCampus()) || course.getCampus().length() > 32
|
||||||
|
|| (course.getAssort() != null && course.getAssort().length() > 30)) {
|
||||||
|
throw new BaseException("请填写有效的名称、校区、地点和分类标识");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(course.getCourseType()) || dao().fetch(TrainSignUpType.class, course.getCourseType()) == null) {
|
||||||
|
throw new BaseException("请选择有效的子活动类型");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(stored.getCourseType(), course.getCourseType())
|
||||||
|
&& dao().count(TrainSignUpUser.class, Cnd.where("courseId", "=", stored.getId())) > 0) {
|
||||||
|
throw new BaseException("已有报名记录,不能修改子活动类型");
|
||||||
|
}
|
||||||
|
int registered = StrUtil.isBlank(stored.getCourseType()) ? 0
|
||||||
|
: statisticsService.queryCourseCount(stored.getId(), stored.getCourseType());
|
||||||
|
if (course.getCoursePeopleNumber() < 0 || course.getCoursePeopleNumber() > 9999
|
||||||
|
|| course.getCourseReservedNumber() < 0 || course.getCourseReservedNumber() > 1000
|
||||||
|
|| (course.getCoursePeopleNumber() > 0 && Objects.equals(stored.getReserveMode(), 1)
|
||||||
|
&& course.getCourseReservedNumber() + registered > course.getCoursePeopleNumber())) {
|
||||||
|
throw new BaseException("人数或预留名额无效,不能低于已报名人数与预留名额之和");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时段ID只能属于当前子活动;校验后再调用原有同步逻辑,保证变更及签到记录在同一事务中提交。 */
|
||||||
|
private void validateManagedTimes(TrainSignUpCourse course, TrainSignUpActivity activity) {
|
||||||
|
if (Lang.isEmpty(course.getCourseTimeList()) || activity.getActivityStartTime() == null || activity.getActivityEndTime() == null) {
|
||||||
|
throw new BaseException("请先设置主活动日期及至少一个有效时段");
|
||||||
|
}
|
||||||
|
Set<String> ids = new HashSet<>();
|
||||||
|
Set<String> existingIds = dao().query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", course.getId()))
|
||||||
|
.stream().map(TrainSignUpActivityCourse::getId).collect(Collectors.toSet());
|
||||||
|
for (TrainSignUpActivityCourse time : course.getCourseTimeList()) {
|
||||||
|
if (time == null || time.getCourseDate() == null || time.getCourseStartTime() == null || time.getCourseEndTime() == null) {
|
||||||
|
throw new BaseException("请完整填写活动日期及起止时间");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(time.getId()) && (!existingIds.contains(time.getId()) || !ids.add(time.getId()))) {
|
||||||
|
throw new BaseException("活动时段无效或重复,请刷新后重试");
|
||||||
|
}
|
||||||
|
String date = DateUtil.formatDate(time.getCourseDate());
|
||||||
|
Date start = DateUtil.parse(date + " " + DateUtil.format(time.getCourseStartTime(), "HH:mm"));
|
||||||
|
Date end = DateUtil.parse(date + " " + DateUtil.format(time.getCourseEndTime(), "HH:mm"));
|
||||||
|
if (!start.before(end) || start.before(activity.getActivityStartTime()) || end.after(activity.getActivityEndTime())
|
||||||
|
|| (Boolean.TRUE.equals(course.getCourseIsLimitApply()) && (time.getCourseLimitNum() == null || time.getCourseLimitNum() < 1))) {
|
||||||
|
throw new BaseException("活动时段必须在主活动范围内,结束晚于开始;限制人数时应填写正整数");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void add(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
public void add(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
||||||
|
// 先校验所有负责人,任何一行存在无效用户或联系方式时均不写入活动。
|
||||||
|
validateCourseResponsibles(activity.getCourseList());
|
||||||
// 新建活动(包括从模板另存)统一归入活动列表,不能继承来源活动的模板状态。
|
// 新建活动(包括从模板另存)统一归入活动列表,不能继承来源活动的模板状态。
|
||||||
activity.setTemplateStatus(TrainSignUpActivity.TEMPLATE_STATUS_UNSET);
|
activity.setTemplateStatus(TrainSignUpActivity.TEMPLATE_STATUS_UNSET);
|
||||||
dao().insert(activity);
|
dao().insert(activity);
|
||||||
@@ -68,6 +291,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
v.setActivityId(activity.getId());
|
v.setActivityId(activity.getId());
|
||||||
v.setOpenOtherUnion(false);
|
v.setOpenOtherUnion(false);
|
||||||
dao().insert(v);
|
dao().insert(v);
|
||||||
|
saveCourseResponsibles(v);
|
||||||
this.setCourseTimeAndInsert(v);
|
this.setCourseTimeAndInsert(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +304,22 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void edit(TrainSignUpActivity activity) {
|
public void edit(TrainSignUpActivity activity) {
|
||||||
|
requireActivityBasicPermission();
|
||||||
|
if (fetch(activity.getId()) == null) {
|
||||||
|
throw new BaseException("活动不存在");
|
||||||
|
}
|
||||||
|
validateCourseResponsibles(activity.getCourseList());
|
||||||
|
// 子活动只能属于当前活动,防止提交其他活动的ID覆盖其负责人关系。
|
||||||
|
if (activity.getCourseList() != null) {
|
||||||
|
for (TrainSignUpCourse course : activity.getCourseList()) {
|
||||||
|
if (StrUtil.isNotBlank(course.getId())) {
|
||||||
|
TrainSignUpCourse oldCourse = dao().fetch(TrainSignUpCourse.class, course.getId());
|
||||||
|
if (oldCourse == null || !activity.getId().equals(oldCourse.getActivityId())) {
|
||||||
|
throw new BaseException("子活动不存在或不属于当前活动,请刷新后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 1. 更新主活动
|
// 1. 更新主活动
|
||||||
update(activity);
|
update(activity);
|
||||||
// 2. 处理类型限制
|
// 2. 处理类型限制
|
||||||
@@ -90,7 +330,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
}
|
}
|
||||||
// 3. 处理课程列表
|
// 3. 处理课程列表
|
||||||
List<TrainSignUpCourse> courseList = activity.getCourseList();
|
List<TrainSignUpCourse> courseList = activity.getCourseList();
|
||||||
if (Lang.isEmpty(courseList)) {
|
if (courseList == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,12 +348,15 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
|
|
||||||
Set<String> deletedIds = Sets.difference(oldIds, newIds).immutableCopy();
|
Set<String> deletedIds = Sets.difference(oldIds, newIds).immutableCopy();
|
||||||
if(Lang.isNotEmpty(deletedIds)) {
|
if(Lang.isNotEmpty(deletedIds)) {
|
||||||
|
// 删除子活动时同步清理负责人,避免后续按 userId 查到已删除的业务。
|
||||||
|
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("courseId", "in", deletedIds));
|
||||||
dao().clear(TrainSignUpCourse.class, Cnd.where("id", "in", deletedIds));
|
dao().clear(TrainSignUpCourse.class, Cnd.where("id", "in", deletedIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (TrainSignUpCourse course : courseList) {
|
for (TrainSignUpCourse course : courseList) {
|
||||||
course.setActivityId(activity.getId());
|
course.setActivityId(activity.getId());
|
||||||
dao().insertOrUpdate(course);
|
dao().insertOrUpdate(course);
|
||||||
|
saveCourseResponsibles(course);
|
||||||
|
|
||||||
if (Lang.isNotEmpty(course.getCourseTimeList())) {
|
if (Lang.isNotEmpty(course.getCourseTimeList())) {
|
||||||
handleCourseTimes(activity, course);
|
handleCourseTimes(activity, course);
|
||||||
@@ -128,6 +371,131 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验多选负责人并补齐可信姓名;前端仅需传 userId 和本次活动使用的 mobile。
|
||||||
|
* null 保留历史联系人,空列表主动清空,同一子活动不允许重复选择同一 userId。
|
||||||
|
*/
|
||||||
|
private void validateCourseResponsibles(List<TrainSignUpCourse> courses) {
|
||||||
|
if (courses == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> userIds = new HashSet<>();
|
||||||
|
for (TrainSignUpCourse course : courses) {
|
||||||
|
if (course == null) {
|
||||||
|
throw new BaseException("子活动信息不能为空");
|
||||||
|
}
|
||||||
|
if (course.getResponsibleList() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Set<String> courseUserIds = new HashSet<>();
|
||||||
|
for (TrainSignUpCourseResponsible responsible : course.getResponsibleList()) {
|
||||||
|
if (responsible == null || StrUtil.isBlank(responsible.getUserId())) {
|
||||||
|
throw new BaseException("请从下拉框选择负责人");
|
||||||
|
}
|
||||||
|
String userId = responsible.getUserId().trim();
|
||||||
|
if (userId.length() > 32 || !courseUserIds.add(userId)) {
|
||||||
|
throw new BaseException("负责人ID无效或同一子活动重复设置了负责人");
|
||||||
|
}
|
||||||
|
String mobile = StrUtil.trim(responsible.getMobile());
|
||||||
|
if (StrUtil.isBlank(mobile) || mobile.length() > 20) {
|
||||||
|
throw new BaseException("请填写负责人联系方式,长度不能超过20个字符");
|
||||||
|
}
|
||||||
|
responsible.setUserId(userId).setMobile(mobile);
|
||||||
|
userIds.add(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Sys_user> users = userIds.isEmpty() ? Collections.emptyMap()
|
||||||
|
: dao().query(Sys_user.class, Cnd.where("id", "in", userIds)).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_user::getId, user -> user));
|
||||||
|
for (TrainSignUpCourse course : courses) {
|
||||||
|
List<TrainSignUpCourseResponsible> responsibles = course.getResponsibleList();
|
||||||
|
if (responsibles == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < responsibles.size(); i++) {
|
||||||
|
TrainSignUpCourseResponsible responsible = responsibles.get(i);
|
||||||
|
Sys_user user = users.get(responsible.getUserId());
|
||||||
|
if (user == null || StrUtil.isBlank(user.getUsername())) {
|
||||||
|
throw new BaseException("所选负责人不存在,请重新选择");
|
||||||
|
}
|
||||||
|
responsible.setUsername(user.getUsername()).setLoginname(user.getLoginname()).setOrderNum(i + 1);
|
||||||
|
}
|
||||||
|
// 原有报名和统计页面仍展示第一位联系人,完整姓名及多人信息保留在关系表。
|
||||||
|
// 原姓名列宽为20,此处只限制兼容展示字段,避免长姓名导致数据库写入失败。
|
||||||
|
course.setCourseInstructor(responsibles.isEmpty() ? "" : StrUtil.sub(responsibles.get(0).getUsername(), 0, 20));
|
||||||
|
course.setCourseInstructorMobile(responsibles.isEmpty() ? "" : responsibles.get(0).getMobile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随子活动在同一事务中保存负责人,关系ID及归属由后端生成,模板复制不会复用旧关系。
|
||||||
|
* null 表示未修改历史设置,不清理原有关系;空列表表示明确删除所有负责人。
|
||||||
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
private void saveCourseResponsibles(TrainSignUpCourse course) {
|
||||||
|
if (course.getResponsibleList() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("courseId", "=", course.getId()));
|
||||||
|
for (TrainSignUpCourseResponsible responsible : course.getResponsibleList()) {
|
||||||
|
responsible.setId(null).setActivityId(course.getActivityId()).setCourseId(course.getId());
|
||||||
|
}
|
||||||
|
if (!course.getResponsibleList().isEmpty()) {
|
||||||
|
dao().insert(course.getResponsibleList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量回显负责人及工号,不按旧姓名猜测用户身份,也不对每个子活动重复查询用户。 */
|
||||||
|
private void fillCourseResponsibles(List<TrainSignUpCourse> courses) {
|
||||||
|
if (courses.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> courseIds = courses.stream().map(TrainSignUpCourse::getId).collect(Collectors.toList());
|
||||||
|
List<TrainSignUpCourseResponsible> responsibles = dao().query(TrainSignUpCourseResponsible.class,
|
||||||
|
Cnd.where("courseId", "in", courseIds).asc("orderNum"));
|
||||||
|
Set<String> userIds = responsibles.stream().map(TrainSignUpCourseResponsible::getUserId).collect(Collectors.toSet());
|
||||||
|
Map<String, Sys_user> users = userIds.isEmpty() ? Collections.emptyMap()
|
||||||
|
: dao().query(Sys_user.class, Cnd.where("id", "in", userIds)).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_user::getId, user -> user));
|
||||||
|
for (TrainSignUpCourseResponsible responsible : responsibles) {
|
||||||
|
Sys_user user = users.get(responsible.getUserId());
|
||||||
|
if (user != null) {
|
||||||
|
responsible.setUsername(user.getUsername()).setLoginname(user.getLoginname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, List<TrainSignUpCourseResponsible>> byCourse = responsibles.stream()
|
||||||
|
.collect(Collectors.groupingBy(TrainSignUpCourseResponsible::getCourseId));
|
||||||
|
for (TrainSignUpCourse course : courses) {
|
||||||
|
List<TrainSignUpCourseResponsible> list = byCourse.get(course.getId());
|
||||||
|
// 旧活动保留 null,避免普通编辑保存时将未绑定用户的历史联系人清空。
|
||||||
|
course.setResponsibleList(list != null ? list :
|
||||||
|
(StrUtil.isNotBlank(course.getCourseInstructor()) || StrUtil.isNotBlank(course.getCourseInstructorMobile())
|
||||||
|
? null : new ArrayList<>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除主活动时在一个事务中清理负责人及原有业务关联,模板活动仍需先取消模板。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void deleteActivity(String id) {
|
||||||
|
requireActivityBasicPermission();
|
||||||
|
if (StrUtil.isBlank(id)) {
|
||||||
|
throw new BaseException("活动ID不能为空");
|
||||||
|
}
|
||||||
|
if (dao().fetch(Sys_home_template.class, id) != null) {
|
||||||
|
throw new BaseException("该活动已设为模板,请先取消模板后再删除");
|
||||||
|
}
|
||||||
|
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().clear(TrainSignUpCourse.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().clear(TrainSignUpActivityCourse.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().clear(TrainSignUpUser.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().clear(TrainSignUpUserCourse.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().clear(TrainSignUpActivity.class, Cnd.where("id", "=", id));
|
||||||
|
dao().clear(TrainSignUpTypeLimit.class, Cnd.where("activityId", "=", id));
|
||||||
|
dao().delete(Sys_home_activity.class, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void handleCourseTimes(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
private void handleCourseTimes(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
||||||
String courseId = course.getId();
|
String courseId = course.getId();
|
||||||
String activityId = activity.getId();
|
String activityId = activity.getId();
|
||||||
@@ -223,6 +591,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void setCourseTimeAndInsert(TrainSignUpCourse course) {
|
private void setCourseTimeAndInsert(TrainSignUpCourse course) {
|
||||||
course.getCourseTimeList().forEach(courseTime -> {
|
course.getCourseTimeList().forEach(courseTime -> {
|
||||||
Calendar calendar = Calendar.getInstance();
|
Calendar calendar = Calendar.getInstance();
|
||||||
@@ -250,7 +619,14 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void updateActivityStatus(TrainSignUpActivity activity) {
|
public void updateActivityStatus(TrainSignUpActivity activity) {
|
||||||
|
requireActivityBasicPermission();
|
||||||
|
if (StrUtil.isBlank(activity.getId()) || fetch(activity.getId()) == null) {
|
||||||
|
throw new BaseException("活动不存在");
|
||||||
|
}
|
||||||
updateIgnoreNull(activity);
|
updateIgnoreNull(activity);
|
||||||
|
// 活动启停及首页可见状态在同一个事务内更新,controller 仅负责接收参数和返回结果。
|
||||||
|
dao().update(Sys_home_activity.class, Chain.make("enable", !activity.isDisabled()),
|
||||||
|
Cnd.where("id", "=", activity.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -270,10 +646,14 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
activity.setCourseList(courseArray);
|
activity.setCourseList(courseArray);
|
||||||
|
|
||||||
List<TrainSignUpCourse> courseList = activity.getCourseList();
|
List<TrainSignUpCourse> courseList = activity.getCourseList();
|
||||||
|
fillCourseResponsibles(courseList);
|
||||||
|
|
||||||
courseList.forEach(c -> {
|
courseList.forEach(c -> {
|
||||||
|
dao().fetchLinks(c, "^(courseTimeList)$", Cnd.NEW().asc("courseStartTime"));
|
||||||
|
if (c.getCourseTimeList() == null) {
|
||||||
|
c.setCourseTimeList(new ArrayList<>());
|
||||||
|
}
|
||||||
if (StrUtil.isNotBlank(c.getCourseType())) {
|
if (StrUtil.isNotBlank(c.getCourseType())) {
|
||||||
dao().fetchLinks(c, "^(courseTimeList)$", Cnd.NEW().asc("courseStartTime"));
|
|
||||||
int courseCount = statisticsService.queryCourseCount(c.getId(), c.getCourseType());
|
int courseCount = statisticsService.queryCourseCount(c.getId(), c.getCourseType());
|
||||||
c.setHasRegisterNum(courseCount);
|
c.setHasRegisterNum(courseCount);
|
||||||
int courseWaitCount = statisticsService.queryCourseWaitCount(c.getId(), c.getCourseType());
|
int courseWaitCount = statisticsService.queryCourseWaitCount(c.getId(), c.getCourseType());
|
||||||
@@ -285,9 +665,36 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
return Lang.obj2nutmap(activity);
|
return Lang.obj2nutmap(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pageForm 为页码和每页条数,cnd 为已有活动筛选条件;返回原分页结构及活动列表。
|
||||||
|
* list 中新增 canManageCourse 布尔值,仅非校工会管理角色且负责当前活动的用户为 true。
|
||||||
|
* 该字段控制子活动入口显示,实际读写仍由 requireManagedCourse 校验负责人关系。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Pagination pageData(PageForm pageForm, Cnd cnd) {
|
public Pagination pageData(PageForm pageForm, Cnd cnd) {
|
||||||
Pagination pagination = listPageLinks(pageForm.getPageNumber(), pageForm.getPageSize(), cnd, "^(courseList)$");
|
Pagination pagination = listPageLinks(pageForm.getPageNumber(), pageForm.getPageSize(), cnd, "^(courseList)$");
|
||||||
|
List<TrainSignUpActivity> activities = pagination.getList(TrainSignUpActivity.class);
|
||||||
|
boolean canEditBasic = canEditActivityBasic();
|
||||||
|
String userId = SecurityUtil.getUserId();
|
||||||
|
Map<String, Set<String>> responsibleCourses = new HashMap<>();
|
||||||
|
// 只批量查询当前页活动的本人负责人关系,避免逐行查询,也不把普通用户当作负责人。
|
||||||
|
if (!canEditBasic && StrUtil.isNotBlank(userId) && !activities.isEmpty()) {
|
||||||
|
List<String> activityIds = activities.stream().map(TrainSignUpActivity::getId).toList();
|
||||||
|
List<TrainSignUpCourseResponsible> relations = dao().query(TrainSignUpCourseResponsible.class,
|
||||||
|
Cnd.where("activityId", "in", activityIds).and("userId", "=", userId));
|
||||||
|
for (TrainSignUpCourseResponsible relation : relations) {
|
||||||
|
responsibleCourses.computeIfAbsent(relation.getActivityId(), key -> new HashSet<>())
|
||||||
|
.add(relation.getCourseId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<NutMap> rows = activities.stream().map(activity -> {
|
||||||
|
Set<String> courseIds = responsibleCourses.getOrDefault(activity.getId(), Collections.emptySet());
|
||||||
|
// 同时核对实际子活动,已删除或已迁出的子活动关系不提供入口;管理员优先走编辑。
|
||||||
|
boolean canManageCourse = !canEditBasic && activity.getCourseList() != null
|
||||||
|
&& activity.getCourseList().stream().anyMatch(course -> courseIds.contains(course.getId()));
|
||||||
|
return Lang.obj2nutmap(activity).setv("canManageCourse", canManageCourse);
|
||||||
|
}).toList();
|
||||||
|
pagination.setList(rows);
|
||||||
return pagination;
|
return pagination;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +1025,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
|||||||
if (SecurityUtil.getUnionId().equals(o.getHostUnionId())) {
|
if (SecurityUtil.getUnionId().equals(o.getHostUnionId())) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
int compare = cn.hutool.core.date.DateUtil.compare(cn.hutool.core.date.DateUtil.date(), cn.hutool.core.date.DateUtil.parse(o.getInterTime()), "yyyy-MM-dd HH:mm");
|
int compare = DateUtil.compare(DateUtil.date(), DateUtil.parse(o.getInterTime()), "yyyy-MM-dd HH:mm");
|
||||||
return compare >= 0;
|
return compare >= 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+342
-3
@@ -1,25 +1,42 @@
|
|||||||
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
||||||
|
|
||||||
|
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.afterturn.easypoi.excel.export.ExcelExportService;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
|
import com.budwk.app.sys.models.Sys_file;
|
||||||
|
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||||
import com.budwk.app.zhgh.activity.family.models.FamilyType;
|
import com.budwk.app.zhgh.activity.family.models.FamilyType;
|
||||||
import com.budwk.app.zhgh.activity.family.models.FamilyUser;
|
import com.budwk.app.zhgh.activity.family.models.FamilyUser;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
||||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
import org.nutz.json.Json;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -37,8 +54,197 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 系统管理员、校工会管理员、校工会活动管理员任意一种角色可管理全部,其他角色按负责人控制。 */
|
||||||
|
private boolean hasAllTrainSignUpScope() {
|
||||||
|
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||||
|
RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将负责人范围放入数据库查询,使分页数据与总数、活动下拉框和导出保持一致。
|
||||||
|
* idField 仅由本 service 传入固定的表别名字段;操作人始终取登录 userId 并使用 SQL 参数绑定。
|
||||||
|
* activityLevel=true 按主活动筛选,false 按子活动筛选;没有登录身份时返回空范围。
|
||||||
|
*/
|
||||||
|
private void applyResponsibleScope(Sql sql, String idField, boolean activityLevel) {
|
||||||
|
if (hasAllTrainSignUpScope()) {
|
||||||
|
sql.setSourceSql(sql.getSourceSql().replace("$responsibleScope", ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String userId = SecurityUtil.getUserId();
|
||||||
|
if (StrUtil.isBlank(userId)) {
|
||||||
|
sql.setSourceSql(sql.getSourceSql().replace("$responsibleScope", " AND 1=0 "));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 同时关联实际子活动,已删除子活动或活动归属不一致的关系不能提供管理权限。
|
||||||
|
String scope = " AND EXISTS (SELECT 1 FROM train_sign_up_course_responsible responsible "
|
||||||
|
+ "INNER JOIN train_sign_up_course responsibleCourse ON responsibleCourse.id=responsible.courseId "
|
||||||
|
+ "AND responsibleCourse.activityId=responsible.activityId WHERE "
|
||||||
|
+ (activityLevel ? "responsibleCourse.activityId=" : "responsibleCourse.id=")
|
||||||
|
+ idField + " AND responsible.userId=@responsibleUserId) ";
|
||||||
|
// Nutz 不会解析 setVar 字符串中的 @参数,必须先替换原始SQL并重新解析,再绑定用户ID。
|
||||||
|
sql.setSourceSql(sql.getSourceSql().replace("$responsibleScope", scope));
|
||||||
|
sql.setParam("responsibleUserId", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按实际子活动校验详情及操作权限;缺少ID不能省略查询条件后查询全部报名人员。 */
|
||||||
|
private TrainSignUpCourse requireCourseScope(String courseId) {
|
||||||
|
if (StrUtil.isBlank(courseId)) {
|
||||||
|
throw new BaseException("子活动ID不能为空");
|
||||||
|
}
|
||||||
|
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
||||||
|
if (course == null) {
|
||||||
|
throw new BaseException("子活动不存在");
|
||||||
|
}
|
||||||
|
if (!hasAllTrainSignUpScope()) {
|
||||||
|
String userId = SecurityUtil.getUserId();
|
||||||
|
if (StrUtil.isBlank(userId) || dao().count(TrainSignUpCourseResponsible.class,
|
||||||
|
Cnd.where("courseId", "=", courseId).and("activityId", "=", course.getActivityId())
|
||||||
|
.and("userId", "=", userId)) == 0) {
|
||||||
|
throw new BaseException("无权查看或操作该子活动,请检查负责人设置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主活动ID用于校验归属,不能将其他活动的子活动作为调整来源或目标。 */
|
||||||
|
private void requireActivityMatch(String activityId, TrainSignUpCourse course) {
|
||||||
|
if (StrUtil.isBlank(activityId) || !activityId.equals(course.getActivityId())) {
|
||||||
|
throw new BaseException("子活动不属于当前主活动");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TrainSignUpActivity> activityList(Integer year, boolean enabledOnly) {
|
||||||
|
// 可选条件也在创建SQL前确定,让年度、启用状态和负责人ID均由预编译参数绑定。
|
||||||
|
Sql sql = Sqls.create("SELECT activity.* FROM train_sign_up_activity activity WHERE 1=1 "
|
||||||
|
+ (year == null ? "" : " AND activity.year=@year ")
|
||||||
|
+ (enabledOnly ? " AND activity.isDisabled=@disabled " : "")
|
||||||
|
+ " $responsibleScope ORDER BY activity.activityStartTime DESC");
|
||||||
|
sql.setParam("year", year);
|
||||||
|
sql.setParam("disabled", false);
|
||||||
|
applyResponsibleScope(sql, "activity.id", true);
|
||||||
|
sql.setEntity(dao().getEntity(TrainSignUpActivity.class));
|
||||||
|
sql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(sql);
|
||||||
|
return sql.getList(TrainSignUpActivity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> getCourse(String activityId) {
|
||||||
|
if (StrUtil.isBlank(activityId)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT tsuc.id, tsuc.courseName, tsuc.coursePeopleNumber, tsuc.courseType,
|
||||||
|
tsuc.courseLocation, tsuc.courseInstructor, tsuc.courseReservedNumber,
|
||||||
|
tsuc.waitingNum, tsuc.reserveMode
|
||||||
|
FROM train_sign_up_course tsuc
|
||||||
|
WHERE tsuc.activityId=@activityId $responsibleScope
|
||||||
|
ORDER BY tsuc.courseName ASC
|
||||||
|
""");
|
||||||
|
sql.setParam("activityId", activityId);
|
||||||
|
applyResponsibleScope(sql, "tsuc.id", false);
|
||||||
|
List<NutMap> courses = listMap(sql);
|
||||||
|
courses.forEach(course -> {
|
||||||
|
course.put("registerNum", queryCourseCount(course.getString("id"), course.getString("courseType")));
|
||||||
|
course.put("hasWaitingNum", queryCourseWaitCount(course.getString("id"), course.getString("courseType")));
|
||||||
|
});
|
||||||
|
return courses;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TrainSignUpActivityCourse> selectTimes(String courseId) {
|
||||||
|
requireCourseScope(courseId);
|
||||||
|
return dao().query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", courseId).asc("courseStartTime"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void signChange(String courseId, Boolean openOtherUnion) {
|
||||||
|
requireCourseScope(courseId);
|
||||||
|
if (openOtherUnion == null) {
|
||||||
|
throw new BaseException("请指定是否开放报名");
|
||||||
|
}
|
||||||
|
dao().update(TrainSignUpCourse.class, Chain.make("openOtherUnion", openOtherUnion), Cnd.where("id", "=", courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void adjustSign(String id) {
|
||||||
|
if (StrUtil.isBlank(id)) {
|
||||||
|
throw new BaseException("签到记录ID不能为空");
|
||||||
|
}
|
||||||
|
TrainSignUpUserCourse userCourse = dao().fetch(TrainSignUpUserCourse.class, id);
|
||||||
|
if (userCourse == null) {
|
||||||
|
throw new BaseException("签到记录不存在");
|
||||||
|
}
|
||||||
|
// 通过记录实际所属子活动验证权限,不接受前端指定负责人身份。
|
||||||
|
requireCourseScope(userCourse.getCourseId());
|
||||||
|
if (userCourse.isAttend()) {
|
||||||
|
userCourse.setAttend(false);
|
||||||
|
userCourse.setAttendTime(null);
|
||||||
|
} else {
|
||||||
|
userCourse.setAttend(true);
|
||||||
|
userCourse.setAttendTime(userCourse.getCourseStartTime());
|
||||||
|
}
|
||||||
|
dao().update(userCourse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||||
|
if (StrUtil.isBlank(userId)) {
|
||||||
|
throw new BaseException("报名人员用户ID不能为空");
|
||||||
|
}
|
||||||
|
// 来源与目标均受负责人范围约束,且必须属于同一主活动,防止绕过目标列表迁移数据。
|
||||||
|
requireActivityMatch(activityId, requireCourseScope(oldCourseId));
|
||||||
|
requireActivityMatch(activityId, requireCourseScope(newCourseId));
|
||||||
|
Cnd oldCnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", oldCourseId).and("userId", "=", userId);
|
||||||
|
TrainSignUpUser oldUser = dao().fetch(TrainSignUpUser.class, oldCnd);
|
||||||
|
if (oldUser == null) {
|
||||||
|
throw new BaseException("原报名记录不存在,请刷新后重试");
|
||||||
|
}
|
||||||
|
oldUser.setCourseId(newCourseId);
|
||||||
|
oldUser.setSignUpTime(DateUtil.date());
|
||||||
|
dao().update(oldUser);
|
||||||
|
|
||||||
|
// 沿用现有人员调整业务,按目标子活动时段重建签到记录,并在同一事务中提交。
|
||||||
|
List<TrainSignUpActivityCourse> times = dao().query(TrainSignUpActivityCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("courseId", "=", newCourseId));
|
||||||
|
dao().clear(TrainSignUpUserCourse.class, oldCnd);
|
||||||
|
List<TrainSignUpUserCourse> userCourses = new ArrayList<>();
|
||||||
|
times.forEach(item -> {
|
||||||
|
TrainSignUpUserCourse course = new TrainSignUpUserCourse();
|
||||||
|
course.setActivityCourseId(activityId);
|
||||||
|
course.setCourseId(newCourseId);
|
||||||
|
course.setUserId(userId);
|
||||||
|
course.setCourseStartTime(item.getCourseStartTime());
|
||||||
|
course.setCourseEndTime(item.getCourseEndTime());
|
||||||
|
course.setActivityCourseId(item.getId());
|
||||||
|
userCourses.add(course);
|
||||||
|
});
|
||||||
|
dao().insert(userCourses);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void deleteSignUser(String activityId, String courseId, String userId) {
|
||||||
|
if (StrUtil.isBlank(userId)) {
|
||||||
|
throw new BaseException("报名人员用户ID不能为空");
|
||||||
|
}
|
||||||
|
requireActivityMatch(activityId, requireCourseScope(courseId));
|
||||||
|
Cnd cnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId).and("userId", "=", userId);
|
||||||
|
// 报名及对应签到记录必须一并删除,禁止只传用户ID造成跨子活动删除。
|
||||||
|
dao().clear(TrainSignUpUser.class, cnd);
|
||||||
|
dao().clear(TrainSignUpUserCourse.class, cnd);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination pageData(PageForm pageForm, String activityId) {
|
public Pagination pageData(PageForm pageForm, String activityId) {
|
||||||
|
// 没有可选主活动时返回空分页,不能将缺少筛选条件解释为查询所有子活动。
|
||||||
|
if (StrUtil.isBlank(activityId)) {
|
||||||
|
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>());
|
||||||
|
}
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
tsuc.id,
|
tsuc.id,
|
||||||
@@ -61,9 +267,11 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
train_sign_up_type type on tsuc.courseType = type.id
|
train_sign_up_type type on tsuc.courseType = type.id
|
||||||
WHERE
|
WHERE
|
||||||
tsuc.activityId = @activityId
|
tsuc.activityId = @activityId
|
||||||
|
$responsibleScope
|
||||||
ORDER BY tsuc.orderNum
|
ORDER BY tsuc.orderNum
|
||||||
""");
|
""");
|
||||||
sql.setParam("activityId", activityId);
|
sql.setParam("activityId", activityId);
|
||||||
|
applyResponsibleScope(sql, "tsuc.id", false);
|
||||||
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
List<NutMap> courseList = pagination.getList();
|
List<NutMap> courseList = pagination.getList();
|
||||||
courseList.forEach(c -> {
|
courseList.forEach(c -> {
|
||||||
@@ -75,6 +283,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NutMap> registerUserList(String courseId) {
|
public List<NutMap> registerUserList(String courseId) {
|
||||||
|
requireCourseScope(courseId);
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
u.loginname,
|
u.loginname,
|
||||||
@@ -92,11 +301,13 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
train_sign_up_user tsuu
|
train_sign_up_user tsuu
|
||||||
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
|
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
|
||||||
$condition
|
$condition
|
||||||
|
$responsibleScope
|
||||||
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
|
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.andEX("tsuu.courseId", "=", courseId);
|
cnd.and("tsuu.courseId", "=", courseId);
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
|
applyResponsibleScope(sql, "tsuu.courseId", false);
|
||||||
List<NutMap> list = listMap(sql);
|
List<NutMap> list = listMap(sql);
|
||||||
for (NutMap o : list) {
|
for (NutMap o : list) {
|
||||||
if(StrUtil.isBlank(o.getString("mobileColumnsValue"))) {
|
if(StrUtil.isBlank(o.getString("mobileColumnsValue"))) {
|
||||||
@@ -114,7 +325,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NutMap> getTaleColumnInfo(String courseId) {
|
public List<NutMap> getTaleColumnInfo(String courseId) {
|
||||||
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
TrainSignUpCourse course = requireCourseScope(courseId);
|
||||||
TrainSignUpType upType = dao().fetch(TrainSignUpType.class, course.getCourseType());
|
TrainSignUpType upType = dao().fetch(TrainSignUpType.class, course.getCourseType());
|
||||||
dao().fetchLinks(upType, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
dao().fetchLinks(upType, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
||||||
|
|
||||||
@@ -125,6 +336,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword) {
|
public List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword) {
|
||||||
|
requireCourseScope(courseId);
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
tsuu.id,
|
tsuu.id,
|
||||||
@@ -141,10 +353,11 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
train_sign_up_user tsuu
|
train_sign_up_user tsuu
|
||||||
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
|
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
|
||||||
$condition
|
$condition
|
||||||
|
$responsibleScope
|
||||||
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
|
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.andEX("tsuu.courseId", "=", courseId);
|
cnd.and("tsuu.courseId", "=", courseId);
|
||||||
cnd.andEX("u.unionid", "=", unionId);
|
cnd.andEX("u.unionid", "=", unionId);
|
||||||
cnd.andEX("u.unitid", "=", unitId);
|
cnd.andEX("u.unitid", "=", unitId);
|
||||||
if (StrUtil.isNotBlank(searchKeyword)) {
|
if (StrUtil.isNotBlank(searchKeyword)) {
|
||||||
@@ -153,11 +366,13 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
cnd.and(group);
|
cnd.and(group);
|
||||||
}
|
}
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
|
applyResponsibleScope(sql, "tsuu.courseId", false);
|
||||||
return listMap(sql);
|
return listMap(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination getSignInfo(PageForm pageForm, String courseId, String timeId) {
|
public Pagination getSignInfo(PageForm pageForm, String courseId, String timeId) {
|
||||||
|
requireCourseScope(courseId);
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
tsuuc.id,
|
tsuuc.id,
|
||||||
@@ -173,6 +388,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
`train_sign_up_user_course` tsuuc
|
`train_sign_up_user_course` tsuuc
|
||||||
LEFT JOIN `vw_user` u ON u.id = tsuuc.userId
|
LEFT JOIN `vw_user` u ON u.id = tsuuc.userId
|
||||||
$condition
|
$condition
|
||||||
|
$responsibleScope
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.and("tsuuc.courseId", "=", courseId);
|
cnd.and("tsuuc.courseId", "=", courseId);
|
||||||
@@ -180,6 +396,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
cnd.desc("isAttend");
|
cnd.desc("isAttend");
|
||||||
cnd.desc("attendTime");
|
cnd.desc("attendTime");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
|
applyResponsibleScope(sql, "tsuuc.courseId", false);
|
||||||
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,12 +418,134 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
|||||||
LEFT JOIN `vw_user` u ON u.id = uu.userId
|
LEFT JOIN `vw_user` u ON u.id = uu.userId
|
||||||
WHERE
|
WHERE
|
||||||
uc.activityId = @activityId
|
uc.activityId = @activityId
|
||||||
|
$responsibleScope
|
||||||
ORDER BY u.unitCode,u.unioncode,u.sex
|
ORDER BY u.unitCode,u.unioncode,u.sex
|
||||||
""");
|
""");
|
||||||
sql.setParam("activityId", activityId);
|
sql.setParam("activityId", activityId);
|
||||||
|
applyResponsibleScope(sql, "uc.id", false);
|
||||||
return listMap(sql);
|
return listMap(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NutMap buildSignUserExport(String activityId) {
|
||||||
|
if (StrUtil.isBlank(activityId)) {
|
||||||
|
throw new BaseException("请选择要导出的活动");
|
||||||
|
}
|
||||||
|
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, activityId);
|
||||||
|
if (activity == null) {
|
||||||
|
throw new BaseException("活动不存在");
|
||||||
|
}
|
||||||
|
// 工作表和报名数据分别在SQL中约束负责人范围,不能仅根据前端可见活动判断导出权限。
|
||||||
|
Sql courseSql = Sqls.create("""
|
||||||
|
SELECT course.* FROM train_sign_up_course course
|
||||||
|
WHERE course.activityId=@activityId $responsibleScope
|
||||||
|
""");
|
||||||
|
courseSql.setParam("activityId", activityId);
|
||||||
|
applyResponsibleScope(courseSql, "course.id", false);
|
||||||
|
courseSql.setEntity(dao().getEntity(TrainSignUpCourse.class));
|
||||||
|
courseSql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(courseSql);
|
||||||
|
List<TrainSignUpCourse> courseList = courseSql.getList(TrainSignUpCourse.class);
|
||||||
|
if (courseList.isEmpty()) {
|
||||||
|
throw new BaseException("没有可导出的子活动,请检查负责人设置");
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT ts.*,
|
||||||
|
CONCAT(DATE_FORMAT(ac.courseStartTime, '%H:%i:%s'),'至',DATE_FORMAT(ac.courseEndTime, '%H:%i:%s')) AS courseTime,
|
||||||
|
u.username, u.loginname, u.sex, IFNULL(ts.mobile, u.mobile) AS mobile,
|
||||||
|
u.birthday, tsc.courseName
|
||||||
|
FROM train_sign_up_user ts
|
||||||
|
LEFT JOIN train_sign_up_activity_course ac ON ts.activityCourseId=ac.id
|
||||||
|
LEFT JOIN vw_user u ON u.id=ts.userId
|
||||||
|
LEFT JOIN train_sign_up_course tsc ON tsc.id=ts.courseId
|
||||||
|
WHERE ts.activityId=@activityId AND tsc.activityId=@activityId $responsibleScope
|
||||||
|
""");
|
||||||
|
sql.setParam("activityId", activityId);
|
||||||
|
applyResponsibleScope(sql, "tsc.id", false);
|
||||||
|
List<NutMap> userList = listMap(sql);
|
||||||
|
|
||||||
|
// 保留原导出公共列、动态报名列和图片处理,仅改变数据范围及Excel文件类型。
|
||||||
|
List<TrainSignUpType> types = dao().query(TrainSignUpType.class, Cnd.NEW());
|
||||||
|
dao().fetchLinks(types, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
||||||
|
Map<String, TrainSignUpType> typeMap = types.stream().collect(Collectors.toMap(TrainSignUpType::getId, type -> type));
|
||||||
|
List<Map<String, Object>> sheetsList = new ArrayList<>();
|
||||||
|
List<ExcelExportEntity> commonEntities = new ArrayList<>();
|
||||||
|
commonEntities.add(new ExcelExportEntity("姓名", "username", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("工号", "loginname", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("单位", "unitName", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("分工会", "unionName", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("性别", "sex", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("手机号", "mobile", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("生日", "birthday", 20));
|
||||||
|
commonEntities.add(new ExcelExportEntity("报名时段", "courseTime", 20));
|
||||||
|
for (TrainSignUpCourse course : courseList) {
|
||||||
|
String name = course.getCourseName();
|
||||||
|
List<NutMap> users = userList.stream().filter(user -> StrUtil.isNotBlank(user.getString("courseName"))
|
||||||
|
&& user.getString("courseName").equals(name)).collect(Collectors.toList());
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
exportParams.setSheetName(name);
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>(commonEntities);
|
||||||
|
TrainSignUpType type = typeMap.get(course.getCourseType());
|
||||||
|
if (Lang.isNotEmpty(type.getTrainMobileSignColumnList())) {
|
||||||
|
for (TrainMobileSignColumn column : type.getTrainMobileSignColumnList()) {
|
||||||
|
ExcelExportEntity entity = new ExcelExportEntity();
|
||||||
|
entity.setName(column.getColumnName());
|
||||||
|
entity.setKey(column.getColumnCode());
|
||||||
|
entity.setWidth(20);
|
||||||
|
if ("FILE".equals(column.getColumnFormType())) {
|
||||||
|
entity.setType(2);
|
||||||
|
entity.setExportImageType(2);
|
||||||
|
}
|
||||||
|
entities.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (NutMap user : users) {
|
||||||
|
String values = user.getString("mobileColumnsValue");
|
||||||
|
if (StrUtil.isBlank(values)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (NutMap value : Json.fromJsonAsList(NutMap.class, values)) {
|
||||||
|
if (!"FILE".equals(value.getString("columnFormType"))) {
|
||||||
|
user.put(value.getString("columnCode"), value.getString("columnValue"));
|
||||||
|
} else if (StrUtil.isNotBlank(value.getString("columnValue"))) {
|
||||||
|
List<NutMap> files = value.getAsList("columnValue", NutMap.class);
|
||||||
|
if (files.size() == 1) {
|
||||||
|
NutMap fileData = files.get(0);
|
||||||
|
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", fileData.get("url")));
|
||||||
|
byte[] imageBytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
|
||||||
|
if (imageBytes.length > 0) {
|
||||||
|
user.put(value.getString("columnCode"), imageBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> sheet = new HashMap<>();
|
||||||
|
sheet.put("name", name);
|
||||||
|
sheet.put("title", exportParams);
|
||||||
|
sheet.put("entity", entities);
|
||||||
|
sheet.put("data", users);
|
||||||
|
sheetsList.add(sheet);
|
||||||
|
}
|
||||||
|
Workbook workbook = new XSSFWorkbook();
|
||||||
|
try {
|
||||||
|
for (Map<String, Object> sheet : sheetsList) {
|
||||||
|
new ExcelExportService().createSheetForMap(workbook, (ExportParams) sheet.get("title"),
|
||||||
|
(List<ExcelExportEntity>) sheet.get("entity"), (Collection<?>) sheet.get("data"));
|
||||||
|
}
|
||||||
|
return NutMap.NEW().setv("fileName", activity.getActivityName() + "报名人员名单.xlsx").setv("workbook", workbook);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
// 构建失败时尚未交给下载层,需在此释放工作簿;成功时由下载方法统一关闭。
|
||||||
|
try {
|
||||||
|
workbook.close();
|
||||||
|
} catch (IOException closeException) {
|
||||||
|
exception.addSuppressed(closeException);
|
||||||
|
}
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int queryCourseCount(String courseId, String courseType) {
|
public int queryCourseCount(String courseId, String courseType) {
|
||||||
return this.queryCourseCount(courseId, courseType, null);
|
return this.queryCourseCount(courseId, courseType, null);
|
||||||
|
|||||||
+15
-3
@@ -1,6 +1,7 @@
|
|||||||
package com.budwk.app.zhgh.club.controller.apply;
|
package com.budwk.app.zhgh.club.controller.apply;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaMode;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
@@ -47,8 +48,19 @@ public class ClubUserClubListController {
|
|||||||
public void index() {
|
public void index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开移动端社团列表,共用电脑端的社团查询及入退会状态。
|
||||||
|
*
|
||||||
|
* @return 无返回值,由模板渲染社团列表页面
|
||||||
|
*/
|
||||||
|
@At("/h5")
|
||||||
|
@Ok("beetl:/platform/zhghh5/club/list/index.html")
|
||||||
|
@SaCheckPermission(value = {"h5.club.join.list", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
|
public void h5Index() {
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("club.join.list")
|
@SaCheckPermission(value = {"club.join.list", "h5.club.join.list", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
public Result pageData(@Valid PageForm pageForm, Boolean hasJoin, String clubName, String clubType) {
|
public Result pageData(@Valid PageForm pageForm, Boolean hasJoin, String clubName, String clubType) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.and("dismiss", "=", false);
|
cnd.and("dismiss", "=", false);
|
||||||
@@ -71,7 +83,7 @@ public class ClubUserClubListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("club.join.list")
|
@SaCheckPermission(value = {"club.join.list", "h5.club.join.list", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
public Result getCount() {
|
public Result getCount() {
|
||||||
// 获取所有社团
|
// 获取所有社团
|
||||||
List<SysClub> list = sysClubService.query();
|
List<SysClub> list = sysClubService.query();
|
||||||
@@ -96,7 +108,7 @@ public class ClubUserClubListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("club.join.list")
|
@SaCheckPermission(value = {"club.join.list", "h5.club.join.list", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
public Result findOne(@Valid String id) {
|
public Result findOne(@Valid String id) {
|
||||||
ClubRegisterVo clubRegisterVo = sysClubService.findOne(id);
|
ClubRegisterVo clubRegisterVo = sysClubService.findOne(id);
|
||||||
return Result.success(clubRegisterVo);
|
return Result.success(clubRegisterVo);
|
||||||
|
|||||||
+22
-8
@@ -74,10 +74,22 @@ public class ClubUserJoinApplyController {
|
|||||||
public void index() {
|
public void index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动端首次入会先选择社团,已选社团或已有申请继续进入申请表单。
|
||||||
|
*
|
||||||
|
* @param clubId 已选择的社团ID
|
||||||
|
* @param bizId 已有入会申请ID
|
||||||
|
* @param taskId 重新提交申请时的流程任务ID
|
||||||
|
* @return 无申请上下文时跳转社团列表,否则渲染原申请页面
|
||||||
|
*/
|
||||||
@At("/h5")
|
@At("/h5")
|
||||||
@SaCheckPermission("h5.club.join.apply")
|
@SaCheckPermission("h5.club.join.apply")
|
||||||
@Ok("beetl:/platform/zhghh5/club/apply/index.html")
|
@Ok("re")
|
||||||
public void h5Index() {
|
public String h5Index(@Param("clubId") String clubId, @Param("bizId") String bizId, @Param("taskId") String taskId) {
|
||||||
|
if (StrUtil.isBlank(clubId) && StrUtil.isBlank(bizId) && StrUtil.isBlank(taskId)) {
|
||||||
|
return ">>:/platform/club/join/list/h5";
|
||||||
|
}
|
||||||
|
return "beetl:/platform/zhghh5/club/apply/index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@@ -87,6 +99,7 @@ public class ClubUserJoinApplyController {
|
|||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
cua.*,
|
cua.*,
|
||||||
|
CAST(cua.mode AS SIGNED) AS mode,
|
||||||
club.clubName,
|
club.clubName,
|
||||||
u.loginname AS loginName,
|
u.loginname AS loginName,
|
||||||
u.username AS userName,
|
u.username AS userName,
|
||||||
@@ -109,14 +122,14 @@ public class ClubUserJoinApplyController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("保存申请")
|
@ApiOperation("保存申请")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
@SLog(tag = "社团管理系统-申请入会", msg = "保存社团入会")
|
@SLog(tag = "社团管理系统-申请入会", msg = "保存社团入会")
|
||||||
public Result save(@Param("data") ClubUserApply clubUserApply) {
|
public Result save(@Param("data") ClubUserApply clubUserApply) {
|
||||||
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
||||||
clubUserApply.setMode(true);
|
clubUserApply.setMode(1);
|
||||||
clubUserApply.setApplyDate(new Date());
|
clubUserApply.setApplyDate(new Date());
|
||||||
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
clubUserJoinService.preparePendingJoinTimes(clubUserApply);
|
||||||
clubUserApply.setExitTime(null);
|
|
||||||
dao.insertOrUpdate(clubUserApply);
|
dao.insertOrUpdate(clubUserApply);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
@@ -151,13 +164,13 @@ public class ClubUserJoinApplyController {
|
|||||||
if(!mode) {
|
if(!mode) {
|
||||||
clubUserApply.setId(null);
|
clubUserApply.setId(null);
|
||||||
}
|
}
|
||||||
clubUserApply.setMode(mode);
|
// 请求参数沿用 PC/H5 的布尔值;持久化统一使用 1 入会、0 退会,2 仅由管理端生成。
|
||||||
|
clubUserApply.setMode(Boolean.TRUE.equals(mode) ? 1 : 0);
|
||||||
Date applyDate = new Date();
|
Date applyDate = new Date();
|
||||||
clubUserApply.setApplyDate(applyDate);
|
clubUserApply.setApplyDate(applyDate);
|
||||||
if (mode) {
|
if (mode) {
|
||||||
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
||||||
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
clubUserJoinService.preparePendingJoinTimes(clubUserApply);
|
||||||
clubUserApply.setExitTime(null);
|
|
||||||
} else {
|
} else {
|
||||||
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
||||||
clubUserApply.setExitTime(applyDate);
|
clubUserApply.setExitTime(applyDate);
|
||||||
@@ -184,6 +197,7 @@ public class ClubUserJoinApplyController {
|
|||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
public Result submitAgain(@Param("data") ClubUserApply clubUserApply, @Param("taskId") Long taskId) {
|
public Result submitAgain(@Param("data") ClubUserApply clubUserApply, @Param("taskId") Long taskId) {
|
||||||
|
clubUserJoinService.preparePendingJoinTimes(clubUserApply);
|
||||||
dao.insertOrUpdate(clubUserApply);
|
dao.insertOrUpdate(clubUserApply);
|
||||||
|
|
||||||
Dict dict = Dict.create();
|
Dict dict = Dict.create();
|
||||||
|
|||||||
+22
-1
@@ -65,9 +65,30 @@ public class ClubUserJoinMineController {
|
|||||||
public Result pageData(@Valid PageForm pageForm,
|
public Result pageData(@Valid PageForm pageForm,
|
||||||
@Param("year") Integer year,
|
@Param("year") Integer year,
|
||||||
@Param("clubName") String clubName){
|
@Param("clubName") String clubName){
|
||||||
|
// 明确列出申请字段,避免分页总数子查询出现重名列;mode 只输出一次整数转换,保留系统处理值 2。
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
info.*,
|
info.id,
|
||||||
|
info.clubId,
|
||||||
|
info.userId,
|
||||||
|
info.roleCode,
|
||||||
|
info.clubPosition,
|
||||||
|
info.email,
|
||||||
|
info.mobile,
|
||||||
|
info.birthday,
|
||||||
|
info.avatar,
|
||||||
|
info.sameTimeJoinOtherClubSituation,
|
||||||
|
info.awardsExperience,
|
||||||
|
info.signature,
|
||||||
|
info.applyDate,
|
||||||
|
info.joinTime,
|
||||||
|
info.exitTime,
|
||||||
|
info.createdBy,
|
||||||
|
info.createdAt,
|
||||||
|
info.updatedBy,
|
||||||
|
info.updatedAt,
|
||||||
|
info.delFlag,
|
||||||
|
CAST(info.mode AS SIGNED) AS mode,
|
||||||
club.clubName,
|
club.clubName,
|
||||||
u.loginname as loginName,
|
u.loginname as loginName,
|
||||||
u.username as userName,
|
u.username as userName,
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package com.budwk.app.zhgh.club.controller.common;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaMode;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
|
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
|
||||||
import com.budwk.app.bpm.enums.BpmProcessTaskStatusEnum;
|
import com.budwk.app.bpm.enums.BpmProcessTaskStatusEnum;
|
||||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||||
|
import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
|
||||||
|
import com.budwk.app.sys.services.SysFileService;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
@@ -19,10 +24,17 @@ import org.nutz.dao.Sqls;
|
|||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.mvc.annotation.AdaptBy;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.upload.TempFile;
|
||||||
|
import org.nutz.mvc.upload.UploadAdaptor;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@At("/platform/club/common")
|
@At("/platform/club/common")
|
||||||
@@ -36,21 +48,42 @@ public class ClubCommonController {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private SysClubService sysClubService;
|
private SysClubService sysClubService;
|
||||||
|
@Inject
|
||||||
|
private SysFileService sysFileService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传社团缩略图,限制图片大小并复用系统文件服务校验图片内容及保存文件。
|
||||||
|
*
|
||||||
|
* @param file 用户上传的 JPG、PNG 或 GIF 图片,大小不得超过 2MB
|
||||||
|
* @return 成功时返回图片下载地址,文件无效或超过限制时返回错误提示
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||||
|
@SaCheckPermission(value = {"club.register.apply", "club.infoManage.manage"}, mode = SaMode.OR)
|
||||||
|
public Result uploadThumbnail(@Param("file") TempFile file) {
|
||||||
|
if (file == null || StrUtil.isBlank(file.getSubmittedFileName())) {
|
||||||
|
return Result.error("请选择社团缩略图");
|
||||||
|
}
|
||||||
|
if (file.getSize() > 2L * 1024 * 1024) {
|
||||||
|
return Result.error("社团缩略图大小不能超过2MB");
|
||||||
|
}
|
||||||
|
String suffix = FileUtil.extName(file.getSubmittedFileName()).toLowerCase(Locale.ROOT);
|
||||||
|
if (!Set.of("jpg", "jpeg", "png", "gif").contains(suffix)) {
|
||||||
|
return Result.error("社团缩略图仅支持JPG、PNG、GIF图片");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Result.success().addData(sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(), file));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
|
/** 无请求参数;返回入会社团选项,每项包含 id、clubName 等社团字段,同一社团不重复。 */
|
||||||
public Result listClub() {
|
public Result listClub() {
|
||||||
Sql sql = Sqls.create("""
|
return Result.success(sysClubService.listJoinClubOptions());
|
||||||
SELECT
|
|
||||||
c.*
|
|
||||||
FROM
|
|
||||||
`sys_club` c
|
|
||||||
LEFT JOIN wf_process_instance inst ON inst.businessNo = c.id
|
|
||||||
WHERE
|
|
||||||
inst.state = @state
|
|
||||||
""").setParam("state", ProcessInstanceStateEnum.FINISHED.getCode());
|
|
||||||
List list = baseService.listVO(sql, SysClub.class);
|
|
||||||
return Result.success(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
|||||||
+24
-9
@@ -93,15 +93,30 @@ public class ClubExamineApplyController {
|
|||||||
return Result.success(myManageClub);
|
return Result.success(myManageClub);
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
// 2026年10月2日wxg屏蔽操作:保留原统计参数处理入口。
|
||||||
@SaCheckLogin
|
// @At
|
||||||
public Result getClubUserNum(String clubId, Integer year) {
|
// @SaCheckLogin
|
||||||
if(StrUtil.isBlank(clubId)) {
|
// public Result getClubUserNum(String clubId, Integer year) {
|
||||||
return Result.error("社团信息为空,请核查");
|
// if(StrUtil.isBlank(clubId)) {
|
||||||
}
|
// return Result.error("社团信息为空,请核查");
|
||||||
List<NutMap> result = sysClubExamineService.getClubUserNum(clubId, year);
|
// }
|
||||||
return Result.success(result);
|
// List<NutMap> result = sysClubExamineService.getClubUserNum(clubId, year);
|
||||||
}
|
// return Result.success(result);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
/**
|
||||||
|
* 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
|
* 接收 clubId(所选社团ID)、year(公历年度),基础校验后调用 service;
|
||||||
|
* 返回 Result,data为两类计数及统计核查信息列表,异常参数返回错误提示。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckLogin
|
||||||
|
public Result getClubUserNum(String clubId, Integer year) {
|
||||||
|
if (StrUtil.isBlank(clubId)) return Result.error("社团信息为空,请核查");
|
||||||
|
if (year == null || year < 1 || year > 9998) return Result.error("请选择有效统计年度");
|
||||||
|
return Result.success(sysClubExamineService.getClubUserNum(clubId, year));
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
|
|||||||
+22
-13
@@ -30,6 +30,7 @@ import com.budwk.app.zhgh.club.model.SysClub;
|
|||||||
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubInfoManageService;
|
import com.budwk.app.zhgh.club.service.SysClubInfoManageService;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubUserImportVo;
|
import com.budwk.app.zhgh.club.vo.ClubUserImportVo;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -77,6 +78,8 @@ public class ClubInfoManageController {
|
|||||||
private SysUserService sysUserService;
|
private SysUserService sysUserService;
|
||||||
@Inject
|
@Inject
|
||||||
private SysClubUserService clubUserService;
|
private SysClubUserService clubUserService;
|
||||||
|
@Inject
|
||||||
|
private ClubPayRecordService payRecordService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/club/infoManage/manage/index.html")
|
@Ok("beetl:/platform/zhgh/club/infoManage/manage/index.html")
|
||||||
@@ -137,19 +140,7 @@ public class ClubInfoManageController {
|
|||||||
@SaCheckPermission("club.infoManage.manage")
|
@SaCheckPermission("club.infoManage.manage")
|
||||||
@SLog(tag = "社团管理系统-信息管理", msg = "退会")
|
@SLog(tag = "社团管理系统-信息管理", msg = "退会")
|
||||||
public Result exitClub(@Valid String id) {
|
public Result exitClub(@Valid String id) {
|
||||||
ClubUser clubUser = dao.fetch(ClubUser.class, id);
|
clubInfoManageService.exitClub(id);
|
||||||
dao.delete(ClubUser.class,id);
|
|
||||||
List<String> roleCodes = clubUser.getRoleCode();
|
|
||||||
for (String roleCode : roleCodes) {
|
|
||||||
Sys_role sysRole = sysRoleService.getByCode(roleCode);
|
|
||||||
if (ObjectUtil.isNotEmpty(sysRole)) {
|
|
||||||
dao.clear(Sys_user_role.class, Cnd.NEW().and("userId", "=", clubUser.getUserId())
|
|
||||||
.and("roleId", "=", sysRole.getId())
|
|
||||||
.and("clubId", "=", clubUser.getClubId()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sysUserService.clearCache();
|
|
||||||
sysRoleService.clearCache();
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +301,8 @@ public class ClubInfoManageController {
|
|||||||
clubUser.setClubId(clubId);
|
clubUser.setClubId(clubId);
|
||||||
clubUser.setUserId(user);
|
clubUser.setUserId(user);
|
||||||
clubUser.setRoleCode(List.of(roleCode));
|
clubUser.setRoleCode(List.of(roleCode));
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:仅新会员调用 service,统一填写确认入会时间和姓名/工号/电话。
|
||||||
|
clubUserService.prepareDirectJoinTimes(List.of(clubUser));
|
||||||
}
|
}
|
||||||
dao.insertOrUpdate(clubUser);
|
dao.insertOrUpdate(clubUser);
|
||||||
|
|
||||||
@@ -335,6 +328,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", payed != null ? payed : false);
|
log.set("newValue", payed != null ? payed : false);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:新增记录先填写人员快照。
|
||||||
|
payRecordService.prepareSnapshots(List.of(record));
|
||||||
dao.insert(record);
|
dao.insert(record);
|
||||||
} else {
|
} else {
|
||||||
payRecord.setPayed(payed != null ? payed : false);
|
payRecord.setPayed(payed != null ? payed : false);
|
||||||
@@ -348,6 +343,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", payed != null ? payed : false);
|
log.set("newValue", payed != null ? payed : false);
|
||||||
|
|
||||||
payRecord.setOperateLogs(List.of(log));
|
payRecord.setOperateLogs(List.of(log));
|
||||||
|
// 已有年度记录仅补缺失快照,原有年度资料和加入时间不覆盖。
|
||||||
|
payRecordService.prepareSnapshots(List.of(payRecord));
|
||||||
dao.update(payRecord);
|
dao.update(payRecord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +353,13 @@ public class ClubInfoManageController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clubId 左侧选中的社团ID,用于导出该社团登记表
|
||||||
|
* @param response 成功时写入 DOCX 附件;失败由统一异常处理返回含 code/msg 的错误结果
|
||||||
|
* @return void,附件直接写入响应,不再渲染 JSON 成功结果
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
|
@Ok("void")
|
||||||
@SaCheckPermission("club.infoManage.manage")
|
@SaCheckPermission("club.infoManage.manage")
|
||||||
@ApiOperation("导出登记表")
|
@ApiOperation("导出登记表")
|
||||||
public void exportRegistrationDoc(@Valid String clubId, HttpServletResponse response){
|
public void exportRegistrationDoc(@Valid String clubId, HttpServletResponse response){
|
||||||
@@ -412,6 +415,8 @@ public class ClubInfoManageController {
|
|||||||
cUser.setRoleCode(List.of(RoleConstant.CLUB_MEMBER.name()));
|
cUser.setRoleCode(List.of(RoleConstant.CLUB_MEMBER.name()));
|
||||||
clubUserList.add(cUser);
|
clubUserList.add(cUser);
|
||||||
}
|
}
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:导入新增会员统一从用户资料批量填写人员字段及后端确认时间。
|
||||||
|
clubUserService.prepareDirectJoinTimes(clubUserList);
|
||||||
dao.insert(clubUserList);
|
dao.insert(clubUserList);
|
||||||
|
|
||||||
List<Sys_user_role> sysUserRoleList = new ArrayList<>();
|
List<Sys_user_role> sysUserRoleList = new ArrayList<>();
|
||||||
@@ -450,6 +455,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", false);
|
log.set("newValue", false);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:新增记录先填写人员快照。
|
||||||
|
payRecordService.prepareSnapshots(List.of(record));
|
||||||
dao.insert(record);
|
dao.insert(record);
|
||||||
} else {
|
} else {
|
||||||
payRecord.setPayed(false);
|
payRecord.setPayed(false);
|
||||||
@@ -463,6 +470,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", false);
|
log.set("newValue", false);
|
||||||
|
|
||||||
payRecord.setOperateLogs(List.of(log));
|
payRecord.setOperateLogs(List.of(log));
|
||||||
|
// 已有年度记录仅补缺失快照,原有年度资料和加入时间不覆盖。
|
||||||
|
payRecordService.prepareSnapshots(List.of(payRecord));
|
||||||
dao.update(payRecord);
|
dao.update(payRecord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-140
@@ -1,42 +1,22 @@
|
|||||||
package com.budwk.app.zhgh.club.controller.infoManage;
|
package com.budwk.app.zhgh.club.controller.infoManage;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.annotation.SaMode;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.json.JSONObject;
|
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
|
||||||
import com.budwk.app.base.page.Pagination;
|
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
|
||||||
import com.budwk.app.zhgh.club.model.SysClub;
|
|
||||||
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubService;
|
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Sqls;
|
|
||||||
import org.nutz.dao.sql.Sql;
|
|
||||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
|
||||||
import org.nutz.lang.util.NutMap;
|
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubPayRecordController
|
* @ClassName ClubPayRecordController
|
||||||
@@ -55,8 +35,6 @@ public class ClubPayRecordController {
|
|||||||
@Inject
|
@Inject
|
||||||
private ClubPayRecordService payRecordService;
|
private ClubPayRecordService payRecordService;
|
||||||
|
|
||||||
@Inject
|
|
||||||
private SysClubService sysClubService;
|
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@SaCheckPermission("club.infoManage.pay")
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
@@ -66,135 +44,40 @@ public class ClubPayRecordController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("club.infoManage.pay")
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 14:33:18;操作人:wxg:接收查询条件,业务筛选交给 service。
|
||||||
|
* pageForm 为分页和姓名/工号关键字;year 为年度、clubId 为社团ID;
|
||||||
|
* payed/assign 为缴费/拨付状态(1是、0否、空值不限);memberState 为当前会员状态(1在会、0退会、空值全部)。
|
||||||
|
* 返回 Result,data 为分页对象,包含 list、totalCount 等;list.count 为当前会员关系是否存在(1在会、0退会)。
|
||||||
|
*/
|
||||||
public Result pageData(@Valid PageForm pageForm,
|
public Result pageData(@Valid PageForm pageForm,
|
||||||
@Param("year") Integer year,
|
@Param("year") Integer year,
|
||||||
@Param("clubId") String clubId,
|
@Param("clubId") String clubId,
|
||||||
@Param("payed") Integer payed,
|
@Param("payed") Integer payed,
|
||||||
@Param("assign") Integer assign) {
|
@Param("assign") Integer assign,
|
||||||
Sql sql = Sqls.create("""
|
@Param("memberState") Integer memberState) {
|
||||||
SELECT
|
if (memberState != null && memberState != 0 && memberState != 1) {
|
||||||
cpr.*,
|
return Result.error("会员状态只能为在会、退会或全部");
|
||||||
c.clubName,
|
}
|
||||||
u.loginName,
|
return Result.success(payRecordService.pageData(pageForm, year, clubId, payed, assign, memberState));
|
||||||
u.userName,
|
}
|
||||||
u.unitName,
|
|
||||||
u.retireDate,
|
|
||||||
(SELECT createdAt FROM club_user WHERE clubId = cpr.clubId AND userId = cpr.userId ORDER BY createdAt LIMIT 1) AS applyTime,
|
|
||||||
u.userState,
|
|
||||||
(select count(1) from club_user where clubId = cpr.clubId and userId = cpr.userId) as count
|
|
||||||
FROM
|
|
||||||
`club_pay_record` cpr
|
|
||||||
LEFT JOIN sys_club c ON cpr.clubid = c.id
|
|
||||||
LEFT JOIN vw_user u ON u.id = cpr.userId
|
|
||||||
$condition
|
|
||||||
""");
|
|
||||||
Cnd cnd = Cnd.NEW();
|
|
||||||
List<String> clubIdList = sysClubService.getMyManageClub().stream()
|
|
||||||
.map(SysClub::getId)
|
|
||||||
.toList();
|
|
||||||
cnd.andEX("cpr.year", "=", year);
|
|
||||||
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
|
|
||||||
cnd.andEX("cpr.clubId", "in", clubIdList);
|
|
||||||
}else{
|
|
||||||
cnd.andEX("cpr.clubId", "=", clubId);
|
|
||||||
}
|
|
||||||
cnd.andEX("cpr.payed", "=", payed);
|
|
||||||
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
|
||||||
SqlExpressionGroup seg = new SqlExpressionGroup();
|
|
||||||
seg.orLike("u.userName", pageForm.getSearchKeyword());
|
|
||||||
seg.orLike("u.loginName", pageForm.getSearchKeyword());
|
|
||||||
cnd.and(seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assign != null) {
|
/** 返回当前用户授权社团的下拉选项,字段包含 id、clubName、clubCode。 */
|
||||||
SqlExpressionGroup mainGroup = new SqlExpressionGroup();
|
@At
|
||||||
if (assign == 0) {
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
SqlExpressionGroup noAssignGroup = new SqlExpressionGroup();
|
public Result listClubs() {
|
||||||
|
return Result.success(payRecordService.getPayManageClubList());
|
||||||
// 不拨付条件1:payed=false(原代码第一行直接return false)
|
|
||||||
noAssignGroup.or("cpr.payed", "=", false);
|
|
||||||
|
|
||||||
// 不拨付条件2:payed=true,但用户状态是退休 且 无退休日期
|
|
||||||
SqlExpressionGroup retireNoDateGroup = new SqlExpressionGroup();
|
|
||||||
retireNoDateGroup.and("cpr.payed", "=", true);
|
|
||||||
retireNoDateGroup.and("u.userState", "=", "退休");
|
|
||||||
retireNoDateGroup.and("u.retireDate", "is", null);
|
|
||||||
|
|
||||||
// 不拨付条件3:payed=true,用户状态是退休,有退休日期,但退休年份 < 缴费年份
|
|
||||||
SqlExpressionGroup retireYearLessGroup = new SqlExpressionGroup();
|
|
||||||
retireYearLessGroup.and("cpr.payed", "=", true);
|
|
||||||
retireYearLessGroup.and("u.userState", "=", "退休");
|
|
||||||
retireYearLessGroup.and("u.retireDate", "is not", null);
|
|
||||||
// 计算缴费年份
|
|
||||||
// 退休年份 < 缴费年份
|
|
||||||
retireYearLessGroup.and("YEAR(u.retireDate)", "<", "cpr.year");
|
|
||||||
|
|
||||||
// 合并所有不拨付条件(满足任一即不拨付)
|
|
||||||
noAssignGroup.or(retireNoDateGroup);
|
|
||||||
noAssignGroup.or(retireYearLessGroup);
|
|
||||||
|
|
||||||
mainGroup.and(noAssignGroup);
|
|
||||||
} else {
|
|
||||||
SqlExpressionGroup assignGroup = new SqlExpressionGroup();
|
|
||||||
|
|
||||||
// 拨付前提:payed必须为true
|
|
||||||
assignGroup.and("cpr.payed", "=", true);
|
|
||||||
|
|
||||||
// 拨付条件1:用户状态≠退休
|
|
||||||
SqlExpressionGroup userNotRetireGroup = new SqlExpressionGroup();
|
|
||||||
userNotRetireGroup.or("u.userState", "<>", "退休");
|
|
||||||
|
|
||||||
// 拨付条件2:用户状态=退休,有退休日期,且退休年份≥缴费年份
|
|
||||||
SqlExpressionGroup retireQualifiedGroup = new SqlExpressionGroup();
|
|
||||||
retireQualifiedGroup.and("u.userState", "=", "退休");
|
|
||||||
retireQualifiedGroup.and("u.retireDate", "is not", null);
|
|
||||||
retireQualifiedGroup.and("YEAR(u.retireDate)", ">=", "cpr.year");
|
|
||||||
|
|
||||||
// 合并拨付条件(满足任一即拨付)
|
|
||||||
userNotRetireGroup.or(retireQualifiedGroup);
|
|
||||||
assignGroup.and(userNotRetireGroup);
|
|
||||||
|
|
||||||
mainGroup.and(assignGroup);
|
|
||||||
}
|
|
||||||
cnd.and(mainGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql.setCondition(cnd);
|
|
||||||
Pagination pagination = payRecordService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
|
||||||
|
|
||||||
List<NutMap> list = pagination.getList();
|
|
||||||
for (NutMap nutMap : list) {
|
|
||||||
boolean assigned = payRecordService.calAssigned(nutMap.getString("id"));
|
|
||||||
nutMap.put("assign", assigned);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success(pagination);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("修改社团成员缴费状态")
|
@ApiOperation("修改社团成员缴费状态")
|
||||||
@SaCheckPermission("club.infoManage.pay")
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
@SLog(tag = "社团管理系统-缴费记录", msg = "修改社团成员缴费状态")
|
@SLog(tag = "社团管理系统-缴费记录", msg = "修改社团成员缴费状态")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result handle(@Param("ids") String[] ids,
|
public Result handle(@Param("ids") String[] ids,
|
||||||
@Param("payed") Boolean payed) {
|
@Param("payed") Boolean payed) {
|
||||||
List<ClubPayRecord> list = payRecordService.query(Cnd.where("id", "in", ids));
|
// 2026年10月3日 09:51:00;操作人:wxg:controller 只接收参数,缴费、拨付结果和日志由 service 在同一事务内保存。
|
||||||
|
payRecordService.updatePayStatus(ids, payed);
|
||||||
for (ClubPayRecord payRecord : list) {
|
|
||||||
List<JSONObject> logs = Optional.ofNullable(payRecord.getOperateLogs()).orElseGet(ArrayList::new);
|
|
||||||
JSONObject log = new JSONObject();
|
|
||||||
log.set("operatorUserId", SecurityUtil.getUserId());
|
|
||||||
log.set("operatorUserName", SecurityUtil.getUserUsername());
|
|
||||||
log.set("operatorTime", DateUtil.now());
|
|
||||||
log.set("operatorType", "缴费管理-手动设置");
|
|
||||||
log.set("oldValue", payRecord.getPayed());
|
|
||||||
log.set("newValue", payed);
|
|
||||||
logs.add(log);
|
|
||||||
|
|
||||||
payRecord.setOperateLogs(logs);
|
|
||||||
payRecord.setPayed(payed);
|
|
||||||
}
|
|
||||||
|
|
||||||
payRecordService.update(list);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
package com.budwk.app.zhgh.club.controller.infoManage;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
|
import com.budwk.app.sys.models.Sys_dict;
|
||||||
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubRegisterImportService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.mvc.annotation.AdaptBy;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.upload.TempFile;
|
||||||
|
import org.nutz.mvc.upload.UploadAdaptor;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/club/infoManage/registerImport")
|
||||||
|
@Ok("json:full")
|
||||||
|
public class ClubRegisterImportController {
|
||||||
|
private static final String[] HEADERS = {"社团名称", "社团编码(选填)", "社团类型编码", "成立日期", "会长工号", "秘书长工号",
|
||||||
|
"发起人工号", "副会长工号(选填)", "副秘书长工号(选填)", "联系人工号(选填)", "会费标准(选填)",
|
||||||
|
"联系电话(选填)", "联系邮箱(选填)", "社团介绍及宗旨(选填)", "活动形式(选填)", "缩略图地址(选填)",
|
||||||
|
"申请成立报告文件ID(选填)", "章程草案文件ID(选填)", "经费管理办法文件ID(选填)", "年度计划文件ID(选填)", "其他附件文件ID(选填)"};
|
||||||
|
@Inject
|
||||||
|
private ClubRegisterImportService clubRegisterImportService;
|
||||||
|
@Inject
|
||||||
|
private SysDictService sysDictService;
|
||||||
|
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SaCheckRole("SYSADMIN")
|
||||||
|
@SaCheckPermission("club.infoManage.manage")
|
||||||
|
public void downloadTemplate(HttpServletResponse response) {
|
||||||
|
Workbook workbook = new XSSFWorkbook();
|
||||||
|
Sheet data = sheet(workbook, "社团导入", HEADERS);
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
style.setDataFormat(workbook.createDataFormat().getFormat("@"));
|
||||||
|
for (int i = 0; i < HEADERS.length; i++) data.setDefaultColumnStyle(i, style);
|
||||||
|
Sheet notes = sheet(workbook, "填写说明", "参数", "填写要求");
|
||||||
|
notes.setColumnWidth(1, 100 * 256);
|
||||||
|
append(notes, "导入规则", "仅读取第一个工作表,每行一个社团,最多1000行有效数据;保留表头,工号与编码按文本填写。");
|
||||||
|
append(notes, "必填", "社团名称、社团类型编码、成立日期、会长工号、秘书长工号、至少3名不同发起人工号。");
|
||||||
|
append(notes, "社团编码", "选填,留空按单个注册的年度编号规则生成;已有名称或编码直接跳过,不覆盖草稿、审批中或已解散记录。");
|
||||||
|
append(notes, "成立日期", "按yyyy-MM-dd填写,如2026-10-23;支持Excel日期单元格。");
|
||||||
|
append(notes, "人员", "使用系统内已有工号。会长和秘书长各1人;副职可选多人,用逗号分隔,理事机构人员不得重复。");
|
||||||
|
append(notes, "发起人", "至少3名不同人员,用逗号分隔;可与理事机构人员重合。");
|
||||||
|
append(notes, "联系人", "选填工号,电话及邮箱默认取人员资料;单独填写电话、邮箱时使用填写值。");
|
||||||
|
append(notes, "社团类型", "参照社团类型工作表填写编码,不填写名称。");
|
||||||
|
append(notes, "其他资料", "会费标准、介绍、活动形式、缩略图地址均选填;介绍及活动形式按纯文本导入。");
|
||||||
|
append(notes, "附件", "选填系统已上传文件的ID,多文件用逗号分隔,每类最多5个;Excel不上传附件本身,留空可后续在维护页面补充。");
|
||||||
|
append(notes, "直接通过", "保存社团、发起人、理事会员、入会时间和年度缴费记录,授予社团角色;直接通过,不生成审批任务。");
|
||||||
|
append(notes, "导入结果", "逐行独立保存,已存在的跳过,失败行回滚并显示原因,不影响其他行。");
|
||||||
|
Sheet types = sheet(workbook, "社团类型", "类型编码", "类型名称");
|
||||||
|
for (Sys_dict type : sysDictService.getSubListByCode("CLUB_REGISTER_TYPE")) append(types, type.getCode(), type.getName());
|
||||||
|
CommonDownloadUtil.download("社团批量注册模板.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckRole("SYSADMIN")
|
||||||
|
@SaCheckPermission("club.infoManage.manage")
|
||||||
|
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
|
||||||
|
public Result doImport(@Param("file") TempFile file) {
|
||||||
|
if (file == null) return Result.error("请选择Excel文件");
|
||||||
|
List<String[]> rows = new ArrayList<>();
|
||||||
|
List<Integer> numbers = new ArrayList<>();
|
||||||
|
try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input)) {
|
||||||
|
if (workbook.getNumberOfSheets() == 0) return Result.error("文件中没有工作表");
|
||||||
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
DataFormatter formatter = new DataFormatter();
|
||||||
|
for (int i = 0; i < HEADERS.length; i++) {
|
||||||
|
if (!HEADERS[i].equals(text(sheet.getRow(0), i, formatter))) return Result.error("表头不匹配,请使用下载的模板并保留完整表头");
|
||||||
|
}
|
||||||
|
for (Row row : sheet) {
|
||||||
|
if (row.getRowNum() == 0) continue;
|
||||||
|
String[] values = new String[HEADERS.length];
|
||||||
|
for (int i = 0; i < values.length; i++) values[i] = text(row, i, formatter);
|
||||||
|
if (Arrays.stream(values).allMatch(StrUtil::isBlank)) continue;
|
||||||
|
if (rows.size() >= 1000) return Result.error("每次最多导入1000行有效数据");
|
||||||
|
rows.add(values);
|
||||||
|
numbers.add(row.getRowNum() + 1);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("读取社团注册导入文件失败", e);
|
||||||
|
return Result.error("文件读取失败,请使用未加密的xls或xlsx模板,不要使用公式");
|
||||||
|
}
|
||||||
|
if (rows.isEmpty()) return Result.error("文件中没有需要导入的数据");
|
||||||
|
List<NutMap> results = new ArrayList<>();
|
||||||
|
int success = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
String[] values = rows.get(i);
|
||||||
|
NutMap result;
|
||||||
|
try {
|
||||||
|
result = clubRegisterImportService.importRow(values);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
result = NutMap.NEW().addv("status", "失败").addv("message", e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("社团批量注册失败,行号:{}", numbers.get(i), e);
|
||||||
|
result = NutMap.NEW().addv("status", "失败").addv("message", "保存失败,该行已回滚,请检查数据或联系管理员");
|
||||||
|
}
|
||||||
|
result.setv("rowNumber", numbers.get(i)).setv("clubName", values[0]);
|
||||||
|
if (!result.containsKey("clubCode")) result.setv("clubCode", values[1]);
|
||||||
|
if ("成功".equals(result.getString("status"))) success++;
|
||||||
|
if ("跳过".equals(result.getString("status"))) skipped++;
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
return Result.success(NutMap.NEW().addv("success", success).addv("skipped", skipped)
|
||||||
|
.addv("failed", results.size() - success - skipped).addv("rows", results));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(Row row, int index, DataFormatter formatter) {
|
||||||
|
Cell cell = row == null ? null : row.getCell(index);
|
||||||
|
if (cell != null && cell.getCellType() == CellType.FORMULA) throw new IllegalArgumentException("不支持公式");
|
||||||
|
if (index == 3 && cell != null && cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
|
||||||
|
return cell.getLocalDateTimeCellValue().toLocalDate().toString();
|
||||||
|
}
|
||||||
|
return formatter.formatCellValue(cell).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Sheet sheet(Workbook workbook, String name, String... headers) {
|
||||||
|
Sheet sheet = workbook.createSheet(name);
|
||||||
|
Row row = sheet.createRow(0);
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
row.createCell(i).setCellValue(headers[i]);
|
||||||
|
sheet.setColumnWidth(i, 28 * 256);
|
||||||
|
}
|
||||||
|
sheet.createFreezePane(0, 1);
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void append(Sheet sheet, String... values) {
|
||||||
|
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
|
||||||
|
for (int i = 0; i < values.length; i++) row.createCell(i).setCellValue(values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package com.budwk.app.zhgh.club.controller.statistics;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import com.budwk.app.base.result.Result;
|
||||||
|
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||||
|
import com.budwk.app.zhgh.club.service.SysClubService;
|
||||||
|
import com.budwk.app.zhgh.club.service.impl.SysClubUserServiceImpl;
|
||||||
|
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.json.Json;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
@Ok("json")
|
||||||
|
@At("/platform/club/statistics/member")
|
||||||
|
public class ClubMemberStatisticsController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SysClubService sysClubService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@Ok("beetl:/platform/zhgh/club/statistics/member.html")
|
||||||
|
@SaCheckPermission("club.statistics.member")
|
||||||
|
public void index() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("club.statistics.member")
|
||||||
|
public Result pageData(@Valid PageForm pageForm, String clubId, String userId, String keyWord, String unitName, String userState, String sex) {
|
||||||
|
Sql sql = generateSql(false, clubId, userId, keyWord, unitName, userState, sex);
|
||||||
|
Pagination pagination = sysClubService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
|
List<NutMap> members = pagination.getList(NutMap.class);
|
||||||
|
for (NutMap member : members) {
|
||||||
|
List<String> roles = StrUtil.isBlank(member.getString("roleCode"))
|
||||||
|
? List.of() : Json.fromJsonAsList(String.class, member.getString("roleCode"));
|
||||||
|
member.put("roleName", SysClubUserServiceImpl.convertRoleName(roles));
|
||||||
|
member.put("roleCode", roles);
|
||||||
|
}
|
||||||
|
pagination.setList(members);
|
||||||
|
return Result.success(pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@SaCheckPermission("club.statistics.member")
|
||||||
|
public Result clubOptions() {
|
||||||
|
return Result.success(sysClubService.listMap(generateSql(true, null, null, null, null, null, null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Sql generateSql(boolean options, String clubId, String userId, String keyWord, String unitName, String userState, String sex) {
|
||||||
|
String fields = options ? "DISTINCT club.id, club.clubCode, club.clubName" : """
|
||||||
|
c.id, c.userId, club.clubCode, club.clubName,
|
||||||
|
COALESCE(NULLIF(u.username, ''), c.userName) AS userName,
|
||||||
|
COALESCE(NULLIF(u.loginname, ''), c.loginName) AS loginName,
|
||||||
|
u.sex, COALESCE(NULLIF(u.mobile, ''), c.mobile) AS mobile,
|
||||||
|
u.unitname AS unitName, u.userState, c.roleCode, c.joinTime
|
||||||
|
""";
|
||||||
|
Sql sql = Sqls.create("SELECT " + fields + "\n" + """
|
||||||
|
FROM sys_club club
|
||||||
|
INNER JOIN club_user c ON c.clubId = club.id
|
||||||
|
LEFT JOIN vw_user u ON u.id = c.userId
|
||||||
|
$condition
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance registerInstance
|
||||||
|
INNER JOIN wf_process_define registerDefine
|
||||||
|
ON registerDefine.id = registerInstance.processDefineId
|
||||||
|
WHERE registerInstance.businessNo = club.id
|
||||||
|
AND registerInstance.state = @finishedState
|
||||||
|
AND registerInstance.delFlag = 0
|
||||||
|
AND registerDefine.name = @registerProcessKey
|
||||||
|
AND registerDefine.delFlag = 0
|
||||||
|
)
|
||||||
|
ORDER BY club.clubCode, club.id
|
||||||
|
""" + (options ? "" : ", c.id"));
|
||||||
|
Cnd cnd = Cnd.where("club.dismiss", "=", false);
|
||||||
|
cnd.and("club.delFlag", "=", false);
|
||||||
|
cnd.and("c.delFlag", "=", false);
|
||||||
|
cnd.andEX("club.id", "=", clubId);
|
||||||
|
cnd.andEX("c.userId", "=", userId);
|
||||||
|
cnd.andEX("u.userState", "=", userState);
|
||||||
|
cnd.andEX("u.sex", "=", sex);
|
||||||
|
if (StrUtil.isNotBlank(unitName)) {
|
||||||
|
cnd.and("u.unitname", "=", unitName);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(keyWord)) {
|
||||||
|
cnd.and(Cnd.exps("COALESCE(NULLIF(u.username, ''), c.userName)", "like", "%" + keyWord + "%")
|
||||||
|
.or("COALESCE(NULLIF(u.loginname, ''), c.loginName)", "like", "%" + keyWord + "%"));
|
||||||
|
}
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
|
||||||
|
sql.setParam("registerProcessKey", "XHZC");
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-1
@@ -129,7 +129,21 @@ public class ClubStatisticsController {
|
|||||||
u.mobile,
|
u.mobile,
|
||||||
u.unitname as unitName,
|
u.unitname as unitName,
|
||||||
u.userState,
|
u.userState,
|
||||||
c.*
|
c.id,
|
||||||
|
c.clubId,
|
||||||
|
c.userId,
|
||||||
|
c.roleCode,
|
||||||
|
c.clubPosition,
|
||||||
|
c.email,
|
||||||
|
c.avatar,
|
||||||
|
c.sameTimeJoinOtherClubSituation,
|
||||||
|
c.awardsExperience,
|
||||||
|
c.joinTime,
|
||||||
|
c.createdBy,
|
||||||
|
c.createdAt,
|
||||||
|
c.updatedBy,
|
||||||
|
c.updatedAt,
|
||||||
|
c.delFlag
|
||||||
FROM
|
FROM
|
||||||
club_user c
|
club_user c
|
||||||
LEFT JOIN `vw_user` u ON u.id = c.userId
|
LEFT JOIN `vw_user` u ON u.id = c.userId
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -43,6 +44,8 @@ public class ClubUserExitInterceptor implements FlowInterceptor {
|
|||||||
|
|
||||||
// 删人
|
// 删人
|
||||||
dao.clear(ClubUser.class, Cnd.where(ClubUser::getUserId, "=", clubUserApply.getUserId()).and(ClubUser::getClubId, "=", clubUserApply.getClubId()));
|
dao.clear(ClubUser.class, Cnd.where(ClubUser::getUserId, "=", clubUserApply.getUserId()).and(ClubUser::getClubId, "=", clubUserApply.getClubId()));
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:WF 退会完成后,同步将该社团会员的拨付字段置为 false。
|
||||||
|
ServiceContext.find(ClubPayRecordService.class).disableExitMemberPayment(clubUserApply.getClubId(), clubUserApply.getUserId());
|
||||||
// 删角色
|
// 删角色
|
||||||
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", clubUserApply.getUserId()).and(Sys_user_role::getClubId, "=", clubUserApply.getClubId()));
|
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", clubUserApply.getUserId()).and(Sys_user_role::getClubId, "=", clubUserApply.getClubId()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import org.bouncycastle.jcajce.provider.util.SecretKeyUtil;
|
import org.bouncycastle.jcajce.provider.util.SecretKeyUtil;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
@@ -41,9 +43,20 @@ public class ClubUserJoinInterceptor implements FlowInterceptor {
|
|||||||
|
|
||||||
// 获取操作类
|
// 获取操作类
|
||||||
Dao dao = ServiceContext.find(Dao.class);
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
// 入会时间以本次会长同意审核任务的完成时间为准,交由 service 校验并写入申请表。
|
||||||
|
clubUserApply.setJoinTime(ServiceContext.find(ClubUserJoinService.class).completeJoinApproval(
|
||||||
|
execution.getProcessInstance().getBusinessNo(), execution.getProcessTaskId()));
|
||||||
|
|
||||||
// 将申请信息复制一份,插入社团成员表
|
// 将申请信息复制一份,插入社团成员表
|
||||||
ClubUser clubUser = BeanUtil.copyProperties(clubUserApply, ClubUser.class);
|
ClubUser clubUser = BeanUtil.copyProperties(clubUserApply, ClubUser.class);
|
||||||
|
// 读取本次已写入的入会申请时间,不能取成立时间或用插入时刻替代审核完成时间。
|
||||||
|
ClubUserApply approvedApply = dao.fetch(ClubUserApply.class, execution.getProcessInstance().getBusinessNo());
|
||||||
|
if (approvedApply == null || approvedApply.getJoinTime() == null) {
|
||||||
|
throw new IllegalArgumentException("本次入会申请尚未生成审核完成时间");
|
||||||
|
}
|
||||||
|
clubUser.setJoinTime(approvedApply.getJoinTime());
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:审核入会保存当前用户资料,joinTime 仍使用本次会长审核完成时间。
|
||||||
|
ServiceContext.find(SysClubUserService.class).prepareMemberSnapshots(List.of(clubUser));
|
||||||
dao.insert(clubUser);
|
dao.insert(clubUser);
|
||||||
|
|
||||||
// 还要加到社团成员缴费表中
|
// 还要加到社团成员缴费表中
|
||||||
@@ -70,6 +83,8 @@ public class ClubUserJoinInterceptor implements FlowInterceptor {
|
|||||||
log.set("newValue", payed);
|
log.set("newValue", payed);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:审核新增缴费记录,保存本次会员加入时间及人员快照。
|
||||||
|
ServiceContext.find(ClubPayRecordService.class).prepareSnapshots(List.of(record));
|
||||||
dao.insert(record);
|
dao.insert(record);
|
||||||
} else {
|
} else {
|
||||||
payRecord.setPayed(payed);
|
payRecord.setPayed(payed);
|
||||||
@@ -83,6 +98,8 @@ public class ClubUserJoinInterceptor implements FlowInterceptor {
|
|||||||
log.set("newValue", payed);
|
log.set("newValue", payed);
|
||||||
|
|
||||||
payRecord.setOperateLogs(List.of(log));
|
payRecord.setOperateLogs(List.of(log));
|
||||||
|
// 同年度重新入会复用原缴费记录,只补缺失资料,不重写已保存的年度快照。
|
||||||
|
ServiceContext.find(ClubPayRecordService.class).prepareSnapshots(List.of(payRecord));
|
||||||
dao.update(payRecord);
|
dao.update(payRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.nutz.dao.entity.annotation.*;
|
|||||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubPayRecord
|
* @ClassName ClubPayRecord
|
||||||
@@ -56,6 +57,38 @@ public class ClubPayRecord extends BaseModel {
|
|||||||
@Default(value = "0")
|
@Default(value = "0")
|
||||||
private Boolean isGiveMoney;
|
private Boolean isGiveMoney;
|
||||||
|
|
||||||
|
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:保存年度名单人员快照,列表不再联查用户视图。
|
||||||
|
@Column
|
||||||
|
@Comment("工号")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 120)
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("姓名")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("所在单位")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||||
|
private String unitName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("人员类型")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String personType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("加入时间")
|
||||||
|
@ColDefine(type = ColType.DATETIME)
|
||||||
|
private Date joinTime;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("退休时间")
|
||||||
|
@ColDefine(type = ColType.DATETIME)
|
||||||
|
private Date retireDate;
|
||||||
|
|
||||||
// 防止有人今天操作缴费,明天操作未缴费,还是记录一下吧
|
// 防止有人今天操作缴费,明天操作未缴费,还是记录一下吧
|
||||||
@Column
|
@Column
|
||||||
@Comment("操作记录")
|
@Comment("操作记录")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.nutz.dao.entity.annotation.*;
|
|||||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -62,5 +63,25 @@ public class ClubUser extends BaseModel {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||||
private String awardsExperience;
|
private String awardsExperience;
|
||||||
|
|
||||||
|
/** 审核入会取本次申请的 joinTime;直接录入或导入取后端确认时间。 */
|
||||||
|
@Column
|
||||||
|
@Comment("入会时间")
|
||||||
|
@ColDefine(type = ColType.DATETIME)
|
||||||
|
private Date joinTime;
|
||||||
|
|
||||||
|
/** 2026年10月3日 12:01:28;操作人:wxg:会员资料在加入时保存,供会员列表直接展示和姓名/工号查询。 */
|
||||||
|
@Column
|
||||||
|
@Comment("姓名")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("工号")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 120)
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("电话")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
private String mobile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ public class ClubUserApply extends BaseModel {
|
|||||||
private String roleCode;
|
private String roleCode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("加入/退出")
|
@Comment("申请模式:0退出、1加入、2系统处理退会")
|
||||||
@ColDefine(type = ColType.BOOLEAN)
|
@ColDefine(type = ColType.INT)
|
||||||
private Boolean mode;
|
private Integer mode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("社团职务")
|
@Comment("社团职务")
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ public class SysClub extends BaseModel {
|
|||||||
@NotEmpty(message = "社团名称不能为空")
|
@NotEmpty(message = "社团名称不能为空")
|
||||||
private String clubName;
|
private String clubName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("社团缩略图")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 1000)
|
||||||
|
private String thumbnail;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("社团编码")
|
@Comment("社团编码")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
|||||||
@@ -18,7 +18,17 @@ public class ClubUserPageForm extends PageForm {
|
|||||||
private String clubId;
|
private String clubId;
|
||||||
private String clubName;
|
private String clubName;
|
||||||
private String personType;
|
private String personType;
|
||||||
|
/** 重点关注名单:true 同时筛选自聘人员、其他;false 或未传时不增加该限制。 */
|
||||||
|
private Boolean focusMembers;
|
||||||
private String userState;
|
private String userState;
|
||||||
|
/** 会员入会审核开始日期,前端传 yyyy-MM-dd;为空时不限制开始时间。 */
|
||||||
|
private String joinDateStart;
|
||||||
|
/** 会员入会审核结束日期,前端传 yyyy-MM-dd,包含当天;为空时不限制结束时间。 */
|
||||||
|
private String joinDateEnd;
|
||||||
|
/** 2026年10月3日 19:42:45;操作人:wxg:退会日期范围起点,传 yyyy-MM-dd,空值不限制。 */
|
||||||
|
private String exitDateStart;
|
||||||
|
/** 2026年10月3日 19:42:45;操作人:wxg:退会日期范围终点,传 yyyy-MM-dd,包含当天,空值不限制。 */
|
||||||
|
private String exitDateEnd;
|
||||||
private String unitId;
|
private String unitId;
|
||||||
private Integer applyType;
|
private Integer applyType;
|
||||||
private Integer auditType;
|
private Integer auditType;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package com.budwk.app.zhgh.club.service;
|
|||||||
|
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
||||||
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubPayRecordService
|
* @ClassName ClubPayRecordService
|
||||||
@@ -12,6 +16,55 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|||||||
*/
|
*/
|
||||||
public interface ClubPayRecordService extends BaseService<ClubPayRecord> {
|
public interface ClubPayRecordService extends BaseService<ClubPayRecord> {
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:05:37;操作人:wxg:定向检查并新增缴费表缺失列;不删除字段,不生成独立升级 SQL 文件。 */
|
||||||
|
void upgradePayRecordSchema();
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:05:37;操作人:wxg:启动时补齐旧记录缺失快照,不覆盖已保存资料;返回 void。 */
|
||||||
|
void backfillPayRecordSnapshots();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 09:05:37;操作人:wxg:批量填写新增缴费记录的人员快照。
|
||||||
|
* @param records 已设置 year、clubId、userId 的缴费记录;现有人员快照不覆盖,空加入时间取有效会员 joinTime;根据当前会员关系及缴费状态计算拨付结果
|
||||||
|
* @return void;填写工号、姓名、单位、人员类型、退休日期、加入时间和 isGiveMoney,由调用方在事务中保存
|
||||||
|
*/
|
||||||
|
void prepareSnapshots(List<ClubPayRecord> records);
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:51:00;操作人:wxg:设置缴费状态并保存拨付结果及日志。
|
||||||
|
* @param ids 非空缴费记录ID数组;只允许操作当前用户授权社团的记录
|
||||||
|
* @param payed true已缴费、false未缴费,不能为空
|
||||||
|
* @return void;参数错误或记录不在授权范围时抛出异常,事务回滚
|
||||||
|
*/
|
||||||
|
void updatePayStatus(String[] ids, Boolean payed);
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:51:00;操作人:wxg:退会后将该用户在指定社团的各年度缴费记录置为不拨付。
|
||||||
|
* @param clubId 退会社团ID,不能为空
|
||||||
|
* @param userId 退会用户ID,不能为空
|
||||||
|
* @return void;保留缴费状态和人员快照,仅更新 isGiveMoney
|
||||||
|
*/
|
||||||
|
void disableExitMemberPayment(String clubId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 09:05:37;操作人:wxg:按当前会员生成指定年度名单,已存在的会员记录不重复生成、不重置缴费。
|
||||||
|
* @param year 要生成名单的公历年度,年度计划任务传当前年份
|
||||||
|
* @return 本次实际新增的缴费记录条数
|
||||||
|
*/
|
||||||
|
int generateYearPayRecords(Integer year);
|
||||||
|
|
||||||
|
/** 返回当前用户可查询的社团选项,每项包含 id、clubName、clubCode;无授权社团时返回空列表。 */
|
||||||
|
List<NutMap> getPayManageClubList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按权限范围分页查询缴费记录,并按社团编号及缴费记录保存的加入时间排序。
|
||||||
|
* @param pageForm 页码、每页条数及姓名/工号关键字
|
||||||
|
* @param year 缴费年度,空值表示不限制
|
||||||
|
* @param clubId 社团ID,空值表示查询全部授权社团
|
||||||
|
* @param payed 缴费状态:1已缴费、0未缴费、空值不限
|
||||||
|
* @param assign 保存的拨付状态:1拨付、0不拨付、空值不限
|
||||||
|
* 2026年10月3日 14:33:18;操作人:wxg:在分页前按实时会员关系筛选,历史年度也使用当前会员状态。
|
||||||
|
* @param memberState 1在会、0退会、null不限;在会以同一 clubId/userId 存在 club_user 记录为准
|
||||||
|
* @return 分页记录,list 包含缴费字段、社团及人员信息、applyTime(保存的入会时间)与 assign(isGiveMoney 保存值转换为布尔值)
|
||||||
|
*/
|
||||||
|
Pagination pageData(PageForm pageForm, Integer year, String clubId, Integer payed, Integer assign, Integer memberState);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取社团成员是否拨付
|
* 获取社团成员是否拨付
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.budwk.app.zhgh.club.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.entity.ProcessDefine;
|
||||||
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
|
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||||
|
import com.budwk.app.sys.models.*;
|
||||||
|
import com.budwk.app.sys.services.SysDictService;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.club.model.SysClub;
|
||||||
|
import com.budwk.app.zhgh.club.model.SysClubSponsor;
|
||||||
|
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.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@IocBean
|
||||||
|
public class ClubRegisterImportService {
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
@Inject
|
||||||
|
private SysClubService sysClubService;
|
||||||
|
@Inject
|
||||||
|
private SysDictService sysDictService;
|
||||||
|
@Inject
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
@Inject
|
||||||
|
private SysUserService sysUserService;
|
||||||
|
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap importRow(String[] values) {
|
||||||
|
String name = values[0];
|
||||||
|
String code = values[1];
|
||||||
|
if (StrUtil.isBlank(name)) {
|
||||||
|
throw new IllegalArgumentException("社团名称不能为空");
|
||||||
|
}
|
||||||
|
if (dao.count(SysClub.class, Cnd.where("clubName", "=", name)) > 0
|
||||||
|
|| (StrUtil.isNotBlank(code) && dao.count(SysClub.class, Cnd.where("clubCode", "=", code)) > 0)) {
|
||||||
|
return NutMap.NEW().addv("status", "跳过").addv("message", "社团名称或编码已存在,保留原记录(含草稿、审批中及已解散社团)");
|
||||||
|
}
|
||||||
|
if (name.length() > 100 || code.length() > 100) {
|
||||||
|
throw new IllegalArgumentException("社团名称和编码不能超过100个字符");
|
||||||
|
}
|
||||||
|
if (sysDictService.getSubListByCode("CLUB_REGISTER_TYPE").stream().noneMatch(item -> values[2].equals(item.getCode()))) {
|
||||||
|
throw new IllegalArgumentException("请选择类型对照表中的社团类型编码");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LocalDate.parse(values[3]);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("成立日期必填,格式为yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
List<NutMap> managers = new ArrayList<>();
|
||||||
|
Set<String> managerIds = new HashSet<>();
|
||||||
|
addManagers(managers, managerIds, values[4], "CLUB_PRESIDENT", true);
|
||||||
|
addManagers(managers, managerIds, values[5], "CLUB_SECRETARY", true);
|
||||||
|
addManagers(managers, managerIds, values[7], "CLUB_VICE_PRESIDENT", false);
|
||||||
|
addManagers(managers, managerIds, values[8], "CLUB_VICE_SECRETARY", false);
|
||||||
|
List<SysClubSponsor> sponsors = new ArrayList<>();
|
||||||
|
for (String loginname : split(values[6])) {
|
||||||
|
SysClubSponsor sponsor = new SysClubSponsor();
|
||||||
|
sponsor.setSponsorId(findUser(loginname).getId());
|
||||||
|
sponsors.add(sponsor);
|
||||||
|
}
|
||||||
|
if (sponsors.size() < 3) {
|
||||||
|
throw new IllegalArgumentException("发起人至少需要3名不同人员,多个工号用逗号分隔");
|
||||||
|
}
|
||||||
|
ProcessDefine define = dao.fetch(ProcessDefine.class, Cnd.where("name", "=", "XHZC").and("state", "=", 1).desc("version"));
|
||||||
|
if (define == null) {
|
||||||
|
throw new IllegalArgumentException("未配置可用的社团注册定义,无法记录直接通过状态");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(code)) {
|
||||||
|
int sequence = dao.count(SysClub.class, Cnd.where("year(createTime)", "=", DateUtil.thisYear())) + 1;
|
||||||
|
do {
|
||||||
|
code = DateUtil.thisYear() + String.format("%02d", sequence++);
|
||||||
|
} while (dao.count(SysClub.class, Cnd.where("clubCode", "=", code)) > 0);
|
||||||
|
}
|
||||||
|
SysClub club = new SysClub();
|
||||||
|
club.setClubName(name);
|
||||||
|
club.setClubCode(code);
|
||||||
|
club.setClubType(values[2]);
|
||||||
|
club.setFoundTime(values[3]);
|
||||||
|
club.setSponsors(sponsors);
|
||||||
|
club.setDue(values[10]);
|
||||||
|
if (values[10].length() > 50 || values[11].length() > 20 || values[12].length() > 100 || values[15].length() > 1000) {
|
||||||
|
throw new IllegalArgumentException("会费标准、联系电话、邮箱或缩略图地址超出字段长度限制");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(values[9])) {
|
||||||
|
Sys_user contact = findUser(values[9]);
|
||||||
|
club.setConcatPerson(contact.getId());
|
||||||
|
club.setConcatPersonMobile(contact.getMobile());
|
||||||
|
club.setConcatPersonEmail(contact.getEmail());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(values[11])) club.setConcatPersonMobile(values[11]);
|
||||||
|
if (StrUtil.isNotBlank(values[12])) club.setConcatPersonEmail(values[12]);
|
||||||
|
club.setIntroduce(escapeText(values[13]));
|
||||||
|
club.setPurpose(escapeText(values[14]));
|
||||||
|
club.setThumbnail(values[15]);
|
||||||
|
club.setEstablishReport(files(values[16]));
|
||||||
|
club.setRulesFile(files(values[17]));
|
||||||
|
club.setManageFile(files(values[18]));
|
||||||
|
club.setYearPlanFile(files(values[19]));
|
||||||
|
club.setFiles(files(values[20]));
|
||||||
|
// 复用单个注册:社团、发起人、理事会员资料及入会时间、年度缴费记录和快照。
|
||||||
|
sysClubService.doAdd(club, managers);
|
||||||
|
// 与注册通过后的角色授权一致,但不启动流程或创建审批任务。
|
||||||
|
for (NutMap manager : managers) {
|
||||||
|
Sys_role role = sysRoleService.getByCode(manager.getString("roleCode"));
|
||||||
|
if (role == null) throw new IllegalArgumentException("社团角色不存在:" + manager.getString("roleCode"));
|
||||||
|
Sys_user_role relation = new Sys_user_role();
|
||||||
|
relation.setUserId(manager.getString("userId"));
|
||||||
|
relation.setRoleId(role.getId());
|
||||||
|
relation.setClubId(club.getId());
|
||||||
|
dao.insert(relation);
|
||||||
|
}
|
||||||
|
// 现有社团查询通过已完成实例识别有效社团;保留可追溯的管理员直接通过记录。
|
||||||
|
ProcessInstance instance = new ProcessInstance();
|
||||||
|
instance.setProcessDefineId(define.getId());
|
||||||
|
instance.setBusinessNo(club.getId());
|
||||||
|
instance.setOperator(SecurityUtil.getUserId());
|
||||||
|
instance.setState(ProcessInstanceStateEnum.FINISHED.getCode());
|
||||||
|
instance.setVariable(JSONUtil.toJsonStr(NutMap.NEW()
|
||||||
|
.addv(FlowConst.FORM_DATA, JSONUtil.toJsonStr(club))
|
||||||
|
.addv(FlowConst.AUTO_GEN_TITLE, name + "(系统管理员批量导入直接通过)")
|
||||||
|
.addv("directImport", true)));
|
||||||
|
dao.insert(instance);
|
||||||
|
sysUserService.clearCache();
|
||||||
|
sysRoleService.clearCache();
|
||||||
|
return NutMap.NEW().addv("status", "成功").addv("message", "注册成功,已直接通过").addv("clubCode", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addManagers(List<NutMap> managers, Set<String> ids, String text, String roleCode, boolean required) {
|
||||||
|
List<String> names = split(text);
|
||||||
|
if (required && names.size() != 1) throw new IllegalArgumentException("会长、秘书长各需填写1名人员工号");
|
||||||
|
if (!names.isEmpty() && sysRoleService.getByCode(roleCode) == null) {
|
||||||
|
throw new IllegalArgumentException("系统未配置社团角色:" + roleCode);
|
||||||
|
}
|
||||||
|
for (String name : names) {
|
||||||
|
Sys_user user = findUser(name);
|
||||||
|
if (!ids.add(user.getId())) throw new IllegalArgumentException("理事机构人员不能重复:" + name);
|
||||||
|
managers.add(NutMap.NEW().addv("userId", user.getId()).addv("roleCode", roleCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Sys_user findUser(String loginname) {
|
||||||
|
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where("loginname", "=", loginname));
|
||||||
|
if (users.size() != 1) throw new IllegalArgumentException("工号不存在或不唯一:" + loginname);
|
||||||
|
return users.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> split(String text) {
|
||||||
|
return Arrays.stream(text.split("[,,]|;|;")).map(String::trim).filter(StrUtil::isNotBlank).distinct().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<JSONObject> files(String text) {
|
||||||
|
List<JSONObject> result = new ArrayList<>();
|
||||||
|
List<String> ids = split(text);
|
||||||
|
if (ids.size() > 5) throw new IllegalArgumentException("每类附件最多5个文件");
|
||||||
|
for (String id : ids) {
|
||||||
|
Sys_file file = dao.fetch(Sys_file.class, id);
|
||||||
|
if (file == null || Boolean.TRUE.equals(file.getDelFlag())) throw new IllegalArgumentException("附件文件不存在:" + id);
|
||||||
|
result.add(JSONUtil.parseObj(file));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeText(String text) {
|
||||||
|
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
.replace("\r\n", "\n").replace("\n", "<br>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,20 @@ import com.budwk.app.zhgh.club.model.ClubUserApply;
|
|||||||
|
|
||||||
public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除待审核入会申请的入退会时间,防止沿用协会成立时间或上次入会时间。
|
||||||
|
* @param application 待保存、提交或重新提交的申请;仅处理 mode=1 的入会申请
|
||||||
|
*/
|
||||||
|
void preparePendingJoinTimes(ClubUserApply application);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会长同意审核后,将本次 WF 任务的实际完成时间写入申请入会时间。
|
||||||
|
* @param applicationId 当前流程 businessNo 对应的入会申请ID
|
||||||
|
* @param taskId 当前会长审核任务ID,必须属于该申请且已完成、同意
|
||||||
|
* @return 写入 club_user_apply.joinTime 的审核完成时间;无有效审核任务时抛出异常
|
||||||
|
*/
|
||||||
|
java.util.Date completeJoinApproval(String applicationId, Long taskId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造退会申请数据。
|
* 构造退会申请数据。
|
||||||
* 当用户没有历史入会申请记录时,使用当前社团成员关系和人员基础信息兜底生成一份退会申请,
|
* 当用户没有历史入会申请记录时,使用当前社团成员关系和人员基础信息兜底生成一份退会申请,
|
||||||
@@ -17,8 +31,8 @@ public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
|||||||
ClubUserApply buildExitApply(String clubId, String userId);
|
ClubUserApply buildExitApply(String clubId, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取社团成员的入会时间,优先取历史有效入会申请记录,
|
* 获取社团成员的入会时间,优先取当前 club_user.joinTime。
|
||||||
* 没有时兜底为社团创办时间。
|
* 旧会员关系尚无入会时间时,保留历史申请及社团创办时间的原有回退规则。
|
||||||
*
|
*
|
||||||
* @param clubId 社团ID
|
* @param clubId 社团ID
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
|
|||||||
@@ -13,12 +13,24 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface SysClubExamineService extends BaseService<SysClubExamineRegister> {
|
public interface SysClubExamineService extends BaseService<SysClubExamineRegister> {
|
||||||
|
|
||||||
|
// 2026年10月2日wxg屏蔽操作:保留原统计接口说明。
|
||||||
|
// /**
|
||||||
|
// * 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。
|
||||||
|
// *
|
||||||
|
// * @param clubId 社团ID
|
||||||
|
// * @param year 统计年度
|
||||||
|
// * @return 在职和退休人员的年初、增加、减少、年末人数
|
||||||
|
// */
|
||||||
|
// List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
|
||||||
|
//
|
||||||
|
//
|
||||||
/**
|
/**
|
||||||
* 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。
|
* 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
*
|
* @param clubId 所选社团ID,不能为空
|
||||||
* @param clubId 社团ID
|
* @param year 所选公历年度,1至9998;按该年1月1日至下一年1月1日左闭右开统计
|
||||||
* @param year 统计年度
|
* @return 两类人员计数列表:yearFirstNum年初、yearAddNum增加、yearReduceNum减少、thisYearNum公式年末;
|
||||||
* @return 在职和退休人员的年初、增加、减少、年末人数
|
* personType为类别,userState仅兼容已有查看页标签;statisticsWarnings为核查提示,
|
||||||
|
* statisticsValid=false时各人数为null,表示数据缺失或冲突,不能当作零人
|
||||||
*/
|
*/
|
||||||
List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
|
List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ public interface SysClubInfoManageService extends BaseService<ClubCommonPageVo>
|
|||||||
|
|
||||||
Pagination<ClubCommonPageVo> infoManagePageData(@Valid ClubUserPageForm pageForm);
|
Pagination<ClubCommonPageVo> infoManagePageData(@Valid ClubUserPageForm pageForm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改时间:2026年10月2日 23:06:55;操作人:wxg。
|
||||||
|
* 当前会员查询前,用已完成 WF 入会流程的最近一次会长同意审核时间补齐空入会时间。
|
||||||
|
* @param pageForm clubId 为选中社团ID;radioType=4 查历史成员且不补齐,其他值查当前会员;
|
||||||
|
* 姓名/工号、人员类型、在职状态、入会日期范围及分页排序参数用于列表筛选
|
||||||
|
* @return 会员分页对象,list 中 joinTime 为会员关系保存的入会时间;历史成员仍取申请表时间
|
||||||
|
*/
|
||||||
Pagination<ClubUserCommonPageVo> infoManageUserPageData(@Valid ClubUserPageForm pageForm);
|
Pagination<ClubUserCommonPageVo> infoManageUserPageData(@Valid ClubUserPageForm pageForm);
|
||||||
|
|
||||||
Pagination<ClubUserCommonPageVo> exitManagePageData(@Valid ClubUserPageForm pageForm);
|
Pagination<ClubUserCommonPageVo> exitManagePageData(@Valid ClubUserPageForm pageForm);
|
||||||
@@ -30,6 +37,13 @@ public interface SysClubInfoManageService extends BaseService<ClubCommonPageVo>
|
|||||||
|
|
||||||
ClubUserJoinVo exitManageInfo(String id);
|
ClubUserJoinVo exitManageInfo(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端直接退会,同时新增系统处理记录并清理当前社团会员关系及角色关联。
|
||||||
|
* @param membershipId club_user 的会员关系ID,不是用户ID或申请ID
|
||||||
|
* @return void;会员关系不存在或已退会时抛出异常,事务回滚
|
||||||
|
*/
|
||||||
|
void exitClub(String membershipId);
|
||||||
|
|
||||||
List<NutMap> getClubTreeData();
|
List<NutMap> getClubTreeData();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ import java.util.List;
|
|||||||
**/
|
**/
|
||||||
public interface SysClubService extends BaseService<SysClub> {
|
public interface SysClubService extends BaseService<SysClub> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取入会申请的社团下拉选项,供 PC 与 H5 共用。
|
||||||
|
* @return 存在已完成 WF 流程的社团列表,每项为 SysClub,包含 id、clubName 等社团字段;同一社团只返回一次
|
||||||
|
*/
|
||||||
|
List<SysClub> listJoinClubOptions();
|
||||||
|
|
||||||
Pagination<ClubCommonPageVo> getAllClubWithPage(@Valid PageForm pageForm, Cnd cnd);
|
Pagination<ClubCommonPageVo> getAllClubWithPage(@Valid PageForm pageForm, Cnd cnd);
|
||||||
|
|
||||||
ClubRegisterVo findOne(String clubId);
|
ClubRegisterVo findOne(String clubId);
|
||||||
|
|||||||
@@ -17,6 +17,32 @@ import java.util.List;
|
|||||||
**/
|
**/
|
||||||
public interface SysClubUserService extends BaseService<ClubUser> {
|
public interface SysClubUserService extends BaseService<ClubUser> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为直接新增的会员关系设置本批次的后端确认时间,不覆盖已有入会时间。
|
||||||
|
* @param members 手工录入、导入或社团创建时直接新增的会员关系;不得传入审核入会记录
|
||||||
|
* @return void;仅填写列表内 joinTime 为空的对象,由调用方在同一事务中保存
|
||||||
|
*/
|
||||||
|
void prepareDirectJoinTimes(List<ClubUser> members);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 12:01:28;操作人:wxg:启动时定向补充会员表缺失列,不删除列或调整索引。
|
||||||
|
* @return void;DDL 失败则抛出异常,阻止缺少字段的应用继续启动
|
||||||
|
*/
|
||||||
|
void upgradeMemberSchema();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动升级时补齐缺失的姓名、工号、电话及 WF 会长审核入会时间;不覆盖非空历史数据。
|
||||||
|
* @return void;没有用户资料或有效 WF 审核记录的字段保留为空
|
||||||
|
*/
|
||||||
|
void backfillMemberSnapshots();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为真正新增的会员关系批量填写用户当前姓名、工号、电话,不修改入会时间。
|
||||||
|
* @param members 待新增的会员关系,必须包含有效 userId;不得传入仅修改身份的既有关系
|
||||||
|
* @return void;填写传入对象的 userName/loginName/mobile,由调用方在同一事务中保存
|
||||||
|
*/
|
||||||
|
void prepareMemberSnapshots(List<ClubUser> members);
|
||||||
|
|
||||||
List<NutMap> getClubUser(@Valid String clubId);
|
List<NutMap> getClubUser(@Valid String clubId);
|
||||||
|
|
||||||
Pagination pageDataByApplyJoinClubAudit(@Valid ClubUserPageForm pageForm);
|
Pagination pageDataByApplyJoinClubAudit(@Valid ClubUserPageForm pageForm);
|
||||||
|
|||||||
@@ -3,13 +3,35 @@ package com.budwk.app.zhgh.club.service.impl;
|
|||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
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.web.commons.auth.utils.AuthUtil;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||||
|
import org.nutz.dao.util.cri.Static;
|
||||||
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import java.util.List;
|
||||||
|
import com.budwk.app.sys.views.View_user;
|
||||||
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import org.nutz.dao.util.Daos;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Date;
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
||||||
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -27,6 +49,301 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl<ClubPayRecord> imp
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:列表只读取已保存结果;缴费设置和退会操作负责更新 isGiveMoney。
|
||||||
|
private static final String ASSIGNED_SQL = "CASE WHEN cpr.isGiveMoney = true THEN 1 ELSE 0 END";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void upgradePayRecordSchema() {
|
||||||
|
// 定向检查独立于 DEBUG;DDL 在 MySQL 中会隐式提交,失败时终止升级以免运行缺列查询。
|
||||||
|
if (!dao().exists("club_pay_record")) {
|
||||||
|
dao().create(ClubPayRecord.class, false);
|
||||||
|
} else {
|
||||||
|
Daos.migration(dao(), ClubPayRecord.class, true, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void backfillPayRecordSnapshots() {
|
||||||
|
// 已有工号或姓名表示快照已建立;退休日期为空可能是真实资料,不能每次启动重新覆盖快照。
|
||||||
|
dao().execute(Sqls.create("""
|
||||||
|
UPDATE club_pay_record cpr
|
||||||
|
INNER JOIN vw_user u ON u.id = cpr.userId
|
||||||
|
SET cpr.loginName = COALESCE(cpr.loginName, u.loginName),
|
||||||
|
cpr.userName = COALESCE(cpr.userName, u.userName),
|
||||||
|
cpr.unitName = COALESCE(cpr.unitName, u.unitName),
|
||||||
|
cpr.personType = COALESCE(cpr.personType, u.personType),
|
||||||
|
cpr.retireDate = COALESCE(cpr.retireDate, u.retireDate)
|
||||||
|
WHERE cpr.loginName IS NULL AND cpr.userName IS NULL
|
||||||
|
"""));
|
||||||
|
// 按年度批量补时间,不能将本年度重新入会时间写入之前年度;无可靠来源保持为空。
|
||||||
|
for (NutMap row : listMap(Sqls.create("SELECT DISTINCT year FROM club_pay_record WHERE joinTime IS NULL AND year IS NOT NULL"))) {
|
||||||
|
int year = row.getInt("year");
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
UPDATE club_pay_record cpr
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT clubId, userId, MIN(joinTime) AS joinTime FROM club_user
|
||||||
|
WHERE joinTime < @yearEnd GROUP BY clubId, userId
|
||||||
|
) memberTime ON memberTime.clubId = cpr.clubId AND memberTime.userId = cpr.userId
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT application.clubId, application.userId, MAX(task.finishTime) AS joinTime
|
||||||
|
FROM club_user_apply application
|
||||||
|
INNER JOIN wf_process_instance instance ON instance.businessNo = application.id
|
||||||
|
INNER JOIN wf_process_task task ON task.processInstanceId = instance.id
|
||||||
|
WHERE application.mode = 1 AND instance.state = 20 AND task.taskState = 20
|
||||||
|
AND task.taskName = '1625a683-3890-4788-95b7-cab8240f6731'
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(task.variable, '$.submitType')) = '1'
|
||||||
|
AND task.finishTime < @yearEnd
|
||||||
|
GROUP BY application.clubId, application.userId
|
||||||
|
) approvalTime ON approvalTime.clubId = cpr.clubId AND approvalTime.userId = cpr.userId
|
||||||
|
SET cpr.joinTime = COALESCE(memberTime.joinTime, approvalTime.joinTime)
|
||||||
|
WHERE cpr.year = @year AND cpr.joinTime IS NULL
|
||||||
|
AND COALESCE(memberTime.joinTime, approvalTime.joinTime) IS NOT NULL
|
||||||
|
""");
|
||||||
|
sql.setParam("year", year);
|
||||||
|
sql.setParam("yearEnd", (year + 1) + "-01-01");
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void prepareSnapshots(List<ClubPayRecord> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 新增记录须明确所属年度、社团和人员,避免缺失参数形成不受限制的批量查询。
|
||||||
|
for (ClubPayRecord record : records) {
|
||||||
|
if (record == null || record.getYear() == null || record.getYear() < 1 || record.getYear() > 9998
|
||||||
|
|| StrUtil.isBlank(record.getClubId()) || StrUtil.isBlank(record.getUserId())) {
|
||||||
|
throw new IllegalArgumentException("缴费记录必须提供有效年度、社团和用户");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList();
|
||||||
|
List<String> clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList();
|
||||||
|
Map<String, View_user> users = new HashMap<>();
|
||||||
|
for (View_user user : dao().query(View_user.class, Cnd.where("id", "in", userIds))) {
|
||||||
|
users.put(user.getId(), user);
|
||||||
|
}
|
||||||
|
Map<String, Date> joinTimes = new HashMap<>();
|
||||||
|
Set<String> activeMembers = new HashSet<>();
|
||||||
|
for (ClubUser member : dao().query(ClubUser.class, Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds))) {
|
||||||
|
activeMembers.add(member.getClubId() + ":" + member.getUserId());
|
||||||
|
if (member.getJoinTime() != null) {
|
||||||
|
joinTimes.merge(member.getClubId() + ":" + member.getUserId(), member.getJoinTime(),
|
||||||
|
(first, next) -> first.before(next) ? first : next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ClubPayRecord record : records) {
|
||||||
|
View_user user = users.get(record.getUserId());
|
||||||
|
if (user != null && record.getLoginName() == null && record.getUserName() == null) {
|
||||||
|
record.setLoginName(user.getLoginname());
|
||||||
|
record.setUserName(user.getUsername());
|
||||||
|
if (record.getUnitName() == null) record.setUnitName(user.getUnitName());
|
||||||
|
if (record.getPersonType() == null) record.setPersonType(user.getPersonType());
|
||||||
|
if (record.getRetireDate() == null) record.setRetireDate(user.getRetireDate());
|
||||||
|
}
|
||||||
|
Date joinTime = joinTimes.get(record.getClubId() + ":" + record.getUserId());
|
||||||
|
if (record.getJoinTime() == null && joinTime != null
|
||||||
|
&& (record.getYear() == null || joinTime.before(DateUtil.parse((record.getYear() + 1) + "-01-01")))) {
|
||||||
|
record.setJoinTime(joinTime);
|
||||||
|
}
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:新增名单或入会更新缴费时,同步保存资格;退会成员优先判定不拨付。
|
||||||
|
record.setIsGiveMoney(qualifiesForPayment(record, activeMembers.contains(record.getClubId() + ":" + record.getUserId())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public int generateYearPayRecords(Integer year) {
|
||||||
|
if (year == null || year < 1 || year > 9998) {
|
||||||
|
throw new IllegalArgumentException("缴费年度不正确");
|
||||||
|
}
|
||||||
|
Set<String> recorded = new HashSet<>();
|
||||||
|
for (ClubPayRecord record : dao().query(ClubPayRecord.class, Cnd.where("year", "=", year))) {
|
||||||
|
recorded.add(record.getClubId() + ":" + record.getUserId());
|
||||||
|
}
|
||||||
|
List<ClubPayRecord> records = new ArrayList<>();
|
||||||
|
for (ClubUser member : dao().query(ClubUser.class, Cnd.NEW())) {
|
||||||
|
if (!recorded.add(member.getClubId() + ":" + member.getUserId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ClubPayRecord record = new ClubPayRecord();
|
||||||
|
record.setYear(year);
|
||||||
|
record.setClubId(member.getClubId());
|
||||||
|
record.setUserId(member.getUserId());
|
||||||
|
// 加入时间与人员资料交由 prepareSnapshots 批量读取,并校验所属年度。
|
||||||
|
record.setPayed(false);
|
||||||
|
JSONObject log = new JSONObject();
|
||||||
|
log.set("operatorUserId", "task");
|
||||||
|
log.set("operatorUserName", "定时任务");
|
||||||
|
log.set("operatorTime", DateUtil.now());
|
||||||
|
log.set("operatorType", "缴费管理-定时任务自动设置");
|
||||||
|
log.set("oldValue", null);
|
||||||
|
log.set("newValue", false);
|
||||||
|
record.setOperateLogs(List.of(log));
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
prepareSnapshots(records);
|
||||||
|
if (!records.isEmpty()) {
|
||||||
|
dao().insert(records);
|
||||||
|
}
|
||||||
|
return records.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校工会社团管理员与系统管理员可查看全部社团缴费记录。 */
|
||||||
|
private boolean canManageAllPayClubs() {
|
||||||
|
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 普通菜单授权用户仅能查看其有效角色关联的社团;无关联时显式返回空范围。 */
|
||||||
|
private void addPayClubScope(Cnd cnd, String clubField) {
|
||||||
|
if (!canManageAllPayClubs()) {
|
||||||
|
// 用户ID使用 SQL 参数绑定,社团集合交给 Cnd 处理,避免空集合被可选条件忽略。
|
||||||
|
Sql scopeSql = Sqls.create("""
|
||||||
|
SELECT DISTINCT ur.clubId FROM sys_user_role ur
|
||||||
|
INNER JOIN sys_role r ON r.id = ur.roleId AND r.disabled = false
|
||||||
|
WHERE ur.userId = @userId AND ur.enable = true AND ur.clubId IS NOT NULL
|
||||||
|
""").setParam("userId", SecurityUtil.getUserId());
|
||||||
|
List<String> clubIds = listMap(scopeSql).stream()
|
||||||
|
.map(row -> row.getString("clubId"))
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.toList();
|
||||||
|
if (clubIds.isEmpty()) {
|
||||||
|
cnd.and(new Static("1 = 0"));
|
||||||
|
} else {
|
||||||
|
cnd.and(clubField, "in", clubIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> getPayManageClubList() {
|
||||||
|
Sql sql = Sqls.create("SELECT c.id, c.clubName, c.clubCode FROM sys_club c $condition");
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
addPayClubScope(cnd, "c.id");
|
||||||
|
cnd.asc("c.clubCode");
|
||||||
|
cnd.asc("c.id");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
return listMap(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pagination pageData(PageForm pageForm, Integer year, String clubId, Integer payed, Integer assign, Integer memberState) {
|
||||||
|
// 2026年10月3日 14:33:18;操作人:wxg:当前会员状态为可选参数,service 拒绝非法值,避免错误条件被当作全部。
|
||||||
|
if (memberState != null && memberState != 0 && memberState != 1) {
|
||||||
|
throw new IllegalArgumentException("会员状态只能为在会、退会或全部");
|
||||||
|
}
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:人员资料直接读年度快照,取消用户视图及逐行会员子查询。
|
||||||
|
Sql sql = Sqls.create("SELECT cpr.*, c.clubName, cpr.joinTime AS applyTime, ("
|
||||||
|
+ ASSIGNED_SQL + ") AS assign FROM club_pay_record cpr "
|
||||||
|
+ "LEFT JOIN sys_club c ON cpr.clubId = c.id $condition");
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.andEX("cpr.year", "=", year);
|
||||||
|
cnd.andEX("cpr.clubId", "=", clubId);
|
||||||
|
addPayClubScope(cnd, "cpr.clubId");
|
||||||
|
cnd.andEX("cpr.payed", "=", payed);
|
||||||
|
// 在分页前筛选,保证总条数及页数准确;EXISTS 不会因重复会员关系增加缴费记录条数。
|
||||||
|
// 与列表 count 标签一致,只看该人员在该社团的当前关系,不依赖退会申请或缴费记录年份。
|
||||||
|
if (memberState != null) {
|
||||||
|
String membershipExists = "EXISTS (SELECT 1 FROM club_user membership "
|
||||||
|
+ "WHERE membership.clubId = cpr.clubId AND membership.userId = cpr.userId)";
|
||||||
|
cnd.and(new Static(memberState == 1 ? membershipExists : "NOT " + membershipExists));
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
|
SqlExpressionGroup seg = new SqlExpressionGroup();
|
||||||
|
seg.orLike("cpr.userName", pageForm.getSearchKeyword());
|
||||||
|
seg.orLike("cpr.loginName", pageForm.getSearchKeyword());
|
||||||
|
cnd.and(seg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assign != null) {
|
||||||
|
cnd.and(new Static("(" + ASSIGNED_SQL + ") = " + (assign == 0 ? 0 : 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全社团管理角色先按社团编号分组;普通角色仅在授权社团内按加入时间倒序。
|
||||||
|
if (canManageAllPayClubs()) {
|
||||||
|
cnd.asc("c.clubCode");
|
||||||
|
cnd.asc("c.id");
|
||||||
|
}
|
||||||
|
cnd.desc("cpr.joinTime");
|
||||||
|
cnd.asc("cpr.id");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
|
|
||||||
|
List<NutMap> list = pagination.getList();
|
||||||
|
Set<String> activeMembers = new HashSet<>();
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
List<String> userIds = list.stream().map(row -> row.getString("userId")).distinct().toList();
|
||||||
|
List<String> clubIds = list.stream().map(row -> row.getString("clubId")).distinct().toList();
|
||||||
|
Sql membershipSql = Sqls.create("SELECT clubId, userId FROM club_user $condition");
|
||||||
|
membershipSql.setCondition(Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds));
|
||||||
|
for (NutMap member : listMap(membershipSql)) {
|
||||||
|
activeMembers.add(member.getString("clubId") + ":" + member.getString("userId"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:当前状态实时查询,拨付状态使用保存值,查询期间不重算、不写库。
|
||||||
|
for (NutMap row : list) {
|
||||||
|
row.put("count", activeMembers.contains(row.getString("clubId") + ":" + row.getString("userId")) ? 1 : 0);
|
||||||
|
row.put("assign", row.getInt("assign") == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void updatePayStatus(String[] ids, Boolean payed) {
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:明确记录ID及缴费布尔值,避免空参数造成批量误更新。
|
||||||
|
if (ids == null || ids.length == 0 || payed == null || java.util.Arrays.stream(ids).anyMatch(StrUtil::isBlank)) {
|
||||||
|
throw new IllegalArgumentException("请选择缴费记录并提供缴费状态");
|
||||||
|
}
|
||||||
|
Cnd scope = Cnd.where("id", "in", ids);
|
||||||
|
addPayClubScope(scope, "clubId");
|
||||||
|
List<ClubPayRecord> records = dao().query(ClubPayRecord.class, scope);
|
||||||
|
if (records.size() != java.util.Arrays.stream(ids).distinct().count()) {
|
||||||
|
throw new IllegalArgumentException("部分缴费记录不存在或无权操作,请刷新列表");
|
||||||
|
}
|
||||||
|
List<String> userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList();
|
||||||
|
List<String> clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList();
|
||||||
|
Set<String> activeMembers = new HashSet<>();
|
||||||
|
for (ClubUser member : dao().query(ClubUser.class, Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds))) {
|
||||||
|
activeMembers.add(member.getClubId() + ":" + member.getUserId());
|
||||||
|
}
|
||||||
|
for (ClubPayRecord record : records) {
|
||||||
|
List<JSONObject> logs = record.getOperateLogs() == null ? new ArrayList<>() : new ArrayList<>(record.getOperateLogs());
|
||||||
|
JSONObject log = new JSONObject();
|
||||||
|
log.set("operatorUserId", SecurityUtil.getUserId());
|
||||||
|
log.set("operatorUserName", SecurityUtil.getUserUsername());
|
||||||
|
log.set("operatorTime", DateUtil.now());
|
||||||
|
log.set("operatorType", "缴费管理-手动设置");
|
||||||
|
log.set("oldValue", record.getPayed());
|
||||||
|
log.set("newValue", payed);
|
||||||
|
log.set("oldIsGiveMoney", record.getIsGiveMoney());
|
||||||
|
record.setPayed(payed);
|
||||||
|
record.setIsGiveMoney(qualifiesForPayment(record, activeMembers.contains(record.getClubId() + ":" + record.getUserId())));
|
||||||
|
log.set("newIsGiveMoney", record.getIsGiveMoney());
|
||||||
|
logs.add(log);
|
||||||
|
record.setOperateLogs(logs);
|
||||||
|
}
|
||||||
|
dao().update(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void disableExitMemberPayment(String clubId, String userId) {
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:退会影响实时会员关系对应的各年度名单,保留原缴费状态。
|
||||||
|
if (StrUtil.isBlank(clubId) || StrUtil.isBlank(userId)) {
|
||||||
|
throw new IllegalArgumentException("退会社团和用户不能为空");
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("UPDATE club_pay_record SET isGiveMoney = false WHERE clubId = @clubId AND userId = @userId");
|
||||||
|
sql.setParam("clubId", clubId);
|
||||||
|
sql.setParam("userId", userId);
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean calAssigned(String userId, String clubId) {
|
public boolean calAssigned(String userId, String clubId) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
@@ -45,18 +362,23 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl<ClubPayRecord> imp
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean calAssigned(ClubPayRecord payRecord) {
|
public boolean calAssigned(ClubPayRecord payRecord) {
|
||||||
// 如果是未缴费,直接是不拨付
|
// 2026年10月3日 09:51:00;操作人:wxg:业务更新时计算资格,会员关系不存在即对应页面退会标签。
|
||||||
if (!payRecord.getPayed()) {
|
boolean active = payRecord != null && dao().count(ClubUser.class, Cnd.where("clubId", "=", payRecord.getClubId())
|
||||||
return false;
|
.and("userId", "=", payRecord.getUserId())) > 0;
|
||||||
}
|
return qualifiesForPayment(payRecord, active);
|
||||||
Sys_user user = dao().fetch(Sys_user.class, payRecord.getUserId());
|
}
|
||||||
if (!Objects.equals("退休", user.getUserState())) {
|
|
||||||
return true;
|
/** 退休年份规则沿用原口径;未缴费、已退会或退休日期缺失时不拨付。 */
|
||||||
}
|
private boolean qualifiesForPayment(ClubPayRecord payRecord, boolean active) {
|
||||||
if (Lang.isEmpty(user.getRetireDate())) {
|
if (!active || payRecord == null || !Boolean.TRUE.equals(payRecord.getPayed())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int retireYear = DateUtil.year(user.getRetireDate());
|
if (!Objects.equals("离退休人员", payRecord.getPersonType())) {
|
||||||
return retireYear >= (payRecord.getYear() != null ? payRecord.getYear() : DateUtil.thisYear());
|
return true;
|
||||||
}
|
}
|
||||||
|
if (payRecord.getRetireDate() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return DateUtil.year(payRecord.getRetireDate()) >= (payRecord.getYear() != null ? payRecord.getYear() : DateUtil.thisYear());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,77 @@ import com.budwk.app.base.constant.RoleConstant;
|
|||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
|
import com.budwk.app.flow.entity.ProcessTask;
|
||||||
|
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
|
||||||
|
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||||
import com.budwk.app.sys.views.View_user;
|
import com.budwk.app.sys.views.View_user;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
import com.budwk.app.zhgh.club.model.SysClub;
|
import com.budwk.app.zhgh.club.model.SysClub;
|
||||||
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
|
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
|
||||||
|
// PC 与 H5 入会资格校验共用此名单,人员类型须与用户视图中的值完全一致。
|
||||||
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
|
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
|
||||||
"教职工",
|
"教职工",
|
||||||
"未起薪人员",
|
|
||||||
"教职工新入会",
|
|
||||||
"教职工其他",
|
|
||||||
"劳动服务公司",
|
"劳动服务公司",
|
||||||
"后勤聘用人员",
|
"劳务派遣人员"
|
||||||
"退休人员[一]",
|
|
||||||
"退休人员[二]",
|
|
||||||
"校聘劳务派遣全额"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
public ClubUserJoinServiceImpl(Dao dao) {
|
public ClubUserJoinServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preparePendingJoinTimes(ClubUserApply application) {
|
||||||
|
if (Integer.valueOf(1).equals(application.getMode())) {
|
||||||
|
// 审核前尚未正式入会;后端清除客户端回传或草稿遗留的时间,退会申请保留原入会时间。
|
||||||
|
application.setJoinTime(null);
|
||||||
|
application.setExitTime(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Date completeJoinApproval(String applicationId, Long taskId) {
|
||||||
|
if (StrUtil.isBlank(applicationId) || taskId == null) {
|
||||||
|
throw new IllegalArgumentException("入会申请或会长审核任务不能为空");
|
||||||
|
}
|
||||||
|
// WF 引擎已先保存完成时间;Execution 中的任务对象可能仍是完成前的数据,必须重新查库。
|
||||||
|
ProcessTask task = dao().fetch(ProcessTask.class, taskId);
|
||||||
|
if (task == null
|
||||||
|
|| !"1625a683-3890-4788-95b7-cab8240f6731".equals(task.getTaskName())
|
||||||
|
|| !ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())
|
||||||
|
|| task.getFinishTime() == null
|
||||||
|
|| StrUtil.isBlank(task.getVariable())
|
||||||
|
|| !ProcessSubmitTypeEnum.AGREE.getCode().equals(JSONUtil.parseObj(task.getVariable()).getInt(FlowConst.SUBMIT_TYPE))) {
|
||||||
|
throw new IllegalArgumentException("没有已完成且同意的会长入会审核任务");
|
||||||
|
}
|
||||||
|
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
|
||||||
|
ClubUserApply application = fetch(applicationId);
|
||||||
|
// 只更新当前任务所属的入会申请,不覆盖其他申请、退会记录或之前的入会周期。
|
||||||
|
if (instance == null || !Objects.equals(applicationId, instance.getBusinessNo())
|
||||||
|
|| application == null || !Integer.valueOf(1).equals(application.getMode())) {
|
||||||
|
throw new IllegalArgumentException("会长审核任务与入会申请不匹配");
|
||||||
|
}
|
||||||
|
dao().update(ClubUserApply.class, Chain.make("joinTime", task.getFinishTime()), Cnd.where("id", "=", applicationId));
|
||||||
|
return task.getFinishTime();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String checkJoinQualification(String userId) {
|
public String checkJoinQualification(String userId) {
|
||||||
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId));
|
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId));
|
||||||
@@ -51,6 +92,8 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClubUserApply buildExitApply(String clubId, String userId) {
|
public ClubUserApply buildExitApply(String clubId, String userId) {
|
||||||
|
// 退会以当前会员关系的入会时间为准,不能沿用上一轮退出申请的时间。
|
||||||
|
ClubUser currentMember = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId));
|
||||||
// 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。
|
// 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。
|
||||||
ClubUserApply latestApply = fetch(
|
ClubUserApply latestApply = fetch(
|
||||||
Cnd.where("userId", "=", userId)
|
Cnd.where("userId", "=", userId)
|
||||||
@@ -59,11 +102,14 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
|
|||||||
);
|
);
|
||||||
if (latestApply != null) {
|
if (latestApply != null) {
|
||||||
latestApply.setId(null);
|
latestApply.setId(null);
|
||||||
|
if (currentMember != null && currentMember.getJoinTime() != null) {
|
||||||
|
latestApply.setJoinTime(currentMember.getJoinTime());
|
||||||
|
}
|
||||||
return latestApply;
|
return latestApply;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于会长等注册社团时直接入会的人员,没有申请记录时从当前成员信息兜底生成退会申请。
|
// 对于会长等注册社团时直接入会的人员,没有申请记录时从当前成员信息兜底生成退会申请。
|
||||||
ClubUser clubUser = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId));
|
ClubUser clubUser = currentMember;
|
||||||
if (clubUser == null) {
|
if (clubUser == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -105,6 +151,11 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Date resolveJoinTime(String clubId, String userId) {
|
public Date resolveJoinTime(String clubId, String userId) {
|
||||||
|
// 新会员直接使用关系表的入会时间;旧记录尚未补齐时仍保留原有回退逻辑。
|
||||||
|
ClubUser member = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId));
|
||||||
|
if (member != null && member.getJoinTime() != null) {
|
||||||
|
return member.getJoinTime();
|
||||||
|
}
|
||||||
ClubUserApply latestJoinApply = fetch(
|
ClubUserApply latestJoinApply = fetch(
|
||||||
Cnd.where("userId", "=", userId)
|
Cnd.where("userId", "=", userId)
|
||||||
.and("clubId", "=", clubId)
|
.and("clubId", "=", clubId)
|
||||||
|
|||||||
+483
-241
@@ -36,6 +36,7 @@ import java.util.Comparator;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -50,260 +51,501 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// 2026年10月2日wxg屏蔽操作:原统计入口及专用辅助代码逐行注释,保留原文便于对比或恢复。
|
||||||
public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
// @Override
|
||||||
Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
|
// public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
||||||
Date nextYearStart = DateUtil.parse((year + 1) + "-01-01 00:00:00");
|
// Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
|
||||||
List<MembershipPeriod> membershipPeriods = getMembershipPeriods(clubId);
|
// Date nextYearStart = DateUtil.parse((year + 1) + "-01-01 00:00:00");
|
||||||
NutMap activeResult = createUserChangeResult("在职");
|
// List<MembershipPeriod> membershipPeriods = getMembershipPeriods(clubId);
|
||||||
NutMap retiredResult = createUserChangeResult("退休");
|
// NutMap activeResult = createUserChangeResult("在职");
|
||||||
|
// NutMap retiredResult = createUserChangeResult("退休");
|
||||||
|
//
|
||||||
|
// for (MembershipPeriod period : membershipPeriods) {
|
||||||
|
// // 非正常历史会员没有退会时间且没有已完成退会申请时,无法确定所属年度,不计入统计。
|
||||||
|
// if (!period.normal && period.exitTime == null) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// if (isMemberAtYearBoundary(period, yearStart)) {
|
||||||
|
// increaseResult(classifyAtBoundary(period, yearStart), activeResult, retiredResult, "yearFirstNum");
|
||||||
|
// }
|
||||||
|
// if (isInYear(period.joinTime, yearStart, nextYearStart)) {
|
||||||
|
// increaseResult(classifyAtEvent(period, period.joinTime), activeResult, retiredResult, "yearAddNum");
|
||||||
|
// }
|
||||||
|
// if (isInYear(period.exitTime, yearStart, nextYearStart)) {
|
||||||
|
// increaseResult(classifyBeforeEvent(period, period.exitTime), activeResult, retiredResult, "yearReduceNum");
|
||||||
|
// }
|
||||||
|
// // 退休发生在会员有效期内时,作为在职减少一人、退休增加一人处理。
|
||||||
|
// if (isInYear(period.retireDate, yearStart, nextYearStart)
|
||||||
|
// && period.joinTime.before(period.retireDate)
|
||||||
|
// && (period.exitTime == null || period.exitTime.after(period.retireDate))) {
|
||||||
|
// increaseResult("在职", activeResult, retiredResult, "yearReduceNum");
|
||||||
|
// increaseResult("退休", activeResult, retiredResult, "yearAddNum");
|
||||||
|
// }
|
||||||
|
// if (isMemberAtYearBoundary(period, nextYearStart)) {
|
||||||
|
// increaseResult(classifyAtBoundary(period, nextYearStart), activeResult, retiredResult, "thisYearNum");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return List.of(activeResult, retiredResult);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。
|
||||||
|
// * 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。
|
||||||
|
// */
|
||||||
|
// private List<MembershipPeriod> getMembershipPeriods(String clubId) {
|
||||||
|
// Sql legacySql = Sqls.create("""
|
||||||
|
// SELECT
|
||||||
|
// cu.userId,
|
||||||
|
// CAST(cu.joinTime AS DATETIME) joinTime,
|
||||||
|
// STR_TO_DATE(NULLIF(cu.changeTime, ''), '%Y-%m-%d %H:%i:%s') exitTime,
|
||||||
|
// cu.isNormal,
|
||||||
|
// u.userState,
|
||||||
|
// u.retireDate
|
||||||
|
// FROM sys_club_user cu
|
||||||
|
// LEFT JOIN sys_user u ON u.id = cu.userId
|
||||||
|
// WHERE cu.clubId = @clubId
|
||||||
|
// AND cu.status = 5
|
||||||
|
// AND cu.joinTime IS NOT NULL
|
||||||
|
// AND cu.joinTime <> ''
|
||||||
|
// """).setParam("clubId", clubId);
|
||||||
|
// List<MembershipPeriod> periods = listMap(legacySql).stream()
|
||||||
|
// .map(row -> new MembershipPeriod(
|
||||||
|
// row.getString("userId"),
|
||||||
|
// row.getTime("joinTime"),
|
||||||
|
// row.getTime("exitTime"),
|
||||||
|
// !Boolean.FALSE.equals(row.getBoolean("isNormal")),
|
||||||
|
// true,
|
||||||
|
// row.getString("userState"),
|
||||||
|
// row.getTime("retireDate")
|
||||||
|
// ))
|
||||||
|
// .filter(period -> period.joinTime != null)
|
||||||
|
// .collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
//
|
||||||
|
// Sql applySql = Sqls.create("""
|
||||||
|
// SELECT
|
||||||
|
// cu.userId,
|
||||||
|
// cu.id,
|
||||||
|
// cu.mode,
|
||||||
|
// cu.applyDate,
|
||||||
|
// cu.joinTime,
|
||||||
|
// cu.exitTime,
|
||||||
|
// u.userState,
|
||||||
|
// u.retireDate
|
||||||
|
// FROM club_user_apply cu
|
||||||
|
// LEFT JOIN sys_user u ON u.id = cu.userId
|
||||||
|
// WHERE cu.clubId = @clubId
|
||||||
|
// AND COALESCE(cu.delFlag, 0) = 0
|
||||||
|
// ORDER BY cu.applyDate, cu.mode
|
||||||
|
// """)
|
||||||
|
// .setParam("clubId", clubId);
|
||||||
|
// List<NutMap> applications = listMap(applySql);
|
||||||
|
// if (!applications.isEmpty()) {
|
||||||
|
// // 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。
|
||||||
|
// List<String> applicationIds = applications.stream()
|
||||||
|
// .map(application -> application.getString("id"))
|
||||||
|
// .filter(StrUtil::isNotBlank)
|
||||||
|
// .collect(Collectors.toList());
|
||||||
|
// List<ProcessInstance> finishedInstances = applicationIds.isEmpty() ? new ArrayList<>()
|
||||||
|
// : dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds)
|
||||||
|
// .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode()));
|
||||||
|
// Set<String> finishedBusinessNos = finishedInstances.stream()
|
||||||
|
// .map(ProcessInstance::getBusinessNo)
|
||||||
|
// .filter(StrUtil::isNotBlank)
|
||||||
|
// .collect(Collectors.toCollection(HashSet::new));
|
||||||
|
// applications.removeIf(application -> !finishedBusinessNos.contains(application.getString("id")));
|
||||||
|
// }
|
||||||
|
// applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo)));
|
||||||
|
//
|
||||||
|
// for (NutMap application : applications) {
|
||||||
|
// Date applyDate = application.getTime("applyDate");
|
||||||
|
// Boolean mode = application.getBoolean("mode");
|
||||||
|
// if (applyDate == null || mode == null) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// if (mode) {
|
||||||
|
// mergeJoinApplication(periods, application, applyDate);
|
||||||
|
// } else {
|
||||||
|
// mergeExitApplication(periods, application, applyDate);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return periods;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 已迁移的入会申请与旧会员记录精确对应时不重复新增;迁移后的新申请以审核申请时间作为入会时间。
|
||||||
|
// */
|
||||||
|
// private void mergeJoinApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
||||||
|
// String userId = application.getString("userId");
|
||||||
|
// Date applicationJoinTime = application.getTime("joinTime");
|
||||||
|
// boolean representedByLegacy = periods.stream()
|
||||||
|
// .filter(period -> period.legacy && Objects.equals(period.userId, userId))
|
||||||
|
// .filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
||||||
|
// .anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
|
||||||
|
// boolean representedByNewPeriod = periods.stream()
|
||||||
|
// .filter(period -> !period.legacy && Objects.equals(period.userId, userId))
|
||||||
|
// .anyMatch(period -> isMemberAt(period, applyDate));
|
||||||
|
// if (representedByLegacy || representedByNewPeriod) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// periods.add(new MembershipPeriod(
|
||||||
|
// userId,
|
||||||
|
// applyDate,
|
||||||
|
// null,
|
||||||
|
// true,
|
||||||
|
// false,
|
||||||
|
// application.getString("userState"),
|
||||||
|
// application.getTime("retireDate")
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
|
||||||
|
// */
|
||||||
|
// private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
||||||
|
// String userId = application.getString("userId");
|
||||||
|
// Date applicationJoinTime = application.getTime("joinTime");
|
||||||
|
// Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
|
||||||
|
// MembershipPeriod target = periods.stream()
|
||||||
|
// .filter(period -> Objects.equals(period.userId, userId))
|
||||||
|
// .filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
||||||
|
// .filter(period -> period.exitTime == null)
|
||||||
|
// .max(Comparator.comparing(period -> period.joinTime))
|
||||||
|
// .orElse(null);
|
||||||
|
// if (target == null) {
|
||||||
|
// target = periods.stream()
|
||||||
|
// .filter(period -> Objects.equals(period.userId, userId))
|
||||||
|
// .filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
|
||||||
|
// .max(Comparator.comparing(period -> period.joinTime))
|
||||||
|
// .orElse(null);
|
||||||
|
// }
|
||||||
|
// if (target != null) {
|
||||||
|
// target.exitTime = exitTime;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private NutMap createUserChangeResult(String userState) {
|
||||||
|
// return NutMap.NEW()
|
||||||
|
// .addv("userState", userState)
|
||||||
|
// .addv("yearFirstNum", 0)
|
||||||
|
// .addv("yearAddNum", 0)
|
||||||
|
// .addv("yearReduceNum", 0)
|
||||||
|
// .addv("thisYearNum", 0);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private void increaseResult(String userState, NutMap activeResult, NutMap retiredResult, String field) {
|
||||||
|
// NutMap result = "退休".equals(userState) ? retiredResult : "在职".equals(userState) ? activeResult : null;
|
||||||
|
// if (result != null) {
|
||||||
|
// result.put(field, result.getInt(field) + 1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isInYear(Date date, Date yearStart, Date nextYearStart) {
|
||||||
|
// return date != null && !date.before(yearStart) && date.before(nextYearStart);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isMemberAt(MembershipPeriod period, Date date) {
|
||||||
|
// return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) {
|
||||||
|
// return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String classifyAtBoundary(MembershipPeriod period, Date boundary) {
|
||||||
|
// if (period.retireDate != null) {
|
||||||
|
// return period.retireDate.before(boundary) ? "退休" : "在职";
|
||||||
|
// }
|
||||||
|
// return normalizeUserState(period.userState);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String classifyAtEvent(MembershipPeriod period, Date eventTime) {
|
||||||
|
// if (period.retireDate != null) {
|
||||||
|
// return period.retireDate.after(eventTime) ? "在职" : "退休";
|
||||||
|
// }
|
||||||
|
// return normalizeUserState(period.userState);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String classifyBeforeEvent(MembershipPeriod period, Date eventTime) {
|
||||||
|
// if (period.retireDate != null) {
|
||||||
|
// return period.retireDate.before(eventTime) ? "退休" : "在职";
|
||||||
|
// }
|
||||||
|
// return normalizeUserState(period.userState);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String normalizeUserState(String userState) {
|
||||||
|
// // 历史成员数据可能未同步人员状态,空值不参与在职、退休人数统计。
|
||||||
|
// if (StrUtil.isBlank(userState)) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// if (List.of("在职", "在岗").contains(userState)) {
|
||||||
|
// return "在职";
|
||||||
|
// }
|
||||||
|
// if (List.of("退休", "退休【变号】").contains(userState)) {
|
||||||
|
// return "退休";
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private static class MembershipPeriod {
|
||||||
|
// private final String userId;
|
||||||
|
// private final Date joinTime;
|
||||||
|
// private Date exitTime;
|
||||||
|
// private final boolean normal;
|
||||||
|
// private final boolean legacy;
|
||||||
|
// private final String userState;
|
||||||
|
// private final Date retireDate;
|
||||||
|
//
|
||||||
|
// private MembershipPeriod(String userId, Date joinTime, Date exitTime, boolean normal, boolean legacy,
|
||||||
|
// String userState, Date retireDate) {
|
||||||
|
// this.userId = userId;
|
||||||
|
// this.joinTime = joinTime;
|
||||||
|
// this.exitTime = exitTime;
|
||||||
|
// this.normal = normal;
|
||||||
|
// this.legacy = legacy;
|
||||||
|
// this.userState = userState;
|
||||||
|
// this.retireDate = retireDate;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 2026年10月2日22:00点开始新开发的代码。
|
||||||
|
// 实际整改时间:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
|
@Override
|
||||||
|
public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
||||||
|
if (StrUtil.isBlank(clubId) || year == null || year < 1 || year > 9998) {
|
||||||
|
throw new IllegalArgumentException("请选择有效社团和统计年度");
|
||||||
|
}
|
||||||
|
MemberChangeStatistics stats = new MemberChangeStatistics(year);
|
||||||
|
// 年初基数来自当年名单,不受缴费、拨付或当前退会状态影响;同一会员按 userId 去重。
|
||||||
|
List<NutMap> roster = listMap(Sqls.create("SELECT userId, joinTime, personType, retireDate, operateLogs FROM club_pay_record WHERE clubId = @clubId AND year = @year")
|
||||||
|
.setParam("clubId", clubId).setParam("year", year));
|
||||||
|
if (roster.isEmpty()) {
|
||||||
|
stats.invalidate(year + "年度该社团没有缴费名单,请检查年度名单生成计划任务及名单数据,不能将缺失名单视为年初零人。");
|
||||||
|
return stats.result();
|
||||||
|
}
|
||||||
|
boolean generated = roster.stream().anyMatch(row -> {
|
||||||
|
String logs = row.getString("operateLogs");
|
||||||
|
return logs != null && logs.contains("缴费管理-定时任务自动设置");
|
||||||
|
});
|
||||||
|
if (!generated) {
|
||||||
|
stats.warnings.add("未发现该社团年度名单的定时生成记录,请检查计划任务及名单完整性;手工入会记录不能证明年度名单已完整生成。");
|
||||||
|
}
|
||||||
|
Map<String, NutMap> snapshots = new java.util.LinkedHashMap<>();
|
||||||
|
Map<String, AnnualMemberPeriod> periods = new java.util.LinkedHashMap<>();
|
||||||
|
for (NutMap row : roster) {
|
||||||
|
String userId = row.getString("userId");
|
||||||
|
if (StrUtil.isBlank(userId) || row.getTime("joinTime") == null) {
|
||||||
|
stats.invalidate("缴费名单存在用户或加入时间缺失,请补齐后核查统计。");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NutMap prior = snapshots.putIfAbsent(userId, row);
|
||||||
|
if (prior != null) {
|
||||||
|
stats.warnings.add("缴费名单存在同一会员重复记录,已按用户去重,请核查。");
|
||||||
|
if (!Objects.equals(prior.getTime("joinTime"), row.getTime("joinTime"))) {
|
||||||
|
stats.invalidate("重复缴费记录的加入时间不一致,无法确定年初基数,请核查。");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AnnualMemberPeriod period = addAnnualPeriod(periods, userId, row.getTime("joinTime"), stats);
|
||||||
|
if (period != null) period.rosterMember = true;
|
||||||
|
}
|
||||||
|
// 只接纳已完成 WF 的 mode=0/1;mode=2 是已生效的系统退会,不需要 WF 实例。
|
||||||
|
// mode 显式转为整数,避免 MySQL tinyint(1) 被驱动返回布尔值后将系统退会误判为入会。
|
||||||
|
List<NutMap> applications = listMap(Sqls.create("""
|
||||||
|
SELECT a.userId, a.id, CAST(a.mode AS SIGNED) AS mode, a.joinTime, a.exitTime
|
||||||
|
FROM club_user_apply a
|
||||||
|
WHERE a.clubId = @clubId AND COALESCE(a.delFlag, 0) = 0
|
||||||
|
AND (a.mode = 2 OR (a.mode IN (0, 1) AND EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = a.id AND ins.state = 20)))
|
||||||
|
""").setParam("clubId", clubId));
|
||||||
|
for (NutMap application : applications) {
|
||||||
|
Date joinTime = application.getTime("joinTime");
|
||||||
|
Date exitTime = application.getTime("exitTime");
|
||||||
|
int mode = application.getInt("mode");
|
||||||
|
if (mode == 1 && joinTime == null) {
|
||||||
|
stats.invalidate("已完成入会申请缺少 joinTime,请核查会长审核时间。");
|
||||||
|
} else if (mode != 1 && exitTime == null) {
|
||||||
|
stats.invalidate("已生效退会记录缺少 exitTime,请核查退会数据。");
|
||||||
|
}
|
||||||
|
// 年初只使用缴费基数,旧入会申请不能另建一个年初周期;同年退会资料补充手工入会。
|
||||||
|
if (stats.inYear(joinTime) && (mode == 1 || stats.inYear(exitTime))) {
|
||||||
|
// 退会记录中的入会时间也能还原已被物理删除的手工入会周期。
|
||||||
|
addAnnualPeriod(periods, application.getString("userId"), joinTime, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<NutMap> members = listMap(Sqls.create("SELECT userId, joinTime FROM club_user WHERE clubId = @clubId").setParam("clubId", clubId));
|
||||||
|
for (NutMap member : members) {
|
||||||
|
if (member.getTime("joinTime") == null) {
|
||||||
|
if (!snapshots.containsKey(member.getString("userId"))) stats.invalidate("当前会员存在加入时间缺失,无法确定年度增加,请核查。");
|
||||||
|
} else {
|
||||||
|
// 手工录入、导入和成立社团时加入的成员没有 mode=1,也需要计入可靠的年度增加。
|
||||||
|
addAnnualPeriod(periods, member.getString("userId"), member.getTime("joinTime"), stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (NutMap application : applications) {
|
||||||
|
if (application.getInt("mode") == 1 || application.getTime("exitTime") == null) continue;
|
||||||
|
Date exitTime = application.getTime("exitTime");
|
||||||
|
if (!exitTime.before(stats.end)) continue;
|
||||||
|
String userId = application.getString("userId");
|
||||||
|
Date joinTime = application.getTime("joinTime");
|
||||||
|
AnnualMemberPeriod target = joinTime == null ? null : periods.get(periodKey(userId, joinTime));
|
||||||
|
// 旧退会申请可能仍保存迁移前的入会日期,只关闭当时唯一有效的最近周期。
|
||||||
|
if (target == null && (joinTime == null || joinTime.before(stats.start))) {
|
||||||
|
target = periods.values().stream().filter(period -> Objects.equals(userId, period.userId)
|
||||||
|
&& !period.joinTime.after(exitTime) && (period.exitTime == null || period.exitTime.equals(exitTime)))
|
||||||
|
.max(Comparator.comparing(period -> period.joinTime)).orElse(null);
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
if (!exitTime.before(stats.start)) stats.invalidate("年度退会记录无法匹配有效入会周期,请核查加入时间。");
|
||||||
|
} else if (exitTime.before(target.joinTime)) {
|
||||||
|
stats.invalidate("退会时间早于入会时间,请核查申请记录。");
|
||||||
|
} else if (target.exitTime == null || exitTime.before(target.exitTime)) {
|
||||||
|
target.exitTime = exitTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<String> userIds = periods.values().stream().map(period -> period.userId).collect(Collectors.toSet());
|
||||||
|
Map<String, NutMap> profiles = new java.util.HashMap<>();
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
// 人员快照可能早于退休资料补录,批量读取当前人员类型及退休日期,不读取 userState。
|
||||||
|
Sql profileSql = Sqls.create("SELECT id, personType, retireDate FROM sys_user $condition");
|
||||||
|
profileSql.setCondition(Cnd.where("id", "in", userIds));
|
||||||
|
for (NutMap profile : listMap(profileSql)) profiles.put(profile.getString("id"), profile);
|
||||||
|
}
|
||||||
|
Map<String, Date> previousEnd = new java.util.HashMap<>();
|
||||||
|
Set<String> retiredTransfers = new HashSet<>();
|
||||||
|
List<AnnualMemberPeriod> ordered = new ArrayList<>(periods.values());
|
||||||
|
ordered.sort(Comparator.comparing((AnnualMemberPeriod period) -> period.userId).thenComparing(period -> period.joinTime));
|
||||||
|
for (AnnualMemberPeriod period : ordered) {
|
||||||
|
// 同一用户不能同时占有两个未结束的周期;异常不强行合并,提示核查。
|
||||||
|
if (previousEnd.containsKey(period.userId)) {
|
||||||
|
Date priorEnd = previousEnd.get(period.userId);
|
||||||
|
if (priorEnd == null || priorEnd.after(period.joinTime)) stats.invalidate("同一会员的入会周期重叠,请核查重复入会或缺失的退会记录。");
|
||||||
|
}
|
||||||
|
previousEnd.put(period.userId, period.exitTime);
|
||||||
|
NutMap snapshot = snapshots.get(period.userId);
|
||||||
|
NutMap profile = profiles.get(period.userId);
|
||||||
|
String snapshotType = snapshot == null ? null : snapshot.getString("personType");
|
||||||
|
String currentType = profile == null ? snapshotType : profile.getString("personType");
|
||||||
|
if (StrUtil.isBlank(currentType)) currentType = snapshotType;
|
||||||
|
Date retireDate = profile == null ? null : profile.getTime("retireDate");
|
||||||
|
if (retireDate == null && snapshot != null) retireDate = snapshot.getTime("retireDate");
|
||||||
|
boolean atStart = period.joinTime.before(stats.start) && (period.exitTime == null || !period.exitTime.before(stats.start));
|
||||||
|
if (atStart && period.rosterMember) {
|
||||||
|
stats.increase(classifyPersonType(snapshotType, retireDate, stats.start, true, stats), "yearFirstNum");
|
||||||
|
} else if (atStart) {
|
||||||
|
stats.invalidate("存在年初已入会但不在年度缴费基数中的会员,请检查名单完整性。");
|
||||||
|
}
|
||||||
|
if (stats.inYear(period.joinTime)) {
|
||||||
|
stats.increase(classifyPersonType(currentType, retireDate, period.joinTime, false, stats), "yearAddNum");
|
||||||
|
}
|
||||||
|
if (stats.inYear(period.exitTime)) {
|
||||||
|
stats.increase(classifyPersonType(currentType, retireDate, period.exitTime, false, stats), "yearReduceNum");
|
||||||
|
}
|
||||||
|
// 同日同刻采用先退休再退会;入会时已退休只算退休入会,不额外算一次转类。
|
||||||
|
if (stats.inYear(retireDate) && period.joinTime.before(retireDate)
|
||||||
|
&& (period.exitTime == null || !period.exitTime.before(retireDate))
|
||||||
|
&& recognizedPersonType(currentType) && retiredTransfers.add(period.userId)) {
|
||||||
|
stats.increase("在职人员", "yearReduceNum");
|
||||||
|
stats.increase("离退休人员", "yearAddNum");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats.result();
|
||||||
|
}
|
||||||
|
|
||||||
for (MembershipPeriod period : membershipPeriods) {
|
/** 合并同一用户、同一加入时间的多来源记录;年度末边界不属于本年。 */
|
||||||
// 非正常历史会员没有退会时间且没有已完成退会申请时,无法确定所属年度,不计入统计。
|
private AnnualMemberPeriod addAnnualPeriod(Map<String, AnnualMemberPeriod> periods, String userId, Date joinTime, MemberChangeStatistics stats) {
|
||||||
if (!period.normal && period.exitTime == null) {
|
if (StrUtil.isBlank(userId)) {
|
||||||
continue;
|
stats.invalidate("会员记录缺少用户ID,请核查。");
|
||||||
}
|
return null;
|
||||||
if (isMemberAtYearBoundary(period, yearStart)) {
|
}
|
||||||
increaseResult(classifyAtBoundary(period, yearStart), activeResult, retiredResult, "yearFirstNum");
|
if (joinTime == null || !joinTime.before(stats.end)) return null;
|
||||||
}
|
return periods.computeIfAbsent(periodKey(userId, joinTime), key -> new AnnualMemberPeriod(userId, joinTime));
|
||||||
if (isInYear(period.joinTime, yearStart, nextYearStart)) {
|
}
|
||||||
increaseResult(classifyAtEvent(period, period.joinTime), activeResult, retiredResult, "yearAddNum");
|
|
||||||
}
|
|
||||||
if (isInYear(period.exitTime, yearStart, nextYearStart)) {
|
|
||||||
increaseResult(classifyBeforeEvent(period, period.exitTime), activeResult, retiredResult, "yearReduceNum");
|
|
||||||
}
|
|
||||||
// 退休发生在会员有效期内时,作为在职减少一人、退休增加一人处理。
|
|
||||||
if (isInYear(period.retireDate, yearStart, nextYearStart)
|
|
||||||
&& period.joinTime.before(period.retireDate)
|
|
||||||
&& (period.exitTime == null || period.exitTime.after(period.retireDate))) {
|
|
||||||
increaseResult("在职", activeResult, retiredResult, "yearReduceNum");
|
|
||||||
increaseResult("退休", activeResult, retiredResult, "yearAddNum");
|
|
||||||
}
|
|
||||||
if (isMemberAtYearBoundary(period, nextYearStart)) {
|
|
||||||
increaseResult(classifyAtBoundary(period, nextYearStart), activeResult, retiredResult, "thisYearNum");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of(activeResult, retiredResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private String periodKey(String userId, Date joinTime) {
|
||||||
* 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。
|
return userId + ":" + joinTime.getTime();
|
||||||
* 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。
|
}
|
||||||
*/
|
|
||||||
private List<MembershipPeriod> getMembershipPeriods(String clubId) {
|
|
||||||
Sql legacySql = Sqls.create("""
|
|
||||||
SELECT
|
|
||||||
cu.userId,
|
|
||||||
CAST(cu.joinTime AS DATETIME) joinTime,
|
|
||||||
STR_TO_DATE(NULLIF(cu.changeTime, ''), '%Y-%m-%d %H:%i:%s') exitTime,
|
|
||||||
cu.isNormal,
|
|
||||||
u.userState,
|
|
||||||
u.retireDate
|
|
||||||
FROM sys_club_user cu
|
|
||||||
LEFT JOIN sys_user u ON u.id = cu.userId
|
|
||||||
WHERE cu.clubId = @clubId
|
|
||||||
AND cu.status = 5
|
|
||||||
AND cu.joinTime IS NOT NULL
|
|
||||||
AND cu.joinTime <> ''
|
|
||||||
""").setParam("clubId", clubId);
|
|
||||||
List<MembershipPeriod> periods = listMap(legacySql).stream()
|
|
||||||
.map(row -> new MembershipPeriod(
|
|
||||||
row.getString("userId"),
|
|
||||||
row.getTime("joinTime"),
|
|
||||||
row.getTime("exitTime"),
|
|
||||||
!Boolean.FALSE.equals(row.getBoolean("isNormal")),
|
|
||||||
true,
|
|
||||||
row.getString("userState"),
|
|
||||||
row.getTime("retireDate")
|
|
||||||
))
|
|
||||||
.filter(period -> period.joinTime != null)
|
|
||||||
.collect(Collectors.toCollection(ArrayList::new));
|
|
||||||
|
|
||||||
Sql applySql = Sqls.create("""
|
/** 只接纳指定的四种人员类型,其他已知类型不纳入这两类统计。 */
|
||||||
SELECT
|
private boolean recognizedPersonType(String personType) {
|
||||||
cu.userId,
|
return List.of("教职工", "劳务派遣人员", "劳动服务公司", "离退休人员").contains(personType == null ? "" : personType);
|
||||||
cu.id,
|
}
|
||||||
cu.mode,
|
|
||||||
cu.applyDate,
|
|
||||||
cu.joinTime,
|
|
||||||
cu.exitTime,
|
|
||||||
u.userState,
|
|
||||||
u.retireDate
|
|
||||||
FROM club_user_apply cu
|
|
||||||
LEFT JOIN sys_user u ON u.id = cu.userId
|
|
||||||
WHERE cu.clubId = @clubId
|
|
||||||
AND COALESCE(cu.delFlag, 0) = 0
|
|
||||||
ORDER BY cu.applyDate, cu.mode
|
|
||||||
""")
|
|
||||||
.setParam("clubId", clubId);
|
|
||||||
List<NutMap> applications = listMap(applySql);
|
|
||||||
if (!applications.isEmpty()) {
|
|
||||||
// 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。
|
|
||||||
List<String> applicationIds = applications.stream()
|
|
||||||
.map(application -> application.getString("id"))
|
|
||||||
.filter(StrUtil::isNotBlank)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
List<ProcessInstance> finishedInstances = applicationIds.isEmpty() ? new ArrayList<>()
|
|
||||||
: dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds)
|
|
||||||
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode()));
|
|
||||||
Set<String> finishedBusinessNos = finishedInstances.stream()
|
|
||||||
.map(ProcessInstance::getBusinessNo)
|
|
||||||
.filter(StrUtil::isNotBlank)
|
|
||||||
.collect(Collectors.toCollection(HashSet::new));
|
|
||||||
applications.removeIf(application -> !finishedBusinessNos.contains(application.getString("id")));
|
|
||||||
}
|
|
||||||
applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo)));
|
|
||||||
|
|
||||||
for (NutMap application : applications) {
|
/** 年初边界当天退休仍属于年初在职基数,随后转类;事件时刻已退休则归入离退休。 */
|
||||||
Date applyDate = application.getTime("applyDate");
|
private String classifyPersonType(String personType, Date retireDate, Date moment, boolean boundary, MemberChangeStatistics stats) {
|
||||||
Boolean mode = application.getBoolean("mode");
|
if (StrUtil.isBlank(personType)) {
|
||||||
if (applyDate == null || mode == null) {
|
stats.invalidate("人员类型为空,不能按在职状态替代分类,请核查人员资料。");
|
||||||
continue;
|
return null;
|
||||||
}
|
}
|
||||||
if (mode) {
|
if (!recognizedPersonType(personType)) {
|
||||||
mergeJoinApplication(periods, application, applyDate);
|
stats.warnings.add("存在不属于指定两类的人员类型,已排除,请核查统计范围。");
|
||||||
} else {
|
return null;
|
||||||
mergeExitApplication(periods, application, applyDate);
|
}
|
||||||
}
|
if (retireDate != null) {
|
||||||
}
|
boolean retired = boundary ? retireDate.before(moment) : !retireDate.after(moment);
|
||||||
return periods;
|
return retired ? "离退休人员" : "在职人员";
|
||||||
}
|
}
|
||||||
|
return "离退休人员".equals(personType) ? "离退休人员" : "在职人员";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private static class AnnualMemberPeriod {
|
||||||
* 已迁移的入会申请与旧会员记录精确对应时不重复新增;迁移后的新申请以审核申请时间作为入会时间。
|
final String userId;
|
||||||
*/
|
final Date joinTime;
|
||||||
private void mergeJoinApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
Date exitTime;
|
||||||
String userId = application.getString("userId");
|
boolean rosterMember;
|
||||||
Date applicationJoinTime = application.getTime("joinTime");
|
|
||||||
boolean representedByLegacy = periods.stream()
|
|
||||||
.filter(period -> period.legacy && Objects.equals(period.userId, userId))
|
|
||||||
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
|
||||||
.anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
|
|
||||||
boolean representedByNewPeriod = periods.stream()
|
|
||||||
.filter(period -> !period.legacy && Objects.equals(period.userId, userId))
|
|
||||||
.anyMatch(period -> isMemberAt(period, applyDate));
|
|
||||||
if (representedByLegacy || representedByNewPeriod) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
periods.add(new MembershipPeriod(
|
|
||||||
userId,
|
|
||||||
applyDate,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
application.getString("userState"),
|
|
||||||
application.getTime("retireDate")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
AnnualMemberPeriod(String userId, Date joinTime) {
|
||||||
* 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
|
this.userId = userId;
|
||||||
*/
|
this.joinTime = joinTime;
|
||||||
private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
}
|
||||||
String userId = application.getString("userId");
|
}
|
||||||
Date applicationJoinTime = application.getTime("joinTime");
|
|
||||||
Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
|
|
||||||
MembershipPeriod target = periods.stream()
|
|
||||||
.filter(period -> Objects.equals(period.userId, userId))
|
|
||||||
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
|
||||||
.filter(period -> period.exitTime == null)
|
|
||||||
.max(Comparator.comparing(period -> period.joinTime))
|
|
||||||
.orElse(null);
|
|
||||||
if (target == null) {
|
|
||||||
target = periods.stream()
|
|
||||||
.filter(period -> Objects.equals(period.userId, userId))
|
|
||||||
.filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
|
|
||||||
.max(Comparator.comparing(period -> period.joinTime))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
if (target != null) {
|
|
||||||
target.exitTime = exitTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NutMap createUserChangeResult(String userState) {
|
/** 返回两类计数及核查信息;userState 仅为已有查看页的显示字段别名,不作为分类数据来源。 */
|
||||||
return NutMap.NEW()
|
private static class MemberChangeStatistics {
|
||||||
.addv("userState", userState)
|
final Date start;
|
||||||
.addv("yearFirstNum", 0)
|
final Date end;
|
||||||
.addv("yearAddNum", 0)
|
final Set<String> warnings = new java.util.LinkedHashSet<>();
|
||||||
.addv("yearReduceNum", 0)
|
final List<NutMap> rows = List.of(row("在职人员"), row("离退休人员"));
|
||||||
.addv("thisYearNum", 0);
|
boolean valid = true;
|
||||||
}
|
|
||||||
|
|
||||||
private void increaseResult(String userState, NutMap activeResult, NutMap retiredResult, String field) {
|
MemberChangeStatistics(int year) {
|
||||||
NutMap result = "退休".equals(userState) ? retiredResult : "在职".equals(userState) ? activeResult : null;
|
start = DateUtil.parse(year + "-01-01 00:00:00");
|
||||||
if (result != null) {
|
end = DateUtil.parse((year + 1) + "-01-01 00:00:00");
|
||||||
result.put(field, result.getInt(field) + 1);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInYear(Date date, Date yearStart, Date nextYearStart) {
|
static NutMap row(String type) {
|
||||||
return date != null && !date.before(yearStart) && date.before(nextYearStart);
|
return NutMap.NEW().addv("personType", type).addv("userState", type)
|
||||||
}
|
.addv("yearFirstNum", 0).addv("yearAddNum", 0).addv("yearReduceNum", 0).addv("thisYearNum", 0);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isMemberAt(MembershipPeriod period, Date date) {
|
boolean inYear(Date time) {
|
||||||
return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date));
|
return time != null && !time.before(start) && time.before(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) {
|
void invalidate(String message) {
|
||||||
return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary));
|
valid = false;
|
||||||
}
|
warnings.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
private String classifyAtBoundary(MembershipPeriod period, Date boundary) {
|
void increase(String type, String field) {
|
||||||
if (period.retireDate != null) {
|
for (NutMap row : rows) if (row.getString("personType").equals(type)) row.put(field, row.getInt(field) + 1);
|
||||||
return period.retireDate.before(boundary) ? "退休" : "在职";
|
}
|
||||||
}
|
|
||||||
return normalizeUserState(period.userState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String classifyAtEvent(MembershipPeriod period, Date eventTime) {
|
List<NutMap> result() {
|
||||||
if (period.retireDate != null) {
|
for (NutMap row : rows) {
|
||||||
return period.retireDate.after(eventTime) ? "在职" : "退休";
|
int yearEnd = row.getInt("yearFirstNum") + row.getInt("yearAddNum") - row.getInt("yearReduceNum");
|
||||||
}
|
row.put("thisYearNum", yearEnd);
|
||||||
return normalizeUserState(period.userState);
|
if (yearEnd < 0) invalidate("年末人数出现负数,请核查年初名单与入退会记录。");
|
||||||
}
|
}
|
||||||
|
for (NutMap row : rows) {
|
||||||
private String classifyBeforeEvent(MembershipPeriod period, Date eventTime) {
|
row.put("statisticsValid", valid);
|
||||||
if (period.retireDate != null) {
|
row.put("statisticsWarnings", new ArrayList<>(warnings));
|
||||||
return period.retireDate.before(eventTime) ? "退休" : "在职";
|
// 无法确定基数或周期时展示空值,避免用户将部分计数当成完整统计提交。
|
||||||
}
|
if (!valid) for (String field : List.of("yearFirstNum", "yearAddNum", "yearReduceNum", "thisYearNum")) row.put(field, null);
|
||||||
return normalizeUserState(period.userState);
|
}
|
||||||
}
|
return rows;
|
||||||
|
}
|
||||||
private String normalizeUserState(String userState) {
|
}
|
||||||
// 历史成员数据可能未同步人员状态,空值不参与在职、退休人数统计。
|
|
||||||
if (StrUtil.isBlank(userState)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (List.of("在职", "在岗").contains(userState)) {
|
|
||||||
return "在职";
|
|
||||||
}
|
|
||||||
if (List.of("退休", "退休【变号】").contains(userState)) {
|
|
||||||
return "退休";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class MembershipPeriod {
|
|
||||||
private final String userId;
|
|
||||||
private final Date joinTime;
|
|
||||||
private Date exitTime;
|
|
||||||
private final boolean normal;
|
|
||||||
private final boolean legacy;
|
|
||||||
private final String userState;
|
|
||||||
private final Date retireDate;
|
|
||||||
|
|
||||||
private MembershipPeriod(String userId, Date joinTime, Date exitTime, boolean normal, boolean legacy,
|
|
||||||
String userState, Date retireDate) {
|
|
||||||
this.userId = userId;
|
|
||||||
this.joinTime = joinTime;
|
|
||||||
this.exitTime = exitTime;
|
|
||||||
this.normal = normal;
|
|
||||||
this.legacy = legacy;
|
|
||||||
this.userState = userState;
|
|
||||||
this.retireDate = retireDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NutMap> getJgUser(String clubId) {
|
public List<NutMap> getJgUser(String clubId) {
|
||||||
|
|||||||
+281
-89
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.club.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
@@ -9,6 +10,14 @@ import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
|||||||
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
|
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
|
||||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import com.budwk.app.sys.models.Sys_user_role;
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
@@ -39,12 +48,17 @@ import org.nutz.lang.Strings;
|
|||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -64,12 +78,74 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
private SysClubService sysClubService;
|
private SysClubService sysClubService;
|
||||||
@Inject
|
@Inject
|
||||||
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
||||||
|
@Inject
|
||||||
|
private ClubUserJoinService clubUserJoinService;
|
||||||
|
@Inject
|
||||||
|
private ClubPayRecordService payRecordService;
|
||||||
|
@Inject
|
||||||
|
private SysUserService sysUserService;
|
||||||
|
@Inject
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
|
|
||||||
public SysClubInfoManageServiceImpl(Dao dao) {
|
public SysClubInfoManageServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接退会不启动 WF;系统处理记录、会员关系删除和角色清理必须同时成功。
|
||||||
|
* @param membershipId 当前 club_user 会员关系ID
|
||||||
|
* @return void;重复退会或会员不存在时抛出异常,避免生成重复记录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void exitClub(String membershipId) {
|
||||||
|
ClubUser member = dao().fetch(ClubUser.class, membershipId);
|
||||||
|
if (member == null) {
|
||||||
|
throw new IllegalArgumentException("该会员已退会或会员关系不存在");
|
||||||
|
}
|
||||||
|
ClubUserApply source = clubUserJoinService.buildExitApply(member.getClubId(), member.getUserId());
|
||||||
|
if (source == null) {
|
||||||
|
throw new IllegalArgumentException("无法获取该会员的退会信息");
|
||||||
|
}
|
||||||
|
// 使用新对象,只复制业务资料,防止沿用历史申请ID、创建人和创建时间。
|
||||||
|
ClubUserApply application = new ClubUserApply();
|
||||||
|
application.setClubId(member.getClubId());
|
||||||
|
application.setUserId(member.getUserId());
|
||||||
|
application.setRoleCode(source.getRoleCode());
|
||||||
|
application.setClubPosition(source.getClubPosition());
|
||||||
|
application.setEmail(source.getEmail());
|
||||||
|
application.setMobile(source.getMobile());
|
||||||
|
application.setBirthday(source.getBirthday());
|
||||||
|
application.setAvatar(source.getAvatar());
|
||||||
|
application.setSameTimeJoinOtherClubSituation(source.getSameTimeJoinOtherClubSituation());
|
||||||
|
application.setAwardsExperience(source.getAwardsExperience());
|
||||||
|
application.setSignature(source.getSignature());
|
||||||
|
application.setJoinTime(source.getJoinTime());
|
||||||
|
Date exitTime = new Date();
|
||||||
|
application.setMode(2);
|
||||||
|
application.setApplyDate(exitTime);
|
||||||
|
application.setExitTime(exitTime);
|
||||||
|
// 条件删除只能成功一次;并发重复操作不再新增退会记录,delFlag 沿用默认值。
|
||||||
|
if (dao().delete(ClubUser.class, membershipId) != 1) {
|
||||||
|
throw new IllegalArgumentException("该会员已退会,请刷新列表");
|
||||||
|
}
|
||||||
|
dao().insert(application);
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:系统直接退会与取消拨付处于同一事务,避免退会后仍保存可拨付。
|
||||||
|
payRecordService.disableExitMemberPayment(member.getClubId(), member.getUserId());
|
||||||
|
if (member.getRoleCode() != null) {
|
||||||
|
for (String roleCode : member.getRoleCode()) {
|
||||||
|
Sys_role role = sysRoleService.getByCode(roleCode);
|
||||||
|
if (role != null) {
|
||||||
|
dao().clear(Sys_user_role.class, Cnd.where("userId", "=", member.getUserId())
|
||||||
|
.and("roleId", "=", role.getId()).and("clubId", "=", member.getClubId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sysUserService.clearCache();
|
||||||
|
sysRoleService.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubCommonPageVo> infoManagePageData(ClubUserPageForm pageForm) {
|
public Pagination<ClubCommonPageVo> infoManagePageData(ClubUserPageForm pageForm) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
@@ -129,102 +205,188 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 12:01:28;操作人:wxg:当前会员以 club_user 分页,查询期间不补齐或修改数据库。
|
||||||
|
* @param pageForm clubId 为选中社团ID;radioType=4 查历史成员,其他值查当前会员;
|
||||||
|
* searchKeyword 匹配会员表姓名/工号,personType/userState 筛选当前用户资料;
|
||||||
|
* focusMembers=true 同时筛选当前人员类型为自聘人员或其他的会员,false/空值不限制;
|
||||||
|
* joinDateStart/joinDateEnd 为 yyyy-MM-dd,pageNumber/pageSize 为分页参数,排序只允许列表字段
|
||||||
|
* @return 会员分页;姓名、工号、电话、joinTime 来自会员表,其余人员资料只补充本页;历史页沿用申请记录
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubUserCommonPageVo> infoManageUserPageData(ClubUserPageForm pageForm) {
|
public Pagination<ClubUserCommonPageVo> infoManageUserPageData(ClubUserPageForm pageForm) {
|
||||||
|
if (Integer.valueOf(4).equals(pageForm.getRadioType())) {
|
||||||
|
return historyMemberPageData(pageForm);
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(pageForm.getClubId())) {
|
||||||
|
throw new IllegalArgumentException("请选择社团后查询会员信息");
|
||||||
|
}
|
||||||
|
// 姓名、工号、电话及入会时间使用持久化字段;列排序需要其他资料时才关联用户表。
|
||||||
|
Map<String, String> sortColumns = Map.of(
|
||||||
|
"loginName", "scu.loginName", "userName", "scu.userName", "mobile", "scu.mobile",
|
||||||
|
"joinTime", "scu.joinTime", "sex", "u.sex", "personType", "u.personType",
|
||||||
|
"userAttribute", "u.userAttribute", "userState", "u.userState", "unitName", "u.unitName");
|
||||||
|
String orderColumn = StrUtil.isBlank(pageForm.getPageOrderName()) ? null : sortColumns.get(pageForm.getPageOrderName());
|
||||||
|
boolean orderRequested = orderColumn != null && StrUtil.isNotBlank(pageForm.getPageOrderBy());
|
||||||
|
boolean needsUnitSort = orderRequested && "unitName".equals(pageForm.getPageOrderName());
|
||||||
|
boolean needsUser = Boolean.TRUE.equals(pageForm.getFocusMembers())
|
||||||
|
|| StrUtil.isNotBlank(pageForm.getPersonType()) || StrUtil.isNotBlank(pageForm.getUserState())
|
||||||
|
|| (orderRequested && orderColumn.startsWith("u."));
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT scu.*
|
||||||
scu.*,
|
FROM club_user scu
|
||||||
u.username as userName,
|
$userJoin
|
||||||
u.loginname as loginName,
|
|
||||||
u.sex,
|
|
||||||
u.personType as personType,
|
|
||||||
u.userState,
|
|
||||||
u.unitid as unitId,
|
|
||||||
u.mobile,
|
|
||||||
club.clubName,
|
|
||||||
u.unitname as unitName,
|
|
||||||
DATE_FORMAT(IFNULL(joinInfo.joinTime, club.foundTime), '%Y-%m-%d %H:%i:%s') as joinTime
|
|
||||||
FROM
|
|
||||||
club_user scu
|
|
||||||
LEFT JOIN sys_club club ON scu.clubId = club.id
|
|
||||||
RIGHT JOIN `vw_user` u ON scu.userId = u.id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
joinApply.userId,
|
|
||||||
joinApply.clubId,
|
|
||||||
MAX(joinApply.joinTime) AS joinTime
|
|
||||||
FROM
|
|
||||||
club_user_apply joinApply
|
|
||||||
LEFT JOIN wf_process_instance joinIns ON joinIns.businessNo = joinApply.id
|
|
||||||
WHERE
|
|
||||||
joinApply.mode = true
|
|
||||||
AND joinIns.state = 20
|
|
||||||
GROUP BY
|
|
||||||
joinApply.userId,
|
|
||||||
joinApply.clubId
|
|
||||||
) joinInfo ON joinInfo.userId = scu.userId AND joinInfo.clubId = scu.clubId
|
|
||||||
$condition
|
$condition
|
||||||
$order
|
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
// 默认查询和姓名/工号/日期筛选不关联 vw_user;单位排序保留视图的单位名称口径。
|
||||||
|
sql.setVar("userJoin", needsUnitSort ? "LEFT JOIN vw_user u ON u.id = scu.userId"
|
||||||
cnd.and("scu.clubId", "=", pageForm.getClubId());
|
: needsUser ? "LEFT JOIN sys_user u ON u.id = scu.userId" : "");
|
||||||
if (StrUtil.isNotBlank(pageForm.getPersonType())) {
|
Cnd cnd = Cnd.where("scu.clubId", "=", pageForm.getClubId());
|
||||||
cnd.and("u.personType", "=", pageForm.getPersonType());
|
// 在分页和总数统计前同时筛选两类人员,保持当前社团及其他查询条件的限制。
|
||||||
|
if (Boolean.TRUE.equals(pageForm.getFocusMembers())) {
|
||||||
|
cnd.and("u.personType", "in", List.of("自聘人员", "其他"));
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(pageForm.getUserState())) {
|
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
||||||
cnd.and("u.userState", "=", pageForm.getUserState());
|
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
|
cnd.and(Cnd.exps("scu.loginName", "like", "%" + pageForm.getSearchKeyword() + "%")
|
||||||
|
.or("scu.userName", "like", "%" + pageForm.getSearchKeyword() + "%"));
|
||||||
}
|
}
|
||||||
if (Strings.isNotBlank(pageForm.getSearchKeyword())) {
|
// 结束日期使用次日零点上界,包含结束当天;只读取已保存的时间,不触发 WF 补齐 SQL。
|
||||||
cnd.and(Cnd.exps("loginname", "like", "%" + pageForm.getSearchKeyword() + "%").or("username", "like", "%" + pageForm.getSearchKeyword() + "%"));
|
if (StrUtil.isNotBlank(pageForm.getJoinDateStart())) {
|
||||||
|
cnd.and("scu.joinTime", ">=", LocalDate.parse(pageForm.getJoinDateStart()).toString());
|
||||||
}
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getJoinDateEnd())) {
|
||||||
if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
cnd.and("scu.joinTime", "<", LocalDate.parse(pageForm.getJoinDateEnd()).plusDays(1).toString());
|
||||||
cnd.orderBy(pageForm.getPageOrderName(), "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
}
|
||||||
|
if (orderRequested) {
|
||||||
|
cnd.orderBy(orderColumn, "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
} else {
|
} else {
|
||||||
sql.setVar("order", """
|
cnd.desc("scu.joinTime");
|
||||||
ORDER BY
|
}
|
||||||
CASE
|
cnd.asc("scu.id");
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_PRESIDENT"') THEN 1
|
if (Integer.valueOf(1).equals(pageForm.getRadioType())) {
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_VICE_PRESIDENT"') THEN 2
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_SECRETARY"') THEN 3
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_VICE_SECRETARY"') THEN 4
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_OPERATOR"') THEN 5
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_MEMBER"') THEN 6
|
|
||||||
ELSE 99
|
|
||||||
END
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
//查询理事机构
|
|
||||||
if (pageForm.getRadioType() != null && 1 == pageForm.getRadioType()) {
|
|
||||||
cnd.and(new Static("NOT JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
cnd.and(new Static("NOT JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
||||||
}
|
}
|
||||||
//查询社团成员
|
if (Integer.valueOf(2).equals(pageForm.getRadioType())) {
|
||||||
if (pageForm.getRadioType() != null && 2 == pageForm.getRadioType()) {
|
|
||||||
cnd.and(new Static("JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
cnd.and(new Static("JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
||||||
//cnd.and(new Static("JSON_LENGTH(scu.roleCode) = 1"));
|
|
||||||
}
|
}
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
Pagination<ClubUserCommonPageVo> listPageVO = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
Pagination<ClubUserCommonPageVo> page = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
||||||
List<ClubUserCommonPageVo> listMap = listPageVO.getList(ClubUserCommonPageVo.class);
|
List<ClubUserCommonPageVo> members = page.getList(ClubUserCommonPageVo.class);
|
||||||
for (ClubUserCommonPageVo vo : listMap) {
|
fillMemberPageDetails(members, pageForm.getClubId());
|
||||||
List<String> list = vo.getRoleCode();
|
for (ClubUserCommonPageVo member : members) {
|
||||||
List<String> cleanedCodes = list.stream()
|
List<String> roleCodes = member.getRoleCode() == null ? List.of() : member.getRoleCode();
|
||||||
.filter(Objects::nonNull)
|
List<String> cleanedCodes = roleCodes.stream().filter(Objects::nonNull).map(String::strip)
|
||||||
.map(String::strip)
|
|
||||||
.map(code -> code.startsWith("\"") && code.endsWith("\"")
|
.map(code -> code.startsWith("\"") && code.endsWith("\"")
|
||||||
? code.substring(1, code.length() - 1)
|
? code.substring(1, code.length() - 1) : code).toList();
|
||||||
: code)
|
member.setRoleName(SysClubUserServiceImpl.convertRoleName(cleanedCodes));
|
||||||
.toList();
|
member.setRoleCode(cleanedCodes);
|
||||||
String roleName = SysClubUserServiceImpl.convertRoleName(cleanedCodes);
|
|
||||||
vo.setRoleName(roleName);
|
|
||||||
vo.setRoleCode(cleanedCodes);
|
|
||||||
}
|
}
|
||||||
listPageVO.setList(listMap);
|
page.setList(members);
|
||||||
return listPageVO;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 12:01:28;操作人:wxg:分页后批量获取本页其他资料,避免把完整用户视图带入默认统计和分页。
|
||||||
|
* @param members 当前页会员关系;空页不查询,姓名/工号/电话/入会时间始终保留会员表值
|
||||||
|
* @param clubId 当前选中社团ID,用于补充社团名称
|
||||||
|
*/
|
||||||
|
private void fillMemberPageDetails(List<ClubUserCommonPageVo> members, String clubId) {
|
||||||
|
if (members.isEmpty()) return;
|
||||||
|
List<String> userIds = members.stream().map(ClubUser::getUserId).filter(Objects::nonNull).distinct().toList();
|
||||||
|
Map<String, NutMap> users = new HashMap<>();
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
Sql details = Sqls.create("""
|
||||||
|
SELECT id AS id, sex AS sex, personType AS personType, userAttribute AS userAttribute,
|
||||||
|
userState AS userState, unitId AS unitId, unitName AS unitName
|
||||||
|
FROM vw_user
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
details.setCondition(Cnd.where("id", "in", userIds));
|
||||||
|
for (NutMap user : listMap(details)) users.put(user.getString("id"), user);
|
||||||
|
}
|
||||||
|
SysClub club = dao().fetch(SysClub.class, clubId);
|
||||||
|
for (ClubUserCommonPageVo member : members) {
|
||||||
|
member.setClubName(club == null ? null : club.getClubName());
|
||||||
|
NutMap user = users.get(member.getUserId());
|
||||||
|
// 用户资料缺失仍展示会员关系及已保存资料,不用当前用户资料覆盖三个持久化字段。
|
||||||
|
if (user == null) continue;
|
||||||
|
member.setSex(user.getString("sex"));
|
||||||
|
member.setPersonType(user.getString("personType"));
|
||||||
|
member.setUserAttribute(user.getString("userAttribute"));
|
||||||
|
member.setUserState(user.getString("userState"));
|
||||||
|
member.setUnitId(user.getString("unitId"));
|
||||||
|
member.setUnitName(user.getString("unitName"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前社团历史退会记录分页;每次退会保留一行,不按人员去重,也不要求关联 WF 已完成流程。
|
||||||
|
* @param pageForm clubId 为当前社团ID;姓名/工号、人员类型、在职状态及 yyyy-MM-dd 入会日期范围为可选筛选;
|
||||||
|
* focusMembers=true 筛选当前人员类型为自聘人员或其他的历史成员,false/空值不限制;
|
||||||
|
* pageNumber/pageSize 为分页参数,pageOrderName/pageOrderBy 为列排序参数
|
||||||
|
* @return 会员展示分页对象;id 为申请ID,joinTime/exitTime 来自申请,人员资料取用户视图当前值
|
||||||
|
*/
|
||||||
|
private Pagination<ClubUserCommonPageVo> historyMemberPageData(ClubUserPageForm pageForm) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
cua.id, cua.clubId, cua.userId, cua.roleCode AS applyRoleCode,
|
||||||
|
u.username AS userName, u.loginname AS loginName, u.sex, u.mobile,
|
||||||
|
u.personType, u.userAttribute, u.userState,
|
||||||
|
u.unitid AS unitId, u.unitname AS unitName,
|
||||||
|
DATE_FORMAT(cua.joinTime, '%Y-%m-%d %H:%i:%s') AS joinTime,
|
||||||
|
DATE_FORMAT(cua.exitTime, '%Y-%m-%d %H:%i:%s') AS exitTime
|
||||||
|
FROM club_user_apply cua
|
||||||
|
LEFT JOIN vw_user u ON u.id = cua.userId
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
Cnd cnd = Cnd.where("cua.clubId", "=", pageForm.getClubId())
|
||||||
|
.and("cua.mode", "in", List.of(0, 2));
|
||||||
|
// 历史页同样按用户当前人员类型筛选,两类人员共同参与分页和总数统计。
|
||||||
|
if (Boolean.TRUE.equals(pageForm.getFocusMembers())) {
|
||||||
|
cnd.and("u.personType", "in", List.of("自聘人员", "其他"));
|
||||||
|
}
|
||||||
|
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
||||||
|
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
|
cnd.and(Cnd.exps("u.loginname", "like", "%" + pageForm.getSearchKeyword() + "%")
|
||||||
|
.or("u.username", "like", "%" + pageForm.getSearchKeyword() + "%"));
|
||||||
|
}
|
||||||
|
// 时间查询保持入会日期语义,结束日期用次日零点上界,包含选择的结束当天。
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getJoinDateStart())) {
|
||||||
|
cnd.and("cua.joinTime", ">=", LocalDate.parse(pageForm.getJoinDateStart()).toString());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getJoinDateEnd())) {
|
||||||
|
cnd.and("cua.joinTime", "<", LocalDate.parse(pageForm.getJoinDateEnd()).plusDays(1).toString());
|
||||||
|
}
|
||||||
|
// 仅允许表格实际展示的列参与排序,退出时间相同则按申请ID保证分页顺序稳定。
|
||||||
|
List<String> sortableFields = List.of("loginName", "userName", "sex", "personType", "userAttribute",
|
||||||
|
"userState", "unitName", "joinTime", "exitTime");
|
||||||
|
if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())
|
||||||
|
&& sortableFields.contains(pageForm.getPageOrderName())) {
|
||||||
|
cnd.orderBy(pageForm.getPageOrderName(), "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
cnd.desc("cua.exitTime");
|
||||||
|
}
|
||||||
|
cnd.asc("cua.id");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
||||||
|
List<ClubUserCommonPageVo> members = pagination.getList(ClubUserCommonPageVo.class);
|
||||||
|
for (ClubUserCommonPageVo member : members) {
|
||||||
|
if (StrUtil.isNotBlank(member.getApplyRoleCode())) {
|
||||||
|
member.setRoleName(SysClubUserServiceImpl.convertRoleName(List.of(member.getApplyRoleCode())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pagination.setList(members);
|
||||||
|
return pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 19:42:45;操作人:wxg:查询审核退会、系统处理与历史直接移出记录。
|
||||||
|
* @param pageForm clubId、姓名/工号、personType、userState 为可选筛选;exitDateStart/exitDateEnd 传 yyyy-MM-dd,结束日包含当天;pageNumber/pageSize 控制分页
|
||||||
|
* @return 分页会员资料,exitType 区分 AUDIT_EXIT、SYSTEM_EXIT、DIRECT_REMOVE,exitTime 为退会时间
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubUserCommonPageVo> exitManagePageData(ClubUserPageForm pageForm) {
|
public Pagination<ClubUserCommonPageVo> exitManagePageData(ClubUserPageForm pageForm) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
@@ -257,6 +419,13 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.andEX("exitInfo.clubId", "=", pageForm.getClubId());
|
cnd.andEX("exitInfo.clubId", "=", pageForm.getClubId());
|
||||||
|
// 2026年10月3日 19:42:45;操作人:wxg:按实际退会时间筛选三类记录,结束日期使用次日零点上界,包含当天。
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getExitDateStart())) {
|
||||||
|
cnd.and("exitInfo.exitTime", ">=", LocalDate.parse(pageForm.getExitDateStart()).toString());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getExitDateEnd())) {
|
||||||
|
cnd.and("exitInfo.exitTime", "<", LocalDate.parse(pageForm.getExitDateEnd()).plusDays(1).toString());
|
||||||
|
}
|
||||||
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
||||||
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
||||||
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
@@ -268,7 +437,9 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
cnd.and("exitInfo.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
|
cnd.and("exitInfo.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
|
||||||
}
|
}
|
||||||
cnd.groupBy("exitInfo.id");
|
cnd.groupBy("exitInfo.id");
|
||||||
cnd.desc("exitTime");
|
// 2026年10月3日 19:42:45;操作人:wxg:最新退会记录优先,时间相同时按ID保证分页稳定;空时间排在最后。
|
||||||
|
cnd.desc("exitInfo.exitTime");
|
||||||
|
cnd.asc("exitInfo.id");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
||||||
List<ClubUserCommonPageVo> list = pagination.getList(ClubUserCommonPageVo.class);
|
List<ClubUserCommonPageVo> list = pagination.getList(ClubUserCommonPageVo.class);
|
||||||
@@ -321,6 +492,8 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
/**
|
/**
|
||||||
* 退会申请审批完成后会删除club_user成员关系,因此退会管理以退会申请记录为主数据源。
|
* 退会申请审批完成后会删除club_user成员关系,因此退会管理以退会申请记录为主数据源。
|
||||||
* 同时保留旧sys_club_user表中已退会的数据,避免历史直接移出记录无法查询。
|
* 同时保留旧sys_club_user表中已退会的数据,避免历史直接移出记录无法查询。
|
||||||
|
* 2026年10月3日 19:42:45;操作人:wxg:mode=2 为系统处理,无需 WF;mode=0 通过 EXISTS 判断 WF 完成,避免一对多重复。
|
||||||
|
* 同一用户、社团存在以上申请时不再重复展示旧历史表记录;每条申请仍独立保留,列表与详情共用。
|
||||||
*
|
*
|
||||||
* @return 退会管理列表和详情共用的数据来源SQL
|
* @return 退会管理列表和详情共用的数据来源SQL
|
||||||
*/
|
*/
|
||||||
@@ -343,11 +516,12 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
cua.applyDate,
|
cua.applyDate,
|
||||||
cua.joinTime,
|
cua.joinTime,
|
||||||
cua.exitTime,
|
cua.exitTime,
|
||||||
'AUDIT_EXIT' AS exitType
|
CASE WHEN cua.mode = 2 THEN 'SYSTEM_EXIT' ELSE 'AUDIT_EXIT' END AS exitType
|
||||||
FROM
|
FROM
|
||||||
club_user_apply cua
|
club_user_apply cua
|
||||||
INNER JOIN wf_process_instance ins ON ins.businessNo = cua.id AND ins.state = 20
|
WHERE cua.mode = 2 OR (cua.mode = 0 AND EXISTS (
|
||||||
WHERE cua.mode = false
|
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = cua.id AND ins.state = 20
|
||||||
|
))
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
scu.id,
|
scu.id,
|
||||||
@@ -378,10 +552,11 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM club_user_apply cua
|
FROM club_user_apply cua
|
||||||
INNER JOIN wf_process_instance ins ON ins.businessNo = cua.id AND ins.state = 20
|
WHERE cua.clubId = scu.clubId
|
||||||
WHERE cua.mode = false
|
|
||||||
AND cua.clubId = scu.clubId
|
|
||||||
AND cua.userId = scu.userId
|
AND cua.userId = scu.userId
|
||||||
|
AND (cua.mode = 2 OR (cua.mode = 0 AND EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = cua.id AND ins.state = 20
|
||||||
|
)))
|
||||||
)
|
)
|
||||||
) exitInfo
|
) exitInfo
|
||||||
LEFT JOIN sys_club club ON club.id = exitInfo.clubId
|
LEFT JOIN sys_club club ON club.id = exitInfo.clubId
|
||||||
@@ -431,6 +606,12 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按现有成员排序渲染 clubRegis Word 模板,模板读取为空时提前阻止解析及下载。
|
||||||
|
* @param clubId 当前选中社团ID,登记表包含该社团资料和成员信息
|
||||||
|
* @param response 写入生成的 DOCX 附件;模板或渲染失败时抛出中文业务异常
|
||||||
|
* @return void,文件内容通过响应输出,错误结果由统一异常处理返回
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void exportRegistrationDoc(String clubId, HttpServletResponse response) {
|
public void exportRegistrationDoc(String clubId, HttpServletResponse response) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
@@ -501,11 +682,22 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
.bind("cys", policy)
|
.bind("cys", policy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
try (InputStream templateStream = sysOfficeTemplateUtil.getTemplate("clubRegis");
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("clubRegis"), config).render(docData).writeAndClose(byteArrayOutputStream);
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||||
|
// 文件存储读取失败会返回空内容,先校验再交给 POI,避免显示英文 EmptyFileException。
|
||||||
|
byte[] templateBytes = templateStream == null ? new byte[0] : templateStream.readAllBytes();
|
||||||
|
if (templateBytes.length == 0) {
|
||||||
|
throw new BaseException("社团登记表模板读取失败,请检查模板文件及文件存储服务。");
|
||||||
|
}
|
||||||
|
XWPFTemplate.compile(new ByteArrayInputStream(templateBytes), config).render(docData).writeAndClose(byteArrayOutputStream);
|
||||||
CommonDownloadUtil.download("教职工文体社团登记表(" + clubInfo.getString("clubName") + ").docx", byteArrayOutputStream.toByteArray(), response);
|
CommonDownloadUtil.download("教职工文体社团登记表(" + clubInfo.getString("clubName") + ").docx", byteArrayOutputStream.toByteArray(), response);
|
||||||
} catch (IOException e) {
|
} catch (BaseException e) {
|
||||||
log.error("社团登记表导出失败,id:{},错误信息:{}", clubId, e.getMessage());
|
// 保留模板配置及空文件校验的具体提示,同时记录完整异常以便排查。
|
||||||
|
log.error("社团登记表导出失败,id:{}", clubId, e);
|
||||||
|
throw e;
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
log.error("社团登记表导出失败,id:{}", clubId, e);
|
||||||
|
throw new BaseException("社团登记表生成失败,请检查模板是否为有效的 Word 文件或联系管理员。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ import com.budwk.app.zhgh.club.model.SysClubSponsor;
|
|||||||
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubService;
|
import com.budwk.app.zhgh.club.service.SysClubService;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubRegisterPageVo;
|
import com.budwk.app.zhgh.club.vo.ClubRegisterPageVo;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubRegisterVo;
|
import com.budwk.app.zhgh.club.vo.ClubRegisterVo;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.Static;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -60,10 +64,27 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
@Inject
|
@Inject
|
||||||
private SysClubUserService sysClubUserService;
|
private SysClubUserService sysClubUserService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private ClubPayRecordService payRecordService;
|
||||||
|
@Inject
|
||||||
private CommonService commonService;
|
private CommonService commonService;
|
||||||
@Inject
|
@Inject
|
||||||
private BpmService bpmService;
|
private BpmService bpmService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysClub> listJoinClubOptions() {
|
||||||
|
// 注册、换届等流程可能使用同一社团ID作为业务号;仅判断流程存在,避免联表将社团重复返回。
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT c.*
|
||||||
|
FROM sys_club c
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM wf_process_instance inst
|
||||||
|
WHERE inst.businessNo = c.id AND inst.state = @state
|
||||||
|
)
|
||||||
|
""").setParam("state", ProcessInstanceStateEnum.FINISHED.getCode());
|
||||||
|
return listVO(sql, SysClub.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubCommonPageVo> getAllClubWithPage(PageForm pageForm, Cnd cnd) {
|
public Pagination<ClubCommonPageVo> getAllClubWithPage(PageForm pageForm, Cnd cnd) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
@@ -77,10 +98,11 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
FROM
|
FROM
|
||||||
sys_club club
|
sys_club club
|
||||||
LEFT JOIN sys_user u on club.concatPerson = u.id
|
LEFT JOIN sys_user u on club.concatPerson = u.id
|
||||||
LEFT JOIN wf_process_instance ins ON ins.businessNo = club.id
|
|
||||||
$condition
|
$condition
|
||||||
""").setParam("userId", SecurityUtil.getUserId());
|
""").setParam("userId", SecurityUtil.getUserId());
|
||||||
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
|
// 同一社团可能关联注册、换届等多个已完成 WF 流程;仅检查流程存在,避免列表和分页总数重复。
|
||||||
|
cnd.and(new Static("EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = club.id AND ins.state = %s)"
|
||||||
|
.formatted(ProcessInstanceStateEnum.FINISHED.getCode())));
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
||||||
}
|
}
|
||||||
@@ -90,11 +112,14 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
if (StrUtil.isBlank(clubId)) {
|
if (StrUtil.isBlank(clubId)) {
|
||||||
return new ClubRegisterVo();
|
return new ClubRegisterVo();
|
||||||
}
|
}
|
||||||
|
// 与社团列表保持一致,按社团成员角色汇总会长、秘书长姓名,避免多名任职人员导致详情记录重复。
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
club.*,
|
club.*,
|
||||||
d.NAME AS typeName,
|
d.NAME AS typeName,
|
||||||
u.username concatPersonName,
|
u.username concatPersonName,
|
||||||
|
(select GROUP_CONCAT(username) from sys_user where id in (select userId from club_user u where u.clubId=club.id and JSON_CONTAINS(u.roleCode, '"CLUB_PRESIDENT"'))) as clubLeader,
|
||||||
|
(select GROUP_CONCAT(username) from sys_user where id in (select userId from club_user u where u.clubId=club.id and JSON_CONTAINS(u.roleCode, '"CLUB_SECRETARY"'))) as clubSecretary,
|
||||||
( SELECT count( 1 ) FROM club_user scu WHERE scu.clubId = club.id ) currentPeopleNum,
|
( SELECT count( 1 ) FROM club_user scu WHERE scu.clubId = club.id ) currentPeopleNum,
|
||||||
(select GROUP_CONCAT(username) from sys_user where id in (select sponsorId from sys_club_sponsor where clubId = club.id)) as sponsorName,
|
(select GROUP_CONCAT(username) from sys_user where id in (select sponsorId from sys_club_sponsor where clubId = club.id)) as sponsorName,
|
||||||
(select GROUP_CONCAT(sponsorId) from sys_club_sponsor where clubId = club.id) as sponsor
|
(select GROUP_CONCAT(sponsorId) from sys_club_sponsor where clubId = club.id) as sponsor
|
||||||
@@ -164,6 +189,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public SysClub doAdd(SysClub club, List<NutMap> managePerson) {
|
public SysClub doAdd(SysClub club, List<NutMap> managePerson) {
|
||||||
club.setDismiss(false);
|
club.setDismiss(false);
|
||||||
club.setUserId(SecurityUtil.getUserId());
|
club.setUserId(SecurityUtil.getUserId());
|
||||||
@@ -178,6 +204,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
clubUser.setUserId(person.getString("userId"));
|
clubUser.setUserId(person.getString("userId"));
|
||||||
return clubUser;
|
return clubUser;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
sysClubUserService.prepareDirectJoinTimes(clubUsers);
|
||||||
dao().insert(clubUsers);
|
dao().insert(clubUsers);
|
||||||
|
|
||||||
for (ClubUser clubUser : clubUsers) {
|
for (ClubUser clubUser : clubUsers) {
|
||||||
@@ -203,6 +230,8 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
log.set("newValue", false);
|
log.set("newValue", false);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:社团创建直接入会的成员同样保存缴费快照。
|
||||||
|
payRecordService.prepareSnapshots(List.of(record));
|
||||||
dao().insert(record);
|
dao().insert(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +240,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public SysClub doEdit(SysClub club, List<String> deleteIds, List<NutMap> managePerson) {
|
public SysClub doEdit(SysClub club, List<String> deleteIds, List<NutMap> managePerson) {
|
||||||
// 修改社团表
|
// 修改社团表
|
||||||
dao().update(club);
|
dao().update(club);
|
||||||
@@ -219,6 +249,8 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
// 重新插入发起人
|
// 重新插入发起人
|
||||||
dao().insertLinks(club, "sponsors");
|
dao().insertLinks(club, "sponsors");
|
||||||
|
|
||||||
|
// 现有编辑流程会重建会员关系,删除前保存原入会时间,避免把编辑时刻算成再次入会。
|
||||||
|
List<ClubUser> previousMembers = dao().query(ClubUser.class, Cnd.where("clubId", "=", club.getId()));
|
||||||
dao().clear(Sys_user_role.class, Cnd.where("clubId", "=", club.getId()));
|
dao().clear(Sys_user_role.class, Cnd.where("clubId", "=", club.getId()));
|
||||||
dao().clear(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", club.getId()));
|
dao().clear(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", club.getId()));
|
||||||
|
|
||||||
@@ -228,8 +260,24 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
clubUser.setClubId(club.getId());
|
clubUser.setClubId(club.getId());
|
||||||
clubUser.setRoleCode(List.of(person.getString("roleCode")));
|
clubUser.setRoleCode(List.of(person.getString("roleCode")));
|
||||||
clubUser.setUserId(person.getString("userId"));
|
clubUser.setUserId(person.getString("userId"));
|
||||||
|
previousMembers.stream()
|
||||||
|
.filter(member -> java.util.Objects.equals(member.getUserId(), clubUser.getUserId()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(member -> {
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:编辑社团重建既有会员关系时保留原入会时间及人员资料。
|
||||||
|
clubUser.setJoinTime(member.getJoinTime());
|
||||||
|
clubUser.setUserName(member.getUserName());
|
||||||
|
clubUser.setLoginName(member.getLoginName());
|
||||||
|
clubUser.setMobile(member.getMobile());
|
||||||
|
});
|
||||||
return clubUser;
|
return clubUser;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
// 仅真正新增的理事成员赋确认时间;原成员即使历史时间为空也保留原值。
|
||||||
|
List<ClubUser> newMembers = clubUsers.stream()
|
||||||
|
.filter(member -> previousMembers.stream().noneMatch(previous ->
|
||||||
|
java.util.Objects.equals(previous.getUserId(), member.getUserId())))
|
||||||
|
.toList();
|
||||||
|
sysClubUserService.prepareDirectJoinTimes(newMembers);
|
||||||
dao().insert(clubUsers);
|
dao().insert(clubUsers);
|
||||||
}
|
}
|
||||||
return club;
|
return club;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user