This commit is contained in:
=
2026-06-06 17:28:14 +08:00
parent c305bc5dfe
commit a0c5cb6ec8
10 changed files with 128 additions and 41 deletions
@@ -22,7 +22,7 @@ import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@Table("sys_user")
@Table("")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes({@Index(name = "INDEX_SYS_USER_LOGINNAMAE", fields = {"loginname"})})
public class Sys_user extends BaseModel implements Serializable {
@@ -67,10 +67,15 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
private static Map<String, String> unitIdNameMap;
/**
* 更新人员数据时仅按在职状态增加会员,不因状态不匹配取消已有会员。
* 全量更新时,以下在职状态会自动设置为会员。
*/
private static final Set<String> MEMBER_USER_STATES = Set.of("在岗", "切线", "免职", "驻村", "援疆(藏)");
/**
* 全量更新时,以下在职状态会自动取消会员身份和会员角色。
*/
private static final Set<String> REMOVE_MEMBER_USER_STATES = Set.of("离退休", "离世", "离校");
/**
* 字段映射类,用于缓存反射结果
*/
@@ -152,6 +157,24 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return StrUtil.isNotBlank(userState) && MEMBER_USER_STATES.contains(userState);
}
/**
* 判断本次人员状态是否需要取消会员身份。
*/
private boolean shouldRemoveMember(String userState) {
return StrUtil.isNotBlank(userState) && REMOVE_MEMBER_USER_STATES.contains(userState);
}
/**
* 人员源数据只负责基础人员信息,会员缴费及会费类字段由会员业务维护。
* 更新已有人员时置空这些字段,配合 updateIgnoreNull 避免把已维护的数据覆盖为空或默认值。
*/
private void ignoreMemberBusinessFields(Sys_user user) {
user.setMemberFeePaid(null);
user.setMemberTime(null);
user.setPreparationMemberFee(null);
user.setContractMemberFee(null);
}
/**
* 批量补充指定角色,已存在该角色的用户不重复插入。
*/
@@ -190,6 +213,26 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return insertCount;
}
/**
* 批量取消会员身份并删除会员角色,仅用于全量更新中明确需要退会的人员状态。
*/
private int removeMemberAndRole(List<String> userIds, Sys_role memberRole) {
if (Lang.isEmpty(userIds) || memberRole == null) {
return 0;
}
List<String> distinctUserIds = userIds.stream().filter(Objects::nonNull).distinct().toList();
if (distinctUserIds.isEmpty()) {
return 0;
}
int removeRoleCount = 0;
for (List<String> userIdBatch : ListUtil.split(distinctUserIds, 500)) {
dao.update(Sys_user.class, Chain.make("member", false), Cnd.where("id", "in", userIdBatch));
removeRoleCount += dao.clear(Sys_user_role.class,
Cnd.where("roleId", "=", memberRole.getId()).and("userId", "in", userIdBatch));
}
return removeRoleCount;
}
/**
* 全量更新用户数据
*
@@ -241,6 +284,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
Sys_user u = new Sys_user();
BeanUtils.copyProperties(source, u);
boolean addMember = shouldAddMember(source.getUserState());
boolean removeMember = shouldRemoveMember(source.getUserState());
if (user == null) {
// 新增用户,初始化数据
@@ -250,20 +294,23 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
String salt = R.UU32();
u.setSalt(salt);
u.setPassword(PwdUtil.getPassword(PwdUtil.generate(12), salt));
// 更新人员数据只增加会员,不因状态不匹配取消已有会员。
if (addMember) {
u.setMember(true);
addMemberUserIds.add(u.getId());
} else if (removeMember) {
u.setMember(false);
}
needInitUserList.add(u);
} else {
// 修改现有用户 - 根据工号判断是原有的人
u.setId(user.getId());
// 保持原有会员身份不变
// 更新人员数据只增加会员,不因状态不匹配取消已有会员。
ignoreMemberBusinessFields(u);
if (addMember) {
u.setMember(true);
addMemberUserIds.add(user.getId());
} else if (removeMember) {
u.setMember(false);
removeMemberUserIds.add(user.getId());
}
needDoUpdateList.add(u);
@@ -362,7 +409,10 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return "更新失败: " + e.getMessage();
}
// 全量更新后统一补齐角色:所有人补公共角色,所有会员补会员角色;仅新增缺失角色,不删除、不重复插入。
int memberRoleRemoveCount = removeMemberAndRole(removeMemberUserIds, memberRole);
log.info("全量更新取消会员完成,取消会员{}个,删除会员角色{}个", removeMemberUserIds.stream().filter(Objects::nonNull).distinct().count(), memberRoleRemoveCount);
// 全量更新后统一补齐角色:所有人补公共角色,所有会员补会员角色;仅新增缺失角色,不重复插入。
List<Sys_user> latestUsers = dao.query(Sys_user.class, Cnd.NEW());
List<String> allUserIds = latestUsers.stream().map(Sys_user::getId).filter(Objects::nonNull).toList();
List<String> memberUserIds = latestUsers.stream()
@@ -435,6 +485,8 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
log.info("全量更新用户数据完成,耗时: {} 毫秒", (endTime - startTime));
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 更新用户 " + needDoUpdateList.size()
+ " 个, 取消会员 " + removeMemberUserIds.stream().filter(Objects::nonNull).distinct().count()
+ " 个, 删除会员角色 " + memberRoleRemoveCount
+ " 个, 新增公共角色 " + publicRoleAddCount + " 个, 新增会员角色 " + memberRoleAddCount + " 个";
}
@@ -196,7 +196,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 特殊处理 根据身份证号获取性别和出生年月
switch (mapping.field.getName()) {
case "idCard" -> {
// 设置出生年月
// 保存证件号码,并尝试根据身份证号解析出生年月;解析失败只影响生日字段。
mapping.field.set(sysUser, value);
if (StrUtil.isNotBlank(value)) {
try {
sysUser.setBirthday(IdcardUtil.getBirthDate(value));
@@ -53,7 +53,7 @@ public class MemberInfoBoardController {
}
/**
* @return 今年会员数、未缴费会员数和去年的会员数
* @return 今年职工数、缴费会员数、未缴费会员数和去年的会员数
*/
@At
@SaCheckPermission(value = {"member.info.board", "h5.member.info.board"}, mode = SaMode.OR)
@@ -111,7 +111,7 @@ public class MemberInfoBoardController {
/**
* 各工会会员数
* 各工会职工数及缴费会员数
*
* @return
*/
@@ -123,7 +123,9 @@ public class MemberInfoBoardController {
un.name as unionname,
un.unionCode,
( SELECT count( 1 ) FROM vw_user WHERE member = 1 and unionId = un.id ) `value`,
( SELECT count( 1 ) FROM vw_user u LEFT JOIN sys_user su ON su.id = u.id WHERE u.member = 1 and u.unionId = un.id and (su.memberFeePaid = 0 or su.memberFeePaid is null) ) unpaidValue
( SELECT count( 1 ) FROM vw_user u LEFT JOIN sys_user su ON su.id = u.id WHERE u.member = 1 and u.unionId = un.id and (su.memberFeePaid = 0 or su.memberFeePaid is null) ) unpaidValue,
( SELECT count( 1 ) FROM vw_user WHERE member = 1 and unionId = un.id )
- ( SELECT count( 1 ) FROM vw_user u LEFT JOIN sys_user su ON su.id = u.id WHERE u.member = 1 and u.unionId = un.id and (su.memberFeePaid = 0 or su.memberFeePaid is null) ) paidValue
FROM
sys_union un
ORDER BY
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.staffmanage.member.controller.info;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.DesensitizedUtil;
import com.alibaba.excel.EasyExcel;
@@ -104,6 +105,7 @@ public class MemberInfoGroupController {
@At
@SaCheckPermission("member.info.group")
@SaCheckRole(value = {"SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_CHAIRMAN"}, mode = SaMode.OR)
public Result toggleMemberFeePaid(String userId) {
try {
return Result.success(memberInfoService.toggleMemberFeePaid(userId));
@@ -115,6 +117,7 @@ public class MemberInfoGroupController {
@At
@SaCheckPermission("member.info.group")
@SaCheckRole(value = {"SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_CHAIRMAN"}, mode = SaMode.OR)
public Result batchSetMemberFeePaid(MemberInfoPageForm pageForm) {
try {
return Result.success(memberInfoService.batchSetMemberFeePaid(pageForm));
@@ -128,6 +131,7 @@ public class MemberInfoGroupController {
@At
@Ok("void")
@SaCheckPermission("member.info.group")
@SaCheckRole(value = {"SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_CHAIRMAN"}, mode = SaMode.OR)
public void downloadMemberTemplate(HttpServletResponse response) {
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -147,6 +151,7 @@ public class MemberInfoGroupController {
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("member.info.group")
@SaCheckRole(value = {"SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_CHAIRMAN"}, mode = SaMode.OR)
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
public Result importMember(TempFile file, Boolean isFlag) {
try {
@@ -64,7 +64,7 @@ public interface MemberInfoService extends BaseService<Sys_user> {
/**
* 获取会员看板会员数统计数据
*
* @return memberNum 会员总数、unpaidMemberNum 未缴费会员数、lastYearMemberNum 去年会员数
* @return memberNum 会员总数、paidMemberNum 缴费会员数、unpaidMemberNum 未缴费会员数、lastYearMemberNum 去年会员数
*/
NutMap getBoardMemberNumber();
@@ -275,9 +275,12 @@ public class MemberInfoServiceImpl extends BaseServiceImpl<Sys_user> implements
int memberNum = count(Sqls.create("select count(1) from sys_user where member = 1"));
// 空值按未缴费处理,保持会员列表中“是否缴费”的展示口径一致。
int unpaidMemberNum = count(Sqls.create("select count(1) from sys_user where member = 1 and (memberFeePaid = 0 or memberFeePaid is null)"));
// 缴费会员数用于会员看板展示,口径为当前会员总数减去未缴费会员数。
int paidMemberNum = memberNum - unpaidMemberNum;
int lastYearMemberNum = count(Sqls.create("select count(1) from member_history where year = year(now()) -1 "));
return NutMap.NEW()
.addv("memberNum", memberNum)
.addv("paidMemberNum", paidMemberNum)
.addv("unpaidMemberNum", unpaidMemberNum)
.addv("lastYearMemberNum", lastYearMemberNum);
}
@@ -292,9 +292,9 @@ layout("/layouts/platform.html"){
<div class="dashboard">
<!-- 顶部四个统计卡片 - 高度固定200px -->
<div class="card stats-card">
<div class="card-title">会员数/未缴费人数</div>
<div class="card-title">职工数/会员数</div>
<div class="stat-value" v-loading="loading.memberNumber">
{{memberNumberData.memberNum}} / {{memberNumberData.unpaidMemberNum}}
{{memberNumberData.memberNum}} / {{memberNumberData.paidMemberNum}}
<span style="font-size: 14px">&ensp;人</span>
</div>
<!-- <div class="stat-meta" v-loading="loading.memberNumber">-->
@@ -322,7 +322,7 @@ layout("/layouts/platform.html"){
<div class="content-left">
<!-- 工会会员数图表 -->
<div class="card">
<div class="card-title">工会会员数/未缴费数</div>
<div class="card-title">工会职工数/会员数</div>
<div class="chart-container" id="unionMemberChart" v-loading="loading.unionMember"></div>
</div>
@@ -356,7 +356,7 @@ layout("/layouts/platform.html"){
align="center"
header-align="center"
label="会员人数"
prop="value"
prop="paidValue"
show-overflow-tooltip
sortable
width="120px"
@@ -392,6 +392,7 @@ layout("/layouts/platform.html"){
},
memberNumberData: {
memberNum: "--",
paidMemberNum: "--",
unpaidMemberNum: "--",
lastYearMemberNum: "--",
ratio: "0.0%",
@@ -412,11 +413,13 @@ layout("/layouts/platform.html"){
this.$axios.post("/platform/member/info/board/memberNumber").then((resp) => {
if (resp.code === 0) {
const {
data: { memberNum, unpaidMemberNum, lastYearMemberNum }
data: { memberNum, paidMemberNum, unpaidMemberNum, lastYearMemberNum }
} = resp
const diff = memberNum - lastYearMemberNum
const ratio = (lastYearMemberNum ? (Math.abs(diff) / lastYearMemberNum) * 100 : (diff / 1) * 10).toFixed(2) + " %"
this.$set(this, "memberNumberData", { memberNum, unpaidMemberNum, lastYearMemberNum, diff, ratio })
// 兼容旧接口未返回 paidMemberNum 的情况,会员数按总会员数减未缴费人数计算。
const paidMemberCount = paidMemberNum !== undefined ? paidMemberNum : memberNum - unpaidMemberNum
this.$set(this, "memberNumberData", { memberNum, paidMemberNum: paidMemberCount, unpaidMemberNum, lastYearMemberNum, diff, ratio })
this.loading.memberNumber = false
} else {
this.$message.warning(resp.msg)
@@ -577,23 +580,26 @@ layout("/layouts/platform.html"){
const unpaidValue = item.unpaidValue || item.unpaidvalue || 0
return Number(unpaidValue) || 0
},
formatUnionMemberValue(item) {
const unpaidValue = this.getUnionUnpaidValue(item)
if (unpaidValue > 0) {
return item.value + "/" + unpaidValue
getUnionPaidValue(item) {
// 缴费会员数优先使用后端返回字段,旧数据则按职工数减未缴费数兜底。
const paidValue = item.paidValue !== undefined && item.paidValue !== null ? item.paidValue : item.paidvalue
if (paidValue !== undefined && paidValue !== null) {
return Number(paidValue) || 0
}
return item.value
return (Number(item.value) || 0) - this.getUnionUnpaidValue(item)
},
formatUnionMemberValue(item) {
return item.value + "/" + this.getUnionPaidValue(item)
},
buildUnionMemberTooltip(title, items) {
if (!items || !items.length) {
return ""
}
const data = items[0].data || {}
const unpaidValue = this.getUnionUnpaidValue(data)
return '<div style="padding:8px 4px">' +
'<div style="font-weight:600;margin-bottom:4px">' + title + '</div>' +
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#217050;margin-right:6px"></span>会员数:' + data.value + '</div>' +
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#217050;margin-right:6px"></span>未缴费数:' + unpaidValue + '</div>' +
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#217050;margin-right:6px"></span>职工数:' + data.value + '</div>' +
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#217050;margin-right:6px"></span>会员数:' + this.getUnionPaidValue(data) + '</div>' +
'</div>'
},
async getUnionMember() {
@@ -602,8 +608,11 @@ layout("/layouts/platform.html"){
this.loading.unionMember = false
if (resp.code === 0) {
const { chartData, tableData } = resp.data
// 标准化缴费会员数字段,避免后端 Record 字段大小写影响表格绑定和排序。
chartData.forEach((item) => this.$set(item, "paidValue", this.getUnionPaidValue(item)))
tableData.forEach((item) => this.$set(item, "paidValue", this.getUnionPaidValue(item)))
this.unionMember = tableData
this.$set(this, "unionMember", tableData)
if (chart.unionMemberChart) {
chart.unionMemberChart.changeData(chartData)
@@ -685,7 +694,7 @@ layout("/layouts/platform.html"){
alias: "类型"
},
value: {
alias: "会员数"
alias: "职工数"
}
}
})
@@ -10,8 +10,20 @@ layout("/layouts/platform.html"){
<el-card shadow="never" class="mt10">
<table-tool label="会员列表">
<el-button type="primary" size="small" icon="el-icon-check" @click="batchSetMemberFeePaid">一键设置已缴费</el-button>
<el-button type="primary" size="small" icon="el-icon-upload2" @click="openImport">导入会员</el-button>
<el-button
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_CHAIRMAN'])"
type="primary"
size="small"
icon="el-icon-check"
@click="batchSetMemberFeePaid"
>一键设置已缴费</el-button>
<el-button
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_CHAIRMAN'])"
type="primary"
size="small"
icon="el-icon-upload2"
@click="openImport"
>导入会员</el-button>
<el-button
type="primary"
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"
@@ -83,6 +95,7 @@ layout("/layouts/platform.html"){
<template scope="scope">
<el-button size="mini" type="primary" @click="openView(scope.row.id)">查看</el-button>
<el-button
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_CHAIRMAN'])"
size="mini"
:type="isMemberFeePaid(scope.row) ? 'success' : 'info'"
@click="toggleMemberFeePaid(scope.row)"
@@ -3,15 +3,9 @@ const filterUser = {
<el-dialog title="选择用户生成福利名单" :visible.sync="visible" width="80%">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名">
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入姓名"
style="width: 100%" v-model="pageForm.userName">
</el-input>
</search-item>
<search-item label="工号">
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入工号"
style="width: 100%" v-model="pageForm.loginName">
<search-item label="姓名/工号">
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入姓名/工号"
style="width: 100%" v-model="pageForm.searchKeyword">
</el-input>
</search-item>
@@ -107,6 +101,13 @@ const filterUser = {
<el-option label="否" :value="false"></el-option>
</el-select>
</search-item>
<search-item label="是否缴费会员">
<el-select clearable placeholder="请选择是否缴费会员" style="width: 100%"
v-model="pageForm.memberFeePaid">
<el-option label="是" :value="true"></el-option>
<el-option label="否" :value="false"></el-option>
</el-select>
</search-item>
</search>
</el-card>
@@ -159,7 +160,8 @@ const filterUser = {
userStates: [],
personTypes: [],
preparedBys: [],
unitIds: []
unitIds: [],
memberFeePaid: null
},
sexOptions: ["男", "女"],
unionOptions: [],
@@ -188,8 +190,7 @@ const filterUser = {
},
resetPageForm() {
this.$set(this.pageForm, "userName", null)
this.$set(this.pageForm, "loginName", null)
this.$set(this.pageForm, "searchKeyword", null)
this.$set(this.pageForm, "sex", null)
this.$set(this.pageForm, "birthday", null)
this.$set(this.pageForm, "birthMonths", [])
@@ -199,6 +200,7 @@ const filterUser = {
this.$set(this.pageForm, "preparedBys", [])
this.$set(this.pageForm, "userStates", [])
this.$set(this.pageForm, "isMember", null)
this.$set(this.pageForm, "memberFeePaid", null)
},
pageData() {