From cba3add65fd85fffc626c6a7856a7c0c1fdb930b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E8=AF=9A?= <1009578407@qq.com> Date: Thu, 10 Sep 2026 17:40:35 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=88=86=E5=B7=A5=E4=BC=9A?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8A=A5=E9=94=80=E4=BA=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../budwk/app/base/constant/RoleConstant.java | 1 + .../ActivityReimbursementApplyController.java | 91 +++++++++++++++++++ .../init_union_temp_reimbursement_role.sql | 67 ++++++++++++++ .../reimbursement/apply/index.html | 26 ++++++ .../reimbursement/apply/index.html | 24 +++++ 5 files changed, 209 insertions(+) create mode 100644 src/main/resources/db/menu/init_union_temp_reimbursement_role.sql diff --git a/src/main/java/com/budwk/app/base/constant/RoleConstant.java b/src/main/java/com/budwk/app/base/constant/RoleConstant.java index 34ca769..e00b5c3 100644 --- a/src/main/java/com/budwk/app/base/constant/RoleConstant.java +++ b/src/main/java/com/budwk/app/base/constant/RoleConstant.java @@ -106,6 +106,7 @@ RoleConstant { LEGAL_DOCTOR("法律顾问"), UNION_REIMBURSEMENT_MANAGER("分工会报销负责人"), + UNION_TEMP_REIMBURSEMENT_APPLICANT("分工会临时报销人"), CLUB_REIMBURSEMENT_MANAGER("协会报销负责人"), SCHOOL_REIMBURSEMENT_MANAGER("校工会报销负责人"), diff --git a/src/main/java/com/budwk/app/zhgh/activity/declarereimbursement/reimbursement/controller/ActivityReimbursementApplyController.java b/src/main/java/com/budwk/app/zhgh/activity/declarereimbursement/reimbursement/controller/ActivityReimbursementApplyController.java index 53d99f2..bc56371 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/declarereimbursement/reimbursement/controller/ActivityReimbursementApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/declarereimbursement/reimbursement/controller/ActivityReimbursementApplyController.java @@ -23,6 +23,7 @@ import com.budwk.app.zhgh.activity.declarereimbursement.declare.models.ActivityD import com.budwk.app.zhgh.activity.declarereimbursement.vo.CommonPageParam; import com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.models.ActivityReimbursementInfo; import com.budwk.app.zhgh.activity.declarereimbursement.reimbursement.service.ActivityReimbursementService; +import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.models.ActivityBudget; import com.budwk.app.zhgh.dayofficework.outlay.outlayReimburse.service.OutlayReimburseApplyService; import io.swagger.annotations.ApiOperation; import org.nutz.aop.interceptor.ioc.TransAop; @@ -94,6 +95,10 @@ public class ActivityReimbursementApplyController { @SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR) @SLog(tag = "活动申报", msg = "保存申请,申请人: ${args[0].username}") public Result save(@Param("data") ActivityReimbursementInfo activityReimbursementInfo) { + Result scopeResult = validateTemporaryUnionReimbursement(activityReimbursementInfo); + if (scopeResult != null) { + return scopeResult; + } activityReimbursementInfo.setUserId(SecurityUtil.getUserId()); activityReimbursementInfo.setLoginName(SecurityUtil.getUserLoginname()); activityReimbursementInfo.setUserName(SecurityUtil.getUserUsername()); @@ -123,6 +128,10 @@ public class ActivityReimbursementApplyController { @SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR) @SLog(tag = "活动申报", msg = "提交申请,申请人: ${args[0].username}") public Result submit(@Param("data") ActivityReimbursementInfo activityReimbursementInfo) { + Result scopeResult = validateTemporaryUnionReimbursement(activityReimbursementInfo); + if (scopeResult != null) { + return scopeResult; + } activityReimbursementInfo.setUserId(SecurityUtil.getUserId()); activityReimbursementInfo.setLoginName(SecurityUtil.getUserLoginname()); activityReimbursementInfo.setUserName(SecurityUtil.getUserUsername()); @@ -170,6 +179,10 @@ public class ActivityReimbursementApplyController { @SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR) @SLog(tag = "活动申报", msg = "重新提交申请,申请人: ${args[0].username}") public Result submitAgain(@Param("data") ActivityReimbursementInfo activityReimbursementInfo, @Param("taskId") Long taskId) { + Result scopeResult = validateTemporaryUnionReimbursement(activityReimbursementInfo); + if (scopeResult != null) { + return scopeResult; + } if (StrUtil.isBlank(activityReimbursementInfo.getDeclareId())) { activityReimbursementInfo.setDeclareId(activityReimbursementInfo.getDeclareId()); } @@ -196,13 +209,78 @@ public class ActivityReimbursementApplyController { } + /** + * 临时报销人按本分工会校验申请及实际扣款预算,防止篡改身份、经费类型或预算ID绕过限制。 + */ + private Result validateTemporaryUnionReimbursement(ActivityReimbursementInfo info) { + if (!AuthUtil.hasRole(RoleConstant.UNION_TEMP_REIMBURSEMENT_APPLICANT.name())) { + return null; + } + if (info == null || !Objects.equals(info.getReimbursementIdentity(), "union") + || !Objects.equals(info.getOutlayManageSource(), "ACTIVITY_BUDGET_TYPE_TWO") + || StrUtil.isNotBlank(info.getClubId())) { + return Result.error("分工会临时报销人仅可使用分工会身份申请分工会经费报销"); + } + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(unionId)) { + return Result.error("当前用户未关联分工会,无法申请报销"); + } + if (StrUtil.isNotBlank(info.getId())) { + ActivityReimbursementInfo savedInfo = dao.fetch(ActivityReimbursementInfo.class, info.getId()); + if (savedInfo == null || !Objects.equals(savedInfo.getUserId(), SecurityUtil.getUserId()) + || !Objects.equals(savedInfo.getUnionId(), unionId) + || !Objects.equals(savedInfo.getReimbursementIdentity(), "union") + || !Objects.equals(savedInfo.getOutlayManageSource(), "ACTIVITY_BUDGET_TYPE_TWO")) { + return Result.error("仅可修改本人所在分工会的分工会经费报销申请"); + } + } + if (StrUtil.isBlank(info.getDeductionBudgetId())) { + return Result.error("请选择本分工会的扣款预算"); + } + ActivityBudget budget = dao.fetch(ActivityBudget.class, info.getDeductionBudgetId()); + if (budget == null || !Objects.equals(budget.getOutlayManageSource(), "ACTIVITY_BUDGET_TYPE_TWO") + || !Objects.equals(budget.getUnionId(), unionId)) { + return Result.error("扣款预算必须属于本分工会的分工会经费"); + } + if (Objects.equals(info.getActivityReimbursementMode(), "activity")) { + if (StrUtil.isBlank(info.getDeclareId())) { + return Result.error("请选择本分工会的申报活动"); + } + // 申报通过活动类型区分工会,实际扣款经费类型及归属已由上方预算校验约束。 + ActivityDeclareInfo declareInfo = dao.fetch(ActivityDeclareInfo.class, info.getDeclareId()); + if (declareInfo == null || !Objects.equals(declareInfo.getUnionId(), unionId) + || !Objects.equals(declareInfo.getActivityType(), "BRANCH_UNION")) { + return Result.error("仅可报销本分工会使用分工会经费的活动"); + } + } else if (Objects.equals(info.getActivityReimbursementMode(), "daily")) { + info.setDeclareId(null); + } else { + return Result.error("请选择报销模式"); + } + // 分工会归属以登录用户为准,供后续审批流程使用。 + info.setUnionId(unionId); + info.setActivityType("BRANCH_UNION"); + return null; + } + @At @ApiOperation("获取当前用户活动报销") @SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR) public Result getActivityReimbursementByUser(String id, String outlayManageSource, String clubId) { String activityType = ""; + boolean temporaryUnionApplicant = AuthUtil.hasRole(RoleConstant.UNION_TEMP_REIMBURSEMENT_APPLICANT.name()); + if (temporaryUnionApplicant && (!Objects.equals(outlayManageSource, "ACTIVITY_BUDGET_TYPE_TWO") + || StrUtil.isBlank(SecurityUtil.getUnionId()))) { + return Result.error("分工会临时报销人仅可申请本分工会经费报销"); + } if (StrUtil.isNotBlank(id)) { ActivityReimbursementInfo reimbursementInfo = dao.fetch(ActivityReimbursementInfo.class, id); + if (temporaryUnionApplicant && (reimbursementInfo == null + || !Objects.equals(reimbursementInfo.getUserId(), SecurityUtil.getUserId()) + || !Objects.equals(reimbursementInfo.getUnionId(), SecurityUtil.getUnionId()) + || !Objects.equals(reimbursementInfo.getOutlayManageSource(), "ACTIVITY_BUDGET_TYPE_TWO"))) { + return Result.error("无权访问该报销申请"); + } if (Objects.equals(reimbursementInfo.getActivityReimbursementMode(), "daily")) { return Result.success(); } @@ -324,7 +402,20 @@ public class ActivityReimbursementApplyController { @ApiOperation("获取申报的记录") @SaCheckPermission(value = {"activityReimbursement.apply", "h5.activityReimbursement.apply"}, mode = SaMode.OR) public Result getBudgetByYear(String outlayManageSource, String clubId) { + boolean temporaryUnionApplicant = AuthUtil.hasRole(RoleConstant.UNION_TEMP_REIMBURSEMENT_APPLICANT.name()); + if (temporaryUnionApplicant && (!Objects.equals(outlayManageSource, "ACTIVITY_BUDGET_TYPE_TWO") + || StrUtil.isBlank(SecurityUtil.getUnionId()))) { + return Result.error("分工会临时报销人仅可申请本分工会经费报销"); + } List budgetMoney = activityReimbursementService.getBudgetMoney(outlayManageSource, clubId); + if (temporaryUnionApplicant) { + // 兼有系统管理员角色时也必须限制为本分工会预算,不改变原预算金额展示规则。 + List unionBudgetIds = dao.query(ActivityBudget.class, + Cnd.where("unionId", "=", SecurityUtil.getUnionId()) + .and("outlayManageSource", "=", "ACTIVITY_BUDGET_TYPE_TWO")) + .stream().map(ActivityBudget::getId).toList(); + budgetMoney = budgetMoney.stream().filter(v -> unionBudgetIds.contains(v.getString("id"))).toList(); + } return Result.success(budgetMoney); } diff --git a/src/main/resources/db/menu/init_union_temp_reimbursement_role.sql b/src/main/resources/db/menu/init_union_temp_reimbursement_role.sql new file mode 100644 index 0000000..2b24b62 --- /dev/null +++ b/src/main/resources/db/menu/init_union_temp_reimbursement_role.sql @@ -0,0 +1,67 @@ +-- 分工会临时报销人:复用现有报销菜单;经费范围由申请页面及后端按角色编码共同限制。 +-- 适用于已配置费用报销菜单的 MySQL 数据库,可重复执行,不关联具体用户。 +START TRANSACTION; + +INSERT INTO sys_role + (id, name, code, aliasName, disabled, unitid, note, sort, createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT REPLACE(UUID(), '-', ''), '分工会临时报销人', 'UNION_TEMP_REIMBURSEMENT_APPLICANT', + '分工会临时报销人', 0, '', '仅可使用分工会身份申请本分工会经费报销', 0, + '', FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000), + '', FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000), 0 +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role WHERE code = 'UNION_TEMP_REIMBURSEMENT_APPLICANT' +); + +-- 只授权报销申请、我的报销及其导航父级,不复制原报销角色的审批等权限。 +-- activityDeclare 用于报销选取活动后读取申报详情,不授权活动申报、预算申报或审核子菜单。 +-- 同时关联已有的手机端报销菜单;通过路径前缀补齐导航父级,不授权同级菜单。 +INSERT INTO sys_role_menu (roleId, menuId) +SELECT DISTINCT role.id, menu.id +FROM sys_role role +JOIN sys_menu entry ON entry.permission IN ( + 'activityReimbursement.apply', + 'activityReimbursement.mine', + 'h5.activityReimbursement.apply', + 'h5.activityReimbursement.mine', + 'activityDeclare' +) +JOIN sys_menu menu ON menu.id = entry.id OR ( + menu.type = 'menu' + AND menu.path IS NOT NULL + AND menu.path <> '' + AND entry.path LIKE CONCAT(menu.path, '%') +) +WHERE role.code = 'UNION_TEMP_REIMBURSEMENT_APPLICANT' + AND entry.disabled = 0 + AND menu.disabled = 0 + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu assigned + WHERE assigned.roleId = role.id AND assigned.menuId = menu.id + ); + +-- 工会管理的角色下拉框及人员列表均读取 BRANCH_UNION_ROLES 字典,角色表记录不会自动成为选项。 +-- 沿用字典每级四位的路径规则,将新角色追加到已有子项之后,并按父级与角色编码避免重复插入。 +INSERT INTO sys_dict + (id, parentId, path, name, remark, code, disabled, location, hasChildren, + createdBy, createdAt, updatedBy, updatedAt, delFlag) +SELECT REPLACE(UUID(), '-', ''), parent.id, + CONCAT(parent.path, LPAD(COALESCE(siblings.maxSuffix, 0) + 1, 4, '0')), + '分工会临时报销人', '仅可申请本分工会经费报销', 'UNION_TEMP_REIMBURSEMENT_APPLICANT', + 0, COALESCE(siblings.maxLocation, 0) + 1, 0, + '', FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000), + '', FLOOR(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000), 0 +FROM sys_dict parent +LEFT JOIN ( + SELECT parentId, MAX(CAST(RIGHT(path, 4) AS UNSIGNED)) AS maxSuffix, + MAX(location) AS maxLocation + FROM sys_dict + GROUP BY parentId +) siblings ON siblings.parentId = parent.id +WHERE parent.code = 'BRANCH_UNION_ROLES' + AND NOT EXISTS ( + SELECT 1 FROM sys_dict existing + WHERE existing.parentId = parent.id + AND existing.code = 'UNION_TEMP_REIMBURSEMENT_APPLICANT' + ); + +COMMIT; diff --git a/src/main/resources/views/platform/zhgh/activity/declarereimbursement/reimbursement/apply/index.html b/src/main/resources/views/platform/zhgh/activity/declarereimbursement/reimbursement/apply/index.html index 329b0de..6c85ac6 100644 --- a/src/main/resources/views/platform/zhgh/activity/declarereimbursement/reimbursement/apply/index.html +++ b/src/main/resources/views/platform/zhgh/activity/declarereimbursement/reimbursement/apply/index.html @@ -380,6 +380,9 @@ layout("/layouts/platform.html"){ } }, methods: { + isTemporaryUnionApplicant() { + return this.$auth.hasRole('UNION_TEMP_REIMBURSEMENT_APPLICANT') + }, hasUnionRole() { return this.$auth.hasPermission('activityReimbursement.apply.unionHas') }, @@ -565,6 +568,10 @@ layout("/layouts/platform.html"){ } this.formData.id = this.bizId ? this.bizId : null + if (this.isTemporaryUnionApplicant()) { + // 申报记录的ID不能作为报销主键,单独保留关联活动以供后端校验。 + this.$set(this.formData, 'declareId', id) + } if (data.planStartTime && data.planEndTime) { this.formData.planDate = [data.planStartTime, data.planEndTime] @@ -602,6 +609,15 @@ layout("/layouts/platform.html"){ }, initBudgetType() { this.$businessTool.getDictOptions("ACTIVITY_BUDGET_TYPE").then(resp => { + // 临时报销人即使兼有其他角色,也只能使用分工会经费。 + if (this.isTemporaryUnionApplicant()) { + this.budgetTypeOption = resp.filter(v => v.code === "ACTIVITY_BUDGET_TYPE_TWO") + if (!this.bizId) { + this.$set(this.formData, 'outlayManageSource', 'ACTIVITY_BUDGET_TYPE_TWO') + this.outlayManageSourceChange('ACTIVITY_BUDGET_TYPE_TWO') + } + return + } this.budgetTypeOption = resp const budgetTypeCloseOption = [] const typeOneOption = resp.find(v => v.code === "ACTIVITY_BUDGET_TYPE_ONE") @@ -636,6 +652,16 @@ layout("/layouts/platform.html"){ }) }, initReimbursementIdentity() { + if (this.isTemporaryUnionApplicant()) { + this.reimbursementIdentityOption = [{name: "分工会", code: "union"}] + if (!this.bizId) { + this.$set(this.formData, 'reimbursementIdentity', 'union') + if (this.$store.state.user.union) { + this.reimbursementIdentityChange('union') + } + } + return + } if (this.$auth.hasPermission('activityReimbursement.apply.schoolHas')) { this.reimbursementIdentityOption.push({name: "校工会", code: "school"}) } diff --git a/src/main/resources/views/platform/zhghh5/activity/declarereimbursement/reimbursement/apply/index.html b/src/main/resources/views/platform/zhghh5/activity/declarereimbursement/reimbursement/apply/index.html index f796892..5a40408 100644 --- a/src/main/resources/views/platform/zhghh5/activity/declarereimbursement/reimbursement/apply/index.html +++ b/src/main/resources/views/platform/zhghh5/activity/declarereimbursement/reimbursement/apply/index.html @@ -276,6 +276,9 @@ layout("/layouts/platform_h5.html"){ } }, methods: { + isTemporaryUnionApplicant() { + return this.$auth.hasRole('UNION_TEMP_REIMBURSEMENT_APPLICANT') + }, hasUnionRole() { return this.$auth.hasPermission('activityReimbursement.apply.unionHas') }, @@ -786,6 +789,16 @@ layout("/layouts/platform_h5.html"){ }, initBudgetType() { return this.$businessTool.getDictOptions('ACTIVITY_BUDGET_TYPE').then((resp) => { + // 与电脑端保持一致,临时报销人的经费范围不叠加其他角色权限。 + if (this.isTemporaryUnionApplicant()) { + this.budgetTypeOption = resp.filter((item) => item.code === 'ACTIVITY_BUDGET_TYPE_TWO') + if (!this.bizId) { + this.$set(this.formData, 'outlayManageSource', 'ACTIVITY_BUDGET_TYPE_TWO') + this.$set(this.formData, 'outlayManageSourceName', '分工会经费') + this.outlayManageSourceChange('ACTIVITY_BUDGET_TYPE_TWO') + } + return + } const result = [] const typeOne = resp.find((item) => item.code === 'ACTIVITY_BUDGET_TYPE_ONE') if (typeOne) { @@ -815,6 +828,17 @@ layout("/layouts/platform_h5.html"){ }) }, initReimbursementIdentity() { + if (this.isTemporaryUnionApplicant()) { + this.reimbursementIdentityOption = [{name: '分工会', code: 'union'}] + if (!this.bizId) { + this.$set(this.formData, 'reimbursementIdentity', 'union') + this.$set(this.formData, 'reimbursementIdentityName', '分工会') + if (this.$store.state.user.union) { + this.reimbursementIdentityChange('union') + } + } + return + } if (this.hasSchoolLevelRole()) { this.reimbursementIdentityOption.push({name: '校工会', code: 'school'}) }