feat:独生子女模块初版

This commit is contained in:
2026-11-12 17:06:18 +08:00
parent 37a9c579cd
commit adad1bfe84
22 changed files with 1890 additions and 38 deletions
@@ -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();
}
@@ -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();
}
@@ -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));
}
}
@@ -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<Object> 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;
}
}
@@ -36,12 +36,23 @@ public class ChildManage extends BaseModel implements Serializable {
@PrevInsert(uu32 = true)
private String id;
// 由关联资格实时计算,仅用于页面展示,不作为数据库字段或保存依据。
private boolean qualificationLocked;
// 更正申请和审核结果仅随详情返回,不覆盖子女原始字段。
private List<JSONObject> 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(教工)姓名")
@@ -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;
}
@@ -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<String> tabs() {
Set<String> permissions = new HashSet<>();
for (Sys_menu menu : sysUserService.getMenus(SecurityUtil.getUserId())) {
permissions.add(menu.getPermission());
}
List<String> 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<String> 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<JSONObject> childChanges() {
boolean reviewer = tabs().contains("ledger");
List<JSONObject> 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<JSONObject> 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<JSONObject> records(String kind) {
List<JSONObject> 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<String> 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<ChildManage> 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<JSONObject> 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<String> 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<String> 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);
}
}
+89
View File
@@ -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');
@@ -1,6 +1,16 @@
<template>
<div>
<div class="file-preview-container" v-if="fileList && fileList.length > 0">
<div class="file-preview-list" v-if="compact && fileList && fileList.length > 0">
<div class="file-preview-row" v-for="item in fileList" :key="item.id">
<i class="el-icon-document file-preview-icon" aria-hidden="true"></i>
<span class="file-preview-name" :title="item.name">{{ item.name }}</span>
<div class="file-preview-actions">
<button type="button" :aria-label="'预览' + item.name" @click="$commonUtil.previewFile(item)">预览</button>
<button type="button" :aria-label="'下载' + item.name" @click="$downLoad(item.downloadPath)">下载</button>
</div>
</div>
</div>
<div class="file-preview-container" v-else-if="fileList && fileList.length > 0">
<div class="file-item" title="点击查看" v-for="item in fileList" :key="item.id">
<el-image
v-if="item.suffix === 'jpg' || item.suffix === 'png' || item.suffix === 'jpeg' || item.suffix === 'gif'"
@@ -24,6 +34,7 @@
</div>
</div>
</div>
<span v-else-if="compact" class="file-preview-empty">暂无证明材料</span>
<el-empty v-else description="暂无" :image-size="60"></el-empty>
</div>
</template>
@@ -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 = {
</script>
<style scoped>
.file-preview-list {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.file-preview-row {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
min-height: 40px;
padding: 0 12px;
box-sizing: border-box;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #f8fafc;
line-height: 20px;
}
.file-preview-icon {
flex-shrink: 0;
color: #0075cf;
font-size: 18px;
}
.file-preview-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #606266;
font-size: 13px;
}
.file-preview-actions {
display: flex;
flex-shrink: 0;
gap: 12px;
}
.file-preview-actions button {
padding: 8px 0;
border: 0;
background: transparent;
color: #0075cf;
font: inherit;
font-size: 13px;
cursor: pointer;
}
.file-preview-actions button:focus-visible {
outline: 2px solid #0075cf;
outline-offset: 2px;
}
.file-preview-empty {
color: #909399;
font-size: 13px;
}
.file-preview-container {
position: relative;
display: flex;
@@ -17,10 +17,10 @@ const childManageInfo = {
<el-descriptions-item label="中考总成绩">{{viewData.childrenMiddleScore}}</el-descriptions-item>
<el-descriptions-item label="学科组合">{{viewData.childrenSubjectCombination}}</el-descriptions-item>
<el-descriptions-item label="户籍所在地" :span="2">{{viewData.childrenHuKouAddress}}</el-descriptions-item>
<el-descriptions-item label="监护人1">{{viewData.guardianUserName1}}</el-descriptions-item>
<el-descriptions-item label="监护人1">{{viewData.guardianUserName1}}({{viewData.guardianRelationship1 || '关系待补充'}})</el-descriptions-item>
<el-descriptions-item label="联系电话">{{viewData.guardianMobile1}}</el-descriptions-item>
<el-descriptions-item label="工作单位">{{viewData.guardianUnitName1}}</el-descriptions-item>
<el-descriptions-item label="监护人2">{{viewData.guardianUserName2}}</el-descriptions-item>
<el-descriptions-item label="监护人2">{{viewData.guardianUserName2}}({{viewData.guardianRelationship2 || '关系待补充'}})</el-descriptions-item>
<el-descriptions-item label="联系电话">{{viewData.guardianMobile2}}</el-descriptions-item>
<el-descriptions-item label="工作单位">{{viewData.guardianUnitName2}}</el-descriptions-item>
<el-descriptions-item label="备注">{{viewData.note}}</el-descriptions-item>
@@ -20,6 +20,7 @@ layout("/layouts/platform.html"){
</el-card>
<el-card class="mt10" shadow="never">
<table-tool label="填报列表">
<el-button type="primary" @click="openOnlyChildFee">独生子女费</el-button>
</table-tool>
<!-- 添加一个带有横向滚动的容器 -->
<div class="table-container">
@@ -89,11 +90,14 @@ layout("/layouts/platform.html"){
return {
pageDataUrl: "/platform/childManage/mine/pageData",
pageForm: {
year: moment().format("YYYY"),
year: null,
}
}
},
methods: {
openOnlyChildFee() {
commonUtil.pjaxPush('/platform/childManage/onlyChildFee/qualification/index')
},
openView(row) {
this.$refs.guava.view(()=>{
this.$refs.childManageInfoRef.onOpen(row)
@@ -0,0 +1,36 @@
<!--# layout("/layouts/platform.html"){ #-->
<div id="app" v-cloak class="only-child-fee" v-loading="busy">
<el-card shadow="never">
<el-empty v-if="ready && !tabs.includes(tab)" description="暂无当前页面的访问权限"></el-empty>
<template v-if="tabs.includes(tab)">
<div class="fee-filter"><label>年度 <el-date-picker v-model="year" type="year" value-format="yyyy" :clearable="false" @change="loadSettlement"></el-date-picker></label><el-select v-model="unit" clearable placeholder="全部单位"><el-option v-for="u in units" :key="u" :value="u"></el-option></el-select><el-input v-model="keyword" placeholder="姓名 / 工号" clearable></el-input><el-select v-model="changeType" clearable placeholder="全部变动"><el-option value="停发"></el-option><el-option value="扣回"></el-option></el-select><el-button type="primary" @click="loadSettlement">查询</el-button><el-button @click="reset">重置</el-button><el-button :disabled="!confirmed" @click="exportChanges">导出Excel</el-button></div>
<div class="fee-recovery-summary fee-period-summary">
<div class="fee-period-item"><span class="fee-period-label">批次</span><strong class="fee-period-value">{{batch ? batch.id : '尚未生成'}}</strong></div>
<div class="fee-period-item"><span class="fee-period-label">核算期间</span><strong class="fee-period-value">{{periodLabel}}</strong></div>
</div>
<div class="fee-paper fee-notice-paper">
<table class="fee-notice-table">
<colgroup><col style="width:4%"><col style="width:10%"><col style="width:10%"><col style="width:22%"><col style="width:6%"><col style="width:6%"><col style="width:8%"><col style="width:6%"><col style="width:19%"><col style="width:9%"></colgroup>
<thead>
<tr><th colspan="10" class="fee-notice-title">{{Number(year)-1}}年11月—{{year}}年10月独生子女父母奖励费变动通知单</th></tr>
<tr><td colspan="4" class="fee-notice-recipient">人力资源处:</td><td colspan="6" class="fee-notice-unit">单位:元</td></tr>
<tr><th>序号</th><th>工号</th><th>姓名</th><th>单位</th><th>调前<br>标准</th><th>调后<br>标准</th><th>扣发<br>金额</th><th>补发<br>金额</th><th>停发时间</th><th>备注<br>说明</th></tr>
</thead>
<tbody>
<tr v-for="(row,index) in changes" :key="row.userId"><td>{{index+1}}</td><td>{{row.loginName}}</td><td>{{row.userName}}</td><td>{{row.unitName}}</td><td>10.00</td><td>{{cash(endMonth(row)<=periodEnd?0:10)}}</td><td>{{cash(row.deduction)}}</td><td>0.00</td><td>{{noticeStopRange(row)}}</td><td>{{row.stopBasis || (endMonth(row)<=periodEnd?'停发':'扣发调整')}}</td></tr>
<tr v-if="!changes.length"><td colspan="10">暂无变动记录</td></tr>
<tr><td colspan="4">合计</td><td>{{cash(changeTotals.before)}}</td><td>{{cash(changeTotals.after)}}</td><td>{{cash(changeTotals.deduction)}}</td><td>0.00</td><td></td><td></td></tr>
</tbody>
<tfoot><tr><td colspan="4">审核人:</td><td colspan="6">经办人:</td></tr></tfoot>
</table>
</div><el-alert title="导出当前筛选的全部记录,仅包含“变动表”工作表;确认前不可导出办理。" type="info" :closable="false" show-icon></el-alert>
</template>
</el-card>
<!--#include('dialogs.html'){}#-->
</div>
<script>
<!--#include('page.js'){}#-->
initOnlyChildFeePage('changes');
</script>
<!--#include('style.html'){}#-->
<!--# } #-->
@@ -0,0 +1,56 @@
<el-dialog title="子女关键资料更正核验" :visible.sync="changeDialog" width="760px" :close-on-click-modal="false">
<template v-if="selectedChange">
<p>申请人:{{selectedChange.applicant}} 申请时间:{{selectedChange.submittedAt}}</p>
<p>更正原因及证明依据:{{selectedChange.reason}}</p>
<el-table :data="correctionDiff" border><el-table-column prop="label" label="更正字段"></el-table-column><el-table-column prop="before" label="修改前"></el-table-column><el-table-column prop="after" label="拟修改为"></el-table-column></el-table>
<el-form v-if="selectedChange.status === '待核验'" label-width="140px" class="mt20">
<el-form-item label="核验结论" required><el-select v-model="changeReview.status"><el-option value="已批准" label="批准更正"></el-option><el-option value="退回" label="退回补正"></el-option></el-select></el-form-item>
<el-form-item label="生效月份" :required="changeReview.status === '已批准'"><el-date-picker v-model="changeReview.effectiveMonth" type="month" value-format="yyyy-MM"></el-date-picker></el-form-item>
<el-form-item label="证明及领取资格"><el-checkbox v-model="changeReview.verified">已核实证明及双方监护人的领取资格</el-checkbox></el-form-item>
<el-form-item label="审核依据 / 原因" required><el-input v-model="changeReview.basis" type="textarea" maxlength="2000"></el-input></el-form-item>
<el-alert type="info" :closable="false" title="批准后按生效月份保留待遇历史版本;不得覆盖已确认清算期间。新监护人的月费资格仍需另行申请。"></el-alert>
</el-form>
<el-descriptions v-else border :column="2" class="mt20"><el-descriptions-item label="审核结果">{{selectedChange.status}}</el-descriptions-item><el-descriptions-item label="生效月份">{{selectedChange.effectiveMonth || '—'}}</el-descriptions-item><el-descriptions-item label="审核人">{{selectedChange.reviewer}}</el-descriptions-item><el-descriptions-item label="审核时间">{{selectedChange.reviewedAt}}</el-descriptions-item><el-descriptions-item label="审核依据">{{selectedChange.reviewBasis}}</el-descriptions-item></el-descriptions>
</template>
<span slot="footer"><el-button @click="changeDialog=false">关闭</el-button><el-button v-if="selectedChange && selectedChange.status === '待核验'" type="primary" :loading="busy" @click="submitCorrectionReview">保存核验结果</el-button></span>
</el-dialog>
<snaker-chart ref="retireChart"></snaker-chart><el-dialog :title="dialogTitle" :visible.sync="dialog" width="680px" :close-on-click-modal="false">
<span slot="title" class="el-dialog__title">{{dialogTitle}}<el-tooltip v-if="['payment','recover'].includes(action)" :content="action === 'payment' ? '用于修订单个月份实际发放的金额。修订优先于起发月份自动计算,并更新未确认清算草稿;不会执行工资发放。' : '用于登记已实际扣回的多发费用,不会执行工资扣款。默认勾选已全部结清,按各月剩余金额结清;取消勾选后按月填写,未填写的月份保持不变。'" placement="top" popper-class="fee-help-popper"><i class="el-icon-question fee-help-icon" tabindex="0" role="button" :aria-label="dialogTitle+'说明'"></i></el-tooltip></span>
<div v-if="['payment','recover','review','stop'].includes(action)" class="fee-recovery-summary">
<div class="fee-recovery-person"><span class="fee-recovery-name">{{editing.userName}}</span><span class="fee-recovery-id">工号 {{editing.loginName}}</span><div class="fee-recovery-caption">{{({recover:'本期扣回登记',payment:'逐月实发修订',review:'资格核验',stop:'停发办理'})[action]}}</div></div>
<div v-if="action === 'recover'" class="fee-recovery-balance"><div class="fee-recovery-caption">本期尚未扣回合计</div><strong>{{cash(remaining(editing))}}</strong><span class="fee-recovery-unit">元</span></div>
<div v-else-if="action === 'payment'" class="fee-recovery-balance fee-payment-start"><div class="fee-recovery-caption">起发月份</div><strong>{{editing.actualStartMonth || '未登记'}}</strong></div>
<div v-else class="fee-recovery-balance fee-record-state"><div class="fee-recovery-caption">{{action === 'review' ? '核验状态' : '年龄停发月'}}</div><strong>{{(action === 'review' ? editing.status : editing.stopMonth) || '—'}}</strong></div>
</div>
<p v-else>{{editing.userName}} {{editing.loginName}}</p>
<div v-if="action === 'review'" class="fee-record-card">
<div class="fee-record-fields">
<div><span class="fee-record-label">独生子女证号</span><span>{{editing.certificateNumber || '—'}}</span></div>
<div><span class="fee-record-label">监护关系</span><span>{{editing.relationship || '—'}}</span></div>
<div><span class="fee-record-label">子女姓名</span><span>{{editing.childName || '—'}}</span></div>
<div><span class="fee-record-label">出生日期</span><span>{{editing.birthday || '—'}}</span></div>
<div class="fee-record-wide"><span class="fee-record-label">证明材料</span><file-preview :files="editing.proof" complete_result compact></file-preview></div>
<div class="fee-record-wide"><span class="fee-record-label">原核定依据</span><span>{{editing.basis || '—'}}</span></div>
</div>
</div>
<el-form :class="{'fee-form-card':['review','stop'].includes(action)}" :label-width="['payment','recover'].includes(action) ? '140px' : '150px'" :label-position="['payment','recover','review','stop'].includes(action) ? 'left' : 'right'" :disabled="action === 'review' && editing.status !== '待核验'">
<template v-if="action === 'review'"><el-form-item label="审核结论" required><el-select v-model="entry.status"><el-option v-for="s in ['已核定','退回','不通过']" :key="s" :value="s"></el-option></el-select></el-form-item><el-form-item label="独立核定起发月份"><el-date-picker v-model="entry.startMonth" type="month" value-format="yyyy-MM"></el-date-picker></el-form-item><el-form-item label="资格核验"><el-checkbox v-model="entry.verified">已核验证明及重复领取情况</el-checkbox></el-form-item></template>
<el-form-item v-if="action === 'stop'" label="终止 / 实际停发月" required><el-date-picker v-model="entry.endMonth" type="month" value-format="yyyy-MM"></el-date-picker></el-form-item>
<el-form-item v-if="['review','stop'].includes(action)" label="核定依据 / 原因" required><el-input v-model="entry.basis" type="textarea" maxlength="2000"></el-input></el-form-item>
<template v-if="action === 'payment'"><el-form-item label="原发放月份" required><span slot="label">原发放月份<el-tooltip content="选择要修订实发金额的那个月份,不是首次起发月份。" placement="top" popper-class="fee-help-popper"><i class="el-icon-question fee-help-icon" tabindex="0" role="button" aria-label="原发放月份说明"></i></el-tooltip></span><el-date-picker v-model="entry.month" type="month" value-format="yyyy-MM"></el-date-picker></el-form-item></template>
<template v-if="action === 'payment'"><el-form-item label="实发金额" required><span slot="label">实发金额<el-tooltip content="填写该月实际发放的总金额,不是差额。核实未发放填0,未知金额请勿登记;其他月份不受影响。" placement="top" popper-class="fee-help-popper"><i class="el-icon-question fee-help-icon" tabindex="0" role="button" aria-label="实发金额说明"></i></el-tooltip></span><el-input-number v-model="entry.paid" :min="0" :precision="2"></el-input-number></el-form-item></template>
<template v-if="action === 'recover'">
<el-form-item label-width="0px"><el-checkbox v-model="entry.settledAll">已全部结清</el-checkbox><el-tooltip content="确认本期剩余多发费用均已追回后勾选,保存时按各月剩余金额结清,不要求指定扣回月份。仅追回部分时取消勾选。" placement="top" popper-class="fee-help-popper"><i class="el-icon-question fee-help-icon" tabindex="0" role="button" aria-label="已全部结清说明"></i></el-tooltip></el-form-item>
<div v-if="!entry.settledAll" style="max-height:360px;overflow-y:auto;padding-right:8px">
<div v-for="item in entry.recoveryMonths" :key="item.month" class="fee-recovery-month">
<div class="fee-recovery-month-title">{{item.month}} · 尚未扣回 {{cash(item.remaining)}} 元</div>
<el-form-item label="实际扣回月份"><span slot="label">实际扣回月份<el-tooltip content="填写实际完成扣款的月份,不得早于该笔费用原发放月份;未办理的月份留空。" placement="top" popper-class="fee-help-popper"><i class="el-icon-question fee-help-icon" tabindex="0" role="button" aria-label="实际扣回月份说明"></i></el-tooltip></span><el-date-picker v-model="item.executionMonth" type="month" value-format="yyyy-MM"></el-date-picker></el-form-item>
<el-form-item label="实际扣回金额"><span slot="label">实际扣回金额<el-tooltip content="填写这次已扣回且归属于上方月份的金额,须大于0且不得超过该月剩余余额;已登记的扣回不要重复填写。" placement="top" popper-class="fee-help-popper"><i class="el-icon-question fee-help-icon" tabindex="0" role="button" aria-label="实际扣回金额说明"></i></el-tooltip></span><el-input-number v-model="item.amount" :min="0" :max="item.remaining" :precision="2"></el-input-number></el-form-item>
</div>
<el-empty v-if="!entry.recoveryMonths.length" description="暂无待扣回月份"></el-empty>
</div>
</template>
<template v-if="action === 'reward'"><el-form-item label="一次性奖励">3000.00元</el-form-item><el-form-item label="历史奖励核查" required><el-checkbox v-model="entry.historyChecked">已核实该教职工未重复领取</el-checkbox></el-form-item><el-form-item label="历史核查依据" required><el-input v-model="entry.historyBasis" type="textarea"></el-input></el-form-item><el-form-item label="实际发放日期" required><el-date-picker v-model="entry.paidDate" type="date" value-format="yyyy-MM-dd"></el-date-picker></el-form-item></template>
<el-form-item v-if="action === 'reward'" label="实际执行凭据" required><el-input v-model="entry.voucher" type="textarea"></el-input></el-form-item>
</el-form><span slot="footer"><el-button @click="dialog=false">关闭</el-button><el-button v-if="action !== 'review' || editing.status === '待核验'" type="primary" :loading="busy" @click="submitEntry">保存办理结果</el-button></span>
</el-dialog>
@@ -0,0 +1,11 @@
<!--# layout("/layouts/platform.html"){ #-->
<div id="app" v-cloak class="only-child-fee" v-loading="busy">
<el-card shadow="never">
<el-empty v-if="ready && !tabs.includes(tab)" description="暂无当前页面的访问权限"></el-empty>
</el-card>
</div>
<script>
<!--#include('page.js'){}#-->
initOnlyChildFeePage('');
</script>
<!--# } #-->
@@ -0,0 +1,42 @@
<!--# layout("/layouts/platform.html"){ #-->
<div id="app" v-cloak class="only-child-fee" v-loading="busy">
<el-card shadow="never">
<el-empty v-if="ready && !tabs.includes(tab)" description="暂无当前页面的访问权限"></el-empty>
<template v-if="tabs.includes(tab)">
<template v-if="corrections.length">
<h4>关键资料更正申请</h4>
<el-table :data="corrections" border class="mb10">
<el-table-column prop="applicant" label="申请人"></el-table-column>
<el-table-column label="子女"><template slot-scope="{row}">{{row.oldData.childrenName}}</template></el-table-column>
<el-table-column prop="reason" label="更正原因"></el-table-column>
<el-table-column prop="submittedAt" label="申请时间"></el-table-column>
<el-table-column prop="status" label="状态"></el-table-column>
<el-table-column prop="effectiveMonth" label="生效月份"></el-table-column>
<el-table-column label="操作"><template slot-scope="{row}"><el-button type="text" @click="openCorrection(row)">{{row.status === '待核验' ? '核验更正' : '查看更正记录'}}</el-button></template></el-table-column>
</el-table>
</template>
<search @search="perform(()=>fetchTab())">
<search-item label="姓名 / 工号"><el-input v-model="ledgerKeyword" placeholder="请输入姓名或工号" clearable @keyup.enter.native="perform(()=>fetchTab())"></el-input></search-item>
<search-item label="所属单位"><el-select v-model="ledgerUnit" placeholder="全部单位" clearable filterable><el-option v-for="unit in ledgerUnits" :key="unit" :value="unit"></el-option></el-select></search-item>
<search-item label="子女姓名"><el-input v-model="ledgerChild" placeholder="请输入子女姓名" clearable @keyup.enter.native="perform(()=>fetchTab())"></el-input></search-item>
<search-item label="核验状态"><el-select v-model="ledgerReviewStatus" placeholder="全部核验状态" clearable><el-option v-for="status in ledgerReviewStatuses" :key="status" :value="status"></el-option></el-select></search-item>
<template slot="actions"><el-button @click="resetLedgerFilters">重置</el-button></template>
</search>
<table-tool label="待遇台账" class="fee-standard-toolbar">
<el-radio-group v-model="ledgerState" aria-label="待遇状态筛选"><el-radio-button label="all">全部</el-radio-button><el-radio-button label="pending">未起发</el-radio-button><el-radio-button label="active">发放中</el-radio-button><el-radio-button label="stopped">已停发</el-radio-button></el-radio-group>
</table-tool>
<el-table :data="qualificationRows" border class="mt10">
<el-table-column prop="loginName" label="工号"></el-table-column><el-table-column prop="userName" label="姓名"></el-table-column><el-table-column prop="unitName" label="单位"></el-table-column><el-table-column prop="childName" label="子女"></el-table-column><el-table-column prop="startMonth" label="起发月"></el-table-column><el-table-column prop="stopMonth" label="年龄停发月"></el-table-column><el-table-column prop="status" label="核验状态"></el-table-column>
<el-table-column label="待遇状态"><template slot-scope="{row}">{{({pending:'未起发',active:'发放中',stopped:'已停发'})[ledgerBenefitStatus(row)]}}</template></el-table-column>
<el-table-column label="操作" width="200"><template slot-scope="{row}"><el-button type="text" @click="open('review',row)">{{row.status === '待核验' ? '核验' : '查看依据'}}</el-button><el-button v-if="row.startMonth" type="text" @click="open('payment',row)">登记实发</el-button></template></el-table-column>
</el-table>
</template>
</el-card>
<!--#include('dialogs.html'){}#-->
</div>
<script>
<!--#include('page.js'){}#-->
initOnlyChildFeePage('ledger');
</script>
<!--#include('style.html'){}#-->
<!--# } #-->
@@ -0,0 +1,148 @@
// 独立页面固定业务入口;旧入口仅跳转至已授权页面,不在无权限时替换页面内容。
function initOnlyChildFeePage(page) {
new Vue({
el: '#app', store,
data() { return {tab:page,busy:false,ready:false,tabs:[],profile:{},children:[],qualifications:[],form:{proof:[]},ledgerKeyword:'',ledgerUnit:'',ledgerChild:'',ledgerReviewStatus:'',ledgerState:'all',corrections:[],changeDialog:false,selectedChange:null,changeReview:{},
year:moment().format('YYYY'),unit:'',keyword:'',changeType:'',batch:null,rows:[],recoveries:[],expandedRowKeys:[],settlementPage:1,settlementPageSize:10,
retirements:[],retireYear:'',retireUnit:'',retireKeyword:'',approval:'',paidStatus:'',retireDetail:null,
dialog:false,action:'',entry:{},editing:{},amountColumns:[{key:'due',label:'应发金额'},{key:'paid',label:'实发金额'},{key:'recovered',label:'已扣回'},{key:'pending',label:'本次扣回'}]}; },
computed:{
periodStart(){return this.batch&&this.batch.periodStart||String(Number(this.year)-1)+'-11';},
periodEnd(){return this.batch&&this.batch.periodEnd||this.year+'-10';},
periodLabel(){return this.periodStart.replace('-','.')+'—'+this.periodEnd.replace('-','.');},
correctionDiff(){
if(!this.selectedChange)return [];
const labels={childrenName:'子女姓名',childrenIdCard:'身份证号',childrenBirthday:'出生日期',childrenSex:'性别',guardianUserName1:'第一监护人姓名',guardianLoginName1:'第一监护人工号',guardianRelationship1:'第一监护人关系',guardianUserName2:'第二监护人姓名',guardianLoginName2:'第二监护人工号',guardianRelationship2:'第二监护人关系'};
return Object.keys(labels).filter(k=>(this.selectedChange.oldData[k]||'')!==(this.selectedChange.newData[k]||'')).map(k=>({label:labels[k],before:this.selectedChange.oldData[k]||'—',after:this.selectedChange.newData[k]||'—'}));
},
editable(){return !this.form.status || ['草稿','退回','不通过'].includes(this.form.status);},
childBirthday(){if(!this.editable && this.form.birthday)return this.form.birthday;const c=this.children.find(c=>c.id===this.form.childId);return c?this.date(c.childrenBirthday):this.form.birthday;},
ledgerUnits(){return [...new Set(this.qualifications.map(q=>q.unitName).filter(Boolean))];},
ledgerReviewStatuses(){return [...new Set(this.qualifications.map(q=>q.status).filter(Boolean))];},
qualificationRows(){
const keyword=this.ledgerKeyword.trim(),child=this.ledgerChild.trim();
return this.qualifications.filter(q=>(!keyword||((q.userName||'')+' '+(q.loginName||'')).includes(keyword))
&&(!this.ledgerUnit||q.unitName===this.ledgerUnit)&&(!child||(q.childName||'').includes(child))
&&(!this.ledgerReviewStatus||q.status===this.ledgerReviewStatus)
&&(this.ledgerState==='all'||this.ledgerBenefitStatus(q)===this.ledgerState));
},
visibleAmountColumns(){return this.amountColumns.filter(c=>c.key!=='pending');},
pagedSettlementRows(){const start=(this.settlementPage-1)*this.settlementPageSize;return this.filteredRows.slice(start,start+this.settlementPageSize);},
units(){return [...new Set(this.rows.map(r=>r.unitName).filter(Boolean))];},
filteredRows(){return this.rows.filter(r=>(!this.unit || r.unitName===this.unit)&&(!this.keyword || (r.userName+r.loginName).includes(this.keyword)));},
confirmed(){return this.batch && this.batch.status!=='草稿';},
canConfirm(){return this.batch && this.batch.status==='草稿' && this.rows.length && this.rows.every(r=>this.completeAmounts(r));},
changes(){return this.filteredRows.map(r=>Object.assign({},r,{deduction:this.noticeDeduction(r)})).filter(r=>{const stop=this.endMonth(r)>=this.periodStart&&this.endMonth(r)<=this.periodEnd;return (stop||r.deduction>0)&&(!this.changeType||(this.changeType==='停发'?stop:r.deduction>0));});},
changeTotals(){return {before:this.changes.length*10,after:this.changes.reduce((n,r)=>n+(this.endMonth(r)<=this.periodEnd?0:10),0),deduction:this.changes.some(r=>r.deduction===null)?null:this.changes.reduce((n,r)=>n+Math.round(r.deduction*100),0)/100};},
recoveryRows(){const ids=this.filteredRows.map(r=>r.userId);return this.recoveries.filter(r=>r.month>=this.periodStart&&r.month<=this.periodEnd&&ids.includes(r.userId));},
retirementUnits(){return [...new Set(this.retirements.map(r=>r.unitName).filter(Boolean))];},
retirementRows(){return this.retirements.filter(r=>(!this.retireYear||this.date(r.retireTime).startsWith(this.retireYear))&&(!this.retireUnit||r.unitName===this.retireUnit)&&(!this.retireKeyword||(r.userName+r.loginName).includes(this.retireKeyword))&&(!this.approval||(this.approval==='已通过')===r.approved)&&(!this.paidStatus||(this.paidStatus==='已发放')===!!r.payment));},
dialogTitle(){return {review:'资格核验',stop:'办理停发 / 资格终止',payment:'登记逐月实发',recover:'登记实际扣回',reward:'登记退休奖励发放'}[this.action];}
},
watch:{
unit(){this.changeSettlementPage(1);},
keyword(){this.changeSettlementPage(1);},
// 刷新后保留当前页;数据减少时回到有效页,避免出现空白页。
rows(){this.settlementPage=Math.min(this.settlementPage,Math.max(1,Math.ceil(this.filteredRows.length/this.settlementPageSize)));}
},
methods:{
// 通知单保留清算期间的扣发总额,已执行扣回只影响余额,不冲减通知单金额。
noticeDeduction(row){
if(!Array.isArray(row.months)||!row.months.length)return null;
let cents=0;
for(const m of row.months){
if(m.month<this.periodStart||m.month>this.periodEnd)continue;
const paid=this.amount(m.paid),due=this.amount(m.due);
if(paid===null||due===null)return null;
cents+=Math.max(0,Math.round(paid*100)-Math.round(due*100));
}
return cents/100;
},
noticeStopRange(row){const end=this.endMonth(row);return end&&end<=this.periodEnd?end.replace('-','.')+'-'+this.year+'.10':'—';},
changeSettlementPage(page){this.settlementPage=page;this.expandedRowKeys=[];},
changeSettlementSize(size){this.settlementPageSize=size;this.changeSettlementPage(1);},
resetLedgerFilters(){this.ledgerKeyword='';this.ledgerUnit='';this.ledgerChild='';this.ledgerReviewStatus='';this.ledgerState='all';},
// 待遇状态按当前月份及终止信息判断,与资料审核状态分别筛选。
ledgerBenefitStatus(row){
const now=moment().format('YYYY-MM'),end=this.endMonth(row);
if(row.status==='已停发'||row.eligible===false||(row.startMonth&&end&&end<=now))return 'stopped';
return row.status==='已核定'&&row.startMonth&&row.startMonth<=now?'active':'pending';
},
openCorrection(row){this.selectedChange=row;this.changeReview={id:row.id,status:'已批准',basis:'',verified:false,effectiveMonth:''};this.changeDialog=true;},
submitCorrectionReview(){this.perform(async()=>{await this.request('reviewChildChange',{data:JSON.stringify(this.changeReview)});this.changeDialog=false;await this.context();this.corrections=await this.request('childChanges');this.$message.success('更正核验结果已保存');});},
async request(path,data={}){
let r;
try { r=await this.$axios.post('/platform/childManage/onlyChildFee/'+path,data); }
// 全局拦截器已提示业务或网络错误,页面只中断后续操作,避免重复弹窗。
catch(e){throw {handled:true,cause:e};}
if(r.code!==0)throw new Error(r.msg||'操作未完成');
return r.data;
},
async perform(work){if(this.busy)return;this.busy=true;try{await work();}catch(e){if(!e || !e.handled)this.$message.error((e && (e.msg||e.message))||'操作未完成');}finally{this.busy=false;}},
async context(){const d=await this.request('context');this.tabs=d.tabs||[];this.ready=true;if(!this.tab){const requested=GetQueryString('tab');const target=this.tabs.includes(requested)?requested:this.tabs[0];if(target)this.go('/platform/childManage/onlyChildFee/'+target+'/index');return;}this.profile=d.profile||{};this.children=d.children||[];this.qualifications=d.qualifications||[];this.form=d.mine||{proof:[],onlyChild:null,childId:'',relationship:'',certificateNumber:''};},
save(submit){this.perform(async()=>{await this.request('save',{data:JSON.stringify(this.form),submit});await this.context();this.$message.success(submit?'已提交核验':'草稿已保存');});},
async fetchTab(){if(this.tab==='ledger'){await this.context();this.corrections=await this.request('childChanges');}else if(this.tab==='retirement')await this.fetchRetirement();else if(['settlement','changes'].includes(this.tab))await this.fetchSettlement();},
async fetchSettlement(){const d=await this.request('settlement',{year:this.year});this.batch=d.batch;this.rows=d.rows.map(row=>Object.assign({},row,{actualStartMonth:row.actualStartMonth||'',savedActualStartMonth:row.actualStartMonth||''}));this.recoveries=d.recoveries;this.expandedRowKeys=[];},
// 选择仅作为待确认值;取消或保存失败时继续显示已保存月份。
savePaymentStart(row){
const selected=row.actualStartMonth,previous=row.savedActualStartMonth;
row.actualStartMonth=previous;
if(!selected||selected===previous)return;
this.perform(async()=>{
try{await this.$confirm('将首次实际发放月份设为 '+selected+'。更改后,各年度未确认的实发记录会重新计算,停发当月起默认实发为0;单月修订和已确认快照保留。是否确认?','确认起发月份',{type:'warning',confirmButtonText:'确认保存',cancelButtonText:'取消'});}
catch(e){return;}
await this.request('paymentStart',{data:JSON.stringify({userId:row.userId,year:Number(this.year),actualStartMonth:selected})});
await this.fetchSettlement();this.expandedRowKeys=[row.userId];
this.$message.success('首次实际发放月份已保存,各年度共用,未确认清算草稿已更新');
});
},
loadSettlement(){this.perform(()=>this.fetchSettlement());},
makeBatch(confirm){const run=()=>this.perform(async()=>{await this.request('batch',{year:this.year,confirm});await this.fetchSettlement();this.$message.success(confirm?'批次已确认并保存快照':'草稿已生成');});if(confirm)this.$confirm('确认当前全部单位 '+this.periodLabel+' 的逐月依据?确认后快照不可修改。','复核确认').then(run).catch(()=>{});else run();},
async fetchRetirement(){this.retirements=await this.request('retirement');this.retireDetail=null;},
loadRetirement(){this.perform(()=>this.fetchRetirement());},
reset(){this.unit='';this.keyword='';this.changeType='';this.loadSettlement();},
open(action,row){this.action=action;this.editing=row;this.entry={userId:row.userId,id:row.id,status:'已核定',verified:false,startMonth:row.startMonth||'',basis:'',endMonth:row.stopMonth||'',month:'',paid:undefined,source:'',amount:undefined,executionMonth:'',voucher:'',historical:false,historyChecked:false,historyBasis:'',paidDate:'',settledAll:true,recoveryMonths:(row.months||[]).map(m=>({month:m.month,remaining:this.remaining(Object.assign({},row,{months:[m]})),executionMonth:'',amount:undefined})).filter(m=>m.remaining>0)};this.dialog=true;},
submitEntry(){this.perform(async()=>{
if(this.action==='recover')this.entry.year=Number(this.year);
const payload=Object.assign({},this.entry);
if(this.action==='recover')payload.recoveryMonths=this.entry.settledAll?[]:this.entry.recoveryMonths.filter(m=>m.executionMonth||m.amount!==undefined&&m.amount!==null);
await this.request(this.action,{data:JSON.stringify(payload)});this.dialog=false;await this.context();
if(['settlement','changes'].includes(this.tab)){
await this.fetchSettlement();
// 补录后展开办理人员,直接呈现更新后的逐月记录。
if(this.action==='payment'&&this.rows.some(r=>r.userId===this.entry.userId))this.expandedRowKeys=[this.entry.userId];
}
if(this.tab==='retirement')await this.fetchRetirement();
this.$message.success(this.action==='payment'?this.entry.month+' 实发记录已保存('+this.cash(this.entry.paid)+'元)':'办理结果已保存');
});},
// 起发前、发放期间及停发后均可能多发,登记入口只按尚未扣回余额判断。
canRegisterRecovery(row){return this.remaining(row)>0;},
recoveryStatus(row){return this.completeAmounts(row)&&this.remaining(row)===0&&(Number(row.pending)>0||Number(row.recovered)>0)?'已结清':row.status;},
// 汇总全部筛选结果,不受分页影响;缺失实发整行不参与金额汇总。
summary({columns}){const valid=this.filteredRows.filter(r=>this.completeAmounts(r));return columns.map(c=>c.property==='loginName'?'筛选结果合计':this.amountColumns.some(a=>a.key===c.property)?(valid.length?valid.reduce((sum,r)=>sum+this.amount(r[c.property]),0).toFixed(2):'—'):'');},
// 展开新行时替换唯一的展开键,重复点击当前行则收起。
expandSettlementRow(row,expandedRows){this.expandedRowKeys=expandedRows.some(r=>r.userId===row.userId)?[row.userId]:[];},
// 空值、空字符串及非数值不能视作实发零元,也不能进入金额汇总。
amount(value){if(typeof value!=='number'&&(typeof value!=='string'||!value.trim()))return null;const n=Number(value);return Number.isFinite(n)?n:null;},
completeAmounts(row){return row.status!=='资料不全'&&this.amountColumns.every(c=>this.amount(row[c.key])!==null);},
// 执行余额按实际扣回记录计算,确认快照的应扣金额保持不变。
remaining(row){
if(!Array.isArray(row.months)||!row.months.length||!this.completeAmounts(row))return null;
let total=0;
for(const m of row.months){
const paid=this.amount(m.paid),due=this.amount(m.due);
const backs=this.recoveries.filter(r=>r.userId===row.userId&&r.month===m.month).map(r=>this.amount(r.amount));
if(paid===null||due===null||backs.some(n=>n===null))return null;
total+=Math.max(0,Math.round((paid-due-backs.reduce((sum,n)=>sum+n,0))*100)/100);
}
return total;
},
exportRewards(){window.location.href='/platform/childManage/onlyChildFee/exportReward?'+new URLSearchParams({year:this.retireYear||'',unit:this.retireUnit,keyword:this.retireKeyword,approval:this.approval,paidStatus:this.paidStatus}).toString();},
exportChanges(){window.location.href='/platform/childManage/onlyChildFee/export?'+new URLSearchParams({year:this.year,unit:this.unit,keyword:this.keyword,type:this.changeType}).toString();},
endMonth(row){return row.endMonth && row.endMonth<row.stopMonth?row.endMonth:row.stopMonth;},
person(id){const r=this.qualifications.concat(this.rows).find(q=>q.userId===id);return r?r.userName+' / '+r.loginName:id;},
cash(n){const value=this.amount(n);return value===null?'待补充':value.toFixed(2);},date(v){return v?moment(v).format('YYYY-MM-DD'):'';},go(url){commonUtil.pjaxPush(url);}
},
created(){this.perform(async()=>{await this.context();if(this.tabs.includes(this.tab))await this.fetchTab();});}
});
}
@@ -0,0 +1,48 @@
<!--# layout("/layouts/platform.html"){ #-->
<div id="app" v-cloak class="only-child-fee" v-loading="busy">
<el-card shadow="never">
<el-empty v-if="ready && !tabs.includes(tab)" description="暂无当前页面的访问权限"></el-empty>
<template v-if="tabs.includes(tab)">
<h3>教职工信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{profile.username}}</el-descriptions-item>
<el-descriptions-item label="工号">{{profile.loginname}}</el-descriptions-item>
<el-descriptions-item label="所属工会">{{profile.unionName}}</el-descriptions-item>
<el-descriptions-item label="所在单位">{{profile.unitName}}</el-descriptions-item>
</el-descriptions>
<h3>子女与资格信息</h3>
<el-form label-width="145px" :disabled="!editable" class="fee-grid">
<el-form-item label="关联子女" required>
<el-select v-model="form.childId" placeholder="请选择已提交子女" style="width:100%">
<el-option v-for="c in children" :key="c.id" :value="c.id" :label="c.childrenName + '(' + date(c.childrenBirthday) + ')'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="出生日期"><el-input :value="childBirthday" readonly></el-input></el-form-item>
<el-form-item label="是否独生子女" required><el-select v-model="form.onlyChild"><el-option label="是" :value="true"></el-option><el-option label="否" :value="false"></el-option></el-select></el-form-item>
<el-form-item label="独生子女证号" required><el-input v-model="form.certificateNumber" maxlength="100"></el-input></el-form-item>
<el-form-item label="与子女关系" required><el-select v-model="form.relationship"><el-option v-for="r in ['父亲','母亲','其他法定监护人']" :key="r" :value="r"></el-option></el-select></el-form-item>
<el-form-item label="证明材料" required>
<file-upload v-if="editable" :value.sync="form.proof" :upload_number="5" upload_result_type="url" complete_result upload_result_category="array"></file-upload>
<file-preview v-else :files="form.proof" complete_result compact></file-preview>
</el-form-item>
</el-form>
<h3>待遇核定</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="领取主体">教职工本人(双职工分别核定)</el-descriptions-item>
<el-descriptions-item label="每月标准">10.00元</el-descriptions-item>
<el-descriptions-item label="起发月份">{{form.startMonth || '待独立核定'}}</el-descriptions-item>
<el-descriptions-item label="年龄停发月份">{{form.stopMonth || '核验后计算'}}</el-descriptions-item>
<el-descriptions-item label="资格状态">{{form.status || '未申请'}}</el-descriptions-item>
<el-descriptions-item label="核定依据 / 退回原因">{{form.basis || '—'}}</el-descriptions-item>
</el-descriptions>
<div class="fee-actions"><el-button @click="go('/platform/childManage/mine/index')">维护子女资料</el-button><el-button v-if="editable" @click="save(false)" :disabled="busy">保存草稿</el-button><el-button v-if="editable" type="primary" @click="save(true)" :disabled="busy">提交核定</el-button></div>
</template>
</el-card>
<!--#include('dialogs.html'){}#-->
</div>
<script>
<!--#include('page.js'){}#-->
initOnlyChildFeePage('qualification');
</script>
<!--#include('style.html'){}#-->
<!--# } #-->
@@ -0,0 +1,32 @@
<!--# layout("/layouts/platform.html"){ #-->
<div id="app" v-cloak class="only-child-fee" v-loading="busy">
<el-card shadow="never">
<el-empty v-if="ready && !tabs.includes(tab)" description="暂无当前页面的访问权限"></el-empty>
<template v-if="tabs.includes(tab)">
<!-- <el-alert title="退休奖励是否限制子女年龄待确认,不自动套用月费停发规则。一次性3000元,审批通过且核实未重复领取后登记实际发放。" type="warning" :closable="false" show-icon></el-alert>-->
<template v-if="tabs.includes('retirement')"><search @search="loadRetirement">
<search-item label="年度"><el-date-picker v-model="retireYear" type="year" value-format="yyyy" placeholder="全部年度" clearable></el-date-picker></search-item>
<search-item label="所属单位"><el-select v-model="retireUnit" clearable placeholder="全部单位"><el-option v-for="u in retirementUnits" :key="u" :value="u"></el-option></el-select></search-item>
<search-item label="姓名 / 工号"><el-input v-model="retireKeyword" placeholder="姓名 / 工号" clearable @keyup.enter.native="loadRetirement"></el-input></search-item>
<search-item label="审核状态"><el-select v-model="approval" clearable placeholder="全部审核状态"><el-option value="已通过"></el-option><el-option value="未通过审批"></el-option></el-select></search-item>
<search-item label="发放状态"><el-select v-model="paidStatus" clearable placeholder="全部发放状态"><el-option value="已发放"></el-option><el-option value="未发放"></el-option></el-select></search-item>
<template slot="actions"><el-button @click="retireYear='';retireUnit='';retireKeyword='';approval='';paidStatus=''">重置</el-button></template>
</search>
<table-tool label="退休奖励申请" class="fee-standard-toolbar">
<el-button type="primary" @click="exportRewards">导出奖励名单</el-button>
<el-button @click="go('/platform/dsznfmtx/apply/index')">进入原申请模块</el-button>
<el-button @click="go('/platform/dsznfmtx/mine/index')">查看原申请及审批流程</el-button>
</table-tool>
<el-table :data="retirementRows" border><el-table-column prop="loginName" label="工号"></el-table-column><el-table-column prop="userName" label="姓名"></el-table-column><el-table-column prop="unitName" label="单位"></el-table-column><el-table-column label="退休日期"><template slot-scope="{row}">{{date(row.retireTime)}}</template></el-table-column><el-table-column label="奖励金额"><template slot-scope="scope">3000.00</template></el-table-column><el-table-column label="审核状态"><template slot-scope="{row}">{{row.approved ? '已通过' : '未通过审批'}}</template></el-table-column><el-table-column label="发放状态"><template slot-scope="{row}">{{row.payment ? '已发放' : '未发放'}}</template></el-table-column><el-table-column label="操作"><template slot-scope="{row}"><el-button type="text" @click="retireDetail=row">查看申请</el-button><el-button type="text" :disabled="!row.approved || !!row.payment" @click="open('reward',row)">登记发放</el-button></template></el-table-column></el-table>
<template v-if="retireDetail"><div class="fee-record-card"><h3>{{retireDetail.userName}} · 申请详情</h3><el-button v-if="retireDetail.instanceId" type="text" @click="$refs.retireChart.onOpenFull(retireDetail.instanceProcessDefineId,retireDetail.instanceId)">查看原审批流程</el-button><el-descriptions :column="2" border><el-descriptions-item label="子女出生日期">{{date(retireDetail.childrenBirthday)}}</el-descriptions-item><el-descriptions-item label="子女年龄条件">待确认</el-descriptions-item><el-descriptions-item label="独生子女证号">{{retireDetail.childrenGraceNumber}}</el-descriptions-item><el-descriptions-item label="退休证"><file-preview :files="retireDetail.retireFiles" complete_result compact></file-preview></el-descriptions-item><el-descriptions-item label="独生子女证明"><file-preview :files="retireDetail.honorFiles" complete_result compact></file-preview></el-descriptions-item><el-descriptions-item label="历史核查依据">{{retireDetail.payment ? retireDetail.payment.historyBasis : '待核实'}}</el-descriptions-item></el-descriptions><h3>实际发放记录</h3><el-descriptions :column="3" border><el-descriptions-item label="发放金额">{{retireDetail.payment ? retireDetail.payment.amount : '—'}}</el-descriptions-item><el-descriptions-item label="发放日期">{{retireDetail.payment ? retireDetail.payment.paidDate : '—'}}</el-descriptions-item><el-descriptions-item label="发放凭据">{{retireDetail.payment ? retireDetail.payment.voucher : '—'}}</el-descriptions-item></el-descriptions></div></template>
</template>
</template>
</el-card>
<!--#include('dialogs.html'){}#-->
</div>
<script>
<!--#include('page.js'){}#-->
initOnlyChildFeePage('retirement');
</script>
<!--#include('style.html'){}#-->
<!--# } #-->
@@ -0,0 +1,33 @@
<!--# layout("/layouts/platform.html"){ #-->
<div id="app" v-cloak class="only-child-fee" v-loading="busy">
<el-card shadow="never">
<el-empty v-if="ready && !tabs.includes(tab)" description="暂无当前页面的访问权限"></el-empty>
<template v-if="tabs.includes(tab)">
<div class="fee-filter"><label>年度 <el-date-picker v-model="year" type="year" value-format="yyyy" :clearable="false" @change="loadSettlement"></el-date-picker></label><el-select v-model="unit" clearable placeholder="全部单位"><el-option v-for="u in units" :key="u" :value="u"></el-option></el-select><el-input v-model="keyword" placeholder="姓名 / 工号" clearable></el-input><el-button type="primary" @click="loadSettlement">查询</el-button><el-button @click="reset">重置</el-button></div>
<div class="fee-recovery-summary fee-period-summary">
<div class="fee-period-item"><span class="fee-period-label">统计月份</span><strong class="fee-period-value">11月</strong></div>
<div class="fee-period-item"><span class="fee-period-label">核算期间</span><strong class="fee-period-value">{{periodLabel}}</strong></div>
<div class="fee-period-item"><span class="fee-period-label">批次</span><strong class="fee-period-value">{{batch ? batch.id : '尚未生成'}}</strong></div>
<div class="fee-period-item"><span class="fee-period-label">状态</span><strong class="fee-period-value">{{batch ? batch.status : '尚未生成'}}</strong></div>
</div>
<!-- <el-alert title="起发月份为第一笔费用实际开始发放的月份,一次登记、各年度共用。更改须确认后生效,未确认年度按起发前0元、起发后每月10元重算;单月修订及已确认快照保留。" type="info" :closable="false" show-icon></el-alert>-->
<div class="fee-actions"><el-button type="primary" :disabled="batch && batch.status !== '草稿'" @click="makeBatch(false)">生成清算草稿</el-button><el-button :disabled="!canConfirm" @click="makeBatch(true)">复核确认</el-button><el-button :disabled="!confirmed" @click="exportChanges">导出变动表</el-button></div>
<el-table :data="pagedSettlementRows" row-key="userId" :expand-row-keys="expandedRowKeys" @expand-change="expandSettlementRow" border show-summary :summary-method="summary">
<el-table-column type="expand" width="48"><template slot-scope="{row: detail}"><h3>{{detail.userName}} · 逐月核算</h3><el-table :data="detail.months" border><el-table-column prop="month" label="月份"></el-table-column><el-table-column v-for="c in visibleAmountColumns" :key="c.key" :label="c.label"><template slot-scope="{row}">{{cash(row[c.key])}}</template></el-table-column><el-table-column prop="source" label="实发来源"></el-table-column><el-table-column prop="note" label="备注"></el-table-column></el-table></template></el-table-column>
<el-table-column prop="loginName" label="工号" min-width="120"></el-table-column><el-table-column prop="userName" label="姓名" min-width="100"></el-table-column><el-table-column prop="birthday" label="子女出生日期" width="120"></el-table-column><el-table-column prop="actualStartMonth" label="起发月份" width="180"><template slot-scope="{row}"><span v-if="confirmed">{{row.actualStartMonth || '未设置'}}</span><template v-else><el-date-picker v-model="row.actualStartMonth" @change="savePaymentStart(row)" type="month" value-format="yyyy-MM" placeholder="实际起发月份" :clearable="false" :disabled="busy" size="small" style="width:150px"></el-date-picker></template></template></el-table-column><el-table-column prop="stopMonth" label="停发起月" min-width="100"></el-table-column>
<el-table-column v-for="c in visibleAmountColumns" :key="c.key" :prop="c.key" :label="c.label" min-width="100"><template slot-scope="{row}">{{cash(row[c.key])}}</template></el-table-column>
<el-table-column label="尚未扣回余额" min-width="130"><template slot-scope="{row}">{{cash(remaining(row))}}</template></el-table-column><el-table-column prop="status" label="状态" min-width="110"><template slot-scope="{row}"><el-tag :type="row.status === '资料不全' ? 'danger' : ['无变动','已结清'].includes(recoveryStatus(row)) ? 'success' : 'warning'">{{recoveryStatus(row)}}</el-tag></template></el-table-column>
<el-table-column label="操作" width="200" fixed="right"><template slot-scope="{row}"><el-button type="text" @click="open('payment',row)" :disabled="confirmed">补录实发</el-button><el-button v-if="canRegisterRecovery(row)" type="text" @click="open('recover',row)">登记扣回</el-button></template></el-table-column>
</el-table>
<div class="fee-pagination"><el-pagination background :current-page="settlementPage" :page-size="settlementPageSize" :page-sizes="[10,20,50,100]" :total="filteredRows.length" layout="total, sizes, prev, pager, next, jumper" @current-change="changeSettlementPage" @size-change="changeSettlementSize"></el-pagination></div>
</template>
</el-card>
<!--#include('dialogs.html'){}#-->
</div>
<script>
<!--#include('page.js'){}#-->
initOnlyChildFeePage('settlement');
</script>
<!--#include('style.html'){}#-->
<!--# } #-->
@@ -0,0 +1,24 @@
<style>
.only-child-fee h2,.only-child-fee h3{color:#0075cf}.only-child-fee h3{border-left:5px solid #0086ed;padding-left:12px;margin:22px 0 14px}.only-child-fee .fee-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding:20px 12px 0;border:1px solid #e1e6ed}.only-child-fee .fee-filter{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:20px;background:#f4f7fa;margin-bottom:15px}.only-child-fee .fee-filter .el-input{width:180px}.only-child-fee .fee-actions{display:flex;justify-content:flex-end;align-items:center;gap:12px;flex-wrap:wrap;margin:16px 0}.only-child-fee .fee-paper{border:1px solid #dce3ec;padding:28px;margin-bottom:12px}.only-child-fee .fee-paper h2{text-align:center;color:#182333}.only-child-fee .fee-sign{display:flex;justify-content:space-between;margin:50px 20px 20px}.only-child-fee small{float:right;font-size:13px;color:#9099aa}.only-child-fee .el-table th{background:#edf2f7}.only-child-fee .el-button:active{transform:scale(.98)}@media(max-width:900px){.only-child-fee .fee-grid{grid-template-columns:1fr}.only-child-fee .fee-paper{padding:12px}.only-child-fee .fee-sign{gap:20px;flex-wrap:wrap}}@media(prefers-reduced-motion:reduce){.only-child-fee .el-button:active{transform:none}}
.only-child-fee .fee-recovery-summary{display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap;padding:18px 20px;margin-bottom:26px;background:#f5f8fc;border:1px solid #e1e8f0;border-radius:6px}
.only-child-fee .fee-recovery-person{min-width:0;overflow-wrap:anywhere}
.only-child-fee .fee-recovery-name{font-size:16px;font-weight:600;color:#303133;margin-right:12px}
.only-child-fee .fee-recovery-id{display:inline-block;font-size:13px;color:#606266}
.only-child-fee .fee-recovery-caption{font-size:12px;line-height:20px;color:#606266;margin-top:6px}
.only-child-fee .fee-recovery-balance{text-align:right;margin-left:auto}
.only-child-fee .fee-recovery-balance strong{font-size:26px;line-height:36px;font-weight:600;color:#b75b00;font-variant-numeric:tabular-nums}
.only-child-fee .fee-recovery-unit{margin-left:6px;font-size:13px;color:#606266}
.only-child-fee .fee-field-help{margin-top:6px;font-size:12px;line-height:20px;color:#606266;overflow-wrap:anywhere}
.only-child-fee .fee-payment-start .fee-recovery-caption,.only-child-fee .fee-payment-start strong{color:#268545}
.only-child-fee .fee-recovery-month{padding:12px 0;border-top:1px solid #ebeef5}.only-child-fee .fee-recovery-month-title{margin-bottom:14px;color:#606266;font-weight:600}
.only-child-fee .fee-help-icon{margin-left:6px;color:#909399;font-size:14px;cursor:help;vertical-align:middle}.only-child-fee .fee-help-icon:hover,.only-child-fee .fee-help-icon:focus{color:#0075cf}.fee-help-popper{max-width:320px;line-height:1.6}
.only-child-fee .fee-period-summary{justify-content:flex-start;gap:20px 48px;margin-bottom:15px}.only-child-fee .fee-period-item{display:flex;flex-direction:column;gap:6px;min-width:0}.only-child-fee .fee-period-label{font-size:12px;line-height:20px;color:#606266}.only-child-fee .fee-period-value{font-size:20px;line-height:28px;font-weight:600;color:#0075cf;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.only-child-fee .fee-record-card,.only-child-fee .fee-form-card{padding:18px 20px;margin:16px 0 24px;background:#f5f8fc;border:1px solid #e1e8f0;border-radius:6px}
.only-child-fee .fee-record-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 24px}.only-child-fee .fee-record-fields>div{min-width:0;overflow-wrap:anywhere}.only-child-fee .fee-record-label{display:block;margin-bottom:6px;color:#606266;font-size:12px}.only-child-fee .fee-record-wide{grid-column:1/-1}
.only-child-fee .fee-record-state strong{font-size:20px;color:#0075cf}.only-child-fee .fee-form-card .el-form-item:last-child{margin-bottom:0}.only-child-fee .fee-record-card .el-descriptions__body{background:transparent}.only-child-fee .fee-record-card>h3:first-child{margin-top:0}
@media(max-width:600px){.only-child-fee .fee-record-fields{grid-template-columns:1fr}.only-child-fee .fee-record-card,.only-child-fee .fee-form-card{padding:16px}}
.only-child-fee .fee-pagination{display:flex;justify-content:flex-end;margin-top:20px;overflow-x:auto;padding-bottom:4px}
.only-child-fee .fee-standard-toolbar{margin:16px 0}.only-child-fee .fee-standard-toolbar .ele-space{display:flex;align-items:center;flex-wrap:wrap;gap:12px}.only-child-fee .fee-standard-toolbar .el-button+.el-button{margin-left:0}
.only-child-fee .fee-notice-paper{overflow-x:auto}.only-child-fee .fee-notice-table{width:100%;min-width:900px;border-collapse:collapse;table-layout:fixed;color:#000;background:#fff;font-family:SimSun,serif;font-size:14px}
.only-child-fee .fee-notice-table th,.only-child-fee .fee-notice-table td{border:1px solid #000;padding:8px 6px;text-align:center;vertical-align:middle;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.only-child-fee .fee-notice-table .fee-notice-title{font-size:22px;padding:12px 6px}.only-child-fee .fee-notice-table .fee-notice-recipient{text-align:left}.only-child-fee .fee-notice-table .fee-notice-unit{text-align:right;padding-right:28px}.only-child-fee .fee-notice-table tfoot td{text-align:left;height:64px;padding-left:24px;border:0}
</style>
@@ -5,13 +5,14 @@ layout("/layouts/platform.html"){
<div id="app" v-cloak>
<el-card shadow="never">
<h3 style="color: var(--color-primary)" slot="header">信息填报</h3>
<el-alert v-if="formData.qualificationLocked" title="该子女已关联待核验、已核定或已停发的独生子女费资格,姓名、身份证号、生日、性别及监护人身份和关系不可直接修改;联系电话、学校、年级等日常信息仍可维护。" type="info" :closable="false" show-icon class="mb10"></el-alert>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":"
class="flow-task-form">
<table-tool label="子女信息"></table-tool>
<el-descriptions :column="2" border>
<el-descriptions-item label="就读年级">
<el-form-item prop="childRelationship">
<dict-select v-model="formData.childRelationship" code="CHILD_MANAGE_GRADE" style="width: 100%" placeholder="请选择就读年级">
<el-form-item prop="childrenGrade">
<dict-select v-model="formData.childrenGrade" code="CHILD_MANAGE_GRADE" style="width: 100%" placeholder="请选择就读年级">
</dict-select>
</el-form-item></el-descriptions-item>
<el-descriptions-item label="入学年份">
@@ -26,20 +27,20 @@ layout("/layouts/platform.html"){
</el-form-item></el-descriptions-item>
<el-descriptions-item label="姓名">
<el-form-item prop="childrenName">
<el-input v-model="formData.childrenName" placeholder="请输入子女姓名"
<el-input :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenName" placeholder="请输入子女姓名"
maxlength="30"></el-input>
</el-form-item></el-descriptions-item>
<el-descriptions-item label="身份证号">
<el-form-item prop="childrenIdCard">
<el-input v-model="formData.childrenIdCard" placeholder="请输入身份证号"
<el-input :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenIdCard" placeholder="请输入身份证号"
maxlength="19">
<el-button slot="append" icon="el-icon-search" @click="getIsRepeatByIdCard"></el-button>
<el-button :disabled="formData.qualificationLocked && !correctionMode" slot="append" icon="el-icon-search" @click="getIsRepeatByIdCard"></el-button>
</el-input>
</el-form-item></el-descriptions-item>
<el-descriptions-item label="出生日期">
<el-form-item prop="childrenBirthday">
<el-date-picker
v-model="formData.childrenBirthday"
:disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenBirthday"
type="date"
placeholder="请选择子女出生年月"
style="width: 100%;"
@@ -50,7 +51,7 @@ layout("/layouts/platform.html"){
<el-form-item prop="childrenSex">
<el-select clearable placeholder="请选择性别"
style="width: 100%;"
v-model="formData.childrenSex">
:disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenSex">
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
@@ -86,11 +87,16 @@ layout("/layouts/platform.html"){
<el-descriptions :column="2" border>
<el-descriptions-item label="监护人1">
<el-form-item label="监护人1" prop="guardianUserName1">
<el-input v-model="formData.guardianUserName1" readonly></el-input>
<el-input :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianUserName1" readonly></el-input>
<el-select v-if="correctionMode" v-model="formData.guardianRelationship1" placeholder="与子女关系" style="width:100%">
<el-option label="父亲" value="父亲"></el-option>
<el-option label="母亲" value="母亲"></el-option>
<el-option label="其他法定监护人" value="其他法定监护人"></el-option>
</el-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="工号" >
<el-form-item prop="guardianLoginName1">{{formData.guardianLoginName1}}
<el-form-item prop="guardianLoginName1"><el-input v-if="correctionMode" v-model="formData.guardianLoginName1" placeholder="本校教职工工号"></el-input><span v-else>{{formData.guardianLoginName1}}</span>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="手机号码">
@@ -105,12 +111,17 @@ layout("/layouts/platform.html"){
</el-descriptions-item>
<el-descriptions-item label="监护人2">
<el-form-item label="监护人2" prop="guardianUserName2">
<el-input v-model="formData.guardianUserName2" placeholder="请输入监护人姓名" maxlength="30"></el-input>
<el-input :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianUserName2" placeholder="请输入监护人姓名" maxlength="30"></el-input>
<el-select v-if="correctionMode" v-model="formData.guardianRelationship2" placeholder="与子女关系" style="width:100%">
<el-option label="父亲" value="父亲"></el-option>
<el-option label="母亲" value="母亲"></el-option>
<el-option label="其他法定监护人" value="其他法定监护人"></el-option>
</el-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="工号">
<el-form-item prop="guardianLoginName2">
<el-input v-model="formData.guardianLoginName2" placeholder="没有则不填" maxlength="30"></el-input>
<el-input :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianLoginName2" placeholder="没有则不填" maxlength="30"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="手机号码">
@@ -130,8 +141,18 @@ layout("/layouts/platform.html"){
</el-form-item></el-descriptions-item>
</el-descriptions>
</el-form>
<el-form v-if="correctionMode" label-width="150px" class="mt20">
<el-alert title="更正申请仅提交关键身份和监护关系,审批前原资料保持不变。日常资料请退出更正模式后保存。" type="info" :closable="false"></el-alert>
<el-form-item label="更正原因及证明依据" required><el-input v-model="correctionReason" type="textarea" maxlength="2000" placeholder="说明更正原因及可供核验的证明依据"></el-input></el-form-item>
</el-form>
<el-table v-if="formData.correctionHistory && formData.correctionHistory.length" :data="formData.correctionHistory" border class="mt20">
<el-table-column prop="submittedAt" label="更正申请时间"></el-table-column><el-table-column prop="reason" label="更正原因"></el-table-column><el-table-column prop="status" label="办理状态"></el-table-column><el-table-column prop="reviewBasis" label="核验结果"></el-table-column><el-table-column prop="effectiveMonth" label="生效月份"></el-table-column>
</el-table>
<el-row type="flex" justify="end" class="mt20">
<el-button type="primary" plain @click="onSave">保存</el-button>
<el-button v-if="formData.qualificationLocked && !correctionMode" @click="beginCorrection">申请关键资料更正</el-button>
<el-button v-if="correctionMode" @click="cancelCorrection">取消更正</el-button>
<el-button v-if="correctionMode" type="primary" @click="submitCorrection">提交更正申请</el-button>
<el-button v-else type="primary" plain @click="onSave">{{formData.qualificationLocked ? '保存日常资料' : '保存'}}</el-button>
</el-row>
</el-card>
</div>
@@ -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
}
}
@@ -12,11 +12,11 @@ layout("/layouts/platform_h5.html"){
<div id="app">
<van-nav-bar title="子女信息填报" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<!-- 表单容器 -->
<van-form ref="formRef" class="form-container">
<van-notice-bar v-if="formData.qualificationLocked" wrapable :scrollable="false" text="该子女已关联待核验、已核定或已停发的独生子女费资格,关键身份和监护关系不可直接修改;联系电话、学校、年级等日常信息仍可维护。"></van-notice-bar><van-form ref="formRef" class="form-container">
<!-- 子女信息 -->
<van-cell-group title="子女信息" class="form-section">
<van-field label="姓名" :rules="[{ required: true }]" v-model="formData.childrenName" placeholder="请输入姓名" required></van-field>
<van-field label="姓名" :rules="[{ required: true }]" :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenName" placeholder="请输入姓名" required></van-field>
<van-field
v-model="formData.childrenGrade"
name="childrenGrade"
@@ -48,16 +48,16 @@ layout("/layouts/platform_h5.html"){
</van-popup>
<van-field
label="身份证号"
v-model="formData.childrenIdCard"
:disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenIdCard"
placeholder="请输入身份证号"
:rules="formRules.childrenIdCard"
required
></van-field> <van-field
label="出生年月"
v-model="formData.childrenBirthday"
:disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenBirthday"
placeholder="请选择出生年月"
readonly
@click="showBirthdayPicker = true"
@click="(!formData.qualificationLocked || correctionMode) && (showBirthdayPicker = true)"
clickable
></van-field>
<van-popup v-model="showBirthdayPicker" position="bottom">
@@ -74,10 +74,10 @@ layout("/layouts/platform_h5.html"){
<van-field
label="性别"
v-model="formData.childrenSex"
:disabled="formData.qualificationLocked && !correctionMode" v-model="formData.childrenSex"
placeholder="请选择性别"
readonly
@click="showSexPicker = true"
@click="(!formData.qualificationLocked || correctionMode) && (showSexPicker = true)"
clickable
></van-field>
@@ -99,8 +99,8 @@ layout("/layouts/platform_h5.html"){
<!-- 监护人信息 -->
<van-cell-group title="监护人信息" class="form-section">
<van-field label="监护人1" v-model="formData.guardianUserName1" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="工号" v-model="formData.guardianLoginName1" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="监护人1" :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianUserName1" readonly placeholder="读取信息中心数据"></van-field>
<van-field label="监护关系" :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianRelationship1" placeholder="父亲、母亲或其他法定监护人" required maxlength="30"></van-field><van-field label="工号" :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianLoginName1" :readonly="!correctionMode" placeholder="读取信息中心数据"></van-field>
<van-field
label="手机号码"
v-model="formData.guardianMobile1"
@@ -110,10 +110,10 @@ layout("/layouts/platform_h5.html"){
<van-field label="所在单位" v-model="formData.guardianUnitName1" readonly placeholder="读取信息中心数据"></van-field>
<van-field
label="监护人2"
v-model="formData.guardianUserName2"
:disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianUserName2"
placeholder="请输入监护人姓名"
></van-field>
<van-field label="工号" v-model="formData.guardianLoginName2" placeholder="没有则不填" ></van-field>
<van-field label="监护关系" :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianRelationship2" placeholder="父亲、母亲或其他法定监护人" maxlength="30"></van-field><van-field label="工号" :disabled="formData.qualificationLocked && !correctionMode" v-model="formData.guardianLoginName2" placeholder="没有则不填" ></van-field>
<van-field
label="手机号码"
v-model="formData.guardianMobile2"
@@ -132,7 +132,12 @@ layout("/layouts/platform_h5.html"){
<!-- 提交按钮 -->
<div class="form-actions">
<van-button @click="onSave" round type="info">保存申请</van-button>
<van-field v-if="correctionMode" v-model="correctionReason" type="textarea" label="更正原因" placeholder="说明更正原因及证明依据;审批前原资料保持不变" maxlength="2000" required></van-field>
<van-button v-if="formData.qualificationLocked && !correctionMode" @click="beginCorrection" round>申请关键资料更正</van-button>
<van-button v-if="correctionMode" @click="cancelCorrection" round>取消更正</van-button>
<van-button v-if="correctionMode" @click="submitCorrection" round type="info">提交更正申请</van-button>
<van-button v-else @click="onSave" round type="info">{{formData.qualificationLocked ? '保存日常资料' : '保存申请'}}</van-button>
<van-cell v-for="change in formData.correctionHistory || []" :key="change.id" :title="'更正申请:' + change.status" :label="change.reason + ';核验结果:' + (change.reviewBasis || '待核验') + ';生效月份:' + (change.effectiveMonth || '待核定')"></van-cell>
</div>
</van-form>
</div>
@@ -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({