diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageManageController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageManageController.java index 16666db..56ef633 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageManageController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageManageController.java @@ -15,6 +15,7 @@ import com.budwk.app.sys.models.Sys_dict; import com.budwk.app.sys.services.SysDictService; import com.budwk.app.zhgh.staffmanage.childManage.models.ChildManage; import com.budwk.app.zhgh.staffmanage.childManage.service.ChildManageService; +import com.budwk.app.zhgh.staffmanage.childManage.service.OnlyChildFeeService; import com.budwk.app.zhgh.staffmanage.childManage.vo.ChildManagePageForm; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -50,6 +51,8 @@ public class ChildManageManageController { @Inject private ChildManageService childManageService; @Inject + private OnlyChildFeeService onlyChildFeeService; + @Inject private SysDictService sysDictService; @@ -125,6 +128,8 @@ public class ChildManageManageController { @SaCheckPermission(value = {"childManage.manage", "h5.childManage.manage"}, mode = SaMode.OR) @SLog(tag = "子女信息-我的填报", msg = "删除id: ${args[0]}") public Result doDelete(String id) { + onlyChildFeeService.lockChildInformation(); + if (onlyChildFeeService.isChildLinked(id)) return Result.error("子女资料已关联资格申请,不能删除"); childManageService.dao().delete(ChildManage.class, id); return Result.success(); } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageMineController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageMineController.java index 324d5c3..67d3092 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageMineController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageMineController.java @@ -10,6 +10,7 @@ import com.budwk.app.base.result.Result; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.staffmanage.childManage.models.ChildManage; import com.budwk.app.zhgh.staffmanage.childManage.service.ChildManageService; +import com.budwk.app.zhgh.staffmanage.childManage.service.OnlyChildFeeService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -32,6 +33,8 @@ public class ChildManageMineController { @Inject private ChildManageService childManageService; + @Inject + private OnlyChildFeeService onlyChildFeeService; @At("/index") @Ok("beetl:/platform/zhgh/staffmanage/childManage/mine/index.html") @@ -59,19 +62,33 @@ public class ChildManageMineController { cnd.andEX("YEAR(info.applyTime)", "=", year); // 使用 or 条件查询两个监护人字段 cnd.and(Cnd.exps("info.guardianUserId1", "=", SecurityUtil.getUserId()) - .or("info.guardianUserId2", "=", SecurityUtil.getUserId())); + .or("info.guardianUserId2", "=", SecurityUtil.getUserId()) + .or("info.guardianLoginName1", "=", currentLoginName()) + .or("info.guardianLoginName2", "=", currentLoginName())); sql.setCondition(cnd); Pagination pagination = childManageService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); return Result.success(pagination); } + private String currentLoginName() { + com.budwk.app.sys.views.View_user user = childManageService.dao().fetch(com.budwk.app.sys.views.View_user.class, + Cnd.where("id", "=", SecurityUtil.getUserId())); + return user == null ? SecurityUtil.getUserId() : user.getLoginname(); + } + @At @ApiOperation("删除子女信息") @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"childManage.mine", "h5.childManage.mine"}, mode = SaMode.OR) @SLog(tag = "子女信息-我的填报", msg = "删除id: ${args[0]}") public Result doDelete(String id) { + onlyChildFeeService.lockChildInformation(); + ChildManage child = childManageService.dao().fetch(ChildManage.class, id); + if (child == null || !(SecurityUtil.getUserId().equals(child.getGuardianUserId1()) + || SecurityUtil.getUserId().equals(child.getGuardianUserId2()) + || currentLoginName().equals(child.getGuardianLoginName1()) || currentLoginName().equals(child.getGuardianLoginName2()))) return Result.error("只能删除本人子女资料"); + if (onlyChildFeeService.isChildLinked(id)) return Result.error("子女资料已关联资格申请,不能删除"); childManageService.dao().delete(ChildManage.class, id); return Result.success(); } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageWriteController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageWriteController.java index 0aaed48..ad8e87d 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageWriteController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/ChildManageWriteController.java @@ -15,10 +15,13 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.staffmanage.childManage.models.ChildManage; import com.budwk.app.zhgh.staffmanage.childManage.service.ChildManageService; +import com.budwk.app.zhgh.staffmanage.childManage.service.OnlyChildFeeService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.nutz.dao.Cnd; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; @@ -30,6 +33,7 @@ import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; import java.util.Date; +import java.util.Objects; @IocBean @At("/platform/childManage/write") @@ -42,6 +46,8 @@ public class ChildManageWriteController { private Dao dao; @Inject private ChildManageService childManageService; + @Inject + private OnlyChildFeeService onlyChildFeeService; @At("/index") @@ -57,14 +63,88 @@ public class ChildManageWriteController { @At @ApiOperation("保存子女信息") + @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"childManage.write", "h5.childManage.write"}, mode = SaMode.OR) @SLog(tag = "子女信息管理-信息填写", msg = "保存子女信息") public Result save(@Param("data") ChildManage childManage) { - if(StrUtil.isBlank(childManage.getId())) childManage.setApplyTime(new Date()); + onlyChildFeeService.lockChildInformation(); + View_user current = dao.fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId())); + if (current == null) return Result.error("人员资料不存在"); + ChildManage old = StrUtil.isBlank(childManage.getId()) ? null : dao.fetch(ChildManage.class, childManage.getId()); + boolean manager = AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name()); + if (StrUtil.isNotBlank(childManage.getId()) && old == null) return Result.error("子女资料不存在"); + if (old != null && !manager && !current.getId().equals(old.getGuardianUserId1()) + && !current.getId().equals(old.getGuardianUserId2()) + && !current.getLoginname().equals(old.getGuardianLoginName1()) + && !current.getLoginname().equals(old.getGuardianLoginName2())) return Result.error("只能维护本人子女资料"); + if (old != null && onlyChildFeeService.isChildInformationLocked(old.getId())) { + if (criticalInformationChanged(old, childManage)) { + return Result.error("该子女已关联待核验、已核定或已停发的独生子女费资格,关键身份和监护关系不可直接修改"); + } + // 锁定记录仅更新日常资料,不重新带入人员身份,也不改写资格或清算快照。 + old.setChildrenGrade(childManage.getChildrenGrade()); + old.setChildrenYear(childManage.getChildrenYear()); + old.setChildrenCountry(childManage.getChildrenCountry()); + old.setChildrenCurrentSchool(childManage.getChildrenCurrentSchool()); + old.setChildrenMiddleScore(childManage.getChildrenMiddleScore()); + old.setChildrenSubjectCombination(childManage.getChildrenSubjectCombination()); + old.setChildrenHuKouAddress(childManage.getChildrenHuKouAddress()); + old.setGuardianMobile1(childManage.getGuardianMobile1()); + old.setGuardianMobile2(childManage.getGuardianMobile2()); + old.setGuardianUnitName2(childManage.getGuardianUnitName2()); + old.setNote(childManage.getNote()); + dao.update(old); + return Result.success(); + } + if (StrUtil.isBlank(childManage.getChildrenName()) || StrUtil.isBlank(childManage.getChildrenIdCard()) + || childManage.getChildrenBirthday() == null || StrUtil.isBlank(childManage.getChildrenSex())) return Result.error("请补全子女姓名、身份证、生日和性别"); + if (childManage.getChildrenBirthday().after(new Date())) return Result.error("出生日期不能晚于今天"); + // 普通填报恢复原监护人表单,监护关系在资格申请或关键资料更正时填写。 + if (dao.count(ChildManage.class, Cnd.where("childrenIdCard", "=", childManage.getChildrenIdCard()) + .andEX("id", "!=", childManage.getId())) > 0) return Result.error("该子女已登记,请维护已有资料"); + // 人员身份由系统带入;双职工共用子女资料,资格分别申请。 + View_user first = old == null ? current : dao.fetch(View_user.class, Cnd.where("loginname", "=", old.getGuardianLoginName1())); + if (first == null) return Result.error("第一监护人人员资料不存在"); + childManage.setGuardianUserId1(first.getId()); + childManage.setGuardianUserName1(first.getUsername()); + childManage.setGuardianLoginName1(first.getLoginname()); + childManage.setGuardianUnitId1(first.getUnitId()); + childManage.setGuardianUnitName1(first.getUnitName()); + childManage.setGuardianUnionId1(first.getUnionId()); + childManage.setGuardianUnionName1(first.getUnionName()); + if (old != null && !manager && !current.getId().equals(first.getId())) childManage.setGuardianLoginName2(current.getLoginname()); + childManage.setGuardianUserId2(null); + if (StrUtil.isNotBlank(childManage.getGuardianLoginName2())) { + View_user second = dao.fetch(View_user.class, Cnd.where("loginname", "=", childManage.getGuardianLoginName2())); + if (second == null) return Result.error("另一位监护人工号不存在,本校以外监护人请勿填写工号"); + if (second.getId().equals(first.getId())) return Result.error("两位监护人不能为同一教职工"); + childManage.setGuardianUserId2(second.getId()); + childManage.setGuardianUserName2(second.getUsername()); + childManage.setGuardianUnitId2(second.getUnitId()); + childManage.setGuardianUnitName2(second.getUnitName()); + childManage.setGuardianUnionId2(second.getUnionId()); + childManage.setGuardianUnionName2(second.getUnionName()); + } + childManage.setApplyTime(old == null ? new Date() : old.getApplyTime()); dao.insertOrUpdate(childManage); return Result.success(); } + /** + * 提交子女关键资料更正,核验通过前不写入原始记录。 + * @param childManage 拟更正资料 + * @param reason 更正原因和证明依据 + * @return 申请提交结果 + */ + @At @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"childManage.write", "h5.childManage.write"}, mode = SaMode.OR) + public Result requestChange(@Param("data") ChildManage childManage, String reason) { + try { + onlyChildFeeService.requestChildChange(childManage, reason); + return Result.success("更正申请已提交,核验通过前原资料保持不变"); + } catch (IllegalArgumentException e) { return Result.error(e.getMessage()); } + } + @At @ApiOperation("查询用户") @SaCheckPermission(value = {"childManage.write", "h5.childManage.write"}, mode = SaMode.OR) @@ -112,7 +192,6 @@ public class ChildManageWriteController { public Result getIsRepeatByIdCard(String idCard, String id) { int count = childManageService.dao().count(ChildManage.class, Cnd.where(ChildManage::getChildrenIdCard, "=", idCard) - .and("YEAR(applyTime)", "=", DateUtil.thisYear()) .andEX(ChildManage::getId, "!=", id)); @@ -122,7 +201,37 @@ public class ChildManageWriteController { @At @SaCheckLogin public Result findOne(String id) { - return Result.success(childManageService.dao().fetch(ChildManage.class, id)); + ChildManage child = childManageService.dao().fetch(ChildManage.class, id); + if (child != null) { + child.setQualificationLocked(onlyChildFeeService.isChildInformationLocked(id)); + child.setCorrectionHistory(onlyChildFeeService.childChanges().stream() + .filter(change -> id.equals(change.getStr("childId"))).collect(java.util.stream.Collectors.toList())); + } + return Result.success(child); + } + + // 比较原始记录而非客户端锁定标记;日期按自然日比较,避免前端日期序列化造成误判。 + private boolean criticalInformationChanged(ChildManage old, ChildManage value) { + return !sameText(old.getChildrenName(), value.getChildrenName()) + || !sameText(old.getChildrenIdCard(), value.getChildrenIdCard()) + || !sameBirthday(old.getChildrenBirthday(), value.getChildrenBirthday()) + || !sameText(old.getChildrenSex(), value.getChildrenSex()) + || !sameText(old.getGuardianUserId1(), value.getGuardianUserId1()) + || !sameText(old.getGuardianUserId2(), value.getGuardianUserId2()) + || !sameText(old.getGuardianUserName1(), value.getGuardianUserName1()) + || !sameText(old.getGuardianUserName2(), value.getGuardianUserName2()) + || !sameText(old.getGuardianLoginName1(), value.getGuardianLoginName1()) + || !sameText(old.getGuardianLoginName2(), value.getGuardianLoginName2()) + || !sameText(old.getGuardianRelationship1(), value.getGuardianRelationship1()) + || !sameText(old.getGuardianRelationship2(), value.getGuardianRelationship2()); + } + + private boolean sameText(String a, String b) { + return Objects.equals(StrUtil.nullToEmpty(a), StrUtil.nullToEmpty(b)); + } + + private boolean sameBirthday(Date a, Date b) { + return a == null || b == null ? a == b : DateUtil.formatDate(a).equals(DateUtil.formatDate(b)); } } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/OnlyChildFeeController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/OnlyChildFeeController.java new file mode 100644 index 0000000..ccc3efb --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/controller/OnlyChildFeeController.java @@ -0,0 +1,303 @@ +package com.budwk.app.zhgh.staffmanage.childManage.controller; + +import cn.dev33.satoken.annotation.*; +import cn.hutool.json.*; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.zhgh.staffmanage.childManage.service.OnlyChildFeeService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.mvc.annotation.*; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.util.function.Supplier; + +/** 子女信息管理中的独生子女费入口,复用既有角色并在服务端校验办理权限。 */ +@IocBean +@At("/platform/childManage/onlyChildFee") +@Ok("json:full") +public class OnlyChildFeeController { + @Inject private OnlyChildFeeService onlyChildFeeService; + + /** 展示独生子女费工作台。 */ + @SaCheckLogin + @At("/index") + @Ok("beetl:/platform/zhgh/staffmanage/childManage/onlyChildFee/index.html") + public void index() { } + + /** 展示资格登记独立页面,数据及办理权限由服务端按子菜单授权校验。 */ + @SaCheckLogin + @At("/qualification/index") + @Ok("beetl:/platform/zhgh/staffmanage/childManage/onlyChildFee/qualification.html") + public void qualificationPage() { } + + /** 展示年度清算独立页面,数据及办理权限由服务端按子菜单授权校验。 */ + @SaCheckLogin + @At("/settlement/index") + @Ok("beetl:/platform/zhgh/staffmanage/childManage/onlyChildFee/settlement.html") + public void settlementPage() { } + + /** 展示变动报表独立页面,数据及办理权限由服务端按子菜单授权校验。 */ + @SaCheckLogin + @At("/changes/index") + @Ok("beetl:/platform/zhgh/staffmanage/childManage/onlyChildFee/changes.html") + public void changesPage() { } + + /** 展示退休奖励独立页面,数据及办理权限由服务端按子菜单授权校验。 */ + @SaCheckLogin + @At("/retirement/index") + @Ok("beetl:/platform/zhgh/staffmanage/childManage/onlyChildFee/retirement.html") + public void retirementPage() { } + + /** 展示待遇台账独立页面,数据及办理权限由服务端按子菜单授权校验。 */ + @SaCheckLogin + @At("/ledger/index") + @Ok("beetl:/platform/zhgh/staffmanage/childManage/onlyChildFee/ledger.html") + public void ledgerPage() { } + + /** 获取人员资料及可见资格。@return 初始化数据 */ + @SaCheckLogin + @At @POST + public Result context() { return Result.success(onlyChildFeeService.context()); } + + private Result result(Supplier work) { + try { return Result.success(work.get()); } + catch (IllegalArgumentException e) { return Result.error(e.getMessage()); } + } + + /** + * 查询当前用户可见的资料更正申请。 + * @return 更正记录及审核结果 + */ + @SaCheckLogin @At @POST + public Result childChanges() { return Result.success(onlyChildFeeService.childChanges()); } + + /** + * 核验资料更正并保存生效月份和审核凭据。 + * @param data 更正申请ID、核验结论和依据 + * @return 办理结果 + */ + @SaCheckLogin @At @POST @Aop(TransAop.READ_COMMITTED) + public Result reviewChildChange(String data) { + return result(() -> { onlyChildFeeService.reviewChildChange(JSONUtil.parseObj(data)); return null; }); + } + + /** 保存或提交本人资格。@param data 申请内容 @param submit 是否提交 @return 保存结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result save(String data, boolean submit) { + return result(() -> onlyChildFeeService.save(JSONUtil.parseObj(data), submit)); + } + + /** 核定起发月份和证明。@param data 核验内容 @return 办理结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result review(String data) { + return result(() -> { onlyChildFeeService.review(JSONUtil.parseObj(data)); return null; }); + } + + /** 登记资格终止或实际停发。@param data 生效月份及依据 @return 办理结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result stop(String data) { + return result(() -> { onlyChildFeeService.stop(JSONUtil.parseObj(data)); return null; }); + } + + /** 登记工资实发。@param data 月份、实发金额及来源 @return 保存结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result payment(String data) { + return result(() -> { onlyChildFeeService.payment(JSONUtil.parseObj(data)); return null; }); + } + + /** + * 保存人员首次实际发放月份并同步重算各年度未确认草稿。 + * @param data 领取人、当前办理年度及首次实际发放月份 + * @return 保存结果;已确认批次或无办理权限时返回失败原因 + */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result paymentStart(String data) { + return result(() -> { onlyChildFeeService.paymentStart(JSONUtil.parseObj(data)); return null; }); + } + + /** 获取清算快照。@param year 清算年度 @return 快照和执行记录 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result settlement(int year) { + // 转为原生集合及空值,避免 Hutool JSONNull 被 Nutz 序列化为对象,导致前端金额计算出现 NaN。 + return result(() -> org.nutz.json.Json.fromJson(java.util.Map.class, onlyChildFeeService.settlement(year).toString())); + } + + /** 生成或确认清算批次。@param year 清算年度 @param confirm 是否确认 @return 办理结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result batch(int year, boolean confirm) { + return result(() -> { onlyChildFeeService.batch(year, confirm); return null; }); + } + + /** 登记实际扣回。@param data 归属月份和实际执行凭据 @return 保存结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result recover(String data) { + return result(() -> { onlyChildFeeService.recover(JSONUtil.parseObj(data)); return null; }); + } + + /** 查询原退休奖励申请。@return 申请、审批与支付状态 */ + @SaCheckLogin + @At @POST + public Result retirement() { return result(() -> onlyChildFeeService.retirement()); } + + /** 登记退休奖励发放。@param data 原申请及核实凭据 @return 保存结果 */ + @SaCheckLogin + @At @POST @Aop(TransAop.READ_COMMITTED) + public Result reward(String data) { + return result(() -> { onlyChildFeeService.reward(JSONUtil.parseObj(data)); return null; }); + } + + /** 按当前筛选导出原退休奖励申请与实际发放记录。 + * @param year 退休年度,可为空 + * @param unit 单位名称 + * @param keyword 姓名或工号 + * @param approval 审核状态 + * @param paidStatus 发放状态 + * @param response 下载响应 + */ + @SaCheckLogin + @At @Ok("void") + public void exportReward(String year, String unit, String keyword, String approval, String paidStatus, HttpServletResponse response) { + JSONArray list = onlyChildFeeService.retirement(); + Workbook book = new XSSFWorkbook(); Sheet sheet = book.createSheet("退休奖励名单"); + String[] labels = {"工号", "姓名", "单位", "退休日期", "奖励金额", "审核状态", "发放状态", "发放日期", "发放凭据", "历史核查依据"}; + Row header = sheet.createRow(0); + for (int i = 0; i < labels.length; i++) { header.createCell(i).setCellValue(labels[i]); sheet.setColumnWidth(i, 22 * 256); } + int index = 1; + for (int i = 0; i < list.size(); i++) { + JSONObject r = list.getJSONObject(i), payment = r.getJSONObject("payment"); + String date = r.getDate("retireTime") == null ? "" : new java.text.SimpleDateFormat("yyyy-MM-dd").format(r.getDate("retireTime")); + String state = r.getBool("approved") ? "已通过" : "未通过审批", paid = payment == null ? "未发放" : "已发放"; + if (year != null && !year.isEmpty() && !date.startsWith(year)) continue; + if (unit != null && !unit.isEmpty() && !unit.equals(r.getStr("unitName"))) continue; + if (keyword != null && !keyword.isEmpty() && !(r.getStr("userName") + r.getStr("loginName")).contains(keyword)) continue; + if (approval != null && !approval.isEmpty() && !approval.equals(state)) continue; + if (paidStatus != null && !paidStatus.isEmpty() && !paidStatus.equals(paid)) continue; + String[] values = {r.getStr("loginName"),r.getStr("userName"),r.getStr("unitName"),date,"3000.00",state,paid, + payment == null ? "" : payment.getStr("paidDate"),payment == null ? "" : payment.getStr("voucher"),payment == null ? "待核实" : payment.getStr("historyBasis")}; + Row row = sheet.createRow(index++); + for (int j = 0; j < values.length; j++) row.createCell(j).setCellValue(values[j]); + } + CommonDownloadUtil.download("退休奖励名单.xlsx", book, response); + } + + /** 导出当前筛选的完整变动表,工作簿只含一个工作表。 + * @param year 年度 + * @param unit 单位名称 + * @param keyword 姓名或工号 + * @param type 变动类型(停发或扣回) + * @param response 下载响应 + */ + @SaCheckLogin + @At @Ok("void") @Aop(TransAop.READ_COMMITTED) + public void export(int year, String unit, String keyword, String type, HttpServletResponse response) { + JSONObject b = onlyChildFeeService.settlement(year).getJSONObject("batch"); + if (b == null || "草稿".equals(b.getStr("status"))) throw new IllegalArgumentException("请先复核确认清算批次"); + Workbook book = new XSSFWorkbook(); Sheet sheet = book.createSheet("变动表"); + String[] labels = {"序号", "工号", "姓名", "单位", "调前标准", "调后标准", "扣发金额", "补发金额", "停发时间", "备注说明"}; + String periodStart = b.getStr("periodStart"), periodEnd = b.getStr("periodEnd"); + sheet.createRow(0).createCell(0).setCellValue((year - 1) + "年11月—" + year + "年10月独生子女父母奖励费变动通知单"); + sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, 9)); + Row recipient = sheet.createRow(1); + recipient.createCell(0).setCellValue("人力资源处:"); recipient.createCell(4).setCellValue("单位:元"); + sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(1, 1, 0, 3)); + sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(1, 1, 4, 9)); + Row header = sheet.createRow(2); + int[] widths = {6, 12, 12, 28, 8, 8, 10, 8, 24, 14}; + for (int i = 0; i < labels.length; i++) { + header.createCell(i).setCellValue(i >= 4 && i <= 7 ? labels[i].substring(0, 2) + "\n" + labels[i].substring(2) : labels[i]); + sheet.setColumnWidth(i, widths[i] * 256); + } + int index = 3; + BigDecimal[] totals = {BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO}; + JSONArray rows = b.getJSONArray("rows"); + for (int i = 0; i < rows.size(); i++) { + JSONObject r = rows.getJSONObject(i); + String end = r.getStr("endMonth", r.getStr("stopMonth")); + if (end.compareTo(r.getStr("stopMonth")) > 0) end = r.getStr("stopMonth"); + boolean stop = end.compareTo(periodStart) >= 0 && end.compareTo(periodEnd) <= 0; + BigDecimal deduction = noticeDeduction(r, periodStart, periodEnd); + if (!stop && deduction.signum() == 0) continue; + if (unit != null && !unit.isEmpty() && !unit.equals(r.getStr("unitName"))) continue; + if (keyword != null && !keyword.isEmpty() && !(r.getStr("userName") + r.getStr("loginName")).contains(keyword)) continue; + if ("停发".equals(type) && !stop || "扣回".equals(type) && deduction.signum() == 0) continue; + Row row = sheet.createRow(index++); + boolean stopped = end.compareTo(periodEnd) <= 0; + String note = r.getStr("stopBasis"); + if (note == null || note.isEmpty()) note = stopped ? "停发" : "扣发调整"; + String[] values = {String.valueOf(index - 3), r.getStr("loginName"), r.getStr("userName"), r.getStr("unitName"), "10", stopped ? "0" : "10", + deduction.toPlainString(), "0", stopped ? end.replace('-', '.') + "-" + year + ".10" : "—", note}; + for (int j = 0; j < values.length; j++) { + Cell cell = row.createCell(j); + if (j >= 4 && j <= 7) { + cell.setCellValue(Double.parseDouble(values[j])); + totals[j - 4] = totals[j - 4].add(new BigDecimal(values[j])); + } else cell.setCellValue(values[j]); + } + } + Row total = sheet.createRow(index); total.createCell(0).setCellValue("合计"); + sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(index, index, 0, 3)); + for (int i = 0; i < totals.length; i++) total.createCell(i + 4).setCellValue(totals[i].doubleValue()); + + // 通知单与页面使用同一纸质表样:黑色边框、金额合计、收件单位及独立签字栏。 + Font font = book.createFont(); font.setFontName("宋体"); font.setFontHeightInPoints((short) 12); + CellStyle bodyStyle = book.createCellStyle(); bodyStyle.setFont(font); + bodyStyle.setAlignment(HorizontalAlignment.CENTER); bodyStyle.setVerticalAlignment(VerticalAlignment.CENTER); bodyStyle.setWrapText(true); + bodyStyle.setBorderTop(BorderStyle.THIN); bodyStyle.setBorderBottom(BorderStyle.THIN); + bodyStyle.setBorderLeft(BorderStyle.THIN); bodyStyle.setBorderRight(BorderStyle.THIN); + CellStyle amountStyle = book.createCellStyle(); amountStyle.cloneStyleFrom(bodyStyle); + amountStyle.setDataFormat(book.createDataFormat().getFormat("0.00")); + Font boldFont = book.createFont(); boldFont.setFontName("宋体"); boldFont.setFontHeightInPoints((short) 12); boldFont.setBold(true); + CellStyle headerStyle = book.createCellStyle(); headerStyle.cloneStyleFrom(bodyStyle); headerStyle.setFont(boldFont); + Font titleFont = book.createFont(); titleFont.setFontName("宋体"); titleFont.setFontHeightInPoints((short) 18); titleFont.setBold(true); + CellStyle titleStyle = book.createCellStyle(); titleStyle.cloneStyleFrom(bodyStyle); titleStyle.setFont(titleFont); + CellStyle leftStyle = book.createCellStyle(); leftStyle.cloneStyleFrom(bodyStyle); leftStyle.setAlignment(HorizontalAlignment.LEFT); + CellStyle rightStyle = book.createCellStyle(); rightStyle.cloneStyleFrom(bodyStyle); rightStyle.setAlignment(HorizontalAlignment.RIGHT); + for (int i = 0; i <= index; i++) { + Row row = sheet.getRow(i); row.setHeightInPoints(i == 0 ? 36 : i == 2 ? 42 : 32); + for (int j = 0; j < labels.length; j++) { + Cell cell = row.getCell(j); if (cell == null) cell = row.createCell(j); + cell.setCellStyle(i == 0 ? titleStyle : i == 1 ? (j < 4 ? leftStyle : rightStyle) + : i == 2 ? headerStyle : j >= 4 && j <= 7 ? amountStyle : bodyStyle); + } + } + CellStyle signStyle = book.createCellStyle(); signStyle.setFont(font); signStyle.setVerticalAlignment(VerticalAlignment.CENTER); + Row signatures = sheet.createRow(index + 1); signatures.setHeightInPoints(54); + signatures.createCell(0).setCellValue("审核人:"); signatures.getCell(0).setCellStyle(signStyle); + signatures.createCell(4).setCellValue("经办人:"); signatures.getCell(4).setCellStyle(signStyle); + sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(index + 1, index + 1, 0, 3)); + sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(index + 1, index + 1, 4, 9)); + sheet.setFitToPage(true); sheet.setHorizontallyCenter(true); + sheet.getPrintSetup().setLandscape(true); sheet.getPrintSetup().setPaperSize(PrintSetup.A4_PAPERSIZE); + sheet.getPrintSetup().setFitWidth((short) 1); sheet.getPrintSetup().setFitHeight((short) 0); + book.setPrintArea(0, 0, 9, 0, index + 1); + CommonDownloadUtil.download(year + "年独生子女费变动表.xlsx", book, response); + } + + // 按确认快照逐月汇总多发额,已扣回金额不冲减通知单,避免结清后扣发额变成零。 + private BigDecimal noticeDeduction(JSONObject row, String periodStart, String periodEnd) { + JSONArray months = row.getJSONArray("months"); + if (months == null || months.isEmpty()) throw new IllegalArgumentException("清算明细不完整,无法导出通知单"); + BigDecimal total = BigDecimal.ZERO; + for (int i = 0; i < months.size(); i++) { + JSONObject month = months.getJSONObject(i); + String value = month.getStr("month"); + if (value.compareTo(periodStart) < 0 || value.compareTo(periodEnd) > 0) continue; + BigDecimal paid = month.getBigDecimal("paid"), due = month.getBigDecimal("due"); + if (paid == null || due == null) throw new IllegalArgumentException("清算实发数据不完整,无法导出通知单"); + total = total.add(paid.subtract(due).max(BigDecimal.ZERO)); + } + return total; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/ChildManage.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/ChildManage.java index 555c659..d4b9a89 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/ChildManage.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/ChildManage.java @@ -36,12 +36,23 @@ public class ChildManage extends BaseModel implements Serializable { @PrevInsert(uu32 = true) private String id; + // 由关联资格实时计算,仅用于页面展示,不作为数据库字段或保存依据。 + private boolean qualificationLocked; + + // 更正申请和审核结果仅随详情返回,不覆盖子女原始字段。 + private List correctionHistory; + @Column @ColDefine(type = ColType.VARCHAR,width = 32) @Comment("guardianUserId1") private String guardianUserId1; + @Column + @ColDefine(type = ColType.VARCHAR, width = 30) + @Comment("第一监护人与子女关系") + private String guardianRelationship1; + @Column @ColDefine(type = ColType.VARCHAR,width = 30) @Comment("监护人1(教工)姓名") @@ -82,6 +93,11 @@ public class ChildManage extends BaseModel implements Serializable { @Comment("guardianUserId2") private String guardianUserId2; + @Column + @ColDefine(type = ColType.VARCHAR, width = 30) + @Comment("第二监护人与子女关系") + private String guardianRelationship2; + @Column @ColDefine(type = ColType.VARCHAR,width = 30) @Comment("监护人2(教工)姓名") diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/OnlyChildFeeRecord.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/OnlyChildFeeRecord.java new file mode 100644 index 0000000..8956e6b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/models/OnlyChildFeeRecord.java @@ -0,0 +1,31 @@ +package com.budwk.app.zhgh.staffmanage.childManage.models; + +import lombok.Data; +import org.nutz.dao.entity.annotation.*; +import java.util.Date; + +/** 独生子女费业务凭据,按资格、月记录、清算快照和实际支付分别保存。 */ +@Data +@Table("only_child_fee_record") +public class OnlyChildFeeRecord { + @Name + @ColDefine(type = ColType.VARCHAR, width = 100) + private String id; + @Column + @ColDefine(type = ColType.VARCHAR, width = 20) + private String kind; + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + @Column + @ColDefine(type = ColType.VARCHAR, width = 10) + private String period; + @Column + @ColDefine(type = ColType.TEXT) + private String content; + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String operatorId; + @Column + private Date recordedAt; +} diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/service/OnlyChildFeeService.java b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/service/OnlyChildFeeService.java new file mode 100644 index 0000000..4bfba09 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/childManage/service/OnlyChildFeeService.java @@ -0,0 +1,704 @@ +package com.budwk.app.zhgh.staffmanage.childManage.service; + +import cn.hutool.json.*; +import com.budwk.app.sys.services.SysUserService; +import com.budwk.app.sys.models.Sys_menu; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.staffmanage.childManage.models.*; +import com.budwk.app.zhgh.staffbenefit.dsznfmtx.models.Dsznfmtx; +import com.budwk.app.flow.entity.ProcessInstance; +import org.nutz.dao.*; +import org.nutz.ioc.loader.annotation.*; +import java.math.BigDecimal; +import java.time.*; +import java.text.SimpleDateFormat; +import java.util.*; + +/** 月费核定、逐月清算和退休奖励登记;所有写操作由控制器事务统一保护。 */ +@IocBean +public class OnlyChildFeeService { + @Inject private Dao dao; + @Inject private SysUserService sysUserService; + private static final BigDecimal TEN = new BigDecimal("10.00"); + + // 页签与菜单使用同一授权来源,角色名称本身不再授予办理权限。 + private List tabs() { + Set permissions = new HashSet<>(); + for (Sys_menu menu : sysUserService.getMenus(SecurityUtil.getUserId())) { + permissions.add(menu.getPermission()); + } + List result = new ArrayList<>(); + for (String tab : Arrays.asList("qualification", "settlement", "changes", "retirement", "ledger")) { + if (permissions.contains("childManage.onlyChildFee." + tab)) result.add(tab); + } + return result; + } + + private void require(boolean valid, String message) { + if (!valid) throw new IllegalArgumentException(message); + } + + private void access(String... allowedTabs) { + List granted = tabs(); + require(Arrays.stream(allowedTabs).anyMatch(granted::contains), "未分配对应的独生子女费子菜单权限"); + } + + private void lock() { + dao.execute(Sqls.create("INSERT IGNORE INTO only_child_fee_record (id,kind,content) VALUES ('LOCK','LOCK','{}')")); + dao.execute(Sqls.create("SELECT id FROM only_child_fee_record WHERE id='LOCK' FOR UPDATE")); + } + + /** + * 子女保存、删除与资格办理共用事务锁,避免检查后资格状态发生变化。 + * 调用方必须处于数据库事务中。 + */ + public void lockChildInformation() { + lock(); + } + + /** + * 判断任一监护人关联的资格是否要求锁定子女关键资料。 + * + * @param childId 子女记录ID + * @return 存在待核验、已核定或已停发资格时返回true + */ + public boolean isChildInformationLocked(String childId) { + if (childId == null) return false; + return records("QUALIFICATION").stream().anyMatch(q -> childId.equals(q.getStr("childId")) + && Arrays.asList("待核验", "已核定", "已停发").contains(q.getStr("status"))); + } + + /** + * 判断子女是否已经被资格申请引用,保留申请与历史依据的关联。 + * + * @param childId 子女记录ID + * @return 任一监护人存在关联申请时返回true + */ + public boolean isChildLinked(String childId) { + return childId != null && records("QUALIFICATION").stream() + .anyMatch(q -> childId.equals(q.getStr("childId"))); + } + + private JSONObject childIdentity(ChildManage c) { + return new JSONObject().set("childrenName", c.getChildrenName()).set("childrenIdCard", c.getChildrenIdCard()) + .set("childrenBirthday", c.getChildrenBirthday() == null ? null : new SimpleDateFormat("yyyy-MM-dd").format(c.getChildrenBirthday())) + .set("childrenSex", c.getChildrenSex()).set("guardianUserId1", c.getGuardianUserId1()) + .set("guardianUserName1", c.getGuardianUserName1()).set("guardianLoginName1", c.getGuardianLoginName1()) + .set("guardianRelationship1", c.getGuardianRelationship1()).set("guardianUserId2", c.getGuardianUserId2()) + .set("guardianUserName2", c.getGuardianUserName2()).set("guardianLoginName2", c.getGuardianLoginName2()) + .set("guardianRelationship2", c.getGuardianRelationship2()); + } + + /** + * 提交关键资料更正申请,保存前后快照,审批前不修改原始资料。 + * + * @param proposed 拟更正的子女及监护人资料 + * @param reason 更正原因和证明依据说明 + */ + public void requestChildChange(ChildManage proposed, String reason) { + lock(); + ChildManage old = dao.fetch(ChildManage.class, proposed.getId()); + require(old != null && isChildInformationLocked(old.getId()), "该记录无需更正审批,请通过普通保存维护"); + String user = SecurityUtil.getUserId(); + View_user profile = dao.fetch(View_user.class, Cnd.where("id", "=", user)); + require(profile != null, "人员资料不存在"); + require(tabs().contains("ledger") || user.equals(old.getGuardianUserId1()) || user.equals(old.getGuardianUserId2()) + || profile.getLoginname().equals(old.getGuardianLoginName1()) || profile.getLoginname().equals(old.getGuardianLoginName2()), "只能申请更正本人子女资料"); + require(reason != null && !reason.trim().isEmpty(), "请填写更正原因及证明依据"); + for (JSONObject change : records("CHILD_CHANGE")) { + require(!old.getId().equals(change.getStr("childId")) || !"待核验".equals(change.getStr("status")), "该子女已有待核验的更正申请"); + } + JSONObject after = childIdentity(proposed); + for (String field : Arrays.asList("childrenName", "childrenIdCard", "childrenBirthday", "childrenSex", "guardianLoginName1", "guardianRelationship1")) text(after, field); + require(!LocalDate.parse(after.getStr("childrenBirthday")).isAfter(LocalDate.now()), "出生日期不能晚于今天"); + require(Arrays.asList("男", "女").contains(after.getStr("childrenSex")), "请选择有效性别"); + for (int i = 1; i <= 2; i++) { + String login = after.getStr("guardianLoginName" + i, ""); + if (!login.isEmpty()) { + View_user guardian = dao.fetch(View_user.class, Cnd.where("loginname", "=", login)); + require(guardian != null, "监护人工号不存在"); + after.set("guardianUserId" + i, guardian.getId()).set("guardianUserName" + i, guardian.getUsername()); + } else after.set("guardianUserId" + i, null); + if (!after.getStr("guardianUserName" + i, "").isEmpty()) text(after, "guardianRelationship" + i); + } + require(!Objects.equals(after.getStr("guardianUserId1"), after.getStr("guardianUserId2")), "两位监护人不能为同一教职工"); + JSONObject before = childIdentity(old); + require(!before.equals(after), "关键资料没有变化"); + String id = UUID.randomUUID().toString(); + JSONObject change = new JSONObject().set("id", id).set("childId", old.getId()).set("userId", user) + .set("applicant", profile.getUsername()).set("reason", reason).set("oldData", before).set("newData", after) + .set("status", "待核验").set("submittedAt", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + put(id, "CHILD_CHANGE", user, null, change); + } + + /** + * 查询更正申请的办理记录,教职工仅可查看本人提交的申请。 + * + * @return 更正前后资料、原因、核验结果和生效月份 + */ + public List childChanges() { + boolean reviewer = tabs().contains("ledger"); + List list = records("CHILD_CHANGE"); + list.removeIf(c -> !reviewer && !SecurityUtil.getUserId().equals(c.getStr("userId"))); + return list; + } + + /** + * 核验并批准或退回更正,按生效月保留待遇历史版本,不改写已确认清算快照。 + * + * @param input 申请ID、核验结论、审核依据和生效月份 + */ + public void reviewChildChange(JSONObject input) { + access("ledger"); lock(); + OnlyChildFeeRecord record = dao.fetch(OnlyChildFeeRecord.class, input.getStr("id")); + require(record != null && "CHILD_CHANGE".equals(record.getKind()), "更正申请不存在"); + JSONObject change = get(input.getStr("id")); + require(change != null && change.getJSONObject("oldData") != null && "待核验".equals(change.getStr("status")), "更正申请不存在或已处理"); + require(!SecurityUtil.getUserId().equals(change.getStr("userId")), "更正申请须由另一位授权经办人核验"); + text(input, "basis"); + String status = input.getStr("status"); + require(Arrays.asList("已批准", "退回").contains(status), "请选择批准或退回"); + if ("已批准".equals(status)) { + require(Boolean.TRUE.equals(input.getBool("verified")), "请核实更正证明及双方监护人的领取资格"); + YearMonth effective = month(input.getStr("effectiveMonth")); + require(!effective.isAfter(YearMonth.now()), "生效月份不能晚于当前月份"); + for (JSONObject b : records("BATCH")) { + require("草稿".equals(b.getStr("status")) || effective.isAfter(YearMonth.of(b.getInt("year"), 10)), "生效月份不能覆盖已确认清算期间"); + } + ChildManage child = dao.fetch(ChildManage.class, change.getStr("childId")); + require(child != null && childIdentity(child).equals(change.getJSONObject("oldData")), "原始关键资料已变化,请退回后重新申请"); + JSONObject after = change.getJSONObject("newData"); + require(dao.count(ChildManage.class, Cnd.where("childrenIdCard", "=", after.getStr("childrenIdCard")).and("id", "!=", child.getId())) == 0, "更正后的身份证号已被其他子女记录使用"); + List affected = new ArrayList<>(); + for (JSONObject q : records("QUALIFICATION")) { + if (!child.getId().equals(q.getStr("childId"))) continue; + JSONArray history = q.getJSONArray("benefitHistory"); + if (history == null) history = new JSONArray(); + if (!history.isEmpty()) require(!effective.isBefore(month(history.getJSONObject(history.size() - 1).getStr("untilMonth"))), "生效月份不能早于上次更正"); + JSONObject previous = new JSONObject(q.toString()); previous.remove("benefitHistory"); + history.add(previous.set("untilMonth", effective.toString())); + q.set("benefitHistory", history).set("childName", after.getStr("childrenName")) + .set("birthday", after.getStr("childrenBirthday")) + .set("stopMonth", YearMonth.from(LocalDate.parse(after.getStr("childrenBirthday")).plusYears(18)).toString()) + .set("changeId", change.getStr("id")).set("changeEffectiveMonth", effective.toString()); + boolean first = q.getStr("userId").equals(after.getStr("guardianUserId1")); + boolean second = q.getStr("userId").equals(after.getStr("guardianUserId2")); + boolean eligible = (first || second) && LocalDate.parse(after.getStr("childrenBirthday")).isBefore(LocalDate.of(2016, 1, 1)); + q.set("eligible", eligible).set("relationship", first ? after.getStr("guardianRelationship1") : second ? after.getStr("guardianRelationship2") : "已解除监护关联"); + if (q.getStr("startMonth") != null) q.set("status", eligible && q.getStr("endMonth") == null && month(q.getStr("stopMonth")).isAfter(YearMonth.now()) ? "已核定" : "已停发"); + else if (!eligible) q.set("status", "不通过").set("basis", "资料更正后不符合原领取条件"); + affected.add(q); + } + // 所有校验先于写入;仅应用批准的关键字段,保留等待审批期间维护的日常资料。 + child.setChildrenName(after.getStr("childrenName")); child.setChildrenIdCard(after.getStr("childrenIdCard")); + child.setChildrenBirthday(java.sql.Date.valueOf(after.getStr("childrenBirthday"))); child.setChildrenSex(after.getStr("childrenSex")); + child.setGuardianUserId1(after.getStr("guardianUserId1")); child.setGuardianUserName1(after.getStr("guardianUserName1")); + child.setGuardianLoginName1(after.getStr("guardianLoginName1")); child.setGuardianRelationship1(after.getStr("guardianRelationship1")); + child.setGuardianUserId2(after.getStr("guardianUserId2")); child.setGuardianUserName2(after.getStr("guardianUserName2")); + child.setGuardianLoginName2(after.getStr("guardianLoginName2")); child.setGuardianRelationship2(after.getStr("guardianRelationship2")); + View_user first = dao.fetch(View_user.class, Cnd.where("id", "=", child.getGuardianUserId1())); + View_user second = child.getGuardianUserId2() == null ? null : dao.fetch(View_user.class, Cnd.where("id", "=", child.getGuardianUserId2())); + require(first != null && (child.getGuardianUserId2() == null || second != null), "监护人人员资料已变化,请重新申请"); + child.setGuardianUnitId1(first.getUnitId()); child.setGuardianUnitName1(first.getUnitName()); + child.setGuardianUnionId1(first.getUnionId()); child.setGuardianUnionName1(first.getUnionName()); + child.setGuardianUnitId2(second == null ? null : second.getUnitId()); + if (second != null) child.setGuardianUnitName2(second.getUnitName()); + else if (change.getJSONObject("oldData").getStr("guardianUserId2") != null) child.setGuardianUnitName2(null); + child.setGuardianUnionId2(second == null ? null : second.getUnionId()); child.setGuardianUnionName2(second == null ? null : second.getUnionName()); + dao.update(child); + for (JSONObject q : affected) { put("Q:" + q.getStr("userId"), "QUALIFICATION", q.getStr("userId"), null, q); audit(q); } + change.set("effectiveMonth", effective.toString()); + } + change.set("status", status).set("reviewBasis", input.getStr("basis")).set("reviewer", SecurityUtil.getUserId()) + .set("reviewedAt", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + put(change.getStr("id"), "CHILD_CHANGE", change.getStr("userId"), change.getStr("effectiveMonth"), change); + } + + private JSONObject get(String id) { + OnlyChildFeeRecord r = dao.fetch(OnlyChildFeeRecord.class, id); + return r == null ? null : JSONUtil.parseObj(r.getContent()); + } + + private void put(String id, String kind, String user, String period, JSONObject data) { + OnlyChildFeeRecord r = new OnlyChildFeeRecord(); + r.setId(id); r.setKind(kind); r.setUserId(user); r.setPeriod(period); + r.setContent(data.toString()); r.setOperatorId(SecurityUtil.getUserId()); r.setRecordedAt(new Date()); + dao.insertOrUpdate(r); + } + + private List records(String kind) { + List result = new ArrayList<>(); + for (OnlyChildFeeRecord r : dao.query(OnlyChildFeeRecord.class, Cnd.where("kind", "=", kind).asc("id"))) { + result.add(JSONUtil.parseObj(r.getContent())); + } + return result; + } + + private void text(JSONObject d, String key) { + require(d.getStr(key) != null && !d.getStr(key).trim().isEmpty(), "请填写" + key); + } + + private YearMonth month(String value) { + require(value != null && value.matches("\\d{4}-\\d{2}"), "月份格式应为 yyyy-MM"); + return YearMonth.parse(value); + } + + private BigDecimal money(JSONObject d, String key) { + BigDecimal n = d.getBigDecimal(key); + require(n != null && n.signum() >= 0 && n.scale() <= 2, "金额必须为非负数,最多两位小数"); + return n; + } + + private JSONObject qualification(String user) { + JSONObject q = get("Q:" + user); + require(q != null, "资格记录不存在"); + return q; + } + + private void audit(JSONObject data) { + put(UUID.randomUUID().toString(), "AUDIT", data.getStr("userId"), null, data); + } + + /** 返回本人子女、人员资料与可见资格,历史子女不按填报年度限制。 + * @return 页面初始化数据 + */ + public JSONObject context() { + List granted = tabs(); + if (granted.isEmpty()) return new JSONObject().set("tabs", granted); + String user = SecurityUtil.getUserId(); + View_user profile = dao.fetch(View_user.class, Cnd.where("id", "=", user)); + List children = dao.query(ChildManage.class, + Cnd.where("guardianUserId1", "=", user).or("guardianUserId2", "=", user) + .or("guardianLoginName1", "=", profile == null ? user : profile.getLoginname()) + .or("guardianLoginName2", "=", profile == null ? user : profile.getLoginname())); + List list = granted.contains("ledger") ? records("QUALIFICATION") : Collections.emptyList(); + for (JSONObject q : list) { + JSONObject baseline = paymentBaseline(q.getStr("userId")); + q.set("actualStartMonth", baseline == null ? "" : baseline.getStr("actualStartMonth")); + } + return new JSONObject().set("tabs", granted) + .set("children", granted.contains("qualification") ? children : Collections.emptyList()) + .set("profile", granted.contains("qualification") ? profile : null) + .set("qualifications", list).set("mine", granted.contains("qualification") ? get("Q:" + user) : null); + } + + /** 保存本人资格草稿或提交核验,不允许申请人设置核定结果。 + * @param input 关联子女、证明和监护关系 + * @param submit 是否提交审核 + * @return 保存后的资格 + */ + public JSONObject save(JSONObject input, boolean submit) { + access("qualification"); + lock(); + String user = SecurityUtil.getUserId(); + JSONObject old = get("Q:" + user); + View_user profile = dao.fetch(View_user.class, Cnd.where("id", "=", user)); + require(profile != null, "人员资料不存在"); + require(old == null || Arrays.asList("草稿", "退回", "不通过").contains(old.getStr("status")), "当前资格不可重复申请"); + ChildManage child = dao.fetch(ChildManage.class, input.getStr("childId")); + require(child != null && (user.equals(child.getGuardianUserId1()) || user.equals(child.getGuardianUserId2()) + || profile.getLoginname().equals(child.getGuardianLoginName1()) || profile.getLoginname().equals(child.getGuardianLoginName2())), "请关联本人已提交的子女资料"); + require(child.getApplyTime() != null && child.getChildrenBirthday() != null + && child.getChildrenName() != null && child.getChildrenIdCard() != null && child.getChildrenSex() != null, "请先补全并提交子女信息"); + if (submit) { + text(input, "certificateNumber"); text(input, "relationship"); + require(input.getJSONArray("proof") != null && !input.getJSONArray("proof").isEmpty(), "请上传独生子女证明"); + require(Boolean.TRUE.equals(input.getBool("onlyChild")), "需要明确声明独生子女资格并提交证明"); + } + String birthday = new SimpleDateFormat("yyyy-MM-dd").format(child.getChildrenBirthday()); + require(LocalDate.parse(birthday).isBefore(LocalDate.of(2016, 1, 1)), "子女出生日期须早于2016-01-01"); + JSONObject q = new JSONObject().set("userId", user).set("userName", profile.getUsername()) + .set("loginName", profile.getLoginname()).set("unitId", profile.getUnitId()) + .set("unitName", profile.getUnitName()).set("unionName", profile.getUnionName()) + .set("childId", child.getId()).set("childName", child.getChildrenName()).set("birthday", birthday) + .set("stopMonth", YearMonth.from(LocalDate.parse(birthday).plusYears(18)).toString()) + .set("certificateNumber", input.getStr("certificateNumber")).set("relationship", input.getStr("relationship")) + .set("proof", input.getJSONArray("proof")).set("onlyChild", input.getBool("onlyChild")) + .set("status", submit ? "待核验" : "草稿"); + put("Q:" + user, "QUALIFICATION", user, null, q); + audit(q); + return q; + } + + /** 核验资格并独立核定起发月,保存依据和经办人。 + * @param d 核验结论、起发月份及依据 + */ + public void review(JSONObject d) { + access("ledger"); lock(); + JSONObject q = qualification(d.getStr("userId")); + require("待核验".equals(q.getStr("status")), "仅待核验申请可以审核"); + String status = d.getStr("status"); + require(Arrays.asList("已核定", "退回", "不通过").contains(status), "无效审核结论"); + text(d, "basis"); + if ("已核定".equals(status)) { + require(Boolean.TRUE.equals(d.getBool("verified")), "须核验证明和重复领取情况"); + YearMonth start = month(d.getStr("startMonth")); + require(!start.isBefore(YearMonth.from(LocalDate.parse(q.getStr("birthday")))) + && start.isBefore(month(q.getStr("stopMonth"))), "起发月份须在出生月至年龄停发月之前"); + q.set("startMonth", start.toString()).set("standard", TEN); + } + q.set("status", status).set("basis", d.getStr("basis")).set("reviewer", SecurityUtil.getUserId()); + put("Q:" + q.getStr("userId"), "QUALIFICATION", q.getStr("userId"), null, q); audit(q); + if ("已核定".equals(status)) { + // 核定起发月默认同步为首次实际发放月,已有人工登记或旧年度登记优先保留。 + String user = q.getStr("userId"); + if (get("S:" + user) == null) { + JSONObject baseline = paymentBaseline(user); + put("S:" + user, "PAY_START", user, null, baseline); audit(baseline); + } + for (JSONObject draft : records("BATCH")) { + if ("草稿".equals(draft.getStr("status"))) refreshDraft(draft); + } + } + } + + /** 登记资格终止及实际停发办理依据,历史有效月份仍参与清算。 + * @param d 领取人、变更生效月份和核实依据 + */ + public void stop(JSONObject d) { + access("ledger"); lock(); JSONObject q = qualification(d.getStr("userId")); + require(q.getStr("startMonth") != null, "尚未核定待遇"); text(d, "basis"); + YearMonth end = month(d.getStr("endMonth")); + JSONArray history = q.getJSONArray("benefitHistory"); + if (history != null && !history.isEmpty()) require(!end.isBefore(month(history.getJSONObject(history.size() - 1).getStr("untilMonth"))), "终止月份不能早于最近更正生效月份"); + require(!end.isBefore(month(q.getStr("startMonth"))), "终止月份不得早于起发月份"); + require(q.getStr("endMonth") == null, "已登记终止,不可覆盖历史依据"); + q.set("endMonth", end.toString()).set("stopBasis", d.getStr("basis")).set("status", "已停发") + .set("stopOperator", SecurityUtil.getUserId()); + put("Q:" + q.getStr("userId"), "QUALIFICATION", q.getStr("userId"), null, q); audit(q); + } + + private BigDecimal due(JSONObject q, YearMonth m) { + JSONArray history = q.getJSONArray("benefitHistory"); + if (history != null) { + for (int i = 0; i < history.size(); i++) { + JSONObject version = history.getJSONObject(i); + if (m.isBefore(month(version.getStr("untilMonth")))) return due(version, m); + } + } + return !Boolean.FALSE.equals(q.getBool("eligible")) && q.getStr("startMonth") != null && !m.isBefore(month(q.getStr("startMonth"))) + && !m.isBefore(YearMonth.from(LocalDate.parse(q.getStr("birthday")))) + && m.isBefore(month(q.getStr("stopMonth"))) + && (q.getStr("endMonth") == null || m.isBefore(month(q.getStr("endMonth")))) ? TEN : BigDecimal.ZERO; + } + + /** 保存单月实发修订;空金额与零金额严格区分。 + * @param d 领取人、月份及实发金额 + */ + public void payment(JSONObject d) { + access("ledger", "settlement"); lock(); qualification(d.getStr("userId")); + YearMonth m = month(d.getStr("month")); money(d, "paid"); + JSONObject batch = get("B:" + settlementYear(m)); + require(batch == null || "草稿".equals(batch.getStr("status")), "已确认期间的实发记录不可改写"); + JSONObject row = new JSONObject().set("userId", d.getStr("userId")).set("month", m.toString()) + .set("paid", d.getBigDecimal("paid")).set("source", "人工修订"); + put("M:" + d.getStr("userId") + ":" + m, "MONTH", d.getStr("userId"), m.toString(), row); audit(row); + // 实发补录与草稿重算在同一事务中完成,确认后的历史快照保持不变。 + if (batch != null && "草稿".equals(batch.getStr("status"))) { + refreshDraft(batch); + } + } + + /** + * 保存领取人首次实际发放月份,各年度共用,并重算未确认草稿。 + * @param d 领取人、当前办理年度和首次实际发放月份 + * 起发前及停发当月起为零,发放期间每月10元;单月人工修订优先,已确认批次禁止修改。 + */ + public void paymentStart(JSONObject d) { + access("settlement"); lock(); + String user = d.getStr("userId"); qualification(user); + Integer year = d.getInt("year"); + require(year != null && year >= 1900 && year <= 9999, "年度无效"); + YearMonth start = month(d.getStr("actualStartMonth")); + JSONObject batch = get("B:" + year); + require(batch != null && "草稿".equals(batch.getStr("status")), "仅可修改未确认清算草稿的起发月份"); + boolean included = false; + JSONArray rows = batch.getJSONArray("rows"); + for (int i = 0; i < rows.size(); i++) included |= rows.getJSONObject(i).getStr("userId").equals(user); + require(included, "该领取人不在当前清算草稿中"); + JSONObject baseline = new JSONObject().set("userId", user).set("actualStartMonth", start.toString()); + // 人员级唯一记录,不再以年度拆分;历史确认快照及人工修订保持原值。 + put("S:" + user, "PAY_START", user, null, baseline); audit(baseline); + for (JSONObject draft : records("BATCH")) { + if (!"草稿".equals(draft.getStr("status"))) continue; + refreshDraft(draft); + } + } + + private JSONObject paymentBaseline(String user) { + JSONObject baseline = get("S:" + user); + if (baseline != null) return baseline; + // 兼容旧年度登记:沿用最后一次保存的月份,重新确认后写入人员级记录。 + OnlyChildFeeRecord legacy = dao.fetch(OnlyChildFeeRecord.class, + Cnd.where("kind", "=", "PAY_START").and("userId", "=", user).desc("recordedAt").desc("id")); + if (legacy != null) return JSONUtil.parseObj(legacy.getContent()); + // 兼容已核定但未登记实际起发月的人员,清算和台账共同使用核定月份作为默认值。 + JSONObject q = get("Q:" + user); + if (q == null || q.getStr("startMonth") == null) return null; + return new JSONObject().set("userId", user).set("actualStartMonth", q.getStr("startMonth")); + } + + private JSONObject actualPayment(JSONObject q, String period, JSONObject baseline) { + String user = q.getStr("userId"); + // 先读取单月修订记录,再按首次发放月份计算默认实发。 + JSONObject actual = get("M:" + user + ":" + period); + if (actual != null && !Boolean.TRUE.equals(actual.getBool("automatic"))) return actual; + // 到龄或资格终止当月自动停发;真实多发由上方的单月修订记录保留。 + if (automaticPaymentStopped(q, month(period))) { + return new JSONObject().set("userId", user).set("month", period).set("automatic", true) + .set("paid", BigDecimal.ZERO).set("source", "按停发月份自动停发"); + } + if (baseline == null) return actual; + // 各年度共用首次发放月份,仅在尚未停发的期间自动计发。 + YearMonth start = month(baseline.getStr("actualStartMonth")); + return new JSONObject().set("userId", user).set("month", period).set("automatic", true) + .set("paid", month(period).isBefore(start) ? BigDecimal.ZERO : TEN) + .set("source", "按首次实际发放月份" + start + "自动计算"); + } + + private boolean automaticPaymentStopped(JSONObject q, YearMonth period) { + // 资料更正按生效月份分段,不能用更正后的停发月倒改此前的默认实发。 + JSONArray history = q.getJSONArray("benefitHistory"); + if (history != null) { + for (int i = 0; i < history.size(); i++) { + JSONObject version = history.getJSONObject(i); + if (period.isBefore(month(version.getStr("untilMonth")))) return automaticPaymentStopped(version, period); + } + } + return Boolean.FALSE.equals(q.getBool("eligible")) || !period.isBefore(month(q.getStr("stopMonth"))) + || (q.getStr("endMonth") != null && !period.isBefore(month(q.getStr("endMonth")))); + } + + // 11、12月归属下一年度清算,每批覆盖上一年11月至当年10月。 + private int settlementYear(YearMonth period) { + return period.getMonthValue() >= 11 ? period.getYear() + 1 : period.getYear(); + } + + private void refreshDraft(JSONObject draft) { + int year = draft.getInt("year"); + draft.set("rows", calculate(year)).set("periodStart", YearMonth.of(year - 1, 11).toString()) + .set("periodEnd", YearMonth.of(year, 10).toString()).set("automaticStop", true).set("qualificationStartSynced", true); + put("B:" + year, "BATCH", null, String.valueOf(year), draft); + } + + private BigDecimal recovered(String user, String period) { + BigDecimal total = BigDecimal.ZERO; + for (OnlyChildFeeRecord r : dao.query(OnlyChildFeeRecord.class, + Cnd.where("kind", "=", "RECOVERY").and("userId", "=", user).and("period", "=", period))) { + total = total.add(JSONUtil.parseObj(r.getContent()).getBigDecimal("amount")); + } + return total; + } + + private JSONArray calculate(int year) { + require(year >= 1900 && year <= 9999, "年度无效"); + YearMonth periodStart = YearMonth.of(year - 1, 11), periodEnd = YearMonth.of(year, 10); + JSONArray rows = new JSONArray(); + for (JSONObject q : records("QUALIFICATION")) { + if (q.getStr("startMonth") == null || month(q.getStr("startMonth")).isAfter(periodEnd)) continue; + JSONObject baseline = paymentBaseline(q.getStr("userId")); + String end = q.getStr("endMonth", q.getStr("stopMonth")); + if (end.compareTo(q.getStr("stopMonth")) > 0) end = q.getStr("stopMonth"); + JSONArray months = new JSONArray(); + BigDecimal expected = BigDecimal.ZERO, paid = BigDecimal.ZERO, back = BigDecimal.ZERO, pending = BigDecimal.ZERO; + boolean complete = true; + for (int i = 0; i < 12; i++) { + YearMonth current = periodStart.plusMonths(i); + String m = current.toString(); + BigDecimal amount = due(q, current); + JSONObject actual = actualPayment(q, m, baseline); + BigDecimal deducted = recovered(q.getStr("userId"), m); + BigDecimal over = actual == null ? null : actual.getBigDecimal("paid").subtract(amount).max(BigDecimal.ZERO); + BigDecimal remaining = over == null ? null : over.subtract(deducted).max(BigDecimal.ZERO); + complete &= actual != null; expected = expected.add(amount); back = back.add(deducted); + if (actual != null) { paid = paid.add(actual.getBigDecimal("paid")); pending = pending.add(remaining); } + months.add(new JSONObject().set("month", m).set("due", amount).set("paid", actual == null ? null : actual.getBigDecimal("paid")) + .set("source", actual == null ? null : actual.getStr("source")).set("recovered", deducted).set("pending", remaining) + .set("note", m.equals(q.getStr("stopMonth")) ? "满18周岁当月停发" : "")); + } + // 已在往期停发且本期无应发、真实实发或扣回记录的人员,不重复进入年度清算。 + if (end.compareTo(periodStart.toString()) < 0 && expected.signum() == 0 && paid.signum() == 0 && back.signum() == 0) continue; + rows.add(new JSONObject(q.toString()).set("actualStartMonth", baseline == null ? null : baseline.getStr("actualStartMonth")) + .set("months", months).set("due", expected).set("paid", complete ? paid : null) + .set("recovered", back).set("pending", complete ? pending : null) + .set("status", !complete ? "资料不全" : pending.signum() > 0 ? "待扣回" : "无变动")); + } + return rows; + } + + /** 查询年度清算;确认后只读取冻结快照。 + * @param year 清算年度 + * @return 批次、逐月依据及扣回执行记录 + */ + public JSONObject settlement(int year) { + access("settlement", "changes"); lock(); + require(year >= 1900 && year <= 9999, "年度无效"); + JSONObject b = get("B:" + year); + // 自动停发及起发月同步只重算未确认草稿,已确认批次的金额及期间保持冻结。 + if (b != null && "草稿".equals(b.getStr("status")) + && (!YearMonth.of(year - 1, 11).toString().equals(b.getStr("periodStart")) + || !Boolean.TRUE.equals(b.getBool("automaticStop")) + || !Boolean.TRUE.equals(b.getBool("qualificationStartSynced")))) refreshDraft(b); + if (b != null && b.getStr("periodStart") == null) { + b.set("periodStart", YearMonth.of(year, 1).toString()).set("periodEnd", YearMonth.of(year, 10).toString()); + } + return new JSONObject().set("batch", b).set("rows", b == null ? new JSONArray() : b.getJSONArray("rows")) + .set("recoveries", records("RECOVERY")); + } + + /** 生成草稿或复核确认;确认时重新核算并拒绝缺失数据。 + * @param year 清算年度 + * @param confirm 是否确认并冻结 + */ + public void batch(int year, boolean confirm) { + access("settlement"); lock(); JSONObject old = get("B:" + year); + require(old == null || "草稿".equals(old.getStr("status")), "批次已经确认,不能重建"); + require(!confirm || old != null, "请先生成草稿"); + JSONArray rows = calculate(year); + require(!rows.isEmpty(), "没有可清算的核定待遇"); + if (confirm) { + for (int i = 0; i < rows.size(); i++) require(!"资料不全".equals(rows.getJSONObject(i).getStr("status")), "请补齐全部实发记录后确认"); + require(rows.toString().equals(old.getJSONArray("rows").toString()), "数据已变化,请重新生成草稿并复核"); + } + boolean pending = false; + for (int i = 0; i < rows.size(); i++) pending |= rows.getJSONObject(i).getBigDecimal("pending", BigDecimal.ZERO).signum() > 0; + JSONObject b = new JSONObject().set("year", year).set("id", year + "年11月").set("rows", rows) + .set("periodStart", YearMonth.of(year - 1, 11).toString()).set("periodEnd", YearMonth.of(year, 10).toString()).set("automaticStop", true).set("qualificationStartSynced", true) + .set("status", confirm ? (pending ? "已确认" : "已完成") : "草稿") + .set("operator", SecurityUtil.getUserId()).set("time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + put("B:" + year, "BATCH", null, String.valueOf(year), b); + } + + /** + * 登记本期全部结清,或按列出的多发月份分别登记实际扣回。 + * @param d 领取人、年度、全部结清标记及取消勾选时填写的月度扣回明细 + * 全部结清的金额由服务端计算;未提供的执行月份留空,单月金额不得超过余额。 + */ + public void recover(JSONObject d) { + access("settlement"); lock(); + // 保留旧单月调用入口;新表单由服务端分摊,不能依赖客户端计算的余额。 + if (!d.containsKey("settledAll")) { recoverMonth(d); return; } + Integer year = d.getInt("year"); + require(year != null && year >= 1900 && year <= 9999, "年度无效"); + String user = d.getStr("userId"); qualification(user); + JSONObject batch = settlement(year).getJSONObject("batch"); + require(batch != null, "请先生成清算批次"); + JSONObject person = null; + JSONArray rows = batch.getJSONArray("rows"); + for (int i = 0; i < rows.size(); i++) { + if (user.equals(rows.getJSONObject(i).getStr("userId"))) person = rows.getJSONObject(i); + } + require(person != null && person.getBigDecimal("paid") != null, "请先补齐该人员的实发记录"); + JSONArray months = person.getJSONArray("months"), entries = new JSONArray(); + Set allowed = new HashSet<>(); + boolean all = Boolean.TRUE.equals(d.getBool("settledAll")); + for (int i = 0; i < months.size(); i++) { + JSONObject detail = months.getJSONObject(i); + String period = detail.getStr("month"); allowed.add(period); + if (!all) continue; + BigDecimal remaining = detail.getBigDecimal("paid").subtract(detail.getBigDecimal("due")).max(BigDecimal.ZERO) + .subtract(recovered(user, period)).max(BigDecimal.ZERO); + if (remaining.signum() > 0) entries.add(new JSONObject().set("month", period).set("amount", remaining)); + } + if (!all) { + entries = d.getJSONArray("recoveryMonths"); + require(entries != null && !entries.isEmpty(), "请至少填写一个月份的实际扣回月份和金额"); + } + Set entered = new HashSet<>(); + for (int i = 0; i < entries.size(); i++) { + JSONObject detail = entries.getJSONObject(i); + String period = detail.getStr("month"); + require(allowed.contains(period) && entered.add(period), "扣回明细月份不在本期内或重复"); + JSONObject item = new JSONObject().set("userId", user).set("year", year).set("month", period) + .set("amount", detail.getBigDecimal("amount")).set("executionMonth", all ? null : detail.getStr("executionMonth")) + .set("settledAll", all).set("historical", "草稿".equals(batch.getStr("status"))); + recoverMonth(item); + } + if ("草稿".equals(batch.getStr("status"))) refreshDraft(batch); + } + + /** 登记针对某一应发月份的实际扣回,限制余额并保留办理记录。 + * @param d 归属月份、实际扣回月份、金额、凭据及是否历史扣回 + */ + private void recoverMonth(JSONObject d) { + access("settlement"); lock(); + String user = d.getStr("userId"); JSONObject q = qualification(user); + YearMonth period = month(d.getStr("month")); + int year = settlementYear(period); + require(d.getInt("year") == null || d.getInt("year") == year, "原发放月份不在当前清算期间内"); + if (!Boolean.TRUE.equals(d.getBool("settledAll"))) { + require(!month(d.getStr("executionMonth")).isBefore(period), "扣回月份不得早于原发放月份"); + } + BigDecimal amount = money(d, "amount"); require(amount.signum() > 0, "实际扣回须大于零"); + JSONObject b = get("B:" + year); + boolean confirmed = b != null && !"草稿".equals(b.getStr("status")); + require(confirmed || Boolean.TRUE.equals(d.getBool("historical")), "请先确认批次,或明确登记历史已扣回"); + JSONObject actual = actualPayment(q, period.toString(), paymentBaseline(user)); + require(actual != null, "请先登记该月实发"); + BigDecimal over = actual.getBigDecimal("paid").subtract(due(q, period)).max(BigDecimal.ZERO); + if (confirmed) { + JSONObject snapshot = null; + for (int i = 0; i < b.getJSONArray("rows").size(); i++) { + JSONObject row = b.getJSONArray("rows").getJSONObject(i); + if (!user.equals(row.getStr("userId"))) continue; + JSONArray details = row.getJSONArray("months"); + for (int j = 0; j < details.size(); j++) { + JSONObject detail = details.getJSONObject(j); + if (period.toString().equals(detail.getStr("month"))) snapshot = detail; + } + } + require(snapshot != null, "该领取人不在确认批次中"); + over = snapshot.getBigDecimal("paid").subtract(snapshot.getBigDecimal("due")).max(BigDecimal.ZERO); + } + require(amount.compareTo(over.subtract(recovered(user, period.toString()))) <= 0, "超过该月份尚未扣回余额"); + JSONObject record = new JSONObject().set("userId", user).set("month", period.toString()).set("amount", amount) + .set("executionMonth", d.getStr("executionMonth")).set("voucher", d.getStr("voucher")) + .set("operator", SecurityUtil.getUserId()).set("historical", !confirmed).set("settledAll", Boolean.TRUE.equals(d.getBool("settledAll"))); + put(UUID.randomUUID().toString(), "RECOVERY", user, period.toString(), record); + if (confirmed) { + boolean finished = true; + for (int i = 0; i < b.getJSONArray("rows").size(); i++) { + JSONObject row = b.getJSONArray("rows").getJSONObject(i); + for (int j = 0; j < row.getJSONArray("months").size(); j++) { + JSONObject m = row.getJSONArray("months").getJSONObject(j); + finished &= m.getBigDecimal("paid").subtract(m.getBigDecimal("due")).max(BigDecimal.ZERO) + .compareTo(recovered(row.getStr("userId"), m.getStr("month"))) <= 0; + } + } + if (finished) { b.set("status", "已完成"); put("B:" + year, "BATCH", null, String.valueOf(year), b); } + } + } + + /** 读取原退休申请及审批状态,退休奖励不套用月费年龄限制。 + * @return 原申请、审批状态和独立支付凭据 + */ + public JSONArray retirement() { + access("retirement"); JSONArray rows = new JSONArray(); + for (Dsznfmtx a : dao.query(Dsznfmtx.class, Cnd.NEW().desc("applyTime"))) { + boolean approved = dao.count(ProcessInstance.class, Cnd.where("businessNo", "=", a.getId()).and("state", "=", 20)) > 0; + ProcessInstance instance = dao.fetch(ProcessInstance.class, Cnd.where("businessNo", "=", a.getId()).desc("id")); + rows.add(JSONUtil.parseObj(a).set("approved", approved).set("payment", get("R:" + a.getUserId())) + .set("instanceId", instance == null ? null : instance.getId()) + .set("instanceProcessDefineId", instance == null ? null : instance.getProcessDefineId())); + } + return rows; + } + + /** 核实历史奖励后登记一次性3000元发放,同一教职工只允许登记一次。 + * @param d 原申请ID、历史核查依据、发放日期及凭据 + */ + public void reward(JSONObject d) { + access("retirement"); lock(); Dsznfmtx a = dao.fetch(Dsznfmtx.class, d.getStr("id")); + require(a != null, "原退休申请不存在"); + require(dao.count(ProcessInstance.class, Cnd.where("businessNo", "=", a.getId()).and("state", "=", 20)) > 0, "原申请尚未审批通过"); + require(get("R:" + a.getUserId()) == null, "该教职工已有奖励发放记录"); + text(d, "historyBasis"); text(d, "voucher"); text(d, "paidDate"); + require(Boolean.TRUE.equals(d.getBool("historyChecked")), "须核实历史记录未重复领取"); + require(!LocalDate.parse(d.getStr("paidDate")).isAfter(LocalDate.now()), "实际发放日期不可晚于今天"); + JSONObject payment = new JSONObject().set("applicationId", a.getId()).set("userId", a.getUserId()) + .set("amount", new BigDecimal("3000.00")).set("paidDate", d.getStr("paidDate")) + .set("voucher", d.getStr("voucher")).set("historyBasis", d.getStr("historyBasis")).set("operator", SecurityUtil.getUserId()); + put("R:" + a.getUserId(), "REWARD", a.getUserId(), d.getStr("paidDate"), payment); + } +} diff --git a/src/main/resources/db/only_child_fee.sql b/src/main/resources/db/only_child_fee.sql new file mode 100644 index 0000000..83743c8 --- /dev/null +++ b/src/main/resources/db/only_child_fee.sql @@ -0,0 +1,89 @@ +-- 月费与退休奖励凭据独立于子女资料;主键确保同一教职工、月份和年度不重复。 +CREATE TABLE IF NOT EXISTS only_child_fee_record ( + id varchar(100) PRIMARY KEY, + kind varchar(20) NOT NULL, + userId varchar(32), + period varchar(10), + content longtext, + operatorId varchar(32), + recordedAt datetime, + KEY idx_fee_kind_user (kind, userId), + KEY idx_fee_kind_period (kind, period) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +-- 事务锁串行化资格变更、快照确认与扣回登记,防止并发重复领取或超额扣回。 +INSERT IGNORE INTO only_child_fee_record (id, kind, content) VALUES ('LOCK', 'LOCK', '{}'); + +-- 补齐旧资料中缺失的账号关联,使原有完整子女信息可以直接复用。 +-- 使用信息架构判断列是否存在,兼容项目自动迁移及重复执行本脚本。 +SET @fee_column_sql = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='child_manage' AND column_name='guardianRelationship1')=0, + 'ALTER TABLE child_manage ADD COLUMN guardianRelationship1 varchar(30) NULL', 'SELECT 1'); +PREPARE fee_column_stmt FROM @fee_column_sql; +EXECUTE fee_column_stmt; +DEALLOCATE PREPARE fee_column_stmt; +SET @fee_column_sql = IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='child_manage' AND column_name='guardianRelationship2')=0, + 'ALTER TABLE child_manage ADD COLUMN guardianRelationship2 varchar(30) NULL', 'SELECT 1'); +PREPARE fee_column_stmt FROM @fee_column_sql; +EXECUTE fee_column_stmt; +DEALLOCATE PREPARE fee_column_stmt; +UPDATE child_manage c JOIN vw_user u ON u.loginname = c.guardianLoginName1 +SET c.guardianUserId1 = u.id WHERE c.guardianUserId1 IS NULL OR c.guardianUserId1 = ''; +UPDATE child_manage c JOIN vw_user u ON u.loginname = c.guardianLoginName2 +SET c.guardianUserId2 = u.id WHERE c.guardianUserId2 IS NULL OR c.guardianUserId2 = ''; + +-- 与“我的子女”使用同一父菜单,沿用已有子女模块角色的可见范围。 +INSERT INTO sys_menu (id,parentId,path,name,type,href,target,icon,showit,disabled,permission,location,hasChildren,platform,moduleId,createdAt,delFlag) +SELECT 'only_child_fee_menu',p.id,CONCAT(p.path,'0099'),'独生子女费','menu', + '/platform/childManage/onlyChildFee/index','data-pjax','fa fa-list-alt',1,0,'childManage.onlyChildFee',99,0,'PC',m.moduleId,UNIX_TIMESTAMP(),0 +FROM sys_menu m JOIN sys_menu p ON p.id=m.parentId +WHERE m.permission='childManage.mine' +AND NOT EXISTS (SELECT 1 FROM sys_menu x WHERE x.permission='childManage.onlyChildFee'); +INSERT INTO sys_role_menu (roleId,menuId) +SELECT DISTINCT rm.roleId,'only_child_fee_menu' +FROM sys_role_menu rm JOIN sys_menu m ON m.id=rm.menuId +WHERE m.permission IN ('childManage.mine','childManage.write','childManage.manage') +AND EXISTS (SELECT 1 FROM sys_menu x WHERE x.id='only_child_fee_menu') +AND NOT EXISTS (SELECT 1 FROM sys_role_menu x WHERE x.roleId=rm.roleId AND x.menuId='only_child_fee_menu'); + +-- 五个独立页面作为独生子女费的子菜单,沿用角色菜单授权;首次迁移后由角色配置维护。 +SET @fee_tabs_first_install = NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE permission IN ( + 'childManage.onlyChildFee.qualification', 'childManage.onlyChildFee.settlement', + 'childManage.onlyChildFee.changes', 'childManage.onlyChildFee.retirement', 'childManage.onlyChildFee.ledger' + ) +); +UPDATE sys_menu SET hasChildren=1, platform='PC' +WHERE permission='childManage.onlyChildFee'; + +INSERT INTO sys_menu (id,parentId,path,name,type,href,target,icon,showit,disabled,permission,location,hasChildren,platform,moduleId,createdAt,delFlag) +SELECT t.id,p.id,CONCAT(p.path,t.pathSuffix),t.name,'menu', + CONCAT('/platform/childManage/onlyChildFee/',t.tab,'/index'),'data-pjax','fa fa-list-alt',1,0, + CONCAT('childManage.onlyChildFee.',t.tab),t.location,0,'PC',p.moduleId,UNIX_TIMESTAMP(),0 +FROM sys_menu p +CROSS JOIN ( + SELECT 'only_child_fee_qualification' AS id, 'qualification' AS tab, '资格登记' AS name, '0001' AS pathSuffix, 1 AS location + UNION ALL SELECT 'only_child_fee_settlement','settlement','年度清算','0002',2 + UNION ALL SELECT 'only_child_fee_changes','changes','变动报表','0003',3 + UNION ALL SELECT 'only_child_fee_retirement','retirement','退休奖励','0004',4 + UNION ALL SELECT 'only_child_fee_ledger','ledger','待遇台账','0005',5 +) t +WHERE p.permission='childManage.onlyChildFee' +AND NOT EXISTS (SELECT 1 FROM sys_menu x WHERE x.permission=CONCAT('childManage.onlyChildFee.',t.tab)); + +-- 普通教职工保留本人资格登记;原管理角色保留办理页签,重复执行不恢复已撤销的子菜单授权。 +INSERT INTO sys_role_menu (roleId,menuId) +SELECT DISTINCT r.id,c.id +FROM sys_role r +JOIN sys_role_menu rm ON rm.roleId=r.id +JOIN sys_menu p ON p.id=rm.menuId AND p.permission='childManage.onlyChildFee' +JOIN sys_menu c ON c.parentId=p.id +WHERE @fee_tabs_first_install=1 +AND c.permission IN ('childManage.onlyChildFee.qualification','childManage.onlyChildFee.settlement', + 'childManage.onlyChildFee.changes','childManage.onlyChildFee.retirement','childManage.onlyChildFee.ledger') +AND (c.permission='childManage.onlyChildFee.qualification' + OR r.code IN ('SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_WELFARE_ADMIN')) +AND NOT EXISTS (SELECT 1 FROM sys_role_menu existing WHERE existing.roleId=r.id AND existing.menuId=c.id); + +-- 将已有子菜单映射至独立页面,仅更新地址,保留现有角色授权。 +UPDATE sys_menu SET href=CONCAT('/platform/childManage/onlyChildFee/',SUBSTRING_INDEX(permission,'.',-1),'/index') +WHERE permission IN ('childManage.onlyChildFee.qualification','childManage.onlyChildFee.settlement', + 'childManage.onlyChildFee.changes','childManage.onlyChildFee.retirement','childManage.onlyChildFee.ledger'); diff --git a/src/main/resources/static/components/plugins/sysFilePreview/index.vue b/src/main/resources/static/components/plugins/sysFilePreview/index.vue index 7fcb532..d7605cd 100644 --- a/src/main/resources/static/components/plugins/sysFilePreview/index.vue +++ b/src/main/resources/static/components/plugins/sysFilePreview/index.vue @@ -1,6 +1,16 @@ @@ -32,6 +43,11 @@ module.exports = { name: "sysFilePreview", props: { + // 表单只读态使用紧凑附件行,保留预览和下载操作,不占用缩略图空间。 + compact: { + type: Boolean, + default: false + }, files: { type: [String, Array], default: undefined, @@ -112,6 +128,69 @@ module.exports = { diff --git a/src/main/resources/views/platform/zhgh/staffmanage/childManage/write/index.html b/src/main/resources/views/platform/zhgh/staffmanage/childManage/write/index.html index ec9d243..2b353b7 100644 --- a/src/main/resources/views/platform/zhgh/staffmanage/childManage/write/index.html +++ b/src/main/resources/views/platform/zhgh/staffmanage/childManage/write/index.html @@ -5,13 +5,14 @@ layout("/layouts/platform.html"){

信息填报

+ - - + + @@ -26,20 +27,20 @@ layout("/layouts/platform.html"){ - - - + + :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenSex"> @@ -86,11 +87,16 @@ layout("/layouts/platform.html"){ - + + + + + + - {{formData.guardianLoginName1}} + {{formData.guardianLoginName1}} @@ -105,12 +111,17 @@ layout("/layouts/platform.html"){ - + + + + + + - + @@ -130,8 +141,18 @@ layout("/layouts/platform.html"){ + + + + + + + - 保存 + 申请关键资料更正 + 取消更正 + 提交更正申请 + {{formData.qualificationLocked ? '保存日常资料' : '保存'}}
@@ -144,9 +165,10 @@ layout("/layouts/platform.html"){ data() { return { bizId: GetQueryString("bizId"), - formData: {}, + formData: {}, correctionMode: false, correctionReason: '', originalData: null, + formRules: { - childRelationship: [{ required: true, message: '请选择就读年级', trigger: 'change' }], + childrenGrade: [{ required: true, message: '请选择就读年级', trigger: 'change' }], childrenYear: [{ required: true, message: '请选择入学年份', trigger: 'change' }], childrenName: [{ required: true, message: '请输入子女姓名', trigger: 'blur' }], childrenIdCard: [{ required: true, message: '请输入子女身份证号', trigger: 'blur' }, {pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入正确的身份证号码', trigger: 'blur'}], @@ -158,6 +180,25 @@ layout("/layouts/platform.html"){ } }, methods: { + beginCorrection() { + this.originalData = JSON.parse(JSON.stringify(this.formData)) + this.correctionReason = '' + this.correctionMode = true + }, + cancelCorrection() { + this.formData = this.originalData + this.correctionMode = false + }, + submitCorrection() { + if (!this.correctionReason.trim()) { this.$message.warning('请填写更正原因及证明依据'); return } + this.$axios.post('/platform/childManage/write/requestChange', {data: JSON.stringify(this.formData), reason: this.correctionReason}).then(async res => { + if (res.code === 0) { + this.correctionMode = false + this.formData = await this.findOne(this.bizId) + this.$message.success(res.msg) + } + }) + }, async getIsRepeatByIdCard() { if (!this.formData.childrenIdCard) { this.$message.error("请填写子女身份证号码") @@ -169,10 +210,10 @@ layout("/layouts/platform.html"){ }) if (res.code === 0) { if (res.data > 0) { - this.$message.error("该身份证在本年度已填报!") + this.$message.error("该身份证已登记,请维护已有子女资料!") return true } else { - this.$message.success("该身份证在本年度暂未填报!") + this.$message.success("该身份证暂未登记!") return false } } diff --git a/src/main/resources/views/platform/zhghh5/staffmanage/childmanage/write/index.html b/src/main/resources/views/platform/zhghh5/staffmanage/childmanage/write/index.html index 20815de..e3e1671 100644 --- a/src/main/resources/views/platform/zhghh5/staffmanage/childmanage/write/index.html +++ b/src/main/resources/views/platform/zhghh5/staffmanage/childmanage/write/index.html @@ -12,11 +12,11 @@ layout("/layouts/platform_h5.html"){
- + - + @@ -74,10 +74,10 @@ layout("/layouts/platform_h5.html"){ @@ -99,8 +99,8 @@ layout("/layouts/platform_h5.html"){ - - + + - +
- 保存申请 + + 申请关键资料更正 + 取消更正 + 提交更正申请 + {{formData.qualificationLocked ? '保存日常资料' : '保存申请'}} +
@@ -143,7 +148,7 @@ layout("/layouts/platform_h5.html"){ dicts: ["CHILD_MANAGE_GRADE"], data() { return { - bizId: GetQueryString("bizId"), + bizId: GetQueryString("bizId"), correctionMode: false, correctionReason: '', originalData: null, formData: { childrenName: '', childrenGrade: '', @@ -215,7 +220,26 @@ layout("/layouts/platform_h5.html"){ }, methods: { - onSave() { + beginCorrection() { + this.originalData = JSON.parse(JSON.stringify(this.formData)) + this.correctionReason = '' + this.correctionMode = true + }, + cancelCorrection() { + this.formData = this.originalData + this.correctionMode = false + }, + submitCorrection() { + if (!this.correctionReason.trim()) { this.$toast('请填写更正原因及证明依据'); return } + this.$axios.post('/platform/childManage/write/requestChange', {data: JSON.stringify(this.formData), reason: this.correctionReason}).then(res => { + if (res.code === 0) { + this.correctionMode = false + this.$toast(res.msg) + this.init() + } + }) + }, + onSave() { // 先进行表单验证 this.$refs.formRef.validate().then(() => { this.$dialog.confirm({