华住会更新人员整改
This commit is contained in:
@@ -85,6 +85,9 @@ public class MainLauncher {
|
||||
init_task();
|
||||
init_auth();
|
||||
ioc.get(Globals.class);
|
||||
// Globals 会清空并恢复系统任务,必须等它完成后再恢复华住未结束批次;新批次由按钮提交时创建调度。
|
||||
log.info("[华住同步][步骤=全局任务初始化完成] 开始检查待恢复批次");
|
||||
ioc.get(com.budwk.app.zhgh.huazhu.service.impl.HuaZhuEmployeeServiceImpl.class).restorePendingSyncTask();
|
||||
ioc.get(FlowEngine.class);
|
||||
ioc.get(GlobalMessageSendService.class);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ package com.budwk.app.base.constant;
|
||||
public class RedisConstant {
|
||||
//项目统一前缀
|
||||
public final static String PLATFORM_REDIS_PREFIX = "budwk5mini:";
|
||||
/** 与原华住认证服务一致,缓存 HuaZhuAuthTokenResultDTO 的 JSON。 */
|
||||
public static final String REDIS_KEY_HUAZHU_AUTH_TOKEN = PLATFORM_REDIS_PREFIX + "hz:token:";
|
||||
public final static String PLATFORM_REDIS_WKCACHE_PREFIX = PLATFORM_REDIS_PREFIX + "wkcache:";
|
||||
|
||||
//聊天室
|
||||
|
||||
@@ -84,7 +84,8 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
||||
}
|
||||
|
||||
for (Sys_unit unit : units) {
|
||||
unit.setParentId("0");
|
||||
// 当前系统学校根单位 ID 为 1,同步的二级单位统一挂在该节点下。
|
||||
unit.setParentId("1");
|
||||
unit.setUnitLevel(2);
|
||||
unit.setUnitTypeCode(1);
|
||||
if (unitIds.contains(unit.getId())) {
|
||||
@@ -92,7 +93,7 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
|
||||
} else {
|
||||
Map<String, Object> beanMap = BeanUtil.beanToMap(unit);
|
||||
beanMap.remove("child");
|
||||
sysUnitService.save(unit, "0");
|
||||
sysUnitService.save(unit, "1");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -21,11 +21,13 @@ import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatis
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
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.dao.util.cri.Static;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
@@ -370,11 +372,27 @@ public class TrainSignUpApplyController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收本次报名信息并校验手机号必填及11位格式,PC 与 H5 共用此入口。
|
||||
*
|
||||
* @param trainSignUpUser 报名参数:activityId 为活动ID,courseId 为课程ID,mobile 为必填的11位大陆手机号;
|
||||
* 限额课程通过 activityCourseId 指定时段,mobileColumnsValue 为动态字段及其填写值列表
|
||||
* @return Result,code 为结果状态码,msg 为手机号校验或报名处理的结果说明
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@ApiOperation("活动报名")
|
||||
@SaCheckPermission(value = {"trainSignUp.apply", "h5.trainSignUp.apply"}, mode = SaMode.OR)
|
||||
@SLog(tag = "品牌活动-活动报名", msg = "活动报名")
|
||||
public Result doSignUp(TrainSignUpUser trainSignUpUser) {
|
||||
// 在进入报名处理前拦截缺失、空串及纯空白手机号,避免绕过页面必填校验。
|
||||
if (trainSignUpUser == null || StrUtil.isBlank(trainSignUpUser.getMobile())) {
|
||||
return Result.error("请填写手机号");
|
||||
}
|
||||
// 与 PC、H5 使用同一规则,仅接受1开头、第二位为3至9的11位数字。
|
||||
if (!trainSignUpUser.getMobile().matches("^1[3-9][0-9]{9}$")) {
|
||||
return Result.error("请输入正确的11位手机号");
|
||||
}
|
||||
try {
|
||||
lock.lock();
|
||||
boolean courseByUser = trainSignUpActivityService.isSignCourseByUser(trainSignUpUser.getCourseId(), SecurityUtil.getUserId());
|
||||
|
||||
+13
-8
@@ -22,7 +22,6 @@ import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatis
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.nutz.dao.Chain;
|
||||
@@ -188,6 +187,12 @@ public class TrainSignUpActivityStatisticsController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出指定活动的报名人员,按课程分别生成工作表,包含手机号等基础信息及该课程的动态报名字段。
|
||||
*
|
||||
* @param activityId 要导出报名人员的活动ID
|
||||
* @param response 接收Excel下载内容的HTTP响应;方法无返回值,输出.xlsx格式的报名人员名单
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
@ApiOperation("导出签到名单")
|
||||
@@ -237,9 +242,10 @@ public class TrainSignUpActivityStatisticsController {
|
||||
for (TrainSignUpCourse c : courseList) {
|
||||
String k = c.getCourseName();
|
||||
List<NutMap> v = userList.stream().filter(x -> StrUtil.isNotBlank(x.getString("courseName")) && x.getString("courseName").equals(k)).collect(Collectors.toList());
|
||||
ExportParams userExportParams = new ExportParams();
|
||||
userExportParams.setSheetName(k);
|
||||
userExportParams.setType(ExcelType.HSSF);
|
||||
ExportParams exportParams = new ExportParams();
|
||||
// 工作表参数与工作簿统一使用XSSF,确保导出的.xlsx文件格式一致。
|
||||
exportParams.setType(ExcelType.XSSF);
|
||||
exportParams.setSheetName(k);
|
||||
|
||||
List<ExcelExportEntity> currentEntities = new ArrayList<>();
|
||||
currentEntities.addAll(excelCommonExportEntity);
|
||||
@@ -285,20 +291,19 @@ public class TrainSignUpActivityStatisticsController {
|
||||
|
||||
Map<String, Object> userExportMap = new HashMap<>();
|
||||
userExportMap.put("name", k);
|
||||
userExportParams.setType(ExcelType.HSSF);
|
||||
userExportMap.put("title", userExportParams);
|
||||
userExportMap.put("title", exportParams);
|
||||
userExportMap.put("entity", currentEntities);
|
||||
userExportMap.put("data", v);
|
||||
|
||||
sheetsList.add(userExportMap);
|
||||
}
|
||||
|
||||
Workbook workbook = new HSSFWorkbook();
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
for (Map<String, Object> map : sheetsList) {
|
||||
ExcelExportService service = new ExcelExportService();
|
||||
service.createSheetForMap(workbook, (ExportParams) map.get("title"), (List<ExcelExportEntity>) map.get("entity"), (Collection<?>) map.get("data"));
|
||||
}
|
||||
CommonDownloadUtil.download(activity.getActivityName() + "报名人员名单" + ".xls", workbook, response);
|
||||
CommonDownloadUtil.download(activity.getActivityName() + "报名人员名单" + ".xlsx", workbook, response);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
+30
-18
@@ -557,6 +557,12 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
||||
return ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按综合导出查询条件将提案逐份生成为 DOCX,再完整打包为 ZIP。
|
||||
*
|
||||
* @param pageForm 综合导出页面的筛选参数,用于筛选提案名称、编号、单位等条件
|
||||
* @param response HTTP 响应,成功时直接输出 ZIP 二进制,包内每个 DOCX 对应一条提案
|
||||
*/
|
||||
@Override
|
||||
public void exportProposalStatisticsAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
|
||||
Sql sql = Sqls.create("""
|
||||
@@ -574,32 +580,38 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
|
||||
// 同一提案人可有多届代表记录,关联查询后按提案 ID 去重,避免重复生成同一提案。
|
||||
cnd.groupBy("info.id");
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> list = listMap(sql);
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
|
||||
for (NutMap proposalRow : list) {
|
||||
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
exportProposalAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
|
||||
ZipEntry zipEntry = new ZipEntry(proposalRow.getString("code") + "-" + sanitizeZipFileName(proposalRow.getString("name")) + ".docx");
|
||||
zipOutputStream.putNextEntry(zipEntry);
|
||||
docxByteArrayOutputStream.writeTo(zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
} catch (IOException e) {
|
||||
// 处理单个文件生成失败的情况
|
||||
throw new RuntimeException("zip entry write failed, proposalId=" + proposalRow.getString("id"), e);
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
LinkedHashSet<String> zipEntryNames = new LinkedHashSet<>();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(bos)) {
|
||||
for (NutMap proposalRow : list) {
|
||||
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
exportProposalAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
|
||||
String baseEntryName = proposalRow.getString("code") + "-" + sanitizeZipFileName(proposalRow.getString("name"));
|
||||
String entryName = baseEntryName + ".docx";
|
||||
int sameNameIndex = 2;
|
||||
// 不同提案可能同名,或清理特殊字符后重名;按最终文件名检查,保留全部提案。
|
||||
while (!zipEntryNames.add(entryName)) {
|
||||
entryName = baseEntryName + "-" + sameNameIndex++ + ".docx";
|
||||
}
|
||||
zipOutputStream.putNextEntry(new ZipEntry(entryName));
|
||||
docxByteArrayOutputStream.writeTo(zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("zip entry write failed, proposalId=" + proposalRow.getString("id"), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
zipOutputStream.finish();
|
||||
zipOutputStream.close();
|
||||
// 压缩流成功关闭、ZIP 目录完整写入后再下载,任一提案失败时不输出部分压缩包。
|
||||
CommonDownloadUtil.download("提案汇总压缩包.zip", bos.toByteArray(), response);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("批量导出提案表 ZIP 失败", e);
|
||||
throw new BaseException("批量导出提案表失败,请联系管理员");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuEmployeePageForm;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuSyncPageForm;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuEmployeeService;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
@@ -37,12 +38,16 @@ public class HuaZhuManageController {
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result pageData(@Valid HuaZhuEmployeePageForm pageForm) {
|
||||
return Result.success(huaZhuEmployeeService.pageUsers(pageForm));
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.pageUsers(pageForm));
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取待推送员工失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userIds 页面勾选员工的本项目用户主键数组,至少传入一个主键
|
||||
* @return Result,data 为逐人同步结果,含 userId、loginName、userName、success 和 message
|
||||
* @return Result,data 为已创建批次,含 id、total、status、submittedAt;失败原因在 msg
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@@ -51,6 +56,7 @@ public class HuaZhuManageController {
|
||||
if (userIds == null || userIds.length == 0) {
|
||||
return Result.error("请先选择需要同步的员工");
|
||||
}
|
||||
// 写入异常交给统一异常处理器;不在事务内部吞掉异常,保证批次与人员一起回滚。
|
||||
return Result.success(huaZhuEmployeeService.syncUsers(Arrays.asList(userIds)));
|
||||
}
|
||||
|
||||
@@ -65,24 +71,45 @@ public class HuaZhuManageController {
|
||||
return Result.success(NutMap.NEW().addv("description", huaZhuEmployeeService.getSyncRuleDescription()));
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取同步规则失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无需请求参数;由服务端按固定业务规则筛选全部待同步人员。
|
||||
* @return Result,data 为逐人同步结果,含 userId、loginName、userName、success 和 message;无人符合时为空列表
|
||||
* @return Result,data 为已创建批次,含 id、total、status、submittedAt;失败原因在 msg;无人需要推送时返回中文错误
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncRuleUsers() {
|
||||
return Result.success(huaZhuEmployeeService.syncRuleUsers());
|
||||
}
|
||||
|
||||
/** taskId 为可选批次主键;data 含 task、counts、tasks、running,不触发推送。 */
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncSummary(@Param("taskId") String taskId) {
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.syncRuleUsers());
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
return Result.success(huaZhuEmployeeService.syncSummary(taskId));
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取推送进度失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/** pageForm 含分页及可选批次、状态、工号/姓名;data.list 为逐人回执,totalCount 为总条数。 */
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result recordPageData(@Valid HuaZhuSyncPageForm pageForm) {
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.pageRecords(pageForm));
|
||||
} catch (Exception e) {
|
||||
return Result.error("读取推送记录失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/** userId 为本项目员工主键;data 为华住实时状态,msg 为可直接显示的中文错误。 */
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result queryStatus(@Param("userId") String userId) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.budwk.app.zhgh.huazhu.dto;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
/** 沿用原系统的认证响应对象及字段映射,同时用于 Redis 缓存序列化。 */
|
||||
@Data
|
||||
public class HuaZhuAuthTokenResultDTO {
|
||||
/** 华住返回的裸 Token,调用接口时再拼接 Bearer 前缀。 */
|
||||
@JSONField(name = "access_token")
|
||||
private String accessToken;
|
||||
@JSONField(name = "token_type")
|
||||
private String tokenType;
|
||||
/** 华住返回的有效时间,单位秒;保持原 DTO 的字符串类型。 */
|
||||
@JSONField(name = "expires_in")
|
||||
private String expiresIn;
|
||||
private String scope;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.budwk.app.zhgh.huazhu.job;
|
||||
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuEmployeeService;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
/** 固定后台入口;同一 Job 串行运行整批人员,页面离开不会中断执行。 */
|
||||
@IocBean
|
||||
@DisallowConcurrentExecution
|
||||
public class HuaZhuSyncJob implements Job {
|
||||
private static final Log log = Logs.get();
|
||||
@Inject
|
||||
private HuaZhuEmployeeService huaZhuEmployeeService;
|
||||
|
||||
/** context 为 Quartz 触发上下文;仅调用业务服务,不在 Job 中编排人员或开启整批事务。 */
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
String fireId = context == null ? "-" : context.getFireInstanceId();
|
||||
long started = System.nanoTime();
|
||||
log.infof("[华住同步][步骤=调度触发] 开始,fireInstanceId=%s", fireId);
|
||||
if (huaZhuEmployeeService == null) {
|
||||
// 工厂回退为普通实例会丢失 IOC 注入,此日志明确区别于触发器没有触发。
|
||||
log.error("[华住同步][步骤=检查服务注入] 失败,Job 未获得华住服务,请检查 Quartz 的 IOC JobFactory 配置");
|
||||
throw new JobExecutionException("华住后台服务未注入", false);
|
||||
}
|
||||
try {
|
||||
huaZhuEmployeeService.runPendingTask();
|
||||
} catch (Exception e) {
|
||||
log.errorf("[华住同步][步骤=后台调用] 异常,fireInstanceId=%s,类型=%s,栈=%s", fireId,
|
||||
e.getClass().getName(), java.util.Arrays.toString(e.getStackTrace()));
|
||||
// 不携带可能含认证信息的原异常消息,不立即重发推送。
|
||||
throw new JobExecutionException("华住后台调用异常,请查看阶段日志", false);
|
||||
} finally {
|
||||
log.infof("[华住同步][步骤=调度结束] fireInstanceId=%s,耗时=%dms", fireId, (System.nanoTime() - started) / 1_000_000L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.budwk.app.zhgh.huazhu.models;
|
||||
|
||||
import lombok.Data;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
/** 每次推送的人员快照与回执;不保存认证 token、卡密码或请求正文。 */
|
||||
@Data
|
||||
@Table("huazhu_sync_record")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4','mysql-engine':'InnoDB'}")
|
||||
// 同批次及同一员工均可存在多条相同状态的记录;这两个索引仅用于查询,不能限制唯一性。
|
||||
@TableIndexes({@Index(name = "IDX_HUAZHU_TASK_STATUS", fields = {"taskId", "status"}, unique = false),
|
||||
@Index(name = "IDX_HUAZHU_USER_STATUS", fields = {"userId", "status"}, unique = false)})
|
||||
public class HuaZhuSyncRecord {
|
||||
@Name
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String id;
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String taskId;
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String userId;
|
||||
@Column
|
||||
private String loginName;
|
||||
@Column
|
||||
private String userName;
|
||||
/** PENDING 待推送、SENDING 发送中、SUCCESS 推送成功、EXISTS 华住已存在、FAILED 明确失败、UNKNOWN 待核实。 */
|
||||
@Column
|
||||
private String status;
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
private String message;
|
||||
@Column
|
||||
private Long submittedAt;
|
||||
@Column
|
||||
private Long finishedAt;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.budwk.app.zhgh.huazhu.models;
|
||||
|
||||
import lombok.Data;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
/** 华住推送批次。activeSlot 唯一约束防止管理员重复提交;后台按当前单实例 Quartz 配置串行执行。 */
|
||||
@Data
|
||||
@Table("huazhu_sync_task")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4','mysql-engine':'InnoDB'}")
|
||||
@TableIndexes({@Index(name = "UK_HUAZHU_ACTIVE_SLOT", fields = {"activeSlot"}, unique = true)})
|
||||
public class HuaZhuSyncTask {
|
||||
@Name
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String id;
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String activeSlot;
|
||||
/** SELECTED 为勾选同步,RULE 为规则同步。 */
|
||||
@Column
|
||||
private String source;
|
||||
/** RUNNING 为已提交待后台处理或处理中,COMPLETED 为遍历结束,FAILED 为批次异常中止。 */
|
||||
@Column
|
||||
private String status;
|
||||
@Column
|
||||
private Integer total;
|
||||
@Column
|
||||
private Long submittedAt;
|
||||
@Column
|
||||
private Long finishedAt;
|
||||
/** 兼容旧表保留的租约字段;现由固定 Quartz Job 串行执行,不再按逐人租约领取。 */
|
||||
@Column
|
||||
private Long leaseUntil;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.budwk.app.zhgh.huazhu.param;
|
||||
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/** 推送记录分页:taskId 为空查全部批次,status 为空查全部状态,searchKeyword 匹配工号或姓名。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class HuaZhuSyncPageForm extends PageForm {
|
||||
private String taskId;
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.budwk.app.zhgh.huazhu.service;
|
||||
|
||||
import com.budwk.app.zhgh.huazhu.dto.HuaZhuAuthTokenResultDTO;
|
||||
|
||||
/** 华住认证,保留原系统 getToken 接口。 */
|
||||
public interface HuaZhuAuthService {
|
||||
/** 无参数;优先读取缓存,否则请求认证。返回 DTO,accessToken 为裸 Token,失败抛出异常。 */
|
||||
HuaZhuAuthTokenResultDTO getToken();
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package com.budwk.app.zhgh.huazhu.service;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuEmployeePageForm;
|
||||
import com.budwk.app.zhgh.huazhu.vo.HuaZhuEmployeeStatus;
|
||||
import com.budwk.app.zhgh.huazhu.vo.HuaZhuSyncResult;
|
||||
import com.budwk.app.zhgh.huazhu.models.HuaZhuSyncTask;
|
||||
import com.budwk.app.zhgh.huazhu.models.HuaZhuSyncRecord;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuSyncPageForm;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
@@ -25,9 +27,9 @@ public interface HuaZhuEmployeeService {
|
||||
* 将选中的本项目员工同步到华住。
|
||||
*
|
||||
* @param userIds 本项目用户主键列表
|
||||
* @return 每位员工的同步结果
|
||||
* @return 已创建批次,含 id、total、status 和 submittedAt;实际推送在后台进行
|
||||
*/
|
||||
List<HuaZhuSyncResult> syncUsers(List<String> userIds);
|
||||
HuaZhuSyncTask syncUsers(List<String> userIds);
|
||||
|
||||
/**
|
||||
* 获取规则同步的中文说明,无需传参;单位名称从当前单位表读取,缺失时拒绝继续同步。
|
||||
@@ -39,9 +41,33 @@ public interface HuaZhuEmployeeService {
|
||||
/**
|
||||
* 同步全部符合规则且未删除的员工,无需传参,不受页面搜索、分页或勾选范围限制。
|
||||
*
|
||||
* @return 每位员工的工号、姓名、success 成功标记和 message 结果说明;无人符合时返回空列表
|
||||
* @return 已创建批次;无人需要推送时抛出中文业务异常
|
||||
*/
|
||||
List<HuaZhuSyncResult> syncRuleUsers();
|
||||
HuaZhuSyncTask syncRuleUsers();
|
||||
|
||||
/** 无参数、无返回值;后台顺序推送一个批次,明确认证失败时刷新 Token 并重试当前人员一次,仍失败则停止批次。 */
|
||||
void runPendingTask();
|
||||
|
||||
/** 无参数;返回活动批次或 null,将上次中断的 SENDING 转待核实,禁止自动重发。 */
|
||||
HuaZhuSyncTask prepareBatch();
|
||||
|
||||
/** recordId 为记录主键、taskId 为批次主键;返回是否将待推送记录成功标为发送中。 */
|
||||
boolean markSending(String recordId, String taskId);
|
||||
|
||||
/** record 为发送结果;单独事务保存,状态不匹配或保存失败时抛出异常。 */
|
||||
void finishRecord(HuaZhuSyncRecord record);
|
||||
|
||||
/** taskId 为完成批次主键;确认无待处理人员后结束批次,无返回值。 */
|
||||
void completeBatch(String taskId);
|
||||
|
||||
/** taskId 为异常批次、reason 为中文原因;保留已完成结果,结束剩余记录及批次。 */
|
||||
void failBatch(String taskId, String reason);
|
||||
|
||||
/** taskId 可为空,返回最近/指定批次、状态计数、最近批次及后台是否运行。 */
|
||||
NutMap syncSummary(String taskId);
|
||||
|
||||
/** pageForm 提供分页及可选批次、状态、关键词;返回每人的持久化推送记录。 */
|
||||
Pagination<HuaZhuSyncRecord> pageRecords(HuaZhuSyncPageForm pageForm);
|
||||
|
||||
/**
|
||||
* 查询指定本项目员工在华住的实时状态。
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.budwk.app.zhgh.huazhu.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.constant.RedisConstant;
|
||||
import com.budwk.app.zhgh.huazhu.constant.HuaZhuConstant;
|
||||
import com.budwk.app.zhgh.huazhu.dto.HuaZhuAuthTokenResultDTO;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuAuthService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
import org.nutz.log.Log;
|
||||
import org.nutz.log.Logs;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/** 原认证方法仅增加阶段日志和响应关闭,认证参数、DTO 转换及缓存内容保持原写法。 */
|
||||
@IocBean
|
||||
public class HuaZhuAuthServiceImpl implements HuaZhuAuthService {
|
||||
private static final Log log = Logs.get();
|
||||
@Inject
|
||||
private RedisService redisService;
|
||||
|
||||
/** 无参数;返回原 DTO。缓存 58 分钟,认证请求 20 秒超时,日志不输出认证头或 Token。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public HuaZhuAuthTokenResultDTO getToken() {
|
||||
String step = "读取Token缓存";
|
||||
long started = System.nanoTime();
|
||||
try {
|
||||
log.info("[华住认证][步骤=读取Token缓存] 开始");
|
||||
String token = redisService.get(RedisConstant.REDIS_KEY_HUAZHU_AUTH_TOKEN);
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
step = "缓存转Token对象";
|
||||
log.info("[华住认证][步骤=缓存转Token对象] 开始,命中缓存");
|
||||
return JSONUtil.toBean(token, HuaZhuAuthTokenResultDTO.class);
|
||||
}
|
||||
step = "构建Basic认证请求";
|
||||
log.info("[华住认证][步骤=构建Basic认证请求] 开始");
|
||||
String url = String.format("%s%s", HuaZhuConstant.OAUTH2_URL, "/oauth/token?scope=ALL&grant_type=client_credentials");
|
||||
String sign = StringUtils.join("Basic ", Base64.getEncoder().encodeToString(String.format("%s:%s", HuaZhuConstant.CLIENT_ID, HuaZhuConstant.CLIENT_SECRET).getBytes(StandardCharsets.UTF_8)));
|
||||
step = "请求华住Token";
|
||||
log.info("[华住认证][步骤=请求华住Token] 开始,超时=20000ms");
|
||||
try (HttpResponse response = HttpRequest.post(url).header("Authorization", sign).timeout(20000).execute()) {
|
||||
log.infof("[华住认证][步骤=请求华住Token] 收到响应,HTTP=%d", response.getStatus());
|
||||
if (response.isOk()) {
|
||||
step = "读取并转换认证响应";
|
||||
log.info("[华住认证][步骤=读取并转换认证响应] 开始");
|
||||
String body = response.body();
|
||||
HuaZhuAuthTokenResultDTO tokenResultDTO = JSONUtil.toBean(body, HuaZhuAuthTokenResultDTO.class);
|
||||
String newToken = tokenResultDTO.getAccessToken();
|
||||
if (StrUtil.isBlank(newToken)) {
|
||||
throw new RuntimeException("请求华住认证接口失败,返回的Token为空");
|
||||
}
|
||||
step = "缓存Token对象";
|
||||
log.info("[华住认证][步骤=缓存Token对象] 开始,有效期=3480秒");
|
||||
redisService.setex(RedisConstant.REDIS_KEY_HUAZHU_AUTH_TOKEN, 60 * 58, JSONUtil.toJsonStr(tokenResultDTO));
|
||||
return tokenResultDTO;
|
||||
} else {
|
||||
throw new RuntimeException("请求华住认证接口失败,状态码:" + response.getStatus());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
// 认证异常原文可能包含响应正文,只记录类型和位置,业务层另行返回受控中文原因。
|
||||
log.errorf("[华住认证][步骤=%s] 失败,异常类型=%s,位置=%s", step, e.getClass().getName(),
|
||||
e.getStackTrace().length == 0 ? "无" : e.getStackTrace()[0]);
|
||||
throw e;
|
||||
} finally {
|
||||
log.infof("[华住认证][步骤=%s] 调用结束,耗时=%dms", step, (System.nanoTime() - started) / 1_000_000L);
|
||||
}
|
||||
}
|
||||
}
|
||||
+878
-98
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -81,10 +81,10 @@ public class CondolenceApplyController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存慰问申报截止日期,服务层会再次校验当前用户必须为系统管理员。
|
||||
* 保存慰问申报截止日期,服务层会再次校验当前用户必须为系统管理员或校工会管理员。
|
||||
*
|
||||
* @param endDate 截止日期,格式为 yyyy-MM-dd
|
||||
* @return 保存成功结果
|
||||
* @return Result,保存成功时返回成功状态和“设置成功”提示
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("保存慰问申报截止日期")
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public interface CondolenceService extends BaseService<Condolence> {
|
||||
String getApplyEndDate();
|
||||
|
||||
/**
|
||||
* 保存慰问申报截止日期,仅允许系统管理员操作。
|
||||
* 保存慰问申报截止日期,仅允许系统管理员或校工会管理员操作。
|
||||
*
|
||||
* @param endDate 截止日期,格式为 yyyy-MM-dd
|
||||
*/
|
||||
|
||||
+3
-3
@@ -90,15 +90,15 @@ public class CondolenceServiceImpl extends BaseServiceImpl<Condolence> implement
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存慰问申报截止日期。参数必须是 yyyy-MM-dd,且仅 SYSADMIN 可以修改。
|
||||
* 保存慰问申报截止日期。参数必须是 yyyy-MM-dd,仅系统管理员或校工会管理员可以修改。
|
||||
*
|
||||
* @param endDate 截止日期,格式为 yyyy-MM-dd
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveApplyEndDate(String endDate) {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员可以设置慰问申报时间");
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员或校工会管理员可以设置慰问申报时间");
|
||||
}
|
||||
parseApplyEndDate(endDate);
|
||||
Sys_config config = sysConfigService.fetch(APPLY_END_DATE_CONFIG_KEY);
|
||||
|
||||
+27
@@ -1,11 +1,14 @@
|
||||
package com.budwk.app.zhgh.staffbenefit.difficultSubsidy.controller.process;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficultSubsidy.models.DifficultSubsidyApply;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficultSubsidy.service.DifficultSubsidyService;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
@@ -36,9 +39,32 @@ public class DifficultSubsidyApplyController {
|
||||
public void form() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 无请求参数;返回 Result,data 为 yyyy-MM-dd 截止日期,空字符串表示未设置。
|
||||
* 审核修改复用本表单,因此允许申请或校工会审核权限读取。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission(value = {"difficultSubsidy.apply", "difficultSubsidy.schoolAudit"}, mode = SaMode.OR)
|
||||
public Result getApplyEndDate() {
|
||||
return Result.success().addData(difficultSubsidyService.getApplyEndDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param endDate yyyy-MM-dd 格式的申报截止日期;允许设置的角色由 service 统一校验
|
||||
* @return Result 成功状态,不附加业务数据
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("difficultSubsidy.apply")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result saveApplyEndDate(@Param("endDate") String endDate) {
|
||||
difficultSubsidyService.saveApplyEndDate(endDate);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("json")
|
||||
@SaCheckPermission("difficultSubsidy.apply")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result save(@Param("data") DifficultSubsidyApply apply) {
|
||||
return Result.success(difficultSubsidyService.saveDraft(apply));
|
||||
}
|
||||
@@ -46,6 +72,7 @@ public class DifficultSubsidyApplyController {
|
||||
@At
|
||||
@Ok("json")
|
||||
@SaCheckPermission("difficultSubsidy.apply")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result submit(@Param("data") DifficultSubsidyApply apply) {
|
||||
return Result.success(difficultSubsidyService.submit(apply));
|
||||
}
|
||||
|
||||
+17
@@ -15,6 +15,23 @@ import java.util.List;
|
||||
*/
|
||||
public interface DifficultSubsidyService extends BaseService<DifficultSubsidyApply> {
|
||||
|
||||
/**
|
||||
* @return 系统参数 DifficultSubsidyApplyEndDate 的 yyyy-MM-dd 日期,未配置时返回空字符串
|
||||
*/
|
||||
String getApplyEndDate();
|
||||
|
||||
/**
|
||||
* 仅系统管理员、校工会管理员可设置特困补助申报截止日期。
|
||||
* @param endDate yyyy-MM-dd 格式的截止日期,当天起停止新增申报
|
||||
*/
|
||||
void saveApplyEndDate(String endDate);
|
||||
|
||||
/**
|
||||
* 校验新申请是否在申报期内,数据库中已存在的申请可继续修改和重新提交。
|
||||
* @param id 申请记录ID;空值或数据库中不存在的ID均按新申请校验
|
||||
*/
|
||||
void checkApplyDeadline(String id);
|
||||
|
||||
DifficultSubsidyApply saveDraft(DifficultSubsidyApply apply);
|
||||
|
||||
DifficultSubsidyApply submit(DifficultSubsidyApply apply);
|
||||
|
||||
+140
-22
@@ -13,6 +13,7 @@ import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.engine.FlowEngine;
|
||||
import com.budwk.app.flow.entity.ProcessDefine;
|
||||
@@ -26,6 +27,8 @@ import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.flow.service.FlowCommonService;
|
||||
import com.budwk.app.flow.vo.ProcessTaskVO;
|
||||
import com.budwk.app.sys.services.SysFileService;
|
||||
import com.budwk.app.sys.services.SysConfigService;
|
||||
import com.budwk.app.sys.models.Sys_config;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.zhgh.staffbenefit.difficultSubsidy.constants.DifficultSubsidyState;
|
||||
@@ -56,6 +59,8 @@ import org.nutz.lang.util.NutMap;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
@@ -78,6 +83,12 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
private static final String PROCESS_KEY = "TKBZ";
|
||||
private static final String SCHOOL_AUDIT_TASK_NAME = "校工会审核";
|
||||
private static final String START_TASK_NAME = "startTask";
|
||||
// 沿用现有角色表中的特困补助审批员编码,供全校代申请范围判断使用。
|
||||
private static final String SCHOOL_AUDIT_ROLE_CODE = "sp04";
|
||||
private static final String APPLY_END_DATE_CONFIG_KEY = "DifficultSubsidyApplyEndDate";
|
||||
|
||||
@Inject
|
||||
private SysConfigService sysConfigService;
|
||||
|
||||
@Inject
|
||||
private FlowEngine flowEngine;
|
||||
@@ -95,9 +106,84 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/** 直接读取系统参数,保证管理员保存后下一次申请校验立即生效。 */
|
||||
@Override
|
||||
public String getApplyEndDate() {
|
||||
Sys_config config = sysConfigService.fetch(APPLY_END_DATE_CONFIG_KEY);
|
||||
return config == null ? "" : StrUtil.blankToDefault(config.getConfigValue(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存申报时间配置,页面隐藏入口之外还需在服务端校验管理角色。
|
||||
* @param endDate yyyy-MM-dd 格式的截止日期,截止当天即停止新增申报
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveApplyEndDate(String endDate) {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员和校工会管理员可以设置特困补助申报时间");
|
||||
}
|
||||
parseApplyEndDate(endDate);
|
||||
Sys_config config = sysConfigService.fetch(APPLY_END_DATE_CONFIG_KEY);
|
||||
if (config == null) {
|
||||
config = new Sys_config();
|
||||
config.setConfigKey(APPLY_END_DATE_CONFIG_KEY);
|
||||
config.setConfigValue(endDate);
|
||||
config.setNote("特困补助申请截止日期(yyyy-MM-dd)");
|
||||
sysConfigService.insert(config);
|
||||
} else {
|
||||
config.setConfigValue(endDate);
|
||||
sysConfigService.update(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截止时间仅限制新申请;已有草稿和退回记录可继续办理,不信任客户端随意传入的ID。
|
||||
* @param id 申请ID;只有数据库中真实存在的记录才免除新申请截止校验
|
||||
*/
|
||||
@Override
|
||||
public void checkApplyDeadline(String id) {
|
||||
if (Strings.isNotBlank(id) && dao().fetch(DifficultSubsidyApply.class, id) != null) {
|
||||
return;
|
||||
}
|
||||
String endDate = getApplyEndDate();
|
||||
if (Strings.isBlank(endDate)) {
|
||||
return;
|
||||
}
|
||||
// 与慰问申报保持一致:截止日期当天已关闭,不按当天 23:59:59 计算。
|
||||
if (!LocalDate.now().isBefore(parseApplyEndDate(endDate))) {
|
||||
throw new BaseException("本次申报已截止请关注下次申报");
|
||||
}
|
||||
}
|
||||
|
||||
/** 严格校验配置格式及真实日历日期,避免错误配置被当作开放申报。 */
|
||||
private LocalDate parseApplyEndDate(String endDate) {
|
||||
if (Strings.isBlank(endDate)) {
|
||||
throw new BaseException("申报截止日期不能为空");
|
||||
}
|
||||
try {
|
||||
if (!endDate.matches("\\d{4}-\\d{2}-\\d{2}")) {
|
||||
throw new DateTimeParseException("Invalid date format", endDate, 0);
|
||||
}
|
||||
return LocalDate.parse(endDate);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new BaseException("申报截止日期格式不正确,应为yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存本人或当前账号有权代填的特困补助申请,受助人身份和组织信息以人员资料为准。
|
||||
*
|
||||
* @param apply 页面表单;userId 为受助人用户ID,未选择时按当前登录人处理,其余为业务填写内容
|
||||
* @return 保存后的申请实体,applyUserId 为填报人,userId 为实际受助人
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public DifficultSubsidyApply saveDraft(DifficultSubsidyApply apply) {
|
||||
if (apply == null) {
|
||||
throw new IllegalArgumentException("申请表单不能为空");
|
||||
}
|
||||
checkApplyDeadline(apply.getId());
|
||||
fillApplicantSnapshot(apply);
|
||||
normalizeFormCollections(apply);
|
||||
if (apply.getApplyDate() == null) {
|
||||
@@ -110,21 +196,18 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
return apply;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按代申请范围搜索受助人,审批员和校级管理员可搜索全校人员,分工会管理角色限本工会。
|
||||
*
|
||||
* @param keyword 姓名或工号关键字,空值时返回当前可选范围内的前十人
|
||||
* @return 最多十条人员信息,包含id、loginname、username、birthday、mobile、sex及单位、工会ID和名称
|
||||
*/
|
||||
@Override
|
||||
public List<NutMap> listRecipients(String keyword) {
|
||||
Sql sql = Sqls.create("SELECT id, loginname, username, birthday, mobile, sex, "
|
||||
+ "unitName, unionName, unionId, unitId FROM vw_user $condition");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
|
||||
// 校工会管理员可选择全校人员,分工会管理员仅可选择本工会人员,普通用户仅可选择本人。
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(),
|
||||
RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) {
|
||||
cnd.and("unionId", "=", SecurityUtil.getUnionId());
|
||||
} else {
|
||||
cnd.and("id", "=", SecurityUtil.getUserId());
|
||||
}
|
||||
}
|
||||
appendRecipientScope(cnd);
|
||||
if (Strings.isNotBlank(keyword)) {
|
||||
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
|
||||
keywordGroup.orLike("username", keyword);
|
||||
@@ -725,23 +808,58 @@ public class DifficultSubsidyServiceImpl extends BaseServiceImpl<DifficultSubsid
|
||||
}
|
||||
|
||||
/**
|
||||
* 补全申请人快照。受助人的姓名、工号、单位等数据来自页面选择结果,不更新 sys_user。
|
||||
* 将同一套代申请范围用于人员搜索和保存校验,避免只限制下拉选项而允许越权提交受助人ID。
|
||||
*
|
||||
* @param cnd 人员查询条件;审批员和校级管理员不追加限制,分工会管理角色限本工会,其他账号限本人
|
||||
*/
|
||||
private void appendRecipientScope(Cnd cnd) {
|
||||
if (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||
SCHOOL_AUDIT_ROLE_CODE)) {
|
||||
return;
|
||||
}
|
||||
if (AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_CHAIRMAN.name(),
|
||||
RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_OPERATOR.name())) {
|
||||
// 未配置工会时仅允许本人,不能把空工会下的其他人员纳入代申请范围。
|
||||
String unionId = SecurityUtil.getUnionId();
|
||||
if (Strings.isNotBlank(unionId)) {
|
||||
cnd.and("unionId", "=", unionId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
cnd.and("id", "=", SecurityUtil.getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录当前填报人,并核实受助人确实在该账号可选择的范围内;跨部门代填时保留受助人真实组织。
|
||||
*
|
||||
* @param apply 页面表单,userId为空时默认本人;姓名、工号、性别、单位和工会字段由查询结果覆盖
|
||||
*/
|
||||
private void fillApplicantSnapshot(DifficultSubsidyApply apply) {
|
||||
if (apply == null) {
|
||||
throw new IllegalArgumentException("申请表单不能为空");
|
||||
}
|
||||
String userId = Strings.isBlank(apply.getUserId()) ? SecurityUtil.getUserId() : apply.getUserId();
|
||||
Sql sql = Sqls.create("SELECT id, loginname, username, sex, unitId, unitName, unionId, unionName "
|
||||
+ "FROM vw_user $condition");
|
||||
Cnd cnd = Cnd.where("id", "=", userId);
|
||||
appendRecipientScope(cnd);
|
||||
sql.setCondition(cnd);
|
||||
List<NutMap> recipients = listMap(sql);
|
||||
if (recipients.isEmpty()) {
|
||||
throw new IllegalArgumentException("受助人不存在或不在当前账号可代申请的范围内");
|
||||
}
|
||||
NutMap recipient = recipients.get(0);
|
||||
apply.setApplyUserId(SecurityUtil.getUserId());
|
||||
apply.setApplyUserName(SecurityUtil.getUserUsername());
|
||||
apply.setApplyLoginName(SecurityUtil.getUserLoginname());
|
||||
if (Strings.isBlank(apply.getUserId())) {
|
||||
apply.setUserId(apply.getApplyUserId());
|
||||
apply.setUserName(apply.getApplyUserName());
|
||||
apply.setLoginName(apply.getApplyLoginName());
|
||||
}
|
||||
if (Strings.isBlank(apply.getUnitId())) {
|
||||
apply.setUnitId(SecurityUtil.getUnitId());
|
||||
}
|
||||
if (Strings.isBlank(apply.getUnionId())) {
|
||||
apply.setUnionId(SecurityUtil.getUnionId());
|
||||
}
|
||||
apply.setUserId(recipient.getString("id"));
|
||||
apply.setUserName(recipient.getString("username"));
|
||||
apply.setLoginName(recipient.getString("loginname"));
|
||||
apply.setSex(recipient.getString("sex"));
|
||||
apply.setUnitId(recipient.getString("unitId"));
|
||||
apply.setUnitName(recipient.getString("unitName"));
|
||||
apply.setUnionId(recipient.getString("unionId"));
|
||||
apply.setUnionName(recipient.getString("unionName"));
|
||||
}
|
||||
|
||||
private void normalizeFormCollections(DifficultSubsidyApply apply) {
|
||||
|
||||
@@ -8,31 +8,27 @@
|
||||
)
|
||||
})(navigator.userAgent || navigator.vendor || window.opera)
|
||||
|
||||
// jQuery 回调的 this 不是 Vue 实例,使用项目已有的 Element 消息入口。
|
||||
// 每个请求最多提示一次;保留响应正文,页面 then/always 继续执行并关闭 loading。
|
||||
$.ajaxSetup({
|
||||
contentType: "application/x-www-form-urlencoded;charset=utf-8",
|
||||
success: function (response, textStatus, jqXHR) {
|
||||
if (response.code !== 0) {
|
||||
this.$message.error(response.msg)
|
||||
} else {
|
||||
return response
|
||||
success: (response, textStatus, jqXHR) => {
|
||||
if (response && typeof response === "object" && response.code !== undefined && response.code !== 0) {
|
||||
ELEMENT.Message.error(response.msg || "请求处理失败,请稍后重试");
|
||||
jqXHR.platformErrorShown = true;
|
||||
}
|
||||
},
|
||||
error: function (v1) {
|
||||
this.$message.error(v1.status + " " + v1.statusText)
|
||||
error: (xhr) => {
|
||||
const message = xhr.responseJSON && xhr.responseJSON.msg;
|
||||
ELEMENT.Message.error(message || (xhr.status === 0 ? "网络连接失败或请求超时,请稍后重试" : "请求失败,状态码:" + xhr.status));
|
||||
xhr.platformErrorShown = true;
|
||||
},
|
||||
complete: function (XMLHttpRequest, textStatus) {
|
||||
var sessionstatus = XMLHttpRequest.getResponseHeader("loginStatus") //通过XMLHttpRequest取得响应头,sessionstatus,
|
||||
if (sessionstatus == "accessDenied") {
|
||||
this.$message({
|
||||
message: "\u767b\u5f55\u5931\u6548\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u91cd\u65b0\u767b\u5f55",
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
if (sessionstatus == "unauthorized") {
|
||||
this.$message({
|
||||
message: "\u6ca1\u6709\u6743\u9650",
|
||||
type: "error"
|
||||
})
|
||||
complete: (xhr) => {
|
||||
const sessionStatus = xhr.getResponseHeader("loginStatus");
|
||||
if (!xhr.platformErrorShown && sessionStatus === "accessDenied") {
|
||||
ELEMENT.Message.error("登录失效,请刷新页面重新登录");
|
||||
} else if (!xhr.platformErrorShown && sessionStatus === "unauthorized") {
|
||||
ELEMENT.Message.error("没有权限");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,13 +69,17 @@ const initTableMixins = {
|
||||
},
|
||||
pageData(data = null) {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
this.tableLoading = false
|
||||
this.$set(this, "tableLoading", true)
|
||||
// 业务错误会由公共拦截器 reject,同样需要 finally 解除列表加载状态。
|
||||
return this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
this.$set(this, "tableData", res.data.list)
|
||||
this.$set(this.pageForm, "totalCount", res.data.totalCount)
|
||||
}
|
||||
}).catch(() => {
|
||||
// 中文错误已由统一响应拦截器提示,避免页面重复弹出。
|
||||
}).finally(() => {
|
||||
this.$set(this, "tableLoading", false)
|
||||
})
|
||||
},
|
||||
notifySuccess(msg) {
|
||||
|
||||
@@ -175,6 +175,12 @@ module.exports = {
|
||||
default: "",
|
||||
required: false
|
||||
},
|
||||
// 默认仅由文件选择器按 accept 过滤;启用后上传前再次校验扩展名,accept 需传 .pdf,.jpg 等白名单。
|
||||
validate_accept: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
// 是否是完整的结果(就是文件上传返回什么,该组件返回什么,uploadResultCategory必须为array)
|
||||
complete_result: {
|
||||
type: Boolean,
|
||||
@@ -316,7 +322,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
// 这是兜底逻辑,保准只让这个image类型上传图片
|
||||
// 保留原有大小限制;业务显式开启 validate_accept 时,额外按扩展名拦截不符合 accept 的文件。
|
||||
beforeUpload(file) {
|
||||
|
||||
// 需要兼容部分业务不限制附件大小,关闭后仅跳过大小校验,不影响数量和格式控制。
|
||||
@@ -324,35 +330,17 @@ module.exports = {
|
||||
this.$message.error("文件过大,请上传小于" + (this.upload_size / 1024 / 1024).toFixed(0) + "M的文件!")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (this.validate_accept && this.fileAccept) {
|
||||
const acceptTypes = this.fileAccept.split(",").map((type) => type.trim().toLowerCase())
|
||||
const fileName = (file.name || "").toLowerCase()
|
||||
const dotIndex = fileName.lastIndexOf(".")
|
||||
// 文件名可能没有扩展名或被伪装为“申请表.pdf.docx”,始终只校验最后一个扩展名。
|
||||
if (dotIndex < 0 || !acceptTypes.includes(fileName.substring(dotIndex))) {
|
||||
this.$message.error("文件只能是 " + this.fileAccept + " 格式!")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
// if (!this.fileAccept || typeof this.fileAccept !== "string") {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// // 去除前后空格并转换为数组
|
||||
// const acceptTypes = this.fileAccept
|
||||
// .trim()
|
||||
// .split(",")
|
||||
// .map((type) => type.trim().toUpperCase())
|
||||
//
|
||||
// let fileType
|
||||
// const dotIndex = file.name.lastIndexOf(".")
|
||||
// if (dotIndex !== -1) {
|
||||
// fileType = file.name.substring(dotIndex).toUpperCase()
|
||||
// } else {
|
||||
// this.$message.error("文件必须包含有效的扩展名!")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// const valid = acceptTypes.includes(fileType)
|
||||
// if (valid) {
|
||||
// return true
|
||||
// }
|
||||
// this.$message.error(`文件只能是 ${this.fileAccept} 格式!`)
|
||||
// return false
|
||||
},
|
||||
|
||||
// 预览图片
|
||||
|
||||
@@ -33,11 +33,16 @@ const applyForm = {
|
||||
<el-input readonly v-model="formData.sex"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!--<el-col :span="24">
|
||||
<el-form-item label="联系方式" prop="mobile">
|
||||
<el-input v-model="formData.mobile"></el-input>
|
||||
<el-col :span="24">
|
||||
<!-- 手机号必填,格式与后端保持一致:1开头、第二位3至9,共11位数字。 -->
|
||||
<el-form-item label="手机号" prop="mobile"
|
||||
:rules="[
|
||||
{ required: true, whitespace: true, message: '请填写手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9][0-9]{9}$/, message: '请输入正确的11位手机号', trigger: 'blur' }
|
||||
]">
|
||||
<el-input v-model="formData.mobile" maxlength="32" placeholder="请填写手机号"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>-->
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-if="courseRow.courseIsLimitApply">
|
||||
@@ -103,8 +108,8 @@ const applyForm = {
|
||||
</el-form>
|
||||
|
||||
<el-row class="mt20" justify="end" type="flex">
|
||||
<el-button @click="signDialog = false">取 消</el-button>
|
||||
<el-button @click="onSubmit" type="primary">提 交</el-button>
|
||||
<el-button @click="$set($data, 'signDialog', false)" :disabled="formLoading">取 消</el-button>
|
||||
<el-button @click="onSubmit" type="primary" :loading="formLoading" :disabled="formLoading">提 交</el-button>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -114,60 +119,81 @@ const applyForm = {
|
||||
data() {
|
||||
return {
|
||||
signDialog: false,
|
||||
formLoading: false,
|
||||
formData: {},
|
||||
mobileColumn: null,
|
||||
courseRow: {},
|
||||
courseTimeSelectList: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getCourseTimeSelectList(o) {
|
||||
const resp = await $.post('/platform/trainSignUp/apply/getCourseTimeSelectList',{ courseId: o.id })
|
||||
if (resp.code === 0) {
|
||||
this.courseTimeSelectList = resp.data
|
||||
} else {
|
||||
this.$message.warning('获取时段信息失败,请联系管理员')
|
||||
}
|
||||
},
|
||||
async onOpen(row, course) {
|
||||
this.initData(row, course)
|
||||
if (row.courseIsLimitApply) {
|
||||
await this.getCourseTimeSelectList(row)
|
||||
}
|
||||
this.courseRow = row
|
||||
this.signDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form.clearValidate()
|
||||
/**
|
||||
* o.id 为当前课程 ID;接口返回 Result,data 为含 value(时段 ID)、text(时段描述)的选项数组。
|
||||
*/
|
||||
getCourseTimeSelectList(o) {
|
||||
this.formLoading = true
|
||||
return $.post('/platform/trainSignUp/apply/getCourseTimeSelectList', { courseId: o.id }).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.courseTimeSelectList = resp.data
|
||||
} else {
|
||||
this.$message.warning('获取时段信息失败,请联系管理员')
|
||||
}
|
||||
}).always(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
},
|
||||
async validateSignUp() {
|
||||
// 获取家属人数
|
||||
// row 为课程记录,course 为课程类型及其动态字段配置;限额课程返回时段加载完成后的 Promise。
|
||||
onOpen(row, course) {
|
||||
this.initData(row, course)
|
||||
const openForm = () => {
|
||||
this.courseRow = row
|
||||
this.signDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form.clearValidate()
|
||||
})
|
||||
}
|
||||
// 限额课程先取得可报名时段,再展示表单,保持原有打开顺序。
|
||||
if (row.courseIsLimitApply) return this.getCourseTimeSelectList(row).then(openForm)
|
||||
openForm()
|
||||
},
|
||||
/**
|
||||
* 传入 courseId(课程 ID)、currentFamilyNumber(动态表单填写的家属总人数),返回报名资格是否通过的 Promise<boolean>。
|
||||
*/
|
||||
validateSignUp() {
|
||||
// 家属人数继续取动态字段 xdqsrs,空值或非数值按 0 参与报名人数校验。
|
||||
let familyCount = this.formData.trainMobileSignColumnList.filter(o => o.columnCode === 'xdqsrs').reduce((sum, item) => {
|
||||
return sum + (Number(item.columnValue) || 0)
|
||||
}, 0)
|
||||
const res = await this.$axios.post("/platform/trainSignUp/apply/validateSignUp", {
|
||||
return this.$axios.post("/platform/trainSignUp/apply/validateSignUp", {
|
||||
courseId: this.formData.courseId,
|
||||
currentFamilyNumber: familyCount
|
||||
})
|
||||
return res.code === 0
|
||||
}).then((res) => res.code === 0)
|
||||
},
|
||||
async validateCourseTime() {
|
||||
const res = await this.$axios.post('/platform/trainSignUp/apply/validateSourceSignUp', {
|
||||
/**
|
||||
* 传入 activityCourseId(所选报名时段 ID)、courseId(课程 ID),返回时段名额校验是否通过的 Promise<boolean>。
|
||||
*/
|
||||
validateCourseTime() {
|
||||
return this.$axios.post('/platform/trainSignUp/apply/validateSourceSignUp', {
|
||||
activityCourseId: this.formData.activityCourseId,
|
||||
courseId: this.courseRow.id
|
||||
})
|
||||
return res.code === 0
|
||||
}).then((res) => res.code === 0)
|
||||
},
|
||||
async onSubmit() {
|
||||
if(!await this.validateSignUp()) return
|
||||
|
||||
this.$refs["form"].validate(async (valid) => {
|
||||
if (valid) {
|
||||
if(this.courseRow.courseIsLimitApply && !await this.validateCourseTime()) return
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
// 返回完整校验和报名提交链路的 Promise;正在处理时直接返回,提交按钮同步显示加载状态。
|
||||
onSubmit() {
|
||||
// 锁定资格校验至提交完成的整个链路,避免重复点击产生多次报名请求。
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
return this.validateSignUp().then((valid) => {
|
||||
if (!valid) return
|
||||
return this.$refs.form.validate().then(() => {
|
||||
return this.courseRow.courseIsLimitApply ? this.validateCourseTime() : true
|
||||
}).then((validTime) => {
|
||||
if (!validTime) return
|
||||
return this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
}).then(() => {
|
||||
const mobileColumnsValue = this.formData.trainMobileSignColumnList.map((v) => {
|
||||
return {
|
||||
columnName: v.columnName,
|
||||
@@ -176,28 +202,51 @@ const applyForm = {
|
||||
columnFormType: v.columnFormType
|
||||
}
|
||||
})
|
||||
this.formData.mobileColumnsValue = JSON.stringify(mobileColumnsValue)
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/apply/doSignUp", this.formData)
|
||||
if (resp.code === 0) {
|
||||
this.signDialog = false
|
||||
this.$message.success(resp.msg)
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
// 已配置 mobile 动态字段时同步最新手机号,兼容按动态字段读取数据的原有导出。
|
||||
if (this.mobileColumn) {
|
||||
mobileColumnsValue.push({
|
||||
columnName: this.mobileColumn.columnName,
|
||||
columnValue: this.formData.mobile,
|
||||
columnCode: this.mobileColumn.columnCode,
|
||||
columnFormType: this.mobileColumn.columnFormType
|
||||
})
|
||||
}
|
||||
this.$set(this.formData, 'mobileColumnsValue', JSON.stringify(mobileColumnsValue))
|
||||
// activityId、courseId、activityCourseId 分别标识活动、课程和所选时段;mobile 为本次报名联系方式。
|
||||
// mobileColumnsValue 为含字段名、编码、控件类型及值的 JSON 数组,其他基础信息保持原表单内容。
|
||||
// 接口返回 Result,code 为 0 表示报名成功,msg 用于结果提示。
|
||||
return this.$axios.post("/platform/trainSignUp/apply/doSignUp", this.formData).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.signDialog = false
|
||||
this.$message.success(resp.msg)
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
// 取消确认或表单校验不通过时保留当前填写内容;网络错误由公共请求拦截器提示。
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
},
|
||||
// row 提供活动、课程 ID,course 提供动态字段;仅准备本次报名表单及手机号元数据。
|
||||
initData(row, course) {
|
||||
this.$set(this.formData, 'username', this.$store.state.user.username)
|
||||
this.$set(this.formData, 'loginname', this.$store.state.user.loginname)
|
||||
this.$set(this.formData, 'unionName', this.$store.state.user.union.name)
|
||||
this.$set(this.formData, 'unitName', this.$store.state.user.unit.name)
|
||||
this.$set(this.formData, 'sex', this.$store.state.user.sex)
|
||||
// 默认取登录用户手机号,可编辑且只随本次报名提交,不修改个人资料。
|
||||
this.$set(this.formData, 'mobile', this.$store.state.user.mobile || '')
|
||||
this.$set(this.formData, 'activityId', row.activityId)
|
||||
this.$set(this.formData, 'courseId', row.id)
|
||||
this.$set(this.formData, 'trainMobileSignColumnList', course.trainMobileSignColumnList)
|
||||
// 复制配置以免修改课程类型原数据;手机号统一必填,不受动态 isRequired 影响,保留 mobile 元数据兼容同步提交。
|
||||
const columns = clone(course.trainMobileSignColumnList)
|
||||
this.mobileColumn = columns.find((item) => item.columnCode === 'mobile') || null
|
||||
this.$set(this.formData, 'trainMobileSignColumnList', columns.filter((item) => item.columnCode !== 'mobile'))
|
||||
|
||||
this.formData.trainMobileSignColumnList.forEach((item) => {
|
||||
item.columnValue = this.$store.state.user[item.columnCode] || ""
|
||||
|
||||
@@ -96,8 +96,8 @@ const basicForm = {
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="活动类型" prop="trainType" :rules="{required:true,message: '请选择活动类型', trigger: 'blur'}">
|
||||
<el-select @change="typeChange" filterable placeholder="请选择活动类型" style="width: 100%" v-model="formData.trainType">
|
||||
<el-option :label="item.name" :value="item.code" :key="item.code" v-for="item in dict.type.TRAIN_SIGNUP_TYPE"></el-option>
|
||||
<el-select @change="typeChange" @visible-change="(visible) => visible && loadDictOptions('TRAIN_SIGNUP_TYPE')" :loading="dictOptionsLoading.TRAIN_SIGNUP_TYPE" filterable placeholder="请选择活动类型" style="width: 100%" v-model="formData.trainType">
|
||||
<el-option :label="item.name" :value="item.code" :key="item.code" v-for="item in trainTypeList"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -221,8 +221,8 @@ const basicForm = {
|
||||
|
||||
<el-table-column label="校区" prop="campus">
|
||||
<template v-slot="{row}">
|
||||
<el-select size="small" v-model="row.campus" style="width: 100%">
|
||||
<el-option :label="item.name" :value="item.name" :key="item.code" v-for="item in dict.type.USER_CAMPUS"></el-option>
|
||||
<el-select size="small" v-model="row.campus" style="width: 100%" :loading="dictOptionsLoading.USER_CAMPUS" @visible-change="(visible) => visible && loadDictOptions('USER_CAMPUS')">
|
||||
<el-option :label="item.name" :value="item.name" :key="item.code" v-for="item in campusList"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -324,7 +324,6 @@ const basicForm = {
|
||||
<custom-form ref="customFormRef"></custom-form>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["USER_CAMPUS", "TRAIN_SIGNUP_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
signUpDialog: false,
|
||||
@@ -356,6 +355,8 @@ const basicForm = {
|
||||
}
|
||||
},
|
||||
campusList: [],
|
||||
dictOptionsLoading: { USER_CAMPUS: false, TRAIN_SIGNUP_TYPE: false },
|
||||
dictOptionsRequests: { USER_CAMPUS: null, TRAIN_SIGNUP_TYPE: null },
|
||||
courseTypeList: [],
|
||||
undertakeOptions: [],
|
||||
}
|
||||
@@ -523,8 +524,56 @@ const basicForm = {
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 查询本表单的下拉字典。code 仅传 USER_CAMPUS(校区)或 TRAIN_SIGNUP_TYPE(活动类型)。
|
||||
* 接口返回 {code: 0, data: [{name, code}]};name 为显示名称,校区保存 name,活动类型保存 code。
|
||||
* 返回 Promise<Array>,失败时返回空数组;共用进行中的请求,失败或无选项时允许重新展开下拉框重试。
|
||||
*/
|
||||
loadDictOptions(code) {
|
||||
const options = {
|
||||
USER_CAMPUS: { field: "campusList", label: "校区" },
|
||||
TRAIN_SIGNUP_TYPE: { field: "trainTypeList", label: "活动类型" }
|
||||
}[code]
|
||||
if (!options) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
if (this.dictOptionsRequests[code]) {
|
||||
return this.dictOptionsRequests[code]
|
||||
}
|
||||
if (this[options.field].length > 0) {
|
||||
return Promise.resolve(this[options.field])
|
||||
}
|
||||
this.$set(this.dictOptionsLoading, code, true)
|
||||
const request = this.$axios.post("/open/common/dictOptions", { code })
|
||||
.then((res) => {
|
||||
if (!res || res.code !== 0 || !Array.isArray(res.data)) {
|
||||
this.$message.warning(options.label + "选项加载失败,请重新展开下拉框重试")
|
||||
return []
|
||||
}
|
||||
this.$set(this, options.field, res.data)
|
||||
if (res.data.length === 0) {
|
||||
this.$message.warning(options.label + "暂无可用选项,请检查字典配置")
|
||||
}
|
||||
// 重试成功后按当前表单值更新业务名称,避免早于字典返回的回显停留在默认名称。
|
||||
if (code === "TRAIN_SIGNUP_TYPE") {
|
||||
this.typeChange(this.formData.trainType)
|
||||
}
|
||||
return res.data
|
||||
})
|
||||
.catch(() => {
|
||||
// 网络及接口异常由全局 axios 拦截器提示;保留原表单值,允许下次展开时重试。
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this.dictOptionsLoading, code, false)
|
||||
this.$set(this.dictOptionsRequests, code, null)
|
||||
})
|
||||
this.$set(this.dictOptionsRequests, code, request)
|
||||
return request
|
||||
},
|
||||
// val 为活动类型字典编码;按已查询的选项更新名称,未匹配时沿用原有默认名称。
|
||||
typeChange(val) {
|
||||
const type = this.dict.type.TRAIN_SIGNUP_TYPE.find(o => o.code === val)
|
||||
const type = this.trainTypeList.find(o => o.code === val)
|
||||
this.trainType = type?.name || "培训班"
|
||||
},
|
||||
changeActivity(val) {
|
||||
@@ -669,13 +718,25 @@ const basicForm = {
|
||||
}
|
||||
}
|
||||
},
|
||||
async initData(row, copyTemplateMode = false) {
|
||||
// row 为待编辑活动(新建时不传),copyTemplateMode 标识模板复制;返回完成字典及表单回显的 Promise。
|
||||
initData(row, copyTemplateMode = false) {
|
||||
this.copyTemplateMode = copyTemplateMode
|
||||
this.activityGroupList = await this.getActivityGroup()
|
||||
this.historicalActList = await this.getHistoricalActList()
|
||||
this.courseTypeList = await this.getAllType()
|
||||
await this.selectUnitAndClub()
|
||||
await this.init(row)
|
||||
// 两类字典查询完成后再初始化表单,避免活动类型回显与字典加载发生竞争。
|
||||
return Promise.all([this.loadDictOptions("USER_CAMPUS"), this.loadDictOptions("TRAIN_SIGNUP_TYPE")])
|
||||
.then(() => this.getActivityGroup())
|
||||
.then((data) => {
|
||||
this.activityGroupList = data
|
||||
return this.getHistoricalActList()
|
||||
})
|
||||
.then((data) => {
|
||||
this.historicalActList = data
|
||||
return this.getAllType()
|
||||
})
|
||||
.then((data) => {
|
||||
this.courseTypeList = data
|
||||
return this.selectUnitAndClub()
|
||||
})
|
||||
.then(() => this.init(row))
|
||||
},
|
||||
async selectUnitAndClub() {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/selectUnitAndClub")
|
||||
|
||||
@@ -1,6 +1,79 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style nonce="${cspNonce!}">
|
||||
.huazhu-sync-dialog { max-width: calc(100vw - 48px); max-height: 88vh; display: flex; flex-direction: column; border-radius: 8px; }
|
||||
.huazhu-sync-dialog .el-dialog__header { padding: 24px 24px 0; border-bottom: 0; }
|
||||
.huazhu-sync-header-copy { flex: 1; min-width: 0; }
|
||||
.huazhu-sync-dialog .el-dialog__headerbtn { align-self: flex-start; margin-left: 16px; }
|
||||
.huazhu-sync-dialog .el-dialog__body { padding: 0 24px; overflow-y: auto; min-height: 0; font-size: 14px; }
|
||||
.huazhu-sync-dialog .el-button--small { padding: 9px 16px; font-size: 13px; border-radius: 4px; }
|
||||
.huazhu-sync-dialog .el-input--small .el-input__inner { height: 36px; line-height: 36px; font-size: 14px; border-radius: 4px; }
|
||||
.huazhu-sync-dialog .el-input--small .el-input__icon { line-height: 36px; }
|
||||
.huazhu-sync-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
|
||||
.huazhu-sync-title { margin: 0 0 6px; color: #182333; font-size: 20px; font-weight: 600; line-height: 28px; }
|
||||
.huazhu-sync-subtitle { margin: 0; color: #8a929f; font-size: 14px; line-height: 22px; }
|
||||
.huazhu-sync-tabs > .el-tabs__header { margin: 16px 0 22px; }
|
||||
.huazhu-sync-tabs .el-tabs__item { padding: 0 22px; font-size: 14px; }
|
||||
.huazhu-sync-tabs .el-tabs__nav-wrap::after { height: 1px; background: #ebeef5; }
|
||||
.huazhu-sync-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; text-align: left; }
|
||||
.huazhu-sync-note { color: #8a929f; font-size: 12px; line-height: 20px; }
|
||||
.huazhu-sync-note .el-icon-info { margin-right: 6px; color: #006bb8; }
|
||||
.huazhu-sync-batch { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
|
||||
.huazhu-sync-batch-label { color: #303b4a; font-weight: 600; white-space: nowrap; }
|
||||
.huazhu-sync-batch .el-select { width: 330px; max-width: 100%; }
|
||||
.huazhu-sync-batch .el-button { margin-left: auto; }
|
||||
.huazhu-sync-batch .el-button--primary.is-plain { color: #006bb8; background: #fff; border-color: #006bb8; }
|
||||
.huazhu-sync-batch .el-button--primary.is-plain:hover { background: #f1f7fc; }
|
||||
.huazhu-sync-stats { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); padding: 18px 0; margin-bottom: 14px; background: #f6f8fc; border-radius: 4px; }
|
||||
.huazhu-sync-stat { text-align: center; }
|
||||
.huazhu-sync-stat + .huazhu-sync-stat { border-left: 1px solid #dde3ed; }
|
||||
.huazhu-sync-stat strong { display: block; color: #182333; font-size: 28px; font-weight: 600; line-height: 34px; }
|
||||
.huazhu-sync-stat span { display: block; margin-top: 5px; color: #606b7b; font-size: 13px; }
|
||||
.huazhu-sync-stat.success strong, .huazhu-result-success { color: #32953d; }
|
||||
.huazhu-sync-stat.exists strong, .huazhu-result-exists { color: #009fb0; }
|
||||
.huazhu-sync-stat.failed strong, .huazhu-result-failed { color: #e3283b; }
|
||||
.huazhu-sync-stat.unknown strong, .huazhu-result-unknown { color: #ed8a00; }
|
||||
.huazhu-sync-stat.pending strong, .huazhu-result-pending, .huazhu-result-sending { color: #006bb8; }
|
||||
.huazhu-sync-progress { display: flex; align-items: center; gap: 16px; color: #606b7b; font-size: 13px; margin-bottom: 24px; }
|
||||
.huazhu-sync-progress .el-progress { flex: 1; min-width: 100px; }
|
||||
.huazhu-sync-progress .el-progress-bar__outer { background: #e0e5ec; }
|
||||
.huazhu-sync-updated { margin-left: auto; color: #8a929f; white-space: nowrap; }
|
||||
.huazhu-sync-filter { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
|
||||
.huazhu-sync-filter > label { color: #303b4a; font-size: 13px; }
|
||||
.huazhu-sync-filter .el-input { width: 260px; }
|
||||
.huazhu-sync-filter .el-select { width: 170px; }
|
||||
.huazhu-sync-filter .el-select .el-input { width: 100%; }
|
||||
.huazhu-sync-filter .filter-status { margin-left: 12px; }
|
||||
.huazhu-sync-record-table .el-table__header th { background: #f4f6fa; color: #303b4a; font-weight: 600; }
|
||||
.huazhu-sync-record-table { font-size: 14px; }
|
||||
.huazhu-sync-record-table .el-table__cell { padding: 7px 0; }
|
||||
.huazhu-sync-record-table .el-button--text { padding: 3px 0; }
|
||||
.huazhu-sync-record-table .result-icon { margin-right: 5px; }
|
||||
.huazhu-sync-records .el-pagination-container { margin: 16px 0 14px !important; text-align: right; }
|
||||
.huazhu-sync-empty { padding: 18px 20px; margin-bottom: 18px; color: #9099a6; background: #f6f8fc; border-radius: 4px; font-size: 13px; line-height: 22px; }
|
||||
.huazhu-sync-submit { padding: 2px 0 24px; }
|
||||
.huazhu-sync-submit-note { margin-bottom: 22px; }
|
||||
.huazhu-sync-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; padding: 12px 0 24px; }
|
||||
.huazhu-sync-option + .huazhu-sync-option { padding-left: 32px; border-left: 1px solid #ebeef5; }
|
||||
.huazhu-sync-option h3 { margin: 0 0 12px; color: #303b4a; font-size: 16px; font-weight: 600; }
|
||||
.huazhu-sync-option h3 i { margin-right: 8px; color: #006bb8; }
|
||||
.huazhu-sync-option p { margin: 0 0 16px; min-height: 46px; color: #7a8595; font-size: 13px; line-height: 23px; }
|
||||
.huazhu-sync-option .el-tag { margin: 0 6px 8px 0; }
|
||||
.huazhu-sync-selected { min-height: 42px; margin-bottom: 14px; }
|
||||
@media (max-width: 900px) {
|
||||
.huazhu-sync-dialog { max-width: calc(100vw - 24px); }
|
||||
.huazhu-sync-batch, .huazhu-sync-progress { flex-wrap: wrap; }
|
||||
.huazhu-sync-batch .el-select { flex: 1; min-width: 180px; }
|
||||
.huazhu-sync-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 20px; }
|
||||
.huazhu-sync-stat:nth-child(4) { border-left: 0; }
|
||||
.huazhu-sync-stat strong { font-size: 24px; }
|
||||
.huazhu-sync-progress .el-progress { min-width: 130px; }
|
||||
.huazhu-sync-updated { width: 100%; text-align: right; }
|
||||
.huazhu-sync-options { grid-template-columns: 1fr; gap: 24px; }
|
||||
.huazhu-sync-option + .huazhu-sync-option { padding: 24px 0 0; border-left: 0; border-top: 1px solid #ebeef5; }
|
||||
}
|
||||
</style>
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
@@ -12,9 +85,8 @@ layout("/layouts/platform.html"){
|
||||
</search>
|
||||
</el-card>
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="华住员工管理">
|
||||
<el-button :disabled="syncLoading || tableLoading" @click="syncSelected" icon="el-icon-refresh" size="small" type="primary">同步选中员工</el-button>
|
||||
<el-button :disabled="tableLoading" :loading="syncLoading" @click="syncRuleUsers" icon="el-icon-refresh" size="small" type="primary">同步规则内人员</el-button>
|
||||
<table-tool :app="this" label="待推送员工(已推送人员自动隐藏)">
|
||||
<el-button @click="openSyncManage" icon="el-icon-refresh" size="small" type="primary">同步管理</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" :size="tableSize" @selection-change="selectionChange" class="vi-table" v-loading="tableLoading">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
@@ -23,12 +95,57 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column label="姓名" min-width="120" prop="username"></el-table-column>
|
||||
<el-table-column label="单位" min-width="180" prop="unitName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="130">
|
||||
<template slot-scope="{row}"><el-button @click="queryStatus(row)" size="mini" type="primary">查询状态</el-button></template>
|
||||
<template slot-scope="{row}"><el-button :loading="queryingUserId === row.id" :disabled="!!queryingUserId" @click="queryStatus(row)" size="mini" type="primary">查询状态</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-dialog :visible.sync="statusDialogVisible" title="华住同步状态" width="520px">
|
||||
<el-dialog :visible.sync="syncDialogVisible" custom-class="huazhu-sync-dialog" width="1080px" top="8vh" :close-on-click-modal="false">
|
||||
<template slot="title">
|
||||
<div class="huazhu-sync-header-copy">
|
||||
<h2 class="huazhu-sync-title">华住同步管理</h2>
|
||||
<p class="huazhu-sync-subtitle">后台逐人推送,提交后可离开页面,稍后查看结果</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-tabs :value="syncTab" @tab-click="changeSyncTab" class="huazhu-sync-tabs">
|
||||
<el-tab-pane label="发起推送" name="submit">
|
||||
<div class="huazhu-sync-submit">
|
||||
<el-alert class="huazhu-sync-submit-note" :closable="false" show-icon :type="summary.running ? 'warning' : 'info'"
|
||||
:title="summary.running ? '已有未结束任务,可前往进度与记录查看后台状态;任务结束后可再次提交。' : '同步过程中时间略长,请稍后再试登录华住会。已推送、已排队和待核实的人员不会重复提交。'"></el-alert>
|
||||
<div class="huazhu-sync-options">
|
||||
<section class="huazhu-sync-option">
|
||||
<h3><i class="el-icon-user"></i>同步选中员工</h3>
|
||||
<p>将主页面已勾选的 {{selectedRows.length}} 名员工加入后台任务。需要调整人员时,关闭弹框后重新勾选。</p>
|
||||
<div class="huazhu-sync-selected">
|
||||
<el-tag v-for="row in selectedRows.slice(0, 6)" :key="row.id" size="small" type="info">{{row.username || row.loginname}}</el-tag>
|
||||
<el-tag v-if="selectedRows.length > 6" size="small" type="info">等 {{selectedRows.length}} 人</el-tag>
|
||||
<span v-if="!selectedRows.length" class="huazhu-sync-note">暂未勾选员工,请先在主页面选择。</span>
|
||||
</div>
|
||||
<el-button :disabled="!selectedRows.length || summary.running || summaryLoading" :loading="syncLoading" @click="syncSelected" type="primary" size="small">推送选中员工</el-button>
|
||||
</section>
|
||||
<section class="huazhu-sync-option">
|
||||
<h3><i class="el-icon-office-building"></i>同步规则内人员</h3>
|
||||
<p v-loading="ruleDescriptionLoading">{{ruleDescription || '读取同步范围后,可将全部符合规则的待推送人员加入后台任务。'}}</p>
|
||||
<div class="huazhu-sync-selected huazhu-sync-note">本操作不受当前搜索、分页和勾选范围影响。</div>
|
||||
<el-button :disabled="summary.running || summaryLoading || ruleDescriptionLoading || !ruleDescription" :loading="syncLoading" @click="syncRuleUsers" type="primary" size="small">推送规则内人员</el-button>
|
||||
<el-button v-if="!ruleDescription && !ruleDescriptionLoading" @click="loadRuleDescription" type="text" size="small">重新读取规则</el-button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="进度与记录" name="records">
|
||||
<hua-zhu-sync-info ref="syncInfo" :key="selectedTaskId" :summary="summary" :summary-loading="summaryLoading" :updated-at="summaryUpdatedAt"
|
||||
:querying-user-id="queryingUserId" @query-status="queryStatus" @task-change="changeSummaryTask" @refresh="refreshResults" v-if="syncDialogVisible"></hua-zhu-sync-info>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template slot="footer">
|
||||
<div class="huazhu-sync-footer">
|
||||
<span class="huazhu-sync-note"><i class="el-icon-info"></i>更新只查看结果,不会重复推送;已推送人员自动从待推送列表隐藏。</span>
|
||||
<el-button @click="closeSyncManage" size="small">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog append-to-body :visible.sync="statusDialogVisible" title="华住同步状态" width="520px">
|
||||
<el-descriptions :column="1" border size="small" v-if="statusData">
|
||||
<el-descriptions-item label="状态码">{{statusData.statusCode || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{statusData.userNumber || "--"}}</el-descriptions-item>
|
||||
@@ -44,82 +161,132 @@ layout("/layouts/platform.html"){
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include("./info.js"){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() { return {selectedRows: [], syncLoading: false, statusDialogVisible: false, statusData: null} },
|
||||
components: {HuaZhuSyncInfo: huaZhuSyncInfo},
|
||||
data() {
|
||||
return {
|
||||
selectedRows: [], syncLoading: false, summaryLoading: false, summaryRequestId: 0,
|
||||
summary: {task: null, counts: {}, tasks: [], running: false}, summaryUpdatedAt: null,
|
||||
selectedTaskId: "", syncDialogVisible: false, syncTab: "records",
|
||||
ruleDescription: "", ruleDescriptionLoading: false,
|
||||
queryingUserId: "", statusDialogVisible: false, statusData: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectionChange(rows) { this.$set(this, "selectedRows", rows) },
|
||||
// 确认期间锁定同步入口,并固定本次勾选范围,防止重复点击或勾选变化影响提交。
|
||||
syncSelected() {
|
||||
if (this.syncLoading || this.tableLoading) { return }
|
||||
if (!this.selectedRows.length) { this.$message.warning("请先选择需要同步的员工"); return }
|
||||
const userIds = this.selectedRows.map((row) => row.id);
|
||||
this.$set(this, "syncLoading", true);
|
||||
this.confirmSync("确定同步选中的 " + userIds.length + " 名员工到华住吗?", "/syncUsers", {userIds: userIds});
|
||||
// 主页面只提供一个入口;每次打开读取最新批次,关闭不会影响后台任务。
|
||||
openSyncManage() {
|
||||
this.$set(this, "syncDialogVisible", true);
|
||||
this.$set(this, "syncTab", "records");
|
||||
this.$nextTick(() => { this.loadSummary("", true) });
|
||||
},
|
||||
// 先读取包含实际单位名称的规则;只有确认后才调用全量规则同步接口。
|
||||
syncRuleUsers() {
|
||||
if (this.syncLoading || this.tableLoading) { return }
|
||||
this.$set(this, "syncLoading", true);
|
||||
let description = "";
|
||||
$.post(loc() + "/syncRuleDescription", {}).then((res) => {
|
||||
if (!res || res.code !== 0 || !res.data || !res.data.description) {
|
||||
this.$message.warning((res && res.msg) || "无法获取同步规则中的单位名称,请检查单位信息");
|
||||
return;
|
||||
}
|
||||
description = res.data.description;
|
||||
}, () => {
|
||||
this.$message.error("无法获取同步规则中的单位名称,请检查单位信息");
|
||||
}).always(() => {
|
||||
if (!description) { this.$set(this, "syncLoading", false); return }
|
||||
// 项目使用旧版 jQuery,完成读取后再开启原生确认 Promise,避免提前解除同步锁。
|
||||
const message = "确定将符合以下规则的人员同步至华住会吗? " + description
|
||||
+ " 本次将同步全部符合规则的有效人员,不受当前搜索条件、分页或勾选范围限制。";
|
||||
this.confirmSync(message, "/syncRuleUsers", {});
|
||||
closeSyncManage() { this.$set(this, "syncDialogVisible", false) },
|
||||
changeSyncTab(tab) {
|
||||
this.$set(this, "syncTab", tab.name);
|
||||
if (tab.name === "submit" && !this.ruleDescription && !this.ruleDescriptionLoading) { this.loadRuleDescription() }
|
||||
},
|
||||
// 只读取并展示实际单位名称和业务规则,不创建任务;提交时服务端会再次校验。
|
||||
loadRuleDescription() {
|
||||
this.$set(this, "ruleDescriptionLoading", true);
|
||||
return $.post(loc() + "/syncRuleDescription", {}).then((res) => {
|
||||
if (res.code === 0 && res.data) { this.$set(this, "ruleDescription", res.data.description || "") }
|
||||
}, () => { /* 中文错误由全局回调显示。 */ }).always(() => {
|
||||
this.$set(this, "ruleDescriptionLoading", false);
|
||||
});
|
||||
},
|
||||
// message 为中文同步范围,path/params 为确认后才提交的接口及参数;取消不推送。
|
||||
// 固定主页面勾选范围,确认后只创建批次,实际推送由后台逐人执行。
|
||||
syncSelected() {
|
||||
if (this.syncLoading || this.summary.running || this.summaryLoading) { return }
|
||||
if (!this.selectedRows.length) { this.$message.warning("请先在主页面选择需要同步的员工"); return }
|
||||
const userIds = this.selectedRows.map((row) => row.id);
|
||||
this.$set(this, "syncLoading", true);
|
||||
this.confirmSync("确定将选中的 " + userIds.length + " 名员工加入后台推送任务吗?", "/syncUsers", {userIds: userIds});
|
||||
},
|
||||
syncRuleUsers() {
|
||||
if (this.syncLoading || this.summary.running || this.summaryLoading || !this.ruleDescription) { return }
|
||||
this.$set(this, "syncLoading", true);
|
||||
this.confirmSync("确定将符合以下规则的待推送人员加入后台任务吗? " + this.ruleDescription
|
||||
+ " 不受当前搜索、分页及勾选影响,已推送及待核实人员不会重复提交。", "/syncRuleUsers", {});
|
||||
},
|
||||
// path/params 为创建任务的接口和参数;用户取消时不提交。
|
||||
confirmSync(message, path, params) {
|
||||
this.$confirm(message, "同步确认", {
|
||||
type: "warning", confirmButtonText: "确定同步", cancelButtonText: "取消"
|
||||
}).then(() => this.submitSync(path, params)).catch((reason) => {
|
||||
if (reason !== "cancel" && reason !== "close") {
|
||||
this.$message.error("同步请求失败,部分人员可能已同步,请核实状态后再操作");
|
||||
}
|
||||
// 旧版 jQuery 的网络失败仍会传到原生 Promise;jqXHR 的中文提示已由全局回调显示。
|
||||
if (reason && typeof reason.status === "number") { return }
|
||||
if (reason !== "cancel" && reason !== "close") { this.$message.error("提交过程异常,请更新结果确认任务是否已创建") }
|
||||
}).finally(() => { this.$set(this, "syncLoading", false) });
|
||||
},
|
||||
// path 为本菜单的同步接口后缀;params 为选中用户主键数组或规则同步的空参数。
|
||||
// 响应 data 为逐人结果数组,success 表示是否成功,message 为结果说明。
|
||||
// data 为批次 id/total 等元数据;提交成功切到该批次记录,不等待后台处理完成。
|
||||
submitSync(path, params) {
|
||||
this.$set(this, "tableLoading", true);
|
||||
return $.post(loc() + path, params).then((res) => {
|
||||
if (!res || res.code !== 0) {
|
||||
this.$message.warning((res && res.msg) || "同步失败");
|
||||
return;
|
||||
}
|
||||
const results = res.data || [];
|
||||
if (!results.length) { this.$message.info("没有符合规则的人员需要同步"); return }
|
||||
const failed = results.filter((item) => !item.success);
|
||||
const message = "同步完成,成功 " + (results.length - failed.length) + " 人,失败 " + failed.length + " 人";
|
||||
if (failed.length) {
|
||||
this.$message.warning(message);
|
||||
this.$alert(failed.map((item) => (item.userName || "未知姓名") + "(" + (item.loginName || item.userId) + "):" + item.message).join("\n"), "失败明细")
|
||||
.catch(() => { /* 关闭失败明细不影响本次同步结果。 */ });
|
||||
} else { this.$message.success(message) }
|
||||
}).always(() => { this.$set(this, "tableLoading", false) });
|
||||
},
|
||||
queryStatus(row) {
|
||||
this.$set(this, "tableLoading", true);
|
||||
$.post(loc() + "/queryStatus", {userId: row.id}).then((res) => {
|
||||
if (res.code === 0) { this.$set(this, "statusData", res.data); this.$set(this, "statusDialogVisible", true) } else { this.$message.warning(res.msg || "查询失败") }
|
||||
if (!res || res.code !== 0) { return }
|
||||
this.$message.success("已创建 " + res.data.total + " 人的后台推送任务,可离开页面,稍后查看结果");
|
||||
this.$set(this, "selectedRows", []);
|
||||
this.$set(this, "syncTab", "records");
|
||||
this.$nextTick(() => { this.loadSummary(res.data.id, true) });
|
||||
this.doSearch();
|
||||
}, (xhr) => {
|
||||
const message = xhr && xhr.responseJSON && xhr.responseJSON.msg;
|
||||
this.$message.error(message || "查询华住状态失败,请稍后重试");
|
||||
}).always(() => { this.$set(this, "tableLoading", false) })
|
||||
// 旧版 jQuery 保留拒绝状态;传回 jqXHR,供确认 Promise 识别已由全局提示的网络错误。
|
||||
return xhr;
|
||||
}).always(() => {
|
||||
this.$set(this, "syncLoading", false);
|
||||
});
|
||||
},
|
||||
// taskId 为空读取最近批次;initializeRecords 只在打开弹框或新任务提交后重置记录范围。
|
||||
// 请求序号防止快速切换批次时,较慢的旧响应覆盖当前批次统计。
|
||||
loadSummary(taskId, initializeRecords = false) {
|
||||
const requestId = this.summaryRequestId + 1;
|
||||
this.$set(this, "summaryRequestId", requestId);
|
||||
this.$set(this, "summaryLoading", true);
|
||||
return $.post(loc() + "/syncSummary", {taskId: taskId || ""}).then((res) => {
|
||||
if (requestId !== this.summaryRequestId || !res || res.code !== 0) { return }
|
||||
this.$set(this, "summary", res.data);
|
||||
this.$set(this, "summaryUpdatedAt", Date.now());
|
||||
if (initializeRecords) {
|
||||
const selectedId = res.data.task ? res.data.task.id : "";
|
||||
this.$set(this, "selectedTaskId", selectedId);
|
||||
this.$nextTick(() => {
|
||||
if (requestId === this.summaryRequestId && this.$refs.syncInfo) { this.$refs.syncInfo.onOpen(selectedId) }
|
||||
});
|
||||
}
|
||||
}, () => { /* 全局回调已显示中文错误。 */ }).always(() => {
|
||||
if (requestId === this.summaryRequestId) { this.$set(this, "summaryLoading", false) }
|
||||
});
|
||||
},
|
||||
changeSummaryTask(taskId) {
|
||||
// 每个批次使用独立记录组件,旧批次迟到的分页响应只会更新已销毁的实例。
|
||||
this.$set(this, "selectedTaskId", taskId);
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.syncInfo) { this.$refs.syncInfo.onOpen(taskId) }
|
||||
});
|
||||
this.loadSummary(taskId);
|
||||
},
|
||||
// 仅刷新数据库统计、回执和主页面待推送名单,绝不调用任何推送接口。
|
||||
refreshResults() {
|
||||
this.loadSummary(this.selectedTaskId);
|
||||
if (this.$refs.syncInfo) { this.$refs.syncInfo.pageData() }
|
||||
this.doSearch();
|
||||
},
|
||||
// row.id 为本项目用户主键;本行独立显示进度,不阻塞列表或后台任务。
|
||||
queryStatus(row) {
|
||||
if (this.queryingUserId) { return }
|
||||
this.$set(this, "queryingUserId", row.id);
|
||||
$.post(loc() + "/queryStatus", {userId: row.id}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "statusData", res.data);
|
||||
this.$set(this, "statusDialogVisible", true);
|
||||
}
|
||||
}, () => { /* 中文错误由全局回调显示一次。 */ }).always(() => {
|
||||
this.$set(this, "queryingUserId", "");
|
||||
});
|
||||
}
|
||||
},
|
||||
created() { this.pageData() }
|
||||
created() { this.pageData(); this.loadSummary("") }
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
const huaZhuSyncInfo = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="huazhu-sync-records">
|
||||
<div class="huazhu-sync-batch">
|
||||
<span class="huazhu-sync-batch-label">推送批次</span>
|
||||
<el-select :value="pageForm.taskId" @change="changeTask" placeholder="选择批次" size="small" :disabled="summaryLoading || tableLoading" aria-label="推送批次">
|
||||
<el-option label="全部历史批次" value=""></el-option>
|
||||
<el-option v-for="task in summary.tasks" :key="task.id" :value="task.id" :label="batchLabel(task)"></el-option>
|
||||
</el-select>
|
||||
<el-tag v-if="activeTask" :type="activeTask.status === 'FAILED' ? 'danger' : activeTask.status === 'RUNNING' ? '' : 'info'" size="small" effect="light">
|
||||
<i :class="activeTask.status === 'FAILED' ? 'el-icon-warning' : activeTask.status === 'RUNNING' ? 'el-icon-time' : 'el-icon-finished'"></i>
|
||||
{{taskStatusText}}
|
||||
</el-tag>
|
||||
<el-button :loading="summaryLoading || tableLoading" @click="refreshResults" icon="el-icon-refresh" size="small" plain type="primary">更新结果</el-button>
|
||||
</div>
|
||||
<el-alert v-if="summary.background && summary.background.message" class="huazhu-sync-submit-note" :title="summary.background.message" type="warning" show-icon :closable="false"></el-alert>
|
||||
<el-alert v-if="activeTask && activeTask.status === 'FAILED'" class="huazhu-sync-submit-note" title="本批次异常中止,已完成的结果已保留。请查看人员结果说明;未发送人员可重新提交,待核实人员请先查询华住状态。" type="error" show-icon :closable="false"></el-alert>
|
||||
<p v-if="summary.background" class="huazhu-sync-note">{{summary.background.lastTriggeredAt ? '本次应用启动后最近后台触发:' + $moment(summary.background.lastTriggeredAt).format('YYYY-MM-DD HH:mm:ss') : '本次应用启动后尚未收到后台执行回报'}}</p>
|
||||
<template v-if="activeTask">
|
||||
<div class="huazhu-sync-stats" v-loading="summaryLoading">
|
||||
<div v-for="item in statistics" :key="item.label" :class="['huazhu-sync-stat', item.type]">
|
||||
<strong>{{formatCount(item.value)}}</strong><span>{{item.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="huazhu-sync-progress">
|
||||
<span>已处理 {{formatCount(processedCount)}} / {{formatCount(activeTask.total)}} 人</span>
|
||||
<el-progress :percentage="progressPercentage" :show-text="false" :stroke-width="8" color="#006bb8"></el-progress>
|
||||
<span>{{progressPercentage}}%</span>
|
||||
<span class="huazhu-sync-updated">{{updatedAt ? '更新于 ' + $moment(updatedAt).format('HH:mm:ss') : ''}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="huazhu-sync-empty" v-loading="summaryLoading">
|
||||
{{summary.tasks.length ? '当前查看全部历史记录。选择具体批次,可查看本批次统计和进度。' : '暂无推送任务。可切换到发起推送,选择勾选员工或规则内人员进行同步。'}}
|
||||
</div>
|
||||
<div class="huazhu-sync-filter">
|
||||
<label>员工信息</label>
|
||||
<el-input v-model="pageForm.searchKeyword" clearable placeholder="输入工号或姓名" @keyup.enter.native="doSearch" size="small" suffix-icon="el-icon-search" aria-label="员工信息"></el-input>
|
||||
<label class="filter-status">处理结果</label>
|
||||
<el-select :value="pageForm.status" @change="changeStatus" :disabled="tableLoading" size="small" aria-label="处理结果">
|
||||
<el-option label="全部状态" value=""></el-option>
|
||||
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value"></el-option>
|
||||
</el-select>
|
||||
<el-button @click="doSearch" :disabled="tableLoading" size="small" type="primary">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" v-loading="tableLoading" :size="tableSize" :max-height="320" class="vi-table huazhu-sync-record-table" empty-text="暂无符合条件的推送记录">
|
||||
<el-table-column label="工号" prop="loginName" min-width="110"></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" min-width="90"></el-table-column>
|
||||
<el-table-column label="处理结果" min-width="125">
|
||||
<template slot-scope="{row}">
|
||||
<span :class="'huazhu-result-' + (row.status || '').toLowerCase()"><i :class="['result-icon', statusIcons[row.status]]"></i>{{statusLabels[row.status] || row.status}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理时间" min-width="165">
|
||||
<template slot-scope="{row}">{{row.finishedAt ? $moment(row.finishedAt).format('YYYY-MM-DD HH:mm:ss') : '--'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果说明" min-width="270" show-overflow-tooltip>
|
||||
<template slot-scope="{row}">{{resultMessage(row)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template slot-scope="{row}"><el-button size="mini" type="text" :loading="queryingUserId === row.userId" :disabled="!!queryingUserId" @click="queryStatus(row)">查询状态</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
mixins: [initTableMixins],
|
||||
props: {
|
||||
summary: {type: Object, required: true},
|
||||
summaryLoading: {type: Boolean, default: false},
|
||||
updatedAt: {type: Number, default: null},
|
||||
queryingUserId: {type: String, default: ""}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: loc() + "/recordPageData",
|
||||
statusLabels: {PENDING: "等待推送", SENDING: "正在推送", SUCCESS: "推送成功", EXISTS: "华住已存在", FAILED: "推送失败", UNKNOWN: "待核实"},
|
||||
statusIcons: {PENDING: "el-icon-time", SENDING: "el-icon-loading", SUCCESS: "el-icon-success", EXISTS: "el-icon-circle-check", FAILED: "el-icon-error", UNKNOWN: "el-icon-warning"}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 数据库 RUNNING 只表示批次未结束,实际进入后台循环才显示正在处理。
|
||||
taskStatusText() {
|
||||
if (!this.activeTask) { return "" }
|
||||
if (this.activeTask.status === "FAILED") { return "异常中止" }
|
||||
if (this.activeTask.status !== "RUNNING") { return "处理结束" }
|
||||
return this.summary.background && this.summary.background.executingTaskId === this.activeTask.id ? "后台处理中" : "等待后台执行";
|
||||
},
|
||||
// 全部历史记录不展示单批次统计;批次切换期间不把上一个批次的计数展示为当前结果。
|
||||
activeTask() {
|
||||
return this.summary.task && this.summary.task.id === this.pageForm.taskId ? this.summary.task : null;
|
||||
},
|
||||
statistics() {
|
||||
const counts = this.summary.counts || {};
|
||||
return [
|
||||
{label: "总人数", value: this.activeTask ? this.activeTask.total : 0, type: "total"},
|
||||
{label: "推送成功", value: counts.SUCCESS || 0, type: "success"},
|
||||
{label: "华住已存在", value: counts.EXISTS || 0, type: "exists"},
|
||||
{label: "失败", value: counts.FAILED || 0, type: "failed"},
|
||||
{label: "待核实", value: counts.UNKNOWN || 0, type: "unknown"},
|
||||
{label: "待处理", value: (counts.PENDING || 0) + (counts.SENDING || 0), type: "pending"}
|
||||
];
|
||||
},
|
||||
processedCount() {
|
||||
if (!this.activeTask) { return 0 }
|
||||
const counts = this.summary.counts || {};
|
||||
return Math.max(0, Math.min(this.activeTask.total, (counts.SUCCESS || 0) + (counts.EXISTS || 0) + (counts.FAILED || 0) + (counts.UNKNOWN || 0)));
|
||||
},
|
||||
progressPercentage() {
|
||||
return this.activeTask && this.activeTask.total > 0 ? Math.round(this.processedCount * 100 / this.activeTask.total) : 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// taskId 为打开时的批次主键,空值查全部;查询、分页、加载状态均复用 initTableMixins。
|
||||
onOpen(taskId) {
|
||||
this.$set(this.pageForm, "taskId", taskId || "");
|
||||
this.$set(this.pageForm, "status", "");
|
||||
this.doSearch();
|
||||
},
|
||||
changeTask(value) {
|
||||
this.$set(this.pageForm, "taskId", value);
|
||||
// 父组件按批次重建记录实例,再调用 onOpen 复用分页查询,隔离旧批次的未完成请求。
|
||||
this.$emit("task-change", value);
|
||||
},
|
||||
changeStatus(value) { this.$set(this.pageForm, "status", value); this.doSearch() },
|
||||
refreshResults() { this.$emit("refresh") },
|
||||
queryStatus(row) { this.$emit("query-status", {id: row.userId}) },
|
||||
batchLabel(task) {
|
||||
return this.$moment(task.submittedAt).format("YYYY-MM-DD HH:mm:ss") + " · " + (task.source === "RULE" ? "规则内人员" : "选中员工");
|
||||
},
|
||||
formatCount(value) { return Number(value || 0).toLocaleString("zh-CN") },
|
||||
// 服务端保存脱敏原始回执;表格优先显示中文 message,未知文本保留用于核实。
|
||||
resultMessage(row) {
|
||||
try {
|
||||
const receipt = JSON.parse(row.message);
|
||||
return receipt.message || receipt.msg || row.message || "--";
|
||||
} catch (ignored) {
|
||||
return row.message || "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ layout("/layouts/platform.html"){
|
||||
<custom-card>
|
||||
<snaker-start slot="header" label="补贴申请" define_key="ZGWW">
|
||||
<template slot="header-right-label">
|
||||
<el-button v-if="$auth.hasRole('SYSADMIN')" type="text" class="mr10"
|
||||
<!-- 系统管理员和校工会管理员均可维护全校慰问申报截止日期。 -->
|
||||
<el-button v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])" type="text" class="mr10"
|
||||
@click="openApplyPeriodDialog">申报时间设置</el-button>
|
||||
</template>
|
||||
</snaker-start>
|
||||
|
||||
@@ -138,6 +138,8 @@ layout("/layouts/platform.html"){
|
||||
{prop: 'applyUnitName', label: '所属单位'},
|
||||
{prop: 'typeName', label: '补贴类型'},
|
||||
{prop: 'taskName', label: '当前节点'},
|
||||
// 展示业务申请时间,与详情页及月份筛选使用的 createTime 一致。
|
||||
{prop: 'createTime', label: '申请时间', width: 180},
|
||||
{prop: 'instanceState', label: '流程状态'},
|
||||
],
|
||||
unionList: [],
|
||||
|
||||
+176
-59
@@ -9,8 +9,13 @@ layout("/layouts/platform.html"){
|
||||
.difficult-apply .nowrap-select .el-select__tags { flex-wrap: nowrap; overflow: hidden; }
|
||||
</style>
|
||||
<div id="app" class="difficult-apply" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<snaker-start slot="header" label="特困补助申请" define_key="TKBZ"></snaker-start>
|
||||
<custom-card>
|
||||
<snaker-start slot="header" label="特困补助申请" define_key="TKBZ">
|
||||
<template slot="header-right-label">
|
||||
<el-button v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])"
|
||||
type="text" class="mr10" @click="openApplyPeriodDialog">申报时间设置</el-button>
|
||||
</template>
|
||||
</snaker-start>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="0"
|
||||
label-suffix=":" class="flow-task-form">
|
||||
<el-descriptions :column="3" border>
|
||||
@@ -98,9 +103,9 @@ layout("/layouts/platform.html"){
|
||||
<template slot-scope="scope">
|
||||
<el-form-item :prop="'hospitalRecords.' + scope.$index + '.selfFundedAmount'"
|
||||
:rules="conditionalRule(isAccidentSubsidy(scope.row))">
|
||||
<el-input-number v-model="scope.row.selfFundedAmount" :min="0" :precision="2"
|
||||
<el-input v-model="scope.row.selfFundedAmount" type="number"
|
||||
:disabled="!isAccidentSubsidy(scope.row)"
|
||||
style="width:100%"></el-input-number>
|
||||
style="width:100%"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -158,6 +163,7 @@ layout("/layouts/platform.html"){
|
||||
<el-form-item prop="applyFile" :rules="requiredRule('请上传纸质申请表')">
|
||||
<file-upload :value.sync="formData.applyFile" upload_mode="drag"
|
||||
:upload_number="1" upload_result_category="array"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.gif,.bmp,.webp,.tif,.tiff" :validate_accept="true"
|
||||
complete_result></file-upload>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
@@ -188,11 +194,27 @@ layout("/layouts/platform.html"){
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
<el-row type="flex" justify="end" class="mt20">
|
||||
<el-button v-if="!isSchoolAuditEdit" type="primary" plain @click="onSave">保存</el-button>
|
||||
<el-button type="primary" @click="onSubmit">提交</el-button>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<template slot="footer">
|
||||
<template v-if="bizId || isSchoolAuditEdit || (applyPeriodLoaded && !applyClosed)">
|
||||
<el-button v-if="!isSchoolAuditEdit" type="primary" plain :loading="formLoading" @click="onSave">保存</el-button>
|
||||
<el-button type="primary" :loading="formLoading" @click="onSubmit">提交</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</custom-card>
|
||||
<el-dialog title="申报时间设置" :visible="applyPeriodDialogVisible" width="460px"
|
||||
:close-on-click-modal="false" @close="closeApplyPeriodDialog">
|
||||
<el-form ref="applyPeriodFormRef" :model="applyPeriodForm" :rules="applyPeriodRules" label-width="120px">
|
||||
<el-form-item label="申报截止日期" prop="endDate">
|
||||
<el-date-picker v-model="applyPeriodForm.endDate" type="date" value-format="yyyy-MM-dd"
|
||||
placeholder="请选择截止日期" style="width:100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-alert title="截止日期当天起停止新增申报,已有申请可继续办理。" type="info" :closable="false"></el-alert>
|
||||
</el-form>
|
||||
<template slot="footer">
|
||||
<el-button @click="closeApplyPeriodDialog">取消</el-button>
|
||||
<el-button type="primary" :loading="applyPeriodSaving" @click="saveApplyEndDate">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
@@ -224,10 +246,26 @@ layout("/layouts/platform.html"){
|
||||
projectList: [],
|
||||
diseaseList: [],
|
||||
userOptions: [],
|
||||
userLoading: false
|
||||
userLoading: false,
|
||||
applyEndDate: "",
|
||||
applyPeriodLoaded: false,
|
||||
applyPeriodDialogVisible: false,
|
||||
applyPeriodSaving: false,
|
||||
applyPeriodForm: {endDate: ""},
|
||||
applyPeriodRules: {
|
||||
endDate: [{required: true, message: "请选择申报截止日期", trigger: "change"}]
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 空配置不限制申请;错误配置关闭新增入口,最终仍由后端校验日期。
|
||||
applyClosed() {
|
||||
if (!this.applyEndDate) {
|
||||
return false;
|
||||
}
|
||||
var deadline = this.$moment(this.applyEndDate, "YYYY-MM-DD", true);
|
||||
return !deadline.isValid() || !this.$moment().startOf("day").isBefore(deadline);
|
||||
},
|
||||
selectedProjects: function () {
|
||||
var selected = this.formData.subsidyType || [];
|
||||
return this.projectList.filter(function (item) {
|
||||
@@ -236,6 +274,78 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 读取 Result.data 中的 yyyy-MM-dd 日期,读取成功前不开放新申请按钮。
|
||||
queryApplyEndDate() {
|
||||
return this.$axios.post("/platform/difficultSubsidy/apply/getApplyEndDate").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "applyEndDate", res.data || "");
|
||||
this.$set(this, "applyPeriodLoaded", true);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 与慰问申请一致:截止时显示“温馨提示”,关闭弹框后留在当前页,便于管理员设置申报时间。
|
||||
showApplyClosedAlert() {
|
||||
this.$alert("本次申报已截止请关注下次申报", "温馨提示", {
|
||||
confirmButtonText: "确定",
|
||||
callback: () => {}
|
||||
});
|
||||
},
|
||||
openApplyPeriodDialog() {
|
||||
this.queryApplyEndDate().then(() => {
|
||||
if (!this.applyPeriodLoaded) {
|
||||
return;
|
||||
}
|
||||
this.$set(this.applyPeriodForm, "endDate", this.applyEndDate);
|
||||
this.$set(this, "applyPeriodDialogVisible", true);
|
||||
this.$nextTick(() => {
|
||||
this.$refs.applyPeriodFormRef.clearValidate();
|
||||
});
|
||||
});
|
||||
},
|
||||
closeApplyPeriodDialog() {
|
||||
this.$set(this, "applyPeriodDialogVisible", false);
|
||||
},
|
||||
// endDate 为日期字符串,后端统一校验管理角色及格式;成功后立即刷新本页截止状态。
|
||||
saveApplyEndDate() {
|
||||
if (this.applyPeriodSaving) {
|
||||
return;
|
||||
}
|
||||
this.$refs.applyPeriodFormRef.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
var endDate = this.applyPeriodForm.endDate;
|
||||
this.$set(this, "applyPeriodSaving", true);
|
||||
this.$axios.post("/platform/difficultSubsidy/apply/saveApplyEndDate", {endDate: endDate})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "applyEndDate", endDate);
|
||||
this.closeApplyPeriodDialog();
|
||||
this.$message.success("申报时间设置成功");
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "applyPeriodSaving", false);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 已有申请和审核修改不受新申请截止限制,后端会核实申请ID确实存在。
|
||||
canSaveApplication() {
|
||||
if (this.formLoading) {
|
||||
return false;
|
||||
}
|
||||
if (this.bizId || this.isSchoolAuditEdit) {
|
||||
return true;
|
||||
}
|
||||
if (!this.applyPeriodLoaded) {
|
||||
this.$message.warning("申报时间尚未加载,请刷新页面后重试");
|
||||
return false;
|
||||
}
|
||||
if (this.applyClosed) {
|
||||
this.showApplyClosedAlert();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
requiredRule: function (message) {
|
||||
return [{required: true, message: message, trigger: ["change", "blur"]}];
|
||||
},
|
||||
@@ -299,23 +409,21 @@ layout("/layouts/platform.html"){
|
||||
this.$set(data, "files", []);
|
||||
}
|
||||
},
|
||||
queryRecipients: function (keyword, selectFirst) {
|
||||
var self = this;
|
||||
queryRecipients(keyword, selectFirst) {
|
||||
if (!keyword) {
|
||||
this.$set(this, "userOptions", []);
|
||||
return;
|
||||
}
|
||||
this.$set(this, "userLoading", true);
|
||||
this.$axios.post("/platform/difficultSubsidy/apply/queryRecipients", {key: keyword}).then(function (res) {
|
||||
this.$axios.post("/platform/difficultSubsidy/apply/queryRecipients", {key: keyword}).then((res) => {
|
||||
var users = res.code === 0 ? (res.data || []) : [];
|
||||
self.$set(self, "userOptions", users);
|
||||
self.$set(self, "userLoading", false);
|
||||
this.$set(this, "userOptions", users);
|
||||
if (selectFirst && users.length > 0) {
|
||||
self.$set(self.formData, "userId", users[0].id);
|
||||
self.setSelectedUser(users[0]);
|
||||
this.$set(this.formData, "userId", users[0].id);
|
||||
this.setSelectedUser(users[0]);
|
||||
}
|
||||
}).catch(function () {
|
||||
self.$set(self, "userLoading", false);
|
||||
}).finally(() => {
|
||||
this.$set(this, "userLoading", false);
|
||||
});
|
||||
},
|
||||
userChange: function (userId) {
|
||||
@@ -389,62 +497,71 @@ layout("/layouts/platform.html"){
|
||||
removeHospitalRecord: function (index) {
|
||||
this.formData.hospitalRecords.splice(index, 1);
|
||||
},
|
||||
onSave: function () {
|
||||
var self = this;
|
||||
onSave() {
|
||||
if (!this.canSaveApplication()) {
|
||||
return;
|
||||
}
|
||||
this.$confirm("您确定保存吗?", "提示", {
|
||||
confirmButtonText: "确定", cancelButtonText: "取消", type: "warning"
|
||||
}).then(function () {
|
||||
self.$axios.post("/platform/difficultSubsidy/apply/save", {
|
||||
data: JSON.stringify(self.formData)
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.$message.success("保存成功");
|
||||
commonUtil.pjaxPush("/platform/difficultSubsidy/mine");
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
this.sendApplication("/platform/difficultSubsidy/apply/save", {
|
||||
data: JSON.stringify(this.formData)
|
||||
}, "保存成功", "/platform/difficultSubsidy/mine");
|
||||
});
|
||||
},
|
||||
onSubmit: function () {
|
||||
var self = this;
|
||||
this.$refs.formRef.validate(function (valid) {
|
||||
onSubmit() {
|
||||
if (!this.canSaveApplication()) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
if (!self.formData.hospitalRecords || self.formData.hospitalRecords.length === 0) {
|
||||
self.$message.warning("请填写住院记录");
|
||||
if (!this.formData.hospitalRecords || this.formData.hospitalRecords.length === 0) {
|
||||
this.$message.warning("请填写住院记录");
|
||||
return;
|
||||
}
|
||||
self.$confirm("您确定要提交吗?", "提示", {
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定", cancelButtonText: "取消", type: "warning"
|
||||
}).then(function () {
|
||||
if (self.isSchoolAuditEdit) {
|
||||
self.$axios.post("/platform/difficultSubsidy/schoolAudit/modifyApply", {
|
||||
processTaskId: self.taskId,
|
||||
data: JSON.stringify(self.formData)
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.$message.success("修改成功");
|
||||
commonUtil.pjaxPush("/platform/difficultSubsidy/schoolAudit");
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this.isSchoolAuditEdit) {
|
||||
this.sendApplication("/platform/difficultSubsidy/schoolAudit/modifyApply", {
|
||||
processTaskId: this.taskId,
|
||||
data: JSON.stringify(this.formData)
|
||||
}, "修改成功", "/platform/difficultSubsidy/schoolAudit");
|
||||
return;
|
||||
}
|
||||
self.$axios.post("/platform/difficultSubsidy/apply/submit", {
|
||||
data: JSON.stringify(self.formData)
|
||||
}).then(function (res) {
|
||||
if (res.code === 0) {
|
||||
self.$message.success("提交成功");
|
||||
commonUtil.pjaxPush("/platform/difficultSubsidy/mine");
|
||||
}
|
||||
});
|
||||
this.sendApplication("/platform/difficultSubsidy/apply/submit", {
|
||||
data: JSON.stringify(this.formData)
|
||||
}, "提交成功", "/platform/difficultSubsidy/mine");
|
||||
});
|
||||
});
|
||||
},
|
||||
// url/params 对应草稿、提交或审核修改接口;Result.code 为0时提示并跳转到对应列表。
|
||||
sendApplication(url, params, successMessage, redirectUrl) {
|
||||
if (!this.canSaveApplication()) {
|
||||
return;
|
||||
}
|
||||
this.$set(this, "formLoading", true);
|
||||
this.$axios.post(url, params).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(successMessage);
|
||||
commonUtil.pjaxPush(redirectUrl);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false);
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this;
|
||||
this.loadProject().then(function () {
|
||||
self.loadDetail();
|
||||
created() {
|
||||
// 仅新申请在截止日期读取成功后弹出提示,已有申请和校工会审核修改仍可继续办理。
|
||||
this.queryApplyEndDate().then(() => {
|
||||
if (this.applyPeriodLoaded && !this.bizId && !this.isSchoolAuditEdit && this.applyClosed) {
|
||||
this.showApplyClosedAlert();
|
||||
}
|
||||
});
|
||||
this.loadProject().then(() => {
|
||||
this.loadDetail();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,19 @@ const applyForm = {
|
||||
<van-field label="所属单位" readonly v-model="formData.unitName"></van-field>
|
||||
<van-field label="所属工会" readonly v-model="formData.unionName"></van-field>
|
||||
<van-field label="性别" readonly v-model="formData.sex"></van-field>
|
||||
<!-- 手机号必填,格式与后端保持一致:1开头、第二位3至9,共11位数字。 -->
|
||||
<van-field label="手机号"
|
||||
name="mobile"
|
||||
type="tel"
|
||||
maxlength="32"
|
||||
placeholder="请输入手机号"
|
||||
required
|
||||
:rules="[
|
||||
{ required: true, validator: (value) => !!String(value || '').trim(), message: '请填写手机号' },
|
||||
{ pattern: /^1[3-9][0-9]{9}$/, message: '请输入正确的11位手机号' }
|
||||
]"
|
||||
v-model="formData.mobile">
|
||||
</van-field>
|
||||
<template v-if="row.courseIsLimitApply">
|
||||
<van-field label="报名时段"
|
||||
required
|
||||
@@ -48,6 +61,7 @@ const applyForm = {
|
||||
row: {},
|
||||
activity: {},
|
||||
dynamicColumnsData: [],
|
||||
mobileColumn: null,
|
||||
visible: false,
|
||||
formLoading: false,
|
||||
formData: {},
|
||||
@@ -76,12 +90,17 @@ const applyForm = {
|
||||
this.$set(this.formData, 'unionName', this.$store.state.user.union.name)
|
||||
this.$set(this.formData, 'unitName', this.$store.state.user.unit.name)
|
||||
this.$set(this.formData, 'sex', this.$store.state.user.sex)
|
||||
// 默认使用个人资料中的手机号,编辑后的号码仅作为本次报名联系方式保存。
|
||||
this.$set(this.formData, 'mobile', this.$store.state.user.mobile || '')
|
||||
this.$set(this.formData, 'activityId', row.activityId)
|
||||
this.$set(this.formData, 'courseId', row.id)
|
||||
|
||||
this.dynamicColumnsData = courseType ? courseType.trainMobileSignColumnList : []
|
||||
// 手机号统一必填,不受动态 isRequired 影响;同名字段保留元数据兼容同步提交,避免重复输入。
|
||||
const dynamicColumns = clone((courseType && courseType.trainMobileSignColumnList) || [])
|
||||
this.$set(this, 'mobileColumn', dynamicColumns.find((item) => item.columnCode === 'mobile') || null)
|
||||
this.$set(this, 'dynamicColumnsData', dynamicColumns.filter((item) => item.columnCode !== 'mobile'))
|
||||
this.dynamicColumnsData.forEach((item) => {
|
||||
item.columnValue = this.$store.state.user[item.columnCode] || ""
|
||||
this.$set(item, 'columnValue', this.$store.state.user[item.columnCode] || '')
|
||||
})
|
||||
},
|
||||
onCourseConfirm(val){
|
||||
@@ -137,10 +156,14 @@ const applyForm = {
|
||||
if (!validTime) return
|
||||
this.$set(this, "formLoading", true)
|
||||
const formData = clone(this.formData)
|
||||
formData.mobileColumnsValue = JSON.stringify(this.dynamicColumnsData.map((item) => ({
|
||||
columnName: item.columnName, columnValue: item.columnValue,
|
||||
columnCode: item.columnCode, columnFormType: item.columnFormType
|
||||
})))
|
||||
// 导出会按动态字段编码回填数据,同名 mobile 必须同步本次编辑值。
|
||||
const dynamicColumns = this.dynamicColumnsData.concat(this.mobileColumn ? [this.mobileColumn] : [])
|
||||
formData.mobileColumnsValue = JSON.stringify(dynamicColumns.map((item) => ({
|
||||
columnName: item.columnName,
|
||||
columnValue: item.columnCode === 'mobile' ? formData.mobile : item.columnValue,
|
||||
columnCode: item.columnCode,
|
||||
columnFormType: item.columnFormType
|
||||
})))
|
||||
return this.$axios.post("/platform/trainSignUp/apply/doSignUp", formData).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$toast(res.msg)
|
||||
|
||||
Reference in New Issue
Block a user