From d8ac0a7aa49924f7b03e8f1ad2ee5cfcb84a73f9 Mon Sep 17 00:00:00 2001 From: hongqiwei <1143662660@qq.com> Date: Tue, 20 Oct 2026 20:50:11 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=8D=8F=E4=BC=9A=E6=95=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sys/club/ClubUserCountController.java | 17 + .../club/applyIn/ClubApplyInController.java | 48 +- .../io/v/nutz/sys/models/Sys_club_user.java | 12 + .../v/nutz/sys/services/SysClubService.java | 14 + .../sys/services/impl/SysClubServiceImpl.java | 109 ++++ .../web/commons/proc/NutShiroProcessor.java | 2 +- .../static/assets/mobile/js/basic.js | 61 ++ .../components/club/clubUserApplyInfo.vue | 94 +--- .../resources/views/mobile/club/inClub.html | 356 ++++++------ .../views/platform/sys/club/applyIn/sqrt.html | 526 ++++++------------ .../platform/sys/club/clubUserCount.html | 8 +- 11 files changed, 661 insertions(+), 586 deletions(-) diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java index 3293415..8b2873e 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/ClubUserCountController.java @@ -204,6 +204,23 @@ public class ClubUserCountController { return list; } + /** + * 无业务请求参数,response 为文件下载响应;返回 XLSX 文件流而非 JSON。 + * 文件首列为社团名称,包含当前用户权限范围内的全部有效成员,不受页面筛选影响。 + */ + @At + @Ok("void") + @RequiresPermissions("sys.club.count") + public void exportAllUserExcel(HttpServletResponse response) throws IOException { + try (Workbook workbook = sysClubService.exportAllUserExcel()) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment;filename=" + + URLEncoder.encode("全部人员明细.xlsx", "UTF-8")); + workbook.write(response.getOutputStream()); + } + } + @At @Ok("void") @RequiresPermissions("sys.club.count") diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java index c14eb29..5949b4a 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/club/applyIn/ClubApplyInController.java @@ -1,6 +1,7 @@ package io.v.nutz.sys.controllers.platform.sys.club.applyIn; import cn.hutool.core.util.StrUtil; +import cn.wizzer.framework.base.Result; import io.v.nutz.base.annontation.ViReturn; import io.v.nutz.base.service.BaseService; import io.v.nutz.base.utils.DateUtil; @@ -33,6 +34,9 @@ import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -53,10 +57,32 @@ public class ClubApplyInController { public void index() { } + /** + * clubId 为二维码携带的社团主键,普通列表访问可不传。 + * 返回手机页面,并将 URL 编码后的 scanClubId 写入页面,供挂载后打开指定社团使用; + * 编码避免请求参数中的引号、标签等内容进入页面脚本。 + */ @At("/mobile") @Ok("beetl:/mobile/club/inClub.html") @RequiresAuthentication - public void mobile() { + public void mobile(@Param("clubId") String clubId, HttpServletRequest request) { + request.setAttribute("scanClubId", + URLEncoder.encode(StrUtil.blankToDefault(clubId, ""), StandardCharsets.UTF_8)); + } + + /** + * clubId 必须传社团主键;当前用户从登录会话获取。 + * 返回统一 JSON 结果,成功 data 为社团详情及当前用户 status,无效社团返回错误提示。 + */ + @At + @RequiresAuthentication + public Object scanDetail(@Param("clubId") String clubId) { + if (StrUtil.isBlank(clubId)) { + return Result.error("请提供社团编号"); + } + Object detail = sysClubService.scanDetail(clubId, ShiroUtil.getPlatformUid()); + return detail == null ? Result.error("社团不存在或暂不可申请") + : Result.success("获取成功", detail); } @Inject @@ -122,11 +148,29 @@ public class ClubApplyInController { } + /** + * clubid 为申请社团主键;birthday、mobile 为本次申请的出生日期和联系电话; + * medicalHistory 必须明确选择是或否,otherThing 为可选说明(最多 100 字); + * emergencyContact、emergencyMobile 为必填紧急联系人及电话。 + * 申请人由登录会话确定;返回值由 ViReturn 包装为 code、msg、data 的 JSON 结果。 + */ @At @ViReturn @RequiresPermissions("sys.club.sq") @Aop(TransAop.READ_COMMITTED) public Object submit(Sys_club_user sys_club_user) { + // 在清理旧申请前校验必填项,false 表示无特殊病史,是有效选择。 + if (StrUtil.isBlank(sys_club_user.getClubid()) || sys_club_user.getBirthday() == null + || StrUtil.isBlank(sys_club_user.getMobile()) || sys_club_user.getMedicalHistory() == null + || StrUtil.isBlank(sys_club_user.getEmergencyContact()) + || StrUtil.isBlank(sys_club_user.getEmergencyMobile())) { + throw new IllegalArgumentException("请完整填写出生年月、联系电话、特殊病史和紧急联系人信息"); + } + if (sys_club_user.getMobile().length() > 20 || sys_club_user.getEmergencyContact().length() > 50 + || sys_club_user.getEmergencyMobile().length() > 20 + || (sys_club_user.getOtherThing() != null && sys_club_user.getOtherThing().length() > 100)) { + throw new IllegalArgumentException("联系电话最多20字,紧急联系人最多50字,特别说明最多100字"); + } sysClubUserService.clear(Cnd.where("clubid", "=", sys_club_user.getClubid()).and("userid", "=", ShiroUtil.getPrincipalProperty("id"))); String dateTime = DateUtil.getDateTime(); @@ -269,7 +313,7 @@ public class ClubApplyInController { c.*, u.username, u.sex, - u.birthday + u.loginname FROM sys_club_user c LEFT JOIN USER u ON c.userid = u.id diff --git a/src/main/java/io/v/nutz/sys/models/Sys_club_user.java b/src/main/java/io/v/nutz/sys/models/Sys_club_user.java index 86b22d0..c891f84 100644 --- a/src/main/java/io/v/nutz/sys/models/Sys_club_user.java +++ b/src/main/java/io/v/nutz/sys/models/Sys_club_user.java @@ -136,6 +136,18 @@ public class Sys_club_user extends BaseModel { @ColDefine(type = ColType.VARCHAR, width = 20) private String mobile; + /** 本次入会申请的紧急联系人,不回写用户档案。 */ + @Column + @Comment("紧急联系人") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String emergencyContact; + + /** 紧急联系号码按文本保存,允许座机区号及国际区号。 */ + @Column + @Comment("紧急联系电话") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String emergencyMobile; + @Column @Comment("邮箱") @ColDefine(type = ColType.VARCHAR, width = 100) diff --git a/src/main/java/io/v/nutz/sys/services/SysClubService.java b/src/main/java/io/v/nutz/sys/services/SysClubService.java index 266c492..4714902 100644 --- a/src/main/java/io/v/nutz/sys/services/SysClubService.java +++ b/src/main/java/io/v/nutz/sys/services/SysClubService.java @@ -5,9 +5,23 @@ import cn.wizzer.framework.page.Pagination; import io.v.nutz.base.query.PageForm; import io.v.nutz.sys.models.Sys_club; import org.nutz.dao.Cnd; +import org.nutz.dao.entity.Record; +import org.apache.poi.ss.usermodel.Workbook; public interface SysClubService extends BaseService { + /** + * 按社团主键和当前登录用户主键查询扫码详情;返回社团字段、联系人、人数及 status + * (1/3 待审核、5 已加入、2/4 被拒绝、null 未申请),无有效社团时返回 null。 + */ + Record scanDetail(String clubId, String userId); + Pagination pageData(Cnd cnd, PageForm page, Integer year, String name); + + /** + * 无筛选及分页参数,按当前登录用户的数据权限导出全部正常且审核通过的社团成员。 + * 返回 XSSF 工作簿,首列为社团名称,其余列为人员明细;调用方负责关闭工作簿。 + */ + Workbook exportAllUserExcel(); } diff --git a/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java b/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java index c458422..9acdba4 100644 --- a/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java +++ b/src/main/java/io/v/nutz/sys/services/impl/SysClubServiceImpl.java @@ -1,24 +1,133 @@ package io.v.nutz.sys.services.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.wizzer.framework.base.service.BaseServiceImpl; import cn.wizzer.framework.page.Pagination; import io.v.nutz.base.query.PageForm; +import io.v.nutz.base.service.BaseService; import io.v.nutz.sys.models.Sys_club; import io.v.nutz.sys.services.SysClubService; +import io.v.nutz.base.utils.Roles; +import io.v.nutz.sys.constant.club.ClubRegistAuditState; +import io.v.nutz.sys.models.Sys_user_role; +import io.v.nutz.web.commons.utils.ShiroUtil; +import org.apache.poi.ss.usermodel.Workbook; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +import org.nutz.dao.entity.Record; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.lang.util.NutMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @IocBean(args = {"refer:dao"}) public class SysClubServiceImpl extends BaseServiceImpl implements SysClubService { + @Inject + private BaseService baseService; + public SysClubServiceImpl(Dao dao) { super(dao); } + /** + * 合并导出所有可见社团的有效成员,不接收页面筛选参数;一条社团成员关系对应一行。 + * 返回首列为社团名称的 XSSF 工作簿,同一人员加入多个社团时分别保留。 + */ + @Override + public Workbook exportAllUserExcel() { + Sql sql = Sqls.create(""" + SELECT + club.name AS clubName, + u.username, + u.loginname, + u.sex, + u.mobile, + u.unitname, + u.userState AS userState, + IF(c.giveMoney = true, '拨付', '不拨付') AS giveMoney, + IF(c.isjf = true, '已缴费', '未缴费') AS isjf, + (CASE c.sf WHEN 1 THEN '会长' WHEN 2 THEN '成员' + WHEN 3 THEN '副会长' WHEN 4 THEN '秘书长' WHEN 5 THEN '副秘书长' END) AS sf + FROM sys_club_user c + INNER JOIN sys_club club ON club.id = c.clubid + LEFT JOIN `user` u ON u.id = c.userid + $condition + ORDER BY club.code ASC, club.id ASC, FIELD(c.sf, 1, 3, 4, 5, 2), u.loginname ASC, c.id ASC + """); + // 社团范围及成员有效性与统计列表、人员明细保持一致。 + Cnd cnd = Cnd.where("club.isjs", "=", false) + .and("club.state", "=", ClubRegistAuditState.SCHOOL_PASS) + .and("c.isNormal", "=", true) + .and("c.status", "=", 5); + List members = new ArrayList<>(); + boolean canExport = true; + if (!ShiroUtil.hasAnyRoles("sysadmin, A06, xxhgly")) { + List userRoles = dao().query(Sys_user_role.class, + Cnd.where("userid", "=", ShiroUtil.getPlatformUid()).and("roleid", "=", Roles.club01)); + List clubIds = userRoles.stream().map(Sys_user_role::getStid) + .filter(id -> id != null && !id.trim().isEmpty()).collect(Collectors.toList()); + // 未分配社团时仅导出表头,防止空 IN 条件导致数据权限失效。 + canExport = !clubIds.isEmpty(); + if (canExport) { + cnd.and("club.id", "in", clubIds); + } + } + if (canExport) { + sql.setCondition(cnd); + members = baseService.listMap(sql); + } + + List columns = new ArrayList<>(); + columns.add(new ExcelExportEntity("社团名称", "clubName", 30)); + columns.add(new ExcelExportEntity("姓名", "username", 20)); + columns.add(new ExcelExportEntity("工号", "loginname", 20)); + columns.add(new ExcelExportEntity("性别", "sex", 20)); + columns.add(new ExcelExportEntity("联系方式", "mobile", 20)); + columns.add(new ExcelExportEntity("所在单位", "unitname", 30)); + columns.add(new ExcelExportEntity("在职状态", "userState", 20)); + columns.add(new ExcelExportEntity("是否拨付", "giveMoney", 20)); + columns.add(new ExcelExportEntity("是否缴费", "isjf", 20)); + columns.add(new ExcelExportEntity("职务", "sf", 20)); + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + exportParams.setSheetName("全部人员明细"); + return ExcelExportUtil.exportExcel(exportParams, columns, members); + } + + /** + * 扫码仅展示已通过审批且未解散的社团;申请状态取当前用户的有效成员记录。 + * clubId 为社团主键,userId 为登录用户主键,返回详情记录,无匹配记录返回 null。 + */ + @Override + public Record scanDetail(String clubId, String userId) { + Sql sql = Sqls.create(""" + SELECT club.*, fzr.username fzrname, unit.name fzr_unit_name, + (SELECT COUNT(1) FROM sys_club_user cu + WHERE cu.clubid = club.id AND cu.status = 5 AND cu.isNormal = true) cynum, + (SELECT cu.status FROM sys_club_user cu + WHERE cu.clubid = club.id AND cu.userid = @userId AND cu.isNormal = true + ORDER BY cu.join_time DESC LIMIT 1) status + FROM sys_club club + LEFT JOIN sys_user fzr ON fzr.id = club.concatPerson + LEFT JOIN sys_unit unit ON unit.id = fzr.unitid + WHERE club.id = @clubId AND club.isjs = false AND club.state = @state + """); + sql.setParam("clubId", clubId).setParam("userId", userId) + .setParam("state", ClubRegistAuditState.SCHOOL_PASS); + List records = list(sql); + return records.isEmpty() ? null : records.get(0); + } + @Override public Pagination pageData(Cnd cnd, PageForm page, Integer year, String name) { Sql sql = Sqls.create(""" diff --git a/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java b/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java index 8d60c9f..40fa7e2 100644 --- a/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java +++ b/src/main/java/io/v/nutz/web/commons/proc/NutShiroProcessor.java @@ -74,7 +74,7 @@ public class NutShiroProcessor extends AbstractProcessor { "/platform/login(/doLogin|/logout|/captcha)", "/platform/qywechat/.*", "/platform/home/(500|403|404|UnknownAccountError|LockedAccountError)", -// "/mobile/login", + "/mobile/login", "/mobile/login/doLogin", "/platform/jsz/login", "/platform/activity/basic/scope/getScopeUser", diff --git a/src/main/resources/static/assets/mobile/js/basic.js b/src/main/resources/static/assets/mobile/js/basic.js index fc21aa9..a077943 100644 --- a/src/main/resources/static/assets/mobile/js/basic.js +++ b/src/main/resources/static/assets/mobile/js/basic.js @@ -34,6 +34,67 @@ const clone = (val) => { return JSON.parse(JSON.stringify(val)) } +// H5 页面按需注册弹框同步回调;全站只保留一个 popstate 监听,弹框按后进先出关闭。 +const mobilePopupHistory = { + owner: null, + token: null, + stack: [], + pending: null, + register(sync) { + this.owner = sync + this.token = String(Date.now()) + Math.random() + this.stack = [] + return this + }, + open(key) { + if (!this.owner || this.pending || this.stack.includes(key)) return + this.stack.push(key) + window.history.pushState(Object.assign({}, window.history.state, { + mobilePopupDepth: this.stack.length, + mobilePopupOwner: this.token + }), '', window.location.href) + this.owner(key, true) + }, + // 返回 Promise,关闭确认框后再提交,避免回退尚未完成便清理父级弹框。 + close() { + if (this.pending) return this.pending.promise + if (!this.stack.length) return Promise.resolve() + return this.back(1) + }, + back(count) { + let complete + const promise = new Promise(resolve => { complete = resolve }) + this.pending = {promise, complete} + window.history.go(-count) + return promise + }, + clear() { + if (this.pending) return this.pending.promise.then(() => this.clear()) + return this.stack.length ? this.back(this.stack.length) : Promise.resolve() + }, + unregister(sync) { + if (this.owner !== sync) return + this.owner = null + this.stack = [] + }, + onPop(event) { + if (!this.owner) return + const depth = event.state && event.state.mobilePopupOwner === this.token + ? event.state.mobilePopupDepth : 0 + // 弹框回退由本管理器消费,避免 PJAX 同时将其识别为业务页面切换。 + if (this.stack.length > depth || this.pending) event.stopImmediatePropagation() + while (this.stack.length > depth) { + this.owner(this.stack.pop(), false) + } + if (this.pending) { + const complete = this.pending.complete + this.pending = null + complete() + } + } +} +window.addEventListener('popstate', event => mobilePopupHistory.onPop(event), true) + const toChinesNum = (num) => { let changeNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']; let unit = ["", "十", "百", "千", "万"]; diff --git a/src/main/resources/static/components/club/clubUserApplyInfo.vue b/src/main/resources/static/components/club/clubUserApplyInfo.vue index 929751c..e90e575 100644 --- a/src/main/resources/static/components/club/clubUserApplyInfo.vue +++ b/src/main/resources/static/components/club/clubUserApplyInfo.vue @@ -6,74 +6,15 @@ - - {{viewData.username}} - - - {{viewData.sex}} - - - {{moment(viewData.birthday).format('YYYY-MM-DD')}} - - - {{viewData.birthPlace}} - - - {{viewData.education}} - - - {{viewData.hobby}} - - - {{viewData.unitName}} - - - {{viewData.position}} - - - {{viewData.mobile}} - - - {{viewData.email}} - - - {{viewData.homeAddress}} - - - {{viewData.QQNumber}} - - - {{viewData.bodyHeight}} - - - {{viewData.bodyWeight}} - - - {{viewData.health}} - - - {{viewData.sport}} - - - {{viewData.medicalHistory === true ? '是' : '否'}} - - - {{viewData.joinActivity === true ? '是' : '否'}} - - -
-
- -
-
- - -
- -
-
+ {{ viewData.username }} + {{ viewData.sex }} + {{ viewData.loginname }} + {{ viewData.birthday ? moment(viewData.birthday).format('YYYY-MM-DD') : '' }} + {{ viewData.mobile }} + {{ viewData.medicalHistory === true ? '是' : viewData.medicalHistory === false ? '否' : '' }} + {{ viewData.otherThing }} + {{ viewData.emergencyContact }} + {{ viewData.emergencyMobile }}
@@ -139,14 +80,15 @@ module.exports = { } return this.panes.includes(name) }, - async getInfo(id) { - this.loading = true - const {data, code, msg} = await $.get('/platform/sys/club/sq/findOne', {id}) - this.loading = false - if (data) { - this.clubUserInfo = this.handle ? "999" : "1" - this.viewData = data - } + // id 为入会记录主键;data 返回申请资料和审核信息,历史新增字段可为空。 + getInfo(id) { + this.$set(this, 'loading', true) + $.post('/platform/sys/club/sq/findOne', {id}).then(res => { + if (res.code === 0 && res.data) { + this.$set(this, 'clubUserInfo', this.handle ? '999' : '1') + this.$set(this, 'viewData', res.data) + } + }).always(() => { this.$set(this, 'loading', false) }) } } } diff --git a/src/main/resources/views/mobile/club/inClub.html b/src/main/resources/views/mobile/club/inClub.html index bc4a5a9..42078ad 100644 --- a/src/main/resources/views/mobile/club/inClub.html +++ b/src/main/resources/views/mobile/club/inClub.html @@ -110,7 +110,7 @@ layout("/mobile/platform.html"){ left-arrow placeholder fixed - @click-left="pjaxReplace('/mobile/index')" + @click-left="leavePage" safe-area-inset-top > @@ -207,13 +207,13 @@ layout("/mobile/platform.html"){ > - + @@ -233,94 +233,29 @@ layout("/mobile/platform.html"){ 点击查看 + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -336,6 +271,9 @@ layout("/mobile/platform.html"){ data() { return { showPicker: false, + birthdayPickerValue: new Date(), + birthdayMinDate: new Date(1900, 0, 1), + birthdayMaxDate: new Date(), infoActive: 0, clickRow: {}, typeOptions: [], @@ -343,17 +281,77 @@ layout("/mobile/platform.html"){ infoShow: false, detailData: {}, viewType: '', + formLoading: false, + // 服务端保留本次页面请求的社团 ID,避免依赖页面切换时的地址栏状态。 + scanClubId: decodeURIComponent('${scanClubId!}'.replace(/\+/g, '%20')), + } + }, + computed: { + canJoin() { + return !!this.detailData.id && ![1, 3, 5].includes(Number(this.detailData.status)) + }, + joinStatus() { + return {1: '待社团审核', 3: '待校工会审核', 5: '已加入'}[this.detailData.status] || '' } }, methods: { - showCode(type) { - if(type === 'QQ' && this.clickRow.qqgroupcode) { - const ids = JSON.parse(this.clickRow.qqgroupcode).map(o => FILE_DOMAIN + '/fileStreamPreview?id=' + o.id) - vant.ImagePreview({images: ids}) - } else if(type === 'weChat' && this.clickRow.wechatgroupcode) { - const ids = JSON.parse(this.clickRow.wechatgroupcode).map(o => FILE_DOMAIN + '/fileStreamPreview?id=' + o.id) - vant.ImagePreview({images: ids}) + // 所有弹层由统一历史栈同步;页面不单独监听 popstate。 + syncPopup(key, visible) { + if (key === 'info') this.$set(this, 'infoShow', visible) + if (key === 'birthday') this.$set(this, 'showPicker', visible) + if (key === 'confirm' && !visible) vant.Dialog.close() + if (key === 'preview' && !visible && this.clubPreview) this.clubPreview.close() + }, + closePopup() { + return mobilePopupHistory.close() + }, + leavePage() { + if (mobilePopupHistory.stack.length) { + this.closePopup() + return } + this.disposePopup() + pjaxReplace('/mobile/index') + }, + disposePopup() { + mobilePopupHistory.unregister(this.syncPopup) + $(document).off('pjax:beforeSend.clubScan') + }, + openBirthday() { + // 选择器定位到系统出生日期;档案为空时允许用户补填。 + const date = moment(this.formData.birthday, 'YYYY-MM-DD', true) + this.$set(this, 'birthdayPickerValue', date.isValid() ? date.toDate() : new Date()) + mobilePopupHistory.open('birthday') + }, + confirmBirthday(value) { + this.$set(this.formData, 'birthday', moment(value).format('YYYY-MM-DD')) + this.closePopup() + }, + // 确认/取消先回退本层历史,系统返回由同步回调关闭确认框。 + confirmAction(message) { + mobilePopupHistory.open('confirm') + return vant.Dialog.confirm({title: '提示', message, closeOnPopstate: false}) + .then(() => this.closePopup()) + .catch(() => { + if (mobilePopupHistory.stack.includes('confirm')) { + return this.closePopup().then(() => Promise.reject()) + } + return Promise.reject() + }) + }, + showCode(type) { + const files = type === 'QQ' ? this.detailData.qqgroupcode : this.detailData.wechatgroupcode + if (!files) return + const images = (typeof files === 'string' ? JSON.parse(files) : files) + .map(o => FILE_DOMAIN + '/fileStreamPreview?id=' + o.id) + if (!images.length) return + mobilePopupHistory.open('preview') + this.clubPreview = vant.ImagePreview({ + images, closeOnPopstate: false, + onClose: () => { + if (mobilePopupHistory.stack.includes('preview')) this.closePopup() + } + }) }, doSearch() { this.pageForm.pageNumber = 1 @@ -361,7 +359,7 @@ layout("/mobile/platform.html"){ this.finished = false this.onLoad() }, - async onLoad() { + onLoad() { this.loading = true $.post('/platform/sys/club/sq/pageData', this.pageForm).then(res => { if (res.code === 0) { @@ -372,99 +370,139 @@ layout("/mobile/platform.html"){ this.pageForm.pageNumber++ } } - this.loading = false - }) + }).always(() => { this.loading = false }) }, openInfo(row, type) { - this.viewType = type - this.infoActive = type === 'view' ? 0 : 1 - if(type === 'join') { - this.formData = { + if (this.mLoading) return + this.mLoading = true + // clubId 是社团主键;data 返回详情及登录用户状态,不受列表页码、筛选影响。 + $.post('/platform/sys/club/sq/scanDetail', {clubId: row.id}).then(res => { + if (res.code !== 0) { + vant.Toast(res.msg || '社团信息加载失败') + return + } + this.detailData = res.data + this.clickRow = res.data + this.viewType = 'view' + this.infoActive = 0 + if (this.canJoin && (type === 'join' || type === 'scan')) { + this.prepareJoin() + } + // 等待条件页签及弹框渲染后再打开;扫码先展示社团信息,列表申请展示个人信息。 + this.$nextTick(() => { + this.infoActive = type === 'join' && this.canJoin ? 1 : 0 + mobilePopupHistory.open('info') + }) + }).fail(() => vant.Toast('社团信息加载失败,请重试')) + .always(() => { this.mLoading = false }) + }, + startJoin() { + if (!this.canJoin) return + // 已进入加入模式时保留用户填写的数据,不因切换页签再次清空。 + if (this.viewType !== 'join') this.prepareJoin() + this.$nextTick(() => { this.infoActive = 1 }) + }, + // 初始化当前社团的加入表单;扫码展示两个页签时也复用此方法。 + prepareJoin() { + this.viewType = 'join' + this.$set(this, 'formData', { username: "${@shiro.getPrincipalProperty('username')}", sex: "${@shiro.getPrincipalProperty('sex')}", + loginname: "${@shiro.getPrincipalProperty('loginname')}", birthday: "${@shiro.getPrincipalProperty('birthday')}", - birthPlace: "${@shiro.getPrincipalProperty('birthPlace')}", - education: "${@shiro.getPrincipalProperty('education')}", - hobby: "${@shiro.getPrincipalProperty('hobby')}", - unitName: "${@shiro.getPrincipalProperty('unit').getName()}", - position: "${@shiro.getPrincipalProperty('position')}", mobile: "${@shiro.getPrincipalProperty('mobile')}", - email: "${@shiro.getPrincipalProperty('email')}", - homeAddress: "${@shiro.getPrincipalProperty('homeAddress')}", - } - } - this.clickRow = row - $.get("/platform/sys/club/mange/findOne", {id: row.id}, (data) => { - if (data.code === 0) { - this.detailData = data.data - this.infoShow = true - } - }, "json"); + medicalHistory: null, otherThing: '', emergencyContact: '', emergencyMobile: '' + }) }, - async needJoin() { + needJoin() { + if (this.formLoading || !this.canJoin) return const row = this.clickRow if(this.formData.medicalHistory === '' || this.formData.medicalHistory === undefined || this.formData.medicalHistory === null) { - vant.Toast('请选择是否有过特殊病史') + vant.Toast('请选择是否有特殊病史') return } - if(this.formData.joinActivity === '' || this.formData.joinActivity === undefined || this.formData.joinActivity === null) { - vant.Toast('请选择是否愿意参加本协会的活动') - return - } - if(this.formData.photo === '' || this.formData.photo === undefined || this.formData.photo === null) { - vant.Toast('请上传两寸登记照') - return - } - const {code, data} = await $.post('/platform/sys/club/sq/getInClubByCurUser') - let str = '' - if (data && data.length > 0) { - str = '您已经加入了' - data.forEach((item) => { - str += '【' + item.name + '】' - }) - str += ',' - } - str += '请确认是否申请加入【' + row.name + '】?' - vant.Dialog.confirm({title: '提示', message: str,}).then(() => { - this.formData.clubid = row.id - this.formData.photo = this.formData.photo[0].filepath - $.post("/platform/sys/club/sq/submit", this.formData, (data) => { - if (data.code === 0) { - vant.Toast(data.msg) - this.doSearch() - this.infoShow = false - } else { - vant.Toast(data.msg) - } - }, "json"); - }).catch(() => { - }); + this.$set(this, 'formLoading', true) + $.post('/platform/sys/club/sq/getInClubByCurUser').then(res => { + if (res.code !== 0) { + vant.Toast(res.msg) + return + } + const data = res.data + let str = '' + if (data && data.length > 0) { + str = '您已经加入了' + data.forEach((item) => { + str += '【' + item.name + '】' + }) + str += ',' + } + str += '请确认是否申请加入【' + row.name + '】?' + this.confirmAction(str).then(() => { + this.$set(this, 'formLoading', true) + // 仅提交精简后的入会资料,出生年月和电话不回写用户档案。 + const params = Object.assign({}, this.formData, { + clubid: row.id + }) + $.post("/platform/sys/club/sq/submit", params).then(data => { + if (data.code === 0) { + vant.Toast(data.msg) + mobilePopupHistory.clear().then(() => this.doSearch()) + } else { + vant.Toast(data.msg) + } + }).always(() => { this.$set(this, 'formLoading', false) }) + }).catch(() => { + }); + }).always(() => { this.$set(this, 'formLoading', false) }) }, rollback(o) { - vant.Dialog.confirm({title: '提示', message: '请确定是否撤销申请?',}).then(() => { + this.confirmAction('请确定是否撤销申请?').then(() => { const id = o.id - $.post("/platform/sys/club/sq/revocation", {id}, (data) => { + this.$set(this, 'formLoading', true) + $.post("/platform/sys/club/sq/revocation", {id}).then(data => { if (data.code === 0) { vant.Toast('撤销申请成功'); this.doSearch(); } else { vant.Toast(data.msg); } - }, "json"); + }).always(() => { this.$set(this, 'formLoading', false) }) }).catch(() => { }); } }, - async created() { - const data = await getClubType() - data.forEach(item => { - this.typeOptions.push({text: item.name, value: item.code}) + created() { + mobilePopupHistory.register(this.syncPopup) + // PJAX 离开前先清理弹框历史,重新发起原页面跳转,并注销本页回调。 + $(document).on('pjax:beforeSend.clubScan', (event, xhr, options) => { + if (mobilePopupHistory.stack.length) { + event.preventDefault() + mobilePopupHistory.clear().then(() => { + this.disposePopup() + pjaxReplace(options.url) + }) + return false + } + this.disposePopup() }) - this.typeOptions.unshift({text: '全部类型'}) - this.$set(this.pageForm, "clubType", this.typeOptions[0].value) - this.$set(this.pageForm, "isjr", this.clubType[0].value) - this.pageForm.searchName = 'club.name' + this.$set(this.pageForm, 'isjr', false) + this.$set(this.pageForm, 'searchName', 'club.name') this.onLoad() + getClubType().then(data => { + data.forEach(item => { + this.typeOptions.push({text: item.name, value: item.code}) + }) + this.typeOptions.unshift({text: '全部类型'}) + this.$set(this.pageForm, "clubType", this.typeOptions[0].value) + }) + }, + mounted() { + // 兼容已有二维码;无 clubId 的普通入口仍显示列表,不猜测目标社团。 + const clubId = this.scanClubId || new URLSearchParams(window.location.search).get('clubId') + if (clubId) this.openInfo({id: clubId}, 'scan') + }, + beforeDestroy() { + mobilePopupHistory.clear().then(() => this.disposePopup()) } }) diff --git a/src/main/resources/views/platform/sys/club/applyIn/sqrt.html b/src/main/resources/views/platform/sys/club/applyIn/sqrt.html index ea0da12..6f1217b 100644 --- a/src/main/resources/views/platform/sys/club/applyIn/sqrt.html +++ b/src/main/resources/views/platform/sys/club/applyIn/sqrt.html @@ -58,6 +58,38 @@ layout("/layouts/platform.html"){ margin: 0 0 18px; } + /* 仅调整入会表单,避免影响公共表格组件及其他业务页面。 */ + #app .club-apply-form th, + #app .club-apply-form .el-form-item__label { + text-align: center; + } + + #app .club-apply-form .el-form-item__label { + width: 100%; + } + + #app .club-apply-form th.club-required::before { + content: "*"; + color: #f56c6c; + margin-right: 4px; + } + + /* 校验信息占据字段下方的正常文档流,长提示也不会遮挡下一行。 */ + #app .club-apply-form .el-form-item__error { + position: relative; + top: auto; + left: auto; + z-index: auto; + background: transparent; + color: #f56c6c; + border-radius: 0; + padding: 4px 0 0; + min-width: 0; + font-size: 12px; + line-height: 1.4; + text-align: left; + } +
@@ -142,10 +174,11 @@ layout("/layouts/platform.html"){ - + + +
+

{{clubCodeName}}

+ 扫码查看社团并申请加入 +

手机扫码查看社团并申请加入

+
+
diff --git a/src/main/resources/views/platform/sys/club/clubUserCount.html b/src/main/resources/views/platform/sys/club/clubUserCount.html index 23e264f..7d85473 100644 --- a/src/main/resources/views/platform/sys/club/clubUserCount.html +++ b/src/main/resources/views/platform/sys/club/clubUserCount.html @@ -58,7 +58,9 @@ layout("/layouts/platform.html"){ @@ -225,6 +227,10 @@ layout("/layouts/platform.html"){ 'dict-select': httpVueLoader('/components/plugins/DictSelect.vue?v=1.0.0'), }, methods: { + exportAllUserExcel() { + // 导出权限范围内全部有效成员,不携带页面筛选或分页参数。 + window.open('/platform/sys/club/clubUserCount/exportAllUserExcel') + }, exportUserData(row) { const id = row === null ? '' : row.id let url = '/platform/sys/club/clubUserCount/exportUserData?clubId=' + id + From bf433727b9095591077e1337f0bcd37eac6114f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E5=AD=A6=E6=88=90?= <1274337408@qq.com> Date: Tue, 20 Oct 2026 21:35:06 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=85=AC=E4=BC=9A=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E7=AE=A1=E7=90=86:=E5=9F=BA=E5=B1=82=E5=B9=B2=E9=83=A8?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=B1=8A=E6=AC=A1?= =?UTF-8?q?=E6=95=B4=E6=94=B9=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=A6=BB=E4=BB=BB?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=92=8C=E5=AE=A1=E6=A0=B8=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=86=E5=B7=A5=E4=BC=9A=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E9=A1=B9=E3=80=82=E7=A7=BB=E9=99=A4=E5=88=A0=E9=99=A4=E5=9F=BA?= =?UTF-8?q?=E5=B1=82=E5=B9=B2=E9=83=A8=E7=9B=B8=E5=85=B3=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E7=9A=84=E5=AE=A1=E6=A0=B8=E6=B5=81=E7=A8=8B=EF=BC=9B=E5=85=81?= =?UTF-8?q?=E8=AE=B8=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=E5=91=98=E5=92=8C?= =?UTF-8?q?=E6=A0=A1=E5=B7=A5=E4=BC=9A=E7=AE=A1=E7=90=86=E5=91=98=E5=85=8D?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E6=B7=BB=E5=8A=A0=E5=9F=BA=E5=B1=82=E5=B9=B2?= =?UTF-8?q?=E9=83=A8=E7=9B=B8=E5=85=B3=E8=A7=92=E8=89=B2=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unioncadre/UnionCadreAuditController.java | 28 ++++++- .../sys/unioncadre/UnionCadreController.java | 81 ++++++++++++++++--- .../java/io/v/nutz/sys/models/UnionCadre.java | 7 ++ .../components/sys/union/cadre/Audit.vue | 53 +++++++++++- .../components/sys/union/cadre/Mange.vue | 64 ++++++++++++++- .../views/platform/sys/union/unionMange.html | 4 +- 6 files changed, 216 insertions(+), 21 deletions(-) diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreAuditController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreAuditController.java index 327e610..0b78bc5 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreAuditController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreAuditController.java @@ -13,10 +13,12 @@ import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Lang; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import java.util.List; +import java.util.stream.Collectors; /** * 基层干部审核 @@ -71,7 +73,7 @@ public class UnionCadreAuditController { @At @ViReturn - public Object pageData(PageForm pageForm) { + public Object pageData(PageForm pageForm, boolean audit, String workersCongressSession, String unionId) { Sql sql = Sqls.create(""" SELECT uc.*, @@ -93,11 +95,33 @@ public class UnionCadreAuditController { pageForm.defaultSortDesc("uc.workersCongressSession"); CndPlus cnd = CndPlus.create(); - cnd.and("uc.auditStateId", "=", UnionCadreAuditState.SCHOOL_UNION); + if (audit) { + cnd.and("uc.auditStateId", "in", Lang.array( + UnionCadreAuditState.SCHOOL_UNION_FAIL, + UnionCadreAuditState.SUCCESS + )); + } else { + cnd.and("uc.auditStateId", "=", UnionCadreAuditState.SCHOOL_UNION); + } + cnd.andEX("uc.workersCongressSession", "=", workersCongressSession); + cnd.andEX("uc.unionId", "=", unionId); cnd.and(pageForm); sql.setCondition(cnd); return cadreService.list(pageForm, sql); } + @At + @ViReturn + public Object usedSessions(String unionId) { + Cnd cnd = Cnd.NEW(); + cnd.andEX("unionId", "=", unionId); + List sessions = cadreService.query(cnd).stream() + .map(UnionCadre::getWorkersCongressSession) + .filter(session -> session != null && !session.isBlank()) + .distinct() + .collect(Collectors.toList()); + return sessions; + } + } diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreController.java index db1ec11..afbcf80 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/unioncadre/UnionCadreController.java @@ -6,19 +6,27 @@ import io.v.nutz.base.utils.PageUtil; import io.v.nutz.base.dao.CndPlus; import io.v.nutz.base.query.PageForm; import io.v.nutz.sys.models.Sys_role; +import io.v.nutz.sys.models.Sys_user_role; import io.v.nutz.sys.models.UnionCadre; import io.v.nutz.sys.services.SysDictService; import io.v.nutz.sys.services.SysRoleService; +import io.v.nutz.sys.services.SysUserRoleService; +import io.v.nutz.sys.services.SysUserService; import io.v.nutz.sys.services.UnionCadreService; +import io.v.nutz.web.commons.utils.ShiroUtil; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.Strings; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; +import java.util.Date; + /** * 基层干部 * @@ -40,6 +48,12 @@ public class UnionCadreController { @Inject private SysDictService sysDictService; + @Inject + private SysUserRoleService userRoleService; + + @Inject + private SysUserService userService; + @At @ViReturn public Object pageData(PageForm pageForm, String unionid) { @@ -64,7 +78,7 @@ public class UnionCadreController { CndPlus cnd = CndPlus.create(); cnd.and(pageForm); - cnd.andEx("u.unionid", "=", unionid); + cnd.andEx("uc.unionId", "=", unionid); if (Strings.isNotBlank(pageForm.getPageOrderName()) && Strings.isNotBlank(pageForm.getPageOrderBy())) { cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); } else { @@ -77,30 +91,79 @@ public class UnionCadreController { @At @ViReturn + @Aop(TransAop.READ_COMMITTED) public Object createCadre(UnionCadre cadre, String roleCode) { Sys_role role = roleService.fetch(Cnd.where("code", "=", roleCode)); - Cnd cnd = Cnd.where("userId", "=", cadre.getUserId()).and("roleId", "=", role.getId()).and("unionId", "=", cadre.getUnionId()).and("workersCongressSession", "=", cadre.getWorkersCongressSession()); + Cnd roleCnd = Cnd.where("userId", "=", cadre.getUserId()).and("roleId", "=", role.getId()).and("unionid", "=", cadre.getUnionId()); + Cnd pendingCnd = Cnd.where("userId", "=", cadre.getUserId()).and("roleId", "=", role.getId()).and("unionId", "=", cadre.getUnionId()) + .and("joining", "=", true).and("auditStateId", "=", UnionCadreAuditState.SCHOOL_UNION).and("leaveDate", "is", null); - if (unionCadreService.count(cnd) > 0) { + if (userRoleService.count(roleCnd) > 0 || unionCadreService.count(pendingCnd) > 0) { return Result.error("此操作重复!"); } - cadre.setJoining(true).setRoleId(role.getId()).setAuditStateId(UnionCadreAuditState.SCHOOL_UNION); + // 校工会管理员和系统管理员直接授权,不进入审核。 + boolean isAdmin = ShiroUtil.hasAnyRoles("sysadmin,SchoolUnionAdmin"); + cadre.setJoining(true).setRoleId(role.getId()).setAuditStateId(isAdmin + ? UnionCadreAuditState.SUCCESS + : UnionCadreAuditState.SCHOOL_UNION); unionCadreService.insert(cadre); + + if (isAdmin) { + Sys_user_role userRole = new Sys_user_role(); + userRole.setUserId(cadre.getUserId()); + userRole.setRoleId(role.getId()); + userRole.setUnionid(cadre.getUnionId()); + userRoleService.insert(userRole); + userService.deleteCache(cadre.getUserId()); + } return null; } @At @ViReturn + @Aop(TransAop.READ_COMMITTED) public Object removeCadre(String id) { UnionCadre cadre = unionCadreService.fetch(id); - if (UnionCadreAuditState.SUCCESS != cadre.getAuditStateId() && cadre.isJoining()) { - unionCadreService.delete(id); - } else { - cadre.setJoining(false).setAuditStateId(UnionCadreAuditState.SCHOOL_UNION); - unionCadreService.update(cadre); + if (cadre == null) { + return Result.error("未找到对应干部记录!"); } + if (cadre.getLeaveDate() != null) { + unionCadreService.delete(id); + return null; + } + + // 删除在任干部时直接移除角色,不发起退出审核。 + userRoleService.clear(Cnd.where("userId", "=", cadre.getUserId()).and("roleId", "=", cadre.getRoleId()).and("unionid", "=", cadre.getUnionId())); + unionCadreService.delete(id); + userService.deleteCache(cadre.getUserId()); + return null; + } + + @At + @ViReturn + @Aop(TransAop.READ_COMMITTED) + public Object leaveCadre(String id, Date leaveDate) { + if (leaveDate == null) { + return Result.error("请选择离任时间!"); + } + + UnionCadre cadre = unionCadreService.fetch(id); + if (cadre == null) { + return Result.error("未找到对应干部记录!"); + } + if (cadre.getLeaveDate() != null) { + return Result.error("该干部已离任!"); + } + if (UnionCadreAuditState.SUCCESS != cadre.getAuditStateId() || !cadre.isJoining()) { + return Result.error("仅已审核通过的在任干部可办理离任!"); + } + + cadre.setLeaveDate(leaveDate); + unionCadreService.update(cadre); + userRoleService.clear(Cnd.where("userId", "=", cadre.getUserId()).and("roleId", "=", cadre.getRoleId()).and("unionid", "=", cadre.getUnionId())); + userService.deleteCache(cadre.getUserId()); return null; } diff --git a/src/main/java/io/v/nutz/sys/models/UnionCadre.java b/src/main/java/io/v/nutz/sys/models/UnionCadre.java index 89a79ac..606ccfc 100644 --- a/src/main/java/io/v/nutz/sys/models/UnionCadre.java +++ b/src/main/java/io/v/nutz/sys/models/UnionCadre.java @@ -5,6 +5,8 @@ import lombok.Data; import lombok.experimental.Accessors; import org.nutz.dao.entity.annotation.*; +import java.util.Date; + /** * 基层干部 * @@ -48,6 +50,11 @@ public class UnionCadre { @ColDefine(type = ColType.BOOLEAN) private boolean joining; + @Column + @Comment("离任时间") + @ColDefine(type = ColType.DATETIME) + private Date leaveDate; + @Column @Comment("审核状态id") @ColDefine(type = ColType.INT, width = 8) diff --git a/src/main/resources/static/components/sys/union/cadre/Audit.vue b/src/main/resources/static/components/sys/union/cadre/Audit.vue index 720a21a..aa57735 100644 --- a/src/main/resources/static/components/sys/union/cadre/Audit.vue +++ b/src/main/resources/static/components/sys/union/cadre/Audit.vue @@ -4,10 +4,26 @@ @@ -18,6 +34,7 @@ @selection-change="handleSelectionChange"> @@ -71,6 +88,11 @@ module.exports = { }, data() { return { + pageForm: { + audit: false, + workersCongressSession: '', + unionId: '' + }, tableColumns: [ {prop: 'username', label: '姓名'}, {prop: 'loginname', label: '工号'}, @@ -79,11 +101,27 @@ module.exports = { {prop: 'rolename', label: '职务'}, {prop: 'workersCongressSession', label: '届次'}, {prop: 'joining', label: '申请类型'}, + {prop: 'stateName', label: '审核状态'}, ], multipleSelection: [], + sessionOptions: [], + usedSessionCodes: [], + unionOptions: [], } }, methods: { + hasSessionData(code) { + return this.usedSessionCodes.includes(code) + }, + async loadUsedSessionCodes() { + const {code, data} = await $.get('/platform/union/cadre/audit/usedSessions', { + unionId: this.pageForm.unionId + }) + this.usedSessionCodes = code === 0 ? (data || []) : [] + }, + unionChange() { + this.doSearch() + }, handleSelectionChange(val) { this.multipleSelection = val; }, @@ -105,6 +143,7 @@ module.exports = { }) }, pageData() { + this.loadUsedSessionCodes() sublime.showLoadingbar(); this.tableLoading = true $.post("/platform/union/cadre/audit/pageData", this.pageForm, (data) => { @@ -124,12 +163,18 @@ module.exports = { this.doSearch() } }, - created() { + async created() { + this.sessionOptions = await getDictOptions('jc') + this.unionOptions = await getUnions(null) this.doSearch() } } diff --git a/src/main/resources/static/components/sys/union/cadre/Mange.vue b/src/main/resources/static/components/sys/union/cadre/Mange.vue index 6245fae..fde2d23 100644 --- a/src/main/resources/static/components/sys/union/cadre/Mange.vue +++ b/src/main/resources/static/components/sys/union/cadre/Mange.vue @@ -87,7 +87,8 @@ align="center" show-overflow-tooltip > @@ -98,11 +99,15 @@ align="center" width="150"> @@ -211,7 +236,10 @@ module.exports = { roleCode: [{required: true, message: '必选', trigger: ['blur', 'change']}], userId: [{required: true, message: '必选', trigger: ['blur', 'change']}], }, - submitLoading: false + submitLoading: false, + leaveDialogVisible: false, + leaveForm: {}, + leaveSubmitLoading: false } }, watch: { @@ -282,6 +310,34 @@ module.exports = { this.$set(row, "delLoading", false) }, + openLeave(row) { + this.leaveForm = { + id: row.id, + leaveDate: '' + } + this.leaveDialogVisible = true + }, + + async doLeave() { + if (!this.leaveForm.leaveDate) { + this.$message.error('请选择离任时间') + return + } + + this.leaveSubmitLoading = true + const {code, msg} = await $.post('/platform/union/cadre/leaveCadre', this.leaveForm) + if (code === 0) { + this.leaveDialogVisible = false + this.pageData() + } + this.$notify({ + title: code === 0 ? '成功' : '失败', + type: code === 0 ? 'success' : 'error', + message: msg, + }) + this.leaveSubmitLoading = false + }, + async userRemoteMethod(query) { if (!query) { this.userOptions = [] diff --git a/src/main/resources/views/platform/sys/union/unionMange.html b/src/main/resources/views/platform/sys/union/unionMange.html index d8b6038..f4d974a 100644 --- a/src/main/resources/views/platform/sys/union/unionMange.html +++ b/src/main/resources/views/platform/sys/union/unionMange.html @@ -1129,8 +1129,8 @@ layout("/layouts/platform.html"){ 'tree-layout': httpVueLoader('/components/plugins/TreeLayout.vue'), 'dict-select': httpVueLoader('/components/plugins/DictSelect.vue'), 'union-cadre': httpVueLoader('/components/sys/union/UnionCadreMange.vue?v=1.0.89'), - 'union-cadre-mange': httpVueLoader('/components/sys/union/cadre/Mange.vue?v=1.0.5'), - 'union-cadre-audit': httpVueLoader('/components/sys/union/cadre/Audit.vue?v=1.0.5'), + 'union-cadre-mange': httpVueLoader('/components/sys/union/cadre/Mange.vue?v=1.0.9'), + 'union-cadre-audit': httpVueLoader('/components/sys/union/cadre/Audit.vue?v=1.0.7'), }, watch: { filterText(val) { From c8c432c7df13ad30a05d3d73b145978e3db04937 Mon Sep 17 00:00:00 2001 From: zhangrui Date: Wed, 21 Oct 2026 09:28:49 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=8B=89=E5=8F=96=E4=BA=BA=E5=91=98?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=9A18=E5=BC=80=E5=A4=B4=E7=9A=84?= =?UTF-8?q?=E5=B7=A5=E5=8F=B7=20=20=E4=B8=94=20=E4=BA=BA=E5=91=98=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E4=B8=BA=E7=A9=BA=20=E4=B8=94=20=E4=BA=BA=E5=91=98?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=8D=E4=B8=BA=E2=80=9C=E9=9D=9E=E5=90=88?= =?UTF-8?q?=E5=90=8C=E5=88=B6=E2=80=9D=E7=9A=84=E4=BA=BA=E5=91=98=EF=BC=8C?= =?UTF-8?q?=E5=B0=86=E8=BF=99=E4=BA=9B=E4=BA=BA=E7=9A=84=E4=BA=BA=E5=91=98?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=94=B9=E4=B8=BA=E2=80=9C=E5=90=88=E5=90=8C?= =?UTF-8?q?=E5=88=B6=E2=80=9D=E4=B8=94=E8=AE=BE=E7=BD=AE=E4=B8=BA=E4=BC=9A?= =?UTF-8?q?=E5=91=98=20=E4=BC=9A=E5=91=98=E6=A1=A3=E6=A1=88=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=EF=BC=9A=E5=88=86=E5=B7=A5=E4=BC=9A=E4=B8=BB=E5=B8=AD?= =?UTF-8?q?=E5=8F=AA=E8=83=BD=E6=9F=A5=E8=AF=A2=E5=BD=93=E5=89=8D=E5=88=86?= =?UTF-8?q?=E5=B7=A5=E4=BC=9A=E7=9A=84=E4=BA=BA=E5=91=98=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/SourceUserServiceImpl.java | 64 +++++++++++++++++++ .../info/MemberGroupInfoController.java | 26 ++++---- .../views/platform/member/info/GroupInfo.html | 9 ++- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java b/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java index cd4fffb..075193f 100644 --- a/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java @@ -20,6 +20,7 @@ import io.v.nutz.zhgh.data.model.UserSource; import io.v.nutz.zhgh.data.service.HistoryUserService; import io.v.nutz.zhgh.data.service.SourceUserService; import io.v.nutz.base.lang.Enum; +import io.v.nutz.zhgh.member.MemberMode; import io.v.nutz.zhgh.member.UserMode; import io.v.nutz.base.service.AsyncService; import io.v.nutz.base.service.impl.ViServiceImpl; @@ -67,6 +68,9 @@ import java.util.stream.Collectors; @CacheDefaults(cacheName = "user_source") @Slf4j public class SourceUserServiceImpl extends ViServiceImpl implements SourceUserService { + private static final String CONTRACT_PERSON_TYPE = "合同制"; + private static final String NON_CONTRACT_PERSON_TYPE = "非合同制"; + public SourceUserServiceImpl(Dao dao) { super(dao); } @@ -96,9 +100,25 @@ public class SourceUserServiceImpl extends ViServiceImpl implements public void pullSourceData(Boolean isIncrement) { Date now = new Date(); List users = SourceData.users(); + // 正式用户已明确标记为非合同制时,不能因数据中心人员类型为空而被改为合同制或自动入会。 + Set nonContractLoginNames = sysUserService.query(Cnd.where("personType", "=", NON_CONTRACT_PERSON_TYPE)) + .stream() + .map(Sys_user::getLoginname) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); asyncService.exe2(users, (user) -> { user.setPullTime(now); UserMode.initUser(user); + // 数据中心人员类型为空且工号以 18 开头时,仅非“非合同制”人员按合同制会员处理。 + if (isContractMemberCandidate(user)) { + if (nonContractLoginNames.contains(user.getLoginname())) { + // 使用 null 避免后续 updateIgnoreNull 将正式用户已有的“非合同制”覆盖为空字符串。 + user.setPersonType(null); + } else { + user.setPersonType(CONTRACT_PERSON_TYPE); + user.setMember(MemberMode.NORMAL.getCode()); + } + } }); dao().insert(users); } @@ -149,6 +169,8 @@ public class SourceUserServiceImpl extends ViServiceImpl implements List unitChangeUserList = new CopyOnWriteArrayList<>(); // 删除博士后出站的会员和福利会员角色 List deleteLeavingStationUserIdList = new CopyOnWriteArrayList<>(); + // 拉取时识别出的合同制人员,在正式用户保存后统一补充会员字段及会员角色。 + List addContractMemberUserIdList = new CopyOnWriteArrayList<>(); // 创建一个固定大小的线程池 可以根据服务器性能调整线程池大小 @@ -171,9 +193,11 @@ public class SourceUserServiceImpl extends ViServiceImpl implements continue; } + boolean shouldAddContractMember = isPulledContractMember(source); source.setMember(null); source.setWelfareMember(null); Sys_user user = userMap.get(source.getLoginname()); + Sys_user systemUser = user; UserChangeType changeType = getChangeType(source, user); @@ -214,12 +238,14 @@ public class SourceUserServiceImpl extends ViServiceImpl implements unitChangeUserList.add(u); } + boolean includedInUpdate = user == null; switch (sourceType) { case "all" -> { //复制属性到一个新的user对象 u.setId(user == null ? source.getId() : user.getId()); if (user != null) { needDoUpdateList.add(u); + includedInUpdate = true; } } case "add" -> { @@ -234,6 +260,7 @@ public class SourceUserServiceImpl extends ViServiceImpl implements } if (Strings.isNotBlank(finalUserPartMap.get(u.getLoginname())) && user != null) { needDoUpdateList.add(u); + includedInUpdate = true; } } } @@ -256,6 +283,12 @@ public class SourceUserServiceImpl extends ViServiceImpl implements histories.add(createUserHistory(user, changeTypes)); } + // 仅对本次实际新增或更新、且尚未入会的人员执行会员初始化,避免重复覆盖原入会时间。 + if (shouldAddContractMember && includedInUpdate + && !ObjectUtil.equal(systemUser != null ? systemUser.getMember() : null, MemberMode.NORMAL.getCode())) { + addContractMemberUserIdList.add(systemUser == null ? u.getId() : systemUser.getId()); + } + } }, Executors.newFixedThreadPool(numberOfThreads)); futures.add(future); @@ -277,6 +310,10 @@ public class SourceUserServiceImpl extends ViServiceImpl implements massUpdatesAsync(needDoUpdateList, false, null); } + if (Lang.isNotEmpty(addContractMemberUserIdList)) { + UserMode.addMemberRole(addContractMemberUserIdList.toArray(new String[0])); + } + if (Lang.isNotEmpty(deleteLeavingStationUserIdList)) { dao().clear(Sys_user_role.class, Cnd.where("userId", "in", deleteLeavingStationUserIdList).and("roleId", "=", Roles.MEMBER)); dao().update(Sys_user.class, Chain.make("member", 0).add("welfareMember", 0), Cnd.where("id", "in", deleteLeavingStationUserIdList)); @@ -377,6 +414,33 @@ public class SourceUserServiceImpl extends ViServiceImpl implements dao().clear(UserSource.class, Cnd.where("DATE(pullTime)", "<", formatAgoWeekDate)); } + /** + * 判断数据中心人员是否需要按合同制会员初始化。 + * + * @param user 数据中心返回的人员;工号需以 18 开头且人员类型为空 + * @return true 表示需要补充“合同制”人员类型并设置为会员 + */ + private boolean isContractMemberCandidate(Sys_user user) { + return user != null + && user.getLoginname() != null + && user.getLoginname().startsWith("18") + && StrUtil.isBlank(user.getPersonType()); + } + + /** + * 判断暂存人员是否为拉取阶段标记的合同制会员。 + * + * @param source 人员拉取暂存数据 + * @return true 表示人员类型及会员标记均符合自动入会条件 + */ + private boolean isPulledContractMember(UserSource source) { + return source != null + && source.getLoginname() != null + && source.getLoginname().startsWith("18") + && CONTRACT_PERSON_TYPE.equals(source.getPersonType()) + && ObjectUtil.equal(source.getMember(), MemberMode.NORMAL.getCode()); + } + private UserChangeType getChangeType(Sys_user source, Sys_user user) { //新入职 if (user == null) { diff --git a/src/main/java/io/v/nutz/zhgh/member/controller/info/MemberGroupInfoController.java b/src/main/java/io/v/nutz/zhgh/member/controller/info/MemberGroupInfoController.java index 9786a6a..41b8918 100644 --- a/src/main/java/io/v/nutz/zhgh/member/controller/info/MemberGroupInfoController.java +++ b/src/main/java/io/v/nutz/zhgh/member/controller/info/MemberGroupInfoController.java @@ -2,6 +2,7 @@ package io.v.nutz.zhgh.member.controller.info; 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.util.StrUtil; import io.v.nutz.base.annontation.ViReturn; @@ -153,16 +154,15 @@ public class MemberGroupInfoController { cnd.where().andLike("jobTitle", jobTitle); } - if (!ShiroUtil.hasAnyRoles("sysadmin,H03,A06")) { - if (ShiroUtil.hasRole("H04")) { - cnd.and("unionId", "=", Vi.getUnionId()); - } else if (ShiroUtil.hasRole("ghxzzz")) { - cnd.and("unionId", "=", Vi.getUnionId()); - SqlExpressionGroup seg = new SqlExpressionGroup(); - seg.or("unionGroupId", "in", io.v.nutz.web.commons.utils.ShiroUtil.getUnionGroupIds()); - seg.or("unionGroupId", "is", null); - cnd.and(seg); - } + // 分工会主席可能同时具有校会员管理员角色,除系统管理员和校级管理员外,必须优先限制为当前分工会。 + if (ShiroUtil.hasRole("H04") && !ShiroUtil.hasAnyRoles("sysadmin,A06")) { + cnd.and("unionId", "=", Vi.getUnionId()); + } else if (!ShiroUtil.hasAnyRoles("sysadmin,H03,A06") && ShiroUtil.hasRole("ghxzzz")) { + cnd.and("unionId", "=", Vi.getUnionId()); + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or("unionGroupId", "in", io.v.nutz.web.commons.utils.ShiroUtil.getUnionGroupIds()); + seg.or("unionGroupId", "is", null); + cnd.and(seg); } @@ -249,7 +249,7 @@ public class MemberGroupInfoController { @Param(value = "memberSearchName", required = false) String memberSearchName, @Param(value = "memberSearchKeyWord", required = false) String memberSearchKeyWord) { try { - ViTool.excelResponse(response, "会员名单.xls"); + ViTool.excelResponse(response, "会员名单.xlsx"); List dataList = memberService.listMap(dataSql(pageForm, unionId, unitId, threeUnitId, unionGroupId, Json.fromJsonAsArray(String.class, personTypes), Json.fromJsonAsArray(String.class, userStates), campus, sex, memberStatus, position, jobTitle, memberSearchName, memberSearchKeyWord)); List entityList = new ArrayList<>(); ExcelExportEntity no = new ExcelExportEntity("序号", "no", 10); @@ -260,7 +260,9 @@ public class MemberGroupInfoController { propMap.forEach((k, v) -> { entityList.add(new ExcelExportEntity(v, k, 20)); }); - Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), entityList, dataList); + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, dataList); workbook.write(response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/resources/views/platform/member/info/GroupInfo.html b/src/main/resources/views/platform/member/info/GroupInfo.html index 7e0b992..d69e8d5 100644 --- a/src/main/resources/views/platform/member/info/GroupInfo.html +++ b/src/main/resources/views/platform/member/info/GroupInfo.html @@ -560,7 +560,12 @@ layout("/layouts/platform.html"){ }, async created() { this.pageData(); - if ("${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')}" === 'true') { + // 分工会主席可能兼具 H03 角色,需优先按当前分工会加载查询选项。 + if ("${@shiro.hasRole('H04')&&!(@shiro.hasRole('sysadmin')||@shiro.hasRole('A06'))}" === 'true') { + this.unions = await getUnions(null, true) + this.$set(this.pageForm, 'unionId', this.unions ? this.unions[0].id : null) + this.flushUnits() + } else if ("${@shiro.hasRole('sysadmin')||@shiro.hasRole('A06')||@shiro.hasRole('H03')}" === 'true') { this.unions = await getUnions(null, false) } else { this.unions = await getUnions(null, true) @@ -574,4 +579,4 @@ layout("/layouts/platform.html"){ \ No newline at end of file +#--> From 0e2c5d49d0e324f3159035a0268327c35bbf70f1 Mon Sep 17 00:00:00 2001 From: hongqiwei <1143662660@qq.com> Date: Wed, 21 Oct 2026 09:52:37 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=89=93=E5=8C=85=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dependency-reduced-pom.xml | 14 +++----------- pom.xml | 17 +++++------------ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 2054f7c..0145022 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -20,10 +20,9 @@ maven-compiler-plugin 3.7.0 - 14 - 14 + ${java.version} + ${java.version} false - --enable-preview @@ -70,13 +69,6 @@ nutzboot-maven-plugin ${nutzboot.version} - - maven-compiler-plugin - - 15 - 15 - - @@ -175,7 +167,7 @@ 1.12.1 1.6.2 2.3.1 - 1.18.12 + 1.18.16 2.2.2 2.4.2-SNAPSHOT diff --git a/pom.xml b/pom.xml index a0c7871..49a90ff 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,8 @@ 4.1.2 1.6.2 4.0.11 - 1.18.12 + + 1.18.16 1.0.1 5.7.20 2.2.2 @@ -762,10 +763,10 @@ maven-compiler-plugin 3.7.0 - 14 - 14 + + ${java.version} + ${java.version} false - --enable-preview @@ -816,14 +817,6 @@ nutzboot-maven-plugin ${nutzboot.version} - - org.apache.maven.plugins - maven-compiler-plugin - - 15 - 15 - -