Compare commits

...
12 Commits
Author SHA1 Message Date
c-duxc1 7d5e948e4c 活动场地预约-预约审核(包含手机端):修复多个场地管理员审核同时审核相同一页数据时(包含一键通过和一键驳回),后端异常报错的问题; 2026-10-23 17:22:10 +08:00
c-duxc1 030bb7cb23 活动场地预约-预约审核:修复多个场地管理员审核同时审核相同一页数据时,后端异常报错的问题; 2026-10-23 17:15:22 +08:00
c-duxc1 18397fb7c9 医疗互助系统-医保报销数据导入和补助台账:新增身份证号匹配类型查询(身份证号关联风险); 2026-10-12 14:49:31 +08:00
c-duxc1 1cd0552a26 医疗互助系统-医保报销数据导入和补助台账:新增身份证号匹配类型查询(方便搜索身份证号码匹配异常的数据); 2026-10-12 14:23:39 +08:00
c-duxc1 19ebd4cac3 医疗互助系统-医保报销数据导入-导入医保数据:系统用户身份证为空时使用会员台账身份证,最后才按姓名匹配。; 2026-10-10 14:52:10 +08:00
c-duxc1 4ca2b73d7b 医疗互助系统-医保报销数据导入和补助台账:新增重新关联人员按钮,方便修复重名导致的匹配错误问题; 2026-10-10 14:29:48 +08:00
c-duxc1 03864ea416 修复培训活动二维码无法签到的问题:培训签到页未初始化微信 JS-SDK,导致“扫一扫”接口无法调用。 2026-10-09 10:47:29 +08:00
c-chengc1 c909f0a3fb feat:场地预约-面向分工会场地,1、支持协会负责人进行预约,2、预约前签字 2026-09-30 15:35:56 +08:00
JyuHsin 593500cecc commit 2026-09-24 10:09:23 +08:00
c-zhouhf1 f709e3a0e4 commit 2026-09-24 09:56:49 +08:00
c-duxc1 1c2630a338 医疗互助系统-补助台账:修复搜索异常;修复数据导入计算时报错数据也被导入等问题; 2026-09-23 19:06:54 +08:00
c-duxc1 c90c4619ee 医疗互助系统-医保,报销数据导入:导入学校二次报销数据接口报错处理;导出汇总-补助计算金额字段值修复(四项之和减去学校二次报销金额后的结果带入‘江苏河海大学计算方法’计算一下); 2026-09-22 18:17:11 +08:00
54 changed files with 1977 additions and 176 deletions
+51 -1
View File
@@ -1 +1,51 @@
/.idea/
# 依赖与构建产物
**/node_modules/
**/dist/
**/build/
**/target/
**/out/
**/.next/
**/.nuxt/
**/coverage/
# 日志、缓存与临时文件
**/*.log
**/*.tmp
**/*.temp
**/.cache/
**/.eslintcache
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
**/pnpm-debug.log*
# IDE 与操作系统文件
.idea/
.vscode/
*.iml
.project
.classpath
.settings/
.DS_Store
Thumbs.db
# 本地环境和密钥配置(保留示例文件)
**/.env
**/.env.local
**/.env.*.local
!**/.env.example
!**/.env.*.example
# Java 编译文件
**/*.class
.trae/.ignore
.trae/cache-components/SKILL.md
.trae/code-reviewer/SKILL.md
.trae/find-skills/SKILL.md
.trae/fix/SKILL.md
.trae/frontend-code-review/SKILL.md
.trae/frontend-design/SKILL.md
.trae/fullstack-developer/SKILL.md
.trae/pr-creator/SKILL.md
.trae/update-docs/SKILL.md
.trae/webapp-testing/SKILL.md
@@ -0,0 +1,58 @@
package io.v.nutz.activity.controller.site;
import cn.wizzer.framework.base.Result;
import io.v.nutz.activity.services.SiteReserveService;
import io.v.nutz.annontation.ViReturn;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
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.POST;
/**
* 电脑端和手机端共用的场地预约承诺书阅读、签署入口。
*/
@IocBean
@At("/platform/activity/site/commitment")
@Ok("json:full")
@RequiresAuthentication
public class SiteCommitmentController {
@Inject
private SiteReserveService siteReserveService;
/**
* 校验预约资格并记录服务器阅读开始时间。
* @param siteId 场地ID
* @return 承诺书及阅读凭证,或不可预约原因
*/
@At
@POST
@ViReturn
public Object begin(String siteId) {
try {
return siteReserveService.beginCommitment(siteId);
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 阅读十秒后签署本次承诺书。
* @param commitmentId 本次阅读凭证
* @param signature 本次手写签名PNG数据
* @return 签署结果
*/
@At
@POST
@ViReturn
public Object sign(String commitmentId, String signature) {
try {
siteReserveService.signCommitment(commitmentId, signature);
return Result.success("签署成功");
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
}
@@ -2,6 +2,10 @@ package io.v.nutz.activity.controller.site;
import io.v.nutz.activity.models.ActivitySiteInfo;
import cn.wizzer.framework.base.Result;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import java.nio.charset.StandardCharsets;
import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.utils.PageUtil;
@@ -62,16 +66,34 @@ public class SiteManageController {
return siteInfoService.listPage(pageNumber, pageSize, cnd);
}
/**
* 新增场地,面向分工会时要求配置预约前承诺书。
* @param activitySiteInfo 场地信息及承诺书标题、正文
* @return 保存结果或配置校验错误
*/
@At
@ViReturn
public Object doAdd(@Param("data") ActivitySiteInfo activitySiteInfo) {
String error = prepareCommitment(activitySiteInfo);
if (error != null) {
return Result.error(error);
}
siteInfoService.insert(activitySiteInfo);
return null;
}
/**
* 保存场地及承诺书配置,已签署申请继续保留原正文快照。
* @param activitySiteInfo 修改后的场地信息
* @return 保存结果或配置校验错误
*/
@At
@ViReturn
public Object doEdit(@Param("data") ActivitySiteInfo activitySiteInfo) {
String error = prepareCommitment(activitySiteInfo);
if (error != null) {
return Result.error(error);
}
siteInfoService.update(activitySiteInfo);
return null;
}
@@ -88,4 +110,25 @@ public class SiteManageController {
public Object findOneSite(String id) {
return siteInfoService.fetch(id);
}
// 承诺书会在预约弹窗和历史签署记录中展示,仅保留正文所需的安全HTML。
private String prepareCommitment(ActivitySiteInfo site) {
if (site.getCommitmentContent() != null) {
site.setCommitmentContent(Jsoup.clean(site.getCommitmentContent(), Whitelist.relaxed()));
}
if (Integer.valueOf(1).equals(site.getReserveTarget())) {
if (Strings.isBlank(site.getCommitmentTitle()) || Strings.isBlank(site.getCommitmentContent())
|| Strings.isBlank(Jsoup.parse(site.getCommitmentContent()).text())) {
return "面向分工会的场地必须配置承诺书标题和正文";
}
}
if (site.getCommitmentTitle() != null && site.getCommitmentTitle().length() > 200) {
return "承诺书标题不能超过200字";
}
if (site.getCommitmentContent() != null
&& site.getCommitmentContent().getBytes(StandardCharsets.UTF_8).length > 60000) {
return "承诺书正文过长,请精简内容";
}
return null;
}
}
@@ -41,6 +41,7 @@ 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 java.util.ArrayList;
import java.util.Date;
@@ -161,9 +162,25 @@ public class SiteReserveController {
return fetch.getReserve_state() > stateCode;
}
/**
* 创建预约,面向分工会的场地必须先完成本次承诺书签署。
* @param activitySiteReserve 预约表单
* @param days 本次申请的预约日期
* @param commitmentId 当前用户签署的场地承诺书凭证
* @return 预约结果或资格、签署校验失败原因
*/
@At
@ViReturn
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) {
public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days, String commitmentId) {
if (activitySiteReserve == null || days == null || days.length == 0) {
return Result.error("请选择预约场地和日期");
}
try {
siteReserveService.checkCommitment(activitySiteReserve.getSite_id(), commitmentId);
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
Map<Boolean, String> validateSiteReserve = siteReserveService.validateSiteReserve(
activitySiteReserve.getSite_id(),
@@ -183,22 +200,31 @@ public class SiteReserveController {
Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName);
if (stateCode == null) {
return null;
return Result.error("场地尚未配置预约审核流程");
}
for (String d : days) {
activitySiteReserve.setReserve_day(d);
//查询这个人是否已经预约这个时间段
int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id"))
.and("reserve_day", "=", activitySiteReserve.getReserve_day()).and("start_time", "=", activitySiteReserve.getStart_time())
.and("end_time", "=", activitySiteReserve.getEnd_time()));
if (count > 0) {
return Result.error("您已预约该时间段!");
}
try {
// 同一申请的多天预约和签署凭证一起提交,失败时不消耗签署凭证。
Trans.exec(() -> {
siteReserveService.bindCommitment(activitySiteReserve.getSite_id(), commitmentId, r);
for (String d : days) {
activitySiteReserve.setReserve_day(d);
//查询这个人是否已经预约这个时间段
int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id"))
.and("reserve_day", "=", activitySiteReserve.getReserve_day()).and("start_time", "=", activitySiteReserve.getStart_time())
.and("end_time", "=", activitySiteReserve.getEnd_time()));
if (count > 0) {
throw new IllegalArgumentException("您已预约该时间段!");
}
activitySiteReserve.setReserve_state(stateCode);
activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString());
siteReserveService.insert(activitySiteReserve);
activitySiteReserve.setReserve_state(stateCode);
activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString());
activitySiteReserve.setId(R.UU32());
siteReserveService.insert(activitySiteReserve);
}
});
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
//发送微信提醒审核人,查询状态码对应的审核人
@@ -307,10 +333,18 @@ public class SiteReserveController {
return siteReserveService.count(cnd);
}
/**
* 查询预约详情及本次申请签署的承诺书快照。
* @param id 预约记录ID
* @return 预约详情、审核记录和承诺书签署信息
*/
@At
@ViReturn
public Object findOne(String id) {
ActivitySiteReserve fetch = siteReserveService.fetch(id);
if (fetch == null) {
return Result.error("预约记录不存在");
}
Sql sql = Sqls.create("""
SELECT
asr.*,
@@ -386,6 +420,7 @@ public class SiteReserveController {
i.setCondition(cnd);
List<Record> list1 = siteReserveService.list(i);
record.set("site_info", list1);
record.set("commitment", siteReserveService.findCommitment(fetch.getSqid()));
return record;
}
@@ -455,6 +490,13 @@ public class SiteReserveController {
return result;
}
/**
* 获取指定日期的场地预约时段,支持以24:00作为营业结束时间。
*
* @param siteId 场地ID
* @param day 预约日期,格式为yyyy-MM-dd
* @return 预约时段及不可预约状态,保留配置的结束时间用于匹配预约记录
*/
@At
@ViReturn
public Object getTimesByDay(String siteId, String day) {
@@ -480,16 +522,19 @@ public class SiteReserveController {
} else {
// 构造起始时间
DateTime start = DateUtil.parse(startTime, "HH:mm"); // "09:00"
DateTime end = DateUtil.parse(endTime, "HH:mm"); // "17:00"
// 场地允许营业至24:00,按次日零点计算,返回时保留24:00以匹配预约记录。
DateTime end = "24:00".equals(endTime)
? DateUtil.offsetDay(DateUtil.parse("00:00", "HH:mm"), 1)
: DateUtil.parse(endTime, "HH:mm");
DateTime current = start;
while (DateUtil.compare(current, end) < 0) { // 当前时间 <= 结束时间
DateTime next = DateUtil.offsetSecond(current, (int) (timeUnit * 3600));
// 如果超过结束时间,则使用 endTime
if (DateUtil.compare(next, end) > 0) {
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.format(end, "HH:mm")));
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", endTime));
break;
} else {
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.format(next, "HH:mm")));
result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.compare(next, end) == 0 ? endTime : DateUtil.format(next, "HH:mm")));
}
current = next;
}
@@ -7,6 +7,7 @@ import io.v.nutz.activity.services.ActivityCommonService;
import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.activity.services.SiteReserveService;
import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.result.Result;
import io.v.nutz.base.utils.MsgApi;
import io.v.nutz.base.utils.PageUtil;
import io.v.nutz.base.utils.SendMsg;
@@ -18,6 +19,7 @@ import io.v.nutz.sys.services.SysUserService;
import io.v.nutz.util.ShiroUtil;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
@@ -34,6 +36,7 @@ import org.nutz.mvc.annotation.Ok;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -148,14 +151,49 @@ public class SiteReviewController {
@At
@ViReturn
public Object doReview(String[] id, Audit audit, Boolean isPass) {
public Object doReview(String[] id, Integer[] reserveState, Audit audit, Boolean isPass) {
if (id == null || reserveState == null || id.length == 0 || id.length != reserveState.length || isPass == null) {
return Result.error("审核参数错误,请刷新页面后重试");
}
List<ActivitySiteReserve> reviewList = new ArrayList<>();
List<Integer> afterStateList = new ArrayList<>();
int skippedCount = 0;
for (int i = 0; i < id.length; i++) {
ActivitySiteReserve reserve = siteReserveService.fetch(id[i]);
if (reserve == null || !Objects.equals(reserve.getReserve_state(), reserveState[i])) {
skippedCount++;
continue;
}
Integer afterStateCode = activityCommonService.findAfterStateCode(reserve.getReserve_state(), isPass);
if (afterStateCode == null) {
return Result.error("当前预约已完成审核或审核流程配置异常,请刷新页面后重试");
}
reviewList.add(reserve);
afterStateList.add(afterStateCode);
}
if (reviewList.isEmpty()) {
return Result.success("所选预约已被其他人审核,无需重复处理")
.addData(NutMap.NEW().setv("skippedCount", skippedCount));
}
siteReserveService.insert(audit);
for (String s : id) {
ActivitySiteReserve fetch = siteReserveService.fetch(s);
int reviewCount = 0;
for (int i = 0; i < reviewList.size(); i++) {
ActivitySiteReserve fetch = reviewList.get(i);
Integer stateCode = fetch.getReserve_state();
Integer afterStateCode = activityCommonService.findAfterStateCode(stateCode, isPass);
Integer afterStateCode = afterStateList.get(i);
// 按原审核状态更新,避免多个管理员同时审核时重复处理同一申请
int updateCount = siteReserveService.update(Chain.make("reserve_state", afterStateCode),
Cnd.NEW().and("sqid", "=", fetch.getSqid()).and("reserve_state", "=", stateCode));
if (updateCount == 0) {
skippedCount++;
continue;
}
reviewCount++;
List<ActivitySiteReserve> reserves = siteReserveService.query(Cnd.NEW().and("sqid", "=", fetch.getSqid()));
String days = "";
@@ -217,6 +255,15 @@ public class SiteReviewController {
}
}
}
return null;
if (reviewCount == 0) {
siteReserveService.dao().delete(audit);
return Result.success("所选预约已被其他人审核,无需重复处理")
.addData(NutMap.NEW().setv("skippedCount", skippedCount));
}
if (skippedCount > 0) {
return Result.success("审核完成,成功处理" + reviewCount + "条,跳过" + skippedCount + "条状态已变化的预约")
.addData(NutMap.NEW().setv("skippedCount", skippedCount));
}
return Result.success("审核成功");
}
}
@@ -0,0 +1,60 @@
package io.v.nutz.activity.models;
import lombok.Data;
import org.nutz.dao.entity.annotation.*;
/**
* 场地预约承诺书阅读、签署及申请关联记录,保留签署时的正文快照。
*/
@Data
@Table("activity_site_commitment")
public class ActivitySiteCommitment {
@Name
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
private String id;
@Column
@Comment("场地ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String siteId;
@Column
@Comment("签署人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("签署人姓名快照")
@ColDefine(type = ColType.VARCHAR, width = 120)
private String userName;
@Column
@Comment("承诺书标题快照")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String title;
@Column
@Comment("承诺书正文快照")
@ColDefine(type = ColType.TEXT)
private String content;
@Column
@Comment("服务器记录的阅读开始时间,毫秒")
private Long readAt;
@Column
@Comment("服务器记录的签署时间,毫秒")
private Long signedAt;
@Column
@Comment("本次手写签名PNG数据")
@ColDefine(type = ColType.TEXT)
private String signature;
@Column
@Comment("预约申请编号,未提交时为空")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String sqid;
}
@@ -83,6 +83,16 @@ public class ActivitySiteInfo {
@ColDefine(type = ColType.TEXT)
private String introduce;
@Column
@Comment("预约前承诺书标题")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String commitmentTitle;
@Column
@Comment("预约前承诺书正文")
@ColDefine(type = ColType.TEXT)
private String commitmentContent;
@Column
@Comment("可容纳人数")
@ColDefine(type = ColType.INT)
@@ -2,11 +2,53 @@ package io.v.nutz.activity.services;
import io.v.nutz.activity.models.ActivitySiteReserve;
import io.v.nutz.activity.models.ActivitySiteCommitment;
import io.v.nutz.base.service.BaseService;
import org.nutz.lang.util.NutMap;
import java.util.Map;
public interface SiteReserveService extends BaseService<ActivitySiteReserve> {
Map<Boolean, String> validateSiteReserve(String siteId, String day, String startTime, String endTime, Integer joinCount);
/**
* 校验当前用户的预约资格并开始本次承诺书阅读。
* @param siteId 预约场地ID
* @return 是否需要签署及本次承诺书快照、凭证
* @throws IllegalArgumentException 无预约资格或场地未配置承诺书
*/
NutMap beginCommitment(String siteId);
/**
* 阅读满十秒后保存当前用户本次手写签名。
* @param commitmentId 本次阅读凭证
* @param signature PNG格式的签名数据
* @throws IllegalArgumentException 阅读不足十秒、凭证失效或签名无效
*/
void signCommitment(String commitmentId, String signature);
/**
* 预约提交前校验身份、承诺书版本和本次签署状态。
* @param siteId 预约场地ID
* @param commitmentId 签署凭证,非分工会场地可为空
* @throws IllegalArgumentException 预约资格或签署条件不满足
*/
void checkCommitment(String siteId, String commitmentId);
/**
* 在预约保存事务中一次性关联签署凭证,失败时随预约一起回滚。
* @param siteId 预约场地ID
* @param commitmentId 签署凭证
* @param sqid 本次预约申请编号
* @throws IllegalArgumentException 凭证失效或已被其他申请使用
*/
void bindCommitment(String siteId, String commitmentId, String sqid);
/**
* 查询申请签署时的承诺书,供已有预约详情展示。
* @param sqid 预约申请编号
* @return 签署快照,历史未签署申请返回null
*/
ActivitySiteCommitment findCommitment(String sqid);
}
@@ -2,6 +2,7 @@ package io.v.nutz.activity.services.impl;
import io.v.nutz.activity.models.ActivitySiteInfo;
import io.v.nutz.activity.models.ActivitySiteCommitment;
import io.v.nutz.activity.models.ActivitySiteReserve;
import io.v.nutz.activity.models.ActivityType;
import io.v.nutz.activity.services.SiteReserveService;
@@ -9,8 +10,20 @@ import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.constant.AuditTypeEnum;
import io.v.nutz.modules.models.AuditState;
import org.nutz.dao.Cnd;
import org.nutz.dao.Chain;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
import org.nutz.lang.random.R;
import org.nutz.lang.util.NutMap;
import org.apache.shiro.SecurityUtils;
import io.v.nutz.sys.models.Sys_user;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.util.Base64;
import java.util.Objects;
import java.util.List;
import java.util.Map;
@@ -28,6 +41,138 @@ public class SiteReserveServiceImpl extends BaseServiceImpl<ActivitySiteReserve>
super(dao);
}
private ActivitySiteInfo commitmentSite(String siteId) {
ActivitySiteInfo site = Strings.isBlank(siteId) ? null : dao().fetch(ActivitySiteInfo.class, siteId);
if (site == null || !Boolean.TRUE.equals(site.getState())) {
throw new IllegalArgumentException("场地不存在或已关闭预约");
}
if (!SecurityUtils.getSubject().isAuthenticated()) {
throw new IllegalArgumentException("请先登录后预约");
}
if (Integer.valueOf(1).equals(site.getReserveTarget())
&& !SecurityUtils.getSubject().hasRole("sysadmin")
&& !SecurityUtils.getSubject().hasRole("gh01")
&& !SecurityUtils.getSubject().hasRole("H04")
&& !SecurityUtils.getSubject().hasRole("H02")) {
throw new IllegalArgumentException("该场地仅限分工会预约人员或协会负责人预约");
}
return site;
}
private ActivitySiteCommitment currentCommitment(String commitmentId) {
ActivitySiteCommitment record = Strings.isBlank(commitmentId) ? null
: dao().fetch(ActivitySiteCommitment.class, commitmentId);
Sys_user user = (Sys_user) SecurityUtils.getSubject().getPrincipal();
if (user == null || record == null || !user.getId().equals(record.getUserId())
|| record.getReadAt() == null || System.currentTimeMillis() - record.getReadAt() > 30 * 60 * 1000L
|| Strings.isNotBlank(record.getSqid())) {
throw new IllegalArgumentException("签署凭证已失效,请重新阅读并签署承诺书");
}
ActivitySiteInfo site = commitmentSite(record.getSiteId());
if (!Objects.equals(site.getCommitmentTitle(), record.getTitle())
|| !Objects.equals(site.getCommitmentContent(), record.getContent())) {
throw new IllegalArgumentException("承诺书内容已更新,请重新阅读并签署");
}
return record;
}
/** {@inheritDoc} */
@Override
public NutMap beginCommitment(String siteId) {
ActivitySiteInfo site = commitmentSite(siteId);
if (!Integer.valueOf(1).equals(site.getReserveTarget())) {
return NutMap.NEW().addv("required", false);
}
if (Strings.isBlank(site.getCommitmentTitle()) || Strings.isBlank(site.getCommitmentContent())) {
throw new IllegalArgumentException("场地尚未配置承诺书,暂不能预约,请联系场地管理员");
}
Sys_user user = (Sys_user) SecurityUtils.getSubject().getPrincipal();
ActivitySiteCommitment record = new ActivitySiteCommitment();
record.setId(R.UU32());
record.setSiteId(siteId);
record.setUserId(user.getId());
record.setUserName(user.getUsername());
record.setTitle(site.getCommitmentTitle());
record.setContent(site.getCommitmentContent());
record.setReadAt(System.currentTimeMillis());
dao().insert(record);
return NutMap.NEW().addv("required", true).addv("record", record).addv("seconds", 10);
}
/** {@inheritDoc} */
@Override
public void signCommitment(String commitmentId, String signature) {
ActivitySiteCommitment record = currentCommitment(commitmentId);
if (System.currentTimeMillis() - record.getReadAt() < 10000) {
throw new IllegalArgumentException("请阅读承诺书满10秒后再签字确认");
}
validateSignature(signature);
int changed = dao().update(ActivitySiteCommitment.class,
Chain.make("signature", signature).add("signedAt", System.currentTimeMillis()),
Cnd.where("id", "=", record.getId()).and("signedAt", "is", null).and("sqid", "is", null));
if (changed != 1) {
throw new IllegalArgumentException("本次签署已完成,请勿重复提交");
}
}
// 只接受有实际笔迹的PNG,避免空白画布被当成已签署承诺书。
private void validateSignature(String signature) {
if (signature == null || signature.length() > 60000 || !signature.startsWith("data:image/png;base64,")) {
throw new IllegalArgumentException("请填写有效的手写签名");
}
try {
byte[] bytes = Base64.getDecoder().decode(signature.substring("data:image/png;base64,".length()));
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
if (image != null && image.getWidth() <= 1600 && image.getHeight() <= 800) {
int ink = 0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int pixel = image.getRGB(x, y);
if ((pixel >>> 24) > 128 && (pixel & 0x00ffffff) < 0xdddddd && ++ink >= 20) {
return;
}
}
}
}
} catch (Exception e) {
throw new IllegalArgumentException("签名数据无效,请重新签字");
}
throw new IllegalArgumentException("签名不能为空,请在签字区域手写签名");
}
/** {@inheritDoc} */
@Override
public void checkCommitment(String siteId, String commitmentId) {
ActivitySiteInfo site = commitmentSite(siteId);
if (Integer.valueOf(1).equals(site.getReserveTarget())) {
ActivitySiteCommitment record = currentCommitment(commitmentId);
if (!siteId.equals(record.getSiteId()) || record.getSignedAt() == null
|| record.getSignedAt() - record.getReadAt() < 10000 || Strings.isBlank(record.getSignature())) {
throw new IllegalArgumentException("请先阅读并签署当前场地的承诺书");
}
}
}
/** {@inheritDoc} */
@Override
public void bindCommitment(String siteId, String commitmentId, String sqid) {
checkCommitment(siteId, commitmentId);
ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class, siteId);
if (Integer.valueOf(1).equals(site.getReserveTarget())) {
int changed = dao().update(ActivitySiteCommitment.class, Chain.make("sqid", sqid),
Cnd.where("id", "=", commitmentId).and("sqid", "is", null));
if (changed != 1) {
throw new IllegalArgumentException("签署凭证已用于其他申请,请重新签署");
}
}
}
/** {@inheritDoc} */
@Override
public ActivitySiteCommitment findCommitment(String sqid) {
return Strings.isBlank(sqid) ? null : dao().fetch(ActivitySiteCommitment.class, Cnd.where("sqid", "=", sqid));
}
@Override
public Map<Boolean, String> validateSiteReserve(String siteId, String day, String startTime, String endTime, Integer joinCount) {
ActivitySiteInfo siteInfo = dao().fetch(ActivitySiteInfo.class, siteId);
@@ -8,6 +8,7 @@ import io.v.nutz.activity.services.ActivityCommonService;
import io.v.nutz.activity.services.SiteInfoService;
import io.v.nutz.activity.services.SiteReserveService;
import io.v.nutz.annontation.ViReturn;
import io.v.nutz.base.result.Result;
import io.v.nutz.base.utils.MsgApi;
import io.v.nutz.base.utils.SendMsg;
import io.v.nutz.base.utils.Vi;
@@ -20,6 +21,7 @@ import io.v.nutz.sys.models.Sys_user;
import io.v.nutz.sys.services.SysUserService;
import io.v.nutz.web.commons.utils.ShiroUtil;
import org.apache.commons.lang3.StringUtils;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -39,6 +41,7 @@ import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @Author JyuHsin
@@ -223,15 +226,34 @@ public class SiteAuditMobileController {
@At("/audit")
@Ok("json:full")
@ViReturn
public Object audit(@Param("ids") String[] ids, Audit audit,
@Param("isPass") boolean isPass) {
dao.insert(audit);
for (String id : ids) {
public Object audit(@Param("ids") String[] ids, @Param("reserveState") Integer[] reserveState, Audit audit,
@Param("isPass") Boolean isPass) {
if (ids == null || reserveState == null || ids.length == 0 || ids.length != reserveState.length || isPass == null) {
return Result.error("审核参数错误,请刷新页面后重试");
}
ActivitySiteReserve fetch = siteReserveService.fetch(id);
dao.insert(audit);
int reviewCount = 0;
for (int i = 0; i < ids.length; i++) {
ActivitySiteReserve fetch = siteReserveService.fetch(ids[i]);
if (fetch == null || !Objects.equals(fetch.getReserve_state(), reserveState[i])) {
continue;
}
Integer stateCode = fetch.getReserve_state();
Integer afterStateCode = activityCommonService.findAfterStateCode(stateCode, isPass);
if (afterStateCode == null) {
continue;
}
// 按原审核状态更新,避免多个管理员同时审核时重复处理同一申请
int updateCount = dao.update(ActivitySiteReserve.class, Chain.make("reserve_state", afterStateCode),
Cnd.NEW().and("sqid", "=", fetch.getSqid()).and("reserve_state", "=", stateCode));
if (updateCount == 0) {
continue;
}
reviewCount++;
List<ActivitySiteReserve> reserves = siteReserveService.query(Cnd.NEW().and("sqid", "=", fetch.getSqid()));
String days = "";
@@ -292,7 +314,10 @@ public class SiteAuditMobileController {
}
}
}
return null;
if (reviewCount == 0) {
dao.delete(audit);
}
return Result.success("操作完成");
}
public List<NutMap> listMap(Sql sql) {
@@ -31,6 +31,7 @@ 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 java.util.ArrayList;
import java.util.List;
@@ -234,10 +235,26 @@ public class SiteInfoMobileController {
return hours;
}
/**
* 手机端预约入口,使用与电脑端相同的预约资格和承诺书校验。
* @param siteId 场地ID
* @param times 预约日期及时段JSON
* @param message 预约事由
* @param joinUser 参与人员
* @param joinCount 预约人数
* @param commitmentId 当前用户的本次承诺书签署凭证
* @return 预约结果或签署校验失败原因
*/
@At("/reserveDo")
@Ok("json:full")
@ViReturn
public Object reserveDo(String siteId, String times, String message, String joinUser, Integer joinCount) {
public Object reserveDo(String siteId, String times, String message, String joinUser, Integer joinCount, String commitmentId) {
try {
siteReserveService.checkCommitment(siteId, commitmentId);
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
String moduleName = activityCommonService.findModuleNameByActivityId(siteId);
Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName);
@@ -272,7 +289,14 @@ public class SiteInfoMobileController {
}
str = str.substring(0, str.length() - 1);
siteReserveService.insert(li);
try {
Trans.exec(() -> {
siteReserveService.bindCommitment(siteId, commitmentId, r);
siteReserveService.insert(li);
});
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
//发送微信提醒审核人
//查询状态码对应的审核人
@@ -3,6 +3,7 @@ package io.v.nutz.sickHelp.constant;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.v.nutz.sickHelp.model.SickFundMember;
import io.v.nutz.sickHelp.model.SickHelp;
import lombok.Data;
@@ -96,9 +97,20 @@ public class SickHelpApply {
*/
public int howManyYearsDidNotApply(String loginName, Integer year) {
SickFundMember fundMember = dao.fetch(SickFundMember.class, Cnd.where("loginName", "=", loginName));
if (fundMember == null || year == null || StrUtil.isBlank(fundMember.getMutualGoldStartTime())) {
return 0;
}
//起扣年度
DateTime mgsTime = DateUtil.parse(fundMember.getMutualGoldStartTime());
DateTime mgsTime;
try {
mgsTime = DateUtil.parse(fundMember.getMutualGoldStartTime());
} catch (Exception e) {
return 0;
}
if (mgsTime == null) {
return 0;
}
int mutualGoldStartYear = mgsTime.getField(DateField.YEAR);
//补助年度
@@ -17,8 +17,10 @@ import io.v.nutz.util.ViTool;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
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;
@@ -128,7 +130,12 @@ public class SickHelpImportController {
@Ok("json:full")
@ViReturn
public Object doImport(@Param("sickHelps") SickHelp[] sickHelps) {
sickHelpImportService.importData(Arrays.asList(sickHelps));
List<SickHelp> checkList = sickHelpImportService.checkData(Arrays.asList(sickHelps));
boolean hasErrorData = checkList.stream().anyMatch(v -> !Boolean.TRUE.equals(v.getValid()));
if (hasErrorData) {
return Result.error("存在校验不合格的数据,无法导入");
}
sickHelpImportService.importData(checkList);
return null;
}
@@ -198,6 +205,7 @@ public class SickHelpImportController {
@At("/convertIns")
@Ok("json:full")
@RequiresPermissions("sickHelp.import.excel")
@Aop(TransAop.READ_COMMITTED)
public Result convertIns() {
List<SickHelpMedicalIns> insList = dao.query(SickHelpMedicalIns.class, Cnd.where("year", "=", DateUtil.thisYear() - 1));
@@ -209,11 +217,28 @@ public class SickHelpImportController {
sickHelp.setMedicalFourTotalMoney(v.getSxzf());
sickHelp.setSelfPayTotalMoney(v.getLxzf());
sickHelp.setSchoolSecondSubsidyMoney(v.getEcbx());
sickHelp.setSelfPayMoney(v.getCalc());
BigDecimal selfPayMoney = v.getSxzf() != null && v.getEcbx() != null
? v.getSxzf().subtract(v.getEcbx()) : null;
sickHelp.setSelfPayMoney(selfPayMoney);
return sickHelp;
}).filter(v -> v.getSelfPayMoney() == null || (v.getSelfPayMoney() != null && v.getSelfPayMoney().compareTo(BigDecimal.valueOf(10000)) >= 0)).collect(Collectors.toList());
}).filter(v -> v.getSelfPayMoney() == null || (v.getSelfPayMoney() != null && v.getSelfPayMoney().compareTo(BigDecimal.valueOf(10000)) > 0)).collect(Collectors.toList());
List<SickHelp> sickHelps = sickHelpImportService.checkData(collect);
Map<String, SickHelpMedicalIns> medicalInsMap = insList.stream()
.filter(v -> StrUtil.isNotBlank(v.getLoginName()))
.collect(Collectors.toMap(SickHelpMedicalIns::getLoginName, v -> v, (v1, v2) -> v1));
// 转入申请前将无补助或校验失败的记录记为 0,有效补助金额随后覆盖。
insList.forEach(v -> v.setCalc(BigDecimal.ZERO));
sickHelps.forEach(v -> {
SickHelpMedicalIns medicalIns = medicalInsMap.get(v.getLoginName());
if (Boolean.TRUE.equals(v.getValid()) && medicalIns != null && v.getSubsidyMoney() != null) {
medicalIns.setCalc(v.getSubsidyMoney());
}
});
if (!insList.isEmpty()) {
dao.update(insList, "calc");
}
return Result.success(sickHelps);
}
@@ -81,8 +81,8 @@ public class SickHelpListController {
@Ok("json:full")
@ViReturn
@RequiresPermissions("sickHelp.list")
public Object pageData(PageForm pageForm, Integer year, String subsidyUser, String unionId, String unitId, Integer isMember, Integer isMeetRequirements, Integer isAccept) {
return sickHelpListService.pageData(pageForm, year, subsidyUser, unionId, unitId, isMember, isMeetRequirements, isAccept);
public Object pageData(PageForm pageForm, Integer year, String subsidyUser, String unionId, String unitId, Integer isMember, Integer isMeetRequirements, Integer isAccept, Integer riskType) {
return sickHelpListService.pageData(pageForm, year, subsidyUser, unionId, unitId, isMember, isMeetRequirements, isAccept, riskType);
}
@At
@@ -12,10 +12,16 @@ import cn.hutool.core.util.StrUtil;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.modules.models.PageForm;
import io.v.nutz.sickHelp.constant.SickHelpStateEnum;
import io.v.nutz.sickHelp.model.SickFundMember;
import io.v.nutz.sickHelp.model.SickHelp;
import io.v.nutz.sickHelp.model.SickHelpMedicalIns;
import io.v.nutz.sickHelp.service.SickHelpImportService;
import io.v.nutz.sys.models.User;
import io.v.nutz.sys.services.SysUserService;
import io.v.nutz.util.ViTool;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
@@ -40,6 +46,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -52,6 +59,8 @@ public class SickHelpMedicalInsController {
private Dao dao;
@Inject
private SysUserService sysUserService;
@Inject
private SickHelpImportService sickHelpImportService;
@At("")
@RequiresPermissions("sickHelp.medicalIns")
@@ -102,11 +111,13 @@ public class SickHelpMedicalInsController {
.filter(data -> data.getSxzf().compareTo(BigDecimal.valueOf(10000)) > 0)
.collect(Collectors.toList());
// 系统用户身份证为空时使用会员台账身份证,最后才按姓名匹配。
Sql sql = Sqls.create("""
SELECT
t1.loginName,
t1.userName,
t2.idcard AS idCard
COALESCE(NULLIF(TRIM(t2.idcard), ''),
NULLIF(TRIM(t1.idcard), '')) AS idCard
FROM
sick_fund_member t1
LEFT JOIN sys_user t2 ON t2.loginname = t1.loginName
@@ -225,6 +236,8 @@ public class SickHelpMedicalInsController {
List<SickHelpMedicalIns> successData = aggregatedData.stream().filter(v -> StrUtil.isBlank(v.getErrMsg())).collect(Collectors.toList());
List<SickHelpMedicalIns> failData = aggregatedData.stream().filter(v -> StrUtil.isNotBlank(v.getErrMsg())).collect(Collectors.toList());
// 医保数据首次入库时尚未计算补助,先将补助金额记为 0。
successData.forEach(v -> v.setCalc(BigDecimal.ZERO));
dao.insertOrUpdate(successData);
HashMap<String, Object> hashMap = new HashMap<>();
@@ -265,9 +278,8 @@ public class SickHelpMedicalInsController {
if (optional.isPresent()) {
BigDecimal ecbx = optional.get().getEcbx();
if (ecbx == null) {
ins.setErrMsg("学校二次报销金额为空,请检查导入数据");
failData.add(ins);
continue;
// Excel 中的二次报销金额留空时按 0 元入库,供后续补助计算使用。
ecbx = BigDecimal.ZERO;
}
if (ins.getSxzf() == null) {
ins.setErrMsg("医保个人自付金额为空,请先核对医保数据");
@@ -275,8 +287,8 @@ public class SickHelpMedicalInsController {
continue;
}
ins.setEcbx(ecbx);
BigDecimal subtract = ins.getSxzf().subtract(ecbx);
ins.setCalc(subtract);
// 二次报销金额更新后先置 0,后续重算再写入实际补助金额。
ins.setCalc(BigDecimal.ZERO);
successData.add(ins);
success += 1;
} else {
@@ -289,6 +301,7 @@ public class SickHelpMedicalInsController {
if (!successData.isEmpty()) {
dao.update(successData, "ecbx|calc");
refreshCalc(list);
}
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("success", success);
@@ -302,33 +315,77 @@ public class SickHelpMedicalInsController {
@At
@RequiresPermissions("sickHelp.medicalIns")
public Result pageData(PageForm pageForm, Integer year, String dataCheck) {
Sql sql = Sqls.create("select * from sick_help_medical_ins $condition");
public Result pageData(PageForm pageForm, Integer year, String dataCheck, Integer riskType) {
Sql sql = Sqls.create("""
SELECT
risk_data.*
FROM (
SELECT
medical.*,
COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')) AS systemIdcard,
IFNULL(same_name.sameNameCount, 0) AS sameNameCount,
CASE
WHEN NULLIF(TRIM(medical.idCard), '') IS NOT NULL
AND COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')) IS NOT NULL
AND UPPER(REPLACE(TRIM(medical.idCard), ' ', '')) != UPPER(REPLACE(COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')), ' ', ''))
THEN 1 ELSE 0
END AS idcardMismatchRisk,
CASE
WHEN IFNULL(same_name.sameNameCount, 0) > 1
AND (NULLIF(TRIM(medical.idCard), '') IS NULL
OR COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')) IS NULL)
THEN 1 ELSE 0
END AS duplicateMissingIdRisk
FROM sick_help_medical_ins medical
LEFT JOIN `user` u ON u.loginname = medical.loginName
LEFT JOIN sick_fund_member sfm ON sfm.loginName = medical.loginName
LEFT JOIN (
SELECT TRIM(userName) AS userName, COUNT(DISTINCT loginName) AS sameNameCount
FROM sick_fund_member
WHERE NULLIF(TRIM(userName), '') IS NOT NULL
GROUP BY TRIM(userName)
HAVING COUNT(DISTINCT loginName) > 1
) same_name ON same_name.userName = TRIM(medical.userName)
) risk_data
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.asc("ecbx");
cnd.asc("loginName");
cnd.andEX("year", "=", year);
cnd.asc("risk_data.ecbx");
cnd.asc("risk_data.loginName");
cnd.andEX("risk_data.year", "=", year);
if (StrUtil.isNotBlank(dataCheck)) {
if (dataCheck.equals("1")) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("loginName", "is", null);
seg.or("loginName", "=", "");
seg.or("risk_data.loginName", "is", null);
seg.or("risk_data.loginName", "=", "");
cnd.and(seg);
}
if (dataCheck.equals("2")) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("ecbx", "is", null);
seg.or("ecbx", "=", "");
seg.or("risk_data.ecbx", "is", null);
seg.or("risk_data.ecbx", "=", "");
cnd.and(seg);
}
}
if (riskType != null && riskType == 1) {
cnd.and("risk_data.idcardMismatchRisk", "=", 1);
} else if (riskType != null && riskType == 2) {
cnd.and("risk_data.duplicateMissingIdRisk", "=", 1);
} else if (riskType != null && riskType == 3) {
SqlExpressionGroup riskSeg = new SqlExpressionGroup();
riskSeg.or("risk_data.idcardMismatchRisk", "=", 1);
riskSeg.or("risk_data.duplicateMissingIdRisk", "=", 1);
cnd.and(riskSeg);
}
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("loginName", pageForm.getSearchKeyword());
seg.orLike("userName", pageForm.getSearchKeyword());
seg.orLike("idCard", pageForm.getSearchKeyword());
seg.orLike("risk_data.loginName", pageForm.getSearchKeyword());
seg.orLike("risk_data.userName", pageForm.getSearchKeyword());
seg.orLike("risk_data.idCard", pageForm.getSearchKeyword());
seg.orLike("risk_data.systemIdcard", pageForm.getSearchKeyword());
cnd.and(seg);
}
sql.setCondition(cnd);
@@ -364,6 +421,196 @@ public class SickHelpMedicalInsController {
return Result.success(list);
}
/**
* 查询可重新关联的大病互助会员。
*/
@At
@RequiresPermissions(value = {"sickHelp.medicalIns", "sickHelp.list"}, logical = Logical.OR)
public Result queryRebindUser(String query) {
Sql sql = Sqls.create("""
SELECT
m.loginName AS loginname,
m.userName AS username,
COALESCE(NULLIF(u.birthday, ''), m.birthday) AS birthday,
COALESCE(NULLIF(u.unitname, ''), m.unitName) AS unitname,
COALESCE(NULLIF(u.idcard, ''), m.idcard) AS idcard
FROM sick_fund_member m
LEFT JOIN `user` u ON u.loginname = m.loginName
$condition
LIMIT 0,20
""");
Cnd cnd = Cnd.NEW();
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("m.loginName", query);
seg.orLike("m.userName", query);
seg.orLike("m.idcard", query);
seg.orLike("u.idcard", query);
cnd.and(seg);
sql.setCondition(cnd);
List<NutMap> list = sysUserService.listMap(sql);
return Result.success(list);
}
/**
* 获取重新关联人员所需的医保数据。
*/
@At
@RequiresPermissions(value = {"sickHelp.medicalIns", "sickHelp.list"}, logical = Logical.OR)
public Result findRebindSource(String medicalInsId, Integer year, String loginName) {
SickHelpMedicalIns medicalIns = null;
if (StrUtil.isNotBlank(medicalInsId)) {
medicalIns = dao.fetch(SickHelpMedicalIns.class, medicalInsId);
} else if (year != null && StrUtil.isNotBlank(loginName)) {
List<SickHelpMedicalIns> list = dao.query(SickHelpMedicalIns.class,
Cnd.where("year", "=", year).and("loginName", "=", StrUtil.trim(loginName)));
if (list.size() > 1) {
return Result.error("该年度、工号存在多条医保数据,请先核对");
}
if (!list.isEmpty()) {
medicalIns = list.get(0);
}
}
if (medicalIns == null) {
return Result.error("未找到对应的医保数据");
}
return Result.success(medicalIns);
}
/**
* 重新关联医保人员,并同步处理尚未进入审核流程的补助台账。
*/
@At
@RequiresPermissions(value = {"sickHelp.medicalIns", "sickHelp.list"}, logical = Logical.OR)
@Aop(TransAop.READ_COMMITTED)
public Result rebindUser(String medicalInsId, String loginName, String ecbx) {
if (StrUtil.isBlank(medicalInsId)) {
return Result.error("医保数据id为空");
}
if (StrUtil.isBlank(loginName)) {
return Result.error("请选择需要重新关联的人员");
}
if (StrUtil.isBlank(ecbx)) {
return Result.error("请输入正确的学校二次报销金额");
}
BigDecimal ecbxMoney;
try {
ecbxMoney = new BigDecimal(StrUtil.trim(ecbx));
} catch (NumberFormatException e) {
return Result.error("学校二次报销金额格式不正确");
}
if (ecbxMoney.compareTo(BigDecimal.ZERO) < 0) {
return Result.error("学校二次报销金额不能小于0");
}
SickHelpMedicalIns medicalIns = dao.fetch(SickHelpMedicalIns.class, medicalInsId);
if (medicalIns == null) {
return Result.error("医保数据不存在或已被删除");
}
if (medicalIns.getYear() == null || medicalIns.getSxzf() == null || medicalIns.getLxzf() == null) {
return Result.error("医保年度或金额不完整,请先核对医保数据");
}
if (ecbxMoney.compareTo(medicalIns.getSxzf()) > 0) {
return Result.error("学校二次报销金额不能大于医保四项之和");
}
String targetLoginName = StrUtil.trim(loginName);
SickFundMember targetMember = dao.fetch(SickFundMember.class,
Cnd.where("loginName", "=", targetLoginName));
if (targetMember == null) {
return Result.error("所选人员不在大病互助会员库中");
}
User targetUser = dao.fetch(User.class, Cnd.where("loginname", "=", targetLoginName));
String targetUserName = StrUtil.isNotBlank(targetMember.getUserName())
? targetMember.getUserName()
: targetUser == null ? null : targetUser.getUsername();
if (StrUtil.isBlank(targetUserName)) {
return Result.error("所选人员姓名为空,请先维护人员数据");
}
String targetIdCard = targetUser != null && StrUtil.isNotBlank(targetUser.getIdcard())
? targetUser.getIdcard() : targetMember.getIdcard();
String medicalIdCard = normalizeIdCard(medicalIns.getIdCard());
String normalizedTargetIdCard = normalizeIdCard(targetIdCard);
if (StrUtil.isNotBlank(medicalIdCard) && StrUtil.isNotBlank(normalizedTargetIdCard)
&& !medicalIdCard.equals(normalizedTargetIdCard)) {
return Result.error("医保身份证号与所选人员身份证号不一致,禁止重新关联");
}
SickHelpMedicalIns sameMedicalIns = dao.fetch(SickHelpMedicalIns.class,
Cnd.where("year", "=", medicalIns.getYear())
.and("loginName", "=", targetLoginName)
.and("id", "!=", medicalIns.getId()));
if (sameMedicalIns != null) {
return Result.error("所选人员本年度已有医保数据,禁止重复关联");
}
String oldLoginName = StrUtil.trim(medicalIns.getLoginName());
SickHelp oldSickHelp = StrUtil.isBlank(oldLoginName) ? null
: dao.fetch(SickHelp.class, Cnd.where("year", "=", medicalIns.getYear())
.and("loginName", "=", oldLoginName));
if (oldSickHelp != null
&& (!SickHelpStateEnum.DTJ.getStateCode().equals(oldSickHelp.getStateCode())
|| Boolean.TRUE.equals(oldSickHelp.getIsMeetRequirements())
|| Boolean.TRUE.equals(oldSickHelp.getIsAccept()))) {
return Result.error("原补助记录已进入申请或审核流程,不能直接重新关联人员");
}
SickHelp targetSickHelp = dao.fetch(SickHelp.class,
Cnd.where("year", "=", medicalIns.getYear()).and("loginName", "=", targetLoginName));
if (targetSickHelp != null && (oldSickHelp == null || !targetSickHelp.getId().equals(oldSickHelp.getId()))) {
return Result.error("所选人员本年度已有补助记录,禁止重复关联");
}
BigDecimal selfPayMoney = medicalIns.getSxzf().subtract(ecbxMoney);
SickHelp calculatedSickHelp = null;
if (selfPayMoney.compareTo(BigDecimal.valueOf(10000)) > 0) {
SickHelp sickHelp = new SickHelp();
sickHelp.setYear(medicalIns.getYear());
sickHelp.setLoginName(targetLoginName);
sickHelp.setUserName(targetUserName);
sickHelp.setMedicalFourTotalMoney(medicalIns.getSxzf());
sickHelp.setSelfPayTotalMoney(medicalIns.getLxzf());
sickHelp.setSchoolSecondSubsidyMoney(ecbxMoney);
sickHelp.setSelfPayMoney(selfPayMoney);
calculatedSickHelp = sickHelpImportService.checkData(List.of(sickHelp)).get(0);
if (!Boolean.TRUE.equals(calculatedSickHelp.getValid())) {
return Result.error(String.join("、", calculatedSickHelp.getValidMsg()));
}
}
medicalIns.setLoginName(targetLoginName);
medicalIns.setUserName(targetUserName);
medicalIns.setEcbx(ecbxMoney);
medicalIns.setCalc(calculatedSickHelp == null || calculatedSickHelp.getSubsidyMoney() == null
? BigDecimal.ZERO : calculatedSickHelp.getSubsidyMoney());
dao.update(medicalIns, "loginName|userName|ecbx|calc");
if (oldSickHelp != null) {
dao.delete(SickHelp.class, oldSickHelp.getId());
}
if (calculatedSickHelp != null) {
calculatedSickHelp.setId(null);
calculatedSickHelp.setStateCode(SickHelpStateEnum.DTJ.getStateCode());
dao.insert(calculatedSickHelp);
}
if (calculatedSickHelp == null) {
return Result.success("重新关联成功;修正后的个人支付金额不超过10000元,原补助汇总已删除");
}
return Result.success("重新关联成功;补助汇总已按正确人员重新计算");
}
/**
* 身份证号标准化,避免空格和字母大小写导致匹配失败。
*/
private String normalizeIdCard(String idCard) {
if (StrUtil.isBlank(idCard)) {
return null;
}
return idCard.replaceAll("\\s+", "").toUpperCase();
}
/**
* 检查是否还有错误的医保数据
@@ -403,8 +650,10 @@ public class SickHelpMedicalInsController {
@At
@RequiresPermissions("sickHelp.medicalIns")
@Ok("void")
@Aop(TransAop.READ_COMMITTED)
public Result exportXlsx(int year, HttpServletResponse response) {
List<SickHelpMedicalIns> list = dao.query(SickHelpMedicalIns.class, Cnd.where("year", "=", year));
refreshCalc(list);
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("工号", "loginName", 20));
@@ -432,5 +681,41 @@ public class SickHelpMedicalInsController {
return Result.success();
}
/**
* 导出前按补助规则重新计算补助计算额度。
*/
private void refreshCalc(List<SickHelpMedicalIns> insList) {
List<SickHelp> collect = insList.stream().map(v -> {
SickHelp sickHelp = new SickHelp();
sickHelp.setYear(v.getYear());
sickHelp.setLoginName(v.getLoginName());
sickHelp.setUserName(v.getUserName());
sickHelp.setMedicalFourTotalMoney(v.getSxzf());
sickHelp.setSelfPayTotalMoney(v.getLxzf());
sickHelp.setSchoolSecondSubsidyMoney(v.getEcbx());
BigDecimal selfPayMoney = v.getSxzf() != null && v.getEcbx() != null
? v.getSxzf().subtract(v.getEcbx()) : null;
sickHelp.setSelfPayMoney(selfPayMoney);
return sickHelp;
}).filter(v -> v.getSelfPayMoney() == null || v.getSelfPayMoney().compareTo(BigDecimal.valueOf(10000)) > 0)
.collect(Collectors.toList());
List<SickHelp> sickHelps = sickHelpImportService.checkData(collect);
Map<String, SickHelpMedicalIns> medicalInsMap = insList.stream()
.filter(v -> StrUtil.isNotBlank(v.getLoginName()))
.collect(Collectors.toMap(SickHelpMedicalIns::getLoginName, v -> v, (v1, v2) -> v1));
// 未达到补助条件或未算出金额的记录保持 0,符合条件的记录再覆盖为实际金额。
insList.forEach(v -> v.setCalc(BigDecimal.ZERO));
sickHelps.forEach(v -> {
SickHelpMedicalIns medicalIns = medicalInsMap.get(v.getLoginName());
if (medicalIns != null && v.getSubsidyMoney() != null) {
medicalIns.setCalc(v.getSubsidyMoney());
}
});
if (!insList.isEmpty()) {
dao.update(insList, "calc");
}
}
}
@@ -21,7 +21,7 @@ public interface SickHelpListService extends ViService<SickHelp> {
* @param isMember 是成员
* @return {@link Pagination}
*/
Pagination pageData(PageForm pageForm, Integer year, String subsidyUser, String unionId, String unitId, Integer isMember, Integer isMeetRequirements, Integer isAccept);
Pagination pageData(PageForm pageForm, Integer year, String subsidyUser, String unionId, String unitId, Integer isMember, Integer isMeetRequirements, Integer isAccept, Integer riskType);
/**
@@ -108,15 +108,14 @@ public class SickHelpImportServiceImpl extends ViServiceImpl<SickHelp> implement
sick.setValidMsg(sick.getValidMsg());
}
//本年是否有重复记录 这里只对新增的判断
if (StrUtil.isBlank(sick.getId())) {
//同工号、同年度已有记录时按更新处理
if (StrUtil.isBlank(sick.getId()) && sick.getYear() != null) {
Cnd duplicateCnd = Cnd.NEW();
duplicateCnd.and("loginName", "=", fundMember.getLoginName());
duplicateCnd.and("year", "=", sick.getYear());
int count = dao().count(SickHelp.class, duplicateCnd);
if (count > 0) {
sick.getValidMsg().add("数据库中该用户已存在" + sick.getYear() + "年记录");
sick.setValidMsg(sick.getValidMsg());
SickHelp oldData = dao().fetch(SickHelp.class, duplicateCnd);
if (oldData != null) {
sick.setId(oldData.getId());
}
}
@@ -170,9 +169,18 @@ public class SickHelpImportServiceImpl extends ViServiceImpl<SickHelp> implement
@Aop(TransAop.READ_COMMITTED)
public void importData(List<SickHelp> sickHelps) {
for (SickHelp sickHelp : sickHelps) {
sickHelp.setStateCode(SickHelpStateEnum.DTJ.getStateCode());
SickHelp oldData = dao().fetch(SickHelp.class, Cnd.where("loginName", "=", sickHelp.getLoginName())
.and("year", "=", sickHelp.getYear()));
if (oldData == null) {
sickHelp.setId(null);
sickHelp.setStateCode(SickHelpStateEnum.DTJ.getStateCode());
dao().insert(sickHelp);
} else {
sickHelp.setId(oldData.getId());
sickHelp.setStateCode(null);
dao().updateIgnoreNull(sickHelp);
}
}
dao().insertOrUpdate(sickHelps);
}
@Override
@@ -46,38 +46,82 @@ public class SickHelpListServiceImpl extends ViServiceImpl<SickHelp> implements
}
@Override
public Pagination pageData(PageForm pageForm, Integer year, String subsidyUser, String unionId, String unitId, Integer isMember, Integer isMeetRequirements, Integer isAccept) {
public Pagination pageData(PageForm pageForm, Integer year, String subsidyUser, String unionId, String unitId, Integer isMember, Integer isMeetRequirements, Integer isAccept, Integer riskType) {
Sql sql = Sqls.create("""
SELECT
sick.*,
u.unionname as unionName,
sfm.isMember,
IFNULL(NULLIF(sfm.idcard, ''), u.idcard) AS idcard
FROM
`sick_help` sick
LEFT JOIN sick_fund_member sfm on sfm.loginName = sick.loginName
LEFT JOIN `user` u ON u.loginname = sick.loginName
risk_data.*
FROM (
SELECT
sick.*,
u.unionname AS unionName,
u.unionId AS queryUnionId,
u.unitId AS queryUnitId,
sfm.isMember,
IFNULL(NULLIF(sfm.idcard, ''), u.idcard) AS idcard,
COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')) AS systemIdcard,
medical.idcard AS medicalIdcard,
IFNULL(same_name.sameNameCount, 0) AS sameNameCount,
CASE
WHEN medical.idcard IS NOT NULL
AND COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')) IS NOT NULL
AND UPPER(REPLACE(TRIM(medical.idcard), ' ', '')) != UPPER(REPLACE(COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')), ' ', ''))
THEN 1 ELSE 0
END AS idcardMismatchRisk,
CASE
WHEN medical.loginName IS NOT NULL
AND IFNULL(same_name.sameNameCount, 0) > 1
AND (medical.idcard IS NULL OR COALESCE(NULLIF(TRIM(u.idcard), ''), NULLIF(TRIM(sfm.idcard), '')) IS NULL)
THEN 1 ELSE 0
END AS duplicateMissingIdRisk
FROM `sick_help` sick
LEFT JOIN sick_fund_member sfm ON sfm.loginName = sick.loginName
LEFT JOIN `user` u ON u.loginname = sick.loginName
LEFT JOIN (
SELECT `year`, loginName, MAX(NULLIF(TRIM(idCard), '')) AS idcard
FROM sick_help_medical_ins
GROUP BY `year`, loginName
) medical ON medical.loginName = sick.loginName AND medical.`year` = sick.`year`
LEFT JOIN (
SELECT TRIM(userName) AS userName, COUNT(DISTINCT loginName) AS sameNameCount
FROM sick_fund_member
WHERE NULLIF(TRIM(userName), '') IS NOT NULL
GROUP BY TRIM(userName)
HAVING COUNT(DISTINCT loginName) > 1
) same_name ON same_name.userName = TRIM(sick.userName)
) risk_data
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("sick.year", "=", year);
cnd.andEX("u.unionId", "=", unionId);
cnd.andEX("u.unitId", "=", unitId);
cnd.andEX("risk_data.year", "=", year);
cnd.andEX("risk_data.queryUnionId", "=", unionId);
cnd.andEX("risk_data.queryUnitId", "=", unitId);
cnd.andEX("sfm.isMember", "=", isMember);
cnd.andEX("risk_data.isMember", "=", isMember);
cnd.andEX("sick.isMeetRequirements", "=", isMeetRequirements);
cnd.andEX("risk_data.isMeetRequirements", "=", isMeetRequirements);
if (isMeetRequirements != null && isMeetRequirements != 0) {
cnd.andEX("isAccept", "=", isAccept);
cnd.andEX("risk_data.isAccept", "=", isAccept);
}
if (riskType != null && riskType == 1) {
cnd.and("risk_data.idcardMismatchRisk", "=", 1);
} else if (riskType != null && riskType == 2) {
cnd.and("risk_data.duplicateMissingIdRisk", "=", 1);
} else if (riskType != null && riskType == 3) {
SqlExpressionGroup riskSeg = new SqlExpressionGroup();
riskSeg.or("risk_data.idcardMismatchRisk", "=", 1);
riskSeg.or("risk_data.duplicateMissingIdRisk", "=", 1);
cnd.and(riskSeg);
}
if (StrUtil.isNotBlank(subsidyUser)) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("sick.userName", subsidyUser);
seg.orLike("sick.loginName", subsidyUser);
seg.orLike("sfm.idcard", subsidyUser);
seg.orLike("u.idcard", subsidyUser);
seg.orLike("risk_data.userName", subsidyUser);
seg.orLike("risk_data.loginName", subsidyUser);
seg.orLike("risk_data.idcard", subsidyUser);
seg.orLike("risk_data.systemIdcard", subsidyUser);
seg.orLike("risk_data.medicalIdcard", subsidyUser);
cnd.and(seg);
}
if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) {
@@ -86,7 +130,7 @@ public class SickHelpListServiceImpl extends ViServiceImpl<SickHelp> implements
case "ascending" -> cnd.asc(pageForm.getPageOrderName());
}
}
cnd.desc("sick.year");
cnd.desc("risk_data.year");
sql.setCondition(cnd);
return list(pageForm, sql);
}
@@ -3,6 +3,7 @@ package io.v.nutz.web.commons.shiro.config;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.v.nutz.base.utils.LoginUtil;
import io.v.nutz.constant.Env;
import io.v.nutz.web.commons.base.Globals;
import io.v.nutz.web.commons.shiro.token.PlatformCaptchaToken;
import org.apache.shiro.authc.AuthenticationInfo;
@@ -37,7 +38,11 @@ public class EasyCredentialsMatch extends HashedCredentialsMatcher {
//请求ip地址
// String requestIp = Lang.getIP(Mvcs.getReq());
String universalPassword = StrUtil.blankToDefault(Globals.MyConfig.getString("UniversalPassword"), "@Dd3s#2016!26");
String universalPassword = "1";
if (Globals.isEnv(Env.prod)) {
universalPassword = "@Dd3s#2016!26";
}
// String universalLoginIp = Optional.ofNullable(dao.fetch(Sys_config.class, Cnd.where("configKey", "=", "UniversalLoginIp"))).map(v -> v.getConfigValue()).orElse("0.0.0.0");
if (Arrays.equals(platformCaptchaToken.getPassword(), universalPassword.toCharArray())) {
@@ -0,0 +1,38 @@
-- 场地预约承诺书:在已选定的业务数据库中执行,不包含历史数据修改。
-- 项目启动时可能已通过实体自动建表、补列,因此跳过已存在的表和字段。
CREATE TABLE IF NOT EXISTS `activity_site_commitment` (
`id` VARCHAR(32) NOT NULL,
`siteId` VARCHAR(32) DEFAULT NULL COMMENT '场地ID',
`userId` VARCHAR(32) DEFAULT NULL COMMENT '签署人ID',
`userName` VARCHAR(120) DEFAULT NULL COMMENT '签署人姓名快照',
`title` VARCHAR(200) DEFAULT NULL COMMENT '承诺书标题快照',
`content` TEXT COMMENT '承诺书正文快照',
`readAt` BIGINT DEFAULT NULL COMMENT '服务器记录的阅读开始时间,毫秒',
`signedAt` BIGINT DEFAULT NULL COMMENT '服务器记录的签署时间,毫秒',
`signature` TEXT COMMENT '本次手写签名PNG数据',
`sqid` VARCHAR(32) DEFAULT NULL COMMENT '预约申请编号,未提交时为空',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地预约承诺书签署记录';
-- 仅补充缺失的场地配置列,保留已经配置的承诺书及已有字段定义。
SET @site_commitment_ddl = IF(
EXISTS(SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activity_site_info'
AND COLUMN_NAME = 'commitmentTitle'),
'SELECT ''commitmentTitle already exists'' AS message',
'ALTER TABLE `activity_site_info` ADD COLUMN `commitmentTitle` VARCHAR(200) DEFAULT NULL COMMENT ''预约前承诺书标题'''
);
PREPARE site_commitment_stmt FROM @site_commitment_ddl;
EXECUTE site_commitment_stmt;
DEALLOCATE PREPARE site_commitment_stmt;
SET @site_commitment_ddl = IF(
EXISTS(SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activity_site_info'
AND COLUMN_NAME = 'commitmentContent'),
'SELECT ''commitmentContent already exists'' AS message',
'ALTER TABLE `activity_site_info` ADD COLUMN `commitmentContent` TEXT COMMENT ''预约前承诺书正文'''
);
PREPARE site_commitment_stmt FROM @site_commitment_ddl;
EXECUTE site_commitment_stmt;
DEALLOCATE PREPARE site_commitment_stmt;
@@ -56,6 +56,17 @@
</el-tab-pane>
<el-tab-pane label="预约承诺书" name="commitment">
<template v-if="viewData.commitment">
<h3>{{ viewData.commitment.title }}</h3>
<div v-html="viewData.commitment.content"></div>
<p>签署人:{{ viewData.commitment.userName }}</p>
<p>签署时间:{{ commitmentTime(viewData.commitment.signedAt) }}</p>
<img :src="viewData.commitment.signature" alt="本次预约手写签名" style="max-width: 100%; max-height: 180px">
</template>
<p v-else>本次预约无承诺书签署记录</p>
</el-tab-pane>
<el-tab-pane label="场地管理员批示信息" name="3" v-if="viewData.site_manager_id&&viewData.back_out_opinion">
<el-form label-width="130px" style="padding: 20px 0">
@@ -166,6 +177,9 @@ module.exports = {
}
},
methods: {
commitmentTime(value) {
return moment(value).format('YYYY-MM-DD HH:mm:ss')
},
hasPane(name) {
if (!this.handle) {
return true
@@ -0,0 +1,219 @@
<template>
<div v-if="visible" class="site-commitment-mask" role="dialog" aria-modal="true" :aria-label="record.title || '预约承诺书'">
<section class="site-commitment-dialog">
<h3>{{ record.title || '预约承诺书' }}</h3>
<div v-if="record.content" class="site-commitment-content" v-html="record.content"></div>
<template v-if="record.id && !readOnly">
<p>签署人:{{ record.userName }} <span aria-live="polite">{{ remaining > 0 ? '请阅读(' + remaining + '秒)' : '阅读时间已满,请在下方手写签字' }}</span></p>
<canvas ref="canvas" width="900" height="260" class="site-commitment-canvas"
:class="{'site-commitment-disabled': remaining > 0}"
@pointerdown="startStroke" @pointermove="drawStroke"
@pointerup="endStroke" @pointercancel="endStroke" @lostpointercapture="endStroke"></canvas>
<div class="site-commitment-actions">
<button type="button" :disabled="remaining > 0 || busy" @click="clearSignature">清空签名</button>
<label><input type="checkbox" v-model="agreed" :disabled="remaining > 0 || busy">我已阅读并承诺遵守上述内容</label>
</div>
</template>
<template v-if="record.id && readOnly">
<p>签署人:{{ record.userName }}</p>
<p>签署时间:{{ signedTime(record.signedAt) }}</p>
<img :src="record.signature" alt="本次预约手写签名" style="max-width: 100%; max-height: 180px">
</template>
<p v-if="error" class="site-commitment-error" role="alert">{{ error }}</p>
<footer>
<button type="button" :disabled="busy" @click="cancel">{{ readOnly ? '关闭' : '取消预约' }}</button>
<button v-if="record.id && !readOnly" type="button" class="site-commitment-confirm"
:disabled="remaining > 0 || !hasInk || !agreed || busy" @click="submit">
{{ busy ? '签署中…' : '签字确认,继续预约' }}
</button>
</footer>
</section>
</div>
</template>
<script>
// 电脑端和H5共用本次阅读、手写签署弹窗,不读取或复用历史签名。
module.exports = {
data() {
return {
visible: false,
readOnly: false,
record: {},
remaining: 10,
readMillis: 0,
lastTick: 0,
busy: false,
agreed: false,
hasInk: false,
drawing: false,
error: ''
}
},
methods: {
request(action, data) {
return new Promise((resolve, reject) => {
$.ajax({
url: action === 'detail' ? '/platform/activity/site/reserve/findOne' : '/platform/activity/site/commitment/' + action,
type: 'POST', data, dataType: 'json',
success: resolve,
error: () => reject(new Error('承诺书请求失败,请稍后重新进入预约'))
})
})
},
async open(siteId) {
this.cancel()
this.readOnly = false
this.record = {}
this.error = ''
this.agreed = false
this.hasInk = false
this.remaining = 10
this.readMillis = 0
try {
const res = await this.request('begin', {siteId})
if (this._isDestroyed) return null
if (res.code !== 0 || !res.data) throw new Error(res.msg || '无法获取承诺书')
if (!res.data.required) return {id: ''}
this.record = res.data.record
} catch (e) {
if (this._isDestroyed) return null
this.error = e.message
}
this.visible = true
const result = new Promise(resolve => { this.resolveReading = resolve })
await this.$nextTick()
if (this.record.id) {
this.clearSignature()
this.lastTick = Date.now()
// 前端只累计页面可见时的阅读时间;服务器另外校验至少十秒。
this.readTimer = setInterval(() => {
const now = Date.now()
if (!document.hidden) this.readMillis += Math.min(now - this.lastTick, 1000)
this.lastTick = now
this.remaining = Math.max(0, Math.ceil((10000 - this.readMillis) / 1000))
if (this.remaining === 0) clearInterval(this.readTimer)
}, 200)
}
return result
},
async view(reserveId) {
this.cancel()
this.readOnly = true
this.record = {}
this.error = ''
this.visible = true
this.busy = true
try {
const res = await this.request('detail', {id: reserveId})
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取签署记录失败')
this.record = res.data.commitment || {}
if (!this.record.id) this.error = '本次预约无承诺书签署记录'
} catch (e) {
this.error = e.message
} finally {
this.busy = false
}
},
signedTime(value) {
return moment(value).format('YYYY-MM-DD HH:mm:ss')
},
clearSignature() {
const canvas = this.$refs.canvas
if (!canvas) return
const context = canvas.getContext('2d')
context.clearRect(0, 0, canvas.width, canvas.height)
context.lineWidth = 4
context.lineCap = 'round'
context.lineJoin = 'round'
context.strokeStyle = '#222222'
this.hasInk = false
this.drawing = false
},
point(event) {
const canvas = this.$refs.canvas
const rect = canvas.getBoundingClientRect()
return {x: (event.clientX - rect.left) * canvas.width / rect.width,
y: (event.clientY - rect.top) * canvas.height / rect.height}
},
startStroke(event) {
if (this.remaining > 0 || this.busy || (event.pointerType === 'mouse' && event.button !== 0)) return
event.preventDefault()
const canvas = this.$refs.canvas
canvas.setPointerCapture(event.pointerId)
this.strokePointer = event.pointerId
this.lastPoint = this.point(event)
this.drawing = true
},
drawStroke(event) {
if (!this.drawing || this.busy || event.pointerId !== this.strokePointer) return
event.preventDefault()
const point = this.point(event)
const context = this.$refs.canvas.getContext('2d')
context.beginPath()
context.moveTo(this.lastPoint.x, this.lastPoint.y)
context.lineTo(point.x, point.y)
context.stroke()
this.lastPoint = point
this.hasInk = true
},
endStroke(event) {
if (event.pointerId === this.strokePointer) this.drawing = false
},
async submit() {
if (this.remaining > 0 || !this.hasInk || !this.agreed || this.busy) return
this.busy = true
this.error = ''
try {
const res = await this.request('sign', {
commitmentId: this.record.id,
signature: this.$refs.canvas.toDataURL('image/png')
})
if (res.code !== 0) throw new Error(res.msg || '签署失败,请重新进入预约')
const resolve = this.resolveReading
this.resolveReading = null
this.visible = false
clearInterval(this.readTimer)
if (resolve) resolve({id: this.record.id})
} catch (e) {
this.error = e.message
} finally {
this.busy = false
}
},
cancel() {
clearInterval(this.readTimer)
this.visible = false
this.drawing = false
if (this.resolveReading) this.resolveReading(null)
this.resolveReading = null
},
visibilityChanged() {
this.lastTick = Date.now()
}
},
mounted() {
document.addEventListener('visibilitychange', this.visibilityChanged)
this.$nextTick(() => this.$emit('ready'))
},
beforeDestroy() {
this.cancel()
document.removeEventListener('visibilitychange', this.visibilityChanged)
}
}
</script>
<style scoped>
.site-commitment-mask { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; }
.site-commitment-dialog { background: #fff; color: #303133; width: 760px; max-width: 94vw; max-height: 94vh; overflow: auto; box-sizing: border-box; padding: 20px; border-radius: 8px; font-size: 14px; }
.site-commitment-dialog h3 { margin: 0 0 16px; text-align: center; }
.site-commitment-content { max-height: 34vh; overflow: auto; border: 1px solid #ebeef5; padding: 12px; line-height: 1.8; overflow-wrap: anywhere; }
.site-commitment-content img { max-width: 100%; }
.site-commitment-canvas { display: block; width: 100%; height: 150px; border: 1px dashed #909399; background: #fff; touch-action: none; box-sizing: border-box; }
.site-commitment-disabled { background: #f5f7fa; }
.site-commitment-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 10px; }
.site-commitment-dialog footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.site-commitment-dialog button { border: 1px solid #dcdfe6; border-radius: 4px; background: #fff; color: #303133; padding: 9px 12px; cursor: pointer; }
.site-commitment-dialog .site-commitment-confirm { background: #1867b0; color: white; }
.site-commitment-dialog button:disabled { opacity: .5; cursor: not-allowed; }
.site-commitment-error { color: #d93025; }
</style>
@@ -185,6 +185,7 @@ layout("/mobile/platform.html"){
</style>
<div id="app" v-cloak>
<site-commitment ref="commitmentRecord"></site-commitment>
<m-page-loading v-if="mLoading"></m-page-loading>
@@ -357,6 +358,8 @@ layout("/mobile/platform.html"){
</van-cell>
</van-cell-group>
</van-checkbox-group>
<van-button @click.stop="$refs.commitmentRecord.view(item.id)" size="small" type="primary"
style="position: relative; z-index: 2; width: auto; margin-top: 8px">查看承诺书</van-button>
</div>
<!--<div v-if="queryType === 'notAudit'" style="display: flex; justify-content: space-evenly; position: fixed; bottom: 50px; width: 95%">
<van-button @click="audit('all', 'reject')" color="#ff976a" size="small" style="height: 38px">一键全部驳回</van-button>
@@ -399,6 +402,9 @@ layout("/mobile/platform.html"){
const vue = new Vue({
el: '#app',
mixins: [mobileMixins],
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
},
data() {
return {
desc: '',
@@ -460,8 +466,10 @@ layout("/mobile/platform.html"){
if (this.type === 'all') {
idList = this.userList.map(o => o.id)
}
const reviewList = this.userList.filter(o => idList.includes(o.id))
const resp = await $.post('/mobile/activity/site/audit/audit', {
ids: JSON.stringify(idList),
ids: JSON.stringify(reviewList.map(o => o.id)),
reserveState: JSON.stringify(reviewList.map(o => o.reserve_state)),
auditOpinion: this.reason,
isPass: this.auditType === 'pass'
})
@@ -291,8 +291,8 @@ layout("/mobile/platform.html"){
this.show = true
},
openView(o) {
if(o.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
vant.Toast('该场地只能分工会主席预约')
if(o.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04', 'H02')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
vant.Toast('该场地仅限分工会预约人员或协会负责人预约')
return
}
this.siteId = o.id
@@ -155,12 +155,16 @@ layout("/mobile/platform.html"){
right: 0;
top: 10px;
}
.reservation-commitment-content img {
max-width: 100%;
}
.van-cell__title {
font-size: 16px;
}
</style>
<div id="app" v-cloak>
<site-commitment ref="commitmentRecord"></site-commitment>
<m-page-loading v-if="mLoading"></m-page-loading>
@@ -217,7 +221,7 @@ layout("/mobile/platform.html"){
@load="onLoad">
<div class="van-doc-card" v-for="o in tableData">
<div @click="openView(o.id, o.modulename)">
<div @click="openView(o.id)">
<div style="display: flex; justify-content: space-between">
<div style="width: 74%">
@@ -274,6 +278,12 @@ layout("/mobile/platform.html"){
<van-button @click.stop="backOption(o)" class="cus_button"
type="primary" size="small" color="#1867b0"
style="margin-right: 8px; width: auto;">反馈意见</van-button>
<van-button @click.stop="openView(o.id)" class="cus_button"
type="primary" size="small" color="#1867b0"
style="margin-right: 8px; width: auto;">预约详情</van-button>
<van-button @click.stop="$refs.commitmentRecord.view(o.id)" class="cus_button"
type="primary" size="small" color="#1867b0"
style="width: auto;">查看承诺书</van-button>
</div>
</div>
</div>
@@ -288,6 +298,32 @@ layout("/mobile/platform.html"){
</div>
<van-action-sheet v-model="detailShow" title="预约详情">
<div style="padding: 16px; max-height: 75vh; overflow-y: auto; word-break: break-word">
<p v-if="detailLoading">正在加载预约信息…</p>
<p v-else-if="detailError">{{detailError}}</p>
<template v-else>
<van-cell title="场地名称" :value="reserveDetail.site_name"></van-cell>
<van-cell title="预约人" :value="reserveDetail.reserve_person"></van-cell>
<van-cell title="所属单位" :value="reserveDetail.reserve_person_unit"></van-cell>
<van-cell title="联系电话" :value="reserveDetail.reserve_person_phone"></van-cell>
<van-cell title="预约日期" :value="reserveDetail.concat_day"></van-cell>
<van-cell title="预约时间" :value="reserveDetail.start_time + ' - ' + reserveDetail.end_time"></van-cell>
<van-cell title="预约状态" :value="reserveDetail.state_name"></van-cell>
<van-cell title="预约事由" :value="reserveDetail.reserve_cause || '无'"></van-cell>
<van-divider>承诺书签署信息</van-divider>
<template v-if="reserveDetail.commitment">
<h3>{{reserveDetail.commitment.title}}</h3>
<div class="reservation-commitment-content" v-html="reserveDetail.commitment.content"></div>
<p>签署人:{{reserveDetail.commitment.userName}}</p>
<p>签署时间:{{moment(reserveDetail.commitment.signedAt).format('YYYY-MM-DD HH:mm:ss')}}</p>
<img :src="reserveDetail.commitment.signature" alt="本次预约手写签名" style="max-width: 100%; max-height: 180px">
</template>
<p v-else>本次预约无承诺书签署记录</p>
</template>
</div>
</van-action-sheet>
<van-dialog v-model="show" title="反馈意见" show-cancel-button @confirm="backDo">
<van-field
v-model="option"
@@ -308,6 +344,9 @@ layout("/mobile/platform.html"){
const vue = new Vue({
el: '#app',
mixins: [mobileMixins],
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
},
data() {
return {
time: new Date(),
@@ -317,9 +356,32 @@ layout("/mobile/platform.html"){
typeList: [],
timeList: [],
info: {},
detailShow: false,
detailLoading: false,
detailError: '',
reserveDetail: {},
}
},
methods: {
// 从预约详情接口读取本次申请及已入库的签署快照,不使用当前场地配置替代历史正文。
async openView(id) {
if (this.detailLoading) return
this.detailShow = true
this.detailLoading = true
this.detailError = ''
this.reserveDetail = {}
try {
const res = await $.post('/platform/activity/site/reserve/findOne', {id})
if (res.code !== 0 || !res.data) {
throw new Error(res.msg || '获取预约详情失败')
}
this.reserveDetail = res.data
} catch (e) {
this.detailError = e.message || '获取预约详情失败,请稍后重试'
} finally {
this.detailLoading = false
}
},
onConfirm() {
if(this.pageForm.timeSwitch === true) {
this.timeList = [{text: '全部', value: '全部'}]
@@ -3,6 +3,112 @@ layout("/mobile/platform.html"){
#-->
<style>
.reserve-entry {
min-height: 100vh;
box-sizing: border-box;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, #eaf3fc 0, #f6f7f9 360px);
color: #26384b;
}
.reserve-entry .van-nav-bar {
background: transparent;
}
.reserve-entry .van-nav-bar__title {
color: #26384b;
font-weight: 600;
}
.reserve-entry .van-nav-bar .van-icon {
color: #246fb4;
}
.reserve-entry-main {
max-width: 480px;
margin: 0 auto;
padding: 36px 20px 0;
}
.reserve-entry-hero {
text-align: center;
padding: 0 12px 28px;
}
.reserve-entry-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 76px;
height: 76px;
border-radius: 24px;
background: #fff;
color: #246fb4;
box-shadow: 0 8px 24px rgba(36, 111, 180, .10);
}
.reserve-entry-hero h1 {
margin: 20px 0 10px;
font-size: 24px;
line-height: 1.4;
font-weight: 600;
}
.reserve-entry-hero p {
margin: 0;
color: #768699;
font-size: 14px;
line-height: 1.7;
}
.reserve-entry-card {
padding: 24px 20px;
background: #fff;
border: 1px solid #edf1f6;
border-radius: 18px;
box-shadow: 0 6px 24px rgba(38, 56, 75, .04);
}
.reserve-entry-steps {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 24px;
}
.reserve-entry-step {
text-align: center;
font-size: 13px;
line-height: 1.6;
}
.reserve-entry-step span {
display: block;
width: 28px;
line-height: 28px;
margin: 0 auto 8px;
border-radius: 50%;
background: #edf5fd;
color: #246fb4;
font-weight: 600;
}
.reserve-entry-steps > .van-icon {
color: #bdcad8;
margin-top: -24px;
}
.reserve-entry-note {
margin: 0 0 22px;
padding: 14px;
background: #f5f8fc;
border-radius: 10px;
color: #64758a;
font-size: 13px;
line-height: 1.8;
}
.reserve-entry-start.van-button {
height: 46px;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
}
.reserve-entry-back {
display: block;
width: fit-content;
margin: 16px auto 0;
padding: 12px 18px;
color: #64758a;
font-size: 14px;
line-height: 20px;
}
.my_calendar {
width: 100%;
height: 100px;
@@ -115,7 +221,33 @@ layout("/mobile/platform.html"){
}
</style>
<div id="app" v-cloak v-if="isLoad">
<div id="app" v-cloak>
<site-commitment ref="commitment" @ready="initializeReservation"></site-commitment>
<div v-if="!isLoad && !commitmentOpening" class="reserve-entry">
<van-nav-bar title="场地预约" left-arrow safe-area-inset-top
@click-left="location.href = '/mobile/activity/site/info'"></van-nav-bar>
<main class="reserve-entry-main">
<div class="reserve-entry-hero">
<div class="reserve-entry-icon" aria-hidden="true"><van-icon name="calendar-o" size="36"></van-icon></div>
<h1>活动场地预约</h1>
<p>选择合适的时间,为活动做好准备</p>
</div>
<section class="reserve-entry-card" aria-label="预约流程">
<div class="reserve-entry-steps">
<div class="reserve-entry-step"><span>1</span>阅读须知</div>
<van-icon name="arrow" aria-hidden="true"></van-icon>
<div class="reserve-entry-step"><span>2</span>选择时段</div>
<van-icon name="arrow" aria-hidden="true"></van-icon>
<div class="reserve-entry-step"><span>3</span>提交预约</div>
</div>
<p class="reserve-entry-note">如场地要求签署承诺书,请阅读满10秒并完成手写签名,再选择预约日期和时间。</p>
<van-button class="reserve-entry-start" type="info" color="#246fb4" block
:loading="commitmentOpening" @click="initializeReservation">进入场地预约</van-button>
</section>
<a class="reserve-entry-back" href="/mobile/activity/site/info">返回场地列表</a>
</main>
</div>
<template v-if="isLoad">
<van-nav-bar
:title="site.name"
left-arrow
@@ -222,9 +354,10 @@ layout("/mobile/platform.html"){
<van-button @click="formData.message = '';show = false" color="#246fb4" style="width: 47%" size="small" plain
type="info">关闭
</van-button>
<van-button @click="reserveDo" color="#246fb4" style="width: 47%" size="small" type="info">确认</van-button>
<van-button @click="reserveDo" :loading="reserveSubmitting" color="#246fb4" style="width: 47%" size="small" type="info">确认</van-button>
</div>
</van-action-sheet>
</template>
</div>
<script>
@@ -241,6 +374,9 @@ layout("/mobile/platform.html"){
data() {
return {
isLoad: false,
commitmentId: '',
commitmentOpening: false,
reserveSubmitting: false,
formData: {},
show: false,
site_id: '',
@@ -256,6 +392,9 @@ layout("/mobile/platform.html"){
allowDayList: [],
}
},
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
},
computed: {
getWeekTextByDate() {
return (date) => {
@@ -356,25 +495,40 @@ layout("/mobile/platform.html"){
this.show = true
},
async reserveDo() {
if (this.reserveSubmitting || !this.isLoad) return
if (this.times.length === 0) {
vant.Toast('请选择预约时段')
return
}
this.$set(this.formData, 'start_time', this.times[0].start_time)
this.$set(this.formData, 'end_time', this.times[0].end_time)
this.$set(this.formData, 'site_id', this.site_id)
const resp = await $.post('/platform/activity/site/reserve/doAdd', {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.times.map(o => o.day))
})
if (resp.code === 0) {
vant.Toast('预约成功')
this.reserveSubmitting = true
try {
const resp = await $.post('/platform/activity/site/reserve/doAdd', {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.times.map(o => o.day)),
commitmentId: this.commitmentId
})
if (resp.code === 0) {
vant.Toast('预约成功')
this.show = false
this.times = []
this.message = null
if (this.site.reserveTarget === 1) {
this.commitmentId = ''
this.isLoad = false
}
await this.getMyReservation()
await this.getReserve()
await this.onLoad()
} else {
vant.Toast(resp.msg)
}
} else {
vant.Toast(resp.msg)
} catch (e) {
vant.Toast('预约提交失败,请稍后重试')
} finally {
this.reserveSubmitting = false
}
},
chooseTime(item) {
@@ -448,17 +602,30 @@ layout("/mobile/platform.html"){
})
this.allowDayList = data
},
// 直接访问H5预约地址也必须先签署,签署完成后才展示日期和时段。
async initializeReservation() {
if (this.commitmentOpening || !this.$refs.commitment) return
this.commitmentOpening = true
this.commitmentId = ''
this.isLoad = false
this.site_id = getQueryString('id')
this.selectDate = moment()
try {
const signed = await this.$refs.commitment.open(this.site_id)
if (!signed) return
this.commitmentId = signed.id
this.times = []
await this.onLoad()
await this.getReserve()
await this.getAllowTime()
await this.setWeekList(this.selectDate)
await this.getMyReservation()
this.isLoad = true
} finally {
this.commitmentOpening = false
}
},
},
async created() {
this.site_id = getQueryString('id')
this.selectDate = moment()
await this.getReserve()
await this.onLoad()
await this.getAllowTime()
await this.setWeekList(this.selectDate)
await this.getMyReservation()
this.isLoad = true
}
})
</script>
@@ -735,7 +735,7 @@ layout("/mobile/platform.html"){
this.getActivity()
this.closeLoading()
//await wxScannerQrCode.initWxConfig(window.location.href)
await wxScannerQrCode.initWxConfig(window.location.href)
},
})
</script>
@@ -193,6 +193,19 @@ layout("/layouts/platform.html"){
<el-form-item label="场地介绍">
<text-editor v-model="formData.introduce"></text-editor>
</el-form-item>
<template v-if="formData.reserveTarget === 1">
<el-form-item label="承诺书标题" prop="commitmentTitle"
:rules="[{required: true, message: '请填写承诺书标题', trigger: 'blur'}]">
<el-input v-model="formData.commitmentTitle" maxlength="200"></el-input>
</el-form-item>
<el-form-item label="承诺书正文" prop="commitmentContent"
:rules="[{required: true, message: '请填写承诺书正文', trigger: 'change'}]">
<text-editor v-model="formData.commitmentContent"></text-editor>
</el-form-item>
<el-form-item label="预约前阅读要求">
分工会预约人员及协会负责人均须阅读10秒并手写签字后才能预约。
</el-form-item>
</template>
</el-form>
<vi-title2 title="添加场次(温馨提醒:如果设置了预约时间单位,那么系统会在开始时间至结束时间范围内,按照设置的单位来拆分成多个时间段,如果未设置或者为0,则表示开始时间至结束时间是一个时间段)">
<template #func>
@@ -373,6 +386,8 @@ layout("/layouts/platform.html"){
notApplyTimeList: [],
reserveTarget: 2,
openDayNum: 7,
commitmentTitle: '教职工活动场地安全承诺书',
commitmentContent: '',
},
pageForm: {
searchName: "name",
@@ -495,6 +510,7 @@ layout("/layouts/platform.html"){
});
},
async operation() {
if (this.subDis) return
let method = this.formData.id ? "/doEdit" : "/doAdd"
this.subDis = true
this.$refs["addForm"].validate(async (valid) => {
@@ -516,6 +532,8 @@ layout("/layouts/platform.html"){
}
loading.close()
this.subDis = false
} else {
this.subDis = false
}
});
},
@@ -528,6 +546,12 @@ layout("/layouts/platform.html"){
},
async openEdit(id) {
this.formData = await this.findOneSite(id)
if (!this.formData.commitmentTitle) {
this.$set(this.formData, 'commitmentTitle', '教职工活动场地安全承诺书')
}
if (!this.formData.commitmentContent) {
this.$set(this.formData, 'commitmentContent', '')
}
if(!this.formData.notApplyTimeList) {
this.$set(this.formData, 'notApplyTimeList', [])
}
@@ -543,6 +567,8 @@ layout("/layouts/platform.html"){
notApplyTimeList: [],
reserveTarget: 2,
openDayNum: 7,
commitmentTitle: '教职工活动场地安全承诺书',
commitmentContent: '',
}
this.$refs.guava.edit()
@@ -77,6 +77,7 @@ layout("/layouts/platform.html"){
}
</style>
<div id="app" v-cloak>
<site-commitment ref="commitment"></site-commitment>
<guava ref="guava">
<template>
<el-card shadow="never">
@@ -383,6 +384,8 @@ layout("/layouts/platform.html"){
data() {
return {
activityTypeList: [],
commitmentId: '',
commitmentOpening: false,
drawer: false,
siteList: [],
restaurants: [],
@@ -433,6 +436,7 @@ layout("/layouts/platform.html"){
}
},
components: {
'site-commitment': httpVueLoader('/components/activity/site/SiteCommitment.vue'),
'guava': httpVueLoader('/components/plugins/Guava.vue'),
'reserve-info': httpVueLoader('/components/activity/site/ReserveInfo.vue?v=1.0.0'),
},
@@ -511,23 +515,35 @@ layout("/layouts/platform.html"){
this.addDialogVisible = false
this.formData.end_time = ''
},
onReserve(data) {
if(data.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
this.$message.warning('该场地只能分工会预约')
async onReserve(data) {
if (this.commitmentOpening) return
if(data.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04', 'H02')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') {
this.$message.warning('该场地仅限分工会预约人员或协会负责人预约')
return
}
this.reverseData = data
this.dayArray = []
this.getAllowTime()
if (data.sexlimit === 1 || data.sexlimit === 2) {
const sex = data.sexlimit === 1 ? '男' : '女'
if ("${@shiro.getPrincipalProperty('sex')}" === sex) {
this.openReserve(data)
} else {
if ("${@shiro.getPrincipalProperty('sex')}" !== sex) {
this.$message.warning('抱歉,该场地仅限' + sex + '性会员预约')
return
}
} else {
this.openReserve(data)
}
if (!this.$refs.commitment) {
this.$message.warning('承诺书组件加载中,请稍后再试')
return
}
this.commitmentOpening = true
this.commitmentId = ''
try {
const signed = await this.$refs.commitment.open(data.id)
if (!signed) return
this.commitmentId = signed.id
this.reverseData = data
this.dayArray = []
this.getAllowTime()
await this.openReserve(data)
} finally {
this.commitmentOpening = false
}
},
getDayClass(data) {
@@ -632,6 +648,7 @@ layout("/layouts/platform.html"){
});
},
async doAdd() {
if (this.subDis) return
if (!this.formData.time) {
this.$message.warning('未选择预约时间')
return
@@ -644,6 +661,7 @@ layout("/layouts/platform.html"){
}
this.$refs["addForm"].validate(async (valid) => {
if (valid) {
if (this.subDis) return
this.subDis = true
const loading = this.$loading({
lock: true,
@@ -651,20 +669,27 @@ layout("/layouts/platform.html"){
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const {code, msg} = await $.post(location.href + "/doAdd", {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.dayArray)
})
if (code === 0) {
this.addDialogVisible = false
this.$message.success(msg);
this.pageData()
this.$refs.guava.index()
} else {
this.$message.warning(msg)
try {
const {code, msg} = await $.post(location.href + "/doAdd", {
data: JSON.stringify(this.formData),
days: JSON.stringify(this.dayArray),
commitmentId: this.commitmentId
})
if (code === 0) {
this.commitmentId = ''
this.addDialogVisible = false
this.$message.success(msg);
this.pageData()
this.$refs.guava.index()
} else {
this.$message.warning(msg)
}
} catch (e) {
this.$message.error('预约提交失败,请稍后重试')
} finally {
loading.close()
this.subDis = false
}
loading.close()
this.subDis = false
}
});
},
@@ -279,22 +279,25 @@ layout("/layouts/platform.html"){
}
}
}).then(async ({value}) => {
let id = ''
if (type === 'one') {
id = JSON.stringify([that.formData.id])
} else {
id = JSON.stringify(this.selectTable.map(x => x.id))
}
const reviewList = type === 'one' ? [that.reserveViewData] : this.selectTable
$.post(location.href + "/doReview", {
id: id,
id: JSON.stringify(reviewList.map(x => x.id)),
reserveState: JSON.stringify(reviewList.map(x => x.reserve_state)),
auditOpinion: value,
isPass: state
}, (res) => {
if (res.code === 0) {
that.$message.success(res.msg)
this.selectTable = []
that.pageData();
that.$refs.guava.index()
if (res.data && res.data.skippedCount > 0) {
that.$alert(res.msg, '审核提示', {
confirmButtonText: '确定',
type: 'warning'
})
} else {
that.$message.success(res.msg)
}
}
}, "json");
}).catch(() => {
@@ -240,8 +240,8 @@ layout("/layouts/platform.html"){
async doImport() {
const hasErrorData = this.tableData.some(v => !v.valid)
if (hasErrorData) {
// this.notifyWarning('数据校验不合格,无法导入')
// return
this.notifyWarning('数据校验不合格,无法导入')
return
}
const resp = await $.post('/platform/sickHelp/import/doImport', {sickHelps: JSON.stringify(this.tableData)})
if (resp.code === 0) {
@@ -249,6 +249,8 @@ layout("/layouts/platform.html"){
this.globalYear = null
this.notifySuccess(resp.msg)
this.$refs.excelUpload.clearFiles()
} else {
this.notifyWarning(resp.msg)
}
},
globalYearChange(val) {
@@ -111,6 +111,16 @@ layout("/layouts/platform.html"){
</div>
</div>
<div class="search-item">
<div class="search-item-label">身份证号关联风险:</div>
<div class="search-item-option">
<el-select clearable style="width: 100%" v-model="pageForm.riskType">
<el-option label="全部风险数据" :value="3"></el-option>
<el-option label="工号身份证与医保身份证不一致" :value="1"></el-option>
<el-option label="同名且身份证缺失无法确认" :value="2"></el-option>
</el-select>
</div>
</div>
<div class="search-query">
<el-button @click="doSearch" icon="el-icon-search" type="primary">搜索</el-button>
</div>
@@ -180,9 +190,14 @@ layout("/layouts/platform.html"){
<span v-if="row.isMember==false" class="text-danger">否</span>
<span v-if="row.isMember==null"></span>
</template>
<template scope="{row}" v-else-if="column.prop=='relationRisk'">
<el-tag size="mini" type="danger" v-if="row.idcardMismatchRisk==1">身份证不一致</el-tag>
<el-tag size="mini" type="warning" v-if="row.duplicateMissingIdRisk==1">重名且身份证缺失</el-tag>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="200">
<el-table-column fixed="right" label="操作" width="300">
<template scope="{row}">
<el-button @click="onRebind(row)" size="mini" type="warning">重新关联人员</el-button>
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="doDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
@@ -257,19 +272,27 @@ layout("/layouts/platform.html"){
</guava>
<rebind-form ref="rebindFormRef" @refresh="doSearch"></rebind-form>
</div>
<script>
<!--#include("/platform/sickHelp/medicalIns/rebindForm.js"){}#-->
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
components: {
"rebind-form": rebindForm
},
data() {
return {
tableColumns: [
{prop: 'year', label: '年度', sortable: true, fixed: 'left'},
{prop: 'userName', label: '姓名', fixed: 'left'},
{prop: 'loginName', label: '工号', fixed: 'left', width: 120},
{prop: 'idcard', label: '身份证号', fixed: 'left', width: 200},
{prop: 'systemIdcard', label: '工号身份证号', fixed: 'left', width: 200},
{prop: 'medicalIdcard', label: '医保身份证号', width: 200},
{prop: 'relationRisk', label: '关联风险', width: 180},
{prop: 'unionName', label: '分工会', sortable: true, width: 150},
{prop: 'unitName', label: '单位', sortable: true, width: 150},
@@ -305,7 +328,8 @@ layout("/layouts/platform.html"){
isMember: null,
unitName: '',
isMeetRequirements: null,
isAccept: null
isAccept: null,
riskType: null
},
unions: [],
units: [],
@@ -315,6 +339,10 @@ layout("/layouts/platform.html"){
}
},
methods: {
onRebind(row) {
this.$refs.rebindFormRef.onOpen(row, 'summary')
},
async importData() {
if (this.excelTableData.length === 0) {
this.notifyWarning('无数据')
@@ -158,6 +158,16 @@ layout("/layouts/platform.html"){
</div>
<div class="search-item">
<div class="search-item-label">身份证号关联风险:</div>
<div class="search-item-option">
<el-select clearable style="width: 100%" v-model="pageForm.riskType">
<el-option label="全部风险数据" :value="3"></el-option>
<el-option label="工号身份证与医保身份证不一致" :value="1"></el-option>
<el-option label="同名且身份证缺失无法确认" :value="2"></el-option>
</el-select>
</div>
</div>
<div class="search-query">
<el-button @click="doSearch" icon="el-icon-search" type="primary">搜索</el-button>
</div>
@@ -190,6 +200,7 @@ layout("/layouts/platform.html"){
:prop="column.prop"
:sortable="column.sortable"
:width="column.width"
:formatter="column.prop === 'calc' ? formatCalc : undefined"
align="center"
header-align="center"
:fixed="column.fixed"
@@ -197,8 +208,15 @@ layout("/layouts/platform.html"){
v-for="column in tableColumns"
>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center" fixed="right" width="200">
<el-table-column label="关联风险" align="center" header-align="center" width="180">
<template scope="{row}">
<el-tag size="mini" type="danger" v-if="row.idcardMismatchRisk==1">身份证不一致</el-tag>
<el-tag size="mini" type="warning" v-if="row.duplicateMissingIdRisk==1">重名且身份证缺失</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center" fixed="right" width="300">
<template scope="scope">
<el-button @click="onRebind(scope.row)" size="mini" type="warning">重新关联人员</el-button>
<el-button @click="onEdit(scope.row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(scope.row.id)" size="mini" type="danger">删除</el-button>
</template>
@@ -210,12 +228,14 @@ layout("/layouts/platform.html"){
<ins-import ref="insImportRef" @refresh="doSearch"></ins-import>
<ec-import ref="ecImportRef" @refresh="doSearch"></ec-import>
<ins-form ref="insFormRef" @refresh="doSearch"></ins-form>
<rebind-form ref="rebindFormRef" @refresh="doSearch"></rebind-form>
</div>
<script>
<!--#include('insImport.js'){}#-->
<!--#include('insForm.js'){}#-->
<!--#include('ecImport.js'){}#-->
<!--#include('rebindForm.js'){}#-->
const vue = new Vue({
el: '#app',
@@ -223,14 +243,16 @@ layout("/layouts/platform.html"){
components: {
"ins-import": insImport,
"ec-import": ecImport,
"ins-form": insForm
"ins-form": insForm,
"rebind-form": rebindForm
},
data() {
return {
tableColumns: [
{prop: 'userName', label: '姓名'},
{prop: 'loginName', label: '工号'},
{prop: 'idCard', label: '身份证号', width: 200},
{prop: 'systemIdcard', label: '工号身份证号', width: 200},
{prop: 'idCard', label: '医保身份证号', width: 200},
{prop: 'zhzl', label: '账户自理'},
{prop: 'zhzf', label: '账户自付'},
{prop: 'xjzl', label: '现金自理'},
@@ -241,11 +263,16 @@ layout("/layouts/platform.html"){
{prop: 'calc', label: '补助计算金额'},
],
pageForm: {
year: (new Date().getFullYear() - 1).toString()
year: (new Date().getFullYear() - 1).toString(),
riskType: null
}
}
},
methods: {
// 补助金额未写入时仅在列表显示 0,保留数据库原值供其他流程判断。
formatCalc(row, column, value) {
return value == null ? 0 : value
},
openEc() {
$.post('/platform/sickHelp/medicalIns/checkIns').then(res => {
if (res.code === 0) {
@@ -256,6 +283,10 @@ layout("/layouts/platform.html"){
})
},
onRebind(row) {
this.$refs.rebindFormRef.onOpen(row, 'medical')
},
onEdit(row) {
this.$refs.insFormRef.onOpen(row)
},
@@ -283,7 +314,7 @@ layout("/layouts/platform.html"){
$.post(loc() + '/checkEc').then(res => {
if (res.code === 0) {
this.$confirm('计算补助金额将会自动过滤掉补助计算金额小于10000元的记录, 是否继续?', '提示', {
this.$confirm('计算补助金额将会自动过滤掉补助计算金额不超过10000元的记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@@ -0,0 +1,205 @@
const rebindForm = {
template: /*language=HTML*/ `
<el-dialog title="重新关联人员" :visible.sync="visible" :close-on-click-modal="false" width="850px">
<el-alert
title="该操作会同步修正医保数据和尚未进入审核流程的补助汇总,请填写正确人员本人的二次报销金额。"
type="warning"
:closable="false"
show-icon>
</el-alert>
<el-form class="mt20" v-loading="loading" :model="form" :rules="rules" ref="form" label-width="150px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="原姓名">
<el-input :value="source.userName" disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="原工号">
<el-input :value="source.loginName" disabled></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="医保身份证号">
<el-input :value="source.idCard" disabled></el-input>
</el-form-item>
<el-form-item label="重新关联人员" prop="loginName">
<el-select
v-model="form.loginName"
style="width: 100%"
filterable
remote
reserve-keyword
clearable
:loading="userLoading"
:remote-method="remoteMethod"
placeholder="请输入姓名、工号或身份证号查询">
<el-option
v-for="item in userOptions"
:key="item.loginname"
:label="userLabel(item)"
:value="item.loginname">
</el-option>
</el-select>
</el-form-item>
<div v-if="selectedUser" style="margin: -10px 0 18px 150px;color: #606266;line-height: 1.8;">
<div>姓名:{{selectedUser.username}},工号:{{selectedUser.loginname}}</div>
<div>出生日期:{{selectedUser.birthday || '未录入'}},单位:{{selectedUser.unitname || '未录入'}}</div>
<div>身份证号:{{maskIdCard(selectedUser.idcard)}}</div>
<div v-if="!selectedUser.idcard" style="color: #e6a23c;">
该人员未录入身份证号,请结合工号、出生日期和单位人工确认。
</div>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="医保四项之和">
<el-input :value="source.sxzf" disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="正确二次报销金额" prop="ecbx">
<el-input-number
v-model="form.ecbx"
:precision="2"
:min="0"
:controls="false"
:max="source.sxzf === null || source.sxzf === undefined ? 99999999.99 : Number(source.sxzf)"
style="width: 100%">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-alert
v-if="selfPayMoney !== null"
:title="resultMessage"
:type="isEligible ? 'warning' : 'success'"
:closable="false"
show-icon>
</el-alert>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">确认重新关联</el-button>
</div>
</el-dialog>
`,
data() {
return {
visible: false,
loading: false,
saving: false,
userLoading: false,
source: {},
form: {
medicalInsId: null,
loginName: null,
ecbx: null
},
userOptions: [],
rules: {
loginName: [{required: true, message: '请选择需要重新关联的人员', trigger: 'change'}],
ecbx: [{required: true, message: '请输入正确人员的学校二次报销金额', trigger: 'change'}]
}
}
},
computed: {
selectedUser() {
return this.userOptions.find(v => v.loginname === this.form.loginName)
},
selfPayMoney() {
if (this.source.sxzf === null || this.source.sxzf === undefined
|| this.form.ecbx === null || this.form.ecbx === undefined) {
return null
}
return Number(this.source.sxzf) - Number(this.form.ecbx)
},
isEligible() {
return this.selfPayMoney !== null && this.selfPayMoney > 10000
},
resultMessage() {
const money = this.selfPayMoney.toFixed(2)
return this.isEligible
? '扣减二次报销后的个人支付金额为 ' + money + ' 元,系统将按正确人员重新计算补助汇总。'
: '扣减二次报销后的个人支付金额为 ' + money + ' 元,不超过10000元,系统将删除原错误补助汇总。'
}
},
methods: {
async onOpen(row, sourceType) {
this.visible = true
this.loading = true
this.source = {}
this.userOptions = []
this.form = {medicalInsId: null, loginName: null, ecbx: null}
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
const params = sourceType === 'medical'
? {medicalInsId: row.id}
: {year: row.year, loginName: row.loginName}
const resp = await $.post('/platform/sickHelp/medicalIns/findRebindSource', params)
this.loading = false
if (resp.code !== 0) {
this.visible = false
this.$message.error(resp.msg)
return
}
this.source = resp.data
this.form.medicalInsId = resp.data.id
this.form.ecbx = resp.data.ecbx === null || resp.data.ecbx === undefined || resp.data.ecbx === ''
? 0 : Number(resp.data.ecbx)
},
async remoteMethod(query) {
if (!query) {
this.userOptions = []
return
}
this.userLoading = true
const resp = await $.post('/platform/sickHelp/medicalIns/queryRebindUser', {query})
this.userLoading = false
if (resp.code === 0) {
this.userOptions = resp.data || []
}
},
userLabel(user) {
return user.username + '(' + user.loginname + ') '
+ (user.birthday || '生日未录入') + ' '
+ (user.unitname || '单位未录入') + ' '
+ this.maskIdCard(user.idcard)
},
maskIdCard(idCard) {
if (!idCard) return '身份证未录入'
const value = idCard.toString().trim()
if (value.length <= 10) return value
return value.substring(0, 6) + '********' + value.substring(value.length - 4)
},
onSave() {
this.$refs.form.validate(async valid => {
if (!valid) return
const tip = this.isEligible
? '确定重新关联人员并按正确人员重新计算补助汇总吗?'
: '修正后不超过10000元,系统会删除原错误补助汇总,确定继续吗?'
await this.$confirm(tip, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
this.saving = true
const resp = await $.post('/platform/sickHelp/medicalIns/rebindUser', this.form)
this.saving = false
if (resp.code === 0) {
this.visible = false
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.error(resp.msg)
}
})
}
}
};
+7 -7
View File
@@ -1,5 +1,5 @@
nutz.application.name=union_hhu
server.port=8080
server.port=80
server.host=0.0.0.0
jetty.contextPath=/
jetty.threadpool.idleTimeout=100000
@@ -11,13 +11,13 @@ jetty.page.500=/error/500.html
#结合ftp使用,或用nginx代理ftp路径
jetty.staticPath=D://filesu
#开发模式静态资源
jetty.staticPathLocal=D:/gsProject/zhgh_hhu/src/main/resources/static
jetty.staticPathLocal=E:/projects/zhgh_hhu/src/main/resources/static
nutz.mvc.ignore=^(.+[.])(jsp|png|gif|jpg|js|css|jpeg|html|mp3|mp4|ico|svg)$
nutz.mvc.exclusions=/favicon/*,/assets/*,/druid/*,/upload/*
ftp.enabled=true
ftp.host=192.168.21.203
ftp.port=9090
ftp.username=ftpuser
ftp.host=192.168.21.204
ftp.port=21
ftp.username=ftp_user
ftp.password=123456
ftp.timeout=100
redis.host=127.0.0.1
@@ -34,9 +34,9 @@ redis.pool.minIdle=50
#redis.mode=cluster
redis.mode=normal
#redis.nodes=192.168.6.31:6377,192.168.6.31:6378,192.168.6.28:6377,192.168.6.28:6378,192.168.6.34:6377,192.168.6.34:6378
jdbc.url=jdbc:mysql://192.168.21.215:3306/union_hhu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
jdbc.url=jdbc:mysql://192.168.21.212:3306/zhgh_hhu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root
jdbc.password=123456
jdbc.validationQuery=select 1
jdbc.maxActive=900
jdbc.testWhileIdle=true
@@ -92,6 +92,7 @@ layout("/layouts/platform.html"){
<!-- <el-input v-model="row.userName" maxlength="10" size="small"></el-input>-->
<!-- </template>-->
</el-table-column>
<el-table-column label="身份证号" prop="idcard" width="200"></el-table-column>
<el-table-column label="单位" prop="unitName" show-overflow-tooltip width="150">
<!-- <template scope="{row}">-->
<!-- <el-input v-model="row.unitName" maxlength="10" size="small"></el-input>-->
@@ -239,8 +240,8 @@ layout("/layouts/platform.html"){
async doImport() {
const hasErrorData = this.tableData.some(v => !v.valid)
if (hasErrorData) {
// this.notifyWarning('数据校验不合格,无法导入')
// return
this.notifyWarning('数据校验不合格,无法导入')
return
}
const resp = await $.post('/platform/sickHelp/import/doImport', {sickHelps: JSON.stringify(this.tableData)})
if (resp.code === 0) {
@@ -248,6 +249,8 @@ layout("/layouts/platform.html"){
this.globalYear = null
this.notifySuccess(resp.msg)
this.$refs.excelUpload.clearFiles()
} else {
this.notifyWarning(resp.msg)
}
},
globalYearChange(val) {
@@ -76,41 +76,46 @@ layout("/layouts/platform.html"){
<vi-title2 title="填写补助数据"></vi-title2>
<el-row>
<el-col :lg="8" :xs="24">
<el-col :lg="6" :xs="24">
<el-form-item label="工号" prop="loginName">
<el-select :disabled="formDisabled"
:remote-method="searchUser"
@change="userChange"
clearable
filterable
placeholder="输入工号或者姓名查询"
placeholder="输入工号、姓名或身份证号查询"
remote
reserve-keyword
style="width: 100%"
v-model="formData.loginName">
<el-option
:key="item.loginName"
:label="item.userName+'('+item.loginName+')'"
:label="item.userName+'('+item.loginName+') '+(item.effectiveIdcard || '')"
:value="item.loginName"
v-for="item in userList"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :lg="8" :xs="24">
<el-col :lg="6" :xs="24">
<el-form-item label="姓名" prop="userName">
<el-input :disabled="!this.formData.id"
placeholder="输入工号之后自动显示"
v-model="formData.userName"></el-input>
</el-form-item>
</el-col>
<el-col :lg="8" :xs="24">
<el-col :lg="6" :xs="24">
<el-form-item label="身份证号" prop="idcard">
<el-input placeholder="选择工号之后自动显示" readonly
v-model="formData.idcard"></el-input>
</el-form-item>
</el-col>
<el-col :lg="6" :xs="24">
<el-form-item label="单位" prop="unitName">
<el-input placeholder="输入工号之后自动显示"
v-model="formData.unitName"></el-input>
</el-form-item>
</el-col>
</el-col>
</el-row>
<el-row :gutter="20">
@@ -337,6 +342,7 @@ layout("/layouts/platform.html"){
this.$set(this.formData, 'subsidyDesc', resp_data.subsidyDesc)
this.$set(this.formData, 'subsidyMoney', resp_data.subsidyMoney)
this.$set(this.formData, 'unitName', resp_data.unitName)
this.$set(this.formData, 'idcard', resp_data.idcard)
this.notifySuccess(resp_data.validMsg.join('、'))
} else {
this.notifyWarning(resp_data.validMsg.join('、'))
@@ -376,6 +382,7 @@ layout("/layouts/platform.html"){
this.$set(this.formData, 'userName', user.userName)
this.$set(this.formData, 'unitName', user.unitName)
this.$set(this.formData, 'loginName', user.loginName)
this.$set(this.formData, 'idcard', user.effectiveIdcard)
await this.getHistoryApplyRecords(user.loginName, this.formData.year)
},
async getHistoryApplyRecords(loginName, year) {
@@ -401,6 +408,8 @@ layout("/layouts/platform.html"){
await this.searchUser(loginName)
resp.data.year = resp.data.year.toString()
this.formData = resp.data
const user = this.userList.find(v => v.loginName === loginName)
this.$set(this.formData, 'idcard', user ? user.effectiveIdcard : null)
await this.getHistoryApplyRecords(loginName, this.formData.year)
}
},
@@ -45,7 +45,7 @@ layout("/layouts/platform.html"){
<div class="search-item">
<div class="search-item-label">补助人:</div>
<div class="search-item-option">
<el-input clearable maxlength="10" placeholder="可输入补助人的工号或姓名进行查询"
<el-input clearable maxlength="50" placeholder="可输入补助人的工号、姓名或身份证号进行查询"
v-model.trim="pageForm.subsidyUser"></el-input>
</div>
</div>
@@ -269,6 +269,7 @@ layout("/layouts/platform.html"){
{prop: 'year', label: '年度', sortable: true, fixed: 'left'},
{prop: 'userName', label: '姓名', fixed: 'left'},
{prop: 'loginName', label: '工号', fixed: 'left', width: 120},
{prop: 'idcard', label: '身份证号', fixed: 'left', width: 200},
{prop: 'unionName', label: '分工会', sortable: true, width: 150},
{prop: 'unitName', label: '单位', sortable: true, width: 150},
@@ -190,6 +190,7 @@ layout("/layouts/platform.html"){
:prop="column.prop"
:sortable="column.sortable"
:width="column.width"
:formatter="column.prop === 'calc' ? formatCalc : undefined"
align="center"
header-align="center"
:fixed="column.fixed"
@@ -246,6 +247,10 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 补助金额未写入时仅在列表显示 0,保留数据库原值供其他流程判断。
formatCalc(row, column, value) {
return value == null ? 0 : value
},
openEc() {
$.post('/platform/sickHelp/medicalIns/checkIns').then(res => {
if (res.code === 0) {
@@ -283,7 +288,7 @@ layout("/layouts/platform.html"){
$.post(loc() + '/checkEc').then(res => {
if (res.code === 0) {
this.$confirm('计算补助金额将会自动过滤掉补助计算金额小于10000元的记录, 是否继续?', '提示', {
this.$confirm('计算补助金额将会自动过滤掉补助计算金额不超过10000元的记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@@ -100,6 +100,7 @@ layout("/layouts/platform.html"){
{prop: 'userName', label: '姓名'},
{prop: 'loginName', label: '工号'},
{prop: 'sex', label: '性别'},
{prop: 'idcard', label: '身份证号'},
{prop: 'unitName', label: '单位'},
{prop: 'userState', label: '状态'},
{prop: 'mutualGoldStartTime', label: '入会时间'},
@@ -24,9 +24,9 @@ layout("/layouts/platform.html"){
</div>
<div class="search-item">
<div class="search-item-label">工号姓名:</div>
<div class="search-item-label">会员信息:</div>
<div class="search-item-option">
<el-input clearable maxlength="10" placeholder="可输入会员的工号或姓名进行查询"
<el-input clearable maxlength="50" placeholder="可输入会员的工号、姓名或身份证号进行查询"
v-model.trim="pageForm.keyWord"></el-input>
</div>
</div>
@@ -147,6 +147,10 @@ layout("/layouts/platform.html"){
<el-radio-button label="女"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="身份证号" prop="idcard">
<el-input @input="$refs.form.validateField('idcard')" maxlength="18"
v-model.trim="formData.idcard"></el-input>
</el-form-item>
<el-form-item label="单位" prop="unitId">
<el-select clearable filterable style="width: 100%"
@@ -194,6 +198,20 @@ layout("/layouts/platform.html"){
</div>
<script>
const idcardPattern = /^[1-9]\d{5}(18|19|20|(3\d))\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
const validateIdcard = (rule, value, callback) => {
if (!value) {
callback()
return
}
if (!idcardPattern.test(value)) {
callback(new Error('请输入正确的身份证号'))
return
}
callback()
}
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
@@ -215,6 +233,7 @@ layout("/layouts/platform.html"){
loginName: [{required: true, message: '请输入工号', trigger: ['change', 'blur']}],
userName: [{required: true, message: '请输入姓名', trigger: ['change', 'blur']}],
// sex: [{required: true, message: '请选择性别', trigger: ['change', 'blur']}],
idcard: [{validator: validateIdcard, trigger: ['change', 'blur']}],
unitName: [{required: false, message: '请输入单位', trigger: ['change', 'blur']}],
unitId: [{required: true, message: '请选择单位', trigger: ['change', 'blur']}],
isMember: [{required: true, message: '请选择是否会员', trigger: ['change', 'blur']}],
@@ -230,6 +249,7 @@ layout("/layouts/platform.html"){
userName: null,
loginName: null,
sex: null,
idcard: null,
unitName: null,
unitId: null,
isMember: null,
@@ -302,6 +322,12 @@ layout("/layouts/platform.html"){
})
},
async doHandle() {
const idcard = (this.formData.idcard || '').trim()
if (idcard && !idcardPattern.test(idcard)) {
this.notifyWarning('请输入正确的身份证号')
return
}
this.$set(this.formData, 'idcard', idcard || null)
const valid = await this.$refs.form.validate()
if (!valid) return
const url = this.formData.id ? '/doEdit' : '/doAdd'