按照我们的设计,项目下边是预算,项目预算包含了若干预算条目,每一个预算条目都由收入,成本,费用组成,并最终计算出每个条目的利润和总的利润,还可以设置虚拟预算条目,用于集中那些调整账目,这都是要在设计的时候考虑的。
这里为了方便,再把设计的路径贴一下:
pms/budget/{pid}/list
,用于列出某个项目对应的所有预算pms/budget/{pid}/detail/{id}
,用于列出某个预算条目的详情pms/budget/{pid}/add
,用于新增预算pms/budget/{pid}/edit/{id}
,用于编辑某个预算条目pms/budget/delete
,用于删除项目
修改@Transactional
注解
这里我突然发现,标注在业务类中方法上的@Transactional
注解,使用的是javax
而非Spring
提供的,这里就把所有的业务类中的注解都统统改掉了。
预算PO
类及准备
上次已经编写过了,这次看了一下,忘记加上一个备注了:
@Length(max = 255, message = "备注最长255个字符")
@Column(name = "description")
private String description;
然后就是业务类,先编写删除、保存和查询的方法
@Service
public class BudgetService {
private final BudgetDao budgetDao;
@Autowired
public BudgetService(BudgetDao budgetDao) {
this.budgetDao = budgetDao;
}
@Transactional
public void deleteBudget(int id) {
budgetDao.deleteById(id);
}
@Transactional
public Budget findById(int id) {
return budgetDao.findById(id).orElseThrow(() -> new RuntimeException("找不到对应的预算条目 id=" + id));
}
@Transactional
public List<Budget> findByProjectId(int id) {
return budgetDao.findBudgetByProjectIdOrderByCreateTime(id);
}
@Transactional
public Budget save(Budget budget) {
return budgetDao.save(budget);
}
}
然后是Dao
类,要编写一个方法,直接根据外键查询:
public interface BudgetDao extends JpaRepository<Budget, Integer> {
List<Budget> findBudgetByProjectIdOrderByCreateTime(int id);
}
这样就做好了准备。
预算展示控制器及页面初步编写
这里的页面就不像项目一样非常简单了,必须把利润和合计数计算出来,因此这里会涉及到一些其他的功能编写。
控制器及页面初步编写
随着不断的开发,我们的编写也越来越成熟,预算展示页面,我们会获取对应的项目id
来查找所对应的预算条目。控制器比较简单:
@GetMapping("/{pid}/list")
public String budgetList(@PathVariable("pid") int id, Model model) {
Project project = projectService.findById(id);
List<Budget> budgetList = budgetService.findByProjectId(id);
model.addAttribute("project", project);
model.addAttribute("budgets", budgetService.findByProjectId(id));
model.addAttribute("count", budgetList.size());
return "pms/budget/budgetList";
}
这里不要忘记依然需要在控制器中把获取是否管理员的那个方法加进来:
@ModelAttribute
public void getCurrentUser(Model model) {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("isAdmin", user.hasRole("ADMIN"));
}
页面也没什么好说的,就是一个表格,套路也是点进去查看详情,这里我仅仅展示税后金额,以便计算利润,页面如下:
<main class="container">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb" class="mt-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/home" th:href="@{/home}" class="link-secondary">首页</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list" th:href="@{/pms/project/list}"
class="link-secondary">项目管理</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list" th:href="${project.absoluteUrl()}" class="link-secondary">[[${project.projectName}]]</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/budget/list" th:href="${project.budgetUrl()}" class="link-secondary">预算</a></li>
</ol>
</nav>
<div class="row py-1" >
<div class="col-sm-4"></div>
<div th:if="${created}" class="text-center alert alert-success col-12 col-sm-4" role="alert">
成功添加预算条目 [[${created}]]
</div>
<div th:if="${updated}" class="text-center alert alert-success col-12 col-sm-4" role="alert">
成功修改预算条目 [[${updated}]]
</div>
<div th:if="${deleted}" class="text-center alert alert-success col-12 col-sm-4" role="alert">
已删除预算条目 [[${deleted}]]
</div>
<div class="col-sm-4"></div>
</div>
<p>
<a class="btn btn-primary" href="pms/budget/add" th:href="${project.addBudgetUrl()}"><i class="fas fa-plus"></i> 新增预算</a>
</p>
<table class="table table-hover table-responsive" th:if="${count != 0}">
<thead>
<tr>
<th>序号</th>
<th>预算名称</th>
<th>收入</th>
<th>成本</th>
<th>费用</th>
</tr>
</thead>
<tbody>
<tr th:each="budget, control:${budgets}">
<td th:text="${control.count}"></td>
<td><a>[[${budget.budgetName}]]</a></td>
<td th:text="${#numbers.formatDecimal(budget.netIncome,0,'COMMA',2,'POINT')}"></td>
<td th:text="${#numbers.formatDecimal(budget.netCost,0,'COMMA',2,'POINT')}"></td>
<td th:text="${#numbers.formatDecimal(budget.expenditure,0,'COMMA',2,'POINT')}"></td>
</tr>
</tbody>
</table>
</main>
如此编写之后,从项目详情页选择查看预算,就可以进入到预算展示页面。
这里还要给Project
添加上对应的生成各种URL
的方法:
public String absoluteUrl() {
return "/pms/project/detail/" + id;
}
public String editUrl() {
return "/pms/project/edit/" + id;
}
public String budgetUrl() {
return "/pms/budget/" + id + "/list";
}
public String addBudgetUrl() {
return "/pms/budget/" + id + "/add";
}
计算利润与其他指标 - 使用BO
现在除了列出所有的预算之外,我们需要在页面里显示其他信息,比如合计的收入、成本,计算的利润和利润率等。这个时候我可不想在控制器中进行如此重型的计算,所以要使用一个业务类,把所有的结果都封装在其中,供调用。
先来创建一个BO
类,位于fms.dataobject.pms.businessobject.BudgetData
:
public class BudgetData {
private List<Budget> budgets;
private BigDecimal netProfit;
private BigDecimal totalNetIncome;
private BigDecimal totalNetCost;
private BigDecimal profitRate;
private BigDecimal totalExpenditure;
public List<Budget> getBudgets() {
return budgets;
}
......
}
这个BO
中包含了我们要计算的内容,包括总净收入、总净成本、总费用、利润和利润率。
然后我们要来修改一下获取预算列表的业务类方法,让其返回一个当前项目对应的BudgetData
对象,包含了所有预算的内容。在后边,还会继续添加其他计算内容:
@Transactional
public BudgetData getBudgetDataByProjectId(int id) {
List<Budget> budgets = budgetDao.findBudgetByProjectIdOrderByCreateTime(id);
BudgetData result = new BudgetData();
result.setBudgets(budgets);
BigDecimal totalNetIncome = new BigDecimal("0.00");
BigDecimal totalNetCost = new BigDecimal("0.00");
BigDecimal totalExpenditure = new BigDecimal("0.00");
for (Budget budget : budgets) {
if (!budget.isVirtual()) {
totalNetIncome = totalNetIncome.add(budget.getNetIncome());
totalNetCost = totalNetCost.add(budget.getNetCost());
totalExpenditure = totalExpenditure.add(budget.getExpenditure());
}
}
result.setTotalNetIncome(totalNetIncome);
result.setTotalNetCost(totalNetCost);
result.setTotalExpenditure(totalExpenditure);
result.setNetProfit(totalNetIncome.subtract(totalNetCost).subtract(totalExpenditure));
try {
result.setProfitRate(result.getNetProfit().divide(totalNetIncome,4, RoundingMode.HALF_UP).multiply(new BigDecimal("100.0000")));
} catch (ArithmeticException e) {
result.setProfitRate(new BigDecimal("0.00"));
}
return result;
}
这个业务类方法组装了BudgetData
对象,仅仅计算不属于虚拟预算条目的预算,还判断了万一发生除以零的错误,就写入一个零,之后再将利润率乘以100
得到最终的利润率。
然后在控制器的部分,我们就直接使用这个方法返回的BudgetData
对象:
@GetMapping("/{pid}/list")
public String budgetList(@PathVariable("pid") int id, Model model) {
Project project = projectService.findById(id);
BudgetData budgetData = budgetService.getBudgetDataByProjectId(id);
model.addAttribute("project", project);
model.addAttribute("budgetData", budgetData);
model.addAttribute("count", budgetData.getBudgets().size());
return "pms/budget/budgetList";
}
之后的页面也需要修改一下,由于开始要渲染数字,这里还要使用到Thymeleaf
的数值格式化功能:
<table class="table table-hover table-responsive" th:if="${count != 0}">
<caption class="text-primary h5">净利润:<span class="text-danger"
th:text="${#numbers.formatDecimal(budgetData.getNetProfit(),0,'COMMA',2,'POINT')}"></span><br>净利润率:
<span class="text-danger" th:text="${#numbers.formatDecimal(budgetData.getProfitRate(),0,'COMMA',2,'POINT') + '%'}"></span>
</caption>
<thead>
<tr>
<th>序号</th>
<th>预算名称</th>
<th>收入</th>
<th>成本</th>
<th>费用</th>
</tr>
</thead>
<tbody>
<tr th:each="budget, control:${budgetData.getBudgets()}">
<td th:text="${control.count}"></td>
<td><a>[[${budget.budgetName}]]</a></td>
<td th:text="${#numbers.formatDecimal(budget.netIncome,0,'COMMA',2,'POINT')}"></td>
<td th:text="${#numbers.formatDecimal(budget.netCost,0,'COMMA',2,'POINT')}"></td>
<td th:text="${#numbers.formatDecimal(budget.expenditure,0,'COMMA',2,'POINT')}"></td>
</tr>
<tr>
<td class="text-center text-primary" colspan="2"><strong>合计</strong></td>
<td class="text-primary"
th:text="${#numbers.formatDecimal(budgetData.getTotalNetIncome(),0,'COMMA',2,'POINT')}"></td>
<td class="text-primary"
th:text="${#numbers.formatDecimal(budgetData.getTotalNetCost(),0,'COMMA',2,'POINT')}"></td>
<td class="text-primary"
th:text="${#numbers.formatDecimal(budgetData.getTotalExpenditure(),0,'COMMA',2,'POINT')}"></td>
</tr>
</tbody>
</table>
这里大量使用了#numbers
l来格式化千分位和两位小数的输出。
可见一个简单的预算其实也不简单。在后边,我们要针对每个预算汇集对应的合同以及明细记录,所以之后还需要扩充BudgetData
对象的内容。同时可以看到,我们的控制器就是获取数据并渲染,重型计算都在业务类中,这也做到了有效分离。
我们这里默认列出了所有条目,不过还没有区分虚拟和非虚拟条目,这都留给以后进行改进。实际上,我们现在也可以把项目详情页的内容做起来了,很显然也不能仅仅获取项目,而是要把项目对应的所有东西都获取到再进行渲染。