品牌活动整改
幸福工惠移植
This commit is contained in:
@@ -27,6 +27,8 @@ RoleConstant {
|
||||
SCHOOL_UNION_PROPOSAL_ADMIN("校工会提案管理员"),
|
||||
SCHOOL_UNION_CLUB_ADMIN("校工会社团管理员"),
|
||||
SCHOOL_UNION_ACTIVITY_ADMIN("校工会活动管理员"),
|
||||
/** 品牌活动统计、人员调整管理员,数据范围按 trainSignUp 子活动负责人关系控制。 */
|
||||
SCHOOL_UNION_TRAIN_SIGN_UP_ADMIN("校工会品牌活动管理员"),
|
||||
SCHOOL_UNION_ARTICLE_CLUB_ADMIN("校工会社团新闻审批管理员"),
|
||||
SCHOOL_UNION_ARTICLE_FGH_ADMIN("校工会分工会新闻审批管理员"),
|
||||
SCHOOL_UNION_ARTICLE_ADMIN("校工会新闻审批管理员"),
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.budwk.app.web.commons.ext.handler;
|
||||
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.Executor;
|
||||
import org.apache.catalina.Wrapper;
|
||||
import org.apache.catalina.core.StandardThreadExecutor;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.valves.ErrorReportValve;
|
||||
@@ -51,6 +52,9 @@ public class WkTomcatConfiguration implements NbAppEventListener {
|
||||
|
||||
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
|
||||
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
|
||||
// 静态文本资源统一按 UTF-8 读取,避免服务器默认 GBK 与 UTF-8 响应编码不一致时破坏中文和正则。
|
||||
Wrapper defaultServlet = (Wrapper) context.findChild("default");
|
||||
defaultServlet.addInitParameter("fileEncoding", "UTF-8");
|
||||
// 为静态 SVG 补齐响应类型,避免浏览器收到文件后仍无法按图片解码。
|
||||
context.addMimeMapping("svg", "image/svg+xml");
|
||||
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
|
||||
|
||||
+48
-99
@@ -1,32 +1,22 @@
|
||||
package com.budwk.app.zhgh.activity.trainSignUp.controller.manage;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
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.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author JyuHsin
|
||||
* @Date 2022/9/21
|
||||
@@ -39,12 +29,6 @@ import java.util.List;
|
||||
@At("/platform/trainSignUp/adjust")
|
||||
public class TrainSignUpAdjustController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private BaseService baseService;
|
||||
@Inject
|
||||
private TrainSignUpActivityService trainSignUpActivityManageService;
|
||||
@Inject
|
||||
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
|
||||
|
||||
@@ -55,122 +39,87 @@ public class TrainSignUpAdjustController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动list
|
||||
* @param year 年度
|
||||
* @return
|
||||
* @param year 查询年度,为空时不限制年度
|
||||
* @return Result.data 为当前登录人可管理的已启用主活动数组,id/activityName 用于下拉框
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("活动列表")
|
||||
@SaCheckPermission("trainSignUp.adjust")
|
||||
public Result activityList(@Param(value = "year") Integer year) {
|
||||
List<TrainSignUpActivity> activityList = dao.query(TrainSignUpActivity.class, Cnd.NEW().andEX("year", "=", year).andEX("isDisabled", "=", false).desc("activityStartTime"));
|
||||
return Result.success(activityList);
|
||||
public Result activityList(@Param("year") Integer year) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.activityList(year, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||
* @param activityId 主活动ID,为空时返回空分页
|
||||
* @return Result.data 为 Pagination,list 和 totalCount 均按负责人范围查询
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("分页查询")
|
||||
@SaCheckPermission("trainSignUp.adjust")
|
||||
public Result pageData(PageForm pageForm,
|
||||
@Param(value = "activityId") String activityId) {
|
||||
public Result pageData(PageForm pageForm, @Param("activityId") String activityId) {
|
||||
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activityId 主活动ID
|
||||
* @return Result.data 为可调整的子活动数组,id 为目标ID,registerNum/hasWaitingNum 为已报及候补人数
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("子活动查询")
|
||||
@SaCheckPermission("trainSignUp.adjust")
|
||||
public Result getCourse(String activityId) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
tsuc.id,
|
||||
tsuc.courseName,
|
||||
tsuc.coursePeopleNumber,
|
||||
tsuc.courseType,
|
||||
tsuc.courseLocation,
|
||||
tsuc.courseInstructor,
|
||||
tsuc.courseReservedNumber,
|
||||
tsuc.waitingNum,
|
||||
tsuc.reserveMode
|
||||
FROM
|
||||
`train_sign_up_course` tsuc
|
||||
WHERE
|
||||
tsuc.activityId = @activityId
|
||||
ORDER BY courseName asc
|
||||
""");
|
||||
sql.setParam("activityId", activityId);
|
||||
List<NutMap> courseList = baseService.listMap(sql);
|
||||
courseList.forEach(c -> {
|
||||
c.put("registerNum", trainSignUpActivityStatisticsService.queryCourseCount(c.getString("id"), c.getString("courseType")));
|
||||
c.put("hasWaitingNum", trainSignUpActivityStatisticsService.queryCourseWaitCount(c.getString("id"), c.getString("courseType")));
|
||||
});
|
||||
return Result.success(courseList);
|
||||
return Result.success(trainSignUpActivityStatisticsService.getCourse(activityId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param courseId 有管理权限的子活动ID
|
||||
* @param unionId 工会ID,可为空
|
||||
* @param unitId 单位ID,可为空
|
||||
* @param searchName 保留现有接口参数,姓名和工号搜索统一使用 searchKeyword
|
||||
* @param searchKeyword 姓名或工号关键字,可为空
|
||||
* @return Result.data 为报名人员数组,userId 为报名人员ID,state 为报名状态
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("报名用户列表")
|
||||
@SaCheckPermission("trainSignUp.adjust")
|
||||
public Result registerUserList(@Param("courseId") String courseId,
|
||||
@Param(value = "unionId") String unionId,
|
||||
@Param(value = "unitId") String unitId,
|
||||
@Param(value = "searchName") String searchName,
|
||||
@Param(value = "searchKeyword") String searchKeyword) {
|
||||
List<NutMap> list = trainSignUpActivityStatisticsService.registerUserList(courseId, unionId, unitId, searchName, searchKeyword);
|
||||
return Result.success(list);
|
||||
public Result registerUserList(@Param("courseId") String courseId, @Param("unionId") String unionId,
|
||||
@Param("unitId") String unitId, @Param("searchName") String searchName,
|
||||
@Param("searchKeyword") String searchKeyword) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId, unionId, unitId, searchName, searchKeyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activityId 主活动ID,来源和目标子活动都必须属于该活动
|
||||
* @param oldCourseId 来源子活动ID,必须有管理权限
|
||||
* @param newCourseId 目标子活动ID,必须有管理权限
|
||||
* @param userId 被调整的报名人员ID,操作人身份由登录会话确定
|
||||
* @return Result,code=0 表示报名及签到记录已在同一事务中调整
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("人员调整")
|
||||
@SaCheckPermission("trainSignUp.adjust")
|
||||
@SLog(tag = "品牌活动-人员调整", msg = "人员调整")
|
||||
@SLog(tag = "品牌活动-人员调整", msg = "人员调整")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||
|
||||
Cnd oldCnd = Cnd.where("activityId", "=", activityId)
|
||||
.and("courseId", "=", oldCourseId).and("userId", "=", userId);
|
||||
|
||||
Cnd newCnd = Cnd.where("activityId", "=", activityId)
|
||||
.and("courseId", "=", newCourseId).and("userId", "=", userId);
|
||||
|
||||
//旧的报名信息
|
||||
TrainSignUpUser oldTrainSignUpUser = dao.fetch(TrainSignUpUser.class, oldCnd);
|
||||
oldTrainSignUpUser.setCourseId(newCourseId);
|
||||
oldTrainSignUpUser.setSignUpTime(DateUtil.date());
|
||||
dao.update(oldTrainSignUpUser);
|
||||
|
||||
//新的课程的信息,上课时间
|
||||
List<TrainSignUpActivityCourse> activityCourseList = dao.query(TrainSignUpActivityCourse.class, Cnd.where("activityId", "=", activityId)
|
||||
.and("courseId", "=", newCourseId));
|
||||
//先清楚旧的信息
|
||||
dao.clear(TrainSignUpUserCourse.class, oldCnd);
|
||||
//添加新的信息
|
||||
List<TrainSignUpUserCourse> trainSignUpUserCourseList = new ArrayList<>();
|
||||
activityCourseList.forEach(item -> {
|
||||
TrainSignUpUserCourse course = new TrainSignUpUserCourse();
|
||||
course.setActivityCourseId(activityId);
|
||||
course.setCourseId(newCourseId);
|
||||
course.setUserId(userId);
|
||||
course.setCourseStartTime(item.getCourseStartTime());
|
||||
course.setCourseEndTime(item.getCourseEndTime());
|
||||
course.setActivityCourseId(item.getId());
|
||||
trainSignUpUserCourseList.add(course);
|
||||
});
|
||||
dao.insert(trainSignUpUserCourseList);
|
||||
trainSignUpActivityStatisticsService.adjust(activityId, oldCourseId, newCourseId, userId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activityId 主活动ID
|
||||
* @param courseId 属于该主活动且有管理权限的子活动ID
|
||||
* @param userId 被删除的报名人员ID
|
||||
* @return Result,code=0 表示该子活动的报名及签到记录已删除
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("删除报名人员")
|
||||
@SaCheckPermission("trainSignUp.adjust")
|
||||
@SLog(tag = "品牌活动-人员调整", msg = "删除报名人员")
|
||||
@SLog(tag = "品牌活动-人员调整", msg = "删除报名人员")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result deleteSignUser(String activityId, String courseId, String userId) {
|
||||
|
||||
//删除
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and("activityId", "=", activityId);
|
||||
cnd.and("courseId", "=", courseId);
|
||||
cnd.and("userId", "=", userId);
|
||||
|
||||
dao.clear(TrainSignUpUser.class, cnd);
|
||||
dao.clear(TrainSignUpUserCourse.class, cnd);
|
||||
trainSignUpActivityStatisticsService.deleteSignUser(activityId, courseId, userId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -128,6 +128,7 @@ public class TrainSignUpApplyController {
|
||||
@Param(value = "assortTypes") String[] assortTypes,
|
||||
@Param(value = "searchKeyword") String searchKeyword,
|
||||
@Param(value = "dataType") String dataType) {
|
||||
// 返回课程签到半径 radius(米),供 H5 地图按实际配置绘制签到范围。
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
tsuc.id,
|
||||
@@ -144,6 +145,7 @@ public class TrainSignUpApplyController {
|
||||
tsuc.unionLimit,
|
||||
tsuc.isMobileSign,
|
||||
tsuc.signType,
|
||||
tsuc.radius,
|
||||
tsuc.isReceiveGift,
|
||||
tsuc.giftType,
|
||||
tsuc.reserveMode,
|
||||
|
||||
+45
-19
@@ -10,8 +10,6 @@ import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.flow.entity.ProcessInstance;
|
||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.sys.models.Sys_home_template;
|
||||
import com.budwk.app.sys.models.Sys_union;
|
||||
import com.budwk.app.sys.models.Sys_unit;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
@@ -22,18 +20,18 @@ import com.budwk.app.zhgh.club.model.SysClub;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
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.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.trans.Trans;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
@@ -66,6 +64,37 @@ public class TrainSignUpManageController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** activityId 为主活动ID,courseName 为可选名称;返回 Result.data 分页对象 list、totalCount。 */
|
||||
@At
|
||||
@SaCheckPermission("trainSignUp.manage")
|
||||
public Result courseManagePageData(PageForm pageForm, @Param("activityId") String activityId,
|
||||
@Param("courseName") String courseName) {
|
||||
return Result.success(trainSignUpActivityManageService.courseManagePageData(pageForm, activityId, courseName));
|
||||
}
|
||||
|
||||
/** activityId、courseId 为所属主活动和子活动ID;返回 Result.data 的 course、activityTime、canEditBasic、courseTypes。 */
|
||||
@At
|
||||
@SaCheckPermission("trainSignUp.manage")
|
||||
public Result courseManageDetail(@Param("activityId") String activityId, @Param("courseId") String courseId) {
|
||||
return Result.success(trainSignUpActivityManageService.courseManageDetail(activityId, courseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* course 接收单个子活动及签到字段,负责人、时段列表按 JSON 传入;updateCourseTimes 仅实际修改时段时为 true。
|
||||
* id、activityId 为子活动和主活动ID;isMobileSign 为签到开关,signType 为1扫码、2被扫、3定位。
|
||||
* signUnit 为非负整数分钟,radius 为正整数米;courseLocationCoordinates、transPosition 分别为地图、WGS-84坐标数组[纬度,经度]。
|
||||
* 管理员可另传名称、类型、人数、预留名额、地点、校区、分类标识;responsibleList 每项包含 userId、mobile。
|
||||
* service 按登录角色决定允许保存的字段,返回 Result,code=0 表示保存成功,msg 为操作结果。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("trainSignUp.manage")
|
||||
@SLog(tag = "品牌活动-子活动管理", msg = "保存子活动配置")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result saveManagedCourse(TrainSignUpCourse course, @Param("updateCourseTimes") boolean updateCourseTimes) {
|
||||
trainSignUpActivityManageService.saveManagedCourse(course, updateCourseTimes);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("分页查询")
|
||||
@SaCheckPermission("trainSignUp.manage")
|
||||
@@ -110,31 +139,22 @@ public class TrainSignUpManageController {
|
||||
@ApiOperation("活动删除")
|
||||
@SaCheckPermission("trainSignUp.manage")
|
||||
@SLog(tag = "品牌活动-活动管理", msg = "删除活动")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result onDelete(String id) {
|
||||
if (dao.fetch(Sys_home_template.class, id) != null) {
|
||||
return Result.error("该活动已设为模板,请先取消模板后再删除");
|
||||
// id 为主活动ID;service 在同一事务中清理子活动负责人及报名关联。
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return Result.error("活动ID不能为空");
|
||||
}
|
||||
Trans.exec(() -> {
|
||||
trainSignUpActivityManageService.delete(id);
|
||||
dao.clear(TrainSignUpCourse.class, Cnd.where("activityId", "=", id));
|
||||
dao.clear(TrainSignUpActivityCourse.class, Cnd.where("activityId", "=", id));
|
||||
dao.clear(TrainSignUpUser.class, Cnd.where("activityId", "=", id));
|
||||
dao.clear(TrainSignUpUserCourse.class, Cnd.where("activityId", "=", id));
|
||||
dao.clear(TrainSignUpActivity.class, Cnd.where("id", "=", id));
|
||||
dao.clear(TrainSignUpTypeLimit.class, Cnd.where("activityId", "=", id));
|
||||
dao.delete(Sys_home_activity.class, id);
|
||||
});
|
||||
trainSignUpActivityManageService.deleteActivity(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("活动状态变更")
|
||||
@SaCheckPermission("trainSignUp.manage")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result activityStatusChange(TrainSignUpActivity activity) {
|
||||
trainSignUpActivityManageService.updateActivityStatus(activity);
|
||||
dao.update(Sys_home_activity.class,
|
||||
Chain.make("enable", !activity.isDisabled()),
|
||||
Cnd.where("id", "=", activity.getId()));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@@ -185,11 +205,17 @@ public class TrainSignUpManageController {
|
||||
return Result.success().addData(dataMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收主活动及 courseList 子活动列表;每个 responsibleList 元素传 userId、mobile。
|
||||
* responsibleList 为 null 保留历史联系人,空列表清空,多人关系由 service 校验并保存。
|
||||
* 返回 Result,code=0 表示主活动、子活动和负责人已在同一事务中保存成功。
|
||||
*/
|
||||
@At
|
||||
@Ok("json:full")
|
||||
@ApiOperation("品牌活动新增/修改")
|
||||
@SaCheckPermission(value = {"trainSignUp.manage", "trainSignUp.applyActivity"}, mode = SaMode.OR)
|
||||
@SLog(tag = "品牌活动-活动管理", msg = "新增/修改活动")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doHandle(TrainSignUpActivity activity) {
|
||||
if (StrUtil.isBlank(activity.getId())) {
|
||||
cleanNewActivityData(activity);
|
||||
|
||||
+61
-212
@@ -1,55 +1,26 @@
|
||||
package com.budwk.app.zhgh.activity.trainSignUp.controller.statistics;
|
||||
|
||||
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
|
||||
import cn.afterturn.easypoi.excel.export.ExcelExportService;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.sys.models.Sys_file;
|
||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityService;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||
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;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
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.json.Json;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
|
||||
/**
|
||||
* @author zxy
|
||||
@@ -63,12 +34,8 @@ import java.util.zip.ZipOutputStream;
|
||||
@At("/platform/trainSignUp/statistics")
|
||||
public class TrainSignUpActivityStatisticsController {
|
||||
|
||||
@Inject
|
||||
private TrainSignUpActivityService trainSignUpActivityManageService;
|
||||
@Inject
|
||||
private TrainSignUpActivityStatisticsService trainSignUpActivityStatisticsService;
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At("")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
@@ -76,231 +43,113 @@ public class TrainSignUpActivityStatisticsController {
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||
* @param activityId 主活动ID,为空时返回空分页
|
||||
* @return Result.data 为 Pagination,list 和 totalCount 均受当前登录人的负责人范围限制
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("分页查询")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result pageData(PageForm pageForm,
|
||||
@Param(value = "activityId") String activityId) {
|
||||
public Result pageData(PageForm pageForm, @Param("activityId") String activityId) {
|
||||
Pagination pagination = trainSignUpActivityStatisticsService.pageData(pageForm, activityId);
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动list
|
||||
*
|
||||
* @param year 年度
|
||||
* @return
|
||||
* @param year 查询年度,为空时不限制年度
|
||||
* @return Result.data 为可管理的主活动列表,id/activityName 用于活动下拉框
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("活动列表")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result activityList(@Param(value = "year") Integer year) {
|
||||
List<TrainSignUpActivity> list = dao.query(TrainSignUpActivity.class, Cnd.NEW().andEX("year", "=", year).desc("activityStartTime"));
|
||||
if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CHAIRMAN.name(), RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name())) {
|
||||
// 获取当前登录用户的工会
|
||||
String unionId = SecurityUtil.getUnionId();
|
||||
// 获取当前登录用户的社团
|
||||
List<String> clubIds = SecurityUtil.getClubIds();
|
||||
|
||||
List<TrainSignUpActivity> result = new ArrayList<>();
|
||||
for (TrainSignUpActivity activity : list) {
|
||||
if(AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
|
||||
if(activity.getHostUnits().contains(unionId) || activity.getHelpUnits().contains(unionId)) {
|
||||
result.add(activity);
|
||||
}
|
||||
}
|
||||
if(AuthUtil.hasRoleOr(RoleConstant.CLUB_PRESIDENT.name(), RoleConstant.CLUB_OPERATOR.name())) {
|
||||
if(clubIds.stream().anyMatch(activity.getHostUnits()::contains) || clubIds.stream().anyMatch(activity.getHelpUnits()::contains)) {
|
||||
result.add(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.success(result);
|
||||
}
|
||||
return Result.success(list);
|
||||
public Result activityList(@Param("year") Integer year) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.activityList(year, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 培训班报名人员list
|
||||
*
|
||||
* @param courseId
|
||||
* @return
|
||||
* @param courseId 子活动ID,必须在当前登录人的管理范围内
|
||||
* @return Result.data 为报名人员数组,包含姓名、工号、单位、联系方式、报名状态及动态报名字段
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("报名人员列表")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result registerUserList(@Param(value = "courseId") String courseId) {
|
||||
public Result registerUserList(@Param("courseId") String courseId) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.registerUserList(courseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param courseId 子活动ID,必须在当前登录人的管理范围内
|
||||
* @return Result.data 为动态列配置数组,label 为列标题,prop 为报名数据字段名
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("报名动态列")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Object getTaleColumnInfo(@Param(value = "courseId") String courseId) {
|
||||
public Object getTaleColumnInfo(@Param("courseId") String courseId) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.getTaleColumnInfo(courseId));
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("获取时间信息")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result selectTimes(@Param("courseId") String courseId) {
|
||||
List<TrainSignUpActivityCourse> activityCourses = dao.query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", courseId).asc("courseStartTime"));
|
||||
return Result.success(activityCourses);
|
||||
}
|
||||
/**
|
||||
* @param courseId 子活动ID,必须在当前登录人的管理范围内
|
||||
* @return Result.data 为时段数组,id 为时段ID,courseStartTime/courseEndTime 为起止时间
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("获取时间信息")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result selectTimes(@Param("courseId") String courseId) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.selectTimes(courseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 培训班上课签到信息
|
||||
*
|
||||
* @param courseId
|
||||
* @return
|
||||
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||
* @param courseId 有管理权限的子活动ID
|
||||
* @param timeId 子活动时段ID
|
||||
* @return Result.data 为签到分页,list 中 isAttend/attendTime 为签到状态及时间
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("获取签到信息")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result getSignInfo(PageForm pageForm,
|
||||
@Param("courseId") String courseId,
|
||||
String timeId) {
|
||||
public Result getSignInfo(PageForm pageForm, @Param("courseId") String courseId, String timeId) {
|
||||
return Result.success(trainSignUpActivityStatisticsService.getSignInfo(pageForm, courseId, timeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param courseId 有管理权限的子活动ID
|
||||
* @param openOtherUnion true 开放给其他工会报名,false 关闭
|
||||
* @return Result,code=0 表示状态已更新
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("开放报名")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result signChange(@Param("courseId") String courseId, @Param("openOtherUnion") Boolean openOtherUnion) {
|
||||
dao.update(TrainSignUpCourse.class, Chain.make("openOtherUnion", openOtherUnion)
|
||||
, Cnd.where("id", "=", courseId));
|
||||
trainSignUpActivityStatisticsService.signChange(courseId, openOtherUnion);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("设置签到")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public Result adjustSign(@Param("id") String id) {
|
||||
TrainSignUpUserCourse userCourse = dao.fetch(TrainSignUpUserCourse.class, id);
|
||||
if(userCourse.isAttend()) {
|
||||
userCourse.setAttend(false);
|
||||
userCourse.setAttendTime(null);
|
||||
} else {
|
||||
userCourse.setAttend(true);
|
||||
userCourse.setAttendTime(userCourse.getCourseStartTime());
|
||||
}
|
||||
dao.update(userCourse);
|
||||
return Result.success();
|
||||
}
|
||||
/**
|
||||
* @param id 签到记录ID,service 根据记录所属子活动校验当前登录人的权限
|
||||
* @return Result,code=0 表示签到状态已切换
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("设置签到")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result adjustSign(@Param("id") String id) {
|
||||
trainSignUpActivityStatisticsService.adjustSign(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activityId 主活动ID,仅导出当前登录人有管理权限的子活动报名记录
|
||||
* @param response 文件下载响应,输出 .xlsx 文件而非 JSON
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
@ApiOperation("导出签到名单")
|
||||
@SaCheckPermission("trainSignUp.statistics")
|
||||
public void exportSignUser(@Param(value = "activityId") String activityId,
|
||||
HttpServletResponse response) throws IOException {
|
||||
try {
|
||||
TrainSignUpActivity activity = dao.fetch(TrainSignUpActivity.class, activityId);
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
ts.*,
|
||||
CONCAT(DATE_FORMAT(ac.courseStartTime, '%H:%i:%s'),'至',DATE_FORMAT(ac.courseEndTime, '%H:%i:%s')) AS courseTime,
|
||||
u.username,
|
||||
u.loginname,
|
||||
u.sex,
|
||||
ifnull(ts.mobile, u.mobile) as mobile,
|
||||
u.birthday,
|
||||
tsc.courseName
|
||||
FROM
|
||||
train_sign_up_user ts
|
||||
left join train_sign_up_activity_course ac on ts.activityCourseId = ac.id
|
||||
left join `vw_user` u on u.id = ts. userId
|
||||
left join train_sign_up_course tsc on tsc.id = ts.courseId
|
||||
WHERE
|
||||
ts.activityId = @activityId
|
||||
""").setParam("activityId", activityId);
|
||||
List<NutMap> userList = trainSignUpActivityManageService.listMap(sql);
|
||||
|
||||
List<TrainSignUpCourse> courseList = dao.query(TrainSignUpCourse.class, Cnd.where("activityId", "=", activityId));
|
||||
|
||||
List<TrainSignUpType> trainSignUpTypeList = dao.query(TrainSignUpType.class, Cnd.NEW());
|
||||
dao.fetchLinks(trainSignUpTypeList, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
||||
Map<String, TrainSignUpType> typeMap = trainSignUpTypeList.stream().collect(Collectors.toMap(TrainSignUpType::getId, o -> o));
|
||||
|
||||
List<Map<String, Object>> sheetsList = new ArrayList<>();
|
||||
|
||||
List<ExcelExportEntity> excelCommonExportEntity = new ArrayList<>();
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("姓名", "username", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("工号", "loginname", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("单位", "unitName", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("分工会", "unionName", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("性别", "sex", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("手机号", "mobile", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("生日", "birthday", 20));
|
||||
excelCommonExportEntity.add(new ExcelExportEntity("报名时段", "courseTime", 20));
|
||||
|
||||
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);
|
||||
|
||||
List<ExcelExportEntity> currentEntities = new ArrayList<>();
|
||||
currentEntities.addAll(excelCommonExportEntity);
|
||||
|
||||
TrainSignUpType signUpType = typeMap.get(c.getCourseType());
|
||||
if (Lang.isNotEmpty(signUpType.getTrainMobileSignColumnList())) {
|
||||
for (TrainMobileSignColumn column : signUpType.getTrainMobileSignColumnList()) {
|
||||
ExcelExportEntity entity = new ExcelExportEntity();
|
||||
entity.setName(column.getColumnName());
|
||||
entity.setKey(column.getColumnCode());
|
||||
entity.setWidth(20);
|
||||
if ("FILE".equals(column.getColumnFormType())) {
|
||||
entity.setType(2);
|
||||
entity.setExportImageType(2);
|
||||
}
|
||||
currentEntities.add(entity);
|
||||
}
|
||||
}
|
||||
for (NutMap userSignData : v) {
|
||||
String mobileColumnsValueStr = userSignData.getString("mobileColumnsValue");
|
||||
if (StrUtil.isNotBlank(mobileColumnsValueStr)) {
|
||||
List<NutMap> mobileColumnsValue = Json.fromJsonAsList(NutMap.class, mobileColumnsValueStr);
|
||||
for (NutMap cv : mobileColumnsValue) {
|
||||
if (!"FILE".equals(cv.getString("columnFormType"))) {
|
||||
userSignData.put(cv.getString("columnCode"), cv.getString("columnValue"));
|
||||
} else {
|
||||
if (StrUtil.isNotBlank(cv.getString("columnValue"))) {
|
||||
List<NutMap> columnValue = cv.getAsList("columnValue", NutMap.class);
|
||||
if (columnValue.size() == 1) {
|
||||
NutMap sysFile = columnValue.get(0);
|
||||
Sys_file file = dao.fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", sysFile.get("url")));
|
||||
byte[] imageBytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
|
||||
if (imageBytes.length > 0) {
|
||||
userSignData.put(cv.getString("columnCode"), imageBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> userExportMap = new HashMap<>();
|
||||
userExportMap.put("name", k);
|
||||
userExportParams.setType(ExcelType.HSSF);
|
||||
userExportMap.put("title", userExportParams);
|
||||
userExportMap.put("entity", currentEntities);
|
||||
userExportMap.put("data", v);
|
||||
|
||||
sheetsList.add(userExportMap);
|
||||
}
|
||||
|
||||
Workbook workbook = new HSSFWorkbook();
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
public void exportSignUser(@Param("activityId") String activityId, HttpServletResponse response) throws IOException {
|
||||
NutMap exportData = trainSignUpActivityStatisticsService.buildSignUserExport(activityId);
|
||||
CommonDownloadUtil.download(exportData.getString("fileName"), exportData.getAs("workbook", Workbook.class), response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,13 @@ public class TrainSignUpCourse extends BaseModel implements Serializable {
|
||||
@Many(field = "courseId")
|
||||
private List<TrainSignUpActivityCourse> courseTimeList;
|
||||
|
||||
/**
|
||||
* 完整负责人关系。null 表示旧数据尚未设置,空列表表示主动清空负责人。
|
||||
* 关系单独保存,后续可按 userId 精确查询,无需解析姓名或 JSON。
|
||||
*/
|
||||
@Many(field = "courseId")
|
||||
private List<TrainSignUpCourseResponsible> responsibleList;
|
||||
|
||||
//已报人数
|
||||
private Integer hasRegisterNum;
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.budwk.app.zhgh.activity.trainSignUp.models;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 子活动负责人关系:一人一条记录,业务控制应以 userId 为准,姓名仅用于展示。
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Table("train_sign_up_course_responsible")
|
||||
@Comment("品牌活动子活动负责人")
|
||||
@TableIndexes({
|
||||
@Index(name = "TSU_RESP_COURSE_USER", fields = {"courseId", "userId"}, unique = true),
|
||||
@Index(name = "TSU_RESP_USER_ACTIVITY", fields = {"userId", "activityId"}, unique = false),
|
||||
@Index(name = "TSU_RESP_ACTIVITY", fields = {"activityId"}, unique = false)
|
||||
})
|
||||
public class TrainSignUpCourseResponsible implements Serializable {
|
||||
|
||||
@Name
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@Comment("主活动ID,由后端根据所属子活动设置")
|
||||
private String activityId;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@Comment("子活动ID,由后端在子活动保存后设置")
|
||||
private String courseId;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@Comment("系统用户ID,用于负责人查询和业务控制")
|
||||
private String userId;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
@Comment("负责人姓名,由后端读取系统用户信息")
|
||||
private String username;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
@Comment("子活动联系号码,可填写手机号或办公电话")
|
||||
private String mobile;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.INT)
|
||||
@Comment("负责人顺序,第一位兼容原有联系人展示")
|
||||
private int orderNum;
|
||||
|
||||
/** 工号仅用于多选回显,不重复存储系统用户资料。 */
|
||||
private String loginname;
|
||||
}
|
||||
+36
@@ -20,6 +20,35 @@ import java.util.List;
|
||||
*/
|
||||
public interface TrainSignUpActivityService extends BaseService<TrainSignUpActivity> {
|
||||
|
||||
/** 当前登录人是否具有活动关键内容编辑权限(三类管理员任意一种)。 */
|
||||
boolean canEditActivityBasic();
|
||||
|
||||
/**
|
||||
* 查询当前主活动中可管理的子活动,普通用户仅返回本人负责的记录。
|
||||
* @param pageForm 页码、每页条数;排序固定使用子活动序号
|
||||
* @param activityId 主活动ID,必填
|
||||
* @param courseName 子活动名称,可为空,按包含匹配
|
||||
* @return 分页对象,list 包含子活动字段、courseTypeName、responsibleNames、courseTimeName
|
||||
*/
|
||||
Pagination courseManagePageData(PageForm pageForm, String activityId, String courseName);
|
||||
|
||||
/**
|
||||
* 读取单个子活动及表单所需选项,同时校验当前登录人的负责人范围。
|
||||
* @param activityId 主活动ID,必须与子活动实际归属一致
|
||||
* @param courseId 子活动ID,必填
|
||||
* @return course 为子活动及负责人、时段;activityTime 为主活动日期范围;
|
||||
* canEditBasic 为关键内容编辑权限,courseTypes 为管理员使用的类型选项
|
||||
*/
|
||||
NutMap courseManageDetail(String activityId, String courseId);
|
||||
|
||||
/**
|
||||
* 保存单个子活动,普通负责人仅更新签到字段,管理员可更新基本信息及负责人。
|
||||
* @param course id、activityId 必填;签到参数为 isMobileSign、signType、signUnit、radius、
|
||||
* courseLocationCoordinates、transPosition,坐标均按纬度、经度顺序传入
|
||||
* @param updateCourseTimes 仅管理员实际修改时段后传 true;false 不改动任何时段及签到记录
|
||||
*/
|
||||
void saveManagedCourse(TrainSignUpCourse course, boolean updateCourseTimes);
|
||||
|
||||
/**
|
||||
* 添加活动
|
||||
*
|
||||
@@ -35,6 +64,13 @@ public interface TrainSignUpActivityService extends BaseService<TrainSignUpActiv
|
||||
*/
|
||||
void edit(TrainSignUpActivity activity);
|
||||
|
||||
/**
|
||||
* 删除活动及其子活动、负责人关系、报名和首页记录。
|
||||
*
|
||||
* @param id 主活动ID;已经设为模板的活动需先取消模板
|
||||
*/
|
||||
void deleteActivity(String id);
|
||||
|
||||
/**
|
||||
* 更新活动状态
|
||||
*
|
||||
|
||||
+96
-16
@@ -3,6 +3,8 @@ package com.budwk.app.zhgh.activity.trainSignUp.service;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpActivity;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpActivityCourse;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpUser;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
@@ -17,44 +19,122 @@ import java.util.Map;
|
||||
public interface TrainSignUpActivityStatisticsService extends BaseService<TrainSignUpUser> {
|
||||
|
||||
/**
|
||||
* 统计分页
|
||||
* 查询当前用户可管理的主活动,只包含至少一个本人负责子活动的主活动。
|
||||
* 系统管理员、校工会管理员、校工会活动管理员不受负责人范围限制,仍保留年度及启用状态条件。
|
||||
*
|
||||
* @param pageForm
|
||||
* @param activityId
|
||||
* @return
|
||||
* @param year 年度,为空时不限制年度
|
||||
* @param enabledOnly true 仅查询已启用活动,人员调整页面使用;false 保留统计页面原范围
|
||||
* @return 主活动实体列表;没有负责记录时返回空列表
|
||||
*/
|
||||
List<TrainSignUpActivity> activityList(Integer year, boolean enabledOnly);
|
||||
|
||||
/**
|
||||
* 查询人员调整的目标子活动,限制为当前用户负责的子活动。
|
||||
*
|
||||
* @param activityId 主活动ID,为空时返回空列表
|
||||
* @return 子活动列表,id/courseName 为标识及名称,registerNum/hasWaitingNum 为已报及候补人数
|
||||
*/
|
||||
List<NutMap> getCourse(String activityId);
|
||||
|
||||
/**
|
||||
* 查询有管理权限的子活动时段。
|
||||
*
|
||||
* @param courseId 子活动ID,必须存在且在当前用户的管理范围内
|
||||
* @return 时段实体列表,id 为时段ID,courseStartTime/courseEndTime 为起止时间
|
||||
*/
|
||||
List<TrainSignUpActivityCourse> selectTimes(String courseId);
|
||||
|
||||
/**
|
||||
* 修改子活动对外报名状态,只允许操作本人负责的子活动或由全量管理员操作。
|
||||
*
|
||||
* @param courseId 子活动ID
|
||||
* @param openOtherUnion true 开放给其他工会报名,false 关闭,不能传 null
|
||||
*/
|
||||
void signChange(String courseId, Boolean openOtherUnion);
|
||||
|
||||
/**
|
||||
* 切换签到状态,依据签到记录实际所属子活动校验负责人权限。
|
||||
*
|
||||
* @param id TrainSignUpUserCourse 签到记录ID
|
||||
*/
|
||||
void adjustSign(String id);
|
||||
|
||||
/**
|
||||
* 迁移报名记录及签到时段,来源、目标子活动均需属于指定主活动且在当前用户管理范围内。
|
||||
*
|
||||
* @param activityId 主活动ID
|
||||
* @param oldCourseId 原子活动ID
|
||||
* @param newCourseId 目标子活动ID
|
||||
* @param userId 被调整的报名人员用户ID,不能用来指定操作人的身份
|
||||
*/
|
||||
void adjust(String activityId, String oldCourseId, String newCourseId, String userId);
|
||||
|
||||
/**
|
||||
* 删除指定子活动的报名及签到记录,先验证实际子活动归属和当前登录人的负责人权限。
|
||||
*
|
||||
* @param activityId 主活动ID
|
||||
* @param courseId 子活动ID
|
||||
* @param userId 被删除的报名人员用户ID
|
||||
*/
|
||||
void deleteSignUser(String activityId, String courseId, String userId);
|
||||
|
||||
/**
|
||||
* 构建当前用户管理范围内的报名人员 Excel,不能导出同一主活动下其他人的子活动。
|
||||
*
|
||||
* @param activityId 主活动ID
|
||||
* @return 内部 NutMap,fileName 为 .xlsx 文件名,workbook 为 XSSF Workbook,由 controller 输出并关闭
|
||||
*/
|
||||
NutMap buildSignUserExport(String activityId);
|
||||
|
||||
/**
|
||||
* 按当前登录人的负责人范围查询子活动分页,系统管理员、校工会管理员、校工会活动管理员可查询全部。
|
||||
*
|
||||
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||
* @param activityId 主活动ID,为空时返回空分页
|
||||
* @return Pagination,list 为子活动数据,totalCount 为相同负责人范围下的记录总数
|
||||
*/
|
||||
Pagination pageData(PageForm pageForm, String activityId);
|
||||
|
||||
/**
|
||||
* 该课程下的报名人员信息
|
||||
* 查询有管理权限的子活动报名明细,并展开动态报名字段。
|
||||
*
|
||||
* @param courseId
|
||||
* @return
|
||||
* @param courseId 子活动ID,不能为空,必须在当前登录人的管理范围内
|
||||
* @return 报名人员数组,包含姓名、工号、单位、联系方式、state 报名状态及动态字段
|
||||
*/
|
||||
List<NutMap> registerUserList(String courseId);
|
||||
|
||||
/**
|
||||
* @param courseId 有管理权限的子活动ID
|
||||
* @return 动态列配置数组,label 为列标题,prop 为报名明细对应字段名
|
||||
*/
|
||||
List<NutMap> getTaleColumnInfo(String courseId);
|
||||
|
||||
/**
|
||||
* 该课程下的报名人员信息
|
||||
* 按工会、单位和姓名/工号搜索有管理权限的子活动报名人员。
|
||||
*
|
||||
* @param courseId
|
||||
* @return
|
||||
* @param courseId 有管理权限的子活动ID,不能为空
|
||||
* @param unionId 工会ID,可为空
|
||||
* @param unitId 单位ID,可为空
|
||||
* @param searchName 保留现有接口参数,姓名和工号统一使用 searchKeyword 搜索
|
||||
* @param searchKeyword 姓名或工号关键字,可为空
|
||||
* @return 报名人员数组,userId 为报名人员用户ID,state 为报名状态
|
||||
*/
|
||||
List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword);
|
||||
|
||||
/**
|
||||
* 获取每个课程的签到情况
|
||||
* 查询有管理权限的子活动指定时段签到分页。
|
||||
*
|
||||
* @param courseId
|
||||
* @return k->每个培训班每节课的上课时间 v->上课记录list
|
||||
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
|
||||
* @param courseId 有管理权限的子活动ID
|
||||
* @param timeId 子活动时段ID
|
||||
* @return Pagination,list 为签到人员,isAttend/attendTime 为签到状态及时间
|
||||
*/
|
||||
Pagination getSignInfo(PageForm pageForm, String courseId, String timeId);
|
||||
|
||||
/**
|
||||
* 报名人员list 导出
|
||||
* @param activityId
|
||||
* @return
|
||||
* 查询主活动下可管理子活动的基础报名导出数据。
|
||||
* @param activityId 主活动ID
|
||||
* @return 报名人员数组,包含姓名、工号、联系方式及子活动名称,不包含管理范围以外的子活动
|
||||
*/
|
||||
List<NutMap> baoMingUserList(String activityId);
|
||||
|
||||
|
||||
+405
-1
@@ -1,13 +1,18 @@
|
||||
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.sys.models.Sys_home_activity;
|
||||
import com.budwk.app.sys.models.Sys_home_template;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.sys.views.View_user;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.zhgh.activity.family.models.FamilyActivity;
|
||||
import com.budwk.app.zhgh.activity.family.models.FamilyCourse;
|
||||
import com.budwk.app.zhgh.activity.family.models.FamilyType;
|
||||
@@ -51,9 +56,227 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEditActivityBasic() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||
RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||
}
|
||||
|
||||
/** 整场编辑、删除和启停均会修改关键内容,不能仅凭页面菜单权限放行。 */
|
||||
private void requireActivityBasicPermission() {
|
||||
if (!canEditActivityBasic()) {
|
||||
throw new BaseException("活动关键信息仅管理员可修改,请通过子活动管理维护签到设置");
|
||||
}
|
||||
}
|
||||
|
||||
/** 每次读写都重新检查实际归属和负责人关系,禁止信任前端传入的角色或负责人名单。 */
|
||||
private TrainSignUpCourse requireManagedCourse(String activityId, String courseId) {
|
||||
if (StrUtil.isBlank(activityId) || StrUtil.isBlank(courseId)) {
|
||||
throw new BaseException("主活动和子活动ID不能为空");
|
||||
}
|
||||
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
||||
if (course == null || !activityId.equals(course.getActivityId()) || fetch(activityId) == null) {
|
||||
throw new BaseException("子活动不存在或不属于当前活动");
|
||||
}
|
||||
if (!canEditActivityBasic()) {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (StrUtil.isBlank(userId) || dao().count(TrainSignUpCourseResponsible.class,
|
||||
Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId)
|
||||
.and("userId", "=", userId)) == 0) {
|
||||
throw new BaseException("无权管理该子活动,请检查负责人设置");
|
||||
}
|
||||
}
|
||||
return course;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pagination courseManagePageData(PageForm pageForm, String activityId, String courseName) {
|
||||
if (StrUtil.isBlank(activityId) || fetch(activityId) == null) {
|
||||
throw new BaseException("主活动不存在");
|
||||
}
|
||||
Cnd cnd = Cnd.where("course.activityId", "=", activityId).and(Cnd.likeEX("course.courseName", courseName));
|
||||
if (!canEditActivityBasic()) {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
List<TrainSignUpCourseResponsible> relations = StrUtil.isBlank(userId) ? Collections.emptyList()
|
||||
: dao().query(TrainSignUpCourseResponsible.class,
|
||||
Cnd.where("activityId", "=", activityId).and("userId", "=", userId));
|
||||
if (relations.isEmpty()) {
|
||||
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>());
|
||||
}
|
||||
// 范围条件在分页前进入数据库查询,记录数和分页总数使用同一负责人范围。
|
||||
cnd.and("course.id", "in", relations.stream().map(TrainSignUpCourseResponsible::getCourseId).toList());
|
||||
}
|
||||
cnd.asc("course.orderNum").asc("course.id");
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT course.*, type.typeName AS courseTypeName,
|
||||
(SELECT GROUP_CONCAT(r.username ORDER BY r.orderNum SEPARATOR '、')
|
||||
FROM train_sign_up_course_responsible r
|
||||
WHERE r.courseId=course.id AND r.activityId=course.activityId) AS responsibleNames,
|
||||
(SELECT GROUP_CONCAT(CONCAT(DATE_FORMAT(t.courseStartTime, '%Y-%m-%d %H:%i'),
|
||||
' 至 ', DATE_FORMAT(t.courseEndTime, '%H:%i')) ORDER BY t.courseStartTime SEPARATOR ';')
|
||||
FROM train_sign_up_activity_course t WHERE t.courseId=course.id) AS courseTimeName
|
||||
FROM train_sign_up_course course
|
||||
LEFT JOIN train_sign_up_type type ON type.id=course.courseType
|
||||
$condition
|
||||
""");
|
||||
sql.setCondition(cnd);
|
||||
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NutMap courseManageDetail(String activityId, String courseId) {
|
||||
TrainSignUpCourse course = requireManagedCourse(activityId, courseId);
|
||||
fillCourseResponsibles(List.of(course));
|
||||
dao().fetchLinks(course, "courseTimeList", Cnd.NEW().asc("courseStartTime"));
|
||||
TrainSignUpActivity activity = fetch(activityId);
|
||||
NutMap data = Lang.obj2nutmap(course);
|
||||
// 时间编辑组件使用日期和 HH:mm 两部分;签到记录仍由 service 按完整日期时间保存。
|
||||
List<NutMap> times = course.getCourseTimeList().stream().map(time -> Lang.obj2nutmap(time)
|
||||
.setv("courseDate", DateUtil.formatDate(time.getCourseDate()))
|
||||
.setv("courseStartTime", DateUtil.format(time.getCourseStartTime(), "HH:mm"))
|
||||
.setv("courseEndTime", DateUtil.format(time.getCourseEndTime(), "HH:mm"))).toList();
|
||||
data.setv("courseTimeList", times).setv("setUpCourseData",
|
||||
times.stream().map(time -> time.getString("courseDate")).distinct().toList());
|
||||
TrainSignUpType type = StrUtil.isBlank(course.getCourseType()) ? null
|
||||
: dao().fetch(TrainSignUpType.class, course.getCourseType());
|
||||
data.setv("courseTypeName", type == null ? "" : type.getTypeName());
|
||||
List<String> activityTime = activity.getActivityStartTime() == null || activity.getActivityEndTime() == null
|
||||
? Collections.emptyList() : List.of(DateUtil.formatDateTime(activity.getActivityStartTime()),
|
||||
DateUtil.formatDateTime(activity.getActivityEndTime()));
|
||||
return NutMap.NEW().setv("course", data).setv("activityTime", activityTime)
|
||||
.setv("canEditBasic", canEditActivityBasic())
|
||||
.setv("courseTypes", canEditActivityBasic() ? dao().query(TrainSignUpType.class, Cnd.NEW()) : Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveManagedCourse(TrainSignUpCourse course, boolean updateCourseTimes) {
|
||||
if (course == null) {
|
||||
throw new BaseException("子活动信息不能为空");
|
||||
}
|
||||
TrainSignUpCourse stored = requireManagedCourse(course.getActivityId(), course.getId());
|
||||
boolean usesSignLocation = validateManagedSign(course);
|
||||
String fields = "isMobileSign";
|
||||
if (course.isMobileSign()) {
|
||||
fields += "|signType";
|
||||
}
|
||||
// 被扫或关闭签到时不更新隐藏配置,即使请求携带这些字段也保留数据库原值。
|
||||
if (usesSignLocation) {
|
||||
fields += "|signUnit|radius|courseLocationCoordinates|transPosition";
|
||||
}
|
||||
if (canEditActivityBasic()) {
|
||||
validateManagedBasic(course, stored);
|
||||
validateCourseResponsibles(List.of(course));
|
||||
fields += "|courseName|courseType|coursePeopleNumber|courseReservedNumber|courseLocation|campus|assort";
|
||||
if (course.getResponsibleList() != null) {
|
||||
fields += "|courseInstructor|courseInstructorMobile";
|
||||
}
|
||||
if (updateCourseTimes) {
|
||||
validateManagedTimes(course, fetch(stored.getActivityId()));
|
||||
fields += "|courseIsLimitApply";
|
||||
}
|
||||
} else if (updateCourseTimes) {
|
||||
throw new BaseException("活动时间仅管理员可修改");
|
||||
}
|
||||
// 明确列出允许更新的列,普通负责人提交其他字段也无法覆盖关键内容或其他子活动。
|
||||
if (dao().update(course, "^(" + fields + ")$") != 1) {
|
||||
throw new BaseException("子活动已变化,请刷新后重试");
|
||||
}
|
||||
if (canEditActivityBasic()) {
|
||||
saveCourseResponsibles(course);
|
||||
if (updateCourseTimes) {
|
||||
handleCourseTimes(fetch(stored.getActivityId()), course);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示扫码或 GPS 签到,需校验并更新时间、位置配置,默认误差0分钟、半径100米。
|
||||
* 被扫使用固定签到时间规则,关闭签到也不使用这些字段,返回 false 跳过校验且不更新原配置。
|
||||
*/
|
||||
private boolean validateManagedSign(TrainSignUpCourse course) {
|
||||
if (!course.isMobileSign()) {
|
||||
return false;
|
||||
}
|
||||
if (!List.of(1, 2, 3).contains(course.getSignType() == null ? 0 : course.getSignType())) {
|
||||
throw new BaseException("请选择有效的签到方式");
|
||||
}
|
||||
if (Objects.equals(course.getSignType(), 2)) {
|
||||
return false;
|
||||
}
|
||||
if (course.getSignUnit() == null) course.setSignUnit(0);
|
||||
if (course.getRadius() == null) course.setRadius(100);
|
||||
if (course.getSignUnit() < 0 || course.getRadius() <= 0) {
|
||||
throw new BaseException("签到时间误差不能为负数,签到半径必须大于0");
|
||||
}
|
||||
validateManagedPosition(course.getCourseLocationCoordinates());
|
||||
validateManagedPosition(course.getTransPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 地图坐标和签到坐标均为 [纬度, 经度],拒绝不完整、非有限值及超出地理范围的坐标。 */
|
||||
private void validateManagedPosition(List<Double> point) {
|
||||
if (point == null || point.size() != 2 || point.stream().anyMatch(value -> value == null || !Double.isFinite(value))
|
||||
|| Math.abs(point.get(0)) > 90 || Math.abs(point.get(1)) > 180) {
|
||||
throw new BaseException("请重新设置有效的签到点");
|
||||
}
|
||||
}
|
||||
|
||||
/** 管理员基本信息沿用原字段范围;人数为0表示不限制,已报名后禁止切换报名类型。 */
|
||||
private void validateManagedBasic(TrainSignUpCourse course, TrainSignUpCourse stored) {
|
||||
if (StrUtil.isBlank(course.getCourseName()) || course.getCourseName().length() > 50
|
||||
|| StrUtil.isBlank(course.getCourseLocation()) || course.getCourseLocation().length() > 50
|
||||
|| StrUtil.isBlank(course.getCampus()) || course.getCampus().length() > 32
|
||||
|| (course.getAssort() != null && course.getAssort().length() > 30)) {
|
||||
throw new BaseException("请填写有效的名称、校区、地点和分类标识");
|
||||
}
|
||||
if (StrUtil.isBlank(course.getCourseType()) || dao().fetch(TrainSignUpType.class, course.getCourseType()) == null) {
|
||||
throw new BaseException("请选择有效的子活动类型");
|
||||
}
|
||||
if (!Objects.equals(stored.getCourseType(), course.getCourseType())
|
||||
&& dao().count(TrainSignUpUser.class, Cnd.where("courseId", "=", stored.getId())) > 0) {
|
||||
throw new BaseException("已有报名记录,不能修改子活动类型");
|
||||
}
|
||||
int registered = StrUtil.isBlank(stored.getCourseType()) ? 0
|
||||
: statisticsService.queryCourseCount(stored.getId(), stored.getCourseType());
|
||||
if (course.getCoursePeopleNumber() < 0 || course.getCoursePeopleNumber() > 9999
|
||||
|| course.getCourseReservedNumber() < 0 || course.getCourseReservedNumber() > 1000
|
||||
|| (course.getCoursePeopleNumber() > 0 && Objects.equals(stored.getReserveMode(), 1)
|
||||
&& course.getCourseReservedNumber() + registered > course.getCoursePeopleNumber())) {
|
||||
throw new BaseException("人数或预留名额无效,不能低于已报名人数与预留名额之和");
|
||||
}
|
||||
}
|
||||
|
||||
/** 时段ID只能属于当前子活动;校验后再调用原有同步逻辑,保证变更及签到记录在同一事务中提交。 */
|
||||
private void validateManagedTimes(TrainSignUpCourse course, TrainSignUpActivity activity) {
|
||||
if (Lang.isEmpty(course.getCourseTimeList()) || activity.getActivityStartTime() == null || activity.getActivityEndTime() == null) {
|
||||
throw new BaseException("请先设置主活动日期及至少一个有效时段");
|
||||
}
|
||||
Set<String> ids = new HashSet<>();
|
||||
Set<String> existingIds = dao().query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", course.getId()))
|
||||
.stream().map(TrainSignUpActivityCourse::getId).collect(Collectors.toSet());
|
||||
for (TrainSignUpActivityCourse time : course.getCourseTimeList()) {
|
||||
if (time == null || time.getCourseDate() == null || time.getCourseStartTime() == null || time.getCourseEndTime() == null) {
|
||||
throw new BaseException("请完整填写活动日期及起止时间");
|
||||
}
|
||||
if (StrUtil.isNotBlank(time.getId()) && (!existingIds.contains(time.getId()) || !ids.add(time.getId()))) {
|
||||
throw new BaseException("活动时段无效或重复,请刷新后重试");
|
||||
}
|
||||
String date = DateUtil.formatDate(time.getCourseDate());
|
||||
Date start = DateUtil.parse(date + " " + DateUtil.format(time.getCourseStartTime(), "HH:mm"));
|
||||
Date end = DateUtil.parse(date + " " + DateUtil.format(time.getCourseEndTime(), "HH:mm"));
|
||||
if (!start.before(end) || start.before(activity.getActivityStartTime()) || end.after(activity.getActivityEndTime())
|
||||
|| (Boolean.TRUE.equals(course.getCourseIsLimitApply()) && (time.getCourseLimitNum() == null || time.getCourseLimitNum() < 1))) {
|
||||
throw new BaseException("活动时段必须在主活动范围内,结束晚于开始;限制人数时应填写正整数");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void add(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
||||
// 先校验所有负责人,任何一行存在无效用户或联系方式时均不写入活动。
|
||||
validateCourseResponsibles(activity.getCourseList());
|
||||
// 新建活动(包括从模板另存)统一归入活动列表,不能继承来源活动的模板状态。
|
||||
activity.setTemplateStatus(TrainSignUpActivity.TEMPLATE_STATUS_UNSET);
|
||||
dao().insert(activity);
|
||||
@@ -68,6 +291,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
v.setActivityId(activity.getId());
|
||||
v.setOpenOtherUnion(false);
|
||||
dao().insert(v);
|
||||
saveCourseResponsibles(v);
|
||||
this.setCourseTimeAndInsert(v);
|
||||
}
|
||||
|
||||
@@ -80,6 +304,22 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void edit(TrainSignUpActivity activity) {
|
||||
requireActivityBasicPermission();
|
||||
if (fetch(activity.getId()) == null) {
|
||||
throw new BaseException("活动不存在");
|
||||
}
|
||||
validateCourseResponsibles(activity.getCourseList());
|
||||
// 子活动只能属于当前活动,防止提交其他活动的ID覆盖其负责人关系。
|
||||
if (activity.getCourseList() != null) {
|
||||
for (TrainSignUpCourse course : activity.getCourseList()) {
|
||||
if (StrUtil.isNotBlank(course.getId())) {
|
||||
TrainSignUpCourse oldCourse = dao().fetch(TrainSignUpCourse.class, course.getId());
|
||||
if (oldCourse == null || !activity.getId().equals(oldCourse.getActivityId())) {
|
||||
throw new BaseException("子活动不存在或不属于当前活动,请刷新后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 1. 更新主活动
|
||||
update(activity);
|
||||
// 2. 处理类型限制
|
||||
@@ -90,7 +330,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
}
|
||||
// 3. 处理课程列表
|
||||
List<TrainSignUpCourse> courseList = activity.getCourseList();
|
||||
if (Lang.isEmpty(courseList)) {
|
||||
if (courseList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,12 +348,15 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
|
||||
Set<String> deletedIds = Sets.difference(oldIds, newIds).immutableCopy();
|
||||
if(Lang.isNotEmpty(deletedIds)) {
|
||||
// 删除子活动时同步清理负责人,避免后续按 userId 查到已删除的业务。
|
||||
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("courseId", "in", deletedIds));
|
||||
dao().clear(TrainSignUpCourse.class, Cnd.where("id", "in", deletedIds));
|
||||
}
|
||||
|
||||
for (TrainSignUpCourse course : courseList) {
|
||||
course.setActivityId(activity.getId());
|
||||
dao().insertOrUpdate(course);
|
||||
saveCourseResponsibles(course);
|
||||
|
||||
if (Lang.isNotEmpty(course.getCourseTimeList())) {
|
||||
handleCourseTimes(activity, course);
|
||||
@@ -128,6 +371,131 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验多选负责人并补齐可信姓名;前端仅需传 userId 和本次活动使用的 mobile。
|
||||
* null 保留历史联系人,空列表主动清空,同一子活动不允许重复选择同一 userId。
|
||||
*/
|
||||
private void validateCourseResponsibles(List<TrainSignUpCourse> courses) {
|
||||
if (courses == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> userIds = new HashSet<>();
|
||||
for (TrainSignUpCourse course : courses) {
|
||||
if (course == null) {
|
||||
throw new BaseException("子活动信息不能为空");
|
||||
}
|
||||
if (course.getResponsibleList() == null) {
|
||||
continue;
|
||||
}
|
||||
Set<String> courseUserIds = new HashSet<>();
|
||||
for (TrainSignUpCourseResponsible responsible : course.getResponsibleList()) {
|
||||
if (responsible == null || StrUtil.isBlank(responsible.getUserId())) {
|
||||
throw new BaseException("请从下拉框选择负责人");
|
||||
}
|
||||
String userId = responsible.getUserId().trim();
|
||||
if (userId.length() > 32 || !courseUserIds.add(userId)) {
|
||||
throw new BaseException("负责人ID无效或同一子活动重复设置了负责人");
|
||||
}
|
||||
String mobile = StrUtil.trim(responsible.getMobile());
|
||||
if (StrUtil.isBlank(mobile) || mobile.length() > 20) {
|
||||
throw new BaseException("请填写负责人联系方式,长度不能超过20个字符");
|
||||
}
|
||||
responsible.setUserId(userId).setMobile(mobile);
|
||||
userIds.add(userId);
|
||||
}
|
||||
}
|
||||
Map<String, Sys_user> users = userIds.isEmpty() ? Collections.emptyMap()
|
||||
: dao().query(Sys_user.class, Cnd.where("id", "in", userIds)).stream()
|
||||
.collect(Collectors.toMap(Sys_user::getId, user -> user));
|
||||
for (TrainSignUpCourse course : courses) {
|
||||
List<TrainSignUpCourseResponsible> responsibles = course.getResponsibleList();
|
||||
if (responsibles == null) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < responsibles.size(); i++) {
|
||||
TrainSignUpCourseResponsible responsible = responsibles.get(i);
|
||||
Sys_user user = users.get(responsible.getUserId());
|
||||
if (user == null || StrUtil.isBlank(user.getUsername())) {
|
||||
throw new BaseException("所选负责人不存在,请重新选择");
|
||||
}
|
||||
responsible.setUsername(user.getUsername()).setLoginname(user.getLoginname()).setOrderNum(i + 1);
|
||||
}
|
||||
// 原有报名和统计页面仍展示第一位联系人,完整姓名及多人信息保留在关系表。
|
||||
// 原姓名列宽为20,此处只限制兼容展示字段,避免长姓名导致数据库写入失败。
|
||||
course.setCourseInstructor(responsibles.isEmpty() ? "" : StrUtil.sub(responsibles.get(0).getUsername(), 0, 20));
|
||||
course.setCourseInstructorMobile(responsibles.isEmpty() ? "" : responsibles.get(0).getMobile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随子活动在同一事务中保存负责人,关系ID及归属由后端生成,模板复制不会复用旧关系。
|
||||
* null 表示未修改历史设置,不清理原有关系;空列表表示明确删除所有负责人。
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void saveCourseResponsibles(TrainSignUpCourse course) {
|
||||
if (course.getResponsibleList() == null) {
|
||||
return;
|
||||
}
|
||||
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("courseId", "=", course.getId()));
|
||||
for (TrainSignUpCourseResponsible responsible : course.getResponsibleList()) {
|
||||
responsible.setId(null).setActivityId(course.getActivityId()).setCourseId(course.getId());
|
||||
}
|
||||
if (!course.getResponsibleList().isEmpty()) {
|
||||
dao().insert(course.getResponsibleList());
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量回显负责人及工号,不按旧姓名猜测用户身份,也不对每个子活动重复查询用户。 */
|
||||
private void fillCourseResponsibles(List<TrainSignUpCourse> courses) {
|
||||
if (courses.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> courseIds = courses.stream().map(TrainSignUpCourse::getId).collect(Collectors.toList());
|
||||
List<TrainSignUpCourseResponsible> responsibles = dao().query(TrainSignUpCourseResponsible.class,
|
||||
Cnd.where("courseId", "in", courseIds).asc("orderNum"));
|
||||
Set<String> userIds = responsibles.stream().map(TrainSignUpCourseResponsible::getUserId).collect(Collectors.toSet());
|
||||
Map<String, Sys_user> users = userIds.isEmpty() ? Collections.emptyMap()
|
||||
: dao().query(Sys_user.class, Cnd.where("id", "in", userIds)).stream()
|
||||
.collect(Collectors.toMap(Sys_user::getId, user -> user));
|
||||
for (TrainSignUpCourseResponsible responsible : responsibles) {
|
||||
Sys_user user = users.get(responsible.getUserId());
|
||||
if (user != null) {
|
||||
responsible.setUsername(user.getUsername()).setLoginname(user.getLoginname());
|
||||
}
|
||||
}
|
||||
Map<String, List<TrainSignUpCourseResponsible>> byCourse = responsibles.stream()
|
||||
.collect(Collectors.groupingBy(TrainSignUpCourseResponsible::getCourseId));
|
||||
for (TrainSignUpCourse course : courses) {
|
||||
List<TrainSignUpCourseResponsible> list = byCourse.get(course.getId());
|
||||
// 旧活动保留 null,避免普通编辑保存时将未绑定用户的历史联系人清空。
|
||||
course.setResponsibleList(list != null ? list :
|
||||
(StrUtil.isNotBlank(course.getCourseInstructor()) || StrUtil.isNotBlank(course.getCourseInstructorMobile())
|
||||
? null : new ArrayList<>()));
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除主活动时在一个事务中清理负责人及原有业务关联,模板活动仍需先取消模板。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void deleteActivity(String id) {
|
||||
requireActivityBasicPermission();
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new BaseException("活动ID不能为空");
|
||||
}
|
||||
if (dao().fetch(Sys_home_template.class, id) != null) {
|
||||
throw new BaseException("该活动已设为模板,请先取消模板后再删除");
|
||||
}
|
||||
dao().clear(TrainSignUpCourseResponsible.class, Cnd.where("activityId", "=", id));
|
||||
dao().clear(TrainSignUpCourse.class, Cnd.where("activityId", "=", id));
|
||||
dao().clear(TrainSignUpActivityCourse.class, Cnd.where("activityId", "=", id));
|
||||
dao().clear(TrainSignUpUser.class, Cnd.where("activityId", "=", id));
|
||||
dao().clear(TrainSignUpUserCourse.class, Cnd.where("activityId", "=", id));
|
||||
dao().clear(TrainSignUpActivity.class, Cnd.where("id", "=", id));
|
||||
dao().clear(TrainSignUpTypeLimit.class, Cnd.where("activityId", "=", id));
|
||||
dao().delete(Sys_home_activity.class, id);
|
||||
}
|
||||
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void handleCourseTimes(TrainSignUpActivity activity, TrainSignUpCourse course) {
|
||||
String courseId = course.getId();
|
||||
String activityId = activity.getId();
|
||||
@@ -223,6 +591,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
return u;
|
||||
}
|
||||
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void setCourseTimeAndInsert(TrainSignUpCourse course) {
|
||||
course.getCourseTimeList().forEach(courseTime -> {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
@@ -250,7 +619,14 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void updateActivityStatus(TrainSignUpActivity activity) {
|
||||
requireActivityBasicPermission();
|
||||
if (StrUtil.isBlank(activity.getId()) || fetch(activity.getId()) == null) {
|
||||
throw new BaseException("活动不存在");
|
||||
}
|
||||
updateIgnoreNull(activity);
|
||||
// 活动启停及首页可见状态在同一个事务内更新,controller 仅负责接收参数和返回结果。
|
||||
dao().update(Sys_home_activity.class, Chain.make("enable", !activity.isDisabled()),
|
||||
Cnd.where("id", "=", activity.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -270,6 +646,7 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
activity.setCourseList(courseArray);
|
||||
|
||||
List<TrainSignUpCourse> courseList = activity.getCourseList();
|
||||
fillCourseResponsibles(courseList);
|
||||
|
||||
courseList.forEach(c -> {
|
||||
if (StrUtil.isNotBlank(c.getCourseType())) {
|
||||
@@ -285,9 +662,36 @@ public class TrainSignUpActivityServiceImpl extends BaseServiceImpl<TrainSignUpA
|
||||
return Lang.obj2nutmap(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* pageForm 为页码和每页条数,cnd 为已有活动筛选条件;返回原分页结构及活动列表。
|
||||
* list 中新增 canManageCourse 布尔值,仅非校工会管理角色且负责当前活动的用户为 true。
|
||||
* 该字段控制子活动入口显示,实际读写仍由 requireManagedCourse 校验负责人关系。
|
||||
*/
|
||||
@Override
|
||||
public Pagination pageData(PageForm pageForm, Cnd cnd) {
|
||||
Pagination pagination = listPageLinks(pageForm.getPageNumber(), pageForm.getPageSize(), cnd, "^(courseList)$");
|
||||
List<TrainSignUpActivity> activities = pagination.getList(TrainSignUpActivity.class);
|
||||
boolean canEditBasic = canEditActivityBasic();
|
||||
String userId = SecurityUtil.getUserId();
|
||||
Map<String, Set<String>> responsibleCourses = new HashMap<>();
|
||||
// 只批量查询当前页活动的本人负责人关系,避免逐行查询,也不把普通用户当作负责人。
|
||||
if (!canEditBasic && StrUtil.isNotBlank(userId) && !activities.isEmpty()) {
|
||||
List<String> activityIds = activities.stream().map(TrainSignUpActivity::getId).toList();
|
||||
List<TrainSignUpCourseResponsible> relations = dao().query(TrainSignUpCourseResponsible.class,
|
||||
Cnd.where("activityId", "in", activityIds).and("userId", "=", userId));
|
||||
for (TrainSignUpCourseResponsible relation : relations) {
|
||||
responsibleCourses.computeIfAbsent(relation.getActivityId(), key -> new HashSet<>())
|
||||
.add(relation.getCourseId());
|
||||
}
|
||||
}
|
||||
List<NutMap> rows = activities.stream().map(activity -> {
|
||||
Set<String> courseIds = responsibleCourses.getOrDefault(activity.getId(), Collections.emptySet());
|
||||
// 同时核对实际子活动,已删除或已迁出的子活动关系不提供入口;管理员优先走编辑。
|
||||
boolean canManageCourse = !canEditBasic && activity.getCourseList() != null
|
||||
&& activity.getCourseList().stream().anyMatch(course -> courseIds.contains(course.getId()));
|
||||
return Lang.obj2nutmap(activity).setv("canManageCourse", canManageCourse);
|
||||
}).toList();
|
||||
pagination.setList(rows);
|
||||
return pagination;
|
||||
}
|
||||
|
||||
|
||||
+342
-3
@@ -1,25 +1,42 @@
|
||||
package com.budwk.app.zhgh.activity.trainSignUp.service.impl;
|
||||
|
||||
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
|
||||
import cn.afterturn.easypoi.excel.export.ExcelExportService;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.sys.models.Sys_file;
|
||||
import com.budwk.app.sys.utils.SysFileMinIoUtil;
|
||||
import com.budwk.app.zhgh.activity.family.models.FamilyType;
|
||||
import com.budwk.app.zhgh.activity.family.models.FamilyUser;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.models.*;
|
||||
import com.budwk.app.zhgh.activity.trainSignUp.service.TrainSignUpActivityStatisticsService;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.Lang;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -37,8 +54,197 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/** 系统管理员、校工会管理员、校工会活动管理员任意一种角色可管理全部,其他角色按负责人控制。 */
|
||||
private boolean hasAllTrainSignUpScope() {
|
||||
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(),
|
||||
RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将负责人范围放入数据库查询,使分页数据与总数、活动下拉框和导出保持一致。
|
||||
* idField 仅由本 service 传入固定的表别名字段;操作人始终取登录 userId 并使用 SQL 参数绑定。
|
||||
* activityLevel=true 按主活动筛选,false 按子活动筛选;没有登录身份时返回空范围。
|
||||
*/
|
||||
private void applyResponsibleScope(Sql sql, String idField, boolean activityLevel) {
|
||||
if (hasAllTrainSignUpScope()) {
|
||||
sql.setSourceSql(sql.getSourceSql().replace("$responsibleScope", ""));
|
||||
return;
|
||||
}
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (StrUtil.isBlank(userId)) {
|
||||
sql.setSourceSql(sql.getSourceSql().replace("$responsibleScope", " AND 1=0 "));
|
||||
return;
|
||||
}
|
||||
// 同时关联实际子活动,已删除子活动或活动归属不一致的关系不能提供管理权限。
|
||||
String scope = " AND EXISTS (SELECT 1 FROM train_sign_up_course_responsible responsible "
|
||||
+ "INNER JOIN train_sign_up_course responsibleCourse ON responsibleCourse.id=responsible.courseId "
|
||||
+ "AND responsibleCourse.activityId=responsible.activityId WHERE "
|
||||
+ (activityLevel ? "responsibleCourse.activityId=" : "responsibleCourse.id=")
|
||||
+ idField + " AND responsible.userId=@responsibleUserId) ";
|
||||
// Nutz 不会解析 setVar 字符串中的 @参数,必须先替换原始SQL并重新解析,再绑定用户ID。
|
||||
sql.setSourceSql(sql.getSourceSql().replace("$responsibleScope", scope));
|
||||
sql.setParam("responsibleUserId", userId);
|
||||
}
|
||||
|
||||
/** 按实际子活动校验详情及操作权限;缺少ID不能省略查询条件后查询全部报名人员。 */
|
||||
private TrainSignUpCourse requireCourseScope(String courseId) {
|
||||
if (StrUtil.isBlank(courseId)) {
|
||||
throw new BaseException("子活动ID不能为空");
|
||||
}
|
||||
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
||||
if (course == null) {
|
||||
throw new BaseException("子活动不存在");
|
||||
}
|
||||
if (!hasAllTrainSignUpScope()) {
|
||||
String userId = SecurityUtil.getUserId();
|
||||
if (StrUtil.isBlank(userId) || dao().count(TrainSignUpCourseResponsible.class,
|
||||
Cnd.where("courseId", "=", courseId).and("activityId", "=", course.getActivityId())
|
||||
.and("userId", "=", userId)) == 0) {
|
||||
throw new BaseException("无权查看或操作该子活动,请检查负责人设置");
|
||||
}
|
||||
}
|
||||
return course;
|
||||
}
|
||||
|
||||
/** 主活动ID用于校验归属,不能将其他活动的子活动作为调整来源或目标。 */
|
||||
private void requireActivityMatch(String activityId, TrainSignUpCourse course) {
|
||||
if (StrUtil.isBlank(activityId) || !activityId.equals(course.getActivityId())) {
|
||||
throw new BaseException("子活动不属于当前主活动");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainSignUpActivity> activityList(Integer year, boolean enabledOnly) {
|
||||
// 可选条件也在创建SQL前确定,让年度、启用状态和负责人ID均由预编译参数绑定。
|
||||
Sql sql = Sqls.create("SELECT activity.* FROM train_sign_up_activity activity WHERE 1=1 "
|
||||
+ (year == null ? "" : " AND activity.year=@year ")
|
||||
+ (enabledOnly ? " AND activity.isDisabled=@disabled " : "")
|
||||
+ " $responsibleScope ORDER BY activity.activityStartTime DESC");
|
||||
sql.setParam("year", year);
|
||||
sql.setParam("disabled", false);
|
||||
applyResponsibleScope(sql, "activity.id", true);
|
||||
sql.setEntity(dao().getEntity(TrainSignUpActivity.class));
|
||||
sql.setCallback(Sqls.callback.entities());
|
||||
dao().execute(sql);
|
||||
return sql.getList(TrainSignUpActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> getCourse(String activityId) {
|
||||
if (StrUtil.isBlank(activityId)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT tsuc.id, tsuc.courseName, tsuc.coursePeopleNumber, tsuc.courseType,
|
||||
tsuc.courseLocation, tsuc.courseInstructor, tsuc.courseReservedNumber,
|
||||
tsuc.waitingNum, tsuc.reserveMode
|
||||
FROM train_sign_up_course tsuc
|
||||
WHERE tsuc.activityId=@activityId $responsibleScope
|
||||
ORDER BY tsuc.courseName ASC
|
||||
""");
|
||||
sql.setParam("activityId", activityId);
|
||||
applyResponsibleScope(sql, "tsuc.id", false);
|
||||
List<NutMap> courses = listMap(sql);
|
||||
courses.forEach(course -> {
|
||||
course.put("registerNum", queryCourseCount(course.getString("id"), course.getString("courseType")));
|
||||
course.put("hasWaitingNum", queryCourseWaitCount(course.getString("id"), course.getString("courseType")));
|
||||
});
|
||||
return courses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainSignUpActivityCourse> selectTimes(String courseId) {
|
||||
requireCourseScope(courseId);
|
||||
return dao().query(TrainSignUpActivityCourse.class, Cnd.where("courseId", "=", courseId).asc("courseStartTime"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void signChange(String courseId, Boolean openOtherUnion) {
|
||||
requireCourseScope(courseId);
|
||||
if (openOtherUnion == null) {
|
||||
throw new BaseException("请指定是否开放报名");
|
||||
}
|
||||
dao().update(TrainSignUpCourse.class, Chain.make("openOtherUnion", openOtherUnion), Cnd.where("id", "=", courseId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void adjustSign(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new BaseException("签到记录ID不能为空");
|
||||
}
|
||||
TrainSignUpUserCourse userCourse = dao().fetch(TrainSignUpUserCourse.class, id);
|
||||
if (userCourse == null) {
|
||||
throw new BaseException("签到记录不存在");
|
||||
}
|
||||
// 通过记录实际所属子活动验证权限,不接受前端指定负责人身份。
|
||||
requireCourseScope(userCourse.getCourseId());
|
||||
if (userCourse.isAttend()) {
|
||||
userCourse.setAttend(false);
|
||||
userCourse.setAttendTime(null);
|
||||
} else {
|
||||
userCourse.setAttend(true);
|
||||
userCourse.setAttendTime(userCourse.getCourseStartTime());
|
||||
}
|
||||
dao().update(userCourse);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void adjust(String activityId, String oldCourseId, String newCourseId, String userId) {
|
||||
if (StrUtil.isBlank(userId)) {
|
||||
throw new BaseException("报名人员用户ID不能为空");
|
||||
}
|
||||
// 来源与目标均受负责人范围约束,且必须属于同一主活动,防止绕过目标列表迁移数据。
|
||||
requireActivityMatch(activityId, requireCourseScope(oldCourseId));
|
||||
requireActivityMatch(activityId, requireCourseScope(newCourseId));
|
||||
Cnd oldCnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", oldCourseId).and("userId", "=", userId);
|
||||
TrainSignUpUser oldUser = dao().fetch(TrainSignUpUser.class, oldCnd);
|
||||
if (oldUser == null) {
|
||||
throw new BaseException("原报名记录不存在,请刷新后重试");
|
||||
}
|
||||
oldUser.setCourseId(newCourseId);
|
||||
oldUser.setSignUpTime(DateUtil.date());
|
||||
dao().update(oldUser);
|
||||
|
||||
// 沿用现有人员调整业务,按目标子活动时段重建签到记录,并在同一事务中提交。
|
||||
List<TrainSignUpActivityCourse> times = dao().query(TrainSignUpActivityCourse.class,
|
||||
Cnd.where("activityId", "=", activityId).and("courseId", "=", newCourseId));
|
||||
dao().clear(TrainSignUpUserCourse.class, oldCnd);
|
||||
List<TrainSignUpUserCourse> userCourses = new ArrayList<>();
|
||||
times.forEach(item -> {
|
||||
TrainSignUpUserCourse course = new TrainSignUpUserCourse();
|
||||
course.setActivityCourseId(activityId);
|
||||
course.setCourseId(newCourseId);
|
||||
course.setUserId(userId);
|
||||
course.setCourseStartTime(item.getCourseStartTime());
|
||||
course.setCourseEndTime(item.getCourseEndTime());
|
||||
course.setActivityCourseId(item.getId());
|
||||
userCourses.add(course);
|
||||
});
|
||||
dao().insert(userCourses);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void deleteSignUser(String activityId, String courseId, String userId) {
|
||||
if (StrUtil.isBlank(userId)) {
|
||||
throw new BaseException("报名人员用户ID不能为空");
|
||||
}
|
||||
requireActivityMatch(activityId, requireCourseScope(courseId));
|
||||
Cnd cnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId).and("userId", "=", userId);
|
||||
// 报名及对应签到记录必须一并删除,禁止只传用户ID造成跨子活动删除。
|
||||
dao().clear(TrainSignUpUser.class, cnd);
|
||||
dao().clear(TrainSignUpUserCourse.class, cnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pagination pageData(PageForm pageForm, String activityId) {
|
||||
// 没有可选主活动时返回空分页,不能将缺少筛选条件解释为查询所有子活动。
|
||||
if (StrUtil.isBlank(activityId)) {
|
||||
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>());
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
tsuc.id,
|
||||
@@ -61,9 +267,11 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
train_sign_up_type type on tsuc.courseType = type.id
|
||||
WHERE
|
||||
tsuc.activityId = @activityId
|
||||
$responsibleScope
|
||||
ORDER BY tsuc.orderNum
|
||||
""");
|
||||
sql.setParam("activityId", activityId);
|
||||
applyResponsibleScope(sql, "tsuc.id", false);
|
||||
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||
List<NutMap> courseList = pagination.getList();
|
||||
courseList.forEach(c -> {
|
||||
@@ -75,6 +283,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
|
||||
@Override
|
||||
public List<NutMap> registerUserList(String courseId) {
|
||||
requireCourseScope(courseId);
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
u.loginname,
|
||||
@@ -92,11 +301,13 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
train_sign_up_user tsuu
|
||||
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
|
||||
$condition
|
||||
$responsibleScope
|
||||
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("tsuu.courseId", "=", courseId);
|
||||
cnd.and("tsuu.courseId", "=", courseId);
|
||||
sql.setCondition(cnd);
|
||||
applyResponsibleScope(sql, "tsuu.courseId", false);
|
||||
List<NutMap> list = listMap(sql);
|
||||
for (NutMap o : list) {
|
||||
if(StrUtil.isBlank(o.getString("mobileColumnsValue"))) {
|
||||
@@ -114,7 +325,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
|
||||
@Override
|
||||
public List<NutMap> getTaleColumnInfo(String courseId) {
|
||||
TrainSignUpCourse course = dao().fetch(TrainSignUpCourse.class, courseId);
|
||||
TrainSignUpCourse course = requireCourseScope(courseId);
|
||||
TrainSignUpType upType = dao().fetch(TrainSignUpType.class, course.getCourseType());
|
||||
dao().fetchLinks(upType, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
||||
|
||||
@@ -125,6 +336,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
|
||||
@Override
|
||||
public List<NutMap> registerUserList(String courseId, String unionId, String unitId, String searchName, String searchKeyword) {
|
||||
requireCourseScope(courseId);
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
tsuu.id,
|
||||
@@ -141,10 +353,11 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
train_sign_up_user tsuu
|
||||
LEFT JOIN `vw_user` u ON u.id = tsuu.userId
|
||||
$condition
|
||||
$responsibleScope
|
||||
ORDER BY FIELD( tsuu.state, 1, 3, 2, 4 ),tsuu.signUpTime desc,u.unionid desc, u.unitid desc
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("tsuu.courseId", "=", courseId);
|
||||
cnd.and("tsuu.courseId", "=", courseId);
|
||||
cnd.andEX("u.unionid", "=", unionId);
|
||||
cnd.andEX("u.unitid", "=", unitId);
|
||||
if (StrUtil.isNotBlank(searchKeyword)) {
|
||||
@@ -153,11 +366,13 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
cnd.and(group);
|
||||
}
|
||||
sql.setCondition(cnd);
|
||||
applyResponsibleScope(sql, "tsuu.courseId", false);
|
||||
return listMap(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pagination getSignInfo(PageForm pageForm, String courseId, String timeId) {
|
||||
requireCourseScope(courseId);
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
tsuuc.id,
|
||||
@@ -173,6 +388,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
`train_sign_up_user_course` tsuuc
|
||||
LEFT JOIN `vw_user` u ON u.id = tsuuc.userId
|
||||
$condition
|
||||
$responsibleScope
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and("tsuuc.courseId", "=", courseId);
|
||||
@@ -180,6 +396,7 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
cnd.desc("isAttend");
|
||||
cnd.desc("attendTime");
|
||||
sql.setCondition(cnd);
|
||||
applyResponsibleScope(sql, "tsuuc.courseId", false);
|
||||
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||
}
|
||||
|
||||
@@ -201,12 +418,134 @@ public class TrainSignUpActivityStatisticsServiceImpl extends BaseServiceImpl<Tr
|
||||
LEFT JOIN `vw_user` u ON u.id = uu.userId
|
||||
WHERE
|
||||
uc.activityId = @activityId
|
||||
$responsibleScope
|
||||
ORDER BY u.unitCode,u.unioncode,u.sex
|
||||
""");
|
||||
sql.setParam("activityId", activityId);
|
||||
applyResponsibleScope(sql, "uc.id", false);
|
||||
return listMap(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NutMap buildSignUserExport(String activityId) {
|
||||
if (StrUtil.isBlank(activityId)) {
|
||||
throw new BaseException("请选择要导出的活动");
|
||||
}
|
||||
TrainSignUpActivity activity = dao().fetch(TrainSignUpActivity.class, activityId);
|
||||
if (activity == null) {
|
||||
throw new BaseException("活动不存在");
|
||||
}
|
||||
// 工作表和报名数据分别在SQL中约束负责人范围,不能仅根据前端可见活动判断导出权限。
|
||||
Sql courseSql = Sqls.create("""
|
||||
SELECT course.* FROM train_sign_up_course course
|
||||
WHERE course.activityId=@activityId $responsibleScope
|
||||
""");
|
||||
courseSql.setParam("activityId", activityId);
|
||||
applyResponsibleScope(courseSql, "course.id", false);
|
||||
courseSql.setEntity(dao().getEntity(TrainSignUpCourse.class));
|
||||
courseSql.setCallback(Sqls.callback.entities());
|
||||
dao().execute(courseSql);
|
||||
List<TrainSignUpCourse> courseList = courseSql.getList(TrainSignUpCourse.class);
|
||||
if (courseList.isEmpty()) {
|
||||
throw new BaseException("没有可导出的子活动,请检查负责人设置");
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT ts.*,
|
||||
CONCAT(DATE_FORMAT(ac.courseStartTime, '%H:%i:%s'),'至',DATE_FORMAT(ac.courseEndTime, '%H:%i:%s')) AS courseTime,
|
||||
u.username, u.loginname, u.sex, IFNULL(ts.mobile, u.mobile) AS mobile,
|
||||
u.birthday, tsc.courseName
|
||||
FROM train_sign_up_user ts
|
||||
LEFT JOIN train_sign_up_activity_course ac ON ts.activityCourseId=ac.id
|
||||
LEFT JOIN vw_user u ON u.id=ts.userId
|
||||
LEFT JOIN train_sign_up_course tsc ON tsc.id=ts.courseId
|
||||
WHERE ts.activityId=@activityId AND tsc.activityId=@activityId $responsibleScope
|
||||
""");
|
||||
sql.setParam("activityId", activityId);
|
||||
applyResponsibleScope(sql, "tsc.id", false);
|
||||
List<NutMap> userList = listMap(sql);
|
||||
|
||||
// 保留原导出公共列、动态报名列和图片处理,仅改变数据范围及Excel文件类型。
|
||||
List<TrainSignUpType> types = dao().query(TrainSignUpType.class, Cnd.NEW());
|
||||
dao().fetchLinks(types, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
||||
Map<String, TrainSignUpType> typeMap = types.stream().collect(Collectors.toMap(TrainSignUpType::getId, type -> type));
|
||||
List<Map<String, Object>> sheetsList = new ArrayList<>();
|
||||
List<ExcelExportEntity> commonEntities = new ArrayList<>();
|
||||
commonEntities.add(new ExcelExportEntity("姓名", "username", 20));
|
||||
commonEntities.add(new ExcelExportEntity("工号", "loginname", 20));
|
||||
commonEntities.add(new ExcelExportEntity("单位", "unitName", 20));
|
||||
commonEntities.add(new ExcelExportEntity("分工会", "unionName", 20));
|
||||
commonEntities.add(new ExcelExportEntity("性别", "sex", 20));
|
||||
commonEntities.add(new ExcelExportEntity("手机号", "mobile", 20));
|
||||
commonEntities.add(new ExcelExportEntity("生日", "birthday", 20));
|
||||
commonEntities.add(new ExcelExportEntity("报名时段", "courseTime", 20));
|
||||
for (TrainSignUpCourse course : courseList) {
|
||||
String name = course.getCourseName();
|
||||
List<NutMap> users = userList.stream().filter(user -> StrUtil.isNotBlank(user.getString("courseName"))
|
||||
&& user.getString("courseName").equals(name)).collect(Collectors.toList());
|
||||
ExportParams exportParams = new ExportParams();
|
||||
exportParams.setType(ExcelType.XSSF);
|
||||
exportParams.setSheetName(name);
|
||||
List<ExcelExportEntity> entities = new ArrayList<>(commonEntities);
|
||||
TrainSignUpType type = typeMap.get(course.getCourseType());
|
||||
if (Lang.isNotEmpty(type.getTrainMobileSignColumnList())) {
|
||||
for (TrainMobileSignColumn column : type.getTrainMobileSignColumnList()) {
|
||||
ExcelExportEntity entity = new ExcelExportEntity();
|
||||
entity.setName(column.getColumnName());
|
||||
entity.setKey(column.getColumnCode());
|
||||
entity.setWidth(20);
|
||||
if ("FILE".equals(column.getColumnFormType())) {
|
||||
entity.setType(2);
|
||||
entity.setExportImageType(2);
|
||||
}
|
||||
entities.add(entity);
|
||||
}
|
||||
}
|
||||
for (NutMap user : users) {
|
||||
String values = user.getString("mobileColumnsValue");
|
||||
if (StrUtil.isBlank(values)) {
|
||||
continue;
|
||||
}
|
||||
for (NutMap value : Json.fromJsonAsList(NutMap.class, values)) {
|
||||
if (!"FILE".equals(value.getString("columnFormType"))) {
|
||||
user.put(value.getString("columnCode"), value.getString("columnValue"));
|
||||
} else if (StrUtil.isNotBlank(value.getString("columnValue"))) {
|
||||
List<NutMap> files = value.getAsList("columnValue", NutMap.class);
|
||||
if (files.size() == 1) {
|
||||
NutMap fileData = files.get(0);
|
||||
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", fileData.get("url")));
|
||||
byte[] imageBytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
|
||||
if (imageBytes.length > 0) {
|
||||
user.put(value.getString("columnCode"), imageBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, Object> sheet = new HashMap<>();
|
||||
sheet.put("name", name);
|
||||
sheet.put("title", exportParams);
|
||||
sheet.put("entity", entities);
|
||||
sheet.put("data", users);
|
||||
sheetsList.add(sheet);
|
||||
}
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
try {
|
||||
for (Map<String, Object> sheet : sheetsList) {
|
||||
new ExcelExportService().createSheetForMap(workbook, (ExportParams) sheet.get("title"),
|
||||
(List<ExcelExportEntity>) sheet.get("entity"), (Collection<?>) sheet.get("data"));
|
||||
}
|
||||
return NutMap.NEW().setv("fileName", activity.getActivityName() + "报名人员名单.xlsx").setv("workbook", workbook);
|
||||
} catch (RuntimeException exception) {
|
||||
// 构建失败时尚未交给下载层,需在此释放工作簿;成功时由下载方法统一关闭。
|
||||
try {
|
||||
workbook.close();
|
||||
} catch (IOException closeException) {
|
||||
exception.addSuppressed(closeException);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int queryCourseCount(String courseId, String courseType) {
|
||||
return this.queryCourseCount(courseId, courseType, null);
|
||||
|
||||
+23
@@ -1,10 +1,15 @@
|
||||
package com.budwk.app.zhgh.democratic.proposal.h5controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService;
|
||||
import io.swagger.annotations.Api;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/h5/proposal/common")
|
||||
@@ -12,6 +17,24 @@ import org.nutz.mvc.annotation.Ok;
|
||||
@Api(tags = "h5提案公共")
|
||||
public class H5ProposalCommonController {
|
||||
|
||||
@Inject
|
||||
private ProposalWriteService proposalWriteService;
|
||||
|
||||
/**
|
||||
* 查询当前登录用户在指定届次下的代表身份,不以是否已分配代表团作为判断依据。
|
||||
*
|
||||
* @param sessionId 当前选择或已有提案所属的教代会届次ID,不能为空
|
||||
* @return Result 的 data 为 Boolean:true 表示存在未删除的代表记录,false 表示不是该届次代表
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("h5.proposal.write")
|
||||
public Result isCurrentUserDelegate(@Param("sessionId") String sessionId) {
|
||||
if (StrUtil.isBlank(sessionId)) {
|
||||
return Result.error("所属教代会不能为空");
|
||||
}
|
||||
return Result.success().addData(proposalWriteService.isCurrentUserDelegate(sessionId));
|
||||
}
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhghh5/democratic/proposal/common/info/index.html")
|
||||
@SaCheckPermission("h5.proposal")
|
||||
|
||||
@@ -49,5 +49,13 @@ public interface ProposalWriteService extends BaseService<ProposalInfo> {
|
||||
*/
|
||||
void checkCurrentUserDelegate(String sessionId);
|
||||
|
||||
/**
|
||||
* 查询当前登录用户是否为指定教代会届次的有效代表。
|
||||
*
|
||||
* @param sessionId 教代会届次ID,不能为空;用户身份由服务端登录信息取得
|
||||
* @return true 表示存在未删除的代表记录,false 表示不存在;不要求已分配代表团
|
||||
*/
|
||||
boolean isCurrentUserDelegate(String sessionId);
|
||||
|
||||
ProposalInfo importProposal(Map<String, String> stringStringMap);
|
||||
}
|
||||
|
||||
+14
-3
@@ -194,6 +194,19 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
|
||||
|
||||
@Override
|
||||
public void checkCurrentUserDelegate(String sessionId) {
|
||||
if (!isCurrentUserDelegate(sessionId)) {
|
||||
throw new RuntimeException("当前用户不是代表,无法撰写提案");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口查询与保存、提交校验共用同一代表身份口径,排除已删除记录。
|
||||
*
|
||||
* @param sessionId 教代会届次ID,空值抛出异常,避免被误判为非代表
|
||||
* @return 是否存在当前登录用户在该届次的有效代表记录
|
||||
*/
|
||||
@Override
|
||||
public boolean isCurrentUserDelegate(String sessionId) {
|
||||
if (StrUtil.isBlank(sessionId)) {
|
||||
throw new RuntimeException("所属教代会不能为空");
|
||||
}
|
||||
@@ -204,9 +217,7 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
|
||||
.and(Teacher_congress_delegate::getDelFlag, "!=", 1)
|
||||
.limit(1)
|
||||
);
|
||||
if (delegate == null) {
|
||||
throw new RuntimeException("当前用户不是代表,无法撰写提案");
|
||||
}
|
||||
return delegate != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.budwk.app.zhgh.huazhu.constant;
|
||||
|
||||
/** 华住对接参数统一在此维护,沿用原项目常量取值。 */
|
||||
public final class HuaZhuConstant {
|
||||
/**
|
||||
* 华住接口请求超时,单位毫秒。
|
||||
*/
|
||||
public static final int TIMEOUT = 20000;
|
||||
|
||||
public static final String BASEURL = "https://htravelserver.huazhu.com";
|
||||
|
||||
public static final String OAUTH2_URL = "https://oauth2-api.huazhu.com";
|
||||
|
||||
/**
|
||||
* 公司卡号
|
||||
*/
|
||||
public static final String CARD_ID = "VCENTCRM1169099135";
|
||||
|
||||
/**
|
||||
* 公司密钥
|
||||
*/
|
||||
public static final String CARD_PASSWORD = "47CD8C9C-A4CD-4CBD-A30F-41C41BDF7E99";
|
||||
|
||||
/**
|
||||
* AES密钥
|
||||
*/
|
||||
public static final String AES_KEY = "JAE4YZJLXZW5HGBU";
|
||||
|
||||
/**
|
||||
* 客户端ID
|
||||
*/
|
||||
public static final String CLIENT_ID = "5e8651d8-a5a8-4358-9986-91f426e191c8";
|
||||
|
||||
/**
|
||||
* 客户端密钥
|
||||
*/
|
||||
public static final String CLIENT_SECRET = "2d20a7f3125b52b8e90936c3b30fb4b62b443a52716a090e55e302cf40f32b66d592ef1e98c1cf0a8e5ec3f9c444a9b4f53a337b1be57263d4fbb71044f67f12";
|
||||
|
||||
/** 华住 addUser 响应的顶层成功字段名;协议未确认前留空,界面显示待核实。 */
|
||||
public static final String SYNC_SUCCESS_FIELD = "";
|
||||
|
||||
/** 成功字段对应的精确字符串值,与 SYNC_SUCCESS_FIELD 一起按华住协议填写。 */
|
||||
public static final String SYNC_SUCCESS_VALUE = "";
|
||||
|
||||
private HuaZhuConstant() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.budwk.app.zhgh.huazhu.controller;
|
||||
|
||||
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.service.HuaZhuEmployeeService;
|
||||
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.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.mvc.annotation.POST;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* PC 端华住员工同步管理。
|
||||
*/
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/huazhu")
|
||||
public class HuaZhuManageController {
|
||||
|
||||
@Inject
|
||||
private HuaZhuEmployeeService huaZhuEmployeeService;
|
||||
|
||||
/** 打开拥有 huazhu.manage 权限的 PC 员工管理页面。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/huazhu/index.html")
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pageForm pageNumber、pageSize 必填,searchKeyword 可选,匹配工号或姓名
|
||||
* @return Result.data 为分页对象,list 包含 id、loginname、username、unitName,totalCount 为总数
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result pageData(@Valid HuaZhuEmployeePageForm pageForm) {
|
||||
return Result.success(huaZhuEmployeeService.pageUsers(pageForm));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userIds 所选本系统员工主键数组,非华住员工编号
|
||||
* @return Result.data 为逐人结果数组;success 为 true/false/null,分别表示成功/失败/待核实,message 为说明
|
||||
*/
|
||||
@At
|
||||
@POST
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result syncUsers(@Param("userIds") String[] userIds) {
|
||||
if (userIds == null || userIds.length == 0) {
|
||||
return Result.error("请先选择需要同步的员工");
|
||||
}
|
||||
return Result.success(huaZhuEmployeeService.syncUsers(Arrays.asList(userIds)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userId 本系统员工主键,服务端转换为工号后查询华住
|
||||
* @return Result.data 为华住状态对象,含 statusCode、userNumber、userName 等;错误说明见 msg
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("huazhu.manage")
|
||||
public Result queryStatus(@Param("userId") String userId) {
|
||||
if (StrUtil.isBlank(userId)) {
|
||||
return Result.error("用户不能为空");
|
||||
}
|
||||
try {
|
||||
return Result.success(huaZhuEmployeeService.queryStatus(userId));
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return Result.error("查询华住状态失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.budwk.app.zhgh.huazhu.h5Controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuLoginService;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* H5 幸福工荟中的华住免密登录入口。
|
||||
*/
|
||||
@IocBean
|
||||
@At("/platform/h5/huazhu")
|
||||
public class H5HuaZhuController {
|
||||
|
||||
@Inject
|
||||
private HuaZhuLoginService huaZhuLoginService;
|
||||
|
||||
/**
|
||||
* 校验幸福工荟访问权限后跳转至华住企业登录页面。
|
||||
*
|
||||
* @param response HTTP 响应,用于返回 302 跳转
|
||||
* @throws IOException 响应输出异常
|
||||
*/
|
||||
@At("/basicLogin")
|
||||
@Ok("void")
|
||||
@SaCheckPermission("h5.inclusive.benefit")
|
||||
public void basicLogin(HttpServletResponse response) throws IOException {
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
response.sendRedirect(huaZhuLoginService.buildBasicLoginUrl(SecurityUtil.getUserLoginname()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.budwk.app.zhgh.huazhu.param;
|
||||
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
|
||||
/**
|
||||
* 华住员工管理分页参数:复用 PageForm 的 searchKeyword、pageNumber、pageSize。
|
||||
*/
|
||||
public class HuaZhuEmployeePageForm extends PageForm {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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 org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 华住员工同步与状态查询服务。
|
||||
*/
|
||||
public interface HuaZhuEmployeeService {
|
||||
|
||||
/**
|
||||
* 分页查询可同步的本项目员工。
|
||||
*
|
||||
* @param pageForm pageNumber、pageSize 必填,searchKeyword 为可选的工号或姓名片段
|
||||
* @return 分页对象,list 为 id/loginname/username/unitName,totalCount 为匹配人数
|
||||
*/
|
||||
Pagination<NutMap> pageUsers(HuaZhuEmployeePageForm pageForm);
|
||||
|
||||
/**
|
||||
* 将选中的本项目员工同步到华住。
|
||||
*
|
||||
* @param userIds 本项目用户主键列表
|
||||
* @return 逐人结果,success=true 为协议确认成功、false 为失败、null 为待核实,message 为说明
|
||||
*/
|
||||
List<HuaZhuSyncResult> syncUsers(List<String> userIds);
|
||||
|
||||
/**
|
||||
* 查询指定本项目员工在华住的实时状态。
|
||||
*
|
||||
* @param userId 本项目用户主键
|
||||
* @return 华住员工状态对象;statusCode 保留第三方原值,userNumber 为已校验匹配的工号
|
||||
*/
|
||||
HuaZhuEmployeeStatus queryStatus(String userId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.budwk.app.zhgh.huazhu.service;
|
||||
|
||||
/**
|
||||
* 华住免密登录服务。
|
||||
*/
|
||||
public interface HuaZhuLoginService {
|
||||
|
||||
/**
|
||||
* 按华住协议为当前职工工号生成免密登录跳转地址。
|
||||
*
|
||||
* @param loginName 当前登录职工的工号
|
||||
* @return 华住免密登录地址
|
||||
*/
|
||||
String buildBasicLoginUrl(String loginName);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.budwk.app.zhgh.huazhu.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.zhgh.huazhu.constant.HuaZhuConstant;
|
||||
import com.budwk.app.zhgh.huazhu.param.HuaZhuEmployeePageForm;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuEmployeeService;
|
||||
import com.budwk.app.zhgh.huazhu.vo.HuaZhuEmployeeStatus;
|
||||
import com.budwk.app.zhgh.huazhu.vo.HuaZhuSyncResult;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
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.lang.util.NutMap;
|
||||
import org.nutz.integration.jedis.RedisService;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 华住员工同步与状态查询实现。
|
||||
*/
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class HuaZhuEmployeeServiceImpl implements HuaZhuEmployeeService {
|
||||
|
||||
private final Dao dao;
|
||||
|
||||
@Inject
|
||||
private RedisService redisService;
|
||||
|
||||
public HuaZhuEmployeeServiceImpl(Dao dao) {
|
||||
this.dao = dao;
|
||||
}
|
||||
|
||||
/** 工号、姓名采用绑定参数查询;分页及筛选参数沿用项目 PageForm。 */
|
||||
@Override
|
||||
public Pagination<NutMap> pageUsers(HuaZhuEmployeePageForm pageForm) {
|
||||
if (pageForm == null || pageForm.getPageNumber() == null || pageForm.getPageNumber() < 1
|
||||
|| pageForm.getPageSize() == null || pageForm.getPageSize() < 1 || pageForm.getPageSize() > 200) {
|
||||
throw new IllegalArgumentException("页码必须大于0,每页数量必须为1至200");
|
||||
}
|
||||
String keyword = StrUtil.trim(pageForm.getSearchKeyword());
|
||||
String condition = " WHERE u.delFlag = 0 ";
|
||||
if (StrUtil.isNotBlank(keyword)) {
|
||||
condition += " AND (u.loginname LIKE @keyword OR u.username LIKE @keyword) ";
|
||||
}
|
||||
Sql countSql = Sqls.create("SELECT COUNT(1) FROM sys_user u" + condition);
|
||||
Sql listSql = Sqls.create("SELECT u.id, u.loginname, u.username, unit.name AS unitName "
|
||||
+ "FROM sys_user u LEFT JOIN sys_unit unit ON unit.id = u.unitId " + condition
|
||||
+ "ORDER BY u.createdAt DESC");
|
||||
if (StrUtil.isNotBlank(keyword)) {
|
||||
String likeKeyword = "%" + keyword + "%";
|
||||
countSql.setParam("keyword", likeKeyword);
|
||||
listSql.setParam("keyword", likeKeyword);
|
||||
}
|
||||
countSql.setCallback(Sqls.callback.integer());
|
||||
dao.execute(countSql);
|
||||
int totalCount = countSql.getInt();
|
||||
listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
|
||||
listSql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(listSql);
|
||||
return new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), totalCount, listSql.getList(NutMap.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个主键独立校验并产生结果,单个员工不存在或接口失败不阻断后续员工。
|
||||
* 本地事务无法撤销华住侧已处理的同步,超时后应先查询状态,避免直接重试整批。
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public List<HuaZhuSyncResult> syncUsers(List<String> userIds) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<HuaZhuSyncResult> results = new ArrayList<>();
|
||||
for (String userId : userIds) {
|
||||
HuaZhuSyncResult result = new HuaZhuSyncResult().setUserId(userId);
|
||||
try {
|
||||
Sys_user user = findAvailableUser(userId);
|
||||
result.setLoginName(user.getLoginname()).setUserName(user.getUsername());
|
||||
String body = request("/userInfo/addUser", buildEmployeeRequest(user.getLoginname()));
|
||||
applySyncResponse(result, body);
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
// 只透出本模块构造的提示;网络和解析异常在下层转换为固定文案。
|
||||
result.setSuccess(false).setMessage(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
result.setSuccess(false).setMessage("同步未完成,请查询华住状态后再决定是否重试");
|
||||
}
|
||||
results.add(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** 根据本系统主键取得工号,返回与该工号对应的华住员工对象,不推断状态码含义。 */
|
||||
@Override
|
||||
public HuaZhuEmployeeStatus queryStatus(String userId) {
|
||||
Sys_user user = findAvailableUser(userId);
|
||||
String body = request("/userInfo/queryUser", buildEmployeeRequest(user.getLoginname()));
|
||||
JSONObject payload = extractQueryPayload(body, user.getLoginname());
|
||||
try {
|
||||
return JSONUtil.toBean(payload, HuaZhuEmployeeStatus.class);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("华住查询接口返回格式无法识别", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容华住接口直接返回员工对象,以及以 data 或 result 包装员工对象的两种响应格式。
|
||||
* 响应中不包含员工状态时,只返回经筛选后的业务提示,避免透出第三方完整响应。
|
||||
*
|
||||
* @param body 华住查询接口响应正文
|
||||
* @param loginName 本次查询的工号,用于避免把错误或其他员工的信息当成有效结果
|
||||
* @return 可转换为员工状态的 JSON 对象
|
||||
*/
|
||||
private JSONObject extractQueryPayload(String body, String loginName) {
|
||||
JSONObject response;
|
||||
try {
|
||||
response = JSONUtil.parseObj(body);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("华住查询接口未返回有效数据", e);
|
||||
}
|
||||
JSONObject payload = extractObject(response, "data");
|
||||
if (payload == null) {
|
||||
payload = extractObject(response, "result");
|
||||
}
|
||||
if (payload == null) {
|
||||
payload = response;
|
||||
}
|
||||
// statusCode 本身可能是错误码;必须有匹配的工号,才将响应作为该员工的状态展示。
|
||||
if (!loginName.equals(payload.getStr("userNumber"))) {
|
||||
throw new IllegalStateException("华住未返回与该工号匹配的员工信息,请核实接口响应协议");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功码未取得协议确认前,HTTP 2xx 仅表示请求送达;不猜测 0、200 等值的含义。
|
||||
* 在 HuaZhuConstant 中填写顶层字段名及精确成功值后才判定成功,字段缺失/格式变化仍返回待核实。
|
||||
*/
|
||||
private void applySyncResponse(HuaZhuSyncResult result, String body) {
|
||||
result.setSuccess(null).setMessage("请求已发送,结果待核实,请查询华住状态");
|
||||
String field = HuaZhuConstant.SYNC_SUCCESS_FIELD.trim();
|
||||
String expected = HuaZhuConstant.SYNC_SUCCESS_VALUE;
|
||||
if (StrUtil.isBlank(field) || StrUtil.isBlank(expected)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject response = JSONUtil.parseObj(body);
|
||||
if (response.get(field) == null) {
|
||||
return;
|
||||
}
|
||||
boolean success = expected.equals(response.getStr(field));
|
||||
result.setSuccess(success).setMessage(success ? "同步成功" : "华住返回非成功结果,请查询状态核实");
|
||||
} catch (Exception e) {
|
||||
result.setMessage("华住响应格式无法识别,结果待核实,请查询华住状态");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从包装响应中提取对象;部分网关会将 data/result 再序列化为 JSON 字符串。
|
||||
*
|
||||
* @param response 华住响应对象
|
||||
* @param key 包装字段名
|
||||
* @return 包装内的 JSON 对象,缺失或格式不匹配时返回 null
|
||||
*/
|
||||
private JSONObject extractObject(JSONObject response, String key) {
|
||||
Object value = response.get(key);
|
||||
if (value instanceof JSONObject) {
|
||||
return (JSONObject) value;
|
||||
}
|
||||
if (value instanceof CharSequence && StrUtil.isNotBlank(value.toString())) {
|
||||
try {
|
||||
return JSONUtil.parseObj(value.toString());
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用华住员工接口,并统一处理 OAuth Token 与非成功状态。
|
||||
*
|
||||
* @param path 华住接口路径
|
||||
* @param requestBody 请求体
|
||||
* @return 华住响应正文
|
||||
*/
|
||||
private String request(String path, NutMap requestBody) {
|
||||
String authorization = getAccessToken();
|
||||
try (HttpResponse response = HttpRequest.post(HuaZhuConstant.BASEURL.trim().replaceAll("/+$", "") + path)
|
||||
.header("Authorization", authorization)
|
||||
.header("accept", "application/json")
|
||||
.contentType("application/json; charset=UTF-8")
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(HuaZhuConstant.TIMEOUT)
|
||||
.execute()) {
|
||||
if (response.getStatus() == 401) {
|
||||
redisService.del(tokenCacheKey());
|
||||
// 不自动重发修改请求,避免上游已执行时重复同步。
|
||||
throw new IllegalStateException("华住认证已失效,请查询状态后重试");
|
||||
}
|
||||
if (!response.isOk()) {
|
||||
throw new IllegalStateException("华住接口调用失败,HTTP 状态码:" + response.getStatus());
|
||||
}
|
||||
return response.body();
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("华住请求异常或超时,结果未确认,请先查询状态再重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取华住 OAuth Token,优先使用 Redis 缓存以避免频繁请求第三方认证接口。
|
||||
*
|
||||
* @return HTTP Authorization 头的 Bearer Token
|
||||
*/
|
||||
private String getAccessToken() {
|
||||
String cacheKey = tokenCacheKey();
|
||||
String cachedToken;
|
||||
try {
|
||||
cachedToken = redisService.get(cacheKey);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("华住认证缓存不可用,请联系管理员");
|
||||
}
|
||||
if (StrUtil.isNotBlank(cachedToken)) {
|
||||
return cachedToken;
|
||||
}
|
||||
String credential = Base64.getEncoder().encodeToString((HuaZhuConstant.CLIENT_ID + ":" + HuaZhuConstant.CLIENT_SECRET).getBytes(StandardCharsets.UTF_8));
|
||||
try (HttpResponse response = HttpRequest.post(HuaZhuConstant.OAUTH2_URL.trim().replaceAll("/+$", "")
|
||||
+ "/oauth/token?scope=ALL&grant_type=client_credentials")
|
||||
.header("Authorization", "Basic " + credential)
|
||||
.timeout(HuaZhuConstant.TIMEOUT)
|
||||
.execute()) {
|
||||
if (!response.isOk()) {
|
||||
throw new IllegalStateException("获取华住认证 Token 失败,HTTP 状态码:" + response.getStatus());
|
||||
}
|
||||
JSONObject tokenData = JSONUtil.parseObj(response.body());
|
||||
String accessToken = tokenData.getStr("access_token");
|
||||
String tokenType = StrUtil.blankToDefault(tokenData.getStr("token_type"), "Bearer");
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new IllegalStateException("华住认证接口未返回 Token");
|
||||
}
|
||||
String authorization = tokenType + " " + accessToken;
|
||||
// expires_in 单位为秒,预留一分钟;缺少有效时长时不缓存,避免沿用猜测的有效期。
|
||||
Long expiresIn = tokenData.getLong("expires_in");
|
||||
if (expiresIn != null && expiresIn > 60) {
|
||||
redisService.setex(cacheKey, (int) Math.min(expiresIn - 60, Integer.MAX_VALUE), authorization);
|
||||
}
|
||||
return authorization;
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("华住认证请求、响应解析或缓存处理失败,请联系管理员");
|
||||
}
|
||||
}
|
||||
|
||||
/** 缓存按企业及接口环境隔离;凭据轮换后自然使用新键,键中不包含明文凭据。 */
|
||||
private String tokenCacheKey() {
|
||||
return "zhgh:huazhu:oauth-token:" + DigestUtil.sha256Hex(JSONUtil.toJsonStr(new String[]{
|
||||
HuaZhuConstant.BASEURL.trim().replaceAll("/+$", ""), HuaZhuConstant.OAUTH2_URL.trim().replaceAll("/+$", ""), HuaZhuConstant.CARD_ID,
|
||||
HuaZhuConstant.CLIENT_ID, HuaZhuConstant.CLIENT_SECRET}));
|
||||
}
|
||||
|
||||
/** 沿用源接口的 cardPassWord 大小写,只发送企业凭据和工号。 */
|
||||
private NutMap buildEmployeeRequest(String loginName) {
|
||||
return NutMap.NEW().addv("cardId", HuaZhuConstant.CARD_ID)
|
||||
.addv("cardPassWord", HuaZhuConstant.CARD_PASSWORD)
|
||||
.addv("userNumber", loginName);
|
||||
}
|
||||
|
||||
private Sys_user findAvailableUser(String userId) {
|
||||
if (StrUtil.isBlank(userId)) {
|
||||
throw new IllegalArgumentException("用户主键不能为空");
|
||||
}
|
||||
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", userId).and("delFlag", "=", false));
|
||||
if (user == null) {
|
||||
throw new IllegalArgumentException("用户不存在或已删除");
|
||||
}
|
||||
if (StrUtil.isBlank(user.getLoginname())) {
|
||||
throw new IllegalArgumentException("用户缺少工号,无法同步华住");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.budwk.app.zhgh.huazhu.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.zhgh.huazhu.constant.HuaZhuConstant;
|
||||
import com.budwk.app.zhgh.huazhu.service.HuaZhuLoginService;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 华住免密登录服务实现。
|
||||
*/
|
||||
@IocBean
|
||||
public class HuaZhuLoginServiceImpl implements HuaZhuLoginService {
|
||||
|
||||
private static final String TRAVEL_TYPE = "YS";
|
||||
|
||||
/** @param loginName 服务端登录会话中的工号;返回含签名的华住免密跳转 URL。 */
|
||||
@Override
|
||||
public String buildBasicLoginUrl(String loginName) {
|
||||
if (StrUtil.isBlank(loginName)) {
|
||||
throw new IllegalArgumentException("当前登录用户缺少工号,无法进入华住");
|
||||
}
|
||||
|
||||
// 华住约定使用 AES/ECB/PKCS5Padding 对公司卡号、工号和当前时间生成签名。
|
||||
String cardId = HuaZhuConstant.CARD_ID;
|
||||
String authorizeSign = encrypt(cardId + "&" + loginName + "&" + DateUtil.now(), HuaZhuConstant.AES_KEY);
|
||||
return HuaZhuConstant.BASEURL.trim().replaceAll("/+$", "") + "/authentication/basicLogin?cardId=" + encode(cardId)
|
||||
+ "&travelType=" + TRAVEL_TYPE
|
||||
+ "&userNumber=" + encode(loginName)
|
||||
+ "&invoiceNo="
|
||||
+ "&authorizeSign=" + encode(authorizeSign);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按华住协议生成 AES 签名,不记录签名内容或密钥。
|
||||
*
|
||||
* @param source 待签名内容
|
||||
* @param aesKey 华住提供的 16 字节 AES 密钥
|
||||
* @return Base64 编码后的签名
|
||||
*/
|
||||
private String encrypt(String source, String aesKey) {
|
||||
try {
|
||||
byte[] keyBytes = aesKey.getBytes(StandardCharsets.UTF_8);
|
||||
if (keyBytes.length != 16) {
|
||||
throw new IllegalStateException("华住 AES 密钥必须为 16 字节");
|
||||
}
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, "AES"));
|
||||
return Base64.getEncoder().encodeToString(cipher.doFinal(source.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("生成华住登录签名失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encode(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.budwk.app.zhgh.huazhu.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 华住查询员工接口的展示结果。
|
||||
*/
|
||||
@Data
|
||||
public class HuaZhuEmployeeStatus {
|
||||
private String statusCode;
|
||||
private String userNumber;
|
||||
private String userName;
|
||||
private String userDep;
|
||||
private String costCenter;
|
||||
private String rank;
|
||||
private String leader;
|
||||
private String mobile;
|
||||
private String email;
|
||||
private List<Permission> permissions;
|
||||
|
||||
/**
|
||||
* 华住返回的预订权限。
|
||||
*/
|
||||
@Data
|
||||
public static class Permission {
|
||||
private Boolean hasPermission;
|
||||
private String permissionCode;
|
||||
private String permissionValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.budwk.app.zhgh.huazhu.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 单个员工同步到华住的执行结果。
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class HuaZhuSyncResult {
|
||||
private String userId;
|
||||
private String loginName;
|
||||
private String userName;
|
||||
/** true=按已配置协议确认成功;false=失败;null=请求结果待核实,不等同同步成功。 */
|
||||
private Boolean success;
|
||||
private String message;
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.budwk.app.zhgh.inclusive.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.inclusive.model.InclusiveBenefit;
|
||||
import com.budwk.app.zhgh.inclusive.param.InclusiveBenefitPageForm;
|
||||
import com.budwk.app.zhgh.inclusive.service.InclusiveBenefitService;
|
||||
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.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* PC 端幸福工荟管理。
|
||||
*/
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/inclusive")
|
||||
public class InclusiveBenefitController {
|
||||
|
||||
@Inject
|
||||
private InclusiveBenefitService inclusiveBenefitService;
|
||||
|
||||
/** 打开幸福工荟 PC 管理页面。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/inclusive/index.html")
|
||||
@SaCheckPermission("inclusive.benefit")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pageForm 必填 pageNumber、pageSize;year、title、tagType 为可选筛选条件
|
||||
* @return Result,data 为分页对象,包含 list、totalCount、pageNo、pageSize
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("inclusive.benefit")
|
||||
public Result pageData(@Valid InclusiveBenefitPageForm pageForm) {
|
||||
return Result.success(inclusiveBenefitService.pageData(pageForm, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 待回显内容主键
|
||||
* @return Result,data 为完整内容对象;记录不存在或已删除时返回错误
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("inclusive.benefit")
|
||||
public Result findOne(@Param("id") String id) {
|
||||
Object detail = inclusiveBenefitService.findVO(id, false);
|
||||
return detail == null ? Result.error("幸福工荟不存在或已删除") : Result.success(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param benefit 内容表单:id 为空新增;title 标题、summary 摘要、tagType 标签编码、
|
||||
* content 富文本、cover 封面地址必填;year、sexLimit、linkUrl 为可选配置
|
||||
* @param status 0 保存草稿,1 发布;为空默认草稿
|
||||
* @return Result,code 为 0 表示成功,失败原因见 msg,无业务 data
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("inclusive.benefit")
|
||||
public Result save(InclusiveBenefit benefit, @Param("status") Integer status) {
|
||||
if (benefit == null || StrUtil.isBlank(benefit.getTitle())) {
|
||||
return Result.error("标题不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(benefit.getTagType())) {
|
||||
return Result.error("标签类型不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(benefit.getSummary()) || StrUtil.isBlank(benefit.getContent())
|
||||
|| StrUtil.isBlank(benefit.getCover())) {
|
||||
return Result.error("摘要、内容和封面图不能为空");
|
||||
}
|
||||
if (benefit.getTitle().length() > 100 || benefit.getSummary().length() > 500
|
||||
|| benefit.getTagType().length() > 50 || benefit.getCover().length() > 255
|
||||
|| (benefit.getLinkUrl() != null && benefit.getLinkUrl().length() > 500)) {
|
||||
return Result.error("标题、摘要、标签、封面或链接长度超过限制");
|
||||
}
|
||||
if (benefit.getSexLimit() != null && (benefit.getSexLimit() < 0 || benefit.getSexLimit() > 2)) {
|
||||
return Result.error("性别配置只能为0、1或2");
|
||||
}
|
||||
inclusiveBenefitService.saveBenefit(benefit, status);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 内容主键
|
||||
* @param status 0 下架,1 发布
|
||||
* @return Result,code 为 0 表示成功,无业务 data
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("inclusive.benefit")
|
||||
public Result updateStatus(@Param("id") String id, @Param("status") Integer status) {
|
||||
inclusiveBenefitService.updateStatus(id, status);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 待逻辑删除的内容主键
|
||||
* @return Result,code 为 0 表示成功,无业务 data
|
||||
*/
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("inclusive.benefit")
|
||||
public Result delete(@Param("id") String id) {
|
||||
inclusiveBenefitService.deleteBenefit(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.budwk.app.zhgh.inclusive.h5Controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.inclusive.param.InclusiveBenefitPageForm;
|
||||
import com.budwk.app.zhgh.inclusive.service.InclusiveBenefitService;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* H5 端幸福工荟浏览。
|
||||
*/
|
||||
@IocBean
|
||||
@Ok("json:full")
|
||||
@At("/platform/h5/inclusive")
|
||||
public class H5InclusiveBenefitController {
|
||||
|
||||
@Inject
|
||||
private InclusiveBenefitService inclusiveBenefitService;
|
||||
|
||||
/** 打开幸福工荟 H5 列表页。 */
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhghh5/inclusive/index.html")
|
||||
@SaCheckPermission("h5.inclusive.benefit")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/** 打开详情页,页面通过查询参数 id 请求具体内容。 */
|
||||
@At("/detail")
|
||||
@Ok("beetl:/platform/zhghh5/inclusive/detail.html")
|
||||
@SaCheckPermission("h5.inclusive.benefit")
|
||||
public void detail() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pageForm 必填 pageNumber、pageSize;tagType 为标签编码,可选 year、title
|
||||
* @return Result,data 为已发布且未删除内容的分页对象,含 list、totalCount、pageNo、pageSize
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("h5.inclusive.benefit")
|
||||
public Result pageData(@Valid InclusiveBenefitPageForm pageForm) {
|
||||
return Result.success(inclusiveBenefitService.pageData(pageForm, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 内容主键
|
||||
* @return Result,data 为详情对象;不存在、未发布或已删除时返回错误,不返回正文
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("h5.inclusive.benefit")
|
||||
public Result findOne(@Param("id") String id) {
|
||||
Object detail = inclusiveBenefitService.findVO(id, true);
|
||||
return detail == null ? Result.error("内容不存在或已下架") : Result.success(detail);
|
||||
}
|
||||
|
||||
/** @return Result,data 为标签数组,每项 text 是标签名称,value 是标签编码;无参数 */
|
||||
@At
|
||||
@SaCheckPermission("h5.inclusive.benefit")
|
||||
public Result tagOptions() {
|
||||
return Result.success(inclusiveBenefitService.tagOptions());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.budwk.app.zhgh.inclusive.model;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.ColDefine;
|
||||
import org.nutz.dao.entity.annotation.ColType;
|
||||
import org.nutz.dao.entity.annotation.Column;
|
||||
import org.nutz.dao.entity.annotation.Comment;
|
||||
import org.nutz.dao.entity.annotation.EL;
|
||||
import org.nutz.dao.entity.annotation.Name;
|
||||
import org.nutz.dao.entity.annotation.Table;
|
||||
import org.nutz.dao.entity.annotation.TableMeta;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 幸福工荟内容。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Table("inclusive_benefit")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||
@Comment("幸福工荟")
|
||||
public class InclusiveBenefit extends BaseModel implements Serializable {
|
||||
|
||||
@Name
|
||||
@Column
|
||||
@Comment("主键")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(els = @EL("uuid()"))
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("年度")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer year;
|
||||
|
||||
@Column
|
||||
@Comment("标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String title;
|
||||
|
||||
@Column
|
||||
@Comment("摘要")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String summary;
|
||||
|
||||
@Column
|
||||
@Comment("标签类型字典编码")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
private String tagType;
|
||||
|
||||
@Column
|
||||
@Comment("富文本内容")
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
private String content;
|
||||
|
||||
@Column
|
||||
@Comment("性别限制:0全部开放,1男性,2女性")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer sexLimit;
|
||||
|
||||
@Column
|
||||
@Comment("封面图")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||
private String cover;
|
||||
|
||||
@Column
|
||||
@Comment("外链地址")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String linkUrl;
|
||||
|
||||
@Column
|
||||
@Comment("发布状态:0草稿,1已发布")
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer status;
|
||||
|
||||
@Column
|
||||
@Comment("发布时间")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String publishTime;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.budwk.app.zhgh.inclusive.param;
|
||||
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 幸福工荟分页查询参数。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class InclusiveBenefitPageForm extends PageForm {
|
||||
/** 可选年度,按内容归属年度精确筛选。 */
|
||||
private Integer year;
|
||||
/** 可选标题关键字,使用包含匹配。 */
|
||||
private String title;
|
||||
/** 可选标签字典编码,空值表示全部标签。 */
|
||||
private String tagType;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.budwk.app.zhgh.inclusive.service;
|
||||
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.inclusive.model.InclusiveBenefit;
|
||||
import com.budwk.app.zhgh.inclusive.param.InclusiveBenefitPageForm;
|
||||
import com.budwk.app.zhgh.inclusive.vo.InclusiveBenefitVO;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 幸福工荟业务服务。
|
||||
*/
|
||||
public interface InclusiveBenefitService extends BaseService<InclusiveBenefit> {
|
||||
|
||||
/**
|
||||
* 查询管理列表或手机端已发布内容。
|
||||
* @param pageForm 页码、每页数量及可选的年度、标题、标签编码
|
||||
* @param onlyPublished 手机端固定传 true,PC 管理端传 false,不接受客户端指定
|
||||
* @return 分页对象,list 为内容列表,totalCount 为符合条件的总条数
|
||||
*/
|
||||
Pagination<InclusiveBenefitVO> pageData(InclusiveBenefitPageForm pageForm, boolean onlyPublished);
|
||||
|
||||
/**
|
||||
* @param id 内容主键
|
||||
* @param onlyPublished true 时只允许读取已发布内容
|
||||
* @return 内容及标签名称;不存在、已删除或未发布时返回 null
|
||||
*/
|
||||
InclusiveBenefitVO findVO(String id, boolean onlyPublished);
|
||||
|
||||
/** @return 启用的标签列表,每项 text 为名称,value 为字典编码 */
|
||||
List<NutMap> tagOptions();
|
||||
|
||||
/**
|
||||
* @param benefit 表单内容,id 为空表示新增,否则编辑对应内容
|
||||
* @param status 0 保存草稿,1 发布;为空按草稿处理
|
||||
* @return 无返回值;内容不存在或参数不合法时抛出 IllegalArgumentException
|
||||
*/
|
||||
void saveBenefit(InclusiveBenefit benefit, Integer status);
|
||||
|
||||
/**
|
||||
* @param id 内容主键
|
||||
* @param status 0 下架,1 发布
|
||||
* @return 无返回值;发布时更新发布时间
|
||||
*/
|
||||
void updateStatus(String id, Integer status);
|
||||
|
||||
/**
|
||||
* @param id 待删除内容主键
|
||||
* @return 无返回值;仅设置删除标记,保留原记录
|
||||
*/
|
||||
void deleteBenefit(String id);
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package com.budwk.app.zhgh.inclusive.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.sys.models.Sys_dict;
|
||||
import com.budwk.app.zhgh.inclusive.model.InclusiveBenefit;
|
||||
import com.budwk.app.zhgh.inclusive.param.InclusiveBenefitPageForm;
|
||||
import com.budwk.app.zhgh.inclusive.service.InclusiveBenefitService;
|
||||
import com.budwk.app.zhgh.inclusive.vo.InclusiveBenefitVO;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 幸福工荟业务实现。
|
||||
*/
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class InclusiveBenefitServiceImpl extends BaseServiceImpl<InclusiveBenefit> implements InclusiveBenefitService {
|
||||
|
||||
private static final String TAG_DICT_CODE = "INCLUSIVE_BENEFIT_TYPE";
|
||||
|
||||
/** @param dao 当前项目的数据库访问对象 */
|
||||
public InclusiveBenefitServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pagination<InclusiveBenefitVO> pageData(InclusiveBenefitPageForm pageForm, boolean onlyPublished) {
|
||||
Cnd cnd = Cnd.where("delFlag", "=", false);
|
||||
// H5 的可见性由服务端调用方决定,不能通过提交 status 参数读取草稿或下架内容。
|
||||
if (onlyPublished) {
|
||||
cnd.and("status", "=", 1);
|
||||
}
|
||||
if (pageForm.getYear() != null) {
|
||||
cnd.and("year", "=", pageForm.getYear());
|
||||
}
|
||||
if (Strings.isNotBlank(pageForm.getTitle())) {
|
||||
cnd.and(Cnd.likeEX("title", pageForm.getTitle()));
|
||||
}
|
||||
if (Strings.isNotBlank(pageForm.getTagType())) {
|
||||
cnd.and("tagType", "=", pageForm.getTagType());
|
||||
}
|
||||
cnd.desc("publishTime").desc("createdAt");
|
||||
Pagination<InclusiveBenefit> pagination = listPage(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
|
||||
Map<String, String> tagNameMap = getTagNameMap();
|
||||
List<InclusiveBenefitVO> list = pagination.getList().stream()
|
||||
.map(benefit -> toVO(benefit, tagNameMap))
|
||||
.collect(Collectors.toList());
|
||||
return new Pagination<>(pagination.getPageNo(), pagination.getPageSize(), pagination.getTotalCount(), list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InclusiveBenefitVO findVO(String id, boolean onlyPublished) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
Cnd cnd = Cnd.where("id", "=", id).and("delFlag", "=", false);
|
||||
if (onlyPublished) {
|
||||
cnd.and("status", "=", 1);
|
||||
}
|
||||
return toVO(fetch(cnd), getTagNameMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NutMap> tagOptions() {
|
||||
Sys_dict parent = dao().fetch(Sys_dict.class, Cnd.where("code", "=", TAG_DICT_CODE)
|
||||
.and("disabled", "=", false).and("delFlag", "=", false));
|
||||
if (parent == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return dao().query(Sys_dict.class, Cnd.where("parentId", "=", parent.getId())
|
||||
.and("disabled", "=", false).and("delFlag", "=", false).asc("location"))
|
||||
.stream()
|
||||
.map(dict -> NutMap.NEW().addv("text", dict.getName()).addv("value", dict.getCode()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveBenefit(InclusiveBenefit benefit, Integer status) {
|
||||
validateStatus(status);
|
||||
InclusiveBenefit target;
|
||||
if (StrUtil.isBlank(benefit.getId())) {
|
||||
target = new InclusiveBenefit();
|
||||
target.setYear(benefit.getYear() == null ? DateUtil.year(DateUtil.date()) : benefit.getYear());
|
||||
} else {
|
||||
target = fetch(Cnd.where("id", "=", benefit.getId()).and("delFlag", "=", false));
|
||||
if (target == null) {
|
||||
throw new IllegalArgumentException("幸福工荟不存在或已删除");
|
||||
}
|
||||
target.setYear(benefit.getYear() == null ? target.getYear() : benefit.getYear());
|
||||
}
|
||||
target.setTitle(benefit.getTitle());
|
||||
target.setSummary(benefit.getSummary());
|
||||
target.setTagType(benefit.getTagType());
|
||||
target.setContent(benefit.getContent());
|
||||
// 沿用西财的配置字段;本次迁移不将性别配置扩展为新的访问控制规则。
|
||||
target.setSexLimit(benefit.getSexLimit() == null ? 0 : benefit.getSexLimit());
|
||||
target.setCover(benefit.getCover());
|
||||
target.setLinkUrl(benefit.getLinkUrl());
|
||||
target.setStatus(status == null ? 0 : status);
|
||||
if (Integer.valueOf(1).equals(target.getStatus())) {
|
||||
target.setPublishTime(DateUtil.now());
|
||||
}
|
||||
if (StrUtil.isBlank(target.getId())) {
|
||||
insert(target);
|
||||
} else {
|
||||
update(target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void updateStatus(String id, Integer status) {
|
||||
validateStatus(status);
|
||||
requireId(id);
|
||||
InclusiveBenefit benefit = fetch(Cnd.where("id", "=", id).and("delFlag", "=", false));
|
||||
if (benefit == null) {
|
||||
throw new IllegalArgumentException("幸福工荟不存在或已删除");
|
||||
}
|
||||
Integer targetStatus = status == null ? 0 : status;
|
||||
Chain chain = Chain.make("status", targetStatus);
|
||||
if (Integer.valueOf(1).equals(targetStatus)) {
|
||||
chain.add("publishTime", DateUtil.now());
|
||||
}
|
||||
update(chain, Cnd.where("id", "=", id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void deleteBenefit(String id) {
|
||||
requireId(id);
|
||||
InclusiveBenefit benefit = fetch(Cnd.where("id", "=", id).and("delFlag", "=", false));
|
||||
if (benefit == null) {
|
||||
throw new IllegalArgumentException("幸福工荟不存在或已删除");
|
||||
}
|
||||
vDelete(id);
|
||||
}
|
||||
|
||||
/** 写入操作只支持草稿和发布两种状态,避免非法值产生无法管理的内容。 */
|
||||
private void validateStatus(Integer status) {
|
||||
if (status != null && status != 0 && status != 1) {
|
||||
throw new IllegalArgumentException("发布状态只能为0或1");
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改状态和删除必须明确指定记录,不允许缺失主键。 */
|
||||
private void requireId(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new IllegalArgumentException("内容ID不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/** 将实体映射为列表/详情对象,附加标签、性别配置和发布状态的展示名称。 */
|
||||
private InclusiveBenefitVO toVO(InclusiveBenefit benefit, Map<String, String> tagNameMap) {
|
||||
if (benefit == null) {
|
||||
return null;
|
||||
}
|
||||
return new InclusiveBenefitVO()
|
||||
.setId(benefit.getId())
|
||||
.setYear(benefit.getYear())
|
||||
.setTitle(benefit.getTitle())
|
||||
.setSummary(benefit.getSummary())
|
||||
.setTagType(benefit.getTagType())
|
||||
.setTagTypeName(tagNameMap.get(benefit.getTagType()))
|
||||
.setContent(benefit.getContent())
|
||||
.setSexLimit(benefit.getSexLimit())
|
||||
.setSexLimitName(getSexLimitName(benefit.getSexLimit()))
|
||||
.setCover(benefit.getCover())
|
||||
.setLinkUrl(benefit.getLinkUrl())
|
||||
.setStatus(benefit.getStatus())
|
||||
.setStatusName(Integer.valueOf(1).equals(benefit.getStatus()) ? "已发布" : "草稿")
|
||||
.setCreatedAt(benefit.getCreatedAt())
|
||||
.setUpdatedAt(benefit.getUpdatedAt())
|
||||
.setPublishTime(benefit.getPublishTime());
|
||||
}
|
||||
|
||||
private Map<String, String> getTagNameMap() {
|
||||
List<NutMap> options = tagOptions();
|
||||
// 字典编码重复时保留排序靠前的一项,避免整个列表查询失败。
|
||||
return options.stream().collect(Collectors.toMap(option -> option.getString("value"),
|
||||
option -> option.getString("text"), (first, ignored) -> first));
|
||||
}
|
||||
|
||||
private String getSexLimitName(Integer sexLimit) {
|
||||
if (Integer.valueOf(1).equals(sexLimit)) {
|
||||
return "男性";
|
||||
}
|
||||
if (Integer.valueOf(2).equals(sexLimit)) {
|
||||
return "女性";
|
||||
}
|
||||
return "全部开放";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.budwk.app.zhgh.inclusive.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* 幸福工荟列表和详情展示对象。
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class InclusiveBenefitVO {
|
||||
private String id;
|
||||
private Integer year;
|
||||
private String title;
|
||||
private String summary;
|
||||
private String tagType;
|
||||
private String tagTypeName;
|
||||
private String content;
|
||||
private Integer sexLimit;
|
||||
private String sexLimitName;
|
||||
private String cover;
|
||||
private String linkUrl;
|
||||
private Integer status;
|
||||
private String statusName;
|
||||
private Long createdAt;
|
||||
private Long updatedAt;
|
||||
private String publishTime;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import javax.validation.Valid;
|
||||
@At("/platform/TMap")
|
||||
public class TMapController {
|
||||
|
||||
private static String KEY = "MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH";
|
||||
private static String KEY = "NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU";
|
||||
@Inject
|
||||
private SysConfigService configService;
|
||||
|
||||
|
||||
+5
-42
@@ -10,7 +10,6 @@ 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.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
@@ -24,11 +23,9 @@ import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
@@ -95,7 +92,7 @@ public class MedicalCalculateMoneyController {
|
||||
* 按页面测算结果分配补助,并保存该年度结余。
|
||||
* 爱心金额显式按参数名绑定,避免 Nutz 将 BigDecimal 当作对象解析其内部字段。
|
||||
*
|
||||
* @param data 申请记录的 JSON 数组,包含申请 id 和最终测算金额 forecastSubsidyMoney
|
||||
* @param data 非空申请 JSON 数组,每项包含 id 和非负金额 forecastSubsidyMoney;保存时按笔四舍五入到元
|
||||
* @param ratio 页面当前补助比例,单位为百分比
|
||||
* @param aidFund 本年度会员缴费总额,单位为元
|
||||
* @param inComeSum 本年度收入累计总额,单位为元
|
||||
@@ -109,46 +106,12 @@ public class MedicalCalculateMoneyController {
|
||||
@SaCheckPermission("medicalMutualAid.medical.calculateMoney")
|
||||
public Result doHandle(String data, float ratio, double aidFund, double inComeSum,
|
||||
@Param("loveSubsidyMoney") BigDecimal loveSubsidyMoney){
|
||||
// 延续原整数参数未传时按零处理的行为,同时保留实际爱心补助的小数精度。
|
||||
if (loveSubsidyMoney == null) {
|
||||
loveSubsidyMoney = BigDecimal.ZERO;
|
||||
// 空请求不执行分配,避免仅更新年度结余而未保存任何申请金额。
|
||||
if (Lang.isEmpty(data)) {
|
||||
return Result.error("请提交需要分配的申请记录");
|
||||
}
|
||||
//获取申请最新年份
|
||||
Sql yearSql = Sqls.create("SELECT MAX(YEAR(applyTime)) AS currentYear FROM medical_apply");
|
||||
yearSql.setCallback(Sqls.callback.longs());
|
||||
medicalCalculateMoneyService.execute(yearSql);
|
||||
Integer year = yearSql.getInt();
|
||||
|
||||
List<MedicalApply> list = Json.fromJsonAsList(MedicalApply.class, data);
|
||||
list.forEach(v -> {
|
||||
v.setSubsidyMoney(v.getForecastSubsidyMoney());
|
||||
});
|
||||
medicalCalculateMoneyService.updateIgnoreNull(list);
|
||||
//补助总额
|
||||
BigDecimal subsidyMoney = list.stream()
|
||||
.map(MedicalApply::getForecastSubsidyMoney)
|
||||
.filter(Objects::nonNull) // 防御性:避免 null
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal incomeRounded = BigDecimal.valueOf(Math.round(inComeSum));
|
||||
BigDecimal thisYearBalance = incomeRounded
|
||||
.subtract(subsidyMoney)
|
||||
.subtract(loveSubsidyMoney);
|
||||
|
||||
|
||||
MedicalMoneyDetail moneyDetail = medicalCalculateMoneyService.dao().fetch(MedicalMoneyDetail.class, Cnd.where("year", "=", year));
|
||||
if (Lang.isEmpty(moneyDetail)) {
|
||||
moneyDetail = new MedicalMoneyDetail();
|
||||
moneyDetail.setYear(year);
|
||||
moneyDetail.setAidFund(new BigDecimal(aidFund));
|
||||
moneyDetail.setRatio(ratio);
|
||||
moneyDetail.setLastYearBalance(thisYearBalance);
|
||||
medicalCalculateMoneyService.insert(moneyDetail);
|
||||
} else {
|
||||
moneyDetail.setAidFund(new BigDecimal(aidFund));
|
||||
moneyDetail.setRatio(ratio);
|
||||
moneyDetail.setLastYearBalance(thisYearBalance);
|
||||
medicalCalculateMoneyService.updateIgnoreNull(moneyDetail);
|
||||
}
|
||||
medicalCalculateMoneyService.saveAllocation(list, ratio, aidFund, inComeSum, loveSubsidyMoney);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -1,9 +1,11 @@
|
||||
package com.budwk.app.zhgh.staffbenefit.medicalMutualAid.medical.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.medical.model.MedicalApply;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public interface MedicalCalculateMoneyService extends BaseService {
|
||||
@@ -16,6 +18,19 @@ public interface MedicalCalculateMoneyService extends BaseService {
|
||||
|
||||
List<NutMap> getApplyData(double ratio);
|
||||
|
||||
/**
|
||||
* 按笔取整并保存分配结果,使用实际写入的补助合计更新年度结余。
|
||||
* 返回类型为 void;申请金额和年度结余在同一事务中保存,校验失败时抛出业务异常。
|
||||
*
|
||||
* @param list 非空申请列表,每项使用 id 和非负的 forecastSubsidyMoney,其他字段不参与更新
|
||||
* @param ratio 当前分配比例,单位为百分比
|
||||
* @param aidFund 本年度会员缴费总额,单位为元
|
||||
* @param inComeSum 本年度收入累计总额,单位为元,沿用原收入取整口径
|
||||
* @param loveSubsidyMoney 实际爱心补助合计,保留小数精度,null 按零处理
|
||||
*/
|
||||
void saveAllocation(List<MedicalApply> list, float ratio, double aidFund, double inComeSum,
|
||||
BigDecimal loveSubsidyMoney);
|
||||
|
||||
/**
|
||||
* 按列表测算口径导出申请数据;返回类型为 void,Excel 列对应申请信息、费用和测算金额。
|
||||
*
|
||||
|
||||
+67
-11
@@ -5,6 +5,7 @@ import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
|
||||
@@ -14,10 +15,13 @@ import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.medical.model.MedicalMon
|
||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.medical.service.MedicalApplyService;
|
||||
import com.budwk.app.zhgh.staffbenefit.medicalMutualAid.medical.service.MedicalCalculateMoneyService;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Lang;
|
||||
@@ -27,8 +31,10 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
@@ -99,7 +105,7 @@ public class MedicalCalculateMoneyServiceImpl extends BaseServiceImpl implements
|
||||
* 返回最新申请年度的已办结测算列表,供页面和导出共用。
|
||||
*
|
||||
* @param ratio 补助比例的百分数,例如 60 表示按 60% 测算
|
||||
* @return 每项为申请及费用字段的 NutMap;subsidyMoney、forecastSubsidyMoney 按笔四舍五入到整数元
|
||||
* @return 每项为申请及费用字段的 NutMap;subsidyMoney 保留已保存金额,forecastSubsidyMoney 按笔四舍五入到整数元
|
||||
*/
|
||||
@Override
|
||||
public List<NutMap> getApplyData(double ratio) {
|
||||
@@ -114,18 +120,68 @@ public class MedicalCalculateMoneyServiceImpl extends BaseServiceImpl implements
|
||||
|
||||
applyList.forEach(apply -> {
|
||||
apply.putAll(medicalApplyService.calcMedicalMoney(apply.getString("id"),null, ratio));
|
||||
// 本页按每笔补助四舍五入到元,列表、导出和按比例分配共用取整后的金额;空金额按零处理。
|
||||
for (String field : List.of("subsidyMoney", "forecastSubsidyMoney")) {
|
||||
String money = apply.getString(field);
|
||||
apply.put(field, StrUtil.isBlank(money) ? BigDecimal.ZERO
|
||||
: new BigDecimal(money).setScale(0, RoundingMode.HALF_UP));
|
||||
}
|
||||
// 仅对待分配的测算金额取整;已保存金额保留原值,避免掩盖尚未重新分配的历史小数。
|
||||
String money = apply.getString("forecastSubsidyMoney");
|
||||
apply.put("forecastSubsidyMoney", StrUtil.isBlank(money) ? BigDecimal.ZERO
|
||||
: new BigDecimal(money).setScale(0, RoundingMode.HALF_UP));
|
||||
});
|
||||
|
||||
|
||||
return applyList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveAllocation(List<MedicalApply> list, float ratio, double aidFund, double inComeSum,
|
||||
BigDecimal loveSubsidyMoney) {
|
||||
// 先校验整批参数;空金额不能当作零覆盖,重复申请不能重复计入年度支出。
|
||||
if (Lang.isEmpty(list)) {
|
||||
throw new BaseException("请提交需要分配的申请记录");
|
||||
}
|
||||
Set<String> applyIds = new HashSet<>();
|
||||
for (MedicalApply apply : list) {
|
||||
if (apply == null || StrUtil.isBlank(apply.getId()) || apply.getForecastSubsidyMoney() == null
|
||||
|| apply.getForecastSubsidyMoney().signum() < 0) {
|
||||
throw new BaseException("申请记录及补助金额不能为空,补助金额不能为负数");
|
||||
}
|
||||
if (!applyIds.add(apply.getId())) {
|
||||
throw new BaseException("申请记录重复,请刷新后重新分配");
|
||||
}
|
||||
}
|
||||
|
||||
Sql yearSql = Sqls.create("SELECT MAX(YEAR(applyTime)) AS currentYear FROM medical_apply");
|
||||
yearSql.setCallback(Sqls.callback.longs());
|
||||
dao().execute(yearSql);
|
||||
Integer year = yearSql.getInt();
|
||||
|
||||
BigDecimal subsidyMoney = BigDecimal.ZERO;
|
||||
for (MedicalApply apply : list) {
|
||||
// 与测算列表使用相同的按笔取整规则,两列写入同一个整数元金额。
|
||||
BigDecimal money = apply.getForecastSubsidyMoney().setScale(0, RoundingMode.HALF_UP);
|
||||
dao().update(MedicalApply.class, Chain.make("forecastSubsidyMoney", money)
|
||||
.add("subsidyMoney", money), Cnd.where("id", "=", apply.getId()));
|
||||
subsidyMoney = subsidyMoney.add(money);
|
||||
}
|
||||
// 结余扣除实际保存的整数补助合计;爱心补助独立保留小数,不受常规补助取整影响。
|
||||
BigDecimal thisYearBalance = BigDecimal.valueOf(Math.round(inComeSum))
|
||||
.subtract(subsidyMoney)
|
||||
.subtract(loveSubsidyMoney == null ? BigDecimal.ZERO : loveSubsidyMoney);
|
||||
MedicalMoneyDetail moneyDetail = dao().fetch(MedicalMoneyDetail.class, Cnd.where("year", "=", year));
|
||||
if (Lang.isEmpty(moneyDetail)) {
|
||||
moneyDetail = new MedicalMoneyDetail();
|
||||
moneyDetail.setYear(year);
|
||||
moneyDetail.setAidFund(BigDecimal.valueOf(aidFund));
|
||||
moneyDetail.setRatio(ratio);
|
||||
moneyDetail.setLastYearBalance(thisYearBalance);
|
||||
insert(moneyDetail);
|
||||
} else {
|
||||
moneyDetail.setAidFund(BigDecimal.valueOf(aidFund));
|
||||
moneyDetail.setRatio(ratio);
|
||||
moneyDetail.setLastYearBalance(thisYearBalance);
|
||||
updateIgnoreNull(moneyDetail);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doExport(double ratio, HttpServletResponse response) {
|
||||
// 导出复用列表查询结果,保证服务端测算金额与页面初次加载时的口径一致。
|
||||
@@ -168,8 +224,8 @@ public class MedicalCalculateMoneyServiceImpl extends BaseServiceImpl implements
|
||||
ExcelExportEntity loveSubsidyMoneyExcelExportEntity = new ExcelExportEntity("爱心补助", "loveSubsidyMoney", 20);
|
||||
loveSubsidyMoneyExcelExportEntity.setType(10);
|
||||
exportEntities.add(loveSubsidyMoneyExcelExportEntity);
|
||||
// 页面“当前补助金额”展示的是测算字段,导出同名列保持相同取值。
|
||||
ExcelExportEntity subsidyMoneyExcelExportEntity = new ExcelExportEntity("当前补助金额", "forecastSubsidyMoney", 20);
|
||||
// 当前补助读取已保存值,按比例补助读取待分配值,与页面两列含义保持一致。
|
||||
ExcelExportEntity subsidyMoneyExcelExportEntity = new ExcelExportEntity("当前补助金额", "subsidyMoney", 20);
|
||||
subsidyMoneyExcelExportEntity.setType(10);
|
||||
exportEntities.add(subsidyMoneyExcelExportEntity);
|
||||
ExcelExportEntity forecastSubsidyMoneyExcelExportEntity = new ExcelExportEntity("按" + ratio + "%补助金额", "forecastSubsidyMoney", 20);
|
||||
@@ -181,10 +237,10 @@ public class MedicalCalculateMoneyServiceImpl extends BaseServiceImpl implements
|
||||
try {
|
||||
List<String> moneyFields = List.of("hospitalSumMoney$", "reimbursementMoney$", "singleBearMoney$",
|
||||
"personExpenseMoney$", "outpatientServiceMoney$", "bxyMoney$", "totalMoney",
|
||||
"loveSubsidyMoney", "forecastSubsidyMoney", "medicalSubsidyMoney");
|
||||
"loveSubsidyMoney", "subsidyMoney", "forecastSubsidyMoney", "medicalSubsidyMoney");
|
||||
NutMap summary = new NutMap();
|
||||
summary.put("username", "合计");
|
||||
// 按金额字段精确累加;两个测算金额列共用的字段只汇总一次,空金额按零处理。
|
||||
// 已保存补助和测算补助分别精确累加,空金额按零处理。
|
||||
for (String field : moneyFields) {
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (NutMap row : list) {
|
||||
|
||||
+391
-86
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<van-action-sheet
|
||||
v-model="visible"
|
||||
<van-popup
|
||||
:value="visible"
|
||||
class="activity-schedule-calendar"
|
||||
:style="{ height: '78%' }"
|
||||
:close-on-click-overlay="true"
|
||||
title="活动日历"
|
||||
closeable>
|
||||
position="right"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false">
|
||||
<van-nav-bar title="课程日历" left-text="返回" left-arrow
|
||||
@click-left="closeSignPopup('calendar')"></van-nav-bar>
|
||||
<div class="activity-schedule-calendar__body">
|
||||
<div class="activity-schedule-calendar__month-bar">
|
||||
<button type="button" class="activity-schedule-calendar__month-btn" @click="changeMonth(-1)">
|
||||
@@ -30,7 +32,10 @@
|
||||
:class="{
|
||||
'is-other-month': !day.currentMonth,
|
||||
'is-selected': day.key === selectedDate,
|
||||
'is-today': day.isToday
|
||||
'is-today': day.isToday,
|
||||
'is-signed': day.status === 'signed',
|
||||
'is-available': day.status === 'available',
|
||||
'is-pending': day.status === 'pending'
|
||||
}"
|
||||
@click="selectDay(day)">
|
||||
<span>
|
||||
@@ -42,13 +47,11 @@
|
||||
</div>
|
||||
|
||||
<div class="activity-schedule-calendar__legend">
|
||||
<span><i class="is-solid"></i>已签到</span>
|
||||
<span><i class="is-outline"></i>可签到</span>
|
||||
<span><i class="is-muted"></i>未开始</span>
|
||||
<span><i class="is-signed"></i>已签到</span>
|
||||
<span><i class="is-available"></i>可签到</span>
|
||||
<span><i class="is-pending"></i>未开始</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showMap" :id="mapContainerId" class="activity-schedule-calendar__map"></div>
|
||||
|
||||
<section class="activity-schedule-calendar__schedule-card">
|
||||
<div class="activity-schedule-calendar__schedule-title">
|
||||
{{ selectedDate ? selectedDate + ' 的活动时段' : '活动时段' }}
|
||||
@@ -72,6 +75,7 @@
|
||||
v-else-if="canSignItem(item)"
|
||||
type="button"
|
||||
class="activity-schedule-calendar__sign-btn"
|
||||
:disabled="formLoading"
|
||||
@click.stop="onSign(item)">签到</button>
|
||||
<span v-else class="activity-schedule-calendar__status" :class="{
|
||||
'is-ended': getScheduleStatus(item) === '已结束',
|
||||
@@ -88,16 +92,53 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
</van-popup>
|
||||
|
||||
<van-popup
|
||||
v-model="signVisible"
|
||||
:value="positionConfirmVisible"
|
||||
class="activity-schedule-position-popup"
|
||||
position="right"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
@opened="initMap"
|
||||
@closed="onPositionPopupClosed">
|
||||
<div class="activity-schedule-position">
|
||||
<van-nav-bar title="定位签到" left-text="返回" left-arrow
|
||||
@click-left="closeSignPopup('position-confirm')"></van-nav-bar>
|
||||
<div :id="mapContainerId" class="activity-schedule-calendar__map"></div>
|
||||
<div class="activity-schedule-position__details">
|
||||
<div class="activity-schedule-position__location-tools">
|
||||
<span class="activity-schedule-position__location-status" role="status">{{ locationMessage }}</span>
|
||||
<van-button size="small" plain type="info" icon="aim" :loading="locationLoading"
|
||||
:disabled="formLoading || !!closingPopupKey" @click="refreshLocation">重新定位</van-button>
|
||||
</div>
|
||||
<div class="activity-schedule-position__title">{{ row ? row.courseName : '' }}</div>
|
||||
<div v-if="selectCourseTime" class="activity-schedule-position__text">
|
||||
{{ dateKey(selectCourseTime) }} {{ formatTime(selectCourseTime.courseStartTime) }} - {{ formatTime(selectCourseTime.courseEndTime) }}
|
||||
</div>
|
||||
<div v-if="row && row.courseLocation" class="activity-schedule-position__text">
|
||||
<van-icon name="location-o"></van-icon>{{ row.courseLocation }}
|
||||
</div>
|
||||
<div class="activity-schedule-position__text">签到范围:{{ row ? (row.radius || 100) : 100 }} 米</div>
|
||||
</div>
|
||||
<div class="activity-schedule-position__footer">
|
||||
<van-button type="info" block round :loading="formLoading" :disabled="formLoading || locationLoading || !!closingPopupKey"
|
||||
loading-text="签到中..." @click="onConfirmPositionSign">确认签到</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-popup
|
||||
:value="signVisible"
|
||||
round
|
||||
closeable
|
||||
:safe-area-inset-bottom="true"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
:style="{ width: '80%', height: '66%' }"
|
||||
get-container="#app"
|
||||
@click-close-icon="closeSignPopup('scan')"
|
||||
@close="onSignClose">
|
||||
<scan-code ref="scanCodeRef"></scan-code>
|
||||
</van-popup>
|
||||
@@ -126,7 +167,17 @@ module.exports = {
|
||||
weekdays: ["日", "一", "二", "三", "四", "五", "六"],
|
||||
selectCourseTime: null,
|
||||
signVisible: false,
|
||||
signCodePreview: null,
|
||||
positionConfirmVisible: false,
|
||||
formLoading: false,
|
||||
popupHistoryToken: "",
|
||||
calendarHistoryOpen: false,
|
||||
closingPopupKey: "",
|
||||
markerLayer: null,
|
||||
userMarkerLayer: null,
|
||||
locationLoading: false,
|
||||
locationMessage: "",
|
||||
locationRequestId: 0,
|
||||
map: null,
|
||||
circle: null,
|
||||
mapContainerId: "activityScheduleMap_" + Math.random().toString(36).slice(2)
|
||||
@@ -181,9 +232,11 @@ module.exports = {
|
||||
this.currentMonth = date.month() + 1
|
||||
this.selectedDate = initialDate
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
if (this.showMap) this.initMap()
|
||||
})
|
||||
// 所有签到模式的日历均入栈;上层扫码、二维码或定位弹框单独入栈,返回时逐层关闭。
|
||||
if (!this.calendarHistoryOpen) {
|
||||
this.calendarHistoryOpen = true
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "calendar")
|
||||
}
|
||||
},
|
||||
dateKey(item) {
|
||||
const value = item && (item.courseDate || item.courseStartTime)
|
||||
@@ -230,18 +283,43 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
onSign(courseTime) {
|
||||
// 确认框关闭或定位提交期间不再切换时段,避免重复打卡和签到对象错位。
|
||||
if (this.formLoading || this.positionConfirmVisible || this.signVisible || this.signCodePreview || this.closingPopupKey) return
|
||||
this.selectCourseTime = courseTime
|
||||
if (this.row.signType === 1) {
|
||||
this.signVisible = true
|
||||
this.$nextTick(() => this.$refs.scanCodeRef.init())
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "scan")
|
||||
this.$nextTick(() => {
|
||||
if (this.signVisible) this.$refs.scanCodeRef.init()
|
||||
})
|
||||
} else if (this.row.signType === 2) {
|
||||
this.makeCode()
|
||||
} else if (this.row.signType === 3 && this.moduleType === "brand") {
|
||||
this.positionSign()
|
||||
this.positionConfirmVisible = true
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "position-confirm")
|
||||
} else {
|
||||
this.$toast("此签到模式正在升级中")
|
||||
}
|
||||
},
|
||||
/**
|
||||
* popupKey 为 calendar(日历)、scan(扫码)、sign-code(二维码)或 position-confirm(定位确认);无返回值。
|
||||
* 仅请求关闭栈顶弹层,由全局历史回调同步显示状态,防止连续点击造成多次返回。
|
||||
*/
|
||||
closeSignPopup(popupKey) {
|
||||
if (this.closingPopupKey) return
|
||||
if (window.h5PopupHistory.close(this.popupHistoryToken, popupKey)) {
|
||||
this.closingPopupKey = popupKey
|
||||
}
|
||||
},
|
||||
// 预览定位不提交签到;点击确认时重新获取位置,失败时保留弹框和当前时段供重试。
|
||||
onConfirmPositionSign() {
|
||||
if (this.formLoading || this.locationLoading || this.closingPopupKey || !this.positionConfirmVisible) return
|
||||
this.positionSign()
|
||||
},
|
||||
// 退场动画完成后释放地图;快速重新打开时保留新弹框正在使用的实例。
|
||||
onPositionPopupClosed() {
|
||||
if (!this.positionConfirmVisible) this.destroyMap()
|
||||
},
|
||||
onSignClose() {
|
||||
if (this.$refs.scanCodeRef) this.$refs.scanCodeRef.closeScan()
|
||||
},
|
||||
@@ -250,90 +328,214 @@ module.exports = {
|
||||
? "/platform/family/mine/passiveScan"
|
||||
: "/platform/trainSignUp/mine/passiveScan"
|
||||
const content = jrQrcode.getQrBase64(baseUrl + "?id=" + this.selectCourseTime.id)
|
||||
vant.ImagePreview([content])
|
||||
// 二维码预览关闭时同步回退历史;历史回调先释放引用,避免再次触发返回。
|
||||
const preview = vant.ImagePreview({
|
||||
images: [content],
|
||||
closeOnPopstate: false,
|
||||
onClose: () => {
|
||||
if (this.signCodePreview === preview) this.closeSignPopup("sign-code")
|
||||
}
|
||||
})
|
||||
this.signCodePreview = preview
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "sign-code")
|
||||
},
|
||||
/**
|
||||
* 确认后提交所选时段的点位签到,无方法参数、无返回值。
|
||||
* 接口参数:timeId 为报名时段 ID,courseId 为课程 ID,lat/lng 为定位经纬度。
|
||||
* 返回对象的 code 为 0 表示成功,msg 为失败提示;定位或请求结束后释放提交状态。
|
||||
*/
|
||||
positionSign() {
|
||||
this.getLocation(coords => {
|
||||
if (!coords) return
|
||||
if (this.formLoading || this.locationLoading) return
|
||||
const courseTime = this.selectCourseTime
|
||||
const course = this.row
|
||||
const requestId = ++this.locationRequestId
|
||||
this.formLoading = true
|
||||
this.locationMessage = "正在获取当前位置..."
|
||||
this.getLocation((coords, errorMessage) => {
|
||||
// 定位失败或等待定位时已退出地图弹框,不再发起签到请求。
|
||||
if (!this.visible || !this.positionConfirmVisible || requestId !== this.locationRequestId || this.row !== course || this.selectCourseTime !== courseTime) {
|
||||
this.formLoading = false
|
||||
return
|
||||
}
|
||||
this.showCurrentLocation(coords, errorMessage)
|
||||
if (!coords) {
|
||||
this.formLoading = false
|
||||
return
|
||||
}
|
||||
// 地图展示使用转换后的坐标,签到请求仍传手机返回的原始 WGS-84 经纬度。
|
||||
this.$axios.post("/platform/trainSignUp/mine/positionSign", {
|
||||
timeId: this.selectCourseTime.id,
|
||||
courseId: this.row.id,
|
||||
timeId: courseTime.id,
|
||||
courseId: course.id,
|
||||
lat: coords.lat,
|
||||
lng: coords.lng
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.selectCourseTime.isAttend = true
|
||||
this.$set(courseTime, "isAttend", true)
|
||||
this.$toast("签到成功")
|
||||
// 签到完成后清理本组件的弹框历史,返回时不再出现已完成的确认框或日历。
|
||||
return window.h5PopupHistory.clear(this.popupHistoryToken)
|
||||
} else {
|
||||
this.$toast(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
/**
|
||||
* callback 接收浏览器 WGS-84 定位对象 {lat, lng, accuracy} 和可选失败提示;无返回值。
|
||||
* lat/lng 为纬度/经度,accuracy 为精度半径(米);失败时坐标为 null。
|
||||
* 预览和确认签到均重新定位,不使用上一次定位缓存,是否提交由调用方决定。
|
||||
*/
|
||||
getLocation(callback) {
|
||||
if (!navigator.geolocation) {
|
||||
this.$toast("当前浏览器不支持定位功能")
|
||||
callback(null)
|
||||
callback(null, "当前浏览器不支持定位功能")
|
||||
return
|
||||
}
|
||||
const loading = vant.Toast.loading({ message: "获取定位中...", forbidClick: false, duration: 0 })
|
||||
navigator.geolocation.getCurrentPosition(position => {
|
||||
loading.close()
|
||||
callback({ lat: position.coords.latitude, lng: position.coords.longitude })
|
||||
callback({ lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy })
|
||||
}, error => {
|
||||
loading.close()
|
||||
const messageMap = {
|
||||
1: "请允许浏览器获取位置信息",
|
||||
2: "无法获取当前位置",
|
||||
3: "定位超时,请重试"
|
||||
}
|
||||
this.$toast(messageMap[error.code] || "定位失败,请稍后重试")
|
||||
callback(null)
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 })
|
||||
callback(null, messageMap[error.code] || "定位失败,请稍后重试")
|
||||
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 })
|
||||
},
|
||||
// 打开地图和点击重新定位只刷新预览;请求编号隔离已关闭或已重新打开的弹框。
|
||||
refreshLocation() {
|
||||
if (!this.map || !this.positionConfirmVisible || this.locationLoading || this.formLoading || this.closingPopupKey) return
|
||||
const requestId = ++this.locationRequestId
|
||||
this.locationLoading = true
|
||||
this.locationMessage = "正在获取当前位置..."
|
||||
this.getLocation((coords, errorMessage) => {
|
||||
if (requestId !== this.locationRequestId || !this.map || !this.positionConfirmVisible) return
|
||||
this.locationLoading = false
|
||||
this.showCurrentLocation(coords, errorMessage)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* coords 为手机原始定位对象,errorMessage 为失败提示;无返回值,不提交签到。
|
||||
* 国内坐标复用项目工具转换为腾讯地图使用的 GCJ-02,境外保留原坐标。
|
||||
* 定位失败时移除旧位置点,避免将过期点位误认为当前位置。
|
||||
*/
|
||||
showCurrentLocation(coords, errorMessage) {
|
||||
if (!coords) {
|
||||
if (this.userMarkerLayer) this.userMarkerLayer.setGeometries([])
|
||||
this.locationMessage = errorMessage || "暂未获取到当前位置"
|
||||
this.$toast(this.locationMessage)
|
||||
return
|
||||
}
|
||||
this.locationMessage = Number.isFinite(coords.accuracy)
|
||||
? "定位精度约 " + Math.ceil(coords.accuracy) + " 米"
|
||||
: "已显示我的位置"
|
||||
if (!this.map || !this.userMarkerLayer) return
|
||||
const converted = this.$coordinateUtil.outOfChina(coords.lng, coords.lat)
|
||||
? [coords.lat, coords.lng]
|
||||
: this.$coordinateUtil.wgs84ToGcj02(coords.lng, coords.lat)
|
||||
const userPosition = new TMap.LatLng(converted[0], converted[1])
|
||||
this.userMarkerLayer.setGeometries([{ id: "current-user", styleId: "user", position: userPosition }])
|
||||
const activityPosition = this.markerLayer.getGeometries()[0].position
|
||||
// 同时显示签到点和我的位置,并留出边距;两点重合时仍保留可辨识的地图范围。
|
||||
const latPadding = Math.max(Math.abs(activityPosition.getLat() - converted[0]) * 0.3, 0.0003)
|
||||
const lngPadding = Math.max(Math.abs(activityPosition.getLng() - converted[1]) * 0.3, 0.0003)
|
||||
const bounds = new TMap.LatLngBounds(
|
||||
new TMap.LatLng(Math.min(activityPosition.getLat(), converted[0]) - latPadding, Math.min(activityPosition.getLng(), converted[1]) - lngPadding),
|
||||
new TMap.LatLng(Math.max(activityPosition.getLat(), converted[0]) + latPadding, Math.max(activityPosition.getLng(), converted[1]) + lngPadding)
|
||||
)
|
||||
this.map.fitBounds(bounds)
|
||||
},
|
||||
// 仅在侧滑弹框完全展开后创建地图,保证容器已有可用尺寸,避免动画期间地图空白。
|
||||
initMap() {
|
||||
if (!this.row.courseLocationCoordinates || typeof TMap === "undefined") return
|
||||
if (!this.positionConfirmVisible || !this.showMap || !this.row.courseLocationCoordinates || typeof TMap === "undefined") return
|
||||
const position = JSON.parse(this.row.courseLocationCoordinates)
|
||||
const center = new TMap.LatLng(position[0], position[1])
|
||||
if (this.map) this.map.destroy()
|
||||
this.destroyMap()
|
||||
this.map = new TMap.Map(this.mapContainerId, { zoom: 16, center })
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: [{ id: "activity", position: center }]
|
||||
})
|
||||
// 蓝点与“我的位置”标签独立于红色签到点;内嵌 SVG 不依赖外部图片请求。
|
||||
const userIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="80" height="48" viewBox="0 0 80 48">'
|
||||
+ '<rect x="2" y="1" width="76" height="22" rx="11" fill="#1989fa"/>'
|
||||
+ '<text x="40" y="16" text-anchor="middle" font-family="sans-serif" font-size="12" fill="white">我的位置</text>'
|
||||
+ '<circle cx="40" cy="36" r="11" fill="#1989fa" fill-opacity="0.2"/>'
|
||||
+ '<circle cx="40" cy="36" r="6" fill="#1989fa" stroke="white" stroke-width="2"/></svg>'
|
||||
this.userMarkerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
zIndex: 2,
|
||||
styles: { user: new TMap.MarkerStyle({ width: 80, height: 48, anchor: { x: 40, y: 36 }, src: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(userIcon) }) },
|
||||
geometries: []
|
||||
})
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{ center, radius: this.row.radius || 100 }]
|
||||
})
|
||||
this.refreshLocation()
|
||||
},
|
||||
// 关闭弹框或销毁组件时释放地图及覆盖物引用,避免重复打开后残留地图实例。
|
||||
destroyMap() {
|
||||
// 使尚未返回的定位请求失效,避免旧结果写入下一次打开的地图。
|
||||
this.locationRequestId += 1
|
||||
this.locationLoading = false
|
||||
this.locationMessage = ""
|
||||
if (this.map) this.map.destroy()
|
||||
this.map = null
|
||||
this.markerLayer = null
|
||||
this.userMarkerLayer = null
|
||||
this.circle = null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 复用布局已加载的全局历史管理器,不在组件内注册 popstate 事件。
|
||||
this.popupHistoryToken = window.h5PopupHistory.register((popupKey) => {
|
||||
this.closingPopupKey = ""
|
||||
if (popupKey === "calendar") {
|
||||
this.calendarHistoryOpen = false
|
||||
this.visible = false
|
||||
} else if (popupKey === "scan") {
|
||||
this.signVisible = false
|
||||
} else if (popupKey === "sign-code") {
|
||||
const preview = this.signCodePreview
|
||||
this.signCodePreview = null
|
||||
if (preview) preview.close()
|
||||
} else if (popupKey === "position-confirm") {
|
||||
this.positionConfirmVisible = false
|
||||
this.locationRequestId += 1
|
||||
this.locationLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 页面离开时停止扫码并释放地图,再清理历史并注销回调,避免摄像头继续占用。
|
||||
if (this.signVisible) this.onSignClose()
|
||||
this.positionConfirmVisible = false
|
||||
this.destroyMap()
|
||||
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
|
||||
window.h5PopupHistory.unregister(this.popupHistoryToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-schedule-calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 18px 18px 0 0;
|
||||
background: #f4f7fc;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar /deep/ .van-action-sheet__header {
|
||||
height: 50px;
|
||||
color: #1f2f43;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar /deep/ .van-action-sheet__close {
|
||||
top: 14px;
|
||||
color: #7e899a;
|
||||
font-size: 21px;
|
||||
.activity-schedule-calendar > .van-nav-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__body {
|
||||
height: 100%;
|
||||
padding: 14px 14px 24px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 18px 14px calc(24px + env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
overflow-anchor: none;
|
||||
box-sizing: border-box;
|
||||
@@ -383,8 +585,9 @@ module.exports = {
|
||||
.activity-schedule-calendar__day {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 38px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
max-width: 44px;
|
||||
height: 52px;
|
||||
margin: 1px auto;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
@@ -400,13 +603,13 @@ module.exports = {
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
@@ -436,15 +639,27 @@ module.exports = {
|
||||
color: #bdc5d0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-signed > span {
|
||||
color: #148a52;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-available > span {
|
||||
color: #1675d1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-pending > span {
|
||||
color: #66758a;
|
||||
}
|
||||
|
||||
/* 选中蓝底与外层状态环独立:切换日期时仍能分辨已签到、可签到和未开始。 */
|
||||
.activity-schedule-calendar__day.is-selected > span {
|
||||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
color: #fff;
|
||||
background: #1989fa;
|
||||
box-shadow: 0 5px 12px rgba(25, 137, 250, .22);
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-today .activity-schedule-calendar__day-number {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
@@ -454,60 +669,150 @@ module.exports = {
|
||||
|
||||
.activity-schedule-calendar__day-status {
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
transform: translateX(-50%);
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-signed {
|
||||
background: #1989fa;
|
||||
.activity-schedule-calendar__day-status.is-signed,
|
||||
.activity-schedule-calendar__legend .is-signed {
|
||||
border: 2px solid #19a965;
|
||||
background: #eaf8f1;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-available {
|
||||
border: 1px solid #1989fa;
|
||||
.activity-schedule-calendar__day-status.is-available,
|
||||
.activity-schedule-calendar__legend .is-available {
|
||||
border: 2px solid #1989fa;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day-status.is-pending {
|
||||
background: #d8dde5;
|
||||
.activity-schedule-calendar__day-status.is-pending,
|
||||
.activity-schedule-calendar__legend .is-pending {
|
||||
border: 2px dashed #98a4b5;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend {
|
||||
display: flex;
|
||||
margin: 12px 4px 16px;
|
||||
margin: 16px 0 20px;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
color: #78869a;
|
||||
font-size: 11px;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: #58677b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend span {
|
||||
display: inline-flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend i {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__legend .is-solid { background: #1989fa; }
|
||||
.activity-schedule-calendar__legend .is-outline { border: 1px solid #1989fa; }
|
||||
.activity-schedule-calendar__legend .is-muted { background: #d8dde5; }
|
||||
/* 小屏幕每列不足 42px 时缩小圆环,保留日期间距和选中态的白色间隔。 */
|
||||
@media (max-width: 350px) {
|
||||
.activity-schedule-calendar__day-status {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__day > span {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__today-label,
|
||||
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-schedule-position-popup {
|
||||
background: #f4f7fc;
|
||||
}
|
||||
|
||||
.activity-schedule-position {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-schedule-position > .van-nav-bar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__map {
|
||||
flex: 1 0 240px;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin-bottom: 12px;
|
||||
min-height: 240px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__details {
|
||||
flex-shrink: 0;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-position__title {
|
||||
color: #263548;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-tools {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-tools .van-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-schedule-position__location-status {
|
||||
color: #78869a;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.activity-schedule-position__text {
|
||||
margin-top: 6px;
|
||||
color: #78869a;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.activity-schedule-position__text .van-icon {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.activity-schedule-position__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activity-schedule-calendar__schedule-card {
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
placeholder="请输入关键词查询地址"
|
||||
@select="handleSelect"
|
||||
:trigger-on-focus="false"
|
||||
:disabled="!map"
|
||||
/>
|
||||
</div>
|
||||
<div id="mapContainer"></div>
|
||||
<el-alert v-if="mapError" :title="mapError" type="error" :closable="false" show-icon></el-alert>
|
||||
<div :id="mapContainerId" class="tmap-container" v-loading="mapLoading"></div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,9 +36,12 @@ module.exports = {
|
||||
data() {
|
||||
return {
|
||||
map: null,
|
||||
// Vue 实例编号区分同页多个地图,避免地图挂到其他弹窗的容器中。
|
||||
mapContainerId: 'tmap-container-' + this._uid,
|
||||
mapLoading: false,
|
||||
mapError: '',
|
||||
mapDisposed: false,
|
||||
poi: this.position,
|
||||
appMapCenterPointX: 0,
|
||||
appMapCenterPointY: 0,
|
||||
markerLayer: null,
|
||||
addressValue: '',
|
||||
circle: null,
|
||||
@@ -43,7 +49,7 @@ module.exports = {
|
||||
},
|
||||
methods: {
|
||||
handleSelect(item) {
|
||||
if (item.location) {
|
||||
if (this.map && item.location) {
|
||||
// 定位到中心点
|
||||
const center = new TMap.LatLng(item.location.lat, item.location.lng)
|
||||
this.map.setCenter(center)
|
||||
@@ -52,41 +58,84 @@ module.exports = {
|
||||
this.poi = [item.location.lat, item.location.lng]
|
||||
}
|
||||
},
|
||||
async querySearchAsync(queryString, cb) {
|
||||
// queryString 为用户输入的地址关键词;接口 data.data 是地点数组,cb 接收带 value 的候选项数组。
|
||||
querySearchAsync(queryString, cb) {
|
||||
if (!queryString) {
|
||||
cb([])
|
||||
return
|
||||
}
|
||||
const res = await this.$axios.post("/platform/TMap/suggestion", {
|
||||
return this.$axios.post("/platform/TMap/suggestion", {
|
||||
keyword: queryString,
|
||||
}).then((res) => {
|
||||
if (this.mapDisposed) return
|
||||
if (res.code === 0 && res.data.status === 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
item.value = item.title + "【详细地址:" + item.address + "】"
|
||||
})
|
||||
cb(res.data.data)
|
||||
} else {
|
||||
this.$message.error((res.data && res.data.message) || res.msg || '地点搜索失败')
|
||||
cb([])
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!this.mapDisposed) cb([])
|
||||
})
|
||||
if (res.code === 0 && res.data.status === 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
item.value = item.title + "【详细地址:" + item.address + "】"
|
||||
})
|
||||
cb(res.data.data)
|
||||
} else {
|
||||
this.$message.error(res.msg)
|
||||
cb([])
|
||||
}
|
||||
},
|
||||
initMap() {
|
||||
const center = new TMap.LatLng(this.appMapCenterPointY, this.appMapCenterPointX)
|
||||
this.map = new TMap.Map('mapContainer', {
|
||||
resizeEnable: true,
|
||||
// 接收 [纬度, 经度],返回数值坐标或 null;拒绝空配置,防止空字符串被当成 0。
|
||||
normalizePosition(position) {
|
||||
if (!Array.isArray(position) || position.length !== 2) return null
|
||||
if (position.some(value => !['number', 'string'].includes(typeof value) || String(value).trim() === '')) return null
|
||||
const point = position.map(value => Number(value))
|
||||
if (!point.every(value => Number.isFinite(value)) || Math.abs(point[0]) > 90 || Math.abs(point[1]) > 180) return null
|
||||
return point
|
||||
},
|
||||
// 已设置的签到点直接用于回显;仅无有效点位时读取默认中心,避免配置请求阻断已有点位。
|
||||
loadMap() {
|
||||
if (this.mapDisposed || this.map || this.mapLoading) return
|
||||
this.mapLoading = true
|
||||
this.mapError = ''
|
||||
return Promise.resolve().then(() => {
|
||||
if (this.mapDisposed) return null
|
||||
if (typeof TMap === 'undefined' || !TMap.Map || !TMap.LatLng) {
|
||||
throw new Error('腾讯地图脚本未加载成功,请刷新页面后重试')
|
||||
}
|
||||
const point = this.normalizePosition(this.poi)
|
||||
if (point) return point
|
||||
return Promise.all([
|
||||
this.getConfigKey('AppMapCenterPointY'),
|
||||
this.getConfigKey('AppMapCenterPointX')
|
||||
]).then(values => {
|
||||
const center = this.normalizePosition(values)
|
||||
if (!center) throw new Error('地图默认中心未配置或无效,请检查系统配置 AppMapCenterPointX(经度)、AppMapCenterPointY(纬度)')
|
||||
return center
|
||||
})
|
||||
}).then(point => {
|
||||
// 弹窗关闭后可能仍收到配置响应,禁止在已销毁的容器中创建地图。
|
||||
if (!this.mapDisposed && point) this.initMap(point)
|
||||
}).catch(error => {
|
||||
if (this.mapDisposed) return
|
||||
this.mapError = (error && (error.message || error.msg)) || '地图初始化失败,请刷新页面后重试'
|
||||
this.destroyMap()
|
||||
}).finally(() => {
|
||||
if (!this.mapDisposed) this.mapLoading = false
|
||||
})
|
||||
},
|
||||
// point 为经过校验的 [纬度, 经度];只设置显示中心,不将默认中心自动保存为签到点。
|
||||
initMap(point) {
|
||||
const center = new TMap.LatLng(point[0], point[1])
|
||||
this.map = new TMap.Map(this.mapContainerId, {
|
||||
zoom: 16,
|
||||
center: center
|
||||
})
|
||||
// 初始化
|
||||
// 标记和签到半径共用同一个中心,点击选点后向表单回传腾讯地图坐标。
|
||||
this.markerLayer = new TMap.MultiMarker({
|
||||
map: this.map,
|
||||
geometries: []
|
||||
});
|
||||
if (this.poi && this.poi.length > 0) {
|
||||
const posi = new TMap.LatLng(this.poi[0], this.poi[1])
|
||||
this.createMarker(posi)
|
||||
this.map.setCenter(posi)
|
||||
} else {
|
||||
this.createMarker(center)
|
||||
// 默认中心仅用于浏览地图;只有已配置的有效坐标才回显签到标记和范围圈。
|
||||
const selectedPoint = this.normalizePosition(this.poi)
|
||||
if (selectedPoint) {
|
||||
this.createMarker(new TMap.LatLng(selectedPoint[0], selectedPoint[1]))
|
||||
}
|
||||
this.map.on("click", (event) => {
|
||||
if (!this.view) {
|
||||
@@ -113,17 +162,24 @@ module.exports = {
|
||||
position: position
|
||||
}
|
||||
]);
|
||||
this.circle = new TMap.MultiCircle({
|
||||
map: this.map,
|
||||
geometries: [{
|
||||
center: position,
|
||||
radius: this.radius,
|
||||
}],
|
||||
});
|
||||
const geometries = [{center: position, radius: this.radius}]
|
||||
// 重复选点复用半径图层,避免每次点击遗留一个空图层。
|
||||
if (this.circle) {
|
||||
this.circle.setGeometries(geometries)
|
||||
} else {
|
||||
this.circle = new TMap.MultiCircle({map: this.map, geometries})
|
||||
}
|
||||
},
|
||||
async getConfigKey(key) {
|
||||
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
|
||||
return resp.data
|
||||
// key 传系统配置名 AppMapCenterPointX/Y;返回 Promise,值为 Result.data 中的经纬度配置字符串。
|
||||
getConfigKey(key) {
|
||||
return this.$axios.post("/open/common/getConfigKey", { key }).then(resp => resp.data)
|
||||
},
|
||||
// 弹窗关闭或初始化失败时释放地图及其图层、事件和 WebGL 资源。
|
||||
destroyMap() {
|
||||
if (this.map) this.map.destroy()
|
||||
this.map = null
|
||||
this.markerLayer = null
|
||||
this.circle = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -134,19 +190,19 @@ module.exports = {
|
||||
this.$emit("update:position", newVal)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// DOM初始化完成进行地图初始化
|
||||
this.$nextTick(async () => {
|
||||
this.appMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
|
||||
this.appMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
|
||||
this.initMap()
|
||||
})
|
||||
mounted() {
|
||||
// 父弹窗显示完成后挂载组件,再等待当前容器完成渲染。
|
||||
this.$nextTick(() => this.loadMap())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.mapDisposed = true
|
||||
this.destroyMap()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#mapContainer {
|
||||
.tmap-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
src="https://webapi.amap.com/maps?v=2.0&key=57f0d098ba1b881ecc436c4cfd23bbbf&plugin=AMap.PolyEditor,AMap.Geolocation"
|
||||
></script>
|
||||
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU"></script>
|
||||
<script nonce="${cspNonce!}">
|
||||
Vue.config.devtools = true
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
src="https://webapi.amap.com/maps?v=2.0&key=b4fa2e2bc10a725ab007b98a3aa447cc&plugin=AMap.PolyEditor,AMap.Geolocation"
|
||||
></script>-->
|
||||
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
|
||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU"></script>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
// 在加载 lodash 后、使用前插入
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<!--#include('/platform/zhgh/activity/trainSignUp/manage/courseTime.js'){}#-->
|
||||
<!--#include('/platform/zhgh/activity/trainSignUp/manage/customForm.js'){}#-->
|
||||
<!--#include('/platform/zhgh/activity/trainSignUp/manage/responsibleForm.js'){}#-->
|
||||
const basicForm = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-form :model="formData" label-width="110px" ref="form">
|
||||
<el-form :model="formData" :disabled="formLoading" label-width="110px" ref="form">
|
||||
<div v-show="step === 1">
|
||||
<el-row :gutter="20" type="flex" v-if="!formData.id">
|
||||
<el-col :span="12">
|
||||
@@ -235,13 +236,11 @@ const basicForm = {
|
||||
|
||||
<el-table-column :label="trainType + '负责人'" prop="courseInstructor">
|
||||
<template v-slot="{row}">
|
||||
<el-input size="small" v-model="row.courseInstructor" :placeholder="'请输入' + trainType + '负责人'"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="手机号" prop="courseInstructorMobile">
|
||||
<template v-slot="{row}">
|
||||
<el-input size="small" v-model="row.courseInstructorMobile" placeholder="请输入手机号"></el-input>
|
||||
<el-button @click="openSetUpResponsible(row)" :disabled="formLoading" type="text"
|
||||
:title="getResponsibleLabel(row)"
|
||||
style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
|
||||
{{ getResponsibleLabel(row) }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -290,11 +289,11 @@ const basicForm = {
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="float: right; margin: 20px 0">
|
||||
<el-button @click="$emit('back')">取消</el-button>
|
||||
<el-button v-if="step === 2" type="primary" @click="step = 1">上一步</el-button>
|
||||
<el-button v-if="step === 1" type="primary" @click="step = 2">下一步</el-button>
|
||||
<el-button type="primary" @click="onSave">{{ copyTemplateMode ? "另存为" : "保存" }}</el-button>
|
||||
<el-button type="primary" @click="onSubmit">{{ copyTemplateMode ? "新活动提交" : "提交" }}</el-button>
|
||||
<el-button :disabled="formLoading" @click="$emit('back')">取消</el-button>
|
||||
<el-button v-if="step === 2" :disabled="formLoading" type="primary" @click="step = 1">上一步</el-button>
|
||||
<el-button v-if="step === 1" :disabled="formLoading" type="primary" @click="step = 2">下一步</el-button>
|
||||
<el-button :loading="formLoading" type="primary" @click="onSave">{{ copyTemplateMode ? "另存为" : "保存" }}</el-button>
|
||||
<el-button :loading="formLoading" type="primary" @click="onSubmit">{{ copyTemplateMode ? "新活动提交" : "提交" }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制" append-to-body>
|
||||
@@ -322,6 +321,7 @@ const basicForm = {
|
||||
|
||||
<course-time ref="courseTimeRef"></course-time>
|
||||
<custom-form ref="customFormRef"></custom-form>
|
||||
<responsible-form ref="responsibleFormRef"></responsible-form>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["USER_CAMPUS", "TRAIN_SIGNUP_TYPE"],
|
||||
@@ -329,12 +329,13 @@ const basicForm = {
|
||||
return {
|
||||
signUpDialog: false,
|
||||
step: 1,
|
||||
formLoading: false,
|
||||
copyTemplateMode: false,
|
||||
copyTemplateName: "",
|
||||
formData: {
|
||||
notice: false,
|
||||
courseList: [
|
||||
{ orderNum: 1, courseName: "", courseInstructorMobile: "", courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false },
|
||||
{ orderNum: 1, courseName: "", courseInstructorMobile: "", responsibleList: [], courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false },
|
||||
],
|
||||
conditionStructure: {
|
||||
method: "AND",
|
||||
@@ -364,6 +365,7 @@ const basicForm = {
|
||||
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue"),
|
||||
"course-time": courseTime,
|
||||
"custom-form": customForm,
|
||||
"responsible-form": responsibleForm,
|
||||
},
|
||||
methods: {
|
||||
async validNumber(row, old) {
|
||||
@@ -464,11 +466,23 @@ const basicForm = {
|
||||
openSetUpCourseTime(index) {
|
||||
this.$refs.courseTimeRef.onOpen(this.formData, index)
|
||||
},
|
||||
// 只显示新方式设置的负责人姓名;没有负责人关系时显示设置入口,不读取历史手填姓名。
|
||||
getResponsibleLabel(row) {
|
||||
if (Array.isArray(row.responsibleList)) {
|
||||
const names = row.responsibleList.map(item => (item.username || '').trim()).filter(name => name)
|
||||
return names.length ? names.join('、') : '设置负责人'
|
||||
}
|
||||
return '设置负责人'
|
||||
},
|
||||
// 和设置时间一样从当前子活动行打开弹框,未保存的新增子活动也可以设置负责人。
|
||||
openSetUpResponsible(row) {
|
||||
this.$refs.responsibleFormRef.onOpen(row)
|
||||
},
|
||||
courseOrderNumChange() {
|
||||
this.formData.courseList = this.formData.courseList.sort((a, b) => a.orderNum - b.orderNum)
|
||||
},
|
||||
addCourse() {
|
||||
this.formData.courseList.push({ courseInstructorMobile: "", courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
||||
this.formData.courseList.push({ courseInstructorMobile: "", responsibleList: [], courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false })
|
||||
},
|
||||
async historicalActChange(val) {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/findOne", {id: val})
|
||||
@@ -510,6 +524,13 @@ const basicForm = {
|
||||
course.updatedBy = null
|
||||
course.updatedAt = null
|
||||
course.delFlag = null
|
||||
// 沿用负责人选择和联系方式,关系ID及所属活动由后端在新活动中重新生成。
|
||||
if (Array.isArray(course.responsibleList)) {
|
||||
this.$set(course, 'responsibleList', course.responsibleList.map(item => ({
|
||||
userId: item.userId, username: item.username, loginname: item.loginname,
|
||||
mobile: item.mobile, orderNum: item.orderNum
|
||||
})))
|
||||
}
|
||||
if (Array.isArray(course.courseTimeList)) {
|
||||
course.courseTimeList = course.courseTimeList.map((courseTime) => {
|
||||
courseTime.id = null
|
||||
@@ -543,7 +564,7 @@ const basicForm = {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/getHistoricalActList", {})
|
||||
return resp.data
|
||||
},
|
||||
async onSave() {
|
||||
onSave() {
|
||||
let valid = false
|
||||
this.$refs.form.validateField("activityName", (errMsg) => {
|
||||
valid = errMsg === ""
|
||||
@@ -555,16 +576,19 @@ const basicForm = {
|
||||
if (!this.validateCopyTemplateName()) {
|
||||
return
|
||||
}
|
||||
await this.doHandle(this.copyTemplateMode ? '另存为' : '保存')
|
||||
this.doHandle(this.copyTemplateMode ? '另存为' : '保存')
|
||||
},
|
||||
onSubmit() {
|
||||
this.$refs["form"].validate(async (valid, errMsg) => {
|
||||
this.$refs["form"].validate((valid, errMsg) => {
|
||||
if (valid) {
|
||||
if (!this.validateCopyTemplateName()) {
|
||||
return
|
||||
}
|
||||
const courseValid = this.formData.courseList.some((v, i) => {
|
||||
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && v.courseInstructor && v.courseType
|
||||
// 新活动通过多选关系设置负责人;未重新设置的旧活动仍允许保留历史联系人。
|
||||
const hasResponsible = Array.isArray(v.responsibleList)
|
||||
? v.responsibleList.length > 0 : !!v.courseInstructor
|
||||
const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && hasResponsible && v.courseType
|
||||
const timeValid =
|
||||
v.courseTimeList.length > 0 &&
|
||||
v.courseTimeList.every((x) => {
|
||||
@@ -578,8 +602,10 @@ const basicForm = {
|
||||
this.$message.warning("第" + (i + 1) + "行信息填写有误,请核查")
|
||||
return true
|
||||
}
|
||||
if (v.courseInstructorMobile && !/^1[3-9]\d{9}$/.test(v.courseInstructorMobile)) {
|
||||
this.$message.warning("第" + (i + 1) + "行手机号格式填写有误,请核查")
|
||||
// 联系方式允许手机号或办公电话,逐人校验,不能只检查兼容展示的第一位。
|
||||
if (Array.isArray(v.responsibleList) && v.responsibleList.some(item =>
|
||||
!item.userId || !item.mobile || !item.mobile.trim() || item.mobile.trim().length > 20)) {
|
||||
this.$message.warning("第" + (i + 1) + "行负责人联系方式未填写或超过20个字符,请核查")
|
||||
return true
|
||||
}
|
||||
if (v.isMobileSign === true && v.signType === 3 && (v.courseLocationCoordinates === undefined || v.courseLocationCoordinates.length < 2)) {
|
||||
@@ -598,7 +624,7 @@ const basicForm = {
|
||||
return
|
||||
}
|
||||
this.formData.isDisabled = false
|
||||
await this.doHandle(this.copyTemplateMode ? '新活动提交' : '提交')
|
||||
this.doHandle(this.copyTemplateMode ? '新活动提交' : '提交')
|
||||
} else {
|
||||
if(Object.keys(errMsg).length > 0) {
|
||||
this.$message.warning(errMsg[Object.keys(errMsg)[0]][0].message)
|
||||
@@ -620,7 +646,10 @@ const basicForm = {
|
||||
}
|
||||
return true
|
||||
},
|
||||
async doHandle(type) {
|
||||
doHandle(type) {
|
||||
if (this.formLoading) {
|
||||
return
|
||||
}
|
||||
const cloneData = clone(this.formData)
|
||||
if (this.copyTemplateMode) {
|
||||
this.cleanCopyTemplateData(cloneData)
|
||||
@@ -636,21 +665,32 @@ const basicForm = {
|
||||
cloneData.courseList = JSON.stringify(cloneData.courseList)
|
||||
cloneData.hostUnits = JSON.stringify(cloneData.hostUnits)
|
||||
cloneData.helpUnits = JSON.stringify(cloneData.helpUnits)
|
||||
const confirm = await this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||
// courseList 以JSON提交,每个负责人仅需 userId、mobile;姓名及关系归属由后端确认。
|
||||
// 返回 Result,code=0 表示保存成功;finally 在失败或网络异常时也解除提交加载状态。
|
||||
return this.$confirm("您确定要" + type + "吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
if (confirm !== "confirm") return
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/manage/doHandle", cloneData)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.step = 1
|
||||
this.$emit('refresh')
|
||||
this.$emit('back')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).then(() => {
|
||||
if (this.formLoading) {
|
||||
return
|
||||
}
|
||||
this.$set(this, 'formLoading', true)
|
||||
return this.$axios.post("/platform/trainSignUp/manage/doHandle", cloneData)
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.$set(this, 'step', 1)
|
||||
this.$emit('refresh')
|
||||
this.$emit('back')
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
}, () => {})
|
||||
},
|
||||
async getAllType() {
|
||||
const resp = await this.$axios.post("/platform/trainSignUp/type/getAllType", {})
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<!--#include('courseManageForm.js'){}#-->
|
||||
const courseManage = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div class="left-span-label">子活动管理</div>
|
||||
<div class="mb10">所属活动:{{ activity.activityName }}</div>
|
||||
<div class="text-info mb10">{{ canEditBasic ? '可管理当前活动的全部子活动' : '仅显示您负责的子活动' }}</div>
|
||||
<search @search="doSearch">
|
||||
<search-item label="子活动名称">
|
||||
<el-input :value="pageForm.courseName" clearable placeholder="请输入子活动名称"
|
||||
@input="(value) => { this.$set(pageForm, 'courseName', value) }"
|
||||
@keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
<el-table :data="tableData" :size="tableSize" v-loading="tableLoading"
|
||||
empty-text="暂无可管理的子活动,请检查负责人设置" class="mt20">
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column :label="trainType + '名称'" prop="courseName" min-width="160" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="类型" prop="courseTypeName" min-width="110" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="人数" prop="coursePeopleNumber" width="80"></el-table-column>
|
||||
<el-table-column label="预留名额" prop="courseReservedNumber" width="90"></el-table-column>
|
||||
<el-table-column label="校区" prop="campus" min-width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="地点" prop="courseLocation" min-width="160" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="负责人" min-width="120" show-overflow-tooltip>
|
||||
<template v-slot="{row}">{{ row.responsibleNames || row.courseInstructor || '未设置' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类标识" prop="assort" width="100" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="活动时间" prop="courseTimeName" min-width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template v-slot="{row}">
|
||||
<el-button size="mini" type="primary" @click="openManage(row)">管理</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include('/layouts/pagination.html'){}#-->
|
||||
</el-card>
|
||||
<course-manage-form ref="manageFormRef" @refresh="pageData"></course-manage-form>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
mixins: [initTableMixins],
|
||||
dicts: ['TRAIN_SIGNUP_TYPE'],
|
||||
components: {'course-manage-form': courseManageForm},
|
||||
data() {
|
||||
return {
|
||||
activity: {},
|
||||
pageDataUrl: '/platform/trainSignUp/manage/courseManagePageData',
|
||||
pageForm: {activityId: '', courseName: ''},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canEditBasic() {
|
||||
return this.$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN', 'SCHOOL_UNION_ACTIVITY_ADMIN'])
|
||||
},
|
||||
trainType() {
|
||||
const type = (this.dict.type.TRAIN_SIGNUP_TYPE || []).find(item => item.code === this.activity.trainType)
|
||||
return type ? type.name : '子活动'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// row 为主活动列表记录;仅传主活动ID和名称查询条件,负责人范围由服务端按登录人确定。
|
||||
initData(row) {
|
||||
this.$set(this, 'activity', row)
|
||||
this.$set(this.pageForm, 'activityId', row.id)
|
||||
this.$set(this.pageForm, 'courseName', '')
|
||||
this.doSearch()
|
||||
},
|
||||
// 详情另行加载并校验权限,不能将列表数据直接作为可写表单提交。
|
||||
openManage(row) {
|
||||
this.$refs.manageFormRef.initData(this.activity.id, row.id, this.trainType)
|
||||
},
|
||||
},
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
// courseTime、responsibleForm 已由首页先 include 的 basicForm.js 引入,沿用原组件,避免重复声明。
|
||||
const courseManageForm = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-drawer :title="'子活动管理 — ' + (formData.courseName || '')" :visible.sync="drawerVisible"
|
||||
size="55%" custom-class="train-course-manage-drawer" append-to-body
|
||||
:wrapper-closable="false" :before-close="beforeClose">
|
||||
<div class="train-course-manage-body" v-loading="detailLoading">
|
||||
<el-alert :title="canEditBasic ? '当前为管理员模式,可修改活动信息和签到设置' : '当前为负责人模式,可修改签到设置;活动关键信息只读'"
|
||||
type="info" :closable="false" show-icon></el-alert>
|
||||
<div class="left-span-label mt20">基本信息
|
||||
<el-tag v-if="!canEditBasic" size="mini" type="info">只读</el-tag>
|
||||
</div>
|
||||
<el-form v-if="canEditBasic" :model="formData" :disabled="formLoading || detailLoading"
|
||||
label-width="110px" ref="basicFormRef">
|
||||
<el-form-item :label="trainType + '名称'" prop="courseName" :rules="{required: true, message: '请输入名称', trigger: 'blur'}">
|
||||
<el-input :value="formData.courseName" maxlength="50" @input="(value) => { this.$set(formData, 'courseName', value) }"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="courseType" :rules="{required: true, message: '请选择类型', trigger: 'change'}">
|
||||
<el-select :value="formData.courseType" @change="(value) => { this.$set(formData, 'courseType', value) }" style="width: 100%">
|
||||
<el-option v-for="item in courseTypes" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12"><el-form-item label="人数">
|
||||
<el-input-number :value="formData.coursePeopleNumber" :min="0" :max="9999" :precision="0" :controls="false" style="width: 100%"
|
||||
@change="(value) => { this.$set(formData, 'coursePeopleNumber', value) }"></el-input-number>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="预留名额">
|
||||
<el-input-number :value="formData.courseReservedNumber" :min="0" :max="1000" :precision="0" :controls="false" style="width: 100%"
|
||||
@change="(value) => { this.$set(formData, 'courseReservedNumber', value) }"></el-input-number>
|
||||
</el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="校区" prop="campus" :rules="{required: true, message: '请选择校区', trigger: 'change'}">
|
||||
<el-select :value="formData.campus" @change="(value) => { this.$set(formData, 'campus', value) }" style="width: 100%">
|
||||
<el-option v-for="item in dict.type.USER_CAMPUS" :key="item.code" :label="item.name" :value="item.name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="地点" prop="courseLocation" :rules="{required: true, message: '请输入地点', trigger: 'blur'}">
|
||||
<el-input :value="formData.courseLocation" maxlength="50" @input="(value) => { this.$set(formData, 'courseLocation', value) }"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人"><el-button type="text" @click="openResponsible">{{ responsibleNames || '设置负责人' }}</el-button></el-form-item>
|
||||
<el-form-item label="分类标识"><el-input :value="formData.assort" maxlength="30" @input="(value) => { this.$set(formData, 'assort', value) }"></el-input></el-form-item>
|
||||
<el-form-item label="活动时间"><el-button type="text" @click="openTimes">设置活动时间</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-descriptions v-else :column="2" border class="table_fixed">
|
||||
<el-descriptions-item :label="trainType + '名称'">{{ formData.courseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ formData.courseTypeName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="人数">{{ formData.coursePeopleNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预留名额">{{ formData.courseReservedNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="校区">{{ formData.campus }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地点">{{ formData.courseLocation }}</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">{{ responsibleNames || formData.courseInstructor || '未设置' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分类标识">{{ formData.assort || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="活动时间" :span="2"><el-button type="text" @click="openTimes">查看时段({{ (formData.courseTimeList || []).length }})</el-button></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider></el-divider>
|
||||
<div class="left-span-label">签到设置</div>
|
||||
<el-form :model="formData" label-width="110px" :disabled="formLoading || detailLoading">
|
||||
<el-form-item label="是否签到">
|
||||
<el-radio-group :value="formData.isMobileSign" size="small" @input="(value) => { this.$set(formData, 'isMobileSign', value) }">
|
||||
<el-radio border :label="true">签到</el-radio><el-radio border :label="false">不签到</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="formData.isMobileSign">
|
||||
<el-form-item label="签到方式">
|
||||
<el-radio-group :value="formData.signType" size="small" @input="(value) => { this.$set(formData, 'signType', value) }">
|
||||
<el-radio border :label="1">扫描二维码</el-radio><el-radio border :label="2">被扫</el-radio><el-radio border :label="3">GPS定位签到</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="usesSignLocation" label="签到时间误差">
|
||||
<el-input :value="formData.signUnit" type="number" min="0" step="1" placeholder="请输入签到时间误差"
|
||||
@input="(value) => { this.$set(formData, 'signUnit', value) }"><template slot="append">分钟</template></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="usesSignLocation" label="签到半径">
|
||||
<el-input :value="formData.radius" type="number" min="1" step="1" placeholder="请输入签到半径"
|
||||
@input="(value) => { this.$set(formData, 'radius', value) }"><template slot="append">米</template></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="usesSignLocation" label="地点坐标">
|
||||
<el-button type="primary" size="small" @click="openMap">设置签到点</el-button>
|
||||
<span :class="hasPosition ? 'text-success' : 'text-danger'" style="margin-left: 12px">{{ hasPosition ? '已设置' : '未设置' }}</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="train-course-manage-footer">
|
||||
<el-button :disabled="formLoading" @click="closeDrawer">取消</el-button>
|
||||
<el-button type="primary" :loading="formLoading" :disabled="detailLoading || !detailReady" @click="onSave">保存</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<el-dialog title="位置信息" :visible.sync="mapVisible" :close-on-click-modal="false" append-to-body
|
||||
@opened="onMapOpened" @close="onMapClosed">
|
||||
<div style="min-height: 550px">
|
||||
<map-container v-if="mapVisible && mapOpened" :position="mapPosition" :radius="Number(formData.radius) || 100"
|
||||
@update:position="(value) => { this.$set(this, 'mapPosition', value) }"></map-container>
|
||||
</div>
|
||||
<span slot="footer"><el-button @click="closeMap">取消</el-button><el-button type="primary" @click="confirmMap">确定</el-button></span>
|
||||
</el-dialog>
|
||||
<el-dialog title="活动时段" :visible.sync="timesVisible" append-to-body width="60%">
|
||||
<el-table :data="formData.courseTimeList || []">
|
||||
<el-table-column label="日期" prop="courseDate"></el-table-column>
|
||||
<el-table-column label="开始时间" prop="courseStartTime"></el-table-column>
|
||||
<el-table-column label="结束时间" prop="courseEndTime"></el-table-column>
|
||||
</el-table>
|
||||
<span slot="footer"><el-button @click="closeTimes">关闭</el-button></span>
|
||||
</el-dialog>
|
||||
<course-time v-if="canEditBasic" ref="courseTimeRef"></course-time>
|
||||
<responsible-form v-if="canEditBasic" ref="responsibleRef"></responsible-form>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`,
|
||||
dicts: ['USER_CAMPUS'],
|
||||
components: {
|
||||
'course-time': courseTime,
|
||||
'responsible-form': responsibleForm,
|
||||
'map-container': httpVueLoader('/components/plugins/mapContainer/TMap.vue?v=1.0.8'),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawerVisible: false, detailLoading: false, detailReady: false, formLoading: false,
|
||||
canEditBasic: false, formData: {}, courseTypes: [], activityTime: [], trainType: '子活动',
|
||||
mapVisible: false, mapOpened: false, mapPosition: [], timesVisible: false, requestVersion: 0,
|
||||
originalTimes: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 扫码和GPS均校验时间及位置;被扫使用固定时间规则,关闭签到时也不使用这些配置。
|
||||
usesSignLocation() {
|
||||
return this.formData.isMobileSign === true && [1, 3].includes(this.formData.signType)
|
||||
},
|
||||
responsibleNames() {
|
||||
return (this.formData.responsibleList || []).map(item => item.username).filter(item => item).join('、')
|
||||
},
|
||||
hasPosition() {
|
||||
return Array.isArray(this.formData.courseLocationCoordinates) && this.formData.courseLocationCoordinates.length === 2
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// activityId/courseId 分别为主、子活动ID;data.course 为详情,canEditBasic 为服务端确认的字段权限。
|
||||
// 每次打开清空旧数据并使用请求序号,避免连续打开不同子活动后旧响应覆盖当前表单。
|
||||
initData(activityId, courseId, trainType) {
|
||||
const version = ++this.requestVersion
|
||||
this.$set(this, 'formData', {})
|
||||
this.$set(this, 'canEditBasic', false)
|
||||
this.$set(this, 'detailReady', false)
|
||||
this.$set(this, 'trainType', trainType)
|
||||
this.$set(this, 'drawerVisible', true)
|
||||
this.$set(this, 'detailLoading', true)
|
||||
return this.$axios.post('/platform/trainSignUp/manage/courseManageDetail', {activityId, courseId})
|
||||
.then((res) => {
|
||||
if (version !== this.requestVersion) return
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg)
|
||||
return
|
||||
}
|
||||
this.$set(this, 'formData', res.data.course)
|
||||
this.$set(this, 'canEditBasic', res.data.canEditBasic === true)
|
||||
this.$set(this, 'courseTypes', res.data.courseTypes || [])
|
||||
this.$set(this, 'activityTime', res.data.activityTime || [])
|
||||
this.$set(this, 'originalTimes', this.timeSnapshot())
|
||||
this.$set(this, 'detailReady', true)
|
||||
}).finally(() => {
|
||||
if (version === this.requestVersion) this.$set(this, 'detailLoading', false)
|
||||
})
|
||||
},
|
||||
// 仅实际更改时段或按时段限额时提交同步标志;普通签到保存不重建关联签到记录。
|
||||
timeSnapshot() {
|
||||
return JSON.stringify({times: this.formData.courseTimeList || [], limit: this.formData.courseIsLimitApply})
|
||||
},
|
||||
openResponsible() {
|
||||
if (this.canEditBasic && !this.formLoading) this.$refs.responsibleRef.onOpen(this.formData)
|
||||
},
|
||||
openTimes() {
|
||||
if (this.canEditBasic) {
|
||||
if (!this.formLoading) this.$refs.courseTimeRef.onOpen({activityTime: this.activityTime, courseList: [this.formData]}, 0)
|
||||
} else {
|
||||
this.$set(this, 'timesVisible', true)
|
||||
}
|
||||
},
|
||||
closeTimes() {
|
||||
this.$set(this, 'timesVisible', false)
|
||||
},
|
||||
// 地图使用副本,取消定位不改变原配置;确认后同时更新腾讯地图坐标及签到使用的WGS-84坐标。
|
||||
openMap() {
|
||||
this.$set(this, 'mapOpened', false)
|
||||
this.$set(this, 'mapPosition', clone(this.formData.courseLocationCoordinates || []))
|
||||
this.$set(this, 'mapVisible', true)
|
||||
},
|
||||
closeMap() {
|
||||
this.$set(this, 'mapVisible', false)
|
||||
},
|
||||
// 显示动画结束后才挂载地图,避免在隐藏容器中初始化;关闭时释放组件。
|
||||
onMapOpened() {
|
||||
this.$set(this, 'mapOpened', this.mapVisible)
|
||||
},
|
||||
onMapClosed() {
|
||||
this.$set(this, 'mapOpened', false)
|
||||
},
|
||||
confirmMap() {
|
||||
if (this.mapPosition.length !== 2) {
|
||||
this.$message.warning('请在地图上选择签到点')
|
||||
return
|
||||
}
|
||||
this.$set(this.formData, 'courseLocationCoordinates', clone(this.mapPosition))
|
||||
this.$set(this.formData, 'transPosition', this.$coordinateUtil.gcj02ToWgs84(this.mapPosition[1], this.mapPosition[0]))
|
||||
this.closeMap()
|
||||
},
|
||||
onSave() {
|
||||
if (this.formLoading || this.detailLoading || !this.detailReady) return
|
||||
if (this.canEditBasic) {
|
||||
this.$refs.basicFormRef.validate((valid) => { if (valid) this.saveCourse() })
|
||||
} else {
|
||||
this.saveCourse()
|
||||
}
|
||||
},
|
||||
// 普通负责人只提交签到白名单字段;管理员额外提交基本信息和实际修改后的负责人、时段。
|
||||
// 返回 Result,code=0 才关闭抽屉并刷新当前页;finally 在网络或业务失败时均释放加载状态。
|
||||
saveCourse() {
|
||||
if (this.formLoading) return
|
||||
if (this.formData.isMobileSign && ![1, 2, 3].includes(this.formData.signType)) {
|
||||
this.$message.warning('请选择签到方式')
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
id: this.formData.id, activityId: this.formData.activityId,
|
||||
isMobileSign: this.formData.isMobileSign,
|
||||
}
|
||||
if (this.formData.isMobileSign) {
|
||||
params.signType = this.formData.signType
|
||||
}
|
||||
// 隐藏字段保留在表单中供切换回来回显,但不校验、不提交,服务端同样保留数据库原值。
|
||||
if (this.usesSignLocation) {
|
||||
const signUnit = this.formData.signUnit === '' || this.formData.signUnit == null ? 0 : Number(this.formData.signUnit)
|
||||
const radius = this.formData.radius === '' || this.formData.radius == null ? 100 : Number(this.formData.radius)
|
||||
if (!Number.isInteger(signUnit) || signUnit < 0 || signUnit > 2147483647 || !Number.isInteger(radius) || radius <= 0 || radius > 2147483647) {
|
||||
this.$message.warning('签到时间误差应为非负整数,签到半径应为正整数')
|
||||
return
|
||||
}
|
||||
if (!this.hasPosition) {
|
||||
this.$message.warning('请设置签到点')
|
||||
return
|
||||
}
|
||||
params.signUnit = signUnit
|
||||
params.radius = radius
|
||||
params.courseLocationCoordinates = JSON.stringify(this.formData.courseLocationCoordinates)
|
||||
params.transPosition = JSON.stringify(this.$coordinateUtil.gcj02ToWgs84(
|
||||
this.formData.courseLocationCoordinates[1], this.formData.courseLocationCoordinates[0]))
|
||||
}
|
||||
if (this.canEditBasic) {
|
||||
const fields = ['courseName', 'courseType', 'coursePeopleNumber', 'courseReservedNumber', 'courseLocation', 'campus', 'assort']
|
||||
fields.forEach(field => { params[field] = this.formData[field] })
|
||||
if (Array.isArray(this.formData.responsibleList)) params.responsibleList = JSON.stringify(this.formData.responsibleList)
|
||||
params.updateCourseTimes = this.originalTimes !== this.timeSnapshot()
|
||||
if (params.updateCourseTimes) {
|
||||
params.courseTimeList = JSON.stringify(this.formData.courseTimeList)
|
||||
params.courseIsLimitApply = this.formData.courseIsLimitApply
|
||||
}
|
||||
}
|
||||
this.$set(this, 'formLoading', true)
|
||||
return this.$axios.post('/platform/trainSignUp/manage/saveManagedCourse', params).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.$set(this, 'drawerVisible', false)
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$message.warning(res.msg)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
},
|
||||
// 提交期间禁止退出;取消关闭仅丢弃本地副本,并使仍在途中的详情响应失效。
|
||||
beforeClose(done) {
|
||||
if (this.formLoading) return
|
||||
this.requestVersion += 1
|
||||
this.$set(this, 'detailLoading', false)
|
||||
done()
|
||||
},
|
||||
closeDrawer() {
|
||||
this.beforeClose(() => { this.$set(this, 'drawerVisible', false) })
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.requestVersion += 1
|
||||
},
|
||||
}
|
||||
@@ -177,10 +177,14 @@ const customForm = {
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="mapDialog" title="位置信息" :append-to-body="true">
|
||||
<map-container v-if="mapDialog"
|
||||
:radius="formData.courseList[moreInfoIndex].radius ? Number(formData.courseList[moreInfoIndex].radius) : 100"
|
||||
:position.sync="formData.courseList[mapIndex].courseLocationCoordinates"></map-container>
|
||||
<el-dialog :close-on-click-modal="false" :visible.sync="mapDialog" title="位置信息" :append-to-body="true"
|
||||
@opened="onMapOpened" @close="onMapClosed">
|
||||
<div style="min-height: 550px">
|
||||
<map-container v-if="mapDialog && mapOpened"
|
||||
:radius="formData.courseList[moreInfoIndex].radius ? Number(formData.courseList[moreInfoIndex].radius) : 100"
|
||||
:position="formData.courseList[mapIndex].courseLocationCoordinates"
|
||||
@update:position="(value) => { this.$set(formData.courseList[mapIndex], 'courseLocationCoordinates', value) }"></map-container>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="onMap">确 定</el-button>
|
||||
</span>
|
||||
@@ -199,18 +203,27 @@ const customForm = {
|
||||
moreInfoIndex: 0,
|
||||
|
||||
mapDialog: false,
|
||||
mapOpened: false,
|
||||
mapIndex: 0,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"union-form": unionForm,
|
||||
"map-container": httpVueLoader("/components/plugins/mapContainer/TMap.vue?v=1.0.6")
|
||||
"map-container": httpVueLoader("/components/plugins/mapContainer/TMap.vue?v=1.0.8")
|
||||
},
|
||||
methods: {
|
||||
openMap(index) {
|
||||
this.mapOpened = false
|
||||
this.mapIndex = index
|
||||
this.mapDialog = true
|
||||
},
|
||||
// 等弹窗动画结束、容器可见后挂载地图;关闭时卸载,重复打开创建新的实例。
|
||||
onMapOpened() {
|
||||
this.mapOpened = this.mapDialog
|
||||
},
|
||||
onMapClosed() {
|
||||
this.mapOpened = false
|
||||
},
|
||||
openSetUpUnionLimit(index) {
|
||||
this.$refs.unionFormRef.onOpen(this.formData, index)
|
||||
},
|
||||
|
||||
@@ -45,6 +45,33 @@ layout("/layouts/platform.html"){
|
||||
#app .train-sign-up-list-card .el-pagination-container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
/* 子活动抽屉沿用平台表单组件,仅内容区滚动,底部操作区始终可见。 */
|
||||
.train-course-manage-drawer {
|
||||
min-width: 720px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
.train-course-manage-drawer .el-drawer__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.train-course-manage-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.train-course-manage-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
text-align: right;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.train-course-manage-drawer { min-width: 100vw; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
@@ -102,6 +129,7 @@ layout("/layouts/platform.html"){
|
||||
>
|
||||
<template v-slot="{ row }" v-if="column.prop === 'isDisabled'">
|
||||
<el-switch
|
||||
:disabled="!canManageActivity"
|
||||
:active-value="false"
|
||||
:inactive-value="true"
|
||||
@change="(val) => { activityStatusChange(row.notice, val,row.id) }"
|
||||
@@ -130,14 +158,16 @@ layout("/layouts/platform.html"){
|
||||
<el-dropdown-item @click.native="openActivityCode(row.id)">活动二维码</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="makeCode(row)">签到二维码</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="onView(row)">查看</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="openEdit(row)">编辑</el-dropdown-item>
|
||||
<!-- 校工会管理角色显示编辑;其余人员须为当前活动负责人,两个入口互斥。 -->
|
||||
<el-dropdown-item v-if="!canManageActivity && row.canManageCourse === true" @click.native="openCourseManage(row)">子活动管理</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canManageActivity" @click.native="openEdit(row)">编辑</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="canManageTemplate && row.templateStatus !== 1"
|
||||
@click.native="setTemplate(row)">设为模板</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="canManageTemplate && row.templateStatus === 1"
|
||||
@click.native="cancelTemplate(row)">取消模板</el-dropdown-item>
|
||||
<el-dropdown-item :disabled="row.templateStatus === 1" @click.native="onDelete(row)">删除</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canManageActivity" :disabled="row.templateStatus === 1" @click.native="onDelete(row)">删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
@@ -156,6 +186,10 @@ layout("/layouts/platform.html"){
|
||||
<info ref="infoRef"></info>
|
||||
</template>
|
||||
|
||||
<template #public>
|
||||
<course-manage ref="courseManageRef"></course-manage>
|
||||
</template>
|
||||
|
||||
<make-qrcode ref="codeRef"></make-qrcode>
|
||||
|
||||
<el-dialog
|
||||
@@ -177,6 +211,7 @@ layout("/layouts/platform.html"){
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include('info.js'){}#-->
|
||||
<!--#include('basicForm.js'){}#-->
|
||||
<!--#include('courseManage.js'){}#-->
|
||||
<!--#include('makeQrcode.js'){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
@@ -187,6 +222,7 @@ layout("/layouts/platform.html"){
|
||||
"info": info,
|
||||
"basic-form": basicForm,
|
||||
"make-qrcode": makeQrcode,
|
||||
"course-manage": courseManage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -208,6 +244,10 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 与后端关键内容编辑角色保持一致,负责人通过子活动管理维护签到。
|
||||
canManageActivity() {
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "SCHOOL_UNION_ACTIVITY_ADMIN"])
|
||||
},
|
||||
// 页签沿用“设为模板”按钮的角色范围,不新增权限标识。
|
||||
canManageTemplate() {
|
||||
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
|
||||
@@ -218,7 +258,6 @@ layout("/layouts/platform.html"){
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
return this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
this.tableLoading = false
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
@@ -227,6 +266,8 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
}
|
||||
return res
|
||||
}).finally(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
// 切换页签时按活动表中的模板状态查询,并重置到第一页。
|
||||
@@ -281,10 +322,20 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
if (!this.canManageActivity) {
|
||||
this.$message.warning('活动关键信息仅管理员可修改,请使用子活动管理')
|
||||
return
|
||||
}
|
||||
this.$refs.guava.edit(() => {
|
||||
this.$refs.formRef.initData(row)
|
||||
})
|
||||
},
|
||||
// 使用 guava 的现有公共页面插槽,返回后保留主列表的筛选条件和页码。
|
||||
openCourseManage(row) {
|
||||
this.$refs.guava.public(() => {
|
||||
this.$refs.courseManageRef.initData(row)
|
||||
})
|
||||
},
|
||||
openUrlEdit() {
|
||||
if (this.urlEditInitialized) {
|
||||
return
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
const responsibleForm = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<el-dialog title="设置负责人" :visible.sync="dialogVisible" width="60%"
|
||||
:close-on-click-modal="false" append-to-body>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="负责人">
|
||||
<el-select :value="selectedIds" multiple filterable remote reserve-keyword
|
||||
:remote-method="queryUsers" :loading="tableLoading" @change="responsibleChange"
|
||||
placeholder="请输入姓名或工号,多选负责人" style="width: 100%">
|
||||
<el-option v-for="item in userOptions" :key="item.id" :value="item.id"
|
||||
:label="item.username + (item.loginname ? '(' + item.loginname + ')' : '')"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="responsibleList">
|
||||
<el-table-column label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column label="负责人" prop="username" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="工号" prop="loginname" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="联系方式">
|
||||
<template v-slot="{row}">
|
||||
<el-input :value="row.mobile" maxlength="20" placeholder="请输入手机号或办公电话"
|
||||
@input="(value) => { this.$set(row, 'mobile', value) }"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt10 text-warning">确定后回填当前表单,保存或提交活动后生效。</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirm">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
tableLoading: false,
|
||||
course: null,
|
||||
selectedIds: [],
|
||||
responsibleList: [],
|
||||
userOptions: [],
|
||||
queryVersion: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 打开当前子活动的负责人副本,关闭或取消弹框不会更改业务表单。
|
||||
onOpen(course) {
|
||||
this.queryVersion += 1
|
||||
this.$set(this, 'course', course)
|
||||
const list = clone(course.responsibleList || [])
|
||||
this.$set(this, 'responsibleList', list)
|
||||
this.$set(this, 'selectedIds', list.map(item => item.userId))
|
||||
this.$set(this, 'userOptions', list.map(item => ({
|
||||
id: item.userId, username: item.username, loginname: item.loginname, mobile: item.mobile
|
||||
})))
|
||||
this.$set(this, 'tableLoading', false)
|
||||
this.$set(this, 'dialogVisible', true)
|
||||
},
|
||||
// query 为姓名或工号;返回 Result.data 用户数组,使用 id、username、loginname、mobile。
|
||||
// 合并已选项以保持多人回显,并忽略过期请求,避免快速搜索覆盖最新结果。
|
||||
queryUsers(query) {
|
||||
const version = ++this.queryVersion
|
||||
if (!query || !query.trim()) {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
return
|
||||
}
|
||||
this.$set(this, 'tableLoading', true)
|
||||
return this.$axios.post('/open/common/userOptions', {query: query.trim()})
|
||||
.then((res) => {
|
||||
if (version !== this.queryVersion || !this.dialogVisible) {
|
||||
return
|
||||
}
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg)
|
||||
return
|
||||
}
|
||||
const selected = this.responsibleList.map(item => ({
|
||||
id: item.userId, username: item.username, loginname: item.loginname, mobile: item.mobile
|
||||
}))
|
||||
const options = new Map(selected.map(item => [item.id, item]))
|
||||
const results = res.data || []
|
||||
results.forEach(item => options.set(item.id, item))
|
||||
this.$set(this, 'userOptions', Array.from(options.values()))
|
||||
})
|
||||
.finally(() => {
|
||||
if (version === this.queryVersion) {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 多选结果一人一行,已选用户保留手工修改的联系方式,新选用户带出系统联系电话。
|
||||
responsibleChange(ids) {
|
||||
const oldRows = new Map(this.responsibleList.map(item => [item.userId, item]))
|
||||
const options = new Map(this.userOptions.map(item => [item.id, item]))
|
||||
const rows = ids.map((id) => {
|
||||
if (oldRows.has(id)) {
|
||||
return oldRows.get(id)
|
||||
}
|
||||
const user = options.get(id)
|
||||
return {userId: id, username: user.username, loginname: user.loginname, mobile: user.mobile || ''}
|
||||
})
|
||||
this.$set(this, 'selectedIds', ids)
|
||||
this.$set(this, 'responsibleList', rows)
|
||||
},
|
||||
// 联系方式按每个人校验;使用新数组回填,弹框副本不再与业务表单共用对象。
|
||||
onConfirm() {
|
||||
const missing = this.responsibleList.find(item => !item.mobile || !item.mobile.trim())
|
||||
if (missing) {
|
||||
this.$message.warning('请填写' + missing.username + '的联系方式')
|
||||
return
|
||||
}
|
||||
if (this.responsibleList.some(item => item.mobile.trim().length > 20)) {
|
||||
this.$message.warning('负责人联系方式不能超过20个字符')
|
||||
return
|
||||
}
|
||||
const list = this.responsibleList.map((item, index) => ({
|
||||
userId: item.userId, username: item.username, loginname: item.loginname,
|
||||
mobile: item.mobile.trim(), orderNum: index + 1
|
||||
}))
|
||||
this.$set(this.course, 'responsibleList', list)
|
||||
// 旧页面保留第一位联系人展示;完整多人数据通过 responsibleList 保存。
|
||||
this.$set(this.course, 'courseInstructor', list.length ? list[0].username : '')
|
||||
this.$set(this.course, 'courseInstructorMobile', list.length ? list[0].mobile : '')
|
||||
this.closeDialog()
|
||||
},
|
||||
closeDialog() {
|
||||
this.queryVersion += 1
|
||||
this.$set(this, 'dialogVisible', false)
|
||||
this.$set(this, 'tableLoading', false)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="员工信息">
|
||||
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入工号或姓名" v-model="pageForm.searchKeyword"></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="华住员工管理">
|
||||
<el-button :loading="formLoading" :disabled="tableLoading" @click="syncSelected" 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 || formLoading">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column label="工号" min-width="140" prop="loginname"></el-table-column>
|
||||
<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 :disabled="tableLoading || formLoading" @click="queryStatus(row)" size="mini" type="primary">查询状态</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-dialog :visible="statusDialogVisible" @update:visible="value => this.$set(this, 'statusDialogVisible', value)" 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>
|
||||
<el-descriptions-item label="姓名">{{statusData.userName || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{statusData.userDep || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="成本中心">{{statusData.costCenter || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="职级">{{statusData.rank || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="直属领导">{{statusData.leader || "--"}}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{statusData.email || "--"}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
// 分页、查询和 loading 复用 initTableMixins,只声明本业务的选择与状态弹框数据。
|
||||
return {selectedRows: [], statusDialogVisible: false, statusData: null}
|
||||
},
|
||||
methods: {
|
||||
selectionChange(rows) {
|
||||
this.$set(this, "selectedRows", rows)
|
||||
},
|
||||
// userIds 为本系统主键;逐人 success=true/false/null 对应成功、失败、待核实。
|
||||
syncSelected() {
|
||||
if (this.formLoading || this.tableLoading) return
|
||||
if (!this.selectedRows.length) {
|
||||
this.$message.warning("请先选择需要同步的员工")
|
||||
return
|
||||
}
|
||||
const userIds = this.selectedRows.map((row) => row.id)
|
||||
this.$confirm("确定同步选中的 " + userIds.length + " 名员工到华住吗?", "提示", {type: "warning"}).then(() => {
|
||||
if (this.formLoading) return
|
||||
this.$set(this, "formLoading", true)
|
||||
return this.$axios.post("/platform/huazhu/syncUsers", {userIds: userIds}).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg || "同步失败")
|
||||
return
|
||||
}
|
||||
const results = res.data || []
|
||||
const successCount = results.filter((item) => item.success === true).length
|
||||
const failedCount = results.filter((item) => item.success === false).length
|
||||
const pendingCount = results.length - successCount - failedCount
|
||||
const summary = "成功 " + successCount + " 人,失败 " + failedCount + " 人,待核实 " + pendingCount + " 人"
|
||||
if (failedCount || pendingCount) {
|
||||
this.$message.warning(summary)
|
||||
const details = results.filter((item) => item.success !== true).map((item) => {
|
||||
return (item.userName || "员工") + "(" + (item.loginName || item.userId) + "):" + item.message
|
||||
}).join("\n")
|
||||
this.$alert(details, "同步结果明细").catch(() => {})
|
||||
} else {
|
||||
this.$message.success(summary)
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 网络中断无法确认华住是否已处理,提示先查询状态,避免盲目重复同步。
|
||||
this.$message.warning("请求未完成,请先查询华住状态再决定是否重试")
|
||||
})
|
||||
}).catch(() => {
|
||||
// 取消确认不发送同步请求。
|
||||
})
|
||||
},
|
||||
// userId 是本系统主键;成功 data 为该员工的华住信息,statusCode 按原值展示。
|
||||
queryStatus(row) {
|
||||
if (this.tableLoading || this.formLoading) return
|
||||
this.$set(this, "tableLoading", true)
|
||||
return this.$axios.post("/platform/huazhu/queryStatus", {userId: row.id}).then((res) => {
|
||||
if (res.code === 0 && res.data) {
|
||||
this.$set(this, "statusData", res.data)
|
||||
this.$set(this, "statusDialogVisible", true)
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "tableLoading", false)
|
||||
}).catch(() => {
|
||||
// 公共 Axios 拦截器提示请求异常,保留页面供用户重试。
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData().catch(() => {})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,187 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker :clearable="true" placeholder="请选择年度" style="width: 100%" type="year" v-model="pageForm.year" value-format="yyyy"></el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="标题">
|
||||
<el-input @keyup.enter.native="doSearch" clearable placeholder="请输入标题" v-model="pageForm.title"></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this">
|
||||
<el-button @click="openAdd" icon="el-icon-plus" size="small" type="primary">新增</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" :size="tableSize" class="vi-table" v-loading="tableLoading">
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column label="标题" min-width="180" prop="title" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="标签类型" prop="tagTypeName" width="130">
|
||||
<template slot-scope="{row}"><el-tag v-if="row.tagTypeName" size="small">{{row.tagTypeName}}</el-tag><span v-else>--</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="摘要" min-width="180" prop="summary" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="封面图" width="110">
|
||||
<template slot-scope="{row}"><el-image v-if="row.cover" :preview-src-list="[row.cover]" :src="row.cover" fit="cover" style="height: 36px; width: 72px"></el-image><span v-else>--</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template slot-scope="{row}">{{formatDate(row.createdAt)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布状态" width="100">
|
||||
<template slot-scope="{row}"><span :class="row.status === 1 ? 'text-success' : 'text-muted'">{{row.statusName}}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="220">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button @click="changeStatus(row, row.status === 1 ? 0 : 1)" size="mini">{{row.status === 1 ? '下架' : '发布'}}</el-button>
|
||||
<el-button @click="deleteRow(row)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</template>
|
||||
<template #edit>
|
||||
<el-form :model="formData" :rules="formRules" label-width="90px" ref="form" v-loading="formLoading">
|
||||
<el-form-item label="标题" prop="title"><el-input maxlength="100" placeholder="请输入标题" v-model="formData.title"></el-input></el-form-item>
|
||||
<el-form-item label="摘要" prop="summary"><el-input maxlength="500" placeholder="请输入摘要" rows="3" type="textarea" v-model="formData.summary"></el-input></el-form-item>
|
||||
<el-form-item label="标签类型" prop="tagType">
|
||||
<el-select placeholder="请先维护标签类型字典" style="width: 100%" v-model="formData.tagType"><el-option :key="item.code" :label="item.label" :value="item.code" v-for="item in dict.type.INCLUSIVE_BENEFIT_TYPE || []"></el-option></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content"><text-editor v-model="formData.content"></text-editor></el-form-item>
|
||||
<el-form-item label="性别限制" prop="sexLimit"><el-radio-group v-model="formData.sexLimit"><el-radio :label="0">全部开放</el-radio><el-radio :label="1">男性</el-radio><el-radio :label="2">女性</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="封面图" prop="cover">
|
||||
<file-upload :upload_number="1" :value.sync="formData.cover" upload_mode="image" upload_result_category="interval" upload_result_type="url"></file-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="链接" prop="linkUrl"><el-input maxlength="500" placeholder="请输入外链地址" v-model="formData.linkUrl"></el-input></el-form-item>
|
||||
<el-form-item><el-button @click="back">取消</el-button><el-button :loading="formLoading" @click="saveForm(0)" type="primary">保存草稿</el-button><el-button :loading="formLoading" @click="saveForm(1)" type="primary">发布</el-button></el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
dicts: ["INCLUSIVE_BENEFIT_TYPE"],
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
// 公共分页参数、列表请求及加载状态由 initTableMixins 提供,仅补充业务筛选项。
|
||||
pageForm: {year: "", title: ""},
|
||||
formRules: {
|
||||
title: [{required: true, message: "请输入标题", trigger: "blur"}],
|
||||
summary: [{required: true, message: "请输入摘要", trigger: "blur"}],
|
||||
tagType: [{required: true, message: "请选择标签类型", trigger: "change"}],
|
||||
content: [{required: true, message: "请输入内容", trigger: "change"}],
|
||||
cover: [{required: true, message: "请上传封面图", trigger: "change"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 新内容归入查询年度;未选年度时使用当前年度,性别配置沿用原模块默认值。
|
||||
emptyForm() {
|
||||
return {id: "", year: this.pageForm.year || new Date().getFullYear() + "", title: "",
|
||||
summary: "", tagType: "", content: "", sexLimit: 0, cover: "", linkUrl: ""}
|
||||
},
|
||||
formatDate(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD HH:mm") : "--"
|
||||
},
|
||||
back() {
|
||||
this.$refs.guava.index()
|
||||
},
|
||||
openAdd() {
|
||||
this.$set(this, "formData", this.emptyForm())
|
||||
this.$refs.guava.edit(() => this.$refs.form.clearValidate())
|
||||
},
|
||||
// id 为所选内容主键;成功响应 data 为详情对象,回显时将年度转换为表单字符串。
|
||||
openEdit(row) {
|
||||
if (this.formLoading) return
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/inclusive/findOne", {id: row.id}).then((res) => {
|
||||
if (res.code === 0 && res.data) {
|
||||
const formData = Object.assign(this.emptyForm(), res.data)
|
||||
this.$set(formData, "year", formData.year == null ? "" : formData.year + "")
|
||||
this.$set(this, "formData", formData)
|
||||
this.$refs.guava.edit(() => this.$refs.form.clearValidate())
|
||||
} else {
|
||||
this.$message.warning(res.msg || "未查询到数据")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 公共 Axios 拦截器已展示失败原因;保留当前页面供用户重试。
|
||||
})
|
||||
},
|
||||
// status=0 保存草稿,status=1 发布;code=0 表示成功,返回列表后复用 mixin 刷新查询。
|
||||
saveForm(status) {
|
||||
if (this.formLoading) return
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.$set(this, "formLoading", true)
|
||||
const params = Object.assign({}, this.formData, {status: status})
|
||||
this.$axios.post("/platform/inclusive/save", params).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.$refs.guava.index()
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 保存失败保留表单,错误提示由公共 Axios 拦截器负责。
|
||||
})
|
||||
})
|
||||
},
|
||||
// 发布/下架只提交 id 和目标 status;确认取消不会发送写请求。
|
||||
changeStatus(row, status) {
|
||||
if (this.formLoading) return
|
||||
this.$confirm("确定要" + (status === 1 ? "发布" : "下架") + "该幸福工荟吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/inclusive/updateStatus", {id: row.id, status: status}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("操作成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "操作失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 发布或下架失败时保留现有列表状态。
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 删除仅提交内容主键,后端执行逻辑删除;成功后刷新分页总数。
|
||||
deleteRow(row) {
|
||||
if (this.formLoading) return
|
||||
this.$confirm("确定要删除该幸福工荟吗?", "提示", {type: "warning"}).then(() => {
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/inclusive/delete", {id: row.id}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 删除失败时保留当前记录,公共拦截器已提示原因。
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+35
-22
@@ -144,6 +144,7 @@ layout("/layouts/platform.html"){
|
||||
:width="column.width"
|
||||
:fixed="column.fixed"
|
||||
:sortable="column.sortable"
|
||||
:formatter="formatSubsidyMoney"
|
||||
show-overflow-tooltip
|
||||
sortable
|
||||
v-for="column in tableColumns"
|
||||
@@ -217,7 +218,7 @@ layout("/layouts/platform.html"){
|
||||
{prop: 'bxyMoney$', label: '非住院肾衰竭治疗、靶向药金额', width: "120"},
|
||||
{prop: 'totalMoney', label: '合计金额', width: "120"},
|
||||
{prop: 'loveSubsidyMoney', label: '爱心补助', width: "120"},
|
||||
{prop: 'forecastSubsidyMoney', label: '当前补助金额', width: "120"},
|
||||
{prop: 'subsidyMoney', label: '当前补助金额', width: "120"},
|
||||
],
|
||||
}
|
||||
},
|
||||
@@ -322,7 +323,7 @@ layout("/layouts/platform.html"){
|
||||
scrollBody.scrollTop = Math.max(0, scrollBody.scrollTop + delta)
|
||||
}
|
||||
},
|
||||
async grantOrIntoChange(key, val) {
|
||||
grantOrIntoChange(key, val) {
|
||||
if (val === '') {
|
||||
if (key === 'grants') {
|
||||
this.calcData.schoolGrants = 0
|
||||
@@ -330,7 +331,11 @@ layout("/layouts/platform.html"){
|
||||
this.calcData.unionInto = 0
|
||||
}
|
||||
}
|
||||
const resp = await this.$axios.post(loc() + '/setGrantsOrUnionInto', {keys: key, value: val})
|
||||
return this.$axios.post(loc() + '/setGrantsOrUnionInto', {keys: key, value: val})
|
||||
},
|
||||
// 已保存补助去掉末尾的零;历史非零小数保持真实值,需完成分配后才变为整数。
|
||||
formatSubsidyMoney(row, column, value) {
|
||||
return column.property === 'subsidyMoney' && value != null && value !== '' ? Number(value) : value
|
||||
},
|
||||
getSummaries(param) {
|
||||
const {columns, data} = param
|
||||
@@ -343,9 +348,13 @@ layout("/layouts/platform.html"){
|
||||
const values = data.map(i => Number(i[column.property]))
|
||||
if (values && values.length > 0) {
|
||||
const everyColumnSum = values.reduce((a, b) => a + b)
|
||||
// 补助合计累加已按笔取整的金额,其他费用仍保留两位小数。
|
||||
const digits = ['subsidyMoney', 'forecastSubsidyMoney'].includes(column.property) ? 0 : 2
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(digits)
|
||||
// 测算合计显示整数;已保存合计仅去掉末尾零,保留历史金额中的真实角分。
|
||||
if (column.property === 'subsidyMoney') {
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : Number(everyColumnSum.toFixed(2))
|
||||
} else {
|
||||
const digits = column.property === 'forecastSubsidyMoney' ? 0 : 2
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(digits)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -358,13 +367,18 @@ layout("/layouts/platform.html"){
|
||||
ratio: this.ratio})
|
||||
},
|
||||
doHandle() {
|
||||
this.$confirm('确定要按此比例来补助吗?', '提示', {
|
||||
return this.$confirm('确定要按此比例来补助吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/doHandle", {
|
||||
data: JSON.stringify(this.applyListData),
|
||||
// 仅提交本次分配所需的申请标识和金额,保存及刷新结束后统一解除按钮加载状态。
|
||||
this.formLoading = true
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/doHandle", {
|
||||
data: JSON.stringify(this.applyListData.map(row => ({
|
||||
id: row.id,
|
||||
forecastSubsidyMoney: row.forecastSubsidyMoney
|
||||
}))),
|
||||
ratio: this.ratio,
|
||||
aidFund: this.calcData.aidFundMoney,
|
||||
inComeSum: this.inComeSum,
|
||||
@@ -372,28 +386,28 @@ layout("/layouts/platform.html"){
|
||||
}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("操作成功")
|
||||
this.getData()
|
||||
this.getApplyData()
|
||||
return this.getData()
|
||||
}
|
||||
}).finally(() => {
|
||||
this.formLoading = false
|
||||
})
|
||||
|
||||
})
|
||||
},
|
||||
async getMedicalSetting() {
|
||||
try {
|
||||
const res = await this.$axios.post("/platform/medicalMutualAid/medical/basicSetting/getSetting")
|
||||
getMedicalSetting() {
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/basicSetting/getSetting").then(res => {
|
||||
if (res.code === 0) {
|
||||
this.medicalSetting = res.data
|
||||
}
|
||||
} catch (e) {
|
||||
}).catch(e => {
|
||||
this.$message.error(e.message)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
getData() {
|
||||
this.formLoading = true
|
||||
this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getData").then(res => {
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getData").then(res => {
|
||||
if (res.code === 0) {
|
||||
if (res.data.ratio) {
|
||||
this.ratio = res.data.ratio
|
||||
@@ -401,14 +415,14 @@ layout("/layouts/platform.html"){
|
||||
this.ratio = this.medicalSetting.subsidyRatio
|
||||
}
|
||||
this.calcData = res.data
|
||||
this.getApplyData()
|
||||
return this.getApplyData()
|
||||
}
|
||||
}).finally(()=>{
|
||||
this.formLoading=false
|
||||
})
|
||||
},
|
||||
getApplyData() {
|
||||
this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getApplyData", {ratio: this.ratio}).then(res => {
|
||||
return this.$axios.post("/platform/medicalMutualAid/medical/calculateMoney/getApplyData", {ratio: this.ratio}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.applyListData = res.data
|
||||
}
|
||||
@@ -416,9 +430,8 @@ layout("/layouts/platform.html"){
|
||||
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.getMedicalSetting()
|
||||
this.getData()
|
||||
created() {
|
||||
this.getMedicalSetting().then(() => this.getData())
|
||||
},
|
||||
mounted() {
|
||||
this.initTableScroll()
|
||||
|
||||
+8
-1
@@ -131,6 +131,7 @@ layout("/layouts/platform.html"){
|
||||
:width="column.width"
|
||||
:fixed="column.fixed"
|
||||
:sortable="column.sortable"
|
||||
:formatter="formatSubsidyMoney"
|
||||
show-overflow-tooltip
|
||||
sortable
|
||||
v-for="column in tableColumns"
|
||||
@@ -394,6 +395,10 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
})
|
||||
},
|
||||
// 整数补助不显示 .00;尚未重新分配的历史小数保留原值,避免显示金额与数据库不符。
|
||||
formatSubsidyMoney(row, column, value) {
|
||||
return column.property === 'subsidyMoney' && value != null && value !== '' ? Number(value) : value
|
||||
},
|
||||
getSummaries(param) {
|
||||
const {columns, data} = param
|
||||
const sums = []
|
||||
@@ -403,7 +408,9 @@ layout("/layouts/platform.html"){
|
||||
const values = data.map(i => Number(i[column.property]))
|
||||
if (values && values.length > 0) {
|
||||
const everyColumnSum = values.reduce((a, b) => a + b)
|
||||
sums[index] = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(2)
|
||||
// 仅补助合计去掉末尾零;爱心补助和医疗费用仍按两位小数显示。
|
||||
const total = isNaN(everyColumnSum) ? 0 : everyColumnSum.toFixed(2)
|
||||
sums[index] = column.property === 'subsidyMoney' ? Number(total) : total
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,6 +30,25 @@ layout("/layouts/platform_h5.html"){
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 年度组件已由 platform_h5.html 注册,两个荣誉分类共用所选年度。 -->
|
||||
<div class="honor-year-filter">
|
||||
<div class="honor-year-filter__label">
|
||||
<van-icon name="calendar-o"></van-icon>
|
||||
<span>荣誉年度</span>
|
||||
</div>
|
||||
<van-dropdown-menu class="honor-year-filter__menu" active-color="#bd0018">
|
||||
<year-van-dropdown-item
|
||||
ref="yearFilter"
|
||||
:num="10"
|
||||
:value="currentYear"
|
||||
@input="setCurrentYear"
|
||||
@change="listHonor"
|
||||
@open="onYearFilterOpen"
|
||||
@close="onYearFilterClose">
|
||||
</year-van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
|
||||
<section class="honor-intro">
|
||||
<div class="honor-intro__corner honor-intro__corner--left"></div>
|
||||
<div class="honor-intro__corner honor-intro__corner--right"></div>
|
||||
@@ -38,7 +57,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<p>致敬劳模先进,礼赞奋斗精神。</p>
|
||||
</section>
|
||||
|
||||
<van-loading v-if="loading" class="honor-loading" color="#fff">加载中...</van-loading>
|
||||
<van-loading v-if="tableLoading" class="honor-loading" color="#fff">加载中...</van-loading>
|
||||
|
||||
<div v-else class="honor-list">
|
||||
<article v-for="item in honorList" :key="item.id" class="honor-card">
|
||||
@@ -57,7 +76,10 @@ layout("/layouts/platform_h5.html"){
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<van-empty v-if="honorList.length === 0" image="search" description="暂无当年荣誉"></van-empty>
|
||||
<van-empty v-if="honorList.length === 0" class="honor-empty" image="search"
|
||||
:description="'暂无' + currentYear + '年度荣誉'">
|
||||
<div class="honor-empty__hint">可切换年度查看往年荣誉</div>
|
||||
</van-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +87,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
activeTab: "labor",
|
||||
currentYear: new Date().getFullYear(),
|
||||
@@ -75,14 +97,32 @@ layout("/layouts/platform_h5.html"){
|
||||
studio: null
|
||||
},
|
||||
honorList: [],
|
||||
loading: false
|
||||
tableLoading: false,
|
||||
honorRequestId: 0,
|
||||
popupHistoryToken: "",
|
||||
yearFilterOpened: false,
|
||||
yearHistoryClosing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack: function () {
|
||||
// 年度下拉展开时,导航返回只消费弹层历史,不离开当前荣誉页面。
|
||||
goBack() {
|
||||
if (this.yearHistoryClosing) {
|
||||
return
|
||||
}
|
||||
if (window.h5PopupHistory.close(this.popupHistoryToken, "honor-year")) {
|
||||
this.$set(this, "yearHistoryClosing", true)
|
||||
return
|
||||
}
|
||||
this.returnHome()
|
||||
},
|
||||
returnHome: function () {
|
||||
returnHome() {
|
||||
// 页面跳转前先清理年度弹层历史,避免返回后恢复已关闭的下拉框。
|
||||
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
|
||||
this.navigateHome()
|
||||
})
|
||||
},
|
||||
navigateHome() {
|
||||
if (window.store && typeof window.store.commit === "function") {
|
||||
window.store.commit("setActiveTarBar", "home")
|
||||
}
|
||||
@@ -90,13 +130,13 @@ layout("/layouts/platform_h5.html"){
|
||||
window.location.replace("/platform/h5/home")
|
||||
return
|
||||
}
|
||||
var fallbackTimer = window.setTimeout(function () {
|
||||
var fallbackTimer = window.setTimeout(() => {
|
||||
window.location.replace("/platform/h5/home")
|
||||
}, 1200)
|
||||
$(document).one("pjax:complete", function () {
|
||||
$(document).one("pjax:complete", () => {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
})
|
||||
$(document).one("pjax:error pjax:timeout", function () {
|
||||
$(document).one("pjax:error pjax:timeout", () => {
|
||||
window.clearTimeout(fallbackTimer)
|
||||
window.location.replace("/platform/h5/home")
|
||||
})
|
||||
@@ -109,11 +149,43 @@ layout("/layouts/platform_h5.html"){
|
||||
timeout: 8000
|
||||
})
|
||||
},
|
||||
switchTab: function (tab) {
|
||||
// 两个分类共用一个年度值,切换分类时保留当前选择。
|
||||
setCurrentYear(year) {
|
||||
// 公共年度组件返回 Number;先更新年度,再由 change 事件查询对应记录。
|
||||
this.$set(this, "currentYear", year)
|
||||
},
|
||||
// 浏览器历史回退完成前不重复打开,避免快速点击残留额外历史记录。
|
||||
onYearFilterOpen() {
|
||||
if (this.yearHistoryClosing) {
|
||||
this.closeYearFilter()
|
||||
return
|
||||
}
|
||||
this.$set(this, "yearFilterOpened", true)
|
||||
window.h5PopupHistory.open(this.popupHistoryToken, "honor-year")
|
||||
},
|
||||
// 选择年份、点击遮罩或再次点击标题关闭时,同步回退对应弹层历史。
|
||||
onYearFilterClose() {
|
||||
if (!this.yearFilterOpened) {
|
||||
return
|
||||
}
|
||||
this.$set(this, "yearFilterOpened", false)
|
||||
if (!this.yearHistoryClosing) {
|
||||
this.$set(this, "yearHistoryClosing",
|
||||
window.h5PopupHistory.close(this.popupHistoryToken, "honor-year"))
|
||||
}
|
||||
},
|
||||
closeYearFilter() {
|
||||
// 公共年度组件的根组件为 van-dropdown-item,复用其 toggle 关闭能力。
|
||||
const yearFilter = this.$refs.yearFilter
|
||||
if (yearFilter && yearFilter.$children[0]) {
|
||||
yearFilter.$children[0].toggle(false)
|
||||
}
|
||||
},
|
||||
switchTab(tab) {
|
||||
if (this.activeTab === tab) {
|
||||
return
|
||||
}
|
||||
this.activeTab = tab
|
||||
this.$set(this, "activeTab", tab)
|
||||
this.listHonor()
|
||||
},
|
||||
getDisplayName: function (item) {
|
||||
@@ -143,33 +215,46 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
return target || null
|
||||
},
|
||||
listHonor: function () {
|
||||
var self = this
|
||||
var type = this.activeTab === "studio" ? this.honorTypeMap.studio : this.honorTypeMap.labor
|
||||
/**
|
||||
* 按当前年度和分类请求荣誉:year 为年度数字,honorType 为分类 ID,
|
||||
* pageNumber/pageSize 固定为第一页 50 条;返回 res.data.list 为荣誉对象数组。
|
||||
* 请求序号用于避免快速切换年度、分类时旧列表或旧图片覆盖最新查询结果。
|
||||
*/
|
||||
listHonor() {
|
||||
const requestId = this.honorRequestId + 1
|
||||
this.$set(this, "honorRequestId", requestId)
|
||||
const type = this.activeTab === "studio" ? this.honorTypeMap.studio : this.honorTypeMap.labor
|
||||
this.$set(this, "honorList", [])
|
||||
if (!type) {
|
||||
this.honorList = []
|
||||
this.$set(this, "tableLoading", false)
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.tableLoading = true
|
||||
this.$axios.post("/platform/honor/manage/h5Data", {
|
||||
year: this.currentYear,
|
||||
honorType: type.id,
|
||||
pageNumber: 1,
|
||||
pageSize: 50
|
||||
}).then(function (res) {
|
||||
}).then((res) => {
|
||||
if (requestId !== this.honorRequestId || this._isDestroyed) {
|
||||
return
|
||||
}
|
||||
if (res.code === 0) {
|
||||
var list = (res.data && res.data.list) || []
|
||||
self.honorList = list.map(function (item) {
|
||||
item.cover = ""
|
||||
item.photoId = self.getFirstPhotoId(item.photoFiles)
|
||||
return item
|
||||
})
|
||||
self.loadPhotoMap()
|
||||
} else {
|
||||
self.honorList = []
|
||||
const list = (res.data && res.data.list) || []
|
||||
this.$set(this, "honorList", list.map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
cover: "",
|
||||
photoId: this.getFirstPhotoId(item.photoFiles)
|
||||
})
|
||||
}))
|
||||
this.loadPhotoMap(requestId)
|
||||
}
|
||||
}).catch(() => {
|
||||
// 错误由全局 axios 拦截器提示;保留当前年度空列表,不显示上一年度记录。
|
||||
}).finally(() => {
|
||||
if (requestId === this.honorRequestId && !this._isDestroyed) {
|
||||
this.tableLoading = false
|
||||
}
|
||||
}).finally(function () {
|
||||
self.loading = false
|
||||
})
|
||||
},
|
||||
getFirstPhotoId: function (photoFiles) {
|
||||
@@ -194,37 +279,57 @@ layout("/layouts/platform_h5.html"){
|
||||
data = String(data)
|
||||
return data.indexOf("=") > -1 ? data.substring(data.lastIndexOf("=") + 1) : data
|
||||
},
|
||||
loadPhotoMap: function () {
|
||||
var self = this
|
||||
var ids = this.honorList.map(function (item) {
|
||||
// ids 为照片 ID 的 JSON 数组字符串,返回 res.data 为含 id、thumbnail、downloadPath 的文件数组。
|
||||
loadPhotoMap(requestId) {
|
||||
var ids = this.honorList.map((item) => {
|
||||
return item.photoId
|
||||
}).filter(function (id) {
|
||||
}).filter((id) => {
|
||||
return !!id
|
||||
})
|
||||
if (!ids.length) {
|
||||
return
|
||||
}
|
||||
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then(function (res) {
|
||||
if (res.code !== 0) {
|
||||
this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify(ids) }).then((res) => {
|
||||
if (res.code !== 0 || requestId !== this.honorRequestId || this._isDestroyed) {
|
||||
return
|
||||
}
|
||||
var fileMap = {}
|
||||
;(res.data || []).forEach(function (file) {
|
||||
fileMap[file.id] = file.thumbnail || file.downloadPath || ""
|
||||
})
|
||||
self.honorList = self.honorList.map(function (item) {
|
||||
item.cover = fileMap[item.photoId] || ""
|
||||
return item
|
||||
;(res.data || []).forEach((file) => {
|
||||
// 封面优先使用原图,避免放大 100×100 缩略图;原图地址缺失时才回退缩略图。
|
||||
fileMap[file.id] = file.downloadPath || file.thumbnail || ""
|
||||
})
|
||||
this.$set(this, "honorList", this.honorList.map((item) => {
|
||||
return Object.assign({}, item, { cover: fileMap[item.photoId] || "" })
|
||||
}))
|
||||
}).catch(() => {
|
||||
// 照片加载失败沿用卡片已有占位内容,错误提示交给全局拦截器。
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
this.getHonorTypes().then(function () {
|
||||
self.listHonor()
|
||||
created() {
|
||||
// 全局历史管理器统一处理系统返回;回调先同步状态,避免 close 事件重复回退。
|
||||
this.$set(this, "popupHistoryToken", window.h5PopupHistory.register(() => {
|
||||
this.$set(this, "yearFilterOpened", false)
|
||||
this.$set(this, "yearHistoryClosing", false)
|
||||
this.closeYearFilter()
|
||||
}))
|
||||
this.getHonorTypes().then(() => {
|
||||
if (!this._isDestroyed) {
|
||||
this.listHonor()
|
||||
}
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
// PJAX 替换页面不会自动销毁根 Vue,替换前注销年度弹层回调及页面监听。
|
||||
$(document).one("pjax:beforeReplace.honorYear", () => {
|
||||
this.$destroy()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.h5PopupHistory.unregister(this.popupHistoryToken)
|
||||
$(document).off("pjax:beforeReplace.honorYear")
|
||||
},
|
||||
// 本项目将 /deep/ 直接替换为作用域属性,须紧贴页面元素;伪元素须放在 /deep/ 后。
|
||||
style: /*language=CSS*/ `
|
||||
body {
|
||||
background: #c80019;
|
||||
@@ -281,7 +386,7 @@ layout("/layouts/platform_h5.html"){
|
||||
color: #fff6d9;
|
||||
}
|
||||
|
||||
.honor-tab.active::after {
|
||||
.honor-tab.active/deep/::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -294,6 +399,82 @@ layout("/layouts/platform_h5.html"){
|
||||
box-shadow: 0 2px 8px rgba(255, 230, 163, .38);
|
||||
}
|
||||
|
||||
.honor-year-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 52px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 10px 6px 14px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 230, 163, .14);
|
||||
border-radius: 8px;
|
||||
color: #ffe6a3;
|
||||
background: rgba(255, 255, 255, .12);
|
||||
}
|
||||
|
||||
.honor-year-filter__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.honor-year-filter__label/deep/ .van-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .honor-year-filter__menu {
|
||||
flex: 0 0 106px;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-menu__bar {
|
||||
height: 38px;
|
||||
border: 1px solid #ffe6a3;
|
||||
border-radius: 24px;
|
||||
background: rgba(151, 0, 17, .48);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-menu__title {
|
||||
color: #ffe6a3 !important;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
padding: 0 18px 0 4px;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-menu__title::after {
|
||||
right: 4px;
|
||||
border-color: transparent transparent #ffe6a3 #ffe6a3;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.honor-year-filter/deep/ .van-dropdown-item__content {
|
||||
max-height: 55vh;
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
.honor-list/deep/ .honor-empty .van-empty__description {
|
||||
padding: 0;
|
||||
color: #fff1dc;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.honor-list/deep/ .honor-empty .van-empty__bottom {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.honor-list/deep/ .honor-empty__hint {
|
||||
color: rgba(255, 233, 189, .68);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.honor-intro {
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
+111
-34
@@ -632,18 +632,28 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<!-- 保存、首次提交和重新提交继续共用原业务入口。 -->
|
||||
<div class="proposal-actions" v-if="isWriteTime">
|
||||
<van-button plain type="info" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading" @click="onSave">
|
||||
<van-button plain type="info" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading || !isDelegate" @click="onSave">
|
||||
保存草稿
|
||||
</van-button>
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading" @click="onSubmit" v-if="!taskId">
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading || !isDelegate" @click="onSubmit" v-if="!taskId">
|
||||
提交提案
|
||||
</van-button>
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading" @click="onSubmitAgain" v-else>
|
||||
<van-button type="primary" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading || !isDelegate" @click="onSubmitAgain" v-else>
|
||||
重新提交
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 非代表提示由统一历史栈控制;确认先清理弹层历史,再返回上一业务页面。 -->
|
||||
<van-dialog :value="delegateDeniedVisible"
|
||||
title="提示"
|
||||
message="当前用户不是代表,无法撰写提案"
|
||||
confirm-button-text="确认"
|
||||
:close-on-click-overlay="false"
|
||||
:close-on-popstate="false"
|
||||
@confirm="confirmDelegateDenied">
|
||||
</van-dialog>
|
||||
|
||||
<!-- 提案撰写须知弹窗 -->
|
||||
<van-action-sheet v-model="noticeDialogVisible"
|
||||
title="提案撰写须知"
|
||||
@@ -684,6 +694,8 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
proposalConfig: {},
|
||||
noticeDialogVisible: false,
|
||||
delegateDeniedVisible: false,
|
||||
isDelegate: false,
|
||||
|
||||
isWriteTime: true,
|
||||
formLoading: false,
|
||||
@@ -699,6 +711,38 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 确认非代表提示后,等待弹层历史回退完成再使用统一页面返回入口。
|
||||
confirmDelegateDenied() {
|
||||
window.h5HistoryLayerManager.closeAll(() => historyBack());
|
||||
},
|
||||
|
||||
/**
|
||||
* 按当前表单的 sessionId 查询登录用户身份,不传客户端用户ID。
|
||||
* @return {Promise<Boolean>} data 为 true 时允许继续初始化;false 时展示非代表提示。
|
||||
* 请求异常沿用全局错误提示并保持禁止提交,不将网络或权限错误当作非代表。
|
||||
*/
|
||||
checkDelegateIdentity() {
|
||||
const sessionId = this.formData.sessionId;
|
||||
this.$set(this, "isDelegate", false);
|
||||
return this.$axios.post("/platform/h5/proposal/common/isCurrentUserDelegate", {sessionId: sessionId})
|
||||
.then((res) => {
|
||||
// 页面退出或届次发生变化后,丢弃旧查询结果,避免在其他页面打开提示。
|
||||
if (this._isDestroyed || window.h5HistoryLayerManager.pageKey !== this.historyLayerPageKey ||
|
||||
this.formData.sessionId !== sessionId) {
|
||||
return false;
|
||||
}
|
||||
if (res.code !== 0 || typeof res.data !== "boolean") {
|
||||
this.$toast("代表身份查询失败,请稍后重试");
|
||||
throw new Error("代表身份查询结果异常");
|
||||
}
|
||||
this.$set(this, "isDelegate", res.data);
|
||||
if (!res.data) {
|
||||
window.h5HistoryLayerManager.open("delegate-denied");
|
||||
}
|
||||
return res.data;
|
||||
});
|
||||
},
|
||||
|
||||
handlePageBack() {
|
||||
if (window.h5HistoryLayerManager.stack.length) {
|
||||
window.h5HistoryLayerManager.close();
|
||||
@@ -722,6 +766,8 @@ layout("/layouts/platform_h5.html"){
|
||||
noticeDialogVisible: "notice"
|
||||
}[stateName];
|
||||
if (visible) {
|
||||
// 初始化或保存、提交期间禁止切换届次,避免并发请求混用不同届次的身份结果。
|
||||
if (this.formLoading) return;
|
||||
window.h5HistoryLayerManager.open(layerName);
|
||||
return;
|
||||
}
|
||||
@@ -736,18 +782,22 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this, "showCommitteePicker", stack.includes("committee-picker"));
|
||||
this.$set(this, "showSuggestionUnitPicker", stack.includes("suggestion-unit-picker"));
|
||||
this.$set(this, "noticeDialogVisible", stack.includes("notice"));
|
||||
this.$set(this, "delegateDeniedVisible", stack.includes("delegate-denied"));
|
||||
});
|
||||
},
|
||||
|
||||
// 选择器确认事件
|
||||
onSessionConfirm(value) {
|
||||
this.formData.sessionId = value.value;
|
||||
if (this.formLoading) return;
|
||||
this.$set(this.formData, 'sessionId', value.value);
|
||||
const selectedOption = this.sessionOptions.find(item => item.id === value.value);
|
||||
if (selectedOption) {
|
||||
this.$set(this.formData, 'sessionName', selectedOption.fullName);
|
||||
}
|
||||
this.setVisibleState("showSessionPicker", false);
|
||||
this.meetingChange(value.value);
|
||||
this.$set(this, "isDelegate", false);
|
||||
this.formLoading = true;
|
||||
// 等待选择器对应历史回退完成后再查询,防止新提示被此次返回一起关闭。
|
||||
window.h5HistoryLayerManager.close("session-picker", () => this.meetingChange(value.value));
|
||||
},
|
||||
|
||||
onSourceConfirm(value) {
|
||||
@@ -810,7 +860,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
// 保存草稿:校验当前表单后保存 ProposalInfo,接口成功时返回提案数据并回到我的提案。
|
||||
onSave() {
|
||||
if (this.formLoading) {
|
||||
if (this.formLoading || !this.isDelegate) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
@@ -862,7 +912,7 @@ layout("/layouts/platform_h5.html"){
|
||||
return true;
|
||||
},
|
||||
|
||||
// 提交成功后等待提示和 PJAX 跳转完成,避免跳转期间提前解除按钮的 loading 状态。
|
||||
// 提交成功后先清理弹层历史,再等待 PJAX 跳转完成,避免残留弹层记录或提前解除 loading。
|
||||
navigateToMine(delay = 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const navigate = () => {
|
||||
@@ -880,16 +930,16 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
};
|
||||
if (delay > 0) {
|
||||
setTimeout(navigate, delay);
|
||||
setTimeout(() => window.h5HistoryLayerManager.closeAll(navigate), delay);
|
||||
} else {
|
||||
navigate();
|
||||
window.h5HistoryLayerManager.closeAll(navigate);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 首次提交:info 为完整提案 JSON,成功后进入提案流程并返回我的提案。
|
||||
onSubmit() {
|
||||
if (this.formLoading) {
|
||||
if (this.formLoading || !this.isDelegate) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
@@ -934,7 +984,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
// 重新提交:除完整提案 JSON 外携带 taskId,用于继续当前退回任务。
|
||||
onSubmitAgain() {
|
||||
if (this.formLoading) {
|
||||
if (this.formLoading || !this.isDelegate) {
|
||||
return;
|
||||
}
|
||||
this.$refs.formRef.validate().then(() => {
|
||||
@@ -1002,8 +1052,9 @@ layout("/layouts/platform_h5.html"){
|
||||
});
|
||||
},
|
||||
|
||||
// 切换教代会后清空跨届次数据,并按类型、代表团、提案方式、撰写时间的顺序重新加载。
|
||||
// 切换教代会后先校验代表身份,通过后再加载该届次的业务选项。
|
||||
meetingChange(val) {
|
||||
this.formLoading = true;
|
||||
this.$set(this.formData, "typeId", null);
|
||||
this.$set(this.formData, "typeName", "");
|
||||
this.typeOptions = [];
|
||||
@@ -1011,11 +1062,19 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this.formData, "committeeId", null);
|
||||
this.$set(this.formData, "delegationName", '');
|
||||
this.$set(this.formData, "committeeName", '');
|
||||
return this.listProposalType()
|
||||
.then(() => this.listDelegation())
|
||||
.then(() => this.searchMineDelegation())
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.checkWriteTime());
|
||||
return this.checkDelegateIdentity().then((allowed) => {
|
||||
if (!allowed) return;
|
||||
return this.listProposalType()
|
||||
.then(() => this.listDelegation())
|
||||
.then(() => this.searchMineDelegation())
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.checkWriteTime());
|
||||
}).catch(() => {
|
||||
// 统一请求拦截器已提示失败;禁止使用未完整加载的届次数据提交。
|
||||
this.$set(this, "isDelegate", false);
|
||||
}).finally(() => {
|
||||
this.formLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 获取代表团
|
||||
@@ -1044,20 +1103,33 @@ layout("/layouts/platform_h5.html"){
|
||||
if (!isModify && this.sessionOptions && this.sessionOptions.length > 0) {
|
||||
this.$set(this.formData, "sessionId", this.sessionOptions[0].id);
|
||||
this.$set(this.formData, "sessionName", this.sessionOptions[0].fullName);
|
||||
}
|
||||
// 无届次不能查询代表身份,也不能当作非代表提示。
|
||||
if (!this.formData.sessionId) {
|
||||
this.$toast("没有可用的教代会,暂时无法撰写提案");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.checkDelegateIdentity().then((allowed) => {
|
||||
if (!allowed) return;
|
||||
if (isModify) {
|
||||
// 编辑以提案原届次校验身份,保留已有表单和选择器回显。
|
||||
return this.listDelegation()
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.listProposalType())
|
||||
.then(() => this.echoPicker());
|
||||
}
|
||||
return this.listProposalType()
|
||||
.then(() => this.listDelegation())
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.searchMineDelegation())
|
||||
.then(() => this.checkWriteTime());
|
||||
}
|
||||
// 没有开启的教代会时不继续请求依赖 sessionId 的接口。
|
||||
if (!this.formData.sessionId) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.listDelegation()
|
||||
.then(() => this.listSource())
|
||||
.then(() => this.listProposalType())
|
||||
.then(() => this.echoPicker());
|
||||
.then(() => this.checkWriteTime())
|
||||
.then(() => {
|
||||
// 初始化请求完成前可能已离开页面,此时不能给新页面追加撰写须知历史。
|
||||
if (!this._isDestroyed && window.h5HistoryLayerManager.pageKey === this.historyLayerPageKey) {
|
||||
window.h5HistoryLayerManager.open("notice");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -1118,18 +1190,15 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
init() {
|
||||
if (this.bizId) {
|
||||
this.$axios.post("/platform/proposal/write/detail", {id: this.bizId}).then((res) => {
|
||||
return this.$axios.post("/platform/proposal/write/detail", {id: this.bizId}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
res.data.brief=removeHTMLTag(res.data.brief)
|
||||
res.data.measures=removeHTMLTag(res.data.measures)
|
||||
this.$set(this, "formData", res.data);
|
||||
this.listOpenSession(true);
|
||||
return this.listOpenSession(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setVisibleState("noticeDialogVisible", true);
|
||||
this.listOpenSession(false);
|
||||
|
||||
// 设置用户信息
|
||||
this.$set(this.formData, "createUserId", this.$store.state.user.id);
|
||||
this.$set(this.formData, "createUserName", this.$store.state.user.username);
|
||||
@@ -1137,12 +1206,20 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$set(this.formData, "createTime", this.$moment().format("YYYY-MM-DD"));
|
||||
this.$set(this.formData, "mobile", this.$store.state.user.mobile);
|
||||
this.$set(this.formData, "unitName", this.$store.state.user.unit?.name);
|
||||
return this.listOpenSession(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerHistoryLayers();
|
||||
this.init();
|
||||
// 从确定届次到完成身份检查及表单初始化期间,保持保存、提交按钮禁用。
|
||||
this.formLoading = true;
|
||||
this.init().catch(() => {
|
||||
// 网络、权限或初始化失败由统一拦截器提示,不放行尚未完成初始化的表单。
|
||||
this.$set(this, "isDelegate", false);
|
||||
}).finally(() => {
|
||||
this.formLoading = false;
|
||||
});
|
||||
this.listSuggestUnit();
|
||||
this.getProposalConfig();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
<div class="inclusive-detail-page" id="app" v-cloak>
|
||||
<van-nav-bar @click-left="back" left-arrow left-text="返回" placeholder title="幸福工荟详情"></van-nav-bar>
|
||||
<van-image :src="previewUrl(detail.cover)" fit="cover" height="220" v-if="detail.cover" width="100%"></van-image>
|
||||
<van-cell-group v-if="detail.id">
|
||||
<van-cell :title="detail.title"></van-cell>
|
||||
<van-cell :value="detail.tagTypeName || '幸福工荟'" title="标签"></van-cell>
|
||||
<van-cell :value="formatDate(detail.publishTime || detail.createdAt)" title="发布时间"></van-cell>
|
||||
<van-cell :label="detail.summary" title="摘要"></van-cell>
|
||||
</van-cell-group>
|
||||
<van-empty v-if="!formLoading && !detail.id" description="内容不存在或已下架"></van-empty>
|
||||
<div class="inclusive-content" v-html="detail.content"></div>
|
||||
<div class="inclusive-footer" v-if="hasLink"><van-button @click="openLink" block type="primary">查看活动页面</van-button></div>
|
||||
</div>
|
||||
<style>
|
||||
.inclusive-content { padding: 16px; line-height: 1.8; word-break: break-word; }
|
||||
.inclusive-content img { height: auto; max-width: 100%; }
|
||||
.inclusive-footer { padding: 12px 12px calc(12px + env(safe-area-inset-bottom)); }
|
||||
</style>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() { return {id: "", detail: {}, formLoading: false} },
|
||||
computed: {
|
||||
hasLink() { return !!(this.detail.linkUrl && this.detail.linkUrl.trim()) }
|
||||
},
|
||||
methods: {
|
||||
back() { this.historyBack() },
|
||||
previewUrl(cover) { return cover || "" },
|
||||
formatDate(value) { return value ? moment(value).format("YYYY-MM-DD") : "" },
|
||||
// id 来自地址参数;data 为已发布详情,无效或已下架时清空旧内容并展示提示。
|
||||
findOne() {
|
||||
if (!this.id) return
|
||||
this.$set(this, "formLoading", true)
|
||||
this.$axios.post("/platform/h5/inclusive/findOne", {id: this.id}).then((res) => {
|
||||
this.$set(this, "detail", res.code === 0 && res.data ? res.data : {})
|
||||
if (res.code !== 0 || !res.data) this.$toast(res.msg || "未查询到活动详情")
|
||||
}).finally(() => {
|
||||
this.$set(this, "formLoading", false)
|
||||
}).catch(() => {
|
||||
// 包括下架、无权限和网络异常;错误提示由公共 Axios 拦截器统一展示。
|
||||
this.$set(this, "detail", {})
|
||||
})
|
||||
},
|
||||
// 外链沿用原模块配置;如公共组件存在弹层,先清理历史再离开当前页面。
|
||||
openLink() {
|
||||
if (!this.hasLink) return
|
||||
const navigate = () => { window.location.href = this.detail.linkUrl.trim() }
|
||||
if (window.h5HistoryLayerManager) {
|
||||
window.h5HistoryLayerManager.closeAll(navigate)
|
||||
} else {
|
||||
navigate()
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$set(this, "id", GetQueryString("id"))
|
||||
this.findOne()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,151 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
<style>
|
||||
.inclusive-page { background: #f6f7f9; min-height: 100vh; }
|
||||
.inclusive-tab-wrap { padding: 10px 12px 8px; background: #fff; box-shadow: 0 2px 8px rgba(8, 23, 118, .08); }
|
||||
.inclusive-tabs { background: transparent; border-radius: 0; padding: 0; box-shadow: none; }
|
||||
.inclusive-tabs .van-tabs__wrap { height: 32px; overflow: hidden; }
|
||||
.inclusive-tabs .van-tabs__nav { display: flex; overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; padding-right: 10px; background: transparent; }
|
||||
.inclusive-tabs .van-tabs__nav::-webkit-scrollbar { display: none; }
|
||||
.inclusive-tabs .van-tab { flex: 0 0 auto; padding: 0 5px; }
|
||||
.inclusive-tabs .van-tab__text { padding: 7px 12px; border-radius: 9px; background: #f2f5f8; color: #303846; font-size: 12px; line-height: 18px; white-space: nowrap; font-weight: 600; }
|
||||
.inclusive-tabs .van-tab--active .van-tab__text { color: #fff; background: #081776; }
|
||||
.inclusive-tabs .van-tabs__line { display: none; }
|
||||
.inclusive-list { padding: 12px 12px calc(22px + env(safe-area-inset-bottom)); }
|
||||
.inclusive-card { overflow: hidden; margin-bottom: 14px; background: #fff; border-radius: 12px; box-shadow: 0 3px 10px rgba(31, 45, 61, .14); }
|
||||
.inclusive-cover { width: 100%; height: 150px; overflow: hidden; background: #e8eef6; }
|
||||
.inclusive-cover img { width: 100%; height: 100%; display: block; }
|
||||
.inclusive-card-main { display: flex; flex-direction: column; min-width: 0; min-height: 132px; position: relative; padding: 12px 42px 14px 14px; }
|
||||
.inclusive-tag { display: inline-block; max-width: 120px; padding: 3px 9px; border-radius: 5px; color: #0b63d8; background: #eaf3ff; font-size: 12px; line-height: 18px; }
|
||||
.inclusive-title { margin-top: 8px; color: #111827; font-size: 16px; line-height: 24px; font-weight: bold; min-height: 24px; }
|
||||
.inclusive-summary { margin-top: 5px; color: #53657f; font-size: 13px; line-height: 19px; min-height: 38px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.inclusive-date { margin-top: auto; color: #6b7c96; font-size: 12px; display: flex; align-items: center; gap: 4px; }
|
||||
.inclusive-arrow { color: #8a96a8; position: absolute; right: 14px; bottom: 16px; font-size: 22px; }
|
||||
.inclusive-empty { min-height: 320px; display: flex; align-items: center; justify-content: center; flex-direction: column; color: #909399; font-size: 14px; background: transparent; }
|
||||
.inclusive-empty-icon { width: 120px; height: 120px; margin-bottom: 10px; opacity: .56; }
|
||||
</style>
|
||||
<div class="inclusive-page" id="app" v-cloak>
|
||||
<van-nav-bar @click-left="back" fixed left-arrow left-text="返回" placeholder title="幸福工荟"></van-nav-bar>
|
||||
<van-sticky :offset-top="46">
|
||||
<div class="inclusive-tab-wrap">
|
||||
<van-tabs @change="tagChange" class="inclusive-tabs" v-model="activeTag">
|
||||
<van-tab name="" title="全部"></van-tab>
|
||||
<van-tab :key="item.value" :name="item.value" :title="item.text" v-for="item in tagOptions"></van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</van-sticky>
|
||||
<div class="inclusive-list">
|
||||
<van-pull-refresh @refresh="onRefresh" v-model="refreshing">
|
||||
<van-list :immediate-check="false" :error.sync="loadError" error-text="加载失败,点击重试" :finished="finished" :finished-text="tableData.length ? '没有更多了' : ''" @load="pageData" v-model="loading">
|
||||
<div :key="item.id" @click="openDetail(item)" class="inclusive-card" v-for="item in tableData">
|
||||
<div class="inclusive-cover"><img :src="previewUrl(item.cover)" v-if="item.cover"></div>
|
||||
<div class="inclusive-card-main">
|
||||
<div class="inclusive-tag van-ellipsis">{{item.tagTypeName || '幸福工荟'}}</div>
|
||||
<div class="inclusive-title van-multi-ellipsis--l2">{{item.title}}</div>
|
||||
<div class="inclusive-summary">{{item.summary}}</div>
|
||||
<div class="inclusive-date"><van-icon name="underway-o"></van-icon><span>{{formatDate(item.publishTime || item.createdAt)}}</span></div>
|
||||
<van-icon class="inclusive-arrow" name="arrow"></van-icon>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
<div class="inclusive-empty" v-if="!loading && tableData.length === 0">
|
||||
<img class="inclusive-empty-icon" src="https://img01.yzcdn.cn/vant/empty-image-default.png">
|
||||
<div>暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
activeTag: "", tagOptions: [], tableData: [], loading: false, refreshing: false,
|
||||
finished: false, loadError: false, requesting: false, loadSequence: 0,
|
||||
pageForm: {pageNumber: 1, pageSize: 6, totalCount: 0, tagType: ""}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
previewUrl(cover) { return cover || "" },
|
||||
formatDate(value) { return value ? moment(value).format("YYYY-MM-DD") : "" },
|
||||
back() { this.historyBack() },
|
||||
// 无参数;data 数组中的 text/value 分别对应标签名称和标签编码。
|
||||
getTagOptions() {
|
||||
this.$axios.post("/platform/h5/inclusive/tagOptions", {}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, "tagOptions", res.data || [])
|
||||
} else {
|
||||
this.$toast(res.msg || "标签加载失败")
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 参数为页码、每页数量及标签编码;data.list 是当前页,totalCount 是过滤后的总数。
|
||||
pageData() {
|
||||
if (this.requesting) return
|
||||
this.$set(this, "requesting", true)
|
||||
this.$set(this, "loading", true)
|
||||
this.$set(this, "loadError", false)
|
||||
this.$set(this, "loadSequence", this.loadSequence + 1)
|
||||
const sequence = this.loadSequence
|
||||
const params = Object.assign({}, this.pageForm, {tagType: this.activeTag})
|
||||
this.$axios.post("/platform/h5/inclusive/pageData", params).then((res) => {
|
||||
// 切换标签或刷新后,旧请求的响应不再追加到新列表。
|
||||
if (sequence !== this.loadSequence) return
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
const list = data.list || []
|
||||
this.$set(this, "tableData", this.tableData.concat(list))
|
||||
this.$set(this.pageForm, "totalCount", data.totalCount || 0)
|
||||
this.$set(this, "finished", list.length === 0 || this.tableData.length >= this.pageForm.totalCount)
|
||||
if (!this.finished) this.$set(this.pageForm, "pageNumber", params.pageNumber + 1)
|
||||
} else {
|
||||
this.$set(this, "loadError", true)
|
||||
this.$toast(res.msg || "查询失败")
|
||||
}
|
||||
}, () => {
|
||||
if (sequence === this.loadSequence) this.$set(this, "loadError", true)
|
||||
}).finally(() => {
|
||||
if (sequence === this.loadSequence) {
|
||||
this.$set(this, "loading", false)
|
||||
this.$set(this, "refreshing", false)
|
||||
this.$set(this, "requesting", false)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 重置分页并使在途响应失效,防止不同标签的数据混在同一个列表中。
|
||||
resetList() {
|
||||
this.$set(this, "loadSequence", this.loadSequence + 1)
|
||||
this.$set(this, "requesting", false)
|
||||
this.$set(this, "tableData", [])
|
||||
this.$set(this, "finished", false)
|
||||
this.$set(this, "loadError", false)
|
||||
this.$set(this.pageForm, "pageNumber", 1)
|
||||
this.$set(this.pageForm, "totalCount", 0)
|
||||
},
|
||||
tagChange(value) {
|
||||
this.$set(this, "activeTag", value)
|
||||
this.resetList()
|
||||
this.pageData()
|
||||
},
|
||||
onRefresh() {
|
||||
this.resetList()
|
||||
this.$set(this, "refreshing", true)
|
||||
this.pageData()
|
||||
},
|
||||
openDetail(item) {
|
||||
this.$pjaxReplace("/platform/h5/inclusive/detail?id=" + encodeURIComponent(item.id))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getTagOptions()
|
||||
this.pageData()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$set(this, "loadSequence", this.loadSequence + 1)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -306,7 +306,7 @@ const home = {
|
||||
action: "查看信息",
|
||||
asset: "/assets/mobile/img/home/v4/feature-inclusive-v8.png",
|
||||
className: "feature-card--inclusive",
|
||||
href: ""
|
||||
href: "/platform/h5/inclusive"
|
||||
}
|
||||
],
|
||||
appLogo: "",
|
||||
|
||||
Reference in New Issue
Block a user