22 Commits
Author SHA1 Message Date
c-chenxs1 790e8e9f50 feat: 特殊慰问-职工慰问优化 2026-10-31 14:36:14 +08:00
c-chenxs1 81fd5cb773 feat: 特殊慰问表单优化2 2026-10-30 20:30:12 +08:00
c-chenxs1 efad953710 feat: 特殊慰问表单优化 2026-10-30 19:35:39 +08:00
c-chenxs1 e0004e410f feat: 慰问、困难帮扶优化 2026-10-30 17:57:20 +08:00
c-chenxs1 f8457386fd feat: 慰问、困难帮扶 2026-10-30 17:49:45 +08:00
c-chenxs1 16cae3ed2b feat: 修复困难帮扶申请表单校验及页面加载闪烁,调整办公室主任拒绝、退回审批时不校验级别和帮扶金额。
新增特殊慰问模块,支持分工会主席统一为多人申请、Excel 导入及人员校验;完善主从表存储、表单校验、审批展示和导出,移除分工会主席审批节点,使用独立流程节点标识。
2026-10-30 16:30:48 +08:00
c-chenxs1 29260423d5 feat: 提取通用 Word/PDF 模板渲染方法,支持 DOC、DOCX 和循环表格 2026-10-30 10:16:46 +08:00
c-chenxs1 d8e7adfe9a feat: 职工慰问 整改 补充日志,排查职工慰问导出 2026-10-30 09:34:46 +08:00
c-chenxs1 bc1b3bf7fe feat: 职工慰问 整改 4 2026-10-29 18:05:13 +08:00
c-chenxs1 59fd83ba24 feat: 职工慰问 整改 3 2026-10-29 17:20:58 +08:00
c-zhouhf1 3059603186 签字不显示问题 2026-10-27 20:11:19 +08:00
c-hongqw1 f17f6153b5 企业微信消息和oa待办 2026-10-27 19:44:55 +08:00
c-hongqw1 e87014fa72 企业微信消息和oa待办 2026-10-26 14:33:27 +08:00
c-hongqw1 0bde359113 提案导出 2026-10-23 11:35:20 +08:00
c-chenxs1 2ebc204803 feat: 职工慰问 整改 优化2 2026-10-09 20:59:26 +08:00
c-chenxs1 5f064b076c feat: 职工慰问 整改 优化 2026-10-09 20:45:33 +08:00
c-chenxs1 783e751560 feat: 职工慰问 整改 2026-10-09 20:19:41 +08:00
c-chenxs1 b325e8a8f5 feat: 职工慰问 2026-10-09 19:39:26 +08:00
c-chenxs1 45121f7b2e feat: 困难帮扶 优化 2026-10-09 17:30:07 +08:00
c-chenxs1 7d76ae4e36 feat: 困难帮扶 整改 2026-10-09 17:14:31 +08:00
c-hongqw1 53bfc7f1cf 提案流程 2026-10-09 10:09:08 +08:00
c-hongqw1 aff992faf0 提案流程 2026-10-09 10:06:08 +08:00
188 changed files with 106831 additions and 1583 deletions
+2
View File
@@ -2,6 +2,8 @@
**/node_modules/ **/node_modules/
**/dist/ **/dist/
**/build/ **/build/
!src/main/resources/static/assets/platform/plugins/pdfJs/build/
!src/main/resources/static/assets/platform/plugins/pdfJs/build/**
**/target/ **/target/
**/out/ **/out/
**/.next/ **/.next/
@@ -19,6 +19,7 @@ RoleConstant {
SCHOOL_UNION_ADMIN("校工会管理员"), SCHOOL_UNION_ADMIN("校工会管理员"),
SCHOOL_UNION_CHAIRMAN("校工会主席"), SCHOOL_UNION_CHAIRMAN("校工会主席"),
SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"), SCHOOL_UNION_VICE_CHAIRMAN("校工会副主席"),
SCHOOL_OFFICE_DIRECTOR("校工会办公室主任"),
SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"), SCHOOL_UNION_WELFARE_ADMIN("校工会福利管理员"),
SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"), SCHOOL_UNION_MEMBER_ADMIN("校工会会员管理员"),
SCHOOL_UNION_TC_ADMIN("校工会教代会管理员"), SCHOOL_UNION_TC_ADMIN("校工会教代会管理员"),
@@ -50,6 +51,7 @@ RoleConstant {
BRANCH_UNION_WENTI_SPORTS("分工会文体福利委员"), BRANCH_UNION_WENTI_SPORTS("分工会文体福利委员"),
UNIT_PARTY_SECRETARY("单位党委书记"), UNIT_PARTY_SECRETARY("单位党委书记"),
UNIT_SCHOOL_LEADER("单位分管校领导"),
TEACHER_CONGRESS_DELEGATE_FORMAL("教代会正式代表"), TEACHER_CONGRESS_DELEGATE_FORMAL("教代会正式代表"),
TEACHER_CONGRESS_DELEGATE_ATTENDANCE("教代会列席代表"), TEACHER_CONGRESS_DELEGATE_ATTENDANCE("教代会列席代表"),
@@ -6,6 +6,8 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.models.Sys_file; import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_office_template; import com.budwk.app.sys.models.Sys_office_template;
import com.budwk.app.sys.utils.SysFileMinIoUtil; import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.ByteArrayPictureRenderData; import com.deepoove.poi.data.ByteArrayPictureRenderData;
import com.deepoove.poi.data.PictureRenderData; import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType; import com.deepoove.poi.data.PictureType;
@@ -18,6 +20,12 @@ import org.nutz.ioc.loader.annotation.IocBean;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -31,6 +39,20 @@ public class SysOfficeTemplateUtil {
@Inject @Inject
private Dao dao; private Dao dao;
public String getTemplateFileName(String templateCode) {
Sys_office_template officeTemplate = dao.fetch(Sys_office_template.class, Cnd.where(Sys_office_template::getTemplateCode, "=", templateCode));
if (ObjectUtil.isNull(officeTemplate)) {
throw new BaseException("代码为:{}的模板不存在", templateCode);
}
String templatePath = officeTemplate.getTemplatePath();
String id = templatePath.substring(templatePath.indexOf("id=") + "id=".length());
Sys_file sysFile = dao.fetch(Sys_file.class, id);
if (ObjectUtil.isEmpty(sysFile)) {
throw new BaseException("模板文件不存在");
}
return sysFile.getName();
}
/** /**
* 获取模板文件输入流 * 获取模板文件输入流
* *
@@ -52,6 +74,88 @@ public class SysOfficeTemplateUtil {
return new ByteArrayInputStream(fileBytes); return new ByteArrayInputStream(fileBytes);
} }
/**
* 统一渲染 DOC/DOCX 模板,保留调用方的循环表格等策略;模板输入流由调用方关闭。
*/
public byte[] renderTemplate(InputStream input, Object data, Configure config, boolean pdf, String context) throws Exception {
if (input == null) throw new BaseException("模板文件不存在");
byte[] templateBytes = input.readAllBytes();
try (ByteArrayInputStream formatInput = new ByteArrayInputStream(templateBytes)) {
if (com.aspose.words.FileFormatUtil.detectFileFormat(formatInput).getLoadFormat() != com.aspose.words.LoadFormat.DOCX) {
try (ByteArrayInputStream source = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream converted = new ByteArrayOutputStream()) {
new com.aspose.words.Document(source).save(converted, com.aspose.words.SaveFormat.DOCX);
templateBytes = converted.toByteArray();
}
}
}
byte[] wordBytes;
try (ByteArrayInputStream templateInput = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream();
XWPFTemplate template = XWPFTemplate.compile(templateInput, config == null ? Configure.builder().build() : config)) {
template.render(data).write(output);
wordBytes = output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] Word模板渲染失败,context={}", context, e);
throw e;
}
log.info("[Office模板渲染] Word渲染完成,context={}, templateBytes={}, outputBytes={}", context, templateBytes.length, wordBytes.length);
if (!pdf) return wordBytes;
try (ByteArrayInputStream wordInput = new ByteArrayInputStream(wordBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
com.aspose.words.Document document = new com.aspose.words.Document(wordInput);
configureDocumentFonts(document, context);
com.aspose.words.PdfSaveOptions options = new com.aspose.words.PdfSaveOptions();
options.setFontEmbeddingMode(com.aspose.words.PdfFontEmbeddingMode.EMBED_ALL);
document.save(output, options);
log.info("[Office模板渲染] PDF转换完成,context={}, outputBytes={}", context, output.size());
return output.toByteArray();
} catch (Exception e) {
log.error("[Office模板渲染] PDF转换失败,context={}", context, e);
throw e;
}
}
private void configureDocumentFonts(com.aspose.words.Document document, String context) throws Exception {
com.aspose.words.FontSettings settings = new com.aspose.words.FontSettings();
List<com.aspose.words.FontSourceBase> sources = new ArrayList<>(List.of(settings.getFontsSources()));
Set<String> folders = new LinkedHashSet<>();
String windowsDir = System.getenv("WINDIR");
if (StrUtil.isNotBlank(windowsDir)) folders.add(new File(windowsDir, "Fonts").getPath());
String localAppData = System.getenv("LOCALAPPDATA");
if (StrUtil.isNotBlank(localAppData)) folders.add(new File(localAppData, "Microsoft/Windows/Fonts").getPath());
folders.add(new File(System.getProperty("user.dir"), "fonts").getPath());
for (String folder : folders) {
File directory = new File(folder);
log.info("[Office模板渲染] 字体目录,context={}, path={}, readable={}", context, folder, directory.isDirectory() && directory.canRead());
if (directory.isDirectory() && directory.canRead()) sources.add(new com.aspose.words.FolderFontSource(folder, true));
}
settings.setFontsSources(sources.toArray(new com.aspose.words.FontSourceBase[0]));
Set<String> availableFonts = new LinkedHashSet<>();
for (com.aspose.words.FontSourceBase source : sources) {
for (com.aspose.words.PhysicalFontInfo font : source.getAvailableFonts()) availableFonts.add(font.getFontFamilyName());
}
String chineseFont = null;
for (String candidate : List.of("SimSun", "宋体", "Microsoft YaHei", "微软雅黑", "DengXian", "等线", "SimHei", "黑体", "FangSong", "仿宋", "Noto Serif CJK SC", "Noto Sans CJK SC")) {
if (availableFonts.contains(candidate)) {
chineseFont = candidate;
break;
}
}
log.info("[Office模板渲染] 中文字体选择,context={}, fontCount={}, chineseFont={}", context, availableFonts.size(), chineseFont);
if (chineseFont != null) {
settings.getSubstitutionSettings().getDefaultFontSubstitution().setDefaultFontName(chineseFont);
for (String name : List.of("宋体", "SimSun", "新宋体", "NSimSun", "黑体", "SimHei", "仿宋", "FangSong", "楷体", "KaiTi", "微软雅黑", "Microsoft YaHei")) {
if (!availableFonts.contains(name)) settings.getSubstitutionSettings().getTableSubstitution().setSubstitutes(name, chineseFont);
}
} else {
log.warn("[Office模板渲染] 未发现常用中文字体,context={}, availableFonts={}", context, availableFonts);
}
settings.getFallbackSettings().loadMsOfficeFallbackSettings();
document.setFontSettings(settings);
document.setWarningCallback(warning -> log.warn("[Office模板渲染] 字体或排版警告,context={}, type={}, detail={}", context, warning.getWarningType(), warning.getDescription()));
}
/** /**
* 签字url转换为poi的图片对象 * 签字url转换为poi的图片对象
* *
@@ -63,13 +167,7 @@ public class SysOfficeTemplateUtil {
public PictureRenderData createPictureRenderData(int width, int height, String url) { public PictureRenderData createPictureRenderData(int width, int height, String url) {
try { try {
byte[] bytes = getFileBytesByUrl(url); byte[] bytes = getFileBytesByUrl(url);
// if (StrUtil.isBlank(url)) { log.info("[Office图片读取] 创建图片渲染数据,referenceLength={}, byteCount={}, width={}, height={}", StrUtil.nullToEmpty(url).length(), bytes == null ? null : bytes.length, width, height);
// return null;
// }
// int startIndex = url.indexOf("id=") + "id=".length();
// String id = url.substring(startIndex);
// Sys_file sysFile = dao.fetch(Sys_file.class, id);
// byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG); ByteArrayPictureRenderData pictureRenderData = new ByteArrayPictureRenderData(bytes, PictureType.PNG);
PictureStyle pictureStyle = new PictureStyle(); PictureStyle pictureStyle = new PictureStyle();
pictureStyle.setWidth(width); pictureStyle.setWidth(width);
@@ -77,7 +175,7 @@ public class SysOfficeTemplateUtil {
pictureRenderData.setPictureStyle(pictureStyle); pictureRenderData.setPictureStyle(pictureStyle);
return pictureRenderData; return pictureRenderData;
} catch (Exception e) { } catch (Exception e) {
log.error("文件转换PictureRenderData错误,文件路径:{},{}", url, e.getMessage()); log.error("[Office图片读取] 创建图片渲染数据失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
return null; return null;
} }
} }
@@ -126,13 +224,20 @@ public class SysOfficeTemplateUtil {
public byte[] getFileBytesByUrl(String url) { public byte[] getFileBytesByUrl(String url) {
try { try {
if (StrUtil.isBlank(url)) { if (StrUtil.isBlank(url)) {
log.warn("[Office图片读取] 图片引用为空");
return null; return null;
} }
int startIndex = url.indexOf("id=") + "id=".length(); int startIndex = url.indexOf("id=") + "id=".length();
String id = url.substring(startIndex); String id = url.substring(startIndex);
String logFileId = url.contains("id=") ? id : "未识别id参数";
log.info("[Office图片读取] 解析文件引用,fileId={}, hasIdParameter={}", logFileId, url.contains("id="));
Sys_file sysFile = dao.fetch(Sys_file.class, id); Sys_file sysFile = dao.fetch(Sys_file.class, id);
return SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath()); log.info("[Office图片读取] 查询文件记录,fileId={}, found={}, bucket={}, storagePathPresent={}", logFileId, sysFile != null, sysFile == null ? null : sysFile.getBucket(), sysFile != null && StrUtil.isNotBlank(sysFile.getStoragePath()));
byte[] bytes = SysFileMinIoUtil.getFileBytes(sysFile.getBucket(), sysFile.getStoragePath());
log.info("[Office图片读取] 读取存储文件完成,fileId={}, byteCount={}", logFileId, bytes == null ? null : bytes.length);
return bytes;
} catch (Exception e) { } catch (Exception e) {
log.error("[Office图片读取] 读取图片失败,referenceLength={}", StrUtil.nullToEmpty(url).length(), e);
return null; return null;
} }
} }
@@ -0,0 +1,29 @@
package com.budwk.app.base.wxwork;
import org.nutz.lang.util.NutMap;
import java.util.List;
/** 企业微信自建应用消息服务,接收人的 loginname 必须对应学校企业微信 userid。 */
public interface WxWorkMessageService {
/**
* 单人发送;link 为空发送文本,否则发送文本卡片,相对链接使用配置域名补全。
* @param loginname 接收人的企业微信 userid,当前学校直接使用系统 loginname
* @param title 消息标题,为空使用配置的默认标题
* @param content 消息正文,可含富文本标签,发送前转为纯文本
* @param link 业务详情链接,可为空、站内相对路径或完整 HTTP(S) 地址
* @return 与 massSend 相同的结果结构;skipped 表示关闭开关而未发送
*/
NutMap send(String loginname, String title, String content, String link);
/**
* 多人发送相同消息,去重并分批调用企业微信接口。
* @param loginNames 接收人 loginname 列表,每项须为企业微信 userid,不接受 @all
* @param title 消息标题,为空使用默认标题
* @param content 消息正文,清理标签后不能为空
* @param link 详情链接;空值为文本消息,非空为文本卡片
* @return NutMap:success 为全部批次被接口接受且无无效接收人;skipped 为未发送;
* batches 为批次结果列表,每项包含 errcode、errmsg、msgid、invaliduser 等接口字段。
* 接口接受不代表用户已读;网络或配置错误抛出业务异常,不自动重发已接受批次。
*/
NutMap massSend(List<String> loginNames, String title, String content, String link);
}
@@ -0,0 +1,189 @@
package com.budwk.app.base.wxwork.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
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.constant.RedisConstant;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.web.commons.base.Globals;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
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.util.NutMap;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/** 企业微信官方消息接口;参考 ypi 的单发、群发及 token 缓存方式,配置与业务调用分离。 */
@Slf4j
@IocBean
public class WxWorkMessageServiceImpl implements WxWorkMessageService {
private static final String PREFIX = "wxwork.message.";
private static final String API = "https://qyapi.weixin.qq.com/cgi-bin/";
@Inject
private PropertiesProxy conf;
@Inject
private RedisService redisService;
/** 参数和返回结构见接口;发送是外部操作,数据库事务无法撤回已发送的消息。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap send(String loginname, String title, String content, String link) {
return massSend(loginname == null ? null : List.of(loginname), title, content, link);
}
/** 先判断独立消息开关,关闭时不读取凭据、不取 token,也不调用外部接口。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap massSend(List<String> loginNames, String title, String content, String link) {
Object switchValue = Globals.MyConfig.get("AppWxWorkMessage");
boolean enabled = Boolean.parseBoolean(switchValue == null
? conf.get(PREFIX + "enabled", "false") : switchValue.toString());
if (!enabled) {
return NutMap.NEW().addv("success", false).addv("skipped", true).addv("batches", List.of());
}
if (loginNames == null || loginNames.isEmpty()
|| loginNames.stream().anyMatch(name -> StrUtil.isBlank(name) || name.contains("|") || name.trim().startsWith("@"))) {
throw new BaseException("企业微信接收人须为非空 loginname,不允许分隔符或群发标记");
}
List<String> receivers = loginNames.stream().map(String::trim).distinct().toList();
String body = plainText(content);
String heading = plainText(StrUtil.blankToDefault(title, conf.get(PREFIX + "default-title", "智慧工会")));
if (StrUtil.isBlank(body)) throw new BaseException("企业微信消息正文不能为空");
String url = resolveLink(link);
long agentId;
try {
agentId = Long.parseLong(required("wxwork.agentId"));
if (agentId <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
throw new BaseException("企业微信 agentId 必须为正整数");
}
JSONObject payload = JSONUtil.createObj().set("agentid", agentId);
if (url == null) {
String text = heading + "\n" + body;
requireLength(text, 2048, "文本消息");
payload.set("msgtype", "text").set("text", JSONUtil.createObj().set("content", text));
} else {
requireLength(heading, 128, "卡片标题");
String description = HtmlUtil.escape(body).replace("\n", "<br>");
requireLength(description, 512, "卡片正文");
requireLength(url, 2048, "卡片链接");
payload.set("msgtype", "textcard").set("textcard", JSONUtil.createObj()
.set("title", heading).set("description", description).set("url", url).set("btntxt", "查看详情"));
}
List<JSONObject> batches = new ArrayList<>();
boolean success = true;
// 企业微信 touser 单次最多 1000 人;按去重后的接收人分批,禁止自动重复整个批次列表。
for (int offset = 0; offset < receivers.size(); offset += 1000) {
payload.set("touser", String.join("|", receivers.subList(offset, Math.min(offset + 1000, receivers.size()))));
String token = getAccessToken(false);
JSONObject result = sendPayload(payload, token);
int code = result.getInt("errcode", -1);
// 仅在明确的 token 无效/过期返回时刷新一次;超时的发送结果不确定,不能盲目重发。
if (code == 40014 || code == 42001 || code == 40001) {
result = sendPayload(payload, getAccessToken(true));
}
batches.add(result);
boolean accepted = result.getInt("errcode", -1) == 0
&& StrUtil.isBlank(result.getStr("invaliduser"))
&& StrUtil.isBlank(result.getStr("unlicenseduser"));
success &= accepted;
log.info("企业微信消息批次完成:offset={},success={},errcode={},msgid={}",
offset, accepted, result.getInt("errcode"), result.getStr("msgid"));
}
return NutMap.NEW().addv("success", success).addv("skipped", false).addv("batches", batches);
}
/** 标题和富文本正文统一转换为可读纯文本,保留常用段落换行。 */
private String plainText(String value) {
return HtmlUtil.unescape(HtmlUtil.cleanHtmlTag(StrUtil.blankToDefault(value, "")
.replaceAll("(?i)<br\\s*/?>|</p>|</div>", "\n"))).trim();
}
/** 使用 UTF-8 字节数校验接口上限,避免中文按字符计数造成发送失败。 */
private void requireLength(String value, int limit, String label) {
if (value.getBytes(StandardCharsets.UTF_8).length > limit) {
throw new BaseException(label + "超过 " + limit + " 字节,请精简内容");
}
}
/** 相对路径按配置的系统域名补全;限制 HTTP(S),避免生成不可打开的卡片链接。 */
private String resolveLink(String link) {
if (StrUtil.isBlank(link)) return null;
try {
URI uri = URI.create(link.trim());
if (!uri.isAbsolute()) {
if (uri.getRawAuthority() != null) throw new IllegalArgumentException();
String base = required(PREFIX + "link-base-url").replaceAll("/+$", "") + "/";
uri = URI.create(base).resolve(uri);
}
if (!("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|| uri.getHost() == null || uri.getUserInfo() != null) throw new IllegalArgumentException();
return uri.toASCIIString();
} catch (IllegalArgumentException e) {
throw new BaseException("企业微信详情链接或系统域名配置无效");
}
}
/** 按企业及应用隔离缓存,与登录 token 缓存分开;提前过期以避开有效期临界点。 */
private synchronized String getAccessToken(boolean refresh) {
String corpId = required("wxwork.corpId");
String agentId = required("wxwork.agentId");
String secret = required("wxwork.corpSecret");
String key = RedisConstant.REDIS_KEY_WX_WORK_TOKEN + "message:" + corpId + ":" + agentId;
String cached = refresh ? null : redisService.get(key);
if (StrUtil.isNotBlank(cached)) return cached;
JSONObject response = execute(HttpRequest.get(API + "gettoken?corpid=" + encode(corpId)
+ "&corpsecret=" + encode(secret)));
String token = response.getStr("access_token");
if (response.getInt("errcode", -1) != 0 || StrUtil.isBlank(token)) {
throw new BaseException("企业微信获取 token 失败,errcode=" + response.getInt("errcode", -1));
}
int expires = response.getInt("expires_in", 7200);
redisService.setex(key, Math.max(1, expires - 120), token);
return token;
}
/** 提交已校验的消息 JSON;返回企业微信原始业务字段供调用方判断部分失败。 */
private JSONObject sendPayload(JSONObject payload, String token) {
return execute(HttpRequest.post(API + "message/send?access_token=" + encode(token))
.header("Content-Type", "application/json; charset=UTF-8").body(payload.toString()));
}
/** 关闭响应资源并校验 HTTP/JSON;异常不输出带 Secret 或 token 的请求 URL。 */
protected JSONObject execute(HttpRequest request) {
request.setConnectionTimeout(conf.getInt(PREFIX + "connect-timeout", 5000));
request.setReadTimeout(conf.getInt(PREFIX + "read-timeout", 10000));
try (HttpResponse response = request.execute()) {
if (!response.isOk()) throw new BaseException("企业微信接口 HTTP 状态异常:" + response.getStatus());
if (!JSONUtil.isTypeJSONObject(response.body())) throw new BaseException("企业微信接口返回非 JSON 对象");
return JSONUtil.parseObj(response.body());
} catch (BaseException e) {
throw e;
} catch (Exception e) {
throw new BaseException("企业微信请求失败,请检查网络及接口配置;异常类型:" + e.getClass().getSimpleName());
}
}
/** 必填参数不允许为空或使用原有占位符 1,防止错误配置请求外部接口。 */
private String required(String key) {
String value = conf.get(key, "").trim();
if (StrUtil.isBlank(value) || "1".equals(value)) throw new BaseException("请配置有效的 " + key);
return value;
}
private String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
@@ -21,6 +21,7 @@ import com.budwk.app.flow.vo.HighLightVO;
import com.budwk.app.flow.vo.ProcessInstanceVO; import com.budwk.app.flow.vo.ProcessInstanceVO;
import com.budwk.app.flow.vo.ProcessTaskVO; import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
@@ -251,6 +252,15 @@ public class FlowCommonController {
public Result revokeTask(@Param("taskId") Long taskId) { public Result revokeTask(@Param("taskId") Long taskId) {
// 自己任务 // 自己任务
ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId); ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId);
if ("startTask".equals(selfTask.getTaskName())) {
ProcessInstance instance = dao.fetch(ProcessInstance.class, selfTask.getProcessInstanceId());
if (instance != null && StrUtil.isNotBlank(instance.getBusinessNo())) {
DifficultHelpInfo helpInfo = dao.fetch(DifficultHelpInfo.class, instance.getBusinessNo());
if (helpInfo != null && !SecurityUtil.getUserId().equals(helpInfo.getCreatedBy())) {
return Result.error("只能撤回自己创建的困难帮扶申请");
}
}
}
selfTask.setTaskState(ProcessTaskStateEnum.DOING.getCode()); selfTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
// 撤销任务 // 撤销任务
@@ -0,0 +1,72 @@
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 com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.lang.Lang;
import java.util.List;
public class FlowRecipientUnitSchoolLeaderHandler implements AssignmentHandler {
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
String businessNo = execution.getProcessInstance().getBusinessNo();
if (StrUtil.isBlank(businessNo)) {
throw new BaseException("申请记录编号不能为空。");
}
String processName = execution.getProcessModel().getName();
String unitId;
if ("KNBF".equals(processName)) {
DifficultHelpInfo difficultHelpInfo = dao.fetch(DifficultHelpInfo.class, businessNo);
if (difficultHelpInfo == null) {
throw new BaseException("未找到申请记录。");
}
unitId = difficultHelpInfo.getUnitId();
} else if ("ZGWW".equals(processName)) {
Condolence condolence = dao.fetch(Condolence.class, businessNo);
if (condolence == null) {
throw new BaseException("未找到申请记录。");
}
unitId = condolence.getHelpUnitId();
} else if ("TSWW".equals(processName)) {
com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence info = dao.fetch(
com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence.class, businessNo);
if (info == null) throw new BaseException("未找到申请记录。");
unitId = info.getHelpUnitId();
} else {
throw new BaseException("当前流程不支持获取受助人所在单位分管校领导。");
}
if (StrUtil.isBlank(unitId)) {
throw new BaseException("受助人所在单位不能为空。");
}
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
Sys_role role = sysRoleService.getByCode(RoleConstant.UNIT_SCHOOL_LEADER);
List<Sys_user_role> roles = dao.query(
Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUnitId, "=", unitId)
);
if (Lang.isEmpty(roles)) {
throw new BaseException("受助人/申请人所在单位未设置单位分管校领导,请联系校工会进行设置。");
}
return roles.stream().map(Sys_user_role::getUserId).distinct().toList();
}
@Override
public String getMessage() {
return "获取受助人所在单位分管校领导";
}
}
@@ -0,0 +1,44 @@
package com.budwk.app.flow.handler;
import com.budwk.app.base.constant.RoleConstant;
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 org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import java.util.List;
public class FlowSchoolOfficeDirectorHandler implements AssignmentHandler {
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
Sql sql = Sqls.create("""
SELECT
userRole.userId
FROM
`sys_user_role` userRole
LEFT JOIN sys_role role ON role.id = userRole.roleId
WHERE
role.`code` = @roleCode
GROUP BY
userRole.userId
""").setParam("roleCode", RoleConstant.SCHOOL_OFFICE_DIRECTOR.name());
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
return sql.getList(String.class);
}
@Override
public String getMessage() {
return "获取校工会办公室主任";
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
}
@@ -0,0 +1,54 @@
package com.budwk.app.flow.handler;
import com.budwk.app.base.constant.RoleConstant;
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 org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import java.util.List;
/**
* 为校工会主席审核任务分配拥有校工会主席角色的用户。
*/
public class FlowSchoolUnionChairmanHandler implements AssignmentHandler {
/**
* 按校工会主席角色查询参与者,去除同一用户的重复角色记录。
*
* @param model 当前流程任务模型,无需额外传入人员参数
* @param execution 当前流程执行上下文,无需额外传入业务参数
* @return 用户 ID 列表,供流程引擎创建任务参与者;未配置角色用户时返回空列表
*/
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
Sql sql = Sqls.create("""
SELECT
userRole.userId
FROM
`sys_user_role` userRole
LEFT JOIN sys_role role ON role.id = userRole.roleId
WHERE
role.`code` = @roleCode
GROUP BY
userRole.userId
""").setParam("roleCode", RoleConstant.SCHOOL_UNION_CHAIRMAN.name());
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
return sql.getList(String.class);
}
@Override
public String getMessage() {
return "获取校工会主席";
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
}
@@ -0,0 +1,49 @@
package com.budwk.app.flow.handler;
import com.budwk.app.base.constant.RoleConstant;
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 org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import java.util.List;
/**
* 为分管校领导审核任务分配拥有 UNIT_SCHOOL_LEADER 角色的用户。
*/
public class FlowUnitSchoolLeaderHandler implements AssignmentHandler {
/**
* 按角色查询参与者,去除同一用户的重复角色记录。
*
* @param model 当前流程任务模型,无需额外传入人员参数
* @param execution 当前流程执行上下文,无需额外传入业务参数
* @return 用户 ID 列表,供流程引擎创建任务参与者;未配置角色用户时返回空列表
*/
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
Sql sql = Sqls.create("""
SELECT
userRole.userId
FROM
sys_user_role userRole
LEFT JOIN sys_role role ON role.id = userRole.roleId
WHERE
role.`code` = @roleCode
GROUP BY
userRole.userId
""").setParam("roleCode", RoleConstant.UNIT_SCHOOL_LEADER.name());
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
return sql.getList(String.class);
}
@Override
public String getMessage() {
return "获取分管校领导";
}
}
@@ -4,6 +4,7 @@ import com.budwk.app.flow.engine.event.ProcessEvent;
import com.budwk.app.flow.engine.event.ProcessEventListener; import com.budwk.app.flow.engine.event.ProcessEventListener;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessEventTypeEnum; import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.service.SchoolOaTodoService;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -17,11 +18,16 @@ public class ProcessTaskEndEventListener implements ProcessEventListener {
@Inject @Inject
private Dao dao; private Dao dao;
@Inject
private SchoolOaTodoService schoolOaTodoService;
@Override @Override
public void onEvent(ProcessEvent event) { public void onEvent(ProcessEvent event) {
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_END) { if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_END) {
Long sourceId = event.getSourceId(); Long sourceId = event.getSourceId();
ProcessTask task = dao.fetch(ProcessTask.class, sourceId); ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
// service 区分正常完成与废弃,只有正常完成才发送通达 status=2。
schoolOaTodoService.completeTodo(task);
System.out.println("流程任务完成事件:" + task.getDisplayName()); System.out.println("流程任务完成事件:" + task.getDisplayName());
} }
} }
@@ -9,6 +9,7 @@ import com.budwk.app.flow.engine.event.ProcessEventListener;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessEventTypeEnum; import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.service.SchoolOaTodoService;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -22,11 +23,16 @@ public class ProcessTaskRevokeEventListener implements ProcessEventListener {
@Inject @Inject
private Dao dao; private Dao dao;
@Inject
private SchoolOaTodoService schoolOaTodoService;
@Override @Override
public void onEvent(ProcessEvent event) { public void onEvent(ProcessEvent event) {
if(event.getEventType()== ProcessEventTypeEnum.PROCESS_TASK_REVOKE){ if(event.getEventType()== ProcessEventTypeEnum.PROCESS_TASK_REVOKE){
Long taskId = event.getSourceId(); Long taskId = event.getSourceId();
ProcessTask task = dao.fetch(ProcessTask.class, taskId); ProcessTask task = dao.fetch(ProcessTask.class, taskId);
// 公开通达协议未定义取消,集中记录未同步事项,不把撤回冒充正常已办。
schoolOaTodoService.cancelTodo(task);
String taskDisplayName = task.getDisplayName(); String taskDisplayName = task.getDisplayName();
@@ -9,6 +9,7 @@ import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor; import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessEventTypeEnum; import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
@@ -30,13 +31,22 @@ public class ProcessTaskStartEventListener implements ProcessEventListener {
private Dao dao; private Dao dao;
@Inject @Inject
private GlobalMessageSendService globalMessageSendService; private GlobalMessageSendService globalMessageSendService;
@Inject
private DifficultHelpCommonService difficultHelpCommonService;
@Override @Override
public void onEvent(ProcessEvent event) { public void onEvent(ProcessEvent event) {
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) { if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_START) {
Long sourceId = event.getSourceId(); Long sourceId = event.getSourceId();
ProcessTask task = dao.fetch(ProcessTask.class, sourceId); ProcessTask task = dao.fetch(ProcessTask.class, sourceId);
if (task == null) return;
sendMessage(task); sendMessage(task);
// 节点及接收人判断集中在业务 service;通知失败不向流程提交接口抛出异常。
try {
difficultHelpCommonService.notifyOfficeApproval(task);
} catch (Exception e) {
log.error("困难帮扶企业微信待办通知异常,taskId={}", sourceId, e);
}
} }
} }
@@ -0,0 +1,27 @@
package com.budwk.app.flow.service;
import com.budwk.app.flow.entity.ProcessTask;
/** 通达 OA 待办同步入口,WF 调用方无需处理鉴权、人员映射或接口参数。 */
public interface SchoolOaTodoService {
/**
* 推送已创建且已分配参与人的 WF 任务;首次自动提交的 startTask 不发送。
* @param task 包含任务 ID、实例 ID、状态和表单地址的完整任务
* @return 无返回值;开关关闭时跳过,开启后请求失败抛出业务异常
*/
void createTodo(ProcessTask task);
/**
* 将已完成任务对应的接收人待办同步为 status=2;非完成状态不冒充已办。
* @param task 数据库中的完整任务,使用原发起人、接收人、标题和地址组装请求
* @return 无返回值;errcode=0 表示接口接受请求,原待办匹配规则仍需学校联调确认
*/
void completeTodo(ProcessTask task);
/**
* 记录需取消的 OA 待办。通达公开文档未提供取消接口,本方法不发送虚构的删除请求。
* @param task 撤回、废弃或转办的原任务,用于记录任务 ID 和状态
* @return 无返回值;仅在开关开启时输出待人工处理提示,不代表 OA 已取消
*/
void cancelTodo(ProcessTask task);
}
@@ -29,6 +29,7 @@ import com.budwk.app.flow.enums.*;
import com.budwk.app.flow.service.ProcessDefineService; import com.budwk.app.flow.service.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService; import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.flow.service.SchoolOaTodoService;
import com.budwk.app.flow.vo.ProcessTaskVO; import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
@@ -56,6 +57,9 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
@Inject @Inject
private ProcessDefineService processDefineService; private ProcessDefineService processDefineService;
@Inject
private SchoolOaTodoService schoolOaTodoService;
@Override @Override
public boolean save(ProcessTask param) { public boolean save(ProcessTask param) {
param.setId(null); param.setId(null);
@@ -213,6 +217,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
System.out.println("创建任务:" + processTask.getTaskName() + "," + processTask.getDisplayName()); System.out.println("创建任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
processTaskList.add(processTask); processTaskList.add(processTask);
addTaskActor(processTask.getId(), getTaskActors(taskModel, execution)); addTaskActor(processTask.getId(), getTaskActors(taskModel, execution));
// 参与人落库后统一推送通达待办,开始监听器不再重复推送。
schoolOaTodoService.createTodo(processTask);
return processTaskList; return processTaskList;
} }
@@ -284,6 +290,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
clone.setUpdatedBy(null); clone.setUpdatedBy(null);
dao().insert(clone); dao().insert(clone);
addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor)); addTaskActor(clone.getId(), CollectionUtil.newArrayList(actor));
// 并行加签绕过普通节点创建入口,需要为新增的个人任务同步待办。
schoolOaTodoService.createTodo(clone);
}); });
} }
@@ -318,6 +326,7 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public ProcessTask rejectTask(ProcessModel model, ProcessTask currentTask) { public ProcessTask rejectTask(ProcessModel model, ProcessTask currentTask) {
Long taskParentId = currentTask.getTaskParentId(); Long taskParentId = currentTask.getTaskParentId();
if (ObjectUtil.isEmpty(taskParentId) || taskParentId.equals(0L)) { if (ObjectUtil.isEmpty(taskParentId) || taskParentId.equals(0L)) {
@@ -351,6 +360,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
} }
saveProcessTask(task); saveProcessTask(task);
addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator())); addTaskActor(task.getId(), CollectionUtil.newArrayList(task.getOperator()));
// 驳回创建了新的任务 ID,按新任务推送,避免复用历史待办。
schoolOaTodoService.createTodo(task);
return task; return task;
} }
@@ -424,6 +435,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
System.out.println("创建会签任务:" + processTask.getTaskName() + "," + processTask.getDisplayName()); System.out.println("创建会签任务:" + processTask.getTaskName() + "," + processTask.getDisplayName());
processTaskList.add(processTask); processTaskList.add(processTask);
addTaskActor(processTask.getId(), CollectionUtil.newArrayList(taskActor)); addTaskActor(processTask.getId(), CollectionUtil.newArrayList(taskActor));
// 串行和并行会签均按实际生成的个人任务发送待办。
schoolOaTodoService.createTodo(processTask);
}); });
ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class); ProcessInstanceService processInstanceService = ServiceContext.find(ProcessInstanceService.class);
// 更新会签变量到流程实例参数中 // 更新会签变量到流程实例参数中
@@ -496,9 +509,12 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
// 创建转办任务参与者 // 创建转办任务参与者
addTaskActor(newTask.getId(), List.of(transferUserId)); addTaskActor(newTask.getId(), List.of(transferUserId));
// 通达取消协议待学校确认;先明确记录原待办未清理,再向新办理人推送。
schoolOaTodoService.cancelTodo(task);
schoolOaTodoService.createTodo(newTask);
// 发布流程任务开始事件 // 发布流程任务开始事件
List<ProcessTask> processTaskList = List.of(task); List<ProcessTask> processTaskList = List.of(newTask);
processTaskList.forEach(processTask -> { processTaskList.forEach(processTask -> {
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(processTask.getId()).build()); ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(processTask.getId()).build());
}); });
@@ -0,0 +1,344 @@
package com.budwk.app.flow.service.impl;
import cn.hutool.core.util.StrUtil;
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.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.SchoolOaTodoService;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.base.Globals;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.IdentityHashMap;
import java.util.stream.Collectors;
/**
* 通达 V13 流程中心接口适配。所有配置、token 和 HTTP 请求集中在本服务。
* 接口依据:https://open.tongda2000.com/oa/v13/interface.html
* 同步请求失败向调用方抛出异常;本地事务不能撤销已经成功的远端请求。
*/
@Slf4j
@IocBean
public class SchoolOaTodoServiceImpl implements SchoolOaTodoService {
private static final String PREFIX = "school-oa.todo.";
@Inject
private Dao dao;
@Inject
private PropertiesProxy conf;
// token 由同步方法保护;实例内缓存有效期,避免每位接收人重复申请凭据。
private String accessToken;
private long tokenExpiresAt;
private String tokenConfig;
@Override
@Aop(TransAop.READ_COMMITTED)
public void createTodo(ProcessTask task) {
if (!isEnabled()) return;
requireTask(task);
if (isInitialTask(task) || !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) return;
sendTask(task, 1);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void completeTodo(ProcessTask task) {
if (!isEnabled()) return;
requireTask(task);
if (isInitialTask(task)) return;
// 废弃任务也会发布结束事件;尤其提案附议仍可能可办理,不能直接改为 OA 已办。
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
cancelTodo(task);
return;
}
sendTask(task, 2);
}
@Override
public void cancelTodo(ProcessTask task) {
if (!isEnabled()) return;
requireTask(task);
if (isInitialTask(task)) return;
log.warn("通达OA取消规则尚未提供,本次未发送取消请求,请核对OA原待办:taskId={},instanceId={},state={}",
task.getId(), task.getProcessInstanceId(), task.getTaskState());
}
/** 系统参数优先于文件配置,只有明确为 true 才启用;未配置不访问数据库或 OA。 */
private boolean isEnabled() {
Object enabled = Globals.MyConfig.get("AppSchoolOa");
return Boolean.parseBoolean(enabled == null ? conf.get(PREFIX + "enabled", "false") : enabled.toString());
}
private void requireTask(ProcessTask task) {
if (task == null || task.getId() == null || task.getProcessInstanceId() == null) {
throw new BaseException("通达OA同步失败:任务或流程实例ID为空");
}
}
private boolean isInitialTask(ProcessTask task) {
return Long.valueOf(0L).equals(task.getTaskParentId()) && "startTask".equals(task.getTaskName());
}
/**
* task 为完整 WF 任务;status 只允许本服务传入 1(待办)或 2(已办)。
* sender/recipient 使用学校绑定的第三方用户标识;content 为 JSON 字符串,url 为 UTF-8 Base64。
* 每个接收人单独请求,响应 JSON 中必须有 errcode=0;不采用参考项目的 uniqueId/state 协议。
*/
private void sendTask(ProcessTask task, int status) {
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null) throw new BaseException("通达OA同步失败:流程实例不存在");
Sys_user sender = dao.fetch(Sys_user.class, instance.getOperator());
if (sender == null) throw new BaseException("通达OA同步失败:流程发起人不存在");
List<ProcessTaskActor> actors = dao.query(ProcessTaskActor.class,
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
if (actors.isEmpty()) throw new BaseException("通达OA同步失败:任务参与人为空");
String senderId = userIdentifier(sender.getId(), sender.getLoginname());
String title = StrUtil.blankToDefault(JSONUtil.parseObj(StrUtil.blankToDefault(instance.getVariable(), "{}"))
.getStr(FlowConst.PROCESS_INSTANCE_NAME), "流程申请") + " - " + task.getDisplayName();
String targetUrl = buildTaskUrl(task, instance);
String systemParam = conf.get(PREFIX + "system-id-param", "clientid");
if (!"clientid".equals(systemParam) && !"exsId".equals(systemParam)) {
throw new BaseException("通达OA配置 system-id-param 仅支持 clientid 或 exsId");
}
Map<String, Object> params = new LinkedHashMap<>();
params.put(systemParam, requireConfig("system-id"));
params.put("status", status);
params.put("sender", senderId);
params.put("content", JSONUtil.quote(title));
params.put("url", Base64.getEncoder().encodeToString(targetUrl.getBytes(StandardCharsets.UTF_8)));
// 先校验全部人员,避免处理到中途才发现缺少账号;不假设本地用户 ID 就是 OA 用户 ID。
List<String> recipients = actors.stream()
.filter(actor -> !FlowConst.ADMIN_ID.equals(actor.getActorId())
&& !"superadmin".equalsIgnoreCase(actor.getActorAccount()))
.map(actor -> userIdentifier(actor.getActorId(), actor.getActorAccount())).distinct().toList();
for (String recipient : recipients) {
params.put("recipient", recipient);
String endpoint = baseUrl() + "/restapi/1_0/appcenter/affairprocessing?" + encodeParams(params);
JSONObject result = execute(HttpRequest.get(endpoint).header("Authorization", "Bearer " + getAccessToken()), "待办同步");
Integer errorCode = result.getInt("errcode");
if (errorCode == null || errorCode != 0) {
String detail = responseError(result);
log.error("通达OA待办业务失败,taskId={},instanceId={},status={},errcode={},detail={}",
task.getId(), task.getProcessInstanceId(), status, errorCode, detail);
throw new BaseException("通达OA待办同步失败:errcode={},taskId={},{}", errorCode, task.getId(), detail);
}
log.info("通达OA接受待办同步请求,taskId={},status={}", task.getId(), status);
}
}
/** 用户映射按学校绑定选择 loginname 或 id,发送人与接收人使用同一规则。 */
private String userIdentifier(String id, String account) {
String mode = conf.get(PREFIX + "user-id-mode", "loginname");
if (!"loginname".equals(mode) && !"id".equals(mode)) {
throw new BaseException("通达OA配置 user-id-mode 仅支持 loginname 或 id");
}
String value = "id".equals(mode) ? id : account;
if (StrUtil.isBlank(value) || value.contains(",")) {
throw new BaseException("通达OA同步失败:绑定用户标识为空或包含逗号");
}
return value;
}
/** 通达仅有一个 url 字段,沿用当前 PC 审核页并携带任务、实例及业务参数。 */
private String buildTaskUrl(ProcessTask task, ProcessInstance instance) {
String form = task.getFormKey();
if (StrUtil.isBlank(form)) throw new BaseException("通达OA同步失败:任务PC表单地址为空");
String url = form.startsWith("http://") || form.startsWith("https://") ? form
: StrUtil.removeSuffix(Globals.AppDomain, "/") + "/" + StrUtil.removePrefix(form, "/");
// 原表单带锚点时,查询参数必须放在 # 前,否则服务端收不到 taskId。
int fragmentIndex = url.indexOf('#');
String fragment = fragmentIndex < 0 ? "" : url.substring(fragmentIndex);
if (fragmentIndex >= 0) url = url.substring(0, fragmentIndex);
Map<String, Object> query = new LinkedHashMap<>();
query.put("taskId", task.getId());
query.put("instanceId", instance.getId());
query.put("bizId", StrUtil.nullToEmpty(instance.getBusinessNo()));
query.put("taskKey", StrUtil.nullToEmpty(task.getTaskName()));
return url + (url.contains("?") ? "&" : "?") + encodeParams(query) + fragment;
}
/**
* POST /oauth2/token:client_secret 必须是学校提供的 AES 加密结果,不是明文密钥。
* 返回 access_token 和 expires_in(秒);缓存提前过期,配置变化立即重新申请。
*/
private synchronized String getAccessToken() {
String base = baseUrl();
String clientId = requireConfig("client-id");
String secret = requireConfig("client-secret-encrypted");
String username = conf.get(PREFIX + "token-username", "username");
String password = conf.get(PREFIX + "token-password", "password");
String currentConfig = JSONUtil.toJsonStr(List.of(base, clientId, secret, username, password));
long now = System.currentTimeMillis();
if (currentConfig.equals(tokenConfig) && StrUtil.isNotBlank(accessToken) && now < tokenExpiresAt) {
// 只记录缓存剩余有效期,便于区分复用与重新获取,不输出凭据。
log.info("通达OA复用token缓存,remainingSeconds={}", (tokenExpiresAt - now) / 1000L);
return accessToken;
}
Map<String, Object> form = new LinkedHashMap<>();
form.put("grant_type", "password");
form.put("client_id", clientId);
form.put("client_secret", secret);
form.put("username", username);
form.put("password", password);
long startedAt = System.nanoTime();
JSONObject result = execute(HttpRequest.post(base + "/oauth2/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.body(encodeParams(form)), "获取凭据");
String token = result.getStr("access_token");
Long expires = result.getLong("expires_in");
if (StrUtil.isBlank(token) || expires == null || expires <= 0 || expires > Integer.MAX_VALUE) {
String detail = responseError(result);
log.error("通达OA获取凭据业务失败,缺少有效的 access_token 或 expires_in,detail={}", detail);
throw new BaseException("通达OA获取凭据失败:缺少有效的 access_token 或 expires_in,{}", detail);
}
accessToken = token;
tokenConfig = currentConfig;
tokenExpiresAt = now + (expires - Math.min(60, expires / 10)) * 1000L;
log.info("通达OA获取token成功,elapsedMs={},expiresInSeconds={}",
(System.nanoTime() - startedAt) / 1_000_000, expires);
return token;
}
/** HTTP 成功不代表业务成功;本方法只验证状态码和 JSON,业务码由调用处校验。 */
private JSONObject execute(HttpRequest request, String action) {
long startedAt = System.nanoTime();
int timeout = conf.getInt(PREFIX + "timeout-ms", 10000);
// 只记录接口地址,不记录 GET 查询参数、请求头或 token 请求正文。
String endpoint = request.getUrl().split("[?#]", 2)[0];
try (HttpResponse response = request.timeout(timeout).execute()) {
String body = response.body();
boolean jsonObject = JSONUtil.isTypeJSONObject(body);
// 非 JSON 错误常为 OA 或网关的 HTML 页面;仅在日志保留脱敏摘要,不把页面返回给前端。
if (!jsonObject) {
log.error("通达OA非JSON响应,action={},method={},endpoint={},httpStatus={},contentType={},bodySummary={}",
action, request.getMethod(), endpoint, response.getStatus(),
redact(response.header("Content-Type")), responseBodySummary(body));
}
if (!response.isOk()) {
if (response.getStatus() == 401) invalidateToken();
String detail = jsonObject
? responseError(JSONUtil.parseObj(body)) : "响应不是JSON对象,脱敏响应摘要请查看服务端日志";
throw new BaseException("通达OA{}失败:HTTP {},{}", action, response.getStatus(), detail);
}
if (!jsonObject) throw new BaseException("通达OA{}失败:响应不是JSON对象,脱敏响应摘要请查看服务端日志", action);
return JSONUtil.parseObj(body);
} catch (Exception e) {
long elapsed = (System.nanoTime() - startedAt) / 1_000_000;
Throwable root = e;
Map<Throwable, Boolean> visited = new IdentityHashMap<>();
while (root.getCause() != null && visited.put(root, Boolean.TRUE) == null
&& !visited.containsKey(root.getCause())) root = root.getCause();
String detail = root.getClass().getName() + ": " + redact(root.getMessage());
// 保留各层原因、原始栈帧及 suppressed 异常;先复制脱敏,避免全局异常处理器泄露凭证。
Throwable safeCause = diagnosticCause(e, new IdentityHashMap<>());
log.error("通达OA请求失败,action={},method={},endpoint={},elapsedMs={},timeoutMs={},rootCause={}",
action, request.getMethod(), endpoint, elapsed, timeout, detail, safeCause);
// 不自动重放写请求,超时可能发生在远端已成功之后;避免重复待办。
BaseException failure = new BaseException("通达OA{}失败,耗时{}ms:{};请核对远端结果后再重试", action, elapsed, detail);
failure.initCause(safeCause);
throw failure;
}
}
/** body 为远端非 JSON 正文;先脱敏再截取最多 2000 字符,避免截断凭据后无法匹配脱敏。 */
private String responseBodySummary(String body) {
if (StrUtil.isBlank(body)) return "[空响应正文]";
String safe = redact(body);
return safe.length() > 2000 ? safe.substring(0, 2000) + "[已截断]" : safe;
}
/** 只提取接口错误字段,不输出含 access_token、refresh_token 等内容的整个响应。 */
private String responseError(JSONObject response) {
Map<String, Object> errors = new LinkedHashMap<>();
for (String key : List.of("error", "error_description", "errcode", "errmsg", "message")) {
if (response.containsKey(key)) errors.put(key, redact(String.valueOf(response.get(key))));
}
return errors.isEmpty() ? "OA未返回错误说明" : JSONUtil.toJsonStr(errors);
}
/**
* 复制异常诊断信息,message 标注原异常类型,保留原始栈帧和原因链。
* visited 防止第三方异常存在循环引用;原异常不修改,也不直接写入日志。
*/
private Throwable diagnosticCause(Throwable source, Map<Throwable, Throwable> visited) {
if (visited.containsKey(source)) return visited.get(source);
RuntimeException copy = new RuntimeException(source.getClass().getName() + ": " + redact(source.getMessage()));
visited.put(source, copy);
copy.setStackTrace(source.getStackTrace());
if (source.getCause() != null && !visited.containsKey(source.getCause())) {
copy.initCause(diagnosticCause(source.getCause(), visited));
}
for (Throwable suppressed : source.getSuppressed()) {
if (!visited.containsKey(suppressed)) copy.addSuppressed(diagnosticCause(suppressed, visited));
}
return copy;
}
/** 清除已知凭证及可能被远端回显的认证字段、URL查询参数,并移除日志换行。 */
private String redact(String message) {
if (message == null) return "无异常说明";
String text = message;
for (String key : List.of("client-secret-encrypted", "token-password", "client-id")) {
String value = conf.get(PREFIX + key);
if (StrUtil.isNotBlank(value)) {
text = text.replace(value, "[REDACTED]")
.replace(URLEncoder.encode(value, StandardCharsets.UTF_8), "[REDACTED]");
}
}
if (StrUtil.isNotBlank(accessToken)) text = text.replace(accessToken, "[REDACTED]");
text = text.replaceAll("(?i)Bearer\\s+[^\\s,;\\\"']+", "Bearer [REDACTED]");
text = text.replaceAll("(?i)((?:client_secret|access_token|refresh_token|password|authorization)[\\\"']?\\s*[:=]\\s*)[\\\"']?[^\\s&,;}\\\"']+[\\\"']?", "$1[REDACTED]");
text = text.replaceAll("(https?://[^\\s?#]+)\\?[^\\s]+", "$1?[REDACTED]");
return text.replace('\r', ' ').replace('\n', ' ');
}
private synchronized void invalidateToken() {
accessToken = null;
tokenExpiresAt = 0;
}
private String baseUrl() {
String value = requireConfig("base-url");
URI uri = URI.create(value);
if ((!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme()))
|| uri.getHost() == null || uri.getQuery() != null || uri.getFragment() != null || uri.getUserInfo() != null) {
throw new BaseException("通达OA base-url 必须为不含账号、查询参数和锚点的HTTP(S)地址");
}
return StrUtil.removeSuffix(value, "/");
}
private String requireConfig(String key) {
String value = conf.get(PREFIX + key);
if (StrUtil.isBlank(value)) throw new BaseException("通达OA配置 {} 未填写", PREFIX + key);
return value.trim();
}
/** 每个参数单独编码,保留标题中的中文、引号及 Base64 中的 +、/、=。 */
private String encodeParams(Map<String, Object> params) {
return params.entrySet().stream().map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)
+ "=" + URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
}
@@ -11,7 +11,8 @@ import java.util.Map;
public interface SysDataUserPullService extends BaseService<Sys_user_source> { public interface SysDataUserPullService extends BaseService<Sys_user_source> {
/** /**
* 从信息中心拉取数据 * 从本地中间表同步单位,并将人员写入 sys_user_source 快照;不更新 sys_user。
* 返回本次人员快照的拉取时间。
*/ */
Date pull(); Date pull();
@@ -1,15 +1,6 @@
package com.budwk.app.sys.services.impl; package com.budwk.app.sys.services.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.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.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit; import com.budwk.app.sys.models.Sys_unit;
@@ -17,41 +8,24 @@ import com.budwk.app.sys.services.SysDataUnitPullService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* @version 1.0 * 将本地单位中间表同步到系统单位表。
* @Author zzr
* @name:SysDataUnitPullServiceImpl
* @Date 2025/9/15 17:52
* @注释
*/ */
@Slf4j @Slf4j
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService { public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
private static final String DMP_UNIT_KEY = "unit";
private static final int DMP_PAGE_SIZE = 1000;
@Inject
private DataCenterProperties dataCenterProperties;
public SysDataUnitPullServiceImpl(Dao dao) { public SysDataUnitPullServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void updateUnits() { public void updateUnits() {
@@ -59,240 +33,65 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
} }
/** /**
* 拉取单位数据 * 从 unit_middle 同步全部二级单位。
* unitIds 是旧调用链传入的已有单位 ID 列表;中间表同步按全表执行,不依赖此参数。
* 返回值为空;已存在单位只更新中间表明确提供的字段。
*/ */
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void updateUnits(List<String> unitIds) { public void updateUnits(List<String> unitIds) {
String accessToken = getDmpAccessToken(); List<NutMap> middleUnits = listMap(Sqls.create("SELECT DWDM, DWMC, BMFLMC FROM unit_middle"));
List<JSONObject> rawDataList = distinctDmpUnits(pullDmpUnits(getDmpUnitUrl(), accessToken)); if (middleUnits.isEmpty()) {
Set<String> parentCodes = rawDataList.stream() throw new BaseException("本地单位中间表没有数据,已停止同步");
.map(this::getNormalizedParentCode)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
rawDataList.stream()
.sorted(Comparator.comparingInt(this::getUnitLevel))
.forEach(raw -> saveOrUpdateUnit(raw, parentCodes));
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); for (NutMap raw : middleUnits) {
String tokenResBody = HttpUtil.createGet(tokenUrl + "?" + params).execute().body(); String unitCode = StrUtil.trim(raw.getString("DWDM"));
JSONObject tokenJsonBody = JSONUtil.parseObj(tokenResBody); String unitName = StrUtil.trim(raw.getString("DWMC"));
String accessToken = tokenJsonBody.getStr("access_token"); if (StrUtil.isBlank(unitCode) || unitCode.length() != 3 || StrUtil.isBlank(unitName)) {
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("result") != null) { throw new BaseException("单位中间表存在无效的二级单位代码或名称,已停止同步");
accessToken = tokenJsonBody.getJSONObject("result").getStr("access_token");
}
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("data") != null) {
accessToken = tokenJsonBody.getJSONObject("data").getStr("access_token");
}
if (StrUtil.isBlank(accessToken)) {
accessToken = tokenJsonBody.getStr("token");
}
if (StrUtil.isBlank(accessToken)) {
throw new BaseException("获取信息中心token失败");
}
return accessToken;
}
/**
* 读取信息中心单位接口地址。
* 配置项为 data-center.urls.unit,返回值为单位全量接口URL。
*/
private String getDmpUnitUrl() {
String url = dataCenterProperties.getUrls().get(DMP_UNIT_KEY);
if (StrUtil.isBlank(url)) {
throw new BaseException("未配置信息中心单位接口地址: data-center.urls." + DMP_UNIT_KEY);
}
return url;
}
/**
* 分页拉取单位接口数据。
* 入参 url 为单位接口地址,accessToken 为信息中心令牌;
* 请求 body 包含 access_token、per_page、page;
* 返回值为 result.data 合并后的单位原始 JSON 列表。
*/
private List<JSONObject> pullDmpUnits(String url, String accessToken) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
int total = 0;
do {
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
log.info("请求信息中心单位数据,第{}页: {}", page, httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求信息中心单位数据第{}页结果: {}", page, resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 10000) {
throw new BaseException("获取信息中心单位数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
} }
JSONObject result = jsonBody.getJSONObject("result"); Sys_unit unit = fetch(unitCode);
if (result == null) { if (unit == null) {
break; unit = new Sys_unit();
unit.setId(unitCode);
unit.setParentId("0");
unit.setPath(getUnitPath("0"));
unit.setAliasName(unitName);
unit.setUnitTypeCode(1);
unit.setHasChildren(false);
unit.setName(unitName);
unit.setUnitcode(unitCode);
unit.setUnitLevel(2);
if (StrUtil.isNotBlank(raw.getString("BMFLMC"))) {
unit.setUnitType(raw.getString("BMFLMC"));
}
dao().fastInsert(unit);
} else {
unit.setName(unitName);
unit.setUnitcode(unitCode);
unit.setUnitLevel(2);
unit.setUnitTypeCode(1);
unit.setParentId("0");
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath("0"));
}
if (StrUtil.isNotBlank(raw.getString("BMFLMC"))) {
unit.setUnitType(raw.getString("BMFLMC"));
}
dao().updateIgnoreNull(unit);
} }
total = result.getInt("total", total); }
maxPage = result.getInt("max_page", maxPage); log.info("本地单位中间表同步完成,单位数量: {}", middleUnits.size());
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 重复写入。 * 按系统根单位的 path 生成新二级单位路径,供单位树正常展示。
*/
private List<JSONObject> distinctDmpUnits(List<JSONObject> rawDataList) {
Map<String, JSONObject> unitMap = new LinkedHashMap<>();
for (JSONObject raw : rawDataList) {
String unitCode = raw.getStr("DWDM");
if (StrUtil.isBlank(unitCode)) {
continue;
}
unitMap.putIfAbsent(unitCode, raw);
}
return new ArrayList<>(unitMap.values());
}
/**
* 保存或更新单位。
* DWDM 对应系统单位 id/unitcode,DWMC 对应名称,LSDWH 对应父级单位号,DWCC 对应信息中心原始单位层级。
*/
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes) {
String unitCode = raw.getStr("DWDM");
if (StrUtil.isBlank(unitCode)) {
return;
}
String parentId = getParentId(unitCode, getNormalizedParentCode(raw));
Sys_unit unit = fetch(unitCode);
if (unit == null) {
unit = new Sys_unit();
unit.setId(unitCode);
unit.setUnitcode(unitCode);
unit.setPath(getUnitPath(parentId));
setUnitValue(unit, raw, parentId, parentCodes);
dao().fastInsert(unit);
} else {
setUnitValue(unit, raw, parentId, parentCodes);
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath(parentId));
}
dao().updateIgnoreNull(unit);
}
}
/**
* 根据接口字段给系统单位赋值。
* DWYXBS 表示单位是否有效,DWCC 表示信息中心原始单位层次;
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
*/
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) {
String unitCode = raw.getStr("DWDM");
String unitName = raw.getStr("DWMC");
String aliasName = StrUtil.blankToDefault(raw.getStr("DWJC"), unitName);
Integer rawUnitLevel = raw.getInt("DWCC");
Integer unitLevel = getNormalizedUnitLevel(raw);
boolean availableBusinessUnit = "是".equals(raw.getStr("DWYXBS")) && Integer.valueOf(2).equals(rawUnitLevel);
unit.setParentId(parentId);
unit.setName(unitName);
unit.setAliasName(aliasName);
unit.setUnitcode(unitCode);
unit.setAddress(raw.getStr("DWDZ"));
unit.setUnitLevel(unitLevel);
unit.setUnitType(raw.getStr("DWLB"));
unit.setUnitTypeCode(availableBusinessUnit ? 1 : 0);
unit.setHasChildren(parentCodes.contains(unitCode));
}
/**
* 计算父级单位号。
* LSDWH 为空、等于自身或本地不存在该父级时按根级处理;系统根单位 0 允许直接作为父级。
*/
private String getParentId(String unitCode, String parentCode) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) {
return "";
}
if ("0".equals(parentCode)) {
return parentCode;
}
Sys_unit parentUnit = fetch(parentCode);
return parentUnit == null ? "" : parentCode;
}
/**
* 根据父级 path 生成当前单位 path。
* 父级为空时生成根节点 path,父级存在时在父级 path 下生成子节点 path。
*/ */
private String getUnitPath(String parentId) { private String getUnitPath(String parentId) {
String parentPath = ""; Sys_unit parentUnit = fetch(parentId);
if (StrUtil.isNotBlank(parentId)) { String parentPath = parentUnit == null ? "" : parentUnit.getPath();
Sys_unit parentUnit = fetch(parentId);
parentPath = parentUnit == null ? "" : parentUnit.getPath();
}
return getSubPath("sys_unit", "path", parentPath); return getSubPath("sys_unit", "path", parentPath);
} }
/**
* 信息中心一级单位应挂在系统根单位 0 下,其余单位沿用接口返回的 LSDWH。
*/
private String getNormalizedParentCode(JSONObject raw) {
Integer rawUnitLevel = raw.getInt("DWCC");
if (Integer.valueOf(1).equals(rawUnitLevel)) {
return "0";
}
return raw.getStr("LSDWH");
}
/**
* 系统内置根单位占用一级层级,信息中心单位层次入库时统一加一。
*/
private Integer getNormalizedUnitLevel(JSONObject raw) {
Integer rawUnitLevel = raw.getInt("DWCC");
return rawUnitLevel == null ? null : rawUnitLevel + 1;
}
/**
* DWCC 为信息中心单位层次,排序时用于尽量先处理父级单位,便于后续子级找到父节点。
*/
private int getUnitLevel(JSONObject raw) {
return raw.getInt("DWCC", 0);
}
} }
@@ -131,12 +131,18 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
log.info("开始全量更新用户数据"); log.info("开始全量更新用户数据");
// 获取角色信息 // 获取角色信息
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC); // Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
// 获取单位信息,后续判断单位变更 // 获取单位信息,后续判断单位变更
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1")); List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1"));
unitIdNameMap = unitList.stream().collect(Collectors.toMap(Sys_unit::getId, Sys_unit::getName)); unitIdNameMap = unitList.stream().collect(Collectors.toMap(Sys_unit::getId, Sys_unit::getName));
// 民族来源值为代码,更新系统用户前按现有码表换成名称。
Map<String, String> nationNames = dao.query(Sys_data_dict.class, Cnd.where("parentCode", "=", "USER_NATION"))
.stream()
.filter(dict -> StrUtil.isAllNotBlank(dict.getCode(), dict.getName()))
.collect(Collectors.toMap(Sys_data_dict::getCode, Sys_data_dict::getName, (first, second) -> first));
// 查询数据源 // 查询数据源
Cnd cnd = Cnd.where(Sys_user_source::getPullTime, "=", updateParam.getPullTime()); Cnd cnd = Cnd.where(Sys_user_source::getPullTime, "=", updateParam.getPullTime());
@@ -159,8 +165,14 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// 处理每条数据 // 处理每条数据
for (Sys_user_source source : sources) { for (Sys_user_source source : sources) {
normalizeSourceForUpdate(source, nationNames);
if (StrUtil.isBlank(source.getLoginname())) {
log.warn("数据源中存在空工号,跳过该记录");
continue;
}
Sys_user user = userMap.get(source.getLoginname()); Sys_user user = userMap.get(source.getLoginname());
source.setMember(null); // 会员及福利会员由会员业务维护;来源快照中的默认 0 不参与本次人员更新。
clearMembershipFields(source);
String unitId = source.getUnitId(); String unitId = source.getUnitId();
if (StringUtils.isNotBlank(unitId) && unitId.length() == 5) { if (StringUtils.isNotBlank(unitId) && unitId.length() == 5) {
source.setUnitId(unitId.substring(0, 3)); source.setUnitId(unitId.substring(0, 3));
@@ -180,11 +192,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
} else { } else {
// 修改现有用户 // 修改现有用户
u.setId(user.getId()); u.setId(user.getId());
/*
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
*/
u.setAidFundMember(null);
u.setPersonType(null); u.setPersonType(null);
needDoUpdateList.add(u); needDoUpdateList.add(u);
} }
@@ -220,6 +227,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
updateTasks.add(insertTask); updateTasks.add(insertTask);
// 异步处理公共角色分配 // 异步处理公共角色分配
/* 暂停人员更新时的角色变动;新用户仅写基本信息。
if (!needInitUserList.isEmpty()) { if (!needInitUserList.isEmpty()) {
CompletableFuture<Void> roleTask = CompletableFuture.runAsync(() -> { CompletableFuture<Void> roleTask = CompletableFuture.runAsync(() -> {
try { try {
@@ -248,6 +256,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
}, executorService); }, executorService);
// 不等待角色分配完成 // 不等待角色分配完成
} }
*/
} }
// 2. 更新现有用户 - 使用批量处理 // 2. 更新现有用户 - 使用批量处理
@@ -352,10 +361,12 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// 6. 处理其他任务 // 6. 处理其他任务
// 6.1 更新提案的校领导角色,发送订阅 // 6.1 更新提案的校领导角色,发送订阅
/* 暂停人员更新触发的角色刷新。
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt")); ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
for (String unitId : config.getSchoolLeaderUnitIds()) { for (String unitId : config.getSchoolLeaderUnitIds()) {
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE)); RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
} }
*/
/* /*
* 所有异步用户更新完成后再校正待确认状态,避免主事务提前锁定 sys_user, * 所有异步用户更新完成后再校正待确认状态,避免主事务提前锁定 sys_user,
@@ -373,6 +384,52 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
+ " 条"; + " 条";
} }
/**
* 更新前将来源记录的空白字符串转为 null,供 updateIgnoreNull 跳过,同时避免生成空白变更记录。
* 入参 source 为本次 sys_user_source 记录,nationNames 为 USER_NATION 的代码名称映射;
* 方法直接规范化 source,无返回值。未匹配到的非空民族代码保留原值。
*/
private void normalizeSourceForUpdate(Sys_user_source source, Map<String, String> nationNames) {
for (Field field : cn.hutool.core.util.ReflectUtil.getFields(Sys_user_source.class)) {
if (field.getType() != String.class) {
continue;
}
try {
field.setAccessible(true);
String value = (String) field.get(source);
field.set(source, StrUtil.isBlank(value) ? null : StrUtil.trim(value));
} catch (IllegalAccessException e) {
throw new IllegalStateException("规范化人员来源字段失败: " + field.getName(), e);
}
}
if ("1".equals(source.getSex())) {
source.setSex("男");
} else if ("2".equals(source.getSex())) {
source.setSex("女");
}
if (StrUtil.isNotBlank(source.getNation())) {
source.setNation(nationNames.getOrDefault(source.getNation(), source.getNation()));
}
}
/**
* 将人员来源中的会员字段置空,避免快照默认值修改系统会员和福利会员状态。
* source 是当前内存中的 sys_user_source 记录;方法不更新来源表,无返回值。
*/
private void clearMembershipFields(Sys_user_source source) {
source.setMember(null);
source.setMemberTime(null);
source.setWelfareMember(null);
source.setWantJoinLoveMutualAidAssociation(null);
source.setPreparationMemberFee(null);
source.setContractMemberFee(null);
source.setAidFundMember(null);
source.setAidFundMemberUserType(null);
source.setAidFundDeductTime(null);
source.setAidFundMemberJoinTime(null);
source.setAidFundMemberQuitTime(null);
}
/** /**
* 根据指定批次的完整数据源校正系统用户待确认状态。 * 根据指定批次的完整数据源校正系统用户待确认状态。
* 已人工确认且仍未重新出现的人员保持已确认状态,避免每次全量更新重复转为待确认。 * 已人工确认且仍未重新出现的人员保持已确认状态,避免每次全量更新重复转为待确认。
@@ -489,7 +546,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// } // }
// 2. 单位变更 // 2. 单位变更
if (!ObjectUtil.equals(user.getUnitId(), source.getUnitId())) { if (StrUtil.isNotBlank(source.getUnitId()) && !ObjectUtil.equals(user.getUnitId(), source.getUnitId())) {
changeTypes.add(MemberChangeType.UNIT_CHANGE.name()); changeTypes.add(MemberChangeType.UNIT_CHANGE.name());
NutMap change = NutMap.NEW(); NutMap change = NutMap.NEW();
change.put("fieldName", "单位"); change.put("fieldName", "单位");
@@ -499,8 +556,9 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
changeList.add(change); changeList.add(change);
// 单位异动发送订阅消息,清空原单位的所有角色 // 单位异动发送订阅消息,清空原单位的所有角色
UserChangePublisher.broadcast(new UserChangeMsg(List.of(user.getId()), // 单位代码和历史仍更新,角色清理由会员及权限业务另行处理。
UserChangeMsg.UNIT_CHANGE_OPERATION, user.getUnitId(), source.getUnitId())); // UserChangePublisher.broadcast(new UserChangeMsg(List.of(user.getId()),
// UserChangeMsg.UNIT_CHANGE_OPERATION, user.getUnitId(), source.getUnitId()));
} }
// 如果没有任何变更,返回null // 如果没有任何变更,返回null
@@ -27,8 +27,11 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R; import org.nutz.lang.random.R;
import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/** /**
* 增量更新系统用户数据 * 增量更新系统用户数据
@@ -58,6 +61,17 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
// 执行查询 // 执行查询
List<Sys_user_source> userSources = dao.query(Sys_user_source.class, cnd); List<Sys_user_source> userSources = dao.query(Sys_user_source.class, cnd);
// 新增人员也按系统显示值保存性别和民族,空白来源字段不写入新用户。
Map<String, String> nationNames = dao.query(Sys_data_dict.class, Cnd.where("parentCode", "=", "USER_NATION"))
.stream()
.filter(dict -> StrUtil.isAllNotBlank(dict.getCode(), dict.getName()))
.collect(Collectors.toMap(Sys_data_dict::getCode, Sys_data_dict::getName, (first, second) -> first));
userSources.forEach(source -> normalizeSourceForUpdate(source, nationNames));
userSources = userSources.stream().filter(source -> StrUtil.isNotBlank(source.getLoginname())).toList();
// 增量新增人员不从来源快照复制会员状态及福利会员状态。
userSources.forEach(this::clearMembershipFields);
//加入到系统用户表 //加入到系统用户表
List<Sys_user> sysUsers = BeanUtil.copyToList(userSources, Sys_user.class); List<Sys_user> sysUsers = BeanUtil.copyToList(userSources, Sys_user.class);
for (Sys_user sysUser : sysUsers) { for (Sys_user sysUser : sysUsers) {
@@ -88,9 +102,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
List<CompletableFuture<List<Sys_user_history>>> splitHistoryFutures = splitHistories.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList(); 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]))); AsyncUtil.waitAll(CompletableFuture.allOf(splitHistoryFutures.toArray(new CompletableFuture[0])));
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC); // Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
//添加公共角色 /* 暂停人员更新时的角色分配。
List<Sys_user_role> sysUserRoles = sysUsers.stream().map(user -> { List<Sys_user_role> sysUserRoles = sysUsers.stream().map(user -> {
Sys_user_role sysUserRole = new Sys_user_role(); Sys_user_role sysUserRole = new Sys_user_role();
sysUserRole.setUserId(user.getId()); sysUserRole.setUserId(user.getId());
@@ -101,6 +115,7 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
dao.insert(sysUserRoles); dao.insert(sysUserRoles);
}); });
sysRoleService.clearCache(); sysRoleService.clearCache();
*/
//比较单位数据 //比较单位数据
List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitLevel, "=", 2)); List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitLevel, "=", 2));
@@ -130,14 +145,62 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
}); });
// 6.1 更新提案的校领导角色,发送订阅 // 6.1 更新提案的校领导角色,发送订阅
/* 暂停人员更新触发的角色刷新。
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt")); ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
for (String unitId : config.getSchoolLeaderUnitIds()) { for (String unitId : config.getSchoolLeaderUnitIds()) {
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE)); RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
} }
*/
if (sysUsers.isEmpty() && insertUnits.isEmpty()) { if (sysUsers.isEmpty() && insertUnits.isEmpty()) {
return "本次没有新增人员或单位。"; return "本次没有新增人员或单位。";
} }
return StrUtil.format("增量更新成功,本次新增人员{}人,新增单位{}个。", sysUsers.size(), insertUnits.size()); return StrUtil.format("增量更新成功,本次新增人员{}人,新增单位{}个。", sysUsers.size(), insertUnits.size());
} }
/**
* 新增用户前规范化来源字段:空白字符串不入库,性别和民族代码转为现有系统名称。
* 入参 source 为 sys_user_source 记录,nationNames 为 USER_NATION 的代码名称映射;
* 方法直接修改内存中的 source,无返回值。未识别的非空编码保持原值。
*/
private void normalizeSourceForUpdate(Sys_user_source source, Map<String, String> nationNames) {
for (Field field : cn.hutool.core.util.ReflectUtil.getFields(Sys_user_source.class)) {
if (field.getType() != String.class) {
continue;
}
try {
field.setAccessible(true);
String value = (String) field.get(source);
field.set(source, StrUtil.isBlank(value) ? null : StrUtil.trim(value));
} catch (IllegalAccessException e) {
throw new IllegalStateException("规范化人员来源字段失败: " + field.getName(), e);
}
}
if ("1".equals(source.getSex())) {
source.setSex("男");
} else if ("2".equals(source.getSex())) {
source.setSex("女");
}
if (StrUtil.isNotBlank(source.getNation())) {
source.setNation(nationNames.getOrDefault(source.getNation(), source.getNation()));
}
}
/**
* 新增系统用户前排除来源快照的会员字段;新账号会员状态按系统默认值初始化。
* source 是当前内存中的 sys_user_source 记录;不修改来源表,无返回值。
*/
private void clearMembershipFields(Sys_user_source source) {
source.setMember(null);
source.setMemberTime(null);
source.setWelfareMember(null);
source.setWantJoinLoveMutualAidAssociation(null);
source.setPreparationMemberFee(null);
source.setContractMemberFee(null);
source.setAidFundMember(null);
source.setAidFundMemberUserType(null);
source.setAidFundDeductTime(null);
source.setAidFundMemberJoinTime(null);
source.setAidFundMemberQuitTime(null);
}
} }
@@ -115,96 +115,81 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Date pull() { public Date pull() {
try { try {
// 获取数据中心的字典 // 先确认本地中间表有人员,避免空表产生一个看似成功的拉取批次。
List<Sys_data_dict> dataDictList = dao().query(Sys_data_dict.class, Cnd.NEW()); List<NutMap> rawDataList = listMap(Sqls.create("SELECT * FROM user_middle"));
// 字典码表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<>();
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工"));
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣"));
rawDataList = distinctDmpUsers(rawDataList);
if (rawDataList.isEmpty()) { if (rawDataList.isEmpty()) {
log.warn("当前未获取到人员数据"); throw new BaseException("本地人员中间表没有数据,已停止拉取");
} }
// 单位必须先入库;人员快照的 DWDM 已是三位二级单位代码。
sysDataUnitPullService.updateUnits();
Date nowDate = new Date(); Date nowDate = new Date();
Map<String, Sys_user_source> sourceMap = new LinkedHashMap<>();
// 赋值 for (NutMap raw : rawDataList) {
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> { String loginname = StrUtil.trim(raw.getString("ZGH"));
Sys_user_source sysUser = new Sys_user_source(); if (StrUtil.isBlank(loginname)) {
throw new BaseException("人员中间表存在空工号,已停止拉取");
// 使用缓存的字段映射
for (FieldMapping mapping : getFieldMappings()) {
try {
// 根据字段类型设置值
if (mapping.field.getType() == String.class) {
if (StrUtil.isNotBlank(mapping.dict)) {
List<Sys_data_dict> sysDataDicts = dataDictMap.getOrDefault(mapping.dict, Collections.emptyList());
Sys_data_dict sysDataDict = sysDataDicts.stream()
.filter(v -> raw.getStr(mapping.key, "").equals(v.getCode()))
.findFirst().orElse(new Sys_data_dict());
mapping.field.set(sysUser, StrUtil.blankToDefault(sysDataDict.getName(), raw.getStr(mapping.key)));
} else {
// 特殊处理 根据身份证号获取性别和出生年月
if ("SFZJH".equals(mapping.key)) {
String idCard = raw.getStr(mapping.key);
mapping.field.set(sysUser, idCard);
// 设置出生年月
try {
sysUser.setBirthday(IdcardUtil.getBirthDate(idCard));
} catch (Exception e) {
sysUser.setBirthday(null);
}
} else if ("LXNY".equals(mapping.key)) {
mapping.field.set(sysUser, normalizeArrivalAtSchoolDate(raw.getStr(mapping.key)));
} else {
mapping.field.set(sysUser, raw.getStr(mapping.key));
}
}
} else if (mapping.field.getType() == Date.class) {
mapping.field.set(sysUser, raw.getDate(mapping.key));
}
} catch (IllegalAccessException e) {
log.error("设置字段值失败: {}", mapping.field.getName(), e);
}
} }
sourceMap.putIfAbsent(loginname, mapMiddleUser(raw, nowDate));
sysUser.setUnitName(raw.getStr("SZDW"));
sysUser.setUnitId(resolveSourceUnitId(raw.getStr("SZDWH")));
// 设置拉取时间
sysUser.setPullTime(nowDate);
return sysUser;
}).toList();
log.info("用户数据初始化完成,等待插入,当前数据条数{}", latestSourceList.size());
// 插入到数据库
if (CollUtil.isNotEmpty(latestSourceList)) {
dao().insert(latestSourceList);
}
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).filter(StrUtil::isNotBlank).distinct().toList();
Sql sql = Sqls.create("select id from sys_unit group by id");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
List<String> unitIds = sql.getList(String.class);
if (!new HashSet<>(unitIds).containsAll(sourceUnitIds)) {
log.info("单位需要更新,正在同步更新");
sysDataUnitPullService.updateUnits(unitIds);
} }
dao().insert(new ArrayList<>(sourceMap.values()));
log.info("本地人员中间表拉取完成,原始记录数: {},去重后记录数: {}", rawDataList.size(), sourceMap.size());
return nowDate; return nowDate;
} catch (Exception e) { } catch (Exception e) {
throw new BaseException("数据拉取失败,{}", e.getMessage()); throw new BaseException("数据拉取失败,{}", e.getMessage());
} }
} }
/**
* 将 user_middle 的一条人员记录映射为本次拉取快照。
* raw 包含工号 ZGH、姓名 XM、单位 DWDM/DWMC 等本地中间表字段;pullTime 是本次批次时间。
* 返回 Sys_user_source,仅用于写入数据源表,不修改 sys_user。
*/
private Sys_user_source mapMiddleUser(NutMap raw, Date pullTime) {
Sys_user_source source = new Sys_user_source();
source.setLoginname(StrUtil.trim(raw.getString("ZGH")));
source.setUsername(middleValue(raw, "XM"));
source.setUnitId(middleValue(raw, "DWDM"));
source.setUnitName(middleValue(raw, "DWMC"));
source.setSex(middleValue(raw, "XB"));
source.setNation(middleValue(raw, "MZ"));
source.setUserState(middleValue(raw, "DQZT"));
source.setPersonType(middleValue(raw, "JZGRYLB"));
source.setEducation(middleValue(raw, "XL"));
source.setMobile(middleValue(raw, "SJH"));
source.setAcademicDegree(middleValue(raw, "XW"));
source.setProfessionalTitle(middleValue(raw, "ZYJSZW"));
source.setProfessionalLevel(middleValue(raw, "ZYJSGWDJ"));
source.setPolitical(middleValue(raw, "ZZMM"));
source.setIdCard(middleValue(raw, "SFZJH"));
source.setBirthday(parseMiddleBirthDate(raw.getString("CSRQ")));
source.setPullTime(pullTime);
return source;
}
/**
* 将中间表的空字符串转为 null,避免后续正式更新时用空值覆盖系统用户字段。
*/
private String middleValue(NutMap raw, String column) {
String value = StrUtil.trim(raw.getString(column));
return StrUtil.isBlank(value) ? null : value;
}
/**
* CSRQ 当前以 yyyy/MM/dd 开头,后面可能包含时间;只取日期部分。
* 空值返回 null,异常格式抛错以阻止生成不完整的拉取批次。
*/
private Date parseMiddleBirthDate(String birthDate) {
if (StrUtil.isBlank(birthDate)) {
return null;
}
String dateText = birthDate.length() >= 10 ? birthDate.substring(0, 10).replace('/', '-') : "";
if (!dateText.matches("\\d{4}-\\d{2}-\\d{2}")) {
throw new BaseException("人员中间表存在异常出生日期,已停止拉取");
}
return DateUtil.parseDate(dateText);
}
/** /**
* 规范化信息中心返回的来校时间。 * 规范化信息中心返回的来校时间。
* 入参 arrivalAtSchoolDate 对应信息中心 LXNY 字段,允许 yyyy-MM-dd 或 yyyy-MM; * 入参 arrivalAtSchoolDate 对应信息中心 LXNY 字段,允许 yyyy-MM-dd 或 yyyy-MM;
@@ -6,6 +6,7 @@ import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.constant.RedisConstant; import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.base.sms.model.MsgPlatformWechatRequest; import com.budwk.app.base.sms.model.MsgPlatformWechatRequest;
import com.budwk.app.base.utils.ManyAddOrRenewUtil; import com.budwk.app.base.utils.ManyAddOrRenewUtil;
import com.budwk.app.sys.models.Sys_msg; import com.budwk.app.sys.models.Sys_msg;
@@ -62,6 +63,8 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
private ManyAddOrRenewUtil manyAddOrRenewUtil; private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Inject @Inject
private SmsService smsService; private SmsService smsService;
@Inject
private WxWorkMessageService wxWorkMessageService;
private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息"); private static NutMap typeMap = NutMap.NEW().addv("system", "系统消息").addv("user", "用户消息");
@@ -102,14 +105,29 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
} }
}); });
//发送学校平台消息 // isExternal=false 为仅站内通知;外部通道分别处理异常,避免短信失败阻断企业微信。
ThreadUtil.execute(() -> { if (isExternal) {
for (String loginName : loginNames) { ThreadUtil.execute(() -> {
smsService.sendSmsByAccount(loginName,sysMsg.getNote()); for (String loginName : loginNames) {
smsService.sendWechatTextByAccountOrMobile(loginName,sysMsg.getNote()); try {
} smsService.sendSmsByAccount(loginName, sysMsg.getNote());
} catch (Exception e) {
}); log.error("消息短信发送失败,msgId=" + sysMsg.getId(), e);
}
}
});
ThreadUtil.execute(() -> {
try {
// 学校企业微信 userid 直接使用 loginname;批量发送标题、正文及业务链接。
NutMap result = wxWorkMessageService.massSend(loginNames, sysMsg.getTitle(), sysMsg.getNote(), sysMsg.getUrl());
if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) {
log.warn("企业微信消息存在失败接收人,msgId=" + sysMsg.getId() + ",result=" + result);
}
} catch (Exception e) {
log.error("企业微信消息发送失败,msgId=" + sysMsg.getId(), e);
}
});
}
return sysMsg; return sysMsg;
} }
@@ -292,11 +310,13 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void sendMsg(String loginname, String title, String body, String sender) { public void sendMsg(String loginname, String title, String body, String sender) {
sendMsg(List.of(loginname), title, body, SecurityUtil.getUserId()); sendMsg(List.of(loginname), title, body, SecurityUtil.getUserId());
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void sendMsg(List<String> loginNames, String title, String body, String sender) { public void sendMsg(List<String> loginNames, String title, String body, String sender) {
Sys_msg sysMsg = new Sys_msg(); Sys_msg sysMsg = new Sys_msg();
sysMsg.setTitle(title); sysMsg.setTitle(title);
@@ -309,6 +329,7 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void sendMsgInSys(List<String> loginNames, String title, String body, String sender) { public void sendMsgInSys(List<String> loginNames, String title, String body, String sender) {
Sys_msg sysMsg = new Sys_msg(); Sys_msg sysMsg = new Sys_msg();
sysMsg.setTitle(title); sysMsg.setTitle(title);
@@ -189,25 +189,16 @@ public class WkFailProcessor extends ViewProcessor {
if (Globals.sso) { if (Globals.sso) {
//1.判断是否为企业微信浏览器并且对接了企业微信 //1.判断是否为企业微信浏览器并且对接了企业微信
if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) { if (browserPlatform == BrowserPlatform.QI_YE_WEI_XIN && propertiesProxy.getBoolean("wxwork.enable", false)) {
//进行企业微信授权 // 专用企业微信认证配置缺失时整组回退到已接通的 CAS,保留卡片原始页面。
// String token = getWxWorkToken(); try {
// if (StrUtil.isNotBlank(token)) { String authUrl = buildEnterpriseCasUrl(redirect);
// //跳转到企业微信授权地址 log.info("auth:" + authUrl);
// new ServerRedirectView(buildWxWorkOauth2Url()).render(request, response, null); new ServerRedirectView(authUrl).render(request, response, null);
// } else { } catch (IllegalArgumentException e) {
// //跳转到错误页 别问我为啥错了 我也不知道 首先去检查配置文件里的参数有没有写对 log.error("企业微信登录跳转配置无效:" + e.getMessage());
//// new ServerRedirectView(Globals.AppDomain + errorUri).render(request, response, null); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
// //跳转到cas登录页 "统一身份认证配置不完整或无效,请联系管理员检查认证地址及回调地址");
// String callBackUrl = propertiesProxy.get("cas.client-host-url") + propertiesProxy.get("cas.client-call-back-url") + "?redirect=" + redirect; }
// String authUrl = propertiesProxy.get("cas.qi-wx-server-login-url") + "?service=" + URLEncoder.encode(callBackUrl, StandardCharsets.UTF_8);
// new ServerRedirectView(authUrl).render(request, response, null);
// }
// 武汉城市职业学院 跳转到企业微信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);
new ServerRedirectView(authUrl).render(request, response, null);
} else if (browserPlatform == BrowserPlatform.WEI_XIN && propertiesProxy.getBoolean("wx.enable", false)) { } else if (browserPlatform == BrowserPlatform.WEI_XIN && propertiesProxy.getBoolean("wx.enable", false)) {
//进行微信公众号授权 //进行微信公众号授权
//跳转到微信授权页 //跳转到微信授权页
@@ -225,6 +216,53 @@ public class WkFailProcessor extends ViewProcessor {
} }
} }
/**
* 构造企业微信内的 CAS 登录地址。redirect 为已 URL 编码的原始站内路径及查询参数;
* 返回携带 service 参数的完整认证 URL,认证回调通过 redirect 返回卡片页面。
* 三个 wecom 配置必须同时存在,否则整体使用 cas 配置,避免混用两套认证平台。
*/
private String buildEnterpriseCasUrl(String redirect) {
String loginUrl = propertiesProxy.get("wecom.wecom-server-login-url");
String clientHost = propertiesProxy.get("wecom.wecom-client-host-url");
String callbackPath = propertiesProxy.get("wecom.wecom-client-call-back-url");
if (missingAuthConfig(loginUrl) || missingAuthConfig(clientHost) || missingAuthConfig(callbackPath)) {
loginUrl = propertiesProxy.get("cas.server-login-url");
clientHost = propertiesProxy.get("cas.client-host-url");
callbackPath = propertiesProxy.get("cas.client-call-back-url");
}
if (missingAuthConfig(loginUrl) || missingAuthConfig(clientHost) || missingAuthConfig(callbackPath)) {
throw new IllegalArgumentException("CAS 登录地址、系统域名或回调路径为空");
}
loginUrl = loginUrl.trim();
clientHost = clientHost.trim();
callbackPath = callbackPath.trim();
validateAuthUrl(loginUrl);
validateAuthUrl(clientHost);
// 回调必须是本系统路径,不能被绝对 URL 或双斜线路径覆盖到其他主机。
if (!callbackPath.startsWith("/") || callbackPath.startsWith("//") || callbackPath.contains("#")) {
throw new IllegalArgumentException("认证回调必须是以单斜线开头的站内路径");
}
String callback = clientHost.replaceAll("/+$", "") + callbackPath
+ (callbackPath.contains("?") ? "&" : "?") + "redirect=" + redirect;
// 保留部署配置中的 HTTP/HTTPS 协议;工会 HTTPS 回调不会被改为 HTTP。
return loginUrl + (loginUrl.contains("?") ? "&" : "?") + "service="
+ URLEncoder.encode(callback, StandardCharsets.UTF_8);
}
/** 空值和字面量 null 都视为未配置,防止拼接出浏览器相对地址 null?service=...。 */
private boolean missingAuthConfig(String value) {
return StrUtil.isBlank(value) || "null".equalsIgnoreCase(value.trim());
}
/** 认证地址和回调域名必须为完整 HTTP(S) URL,拒绝无主机名、片段或用户信息的地址。 */
private void validateAuthUrl(String value) {
java.net.URI uri = java.net.URI.create(value);
if (!("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|| uri.getHost() == null || uri.getFragment() != null || uri.getUserInfo() != null) {
throw new IllegalArgumentException("认证配置不是有效的 HTTP(S) 地址");
}
}
/** /**
* 判断浏览器平台 * 判断浏览器平台
* *
@@ -35,7 +35,6 @@ public class ProposalQueryYearReportController {
*/ */
private static final Map<String, String> YEAR_REPORT_ORDER_COLUMNS = Map.ofEntries( private static final Map<String, String> YEAR_REPORT_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"), Map.entry("code", "info.code"),
Map.entry("caseFilingCode", "info.caseFilingCode"),
Map.entry("name", "info.name"), Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"), Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"), Map.entry("typeName", "typeName"),
@@ -97,7 +97,7 @@ public class ProposalSeniorBasicController {
return Result.error("提案编号已存在"); return Result.error("提案编号已存在");
} }
dao.update(proposalInfo,"^code|caseFilingCode|name|typeId|brief|measures|excerpt|suggestUnits|files$"); dao.update(proposalInfo,"^code|name|typeId|brief|measures|excerpt|suggestUnits|files$");
return Result.success(); return Result.success();
} }
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact; package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
@@ -11,6 +12,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam; import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService; import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingUnitService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService; import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
@@ -44,16 +46,24 @@ public class ProposalCommitteeFilingController {
@Inject @Inject
private ProposalCommitteeFilingService proposalCommitteeFilingService; private ProposalCommitteeFilingService proposalCommitteeFilingService;
@Inject @Inject
private ProposalCommitteeFilingUnitService proposalCommitteeFilingUnitService;
@Inject
private ProposalCommonService proposalCommonService; private ProposalCommonService proposalCommonService;
@At("") @At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/committeeFiling/index.html") @Ok("beetl:/platform/zhgh/democratic/proposal/transact/committeeFilingUnit/index.html")
@SaCheckPermission("proposal.committeeFiling") @SaCheckPermission("proposal.committeeFiling")
public void index() { public void index() {
} }
@At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/committeeFiling/index.html")
@SaCheckPermission("h5.proposal.committeeFiling")
public void h5Index() {
}
@At @At
@SaCheckPermission("proposal.committeeFiling") @SaCheckPermission(value = {"proposal.committeeFiling", "h5.proposal.committeeFiling"}, mode = SaMode.OR)
@ApiOperation("分页列表") @ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) { public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
@@ -112,7 +122,7 @@ public class ProposalCommitteeFilingController {
cnd.desc("t.createdAt"); cnd.desc("t.createdAt");
} }
sql.setCondition(cnd); sql.setCondition(cnd);
Pagination pagination = proposalCommitteeFilingService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); Pagination pagination = proposalCommitteeFilingUnitService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination); return Result.success(pagination);
} }
@@ -126,11 +136,11 @@ public class ProposalCommitteeFilingController {
} }
@At @At
@SaCheckPermission("proposal.committeeFiling") @SaCheckPermission(value = {"proposal.committeeFiling", "h5.proposal.committeeFiling"}, mode = SaMode.OR)
@ApiOperation("执行单案立案审核任务") @ApiOperation("执行单案立案审核任务")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) { public Result executeTask(@Param("data") String data) {
// data 包含流程任务ID、提交类型、立案结果及主协办单位;Service 校验负责人后执行 WF,成功返回统一 Result。 // data 包含流程任务 ID、提交类型、提案 ID、立案结果及主协办单位;Service 先保存立案数据再执行 WF,成功返回统一 Result。
Dict args = Json.fromJson(Dict.class, data); Dict args = Json.fromJson(Dict.class, data);
proposalCommitteeFilingService.executeFlowTask(args); proposalCommitteeFilingService.executeFlowTask(args);
return Result.success(); return Result.success();
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact; package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
@@ -34,13 +35,13 @@ import javax.validation.Valid;
import java.util.List; import java.util.List;
/** /**
* 团长审核 * 分工会主席审核
*/ */
@IocBean @IocBean
@At("/platform/proposal/delegation") @At("/platform/proposal/delegation")
@Slf4j @Slf4j
@Ok("json:full") @Ok("json:full")
@Api(tags = "提案-办理-团长审核") @Api(tags = "提案-办理-分工会主席审核")
public class ProposalDelegationController { public class ProposalDelegationController {
@Inject @Inject
@@ -60,12 +61,12 @@ public class ProposalDelegationController {
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/delegation/index.html") @Ok("beetl:/platform/zhghh5/democratic/proposal/transact/delegation/index.html")
@SaCheckPermission("proposal.delegation") @SaCheckPermission("h5.proposal.delegation")
public void h5Index() { public void h5Index() {
} }
@At @At
@SaCheckPermission("proposal.delegation") @SaCheckPermission(value = {"proposal.delegation", "h5.proposal.delegation"}, mode = SaMode.OR)
@ApiOperation("分页列表") @ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm,boolean approval) { public Result pageData(@Valid ProposalSearchParam pageForm,boolean approval) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
@@ -116,7 +117,7 @@ public class ProposalDelegationController {
} }
ProposalSearchParam.buildSearch(cnd, pageForm); ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id"); cnd.groupBy("t.id");
// 团长审核列表未指定有效排序时保留任务创建时间倒序。 // 分工会主席审核列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "delegation")) { if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "delegation")) {
cnd.desc("t.createdAt"); cnd.desc("t.createdAt");
} }
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact; package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns; import com.budwk.app.base.param.ExportTableColumns;
@@ -30,15 +31,15 @@ import java.util.List;
/** /**
* @version 1.0 * @version 1.0
* @Author FKY * @Author FKY
* @name:ProposalPreAuditController * @name:校工会主席审核(复用原预审核页面)
* @Date 2026/3/16 17:30 * @Date 2026/3/16 17:30
* @注释 * @注释
*/ */
@IocBean @IocBean
@At("/platform/proposal/pre/audit") @At("/platform/proposal/schoolUnionChairApproval")
@Slf4j @Slf4j
@Ok("json:full") @Ok("json:full")
@Api(tags = "提案管理-校工会预审核") @Api(tags = "提案管理-校工会主席审核")
public class ProposalPreAuditController { public class ProposalPreAuditController {
@Inject @Inject
@@ -48,18 +49,18 @@ public class ProposalPreAuditController {
@At("") @At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/preAudit/index.html") @Ok("beetl:/platform/zhgh/democratic/proposal/transact/preAudit/index.html")
@SaCheckPermission("proposal.preAudit") @SaCheckPermission("proposal.schoolUnionChairApproval")
public void index() { public void index() {
} }
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/preAudit/index.html") @Ok("beetl:/platform/zhghh5/democratic/proposal/transact/preAudit/index.html")
@SaCheckPermission("proposal.preAudit") @SaCheckPermission("h5.proposal.schoolUnionChairApproval")
public void h5Index() { public void h5Index() {
} }
@At @At
@SaCheckPermission("proposal.preAudit") @SaCheckPermission(value = {"proposal.schoolUnionChairApproval", "h5.proposal.schoolUnionChairApproval"}, mode = SaMode.OR)
@ApiOperation("分页列表") @ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) { public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
@@ -98,7 +99,8 @@ public class ProposalPreAuditController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and("t.taskName", "=", "preAudit"); // 校工会主席沿用预审核界面,待办必须按已发布流程的实际节点编码筛选。
cnd.and("t.taskName", "=", "schoolUnionChairApproval");
if(!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())){ if(!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())){
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
@@ -113,8 +115,8 @@ public class ProposalPreAuditController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId()); cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm); ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id"); cnd.groupBy("t.id");
// 预审核列表未指定有效排序时保留任务创建时间倒序。 // 校工会主席审核列表未指定有效排序时保留任务创建时间倒序。
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "preAudit")) { if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "schoolUnionChairApproval")) {
cnd.desc("t.createdAt"); cnd.desc("t.createdAt");
} }
sql.setCondition(cnd); sql.setCondition(cnd);
@@ -124,10 +126,10 @@ public class ProposalPreAuditController {
@At @At
@Ok("void") @Ok("void")
@SaCheckPermission("proposal.preAudit") @SaCheckPermission("proposal.schoolUnionChairApproval")
@ApiOperation("导出列表") @ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval, public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) { @Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("preAudit", pageForm, approval, tableColumns, response); proposalCommonService.exportWorkflowList("schoolUnionChairApproval", pageForm, approval, tableColumns, response);
} }
} }
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact; package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@@ -13,7 +14,6 @@ import com.budwk.app.base.vo.LabelValueVO;
import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.FlowCommonService; import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
@@ -70,12 +70,12 @@ public class ProposalSchoolLeaderApprovalController {
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/schoolLeaderApproval/index.html") @Ok("beetl:/platform/zhghh5/democratic/proposal/transact/schoolLeaderApproval/index.html")
@SaCheckPermission("proposal.schoolLeaderApproval") @SaCheckPermission("h5.proposal.schoolLeaderApproval")
public void h5Index() { public void h5Index() {
} }
@At @At
@SaCheckPermission("proposal.schoolLeaderApproval") @SaCheckPermission(value = {"proposal.schoolLeaderApproval", "h5.proposal.schoolLeaderApproval"}, mode = SaMode.OR)
@ApiOperation("分页列表") @ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) { public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
// List<LabelValueVO> vos = processTaskService.jumpAbleTaskNameList(280L); // List<LabelValueVO> vos = processTaskService.jumpAbleTaskNameList(280L);
@@ -121,7 +121,7 @@ public class ProposalSchoolLeaderApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and("t.taskName", "=", "schoolLeader"); cnd.and("t.taskName", "=", "school_leader");
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) { if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
@@ -163,18 +163,13 @@ public class ProposalSchoolLeaderApprovalController {
} }
@At @At
@SaCheckPermission("proposal.schoolLeaderApproval") @SaCheckPermission(value = {"proposal.schoolLeaderApproval", "h5.proposal.schoolLeaderApproval"}, mode = SaMode.OR)
@ApiOperation("执行任务") @ApiOperation("执行任务")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) { public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data); Dict args = Json.fromJson(Dict.class, data);
if("back".equals(args.getStr("tf_approval"))) { // 退回答复由已发布的 WF 分支按 tf_approval=back 流转到主办单位答复。
args.put("submitType", ProcessSubmitTypeEnum.JUMP.getCode());
args.put("taskName", "committee");
flowCommonService.executeTask(args);
return Result.success();
}
String proposalId = args.getStr("proposalId"); String proposalId = args.getStr("proposalId");
@@ -44,7 +44,9 @@ public class ProposalUnSubmitQueryController {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
distinct distinct
info.id, info.id,
ins.id AS instanceId,
ins.processDefineId AS instanceProcessDefineId,
info.code as proposalCode, info.code as proposalCode,
info.name as proposalName, info.name as proposalName,
vu.username, vu.username,
@@ -55,15 +57,17 @@ public class ProposalUnSubmitQueryController {
manner.name as mannerName, manner.name as mannerName,
(select count(DISTINCT(seconderId))from proposal_second where proposalId = info.id) AS secondedNum, (select count(DISTINCT(seconderId))from proposal_second where proposalId = info.id) AS secondedNum,
(select count(DISTINCT(seconderId))from proposal_second where proposalId = info.id and isAgree = 1) AS secondedAgreeNum, (select count(DISTINCT(seconderId))from proposal_second where proposalId = info.id and isAgree = 1) AS secondedAgreeNum,
IF(info.stateCode <= 200 ,'未审核','已审核') AS delegationAudit IF(EXISTS (SELECT 1 FROM wf_process_task dt WHERE dt.processInstanceId = ins.id
AND dt.taskName = 'delegation' AND dt.taskState = 20), '已审核', '未审核') AS delegationAudit
FROM FROM
proposal_info info proposal_info info
left join vw_user vu on info.createUserId = vu.id left join vw_user vu on info.createUserId = vu.id
left join teacher_congress_delegation tcd on info.delegationId = tcd.id left join teacher_congress_delegation tcd on info.delegationId = tcd.id
left join proposal_type pt on info.typeId = pt.id left join proposal_type pt on info.typeId = pt.id
left join sys_dict manner on manner.`code` = info.source left join sys_dict manner on manner.`code` = info.source
left join `wf_process_instance` ins on ins.businessNo = info.id left join `wf_process_instance` ins on ins.id = (
left join wf_process_task nt on nt.processInstanceId = ins.id select latest.id from wf_process_instance latest
where latest.businessNo = info.id order by latest.createdAt desc, latest.id desc limit 1)
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
@@ -76,6 +80,8 @@ public class ProposalUnSubmitQueryController {
SqlExpressionGroup group2 = Cnd.exps("ins.state", "=", 30); SqlExpressionGroup group2 = Cnd.exps("ins.state", "=", 30);
group2.or("ins.state", "=", 45); group2.or("ins.state", "=", 45);
group2.or(new Static("ins.id not in (select processInstanceId from wf_process_task where taskName='delegation')")); group2.or(new Static("ins.id not in (select processInstanceId from wf_process_task where taskName='delegation')"));
// 尚未发起 WF 实例的提案也属于未提交汇总,不能被 NOT IN 的 NULL 判断排除。
group2.orIsNull("ins.id");
cnd.and(group2); cnd.and(group2);
if(searchName != null && StrUtil.isNotBlank(searchKeyword)) { if(searchName != null && StrUtil.isNotBlank(searchKeyword)) {
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact; package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
@@ -70,12 +71,12 @@ public class ProposalUnderTakeReplyController {
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/unitReply/index.html") @Ok("beetl:/platform/zhghh5/democratic/proposal/transact/unitReply/index.html")
@SaCheckPermission("proposal.unitReply") @SaCheckPermission("h5.proposal.unitReply")
public void h5Index() { public void h5Index() {
} }
@At @At
@SaCheckPermission("proposal.unitReply") @SaCheckPermission(value = {"proposal.unitReply", "h5.proposal.unitReply"}, mode = SaMode.OR)
@ApiOperation("分页列表") @ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval, boolean transfer) { public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval, boolean transfer) {
/* /*
@@ -134,8 +135,8 @@ public class ProposalUnderTakeReplyController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
// cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply", "opinion_master_reply")); // 当前已发布的 WF 版本使用主办、协办和建议办理三个答复节点。
cnd.and("t.taskName", "in", List.of("unit_reply", "two_unit_reply", "opinion_unit_reply")); cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply", "opinion_master_reply"));
if (!SecurityUtil.getUserLoginname().equals("superadmin")) { if (!SecurityUtil.getUserLoginname().equals("superadmin")) {
cnd.and("ta.actorId", "=", SecurityUtil.getUserId()); cnd.and("ta.actorId", "=", SecurityUtil.getUserId());
@@ -159,9 +160,9 @@ public class ProposalUnderTakeReplyController {
for (NutMap row : list) { for (NutMap row : list) {
/* /*
* 承办单位答复撤回必须按“当前整组待办节点”统一判断。 * 承办单位答复撤回必须按“当前整组待办节点”统一判断。
* 这里不能再用 contains,只要出现过 unit_reply/two_unit_reply 就会误判可撤回; * 这里不能再用 contains,否则出现过其他答复节点时会误判可撤回;
* 正确规则是:curTaskKey 必须精确等于某一个允许撤回的节点编码, * 正确规则是:curTaskKey 必须精确等于某一个允许撤回的节点编码,
* 像 unit_reply,two_unit_reply 这种混合状态一律不允许撤回。 * 多个答复节点同时存在的混合状态一律不允许撤回。
*/ */
row.setv("canRevoke", isReplyTaskGroupRevocable(row)); row.setv("canRevoke", isReplyTaskGroupRevocable(row));
} }
@@ -173,9 +174,8 @@ public class ProposalUnderTakeReplyController {
/** /**
* 承办单位答复节点撤回需要基于整组当前节点判断。 * 承办单位答复节点撤回需要基于整组当前节点判断。
* 只有“当前流程正在办理的节点”和“这条列表记录所属节点”完全一致时,才允许撤回; * 只有“当前流程正在办理的节点”和“这条列表记录所属节点”完全一致时,才允许撤回;
* 这样流程已经从 unit_reply 走到 two_unit_reply 后,历史 unit_reply 记录会自动失去撤回资格。 * 这样流程进入后续节点后,历史答复记录会自动失去撤回资格。
* 主办只能撤回 unit_reply、two_unit_reply、school_leader; * 主办只能在主办或建议办理节点仍为当前节点时撤回;协办只能在协办节点撤回。
* 协办只能撤回 unit_reply、two_unit_reply。
*/ */
private boolean isReplyTaskGroupRevocable(NutMap row) { private boolean isReplyTaskGroupRevocable(NutMap row) {
if (!ObjectUtil.equal(row.getInt("taskState"), ProcessTaskStateEnum.FINISHED.getCode())) { if (!ObjectUtil.equal(row.getInt("taskState"), ProcessTaskStateEnum.FINISHED.getCode())) {
@@ -187,10 +187,10 @@ public class ProposalUnderTakeReplyController {
return false; return false;
} }
if (row.getInt("underTakeIsMaster") == 1) { if (row.getInt("underTakeIsMaster") == 1) {
return Stream.of("unit_reply", "two_unit_reply", "school_leader") return Stream.of("master_reply", "opinion_master_reply")
.anyMatch(a -> ObjectUtil.equal(a, taskKey)); .anyMatch(a -> ObjectUtil.equal(a, taskKey));
} }
return Stream.of("unit_reply", "two_unit_reply") return Stream.of("slave_reply")
.anyMatch(a -> ObjectUtil.equal(a, taskKey)); .anyMatch(a -> ObjectUtil.equal(a, taskKey));
} }
@@ -200,7 +200,7 @@ public class ProposalUnderTakeReplyController {
@At @At
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("proposal.unitReply") @SaCheckPermission(value = {"proposal.unitReply", "h5.proposal.unitReply"}, mode = SaMode.OR)
@ApiOperation("执行任务") @ApiOperation("执行任务")
public Result executeTask(@Param("data") String data) { public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data); Dict args = Json.fromJson(Dict.class, data);
@@ -0,0 +1,75 @@
package com.budwk.app.zhgh.democratic.proposal.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 com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import java.util.List;
/**
* 按提案申请人所属分工会分配主席审核人,不使用当前附议人的组织信息。
*/
public class ProposalApplicantBranchUnionChairmanAssignmentHandler implements AssignmentHandler {
/**
* 从流程业务编号定位提案申请人,再查询其所属分工会已启用的主席角色用户。
*
* @param model 当前审核任务模型,无需额外配置人员参数
* @param execution 流程执行上下文,流程实例的 businessNo 应为提案 ID
* @return 分工会主席用户 ID 列表;申请人或主席配置缺失时抛出业务异常
*/
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
String proposalId = execution.getProcessInstance().getBusinessNo();
ProposalInfo proposal = StrUtil.isBlank(proposalId) ? null : dao.fetch(ProposalInfo.class, proposalId);
if (proposal == null || StrUtil.isBlank(proposal.getCreateUserId())) {
throw new BaseException("提案申请人不存在,无法分配分工会主席审核人");
}
View_user applicant = dao.fetch(View_user.class, Cnd.where(View_user::getId, "=", proposal.getCreateUserId()));
if (applicant == null || StrUtil.isBlank(applicant.getUnionId())) {
throw new BaseException("提案申请人未设置所属分工会,无法分配主席审核人");
}
Sys_role chairmanRole = ServiceContext.find(SysRoleService.class)
.getByCode(RoleConstant.BRANCH_UNION_CHAIRMAN);
if (chairmanRole == null) {
throw new BaseException("分工会主席角色不存在,请联系校工会进行设置");
}
List<String> chairmanIds = dao.query(Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", chairmanRole.getId())
.and(Sys_user_role::getUnionId, "=", applicant.getUnionId())
.and(Sys_user_role::getEnable, "=", true))
.stream()
.map(Sys_user_role::getUserId)
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (chairmanIds.isEmpty()) {
throw new BaseException("提案申请人所属分工会没有设置主席,请联系校工会进行设置");
}
return chairmanIds;
}
@Override
public String getMessage() {
return "提案申请人所属分工会主席";
}
@Override
public int getOrder() {
return 10;
}
}
@@ -19,8 +19,6 @@ public class ProposalQueryComprehensiveParam extends PageForm {
private String name; private String name;
@ApiModelProperty(name = "提案编号") @ApiModelProperty(name = "提案编号")
private String code; private String code;
@ApiModelProperty(name = "立案编号")
private String caseFilingCode;
@ApiModelProperty(name = "教代会ID") @ApiModelProperty(name = "教代会ID")
private String sessionId; private String sessionId;
@ApiModelProperty(name = "search session id") @ApiModelProperty(name = "search session id")
@@ -63,7 +61,7 @@ public class ProposalQueryComprehensiveParam extends PageForm {
@ApiModelProperty(name = "通用关键字") @ApiModelProperty(name = "通用关键字")
private String commonKeyword; private String commonKeyword;
@ApiModelProperty(name = "提案名称或编号搜索字段,仅支持name、code、caseFilingCode") @ApiModelProperty(name = "提案名称或编号搜索字段,仅支持name、code")
private String proposalSearchField; private String proposalSearchField;
@ApiModelProperty(name = "提案名称或编号搜索内容") @ApiModelProperty(name = "提案名称或编号搜索内容")
@@ -77,7 +75,7 @@ public class ProposalQueryComprehensiveParam extends PageForm {
* 将综合查询参数追加到查询条件中,无返回值,处理结果直接写入 cnd。 * 将综合查询参数追加到查询条件中,无返回值,处理结果直接写入 cnd。
* *
* @param cnd 查询条件容器,用于接收届次、名称/编号、提案人、组织及立案条件 * @param cnd 查询条件容器,用于接收届次、名称/编号、提案人、组织及立案条件
* @param searchParam 页面查询参数;proposalSearchField 传 name、code 或 caseFilingCode, * @param searchParam 页面查询参数;proposalSearchField 传 name 或 code,
* proposalKeyword 传对应字段的模糊搜索内容 * proposalKeyword 传对应字段的模糊搜索内容
*/ */
public static void buildSearch(Cnd cnd, ProposalQueryComprehensiveParam searchParam) { public static void buildSearch(Cnd cnd, ProposalQueryComprehensiveParam searchParam) {
@@ -93,12 +91,11 @@ public class ProposalQueryComprehensiveParam extends PageForm {
// Isolated single-session filter for pages that should not reuse sessionId semantics. // Isolated single-session filter for pages that should not reuse sessionId semantics.
cnd.andEX("info.sessionId", "=", searchParam.getSearchSessionId()); cnd.andEX("info.sessionId", "=", searchParam.getSearchSessionId());
// 名称/编号组合搜索仅允许三个固定字段,前端提交值不能直接作为SQL字段使用。 // 名称/编号组合搜索仅允许两个固定字段,前端提交值不能直接作为SQL字段使用。
if (StrUtil.isNotBlank(searchParam.getProposalKeyword())) { if (StrUtil.isNotBlank(searchParam.getProposalKeyword())) {
String proposalSearchColumn = switch (StrUtil.blankToDefault(searchParam.getProposalSearchField(), "name")) { String proposalSearchColumn = switch (StrUtil.blankToDefault(searchParam.getProposalSearchField(), "name")) {
case "name" -> "info.name"; case "name" -> "info.name";
case "code" -> "info.code"; case "code" -> "info.code";
case "caseFilingCode" -> "info.caseFilingCode";
default -> null; default -> null;
}; };
if (proposalSearchColumn != null) { if (proposalSearchColumn != null) {
@@ -114,10 +111,6 @@ public class ProposalQueryComprehensiveParam extends PageForm {
if (StrUtil.isNotBlank(searchParam.getCode())) { if (StrUtil.isNotBlank(searchParam.getCode())) {
cnd.where().andLike("info.code", searchParam.getCode()); cnd.where().andLike("info.code", searchParam.getCode());
} }
// 立案编号由立案环节生成,支持按已生成编号进行模糊检索。
if (StrUtil.isNotBlank(searchParam.getCaseFilingCode())) {
cnd.where().andLike("info.caseFilingCode", searchParam.getCaseFilingCode());
}
//代表团ID //代表团ID
cnd.andEX("info.delegationId", "=", searchParam.getDelegationId()); cnd.andEX("info.delegationId", "=", searchParam.getDelegationId());
if (StrUtil.isNotBlank(searchParam.getCreateUserName())) { if (StrUtil.isNotBlank(searchParam.getCreateUserName())) {
@@ -24,7 +24,7 @@ public class ProposalSearchParam extends PageForm {
private String createUserLoginName; private String createUserLoginName;
private String createUserKeyword; private String createUserKeyword;
private String caseFilingResult; private String caseFilingResult;
/** 名称/编号组合搜索字段,仅支持 name、code、caseFilingCode。 */ /** 名称/编号组合搜索字段,仅支持 name、code。 */
private String proposalSearchField; private String proposalSearchField;
/** 名称/编号组合搜索内容。 */ /** 名称/编号组合搜索内容。 */
private String proposalKeyword; private String proposalKeyword;
@@ -33,7 +33,7 @@ public class ProposalSearchParam extends PageForm {
* 将提案列表参数追加到查询条件中,无返回值,处理结果直接写入 cnd。 * 将提案列表参数追加到查询条件中,无返回值,处理结果直接写入 cnd。
* *
* @param cnd 查询条件容器,用于接收名称/编号、届次、提案人及立案结果条件 * @param cnd 查询条件容器,用于接收名称/编号、届次、提案人及立案结果条件
* @param searchParam 页面查询参数;proposalSearchField 传 name、code 或 caseFilingCode, * @param searchParam 页面查询参数;proposalSearchField 传 name 或 code,
* proposalKeyword 传对应字段的模糊搜索内容 * proposalKeyword 传对应字段的模糊搜索内容
*/ */
public static void buildSearch(Cnd cnd, ProposalSearchParam searchParam) { public static void buildSearch(Cnd cnd, ProposalSearchParam searchParam) {
@@ -45,7 +45,6 @@ public class ProposalSearchParam extends PageForm {
String proposalSearchColumn = switch (StrUtil.blankToDefault(searchParam.getProposalSearchField(), "name")) { String proposalSearchColumn = switch (StrUtil.blankToDefault(searchParam.getProposalSearchField(), "name")) {
case "name" -> "info.name"; case "name" -> "info.name";
case "code" -> "info.code"; case "code" -> "info.code";
case "caseFilingCode" -> "info.caseFilingCode";
default -> null; default -> null;
}; };
if (proposalSearchColumn != null) { if (proposalSearchColumn != null) {
@@ -82,16 +82,7 @@ import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService { public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService {
/** 确定立案提案的 Word 模板编码。 */ /** 所有立案结果统一使用重庆工贸提案征集表模板。 */
private static final String PROPOSAL_CONFIRM_FILING_TEMPLATE_CODE = "proposal_confirm_filing";
/** 工作建议提案的 Word 模板编码。 */
private static final String PROPOSAL_SUGGESTION_TEMPLATE_CODE = "proposal_suggestion";
/** 不予立案提案的 Word 模板编码。 */
private static final String PROPOSAL_NOT_TEMPLATE_CODE = "proposal_not";
/** 历史提案立案结果为空或无法识别时使用的原 Word 模板编码。 */
private static final String PROPOSAL_DEFAULT_TEMPLATE_CODE = "proposal"; private static final String PROPOSAL_DEFAULT_TEMPLATE_CODE = "proposal";
/** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */ /** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */
@@ -103,7 +94,6 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 征集进度页面允许排序的字段,避免排序参数直接拼接到SQL。 */ /** 征集进度页面允许排序的字段,避免排序参数直接拼接到SQL。 */
private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries( private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"), Map.entry("code", "info.code"),
Map.entry("caseFilingCode", "info.caseFilingCode"),
Map.entry("name", "info.name"), Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"), Map.entry("createUserName", "info.createUserName"),
Map.entry("delegationName", "delegationName"), Map.entry("delegationName", "delegationName"),
@@ -116,7 +106,6 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 征集进度页面允许自定义导出的字段及固定表头。 */ /** 征集进度页面允许自定义导出的字段及固定表头。 */
private static final Map<String, String> COLLECT_PROGRESS_EXPORT_COLUMNS = Map.ofEntries( private static final Map<String, String> COLLECT_PROGRESS_EXPORT_COLUMNS = Map.ofEntries(
Map.entry("code", "提案编号"), Map.entry("code", "提案编号"),
Map.entry("caseFilingCode", "立案编号"),
Map.entry("name", "提案名称"), Map.entry("name", "提案名称"),
Map.entry("createUserName", "提案人"), Map.entry("createUserName", "提案人"),
Map.entry("delegationName", "代表团"), Map.entry("delegationName", "代表团"),
@@ -147,7 +136,6 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 综合查询页面允许排序的字段,避免前端排序参数直接进入SQL。 */ /** 综合查询页面允许排序的字段,避免前端排序参数直接进入SQL。 */
private static final Map<String, String> COMPREHENSIVE_ORDER_COLUMNS = Map.ofEntries( private static final Map<String, String> COMPREHENSIVE_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"), Map.entry("code", "info.code"),
Map.entry("caseFilingCode", "info.caseFilingCode"),
Map.entry("name", "info.name"), Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"), Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"), Map.entry("typeName", "typeName"),
@@ -163,7 +151,6 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 综合查询页面允许自定义导出的字段及固定表头,防止请求伪造页面外字段。 */ /** 综合查询页面允许自定义导出的字段及固定表头,防止请求伪造页面外字段。 */
private static final Map<String, String> COMPREHENSIVE_EXPORT_COLUMNS = Map.ofEntries( private static final Map<String, String> COMPREHENSIVE_EXPORT_COLUMNS = Map.ofEntries(
Map.entry("code", "提案编号"), Map.entry("code", "提案编号"),
Map.entry("caseFilingCode", "立案编号"),
Map.entry("name", "提案名称"), Map.entry("name", "提案名称"),
Map.entry("createUserName", "提案人"), Map.entry("createUserName", "提案人"),
Map.entry("typeName", "提案类别"), Map.entry("typeName", "提案类别"),
@@ -402,10 +389,6 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
keywordCondition.orLike("info.createUserName", keyword); keywordCondition.orLike("info.createUserName", keyword);
keywordCondition.orLike("info.createUserLoginName", keyword); keywordCondition.orLike("info.createUserLoginName", keyword);
} }
// 立案编号由立案环节生成,选中该搜索来源时按编号进行模糊匹配。
if (ArrayUtil.contains(pageForm.getOrigins(), "caseFilingCode")) {
keywordCondition.orLike("info.caseFilingCode", keyword);
}
if (searchAnswer) { if (searchAnswer) {
keywordCondition.orLike("JSON_EXTRACT(replyTask.variable,'$.tf_opinion')", keyword); keywordCondition.orLike("JSON_EXTRACT(replyTask.variable,'$.tf_opinion')", keyword);
} }
@@ -469,7 +452,6 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
info.id, info.id,
info.name, info.name,
info.code, info.code,
info.caseFilingCode,
info.createUserName, info.createUserName,
type.NAME AS typeName, type.NAME AS typeName,
tcs.fullName AS sessionName, tcs.fullName AS sessionName,
@@ -924,19 +906,20 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
pages.put("commissioner", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "CONFIRM_FILING_COUNT", "CONFIRM_FILING_COUNT", "SUGGESTION_COUNT", "SUGGESTION_COUNT", "NOT_COUNT", "NOT_COUNT", "taskName", "taskName", "instanceState", "ins.state")); pages.put("commissioner", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "CONFIRM_FILING_COUNT", "CONFIRM_FILING_COUNT", "SUGGESTION_COUNT", "SUGGESTION_COUNT", "NOT_COUNT", "NOT_COUNT", "taskName", "taskName", "instanceState", "ins.state"));
pages.put("feedbackEvaluation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "merge", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state")); pages.put("feedbackEvaluation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "merge", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("delegation", standardOrderColumns()); pages.put("delegation", standardOrderColumns());
pages.put("control", mergeOrderColumns(standardOrderColumns(), orderColumns("caseFilingCode", "info.caseFilingCode"))); pages.put("control", standardOrderColumns());
pages.put("schoolLeaderApproval", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "undertakeUnits", "masterUnitName", "merge", "merge", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state")); pages.put("schoolLeaderApproval", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "undertakeUnits", "masterUnitName", "merge", "merge", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state"));
pages.put("committeeFilingUnit", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state")); pages.put("committeeFilingUnit", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("allFinished", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "instanceState", "ins.state")); pages.put("allFinished", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "instanceState", "ins.state"));
pages.put("preAudit", standardOrderColumns()); pages.put("preAudit", standardOrderColumns());
pages.put("schoolUnionChairApproval", standardOrderColumns());
pages.put("committeeFiling", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state")); pages.put("committeeFiling", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("caseCheck", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state", "finishTime", "t.finishTime")); pages.put("caseCheck", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state", "finishTime", "t.finishTime"));
pages.put("exportComprehensive", orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state")); pages.put("exportComprehensive", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("exportSingleCustom", standardOrderColumns()); pages.put("exportSingleCustom", standardOrderColumns());
pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username")); pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username"));
pages.put("configType", orderColumns("name", "name", "code", "code")); pages.put("configType", orderColumns("name", "name", "code", "code"));
pages.put("seniorBasic", seniorOrderColumns()); pages.put("seniorBasic", seniorOrderColumns());
pages.put("expediting", orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "curTaskName", "curTaskName", "underTakeName", "underTakeName", "taskName", "t.displayName", "actorName", "ta.actorName", "instanceState", "ins.state")); pages.put("expediting", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "curTaskName", "curTaskName", "underTakeName", "underTakeName", "taskName", "t.displayName", "actorName", "ta.actorName", "instanceState", "ins.state"));
pages.put("seniorDelete", seniorOrderColumns()); pages.put("seniorDelete", seniorOrderColumns());
pages.put("seniorFeedback", mergeOrderColumns(seniorOrderColumns(), orderColumns("tf_feedback", "tf_feedback"))); pages.put("seniorFeedback", mergeOrderColumns(seniorOrderColumns(), orderColumns("tf_feedback", "tf_feedback")));
pages.put("seniorType", seniorOrderColumns()); pages.put("seniorType", seniorOrderColumns());
@@ -950,7 +933,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 创建高级管理列表共用的字段映射。 */ /** 创建高级管理列表共用的字段映射。 */
private static Map<String, String> seniorOrderColumns() { private static Map<String, String> seniorOrderColumns() {
return orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state"); return orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state");
} }
/** /**
@@ -1013,7 +996,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
private Sql buildWorkflowExportSql(String pageCode, ProposalSearchParam pageForm, boolean approval) { private Sql buildWorkflowExportSql(String pageCode, ProposalSearchParam pageForm, boolean approval) {
boolean isUnitReply = "unitReply".equals(pageCode); boolean isUnitReply = "unitReply".equals(pageCode);
boolean isSchoolLeaderApproval = "schoolLeaderApproval".equals(pageCode); boolean isSchoolLeaderApproval = "schoolLeaderApproval".equals(pageCode);
boolean requiresUnionFilter = Set.of("preAudit", "committeeFiling", "committeeFilingUnit", boolean requiresUnionFilter = Set.of("preAudit", "schoolUnionChairApproval", "committeeFiling", "committeeFilingUnit",
"schoolLeaderApproval", "caseCheck").contains(pageCode); "schoolLeaderApproval", "caseCheck").contains(pageCode);
String taskName = getWorkflowTaskName(pageCode); String taskName = getWorkflowTaskName(pageCode);
@@ -1068,7 +1051,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
if (isUnitReply) { if (isUnitReply) {
cnd.and("t.taskName", "in", List.of("unit_reply", "two_unit_reply", "opinion_unit_reply")); cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply", "opinion_master_reply"));
if (!"superadmin".equals(SecurityUtil.getUserLoginname())) { if (!"superadmin".equals(SecurityUtil.getUserLoginname())) {
cnd.and("ta.actorId", "=", SecurityUtil.getUserId()); cnd.and("ta.actorId", "=", SecurityUtil.getUserId());
} }
@@ -1102,9 +1085,10 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
Map<String, String> taskNames = Map.of( Map<String, String> taskNames = Map.of(
"delegation", "delegation", "delegation", "delegation",
"preAudit", "preAudit", "preAudit", "preAudit",
"schoolUnionChairApproval", "schoolUnionChairApproval",
"committeeFiling", "committee", "committeeFiling", "committee",
"committeeFilingUnit", "committeeFilingUnit", "committeeFilingUnit", "committeeFilingUnit",
"schoolLeaderApproval", "schoolLeader", "schoolLeaderApproval", "school_leader",
"feedbackEvaluation", "feedback", "feedbackEvaluation", "feedback",
"caseCheck", "caseCheck" "caseCheck", "caseCheck"
); );
@@ -1170,22 +1154,23 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 初始化各办理页面可导出的列表字段白名单。 */ /** 初始化各办理页面可导出的列表字段白名单。 */
private static Map<String, Set<String>> createWorkflowExportColumns() { private static Map<String, Set<String>> createWorkflowExportColumns() {
Set<String> commonColumns = Set.of("code", "caseFilingCode", "name", "createUserName", "typeName", Set<String> commonColumns = Set.of("code", "name", "createUserName", "typeName",
"sessionName", "delegationName", "curTaskName", "instanceState"); "sessionName", "delegationName", "curTaskName", "instanceState");
Map<String, Set<String>> columns = new HashMap<>(); Map<String, Set<String>> columns = new HashMap<>();
columns.put("delegation", commonColumns); columns.put("delegation", commonColumns);
columns.put("preAudit", commonColumns); columns.put("preAudit", commonColumns);
columns.put("caseCheck", Set.of("code", "caseFilingCode", "name", "createUserName", "typeName", columns.put("schoolUnionChairApproval", commonColumns);
columns.put("caseCheck", Set.of("code", "name", "createUserName", "typeName",
"sessionName", "delegationName", "curTaskName", "instanceState", "finishTime")); "sessionName", "delegationName", "curTaskName", "instanceState", "finishTime"));
columns.put("committeeFiling", Set.of("code", "caseFilingCode", "name", "typeName", "sessionName", columns.put("committeeFiling", Set.of("code", "name", "typeName", "sessionName",
"caseFilingResult", "delegationName", "curTaskName", "instanceState")); "caseFilingResult", "delegationName", "curTaskName", "instanceState"));
columns.put("committeeFilingUnit", Set.of("code", "caseFilingCode", "name", "typeName", "sessionName", columns.put("committeeFilingUnit", Set.of("code", "name", "typeName", "sessionName",
"caseFilingResult", "delegationName", "curTaskName", "instanceState")); "caseFilingResult", "delegationName", "curTaskName", "instanceState"));
columns.put("schoolLeaderApproval", Set.of("code", "caseFilingCode", "name", "typeName", "undertakeUnits", columns.put("schoolLeaderApproval", Set.of("code", "name", "typeName", "undertakeUnits",
"merge", "curTaskName", "auditUser", "instanceState")); "merge", "curTaskName", "auditUser", "instanceState"));
columns.put("unitReply", Set.of("code", "caseFilingCode", "name", "typeName", "caseFilingResult", columns.put("unitReply", Set.of("code", "name", "typeName", "caseFilingResult",
"merge", "underTakeName", "underTakeIsMaster", "curTaskName", "auditUser", "instanceState")); "merge", "underTakeName", "underTakeIsMaster", "curTaskName", "auditUser", "instanceState"));
columns.put("feedbackEvaluation", Set.of("code", "caseFilingCode", "name", "typeName", "merge", columns.put("feedbackEvaluation", Set.of("code", "name", "typeName", "merge",
"curTaskName", "instanceState")); "curTaskName", "instanceState"));
return Collections.unmodifiableMap(columns); return Collections.unmodifiableMap(columns);
} }
@@ -1373,6 +1358,11 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
execute(sql); execute(sql);
NutMap docData = (NutMap) sql.getResult(); NutMap docData = (NutMap) sql.getResult();
// 提案可能已被删除,避免在读取模板字段时产生空指针。
if (docData == null) {
throw new BaseException("提案信息不存在");
}
//处理下富文本 //处理下富文本
String brief = docData.getString("brief"); String brief = docData.getString("brief");
String measures = docData.getString("measures"); String measures = docData.getString("measures");
@@ -1387,12 +1377,23 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
ProcessInstance processInstance = dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", id)); ProcessInstance processInstance = dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", id));
List<ProcessTaskVO> doneTaskVos = new ArrayList<>(); List<ProcessTaskVO> doneTaskVos = new ArrayList<>();
List<ProcessTask> doneTaskList = flowEngine.processTaskService().getDoneTaskList(processInstance.getId(), null); // 尚未启动或缺少 WF 实例的提案仍导出基础信息,附议和审批栏保持为空。
for (ProcessTask doneTask : doneTaskList) { if (processInstance != null) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(doneTask.getId()); List<ProcessTask> doneTaskList = flowEngine.processTaskService().getDoneTaskList(processInstance.getId(), null);
doneTaskVos.add(taskVO); for (ProcessTask doneTask : doneTaskList) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(doneTask.getId());
if (taskVO != null && taskVO.getExt() != null) {
doneTaskVos.add(taskVO);
}
}
} }
// 附议人单元格仅展示已完成附议任务中的姓名,多人以顿号分隔并去重。
docData.put("secondNames", doneTaskVos.stream()
.filter(task -> "second".equals(task.getTaskName()))
.map(task -> task.getTaskFormData().getStr("userName"))
.filter(StrUtil::isNotBlank).distinct().collect(Collectors.joining("、")));
// 按任务节点分组 // 按任务节点分组
Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream() Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
@@ -1400,6 +1401,17 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
docData.putAll(taskGroups); docData.putAll(taskGroups);
putProposalExportTaskAliases(id, taskGroups, docData); putProposalExportTaskAliases(id, taskGroups, docData);
// 中文区块名称保持与现有模板一致,实际审核记录按 WF 节点编码匹配。
// 区块内继续读取 taskFormData 的意见、姓名和日期;无记录时写入空列表,避免同名节点误填。
Map<String, String> approvalTemplateNodes = Map.of(
"分工会主席审批", "delegation",
"校工会主席审批", "schoolUnionChairApproval",
"分管校领导审批", "school_leader");
approvalTemplateNodes.forEach((templateKey, taskName) -> docData.put(templateKey, doneTaskVos.stream()
.filter(task -> taskName.equals(task.getTaskName()))
.sorted(Comparator.comparing(ProcessTaskVO::getFinishTime, Comparator.nullsLast(Comparator.naturalOrder())))
.collect(Collectors.toList())));
// 提案附议 // 提案附议
List<NutMap> secondInfos = new ArrayList<>(); List<NutMap> secondInfos = new ArrayList<>();
List<ProcessTaskVO> secondTaskVos = taskGroups.get("提案附议"); List<ProcessTaskVO> secondTaskVos = taskGroups.get("提案附议");
@@ -1481,29 +1493,13 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
docData.put("secondedList", secondInfos); docData.put("secondedList", secondInfos);
docData.put("delegationAuditList", delegationAuditList); docData.put("delegationAuditList", delegationAuditList);
String templateCode = getProposalTemplateCode(docData.getString("caseFilingResult"));
try { try {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateCode), config).render(docData).writeAndClose(byteArrayOutputStream); XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(PROPOSAL_DEFAULT_TEMPLATE_CODE), config).render(docData).writeAndClose(byteArrayOutputStream);
} catch (IOException e) { } catch (IOException e) {
log.error("提案导出失败{},提案id:{}", e.getMessage(), id); log.error("提案导出失败{},提案id:{}", e.getMessage(), id);
} }
} }
/**
* 根据立案结果选择数据库办公模板编码。
*
* @param caseFilingResult 立案结果字典编码:CONFIRM_FILING、SUGGESTION 或 NOT
* @return 对应的 Word 模板编码;历史空值或未知编码返回 proposal
*/
private String getProposalTemplateCode(String caseFilingResult) {
return switch (StrUtil.nullToEmpty(caseFilingResult)) {
case "CONFIRM_FILING" -> PROPOSAL_CONFIRM_FILING_TEMPLATE_CODE;
case "SUGGESTION" -> PROPOSAL_SUGGESTION_TEMPLATE_CODE;
case "NOT" -> PROPOSAL_NOT_TEMPLATE_CODE;
default -> PROPOSAL_DEFAULT_TEMPLATE_CODE;
};
}
/** /**
* 将现行工作流节点映射到历史提案表模板使用的区块名称,并按主协办单位拆分办理答复。 * 将现行工作流节点映射到历史提案表模板使用的区块名称,并按主协办单位拆分办理答复。
* 模板仍使用旧节点名称时,避免已完成的意见和答复因键名不一致而无法渲染。 * 模板仍使用旧节点名称时,避免已完成的意见和答复因键名不一致而无法渲染。
@@ -20,6 +20,7 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge; import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake; import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService; import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingUnitService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService; import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
@@ -44,6 +45,8 @@ public class ProposalCommitteeFilingServiceImpl extends BaseServiceImpl<Proposal
@Inject @Inject
private ProposalCommonService proposalCommonService; private ProposalCommonService proposalCommonService;
@Inject @Inject
private ProposalCommitteeFilingUnitService proposalCommitteeFilingUnitService;
@Inject
private FlowCommonService flowCommonService; private FlowCommonService flowCommonService;
@Inject @Inject
private ProcessTaskService processTaskService; private ProcessTaskService processTaskService;
@@ -58,7 +61,8 @@ public class ProposalCommitteeFilingServiceImpl extends BaseServiceImpl<Proposal
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void executeFlowTask(Dict args) { public void executeFlowTask(Dict args) {
validateSelectedUnitLeaders(args); validateSelectedUnitLeaders(args);
flowCommonService.executeTask(args); // 立案审核复用原确认承办单位的表单,业务字段和单位记录须在同一事务内先于 WF 流转保存。
proposalCommitteeFilingUnitService.executeFlowTask(args);
} }
@Override @Override
@@ -117,7 +121,7 @@ public class ProposalCommitteeFilingServiceImpl extends BaseServiceImpl<Proposal
selectedUnitIds.addAll(getUnitIdList(args, FlowConst.TASK_FORM_DATA_PREFIX + "slaveUnitIds")); selectedUnitIds.addAll(getUnitIdList(args, FlowConst.TASK_FORM_DATA_PREFIX + "slaveUnitIds"));
} }
if (selectedUnitIds.isEmpty()) { if (selectedUnitIds.isEmpty()) {
return; throw new BaseException("请选择主办或办理单位");
} }
Sys_role unitLeaderRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER); Sys_role unitLeaderRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER);
@@ -79,6 +79,21 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
if (currentTask == null) { if (currentTask == null) {
throw new BaseException("没有进行中的流程任务"); throw new BaseException("没有进行中的流程任务");
} }
if (!"committee".equals(currentTask.getTaskName()) && !"committeeFilingUnit".equals(currentTask.getTaskName())) {
throw new BaseException("当前任务不是立案审核节点");
}
Integer submitType = args.getInt(FlowConst.SUBMIT_TYPE);
if (submitType == null) {
// 兼容未显式传提交类型的旧页面,默认按同意立案审核处理。
submitType = ProcessSubmitTypeEnum.AGREE.getCode();
args.put(FlowConst.SUBMIT_TYPE, submitType);
}
if (!ProcessSubmitTypeEnum.AGREE.getCode().equals(submitType)) {
// 退回或拒绝不形成立案结果,也不能清除上次已保存的承办单位。
flowCommonService.executeTask(args);
return;
}
String proposalId = StrUtil.trim(args.getStr("proposalId")); String proposalId = StrUtil.trim(args.getStr("proposalId"));
if (StrUtil.isBlank(proposalId) || dao().fetch(ProposalInfo.class, proposalId) == null) { if (StrUtil.isBlank(proposalId) || dao().fetch(ProposalInfo.class, proposalId) == null) {
@@ -90,7 +105,6 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
if (ObjectUtil.isEmpty(proposalIds)) { if (ObjectUtil.isEmpty(proposalIds)) {
proposalIds = List.of(proposalId); proposalIds = List.of(proposalId);
} }
generateAndNormalizeCaseFilingCode(args, proposalIds);
/* /*
* 先同步 proposal_info 和承办单位,再执行 WF 流转,使查看页面及下一节点读取到的都是本次最新数据。 * 先同步 proposal_info 和承办单位,再执行 WF 流转,使查看页面及下一节点读取到的都是本次最新数据。
@@ -114,129 +128,18 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
} }
/** /**
* 按立案结果和服务器当前年份生成立案编号,并覆盖页面可能传入的编号。 * 立案审核提交后,只同步立案结果、立案类型和主协办单位,不生成立案编号。
* 正常提交时生成编号;退回等非同意操作不占用流水号。并案提案共用同一个编号。
*
* @param args 流程提交参数,tf_caseFilingResult 分别支持 CONFIRM_FILING、SUGGESTION、NOT
* @param proposalIds 当前办理的提案主键集合,并案时包含同组全部提案
*/
private void generateAndNormalizeCaseFilingCode(Dict args, List<String> proposalIds) {
String caseFilingResultKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult";
String caseFilingCodeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode";
String caseFilingTypeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType";
String caseFilingResult = args.getStr(caseFilingResultKey);
if (!"CONFIRM_FILING".equals(caseFilingResult)) {
// 立案类型仅适用于确定立案,切换为其他结果时同步清理历史值。
args.put(caseFilingTypeKey, "");
}
Integer submitType = args.getInt(FlowConst.SUBMIT_TYPE);
if (submitType == null) {
submitType = ProcessSubmitTypeEnum.AGREE.getCode();
args.put(FlowConst.SUBMIT_TYPE, submitType);
}
if (!ProcessSubmitTypeEnum.AGREE.getCode().equals(submitType)) {
args.put(caseFilingCodeKey, "");
return;
}
String codePrefix = resolveCaseFilingCodePrefix(caseFilingResult);
int currentYear = java.time.LocalDate.now().getYear();
String codeHead = currentYear + codePrefix;
/*
* proposal_config 当前是单行配置表,锁定该行可让所有应用实例串行生成流水号。
* 行锁一直持有到本次 WF 提交事务结束,避免两个审核请求同时读到相同最大值。
*/
Sql lockSql = Sqls.create("SELECT id FROM proposal_config ORDER BY id LIMIT 1 FOR UPDATE");
lockSql.setCallback(Sqls.callback.map());
dao().execute(lockSql);
Map<?, ?> configRow = lockSql.getObject(Map.class);
if (ObjectUtil.isEmpty(configRow)) {
throw new BaseException("提案配置不存在,无法生成立案编号");
}
String reusableCode = findReusableCaseFilingCode(proposalIds, caseFilingResult, codeHead);
if (StrUtil.isNotBlank(reusableCode)) {
args.put(caseFilingCodeKey, reusableCode);
return;
}
Sql maxSequenceSql = Sqls.create("""
SELECT COALESCE(MAX(CAST(SUBSTRING(caseFilingCode, 6) AS UNSIGNED)), 0)
FROM proposal_info
WHERE caseFilingCode REGEXP @codePattern
""");
maxSequenceSql.setParam("codePattern", "^" + codeHead + "[0-9]{3,}$");
maxSequenceSql.setCallback(Sqls.callback.integer());
dao().execute(maxSequenceSql);
int nextSequence = maxSequenceSql.getInt() + 1;
String caseFilingCode = codeHead + String.format("%03d", nextSequence);
args.put(caseFilingCodeKey, caseFilingCode);
}
/**
* 将立案结果转换成编号前缀,未知结果直接阻止流程提交,避免生成无业务含义的编号。
*
* @param caseFilingResult 立案结果编码
* @return 确定立案返回 A、工作建议返回 B、不予立案返回 C
*/
private String resolveCaseFilingCodePrefix(String caseFilingResult) {
return switch (StrUtil.nullToEmpty(caseFilingResult)) {
case "CONFIRM_FILING" -> "A";
case "SUGGESTION" -> "B";
case "NOT" -> "C";
default -> throw new BaseException("立案结果不正确,无法生成立案编号");
};
}
/**
* 流程撤回或重新提交时,复用同一批提案已有的本年同类型编号,避免流水号无意义增长。
* 只有全部提案的结果和编号完全一致且符合新编号格式时才允许复用。
*
* @param proposalIds 当前单案或并案提案主键集合
* @param caseFilingResult 本次选择的立案结果
* @param codeHead 年份和编号前缀,例如 2026A
* @return 可复用的编号;不存在时返回空字符串
*/
private String findReusableCaseFilingCode(List<String> proposalIds, String caseFilingResult, String codeHead) {
List<ProposalInfo> proposalInfos = dao().query(ProposalInfo.class,
Cnd.where(ProposalInfo::getId, "in", proposalIds));
if (proposalInfos.size() != new LinkedHashSet<>(proposalIds).size()) {
return "";
}
boolean allProposalsMatch = proposalInfos.stream().allMatch(proposalInfo ->
Objects.equals(caseFilingResult, proposalInfo.getCaseFilingResult())
&& StrUtil.isNotBlank(proposalInfo.getCaseFilingCode()));
if (!allProposalsMatch) {
return "";
}
LinkedHashSet<String> existingCodes = proposalInfos.stream()
.map(ProposalInfo::getCaseFilingCode)
.map(StrUtil::trim)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (existingCodes.size() != 1) {
return "";
}
String existingCode = existingCodes.iterator().next();
return existingCode.matches(codeHead + "\\d{3,}") ? existingCode : "";
}
/**
* old-flow 页面提交后,需要把立案编号、立案结果以及主协办单位同步落业务表。
* 若这次改成“不予立案”等无需承办单位的结果,也要先清掉旧的承办单位记录,避免脏数据残留。 * 若这次改成“不予立案”等无需承办单位的结果,也要先清掉旧的承办单位记录,避免脏数据残留。
*/ */
private void syncCommitteeFilingUnitData(List<String> proposalIds, Dict args) { private void syncCommitteeFilingUnitData(List<String> proposalIds, Dict args) {
String caseFilingCode = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode");
String caseFilingResult = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult"); String caseFilingResult = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult");
String caseFilingType = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType"); String caseFilingType = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType");
dao().update(ProposalInfo.class, dao().update(ProposalInfo.class,
Chain.make("caseFilingCode", caseFilingCode) Chain.make("caseFilingResult", caseFilingResult)
.add("caseFilingResult", caseFilingResult)
.add("caseFilingType", caseFilingType), .add("caseFilingType", caseFilingType),
Cnd.where(ProposalInfo::getId, "in", proposalIds)); Cnd.where(ProposalInfo::getId, "in", proposalIds));
verifyCommitteeFilingUnitData(proposalIds, caseFilingCode, caseFilingResult, caseFilingType); verifyCommitteeFilingUnitData(proposalIds, caseFilingResult, caseFilingType);
dao().clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds)); dao().clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds));
if (!List.of("CONFIRM_FILING", "SUGGESTION").contains(caseFilingResult)) { if (!List.of("CONFIRM_FILING", "SUGGESTION").contains(caseFilingResult)) {
@@ -276,7 +179,7 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
/** /**
* 校验本次立案字段是否已同步到全部提案,避免流程变量更新成功但 info 基础信息仍保留旧值。 * 校验本次立案字段是否已同步到全部提案,避免流程变量更新成功但 info 基础信息仍保留旧值。
*/ */
private void verifyCommitteeFilingUnitData(List<String> proposalIds, String caseFilingCode, private void verifyCommitteeFilingUnitData(List<String> proposalIds,
String caseFilingResult, String caseFilingType) { String caseFilingResult, String caseFilingType) {
List<ProposalInfo> proposalInfos = dao().query(ProposalInfo.class, List<ProposalInfo> proposalInfos = dao().query(ProposalInfo.class,
Cnd.where(ProposalInfo::getId, "in", proposalIds)); Cnd.where(ProposalInfo::getId, "in", proposalIds));
@@ -284,9 +187,7 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
throw new BaseException("部分提案信息不存在,无法同步立案结果"); throw new BaseException("部分提案信息不存在,无法同步立案结果");
} }
boolean syncFailed = proposalInfos.stream().anyMatch(proposalInfo -> boolean syncFailed = proposalInfos.stream().anyMatch(proposalInfo ->
!Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingCode(), ""), !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingResult(), ""),
StrUtil.nullToDefault(caseFilingCode, ""))
|| !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingResult(), ""),
StrUtil.nullToDefault(caseFilingResult, "")) StrUtil.nullToDefault(caseFilingResult, ""))
|| !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingType(), ""), || !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingType(), ""),
StrUtil.nullToDefault(caseFilingType, ""))); StrUtil.nullToDefault(caseFilingType, "")));
@@ -189,7 +189,6 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
info.id, info.id,
info.name, info.name,
info.code, info.code,
info.caseFilingCode,
info.caseFilingResult, info.caseFilingResult,
info.caseFilingType, info.caseFilingType,
info.researchFindings, info.researchFindings,
@@ -259,7 +258,6 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
List<ExcelExportEntity> exportEntities = new ArrayList<>(); List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("序号", "index", 10)); exportEntities.add(new ExcelExportEntity("序号", "index", 10));
exportEntities.add(new ExcelExportEntity("提案编号", "code", 20)); exportEntities.add(new ExcelExportEntity("提案编号", "code", 20));
exportEntities.add(new ExcelExportEntity("立案编号", "caseFilingCode", 20));
exportEntities.add(new ExcelExportEntity("立案结果", "caseFilingResultName", 15)); exportEntities.add(new ExcelExportEntity("立案结果", "caseFilingResultName", 15));
exportEntities.add(new ExcelExportEntity("立案类型", "caseFilingTypeName", 15)); exportEntities.add(new ExcelExportEntity("立案类型", "caseFilingTypeName", 15));
exportEntities.add(new ExcelExportEntity("是否并案", "isConsolidationName", 12)); exportEntities.add(new ExcelExportEntity("是否并案", "isConsolidationName", 12));
@@ -318,7 +316,6 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
NutMap proposalRow = proposalRows.computeIfAbsent(proposalId, key -> NutMap.NEW() NutMap proposalRow = proposalRows.computeIfAbsent(proposalId, key -> NutMap.NEW()
.addv("id", proposalId) .addv("id", proposalId)
.addv("code", row.getString("code")) .addv("code", row.getString("code"))
.addv("caseFilingCode", row.getString("caseFilingCode"))
.addv("name", row.getString("name")) .addv("name", row.getString("name"))
.addv("createUserName", row.getString("createUserName")) .addv("createUserName", row.getString("createUserName"))
.addv("caseFilingResult", row.getString("caseFilingResult")) .addv("caseFilingResult", row.getString("caseFilingResult"))
@@ -373,7 +370,6 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
SELECT DISTINCT SELECT DISTINCT
info.id, info.id,
info.code, info.code,
info.caseFilingCode,
info.name, info.name,
info.createUserName, info.createUserName,
info.caseFilingResult, info.caseFilingResult,
@@ -413,9 +409,8 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
LinkedHashSet<String> masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames"); LinkedHashSet<String> masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames");
LinkedHashSet<String> slaveUnitNames = getUnitNameSet(proposalRow, "slaveUnitNames"); LinkedHashSet<String> slaveUnitNames = getUnitNameSet(proposalRow, "slaveUnitNames");
NutMap excelRow = NutMap.NEW() NutMap excelRow = NutMap.NEW()
// 承办单位表统一展示为“立案编号”;未生成立案编号的工作建议回退显示提案编号。 // 承办单位表只展示提案编号,立案结果不再生成独立编号。
.addv("caseFilingCode", StrUtil.blankToDefault(proposalRow.getString("caseFilingCode"), .addv("code", proposalRow.getString("code"))
proposalRow.getString("code")))
.addv("name", proposalRow.getString("name")) .addv("name", proposalRow.getString("name"))
.addv("createUserName", proposalRow.getString("createUserName")) .addv("createUserName", proposalRow.getString("createUserName"))
.addv("masterUnitNames", "") .addv("masterUnitNames", "")
@@ -434,7 +429,7 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
} }
if (!suggestionRows.isEmpty()) { if (!suggestionRows.isEmpty()) {
filingRows.add(NutMap.NEW() filingRows.add(NutMap.NEW()
.addv("caseFilingCode", WORK_SUGGESTION_SECTION_MARKER) .addv("code", WORK_SUGGESTION_SECTION_MARKER)
.addv("name", "") .addv("name", "")
.addv("createUserName", "") .addv("createUserName", "")
.addv("masterUnitNames", "") .addv("masterUnitNames", "")
@@ -450,7 +445,7 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
*/ */
private Workbook buildUndertakeUnitWorkbook(String title, String unitName, List<NutMap> excelRows) { private Workbook buildUndertakeUnitWorkbook(String title, String unitName, List<NutMap> excelRows) {
List<ExcelExportEntity> exportEntities = new ArrayList<>(); List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("立案编号", "caseFilingCode", 11)); exportEntities.add(new ExcelExportEntity("提案编号", "code", 11));
exportEntities.add(new ExcelExportEntity("提案名称", "name", 43)); exportEntities.add(new ExcelExportEntity("提案名称", "name", 43));
exportEntities.add(new ExcelExportEntity("提案人", "createUserName", 13)); exportEntities.add(new ExcelExportEntity("提案人", "createUserName", 13));
exportEntities.add(new ExcelExportEntity("主办单位", "masterUnitNames", 19)); exportEntities.add(new ExcelExportEntity("主办单位", "masterUnitNames", 19));
@@ -925,7 +920,6 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
info.id, info.id,
info.name, info.name,
info.code, info.code,
info.caseFilingCode,
info.caseFilingResult, info.caseFilingResult,
info.caseFilingType, info.caseFilingType,
info.researchFindings, info.researchFindings,
@@ -336,7 +336,7 @@ public class ProposalUnderTakeReplyServiceImpl extends BaseServiceImpl<ProposalI
if (replyTask == null) { if (replyTask == null) {
throw new BaseException("未找到答复任务"); throw new BaseException("未找到答复任务");
} }
if (!List.of("unit_reply", "two_unit_reply", "opinion_master_reply").contains(replyTask.getTaskName())) { if (!List.of("master_reply", "slave_reply", "opinion_master_reply").contains(replyTask.getTaskName())) {
throw new BaseException("当前任务不支持编辑答复内容"); throw new BaseException("当前任务不支持编辑答复内容");
} }
if (!Objects.equals(ProcessTaskStateEnum.FINISHED.getCode(), replyTask.getTaskState())) { if (!Objects.equals(ProcessTaskStateEnum.FINISHED.getCode(), replyTask.getTaskState())) {
@@ -0,0 +1,60 @@
package com.budwk.app.zhgh.staffbenefit.condolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@Ok("json")
@At("/platform/condolence/approval")
public class CondolenceApprovalController {
@Inject
private Dao dao;
@Inject
private FlowCommonService flowCommonService;
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data);
if (args == null || args.getLong(FlowConst.PROCESS_TASK_ID_KEY) == null) {
throw new BaseException("审批任务不能为空");
}
ProcessTask task = dao.fetch(ProcessTask.class, args.getLong(FlowConst.PROCESS_TASK_ID_KEY));
if (task == null) throw new BaseException("审批任务不存在");
if (!"4dad0bef-3cf2-4a7d-bba3-35020b9b216d".equals(task.getTaskName())
&& !"34f7938e-47a3-4b3a-b4e0-92b7a3f47865".equals(task.getTaskName())
&& !"ecdaef3b-edf4-4143-9a96-887dd5921711".equals(task.getTaskName())
&& !"cbd41337-3c87-4959-9027-809df5c7ef5e".equals(task.getTaskName())) {
throw new BaseException("当前任务不是职工慰问审批任务");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
Condolence info = instance == null ? null : dao.fetch(Condolence.class, instance.getBusinessNo());
if (info == null) throw new BaseException("职工慰问申请不存在");
if (StrUtil.isBlank(args.getStr("tf_sign"))) throw new BaseException("请完成审批签字");
if (StrUtil.isBlank(args.getStr("tf_opinion"))) throw new BaseException("请填写审批意见");
// 参与者权限由流程引擎校验,业务入口不接受客户端指定管理员代办。
args.remove(FlowConst.EXT_ENABLE_OPERATOR);
flowCommonService.executeTask(args);
return Result.success();
}
}
@@ -17,6 +17,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -27,7 +28,7 @@ import org.nutz.mvc.annotation.Param;
import java.util.List; import java.util.List;
/** /**
* @ClassName CondolenceBranchUnionApprovalController * @ClassName CondolenceFirstApprovalController
* @Author JyuHsin * @Author JyuHsin
* @Date 2025/7/28 11:33 * @Date 2025/7/28 11:33
* @Version 1.0 * @Version 1.0
@@ -35,26 +36,26 @@ import java.util.List;
*/ */
@IocBean @IocBean
@At("/platform/condolence/branchUnionApproval") @At("/platform/condolence/branchUnionApproval")
@Api("职工慰问分工会审核") @Api("职工慰问一级审核")
@Ok("json:full") @Ok("json:full")
public class CondolenceBranchUnionApprovalController { public class CondolenceFirstApprovalController {
@Inject @Inject
private CondolenceService condolenceService; private CondolenceService condolenceService;
@At("/") @At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/condolence/branchUnionApproval/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/condolence/branchUnionApproval/index.html")
@SaCheckPermission("condolence.branchUnionApproval") @SaCheckPermission("condolence.firstApproval")
public void index() {} public void index() {}
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/condolence/branchUnionApproval/index.html") @Ok("beetl:/platform/zhghh5/staffbenefit/condolence/branchUnionApproval/index.html")
@SaCheckPermission("h5.condolence.branchUnionApproval") @SaCheckPermission("h5.condolence.firstApproval")
public void h5Index() {} public void h5Index() {}
@At @At
@ApiOperation("分页查询") @ApiOperation("分页查询")
@SaCheckPermission(value = {"condolence.branchUnionApproval", "h5.condolence.branchUnionApproval"}, mode = SaMode.OR) @SaCheckPermission(value = {"condolence.firstApproval", "h5.condolence.firstApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm, public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year, @Param(value = "year") Integer year,
@Param(value = "type") String type, @Param(value = "type") String type,
@@ -90,6 +91,14 @@ public class CondolenceBranchUnionApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
@@ -100,7 +109,9 @@ public class CondolenceBranchUnionApprovalController {
cnd.andEX("YEAR(info.createTime)", "=", year); cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.and("t.taskName", "=", "4dad0bef-3cf2-4a7d-bba3-35020b9b216d"); cnd.and("t.taskName", "=", "4dad0bef-3cf2-4a7d-bba3-35020b9b216d");
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); if (!"superadmin".equals(SecurityUtil.getUserLoginname())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (approval) { if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode())); cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
@@ -0,0 +1,160 @@
package com.budwk.app.zhgh.staffbenefit.condolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
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.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.dao.util.cri.Static;
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.List;
/**
* @ClassName CondolenceFourthApprovalController
* @Author JyuHsin
* @Date 2025/7/29 16:23
* @Version 1.0
* @Description TODO
*/
@IocBean
@At("/platform/condolence/fourthApproval")
@Api("职工慰问四级审核")
@Ok("json:full")
public class CondolenceFourthApprovalController {
@Inject
private Dao dao;
@Inject
private CondolenceService condolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/condolence/fourthApproval/index.html")
@SaCheckPermission("condolence.fourthApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/condolence/fourthApproval/index.html")
@SaCheckPermission("h5.condolence.fourthApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"condolence.fourthApproval", "h5.condolence.fourthApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(Condolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "cbd41337-3c87-4959-9027-809df5c7ef5e");
if (!"superadmin".equals(SecurityUtil.getUserLoginname()) && !AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
Sql unitSql = Sqls.queryString("""
SELECT DISTINCT ur.unitId
FROM sys_user_role ur
INNER JOIN sys_role r ON r.id = ur.roleId
WHERE ur.userId = @userId
AND ur.enable = true
AND r.disabled = false
AND r.code = @roleCode
AND ur.unitId IS NOT NULL
AND ur.unitId <> ''
""");
unitSql.setParam("userId", SecurityUtil.getUserId());
unitSql.setParam("roleCode", RoleConstant.UNIT_SCHOOL_LEADER.name());
dao.execute(unitSql);
List<String> managedUnitIds = unitSql.getList(String.class);
if (managedUnitIds.isEmpty()) {
cnd.and(new Static("1 = 0"));
} else {
cnd.and("info.applyUnitId", "in", managedUnitIds);
}
}
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = condolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -3,23 +3,35 @@ package com.budwk.app.zhgh.staffbenefit.condolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm; import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.sms.SmsService; import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.sys.views.View_user; import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence; import com.budwk.app.zhgh.staffbenefit.condolence.model.Condolence;
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType; import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService; import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceService;
import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
@@ -34,6 +46,10 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
/** /**
* @ClassName CondolenceApplyController * @ClassName CondolenceApplyController
* @Author JyuHsin * @Author JyuHsin
@@ -41,6 +57,7 @@ import org.nutz.mvc.annotation.Param;
* @Version 1.0 * @Version 1.0
* @Description TODO * @Description TODO
*/ */
@Slf4j
@IocBean @IocBean
@At("/platform/condolence/mine") @At("/platform/condolence/mine")
@Api("职工慰问我的") @Api("职工慰问我的")
@@ -51,6 +68,8 @@ public class CondolenceMineController {
private CondolenceService condolenceService; private CondolenceService condolenceService;
@Inject @Inject
private FlowEngine flowEngine; private FlowEngine flowEngine;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@At("/") @At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/condolence/mine/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/condolence/mine/index.html")
@@ -132,6 +151,98 @@ public class CondolenceMineController {
return Result.success(pagination); return Result.success(pagination);
} }
@At
@Ok("void")
@ApiOperation("导出职工慰问申请表")
@SaCheckPermission("condolence.mine")
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
log.info("[职工慰问导出] 开始,applyId={}, userId={}, format={}", id, SecurityUtil.getUserId(), Boolean.TRUE.equals(print) ? "PDF" : "Word");
Condolence condolence = condolenceService.fetch(id);
if (condolence == null) throw new BaseException("该条记录不存在");
if (!SecurityUtil.getUserId().equals(condolence.getCreatedBy())) {
throw new BaseException("只能导出本人提交的申请");
}
ProcessInstance instance = condolenceService.dao().fetch(ProcessInstance.class,
Cnd.where("businessNo", "=", id).desc("id"));
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
throw new BaseException("只能导出已完成的申请");
}
log.info("[职工慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(condolence.getSignature()));
NutMap docData = Lang.obj2nutmap(condolence);
docData.put("payUserName", StrUtil.nullToEmpty(condolence.getBankUserName()));
CondolenceType type = condolenceService.dao().fetch(CondolenceType.class, condolence.getType());
docData.put("typeName", type == null ? "" : type.getName());
docData.put("signature", null);
if (StrUtil.isNotBlank(condolence.getSignature())) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, condolence.getSignature());
if (signature == null) throw new BaseException("申请人签字图片读取失败");
docData.put("signature", signature);
}
fillApplyApprovals(docData, instance);
for (String field : List.of("signature", "firstApprovalSign", "secondApprovalSign", "thirdApprovalSign", "fourthApprovalSign")) {
Object value = docData.get(field);
log.info("[职工慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
}
String fileName = "重庆工贸职业技术学院职工慰问申请表_" + StrUtil.nullToEmpty(condolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
if (input == null) throw new BaseException("职工慰问申请表模板不存在");
boolean pdf = Boolean.TRUE.equals(print);
byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, null, pdf, "condolence:" + id);
log.info("[职工慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} catch (Exception e) {
log.error("[职工慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
throw e;
}
}
private void fillApplyApprovals(NutMap docData, ProcessInstance instance) {
String[] taskKeys = {"4dad0bef-3cf2-4a7d-bba3-35020b9b216d", "34f7938e-47a3-4b3a-b4e0-92b7a3f47865",
"ecdaef3b-edf4-4143-9a96-887dd5921711", "cbd41337-3c87-4959-9027-809df5c7ef5e"};
String[] fields = {"firstApproval", "secondApproval", "thirdApproval", "fourthApproval"};
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
List<ProcessTask> tasks = condolenceService.dao().query(ProcessTask.class,
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
log.info("[职工慰问导出] 查询审批记录,applyId={}, instanceId={}, taskCount={}, expectedTaskKeys={}", instance.getBusinessNo(), instance.getId(), tasks.size(), List.of(taskKeys));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
log.info("[职工慰问导出] 审批记录,instanceId={}, taskId={}, taskKey={}, displayName={}, state={}, finishTime={}, isFirstTaskNode={}, matchedApproval={}", instance.getId(), task.getId(), task.getTaskName(), task.getDisplayName(), task.getTaskState(), task.getFinishTime(), taskVO.getExt() == null ? null : taskVO.getExt().get("isFirstTaskNode"), List.of(taskKeys).contains(task.getTaskName()));
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
log.info("[职工慰问导出] 申请节点重置本轮审批字段,instanceId={}, taskId={}, taskKey={}", instance.getId(), task.getId(), task.getTaskName());
// 重新申请后,仅导出本轮审批记录。
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
for (int i = 0; i < taskKeys.length; i++) {
if (!taskKeys[i].equals(task.getTaskName())) continue;
for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
log.info("[职工慰问导出] 跳过未完成审批,instanceId={}, taskId={}, field={}, state={}", instance.getId(), task.getId(), fields[i], task.getTaskState());
continue;
}
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
log.info("[职工慰问导出] 读取审批表单,instanceId={}, taskId={}, field={}, formKeys={}, signPresent={}, signReferenceLength={}", instance.getId(), task.getId(), fields[i], data.keySet(), StrUtil.isNotBlank(data.getStr("sign")), StrUtil.nullToEmpty(data.getStr("sign")).length());
if (StrUtil.isBlank(data.getStr("sign"))) log.warn("[职工慰问导出] 已完成审批缺少签名字段,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i]);
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
if (StrUtil.isNotBlank(data.getStr("sign"))) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
if (signature == null) throw new BaseException("审批签字图片读取失败");
docData.put(fields[i] + "Sign", signature);
log.info("[职工慰问导出] 审批签名填充完成,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i] + "Sign");
}
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
docData.put(fields[i] + "Day", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "d"));
}
}
}
@At @At
@ApiOperation("删除") @ApiOperation("删除")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@@ -6,14 +6,13 @@ import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm; import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.param.SysMsgSummaryPageForm; import com.budwk.app.sys.param.SysMsgSummaryPageForm;
import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -94,10 +93,12 @@ public class CondolenceReadController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("info.type", "=", type); cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year); cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId); cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId); cnd.andEX("info.applyUnitId", "=", unitId);
applyReadScope(cnd, "info");
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
@@ -106,9 +107,6 @@ public class CondolenceReadController {
cnd.and(seg); cnd.and(seg);
} }
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("info.applyUnionId", "=", SecurityUtil.getUnionId());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime"); cnd.desc("info.createTime");
@@ -142,20 +140,18 @@ public class CondolenceReadController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and(ProcessInstance::getState, "=", ProcessTaskStateEnum.FINISHED.getCode()); cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("con.type", "=", type); cnd.andEX("con.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year); cnd.andEX("YEAR(con.createTime)", "=", year);
cnd.andEX("con.applyUnionId", "=", unionId); cnd.andEX("con.applyUnionId", "=", unionId);
cnd.andEX("con.applyUnitId", "=", unitId); cnd.andEX("con.applyUnitId", "=", unitId);
applyReadScope(cnd, "con");
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("info.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or("con.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("info.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or("con.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg); cnd.and(seg);
} }
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("con.applyUnionId", "=", SecurityUtil.getUnionId());
}
sql.setCondition(cnd); sql.setCondition(cnd);
List<NutMap> list = condolenceService.listMap(sql); List<NutMap> list = condolenceService.listMap(sql);
@@ -174,4 +170,21 @@ public class CondolenceReadController {
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list); Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("申报人汇总.xlsx", workbook, response); CommonDownloadUtil.download("申报人汇总.xlsx", workbook, response);
} }
private void applyReadScope(Cnd cnd, String alias) {
if (AuthUtil.hasPermission("all_school")) {
return;
}
if (AuthUtil.hasPermission("branch_union")) {
String unionId = SecurityUtil.getUnionId();
if (StrUtil.isNotBlank(unionId)) {
cnd.and(alias + ".applyUnionId", "=", unionId);
return;
}
} else if (AuthUtil.hasPermission("self")) {
cnd.and(alias + ".createdBy", "=", SecurityUtil.getUserId());
return;
}
cnd.and("1", "=", 0);
}
} }
@@ -17,6 +17,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -33,26 +34,26 @@ import java.util.List;
*/ */
@IocBean @IocBean
@At("/platform/condolence/schoolPrincipalApproval") @At("/platform/condolence/schoolPrincipalApproval")
@Api("职工慰问校工会预审") @Api("职工慰问二级审核")
@Ok("json:full") @Ok("json:full")
public class CondolenceSchoolPrincipalApprovalController { public class CondolenceSecondApprovalController {
@Inject @Inject
private CondolenceService condolenceService; private CondolenceService condolenceService;
@At("/") @At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/condolence/schoolPrincipalApproval/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/condolence/schoolPrincipalApproval/index.html")
@SaCheckPermission("condolence.schoolPrincipalApproval") @SaCheckPermission("condolence.secondApproval")
public void index() {} public void index() {}
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/condolence/schoolPrincipalApproval/index.html") @Ok("beetl:/platform/zhghh5/staffbenefit/condolence/schoolPrincipalApproval/index.html")
@SaCheckPermission("h5.condolence.schoolPrincipalApproval") @SaCheckPermission("h5.condolence.secondApproval")
public void h5Index() {} public void h5Index() {}
@At @At
@ApiOperation("分页查询") @ApiOperation("分页查询")
@SaCheckPermission(value = {"condolence.schoolPrincipalApproval", "h5.condolence.schoolPrincipalApproval"}, mode = SaMode.OR) @SaCheckPermission(value = {"condolence.secondApproval", "h5.condolence.secondApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm, public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year, @Param(value = "year") Integer year,
@Param(value = "type") String type, @Param(value = "type") String type,
@@ -90,6 +91,14 @@ public class CondolenceSchoolPrincipalApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
@@ -17,6 +17,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -27,7 +28,7 @@ import org.nutz.mvc.annotation.Param;
import java.util.List; import java.util.List;
/** /**
* @ClassName CondolenceSchoolUnionApprovalController * @ClassName CondolenceThirdApprovalController
* @Author JyuHsin * @Author JyuHsin
* @Date 2025/7/29 16:23 * @Date 2025/7/29 16:23
* @Version 1.0 * @Version 1.0
@@ -35,26 +36,26 @@ import java.util.List;
*/ */
@IocBean @IocBean
@At("/platform/condolence/schoolUnionApproval") @At("/platform/condolence/schoolUnionApproval")
@Api("职工慰问校工会审核") @Api("职工慰问三级审核")
@Ok("json:full") @Ok("json:full")
public class CondolenceSchoolUnionApprovalController { public class CondolenceThirdApprovalController {
@Inject @Inject
private CondolenceService condolenceService; private CondolenceService condolenceService;
@At("/") @At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/condolence/schoolUnionApproval/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/condolence/schoolUnionApproval/index.html")
@SaCheckPermission("condolence.schoolUnionApproval") @SaCheckPermission("condolence.thirdApproval")
public void index() {} public void index() {}
@At("/h5") @At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/condolence/schoolUnionApproval/index.html") @Ok("beetl:/platform/zhghh5/staffbenefit/condolence/schoolUnionApproval/index.html")
@SaCheckPermission("h5.condolence.schoolUnionApproval") @SaCheckPermission("h5.condolence.thirdApproval")
public void h5Index() {} public void h5Index() {}
@At @At
@ApiOperation("分页查询") @ApiOperation("分页查询")
@SaCheckPermission(value = {"condolence.schoolUnionApproval", "h5.condolence.schoolUnionApproval"}, mode = SaMode.OR) @SaCheckPermission(value = {"condolence.thirdApproval", "h5.condolence.thirdApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm, public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year, @Param(value = "year") Integer year,
@Param(value = "type") String type, @Param(value = "type") String type,
@@ -92,6 +93,14 @@ public class CondolenceSchoolUnionApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup(); SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); seg.or(Condolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
@@ -104,7 +113,9 @@ public class CondolenceSchoolUnionApprovalController {
cnd.andEX("info.applyUnitId", "=", unitId); cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "ecdaef3b-edf4-4143-9a96-887dd5921711"); cnd.and("t.taskName", "=", "ecdaef3b-edf4-4143-9a96-887dd5921711");
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); if (!"superadmin".equals(SecurityUtil.getUserLoginname())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (approval) { if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode())); cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
@@ -112,6 +112,7 @@ public class DifficultHelpApplyController {
@SaCheckPermission("member.apply.submit") @SaCheckPermission("member.apply.submit")
@SLog(tag = "困难补助申请", msg = "保存申请,填写人: ${args[0].proxyUserName}") @SLog(tag = "困难补助申请", msg = "保存申请,填写人: ${args[0].proxyUserName}")
public Result save(@Param("data") DifficultHelpInfo difficultHelpInfo) { public Result save(@Param("data") DifficultHelpInfo difficultHelpInfo) {
difficultHelpCommonService.validateApplyUser(difficultHelpInfo);
difficultHelpInfo.setApplyTime(ObjectUtil.defaultIfNull(difficultHelpInfo.getApplyTime(), new Date())); difficultHelpInfo.setApplyTime(ObjectUtil.defaultIfNull(difficultHelpInfo.getApplyTime(), new Date()));
difficultHelpCommonService.refreshApplyCount(difficultHelpInfo); difficultHelpCommonService.refreshApplyCount(difficultHelpInfo);
if (difficultHelpCommonService.hasAppliedInYear(difficultHelpInfo.getUserId(), difficultHelpInfo.getApplyTime(), difficultHelpInfo.getId())) { if (difficultHelpCommonService.hasAppliedInYear(difficultHelpInfo.getUserId(), difficultHelpInfo.getApplyTime(), difficultHelpInfo.getId())) {
@@ -128,6 +129,7 @@ public class DifficultHelpApplyController {
@SaCheckPermission("member.apply.submit") @SaCheckPermission("member.apply.submit")
@SLog(tag = "困难补助申请", msg = "提交申请,填写人: ${args[0].proxyUserName}") @SLog(tag = "困难补助申请", msg = "提交申请,填写人: ${args[0].proxyUserName}")
public Result submit(@Param("data") DifficultHelpInfo difficultHelpInfo){ public Result submit(@Param("data") DifficultHelpInfo difficultHelpInfo){
difficultHelpCommonService.validateApplyUser(difficultHelpInfo);
difficultHelpInfo.setApplyTime(new Date()); difficultHelpInfo.setApplyTime(new Date());
difficultHelpCommonService.refreshApplyCount(difficultHelpInfo); difficultHelpCommonService.refreshApplyCount(difficultHelpInfo);
if (difficultHelpCommonService.hasAppliedInYear(difficultHelpInfo.getUserId(), difficultHelpInfo.getApplyTime(), difficultHelpInfo.getId())) { if (difficultHelpCommonService.hasAppliedInYear(difficultHelpInfo.getUserId(), difficultHelpInfo.getApplyTime(), difficultHelpInfo.getId())) {
@@ -156,6 +158,7 @@ public class DifficultHelpApplyController {
@SaCheckPermission("member.apply.submit") @SaCheckPermission("member.apply.submit")
@SLog(tag = "困难补助申请", msg = "重新提交申请,填写人: ${args[0].proxyUserName}") @SLog(tag = "困难补助申请", msg = "重新提交申请,填写人: ${args[0].proxyUserName}")
public Result submitAgain(@Param("data") DifficultHelpInfo difficultHelpInfo, @Param("taskId") Long taskId) { public Result submitAgain(@Param("data") DifficultHelpInfo difficultHelpInfo, @Param("taskId") Long taskId) {
difficultHelpCommonService.validateApplyUser(difficultHelpInfo);
difficultHelpInfo.setApplyTime(ObjectUtil.defaultIfNull(difficultHelpInfo.getApplyTime(), new Date())); difficultHelpInfo.setApplyTime(ObjectUtil.defaultIfNull(difficultHelpInfo.getApplyTime(), new Date()));
difficultHelpCommonService.refreshApplyCount(difficultHelpInfo); difficultHelpCommonService.refreshApplyCount(difficultHelpInfo);
if (difficultHelpCommonService.hasAppliedInYear(difficultHelpInfo.getUserId(), difficultHelpInfo.getApplyTime(), difficultHelpInfo.getId())) { if (difficultHelpCommonService.hasAppliedInYear(difficultHelpInfo.getUserId(), difficultHelpInfo.getApplyTime(), difficultHelpInfo.getId())) {
@@ -176,6 +179,12 @@ public class DifficultHelpApplyController {
@ApiOperation("获取受助人") @ApiOperation("获取受助人")
@SaCheckPermission("difficultHelp.apply") @SaCheckPermission("difficultHelp.apply")
public Result queryRecipients(String key){ public Result queryRecipients(String key){
if (!AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
return Result.error("只有分工会主席可以替他人申请");
}
if (Strings.isBlank(SecurityUtil.getUnionId())) {
return Result.error("当前用户未设置所属分工会");
}
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
id, id,
@@ -194,13 +203,7 @@ public class DifficultHelpApplyController {
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and("id", "<>", SecurityUtil.getUserId()); cnd.and("id", "<>", SecurityUtil.getUserId());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { cnd.and("unionid", "=", SecurityUtil.getUnionId());
if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(), RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) {
cnd.and("unionid", "=", SecurityUtil.getUnionId());
} else {
cnd.and("id", "=", SecurityUtil.getUserId());
}
}
if (Strings.isNotBlank(key)) { if (Strings.isNotBlank(key)) {
SqlExpressionGroup group = new SqlExpressionGroup(); SqlExpressionGroup group = new SqlExpressionGroup();
@@ -0,0 +1,77 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
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.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@Ok("json")
@At("/platform/difficultHelp/approval")
public class DifficultHelpApprovalController {
@Inject
private Dao dao;
@Inject
private FlowCommonService flowCommonService;
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data);
if (args == null || args.getLong(FlowConst.PROCESS_TASK_ID_KEY) == null) {
throw new BaseException("审批任务不能为空");
}
ProcessTask task = dao.fetch(ProcessTask.class, args.getLong(FlowConst.PROCESS_TASK_ID_KEY));
if (task == null) throw new BaseException("审批任务不存在");
boolean amountApproval = "715ece82-4ae3-433c-ad18-ed875f45d1e3".equals(task.getTaskName());
if (!amountApproval && !"36ebafda-44c2-4bd3-bd7f-10839a575a0d".equals(task.getTaskName())
&& !"e405f96c-5fc0-4f63-b323-e69dfe853253".equals(task.getTaskName())) {
throw new BaseException("当前任务不是困难帮扶审批任务");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
DifficultHelpInfo info = instance == null ? null : dao.fetch(DifficultHelpInfo.class, instance.getBusinessNo());
if (info == null) throw new BaseException("困难帮扶申请不存在");
if (StrUtil.isBlank(args.getStr("tf_sign"))) throw new BaseException("请完成审批签字");
if (StrUtil.isBlank(args.getStr("tf_opinion"))) throw new BaseException("请填写审批意见");
args.remove("level");
args.remove("tf_level");
Double money = amountApproval ? args.getDouble("money") : null;
if (amountApproval) {
if (money == null || !Double.isFinite(money) || money < 0) {
throw new BaseException("请填写有效的帮扶金额");
}
args.put("tf_money", money);
} else {
args.remove("money");
args.remove("tf_money");
}
// 参与者权限由流程引擎校验,业务入口不接受客户端指定管理员代办。
args.remove(FlowConst.EXT_ENABLE_OPERATOR);
flowCommonService.executeTask(args);
if (amountApproval && Integer.valueOf(ProcessSubmitTypeEnum.AGREE.getCode()).equals(args.getInt(FlowConst.SUBMIT_TYPE))) {
dao.update(DifficultHelpInfo.class, Chain.make("money", money), Cnd.where("id", "=", info.getId()));
}
return Result.success();
}
}
@@ -2,10 +2,12 @@ package com.budwk.app.zhgh.staffbenefit.difficulthelp.controller;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService; import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
@@ -14,6 +16,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -25,15 +28,15 @@ import java.util.List;
/** /**
* @version 1.0 * @version 1.0
* @Author zzr * @Author zzr
* @name:DifficultHelpBranchUnionApprovalController * @name:DifficultHelpFirstApprovalController
* @Date 2025/7/31 11:47 * @Date 2025/7/31 11:47
* @注释 * @注释 困难帮扶一级审批
*/ */
@IocBean @IocBean
@Ok("json") @Ok("json")
@ApiOperation("困难补助分工会审核") @ApiOperation("困难帮扶一级审批")
@At("/platform/difficultHelp/branchUnionApproval") @At("/platform/difficultHelp/branchUnionApproval")
public class DifficultHelpBranchUnionApprovalController { public class DifficultHelpFirstApprovalController {
@Inject @Inject
private Dao dao; private Dao dao;
@@ -41,27 +44,27 @@ public class DifficultHelpBranchUnionApprovalController {
private DifficultHelpCommonService difficultHelpCommonService; private DifficultHelpCommonService difficultHelpCommonService;
@At("") @At("")
@SaCheckPermission("difficultHelp.branchUnionApproval") @SaCheckPermission("difficultHelp.firstApproval")
@Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/branchunionapproval/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/branchunionapproval/index.html")
public void index() { public void index() {
} }
@At @At
@SaCheckPermission("difficultHelp.branchUnionApproval") @SaCheckPermission("difficultHelp.firstApproval")
@Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/branchunionapproval/form.html") @Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/branchunionapproval/form.html")
public void form() { public void form() {
} }
@At @At
@SaCheckPermission("h5.difficultHelp.branchUnionApproval") @SaCheckPermission("h5.difficultHelp.firstApproval")
@Ok("beetl:/platform/zhghh5/staffbenefit/difficulthelp/branchunionapproval/index.html") @Ok("beetl:/platform/zhghh5/staffbenefit/difficulthelp/branchunionapproval/index.html")
public void h5() { public void h5() {
} }
@At @At
@ApiOperation("困难补助分工会审核列表") @ApiOperation("困难帮扶一级审批列表")
@SaCheckPermission("difficultHelp.branchUnionApproval") @SaCheckPermission("difficultHelp.firstApproval")
public Result pageData(DifficultHelpPageParam pageParam) { public Result pageData(DifficultHelpPageParam pageParam) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -102,10 +105,21 @@ public class DifficultHelpBranchUnionApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info."); cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
boolean schoolApproval = AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(), RoleConstant.SCHOOL_OFFICE_DIRECTOR.name());
pageParam.buildSearch(cnd, "info.", !schoolApproval);
cnd.and("t.taskName", "=", "9d11798e-a0e4-4ff5-939b-e6c5519e0eb6"); cnd.and("t.taskName", "=", "36ebafda-44c2-4bd3-bd7f-10839a575a0d");
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); if (!schoolApproval) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (pageParam.getAudit()) { if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode())); cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.controller; package com.budwk.app.zhgh.staffbenefit.difficulthelp.controller;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
@@ -68,6 +69,7 @@ public class DifficultHelpMineController {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
info.id, info.id,
info.createdBy,
info.proxyUserName, info.proxyUserName,
info.proxyLoginName, info.proxyLoginName,
info.userName, info.userName,
@@ -78,6 +80,7 @@ public class DifficultHelpMineController {
LEFT(info.applyTime, 10) AS applyTime, LEFT(info.applyTime, 10) AS applyTime,
info.applyCount, info.applyCount,
info.mobile, info.mobile,
info.applyMoney,
info.money, info.money,
ins.id AS instanceId, ins.id AS instanceId,
ins.businessNo, ins.businessNo,
@@ -103,7 +106,8 @@ public class DifficultHelpMineController {
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info."); pageParam.buildSearch(cnd, "info.");
cnd.and("info.createdBy", "=", SecurityUtil.getUserId()); cnd.and(Cnd.exps("info.createdBy", "=", SecurityUtil.getUserId())
.or("info.userId", "=", SecurityUtil.getUserId()));
if (StrUtil.isAllBlank(pageParam.getPageOrderName(), pageParam.getPageOrderBy())) { if (StrUtil.isAllBlank(pageParam.getPageOrderName(), pageParam.getPageOrderBy())) {
cnd.desc("info.applyTime"); cnd.desc("info.applyTime");
} else { } else {
@@ -121,6 +125,10 @@ public class DifficultHelpMineController {
@SaCheckPermission("difficultHelp.mine") @SaCheckPermission("difficultHelp.mine")
@SLog(tag = "困难补助申请", msg = "删除困难补助申请,id: ${args[0]}") @SLog(tag = "困难补助申请", msg = "删除困难补助申请,id: ${args[0]}")
public Result onDelete(@Valid String id){ public Result onDelete(@Valid String id){
DifficultHelpInfo helpInfo = dao.fetch(DifficultHelpInfo.class, id);
if (helpInfo == null || !SecurityUtil.getUserId().equals(helpInfo.getCreatedBy())) {
return Result.error("只能删除自己创建的申请记录");
}
dao.clear(DifficultHelpInfo.class, Cnd.where("id", "=", id)); dao.clear(DifficultHelpInfo.class, Cnd.where("id", "=", id));
// 删除流程相关操作 // 删除流程相关操作
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id); flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
@@ -130,7 +138,7 @@ public class DifficultHelpMineController {
@At @At
@ApiModelProperty("获取困难补助申请") @ApiModelProperty("获取困难补助申请")
@SaCheckPermission("difficultHelp.mine") @SaCheckPermission(value = {"difficultHelp.mine", "difficultHelp.reading"}, mode = SaMode.OR)
public Result findOne(String id) { public Result findOne(String id) {
DifficultHelpInfo helpInfo = dao.fetch(DifficultHelpInfo.class, id); DifficultHelpInfo helpInfo = dao.fetch(DifficultHelpInfo.class, id);
return Result.success().addData(helpInfo); return Result.success().addData(helpInfo);
@@ -9,7 +9,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
@@ -18,9 +17,12 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.MoneyUtil; import com.budwk.app.base.utils.MoneyUtil;
import com.budwk.app.base.utils.OfficePlusUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil; import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.models.Sys_union; import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysUserService; import com.budwk.app.sys.services.SysUserService;
@@ -35,7 +37,6 @@ import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommon
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.moneyImportVo;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm; import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure; import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.PictureRenderData; import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -70,8 +71,6 @@ import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.*;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@@ -155,7 +154,7 @@ public class DifficultHelpReadingController {
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", 20); cnd.and("ins.state", "=", 20);
pageParam.buildSearch(cnd, "info."); pageParam.buildSearch(cnd, "info.", false);
cnd.groupBy("info.id"); cnd.groupBy("info.id");
sql.setCondition(cnd); sql.setCondition(cnd);
Pagination<NutMap> pagination = difficultHelpCommonService.listPageMap(pageParam.getPageNumber(), pageParam.getPageSize(), sql); Pagination<NutMap> pagination = difficultHelpCommonService.listPageMap(pageParam.getPageNumber(), pageParam.getPageSize(), sql);
@@ -239,6 +238,7 @@ public class DifficultHelpReadingController {
@At @At
@Ok("void") @Ok("void")
@SaCheckPermission("difficultHelp.reading")
public void doExportAllApply(DifficultHelpPageParam pageParam, HttpServletResponse response) throws Exception { public void doExportAllApply(DifficultHelpPageParam pageParam, HttpServletResponse response) throws Exception {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -269,7 +269,7 @@ public class DifficultHelpReadingController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info."); pageParam.buildSearch(cnd, "info.", false);
cnd.groupBy("info.id"); cnd.groupBy("info.id");
sql.setCondition(cnd); sql.setCondition(cnd);
List<NutMap> list = difficultHelpCommonService.listMap(sql); List<NutMap> list = difficultHelpCommonService.listMap(sql);
@@ -315,9 +315,9 @@ public class DifficultHelpReadingController {
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder().bind("familyList", policy).build(); Configure config = Configure.builder().bind("familyList", policy).build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("difficulty_apply"), config) try (InputStream input = sysOfficeTemplateUtil.getTemplate("difficulty_apply")) {
.render(docData) zipOutputStream.write(sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:batch:" + record.getString("id")));
.write(zipOutputStream); }
zipOutputStream.closeEntry(); zipOutputStream.closeEntry();
} }
zipOutputStream.flush(); zipOutputStream.flush();
@@ -361,6 +361,9 @@ public class DifficultHelpReadingController {
.build(); .build();
String templateName= "difficulty_apply"; String templateName= "difficulty_apply";
docData.put("applyMoney", difficultHelp.getApplyMoney());
docData.put("money", difficultHelp.getMoney());
fillApplyApprovals(docData, difficultHelp.getId());
if (print) { if (print) {
exportAsPDF(templateName, docData, config, difficultHelp, response); exportAsPDF(templateName, docData, config, difficultHelp, response);
} else { } else {
@@ -368,49 +371,106 @@ public class DifficultHelpReadingController {
} }
} }
private void fillApplyApprovals(HashMap<String, Object> docData, String businessId) {
String[] taskKeys = {"36ebafda-44c2-4bd3-bd7f-10839a575a0d", "715ece82-4ae3-433c-ad18-ed875f45d1e3", "e405f96c-5fc0-4f63-b323-e69dfe853253"};
String[] fields = {"firstApproval", "secondApproval", "thirdApproval"};
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", businessId).desc("id"));
if (instance == null) return;
List<ProcessTask> tasks = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
// 重新申请后,仅导出本轮审批,避免混入上一次退回或撤销前的意见。
for (String field : fields) {
for (String suffix : List.of("", "Opinion", "User", "Date", "Year", "Month", "Day", "Sign")) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
if (!Integer.valueOf(ProcessTaskStateEnum.FINISHED.getCode()).equals(task.getTaskState())) continue;
for (int i = 0; i < taskKeys.length; i++) {
if (!taskKeys[i].equals(task.getTaskName())) continue;
cn.hutool.core.lang.Dict data = taskVO.getTaskFormData();
String opinion = StrUtil.nullToEmpty(data.getStr("opinion"));
docData.put(fields[i] + "Opinion", opinion);
docData.put(fields[i] + "User", StrUtil.nullToEmpty(data.getStr("userName")));
if (StrUtil.isNotBlank(data.getStr("sign"))) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
if (signature == null) throw new IllegalStateException("审批签字图片读取失败");
docData.put(fields[i] + "Sign", signature);
}
docData.put(fields[i] + "Date", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy-MM-dd"));
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
docData.put(fields[i] + "Day", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "d"));
String approval = "审批意见:" + opinion;
approval += "\n审批人:" + StrUtil.nullToEmpty(data.getStr("userName"));
approval += "\n审批日期:" + (task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy-MM-dd"));
docData.put(fields[i], approval);
}
}
}
private InputStream getApplyTemplate(String templateName) throws Exception {
byte[] templateBytes;
try (InputStream input = sysOfficeTemplateUtil.getTemplate(templateName)) {
templateBytes = input.readAllBytes();
}
// 按审批栏限定通用日期占位符,同时将旧版 Word 模板转换为 DOCX。
try (ByteArrayInputStream input = new ByteArrayInputStream(templateBytes);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
com.aspose.words.Document document = new com.aspose.words.Document(input);
com.aspose.words.NodeCollection cells = document.getChildNodes(com.aspose.words.NodeType.CELL, true);
for (int index = 0; index < cells.getCount(); index++) {
com.aspose.words.Cell cell = (com.aspose.words.Cell) cells.get(index);
for (String field : List.of("firstApproval", "secondApproval", "thirdApproval")) {
if (!cell.getText().contains("{{" + field + "Opinion}}")) continue;
com.aspose.words.FindReplaceOptions options = new com.aspose.words.FindReplaceOptions();
cell.getRange().replace("{{year}}", "{{" + field + "Year}}", options);
cell.getRange().replace("{{month}}", "{{" + field + "Month}}", options);
cell.getRange().replace("{{day}}", "{{" + field + "Day}}", options);
if ("firstApproval".equals(field)) {
cell.getRange().replace("{{secondApprovalUser}}", "{{firstApprovalUser}}", options);
}
cell.getRange().replace("{{" + field + "User}}", "{{@" + field + "Sign}}", options);
}
}
document.save(output, com.aspose.words.SaveFormat.DOCX);
templateBytes = output.toByteArray();
}
return new ByteArrayInputStream(templateBytes);
}
@At @At
private void exportAsPDF(String templateName, HashMap<String, Object> docData,Configure config, private void exportAsPDF(String templateName, HashMap<String, Object> docData,Configure config,
DifficultHelpInfo difficultHelp, HttpServletResponse response) throws Exception { DifficultHelpInfo difficultHelp, HttpServletResponse response) throws Exception {
String fileName = "中国地质大学(武汉)困难慰问申请表_" + difficultHelp.getUserName() + "_" + String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf"; DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".pdf";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { try (InputStream input = getApplyTemplate(templateName)) {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateName),config) byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, true, "difficulthelp:" + difficultHelp.getId());
.render(docData) CommonDownloadUtil.download(fileName, bytes, response);
.writeAndClose(byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
File pdfFile = File.createTempFile("file_convert", ".pdf");
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
// 删除临时文件
FileUtil.del(sourceFile);
FileUtil.del(pdfFile.toPath());
} catch (IOException e) {
log.error("困难慰问申请表导出PDF失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
} }
} }
@At @At
private void exportAsWord(String templateName, HashMap<String, Object> docData,Configure config, private void exportAsWord(String templateName, HashMap<String, Object> docData,Configure config,
DifficultHelpInfo difficultHelp, HttpServletResponse response) throws Exception { DifficultHelpInfo difficultHelp, HttpServletResponse response) throws Exception {
String fileName = "中国地质大学(武汉)困难慰问申请表_" + difficultHelp.getUserName() + "_" + String fileName = FileUtil.mainName(sysOfficeTemplateUtil.getTemplateFileName(templateName)) + "_" + difficultHelp.getUserName() + "_" +
DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx"; DateUtil.format(difficultHelp.getApplyTime(), "yyyyMMdd") + ".docx";
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { try (InputStream input = getApplyTemplate(templateName)) {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateName),config) byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, docData, config, false, "difficulthelp:" + difficultHelp.getId());
.render(docData) CommonDownloadUtil.download(fileName, bytes, response);
.writeAndClose(byteArrayOutputStream);
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
} catch (IOException e) {
log.error("困难慰问申请表导出Word失败,id:{},错误信息:{}", difficultHelp.getId(), e.getMessage());
} }
} }
@At @At
@Ok("void") @Ok("void")
@SaCheckPermission("difficultHelp.reading")
public void exportSummary(DifficultHelpPageParam pageParam,String flag, public void exportSummary(DifficultHelpPageParam pageParam,String flag,
HttpServletResponse response) throws Exception { HttpServletResponse response) throws Exception {
NutMap result = new NutMap(); NutMap result = new NutMap();
@@ -440,7 +500,7 @@ public class DifficultHelpReadingController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info."); pageParam.buildSearch(cnd, "info.", false);
cnd.groupBy("info.id"); cnd.groupBy("info.id");
sql.setCondition(cnd); sql.setCondition(cnd);
List<NutMap> list = difficultHelpCommonService.listMap(sql); List<NutMap> list = difficultHelpCommonService.listMap(sql);
@@ -483,15 +543,13 @@ public class DifficultHelpReadingController {
throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage()); throw new RuntimeException("导出Excel汇总表失败:" + e.getMessage());
} }
} else if ("docx".equals(flag)) { } else if ("docx".equals(flag)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { try (InputStream input = sysOfficeTemplateUtil.getTemplate(resource)) {
Configure config = Configure.builder() Configure config = Configure.builder()
.bind("da", new LoopRowTableRenderPolicy()) .bind("da", new LoopRowTableRenderPolicy())
.build(); .build();
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(resource), config) byte[] bytes = sysOfficeTemplateUtil.renderTemplate(input, result, config, false, "difficulthelp:summary:" + resource);
.render(result) CommonDownloadUtil.download(fileName + ".docx", bytes, response);
.writeAndClose(out);
CommonDownloadUtil.download(fileName + ".docx", out.toByteArray(), response);
} }
} }
} }
@@ -2,10 +2,12 @@ package com.budwk.app.zhgh.staffbenefit.difficulthelp.controller;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService; import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
@@ -14,6 +16,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -26,15 +29,15 @@ import java.util.List;
/** /**
* @version 1.0 * @version 1.0
* @Author zzr * @Author zzr
* @name:DifficultHelpBranchUnionApprovalController * @name:DifficultHelpSecondApprovalController
* @Date 2025/7/31 11:47 * @Date 2025/7/31 11:47
* @注释 * @注释 困难帮扶二级审批
*/ */
@IocBean @IocBean
@Ok("json") @Ok("json")
@ApiOperation("困难补助二级党组织审核") @ApiOperation("困难帮扶二级审批")
@At("/platform/difficultHelp/unionLeaderApproval") @At("/platform/difficultHelp/unionLeaderApproval")
public class DifficultHelpBranchUnionLeaderApprovalController { public class DifficultHelpSecondApprovalController {
@Inject @Inject
private Dao dao; private Dao dao;
@@ -42,27 +45,27 @@ public class DifficultHelpBranchUnionLeaderApprovalController {
private DifficultHelpCommonService difficultHelpCommonService; private DifficultHelpCommonService difficultHelpCommonService;
@At("") @At("")
@SaCheckPermission("difficultHelp.unionLeaderApproval") @SaCheckPermission("difficultHelp.secondApproval")
@Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/unionLeaderApproval/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/unionLeaderApproval/index.html")
public void index() { public void index() {
} }
@At @At
@SaCheckPermission("difficultHelp.unionLeaderApproval") @SaCheckPermission("difficultHelp.secondApproval")
@Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/unionLeaderApproval/form.html") @Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/unionLeaderApproval/form.html")
public void form() { public void form() {
} }
@At @At
@SaCheckPermission("h5.difficultHelp.unionLeaderApproval") @SaCheckPermission("h5.difficultHelp.secondApproval")
@Ok("beetl:/platform/zhghh5/staffbenefit/difficulthelp/unionLeaderApproval/index.html") @Ok("beetl:/platform/zhghh5/staffbenefit/difficulthelp/unionLeaderApproval/index.html")
public void h5() { public void h5() {
} }
@At @At
@ApiOperation("困难补助分工会审核列表") @ApiOperation("困难帮扶二级审批列表")
@SaCheckPermission("difficultHelp.unionLeaderApproval") @SaCheckPermission("difficultHelp.secondApproval")
public Result pageData(DifficultHelpPageParam pageParam) { public Result pageData(DifficultHelpPageParam pageParam) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -103,6 +106,14 @@ public class DifficultHelpBranchUnionLeaderApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
// pageParam.buildSearchUnit(cnd, "info."); // pageParam.buildSearchUnit(cnd, "info.");
if (StrUtil.isNotBlank(pageParam.getSearchKeyword())) { if (StrUtil.isNotBlank(pageParam.getSearchKeyword())) {
@@ -114,8 +125,10 @@ public class DifficultHelpBranchUnionLeaderApprovalController {
cnd.andEX("info.unionId", "=", pageParam.getUnionId()); cnd.andEX("info.unionId", "=", pageParam.getUnionId());
cnd.andEX("info.unitId", "=", pageParam.getUnitId()); cnd.andEX("info.unitId", "=", pageParam.getUnitId());
cnd.and("t.taskName", "=", "36ebafda-44c2-4bd3-bd7f-10839a575a0d"); cnd.and("t.taskName", "=", "715ece82-4ae3-433c-ad18-ed875f45d1e3");
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(), RoleConstant.SCHOOL_UNION_CHAIRMAN.name())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (pageParam.getAudit()) { if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode())); cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
@@ -4,10 +4,12 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService; import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
@@ -17,6 +19,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -30,15 +33,15 @@ import java.util.List;
/** /**
* @version 1.0 * @version 1.0
* @Author zzr * @Author zzr
* @name:DifficultHelpSchoolUnionApprovalController * @name:DifficultHelpThirdApprovalController
* @Date 2025/7/31 11:47 * @Date 2025/7/31 11:47
* @注释 * @注释 困难帮扶三级审批
*/ */
@IocBean @IocBean
@Ok("json") @Ok("json")
@ApiOperation("困难补助校工会审核") @ApiOperation("困难帮扶三级审批")
@At("/platform/difficultHelp/schoolUnionApproval") @At("/platform/difficultHelp/schoolUnionApproval")
public class DifficultHelpSchoolUnionApprovalController { public class DifficultHelpThirdApprovalController {
@Inject @Inject
private Dao dao; private Dao dao;
@@ -46,26 +49,26 @@ public class DifficultHelpSchoolUnionApprovalController {
private DifficultHelpCommonService difficultHelpCommonService; private DifficultHelpCommonService difficultHelpCommonService;
@At("") @At("")
@SaCheckPermission("difficultHelp.schoolUnionApproval") @SaCheckPermission("difficultHelp.thirdApproval")
@Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/schoolunionapproval/index.html") @Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/schoolunionapproval/index.html")
public void index() { public void index() {
} }
@At @At
@SaCheckPermission("difficultHelp.schoolUnionApproval") @SaCheckPermission("difficultHelp.thirdApproval")
@Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/schoolunionapproval/form.html") @Ok("beetl:/platform/zhgh/staffbenefit/difficulthelp/schoolunionapproval/form.html")
public void form() { public void form() {
} }
@At @At
@SaCheckPermission("h5.difficultHelp.schoolUnionApproval") @SaCheckPermission("h5.difficultHelp.thirdApproval")
@Ok("beetl:/platform/zhghh5/staffbenefit/difficulthelp/schoolunionapproval/index.html") @Ok("beetl:/platform/zhghh5/staffbenefit/difficulthelp/schoolunionapproval/index.html")
public void h5() { public void h5() {
} }
@At @At
@ApiOperation("困难补助校工会审核列表") @ApiOperation("困难帮扶三级审批列表")
@SaCheckPermission("difficultHelp.schoolUnionApproval") @SaCheckPermission("difficultHelp.thirdApproval")
public Result pageData(DifficultHelpPageParam pageParam) { public Result pageData(DifficultHelpPageParam pageParam) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -106,10 +109,39 @@ public class DifficultHelpSchoolUnionApprovalController {
$condition $condition
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
pageParam.buildSearch(cnd, "info."); cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
pageParam.buildSearch(cnd, "info.", false);
cnd.and("t.taskName", "=", "715ece82-4ae3-433c-ad18-ed875f45d1e3"); cnd.and("t.taskName", "=", "e405f96c-5fc0-4f63-b323-e69dfe853253");
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
Sql unitSql = Sqls.queryString("""
SELECT DISTINCT ur.unitId
FROM sys_user_role ur
INNER JOIN sys_role r ON r.id = ur.roleId
WHERE ur.userId = @userId
AND ur.enable = true
AND r.disabled = false
AND r.code = @roleCode
AND ur.unitId IS NOT NULL
AND ur.unitId <> ''
""");
unitSql.setParam("userId", SecurityUtil.getUserId());
unitSql.setParam("roleCode", RoleConstant.UNIT_SCHOOL_LEADER.name());
dao.execute(unitSql);
List<String> managedUnitIds = unitSql.getList(String.class);
if (managedUnitIds.isEmpty()) {
cnd.and(new Static("1 = 0"));
} else {
cnd.and("info.unitId", "in", managedUnitIds);
}
}
if (pageParam.getAudit()) { if (pageParam.getAudit()) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode())); cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.WITHDRAW.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
@@ -131,11 +163,10 @@ public class DifficultHelpSchoolUnionApprovalController {
@At @At
@ApiOperation("保存补助金额、级别") @ApiOperation("保存补助金额、级别")
@SaCheckPermission("difficultHelp.schoolUnionApproval") @SaCheckPermission("difficultHelp.thirdApproval")
@SLog(tag = "困难补助申请", msg = "保存申请,填写人: ${args[0].proxyUserName}") @SLog(tag = "困难补助申请", msg = "保存申请,填写人: ${args[0].proxyUserName}")
public Result save(@Param("data") DifficultHelpInfo difficultHelpInfo) { public Result save(@Param("data") DifficultHelpInfo difficultHelpInfo) {
dao.updateIgnoreNull(difficultHelpInfo); return Result.error("三级审批不再设置级别和帮扶金额,请在一级、二级审批中填写");
return Result.success();
} }
} }
@@ -6,6 +6,7 @@ import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext; import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.service.DifficultHelpCommonService;
import org.nutz.dao.Chain; import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
@@ -29,6 +30,7 @@ public class DifficultHelpSaveInterceptor implements FlowInterceptor {
Dao dao = ServiceContext.find(Dao.class); Dao dao = ServiceContext.find(Dao.class);
DifficultHelpInfo difficultHelpInfo = Json.fromJson(DifficultHelpInfo.class, formDataStr); DifficultHelpInfo difficultHelpInfo = Json.fromJson(DifficultHelpInfo.class, formDataStr);
ServiceContext.find(DifficultHelpCommonService.class).validateApplyUser(difficultHelpInfo);
difficultHelpInfo.setApplyTime(new Date()); difficultHelpInfo.setApplyTime(new Date());
dao.insertOrUpdate(difficultHelpInfo); dao.insertOrUpdate(difficultHelpInfo);
@@ -194,6 +194,11 @@ public class DifficultHelpInfo extends BaseModel {
@ColDefine(type = ColType.INT) @ColDefine(type = ColType.INT)
private Integer applyCount; private Integer applyCount;
@Column
@Comment("申请金额")
@ColDefine(type = ColType.VARCHAR, width = 200)
private Double applyMoney;
@Column @Column
@Comment("补助金额") @Comment("补助金额")
@ColDefine(type = ColType.VARCHAR, width = 200) @ColDefine(type = ColType.VARCHAR, width = 200)
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffbenefit.difficulthelp.service; package com.budwk.app.zhgh.staffbenefit.difficulthelp.service;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo; import com.budwk.app.zhgh.staffbenefit.difficulthelp.models.DifficultHelpInfo;
import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam; import com.budwk.app.zhgh.staffbenefit.difficulthelp.vo.DifficultHelpPageParam;
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm; import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.aidFund.param.AidFundPageForm;
@@ -19,6 +20,16 @@ import java.util.List;
*/ */
public interface DifficultHelpCommonService extends BaseService<DifficultHelpInfo> { public interface DifficultHelpCommonService extends BaseService<DifficultHelpInfo> {
void validateApplyUser(DifficultHelpInfo difficultHelpInfo);
/**
* 困难帮扶进入校工会办公室主任审批时,通知本次任务的实际参与人。
* @param task WF 已创建且参与人已落库的任务;仅 KNBF 办公室审批待办触发,
* 接收人使用任务参与人的 actorAccount(系统 loginname)
* @return 无返回值;开关关闭或非目标节点不发送,发送失败记录日志而不阻断流程
*/
void notifyOfficeApproval(ProcessTask task);
/** /**
* 导入实际金额数据处理 * 导入实际金额数据处理
* @param file 文件 * @param file 文件
@@ -4,6 +4,16 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.wxwork.WxWorkMessageService;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.base.utils.ManyAddOrRenewUtil; import com.budwk.app.base.utils.ManyAddOrRenewUtil;
import com.budwk.app.base.utils.easyexcel.EasyExcelUtil; import com.budwk.app.base.utils.easyexcel.EasyExcelUtil;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
@@ -27,6 +37,8 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.random.R; import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.upload.TempFile; import org.nutz.mvc.upload.TempFile;
import java.util.*; import java.util.*;
@@ -42,6 +54,8 @@ import java.util.stream.IntStream;
*/ */
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService { public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHelpInfo> implements DifficultHelpCommonService {
private static final Log log = Logs.get();
private static final String OFFICE_APPROVAL_TASK = "36ebafda-44c2-4bd3-bd7f-10839a575a0d";
public DifficultHelpCommonServiceImpl(Dao dao) { public DifficultHelpCommonServiceImpl(Dao dao) {
super(dao); super(dao);
@@ -49,6 +63,83 @@ public class DifficultHelpCommonServiceImpl extends BaseServiceImpl<DifficultHel
@Inject @Inject
private ManyAddOrRenewUtil manyAddOrRenewUtil; private ManyAddOrRenewUtil manyAddOrRenewUtil;
@Inject
private WxWorkMessageService wxWorkMessageService;
/**
* 只通知 KNBF 办公室待办的实际审核人;重新提交生成的新任务沿用同一入口。
* task 为任务开始事件对应的 WF 任务,actorAccount 为学校企业微信 userid。
* 无返回值;服务返回 skipped 表示开关关闭,success=false 表示接口失败或部分接收人无效。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void notifyOfficeApproval(ProcessTask task) {
if (task == null || task.getId() == null || task.getProcessInstanceId() == null
|| !OFFICE_APPROVAL_TASK.equals(task.getTaskName())
|| !ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) {
return;
}
try {
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || instance.getProcessDefineId() == null) return;
ProcessDefine define = dao().fetch(ProcessDefine.class, instance.getProcessDefineId());
// 使用实例所属定义核验流程 KEY,避免其他流程复用同一个节点 ID 时误发。
if (define == null || !"KNBF".equals(define.getName())) return;
DifficultHelpInfo info = StrUtil.isBlank(instance.getBusinessNo()) ? null : fetch(instance.getBusinessNo());
if (info == null) {
log.warnf("困难帮扶企业微信通知跳过:申请不存在,taskId=%s", task.getId());
return;
}
List<ProcessTaskActor> actors = dao().query(ProcessTaskActor.class,
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
List<String> loginNames = actors.stream().map(ProcessTaskActor::getActorAccount)
.filter(StrUtil::isNotBlank).map(String::trim).distinct().toList();
// 不按角色扩大接收范围,未绑定工号时不以内部用户 ID 代替企业微信 userid。
if (loginNames.isEmpty()) {
log.warnf("困难帮扶企业微信通知跳过:审核人 loginname 为空,taskId=%s", task.getId());
return;
}
String content = "您有一条困难帮扶申请待审核,受助人:"
+ StrUtil.blankToDefault(info.getUserName(), "未填写") + ",请进入校工会办公室审批页面办理。";
// 流程定义中仍保留旧的 unionLeaderApproval 链接,本节点实际办理入口为 branchUnionApproval。
NutMap result = wxWorkMessageService.massSend(loginNames, "困难帮扶待办提醒", content,
"/platform/difficultHelp/branchUnionApproval/h5");
if (!result.getBoolean("skipped", false) && !result.getBoolean("success", false)) {
log.warnf("困难帮扶企业微信通知发送失败或部分失败,taskId=%s,result=%s", task.getId(), result);
}
} catch (Exception e) {
// 外部消息不改变审批结果;日志保留任务 ID,便于定位接口、账号或配置问题。
log.error("困难帮扶企业微信通知异常,taskId=" + task.getId(), e);
}
}
@Override
public void validateApplyUser(DifficultHelpInfo difficultHelpInfo) {
if (difficultHelpInfo == null || StrUtil.isBlank(difficultHelpInfo.getUserId())) {
throw new BaseException("请选择受助人");
}
if (StrUtil.isNotBlank(difficultHelpInfo.getId())) {
DifficultHelpInfo dbInfo = dao().fetch(DifficultHelpInfo.class, difficultHelpInfo.getId());
if (dbInfo != null && !SecurityUtil.getUserId().equals(dbInfo.getCreatedBy())) {
throw new BaseException("只能查看他人代为申请的记录,不能修改或提交");
}
}
if (difficultHelpInfo.getApplyMoney() == null || !Double.isFinite(difficultHelpInfo.getApplyMoney())
|| difficultHelpInfo.getApplyMoney() <= 0) {
throw new BaseException("请填写大于零的申请金额");
}
if (difficultHelpInfo.getUserId().equals(SecurityUtil.getUserId())) {
return;
}
if (!AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
throw new BaseException("只有分工会主席可以替他人申请");
}
View_user recipient = dao().fetch(View_user.class, Cnd.where("id", "=", difficultHelpInfo.getUserId()));
if (recipient == null || StrUtil.isBlank(SecurityUtil.getUnionId())
|| !SecurityUtil.getUnionId().equals(recipient.getUnionId())) {
throw new BaseException("只能替本分工会人员申请");
}
}
@Override @Override
public boolean hasAppliedInYear(String userId, Date applyTime, String excludeRecordId) { public boolean hasAppliedInYear(String userId, Date applyTime, String excludeRecordId) {
@@ -33,6 +33,10 @@ public class DifficultHelpPageParam extends PageForm {
private List<String> dateRange; private List<String> dateRange;
public void buildSearch(Cnd cnd, String prefix){ public void buildSearch(Cnd cnd, String prefix){
buildSearch(cnd, prefix, true);
}
public void buildSearch(Cnd cnd, String prefix, boolean restrictDataScope){
if (cnd == null) { if (cnd == null) {
cnd = Cnd.NEW(); cnd = Cnd.NEW();
} }
@@ -46,7 +50,7 @@ public class DifficultHelpPageParam extends PageForm {
cnd.andEX("YEAR(" + prefix + "applyTime)", "=", this.getYear()); cnd.andEX("YEAR(" + prefix + "applyTime)", "=", this.getYear());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if (restrictDataScope && !AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(), RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) { if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(), RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) {
cnd.and(prefix + "unionid", "=", SecurityUtil.getUnionId()); cnd.and(prefix + "unionid", "=", SecurityUtil.getUnionId());
} else { } else {
@@ -0,0 +1,252 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.suggestionBox.models.SuggestionBox;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
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;
import java.util.List;
/**
* @ClassName SpecialCondolenceApplyController
* @Author JyuHsin
* @Date 2025/8/20 15:40
* @Version 1.0
* @Description TODO
*/
@SaCheckRole("BRANCH_UNION_CHAIRMAN")
@IocBean
@At("/platform/special_condolence/apply")
@Api("特殊慰问申请")
@Ok("json:full")
public class SpecialCondolenceApplyController {
@Inject
private Dao dao;
@Inject
private FlowEngine flowEngine;
@Inject
private SpecialCondolenceService specialcondolenceService;
@Inject
private FlowCommonService flowCommonService;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/apply/index.html")
@SaCheckPermission("special_condolence.apply")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/apply/index.html")
@SaCheckPermission("h5.special_condolence.apply")
public void h5Index() {}
@At
@ApiOperation("保存申请")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
@SLog(tag = "特殊慰问系统-慰问申请", msg = "保存申请")
public Result save(@Param("data") SpecialCondolence specialcondolence) {
if(StrUtil.isBlank(specialcondolence.getId())) specialcondolence.setCreateTime(DateUtil.now());
prepareApplication(specialcondolence);
specialcondolenceService.saveDraftApplication(specialcondolence);
return Result.success();
}
@At
@ApiOperation("提交申请")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
@SLog(tag = "特殊慰问系统-慰问申请", msg = "提交申请")
@Aop(TransAop.READ_COMMITTED)
public Result submit(@Param("data") SpecialCondolence specialcondolence) {
if (StrUtil.isBlank(specialcondolence.getRemark()) || specialcondolence.getRemark().trim().length() > 500) throw new com.budwk.app.base.exception.BaseException("请填写500字以内的申请事由");
if (StrUtil.isBlank(specialcondolence.getSignature())) throw new com.budwk.app.base.exception.BaseException("请完成申请人签字");
if(StrUtil.isBlank(specialcondolence.getId())) specialcondolence.setCreateTime(DateUtil.now());
prepareApplication(specialcondolence);
specialcondolenceService.saveApplication(specialcondolence);
// 开启流程实例
Dict args = Dict.create();
args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode());
args.set(FlowConst.FORM_DATA, specialcondolence);
ProcessInstance instance = flowEngine.startProcessInstanceByKey("TSWW", specialcondolence.getId(), SecurityUtil.getUserId(), args);
// 自动完成第一个申请任务
List<ProcessTask> doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null);
for (ProcessTask task : doingTaskList) {
flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args);
}
return Result.success();
}
@At
@ApiOperation("重新提交申请")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
public Result submitAgain(@Param("data") SpecialCondolence specialcondolence, @Param("taskId") Long taskId) {
if (StrUtil.isBlank(specialcondolence.getRemark()) || specialcondolence.getRemark().trim().length() > 500) throw new com.budwk.app.base.exception.BaseException("请填写500字以内的申请事由");
if (StrUtil.isBlank(specialcondolence.getSignature())) throw new com.budwk.app.base.exception.BaseException("请完成申请人签字");
ProcessTask task = taskId == null ? null : dao.fetch(ProcessTask.class, taskId);
ProcessInstance instance = task == null ? null : dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || !StrUtil.equals(instance.getBusinessNo(), specialcondolence.getId())
|| !"startTask".equals(task.getTaskName()) || !SecurityUtil.getUserId().equals(instance.getOperator())) {
throw new com.budwk.app.base.exception.BaseException("申请任务不匹配");
}
prepareApplication(specialcondolence);
specialcondolenceService.saveApplication(specialcondolence);
Dict dict = Dict.create();
dict.set(FlowConst.PROCESS_TASK_ID_KEY, taskId);
dict.set(FlowConst.FORM_DATA, specialcondolence);
dict.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.RE_APPLY.getCode());
flowCommonService.executeTask(dict);
return Result.success();
}
@At
@ApiOperation("查询用户")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
public Object listUser(String keyword) {
Sql sql = Sqls.create("""
select
u.id,
u.idCard,
u.personType,
bank.bankOfDeposit,
bank.bankUserName,
bank.bankCardNumber,
username as userName,
loginname as loginName,
sex,
mobile,
technicalTitle,
IFNULL(unitname, '暂无') as unitName,
unitid as unitId,
unionid as unionId,
unionname as unionName
from
vw_user u
LEFT JOIN (
SELECT idCard, bankOfDeposit, bankUserName, bankCardNumber,
ROW_NUMBER() OVER (PARTITION BY idCard ORDER BY createdAt DESC, id DESC) rn
FROM special_condolence_recipient WHERE delFlag = false
) bank ON bank.idCard = u.idCard AND bank.rn = 1 AND u.idCard <> ''
$condition
""");
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(View_user::getLoginname, "like", "%" + keyword + "%");
seg.or(View_user::getUsername, "like", "%" + keyword + "%");
cnd.and(seg);
}
if (StrUtil.isBlank(SecurityUtil.getUnionId())) throw new com.budwk.app.base.exception.BaseException("申请人未关联分工会");
cnd.and("u.unionId", "=", SecurityUtil.getUnionId());
sql.setCondition(cnd);
Pagination pagination = specialcondolenceService.listPageMap(1, 50, sql);
return Result.success(pagination.getList());
}
@At
@Ok("void")
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
public void importTemplate(javax.servlet.http.HttpServletResponse response) throws Exception {
String templateCode = "special_condolence_import";
try (java.io.InputStream input = sysOfficeTemplateUtil.getTemplate(templateCode)) {
com.budwk.app.base.utils.CommonDownloadUtil.download(
sysOfficeTemplateUtil.getTemplateFileName(templateCode), input.readAllBytes(), response);
}
}
@At
@SaCheckPermission(value = {"special_condolence.apply", "h5.special_condolence.apply"}, mode = SaMode.OR)
@org.nutz.mvc.annotation.AdaptBy(type = org.nutz.mvc.upload.UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result importRecipients(@Param("file") org.nutz.mvc.upload.TempFile file) throws Exception {
if (file == null) throw new com.budwk.app.base.exception.BaseException("请选择Excel文件");
if (!file.getSubmittedFileName().toLowerCase(java.util.Locale.ROOT).matches(".*\\.(xlsx|xls)$")) throw new com.budwk.app.base.exception.BaseException("仅支持xlsx、xls文件");
String[] headers = {"慰问对象", "工号", "身份证号码", "开户行", "户名", "收款账户"};
String[] fields = {"helpUserName", "helpLoginName", "idCard", "bankOfDeposit", "bankUserName", "bankCardNumber"};
java.util.List<cn.hutool.json.JSONObject> result = new java.util.ArrayList<>();
try (java.io.InputStream input = new java.io.FileInputStream(file.getFile());
org.apache.poi.ss.usermodel.Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(input)) {
if (workbook.getNumberOfSheets() == 0) throw new com.budwk.app.base.exception.BaseException("Excel中没有工作表");
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
org.apache.poi.ss.usermodel.DataFormatter formatter = new org.apache.poi.ss.usermodel.DataFormatter();
org.apache.poi.ss.usermodel.Row header = sheet.getRow(0);
for (int c = 0; c < headers.length; c++) {
if (header == null || !headers[c].equals(formatter.formatCellValue(header.getCell(c)).trim())) throw new com.budwk.app.base.exception.BaseException("表头不正确,请下载导入模板");
}
if (sheet.getLastRowNum() > 1000) throw new com.budwk.app.base.exception.BaseException("最多导入1000行");
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
org.apache.poi.ss.usermodel.Row excelRow = sheet.getRow(r);
if (excelRow == null) continue;
String prefix = "慰问对象" + (result.size() + 1) + " ";
cn.hutool.json.JSONObject row = new cn.hutool.json.JSONObject();
boolean empty = true;
for (int c = 0; c < fields.length; c++) {
org.apache.poi.ss.usermodel.Cell cell = excelRow.getCell(c);
String value = formatter.formatCellValue(cell).trim();
if (c != 1 && cell != null && cell.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) throw new com.budwk.app.base.exception.BaseException(prefix + "不支持公式,请填写实际值");
if ((c == 2 || c == 5) && cell != null && cell.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC) throw new com.budwk.app.base.exception.BaseException(prefix + "工号、身份证和收款账户请使用文本格式,避免数字失真");
row.set(fields[c], value);
empty = empty && value.isEmpty();
}
if (empty) continue;
if (!com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceRecipients.validIdCard(row.getStr("idCard"))) throw new com.budwk.app.base.exception.BaseException(prefix + "身份证号码格式不正确");
if (!row.getStr("bankCardNumber").matches("[0-9]{6,30}")) throw new com.budwk.app.base.exception.BaseException(prefix + "收款账户格式不正确");
result.add(row);
}
}
if (result.isEmpty()) throw new com.budwk.app.base.exception.BaseException("没有可导入的数据");
return Result.success(result);
}
private void prepareApplication(SpecialCondolence info) {
if (StrUtil.isNotBlank(info.getId())) {
SpecialCondolence existing = dao.fetch(SpecialCondolence.class, info.getId());
if (existing == null || !SecurityUtil.getUserId().equals(existing.getCreatedBy())) {
throw new com.budwk.app.base.exception.BaseException("只能修改本人申请");
}
info.setCreatedBy(existing.getCreatedBy());
}
info.setApplyUserId(SecurityUtil.getUserId());
info.setApplyUserName(SecurityUtil.getUserUsername());
info.setApplyLoginName(SecurityUtil.getUserLoginname());
info.setApplyUnionId(SecurityUtil.getUnionId());
info.setApplyUnitId(SecurityUtil.getUnitId());
}
}
@@ -0,0 +1,60 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@Ok("json")
@At("/platform/special_condolence/approval")
public class SpecialCondolenceApprovalController {
@Inject
private Dao dao;
@Inject
private FlowCommonService flowCommonService;
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data);
if (args == null || args.getLong(FlowConst.PROCESS_TASK_ID_KEY) == null) {
throw new BaseException("审批任务不能为空");
}
ProcessTask task = dao.fetch(ProcessTask.class, args.getLong(FlowConst.PROCESS_TASK_ID_KEY));
if (task == null) throw new BaseException("审批任务不存在");
if (!"c438972e-2f67-4907-b229-cb7aa17da147".equals(task.getTaskName())
&& !"7f560675-aeea-4254-a57e-957d03d6b786".equals(task.getTaskName())
&& !"af363a4e-fc56-42a0-9906-8c7a2814006b".equals(task.getTaskName())) {
throw new BaseException("当前任务不是特殊慰问审批任务");
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
com.budwk.app.flow.entity.ProcessDefine define = instance == null ? null : dao.fetch(com.budwk.app.flow.entity.ProcessDefine.class, instance.getProcessDefineId());
SpecialCondolence info = define == null || !"TSWW".equals(define.getName()) ? null : dao.fetch(SpecialCondolence.class, instance.getBusinessNo());
if (info == null) throw new BaseException("特殊慰问申请不存在");
if (StrUtil.isBlank(args.getStr("tf_sign"))) throw new BaseException("请完成审批签字");
if (StrUtil.isBlank(args.getStr("tf_opinion"))) throw new BaseException("请填写审批意见");
// 参与者权限由流程引擎校验,业务入口不接受客户端指定管理员代办。
args.remove(FlowConst.EXT_ENABLE_OPERATOR);
flowCommonService.executeTask(args);
return Result.success();
}
}
@@ -0,0 +1,168 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
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.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.dao.util.cri.Static;
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.List;
/**
* @ClassName SpecialCondolenceFourthApprovalController
* @Author JyuHsin
* @Date 2025/7/29 16:23
* @Version 1.0
* @Description TODO
*/
@IocBean
@At("/platform/special_condolence/fourthApproval")
@Api("特殊慰问四级审核")
@Ok("json:full")
public class SpecialCondolenceFourthApprovalController {
@Inject
private Dao dao;
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/fourthApproval/index.html")
@SaCheckPermission("special_condolence.fourthApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/fourthApproval/index.html")
@SaCheckPermission("h5.special_condolence.fourthApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.fourthApproval", "h5.special_condolence.fourthApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN special_condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.id", "is not", null);
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "af363a4e-fc56-42a0-9906-8c7a2814006b");
if (!"superadmin".equals(SecurityUtil.getUserLoginname()) && !AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
Sql unitSql = Sqls.queryString("""
SELECT DISTINCT ur.unitId
FROM sys_user_role ur
INNER JOIN sys_role r ON r.id = ur.roleId
WHERE ur.userId = @userId
AND ur.enable = true
AND r.disabled = false
AND r.code = @roleCode
AND ur.unitId IS NOT NULL
AND ur.unitId <> ''
""");
unitSql.setParam("userId", SecurityUtil.getUserId());
unitSql.setParam("roleCode", RoleConstant.UNIT_SCHOOL_LEADER.name());
dao.execute(unitSql);
List<String> managedUnitIds = unitSql.getList(String.class);
if (managedUnitIds.isEmpty()) {
cnd.and(new Static("1 = 0"));
} else {
cnd.and("info.applyUnitId", "in", managedUnitIds);
}
}
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -0,0 +1,303 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
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.result.Result;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import com.deepoove.poi.data.PictureRenderData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
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.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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.InputStream;
import java.util.List;
/**
* @ClassName SpecialCondolenceApplyController
* @Author JyuHsin
* @Date 2025/7/28 11:06
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@At("/platform/special_condolence/mine")
@Api("特殊慰问我的")
@Ok("json:full")
public class SpecialCondolenceMineController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@Inject
private FlowEngine flowEngine;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/mine/index.html")
@SaCheckPermission("special_condolence.mine")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/mine/index.html")
@SaCheckPermission("h5.special_condolence.mine")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.mine", "h5.special_condolence.mine"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = t.taskParentId) = 'startTask', 1, 0) canRevoke,
(select MAX(id) from wf_process_task where processInstanceId = ins.id and taskName = 'startTask') AS startTaskId
FROM
special_condolence info
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT
id,
processInstanceId,
taskName,
displayName,
taskType,
performType,
taskState,
finishTime,
taskParentId,
variable,
ROW_NUMBER() OVER (PARTITION BY processInstanceId ORDER BY createdAt DESC, id DESC) AS rn
FROM wf_process_task
WHERE taskState = 10
) t ON t.processInstanceId = ins.id AND t.rn = 1
$condition
""");
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("year(info.createTime)", "=", year);
cnd.andEX("info.type", "=", type);
cnd.and("info.createdBy", "=", SecurityUtil.getUserId());
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("info.createTime");
}
sql.setCondition(cnd);
Pagination<NutMap> pagination = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@ApiOperation("导出特殊慰问申请表")
@SaCheckPermission("special_condolence.mine")
public void doExportApply(String id, HttpServletResponse response, Boolean print) throws Exception {
log.info("[特殊慰问导出] 开始,applyId={}, userId={}, format={}", id, SecurityUtil.getUserId(), Boolean.TRUE.equals(print) ? "PDF" : "Word");
SpecialCondolence specialcondolence = specialcondolenceService.loadApplication(id);
if (specialcondolence == null) throw new BaseException("该条记录不存在");
if (!SecurityUtil.getUserId().equals(specialcondolence.getCreatedBy())) {
throw new BaseException("只能导出本人提交的申请");
}
ProcessInstance instance = specialcondolenceService.dao().fetch(ProcessInstance.class,
Cnd.where("businessNo", "=", id).desc("id"));
if (instance == null || !ProcessInstanceStateEnum.FINISHED.getCode().equals(instance.getState())) {
throw new BaseException("只能导出已完成的申请");
}
log.info("[特殊慰问导出] 流程已完成,applyId={}, instanceId={}, state={}, applicantSignPresent={}", id, instance.getId(), instance.getState(), StrUtil.isNotBlank(specialcondolence.getSignature()));
NutMap docData = Lang.obj2nutmap(specialcondolence);
docData.put("payUserName", StrUtil.nullToEmpty(specialcondolence.getBankUserName()));
SpecialCondolenceType type = specialcondolenceService.dao().fetch(SpecialCondolenceType.class, specialcondolence.getType());
docData.put("typeName", type == null ? "" : type.getName());
docData.put("signature", null);
if (StrUtil.isNotBlank(specialcondolence.getSignature())) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, specialcondolence.getSignature());
if (signature == null) throw new BaseException("申请人签字图片读取失败");
docData.put("signature", signature);
}
fillApplyApprovals(docData, instance);
for (String field : List.of("signature", "secondApprovalSign", "thirdApprovalSign", "fourthApprovalSign")) {
Object value = docData.get(field);
log.info("[特殊慰问导出] 渲染前图片字段,applyId={}, instanceId={}, field={}, valueType={}", id, instance.getId(), field, value == null ? "null" : value.getClass().getName());
}
String fileName = "重庆工贸职业技术学院特殊慰问申请表_" + StrUtil.nullToEmpty(specialcondolence.getHelpUserName());
try (InputStream input = getClass().getResourceAsStream("/templates/condolence/重庆工贸职业技术学院职工慰问申请表.doc")) {
if (input == null) throw new BaseException("特殊慰问申请表模板不存在");
boolean pdf = Boolean.TRUE.equals(print);
com.aspose.words.Document template = new com.aspose.words.Document(input);
template.getRange().replace("职工慰问", "特殊慰问", new com.aspose.words.FindReplaceOptions());
com.aspose.words.NodeCollection rows = template.getChildNodes(com.aspose.words.NodeType.ROW, true);
for (int i = rows.getCount() - 1; i >= 0; i--) {
com.aspose.words.Node row = rows.get(i);
if (row.getText().contains("firstApproval") || row.getText().contains("分工会主席")) row.remove();
}
java.io.ByteArrayOutputStream templateOutput = new java.io.ByteArrayOutputStream();
template.save(templateOutput, com.aspose.words.SaveFormat.DOCX);
byte[] bytes;
if (specialcondolence.getRecipients() != null && !specialcondolence.getRecipients().isEmpty()) {
com.aspose.words.Document combined = null;
for (cn.hutool.json.JSONObject person : specialcondolence.getRecipients()) {
NutMap personData = new NutMap();
personData.putAll(docData);
personData.putAll(person);
personData.put("payUserName", person.getStr("bankUserName"));
byte[] rendered = sysOfficeTemplateUtil.renderTemplate(new java.io.ByteArrayInputStream(templateOutput.toByteArray()), personData, null, false, "specialcondolence:" + id);
com.aspose.words.Document part = new com.aspose.words.Document(new java.io.ByteArrayInputStream(rendered));
if (combined == null) combined = part;
else combined.appendDocument(part, com.aspose.words.ImportFormatMode.KEEP_SOURCE_FORMATTING);
}
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
combined.save(output, pdf ? com.aspose.words.SaveFormat.PDF : com.aspose.words.SaveFormat.DOCX);
bytes = output.toByteArray();
} else {
bytes = sysOfficeTemplateUtil.renderTemplate(new java.io.ByteArrayInputStream(templateOutput.toByteArray()), docData, null, pdf, "specialcondolence:" + id);
}
log.info("[特殊慰问导出] 渲染完成,applyId={}, format={}, outputBytes={}", id, pdf ? "PDF" : "Word", bytes.length);
CommonDownloadUtil.download(fileName + (pdf ? ".pdf" : ".docx"), bytes, response);
} catch (Exception e) {
log.error("[特殊慰问导出] 模板渲染或文件输出失败,applyId={}, instanceId={}, format={}", id, instance.getId(), Boolean.TRUE.equals(print) ? "PDF" : "Word", e);
throw e;
}
}
private void fillApplyApprovals(NutMap docData, ProcessInstance instance) {
String[] taskKeys = {"c438972e-2f67-4907-b229-cb7aa17da147",
"7f560675-aeea-4254-a57e-957d03d6b786", "af363a4e-fc56-42a0-9906-8c7a2814006b"};
String[] fields = {"secondApproval", "thirdApproval", "fourthApproval"};
List<String> suffixes = List.of("Opinion", "Sign", "Year", "Month", "Day");
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
List<ProcessTask> tasks = specialcondolenceService.dao().query(ProcessTask.class,
Cnd.where("processInstanceId", "=", instance.getId()).asc("id"));
log.info("[特殊慰问导出] 查询审批记录,applyId={}, instanceId={}, taskCount={}, expectedTaskKeys={}", instance.getBusinessNo(), instance.getId(), tasks.size(), List.of(taskKeys));
for (ProcessTask task : tasks) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(task.getId());
log.info("[特殊慰问导出] 审批记录,instanceId={}, taskId={}, taskKey={}, displayName={}, state={}, finishTime={}, isFirstTaskNode={}, matchedApproval={}", instance.getId(), task.getId(), task.getTaskName(), task.getDisplayName(), task.getTaskState(), task.getFinishTime(), taskVO.getExt() == null ? null : taskVO.getExt().get("isFirstTaskNode"), List.of(taskKeys).contains(task.getTaskName()));
if ("startTask".equals(task.getTaskName()) || (!List.of(taskKeys).contains(task.getTaskName())
&& taskVO.getExt() != null && Boolean.TRUE.equals(taskVO.getExt().getBool("isFirstTaskNode")))) {
log.info("[特殊慰问导出] 申请节点重置本轮审批字段,instanceId={}, taskId={}, taskKey={}", instance.getId(), task.getId(), task.getTaskName());
// 重新申请后,仅导出本轮审批记录。
for (String field : fields) {
for (String suffix : suffixes) docData.put(field + suffix, "Sign".equals(suffix) ? null : "");
}
continue;
}
for (int i = 0; i < taskKeys.length; i++) {
if (!taskKeys[i].equals(task.getTaskName())) continue;
for (String suffix : suffixes) docData.put(fields[i] + suffix, "Sign".equals(suffix) ? null : "");
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())) {
log.info("[特殊慰问导出] 跳过未完成审批,instanceId={}, taskId={}, field={}, state={}", instance.getId(), task.getId(), fields[i], task.getTaskState());
continue;
}
Dict data = taskVO.getExt() == null ? Dict.create() : taskVO.getTaskFormData();
log.info("[特殊慰问导出] 读取审批表单,instanceId={}, taskId={}, field={}, formKeys={}, signPresent={}, signReferenceLength={}", instance.getId(), task.getId(), fields[i], data.keySet(), StrUtil.isNotBlank(data.getStr("sign")), StrUtil.nullToEmpty(data.getStr("sign")).length());
if (StrUtil.isBlank(data.getStr("sign"))) log.warn("[特殊慰问导出] 已完成审批缺少签名字段,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i]);
docData.put(fields[i] + "Opinion", StrUtil.nullToEmpty(data.getStr("opinion")));
if (StrUtil.isNotBlank(data.getStr("sign"))) {
PictureRenderData signature = sysOfficeTemplateUtil.createPictureRenderData(90, 40, data.getStr("sign"));
if (signature == null) throw new BaseException("审批签字图片读取失败");
docData.put(fields[i] + "Sign", signature);
log.info("[特殊慰问导出] 审批签名填充完成,instanceId={}, taskId={}, field={}", instance.getId(), task.getId(), fields[i] + "Sign");
}
docData.put(fields[i] + "Year", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "yyyy"));
docData.put(fields[i] + "Month", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "M"));
docData.put(fields[i] + "Day", task.getFinishTime() == null ? "" : DateUtil.format(task.getFinishTime(), "d"));
}
}
}
@At
@ApiOperation("删除")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"special_condolence.mine", "h5.special_condolence.mine"}, mode = SaMode.OR)
@SLog(tag = "特殊慰问系统-我的申请", msg = "删除特殊慰问")
public Result delete(@Param("id") String id) {
specialcondolenceService.deleteApplication(id);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
return Result.success();
}
@At
@ApiOperation("查询单个慰问申请信息")
@SaCheckPermission(value = {"special_condolence", "h5.special_condolence"}, mode = SaMode.OR)
public Result info(String id) {
SpecialCondolence specialcondolence = specialcondolenceService.loadApplication(id);
SpecialCondolenceType type = specialcondolenceService.dao().fetch(SpecialCondolenceType.class, Cnd.where(SpecialCondolenceType::getId, "=", specialcondolence.getType()));
NutMap nutMap = Lang.obj2nutmap(specialcondolence);
nutMap.put("typeName", type == null ? "" : type.getName());
return Result.success(nutMap);
}
@Inject
private SmsService smsService;
}
@@ -0,0 +1,180 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.param.SysMsgSummaryPageForm;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.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 javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName SpecialCondolenceReadController
* @Author JyuHsin
* @Date 2025/7/29 16:25
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "特殊慰问阅览")
@At("/platform/special_condolence/read")
public class SpecialCondolenceReadController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("")
@SaCheckPermission("special_condolence.read")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/read/index.html")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("special_condolence.read")
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId) {
Sql sql = Sqls.create("""
SELECT
info.*,
(SELECT COUNT(*) FROM special_condolence_recipient recipient WHERE recipient.applyId = info.id AND recipient.delFlag = false) AS recipientCount,
ins.id AS instanceId,
type.name as typeName,
ins.state instanceState,
ins.processDefineId instanceProcessDefineId,
t.displayName taskName
FROM
special_condolence info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT
processInstanceId,
displayName,
createdAt,
ROW_NUMBER() OVER (PARTITION BY processInstanceId ORDER BY createdAt DESC) AS rn
FROM wf_process_task
) t ON t.processInstanceId = ins.id AND t.rn = 1
LEFT JOIN special_condolence_type type ON info.type = type.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("info.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("info.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
@At
@Ok("void")
@SaCheckPermission("special_condolence.read")
@ApiOperation("导出特殊慰问汇总表")
public void onExport(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
HttpServletResponse response) {
//查询审核通过的数据
Sql sql = Sqls.create("""
select
con.subject,
case when recipient.id is null then con.helpUserName else recipient.helpUserName end as helpUserName,
case when recipient.id is null then con.helpLoginName else recipient.helpLoginName end as helpLoginName,
con.applyUserName,
con.helpUnionName,
con.helpUnitName,
case when recipient.id is null then type.name else recipient.typeName end as typeName,
case when recipient.id is null then con.money else recipient.money end as money,
con.remark
from
special_condolence con
left join special_condolence_recipient recipient on recipient.applyId = con.id and recipient.delFlag = false
left join wf_process_instance ins on ins.businessNo = con.id
left join special_condolence_type type on type.id = con.type
$condition
order by con.createTime desc, con.id, recipient.sortNum, recipient.id
""");
Cnd cnd = Cnd.NEW();
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("con.type", "=", type);
cnd.andEX("YEAR(con.createTime)", "=", year);
cnd.andEX("con.applyUnionId", "=", unionId);
cnd.andEX("con.applyUnitId", "=", unitId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("con.helpUserName", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("con.helpLoginName", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
sql.setCondition(cnd);
List<NutMap> list = specialcondolenceService.listMap(sql);
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("事项", "subject", 30));
exportEntities.add(new ExcelExportEntity("慰问对象", "helpUserName", 20));
exportEntities.add(new ExcelExportEntity("慰问对象工号", "helpLoginName", 20));
exportEntities.add(new ExcelExportEntity("申请人", "applyUserName", 20));
exportEntities.add(new ExcelExportEntity("所在工会", "helpUnionName", 20));
exportEntities.add(new ExcelExportEntity("所在单位", "helpUnitName", 20));
exportEntities.add(new ExcelExportEntity("慰问类型", "typeName", 20));
exportEntities.add(new ExcelExportEntity("慰问金额", "money", 20));
exportEntities.add(new ExcelExportEntity("申请理由", "remark", 20));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("申报人汇总.xlsx", workbook, response);
}
}
@@ -0,0 +1,140 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
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.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
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.List;
/**
* @author zhf
* @date 2025/12/23 14:13
* @description 校工会预审
*/
@IocBean
@At("/platform/special_condolence/schoolPrincipalApproval")
@Api("特殊慰问二级审核")
@Ok("json:full")
public class SpecialCondolenceSecondApprovalController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/schoolPrincipalApproval/index.html")
@SaCheckPermission("special_condolence.secondApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/schoolPrincipalApproval/index.html")
@SaCheckPermission("h5.special_condolence.secondApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.secondApproval", "h5.special_condolence.secondApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN special_condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.id", "is not", null);
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "c438972e-2f67-4907-b229-cb7aa17da147");
// cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -0,0 +1,144 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import io.swagger.annotations.Api;
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.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
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.List;
/**
* @ClassName SpecialCondolenceThirdApprovalController
* @Author JyuHsin
* @Date 2025/7/29 16:23
* @Version 1.0
* @Description TODO
*/
@IocBean
@At("/platform/special_condolence/schoolUnionApproval")
@Api("特殊慰问三级审核")
@Ok("json:full")
public class SpecialCondolenceThirdApprovalController {
@Inject
private SpecialCondolenceService specialcondolenceService;
@At("/")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/schoolUnionApproval/index.html")
@SaCheckPermission("special_condolence.thirdApproval")
public void index() {}
@At("/h5")
@Ok("beetl:/platform/zhghh5/staffbenefit/specialcondolence/schoolUnionApproval/index.html")
@SaCheckPermission("h5.special_condolence.thirdApproval")
public void h5Index() {}
@At
@ApiOperation("分页查询")
@SaCheckPermission(value = {"special_condolence.thirdApproval", "h5.special_condolence.thirdApproval"}, mode = SaMode.OR)
public Result pageData(PageForm pageForm,
@Param(value = "year") Integer year,
@Param(value = "type") String type,
@Param(value = "unionId") String unionId,
@Param(value = "unitId") String unitId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
COALESCE(recipientTotals.recipientCount, 0) AS recipientCount,
type.name as typeName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN special_condolence info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN (
SELECT applyId, COUNT(*) AS recipientCount
FROM special_condolence_recipient
WHERE delFlag = false
GROUP BY applyId
) recipientTotals ON recipientTotals.applyId = info.id
LEFT JOIN special_condolence_type type ON info.type = type.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.id", "is not", null);
cnd.and(new Static("""
NOT EXISTS (
SELECT 1 FROM wf_process_task newer
WHERE newer.processInstanceId = t.processInstanceId
AND newer.taskName = t.taskName
AND newer.id > t.id
)
"""));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolence::getHelpUserName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolence::getHelpLoginName, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("info.type", "=", type);
cnd.andEX("YEAR(info.createTime)", "=", year);
cnd.andEX("info.applyUnionId", "=", unionId);
cnd.andEX("info.applyUnitId", "=", unitId);
cnd.and("t.taskName", "=", "7f560675-aeea-4254-a57e-957d03d6b786");
if (!"superadmin".equals(SecurityUtil.getUserLoginname())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.desc("info.createTime");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = specialcondolenceService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
}
}
@@ -0,0 +1,91 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
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.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceTypeService;
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.util.cri.SqlExpressionGroup;
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 java.util.List;
/**
* @ClassName SpecialCondolenceTypeController
* @Author JyuHsin
* @Date 2025/7/29 10:00
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "特殊慰问类型")
@At("/platform/special_condolence/type")
public class SpecialCondolenceTypeController {
@Inject
private Dao dao;
@Inject
private SpecialCondolenceTypeService typeService;
@At("")
@SaCheckPermission("special_condolence.type")
@Ok("beetl:/platform/zhgh/staffbenefit/specialcondolence/type/index.html")
public void index() {
}
@At
@ApiOperation("分页查询")
@SaCheckPermission("special_condolence.type")
public Result pageData(PageForm pageForm) {
Cnd cnd = Cnd.NEW();
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or(SpecialCondolenceType::getName, "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or(SpecialCondolenceType::getCode, "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.asc("code");
Pagination pagination = typeService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
@At
@ApiOperation("新增/修改特殊慰问类型")
@SaCheckPermission("special_condolence.type")
@SLog(tag = "特殊慰问系统-慰问类型", msg = "新增/修改特殊慰问类型")
public Object onSubmit(SpecialCondolenceType type) {
typeService.insertOrUpdate(type);
return Result.success();
}
@At
@ApiOperation("删除特殊慰问类型")
@SaCheckPermission("special_condolence.type")
@SLog(tag = "特殊慰问系统-慰问类型", msg = "删除特殊慰问类型")
public Object onDelete(String id) {
typeService.delete(id);
return Result.success();
}
@At
@ApiOperation("查询特殊慰问类型")
@SaCheckLogin
public Result querySpecialCondolenceType() {
List<SpecialCondolenceType> list = typeService.query(Cnd.where(SpecialCondolenceType::getEnable, "=", true).asc(SpecialCondolenceType::getCode));
return Result.success(list);
}
}
@@ -0,0 +1,46 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.interceptor;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowInterceptor;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.zhgh.dayofficework.article.models.Article;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.json.Json;
/**
* @ClassName SpecialCondolenceSaveInterceptor
* @Author JyuHsin
* @Date 2025/7/28 11:01
* @Version 1.0
* @Description TODO
*/
public class SpecialCondolenceSaveInterceptor implements FlowInterceptor {
@Override
public void intercept(Execution execution) {
if (!com.budwk.app.web.commons.auth.utils.AuthUtil.hasRoleOr("BRANCH_UNION_CHAIRMAN")) {
throw new com.budwk.app.base.exception.BaseException("仅分工会主席可发起特殊慰问申请");
}
String formDataStr = execution.getArgs().getStr(FlowConst.FORM_DATA);
SpecialCondolence specialcondolence = Json.fromJson(SpecialCondolence.class, formDataStr);
if(StrUtil.isBlank(specialcondolence.getId())) specialcondolence.setCreateTime(DateUtil.now());
Dao dao = ServiceContext.find(Dao.class);
ServiceContext.find(com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService.class).saveApplication(specialcondolence);
// 设置流程变量
execution.getArgs().set("origin", specialcondolence.getOrigin());
int instanceId = execution.getArgs().getInt(FlowConst.PROCESS_INSTANCE_ID_KEY);
dao.update(ProcessInstance.class, Chain.make("businessNo", specialcondolence.getId()), Cnd.where(ProcessInstance::getId, "=", instanceId));
}
}
@@ -0,0 +1,310 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.model;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.model.BaseModel;
import com.budwk.app.sys.models.Sys_file;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.List;
/**
* @ClassName SpecialCondolence
* @Author JyuHsin
* @Date 2025/7/28 11:03
* @Version 1.0
* @Description TODO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("special_condolence")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("特殊慰问")
public class SpecialCondolence extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("事项")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String subject;
@Column
@Comment("申请人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUserId;
@Column
@Comment("申请人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUserName;
@Column
@Comment("申请人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String applyLoginName;
@Column
@Comment("申请人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUnionId;
@Column
@Comment("申请人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUnionName;
@Column
@Comment("申请人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyUnitId;
@Column
@Comment("申请人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String applyUnitName;
@Column
@Comment("补助人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUserId;
@Column
@Comment("补助人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUserName;
@Column
@Comment("补助人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String helpLoginName;
@Column
@Comment("补助人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUnionId;
@Column
@Comment("补助人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUnionName;
@Column
@Comment("补助人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUnitId;
@Column
@Comment("补助人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUnitName;
@Column
@Comment("类型")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String origin;
@Column
@Comment("收款人")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String payUserId;
@Column
@Comment("收款人姓名")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String payUserName;
@Column
@Comment("收款人工号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String payLoginName;
@Column
@Comment("慰问类型")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String type;
@Column
@Comment("慰问编码")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String typeCode;
@Column
@Comment("慰问方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String way;
@Column
@Comment("统一每人慰问金额")
@ColDefine(customType = "decimal(10,2)")
private java.math.BigDecimal perMoney;
@Column
@Comment("慰问金额")
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
private Double money;
@Column
@Comment("发生时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String occurTime;
@Column
@Comment("生育几孩(孩次)")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String child;
@Column
@Comment("所在医院")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String hospital;
@Column
@Comment("病房床号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String hospitalHouseNum;
@Column
@Comment("病房床号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String hospitalHouseBedNum;
@Column
@Comment("住院病由")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String hospitalBy;
@Column
@Comment("备注")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String remark;
@Column
@Comment("附件")
@ColDefine(type = ColType.MYSQL_JSON)
private List<JSONObject> files;
@Comment("签字")
@ColDefine(type = ColType.VARCHAR, width = 255)
@Column(hump = true)
private String signature;
@Column
@Comment("申请时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String createTime;
@Column
@Comment("户名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankUserName;
@Column
@Comment("银行账号")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankCardNumber;
@Column
@Comment("开户行")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankOfDeposit;
@Column
@Comment("证明人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String certifierUserId;
@Column
@Comment("证明人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String certifierUserName;
@Column
@Comment("证明人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String certifierLoginName;
@Column
@Comment("证明人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String certifierUnionId;
@Column
@Comment("证明人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String certifierUnionName;
@Column
@Comment("证明人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String certifierUnitId;
@Column
@Comment("证明人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String certifierUnitName;
@Column
@Comment("经办人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String handlerUserId;
@Column
@Comment("经办人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String handlerUserName;
@Column
@Comment("经办人工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String handlerLoginName;
@Column
@Comment("经办人分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String handlerUnionId;
@Column
@Comment("经办人分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String handlerUnionName;
@Column
@Comment("经办人单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String handlerUnitId;
@Column
@Comment("经办人单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String handlerUnitName;
@Column
@Comment("入院时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String hospitalizationTime;
@Column
@Comment("出院时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String leaveHospitalTime;
@Column
@Comment("当年第几次住院")
@ColDefine(type = ColType.INT)
private Integer thisYearHospitalizationNum;
@Comment("慰问对象明细,由明细表加载")
private List<JSONObject> recipients;
}
@@ -0,0 +1,100 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("special_condolence_recipient")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes({@Index(name = "idx_special_condolence_recipient_apply", fields = {"applyId"}, unique = false)})
@Comment("特殊慰问被申请人明细")
public class SpecialCondolenceRecipient extends BaseModel {
@Name
@PrevInsert(uu32 = true)
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("申请ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String applyId;
@Column
@Comment("慰问对象用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String helpUserId;
@Column
@Comment("人员类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String personType;
@Column
@Comment("慰问对象工号")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String helpLoginName;
@Column
@Comment("慰问对象")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String helpUserName;
@Column
@Comment("慰问类型ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String type;
@Column
@Comment("慰问类型名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String typeName;
@Column
@Comment("慰问类型编码")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String typeCode;
@Column
@Comment("慰问方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String way;
@Column
@Comment("身份证号码")
@ColDefine(type = ColType.VARCHAR, width = 18)
private String idCard;
@Column
@Comment("开户行")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankOfDeposit;
@Column
@Comment("户名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String bankUserName;
@Column
@Comment("收款账户")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String bankCardNumber;
@Column
@Comment("慰问金额")
@ColDefine(customType = "decimal(10,2)")
private java.math.BigDecimal money;
@Column
@Comment("慰问时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String occurTime;
@Column
@Comment("行序号")
@ColDefine(type = ColType.INT)
private Integer sortNum;
}
@@ -0,0 +1,84 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.model;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.DB;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
/**
* @ClassName SpecialCondolenceType
* @Author JyuHsin
* @Date 2025/7/29 9:59
* @Version 1.0
* @Description TODO
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("special_condolence_type")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("特殊慰问类型")
public class SpecialCondolenceType extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("编码")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String code;
@Column
@Comment("名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String name;
@Column
@Comment("费用")
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
private Double money;
@Column
@Comment("慰问方式")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String way;
@Column
@Comment("是否启用")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enable;
@Column
@Comment("是否上传附件")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isUploadFile;
@Column
@Comment("上传附件说明")
@ColDefine(type = ColType.VARCHAR,width = 50)
private String uploadFileDesc;
@Column
@Comment("排序字段")
@Prev({
@SQL(db = DB.MYSQL, value = "SELECT IFNULL(MAX(location),0)+1 FROM special_condolence_type"),
@SQL(db = DB.ORACLE, value = "SELECT COALESCE(MAX(location),0)+1 FROM special_condolence_type")
})
private Integer location;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT)
private int sortNum;
@Column
@Comment("上传会议纪要")
@Default("0")
@ColDefine(type = ColType.BOOLEAN)
private Boolean uploadMeetingRecord;
}
@@ -0,0 +1,122 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import org.nutz.dao.Dao;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
public final class SpecialCondolenceRecipients {
private SpecialCondolenceRecipients() {}
public static void prepare(SpecialCondolence info, Dao dao) {
if (info.getSubject() == null || info.getSubject().trim().isEmpty() || info.getSubject().trim().length() > 200) throw new BaseException("请填写200字以内的事项");
info.setSubject(info.getSubject().trim());
List<JSONObject> rows = info.getRecipients();
if (rows == null || rows.isEmpty()) throw new BaseException("请添加慰问对象");
if (rows.size() > 1000) throw new BaseException("每次申请最多1000人");
if (info.getFiles() != null && info.getFiles().size() > 5) throw new BaseException("最多上传5个附件");
if (info.getRemark() != null && info.getRemark().trim().length() > 500) throw new BaseException("申请事由最多500字");
if (info.getPerMoney() == null || info.getPerMoney().signum() <= 0 || info.getPerMoney().compareTo(new BigDecimal("10000")) > 0 || info.getPerMoney().scale() > 2) throw new BaseException("每人慰问金额须大于0且不超过10000元,最多两位小数");
try { LocalDate.parse(info.getOccurTime()); }
catch (Exception e) { throw new BaseException("请选择有效的统一慰问时间"); }
BigDecimal total = BigDecimal.ZERO;
for (int i = 0; i < rows.size(); i++) {
JSONObject row = rows.get(i);
String prefix = "第" + (i + 1) + "行:";
if (row == null) throw new BaseException(prefix + "明细不能为空");
row.set("type", info.getType());
row.set("money", info.getPerMoney().toPlainString());
row.set("occurTime", info.getOccurTime());
if (cn.hutool.core.util.StrUtil.isNotBlank(row.getStr("helpUserId"))) resolveUser(row, dao, prefix);
String[] fields = {"helpUserName", "type", "idCard", "bankOfDeposit", "bankUserName", "bankCardNumber", "money", "occurTime"};
for (String field : fields) {
String value = row.getStr(field);
if (value == null || value.trim().isEmpty()) throw new BaseException(prefix + "请填写完整的慰问对象信息");
row.set(field, value.trim());
}
if (!validIdCard(row.getStr("idCard"))) throw new BaseException(prefix + "身份证号码不正确");
if (!row.getStr("bankCardNumber").matches("[0-9]{6,30}")) throw new BaseException(prefix + "收款账户须为6至30位数字");
if (row.getStr("helpUserName").length() > 100 || row.getStr("bankOfDeposit").length() > 100 || row.getStr("bankUserName").length() > 100) throw new BaseException(prefix + "姓名、开户行、户名不能超过100字");
if (!row.getStr("money").matches("[0-9]+(\\.[0-9]{1,2})?")) throw new BaseException(prefix + "金额最多两位小数");
BigDecimal money = new BigDecimal(row.getStr("money"));
if (money.signum() <= 0 || money.compareTo(new BigDecimal("10000")) > 0) throw new BaseException(prefix + "金额须大于0且不超过10000元");
try { LocalDate.parse(row.getStr("occurTime")); }
catch (Exception e) { throw new BaseException(prefix + "慰问时间格式须为yyyy-MM-dd"); }
SpecialCondolenceType type = dao.fetch(SpecialCondolenceType.class, row.getStr("type"));
if (type == null || !Boolean.TRUE.equals(type.getEnable())) throw new BaseException(prefix + "慰问类型不存在或已停用");
row.set("typeName", type.getName());
row.set("typeCode", type.getCode());
row.set("way", type.getWay());
if (Boolean.TRUE.equals(type.getIsUploadFile()) && (info.getFiles() == null || info.getFiles().isEmpty())) throw new BaseException(prefix + "该慰问类型需要上传附件");
total = total.add(money);
}
JSONObject first = rows.get(0);
String name = first.getStr("helpUserName");
info.setHelpUserName((rows.size() > 1 && name.length() > 90 ? name.substring(0, 90) : name) + (rows.size() > 1 ? "等" + rows.size() + "人" : ""));
info.setHelpUnitId(info.getApplyUnitId());
info.setHelpUnitName(info.getApplyUnitName());
info.setHelpUnionId(info.getApplyUnionId());
info.setHelpUnionName(info.getApplyUnionName());
info.setType(first.getStr("type"));
info.setTypeCode(first.getStr("typeCode"));
info.setWay(first.getStr("way"));
info.setOccurTime(first.getStr("occurTime"));
info.setMoney(total.doubleValue());
info.setBankOfDeposit(first.getStr("bankOfDeposit"));
info.setBankUserName(first.getStr("bankUserName"));
info.setBankCardNumber(first.getStr("bankCardNumber"));
}
public static void resolveUser(JSONObject row, Dao dao, String prefix) {
String userId = row.getStr("helpUserId");
com.budwk.app.sys.views.View_user user;
String unionId = com.budwk.app.web.commons.auth.utils.SecurityUtil.getUnionId();
if (cn.hutool.core.util.StrUtil.isBlank(unionId)) throw new BaseException(prefix + "申请人未关联分工会");
if (cn.hutool.core.util.StrUtil.isNotBlank(userId)) {
user = dao.fetch(com.budwk.app.sys.views.View_user.class, org.nutz.dao.Cnd.where("id", "=", userId));
} else {
java.util.List<com.budwk.app.sys.views.View_user> users = dao.query(com.budwk.app.sys.views.View_user.class,
org.nutz.dao.Cnd.where("idCard", "=", row.getStr("idCard"))
.and("username", "=", row.getStr("helpUserName")));
if (users.isEmpty()) throw new BaseException(prefix + "姓名、身份证与人员档案不匹配");
java.util.List<com.budwk.app.sys.views.View_user> members = users.stream()
.filter(member -> java.util.Objects.equals(member.getUnionId(), unionId))
.collect(java.util.stream.Collectors.toList());
if (members.isEmpty()) throw new BaseException(prefix + "只能选择申请人本工会人员");
String loginName = row.getStr("helpLoginName");
if (cn.hutool.core.util.StrUtil.isNotBlank(loginName)) {
members = members.stream().filter(member -> "劳务派遣人员".equals(member.getPersonType())
|| java.util.Objects.equals(member.getLoginname(), loginName))
.collect(java.util.stream.Collectors.toList());
}
if (members.isEmpty()) throw new BaseException(prefix + "工号与慰问对象不一致");
if (members.size() > 1) throw new BaseException(prefix + "人员身份重复,请核对档案");
user = members.get(0);
}
if (user == null) throw new BaseException(prefix + "慰问对象不存在");
if (!java.util.Objects.equals(user.getUnionId(), unionId)) throw new BaseException(prefix + "只能选择申请人本工会人员");
if (!"劳务派遣人员".equals(user.getPersonType()) && cn.hutool.core.util.StrUtil.isBlank(row.getStr("helpLoginName"))) throw new BaseException(prefix + "普通人员必须填写工号");
if (!"劳务派遣人员".equals(user.getPersonType()) && (cn.hutool.core.util.StrUtil.isBlank(row.getStr("helpLoginName")) || !java.util.Objects.equals(user.getLoginname(), row.getStr("helpLoginName")))) throw new BaseException(prefix + "工号与慰问对象不一致");
if (cn.hutool.core.util.StrUtil.isNotBlank(user.getIdCard()) && !user.getIdCard().equalsIgnoreCase(row.getStr("idCard"))) throw new BaseException(prefix + "身份证号码与所选人员不一致");
row.set("helpUserId", user.getId());
row.set("helpLoginName", user.getLoginname());
row.set("personType", user.getPersonType());
row.set("helpUserName", user.getUsername());
}
public static boolean validIdCard(String value) {
if (value == null || !value.matches("[1-9][0-9]{16}[0-9Xx]")) return false;
try { LocalDate.parse(value.substring(6, 14), java.time.format.DateTimeFormatter.BASIC_ISO_DATE); }
catch (Exception e) { return false; }
int[] weights = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
int sum = 0;
for (int i = 0; i < weights.length; i++) sum += (value.charAt(i) - '0') * weights[i];
return "10X98765432".charAt(sum % 11) == Character.toUpperCase(value.charAt(17));
}
}
@@ -0,0 +1,18 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
/**
* @ClassName SpecialCondolenceService
* @Author JyuHsin
* @Date 2025/7/29 11:21
* @Version 1.0
* @Description TODO
*/
public interface SpecialCondolenceService extends BaseService<SpecialCondolence> {
void saveApplication(SpecialCondolence info);
void saveDraftApplication(SpecialCondolence info);
SpecialCondolence loadApplication(String id);
void deleteApplication(String id);
}
@@ -0,0 +1,14 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
/**
* @ClassName SpecialCondolenceTypeService
* @Author JyuHsin
* @Date 2025/7/29 10:03
* @Version 1.0
* @Description TODO
*/
public interface SpecialCondolenceTypeService extends BaseService<SpecialCondolenceType> {
}
@@ -0,0 +1,110 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceRecipient;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceRecipients;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.nutz.ioc.aop.Aop;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolence;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
/**
* @ClassName SpecialCondolenceServiceImpl
* @Author JyuHsin
* @Date 2025/7/29 11:22
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class SpecialCondolenceServiceImpl extends BaseServiceImpl<SpecialCondolence> implements SpecialCondolenceService {
public SpecialCondolenceServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveApplication(SpecialCondolence info) {
SpecialCondolenceRecipients.prepare(info, dao());
persistApplication(info);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveDraftApplication(SpecialCondolence info) {
if (info.getRecipients() == null) info.setRecipients(new ArrayList<>());
info.getRecipients().removeIf(row -> row == null || java.util.stream.Stream.of(
"helpUserId", "helpUserName", "helpLoginName", "idCard", "bankOfDeposit", "bankUserName", "bankCardNumber")
.allMatch(field -> StrUtil.isBlank(row.getStr(field))));
for (JSONObject row : info.getRecipients()) {
row.set("type", info.getType());
row.set("typeCode", info.getTypeCode());
row.set("way", info.getWay());
row.set("money", info.getPerMoney());
row.set("occurTime", info.getOccurTime());
}
info.setMoney(info.getPerMoney() == null ? null : info.getPerMoney().multiply(BigDecimal.valueOf(info.getRecipients().size())).doubleValue());
persistApplication(info);
}
private void persistApplication(SpecialCondolence info) {
dao().insertOrUpdate(info);
dao().clear(SpecialCondolenceRecipient.class,
Cnd.where("applyId", "=", info.getId()));
int sort = 0;
for (JSONObject row : info.getRecipients()) {
SpecialCondolenceRecipient person = new SpecialCondolenceRecipient();
person.setApplyId(info.getId());
person.setHelpUserName(row.getStr("helpUserName", ""));
person.setHelpUserId(row.getStr("helpUserId", ""));
person.setHelpLoginName(row.getStr("helpLoginName"));
person.setPersonType(row.getStr("personType"));
person.setType(row.getStr("type", ""));
person.setTypeName(row.getStr("typeName"));
person.setTypeCode(row.getStr("typeCode"));
person.setWay(row.getStr("way"));
person.setIdCard(row.getStr("idCard", ""));
person.setBankOfDeposit(row.getStr("bankOfDeposit", ""));
person.setBankUserName(row.getStr("bankUserName", ""));
person.setBankCardNumber(row.getStr("bankCardNumber", ""));
person.setMoney(row.getBigDecimal("money", BigDecimal.ZERO));
person.setOccurTime(row.getStr("occurTime", ""));
person.setSortNum(sort++);
dao().insert(person);
}
}
@Override
public SpecialCondolence loadApplication(String id) {
SpecialCondolence info = fetch(id);
if (info != null) {
List<JSONObject> rows = new ArrayList<>();
for (SpecialCondolenceRecipient person : dao().query(
SpecialCondolenceRecipient.class,
Cnd.where("applyId", "=", id).asc("sortNum").asc("id"))) {
rows.add(JSONUtil.parseObj(person));
}
info.setRecipients(rows);
}
return info;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteApplication(String id) {
dao().clear(SpecialCondolenceRecipient.class,
Cnd.where("applyId", "=", id));
delete(id);
}
}
@@ -0,0 +1,24 @@
package com.budwk.app.zhgh.staffbenefit.specialcondolence.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.model.SpecialCondolenceType;
import com.budwk.app.zhgh.staffbenefit.specialcondolence.service.SpecialCondolenceTypeService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
/**
* @ClassName SpecialCondolenceTypeServiceImpl
* @Author JyuHsin
* @Date 2025/7/29 10:03
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class SpecialCondolenceTypeServiceImpl extends BaseServiceImpl<SpecialCondolenceType> implements SpecialCondolenceTypeService {
public SpecialCondolenceTypeServiceImpl(Dao dao) {
super(dao);
}
}
@@ -147,9 +147,12 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
Cnd.where(WelfareProjectSubjectOption::getId, "in", toDeleteIds)); Cnd.where(WelfareProjectSubjectOption::getId, "in", toDeleteIds));
} }
for (WelfareProjectSubjectOption option : project.getOptions()) { // 慰问金可不配置福利选项,编辑空选项项目时无需写入关联记录。
option.setWelfareId(project.getId()); if (project.getOptions() != null) {
dao().insertOrUpdate(option); for (WelfareProjectSubjectOption option : project.getOptions()) {
option.setWelfareId(project.getId());
dao().insertOrUpdate(option);
}
} }
// 更新首页活动 // 更新首页活动
@@ -0,0 +1,117 @@
# 新增角色
INSERT INTO sys_role (id, name, code, aliasName, disabled, unitid, moduleId, note, sort, createdBy, createdAt,
updatedBy, updatedAt, delFlag)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', '校工会办公室主任', 'SCHOOL_OFFICE_DIRECTOR', null, false, null,
'07d940ba34bd47399cda140894f5d86b', null, null, '17a7f8ad3ee947b4a26175049a9c253d', 1791510778436,
'17a7f8ad3ee947b4a26175049a9c253d', 1791510778437, false);
# 修改审核菜单名称
UPDATE sys_menu t
SET t.name = '校工会办公室主任审批'
WHERE t.id = 'df2fb530141c4245826ac9974ec90c9a';
UPDATE sys_menu t
SET t.name = '校工会主席审批'
WHERE t.id = 'a473302b80ce404e93bc820f858a14da';
UPDATE sys_menu t
SET t.name = '分管校领导审批'
WHERE t.id = '3e0bb63c5f0044bd837ad24633e92560';
UPDATE sys_menu t
SET t.name = '校工会办公室主任审批'
WHERE t.id = '23d30124199b486d8af79aba62440b15';
UPDATE sys_menu t
SET t.name = '校工会主席审批'
WHERE t.id = 'd736e4d7e8644cf38f248e7ad87ed6ad';
UPDATE sys_menu t
SET t.name = '分管校领导审批'
WHERE t.id = '35a9239d754c47b9afccce173cb72d64';
# 清除之前的菜单角色权限
# 校工会办公室主任审批
delete
from sys_role_menu
where menuId = 'df2fb530141c4245826ac9974ec90c9a';
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'df2fb530141c4245826ac9974ec90c9a');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', 'df2fb530141c4245826ac9974ec90c9a');
delete
from sys_role_menu
where menuId = '23d30124199b486d8af79aba62440b15';
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '23d30124199b486d8af79aba62440b15');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', '23d30124199b486d8af79aba62440b15');
# 校工会主席审批
delete
from sys_role_menu
where menuId = 'a473302b80ce404e93bc820f858a14da';
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'a473302b80ce404e93bc820f858a14da');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'a473302b80ce404e93bc820f858a14da');
delete
from sys_role_menu
where menuId = 'd736e4d7e8644cf38f248e7ad87ed6ad';
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'd736e4d7e8644cf38f248e7ad87ed6ad');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'd736e4d7e8644cf38f248e7ad87ed6ad');
# 分管校领导审批
delete
from sys_role_menu
where menuId = '3e0bb63c5f0044bd837ad24633e92560';
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '3e0bb63c5f0044bd837ad24633e92560');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('b3854469e6f44901b22bb791cb538431', '3e0bb63c5f0044bd837ad24633e92560');
delete
from sys_role_menu
where menuId = '35a9239d754c47b9afccce173cb72d64';
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '35a9239d754c47b9afccce173cb72d64');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('b3854469e6f44901b22bb791cb538431', '35a9239d754c47b9afccce173cb72d64');
# 更新权限
UPDATE zhgh_cqgmy.sys_menu
SET permission = 'difficultHelp.firstApproval'
WHERE permission = 'difficultHelp.branchUnionApproval';
UPDATE zhgh_cqgmy.sys_menu
SET permission = 'difficultHelp.secondApproval'
WHERE permission = 'difficultHelp.unionLeaderApproval';
UPDATE zhgh_cqgmy.sys_menu
SET permission = 'difficultHelp.thirdApproval'
WHERE permission = 'difficultHelp.schoolUnionApproval';
UPDATE zhgh_cqgmy.sys_menu
SET permission = 'h5.difficultHelp.firstApproval'
WHERE permission = 'h5.difficultHelp.branchUnionApproval';
UPDATE zhgh_cqgmy.sys_menu
SET permission = 'h5.difficultHelp.secondApproval'
WHERE permission = 'h5.difficultHelp.unionLeaderApproval';
UPDATE zhgh_cqgmy.sys_menu
SET permission = 'h5.difficultHelp.thirdApproval'
WHERE permission = 'h5.difficultHelp.schoolUnionApproval';
# 新增字典项
INSERT INTO sys_dict (id, parentId, path, name, code, disabled, location, hasChildren, createdBy, createdAt, updatedBy,
updatedAt, delFlag, remark)
VALUES ('4d3e3472ae324552b16f0aaf07c37004', '501e0a4edca748ddb739ffb679170d50', '00020017', '校工会办公室主任',
'SCHOOL_OFFICE_DIRECTOR', false, 9614, false, '17a7f8ad3ee947b4a26175049a9c253d', 1791517263500,
'17a7f8ad3ee947b4a26175049a9c253d', 1791517263500, false, null);
ALTER TABLE difficult_help_info ADD COLUMN applyMoney VARCHAR(200) NULL COMMENT '申请金额';
@@ -0,0 +1,136 @@
START TRANSACTION;
-- 一级:分工会主席审批,节点 4dad0bef-3cf2-4a7d-bba3-35020b9b216d。
UPDATE zhgh_cqgmy.sys_menu
SET name = '分工会主席审批', initialPinyinName = 'f', note = '分工会主席审批',
permission = 'condolence.firstApproval'
WHERE id = '8e24dfdb70e448a899ea7de92319789c';
UPDATE zhgh_cqgmy.sys_menu
SET name = '分工会主席审批', initialPinyinName = 'f', note = '分工会主席审批',
permission = 'h5.condolence.firstApproval'
WHERE id = '330e42e0ae914f5392b96fb933f17b5b';
-- 二级:校工会办公室主任审批,节点 34f7938e-47a3-4b3a-b4e0-92b7a3f47865。
UPDATE zhgh_cqgmy.sys_menu
SET name = '校工会办公室主任审批', initialPinyinName = 'x', note = '校工会办公室主任审批',
permission = 'condolence.secondApproval'
WHERE id = '09a59e7a99204ceca8e5dbbf8b2fafcf';
UPDATE zhgh_cqgmy.sys_menu
SET name = '校工会办公室主任审批', initialPinyinName = 'x', note = '校工会办公室主任审批',
permission = 'h5.condolence.secondApproval'
WHERE id = '87a2dcfac0da4ec4870b65693807a4d1';
-- 三级:校工会主席审批,节点 ecdaef3b-edf4-4143-9a96-887dd5921711。
UPDATE zhgh_cqgmy.sys_menu
SET name = '校工会主席审批', initialPinyinName = 'x', note = '校工会主席审批',
permission = 'condolence.thirdApproval'
WHERE id = '0917928006574b2a99f7ef3e8804d873';
UPDATE zhgh_cqgmy.sys_menu
SET name = '校工会主席审批', initialPinyinName = 'x', note = '校工会主席审批',
permission = 'h5.condolence.thirdApproval'
WHERE id = 'c269f94a5c8a4a4a8c8de0bb8f8f9f9a';
-- 四级:分管校领导审批,节点 cbd41337-3c87-4959-9027-809df5c7ef5e。
INSERT INTO zhgh_cqgmy.sys_menu
(id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName,
isRecommendApp, isRecommendService)
VALUES ('3561e97f44d14b47ae07f63ff8312a90', '3b49290f7db44130b9f6394a8212321f', '00900009',
'分管校领导审批', null, 'menu', '/platform/condolence/fourthApproval', null, null, true, false,
'condolence.fourthApproval', '分管校领导审批', 146, false,
'17a7f8ad3ee947b4a26175049a9c253d', UNIX_TIMESTAMP() * 1000,
'17a7f8ad3ee947b4a26175049a9c253d', UNIX_TIMESTAMP() * 1000,
false, null, 'PC', '07d940ba34bd47399cda140894f5d86b', false, null, 'f', false, false);
INSERT INTO zhgh_cqgmy.sys_menu
(id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName,
isRecommendApp, isRecommendService)
VALUES ('f02d5a68bdeb4c63a1b8ce47296d7304', '024811fd168e44948e6ab84911fc26bb', '01220006',
'分管校领导审批', null, 'menu', '/platform/condolence/fourthApproval/h5', null, null, true, false,
'h5.condolence.fourthApproval', '分管校领导审批', 32, false,
'17a7f8ad3ee947b4a26175049a9c253d', UNIX_TIMESTAMP() * 1000,
'17a7f8ad3ee947b4a26175049a9c253d', UNIX_TIMESTAMP() * 1000,
false, null, 'H5', '586fffaeddc94a7493c81b6d69e546ab', false, '/platform/sys/file/download?id=d2095854e67dcf28dea1a4a975e90aba', 'f', false, false);
-- 清除之前的菜单角色权限,按审核角色重新配置。
-- 一级:分工会主席审批,节点 4dad0bef-3cf2-4a7d-bba3-35020b9b216d。
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = '8e24dfdb70e448a899ea7de92319789c';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '8e24dfdb70e448a899ea7de92319789c');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
values ('fd753e8a8c5a404e8e00ef8e0b0c69da', '8e24dfdb70e448a899ea7de92319789c');
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = '330e42e0ae914f5392b96fb933f17b5b';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '330e42e0ae914f5392b96fb933f17b5b');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
values ('fd753e8a8c5a404e8e00ef8e0b0c69da', '330e42e0ae914f5392b96fb933f17b5b');
-- 二级:校工会办公室主任审批,节点 34f7938e-47a3-4b3a-b4e0-92b7a3f47865。
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = '09a59e7a99204ceca8e5dbbf8b2fafcf';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '09a59e7a99204ceca8e5dbbf8b2fafcf');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', '09a59e7a99204ceca8e5dbbf8b2fafcf');
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = '87a2dcfac0da4ec4870b65693807a4d1';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '87a2dcfac0da4ec4870b65693807a4d1');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', '87a2dcfac0da4ec4870b65693807a4d1');
-- 三级:校工会主席审批,节点 ecdaef3b-edf4-4143-9a96-887dd5921711。
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = '0917928006574b2a99f7ef3e8804d873';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '0917928006574b2a99f7ef3e8804d873');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', '0917928006574b2a99f7ef3e8804d873');
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = 'c269f94a5c8a4a4a8c8de0bb8f8f9f9a';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'c269f94a5c8a4a4a8c8de0bb8f8f9f9a');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'c269f94a5c8a4a4a8c8de0bb8f8f9f9a');
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = '3561e97f44d14b47ae07f63ff8312a90';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '3561e97f44d14b47ae07f63ff8312a90');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('b3854469e6f44901b22bb791cb538431', '3561e97f44d14b47ae07f63ff8312a90');
DELETE FROM zhgh_cqgmy.sys_role_menu WHERE menuId = 'f02d5a68bdeb4c63a1b8ce47296d7304';
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', 'f02d5a68bdeb4c63a1b8ce47296d7304');
INSERT INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('b3854469e6f44901b22bb791cb538431', 'f02d5a68bdeb4c63a1b8ce47296d7304');
-- 补充职工慰问父菜单权限。
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '3b49290f7db44130b9f6394a8212321f');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', '3b49290f7db44130b9f6394a8212321f');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', '3b49290f7db44130b9f6394a8212321f');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('b3854469e6f44901b22bb791cb538431', '3b49290f7db44130b9f6394a8212321f');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
SELECT id, '3b49290f7db44130b9f6394a8212321f' FROM zhgh_cqgmy.sys_role WHERE code = 'BRANCH_UNION_CHAIRMAN' AND disabled = false AND delFlag = false;
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '024811fd168e44948e6ab84911fc26bb');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', '024811fd168e44948e6ab84911fc26bb');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', '024811fd168e44948e6ab84911fc26bb');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
VALUES ('b3854469e6f44901b22bb791cb538431', '024811fd168e44948e6ab84911fc26bb');
INSERT IGNORE INTO zhgh_cqgmy.sys_role_menu (roleId, menuId)
SELECT id, '024811fd168e44948e6ab84911fc26bb' FROM zhgh_cqgmy.sys_role WHERE code = 'BRANCH_UNION_CHAIRMAN' AND disabled = false AND delFlag = false;
COMMIT;
@@ -0,0 +1,14 @@
truncate table condolence_type;
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('00016fcb72234a8f95cd173665bbe563', 'C3', '生育(三孩)', 5000.00, '慰问金 (A)', false, null, null, 0, '17a7f8ad3ee947b4a26175049a9c253d', 1791539298087, '17a7f8ad3ee947b4a26175049a9c253d', 1791539298087, false, null, 8, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('3777d15929dd4c66b3aa9ae67dc62528', 'C2', '生育(二孩)', 3000.00, '慰问金 (A)', false, null, null, 0, '17a7f8ad3ee947b4a26175049a9c253d', 1791539214037, '17a7f8ad3ee947b4a26175049a9c253d', 1791539269588, false, null, 7, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('4cdf253d26044261a1a81359bbde1bb8', 'G', '直系亲属去世', 2000.00, '慰问金 (A)', false, true, '', 0, '17a7f8ad3ee947b4a26175049a9c253d', 1765427298440, '17a7f8ad3ee947b4a26175049a9c253d', 1791539615689, false, null, 2, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('4e316737e71047e0b01aea78524c1416', 'E', '生病住院', 1000.00, '慰问金 (A)', false, true, '每年累计不超过3000元、每人每年最多申请3次', 1, '29847d7826aa415a92d03b9576d01d3f', 1722084651317, '29847d7826aa415a92d03b9576d01d3f', 1791539558039, false, false, null, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('5b5f135e523d4cc5b5dc4355ffe9e024', 'B', '生日', 600.00, '慰问金 (A)', false, null, null, 0, '17a7f8ad3ee947b4a26175049a9c253d', 1791539148841, '17a7f8ad3ee947b4a26175049a9c253d', 1791539148841, false, null, 6, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('60f03a87b62b4823855814afdbf5672a', 'F', '会员去世', 3000.00, '慰问金 (A)', false, true, '需要提供购买的慰问品发票,慰问照片', 0, '17a7f8ad3ee947b4a26175049a9c253d', 1765427276486, '17a7f8ad3ee947b4a26175049a9c253d', 1791539570707, false, null, 1, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('b8ee30faacb2469593dfedb5b52565aa', 'D', '光荣退休', 2000.00, '慰问金 (A)', false, true, '需要提供购买的慰问品发票,慰问照片', 0, '17a7f8ad3ee947b4a26175049a9c253d', 1765427383848, '17a7f8ad3ee947b4a26175049a9c253d', 1791539529423, false, null, 3, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('c90cb10dce6542e99ae271bee6fe8cc0', 'C1', '生育(一孩)', 2000.00, '慰问金 (A)', false, true, '需要提供购买的慰问品发票,慰问照片(会员与婴儿合影照,或慰问人员与会员和婴儿的合影照)', 0, '17a7f8ad3ee947b4a26175049a9c253d', 1765427542820, '17a7f8ad3ee947b4a26175049a9c253d', 1791539263656, false, null, 5, false, true);
INSERT INTO condolence_type (id, code, name, money, way, disabled, isUploadFile, uploadFileDesc, sortNum, createdBy, createdAt, updatedBy, updatedAt, delFlag, isDisabled, location, uploadMeetingRecord, enable) VALUES ('cf4ce7af322d464f8f86d818fcc00203', 'A', '结婚', 1000.00, '慰问金 (A)', false, true, '需要提供购买的慰问品的发票、慰问照片、会员的结婚证复印件', 2, '29847d7826aa415a92d03b9576d01d3f', 1722084861840, '29847d7826aa415a92d03b9576d01d3f', 1768875320985, false, false, null, false, true);
@@ -0,0 +1,227 @@
-- 全量初始化:完整定义特殊慰问申请主表、被申请人明细表和慰问类型表。
-- 表已存在时不执行本文件中的建表语句;本文件不复制数据、不配置角色授权和工作流。
CREATE TABLE `special_condolence` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`subject` varchar(255) DEFAULT NULL COMMENT '事项',
`applyUserId` varchar(32) DEFAULT NULL COMMENT '申请人ID',
`applyUserName` varchar(100) DEFAULT NULL COMMENT '申请人姓名',
`applyLoginName` varchar(120) DEFAULT NULL COMMENT '申请人工号',
`applyUnionId` varchar(32) DEFAULT NULL COMMENT '申请人分工会ID',
`applyUnionName` varchar(100) DEFAULT NULL COMMENT '申请人分工会',
`applyUnitId` varchar(32) DEFAULT NULL COMMENT '申请人单位ID',
`applyUnitName` varchar(100) DEFAULT NULL COMMENT '申请人单位',
`helpUserId` varchar(32) DEFAULT NULL COMMENT '补助人ID',
`helpUserName` varchar(100) DEFAULT NULL COMMENT '补助人姓名',
`helpLoginName` varchar(120) DEFAULT NULL COMMENT '补助人工号',
`helpUnionId` varchar(32) DEFAULT NULL COMMENT '补助人分工会ID',
`helpUnionName` varchar(100) DEFAULT NULL COMMENT '补助人分工会',
`helpUnitId` varchar(32) DEFAULT NULL COMMENT '补助人单位ID',
`helpUnitName` varchar(100) DEFAULT NULL COMMENT '补助人单位',
`origin` varchar(20) DEFAULT NULL COMMENT '类型',
`payUserId` varchar(32) DEFAULT NULL COMMENT '收款人',
`payUserName` varchar(30) DEFAULT NULL COMMENT '收款人姓名',
`payLoginName` varchar(20) DEFAULT NULL COMMENT '收款人工号',
`type` varchar(32) DEFAULT NULL COMMENT '慰问类型',
`typeCode` varchar(32) DEFAULT NULL COMMENT '慰问编码',
`way` varchar(20) DEFAULT NULL COMMENT '慰问方式',
`perMoney` decimal(10,2) DEFAULT NULL COMMENT '统一每人慰问金额',
`money` float(10,2) DEFAULT NULL COMMENT '慰问金额',
`occurTime` varchar(30) DEFAULT NULL COMMENT '发生时间',
`child` varchar(10) DEFAULT NULL COMMENT '生育几孩(孩次)',
`hospital` varchar(50) DEFAULT NULL COMMENT '所在医院',
`hospitalHouseNum` varchar(20) DEFAULT NULL COMMENT '病房床号',
`hospitalHouseBedNum` varchar(20) DEFAULT NULL COMMENT '病房床号',
`hospitalBy` varchar(100) DEFAULT NULL COMMENT '住院病由',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`files` json DEFAULT NULL COMMENT '附件',
`signature` varchar(255) DEFAULT NULL COMMENT '签字',
`createTime` varchar(30) DEFAULT NULL COMMENT '申请时间',
`bankUserName` varchar(100) DEFAULT NULL COMMENT '户名',
`bankCardNumber` varchar(100) DEFAULT NULL COMMENT '银行账号',
`bankOfDeposit` varchar(100) DEFAULT NULL COMMENT '开户行',
`certifierUserId` varchar(32) DEFAULT NULL COMMENT '证明人ID',
`certifierUserName` varchar(100) DEFAULT NULL COMMENT '证明人姓名',
`certifierLoginName` varchar(120) DEFAULT NULL COMMENT '证明人工号',
`certifierUnionId` varchar(32) DEFAULT NULL COMMENT '证明人分工会ID',
`certifierUnionName` varchar(100) DEFAULT NULL COMMENT '证明人分工会',
`certifierUnitId` varchar(32) DEFAULT NULL COMMENT '证明人单位ID',
`certifierUnitName` varchar(100) DEFAULT NULL COMMENT '证明人单位',
`handlerUserId` varchar(32) DEFAULT NULL COMMENT '经办人ID',
`handlerUserName` varchar(100) DEFAULT NULL COMMENT '经办人姓名',
`handlerLoginName` varchar(120) DEFAULT NULL COMMENT '经办人工号',
`handlerUnionId` varchar(32) DEFAULT NULL COMMENT '经办人分工会ID',
`handlerUnionName` varchar(100) DEFAULT NULL COMMENT '经办人分工会',
`handlerUnitId` varchar(32) DEFAULT NULL COMMENT '经办人单位ID',
`handlerUnitName` varchar(100) DEFAULT NULL COMMENT '经办人单位',
`hospitalizationTime` varchar(30) DEFAULT NULL COMMENT '入院时间',
`leaveHospitalTime` varchar(30) DEFAULT NULL COMMENT '出院时间',
`thisYearHospitalizationNum` int DEFAULT NULL COMMENT '当年第几次住院',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间(毫秒时间戳)',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间(毫秒时间戳)',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特殊慰问申请';
CREATE TABLE `special_condolence_type` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`code` varchar(30) DEFAULT NULL COMMENT '编码',
`name` varchar(50) DEFAULT NULL COMMENT '名称',
`money` float(10,2) DEFAULT NULL COMMENT '费用',
`way` varchar(20) DEFAULT NULL COMMENT '慰问方式',
`enable` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`isUploadFile` tinyint(1) DEFAULT NULL COMMENT '是否上传附件',
`uploadFileDesc` varchar(50) DEFAULT NULL COMMENT '上传附件说明',
`location` int DEFAULT NULL COMMENT '排序字段',
`sortNum` int NOT NULL DEFAULT 0 COMMENT '排序',
`uploadMeetingRecord` tinyint(1) DEFAULT 0 COMMENT '上传会议纪要',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间(毫秒时间戳)',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间(毫秒时间戳)',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特殊慰问类型';
-- 一份申请对应一个流程实例;多名被申请人通过 applyId 关联同一份申请。
CREATE TABLE `special_condolence_recipient` (
`id` varchar(32) NOT NULL COMMENT '明细ID',
`applyId` varchar(32) NOT NULL COMMENT '申请主表ID',
`helpUserId` varchar(32) NOT NULL COMMENT '慰问对象用户ID',
`helpLoginName` varchar(120) DEFAULT NULL COMMENT '慰问对象工号',
`personType` varchar(50) DEFAULT NULL COMMENT '人员类型',
`helpUserName` varchar(100) NOT NULL COMMENT '慰问对象',
`type` varchar(32) NOT NULL COMMENT '慰问类型ID',
`typeName` varchar(50) DEFAULT NULL COMMENT '慰问类型名称',
`typeCode` varchar(32) DEFAULT NULL COMMENT '慰问类型编码',
`way` varchar(20) DEFAULT NULL COMMENT '慰问方式',
`idCard` varchar(18) NOT NULL COMMENT '身份证号码',
`bankOfDeposit` varchar(100) NOT NULL COMMENT '开户行',
`bankUserName` varchar(100) NOT NULL COMMENT '户名',
`bankCardNumber` varchar(30) NOT NULL COMMENT '收款账户',
`money` decimal(10,2) NOT NULL COMMENT '慰问金额',
`occurTime` varchar(30) NOT NULL COMMENT '慰问时间',
`sortNum` int NOT NULL DEFAULT 0 COMMENT '行序号',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间(毫秒时间戳)',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间(毫秒时间戳)',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_special_condolence_recipient_apply` (`applyId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特殊慰问被申请人明细';
-- 主表保留原有摘要字段兼容现有列表,money 为整份申请的金额合计。
-- 被申请人的完整信息保存在 special_condolence_recipient 中,不在主表使用 JSON 存储。
-- 新增 PC、H5 菜单,角色授权由管理员另行配置。
START TRANSACTION;
CREATE TEMPORARY TABLE special_condolence_menu_map AS
SELECT id oldId, MD5(CONCAT('special_condolence:', id)) newId
FROM sys_menu
WHERE id IN ('3b49290f7db44130b9f6394a8212321f', '024811fd168e44948e6ab84911fc26bb')
OR (href LIKE '/platform/condolence/%' AND href NOT LIKE '%/branchUnionApproval%');
CREATE TEMPORARY TABLE special_condolence_parent_map (
oldId varchar(32) NOT NULL COMMENT '原父菜单ID',
newId varchar(32) NOT NULL COMMENT '特殊慰问父菜单ID',
PRIMARY KEY (oldId)
);
INSERT INTO special_condolence_parent_map (oldId, newId)
SELECT oldId, newId FROM special_condolence_menu_map;
INSERT INTO sys_menu
(id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName,
isRecommendApp, isRecommendService)
SELECT map.newId, COALESCE(parent.newId, m.parentId),
CONCAT('T', SUBSTRING(map.newId, 1, 7)), REPLACE(m.name, '职工慰问', '特殊慰问'),
m.aliasName, m.type, REPLACE(m.href, '/condolence/', '/special_condolence/'), m.target,
m.icon, m.showit, m.disabled,
CASE m.id WHEN '3b49290f7db44130b9f6394a8212321f' THEN 'special_condolence'
WHEN '024811fd168e44948e6ab84911fc26bb' THEN 'h5.special_condolence'
ELSE REPLACE(m.permission, 'condolence', 'special_condolence') END,
REPLACE(m.note, '职工慰问', '特殊慰问'), m.location, m.hasChildren,
m.createdBy, UNIX_TIMESTAMP()*1000, m.updatedBy, UNIX_TIMESTAMP()*1000, m.delFlag,
REPLACE(m.processKey, 'ZGWW', 'TSWW'), m.platform, m.moduleId, m.isQuickEntry,
m.picIcon, IF(m.name = '职工慰问', 't', m.initialPinyinName), m.isRecommendApp, m.isRecommendService
FROM sys_menu m JOIN special_condolence_menu_map map ON map.oldId = m.id
LEFT JOIN special_condolence_parent_map parent ON parent.oldId = m.parentId
WHERE NOT EXISTS (SELECT 1 FROM sys_menu existing WHERE existing.id = map.newId);
-- 父子菜单路径保持树形前缀关系。
UPDATE sys_menu child JOIN sys_menu parent ON parent.id = child.parentId
JOIN special_condolence_menu_map map ON map.newId = child.id
SET child.path = CONCAT(parent.path, RIGHT(child.id, 4));
DROP TEMPORARY TABLE special_condolence_menu_map;
DROP TEMPORARY TABLE special_condolence_parent_map;
-- 权限部分,不要删除
-- SYSADMIN
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '4c46696d75ff0eddf5cb57fac3e5d06d');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '3e97a91e7aa9566cdae94850038a6212'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '6c5be59ffb8d7c524ec72f2a02d6c6d2'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '7f66eb63b6ebd81c5eee9c5c2de2ed98'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '9c37242f50c5fb4a9c1b2b9b9dd0e2fd'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '9f4f850904fe751db1cd13977374f99b'),
('7f4a820d5a394bb8a0a92d6699afe7c8', 'b24a5bef9ccbf7241c5bb85e80352f8f');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '2145812145ee6548552504b4f5a55ff7');
INSERT INTO sys_role_menu (roleId, menuId)
VALUES ('7f4a820d5a394bb8a0a92d6699afe7c8', '42b4b5f54593cf023961ad1c9ce36f53'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '4f3212a13a54fbb2a082ee77651c9a73'),
('7f4a820d5a394bb8a0a92d6699afe7c8', '739f64c71fe776dc3520ebb2977c2d68'),
('7f4a820d5a394bb8a0a92d6699afe7c8', 'b6c725beaf3bfe9f2b29f46855c45a24'),
('7f4a820d5a394bb8a0a92d6699afe7c8', 'eb3d1b27d51b84df1cebf24e1278e26c');
-- 校工会办公室主任
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('494ac71eb8b44d0e99e19b7fde6bd469', '4c46696d75ff0eddf5cb57fac3e5d06d'),
('494ac71eb8b44d0e99e19b7fde6bd469', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('494ac71eb8b44d0e99e19b7fde6bd469', '7f66eb63b6ebd81c5eee9c5c2de2ed98');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('494ac71eb8b44d0e99e19b7fde6bd469', '2145812145ee6548552504b4f5a55ff7'),
('494ac71eb8b44d0e99e19b7fde6bd469', '42b4b5f54593cf023961ad1c9ce36f53');
-- 校工会主席
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('6aece46b946e4fb9ac4cb15c0cd8c3e6', '4c46696d75ff0eddf5cb57fac3e5d06d'),
('6aece46b946e4fb9ac4cb15c0cd8c3e6', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'b24a5bef9ccbf7241c5bb85e80352f8f');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('6aece46b946e4fb9ac4cb15c0cd8c3e6', '2145812145ee6548552504b4f5a55ff7'),
('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'b6c725beaf3bfe9f2b29f46855c45a24');
-- 校工会主席
-- 特殊慰问 web
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('b3854469e6f44901b22bb791cb538431', '4c46696d75ff0eddf5cb57fac3e5d06d'),
('b3854469e6f44901b22bb791cb538431', '8d5ebaa091eee01abb33e89e3dcc92f8'),
('b3854469e6f44901b22bb791cb538431', '9c37242f50c5fb4a9c1b2b9b9dd0e2fd');
-- 特殊慰问 h5
INSERT INTO sys_role_menu (roleId, menuId)
VALUES
('b3854469e6f44901b22bb791cb538431', '2145812145ee6548552504b4f5a55ff7'),
('b3854469e6f44901b22bb791cb538431', '739f64c71fe776dc3520ebb2977c2d68');
COMMIT;
@@ -0,0 +1,7 @@
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName, isRecommendApp, isRecommendService) VALUES ('184d56fd537041caa2bde800d0d32436', '90bf20d9065e4e11864952ea18544c25', '009000040003', '个人范围', null, 'data', null, null, null, false, false, 'self', null, 1027, false, '17a7f8ad3ee947b4a26175049a9c253d', 1793418331288, '17a7f8ad3ee947b4a26175049a9c253d', 1793418331288, false, null, 'PC', null, false, null, null, false, false);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName, isRecommendApp, isRecommendService) VALUES ('df87d83ed3184019b3872139c15b53ec', '90bf20d9065e4e11864952ea18544c25', '009000040002', '分工会范围', null, 'data', null, null, null, false, false, 'branch_union', null, 1026, false, '17a7f8ad3ee947b4a26175049a9c253d', 1793418331286, '17a7f8ad3ee947b4a26175049a9c253d', 1793418331286, false, null, 'PC', null, false, null, null, false, false);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, processKey, platform, moduleId, isQuickEntry, picIcon, initialPinyinName, isRecommendApp, isRecommendService) VALUES ('f2a75d2494724978b5325075815808c9', '90bf20d9065e4e11864952ea18544c25', '009000040001', '全校范围', null, 'data', null, null, null, false, false, 'all_school', null, 1025, false, '17a7f8ad3ee947b4a26175049a9c253d', 1793418331280, '17a7f8ad3ee947b4a26175049a9c253d', 1793418331280, false, null, 'PC', null, false, null, null, false, false);
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('fd753e8a8c5a404e8e00ef8e0b0c69da', 'df87d83ed3184019b3872139c15b53ec');
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('494ac71eb8b44d0e99e19b7fde6bd469', 'f2a75d2494724978b5325075815808c9');
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('6aece46b946e4fb9ac4cb15c0cd8c3e6', 'f2a75d2494724978b5325075815808c9');
INSERT INTO sys_role_menu (roleId, menuId) VALUES ('b3854469e6f44901b22bb791cb538431', 'f2a75d2494724978b5325075815808c9');
+89 -9
View File
@@ -1,17 +1,85 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="false" scanPeriod="60000" debug="false"> <configuration scan="false" scanPeriod="60000" debug="false">
<!-- 定义日志文件的存储路径 --> <!-- 日志存储路径 -->
<property name="LOG_HOME" value="./logs"/> <property name="LOG_HOME" value="./logs"/>
<!-- 控制台输出配置 --> <!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<!-- INFO级别日志文件配置 -->
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)start #
# #
#################################################################################
-->
<!-- 所有级别日志写入同一个文件 -->
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<!-- 框架日志级别(按需调整) -->
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/>
<!-- root logger:DEBUG 级别,输出到控制台和全量文件 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ALL_FILE"/>
</root>
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)end #
# #
#################################################################################
-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 start #
# #
#################################################################################
-->
<!-- DEBUG级别日志文件配置 -->
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
&lt;!&ndash; INFO级别日志文件配置 &ndash;&gt;
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file> <file>${LOG_HOME}/info.log</file>
<encoder> <encoder>
@@ -24,11 +92,11 @@
</filter> </filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <maxHistory>300</maxHistory>
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<!-- ERROR级别日志文件配置 --> &lt;!&ndash; ERROR级别日志文件配置 &ndash;&gt;
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file> <file>${LOG_HOME}/error.log</file>
<encoder> <encoder>
@@ -41,19 +109,31 @@
</filter> </filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <maxHistory>300</maxHistory>
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<logger name="java" additivity="false" /> &lt;!&ndash; 框架日志级别 &ndash;&gt;
&lt;!&ndash; <logger name="java" additivity="false" />&ndash;&gt;
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/> <logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/> <logger name="org.nutz" level="DEBUG"/>
<!-- 日志级别和appender的关联 --> &lt;!&ndash; 日志级别和appender的关联 &ndash;&gt;
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/> <appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/> <appender-ref ref="ERROR_FILE"/>
</root> </root>-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 end #
# #
#################################################################################
-->
</configuration> </configuration>
@@ -0,0 +1 @@
7pKr23u2tZHcvXud
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
<template> <template>
<div class="signature-wrap"> <div class="signature-wrap">
<canvas id="canvas"></canvas> <canvas ref="canvas" @touchstart.stop @touchmove.stop></canvas>
<div :class="isLikelyMobile() ? 'action-buttons' : 'action-buttons pc-action-buttons'"> <div :class="isLikelyMobile() ? 'action-buttons' : 'action-buttons pc-action-buttons'">
<button @click="clear" class="action-button-danger" type="button">清空</button> <button @click="clear" class="action-button-danger" type="button">清空</button>
<button @click="undo" class="action-button-warning" type="button">撤销</button> <button @click="undo" class="action-button-warning" type="button">撤销</button>
@@ -30,23 +30,23 @@
// window.addEventListener("orientationchange", a) // window.addEventListener("orientationchange", a)
// a() // a()
let signature = null
module.exports = { module.exports = {
name: "sysSignature", name: "sysSignature",
data() { data() {
return { return {
content: null content: null,
signature: null
} }
}, },
methods: { methods: {
clear() { clear() {
signature.clear() this.signature.clear()
}, },
undo() { undo() {
signature.undo() this.signature.undo()
}, },
rotate() { rotate() {
signature.getRotateCanvas(90) this.signature.getRotateCanvas(90)
}, },
isLikelyMobile() { isLikelyMobile() {
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
@@ -56,7 +56,7 @@ module.exports = {
return hasMobileUA || (isTouchDevice && isSmallScreen); return hasMobileUA || (isTouchDevice && isSmallScreen);
}, },
save() { save() {
if (signature.isEmpty()) { if (this.signature.isEmpty()) {
// H5 页面使用统一的 Vant 提示;未加载 Vant 的独立签字页保留原生提示作为兼容兜底。 // H5 页面使用统一的 Vant 提示;未加载 Vant 的独立签字页保留原生提示作为兼容兜底。
if (typeof this.$toast === "function") { if (typeof this.$toast === "function") {
this.$toast({ this.$toast({
@@ -71,11 +71,11 @@ module.exports = {
} }
// const png = signature.getPNG() // const png = signature.getPNG()
//旋转一下 横过来 //旋转一下 横过来
const rotateCanvas = signature.getRotateCanvas(-90) const rotateCanvas = this.signature.getRotateCanvas(-90)
if (this.isLikelyMobile()) { if (this.isLikelyMobile()) {
this.$emit("save", rotateCanvas.toDataURL()) this.$emit("save", rotateCanvas.toDataURL())
} else { } else {
this.$emit("save", signature.toDataURL()) this.$emit("save", this.signature.toDataURL())
} }
} }
}, },
@@ -83,10 +83,10 @@ module.exports = {
const innerWidth = window.innerWidth const innerWidth = window.innerWidth
const innerHeight = window.innerHeight const innerHeight = window.innerHeight
const canvas = document.getElementById("canvas") const canvas = this.$refs.canvas
canvas.width = window.innerWidth canvas.width = window.innerWidth
canvas.height = window.innerHeight canvas.height = window.innerHeight
signature = new SmoothSignature(canvas, { this.signature = new SmoothSignature(canvas, {
width: innerWidth - 30, width: innerWidth - 30,
height: innerHeight - 30, height: innerHeight - 30,
scale: 2, scale: 2,
@@ -95,6 +95,15 @@ module.exports = {
color: "#000000" color: "#000000"
// bgColor: "#f6f6f6" // bgColor: "#f6f6f6"
}) })
canvas.addEventListener("touchstart", this.signature.onDrawStart, { passive: false })
canvas.addEventListener("touchmove", this.signature.onDrawMove, { passive: false })
document.addEventListener("touchend", this.signature.onDrawEnd, { passive: false })
document.addEventListener("touchcancel", this.signature.onDrawEnd, { passive: false })
},
beforeDestroy() {
if (this.signature) {
this.signature.removeListener()
}
} }
} }
</script> </script>
@@ -35,22 +35,58 @@ module.exports = {
}, },
data() { data() {
return { return {
//前端唯一标识 当然后端还有用户id作为唯一标识 // 二维码携带本次组件实例的标识,与服务端当前登录用户共同确定签字记录。
id: new Date().getTime() + Math.floor(Math.random() * 10000) + 1, id: new Date().getTime() + Math.floor(Math.random() * 10000) + 1,
signatureAddress: null, signatureAddress: null,
interval: null, interval: null,
connectionWaitResolve: null,
unsubscribeSignature: null,
signatureContent: null, signatureContent: null,
loading: true, loading: true,
} }
}, },
methods: { methods: {
async init() { /**
const {code, data} = await this.$axios.post("/platform/signature/get") * 无请求参数,返回 Promise;响应 code 为0表示成功,data.signature 为已有签字图片地址。
if (code === 0) { * 请求结束后由 mounted 的 finally 统一关闭 loading。
this.signatureContent = data && data.signature */
this.$emit("input", this.signatureContent) init() {
}
this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id this.signatureAddress = origin + "/platform/signature/scanPcCode?id=" + this.id
return this.$axios.post("/platform/signature/get")
.then((res) => {
if (this._isBeingDestroyed || this._isDestroyed) {
return
}
if (res.code !== 0) {
throw new Error("签字信息加载失败")
}
this.signatureContent = res.data && res.data.signature
this.$emit("input", this.signatureContent)
})
},
// 返回等待完成的 Promise;连接成功、等待30秒或组件销毁时结束等待。
waitForConnection() {
return new Promise((resolve) => {
const startedAt = Date.now()
this.connectionWaitResolve = resolve
const checkReady = () => {
this.interval = null
if (this._isBeingDestroyed || this._isDestroyed ||
(webSocketPubSub.ws && webSocketPubSub.ws.readyState === WebSocket.OPEN)) {
this.connectionWaitResolve = null
resolve()
return
}
if (Date.now() - startedAt >= 30000) {
this.connectionWaitResolve = null
this.$message.warning("签字消息连接超时,请刷新页面重试")
resolve()
return
}
this.interval = setTimeout(checkReady, 100)
}
checkReady()
})
}, },
signatureAgain() { signatureAgain() {
const val = sessionStorage.getItem("h5-scan-code-signature-" + this.id) const val = sessionStorage.getItem("h5-scan-code-signature-" + this.id)
@@ -58,25 +94,42 @@ module.exports = {
this.$emit("input", val) this.$emit("input", val)
} }
}, },
async mounted() { mounted() {
await this.init() this.loading = true
webSocketPubSub.subscribe("h5-scan-code-signature", (data) => { // 先登记消息回调,连接成功后由公共连接加入房间,避免请求期间遗漏订阅。
console.info("ws:收到签字成功消息:", data) this.unsubscribeSignature = webSocketPubSub.subscribe("h5-scan-code-signature", (data) => {
this.signatureContent = data.value this.signatureContent = data.value
this.$emit("input", data.value) this.$emit("input", data.value)
}) })
this.init()
// 可选:轮询或 watch ready 状态 .then(() => {
const checkReady = () => { if (!this._isBeingDestroyed && !this._isDestroyed) {
if (webSocketPubSub.ws && webSocketPubSub.ws.readyState === WebSocket.OPEN) { return this.waitForConnection()
console.info("ws.readyState is open"); }
this.loading = false; })
console.log(this.signatureAddress) .catch(() => {
} else { if (!this._isBeingDestroyed && !this._isDestroyed) {
setTimeout(checkReady, 100); this.$message.error("签字信息加载失败,请刷新页面重试")
} }
}; })
checkReady(); .finally(() => {
if (!this._isBeingDestroyed && !this._isDestroyed) {
this.loading = false
}
})
},
beforeDestroy() {
// pjax 切换页面时取消轮询和订阅,旧组件不得继续接收签字消息。
clearTimeout(this.interval)
this.interval = null
if (this.connectionWaitResolve) {
this.connectionWaitResolve()
this.connectionWaitResolve = null
}
if (this.unsubscribeSignature) {
this.unsubscribeSignature()
this.unsubscribeSignature = null
}
} }
} }
</script> </script>
@@ -9,6 +9,7 @@
progress progress
:accept="accept" :accept="accept"
:max-count="upload_number" :max-count="upload_number"
:upload-text="upload_text"
title="" title=""
description="" description=""
tip="" tip=""
@@ -50,6 +51,11 @@ module.exports = {
default: 1, default: 1,
required: false required: false
}, },
upload_text: {
type: String,
default: "",
required: false
},
// 上传返回id或url // 上传返回id或url
upload_result_type: { upload_result_type: {
type: String, type: String,
@@ -101,6 +107,7 @@ module.exports = {
this.fileList = val.map((v) => { this.fileList = val.map((v) => {
return { return {
...v, ...v,
file: v.file || { name: v.name },
status: null, status: null,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()) isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase())
} }
@@ -118,6 +125,7 @@ module.exports = {
this.fileList = val.map((v) => { this.fileList = val.map((v) => {
return { return {
...v, ...v,
file: v.file || { name: v.name },
url: v.url ? v.url : v.response?.data, url: v.url ? v.url : v.response?.data,
isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()), isImage: ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(v.name.split(".").pop().toLowerCase()),
status: null status: null
@@ -171,8 +179,8 @@ module.exports = {
f.url = resp.data f.url = resp.data
f.response = resp f.response = resp
f.percentage = 100 f.percentage = 100
f.isImage = true f.isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(f.name.split(".").pop().toLowerCase())
delete f.file f.file = { name: f.name }
delete f.content delete f.content
} else { } else {
f.status = "fail" f.status = "fail"
@@ -0,0 +1,98 @@
<template>
<div class="h5-user-select">
<van-field v-bind="$attrs" :value="displayName" readonly clickable is-link @click="open"></van-field>
<van-popup v-model="visible" position="bottom" round get-container="body" class="h5-user-select-popup">
<div class="h5-user-select-popup__header">
<van-button native-type="button" type="default" size="small" @click="visible = false">取消</van-button>
<strong>选择慰问对象</strong>
<van-button native-type="button" type="info" size="small" :disabled="!pendingUser" @click="confirm">确认</van-button>
</div>
<van-search v-model="keyword" placeholder="请输入姓名或工号查询" @input="scheduleSearch" @search="search" @clear="search"></van-search>
<div class="h5-user-select-popup__list">
<div v-if="loading" class="h5-user-select-popup__status"><van-loading size="24px">加载中</van-loading></div>
<div v-else-if="error" class="h5-user-select-popup__status"><span class="h5-user-select-popup__error">{{ error }}</span><van-button native-type="button" size="small" @click="search">重新加载</van-button></div>
<van-empty v-else-if="!options.length" description="未找到本工会人员"></van-empty>
<template v-else>
<van-cell v-for="person in options" :key="person.id" clickable
:class="{ 'h5-user-select-popup__selected': pendingUser && pendingUser.id === person.id }"
:title="person.userName + (person.loginName ? '(' + person.loginName + ')' : '')"
:label="[person.unitName, person.personType].filter(Boolean).join(' · ')"
@click="pendingUser = person">
<van-icon v-if="pendingUser && pendingUser.id === person.id" name="success" color="#1989fa"></van-icon>
</van-cell>
</template>
</div>
</van-popup>
</div>
</template>
<script>
module.exports = {
inheritAttrs: false,
model: { prop: 'value', event: 'change' },
props: {
value: { type: String, default: '' },
displayName: { type: String, default: '' },
api: { type: String, required: true }
},
data() {
return { visible: false, keyword: '', options: [], pendingUser: null, loading: false, error: '', searchTimer: null, requestId: 0 }
},
beforeDestroy() {
clearTimeout(this.searchTimer)
this.requestId++
},
methods: {
open() {
this.keyword = ''
this.pendingUser = null
this.visible = true
this.search()
},
scheduleSearch() {
clearTimeout(this.searchTimer)
this.requestId++
this.pendingUser = null
this.loading = true
this.searchTimer = setTimeout(() => this.search(), 250)
},
async search() {
clearTimeout(this.searchTimer)
const requestId = ++this.requestId
this.loading = true
this.error = ''
this.pendingUser = null
try {
const result = await this.$axios.post(this.api, { keyword: this.keyword })
if (requestId !== this.requestId) return
if (result.code !== 0) throw new Error(result.msg || '人员加载失败')
this.options = result.data || []
} catch (error) {
if (requestId !== this.requestId) return
this.options = []
this.error = error.message || '人员加载失败'
} finally {
if (requestId === this.requestId) this.loading = false
}
},
confirm() {
if (!this.pendingUser) return
const person = this.pendingUser
this.$emit('change', person.id)
this.$emit('selected', person, person.id)
this.visible = false
}
}
}
</script>
<style>
.h5-user-select-popup { display: flex; flex-direction: column; height: 72vh; max-height: 640px; padding-bottom: env(safe-area-inset-bottom); box-sizing: border-box; }
.h5-user-select-popup__header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 14px 16px; border-bottom: 1px solid #ebedf0; }
.h5-user-select-popup__header strong { font-size: 16px; color: #323233; }
.h5-user-select-popup__header .van-button { min-width: 56px; }
.h5-user-select-popup__list { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.h5-user-select-popup__selected { background: #edf6ff; }
.h5-user-select-popup__status { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 12px; padding: 40px 16px; }
.h5-user-select-popup__error { color: #ee0a24; }
</style>
@@ -106,7 +106,9 @@ module.exports = {
} }
}, },
onChange(val) { onChange(val) {
const selected = this.options.find(item => String(item[this.option_value]) === String(val)) || null
this.$emit("change", val) this.$emit("change", val)
this.$emit("selected", selected, val)
}, },
clearOptions() { clearOptions() {
@@ -114,7 +116,7 @@ module.exports = {
} }
}, },
created() { created() {
this.clearOptions() this.options = this.option_list || []
console.log(this.placeholder) console.log(this.placeholder)
} }
} }
+142 -86
View File
@@ -443,50 +443,76 @@
constructor() { constructor() {
this.ws = null this.ws = null
this.subscribers = new Map() this.subscribers = new Map()
this.isSubscribed = false // 添加订阅状态标记 this.isSubscribed = false
this.connectTimer = null
this.reconnectTimer = null
this.pingTimer = null
this.isDisposed = false
this.init() this.init()
} }
// 页面只创建一个心跳任务;每次连接自行绑定事件及握手超时。
init() { init() {
this.setupWindowEvents()
this.setupPing()
this.connect()
}
// 正在连接或关闭时等待对应事件,避免同时创建多个连接。
connect() {
if (this.isDisposed || this.reconnectTimer !== null ||
(this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
return
}
try { try {
this.connect() const WS_URL = window.location.host + "${base}/websocket"
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
this.ws = new WebSocket(protocol + WS_URL)
this.isSubscribed = false
this.setupEventListeners() this.setupEventListeners()
this.setupPing() const socket = this.ws
this.setupWindowEvents() // 握手最多等待30秒,心跳不得提前关闭仍处于 CONNECTING 的连接。
} catch (ex) { this.connectTimer = setTimeout(() => {
console.info(ex) this.connectTimer = null
if (this.ws === socket && socket.readyState === WebSocket.CONNECTING) {
console.warn("WebSocket 握手超时,关闭当前连接后重试", socket.url)
socket.close()
}
}, 30000)
} catch (error) {
console.error("WebSocket 创建连接失败", error)
this.reconnect()
} }
} }
connect() { /**
const WS_URL = window.location.host + "${base}/websocket" * type 为消息 action 名称,callback 接收服务端消息对象;返回取消本次订阅的方法。
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://" * 连接尚未建立时先保留回调,在 onopen 中统一加入当前用户的消息房间。
this.ws = new WebSocket(protocol + WS_URL) */
}
//订阅
subscribe(type, callback) { subscribe(type, callback) {
if (!this.subscribers.has(type)) { if (!this.subscribers.has(type)) {
this.subscribers.set(type, new Set()) this.subscribers.set(type, new Set())
} }
this.subscribers.get(type).add(callback) const callbacks = this.subscribers.get(type)
callbacks.add(callback)
setTimeout(() => { this.sendJoinMessage()
// 所有订阅完成后,发送 join 消息 return () => {
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) { callbacks.delete(callback)
this.isSubscribed = true if (callbacks.size === 0 && this.subscribers.get(type) === callbacks) {
this.sendJoinMessage() this.subscribers.delete(type)
} }
}, 2000) }
} }
// 事件处理 // 事件绑定到本次连接,旧连接的延迟事件不得影响新连接。
setupEventListeners() { setupEventListeners() {
// 消息处理 const socket = this.ws
this.ws.onmessage = (event) => { socket.onmessage = (event) => {
if (this.isDisposed || this.ws !== socket) {
return
}
try { try {
const data = JSON.parse(event.data) const data = JSON.parse(event.data)
// 触发订阅者的回调
if (this.subscribers.has(data.action)) { if (this.subscribers.has(data.action)) {
this.subscribers.get(data.action).forEach((callback) => { this.subscribers.get(data.action).forEach((callback) => {
try { try {
@@ -500,82 +526,112 @@
console.error("消息解析错误:", error) console.error("消息解析错误:", error)
} }
} }
socket.onopen = () => {
// 连接建立 if (this.isDisposed || this.ws !== socket) {
this.ws.onopen = () => { return
// this.ws.send(
// JSON.stringify({
// room: store.state.room,
// action: "join"
// })
// )
// 连接建立时不立即发送 join,等待订阅完成
this.ping()
}
}
// 新增发送 join 消息的方法
sendJoinMessage() {
this.send({
room: store.state.room,
action: "join"
})
}
// 心跳检测
setupPing() {
setInterval(() => this.ping(), (11624317 / 1000) * 2)
}
ping() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send("{}")
} catch (e) {
this.reconnect()
} }
} else { clearTimeout(this.connectTimer)
this.connectTimer = null
this.sendJoinMessage()
this.ping()
console.info("WebSocket 连接已建立", socket.url)
}
socket.onerror = () => {
if (!this.isDisposed && this.ws === socket) {
console.warn("WebSocket 连接异常,等待关闭后重连", socket.url)
}
}
socket.onclose = (event) => {
if (this.isDisposed || this.ws !== socket) {
return
}
clearTimeout(this.connectTimer)
this.connectTimer = null
this.isSubscribed = false
console.warn("WebSocket 已关闭", event.code, event.reason)
this.reconnect() this.reconnect()
} }
} }
// 每个已建立的连接只加入一次房间,首次连接及断线恢复都补发已有订阅。
sendJoinMessage() {
if (this.isDisposed || this.isSubscribed || this.subscribers.size === 0 ||
!window.store || !store.state.room || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
return
}
this.send({
room: store.state.room,
action: "join"
})
this.isSubscribed = true
}
// 沿用原心跳间隔,仅对已建立的连接发送心跳。
setupPing() {
this.pingTimer = setInterval(() => this.ping(), (11624317 / 1000) * 2)
}
// CONNECTING/CLOSING 状态分别由握手超时和关闭事件处理。
ping() {
if (this.isDisposed) {
return
}
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send("{}")
} catch (error) {
console.warn("WebSocket 心跳发送失败,等待关闭后重连", error)
this.ws.close()
}
} else if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
this.reconnect()
}
}
// 关闭事件和心跳共用一个重连任务,避免重复定时器创建并行连接。
reconnect() { reconnect() {
if (this.ws) { if (this.isDisposed || this.reconnectTimer !== null ||
this.ws.close() // 确保旧连接已关闭 (this.ws && this.ws.readyState !== WebSocket.CLOSED)) {
return
} }
setTimeout(() => { this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null
this.connect() this.connect()
this.setupEventListeners() }, 3000)
// 如果之前已订阅,重新发送join消息
if (this.isSubscribed) {
setTimeout(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.sendJoinMessage()
}
}, 1000)
}
}, 3000) // 延迟3秒重连
} }
// 窗口事件处理 // 真正离开页面时释放连接和定时器;进入往返缓存时保留页面状态。
setupWindowEvents() { setupWindowEvents() {
let _beforeUnload_time = 0 window.addEventListener("pagehide", (event) => {
window.onbeforeunload = () => { if (event.persisted) {
_beforeUnload_time = new Date().getTime() return
}
window.onunload = () => {
const _gap_time = new Date().getTime() - _beforeUnload_time
if (_gap_time <= 5) {
this.send({
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
action: "left"
})
} }
} this.isDisposed = true
clearTimeout(this.connectTimer)
clearTimeout(this.reconnectTimer)
clearInterval(this.pingTimer)
this.connectTimer = null
this.reconnectTimer = null
this.pingTimer = null
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.send({
room: store.state.room,
action: "left"
})
} catch (error) {
console.warn("WebSocket 离开房间消息发送失败", error)
}
}
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
this.ws.close()
}
})
} }
// message 为消息对象或 JSON 字符串;连接未建立时不发送,返回值为 undefined。
send(message) { send(message) {
if (this.ws) { if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const messageStr = typeof message === "string" ? message : JSON.stringify(message) const messageStr = typeof message === "string" ? message : JSON.stringify(message)
this.ws.send(messageStr) this.ws.send(messageStr)
} }

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