Compare commits

89 Commits
Author SHA1 Message Date
c-chenxs1 d5077d092c feat: 定时任务失效优化,并补充功能 2026-11-13 17:56:58 +08:00
c-chenxs1 63fc01cbb3 feat: 信息查询加姓名查询 2026-11-12 10:23:43 +08:00
c-chenxs1 902ad7e0e9 feat: 社团统一改为协会 2026-11-11 18:21:26 +08:00
c-chenxs1 651435d568 feat: 职工福利优化3 2026-11-11 17:59:50 +08:00
c-chenxs1 2ee171b4f3 feat: 职工福利优化2 2026-11-11 17:55:30 +08:00
c-chenxs1 a7a7aa5e3a feat: 职工福利优化 2026-11-11 17:39:15 +08:00
c-chenxs1 336bea15bc feat: 普惠疗休养优化6 2026-11-11 09:35:29 +08:00
c-chenxs1 49237dc2ea feat: 普惠疗休养优化5 2026-11-10 20:48:58 +08:00
c-chenxs1 faafb72eee feat: 普惠疗休养优化4 2026-11-10 14:29:19 +08:00
c-chenxs1 e4e856f6b2 feat: 普惠疗休养优化3 2026-11-10 11:45:05 +08:00
c-chenxs1 933daac134 feat: 普惠疗休养优化2 2026-11-09 18:09:05 +08:00
c-chenxs1 803333feac feat: 普惠疗休养优化 2026-11-09 11:42:38 +08:00
c-chenxs1 22a18986d0 feat: 工会报销优化2 2026-11-06 18:35:51 +08:00
c-chenxs1 d6e3815b3d feat: 工会报销优化 2026-11-06 18:08:21 +08:00
c-chenxs1 1ddfa7e609 feat: 经费预算管理2 2026-11-05 19:39:22 +08:00
c-chenxs1 e7fbf4021e feat: 经费预算管理 2026-11-05 19:23:13 +08:00
c-chenxs1 c21e92e418 feat: 疗休养UI优化2 2026-11-05 17:42:34 +08:00
c-chenxs1 134a11da09 feat: 疗休养UI优化 2026-11-05 16:17:06 +08:00
c-chenxs1 506121ac5f feat: 疗休养优化站内消息发送 2026-11-05 10:23:11 +08:00
c-chenxs1 8c401a5132 feat: 疗休养优化布局 2026-11-05 10:09:39 +08:00
c-chenxs1 4d9fd57d6c feat: 疗休养优化移动端 2026-11-05 08:37:27 +08:00
c-chenxs1 fd117b4fc3 feat: 疗休养优化2 2026-11-04 18:12:51 +08:00
c-chenxs1 060e8d5bc7 feat: 疗休养优化 2026-11-04 17:11:14 +08:00
c-chenxs1 894c720bb8 feat: 优化福利2 2026-11-03 20:35:12 +08:00
c-chenxs1 39ee74f0a9 feat: 优化福利 2026-11-03 20:20:59 +08:00
c-chenxs1 7faa2730b5 feat: 同步人员时按人员类型或在职状态设置工会会员标记 2026-11-03 17:41:57 +08:00
c-chenxs1 fa019786d6 feat: 完善人员数据同步与筛选
- 在人员来源表中保存专业技术职务代码 ZYJSZWDM
- 支持按所属单位、在职状态和人员类型筛选空数据
- 在获取信息中心数据和更新系统用户数据页面添加搜索重置按钮
- 同步人员时按人员类型或在职状态设置工会会员标记
2026-11-03 17:30:29 +08:00
c-chenxs1 ffe782e538 feat: 人员映射 2026-11-03 15:06:04 +08:00
c-chenxs1 3c65c9d561 feat: 同步职工慰问和困难帮扶 2026-11-02 18:12:23 +08:00
c-chenxs1 89a77c41af Merge branch 'master' into feat_本工会单独业务 2026-11-02 11:30:59 +08:00
c-chenxs1 f51b24686f Merge branch 'feat_可同步长江大学的优化_fix' 2026-11-02 11:30:17 +08:00
c-chenxs1 0f460e6b1a feat: 优化人员拉取和数据字典部分4 2026-11-02 11:22:39 +08:00
c-chenxs1 d1050db6b3 feat: 优化人员拉取和数据字典部分3 2026-10-29 16:14:08 +08:00
c-chenxs1 e3f32fe72b feat: 优化人员拉取和数据字典部分2 2026-10-29 15:52:02 +08:00
c-chenxs1 b885f449c7 feat: 优化人员拉取和数据字典部分 2026-10-29 15:29:55 +08:00
c-chenxs1 3f441828a1 Merge branch 'feat_可同步长江大学的优化_fix' into feat_本工会单独业务 2026-10-29 10:45:35 +08:00
c-chenxs1 3f7b5dc27a feat: 优化活动相关模块 2026-10-29 09:43:43 +08:00
c-chenxs1 e944ebf85f Merge branch 'feat_本工会单独业务' 2026-10-28 17:58:29 +08:00
c-chenxs1 a0732fdb08 Merge branch 'feat_可同步长江大学的优化_fix' into feat_本工会单独业务 2026-10-28 15:26:58 +08:00
c-chenxs1 db26813635 feat: move 2026-10-28 15:22:43 +08:00
c-chenxs1 d5f1c642c4 Merge branch 'feat_可同步长江大学的优化_fix' 2026-10-28 15:21:46 +08:00
c-chenxs1 a09cd7b8fd feat: 优化批量导入人员角色 2026-10-28 15:21:27 +08:00
c-chenxs1 d18d9bda6d Merge branch 'feat_可同步长江大学的优化_fix'
# Conflicts:
#	.gitignore
2026-10-28 13:54:48 +08:00
c-chenxs1 919f8c9dec feat: 同步地大代码-打开密码sso验证 2026-10-28 13:53:45 +08:00
c-chenxs1 0fb779ff5a feat: v3版疗休养优化9 2026-10-27 11:59:57 +08:00
c-chenxs1 55e94de004 feat: v3版疗休养优化8 2026-10-27 09:12:52 +08:00
c-chenxs1 d452d4ad92 feat: v3版疗休养优化7 2026-10-26 18:19:28 +08:00
c-chenxs1 6ea864cb64 feat: v3版疗休养优化6 2026-10-26 17:33:07 +08:00
c-chenxs1 56cc3fee91 feat: v3版疗休养优化6 2026-10-26 16:50:49 +08:00
c-chenxs1 4c03b389d4 feat: v3版疗休养优化5 2026-10-26 15:27:53 +08:00
c-chenxs1 fcb6ba7e2d feat: v3版疗休养优化4 2026-10-26 14:28:19 +08:00
c-chenxs1 65fac78cfb feat: v3版疗休养优化3 2026-10-26 13:35:39 +08:00
c-chenxs1 2d20959355 feat: v3版疗休养优化2 2026-10-26 09:49:06 +08:00
c-chenxs1 1a1946b1f8 feat: v3版疗休养优化 2026-10-26 09:45:30 +08:00
c-chenxs1 7f2ac97dea feat: v3版疗休养 2026-10-26 09:21:26 +08:00
c-chenxs1 82713327db feat: v3版疗休养 2026-10-26 09:19:45 +08:00
c-chenxs1 d598f1bf0f feat: 补充角色工作流 2026-10-23 17:12:29 +08:00
c-chenxs1 01e2443db8 feat: 俱乐部增加导入社团 2026-10-23 16:58:42 +08:00
c-chenxs1 bb4c44443d feat: 本地开发1登录 2026-10-23 14:50:06 +08:00
c-chenxs1 15d3ec0ddd feat:
给校工会增加角色时,如果已经给这个用户增加了,再增加会报错,并且页面无提示

分工会干部 加一个 编辑按钮, 只有超级管理员可以看到,另外添加这里的届次改为非必填
2026-10-23 14:36:43 +08:00
c-chenxs1 713e174650 feat: prd -> dev 2026-10-23 14:02:16 +08:00
c-chenxs1 6e351c6829 feat: 优化 2026-10-23 11:03:32 +08:00
c-chenxs1 5d160f660f feat: 同步地大更新 2026-10-22 18:24:14 +08:00
c-chenxs1 57d84724cb feat: 恢复密码不匹配时抛出“用户名或者密码不正确”,阻止登录。 2026-10-22 18:11:59 +08:00
c-chenxs1 018ee96402 feat: 提案配置不存在、校领导单位列表为 null 或为空时,记录警告并跳过角色刷新,继续人员全量更新 2026-10-22 17:50:19 +08:00
c-chenxs1 98c7804c28 feat: 新增 [CAS诊断] 日志 2026-10-22 17:15:49 +08:00
c-chenxs1 e257f4bae2 feat: 优化配置和人员拉取 2026-10-22 16:57:21 +08:00
c-chenxs1 054cf966c3 feat: 优化3 2026-10-22 15:04:33 +08:00
c-chenxs1 c497c0c0cc feat: 优化2 2026-10-22 14:15:35 +08:00
c-chenxs1 fa9ff1e9d5 feat: 优化查询 2026-10-22 14:10:41 +08:00
c-chenxs1 19a2a6d63a feat: 优化 2026-10-22 13:57:41 +08:00
c-chenxs1 33818d0972 feat: 优化劳模先进报错 2026-10-21 18:24:07 +08:00
c-chenxs1 0a93a71346 feat: 调整logo 2026-10-21 18:21:19 +08:00
c-chenxs1 10b9a5ba95 feat: 统一调整高校名 2026-10-21 17:26:52 +08:00
c-chenxs1 b19996f57e feat: 同步长江大学的风采墙优化 2026-10-21 15:23:13 +08:00
c-chenxs1 88d624172c feat: 同步地大功能至2026年10月20日 2026-10-21 09:26:25 +08:00
c-chenxs1 6b5d27c68d feat: dev 2026-10-10 17:32:22 +08:00
c-chenxs1 7c5bc65fa0 feat: 登录补上验证码 2026-10-10 17:15:53 +08:00
c-chenxs1 fbdf38389d feat: 打包mini 2026-10-10 16:18:26 +08:00
c-chenxs1 48329f933b feat:
prd 消息平台改用浙师大接口及 v3 鉴权参数。
    企业微信沿用 v3 参数,补齐授权入口和回调。
    prd 系统域名固定为 https://lxy.zjnu.edu.cn。
    登录跳转兼容本站完整 URL。
    清空其他学校的废弃对接参数。
    公众号填入模拟参数,保持关闭;dev 配置不变。
2026-10-10 16:13:37 +08:00
c-chenxs1 c262c8e715 feat: 登录部分2 2026-10-10 15:58:42 +08:00
c-chenxs1 17369c90e6 feat: 登录部分 2026-10-10 15:26:02 +08:00
c-chenxs1 426c389d5b feat: 优化拉取接口 2026-10-10 15:06:54 +08:00
c-chenxs1 24f7267fbc feat: 修改参数配置 2026-10-10 13:43:37 +08:00
c-chenxs1 b4d6c4bfa3 feat: 补充 民族 2026-10-09 09:30:30 +08:00
c-chenxs1 82cbea973f feat: dev 拉取资源接口 2026-10-09 08:48:19 +08:00
c-chenxs1 d039559f47 feat: 学习平台优化 2026-10-09 08:40:10 +08:00
c-chenxs1 123a73e5ca feat: 修复菜单图标bug 2026-10-08 20:06:43 +08:00
c-chenxs1 1650383670 feat: 人资+机构同步 2026-10-08 19:45:38 +08:00
765 changed files with 79433 additions and 7104 deletions
+5
View File
@@ -49,3 +49,8 @@ Thumbs.db
.trae/pr-creator/SKILL.md
.trae/update-docs/SKILL.md
.trae/webapp-testing/SKILL.md
outputs
semgrep.json
trivy.json
.workbuddy
+1 -1
View File
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<groupId>com.budwk</groupId>
<artifactId>zhgh_zjnu</artifactId>
<artifactId>mini</artifactId>
<version>5.6.0-plus</version>
<properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
+13 -12
View File
@@ -5,6 +5,7 @@ import cn.dev33.satoken.config.SaTokenConfig;
import com.budwk.app.base.utils.AppIocUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.sys.services.SysTaskService;
import com.budwk.app.task.log.TaskExecutionListener;
import com.budwk.app.task.services.TaskPlatformService;
import com.budwk.app.web.commons.auth.satoken.SaTokenContextImpl;
import com.budwk.app.web.commons.auth.satoken.SaTokenDaoRedisImpl;
@@ -20,7 +21,6 @@ import org.beetl.core.GroupTemplate;
import org.nutz.boot.NbApp;
import org.nutz.dao.Dao;
import org.nutz.dao.impl.FileSqlManager;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos;
import org.nutz.integration.jedis.JedisAgent;
import org.nutz.ioc.Ioc;
@@ -31,6 +31,7 @@ import org.nutz.lang.Mirror;
import org.nutz.mvc.Mvcs;
import org.nutz.mvc.annotation.*;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
@@ -39,7 +40,6 @@ import java.lang.management.ManagementFactory;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Enumeration;
import java.util.List;
@IocBean(create = "init", depose = "depose")
@Localization(value = "locales/", defaultLocalizationKey = "zh_CN")
@@ -82,7 +82,9 @@ public class MainLauncher {
Mvcs.DISABLE_X_POWERED_BY = true;
Globals.AppBase = Mvcs.getServletContext().getContextPath();
Globals.AppRoot = Mvcs.getServletContext().getRealPath("/");
init_task();
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();
@@ -92,7 +94,6 @@ public class MainLauncher {
ClubPayRecordService payRecords = ioc.get(ClubPayRecordService.class, "clubPayRecordServiceImpl");
payRecords.upgradePayRecordSchema();
payRecords.backfillPayRecordSnapshots();
init_task();
init_auth();
ioc.get(Globals.class);
ioc.get(FlowEngine.class);
@@ -115,17 +116,17 @@ public class MainLauncher {
}
private void init_task() {
if (log.isDebugEnabled() && !dao.exists("sys_qrtz_triggers") && !dao.exists("sys_qrtz_triggers".toUpperCase())) {
//执行Quartz SQL脚本
String dbType = dao.getJdbcExpert().getDatabaseType();
log.debug("dbType:::" + dbType);
FileSqlManager fmq = new FileSqlManager("db/quartz/" + dbType.toLowerCase() + ".sql");
List<Sql> sqlListq = fmq.createCombo(fmq.keys());
Sql[] sqlsq = sqlListq.toArray(new Sql[sqlListq.size()]);
for (Sql sql : sqlsq) {
dao.execute(sql);
FileSqlManager schema = new FileSqlManager("db/quartz/mysql.sql");
for (String table : schema.keys()) {
if (!dao.exists(table)) {
dao.execute(schema.create(table));
}
}
try {
ioc.get(TaskExecutionListener.class).register();
} catch (SchedulerException e) {
throw new IllegalStateException("注册任务执行记录监听器失败", e);
}
}
private void init_sys() {
@@ -24,6 +24,7 @@ public class DataCenterProperties {
@Inject
private PropertiesProxy conf;
private boolean tokenEnabled;
private String tokenUrl;
private String key;
private String secret;
@@ -31,6 +32,7 @@ public class DataCenterProperties {
public void init() {
Map<String, String> all = conf.toMap();
tokenEnabled = conf.getBoolean("data-center.token-enabled", true);
tokenUrl = all.get("data-center.token-url");
key = all.get("data-center.key");
secret = all.get("data-center.secret");
@@ -29,13 +29,13 @@ public enum BpmProcessConstant {
MEMBER_BRANCH_UNION_CHANGE("会员分工会变更"),
CLUB_JOIN("社团入会申请"),
CLUB_JOIN("协会入会申请"),
CLUB_EXAMINE("社团考核"),
CLUB_EXAMINE("协会考核"),
CLUB_EVALUATE("社团评优"),
CLUB_EVALUATE("协会评优"),
CLUB_REGISTER("社团注册"),
CLUB_REGISTER("协会注册"),
PERSON_EVALUATE("评优评先申请"),
@@ -48,6 +48,13 @@ public class RedisConstant {
*/
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 REDIS_KEY_PERSON_API_DATA_QUERY_RESULT =
PLATFORM_REDIS_PREFIX + "personGovernance:dataQuery:";
}
@@ -20,14 +20,17 @@ RoleConstant {
SCHOOL_UNION_CHAIRMAN("校工会主席"),
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
SCHOOL_UNION_WELFARE_COMMITTEE("校福利委员会"),
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
SCHOOL_UNION_TC_ADMIN("校工会教代会管理员"),
SCHOOL_UNION_WC_ADMIN("校工会工代会管理员"),
SCHOOL_UNION_DC_ADMIN("校工会民主管理员"),
SCHOOL_UNION_PROPOSAL_ADMIN("校工会提案管理员"),
SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
SCHOOL_UNION_CLUB_ADMIN("校工会协会管理员"),
SCHOOL_UNION_ACTIVITY_ADMIN("校工会活动管理员"),
SCHOOL_UNION_ARTICLE_CLUB_ADMIN("校工会社团新闻审批管理员"),
/** 品牌活动统计、人员调整管理员,数据范围按 trainSignUp 子活动负责人关系控制。 */
SCHOOL_UNION_TRAIN_SIGN_UP_ADMIN("校工会品牌活动管理员"),
SCHOOL_UNION_ARTICLE_CLUB_ADMIN("校工会协会新闻审批管理员"),
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
SCHOOL_OUTLAY_ADMIN("校工会经费管理员"),
@@ -48,6 +51,11 @@ RoleConstant {
BRANCH_UNION_SHENGGHUO_WY("分工会生活委员"),
BRANCH_UNION_TIAOJIE_WY("分工会调解委员"),
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("单位党委书记"),
@@ -78,16 +86,16 @@ RoleConstant {
SCHOOL_HOSPITAL_LEADER("校医院负责人"),
/*社团模块*/
CLUB_MEMBER("社团会员"),
CLUB_MANAGER("社团负责人"),
CLUB_PRESIDENT("社团会长"),
CLUB_VICE_PRESIDENT("社团副会长"),
CLUB_SECRETARY("社团秘书长"),
CLUB_VICE_SECRETARY("社团副秘书长"),
// SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
CLUB_AUDIT_LEADER("社团分管领导"),
CLUB_OPERATOR("社团操作员"),
/*协会模块*/
CLUB_MEMBER("协会会员"),
CLUB_MANAGER("协会负责人"),
CLUB_PRESIDENT("协会会长"),
CLUB_VICE_PRESIDENT("协会副会长"),
CLUB_SECRETARY("协会秘书长"),
CLUB_VICE_SECRETARY("协会副秘书长"),
// SCHOOL_UNION_CLUB_ADMIN("校工会协会管理员"),
CLUB_AUDIT_LEADER("协会分管领导"),
CLUB_OPERATOR("协会操作员"),
/*大病基金会员*/
AID_FUND_MEMBER("大病基金会员"),
@@ -103,7 +111,7 @@ RoleConstant {
LEGAL_DOCTOR("法律顾问"),
UNION_REIMBURSEMENT_MANAGER("分工会报销负责人"),
CLUB_REIMBURSEMENT_MANAGER("社团报销负责人"),
CLUB_REIMBURSEMENT_MANAGER("协会报销负责人"),
SCHOOL_REIMBURSEMENT_MANAGER("校工会报销负责人"),
;
@@ -13,6 +13,7 @@ public enum LoginType {
QI_YE_WECHAT("企业微信"),
WECHAT("微信"),
PC_LOCAL("PC本地"),
SMS_CODE("手机验证码"),
MOBILE_LOCAL("手机本地"),
CAS("CAS");
@@ -50,6 +50,8 @@ public class SmsServiceImpl implements SmsService {
@Inject
private PropertiesProxy conf;
@Inject
private ZjnuMsgService zjnuMsgService;
/**
* 发送短信消息。
@@ -59,6 +61,9 @@ public class SmsServiceImpl implements SmsService {
*/
@Override
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);
JSONObject payload = buildCommonPayload(request, true);
return doPost(SMS_API_PATH, payload);
@@ -107,6 +112,9 @@ public class SmsServiceImpl implements SmsService {
*/
@Override
public MsgPlatformResponse sendWechat(MsgPlatformWechatRequest request) {
if (Strings.isNotBlank(conf.get("custom.msg.platform.app-id"))) {
return zjnuMsgService.send(request, "WeChat");
}
validateCommonRequest(request);
if (Strings.isBlank(request.getWechatType())) {
request.setWechatType("text");
@@ -342,6 +350,9 @@ public class SmsServiceImpl implements SmsService {
* @param 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.setSendUserName(getRequiredConfig("msg.platform.default-send-user-name", "请先配置 msg.platform.default-send-user-name"));
request.setSendUnitId(conf.get("msg.platform.default-send-unit-id", ""));
@@ -351,7 +362,7 @@ public class SmsServiceImpl implements SmsService {
}
/**
* 调用地大通讯平台 REST 接口。
* 调用浙江师范大学通讯平台 REST 接口。
*
* @param apiPath 具体接口路径
* @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("&lt;", "<").replace("&gt;", ">"));
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("/+$", "");
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -22,7 +22,7 @@ import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 获取社团审批人根据流程变量参数(clubId) 必传
* 获取协会审批人根据流程变量参数(clubId) 必传
*/
public class FlowClubPresidentByArgsAssignmentHandler implements AssignmentHandler {
@Override
@@ -45,7 +45,7 @@ public class FlowClubPresidentByArgsAssignmentHandler implements AssignmentHandl
@Override
public String getMessage() {
return "获取社团会长(根据args中的clubId)";
return "获取协会会长(根据args中的clubId)";
}
@Override
@@ -0,0 +1,47 @@
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.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 org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import java.util.List;
public class FlowSchoolUnionWelfareCommitteeHandler 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.SCHOOL_UNION_WELFARE_COMMITTEE);
if (role == null || role.isDisabled()) {
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::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;
}
}
@@ -7,10 +7,12 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_config;
import com.budwk.app.sys.services.SysConfigService;
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.Dao;
import org.nutz.dao.Sqls;
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.IocBean;
import org.nutz.lang.Strings;
@@ -139,12 +141,15 @@ public class SysConfController {
@At
@Ok("json:full")
@SaCheckPermission("sys.manager.conf")
@Aop(TransAop.READ_COMMITTED)
public Object data(@Param("pageNumber") int pageNumber,
@Param("pageSize") int pageSize,
@Param("pageOrderName") String pageOrderName,
@Param("pageOrderBy") String pageOrderBy,
@Param("configKey") String configKey) {
try {
// 由 service 补充默认关闭的入口参数,管理员可通过现有“是/否”控件修改。
sysConfigService.ensureSmsLoginVisibleConfig(SecurityUtil.getUserId());
ensureAppImageConfig("AppHomeImg", "PC首页轮播图");
ensureAppImageConfig("H5AppHomeImg", "移动端首页轮播图");
ensureAppImageConfig("AppFeaturedActivityImg", "精彩活动页顶部图片");
@@ -14,6 +14,7 @@ import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
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.IocBean;
import org.nutz.mvc.annotation.At;
@@ -47,10 +48,10 @@ public class SysDataUserPullController {
cnd.andEX(Sys_user_source::getPullTime, "=", pageForm.getPullTime());
cnd.and(Cnd.likeEX(Sys_user_source::getUsername, pageForm.getUserName()));
cnd.and(Cnd.likeEX(Sys_user_source::getLoginname, pageForm.getLoginName()));
cnd.andEX(Sys_user_source::getUnitName, "=", pageForm.getUnitName());
cnd.andEX(Sys_user_source::getUserState, "=", pageForm.getUserState());
addSourceFilter(cnd, "us.unitName", pageForm.getUnitName());
addSourceFilter(cnd, "us.userState", pageForm.getUserState());
cnd.andEX(Sys_user_source::getPreparedBy, "=", pageForm.getPreparedBy());
cnd.andEX(Sys_user_source::getPersonType, "=", pageForm.getPersonType());
addSourceFilter(cnd, "us.personType", pageForm.getPersonType());
if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
@@ -61,6 +62,15 @@ public class SysDataUserPullController {
return Result.success(pagination);
}
private void addSourceFilter(Cnd cnd, String column, String value) {
if ("__EMPTY__".equals(value)) {
cnd.and(new Static("(%s IS NULL OR LOWER(%s) REGEXP '^[[:space:]]*(null)?[[:space:]]*$')"
.formatted(column, column)));
} else {
cnd.andEX(column, "=", value);
}
}
@At
@SaCheckPermission("sys.data.user.pull")
@ApiOperation("拉取用户数据")
@@ -0,0 +1,32 @@
package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.services.SysDataUserPullService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@At("/platform/sys/data/user/query")
@Ok("json:full")
public class SysDataUserQueryController {
@Inject
private SysDataUserPullService sysUserPullService;
@At("")
@Ok("beetl:/platform/sys/data/user/query.html")
@SaCheckPermission("sys.data.user.query")
public void index() {
}
@At
@SaCheckPermission("sys.data.user.query")
public Result queryData(@Param("zgh") String zgh, @Param("xm") String xm,
@Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize) {
return Result.success(sysUserPullService.queryUsers(zgh, xm, pageNumber, pageSize));
}
}
@@ -3,6 +3,7 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
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.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
@@ -119,6 +120,8 @@ public class SysDictController {
sysDictService.save(dict, parentId);
sysDictService.clearCache();
return Result.success("system.success");
} catch (BaseException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
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) {
try {
dict.setUpdatedBy(SecurityUtil.getUserId());
sysDictService.updateIgnoreNull(dict);
sysDictService.updateDict(dict);
sysDictService.clearCache();
return Result.success();
} catch (BaseException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
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.utils.SysFileMinIoUtil;
import com.google.common.net.HttpHeaders;
import com.budwk.app.zhgh.learning.service.LearningFileAccessService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -52,6 +53,9 @@ public class SysFileController {
private SysConfigService sysConfigService;
private SysFileMinIoUtil sysFileMinIoUtil;
@Inject
private LearningFileAccessService learningFileAccessService;
@At("")
@Ok("beetl:/platform/sys/file/index.html")
@SaCheckPermission("sys.manager.file")
@@ -124,6 +128,7 @@ public class SysFileController {
@Ok("void")
@SaCheckLogin
public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (!learningFileAccessService.check(id, true, response)) return;
sysFileService.download(id, request, response);
}
@@ -132,6 +137,7 @@ public class SysFileController {
@SaCheckLogin
@ApiOperation("文件管理-转换PDF")
public void convertPDF(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (!learningFileAccessService.check(id, true, response)) return;
sysFileService.convertPDF(id, request, response);
}
@@ -170,11 +176,82 @@ public class SysFileController {
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
@SaCheckLogin
@ApiOperation("文件管理-视频播放")
@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);
byte[] bytes = SysFileMinIoUtil.getFileBytes(sys_file.getBucket(), sys_file.getStoragePath());
@@ -1,39 +1,29 @@
package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.constant.RoleConstant;
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_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.SysUserService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
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.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.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
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.HttpSession;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@@ -58,9 +47,9 @@ public class SysHomeController {
@Inject
private SysUserService sysUserService;
@Inject
private RedisService redisService;
private SysHomeActivityService sysHomeActivityService;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
private SysHomeNewsService sysHomeNewsService;
@At("")
@Ok("re")
@@ -207,71 +196,16 @@ public class SysHomeController {
}
/**
* includeEnded可选:true供PC首页展示已结束活动,缺省false保持既有调用行为。
* 返回Result,data为当前登录用户可见的活动实体列表,含名称、封面、链接及起止时间,不含内部SQL和类路径。
*/
@At
@SaCheckLogin
@ApiOperation("首页推送活动")
@Ok("json")
public Result listHomeActivity() {
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"));
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);
public Result listHomeActivity(@Param(value = "includeEnded", df = "false") boolean includeEnded) {
return Result.success(sysHomeActivityService.listHomeActivity(SecurityUtil.getUserId(), includeEnded));
}
@At
@@ -293,34 +227,17 @@ public class SysHomeController {
@SaCheckLogin
@ApiOperation("获取新闻")
@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() {
try {
String url = "https://xgh.cug.edu.cn/";
Document document = Jsoup.connect(url).get();
Elements homeElements = document.select(".gh,.jc,.fc,.jg");
List<Map<String, Object>> news = homeElements.stream().map(homeElement -> {
//获取板块名称
String name = homeElement.selectFirst(".title").text();
Elements liElements = homeElement.select("li");
List<NutMap> ele = liElements.stream().map(liElement -> {
NutMap row = NutMap.NEW();
String href = liElement.selectFirst("a").attr("href");
String time = liElement.selectFirst("span").text();
String title = liElement.selectFirst("a").attr("title");
row.put("href", href);
row.put("time", time);
row.put("text", title);
return row;
}).toList();
return Map.of("label", name, "value", ele);
}).collect(Collectors.toList());
redisService.setex("gonghui_news", 60 * 60 * 24, Json.toJson(news));
return Result.success(news);
} catch (IOException e) {
throw new RuntimeException(e);
return Result.success(sysHomeNewsService.getWebsiteNews());
}
/**
* href 传首页新闻项原始链接;成功结果 data 为图片地址字符串,空字符串表示没有可用图片。
*/
@At
@SaCheckLogin
@Ok("json")
public Result getNewsImage(@Param("href") String href) {
return Result.success(sysHomeNewsService.getNewsImage(href));
}
}
@@ -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_user;
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.zhgh.dayofficework.message.service.SliderCaptchaService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.integration.jedis.RedisService;
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.IocBean;
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.Ok;
import org.nutz.mvc.annotation.Param;
import org.nutz.mvc.annotation.POST;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -55,19 +60,19 @@ import java.util.concurrent.ConcurrentHashMap;
public class SysLoginController {
private static final Log log = Logs.get();
private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing();
/**
* TODO: 临时屏蔽回环地址及指定局域网 IP 的平台登录验证码,全部恢复时改为 false。
*/
private static final boolean DISABLE_LOGIN_CAPTCHA = true;
@Inject
private SysUserService sysUserService;
@Inject
private SysConfigService sysConfigService;
@Inject
private RedisService redisService;
@Inject
private SLogService sLogService;
@Inject
private ValidateService validateService;
@Inject
private SliderCaptchaService sliderCaptchaService;
@Inject
protected PropertiesProxy conf;
@At("")
@@ -75,13 +80,17 @@ public class SysLoginController {
@ApiOperation("用户本地登录页面")
@Filters
public String login(HttpServletRequest req, HttpSession session) {
// 只向模板提供入口显示状态,短信发送及验证码登录接口继续使用原有规则。
req.setAttribute("smsLoginVisible", sysConfigService.isSmsLoginVisible());
return "beetl:/platform/sys/login.html";
}
@At
@Ok("re")
@Filters
public String h5() {
public String h5(HttpServletRequest req) {
// 手机端复用同一个登录模板,读取相同的系统参数。
req.setAttribute("smsLoginVisible", sysConfigService.isSmsLoginVisible());
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")
@Ok("json")
@ApiOperation("用户本地账号密码登录")
@@ -124,24 +140,16 @@ public class SysLoginController {
// return Result.error("登录失败次数过多,请5分钟后再试");
// }
try {
// 验证码校验
if (!isLoginCaptchaDisabled(req)) {
try {
validateService.checkCode(captchaKey, captchaCode);
} catch (BaseException e) {
return Result.error(e.getMessage());
}
}
// 所有访问地址均需消费绑定当前账号的滑块凭证,旧图形码不能用于登录。
sliderCaptchaService.consume(session.getId(), captchaKey, captchaCode,
SliderCaptchaService.PASSWORD, username);
// 解密密码
LoginRsaKey rsaKey = LOGIN_RSA_KEY_RING.getPrivateKey(keyId);
if (rsaKey == null) {
throw new BaseException("Login key expired, please refresh the login page");
throw new BaseException("登录凭证已过期,请刷新登录页面。");
}
String decryptPwd = RsaUtils.decrypt(password, rsaKey.privateKey);
if (decryptPwd == null) {
throw new BaseException("用户登录失败");
}
decryptPwd = Base64Encoder.encode(decryptPwd);
// 用户名密码校验
@@ -162,32 +170,40 @@ public class SysLoginController {
}
}
private boolean isLoginCaptchaDisabled(HttpServletRequest req) {
if (!DISABLE_LOGIN_CAPTCHA || req == null) {
return false;
}
String serverName = req.getServerName();
// 临时允许该局域网地址免验证码;恢复 IP 验证码时删除此条件,并同步删除登录页中的该 IP。
return "localhost".equalsIgnoreCase(serverName)
|| "192.168.0.108".equals(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)
@At(value = {"/platform/sso/login", "/sso/login"}, top = true)
@Ok("re")
@ApiOperation("用户cas登录统一入口")
public String ssoLogin(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response) {
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) {
log.warn("[CAS诊断] Session 中没有有效认证结果,未执行本地登录,即将跳转首页");
return ">>:/platform/home";
}
String loginName = assertion.getPrincipal().getName();
String stage = "查询本地用户";
try {
sysUserService.checkThirdPlatformLoginName(loginName);
Sys_user sysUser = sysUserService.loginByLoginName(loginName);
return ">>:" + sysUserService.loginPlus(sysUser, LoginType.CAS, request);
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)
@@ -277,6 +293,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")
@Ok("json")
@ApiOperation("获取验证码")
@@ -2,15 +2,18 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.sys.models.Sys_task;
import com.budwk.app.sys.models.Sys_task_execution;
import com.budwk.app.sys.services.SysTaskService;
import com.budwk.app.task.services.TaskPlatformService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
@@ -18,9 +21,12 @@ import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.POST;
import org.nutz.mvc.annotation.Param;
import org.nutz.trans.Trans;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
;
@@ -32,6 +38,8 @@ public class SysTaskController {
private SysTaskService sysTaskService;
@Inject
private TaskPlatformService taskPlatformService;
@Inject
private Dao dao;
@At("")
@Ok("beetl:/platform/sys/task/index.html")
@@ -41,10 +49,10 @@ public class SysTaskController {
}
@At
@Ok("beetl:/platform/sys/task/cron.html")
@Ok("beetl:/platform/sys/task/index.html")
@SaCheckPermission("sys.manager.task")
public void cron() {
public void cron(HttpServletRequest req) {
req.setAttribute("openCron", true);
}
@At
@@ -52,9 +60,9 @@ public class SysTaskController {
@SaCheckPermission("sys.manager.task")
public Object cronExp(@Param("cron") String cron) {
try {
return Result.success().addData(taskPlatformService.getCronExeTimes(cron));
return Result.success().addData(previewCron(normalizeCron(cron)));
} catch (Exception e) {
return Result.error(e.getMessage());
return taskError("获取执行时间失败", e);
}
}
@@ -73,20 +81,57 @@ public class SysTaskController {
}
}
@At("/history/?")
@Ok("json")
@SaCheckPermission("sys.manager.task")
public Object history(String taskId, @Param("pageNumber") int pageNumber) {
try {
requireTask(taskId);
int page = Math.max(1, Math.min(pageNumber, Integer.MAX_VALUE / 10));
Cnd condition = Cnd.where("taskId", "=", taskId);
condition.desc("startedAt").desc("id");
List<Sys_task_execution> list = dao.query(Sys_task_execution.class, condition, dao.createPager(page, 10),
"^(id|taskId|startedAt|finishedAt|status)$");
int total = dao.count(Sys_task_execution.class, Cnd.where("taskId", "=", taskId));
return Result.success(new Pagination<>(page, 10, total, list));
} catch (Exception e) {
return taskError("获取执行历史失败", e);
}
}
@At("/historyLog/?/?")
@Ok("json")
@SaCheckPermission("sys.manager.task")
public Object historyLog(String taskId, String executionId) {
try {
requireTask(taskId);
Sys_task_execution execution = dao.fetch(Sys_task_execution.class,
Cnd.where("id", "=", executionId).and("taskId", "=", taskId));
return execution == null ? Result.error("执行记录不存在") : Result.success(execution);
} catch (Exception e) {
return taskError("获取执行日志失败", e);
}
}
@At
@Ok("json")
@SLog(tag = "新建任务", msg = "任务名称:${args[0].name}")
@SaCheckPermission("sys.manager.task.add")
@Aop(TransAop.READ_COMMITTED)
public Object addDo(@Param("..") Sys_task task, HttpServletRequest req) {
try {
task.setCron(validateCron(task.getCron()));
task.setCreatedBy(SecurityUtil.getUserId());
Sys_task sysTask = sysTaskService.insert(task);
taskPlatformService.add(sysTask.getId(), sysTask.getId(), sysTask.getJobClass(), sysTask.getCron(),
sysTask.getNote(), sysTask.getData());
task.setExeAt(null);
task.setExeResult(null);
Trans.exec(() -> {
Sys_task saved = sysTaskService.insert(task);
if (!saved.isDisabled()) {
schedule(saved);
}
});
return Result.success();
} catch (Exception e) {
return Result.error();
return taskError("新建任务失败", e);
}
}
@@ -107,18 +152,24 @@ public class SysTaskController {
@SaCheckPermission("sys.manager.task.edit")
public Object editDo(@Param("..") Sys_task task, HttpServletRequest req) {
try {
if (task.getCron() != null) {
task.setCron(validateCron(task.getCron()));
}
task.setUpdatedBy(SecurityUtil.getUserId());
sysTaskService.updateIgnoreNull(task);
if (taskPlatformService.isExist(task.getId(), task.getId())) {
taskPlatformService.delete(task.getId(), task.getId());
}
if (!task.isDisabled()) {
taskPlatformService.add(task.getId(), task.getId(), task.getJobClass(), task.getCron(),
task.getNote(), task.getData());
Trans.exec(() -> {
requireTask(task.getId());
FieldFilter.locked(Sys_task.class, "^(exeAt|exeResult)$")
.run(() -> sysTaskService.updateIgnoreNull(task));
Sys_task saved = requireTask(task.getId());
if (saved.isDisabled()) {
taskPlatformService.delete(saved.getId(), saved.getId());
} else {
schedule(saved);
}
});
return Result.success();
} catch (Exception e) {
return Result.error();
return taskError("修改任务失败", e);
}
}
@@ -129,19 +180,35 @@ public class SysTaskController {
@SaCheckPermission("sys.manager.task.delete")
public Object delete(String id, HttpServletRequest req) {
try {
Sys_task sysTask = sysTaskService.fetch(id);
try {
if (taskPlatformService.isExist(sysTask.getId(), sysTask.getId())) {
taskPlatformService.delete(sysTask.getId(), sysTask.getId());
}
} catch (Exception e) {
log.error(e.getMessage());
}
Trans.exec(() -> {
Sys_task task = requireTask(id);
sysTaskService.delete(id);
req.setAttribute("name", sysTask.getName());
taskPlatformService.delete(id, id);
req.setAttribute("name", task.getName());
});
return Result.success();
} catch (Exception e) {
return Result.error();
return taskError("删除任务失败", e);
}
}
@At("/runOnce/?")
@POST
@Ok("json")
@SaCheckPermission("sys.manager.task.edit")
@SLog(tag = "执行任务一次", msg = "任务名:${args[1].getAttribute('name')}")
public Object runOnce(String id, HttpServletRequest req) {
Sys_task task = sysTaskService.fetch(id);
if (task == null) {
return Result.error("任务不存在");
}
req.setAttribute("name", task.getName());
try {
taskPlatformService.runOnce(task);
return Result.success("已提交执行");
} catch (Exception e) {
log.error("提交单次任务失败:" + task.getName(), e);
return Result.error("提交执行失败");
}
}
@@ -150,18 +217,7 @@ public class SysTaskController {
@SaCheckPermission("sys.manager.task.edit")
@SLog(tag = "启用任务", msg = "任务名:${args[1].getAttribute('name')}")
public Object enable(String id, HttpServletRequest req) {
try {
Sys_task sysTask = sysTaskService.fetch(id);
req.setAttribute("name", sysTask.getName());
sysTaskService.update(org.nutz.dao.Chain.make("disabled", false), Cnd.where("id", "=", id));
if (!taskPlatformService.isExist(sysTask.getId(), sysTask.getId())) {
taskPlatformService.add(sysTask.getId(), sysTask.getId(), sysTask.getJobClass(), sysTask.getCron(),
sysTask.getNote(), sysTask.getData());
}
return Result.success();
} catch (Exception e) {
return Result.error();
}
return changeDisabled(id, false, req);
}
@At("/disable/?")
@@ -169,16 +225,66 @@ public class SysTaskController {
@SaCheckPermission("sys.manager.task.edit")
@SLog(tag = "禁用任务", msg = "任务名:${args[1].getAttribute('name')}")
public Object disable(String id, HttpServletRequest req) {
try {
Sys_task sysTask = sysTaskService.fetch(id);
req.setAttribute("name", sysTask.getName());
sysTaskService.update(org.nutz.dao.Chain.make("disabled", true), Cnd.where("id", "=", id));
if (taskPlatformService.isExist(sysTask.getId(), sysTask.getId())) {
taskPlatformService.delete(sysTask.getId(), sysTask.getId());
return changeDisabled(id, true, req);
}
private Object changeDisabled(String id, boolean disabled, HttpServletRequest req) {
try {
Trans.exec(() -> {
sysTaskService.update(Chain.make("disabled", disabled), Cnd.where("id", "=", id));
Sys_task task = requireTask(id);
req.setAttribute("name", task.getName());
if (disabled) {
taskPlatformService.delete(id, id);
} else {
schedule(task);
}
});
return Result.success();
} catch (Exception e) {
return Result.error();
return taskError(disabled ? "禁用任务失败" : "启用任务失败", e);
}
}
private Sys_task requireTask(String id) {
Sys_task task = Strings.isBlank(id) ? null : sysTaskService.fetch(id);
if (task == null) {
throw new IllegalArgumentException("任务不存在");
}
return task;
}
private void schedule(Sys_task task) {
taskPlatformService.add(task.getId(), task.getId(), task.getJobClass(), task.getCron(),
task.getNote(), task.getData());
}
private String normalizeCron(String cron) {
return Strings.sNull(cron).trim().replaceAll("\\s+", " ");
}
private String validateCron(String cron) throws Exception {
String normalized = normalizeCron(cron);
previewCron(normalized);
return normalized;
}
private List<String> previewCron(String normalized) throws Exception {
if (normalized.length() > 50) {
throw new IllegalArgumentException("Cron 表达式最多 50 个字符");
}
List<String> times = taskPlatformService.getCronExeTimes(normalized);
if (times.isEmpty()) {
throw new IllegalArgumentException("Cron 表达式没有未来执行时间");
}
return times;
}
private Result taskError(String message, Exception e) {
if (e instanceof IllegalArgumentException) {
return Result.error(e.getMessage());
}
log.error(message, e);
return Result.error(message);
}
}
@@ -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();
}
@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
@ApiOperation("分工会干部离任")
@SaCheckPermission("sys.manager.union.branchOfficer")
@@ -84,6 +84,7 @@ public class SysUnitController {
// cnd.and("parentId", "is not", null).andEX("unitLevel", "=", unitLevel).asc("unitLevel").asc("unitcode");
cnd.and("parentId", "is not", null);
cnd.andEX("parentId", "=", unitId);
cnd.andEX("id", "!=", unitId);
// cnd.and("unitTypeCode", "=", "1");
cnd.asc("unitcode");
cnd.and(Cnd.likeEX("name", unitName));
@@ -295,6 +296,14 @@ public class SysUnitController {
}
}
/**
* 获取以指定单位为根的单位树。
*
* @param pid 根单位 ID;未传时使用系统根单位 0
* @param req 当前 HTTP 请求
* @return Result 对象;code 为 0 时 data 为树数组,节点包含 id、parentId、name、children、unitTypeCode;
* unitTypeCode 为 0 表示其他类、1 表示部门类,历史空值仅在返回时按 0 展示
*/
@At("/tree")
@Ok("json")
@SaCheckLogin
@@ -305,42 +314,59 @@ public class SysUnitController {
Cnd cnd = Cnd.NEW();
cnd.asc("unitcode");
List<Sys_unit> list = sysUnitService.query(cnd);
boolean defaultRoot = "0".equals(rootId);
boolean hasDefaultRoot = defaultRoot && list.stream().anyMatch(unit -> rootId.equals(unit.getId()));
List<TreeNode<String>> nodeList = CollUtil.newArrayList();
for (int i = 0; i < list.size(); i++) {
Sys_unit unit = list.get(i);
/*
* 单位根节点存在 id 与 parentId 都为 0 的自引用数据。
* 构建树时把当前查询根挂到虚拟根下,避免“中国地质大学”和 parentId=0 的学院被构造成同级。
*/
String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId();
String parentId = unit.getParentId();
if (rootId.equals(unit.getId()) || (defaultRoot && (StrUtil.isBlank(parentId)
|| (!hasDefaultRoot && rootId.equals(parentId))))) {
parentId = virtualRootId;
}
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(
Map.of(
"unitTypeCode", unit.getUnitTypeCode()
// 历史手工新建单位可能未设置类型,展示时按其他类处理,避免空值使整棵树加载失败。
"unitTypeCode", unit.getUnitTypeCode() == null ? 0 : unit.getUnitTypeCode()
)
));
}
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
return Result.success(treeList);
} catch (Exception e) {
log.error("加载单位树失败", e);
return Result.error();
}
}
/**
* 新建当前单位的下设单位。
*
* @param unit 表单数据;name 为单位名称,unitcode 为唯一编码,unitLevel 为单位等级,unionId 为所属工会;
* unitTypeCode 为部门类别,0 表示其他类、1 表示部门类,未传时由 service 按其他类保存
* @param parentId 当前选中上级单位的 ID,必须对应已存在的单位
* @param req 当前 HTTP 请求
* @return Result 对象;code 为 0 表示保存成功,非 0 表示失败,msg 为提示信息,data 为空
*/
@At
@Ok("json")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("sys.manager.unit.add")
@SLog(tag = "新建单位", msg = "单位名称:${args[0].name}")
public Object addDo(@Param("..") Sys_unit unit, @Param("parentId") String parentId, HttpServletRequest req) {
try {
if ("root".equals(parentId)) {
parentId = "";
if (Strings.isBlank(unit.getName()) || Strings.isBlank(unit.getUnitcode()) || Strings.isBlank(parentId) || "root".equals(parentId)) {
return Result.error("单位名称、单位编码和上级单位不能为空");
}
unit.setCreatedBy(SecurityUtil.getUserId());
sysUnitService.save(unit, parentId);
return Result.success();
} catch (BaseException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
log.error("新建单位失败", e);
return Result.error();
}
}
@@ -52,6 +52,16 @@ public class Sys_dict extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 100)
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
@Comment("启用状态")
@ColDefine(type = ColType.BOOLEAN)
@@ -0,0 +1,37 @@
package com.budwk.app.sys.models;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
@Data
@Table("sys_task_execution")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes(@Index(name = "IDX_SYS_TASK_EXECUTION_STARTED", fields = {"taskId", "startedAt"}))
public class Sys_task_execution {
public enum Status {
RUNNING, SUCCESS, FAILED, SKIPPED, INTERRUPTED
}
@Name
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
private String taskId;
@Column
private long startedAt;
@Column
private Long finishedAt;
@Column
@ColDefine(type = ColType.VARCHAR, width = 16)
private String status;
@Column
@ColDefine(customType = "MEDIUMTEXT")
private String executionLog;
}
@@ -65,6 +65,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("出生日期")
@ColDefine(type = ColType.DATE)
@DataCenterColumn(name = "出生日期", key = "CSRQ")
private Date birthday;
@Column
@@ -97,6 +98,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("证件类型")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "证件类型", key = "SFZJLXDM", dict = "USER_ID_CARD_TYPE")
private String idCardType;
@Column
@@ -114,6 +116,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("学历")
@ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "学历", key = "ZGXLMC")
private String education;
@Column
@@ -125,16 +128,19 @@ public class Sys_user extends BaseModel implements Serializable {
@Comment("学位")
@ColDefine(type = ColType.VARCHAR, width = 50)
// @DataCenterColumn(name = "学位", key = "zgxwmc")
@DataCenterColumn(name = "学位", key = "ZGXWMC")
private String academicDegree;
@Column
@Comment("岗位类别")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "聘岗类型", key = "ZYJSGWLBDMMC")
private String jobCategory;
@Column
@Comment("职称")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "职称", key = "ZYJSZWDMMC")
private String professionalTitle;
@Column
@@ -206,9 +212,9 @@ public class Sys_user extends BaseModel implements Serializable {
private String dutySituation;
@Column
@Comment("教职工类别码")
@Comment("人员类型")
@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;
@Column
@@ -218,8 +224,9 @@ public class Sys_user extends BaseModel implements Serializable {
private Integer pendingConfirmStatus;
@Column
@Comment("编制类别码")
@Comment("编制类别")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "编制类别", key = "BZLBDMMC")
private String preparedBy;
@Column
@@ -290,6 +297,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Comment("是否双肩挑")
@ColDefine(type = ColType.BOOLEAN)
@Default(value = "0")
@DataCenterColumn(name = "是否双肩挑", key = "SFSJT")
private Boolean manyUnit;
@Column
@@ -459,4 +467,13 @@ public class Sys_user extends BaseModel implements Serializable {
@Comment("基金会员退出时间")
@ColDefine(type = ColType.DATETIME)
private Date aidFundMemberQuitTime;
/**
* 人员数据治理同步保护标识:1 表示人工确认后的数据,自动同步时不得覆盖。
*/
@Column
@Comment("人员数据治理同步保护标识")
@ColDefine(type = ColType.INT)
@Default(value = "0")
private Integer flag;
}
@@ -61,7 +61,7 @@ public class Sys_user_role {
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
@Comment("社团ID")
@Comment("协会ID")
private String clubId;
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -1,5 +1,6 @@
package com.budwk.app.sys.models;
import com.budwk.app.sys.annotation.DataCenterColumn;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@@ -31,6 +32,42 @@ public class Sys_user_source extends Sys_user {
@ColDefine(type = ColType.VARCHAR, width = 255)
private String unitName;
@Column
@Comment("最高学历代码")
@ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "最高学历代码", key = "ZGXLDM")
private String educationCode;
@Column
@Comment("最高学位代码")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "最高学位代码", key = "XWDM")
private String academicDegreeCode;
@Column
@Comment("编制类别代码")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "编制类别代码", key = "BZLBDM")
private String preparedByCode;
@Column
@Comment("聘岗类型代码")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "聘岗类型代码", key = "ZYJSGWLBDM")
private String jobCategoryCode;
@Column
@Comment("专业技术职务代码")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "专业技术职务代码", key = "ZYJSZWDM")
private String professionalTitleCode;
@Column
@Comment("当前状态代码")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "当前状态代码", key = "DQZTDM")
private String userStateCode;
@Column
@Comment("拉取时间")
@ColDefine(type = ColType.DATETIME)
@@ -10,6 +10,23 @@ import java.util.List;
* Created by wizzer on 2016/12/23.
*/
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
@@ -15,6 +15,12 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
*/
Date pull();
default NutMap queryUsers(String zgh, int pageNumber, int pageSize) {
return queryUsers(zgh, null, pageNumber, pageSize);
}
NutMap queryUsers(String zgh, String xm, int pageNumber, int pageSize);
/**
* 拉取人员博士后的信息
*/
@@ -7,4 +7,15 @@ import com.budwk.app.sys.param.SysDataUserUpdateParam;
*/
public interface SysDataUserUpdateService {
String update(SysDataUserUpdateParam updateParam);
static boolean qualifiesAsUnionMember(String personType, String userState) {
if (!"在岗".equals(userState) || personType == null) {
return false;
}
return switch (personType) {
case "在编", "兰溪编制", "聘用合同制(享受人事代理待遇)",
"聘用合同制", "聘用制专家", "人事代理" -> true;
default -> false;
};
}
}
@@ -79,6 +79,13 @@ public interface SysDictService extends BaseService<Sys_dict> {
*/
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.sys.models.Sys_home_activity;
import java.util.List;
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);
}
@@ -136,6 +136,9 @@ public interface SysRoleService extends BaseService<Sys_role> {
*/
NutMap settingMemberRole();
/** 为指定人员中 member=1 的用户批量补齐启用的会员角色。 */
NutMap settingMemberRole(List<String> userIds);
/**
* 清空缓存
*/
@@ -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_role;
import com.budwk.app.sys.models.Sys_user;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@@ -155,14 +156,37 @@ public interface SysUserService extends BaseService<Sys_user> {
Sys_user loginByPassword(String loginname, String passowrd) throws BaseException;
/**
* 通过短信验证码登录
* 根据手机号获取唯一可登录用户,并加载所属单位及分工会。
*
* @param mobile 手机号
* @return
* @throws BaseException
* @param mobile 已在系统人员资料中登记的 11 位大陆手机号,允许首尾空格
* @return Sys_user,包含用户基本资料、unit 单位及 union 分工会关联
* @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
@@ -6,8 +6,11 @@ import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.sys.models.Sys_config;
import com.budwk.app.sys.services.SysConfigService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
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.lang.Lang;
import org.nutz.lang.Strings;
@@ -24,6 +27,35 @@ public class SysConfigServiceImpl extends BaseServiceImpl<Sys_config> implements
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() {
return this.query(Cnd.where("delFlag", "=", false));
}
@@ -1,29 +1,28 @@
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.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.config.DataCenterProperties;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.services.SysDataUnitPullService;
import com.budwk.app.sys.utils.DataCenterUtil;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -42,7 +41,7 @@ import java.util.stream.Collectors;
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
private static final String DMP_UNIT_KEY = "unit";
private static final int DMP_PAGE_SIZE = 1000;
private static final int BATCH_SIZE = 500;
@Inject
private DataCenterProperties dataCenterProperties;
@@ -64,58 +63,61 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateUnits(List<String> unitIds) {
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = distinctDmpUnits(pullDmpUnits(getDmpUnitUrl(), accessToken));
List<Sys_unit> localUnits = query(Cnd.NEW());
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()
.map(this::getNormalizedParentCode)
.map(raw -> getNormalizedParentCode(raw, root.getId()))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
rawDataList.stream()
.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());
}
/**
* 获取信息中心开放平台访问令牌。
* 参数来自 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。
@@ -128,108 +130,88 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
return url;
}
/**
* 分页拉取单位接口数据。
* 入参 url 为单位接口地址,accessToken 为信息中心令牌;
* 请求 body 包含 access_token、per_page、page;
* 返回值为 result.data 合并后的单位原始 JSON 列表。
*/
private List<JSONObject> pullDmpUnits(String url, String accessToken) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
int total = 0;
do {
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
log.info("请求信息中心单位数据,第{}页: {}", page, httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求信息中心单位数据第{}页结果: {}", page, resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 10000) {
throw new BaseException("获取信息中心单位数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
}
JSONObject result = jsonBody.getJSONObject("result");
if (result == null) {
break;
}
total = result.getInt("total", total);
maxPage = result.getInt("max_page", maxPage);
JSONArray data = result.getJSONArray("data");
if (CollUtil.isNotEmpty(data)) {
rawDataList.addAll(data.stream().map(v -> (JSONObject) v).toList());
}
log.info("信息中心单位数据拉取进度: {}/{}", rawDataList.size(), total);
page++;
} while (page <= maxPage);
return rawDataList;
}
/**
* 按单位代码去重,避免接口分页或源数据重复导致同一 DWDM 重复写入。
*/
private List<JSONObject> distinctDmpUnits(List<JSONObject> rawDataList) {
Map<String, JSONObject> unitMap = new LinkedHashMap<>();
for (JSONObject raw : rawDataList) {
try {
String unitCode = raw.getStr("DWDM");
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());
}
}
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");
if (StrUtil.isBlank(unitCode)) {
return;
}
String parentId = getParentId(unitCode, getNormalizedParentCode(raw));
Sys_unit unit = fetch(unitCode);
if (unit == null) {
unit = new Sys_unit();
if (root.getId().equals(unitCode)) {
throw new BaseException("接口单位{}与本地顶级机构编号冲突,中止同步", unitCode);
}
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.setUnitcode(unitCode);
unit.setPath(getUnitPath(parentId));
setUnitValue(unit, raw, parentId, parentCodes);
dao().fastInsert(unit);
unit.setPath(getUnitPath(parentId, unitMap));
} else {
setUnitValue(unit, raw, parentId, parentCodes);
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath(parentId));
if (!parentId.equals(existing.getParentId()) || StrUtil.isBlank(existing.getPath())) {
String oldPath = existing.getPath();
String newPath = getUnitPath(parentId, unitMap);
if (StrUtil.isNotBlank(oldPath)) {
if (newPath.startsWith(oldPath)) {
throw new BaseException("单位{}不能移动到自身的下级单位", unitCode);
}
dao().updateIgnoreNull(unit);
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);
}
}
unitMap.put(unitCode, unit);
changedUnits.put(unitCode, unit);
}
/**
* 根据接口字段给系统单位赋值。
* DWYXBS 表示单位是否有效,DWCC 表示信息中心原始单位层次;
* DWCC 表示信息中心原始单位层次;
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
*/
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) {
String unitCode = raw.getStr("DWDM");
String unitName = raw.getStr("DWMC");
String aliasName = StrUtil.blankToDefault(raw.getStr("DWJC"), unitName);
Integer rawUnitLevel = raw.getInt("DWCC");
Integer unitLevel = getNormalizedUnitLevel(raw);
boolean availableBusinessUnit = "是".equals(raw.getStr("DWYXBS")) && Integer.valueOf(2).equals(rawUnitLevel);
unit.setParentId(parentId);
unit.setName(unitName);
@@ -237,48 +219,71 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
unit.setUnitcode(unitCode);
unit.setAddress(raw.getStr("DWDZ"));
unit.setUnitLevel(unitLevel);
unit.setUnitType(raw.getStr("DWLB"));
unit.setUnitTypeCode(availableBusinessUnit ? 1 : 0);
unit.setUnitType(raw.getStr("DWLBDM"));
unit.setUnitTypeCode(1);
unit.setHasChildren(parentCodes.contains(unitCode));
}
/**
* 计算父级单位号。
* LSDWH 为空、等于自身或本地不存在该父级时按根级处理;系统根单位 0 允许直接作为父级。
* 异常父级仅允许沿用本地已修正的关系,并确认父级链最终到达顶级机构。
*/
private String getParentId(String unitCode, String parentCode) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) {
return "";
private String getParentId(String unitCode, String parentCode, String rootId, Map<String, Sys_unit> unitMap) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode) || !unitMap.containsKey(parentCode)) {
Sys_unit existing = unitMap.get(unitCode);
parentCode = existing == null ? null : existing.getParentId();
}
String ancestorId = 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();
}
if ("0".equals(parentCode)) {
return parentCode;
}
Sys_unit parentUnit = fetch(parentCode);
return parentUnit == null ? "" : parentCode;
}
/**
* 根据父级 path 生成当前单位 path。
* 父级为空时生成根节点 path,父级存在时在父级 path 下生成子节点 path。
*/
private String getUnitPath(String parentId) {
private String getUnitPath(String parentId, Map<String, Sys_unit> unitMap) {
String parentPath = "";
if (StrUtil.isNotBlank(parentId)) {
Sys_unit parentUnit = fetch(parentId);
parentPath = parentUnit == null ? "" : parentUnit.getPath();
Sys_unit parentUnit = unitMap.get(parentId);
if (parentUnit == null || StrUtil.isBlank(parentUnit.getPath())) {
throw new BaseException("父机构{}不存在或路径为空", parentId);
}
return getSubPath("sys_unit", "path", parentPath);
parentPath = parentUnit.getPath();
}
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");
if (Integer.valueOf(1).equals(rawUnitLevel)) {
return "0";
return rootId;
}
return raw.getStr("LSDWH");
return raw.getStr("LSDWDM");
}
/**
@@ -133,7 +133,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
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));
// 查询数据源
@@ -164,6 +164,9 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// 创建用户对象
Sys_user u = new Sys_user();
BeanUtils.copyProperties(source, u);
if (SysDataUserUpdateService.qualifiesAsUnionMember(u.getPersonType(), u.getUserState())) {
u.setMember(true);
}
if (user == null) {
// 新增用户,初始化数据
@@ -298,6 +301,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return "更新失败: " + e.getMessage();
}
List<String> updatedUserIds = new ArrayList<>();
needInitUserList.forEach(user -> updatedUserIds.add(user.getId()));
needDoUpdateList.forEach(user -> updatedUserIds.add(user.getId()));
sysRoleService.settingMemberRole(updatedUserIds);
sysRoleService.clearCache();
sysUserService.clearCache();
/* // 4. 异步添加会员角色 - 不等待完成
if (Lang.isNotEmpty(addMemberUserIds)) {
executorService.execute(() -> {
@@ -351,9 +361,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// 6. 处理其他任务
// 6.1 更新提案的校领导角色,发送订阅
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
if (config == null || config.getSchoolLeaderUnitIds() == null || config.getSchoolLeaderUnitIds().isEmpty()) {
log.warn("未配置提案校领导单位,跳过校领导角色刷新,继续完成人员全量更新");
} else {
for (String unitId : config.getSchoolLeaderUnitIds()) {
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
}
}
/*
* 所有异步用户更新完成后再校正待确认状态,避免主事务提前锁定 sys_user,
@@ -3,8 +3,6 @@ package com.budwk.app.sys.services.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
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 com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.event.role.RoleEventMsg;
@@ -15,9 +13,13 @@ import com.budwk.app.sys.models.*;
import com.budwk.app.sys.param.SysDataUserUpdateParam;
import com.budwk.app.sys.services.SysDataUserUpdateService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUnitService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.utils.DataCenterUtil;
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.MemberChangeType;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
@@ -28,18 +30,23 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.ArrayList;
/**
* 增量更新系统用户数据
*/
@IocBean
@Slf4j
public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateService {
@Inject
private Dao dao;
@Inject
private SysRoleService sysRoleService;
@Inject
private SysUnitService sysUnitService;
@Inject
private SysUserService sysUserService;
@Override
@Aop(TransAop.READ_COMMITTED)
@@ -58,9 +65,70 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
// 执行查询
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);
for (Sys_user sysUser : sysUsers) {
if (SysDataUserUpdateService.qualifiesAsUnionMember(sysUser.getPersonType(), sysUser.getUserState())) {
sysUser.setMember(true);
}
//重新设置
sysUser.setId(R.UU32());
String salt = R.UU32();
@@ -72,9 +140,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
}
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();
//等待任务全部完成
AsyncUtil.waitAll(CompletableFuture.allOf(splitSysUsersFutures.toArray(new CompletableFuture[0])));
for (List<Sys_user> batch : splitSysUsers) {
dao.fastInsert(batch);
}
// 增量更新只处理新增人员,需要同步写入用户历史记录;列表的变更类型、变更时间通过 sys_user_history 最新记录展示。
List<Sys_user_history> histories = BeanUtil.copyToList(userSources, Sys_user_history.class);
@@ -85,8 +153,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
}
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();
AsyncUtil.waitAll(CompletableFuture.allOf(splitHistoryFutures.toArray(new CompletableFuture[0])));
for (List<Sys_user_history> batch : splitHistories) {
dao.fastInsert(batch);
}
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
@@ -97,43 +166,20 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
sysUserRole.setRoleId(publicRole.getId());
return sysUserRole;
}).toList();
ThreadUtil.execAsync(() -> {
dao.insert(sysUserRoles);
});
sysRoleService.settingMemberRole(sysUsers.stream().map(Sys_user::getId).toList());
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);
});
sysUserService.clearCache();
// 6.1 更新提案的校领导角色,发送订阅
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
if (config == null || config.getSchoolLeaderUnitIds() == null || config.getSchoolLeaderUnitIds().isEmpty()) {
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()) {
return "本次没有新增人员或单位。";
@@ -29,6 +29,7 @@ import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.utils.DataCenterUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.nutz.aop.interceptor.async.Async;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
@@ -97,7 +98,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
synchronized (SysDataUserPullServiceImpl.class) {
if (fieldMappings == null) {
List<FieldMapping> mappings = new ArrayList<>();
Field[] fields = Sys_user.class.getDeclaredFields();
Field[] fields = cn.hutool.core.util.ReflectUtil.getFields(Sys_user_source.class);
for (Field field : fields) {
DataCenterColumn annotation = field.getAnnotation(DataCenterColumn.class);
if (annotation != null) {
@@ -120,14 +121,24 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 字典码表Map,key为父级编码,value为子级字典列表
Map<String, List<Sys_data_dict>> dataDictMap = dataDictList.stream().collect(Collectors.groupingBy(Sys_data_dict::getParentCode));
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = new ArrayList<>();
if (dataCenterProperties.isTokenEnabled()) {
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);
if (rawDataList.isEmpty()) {
log.warn("当前未获取到人员数据");
throw new BaseException("当前未获取到人员数据,中止本次同步");
}
Date nowDate = new Date();
@@ -165,6 +176,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
tertiaryParentUnitIds.put(sourceUnitId, parentUnit.getId());
}
Map<String, Sys_unit> resolvedUnitMap = unitMap;
// 赋值
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
Sys_user_source sysUser = new Sys_user_source();
@@ -182,38 +194,55 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
.findFirst().orElse(new Sys_data_dict());
mapping.field.set(sysUser, StrUtil.blankToDefault(sysDataDict.getName(), raw.getStr(mapping.key)));
} else {
// 特殊处理 根据身份证号获取性别和出生年月
if ("SFZJH".equals(mapping.key)) {
String idCard = raw.getStr(mapping.key);
mapping.field.set(sysUser, idCard);
// 设置出生年月
try {
sysUser.setBirthday(IdcardUtil.getBirthDate(idCard));
} catch (Exception e) {
sysUser.setBirthday(null);
}
} else if ("LXNY".equals(mapping.key)) {
if ("LXNY".equals(mapping.key)) {
mapping.field.set(sysUser, normalizeArrivalAtSchoolDate(raw.getStr(mapping.key)));
} else {
mapping.field.set(sysUser, raw.getStr(mapping.key));
}
}
} else if (mapping.field.getType() == Boolean.class) {
mapping.field.set(sysUser, raw.getBool(mapping.key));
} 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) {
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"));
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);
if(StringUtils.isNotBlank(sysUser.getPersonType()) && sysUser.getPersonType().equals("教职工")){
sysUser.setUserAttribute("新增“教职工”");
}
if(StringUtils.isNotBlank(sysUser.getPersonType()) && sysUser.getPersonType().equals("劳务派遣人员")){
sysUser.setUserAttribute("劳务派遣人员");
}
return sysUser;
}).toList();
@@ -248,7 +277,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
String[] parts = dateText.split("-");
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));
}
/**
@@ -304,12 +334,50 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
return url;
}
@Override
public NutMap queryUsers(String zgh, String xm, int pageNumber, int pageSize) {
if (pageNumber < 1 || pageSize < 1 || pageSize > 200) {
throw new BaseException("分页参数不合法");
}
String jobNumber = StrUtil.trim(zgh);
String name = StrUtil.trim(xm);
boolean tokenEnabled = dataCenterProperties.isTokenEnabled();
if (!tokenEnabled && StrUtil.isBlank(name)) {
JSONObject response = DataCenterUtil.queryPage(getDmpUrl(DMP_TEACHER_KEY), jobNumber, pageNumber, pageSize);
JSONObject pageInfo = response.getJSONObject("pageInfo");
JSONArray data = response.getJSONArray("dataSet");
if (pageInfo == null || pageInfo.getLong("total") == null || data == null) {
throw new BaseException("数据中心接口未返回有效的分页数据");
}
return NutMap.NEW().addv("list", data).addv("totalCount", pageInfo.getLong("total"));
}
List<JSONObject> records = new ArrayList<>();
if (tokenEnabled) {
String accessToken = getDmpAccessToken();
records.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工", jobNumber));
records.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣", jobNumber));
} else {
Map<String, String> params = StrUtil.isBlank(jobNumber) ? Map.of() : Map.of("zgh", jobNumber);
records = DataCenterUtil.pullData(getDmpUrl(DMP_TEACHER_KEY), params);
}
if (StrUtil.isNotBlank(name)) {
records.removeIf(record -> !StringUtils.contains(record.getStr("XM"), name));
}
int from = (int) Math.min((long) (pageNumber - 1) * pageSize, records.size());
return NutMap.NEW().addv("list", records.subList(from, Math.min(from + pageSize, records.size())))
.addv("totalCount", records.size());
}
/**
* 分页拉取单个人员接口数据。
* 入参 url 为人员接口地址,accessToken 为信息中心令牌,sourceName 用于日志区分接口来源。
* 返回值为该接口所有分页合并后的原始人员 JSON 列表。
*/
private List<JSONObject> pullDmpUsers(String url, String accessToken, String sourceName) {
return pullDmpUsers(url, accessToken, sourceName, null);
}
private List<JSONObject> pullDmpUsers(String url, String accessToken, String sourceName, String zgh) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
@@ -319,11 +387,14 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 信息中心接口单页最多返回1000条,按page循环拉完当前接口全部数据。
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
Map<String, Object> body = new HashMap<>();
body.put("access_token", accessToken);
body.put("per_page", String.valueOf(DMP_PAGE_SIZE));
body.put("page", String.valueOf(page));
if (StrUtil.isNotBlank(zgh)) {
body.put("zgh", zgh);
}
httpRequest.body(JSONUtil.toJsonStr(body));
log.info("请求{}人员数据,第{}页: {}", sourceName, page, httpRequest);
String resBody = httpRequest.execute().body();
@@ -362,6 +433,9 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
if (StrUtil.isBlank(loginName)) {
loginName = raw.getStr("ID");
}
if (StrUtil.isBlank(raw.getStr("ZGH"))) {
throw new BaseException("人员接口存在缺少职工号 ZGH 的记录");
}
userMap.putIfAbsent(loginName, raw);
}
return new ArrayList<>(userMap.values());
@@ -27,6 +27,9 @@ import java.util.Map;
@IocBean(args = {"refer:dao"})
@CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "sys_dict", isHash = true)
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) {
super(dao);
}
@@ -132,18 +135,76 @@ public class SysDictServiceImpl extends BaseServiceImpl<Sys_dict> implements Sys
@Aop(TransAop.READ_COMMITTED)
public void save(Sys_dict dict, String pid) {
String path = "";
Sys_dict parentDict = null;
if (!Strings.isEmpty(pid)) {
Sys_dict pp = this.fetch(pid);
path = pp.getPath();
parentDict = this.fetch(pid);
path = parentDict.getPath();
}
dict.setPath(getSubPath("sys_dict", "path", path));
dict.setParentId(pid);
validateUserAttribute(dict, parentDict);
dao().insert(dict);
if (!Strings.isEmpty(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)
public void saveByParentCode(Sys_dict dict, String parentCode) {
Sys_dict parentDict = fetch(Cnd.where("code", "=", parentCode));
@@ -1,15 +1,87 @@
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.sys.models.Sys_home_activity;
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.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.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"})
public class SysHomeActivityServiceImpl extends BaseServiceImpl<Sys_home_activity> implements SysHomeActivityService {
@Inject
private ActivityBasicScopeService activityBasicScopeService;
public SysHomeActivityServiceImpl(Dao 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.pubsub.PubSubService;
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.IocBean;
import org.nutz.json.Json;
@@ -62,6 +63,8 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Inject
private SmsService smsService;
@Inject
private PropertiesProxy conf;
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
@@ -106,8 +109,11 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
ThreadUtil.execute(() -> {
for (String loginName : loginNames) {
smsService.sendSmsByAccount(loginName,sysMsg.getNote());
if (!(Strings.isNotBlank(conf.get("custom.msg.platform.app-id"))
&& conf.getBoolean("msg.platform.IntelligenceMode", false))) {
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote());
}
}
});
@@ -294,6 +294,27 @@ public class SysRoleServiceImpl extends BaseServiceImpl<Sys_role> implements Sys
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap settingMemberRole() {
return settingMemberRole(Cnd.where("u.member", "=", 1));
}
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap settingMemberRole(List<String> userIds) {
int addedCount = 0;
int enabledCount = 0;
if (userIds != null && !userIds.isEmpty()) {
List<String> distinctIds = userIds.stream().distinct().toList();
for (int start = 0; start < distinctIds.size(); start += 500) {
NutMap counts = settingMemberRole(Cnd.where("u.member", "=", 1)
.and("u.id", "in", distinctIds.subList(start, Math.min(start + 500, distinctIds.size()))));
addedCount += counts.getInt("addedCount");
enabledCount += counts.getInt("enabledCount");
}
}
return NutMap.NEW().addv("addedCount", addedCount).addv("enabledCount", enabledCount);
}
private NutMap settingMemberRole(Cnd condition) {
// 锁定同一会员角色,使本接口的并发请求依次执行,避免重复插入用户角色关系。
Sql roleSql = Sqls.create("SELECT * FROM sys_role WHERE code=@code FOR UPDATE");
roleSql.setParam("code", RoleConstant.MEMBER.name());
@@ -312,14 +333,16 @@ public class SysRoleServiceImpl extends BaseServiceImpl<Sys_role> implements Sys
// 历史关联可能已停用或未填写 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)");
+ "AND EXISTS (SELECT 1 FROM sys_user u $condition AND u.id=sys_user_role.userId)");
enableSql.setCondition(condition);
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 "
+ "SELECT u.id,@roleId,1 FROM sys_user u $condition "
+ "AND NOT EXISTS (SELECT 1 FROM sys_user_role ur WHERE ur.userId=u.id AND ur.roleId=@roleId)");
insertSql.setCondition(condition);
insertSql.setParam("roleId", role.getId());
dao().execute(insertSql);
return NutMap.NEW().addv("addedCount", insertSql.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.SysUserService;
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.zhgh.staffmanage.specialstaff.model.SpecialStaff;
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.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.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
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.CacheRemove;
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
@@ -63,6 +66,10 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
private SLogService sLogService;
@Inject
private RedisService redisService;
@Inject
private ValidateService validateService;
@Inject
private PropertiesProxy conf;
@Override
@CacheResult(cacheKey = "${userId}_getPermissionList")
@@ -355,11 +362,10 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
// base64解码密码
String decodePwd = Base64Decoder.decodeStr(passowrd);
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
if (Globals.sso) {
if (!"dev".equals(conf.get("nutz.profiles.active"))
&& !Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
throw new BaseException("用户名或者密码不正确");
}
}
user = this.fetchLinks(user, "unit");
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
Sys_union union = dao().fetch(Sys_union.class, user.getUnit().getUnionId());
@@ -368,16 +374,90 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
return user;
}
/**
* 按登记手机号确认唯一且未禁用的账号,补齐后续登录会话使用的组织关系。
*
* @param mobile 已登记的 11 位大陆手机号,查询前移除首尾空格
* @return Sys_user,包含 unit 单位和按单位关联的 union 分工会信息
* @throws BaseException 手机号无效、未登记、关联多条用户或账号已禁用
*/
@Override
public Sys_user loginByMobile(String mobile) throws BaseException {
Sys_user user = this.fetch(Cnd.where("mobile", "=", mobile));
if (user == null) {
throw new BaseException("用户不存在");
String normalizedMobile = Strings.sNull(mobile).trim();
if (!normalizedMobile.matches("^1[3-9]\\d{9}$")) {
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");
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
user.setUnion(dao().fetch(Sys_union.class, user.getUnit().getUnionId()));
}
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
public Sys_user loginByLoginName(String loginname) {
Sys_user user = this.fetch(Cnd.where("loginname", "=", loginname));
@@ -446,6 +526,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
}
@Override
@Aop(TransAop.READ_COMMITTED)
public String loginPlus(Sys_user user, LoginType loginType, HttpServletRequest request) {
String wxOpenId = request.getParameter("wxOpenId");
@@ -3,9 +3,19 @@ package com.budwk.app.sys.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassUtil;
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.JSONUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.models.Sys_unit;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@@ -17,6 +27,68 @@ import java.util.Map;
*/
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) {
return pullData(url, Map.of("", ""));
}
public static List<JSONObject> pullData(String url, Map<String, ?> params) {
JSONObject firstResponse = requestData(url, params);
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++) {
Map<String, Object> body = new HashMap<>(params);
body.put("pageNum", page);
JSONObject response = requestData(url, body);
JSONArray data = response.getJSONArray("dataSet");
if (data == null) {
throw new BaseException("数据中心接口第{}页缺少 dataSet", page);
}
records.addAll(data.toList(JSONObject.class));
}
return records;
}
public static JSONObject queryPage(String url, String zgh, int pageNumber, int pageSize) {
Map<String, Object> body = new HashMap<>();
body.put("pageNum", pageNumber);
body.put("pageSize", pageSize);
if (StrUtil.isNotBlank(zgh)) {
body.put("zgh", zgh.trim());
}
return requestData(url, body);
}
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
@@ -1,62 +1,50 @@
package com.budwk.app.task.job;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.ConditionGroup;
import com.budwk.app.sys.enums.SysDataUpdateMode;
import com.budwk.app.sys.models.Sys_user_source;
import com.budwk.app.sys.param.SysDataUserUpdateParam;
import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysDataUserUpdateService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.Ioc;
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.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;
/**
* 人员全量更新任务
*/
@IocBean
@Slf4j
@DisallowConcurrentExecution
public class SysUserAllRenewJob implements Job {
@Inject
private Dao dao;
@Inject("refer:$ioc")
private Ioc ioc;
@Inject
private SysDataUserPullService sysDataUserPullService;
@Inject
private SysDataUserUpdateService sysDataUserUpdateService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Date pullTime = sysDataUserPullService.pull();
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
NutMap conditionGroupMap = (NutMap)jobDataMap.get("conditionGroup");
if(conditionGroupMap == null || conditionGroupMap.isEmpty()){
log.error("SysUserAllRenewJob定时任务执行中止,原因:conditionGroup为null");
}
String conditionGroupStr = Json.toJson(conditionGroupMap).replace("@currentDate", DateUtil.today());
ConditionGroup conditionGroup = null;
try {
conditionGroup = Json.fromJson(ConditionGroup.class, conditionGroupStr);
} catch (Exception e) {
log.error("SysUserAllRenewJob定时任务执行失败,原因:{}", e.getMessage());
SysDataUserUpdateParam param = new SysDataUserUpdateParam();
param.setUpdateMode(SysDataUpdateMode.ALL.name());
Object conditionGroup = jobExecutionContext.getMergedJobDataMap().get("conditionGroup");
if (conditionGroup != null) {
String conditionGroupJson = Json.toJson(conditionGroup).replace("@currentDate", DateUtil.today());
param.setConditionGroup(Json.fromJson(ConditionGroup.class, conditionGroupJson));
}
SysDataUserUpdateParam param = new SysDataUserUpdateParam();
param.setPullTime(DateUtil.formatDateTime(pullTime));
param.setUpdateMode(SysDataUpdateMode.ALL.name());
param.setConditionGroup(conditionGroup);
// sysDataUserUpdateService.update(param);
param.setPullTime(DateUtil.formatDateTime(sysDataUserPullService.pull()));
SysDataUserUpdateService updateService = (SysDataUserUpdateService) ioc.get(SysDataUpdateMode.ALL.getClazz());
log.info("人员全量更新任务,拉取批次 {}:{}", param.getPullTime(), updateService.update(param));
} catch (Exception e) {
throw new JobExecutionException("人员拉取及全量更新任务执行失败", e);
}
}
}
@@ -27,6 +27,6 @@ public class TestJob implements Job {
String taskId = context.getJobDetail().getKey().getName();
String hi = data.getString("hi");
log.info("Test Job hi::" + hi);
sysTaskService.update(Chain.make("exeAt", (int) (System.currentTimeMillis() / 1000)).add("exeResult", "执行成功"), Cnd.where("id", "=", taskId));
sysTaskService.update(Chain.make("exeResult", "执行成功"), Cnd.where("id", "=", taskId));
}
}
@@ -17,7 +17,7 @@ import org.quartz.JobExecutionException;
* @Author JyuHsin
* @Date 2026/1/22 16:56
* @Version 1.0
* @Description 每年的1月1日生成社团成员当年的缴费记录
* @Description 每年的1月1日生成协会成员当年的缴费记录
*/
@IocBean
@DisallowConcurrentExecution
@@ -0,0 +1,108 @@
package com.budwk.app.task.log;
import com.budwk.app.sys.models.Sys_task;
import com.budwk.app.sys.models.Sys_task_execution;
import com.budwk.app.sys.models.Sys_task_execution.Status;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Trans;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.listeners.JobListenerSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.UUID;
@IocBean
public class TaskExecutionListener extends JobListenerSupport {
private static final Logger log = LoggerFactory.getLogger(TaskExecutionListener.class);
@Inject
private Dao dao;
@Inject
private Scheduler scheduler;
public void register() throws SchedulerException {
if (!dao.exists(Sys_task_execution.class)) {
dao.create(Sys_task_execution.class, false);
}
dao.update(Sys_task_execution.class, Chain.make("status", Status.INTERRUPTED.name()),
Cnd.where("status", "=", Status.RUNNING.name()));
scheduler.getListenerManager().addJobListener(this);
}
@Override
public String getName() {
return "sysTaskExecutionListener";
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
JobKey key = context.getJobDetail().getKey();
context.put(getName(), null);
if (!key.getName().equals(key.getGroup())) {
return;
}
try {
if (dao.fetch(Sys_task.class, key.getName()) == null) {
return;
}
Sys_task_execution execution = new Sys_task_execution();
execution.setId(UUID.randomUUID().toString().replace("-", ""));
execution.setTaskId(key.getName());
execution.setStartedAt(System.currentTimeMillis());
execution.setStatus(Status.RUNNING.name());
Trans.exec(() -> {
dao.insert(execution);
long startedAtSeconds = execution.getStartedAt() / 1000;
dao.execute(Sqls.create("UPDATE sys_task SET exeAt = @startedAt WHERE id = @id AND (exeAt IS NULL OR exeAt < @startedAt)")
.setParam("startedAt", startedAtSeconds).setParam("id", execution.getTaskId()));
});
context.put(getName(), execution.getId());
context.setResult(null);
TaskExecutionLogAppender.begin();
log.info("开始执行任务 {}", key.getName());
} catch (RuntimeException e) {
log.error("记录任务开始时间失败,任务 {}", key, e);
}
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
String id = (String) context.get(getName());
if (id == null) {
return;
}
Status status = exception != null ? Status.FAILED
: context.getResult() instanceof Status result ? result : Status.SUCCESS;
String executionLog = TaskExecutionLogAppender.end("\n任务执行结束,状态 " + status + "\n");
try {
if (exception != null) {
StringWriter stack = new StringWriter();
exception.printStackTrace(new PrintWriter(stack));
String detail = stack.toString();
if (detail.length() > 8000) {
int end = Character.isHighSurrogate(detail.charAt(7999)) ? 7999 : 8000;
detail = detail.substring(0, end) + "\n[异常堆栈已截断]";
}
executionLog += (exception.refireImmediately() ? "Quartz 将立即重试\n" : "") + detail;
}
dao.update(Sys_task_execution.class,
Chain.make("status", status.name()).add("finishedAt", System.currentTimeMillis())
.add("executionLog", executionLog),
Cnd.where("id", "=", id));
} catch (RuntimeException e) {
log.error("保存任务执行结果失败,记录 {}", id, e);
}
}
}
@@ -0,0 +1,49 @@
package com.budwk.app.task.log;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class TaskExecutionLogAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private static final int MAX_LENGTH = 16000;
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
.withZone(ZoneId.of("Asia/Shanghai"));
private static final ThreadLocal<StringBuilder> CURRENT_LOG = new ThreadLocal<>();
static void begin() {
CURRENT_LOG.set(new StringBuilder());
}
static String end(String outcome) {
StringBuilder log = CURRENT_LOG.get();
CURRENT_LOG.remove();
return (log == null ? "" : log.toString()) + outcome;
}
@Override
protected void append(ILoggingEvent event) {
StringBuilder log = CURRENT_LOG.get();
if (log == null || log.length() >= MAX_LENGTH || !event.getLevel().isGreaterOrEqual(Level.INFO)) {
return;
}
log.append(TIME_FORMAT.format(Instant.ofEpochMilli(event.getTimeStamp())))
.append(" ").append(event.getLevel()).append(" ").append(event.getLoggerName())
.append(" - ").append(event.getFormattedMessage()).append('\n');
if (event.getThrowableProxy() != null) {
log.append(ThrowableProxyUtil.asString(event.getThrowableProxy()));
}
if (log.length() >= MAX_LENGTH) {
int end = MAX_LENGTH;
if (Character.isHighSurrogate(log.charAt(end - 1))) {
end--;
}
log.setLength(end);
log.append("\n[后续日志已截断]\n");
}
}
}
@@ -1,5 +1,7 @@
package com.budwk.app.task.services;
import com.budwk.app.sys.models.Sys_task;
import java.util.List;
/**
@@ -15,6 +17,10 @@ public interface TaskPlatformService {
*/
boolean isExist(String jobName, String jobGroup);
void runOnce(Sys_task task) throws Exception;
void restore(Sys_task task) throws Exception;
/**
* 添加新任务
*
@@ -1,28 +1,45 @@
package com.budwk.app.task.services.impl;
import com.budwk.app.sys.models.Sys_task;
import com.budwk.app.task.services.TaskPlatformService;
import org.nutz.integration.quartz.QuartzJob;
import org.nutz.integration.quartz.QuartzManager;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.TriggerUtils;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
/**
* Created by wizzer on 2018/3/19.
*/
@IocBean
public class TaskPlatformServiceImpl implements TaskPlatformService {
private static final TimeZone TASK_TIME_ZONE = TimeZone.getTimeZone("Asia/Shanghai");
@Inject
private QuartzManager quartzManager;
@Inject
private Scheduler scheduler;
/**
* 判断任务是否存在
@@ -35,6 +52,40 @@ public class TaskPlatformServiceImpl implements TaskPlatformService {
return quartzManager.exist(new JobKey(jobName, jobGroup));
}
@Override
public void runOnce(Sys_task task) throws Exception {
JobKey key = JobKey.jobKey(task.getId(), task.getId());
if (scheduler.checkExists(key)) {
scheduler.triggerJob(key);
return;
}
JobDetail job = buildJob(key, task.getJobClass(), task.getData());
scheduler.scheduleJob(job, TriggerBuilder.newTrigger().forJob(key).startNow().build());
}
private JobDetail buildJob(JobKey key, String className, String dataMap) {
Class<? extends Job> jobClass;
try {
jobClass = Class.forName(Strings.sNull(className).trim(), false, getClass().getClassLoader())
.asSubclass(Job.class);
} catch (ClassNotFoundException | ClassCastException e) {
throw new IllegalArgumentException("执行类必须是有效的 Quartz Job", e);
}
JobDataMap data = new JobDataMap();
if (Strings.isNotBlank(dataMap)) {
try {
NutMap params = Json.fromJson(NutMap.class, dataMap);
if (params != null) {
data.putAll(params);
}
} catch (RuntimeException e) {
throw new IllegalArgumentException("执行参数必须是有效的 JSON 对象", e);
}
}
return JobBuilder.newJob(jobClass).withIdentity(key).usingJobData(data).build();
}
/**
* 添加新任务
*
@@ -46,14 +97,55 @@ public class TaskPlatformServiceImpl implements TaskPlatformService {
* @param dataMap
*/
public void add(String jobName, String jobGroup, String className, String cron, String comment, String dataMap) {
QuartzJob qj = new QuartzJob();
qj.setJobName(jobName);
qj.setJobGroup(jobGroup);
qj.setClassName(className);
qj.setCron(cron);
qj.setComment(comment);
qj.setDataMap(dataMap);
quartzManager.add(qj);
JobKey key = JobKey.jobKey(jobName, jobGroup);
JobDetail job = buildJob(key, className, dataMap);
CronExpression expression = parseCron(cron);
if (expression.getNextValidTimeAfter(new Date()) == null) {
throw new IllegalArgumentException("Cron 表达式没有未来执行时间");
}
schedule(job, expression);
}
@Override
public void restore(Sys_task task) throws SchedulerException {
JobKey key = JobKey.jobKey(task.getId(), task.getId());
if (task.isDisabled()) {
scheduler.deleteJob(key);
return;
}
CronExpression expression = parseCron(task.getCron());
JobDetail job = buildJob(key, task.getJobClass(), task.getData());
JobDetail existingJob = scheduler.getJobDetail(key);
if (existingJob != null
&& existingJob.getJobClass().equals(job.getJobClass())
&& existingJob.getJobDataMap().equals(job.getJobDataMap())
&& scheduler.getTrigger(TriggerKey.triggerKey(key.getName(), key.getGroup())) instanceof CronTrigger trigger
&& trigger.getCronExpression().equals(expression.getCronExpression())
&& trigger.getTimeZone().hasSameRules(TASK_TIME_ZONE)
&& trigger.getNextFireTime() != null) {
scheduler.resetTriggerFromErrorState(trigger.getKey());
return;
}
if (expression.getNextValidTimeAfter(new Date()) != null) {
schedule(job, expression);
} else if (existingJob != null) {
scheduler.deleteJob(key);
}
}
private void schedule(JobDetail job, CronExpression expression) {
JobKey key = job.getKey();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(key.getName(), key.getGroup())
.forJob(key)
.withSchedule(CronScheduleBuilder.cronSchedule(expression))
.build();
try {
scheduler.scheduleJob(job, Set.of(trigger), true);
} catch (SchedulerException e) {
throw new IllegalStateException("Quartz 调度注册失败", e);
}
}
/**
@@ -64,10 +156,7 @@ public class TaskPlatformServiceImpl implements TaskPlatformService {
* @return
*/
public boolean delete(String jobName, String jobGroup) {
QuartzJob qj = new QuartzJob();
qj.setJobName(jobName);
qj.setJobGroup(jobGroup);
return quartzManager.delete(qj);
return quartzManager.delete(JobKey.jobKey(jobName, jobGroup));
}
/**
@@ -83,36 +172,40 @@ public class TaskPlatformServiceImpl implements TaskPlatformService {
* @param cronExpression
* @return
*/
public List<String> getCronExeTimes(String cronExpression) throws Exception {
List<String> list = new ArrayList<>();
CronTriggerImpl cronTriggerImpl = new CronTriggerImpl();
cronTriggerImpl.setCronExpression(cronExpression);
Calendar calendar = Calendar.getInstance();
Date now = calendar.getTime();
calendar.add(2, 1);
List<Date> dates = TriggerUtils.computeFireTimesBetween(cronTriggerImpl, null, now, calendar.getTime());
@Override
public List<String> getCronExeTimes(String cronExpression) {
CronExpression expression = parseCron(cronExpression);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < dates.size() && i <= 4; ++i) {
list.add(dateFormat.format((Date) dates.get(i)));
dateFormat.setTimeZone(TASK_TIME_ZONE);
List<String> times = new ArrayList<>(5);
Date time = new Date();
for (int i = 0; i < 5; i++) {
time = expression.getNextValidTimeAfter(time);
if (time == null) {
break;
}
return list;
times.add(dateFormat.format(time));
}
return times;
}
@Override
public List<String> getCronExeTimesPlus(String cronExpression) throws Exception {
// 创建一个列表来存储执行时间
List<String> executionTimes = new ArrayList<>();
// 创建 CronTriggerImpl 对象,并设置 cron 表达式
CronTriggerImpl cronTrigger = new CronTriggerImpl();
cronTrigger.setCronExpression(new CronExpression(cronExpression));
// 计算未来的执行时间
List<Date> nextExecutionTimes = TriggerUtils.computeFireTimes(cronTrigger, null, 5);
// 格式化日期并添加到执行时间列表中
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (Date date : nextExecutionTimes) {
executionTimes.add(dateFormat.format(date));
public List<String> getCronExeTimesPlus(String cronExpression) {
return getCronExeTimes(cronExpression);
}
private CronExpression parseCron(String cron) {
String normalized = Strings.sNull(cron).trim().replaceAll("\\s+", " ");
int fieldCount = normalized.split(" ").length;
if (fieldCount < 6 || fieldCount > 7) {
throw new IllegalArgumentException("Quartz Cron 表达式必须包含 6 或 7 个字段");
}
try {
CronExpression expression = new CronExpression(normalized);
expression.setTimeZone(TASK_TIME_ZONE);
return expression;
} catch (ParseException e) {
throw new IllegalArgumentException("无效的 Quartz Cron 表达式:" + e.getMessage(), e);
}
return executionTimes;
}
}
@@ -3,19 +3,30 @@ package com.budwk.app.web.commons.auth.service;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.exception.BaseException;
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 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.ioc.aop.Aop;
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.lang.random.R;
import org.nutz.lang.util.NutMap;
import redis.clients.jedis.Jedis;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
/**
* @author wizzer@qq.com
@@ -23,14 +34,45 @@ import java.util.Base64;
@IocBean
@Slf4j
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
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() {
String uuid = R.UU32();
ArithmeticCaptcha captcha = new ArithmeticCaptcha(120, 40);
@@ -41,21 +83,66 @@ public class ValidateService {
}
/**
* 发送短信验证码
* 校验发送条件后发送登录短信,只有腾讯云确认发送成功才写入有效验证码。
*
* @param mobile 手机号码
* @throws BaseException
* @param mobile 已由用户 service 确认唯一绑定、未禁用的 11 位大陆手机号
* @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 {
String text = R.captchaNumber(4);
String codeFromRedis = redisService.get(RedisConstant.UCENTER_SMSCODE + mobile + ":LOCK");
if (Strings.isNotBlank(codeFromRedis)) {
throw new BaseException("请1分钟之后再试");
@Aop(TransAop.READ_COMMITTED)
public NutMap sendLoginSmsCode(String mobile, String captchaKey, String captchaCode, String clientIp, String sessionId) {
if (mobile == null || !mobile.matches("^1[3-9]\\d{9}$") || Strings.isBlank(clientIp)) {
throw new BaseException("手机号或请求来源不正确");
}
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 验证码的值
* @throws BaseException
*/
@Aop(TransAop.READ_COMMITTED)
public void checkCode(String key, String code) throws BaseException {
String codeFromRedis = redisService.get(RedisConstant.UCENTER_CAPTCHA + key);
@@ -81,27 +169,34 @@ public class ValidateService {
}
/**
* 核验短信验证码
* 按服务端收到的手机号核验登录短信验证码,成功后原子删除,输错五次作废。
*
* @param mobile 手机号码
* @param code 验证码值
* @throws BaseException
* @param mobile 本次登录的 11 位大陆手机号,与发送时的手机号完全一致
* @param code 用户输入的六位数字短信验证码
* @throws BaseException 验证码为空、格式错误、不匹配、过期或已使用
*/
public void checkSMSCode(String mobile, String code) throws BaseException {
String codeFromRedis = redisService.get(RedisConstant.UCENTER_SMSCODE + mobile);
if (Strings.isBlank(code)) {
throw new BaseException("请输入短信验证码");
@Aop(TransAop.READ_COMMITTED)
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}$")) {
throw new BaseException("请输入正确的手机号和六位短信验证码");
}
if (Strings.isEmpty(codeFromRedis)) {
throw new BaseException("短信验证码已过期");
String mobileKey = RedisConstant.LOGIN_SMSCODE + "{" + mobile + "}";
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 (!Strings.equalsIgnoreCase(code, codeFromRedis)) {
if (result == -2) {
throw new BaseException("短信验证码错误次数过多,请重新获取");
}
if (result != 1) {
throw new BaseException("短信验证码不正确");
}
redisService.del(RedisConstant.UCENTER_SMSCODE + mobile);
}
}
@Aop(TransAop.READ_COMMITTED)
public String decryptPwd(String keyId, String password) throws Exception {
// 从 Redis 获取私钥
String rsaKey = RedisConstant.RSA_KEY_PREFIX + keyId;
@@ -56,7 +56,7 @@ public class SecurityUtil {
}
/**
* 获取当前用户社团ID
* 获取当前用户协会ID
*/
public static List<String> getClubIds() {
String clubIds = getSession().get("clubIds").toString();
@@ -16,6 +16,8 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import java.util.HashMap;
import java.util.List;
@@ -25,6 +27,7 @@ import java.util.Set;
@IocBean(create = "init")
public class Globals {
private static final Log log = Logs.get();
//项目路径
public static String AppRoot = "";
//项目目录
@@ -69,15 +72,12 @@ public class Globals {
}
public void initTask(SysTaskService sysTaskService) {
taskPlatformService.clear();
List<Sys_task> taskList = sysTaskService.query();
for (Sys_task sysTask : taskList) {
try {
if (!sysTask.isDisabled())//不存在则新建
taskPlatformService.add(sysTask.getId(), sysTask.getId(), sysTask.getJobClass(), sysTask.getCron()
, sysTask.getNote(), sysTask.getData());
taskPlatformService.restore(sysTask);
} catch (Exception e) {
e.printStackTrace();
log.error("恢复定时任务失败:" + sysTask.getName() + " (" + sysTask.getId() + ")", e);
}
}
}
@@ -3,6 +3,7 @@ package com.budwk.app.web.commons.ext.handler;
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.Executor;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardThreadExecutor;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.valves.ErrorReportValve;
@@ -51,6 +52,11 @@ public class WkTomcatConfiguration implements NbAppEventListener {
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
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.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
@@ -10,7 +10,6 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.plugins.mvc.websocket.AbstractWsEndpoint;
import org.nutz.plugins.mvc.websocket.NutWsConfigurator;
import org.nutz.plugins.mvc.websocket.WsHandler;
import redis.clients.jedis.*;
@@ -21,7 +20,7 @@ import javax.websocket.server.ServerEndpoint;
import java.util.ArrayList;
import java.util.List;
@ServerEndpoint(value = "/websocket", configurator = NutWsConfigurator.class)
@ServerEndpoint(value = "/websocket", configurator = WkWsConfigurator.class)
@IocBean(create = "init") // 使用NutWsConfigurator的必备条件
public class WkWebSocket extends AbstractWsEndpoint implements PubSub {
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 {
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) {
// //判断cas里面是否有用户名 那么造成的原因就是系统中没他的信息
// //就是cas已经认证了 但是本地系统没有认证
@@ -188,7 +197,10 @@ public class WkFailProcessor extends ViewProcessor {
if (Globals.sso) {
//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();
// if (StrUtil.isNotBlank(token)) {
@@ -203,7 +215,7 @@ public class WkFailProcessor extends ViewProcessor {
// 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 authUrl = propertiesProxy.get("wecom.wecom-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
log.info("auth:"+authUrl);
@@ -115,7 +115,7 @@ public class CommonController {
@At
@SaCheckLogin
@ApiOperation("社团列表")
@ApiOperation("协会列表")
public Result clubOptions(String clubId) {
List<SysClub> sysClubs = sysClubService.dao().query(SysClub.class,
Cnd.NEW().andEX("id", "=", clubId));
@@ -355,7 +355,7 @@ public class ActivityBasicScopeController {
boolean isBranchUnionChairman = AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name());
boolean isClubPresident = AuthUtil.hasRole(RoleConstant.CLUB_PRESIDENT.name());
if (isBranchUnionChairman && isClubPresident) {
// 同时具备分工会主席和社团会长身份时,未额外筛选前默认查看两类权限范围的并集。
// 同时具备分工会主席和协会会长身份时,未额外筛选前默认查看两类权限范围的并集。
List<String> manageClubIdList = sysClubService.getMyManageClub().stream().map(SysClub::getId).toList();
SqlExpressionGroup scopeGroup = new SqlExpressionGroup();
scopeGroup.or("u.unionid", "=", SecurityUtil.getUnionId());
@@ -364,7 +364,7 @@ public class ActivityBasicScopeController {
}
cnd.and(scopeGroup);
} else if (isClubPresident) {
// 社团会长设置活动人员范围时,只允许查看自己可管理社团下的成员。
// 协会会长设置活动人员范围时,只允许查看自己可管理协会下的成员。
List<String> manageClubIdList = sysClubService.getMyManageClub().stream().map(SysClub::getId).toList();
if (manageClubIdList.isEmpty()) {
cnd.and("clubuser.clubid", "in", List.of("__EMPTY_CLUB_ID__"));
@@ -54,9 +54,8 @@ public class ActivityBasicSettingsController {
if (parentId != null) {
parentId.setChild(child(parentId.getId()));
list.add(parentId);
return Result.success().addData(list);
}
return Result.success();
return Result.success(list);
}
private List<ActivityBasicSettings> child(String parentId) {
@@ -73,6 +72,9 @@ public class ActivityBasicSettingsController {
Cnd cnd = Cnd.NEW();
if (StrUtil.isBlank(parentId)) {
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());
} else {
cnd.and("parentId", "=", parentId);
@@ -116,7 +118,13 @@ public class ActivityBasicSettingsController {
@At
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));
if (fetch == null) {
return Result.success(new ArrayList<ActivityBasicSettings>());
}
List<ActivityBasicSettings> query = dao.query(ActivityBasicSettings.class, Cnd.where("parentId", "=", fetch.getId()).asc("code"));
return Result.success(query);
}
@@ -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();
}
@@ -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));
}
}
}
@@ -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());
}
}
@@ -162,7 +162,7 @@ public class ActivityCultureUserStatisticsController {
if (activity_type == 40002) {
cnd.and("tissue.unionId", "=", SecurityUtil.getUnionId());
} else if (activity_type == 40003) {
// 协会会长进入社团报名统计时,只允许查看自己管理社团下的活动名称。
// 协会会长进入协会报名统计时,只允许查看自己管理协会下的活动名称。
List<SysClub> myManageClub = sysClubService.getMyManageClub();
List<String> clubIdList = myManageClub.stream().map(SysClub::getId).collect(Collectors.toList());
cnd.and("tissue.clubId", "in", clubIdList);
@@ -19,7 +19,7 @@ import java.util.List;
/**
* @author: Aaron
* @create: 2021-03-09 13:57
* @description: 社团或分工会活动校工会活动
* @description: 协会或分工会活动校工会活动
**/
@EqualsAndHashCode(callSuper = true)
@@ -147,7 +147,7 @@ public class ActivityTissue extends BaseModel implements Serializable, SysHomeCo
private String unionId;
@Column
@Comment("创建人所属社团")
@Comment("创建人所属协会")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String clubId;
@@ -28,7 +28,7 @@ public class ActivityTissuePerson extends BaseModel implements Serializable {
private String id;
@Column
@Comment("分工会或社团活动ID")
@Comment("分工会或协会活动ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String tissueId;
@@ -30,7 +30,7 @@ import java.util.List;
*/
@IocBean
@Ok("json")
@ApiOperation("活动申报,社团负责人审核")
@ApiOperation("活动申报,协会负责人审核")
@At("/platform/activityDeclare/clubPrincipal")
public class ActivityDeclareClubPrincipalController {
@@ -51,7 +51,7 @@ public class ActivityDeclareClubPrincipalController {
@At
@ApiOperation("活动申报,社团负责人审核列表")
@ApiOperation("活动申报,协会负责人审核列表")
@SaCheckPermission("activityDeclare.clubPrincipal")
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
@@ -222,8 +222,8 @@ public class ActivityDeclareMineController {
docData.put("fgh", approval);
});
// 社团负责人审核
doneTaskVos.stream().filter(task -> "社团负责人审核".equals(task.getDisplayName()))
// 协会负责人审核
doneTaskVos.stream().filter(task -> "协会负责人审核".equals(task.getDisplayName()))
.max(Comparator.comparing(ProcessTaskVO::getCreatedAt))
.ifPresent(v -> {
Dict taskFormData = v.getTaskFormData();
@@ -58,7 +58,7 @@ public class ActivityDeclareInfo extends BaseModel {
private String mobile;
@Column
@Comment("申报单位(校工会/二级工会名称/社团名称)")
@Comment("申报单位(校工会/二级工会名称/协会名称)")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String declareUnitName;
@@ -68,7 +68,7 @@ public class ActivityDeclareInfo extends BaseModel {
private String unionId;
@Column
@Comment("社团id")
@Comment("协会id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String clubId;
@@ -25,9 +25,9 @@ public enum ActivityDeclareReimbursement {
*/
BRANCH_UNION("分工会活动"),
/**
* 社团
* 协会
*/
CLUB("社团活动");
CLUB("协会活动");
private final String typeName;
}
@@ -110,7 +110,7 @@ public class ActivityReimbursementBoardController {
list.add(new NutMap() {{
put("year", year);
put("type", "社团活动");
put("type", "协会活动");
put("num", activityReimbursementService.count(sql.setParam("type", "ACTIVITY_BUDGET_TYPE_THREE").setParam("year", year)));
}});
}
@@ -145,7 +145,7 @@ public class ActivityReimbursementBoardController {
put("num", activityReimbursementService.count(sql.setParam("type", "ACTIVITY_BUDGET_TYPE_TWO")));
}});
list.add(new NutMap() {{
put("type", "社团活动");
put("type", "协会活动");
put("num", activityReimbursementService.count(sql.setParam("type", "ACTIVITY_BUDGET_TYPE_THREE")));
}});
return Result.success(list);
@@ -191,7 +191,7 @@ public class ActivityReimbursementBoardController {
list.add(new NutMap() {{
put("year", year);
put("type", "社团费用");
put("type", "协会费用");
put("num", activityReimbursementService.list(sql.setParam("type", "ACTIVITY_BUDGET_TYPE_THREE").setParam("year", year)).get(0).getDouble("money"));
}});
}
@@ -218,7 +218,7 @@ public class ActivityReimbursementBoardController {
put("num", activityReimbursementService.count(unionSql));
}});
list.add(new NutMap() {{
put("type", "社团费用");
put("type", "协会费用");
put("num", activityReimbursementService.count(clubSql));
}});
return Result.success().addData(list);
@@ -250,7 +250,7 @@ public class ActivityReimbursementBoardController {
@At
@ApiOperation("所有社团的报销费用或活动数统计")
@ApiOperation("所有协会的报销费用或活动数统计")
@SaCheckPermission("activityReimbursement.board")
public Result allClubActivityAndMoney(Integer year, Boolean isClubMoney) {
Sql sql = Sqls.create("""
@@ -30,7 +30,7 @@ import java.util.List;
*/
@IocBean
@Ok("json")
@ApiOperation("活动报销,社团负责人审核")
@ApiOperation("活动报销,协会负责人审核")
@At("/platform/activityReimbursement/clubPrincipal")
public class ActivityReimbursementClubPrincipalController {
@@ -51,7 +51,7 @@ public class ActivityReimbursementClubPrincipalController {
@At
@ApiOperation("活动报销,社团负责人审核列表")
@ApiOperation("活动报销,协会负责人审核列表")
@SaCheckPermission("activityReimbursement.clubPrincipal")
public Result pageData(CommonPageParam pageParam) {
Sql sql = Sqls.create("""
@@ -323,8 +323,8 @@ public class ActivityReimbursementMineController {
docData.put("fgh", approval);
});
// 社团负责人审核
doneTaskVos.stream().filter(task -> "社团负责人审核".equals(task.getDisplayName()))
// 协会负责人审核
doneTaskVos.stream().filter(task -> "协会负责人审核".equals(task.getDisplayName()))
.max(Comparator.comparing(ProcessTaskVO::getCreatedAt))
.ifPresent(v -> {
Dict taskFormData = v.getTaskFormData();
@@ -141,7 +141,7 @@ public class YearPlanController {
}
@At
@ApiOperation("获取社团")
@ApiOperation("获取协会")
@SaCheckPermission("planSummary.yearPlan.manage")
public Result queryClub() {
Cnd cnd = Cnd.NEW();
@@ -51,17 +51,17 @@ public class YearPlan extends BaseModel {
private Double money;
@Column
@Comment("计划类型(40001-校活动、40002-分工会活动、40003-社团社团活动)")
@Comment("计划类型(40001-校活动、40002-分工会活动、40003-协会协会活动)")
@ColDefine(type = ColType.VARCHAR, width = 7)
private String planType;
@Column
@Comment("分工会ID或社团ID")
@Comment("分工会ID或协会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String tissueId;
@Column
@Comment("分工会或社团")
@Comment("分工会或协会")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String tissueName;
@@ -72,12 +72,12 @@ public class YearSummary extends BaseModel {
private String cTime;
@Column
@Comment("工会/社团id")
@Comment("工会/协会id")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String tissueId;
@Column
@Comment("分工会或社团")
@Comment("分工会或协会")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String tissueName;
@@ -87,7 +87,7 @@ public class YearSummary extends BaseModel {
private String summaryType;
@Column
@Comment("总结来源(1-校活动、2-分工会活动、3社团社团活动)")
@Comment("总结来源(1-校活动、2-分工会活动、3协会协会活动)")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String summaryFrom;
@@ -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;
}
@@ -162,7 +162,7 @@ public class ActivitySchool extends BaseModel implements Serializable , SysHomeC
private Boolean close;
@Column
@Comment("活动级别(40001-校级、40002-分工会、40003-社团或社团)")
@Comment("活动级别(40001-校级、40002-分工会、40003-协会或协会)")
@ColDefine(type = ColType.INT, width = 10)
private Integer activityLevel;
@@ -198,7 +198,7 @@ public class ActivitySchool extends BaseModel implements Serializable , SysHomeC
private String belongUnionId;
@Column
@Comment("活动所属社团")
@Comment("活动所属协会")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String belongClubId;
@@ -1,32 +1,22 @@
package com.budwk.app.zhgh.activity.trainSignUp.controller.manage;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
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.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
/**
* @Author JyuHsin
* @Date 2022/9/21
@@ -39,12 +29,6 @@ import java.util.List;
@At("/platform/trainSignUp/adjust")
public class TrainSignUpAdjustController {
@Inject
private Dao dao;
@Inject
private BaseService baseService;
@Inject
private TrainSignUpActivityService trainSignUpActivityManageService;
@Inject
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
@@ -55,122 +39,87 @@ public class TrainSignUpAdjustController {
}
/**
* 活动list
* @param year 年度
* @return
* @param year 查询年度,为空时不限制年度
* @return Result.data 为当前登录人可管理的已启用主活动数组,id/activityName 用于下拉框
*/
@At
@ApiOperation("活动列表")
@SaCheckPermission("trainSignUp.adjust")
public Result activityList(@Param(value = "year") Integer year) {
List<TrainSignUpActivity> activityList = dao.query(TrainSignUpActivity.class, Cnd.NEW().andEX("year", "=", year).andEX("isDisabled", "=", false).desc("activityStartTime"));
return Result.success(activityList);
public Result activityList(@Param("year") Integer year) {
return Result.success(trainSignUpActivityStatisticsService.activityList(year, true));
}
/**
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
* @param activityId 主活动ID,为空时返回空分页
* @return Result.data 为 Pagination,list 和 totalCount 均按负责人范围查询
*/
@At
@ApiOperation("分页查询")
@SaCheckPermission("trainSignUp.adjust")
public Result pageData(PageForm pageForm,
@Param(value = "activityId") String activityId) {
public Result pageData(PageForm pageForm, @Param("activityId") String activityId) {
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
return Result.success(pagination);
}
/**
* @param activityId 主活动ID
* @return Result.data 为可调整的子活动数组,id 为目标ID,registerNum/hasWaitingNum 为已报及候补人数
*/
@At
@ApiOperation("子活动查询")
@SaCheckPermission("trainSignUp.adjust")
public Result getCourse(String activityId) {
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
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);
return Result.success(trainSignUpActivityStatisticsService.getCourse(activityId));
}
/**
* @param courseId 有管理权限的子活动ID
* @param unionId 工会ID,可为空
* @param unitId 单位ID,可为空
* @param searchName 保留现有接口参数,姓名和工号搜索统一使用 searchKeyword
* @param searchKeyword 姓名或工号关键字,可为空
* @return Result.data 为报名人员数组,userId 为报名人员ID,state 为报名状态
*/
@At
@ApiOperation("报名用户列表")
@SaCheckPermission("trainSignUp.adjust")
public Result registerUserList(@Param("courseId") String courseId,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "searchName") String searchName,
@Param(value = "searchKeyword") String searchKeyword) {
List<NutMap> list = trainSignUpActivityStatisticsService.registerUserList(courseId, unionId, unitId, searchName, searchKeyword);
return Result.success(list);
public Result registerUserList(@Param("courseId") String courseId, @Param("unionId") String unionId,
@Param("unitId") String unitId, @Param("searchName") String searchName,
@Param("searchKeyword") String searchKeyword) {
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId, unionId, unitId, searchName, searchKeyword));
}
/**
* @param activityId 主活动ID,来源和目标子活动都必须属于该活动
* @param oldCourseId 来源子活动ID,必须有管理权限
* @param newCourseId 目标子活动ID,必须有管理权限
* @param userId 被调整的报名人员ID,操作人身份由登录会话确定
* @return Result,code=0 表示报名及签到记录已在同一事务中调整
*/
@At
@ApiOperation("人员调整")
@SaCheckPermission("trainSignUp.adjust")
@SLog(tag = "品牌活动-人员调整", msg = "人员调整")
@Aop(TransAop.READ_COMMITTED)
public Result adjust(String activityId, String oldCourseId, String newCourseId, String 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);
trainSignUpActivityStatisticsService.adjust(activityId, oldCourseId, newCourseId, userId);
return Result.success();
}
/**
* @param activityId 主活动ID
* @param courseId 属于该主活动且有管理权限的子活动ID
* @param userId 被删除的报名人员ID
* @return Result,code=0 表示该子活动的报名及签到记录已删除
*/
@At
@ApiOperation("删除报名人员")
@SaCheckPermission("trainSignUp.adjust")
@SLog(tag = "品牌活动-人员调整", msg = "删除报名人员")
@Aop(TransAop.READ_COMMITTED)
public Result deleteSignUser(String activityId, String courseId, String 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);
trainSignUpActivityStatisticsService.deleteSignUser(activityId, courseId, userId);
return Result.success();
}
}
@@ -128,6 +128,7 @@ public class TrainSignUpApplyController {
@Param(value = "assortTypes") String[] assortTypes,
@Param(value = "searchKeyword") String searchKeyword,
@Param(value = "dataType") String dataType) {
// 返回课程签到半径 radius(米),供 H5 地图按实际配置绘制签到范围。
Sql sql = Sqls.create("""
SELECT
tsuc.id,
@@ -144,6 +145,7 @@ public class TrainSignUpApplyController {
tsuc.unionLimit,
tsuc.isMobileSign,
tsuc.signType,
tsuc.radius,
tsuc.isReceiveGift,
tsuc.giftType,
tsuc.reserveMode,
@@ -10,8 +10,6 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.entity.ProcessInstance;
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_unit;
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.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
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.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.nutz.trans.Trans;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
@@ -66,6 +64,37 @@ public class TrainSignUpManageController {
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
@ApiOperation("分页查询")
@SaCheckPermission("trainSignUp.manage")
@@ -110,31 +139,22 @@ public class TrainSignUpManageController {
@ApiOperation("活动删除")
@SaCheckPermission("trainSignUp.manage")
@SLog(tag = "品牌活动-活动管理", msg = "删除活动")
@Aop(TransAop.READ_COMMITTED)
public Result onDelete(String id) {
if (dao.fetch(Sys_home_template.class, id) != null) {
return Result.error("该活动已设为模板,请先取消模板后再删除");
// id 为主活动ID;service 在同一事务中清理子活动负责人及报名关联。
if (StrUtil.isBlank(id)) {
return Result.error("活动ID不能为空");
}
Trans.exec(() -> {
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);
});
trainSignUpActivityManageService.deleteActivity(id);
return Result.success();
}
@At
@ApiOperation("活动状态变更")
@SaCheckPermission("trainSignUp.manage")
@Aop(TransAop.READ_COMMITTED)
public Result activityStatusChange(TrainSignUpActivity activity) {
trainSignUpActivityManageService.updateActivityStatus(activity);
dao.update(Sys_home_activity.class,
Chain.make("enable", !activity.isDisabled()),
Cnd.where("id", "=", activity.getId()));
return Result.success();
}
@@ -185,11 +205,17 @@ public class TrainSignUpManageController {
return Result.success().addData(dataMap);
}
/**
* 接收主活动及 courseList 子活动列表;每个 responsibleList 元素传 userId、mobile。
* responsibleList 为 null 保留历史联系人,空列表清空,多人关系由 service 校验并保存。
* 返回 Result,code=0 表示主活动、子活动和负责人已在同一事务中保存成功。
*/
@At
@Ok("json:full")
@ApiOperation("品牌活动新增/修改")
@SaCheckPermission(value = {"trainSignUp.manage", "trainSignUp.applyActivity"}, mode = SaMode.OR)
@SLog(tag = "品牌活动-活动管理", msg = "新增/修改活动")
@Aop(TransAop.READ_COMMITTED)
public Result doHandle(TrainSignUpActivity activity) {
if (StrUtil.isBlank(activity.getId())) {
cleanNewActivityData(activity);
@@ -300,7 +326,7 @@ public class TrainSignUpManageController {
@At
@Ok("json:full")
@ApiOperation("查询分工会和社团")
@ApiOperation("查询分工会和协会")
@SaCheckPermission(value = {"trainSignUp.manage", "trainSignUp.applyActivity"}, mode = SaMode.OR)
public Result selectUnitAndClub() {
List<NutMap> result = new ArrayList<>();
@@ -1,55 +1,26 @@
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.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.param.PageForm;
import com.budwk.app.base.result.Result;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
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.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
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
@@ -63,12 +34,8 @@ import java.util.zip.ZipOutputStream;
@At("/platform/trainSignUp/statistics")
public class TrainSignUpActivityStatisticsController {
@Inject
private TrainSignUpActivityService trainSignUpActivityManageService;
@Inject
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
@Inject
private Dao dao;
@At("")
@SaCheckPermission("trainSignUp.statistics")
@@ -76,231 +43,113 @@ public class TrainSignUpActivityStatisticsController {
public void index() {
}
/**
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
* @param activityId 主活动ID,为空时返回空分页
* @return Result.data 为 Pagination,list 和 totalCount 均受当前登录人的负责人范围限制
*/
@At
@ApiOperation("分页查询")
@SaCheckPermission("trainSignUp.statistics")
public Result pageData(PageForm pageForm,
@Param(value = "activityId") String activityId) {
public Result pageData(PageForm pageForm, @Param("activityId") String activityId) {
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
return Result.success(pagination);
}
/**
* 活动list
*
* @param year 年度
* @return
* @param year 查询年度,为空时不限制年度
* @return Result.data 为可管理的主活动列表,id/activityName 用于活动下拉框
*/
@At
@ApiOperation("活动列表")
@SaCheckPermission("trainSignUp.statistics")
public Result activityList(@Param(value = "year") Integer year) {
List<TrainSignUpActivity> list = dao.query(TrainSignUpActivity.class, Cnd.NEW().andEX("year", "=", year).desc("activityStartTime"));
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);
public Result activityList(@Param("year") Integer year) {
return Result.success(trainSignUpActivityStatisticsService.activityList(year, false));
}
/**
* 培训班报名人员list
*
* @param courseId
* @return
* @param courseId 子活动ID,必须在当前登录人的管理范围内
* @return Result.data 为报名人员数组,包含姓名、工号、单位、联系方式、报名状态及动态报名字段
*/
@At
@ApiOperation("报名人员列表")
@SaCheckPermission("trainSignUp.statistics")
public Result registerUserList(@Param(value = "courseId") String courseId) {
public Result registerUserList(@Param("courseId") String courseId) {
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId));
}
/**
* @param courseId 子活动ID,必须在当前登录人的管理范围内
* @return Result.data 为动态列配置数组,label 为列标题,prop 为报名数据字段名
*/
@At
@ApiOperation("报名动态列")
@SaCheckPermission("trainSignUp.statistics")
public Object getTaleColumnInfo(@Param(value = "courseId") String courseId) {
public Object getTaleColumnInfo(@Param("courseId") String courseId) {
return Result.success(trainSignUpActivityStatisticsService.getTaleColumnInfo(courseId));
}
/**
* @param courseId 子活动ID,必须在当前登录人的管理范围内
* @return Result.data 为时段数组,id 为时段ID,courseStartTime/courseEndTime 为起止时间
*/
@At
@ApiOperation("获取时间信息")
@SaCheckPermission("trainSignUp.statistics")
public Result selectTimes(@Param("courseId") String courseId) {
List<TrainSignUpActivityCourse> activityCourses = dao.query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", courseId).asc("courseStartTime"));
return Result.success(activityCourses);
return Result.success(trainSignUpActivityStatisticsService.selectTimes(courseId));
}
/**
* 培训班上课签到信息
*
* @param courseId
* @return
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
* @param courseId 有管理权限的子活动ID
* @param timeId 子活动时段ID
* @return Result.data 为签到分页,list 中 isAttend/attendTime 为签到状态及时间
*/
@At
@ApiOperation("获取签到信息")
@SaCheckPermission("trainSignUp.statistics")
public Result getSignInfo(PageForm pageForm,
@Param("courseId") String courseId,
String timeId) {
public Result getSignInfo(PageForm pageForm, @Param("courseId") String courseId, String timeId) {
return Result.success(trainSignUpActivityStatisticsService.getSignInfo(pageForm, courseId, timeId));
}
/**
* @param courseId 有管理权限的子活动ID
* @param openOtherUnion true 开放给其他工会报名,false 关闭
* @return Result,code=0 表示状态已更新
*/
@At
@ApiOperation("开放报名")
@SaCheckPermission("trainSignUp.statistics")
@Aop(TransAop.READ_COMMITTED)
public Result signChange(@Param("courseId") String courseId, @Param("openOtherUnion") Boolean openOtherUnion) {
dao.update(TrainSignUpCourse.class, Chain.make("openOtherUnion", openOtherUnion)
, Cnd.where("id", "=", courseId));
trainSignUpActivityStatisticsService.signChange(courseId, openOtherUnion);
return Result.success();
}
/**
* @param id 签到记录ID,service 根据记录所属子活动校验当前登录人的权限
* @return Result,code=0 表示签到状态已切换
*/
@At
@ApiOperation("设置签到")
@SaCheckPermission("trainSignUp.statistics")
@Aop(TransAop.READ_COMMITTED)
public Result adjustSign(@Param("id") String id) {
TrainSignUpUserCourse userCourse = dao.fetch(TrainSignUpUserCourse.class, id);
if(userCourse.isAttend()) {
userCourse.setAttend(false);
userCourse.setAttendTime(null);
} else {
userCourse.setAttend(true);
userCourse.setAttendTime(userCourse.getCourseStartTime());
}
dao.update(userCourse);
trainSignUpActivityStatisticsService.adjustSign(id);
return Result.success();
}
/**
* @param activityId 主活动ID,仅导出当前登录人有管理权限的子活动报名记录
* @param response 文件下载响应,输出 .xlsx 文件而非 JSON
*/
@At
@Ok("void")
@ApiOperation("导出签到名单")
@SaCheckPermission("trainSignUp.statistics")
public void exportSignUser(@Param(value = "activityId") String activityId,
HttpServletResponse response) throws IOException {
try {
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();
}
public void exportSignUser(@Param("activityId") String activityId, HttpServletResponse response) throws IOException {
NutMap exportData = trainSignUpActivityStatisticsService.buildSignUserExport(activityId);
CommonDownloadUtil.download(exportData.getString("fileName"), exportData.getAs("workbook", Workbook.class), response);
}
}
@@ -96,6 +96,13 @@ public class TrainSignUpCourse extends BaseModel implements Serializable {
@Many(field = "courseId")
private List<TrainSignUpActivityCourse> courseTimeList;
/**
* 完整负责人关系。null 表示旧数据尚未设置,空列表表示主动清空负责人。
* 关系单独保存,后续可按 userId 精确查询,无需解析姓名或 JSON。
*/
@Many(field = "courseId")
private List<TrainSignUpCourseResponsible> responsibleList;
//已报人数
private Integer hasRegisterNum;
@@ -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;
}
@@ -20,6 +20,35 @@ import java.util.List;
*/
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);
/**
* 删除活动及其子活动、负责人关系、报名和首页记录。
*
* @param id 主活动ID;已经设为模板的活动需先取消模板
*/
void deleteActivity(String id);
/**
* 更新活动状态
*
@@ -3,6 +3,8 @@ package com.budwk.app.zhgh.activity.trainSignUp.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
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 org.nutz.lang.util.NutMap;
@@ -17,44 +19,122 @@ import java.util.Map;
public interface TrainSignUpActivityStatisticsService extends BaseService<TrainSignUpUser> {
/**
* 统计分页
* 查询当前用户可管理的主活动,只包含至少一个本人负责子活动的主活动。
* 系统管理员、校工会管理员、校工会活动管理员不受负责人范围限制,仍保留年度及启用状态条件。
*
* @param pageForm
* @param activityId
* @return
* @param year 年度,为空时不限制年度
* @param enabledOnly true 仅查询已启用活动,人员调整页面使用;false 保留统计页面原范围
* @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);
/**
* 该课程下的报名人员信息
* 查询有管理权限的子活动报名明细,并展开动态报名字段。
*
* @param courseId
* @return
* @param courseId 子活动ID,不能为空,必须在当前登录人的管理范围内
* @return 报名人员数组,包含姓名、工号、单位、联系方式、state 报名状态及动态字段
*/
List<NutMap> registerUserList(String courseId);
/**
* @param courseId 有管理权限的子活动ID
* @return 动态列配置数组,label 为列标题,prop 为报名明细对应字段名
*/
List<NutMap> getTaleColumnInfo(String courseId);
/**
* 该课程下的报名人员信息
* 按工会、单位和姓名/工号搜索有管理权限的子活动报名人员。
*
* @param courseId
* @return
* @param courseId 有管理权限的子活动ID,不能为空
* @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);
/**
* 获取每个课程的签到情况
* 查询有管理权限的子活动指定时段签到分页。
*
* @param courseId
* @return k->每个培训班每节课的上课时间 v->上课记录list
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
* @param courseId 有管理权限的子活动ID
* @param timeId 子活动时段ID
* @return Pagination,list 为签到人员,isAttend/attendTime 为签到状态及时间
*/
Pagination getSignInfo(PageForm pageForm, String courseId, String timeId);
/**
* 报名人员list 导出
* @param activityId
* @return
* 查询主活动下可管理子活动的基础报名导出数据。
* @param activityId 主活动ID
* @return 报名人员数组,包含姓名、工号、联系方式及子活动名称,不包含管理范围以外的子活动
*/
List<NutMap> baoMingUserList(String activityId);
@@ -1,13 +1,18 @@
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
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.param.PageForm;
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_template;
import com.budwk.app.sys.models.Sys_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.AuthUtil;
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.FamilyType;
@@ -51,9 +56,227 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
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
@Aop(TransAop.READ_COMMITTED)
public void add(TrainSignUpActivity activity, TrainSignUpCourse course) {
// 先校验所有负责人,任何一行存在无效用户或联系方式时均不写入活动。
validateCourseResponsibles(activity.getCourseList());
// 新建活动(包括从模板另存)统一归入活动列表,不能继承来源活动的模板状态。
activity.setTemplateStatus(TrainSignUpActivity.TEMPLATE_STATUS_UNSET);
dao().insert(activity);
@@ -68,6 +291,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
v.setActivityId(activity.getId());
v.setOpenOtherUnion(false);
dao().insert(v);
saveCourseResponsibles(v);
this.setCourseTimeAndInsert(v);
}
@@ -80,6 +304,22 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
@Override
@Aop(TransAop.READ_COMMITTED)
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. 更新主活动
update(activity);
// 2. 处理类型限制
@@ -90,7 +330,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
}
// 3. 处理课程列表
List<TrainSignUpCourse> courseList = activity.getCourseList();
if (Lang.isEmpty(courseList)) {
if (courseList == null) {
return;
}
@@ -108,12 +348,15 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
Set<String> deletedIds = Sets.difference(oldIds, newIds).immutableCopy();
if(Lang.isNotEmpty(deletedIds)) {
// 删除子活动时同步清理负责人,避免后续按 userId 查到已删除的业务。
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("courseId", "in", deletedIds));
dao().clear(TrainSignUpCourse.class, Cnd.where("id", "in", deletedIds));
}
for (TrainSignUpCourse course : courseList) {
course.setActivityId(activity.getId());
dao().insertOrUpdate(course);
saveCourseResponsibles(course);
if (Lang.isNotEmpty(course.getCourseTimeList())) {
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) {
String courseId = course.getId();
String activityId = activity.getId();
@@ -223,6 +591,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
return u;
}
@Aop(TransAop.READ_COMMITTED)
private void setCourseTimeAndInsert(TrainSignUpCourse course) {
course.getCourseTimeList().forEach(courseTime -> {
Calendar calendar = Calendar.getInstance();
@@ -250,7 +619,14 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateActivityStatus(TrainSignUpActivity activity) {
requireActivityBasicPermission();
if (StrUtil.isBlank(activity.getId()) || fetch(activity.getId()) == null) {
throw new BaseException("活动不存在");
}
updateIgnoreNull(activity);
// 活动启停及首页可见状态在同一个事务内更新,controller 仅负责接收参数和返回结果。
dao().update(Sys_home_activity.class, Chain.make("enable", !activity.isDisabled()),
Cnd.where("id", "=", activity.getId()));
}
@Override
@@ -270,10 +646,14 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
activity.setCourseList(courseArray);
List<TrainSignUpCourse> courseList = activity.getCourseList();
fillCourseResponsibles(courseList);
courseList.forEach(c -> {
if (StrUtil.isNotBlank(c.getCourseType())) {
dao().fetchLinks(c, "^(courseTimeList)$", Cnd.NEW().asc("courseStartTime"));
if (c.getCourseTimeList() == null) {
c.setCourseTimeList(new ArrayList<>());
}
if (StrUtil.isNotBlank(c.getCourseType())) {
int courseCount = statisticsService.queryCourseCount(c.getId(), c.getCourseType());
c.setHasRegisterNum(courseCount);
int courseWaitCount = statisticsService.queryCourseWaitCount(c.getId(), c.getCourseType());
@@ -285,9 +665,36 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
return Lang.obj2nutmap(activity);
}
/**
* pageForm 为页码和每页条数,cnd 为已有活动筛选条件;返回原分页结构及活动列表。
* list 中新增 canManageCourse 布尔值,仅非校工会管理角色且负责当前活动的用户为 true。
* 该字段控制子活动入口显示,实际读写仍由 requireManagedCourse 校验负责人关系。
*/
@Override
public Pagination pageData(PageForm pageForm, Cnd cnd) {
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;
}
@@ -618,7 +1025,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
if (SecurityUtil.getUnionId().equals(o.getHostUnionId())) {
return true;
} 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;
}
}
@@ -1,25 +1,42 @@
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.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.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
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.FamilyUser;
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
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 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.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -37,8 +54,197 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
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
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("""
SELECT
tsuc.id,
@@ -61,9 +267,11 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
train_sign_up_type type on tsuc.courseType = type.id
WHERE
tsuc.activityId = @activityId
$responsibleScope
ORDER BY tsuc.orderNum
""");
sql.setParam("activityId", activityId);
applyResponsibleScope(sql, "tsuc.id", false);
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
List<NutMap> courseList = pagination.getList();
courseList.forEach(c -> {
@@ -75,6 +283,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
@Override
public List<NutMap> registerUserList(String courseId) {
requireCourseScope(courseId);
Sql sql = Sqls.create("""
SELECT
u.loginname,
@@ -92,11 +301,13 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
train_sign_up_user tsuu
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
$condition
$responsibleScope
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("tsuu.courseId", "=", courseId);
cnd.and("tsuu.courseId", "=", courseId);
sql.setCondition(cnd);
applyResponsibleScope(sql, "tsuu.courseId", false);
List<NutMap> list = listMap(sql);
for (NutMap o : list) {
if(StrUtil.isBlank(o.getString("mobileColumnsValue"))) {
@@ -114,7 +325,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
@Override
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());
dao().fetchLinks(upType, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
@@ -125,6 +336,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
@Override
public List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword) {
requireCourseScope(courseId);
Sql sql = Sqls.create("""
SELECT
tsuu.id,
@@ -141,10 +353,11 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
train_sign_up_user tsuu
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
$condition
$responsibleScope
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("tsuu.courseId", "=", courseId);
cnd.and("tsuu.courseId", "=", courseId);
cnd.andEX("u.unionid", "=", unionId);
cnd.andEX("u.unitid", "=", unitId);
if (StrUtil.isNotBlank(searchKeyword)) {
@@ -153,11 +366,13 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
cnd.and(group);
}
sql.setCondition(cnd);
applyResponsibleScope(sql, "tsuu.courseId", false);
return listMap(sql);
}
@Override
public Pagination getSignInfo(PageForm pageForm, String courseId, String timeId) {
requireCourseScope(courseId);
Sql sql = Sqls.create("""
SELECT
tsuuc.id,
@@ -173,6 +388,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
`train_sign_up_user_course` tsuuc
LEFT JOIN `vw_user` u ON u.id = tsuuc.userId
$condition
$responsibleScope
""");
Cnd cnd = Cnd.NEW();
cnd.and("tsuuc.courseId", "=", courseId);
@@ -180,6 +396,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
cnd.desc("isAttend");
cnd.desc("attendTime");
sql.setCondition(cnd);
applyResponsibleScope(sql, "tsuuc.courseId", false);
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
WHERE
uc.activityId = @activityId
$responsibleScope
ORDER BY u.unitCode,u.unioncode,u.sex
""");
sql.setParam("activityId", activityId);
applyResponsibleScope(sql, "uc.id", false);
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
public int queryCourseCount(String courseId, String courseType) {
return this.queryCourseCount(courseId, courseType, null);

Some files were not shown because too many files have changed in this diff Show More