工会报销支付信息修改

This commit is contained in:
2026-09-18 14:39:24 +08:00
parent 5fa9c4c368
commit 89ed1af284
3 changed files with 128 additions and 22 deletions
@@ -24,6 +24,8 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHist
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -116,11 +118,22 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return sql;
}
/**
* 保存、提交及重新提交共用支付信息校验,并在同一事务内保存申请和发票明细。
* @param unionReimburse 申请表单,paymentDetails 为支付方式、金额及对私收款资料组成的明细列表
* @param stateId 1 表示保存草稿,2 表示提交;两种状态均要求支付信息完整
* @return Result 成功或失败结果,msg 包含失败明细的行号及原因
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public Result saveApply(UnionReimburse unionReimburse, int stateId) {
if (unionReimburse == null) {
return Result.error("报销数据不能为空");
}
Result paymentResult = validatePaymentDetails(unionReimburse);
if (paymentResult != null) {
return paymentResult;
}
UnionReimburse oldRecord = null;
if (StrUtil.isNotBlank(unionReimburse.getId())) {
oldRecord = this.fetch(unionReimburse.getId());
@@ -383,7 +396,6 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
seg.or("u.username", "like", "%" + keyword + "%");
cnd.and(seg);
}
cnd.and("u.id", "!=", SecurityUtil.getUserId());
if (clubFundSource) {
cnd.and("cu.clubId", "=", clubId);
} else if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
@@ -668,6 +680,53 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
unionReimburse.setRealMoney(getDeclaredApplyMoney(unionReimburse).doubleValue());
}
/**
* 校验支付明细,防止绕过 PC/H5 校验直接保存。对公只需方式和金额,对私必须补齐人员及银行资料。
* @param form 本次保存的申请,paymentDetails 必须至少包含一条完整支付明细
* @return 校验通过返回 null,失败返回带行号和原因的 Result
*/
private Result validatePaymentDetails(UnionReimburse form) {
if (Lang.isEmpty(form.getPaymentDetails())) {
return Result.error("请至少维护一条支付信息");
}
for (int i = 0; i < form.getPaymentDetails().size(); i++) {
cn.hutool.json.JSONObject detail = form.getPaymentDetails().get(i);
String prefix = "第" + (i + 1) + "条支付信息";
if (detail == null) {
return Result.error(prefix + "不能为空");
}
String way = detail.getStr("paymentWay");
if (!"UNION_REIMBURSE_PAYMENT_WAY_1".equals(way)
&& !"UNION_REIMBURSE_PAYMENT_WAY_2".equals(way)) {
return Result.error(prefix + "请选择有效的支付方式");
}
Object rawMoney = detail.get("money");
String money = rawMoney == null ? "" : rawMoney.toString();
// 先严格检查十进制格式,再比较金额;禁止字母、科学计数法、负数和超出两位的小数。
if (!(rawMoney instanceof String || rawMoney instanceof Number)
|| !money.matches("[0-9]+(\\.[0-9]{1,2})?")
|| new BigDecimal(money).compareTo(BigDecimal.ZERO) <= 0) {
return Result.error(prefix + "金额必须大于0且最多两位小数");
}
if ("UNION_REIMBURSE_PAYMENT_WAY_2".equals(way)) {
if (StrUtil.isBlank(detail.getStr("payer"))) {
return Result.error(prefix + "请选择付款人");
}
if (StrUtil.isBlank(detail.getStr("bankUserName"))) {
return Result.error(prefix + "请填写户名");
}
if (StrUtil.isBlank(detail.getStr("bankOfDeposit"))) {
return Result.error(prefix + "请填写开户行");
}
String card = detail.getStr("bankCardNumber");
if (card == null || !card.matches("[1-9]([0-9]{15}|[0-9]{18})")) {
return Result.error(prefix + "请填写正确的银行卡号");
}
}
}
return null;
}
/**
* 提交前补一层后端校验,防止前端绕过校验直接提交。
*/
@@ -489,7 +489,7 @@ layout("/layouts/platform.html"){
<template slot-scope="scope">
<el-form-item :prop="'paymentDetails.' + scope.$index + '.money'"
:rules="paymentDetailRules.money">
<el-input v-model="scope.row.money" placeholder="请输入金额"></el-input>
<el-input :value="scope.row.money" @input="inputPaymentMoney(scope.row, $event)" inputmode="decimal" placeholder="请输入金额(最多两位小数)"></el-input>
</el-form-item>
</template>
</el-table-column>
@@ -784,13 +784,13 @@ layout("/layouts/platform.html"){
paymentWay: [{required: true, message: "请选择支付方式", trigger: ["change", "blur"]}],
reimburseProject: [{required: true, message: "请选择报销项目", trigger: ["change", "blur"]}],
reimburseFundSource: [{required: true, message: "请选择经费来源", trigger: ["change", "blur"]}],
bankUserName: [{required: true, message: "请填写户名", trigger: ["change", "blur"]}],
bankUserName: [{required: true, whitespace: true, message: "请填写户名", trigger: ["change", "blur"]}],
bankCardNumber: [{
required: true,
message: "请填写银行账号",
trigger: ["change", "blur"]
}, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}],
bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}],
bankOfDeposit: [{required: true, whitespace: true, message: "请填写开户行", trigger: ["change", "blur"]}],
condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}],
// invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}],
// invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}],
@@ -837,15 +837,20 @@ layout("/layouts/platform.html"){
},
paymentDetailRules: {
paymentWay: [{required: true, message: "请选择支付方式", trigger: ["change", "blur"]}],
money: [{required: true, message: "请填写金额", trigger: ["change", "blur"]}],
money: [{required: true, message: "请填写金额", trigger: ["change", "blur"]}, {
validator: (rule, value, callback) => {
callback(this.isValidPaymentMoney(value) ? undefined : new Error("金额必须大于0且最多两位小数"))
},
trigger: ["change", "blur"]
}],
payer: [{required: true, message: "请选择付款人", trigger: ["change", "blur"]}],
bankUserName: [{required: true, message: "请填写户名", trigger: ["change", "blur"]}],
bankUserName: [{required: true, whitespace: true, message: "请填写户名", trigger: ["change", "blur"]}],
bankCardNumber: [{
required: true,
message: "请填写银行卡号",
trigger: ["change", "blur"]
}, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}],
bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}]
bankOfDeposit: [{required: true, whitespace: true, message: "请填写开户行", trigger: ["change", "blur"]}]
},
chooseType: {},
userOptions: [],
@@ -1478,6 +1483,22 @@ layout("/layouts/platform.html"){
this.syncLegacyPaymentFields(submitData.paymentDetails, submitData)
return submitData
},
// 金额输入允许编辑中的空值和末尾小数点;非法字符或超出两位小数时恢复原值,避免改变金额含义。
inputPaymentMoney(row, value) {
const previous = row.money
const text = String(value == null ? "" : value)
this.$set(row, "money", text)
if (!/^\d*(\.\d{0,2})?$/.test(text)) {
this.$nextTick(() => { this.$set(row, "money", previous) })
this.$message.warning("金额只能输入数字,最多两位小数")
}
},
// 保存与提交统一要求完整的正金额,不接受科学计数法、非数字或超过两位小数。
isValidPaymentMoney(value) {
const text = String(value == null ? "" : value)
return /^\d+(\.\d{1,2})?$/.test(text) && Number.isFinite(Number(text)) && Number(text) > 0
},
// 校验每行支付信息;对私支付还需完整的付款人和银行信息,空白文字不能视为已填写。
validatePaymentDetailsBeforeSubmit(paymentDetails) {
if (!paymentDetails.length) {
this.$message.warning("请至少维护一条支付信息")
@@ -1485,24 +1506,24 @@ layout("/layouts/platform.html"){
}
for (let i = 0; i < paymentDetails.length; i++) {
const currentDetail = paymentDetails[i]
if (!currentDetail.paymentWay) {
if (!["UNION_REIMBURSE_PAYMENT_WAY_1", "UNION_REIMBURSE_PAYMENT_WAY_2"].includes(currentDetail.paymentWay)) {
this.$message.warning("第" + (i + 1) + "条支付信息请选择支付方式")
return false
}
if (!currentDetail.money || Number(currentDetail.money) <= 0) {
this.$message.warning("第" + (i + 1) + "条支付信息请填写正确的金额")
if (!this.isValidPaymentMoney(currentDetail.money)) {
this.$message.warning("第" + (i + 1) + "条支付信息金额必须大于0且最多两位小数")
return false
}
if (currentDetail.paymentWay === "UNION_REIMBURSE_PAYMENT_WAY_2") {
if (!currentDetail.payer) {
if (!String(currentDetail.payer || "").trim()) {
this.$message.warning("第" + (i + 1) + "条支付信息请选择付款人")
return false
}
if (!currentDetail.bankUserName) {
if (!String(currentDetail.bankUserName || "").trim()) {
this.$message.warning("第" + (i + 1) + "条支付信息请填写户名")
return false
}
if (!currentDetail.bankOfDeposit) {
if (!String(currentDetail.bankOfDeposit || "").trim()) {
this.$message.warning("第" + (i + 1) + "条支付信息请填写开户行")
return false
}
@@ -1531,6 +1552,11 @@ layout("/layouts/platform.html"){
this.rebuildInvoiceSummary()
this.normalizeFundBalance()
const paymentDetails = this.preparePaymentDetailsForSubmit()
// 保存草稿也必须填写完整支付信息,避免非法金额和空明细入库。
if (!this.validatePaymentDetailsBeforeSubmit(paymentDetails)) {
this.$set(this, "activeTabName", "invoice")
return
}
this.$confirm("您确定保存吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
@@ -858,9 +858,10 @@ layout("/layouts/platform_h5.html"){
></van-field>
<van-field
label="金额"
v-model="item.money"
placeholder="请输入金额"
type="number"
:value="item.money"
@input="inputPaymentMoney(item, $event)"
placeholder="请输入金额(最多两位小数)"
inputmode="decimal"
required
></van-field>
<template v-if="isPrivatePayment(item)">
@@ -2003,6 +2004,22 @@ layout("/layouts/platform_h5.html"){
this.syncLegacyPaymentFields(submitData.paymentDetails, submitData)
return submitData
},
// 金额输入允许编辑中的空值和末尾小数点;非法字符或超出两位小数时恢复原值,避免改变金额含义。
inputPaymentMoney(row, value) {
const previous = row.money
const text = String(value == null ? "" : value)
this.$set(row, "money", text)
if (!/^\d*(\.\d{0,2})?$/.test(text)) {
this.$nextTick(() => { this.$set(row, "money", previous) })
this.$toast.fail("金额只能输入数字,最多两位小数")
}
},
// 保存与提交统一要求完整的正金额,不接受科学计数法、非数字或超过两位小数。
isValidPaymentMoney(value) {
const text = String(value == null ? "" : value)
return /^\d+(\.\d{1,2})?$/.test(text) && Number.isFinite(Number(text)) && Number(text) > 0
},
// 校验每行支付信息;对私支付还需完整的付款人和银行信息,空白文字不能视为已填写。
validatePaymentDetailsBeforeSubmit(paymentDetails) {
if (!paymentDetails.length) {
this.$toast.fail("请至少维护一条支付信息")
@@ -2010,24 +2027,24 @@ layout("/layouts/platform_h5.html"){
}
for (let i = 0; i < paymentDetails.length; i++) {
const currentDetail = paymentDetails[i]
if (!currentDetail.paymentWay) {
if (!["UNION_REIMBURSE_PAYMENT_WAY_1", "UNION_REIMBURSE_PAYMENT_WAY_2"].includes(currentDetail.paymentWay)) {
this.$toast.fail("第" + (i + 1) + "条支付信息请选择支付方式")
return false
}
if (!currentDetail.money || Number(currentDetail.money) <= 0) {
this.$toast.fail("第" + (i + 1) + "条支付信息请填写正确的金额")
if (!this.isValidPaymentMoney(currentDetail.money)) {
this.$toast.fail("第" + (i + 1) + "条支付信息金额必须大于0且最多两位小数")
return false
}
if (currentDetail.paymentWay === "UNION_REIMBURSE_PAYMENT_WAY_2") {
if (!currentDetail.payer) {
if (!String(currentDetail.payer || "").trim()) {
this.$toast.fail("第" + (i + 1) + "条支付信息请选择付款人")
return false
}
if (!currentDetail.bankUserName) {
if (!String(currentDetail.bankUserName || "").trim()) {
this.$toast.fail("第" + (i + 1) + "条支付信息请填写户名")
return false
}
if (!currentDetail.bankOfDeposit) {
if (!String(currentDetail.bankOfDeposit || "").trim()) {
this.$toast.fail("第" + (i + 1) + "条支付信息请填写开户行")
return false
}
@@ -2071,6 +2088,10 @@ layout("/layouts/platform_h5.html"){
this.normalizeCondolenceDateFields()
this.rebuildInvoiceSummary()
const paymentDetails = this.preparePaymentDetailsForSubmit()
// 保存草稿也必须填写完整支付信息,避免非法金额和空明细入库。
if (!this.validatePaymentDetailsBeforeSubmit(paymentDetails)) {
return
}
this.$set(this, "formLoading", true)
this.$axios.post("/platform/unionReimburse/apply/save", { data: JSON.stringify(this.buildSubmitFormData(paymentDetails)) })
.then((res) => {