diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/conteoller/ActivityBudgetApplyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/conteoller/ActivityBudgetApplyController.java index d7fe10d..5cfa1ec 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/conteoller/ActivityBudgetApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/conteoller/ActivityBudgetApplyController.java @@ -22,6 +22,7 @@ import com.budwk.app.zhgh.club.service.SysClubService; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.models.ActivityBudget; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.service.ActivityBudgetService; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.template.ActivityBudgetTemp; +import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.vo.ActivityBudgetQuotaInfoVo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -91,6 +92,10 @@ public class ActivityBudgetApplyController { @Aop(TransAop.READ_COMMITTED) @SLog( tag = "年度预算申报-预算申报", msg = "重新提交申请") public Result submitAgain(@Param("data") ActivityBudget activityBudget, @Param("taskId") Long taskId) { + Result quotaValidateResult = activityBudgetService.validateQuota(activityBudget); + if (quotaValidateResult.getCode() != 0) { + return quotaValidateResult; + } activityBudgetService.insertOrUpdate(activityBudget); Dict dict = Dict.create(); @@ -100,6 +105,30 @@ public class ActivityBudgetApplyController { return Result.success(); } + /** + * 查询预算额度占用情况。 + * 参数说明: + * 1. outlayManageSource:预算类型,当前仅分工会预算需要查询额度。 + * 2. unionId:分工会ID。 + * 3. applyDate:申报时间,用来确定预算所属年度。 + * 返回值字段: + * 1. totalQuota:本年度总额度。 + * 2. applyingQuota:申请中额度。 + * 3. approvedQuota:已通过额度。 + * 4. occupiedQuota:已占用额度。 + * 5. remainQuota:剩余额度。 + * + * @param activityBudget 预算申报参数 + * @return 当前预算主体的额度统计 + */ + @At + @ApiOperation("查询年度预算额度信息") + @SaCheckPermission("activity.budget.apply") + public Result getQuotaInfo(@Param("data") ActivityBudget activityBudget) { + ActivityBudgetQuotaInfoVo quotaInfoVo = activityBudgetService.getQuotaInfo(activityBudget); + return Result.success(quotaInfoVo); + } + @At @SaCheckPermission("activity.budget.apply") diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/ActivityBudgetService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/ActivityBudgetService.java index dddef8a..d25c314 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/ActivityBudgetService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/ActivityBudgetService.java @@ -4,6 +4,7 @@ import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.models.ActivityBudget; +import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.vo.ActivityBudgetQuotaInfoVo; import org.nutz.dao.Cnd; import org.nutz.dao.sql.Sql; import org.nutz.lang.util.NutMap; @@ -22,6 +23,41 @@ public interface ActivityBudgetService extends BaseService { */ Result submit(ActivityBudget activityBudget); + /** + * 校验预算申报额度。 + * 参数说明: + * 1. activityBudget.outlayManageSource:预算类型,分工会/协会/校工会。 + * 2. activityBudget.unionId:分工会预算时传对应分工会ID。 + * 4. activityBudget.declareTotalBudgetMoney:本次申报金额。 + * 5. activityBudget.applyDate:申报时间,用于判定所属年度;为空时按当前年份处理。 + * 返回值: + * Result.success 表示可提交;当前仅分工会预算会触发年度额度校验; + * Result.error 表示超额或年度额度未配置,message 会直接返回给前端提示。 + * + * @param activityBudget 预算申报数据 + * @return 校验结果 + */ + Result validateQuota(ActivityBudget activityBudget); + + /** + * 查询预算额度占用信息。 + * 参数说明: + * 1. activityBudget.outlayManageSource:预算类型;当前仅分工会预算需要返回额度占用信息。 + * 2. activityBudget.unionId:分工会预算时用于定位对应年度额度记录。 + * 3. activityBudget.applyDate:用于计算预算所属年度;为空时按当前年份处理。 + * 返回值字段: + * 1. year:预算所属年度。 + * 2. totalQuota:本年度总额度。 + * 3. applyingQuota:申请中额度,除已通过和已拒绝外的流程金额总和。 + * 4. approvedQuota:已通过额度。 + * 5. occupiedQuota:当前已占用额度,除已拒绝外全部计入。 + * 6. remainQuota:当前剩余额度。 + * + * @param activityBudget 预算申报数据 + * @return 预算额度统计信息 + */ + ActivityBudgetQuotaInfoVo getQuotaInfo(ActivityBudget activityBudget); + NutMap findOne(String id); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/impl/ActivityBudgetServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/impl/ActivityBudgetServiceImpl.java index b93de5e..ebd4a63 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/impl/ActivityBudgetServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/service/impl/ActivityBudgetServiceImpl.java @@ -4,7 +4,9 @@ import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.model.Audit; @@ -16,6 +18,7 @@ import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; import com.budwk.app.sys.models.Sys_dict; import com.budwk.app.sys.services.SysDictService; @@ -26,6 +29,8 @@ import com.budwk.app.zhgh.club.service.SysClubService; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.models.ActivityBudget; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.models.ActivityBudgetDetails; import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.service.ActivityBudgetService; +import com.budwk.app.zhgh.dayofficework.outlay.activityBudget.vo.ActivityBudgetQuotaInfoVo; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion; import org.apache.poi.ss.usermodel.Workbook; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; @@ -37,6 +42,7 @@ import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.util.NutMap; import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -140,6 +146,10 @@ public class ActivityBudgetServiceImpl extends BaseServiceImpl i } } } + Result quotaValidateResult = validateQuota(activityBudget); + if (quotaValidateResult != null && quotaValidateResult.getCode() != 0) { + return quotaValidateResult; + } //删除预算详情表 if (StrUtil.isNotBlank(activityBudget.getId()) ) { dao().clear(ActivityBudgetDetails.class, Cnd.where("budgetId", "=", activityBudget.getId())); @@ -227,6 +237,80 @@ public class ActivityBudgetServiceImpl extends BaseServiceImpl i return Result.success(); } + @Override + public Result validateQuota(ActivityBudget activityBudget) { + if (ObjectUtil.isEmpty(activityBudget) || StrUtil.isBlank(activityBudget.getOutlayManageSource())) { + return Result.error("预算类型不能为空"); + } + if (!"ACTIVITY_BUDGET_TYPE_TWO".equals(activityBudget.getOutlayManageSource())) { + // 当前业务仅分工会预算保留年度额度校验,校工会和协会预算直接跳过。 + return Result.success(); + } + if (ObjectUtil.isEmpty(activityBudget.getDeclareTotalBudgetMoney())) { + return Result.error("预算金额不能为空"); + } + + ActivityBudgetQuotaInfoVo quotaInfo = getQuotaInfo(activityBudget); + if (!quotaInfo.getSelectedTarget()) { + return Result.error(quotaInfo.getMessage()); + } + if (ObjectUtil.isEmpty(quotaInfo.getTotalQuota())) { + return Result.error(quotaInfo.getMessage()); + } + + BigDecimal remainQuota = quotaInfo.getRemainQuota(); + BigDecimal declareTotalBudgetMoney = activityBudget.getDeclareTotalBudgetMoney(); + if (remainQuota.compareTo(declareTotalBudgetMoney) < 0) { + return Result.error("本年度预算额度不足,剩余额度:" + remainQuota + + "元,申请中额度:" + quotaInfo.getApplyingQuota() + + "元,已通过额度:" + quotaInfo.getApprovedQuota() + "元"); + } + return Result.success(); + } + + @Override + public ActivityBudgetQuotaInfoVo getQuotaInfo(ActivityBudget activityBudget) { + if (ObjectUtil.isEmpty(activityBudget)) { + activityBudget = new ActivityBudget(); + } + String outlayManageSource = activityBudget.getOutlayManageSource(); + Integer year = getBudgetYear(activityBudget); + ActivityBudgetQuotaInfoVo quotaInfo = new ActivityBudgetQuotaInfoVo(); + quotaInfo.setYear(year); + quotaInfo.setNeedLimit("ACTIVITY_BUDGET_TYPE_TWO".equals(outlayManageSource)); + quotaInfo.setSelectedTarget(true); + quotaInfo.setTotalQuota(null); + quotaInfo.setApplyingQuota(BigDecimal.ZERO); + quotaInfo.setApprovedQuota(BigDecimal.ZERO); + quotaInfo.setOccupiedQuota(BigDecimal.ZERO); + quotaInfo.setRemainQuota(BigDecimal.ZERO); + + if (!"ACTIVITY_BUDGET_TYPE_TWO".equals(outlayManageSource)) { + return quotaInfo; + } + + BigDecimal totalQuota = getCurrentYearTotalQuota(activityBudget, year, quotaInfo); + if (!quotaInfo.getSelectedTarget()) { + return quotaInfo; + } + if (ObjectUtil.isEmpty(totalQuota)) { + quotaInfo.setMessage("该年度未配置预算额度,请先维护年度预算"); + return quotaInfo; + } + + NutMap quotaUseInfo = getQuotaUseInfo(activityBudget, year); + BigDecimal occupiedQuota = getMapBigDecimal(quotaUseInfo, "occupiedQuota"); + BigDecimal approvedQuota = getMapBigDecimal(quotaUseInfo, "approvedQuota"); + BigDecimal applyingQuota = getMapBigDecimal(quotaUseInfo, "applyingQuota"); + + quotaInfo.setTotalQuota(totalQuota); + quotaInfo.setOccupiedQuota(occupiedQuota); + quotaInfo.setApprovedQuota(approvedQuota); + quotaInfo.setApplyingQuota(applyingQuota); + quotaInfo.setRemainQuota(totalQuota.subtract(occupiedQuota)); + return quotaInfo; + } + @Override public NutMap findOne(String id) { Sql sql = Sqls.create(""" @@ -277,4 +361,108 @@ public class ActivityBudgetServiceImpl extends BaseServiceImpl i Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, mapList); CommonDownloadUtil.download("预算申报汇总表.xlsx", workbook, response); } + + /** + * 解析预算所属年度,页面没有单独年度字段时默认取申报时间中的年份。 + * + * @param activityBudget 预算申报数据 + * @return 预算所属年度 + */ + private Integer getBudgetYear(ActivityBudget activityBudget) { + if (ObjectUtil.isEmpty(activityBudget) || StrUtil.isBlank(activityBudget.getApplyDate())) { + return DateUtil.thisYear(); + } + try { + return DateUtil.parse(activityBudget.getApplyDate()).year(); + } catch (Exception e) { + return DateUtil.thisYear(); + } + } + + /** + * 查询当前预算类型在所属年度的总额度配置。 + * + * @param activityBudget 预算申报数据 + * @param year 预算所属年度 + * @param quotaInfo 返回前端的额度信息对象,用于补充缺失主体的提示 + * @return 年度总额度;未配置时返回 null + */ + private BigDecimal getCurrentYearTotalQuota(ActivityBudget activityBudget, Integer year, ActivityBudgetQuotaInfoVo quotaInfo) { + String outlayManageSource = activityBudget.getOutlayManageSource(); + if ("ACTIVITY_BUDGET_TYPE_TWO".equals(outlayManageSource)) { + String unionId = StrUtil.blankToDefault(activityBudget.getUnionId(), SecurityUtil.getUnionId()); + if (StrUtil.isBlank(unionId)) { + quotaInfo.setSelectedTarget(false); + quotaInfo.setMessage("请选择分工会"); + return null; + } + OutlayManageUnion outlayManageUnion = dao().fetch(OutlayManageUnion.class, + Cnd.where("year", "=", year).and("unionId", "=", unionId)); + return ObjectUtil.isEmpty(outlayManageUnion) ? null : outlayManageUnion.getTotalQuota(); + } + return null; + } + + /** + * 统计当前主体在本年度已经占用的预算。 + * 统计规则: + * 1. 只统计已进入流程的申请; + * 2. 已拒绝不计入额度; + * 3. 其他状态全部计入占用额度; + * 4. 申请中额度 = 非已通过、非已拒绝的金额合计。 + * + * @param activityBudget 预算申报数据 + * @param year 预算所属年度 + * @return occupiedQuota/applyingQuota/approvedQuota 统计结果 + */ + private NutMap getQuotaUseInfo(ActivityBudget activityBudget, Integer year) { + Sql sql = Sqls.create(""" + SELECT + IFNULL(SUM(ab.declareTotalBudgetMoney), 0) occupiedQuota, + IFNULL(SUM(CASE WHEN ins.state = @finishedState THEN ab.declareTotalBudgetMoney ELSE 0 END), 0) approvedQuota, + IFNULL(SUM(CASE WHEN ins.state != @finishedState AND ins.state != @rejectState THEN ab.declareTotalBudgetMoney ELSE 0 END), 0) applyingQuota + FROM + activity_budget ab + LEFT JOIN wf_process_instance ins ON ins.businessNo = ab.id + $condition + """); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + sql.setParam("rejectState", ProcessInstanceStateEnum.REJECT.getCode()); + Cnd cnd = Cnd.NEW(); + cnd.andEX("YEAR(ab.applyDate)", "=", year); + cnd.and("ab.outlayManageSource", "=", activityBudget.getOutlayManageSource()); + cnd.and("ins.id", "IS NOT", null); + cnd.and("ins.state", "!=", ProcessInstanceStateEnum.REJECT.getCode()); + if (StrUtil.isNotBlank(activityBudget.getId())) { + // 重新提交或编辑当前记录时,先排除本条数据,避免把旧记录金额重复计入占用额度。 + cnd.and("ab.id", "!=", activityBudget.getId()); + } + if ("ACTIVITY_BUDGET_TYPE_TWO".equals(activityBudget.getOutlayManageSource())) { + cnd.and("ab.unionId", "=", StrUtil.blankToDefault(activityBudget.getUnionId(), SecurityUtil.getUnionId())); + } else if ("ACTIVITY_BUDGET_TYPE_THREE".equals(activityBudget.getOutlayManageSource())) { + cnd.and("ab.clubId", "=", activityBudget.getClubId()); + } + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.map()); + execute(sql); + return (NutMap) sql.getResult(); + } + + /** + * 统一把数据库聚合结果转成 BigDecimal,避免前后端展示时出现 null。 + * + * @param map 聚合结果 + * @param key 字段名 + * @return 对应金额,默认 0 + */ + private BigDecimal getMapBigDecimal(NutMap map, String key) { + Object value = ObjectUtil.isEmpty(map) ? null : map.get(key); + if (ObjectUtil.isEmpty(value)) { + return BigDecimal.ZERO; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + return new BigDecimal(String.valueOf(value)); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/vo/ActivityBudgetQuotaInfoVo.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/vo/ActivityBudgetQuotaInfoVo.java new file mode 100644 index 0000000..d688d3d --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/activityBudget/vo/ActivityBudgetQuotaInfoVo.java @@ -0,0 +1,59 @@ +package com.budwk.app.zhgh.dayofficework.outlay.activityBudget.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * @author zhf + * @date 2026/4/20 + * @description 预算申报额度信息 + */ +@Data +public class ActivityBudgetQuotaInfoVo { + /** + * 预算所属年度。 + */ + private Integer year; + + /** + * 当前预算类型是否需要做额度限制。 + */ + private Boolean needLimit = false; + + /** + * 是否已经选择了有效的预算主体。 + * 分工会预算需要分工会ID,协会预算需要协会ID。 + */ + private Boolean selectedTarget = true; + + /** + * 本年度总额度;未配置时为 null。 + */ + private BigDecimal totalQuota; + + /** + * 当前申请中额度。 + */ + private BigDecimal applyingQuota = BigDecimal.ZERO; + + /** + * 当前已通过额度。 + */ + private BigDecimal approvedQuota = BigDecimal.ZERO; + + /** + * 当前已占用额度,除已拒绝外全部计入。 + */ + private BigDecimal occupiedQuota = BigDecimal.ZERO; + + /** + * 当前剩余额度。 + */ + private BigDecimal remainQuota = BigDecimal.ZERO; + + /** + * 当前额度提示信息。 + */ + private String message; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java index 47f0755..220f964 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java @@ -16,12 +16,15 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.club.model.SysClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.service.OutlayManageClubService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.util.NutMap; @@ -49,6 +52,8 @@ public class OutlayManageClubController { @Inject private SysRoleService sysRoleService; + @Inject + private OutlayManageClubService outlayManageClubService; @At("") @Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html") @@ -89,6 +94,45 @@ public class OutlayManageClubController { return Result.success(); } + /** + * 查询某个年度已经分配预算的协会ID。 + * 参数说明: + * 1. year:预算年度。 + * 返回值说明: + * 1. 返回当前年度已存在预算分配记录的协会ID列表。 + * + * @param year 预算年度 + * @return 已分配预算的协会ID列表 + */ + @At + @ApiOperation("查询某个年度已分配预算的协会ID") + @SaCheckPermission("outlay.outlayManage.club.manage") + public Result getAllocatedClubIds(Integer year) { + return Result.success(outlayManageClubService.getAllocatedClubIds(year)); + } + + /** + * 手动新增协会预算分配。 + * 参数说明: + * 1. outlayManageClub.year:预算年度。 + * 2. outlayManageClub.clubId:协会ID。 + * 3. outlayManageClub.totalQuota:分配总额度。 + * 返回值说明: + * 1. 返回新增成功结果,失败时返回业务提示信息。 + * + * @param outlayManageClub 新增分配数据 + * @return 新增结果 + */ + @At + @ApiOperation("手动新增协会预算分配") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("outlay.outlayManage.club.manage") + @SLog(tag = "协会预算分配", msg = "手动新增了协会预算分配,年度:${args[0].year},协会:${args[0].clubId},总额度:${args[0].totalQuota}") + public Result addAllocation(OutlayManageClub outlayManageClub) { + outlayManageClubService.addAllocation(outlayManageClub); + return Result.success(); + } + @At @ApiOperation("查看是否分配") diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/service/OutlayManageClubService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/service/OutlayManageClubService.java new file mode 100644 index 0000000..c95ede3 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/service/OutlayManageClubService.java @@ -0,0 +1,42 @@ +package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; + +import java.util.List; + +/** + * @author zhf + * @date 2026/4/20 + * @description 协会预算分配公共方法 + */ +public interface OutlayManageClubService extends BaseService { + + /** + * 查询某个年度已经分配过预算的协会ID列表。 + * 参数说明: + * 1. year:预算年度,查询该年度已存在的协会预算记录。 + * 返回值说明: + * 1. 返回当前年度在 outlay_manage_club 表中已经存在的 clubId 集合。 + * 2. 前端可基于该结果过滤“审核通过但未分配预算”的协会下拉。 + * + * @param year 预算年度 + * @return 已分配预算的协会ID列表 + */ + List getAllocatedClubIds(Integer year); + + /** + * 手动新增协会预算分配。 + * 参数说明: + * 1. outlayManageClub.year:预算年度。 + * 2. outlayManageClub.clubId:协会ID。 + * 3. outlayManageClub.totalQuota:本次分配的总额度。 + * 返回值说明: + * 1. 返回已补全协会名称、协会编码后的预算分配实体。 + * 2. 如果年度、协会、总额度为空,或该年度该协会已存在预算记录,则抛出业务异常。 + * + * @param outlayManageClub 手动新增的预算分配数据 + * @return 新增后的预算分配实体 + */ + OutlayManageClub addAllocation(OutlayManageClub outlayManageClub); +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/service/impl/OutlayManageClubServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/service/impl/OutlayManageClubServiceImpl.java new file mode 100644 index 0000000..d2bb349 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/service/impl/OutlayManageClubServiceImpl.java @@ -0,0 +1,77 @@ +package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.exception.BaseException; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.club.model.SysClub; +import com.budwk.app.zhgh.club.service.SysClubService; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.service.OutlayManageClubService; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author zhf + * @date 2026/4/20 + * @description 协会预算分配公共方法实现 + */ +@IocBean(args = {"refer:dao"}) +public class OutlayManageClubServiceImpl extends BaseServiceImpl implements OutlayManageClubService { + public OutlayManageClubServiceImpl(Dao dao) { + super(dao); + } + + @Inject + private SysClubService sysClubService; + + @Override + public List getAllocatedClubIds(Integer year) { + if (ObjectUtil.isEmpty(year)) { + return List.of(); + } + List outlayManageClubList = query(Cnd.where("year", "=", year)); + return outlayManageClubList.stream() + .map(OutlayManageClub::getClubId) + .filter(StrUtil::isNotBlank) + .toList(); + } + + @Override + public OutlayManageClub addAllocation(OutlayManageClub outlayManageClub) { + if (ObjectUtil.isEmpty(outlayManageClub) || ObjectUtil.isEmpty(outlayManageClub.getYear())) { + throw new BaseException("预算年度不能为空"); + } + if (StrUtil.isBlank(outlayManageClub.getClubId())) { + throw new BaseException("协会不能为空"); + } + if (ObjectUtil.isEmpty(outlayManageClub.getTotalQuota())) { + throw new BaseException("总额度不能为空"); + } + if (outlayManageClub.getTotalQuota().compareTo(BigDecimal.ZERO) < 0) { + throw new BaseException("总额度不能小于0"); + } + + int count = count(Cnd.where("year", "=", outlayManageClub.getYear()).and("clubId", "=", outlayManageClub.getClubId())); + if (count > 0) { + throw new BaseException("该年度该协会已分配预算,请勿重复新增"); + } + + SysClub sysClub = sysClubService.fetch(outlayManageClub.getClubId()); + if (ObjectUtil.isEmpty(sysClub)) { + throw new BaseException("未找到对应协会信息"); + } + + outlayManageClub.setClubName(sysClub.getClubName()); + outlayManageClub.setClubCode(sysClub.getClubCode()); + if (ObjectUtil.isEmpty(outlayManageClub.getUsedQuota())) { + outlayManageClub.setUsedQuota(BigDecimal.ZERO); + } + return insert(outlayManageClub); + } +} diff --git a/src/main/resources/views/platform/zhgh/dayofficework/outlay/activityBudget/apply/index.html b/src/main/resources/views/platform/zhgh/dayofficework/outlay/activityBudget/apply/index.html index d16f640..d6013a9 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/outlay/activityBudget/apply/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/outlay/activityBudget/apply/index.html @@ -90,6 +90,17 @@ layout("/layouts/platform.html"){ v-model="formData.declareTotalBudgetMoney"> + +
+
本年度总额度:{{quotaInfo.totalQuota}}元,申请中额度:{{quotaInfo.applyingQuota}}元,已通过额度:{{quotaInfo.approvedQuota}}元,当前剩余额度:{{quotaInfo.remainQuota}}元
+
+ 当前申报金额已超出剩余额度,请先调整预算金额。 +
+
+
+ {{quotaInfo.message}} +
+
{ - this.formData = data - }) + const data = await this.findOne(this.bizId) + this.$set(this, "formData", data) } else { - this.formData = { + this.$set(this, "formData", { isSchoolBudget: false, isRepeatReimburse: true, budgetDetails: [], applyDate: this.$moment().format('YYYY-MM-DD'), userName: this.$store.state.user.username, mobile: this.$store.state.user.mobile, - } + }) } }, clubChange(val) { const club = this.clubOption.find(c => c.id === val) - this.formData.helpUnitName = club.clubName + this.$set(this.formData, "helpUnitName", club ? club.clubName : "") + this.refreshQuotaInfo() }, unionChange(id) { if (id) { const union = this.unionList.find(c => c.id === id) - this.$set(this.formData, "helpUnitName", union.unionname) + this.$set(this.formData, "helpUnitName", union ? (union.name || union.unionname) : "") } else { this.$set(this.formData, "helpUnitName", '') } + this.refreshQuotaInfo() }, budgetTypeCodeChange(val) { if (val === "ACTIVITY_BUDGET_TYPE_ONE") { this.$set(this.formData, "helpUnitName", "校工会") + this.$set(this.formData, "unionId", "") + this.$set(this.formData, "clubId", "") } else if (val === "ACTIVITY_BUDGET_TYPE_TWO") { this.$set(this.formData, "helpUnitName", this.$store.state.user.union.name) this.$set(this.formData, "unionId", this.$store.state.user.union.id) + this.$set(this.formData, "clubId", "") } else if (["ACTIVITY_BUDGET_TYPE_THREE"].includes(val)) { this.$set(this.formData, "helpUnitName", '') this.$set(this.formData, "clubId", '') + this.$set(this.formData, "unionId", "") } else { this.$set(this.formData, "helpUnitName", "校工会") + this.$set(this.formData, "unionId", "") + this.$set(this.formData, "clubId", "") } if (val) { const budgetType = this.budgetTypeOption.find(b => b.code === val) this.$set(this.formData, "budgetTypeId", budgetType.code) } + this.refreshQuotaInfo() }, async getActivityBudgetType() { const data = await this.$businessTool.getDictOptions("ACTIVITY_BUDGET_TYPE") let budgetTypeOption = [] if (this.$auth.hasRoleOr(['SYSADMIN'])) { - this.budgetTypeOption = data + this.$set(this, "budgetTypeOption", data) } else { if (this.$auth.hasRoleOr(['SCHOOL_UNION_ADMIN', 'SCHOOL_UNION_CHAIRMAN', 'SCHOOL_UNION_VICE_CHAIRMAN'])) { data.map(v => { @@ -273,7 +302,7 @@ layout("/layouts/platform.html"){ } }) } - this.budgetTypeOption = budgetTypeOption + this.$set(this, "budgetTypeOption", budgetTypeOption) if (this.budgetTypeOption.length === 1){ this.$set(this.formData, "outlayManageSource", this.budgetTypeOption[0].code) this.budgetTypeCodeChange(this.budgetTypeOption[0].code) @@ -283,10 +312,81 @@ layout("/layouts/platform.html"){ getSchoolBudget() { this.$axios.post("/platform/activity/budget/apply/getSchoolBudget").then((res) => { if (res.code === 0) { - this.activityList = res.data + this.$set(this, "activityList", res.data) } }) }, + clearQuotaInfo() { + this.$set(this, "quotaInfo", { + needLimit: false, + selectedTarget: true, + totalQuota: null, + applyingQuota: 0, + approvedQuota: 0, + occupiedQuota: 0, + remainQuota: 0, + message: "" + }) + }, + refreshQuotaInfo() { + const outlayManageSource = this.formData.outlayManageSource + if (!outlayManageSource || outlayManageSource !== "ACTIVITY_BUDGET_TYPE_TWO") { + this.clearQuotaInfo() + return + } + if (!this.formData.unionId) { + this.$set(this, "quotaInfo", { + needLimit: true, + selectedTarget: false, + totalQuota: null, + applyingQuota: 0, + approvedQuota: 0, + occupiedQuota: 0, + remainQuota: 0, + message: "请选择分工会" + }) + return + } + this.$axios.post("/platform/activity/budget/apply/getQuotaInfo", { + data: JSON.stringify({ + id: this.formData.id, + applyDate: this.formData.applyDate, + outlayManageSource: this.formData.outlayManageSource, + unionId: this.formData.unionId, + clubId: this.formData.clubId + }) + }).then((res) => { + if (res.code === 0) { + this.$set(this, "quotaInfo", { + needLimit: res.data.needLimit, + selectedTarget: res.data.selectedTarget, + totalQuota: res.data.totalQuota, + applyingQuota: res.data.applyingQuota, + approvedQuota: res.data.approvedQuota, + occupiedQuota: res.data.occupiedQuota, + remainQuota: res.data.remainQuota, + message: res.data.message || "" + }) + } else { + this.$set(this, "quotaInfo", { + needLimit: true, + selectedTarget: false, + totalQuota: null, + applyingQuota: 0, + approvedQuota: 0, + occupiedQuota: 0, + remainQuota: 0, + message: res.message || "预算额度查询失败" + }) + } + }) + }, + isQuotaExceeded() { + if (!this.quotaInfo.needLimit || !this.quotaInfo.selectedTarget || this.quotaInfo.totalQuota === null) { + return false + } + return Number(this.formData.declareTotalBudgetMoney || 0) > Number(this.quotaInfo.remainQuota || 0) + }, // 保存 onSave() { this.$confirm("您确定保存吗?", "提示", { @@ -316,6 +416,8 @@ layout("/layouts/platform.html"){ if (res.code === 0) { this.$message.success("提交成功") commonUtil.pjaxPush('/platform/activity/budget/applyList') + } else { + this.$message.error(res.message) } }) }) @@ -333,6 +435,8 @@ layout("/layouts/platform.html"){ if (res.code === 0) { this.$message.success("提交成功") commonUtil.pjaxPush('/platform/activity/budget/applyList') + } else { + this.$message.error(res.message) } }) }) @@ -340,11 +444,12 @@ layout("/layouts/platform.html"){ }, async created() { - this.init() + await this.init() await this.getActivityBudgetType() await this.getSchoolBudget() - this.unionList = await this.$businessTool.listUnion(this.$store.state.user.union.id) - this.clubOption = await this.$businessTool.listCLubByRole() + this.$set(this, "unionList", await this.$businessTool.listUnion(this.$store.state.user.union.id)) + this.$set(this, "clubOption", await this.$businessTool.listCLubByRole()) + this.refreshQuotaInfo() } }) diff --git a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html index 2ecdf52..71c7d24 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html @@ -24,6 +24,9 @@ layout("/layouts/platform.html"){ + + 新增分配 + @@ -102,11 +139,70 @@ layout("/layouts/platform.html"){ pageForm: { year: this.$moment().format("YYYY") }, - isAllocation: false + isAllocation: false, + addDialogVisible: false, + addClubOption: [], + addForm: { + year: this.$moment().format("YYYY"), + clubId: "", + totalQuota: undefined + }, + addFormRules: { + year: [{required: true, message: '必填', trigger: ['change', 'blur']}], + clubId: [{required: true, message: '必填', trigger: ['change', 'blur']}], + totalQuota: [{required: true, message: '必填', trigger: ['change', 'blur']}] + }, + addYearPickerOptions: { + disabledDate(time) { + const currentYear = Number(moment().format("YYYY")) + return time.getFullYear() < currentYear + } + } } }, components: {}, methods: { + openAddDialog() { + this.$set(this, "addDialogVisible", true) + this.$set(this, "addForm", { + year: this.pageForm.year, + clubId: "", + totalQuota: undefined + }) + this.$nextTick(() => { + if (this.$refs.addFormRef) { + this.$refs.addFormRef.clearValidate() + } + }) + this.loadAvailableClubOption() + }, + closeAddDialog() { + this.$set(this, "addDialogVisible", false) + }, + addYearChange() { + this.$set(this.addForm, "clubId", "") + this.loadAvailableClubOption() + }, + loadAvailableClubOption() { + if (!this.addForm.year) { + this.$set(this, "addClubOption", []) + return + } + Promise.all([ + this.$businessTool.listCLubByRole(), + this.$axios.post("/platform/outlay/outlayManage/clubManage/getAllocatedClubIds", { + year: this.addForm.year + }) + ]).then((res) => { + const allClubOption = res[0] || [] + const allocatedClubIds = res[1].code === 0 ? res[1].data : [] + const availableClubOption = allClubOption.filter(item => !allocatedClubIds.includes(item.id)) + this.$set(this, "addClubOption", availableClubOption) + if (availableClubOption.length === 0) { + this.$message.warning("当前年度没有可新增分配的协会"); + } + }) + }, doSubmit(row) { this.$confirm('您确定要提交此条记录吗?', '提示', { confirmButtonText: '确定', @@ -125,6 +221,29 @@ layout("/layouts/platform.html"){ }) }) }, + submitAddAllocation() { + this.$refs.addFormRef.validate((valid) => { + if (valid) { + this.$confirm('您确定要新增这条分配记录吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.$axios.post("/platform/outlay/outlayManage/clubManage/addAllocation", this.addForm) + .then((resp) => { + if (resp.code === 0) { + this.$message.success("新增成功"); + this.closeAddDialog() + this.doSearch() + this.getIsAllocation() + } else { + this.$message.error(resp.message) + } + }) + }) + } + }) + }, resetOutlay() { this.$confirm('您确定要重置今年的记录吗?', '提示', { confirmButtonText: '确定',