From 89ed1af284e6c7bb4a35e05ebee5eff0893fe1c7 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Fri, 18 Sep 2026 14:39:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BC=9A=E6=8A=A5=E9=94=80=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E4=BF=A1=E6=81=AF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/UnionReimburseServiceImpl.java | 61 ++++++++++++++++++- .../unionReimburse/apply/index.html | 50 +++++++++++---- .../unionReimburse/apply/index.html | 39 +++++++++--- 3 files changed, 128 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java index 46d5a051..42b6951e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java @@ -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 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 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 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; + } + /** * 提交前补一层后端校验,防止前端绕过校验直接提交。 */ diff --git a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html index 473972e2..6992381c 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/unionReimburse/apply/index.html @@ -489,7 +489,7 @@ layout("/layouts/platform.html"){ @@ -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: "取消", diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html index fdd34d50..c8dc4f77 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/unionReimburse/apply/index.html @@ -858,9 +858,10 @@ layout("/layouts/platform_h5.html"){ >