feat: 经费预算管理

This commit is contained in:
2026-11-05 19:23:13 +08:00
parent c21e92e418
commit e7fbf4021e
10 changed files with 781 additions and 135 deletions
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service;
import com.budwk.app.base.service.BaseService;
import org.nutz.lang.util.NutMap;
import org.nutz.dao.sql.Sql;
/**
* 分工会预算统计服务。
@@ -28,4 +29,6 @@ public interface OutlayManageUnionStatisticsService extends BaseService {
* @return 包含各项收入、支出和余额字段的合计数据
*/
NutMap queryIncomeExpenseSummary(Integer year, String unionId);
Sql buildIncomeExpenseSql(Integer year, String unionId);
}
@@ -0,0 +1,243 @@
package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import java.math.BigDecimal;
import java.time.Year;
import java.util.List;
import java.util.UUID;
@IocBean(args = {"refer:dao"})
public class OutlayAnnualAllocateService extends BaseServiceImpl<OutlayManageUnion> {
public OutlayAnnualAllocateService(Dao dao) { super(dao); }
public Sql sql(String text, Object... args) {
Sql sql = Sqls.create(text);
for (int i = 0; i < args.length; i += 2) sql.params().set(args[i].toString(), args[i + 1]);
return sql;
}
private NutMap first(Sql sql) {
List<NutMap> rows = listMap(sql);
return rows.isEmpty() ? null : rows.get(0);
}
private String table(String type) {
if ("base".equals(type)) return "outlay_annual_base";
if ("special".equals(type)) return "outlay_annual_special";
throw new IllegalArgumentException("经费类型错误");
}
public Sql pageSql(String type, Integer year, String unionId, String projectName) {
String table = table(type);
return sql("SELECT a.*, un.name unionName, creator.username operatorName FROM " + table +
" a LEFT JOIN sys_union un ON un.id=a.unionId LEFT JOIN sys_user creator ON creator.id=a.updatedBy " +
"WHERE a.year=@year AND (@unionId='' OR a.unionId=@unionId)" +
("special".equals(type) ? " AND (@projectName='' OR LOCATE(@projectName,a.projectName)>0)" : "") +
" ORDER BY un.unionCode,a.createdAt DESC",
"year", year, "unionId", StrUtil.blankToDefault(unionId, ""),
"projectName", StrUtil.blankToDefault(StrUtil.trim(projectName), ""));
}
public List<NutMap> summary(Integer year, String unionId) {
return listMap(sql("""
SELECT un.id, un.name unionName, COALESCE(b.money,0) baseMoney,
COALESCE(s.money,0) specialMoney, COALESCE(b.money,0)+COALESCE(s.money,0) totalMoney
FROM sys_union un
LEFT JOIN (SELECT unionId,SUM(allocateMoney) money FROM outlay_annual_base
WHERE year=@year AND status=1 GROUP BY unionId) b ON b.unionId=un.id
LEFT JOIN (SELECT unionId,SUM(allocateMoney) money FROM outlay_annual_special
WHERE year=@year AND status=1 GROUP BY unionId) s ON s.unionId=un.id
WHERE (@unionId='' OR un.id=@unionId) ORDER BY un.unionCode
""", "year", year, "unionId", StrUtil.blankToDefault(unionId, "")));
}
public List<NutMap> historyRecords(Integer year, String unionId) {
return listMap(sql("""
SELECT a.year,un.name unionName,a.quarterly,a.allocateHeadMoney,a.allocateMoney
FROM outlay_allocate_union a LEFT JOIN sys_union un ON un.id=a.unionId
WHERE a.year=@year AND a.delFlag=0 AND (@unionId='' OR a.unionId=@unionId)
ORDER BY un.unionCode,a.quarterly,a.id
""", "year", year, "unionId", StrUtil.blankToDefault(unionId, "")));
}
public Result generate(Integer year, BigDecimal standard, String unionId) {
if (year == null || year < 1900 || year > Year.now().getValue() || !moneyValid(standard))
return Result.error("请选择有效年度和人均标准(最多两位小数)");
boolean current = year == Year.now().getValue();
if (!current && first(sql("SELECT id FROM member_history WHERE year=@year LIMIT 1", "year", year)) == null)
return Result.error("该年度没有会员历史快照,无法生成统计人数");
List<NutMap> unions = listMap(sql("SELECT id FROM sys_union WHERE (@unionId='' OR id=@unionId) ORDER BY id",
"unionId", StrUtil.blankToDefault(unionId, "")));
int added = 0;
for (NutMap union : unions) {
String uid = union.getString("id");
lockUnion(uid);
if (first(sql("SELECT id FROM outlay_annual_base WHERE year=@year AND unionId=@unionId", "year", year, "unionId", uid)) != null) continue;
NutMap count = first(sql("SELECT COUNT(DISTINCT id) memberCount FROM " + (current ? "vw_user" : "member_history") +
" WHERE member=1 AND unionId=@unionId" + (current ? "" : " AND year=@year"), "unionId", uid, "year", year));
int members = count.getInt("memberCount");
BigDecimal calculated = standard.multiply(BigDecimal.valueOf(members));
if (!moneyValid(calculated)) throw new IllegalArgumentException("计算金额超出允许范围");
String id = UUID.randomUUID().toString().replace("-", "");
execute(sql("""
INSERT INTO outlay_annual_base
(id,year,unionId,memberCount,standard,calculatedMoney,allocateMoney,remark,status,version,createdAt,createdBy,updatedAt,updatedBy)
VALUES (@id,@year,@unionId,@count,@standard,@money,@money,'',0,0,@now,@actor,@now,@actor)
""", "id", id, "year", year, "unionId", uid, "count", members, "standard", standard,
"money", calculated, "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString()));
audit("base", id, "生成", null, first(sql("SELECT * FROM outlay_annual_base WHERE id=@id", "id", id)), "生成年度人数快照");
added++;
}
return Result.success("已生成" + added + "条记录,已有记录保持不变");
}
private void lockUnion(String unionId) {
if (first(sql("SELECT id FROM sys_union WHERE id=@id FOR UPDATE", "id", unionId)) == null)
throw new IllegalArgumentException("分工会不存在");
}
private BigDecimal decimal(Object value) {
return value == null ? BigDecimal.ZERO : new BigDecimal(value.toString());
}
private boolean moneyValid(BigDecimal money) {
return money != null && money.signum() >= 0 && money.scale() <= 2
&& money.compareTo(new BigDecimal("99999999.99")) <= 0;
}
public Result save(String type, String id, Integer year, String unionId, String projectName,
Integer memberCount, BigDecimal standard, BigDecimal allocateMoney, String remark, Integer version) {
String table = table(type);
if (StrUtil.isBlank(id) || !id.matches("[a-fA-F0-9]{32}") || year == null || year < 1900 || year > 9999
|| StrUtil.isBlank(unionId) || !moneyValid(allocateMoney) || remark != null && remark.length() > 1000)
return Result.error("请填写有效年度、分工会和金额,说明最多1000字");
lockUnion(unionId);
NutMap old = first(sql("SELECT * FROM " + table + " WHERE id=@id FOR UPDATE", "id", id));
if (old != null && (old.getInt("year") != year || !unionId.equals(old.getString("unionId"))))
return Result.error("已有记录不能变更年度和分工会");
if (old != null && (version == null || old.getInt("version") != version))
return Result.error("记录已更新,请刷新后重试");
boolean confirmed = old != null && old.getInt("status") == 1;
BigDecimal calculated = null;
if ("base".equals(type)) {
if (old == null) return Result.error("请先生成基础经费记录");
if (confirmed) {
memberCount = old.getInt("memberCount");
standard = new BigDecimal(old.get("standard").toString());
}
if (memberCount == null || memberCount < 0 || !moneyValid(standard)) return Result.error("人数和标准不正确");
calculated = standard.multiply(BigDecimal.valueOf(memberCount));
if (!moneyValid(calculated)) return Result.error("计算金额超出允许范围");
if (allocateMoney.compareTo(calculated) != 0 && StrUtil.isBlank(remark)) return Result.error("实际金额与计算金额不一致,请填写调整说明");
} else if (StrUtil.isBlank(projectName) || projectName.trim().length() > 100 || allocateMoney.signum() <= 0) {
return Result.error("请填写专项名称(最多100字)和大于0的金额");
}
if (confirmed && StrUtil.isBlank(remark)) return Result.error("调整已下拨记录必须填写原因");
if (confirmed) syncBudget(old, allocateMoney.subtract(new BigDecimal(old.get("allocateMoney").toString())));
long now = System.currentTimeMillis();
String actor = StpUtil.getLoginIdAsString();
if (old == null) {
execute(sql("""
INSERT INTO outlay_annual_special
(id,year,unionId,projectName,allocateMoney,remark,status,version,createdAt,createdBy,updatedAt,updatedBy)
VALUES (@id,@year,@unionId,@name,@money,@remark,0,0,@now,@actor,@now,@actor)
""", "id", id, "year", year, "unionId", unionId, "name", projectName.trim(), "money", allocateMoney,
"remark", remark, "now", now, "actor", actor));
} else {
execute(sql("UPDATE " + table + " SET allocateMoney=@money,remark=@remark,updatedAt=@now,updatedBy=@actor,version=version+1" +
("base".equals(type) ? ",memberCount=@count,standard=@standard,calculatedMoney=@calculated" : ",projectName=@name") + " WHERE id=@id",
"money", allocateMoney, "remark", remark, "now", now, "actor", actor, "count", memberCount,
"standard", standard, "calculated", calculated, "name", StrUtil.trim(projectName), "id", id));
}
audit(type, id, confirmed ? "调整下拨" : "保存", old, first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id)), remark);
return Result.success();
}
public Result confirm(String type, String id, Integer version) {
String table = table(type);
NutMap snapshot = first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id));
if (snapshot == null) return Result.error("记录不存在");
lockUnion(snapshot.getString("unionId"));
NutMap row = first(sql("SELECT * FROM " + table + " WHERE id=@id FOR UPDATE", "id", id));
if (row.getInt("status") == 1) return Result.success("该记录已下拨,无需重复操作");
if (version == null || row.getInt("version") != version) return Result.error("记录已更新,请刷新后重试");
confirmRecord(type, table, row);
return Result.success();
}
public Result batchConfirm(String type, Integer year, String unionId, String projectName) {
String table = table(type);
if (year == null || year < 1900 || year > 9999) return Result.error("请选择有效年度");
List<NutMap> pending = listMap(sql("SELECT * FROM " + table +
" WHERE year=@year AND status=0 AND (@unionId='' OR unionId=@unionId)" +
("special".equals(type) ? " AND (@projectName='' OR LOCATE(@projectName,projectName)>0)" : "") +
" ORDER BY unionId,id",
"year", year, "unionId", StrUtil.blankToDefault(unionId, ""),
"projectName", StrUtil.blankToDefault(StrUtil.trim(projectName), "")));
int count = 0;
for (NutMap candidate : pending) {
lockUnion(candidate.getString("unionId"));
NutMap row = first(sql("SELECT * FROM " + table + " WHERE id=@id FOR UPDATE", "id", candidate.getString("id")));
if (row == null || row.getInt("status") == 1) continue;
if (row.getInt("version") != candidate.getInt("version"))
throw new IllegalArgumentException("待下拨记录已被修改,本次操作已取消,请刷新后重试");
confirmRecord(type, table, row);
count++;
}
return Result.success(count == 0 ? "当前筛选范围没有待下拨记录" : "成功下拨" + count + "条记录");
}
private void confirmRecord(String type, String table, NutMap row) {
String id = row.getString("id");
syncBudget(row, new BigDecimal(row.get("allocateMoney").toString()));
execute(sql("UPDATE " + table + " SET status=1,allocatedAt=@now,updatedAt=@now,updatedBy=@actor,version=version+1 WHERE id=@id",
"id", id, "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString()));
audit(type, id, "确认下拨", row, first(sql("SELECT * FROM " + table + " WHERE id=@id", "id", id)), "确认下拨");
}
private void syncBudget(NutMap row, BigDecimal delta) {
List<NutMap> budgets = listMap(sql("SELECT * FROM outlay_manage_union WHERE year=@year AND unionId=@unionId FOR UPDATE",
"year", row.getInt("year"), "unionId", row.getString("unionId")));
if (budgets.size() > 1) throw new IllegalArgumentException("该分工会年度预算存在重复记录,不能下拨");
if (budgets.isEmpty()) {
if (delta.signum() < 0) throw new IllegalArgumentException("年度预算不存在,不能扣减");
// 新建预算承接上年结余;已有预算仅增加本次差额,避免重复结转。
NutMap previous = first(sql("SELECT COALESCE(totalQuota,0)-COALESCE(usedQuota,0) surplus FROM outlay_manage_union WHERE year=@year AND unionId=@unionId",
"year", row.getInt("year") - 1, "unionId", row.getString("unionId")));
BigDecimal carry = previous == null ? BigDecimal.ZERO : new BigDecimal(previous.get("surplus").toString());
if (!moneyValid(carry.add(delta))) throw new IllegalArgumentException("下拨后年度总额度超出允许范围");
execute(sql("""
INSERT INTO outlay_manage_union (id,year,unionId,unionName,unionCode,totalQuota,usedQuota,delFlag,createdAt,updatedAt,createdBy,updatedBy)
SELECT @id,@year,id,name,unionCode,@money,0,0,@now,@now,@actor,@actor FROM sys_union WHERE id=@unionId
""", "id", UUID.randomUUID().toString().replace("-", ""), "year", row.getInt("year"),
"unionId", row.getString("unionId"), "money", carry.add(delta), "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString()));
} else {
NutMap budget = budgets.get(0);
BigDecimal total = decimal(budget.get("totalQuota")).add(delta);
if (!moneyValid(total)) throw new IllegalArgumentException("下拨后年度总额度超出允许范围");
if (total.compareTo(decimal(budget.get("usedQuota"))) < 0)
throw new IllegalArgumentException("调整后总额度不能低于已使用额度");
execute(sql("UPDATE outlay_manage_union SET totalQuota=COALESCE(totalQuota,0)+@delta,updatedAt=@now,updatedBy=@actor WHERE id=@id",
"delta", delta, "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString(), "id", budget.getString("id")));
}
}
private void audit(String type, String id, String action, NutMap before, NutMap after, String remark) {
execute(sql("""
INSERT INTO outlay_annual_allocate_log (id,allocateType,allocateId,action,beforeData,afterData,remark,createdAt,createdBy)
VALUES (@id,@type,@allocateId,@action,@before,@after,@remark,@now,@actor)
""", "id", UUID.randomUUID().toString().replace("-", ""), "type", type, "allocateId", id,
"action", action, "before", before == null ? null : Json.toJson(before), "after", Json.toJson(after),
"remark", remark, "now", System.currentTimeMillis(), "actor", StpUtil.getLoginIdAsString()));
}
}
@@ -42,39 +42,72 @@ public class OutlayManageUnionStatisticsServiceImpl extends BaseServiceImpl impl
@Override
public NutMap queryIncomeExpenseSummary(Integer year, String unionId) {
Cnd cnd = buildUnionManageCondition(year, unionId, true);
Sql sql = Sqls.create("""
SELECT
COALESCE(SUM(t.beginMoney), 0) AS beginMoney,
COALESCE(SUM(t.feeJanApr), 0) AS feeJanApr,
COALESCE(SUM(t.feeMayAug), 0) AS feeMayAug,
COALESCE(SUM(t.feeSepDec), 0) AS feeSepDec,
COALESCE(SUM(t.awardMoney), 0) AS awardMoney,
COALESCE(SUM(t.usedMoney), 0) AS usedMoney,
COALESCE(SUM(t.remainMoney), 0) AS remainMoney
COALESCE(SUM(t.beginMoney),0) AS beginMoney,
COALESCE(SUM(t.baseMoney),0) AS baseMoney,
COALESCE(SUM(t.specialMoney),0) AS specialMoney,
COALESCE(SUM(t.incomeMoney),0) AS incomeMoney,
COALESCE(SUM(t.feeJanApr),0) AS feeJanApr,
COALESCE(SUM(t.feeMayAug),0) AS feeMayAug,
COALESCE(SUM(t.feeSepDec),0) AS feeSepDec,
COALESCE(SUM(t.awardMoney),0) AS awardMoney,
COALESCE(SUM(t.usedMoney),0) AS usedMoney,
COALESCE(SUM(t.remainMoney),0) AS remainMoney
FROM (
SELECT
mu.unionId,
COALESCE(SUM(CASE WHEN au.quarterly = 1 THEN au.allocateHeadMoney ELSE 0 END), 0) AS beginMoney,
COALESCE(SUM(CASE WHEN au.quarterly = 1 THEN au.allocateMoney ELSE 0 END), 0) AS feeJanApr,
COALESCE(SUM(CASE WHEN au.quarterly = 2 THEN au.allocateMoney ELSE 0 END), 0) AS feeMayAug,
COALESCE(SUM(CASE WHEN au.quarterly = 3 THEN au.allocateMoney ELSE 0 END), 0) AS feeSepDec,
COALESCE(SUM(CASE WHEN au.quarterly = 4 THEN au.allocateMoney ELSE 0 END), 0) AS awardMoney,
COALESCE(mu.usedQuota, 0) AS usedMoney,
COALESCE(mu.totalQuota, 0) - COALESCE(mu.usedQuota, 0) AS remainMoney
FROM outlay_manage_union mu
LEFT JOIN outlay_allocate_union au ON au.year = mu.year
AND au.unionId = mu.unionId
AND au.delFlag = 0
LEFT JOIN sys_union su ON su.id = mu.unionId
$condition
GROUP BY mu.year, mu.unionId, mu.usedQuota, mu.totalQuota
) t
""");
sql.setCondition(cnd);
""" + incomeExpenseQuery() + ") t");
sql.params().set("incomeYear", year == null ? DateUtil.thisYear() : year);
sql.setCondition(buildUnionManageCondition(year, unionId, true));
return executeMap(sql);
}
@Override
public Sql buildIncomeExpenseSql(Integer year, String unionId) {
Sql sql = Sqls.create(incomeExpenseQuery() + " ORDER BY COALESCE(mu.unionCode,su.unionCode)");
sql.params().set("incomeYear", year == null ? DateUtil.thisYear() : year);
sql.setCondition(buildUnionManageCondition(year, unionId, true));
return sql;
}
private String incomeExpenseQuery() {
// 历史会费并入年度基础经费,历史奖励并入专项经费;分别汇总避免关联后重复累计。
// 年初数由年度总额度扣除当年各类下拨计算,兼容没有历史第一期记录的新流程。
return """
SELECT mu.year, mu.unionId,
COALESCE(mu.unionName,su.name) AS unionName,
COALESCE(mu.unionCode,su.unionCode) AS unionCode,
COALESCE(mu.totalQuota,0) - (COALESCE(b.money,0) + COALESCE(sp.money,0) + COALESCE(au.feeJanApr,0) + COALESCE(au.feeMayAug,0) + COALESCE(au.feeSepDec,0) + COALESCE(au.awardMoney,0)) AS beginMoney,
COALESCE(b.money,0) + COALESCE(au.feeJanApr,0) + COALESCE(au.feeMayAug,0) + COALESCE(au.feeSepDec,0) AS baseMoney,
COALESCE(sp.money,0) + COALESCE(au.awardMoney,0) AS specialMoney,
(COALESCE(b.money,0) + COALESCE(sp.money,0) + COALESCE(au.feeJanApr,0) + COALESCE(au.feeMayAug,0) + COALESCE(au.feeSepDec,0) + COALESCE(au.awardMoney,0)) AS incomeMoney,
COALESCE(au.feeJanApr,0) AS feeJanApr,
COALESCE(au.feeMayAug,0) AS feeMayAug,
COALESCE(au.feeSepDec,0) AS feeSepDec,
COALESCE(au.awardMoney,0) AS awardMoney,
COALESCE(mu.usedQuota,0) AS usedMoney,
COALESCE(mu.totalQuota,0)-COALESCE(mu.usedQuota,0) AS remainMoney
FROM outlay_manage_union mu
LEFT JOIN sys_union su ON su.id=mu.unionId
LEFT JOIN (
SELECT year,unionId,SUM(allocateMoney) money FROM outlay_annual_base
WHERE year=@incomeYear AND status=1 GROUP BY year,unionId
) b ON b.year=mu.year AND b.unionId=mu.unionId
LEFT JOIN (
SELECT year,unionId,SUM(allocateMoney) money FROM outlay_annual_special
WHERE year=@incomeYear AND status=1 GROUP BY year,unionId
) sp ON sp.year=mu.year AND sp.unionId=mu.unionId
LEFT JOIN (
SELECT year,unionId,
SUM(CASE WHEN quarterly=1 THEN allocateMoney ELSE 0 END) feeJanApr,
SUM(CASE WHEN quarterly=2 THEN allocateMoney ELSE 0 END) feeMayAug,
SUM(CASE WHEN quarterly=3 THEN allocateMoney ELSE 0 END) feeSepDec,
SUM(CASE WHEN quarterly=4 THEN allocateMoney ELSE 0 END) awardMoney
FROM outlay_allocate_union WHERE year=@incomeYear AND delFlag=0 GROUP BY year,unionId
) au ON au.year=mu.year AND au.unionId=mu.unionId
$condition
""";
}
/**
* 构造两个列表共用的预算主表过滤条件,保证汇总和分页列表权限口径一致。
*/
@@ -0,0 +1,141 @@
package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.controller;
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 com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl.OutlayAnnualAllocateService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.*;
import java.math.BigDecimal;
import java.util.UUID;
@IocBean
@At("/platform/outlay/outlayManage/unionAllocate/annual")
@Ok("json:full")
public class OutlayAnnualAllocateController {
@Inject
private OutlayAnnualAllocateService outlayAnnualAllocateService;
@Inject
private SysDictService sysDictService;
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result pageData(PageForm pageForm, String type, Integer year, String unionId, String projectName) {
return Result.success(outlayAnnualAllocateService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(),
outlayAnnualAllocateService.pageSql(type, year, unionId, projectName)));
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result summary(Integer year, String unionId) {
return Result.success(outlayAnnualAllocateService.summary(year, unionId));
}
@At
@Ok("void")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public void doExport(String type, Integer year, String unionId, HttpServletResponse response) {
int queryYear = year == null ? DateUtil.thisYear() : year;
if (queryYear < 1900 || queryYear > 9999) throw new IllegalArgumentException("请选择有效年度");
List<NutMap> rows;
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("年度", "year", 12));
columns.add(new ExcelExportEntity("分工会", "unionName", 30));
String title;
if ("summary".equals(type)) {
title = "下拨汇总";
rows = outlayAnnualAllocateService.summary(queryYear, unionId);
for (NutMap row : rows) row.put("year", queryYear);
columns.add(new ExcelExportEntity("基础经费已下拨(元)", "baseMoney", 24));
columns.add(new ExcelExportEntity("专项经费已下拨(元)", "specialMoney", 24));
columns.add(new ExcelExportEntity("新流程合计下拨(元)", "totalMoney", 24));
} else if ("history".equals(type)) {
title = "历史季度记录";
rows = outlayAnnualAllocateService.historyRecords(queryYear, unionId);
Map<String, String> projectNames = new HashMap<>();
for (Sys_dict dict : sysDictService.getSubListByCode("OUTLAY_QUARTERLY")) {
projectNames.put(dict.getCode(), dict.getName());
}
for (NutMap row : rows) {
String code = row.getString("quarterly");
row.put("projectName", projectNames.getOrDefault(code, code));
}
columns.add(new ExcelExportEntity("分配项目", "projectName", 24));
columns.add(new ExcelExportEntity("分配前额度(元)", "allocateHeadMoney", 24));
columns.add(new ExcelExportEntity("分配金额(元)", "allocateMoney", 24));
} else {
throw new IllegalArgumentException("不支持的导出类型");
}
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, columns, rows);
CommonDownloadUtil.download(queryYear + "年" + title + ".xlsx", workbook, response);
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result newId() {
return Result.success("", UUID.randomUUID().toString().replace("-", ""));
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
@POST
@Aop(TransAop.READ_COMMITTED)
public Result generate(Integer year, @Param("standard") BigDecimal standard, String unionId) {
return outlayAnnualAllocateService.generate(year, standard, unionId);
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
@POST
@Aop(TransAop.READ_COMMITTED)
public Result save(String type, String id, Integer year, String unionId, String projectName,
Integer memberCount, @Param("standard") BigDecimal standard, @Param("allocateMoney") BigDecimal allocateMoney, String remark, Integer version) {
return outlayAnnualAllocateService.save(type, id, year, unionId, projectName, memberCount, standard, allocateMoney, remark, version);
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
@POST
@Aop(TransAop.READ_COMMITTED)
public Result confirm(String type, String id, Integer version) {
return outlayAnnualAllocateService.confirm(type, id, version);
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
@POST
@Aop(TransAop.READ_COMMITTED)
public Result batchConfirm(String type, Integer year, String unionId, String projectName) {
return outlayAnnualAllocateService.batchConfirm(type, year, unionId, projectName);
}
@At
@SaCheckPermission("outlay.outlayManage.union.allocate")
public Result logs(PageForm pageForm, String type, String id) {
return Result.success(outlayAnnualAllocateService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), outlayAnnualAllocateService.sql("""
SELECT l.*,u.username operatorName FROM outlay_annual_allocate_log l
LEFT JOIN sys_user u ON u.id=l.createdBy
WHERE l.allocateType=@type AND l.allocateId=@id ORDER BY l.createdAt DESC,l.id
""", "type", type, "id", id)));
}
}
@@ -53,7 +53,7 @@ public class OutlayManageUnionAllocateController {
private OutlayManageUnionAllocateService outlayManageUnionAllocateService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/union/allocate/index.html")
@Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/union/allocate/annual.html")
@SaCheckPermission("outlay.outlayManage.union.allocate")
public void index() {
}
@@ -6,22 +6,17 @@ import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
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.base.utils.CommonDownloadUtil;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayManageUnionStatisticsService;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.vo.OutlayUnionIncomeExpenseVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -85,11 +80,10 @@ public class OutlayManageUnionIncomeExpenseController {
ArrayList<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("单位", "unionName", 25));
entities.add(new ExcelExportEntity("年初数", "beginMoney", 20));
entities.add(new ExcelExportEntity("1-4月会费", "feeJanApr", 20));
entities.add(new ExcelExportEntity("5-8月会费", "feeMayAug", 20));
entities.add(new ExcelExportEntity("9-12月会费", "feeSepDec", 20));
entities.add(new ExcelExportEntity("评优奖励", "awardMoney", 20));
entities.add(new ExcelExportEntity("1-12月支出", "usedMoney", 20));
entities.add(new ExcelExportEntity("年度基础经费", "baseMoney", 20));
entities.add(new ExcelExportEntity("专项经费", "specialMoney", 20));
entities.add(new ExcelExportEntity("合计下拨", "incomeMoney", 20));
entities.add(new ExcelExportEntity("年度支出", "usedMoney", 20));
entities.add(new ExcelExportEntity("经费余额", "remainMoney", 20));
ExportParams exportParams = new ExportParams();
@@ -98,52 +92,8 @@ public class OutlayManageUnionIncomeExpenseController {
CommonDownloadUtil.download(queryYear + "年二级分工会经费收支使用情况表.xlsx", workbook, response);
}
/**
* 统计参数说明:
* year 为统计年度,未传时默认当前年份;unionId 为二级分工会主键,管理员可为空查询全部。
* 返回 VO 中 beginMoney 是年初数,feeJanApr/feeMayAug/feeSepDec 是三个会费收入段,
* awardMoney 是评优奖励,usedMoney 是年度支出,remainMoney 是当前经费余额。
*/
private Sql buildIncomeExpenseSql(Integer year, String unionId) {
Sql sql = Sqls.create("""
SELECT
mu.year AS year,
mu.unionId AS unionId,
COALESCE(mu.unionName, su.name) AS unionName,
COALESCE(mu.unionCode, su.unionCode) AS unionCode,
COALESCE(SUM(CASE WHEN au.quarterly = 1 THEN au.allocateHeadMoney ELSE 0 END), 0) AS beginMoney,
COALESCE(SUM(CASE WHEN au.quarterly = 1 THEN au.allocateMoney ELSE 0 END), 0) AS feeJanApr,
COALESCE(SUM(CASE WHEN au.quarterly = 2 THEN au.allocateMoney ELSE 0 END), 0) AS feeMayAug,
COALESCE(SUM(CASE WHEN au.quarterly = 3 THEN au.allocateMoney ELSE 0 END), 0) AS feeSepDec,
COALESCE(SUM(CASE WHEN au.quarterly = 4 THEN au.allocateMoney ELSE 0 END), 0) AS awardMoney,
COALESCE(mu.usedQuota, 0) AS usedMoney,
COALESCE(mu.totalQuota, 0) - COALESCE(mu.usedQuota, 0) AS remainMoney
FROM outlay_manage_union mu
LEFT JOIN sys_union su ON su.id = mu.unionId
LEFT JOIN outlay_allocate_union au ON au.year = mu.year
AND au.unionId = mu.unionId
AND au.delFlag = 0
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("mu.delFlag", "=", false);
cnd.and("mu.year", "=", getQueryYear(year));
cnd.and("mu.totalQuota", "IS NOT", null);
cnd.andEX("mu.unionId", "=", unionId);
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("mu.unionId", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("mu.year");
cnd.groupBy("mu.unionId");
cnd.groupBy("mu.unionName");
cnd.groupBy("mu.unionCode");
cnd.groupBy("su.name");
cnd.groupBy("su.unionCode");
cnd.groupBy("mu.usedQuota");
cnd.groupBy("mu.totalQuota");
cnd.asc("mu.unionCode");
sql.setCondition(cnd);
return sql;
return outlayManageUnionStatisticsService.buildIncomeExpenseSql(year, unionId);
}
private Integer getQueryYear(Integer year) {
@@ -20,6 +20,12 @@ public class OutlayUnionIncomeExpenseVO {
private BigDecimal beginMoney;
private BigDecimal baseMoney;
private BigDecimal specialMoney;
private BigDecimal incomeMoney;
private BigDecimal feeJanApr;
private BigDecimal feeMayAug;
@@ -0,0 +1,58 @@
-- 年度基础经费与专项经费下拨;原 outlay_allocate_union 历史记录及余额保持不变。
-- 人数口径沿用会员统计:本年度 vw_user.member=1,历史年度 member_history.member=1。
-- 新表仅记录新流程数据,不自动将历史季度下拨转为年度基础经费,避免重复计入预算。
CREATE TABLE IF NOT EXISTS `outlay_annual_base` (
`id` varchar(32) NOT NULL,
`year` int NOT NULL,
`unionId` varchar(32) NOT NULL,
`memberCount` int NOT NULL,
`standard` decimal(10,2) NOT NULL DEFAULT 800.00,
`calculatedMoney` decimal(10,2) NOT NULL,
`allocateMoney` decimal(10,2) NOT NULL,
`remark` varchar(1000) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待下拨 1已下拨',
`version` int NOT NULL DEFAULT 0,
`allocatedAt` bigint DEFAULT NULL,
`createdAt` bigint NOT NULL,
`createdBy` varchar(32) NOT NULL,
`updatedAt` bigint NOT NULL,
`updatedBy` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_annual_base_year_union` (`year`,`unionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='年度基础经费下拨';
CREATE TABLE IF NOT EXISTS `outlay_annual_special` (
`id` varchar(32) NOT NULL COMMENT '同时作为提交幂等标识',
`year` int NOT NULL,
`unionId` varchar(32) NOT NULL,
`projectName` varchar(100) NOT NULL,
`allocateMoney` decimal(10,2) NOT NULL,
`remark` varchar(1000) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待下拨 1已下拨',
`version` int NOT NULL DEFAULT 0,
`allocatedAt` bigint DEFAULT NULL,
`createdAt` bigint NOT NULL,
`createdBy` varchar(32) NOT NULL,
`updatedAt` bigint NOT NULL,
`updatedBy` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_annual_special_year_union` (`year`,`unionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专项经费下拨';
CREATE TABLE IF NOT EXISTS `outlay_annual_allocate_log` (
`id` varchar(32) NOT NULL,
`allocateType` varchar(10) NOT NULL,
`allocateId` varchar(32) NOT NULL,
`action` varchar(30) NOT NULL,
`beforeData` text DEFAULT NULL COMMENT '操作前完整快照',
`afterData` text NOT NULL COMMENT '操作后完整快照',
`remark` varchar(1000) DEFAULT NULL,
`createdAt` bigint NOT NULL,
`createdBy` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_annual_log_record_time` (`allocateType`,`allocateId`,`createdAt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='经费下拨及调整记录';
-- 沿用现有权限与入口,仅调整菜单名称。
UPDATE sys_menu SET name='年度经费下拨', aliasName='年度经费下拨'
WHERE permission='outlay.outlayManage.union.allocate';
@@ -0,0 +1,248 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
<!--#include("../fixedListLayout.css"){}#-->
.fund-edit-dialog .el-dialog { max-width: calc(100vw - 32px); }
.fund-edit-dialog .el-dialog__body { padding: 20px 28px; max-height: 65vh; overflow-y: auto; }
.fund-edit-dialog .el-dialog__footer { padding: 16px 28px; border-top: 1px solid #ebeef5; }
.fund-edit-dialog .el-alert { margin-bottom: 20px; }
.fund-edit-section { grid-column: 1 / -1; font-weight: 600; color: #303133; border-bottom: 1px solid #ebeef5; padding-bottom: 10px; }
.fund-edit-hint { color: #909399; font-size: 12px; line-height: 20px; }
.fund-edit-difference { grid-column: 1 / -1; background: #fdf6ec; color: #b88230; border-radius: 4px; padding: 10px 14px; line-height: 22px; }
.fund-edit-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px; }
.fund-edit-form .el-form-item { margin-bottom: 0; min-width: 0; }
.fund-edit-form .el-form-item__label { padding-bottom: 6px; line-height: 24px; }
.fund-edit-form .el-select,
.fund-edit-form .el-date-editor,
.fund-edit-form .el-input-number { width: 100%; }
.fund-edit-form .fund-edit-wide { grid-column: 1 / -1; }
.fund-edit-amount { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; }
.fund-edit-amount .el-input-number { flex: 1; min-width: 200px; }
.fund-edit-calculated { display: block; padding: 0 15px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; font-size: 20px; font-weight: 600; color: #303133; }
@media (max-width: 600px) {
.fund-edit-form { grid-template-columns: minmax(0, 1fr); }
}
</style>
<div id="app" v-cloak>
<guava ref="guava"><template>
<div class="outlay-union-fixed-list-page">
<el-card class="outlay-union-fixed-query-card" shadow="never">
<search @search="doSearch">
<search-item label="年度"><el-date-picker v-model="query.year" type="year" value-format="yyyy" :clearable="false" @change="doSearch"></el-date-picker></search-item>
<search-item label="分工会"><el-select v-model="query.unionId" clearable filterable @change="doSearch"><el-option v-for="u in unionList" :key="u.id" :value="u.id" :label="u.name"></el-option></el-select></search-item>
<search-item v-if="tab==='special'" label="专项名称"><el-input v-model="query.projectName" clearable placeholder="请输入专项名称" @keyup.enter.native="doSearch" @clear="doSearch"></el-input></search-item>
</search>
</el-card>
<el-card class="outlay-union-fixed-list-card mt10" shadow="never">
<el-tabs class="outlay-union-summary-table" v-model="tab" @tab-click="changeTab">
<el-tab-pane label="年度基础经费" name="base"></el-tab-pane>
<el-tab-pane label="专项经费" name="special"></el-tab-pane>
<el-tab-pane label="下拨汇总" name="summary"></el-tab-pane>
<el-tab-pane label="历史季度记录" name="history"></el-tab-pane>
</el-tabs>
<table-tool label="年度经费下拨">
<el-button v-if="tab==='base'" size="small" type="primary" @click="generateVisible=true" :disabled="busy">生成年度基础经费</el-button>
<el-button v-if="tab==='special'" size="small" type="primary" @click="addSpecial" :disabled="busy">新增专项经费</el-button>
<el-button v-if="tab==='base' || tab==='special'" size="small" type="success" @click="batchConfirm" :loading="busy">一键下拨</el-button>
<el-button v-if="tab==='summary' || tab==='history'" size="small" type="primary" @click="doExport" :disabled="loading || busy">导出Excel</el-button>
</table-tool>
<el-table :key="tab" ref="tableRef" :data="rows" :height="tableHeight" v-loading="loading" border row-key="id">
<el-table-column v-if="tab!=='summary'" prop="year" label="年度" width="85"></el-table-column>
<el-table-column prop="unionName" label="分工会" min-width="170"></el-table-column>
<template v-if="tab==='base'">
<el-table-column prop="memberCount" label="统计人数" width="100"></el-table-column>
<el-table-column prop="standard" label="人均标准(元/年)" width="150"></el-table-column>
<el-table-column prop="calculatedMoney" label="计算金额(元)" width="140"></el-table-column>
</template>
<el-table-column v-if="tab==='special'" prop="projectName" label="专项名称" min-width="150"></el-table-column>
<template v-if="tab==='summary'">
<el-table-column prop="baseMoney" label="基础经费已下拨(元)" min-width="160"></el-table-column>
<el-table-column prop="specialMoney" label="专项经费已下拨(元)" min-width="160"></el-table-column>
<el-table-column prop="totalMoney" label="新流程合计下拨(元)" min-width="160"></el-table-column>
</template>
<template v-else-if="tab==='history'">
<el-table-column label="分配项目" min-width="140"><template slot-scope="{row}"><dict-tag :options="dict.type.OUTLAY_QUARTERLY" :value="row.quarterly"></dict-tag></template></el-table-column>
<el-table-column prop="allocateHeadMoney" label="分配前额度(元)" min-width="140"></el-table-column>
<el-table-column prop="allocateMoney" label="分配金额(元)" min-width="140"></el-table-column>
</template>
<template v-else>
<el-table-column prop="allocateMoney" label="实际下拨金额(元)" width="160"></el-table-column>
<el-table-column label="状态" width="100"><template slot-scope="{row}">{{Number(row.status)===1?'已下拨':'待下拨'}}</template></el-table-column>
<el-table-column prop="remark" label="说明" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column label="下拨时间" width="165"><template slot-scope="{row}">{{time(row.allocatedAt)}}</template></el-table-column>
<el-table-column prop="operatorName" label="操作人" width="100"></el-table-column>
<el-table-column label="最后操作时间" width="165"><template slot-scope="{row}">{{time(row.updatedAt)}}</template></el-table-column>
<el-table-column label="操作" fixed="right" width="220"><template slot-scope="{row}">
<el-button type="text" :disabled="busy" @click="edit(row)">{{Number(row.status)===1?'调整':'编辑'}}</el-button>
<el-button v-if="Number(row.status)===0" type="text" :disabled="busy" @click="confirm(row)">确认下拨</el-button>
<el-button type="text" @click="showLogs(row)">操作记录</el-button>
</template></el-table-column>
</template>
</el-table>
<div class="el-pagination-container" v-if="tab!=='summary'">
<el-pagination :current-page.sync="query.pageNumber" :page-size.sync="query.pageSize" :total="total" layout="total, sizes, prev, pager, next" @current-change="load" @size-change="doSearch"></el-pagination>
</div>
</el-card>
</div>
<el-dialog title="生成年度基础经费" :visible.sync="generateVisible" width="550px" :close-on-click-modal="false">
<el-alert title="按所选年度、分工会生成;已有记录不覆盖。人数沿用会员统计口径并保存快照,确认下拨前请核对。" type="info" :closable="false"></el-alert>
<el-form label-width="130px" class="mt10">
<el-form-item label="年度">{{query.year}}</el-form-item>
<el-form-item label="分工会">{{selectedUnionName}}</el-form-item>
<el-form-item label="人均标准(元/年)"><el-input-number v-model="standard" :min="0" :max="99999999.99" :precision="2"></el-input-number></el-form-item>
</el-form>
<span slot="footer"><el-button @click="generateVisible=false" :disabled="busy">取消</el-button><el-button type="primary" :loading="busy" @click="generate">生成</el-button></span>
</el-dialog>
<el-dialog :title="form.status===1?'调整已下拨经费':'编辑经费'" :visible.sync="editVisible" width="860px" class="fund-edit-dialog" top="6vh" :close-on-click-modal="false" :show-close="!busy">
<el-alert v-if="form.status===1" title="当前经费已下拨,保存后将按新旧金额差额调整年度预算,请填写调整原因。" type="warning" show-icon :closable="false"></el-alert>
<el-form class="fund-edit-form" label-position="top">
<div class="fund-edit-section">基础信息 · {{tab==='base'?'年度基础经费':'专项经费'}}</div>
<el-form-item label="年度"><el-date-picker v-model="form.year" type="year" value-format="yyyy" :clearable="false" :disabled="!isNew"></el-date-picker></el-form-item>
<el-form-item label="分工会"><el-select v-model="form.unionId" filterable :disabled="!isNew"><el-option v-for="u in unionList" :key="u.id" :value="u.id" :label="u.name"></el-option></el-select></el-form-item>
<div class="fund-edit-section">金额核算</div>
<template v-if="tab==='base'">
<el-form-item label="统计人数"><el-input-number v-model="form.memberCount" :min="0" :max="99999999" :precision="0" :disabled="form.status===1"></el-input-number></el-form-item>
<el-form-item label="人均标准(元/年)"><el-input-number v-model="form.standard" :min="0" :max="99999999.99" :precision="2" :disabled="form.status===1"></el-input-number></el-form-item>
<el-form-item label="计算金额(元)"><span class="fund-edit-calculated">{{calculated}} <small>元</small></span><div class="fund-edit-hint">统计人数 × 人均年度标准</div></el-form-item>
</template>
<el-form-item v-else label="专项名称" class="fund-edit-wide" required><el-input v-model="form.projectName" maxlength="100" placeholder="如:一会一品、评优奖励"></el-input></el-form-item>
<el-form-item label="实际下拨金额(元)" :class="{'fund-edit-wide': tab!=='base'}" required><div class="fund-edit-amount"><el-input-number v-model="form.allocateMoney" :min="0" :max="99999999.99" :precision="2" controls-position="right"></el-input-number><el-button v-if="tab==='base' && form.status===0" type="text" @click="form.allocateMoney=Number(calculated)">使用计算金额</el-button></div></el-form-item>
<div v-if="form.status===1 || amountDifference!==0" class="fund-edit-difference">
<template v-if="form.status===1">原下拨金额:{{Number(originalAllocateMoney).toFixed(2)}} 元;本次调整:{{amountDifference>0?'+':''}}{{amountDifference.toFixed(2)}} 元。</template>
<template v-else>实际下拨金额较计算金额{{amountDifference>0?'增加':'减少'}} {{Math.abs(amountDifference).toFixed(2)}} 元,请填写原因。</template>
</div>
<el-form-item :label="form.status===1?'调整原因':'说明'" class="fund-edit-wide" :required="remarkRequired"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit :placeholder="remarkRequired?'请填写本次金额调整的具体原因':'可填写经费用途或其他说明'"></el-input></el-form-item>
</el-form>
<span slot="footer"><el-button @click="editVisible=false" :disabled="busy">取消</el-button><el-button type="primary" :loading="busy" @click="save">{{form.status===1?'保存调整':'保存'}}</el-button></span>
</el-dialog>
<el-dialog title="经费操作记录" :visible.sync="logsVisible" width="900px">
<el-table :data="logs" border v-loading="logsLoading">
<el-table-column prop="action" label="操作" width="100"></el-table-column>
<el-table-column prop="operatorName" label="操作人" width="100"></el-table-column>
<el-table-column label="时间" width="165"><template slot-scope="{row}">{{time(row.createdAt)}}</template></el-table-column>
<el-table-column label="操作前" min-width="180"><template slot-scope="{row}">{{snapshot(row.beforeData)}}</template></el-table-column>
<el-table-column label="操作后" min-width="180"><template slot-scope="{row}">{{snapshot(row.afterData)}}</template></el-table-column>
<el-table-column prop="remark" label="说明" min-width="140"></el-table-column>
</el-table>
<el-pagination :current-page.sync="logPage" :page-size="10" :total="logTotal" layout="total,prev,pager,next" @current-change="loadLogs"></el-pagination>
</el-dialog>
</template></guava>
</div>
<script nonce="${cspNonce!}">
<!--#include("../fixedListLayout.js"){}#-->
new Vue({
el: '#app', store,
mixins: [OUTLAY_UNION_FIXED_LIST_MIXIN],
dicts: ['OUTLAY_QUARTERLY'],
data() { return {
tab: 'base', query: {year: this.$moment().format('YYYY'), unionId: '', projectName: '', pageNumber: 1, pageSize: 20},
unionList: [], rows: [], total: 0, loading: false, busy: false, requestSequence: 0,
standard: 800, generateVisible: false, editVisible: false, isNew: false, form: {}, originalAllocateMoney: 0,
logsVisible: false, logsLoading: false, logs: [], logPage: 1, logTotal: 0, logRecord: null
}},
computed: {
calculated() { return ((this.form.memberCount || 0) * (this.form.standard || 0)).toFixed(2) },
amountDifference() {
const amount=Number(this.form.allocateMoney)
if (!Number.isFinite(amount)) return 0
const reference=this.form.status===1?Number(this.originalAllocateMoney):(this.tab==='base'?Number(this.calculated):amount)
return Number((amount-reference).toFixed(2))
},
remarkRequired() { return this.form.status===1 || (this.tab==='base' && this.amountDifference!==0) },
selectedUnionName() { const u=this.unionList.find(u=>u.id===this.query.unionId); return u?u.name:'全部分工会' }
},
methods: {
time(value) { return value?this.$moment(Number(value)).format('YYYY-MM-DD HH:mm:ss'):'—' },
snapshot(value) {
if (!value) return '—'
try { const v=JSON.parse(value); return v.year+'年 / '+(v.memberCount==null?(v.projectName || ''):v.memberCount+'人 × '+v.standard+'元,计算 '+v.calculatedMoney+'元')+' / 实际 '+v.allocateMoney+'元 / '+(Number(v.status)===1?'已下拨':'待下拨') }
catch(e) { return value }
},
doExport() {
if (!this.query.year) { this.$message.warning('请选择年度'); return }
const params='type='+encodeURIComponent(this.tab)+'&year='+encodeURIComponent(this.query.year)+'&unionId='+encodeURIComponent(this.query.unionId || '')
window.open('/platform/outlay/outlayManage/unionAllocate/annual/doExport?'+params)
},
async request(action, data) {
const resp=await this.$axios.post('/platform/outlay/outlayManage/unionAllocate/annual/'+action, data)
if (resp.code!==0) { this.$message.error(resp.msg || '操作失败'); throw new Error(resp.msg) }
return resp
},
changeTab() {
this.rows=[]
this.total=0
this.doSearch()
this.updateOutlayUnionTableHeight()
},
doSearch() { this.query.pageNumber=1; this.load() },
async load() {
const sequence=++this.requestSequence
this.loading=true
try {
let resp
if (this.tab==='history') resp=await this.$axios.post('/platform/outlay/outlayManage/unionAllocate/pageData', this.query)
else resp=await this.request(this.tab==='summary'?'summary':'pageData', Object.assign({},this.query,{type:this.tab}))
if (sequence!==this.requestSequence) return
if (resp.code===0) { this.rows=this.tab==='summary'?(resp.data || []):(resp.data.list || []); this.total=this.tab==='summary'?0:resp.data.totalCount }
} catch(e) { if(sequence===this.requestSequence) this.rows=[] }
finally { if(sequence===this.requestSequence) { this.loading=false; this.updateOutlayUnionTableHeight() } }
},
async generate() {
this.busy=true
try { const resp=await this.request('generate',Object.assign({},this.query,{standard:this.standard})); this.$message.success(resp.msg); this.generateVisible=false; this.doSearch() }
catch(e) {} finally { this.busy=false }
},
edit(row) {
this.isNew=false
this.originalAllocateMoney=Number(row.allocateMoney)
this.form=Object.assign({},row,{year:String(row.year), status:Number(row.status), memberCount:Number(row.memberCount), standard:Number(row.standard), allocateMoney:Number(row.allocateMoney)})
this.editVisible=true
},
async addSpecial() {
this.busy=true
try { const resp=await this.request('newId',{}); this.isNew=true; this.form={id:resp.data,year:this.query.year,unionId:this.query.unionId,projectName:'',allocateMoney:0,remark:'',status:0}; this.editVisible=true }
catch(e) {} finally { this.busy=false }
},
async save() {
if (!this.form.year || !this.form.unionId || this.form.allocateMoney==null) { this.$message.warning('请填写年度、分工会和金额'); return }
if (this.busy) return
if (this.remarkRequired && !(this.form.remark || '').trim()) { this.$message.warning('请填写金额调整原因'); return }
this.busy=true
try { await this.request('save',Object.assign({},this.form,{type:this.tab})); this.$message.success('保存成功'); this.editVisible=false; this.doSearch() }
catch(e) {} finally { this.busy=false }
},
async batchConfirm() {
if (this.busy) return
const params={type:this.tab,year:this.query.year,unionId:this.query.unionId || '',projectName:this.tab==='special'?(this.query.projectName || '').trim():''}
const typeName=this.tab==='base'?'年度基础经费':'专项经费'
const scope=this.selectedUnionName+(params.projectName?' / 专项名称包含:'+params.projectName:'')
if (!params.year) { this.$message.warning('请选择年度'); return }
this.busy=true
try {
await this.$confirm('确认一键下拨【'+params.year+'年 / '+scope+'】的全部待下拨'+typeName+'?范围包含所有分页,已下拨记录自动跳过,请先核对金额及历史下拨记录。','一键下拨',{type:'warning'})
const resp=await this.request('batchConfirm',params)
this.$message.success(resp.msg)
this.load()
} catch(e) {} finally { this.busy=false }
},
async confirm(row) {
try { await this.$confirm('确认向【'+row.unionName+'】下拨 '+row.allocateMoney+' 元?此操作将计入年度预算,请核对历史下拨记录。','确认下拨',{type:'warning'}) }
catch(e) { return }
this.busy=true
try { await this.request('confirm',{type:this.tab,id:row.id,version:row.version}); this.$message.success('下拨成功'); this.load() }
catch(e) {} finally { this.busy=false }
},
showLogs(row) { this.logRecord={id:row.id,type:this.tab}; this.logPage=1; this.logsVisible=true; this.loadLogs() },
async loadLogs() {
this.logsLoading=true
try { const resp=await this.request('logs',Object.assign({},this.logRecord,{pageNumber:this.logPage,pageSize:10})); this.logs=resp.data.list || []; this.logTotal=resp.data.totalCount }
catch(e) {} finally { this.logsLoading=false }
}
},
created() { this.$businessTool.listUnion().then(data=>{this.unionList=data}); this.load() }
})
</script>
<!--#
}
#-->
@@ -61,52 +61,19 @@ layout("/layouts/platform.html"){
{{formatMoney(row.beginMoney)}}
</template>
</el-table-column>
<el-table-column align="center" header-align="center" :label="pageForm.year + '年会费收入'">
<el-table-column
align="center"
header-align="center"
label="1-4月会费"
prop="feeJanApr"
min-width="120">
<template v-slot="{row}">
{{formatMoney(row.feeJanApr)}}
</template>
</el-table-column>
<el-table-column
align="center"
header-align="center"
label="5-8月会费"
prop="feeMayAug"
min-width="120">
<template v-slot="{row}">
{{formatMoney(row.feeMayAug)}}
</template>
</el-table-column>
<el-table-column
align="center"
header-align="center"
label="9-12月会费"
prop="feeSepDec"
min-width="120">
<template v-slot="{row}">
{{formatMoney(row.feeSepDec)}}
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="年度基础经费" prop="baseMoney" min-width="120">
<template v-slot="{row}">{{formatMoney(row.baseMoney)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="专项经费" prop="specialMoney" min-width="120">
<template v-slot="{row}">{{formatMoney(row.specialMoney)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="合计下拨" prop="incomeMoney" min-width="120">
<template v-slot="{row}">{{formatMoney(row.incomeMoney)}}</template>
</el-table-column>
<el-table-column
align="center"
header-align="center"
label="评优奖励"
prop="awardMoney"
min-width="120">
<template v-slot="{row}">
{{formatMoney(row.awardMoney)}}
</template>
</el-table-column>
<el-table-column
align="center"
header-align="center"
label="1-12月支出"
label="年度支出"
prop="usedMoney"
min-width="120">
<template v-slot="{row}">
@@ -141,19 +108,16 @@ layout("/layouts/platform.html"){
<el-table-column align="center" header-align="center" label="年初数" prop="beginMoney" min-width="120">
<template v-slot="{row}">{{summaryValue('beginMoney', row)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="1-4月会费" prop="feeJanApr" min-width="120">
<template v-slot="{row}">{{summaryValue('feeJanApr', row)}}</template>
<el-table-column align="center" header-align="center" label="年度基础经费" prop="baseMoney" min-width="120">
<template v-slot="{row}">{{summaryValue('baseMoney', row)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="5-8月会费" prop="feeMayAug" min-width="120">
<template v-slot="{row}">{{summaryValue('feeMayAug', row)}}</template>
<el-table-column align="center" header-align="center" label="专项经费" prop="specialMoney" min-width="120">
<template v-slot="{row}">{{summaryValue('specialMoney', row)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="9-12月会费" prop="feeSepDec" min-width="120">
<template v-slot="{row}">{{summaryValue('feeSepDec', row)}}</template>
<el-table-column align="center" header-align="center" label="合计下拨" prop="incomeMoney" min-width="120">
<template v-slot="{row}">{{summaryValue('incomeMoney', row)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="评优奖励" prop="awardMoney" min-width="120">
<template v-slot="{row}">{{summaryValue('awardMoney', row)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="本年累计支出" prop="usedMoney" min-width="120">
<el-table-column align="center" header-align="center" label="年度支出" prop="usedMoney" min-width="120">
<template v-slot="{row}">{{summaryValue('usedMoney', row)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="经费余额" prop="remainMoney" min-width="120">