Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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("删除用户数据")
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,4 +53,20 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
|
||||
* @return 同步过程统计信息,包含工号手机号映射
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台任务逐工号读取号码:每个分页请求均设置超时,且不输出号码或接口原文。
|
||||
* 分页间等待一秒,避免单人工号多页响应绕过任务限速。
|
||||
*/
|
||||
@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。
|
||||
*/
|
||||
|
||||
-29
@@ -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<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
|
||||
@SaCheckPermission("member.info.group")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -63,7 +63,6 @@ layout("/layouts/platform.html"){
|
||||
<el-card shadow="never">
|
||||
<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="syncTeacherMobileLoading" @click="syncTeacherMobile" icon="el-icon-mobile-phone">同步教职工手机号</el-button>
|
||||
<el-button type="danger" size="mini" @click="openDelete" icon="el-icon-delete">删除本地数据源</el-button>
|
||||
</table-tool>
|
||||
<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,
|
||||
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) +
|
||||
"<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() {
|
||||
this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
|
||||
@@ -51,10 +51,11 @@ layout("/layouts/platform.html"){
|
||||
<el-card shadow="never">
|
||||
<table-tool label="系统当前用户数据">
|
||||
<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="openUpdate" icon="el-icon-refresh">更新【人员】数据</el-button>
|
||||
</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 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>
|
||||
@@ -67,7 +68,8 @@ layout("/layouts/platform.html"){
|
||||
</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="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">
|
||||
<template slot-scope="{row}">{{$moment(row.birthday).format('YYYY-MM-DD')}}</template>
|
||||
</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="personType" 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="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>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
@@ -129,6 +125,51 @@ layout("/layouts/platform.html"){
|
||||
<el-button type="primary" @click="execUpdate" :disabled="updateLoading">确 定</el-button>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
@@ -139,6 +180,17 @@ layout("/layouts/platform.html"){
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
mobileDialog: false,
|
||||
mobileScope: "ALL",
|
||||
mobileMode: "FILL",
|
||||
mobileFilters: {},
|
||||
mobilePreview: null,
|
||||
mobilePreviewVersion: 0,
|
||||
mobilePreviewLoading: false,
|
||||
mobileStatusLoading: false,
|
||||
mobileTask: {},
|
||||
mobilePollTimer: null,
|
||||
mobileDisposed: false,
|
||||
updateDialog: false,
|
||||
pullTimeOptions: false,
|
||||
updateFromData: {
|
||||
@@ -176,7 +228,114 @@ layout("/layouts/platform.html"){
|
||||
components: {
|
||||
"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: {
|
||||
// 打开时固定本次列表筛选条件;只读取进度,外部手机号请求必须由开始按钮触发。
|
||||
openMobileSync() {
|
||||
this.mobileFilters = Object.assign({}, this.pageForm)
|
||||
this.mobileDialog = true
|
||||
this.invalidateMobilePreview()
|
||||
this.loadMobileStatus()
|
||||
},
|
||||
invalidateMobilePreview() {
|
||||
this.mobilePreviewVersion++
|
||||
this.mobilePreview = null
|
||||
},
|
||||
// scope为ALL/MEMBER/FILTER,mode为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() {
|
||||
this.$confirm("确定要更新单位数据吗?", "提示", {
|
||||
|
||||
@@ -20,14 +20,6 @@ layout("/layouts/platform.html"){
|
||||
>
|
||||
备份历史会员
|
||||
</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-popover
|
||||
placement="bottom"
|
||||
@@ -157,8 +149,7 @@ layout("/layouts/platform.html"){
|
||||
],
|
||||
checkedFields: [],
|
||||
|
||||
importDialog: false,
|
||||
syncMobileLoading: false
|
||||
importDialog: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -189,39 +180,6 @@ layout("/layouts/platform.html"){
|
||||
openArchive() {
|
||||
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) {
|
||||
this.$refs.guava.view(() => {
|
||||
this.$refs.infoRef.onOpen(userId)
|
||||
|
||||
@@ -2,10 +2,50 @@
|
||||
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">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" class="summary-search-card">
|
||||
<search @search="doSearch">
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会"
|
||||
@@ -29,29 +69,31 @@ layout("/layouts/platform.html"){
|
||||
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<el-card shadow="never" class="mt10 summary-list-card">
|
||||
<table-tool label="教职工汇总">
|
||||
<el-button size="small" @click="doExportSummary" type="primary">
|
||||
导出汇总表
|
||||
</el-button>
|
||||
</table-tool>
|
||||
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
|
||||
@sort-change="pageOrder" show-summary
|
||||
v-loading="tableLoading" :size="tableSize" class="vi-table">
|
||||
<el-table-column align="center" header-align="center" type="index"
|
||||
:index="indexMethod" label="序号"
|
||||
width="80px"></el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
header-align="center"
|
||||
v-for="column in tableColumns"
|
||||
show-overflow-tooltip
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="summary-table-container">
|
||||
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
|
||||
@sort-change="pageOrder" show-summary height="100%"
|
||||
v-loading="tableLoading" :size="tableSize" class="vi-table">
|
||||
<el-table-column align="center" header-align="center" type="index"
|
||||
:index="indexMethod" label="序号"
|
||||
width="80px"></el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
header-align="center"
|
||||
v-for="column in tableColumns"
|
||||
show-overflow-tooltip
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -71,10 +113,10 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
tableColumns: [
|
||||
{prop: 'name', label: '分工会名称', sortable: true},
|
||||
{prop: 'userNum', label: '人数', sortable: true},
|
||||
{prop: 'willThirtyNum', label: '即将满30年', sortable: true},
|
||||
{prop: 'anniversaryThirtyNum', label: '周年满30年', sortable: true},
|
||||
{prop: 'overThirtyNum', label: '超过30年', sortable: true},
|
||||
{prop: 'userNum', label: '人数', sortable: true},
|
||||
],
|
||||
unionOptions: [],
|
||||
unitOptions: [],
|
||||
|
||||
@@ -2,10 +2,50 @@
|
||||
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">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-card class="statistics-search-card" shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
@@ -31,7 +71,7 @@ layout("/layouts/platform.html"){
|
||||
</search>
|
||||
</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="工会列表">
|
||||
<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'])">
|
||||
@@ -40,38 +80,41 @@ layout("/layouts/platform.html"){
|
||||
</el-button>
|
||||
</template>
|
||||
</table-tool>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
@sort-change="pageOrder"
|
||||
class="vi-table"
|
||||
lazy
|
||||
ref="table"
|
||||
row-key="id"
|
||||
show-summary
|
||||
style="width: 100%"
|
||||
v-if="tableData&&tableData.length>0"
|
||||
>
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column
|
||||
:key="column.prop"
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
:width="column.width"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
v-for="column in tableColumns"
|
||||
<div class="statistics-table-container">
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
@sort-change="pageOrder"
|
||||
class="vi-table"
|
||||
height="100%"
|
||||
lazy
|
||||
ref="table"
|
||||
row-key="id"
|
||||
show-summary
|
||||
style="width: 100%"
|
||||
v-if="tableData&&tableData.length>0"
|
||||
>
|
||||
<template scope="{row}" v-if="column.prop=='welfareUnitName'">{{row.welfareUnitName}}</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='selectedNum'">
|
||||
<el-link @click="openSelected(row)" type="primary">{{row.selectedNum}}</el-link>
|
||||
</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='unSelectedNum'">
|
||||
<el-link @click="openUnSelected(row)" type="primary">{{row.unSelectedNum}}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column
|
||||
:key="column.prop"
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
:width="column.width"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
v-for="column in tableColumns"
|
||||
>
|
||||
<template scope="{row}" v-if="column.prop=='welfareUnitName'">{{row.welfareUnitName}}</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='selectedNum'">
|
||||
<el-link @click="openSelected(row)" type="primary">{{row.selectedNum}}</el-link>
|
||||
</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='unSelectedNum'">
|
||||
<el-link @click="openUnSelected(row)" type="primary">{{row.unSelectedNum}}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</guava>
|
||||
|
||||
Reference in New Issue
Block a user