feat: 同步地大代码
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.budwk</groupId>
|
<groupId>com.budwk</groupId>
|
||||||
<artifactId>zhgh_zjnu</artifactId>
|
<artifactId>mini</artifactId>
|
||||||
<version>5.6.0-plus</version>
|
<version>5.6.0-plus</version>
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import com.budwk.app.web.commons.base.Globals;
|
|||||||
import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
|
import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
|
||||||
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
|
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
|
||||||
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
|
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.beetl.core.GroupTemplate;
|
import org.beetl.core.GroupTemplate;
|
||||||
import org.nutz.boot.NbApp;
|
import org.nutz.boot.NbApp;
|
||||||
@@ -81,6 +83,15 @@ public class MainLauncher {
|
|||||||
Globals.AppBase = Mvcs.getServletContext().getContextPath();
|
Globals.AppBase = Mvcs.getServletContext().getContextPath();
|
||||||
Globals.AppRoot = Mvcs.getServletContext().getRealPath("/");
|
Globals.AppRoot = Mvcs.getServletContext().getRealPath("/");
|
||||||
init_sys();
|
init_sys();
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:先升级会员表及补齐历史资料,再处理缴费快照和启动计划任务;不依赖 DEBUG。
|
||||||
|
SysClubUserService members = ioc.get(SysClubUserService.class, "sysClubUserServiceImpl");
|
||||||
|
members.upgradeMemberSchema();
|
||||||
|
members.backfillMemberSnapshots();
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:缴费表升级独立于 DEBUG,先完成字段检查和旧数据补齐再启动任务。
|
||||||
|
// 2026年10月3日 09:28:37;操作人:wxg:按实际注册的实现类 Bean 名称获取服务,避免接口默认名称不存在导致启动失败。
|
||||||
|
ClubPayRecordService payRecords = ioc.get(ClubPayRecordService.class, "clubPayRecordServiceImpl");
|
||||||
|
payRecords.upgradePayRecordSchema();
|
||||||
|
payRecords.backfillPayRecordSnapshots();
|
||||||
init_task();
|
init_task();
|
||||||
init_auth();
|
init_auth();
|
||||||
ioc.get(Globals.class);
|
ioc.get(Globals.class);
|
||||||
|
|||||||
@@ -243,6 +243,27 @@ public class FlowDesignController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置流程设计在流程中心的展示状态。
|
||||||
|
* @param id 流程设计 ID
|
||||||
|
* @param centerEnabled 传 1 表示显示,传 0 表示隐藏
|
||||||
|
* @return Result;code 为 0 表示保存成功,非 0 时 msg 说明错误,data 无业务数据
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@ApiOperation("设置流程中心显示状态")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@SaCheckPermission("flow.design")
|
||||||
|
public Result updateCenterEnabled(@Param("id") Long id, @Param("centerEnabled") Integer centerEnabled) {
|
||||||
|
if (id == null || centerEnabled == null || (centerEnabled != 0 && centerEnabled != 1)) {
|
||||||
|
return Result.error("流程设计或显示状态无效");
|
||||||
|
}
|
||||||
|
int updated = processDesignService.update(Chain.make("centerEnabled", centerEnabled), Cnd.where(ProcessDesign::getId, "=", id));
|
||||||
|
if (updated == 0 && processDesignService.fetch(id) == null) {
|
||||||
|
return Result.error("流程设计不存在");
|
||||||
|
}
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
@ApiOperation("获取流程设计任务参与者处理类")
|
@ApiOperation("获取流程设计任务参与者处理类")
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ public class ProcessDesign extends BaseModel {
|
|||||||
@Column
|
@Column
|
||||||
private Integer isDeployed;
|
private Integer isDeployed;
|
||||||
|
|
||||||
|
@Comment("是否在流程中心显示(1显示,0隐藏)")
|
||||||
|
@Column
|
||||||
|
@Default("1")
|
||||||
|
private Integer centerEnabled;
|
||||||
|
|
||||||
@Comment("排序编码")
|
@Comment("排序编码")
|
||||||
@Column
|
@Column
|
||||||
@Default("0")
|
@Default("0")
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class SysLoginController {
|
|||||||
private static final Log log = Logs.get();
|
private static final Log log = Logs.get();
|
||||||
private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing();
|
private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing();
|
||||||
/**
|
/**
|
||||||
* TODO: 临时屏蔽 localhost 平台登录验证码,恢复时改为 false。
|
* TODO: 临时屏蔽回环地址及指定局域网 IP 的平台登录验证码,全部恢复时改为 false。
|
||||||
*/
|
*/
|
||||||
private static final boolean DISABLE_LOGIN_CAPTCHA = true;
|
private static final boolean DISABLE_LOGIN_CAPTCHA = true;
|
||||||
@Inject
|
@Inject
|
||||||
@@ -114,14 +114,15 @@ public class SysLoginController {
|
|||||||
return Result.error("参数缺失");
|
return Result.error("参数缺失");
|
||||||
}
|
}
|
||||||
|
|
||||||
String lockKey = RedisConstant.USER_LOGIN_LOCK_PREFIX + username;
|
// TODO: 临时停用所有账号的登录失败次数和 5 分钟锁定限制;恢复时同步取消下方失败计数写入的注释。
|
||||||
int errCount = Convert.toInt(StrUtil.blankToDefault(redisService.get(lockKey), "0"));
|
// String lockKey = RedisConstant.USER_LOGIN_LOCK_PREFIX + username;
|
||||||
log.info("用户名:" + username + "登录失败次数:" + errCount);
|
// int errCount = Convert.toInt(StrUtil.blankToDefault(redisService.get(lockKey), "0"));
|
||||||
|
// log.info("用户名:" + username + "登录失败次数:" + errCount);
|
||||||
|
|
||||||
if (errCount > 5) {
|
// if (errCount > 5) {
|
||||||
redisService.setex(lockKey, 5 * 60, String.valueOf(errCount + 1));
|
// redisService.setex(lockKey, 5 * 60, String.valueOf(errCount + 1));
|
||||||
return Result.error("登录失败次数过多,请5分钟后再试");
|
// return Result.error("登录失败次数过多,请5分钟后再试");
|
||||||
}
|
// }
|
||||||
try {
|
try {
|
||||||
// 验证码校验
|
// 验证码校验
|
||||||
if (!isLoginCaptchaDisabled(req)) {
|
if (!isLoginCaptchaDisabled(req)) {
|
||||||
@@ -154,7 +155,8 @@ public class SysLoginController {
|
|||||||
return Result.success("login.success");
|
return Result.success("login.success");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
redisService.set(lockKey, Convert.toStr(errCount + 1));
|
// TODO: 临时暂停累计登录失败次数;恢复限制时与上方计数读取、锁定拦截一并恢复。
|
||||||
|
// redisService.set(lockKey, Convert.toStr(errCount + 1));
|
||||||
String message = e.getMessage();
|
String message = e.getMessage();
|
||||||
return Result.error(e.getMessage());
|
return Result.error(e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -165,7 +167,9 @@ public class SysLoginController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String serverName = req.getServerName();
|
String serverName = req.getServerName();
|
||||||
|
// 临时允许该局域网地址免验证码;恢复 IP 验证码时删除此条件,并同步删除登录页中的该 IP。
|
||||||
return "localhost".equalsIgnoreCase(serverName)
|
return "localhost".equalsIgnoreCase(serverName)
|
||||||
|
|| "192.168.0.108".equals(serverName)
|
||||||
|| "127.0.0.1".equals(serverName)
|
|| "127.0.0.1".equals(serverName)
|
||||||
|| "0:0:0:0:0:0:0:1".equals(serverName)
|
|| "0:0:0:0:0:0:0:1".equals(serverName)
|
||||||
|| "::1".equals(serverName);
|
|| "::1".equals(serverName);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.nutz.mvc.annotation.At;
|
|||||||
import org.nutz.mvc.annotation.GET;
|
import org.nutz.mvc.annotation.GET;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
@@ -499,6 +500,24 @@ public class SysRoleController {
|
|||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为全部 member=1 的用户设置会员角色,无需传入用户 ID、角色 ID 或列表筛选参数。
|
||||||
|
*
|
||||||
|
* @return JSON Result,data.addedCount 为新增关联数,data.enabledCount 为恢复启用关联数
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@Ok("json")
|
||||||
|
@SaCheckPermission("sys.manager.role.user")
|
||||||
|
@SLog(tag = "一键设置会员角色", msg = "为全部member=1的人员补齐会员角色")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result doSettingMemberRole() {
|
||||||
|
NutMap counts = sysRoleService.settingMemberRole();
|
||||||
|
sysRoleService.clearCache();
|
||||||
|
sysUserService.clearCache();
|
||||||
|
return Result.success("会员角色设置完成", counts);
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
@SaCheckPermission("sys.manager.role")
|
@SaCheckPermission("sys.manager.role")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.nutz.dao.Cnd;
|
|||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.Static;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -57,6 +58,8 @@ public class SysV4ServController {
|
|||||||
$condition
|
$condition
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
|
// 只隐藏流程中心入口;历史流程实例及已有流程定义保持原有状态。
|
||||||
|
cnd.and(new Static("COALESCE(d.centerEnabled, 1) = 1"));
|
||||||
if (StrUtil.isNotBlank(keyword)) {
|
if (StrUtil.isNotBlank(keyword)) {
|
||||||
cnd.where().andLike("t.disPlayName", keyword);
|
cnd.where().andLike("t.disPlayName", keyword);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,11 @@ public class Sys_user extends BaseModel implements Serializable {
|
|||||||
@ColDefine(type = ColType.DATE)
|
@ColDefine(type = ColType.DATE)
|
||||||
private Date retireDate;
|
private Date retireDate;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("福利享受截止时间")
|
||||||
|
@ColDefine(type = ColType.DATE)
|
||||||
|
private Date welfareStopDate;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("博士后进站时间")
|
@Comment("博士后进站时间")
|
||||||
@ColDefine(type = ColType.DATE)
|
@ColDefine(type = ColType.DATE)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.budwk.app.base.service.BaseService;
|
|||||||
import com.budwk.app.sys.models.Sys_menu;
|
import com.budwk.app.sys.models.Sys_menu;
|
||||||
import com.budwk.app.sys.models.Sys_role;
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
import com.budwk.app.sys.models.Sys_unit;
|
import com.budwk.app.sys.models.Sys_unit;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -128,6 +129,13 @@ public interface SysRoleService extends BaseService<Sys_role> {
|
|||||||
*/
|
*/
|
||||||
List<String> getRoleIdsByCode(List<RoleConstant> roleConstantList);
|
List<String> getRoleIdsByCode(List<RoleConstant> roleConstantList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为用户表中 member=1 的全部人员补齐启用的 MEMBER 角色,不受列表筛选影响。
|
||||||
|
*
|
||||||
|
* @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数;已有启用关联不重复添加
|
||||||
|
*/
|
||||||
|
NutMap settingMemberRole();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空缓存
|
* 清空缓存
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
|||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
||||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -161,10 +160,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
for (Sys_user_source source : sources) {
|
for (Sys_user_source source : sources) {
|
||||||
Sys_user user = userMap.get(source.getLoginname());
|
Sys_user user = userMap.get(source.getLoginname());
|
||||||
source.setMember(null);
|
source.setMember(null);
|
||||||
String unitId = source.getUnitId();
|
|
||||||
if (StringUtils.isNotBlank(unitId) && unitId.length() == 5) {
|
|
||||||
source.setUnitId(unitId.substring(0, 3));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建用户对象
|
// 创建用户对象
|
||||||
Sys_user u = new Sys_user();
|
Sys_user u = new Sys_user();
|
||||||
@@ -180,12 +175,15 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
} else {
|
} else {
|
||||||
// 修改现有用户
|
// 修改现有用户
|
||||||
u.setId(user.getId());
|
u.setId(user.getId());
|
||||||
|
// 拉取数据未分配三级单位时写入空字符串,避免 updateIgnoreNull 保留人员原有的三级单位。
|
||||||
|
if (StrUtil.isBlank(source.getThreeUnitId())) {
|
||||||
|
u.setThreeUnitId("");
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
|
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
|
||||||
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
|
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
|
||||||
*/
|
*/
|
||||||
u.setAidFundMember(null);
|
u.setAidFundMember(null);
|
||||||
u.setPersonType(null);
|
|
||||||
needDoUpdateList.add(u);
|
needDoUpdateList.add(u);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,14 +381,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
Sql resetStatusSql = Sqls.create("""
|
Sql resetStatusSql = Sqls.create("""
|
||||||
UPDATE sys_user userInfo
|
UPDATE sys_user userInfo
|
||||||
SET userInfo.pendingConfirmStatus = @noneStatus
|
SET userInfo.pendingConfirmStatus = @noneStatus
|
||||||
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
|
WHERE userInfo.pendingConfirmStatus <> @noneStatus
|
||||||
AND userInfo.pendingConfirmStatus <> @noneStatus
|
AND userInfo.member='1'
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM sys_user_source sourceInfo
|
FROM sys_user_source sourceInfo
|
||||||
WHERE sourceInfo.pullTime = @pullTime
|
WHERE sourceInfo.pullTime = @pullTime
|
||||||
AND sourceInfo.loginname = userInfo.loginname
|
AND sourceInfo.loginname = userInfo.loginname
|
||||||
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
|
|
||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
resetStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
|
resetStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
|
||||||
@@ -400,14 +397,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
|||||||
Sql pendingStatusSql = Sqls.create("""
|
Sql pendingStatusSql = Sqls.create("""
|
||||||
UPDATE sys_user userInfo
|
UPDATE sys_user userInfo
|
||||||
SET userInfo.pendingConfirmStatus = @pendingStatus
|
SET userInfo.pendingConfirmStatus = @pendingStatus
|
||||||
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
|
WHERE IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
|
||||||
AND IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
|
AND userInfo.member='1'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM sys_user_source sourceInfo
|
FROM sys_user_source sourceInfo
|
||||||
WHERE sourceInfo.pullTime = @pullTime
|
WHERE sourceInfo.pullTime = @pullTime
|
||||||
AND sourceInfo.loginname = userInfo.loginname
|
AND sourceInfo.loginname = userInfo.loginname
|
||||||
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
|
|
||||||
)
|
)
|
||||||
""");
|
""");
|
||||||
pendingStatusSql.setParam("pendingStatus", PENDING_CONFIRM_STATUS_PENDING);
|
pendingStatusSql.setParam("pendingStatus", PENDING_CONFIRM_STATUS_PENDING);
|
||||||
|
|||||||
@@ -132,6 +132,39 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
|
|
||||||
Date nowDate = new Date();
|
Date nowDate = new Date();
|
||||||
|
|
||||||
|
// 人员单位号先与单位表批量比对,缺失单位只同步一次,避免逐人查询单位表。
|
||||||
|
Set<String> sourceUnitIds = rawDataList.stream()
|
||||||
|
.map(raw -> raw.getStr("SZDWH"))
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<String, Sys_unit> unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
|
||||||
|
if (!unitMap.keySet().containsAll(sourceUnitIds)) {
|
||||||
|
log.info("单位需要更新,正在同步更新");
|
||||||
|
sysDataUnitPullService.updateUnits(new ArrayList<>(unitMap.keySet()));
|
||||||
|
unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按单位表的实际等级判断:三级单位为3、直属二级单位为2;单位号位数不参与判断。
|
||||||
|
Map<String, String> tertiaryParentUnitIds = new HashMap<>();
|
||||||
|
for (String sourceUnitId : sourceUnitIds) {
|
||||||
|
Sys_unit unit = unitMap.get(sourceUnitId);
|
||||||
|
if (unit == null) {
|
||||||
|
log.warn("人员单位号{}在单位表中不存在,保留信息中心原始单位号", sourceUnitId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Integer.valueOf(3).equals(unit.getUnitLevel())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Sys_unit parentUnit = unitMap.get(unit.getParentId());
|
||||||
|
if (parentUnit == null || !Integer.valueOf(2).equals(parentUnit.getUnitLevel())) {
|
||||||
|
log.warn("三级单位{}缺少有效的二级父单位,保留信息中心原始单位号", sourceUnitId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tertiaryParentUnitIds.put(sourceUnitId, parentUnit.getId());
|
||||||
|
}
|
||||||
|
|
||||||
// 赋值
|
// 赋值
|
||||||
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
|
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
|
||||||
Sys_user_source sysUser = new Sys_user_source();
|
Sys_user_source sysUser = new Sys_user_source();
|
||||||
@@ -174,7 +207,10 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
}
|
}
|
||||||
|
|
||||||
sysUser.setUnitName(raw.getStr("SZDW"));
|
sysUser.setUnitName(raw.getStr("SZDW"));
|
||||||
sysUser.setUnitId(resolveSourceUnitId(raw.getStr("SZDWH")));
|
String sourceUnitId = raw.getStr("SZDWH");
|
||||||
|
String parentUnitId = tertiaryParentUnitIds.get(sourceUnitId);
|
||||||
|
sysUser.setUnitId(StrUtil.blankToDefault(parentUnitId, sourceUnitId));
|
||||||
|
sysUser.setThreeUnitId(parentUnitId == null ? null : sourceUnitId);
|
||||||
|
|
||||||
// 设置拉取时间
|
// 设置拉取时间
|
||||||
sysUser.setPullTime(nowDate);
|
sysUser.setPullTime(nowDate);
|
||||||
@@ -188,17 +224,6 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
dao().insert(latestSourceList);
|
dao().insert(latestSourceList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
|
|
||||||
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).filter(StrUtil::isNotBlank).distinct().toList();
|
|
||||||
Sql sql = Sqls.create("select id from sys_unit group by id");
|
|
||||||
sql.setCallback(Sqls.callback.strList());
|
|
||||||
dao().execute(sql);
|
|
||||||
List<String> unitIds = sql.getList(String.class);
|
|
||||||
|
|
||||||
if (!new HashSet<>(unitIds).containsAll(sourceUnitIds)) {
|
|
||||||
log.info("单位需要更新,正在同步更新");
|
|
||||||
sysDataUnitPullService.updateUnits(unitIds);
|
|
||||||
}
|
|
||||||
return nowDate;
|
return nowDate;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BaseException("数据拉取失败,{}", e.getMessage());
|
throw new BaseException("数据拉取失败,{}", e.getMessage());
|
||||||
@@ -342,22 +367,6 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
|||||||
return new ArrayList<>(userMap.values());
|
return new ArrayList<>(userMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据接口返回的所在单位号转换数据源用户单位。
|
|
||||||
* 入参 rawUnitId 对应人员接口 SZDWH:5位表示三级单位,需要写入其二级父单位;其他长度保持接口原值。
|
|
||||||
* 返回值为 sys_user_source.unitId 使用的单位ID,找不到三级单位或父级为空时保留接口原始单位号,避免人员数据丢失。
|
|
||||||
*/
|
|
||||||
private String resolveSourceUnitId(String rawUnitId) {
|
|
||||||
if (StrUtil.isBlank(rawUnitId) || rawUnitId.length() != 5) {
|
|
||||||
return rawUnitId;
|
|
||||||
}
|
|
||||||
Sys_unit unit = dao().fetch(Sys_unit.class, rawUnitId);
|
|
||||||
if (unit == null || StrUtil.isBlank(unit.getParentId())) {
|
|
||||||
return rawUnitId;
|
|
||||||
}
|
|
||||||
return unit.getParentId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
private void updateDict(List<Sys_user_source> userSources) {
|
private void updateDict(List<Sys_user_source> userSources) {
|
||||||
//判断是否要更新在职状态字典
|
//判断是否要更新在职状态字典
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.nutz.ioc.aop.Aop;
|
|||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheResult;
|
import org.nutz.plugins.wkcache.annotation.CacheResult;
|
||||||
@@ -284,4 +285,44 @@ public class SysRoleServiceImpl extends BaseServiceImpl<Sys_role> implements Sys
|
|||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量补齐会员角色。仅处理 member=1 的用户,保留其他角色及账号禁用状态。
|
||||||
|
*
|
||||||
|
* @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap settingMemberRole() {
|
||||||
|
// 锁定同一会员角色,使本接口的并发请求依次执行,避免重复插入用户角色关系。
|
||||||
|
Sql roleSql = Sqls.create("SELECT * FROM sys_role WHERE code=@code FOR UPDATE");
|
||||||
|
roleSql.setParam("code", RoleConstant.MEMBER.name());
|
||||||
|
roleSql.setEntity(dao().getEntity(Sys_role.class));
|
||||||
|
roleSql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(roleSql);
|
||||||
|
List<Sys_role> roles = roleSql.getList(Sys_role.class);
|
||||||
|
if (roles.isEmpty()) {
|
||||||
|
throw new BaseException("会员角色(MEMBER)不存在,请先配置会员角色");
|
||||||
|
}
|
||||||
|
Sys_role role = roles.get(0);
|
||||||
|
if (role.isDisabled()) {
|
||||||
|
throw new BaseException("会员角色(MEMBER)已禁用,请先启用会员角色");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史关联可能已停用或未填写 enable;恢复关联即可,无需重新插入。
|
||||||
|
Sql enableSql = Sqls.create("UPDATE sys_user_role SET enable=1 WHERE roleId=@roleId "
|
||||||
|
+ "AND (enable IS NULL OR enable<>1) "
|
||||||
|
+ "AND EXISTS (SELECT 1 FROM sys_user u WHERE u.id=sys_user_role.userId AND u.member=1)");
|
||||||
|
enableSql.setParam("roleId", role.getId());
|
||||||
|
dao().execute(enableSql);
|
||||||
|
|
||||||
|
// 按用户、角色判重,只为缺少会员关联的人员添加记录;不删除任何已有角色。
|
||||||
|
Sql insertSql = Sqls.create("INSERT INTO sys_user_role (userId,roleId,enable) "
|
||||||
|
+ "SELECT u.id,@roleId,1 FROM sys_user u WHERE u.member=1 "
|
||||||
|
+ "AND NOT EXISTS (SELECT 1 FROM sys_user_role ur WHERE ur.userId=u.id AND ur.roleId=@roleId)");
|
||||||
|
insertSql.setParam("roleId", role.getId());
|
||||||
|
dao().execute(insertSql);
|
||||||
|
return NutMap.NEW().addv("addedCount", insertSql.getUpdateCount())
|
||||||
|
.addv("enabledCount", enableSql.getUpdateCount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,35 @@
|
|||||||
package com.budwk.app.task.job.club;
|
package com.budwk.app.task.job.club;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.json.JSONObject;
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import org.quartz.DisallowConcurrentExecution;
|
||||||
import org.nutz.dao.Cnd;
|
|
||||||
import org.nutz.dao.Dao;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.quartz.Job;
|
import org.quartz.Job;
|
||||||
import org.quartz.JobExecutionContext;
|
import org.quartz.JobExecutionContext;
|
||||||
import org.quartz.JobExecutionException;
|
import org.quartz.JobExecutionException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubGeneratePayRecordJob
|
* @ClassName ClubGeneratePayRecordJob
|
||||||
* @Author JyuHsin
|
* @Author JyuHsin
|
||||||
* @Date 2026/1/22 16:56
|
* @Date 2026/1/22 16:56
|
||||||
* @Version 1.0
|
* @Version 1.0
|
||||||
* @Description 每年的1月1日生成社团成员下年的缴费记录
|
* @Description 每年的1月1日生成社团成员当年的缴费记录
|
||||||
*/
|
*/
|
||||||
@IocBean
|
@IocBean
|
||||||
|
@DisallowConcurrentExecution
|
||||||
public class ClubGeneratePayRecordJob implements Job {
|
public class ClubGeneratePayRecordJob implements Job {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private ClubPayRecordService payRecordService;
|
||||||
|
|
||||||
@Override
|
/** 2026年10月3日 09:05:37;操作人:wxg:生成当年完整快照名单,service 批量去重并在事务中保存。 */
|
||||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
@Override
|
||||||
// 当前年份
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
int thisYear = DateUtil.thisYear();
|
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||||
// 获取最新的社团成员信息
|
payRecordService.generateYearPayRecords(DateUtil.thisYear());
|
||||||
List<ClubUser> clubUserList = dao.query(ClubUser.class, Cnd.NEW());
|
}
|
||||||
// 生成缴费记录对象
|
|
||||||
List<ClubPayRecord> list = new ArrayList<>();
|
|
||||||
for (ClubUser clubUser : clubUserList) {
|
|
||||||
ClubPayRecord clubPayRecord = new ClubPayRecord();
|
|
||||||
clubPayRecord.setYear(thisYear);
|
|
||||||
clubPayRecord.setClubId(clubUser.getClubId());
|
|
||||||
clubPayRecord.setUserId(clubUser.getUserId());
|
|
||||||
clubPayRecord.setPayed(false);
|
|
||||||
|
|
||||||
JSONObject log = new JSONObject();
|
|
||||||
log.set("operatorUserId", "task");
|
|
||||||
log.set("operatorUserName", "定时任务");
|
|
||||||
log.set("operatorTime", DateUtil.now());
|
|
||||||
log.set("operatorType", "缴费管理-定时任务自动设置");
|
|
||||||
log.set("oldValue", null);
|
|
||||||
log.set("newValue", false);
|
|
||||||
clubPayRecord.setOperateLogs(List.of(log));
|
|
||||||
|
|
||||||
list.add(clubPayRecord);
|
|
||||||
}
|
|
||||||
dao.insert(list);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -87,6 +87,7 @@ public class ClubUserJoinApplyController {
|
|||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
cua.*,
|
cua.*,
|
||||||
|
CAST(cua.mode AS SIGNED) AS mode,
|
||||||
club.clubName,
|
club.clubName,
|
||||||
u.loginname AS loginName,
|
u.loginname AS loginName,
|
||||||
u.username AS userName,
|
u.username AS userName,
|
||||||
@@ -109,14 +110,14 @@ public class ClubUserJoinApplyController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("保存申请")
|
@ApiOperation("保存申请")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
@SLog(tag = "社团管理系统-申请入会", msg = "保存社团入会")
|
@SLog(tag = "社团管理系统-申请入会", msg = "保存社团入会")
|
||||||
public Result save(@Param("data") ClubUserApply clubUserApply) {
|
public Result save(@Param("data") ClubUserApply clubUserApply) {
|
||||||
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
||||||
clubUserApply.setMode(true);
|
clubUserApply.setMode(1);
|
||||||
clubUserApply.setApplyDate(new Date());
|
clubUserApply.setApplyDate(new Date());
|
||||||
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
clubUserJoinService.preparePendingJoinTimes(clubUserApply);
|
||||||
clubUserApply.setExitTime(null);
|
|
||||||
dao.insertOrUpdate(clubUserApply);
|
dao.insertOrUpdate(clubUserApply);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
@@ -151,13 +152,13 @@ public class ClubUserJoinApplyController {
|
|||||||
if(!mode) {
|
if(!mode) {
|
||||||
clubUserApply.setId(null);
|
clubUserApply.setId(null);
|
||||||
}
|
}
|
||||||
clubUserApply.setMode(mode);
|
// 请求参数沿用 PC/H5 的布尔值;持久化统一使用 1 入会、0 退会,2 仅由管理端生成。
|
||||||
|
clubUserApply.setMode(Boolean.TRUE.equals(mode) ? 1 : 0);
|
||||||
Date applyDate = new Date();
|
Date applyDate = new Date();
|
||||||
clubUserApply.setApplyDate(applyDate);
|
clubUserApply.setApplyDate(applyDate);
|
||||||
if (mode) {
|
if (mode) {
|
||||||
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name());
|
||||||
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
clubUserJoinService.preparePendingJoinTimes(clubUserApply);
|
||||||
clubUserApply.setExitTime(null);
|
|
||||||
} else {
|
} else {
|
||||||
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId()));
|
||||||
clubUserApply.setExitTime(applyDate);
|
clubUserApply.setExitTime(applyDate);
|
||||||
@@ -184,6 +185,7 @@ public class ClubUserJoinApplyController {
|
|||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR)
|
||||||
public Result submitAgain(@Param("data") ClubUserApply clubUserApply, @Param("taskId") Long taskId) {
|
public Result submitAgain(@Param("data") ClubUserApply clubUserApply, @Param("taskId") Long taskId) {
|
||||||
|
clubUserJoinService.preparePendingJoinTimes(clubUserApply);
|
||||||
dao.insertOrUpdate(clubUserApply);
|
dao.insertOrUpdate(clubUserApply);
|
||||||
|
|
||||||
Dict dict = Dict.create();
|
Dict dict = Dict.create();
|
||||||
|
|||||||
+22
-1
@@ -65,9 +65,30 @@ public class ClubUserJoinMineController {
|
|||||||
public Result pageData(@Valid PageForm pageForm,
|
public Result pageData(@Valid PageForm pageForm,
|
||||||
@Param("year") Integer year,
|
@Param("year") Integer year,
|
||||||
@Param("clubName") String clubName){
|
@Param("clubName") String clubName){
|
||||||
|
// 明确列出申请字段,避免分页总数子查询出现重名列;mode 只输出一次整数转换,保留系统处理值 2。
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
info.*,
|
info.id,
|
||||||
|
info.clubId,
|
||||||
|
info.userId,
|
||||||
|
info.roleCode,
|
||||||
|
info.clubPosition,
|
||||||
|
info.email,
|
||||||
|
info.mobile,
|
||||||
|
info.birthday,
|
||||||
|
info.avatar,
|
||||||
|
info.sameTimeJoinOtherClubSituation,
|
||||||
|
info.awardsExperience,
|
||||||
|
info.signature,
|
||||||
|
info.applyDate,
|
||||||
|
info.joinTime,
|
||||||
|
info.exitTime,
|
||||||
|
info.createdBy,
|
||||||
|
info.createdAt,
|
||||||
|
info.updatedBy,
|
||||||
|
info.updatedAt,
|
||||||
|
info.delFlag,
|
||||||
|
CAST(info.mode AS SIGNED) AS mode,
|
||||||
club.clubName,
|
club.clubName,
|
||||||
u.loginname as loginName,
|
u.loginname as loginName,
|
||||||
u.username as userName,
|
u.username as userName,
|
||||||
|
|||||||
@@ -39,18 +39,9 @@ public class ClubCommonController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
|
/** 无请求参数;返回入会社团选项,每项包含 id、clubName 等社团字段,同一社团不重复。 */
|
||||||
public Result listClub() {
|
public Result listClub() {
|
||||||
Sql sql = Sqls.create("""
|
return Result.success(sysClubService.listJoinClubOptions());
|
||||||
SELECT
|
|
||||||
c.*
|
|
||||||
FROM
|
|
||||||
`sys_club` c
|
|
||||||
LEFT JOIN wf_process_instance inst ON inst.businessNo = c.id
|
|
||||||
WHERE
|
|
||||||
inst.state = @state
|
|
||||||
""").setParam("state", ProcessInstanceStateEnum.FINISHED.getCode());
|
|
||||||
List list = baseService.listVO(sql, SysClub.class);
|
|
||||||
return Result.success(list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
|
|||||||
+24
-9
@@ -93,15 +93,30 @@ public class ClubExamineApplyController {
|
|||||||
return Result.success(myManageClub);
|
return Result.success(myManageClub);
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
// 2026年10月2日wxg屏蔽操作:保留原统计参数处理入口。
|
||||||
@SaCheckLogin
|
// @At
|
||||||
public Result getClubUserNum(String clubId, Integer year) {
|
// @SaCheckLogin
|
||||||
if(StrUtil.isBlank(clubId)) {
|
// public Result getClubUserNum(String clubId, Integer year) {
|
||||||
return Result.error("社团信息为空,请核查");
|
// if(StrUtil.isBlank(clubId)) {
|
||||||
}
|
// return Result.error("社团信息为空,请核查");
|
||||||
List<NutMap> result = sysClubExamineService.getClubUserNum(clubId, year);
|
// }
|
||||||
return Result.success(result);
|
// List<NutMap> result = sysClubExamineService.getClubUserNum(clubId, year);
|
||||||
}
|
// return Result.success(result);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
/**
|
||||||
|
* 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
|
* 接收 clubId(所选社团ID)、year(公历年度),基础校验后调用 service;
|
||||||
|
* 返回 Result,data为两类计数及统计核查信息列表,异常参数返回错误提示。
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@SaCheckLogin
|
||||||
|
public Result getClubUserNum(String clubId, Integer year) {
|
||||||
|
if (StrUtil.isBlank(clubId)) return Result.error("社团信息为空,请核查");
|
||||||
|
if (year == null || year < 1 || year > 9998) return Result.error("请选择有效统计年度");
|
||||||
|
return Result.success(sysClubExamineService.getClubUserNum(clubId, year));
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckLogin
|
@SaCheckLogin
|
||||||
|
|||||||
+22
-13
@@ -30,6 +30,7 @@ import com.budwk.app.zhgh.club.model.SysClub;
|
|||||||
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubInfoManageService;
|
import com.budwk.app.zhgh.club.service.SysClubInfoManageService;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubUserImportVo;
|
import com.budwk.app.zhgh.club.vo.ClubUserImportVo;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -77,6 +78,8 @@ public class ClubInfoManageController {
|
|||||||
private SysUserService sysUserService;
|
private SysUserService sysUserService;
|
||||||
@Inject
|
@Inject
|
||||||
private SysClubUserService clubUserService;
|
private SysClubUserService clubUserService;
|
||||||
|
@Inject
|
||||||
|
private ClubPayRecordService payRecordService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/club/infoManage/manage/index.html")
|
@Ok("beetl:/platform/zhgh/club/infoManage/manage/index.html")
|
||||||
@@ -137,19 +140,7 @@ public class ClubInfoManageController {
|
|||||||
@SaCheckPermission("club.infoManage.manage")
|
@SaCheckPermission("club.infoManage.manage")
|
||||||
@SLog(tag = "社团管理系统-信息管理", msg = "退会")
|
@SLog(tag = "社团管理系统-信息管理", msg = "退会")
|
||||||
public Result exitClub(@Valid String id) {
|
public Result exitClub(@Valid String id) {
|
||||||
ClubUser clubUser = dao.fetch(ClubUser.class, id);
|
clubInfoManageService.exitClub(id);
|
||||||
dao.delete(ClubUser.class,id);
|
|
||||||
List<String> roleCodes = clubUser.getRoleCode();
|
|
||||||
for (String roleCode : roleCodes) {
|
|
||||||
Sys_role sysRole = sysRoleService.getByCode(roleCode);
|
|
||||||
if (ObjectUtil.isNotEmpty(sysRole)) {
|
|
||||||
dao.clear(Sys_user_role.class, Cnd.NEW().and("userId", "=", clubUser.getUserId())
|
|
||||||
.and("roleId", "=", sysRole.getId())
|
|
||||||
.and("clubId", "=", clubUser.getClubId()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sysUserService.clearCache();
|
|
||||||
sysRoleService.clearCache();
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +301,8 @@ public class ClubInfoManageController {
|
|||||||
clubUser.setClubId(clubId);
|
clubUser.setClubId(clubId);
|
||||||
clubUser.setUserId(user);
|
clubUser.setUserId(user);
|
||||||
clubUser.setRoleCode(List.of(roleCode));
|
clubUser.setRoleCode(List.of(roleCode));
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:仅新会员调用 service,统一填写确认入会时间和姓名/工号/电话。
|
||||||
|
clubUserService.prepareDirectJoinTimes(List.of(clubUser));
|
||||||
}
|
}
|
||||||
dao.insertOrUpdate(clubUser);
|
dao.insertOrUpdate(clubUser);
|
||||||
|
|
||||||
@@ -335,6 +328,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", payed != null ? payed : false);
|
log.set("newValue", payed != null ? payed : false);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:新增记录先填写人员快照。
|
||||||
|
payRecordService.prepareSnapshots(List.of(record));
|
||||||
dao.insert(record);
|
dao.insert(record);
|
||||||
} else {
|
} else {
|
||||||
payRecord.setPayed(payed != null ? payed : false);
|
payRecord.setPayed(payed != null ? payed : false);
|
||||||
@@ -348,6 +343,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", payed != null ? payed : false);
|
log.set("newValue", payed != null ? payed : false);
|
||||||
|
|
||||||
payRecord.setOperateLogs(List.of(log));
|
payRecord.setOperateLogs(List.of(log));
|
||||||
|
// 已有年度记录仅补缺失快照,原有年度资料和加入时间不覆盖。
|
||||||
|
payRecordService.prepareSnapshots(List.of(payRecord));
|
||||||
dao.update(payRecord);
|
dao.update(payRecord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +353,13 @@ public class ClubInfoManageController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clubId 左侧选中的社团ID,用于导出该社团登记表
|
||||||
|
* @param response 成功时写入 DOCX 附件;失败由统一异常处理返回含 code/msg 的错误结果
|
||||||
|
* @return void,附件直接写入响应,不再渲染 JSON 成功结果
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
|
@Ok("void")
|
||||||
@SaCheckPermission("club.infoManage.manage")
|
@SaCheckPermission("club.infoManage.manage")
|
||||||
@ApiOperation("导出登记表")
|
@ApiOperation("导出登记表")
|
||||||
public void exportRegistrationDoc(@Valid String clubId, HttpServletResponse response){
|
public void exportRegistrationDoc(@Valid String clubId, HttpServletResponse response){
|
||||||
@@ -412,6 +415,8 @@ public class ClubInfoManageController {
|
|||||||
cUser.setRoleCode(List.of(RoleConstant.CLUB_MEMBER.name()));
|
cUser.setRoleCode(List.of(RoleConstant.CLUB_MEMBER.name()));
|
||||||
clubUserList.add(cUser);
|
clubUserList.add(cUser);
|
||||||
}
|
}
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:导入新增会员统一从用户资料批量填写人员字段及后端确认时间。
|
||||||
|
clubUserService.prepareDirectJoinTimes(clubUserList);
|
||||||
dao.insert(clubUserList);
|
dao.insert(clubUserList);
|
||||||
|
|
||||||
List<Sys_user_role> sysUserRoleList = new ArrayList<>();
|
List<Sys_user_role> sysUserRoleList = new ArrayList<>();
|
||||||
@@ -450,6 +455,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", false);
|
log.set("newValue", false);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:新增记录先填写人员快照。
|
||||||
|
payRecordService.prepareSnapshots(List.of(record));
|
||||||
dao.insert(record);
|
dao.insert(record);
|
||||||
} else {
|
} else {
|
||||||
payRecord.setPayed(false);
|
payRecord.setPayed(false);
|
||||||
@@ -463,6 +470,8 @@ public class ClubInfoManageController {
|
|||||||
log.set("newValue", false);
|
log.set("newValue", false);
|
||||||
|
|
||||||
payRecord.setOperateLogs(List.of(log));
|
payRecord.setOperateLogs(List.of(log));
|
||||||
|
// 已有年度记录仅补缺失快照,原有年度资料和加入时间不覆盖。
|
||||||
|
payRecordService.prepareSnapshots(List.of(payRecord));
|
||||||
dao.update(payRecord);
|
dao.update(payRecord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-140
@@ -1,42 +1,22 @@
|
|||||||
package com.budwk.app.zhgh.club.controller.infoManage;
|
package com.budwk.app.zhgh.club.controller.infoManage;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.annotation.SaMode;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.json.JSONObject;
|
|
||||||
import com.budwk.app.base.annotation.SLog;
|
import com.budwk.app.base.annotation.SLog;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
|
||||||
import com.budwk.app.base.page.Pagination;
|
|
||||||
import com.budwk.app.base.param.PageForm;
|
import com.budwk.app.base.param.PageForm;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
|
||||||
import com.budwk.app.zhgh.club.model.SysClub;
|
|
||||||
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubService;
|
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Sqls;
|
|
||||||
import org.nutz.dao.sql.Sql;
|
|
||||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
|
||||||
import org.nutz.lang.util.NutMap;
|
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubPayRecordController
|
* @ClassName ClubPayRecordController
|
||||||
@@ -55,8 +35,6 @@ public class ClubPayRecordController {
|
|||||||
@Inject
|
@Inject
|
||||||
private ClubPayRecordService payRecordService;
|
private ClubPayRecordService payRecordService;
|
||||||
|
|
||||||
@Inject
|
|
||||||
private SysClubService sysClubService;
|
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@SaCheckPermission("club.infoManage.pay")
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
@@ -66,135 +44,40 @@ public class ClubPayRecordController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@SaCheckPermission("club.infoManage.pay")
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 14:33:18;操作人:wxg:接收查询条件,业务筛选交给 service。
|
||||||
|
* pageForm 为分页和姓名/工号关键字;year 为年度、clubId 为社团ID;
|
||||||
|
* payed/assign 为缴费/拨付状态(1是、0否、空值不限);memberState 为当前会员状态(1在会、0退会、空值全部)。
|
||||||
|
* 返回 Result,data 为分页对象,包含 list、totalCount 等;list.count 为当前会员关系是否存在(1在会、0退会)。
|
||||||
|
*/
|
||||||
public Result pageData(@Valid PageForm pageForm,
|
public Result pageData(@Valid PageForm pageForm,
|
||||||
@Param("year") Integer year,
|
@Param("year") Integer year,
|
||||||
@Param("clubId") String clubId,
|
@Param("clubId") String clubId,
|
||||||
@Param("payed") Integer payed,
|
@Param("payed") Integer payed,
|
||||||
@Param("assign") Integer assign) {
|
@Param("assign") Integer assign,
|
||||||
Sql sql = Sqls.create("""
|
@Param("memberState") Integer memberState) {
|
||||||
SELECT
|
if (memberState != null && memberState != 0 && memberState != 1) {
|
||||||
cpr.*,
|
return Result.error("会员状态只能为在会、退会或全部");
|
||||||
c.clubName,
|
}
|
||||||
u.loginName,
|
return Result.success(payRecordService.pageData(pageForm, year, clubId, payed, assign, memberState));
|
||||||
u.userName,
|
}
|
||||||
u.unitName,
|
|
||||||
u.retireDate,
|
|
||||||
(SELECT createdAt FROM club_user WHERE clubId = cpr.clubId AND userId = cpr.userId ORDER BY createdAt LIMIT 1) AS applyTime,
|
|
||||||
u.userState,
|
|
||||||
(select count(1) from club_user where clubId = cpr.clubId and userId = cpr.userId) as count
|
|
||||||
FROM
|
|
||||||
`club_pay_record` cpr
|
|
||||||
LEFT JOIN sys_club c ON cpr.clubid = c.id
|
|
||||||
LEFT JOIN vw_user u ON u.id = cpr.userId
|
|
||||||
$condition
|
|
||||||
""");
|
|
||||||
Cnd cnd = Cnd.NEW();
|
|
||||||
List<String> clubIdList = sysClubService.getMyManageClub().stream()
|
|
||||||
.map(SysClub::getId)
|
|
||||||
.toList();
|
|
||||||
cnd.andEX("cpr.year", "=", year);
|
|
||||||
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
|
|
||||||
cnd.andEX("cpr.clubId", "in", clubIdList);
|
|
||||||
}else{
|
|
||||||
cnd.andEX("cpr.clubId", "=", clubId);
|
|
||||||
}
|
|
||||||
cnd.andEX("cpr.payed", "=", payed);
|
|
||||||
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
|
||||||
SqlExpressionGroup seg = new SqlExpressionGroup();
|
|
||||||
seg.orLike("u.userName", pageForm.getSearchKeyword());
|
|
||||||
seg.orLike("u.loginName", pageForm.getSearchKeyword());
|
|
||||||
cnd.and(seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assign != null) {
|
/** 返回当前用户授权社团的下拉选项,字段包含 id、clubName、clubCode。 */
|
||||||
SqlExpressionGroup mainGroup = new SqlExpressionGroup();
|
@At
|
||||||
if (assign == 0) {
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
SqlExpressionGroup noAssignGroup = new SqlExpressionGroup();
|
public Result listClubs() {
|
||||||
|
return Result.success(payRecordService.getPayManageClubList());
|
||||||
// 不拨付条件1:payed=false(原代码第一行直接return false)
|
|
||||||
noAssignGroup.or("cpr.payed", "=", false);
|
|
||||||
|
|
||||||
// 不拨付条件2:payed=true,但用户状态是退休 且 无退休日期
|
|
||||||
SqlExpressionGroup retireNoDateGroup = new SqlExpressionGroup();
|
|
||||||
retireNoDateGroup.and("cpr.payed", "=", true);
|
|
||||||
retireNoDateGroup.and("u.userState", "=", "退休");
|
|
||||||
retireNoDateGroup.and("u.retireDate", "is", null);
|
|
||||||
|
|
||||||
// 不拨付条件3:payed=true,用户状态是退休,有退休日期,但退休年份 < 缴费年份
|
|
||||||
SqlExpressionGroup retireYearLessGroup = new SqlExpressionGroup();
|
|
||||||
retireYearLessGroup.and("cpr.payed", "=", true);
|
|
||||||
retireYearLessGroup.and("u.userState", "=", "退休");
|
|
||||||
retireYearLessGroup.and("u.retireDate", "is not", null);
|
|
||||||
// 计算缴费年份
|
|
||||||
// 退休年份 < 缴费年份
|
|
||||||
retireYearLessGroup.and("YEAR(u.retireDate)", "<", "cpr.year");
|
|
||||||
|
|
||||||
// 合并所有不拨付条件(满足任一即不拨付)
|
|
||||||
noAssignGroup.or(retireNoDateGroup);
|
|
||||||
noAssignGroup.or(retireYearLessGroup);
|
|
||||||
|
|
||||||
mainGroup.and(noAssignGroup);
|
|
||||||
} else {
|
|
||||||
SqlExpressionGroup assignGroup = new SqlExpressionGroup();
|
|
||||||
|
|
||||||
// 拨付前提:payed必须为true
|
|
||||||
assignGroup.and("cpr.payed", "=", true);
|
|
||||||
|
|
||||||
// 拨付条件1:用户状态≠退休
|
|
||||||
SqlExpressionGroup userNotRetireGroup = new SqlExpressionGroup();
|
|
||||||
userNotRetireGroup.or("u.userState", "<>", "退休");
|
|
||||||
|
|
||||||
// 拨付条件2:用户状态=退休,有退休日期,且退休年份≥缴费年份
|
|
||||||
SqlExpressionGroup retireQualifiedGroup = new SqlExpressionGroup();
|
|
||||||
retireQualifiedGroup.and("u.userState", "=", "退休");
|
|
||||||
retireQualifiedGroup.and("u.retireDate", "is not", null);
|
|
||||||
retireQualifiedGroup.and("YEAR(u.retireDate)", ">=", "cpr.year");
|
|
||||||
|
|
||||||
// 合并拨付条件(满足任一即拨付)
|
|
||||||
userNotRetireGroup.or(retireQualifiedGroup);
|
|
||||||
assignGroup.and(userNotRetireGroup);
|
|
||||||
|
|
||||||
mainGroup.and(assignGroup);
|
|
||||||
}
|
|
||||||
cnd.and(mainGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql.setCondition(cnd);
|
|
||||||
Pagination pagination = payRecordService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
|
||||||
|
|
||||||
List<NutMap> list = pagination.getList();
|
|
||||||
for (NutMap nutMap : list) {
|
|
||||||
boolean assigned = payRecordService.calAssigned(nutMap.getString("id"));
|
|
||||||
nutMap.put("assign", assigned);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success(pagination);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("修改社团成员缴费状态")
|
@ApiOperation("修改社团成员缴费状态")
|
||||||
@SaCheckPermission("club.infoManage.pay")
|
@SaCheckPermission("club.infoManage.pay")
|
||||||
@SLog(tag = "社团管理系统-缴费记录", msg = "修改社团成员缴费状态")
|
@SLog(tag = "社团管理系统-缴费记录", msg = "修改社团成员缴费状态")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result handle(@Param("ids") String[] ids,
|
public Result handle(@Param("ids") String[] ids,
|
||||||
@Param("payed") Boolean payed) {
|
@Param("payed") Boolean payed) {
|
||||||
List<ClubPayRecord> list = payRecordService.query(Cnd.where("id", "in", ids));
|
// 2026年10月3日 09:51:00;操作人:wxg:controller 只接收参数,缴费、拨付结果和日志由 service 在同一事务内保存。
|
||||||
|
payRecordService.updatePayStatus(ids, payed);
|
||||||
for (ClubPayRecord payRecord : list) {
|
|
||||||
List<JSONObject> logs = Optional.ofNullable(payRecord.getOperateLogs()).orElseGet(ArrayList::new);
|
|
||||||
JSONObject log = new JSONObject();
|
|
||||||
log.set("operatorUserId", SecurityUtil.getUserId());
|
|
||||||
log.set("operatorUserName", SecurityUtil.getUserUsername());
|
|
||||||
log.set("operatorTime", DateUtil.now());
|
|
||||||
log.set("operatorType", "缴费管理-手动设置");
|
|
||||||
log.set("oldValue", payRecord.getPayed());
|
|
||||||
log.set("newValue", payed);
|
|
||||||
logs.add(log);
|
|
||||||
|
|
||||||
payRecord.setOperateLogs(logs);
|
|
||||||
payRecord.setPayed(payed);
|
|
||||||
}
|
|
||||||
|
|
||||||
payRecordService.update(list);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -43,6 +44,8 @@ public class ClubUserExitInterceptor implements FlowInterceptor {
|
|||||||
|
|
||||||
// 删人
|
// 删人
|
||||||
dao.clear(ClubUser.class, Cnd.where(ClubUser::getUserId, "=", clubUserApply.getUserId()).and(ClubUser::getClubId, "=", clubUserApply.getClubId()));
|
dao.clear(ClubUser.class, Cnd.where(ClubUser::getUserId, "=", clubUserApply.getUserId()).and(ClubUser::getClubId, "=", clubUserApply.getClubId()));
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:WF 退会完成后,同步将该社团会员的拨付字段置为 false。
|
||||||
|
ServiceContext.find(ClubPayRecordService.class).disableExitMemberPayment(clubUserApply.getClubId(), clubUserApply.getUserId());
|
||||||
// 删角色
|
// 删角色
|
||||||
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", clubUserApply.getUserId()).and(Sys_user_role::getClubId, "=", clubUserApply.getClubId()));
|
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", clubUserApply.getUserId()).and(Sys_user_role::getClubId, "=", clubUserApply.getClubId()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import org.bouncycastle.jcajce.provider.util.SecretKeyUtil;
|
import org.bouncycastle.jcajce.provider.util.SecretKeyUtil;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
@@ -41,9 +43,20 @@ public class ClubUserJoinInterceptor implements FlowInterceptor {
|
|||||||
|
|
||||||
// 获取操作类
|
// 获取操作类
|
||||||
Dao dao = ServiceContext.find(Dao.class);
|
Dao dao = ServiceContext.find(Dao.class);
|
||||||
|
// 入会时间以本次会长同意审核任务的完成时间为准,交由 service 校验并写入申请表。
|
||||||
|
clubUserApply.setJoinTime(ServiceContext.find(ClubUserJoinService.class).completeJoinApproval(
|
||||||
|
execution.getProcessInstance().getBusinessNo(), execution.getProcessTaskId()));
|
||||||
|
|
||||||
// 将申请信息复制一份,插入社团成员表
|
// 将申请信息复制一份,插入社团成员表
|
||||||
ClubUser clubUser = BeanUtil.copyProperties(clubUserApply, ClubUser.class);
|
ClubUser clubUser = BeanUtil.copyProperties(clubUserApply, ClubUser.class);
|
||||||
|
// 读取本次已写入的入会申请时间,不能取成立时间或用插入时刻替代审核完成时间。
|
||||||
|
ClubUserApply approvedApply = dao.fetch(ClubUserApply.class, execution.getProcessInstance().getBusinessNo());
|
||||||
|
if (approvedApply == null || approvedApply.getJoinTime() == null) {
|
||||||
|
throw new IllegalArgumentException("本次入会申请尚未生成审核完成时间");
|
||||||
|
}
|
||||||
|
clubUser.setJoinTime(approvedApply.getJoinTime());
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:审核入会保存当前用户资料,joinTime 仍使用本次会长审核完成时间。
|
||||||
|
ServiceContext.find(SysClubUserService.class).prepareMemberSnapshots(List.of(clubUser));
|
||||||
dao.insert(clubUser);
|
dao.insert(clubUser);
|
||||||
|
|
||||||
// 还要加到社团成员缴费表中
|
// 还要加到社团成员缴费表中
|
||||||
@@ -70,6 +83,8 @@ public class ClubUserJoinInterceptor implements FlowInterceptor {
|
|||||||
log.set("newValue", payed);
|
log.set("newValue", payed);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:审核新增缴费记录,保存本次会员加入时间及人员快照。
|
||||||
|
ServiceContext.find(ClubPayRecordService.class).prepareSnapshots(List.of(record));
|
||||||
dao.insert(record);
|
dao.insert(record);
|
||||||
} else {
|
} else {
|
||||||
payRecord.setPayed(payed);
|
payRecord.setPayed(payed);
|
||||||
@@ -83,6 +98,8 @@ public class ClubUserJoinInterceptor implements FlowInterceptor {
|
|||||||
log.set("newValue", payed);
|
log.set("newValue", payed);
|
||||||
|
|
||||||
payRecord.setOperateLogs(List.of(log));
|
payRecord.setOperateLogs(List.of(log));
|
||||||
|
// 同年度重新入会复用原缴费记录,只补缺失资料,不重写已保存的年度快照。
|
||||||
|
ServiceContext.find(ClubPayRecordService.class).prepareSnapshots(List.of(payRecord));
|
||||||
dao.update(payRecord);
|
dao.update(payRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.nutz.dao.entity.annotation.*;
|
|||||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubPayRecord
|
* @ClassName ClubPayRecord
|
||||||
@@ -56,6 +57,38 @@ public class ClubPayRecord extends BaseModel {
|
|||||||
@Default(value = "0")
|
@Default(value = "0")
|
||||||
private Boolean isGiveMoney;
|
private Boolean isGiveMoney;
|
||||||
|
|
||||||
|
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:保存年度名单人员快照,列表不再联查用户视图。
|
||||||
|
@Column
|
||||||
|
@Comment("工号")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 120)
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("姓名")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("所在单位")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||||
|
private String unitName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("人员类型")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String personType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("加入时间")
|
||||||
|
@ColDefine(type = ColType.DATETIME)
|
||||||
|
private Date joinTime;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("退休时间")
|
||||||
|
@ColDefine(type = ColType.DATETIME)
|
||||||
|
private Date retireDate;
|
||||||
|
|
||||||
// 防止有人今天操作缴费,明天操作未缴费,还是记录一下吧
|
// 防止有人今天操作缴费,明天操作未缴费,还是记录一下吧
|
||||||
@Column
|
@Column
|
||||||
@Comment("操作记录")
|
@Comment("操作记录")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.nutz.dao.entity.annotation.*;
|
|||||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -62,5 +63,25 @@ public class ClubUser extends BaseModel {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||||
private String awardsExperience;
|
private String awardsExperience;
|
||||||
|
|
||||||
|
/** 审核入会取本次申请的 joinTime;直接录入或导入取后端确认时间。 */
|
||||||
|
@Column
|
||||||
|
@Comment("入会时间")
|
||||||
|
@ColDefine(type = ColType.DATETIME)
|
||||||
|
private Date joinTime;
|
||||||
|
|
||||||
|
/** 2026年10月3日 12:01:28;操作人:wxg:会员资料在加入时保存,供会员列表直接展示和姓名/工号查询。 */
|
||||||
|
@Column
|
||||||
|
@Comment("姓名")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
private String userName;
|
private String userName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("工号")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 120)
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("电话")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||||
|
private String mobile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ public class ClubUserApply extends BaseModel {
|
|||||||
private String roleCode;
|
private String roleCode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("加入/退出")
|
@Comment("申请模式:0退出、1加入、2系统处理退会")
|
||||||
@ColDefine(type = ColType.BOOLEAN)
|
@ColDefine(type = ColType.INT)
|
||||||
private Boolean mode;
|
private Integer mode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("社团职务")
|
@Comment("社团职务")
|
||||||
|
|||||||
@@ -18,7 +18,17 @@ public class ClubUserPageForm extends PageForm {
|
|||||||
private String clubId;
|
private String clubId;
|
||||||
private String clubName;
|
private String clubName;
|
||||||
private String personType;
|
private String personType;
|
||||||
|
/** 重点关注名单:true 同时筛选自聘人员、其他;false 或未传时不增加该限制。 */
|
||||||
|
private Boolean focusMembers;
|
||||||
private String userState;
|
private String userState;
|
||||||
|
/** 会员入会审核开始日期,前端传 yyyy-MM-dd;为空时不限制开始时间。 */
|
||||||
|
private String joinDateStart;
|
||||||
|
/** 会员入会审核结束日期,前端传 yyyy-MM-dd,包含当天;为空时不限制结束时间。 */
|
||||||
|
private String joinDateEnd;
|
||||||
|
/** 2026年10月3日 19:42:45;操作人:wxg:退会日期范围起点,传 yyyy-MM-dd,空值不限制。 */
|
||||||
|
private String exitDateStart;
|
||||||
|
/** 2026年10月3日 19:42:45;操作人:wxg:退会日期范围终点,传 yyyy-MM-dd,包含当天,空值不限制。 */
|
||||||
|
private String exitDateEnd;
|
||||||
private String unitId;
|
private String unitId;
|
||||||
private Integer applyType;
|
private Integer applyType;
|
||||||
private Integer auditType;
|
private Integer auditType;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package com.budwk.app.zhgh.club.service;
|
|||||||
|
|
||||||
import com.budwk.app.base.service.BaseService;
|
import com.budwk.app.base.service.BaseService;
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
||||||
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName ClubPayRecordService
|
* @ClassName ClubPayRecordService
|
||||||
@@ -12,6 +16,55 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
|||||||
*/
|
*/
|
||||||
public interface ClubPayRecordService extends BaseService<ClubPayRecord> {
|
public interface ClubPayRecordService extends BaseService<ClubPayRecord> {
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:05:37;操作人:wxg:定向检查并新增缴费表缺失列;不删除字段,不生成独立升级 SQL 文件。 */
|
||||||
|
void upgradePayRecordSchema();
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:05:37;操作人:wxg:启动时补齐旧记录缺失快照,不覆盖已保存资料;返回 void。 */
|
||||||
|
void backfillPayRecordSnapshots();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 09:05:37;操作人:wxg:批量填写新增缴费记录的人员快照。
|
||||||
|
* @param records 已设置 year、clubId、userId 的缴费记录;现有人员快照不覆盖,空加入时间取有效会员 joinTime;根据当前会员关系及缴费状态计算拨付结果
|
||||||
|
* @return void;填写工号、姓名、单位、人员类型、退休日期、加入时间和 isGiveMoney,由调用方在事务中保存
|
||||||
|
*/
|
||||||
|
void prepareSnapshots(List<ClubPayRecord> records);
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:51:00;操作人:wxg:设置缴费状态并保存拨付结果及日志。
|
||||||
|
* @param ids 非空缴费记录ID数组;只允许操作当前用户授权社团的记录
|
||||||
|
* @param payed true已缴费、false未缴费,不能为空
|
||||||
|
* @return void;参数错误或记录不在授权范围时抛出异常,事务回滚
|
||||||
|
*/
|
||||||
|
void updatePayStatus(String[] ids, Boolean payed);
|
||||||
|
|
||||||
|
/** 2026年10月3日 09:51:00;操作人:wxg:退会后将该用户在指定社团的各年度缴费记录置为不拨付。
|
||||||
|
* @param clubId 退会社团ID,不能为空
|
||||||
|
* @param userId 退会用户ID,不能为空
|
||||||
|
* @return void;保留缴费状态和人员快照,仅更新 isGiveMoney
|
||||||
|
*/
|
||||||
|
void disableExitMemberPayment(String clubId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 09:05:37;操作人:wxg:按当前会员生成指定年度名单,已存在的会员记录不重复生成、不重置缴费。
|
||||||
|
* @param year 要生成名单的公历年度,年度计划任务传当前年份
|
||||||
|
* @return 本次实际新增的缴费记录条数
|
||||||
|
*/
|
||||||
|
int generateYearPayRecords(Integer year);
|
||||||
|
|
||||||
|
/** 返回当前用户可查询的社团选项,每项包含 id、clubName、clubCode;无授权社团时返回空列表。 */
|
||||||
|
List<NutMap> getPayManageClubList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按权限范围分页查询缴费记录,并按社团编号及缴费记录保存的加入时间排序。
|
||||||
|
* @param pageForm 页码、每页条数及姓名/工号关键字
|
||||||
|
* @param year 缴费年度,空值表示不限制
|
||||||
|
* @param clubId 社团ID,空值表示查询全部授权社团
|
||||||
|
* @param payed 缴费状态:1已缴费、0未缴费、空值不限
|
||||||
|
* @param assign 保存的拨付状态:1拨付、0不拨付、空值不限
|
||||||
|
* 2026年10月3日 14:33:18;操作人:wxg:在分页前按实时会员关系筛选,历史年度也使用当前会员状态。
|
||||||
|
* @param memberState 1在会、0退会、null不限;在会以同一 clubId/userId 存在 club_user 记录为准
|
||||||
|
* @return 分页记录,list 包含缴费字段、社团及人员信息、applyTime(保存的入会时间)与 assign(isGiveMoney 保存值转换为布尔值)
|
||||||
|
*/
|
||||||
|
Pagination pageData(PageForm pageForm, Integer year, String clubId, Integer payed, Integer assign, Integer memberState);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取社团成员是否拨付
|
* 获取社团成员是否拨付
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ import com.budwk.app.zhgh.club.model.ClubUserApply;
|
|||||||
|
|
||||||
public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除待审核入会申请的入退会时间,防止沿用协会成立时间或上次入会时间。
|
||||||
|
* @param application 待保存、提交或重新提交的申请;仅处理 mode=1 的入会申请
|
||||||
|
*/
|
||||||
|
void preparePendingJoinTimes(ClubUserApply application);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会长同意审核后,将本次 WF 任务的实际完成时间写入申请入会时间。
|
||||||
|
* @param applicationId 当前流程 businessNo 对应的入会申请ID
|
||||||
|
* @param taskId 当前会长审核任务ID,必须属于该申请且已完成、同意
|
||||||
|
* @return 写入 club_user_apply.joinTime 的审核完成时间;无有效审核任务时抛出异常
|
||||||
|
*/
|
||||||
|
java.util.Date completeJoinApproval(String applicationId, Long taskId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造退会申请数据。
|
* 构造退会申请数据。
|
||||||
* 当用户没有历史入会申请记录时,使用当前社团成员关系和人员基础信息兜底生成一份退会申请,
|
* 当用户没有历史入会申请记录时,使用当前社团成员关系和人员基础信息兜底生成一份退会申请,
|
||||||
@@ -17,8 +31,8 @@ public interface ClubUserJoinService extends BaseService<ClubUserApply> {
|
|||||||
ClubUserApply buildExitApply(String clubId, String userId);
|
ClubUserApply buildExitApply(String clubId, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取社团成员的入会时间,优先取历史有效入会申请记录,
|
* 获取社团成员的入会时间,优先取当前 club_user.joinTime。
|
||||||
* 没有时兜底为社团创办时间。
|
* 旧会员关系尚无入会时间时,保留历史申请及社团创办时间的原有回退规则。
|
||||||
*
|
*
|
||||||
* @param clubId 社团ID
|
* @param clubId 社团ID
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
|
|||||||
@@ -13,12 +13,24 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface SysClubExamineService extends BaseService<SysClubExamineRegister> {
|
public interface SysClubExamineService extends BaseService<SysClubExamineRegister> {
|
||||||
|
|
||||||
|
// 2026年10月2日wxg屏蔽操作:保留原统计接口说明。
|
||||||
|
// /**
|
||||||
|
// * 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。
|
||||||
|
// *
|
||||||
|
// * @param clubId 社团ID
|
||||||
|
// * @param year 统计年度
|
||||||
|
// * @return 在职和退休人员的年初、增加、减少、年末人数
|
||||||
|
// */
|
||||||
|
// List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
|
||||||
|
//
|
||||||
|
//
|
||||||
/**
|
/**
|
||||||
* 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。
|
* 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
*
|
* @param clubId 所选社团ID,不能为空
|
||||||
* @param clubId 社团ID
|
* @param year 所选公历年度,1至9998;按该年1月1日至下一年1月1日左闭右开统计
|
||||||
* @param year 统计年度
|
* @return 两类人员计数列表:yearFirstNum年初、yearAddNum增加、yearReduceNum减少、thisYearNum公式年末;
|
||||||
* @return 在职和退休人员的年初、增加、减少、年末人数
|
* personType为类别,userState仅兼容已有查看页标签;statisticsWarnings为核查提示,
|
||||||
|
* statisticsValid=false时各人数为null,表示数据缺失或冲突,不能当作零人
|
||||||
*/
|
*/
|
||||||
List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
|
List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ public interface SysClubInfoManageService extends BaseService<ClubCommonPageVo>
|
|||||||
|
|
||||||
Pagination<ClubCommonPageVo> infoManagePageData(@Valid ClubUserPageForm pageForm);
|
Pagination<ClubCommonPageVo> infoManagePageData(@Valid ClubUserPageForm pageForm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改时间:2026年10月2日 23:06:55;操作人:wxg。
|
||||||
|
* 当前会员查询前,用已完成 WF 入会流程的最近一次会长同意审核时间补齐空入会时间。
|
||||||
|
* @param pageForm clubId 为选中社团ID;radioType=4 查历史成员且不补齐,其他值查当前会员;
|
||||||
|
* 姓名/工号、人员类型、在职状态、入会日期范围及分页排序参数用于列表筛选
|
||||||
|
* @return 会员分页对象,list 中 joinTime 为会员关系保存的入会时间;历史成员仍取申请表时间
|
||||||
|
*/
|
||||||
Pagination<ClubUserCommonPageVo> infoManageUserPageData(@Valid ClubUserPageForm pageForm);
|
Pagination<ClubUserCommonPageVo> infoManageUserPageData(@Valid ClubUserPageForm pageForm);
|
||||||
|
|
||||||
Pagination<ClubUserCommonPageVo> exitManagePageData(@Valid ClubUserPageForm pageForm);
|
Pagination<ClubUserCommonPageVo> exitManagePageData(@Valid ClubUserPageForm pageForm);
|
||||||
@@ -30,6 +37,13 @@ public interface SysClubInfoManageService extends BaseService<ClubCommonPageVo>
|
|||||||
|
|
||||||
ClubUserJoinVo exitManageInfo(String id);
|
ClubUserJoinVo exitManageInfo(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端直接退会,同时新增系统处理记录并清理当前社团会员关系及角色关联。
|
||||||
|
* @param membershipId club_user 的会员关系ID,不是用户ID或申请ID
|
||||||
|
* @return void;会员关系不存在或已退会时抛出异常,事务回滚
|
||||||
|
*/
|
||||||
|
void exitClub(String membershipId);
|
||||||
|
|
||||||
List<NutMap> getClubTreeData();
|
List<NutMap> getClubTreeData();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ import java.util.List;
|
|||||||
**/
|
**/
|
||||||
public interface SysClubService extends BaseService<SysClub> {
|
public interface SysClubService extends BaseService<SysClub> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取入会申请的社团下拉选项,供 PC 与 H5 共用。
|
||||||
|
* @return 存在已完成 WF 流程的社团列表,每项为 SysClub,包含 id、clubName 等社团字段;同一社团只返回一次
|
||||||
|
*/
|
||||||
|
List<SysClub> listJoinClubOptions();
|
||||||
|
|
||||||
Pagination<ClubCommonPageVo> getAllClubWithPage(@Valid PageForm pageForm, Cnd cnd);
|
Pagination<ClubCommonPageVo> getAllClubWithPage(@Valid PageForm pageForm, Cnd cnd);
|
||||||
|
|
||||||
ClubRegisterVo findOne(String clubId);
|
ClubRegisterVo findOne(String clubId);
|
||||||
|
|||||||
@@ -17,6 +17,32 @@ import java.util.List;
|
|||||||
**/
|
**/
|
||||||
public interface SysClubUserService extends BaseService<ClubUser> {
|
public interface SysClubUserService extends BaseService<ClubUser> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为直接新增的会员关系设置本批次的后端确认时间,不覆盖已有入会时间。
|
||||||
|
* @param members 手工录入、导入或社团创建时直接新增的会员关系;不得传入审核入会记录
|
||||||
|
* @return void;仅填写列表内 joinTime 为空的对象,由调用方在同一事务中保存
|
||||||
|
*/
|
||||||
|
void prepareDirectJoinTimes(List<ClubUser> members);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 12:01:28;操作人:wxg:启动时定向补充会员表缺失列,不删除列或调整索引。
|
||||||
|
* @return void;DDL 失败则抛出异常,阻止缺少字段的应用继续启动
|
||||||
|
*/
|
||||||
|
void upgradeMemberSchema();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动升级时补齐缺失的姓名、工号、电话及 WF 会长审核入会时间;不覆盖非空历史数据。
|
||||||
|
* @return void;没有用户资料或有效 WF 审核记录的字段保留为空
|
||||||
|
*/
|
||||||
|
void backfillMemberSnapshots();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为真正新增的会员关系批量填写用户当前姓名、工号、电话,不修改入会时间。
|
||||||
|
* @param members 待新增的会员关系,必须包含有效 userId;不得传入仅修改身份的既有关系
|
||||||
|
* @return void;填写传入对象的 userName/loginName/mobile,由调用方在同一事务中保存
|
||||||
|
*/
|
||||||
|
void prepareMemberSnapshots(List<ClubUser> members);
|
||||||
|
|
||||||
List<NutMap> getClubUser(@Valid String clubId);
|
List<NutMap> getClubUser(@Valid String clubId);
|
||||||
|
|
||||||
Pagination pageDataByApplyJoinClubAudit(@Valid ClubUserPageForm pageForm);
|
Pagination pageDataByApplyJoinClubAudit(@Valid ClubUserPageForm pageForm);
|
||||||
|
|||||||
@@ -3,13 +3,35 @@ package com.budwk.app.zhgh.club.service.impl;
|
|||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.page.Pagination;
|
||||||
|
import com.budwk.app.base.param.PageForm;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import org.nutz.dao.Sqls;
|
||||||
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||||
|
import org.nutz.dao.util.cri.Static;
|
||||||
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import java.util.List;
|
||||||
|
import com.budwk.app.sys.views.View_user;
|
||||||
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import org.nutz.dao.util.Daos;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Date;
|
||||||
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
import com.budwk.app.zhgh.club.model.ClubPayRecord;
|
||||||
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -27,6 +49,301 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl<ClubPayRecord> imp
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:列表只读取已保存结果;缴费设置和退会操作负责更新 isGiveMoney。
|
||||||
|
private static final String ASSIGNED_SQL = "CASE WHEN cpr.isGiveMoney = true THEN 1 ELSE 0 END";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void upgradePayRecordSchema() {
|
||||||
|
// 定向检查独立于 DEBUG;DDL 在 MySQL 中会隐式提交,失败时终止升级以免运行缺列查询。
|
||||||
|
if (!dao().exists("club_pay_record")) {
|
||||||
|
dao().create(ClubPayRecord.class, false);
|
||||||
|
} else {
|
||||||
|
Daos.migration(dao(), ClubPayRecord.class, true, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void backfillPayRecordSnapshots() {
|
||||||
|
// 已有工号或姓名表示快照已建立;退休日期为空可能是真实资料,不能每次启动重新覆盖快照。
|
||||||
|
dao().execute(Sqls.create("""
|
||||||
|
UPDATE club_pay_record cpr
|
||||||
|
INNER JOIN vw_user u ON u.id = cpr.userId
|
||||||
|
SET cpr.loginName = COALESCE(cpr.loginName, u.loginName),
|
||||||
|
cpr.userName = COALESCE(cpr.userName, u.userName),
|
||||||
|
cpr.unitName = COALESCE(cpr.unitName, u.unitName),
|
||||||
|
cpr.personType = COALESCE(cpr.personType, u.personType),
|
||||||
|
cpr.retireDate = COALESCE(cpr.retireDate, u.retireDate)
|
||||||
|
WHERE cpr.loginName IS NULL AND cpr.userName IS NULL
|
||||||
|
"""));
|
||||||
|
// 按年度批量补时间,不能将本年度重新入会时间写入之前年度;无可靠来源保持为空。
|
||||||
|
for (NutMap row : listMap(Sqls.create("SELECT DISTINCT year FROM club_pay_record WHERE joinTime IS NULL AND year IS NOT NULL"))) {
|
||||||
|
int year = row.getInt("year");
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
UPDATE club_pay_record cpr
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT clubId, userId, MIN(joinTime) AS joinTime FROM club_user
|
||||||
|
WHERE joinTime < @yearEnd GROUP BY clubId, userId
|
||||||
|
) memberTime ON memberTime.clubId = cpr.clubId AND memberTime.userId = cpr.userId
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT application.clubId, application.userId, MAX(task.finishTime) AS joinTime
|
||||||
|
FROM club_user_apply application
|
||||||
|
INNER JOIN wf_process_instance instance ON instance.businessNo = application.id
|
||||||
|
INNER JOIN wf_process_task task ON task.processInstanceId = instance.id
|
||||||
|
WHERE application.mode = 1 AND instance.state = 20 AND task.taskState = 20
|
||||||
|
AND task.taskName = '1625a683-3890-4788-95b7-cab8240f6731'
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(task.variable, '$.submitType')) = '1'
|
||||||
|
AND task.finishTime < @yearEnd
|
||||||
|
GROUP BY application.clubId, application.userId
|
||||||
|
) approvalTime ON approvalTime.clubId = cpr.clubId AND approvalTime.userId = cpr.userId
|
||||||
|
SET cpr.joinTime = COALESCE(memberTime.joinTime, approvalTime.joinTime)
|
||||||
|
WHERE cpr.year = @year AND cpr.joinTime IS NULL
|
||||||
|
AND COALESCE(memberTime.joinTime, approvalTime.joinTime) IS NOT NULL
|
||||||
|
""");
|
||||||
|
sql.setParam("year", year);
|
||||||
|
sql.setParam("yearEnd", (year + 1) + "-01-01");
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void prepareSnapshots(List<ClubPayRecord> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 新增记录须明确所属年度、社团和人员,避免缺失参数形成不受限制的批量查询。
|
||||||
|
for (ClubPayRecord record : records) {
|
||||||
|
if (record == null || record.getYear() == null || record.getYear() < 1 || record.getYear() > 9998
|
||||||
|
|| StrUtil.isBlank(record.getClubId()) || StrUtil.isBlank(record.getUserId())) {
|
||||||
|
throw new IllegalArgumentException("缴费记录必须提供有效年度、社团和用户");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList();
|
||||||
|
List<String> clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList();
|
||||||
|
Map<String, View_user> users = new HashMap<>();
|
||||||
|
for (View_user user : dao().query(View_user.class, Cnd.where("id", "in", userIds))) {
|
||||||
|
users.put(user.getId(), user);
|
||||||
|
}
|
||||||
|
Map<String, Date> joinTimes = new HashMap<>();
|
||||||
|
Set<String> activeMembers = new HashSet<>();
|
||||||
|
for (ClubUser member : dao().query(ClubUser.class, Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds))) {
|
||||||
|
activeMembers.add(member.getClubId() + ":" + member.getUserId());
|
||||||
|
if (member.getJoinTime() != null) {
|
||||||
|
joinTimes.merge(member.getClubId() + ":" + member.getUserId(), member.getJoinTime(),
|
||||||
|
(first, next) -> first.before(next) ? first : next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ClubPayRecord record : records) {
|
||||||
|
View_user user = users.get(record.getUserId());
|
||||||
|
if (user != null && record.getLoginName() == null && record.getUserName() == null) {
|
||||||
|
record.setLoginName(user.getLoginname());
|
||||||
|
record.setUserName(user.getUsername());
|
||||||
|
if (record.getUnitName() == null) record.setUnitName(user.getUnitName());
|
||||||
|
if (record.getPersonType() == null) record.setPersonType(user.getPersonType());
|
||||||
|
if (record.getRetireDate() == null) record.setRetireDate(user.getRetireDate());
|
||||||
|
}
|
||||||
|
Date joinTime = joinTimes.get(record.getClubId() + ":" + record.getUserId());
|
||||||
|
if (record.getJoinTime() == null && joinTime != null
|
||||||
|
&& (record.getYear() == null || joinTime.before(DateUtil.parse((record.getYear() + 1) + "-01-01")))) {
|
||||||
|
record.setJoinTime(joinTime);
|
||||||
|
}
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:新增名单或入会更新缴费时,同步保存资格;退会成员优先判定不拨付。
|
||||||
|
record.setIsGiveMoney(qualifiesForPayment(record, activeMembers.contains(record.getClubId() + ":" + record.getUserId())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public int generateYearPayRecords(Integer year) {
|
||||||
|
if (year == null || year < 1 || year > 9998) {
|
||||||
|
throw new IllegalArgumentException("缴费年度不正确");
|
||||||
|
}
|
||||||
|
Set<String> recorded = new HashSet<>();
|
||||||
|
for (ClubPayRecord record : dao().query(ClubPayRecord.class, Cnd.where("year", "=", year))) {
|
||||||
|
recorded.add(record.getClubId() + ":" + record.getUserId());
|
||||||
|
}
|
||||||
|
List<ClubPayRecord> records = new ArrayList<>();
|
||||||
|
for (ClubUser member : dao().query(ClubUser.class, Cnd.NEW())) {
|
||||||
|
if (!recorded.add(member.getClubId() + ":" + member.getUserId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ClubPayRecord record = new ClubPayRecord();
|
||||||
|
record.setYear(year);
|
||||||
|
record.setClubId(member.getClubId());
|
||||||
|
record.setUserId(member.getUserId());
|
||||||
|
// 加入时间与人员资料交由 prepareSnapshots 批量读取,并校验所属年度。
|
||||||
|
record.setPayed(false);
|
||||||
|
JSONObject log = new JSONObject();
|
||||||
|
log.set("operatorUserId", "task");
|
||||||
|
log.set("operatorUserName", "定时任务");
|
||||||
|
log.set("operatorTime", DateUtil.now());
|
||||||
|
log.set("operatorType", "缴费管理-定时任务自动设置");
|
||||||
|
log.set("oldValue", null);
|
||||||
|
log.set("newValue", false);
|
||||||
|
record.setOperateLogs(List.of(log));
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
prepareSnapshots(records);
|
||||||
|
if (!records.isEmpty()) {
|
||||||
|
dao().insert(records);
|
||||||
|
}
|
||||||
|
return records.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校工会社团管理员与系统管理员可查看全部社团缴费记录。 */
|
||||||
|
private boolean canManageAllPayClubs() {
|
||||||
|
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 普通菜单授权用户仅能查看其有效角色关联的社团;无关联时显式返回空范围。 */
|
||||||
|
private void addPayClubScope(Cnd cnd, String clubField) {
|
||||||
|
if (!canManageAllPayClubs()) {
|
||||||
|
// 用户ID使用 SQL 参数绑定,社团集合交给 Cnd 处理,避免空集合被可选条件忽略。
|
||||||
|
Sql scopeSql = Sqls.create("""
|
||||||
|
SELECT DISTINCT ur.clubId FROM sys_user_role ur
|
||||||
|
INNER JOIN sys_role r ON r.id = ur.roleId AND r.disabled = false
|
||||||
|
WHERE ur.userId = @userId AND ur.enable = true AND ur.clubId IS NOT NULL
|
||||||
|
""").setParam("userId", SecurityUtil.getUserId());
|
||||||
|
List<String> clubIds = listMap(scopeSql).stream()
|
||||||
|
.map(row -> row.getString("clubId"))
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.toList();
|
||||||
|
if (clubIds.isEmpty()) {
|
||||||
|
cnd.and(new Static("1 = 0"));
|
||||||
|
} else {
|
||||||
|
cnd.and(clubField, "in", clubIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NutMap> getPayManageClubList() {
|
||||||
|
Sql sql = Sqls.create("SELECT c.id, c.clubName, c.clubCode FROM sys_club c $condition");
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
addPayClubScope(cnd, "c.id");
|
||||||
|
cnd.asc("c.clubCode");
|
||||||
|
cnd.asc("c.id");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
return listMap(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pagination pageData(PageForm pageForm, Integer year, String clubId, Integer payed, Integer assign, Integer memberState) {
|
||||||
|
// 2026年10月3日 14:33:18;操作人:wxg:当前会员状态为可选参数,service 拒绝非法值,避免错误条件被当作全部。
|
||||||
|
if (memberState != null && memberState != 0 && memberState != 1) {
|
||||||
|
throw new IllegalArgumentException("会员状态只能为在会、退会或全部");
|
||||||
|
}
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:人员资料直接读年度快照,取消用户视图及逐行会员子查询。
|
||||||
|
Sql sql = Sqls.create("SELECT cpr.*, c.clubName, cpr.joinTime AS applyTime, ("
|
||||||
|
+ ASSIGNED_SQL + ") AS assign FROM club_pay_record cpr "
|
||||||
|
+ "LEFT JOIN sys_club c ON cpr.clubId = c.id $condition");
|
||||||
|
Cnd cnd = Cnd.NEW();
|
||||||
|
cnd.andEX("cpr.year", "=", year);
|
||||||
|
cnd.andEX("cpr.clubId", "=", clubId);
|
||||||
|
addPayClubScope(cnd, "cpr.clubId");
|
||||||
|
cnd.andEX("cpr.payed", "=", payed);
|
||||||
|
// 在分页前筛选,保证总条数及页数准确;EXISTS 不会因重复会员关系增加缴费记录条数。
|
||||||
|
// 与列表 count 标签一致,只看该人员在该社团的当前关系,不依赖退会申请或缴费记录年份。
|
||||||
|
if (memberState != null) {
|
||||||
|
String membershipExists = "EXISTS (SELECT 1 FROM club_user membership "
|
||||||
|
+ "WHERE membership.clubId = cpr.clubId AND membership.userId = cpr.userId)";
|
||||||
|
cnd.and(new Static(memberState == 1 ? membershipExists : "NOT " + membershipExists));
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
|
SqlExpressionGroup seg = new SqlExpressionGroup();
|
||||||
|
seg.orLike("cpr.userName", pageForm.getSearchKeyword());
|
||||||
|
seg.orLike("cpr.loginName", pageForm.getSearchKeyword());
|
||||||
|
cnd.and(seg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assign != null) {
|
||||||
|
cnd.and(new Static("(" + ASSIGNED_SQL + ") = " + (assign == 0 ? 0 : 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全社团管理角色先按社团编号分组;普通角色仅在授权社团内按加入时间倒序。
|
||||||
|
if (canManageAllPayClubs()) {
|
||||||
|
cnd.asc("c.clubCode");
|
||||||
|
cnd.asc("c.id");
|
||||||
|
}
|
||||||
|
cnd.desc("cpr.joinTime");
|
||||||
|
cnd.asc("cpr.id");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
|
|
||||||
|
List<NutMap> list = pagination.getList();
|
||||||
|
Set<String> activeMembers = new HashSet<>();
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
List<String> userIds = list.stream().map(row -> row.getString("userId")).distinct().toList();
|
||||||
|
List<String> clubIds = list.stream().map(row -> row.getString("clubId")).distinct().toList();
|
||||||
|
Sql membershipSql = Sqls.create("SELECT clubId, userId FROM club_user $condition");
|
||||||
|
membershipSql.setCondition(Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds));
|
||||||
|
for (NutMap member : listMap(membershipSql)) {
|
||||||
|
activeMembers.add(member.getString("clubId") + ":" + member.getString("userId"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:当前状态实时查询,拨付状态使用保存值,查询期间不重算、不写库。
|
||||||
|
for (NutMap row : list) {
|
||||||
|
row.put("count", activeMembers.contains(row.getString("clubId") + ":" + row.getString("userId")) ? 1 : 0);
|
||||||
|
row.put("assign", row.getInt("assign") == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void updatePayStatus(String[] ids, Boolean payed) {
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:明确记录ID及缴费布尔值,避免空参数造成批量误更新。
|
||||||
|
if (ids == null || ids.length == 0 || payed == null || java.util.Arrays.stream(ids).anyMatch(StrUtil::isBlank)) {
|
||||||
|
throw new IllegalArgumentException("请选择缴费记录并提供缴费状态");
|
||||||
|
}
|
||||||
|
Cnd scope = Cnd.where("id", "in", ids);
|
||||||
|
addPayClubScope(scope, "clubId");
|
||||||
|
List<ClubPayRecord> records = dao().query(ClubPayRecord.class, scope);
|
||||||
|
if (records.size() != java.util.Arrays.stream(ids).distinct().count()) {
|
||||||
|
throw new IllegalArgumentException("部分缴费记录不存在或无权操作,请刷新列表");
|
||||||
|
}
|
||||||
|
List<String> userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList();
|
||||||
|
List<String> clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList();
|
||||||
|
Set<String> activeMembers = new HashSet<>();
|
||||||
|
for (ClubUser member : dao().query(ClubUser.class, Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds))) {
|
||||||
|
activeMembers.add(member.getClubId() + ":" + member.getUserId());
|
||||||
|
}
|
||||||
|
for (ClubPayRecord record : records) {
|
||||||
|
List<JSONObject> logs = record.getOperateLogs() == null ? new ArrayList<>() : new ArrayList<>(record.getOperateLogs());
|
||||||
|
JSONObject log = new JSONObject();
|
||||||
|
log.set("operatorUserId", SecurityUtil.getUserId());
|
||||||
|
log.set("operatorUserName", SecurityUtil.getUserUsername());
|
||||||
|
log.set("operatorTime", DateUtil.now());
|
||||||
|
log.set("operatorType", "缴费管理-手动设置");
|
||||||
|
log.set("oldValue", record.getPayed());
|
||||||
|
log.set("newValue", payed);
|
||||||
|
log.set("oldIsGiveMoney", record.getIsGiveMoney());
|
||||||
|
record.setPayed(payed);
|
||||||
|
record.setIsGiveMoney(qualifiesForPayment(record, activeMembers.contains(record.getClubId() + ":" + record.getUserId())));
|
||||||
|
log.set("newIsGiveMoney", record.getIsGiveMoney());
|
||||||
|
logs.add(log);
|
||||||
|
record.setOperateLogs(logs);
|
||||||
|
}
|
||||||
|
dao().update(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void disableExitMemberPayment(String clubId, String userId) {
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:退会影响实时会员关系对应的各年度名单,保留原缴费状态。
|
||||||
|
if (StrUtil.isBlank(clubId) || StrUtil.isBlank(userId)) {
|
||||||
|
throw new IllegalArgumentException("退会社团和用户不能为空");
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("UPDATE club_pay_record SET isGiveMoney = false WHERE clubId = @clubId AND userId = @userId");
|
||||||
|
sql.setParam("clubId", clubId);
|
||||||
|
sql.setParam("userId", userId);
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean calAssigned(String userId, String clubId) {
|
public boolean calAssigned(String userId, String clubId) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
@@ -45,18 +362,23 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl<ClubPayRecord> imp
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean calAssigned(ClubPayRecord payRecord) {
|
public boolean calAssigned(ClubPayRecord payRecord) {
|
||||||
// 如果是未缴费,直接是不拨付
|
// 2026年10月3日 09:51:00;操作人:wxg:业务更新时计算资格,会员关系不存在即对应页面退会标签。
|
||||||
if (!payRecord.getPayed()) {
|
boolean active = payRecord != null && dao().count(ClubUser.class, Cnd.where("clubId", "=", payRecord.getClubId())
|
||||||
return false;
|
.and("userId", "=", payRecord.getUserId())) > 0;
|
||||||
}
|
return qualifiesForPayment(payRecord, active);
|
||||||
Sys_user user = dao().fetch(Sys_user.class, payRecord.getUserId());
|
}
|
||||||
if (!Objects.equals("退休", user.getUserState())) {
|
|
||||||
return true;
|
/** 退休年份规则沿用原口径;未缴费、已退会或退休日期缺失时不拨付。 */
|
||||||
}
|
private boolean qualifiesForPayment(ClubPayRecord payRecord, boolean active) {
|
||||||
if (Lang.isEmpty(user.getRetireDate())) {
|
if (!active || payRecord == null || !Boolean.TRUE.equals(payRecord.getPayed())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int retireYear = DateUtil.year(user.getRetireDate());
|
if (!Objects.equals("离退休人员", payRecord.getPersonType())) {
|
||||||
return retireYear >= (payRecord.getYear() != null ? payRecord.getYear() : DateUtil.thisYear());
|
return true;
|
||||||
}
|
}
|
||||||
|
if (payRecord.getRetireDate() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return DateUtil.year(payRecord.getRetireDate()) >= (payRecord.getYear() != null ? payRecord.getYear() : DateUtil.thisYear());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,77 @@ import com.budwk.app.base.constant.RoleConstant;
|
|||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.budwk.app.flow.constant.FlowConst;
|
||||||
|
import com.budwk.app.flow.entity.ProcessInstance;
|
||||||
|
import com.budwk.app.flow.entity.ProcessTask;
|
||||||
|
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
|
||||||
|
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||||
import com.budwk.app.sys.views.View_user;
|
import com.budwk.app.sys.views.View_user;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
import com.budwk.app.zhgh.club.model.SysClub;
|
import com.budwk.app.zhgh.club.model.SysClub;
|
||||||
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Chain;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
|
public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> implements ClubUserJoinService {
|
||||||
|
// PC 与 H5 入会资格校验共用此名单,人员类型须与用户视图中的值完全一致。
|
||||||
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
|
private static final List<String> ALLOW_JOIN_PERSON_TYPES = List.of(
|
||||||
"教职工",
|
"教职工",
|
||||||
"未起薪人员",
|
|
||||||
"教职工新入会",
|
|
||||||
"教职工其他",
|
|
||||||
"劳动服务公司",
|
"劳动服务公司",
|
||||||
"后勤聘用人员",
|
"劳务派遣人员"
|
||||||
"退休人员[一]",
|
|
||||||
"退休人员[二]",
|
|
||||||
"校聘劳务派遣全额"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
public ClubUserJoinServiceImpl(Dao dao) {
|
public ClubUserJoinServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preparePendingJoinTimes(ClubUserApply application) {
|
||||||
|
if (Integer.valueOf(1).equals(application.getMode())) {
|
||||||
|
// 审核前尚未正式入会;后端清除客户端回传或草稿遗留的时间,退会申请保留原入会时间。
|
||||||
|
application.setJoinTime(null);
|
||||||
|
application.setExitTime(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Date completeJoinApproval(String applicationId, Long taskId) {
|
||||||
|
if (StrUtil.isBlank(applicationId) || taskId == null) {
|
||||||
|
throw new IllegalArgumentException("入会申请或会长审核任务不能为空");
|
||||||
|
}
|
||||||
|
// WF 引擎已先保存完成时间;Execution 中的任务对象可能仍是完成前的数据,必须重新查库。
|
||||||
|
ProcessTask task = dao().fetch(ProcessTask.class, taskId);
|
||||||
|
if (task == null
|
||||||
|
|| !"1625a683-3890-4788-95b7-cab8240f6731".equals(task.getTaskName())
|
||||||
|
|| !ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())
|
||||||
|
|| task.getFinishTime() == null
|
||||||
|
|| StrUtil.isBlank(task.getVariable())
|
||||||
|
|| !ProcessSubmitTypeEnum.AGREE.getCode().equals(JSONUtil.parseObj(task.getVariable()).getInt(FlowConst.SUBMIT_TYPE))) {
|
||||||
|
throw new IllegalArgumentException("没有已完成且同意的会长入会审核任务");
|
||||||
|
}
|
||||||
|
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
|
||||||
|
ClubUserApply application = fetch(applicationId);
|
||||||
|
// 只更新当前任务所属的入会申请,不覆盖其他申请、退会记录或之前的入会周期。
|
||||||
|
if (instance == null || !Objects.equals(applicationId, instance.getBusinessNo())
|
||||||
|
|| application == null || !Integer.valueOf(1).equals(application.getMode())) {
|
||||||
|
throw new IllegalArgumentException("会长审核任务与入会申请不匹配");
|
||||||
|
}
|
||||||
|
dao().update(ClubUserApply.class, Chain.make("joinTime", task.getFinishTime()), Cnd.where("id", "=", applicationId));
|
||||||
|
return task.getFinishTime();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String checkJoinQualification(String userId) {
|
public String checkJoinQualification(String userId) {
|
||||||
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId));
|
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId));
|
||||||
@@ -51,6 +92,8 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClubUserApply buildExitApply(String clubId, String userId) {
|
public ClubUserApply buildExitApply(String clubId, String userId) {
|
||||||
|
// 退会以当前会员关系的入会时间为准,不能沿用上一轮退出申请的时间。
|
||||||
|
ClubUser currentMember = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId));
|
||||||
// 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。
|
// 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。
|
||||||
ClubUserApply latestApply = fetch(
|
ClubUserApply latestApply = fetch(
|
||||||
Cnd.where("userId", "=", userId)
|
Cnd.where("userId", "=", userId)
|
||||||
@@ -59,11 +102,14 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
|
|||||||
);
|
);
|
||||||
if (latestApply != null) {
|
if (latestApply != null) {
|
||||||
latestApply.setId(null);
|
latestApply.setId(null);
|
||||||
|
if (currentMember != null && currentMember.getJoinTime() != null) {
|
||||||
|
latestApply.setJoinTime(currentMember.getJoinTime());
|
||||||
|
}
|
||||||
return latestApply;
|
return latestApply;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于会长等注册社团时直接入会的人员,没有申请记录时从当前成员信息兜底生成退会申请。
|
// 对于会长等注册社团时直接入会的人员,没有申请记录时从当前成员信息兜底生成退会申请。
|
||||||
ClubUser clubUser = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId));
|
ClubUser clubUser = currentMember;
|
||||||
if (clubUser == null) {
|
if (clubUser == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -105,6 +151,11 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl<ClubUserApply> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Date resolveJoinTime(String clubId, String userId) {
|
public Date resolveJoinTime(String clubId, String userId) {
|
||||||
|
// 新会员直接使用关系表的入会时间;旧记录尚未补齐时仍保留原有回退逻辑。
|
||||||
|
ClubUser member = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId));
|
||||||
|
if (member != null && member.getJoinTime() != null) {
|
||||||
|
return member.getJoinTime();
|
||||||
|
}
|
||||||
ClubUserApply latestJoinApply = fetch(
|
ClubUserApply latestJoinApply = fetch(
|
||||||
Cnd.where("userId", "=", userId)
|
Cnd.where("userId", "=", userId)
|
||||||
.and("clubId", "=", clubId)
|
.and("clubId", "=", clubId)
|
||||||
|
|||||||
+483
-241
@@ -36,6 +36,7 @@ import java.util.Comparator;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -50,260 +51,501 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// 2026年10月2日wxg屏蔽操作:原统计入口及专用辅助代码逐行注释,保留原文便于对比或恢复。
|
||||||
public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
// @Override
|
||||||
Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
|
// public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
||||||
Date nextYearStart = DateUtil.parse((year + 1) + "-01-01 00:00:00");
|
// Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
|
||||||
List<MembershipPeriod> membershipPeriods = getMembershipPeriods(clubId);
|
// Date nextYearStart = DateUtil.parse((year + 1) + "-01-01 00:00:00");
|
||||||
NutMap activeResult = createUserChangeResult("在职");
|
// List<MembershipPeriod> membershipPeriods = getMembershipPeriods(clubId);
|
||||||
NutMap retiredResult = createUserChangeResult("退休");
|
// NutMap activeResult = createUserChangeResult("在职");
|
||||||
|
// NutMap retiredResult = createUserChangeResult("退休");
|
||||||
|
//
|
||||||
|
// for (MembershipPeriod period : membershipPeriods) {
|
||||||
|
// // 非正常历史会员没有退会时间且没有已完成退会申请时,无法确定所属年度,不计入统计。
|
||||||
|
// if (!period.normal && period.exitTime == null) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// if (isMemberAtYearBoundary(period, yearStart)) {
|
||||||
|
// increaseResult(classifyAtBoundary(period, yearStart), activeResult, retiredResult, "yearFirstNum");
|
||||||
|
// }
|
||||||
|
// if (isInYear(period.joinTime, yearStart, nextYearStart)) {
|
||||||
|
// increaseResult(classifyAtEvent(period, period.joinTime), activeResult, retiredResult, "yearAddNum");
|
||||||
|
// }
|
||||||
|
// if (isInYear(period.exitTime, yearStart, nextYearStart)) {
|
||||||
|
// increaseResult(classifyBeforeEvent(period, period.exitTime), activeResult, retiredResult, "yearReduceNum");
|
||||||
|
// }
|
||||||
|
// // 退休发生在会员有效期内时,作为在职减少一人、退休增加一人处理。
|
||||||
|
// if (isInYear(period.retireDate, yearStart, nextYearStart)
|
||||||
|
// && period.joinTime.before(period.retireDate)
|
||||||
|
// && (period.exitTime == null || period.exitTime.after(period.retireDate))) {
|
||||||
|
// increaseResult("在职", activeResult, retiredResult, "yearReduceNum");
|
||||||
|
// increaseResult("退休", activeResult, retiredResult, "yearAddNum");
|
||||||
|
// }
|
||||||
|
// if (isMemberAtYearBoundary(period, nextYearStart)) {
|
||||||
|
// increaseResult(classifyAtBoundary(period, nextYearStart), activeResult, retiredResult, "thisYearNum");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return List.of(activeResult, retiredResult);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。
|
||||||
|
// * 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。
|
||||||
|
// */
|
||||||
|
// private List<MembershipPeriod> getMembershipPeriods(String clubId) {
|
||||||
|
// Sql legacySql = Sqls.create("""
|
||||||
|
// SELECT
|
||||||
|
// cu.userId,
|
||||||
|
// CAST(cu.joinTime AS DATETIME) joinTime,
|
||||||
|
// STR_TO_DATE(NULLIF(cu.changeTime, ''), '%Y-%m-%d %H:%i:%s') exitTime,
|
||||||
|
// cu.isNormal,
|
||||||
|
// u.userState,
|
||||||
|
// u.retireDate
|
||||||
|
// FROM sys_club_user cu
|
||||||
|
// LEFT JOIN sys_user u ON u.id = cu.userId
|
||||||
|
// WHERE cu.clubId = @clubId
|
||||||
|
// AND cu.status = 5
|
||||||
|
// AND cu.joinTime IS NOT NULL
|
||||||
|
// AND cu.joinTime <> ''
|
||||||
|
// """).setParam("clubId", clubId);
|
||||||
|
// List<MembershipPeriod> periods = listMap(legacySql).stream()
|
||||||
|
// .map(row -> new MembershipPeriod(
|
||||||
|
// row.getString("userId"),
|
||||||
|
// row.getTime("joinTime"),
|
||||||
|
// row.getTime("exitTime"),
|
||||||
|
// !Boolean.FALSE.equals(row.getBoolean("isNormal")),
|
||||||
|
// true,
|
||||||
|
// row.getString("userState"),
|
||||||
|
// row.getTime("retireDate")
|
||||||
|
// ))
|
||||||
|
// .filter(period -> period.joinTime != null)
|
||||||
|
// .collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
//
|
||||||
|
// Sql applySql = Sqls.create("""
|
||||||
|
// SELECT
|
||||||
|
// cu.userId,
|
||||||
|
// cu.id,
|
||||||
|
// cu.mode,
|
||||||
|
// cu.applyDate,
|
||||||
|
// cu.joinTime,
|
||||||
|
// cu.exitTime,
|
||||||
|
// u.userState,
|
||||||
|
// u.retireDate
|
||||||
|
// FROM club_user_apply cu
|
||||||
|
// LEFT JOIN sys_user u ON u.id = cu.userId
|
||||||
|
// WHERE cu.clubId = @clubId
|
||||||
|
// AND COALESCE(cu.delFlag, 0) = 0
|
||||||
|
// ORDER BY cu.applyDate, cu.mode
|
||||||
|
// """)
|
||||||
|
// .setParam("clubId", clubId);
|
||||||
|
// List<NutMap> applications = listMap(applySql);
|
||||||
|
// if (!applications.isEmpty()) {
|
||||||
|
// // 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。
|
||||||
|
// List<String> applicationIds = applications.stream()
|
||||||
|
// .map(application -> application.getString("id"))
|
||||||
|
// .filter(StrUtil::isNotBlank)
|
||||||
|
// .collect(Collectors.toList());
|
||||||
|
// List<ProcessInstance> finishedInstances = applicationIds.isEmpty() ? new ArrayList<>()
|
||||||
|
// : dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds)
|
||||||
|
// .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode()));
|
||||||
|
// Set<String> finishedBusinessNos = finishedInstances.stream()
|
||||||
|
// .map(ProcessInstance::getBusinessNo)
|
||||||
|
// .filter(StrUtil::isNotBlank)
|
||||||
|
// .collect(Collectors.toCollection(HashSet::new));
|
||||||
|
// applications.removeIf(application -> !finishedBusinessNos.contains(application.getString("id")));
|
||||||
|
// }
|
||||||
|
// applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo)));
|
||||||
|
//
|
||||||
|
// for (NutMap application : applications) {
|
||||||
|
// Date applyDate = application.getTime("applyDate");
|
||||||
|
// Boolean mode = application.getBoolean("mode");
|
||||||
|
// if (applyDate == null || mode == null) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// if (mode) {
|
||||||
|
// mergeJoinApplication(periods, application, applyDate);
|
||||||
|
// } else {
|
||||||
|
// mergeExitApplication(periods, application, applyDate);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return periods;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 已迁移的入会申请与旧会员记录精确对应时不重复新增;迁移后的新申请以审核申请时间作为入会时间。
|
||||||
|
// */
|
||||||
|
// private void mergeJoinApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
||||||
|
// String userId = application.getString("userId");
|
||||||
|
// Date applicationJoinTime = application.getTime("joinTime");
|
||||||
|
// boolean representedByLegacy = periods.stream()
|
||||||
|
// .filter(period -> period.legacy && Objects.equals(period.userId, userId))
|
||||||
|
// .filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
||||||
|
// .anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
|
||||||
|
// boolean representedByNewPeriod = periods.stream()
|
||||||
|
// .filter(period -> !period.legacy && Objects.equals(period.userId, userId))
|
||||||
|
// .anyMatch(period -> isMemberAt(period, applyDate));
|
||||||
|
// if (representedByLegacy || representedByNewPeriod) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// periods.add(new MembershipPeriod(
|
||||||
|
// userId,
|
||||||
|
// applyDate,
|
||||||
|
// null,
|
||||||
|
// true,
|
||||||
|
// false,
|
||||||
|
// application.getString("userState"),
|
||||||
|
// application.getTime("retireDate")
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
|
||||||
|
// */
|
||||||
|
// private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
||||||
|
// String userId = application.getString("userId");
|
||||||
|
// Date applicationJoinTime = application.getTime("joinTime");
|
||||||
|
// Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
|
||||||
|
// MembershipPeriod target = periods.stream()
|
||||||
|
// .filter(period -> Objects.equals(period.userId, userId))
|
||||||
|
// .filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
||||||
|
// .filter(period -> period.exitTime == null)
|
||||||
|
// .max(Comparator.comparing(period -> period.joinTime))
|
||||||
|
// .orElse(null);
|
||||||
|
// if (target == null) {
|
||||||
|
// target = periods.stream()
|
||||||
|
// .filter(period -> Objects.equals(period.userId, userId))
|
||||||
|
// .filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
|
||||||
|
// .max(Comparator.comparing(period -> period.joinTime))
|
||||||
|
// .orElse(null);
|
||||||
|
// }
|
||||||
|
// if (target != null) {
|
||||||
|
// target.exitTime = exitTime;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private NutMap createUserChangeResult(String userState) {
|
||||||
|
// return NutMap.NEW()
|
||||||
|
// .addv("userState", userState)
|
||||||
|
// .addv("yearFirstNum", 0)
|
||||||
|
// .addv("yearAddNum", 0)
|
||||||
|
// .addv("yearReduceNum", 0)
|
||||||
|
// .addv("thisYearNum", 0);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private void increaseResult(String userState, NutMap activeResult, NutMap retiredResult, String field) {
|
||||||
|
// NutMap result = "退休".equals(userState) ? retiredResult : "在职".equals(userState) ? activeResult : null;
|
||||||
|
// if (result != null) {
|
||||||
|
// result.put(field, result.getInt(field) + 1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isInYear(Date date, Date yearStart, Date nextYearStart) {
|
||||||
|
// return date != null && !date.before(yearStart) && date.before(nextYearStart);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isMemberAt(MembershipPeriod period, Date date) {
|
||||||
|
// return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) {
|
||||||
|
// return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String classifyAtBoundary(MembershipPeriod period, Date boundary) {
|
||||||
|
// if (period.retireDate != null) {
|
||||||
|
// return period.retireDate.before(boundary) ? "退休" : "在职";
|
||||||
|
// }
|
||||||
|
// return normalizeUserState(period.userState);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String classifyAtEvent(MembershipPeriod period, Date eventTime) {
|
||||||
|
// if (period.retireDate != null) {
|
||||||
|
// return period.retireDate.after(eventTime) ? "在职" : "退休";
|
||||||
|
// }
|
||||||
|
// return normalizeUserState(period.userState);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String classifyBeforeEvent(MembershipPeriod period, Date eventTime) {
|
||||||
|
// if (period.retireDate != null) {
|
||||||
|
// return period.retireDate.before(eventTime) ? "退休" : "在职";
|
||||||
|
// }
|
||||||
|
// return normalizeUserState(period.userState);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String normalizeUserState(String userState) {
|
||||||
|
// // 历史成员数据可能未同步人员状态,空值不参与在职、退休人数统计。
|
||||||
|
// if (StrUtil.isBlank(userState)) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// if (List.of("在职", "在岗").contains(userState)) {
|
||||||
|
// return "在职";
|
||||||
|
// }
|
||||||
|
// if (List.of("退休", "退休【变号】").contains(userState)) {
|
||||||
|
// return "退休";
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private static class MembershipPeriod {
|
||||||
|
// private final String userId;
|
||||||
|
// private final Date joinTime;
|
||||||
|
// private Date exitTime;
|
||||||
|
// private final boolean normal;
|
||||||
|
// private final boolean legacy;
|
||||||
|
// private final String userState;
|
||||||
|
// private final Date retireDate;
|
||||||
|
//
|
||||||
|
// private MembershipPeriod(String userId, Date joinTime, Date exitTime, boolean normal, boolean legacy,
|
||||||
|
// String userState, Date retireDate) {
|
||||||
|
// this.userId = userId;
|
||||||
|
// this.joinTime = joinTime;
|
||||||
|
// this.exitTime = exitTime;
|
||||||
|
// this.normal = normal;
|
||||||
|
// this.legacy = legacy;
|
||||||
|
// this.userState = userState;
|
||||||
|
// this.retireDate = retireDate;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 2026年10月2日22:00点开始新开发的代码。
|
||||||
|
// 实际整改时间:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
|
@Override
|
||||||
|
public List<NutMap> getClubUserNum(String clubId, Integer year) {
|
||||||
|
if (StrUtil.isBlank(clubId) || year == null || year < 1 || year > 9998) {
|
||||||
|
throw new IllegalArgumentException("请选择有效社团和统计年度");
|
||||||
|
}
|
||||||
|
MemberChangeStatistics stats = new MemberChangeStatistics(year);
|
||||||
|
// 年初基数来自当年名单,不受缴费、拨付或当前退会状态影响;同一会员按 userId 去重。
|
||||||
|
List<NutMap> roster = listMap(Sqls.create("SELECT userId, joinTime, personType, retireDate, operateLogs FROM club_pay_record WHERE clubId = @clubId AND year = @year")
|
||||||
|
.setParam("clubId", clubId).setParam("year", year));
|
||||||
|
if (roster.isEmpty()) {
|
||||||
|
stats.invalidate(year + "年度该社团没有缴费名单,请检查年度名单生成计划任务及名单数据,不能将缺失名单视为年初零人。");
|
||||||
|
return stats.result();
|
||||||
|
}
|
||||||
|
boolean generated = roster.stream().anyMatch(row -> {
|
||||||
|
String logs = row.getString("operateLogs");
|
||||||
|
return logs != null && logs.contains("缴费管理-定时任务自动设置");
|
||||||
|
});
|
||||||
|
if (!generated) {
|
||||||
|
stats.warnings.add("未发现该社团年度名单的定时生成记录,请检查计划任务及名单完整性;手工入会记录不能证明年度名单已完整生成。");
|
||||||
|
}
|
||||||
|
Map<String, NutMap> snapshots = new java.util.LinkedHashMap<>();
|
||||||
|
Map<String, AnnualMemberPeriod> periods = new java.util.LinkedHashMap<>();
|
||||||
|
for (NutMap row : roster) {
|
||||||
|
String userId = row.getString("userId");
|
||||||
|
if (StrUtil.isBlank(userId) || row.getTime("joinTime") == null) {
|
||||||
|
stats.invalidate("缴费名单存在用户或加入时间缺失,请补齐后核查统计。");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NutMap prior = snapshots.putIfAbsent(userId, row);
|
||||||
|
if (prior != null) {
|
||||||
|
stats.warnings.add("缴费名单存在同一会员重复记录,已按用户去重,请核查。");
|
||||||
|
if (!Objects.equals(prior.getTime("joinTime"), row.getTime("joinTime"))) {
|
||||||
|
stats.invalidate("重复缴费记录的加入时间不一致,无法确定年初基数,请核查。");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AnnualMemberPeriod period = addAnnualPeriod(periods, userId, row.getTime("joinTime"), stats);
|
||||||
|
if (period != null) period.rosterMember = true;
|
||||||
|
}
|
||||||
|
// 只接纳已完成 WF 的 mode=0/1;mode=2 是已生效的系统退会,不需要 WF 实例。
|
||||||
|
// mode 显式转为整数,避免 MySQL tinyint(1) 被驱动返回布尔值后将系统退会误判为入会。
|
||||||
|
List<NutMap> applications = listMap(Sqls.create("""
|
||||||
|
SELECT a.userId, a.id, CAST(a.mode AS SIGNED) AS mode, a.joinTime, a.exitTime
|
||||||
|
FROM club_user_apply a
|
||||||
|
WHERE a.clubId = @clubId AND COALESCE(a.delFlag, 0) = 0
|
||||||
|
AND (a.mode = 2 OR (a.mode IN (0, 1) AND EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = a.id AND ins.state = 20)))
|
||||||
|
""").setParam("clubId", clubId));
|
||||||
|
for (NutMap application : applications) {
|
||||||
|
Date joinTime = application.getTime("joinTime");
|
||||||
|
Date exitTime = application.getTime("exitTime");
|
||||||
|
int mode = application.getInt("mode");
|
||||||
|
if (mode == 1 && joinTime == null) {
|
||||||
|
stats.invalidate("已完成入会申请缺少 joinTime,请核查会长审核时间。");
|
||||||
|
} else if (mode != 1 && exitTime == null) {
|
||||||
|
stats.invalidate("已生效退会记录缺少 exitTime,请核查退会数据。");
|
||||||
|
}
|
||||||
|
// 年初只使用缴费基数,旧入会申请不能另建一个年初周期;同年退会资料补充手工入会。
|
||||||
|
if (stats.inYear(joinTime) && (mode == 1 || stats.inYear(exitTime))) {
|
||||||
|
// 退会记录中的入会时间也能还原已被物理删除的手工入会周期。
|
||||||
|
addAnnualPeriod(periods, application.getString("userId"), joinTime, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<NutMap> members = listMap(Sqls.create("SELECT userId, joinTime FROM club_user WHERE clubId = @clubId").setParam("clubId", clubId));
|
||||||
|
for (NutMap member : members) {
|
||||||
|
if (member.getTime("joinTime") == null) {
|
||||||
|
if (!snapshots.containsKey(member.getString("userId"))) stats.invalidate("当前会员存在加入时间缺失,无法确定年度增加,请核查。");
|
||||||
|
} else {
|
||||||
|
// 手工录入、导入和成立社团时加入的成员没有 mode=1,也需要计入可靠的年度增加。
|
||||||
|
addAnnualPeriod(periods, member.getString("userId"), member.getTime("joinTime"), stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (NutMap application : applications) {
|
||||||
|
if (application.getInt("mode") == 1 || application.getTime("exitTime") == null) continue;
|
||||||
|
Date exitTime = application.getTime("exitTime");
|
||||||
|
if (!exitTime.before(stats.end)) continue;
|
||||||
|
String userId = application.getString("userId");
|
||||||
|
Date joinTime = application.getTime("joinTime");
|
||||||
|
AnnualMemberPeriod target = joinTime == null ? null : periods.get(periodKey(userId, joinTime));
|
||||||
|
// 旧退会申请可能仍保存迁移前的入会日期,只关闭当时唯一有效的最近周期。
|
||||||
|
if (target == null && (joinTime == null || joinTime.before(stats.start))) {
|
||||||
|
target = periods.values().stream().filter(period -> Objects.equals(userId, period.userId)
|
||||||
|
&& !period.joinTime.after(exitTime) && (period.exitTime == null || period.exitTime.equals(exitTime)))
|
||||||
|
.max(Comparator.comparing(period -> period.joinTime)).orElse(null);
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
if (!exitTime.before(stats.start)) stats.invalidate("年度退会记录无法匹配有效入会周期,请核查加入时间。");
|
||||||
|
} else if (exitTime.before(target.joinTime)) {
|
||||||
|
stats.invalidate("退会时间早于入会时间,请核查申请记录。");
|
||||||
|
} else if (target.exitTime == null || exitTime.before(target.exitTime)) {
|
||||||
|
target.exitTime = exitTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<String> userIds = periods.values().stream().map(period -> period.userId).collect(Collectors.toSet());
|
||||||
|
Map<String, NutMap> profiles = new java.util.HashMap<>();
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
// 人员快照可能早于退休资料补录,批量读取当前人员类型及退休日期,不读取 userState。
|
||||||
|
Sql profileSql = Sqls.create("SELECT id, personType, retireDate FROM sys_user $condition");
|
||||||
|
profileSql.setCondition(Cnd.where("id", "in", userIds));
|
||||||
|
for (NutMap profile : listMap(profileSql)) profiles.put(profile.getString("id"), profile);
|
||||||
|
}
|
||||||
|
Map<String, Date> previousEnd = new java.util.HashMap<>();
|
||||||
|
Set<String> retiredTransfers = new HashSet<>();
|
||||||
|
List<AnnualMemberPeriod> ordered = new ArrayList<>(periods.values());
|
||||||
|
ordered.sort(Comparator.comparing((AnnualMemberPeriod period) -> period.userId).thenComparing(period -> period.joinTime));
|
||||||
|
for (AnnualMemberPeriod period : ordered) {
|
||||||
|
// 同一用户不能同时占有两个未结束的周期;异常不强行合并,提示核查。
|
||||||
|
if (previousEnd.containsKey(period.userId)) {
|
||||||
|
Date priorEnd = previousEnd.get(period.userId);
|
||||||
|
if (priorEnd == null || priorEnd.after(period.joinTime)) stats.invalidate("同一会员的入会周期重叠,请核查重复入会或缺失的退会记录。");
|
||||||
|
}
|
||||||
|
previousEnd.put(period.userId, period.exitTime);
|
||||||
|
NutMap snapshot = snapshots.get(period.userId);
|
||||||
|
NutMap profile = profiles.get(period.userId);
|
||||||
|
String snapshotType = snapshot == null ? null : snapshot.getString("personType");
|
||||||
|
String currentType = profile == null ? snapshotType : profile.getString("personType");
|
||||||
|
if (StrUtil.isBlank(currentType)) currentType = snapshotType;
|
||||||
|
Date retireDate = profile == null ? null : profile.getTime("retireDate");
|
||||||
|
if (retireDate == null && snapshot != null) retireDate = snapshot.getTime("retireDate");
|
||||||
|
boolean atStart = period.joinTime.before(stats.start) && (period.exitTime == null || !period.exitTime.before(stats.start));
|
||||||
|
if (atStart && period.rosterMember) {
|
||||||
|
stats.increase(classifyPersonType(snapshotType, retireDate, stats.start, true, stats), "yearFirstNum");
|
||||||
|
} else if (atStart) {
|
||||||
|
stats.invalidate("存在年初已入会但不在年度缴费基数中的会员,请检查名单完整性。");
|
||||||
|
}
|
||||||
|
if (stats.inYear(period.joinTime)) {
|
||||||
|
stats.increase(classifyPersonType(currentType, retireDate, period.joinTime, false, stats), "yearAddNum");
|
||||||
|
}
|
||||||
|
if (stats.inYear(period.exitTime)) {
|
||||||
|
stats.increase(classifyPersonType(currentType, retireDate, period.exitTime, false, stats), "yearReduceNum");
|
||||||
|
}
|
||||||
|
// 同日同刻采用先退休再退会;入会时已退休只算退休入会,不额外算一次转类。
|
||||||
|
if (stats.inYear(retireDate) && period.joinTime.before(retireDate)
|
||||||
|
&& (period.exitTime == null || !period.exitTime.before(retireDate))
|
||||||
|
&& recognizedPersonType(currentType) && retiredTransfers.add(period.userId)) {
|
||||||
|
stats.increase("在职人员", "yearReduceNum");
|
||||||
|
stats.increase("离退休人员", "yearAddNum");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats.result();
|
||||||
|
}
|
||||||
|
|
||||||
for (MembershipPeriod period : membershipPeriods) {
|
/** 合并同一用户、同一加入时间的多来源记录;年度末边界不属于本年。 */
|
||||||
// 非正常历史会员没有退会时间且没有已完成退会申请时,无法确定所属年度,不计入统计。
|
private AnnualMemberPeriod addAnnualPeriod(Map<String, AnnualMemberPeriod> periods, String userId, Date joinTime, MemberChangeStatistics stats) {
|
||||||
if (!period.normal && period.exitTime == null) {
|
if (StrUtil.isBlank(userId)) {
|
||||||
continue;
|
stats.invalidate("会员记录缺少用户ID,请核查。");
|
||||||
}
|
return null;
|
||||||
if (isMemberAtYearBoundary(period, yearStart)) {
|
}
|
||||||
increaseResult(classifyAtBoundary(period, yearStart), activeResult, retiredResult, "yearFirstNum");
|
if (joinTime == null || !joinTime.before(stats.end)) return null;
|
||||||
}
|
return periods.computeIfAbsent(periodKey(userId, joinTime), key -> new AnnualMemberPeriod(userId, joinTime));
|
||||||
if (isInYear(period.joinTime, yearStart, nextYearStart)) {
|
}
|
||||||
increaseResult(classifyAtEvent(period, period.joinTime), activeResult, retiredResult, "yearAddNum");
|
|
||||||
}
|
|
||||||
if (isInYear(period.exitTime, yearStart, nextYearStart)) {
|
|
||||||
increaseResult(classifyBeforeEvent(period, period.exitTime), activeResult, retiredResult, "yearReduceNum");
|
|
||||||
}
|
|
||||||
// 退休发生在会员有效期内时,作为在职减少一人、退休增加一人处理。
|
|
||||||
if (isInYear(period.retireDate, yearStart, nextYearStart)
|
|
||||||
&& period.joinTime.before(period.retireDate)
|
|
||||||
&& (period.exitTime == null || period.exitTime.after(period.retireDate))) {
|
|
||||||
increaseResult("在职", activeResult, retiredResult, "yearReduceNum");
|
|
||||||
increaseResult("退休", activeResult, retiredResult, "yearAddNum");
|
|
||||||
}
|
|
||||||
if (isMemberAtYearBoundary(period, nextYearStart)) {
|
|
||||||
increaseResult(classifyAtBoundary(period, nextYearStart), activeResult, retiredResult, "thisYearNum");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of(activeResult, retiredResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private String periodKey(String userId, Date joinTime) {
|
||||||
* 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。
|
return userId + ":" + joinTime.getTime();
|
||||||
* 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。
|
}
|
||||||
*/
|
|
||||||
private List<MembershipPeriod> getMembershipPeriods(String clubId) {
|
|
||||||
Sql legacySql = Sqls.create("""
|
|
||||||
SELECT
|
|
||||||
cu.userId,
|
|
||||||
CAST(cu.joinTime AS DATETIME) joinTime,
|
|
||||||
STR_TO_DATE(NULLIF(cu.changeTime, ''), '%Y-%m-%d %H:%i:%s') exitTime,
|
|
||||||
cu.isNormal,
|
|
||||||
u.userState,
|
|
||||||
u.retireDate
|
|
||||||
FROM sys_club_user cu
|
|
||||||
LEFT JOIN sys_user u ON u.id = cu.userId
|
|
||||||
WHERE cu.clubId = @clubId
|
|
||||||
AND cu.status = 5
|
|
||||||
AND cu.joinTime IS NOT NULL
|
|
||||||
AND cu.joinTime <> ''
|
|
||||||
""").setParam("clubId", clubId);
|
|
||||||
List<MembershipPeriod> periods = listMap(legacySql).stream()
|
|
||||||
.map(row -> new MembershipPeriod(
|
|
||||||
row.getString("userId"),
|
|
||||||
row.getTime("joinTime"),
|
|
||||||
row.getTime("exitTime"),
|
|
||||||
!Boolean.FALSE.equals(row.getBoolean("isNormal")),
|
|
||||||
true,
|
|
||||||
row.getString("userState"),
|
|
||||||
row.getTime("retireDate")
|
|
||||||
))
|
|
||||||
.filter(period -> period.joinTime != null)
|
|
||||||
.collect(Collectors.toCollection(ArrayList::new));
|
|
||||||
|
|
||||||
Sql applySql = Sqls.create("""
|
/** 只接纳指定的四种人员类型,其他已知类型不纳入这两类统计。 */
|
||||||
SELECT
|
private boolean recognizedPersonType(String personType) {
|
||||||
cu.userId,
|
return List.of("教职工", "劳务派遣人员", "劳动服务公司", "离退休人员").contains(personType == null ? "" : personType);
|
||||||
cu.id,
|
}
|
||||||
cu.mode,
|
|
||||||
cu.applyDate,
|
|
||||||
cu.joinTime,
|
|
||||||
cu.exitTime,
|
|
||||||
u.userState,
|
|
||||||
u.retireDate
|
|
||||||
FROM club_user_apply cu
|
|
||||||
LEFT JOIN sys_user u ON u.id = cu.userId
|
|
||||||
WHERE cu.clubId = @clubId
|
|
||||||
AND COALESCE(cu.delFlag, 0) = 0
|
|
||||||
ORDER BY cu.applyDate, cu.mode
|
|
||||||
""")
|
|
||||||
.setParam("clubId", clubId);
|
|
||||||
List<NutMap> applications = listMap(applySql);
|
|
||||||
if (!applications.isEmpty()) {
|
|
||||||
// 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。
|
|
||||||
List<String> applicationIds = applications.stream()
|
|
||||||
.map(application -> application.getString("id"))
|
|
||||||
.filter(StrUtil::isNotBlank)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
List<ProcessInstance> finishedInstances = applicationIds.isEmpty() ? new ArrayList<>()
|
|
||||||
: dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds)
|
|
||||||
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode()));
|
|
||||||
Set<String> finishedBusinessNos = finishedInstances.stream()
|
|
||||||
.map(ProcessInstance::getBusinessNo)
|
|
||||||
.filter(StrUtil::isNotBlank)
|
|
||||||
.collect(Collectors.toCollection(HashSet::new));
|
|
||||||
applications.removeIf(application -> !finishedBusinessNos.contains(application.getString("id")));
|
|
||||||
}
|
|
||||||
applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo)));
|
|
||||||
|
|
||||||
for (NutMap application : applications) {
|
/** 年初边界当天退休仍属于年初在职基数,随后转类;事件时刻已退休则归入离退休。 */
|
||||||
Date applyDate = application.getTime("applyDate");
|
private String classifyPersonType(String personType, Date retireDate, Date moment, boolean boundary, MemberChangeStatistics stats) {
|
||||||
Boolean mode = application.getBoolean("mode");
|
if (StrUtil.isBlank(personType)) {
|
||||||
if (applyDate == null || mode == null) {
|
stats.invalidate("人员类型为空,不能按在职状态替代分类,请核查人员资料。");
|
||||||
continue;
|
return null;
|
||||||
}
|
}
|
||||||
if (mode) {
|
if (!recognizedPersonType(personType)) {
|
||||||
mergeJoinApplication(periods, application, applyDate);
|
stats.warnings.add("存在不属于指定两类的人员类型,已排除,请核查统计范围。");
|
||||||
} else {
|
return null;
|
||||||
mergeExitApplication(periods, application, applyDate);
|
}
|
||||||
}
|
if (retireDate != null) {
|
||||||
}
|
boolean retired = boundary ? retireDate.before(moment) : !retireDate.after(moment);
|
||||||
return periods;
|
return retired ? "离退休人员" : "在职人员";
|
||||||
}
|
}
|
||||||
|
return "离退休人员".equals(personType) ? "离退休人员" : "在职人员";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private static class AnnualMemberPeriod {
|
||||||
* 已迁移的入会申请与旧会员记录精确对应时不重复新增;迁移后的新申请以审核申请时间作为入会时间。
|
final String userId;
|
||||||
*/
|
final Date joinTime;
|
||||||
private void mergeJoinApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
Date exitTime;
|
||||||
String userId = application.getString("userId");
|
boolean rosterMember;
|
||||||
Date applicationJoinTime = application.getTime("joinTime");
|
|
||||||
boolean representedByLegacy = periods.stream()
|
|
||||||
.filter(period -> period.legacy && Objects.equals(period.userId, userId))
|
|
||||||
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
|
||||||
.anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
|
|
||||||
boolean representedByNewPeriod = periods.stream()
|
|
||||||
.filter(period -> !period.legacy && Objects.equals(period.userId, userId))
|
|
||||||
.anyMatch(period -> isMemberAt(period, applyDate));
|
|
||||||
if (representedByLegacy || representedByNewPeriod) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
periods.add(new MembershipPeriod(
|
|
||||||
userId,
|
|
||||||
applyDate,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
application.getString("userState"),
|
|
||||||
application.getTime("retireDate")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
AnnualMemberPeriod(String userId, Date joinTime) {
|
||||||
* 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
|
this.userId = userId;
|
||||||
*/
|
this.joinTime = joinTime;
|
||||||
private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
|
}
|
||||||
String userId = application.getString("userId");
|
}
|
||||||
Date applicationJoinTime = application.getTime("joinTime");
|
|
||||||
Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
|
|
||||||
MembershipPeriod target = periods.stream()
|
|
||||||
.filter(period -> Objects.equals(period.userId, userId))
|
|
||||||
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
|
|
||||||
.filter(period -> period.exitTime == null)
|
|
||||||
.max(Comparator.comparing(period -> period.joinTime))
|
|
||||||
.orElse(null);
|
|
||||||
if (target == null) {
|
|
||||||
target = periods.stream()
|
|
||||||
.filter(period -> Objects.equals(period.userId, userId))
|
|
||||||
.filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
|
|
||||||
.max(Comparator.comparing(period -> period.joinTime))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
if (target != null) {
|
|
||||||
target.exitTime = exitTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NutMap createUserChangeResult(String userState) {
|
/** 返回两类计数及核查信息;userState 仅为已有查看页的显示字段别名,不作为分类数据来源。 */
|
||||||
return NutMap.NEW()
|
private static class MemberChangeStatistics {
|
||||||
.addv("userState", userState)
|
final Date start;
|
||||||
.addv("yearFirstNum", 0)
|
final Date end;
|
||||||
.addv("yearAddNum", 0)
|
final Set<String> warnings = new java.util.LinkedHashSet<>();
|
||||||
.addv("yearReduceNum", 0)
|
final List<NutMap> rows = List.of(row("在职人员"), row("离退休人员"));
|
||||||
.addv("thisYearNum", 0);
|
boolean valid = true;
|
||||||
}
|
|
||||||
|
|
||||||
private void increaseResult(String userState, NutMap activeResult, NutMap retiredResult, String field) {
|
MemberChangeStatistics(int year) {
|
||||||
NutMap result = "退休".equals(userState) ? retiredResult : "在职".equals(userState) ? activeResult : null;
|
start = DateUtil.parse(year + "-01-01 00:00:00");
|
||||||
if (result != null) {
|
end = DateUtil.parse((year + 1) + "-01-01 00:00:00");
|
||||||
result.put(field, result.getInt(field) + 1);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInYear(Date date, Date yearStart, Date nextYearStart) {
|
static NutMap row(String type) {
|
||||||
return date != null && !date.before(yearStart) && date.before(nextYearStart);
|
return NutMap.NEW().addv("personType", type).addv("userState", type)
|
||||||
}
|
.addv("yearFirstNum", 0).addv("yearAddNum", 0).addv("yearReduceNum", 0).addv("thisYearNum", 0);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isMemberAt(MembershipPeriod period, Date date) {
|
boolean inYear(Date time) {
|
||||||
return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date));
|
return time != null && !time.before(start) && time.before(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) {
|
void invalidate(String message) {
|
||||||
return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary));
|
valid = false;
|
||||||
}
|
warnings.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
private String classifyAtBoundary(MembershipPeriod period, Date boundary) {
|
void increase(String type, String field) {
|
||||||
if (period.retireDate != null) {
|
for (NutMap row : rows) if (row.getString("personType").equals(type)) row.put(field, row.getInt(field) + 1);
|
||||||
return period.retireDate.before(boundary) ? "退休" : "在职";
|
}
|
||||||
}
|
|
||||||
return normalizeUserState(period.userState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String classifyAtEvent(MembershipPeriod period, Date eventTime) {
|
List<NutMap> result() {
|
||||||
if (period.retireDate != null) {
|
for (NutMap row : rows) {
|
||||||
return period.retireDate.after(eventTime) ? "在职" : "退休";
|
int yearEnd = row.getInt("yearFirstNum") + row.getInt("yearAddNum") - row.getInt("yearReduceNum");
|
||||||
}
|
row.put("thisYearNum", yearEnd);
|
||||||
return normalizeUserState(period.userState);
|
if (yearEnd < 0) invalidate("年末人数出现负数,请核查年初名单与入退会记录。");
|
||||||
}
|
}
|
||||||
|
for (NutMap row : rows) {
|
||||||
private String classifyBeforeEvent(MembershipPeriod period, Date eventTime) {
|
row.put("statisticsValid", valid);
|
||||||
if (period.retireDate != null) {
|
row.put("statisticsWarnings", new ArrayList<>(warnings));
|
||||||
return period.retireDate.before(eventTime) ? "退休" : "在职";
|
// 无法确定基数或周期时展示空值,避免用户将部分计数当成完整统计提交。
|
||||||
}
|
if (!valid) for (String field : List.of("yearFirstNum", "yearAddNum", "yearReduceNum", "thisYearNum")) row.put(field, null);
|
||||||
return normalizeUserState(period.userState);
|
}
|
||||||
}
|
return rows;
|
||||||
|
}
|
||||||
private String normalizeUserState(String userState) {
|
}
|
||||||
// 历史成员数据可能未同步人员状态,空值不参与在职、退休人数统计。
|
|
||||||
if (StrUtil.isBlank(userState)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (List.of("在职", "在岗").contains(userState)) {
|
|
||||||
return "在职";
|
|
||||||
}
|
|
||||||
if (List.of("退休", "退休【变号】").contains(userState)) {
|
|
||||||
return "退休";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class MembershipPeriod {
|
|
||||||
private final String userId;
|
|
||||||
private final Date joinTime;
|
|
||||||
private Date exitTime;
|
|
||||||
private final boolean normal;
|
|
||||||
private final boolean legacy;
|
|
||||||
private final String userState;
|
|
||||||
private final Date retireDate;
|
|
||||||
|
|
||||||
private MembershipPeriod(String userId, Date joinTime, Date exitTime, boolean normal, boolean legacy,
|
|
||||||
String userState, Date retireDate) {
|
|
||||||
this.userId = userId;
|
|
||||||
this.joinTime = joinTime;
|
|
||||||
this.exitTime = exitTime;
|
|
||||||
this.normal = normal;
|
|
||||||
this.legacy = legacy;
|
|
||||||
this.userState = userState;
|
|
||||||
this.retireDate = retireDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NutMap> getJgUser(String clubId) {
|
public List<NutMap> getJgUser(String clubId) {
|
||||||
|
|||||||
+281
-89
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.club.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.budwk.app.base.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||||
@@ -9,6 +10,14 @@ import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
|||||||
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
|
import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum;
|
||||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
|
import com.budwk.app.sys.models.Sys_role;
|
||||||
|
import com.budwk.app.sys.services.SysRoleService;
|
||||||
|
import com.budwk.app.sys.services.SysUserService;
|
||||||
|
import com.budwk.app.zhgh.club.model.ClubUserApply;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubUserJoinService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import com.budwk.app.sys.models.Sys_user_role;
|
import com.budwk.app.sys.models.Sys_user_role;
|
||||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||||
@@ -39,12 +48,17 @@ import org.nutz.lang.Strings;
|
|||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -64,12 +78,74 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
private SysClubService sysClubService;
|
private SysClubService sysClubService;
|
||||||
@Inject
|
@Inject
|
||||||
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
||||||
|
@Inject
|
||||||
|
private ClubUserJoinService clubUserJoinService;
|
||||||
|
@Inject
|
||||||
|
private ClubPayRecordService payRecordService;
|
||||||
|
@Inject
|
||||||
|
private SysUserService sysUserService;
|
||||||
|
@Inject
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
|
|
||||||
public SysClubInfoManageServiceImpl(Dao dao) {
|
public SysClubInfoManageServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接退会不启动 WF;系统处理记录、会员关系删除和角色清理必须同时成功。
|
||||||
|
* @param membershipId 当前 club_user 会员关系ID
|
||||||
|
* @return void;重复退会或会员不存在时抛出异常,避免生成重复记录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void exitClub(String membershipId) {
|
||||||
|
ClubUser member = dao().fetch(ClubUser.class, membershipId);
|
||||||
|
if (member == null) {
|
||||||
|
throw new IllegalArgumentException("该会员已退会或会员关系不存在");
|
||||||
|
}
|
||||||
|
ClubUserApply source = clubUserJoinService.buildExitApply(member.getClubId(), member.getUserId());
|
||||||
|
if (source == null) {
|
||||||
|
throw new IllegalArgumentException("无法获取该会员的退会信息");
|
||||||
|
}
|
||||||
|
// 使用新对象,只复制业务资料,防止沿用历史申请ID、创建人和创建时间。
|
||||||
|
ClubUserApply application = new ClubUserApply();
|
||||||
|
application.setClubId(member.getClubId());
|
||||||
|
application.setUserId(member.getUserId());
|
||||||
|
application.setRoleCode(source.getRoleCode());
|
||||||
|
application.setClubPosition(source.getClubPosition());
|
||||||
|
application.setEmail(source.getEmail());
|
||||||
|
application.setMobile(source.getMobile());
|
||||||
|
application.setBirthday(source.getBirthday());
|
||||||
|
application.setAvatar(source.getAvatar());
|
||||||
|
application.setSameTimeJoinOtherClubSituation(source.getSameTimeJoinOtherClubSituation());
|
||||||
|
application.setAwardsExperience(source.getAwardsExperience());
|
||||||
|
application.setSignature(source.getSignature());
|
||||||
|
application.setJoinTime(source.getJoinTime());
|
||||||
|
Date exitTime = new Date();
|
||||||
|
application.setMode(2);
|
||||||
|
application.setApplyDate(exitTime);
|
||||||
|
application.setExitTime(exitTime);
|
||||||
|
// 条件删除只能成功一次;并发重复操作不再新增退会记录,delFlag 沿用默认值。
|
||||||
|
if (dao().delete(ClubUser.class, membershipId) != 1) {
|
||||||
|
throw new IllegalArgumentException("该会员已退会,请刷新列表");
|
||||||
|
}
|
||||||
|
dao().insert(application);
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:系统直接退会与取消拨付处于同一事务,避免退会后仍保存可拨付。
|
||||||
|
payRecordService.disableExitMemberPayment(member.getClubId(), member.getUserId());
|
||||||
|
if (member.getRoleCode() != null) {
|
||||||
|
for (String roleCode : member.getRoleCode()) {
|
||||||
|
Sys_role role = sysRoleService.getByCode(roleCode);
|
||||||
|
if (role != null) {
|
||||||
|
dao().clear(Sys_user_role.class, Cnd.where("userId", "=", member.getUserId())
|
||||||
|
.and("roleId", "=", role.getId()).and("clubId", "=", member.getClubId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sysUserService.clearCache();
|
||||||
|
sysRoleService.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubCommonPageVo> infoManagePageData(ClubUserPageForm pageForm) {
|
public Pagination<ClubCommonPageVo> infoManagePageData(ClubUserPageForm pageForm) {
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
@@ -129,102 +205,188 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 12:01:28;操作人:wxg:当前会员以 club_user 分页,查询期间不补齐或修改数据库。
|
||||||
|
* @param pageForm clubId 为选中社团ID;radioType=4 查历史成员,其他值查当前会员;
|
||||||
|
* searchKeyword 匹配会员表姓名/工号,personType/userState 筛选当前用户资料;
|
||||||
|
* focusMembers=true 同时筛选当前人员类型为自聘人员或其他的会员,false/空值不限制;
|
||||||
|
* joinDateStart/joinDateEnd 为 yyyy-MM-dd,pageNumber/pageSize 为分页参数,排序只允许列表字段
|
||||||
|
* @return 会员分页;姓名、工号、电话、joinTime 来自会员表,其余人员资料只补充本页;历史页沿用申请记录
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubUserCommonPageVo> infoManageUserPageData(ClubUserPageForm pageForm) {
|
public Pagination<ClubUserCommonPageVo> infoManageUserPageData(ClubUserPageForm pageForm) {
|
||||||
|
if (Integer.valueOf(4).equals(pageForm.getRadioType())) {
|
||||||
|
return historyMemberPageData(pageForm);
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(pageForm.getClubId())) {
|
||||||
|
throw new IllegalArgumentException("请选择社团后查询会员信息");
|
||||||
|
}
|
||||||
|
// 姓名、工号、电话及入会时间使用持久化字段;列排序需要其他资料时才关联用户表。
|
||||||
|
Map<String, String> sortColumns = Map.of(
|
||||||
|
"loginName", "scu.loginName", "userName", "scu.userName", "mobile", "scu.mobile",
|
||||||
|
"joinTime", "scu.joinTime", "sex", "u.sex", "personType", "u.personType",
|
||||||
|
"userAttribute", "u.userAttribute", "userState", "u.userState", "unitName", "u.unitName");
|
||||||
|
String orderColumn = StrUtil.isBlank(pageForm.getPageOrderName()) ? null : sortColumns.get(pageForm.getPageOrderName());
|
||||||
|
boolean orderRequested = orderColumn != null && StrUtil.isNotBlank(pageForm.getPageOrderBy());
|
||||||
|
boolean needsUnitSort = orderRequested && "unitName".equals(pageForm.getPageOrderName());
|
||||||
|
boolean needsUser = Boolean.TRUE.equals(pageForm.getFocusMembers())
|
||||||
|
|| StrUtil.isNotBlank(pageForm.getPersonType()) || StrUtil.isNotBlank(pageForm.getUserState())
|
||||||
|
|| (orderRequested && orderColumn.startsWith("u."));
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT scu.*
|
||||||
scu.*,
|
FROM club_user scu
|
||||||
u.username as userName,
|
$userJoin
|
||||||
u.loginname as loginName,
|
|
||||||
u.sex,
|
|
||||||
u.personType as personType,
|
|
||||||
u.userState,
|
|
||||||
u.unitid as unitId,
|
|
||||||
u.mobile,
|
|
||||||
club.clubName,
|
|
||||||
u.unitname as unitName,
|
|
||||||
DATE_FORMAT(IFNULL(joinInfo.joinTime, club.foundTime), '%Y-%m-%d %H:%i:%s') as joinTime
|
|
||||||
FROM
|
|
||||||
club_user scu
|
|
||||||
LEFT JOIN sys_club club ON scu.clubId = club.id
|
|
||||||
RIGHT JOIN `vw_user` u ON scu.userId = u.id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
joinApply.userId,
|
|
||||||
joinApply.clubId,
|
|
||||||
MAX(joinApply.joinTime) AS joinTime
|
|
||||||
FROM
|
|
||||||
club_user_apply joinApply
|
|
||||||
LEFT JOIN wf_process_instance joinIns ON joinIns.businessNo = joinApply.id
|
|
||||||
WHERE
|
|
||||||
joinApply.mode = true
|
|
||||||
AND joinIns.state = 20
|
|
||||||
GROUP BY
|
|
||||||
joinApply.userId,
|
|
||||||
joinApply.clubId
|
|
||||||
) joinInfo ON joinInfo.userId = scu.userId AND joinInfo.clubId = scu.clubId
|
|
||||||
$condition
|
$condition
|
||||||
$order
|
|
||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
// 默认查询和姓名/工号/日期筛选不关联 vw_user;单位排序保留视图的单位名称口径。
|
||||||
|
sql.setVar("userJoin", needsUnitSort ? "LEFT JOIN vw_user u ON u.id = scu.userId"
|
||||||
cnd.and("scu.clubId", "=", pageForm.getClubId());
|
: needsUser ? "LEFT JOIN sys_user u ON u.id = scu.userId" : "");
|
||||||
if (StrUtil.isNotBlank(pageForm.getPersonType())) {
|
Cnd cnd = Cnd.where("scu.clubId", "=", pageForm.getClubId());
|
||||||
cnd.and("u.personType", "=", pageForm.getPersonType());
|
// 在分页和总数统计前同时筛选两类人员,保持当前社团及其他查询条件的限制。
|
||||||
|
if (Boolean.TRUE.equals(pageForm.getFocusMembers())) {
|
||||||
|
cnd.and("u.personType", "in", List.of("自聘人员", "其他"));
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(pageForm.getUserState())) {
|
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
||||||
cnd.and("u.userState", "=", pageForm.getUserState());
|
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
|
cnd.and(Cnd.exps("scu.loginName", "like", "%" + pageForm.getSearchKeyword() + "%")
|
||||||
|
.or("scu.userName", "like", "%" + pageForm.getSearchKeyword() + "%"));
|
||||||
}
|
}
|
||||||
if (Strings.isNotBlank(pageForm.getSearchKeyword())) {
|
// 结束日期使用次日零点上界,包含结束当天;只读取已保存的时间,不触发 WF 补齐 SQL。
|
||||||
cnd.and(Cnd.exps("loginname", "like", "%" + pageForm.getSearchKeyword() + "%").or("username", "like", "%" + pageForm.getSearchKeyword() + "%"));
|
if (StrUtil.isNotBlank(pageForm.getJoinDateStart())) {
|
||||||
|
cnd.and("scu.joinTime", ">=", LocalDate.parse(pageForm.getJoinDateStart()).toString());
|
||||||
}
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getJoinDateEnd())) {
|
||||||
if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
cnd.and("scu.joinTime", "<", LocalDate.parse(pageForm.getJoinDateEnd()).plusDays(1).toString());
|
||||||
cnd.orderBy(pageForm.getPageOrderName(), "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
}
|
||||||
|
if (orderRequested) {
|
||||||
|
cnd.orderBy(orderColumn, "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
} else {
|
} else {
|
||||||
sql.setVar("order", """
|
cnd.desc("scu.joinTime");
|
||||||
ORDER BY
|
}
|
||||||
CASE
|
cnd.asc("scu.id");
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_PRESIDENT"') THEN 1
|
if (Integer.valueOf(1).equals(pageForm.getRadioType())) {
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_VICE_PRESIDENT"') THEN 2
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_SECRETARY"') THEN 3
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_VICE_SECRETARY"') THEN 4
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_OPERATOR"') THEN 5
|
|
||||||
WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_MEMBER"') THEN 6
|
|
||||||
ELSE 99
|
|
||||||
END
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
//查询理事机构
|
|
||||||
if (pageForm.getRadioType() != null && 1 == pageForm.getRadioType()) {
|
|
||||||
cnd.and(new Static("NOT JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
cnd.and(new Static("NOT JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
||||||
}
|
}
|
||||||
//查询社团成员
|
if (Integer.valueOf(2).equals(pageForm.getRadioType())) {
|
||||||
if (pageForm.getRadioType() != null && 2 == pageForm.getRadioType()) {
|
|
||||||
cnd.and(new Static("JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
cnd.and(new Static("JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')"));
|
||||||
//cnd.and(new Static("JSON_LENGTH(scu.roleCode) = 1"));
|
|
||||||
}
|
}
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
Pagination<ClubUserCommonPageVo> listPageVO = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
Pagination<ClubUserCommonPageVo> page = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
||||||
List<ClubUserCommonPageVo> listMap = listPageVO.getList(ClubUserCommonPageVo.class);
|
List<ClubUserCommonPageVo> members = page.getList(ClubUserCommonPageVo.class);
|
||||||
for (ClubUserCommonPageVo vo : listMap) {
|
fillMemberPageDetails(members, pageForm.getClubId());
|
||||||
List<String> list = vo.getRoleCode();
|
for (ClubUserCommonPageVo member : members) {
|
||||||
List<String> cleanedCodes = list.stream()
|
List<String> roleCodes = member.getRoleCode() == null ? List.of() : member.getRoleCode();
|
||||||
.filter(Objects::nonNull)
|
List<String> cleanedCodes = roleCodes.stream().filter(Objects::nonNull).map(String::strip)
|
||||||
.map(String::strip)
|
|
||||||
.map(code -> code.startsWith("\"") && code.endsWith("\"")
|
.map(code -> code.startsWith("\"") && code.endsWith("\"")
|
||||||
? code.substring(1, code.length() - 1)
|
? code.substring(1, code.length() - 1) : code).toList();
|
||||||
: code)
|
member.setRoleName(SysClubUserServiceImpl.convertRoleName(cleanedCodes));
|
||||||
.toList();
|
member.setRoleCode(cleanedCodes);
|
||||||
String roleName = SysClubUserServiceImpl.convertRoleName(cleanedCodes);
|
|
||||||
vo.setRoleName(roleName);
|
|
||||||
vo.setRoleCode(cleanedCodes);
|
|
||||||
}
|
}
|
||||||
listPageVO.setList(listMap);
|
page.setList(members);
|
||||||
return listPageVO;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 12:01:28;操作人:wxg:分页后批量获取本页其他资料,避免把完整用户视图带入默认统计和分页。
|
||||||
|
* @param members 当前页会员关系;空页不查询,姓名/工号/电话/入会时间始终保留会员表值
|
||||||
|
* @param clubId 当前选中社团ID,用于补充社团名称
|
||||||
|
*/
|
||||||
|
private void fillMemberPageDetails(List<ClubUserCommonPageVo> members, String clubId) {
|
||||||
|
if (members.isEmpty()) return;
|
||||||
|
List<String> userIds = members.stream().map(ClubUser::getUserId).filter(Objects::nonNull).distinct().toList();
|
||||||
|
Map<String, NutMap> users = new HashMap<>();
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
Sql details = Sqls.create("""
|
||||||
|
SELECT id AS id, sex AS sex, personType AS personType, userAttribute AS userAttribute,
|
||||||
|
userState AS userState, unitId AS unitId, unitName AS unitName
|
||||||
|
FROM vw_user
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
details.setCondition(Cnd.where("id", "in", userIds));
|
||||||
|
for (NutMap user : listMap(details)) users.put(user.getString("id"), user);
|
||||||
|
}
|
||||||
|
SysClub club = dao().fetch(SysClub.class, clubId);
|
||||||
|
for (ClubUserCommonPageVo member : members) {
|
||||||
|
member.setClubName(club == null ? null : club.getClubName());
|
||||||
|
NutMap user = users.get(member.getUserId());
|
||||||
|
// 用户资料缺失仍展示会员关系及已保存资料,不用当前用户资料覆盖三个持久化字段。
|
||||||
|
if (user == null) continue;
|
||||||
|
member.setSex(user.getString("sex"));
|
||||||
|
member.setPersonType(user.getString("personType"));
|
||||||
|
member.setUserAttribute(user.getString("userAttribute"));
|
||||||
|
member.setUserState(user.getString("userState"));
|
||||||
|
member.setUnitId(user.getString("unitId"));
|
||||||
|
member.setUnitName(user.getString("unitName"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前社团历史退会记录分页;每次退会保留一行,不按人员去重,也不要求关联 WF 已完成流程。
|
||||||
|
* @param pageForm clubId 为当前社团ID;姓名/工号、人员类型、在职状态及 yyyy-MM-dd 入会日期范围为可选筛选;
|
||||||
|
* focusMembers=true 筛选当前人员类型为自聘人员或其他的历史成员,false/空值不限制;
|
||||||
|
* pageNumber/pageSize 为分页参数,pageOrderName/pageOrderBy 为列排序参数
|
||||||
|
* @return 会员展示分页对象;id 为申请ID,joinTime/exitTime 来自申请,人员资料取用户视图当前值
|
||||||
|
*/
|
||||||
|
private Pagination<ClubUserCommonPageVo> historyMemberPageData(ClubUserPageForm pageForm) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
cua.id, cua.clubId, cua.userId, cua.roleCode AS applyRoleCode,
|
||||||
|
u.username AS userName, u.loginname AS loginName, u.sex, u.mobile,
|
||||||
|
u.personType, u.userAttribute, u.userState,
|
||||||
|
u.unitid AS unitId, u.unitname AS unitName,
|
||||||
|
DATE_FORMAT(cua.joinTime, '%Y-%m-%d %H:%i:%s') AS joinTime,
|
||||||
|
DATE_FORMAT(cua.exitTime, '%Y-%m-%d %H:%i:%s') AS exitTime
|
||||||
|
FROM club_user_apply cua
|
||||||
|
LEFT JOIN vw_user u ON u.id = cua.userId
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
Cnd cnd = Cnd.where("cua.clubId", "=", pageForm.getClubId())
|
||||||
|
.and("cua.mode", "in", List.of(0, 2));
|
||||||
|
// 历史页同样按用户当前人员类型筛选,两类人员共同参与分页和总数统计。
|
||||||
|
if (Boolean.TRUE.equals(pageForm.getFocusMembers())) {
|
||||||
|
cnd.and("u.personType", "in", List.of("自聘人员", "其他"));
|
||||||
|
}
|
||||||
|
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
||||||
|
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
|
cnd.and(Cnd.exps("u.loginname", "like", "%" + pageForm.getSearchKeyword() + "%")
|
||||||
|
.or("u.username", "like", "%" + pageForm.getSearchKeyword() + "%"));
|
||||||
|
}
|
||||||
|
// 时间查询保持入会日期语义,结束日期用次日零点上界,包含选择的结束当天。
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getJoinDateStart())) {
|
||||||
|
cnd.and("cua.joinTime", ">=", LocalDate.parse(pageForm.getJoinDateStart()).toString());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getJoinDateEnd())) {
|
||||||
|
cnd.and("cua.joinTime", "<", LocalDate.parse(pageForm.getJoinDateEnd()).plusDays(1).toString());
|
||||||
|
}
|
||||||
|
// 仅允许表格实际展示的列参与排序,退出时间相同则按申请ID保证分页顺序稳定。
|
||||||
|
List<String> sortableFields = List.of("loginName", "userName", "sex", "personType", "userAttribute",
|
||||||
|
"userState", "unitName", "joinTime", "exitTime");
|
||||||
|
if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())
|
||||||
|
&& sortableFields.contains(pageForm.getPageOrderName())) {
|
||||||
|
cnd.orderBy(pageForm.getPageOrderName(), "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
cnd.desc("cua.exitTime");
|
||||||
|
}
|
||||||
|
cnd.asc("cua.id");
|
||||||
|
sql.setCondition(cnd);
|
||||||
|
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
||||||
|
List<ClubUserCommonPageVo> members = pagination.getList(ClubUserCommonPageVo.class);
|
||||||
|
for (ClubUserCommonPageVo member : members) {
|
||||||
|
if (StrUtil.isNotBlank(member.getApplyRoleCode())) {
|
||||||
|
member.setRoleName(SysClubUserServiceImpl.convertRoleName(List.of(member.getApplyRoleCode())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pagination.setList(members);
|
||||||
|
return pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 19:42:45;操作人:wxg:查询审核退会、系统处理与历史直接移出记录。
|
||||||
|
* @param pageForm clubId、姓名/工号、personType、userState 为可选筛选;exitDateStart/exitDateEnd 传 yyyy-MM-dd,结束日包含当天;pageNumber/pageSize 控制分页
|
||||||
|
* @return 分页会员资料,exitType 区分 AUDIT_EXIT、SYSTEM_EXIT、DIRECT_REMOVE,exitTime 为退会时间
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubUserCommonPageVo> exitManagePageData(ClubUserPageForm pageForm) {
|
public Pagination<ClubUserCommonPageVo> exitManagePageData(ClubUserPageForm pageForm) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
@@ -257,6 +419,13 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
""");
|
""");
|
||||||
Cnd cnd = Cnd.NEW();
|
Cnd cnd = Cnd.NEW();
|
||||||
cnd.andEX("exitInfo.clubId", "=", pageForm.getClubId());
|
cnd.andEX("exitInfo.clubId", "=", pageForm.getClubId());
|
||||||
|
// 2026年10月3日 19:42:45;操作人:wxg:按实际退会时间筛选三类记录,结束日期使用次日零点上界,包含当天。
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getExitDateStart())) {
|
||||||
|
cnd.and("exitInfo.exitTime", ">=", LocalDate.parse(pageForm.getExitDateStart()).toString());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(pageForm.getExitDateEnd())) {
|
||||||
|
cnd.and("exitInfo.exitTime", "<", LocalDate.parse(pageForm.getExitDateEnd()).plusDays(1).toString());
|
||||||
|
}
|
||||||
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
cnd.andEX("u.personType", "=", pageForm.getPersonType());
|
||||||
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
cnd.andEX("u.userState", "=", pageForm.getUserState());
|
||||||
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||||
@@ -268,7 +437,9 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
cnd.and("exitInfo.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
|
cnd.and("exitInfo.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
|
||||||
}
|
}
|
||||||
cnd.groupBy("exitInfo.id");
|
cnd.groupBy("exitInfo.id");
|
||||||
cnd.desc("exitTime");
|
// 2026年10月3日 19:42:45;操作人:wxg:最新退会记录优先,时间相同时按ID保证分页稳定;空时间排在最后。
|
||||||
|
cnd.desc("exitInfo.exitTime");
|
||||||
|
cnd.asc("exitInfo.id");
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
|
||||||
List<ClubUserCommonPageVo> list = pagination.getList(ClubUserCommonPageVo.class);
|
List<ClubUserCommonPageVo> list = pagination.getList(ClubUserCommonPageVo.class);
|
||||||
@@ -321,6 +492,8 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
/**
|
/**
|
||||||
* 退会申请审批完成后会删除club_user成员关系,因此退会管理以退会申请记录为主数据源。
|
* 退会申请审批完成后会删除club_user成员关系,因此退会管理以退会申请记录为主数据源。
|
||||||
* 同时保留旧sys_club_user表中已退会的数据,避免历史直接移出记录无法查询。
|
* 同时保留旧sys_club_user表中已退会的数据,避免历史直接移出记录无法查询。
|
||||||
|
* 2026年10月3日 19:42:45;操作人:wxg:mode=2 为系统处理,无需 WF;mode=0 通过 EXISTS 判断 WF 完成,避免一对多重复。
|
||||||
|
* 同一用户、社团存在以上申请时不再重复展示旧历史表记录;每条申请仍独立保留,列表与详情共用。
|
||||||
*
|
*
|
||||||
* @return 退会管理列表和详情共用的数据来源SQL
|
* @return 退会管理列表和详情共用的数据来源SQL
|
||||||
*/
|
*/
|
||||||
@@ -343,11 +516,12 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
cua.applyDate,
|
cua.applyDate,
|
||||||
cua.joinTime,
|
cua.joinTime,
|
||||||
cua.exitTime,
|
cua.exitTime,
|
||||||
'AUDIT_EXIT' AS exitType
|
CASE WHEN cua.mode = 2 THEN 'SYSTEM_EXIT' ELSE 'AUDIT_EXIT' END AS exitType
|
||||||
FROM
|
FROM
|
||||||
club_user_apply cua
|
club_user_apply cua
|
||||||
INNER JOIN wf_process_instance ins ON ins.businessNo = cua.id AND ins.state = 20
|
WHERE cua.mode = 2 OR (cua.mode = 0 AND EXISTS (
|
||||||
WHERE cua.mode = false
|
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = cua.id AND ins.state = 20
|
||||||
|
))
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
scu.id,
|
scu.id,
|
||||||
@@ -378,10 +552,11 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM club_user_apply cua
|
FROM club_user_apply cua
|
||||||
INNER JOIN wf_process_instance ins ON ins.businessNo = cua.id AND ins.state = 20
|
WHERE cua.clubId = scu.clubId
|
||||||
WHERE cua.mode = false
|
|
||||||
AND cua.clubId = scu.clubId
|
|
||||||
AND cua.userId = scu.userId
|
AND cua.userId = scu.userId
|
||||||
|
AND (cua.mode = 2 OR (cua.mode = 0 AND EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = cua.id AND ins.state = 20
|
||||||
|
)))
|
||||||
)
|
)
|
||||||
) exitInfo
|
) exitInfo
|
||||||
LEFT JOIN sys_club club ON club.id = exitInfo.clubId
|
LEFT JOIN sys_club club ON club.id = exitInfo.clubId
|
||||||
@@ -431,6 +606,12 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按现有成员排序渲染 clubRegis Word 模板,模板读取为空时提前阻止解析及下载。
|
||||||
|
* @param clubId 当前选中社团ID,登记表包含该社团资料和成员信息
|
||||||
|
* @param response 写入生成的 DOCX 附件;模板或渲染失败时抛出中文业务异常
|
||||||
|
* @return void,文件内容通过响应输出,错误结果由统一异常处理返回
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void exportRegistrationDoc(String clubId, HttpServletResponse response) {
|
public void exportRegistrationDoc(String clubId, HttpServletResponse response) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
@@ -501,11 +682,22 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
|
|||||||
.bind("cys", policy)
|
.bind("cys", policy)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
try (InputStream templateStream = sysOfficeTemplateUtil.getTemplate("clubRegis");
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("clubRegis"), config).render(docData).writeAndClose(byteArrayOutputStream);
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||||
|
// 文件存储读取失败会返回空内容,先校验再交给 POI,避免显示英文 EmptyFileException。
|
||||||
|
byte[] templateBytes = templateStream == null ? new byte[0] : templateStream.readAllBytes();
|
||||||
|
if (templateBytes.length == 0) {
|
||||||
|
throw new BaseException("社团登记表模板读取失败,请检查模板文件及文件存储服务。");
|
||||||
|
}
|
||||||
|
XWPFTemplate.compile(new ByteArrayInputStream(templateBytes), config).render(docData).writeAndClose(byteArrayOutputStream);
|
||||||
CommonDownloadUtil.download("教职工文体社团登记表(" + clubInfo.getString("clubName") + ").docx", byteArrayOutputStream.toByteArray(), response);
|
CommonDownloadUtil.download("教职工文体社团登记表(" + clubInfo.getString("clubName") + ").docx", byteArrayOutputStream.toByteArray(), response);
|
||||||
} catch (IOException e) {
|
} catch (BaseException e) {
|
||||||
log.error("社团登记表导出失败,id:{},错误信息:{}", clubId, e.getMessage());
|
// 保留模板配置及空文件校验的具体提示,同时记录完整异常以便排查。
|
||||||
|
log.error("社团登记表导出失败,id:{}", clubId, e);
|
||||||
|
throw e;
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
log.error("社团登记表导出失败,id:{}", clubId, e);
|
||||||
|
throw new BaseException("社团登记表生成失败,请检查模板是否为有效的 Word 文件或联系管理员。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ import com.budwk.app.zhgh.club.model.SysClubSponsor;
|
|||||||
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubService;
|
import com.budwk.app.zhgh.club.service.SysClubService;
|
||||||
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
import com.budwk.app.zhgh.club.service.SysClubUserService;
|
||||||
|
import com.budwk.app.zhgh.club.service.ClubPayRecordService;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
import com.budwk.app.zhgh.club.vo.ClubCommonPageVo;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubRegisterPageVo;
|
import com.budwk.app.zhgh.club.vo.ClubRegisterPageVo;
|
||||||
import com.budwk.app.zhgh.club.vo.ClubRegisterVo;
|
import com.budwk.app.zhgh.club.vo.ClubRegisterVo;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.dao.util.cri.Static;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -60,10 +64,27 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
@Inject
|
@Inject
|
||||||
private SysClubUserService sysClubUserService;
|
private SysClubUserService sysClubUserService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private ClubPayRecordService payRecordService;
|
||||||
|
@Inject
|
||||||
private CommonService commonService;
|
private CommonService commonService;
|
||||||
@Inject
|
@Inject
|
||||||
private BpmService bpmService;
|
private BpmService bpmService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysClub> listJoinClubOptions() {
|
||||||
|
// 注册、换届等流程可能使用同一社团ID作为业务号;仅判断流程存在,避免联表将社团重复返回。
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT c.*
|
||||||
|
FROM sys_club c
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM wf_process_instance inst
|
||||||
|
WHERE inst.businessNo = c.id AND inst.state = @state
|
||||||
|
)
|
||||||
|
""").setParam("state", ProcessInstanceStateEnum.FINISHED.getCode());
|
||||||
|
return listVO(sql, SysClub.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pagination<ClubCommonPageVo> getAllClubWithPage(PageForm pageForm, Cnd cnd) {
|
public Pagination<ClubCommonPageVo> getAllClubWithPage(PageForm pageForm, Cnd cnd) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
@@ -77,10 +98,11 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
FROM
|
FROM
|
||||||
sys_club club
|
sys_club club
|
||||||
LEFT JOIN sys_user u on club.concatPerson = u.id
|
LEFT JOIN sys_user u on club.concatPerson = u.id
|
||||||
LEFT JOIN wf_process_instance ins ON ins.businessNo = club.id
|
|
||||||
$condition
|
$condition
|
||||||
""").setParam("userId", SecurityUtil.getUserId());
|
""").setParam("userId", SecurityUtil.getUserId());
|
||||||
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
|
// 同一社团可能关联注册、换届等多个已完成 WF 流程;仅检查流程存在,避免列表和分页总数重复。
|
||||||
|
cnd.and(new Static("EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = club.id AND ins.state = %s)"
|
||||||
|
.formatted(ProcessInstanceStateEnum.FINISHED.getCode())));
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
|
||||||
}
|
}
|
||||||
@@ -164,6 +186,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public SysClub doAdd(SysClub club, List<NutMap> managePerson) {
|
public SysClub doAdd(SysClub club, List<NutMap> managePerson) {
|
||||||
club.setDismiss(false);
|
club.setDismiss(false);
|
||||||
club.setUserId(SecurityUtil.getUserId());
|
club.setUserId(SecurityUtil.getUserId());
|
||||||
@@ -178,6 +201,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
clubUser.setUserId(person.getString("userId"));
|
clubUser.setUserId(person.getString("userId"));
|
||||||
return clubUser;
|
return clubUser;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
sysClubUserService.prepareDirectJoinTimes(clubUsers);
|
||||||
dao().insert(clubUsers);
|
dao().insert(clubUsers);
|
||||||
|
|
||||||
for (ClubUser clubUser : clubUsers) {
|
for (ClubUser clubUser : clubUsers) {
|
||||||
@@ -203,6 +227,8 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
log.set("newValue", false);
|
log.set("newValue", false);
|
||||||
|
|
||||||
record.setOperateLogs(List.of(log));
|
record.setOperateLogs(List.of(log));
|
||||||
|
// 2026年10月3日 09:05:37;操作人:wxg:社团创建直接入会的成员同样保存缴费快照。
|
||||||
|
payRecordService.prepareSnapshots(List.of(record));
|
||||||
dao().insert(record);
|
dao().insert(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +237,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public SysClub doEdit(SysClub club, List<String> deleteIds, List<NutMap> managePerson) {
|
public SysClub doEdit(SysClub club, List<String> deleteIds, List<NutMap> managePerson) {
|
||||||
// 修改社团表
|
// 修改社团表
|
||||||
dao().update(club);
|
dao().update(club);
|
||||||
@@ -219,6 +246,8 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
// 重新插入发起人
|
// 重新插入发起人
|
||||||
dao().insertLinks(club, "sponsors");
|
dao().insertLinks(club, "sponsors");
|
||||||
|
|
||||||
|
// 现有编辑流程会重建会员关系,删除前保存原入会时间,避免把编辑时刻算成再次入会。
|
||||||
|
List<ClubUser> previousMembers = dao().query(ClubUser.class, Cnd.where("clubId", "=", club.getId()));
|
||||||
dao().clear(Sys_user_role.class, Cnd.where("clubId", "=", club.getId()));
|
dao().clear(Sys_user_role.class, Cnd.where("clubId", "=", club.getId()));
|
||||||
dao().clear(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", club.getId()));
|
dao().clear(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", club.getId()));
|
||||||
|
|
||||||
@@ -228,8 +257,24 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
|
|||||||
clubUser.setClubId(club.getId());
|
clubUser.setClubId(club.getId());
|
||||||
clubUser.setRoleCode(List.of(person.getString("roleCode")));
|
clubUser.setRoleCode(List.of(person.getString("roleCode")));
|
||||||
clubUser.setUserId(person.getString("userId"));
|
clubUser.setUserId(person.getString("userId"));
|
||||||
|
previousMembers.stream()
|
||||||
|
.filter(member -> java.util.Objects.equals(member.getUserId(), clubUser.getUserId()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(member -> {
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:编辑社团重建既有会员关系时保留原入会时间及人员资料。
|
||||||
|
clubUser.setJoinTime(member.getJoinTime());
|
||||||
|
clubUser.setUserName(member.getUserName());
|
||||||
|
clubUser.setLoginName(member.getLoginName());
|
||||||
|
clubUser.setMobile(member.getMobile());
|
||||||
|
});
|
||||||
return clubUser;
|
return clubUser;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
// 仅真正新增的理事成员赋确认时间;原成员即使历史时间为空也保留原值。
|
||||||
|
List<ClubUser> newMembers = clubUsers.stream()
|
||||||
|
.filter(member -> previousMembers.stream().noneMatch(previous ->
|
||||||
|
java.util.Objects.equals(previous.getUserId(), member.getUserId())))
|
||||||
|
.toList();
|
||||||
|
sysClubUserService.prepareDirectJoinTimes(newMembers);
|
||||||
dao().insert(clubUsers);
|
dao().insert(clubUsers);
|
||||||
}
|
}
|
||||||
return club;
|
return club;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.budwk.app.zhgh.club.service.impl;
|
package com.budwk.app.zhgh.club.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import 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.service.impl.BaseServiceImpl;
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
@@ -22,6 +23,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.Daos;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
@@ -55,15 +57,96 @@ public class SysClubUserServiceImpl extends BaseServiceImpl<ClubUser> implements
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 2026年10月3日 12:01:28;操作人:wxg:与缴费表升级一致,只增加缺失列,保留现有数据、额外字段和索引。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void upgradeMemberSchema() {
|
||||||
|
// MySQL DDL 会隐式提交,升级失败必须终止启动,不得继续执行缺列的业务查询。
|
||||||
|
if (!dao().exists("club_user")) {
|
||||||
|
dao().create(ClubUser.class, false);
|
||||||
|
} else {
|
||||||
|
Daos.migration(dao(), ClubUser.class, true, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 2026年10月3日 12:01:28;操作人:wxg:历史资料和空入会时间仅在启动升级时补齐,会员分页查询不再执行 UPDATE。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void backfillMemberSnapshots() {
|
||||||
|
// 每个字段独立补缺;没有电话号码属于正常情况,不覆盖已经保存的非空资料。
|
||||||
|
dao().execute(Sqls.create("""
|
||||||
|
UPDATE club_user member
|
||||||
|
INNER JOIN sys_user u ON u.id = member.userId
|
||||||
|
SET member.userName = CASE WHEN NULLIF(TRIM(member.userName), '') IS NULL THEN u.username ELSE member.userName END,
|
||||||
|
member.loginName = CASE WHEN NULLIF(TRIM(member.loginName), '') IS NULL THEN u.loginname ELSE member.loginName END,
|
||||||
|
member.mobile = CASE WHEN NULLIF(TRIM(member.mobile), '') IS NULL THEN u.mobile ELSE member.mobile END
|
||||||
|
WHERE (NULLIF(TRIM(member.userName), '') IS NULL AND NULLIF(TRIM(u.username), '') IS NOT NULL)
|
||||||
|
OR (NULLIF(TRIM(member.loginName), '') IS NULL AND NULLIF(TRIM(u.loginname), '') IS NOT NULL)
|
||||||
|
OR (NULLIF(TRIM(member.mobile), '') IS NULL AND NULLIF(TRIM(u.mobile), '') IS NOT NULL)
|
||||||
|
"""));
|
||||||
|
// 没有空入会时间时跳过 WF 汇总,避免每次启动扫描不必要的流程历史。
|
||||||
|
if (dao().count(ClubUser.class, Cnd.where("joinTime", "is", null)) == 0) return;
|
||||||
|
// 沿用原查询前补齐规则:同一会员最近一次已完成 WF 会长同意审核;没有有效流程保持空值。
|
||||||
|
dao().execute(Sqls.create("""
|
||||||
|
UPDATE club_user member
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT application.userId, application.clubId, MAX(task.finishTime) AS joinTime
|
||||||
|
FROM club_user_apply application
|
||||||
|
INNER JOIN wf_process_instance instance ON instance.businessNo = application.id
|
||||||
|
INNER JOIN wf_process_task task ON task.processInstanceId = instance.id
|
||||||
|
WHERE application.mode = 1
|
||||||
|
AND instance.state = 20
|
||||||
|
AND task.taskName = '1625a683-3890-4788-95b7-cab8240f6731'
|
||||||
|
AND task.taskState = 20
|
||||||
|
AND task.finishTime IS NOT NULL
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(task.variable, '$.submitType')) = '1'
|
||||||
|
GROUP BY application.userId, application.clubId
|
||||||
|
) approval ON approval.userId = member.userId AND approval.clubId = member.clubId
|
||||||
|
SET member.joinTime = approval.joinTime
|
||||||
|
WHERE member.joinTime IS NULL
|
||||||
|
"""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 2026年10月3日 12:01:28;操作人:wxg:统一从系统用户资料取值,禁止依赖申请页面传回的姓名、工号和电话。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void prepareMemberSnapshots(List<ClubUser> members) {
|
||||||
|
if (members == null || members.isEmpty()) return;
|
||||||
|
if (members.stream().anyMatch(member -> member == null || StrUtil.isBlank(member.getUserId()))) {
|
||||||
|
throw new IllegalArgumentException("新增会员必须提供有效用户ID");
|
||||||
|
}
|
||||||
|
List<String> userIds = members.stream().map(ClubUser::getUserId).distinct().toList();
|
||||||
|
Map<String, Sys_user> users = dao().query(Sys_user.class, Cnd.where("id", "in", userIds)).stream()
|
||||||
|
.collect(Collectors.toMap(Sys_user::getId, user -> user));
|
||||||
|
for (ClubUser member : members) {
|
||||||
|
Sys_user user = users.get(member.getUserId());
|
||||||
|
if (user == null) throw new IllegalArgumentException("新增会员的用户资料不存在:" + member.getUserId());
|
||||||
|
member.setUserName(user.getUsername());
|
||||||
|
member.setLoginName(user.getLoginname());
|
||||||
|
member.setMobile(user.getMobile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void prepareDirectJoinTimes(List<ClubUser> members) {
|
||||||
|
// 2026年10月3日 12:01:28;操作人:wxg:直接新增入口同时准备人员资料,确认时间不覆盖已保存的入会时间。
|
||||||
|
prepareMemberSnapshots(members);
|
||||||
|
if (members == null || members.isEmpty()) return;
|
||||||
|
Date confirmedAt = new Date();
|
||||||
|
for (ClubUser member : members) {
|
||||||
|
if (member.getJoinTime() == null) {
|
||||||
|
member.setJoinTime(confirmedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<NutMap> getClubUser(String clubId) {
|
public List<NutMap> getClubUser(String clubId) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
scu.*,
|
scu.*,
|
||||||
u.username as userName,
|
|
||||||
u.loginname as loginName,
|
|
||||||
u.sex,
|
u.sex,
|
||||||
u.mobile,
|
|
||||||
u.userState,
|
u.userState,
|
||||||
unit.`name` unitName
|
unit.`name` unitName
|
||||||
FROM
|
FROM
|
||||||
@@ -96,10 +179,7 @@ public class SysClubUserServiceImpl extends BaseServiceImpl<ClubUser> implements
|
|||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
scu.*,
|
scu.*,
|
||||||
u.username as userName,
|
|
||||||
u.loginname as loginName,
|
|
||||||
u.sex,
|
u.sex,
|
||||||
u.mobile,
|
|
||||||
u.userState,
|
u.userState,
|
||||||
u.birthday,
|
u.birthday,
|
||||||
u.unionname as unionName,
|
u.unionname as unionName,
|
||||||
|
|||||||
@@ -14,22 +14,21 @@ public class ClubUserCommonPageVo extends ClubUser {
|
|||||||
private String clubName;
|
private String clubName;
|
||||||
private String clubCode;
|
private String clubCode;
|
||||||
private String typeName;
|
private String typeName;
|
||||||
private String userName;
|
// 2026年10月3日 12:01:28;操作人:wxg:姓名、工号、电话继承 ClubUser 的持久化字段,避免子类同名属性遮蔽。
|
||||||
private List<JSONObject> replaceReport;
|
private List<JSONObject> replaceReport;
|
||||||
private List<JSONObject> rulesFile;
|
private List<JSONObject> rulesFile;
|
||||||
private List<JSONObject> afterRulesFile;
|
private List<JSONObject> afterRulesFile;
|
||||||
private List<JSONObject> establishReport;
|
private List<JSONObject> establishReport;
|
||||||
private String loginName;
|
|
||||||
private String sex;
|
private String sex;
|
||||||
private String personType;
|
private String personType;
|
||||||
|
/** 用户当前人员属性,来自 vw_user.userAttribute,用于会员列表展示。 */
|
||||||
|
private String userAttribute;
|
||||||
private String userState;
|
private String userState;
|
||||||
private String unitId;
|
private String unitId;
|
||||||
private String mobile;
|
|
||||||
private String unitName;
|
private String unitName;
|
||||||
private String unionName;
|
private String unionName;
|
||||||
private String birthday;
|
private String birthday;
|
||||||
private String unionId;
|
private String unionId;
|
||||||
private String joinTime;
|
|
||||||
private String exitTime;
|
private String exitTime;
|
||||||
private String exitType;
|
private String exitType;
|
||||||
private String applyRoleCode;
|
private String applyRoleCode;
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ public class ClubUserJoinPageVo{
|
|||||||
private String clubId;
|
private String clubId;
|
||||||
private String userId;
|
private String userId;
|
||||||
private String roleCode;
|
private String roleCode;
|
||||||
private Boolean mode;
|
// 0:普通退会;1:入会;2:系统处理退会。
|
||||||
|
private Integer mode;
|
||||||
private String clubPosition;
|
private String clubPosition;
|
||||||
private String email;
|
private String email;
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|||||||
+12
-63
@@ -18,6 +18,9 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
|||||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||||
import com.budwk.app.zhgh.club.model.SysClub;
|
import com.budwk.app.zhgh.club.model.SysClub;
|
||||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
|
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayManageClubQueryService;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -53,6 +56,9 @@ public class OutlayManageClubController {
|
|||||||
@Inject
|
@Inject
|
||||||
private SysRoleService sysRoleService;
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private OutlayManageClubQueryService clubQueryService;
|
||||||
|
|
||||||
@At("")
|
@At("")
|
||||||
@Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html")
|
@Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html")
|
||||||
@SaCheckPermission("outlay.outlayManage.club.manage")
|
@SaCheckPermission("outlay.outlayManage.club.manage")
|
||||||
@@ -111,74 +117,17 @@ public class OutlayManageClubController {
|
|||||||
return Result.success(count);
|
return Result.success(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 16:23:13;操作人:wxg:年度分配只委托 service,避免 WF 一对多关联造成重复社团预算。
|
||||||
|
* 无业务参数,年度固定为系统当前年;返回 Result.success,新增及重复检查在同一事务完成。
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("下发预算")
|
@ApiOperation("下发预算")
|
||||||
@SaCheckPermission("outlay.outlayManage.club.manage")
|
@SaCheckPermission("outlay.outlayManage.club.manage")
|
||||||
@SLog(tag = "社团预算分配", msg = "根据申报的金额修改本年的预算预算")
|
@SLog(tag = "社团预算分配", msg = "根据申报的金额修改本年的预算预算")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Result issuedOutlay() {
|
public Result issuedOutlay() {
|
||||||
Sql sql = Sqls.create("""
|
clubQueryService.generateCurrentYearAllocation();
|
||||||
SELECT
|
|
||||||
c.*,
|
|
||||||
COALESCE(pay_stats.historyGiveMoneyNum, 0) AS historyGiveMoneyNum,
|
|
||||||
COALESCE(pay_stats.historyNotGiveMoneyNum, 0) AS historyNotGiveMoneyNum,
|
|
||||||
COALESCE(pay_stats.historyUserNum, 0) AS historyUserNum
|
|
||||||
FROM
|
|
||||||
sys_club c
|
|
||||||
LEFT JOIN wf_process_instance inst ON inst.businessNo = c.id
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT
|
|
||||||
cu.clubId,
|
|
||||||
COUNT(CASE WHEN re.isGiveMoney = 1 THEN 1 END) AS historyGiveMoneyNum,
|
|
||||||
COUNT(CASE WHEN re.isGiveMoney = 0 THEN 1 END) AS historyNotGiveMoneyNum,
|
|
||||||
COUNT(*) AS historyUserNum
|
|
||||||
FROM
|
|
||||||
club_user cu
|
|
||||||
INNER JOIN club_user_apply cua
|
|
||||||
ON cu.userId = cua.userId
|
|
||||||
AND cu.clubId = cua.clubId
|
|
||||||
LEFT JOIN club_pay_record re
|
|
||||||
ON re.userId = cu.userId
|
|
||||||
AND re.clubId = cu.clubId
|
|
||||||
AND re.year = @year
|
|
||||||
WHERE
|
|
||||||
YEAR(cua.applyDate) < @year
|
|
||||||
AND cua.`mode` = 1
|
|
||||||
GROUP BY
|
|
||||||
cu.clubId
|
|
||||||
) pay_stats ON pay_stats.clubId = c.id
|
|
||||||
WHERE
|
|
||||||
inst.state = 20;
|
|
||||||
""").setParam("year", DateUtil.thisYear());
|
|
||||||
|
|
||||||
List<NutMap> clubList = baseService.listMap(sql);
|
|
||||||
|
|
||||||
|
|
||||||
List<OutlayManageClub> insertUnionList = new ArrayList<>();
|
|
||||||
clubList.forEach(v -> {
|
|
||||||
|
|
||||||
OutlayManageClub outlayManageClub = new OutlayManageClub();
|
|
||||||
outlayManageClub.setYear(DateUtil.thisYear());
|
|
||||||
outlayManageClub.setClubName(v.getString("clubName"));
|
|
||||||
outlayManageClub.setClubCode(v.getString("clubCode"));
|
|
||||||
outlayManageClub.setClubId(v.getString("id"));
|
|
||||||
outlayManageClub.setHistoryUserNum(v.getInt("historyUserNum"));
|
|
||||||
outlayManageClub.setHistoryNotGiveMoneyNum(v.getInt("historyNotGiveMoneyNum"));
|
|
||||||
outlayManageClub.setHistoryGiveMoneyNum(v.getInt("historyGiveMoneyNum"));
|
|
||||||
|
|
||||||
BigDecimal totalBudgetMoney;
|
|
||||||
//如果人数超过50人就是5000,否则就是3000
|
|
||||||
if (v.getInt("historyGiveMoneyNum") >= 50) {
|
|
||||||
totalBudgetMoney = BigDecimal.valueOf(5000);
|
|
||||||
} else {
|
|
||||||
totalBudgetMoney = BigDecimal.valueOf(3000);
|
|
||||||
}
|
|
||||||
if (v.getInt("historyGiveMoneyNum") == 0) {
|
|
||||||
totalBudgetMoney = BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
outlayManageClub.setTotalQuota(totalBudgetMoney);
|
|
||||||
insertUnionList.add(outlayManageClub);
|
|
||||||
});
|
|
||||||
baseService.insert(insertUnionList);
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
@@ -9,6 +9,13 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
|
|
||||||
public interface OutlayManageClubQueryService extends BaseService {
|
public interface OutlayManageClubQueryService extends BaseService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026年10月3日 16:23:13;操作人:wxg:生成当前年度社团预算,同年度同社团已存在时跳过,不覆盖金额和核对状态。
|
||||||
|
* 社团须存在已完成 WF 流程;保留原年度人数统计和拨付金额规则。
|
||||||
|
* @return 本次实际新增的预算记录条数,重复调用且没有新增社团时为 0
|
||||||
|
*/
|
||||||
|
int generateCurrentYearAllocation();
|
||||||
|
|
||||||
void doMoney(String clubId);
|
void doMoney(String clubId);
|
||||||
|
|
||||||
Sql backUserSql(PageForm pageForm, String clubId, String type, Integer year, String payed);
|
Sql backUserSql(PageForm pageForm, String clubId, String type, Integer year, String payed);
|
||||||
|
|||||||
+76
@@ -39,6 +39,8 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -58,6 +60,80 @@ public class OutlayManageClubQueryServiceImpl extends BaseServiceImpl implements
|
|||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 2026年10月3日 16:23:13;操作人:wxg:WF 仅用于判断存在;锁定社团并检查已有年度预算,连续或并发分配不重复新增。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public int generateCurrentYearAllocation() {
|
||||||
|
int year = DateUtil.thisYear();
|
||||||
|
// 同一事务先按固定顺序锁定候选社团,串行处理分配请求,随后读取最新年度预算。
|
||||||
|
// 不改动 WF 流程,不增加索引;多个已完成流程不会增加候选社团行数。
|
||||||
|
Sql lockSql = Sqls.create("""
|
||||||
|
SELECT c.id FROM sys_club c
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance inst
|
||||||
|
WHERE inst.businessNo = c.id AND inst.state = 20
|
||||||
|
)
|
||||||
|
ORDER BY c.id
|
||||||
|
FOR UPDATE
|
||||||
|
""");
|
||||||
|
List<NutMap> clubs = listMap(lockSql);
|
||||||
|
if (clubs.isEmpty()) return 0;
|
||||||
|
Set<String> recordedClubs = new HashSet<>();
|
||||||
|
for (OutlayManageClub existing : dao().query(OutlayManageClub.class, Cnd.where("year", "=", year))) {
|
||||||
|
recordedClubs.add(existing.getClubId());
|
||||||
|
}
|
||||||
|
// 2026年10月3日 17:11:57;操作人:wxg:年度预算人数取预算年度减一的缴费名单。
|
||||||
|
// 按名单保存的 isGiveMoney 分别统计拨付、未拨付人数,不依赖当前会员关系或入会申请,不重新计算历史拨付资格。
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT
|
||||||
|
c.*,
|
||||||
|
COALESCE(pay_stats.historyGiveMoneyNum, 0) AS historyGiveMoneyNum,
|
||||||
|
COALESCE(pay_stats.historyNotGiveMoneyNum, 0) AS historyNotGiveMoneyNum,
|
||||||
|
COALESCE(pay_stats.historyUserNum, 0) AS historyUserNum
|
||||||
|
FROM
|
||||||
|
sys_club c
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
re.clubId,
|
||||||
|
COUNT(CASE WHEN re.isGiveMoney = 1 THEN 1 END) AS historyGiveMoneyNum,
|
||||||
|
COUNT(CASE WHEN re.isGiveMoney = 0 THEN 1 END) AS historyNotGiveMoneyNum,
|
||||||
|
COUNT(*) AS historyUserNum
|
||||||
|
FROM
|
||||||
|
club_pay_record re
|
||||||
|
WHERE
|
||||||
|
re.year = @historyYear
|
||||||
|
GROUP BY
|
||||||
|
re.clubId
|
||||||
|
) pay_stats ON pay_stats.clubId = c.id
|
||||||
|
WHERE
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM wf_process_instance inst
|
||||||
|
WHERE inst.businessNo = c.id AND inst.state = 20
|
||||||
|
)
|
||||||
|
ORDER BY c.id;
|
||||||
|
""").setParam("historyYear", year - 1);
|
||||||
|
List<NutMap> clubList = listMap(sql);
|
||||||
|
Set<String> lockedClubs = clubs.stream().map(club -> club.getString("id")).collect(Collectors.toSet());
|
||||||
|
List<OutlayManageClub> budgets = new ArrayList<>();
|
||||||
|
for (NutMap club : clubList) {
|
||||||
|
String clubId = club.getString("id");
|
||||||
|
// 新增社团若在候选锁定后完成审批,则留到下次处理;既有预算不得重置或覆盖。
|
||||||
|
if (!lockedClubs.contains(clubId) || !recordedClubs.add(clubId)) continue;
|
||||||
|
OutlayManageClub budget = new OutlayManageClub();
|
||||||
|
budget.setYear(year);
|
||||||
|
budget.setClubId(clubId);
|
||||||
|
budget.setClubName(club.getString("clubName"));
|
||||||
|
budget.setClubCode(club.getString("clubCode"));
|
||||||
|
budget.setHistoryUserNum(club.getInt("historyUserNum"));
|
||||||
|
budget.setHistoryGiveMoneyNum(club.getInt("historyGiveMoneyNum"));
|
||||||
|
budget.setHistoryNotGiveMoneyNum(club.getInt("historyNotGiveMoneyNum"));
|
||||||
|
budget.setTotalQuota(getAllocateStandard(club.getInt("historyGiveMoneyNum")));
|
||||||
|
budgets.add(budget);
|
||||||
|
}
|
||||||
|
if (!budgets.isEmpty()) dao().insert(budgets);
|
||||||
|
return budgets.size();
|
||||||
|
}
|
||||||
|
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@Override
|
@Override
|
||||||
public void doMoney(String clubId) {
|
public void doMoney(String clubId) {
|
||||||
|
|||||||
+36
-1
@@ -27,6 +27,7 @@ import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceTypeService;
|
|||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseBankAccountService;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -45,6 +46,7 @@ import org.nutz.lang.util.NutMap;
|
|||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
@@ -65,6 +67,8 @@ public class UnionReimburseApplyController {
|
|||||||
@Inject
|
@Inject
|
||||||
private UnionReimburseService unionReimburseService;
|
private UnionReimburseService unionReimburseService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private UnionReimburseBankAccountService unionReimburseBankAccountService;
|
||||||
|
@Inject
|
||||||
private FlowCommonService flowCommonService;
|
private FlowCommonService flowCommonService;
|
||||||
@Inject
|
@Inject
|
||||||
private CondolenceTypeService typeService;
|
private CondolenceTypeService typeService;
|
||||||
@@ -83,6 +87,7 @@ public class UnionReimburseApplyController {
|
|||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("保存申请")
|
@ApiOperation("保存申请")
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||||
@SLog(type = "unionReimburse", tag = "工会报销-报销申请", msg = "保存报销申请")
|
@SLog(type = "unionReimburse", tag = "工会报销-报销申请", msg = "保存报销申请")
|
||||||
public Result save(@Param("data") UnionReimburse unionReimburse) {
|
public Result save(@Param("data") UnionReimburse unionReimburse) {
|
||||||
@@ -204,6 +209,31 @@ public class UnionReimburseApplyController {
|
|||||||
return Result.success(result);
|
return Result.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param payer 付款人用户ID,不传姓名或工号
|
||||||
|
* @param reimburseFundSource 当前经费来源编码
|
||||||
|
* @param clubId 协会经费对应的协会ID,其他来源可为空
|
||||||
|
* @return Result.data 为账户列表,每项包含 id、bankUserName、bankCardNumber、bankOfDeposit
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@ApiOperation("查询付款人常用收款账户")
|
||||||
|
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||||
|
public Result listBankAccounts(String payer, String reimburseFundSource, String clubId) {
|
||||||
|
return Result.success(unionReimburseBankAccountService.listAccounts(payer, reimburseFundSource, clubId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无请求参数;仅系统管理员执行。Result.data 为扫描的历史单据数量,重复调用不会新增重复账户。 */
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@ApiOperation("初始化历史收款账户")
|
||||||
|
@SaCheckLogin
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
@SLog(type = "unionReimburse", tag = "历史收款账户初始化", msg = "导入历史报销收款账户")
|
||||||
|
public Result importBankAccountHistory() {
|
||||||
|
return Result.success(unionReimburseBankAccountService.importHistory());
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ApiOperation("校验发票号码是否重复")
|
@ApiOperation("校验发票号码是否重复")
|
||||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||||
@@ -212,8 +242,13 @@ public class UnionReimburseApplyController {
|
|||||||
return result == null ? Result.success() : result;
|
return result == null ? Result.success() : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fileId 上传接口返回的文件ID,服务端读取对应发票文件
|
||||||
|
* @param verifySwitch 旧页面兼容参数,任何取值均强制验真
|
||||||
|
* @return Result,code=0 时 data 为票号、金额、销售方、项目及 verified=true;非零时 msg 为失败原因
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ApiOperation("自动识别发票信息")
|
@ApiOperation("识别发票并强制验真")
|
||||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||||
public Result recognizeInvoice(String fileId, @Param("verifySwitch") Boolean verifySwitch) {
|
public Result recognizeInvoice(String fileId, @Param("verifySwitch") Boolean verifySwitch) {
|
||||||
return unionReimburseService.recognizeInvoice(fileId, verifySwitch);
|
return unionReimburseService.recognizeInvoice(fileId, verifySwitch);
|
||||||
|
|||||||
+20
-2
@@ -22,6 +22,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.dayofficework.unionReimburse.model.UnionReimburse;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseCollectExcelVO;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseCollectExcelVO;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@@ -64,6 +65,9 @@ public class UnionReimburseCollectController {
|
|||||||
@Inject
|
@Inject
|
||||||
private UnionReimburseService unionReimburseService;
|
private UnionReimburseService unionReimburseService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private UnionReimburseExportService unionReimburseExportService;
|
||||||
|
|
||||||
@At("/index")
|
@At("/index")
|
||||||
@Ok("beetl:/platform/zhgh/dayofficework/unionReimburse/collect/index.html")
|
@Ok("beetl:/platform/zhgh/dayofficework/unionReimburse/collect/index.html")
|
||||||
@SaCheckPermission("unionReimburse.collect")
|
@SaCheckPermission("unionReimburse.collect")
|
||||||
@@ -134,6 +138,20 @@ public class UnionReimburseCollectController {
|
|||||||
return unionReimburseService.updateReimburseProject(id, reimburseProject);
|
return unionReimburseService.updateReimburseProject(id, reimburseProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总页打印复用审批单 PDF 导出,支持所有申请状态,数据范围由 service 校验。
|
||||||
|
* @param id 汇总页选中的报销单 ID,不受申请状态限制
|
||||||
|
* @param response 文件下载响应,返回完整 PDF 附件;失败由统一异常处理返回业务提示
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@ApiOperation("打印报销审批单PDF")
|
||||||
|
@SaCheckPermission("unionReimburse.collect")
|
||||||
|
public void exportPdf(@Param("id") String id, HttpServletResponse response) {
|
||||||
|
byte[] pdf = unionReimburseExportService.exportCollectPdf(id);
|
||||||
|
CommonDownloadUtil.download("工会经费报销审批单_" + id + ".pdf", pdf, response);
|
||||||
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
|
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
|
||||||
@@ -250,9 +268,9 @@ public class UnionReimburseCollectController {
|
|||||||
item.setStateName(formatStateName(item.getStateId()));
|
item.setStateName(formatStateName(item.getStateId()));
|
||||||
|
|
||||||
// 备注字段与页面表格保持一致:慰问显示被慰问人,其他项目显示活动名称。
|
// 备注字段与页面表格保持一致:慰问显示被慰问人,其他项目显示活动名称。
|
||||||
if (cn.hutool.core.util.StrUtil.isNotBlank(item.getActivityName())) {
|
if (StrUtil.isNotBlank(item.getActivityName())) {
|
||||||
item.setRemark("活动名称:" + item.getActivityName());
|
item.setRemark("活动名称:" + item.getActivityName());
|
||||||
} else if (cn.hutool.core.util.StrUtil.isNotBlank(item.getCondolenceUserName())) {
|
} else if (StrUtil.isNotBlank(item.getCondolenceUserName())) {
|
||||||
item.setRemark("被慰问人:" + item.getCondolenceUserName());
|
item.setRemark("被慰问人:" + item.getCondolenceUserName());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-276
@@ -1,62 +1,39 @@
|
|||||||
package com.budwk.app.zhgh.dayofficework.unionReimburse.controller;
|
package com.budwk.app.zhgh.dayofficework.unionReimburse.controller;
|
||||||
|
|
||||||
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.io.FileUtil;
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
|
||||||
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.model.Audit;
|
|
||||||
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.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.MoneyUtil;
|
|
||||||
import com.budwk.app.base.utils.OfficePlusUtil;
|
|
||||||
import com.budwk.app.base.utils.PageUtil;
|
import com.budwk.app.base.utils.PageUtil;
|
||||||
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.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.dayofficework.unionReimburse.model.UnionReimburse;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||||
import com.deepoove.poi.XWPFTemplate;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService;
|
||||||
import com.deepoove.poi.config.Configure;
|
|
||||||
import com.deepoove.poi.data.PictureRenderData;
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
|
||||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
|
||||||
import com.google.zxing.common.BitMatrix;
|
|
||||||
import com.google.zxing.qrcode.QRCodeWriter;
|
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.entity.Record;
|
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
|
||||||
import org.nutz.lang.random.R;
|
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ClassName UnionReimburseMineController
|
* @ClassName UnionReimburseMineController
|
||||||
@@ -73,11 +50,11 @@ import java.util.HashMap;
|
|||||||
public class UnionReimburseMineController {
|
public class UnionReimburseMineController {
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
|
||||||
@Inject
|
@Inject
|
||||||
private UnionReimburseService unionReimburseService;
|
private UnionReimburseService unionReimburseService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private UnionReimburseExportService unionReimburseExportService;
|
||||||
|
@Inject
|
||||||
private FlowEngine flowEngine;
|
private FlowEngine flowEngine;
|
||||||
@Inject
|
@Inject
|
||||||
private Dao dao;
|
private Dao dao;
|
||||||
@@ -206,258 +183,17 @@ public class UnionReimburseMineController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id 当前登录人的已提交报销单ID
|
||||||
|
* @param response 返回PDF附件;失败由统一异常处理返回业务提示
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("void")
|
@Ok("void")
|
||||||
public void ActExport(String id, HttpServletResponse response, boolean Print) throws Exception {
|
@ApiOperation("导出报销审批单PDF")
|
||||||
UnionReimburse unionReimburse = unionReimburseService.fetch(id);
|
@SaCheckPermission("unionReimburse.mine")
|
||||||
if (unionReimburse == null) {
|
public void exportPdf(@Param("id") String id, HttpServletResponse response) {
|
||||||
throw new RuntimeException("报销记录不存在");
|
byte[] pdf = unionReimburseExportService.exportPdf(id);
|
||||||
}
|
CommonDownloadUtil.download("工会经费报销审批单_" + id + ".pdf", pdf, response);
|
||||||
HashMap<String, Object> docData = new HashMap<>();
|
|
||||||
|
|
||||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
|
||||||
BitMatrix bitMatrix = qrCodeWriter.encode(unionReimburse.getDocumentNo(), BarcodeFormat.QR_CODE, 80, 80);
|
|
||||||
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
|
|
||||||
File tempFile = File.createTempFile("qrcode_", ".png");
|
|
||||||
javax.imageio.ImageIO.write(bufferedImage, "PNG", tempFile);
|
|
||||||
PictureRenderData pictureRenderData = com.deepoove.poi.data.Pictures.of(tempFile.getAbsolutePath())
|
|
||||||
.size(80, 80)
|
|
||||||
.create();
|
|
||||||
docData.put("qrcode", pictureRenderData);
|
|
||||||
|
|
||||||
tempFile.deleteOnExit();
|
|
||||||
|
|
||||||
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
|
|
||||||
docData.put("unitName", unionReimburse.getUnitName());
|
|
||||||
docData.put("userName", unionReimburse.getUserName());
|
|
||||||
docData.put("loginName", unionReimburse.getLoginName());
|
|
||||||
docData.put("mobile", unionReimburse.getMobile());
|
|
||||||
docData.put("paymentNotes", unionReimburse.getPaymentNotes());
|
|
||||||
docData.put("invoiceNumber", unionReimburse.getInvoiceNumber());
|
|
||||||
docData.put("notes", unionReimburse.getNotes());
|
|
||||||
docData.put("realMoney", unionReimburse.getRealMoney());
|
|
||||||
docData.put("reimburseProject", unionReimburse.getReimburseProject());
|
|
||||||
docData.put("paymentWay", unionReimburse.getPaymentWay());
|
|
||||||
//对私支付,付款人信息
|
|
||||||
docData.put("payerLoginname", unionReimburse.getPayerLoginname());
|
|
||||||
docData.put("payerName", unionReimburse.getPayerName());
|
|
||||||
docData.put("bankOfDeposit", unionReimburse.getBankOfDeposit());
|
|
||||||
docData.put("bankCardNumber", unionReimburse.getBankCardNumber());
|
|
||||||
|
|
||||||
// 金额转大写
|
|
||||||
if (unionReimburse.getRealMoney() != null) {
|
|
||||||
docData.put("money_big", MoneyUtil.toRMBUpper(String.valueOf(unionReimburse.getRealMoney())));
|
|
||||||
} else {
|
|
||||||
docData.put("money_big", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置报销项目类型标记
|
|
||||||
String wtAct = "□文体活动 □慰问 □日常活动 □专项活动";
|
|
||||||
if ("UNION_REIMBURSE_PROJECT_2".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□文体活动", "√文体活动");
|
|
||||||
} else if ("UNION_REIMBURSE_PROJECT_3".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□日常活动", "√日常活动");
|
|
||||||
} else if ("UNION_REIMBURSE_PROJECT_4".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□专项活动", "√专项活动");
|
|
||||||
} else if ("UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□慰问", "√慰问");
|
|
||||||
}
|
|
||||||
docData.put("bxxm", wtAct);
|
|
||||||
String templateName;
|
|
||||||
Double realMoney = unionReimburse.getRealMoney() != null ? unionReimburse.getRealMoney() : 0.0;
|
|
||||||
String paymentWay = unionReimburse.getPaymentWay();
|
|
||||||
|
|
||||||
if (("UNION_REIMBURSE_PAYMENT_WAY_2").equals(paymentWay)) { // 对私支付
|
|
||||||
docData.put("paymentWay", "对私支付");
|
|
||||||
if (realMoney < 5000) {
|
|
||||||
templateName = "unionReimburse_act_private_small";
|
|
||||||
} else {
|
|
||||||
templateName = "unionReimburse_act_private_big";
|
|
||||||
}
|
|
||||||
} else { // 对公支付
|
|
||||||
docData.put("paymentWay", "对公支付");
|
|
||||||
if (realMoney < 5000) {
|
|
||||||
templateName = "unionReimburse_act_public_small";
|
|
||||||
} else {
|
|
||||||
templateName = "unionReimburse_act_public_big";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Print) {
|
|
||||||
exportAsPDF(templateName, docData, unionReimburse, response);
|
|
||||||
} else {
|
|
||||||
exportAsWord(templateName, docData, unionReimburse, response);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
|
||||||
@Ok("void")
|
|
||||||
public void ConExport(String id, HttpServletResponse response, boolean Print) throws Exception {
|
|
||||||
UnionReimburse unionReimburse = unionReimburseService.fetch(id);
|
|
||||||
if (unionReimburse == null) {
|
|
||||||
throw new RuntimeException("报销记录不存在");
|
|
||||||
}
|
|
||||||
HashMap<String, Object> docData = new HashMap<>();
|
|
||||||
|
|
||||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
|
||||||
BitMatrix bitMatrix = qrCodeWriter.encode(unionReimburse.getDocumentNo(), BarcodeFormat.QR_CODE, 80, 80);
|
|
||||||
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
|
|
||||||
File tempFile = File.createTempFile("qrcode_", ".png");
|
|
||||||
javax.imageio.ImageIO.write(bufferedImage, "PNG", tempFile);
|
|
||||||
PictureRenderData pictureRenderData = com.deepoove.poi.data.Pictures.of(tempFile.getAbsolutePath())
|
|
||||||
.size(80, 80)
|
|
||||||
.create();
|
|
||||||
docData.put("qrcode", pictureRenderData);
|
|
||||||
|
|
||||||
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
|
|
||||||
docData.put("unitName", unionReimburse.getUnitName());
|
|
||||||
docData.put("userName", unionReimburse.getUserName());
|
|
||||||
docData.put("loginName", unionReimburse.getLoginName());
|
|
||||||
docData.put("mobile", unionReimburse.getMobile());
|
|
||||||
docData.put("paymentNotes", unionReimburse.getPaymentNotes());
|
|
||||||
docData.put("invoiceNumber", unionReimburse.getInvoiceNumber());
|
|
||||||
docData.put("notes", unionReimburse.getNotes());
|
|
||||||
docData.put("realMoney", unionReimburse.getRealMoney());
|
|
||||||
docData.put("reimburseProject", unionReimburse.getReimburseProject());
|
|
||||||
docData.put("paymentWay", unionReimburse.getPaymentWay());
|
|
||||||
//对私支付,付款人信息
|
|
||||||
docData.put("payerLoginname", unionReimburse.getPayerLoginname());
|
|
||||||
docData.put("payerName", unionReimburse.getPayerName());
|
|
||||||
docData.put("bankOfDeposit", unionReimburse.getBankOfDeposit());
|
|
||||||
docData.put("bankCardNumber", unionReimburse.getBankCardNumber());
|
|
||||||
|
|
||||||
//慰问
|
|
||||||
docData.put("condolenceUserName", unionReimburse.getCondolenceUserName());
|
|
||||||
docData.put("condolenceLoginName", unionReimburse.getCondolenceLoginName());
|
|
||||||
docData.put("typeName", unionReimburse.getTypeName());
|
|
||||||
docData.put("way", unionReimburse.getWay());
|
|
||||||
docData.put("condolenceTime", DateUtil.format(unionReimburse.getCondolenceTime(), "yyyy-MM-dd"));
|
|
||||||
docData.put("participants", unionReimburse.getParticipants());
|
|
||||||
//结婚
|
|
||||||
docData.put("marryTime", DateUtil.format(unionReimburse.getMarryTime(), "yyyy-MM-dd"));
|
|
||||||
//生育
|
|
||||||
docData.put("fertilityTime", DateUtil.format(unionReimburse.getFertilityTime(), "yyyy-MM-dd"));
|
|
||||||
//生病住院
|
|
||||||
docData.put("hospitalCausation", unionReimburse.getHospitalCausation());
|
|
||||||
docData.put("hospital", unionReimburse.getHospital());
|
|
||||||
docData.put("hospitalCount", unionReimburse.getHospitalCount());
|
|
||||||
String hospitalTime = DateUtil.format(unionReimburse.getHospitalizationStartTime(), "yyyy-MM-dd") +
|
|
||||||
" 至 " +
|
|
||||||
DateUtil.format(unionReimburse.getHospitalizationEndTime(), "yyyy-MM-dd");
|
|
||||||
docData.put("hospitalTime", hospitalTime);
|
|
||||||
//去世
|
|
||||||
docData.put("deathTime", DateUtil.format(unionReimburse.getDeathTime(), "yyyy-MM-dd"));
|
|
||||||
docData.put("condolenceRelationship", unionReimburse.getCondolenceRelationship());
|
|
||||||
|
|
||||||
|
|
||||||
// 金额转大写
|
|
||||||
if (unionReimburse.getRealMoney() != null) {
|
|
||||||
docData.put("money_big", MoneyUtil.toRMBUpper(String.valueOf(unionReimburse.getRealMoney())));
|
|
||||||
} else {
|
|
||||||
docData.put("money_big", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置报销项目类型标记
|
|
||||||
String wtAct = "□文体活动 □慰问 □日常活动 □专项活动";
|
|
||||||
if ("UNION_REIMBURSE_PROJECT_2".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□文体活动", "√文体活动");
|
|
||||||
} else if ("UNION_REIMBURSE_PROJECT_3".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□日常活动", "√日常活动");
|
|
||||||
} else if ("UNION_REIMBURSE_PROJECT_4".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□专项活动", "√专项活动");
|
|
||||||
} else if ("UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) {
|
|
||||||
wtAct = wtAct.replace("□慰问", "√慰问");
|
|
||||||
}
|
|
||||||
docData.put("bxxm", wtAct);
|
|
||||||
String templateName;
|
|
||||||
Double realMoney = unionReimburse.getRealMoney() != null ? unionReimburse.getRealMoney() : 0.0;
|
|
||||||
String paymentWay = unionReimburse.getPaymentWay();
|
|
||||||
|
|
||||||
if (("UNION_REIMBURSE_PAYMENT_WAY_2").equals(paymentWay)) { // 对私支付
|
|
||||||
docData.put("paymentWay", "对私支付");
|
|
||||||
if (("生病住院").equals(unionReimburse.getTypeName())) {
|
|
||||||
templateName = "unionReimburse_con_private_hospital";
|
|
||||||
} else if (("结婚").equals(unionReimburse.getTypeName())) {
|
|
||||||
templateName = "unionReimburse_con_private_mary";
|
|
||||||
} else if (("生育").equals(unionReimburse.getTypeName())) {
|
|
||||||
templateName = "unionReimburse_con_private_birth";
|
|
||||||
} else if (("会员去世").equals(unionReimburse.getTypeName()) || ("直系亲属去世").equals(unionReimburse.getTypeName())) {
|
|
||||||
if (realMoney < 5000) {
|
|
||||||
templateName = "unionReimburse_con_private_dead_small";
|
|
||||||
} else {
|
|
||||||
templateName = "unionReimburse_con_private_dead_big";
|
|
||||||
}
|
|
||||||
} else {//其他
|
|
||||||
templateName = "unionReimburse_con_private_other";
|
|
||||||
}
|
|
||||||
} else { // 对公支付
|
|
||||||
docData.put("paymentWay", "对公支付");
|
|
||||||
if (("生病住院").equals(unionReimburse.getTypeName())) {
|
|
||||||
templateName = "unionReimburse_con_public_hospital";
|
|
||||||
} else if (("结婚").equals(unionReimburse.getTypeName())) {
|
|
||||||
templateName = "unionReimburse_con_public_mary";
|
|
||||||
} else if (("生育").equals(unionReimburse.getTypeName())) {
|
|
||||||
templateName = "unionReimburse_con_public_birth";
|
|
||||||
} else if (("会员去世").equals(unionReimburse.getTypeName()) || ("直系亲属去世").equals(unionReimburse.getTypeName())) {
|
|
||||||
if (realMoney < 5000) {
|
|
||||||
templateName = "unionReimburse_con_public_dead_small";
|
|
||||||
} else {
|
|
||||||
templateName = "unionReimburse_con_public_dead_big";
|
|
||||||
}
|
|
||||||
} else {//其他
|
|
||||||
templateName = "unionReimburse_con_public_other";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Print) {
|
|
||||||
exportAsPDF(templateName, docData, unionReimburse, response);
|
|
||||||
} else {
|
|
||||||
exportAsWord(templateName, docData, unionReimburse, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出PDF
|
|
||||||
@At
|
|
||||||
private void exportAsPDF(String templateName, HashMap<String, Object> docData,
|
|
||||||
UnionReimburse unionReimburse, HttpServletResponse response) throws Exception {
|
|
||||||
String fileName = "中国地质大学(武汉)工会经费日常报销_" + unionReimburse.getUserName() + "_" +
|
|
||||||
DateUtil.format(unionReimburse.getCreateTime(), "yyyyMMdd") + ".pdf";
|
|
||||||
|
|
||||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateName))
|
|
||||||
.render(docData)
|
|
||||||
.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:{},错误信息:{}", unionReimburse.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出Word
|
|
||||||
@At
|
|
||||||
private void exportAsWord(String templateName, HashMap<String, Object> docData,
|
|
||||||
UnionReimburse unionReimburse, HttpServletResponse response) throws Exception {
|
|
||||||
String fileName = "中国地质大学(武汉)工会经费日常报销_" + unionReimburse.getUserName() + "_" +
|
|
||||||
DateUtil.format(unionReimburse.getCreateTime(), "yyyyMMdd") + ".docx";
|
|
||||||
|
|
||||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
|
||||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateName))
|
|
||||||
.render(docData)
|
|
||||||
.writeAndClose(byteArrayOutputStream);
|
|
||||||
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("工会报销单据导出Word失败,id:{},错误信息:{}", unionReimburse.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -279,6 +279,9 @@ public class UnionReimburse extends BaseModel {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
private String condolenceTypeId;
|
private String condolenceTypeId;
|
||||||
|
|
||||||
|
/** 慰问类型业务编码,仅供申请和详情回显控制字段显示,不映射数据库列。 */
|
||||||
|
private String condolenceTypeCode;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("慰问类型方式")
|
@Comment("慰问类型方式")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.unionReimburse.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("union_reimburse_bank_account")
|
||||||
|
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||||
|
@TableIndexes(@Index(name = "uk_reimburse_bank_account", fields = {
|
||||||
|
"payer", "bankUserName", "bankCardNumber", "bankOfDeposit"
|
||||||
|
}, unique = true))
|
||||||
|
public class UnionReimburseBankAccount extends BaseModel {
|
||||||
|
@Name
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
@PrevInsert(uu32 = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("付款人用户ID")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
|
private String payer;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
+15
@@ -44,11 +44,26 @@ public class UnionReimburseInvoiceDetail extends BaseModel {
|
|||||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||||
private String invoiceNo;
|
private String invoiceNo;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("开票日期,格式 yyyy-MM-dd")
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 10)
|
||||||
|
private String invoiceDate;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("发票金额")
|
@Comment("发票金额")
|
||||||
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
||||||
private Double invoiceAmount;
|
private Double invoiceAmount;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("税前金额")
|
||||||
|
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
||||||
|
private Double amountWithoutTax;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Comment("合计税额")
|
||||||
|
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
||||||
|
private Double taxAmount;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@Comment("销售方信息名称")
|
@Comment("销售方信息名称")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 200)
|
@ColDefine(type = ColType.VARCHAR, width = 200)
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.unionReimburse.service;
|
||||||
|
|
||||||
|
import com.budwk.app.base.service.BaseService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseBankAccount;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface UnionReimburseBankAccountService extends BaseService<UnionReimburseBankAccount> {
|
||||||
|
/**
|
||||||
|
* @param payer 付款人用户ID,非姓名或工号
|
||||||
|
* @param reimburseFundSource 当前经费来源字典编码
|
||||||
|
* @param clubId 协会经费对应的协会ID,其他来源可为空
|
||||||
|
* @return 可选账户列表,每项包含 id、bankUserName、bankCardNumber、bankOfDeposit
|
||||||
|
*/
|
||||||
|
List<NutMap> listAccounts(String payer, String reimburseFundSource, String clubId);
|
||||||
|
|
||||||
|
/** @param form 已通过校验的报销单;提取全部对私支付明细,去重保存完整账户。 */
|
||||||
|
void saveAccounts(UnionReimburse form);
|
||||||
|
|
||||||
|
/** @return 扫描的历史报销单数量;仅系统管理员可执行,可重复执行且不覆盖已有账户。 */
|
||||||
|
int importHistory();
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.unionReimburse.service;
|
||||||
|
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** 工会报销活动及慰问审批单共用的导出服务。 */
|
||||||
|
public interface UnionReimburseExportService {
|
||||||
|
/**
|
||||||
|
* @param id 当前登录人已提交的报销单ID,不接受其他人的单据或待提交单据
|
||||||
|
* @return 完整PDF字节数组,模板或转换失败时抛出业务异常
|
||||||
|
*/
|
||||||
|
byte[] exportPdf(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总页所有状态的报销单均可打印,必须具有汇总权限并符合所属工会数据范围。
|
||||||
|
* @param id 汇总页选中的报销单 ID,可为待提交或其他申请状态
|
||||||
|
* @return 按慰问类型选择模板生成的完整 PDF 字节数组,活动沿用原模板;无权限或生成失败时抛出业务异常
|
||||||
|
*/
|
||||||
|
byte[] exportCollectPdf(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param form 已加载发票明细的报销单,支付明细为空时兼容旧主表字段
|
||||||
|
* @return 活动及慰问模板通用的平铺字段及 payments、invoices 循环行
|
||||||
|
*/
|
||||||
|
Map<String, Object> buildExportData(UnionReimburse form);
|
||||||
|
}
|
||||||
+4
-1
@@ -50,7 +50,10 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
|
|||||||
Result checkInvoiceDuplicate(String invoiceNo, String reimburseId);
|
Result checkInvoiceDuplicate(String invoiceNo, String reimburseId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 识别单张发票文件,并在需要时调用验真接口补齐票面信息。
|
* 识别单张发票文件并强制验真。
|
||||||
|
* @param fileId 已上传发票的文件ID
|
||||||
|
* @param verifySwitch 兼容旧页面参数,不再允许关闭验真
|
||||||
|
* @return Result.data 包含票号、金额、销售方、项目名称及 verified=true;失败返回非零 code 和原因
|
||||||
*/
|
*/
|
||||||
Result recognizeInvoice(String fileId, Boolean verifySwitch);
|
Result recognizeInvoice(String fileId, Boolean verifySwitch);
|
||||||
|
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.unionReimburse.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||||
|
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.club.model.ClubUser;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseBankAccount;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseBankAccountService;
|
||||||
|
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.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@IocBean(args = {"refer:dao"})
|
||||||
|
public class UnionReimburseBankAccountServiceImpl extends BaseServiceImpl<UnionReimburseBankAccount>
|
||||||
|
implements UnionReimburseBankAccountService {
|
||||||
|
public UnionReimburseBankAccountServiceImpl(Dao dao) {
|
||||||
|
super(dao);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按当前付款人候选范围校验用户ID,防止直接传入其他人员ID读取账户。 */
|
||||||
|
@Override
|
||||||
|
public List<NutMap> listAccounts(String payer, String reimburseFundSource, String clubId) {
|
||||||
|
if (StrUtil.isBlank(payer)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", payer));
|
||||||
|
if (user == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource)) {
|
||||||
|
if (StrUtil.isBlank(clubId) || dao().count(ClubUser.class,
|
||||||
|
Cnd.where("clubId", "=", clubId).and("userId", "=", payer)) == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} else if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())
|
||||||
|
&& (StrUtil.isBlank(SecurityUtil.getUnionId())
|
||||||
|
|| !Objects.equals(user.getUnionId(), SecurityUtil.getUnionId()))) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("SELECT id, bankUserName, bankCardNumber, bankOfDeposit "
|
||||||
|
+ "FROM union_reimburse_bank_account WHERE payer = @payer AND delFlag = 0 "
|
||||||
|
+ "ORDER BY createdAt DESC, id DESC");
|
||||||
|
sql.params().set("payer", payer);
|
||||||
|
return listMap(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存全部对私明细,账户写入与调用方的报销保存共用事务。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void saveAccounts(UnionReimburse form) {
|
||||||
|
if (form == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (form.getPaymentDetails() != null && !form.getPaymentDetails().isEmpty()) {
|
||||||
|
for (JSONObject detail : form.getPaymentDetails()) {
|
||||||
|
if (detail != null && "UNION_REIMBURSE_PAYMENT_WAY_2".equals(detail.getStr("paymentWay"))) {
|
||||||
|
saveAccount(detail.getStr("payer"), detail.getStr("bankUserName"),
|
||||||
|
detail.getStr("bankCardNumber"), detail.getStr("bankOfDeposit"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("UNION_REIMBURSE_PAYMENT_WAY_2".equals(form.getPaymentWay())) {
|
||||||
|
// 仅无支付明细的旧单据使用主表字段,避免导入已过时的第一条账户副本。
|
||||||
|
saveAccount(form.getPayer(), form.getBankUserName(), form.getBankCardNumber(), form.getBankOfDeposit());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤不完整历史资料;唯一索引配合原子插入,防止并发报销生成重复账户。 */
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
protected void saveAccount(String payer, String name, String card, String bank) {
|
||||||
|
payer = StrUtil.trim(payer);
|
||||||
|
name = StrUtil.trim(name);
|
||||||
|
card = StrUtil.trim(card);
|
||||||
|
bank = StrUtil.trim(bank);
|
||||||
|
if (StrUtil.hasBlank(payer, name, card, bank) || payer.length() > 100 || name.length() > 100
|
||||||
|
|| card.length() > 100 || bank.length() > 100 || !card.matches("[1-9]([0-9]{15}|[0-9]{18})")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
INSERT INTO union_reimburse_bank_account
|
||||||
|
(id, payer, bankUserName, bankCardNumber, bankOfDeposit,
|
||||||
|
createdBy, createdAt, updatedBy, updatedAt, delFlag)
|
||||||
|
VALUES (@id, @payer, @name, @card, @bank, @userId, @now, @userId, @now, 0)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id
|
||||||
|
""");
|
||||||
|
sql.params().set("id", UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
sql.params().set("payer", payer).set("name", name).set("card", card).set("bank", bank);
|
||||||
|
sql.params().set("userId", SecurityUtil.getUserId()).set("now", System.currentTimeMillis());
|
||||||
|
dao().execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 管理员一次性初始化;按ID分批读取,避免一次加载全部历史单据。 */
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public int importHistory() {
|
||||||
|
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
|
||||||
|
throw new BaseException("仅系统管理员可初始化历史收款账户");
|
||||||
|
}
|
||||||
|
int processed = 0;
|
||||||
|
String lastId = "";
|
||||||
|
while (true) {
|
||||||
|
List<UnionReimburse> records = dao().query(UnionReimburse.class,
|
||||||
|
Cnd.where("id", ">", lastId).and("delFlag", "=", false).asc("id"), dao().createPager(1, 200));
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
for (UnionReimburse record : records) {
|
||||||
|
saveAccounts(record);
|
||||||
|
}
|
||||||
|
processed += records.size();
|
||||||
|
lastId = records.get(records.size() - 1).getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+387
@@ -0,0 +1,387 @@
|
|||||||
|
package com.budwk.app.zhgh.dayofficework.unionReimburse.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.dao.Dao;
|
||||||
|
import com.budwk.app.base.exception.BaseException;
|
||||||
|
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.web.commons.auth.utils.SecurityUtil;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseInvoiceDetail;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||||
|
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||||
|
import com.deepoove.poi.XWPFTemplate;
|
||||||
|
import com.deepoove.poi.config.Configure;
|
||||||
|
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||||
|
import com.deepoove.poi.data.PictureType;
|
||||||
|
import com.deepoove.poi.data.Pictures;
|
||||||
|
import com.deepoove.poi.template.ElementTemplate;
|
||||||
|
import com.google.zxing.*;
|
||||||
|
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||||
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||||
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
import com.google.zxing.qrcode.QRCodeReader;
|
||||||
|
import org.apache.poi.xwpf.usermodel.*;
|
||||||
|
import org.apache.poi.util.Units;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@IocBean
|
||||||
|
public class UnionReimburseExportServiceImpl implements UnionReimburseExportService {
|
||||||
|
public static final String ACTIVITY_TEMPLATE = "union_reimburse_activity_pdf";
|
||||||
|
public static final String CONDOLENCE_TEMPLATE = "union_reimburse_condolence_pdf";
|
||||||
|
// 使用慰问类型业务编码匹配模板,避免类型名称调整后选择错误;编码与现有申请页一致。
|
||||||
|
private static final Map<String, String> COLLECT_CONDOLENCE_TEMPLATES = Map.of(
|
||||||
|
"A", "union_reimburse_condolence_marriage_pdf",
|
||||||
|
"D", "union_reimburse_condolence_birth_pdf",
|
||||||
|
"E", "union_reimburse_condolence_hospital_pdf",
|
||||||
|
"G", "union_reimburse_condolence_member_death_pdf",
|
||||||
|
"H", "union_reimburse_condolence_relative_death_pdf");
|
||||||
|
@Inject
|
||||||
|
private UnionReimburseService unionReimburseService;
|
||||||
|
@Inject
|
||||||
|
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
||||||
|
@Inject
|
||||||
|
private Dao dao;
|
||||||
|
|
||||||
|
/** 当前“我的报销”导出仅允许单据经办人,状态白名单与页面按钮保持一致。 */
|
||||||
|
@Override
|
||||||
|
public byte[] exportPdf(String id) {
|
||||||
|
return renderDocument(loadExportForm(id, false, false), true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总打印支持所有状态,按汇总权限和所属工会范围读取,并按慰问类型选择模板。 */
|
||||||
|
@Override
|
||||||
|
public byte[] exportCollectPdf(String id) {
|
||||||
|
return renderDocument(loadExportForm(id, true, true), true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读取申请与发票明细,不修改业务数据。
|
||||||
|
* @param id 报销单 ID,空值、记录不存在或已删除时拒绝导出
|
||||||
|
* @param allowCollect 是否允许具有汇总数据权限的用户读取他人单据
|
||||||
|
* @param collectPrint 是否为汇总页全状态打印;此时必须通过汇总数据权限校验,经办人身份不能绕过工会范围
|
||||||
|
* @return 已校验权限并补齐发票明细的报销单,原有导出入口仍只允许已提交状态
|
||||||
|
*/
|
||||||
|
private UnionReimburse loadExportForm(String id, boolean allowCollect, boolean collectPrint) {
|
||||||
|
if (StrUtil.isBlank(id)) {
|
||||||
|
throw new BaseException("请选择需要导出的报销单");
|
||||||
|
}
|
||||||
|
UnionReimburse form = unionReimburseService.fetch(id);
|
||||||
|
if (form == null || Boolean.TRUE.equals(form.getDelFlag())) {
|
||||||
|
throw new BaseException("报销记录不存在");
|
||||||
|
}
|
||||||
|
String userId = SecurityUtil.getUserId();
|
||||||
|
boolean owner = StrUtil.isNotBlank(userId) && Objects.equals(form.getUserId(), userId);
|
||||||
|
boolean collectAllowed = allowCollect && StrUtil.isNotBlank(userId)
|
||||||
|
&& AuthUtil.hasPermission("unionReimburse.collect")
|
||||||
|
&& (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())
|
||||||
|
|| (StrUtil.isNotBlank(SecurityUtil.getUnionId())
|
||||||
|
&& Objects.equals(form.getUnionId(), SecurityUtil.getUnionId())));
|
||||||
|
if (collectPrint ? !collectAllowed : (!owner && !collectAllowed)) {
|
||||||
|
throw new BaseException("无权导出此报销单");
|
||||||
|
}
|
||||||
|
// 汇总页打印不限制申请状态,我的报销导出仍只允许已提交状态。
|
||||||
|
if (!collectPrint && !Set.of(2, 3, 4, 5).contains(form.getStateId())) {
|
||||||
|
throw new BaseException("报销单提交后才能导出");
|
||||||
|
}
|
||||||
|
// 已核对导出数据权限,单独补齐发票明细,避免申请页权限误拦截汇总打印。
|
||||||
|
form.setInvoiceDetails(dao.query(UnionReimburseInvoiceDetail.class,
|
||||||
|
Cnd.where("reimburseId", "=", id).asc("createdAt")));
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总页五类慰问使用独立模板,其余类型和我的报销沿用原模板。
|
||||||
|
* @param form 已通过数据权限校验的报销单,使用慰问类型 ID 读取业务编码
|
||||||
|
* @param collectPrint 是否为汇总页打印,只有该入口启用分类模板
|
||||||
|
* @return 系统导出模板管理中的模板编码;类型关联缺失时提示核对,不猜测类型名称
|
||||||
|
*/
|
||||||
|
private String resolveTemplateCode(UnionReimburse form, boolean collectPrint) {
|
||||||
|
if (!"UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())) {
|
||||||
|
return ACTIVITY_TEMPLATE;
|
||||||
|
}
|
||||||
|
if (!collectPrint) {
|
||||||
|
return CONDOLENCE_TEMPLATE;
|
||||||
|
}
|
||||||
|
// 兼容旧申请将类型 ID 保存在 condolenceType 的情况,不回写历史数据。
|
||||||
|
String typeId = StrUtil.blankToDefault(form.getCondolenceTypeId(), form.getCondolenceType());
|
||||||
|
CondolenceType type = StrUtil.isBlank(typeId) ? null : dao.fetch(CondolenceType.class, typeId);
|
||||||
|
if (type == null || StrUtil.isBlank(type.getCode())) {
|
||||||
|
throw new BaseException("慰问类型不存在或编码为空,请核对后打印");
|
||||||
|
}
|
||||||
|
// 五类之外的既有慰问继续使用通用模板;已匹配的专用模板缺失时由模板加载器明确报错。
|
||||||
|
return COLLECT_CONDOLENCE_TEMPLATES.getOrDefault(type.getCode(), CONDOLENCE_TEMPLATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模板共用字段和循环行配置;汇总打印按慰问类型选模板并补充编号二维码。 */
|
||||||
|
private byte[] renderDocument(UnionReimburse form, boolean asPdf, boolean collectPrint) {
|
||||||
|
String templateCode = resolveTemplateCode(form, collectPrint);
|
||||||
|
Path docx = null;
|
||||||
|
Path pdf = null;
|
||||||
|
try {
|
||||||
|
docx = Files.createTempFile("union_reimburse_", ".docx");
|
||||||
|
if (asPdf) pdf = Files.createTempFile("union_reimburse_", ".pdf");
|
||||||
|
// 活动和慰问模板的支付标记均位于表头,支付明细统一循环下一行。
|
||||||
|
// 活动模板的发票标记也位于表头,循环下一行;慰问保留原有的当前行配置。
|
||||||
|
boolean invoiceOnSameRow = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject());
|
||||||
|
Configure config = Configure.builder()
|
||||||
|
.bind("payments", new LoopRowTableRenderPolicy(false))
|
||||||
|
.bind("invoices", new LoopRowTableRenderPolicy(invoiceOnSameRow)).build();
|
||||||
|
try (InputStream input = sysOfficeTemplateUtil.getTemplate(templateCode);
|
||||||
|
XWPFTemplate template = XWPFTemplate.compile(input, config);
|
||||||
|
OutputStream output = Files.newOutputStream(docx)) {
|
||||||
|
Map<String, Object> data = buildExportData(form);
|
||||||
|
if (collectPrint) {
|
||||||
|
prepareCollectQrCode(template, data, form.getDocumentNo());
|
||||||
|
}
|
||||||
|
template.render(data).write(output);
|
||||||
|
}
|
||||||
|
if (!asPdf) {
|
||||||
|
return Files.readAllBytes(docx);
|
||||||
|
}
|
||||||
|
OfficePlusUtil.convert(docx.toString(), pdf.toString());
|
||||||
|
byte[] bytes = Files.readAllBytes(pdf);
|
||||||
|
// 公共转换器可能记录异常后返回,下载前验证结果,避免把空文件当成功响应。
|
||||||
|
if (bytes.length < 5 || bytes[0] != '%' || bytes[1] != 'P' || bytes[2] != 'D'
|
||||||
|
|| bytes[3] != 'F' || bytes[4] != '-') {
|
||||||
|
throw new BaseException("PDF生成失败,请检查模板及转换服务");
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
} catch (BaseException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("报销导出失败,单据ID:{},模板:{}", form.getId(), templateCode, e);
|
||||||
|
throw new BaseException("报销导出失败,请检查模板配置");
|
||||||
|
} finally {
|
||||||
|
// 成功或异常均清理本次转换文件,不影响其他并发导出。
|
||||||
|
for (Path path : new Path[]{docx, pdf}) {
|
||||||
|
if (path != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("报销导出临时文件清理失败:{}", path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 汇总打印沿用老系统的单据编号二维码,仅修改本次生成的文档,不覆盖模板。
|
||||||
|
* @param template 当前已编译模板,优先使用 qrcode 图片标记,其次替换标题区的固定二维码
|
||||||
|
* @param data 本次渲染数据,qrcode 字段为 80×80 的 PNG 图片对象
|
||||||
|
* @param documentNo 原始单据编号,缺失时拒绝打印,避免用业务 ID 冒充编号
|
||||||
|
*/
|
||||||
|
private void prepareCollectQrCode(XWPFTemplate template, Map<String, Object> data,
|
||||||
|
String documentNo) throws Exception {
|
||||||
|
if (StrUtil.isBlank(documentNo)) {
|
||||||
|
throw new BaseException("报销单编号为空,无法生成打印二维码,请先核对单据编号");
|
||||||
|
}
|
||||||
|
var matrix = new QRCodeWriter().encode(documentNo, BarcodeFormat.QR_CODE, 80, 80);
|
||||||
|
var image = MatrixToImageWriter.toBufferedImage(matrix);
|
||||||
|
data.put("qrcode", Pictures.ofBufferedImage(image, PictureType.PNG).size(80, 80).create());
|
||||||
|
boolean hasTag = template.getElementTemplates().stream()
|
||||||
|
.anyMatch(item -> item instanceof ElementTemplate
|
||||||
|
&& "qrcode".equals(((ElementTemplate) item).getTagName()));
|
||||||
|
if (hasTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(image, "png", bytes);
|
||||||
|
XWPFDocument document = template.getXWPFDocument();
|
||||||
|
// 部分现用模板内嵌了固定二维码。仅识别第一张业务表格之前的二维码,保留徽标等普通图片。
|
||||||
|
boolean replaced = false;
|
||||||
|
for (IBodyElement element : document.getBodyElements()) {
|
||||||
|
if (element instanceof XWPFTable) break;
|
||||||
|
if (!(element instanceof XWPFParagraph)) continue;
|
||||||
|
for (XWPFRun run : ((XWPFParagraph) element).getRuns()) {
|
||||||
|
for (XWPFPicture picture : run.getEmbeddedPictures()) {
|
||||||
|
if (picture.getPictureData() == null) continue;
|
||||||
|
var existing = ImageIO.read(new ByteArrayInputStream(picture.getPictureData().getData()));
|
||||||
|
if (existing == null) continue;
|
||||||
|
try {
|
||||||
|
new QRCodeReader().decode(new BinaryBitmap(new HybridBinarizer(
|
||||||
|
new BufferedImageLuminanceSource(existing))));
|
||||||
|
} catch (NotFoundException | ChecksumException | FormatException e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String relationship = document.addPictureData(bytes.toByteArray(), Document.PICTURE_TYPE_PNG);
|
||||||
|
picture.getCTPicture().getBlipFill().getBlip().setEmbed(relationship);
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (replaced) return;
|
||||||
|
// 无二维码标记或固定二维码的模板,在正文开头添加右对齐图片,不依赖模板重新上传。
|
||||||
|
XWPFParagraph paragraph;
|
||||||
|
if (document.getBodyElements().isEmpty()) {
|
||||||
|
paragraph = document.createParagraph();
|
||||||
|
} else {
|
||||||
|
IBodyElement first = document.getBodyElements().get(0);
|
||||||
|
var cursor = first instanceof XWPFParagraph
|
||||||
|
? ((XWPFParagraph) first).getCTP().newCursor()
|
||||||
|
: ((XWPFTable) first).getCTTbl().newCursor();
|
||||||
|
try {
|
||||||
|
paragraph = document.insertNewParagraph(cursor);
|
||||||
|
} finally {
|
||||||
|
cursor.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.RIGHT);
|
||||||
|
paragraph.setSpacingBefore(0);
|
||||||
|
paragraph.setSpacingAfter(0);
|
||||||
|
paragraph.createRun().addPicture(new ByteArrayInputStream(bytes.toByteArray()),
|
||||||
|
Document.PICTURE_TYPE_PNG, "qrcode.png", Units.pixelToEMU(80), Units.pixelToEMU(80));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 平铺字段供两类模板复用,支付总额与发票总额分别计算,不使用第一条支付代替全部明细。 */
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> buildExportData(UnionReimburse form) {
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("date", date(form.getCreateTime()));
|
||||||
|
data.put("no", text(form.getDocumentNo()));
|
||||||
|
String unit = text(form.getUnitName());
|
||||||
|
data.put("union", text(form.getUnionName()) + (unit.isEmpty() ? "" : "(" + unit + ")"));
|
||||||
|
data.put("handler", text(form.getUserName()));
|
||||||
|
data.put("login", text(form.getLoginName()));
|
||||||
|
data.put("phone", text(form.getMobile()));
|
||||||
|
String project = form.getReimburseProject();
|
||||||
|
data.put("project", ("UNION_REIMBURSE_PROJECT_2".equals(project) ? "√" : "□") + "文体活动 "
|
||||||
|
+ ("UNION_REIMBURSE_PROJECT_1".equals(project) ? "√" : "□") + "慰问 "
|
||||||
|
+ ("UNION_REIMBURSE_PROJECT_3".equals(project) ? "√" : "□") + "日常活动 "
|
||||||
|
+ ("UNION_REIMBURSE_PROJECT_4".equals(project) ? "√" : "□") + "专项活动");
|
||||||
|
data.put("activity", text(form.getActivityName()));
|
||||||
|
data.put("reason", text(form.getPaymentNotes()));
|
||||||
|
data.put("notes", text(form.getNotes()));
|
||||||
|
// 附件只有上传文件数量,没有纸质页数,模板明确标注为“附件份数”。
|
||||||
|
data.put("attachments", form.getFiles() == null ? 0 : form.getFiles().size());
|
||||||
|
List<JSONObject> details = form.getPaymentDetails();
|
||||||
|
boolean legacy = details == null || details.isEmpty();
|
||||||
|
if (legacy) {
|
||||||
|
JSONObject item = new JSONObject();
|
||||||
|
item.set("paymentWay", form.getPaymentWay());
|
||||||
|
item.set("payerLoginname", form.getPayerLoginname());
|
||||||
|
item.set("payerName", form.getPayerName());
|
||||||
|
item.set("bankUserName", form.getBankUserName());
|
||||||
|
item.set("bankCardNumber", form.getBankCardNumber());
|
||||||
|
item.set("bankOfDeposit", form.getBankOfDeposit());
|
||||||
|
// 历史记录未保存支付明细时,沿用实际金额、申请金额的优先级;慰问单再回退到慰问金额。
|
||||||
|
// 仅空值触发回退,已明确填写的零金额不能被其他字段覆盖。
|
||||||
|
Double legacyMoney = form.getRealMoney() != null ? form.getRealMoney() : form.getMoney();
|
||||||
|
if (legacyMoney == null && "UNION_REIMBURSE_PROJECT_1".equals(project)) {
|
||||||
|
legacyMoney = form.getCondolenceMoney();
|
||||||
|
}
|
||||||
|
item.set("money", legacyMoney);
|
||||||
|
details = List.of(item);
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> payments = new ArrayList<>();
|
||||||
|
BigDecimal paymentTotal = BigDecimal.ZERO;
|
||||||
|
for (JSONObject item : details) {
|
||||||
|
if (item == null) continue;
|
||||||
|
boolean privatePayment = "UNION_REIMBURSE_PAYMENT_WAY_2".equals(item.getStr("paymentWay"));
|
||||||
|
BigDecimal amount = money(item.get("money"));
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("way", privatePayment ? "对私支付" : "对公支付");
|
||||||
|
row.put("login", privatePayment ? text(item.getStr("payerLoginname")) : "");
|
||||||
|
row.put("name", privatePayment ? text(item.getStr("payerName")) : "");
|
||||||
|
// 开户行仅输出银行名称,不拼接户名或文字前缀;对公支付保持空白。
|
||||||
|
row.put("bank", privatePayment ? text(item.getStr("bankOfDeposit")) : "");
|
||||||
|
row.put("card", privatePayment ? text(item.getStr("bankCardNumber")) : "");
|
||||||
|
row.put("amount", amount.toPlainString());
|
||||||
|
payments.add(row);
|
||||||
|
paymentTotal = paymentTotal.add(amount);
|
||||||
|
}
|
||||||
|
data.put("payments", payments);
|
||||||
|
// 两类模板共用支付合计 total 与大写 upper,单条支付使用 payments.amount。
|
||||||
|
data.put("total", paymentTotal.setScale(2).toPlainString());
|
||||||
|
data.put("upper", upper(paymentTotal));
|
||||||
|
List<Map<String, Object>> invoices = new ArrayList<>();
|
||||||
|
BigDecimal invoiceTotal = BigDecimal.ZERO;
|
||||||
|
if (form.getInvoiceDetails() != null) {
|
||||||
|
for (UnionReimburseInvoiceDetail item : form.getInvoiceDetails()) {
|
||||||
|
if (item == null) continue;
|
||||||
|
BigDecimal amount = money(item.getInvoiceAmount());
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("seq", invoices.size() + 1);
|
||||||
|
row.put("no", text(item.getInvoiceNo()));
|
||||||
|
row.put("seller", text(item.getSellerName()));
|
||||||
|
// 发票行分别绑定开票日期、税前金额、税额及价税合计;历史空值保持空白。
|
||||||
|
row.put("date", text(item.getInvoiceDate()));
|
||||||
|
row.put("amount", item.getAmountWithoutTax() == null ? "" : money(item.getAmountWithoutTax()).toPlainString());
|
||||||
|
row.put("tax", item.getTaxAmount() == null ? "" : money(item.getTaxAmount()).toPlainString());
|
||||||
|
row.put("total", amount.toPlainString());
|
||||||
|
invoices.add(row);
|
||||||
|
invoiceTotal = invoiceTotal.add(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.put("invoices", invoices);
|
||||||
|
data.put("invoiceTotal", invoiceTotal.setScale(2).toPlainString());
|
||||||
|
data.put("invoiceCount", invoices.isEmpty() ? text(form.getInvoiceNumber()) : invoices.size());
|
||||||
|
// 验真状态没有单独落库,导出模板不可将其固定写成“已验真”。
|
||||||
|
data.put("person", text(form.getCondolenceUserName()));
|
||||||
|
data.put("personNo", text(form.getCondolenceLoginName()));
|
||||||
|
data.put("type", text(form.getTypeName()));
|
||||||
|
data.put("way", text(form.getWay()));
|
||||||
|
data.put("visitDate", date(form.getCondolenceTime()));
|
||||||
|
data.put("participants", text(form.getParticipants()));
|
||||||
|
data.put("illness", text(form.getHospitalCausation()));
|
||||||
|
data.put("hospital", text(form.getHospital()));
|
||||||
|
data.put("times", text(form.getHospitalCount()));
|
||||||
|
String start = date(form.getHospitalizationStartTime());
|
||||||
|
String end = date(form.getHospitalizationEndTime());
|
||||||
|
data.put("hospitalDates", start + (start.isEmpty() || end.isEmpty() ? "" : " 至 ") + end);
|
||||||
|
data.put("birthDate", date(form.getFertilityTime()));
|
||||||
|
data.put("marryDate", date(form.getMarryTime()));
|
||||||
|
data.put("deathDate", date(form.getDeathTime()));
|
||||||
|
data.put("relation", text(form.getCondolenceRelationship()));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 金额统一为两位小数,空值显示0.00,非法历史金额拒绝生成错误单据。 */
|
||||||
|
private BigDecimal money(Object value) {
|
||||||
|
if (value == null || StrUtil.isBlank(value.toString())) return BigDecimal.ZERO.setScale(2);
|
||||||
|
try {
|
||||||
|
BigDecimal result = new BigDecimal(value.toString()).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
if (result.signum() < 0) throw new NumberFormatException();
|
||||||
|
return result;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new BaseException("报销记录金额异常,请核对后导出");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复用项目人民币大写转换,将转换异常统一为导出业务提示。 */
|
||||||
|
private String upper(BigDecimal value) {
|
||||||
|
try {
|
||||||
|
return MoneyUtil.toRMBUpper(value.toPlainString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BaseException("报销金额大写转换失败,请核对金额");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空字段在审批单中显示为空白。 */
|
||||||
|
private String text(String value) { return StrUtil.nullToEmpty(value); }
|
||||||
|
|
||||||
|
/** 业务日期只显示年月日,未填写的日期保持空白。 */
|
||||||
|
private String date(Date value) { return value == null ? "" : DateUtil.format(value, "yyyy-MM-dd"); }
|
||||||
|
}
|
||||||
+316
-74
@@ -16,10 +16,12 @@ import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayMan
|
|||||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail;
|
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail;
|
||||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.school.model.OutlayManageSchool;
|
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.school.model.OutlayManageSchool;
|
||||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion;
|
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion;
|
||||||
|
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseInvoiceDetail;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseInvoiceDetail;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.param.UnionReimbursePageForm;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.param.UnionReimbursePageForm;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||||
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseBankAccountService;
|
||||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
|
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
|
||||||
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;
|
||||||
@@ -47,8 +49,8 @@ import com.tencentcloudapi.ocr.v20181119.models.VatInvoice;
|
|||||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceItem;
|
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceItem;
|
||||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceOCRRequest;
|
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceOCRRequest;
|
||||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceOCRResponse;
|
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceOCRResponse;
|
||||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceVerifyRequest;
|
import cn.hutool.json.JSONObject;
|
||||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceVerifyResponse;
|
import cn.hutool.json.JSONUtil;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
@@ -80,6 +82,8 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
@Inject
|
@Inject
|
||||||
private SysFileService sysFileService;
|
private SysFileService sysFileService;
|
||||||
@Inject
|
@Inject
|
||||||
|
private UnionReimburseBankAccountService unionReimburseBankAccountService;
|
||||||
|
@Inject
|
||||||
private PropertiesProxy conf;
|
private PropertiesProxy conf;
|
||||||
public UnionReimburseServiceImpl(Dao dao) {
|
public UnionReimburseServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
@@ -121,7 +125,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
/**
|
/**
|
||||||
* 保存、提交及重新提交共用支付信息校验,并在同一事务内保存申请和发票明细。
|
* 保存、提交及重新提交共用支付信息校验,并在同一事务内保存申请和发票明细。
|
||||||
* @param unionReimburse 申请表单,paymentDetails 为支付方式、金额及对私收款资料组成的明细列表
|
* @param unionReimburse 申请表单,paymentDetails 为支付方式、金额及对私收款资料组成的明细列表
|
||||||
* @param stateId 1 表示保存草稿,2 表示提交;两种状态均要求支付信息完整
|
* @param stateId 1 表示保存草稿,2 表示提交;均要求支付信息完整,发票选填,上传后须通过服务端验真
|
||||||
* @return Result 成功或失败结果,msg 包含失败明细的行号及原因
|
* @return Result 成功或失败结果,msg 包含失败明细的行号及原因
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -130,10 +134,20 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
if (unionReimburse == null) {
|
if (unionReimburse == null) {
|
||||||
return Result.error("报销数据不能为空");
|
return Result.error("报销数据不能为空");
|
||||||
}
|
}
|
||||||
|
// 发票选填;保存、提交及重提均对已上传的发票检查完整性。
|
||||||
|
Result invoiceResult = validateInvoiceDetails(unionReimburse);
|
||||||
|
if (invoiceResult != null) {
|
||||||
|
return invoiceResult;
|
||||||
|
}
|
||||||
Result paymentResult = validatePaymentDetails(unionReimburse);
|
Result paymentResult = validatePaymentDetails(unionReimburse);
|
||||||
if (paymentResult != null) {
|
if (paymentResult != null) {
|
||||||
return paymentResult;
|
return paymentResult;
|
||||||
}
|
}
|
||||||
|
// 保存、提交及重提均在写入前检查支付总额,不能只依赖页面的金额限制。
|
||||||
|
Result amountResult = validatePaymentAmountLimit(unionReimburse);
|
||||||
|
if (amountResult != null) {
|
||||||
|
return amountResult;
|
||||||
|
}
|
||||||
UnionReimburse oldRecord = null;
|
UnionReimburse oldRecord = null;
|
||||||
if (StrUtil.isNotBlank(unionReimburse.getId())) {
|
if (StrUtil.isNotBlank(unionReimburse.getId())) {
|
||||||
oldRecord = this.fetch(unionReimburse.getId());
|
oldRecord = this.fetch(unionReimburse.getId());
|
||||||
@@ -174,11 +188,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
normalizeInvoiceDetails(unionReimburse);
|
normalizeInvoiceDetails(unionReimburse);
|
||||||
if (needInvoiceDetails(unionReimburse)) {
|
fillInvoiceSummary(unionReimburse);
|
||||||
fillInvoiceSummary(unionReimburse);
|
|
||||||
} else {
|
|
||||||
fillCondolenceInvoiceSummary(unionReimburse);
|
|
||||||
}
|
|
||||||
fillDefaultRealMoney(unionReimburse);
|
fillDefaultRealMoney(unionReimburse);
|
||||||
if (stateId == 2) {
|
if (stateId == 2) {
|
||||||
Result validateResult = validateBeforeSubmit(unionReimburse);
|
Result validateResult = validateBeforeSubmit(unionReimburse);
|
||||||
@@ -195,6 +205,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验实际文件与票面内容后才允许写入,不接收前端的验真状态作为凭据。
|
||||||
|
Result verifyResult = validateInvoiceAuthenticity(unionReimburse);
|
||||||
|
if (verifyResult != null) {
|
||||||
|
return verifyResult;
|
||||||
|
}
|
||||||
if (oldRecord == null) {
|
if (oldRecord == null) {
|
||||||
unionReimburse.setCreateTime(toSecondPrecision(DateUtil.date()));
|
unionReimburse.setCreateTime(toSecondPrecision(DateUtil.date()));
|
||||||
unionReimburse.setDocumentNo(generateDocumentNo());
|
unionReimburse.setDocumentNo(generateDocumentNo());
|
||||||
@@ -206,6 +221,8 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
fillDefaultRealMoney(unionReimburse);
|
fillDefaultRealMoney(unionReimburse);
|
||||||
this.dao().insertOrUpdate(unionReimburse);
|
this.dao().insertOrUpdate(unionReimburse);
|
||||||
saveInvoiceDetails(unionReimburse.getId(), unionReimburse.getInvoiceDetails());
|
saveInvoiceDetails(unionReimburse.getId(), unionReimburse.getInvoiceDetails());
|
||||||
|
// 报销保存成功后积累每条对私账户;失败时随本次报销事务一起回滚。
|
||||||
|
unionReimburseBankAccountService.saveAccounts(unionReimburse);
|
||||||
return Result.success(stateId == 2 ? "提交成功" : "保存成功");
|
return Result.success(stateId == 2 ? "提交成功" : "保存成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +236,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return new Date((date.getTime() / 1000) * 1000);
|
return new Date((date.getTime() / 1000) * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id 报销申请 ID,用于读取申请及关联的慰问类型
|
||||||
|
* @return 申请对象,condolenceTypeCode 为类型业务编码;未找到申请返回 null,类型缺失时编码为空
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public UnionReimburse getApplyForm(String id) {
|
public UnionReimburse getApplyForm(String id) {
|
||||||
UnionReimburse unionReimburse = this.fetch(id);
|
UnionReimburse unionReimburse = this.fetch(id);
|
||||||
@@ -228,6 +249,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
if (!canReadApply(unionReimburse)) {
|
if (!canReadApply(unionReimburse)) {
|
||||||
throw new BaseException("无权查看此报销申请");
|
throw new BaseException("无权查看此报销申请");
|
||||||
}
|
}
|
||||||
|
// 兼容旧申请将类型 ID 存在 condolenceType 中的情况;只读取编码,不改动历史关联。
|
||||||
|
String typeId = StrUtil.blankToDefault(unionReimburse.getCondolenceTypeId(), unionReimburse.getCondolenceType());
|
||||||
|
CondolenceType type = StrUtil.isBlank(typeId) ? null : dao().fetch(CondolenceType.class, typeId);
|
||||||
|
unionReimburse.setCondolenceTypeCode(type == null ? null : type.getCode());
|
||||||
List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query(
|
List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query(
|
||||||
UnionReimburseInvoiceDetail.class,
|
UnionReimburseInvoiceDetail.class,
|
||||||
Cnd.where("reimburseId", "=", id).asc("createdAt")
|
Cnd.where("reimburseId", "=", id).asc("createdAt")
|
||||||
@@ -416,6 +441,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return queryHistoryInvoiceDuplicate(currentInvoiceNo, reimburseId);
|
return queryHistoryInvoiceDuplicate(currentInvoiceNo, reimburseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param fileId 已上传发票的文件ID,只读取服务端存储的文件
|
||||||
|
* @param verifySwitch 兼容旧页面参数,传 false 或不传也必须验真
|
||||||
|
* @return Result.data 为票号、金额、销售方、项目等字段及 verified=true;识别或验真失败返回非零 code
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Result recognizeInvoice(String fileId, Boolean verifySwitch) {
|
public Result recognizeInvoice(String fileId, Boolean verifySwitch) {
|
||||||
if (StrUtil.isBlank(fileId)) {
|
if (StrUtil.isBlank(fileId)) {
|
||||||
@@ -431,14 +461,22 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
byte[] fileBytes = sysFileService.download(fileId);
|
byte[] fileBytes = sysFileService.download(fileId);
|
||||||
|
// OCR当前仅读取PDF第一页,拒绝多页文件,避免后续页未核验却随整份附件入库。
|
||||||
|
if (StrUtil.blankToDefault(sysFile.getName(), "").toLowerCase(java.util.Locale.ROOT).endsWith(".pdf")) {
|
||||||
|
try (org.apache.pdfbox.pdmodel.PDDocument document = org.apache.pdfbox.Loader.loadPDF(fileBytes)) {
|
||||||
|
if (document.getNumberOfPages() != 1) {
|
||||||
|
return Result.error("请将发票拆分为单页PDF后逐张上传验真");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
VatInvoiceOCRResponse response = executeOcr(credential, sysFile, fileBytes);
|
VatInvoiceOCRResponse response = executeOcr(credential, sysFile, fileBytes);
|
||||||
NutMap result = buildOcrResult(response);
|
NutMap result = buildOcrResult(response);
|
||||||
String msg = "发票识别成功";
|
String verifyMsg = enrichByVerify(credential, result);
|
||||||
if (Boolean.TRUE.equals(verifySwitch)) {
|
if (StrUtil.isNotBlank(verifyMsg)) {
|
||||||
String verifyMsg = enrichByVerify(credential, result);
|
return Result.error(verifyMsg);
|
||||||
msg = StrUtil.isBlank(verifyMsg) ? "发票识别并校验成功" : verifyMsg;
|
|
||||||
}
|
}
|
||||||
return Result.success(msg, result);
|
result.setv("verified", true);
|
||||||
|
return Result.success("发票识别并验真成功", result);
|
||||||
} catch (TencentCloudSDKException e) {
|
} catch (TencentCloudSDKException e) {
|
||||||
log.error("腾讯云发票识别失败,fileId:{}", fileId, e);
|
log.error("腾讯云发票识别失败,fileId:{}", fileId, e);
|
||||||
return Result.error("腾讯云发票识别失败:" + e.getMessage());
|
return Result.error("腾讯云发票识别失败:" + e.getMessage());
|
||||||
@@ -631,7 +669,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动类报销由发票明细自动汇总金额和发票张数,减少手工输入出错。
|
* 有发票时汇总票面金额和张数;无发票时按已校验的支付明细汇总金额,张数为零。
|
||||||
*/
|
*/
|
||||||
private void fillInvoiceSummary(UnionReimburse unionReimburse) {
|
private void fillInvoiceSummary(UnionReimburse unionReimburse) {
|
||||||
double totalMoney = 0D;
|
double totalMoney = 0D;
|
||||||
@@ -646,30 +684,15 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
invoiceFileCount++;
|
invoiceFileCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue());
|
// 无发票申请从服务端已校验的支付明细计算金额,不能信任前端传入的报销总额。
|
||||||
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
|
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
|
||||||
}
|
BigDecimal paymentTotal = BigDecimal.ZERO;
|
||||||
|
for (JSONObject detail : unionReimburse.getPaymentDetails()) {
|
||||||
/**
|
paymentTotal = paymentTotal.add(new BigDecimal(detail.get("money").toString()));
|
||||||
* 慰问类报销允许不填发票;有发票时按发票汇总金额申请,没有发票时按慰问金额申请。
|
|
||||||
*/
|
|
||||||
private void fillCondolenceInvoiceSummary(UnionReimburse unionReimburse) {
|
|
||||||
double totalMoney = 0D;
|
|
||||||
int invoiceFileCount = 0;
|
|
||||||
for (UnionReimburseInvoiceDetail detail : unionReimburse.getInvoiceDetails()) {
|
|
||||||
if (detail.getInvoiceAmount() != null) {
|
|
||||||
totalMoney = BigDecimal.valueOf(totalMoney)
|
|
||||||
.add(BigDecimal.valueOf(detail.getInvoiceAmount()))
|
|
||||||
.doubleValue();
|
|
||||||
}
|
|
||||||
if (Lang.isNotEmpty(detail.getInvoiceFiles())) {
|
|
||||||
invoiceFileCount++;
|
|
||||||
}
|
}
|
||||||
|
totalMoney = paymentTotal.setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||||
}
|
}
|
||||||
Double applyMoney = invoiceFileCount > 0
|
unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue());
|
||||||
? BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()
|
|
||||||
: unionReimburse.getCondolenceMoney();
|
|
||||||
unionReimburse.setMoney(applyMoney);
|
|
||||||
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
|
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +713,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return Result.error("请至少维护一条支付信息");
|
return Result.error("请至少维护一条支付信息");
|
||||||
}
|
}
|
||||||
for (int i = 0; i < form.getPaymentDetails().size(); i++) {
|
for (int i = 0; i < form.getPaymentDetails().size(); i++) {
|
||||||
cn.hutool.json.JSONObject detail = form.getPaymentDetails().get(i);
|
JSONObject detail = form.getPaymentDetails().get(i);
|
||||||
String prefix = "第" + (i + 1) + "条支付信息";
|
String prefix = "第" + (i + 1) + "条支付信息";
|
||||||
if (detail == null) {
|
if (detail == null) {
|
||||||
return Result.error(prefix + "不能为空");
|
return Result.error(prefix + "不能为空");
|
||||||
@@ -718,6 +741,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
if (StrUtil.isBlank(detail.getStr("bankOfDeposit"))) {
|
if (StrUtil.isBlank(detail.getStr("bankOfDeposit"))) {
|
||||||
return Result.error(prefix + "请填写开户行");
|
return Result.error(prefix + "请填写开户行");
|
||||||
}
|
}
|
||||||
|
// 账户表和报销快照的文本字段长度一致,超长输入应明确拒绝而非丢失账户。
|
||||||
|
if (detail.getStr("payer").length() > 100 || detail.getStr("bankUserName").length() > 100
|
||||||
|
|| detail.getStr("bankOfDeposit").length() > 100) {
|
||||||
|
return Result.error(prefix + "付款人、户名及开户行不能超过100个字符");
|
||||||
|
}
|
||||||
String card = detail.getStr("bankCardNumber");
|
String card = detail.getStr("bankCardNumber");
|
||||||
if (card == null || !card.matches("[1-9]([0-9]{15}|[0-9]{18})")) {
|
if (card == null || !card.matches("[1-9]([0-9]{15}|[0-9]{18})")) {
|
||||||
return Result.error(prefix + "请填写正确的银行卡号");
|
return Result.error(prefix + "请填写正确的银行卡号");
|
||||||
@@ -727,6 +755,46 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付明细须先通过单行金额校验,再按分比较全部对公、对私金额与发票总额。
|
||||||
|
* @param form 当前申请,使用 paymentDetails 和 invoiceDetails 重新汇总,不信任前端 money 字段
|
||||||
|
* @return 超额或发票金额无效时返回 Result 错误,通过返回 null
|
||||||
|
*/
|
||||||
|
private Result validatePaymentAmountLimit(UnionReimburse form) {
|
||||||
|
List<UnionReimburseInvoiceDetail> invoices = form.getInvoiceDetails();
|
||||||
|
// 无发票时不设票面金额上限,支付明细仍须通过必填及正金额校验。
|
||||||
|
if (Lang.isEmpty(invoices)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BigDecimal invoiceTotal = BigDecimal.ZERO.setScale(2);
|
||||||
|
if (invoices != null) {
|
||||||
|
for (int i = 0; i < invoices.size(); i++) {
|
||||||
|
UnionReimburseInvoiceDetail detail = invoices.get(i);
|
||||||
|
if (detail == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Double amount = detail.getInvoiceAmount();
|
||||||
|
if (amount == null || !Double.isFinite(amount) || amount <= 0) {
|
||||||
|
return Result.error("第" + (i + 1) + "条发票明细请填写正确的发票金额");
|
||||||
|
}
|
||||||
|
BigDecimal invoiceMoney = BigDecimal.valueOf(amount).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
if (invoiceMoney.signum() <= 0) {
|
||||||
|
return Result.error("第" + (i + 1) + "条发票明细请填写正确的发票金额");
|
||||||
|
}
|
||||||
|
invoiceTotal = invoiceTotal.add(invoiceMoney);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BigDecimal paymentTotal = BigDecimal.ZERO.setScale(2);
|
||||||
|
for (JSONObject detail : form.getPaymentDetails()) {
|
||||||
|
paymentTotal = paymentTotal.add(new BigDecimal(detail.get("money").toString()));
|
||||||
|
}
|
||||||
|
if (paymentTotal.compareTo(invoiceTotal) > 0) {
|
||||||
|
return Result.error("支付金额合计不能超过发票总额(支付合计:" + paymentTotal.toPlainString()
|
||||||
|
+ "元,发票总额:" + invoiceTotal.toPlainString() + "元)");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交前补一层后端校验,防止前端绕过校验直接提交。
|
* 提交前补一层后端校验,防止前端绕过校验直接提交。
|
||||||
*/
|
*/
|
||||||
@@ -747,7 +815,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
// return Result.error("请填写开户行");
|
// return Result.error("请填写开户行");
|
||||||
// }
|
// }
|
||||||
if (!needInvoiceDetails(unionReimburse)) {
|
if (!needInvoiceDetails(unionReimburse)) {
|
||||||
return validateInvoiceDetails(unionReimburse, false);
|
return validateInvoiceDetails(unionReimburse);
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(unionReimburse.getActivityName())) {
|
if (StrUtil.isBlank(unionReimburse.getActivityName())) {
|
||||||
return Result.error("请填写活动名称");
|
return Result.error("请填写活动名称");
|
||||||
@@ -761,29 +829,29 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
if (Lang.isEmpty(unionReimburse.getFiles())) {
|
if (Lang.isEmpty(unionReimburse.getFiles())) {
|
||||||
return Result.error("请上传附件");
|
return Result.error("请上传附件");
|
||||||
}
|
}
|
||||||
return validateInvoiceDetails(unionReimburse, true);
|
return validateInvoiceDetails(unionReimburse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发票明细提交校验:活动类必须至少一条,慰问类允许为空但填写后必须完整。
|
* 发票列表允许为空;已有明细必须且只能包含一张可定位的发票文件。
|
||||||
*/
|
*/
|
||||||
private Result validateInvoiceDetails(UnionReimburse unionReimburse, boolean required) {
|
private Result validateInvoiceDetails(UnionReimburse unionReimburse) {
|
||||||
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
|
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
|
||||||
if (!required) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Result.error("请至少维护一条发票明细");
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < unionReimburse.getInvoiceDetails().size(); i++) {
|
for (int i = 0; i < unionReimburse.getInvoiceDetails().size(); i++) {
|
||||||
UnionReimburseInvoiceDetail detail = unionReimburse.getInvoiceDetails().get(i);
|
UnionReimburseInvoiceDetail detail = unionReimburse.getInvoiceDetails().get(i);
|
||||||
int rowIndex = i + 1;
|
int rowIndex = i + 1;
|
||||||
if (Lang.isEmpty(detail.getInvoiceFiles())) {
|
if (detail == null || Lang.isEmpty(detail.getInvoiceFiles())) {
|
||||||
return Result.error("第" + rowIndex + "条发票明细请上传发票文件");
|
return Result.error("第" + rowIndex + "条发票明细请上传发票文件");
|
||||||
}
|
}
|
||||||
|
if (detail.getInvoiceFiles().size() != 1 || StrUtil.isBlank(getInvoiceFileId(detail.getInvoiceFiles().get(0)))) {
|
||||||
|
return Result.error("第" + rowIndex + "条发票明细须上传一个有效发票文件");
|
||||||
|
}
|
||||||
if (StrUtil.isBlank(detail.getInvoiceNo())) {
|
if (StrUtil.isBlank(detail.getInvoiceNo())) {
|
||||||
return Result.error("第" + rowIndex + "条发票明细请填写发票号码");
|
return Result.error("第" + rowIndex + "条发票明细请填写发票号码");
|
||||||
}
|
}
|
||||||
if (detail.getInvoiceAmount() == null || detail.getInvoiceAmount() <= 0) {
|
if (detail.getInvoiceAmount() == null || !Double.isFinite(detail.getInvoiceAmount()) || detail.getInvoiceAmount() <= 0) {
|
||||||
return Result.error("第" + rowIndex + "条发票明细请填写正确的发票金额");
|
return Result.error("第" + rowIndex + "条发票明细请填写正确的发票金额");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(detail.getSellerName())) {
|
if (StrUtil.isBlank(detail.getSellerName())) {
|
||||||
@@ -796,6 +864,67 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param form 已通过完整性校验的申请,逐条使用附件ID读取真实文件并验真
|
||||||
|
* @return 全部通过返回 null;失败返回包含明细行号的 Result,不写入任何报销数据
|
||||||
|
*/
|
||||||
|
private Result validateInvoiceAuthenticity(UnionReimburse form) {
|
||||||
|
Set<String> invoiceNumbers = new LinkedHashSet<>();
|
||||||
|
for (int i = 0; i < form.getInvoiceDetails().size(); i++) {
|
||||||
|
UnionReimburseInvoiceDetail detail = form.getInvoiceDetails().get(i);
|
||||||
|
String prefix = "第" + (i + 1) + "条发票明细:";
|
||||||
|
if (!invoiceNumbers.add(detail.getInvoiceNo())) {
|
||||||
|
return Result.error(prefix + "发票号码重复,不能保存或提交");
|
||||||
|
}
|
||||||
|
Result verified = recognizeInvoice(getInvoiceFileId(detail.getInvoiceFiles().get(0)), true);
|
||||||
|
if (verified.getCode() != 0) {
|
||||||
|
return Result.error(prefix + verified.getMsg());
|
||||||
|
}
|
||||||
|
if (!(verified.getData() instanceof NutMap data) || !Boolean.TRUE.equals(data.get("verified"))) {
|
||||||
|
return Result.error(prefix + "未取得有效验真结果,请重试");
|
||||||
|
}
|
||||||
|
Double verifiedAmount = parseMoney(data.get("invoiceAmount"));
|
||||||
|
if (!Objects.equals(detail.getInvoiceNo(), data.getString("invoiceNo"))
|
||||||
|
|| verifiedAmount == null
|
||||||
|
|| BigDecimal.valueOf(detail.getInvoiceAmount()).setScale(2, RoundingMode.HALF_UP)
|
||||||
|
.compareTo(BigDecimal.valueOf(verifiedAmount).setScale(2, RoundingMode.HALF_UP)) != 0
|
||||||
|
|| !Objects.equals(detail.getSellerName(), data.getString("sellerName"))
|
||||||
|
|| !Objects.equals(detail.getItemName(), data.getString("itemName"))) {
|
||||||
|
return Result.error(prefix + "票号、金额、销售方或项目名称与验真结果不一致,请重新识别后保存");
|
||||||
|
}
|
||||||
|
// 日期和分项金额均采用服务端重读发票后的结果,不信任前端传入值。
|
||||||
|
detail.setInvoiceDate(data.getString("invoiceDate"));
|
||||||
|
detail.setAmountWithoutTax(parseMoney(data.get("amountWithoutTax")));
|
||||||
|
detail.setTaxAmount(parseMoney(data.get("taxAmount")));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容上传组件完整对象和历史下载地址;只提取ID,绝不请求前端传入的URL。 */
|
||||||
|
private String getInvoiceFileId(JSONObject file) {
|
||||||
|
if (file == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(file.getStr("id"))) {
|
||||||
|
return file.getStr("id").trim();
|
||||||
|
}
|
||||||
|
JSONObject response = file.getJSONObject("response");
|
||||||
|
Object data = response == null ? null : response.get("data");
|
||||||
|
if (data instanceof JSONObject object && StrUtil.isNotBlank(object.getStr("id"))) {
|
||||||
|
return object.getStr("id").trim();
|
||||||
|
}
|
||||||
|
String value = data instanceof String ? (String) data
|
||||||
|
: StrUtil.blankToDefault(file.getStr("downloadPath"), file.getStr("url"));
|
||||||
|
if (StrUtil.isBlank(value)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("[?&]id=([^&#]+)").matcher(value);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
return data instanceof String && value.matches("[a-zA-Z0-9_-]+") ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交和审核前统一校验预算是否已分配、余额是否充足。
|
* 提交和审核前统一校验预算是否已分配、余额是否充足。
|
||||||
*/
|
*/
|
||||||
@@ -1227,52 +1356,153 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
result.setv("invoiceCode", pickValue(infos, "发票代码", "代码", "Code"));
|
result.setv("invoiceCode", pickValue(infos, "发票代码", "代码", "Code"));
|
||||||
result.setv("invoiceDate", normalizeDate(pickValue(infos, "开票日期", "日期", "Date")));
|
result.setv("invoiceDate", normalizeDate(pickValue(infos, "开票日期", "日期", "Date")));
|
||||||
result.setv("invoiceCheckCode", pickValue(infos, "校验码", "校验码后6位", "CheckCode"));
|
result.setv("invoiceCheckCode", pickValue(infos, "校验码", "校验码后6位", "CheckCode"));
|
||||||
result.setv("invoiceAmount", parseMoney(pickValue(infos, "小写金额", "价税合计(小写)", "价税合计", "合计金额", "AmountWithTax")));
|
// 含税总额按字段优先级提取;“合计金额”通常是不含税金额,不能用于全电票核验。
|
||||||
|
result.setv("invoiceAmount", parseMoney(pickPreferredValue(infos,
|
||||||
|
"价税合计(小写)", "价税合计(小写)", "价税合计", "小写金额", "AmountWithTax")));
|
||||||
|
result.setv("amountWithoutTax", parseMoney(pickValue(infos, "合计金额", "金额合计", "不含税金额", "AmountWithoutTax")));
|
||||||
|
result.setv("taxAmount", parseMoney(pickPreferredValue(infos, "合计税额", "税额合计", "TaxAmount")));
|
||||||
result.setv("sellerName", pickValue(infos, "销售方名称", "销售方信息名称", "销售方", "SellerName"));
|
result.setv("sellerName", pickValue(infos, "销售方名称", "销售方信息名称", "销售方", "SellerName"));
|
||||||
result.setv("itemName", joinItemNames(items));
|
result.setv("itemName", joinItemNames(items));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动校验需要票面关键信息,缺字段时只提示人工核对,不阻塞用户继续录入。
|
* 从OCR票面构造新版核验参数:全电票使用含税金额,普通票使用校验码,专票使用不含税金额。
|
||||||
|
* @param result 服务端识别的票面数据,不接受前端手工填写的验真参数
|
||||||
|
* @return 腾讯云请求参数;信息不足时抛出异常,由验真入口返回失败
|
||||||
*/
|
*/
|
||||||
private String enrichByVerify(Credential credential, NutMap result) {
|
private NutMap buildVerifyRequest(NutMap result) {
|
||||||
String invoiceCode = result.getString("invoiceCode");
|
String invoiceCode = result.getString("invoiceCode");
|
||||||
String invoiceNo = result.getString("invoiceNo");
|
String invoiceNo = result.getString("invoiceNo");
|
||||||
String invoiceDate = result.getString("invoiceDate");
|
String invoiceDate = result.getString("invoiceDate");
|
||||||
String checkCode = lastDigits(result.getString("invoiceCheckCode"), 6);
|
String checkCode = lastDigits(result.getString("invoiceCheckCode"), 6);
|
||||||
if (StrUtil.isBlank(checkCode)) {
|
boolean electronic = invoiceNo != null && invoiceNo.matches("[0-9]{20}");
|
||||||
checkCode = moneyToVerifyValue(result.get("invoiceAmount"));
|
if (StrUtil.hasBlank(invoiceNo, invoiceDate) || (!electronic && StrUtil.isBlank(invoiceCode))) {
|
||||||
|
throw new IllegalArgumentException("发票号码、代码或开票日期识别不完整,请上传清晰发票后重试");
|
||||||
}
|
}
|
||||||
if (StrUtil.hasBlank(invoiceCode, invoiceNo, invoiceDate, checkCode)) {
|
NutMap request = NutMap.NEW().setv("InvoiceNo", invoiceNo).setv("InvoiceDate", invoiceDate);
|
||||||
return "发票识别成功,但自动校验信息不完整,请人工核对后提交";
|
if (!electronic) {
|
||||||
|
request.setv("InvoiceCode", invoiceCode);
|
||||||
}
|
}
|
||||||
|
String amount = moneyToVerifyValue(result.get(electronic ? "invoiceAmount" : "amountWithoutTax"));
|
||||||
|
if (electronic && StrUtil.isBlank(amount)
|
||||||
|
|| !electronic && checkCode.length() != 6 && StrUtil.isBlank(amount)) {
|
||||||
|
throw new IllegalArgumentException("发票校验码或核验金额识别不完整,请上传清晰发票后重试");
|
||||||
|
}
|
||||||
|
if (!electronic && checkCode.length() == 6) {
|
||||||
|
request.setv("CheckCode", checkCode);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(amount)) {
|
||||||
|
request.setv("Amount", amount);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 强制验真:缺字段、无结果、作废红冲、票面不符及接口异常均不允许按成功返回。 */
|
||||||
|
private String enrichByVerify(Credential credential, NutMap result) {
|
||||||
try {
|
try {
|
||||||
|
NutMap request = buildVerifyRequest(result);
|
||||||
OcrClient client = createOcrClient(credential);
|
OcrClient client = createOcrClient(credential);
|
||||||
VatInvoiceVerifyRequest request = new VatInvoiceVerifyRequest();
|
// 当前SDK没有新版模型,通过已有签名调用能力请求新版接口,避免升级整个SDK。
|
||||||
request.setInvoiceCode(invoiceCode);
|
JSONObject response = JSONUtil.parseObj(client.call("VatInvoiceVerifyNew", JSONUtil.toJsonStr(request)))
|
||||||
request.setInvoiceNo(invoiceNo);
|
.getJSONObject("Response");
|
||||||
request.setInvoiceDate(invoiceDate.replace("-", ""));
|
if (response == null || response.getJSONObject("Error") != null) {
|
||||||
request.setAdditional(checkCode);
|
String code = response == null ? "EmptyResponse" : response.getJSONObject("Error").getStr("Code");
|
||||||
VatInvoiceVerifyResponse response = client.VatInvoiceVerify(request);
|
return "发票验真未通过(" + code + "),请核对票据或稍后重试";
|
||||||
VatInvoice invoice = response.getInvoice();
|
|
||||||
if (invoice == null) {
|
|
||||||
return "发票识别成功,但自动校验未返回有效结果,请人工核对后提交";
|
|
||||||
}
|
}
|
||||||
result.setv("invoiceCode", invoice.getCode());
|
return applyVerifiedInvoice(result, response.getJSONObject("Invoice"));
|
||||||
result.setv("invoiceNo", invoice.getNumber());
|
} catch (IllegalArgumentException e) {
|
||||||
result.setv("invoiceDate", normalizeDate(invoice.getDate()));
|
return e.getMessage();
|
||||||
result.setv("invoiceCheckCode", invoice.getCheckCode());
|
} catch (TencentCloudSDKException e) {
|
||||||
result.setv("sellerName", invoice.getSellerName());
|
String code = StrUtil.blankToDefault(e.getErrorCode(), "UnknownError");
|
||||||
result.setv("invoiceAmount", parseMoney(invoice.getAmountWithTax()));
|
log.warn("腾讯云发票验真失败,invoiceNo:{},invoiceDate:{},amount:{},errorCode:{},requestId:{}",
|
||||||
result.setv("itemName", joinItemNames(invoice.getItems()));
|
result.getString("invoiceNo"), result.getString("invoiceDate"),
|
||||||
return null;
|
result.get("invoiceAmount"), code, e.getRequestId(), e);
|
||||||
|
if ("FailedOperation.InvoiceMismatch".equals(code)
|
||||||
|
|| "InvalidParameterValue.PriceOrVerificationParameterValueLimit".equals(code)) {
|
||||||
|
return "发票验真数据不一致,请核对票号、开票日期及价税合计金额后重新上传";
|
||||||
|
}
|
||||||
|
if ("FailedOperation.CountLimitError".equals(code)) {
|
||||||
|
return "该发票今日核验次数已达上限,请次日再试";
|
||||||
|
}
|
||||||
|
if ("ResourceNotFound.NotSupportCurrentInvoiceQuery".equals(code)) {
|
||||||
|
return "该发票今天开具,暂时无法核验,请稍后再试";
|
||||||
|
}
|
||||||
|
return "发票验真未完成(" + code + "),请核对票据或稍后重试";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("腾讯云发票验真失败,invoiceNo:{}", invoiceNo, e);
|
log.warn("腾讯云发票验真失败,invoiceNo:{}", result.getString("invoiceNo"), e);
|
||||||
return "发票识别成功,但自动校验失败,请人工核对后提交";
|
return "发票验真未完成,请稍后重试;验真通过后才能保存或提交";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 核验回包必须与上传票面的票号、金额、销售方一致,避免仅凭真实票号放行被改动的票面。 */
|
||||||
|
private String applyVerifiedInvoice(NutMap result, JSONObject verifiedInvoice) {
|
||||||
|
if (verifiedInvoice == null) {
|
||||||
|
return "发票验真未返回有效票据,不能保存或提交";
|
||||||
|
}
|
||||||
|
VatInvoice invoice = new com.google.gson.Gson().fromJson(verifiedInvoice.toString(), VatInvoice.class);
|
||||||
|
if (!"N".equalsIgnoreCase(invoice.getIsAbandoned()) || Boolean.TRUE.equals(verifiedInvoice.getBool("RedLetterInvoiceMark"))) {
|
||||||
|
return "发票已作废、红冲或状态无法确认,不能保存或提交";
|
||||||
|
}
|
||||||
|
Double amount = parseMoney(invoice.getAmountWithTax());
|
||||||
|
Double ocrAmount = parseMoney(result.get("invoiceAmount"));
|
||||||
|
// 分项比对只记录必要字段,区分票面识别差异;金额必须保持一致,不能用验真值覆盖后放行。
|
||||||
|
if (!Objects.equals(result.getString("invoiceNo"), StrUtil.trim(invoice.getNumber()))) {
|
||||||
|
log.warn("发票票号比对失败,ocrInvoiceNo:{},verifiedInvoiceNo:{}",
|
||||||
|
result.getString("invoiceNo"), StrUtil.trim(invoice.getNumber()));
|
||||||
|
return "发票号码与验真结果不一致,请核对上传的发票";
|
||||||
|
}
|
||||||
|
if (amount == null || amount <= 0 || ocrAmount == null
|
||||||
|
|| BigDecimal.valueOf(amount).compareTo(BigDecimal.valueOf(ocrAmount)) != 0) {
|
||||||
|
log.warn("发票价税合计比对失败,invoiceNo:{},ocrAmount:{},verifiedAmount:{}",
|
||||||
|
result.getString("invoiceNo"), ocrAmount, amount);
|
||||||
|
return "发票价税合计与验真结果不一致,请核对含税总金额后重新识别";
|
||||||
|
}
|
||||||
|
if (!Objects.equals(normalizeInvoiceSellerName(result.getString("sellerName")),
|
||||||
|
normalizeInvoiceSellerName(invoice.getSellerName()))) {
|
||||||
|
log.warn("发票销售方比对失败,invoiceNo:{},ocrSellerName:{},verifiedSellerName:{}",
|
||||||
|
result.getString("invoiceNo"), result.getString("sellerName"), invoice.getSellerName());
|
||||||
|
return "发票销售方名称与验真结果不一致,请核对上传的发票";
|
||||||
|
}
|
||||||
|
String itemName = joinItemNames(invoice.getItems());
|
||||||
|
if (StrUtil.hasBlank(invoice.getSellerName(), itemName)) {
|
||||||
|
return "发票验真结果缺少销售方或项目名称,不能保存或提交";
|
||||||
|
}
|
||||||
|
result.setv("invoiceCode", invoice.getCode());
|
||||||
|
result.setv("invoiceNo", StrUtil.trim(invoice.getNumber()));
|
||||||
|
// 个别票种核验回包不提供日期,保留已用于本次验真请求的 OCR 开票日期。
|
||||||
|
if (StrUtil.isNotBlank(invoice.getDate())) {
|
||||||
|
result.setv("invoiceDate", normalizeDate(invoice.getDate()));
|
||||||
|
}
|
||||||
|
result.setv("invoiceCheckCode", invoice.getCheckCode());
|
||||||
|
result.setv("sellerName", StrUtil.trim(invoice.getSellerName()));
|
||||||
|
result.setv("invoiceAmount", amount);
|
||||||
|
// 核验结果优先;部分票种未返回分项金额时保留 OCR 提取值,不用差额推算税额。
|
||||||
|
Double amountWithoutTax = parseMoney(invoice.getAmountWithoutTax());
|
||||||
|
Double taxAmount = parseMoney(invoice.getTaxAmount());
|
||||||
|
Double ocrAmountWithoutTax = parseMoney(result.get("amountWithoutTax"));
|
||||||
|
Double ocrTaxAmount = parseMoney(result.get("taxAmount"));
|
||||||
|
// 腾讯云对部分票种用 0.00 占位;票面 OCR 有正数时保留票面值。
|
||||||
|
if (amountWithoutTax == null || amountWithoutTax == 0D && ocrAmountWithoutTax != null && ocrAmountWithoutTax > 0) {
|
||||||
|
amountWithoutTax = ocrAmountWithoutTax;
|
||||||
|
}
|
||||||
|
if (taxAmount == null || taxAmount == 0D && ocrTaxAmount != null && ocrTaxAmount > 0) {
|
||||||
|
taxAmount = ocrTaxAmount;
|
||||||
|
}
|
||||||
|
result.setv("amountWithoutTax", amountWithoutTax);
|
||||||
|
result.setv("taxAmount", taxAmount);
|
||||||
|
result.setv("itemName", itemName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销售方比对仅统一中英文括号及首尾空白,保留名称文字与内部空白,避免将不同主体误判为一致。
|
||||||
|
* 空值保持为空值;回填仍使用验真返回的原名称,不使用此比较用字符串。
|
||||||
|
*/
|
||||||
|
private String normalizeInvoiceSellerName(String name) {
|
||||||
|
return name == null ? null : StrUtil.trim(name).replace('(', '(').replace(')', ')');
|
||||||
|
}
|
||||||
|
|
||||||
private String pickValue(TextVatInvoice[] infos, String... aliases) {
|
private String pickValue(TextVatInvoice[] infos, String... aliases) {
|
||||||
if (infos == null || aliases == null) {
|
if (infos == null || aliases == null) {
|
||||||
return "";
|
return "";
|
||||||
@@ -1292,6 +1522,17 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按候选字段优先级取首个非空票面值,避免 OCR 返回顺序决定金额含义。 */
|
||||||
|
private String pickPreferredValue(TextVatInvoice[] infos, String... aliases) {
|
||||||
|
for (String alias : aliases) {
|
||||||
|
String value = pickValue(infos, alias);
|
||||||
|
if (StrUtil.isNotBlank(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeFieldName(String name) {
|
private String normalizeFieldName(String name) {
|
||||||
return StrUtil.blankToDefault(name, "")
|
return StrUtil.blankToDefault(name, "")
|
||||||
.replace(":", "")
|
.replace(":", "")
|
||||||
@@ -1353,6 +1594,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
return BigDecimal.valueOf(money).setScale(2, RoundingMode.HALF_UP).toPlainString();
|
return BigDecimal.valueOf(money).setScale(2, RoundingMode.HALF_UP).toPlainString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void saveInvoiceDetails(String reimburseId, List<UnionReimburseInvoiceDetail> invoiceDetails) {
|
private void saveInvoiceDetails(String reimburseId, List<UnionReimburseInvoiceDetail> invoiceDetails) {
|
||||||
this.dao().clear(UnionReimburseInvoiceDetail.class, Cnd.where("reimburseId", "=", reimburseId));
|
this.dao().clear(UnionReimburseInvoiceDetail.class, Cnd.where("reimburseId", "=", reimburseId));
|
||||||
if (Lang.isEmpty(invoiceDetails)) {
|
if (Lang.isEmpty(invoiceDetails)) {
|
||||||
@@ -1366,7 +1608,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 非慰问类报销必须维护发票明细,慰问类发票明细为可选。
|
* 区分活动类与慰问类的基本信息校验;所有类型的发票均为选填。
|
||||||
*/
|
*/
|
||||||
private boolean needInvoiceDetails(UnionReimburse unionReimburse) {
|
private boolean needInvoiceDetails(UnionReimburse unionReimburse) {
|
||||||
return unionReimburse != null && !"UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject());
|
return unionReimburse != null && !"UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject());
|
||||||
|
|||||||
+2
@@ -17,6 +17,8 @@ public class AidFundPageForm extends PageForm {
|
|||||||
private String unitId;
|
private String unitId;
|
||||||
//是否当年未缴费
|
//是否当年未缴费
|
||||||
private Boolean notPayedCurrentYear;
|
private Boolean notPayedCurrentYear;
|
||||||
|
//是否筛选人员类型与基金会员类型不一致的基金会员
|
||||||
|
private Boolean personTypeChanged;
|
||||||
|
|
||||||
//基金会员变更类型
|
//基金会员变更类型
|
||||||
private String changeType;
|
private String changeType;
|
||||||
|
|||||||
+2
@@ -88,6 +88,8 @@ public class AidFundMemberPayServiceImpl extends BaseServiceImpl<AidFundMemberPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
|
||||||
|
// 2026年10月3日 19:04:27;操作人:wxg:默认优先展示最新年度,同年度内保留单位编码倒序;主动指定排序时沿用原规则。
|
||||||
|
cnd.desc("pay.year");
|
||||||
cnd.desc("us.unitCode");
|
cnd.desc("us.unitCode");
|
||||||
} else {
|
} else {
|
||||||
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
|
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
|
||||||
|
|||||||
+87
-8
@@ -4,9 +4,11 @@ import cn.hutool.core.date.DateUtil;
|
|||||||
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.constant.RoleConstant;
|
import com.budwk.app.base.constant.RoleConstant;
|
||||||
|
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.base.utils.PageUtil;
|
import com.budwk.app.base.utils.PageUtil;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
|
import com.budwk.app.sys.models.Sys_dict;
|
||||||
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;
|
||||||
@@ -22,6 +24,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.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
@@ -29,6 +32,7 @@ import org.nutz.lang.Strings;
|
|||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -139,6 +143,7 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Sql getManageSql(AidFundPageForm pageForm) {
|
public Sql getManageSql(AidFundPageForm pageForm) {
|
||||||
|
// 返回当前人员类型,供人员类型变化名单与基金会员类型对照核实。
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -149,6 +154,7 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
|
|||||||
unitName,
|
unitName,
|
||||||
unionName,
|
unionName,
|
||||||
aidFundMemberUserType,
|
aidFundMemberUserType,
|
||||||
|
personType,
|
||||||
aidFundMemberJoinTime
|
aidFundMemberJoinTime
|
||||||
FROM
|
FROM
|
||||||
vw_user
|
vw_user
|
||||||
@@ -167,6 +173,17 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
|
|||||||
if (pageForm.getNotPayedCurrentYear()) {
|
if (pageForm.getNotPayedCurrentYear()) {
|
||||||
cnd.and("id", "in", Sqls.create("SELECT userid FROM `aid_fund_member_pay` where `year` = YEAR(CURDATE()) and isPayed = 0"));
|
cnd.and("id", "in", Sqls.create("SELECT userid FROM `aid_fund_member_pay` where `year` = YEAR(CURDATE()) and isPayed = 0"));
|
||||||
}
|
}
|
||||||
|
if (ObjectUtil.equals(pageForm.getPersonTypeChanged(), true)) {
|
||||||
|
// 按工号比对当前用户表人员类型与基金会员类型,筛出需要人工核实变更的会员。
|
||||||
|
cnd.and(new Static("""
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_user su
|
||||||
|
WHERE su.loginname = vw_user.loginname
|
||||||
|
AND IFNULL(su.personType, '') <> IFNULL(vw_user.aidFundMemberUserType, '')
|
||||||
|
)
|
||||||
|
"""));
|
||||||
|
}
|
||||||
cnd.andEX(Sys_user::getAidFundMemberUserType, "=", pageForm.getAidFundMemberUserType());
|
cnd.andEX(Sys_user::getAidFundMemberUserType, "=", pageForm.getAidFundMemberUserType());
|
||||||
cnd.andEX(View_user::getUnionId, "=", pageForm.getUnionId());
|
cnd.andEX(View_user::getUnionId, "=", pageForm.getUnionId());
|
||||||
cnd.andEX(View_user::getUnitId, "=", pageForm.getUnitId());
|
cnd.andEX(View_user::getUnitId, "=", pageForm.getUnitId());
|
||||||
@@ -243,13 +260,10 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void doBatchChangeTypeByAdmin(String[] ids, String changeType) {
|
public void doBatchChangeTypeByAdmin(String[] ids, String changeType) {
|
||||||
List<Sys_user> sysUserList = dao().query(Sys_user.class, Cnd.where("id", "in", ids));
|
List<Sys_user> sysUserList = lockChangeUsers(ids);
|
||||||
|
validateRepeatedChange(sysUserList, changeType);
|
||||||
List<AidFundMemberChangeRecord> recordArrayList = new ArrayList<>();
|
List<AidFundMemberChangeRecord> recordArrayList = new ArrayList<>();
|
||||||
for (String id : ids) {
|
for (Sys_user user : sysUserList) {
|
||||||
Sys_user user = sysUserList.stream().filter(sysUser -> sysUser.getId().equals(id)).findFirst().orElse(null);
|
|
||||||
if (user == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
recordArrayList.add(buildAdminChangeRecord(user, changeType));
|
recordArrayList.add(buildAdminChangeRecord(user, changeType));
|
||||||
}
|
}
|
||||||
updateUserChangeType(ids, changeType);
|
updateUserChangeType(ids, changeType);
|
||||||
@@ -267,14 +281,78 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void doEditChangeTypeByAdmin(String userId, String changeType) {
|
public void doEditChangeTypeByAdmin(String userId, String changeType) {
|
||||||
Sys_user user = dao().fetch(Sys_user.class, userId);
|
List<Sys_user> users = lockChangeUsers(new String[]{userId});
|
||||||
if (user == null) {
|
if (users.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
validateRepeatedChange(users, changeType);
|
||||||
|
Sys_user user = users.get(0);
|
||||||
dao().insert(buildAdminChangeRecord(user, changeType));
|
dao().insert(buildAdminChangeRecord(user, changeType));
|
||||||
updateUserChangeType(new String[]{userId}, changeType);
|
updateUserChangeType(new String[]{userId}, changeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在调用方 READ_COMMITTED 事务中按固定顺序锁定用户,串行处理同一人的并发变更。
|
||||||
|
* ids 为单人或批量操作的用户 ID;返回去重后的当前用户,锁持续到事务结束。
|
||||||
|
*/
|
||||||
|
private List<Sys_user> lockChangeUsers(String[] ids) {
|
||||||
|
List<Sys_user> users = new ArrayList<>();
|
||||||
|
for (String id : Arrays.stream(ids).filter(StrUtil::isNotBlank).distinct().sorted().toList()) {
|
||||||
|
Sql sql = Sqls.create("SELECT * FROM sys_user WHERE id=@id FOR UPDATE");
|
||||||
|
sql.setParam("id", id);
|
||||||
|
sql.setEntity(dao().getEntity(Sys_user.class));
|
||||||
|
sql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(sql);
|
||||||
|
users.addAll(sql.getList(Sys_user.class));
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 整批写入前检查最近已生效变更;有重复时一次性提示全部人员,不更新档案或新增记录。
|
||||||
|
* users 为已加行锁的用户,changeType 为变更类型字典编码;正常返回表示允许提交。
|
||||||
|
* 只比较连续的相同变更,并核对当前状态;退会后重新入会允许再次合法退会。
|
||||||
|
*/
|
||||||
|
private void validateRepeatedChange(List<Sys_user> users, String changeType) {
|
||||||
|
List<String> repeated = new ArrayList<>();
|
||||||
|
Sys_dict dict = dao().fetch(Sys_dict.class, Cnd.where("code", "=", changeType));
|
||||||
|
String typeName = dict == null ? changeType : dict.getName();
|
||||||
|
for (Sys_user user : users) {
|
||||||
|
// 管理员记录和无流程的历史记录直接生效;wf 申请仅纳入已完成流程,排除处理中、撤回等记录。
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT info.* FROM aid_fund_member_change_record info
|
||||||
|
WHERE info.userId=@userId AND IFNULL(info.delFlag, 0)=0
|
||||||
|
AND (info.adminOperate=1
|
||||||
|
OR NOT EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo=info.id)
|
||||||
|
OR EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo=info.id AND ins.state=20))
|
||||||
|
ORDER BY info.applyTime DESC, info.createdAt DESC, info.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""");
|
||||||
|
sql.setParam("userId", user.getId());
|
||||||
|
sql.setEntity(dao().getEntity(AidFundMemberChangeRecord.class));
|
||||||
|
sql.setCallback(Sqls.callback.entities());
|
||||||
|
dao().execute(sql);
|
||||||
|
List<AidFundMemberChangeRecord> records = sql.getList(AidFundMemberChangeRecord.class);
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AidFundMemberChangeRecord record = records.get(0);
|
||||||
|
boolean retirement = ObjectUtil.equals(changeType, "AIDFUND_MEMBER_CHANGE_TYPE_EIGTH");
|
||||||
|
boolean targetState = retirement ? ObjectUtil.equals(user.getAidFundMemberUserType(), "离退休人员")
|
||||||
|
: ObjectUtil.equals(user.getAidFundMember(), AidFundMemberMode.NONE.getCode());
|
||||||
|
// 入会时间晚于上次退会申请时,属于新的会员周期,不受旧退会记录限制。
|
||||||
|
boolean rejoined = !retirement && user.getAidFundMemberJoinTime() != null && record.getApplyTime() != null
|
||||||
|
&& user.getAidFundMemberJoinTime().after(record.getApplyTime());
|
||||||
|
if (ObjectUtil.equals(record.getChangeType(), changeType) && targetState && !rejoined) {
|
||||||
|
String time = record.getApplyTime() == null ? "未知时间" : DateUtil.format(record.getApplyTime(), "yyyy-MM-dd HH:mm:ss");
|
||||||
|
repeated.add(user.getUsername() + "(" + user.getLoginname() + ")已于" + time + "执行“" + typeName + "”");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!repeated.isEmpty()) {
|
||||||
|
throw new BaseException(String.join(";", repeated) + ",请勿重复操作。本次全部未保存,请调整名单后重新提交。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理端变更落记录时,需要明确标记记录来源,避免被“我的申请”误识别为本人发起。
|
* 管理端变更落记录时,需要明确标记记录来源,避免被“我的申请”误识别为本人发起。
|
||||||
*
|
*
|
||||||
@@ -311,6 +389,7 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
|
|||||||
* @param ids 需要更新的用户ID数组
|
* @param ids 需要更新的用户ID数组
|
||||||
* @param changeType 变更类型字典值
|
* @param changeType 变更类型字典值
|
||||||
*/
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void updateUserChangeType(String[] ids, String changeType) {
|
private void updateUserChangeType(String[] ids, String changeType) {
|
||||||
Chain add;
|
Chain add;
|
||||||
if (ObjectUtil.equals(changeType, "AIDFUND_MEMBER_CHANGE_TYPE_EIGTH")) {
|
if (ObjectUtil.equals(changeType, "AIDFUND_MEMBER_CHANGE_TYPE_EIGTH")) {
|
||||||
|
|||||||
+13
-12
@@ -99,9 +99,8 @@ public class MedicalApplyServiceImpl extends BaseServiceImpl<MedicalApply> imple
|
|||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
map.put("bxyMoney$", bxyMoney$);
|
map.put("bxyMoney$", bxyMoney$);
|
||||||
|
|
||||||
map.put("totalMoney", hospitalSumMoney$
|
// 合计金额只统计个人实际承担的费用,住院总额和已报销金额不重复计入。
|
||||||
.add(reimbursementMoney$)
|
map.put("totalMoney", singleBearMoney$
|
||||||
.add(singleBearMoney$)
|
|
||||||
.add(personExpenseMoney$)
|
.add(personExpenseMoney$)
|
||||||
.add(outpatientServiceMoney$)
|
.add(outpatientServiceMoney$)
|
||||||
.add(bxyMoney$));
|
.add(bxyMoney$));
|
||||||
@@ -237,10 +236,10 @@ public class MedicalApplyServiceImpl extends BaseServiceImpl<MedicalApply> imple
|
|||||||
boolean fullFifYears = DateUtil.thisYear() - Integer.parseInt(user.getAidFundDeductTime()) >= 15;
|
boolean fullFifYears = DateUtil.thisYear() - Integer.parseInt(user.getAidFundDeductTime()) >= 15;
|
||||||
|
|
||||||
|
|
||||||
// 连续缴满15年 或者是 连续缴满10年
|
// 年限加成仅适用于首次申请;历史已完成申请记录已排除本次申请。
|
||||||
if (fullFifYears) {
|
if (applyRecords.isEmpty() && fullFifYears) {
|
||||||
ratio += medicalSetting.getFifteenYearNeverAddSubsidyRatio();
|
ratio += medicalSetting.getFifteenYearNeverAddSubsidyRatio();
|
||||||
} else if (fullTenYears) {
|
} else if (applyRecords.isEmpty() && fullTenYears) {
|
||||||
ratio += medicalSetting.getTenYearNeverAddSubsidyRatio();
|
ratio += medicalSetting.getTenYearNeverAddSubsidyRatio();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,15 +260,17 @@ public class MedicalApplyServiceImpl extends BaseServiceImpl<MedicalApply> imple
|
|||||||
//如果是重大疾病
|
//如果是重大疾病
|
||||||
allMedicalMoney = personExpenseMoney.add(singleBearMoney).add(outpatientServiceMoney).add(bxyMoney);
|
allMedicalMoney = personExpenseMoney.add(singleBearMoney).add(outpatientServiceMoney).add(bxyMoney);
|
||||||
} else {
|
} else {
|
||||||
//个人承担费用
|
// 普通疾病以每次住院的个人承担金额是否达到一万元为准,达标记录的自费金额一并计入。
|
||||||
singleBearMoney = costs.stream()
|
List<MedicalApplyCost> eligibleCosts = costs.stream()
|
||||||
|
.filter(cost -> cost.getSingleBearMoney() != null
|
||||||
|
&& cost.getSingleBearMoney().compareTo(BigDecimal.valueOf(10000)) >= 0)
|
||||||
|
.toList();
|
||||||
|
singleBearMoney = eligibleCosts.stream()
|
||||||
.map(MedicalApplyCost::getSingleBearMoney)
|
.map(MedicalApplyCost::getSingleBearMoney)
|
||||||
.filter(amount -> amount.compareTo(BigDecimal.valueOf(10000)) >= 0) // >= 10000
|
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
//自费
|
personExpenseMoney = eligibleCosts.stream()
|
||||||
personExpenseMoney = costs.stream()
|
|
||||||
.map(MedicalApplyCost::getPersonExpenseMoney)
|
.map(MedicalApplyCost::getPersonExpenseMoney)
|
||||||
.filter(amount -> amount.compareTo(BigDecimal.valueOf(10000)) >= 0) // >= 10000
|
.filter(Objects::nonNull)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
allMedicalMoney = personExpenseMoney.add(singleBearMoney).add(outpatientServiceMoney).add(bxyMoney);
|
allMedicalMoney = personExpenseMoney.add(singleBearMoney).add(outpatientServiceMoney).add(bxyMoney);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -18,10 +18,21 @@ public enum MemberChangeType {
|
|||||||
|
|
||||||
//变更类型
|
//变更类型
|
||||||
NEW("NEW", "新入职"),
|
NEW("NEW", "新入职"),
|
||||||
|
|
||||||
|
BASIC_CHANGE("BASIC_CHANGE", "基本信息异动"),
|
||||||
|
UNIT_CHANGE("UNIT_CHANGE", "单位异动"),
|
||||||
|
UNION_CHANGE("UNION_CHANGE", "工会关系异动"),
|
||||||
|
|
||||||
|
ONLINE("ONLINE", "在岗"),
|
||||||
|
OFFLINE("OFFLINE", "不在岗"),
|
||||||
|
|
||||||
RESTORE("RESTORE", "入会"),
|
RESTORE("RESTORE", "入会"),
|
||||||
WITHDRAWAL("WITHDRAWAL", "退会"),
|
WITHDRAWAL("WITHDRAWAL", "退会"),
|
||||||
|
|
||||||
ONLINE("ONLINE", "在岗"),
|
EXTEND("EXTEND", "延聘"),
|
||||||
|
REHIRE_SCH("REHIRE_SCH", "学校返聘"),
|
||||||
|
REHIRE_DEPT("REHIRE_DEPT", "部门返聘"),
|
||||||
|
|
||||||
FLEX("FLEX", "柔性"),
|
FLEX("FLEX", "柔性"),
|
||||||
SECONDED("SECONDED", "借调挂职"),
|
SECONDED("SECONDED", "借调挂职"),
|
||||||
REMOTE_POST("REMOTE_POST", "异地任职"),
|
REMOTE_POST("REMOTE_POST", "异地任职"),
|
||||||
@@ -40,18 +51,7 @@ public enum MemberChangeType {
|
|||||||
STUDY_DOMESTIC("STUDY_DOMESTIC", "国内进修"),
|
STUDY_DOMESTIC("STUDY_DOMESTIC", "国内进修"),
|
||||||
STUDY_ABROAD("STUDY_ABROAD", "境外留学"),
|
STUDY_ABROAD("STUDY_ABROAD", "境外留学"),
|
||||||
OFF_JOB_START("OFF_JOB_START", "离岗创业"),
|
OFF_JOB_START("OFF_JOB_START", "离岗创业"),
|
||||||
INNER_RETIRE("INNER_RETIRE", "内部退养"),
|
INNER_RETIRE("INNER_RETIRE", "内部退养");
|
||||||
|
|
||||||
EXTEND("EXTEND", "延聘"),
|
|
||||||
REHIRE_SCH("REHIRE_SCH", "学校返聘"),
|
|
||||||
REHIRE_DEPT("REHIRE_DEPT", "部门返聘"),
|
|
||||||
|
|
||||||
OFFLINE("OFFLINE", "不在岗"),
|
|
||||||
|
|
||||||
UNIT_CHANGE("UNIT_CHANGE", "单位异动"),
|
|
||||||
UNION_CHANGE("UNION_CHANGE", "工会关系异动"),
|
|
||||||
BASIC_CHANGE("BASIC_CHANGE", "基本信息异动");
|
|
||||||
|
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String changeTypeName;
|
private final String changeTypeName;
|
||||||
|
|||||||
+56
@@ -262,6 +262,15 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
|||||||
allowChangeFieldNames.add("threeUnitId");
|
allowChangeFieldNames.add("threeUnitId");
|
||||||
dictMap.put("threeUnitId", "三级单位");
|
dictMap.put("threeUnitId", "三级单位");
|
||||||
}
|
}
|
||||||
|
// 日期字段由具体变更表单明确传入时才参与比较,避免旧变更记录被误判为清空日期。
|
||||||
|
if (record.getRetireDate() != null) {
|
||||||
|
allowChangeFieldNames.add("retireDate");
|
||||||
|
dictMap.put("retireDate", "退休日期");
|
||||||
|
}
|
||||||
|
if (record.getWelfareStopDate() != null) {
|
||||||
|
allowChangeFieldNames.add("welfareStopDate");
|
||||||
|
dictMap.put("welfareStopDate", "福利享受截止时间");
|
||||||
|
}
|
||||||
|
|
||||||
// 新数据
|
// 新数据
|
||||||
NutMap newMap = Lang.obj2nutmap(record);
|
NutMap newMap = Lang.obj2nutmap(record);
|
||||||
@@ -305,14 +314,18 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void compareChangeInfoAndUpdateMember(String recordId) {
|
public void compareChangeInfoAndUpdateMember(String recordId) {
|
||||||
MemberChangeRecord record = dao().fetch(MemberChangeRecord.class, recordId);
|
MemberChangeRecord record = dao().fetch(MemberChangeRecord.class, recordId);
|
||||||
compareChangeInfoAndUpdateMember(record);
|
compareChangeInfoAndUpdateMember(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void compareChangeInfoAndUpdateMember(MemberChangeRecord record) {
|
public void compareChangeInfoAndUpdateMember(MemberChangeRecord record) {
|
||||||
|
applyRetiredPersonType(record);
|
||||||
normalizeArrivalAtSchoolDate(record);
|
normalizeArrivalAtSchoolDate(record);
|
||||||
|
normalizeWelfareStopDate(record);
|
||||||
List<NutMap> changeList = getChangeInfos(record);
|
List<NutMap> changeList = getChangeInfos(record);
|
||||||
|
|
||||||
// 如果有工会关系人员,这个user用来存储数据
|
// 如果有工会关系人员,这个user用来存储数据
|
||||||
@@ -382,6 +395,10 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
|||||||
|
|
||||||
// 到此,表明审核通过,需要存储历史数据,并修改用户数据
|
// 到此,表明审核通过,需要存储历史数据,并修改用户数据
|
||||||
BeanUtil.copyProperties(record, info);
|
BeanUtil.copyProperties(record, info);
|
||||||
|
// 变更记录使用字符串接收页面日期,人员表使用 Date,保存前显式转换避免类型转换差异。
|
||||||
|
if (StrUtil.isNotBlank(record.getWelfareStopDate())) {
|
||||||
|
info.setWelfareStopDate(DateUtil.parseDate(record.getWelfareStopDate()));
|
||||||
|
}
|
||||||
info.setId(userId);
|
info.setId(userId);
|
||||||
|
|
||||||
// 如果有单位变更,则需要记录单位关系
|
// 如果有单位变更,则需要记录单位关系
|
||||||
@@ -518,7 +535,9 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validateChangeAndSetBasicData(MemberChangeRecord record, MemberChangeOrigin origin) {
|
public void validateChangeAndSetBasicData(MemberChangeRecord record, MemberChangeOrigin origin) {
|
||||||
|
applyRetiredPersonType(record);
|
||||||
normalizeArrivalAtSchoolDate(record);
|
normalizeArrivalAtSchoolDate(record);
|
||||||
|
normalizeWelfareStopDate(record);
|
||||||
// 校验有没有发生变更
|
// 校验有没有发生变更
|
||||||
List<NutMap> changeInfos = getChangeInfos(record);
|
List<NutMap> changeInfos = getChangeInfos(record);
|
||||||
if (Lang.isEmpty(changeInfos)) {
|
if (Lang.isEmpty(changeInfos)) {
|
||||||
@@ -536,6 +555,43 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
|
|||||||
record.setApplyDateTime(DateUtil.date());
|
record.setApplyDateTime(DateUtil.date());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在职状态调整为退休时,按人员类型字典名称自动设置“离退休人员”编码。
|
||||||
|
* 字典中不存在该名称时保留原人员类型,不阻断其他变更。
|
||||||
|
*
|
||||||
|
* @param record 人员变更内容,userState 为“退休”时才执行自动设置
|
||||||
|
*/
|
||||||
|
private void applyRetiredPersonType(MemberChangeRecord record) {
|
||||||
|
if (record == null || !StrUtil.equals("退休", record.getUserState())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Sys_dict> personTypeOptions = sysDictService.getSubListByCode("USER_PERSON_TYPE");
|
||||||
|
if (Lang.isEmpty(personTypeOptions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
personTypeOptions.stream()
|
||||||
|
.filter(option -> StrUtil.equals("离退休人员", option.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(option -> record.setPersonType(option.getCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验并规范福利享受截止时间,统一使用 yyyy-MM-dd 参与比较及保存。
|
||||||
|
*
|
||||||
|
* @param record 人员变更内容,空日期保持为空
|
||||||
|
*/
|
||||||
|
private void normalizeWelfareStopDate(MemberChangeRecord record) {
|
||||||
|
if (record == null || StrUtil.isBlank(record.getWelfareStopDate())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Date parsedDate = parseArrivalAtSchoolDate(StrUtil.trim(record.getWelfareStopDate()));
|
||||||
|
record.setWelfareStopDate(DateUtil.format(parsedDate, "yyyy-MM-dd"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BaseException("福利享受截止时间格式不正确,请使用yyyy-MM-dd格式");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一规范变更记录中的到校日期,避免 Date 对象复制后以英文字符串形式参与比较或直接落库。
|
* 统一规范变更记录中的到校日期,避免 Date 对象复制后以英文字符串形式参与比较或直接落库。
|
||||||
|
|||||||
+17
@@ -15,6 +15,8 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待确认人员记录页面及查询接口。
|
* 待确认人员记录页面及查询接口。
|
||||||
*/
|
*/
|
||||||
@@ -49,6 +51,21 @@ public class PendingConfirmUserController {
|
|||||||
return Result.success(pendingConfirmUserService.pageData(pageForm));
|
return Result.success(pendingConfirmUserService.pageData(pageForm));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按当前列表查询条件导出待确认或已确认人员。
|
||||||
|
*
|
||||||
|
* @param pageForm 列表查询条件,包括姓名/工号、工会、单位、人员类型、在职状态、确认状态及排序
|
||||||
|
* @param response HTTP 文件下载响应,返回内容为 XSSF 格式的 Excel 文件
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@SLog(tag = "待确认人员记录", msg = "按条件导出待确认人员记录")
|
||||||
|
@ApiOperation("按条件导出待确认人员记录")
|
||||||
|
@SaCheckPermission("staff.sourcechange.pending")
|
||||||
|
public void exportExcel(PendingConfirmUserPageForm pageForm, HttpServletResponse response) {
|
||||||
|
pendingConfirmUserService.exportExcel(pageForm, response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直接提交待确认人员变更,不创建会员变更申请及审批流程。
|
* 直接提交待确认人员变更,不创建会员变更申请及审批流程。
|
||||||
*
|
*
|
||||||
|
|||||||
+32
@@ -2,6 +2,8 @@ package com.budwk.app.zhgh.staffmanage.sourcechange.param;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待确认人员批量变更参数。
|
* 待确认人员批量变更参数。
|
||||||
*/
|
*/
|
||||||
@@ -17,4 +19,34 @@ public class PendingConfirmUserBatchChangeParam {
|
|||||||
* 批量设置的人员类型。
|
* 批量设置的人员类型。
|
||||||
*/
|
*/
|
||||||
private String personType;
|
private String personType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置的在职状态,空值不参与批量变更。
|
||||||
|
*/
|
||||||
|
private String userState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置的退休时间;目标在职状态为退休或人员类型为离退休人员时必填,并统一应用于勾选人员。
|
||||||
|
*/
|
||||||
|
private Date retireDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置的福利享受截止时间,仅在目标在职状态为退休且传入日期时参与变更。
|
||||||
|
*/
|
||||||
|
private Date welfareStopDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置的人员属性,空值不参与批量变更。
|
||||||
|
*/
|
||||||
|
private String userAttribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置的会员状态,null不参与批量变更,false表示设置为非会员。
|
||||||
|
*/
|
||||||
|
private Boolean member;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置的福利会员状态,null不参与批量变更,false表示设置为非福利会员。
|
||||||
|
*/
|
||||||
|
private Boolean welfareMember;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -4,6 +4,8 @@ import com.budwk.app.base.param.PageForm;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待确认人员记录分页查询参数。
|
* 待确认人员记录分页查询参数。
|
||||||
*/
|
*/
|
||||||
@@ -20,6 +22,9 @@ public class PendingConfirmUserPageForm extends PageForm {
|
|||||||
/** 人员类型。 */
|
/** 人员类型。 */
|
||||||
private String personType;
|
private String personType;
|
||||||
|
|
||||||
|
/** 人员属性字典编码集合,前端以 JSON 数组提交;空集合表示不限制人员属性。 */
|
||||||
|
private List<String> userAttributes;
|
||||||
|
|
||||||
/** 在职状态。 */
|
/** 在职状态。 */
|
||||||
private String userState;
|
private String userState;
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -7,6 +7,8 @@ import com.budwk.app.zhgh.staffmanage.member.models.MemberChangeRecord;
|
|||||||
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserBatchChangeParam;
|
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserBatchChangeParam;
|
||||||
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserPageForm;
|
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserPageForm;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待确认人员记录业务服务。
|
* 待确认人员记录业务服务。
|
||||||
*/
|
*/
|
||||||
@@ -20,6 +22,14 @@ public interface PendingConfirmUserService extends BaseService<Sys_user> {
|
|||||||
*/
|
*/
|
||||||
Pagination pageData(PendingConfirmUserPageForm pageForm);
|
Pagination pageData(PendingConfirmUserPageForm pageForm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按列表查询条件导出全部匹配人员,不受当前分页大小限制。
|
||||||
|
*
|
||||||
|
* @param pageForm 列表查询条件及排序条件
|
||||||
|
* @param response HTTP 文件下载响应,返回 XSSF 格式的 Excel 文件
|
||||||
|
*/
|
||||||
|
void exportExcel(PendingConfirmUserPageForm pageForm, HttpServletResponse response);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直接提交待确认人员变更,不生成会员变更申请及流程数据。
|
* 直接提交待确认人员变更,不生成会员变更申请及流程数据。
|
||||||
*
|
*
|
||||||
@@ -29,9 +39,9 @@ public interface PendingConfirmUserService extends BaseService<Sys_user> {
|
|||||||
boolean submitChange(MemberChangeRecord record);
|
boolean submitChange(MemberChangeRecord record);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量修改待确认人员的人员类型,并将处理成功的人员标记为已确认。
|
* 批量修改待确认人员允许整改的字段,并将处理成功的人员标记为已确认。
|
||||||
*
|
*
|
||||||
* @param changeParam 批量勾选的用户ID及目标人员类型
|
* @param changeParam 批量勾选的用户ID及待整改字段
|
||||||
* @return 实际完成确认的人员数量
|
* @return 实际完成确认的人员数量
|
||||||
*/
|
*/
|
||||||
int batchChangePersonType(PendingConfirmUserBatchChangeParam changeParam);
|
int batchChangePersonType(PendingConfirmUserBatchChangeParam changeParam);
|
||||||
|
|||||||
+231
-15
@@ -1,5 +1,9 @@
|
|||||||
package com.budwk.app.zhgh.staffmanage.sourcechange.service.impl;
|
package com.budwk.app.zhgh.staffmanage.sourcechange.service.impl;
|
||||||
|
|
||||||
|
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.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
@@ -7,6 +11,7 @@ import cn.hutool.json.JSONUtil;
|
|||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
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.base.utils.CommonDownloadUtil;
|
||||||
import com.budwk.app.base.utils.PageUtil;
|
import com.budwk.app.base.utils.PageUtil;
|
||||||
import com.budwk.app.sys.models.Sys_dict;
|
import com.budwk.app.sys.models.Sys_dict;
|
||||||
import com.budwk.app.sys.models.Sys_user;
|
import com.budwk.app.sys.models.Sys_user;
|
||||||
@@ -18,6 +23,7 @@ import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
|
|||||||
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserBatchChangeParam;
|
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserBatchChangeParam;
|
||||||
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserPageForm;
|
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingConfirmUserPageForm;
|
||||||
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingConfirmUserService;
|
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingConfirmUserService;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
@@ -27,6 +33,9 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.ioc.aop.Aop;
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -63,6 +72,51 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Pagination pageData(PendingConfirmUserPageForm pageForm) {
|
public Pagination pageData(PendingConfirmUserPageForm pageForm) {
|
||||||
|
Sql sql = buildQuerySql(pageForm);
|
||||||
|
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按列表全部查询条件导出人员,查询范围不受 pageNumber、pageSize 限制。
|
||||||
|
*
|
||||||
|
* @param pageForm 列表查询条件及排序条件
|
||||||
|
* @param response HTTP 文件下载响应,返回字段与列表展示字段保持一致
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void exportExcel(PendingConfirmUserPageForm pageForm, HttpServletResponse response) {
|
||||||
|
List<NutMap> list = listMap(buildQuerySql(pageForm));
|
||||||
|
for (int index = 0; index < list.size(); index++) {
|
||||||
|
NutMap row = list.get(index);
|
||||||
|
row.put("index", index + 1);
|
||||||
|
row.put("pendingConfirmStatusName",
|
||||||
|
Integer.valueOf(STATUS_CONFIRMED).equals(row.getInt("pendingConfirmStatus")) ? "已确认" : "待确认");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExcelExportEntity> exportEntities = new ArrayList<>();
|
||||||
|
exportEntities.add(new ExcelExportEntity("序号", "index", 10));
|
||||||
|
exportEntities.add(new ExcelExportEntity("工号", "loginname", 20));
|
||||||
|
exportEntities.add(new ExcelExportEntity("姓名", "username", 20));
|
||||||
|
exportEntities.add(new ExcelExportEntity("性别", "sex", 10));
|
||||||
|
exportEntities.add(new ExcelExportEntity("所属工会", "unionName", 25));
|
||||||
|
exportEntities.add(new ExcelExportEntity("所属单位", "unitName", 30));
|
||||||
|
exportEntities.add(new ExcelExportEntity("人员类型", "personType", 20));
|
||||||
|
exportEntities.add(new ExcelExportEntity("人员属性", "userAttribute", 20));
|
||||||
|
exportEntities.add(new ExcelExportEntity("在职状态", "userState", 15));
|
||||||
|
exportEntities.add(new ExcelExportEntity("确认状态", "pendingConfirmStatusName", 15));
|
||||||
|
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
|
||||||
|
CommonDownloadUtil.download("待确认人员记录.xlsx", workbook, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建分页查询和导出共用的人员 SQL,确保筛选条件、确认状态及排序口径一致。
|
||||||
|
*
|
||||||
|
* @param pageForm 查询条件,包括姓名/工号、工会、单位、人员类型、在职状态、确认状态及排序
|
||||||
|
* @return 已绑定查询条件和安全排序字段的 SQL 对象
|
||||||
|
*/
|
||||||
|
private Sql buildQuerySql(PendingConfirmUserPageForm pageForm) {
|
||||||
Sql sql = Sqls.create("""
|
Sql sql = Sqls.create("""
|
||||||
SELECT
|
SELECT
|
||||||
userInfo.id,
|
userInfo.id,
|
||||||
@@ -71,6 +125,7 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
userInfo.sex,
|
userInfo.sex,
|
||||||
userInfo.userState,
|
userInfo.userState,
|
||||||
userInfo.personType,
|
userInfo.personType,
|
||||||
|
userInfo.userAttribute,
|
||||||
userInfo.unitId,
|
userInfo.unitId,
|
||||||
unitInfo.name AS unitName,
|
unitInfo.name AS unitName,
|
||||||
unionInfo.id unionId,
|
unionInfo.id unionId,
|
||||||
@@ -95,6 +150,10 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
cnd.andEX("userInfo.unionId", "=", pageForm.getUnionId());
|
cnd.andEX("userInfo.unionId", "=", pageForm.getUnionId());
|
||||||
cnd.andEX("userInfo.unitId", "=", pageForm.getUnitId());
|
cnd.andEX("userInfo.unitId", "=", pageForm.getUnitId());
|
||||||
cnd.andEX("userInfo.personType", "=", pageForm.getPersonType());
|
cnd.andEX("userInfo.personType", "=", pageForm.getPersonType());
|
||||||
|
// 多选人员属性匹配任一所选编码;未选择时不增加条件,列表和导出共用此筛选。
|
||||||
|
if (pageForm.getUserAttributes() != null && !pageForm.getUserAttributes().isEmpty()) {
|
||||||
|
cnd.and("userInfo.userAttribute", "in", pageForm.getUserAttributes());
|
||||||
|
}
|
||||||
cnd.andEX("userInfo.userState", "=", pageForm.getUserState());
|
cnd.andEX("userInfo.userState", "=", pageForm.getUserState());
|
||||||
|
|
||||||
String orderColumn = resolveOrderColumn(pageForm.getPageOrderName());
|
String orderColumn = resolveOrderColumn(pageForm.getPageOrderName());
|
||||||
@@ -104,7 +163,7 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
cnd.asc("userInfo.loginname");
|
cnd.asc("userInfo.loginname");
|
||||||
}
|
}
|
||||||
sql.setCondition(cnd);
|
sql.setCondition(cnd);
|
||||||
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
return sql;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,18 +178,27 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
if (record == null || StrUtil.isBlank(record.getUserId())) {
|
if (record == null || StrUtil.isBlank(record.getUserId())) {
|
||||||
throw new BaseException("未获取到待确认人员数据");
|
throw new BaseException("未获取到待确认人员数据");
|
||||||
}
|
}
|
||||||
|
applyRetiredPersonType(record);
|
||||||
if (StrUtil.isBlank(record.getPersonType())) {
|
if (StrUtil.isBlank(record.getPersonType())) {
|
||||||
throw new BaseException("请选择人员类型");
|
throw new BaseException("请选择人员类型");
|
||||||
}
|
}
|
||||||
validatePersonType(record.getPersonType());
|
validatePersonType(record.getPersonType());
|
||||||
// 单人页面虽然提交完整表单,服务层只采用用户ID和人员类型,防止请求篡改其他只读字段。
|
// 单人退休变更必须显式提交退休日期,避免绕过页面条件必填校验;不影响批量入口。
|
||||||
return changePersonType(record.getUserId(), record.getPersonType());
|
String retiredPersonTypeCode = resolveRetiredPersonTypeCode();
|
||||||
|
boolean requiresRetireDate = StrUtil.equals("退休", record.getUserState())
|
||||||
|
|| (StrUtil.isNotBlank(retiredPersonTypeCode)
|
||||||
|
&& StrUtil.equals(retiredPersonTypeCode, StrUtil.trim(record.getPersonType())));
|
||||||
|
if (requiresRetireDate && record.getRetireDate() == null) {
|
||||||
|
throw new BaseException("请选择退休日期");
|
||||||
|
}
|
||||||
|
// 单人页面虽然提交完整表单,服务层只采纳待确认业务允许编辑的字段,防止请求篡改其他只读字段。
|
||||||
|
return changePendingUserFields(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在同一事务内批量处理选中人员;任意人员状态异常时整体回滚,避免出现部分成功。
|
* 在同一事务内批量处理选中人员;任意人员状态异常时整体回滚,避免出现部分成功。
|
||||||
*
|
*
|
||||||
* @param changeParam 批量用户ID及目标人员类型
|
* @param changeParam 批量用户ID及待确认字段变更内容
|
||||||
* @return 完成确认的人员数量
|
* @return 完成确认的人员数量
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -139,10 +207,22 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
if (changeParam == null || StrUtil.isBlank(changeParam.getUserIds())) {
|
if (changeParam == null || StrUtil.isBlank(changeParam.getUserIds())) {
|
||||||
throw new BaseException("请选择需要变更的人员");
|
throw new BaseException("请选择需要变更的人员");
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(changeParam.getPersonType())) {
|
if (!hasBatchChangeField(changeParam)) {
|
||||||
throw new BaseException("请选择人员类型");
|
throw new BaseException("请选择需要变更的内容");
|
||||||
|
}
|
||||||
|
applyRetiredPersonType(changeParam);
|
||||||
|
if (StrUtil.isNotBlank(changeParam.getPersonType())) {
|
||||||
|
validatePersonType(changeParam.getPersonType());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在修改任何人员前校验批量退休时间,避免绕过前端后复用各人员原日期或产生部分更新。
|
||||||
|
String retiredPersonTypeCode = resolveRetiredPersonTypeCode();
|
||||||
|
boolean requiresRetireDate = StrUtil.equals("退休", changeParam.getUserState())
|
||||||
|
|| (StrUtil.isNotBlank(retiredPersonTypeCode)
|
||||||
|
&& StrUtil.equals(retiredPersonTypeCode, StrUtil.trim(changeParam.getPersonType())));
|
||||||
|
if (requiresRetireDate && changeParam.getRetireDate() == null) {
|
||||||
|
throw new BaseException("请选择退休时间");
|
||||||
}
|
}
|
||||||
validatePersonType(changeParam.getPersonType());
|
|
||||||
|
|
||||||
List<String> userIds;
|
List<String> userIds;
|
||||||
try {
|
try {
|
||||||
@@ -162,11 +242,49 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (String userId : new ArrayList<>(normalizedUserIds)) {
|
for (String userId : new ArrayList<>(normalizedUserIds)) {
|
||||||
changePersonType(userId, changeParam.getPersonType());
|
MemberChangeRecord record = new MemberChangeRecord();
|
||||||
|
record.setUserId(userId);
|
||||||
|
if (StrUtil.isNotBlank(changeParam.getPersonType())) {
|
||||||
|
record.setPersonType(changeParam.getPersonType());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(changeParam.getUserState())) {
|
||||||
|
record.setUserState(changeParam.getUserState());
|
||||||
|
}
|
||||||
|
// 满足退休状态或离退休人员类型时统一写入本次指定日期,其他情况忽略隐藏字段。
|
||||||
|
if (requiresRetireDate) {
|
||||||
|
record.setRetireDate(changeParam.getRetireDate());
|
||||||
|
}
|
||||||
|
if (StrUtil.equals("退休", changeParam.getUserState()) && changeParam.getWelfareStopDate() != null) {
|
||||||
|
record.setWelfareStopDate(DateUtil.formatDate(changeParam.getWelfareStopDate()));
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(changeParam.getUserAttribute())) {
|
||||||
|
record.setUserAttribute(changeParam.getUserAttribute());
|
||||||
|
}
|
||||||
|
if (changeParam.getMember() != null) {
|
||||||
|
record.setMember(changeParam.getMember());
|
||||||
|
}
|
||||||
|
if (changeParam.getWelfareMember() != null) {
|
||||||
|
record.setWelfareMember(changeParam.getWelfareMember());
|
||||||
|
}
|
||||||
|
changePendingUserFields(record);
|
||||||
}
|
}
|
||||||
return normalizedUserIds.size();
|
return normalizedUserIds.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断批量请求是否至少包含一个待整改字段;字符串空值不参与,Boolean false 是有效值。
|
||||||
|
*
|
||||||
|
* @param changeParam 批量变更参数
|
||||||
|
* @return true 表示存在至少一个待变更字段
|
||||||
|
*/
|
||||||
|
private boolean hasBatchChangeField(PendingConfirmUserBatchChangeParam changeParam) {
|
||||||
|
return StrUtil.isNotBlank(changeParam.getPersonType())
|
||||||
|
|| StrUtil.isNotBlank(changeParam.getUserState())
|
||||||
|
|| StrUtil.isNotBlank(changeParam.getUserAttribute())
|
||||||
|
|| changeParam.getMember() != null
|
||||||
|
|| changeParam.getWelfareMember() != null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅修改指定待确认人员的人员类型,并复用会员公共服务生成用户历史记录。
|
* 仅修改指定待确认人员的人员类型,并复用会员公共服务生成用户历史记录。
|
||||||
*
|
*
|
||||||
@@ -175,6 +293,20 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
* @return true 表示人员类型发生变化,false 表示类型未变化但已完成确认
|
* @return true 表示人员类型发生变化,false 表示类型未变化但已完成确认
|
||||||
*/
|
*/
|
||||||
private boolean changePersonType(String userId, String personType) {
|
private boolean changePersonType(String userId, String personType) {
|
||||||
|
MemberChangeRecord record = new MemberChangeRecord();
|
||||||
|
record.setUserId(userId);
|
||||||
|
record.setPersonType(personType);
|
||||||
|
return changePendingUserFields(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅采纳待确认页面允许编辑的人员字段,并复用会员公共服务生成用户历史记录。
|
||||||
|
*
|
||||||
|
* @param record 前端提交的待确认人员变更内容
|
||||||
|
* @return true 表示人员字段发生变化,false 表示字段未变化但已完成确认
|
||||||
|
*/
|
||||||
|
private boolean changePendingUserFields(MemberChangeRecord record) {
|
||||||
|
String userId = record.getUserId();
|
||||||
Sys_user user = dao().fetch(Sys_user.class, userId);
|
Sys_user user = dao().fetch(Sys_user.class, userId);
|
||||||
if (user == null || !Integer.valueOf(STATUS_PENDING).equals(user.getPendingConfirmStatus())) {
|
if (user == null || !Integer.valueOf(STATUS_PENDING).equals(user.getPendingConfirmStatus())) {
|
||||||
throw new BaseException("当前人员不属于待确认人员记录");
|
throw new BaseException("当前人员不属于待确认人员记录");
|
||||||
@@ -185,24 +317,32 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
if (userView == null) {
|
if (userView == null) {
|
||||||
throw new BaseException("未获取到人员基础信息");
|
throw new BaseException("未获取到人员基础信息");
|
||||||
}
|
}
|
||||||
// 从数据库当前值重建完整变更对象,除人员类型外不信任前端提交的任何人员字段。
|
// 从数据库当前值重建完整变更对象,仅覆盖待确认页面允许编辑的字段。
|
||||||
MemberChangeRecord safeRecord = new MemberChangeRecord();
|
MemberChangeRecord safeRecord = new MemberChangeRecord();
|
||||||
BeanUtil.copyProperties(userView, safeRecord);
|
BeanUtil.copyProperties(userView, safeRecord);
|
||||||
safeRecord.setUserId(userId);
|
safeRecord.setUserId(userId);
|
||||||
safeRecord.setPersonType(StrUtil.trim(personType));
|
safeRecord.setPersonType(record.getPersonType() == null ? userView.getPersonType() : StrUtil.trim(record.getPersonType()));
|
||||||
safeRecord.setMember(Integer.valueOf(1).equals(userView.getMember()));
|
safeRecord.setUserState(record.getUserState() == null ? userView.getUserState() : record.getUserState());
|
||||||
safeRecord.setWelfareMember(Integer.valueOf(1).equals(userView.getWelfareMember()));
|
safeRecord.setRetireDate(record.getRetireDate() == null ? userView.getRetireDate() : record.getRetireDate());
|
||||||
|
safeRecord.setUserAttribute(record.getUserAttribute() == null ? userView.getUserAttribute() : record.getUserAttribute());
|
||||||
|
safeRecord.setMember(record.getMember() == null ? Integer.valueOf(1).equals(userView.getMember()) : record.getMember());
|
||||||
|
safeRecord.setWelfareMember(record.getWelfareMember() == null ? Integer.valueOf(1).equals(userView.getWelfareMember()) : record.getWelfareMember());
|
||||||
safeRecord.setArrivalAtSchoolDate(userView.getArrivalAtSchoolDate() == null
|
safeRecord.setArrivalAtSchoolDate(userView.getArrivalAtSchoolDate() == null
|
||||||
? null : DateUtil.formatDate(userView.getArrivalAtSchoolDate()));
|
? null : DateUtil.formatDate(userView.getArrivalAtSchoolDate()));
|
||||||
safeRecord.setWelfareStopDate(userView.getWelfareStopDate() == null
|
safeRecord.setWelfareStopDate(record.getWelfareStopDate() == null
|
||||||
? null : DateUtil.formatDate(userView.getWelfareStopDate()));
|
? (userView.getWelfareStopDate() == null ? null : DateUtil.formatDate(userView.getWelfareStopDate()))
|
||||||
|
: record.getWelfareStopDate());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 复用公共校验和历史记录生成口径,确保单人、批量变更结果保持一致。
|
// 复用公共校验和历史记录生成口径,确保单人、批量变更结果保持一致。
|
||||||
memberCommonService.validateChangeAndSetBasicData(safeRecord, MemberChangeOrigin.SYSTEM);
|
memberCommonService.validateChangeAndSetBasicData(safeRecord, MemberChangeOrigin.SYSTEM);
|
||||||
} catch (BaseException e) {
|
} catch (BaseException e) {
|
||||||
// 人员类型与当前值相同时允许直接完成确认;其他业务异常必须继续抛出并触发事务回滚。
|
// 公共允许变更字段暂不包含福利会员状态,单独修改该字段时由待确认入口专用逻辑直接更新。
|
||||||
if ("未获取到异动数据".equals(e.getMessage())) {
|
if ("未获取到异动数据".equals(e.getMessage())) {
|
||||||
|
if (updateWelfareMemberIfChanged(record, userView)) {
|
||||||
|
markConfirmed(userId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
markConfirmed(userId);
|
markConfirmed(userId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -214,6 +354,34 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅在本次请求明确传入福利会员状态且与当前值不一致时更新;未传入时不参与整改。
|
||||||
|
*
|
||||||
|
* @param record 待确认页面提交的变更内容
|
||||||
|
* @param userView 当前人员视图数据
|
||||||
|
* @return true 表示已更新福利会员状态
|
||||||
|
*/
|
||||||
|
private boolean updateWelfareMemberIfChanged(MemberChangeRecord record, View_user userView) {
|
||||||
|
if (record.getWelfareMember() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean currentWelfareMember = Integer.valueOf(1).equals(userView.getWelfareMember());
|
||||||
|
if (record.getWelfareMember().equals(currentWelfareMember)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
UPDATE sys_user
|
||||||
|
SET welfareMember = @welfareMember
|
||||||
|
WHERE id = @userId
|
||||||
|
AND pendingConfirmStatus = @pendingStatus
|
||||||
|
""");
|
||||||
|
sql.setParam("welfareMember", record.getWelfareMember());
|
||||||
|
sql.setParam("userId", userView.getId());
|
||||||
|
sql.setParam("pendingStatus", STATUS_PENDING);
|
||||||
|
dao().execute(sql);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验待确认人员允许变更的目标人员类型。
|
* 校验待确认人员允许变更的目标人员类型。
|
||||||
*
|
*
|
||||||
@@ -230,6 +398,53 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单人待确认变更选择退休时,存在“离退休人员”字典项才自动设置人员类型编码。
|
||||||
|
*
|
||||||
|
* @param record 单人待确认变更内容
|
||||||
|
*/
|
||||||
|
private void applyRetiredPersonType(MemberChangeRecord record) {
|
||||||
|
if (record == null || !StrUtil.equals("退休", record.getUserState())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String retiredPersonTypeCode = resolveRetiredPersonTypeCode();
|
||||||
|
if (StrUtil.isNotBlank(retiredPersonTypeCode)) {
|
||||||
|
record.setPersonType(retiredPersonTypeCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量待确认变更选择退休时,存在“离退休人员”字典项才自动设置人员类型编码。
|
||||||
|
*
|
||||||
|
* @param changeParam 批量待确认变更参数
|
||||||
|
*/
|
||||||
|
private void applyRetiredPersonType(PendingConfirmUserBatchChangeParam changeParam) {
|
||||||
|
if (changeParam == null || !StrUtil.equals("退休", changeParam.getUserState())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String retiredPersonTypeCode = resolveRetiredPersonTypeCode();
|
||||||
|
if (StrUtil.isNotBlank(retiredPersonTypeCode)) {
|
||||||
|
changeParam.setPersonType(retiredPersonTypeCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据人员类型字典名称获取“离退休人员”编码,未配置时返回空字符串。
|
||||||
|
*
|
||||||
|
* @return 离退休人员字典编码,未配置时为空字符串
|
||||||
|
*/
|
||||||
|
private String resolveRetiredPersonTypeCode() {
|
||||||
|
List<Sys_dict> personTypeDicts = sysDictService.getSubListByCode("USER_PERSON_TYPE");
|
||||||
|
if (personTypeDicts == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return personTypeDicts.stream()
|
||||||
|
.filter(dict -> StrUtil.equals("离退休人员", dict.getName()))
|
||||||
|
.map(Sys_dict::getCode)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 变更提交成功后仅把待确认人员更新为已确认,重复调用不会改变其他状态。
|
* 变更提交成功后仅把待确认人员更新为已确认,重复调用不会改变其他状态。
|
||||||
*
|
*
|
||||||
@@ -265,6 +480,7 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> imp
|
|||||||
"unionName", "unionInfo.name",
|
"unionName", "unionInfo.name",
|
||||||
"unitName", "unitInfo.name",
|
"unitName", "unitInfo.name",
|
||||||
"personType", "userInfo.personType",
|
"personType", "userInfo.personType",
|
||||||
|
"userAttribute", "userInfo.userAttribute",
|
||||||
"userState", "userInfo.userState"
|
"userState", "userInfo.userState"
|
||||||
);
|
);
|
||||||
return orderColumns.getOrDefault(StrUtil.nullToEmpty(pageOrderName), "");
|
return orderColumns.getOrDefault(StrUtil.nullToEmpty(pageOrderName), "");
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- 系统用户增加福利享受截止时间,字段保持可空以兼容历史人员数据。
|
||||||
|
-- 脚本支持重复执行,字段已存在时不再修改表结构。
|
||||||
|
SET @welfare_stop_date_column_count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'sys_user'
|
||||||
|
AND COLUMN_NAME = 'welfareStopDate'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @welfare_stop_date_ddl = IF(
|
||||||
|
@welfare_stop_date_column_count = 0,
|
||||||
|
'ALTER TABLE sys_user ADD COLUMN welfareStopDate DATE NULL COMMENT ''福利享受截止时间'' AFTER retireDate',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE welfare_stop_date_stmt FROM @welfare_stop_date_ddl;
|
||||||
|
EXECUTE welfare_stop_date_stmt;
|
||||||
|
DEALLOCATE PREPARE welfare_stop_date_stmt;
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/* 首页服务门户布局单独限定作用域,避免影响应用中心和其他业务页。 */
|
||||||
|
#v4-home-app.home-portal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
color: #20344d;
|
||||||
|
background: #f2f6fb;
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
#v4-home-app.home-portal * { box-sizing: border-box; }
|
||||||
|
#v4-home-app .portal-top { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; max-width: 1720px; margin: auto; padding: 18px 24px 0; }
|
||||||
|
/* 使用完整不透明背景铺满横幅,会员信息独立成卡,不再覆盖校园景观。 */
|
||||||
|
#v4-home-app .portal-hero { position: relative; min-width: 0; min-height: 260px; overflow: hidden; border-radius: 12px; background: #e8f4ff url('/assets/platform/img/home/service-portal-banner-campus-v4.png') no-repeat right bottom / cover; }
|
||||||
|
#v4-home-app .portal-hero::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, #e8f4ff 0%, #e8f4ff 28%, rgba(232,244,255,.92) 39%, rgba(232,244,255,.35) 54%, rgba(232,244,255,0) 68%); }
|
||||||
|
#v4-home-app .portal-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; min-height: 260px; height: 100%; padding: 32px 40px; }
|
||||||
|
#v4-home-app .portal-hero-copy { max-width: 80%; }
|
||||||
|
#v4-home-app .portal-hero-eyebrow { margin-bottom: 12px; color: #527ca1; font-size: 14px; line-height: 22px; letter-spacing: 2px; }
|
||||||
|
#v4-home-app .portal-hero-copy h1 { margin: 0 0 10px; font-size: 36px; line-height: 1.3; font-weight: 700; color: #083765; }
|
||||||
|
#v4-home-app .portal-hero-copy p { margin: 0; color: #335675; font-size: 17px; line-height: 1.8; }
|
||||||
|
#v4-home-app .portal-values { display: flex; flex-wrap: wrap; gap: 22px 36px; margin-top: 24px; }
|
||||||
|
#v4-home-app .portal-values span { display: inline-flex; align-items: center; gap: 12px; }
|
||||||
|
#v4-home-app .portal-values i { color: #3599f2; font-size: 23px; }
|
||||||
|
/* 首次渲染时分层淡入文字,只改变透明度和位移,不影响卡片布局。 */
|
||||||
|
@keyframes portal-text-enter {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
#v4-home-app .portal-hero-eyebrow,
|
||||||
|
#v4-home-app .portal-hero-copy h1,
|
||||||
|
#v4-home-app .portal-hero-copy p,
|
||||||
|
#v4-home-app .portal-values > span,
|
||||||
|
#v4-home-app .portal-member-date,
|
||||||
|
#v4-home-app .portal-member-heading,
|
||||||
|
#v4-home-app .portal-member .meta-row { animation: portal-text-enter .6s cubic-bezier(.22, 1, .36, 1) backwards; }
|
||||||
|
#v4-home-app .portal-hero-copy h1 { animation-delay: .08s; }
|
||||||
|
#v4-home-app .portal-hero-copy p { animation-delay: .16s; }
|
||||||
|
#v4-home-app .portal-values > span:nth-child(1) { animation-delay: .24s; }
|
||||||
|
#v4-home-app .portal-values > span:nth-child(2) { animation-delay: .30s; }
|
||||||
|
#v4-home-app .portal-values > span:nth-child(3) { animation-delay: .36s; }
|
||||||
|
#v4-home-app .portal-values > span:nth-child(4) { animation-delay: .42s; }
|
||||||
|
#v4-home-app .portal-member-date { animation-delay: .10s; }
|
||||||
|
#v4-home-app .portal-member-heading { animation-delay: .18s; }
|
||||||
|
#v4-home-app .portal-member .meta-row:nth-child(1) { animation-delay: .26s; }
|
||||||
|
#v4-home-app .portal-member .meta-row:nth-child(2) { animation-delay: .34s; }
|
||||||
|
#v4-home-app .portal-member .meta-row:nth-child(3) { animation-delay: .42s; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#v4-home-app .portal-hero-eyebrow,
|
||||||
|
#v4-home-app .portal-hero-copy h1,
|
||||||
|
#v4-home-app .portal-hero-copy p,
|
||||||
|
#v4-home-app .portal-values > span,
|
||||||
|
#v4-home-app .portal-member-date,
|
||||||
|
#v4-home-app .portal-member-heading,
|
||||||
|
#v4-home-app .portal-member .meta-row { animation: none; }
|
||||||
|
}
|
||||||
|
#v4-home-app .portal-content { max-width: 1720px; margin: auto; padding: 18px 24px 28px; }
|
||||||
|
/* 同行共用网格行高,统一横纵间距;待办使用紧凑两行信息,避免撑出大块留白。 */
|
||||||
|
#v4-home-app .portal-columns { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; margin-top: 18px; align-items: stretch; }
|
||||||
|
#v4-home-app .portal-columns > * { min-width: 0; }
|
||||||
|
#v4-home-app .portal-news-wide { grid-column: 1 / -1; }
|
||||||
|
#v4-home-app .portal-panel,
|
||||||
|
#v4-home-app .entry-wrapper,
|
||||||
|
#v4-home-app .todo-center,
|
||||||
|
#v4-home-app .work-template-wrapper,
|
||||||
|
#v4-home-app .jcdt-container { background: #fff; border: 0; border-radius: 12px; padding: 20px; box-shadow: 0 3px 14px rgba(30, 66, 108, .035); }
|
||||||
|
#v4-home-app .home-section-header,
|
||||||
|
#v4-home-app .portal-todo-heading,
|
||||||
|
#v4-home-app .work-template-header,
|
||||||
|
#v4-home-app .portal-news-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 28px; margin: 0 0 16px; }
|
||||||
|
#v4-home-app .home-section-title,
|
||||||
|
#v4-home-app .work-template-title,
|
||||||
|
#v4-home-app .portal-news-title,
|
||||||
|
#v4-home-app .portal-todo-heading h3 { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 0; border: 0; font-size: 20px; line-height: 1.4; font-weight: 600; color: #123356; }
|
||||||
|
#v4-home-app .home-section-title i,
|
||||||
|
#v4-home-app .work-template-title i,
|
||||||
|
#v4-home-app .portal-news-title > i:first-child,
|
||||||
|
#v4-home-app .portal-todo-heading i { color: #1687f5; font-size: 19px; }
|
||||||
|
#v4-home-app .portal-more { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; border: 0; padding: 4px 0; background: transparent; color: #1687f5; font: inherit; font-size: 13px; cursor: pointer; text-decoration: none; }
|
||||||
|
#v4-home-app .portal-more:hover { color: #0565c6; }
|
||||||
|
#v4-home-app button:focus-visible,
|
||||||
|
#v4-home-app a:focus-visible,
|
||||||
|
#v4-home-app [tabindex]:focus-visible { outline: 2px solid #1687f5; outline-offset: 3px; }
|
||||||
|
#v4-home-app .entry-card { min-width: 0; }
|
||||||
|
#v4-home-app .entry-wrapper { padding-bottom: 0; }
|
||||||
|
#v4-home-app .portal-entry-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #e9eff6; }
|
||||||
|
#v4-home-app .portal-entry-tabs { display: flex; gap: 34px; }
|
||||||
|
#v4-home-app .portal-entry-tabs button { border: 0; border-bottom: 3px solid transparent; padding: 0 0 13px; background: transparent; font: inherit; font-size: 19px; color: #465d78; font-weight: 600; cursor: pointer; }
|
||||||
|
#v4-home-app .portal-entry-tabs button.active { color: #0784fa; border-bottom-color: #0784fa; }
|
||||||
|
#v4-home-app .entry-section + .entry-section { margin-top: 0; }
|
||||||
|
#v4-home-app .app-grid { display: flex; justify-content: flex-start; align-items: center; gap: 12px; min-height: 0; overflow-x: auto; padding: 20px 0; border: 0; border-radius: 0; box-shadow: none; }
|
||||||
|
#v4-home-app .app-item { flex: 0 0 96px; border: 0; font: inherit; padding: 0 4px; max-width: none; }
|
||||||
|
#v4-home-app .app-item:hover { background: #f4f9ff; box-shadow: none; transform: translateY(-2px); }
|
||||||
|
#v4-home-app .app-icon { background: transparent; }
|
||||||
|
#v4-home-app .app-name { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; line-height: 20px; min-height: 0; margin-top: 2px; }
|
||||||
|
#v4-home-app .app-empty { padding: 10px 0; }
|
||||||
|
#v4-home-app .act-card { padding: 0; border: 0; border-radius: 0; box-shadow: none; }
|
||||||
|
#v4-home-app .activity-swiper-container { padding: 0 0 4px; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item { display: flex; flex-direction: column; height: 100%; }
|
||||||
|
/* 加高封面并统一裁剪容器,图片、加载态和占位图都保持相同圆角。 */
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item .img-box { flex-shrink: 0; height: auto; aspect-ratio: 1.5; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #1687f5; isolation: isolate; }
|
||||||
|
#v4-home-app .img-box .el-image { display: block; width: 100%; height: 100%; position: absolute; inset: 0; border-radius: inherit; overflow: hidden; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item .img-box img { height: 100% !important; max-height: none !important; object-fit: cover !important; border-radius: 10px; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status { top: 8px; left: 8px; border-radius: 5px; padding: 4px 7px; background: #ed4859; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-ended { background: #8b97a7; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-upcoming { background: #1687f5; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item h4 { flex: 1; margin: 10px 0 8px; height: auto; min-height: 22px; max-height: 44px; font-size: 15px; color: #20344d; line-height: 22px; }
|
||||||
|
#v4-home-app .activity-swiper .swiper-slide .item .time p { white-space: normal; font-size: 12px; color: #7b899d; line-height: 1.6; }
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-next,
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-prev { width: 44px; height: 44px; margin-top: 0; padding: 0; transform: translateY(-50%); border: 1px solid #dbeaff; background: rgba(255,255,255,.96); color: #1687f5; box-shadow: 0 3px 12px rgba(30, 66, 108, .18); }
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-next { right: 8px; }
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-prev { left: 8px; }
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-next:hover,
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-prev:hover { transform: translateY(-50%) scale(1.05); background: #1687f5; color: #fff; }
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-next::after,
|
||||||
|
#v4-home-app .activity-swiper-container .swiper-button-prev::after { font-size: 18px; font-weight: 600; }
|
||||||
|
#v4-home-app .portal-cover-empty { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 9px; color: #94a7bd; background: #f0f5fb; }
|
||||||
|
#v4-home-app .portal-cover-empty i { font-size: 28px; }
|
||||||
|
#v4-home-app .user-wrapper { background: transparent; }
|
||||||
|
#v4-home-app .user-container { display: block; width: 100%; height: 100%; margin: 0; }
|
||||||
|
#v4-home-app .todo-center { display: flex; flex-direction: column; min-width: 0; height: 100%; }
|
||||||
|
#v4-home-app .todo-center .header { display: block; margin: 0; }
|
||||||
|
#v4-home-app .todo-center .el-tabs { margin: 0 0 8px; }
|
||||||
|
#v4-home-app .todo-center .el-tabs__header { margin: 0; }
|
||||||
|
#v4-home-app .todo-center .el-tabs__nav { display: flex; width: 100%; border-color: #e9eff6; border-radius: 6px 6px 0 0; }
|
||||||
|
#v4-home-app .todo-center .el-tabs__item { flex: 1; min-width: 0; padding: 0 7px; font-size: 12px; height: 36px; line-height: 36px; text-align: center; }
|
||||||
|
#v4-home-app .todo-center .el-tabs__item.is-active { border-bottom: 2px solid #1687f5; color: #1687f5; }
|
||||||
|
#v4-home-app .portal-task-list { display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; min-height: 180px; }
|
||||||
|
#v4-home-app .portal-task-row { display: flex; gap: 10px; align-items: center; padding: 8px 6px; border-bottom: 1px solid #eef3f8; }
|
||||||
|
#v4-home-app .portal-task-row:last-child { border-bottom: 0; }
|
||||||
|
#v4-home-app .portal-task-row:hover { background: #f7fbff; }
|
||||||
|
#v4-home-app .portal-task-icon { width: 34px; height: 40px; display: flex; flex: 0 0 34px; align-items: center; justify-content: center; border-radius: 8px; background: #eef7ff; color: #1687f5; font-size: 22px; }
|
||||||
|
#v4-home-app .portal-task-content { min-width: 0; flex: 1; }
|
||||||
|
#v4-home-app .portal-task-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #263548; font-weight: 600; font-size: 14px; line-height: 22px; }
|
||||||
|
#v4-home-app .portal-task-meta { display: flex; align-items: baseline; gap: 10px; font-size: 12px; color: #8390a2; line-height: 21px; }
|
||||||
|
#v4-home-app .portal-task-meta span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
#v4-home-app .portal-task-meta > span:first-child { max-width: 45%; flex-shrink: 0; }
|
||||||
|
#v4-home-app .portal-task-date { flex-shrink: 0; font-size: 12px; color: #7b899d; }
|
||||||
|
#v4-home-app .portal-task-node { flex: 1; min-width: 0; color: #8b98a9; font-size: 11px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
#v4-home-app .portal-task-row .el-button { flex-shrink: 0; padding: 8px 14px; }
|
||||||
|
#v4-home-app .portal-task-pagination { text-align: right; margin-top: 8px; }
|
||||||
|
#v4-home-app .portal-empty { min-height: 132px; display: flex; align-items: center; justify-content: center; gap: 10px; color: #8a9aac; }
|
||||||
|
#v4-home-app .portal-empty i { font-size: 26px; color: #83b9ee; }
|
||||||
|
#v4-home-app .work-template-wrapper { width: 100%; margin: 0; }
|
||||||
|
#v4-home-app .work-template-card { min-height: 98px; padding: 0; border: 0; border-radius: 0; box-shadow: none; }
|
||||||
|
#v4-home-app .work-template-grid { display: flex; align-items: flex-start; gap: 14px; overflow-x: auto; padding: 4px 0 10px; scroll-snap-type: x proximity; }
|
||||||
|
#v4-home-app .work-template-item { flex: 0 0 88px; width: 88px; padding: 4px; border: 0; font: inherit; scroll-snap-align: start; }
|
||||||
|
#v4-home-app .work-template-item:hover { background: #f4f9ff; box-shadow: none; }
|
||||||
|
#v4-home-app .work-template-icon,
|
||||||
|
#v4-home-app .work-template-icon .el-image { width: 48px; height: 48px; }
|
||||||
|
#v4-home-app .work-template-icon img { width: 48px !important; height: 48px !important; object-fit: contain; }
|
||||||
|
#v4-home-app .work-template-icon i { font-size: 30px; line-height: 48px; color: #83b9ee; }
|
||||||
|
#v4-home-app .work-template-name { font-size: 13px !important; line-height: 20px; min-height: 40px; max-height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||||
|
#v4-home-app .work-template-wrapper .no-activity-placeholder { padding: 12px; }
|
||||||
|
#v4-home-app .jcdt-container { width: 100%; max-width: none; height: 100%; margin: 0; }
|
||||||
|
#v4-home-app .portal-news-title { background: transparent; cursor: pointer; font-family: inherit; }
|
||||||
|
#v4-home-app .portal-news-title > i:last-child:not(:first-child) { color: #8092a6; font-size: 14px; }
|
||||||
|
#v4-home-app .portal-news-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 0; border: 0; background: transparent; color: #42536a; font: inherit; text-align: left; cursor: pointer; }
|
||||||
|
#v4-home-app .portal-news-row time { flex-shrink: 0; padding: 3px 7px; background: #eff5fc; border-radius: 4px; color: #45698f; font-size: 13px; }
|
||||||
|
#v4-home-app .portal-news-row span { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
|
#v4-home-app .portal-news-row:hover span { color: #1687f5; }
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
#v4-home-app .portal-hero-copy h1 { font-size: 42px; }
|
||||||
|
#v4-home-app .app-item { flex-basis: 104px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 1199px) {
|
||||||
|
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr) 300px; padding: 18px 18px 0; }
|
||||||
|
#v4-home-app .portal-hero-inner { padding-left: 32px; }
|
||||||
|
#v4-home-app .portal-content { padding: 18px 18px 24px; }
|
||||||
|
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); }
|
||||||
|
#v4-home-app .portal-values { gap: 16px; }
|
||||||
|
#v4-home-app .portal-panel, #v4-home-app .todo-center { padding: 16px; }
|
||||||
|
#v4-home-app .portal-task-row { gap: 8px; padding: 8px 0; }
|
||||||
|
#v4-home-app .portal-task-icon { display: none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
#v4-home-app .portal-activities { order: 1; }
|
||||||
|
#v4-home-app .portal-todo { order: 2; }
|
||||||
|
#v4-home-app .portal-templates { order: 3; }
|
||||||
|
#v4-home-app .portal-news { order: 4; }
|
||||||
|
#v4-home-app .portal-hero-copy { max-width: 68%; }
|
||||||
|
#v4-home-app .portal-hero-copy h1 { font-size: 30px; }
|
||||||
|
#v4-home-app .portal-hero-copy p { font-size: 14px; }
|
||||||
|
#v4-home-app .portal-task-icon { display: flex; }
|
||||||
|
#v4-home-app .portal-task-meta { flex-wrap: nowrap; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#v4-home-app .portal-top { padding: 10px 10px 8px; }
|
||||||
|
#v4-home-app .portal-hero-inner { padding: 24px 20px; }
|
||||||
|
#v4-home-app .portal-hero-copy { max-width: 100%; }
|
||||||
|
#v4-home-app .portal-hero-copy h1 { font-size: 28px; }
|
||||||
|
#v4-home-app .portal-values { gap: 12px 18px; font-size: 12px; }
|
||||||
|
#v4-home-app .portal-values i { font-size: 17px; }
|
||||||
|
#v4-home-app .portal-content { padding: 10px; }
|
||||||
|
#v4-home-app .entry-wrapper { padding: 16px 12px 0; }
|
||||||
|
#v4-home-app .portal-entry-tabs { gap: 20px; }
|
||||||
|
#v4-home-app .portal-entry-tabs button { font-size: 16px; }
|
||||||
|
#v4-home-app .app-item { flex-basis: 84px; }
|
||||||
|
#v4-home-app .home-section-title, #v4-home-app .portal-todo-heading h3, #v4-home-app .work-template-title, #v4-home-app .portal-news-title { font-size: 18px; }
|
||||||
|
#v4-home-app .portal-task-row .el-button { padding: 8px 10px; }
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -68,9 +68,9 @@
|
|||||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
|
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
|
||||||
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
|
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
|
||||||
|
|
||||||
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/vxe/xe-utils.js"></script>
|
||||||
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.js"></script>
|
||||||
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/vxe/vue-table.js"></script>
|
||||||
|
|
||||||
<!-- 引入 form-create 和 designer -->
|
<!-- 引入 form-create 和 designer -->
|
||||||
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
|
||||||
@@ -436,6 +436,8 @@
|
|||||||
.v4-logo {
|
.v4-logo {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
/* 保留 Logo 透明轮廓,将所有可见颜色统一转换为白色。 */
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.v4-nav {
|
.v4-nav {
|
||||||
@@ -754,7 +756,8 @@
|
|||||||
var currentPath = getBaseSubAppPath()
|
var currentPath = getBaseSubAppPath()
|
||||||
var footer = $("#v4-footer")
|
var footer = $("#v4-footer")
|
||||||
|
|
||||||
if (currentPath === "/platform/v4/home") {
|
// 服务门户首页不显示版权栏,PJAX 切换时同步判断当前页面。
|
||||||
|
if (currentPath === "/platform/v4/home" && !$("#v4-home-app").hasClass("home-portal")) {
|
||||||
footer.show()
|
footer.show()
|
||||||
} else {
|
} else {
|
||||||
footer.hide()
|
footer.hide()
|
||||||
|
|||||||
@@ -10,22 +10,23 @@ const act = {
|
|||||||
<div ref="activitySwiper" class="swiper activity-swiper">
|
<div ref="activitySwiper" class="swiper activity-swiper">
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper">
|
||||||
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
|
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
|
||||||
<div class="item" @click="go(activity)">
|
<div class="item" role="button" tabindex="0" @click="go(activity)" @keydown.enter="go(activity)" @keydown.space.prevent="go(activity)">
|
||||||
<div class="img-box">
|
<div class="img-box">
|
||||||
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
<el-image :src="activity.cover" fit="cover" :alt="activity.name">
|
||||||
alt="">
|
<div slot="error" class="portal-cover-empty"><i class="fa fa-picture-o" aria-hidden="true"></i><span>暂无图片</span></div>
|
||||||
<span class="status">
|
</el-image>
|
||||||
|
<span class="status" :class="{'status-ended': getStatusText(activity) === '已结束', 'status-upcoming': getStatusText(activity) === '未开始'}">
|
||||||
{{getStatusText(activity)}}
|
{{getStatusText(activity)}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="title">
|
<h4 class="title" :title="activity.name">
|
||||||
{{ activity.name}}
|
{{ activity.name}}
|
||||||
</h4>
|
</h4>
|
||||||
<!--报名时间-->
|
<!--报名时间-->
|
||||||
<div class="time">
|
<div class="time">
|
||||||
<p>
|
<p :title="[activity.startDate, activity.endDate].filter(Boolean).join(' 至 ')">
|
||||||
<i class="fa fa-clock-o"></i>
|
<i class="fa fa-clock-o"></i>
|
||||||
时间:{{ activity.startDate }}至{{ activity.endDate }}
|
{{ formatActivityDate(activity.startDate) }} 至 {{ formatActivityDate(activity.endDate) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,15 +35,15 @@ const act = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 导航按钮 -->
|
<!-- 导航按钮 -->
|
||||||
<div ref="nextButton" class="swiper-button-next"></div>
|
<button type="button" ref="nextButton" class="swiper-button-next" aria-label="下一组活动"></button>
|
||||||
<div ref="prevButton" class="swiper-button-prev"></div>
|
<button type="button" ref="prevButton" class="swiper-button-prev" aria-label="上一组活动"></button>
|
||||||
|
|
||||||
<!-- 进度条 -->
|
<!-- 进度条 -->
|
||||||
<div ref="pagination" class="swiper-pagination"></div>
|
<div ref="pagination" class="swiper-pagination"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="no-activity-placeholder">
|
<div v-else class="no-activity-placeholder">
|
||||||
<div class="icon">📅</div>
|
<div class="icon"><i class="fa fa-calendar" aria-hidden="true"></i></div>
|
||||||
<p>暂无近期活动</p>
|
<p>暂无近期活动</p>
|
||||||
<p class="subtext">敬请关注后续通知</p>
|
<p class="subtext">敬请关注后续通知</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,6 +58,24 @@ const act = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateNavigationPosition() {
|
||||||
|
const swiperElement = this.$refs.activitySwiper
|
||||||
|
const cover = swiperElement && swiperElement.querySelector('.img-box')
|
||||||
|
if (!cover) return
|
||||||
|
// 按实际封面高度定位,避免标题换行或响应式列数变化导致箭头偏移。
|
||||||
|
const container = swiperElement.parentElement
|
||||||
|
const top = cover.getBoundingClientRect().top - container.getBoundingClientRect().top + cover.offsetHeight / 2
|
||||||
|
;[this.$refs.prevButton, this.$refs.nextButton].forEach(button => {
|
||||||
|
if (button) button.style.top = top + 'px'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatActivityDate(value) {
|
||||||
|
return value ? this.$moment(value).format('YYYY-MM-DD') : '待定'
|
||||||
|
},
|
||||||
|
browseNext() {
|
||||||
|
if (!this.swiper) return
|
||||||
|
this.swiper.isEnd ? this.swiper.slideTo(0) : this.swiper.slideNext()
|
||||||
|
},
|
||||||
/*查询活动*/
|
/*查询活动*/
|
||||||
listAct() {
|
listAct() {
|
||||||
this.$axios.post("/platform/home/listHomeActivity").then((res) => {
|
this.$axios.post("/platform/home/listHomeActivity").then((res) => {
|
||||||
@@ -89,13 +108,22 @@ const act = {
|
|||||||
centeredSlides: false,
|
centeredSlides: false,
|
||||||
loop: false,
|
loop: false,
|
||||||
watchOverflow: true,
|
watchOverflow: true,
|
||||||
|
// 双栏中的活动按容器宽度排布,避免用整个窗口宽度挤出过多卡片。
|
||||||
|
breakpointsBase: 'container',
|
||||||
observer: false,
|
observer: false,
|
||||||
observeParents: false,
|
observeParents: false,
|
||||||
resizeObserver: false,
|
// 页面内容加载后可能出现滚动条,需重新计算列宽,避免末张封面及圆角被裁切。
|
||||||
|
resizeObserver: true,
|
||||||
|
roundLengths: true,
|
||||||
|
on: {
|
||||||
|
init: () => this.$nextTick(() => this.updateNavigationPosition()),
|
||||||
|
resize: () => this.$nextTick(() => this.updateNavigationPosition()),
|
||||||
|
slidesUpdated: () => this.$nextTick(() => this.updateNavigationPosition()),
|
||||||
|
},
|
||||||
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
|
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
|
||||||
mousewheel: {
|
mousewheel: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
forceToAxis: false,
|
forceToAxis: true,
|
||||||
releaseOnEdges: true,
|
releaseOnEdges: true,
|
||||||
sensitivity: 0.8,
|
sensitivity: 0.8,
|
||||||
},
|
},
|
||||||
@@ -108,14 +136,11 @@ const act = {
|
|||||||
type: 'progressbar',
|
type: 'progressbar',
|
||||||
},
|
},
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
768: {
|
480: {
|
||||||
slidesPerView: 2,
|
slidesPerView: 2,
|
||||||
},
|
},
|
||||||
1024: {
|
740: {
|
||||||
slidesPerView: 4,
|
slidesPerView: 3,
|
||||||
},
|
|
||||||
1200: {
|
|
||||||
slidesPerView: 5,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
const entry = {
|
const entry = {
|
||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div class="entry-wrapper">
|
<div class="entry-wrapper">
|
||||||
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
|
<div class="portal-entry-header">
|
||||||
<div class="entry-section-header">
|
<div class="portal-entry-tabs" role="tablist" aria-label="推荐入口">
|
||||||
<div class="entry-section-title">
|
<button v-for="section in entrySections" :key="section.key"
|
||||||
<i :class="section.icon"></i>
|
type="button" role="tab" :aria-selected="activeSection === section.key"
|
||||||
<span>{{ section.title }}</span>
|
:class="{active: activeSection === section.key}" @click="activeSection = section.key">{{ section.title }}</button>
|
||||||
<em>/Applications</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a class="portal-more" href="/platform/v4/apps">全部应用 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
|
||||||
|
</div>
|
||||||
|
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key" v-show="activeSection === section.key" role="tabpanel">
|
||||||
|
|
||||||
<div class="apps-container">
|
<div class="apps-container">
|
||||||
<div class="app-grid">
|
<div class="app-grid" tabindex="0" :aria-label="section.title + ',可横向滚动浏览'">
|
||||||
<div class="app-item"
|
<button type="button" class="app-item"
|
||||||
v-for="(item, index) in section.list"
|
v-for="(item, index) in section.list"
|
||||||
:key="item.id || index"
|
:key="item.id || index"
|
||||||
@click="openService(item, section.key)">
|
@click="openService(item, section.key)">
|
||||||
@@ -20,8 +21,8 @@ const entry = {
|
|||||||
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
|
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
|
||||||
<i v-else class="fa fa-cube app-icon-fallback"></i>
|
<i v-else class="fa fa-cube app-icon-fallback"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-name">{{ item.name }}</div>
|
<div class="app-name" :title="item.name">{{ item.name }}</div>
|
||||||
</div>
|
</button>
|
||||||
<el-empty class="app-empty"
|
<el-empty class="app-empty"
|
||||||
v-if="!section.list || section.list.length === 0"
|
v-if="!section.list || section.list.length === 0"
|
||||||
description="暂无数据"
|
description="暂无数据"
|
||||||
@@ -34,6 +35,7 @@ const entry = {
|
|||||||
`,
|
`,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
activeSection: 'serv',
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
entries: {
|
entries: {
|
||||||
serv: [],
|
serv: [],
|
||||||
@@ -127,11 +129,6 @@ const entry = {
|
|||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
|
|
||||||
.entry-section-fav {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-section-header {
|
.entry-section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -118,42 +118,51 @@ layout("/layouts/v4/baseLayout.html"){
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="v4-container" id="v4-home-app">
|
<link rel="stylesheet" href="${base!}/assets/platform/css/home-portal.css?v=20260929-1" />
|
||||||
|
|
||||||
|
<div class="v4-container home-portal" id="v4-home-app">
|
||||||
<!-- 首页banner -->
|
<!-- 首页banner -->
|
||||||
<div class="section-banner">
|
<div class="portal-top">
|
||||||
<div class="banner-img">
|
<div class="portal-hero">
|
||||||
<img :src="homeBannerList[0] || ''" alt=""/>
|
<div class="portal-hero-inner">
|
||||||
<stats></stats>
|
<div class="portal-hero-copy">
|
||||||
|
<div class="portal-hero-eyebrow">教职工一站式服务平台</div>
|
||||||
|
<h1>智慧工会系统</h1>
|
||||||
|
<p>情系教职工,服务零距离</p>
|
||||||
|
<div class="portal-values">
|
||||||
|
<span><i class="fa fa-heart" aria-hidden="true"></i>服务职工</span>
|
||||||
|
<span><i class="fa fa-users" aria-hidden="true"></i>凝聚职工</span>
|
||||||
|
<span><i class="fa fa-leaf" aria-hidden="true"></i>关爱职工</span>
|
||||||
|
<span><i class="fa fa-file-text" aria-hidden="true"></i>助力发展</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<user class="portal-member" :profile-only="true"></user>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="" style="padding-bottom: 20px">
|
<div class="portal-content">
|
||||||
<user></user>
|
|
||||||
|
|
||||||
<div class="home-grid">
|
|
||||||
<div class="entry-card">
|
<div class="entry-card">
|
||||||
<entry></entry>
|
<entry></entry>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-grid">
|
<div class="portal-columns">
|
||||||
<div class="home-section">
|
<div class="home-section portal-panel portal-activities">
|
||||||
<div class="home-section-header">
|
<div class="home-section-header">
|
||||||
<div class="home-section-title">
|
<div class="home-section-title">
|
||||||
<i class="fa fa-calendar"></i>
|
<i class="fa fa-calendar"></i>
|
||||||
<span>最新活动</span>
|
<span>最新活动</span>
|
||||||
<em>/Activities</em>
|
</div>
|
||||||
</div>
|
<button type="button" class="portal-more" @click="$refs.homeActivities.browseNext()">浏览更多 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="act-card">
|
<div class="act-card">
|
||||||
<act></act>
|
<act ref="homeActivities"></act>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<user ref="homeTodo" class="portal-todo"></user>
|
||||||
|
<work-template class="portal-templates" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
|
||||||
|
<jcdt class="portal-news" :class="{'portal-news-wide': !$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])}" :list="websiteNews"></jcdt>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
|
|
||||||
|
|
||||||
<jcdt :list="websiteNews"></jcdt>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,42 +2,63 @@ const jcdt = {
|
|||||||
template: /*language=HTML*/ `
|
template: /*language=HTML*/ `
|
||||||
<div class="jcdt-wrapper">
|
<div class="jcdt-wrapper">
|
||||||
<div class="jcdt-container">
|
<div class="jcdt-container">
|
||||||
<el-carousel height="360px" :interval="10000" indicator-position="none">
|
<div class="portal-news-header">
|
||||||
<el-carousel-item v-for="item,index in list" :key="index">
|
<el-dropdown @command="selectSection" trigger="click">
|
||||||
<div class="jcdt-title">
|
<button type="button" class="portal-news-title"><i class="fa fa-bullhorn" aria-hidden="true"></i> {{ currentNews.label }} <i v-if="list.length > 1" class="fa fa-angle-down" aria-hidden="true"></i></button>
|
||||||
<span>{{ item.label }}</span>
|
<el-dropdown-menu slot="dropdown">
|
||||||
</div>
|
<el-dropdown-item v-for="(section, index) in list" :key="index" :command="index">{{ section.label }}</el-dropdown-item>
|
||||||
<div class="jcdt-content">
|
</el-dropdown-menu>
|
||||||
<div class="jcdt-list">
|
</el-dropdown>
|
||||||
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
|
<div v-if="pageCount > 1" class="portal-news-pager" aria-label="动态分页">
|
||||||
<div class="date" v-if="o.time">
|
<span class="portal-news-count">共 {{ currentNews.value.length }} 条</span>
|
||||||
<i class="el-icon-alarm-clock"></i>
|
<button type="button" :disabled="pageNumber === 1" title="上一页动态" aria-label="上一页动态" @click="pageNumber--"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
|
||||||
<span>{{o.time}}</span>
|
<span class="portal-news-page" aria-live="polite">{{ pageNumber }} / {{ pageCount }}</span>
|
||||||
</div>
|
<button type="button" :disabled="pageNumber === pageCount" title="下一页动态" aria-label="下一页动态" @click="pageNumber++"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
|
||||||
<div style="display: flex; align-items: center">
|
</div>
|
||||||
<div>
|
</div>
|
||||||
|
<div class="portal-news-list">
|
||||||
</div>
|
<button type="button" class="portal-news-row" v-for="(item, index) in visibleNews" :key="index" @click="onLink(item)">
|
||||||
<div class="title">{{o.text}}</div>
|
<time v-if="item.time" :title="item.time">{{ item.time.slice(5) }}</time>
|
||||||
</div>
|
<span :title="item.text">{{ item.text }}</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<div v-if="!currentNews.value.length" class="portal-empty">暂无动态</div>
|
||||||
</div>
|
</div>
|
||||||
</el-carousel-item>
|
|
||||||
</el-carousel>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {activeIndex: 0, pageNumber: 1, pageSize: 3}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
list: {
|
list: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: []
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentNews() {
|
||||||
|
// 默认遵循接口栏目顺序,不再按栏目名称优先选择。
|
||||||
|
const section = this.list[this.activeIndex] || this.list[0]
|
||||||
|
return section ? {label: section.label, value: section.value || []} : {label: '最新动态', value: []}
|
||||||
|
},
|
||||||
|
visibleNews() {
|
||||||
|
const start = (this.pageNumber - 1) * this.pageSize
|
||||||
|
return this.currentNews.value.slice(start, start + this.pageSize)
|
||||||
|
},
|
||||||
|
pageCount() {
|
||||||
|
return Math.max(1, Math.ceil(this.currentNews.value.length / this.pageSize))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
list() {
|
||||||
|
this.selectSection(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
selectSection(index) {
|
||||||
|
this.activeIndex = index
|
||||||
|
this.pageNumber = 1
|
||||||
|
},
|
||||||
onLink(item) {
|
onLink(item) {
|
||||||
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href
|
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href
|
||||||
window.open(url)
|
window.open(url)
|
||||||
@@ -47,6 +68,15 @@ const jcdt = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
|
/* 每页固定三行,不因展开或末页条数变化撑高相邻的模板卡片。 */
|
||||||
|
#v4-home-app .portal-news-list { min-height: 132px; }
|
||||||
|
#v4-home-app .portal-news-list .portal-news-row { height: 44px; padding: 9px 0; }
|
||||||
|
#v4-home-app .portal-news-pager { display: flex; flex-shrink: 0; align-items: center; gap: 8px; }
|
||||||
|
#v4-home-app .portal-news-count { margin-right: 4px; color: #8a9aac; font-size: 12px; }
|
||||||
|
#v4-home-app .portal-news-page { min-width: 32px; color: #637b96; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||||
|
#v4-home-app .portal-news-pager button { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 1px solid #e4edf7; border-radius: 6px; background: #fff; color: #1687f5; font-size: 18px; cursor: pointer; }
|
||||||
|
#v4-home-app .portal-news-pager button:hover:not(:disabled) { border-color: #b9d8fb; background: #f0f7ff; }
|
||||||
|
#v4-home-app .portal-news-pager button:disabled { border-color: #eef2f7; background: #f8fafc; color: #c3cedb; cursor: default; }
|
||||||
.jcdt-wrapper {
|
.jcdt-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|||||||
@@ -5,21 +5,22 @@ const workTemplate = {
|
|||||||
<div class="work-template-title">
|
<div class="work-template-title">
|
||||||
<i class="fa fa-clipboard"></i>
|
<i class="fa fa-clipboard"></i>
|
||||||
<span>工作模板</span>
|
<span>工作模板</span>
|
||||||
<em>/Templates</em>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="portal-more" @click="browseTemplates">更多模板 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="work-template-card">
|
<div class="work-template-card">
|
||||||
<div v-if="templateList && templateList.length > 0" class="work-template-grid">
|
<div ref="templateGrid" v-if="templateList && templateList.length > 0" class="work-template-grid">
|
||||||
<div class="work-template-item"
|
<button type="button" class="work-template-item"
|
||||||
v-for="template in templateList"
|
v-for="template in templateList"
|
||||||
:key="template.id"
|
:key="template.id"
|
||||||
@click="go(template)">
|
@click="go(template)">
|
||||||
<div class="work-template-icon">
|
<div class="work-template-icon">
|
||||||
<img :src="template.templateIcon || template.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
|
<el-image :src="template.templateIcon || template.cover" fit="contain">
|
||||||
alt="">
|
<i slot="error" class="fa fa-clipboard" aria-hidden="true"></i>
|
||||||
|
</el-image>
|
||||||
</div>
|
</div>
|
||||||
<div class="work-template-name">{{ template.templateName || template.name }}</div>
|
<div class="work-template-name" :title="template.templateName || template.name">{{ template.templateName || template.name }}</div>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="no-activity-placeholder">
|
<div v-else class="no-activity-placeholder">
|
||||||
@@ -36,6 +37,13 @@ const workTemplate = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
browseTemplates() {
|
||||||
|
const grid = this.$refs.templateGrid
|
||||||
|
if (!grid) return
|
||||||
|
// 模板仍使用全部真实数据,横向翻页不截断管理员可用的入口。
|
||||||
|
const atEnd = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 2
|
||||||
|
grid.scrollTo({left: atEnd ? 0 : grid.scrollLeft + grid.clientWidth, behavior: 'smooth'})
|
||||||
|
},
|
||||||
listTemplate() {
|
listTemplate() {
|
||||||
this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
|
this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ const user = {
|
|||||||
<!-- 用户容器:左右布局 -->
|
<!-- 用户容器:左右布局 -->
|
||||||
<div class="user-container">
|
<div class="user-container">
|
||||||
<!-- 左侧:用户信息卡片 -->
|
<!-- 左侧:用户信息卡片 -->
|
||||||
<div class="user-info-card">
|
<div v-if="profileOnly" class="user-info-card">
|
||||||
|
<div class="portal-member-date"><i class="fa fa-calendar-o" aria-hidden="true"></i>{{ $moment().format('YYYY年MM月DD日') }} {{ ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][$moment().day()] }}</div>
|
||||||
<div class="user-details">
|
<div class="user-details">
|
||||||
<div class="user-name">${@auth.getPrincipalProperty('username')}</div>
|
<div class="portal-member-heading">
|
||||||
|
<span class="portal-member-avatar"><i class="fa fa-user" aria-hidden="true"></i></span>
|
||||||
|
<div><div class="portal-member-welcome">欢迎回来</div><div class="user-name">${@auth.getPrincipalProperty('username')}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="user-meta-grid">
|
<div class="user-meta-grid">
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="meta-icon">🆔</span>
|
<span class="meta-icon"><i class="fa fa-id-card-o" aria-hidden="true"></i></span>
|
||||||
<div>
|
<div>
|
||||||
<span class="meta-label">工号</span>
|
<span class="meta-label">工号</span>
|
||||||
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
|
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
|
||||||
@@ -18,7 +22,7 @@ const user = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<span class="meta-icon">🏢</span>
|
<span class="meta-icon"><i class="fa fa-building-o" aria-hidden="true"></i></span>
|
||||||
<div>
|
<div>
|
||||||
<span class="meta-label">单位信息</span>
|
<span class="meta-label">单位信息</span>
|
||||||
<span class="meta-value">
|
<span class="meta-value">
|
||||||
@@ -33,11 +37,11 @@ const user = {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-row">
|
<div class="meta-row meta-row-roles">
|
||||||
<span class="meta-icon">🎭</span>
|
<span class="meta-icon"><i class="fa fa-shield" aria-hidden="true"></i></span>
|
||||||
<div>
|
<div>
|
||||||
<span class="meta-label">角色</span>
|
<span class="meta-label">角色</span>
|
||||||
<span class="meta-value">{{ roleNames || '—' }}</span>
|
<div class="portal-member-roles"><span v-for="(role, index) in roleTags" :key="index" class="portal-member-role">{{ role }}</span><span v-if="!roleTags.length" class="meta-value">—</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,10 +67,13 @@ const user = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:待办中心 -->
|
<!-- 右侧:待办中心 -->
|
||||||
<div class="todo-center">
|
<div v-if="!profileOnly" class="todo-center">
|
||||||
<!-- 标题和标签 -->
|
<!-- 标题和标签 -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h3>待办中心</h3>
|
<div class="portal-todo-heading">
|
||||||
|
<h3><i class="fa fa-list-ul" aria-hidden="true"></i> 待办中心</h3>
|
||||||
|
<a class="portal-more" :href="'/flow/todoCenter?status=' + activeTab" target="_blank">全部待办 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
|
||||||
|
</div>
|
||||||
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
||||||
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
|
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
|
||||||
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
|
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
|
||||||
@@ -75,54 +82,41 @@ const user = {
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<div class="portal-task-list" v-loading="loading">
|
||||||
<el-table
|
<div class="portal-task-row" v-for="(task, index) in todoList" :key="task.taskId || task.instanceId || index">
|
||||||
:data="todoList"
|
<i class="fa fa-file-text-o portal-task-icon" aria-hidden="true"></i>
|
||||||
style="width: 100%"
|
<div class="portal-task-content">
|
||||||
border
|
<div class="portal-task-title" :title="task.instanceName">{{ task.instanceName }}</div>
|
||||||
stripe
|
<div class="portal-task-meta">
|
||||||
v-loading="loading"
|
<span :title="task.variable && task.variable.initiatorName">申请人:{{ (task.variable && task.variable.initiatorName) || '—' }}</span>
|
||||||
:cell-style="{ padding: '8px' }"
|
<span class="portal-task-node" :title="[task.categoryName, task.taskName].filter(Boolean).join(' · ')">{{ [task.categoryName, task.taskName].filter(Boolean).join(' · ') }}</span>
|
||||||
:header-cell-style="{ background: '#f8f9fa', color: '#444', fontWeight: '500' }"
|
</div>
|
||||||
>
|
</div>
|
||||||
<el-table-column label="序号" type="index" :index="indexMethod" width="100px"></el-table-column>
|
<time class="portal-task-date">{{ task.createdAt ? $moment(task.createdAt).format('YYYY-MM-DD') : '—' }}</time>
|
||||||
<el-table-column prop="categoryName" label="流程分类" width="100"></el-table-column>
|
<el-button type="primary" size="mini" @click="openView(task)">查看</el-button>
|
||||||
<el-table-column prop="title" label="流程名称" min-width="300">
|
</div>
|
||||||
<template slot-scope="scope">
|
<div v-if="!loading && !todoList.length" class="portal-empty"><i class="fa fa-check-circle-o" aria-hidden="true"></i><span>暂无{{ activeTab === 'todo' ? '待办事项' : '相关记录' }}</span></div>
|
||||||
<el-tooltip effect="dark" :content="scope.row.instanceName" placement="top">
|
</div>
|
||||||
<span class="table-title">{{ scope.row.instanceName }}</span>
|
<el-pagination v-if="pageForm.totalCount > pageForm.pageSize" class="portal-task-pagination"
|
||||||
</el-tooltip>
|
@current-change="pageNumberChange" :current-page="pageForm.pageNumber" :page-size="pageForm.pageSize"
|
||||||
</template>
|
layout="total, prev, next" :total="pageForm.totalCount"></el-pagination>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="sender" label="申请人" width="120">
|
|
||||||
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="taskName" label="当前流程节点" width="140" show-overflow-tooltip></el-table-column>
|
|
||||||
<el-table-column prop="date" label="发起日期" width="120">
|
|
||||||
<template slot-scope="{row}">
|
|
||||||
{{$moment(row.createdAt).format('YYYY-MM-DD')}}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="100px" fixed="right">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button type="primary" size="mini" @click="openView(scope.row)">查看</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<!--#include("/layouts/pagination.html"){}#-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
|
// 同一组件分别承载会员卡和待办卡,各自只加载需要的数据。
|
||||||
|
props: {
|
||||||
|
profileOnly: { type: Boolean, default: false }
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeTab: 'todo',
|
activeTab: 'todo',
|
||||||
todoList: [],
|
todoList: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
pageForm: {
|
pageForm: {
|
||||||
pageNum: 1,
|
pageNumber: 1,
|
||||||
pageSize: 5,
|
pageSize: 3,
|
||||||
totalCount: 0
|
totalCount: 0
|
||||||
},
|
},
|
||||||
taskStats: {
|
taskStats: {
|
||||||
@@ -140,6 +134,12 @@ const user = {
|
|||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
roleTags() {
|
||||||
|
// 兼容角色接口已有的中英文分隔符,仅改变展示,不改变角色权限。
|
||||||
|
return String(this.roleNames || '').split(/[,,、;;]/).map(role => role.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getWeatherInfo(iconCode, temp) {
|
getWeatherInfo(iconCode, temp) {
|
||||||
const info = {
|
const info = {
|
||||||
@@ -268,6 +268,7 @@ const user = {
|
|||||||
this.getStatistics()
|
this.getStatistics()
|
||||||
},
|
},
|
||||||
handleTabClick(tab) {
|
handleTabClick(tab) {
|
||||||
|
this.pageForm.pageNumber = 1
|
||||||
this.pageData()
|
this.pageData()
|
||||||
console.log('切换到:', tab.name);
|
console.log('切换到:', tab.name);
|
||||||
},
|
},
|
||||||
@@ -291,13 +292,42 @@ const user = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.pageData()
|
if (this.profileOnly) {
|
||||||
this.getRoleNames()
|
this.getRoleNames()
|
||||||
|
} else {
|
||||||
|
this.pageData()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchWeather()
|
if (this.profileOnly) this.fetchWeather()
|
||||||
},
|
},
|
||||||
style: /*language=CSS*/ `
|
style: /*language=CSS*/ `
|
||||||
|
/* 会员卡样式随组件注入,确保与卡片结构同步加载,避免旧外链样式造成排版缺失。 */
|
||||||
|
#v4-home-app .portal-member { min-width: 0; }
|
||||||
|
#v4-home-app .portal-member .user-info-card { height: 100%; padding: 20px; gap: 14px; border: 1px solid #e3edf8; border-radius: 12px; background: linear-gradient(180deg, #f1f7ff 0, #fff 110px); box-shadow: 0 3px 14px rgba(30,66,108,.035); }
|
||||||
|
#v4-home-app .portal-member-date { display: flex; align-items: center; gap: 8px; color: #71839b; font-size: 12px; line-height: 20px; }
|
||||||
|
#v4-home-app .portal-member-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #eaf0f7; }
|
||||||
|
#v4-home-app .portal-member-heading > div { min-width: 0; }
|
||||||
|
#v4-home-app .portal-member-avatar { display: flex; align-items: center; justify-content: center; flex: 0 0 46px; height: 46px; border-radius: 50%; background: #e1eeff; color: #1687f5; font-size: 25px; }
|
||||||
|
#v4-home-app .portal-member-welcome { margin-bottom: 3px; color: #8797a9; font-size: 12px; line-height: 18px; }
|
||||||
|
#v4-home-app .portal-member .user-name { margin: 0; color: #163c61; text-align: left; font-size: 18px; line-height: 24px; overflow-wrap: anywhere; }
|
||||||
|
#v4-home-app .portal-member .user-meta-grid { gap: 12px; margin: 0; }
|
||||||
|
#v4-home-app .portal-member .meta-row { gap: 8px; }
|
||||||
|
#v4-home-app .portal-member .meta-icon { min-width: 18px; color: #81a4c9; font-size: 14px; }
|
||||||
|
#v4-home-app .portal-member .meta-row > div { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 8px; min-width: 0; }
|
||||||
|
#v4-home-app .portal-member .meta-label { margin: 0; color: #8995a5; font-size: 12px; line-height: 20px; }
|
||||||
|
#v4-home-app .portal-member .meta-value { color: #283c54; font-size: 12px; font-weight: 400; line-height: 20px; overflow-wrap: anywhere; }
|
||||||
|
#v4-home-app .portal-member .meta-row-roles > div { display: block; }
|
||||||
|
#v4-home-app .portal-member-roles { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
|
||||||
|
#v4-home-app .portal-member-role { max-width: 100%; display: inline-block; padding: 3px 8px; border: 1px solid #e1ebf7; border-radius: 5px; background: #f3f7fc; color: #46617f; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
|
||||||
|
#v4-home-app .portal-member .weather-section { margin-top: auto; padding: 8px 10px; border-radius: 8px; background: #f0f7ff; color: #527494; }
|
||||||
|
#v4-home-app .portal-member .weather-header { gap: 6px; }
|
||||||
|
#v4-home-app .portal-member .weather-location,
|
||||||
|
#v4-home-app .portal-member .weather-text { font-size: 12px; }
|
||||||
|
#v4-home-app .portal-member .weather-emoji { font-size: 20px; }
|
||||||
|
#v4-home-app .portal-member .weather-temp { font-size: 18px; }
|
||||||
|
#v4-home-app .portal-member .weather-tip { margin-top: 4px; font-size: 11px; line-height: 18px; }
|
||||||
|
#v4-home-app .portal-member .weather-error { margin-top: auto; padding: 8px; font-size: 12px; }
|
||||||
.user-wrapper {
|
.user-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ layout("/layouts/platform.html"){
|
|||||||
<el-tag size="mini" v-else-if="row.isDeployed===0" type="info">否</el-tag>
|
<el-tag size="mini" v-else-if="row.isDeployed===0" type="info">否</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="流程中心" width="150" align="center" header-align="center">
|
||||||
|
<template slot-scope="{row}">
|
||||||
|
<el-switch :value="row.centerEnabled !== 0"
|
||||||
|
:loading="row.centerUpdating === true"
|
||||||
|
active-text="开启" inactive-text="关闭"
|
||||||
|
@change="updateCenterEnabled(row, $event)"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="排序编码" prop="sortNo" width="120" align="center" header-align="center" sortable="custom">
|
<el-table-column label="排序编码" prop="sortNo" width="120" align="center" header-align="center" sortable="custom">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -266,6 +274,25 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// 开关仅控制流程中心是否展示该流程,接口失败时保留原状态。
|
||||||
|
updateCenterEnabled(row, enabled) {
|
||||||
|
this.$set(row, "centerUpdating", true)
|
||||||
|
this.$axios.post("/flow/design/updateCenterEnabled", {
|
||||||
|
id: row.id,
|
||||||
|
centerEnabled: enabled ? 1 : 0
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$set(row, "centerEnabled", enabled ? 1 : 0)
|
||||||
|
this.$message.success(enabled ? "已开启" : "已关闭")
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || "更新流程中心显示状态失败")
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.error("更新流程中心显示状态失败")
|
||||||
|
}).finally(() => {
|
||||||
|
this.$set(row, "centerUpdating", false)
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.pageData()
|
this.pageData()
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ layout("/layouts/platform.html"){
|
|||||||
<el-option v-for="item in userStateOptions" :label="item" :value="item"></el-option>
|
<el-option v-for="item in userStateOptions" :label="item" :value="item"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
|
<!-- 暂停用人方式筛选,保留查询控件供后续恢复。
|
||||||
<search-item label="用人方式">
|
<search-item label="用人方式">
|
||||||
<el-select v-model="pageForm.preparedBy" clearable filterable>
|
<el-select v-model="pageForm.preparedBy" clearable filterable>
|
||||||
<el-option v-for="item in preparedByOptions" :label="item" :value="item"></el-option>
|
<el-option v-for="item in preparedByOptions" :label="item" :value="item"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
|
-->
|
||||||
<search-item label="人员类型">
|
<search-item label="人员类型">
|
||||||
<el-select v-model="pageForm.personType" clearable filterable>
|
<el-select v-model="pageForm.personType" clearable filterable>
|
||||||
<el-option v-for="item in personTypeOptions" :label="item" :value="item"></el-option>
|
<el-option v-for="item in personTypeOptions" :label="item" :value="item"></el-option>
|
||||||
@@ -124,14 +126,25 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
pull() {
|
pull() {
|
||||||
|
// 拉取未结束时不再打开确认框或发送重复请求。
|
||||||
|
if (this.pullLoading) return
|
||||||
this.$confirm("确定要拉取数据源吗?", "提示", {
|
this.$confirm("确定要拉取数据源吗?", "提示", {
|
||||||
type: "warning",
|
type: "warning",
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消"
|
cancelButtonText: "取消"
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.pullLoading = true
|
if (this.pullLoading) return
|
||||||
this.$axios
|
this.$set(this, "pullLoading", true)
|
||||||
|
// 全屏提示正在拉取,锁定页面操作,避免仅按钮转圈不易察觉。
|
||||||
|
const loading = this.$loading({
|
||||||
|
fullscreen: true,
|
||||||
|
lock: true,
|
||||||
|
text: "正在拉取信息中心数据,请稍候",
|
||||||
|
spinner: "el-icon-loading",
|
||||||
|
background: "rgba(0, 0, 0, 0.5)"
|
||||||
|
})
|
||||||
|
return this.$axios
|
||||||
.post("/platform/sys/data/user/pull/pullData")
|
.post("/platform/sys/data/user/pull/pullData")
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
@@ -141,7 +154,9 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.pullLoading = false
|
// 成功、业务失败或网络异常均关闭遮罩,并恢复按钮状态。
|
||||||
|
loading.close()
|
||||||
|
this.$set(this, "pullLoading", false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
execUpdate() {
|
execUpdate() {
|
||||||
|
// 人员更新未结束时不再打开确认框或提交重复请求。
|
||||||
|
if (this.updateLoading) return
|
||||||
if (!this.updateFromData.pullTime) {
|
if (!this.updateFromData.pullTime) {
|
||||||
this.$message.warning("请选择数据源时间")
|
this.$message.warning("请选择数据源时间")
|
||||||
return
|
return
|
||||||
@@ -269,8 +271,17 @@ layout("/layouts/platform.html"){
|
|||||||
type: "warning"
|
type: "warning"
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.updateLoading = true
|
if (this.updateLoading) return
|
||||||
this.$axios
|
this.$set(this, "updateLoading", true)
|
||||||
|
// 遮罩覆盖人员更新弹窗及页面,明确提示处理中并阻止重复操作。
|
||||||
|
const loading = this.$loading({
|
||||||
|
fullscreen: true,
|
||||||
|
lock: true,
|
||||||
|
text: "正在更新人员数据,请稍候",
|
||||||
|
spinner: "el-icon-loading",
|
||||||
|
background: "rgba(0, 0, 0, 0.5)"
|
||||||
|
})
|
||||||
|
return this.$axios
|
||||||
.post("/platform/sys/data/user/update/updateUserFormSource", { param: JSON.stringify(this.updateFromData) })
|
.post("/platform/sys/data/user/update/updateUserFormSource", { param: JSON.stringify(this.updateFromData) })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
@@ -280,9 +291,14 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.updateLoading = false
|
// 成功、业务失败及网络异常均释放遮罩,恢复人员更新按钮。
|
||||||
|
loading.close()
|
||||||
|
this.$set(this, "updateLoading", false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消确认不提交;请求错误提示继续由全局 axios 拦截器处理。
|
||||||
|
})
|
||||||
},
|
},
|
||||||
listUnit() {
|
listUnit() {
|
||||||
this.$businessTool.listUnit(this.pageForm.unionId).then((res) => {
|
this.$businessTool.listUnit(this.pageForm.unionId).then((res) => {
|
||||||
|
|||||||
@@ -124,7 +124,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script nonce="${cspNonce!}">
|
<script nonce="${cspNonce!}">
|
||||||
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname)
|
// 临时屏蔽指定局域网 IP 的验证码;恢复时删除 192.168.0.108,并同步删除后端对应条件。
|
||||||
|
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1", "192.168.0.108"].includes(window.location.hostname)
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
|
|||||||
@@ -155,14 +155,13 @@ layout("/layouts/platform.html"){
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="$refs.sortRef.onOpen()" size="medium" type="primary" icon="el-icon-sort">排序
|
<el-button @click="$refs.sortRef.onOpen()" size="medium" type="primary" icon="el-icon-sort">排序
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button v-if="$auth.hasPermission('sys.manager.role.user')" size="medium" type="primary"
|
||||||
|
icon="el-icon-user" :loading="memberRoleLoading" @click="doSettingMemberRole">
|
||||||
|
一键设置会员角色
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
|
|
||||||
<!-- <el-button size="medium" @click="doSettingMemberRole" type="primary"><i class="el-icon-s-tools"></i>-->
|
|
||||||
<!-- 一键设会员角色-->
|
|
||||||
<!-- </el-button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
|
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
|
||||||
<!-- <el-button size="medium" @click="doPublic" type="primary">一键设置公共角色</el-button>-->
|
<!-- <el-button size="medium" @click="doPublic" type="primary">一键设置公共角色</el-button>-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
@@ -516,6 +515,7 @@ layout("/layouts/platform.html"){
|
|||||||
roleDialogVisible: false,
|
roleDialogVisible: false,
|
||||||
menuDialogVisible: false,
|
menuDialogVisible: false,
|
||||||
userDialogVisible: false,
|
userDialogVisible: false,
|
||||||
|
memberRoleLoading: false,
|
||||||
treeData: {
|
treeData: {
|
||||||
label: "name",
|
label: "name",
|
||||||
children: "zones",
|
children: "zones",
|
||||||
@@ -689,18 +689,30 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
doSettingMemberRole() {
|
doSettingMemberRole() {
|
||||||
this.$confirm("是否确定一键设置会员角色?", "提示", {
|
if (this.memberRoleLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 操作面向全部会员,忽略当前列表筛选;确认期间也锁定按钮,避免重复提交。
|
||||||
|
this.$set(this, "memberRoleLoading", true)
|
||||||
|
this.$confirm("确定为全部已标记为会员的人员设置会员角色吗?已有角色将保留。", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(() => {
|
||||||
const {code, msg} = await this.$axios.post("/platform/sys/role/doSettingMemberRole")
|
return this.$axios.post("/platform/sys/role/doSettingMemberRole").then((res) => {
|
||||||
if (code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success(msg)
|
this.$message.success("设置完成:新增 " + res.data.addedCount
|
||||||
}
|
+ " 条,恢复启用 " + res.data.enabledCount + " 条会员角色关联")
|
||||||
|
this.pageData()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
// 取消确认无需提示;请求异常由全局 axios 拦截器统一提示。
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$set(this, "memberRoleLoading", false)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
formatAt(val) {
|
formatAt(val) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const clubUserJoin = {
|
|||||||
<div>
|
<div>
|
||||||
<div class="process-title">
|
<div class="process-title">
|
||||||
申请信息
|
申请信息
|
||||||
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
|
<el-link v-if="row.mode !== 2" type="primary" @click="openChart">点击查看流程图</el-link>
|
||||||
</div>
|
</div>
|
||||||
<el-descriptions border>
|
<el-descriptions border>
|
||||||
<el-descriptions-item label="姓名">
|
<el-descriptions-item label="姓名">
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ layout("/layouts/platform.html"){
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in clubOptions"
|
v-for="item in sortedClubOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.clubName"
|
:label="item.clubName"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
@@ -380,9 +380,14 @@ layout("/layouts/platform.html"){
|
|||||||
>
|
>
|
||||||
{{(lastClubData.year - 1) + '年度注册会员人数为' + lastClubData.history_member + ',其中拨付人数为' + lastClubData.history_y_givemoney_member}}
|
{{(lastClubData.year - 1) + '年度注册会员人数为' + lastClubData.history_member + ',其中拨付人数为' + lastClubData.history_y_givemoney_member}}
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="formData.changeUserNum" max-height="300">
|
<!-- 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。 -->
|
||||||
|
<el-alert v-if="memberStatisticsWarnings.length"
|
||||||
|
:title="memberStatisticsWarnings.join(';')"
|
||||||
|
type="warning" :closable="false" show-icon></el-alert>
|
||||||
|
<el-table :data="formData.changeUserNum" max-height="300" v-loading="memberStatisticsLoading">
|
||||||
<el-table-column label="序号" type="index" width="50"></el-table-column>
|
<el-table-column label="序号" type="index" width="50"></el-table-column>
|
||||||
<el-table-column label="人员类型" prop="userState"></el-table-column>
|
<!-- 2026年10月2日wxg屏蔽操作:<el-table-column label="人员类型" prop="userState"></el-table-column> -->
|
||||||
|
<el-table-column label="人员类型" prop="personType"></el-table-column>
|
||||||
<el-table-column label="年初人员" prop="yearFirstNum"></el-table-column>
|
<el-table-column label="年初人员" prop="yearFirstNum"></el-table-column>
|
||||||
<el-table-column label="年度增加" prop="yearAddNum"></el-table-column>
|
<el-table-column label="年度增加" prop="yearAddNum"></el-table-column>
|
||||||
<el-table-column label="年度减少" prop="yearReduceNum"></el-table-column>
|
<el-table-column label="年度减少" prop="yearReduceNum"></el-table-column>
|
||||||
@@ -651,6 +656,11 @@ layout("/layouts/platform.html"){
|
|||||||
memberList: [], // 成员名单
|
memberList: [], // 成员名单
|
||||||
lastClubData: {}, // 上年度社团数据
|
lastClubData: {}, // 上年度社团数据
|
||||||
|
|
||||||
|
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。统计加载与核查提示只服务于成员变化表。
|
||||||
|
memberStatisticsLoading: false,
|
||||||
|
memberStatisticsWarnings: [],
|
||||||
|
memberStatisticsSequence: 0,
|
||||||
|
|
||||||
// 核心表单数据
|
// 核心表单数据
|
||||||
formData: {
|
formData: {
|
||||||
year: new Date().getFullYear() + "", // 默认当前年度
|
year: new Date().getFullYear() + "", // 默认当前年度
|
||||||
@@ -678,6 +688,18 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
// 常量定义(便于维护和修改)
|
// 常量定义(便于维护和修改)
|
||||||
computed: {
|
computed: {
|
||||||
|
// 仅对本页社团选项按编码升序展示;空编码放末尾,保留接口原始数组。
|
||||||
|
sortedClubOptions() {
|
||||||
|
return (this.clubOptions || []).slice().sort((left, right) => {
|
||||||
|
const leftCode = left.clubCode == null ? "" : String(left.clubCode);
|
||||||
|
const rightCode = right.clubCode == null ? "" : String(right.clubCode);
|
||||||
|
if (!leftCode && rightCode) return 1;
|
||||||
|
if (leftCode && !rightCode) return -1;
|
||||||
|
if (leftCode < rightCode) return -1;
|
||||||
|
if (leftCode > rightCode) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
// 收支类型选项
|
// 收支类型选项
|
||||||
INCOME_TYPE_OPTIONS() {
|
INCOME_TYPE_OPTIONS() {
|
||||||
return [
|
return [
|
||||||
@@ -780,8 +802,9 @@ layout("/layouts/platform.html"){
|
|||||||
async fetchClubList() {
|
async fetchClubList() {
|
||||||
const resp = await this.$axios.post("/platform/club/examine/apply/getClubsByRole");
|
const resp = await this.$axios.post("/platform/club/examine/apply/getClubsByRole");
|
||||||
this.clubOptions = resp.data || [];
|
this.clubOptions = resp.data || [];
|
||||||
if (this.clubOptions.length > 0) {
|
if (this.sortedClubOptions.length > 0) {
|
||||||
this.formData.clubId = this.clubOptions[0].id;
|
// 新增登记默认选中编码最小的社团;编辑回显仍使用原登记社团。
|
||||||
|
this.$set(this.formData, "clubId", this.sortedClubOptions[0].id);
|
||||||
await this.handleClubChange(this.formData.clubId);
|
await this.handleClubChange(this.formData.clubId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -833,17 +856,50 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
return [...sortedDefaults, ...customItems];
|
return [...sortedDefaults, ...customItems];
|
||||||
},
|
},
|
||||||
// 获取社团成员数量
|
// 2026年10月2日wxg屏蔽操作:原统计请求逐行注释保留。
|
||||||
async fetchClubUserNum(clubId) {
|
// // 获取社团成员数量
|
||||||
const resp = await this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
|
// async fetchClubUserNum(clubId) {
|
||||||
clubId,
|
// const resp = await this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
|
||||||
year: this.formData.year
|
// clubId,
|
||||||
});
|
// year: this.formData.year
|
||||||
if (resp.code === 0) {
|
// });
|
||||||
this.formData.changeUserNum = resp.data || [];
|
// if (resp.code === 0) {
|
||||||
} else {
|
// this.formData.changeUserNum = resp.data || [];
|
||||||
this.$message.warning(resp.msg);
|
// } else {
|
||||||
|
// this.$message.warning(resp.msg);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
|
||||||
|
fetchClubUserNum(clubId) {
|
||||||
|
const sequence = ++this.memberStatisticsSequence;
|
||||||
|
this.$set(this.formData, "changeUserNum", []);
|
||||||
|
this.memberStatisticsWarnings = [];
|
||||||
|
if (!clubId || !this.formData.year) {
|
||||||
|
this.memberStatisticsLoading = false;
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
this.memberStatisticsLoading = true;
|
||||||
|
return this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
|
||||||
|
clubId: clubId,
|
||||||
|
year: this.formData.year
|
||||||
|
}).then((resp) => {
|
||||||
|
// 切换年度或社团时只接收最近一次请求,避免旧条件的结果覆盖当前统计。
|
||||||
|
if (sequence !== this.memberStatisticsSequence) return;
|
||||||
|
if (resp.code === 0) {
|
||||||
|
const rows = resp.data || [];
|
||||||
|
this.$set(this.formData, "changeUserNum", rows);
|
||||||
|
this.memberStatisticsWarnings = rows.length ? rows[0].statisticsWarnings || [] : [];
|
||||||
|
} else {
|
||||||
|
this.memberStatisticsWarnings = [resp.msg || "成员变化统计获取失败,请核查"];
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
if (sequence === this.memberStatisticsSequence) {
|
||||||
|
this.memberStatisticsWarnings = ["成员变化统计请求失败,请检查后重新选择社团或年度"];
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
if (sequence === this.memberStatisticsSequence) this.memberStatisticsLoading = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// 获取成员名单
|
// 获取成员名单
|
||||||
async fetchMemberList(clubId) {
|
async fetchMemberList(clubId) {
|
||||||
@@ -1101,13 +1157,22 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
|
|
||||||
/************************ 事件处理方法 ************************/
|
/************************ 事件处理方法 ************************/
|
||||||
// 年度变化处理
|
// 2026年10月2日wxg屏蔽操作:保留原年度切换事件。
|
||||||
handleYearChange: function() {
|
// // 年度变化处理
|
||||||
|
// handleYearChange: function() {
|
||||||
|
// this.checkIsRegistered();
|
||||||
|
// },
|
||||||
|
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。所选年度改变后同步刷新成员变化表。
|
||||||
|
handleYearChange() {
|
||||||
this.checkIsRegistered();
|
this.checkIsRegistered();
|
||||||
|
return this.fetchClubUserNum(this.formData.clubId);
|
||||||
},
|
},
|
||||||
// 社团变化处理
|
// 社团变化处理
|
||||||
async handleClubChange(val) {
|
async handleClubChange(val) {
|
||||||
if (!val) return;
|
if (!val) {
|
||||||
|
this.fetchClubUserNum(val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const club = this.clubOptions.find(item => item.id === val);
|
const club = this.clubOptions.find(item => item.id === val);
|
||||||
// 更新社团基础信息
|
// 更新社团基础信息
|
||||||
this.formData.due = club?.due || "";
|
this.formData.due = club?.due || "";
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ layout("/layouts/platform.html"){
|
|||||||
<search-item label="在职状态">
|
<search-item label="在职状态">
|
||||||
<dict-select code="USER_STATE" placeholder="请选择人员状态" clearable v-model="pageForm.userState"></dict-select>
|
<dict-select code="USER_STATE" placeholder="请选择人员状态" clearable v-model="pageForm.userState"></dict-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
|
<!-- 2026年10月3日 19:42:45;操作人:wxg:搜索按钮前提供退会日期范围,覆盖审核、系统处理及历史直接移出。 -->
|
||||||
|
<search-item label="退会时间查询">
|
||||||
|
<el-date-picker v-model="pageForm.exitDateRange" :picker-options="exitDatePickerOptions"
|
||||||
|
@change="exitDateRangeChange" type="datetimerange" clearable align="right"
|
||||||
|
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
|
||||||
|
format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%">
|
||||||
|
</el-date-picker>
|
||||||
|
</search-item>
|
||||||
</search>
|
</search>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -76,7 +84,23 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
clubList: []
|
clubList: [],
|
||||||
|
pageForm: { exitDateRange: [], exitDateStart: null, exitDateEnd: null },
|
||||||
|
// 2026年10月3日 19:42:45;操作人:wxg:复用通用查询和分页,快捷范围按自然月计算。
|
||||||
|
pageDataUrl: "/platform/club/infoManage/exitManage/pageData",
|
||||||
|
exitDatePickerOptions: {
|
||||||
|
shortcuts: [
|
||||||
|
{ text: "当前年度", onClick: picker => {
|
||||||
|
picker.$emit("pick", [this.$moment().startOf("year").toDate(), new Date()])
|
||||||
|
} },
|
||||||
|
{ text: "近半年", onClick: picker => {
|
||||||
|
picker.$emit("pick", [this.$moment().subtract(6, "months").startOf("day").toDate(), new Date()])
|
||||||
|
} },
|
||||||
|
{ text: "近三个月", onClick: picker => {
|
||||||
|
picker.$emit("pick", [this.$moment().subtract(3, "months").startOf("day").toDate(), new Date()])
|
||||||
|
} }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -85,28 +109,35 @@ layout("/layouts/platform.html"){
|
|||||||
this.$refs.infoRef.onOpen(row)
|
this.$refs.infoRef.onOpen(row)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async getClubList() {
|
getClubList() {
|
||||||
const resp = await this.$axios.post("/platform/club/infoManage/exitManage/getClubList")
|
return this.$axios.post("/platform/club/infoManage/exitManage/getClubList").then(resp => {
|
||||||
if (resp.code === 0) {
|
if (resp.code === 0) {
|
||||||
this.$set(this, "clubList", resp.data)
|
// 仅调整当前页面的社团选项顺序;编码为空时排在有编码的社团之后。
|
||||||
}
|
const clubs = (resp.data || []).slice().sort((left, right) => {
|
||||||
|
const leftCode = left.clubCode == null ? "" : String(left.clubCode)
|
||||||
|
const rightCode = right.clubCode == null ? "" : String(right.clubCode)
|
||||||
|
if (!leftCode && rightCode) return 1
|
||||||
|
if (leftCode && !rightCode) return -1
|
||||||
|
return leftCode < rightCode ? -1 : leftCode > rightCode ? 1 : 0
|
||||||
|
})
|
||||||
|
this.$set(this, "clubList", clubs)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
exitTypeName(row) {
|
exitTypeName(row) {
|
||||||
|
// 2026年10月3日 19:42:45;操作人:wxg:系统处理退会独立标识,保留原有两类名称。
|
||||||
|
if (row.exitType === "SYSTEM_EXIT") return "系统处理"
|
||||||
return row.exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
|
return row.exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
|
||||||
},
|
},
|
||||||
async pageData() {
|
/** 2026年10月3日 19:42:45;操作人:wxg:清空范围时同时清空接口参数,筛选后由 mixin 重置页码。 */
|
||||||
const resp = await this.$axios.post("/platform/club/infoManage/exitManage/pageData", this.pageForm)
|
exitDateRangeChange(val) {
|
||||||
if (resp.code === 0) {
|
this.$set(this.pageForm, "exitDateStart", val && val.length === 2 ? val[0] : null)
|
||||||
this.$set(this, "tableData", resp.data.list)
|
this.$set(this.pageForm, "exitDateEnd", val && val.length === 2 ? val[1] : null)
|
||||||
this.$set(this.pageForm, "totalCount", resp.data.totalCount)
|
this.doSearch()
|
||||||
} else {
|
|
||||||
this.$message.warning(resp.msg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
created() {
|
||||||
await this.getClubList()
|
this.getClubList().then(() => this.pageData())
|
||||||
await this.pageData()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ const clubExitInfo = {
|
|||||||
},
|
},
|
||||||
getExitTypeName() {
|
getExitTypeName() {
|
||||||
const exitType = this.row.exitType || this.viewData.exitType
|
const exitType = this.row.exitType || this.viewData.exitType
|
||||||
|
// 2026年10月3日 19:42:45;操作人:wxg:详情与列表保持一致,mode=2 显示系统处理。
|
||||||
|
if (exitType === "SYSTEM_EXIT") return "系统处理"
|
||||||
return exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
|
return exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
|
||||||
},
|
},
|
||||||
openChart() {
|
openChart() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CLUB_INFO_MANAGE_TEMPLATE = {
|
const CLUB_INFO_MANAGE_TEMPLATE = {
|
||||||
template: `
|
template: `
|
||||||
<div class="club-manage-panel">
|
<div class="club-manage-panel">
|
||||||
<el-card class="club-manage-query-card" shadow="never">
|
<el-card class="club-manage-query-card" shadow="never">
|
||||||
@@ -7,29 +7,44 @@
|
|||||||
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
|
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="人员类型">
|
<search-item label="人员类型">
|
||||||
<dict-select code="USER_PERSON_TYPE" placeholder="请选择人员类型" clearable v-model="pageForm.personType"></dict-select>
|
<dict-select code="USER_PERSON_TYPE" placeholder="请选择人员类型" clearable v-model="pageForm.personType" :disabled="pageForm.focusMembers"></dict-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="在职状态">
|
<search-item label="在职状态">
|
||||||
<dict-select clearable code="USER_STATE" placeholder="请选择人员状态" v-model="pageForm.userState"></dict-select>
|
<dict-select clearable code="USER_STATE" placeholder="请选择人员状态" v-model="pageForm.userState"></dict-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
|
<!-- 2026年10月3日 19:28:31;操作人:wxg:查询标签明确为入会时间,日期筛选逻辑保持不变。 -->
|
||||||
|
<search-item label="入会时间查询" class="club-join-date-query">
|
||||||
|
<el-date-picker v-model="pageForm.joinDateRange" :picker-options="joinDatePickerOptions"
|
||||||
|
@change="joinDateRangeChange" type="datetimerange" clearable align="right"
|
||||||
|
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
|
||||||
|
format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%">
|
||||||
|
</el-date-picker>
|
||||||
|
</search-item>
|
||||||
</search>
|
</search>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" class="mt10 club-manage-list-card">
|
<el-card shadow="never" class="mt10 club-manage-list-card">
|
||||||
<table-tool label="会员信息">
|
<table-tool label="会员信息">
|
||||||
|
<template slot="content">
|
||||||
|
<el-tag :effect="pageForm.focusMembers ? 'dark' : 'plain'" @click="toggleFocusMembers"
|
||||||
|
class="mr10" style="cursor: pointer">重点关注名单</el-tag>
|
||||||
|
<span class="text-danger">提醒:再次点击可切换</span>
|
||||||
|
</template>
|
||||||
<template v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])">
|
<template v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])">
|
||||||
<el-button size="small" type="primary" icon="el-icon-download" @click="exportRegistrationDoc">导出登记表</el-button>
|
<el-button size="small" type="primary" icon="el-icon-download" @click="exportRegistrationDoc">导出登记表</el-button>
|
||||||
<el-button @click="openImport" size="small" type="primary">导入成员</el-button>
|
<el-button @click="openImport" size="small" type="primary">导入成员</el-button>
|
||||||
<el-button size="small" type="primary" @click="openUserAdd(true)">录入成员</el-button>
|
<el-button size="small" type="primary" @click="openUserAdd(true)">录入成员</el-button>
|
||||||
<el-button size="small" type="primary" @click="openUserAdd(false)">设置理事机构</el-button>
|
<el-button size="small" type="primary" @click="openUserAdd(false)">设置理事机构</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-radio-group @change="doSearch" style="margin-left: 10px" v-model="pageForm.radioType" size="small">
|
<el-radio-group @change="memberTabChange" style="margin-left: 10px" v-model="pageForm.radioType" size="small">
|
||||||
<el-radio-button label="3">全部</el-radio-button>
|
<el-radio-button label="3">全部</el-radio-button>
|
||||||
<el-radio-button label="2">社团成员</el-radio-button>
|
<!-- 修改时间:2026-10-03 12:47:38;操作人:wxg;会员页签统一使用“会员”称谓,保留原筛选值。 -->
|
||||||
|
<el-radio-button label="2">社团会员</el-radio-button>
|
||||||
<el-radio-button label="1">理事机构</el-radio-button>
|
<el-radio-button label="1">理事机构</el-radio-button>
|
||||||
|
<el-radio-button label="4">历史会员</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
|
<el-table :key="pageForm.radioType" v-loading="tableLoading" :data="tableData" :height="tableHeight"
|
||||||
row-key="id" ref="tableRef" @sort-change='pageOrder'>
|
row-key="id" ref="tableRef" @sort-change='pageOrder'>
|
||||||
<el-table-column :index="indexMethod" label="序号" type="index" width="70"></el-table-column>
|
<el-table-column :index="indexMethod" label="序号" type="index" width="70"></el-table-column>
|
||||||
<el-table-column label="工号" prop="loginName" show-overflow-tooltip sortable></el-table-column>
|
<el-table-column label="工号" prop="loginName" show-overflow-tooltip sortable></el-table-column>
|
||||||
@@ -37,11 +52,15 @@
|
|||||||
<el-table-column label="性别" prop="sex" show-overflow-tooltip sortable></el-table-column>
|
<el-table-column label="性别" prop="sex" show-overflow-tooltip sortable></el-table-column>
|
||||||
<el-table-column label="电话" prop="mobile" show-overflow-tooltip></el-table-column>
|
<el-table-column label="电话" prop="mobile" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column label="人员类型" prop="personType" show-overflow-tooltip sortable></el-table-column>
|
<el-table-column label="人员类型" prop="personType" show-overflow-tooltip sortable></el-table-column>
|
||||||
|
<!-- 展示用户当前人员属性,便于结合人员类型及在职状态核实会员信息。 -->
|
||||||
|
<el-table-column label="人员属性" prop="userAttribute" show-overflow-tooltip sortable></el-table-column>
|
||||||
<el-table-column label="在职状态" prop="userState" show-overflow-tooltip sortable></el-table-column>
|
<el-table-column label="在职状态" prop="userState" show-overflow-tooltip sortable></el-table-column>
|
||||||
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
|
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column label="身份" prop="roleName" show-overflow-tooltip></el-table-column>
|
<el-table-column label="身份" prop="roleName" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column label="入会时间" prop="joinTime" show-overflow-tooltip sortable></el-table-column>
|
<el-table-column label="入会时间" prop="joinTime" show-overflow-tooltip sortable></el-table-column>
|
||||||
<el-table-column label="操作">
|
<!-- 历史成员仅供查询,退出时间取申请记录,不提供会员关系修改入口。 -->
|
||||||
|
<el-table-column v-if="pageForm.radioType === '4'" label="退出时间" prop="exitTime" show-overflow-tooltip sortable></el-table-column>
|
||||||
|
<el-table-column v-if="pageForm.radioType !== '4'" label="操作">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<el-dropdown>
|
<el-dropdown>
|
||||||
<el-button size="mini">
|
<el-button size="mini">
|
||||||
@@ -54,8 +73,11 @@
|
|||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<!--<el-dropdown-item @click.native="viewInfo(row)">查看注册信息</el-dropdown-item>
|
<!--<el-dropdown-item @click.native="viewInfo(row)">查看注册信息</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="exportInfo(row)">导出登记表</el-dropdown-item>-->
|
<el-dropdown-item @click.native="exportInfo(row)">导出登记表</el-dropdown-item>-->
|
||||||
|
<!-- 临时屏蔽删除入口,保留原调用以便后续恢复。
|
||||||
<el-dropdown-item @click.native="userDelete(row)">删除</el-dropdown-item>
|
<el-dropdown-item @click.native="userDelete(row)">删除</el-dropdown-item>
|
||||||
<!--<el-dropdown-item @click.native="exitClub(row)">退会</el-dropdown-item>-->
|
-->
|
||||||
|
<!-- 管理端直接退会沿用现有接口,提交期间禁用入口以防重复操作。 -->
|
||||||
|
<el-dropdown-item :disabled="formLoading" @click.native="exitClub(row)">退会</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
@@ -157,6 +179,37 @@
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
pageForm: {
|
||||||
|
focusMembers: false,
|
||||||
|
joinDateRange: [],
|
||||||
|
joinDateStart: null,
|
||||||
|
joinDateEnd: null,
|
||||||
|
pageOrderName: "joinTime",
|
||||||
|
pageOrderBy: "descending"
|
||||||
|
},
|
||||||
|
// 快捷范围按自然月计算,月底由 moment 自动调整为目标月份的有效日期。
|
||||||
|
joinDatePickerOptions: {
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
text: "当前年度",
|
||||||
|
onClick: picker => {
|
||||||
|
picker.$emit("pick", [this.$moment().startOf("year").toDate(), new Date()])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "近半年",
|
||||||
|
onClick: picker => {
|
||||||
|
picker.$emit("pick", [this.$moment().subtract(6, "months").startOf("day").toDate(), new Date()])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "近三个月",
|
||||||
|
onClick: picker => {
|
||||||
|
picker.$emit("pick", [this.$moment().subtract(3, "months").startOf("day").toDate(), new Date()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
roleCodeFormData: {},
|
roleCodeFormData: {},
|
||||||
roleCodeFormRules: {
|
roleCodeFormRules: {
|
||||||
userName: [{ required: true, message: "必填", trigger: ["blur"] }],
|
userName: [{ required: true, message: "必填", trigger: ["blur"] }],
|
||||||
@@ -183,6 +236,28 @@
|
|||||||
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime())
|
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime())
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 启用重点关注时同时筛选自聘人员和其他人员,清空单选条件以避免冲突;取消后恢复普通查询。 */
|
||||||
|
toggleFocusMembers() {
|
||||||
|
this.$set(this.pageForm, "focusMembers", !this.pageForm.focusMembers)
|
||||||
|
if (this.pageForm.focusMembers) {
|
||||||
|
this.$set(this.pageForm, "personType", "")
|
||||||
|
}
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
|
/** 页签切换只调整业务排序,查询和重置页码继续复用 initTableMixins。 */
|
||||||
|
memberTabChange() {
|
||||||
|
this.$set(this.pageForm, "pageOrderName", this.pageForm.radioType === "4" ? "exitTime" : "joinTime")
|
||||||
|
this.$set(this.pageForm, "pageOrderBy", "descending")
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
|
/** 日期范围始终筛选入会时间;当前会员按入会时间倒序,历史成员按退出时间倒序。 */
|
||||||
|
joinDateRangeChange(val) {
|
||||||
|
this.$set(this.pageForm, "joinDateStart", val && val.length === 2 ? val[0] : null)
|
||||||
|
this.$set(this.pageForm, "joinDateEnd", val && val.length === 2 ? val[1] : null)
|
||||||
|
this.$set(this.pageForm, "pageOrderName", this.pageForm.radioType === "4" ? "exitTime" : "joinTime")
|
||||||
|
this.$set(this.pageForm, "pageOrderBy", "descending")
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
openImport() {
|
openImport() {
|
||||||
this.importData = {
|
this.importData = {
|
||||||
isfg: 1,
|
isfg: 1,
|
||||||
@@ -316,21 +391,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async exitClub(row) {
|
exitClub(row) {
|
||||||
const confirm = await this.$confirm("确定要将其退会吗?", "提示", {
|
if (this.formLoading) return
|
||||||
|
return this.$confirm("确定要将其退会吗?", "提示", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: "取消",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
|
}).then(() => {
|
||||||
|
if (this.formLoading) return
|
||||||
|
this.$set(this, "formLoading", true)
|
||||||
|
return this.$axios.post("/platform/club/infoManage/manage/exitClub", { id: row.id }).then(resp => {
|
||||||
|
if (resp.code === 0) {
|
||||||
|
this.$message.success(resp.msg)
|
||||||
|
return this.pageData()
|
||||||
|
} else {
|
||||||
|
this.$message.warning(resp.msg)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
// 接口或列表刷新异常时也释放提交状态,允许后续操作。
|
||||||
|
this.$set(this, "formLoading", false)
|
||||||
|
})
|
||||||
|
}, () => {
|
||||||
|
// 取消或关闭确认框时,不发送退会请求。
|
||||||
})
|
})
|
||||||
if (confirm === "confirm") {
|
|
||||||
const resp = await this.$axios.post("/platform/club/infoManage/manage/exitClub", { id: row.id })
|
|
||||||
if (resp.code === 0) {
|
|
||||||
this.$message.success(resp.msg)
|
|
||||||
await this.pageData()
|
|
||||||
} else {
|
|
||||||
this.$message.warning(resp.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
//导出登记表
|
//导出登记表
|
||||||
@@ -369,4 +452,3 @@
|
|||||||
this.pageData()
|
this.pageData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ layout("/layouts/platform.html"){
|
|||||||
#app .club-manage-query-card {
|
#app .club-manage-query-card {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
/* 日期范围框预留约 320px,包含标签的查询项同步加宽;窄屏仍可换行并适配容器。 */
|
||||||
|
#app .club-manage-query-card .search .club-join-date-query {
|
||||||
|
flex: 0 0 420px;
|
||||||
|
width: 420px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
#app .club-manage-query-card .club-join-date-query .search-item-option {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
#app .club-manage-list-card {
|
#app .club-manage-list-card {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -71,6 +81,7 @@ layout("/layouts/platform.html"){
|
|||||||
<div class="club-tree-scroll">
|
<div class="club-tree-scroll">
|
||||||
<el-tree
|
<el-tree
|
||||||
:data="treeData"
|
:data="treeData"
|
||||||
|
node-key="id"
|
||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:props="{children: 'children', label: 'clubName'}"
|
:props="{children: 'children', label: 'clubName'}"
|
||||||
@@ -221,12 +232,24 @@ layout("/layouts/platform.html"){
|
|||||||
this.currentTreeData = data
|
this.currentTreeData = data
|
||||||
this.currentTreeNode = node
|
this.currentTreeNode = node
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.clubInfo.pageData()
|
// 一级节点展示社团总览,只有具体社团节点才需要刷新会员列表。
|
||||||
|
if (node.level === 2 && this.$refs.clubInfo) {
|
||||||
|
this.$refs.clubInfo.pageData()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getTreeData() {
|
getTreeData() {
|
||||||
this.$axios.post("/platform/club/infoManage/manage/getClubTreeData").then((res) => {
|
this.$axios.post("/platform/club/infoManage/manage/getClubTreeData").then((res) => {
|
||||||
this.treeData = res.data
|
this.treeData = res.data
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// 首次进入时选中一级节点,同步树高亮与右侧总览;后续刷新保留用户选择。
|
||||||
|
if (!this.currentTreeNode && this.treeData && this.treeData.length > 0) {
|
||||||
|
const rootData = this.treeData[0]
|
||||||
|
this.$refs.treeRef.setCurrentKey(rootData.id)
|
||||||
|
this.$set(this, "currentTreeData", rootData)
|
||||||
|
this.$set(this, "currentTreeNode", this.$refs.treeRef.getNode(rootData.id))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async userRemoteMethod(query) {
|
async userRemoteMethod(query) {
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
|
|||||||
</el-card>
|
</el-card>
|
||||||
<el-card shadow="never" class="mt10 club-manage-list-card">
|
<el-card shadow="never" class="mt10 club-manage-list-card">
|
||||||
<table-tool label="社团列表">
|
<table-tool label="社团列表">
|
||||||
<template>
|
<template slot="content">
|
||||||
|
<!-- 一级节点展示社团总览时,引导用户从左侧树选择社团查看详细信息。 -->
|
||||||
|
<span v-if="parentNode.currentTreeNode && parentNode.currentTreeNode.level === 1"
|
||||||
|
style="display: block; text-align: center; color: #f56c6c;">
|
||||||
|
温馨提醒:选择左侧树中的某个社团,可查询该社团的详细信息。
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
|
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ layout("/layouts/platform.html"){
|
|||||||
@change="doSearch"
|
@change="doSearch"
|
||||||
></el-date-picker>
|
></el-date-picker>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="社团名称" v-if="$auth.hasRoleOr(['SYSADMIN'])">
|
<search-item label="社团名称">
|
||||||
<el-select clearable filterable placeholder="请选择社团" @change="doSearch"
|
<el-select clearable filterable placeholder="请选择社团" @change="doSearch"
|
||||||
style="width: 100%;" v-model="pageForm.clubId">
|
style="width: 100%;" v-model="pageForm.clubId">
|
||||||
<el-option :label="item.clubName" :value="item.id"
|
<el-option :label="item.clubName" :value="item.id"
|
||||||
v-for="item in clubs"></el-option>
|
v-for="item in clubs"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="关键字">
|
<search-item label="姓名工号">
|
||||||
<el-input v-model="pageForm.searchKeyword" placeholder="请输入工号或者姓名查询"
|
<el-input v-model="pageForm.searchKeyword" placeholder="请输入工号或者姓名查询"
|
||||||
clearable @keyup.enter="doSearch"></el-input>
|
clearable @keyup.enter="doSearch"></el-input>
|
||||||
</search-item>
|
</search-item>
|
||||||
@@ -61,8 +61,14 @@ layout("/layouts/platform.html"){
|
|||||||
<el-button @click="batchOperatePay" size="small" type="primary">
|
<el-button @click="batchOperatePay" size="small" type="primary">
|
||||||
批量设置
|
批量设置
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<!-- 2026年10月3日 14:33:18;操作人:wxg:筛选当前会员状态,按钮风格沿用历史拨付页面,首次进入默认在会。 -->
|
||||||
|
<el-radio-group v-model="pageForm.memberState" size="small" @change="memberStateChange" class="ml5">
|
||||||
|
<el-radio-button label="">全部</el-radio-button>
|
||||||
|
<el-radio-button label="1">在会</el-radio-button>
|
||||||
|
<el-radio-button label="0">退会</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table :data="tableData" :height="tableHeight" row-key="id" @selection-change="handleSelectionChange" ref="tableRef">
|
<el-table :data="tableData" :height="tableHeight" v-loading="tableLoading" row-key="id" @selection-change="handleSelectionChange" ref="tableRef">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:reserve-selection="true"
|
:reserve-selection="true"
|
||||||
type="selection"
|
type="selection"
|
||||||
@@ -75,15 +81,16 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
|
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column label="加入时间" prop="applyTime" sortable show-overflow-tooltip>
|
<el-table-column label="加入时间" prop="applyTime" sortable show-overflow-tooltip>
|
||||||
<template v-slot="{ row }">
|
<template v-slot="{ row }">
|
||||||
{{ $moment(row.applyTime).format('YYYY-MM-DD HH:mm:ss') }}
|
{{ row.applyTime ? $moment(row.applyTime).format('YYYY-MM-DD') : '' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="在职状态" prop="userState" sortable show-overflow-tooltip>
|
<!-- 离退休人员有退休日期时补充退休年份;其他类型或日期缺失时保留原值。 -->
|
||||||
|
<el-table-column label="人员类型" prop="personType" min-width="120" sortable show-overflow-tooltip>
|
||||||
<template v-slot="{ row }">
|
<template v-slot="{ row }">
|
||||||
<span v-if="row.userState === '退休' && row.retireDate">
|
<span v-if="row.personType === '离退休人员' && row.retireDate">
|
||||||
{{ row.userState + '(' + row.retireDate + ')' }}
|
{{ row.personType + '(' + $moment(row.retireDate).format('YYYY') + ')' }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ row.userState }}</span>
|
<span v-else>{{ row.personType }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="当前状态" prop="count" sortable show-overflow-tooltip>
|
<el-table-column label="当前状态" prop="count" sortable show-overflow-tooltip>
|
||||||
@@ -97,10 +104,12 @@ layout("/layouts/platform.html"){
|
|||||||
<el-tooltip placement="bottom">
|
<el-tooltip placement="bottom">
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
是否具备拨付资格,将按以下规则判定:<br/>
|
是否具备拨付资格,将按以下规则判定:<br/>
|
||||||
第一步:缴费记录必须是已缴费状态,否则直接判定无资格;<br/>
|
第一步:当前状态为“退会”,直接判定不拨付;<br/>
|
||||||
第二步:如果在职状态不是“退休”,则直接判定有领取资格;<br/>
|
第二步:缴费记录必须是已缴费状态,否则判定不拨付;<br/>
|
||||||
第三步:如果是“退休”状态但未有退休日期信息,判定无领取资格;<br/>
|
第三步:如果人员类型不是“离退休人员”,则直接判定有领取资格;<br/>
|
||||||
第四步:如果是“退休”状态且填写了退休日期,当您的退休年份 ≥ 缴费记录年份(无缴费年份则按今年算)时,判定有资格,反之则无。
|
第四步:如果人员类型为“离退休人员”但未有退休日期信息,判定无领取资格;<br/>
|
||||||
|
第五步:如果人员类型为“离退休人员”且填写了退休日期,当您的退休年份 ≥ 缴费记录年份(无缴费年份则按今年算)时,判定有资格,反之则无。<br/>
|
||||||
|
修改缴费状态时重新计算并保存;退会时保存为不拨付。列表读取已保存的拨付结果。
|
||||||
</div>
|
</div>
|
||||||
<span>拨付状态<i class="el-icon-question"></i></span>
|
<span>拨付状态<i class="el-icon-question"></i></span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -153,7 +162,7 @@ layout("/layouts/platform.html"){
|
|||||||
|
|
||||||
<span class="dialog-footer" slot="footer">
|
<span class="dialog-footer" slot="footer">
|
||||||
<el-button @click="row = {}; payDialogVisible = false">取消</el-button>
|
<el-button @click="row = {}; payDialogVisible = false">取消</el-button>
|
||||||
<el-button @click="onBatchHandle" type="primary">确定</el-button>
|
<el-button @click="onBatchHandle" :loading="formLoading" type="primary">确定</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +179,10 @@ layout("/layouts/platform.html"){
|
|||||||
return {
|
return {
|
||||||
pageForm: {
|
pageForm: {
|
||||||
payed: null,
|
payed: null,
|
||||||
|
// 2026年10月3日 14:33:18;操作人:wxg:列表默认锚点及首次请求均为“在会”。
|
||||||
|
memberState: '1',
|
||||||
year: new Date().getFullYear() + '',
|
year: new Date().getFullYear() + '',
|
||||||
|
clubId: null,
|
||||||
},
|
},
|
||||||
clubs: [],
|
clubs: [],
|
||||||
row: {},
|
row: {},
|
||||||
@@ -178,9 +190,16 @@ layout("/layouts/platform.html"){
|
|||||||
payDialogVisible: false,
|
payDialogVisible: false,
|
||||||
payed: false,
|
payed: false,
|
||||||
multipleSelection: [],
|
multipleSelection: [],
|
||||||
|
pageRequestSequence: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 2026年10月3日 14:33:18;操作人:wxg:切换会员状态清空原勾选;复用 mixin 搜索方法回到第一页。
|
||||||
|
memberStateChange() {
|
||||||
|
this.$set(this, "multipleSelection", [])
|
||||||
|
if (this.$refs.tableRef) this.$refs.tableRef.clearSelection()
|
||||||
|
this.doSearch()
|
||||||
|
},
|
||||||
handleSelectionChange(val) {
|
handleSelectionChange(val) {
|
||||||
this.multipleSelection = val
|
this.multipleSelection = val
|
||||||
},
|
},
|
||||||
@@ -221,48 +240,64 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
this.operateRequest(data)
|
this.operateRequest(data)
|
||||||
},
|
},
|
||||||
|
// 2026年10月3日 09:51:00;操作人:wxg:缴费设置会同步重算并保存拨付状态;列表刷新读取保存结果。
|
||||||
operateRequest(data) {
|
operateRequest(data) {
|
||||||
let message
|
let message
|
||||||
if (this.payed === false) {
|
if (this.payed === false) {
|
||||||
message = '您确定要设置为未缴费吗,此操作将影响该成员的拨付状态。'
|
message = '您确定要设置为未缴费吗?此操作将同步保存为不拨付。'
|
||||||
} else {
|
} else {
|
||||||
message = '您确定要设置为缴费吗?'
|
message = '您确定要设置为已缴费吗?此操作将按退会状态、人员类型及退休年份重新计算并保存拨付状态。'
|
||||||
}
|
}
|
||||||
this.$confirm(message, '温馨提示', {
|
this.$confirm(message, '温馨提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(() => {
|
||||||
const resp = await this.$axios.post("/platform/club/pay/handle", {
|
this.formLoading = true
|
||||||
|
return this.$axios.post("/platform/club/pay/handle", {
|
||||||
ids: JSON.stringify(data),
|
ids: JSON.stringify(data),
|
||||||
payed: this.payed,
|
payed: this.payed,
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp.code === 0) {
|
||||||
|
this.$message.success(resp.msg)
|
||||||
|
this.doSearch()
|
||||||
|
this.payDialogVisible = false
|
||||||
|
this.multipleSelection = []
|
||||||
|
this.$refs.tableRef.clearSelection()
|
||||||
|
} else {
|
||||||
|
this.$message.error(resp.msg)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.formLoading = false
|
||||||
})
|
})
|
||||||
if (resp.code === 0) {
|
|
||||||
this.$message.success(resp.msg)
|
|
||||||
this.doSearch()
|
|
||||||
this.payDialogVisible = false
|
|
||||||
this.multipleSelection = []
|
|
||||||
this.$refs.tableRef.clearSelection()
|
|
||||||
} else {
|
|
||||||
this.$message.error(resp.msg)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
},
|
},
|
||||||
async pageData() {
|
// 每次只请求当前分页,连续切换条件时仅接收最后一次响应,避免旧页覆盖新结果。
|
||||||
const resp = await this.$axios.post("/platform/club/pay/pageData", this.pageForm)
|
pageData() {
|
||||||
if (resp.code === 0) {
|
const requestSequence = ++this.pageRequestSequence
|
||||||
this.tableData = resp.data.list
|
this.tableLoading = true
|
||||||
this.pageForm.totalCount = resp.data.totalCount
|
return this.$axios.post("/platform/club/pay/pageData", this.pageForm).then(resp => {
|
||||||
} else {
|
if (requestSequence !== this.pageRequestSequence) return
|
||||||
this.$message.warning(resp.msg)
|
if (resp.code === 0) {
|
||||||
}
|
this.tableData = resp.data.list || []
|
||||||
|
this.$set(this.pageForm, "totalCount", resp.data.totalCount || 0)
|
||||||
|
this.updateClubInfoListTableHeight()
|
||||||
|
} else {
|
||||||
|
this.$message.warning(resp.msg)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
if (requestSequence === this.pageRequestSequence) this.tableLoading = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
created() {
|
||||||
this.clubs = await this.$businessTool.listClubByRole()
|
// 下拉选项与缴费列表共用后端授权范围,不复用其他业务的社团权限口径。
|
||||||
await this.pageData()
|
this.$axios.post("/platform/club/pay/listClubs").then(resp => {
|
||||||
|
if (resp.code === 0) this.clubs = resp.data || []
|
||||||
|
})
|
||||||
|
this.pageData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="mode" label="申请模式">
|
<el-table-column prop="mode" label="申请模式">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
|
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
|
||||||
<el-tag size="mini" v-else type="danger">退出</el-tag>
|
<el-tag size="mini" v-else type="danger">退出</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -90,7 +90,7 @@ layout("/layouts/platform.html"){
|
|||||||
{{formData.taskName}}
|
{{formData.taskName}}
|
||||||
</div>
|
</div>
|
||||||
<el-form :model="formData" ref="formRef" label-width="80px" :rules="formRules" label-suffix="">
|
<el-form :model="formData" ref="formRef" label-width="80px" :rules="formRules" label-suffix="">
|
||||||
<el-form-item v-if="formData.mode === true" label="缴费状态" prop="payed"
|
<el-form-item v-if="formData.mode === 1" label="缴费状态" prop="payed"
|
||||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||||
<span class="form-item-tooltip">
|
<span class="form-item-tooltip">
|
||||||
(说明:请确认{{ formData.userName }}当年是否缴费)
|
(说明:请确认{{ formData.userName }}当年是否缴费)
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column prop="clubName" label="社团名称"></el-table-column>
|
<el-table-column prop="clubName" label="社团名称"></el-table-column>
|
||||||
<el-table-column prop="mode" label="申请模式">
|
<el-table-column prop="mode" label="申请模式">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
|
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
|
||||||
<el-tag size="mini" v-else type="danger">退出</el-tag>
|
<el-tag size="mini" v-else-if="scope.row.mode === 0" type="danger">退出</el-tag>
|
||||||
|
<!-- 系统直接退会以 mode=2 标识,不属于待提交的 WF 申请。 -->
|
||||||
|
<el-tag size="mini" v-else-if="scope.row.mode === 2" type="info">系统处理</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="applyDate" label="申请时间"></el-table-column>
|
<el-table-column prop="applyDate" label="申请时间"></el-table-column>
|
||||||
@@ -42,10 +44,10 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column label="操作" width="300">
|
<el-table-column label="操作" width="300">
|
||||||
<template v-slot="{row}">
|
<template v-slot="{row}">
|
||||||
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
|
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
|
||||||
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === true" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 1" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||||
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === false" @click="exitSubmitAgain(row)" size="mini" type="primary">提交</el-button>
|
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 0" @click="exitSubmitAgain(row)" size="mini" type="primary">提交</el-button>
|
||||||
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="onRevoke(row)">撤回</el-button>
|
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="onRevoke(row)">撤回</el-button>
|
||||||
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)" size="mini" type="danger">
|
<el-button v-if="row.mode !== 2 && (row.taskKey === 'startTask' || !row.instanceId)" @click="onDelete(row.id)" size="mini" type="danger">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="mode" label="申请模式">
|
<el-table-column prop="mode" label="申请模式">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
|
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
|
||||||
<el-tag size="mini" v-else type="danger">退出</el-tag>
|
<el-tag size="mini" v-else type="danger">退出</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|||||||
+496
-262
File diff suppressed because it is too large
Load Diff
+29
-29
@@ -13,24 +13,24 @@ layout("/layouts/platform.html"){
|
|||||||
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
|
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="所属工会" v-if="isAdmin">
|
<search-item label="所属工会" v-if="isAdmin">
|
||||||
<el-select v-model="pageForm.unionId" @change="doSearch" style="width: 100%"
|
<el-select v-model="pageForm.unionId" @change="doSearch" style="width: 100%"
|
||||||
placeholder="请选择所属工会" clearable>
|
placeholder="请选择所属工会" clearable>
|
||||||
<el-option v-for="item in unionOptions"
|
<el-option v-for="item in unionOptions"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
></el-option>
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="所属单位" v-if="isAdmin">
|
<search-item label="所属单位" v-if="isAdmin">
|
||||||
<el-select v-model="pageForm.unitId" @change="doSearch" style="width: 100%"
|
<el-select v-model="pageForm.unitId" @change="doSearch" style="width: 100%"
|
||||||
placeholder="请选择所属单位" clearable>
|
placeholder="请选择所属单位" clearable>
|
||||||
<el-option v-for="item in unitOptions"
|
<el-option v-for="item in unitOptions"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
></el-option>
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</search-item>
|
</search-item>
|
||||||
<search-item label="报销项目">
|
<search-item label="报销项目">
|
||||||
<el-select v-model="pageForm.reimburseProject" @change="doSearch" style="width: 100%"
|
<el-select v-model="pageForm.reimburseProject" @change="doSearch" style="width: 100%"
|
||||||
@@ -95,9 +95,13 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column label="操作" fixed="right" width="350px">
|
<el-table-column label="操作" fixed="right" width="350px">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
||||||
<el-button v-if="isAdmin && row.stateId == 3" @click="openEdit(row)" size="mini" type="primary">实际金额</el-button>
|
<el-button v-if="isAdmin && row.stateId == 3" @click="openEdit(row)" size="mini" type="primary">
|
||||||
<el-button v-if="row.stateId == 3" @click="doPrint(row)" size="mini" type="primary">打印</el-button>
|
实际金额
|
||||||
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
</el-button>
|
||||||
|
<el-button @click="doPrint(row)" size="mini" type="primary">打印</el-button>
|
||||||
|
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="onDelete(row.id)" size="mini" type="danger">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -124,13 +128,12 @@ layout("/layouts/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script nonce="${cspNonce!}">
|
<script nonce="${cspNonce!}">
|
||||||
<!--#include('../info.js'){}#-->
|
<!--#include('../info.js'){}#-->
|
||||||
new Vue({
|
new Vue({
|
||||||
el: "#app",
|
el: "#app",
|
||||||
store,
|
store,
|
||||||
dicts: ["UNION_REIMBURSE_TYPE","UNION_REIMBURSE_PROJECT"],
|
dicts: ["UNION_REIMBURSE_TYPE", "UNION_REIMBURSE_PROJECT"],
|
||||||
//分页数据
|
//分页数据
|
||||||
mixins: [initTableMixins],
|
mixins: [initTableMixins],
|
||||||
components: {
|
components: {
|
||||||
@@ -149,8 +152,8 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
editFormRules: {
|
editFormRules: {
|
||||||
realMoney: [
|
realMoney: [
|
||||||
{ required: true, message: '请输入实际报销金额', trigger: 'blur' },
|
{required: true, message: '请输入实际报销金额', trigger: 'blur'},
|
||||||
{ pattern: /^([0-9]+)(\.[0-9]{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur' }
|
{pattern: /^([0-9]+)(\.[0-9]{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur'}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +207,7 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onView(row) {
|
onView(row) {
|
||||||
this.$refs.guava.edit(()=>{
|
this.$refs.guava.edit(() => {
|
||||||
this.showApprovalForm = false
|
this.showApprovalForm = false
|
||||||
this.$refs.unionReimburseInfoRef.onOpen(row)
|
this.$refs.unionReimburseInfoRef.onOpen(row)
|
||||||
})
|
})
|
||||||
@@ -266,11 +269,8 @@ layout("/layouts/platform.html"){
|
|||||||
window.open('/platform/unionReimburse/collect/exportSkr?year=' + year + "&unionId=" + unionId + '&unitId=' + unitId + "&reimburseProject=" + reimburseProject)
|
window.open('/platform/unionReimburse/collect/exportSkr?year=' + year + "&unionId=" + unionId + '&unitId=' + unitId + "&reimburseProject=" + reimburseProject)
|
||||||
},
|
},
|
||||||
doPrint(row) {
|
doPrint(row) {
|
||||||
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
|
// 所有状态均可下载审批单 PDF,复用公共下载的 POST 请求、错误提示和 loading 收尾。
|
||||||
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '') + "&Print=true"
|
this.$downLoad("/platform/unionReimburse/collect/exportPdf", {id: row.id})
|
||||||
} else {
|
|
||||||
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '') + "&Print=true"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onExport() {
|
onExport() {
|
||||||
this.$downLoad('/platform/unionReimburse/collect/onExport', this.pageForm)
|
this.$downLoad('/platform/unionReimburse/collect/onExport', this.pageForm)
|
||||||
|
|||||||
@@ -130,38 +130,38 @@ const unionReimburseInfo = {
|
|||||||
<span>{{ viewData.condolenceTime | dateFormat }}</span>
|
<span>{{ viewData.condolenceTime | dateFormat }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="结婚时间" v-if="viewData.condolenceTypeId === 'cf4ce7af322d464f8f86d818fcc00203'">
|
<el-descriptions-item label="结婚时间" v-if="condolenceTypeCode === 'A'">
|
||||||
<span>{{ viewData.marryTime | dateFormat }}</span>
|
<span>{{ viewData.marryTime | dateFormat }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="生育时间" v-if="viewData.condolenceTypeId === 'c90cb10dce6542e99ae271bee6fe8cc0'">
|
<el-descriptions-item label="生育时间" v-if="condolenceTypeCode === 'D'">
|
||||||
<span>{{ viewData.fertilityTime | dateFormat }}</span>
|
<span>{{ viewData.fertilityTime | dateFormat }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="住院病由" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
<el-descriptions-item label="住院病由" v-if="condolenceTypeCode === 'E'">
|
||||||
<span>{{ viewData.hospitalCausation }}</span>
|
<span>{{ viewData.hospitalCausation }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="住院时间" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
<el-descriptions-item label="住院时间" v-if="condolenceTypeCode === 'E'">
|
||||||
<span v-if="viewData.hospitalizationStartTime && viewData.hospitalizationEndTime">
|
<span v-if="viewData.hospitalizationStartTime && viewData.hospitalizationEndTime">
|
||||||
{{ $moment(viewData.hospitalizationStartTime).format('YYYY-MM-DD') }} 至 {{ $moment(viewData.hospitalizationEndTime).format('YYYY-MM-DD') }}
|
{{ $moment(viewData.hospitalizationStartTime).format('YYYY-MM-DD') }} 至 {{ $moment(viewData.hospitalizationEndTime).format('YYYY-MM-DD') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>暂无住院时间</span>
|
<span v-else>暂无住院时间</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="入住医院" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
<el-descriptions-item label="入住医院" v-if="condolenceTypeCode === 'E'">
|
||||||
<span>{{ viewData.hospital }}</span>
|
<span>{{ viewData.hospital }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="当年次数" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
<el-descriptions-item label="当年次数" v-if="condolenceTypeCode === 'E'">
|
||||||
<span>{{ viewData.hospitalCount }}</span>
|
<span>{{ viewData.hospitalCount }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="去逝时间" v-if="viewData.condolenceTypeId === '60f03a87b62b4823855814afdbf5672a'">
|
<el-descriptions-item label="去逝时间" v-if="condolenceTypeCode === 'G' || condolenceTypeCode === 'H'">
|
||||||
<span>{{ viewData.deathTime | dateFormat }}</span>
|
<span>{{ viewData.deathTime | dateFormat }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="与被慰问人关系" v-if="viewData.condolenceTypeId === '60f03a87b62b4823855814afdbf5672a'">
|
<el-descriptions-item label="与被慰问人关系" v-if="condolenceTypeCode === 'H'">
|
||||||
<span>{{ viewData.condolenceRelationship }}</span>
|
<span>{{ viewData.condolenceRelationship }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
@@ -174,10 +174,6 @@ const unionReimburseInfo = {
|
|||||||
<span>{{ viewData.paymentNotes }}</span>
|
<span>{{ viewData.paymentNotes }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="备注" :span="3" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
|
|
||||||
<span>{{ viewData.notes }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item label="附件" :span="3">
|
<el-descriptions-item label="附件" :span="3">
|
||||||
<file-preview v-if="viewData.files && viewData.files.length > 0"
|
<file-preview v-if="viewData.files && viewData.files.length > 0"
|
||||||
:files="viewData.files"
|
:files="viewData.files"
|
||||||
@@ -204,15 +200,6 @@ const unionReimburseInfo = {
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="支付内容" label-width="100px">
|
<el-form-item label="支付内容" label-width="100px">
|
||||||
<el-input :autosize="{ minRows: 4, maxRows: 8}"
|
<el-input :autosize="{ minRows: 4, maxRows: 8}"
|
||||||
:value="viewData.paymentNotes"
|
|
||||||
readonly
|
|
||||||
type="textarea"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="24">
|
|
||||||
<el-form-item label="备注" label-width="100px">
|
|
||||||
<el-input :autosize="{ minRows: 3, maxRows: 6}"
|
|
||||||
:value="viewData.notes"
|
:value="viewData.notes"
|
||||||
readonly
|
readonly
|
||||||
type="textarea"></el-input>
|
type="textarea"></el-input>
|
||||||
@@ -240,7 +227,17 @@ const unionReimburseInfo = {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="发票号码" min-width="140" prop="invoiceNo"></el-table-column>
|
<el-table-column label="发票号码" min-width="140" prop="invoiceNo"></el-table-column>
|
||||||
<el-table-column align="right" label="发票金额" min-width="110">
|
<el-table-column align="right" label="税前金额" min-width="110">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.amountWithoutTax == null ? '-' : formatMoney(scope.row.amountWithoutTax) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="right" label="税额" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.taxAmount == null ? '-' : formatMoney(scope.row.taxAmount) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="right" label="价税合计" min-width="110">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ formatMoney(scope.row.invoiceAmount) }}</span>
|
<span>{{ formatMoney(scope.row.invoiceAmount) }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -299,6 +296,11 @@ const unionReimburseInfo = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// 详情接口按关联类型返回业务编码,避免不同环境的类型 ID 影响字段显示。
|
||||||
|
condolenceTypeCode() {
|
||||||
|
return this.viewData.reimburseProject === "UNION_REIMBURSE_PROJECT_1"
|
||||||
|
? (this.viewData.condolenceTypeCode || "") : ""
|
||||||
|
},
|
||||||
showInvoiceTab() {
|
showInvoiceTab() {
|
||||||
return !!this.viewData.reimburseProject
|
return !!this.viewData.reimburseProject
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-15
@@ -86,10 +86,9 @@ layout("/layouts/platform.html"){
|
|||||||
<el-table-column label="操作" fixed="right" width="300px">
|
<el-table-column label="操作" fixed="right" width="300px">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
||||||
|
<el-button v-if="[2, 3, 4, 5].includes(Number(row.stateId))" @click="doExport(row)" size="mini" type="primary">导出</el-button>
|
||||||
<el-button v-if="row.stateId == 1 || row.stateId == 5" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
<el-button v-if="row.stateId == 1 || row.stateId == 5" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||||
<el-button v-if="row.stateId == 2" @click="onRevoke(row.id)" size="mini" type="danger">撤回</el-button>
|
<el-button v-if="row.stateId == 2" @click="onRevoke(row.id)" size="mini" type="danger">撤回</el-button>
|
||||||
<el-button v-if="row.stateId == 3" @click="doExport(row)" size="mini" type="primary">导出报销表</el-button>
|
|
||||||
<el-button v-if="row.stateId == 3" @click="doPrint(row)" size="mini" type="primary">打印</el-button>
|
|
||||||
<el-button v-if="row.stateId == 1" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
<el-button v-if="row.stateId == 1" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -171,19 +170,8 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
|
|
||||||
doExport(row) {
|
doExport(row) {
|
||||||
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
|
// 复用公共下载的POST请求、错误提示和loading收尾,由后端选择对应审批单模板。
|
||||||
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '')
|
this.$downLoad("/platform/unionReimburse/mine/exportPdf", {id: row.id})
|
||||||
} else {
|
|
||||||
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
doPrint(row) {
|
|
||||||
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
|
|
||||||
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '') + "&Print=true"
|
|
||||||
} else {
|
|
||||||
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '') + "&Print=true"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -212,7 +212,7 @@ layout("/layouts/platform.html"){
|
|||||||
this.showApprovalForm = true
|
this.showApprovalForm = true
|
||||||
this.formData = {
|
this.formData = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
reviewOpinion: '通过'
|
reviewOpinion: '已付款成功'
|
||||||
}
|
}
|
||||||
this.$refs.unionReimburseInfoRef.onOpen(row)
|
this.$refs.unionReimburseInfoRef.onOpen(row)
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -136,9 +136,9 @@ layout("/layouts/platform.html"){
|
|||||||
this.$set(this.formData, 'isRead', true)
|
this.$set(this.formData, 'isRead', true)
|
||||||
this.$commonUtil.previewFile({
|
this.$commonUtil.previewFile({
|
||||||
suffix: "pdf",
|
suffix: "pdf",
|
||||||
downloadPath: "/platform/sys/file/download?id=orpgde84uggjmpflbob83tadan",
|
downloadPath: "/platform/sys/file/download?id=15eblmambgh3hoj7hg20n4obsg",
|
||||||
name: "中国地质大学(武汉)教职工重大疾病互助基金管理办法.pdf",
|
name: "中国地质大学(武汉)教职工重大疾病互助基金管理办法.pdf",
|
||||||
id: "orpgde84uggjmpflbob83tadan"
|
id: "15eblmambgh3hoj7hg20n4obsg"
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
isReadChange() {
|
isReadChange() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user