Compare commits

...
12 Commits
Author SHA1 Message Date
c-zhangr1 c18579fe40 教代会意见模块->承办答复 页面初始化查询数据有误 2026-09-03 17:53:59 +08:00
c-zhangr1 3a192a932f change 2026-09-02 16:52:30 +08:00
c-zhangr1 6af2967fd5 change 2026-07-17 16:40:35 +08:00
c-zhangr1 6facd195c4 南大三十年教职工疗休养 2026-07-08 09:01:43 +08:00
c-zhangr1 c87aad7ead 南大三十年教职工疗休养 2026-07-07 09:05:14 +08:00
c-zhangr1 9fc06ba44a Merge remote-tracking branch 'origin/main' 2026-07-06 14:34:43 +08:00
c-zhangr1 c903e0d934 南大三十年教职工疗休养 2026-07-06 14:34:20 +08:00
c-zhouhf1 d6f668e6b7 commit 2026-07-06 11:18:00 +08:00
c-zhouhf1 f437c0444d Merge remote-tracking branch 'origin/main' 2026-07-04 01:22:46 +08:00
c-zhouhf1 91556ee146 commit 2026-07-04 01:22:36 +08:00
c-zhangr1 f0bec867b8 Merge remote-tracking branch 'origin/main' 2026-07-04 01:18:28 +08:00
c-zhangr1 67c21e5dfa 南大三十年教职工疗休养 2026-07-04 01:08:45 +08:00
37 changed files with 1469 additions and 223 deletions
@@ -69,6 +69,17 @@ 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("删除用户数据")
@@ -391,4 +391,12 @@ public class Sys_user extends BaseModel implements Serializable {
@Comment("openId")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String openId;
@Column
@Comment("编制信息")
@ColDefine(type = ColType.VARCHAR, width = 64)
@DataCenterColumn(name = "编制信息", key = "bzxx")
private String compilationInformation;
}
@@ -38,4 +38,19 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
* @return
*/
Map<String, NutMap> pullFinance();
/**
* 从数据中心分页拉取教职工手机号,并按工号同步到 sys_user.mobile。
*
* @return 同步过程统计信息
*/
NutMap syncTeacherMobile();
/**
* 根据工号逐个调用数据中心手机号接口,并按工号同步到 sys_user.mobile。
*
* @param jobNos 工号集合
* @return 同步过程统计信息,包含工号手机号映射
*/
NutMap syncTeacherMobileByJobNos(List<String> jobNos);
}
@@ -26,8 +26,10 @@ import com.budwk.app.sys.services.SysDataUnitPullService;
import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysDictService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.nutz.aop.interceptor.async.Async;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -47,6 +49,10 @@ import java.util.stream.Collectors;
@Slf4j
public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService {
private static final String TEACHER_MOBILE_CONFIG_KEY = "teacherMobile";
private static final int TEACHER_MOBILE_PAGE_SIZE = 10;
private static final int TEACHER_MOBILE_DB_BATCH_SIZE = 500;
@Inject
private SysDictService sysDictService;
@Inject
@@ -223,6 +229,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 插入到数据库
dao().insert(latestSourceList);
updateDictCompilationInformation(latestSourceList);
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).distinct().toList();
Sql sql = Sqls.create("select id from sys_unit group by id");
@@ -240,6 +248,36 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
}
}
/**
* 更新编制信息
* @param userSources
*/
private void updateDictCompilationInformation(List<Sys_user_source> userSources){
if(CollUtil.isNotEmpty(userSources)){
List<String> compilationInformationList = userSources.stream()
.map(Sys_user_source::getCompilationInformation)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
//系统在职状态字典
List<Sys_dict> sysUserStates = sysDictService.getSubListByCode("USER_COMPILATION_INFORMATION");
//系统中不存在的就插入
List<Sys_dict> insertUserStates = compilationInformationList.stream()
.filter(compilationInformation -> sysUserStates.stream().noneMatch(dict -> dict.getCode().equals(compilationInformation)))
.map(compilationInformation -> {
Sys_dict dict = new Sys_dict();
dict.setCode(compilationInformation);
dict.setName(compilationInformation);
return dict;
}).toList();
if(CollUtil.isNotEmpty(insertUserStates)){
for (Sys_dict dict : insertUserStates) {
sysDictService.saveByParentCode(dict, "USER_COMPILATION_INFORMATION");
}
}
}
}
@Async
private void updateDict(List<Sys_user_source> userSources) {
//判断是否要更新在职状态字典
@@ -400,6 +438,186 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
return listMap(sql);
}
/**
* 从数据中心分页同步教职工手机号,并按工号更新 sys_user.mobile。
*/
@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={}, requestUserCount={}", url, StrUtil.isNotBlank(token), distinctJobNos.size());
List<JSONObject> rawDataList = new ArrayList<>();
int pageSize = TEACHER_MOBILE_PAGE_SIZE;
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(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");
httpRequest.header("X-H3C-TOKEN", token);
httpRequest.body(JSONUtil.toJsonStr(reqBody));
String resBody = httpRequest.execute().body();
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"));
}
JSONObject data = resp.getJSONObject("data");
if (data != null) {
total = data.getInt("total", total);
pages = data.getInt("pages", pages);
if (pages <= 0 && total > 0) {
pages = (int) Math.ceil((double) total / pageSize);
}
}
JSONArray records = readTeacherMobileRecords(resp, data);
if (CollUtil.isNotEmpty(records)) {
rawDataList.addAll(records.stream().map(v -> (JSONObject) v).toList());
}
log.info("教职工手机号单人工号拉取进度:jobNo={}, pageNum={}, pages={}, total={}, pulled={}",
jobNo, pageNum, pages, total, rawDataList.size());
pageNum++;
} while (pageNum <= Math.max(pages, 1));
return rawDataList;
}
/**
* 组装手机号接口分页请求体,uid 和 sjh 保持空值,gh 使用当前待查询工号。
*/
private Map<String, Object> buildTeacherMobileRequestBody(String jobNo, int pageNum, int pageSize) {
Map<String, Object> reqBody = new LinkedHashMap<>();
reqBody.put("uid", null);
reqBody.put("gh", jobNo);
reqBody.put("sjh", null);
reqBody.put("pageNum", pageNum);
reqBody.put("pageSize", pageSize);
return reqBody;
}
/**
* 读取手机号接口明细数据,优先按文档读取顶层 records,同时兼容 data.records。
*/
private JSONArray readTeacherMobileRecords(JSONObject resp, JSONObject data) {
JSONArray records = resp.getJSONArray("records");
if (records == null && data != null) {
records = data.getJSONArray("records");
}
return records == null ? new JSONArray() : records;
}
/**
* 将接口记录转换为工号到手机号的映射,过滤缺少工号或手机号的无效数据。
*/
private Map<String, String> collectTeacherMobileMap(List<JSONObject> rawDataList) {
Map<String, String> mobileMap = new LinkedHashMap<>();
for (JSONObject rawData : rawDataList) {
String loginname = StrUtil.trimToEmpty(rawData.getStr("gh"));
String mobile = StrUtil.trimToEmpty(rawData.getStr("sjh"));
if (StrUtil.isAllNotBlank(loginname, mobile)) {
mobileMap.put(loginname, mobile);
}
}
log.info("教职工手机号有效数据整理完成:rawCount={}, validCount={}", rawDataList.size(), mobileMap.size());
return mobileMap;
}
/**
* 分批匹配 sys_user.loginname 并更新手机号,避免一次性 IN 条件过长。
*/
private NutMap updateTeacherMobile(Map<String, String> mobileMap) {
if (mobileMap == null || mobileMap.isEmpty()) {
return NutMap.NEW().addv("matchedUserCount", 0).addv("updatedUserCount", 0);
}
int matchedUserCount = 0;
int updatedUserCount = 0;
List<String> loginNames = new ArrayList<>(mobileMap.keySet());
for (int start = 0; start < loginNames.size(); start += TEACHER_MOBILE_DB_BATCH_SIZE) {
int end = Math.min(start + TEACHER_MOBILE_DB_BATCH_SIZE, loginNames.size());
List<String> batchLoginNames = loginNames.subList(start, end);
List<Sys_user> users = dao().query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", batchLoginNames));
matchedUserCount += users.size();
for (Sys_user user : users) {
String newMobile = mobileMap.get(user.getLoginname());
if (StrUtil.isNotBlank(newMobile) && !Objects.equals(newMobile, user.getMobile())) {
updatedUserCount += dao().update(Sys_user.class, Chain.make("mobile", newMobile), Cnd.where(Sys_user::getId, "=", user.getId()));
}
}
log.info("教职工手机号批次更新完成:batchStart={}, batchEnd={}, batchMatched={}, updatedTotal={}",
start, end, users.size(), updatedUserCount);
}
return NutMap.NEW().addv("matchedUserCount", matchedUserCount).addv("updatedUserCount", updatedUserCount);
}
@Override
public Map<String, NutMap> pullFinance() {
@@ -25,6 +25,7 @@ import com.budwk.app.base.exception.BaseException;
import io.minio.*;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.nutz.boot.AppContext;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.mvc.Mvcs;
@@ -49,7 +50,7 @@ public class SysFileMinIoUtil {
* 初始化操作的客户端
*/
private static void initClient() {
Ioc ioc = Mvcs.getIoc();
Ioc ioc = AppContext.getDefault().getIoc();
PropertiesProxy propertiesProxy = ioc.get(PropertiesProxy.class, "conf");
String accessKey = propertiesProxy.get("minio.accessKey");
@@ -96,6 +96,9 @@ public class View_user {
@Column
private String preparedBy;
@Column
private String compilationInformation;
@Column
private String identityType;
@@ -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;
@@ -91,8 +92,16 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -103,6 +112,11 @@ import java.util.zip.ZipOutputStream;
@Slf4j
public class ThirtyTeachTourLedgerController {
private static final int LEDGER_EXPORT_IMAGE_THREAD_COUNT = 3;
private static final int LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE = 500;
private static final int LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS = 300;
private static final String LEDGER_EXPORT_ROW_IMAGES_KEY = "__ledgerExportImages";
@Inject
private ThirtyTeachTourLedgerService tourLedgerService;
@@ -118,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")
@@ -284,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")
@@ -379,6 +501,7 @@ public class ThirtyTeachTourLedgerController {
entities.add(new ExcelExportEntity("年龄", "age", 10));
entities.add(new ExcelExportEntity("身份证号", "idCard", 24));
entities.add(new ExcelExportEntity("手机号", "mobile", 18));
entities.add(new ExcelExportEntity("所属工会", "unionName", 18));
entities.add(new ExcelExportEntity("所在单位", "unitName", 26));
entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18));
entities.add(new ExcelExportEntity("报名线路", "lineName", 30));
@@ -405,12 +528,13 @@ public class ThirtyTeachTourLedgerController {
ExcelExportEntity imageEntity = new ExcelExportEntity("当前时间段图片" + i, "photoCell" + (maxPhotoCount + i), 18);
entities.add(imageEntity);
}
Map<String, LedgerExportImage> imageMap = batchResolveLedgerImages(rows);
rows.forEach(row -> {
Integer familyCount = row.getInt("familyCount");
row.put("familyCountText", (familyCount == null ? 0 : familyCount) + "");
row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "" : "");
row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "" : "");
fillLedgerPhotoRowImages(row, maxPhotoCount, maxCurrentPeriodPhotoCount);
fillLedgerPhotoRowImages(row, maxPhotoCount, maxCurrentPeriodPhotoCount, imageMap);
});
List<List<LedgerExportImage>> rowImages = snapshotLedgerRowImages(rows);
@@ -439,22 +563,23 @@ public class ThirtyTeachTourLedgerController {
/**
* 将每条台账的图片材料解析为可写入 Excel 的图片对象,图片列文本保持为空,后续由 POI 手动插入图片。
*/
private void fillLedgerPhotoRowImages(NutMap row, int maxPhotoCount, int maxCurrentPeriodPhotoCount) {
private void fillLedgerPhotoRowImages(NutMap row, int maxPhotoCount, int maxCurrentPeriodPhotoCount,
Map<String, LedgerExportImage> imageMap) {
List<String> photoFiles = splitPhotoFiles(row.getString("photoFiles", ""));
List<String> currentPeriodPhotoFiles = splitPhotoFiles(row.getString("currentPeriodPhotoFiles", ""));
if (!photoFiles.isEmpty()) {
log.info("三十年教龄疗休养台账30年前图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
log.debug("三十年教龄疗休养台账30年前图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
row.getString("jobNo", ""), row.getString("userName", ""), photoFiles.size(), row.getString("photoFiles", ""));
}
if (!currentPeriodPhotoFiles.isEmpty()) {
log.info("三十年教龄疗休养台账当前时间段图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
log.debug("三十年教龄疗休养台账当前时间段图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
row.getString("jobNo", ""), row.getString("userName", ""), currentPeriodPhotoFiles.size(), row.getString("currentPeriodPhotoFiles", ""));
}
// 图片对象按导出列顺序组装:先 30 年前图片,再当前时间段图片,确保同一教职工图片仍在同一行。
List<LedgerExportImage> photoImages = new ArrayList<>();
photoImages.addAll(resolveLedgerImages(row.getString("photoFiles", ""), 0));
photoImages.addAll(resolveLedgerImages(row.getString("currentPeriodPhotoFiles", ""), maxPhotoCount));
row.put("photoImages", photoImages);
photoImages.addAll(resolveLedgerImages(row.getString("photoFiles", ""), 0, imageMap));
photoImages.addAll(resolveLedgerImages(row.getString("currentPeriodPhotoFiles", ""), maxPhotoCount, imageMap));
row.put(LEDGER_EXPORT_ROW_IMAGES_KEY, photoImages);
for (int i = 1; i <= maxPhotoCount + maxCurrentPeriodPhotoCount; i++) {
row.put("photoCell" + i, "");
}
@@ -490,7 +615,7 @@ public class ThirtyTeachTourLedgerController {
}
for (NutMap row : rows) {
List<LedgerExportImage> images = new ArrayList<>();
Object photoImageData = row.get("photoImages");
Object photoImageData = row.get(LEDGER_EXPORT_ROW_IMAGES_KEY);
if (photoImageData instanceof List) {
for (Object image : (List<?>) photoImageData) {
if (image instanceof LedgerExportImage) {
@@ -532,7 +657,7 @@ public class ThirtyTeachTourLedgerController {
anchor.setRow2(excelRowIndex + 1);
drawing.createPicture(anchor, pictureIndex);
insertedPictureCount++;
log.info("三十年教龄疗休养台账图片写入Excel成功:rowIndex={}, columnOffset={}, fileId={}, bytes={}, pictureType={}",
log.debug("三十年教龄疗休养台账图片写入Excel成功:rowIndex={}, columnOffset={}, fileId={}, bytes={}, pictureType={}",
excelRowIndex, image.columnOffset, image.fileId, image.bytes.length, image.pictureType);
}
}
@@ -540,21 +665,193 @@ public class ThirtyTeachTourLedgerController {
}
/**
* 将台账图片字段解析成可插入 Excel 的图片对象,当前优先支持系统文件下载路径
* 先收集本次导出涉及的全部图片,再批量查询 sys_file 并并行读取图片字节,避免逐行逐图查库
* 图片字节处理仅生成不可变结果,Excel Workbook 仍在后续单线程写入,避免 POI 对象并发访问。
*/
private List<LedgerExportImage> resolveLedgerImages(String photoFiles) {
return resolveLedgerImages(photoFiles, 0);
private Map<String, LedgerExportImage> batchResolveLedgerImages(List<NutMap> rows) {
Map<String, Set<String>> pathCandidateMap = collectLedgerPhotoPathCandidates(rows);
List<String> normalizedPaths = new ArrayList<>(pathCandidateMap.keySet());
if (normalizedPaths.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Sys_file> sysFileMap = batchFetchSysFiles(pathCandidateMap);
Map<String, LedgerExportImage> imageMap = new LinkedHashMap<>();
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(LEDGER_EXPORT_IMAGE_THREAD_COUNT, normalizedPaths.size()));
List<Future<LedgerExportImageResolveResult>> futures = new ArrayList<>();
for (String normalizedPath : normalizedPaths) {
Sys_file sysFile = sysFileMap.get(normalizedPath);
if (sysFile == null) {
sysFile = sysFileMap.get(extractIdParam(normalizedPath));
}
Sys_file resolvedSysFile = sysFile;
futures.add(executorService.submit(() -> new LedgerExportImageResolveResult(normalizedPath,
resolveLedgerImage(normalizedPath, resolvedSysFile))));
}
executorService.shutdown();
for (Future<LedgerExportImageResolveResult> future : futures) {
try {
LedgerExportImageResolveResult result = future.get(LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (result != null && result.image != null) {
imageMap.put(result.normalizedPath, result.image);
if (StrUtil.isNotBlank(result.image.fileId)) {
imageMap.putIfAbsent(result.image.fileId, result.image);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("三十年教龄疗休养台账导出图片并行读取被中断:msg={}", e.getMessage());
break;
} catch (ExecutionException e) {
log.warn("三十年教龄疗休养台账导出图片并行读取失败:msg={}", e.getMessage());
} catch (TimeoutException e) {
future.cancel(true);
log.warn("三十年教龄疗休养台账导出图片并行读取超时:timeoutSeconds={}", LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS);
}
}
try {
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
executorService.shutdownNow();
}
log.info("三十年教龄疗休养台账导出图片批量解析完成:pathCount={}, sysFileCount={}, imageCount={}",
normalizedPaths.size(), sysFileMap.size(), imageMap.size());
return imageMap;
}
/**
* 台账图片字段解析成可插入 Excel 的图片对象,并记录图片列偏移,支持多组图片列分区写入
* 台账图片字段原始顺序收集并去重,同时保留原始路径候选值用于批量匹配历史 sys_file.downloadPath
*/
private List<LedgerExportImage> resolveLedgerImages(String photoFiles, int columnOffsetStart) {
private Map<String, Set<String>> collectLedgerPhotoPathCandidates(List<NutMap> rows) {
if (rows == null || rows.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Set<String>> pathCandidateMap = new LinkedHashMap<>();
for (NutMap row : rows) {
splitPhotoFiles(row.getString("photoFiles", "")).forEach(path -> addPhotoPathCandidates(pathCandidateMap, path));
splitPhotoFiles(row.getString("currentPeriodPhotoFiles", "")).forEach(path -> addPhotoPathCandidates(pathCandidateMap, path));
}
return pathCandidateMap;
}
/**
* 同一图片用规范化路径作为回填 key,并保留原始值、解码值和规范化值,避免批量查询漏掉历史保存格式。
*/
private void addPhotoPathCandidates(Map<String, Set<String>> pathCandidateMap, String photoPath) {
String originalPath = StrUtil.trimToEmpty(photoPath);
String normalizedPath = normalizePhotoPath(photoPath);
if (StrUtil.isNotBlank(normalizedPath)) {
Set<String> candidates = pathCandidateMap.computeIfAbsent(normalizedPath, key -> new LinkedHashSet<>());
addPhotoPathCandidate(candidates, originalPath);
addPhotoPathCandidate(candidates, decodePhotoPath(originalPath));
addPhotoPathCandidate(candidates, normalizedPath);
if (StrUtil.isNotBlank(Globals.AppDomain) && normalizedPath.startsWith("/")) {
addPhotoPathCandidate(candidates, Globals.AppDomain + normalizedPath);
}
}
}
/**
* 图片路径候选值统一入集合,过滤空值,避免 IN 条件中出现无效参数。
*/
private void addPhotoPathCandidate(Set<String> candidates, String candidate) {
if (StrUtil.isNotBlank(candidate)) {
candidates.add(candidate);
}
}
/**
* 批量查询本次导出涉及的 sys_file 记录,同时兼容按文件 ID、原始 downloadPath 和规范化 downloadPath 保存的历史图片路径。
*/
private Map<String, Sys_file> batchFetchSysFiles(Map<String, Set<String>> pathCandidateMap) {
if (pathCandidateMap == null || pathCandidateMap.isEmpty()) {
return Collections.emptyMap();
}
Set<String> fileIds = new LinkedHashSet<>();
Set<String> downloadPaths = new LinkedHashSet<>();
for (Map.Entry<String, Set<String>> entry : pathCandidateMap.entrySet()) {
String normalizedPath = entry.getKey();
String fileId = extractIdParam(normalizedPath);
if (StrUtil.isNotBlank(fileId)) {
fileIds.add(fileId);
}
for (String candidate : entry.getValue()) {
String candidateFileId = extractIdParam(candidate);
if (StrUtil.isNotBlank(candidateFileId)) {
fileIds.add(candidateFileId);
}
downloadPaths.add(candidate);
}
}
Map<String, Sys_file> result = new LinkedHashMap<>();
querySysFilesByIds(fileIds).forEach(file -> putSysFileIndexes(result, file));
querySysFilesByDownloadPaths(downloadPaths).forEach(file -> putSysFileIndexes(result, file));
return result;
}
/**
* sys_file 按文件 ID 批量查询,使用模型字段引用兼容 @Name 主键字段映射。
*/
private List<Sys_file> querySysFilesByIds(Set<String> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
List<String> valueList = new ArrayList<>(values);
List<Sys_file> files = new ArrayList<>();
for (int start = 0; start < valueList.size(); start += LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE) {
int end = Math.min(start + LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE, valueList.size());
files.addAll(sysFileService.query(Cnd.where(Sys_file::getId, "in", valueList.subList(start, end))));
}
return files;
}
/**
* sys_file 按下载路径批量查询,按固定大小分片避免超长 IN 条件影响数据库解析。
*/
private List<Sys_file> querySysFilesByDownloadPaths(Set<String> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
List<String> valueList = new ArrayList<>(values);
List<Sys_file> files = new ArrayList<>();
for (int start = 0; start < valueList.size(); start += LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE) {
int end = Math.min(start + LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE, valueList.size());
files.addAll(sysFileService.query(Cnd.where(Sys_file::getDownloadPath, "in", valueList.subList(start, end))));
}
return files;
}
/**
* 同一文件同时建立 id 和 downloadPath 索引,行内回填时统一用规范化路径查找。
*/
private void putSysFileIndexes(Map<String, Sys_file> result, Sys_file file) {
if (file == null) {
return;
}
if (StrUtil.isNotBlank(file.getId())) {
result.putIfAbsent(file.getId(), file);
}
if (StrUtil.isNotBlank(file.getDownloadPath())) {
result.putIfAbsent(normalizePhotoPath(file.getDownloadPath()), file);
}
}
/**
* 将台账图片字段解析成可插入 Excel 的图片对象,并记录图片列偏移,图片字节来自批量预解析结果。
*/
private List<LedgerExportImage> resolveLedgerImages(String photoFiles, int columnOffsetStart,
Map<String, LedgerExportImage> imageMap) {
List<LedgerExportImage> images = new ArrayList<>();
List<String> photoPaths = splitPhotoFiles(photoFiles);
for (int photoIndex = 0; photoIndex < photoPaths.size(); photoIndex++) {
String originalPath = photoPaths.get(photoIndex);
LedgerExportImage image = resolveLedgerImage(originalPath);
String normalizedPath = normalizePhotoPath(originalPath);
LedgerExportImage image = imageMap == null ? null : imageMap.get(normalizedPath);
if (image == null && imageMap != null) {
image = imageMap.get(extractIdParam(normalizedPath));
}
if (image != null) {
images.add(image.withColumnOffset(columnOffsetStart + photoIndex));
} else {
@@ -567,13 +864,10 @@ public class ThirtyTeachTourLedgerController {
/**
* 根据后台保存的下载路径定位 sys_file,并读取真实图片字节及 POI 可识别的图片类型。
*/
private LedgerExportImage resolveLedgerImage(String originalPath) {
String normalizedPath = normalizePhotoPath(originalPath);
private LedgerExportImage resolveLedgerImage(String normalizedPath, Sys_file sysFile) {
String fileId = extractIdParam(normalizedPath);
Sys_file sysFile = fetchSysFile(fileId, normalizedPath);
if (sysFile == null) {
log.warn("三十年教龄疗休养台账图片文件记录不存在:fileId={}, originalPath={}, normalizedPath={}",
fileId, originalPath, normalizedPath);
log.warn("三十年教龄疗休养台账图片文件记录不存在:fileId={}, normalizedPath={}", fileId, normalizedPath);
return null;
}
byte[] bytes = readSysFileBytes(sysFile);
@@ -587,25 +881,11 @@ public class ThirtyTeachTourLedgerController {
sysFile.getId(), sysFile.getEngine(), sysFile.getStoragePath(), bytes == null ? 0 : bytes.length, pictureType);
return null;
}
log.info("三十年教龄疗休养台账图片读取成功:fileId={}, engine={}, bucket={}, storagePath={}, bytes={}, pictureType={}",
log.debug("三十年教龄疗休养台账图片读取成功:fileId={}, engine={}, bucket={}, storagePath={}, bytes={}, pictureType={}",
sysFile.getId(), sysFile.getEngine(), sysFile.getBucket(), sysFile.getStoragePath(), bytes.length, pictureType);
return new LedgerExportImage(sysFile.getId(), normalizedPath, bytes, pictureType);
}
/**
* 优先按文件ID查询,失败时按 downloadPath 查询,兼容后台保存的是完整下载路径的场景。
*/
private Sys_file fetchSysFile(String fileId, String normalizedPath) {
Sys_file sysFile = null;
if (StrUtil.isNotBlank(fileId)) {
sysFile = sysFileService.fetch(fileId);
}
if (sysFile == null && StrUtil.isNotBlank(normalizedPath)) {
sysFile = sysFileService.fetch(Cnd.where(Sys_file::getDownloadPath, "=", normalizedPath));
}
return sysFile;
}
/**
* 根据图片字节头判断 POI 图片类型,无法识别时尝试通过 ImageIO 转为 PNG。
*/
@@ -703,14 +983,10 @@ public class ThirtyTeachTourLedgerController {
* 将完整URL规整为站内相对路径,避免导出时重复拼接系统域名。
*/
private String normalizePhotoPath(String photoFile) {
String path = StrUtil.trimToEmpty(photoFile);
String path = decodePhotoPath(photoFile);
if (StrUtil.isBlank(path)) {
return "";
}
try {
path = URLDecoder.decode(path, StandardCharsets.UTF_8.toString());
} catch (Exception ignored) {
}
if (StrUtil.isNotBlank(Globals.AppDomain) && path.startsWith(Globals.AppDomain)) {
path = path.substring(Globals.AppDomain.length());
}
@@ -722,6 +998,21 @@ public class ThirtyTeachTourLedgerController {
return path;
}
/**
* 图片路径可能由前端 URL 编码后保存,统一解码后再参与匹配和规范化处理。
*/
private String decodePhotoPath(String photoFile) {
String path = StrUtil.trimToEmpty(photoFile);
if (StrUtil.isBlank(path)) {
return "";
}
try {
return URLDecoder.decode(path, StandardCharsets.UTF_8.toString());
} catch (Exception ignored) {
return path;
}
}
/**
* 台账导出图片对象,保存 POI 写图所需的图片字节、类型和来源文件信息。
*/
@@ -749,6 +1040,19 @@ public class ThirtyTeachTourLedgerController {
}
}
/**
* 图片并行读取任务的返回对象,保留规范化路径用于回填到导出行。
*/
private static class LedgerExportImageResolveResult {
private final String normalizedPath;
private final LedgerExportImage image;
private LedgerExportImageResolveResult(String normalizedPath, LedgerExportImage image) {
this.normalizedPath = normalizedPath;
this.image = image;
}
}
private List<NutMap> queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
@@ -761,6 +1065,7 @@ public class ThirtyTeachTourLedgerController {
COALESCE(t.age, vu.age) AS age,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(t.unionName, ''), vu.unionName, '') AS unionName,
t.unitName,
t.boardingPlace,
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
@@ -34,6 +34,7 @@ import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourL
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourMatterService;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
@@ -445,12 +446,14 @@ public class ThirtyTeachTourMySignupController {
* @param ledger 报名台账主信息
* @param families 家属信息 JSON 数组
* @param directRelative 直系亲属线路报名信息 JSON
* @param boardingPlaceOnly H5 修改页只更新乘车地点时传 true,避免修改其他只读报名信息
* @return 修改结果;直系亲属线路、超额报销等场景会返回待审核提示
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"thirtyTeachTour.mysignup", "h5.thirtyTeachTour.mysignup"}, mode = SaMode.OR)
public Result doSignup(ThirtyTeachTourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative) {
public Result doSignup(ThirtyTeachTourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative,
@Param("boardingPlaceOnly") Boolean boardingPlaceOnly) {
Result checkResult = checkSignup(ledger);
if (checkResult != null) {
return checkResult;
@@ -462,6 +465,9 @@ public class ThirtyTeachTourMySignupController {
if (oldLedger == null) {
return Result.error("报名记录不存在");
}
if (Boolean.TRUE.equals(boardingPlaceOnly)) {
return updateH5BoardingPlaceOnly(ledger, oldLedger);
}
List<ThirtyTeachTourLedgerFamily> familyList = parseFamilies(families);
Result familyResult = checkFamilies(familyList);
if (familyResult != null) {
@@ -541,6 +547,41 @@ public class ThirtyTeachTourMySignupController {
return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功");
}
/**
* H5 我的疗休养修改页仅允许调整乘车地点,其他报名、家属和直系亲属信息均以原台账为准。
* 保存时同步当前人员分配记录中的乘车地点,保证移动端列表、人员分配和台账展示一致。
*
* @param submitLedger 页面提交的台账信息,仅读取 id 和 boardingPlace
* @param oldLedger 当前登录人的原台账记录
* @return 乘车地点保存结果
*/
private Result updateH5BoardingPlaceOnly(ThirtyTeachTourLedger submitLedger, ThirtyTeachTourLedger oldLedger) {
if (oldLedger == null || StrUtil.isBlank(oldLedger.getMatterId())) {
return Result.error("报名记录缺少事项信息,不能修改");
}
ThirtyTeachTourMatter matter = tourMatterService.fetch(oldLedger.getMatterId());
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
return Result.error("报名出行时段不存在或已停用");
}
if (isTravelEnded(matter.getTravelEndTime())) {
return Result.error("线路出行已结束,不能修改");
}
ThirtyTeachTourSetting setting = tourLedgerService.dao().fetch(ThirtyTeachTourSetting.class, matter.getSettingId());
oldLedger.setBoardingPlace(submitLedger == null ? "" : submitLedger.getBoardingPlace());
Result boardingPlaceResult = normalizeBoardingPlace(oldLedger, matter, setting);
if (boardingPlaceResult != null) {
return boardingPlaceResult;
}
// H5 修改页除乘车地点外全部只读,数据库更新也只落乘车地点字段,避免前端绕过只读限制。
tourLedgerService.update(Chain.make("boardingPlace", oldLedger.getBoardingPlace()),
Cnd.where(ThirtyTeachTourLedger::getId, "=", oldLedger.getId())
.and(ThirtyTeachTourLedger::getJobNo, "=", currentJobNo())
.and(ThirtyTeachTourLedger::getDelFlag, "=", false));
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(),
oldLedger.getBoardingPlace(), oldLedger.getPhotoFiles(), oldLedger.getCurrentPeriodPhotoFiles());
return Result.success().addMsg("修改成功");
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"thirtyTeachTour.mysignup", "h5.thirtyTeachTour.mysignup"}, mode = SaMode.OR)
@@ -698,11 +698,13 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql);
NutMap quotaInfo = Lang.isEmpty(rows) ? emptyQuotaInfo() : rows.get(0);
int branchTotalQuota = branchPeriodQuotaTotal(settingId, unionId);
int formalQuota = defaultInt(quotaInfo.getInt("formalQuota"));
int backupQuota = defaultInt(quotaInfo.getInt("backupQuota"));
int formalUsed = defaultInt(quotaInfo.getInt("formalUsed"));
int backupUsed = defaultInt(quotaInfo.getInt("backupUsed"));
return NutMap.NEW()
.addv("branchTotalQuota", branchTotalQuota)
.addv("formalQuota", formalQuota)
.addv("backupQuota", backupQuota)
.addv("formalUsed", formalUsed)
@@ -2072,6 +2074,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
private NutMap emptyQuotaInfo() {
return NutMap.NEW()
.addv("branchTotalQuota", 0)
.addv("formalQuota", 0)
.addv("backupQuota", 0)
.addv("formalUsed", 0)
@@ -2080,6 +2083,29 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.addv("backupRemaining", 0);
}
/**
* 统计当前配置下当前分工会所有出行时间段分配人员名额的汇总数量,用于人员分配弹窗展示分工会总名额。
*/
private int branchPeriodQuotaTotal(String settingId, String unionId) {
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(unionId)) {
return 0;
}
Sql sql = Sqls.create("""
SELECT COALESCE(SUM(formalQuota), 0) AS branchTotalQuota
FROM thirty_teach_tour_setting_period_quota
WHERE settingId = @settingId
AND unionId = @unionId
AND delFlag = 0
""");
sql.setParam("settingId", settingId);
sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql);
if (Lang.isEmpty(rows)) {
return 0;
}
return defaultInt(rows.get(0).getInt("branchTotalQuota"));
}
private NutMap emptyPeriodQuotaInfo(String periodId) {
return NutMap.NEW()
.addv("periodId", periodId)
@@ -1,29 +1,19 @@
package com.budwk.app.zhgh.democratic.opinion.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam;
import com.budwk.app.zhgh.democratic.opinion.service.common.OpinionCommonService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.List;
/**
* @ClassName OpinionUnitReplyController
@@ -39,8 +29,6 @@ import java.util.List;
@Api(tags = "意见管理-承办答复")
public class OpinionUnitReplyController {
@Inject
private Dao dao;
@Inject
private OpinionCommonService opinionCommonService;
@@ -50,67 +38,15 @@ public class OpinionUnitReplyController {
public void index() {
}
/**
* 承办答复分页查询pageForm 接收届次 sessionId意见搜索条件分页及排序参数
* approval true 查询已审核 false 或未传时查询未审核
* 返回 Resultdata 为分页结果其中 list 为意见及任务信息totalCount 为匹配总数
*/
@At
@SaCheckPermission("opinion.unitReply")
@Aop(TransAop.READ_COMMITTED)
public Result pageData(@Valid OpinionSearchParam pageForm,
@Param(value = "String") String sessionId,
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke,
t.variable->>'$.underTakeName' AS underTakeName,
IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') in (true, 1), 1, 0) AS underTakeIsMaster
FROM
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN opinion_info info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN opinion_type type on type.id = info.typeId
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
LEFT JOIN wf_process_task_actor pta ON pta.processTaskId = nt.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("info.sessionId", "=", sessionId);
cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply"));
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("pta.actorId", "in", List.of(SecurityUtil.getUserId()));
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
OpinionSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = opinionCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
public Result pageData(@Valid OpinionSearchParam pageForm) {
return Result.success(opinionCommonService.listUnitReplyPage(pageForm));
}
}
@@ -1,7 +1,9 @@
package com.budwk.app.zhgh.democratic.opinion.service.common;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo;
import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.nutz.lang.util.NutMap;
@@ -12,6 +14,14 @@ import java.util.List;
public interface OpinionCommonService extends BaseService<OpinionInfo> {
/**
* 查询当前登录人的意见承办答复任务仅包含 JDHYJ 流程且业务意见存在的记录
*
* @param pageForm 届次意见搜索条件分页及排序参数approval true 查询已审核否则查询未审核
* @return 分页结果list 包含意见及任务信息totalCount 为匹配总数
*/
Pagination<NutMap> listUnitReplyPage(OpinionSearchParam pageForm);
/**
* 查询意见信息
*/
@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
@@ -26,6 +27,7 @@ import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo;
import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConsolidation;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge;
@@ -75,6 +77,71 @@ public class OpinionCommonServiceImpl extends BaseServiceImpl<OpinionInfo> imple
super(dao);
}
/**
* 按当前登录人和审核状态查询意见承办任务避免其他流程的同名节点混入列表
*
* @param pageForm 届次搜索分页及排序参数approval 为空时按未审核处理
* @return 包含 list totalCount 的分页结果保留页面所需的意见及任务字段
*/
@Override
public Pagination<NutMap> listUnitReplyPage(OpinionSearchParam pageForm) {
// 提案与意见存在同名答复节点必须同时校验流程标识及意见业务记录
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke,
t.variable->>'$.underTakeName' AS underTakeName,
IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') in (true, 1), 1, 0) AS underTakeIsMaster
FROM
wf_process_task t
INNER JOIN wf_process_instance ins ON ins.id = t.processInstanceId
INNER JOIN wf_process_define def ON def.id = ins.processDefineId
INNER JOIN opinion_info info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN opinion_type type on type.id = info.typeId
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
LEFT JOIN wf_process_task_actor pta ON pta.processTaskId = nt.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("def.name", "=", "JDHYJ");
cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply"));
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (Boolean.TRUE.equals(pageForm.getApproval())) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("pta.actorId", "in", List.of(SecurityUtil.getUserId()));
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
OpinionSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public NutMap info(String id) {
if (StrUtil.isBlank(id)) {
@@ -2,14 +2,21 @@ package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.hutool.core.bean.BeanUtil;
import cn.dev33.satoken.annotation.SaCheckPermission;
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.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalUnitReplyProposalVO;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.ApiOperation;
@@ -40,6 +47,8 @@ public class ProposalQueryUnitReplyController {
private Dao dao;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private SysRoleService sysRoleService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/underTakeReply/index.html")
@@ -54,6 +63,19 @@ public class ProposalQueryUnitReplyController {
// 提案配置 协办是否需要答复
ProposalConfig proposalConfig = dao.fetch(ProposalConfig.class, Cnd.NEW());
boolean slaveNeedReply = proposalConfig.getSlaveUnitNeedReply();
boolean proposalAdmin = isProposalAdmin();
if (!proposalAdmin) {
List<ProposalUndertake> manageUndertakes = getManageUndertakes();
List<String> manageUndertakeIds = manageUndertakes.stream().map(ProposalUndertake::getId).toList();
if (manageUndertakeIds.isEmpty()) {
return Result.success().addData(Map.of("tableData", Collections.EMPTY_LIST, "slaveNeedReply", slaveNeedReply));
}
// 非超级管理员提案管理员时统计范围固定为当前用户角色绑定的承办单位避免前端传其他单位越权查询
if (undertakeUnitId != null && !undertakeUnitId.isBlank() && !manageUndertakeIds.contains(undertakeUnitId)) {
return Result.success().addData(Map.of("tableData", Collections.EMPTY_LIST, "slaveNeedReply", slaveNeedReply));
}
}
// 当前届次所有的提案ID
Sql sql = Sqls.create("select id from proposal_info where sessionId = @sessionId").setParam("sessionId", sessionId);
@@ -75,7 +97,16 @@ public class ProposalQueryUnitReplyController {
// 提案委员会立案分配的承办单位
Cnd cnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds);
cnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
if (proposalAdmin) {
cnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
} else {
List<String> manageUndertakeIds = getManageUndertakes().stream().map(ProposalUndertake::getId).toList();
if (undertakeUnitId != null && !undertakeUnitId.isBlank()) {
cnd.and(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
} else {
cnd.and(ProposalReplyUnit::getUnitId, "in", manageUndertakeIds);
}
}
List<ProposalReplyUnit> replyUnits = dao.query(ProposalReplyUnit.class, cnd);
// 承办单位答复记录
@@ -142,6 +173,14 @@ public class ProposalQueryUnitReplyController {
@SaCheckPermission("proposal.query.unitReply")
@ApiOperation("承办单位提案明细")
public Result getProposalsByUnit(@Valid ProposalSearchParam pageForm, String unitCode) {
if (!isProposalAdmin()) {
List<ProposalUndertake> manageUndertakes = getManageUndertakes();
List<String> manageUnitCodes = manageUndertakes.stream().map(ProposalUndertake::getCode).toList();
if (manageUnitCodes.isEmpty() || !manageUnitCodes.contains(unitCode)) {
return Result.success(new Pagination(pageForm.getPageNumber(), pageForm.getPageSize(), 0, Collections.emptyList()));
}
}
Sql sql = Sqls.create("""
SELECT
info.id,
@@ -167,6 +206,10 @@ public class ProposalQueryUnitReplyController {
Cnd cnd = Cnd.NEW();
// 明细弹窗按当前届次和点击的承办单位编码查询返回该单位主办协办的提案列表
cnd.andEX("pru.unitCode", "=", unitCode);
if (!isProposalAdmin()) {
List<String> manageUndertakeIds = getManageUndertakes().stream().map(ProposalUndertake::getId).toList();
cnd.and("pru.unitId", "in", manageUndertakeIds);
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id", "pru.unitName", "pru.isMaster");
sql.setCondition(cnd);
@@ -176,4 +219,45 @@ public class ProposalQueryUnitReplyController {
pagination.setList(list);
return Result.success(pagination);
}
/**
* 判断当前用户是否拥有本页面全量统计权限
*
* 角色说明
* 1. SYSADMIN 为超级管理员可查看全部承办单位
* 2. SCHOOL_UNION_PROPOSAL_ADMIN 为校工会提案管理员可查看全部承办单位
* 3. 其他用户只能查看自己角色绑定的承办单位
*
* @return booleantrue 表示可查看全部承办单位false 表示需要限制到当前用户分管承办单位
*/
private boolean isProposalAdmin() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_PROPOSAL_ADMIN.name());
}
/**
* 获取当前用户角色绑定的提案承办单位
*
* 参数无参数方法内部根据当前登录用户和 PROPOSAL_UNIT_LEADER 角色查询 sys_user_role.underTakeId
* 处理逻辑用户角色表中的 underTakeId 表示该用户负责的提案承办单位不按教代会届次过滤
* 返回值List<ProposalUndertake>表示当前用户角色绑定的承办单位列表没有配置时返回空列表
*/
private List<ProposalUndertake> getManageUndertakes() {
Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER.name());
if (role == null) {
return Collections.emptyList();
}
List<Sys_user_role> userRoles = dao.query(Sys_user_role.class, Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUserId, "=", SecurityUtil.getUserId())
.and(Sys_user_role::getUnderTakeId, "is not", null));
List<String> undertakeIds = userRoles.stream()
.map(Sys_user_role::getUnderTakeId)
.filter(Objects::nonNull)
.filter(id -> !id.isBlank())
.distinct()
.toList();
if (undertakeIds.isEmpty()) {
return Collections.emptyList();
}
return dao.query(ProposalUndertake.class, Cnd.where(ProposalUndertake::getId, "in", undertakeIds));
}
}
@@ -0,0 +1,41 @@
package com.budwk.app.zhgh.democratic.proposal.controller.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 承办单位办理统计中的提案明细
*/
@Data
public class ProposalUnitReplyProposalVO {
@ApiModelProperty("主键")
private String id;
@ApiModelProperty("提案编号")
private String code;
@ApiModelProperty("提案名称")
private String name;
@ApiModelProperty("提案人姓名")
private String createUserName;
@ApiModelProperty("提案类型名称")
private String typeName;
@ApiModelProperty("届次")
private String sessionName;
@ApiModelProperty("代表团")
private String delegationName;
@ApiModelProperty("承办单位名称")
private String underTakeUnitName;
@ApiModelProperty("承办类型,主办或协办")
private String underTakeType;
@ApiModelProperty("当前流程节点")
private String curTaskName;
}
@@ -1,17 +1,24 @@
package com.budwk.app.zhgh.staffmanage.member.controller.change;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberManagePageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.List;
@@ -44,6 +51,29 @@ public class MemberChangeRecordController {
return Result.success(pagination);
}
/**
* 导出变更记录列表导出的字段和数据范围与当前页面查询结果保持一致
*
* @param pageForm 当前页面查询条件和显示字段
* @param response 文件下载响应
*/
@At
@Ok("void")
@SaCheckPermission("member.change.record")
public void doExport(MemberManagePageForm pageForm, HttpServletResponse response) {
try {
// 导出列由前端当前表格列配置决定额外补充页面固定展示的异动信息列
List<ExcelExportEntity> exportEntities = memberCommonService.buildMemberHistoryRecordExportEntities(pageForm.getColumns());
List<NutMap> list = memberCommonService.memberHistoryRecordExportData(pageForm);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("会员变更记录.xlsx", workbook, response);
} catch (Exception e) {
log.error("导出会员变更记录失败", e);
}
}
@At
@SaCheckPermission("member.change.record")
@@ -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")
@@ -14,6 +14,7 @@ import org.nutz.dao.util.cri.Static;
import org.nutz.lang.Lang;
import java.util.List;
import java.util.Map;
/**
* @version 1.0
@@ -59,6 +60,11 @@ public class MemberManagePageForm extends PageForm {
//变更来源
private List<Integer> changeOrigins;
/**
* 页面当前显示的列表字段导出时按照该字段顺序动态生成表头
*/
private List<Map<String, String>> columns;
public static void buildSearch(Cnd cnd, MemberManagePageForm pageForm) {
if (cnd == null) {
cnd = Cnd.NEW();
@@ -44,6 +44,9 @@ public class MemberStatisticsPageForm extends PageForm {
private List<String> preparedBys;
@ApiModelProperty("编制信息")
private List<String> compilationInformations;
private List<String> userStates;
private List<String> memberStatus;
@@ -1,5 +1,6 @@
package com.budwk.app.zhgh.staffmanage.member.service;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_user;
@@ -43,6 +44,22 @@ public interface MemberCommonService extends BaseService<Sys_user> {
*/
Pagination memberHistoryRecords(MemberManagePageForm pageForm);
/**
* 根据变更记录页面查询条件获取全部导出数据导出数据与列表查询口径保持一致
*
* @param pageForm 变更记录查询参数
* @return 变更记录导出数据
*/
List<NutMap> memberHistoryRecordExportData(MemberManagePageForm pageForm);
/**
* 根据前端当前列表列配置生成导出列确保页面展示什么字段就导出什么字段
*
* @param columns 页面列配置
* @return Excel 导出列配置
*/
List<ExcelExportEntity> buildMemberHistoryRecordExportEntities(List<Map<String, String>> columns);
/**
* 获取可变更字段
@@ -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) {
@@ -1,11 +1,13 @@
package com.budwk.app.zhgh.staffmanage.member.service.impl;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
@@ -139,6 +141,54 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
@Override
public Pagination memberHistoryRecords(MemberManagePageForm pageForm) {
Sql sql = getMemberHistoryRecordSql(pageForm);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/**
* 根据变更记录查询条件获取全部数据用于页面导出保证导出数据与列表查询口径一致
*
* @param pageForm 变更记录查询参数
* @return 已转换异动类型显示文本的导出数据
*/
@Override
public List<NutMap> memberHistoryRecordExportData(MemberManagePageForm pageForm) {
List<NutMap> list = listMap(getMemberHistoryRecordSql(pageForm));
Map<String, String> changeTypeNameMap = Arrays.stream(MemberChangeType.values())
.collect(Collectors.toMap(MemberChangeType::getCode, MemberChangeType::getChangeTypeName));
int[] no = {1};
list.forEach(row -> {
row.put("no", no[0]++);
row.put("changeTypesText", getChangeTypesText(row.get("changeTypes"), changeTypeNameMap));
});
return list;
}
/**
* 根据页面表格列生成导出列并补充页面分组展示的更新时间异动类型两列
*
* @param columns 页面当前显示的基础列
* @return Excel 导出列配置
*/
@Override
public List<ExcelExportEntity> buildMemberHistoryRecordExportEntities(List<Map<String, String>> columns) {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("序号", "no", 10));
if (Lang.isNotEmpty(columns)) {
columns.forEach(column -> exportEntities.add(new ExcelExportEntity(column.get("label"), column.get("prop"), 20)));
}
exportEntities.add(new ExcelExportEntity("更新时间", "changeTime", 25));
exportEntities.add(new ExcelExportEntity("异动类型", "changeTypesText", 30));
return exportEntities;
}
/**
* 构建变更记录列表 SQL分页查询和导出查询共用该方法避免筛选条件不一致
*
* @param pageForm 变更记录查询参数
* @return 变更记录查询 SQL
*/
private Sql getMemberHistoryRecordSql(MemberManagePageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
his.id,
@@ -206,7 +256,31 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
cnd.andEX("his.unionId", "=", SecurityUtil.getUnionId());
}
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return sql;
}
/**
* 将数据库保存的异动类型 JSON 数组转换为页面展示的中文名称供导出 Excel 使用
*
* @param changeTypes 数据库中的异动类型值
* @param changeTypeNameMap 异动类型枚举名称映射
* @return 逗号分隔的异动类型名称
*/
private String getChangeTypesText(Object changeTypes, Map<String, String> changeTypeNameMap) {
if (changeTypes == null) {
return null;
}
List<String> changeTypeList;
if (changeTypes instanceof Collection<?> collection) {
changeTypeList = collection.stream().map(String::valueOf).toList();
} else if (StrUtil.isNotBlank(String.valueOf(changeTypes))) {
changeTypeList = JSONUtil.parseArray(String.valueOf(changeTypes)).toList(String.class);
} else {
return null;
}
return changeTypeList.stream()
.map(type -> changeTypeNameMap.getOrDefault(type, type))
.collect(Collectors.joining(","));
}
@@ -90,6 +90,8 @@ public class MemberStatisticsServiceImpl extends BaseServiceImpl<Sys_user> imple
handleField("unitId", pageForm.getUnitId(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("personType", pageForm.getPersonTypes(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("preparedBy", pageForm.getPreparedBys(), reverseSelection, IN_OR_NIN_OP, cnd);
// 编制信息前端使用独立字典传值实际对应用户表/视图中的 compilationInformation 字段
handleField("compilationInformation", pageForm.getCompilationInformations(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("userState", pageForm.getUserStates(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("sex", pageForm.getSexTypes(), reverseSelection, IN_OR_NIN_OP, cnd);
cnd.andEX("sur.roleId", IN_OR_NIN_OP, pageForm.getRoleIds());
@@ -145,6 +147,7 @@ public class MemberStatisticsServiceImpl extends BaseServiceImpl<Sys_user> imple
u.arrivalAtSchoolDate,
u.personType,
u.preparedBy,
u.compilationInformation,
u.identityType,
u.userState,
u.unionName,
@@ -0,0 +1,6 @@
-- 30年教龄疗休养当前时间段图片材料字段。
ALTER TABLE `thirty_teach_tour_user_assignment`
ADD COLUMN `currentPeriodPhotoFiles` text DEFAULT NULL COMMENT '当前时间段图片材料' AFTER `photoFiles`;
ALTER TABLE `thirty_teach_tour_ledger`
ADD COLUMN `currentPeriodPhotoFiles` text DEFAULT NULL COMMENT '当前时间段图片材料' AFTER `photoFiles`;
@@ -63,6 +63,7 @@ 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">
@@ -120,7 +121,8 @@ layout("/layouts/platform.html"){
pullTimeOptions: false,
sourceTime: [],
pullTimeDialogVisible: false,
pullLoading: false
pullLoading: false,
syncTeacherMobileLoading: false
}
},
methods: {
@@ -148,6 +150,42 @@ 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) {
@@ -41,7 +41,6 @@ const basicForm = {
<el-col :span="12">
<el-form-item :rules="{required:true,message: '请选择活动时间', trigger: 'blur'}" label="活动时间" prop="activityTime">
<el-date-picker
:picker-options="pickerOptions"
end-placeholder="请选择活动结束日期"
range-separator="至"
start-placeholder="请选择活动开始日期"
@@ -41,7 +41,6 @@ const basicForm = {
<el-col :span="12">
<el-form-item :rules="{required:true,message: '请选择活动时间', trigger: 'blur'}" label="活动时间" prop="activityTime">
<el-date-picker
:picker-options="pickerOptions"
end-placeholder="请选择活动结束日期"
range-separator="至"
start-placeholder="请选择活动开始日期"
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
<el-table-column label="工号" prop="loginName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="已上传图片数量" width="140" align="center" header-align="center">
@@ -159,7 +159,7 @@ layout("/layouts/platform.html"){
</el-form-item>
</el-col>
<el-col :span="18" v-if="assignForm.personType === 'FORMAL'">
<el-form-item label="出行时间">
<el-form-item label="出行时间">
<div class="period-button-list">
<el-button
v-for="item in assignPeriodOptions"
@@ -177,6 +177,10 @@ layout("/layouts/platform.html"){
</el-form>
<div class="quota-bar">
<div class="quota-item quota-item-total">
<span class="quota-label">分工会总名额</span>
<span>{{ quotaInfo.branchTotalQuota }}</span>
</div>
<div class="quota-item">
<span class="quota-label">正式名额</span>
<span>{{ quotaInfo.formalQuota }}</span>
@@ -424,6 +428,9 @@ layout("/layouts/platform.html"){
color: #303133;
background: #fafafa;
}
.quota-item-total {
min-width: 150px;
}
.quota-label {
font-weight: 600;
margin-right: 10px;
@@ -503,6 +510,7 @@ layout("/layouts/platform.html"){
assignMatterOptions: [],
assignPeriodOptions: [],
quotaInfo: {
branchTotalQuota: 0,
formalQuota: 0,
backupQuota: 0,
formalUsed: 0,
@@ -511,6 +519,7 @@ layout("/layouts/platform.html"){
backupRemaining: 0
},
assignDialogVisible: false,
assignOpenToken: 0,
candidateLoading: false,
candidateSelectLoading: false,
candidateData: [],
@@ -617,6 +626,7 @@ layout("/layouts/platform.html"){
},
defaultQuotaInfo() {
return {
branchTotalQuota: 0,
formalQuota: 0,
backupQuota: 0,
formalUsed: 0,
@@ -726,21 +736,11 @@ layout("/layouts/platform.html"){
window.location.href = loc() + "/exportData?" + params.toString()
},
openAssign() {
const token = this.nextAssignOpenToken()
const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year)
this.candidateForm = this.defaultCandidateForm()
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignDialogVisible = true
this.loadAssignSettingOptions()
},
resetAssignDialog() {
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignSettingOptions = []
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
@@ -749,8 +749,37 @@ layout("/layouts/platform.html"){
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
this.assignDialogVisible = true
this.loadAssignSettingOptions(token)
},
resetAssignDialog() {
this.assignOpenToken += 1
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignSettingOptions = []
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
},
// 人员分配弹窗每次打开都生成新的批次标记,旧请求返回时不再覆盖新弹窗数据。
nextAssignOpenToken() {
this.assignOpenToken += 1
return this.assignOpenToken
},
isCurrentAssignOpen(token) {
return this.assignDialogVisible && token === this.assignOpenToken
},
assignYearChange() {
const token = this.assignOpenToken
this.assignForm.settingId = ""
this.assignForm.matterId = ""
this.assignForm.periodId = ""
@@ -761,9 +790,10 @@ layout("/layouts/platform.html"){
this.candidateUserOptions = []
this.candidateForm.keyword = ""
this.clearCandidateSelection()
this.loadAssignSettingOptions()
this.loadAssignSettingOptions(token)
},
assignSettingChange() {
const token = this.assignOpenToken
this.assignForm.matterId = ""
this.assignForm.periodId = ""
this.assignMatterOptions = []
@@ -772,30 +802,32 @@ layout("/layouts/platform.html"){
this.candidateUserOptions = []
this.candidateForm.keyword = ""
this.clearCandidateSelection()
this.loadAssignMatterOptions()
this.loadAssignTravelPeriods(true)
this.loadCandidatePageData()
this.loadAssignMatterOptions(token)
this.loadAssignTravelPeriods(token)
this.loadCandidatePageData(token)
},
assignPersonTypeChange() {
if (this.assignForm.personType === "BACKUP") {
this.assignForm.matterId = ""
}
this.loadQuotaInfo()
this.loadQuotaInfo(this.assignOpenToken)
},
loadAssignSettingOptions() {
loadAssignSettingOptions(token) {
token = token || this.assignOpenToken
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
this.assignSettingOptions = res.data || []
// 人员分配弹窗独立默认取当前年度第一条配置,再加载名额、线路和候选人员。
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions()
this.loadAssignTravelPeriods(true)
this.loadCandidatePageData()
this.loadAssignMatterOptions(token)
this.loadAssignTravelPeriods(token)
this.loadCandidatePageData(token)
} else if (this.assignForm.settingId) {
this.loadAssignMatterOptions()
this.loadAssignTravelPeriods(true)
this.loadCandidatePageData()
this.loadAssignMatterOptions(token)
this.loadAssignTravelPeriods(token)
this.loadCandidatePageData(token)
} else {
this.assignMatterOptions = []
this.assignPeriodOptions = []
@@ -806,46 +838,50 @@ layout("/layouts/platform.html"){
}
})
},
loadAssignTravelPeriods(defaultFirst) {
loadAssignTravelPeriods(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) {
this.assignPeriodOptions = []
this.assignForm.periodId = ""
this.loadQuotaInfo()
this.loadQuotaInfo(token)
return
}
this.$axios.post(loc() + "/travelPeriodOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
this.assignPeriodOptions = res.data || []
const hasCurrent = this.assignPeriodOptions.some(item => item.id === this.assignForm.periodId)
if ((defaultFirst || !hasCurrent) && this.assignPeriodOptions.length > 0) {
this.assignForm.periodId = this.assignPeriodOptions[0].id
} else if (!hasCurrent) {
if (!hasCurrent) {
this.assignForm.periodId = ""
}
} else {
this.assignPeriodOptions = []
this.assignForm.periodId = ""
}
this.loadQuotaInfo()
this.loadQuotaInfo(token)
})
},
loadAssignMatterOptions() {
loadAssignMatterOptions(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) {
this.assignMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
this.assignMatterOptions = res.data || []
}
})
},
loadQuotaInfo() {
loadQuotaInfo(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) {
this.quotaInfo = this.defaultQuotaInfo()
return
}
this.$axios.post(loc() + "/quotaInfo", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
const baseQuota = Object.assign(this.defaultQuotaInfo(), res.data || {})
if (this.assignForm.personType === "FORMAL") {
@@ -855,16 +891,18 @@ layout("/layouts/platform.html"){
}
this.quotaInfo = baseQuota
if (this.assignForm.personType === "FORMAL" && this.assignForm.periodId) {
this.loadPeriodQuotaInfo()
this.loadPeriodQuotaInfo(token)
}
}
})
},
loadPeriodQuotaInfo() {
loadPeriodQuotaInfo(token) {
token = token || this.assignOpenToken
this.$axios.post(loc() + "/periodQuotaInfo", {
settingId: this.assignForm.settingId,
periodId: this.assignForm.periodId
}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
const periodQuota = res.data || {}
this.quotaInfo = Object.assign({}, this.quotaInfo, {
@@ -876,13 +914,29 @@ layout("/layouts/platform.html"){
})
},
toggleAssignPeriod(item) {
const token = this.assignOpenToken
this.assignForm.periodId = this.assignForm.periodId === item.id ? "" : item.id
// 选择出行时间段时提醒先核对分配批次,避免批次线路时间与时间段不一致。
if (this.assignForm.periodId) {
this.$message({
type: "warning",
message: this.assignPeriodCheckMessage(),
duration: 5000
})
}
this.candidateForm.pageNumber = 1
this.clearCandidateSelection()
this.loadQuotaInfo()
this.loadCandidatePageData()
this.loadQuotaInfo(token)
this.loadCandidatePageData(token)
},
loadCandidatePageData() {
assignPeriodCheckMessage() {
if (!this.assignForm.matterId) {
return "请选择分配批次,并确认选中的 “分配批次” 与 “出行时间段” 出行时间是否一致"
}
return "请确认选中的 “分配批次” 与 “出行时间段” 出行时间是否一致"
},
loadCandidatePageData(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) {
this.candidateData = []
this.candidateForm.totalCount = 0
@@ -897,6 +951,7 @@ layout("/layouts/platform.html"){
keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword,
userIds: JSON.stringify(this.selectedCandidateIds || [])
}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
const data = res.data || {}
this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
@@ -909,7 +964,9 @@ layout("/layouts/platform.html"){
this.$message.warning(res.msg || "候选人员查询失败")
}
}).finally(() => {
this.candidateLoading = false
if (this.isCurrentAssignOpen(token)) {
this.candidateLoading = false
}
})
},
candidateSearch() {
@@ -948,6 +1005,7 @@ layout("/layouts/platform.html"){
// 人员选择器复用候选人员接口,后端会限定为当前登录人所在分工会会员。
this.candidateForm.keyword = keyword || ""
this.candidateSelectLoading = true
const token = this.assignOpenToken
this.$axios.post(loc() + "/candidatePageData", {
pageNumber: 1,
pageSize: 20,
@@ -955,12 +1013,15 @@ layout("/layouts/platform.html"){
retiringSoon: this.normalizeCandidateFilterValue(this.candidateForm.retiringSoon),
keyword: keyword || ""
}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
const data = res.data || {}
this.candidateUserOptions = this.mergeOptionLists(this.selectedCandidates, data.list || [])
}
}).finally(() => {
this.candidateSelectLoading = false
if (this.isCurrentAssignOpen(token)) {
this.candidateSelectLoading = false
}
})
},
candidateUserSelectChange(userIds) {
@@ -1149,7 +1210,7 @@ layout("/layouts/platform.html"){
return
}
if (this.assignForm.personType === "FORMAL" && !this.assignForm.periodId) {
this.$message.warning("请选择出行时间")
this.$message.warning("请选择出行时间")
return
}
this.$confirm("确定保存当前人员分配吗?", "提示", {
@@ -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>
@@ -109,6 +110,7 @@ layout("/layouts/platform.html"){
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属工会" prop="unionName" min-width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -365,6 +367,7 @@ layout("/layouts/platform.html"){
detailRow: {},
doneTasks: [],
showImportDialog: false,
syncMobileLoading: false,
multipleSelection: [],
filterOptionsTimer: null,
currentUnionId: "",
@@ -439,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 => {
@@ -91,7 +91,7 @@ layout("/layouts/platform.html"){
<el-table-column label="所属分工会" prop="unionName" min-width="160" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="已上传图片数量" width="140" align="center" header-align="center">
@@ -475,6 +475,7 @@ layout("/layouts/platform.html"){
assignMatterOptions: [],
unionOptions: [],
assignDialogVisible: false,
assignOpenToken: 0,
remindDialogVisible: false,
remindSubmitting: false,
candidateLoading: false,
@@ -760,28 +761,47 @@ layout("/layouts/platform.html"){
})
},
openAssign() {
const token = this.nextAssignOpenToken()
const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year)
this.assignForm.matterId = ""
this.candidateForm = this.defaultCandidateForm()
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignDialogVisible = true
this.loadAssignSettingOptions()
},
resetAssignDialog() {
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignSettingOptions = []
this.assignMatterOptions = []
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
this.assignDialogVisible = true
this.loadAssignSettingOptions(token)
},
resetAssignDialog() {
this.assignOpenToken += 1
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignSettingOptions = []
this.assignMatterOptions = []
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
},
// 人员分配弹窗每次打开都生成新的批次标记,旧请求返回时不再覆盖新弹窗数据。
nextAssignOpenToken() {
this.assignOpenToken += 1
return this.assignOpenToken
},
isCurrentAssignOpen(token) {
return this.assignDialogVisible && token === this.assignOpenToken
},
assignYearChange() {
const token = this.assignOpenToken
this.assignForm.settingId = ""
this.assignForm.matterId = ""
this.assignMatterOptions = []
@@ -789,31 +809,34 @@ layout("/layouts/platform.html"){
this.candidateUserOptions = []
this.candidateForm.keyword = ""
this.clearCandidateSelection()
this.loadAssignSettingOptions()
this.loadCandidatePageData()
this.loadAssignSettingOptions(token)
this.loadCandidatePageData(token)
},
assignSettingChange() {
const token = this.assignOpenToken
this.assignForm.matterId = ""
this.assignMatterOptions = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.candidateForm.keyword = ""
this.clearCandidateSelection()
this.loadAssignMatterOptions()
this.loadCandidatePageData()
this.loadAssignMatterOptions(token)
this.loadCandidatePageData(token)
},
loadAssignSettingOptions() {
loadAssignSettingOptions(token) {
token = token || this.assignOpenToken
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
this.assignSettingOptions = res.data || []
// 人员分配弹窗独立于主列表筛选,默认取当前年度第一条可用配置。
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions()
this.loadCandidatePageData()
this.loadAssignMatterOptions(token)
this.loadCandidatePageData(token)
} else if (this.assignForm.settingId) {
this.loadAssignMatterOptions()
this.loadCandidatePageData()
this.loadAssignMatterOptions(token)
this.loadCandidatePageData(token)
} else {
this.assignMatterOptions = []
this.candidateData = []
@@ -822,18 +845,21 @@ layout("/layouts/platform.html"){
}
})
},
loadAssignMatterOptions() {
loadAssignMatterOptions(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) {
this.assignMatterOptions = []
return
}
this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
this.assignMatterOptions = res.data || []
}
})
},
loadCandidatePageData() {
loadCandidatePageData(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) {
this.candidateData = []
this.candidateForm.totalCount = 0
@@ -851,6 +877,7 @@ layout("/layouts/platform.html"){
keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword,
userIds: JSON.stringify(this.selectedCandidateIds || [])
}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
const data = res.data || {}
this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
@@ -864,7 +891,9 @@ layout("/layouts/platform.html"){
this.$message.warning(res.msg || "候选人员查询失败")
}
}).finally(() => {
this.candidateLoading = false
if (this.isCurrentAssignOpen(token)) {
this.candidateLoading = false
}
})
},
assignMatterChange(value) {
@@ -922,6 +951,7 @@ layout("/layouts/platform.html"){
// 人员选择器复用候选人员接口,校工会可按分工会过滤,也可不选分工会查询全部会员。
this.candidateForm.keyword = keyword || ""
this.candidateSelectLoading = true
const token = this.assignOpenToken
this.$axios.post(loc() + "/candidatePageData", {
pageNumber: 1,
pageSize: 20,
@@ -932,12 +962,15 @@ layout("/layouts/platform.html"){
lastThirtyJoined: this.normalizeCandidateFilterValue(this.candidateForm.lastThirtyJoined),
keyword: keyword || ""
}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) {
const data = res.data || {}
this.candidateUserOptions = this.mergeOptionLists(this.selectedCandidates, data.list || [])
}
}).finally(() => {
this.candidateSelectLoading = false
if (this.isCurrentAssignOpen(token)) {
this.candidateSelectLoading = false
}
})
},
candidateUserSelectChange(userIds) {
@@ -202,21 +202,21 @@ layout("/layouts/platform.html"){
this.doSearch()
},
// 查询开启的教代会
// 默认届次确定后再查询列表,避免无届次请求覆盖带届次的查询结果。
listOpenSession() {
this.$axios.post("/platform/opinion/common/listOpenSession").then((res) => {
return this.$axios.post("/platform/opinion/common/listOpenSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = res.data
if (this.sessionOptions) {
this.$set(this, "sessionOptions", res.data || [])
if (this.sessionOptions.length > 0) {
this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id)
this.pageData()
}
// 无开启届次时仍按当前用户查询,由后端限定为有效的意见任务。
this.pageData()
}
})
},
},
created() {
this.pageData()
this.listOpenSession()
}
})
@@ -39,7 +39,9 @@ layout("/layouts/platform.html"){
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="变更列表" :app="this"></table-tool>
<table-tool label="变更列表" :app="this">
<el-button icon="el-icon-printer" type="primary" size="small" @click="doExport">导出</el-button>
</table-tool>
<el-table :data="tableData" style="width: 100%" row-key="id"
@sort-change="pageOrder"
resizable
@@ -189,6 +191,19 @@ layout("/layouts/platform.html"){
})
},
doExport() {
const pageForm = clone(this.pageForm)
pageForm.userStates = JSON.stringify(pageForm.userStates)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.changeTypes = JSON.stringify(pageForm.changeTypes)
pageForm.changeOrigins = JSON.stringify(pageForm.changeOrigins)
// 导出字段取当前页面列表列,并补充后端固定处理的异动信息列。
pageForm.columns = JSON.stringify(this.tableColumns.map((item) => {
return {prop: item.prop, label: item.label}
}))
this.$downLoad(loc() + "/doExport", pageForm)
},
},
async created() {
@@ -72,6 +72,7 @@
<el-descriptions-item label="在职状态">{{ viewData.userState }}</el-descriptions-item>
<el-descriptions-item label="教职工类别">{{ viewData.personType }}</el-descriptions-item>
<el-descriptions-item label="编制类别">{{ viewData.preparedBy }}</el-descriptions-item>
<el-descriptions-item label="编制信息">{{ viewData.compilationInformation }}</el-descriptions-item>
<!-- 4 -->
<el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item>
@@ -82,6 +83,8 @@
<el-descriptions-item label="行政级别(管理岗位)">{{ viewData.administrativeLevel }}</el-descriptions-item>
<el-descriptions-item label="岗级">{{ viewData.positionLevel }}</el-descriptions-item>
<el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item>
<el-descriptions-item></el-descriptions-item>
<el-descriptions-item></el-descriptions-item>
</el-descriptions>
</div>
`,
@@ -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)
@@ -160,6 +160,7 @@ layout("/layouts/platform.html"){
{ prop: "userState", label: "在职状态", width: 120, sortable: true },
{ prop: "personType", label: "教职工类别", width: 120, sortable: true },
{ prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
{ prop: "compilationInformation", label: "编制信息", width: 120, sortable: true },
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
{ prop: "unionName", label: "所属工会", width: 120, sortable: true },
{ prop: "unitName", label: "所属单位", width: 120, sortable: true },
@@ -201,6 +202,8 @@ layout("/layouts/platform.html"){
pageForm.sexTypes = JSON.stringify(this.pageForm.sexTypes)
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
pageForm.compilationInformations = JSON.stringify(this.pageForm.compilationInformations)
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
return {prop: item.prop, label: item.label}
@@ -225,6 +228,7 @@ layout("/layouts/platform.html"){
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
pageForm.compilationInformations = JSON.stringify(this.pageForm.compilationInformations)
pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus)
pageForm.userStates = JSON.stringify(this.pageForm.userStates)
if (pageForm.birthdayRange && pageForm.birthdayRange.length > 0) {
@@ -111,6 +111,31 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
</el-link>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">编制信息</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag
style="margin-right: 10px;cursor: pointer"
v-for="item in compilationInformationOptions"
:key="item.code"
:type="item.name"
:effect="pageForm.compilationInformations.includes(item.name)?'dark':'plain'"
@click="tagClick('compilationInformations',item.name)">
{{ item.name }}
</el-tag>
<el-link type="danger"
v-if="compilationInformationOptions.length&&pageForm.compilationInformations.length"
:underline="false"
@click="pageForm.compilationInformations=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.compilationInformations=compilationInformationOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="query-row">
<el-col class="query-row-content">
@@ -208,6 +233,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
searchName: "username",
personTypes: [],
preparedBys: [],
compilationInformations: [],
userStates: [],
memberStatus: [],
year: moment().format("YYYY"),
@@ -224,6 +250,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
},
personTypeOptions: [],
preparedByOptions: [],
compilationInformationOptions: [],
userStateOptions: [],
memberStatusOptions: [],
unions: [],
@@ -315,6 +342,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
this.pageForm.sexTypes = []
this.pageForm.personTypes = []
this.pageForm.preparedBys = []
this.pageForm.compilationInformations = []
this.pageForm.memberStatus = []
this.pageForm.userStates = []
this.pageForm.unionId = []
@@ -352,6 +380,10 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
this.$businessTool.getDictOptions("USER_PREPARED_BY_TYPE").then((data) => {
this.preparedByOptions = data
})
// 编制信息使用独立字典,后端按 sys_user.compilationInformation 字段进行筛选。
this.$businessTool.getDictOptions("USER_COMPILATION_INFORMATION").then((data) => {
this.compilationInformationOptions = data
})
this.$businessTool.getDictOptions("USER_STATE").then((data) => {
this.userStateOptions = data
})
@@ -379,7 +379,6 @@ layout("/layouts/platform_h5.html"){
<van-button v-if="canModify(row) || isTravelEnded(row)" size="small" type="info" :disabled="isTravelEnded(row)" @click="openSignup(row)">修改</van-button>
<van-button v-if="canRevoke(row)" size="small" type="danger" plain @click="revokeSignup(row)">撤回</van-button>
<van-button v-if="showLeaveThirtyTeachTour(row)" size="small" type="danger" plain :loading="leaveLoading" @click="leaveThirtyTeachTour(row)">退出疗休养报名</van-button>
<van-button v-if="showCancel(row)" size="small" type="danger" plain :disabled="isTravelEnded(row)" @click="cancelByRow(row)">取消报名</van-button>
<van-button v-if="showExport(row)" size="small" type="info" plain :disabled="!canExport(row)" @click="downloadExport(row, 'cost')">报销申请</van-button>
<van-button v-if="showDirectRelativeExport(row)" size="small" type="info" plain :disabled="!canDirectRelativeExport(row)" @click="downloadExport(row, 'direct')">亲属线路申请</van-button>
</div>
@@ -470,8 +469,9 @@ layout("/layouts/platform_h5.html"){
<div class="tour-section-title">教职工信息</div>
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
<van-field label="身份证号" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号"></van-field>
<van-field label="身份证号" readonly v-model="signupForm.idCard"></van-field>
<van-field label="年龄" readonly :value="staffAge(signupForm)"></van-field>
<van-field label="手机号" readonly v-model="signupForm.mobile"></van-field>
<van-field label="所在工会" readonly v-model="signupForm.unionName"></van-field>
</div>
@@ -480,13 +480,13 @@ layout("/layouts/platform_h5.html"){
<van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field>
<van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field>
<van-field label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
<van-field readonly clickable is-link label="乘车地点" v-model="signupForm.boardingPlace" placeholder="请选择乘车地点" @click="openBoardingPlacePicker"></van-field>
<van-field v-if="fillBedInfo" readonly clickable is-link label="床型" v-model="signupForm.bedType" placeholder="请选择床型" @click="openBedPicker('self')"></van-field>
<van-field v-if="fillBedInfo" label="床位信息" v-model="signupForm.bedInfo" maxlength="100" placeholder="请输入床位信息"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="signupForm.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
<van-field required readonly clickable is-link label="乘车地点" v-model="signupForm.boardingPlace" placeholder="请选择乘车地点" @click="openBoardingPlacePicker"></van-field>
<van-field v-if="fillBedInfo" label="床型" readonly v-model="signupForm.bedType"></van-field>
<van-field v-if="fillBedInfo" label="床位信息" readonly v-model="signupForm.bedInfo"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" readonly v-model="signupForm.intendedRoommate"></van-field>
<van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用">
<template #input>
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal">
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal" disabled>
<van-radio :name="true"></van-radio>
<van-radio :name="false"></van-radio>
</van-radio-group>
@@ -499,12 +499,12 @@ layout("/layouts/platform_h5.html"){
<div v-if="isDirectFamilyLine(signupForm)" class="tour-section">
<div class="tour-section-title">直系亲属线路</div>
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
<van-field readonly clickable is-link label="亲属关系" v-model="directRelativeForm.relationshipName" placeholder="请选择亲属关系" @click="openDirectRelativePicker"></van-field>
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
<van-field label="亲属姓名" readonly v-model="directRelativeForm.relativeName"></van-field>
<van-field label="所在单位" readonly v-model="directRelativeForm.unitName"></van-field>
<van-field label="亲属关系" readonly v-model="directRelativeForm.relationshipName"></van-field>
<van-field label="线路名称" readonly v-model="directRelativeForm.lineName"></van-field>
<van-field label="出行开始" readonly v-model="directRelativeForm.travelStartTime"></van-field>
<van-field label="出行结束" readonly v-model="directRelativeForm.travelEndTime"></van-field>
</div>
<div v-if="allowFamily && !isDirectFamilyLine(signupForm)" class="tour-section">
@@ -512,29 +512,26 @@ layout("/layouts/platform_h5.html"){
<div v-for="(item,index) in familyData" :key="index" class="tour-family-card">
<div class="tour-family-head">
<span>家属{{ index + 1 }}</span>
<van-button size="mini" type="danger" plain @click="removeFamily(index)">删除</van-button>
</div>
<van-field label="姓名" v-model="item.familyName" maxlength="100" placeholder="请填写姓名"></van-field>
<van-field label="年龄" v-model.number="item.age" type="digit" placeholder="请填写年龄"></van-field>
<van-field label="姓名" readonly v-model="item.familyName"></van-field>
<van-field label="年龄" readonly v-model="item.age"></van-field>
<van-field label="性别">
<template #input>
<van-radio-group v-model="item.gender" direction="horizontal">
<van-radio-group v-model="item.gender" direction="horizontal" disabled>
<van-radio name="男"></van-radio>
<van-radio name="女"></van-radio>
</van-radio-group>
</template>
</van-field>
<van-field label="身份证号" v-model="item.idCard" maxlength="18" placeholder="请填写身份证号" @blur="normalizeFamilyIdCard(item)"></van-field>
<van-field readonly clickable is-link label="关系" v-model="item.relationship" placeholder="请选择关系" @click="openRelationshipPicker(index)"></van-field>
<van-field v-if="fillBedInfo" readonly clickable is-link label="床型" v-model="item.bedType" placeholder="请选择床型" @click="openBedPicker('family', index)"></van-field>
<van-field v-if="fillBedInfo" label="床位" v-model="item.bedInfo" maxlength="100" placeholder="请输入床位信息"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="item.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
<van-field label="身份证号" readonly v-model="item.idCard"></van-field>
<van-field label="关系" readonly v-model="item.relationship"></van-field>
<van-field v-if="fillBedInfo" label="床型" readonly v-model="item.bedType"></van-field>
<van-field v-if="fillBedInfo" label="床位" readonly v-model="item.bedInfo"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" readonly v-model="item.intendedRoommate"></van-field>
</div>
<van-cell title="添加家属" is-link @click="addFamily"></van-cell>
</div>
</div>
<div class="tour-bottom-bar">
<van-button v-if="showCancel(signupForm)" type="danger" plain block round :loading="cancelLoading" @click="cancelSignup">取消报名</van-button>
<van-button type="info" block round :loading="signupLoading" @click="submitSignup">提交</van-button>
</div>
</van-popup>
@@ -623,7 +620,7 @@ layout("/layouts/platform_h5.html"){
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
},
boardingPlaceColumns() {
return this.boardingPlaceOptions
return [""].concat(this.boardingPlaceOptions || [])
}
},
methods: {
@@ -986,7 +983,7 @@ layout("/layouts/platform_h5.html"){
this.boardingPlacePickerVisible = true
},
confirmBoardingPlace(value) {
this.signupForm.boardingPlace = value
this.signupForm.boardingPlace = value || ""
this.boardingPlacePickerVisible = false
},
normalizeFamilyIdCard(row) {
@@ -1032,11 +1029,11 @@ layout("/layouts/platform_h5.html"){
vant.Toast("请选择乘车地点")
return
}
if (!this.validateFamilies() || !this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
// H5 修改页当前只允许调整乘车地点,家属、直系亲属等历史报名信息仅展示不参与提交校验。
const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families),
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : ""
boardingPlaceOnly: true,
families: "[]",
directRelative: ""
})
this.signupLoading = true
this.$axios.post("/platform/thirtyTeachTour/mysignup/doSignup", form).then((res) => {