Merge remote-tracking branch 'origin/main'

This commit is contained in:
dd3s_206
2026-09-14 20:46:22 +08:00
12 changed files with 473 additions and 188 deletions
@@ -230,7 +230,7 @@ public class FlowTodoCenterController {
dao.execute(todoSql); dao.execute(todoSql);
int todoCount = todoSql.getInt(); int todoCount = todoSql.getInt();
// 查询我的已办任务 // 已办统计排除发起节点 startTask,与已办列表保持一致,避免将本人发起流程计入已办数量。
Sql doneSql = Sqls.create(""" Sql doneSql = Sqls.create("""
SELECT SELECT
count(1) count(1)
@@ -240,6 +240,7 @@ public class FlowTodoCenterController {
WHERE WHERE
ta.actorId = @userId ta.actorId = @userId
AND t.taskState = 20 AND t.taskState = 20
AND t.taskName != 'startTask'
"""); """);
doneSql.setParam("userId", SecurityUtil.getUserId()); doneSql.setParam("userId", SecurityUtil.getUserId());
doneSql.setCallback(Sqls.callback.integer()); doneSql.setCallback(Sqls.callback.integer());
@@ -69,17 +69,6 @@ public class SysDataUserPullController {
return Result.success(); 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 @At
@SaCheckPermission("sys.data.user.pull") @SaCheckPermission("sys.data.user.pull")
@ApiOperation("删除用户数据") @ApiOperation("删除用户数据")
@@ -17,6 +17,8 @@ import com.budwk.app.sys.services.*;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation; 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.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
@@ -47,6 +49,8 @@ public class SysDataUserUpdateController {
@Inject @Inject
private SysUserService sysUserService; private SysUserService sysUserService;
@Inject @Inject
private SysUserMobileSyncService sysUserMobileSyncService;
@Inject
private SysDataUserPullService sysDataUserPullService; private SysDataUserPullService sysDataUserPullService;
@Inject @Inject
private SysDataUnitPullService sysDataUnitPullService; private SysDataUnitPullService sysDataUnitPullService;
@@ -62,7 +66,8 @@ public class SysDataUserUpdateController {
@At @At
@SaCheckPermission("sys.data.user.pull") @SaCheckPermission("sys.data.user.pull")
@ApiOperation("分页数据") @ApiOperation("分页数据")
@Ok("json:{locked:'password|idCard|mobile'}") // 手机号用于列表核对拉取结果;密码和身份证号仍不返回。
@Ok("json:{locked:'password|idCard'}")
public Result pageData(@Valid SysDataUserUpdatePageForm pageForm) { public Result pageData(@Valid SysDataUserUpdatePageForm pageForm) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -136,4 +141,46 @@ public class SysDataUserUpdateController {
sysDataDictPullService.pullDataDict(); sysDataDictPullService.pullDataDict();
return Result.success(); return Result.success();
} }
/**
* 预览:scope传ALL/MEMBER/FILTERmode传FILL/REFRESHpageForm传列表筛选条件。
* 返回NutMapscopeCount/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/RETRYsourceTaskId仅继续或重试时传原任务编号。
* 返回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));
}
} }
@@ -53,4 +53,20 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
* @return 同步过程统计信息,包含工号手机号映射 * @return 同步过程统计信息,包含工号手机号映射
*/ */
NutMap syncTeacherMobileByJobNos(List<String> jobNos); NutMap syncTeacherMobileByJobNos(List<String> 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<String, String> originalMobiles, boolean fillOnly);
} }
@@ -438,6 +438,101 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
return listMap(sql); return listMap(sql);
} }
/**
* 后台任务逐工号读取号码:每个分页请求均设置超时,且不输出号码或接口原文。
* 分页间等待一秒,避免单人工号多页响应绕过任务限速。
*/
@Override
public String fetchTeacherMobile(String jobNo) {
DataCenterProperties.Credential credential = dataCenterProperties.credential(TEACHER_MOBILE_CONFIG_KEY);
Set<String> 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<String> 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<String, String> originalMobiles, boolean fillOnly) {
if (StrUtil.isBlank(mobile) || !mobile.matches("1[3-9][0-9]{9}")) return 0;
int updated = 0;
for (Map.Entry<String, String> 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。 * 从数据中心分页同步教职工手机号,并按工号更新 sys_user.mobile。
*/ */
@@ -5,11 +5,8 @@ import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.DesensitizedUtil;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.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.models.MemberHistory;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm; import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService; import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService;
@@ -51,9 +48,6 @@ public class MemberInfoGroupController {
@Inject @Inject
private MemberInfoService memberInfoService; private MemberInfoService memberInfoService;
@Inject
private SysDataUserPullService sysUserPullService;
@At("") @At("")
@Ok("beetl:/platform/zhgh/staffmanage/member/info/group/index.html") @Ok("beetl:/platform/zhgh/staffmanage/member/info/group/index.html")
@SaCheckPermission("member.info.group") @SaCheckPermission("member.info.group")
@@ -86,29 +80,6 @@ public class MemberInfoGroupController {
return Result.success(memberInfoService.dao().count(MemberHistory.class, Cnd.where("year", "=", year))); 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<String> 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 @At
@SaCheckPermission("member.info.group") @SaCheckPermission("member.info.group")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@@ -69,11 +69,13 @@ const initTableMixins = {
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData" const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
this.tableLoading = true this.tableLoading = true
this.$axios.post(address, data ? data : this.pageForm).then((res) => { this.$axios.post(address, data ? data : this.pageForm).then((res) => {
this.tableLoading = false
if (res.code === 0) { if (res.code === 0) {
this.tableData = res.data.list this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount this.pageForm.totalCount = res.data.totalCount
} }
}).finally(() => {
// 列表请求成功或失败后均关闭加载遮罩,避免异常时持续转圈。
this.tableLoading = false
}) })
}, },
notifySuccess(msg) { notifySuccess(msg) {
@@ -63,7 +63,6 @@ layout("/layouts/platform.html"){
<el-card shadow="never"> <el-card shadow="never">
<table-tool :label="pageForm.pullTime ? pageForm.pullTime + '数据' : '全部数据'"> <table-tool :label="pageForm.pullTime ? pageForm.pullTime + '数据' : '全部数据'">
<el-button type="primary" size="mini" :loading="pullLoading" @click="pull" icon="el-icon-download">拉取信息中心数据</el-button> <el-button type="primary" size="mini" :loading="pullLoading" @click="pull" icon="el-icon-download">拉取信息中心数据</el-button>
<el-button type="primary" size="mini" :loading="syncTeacherMobileLoading" @click="syncTeacherMobile" icon="el-icon-mobile-phone">同步教职工手机号</el-button>
<el-button type="danger" size="mini" @click="openDelete" icon="el-icon-delete">删除本地数据源</el-button> <el-button type="danger" size="mini" @click="openDelete" icon="el-icon-delete">删除本地数据源</el-button>
</table-tool> </table-tool>
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center" v-loading="tableLoading"> <el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center" v-loading="tableLoading">
@@ -121,8 +120,7 @@ layout("/layouts/platform.html"){
pullTimeOptions: false, pullTimeOptions: false,
sourceTime: [], sourceTime: [],
pullTimeDialogVisible: false, pullTimeDialogVisible: false,
pullLoading: false, pullLoading: false
syncTeacherMobileLoading: false
} }
}, },
methods: { methods: {
@@ -150,42 +148,6 @@ layout("/layouts/platform.html"){
.catch(() => {}) .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) +
"<br>总页数:" + (data.pages || 0) +
"<br>拉取数量:" + (data.pulledCount || 0) +
"<br>有效手机号数量:" + (data.validMobileCount || 0) +
"<br>匹配用户数量:" + (data.matchedUserCount || 0) +
"<br>更新用户数量:" + (data.updatedUserCount || 0),
"同步完成",
{
dangerouslyUseHTMLString: true,
confirmButtonText: "确定"
}
)
this.pageData()
}
})
.finally(() => {
this.syncTeacherMobileLoading = false
})
})
.catch(() => {})
},
getSearchOptions() { getSearchOptions() {
this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => { this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => {
if (resp.code === 0) { if (resp.code === 0) {
@@ -51,10 +51,11 @@ layout("/layouts/platform.html"){
<el-card shadow="never"> <el-card shadow="never">
<table-tool label="系统当前用户数据"> <table-tool label="系统当前用户数据">
<el-button type="primary" size="mini" @click="updateDataDict" icon="el-icon-refresh">更新【码表】数据</el-button> <el-button type="primary" size="mini" @click="updateDataDict" icon="el-icon-refresh">更新【码表】数据</el-button>
<el-button v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])" type="primary" size="mini" @click="openMobileSync" icon="el-icon-phone">拉取【手机号】</el-button>
<el-button type="primary" size="mini" @click="updateUnits" icon="el-icon-refresh">更新【单位】数据</el-button> <el-button type="primary" size="mini" @click="updateUnits" icon="el-icon-refresh">更新【单位】数据</el-button>
<el-button type="primary" size="mini" @click="openUpdate" icon="el-icon-refresh">更新【人员】数据</el-button> <el-button type="primary" size="mini" @click="openUpdate" icon="el-icon-refresh">更新【人员】数据</el-button>
</table-tool> </table-tool>
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center"> <el-table v-loading="tableLoading" :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center">
<el-table-column type="index" width="70" label="序号" fixed="left"></el-table-column> <el-table-column type="index" width="70" label="序号" fixed="left"></el-table-column>
<el-table-column prop="loginname" label="工号" width="100" fixed="left"></el-table-column> <el-table-column prop="loginname" label="工号" width="100" fixed="left"></el-table-column>
<el-table-column prop="username" label="姓名" fixed="left" width="120" show-overflow-tooltip></el-table-column> <el-table-column prop="username" label="姓名" fixed="left" width="120" show-overflow-tooltip></el-table-column>
@@ -67,7 +68,8 @@ layout("/layouts/platform.html"){
</el-table-column> </el-table-column>
<el-table-column prop="changeTime" label="变更时间" sortable width="150"></el-table-column> <el-table-column prop="changeTime" label="变更时间" sortable width="150"></el-table-column>
<el-table-column prop="sex" label="性别" sortable></el-table-column> <el-table-column prop="sex" label="性别" sortable></el-table-column>
<!-- <el-table-column prop="mobile" label="手机号" width="120"></el-table-column>--> <!-- 展示系统当前手机号,便于核对后台拉取结果。 -->
<el-table-column prop="mobile" label="手机号" width="130"></el-table-column>
<el-table-column prop="birthday" label="生日" sortable width="120"> <el-table-column prop="birthday" label="生日" sortable width="120">
<template slot-scope="{row}">{{$moment(row.birthday).format('YYYY-MM-DD')}}</template> <template slot-scope="{row}">{{$moment(row.birthday).format('YYYY-MM-DD')}}</template>
</el-table-column> </el-table-column>
@@ -75,14 +77,8 @@ layout("/layouts/platform.html"){
<el-table-column prop="preparedBy" label="编制类别" sortable width="120"></el-table-column> <el-table-column prop="preparedBy" label="编制类别" sortable width="120"></el-table-column>
<el-table-column prop="personType" label="教职工类别" sortable width="120"></el-table-column> <el-table-column prop="personType" label="教职工类别" sortable width="120"></el-table-column>
<el-table-column prop="arrivalAtSchoolDate" label="来校年月" sortable width="120"></el-table-column> <el-table-column prop="arrivalAtSchoolDate" label="来校年月" sortable width="120"></el-table-column>
<el-table-column prop="technicalTitle" label="技术职称" sortable width="120"></el-table-column>
<el-table-column prop="position" label="职务" sortable width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="education" label="学历" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="academicDegree" label="学位" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="unitName" label="单位" show-overflow-tooltip sortable width="120"></el-table-column> <el-table-column prop="unitName" label="单位" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="unitId" label="单位编码" sortable width="120"></el-table-column> <el-table-column prop="unitId" label="单位编码" sortable width="120"></el-table-column>
<el-table-column prop="nationality" label="国籍" sortable></el-table-column>
<el-table-column prop="nation" label="民族" sortable></el-table-column>
</el-table> </el-table>
<!--#include("/layouts/pagination.html"){}#--> <!--#include("/layouts/pagination.html"){}#-->
</el-card> </el-card>
@@ -129,6 +125,51 @@ layout("/layouts/platform.html"){
<el-button type="primary" @click="execUpdate" :disabled="updateLoading">确 定</el-button> <el-button type="primary" @click="execUpdate" :disabled="updateLoading">确 定</el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog title="拉取手机号" :visible.sync="mobileDialog" width="680px" @closed="closeMobilePolling">
<el-alert title="后台逐人拉取,会员优先。关闭弹框不停止任务;无号码结果七天内跳过。" type="info" :closable="false"></el-alert>
<el-form label-width="110px" style="margin-top: 16px" :disabled="mobileRunning || formLoading">
<el-form-item label="人员范围">
<el-select v-model="mobileScope" @change="invalidateMobilePreview">
<el-option label="全部系统人员" value="ALL"></el-option>
<el-option label="仅会员" value="MEMBER"></el-option>
<el-option label="当前筛选的全部结果" value="FILTER"></el-option>
</el-select>
</el-form-item>
<el-form-item label="拉取模式">
<el-radio-group v-model="mobileMode" @change="invalidateMobilePreview">
<el-radio label="FILL">只补手机号为空的人员</el-radio>
<el-radio label="REFRESH">重新核对(含已有号码)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button :loading="mobilePreviewLoading" @click="previewMobileSync">预览本次人数</el-button>
</el-form-item>
</el-form>
<p v-if="mobilePreview">
范围 {{mobilePreview.scopeCount}} 人,已有号码 {{mobilePreview.existingCount}} 人,
冷却跳过 {{mobilePreview.cooldownCount}} 人,本次待请求 {{mobilePreview.total}} 人(按工号去重)。
</p>
<p v-if="mobilePreview && mobilePreview.total">按每秒最多 1 次请求执行,实际耗时还取决于接口响应速度。</p>
<div v-if="mobileTask.taskId" v-loading="mobileStatusLoading && !mobileTask.status">
<el-divider>最近任务</el-divider>
<p>{{mobileStatusLabel}} · {{mobileTask.message}}</p>
<p>任务范围:{{mobileTask.scope === 'MEMBER' ? '仅会员' : (mobileTask.scope === 'FILTER' ? '筛选结果' : '全部系统人员')}}
模式:{{mobileTask.mode === 'REFRESH' ? '重新核对' : '只补空号'}}</p>
<el-progress :percentage="mobileProgress"></el-progress>
<p>已处理 {{mobileTask.processed || 0}} / {{mobileTask.total || 0}}
已更新 {{mobileTask.updated || 0}};无号码 {{mobileTask.empty || 0}}
失败 {{mobileTask.failed || 0}};跳过或未变更 {{mobileTask.skipped || 0}}。</p>
<p v-if="!mobileRunning">任务与续传记录保留七天。继续任务会先处理剩余人员,再重试原失败项。</p>
</div>
<span slot="footer">
<el-button @click="closeMobileSync">关闭</el-button>
<el-button v-if="mobileRunning" type="warning" :loading="formLoading" @click="stopMobileSync">停止任务</el-button>
<el-button v-if="!mobileRunning && mobileTask.taskId && mobileTask.processed < mobileTask.total" :loading="formLoading" @click="startMobileSync('RESUME')">继续任务</el-button>
<el-button v-if="!mobileRunning && mobileTask.failed > 0" :loading="formLoading" @click="startMobileSync('RETRY')">仅重试失败项</el-button>
<el-button type="primary" :loading="formLoading" :disabled="mobileRunning || mobileStatusLoading || mobilePreviewLoading || !mobilePreview || !mobilePreview.total" @click="startMobileSync('NEW')">开始拉取</el-button>
</span>
</el-dialog>
</div> </div>
<script> <script>
@@ -139,6 +180,17 @@ layout("/layouts/platform.html"){
mixins: [initTableMixins], mixins: [initTableMixins],
data() { data() {
return { return {
mobileDialog: false,
mobileScope: "ALL",
mobileMode: "FILL",
mobileFilters: {},
mobilePreview: null,
mobilePreviewVersion: 0,
mobilePreviewLoading: false,
mobileStatusLoading: false,
mobileTask: {},
mobilePollTimer: null,
mobileDisposed: false,
updateDialog: false, updateDialog: false,
pullTimeOptions: false, pullTimeOptions: false,
updateFromData: { updateFromData: {
@@ -176,7 +228,114 @@ layout("/layouts/platform.html"){
components: { components: {
"member-all-change-info": MEMBER_ALL_CHANGE_INFO "member-all-change-info": MEMBER_ALL_CHANGE_INFO
}, },
computed: {
mobileRunning() { return this.mobileTask.status === "RUNNING" },
mobileProgress() {
return this.mobileTask.total ? Math.min(100, Math.floor((this.mobileTask.processed || 0) * 100 / this.mobileTask.total)) : 0
},
mobileStatusLabel() {
const labels = { RUNNING: "执行中", STOPPED: "已停止", PAUSED: "已暂停", INTERRUPTED: "已中断", COMPLETED: "已完成" }
return labels[this.mobileTask.status] || "读取中"
}
},
beforeDestroy() {
this.mobileDisposed = true
this.closeMobilePolling()
},
methods: { methods: {
// 打开时固定本次列表筛选条件;只读取进度,外部手机号请求必须由开始按钮触发。
openMobileSync() {
this.mobileFilters = Object.assign({}, this.pageForm)
this.mobileDialog = true
this.invalidateMobilePreview()
this.loadMobileStatus()
},
invalidateMobilePreview() {
this.mobilePreviewVersion++
this.mobilePreview = null
},
// scope为ALL/MEMBER/FILTERmode为FILL/REFRESH;筛选参数只参与FILTER,不传分页含义。
mobileParams() {
return Object.assign({}, this.mobileFilters, { scope: this.mobileScope, mode: this.mobileMode })
},
previewMobileSync() {
if (this.mobilePreviewLoading) return
const version = ++this.mobilePreviewVersion
this.mobilePreview = null
this.mobilePreviewLoading = true
this.$axios.post("/platform/sys/data/user/update/mobilePreview", this.mobileParams())
.then((res) => {
// 选项改变后不显示旧请求的预览,避免按旧人数开始新范围任务。
if (res.code === 0 && version === this.mobilePreviewVersion) this.mobilePreview = res.data
})
.catch(() => { this.$message.error("预览失败,请稍后重试") })
.finally(() => { this.mobilePreviewLoading = false })
},
// NEW使用预览范围;RESUME/RETRY由后端按sourceTaskId恢复原任务范围及模式,不能被当前选项替换。
startMobileSync(action) {
if (this.formLoading || this.mobileRunning) return
const params = Object.assign(this.mobileParams(), { action: action, sourceTaskId: this.mobileTask.taskId || "" })
const refresh = action === "NEW" ? this.mobileMode === "REFRESH" : this.mobileTask.mode === "REFRESH"
const message = refresh ? "本次会核对并更新已有手机号,确定开始吗?" : "确定在后台拉取手机号吗?"
this.$confirm(message, "提示", { type: "warning" }).then(() => {
this.formLoading = true
return this.$axios.post("/platform/sys/data/user/update/mobileStart", params)
.then((res) => {
if (res.code === 0) {
this.mobileTask = res.data || {}
this.invalidateMobilePreview()
this.$message.success("任务已提交,可关闭弹框稍后查看")
}
})
.catch(() => { this.$message.error("启动未成功,请查看最新任务状态") })
.finally(() => {
this.formLoading = false
this.loadMobileStatus()
})
}).catch(() => {})
},
loadMobileStatus() {
if (this.mobileStatusLoading || this.mobileDisposed || !this.mobileDialog) return
this.closeMobilePolling()
this.mobileStatusLoading = true
this.$axios.post("/platform/sys/data/user/update/mobileStatus", {})
.then((res) => {
if (res.code === 0) {
const wasRunning = this.mobileRunning
this.mobileTask = res.data || {}
if (wasRunning && !this.mobileRunning) this.pageData()
}
})
.catch(() => { this.$message.error("读取任务进度失败,将稍后重试") })
.finally(() => {
this.mobileStatusLoading = false
// 使用单次计时器串行轮询;弹框关闭或页面销毁后不再注册,避免重复绑定与泄漏。
if (this.mobileDialog && !this.mobileDisposed) {
this.mobilePollTimer = setTimeout(() => { this.loadMobileStatus() }, 3000)
}
})
},
stopMobileSync() {
if (this.formLoading) return
this.formLoading = true
this.$axios.post("/platform/sys/data/user/update/mobileStop", { taskId: this.mobileTask.taskId })
.then((res) => {
if (res.code === 0) {
this.mobileTask = res.data || {}
this.$message.success("停止请求已提交,等待当前工号处理结束")
}
})
.catch(() => { this.$message.error("停止请求失败,请稍后重试") })
.finally(() => { this.formLoading = false })
},
closeMobilePolling() {
if (this.mobilePollTimer) clearTimeout(this.mobilePollTimer)
this.mobilePollTimer = null
},
closeMobileSync() {
this.$set(this, "mobileDialog", false)
this.closeMobilePolling()
},
// 更新单位数据 // 更新单位数据
updateUnits() { updateUnits() {
this.$confirm("确定要更新单位数据吗?", "提示", { this.$confirm("确定要更新单位数据吗?", "提示", {
@@ -20,14 +20,6 @@ layout("/layouts/platform.html"){
> >
备份历史会员 备份历史会员
</el-button> </el-button>
<el-button
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"
type="primary"
size="small"
icon="el-icon-mobile-phone"
:loading="syncMobileLoading"
@click="syncMobile"
>获取手机号</el-button>
<el-button type="primary" size="small" icon="el-icon-download" @click="exportMember">会员档案导出</el-button> <el-button type="primary" size="small" icon="el-icon-download" @click="exportMember">会员档案导出</el-button>
<el-popover <el-popover
placement="bottom" placement="bottom"
@@ -157,8 +149,7 @@ layout("/layouts/platform.html"){
], ],
checkedFields: [], checkedFields: [],
importDialog: false, importDialog: false
syncMobileLoading: false
} }
}, },
components: { components: {
@@ -189,39 +180,6 @@ layout("/layouts/platform.html"){
openArchive() { openArchive() {
this.$refs.archiveMemberRef.archiveDialogVisible = true this.$refs.archiveMemberRef.archiveDialogVisible = true
}, },
syncMobile() {
this.$confirm("将从手机号为空的会员中随机取500人获取手机号,确定继续吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.syncMobileLoading = true
this.$axios.post(loc() + "/syncMobile").then((res) => {
if (res.code === 0) {
const data = res.data || {}
const failedJobNos = data.failedJobNos || []
this.$alert(
"抽取人数:" + (data.sampleCount || 0) +
"<br>请求人数:" + (data.requestUserCount || 0) +
"<br>接口返回数量:" + (data.pulledCount || 0) +
"<br>有效手机号数量:" + (data.validMobileCount || 0) +
"<br>系统用户更新数量:" + (data.updatedUserCount || 0) +
"<br>失败工号数量:" + failedJobNos.length,
"手机号获取完成",
{
dangerouslyUseHTMLString: true,
confirmButtonText: "确定"
}
)
this.pageData()
} else {
this.$message.warning(res.msg || "手机号获取失败")
}
}).finally(() => {
this.syncMobileLoading = false
})
}).catch(() => {})
},
openView(userId) { openView(userId) {
this.$refs.guava.view(() => { this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(userId) this.$refs.infoRef.onOpen(userId)
@@ -2,10 +2,50 @@
layout("/layouts/platform.html"){ layout("/layouts/platform.html"){
#--> #-->
<div id="app" v-cloak> <style>
/* 扣除平台顶部导航 64px 和内容区上下间距 20px,让列表使用剩余可视高度。 */
.thirty-teach-summary-page {
height: calc(100vh - 84px);
}
.thirty-teach-summary-page > .guava-main-content {
height: 100%;
}
.thirty-teach-summary-page > .guava-main-content > .transition-item {
display: flex;
flex-direction: column;
min-height: 0;
}
.thirty-teach-summary-page .summary-search-card,
.thirty-teach-summary-page .summary-list-card > .el-card__body > .ele-table-tool {
flex-shrink: 0;
}
/* 列表卡片允许收缩,避免数据行撑高外层页面;表头和合计由 el-table 固定。 */
.thirty-teach-summary-page .summary-list-card {
flex: 1;
min-height: 0;
}
.thirty-teach-summary-page .summary-list-card > .el-card__body {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.thirty-teach-summary-page .summary-table-container {
flex: 1;
min-height: 0;
}
</style>
<div id="app" class="thirty-teach-summary-page" v-cloak>
<guava ref="guava"> <guava ref="guava">
<template> <template>
<el-card shadow="never"> <el-card shadow="never" class="summary-search-card">
<search @search="doSearch"> <search @search="doSearch">
<search-item label="所属工会"> <search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" <el-select v-model="pageForm.unionId" placeholder="请选择所属工会"
@@ -29,14 +69,15 @@ layout("/layouts/platform.html"){
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10 summary-list-card">
<table-tool label="教职工汇总"> <table-tool label="教职工汇总">
<el-button size="small" @click="doExportSummary" type="primary"> <el-button size="small" @click="doExportSummary" type="primary">
导出汇总表 导出汇总表
</el-button> </el-button>
</table-tool> </table-tool>
<div class="summary-table-container">
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id" <el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
@sort-change="pageOrder" show-summary @sort-change="pageOrder" show-summary height="100%"
v-loading="tableLoading" :size="tableSize" class="vi-table"> v-loading="tableLoading" :size="tableSize" class="vi-table">
<el-table-column align="center" header-align="center" type="index" <el-table-column align="center" header-align="center" type="index"
:index="indexMethod" label="序号" :index="indexMethod" label="序号"
@@ -52,6 +93,7 @@ layout("/layouts/platform.html"){
> >
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
</el-card> </el-card>
</template> </template>
@@ -71,10 +113,10 @@ layout("/layouts/platform.html"){
}, },
tableColumns: [ tableColumns: [
{prop: 'name', label: '分工会名称', sortable: true}, {prop: 'name', label: '分工会名称', sortable: true},
{prop: 'userNum', label: '人数', sortable: true},
{prop: 'willThirtyNum', label: '即将满30年', sortable: true}, {prop: 'willThirtyNum', label: '即将满30年', sortable: true},
{prop: 'anniversaryThirtyNum', label: '周年满30年', sortable: true}, {prop: 'anniversaryThirtyNum', label: '周年满30年', sortable: true},
{prop: 'overThirtyNum', label: '超过30年', sortable: true}, {prop: 'overThirtyNum', label: '超过30年', sortable: true},
{prop: 'userNum', label: '人数', sortable: true},
], ],
unionOptions: [], unionOptions: [],
unitOptions: [], unitOptions: [],
@@ -2,10 +2,50 @@
layout("/layouts/platform.html"){ layout("/layouts/platform.html"){
#--> #-->
<div id="app" v-cloak> <style>
/* 扣除平台顶部导航 64px 和内容区上下间距 20px,让列表使用剩余可视高度。 */
.welfare-statistics-page {
height: calc(100vh - 84px);
}
.welfare-statistics-page > .guava-main-content {
height: 100%;
}
.welfare-statistics-page > .guava-main-content > .transition-item {
display: flex;
flex-direction: column;
min-height: 0;
}
.welfare-statistics-page .statistics-search-card,
.welfare-statistics-page .statistics-list-card > .el-card__body > .ele-table-tool {
flex-shrink: 0;
}
/* 列表卡片允许收缩,避免数据行撑高外层页面;表头和合计由 el-table 固定。 */
.welfare-statistics-page .statistics-list-card {
flex: 1;
min-height: 0;
}
.welfare-statistics-page .statistics-list-card > .el-card__body {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.welfare-statistics-page .statistics-table-container {
flex: 1;
min-height: 0;
}
</style>
<div id="app" class="welfare-statistics-page" v-cloak>
<guava ref="guava"> <guava ref="guava">
<template> <template>
<el-card shadow="never"> <el-card class="statistics-search-card" shadow="never">
<search @search="doSearch"> <search @search="doSearch">
<search-item label="年度"> <search-item label="年度">
<el-date-picker <el-date-picker
@@ -31,7 +71,7 @@ layout("/layouts/platform.html"){
</search> </search>
</el-card> </el-card>
<el-card class="mt10" shadow="never" v-loading="tableLoading"> <el-card class="mt10 statistics-list-card" shadow="never" v-loading="tableLoading">
<table-tool label="工会列表"> <table-tool label="工会列表">
<el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button> <el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button>
<template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_WELFARE_ADMIN'])"> <template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_WELFARE_ADMIN'])">
@@ -40,11 +80,13 @@ layout("/layouts/platform.html"){
</el-button> </el-button>
</template> </template>
</table-tool> </table-tool>
<div class="statistics-table-container">
<el-table <el-table
:data="tableData" :data="tableData"
:size="tableSize" :size="tableSize"
@sort-change="pageOrder" @sort-change="pageOrder"
class="vi-table" class="vi-table"
height="100%"
lazy lazy
ref="table" ref="table"
row-key="id" row-key="id"
@@ -72,6 +114,7 @@ layout("/layouts/platform.html"){
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
</el-card> </el-card>
</template> </template>
</guava> </guava>