From 4373cc0095d650308d8a13b3b140415d766b7e6d Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Mon, 14 Sep 2026 20:21:23 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FlowTodoCenterController.java | 3 +- .../controller/SysDataUserPullController.java | 11 -- .../SysDataUserUpdateController.java | 49 ++++- .../sys/services/SysDataUserPullService.java | 16 ++ .../impl/SysDataUserPullServiceImpl.java | 95 ++++++++++ .../info/MemberInfoGroupController.java | 29 --- .../platform/js/mixin/initTableMixins.js | 4 +- .../views/platform/sys/data/user/pull.html | 40 +--- .../views/platform/sys/data/user/update.html | 175 +++++++++++++++++- .../staffmanage/member/info/group/index.html | 44 +---- 10 files changed, 333 insertions(+), 133 deletions(-) diff --git a/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java b/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java index 11fcd5ae..9fb6c3ec 100644 --- a/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java +++ b/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java @@ -230,7 +230,7 @@ public class FlowTodoCenterController { dao.execute(todoSql); int todoCount = todoSql.getInt(); - // 查询我的已办任务 + // 已办统计排除发起节点 startTask,与已办列表保持一致,避免将本人发起流程计入已办数量。 Sql doneSql = Sqls.create(""" SELECT count(1) @@ -240,6 +240,7 @@ public class FlowTodoCenterController { WHERE ta.actorId = @userId AND t.taskState = 20 + AND t.taskName != 'startTask' """); doneSql.setParam("userId", SecurityUtil.getUserId()); doneSql.setCallback(Sqls.callback.integer()); diff --git a/src/main/java/com/budwk/app/sys/controller/SysDataUserPullController.java b/src/main/java/com/budwk/app/sys/controller/SysDataUserPullController.java index 95682a9f..fa206eb9 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysDataUserPullController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysDataUserPullController.java @@ -69,17 +69,6 @@ public class SysDataUserPullController { return Result.success(); } - @At - @SaCheckPermission("sys.data.user.pull") - @ApiOperation("同步教职工手机号") - public Result syncTeacherMobile() { - try { - return Result.success(sysUserPullService.syncTeacherMobile()); - } catch (Exception e) { - return Result.error(e.getMessage()); - } - } - @At @SaCheckPermission("sys.data.user.pull") @ApiOperation("删除用户数据") diff --git a/src/main/java/com/budwk/app/sys/controller/SysDataUserUpdateController.java b/src/main/java/com/budwk/app/sys/controller/SysDataUserUpdateController.java index 3f517992..28c78835 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysDataUserUpdateController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysDataUserUpdateController.java @@ -17,6 +17,8 @@ import com.budwk.app.sys.services.*; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiOperation; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.dao.Cnd; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; @@ -47,6 +49,8 @@ public class SysDataUserUpdateController { @Inject private SysUserService sysUserService; @Inject + private SysUserMobileSyncService sysUserMobileSyncService; + @Inject private SysDataUserPullService sysDataUserPullService; @Inject private SysDataUnitPullService sysDataUnitPullService; @@ -62,7 +66,8 @@ public class SysDataUserUpdateController { @At @SaCheckPermission("sys.data.user.pull") @ApiOperation("分页数据") - @Ok("json:{locked:'password|idCard|mobile'}") + // 手机号用于列表核对拉取结果;密码和身份证号仍不返回。 + @Ok("json:{locked:'password|idCard'}") public Result pageData(@Valid SysDataUserUpdatePageForm pageForm) { Sql sql = Sqls.create(""" SELECT @@ -136,4 +141,46 @@ public class SysDataUserUpdateController { sysDataDictPullService.pullDataDict(); return Result.success(); } + /** + * 预览:scope传ALL/MEMBER/FILTER,mode传FILL/REFRESH,pageForm传列表筛选条件。 + * 返回NutMap,scopeCount/existingCount/cooldownCount/total分别为范围、已有号、冷却及待请求工号数。 + */ + @At + @SaCheckPermission("sys.data.user.update") + public Result mobilePreview(String scope, String mode, SysDataUserUpdatePageForm pageForm) { + return Result.success(sysUserMobileSyncService.preview(scope, mode, pageForm)); + } + + /** + * 启动:scope/mode及pageForm同预览;action传NEW/RESUME/RETRY,sourceTaskId仅继续或重试时传原任务编号。 + * 返回NutMap进度:taskId任务编号、status状态及total/processed/updated/empty/failed/skipped各项人数。 + */ + @At + @SaCheckPermission("sys.data.user.update") + @Aop(TransAop.READ_COMMITTED) + @SLog(tag = "手机号拉取", msg = "启动手机号后台拉取任务") + public Result mobileStart(String scope, String mode, SysDataUserUpdatePageForm pageForm, String sourceTaskId, String action) { + return Result.success(sysUserMobileSyncService.start(scope, mode, pageForm, sourceTaskId, action)); + } + + /** + * 进度:taskId可为空(最近任务)或传启动接口返回的编号;返回与启动一致的NutMap,不返回手机号与工号队列。 + */ + @At + @SaCheckPermission("sys.data.user.update") + public Result mobileStatus(String taskId) { + return Result.success(sysUserMobileSyncService.status(taskId)); + } + + /** + * 停止:taskId必须传当前任务编号;返回NutMap最新进度,当前请求结束后停止且保留已完成结果。 + */ + @At + @SaCheckPermission("sys.data.user.update") + @Aop(TransAop.READ_COMMITTED) + @SLog(tag = "手机号拉取", msg = "停止手机号后台拉取任务") + public Result mobileStop(String taskId) { + return Result.success(sysUserMobileSyncService.stop(taskId)); + } + } diff --git a/src/main/java/com/budwk/app/sys/services/SysDataUserPullService.java b/src/main/java/com/budwk/app/sys/services/SysDataUserPullService.java index f2d90c61..f21837bb 100644 --- a/src/main/java/com/budwk/app/sys/services/SysDataUserPullService.java +++ b/src/main/java/com/budwk/app/sys/services/SysDataUserPullService.java @@ -53,4 +53,20 @@ public interface SysDataUserPullService extends BaseService { * @return 同步过程统计信息,包含工号手机号映射 */ NutMap syncTeacherMobileByJobNos(List jobNos); + /** + * 按单个工号读取数据中心手机号,不修改数据库;空字符串表示接口明确没有号码。 + * @param jobNo 非空教职工工号 + * @return 已校验的手机号;接口异常、工号不匹配或号码格式异常时抛出异常 + */ + String fetchTeacherMobile(String jobNo); + + /** + * 保存单人工号的有效号码,使用请求前快照避免覆盖期间的人工修改。 + * @param jobNo 数据中心查询工号 + * @param mobile 已校验的非空手机号 + * @param originalMobiles 请求前的用户ID到原手机号映射 + * @param fillOnly true仅补空号,false核对更新已有号码 + * @return 实际更新的系统用户行数 + */ + int saveTeacherMobile(String jobNo, String mobile, Map originalMobiles, boolean fillOnly); } diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java index b96b8aa2..e3d2695d 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java @@ -438,6 +438,101 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl return listMap(sql); } + /** + * 后台任务逐工号读取号码:每个分页请求均设置超时,且不输出号码或接口原文。 + * 分页间等待一秒,避免单人工号多页响应绕过任务限速。 + */ + @Override + public String fetchTeacherMobile(String jobNo) { + DataCenterProperties.Credential credential = dataCenterProperties.credential(TEACHER_MOBILE_CONFIG_KEY); + Set numbers = new LinkedHashSet<>(); + int pages = 1; + for (int page = 1; page <= pages; page++) { + if (page > 1) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BaseException("手机号拉取已中断"); + } + } + String body; + try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(credential.getUrl()) + .header(Header.CONTENT_TYPE, "application/json") + .header("X-H3C-TOKEN", credential.getToken()) + .timeout(10000) + .body(JSONUtil.toJsonStr(buildTeacherMobileRequestBody(jobNo, page, TEACHER_MOBILE_PAGE_SIZE))) + .execute()) { + if (response.getStatus() != 200) { + throw new BaseException("手机号接口HTTP异常,请稍后重试"); + } + body = response.body(); + } + pages = collectTeacherMobilePage(body, jobNo, numbers); + } + return numbers.isEmpty() ? "" : numbers.iterator().next(); + } + + /** 校验单工号响应结构、页数与号码一致性;只有明确的空records或空号码才视为无号码。 */ + private int collectTeacherMobilePage(String body, String jobNo, Set numbers) { + JSONObject response = JSONUtil.parseObj(body); + if (!"0".equals(response.getStr("code"))) { + throw new BaseException("手机号接口未成功返回,请检查接口授权或稍后重试"); + } + JSONObject data = response.getJSONObject("data"); + JSONObject pagination = data == null ? response : data; + int pages = Math.max(pagination.getInt("pages", 1), + (int) Math.ceil(pagination.getInt("total", 0) / (double) TEACHER_MOBILE_PAGE_SIZE)); + // 单工号异常大分页通常意味着过滤条件失效,禁止继续当作全量接口拉取。 + if (pages > 5) { + throw new BaseException("单工号返回数据量异常"); + } + JSONArray records = response.getJSONArray("records"); + if (records == null && data != null) records = data.getJSONArray("records"); + if (records == null) { + throw new BaseException("手机号接口缺少records字段"); + } + for (Object record : records) { + JSONObject row = JSONUtil.parseObj(record); + if (!jobNo.equals(StrUtil.trimToEmpty(row.getStr("gh")))) { + throw new BaseException("手机号接口返回工号不匹配"); + } + String mobile = StrUtil.trimToEmpty(row.getStr("sjh")); + if (mobile.startsWith("+86")) mobile = mobile.substring(3); + if (mobile.startsWith("0086")) mobile = mobile.substring(4); + if (StrUtil.isNotBlank(mobile)) { + if (!mobile.matches("1[3-9][0-9]{9}")) { + throw new BaseException("接口手机号格式异常"); + } + numbers.add(mobile); + } + } + if (numbers.size() > 1) { + throw new BaseException("同一工号返回多个不同手机号,需人工核实"); + } + return pages; + } + + /** + * 仅在原手机号未被他人修改时落库;空返回值不清空原号,补齐模式不覆盖已有号码。 + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public int saveTeacherMobile(String jobNo, String mobile, Map originalMobiles, boolean fillOnly) { + if (StrUtil.isBlank(mobile) || !mobile.matches("1[3-9][0-9]{9}")) return 0; + int updated = 0; + for (Map.Entry entry : originalMobiles.entrySet()) { + String oldMobile = entry.getValue(); + if (Objects.equals(oldMobile, mobile) || (fillOnly && StrUtil.isNotBlank(oldMobile))) continue; + Cnd cnd = Cnd.where(Sys_user::getId, "=", entry.getKey()) + .and(Sys_user::getLoginname, "=", jobNo); + if (oldMobile == null) cnd.and("mobile", "IS", null); + else cnd.and("mobile", "=", oldMobile); + updated += dao().update(Sys_user.class, Chain.make("mobile", mobile), cnd); + } + return updated; + } + /** * 从数据中心分页同步教职工手机号,并按工号更新 sys_user.mobile。 */ diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/info/MemberInfoGroupController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/info/MemberInfoGroupController.java index 462a4476..bd1c659a 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/info/MemberInfoGroupController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/info/MemberInfoGroupController.java @@ -5,11 +5,8 @@ import cn.dev33.satoken.annotation.SaMode; import cn.hutool.core.util.DesensitizedUtil; import com.alibaba.excel.EasyExcel; import com.budwk.app.base.annotation.SLog; -import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.result.Result; -import com.budwk.app.sys.services.SysDataUserPullService; -import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.zhgh.staffmanage.member.models.MemberHistory; import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm; import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService; @@ -51,9 +48,6 @@ public class MemberInfoGroupController { @Inject private MemberInfoService memberInfoService; - @Inject - private SysDataUserPullService sysUserPullService; - @At("") @Ok("beetl:/platform/zhgh/staffmanage/member/info/group/index.html") @SaCheckPermission("member.info.group") @@ -86,29 +80,6 @@ public class MemberInfoGroupController { return Result.success(memberInfoService.dao().count(MemberHistory.class, Cnd.where("year", "=", year))); } - @At - @SaCheckPermission("member.info.group") - @Aop(TransAop.READ_COMMITTED) - @SLog(tag = "会员档案管理", msg = "获取会员手机号") - public Result syncMobile() { - try { - if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { - return Result.error("当前用户无权获取会员手机号"); - } - List jobNos = memberInfoService.queryRandomBlankMobileMemberJobNos(500); - if (jobNos.isEmpty()) { - return Result.error("当前没有手机号为空的会员"); - } - NutMap result = sysUserPullService.syncTeacherMobileByJobNos(jobNos); - result.put("sampleCount", jobNos.size()); - return Result.success(result); - } catch (Exception e) { - log.error("会员手机号获取失败", e); - return Result.error(e.getMessage()); - } - } - - @At @SaCheckPermission("member.info.group") @Aop(TransAop.READ_COMMITTED) diff --git a/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js b/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js index f038667d..9a106977 100644 --- a/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js +++ b/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js @@ -69,11 +69,13 @@ const initTableMixins = { const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData" this.tableLoading = true this.$axios.post(address, data ? data : this.pageForm).then((res) => { - this.tableLoading = false if (res.code === 0) { this.tableData = res.data.list this.pageForm.totalCount = res.data.totalCount } + }).finally(() => { + // 列表请求成功或失败后均关闭加载遮罩,避免异常时持续转圈。 + this.tableLoading = false }) }, notifySuccess(msg) { diff --git a/src/main/resources/views/platform/sys/data/user/pull.html b/src/main/resources/views/platform/sys/data/user/pull.html index 34036c1b..bf9f69f8 100644 --- a/src/main/resources/views/platform/sys/data/user/pull.html +++ b/src/main/resources/views/platform/sys/data/user/pull.html @@ -63,7 +63,6 @@ layout("/layouts/platform.html"){ 拉取信息中心数据 - 同步教职工手机号 删除本地数据源 @@ -121,8 +120,7 @@ layout("/layouts/platform.html"){ pullTimeOptions: false, sourceTime: [], pullTimeDialogVisible: false, - pullLoading: false, - syncTeacherMobileLoading: false + pullLoading: false } }, methods: { @@ -150,42 +148,6 @@ layout("/layouts/platform.html"){ .catch(() => {}) }, - syncTeacherMobile() { - this.$confirm("确定要同步教职工手机号吗?", "提示", { - type: "warning", - confirmButtonText: "确定", - cancelButtonText: "取消" - }) - .then(() => { - this.syncTeacherMobileLoading = true - this.$axios - .post("/platform/sys/data/user/pull/syncTeacherMobile") - .then((resp) => { - if (resp.code === 0) { - const data = resp.data || {} - this.$alert( - "接口总数:" + (data.total || 0) + - "
总页数:" + (data.pages || 0) + - "
拉取数量:" + (data.pulledCount || 0) + - "
有效手机号数量:" + (data.validMobileCount || 0) + - "
匹配用户数量:" + (data.matchedUserCount || 0) + - "
更新用户数量:" + (data.updatedUserCount || 0), - "同步完成", - { - dangerouslyUseHTMLString: true, - confirmButtonText: "确定" - } - ) - this.pageData() - } - }) - .finally(() => { - this.syncTeacherMobileLoading = false - }) - }) - .catch(() => {}) - }, - getSearchOptions() { this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => { if (resp.code === 0) { diff --git a/src/main/resources/views/platform/sys/data/user/update.html b/src/main/resources/views/platform/sys/data/user/update.html index 299ffc1b..f664e4b7 100644 --- a/src/main/resources/views/platform/sys/data/user/update.html +++ b/src/main/resources/views/platform/sys/data/user/update.html @@ -51,10 +51,11 @@ layout("/layouts/platform.html"){ 更新【码表】数据 + 拉取【手机号】 更新【单位】数据 更新【人员】数据 - + @@ -67,7 +68,8 @@ layout("/layouts/platform.html"){ - + + @@ -75,14 +77,8 @@ layout("/layouts/platform.html"){ - - - - - - @@ -129,6 +125,51 @@ layout("/layouts/platform.html"){ 确 定 + + + + + + + + + + + + + 只补手机号为空的人员 + 重新核对(含已有号码) + + + + 预览本次人数 + + +

+ 范围 {{mobilePreview.scopeCount}} 人,已有号码 {{mobilePreview.existingCount}} 人, + 冷却跳过 {{mobilePreview.cooldownCount}} 人,本次待请求 {{mobilePreview.total}} 人(按工号去重)。 +

+

按每秒最多 1 次请求执行,实际耗时还取决于接口响应速度。

+
+ 最近任务 +

{{mobileStatusLabel}} · {{mobileTask.message}}

+

任务范围:{{mobileTask.scope === 'MEMBER' ? '仅会员' : (mobileTask.scope === 'FILTER' ? '筛选结果' : '全部系统人员')}}; + 模式:{{mobileTask.mode === 'REFRESH' ? '重新核对' : '只补空号'}}

+ +

已处理 {{mobileTask.processed || 0}} / {{mobileTask.total || 0}}; + 已更新 {{mobileTask.updated || 0}};无号码 {{mobileTask.empty || 0}}; + 失败 {{mobileTask.failed || 0}};跳过或未变更 {{mobileTask.skipped || 0}}。

+

任务与续传记录保留七天。继续任务会先处理剩余人员,再重试原失败项。

+
+ + 关闭 + 停止任务 + 继续任务 + 仅重试失败项 + 开始拉取 + +
+