医疗互助系统-医保报销数据导入和补助台账:新增重新关联人员按钮,方便修复重名导致的匹配错误问题;

This commit is contained in:
2026-10-10 14:29:48 +08:00
parent 03864ea416
commit 4ca2b73d7b
4 changed files with 419 additions and 3 deletions
@@ -12,12 +12,16 @@ import cn.hutool.core.util.StrUtil;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.modules.models.PageForm;
import io.v.nutz.sickHelp.constant.SickHelpStateEnum;
import io.v.nutz.sickHelp.model.SickFundMember;
import io.v.nutz.sickHelp.model.SickHelp;
import io.v.nutz.sickHelp.model.SickHelpMedicalIns;
import io.v.nutz.sickHelp.service.SickHelpImportService;
import io.v.nutz.sys.models.User;
import io.v.nutz.sys.services.SysUserService;
import io.v.nutz.util.ViTool;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
@@ -371,6 +375,196 @@ public class SickHelpMedicalInsController {
return Result.success(list);
}
/**
* 查询可重新关联的大病互助会员。
*/
@At
@RequiresPermissions(value = {"sickHelp.medicalIns", "sickHelp.list"}, logical = Logical.OR)
public Result queryRebindUser(String query) {
Sql sql = Sqls.create("""
SELECT
m.loginName AS loginname,
m.userName AS username,
COALESCE(NULLIF(u.birthday, ''), m.birthday) AS birthday,
COALESCE(NULLIF(u.unitname, ''), m.unitName) AS unitname,
COALESCE(NULLIF(u.idcard, ''), m.idcard) AS idcard
FROM sick_fund_member m
LEFT JOIN `user` u ON u.loginname = m.loginName
$condition
LIMIT 0,20
""");
Cnd cnd = Cnd.NEW();
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("m.loginName", query);
seg.orLike("m.userName", query);
seg.orLike("m.idcard", query);
seg.orLike("u.idcard", query);
cnd.and(seg);
sql.setCondition(cnd);
List<NutMap> list = sysUserService.listMap(sql);
return Result.success(list);
}
/**
* 获取重新关联人员所需的医保数据。
*/
@At
@RequiresPermissions(value = {"sickHelp.medicalIns", "sickHelp.list"}, logical = Logical.OR)
public Result findRebindSource(String medicalInsId, Integer year, String loginName) {
SickHelpMedicalIns medicalIns = null;
if (StrUtil.isNotBlank(medicalInsId)) {
medicalIns = dao.fetch(SickHelpMedicalIns.class, medicalInsId);
} else if (year != null && StrUtil.isNotBlank(loginName)) {
List<SickHelpMedicalIns> list = dao.query(SickHelpMedicalIns.class,
Cnd.where("year", "=", year).and("loginName", "=", StrUtil.trim(loginName)));
if (list.size() > 1) {
return Result.error("该年度、工号存在多条医保数据,请先核对");
}
if (!list.isEmpty()) {
medicalIns = list.get(0);
}
}
if (medicalIns == null) {
return Result.error("未找到对应的医保数据");
}
return Result.success(medicalIns);
}
/**
* 重新关联医保人员,并同步处理尚未进入审核流程的补助台账。
*/
@At
@RequiresPermissions(value = {"sickHelp.medicalIns", "sickHelp.list"}, logical = Logical.OR)
@Aop(TransAop.READ_COMMITTED)
public Result rebindUser(String medicalInsId, String loginName, String ecbx) {
if (StrUtil.isBlank(medicalInsId)) {
return Result.error("医保数据id为空");
}
if (StrUtil.isBlank(loginName)) {
return Result.error("请选择需要重新关联的人员");
}
if (StrUtil.isBlank(ecbx)) {
return Result.error("请输入正确的学校二次报销金额");
}
BigDecimal ecbxMoney;
try {
ecbxMoney = new BigDecimal(StrUtil.trim(ecbx));
} catch (NumberFormatException e) {
return Result.error("学校二次报销金额格式不正确");
}
if (ecbxMoney.compareTo(BigDecimal.ZERO) < 0) {
return Result.error("学校二次报销金额不能小于0");
}
SickHelpMedicalIns medicalIns = dao.fetch(SickHelpMedicalIns.class, medicalInsId);
if (medicalIns == null) {
return Result.error("医保数据不存在或已被删除");
}
if (medicalIns.getYear() == null || medicalIns.getSxzf() == null || medicalIns.getLxzf() == null) {
return Result.error("医保年度或金额不完整,请先核对医保数据");
}
if (ecbxMoney.compareTo(medicalIns.getSxzf()) > 0) {
return Result.error("学校二次报销金额不能大于医保四项之和");
}
String targetLoginName = StrUtil.trim(loginName);
SickFundMember targetMember = dao.fetch(SickFundMember.class,
Cnd.where("loginName", "=", targetLoginName));
if (targetMember == null) {
return Result.error("所选人员不在大病互助会员库中");
}
User targetUser = dao.fetch(User.class, Cnd.where("loginname", "=", targetLoginName));
String targetUserName = StrUtil.isNotBlank(targetMember.getUserName())
? targetMember.getUserName()
: targetUser == null ? null : targetUser.getUsername();
if (StrUtil.isBlank(targetUserName)) {
return Result.error("所选人员姓名为空,请先维护人员数据");
}
String targetIdCard = targetUser != null && StrUtil.isNotBlank(targetUser.getIdcard())
? targetUser.getIdcard() : targetMember.getIdcard();
String medicalIdCard = normalizeIdCard(medicalIns.getIdCard());
String normalizedTargetIdCard = normalizeIdCard(targetIdCard);
if (StrUtil.isNotBlank(medicalIdCard) && StrUtil.isNotBlank(normalizedTargetIdCard)
&& !medicalIdCard.equals(normalizedTargetIdCard)) {
return Result.error("医保身份证号与所选人员身份证号不一致,禁止重新关联");
}
SickHelpMedicalIns sameMedicalIns = dao.fetch(SickHelpMedicalIns.class,
Cnd.where("year", "=", medicalIns.getYear())
.and("loginName", "=", targetLoginName)
.and("id", "!=", medicalIns.getId()));
if (sameMedicalIns != null) {
return Result.error("所选人员本年度已有医保数据,禁止重复关联");
}
String oldLoginName = StrUtil.trim(medicalIns.getLoginName());
SickHelp oldSickHelp = StrUtil.isBlank(oldLoginName) ? null
: dao.fetch(SickHelp.class, Cnd.where("year", "=", medicalIns.getYear())
.and("loginName", "=", oldLoginName));
if (oldSickHelp != null
&& (!SickHelpStateEnum.DTJ.getStateCode().equals(oldSickHelp.getStateCode())
|| Boolean.TRUE.equals(oldSickHelp.getIsMeetRequirements())
|| Boolean.TRUE.equals(oldSickHelp.getIsAccept()))) {
return Result.error("原补助记录已进入申请或审核流程,不能直接重新关联人员");
}
SickHelp targetSickHelp = dao.fetch(SickHelp.class,
Cnd.where("year", "=", medicalIns.getYear()).and("loginName", "=", targetLoginName));
if (targetSickHelp != null && (oldSickHelp == null || !targetSickHelp.getId().equals(oldSickHelp.getId()))) {
return Result.error("所选人员本年度已有补助记录,禁止重复关联");
}
BigDecimal selfPayMoney = medicalIns.getSxzf().subtract(ecbxMoney);
SickHelp calculatedSickHelp = null;
if (selfPayMoney.compareTo(BigDecimal.valueOf(10000)) > 0) {
SickHelp sickHelp = new SickHelp();
sickHelp.setYear(medicalIns.getYear());
sickHelp.setLoginName(targetLoginName);
sickHelp.setUserName(targetUserName);
sickHelp.setMedicalFourTotalMoney(medicalIns.getSxzf());
sickHelp.setSelfPayTotalMoney(medicalIns.getLxzf());
sickHelp.setSchoolSecondSubsidyMoney(ecbxMoney);
sickHelp.setSelfPayMoney(selfPayMoney);
calculatedSickHelp = sickHelpImportService.checkData(List.of(sickHelp)).get(0);
if (!Boolean.TRUE.equals(calculatedSickHelp.getValid())) {
return Result.error(String.join("、", calculatedSickHelp.getValidMsg()));
}
}
medicalIns.setLoginName(targetLoginName);
medicalIns.setUserName(targetUserName);
medicalIns.setEcbx(ecbxMoney);
medicalIns.setCalc(calculatedSickHelp == null || calculatedSickHelp.getSubsidyMoney() == null
? BigDecimal.ZERO : calculatedSickHelp.getSubsidyMoney());
dao.update(medicalIns, "loginName|userName|ecbx|calc");
if (oldSickHelp != null) {
dao.delete(SickHelp.class, oldSickHelp.getId());
}
if (calculatedSickHelp != null) {
calculatedSickHelp.setId(null);
calculatedSickHelp.setStateCode(SickHelpStateEnum.DTJ.getStateCode());
dao.insert(calculatedSickHelp);
}
if (calculatedSickHelp == null) {
return Result.success("重新关联成功;修正后的个人支付金额不超过10000元,原补助汇总已删除");
}
return Result.success("重新关联成功;补助汇总已按正确人员重新计算");
}
/**
* 身份证号标准化,避免空格和字母大小写导致匹配失败。
*/
private String normalizeIdCard(String idCard) {
if (StrUtil.isBlank(idCard)) {
return null;
}
return idCard.replaceAll("\\s+", "").toUpperCase();
}
/**
* 检查是否还有错误的医保数据
@@ -181,8 +181,9 @@ layout("/layouts/platform.html"){
<span v-if="row.isMember==null"></span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="200">
<el-table-column fixed="right" label="操作" width="300">
<template scope="{row}">
<el-button @click="onRebind(row)" size="mini" type="warning">重新关联人员</el-button>
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="doDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
@@ -257,12 +258,18 @@ layout("/layouts/platform.html"){
</guava>
<rebind-form ref="rebindFormRef" @refresh="doSearch"></rebind-form>
</div>
<script>
<!--#include("/platform/sickHelp/medicalIns/rebindForm.js"){}#-->
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
components: {
"rebind-form": rebindForm
},
data() {
return {
tableColumns: [
@@ -315,6 +322,10 @@ layout("/layouts/platform.html"){
}
},
methods: {
onRebind(row) {
this.$refs.rebindFormRef.onOpen(row, 'summary')
},
async importData() {
if (this.excelTableData.length === 0) {
this.notifyWarning('无数据')
@@ -198,8 +198,9 @@ layout("/layouts/platform.html"){
v-for="column in tableColumns"
>
</el-table-column>
<el-table-column label="操作" align="center" header-align="center" fixed="right" width="200">
<el-table-column label="操作" align="center" header-align="center" fixed="right" width="300">
<template scope="scope">
<el-button @click="onRebind(scope.row)" size="mini" type="warning">重新关联人员</el-button>
<el-button @click="onEdit(scope.row)" size="mini" type="primary">编辑</el-button>
<el-button @click="onDelete(scope.row.id)" size="mini" type="danger">删除</el-button>
</template>
@@ -211,12 +212,14 @@ layout("/layouts/platform.html"){
<ins-import ref="insImportRef" @refresh="doSearch"></ins-import>
<ec-import ref="ecImportRef" @refresh="doSearch"></ec-import>
<ins-form ref="insFormRef" @refresh="doSearch"></ins-form>
<rebind-form ref="rebindFormRef" @refresh="doSearch"></rebind-form>
</div>
<script>
<!--#include('insImport.js'){}#-->
<!--#include('insForm.js'){}#-->
<!--#include('ecImport.js'){}#-->
<!--#include('rebindForm.js'){}#-->
const vue = new Vue({
el: '#app',
@@ -224,7 +227,8 @@ layout("/layouts/platform.html"){
components: {
"ins-import": insImport,
"ec-import": ecImport,
"ins-form": insForm
"ins-form": insForm,
"rebind-form": rebindForm
},
data() {
return {
@@ -261,6 +265,10 @@ layout("/layouts/platform.html"){
})
},
onRebind(row) {
this.$refs.rebindFormRef.onOpen(row, 'medical')
},
onEdit(row) {
this.$refs.insFormRef.onOpen(row)
},
@@ -0,0 +1,203 @@
const rebindForm = {
template: /*language=HTML*/ `
<el-dialog title="重新关联人员" :visible.sync="visible" :close-on-click-modal="false" width="850px">
<el-alert
title="该操作会同步修正医保数据和尚未进入审核流程的补助汇总,请填写正确人员本人的二次报销金额。"
type="warning"
:closable="false"
show-icon>
</el-alert>
<el-form class="mt20" v-loading="loading" :model="form" :rules="rules" ref="form" label-width="150px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="原姓名">
<el-input :value="source.userName" disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="原工号">
<el-input :value="source.loginName" disabled></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="医保身份证号">
<el-input :value="source.idCard" disabled></el-input>
</el-form-item>
<el-form-item label="重新关联人员" prop="loginName">
<el-select
v-model="form.loginName"
style="width: 100%"
filterable
remote
reserve-keyword
clearable
:loading="userLoading"
:remote-method="remoteMethod"
placeholder="请输入姓名、工号或身份证号查询">
<el-option
v-for="item in userOptions"
:key="item.loginname"
:label="userLabel(item)"
:value="item.loginname">
</el-option>
</el-select>
</el-form-item>
<div v-if="selectedUser" style="margin: -10px 0 18px 150px;color: #606266;line-height: 1.8;">
<div>姓名:{{selectedUser.username}},工号:{{selectedUser.loginname}}</div>
<div>出生日期:{{selectedUser.birthday || '未录入'}},单位:{{selectedUser.unitname || '未录入'}}</div>
<div>身份证号:{{maskIdCard(selectedUser.idcard)}}</div>
<div v-if="!selectedUser.idcard" style="color: #e6a23c;">
该人员未录入身份证号,请结合工号、出生日期和单位人工确认。
</div>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="医保四项之和">
<el-input :value="source.sxzf" disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="正确二次报销金额" prop="ecbx">
<el-input-number
v-model="form.ecbx"
:precision="2"
:min="0"
:controls="false"
:max="source.sxzf === null || source.sxzf === undefined ? 99999999.99 : Number(source.sxzf)"
style="width: 100%">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-alert
v-if="selfPayMoney !== null"
:title="resultMessage"
:type="isEligible ? 'warning' : 'success'"
:closable="false"
show-icon>
</el-alert>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">确认重新关联</el-button>
</div>
</el-dialog>
`,
data() {
return {
visible: false,
loading: false,
saving: false,
userLoading: false,
source: {},
form: {
medicalInsId: null,
loginName: null,
ecbx: null
},
userOptions: [],
rules: {
loginName: [{required: true, message: '请选择需要重新关联的人员', trigger: 'change'}],
ecbx: [{required: true, message: '请输入正确人员的学校二次报销金额', trigger: 'change'}]
}
}
},
computed: {
selectedUser() {
return this.userOptions.find(v => v.loginname === this.form.loginName)
},
selfPayMoney() {
if (this.source.sxzf === null || this.source.sxzf === undefined
|| this.form.ecbx === null || this.form.ecbx === undefined) {
return null
}
return Number(this.source.sxzf) - Number(this.form.ecbx)
},
isEligible() {
return this.selfPayMoney !== null && this.selfPayMoney > 10000
},
resultMessage() {
const money = this.selfPayMoney.toFixed(2)
return this.isEligible
? '扣减二次报销后的个人支付金额为 ' + money + ' 元,系统将按正确人员重新计算补助汇总。'
: '扣减二次报销后的个人支付金额为 ' + money + ' 元,不超过10000元,系统将删除原错误补助汇总。'
}
},
methods: {
async onOpen(row, sourceType) {
this.visible = true
this.loading = true
this.source = {}
this.userOptions = []
this.form = {medicalInsId: null, loginName: null, ecbx: null}
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
const params = sourceType === 'medical'
? {medicalInsId: row.id}
: {year: row.year, loginName: row.loginName}
const resp = await $.post('/platform/sickHelp/medicalIns/findRebindSource', params)
this.loading = false
if (resp.code !== 0) {
this.visible = false
this.$message.error(resp.msg)
return
}
this.source = resp.data
this.form.medicalInsId = resp.data.id
},
async remoteMethod(query) {
if (!query) {
this.userOptions = []
return
}
this.userLoading = true
const resp = await $.post('/platform/sickHelp/medicalIns/queryRebindUser', {query})
this.userLoading = false
if (resp.code === 0) {
this.userOptions = resp.data || []
}
},
userLabel(user) {
return user.username + '(' + user.loginname + ') '
+ (user.birthday || '生日未录入') + ' '
+ (user.unitname || '单位未录入') + ' '
+ this.maskIdCard(user.idcard)
},
maskIdCard(idCard) {
if (!idCard) return '身份证未录入'
const value = idCard.toString().trim()
if (value.length <= 10) return value
return value.substring(0, 6) + '********' + value.substring(value.length - 4)
},
onSave() {
this.$refs.form.validate(async valid => {
if (!valid) return
const tip = this.isEligible
? '确定重新关联人员并按正确人员重新计算补助汇总吗?'
: '修正后不超过10000元,系统会删除原错误补助汇总,确定继续吗?'
await this.$confirm(tip, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
this.saving = true
const resp = await $.post('/platform/sickHelp/medicalIns/rebindUser', this.form)
this.saving = false
if (resp.code === 0) {
this.visible = false
this.$message.success(resp.msg)
this.$emit('refresh')
} else {
this.$message.error(resp.msg)
}
})
}
}
};