南大三十年教职工疗休养

This commit is contained in:
2026-07-08 09:01:43 +08:00
parent c87aad7ead
commit 6facd195c4
8 changed files with 352 additions and 25 deletions
@@ -45,4 +45,12 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
* @return 同步过程统计信息
*/
NutMap syncTeacherMobile();
/**
* 根据工号逐个调用数据中心手机号接口,并按工号同步到 sys_user.mobile。
*
* @param jobNos 工号集合
* @return 同步过程统计信息,包含工号手机号映射
*/
NutMap syncTeacherMobileByJobNos(List<String> jobNos);
}
@@ -411,19 +411,81 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap syncTeacherMobile() {
Sql sql = Sqls.create("SELECT loginname FROM sys_user WHERE loginname IS NOT NULL AND loginname <> ''");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
List<String> jobNos = sql.getList(String.class);
return syncTeacherMobileByJobNos(jobNos);
}
/**
* 根据传入工号逐个调用手机号接口,接口分页参数保持原有 pageNum/pageSize,仅替换 gh 查询条件。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap syncTeacherMobileByJobNos(List<String> jobNos) {
List<String> distinctJobNos = jobNos == null ? Collections.emptyList() : jobNos.stream()
.map(StrUtil::trimToEmpty)
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (distinctJobNos.isEmpty()) {
return NutMap.NEW()
.addv("total", 0)
.addv("pages", 0)
.addv("requestUserCount", 0)
.addv("pulledCount", 0)
.addv("validMobileCount", 0)
.addv("matchedUserCount", 0)
.addv("updatedUserCount", 0)
.addv("mobileMap", Collections.emptyMap())
.addv("failedJobNos", Collections.emptyList());
}
DataCenterProperties.Credential credential = dataCenterProperties.credential(TEACHER_MOBILE_CONFIG_KEY);
String url = credential.getUrl();
String token = credential.getToken();
log.info("教职工手机号同步开始:url={}, tokenReady={}", url, StrUtil.isNotBlank(token));
log.info("教职工手机号同步开始:url={}, tokenReady={}, requestUserCount={}", url, StrUtil.isNotBlank(token), distinctJobNos.size());
List<JSONObject> rawDataList = new ArrayList<>();
int pageNum = 1;
int pageSize = TEACHER_MOBILE_PAGE_SIZE;
int total = 0;
List<String> failedJobNos = new ArrayList<>();
for (String jobNo : distinctJobNos) {
try {
rawDataList.addAll(requestTeacherMobileByJobNo(url, token, jobNo, pageSize));
} catch (Exception e) {
failedJobNos.add(jobNo);
log.warn("教职工手机号单人工号拉取失败:jobNo={}, msg={}", jobNo, e.getMessage());
}
}
Map<String, String> mobileMap = collectTeacherMobileMap(rawDataList);
NutMap updateResult = updateTeacherMobile(mobileMap);
NutMap result = NutMap.NEW()
.addv("total", distinctJobNos.size())
.addv("pages", distinctJobNos.size())
.addv("requestUserCount", distinctJobNos.size())
.addv("pulledCount", rawDataList.size())
.addv("validMobileCount", mobileMap.size())
.addv("matchedUserCount", updateResult.getInt("matchedUserCount", 0))
.addv("updatedUserCount", updateResult.getInt("updatedUserCount", 0))
.addv("mobileMap", mobileMap)
.addv("failedJobNos", failedJobNos);
log.info("教职工手机号同步完成:{}", result);
return result;
}
/**
* 按单个工号请求手机号接口,保留 pageNum/pageSize 分页参数,避免调用第三方全量分页。
*/
private List<JSONObject> requestTeacherMobileByJobNo(String url, String token, String jobNo, int pageSize) {
List<JSONObject> rawDataList = new ArrayList<>();
int pageNum = 1;
int pages = 1;
int total = 0;
do {
Map<String, Object> reqBody = buildTeacherMobileRequestBody(pageNum, pageSize);
log.info("教职工手机号接口请求:pageNum={}, pageSize={}, body={}", pageNum, pageSize, reqBody);
Map<String, Object> reqBody = buildTeacherMobileRequestBody(jobNo, pageNum, pageSize);
log.info("教职工手机号接口请求:jobNo={}, pageNum={}, pageSize={}, body={}", jobNo, pageNum, pageSize, reqBody);
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
@@ -431,7 +493,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
httpRequest.body(JSONUtil.toJsonStr(reqBody));
String resBody = httpRequest.execute().body();
log.info("教职工手机号接口返回:pageNum={}, response={}", pageNum, resBody);
log.info("教职工手机号接口返回:jobNo={}, pageNum={}, response={}", jobNo, pageNum, resBody);
JSONObject resp = JSONUtil.parseObj(resBody);
if (!"0".equals(resp.getStr("code"))) {
throw new BaseException("获取教职工手机号失败,错误码: " + resp.getStr("code") + ";错误原因:" + resp.getStr("msg"));
@@ -450,31 +512,20 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
if (CollUtil.isNotEmpty(records)) {
rawDataList.addAll(records.stream().map(v -> (JSONObject) v).toList());
}
log.info("教职工手机号拉取进度:pageNum={}, pages={}, total={}, pulled={}",
pageNum, pages, total, rawDataList.size());
log.info("教职工手机号单人工号拉取进度:jobNo={}, pageNum={}, pages={}, total={}, pulled={}",
jobNo, pageNum, pages, total, rawDataList.size());
pageNum++;
} while (pageNum <= Math.max(pages, 1));
Map<String, String> mobileMap = collectTeacherMobileMap(rawDataList);
NutMap updateResult = updateTeacherMobile(mobileMap);
NutMap result = NutMap.NEW()
.addv("total", total)
.addv("pages", pages)
.addv("pulledCount", rawDataList.size())
.addv("validMobileCount", mobileMap.size())
.addv("matchedUserCount", updateResult.getInt("matchedUserCount", 0))
.addv("updatedUserCount", updateResult.getInt("updatedUserCount", 0));
log.info("教职工手机号同步完成:{}", result);
return result;
return rawDataList;
}
/**
* 组装手机号接口分页请求体,uid、gh、sjh 为空时表示全量分页拉取
* 组装手机号接口分页请求体,uid 和 sjh 保持空值,gh 使用当前待查询工号
*/
private Map<String, Object> buildTeacherMobileRequestBody(int pageNum, int pageSize) {
private Map<String, Object> buildTeacherMobileRequestBody(String jobNo, int pageNum, int pageSize) {
Map<String, Object> reqBody = new LinkedHashMap<>();
reqBody.put("uid", null);
reqBody.put("gh", null);
reqBody.put("gh", jobNo);
reqBody.put("sjh", null);
reqBody.put("pageNum", pageNum);
reqBody.put("pageSize", pageSize);
@@ -19,6 +19,7 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.sys.views.View_user;
@@ -131,6 +132,9 @@ public class ThirtyTeachTourLedgerController {
@Inject
private SysFileService sysFileService;
@Inject
private SysDataUserPullService sysUserPullService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/ThirtyTeachTour/ledger/index.html")
@SaCheckPermission("thirtyTeachTour.ledger")
@@ -297,6 +301,111 @@ public class ThirtyTeachTourLedgerController {
return Result.success();
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.ledger")
@SLog(type = "tour", tag = "疗休养台账", msg = "获取台账人员手机号")
public Result syncMobile(@Param("ids") String ids, Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
try {
List<String> idList = StrUtil.isBlank(ids) ? Collections.emptyList() : Json.fromJsonAsList(String.class, ids);
List<NutMap> ledgers = queryLedgerMobileTargets(idList, startYear, endYear, keyword, unionId, lineId, travelPeriod,
lineType, directFamilyOnly, overCostOnly);
List<String> jobNos = ledgers.stream()
.map(row -> row.getString("jobNo", ""))
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (jobNos.isEmpty()) {
return Result.error("当前台账范围内没有可获取手机号的人员");
}
NutMap syncResult = sysUserPullService.syncTeacherMobileByJobNos(jobNos);
int ledgerUpdatedCount = updateLedgerMobileBySyncResult(ledgers, syncResult);
syncResult.put("ledgerCount", ledgers.size());
syncResult.put("ledgerUpdatedCount", ledgerUpdatedCount);
return Result.success(syncResult);
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 查询本次需要获取手机号的台账人员;勾选时按台账ID取数,未勾选时复用页面当前筛选条件取数。
*/
private List<NutMap> queryLedgerMobileTargets(List<String> idList, Integer startYear, Integer endYear, String keyword, String unionId,
String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly,
Boolean overCostOnly) {
Cnd cnd;
if (idList != null && !idList.isEmpty()) {
cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.and("t.id", "in", idList);
cnd.andEX("t.unionId", "=", resolveLedgerUnionId(null));
appendApprovedWorkflowFilter(cnd);
} else {
cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
}
Sql sql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.mobile
FROM thirty_teach_tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN thirty_teach_tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN thirty_teach_tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN thirty_teach_tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM thirty_teach_tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return sql.getList(NutMap.class);
}
/**
* 将手机号同步结果回写到当前台账范围内,保证台账列表刷新后展示最新手机号。
*/
private int updateLedgerMobileBySyncResult(List<NutMap> ledgers, NutMap syncResult) {
if (ledgers == null || ledgers.isEmpty() || syncResult == null || !(syncResult.get("mobileMap") instanceof Map)) {
return 0;
}
Map<?, ?> mobileMap = (Map<?, ?>) syncResult.get("mobileMap");
int updatedCount = 0;
for (NutMap ledger : ledgers) {
String jobNo = ledger.getString("jobNo", "");
Object mobileValue = mobileMap.get(jobNo);
String newMobile = mobileValue == null ? "" : String.valueOf(mobileValue);
if (StrUtil.isBlank(newMobile) || StrUtil.equals(newMobile, ledger.getString("mobile", ""))) {
continue;
}
updatedCount += tourLedgerService.dao().update(ThirtyTeachTourLedger.class,
Chain.make("mobile", newMobile), Cnd.where("id", "=", ledger.getString("id", "")));
}
return updatedCount;
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.ledger")
@@ -5,8 +5,11 @@ 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;
@@ -30,6 +33,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
/**
* @version 1.0
@@ -47,6 +51,9 @@ 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")
@@ -79,6 +86,28 @@ 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")
@@ -68,4 +68,12 @@ public interface MemberInfoService extends BaseService<Sys_user> {
* @param isFlag 是否清空更新
*/
NutMap handlingMemberImport(TempFile file, Boolean isFlag);
/**
* 随机查询当前权限范围内手机号为空的会员工号。
*
* @param limit 查询人数上限
* @return 会员工号集合
*/
List<String> queryRandomBlankMobileMemberJobNos(int limit);
}
@@ -113,6 +113,34 @@ public class MemberInfoServiceImpl extends BaseServiceImpl<Sys_user> implements
return sql;
}
/**
* 随机查询当前权限范围内手机号为空的会员工号,供批量补手机号功能使用。
*/
@Override
public List<String> queryRandomBlankMobileMemberJobNos(int limit) {
Cnd cnd = Cnd.NEW();
cnd.and("u.member", "=", true);
cnd.and("u.loginname", "<>", "");
cnd.and(new SqlExpressionGroup()
.or("u.mobile", "IS", null)
.or("u.mobile", "=", ""));
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("u.unionId", "=", SecurityUtil.getUnionId());
}
Sql sql = Sqls.create("""
SELECT DISTINCT u.loginname
FROM vw_user u
$condition
ORDER BY RAND()
LIMIT @limit
""");
sql.setCondition(cnd);
sql.setParam("limit", limit);
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
return sql.getList(String.class);
}
@Override
public Sql getMemberHistorySql(MemberInfoPageForm pageForm) {
@@ -71,6 +71,7 @@ layout("/layouts/platform.html"){
<div class="tour-ledger-actions">
<el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button>
<el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button>
<el-button size="medium" type="primary" icon="el-icon-mobile-phone" :loading="syncMobileLoading" @click="syncMobile">获取手机号</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportLedgerData">导出台账数据</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button>
</div>
@@ -366,6 +367,7 @@ layout("/layouts/platform.html"){
detailRow: {},
doneTasks: [],
showImportDialog: false,
syncMobileLoading: false,
multipleSelection: [],
filterOptionsTimer: null,
currentUnionId: "",
@@ -440,6 +442,56 @@ layout("/layouts/platform.html"){
})
})
},
syncMobile() {
const selectedCount = this.multipleSelection.length
const confirmText = selectedCount > 0
? "确定获取已勾选的" + selectedCount + "名台账人员手机号吗?"
: "未勾选人员,将获取当前查询条件下全部台账人员手机号,确定继续吗?"
this.$confirm(confirmText, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const params = {
ids: selectedCount > 0 ? JSON.stringify(this.multipleSelection.map(item => item.id)) : "",
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType,
directFamilyOnly: this.pageForm.directFamilyOnly,
overCostOnly: this.pageForm.overCostOnly
}
this.syncMobileLoading = true
this.$axios.post(loc() + "/syncMobile", params).then((res) => {
if (res.code === 0) {
const data = res.data || {}
const failedJobNos = data.failedJobNos || []
this.$alert(
"请求人数:" + (data.requestUserCount || 0) +
"<br>接口返回数量:" + (data.pulledCount || 0) +
"<br>有效手机号数量:" + (data.validMobileCount || 0) +
"<br>系统用户更新数量:" + (data.updatedUserCount || 0) +
"<br>台账更新数量:" + (data.ledgerUpdatedCount || 0) +
"<br>失败工号数量:" + failedJobNos.length,
"手机号获取完成",
{
dangerouslyUseHTMLString: true,
confirmButtonText: "确定"
}
)
this.clearTableSelection()
this.pageData()
} else {
this.$message.warning(res.msg || "手机号获取失败")
}
}).finally(() => {
this.syncMobileLoading = false
})
}).catch(() => {})
},
exportUnionSignupZip() {
const params = new URLSearchParams()
;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => {
@@ -20,6 +20,14 @@ 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"
@@ -148,8 +156,9 @@ layout("/layouts/platform.html"){
{ prop: "expectedLeaveSchoolDate", label: "预计/最后离校时间", width: 120, sortable: true, checked: 0 },
],
checkedFields: [],
importDialog: false
importDialog: false,
syncMobileLoading: false
}
},
components: {
@@ -180,6 +189,39 @@ 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)